[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM golang:1.24\n\nRUN apt-get update && apt-get install -y sudo\nRUN curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && \\\n\tapt-get install -y nodejs\n\nADD scripts /scripts\nRUN bash /scripts/install.sh\nRUN bash /scripts/godeps.sh\n\nENV ENCORE_GOROOT=/encore-release/encore-go\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"build\": {\"dockerfile\": \"Dockerfile\"},\n  \"containerEnv\": {\n    \"ENCORE_DAEMON_DEV\": \"1\",\n    \"ENCORE_RUNTIMES_PATH\": \"${containerWorkspaceFolder}/runtimes\"\n  },\n  \"extensions\": [\"golang.go\"],\n  \"postCreateCommand\": \"bash /scripts/prepare.sh\",\n  \"forwardPorts\": [4000, 9400]\n}\n"
  },
  {
    "path": ".devcontainer/scripts/godeps.sh",
    "content": "#!/usr/bin/env\nset -ex\n\ngo install github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest \ngo install github.com/ramya-rao-a/go-outline@latest\ngo install github.com/cweill/gotests/gotests@latest\ngo install github.com/fatih/gomodifytags@latest\ngo install github.com/josharian/impl@latest\ngo install github.com/haya14busa/goplay/cmd/goplay@latest\ngo install github.com/go-delve/delve/cmd/dlv@latest\ngo install honnef.co/go/tools/cmd/staticcheck@master\ngo install golang.org/x/tools/gopls@latest\n\nGOBIN=/tmp/ go install github.com/go-delve/delve/cmd/dlv@master\nmv /tmp/dlv $GOPATH/bin/dlv-dap\n"
  },
  {
    "path": ".devcontainer/scripts/install.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\ntarget=\"$(go env GOOS)_$(go env GOARCH)\"\n\nencore_uri=$(curl -sSf -N \"https://encore.dev/api/releases?target=${target}&show=url\")\nif [ ! \"$encore_uri\" ]; then\n    echo \"Error: Unable to determine latest Encore release.\" 1>&2\n    exit 1\nfi\n\nencore_install=\"/encore-release\"\nbin_dir=\"$encore_install/bin\"\nexe=\"$bin_dir/encore\"\ntar=\"$encore_install/encore.tar.gz\"\n\nif [ ! -d \"$bin_dir\" ]; then\n \tmkdir -p \"$bin_dir\"\nfi\n\ncurl --fail --location --progress-bar --output \"$tar\" \"$encore_uri\"\ncd \"$encore_install\"\ntar -C \"$encore_install\" -xzf \"$tar\"\nchmod +x \"$bin_dir\"/*\nrm \"$tar\"\n\n\"$exe\" version\n\necho \"Encore was installed successfully to $exe\"\nif command -v encore >/dev/null; then\n\techo \"Run 'encore --help' to get started\"\nelse\n\tcase $SHELL in\n\t/bin/zsh) shell_profile=\".zshrc\" ;;\n\t*) shell_profile=\".bash_profile\" ;;\n\tesac\n\techo \"Manually add the directory to your \\$HOME/$shell_profile (or similar)\"\n\techo \"  export ENCORE_INSTALL=\\\"$encore_install\\\"\"\n\techo \"  export PATH=\\\"\\$ENCORE_INSTALL/bin:\\$PATH\\\"\"\n\techo \"Run '$exe --help' to get started\"\nfi"
  },
  {
    "path": ".devcontainer/scripts/prepare.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\nset -x\n\ngo mod download\n"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/help.yml",
    "content": "body:\n  - type: markdown\n    attributes:\n      value: |\n        Before asking a question, please check our [documentation](https://encore.dev/docs) to see if your question is already answered there.\n\n        If you are not sure if your issue is a bug you can ask a question on our [Discord community](https://encore.dev/discord).\n\n        **NOTE:** You don't need to answer questions that you know that aren't relevant.\n\n        ---\n\n  - type: checkboxes\n    attributes:\n      label: \"Is there an existing issue/discussion for this?\"\n      description: \"Please search in Issues and Discussions to see if this question has already been asked\"\n      options:\n      - label: \"I have searched the existing issues and discussions\"\n        required: true\n\n  - type: input\n    attributes:\n      label: \"Encore CLI version\"\n      description: |\n        Which exact version of `encore` CLI are you using? Run `encore version` in your terminal to see your version.\n      placeholder: \"1.54.0\"\n\n  - type: input\n    attributes:\n      label: \"Node.js version\"\n      description: \"Which version of Node.js are you using?\"\n      placeholder: \"24.0.0\"\n\n  - type: checkboxes\n    validations:\n      required: true\n    attributes:\n      label: \"In which operating systems have you tested?\"\n      options:\n        - label: macOS\n        - label: Windows\n        - label: Linux\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n\n  - type: textarea\n    attributes:\n      label: \"Question\"\n      description: |\n        What is your question?\n        **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in\n"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/suggestions.yml",
    "content": "body:\n  - type: markdown\n    attributes:\n      value: |\n        Check out our [documentation](https://encore.dev/docs) to see if your suggestion is already implemented.\n\n        If you are not sure if your suggestion is a feature request you can ask a question on our [Discord community](https://encore.dev/discord).\n\n        ---\n\n  - type: checkboxes\n    attributes:\n      label: \"Is there an existing discussion that is already proposing this?\"\n      description: \"Please search [here](https://github.com/encoredev/encore/discussions) to see if a discussion already exists for the feature you are requesting\"\n      options:\n      - label: \"I have searched the existing discussions\"\n        required: true\n\n  - type: checkboxes\n    validations:\n      required: true\n    attributes:\n      label: \"What part(s) of Encore does this feature request apply to?\"\n      options:\n        - label: Encore.ts (TypeScript)\n        - label: Encore.go (Go)\n        - label: Encore CLI\n        - label: Local Development Dashboard\n        - label: Encore Cloud\n        - label: Other\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: \"Is your feature request related to a problem? Please describe it\"\n      description: \"A clear and concise description of what the problem is\"\n      placeholder: |\n        I have an issue when ...\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: \"Describe the solution you'd like\"\n      description: \"A clear and concise description of what you want to happen. Add any considered drawbacks\"\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: \"What is the motivation / use case for changing the behavior?\"\n      description: \"Describe the motivation or the concrete use case\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug_report.yml",
    "content": "name: \"\\U0001F41B Bug Report\"\ndescription: \"If something isn't working as expected\"\nlabels: [\"type: bug\"]\ntype: bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### We use GitHub Issues to track bug reports\n\n        For suggestions and feature requests, please add those to our [GitHub discussions](https://github.com/encoredev/encore/discussions) forum.\n\n        If you are not sure if your issue is a bug you can ask a question on our [Discord community](https://encore.dev/discord).\n\n        **NOTE:** You don't need to answer questions that you know that aren't relevant.\n\n        ---\n\n  - type: checkboxes\n    attributes:\n      label: \"Is there an existing issue for this?\"\n      description: \"Please search [here](../issues?q=is%3Aissue) to see if an issue already exists for the bug you encountered\"\n      options:\n      - label: \"I have searched the existing issues\"\n        required: true\n  \n  - type: checkboxes\n    id: area\n    attributes:\n      label: \"What part(s) of Encore does this bug report apply to?\"\n      options:\n        - label: Encore.ts (TypeScript)\n        - label: Encore.go (Go)\n        - label: Encore CLI\n        - label: Local Development Dashboard\n        - label: Encore Cloud\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: \"Current behavior\"\n      description: \"How the issue manifests?\"\n\n  - type: input\n    attributes:\n      label: \"Minimum reproduction code\"\n      placeholder: \"https://github.com/...\"\n      description: |\n        URL to a Git repository that reproduces your issue. [What is a minimum reproduction?](https://github.com/encoredev/encore/blob/main/.github/minimum-reproduction.md)\n\n  - type: textarea\n    attributes:\n      label: \"Steps to reproduce\"\n      description: |\n        How the issue manifests?\n        You could leave this blank if you already write this in your reproduction code\n      placeholder: |\n        1. `encore run`\n        2. `curl localhost:4000/path`\n        3. See error...\n\n  - type: textarea\n    validations:\n      required: true\n    attributes:\n      label: \"Expected behavior\"\n      description: \"A clear and concise description of what you expected to happened (or code)\"\n\n  - type: input\n    attributes:\n      label: \"Encore CLI version\"\n      description: |\n        Which exact version of `encore` CLI are you using? Run `encore version` in your terminal to see your version.\n      placeholder: \"1.54.0\"\n\n  - type: input\n    attributes:\n      label: \"Node.js version\"\n      description: \"Which version of Node.js are you using?\"\n      placeholder: \"24.0.0\"\n\n  - type: checkboxes\n    validations:\n      required: true\n    attributes:\n      label: \"In which operating systems have you tested?\"\n      options:\n        - label: macOS\n        - label: Windows\n        - label: Linux\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n\n  - type: textarea\n    attributes:\n      label: \"Other\"\n      description: |\n        Anything else relevant? eg: Logs, OS version, IDE, package manager, etc.\n        **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "## To encourage contributors to use issue templates, we don't allow blank issues\nblank_issues_enabled: false\n\ncontact_links:\n  - name: \"Suggestions and General Help\"\n    url: \"https://github.com/encoredev/encore/discussions\"\n    about: \"Please add suggestions or ask general help questions to our GitHub discussions forum.\"\n  - name: \"\\U0001F4D5 Documentation\"\n    url: \"https://encore.dev/docs\"\n    about: \"Read about every Encore feature in-depth and search for things you are unsure about.\"\n"
  },
  {
    "path": ".github/dockerimg/Dockerfile",
    "content": "# syntax=docker/dockerfile:1.4\nFROM --platform=$TARGETPLATFORM ubuntu:22.04 AS build\nARG TARGETPLATFORM\nARG RELEASE_VERSION\nRUN mkdir /encore\nADD rename-binary-if-needed.bash rename-binary-if-needed.bash\nADD artifacts /artifacts\nRUN /bin/bash -c 'SRC=encore-$(echo $TARGETPLATFORM | tr '/' '_'); tar -C /encore -xzf /artifacts/$SRC.tar.gz'\nRUN /bin/bash rename-binary-if-needed.bash\n\nFROM --platform=$TARGETPLATFORM ubuntu:22.04\nRUN apt-get update && apt-get install -y -f ca-certificates\nENV PATH=\"/encore/bin:${PATH}\"\nWORKDIR /src\nADD encore-entrypoint.bash /bin/encore-entrypoint.bash\nENTRYPOINT [\"/bin/encore-entrypoint.bash\"]\nCOPY --from=build /encore /encore\n"
  },
  {
    "path": ".github/dockerimg/encore-entrypoint.bash",
    "content": "#!/usr/bin/env bash\nset -eo pipefail\n\n# If the ENCORE_AUTHKEY environment variable is set, log in with it.\nif [ -n \"$ENCORE_AUTHKEY\" ]; then\n  echo \"Logging in to Encore using provided auth key...\"\n  encore auth login --auth-key \"$ENCORE_AUTHKEY\"\nfi\n\n# Run the encore command.\nencore \"$@\"\n"
  },
  {
    "path": ".github/dockerimg/rename-binary-if-needed.bash",
    "content": "#!/usr/bin/env bash\nset -eo pipefail\n\n# Check if `encore-nightly`, `encore-beta` or `encore-develop` are present, and if one of them are, rename it to `encore`.\nfor binary in encore-nightly encore-beta encore-develop; do\n  if [ -f \"/encore/bin/$binary\" ]; then\n    echo \"Renaming $binary to encore...\"\n    mv /encore/bin/$binary /encore/bin/encore\n  fi\ndone\n\n# Sanity check that /ecore/bin/encore exists.\nif [ ! -f \"/encore/bin/encore\" ]; then\n  echo \"ERROR: /encore/bin/encore does not exist. Did you mount the Encore binary directory to /encore/bin?\"\n  exit 1\nfi\n"
  },
  {
    "path": ".github/minimum-reproduction.md",
    "content": "# Minimum Reproduction Repository\n\nA minimum reproduction repository is a git repository that can be shared publicly (doesn't expose private business logic), shows the problem you're running into, and has the fewest dependencies installed possible. It also has the steps in place for how to replicate the error you're running into. This is easiest to add to the README.\n\n## Doesn't Expose Business Logic\n\nIf your error resolves around a specific step in business logic, replicate the business logic in a way that doesn't make it evident what you're working on.\n\n## Shows The Problem You're Running Into\n\nThis is why the minimum reproduction should be created in the first place, cause you have an error you want someone to look into.\n\n## Has The Fewest Dependencies Installed Possible\n\nIf the reproduction doesn't need it, get rid of it.\n\n## Steps To Replicate\n\nA set of clear, defined steps on how to replicate the error. You can separate the setup and reproduction steps as well if you'd like. An example would be something like\n\n```\n# Setup\n\n1) npm install\n\n# Reproduction\n\n1) encore run\n2) open new terminal\n3) curl http://localhost:4000/users\n4) see the error\n```\n\n## Okay I Understand What It Is, What Else Do I Need?\n\nGenerally speaking, if you meet the above, it's good to go. This helps out those who debug errors and provide support immensely.\n\n## So why am I being asked for this?\n\nThere's a few reasons to provide a minimum reproduction:\n\n1. it makes debugging where the error _could_ be so much easier. Instead of looking across 20 files and 5 directories, it's now 2 files in 1 directory. Much less to dig through and understand\n2. half the time while creating the minimum reproduction, you'll find what the problem was yourself and grow as a developer and as a knowledge sharer."
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n  schedule:\n    - cron: \"30 2 * * *\" # Every night at 2:30am UTC (if you change this schedule, also change the if statement in the test steps)\n\njobs:\n  build:\n    name: \"Build\"\n    runs-on: ubuntu-24.04\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          path: encr.dev\n\n      - name: Set up Node\n        uses: actions/setup-node@v3\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version-file: \"encr.dev/go.mod\"\n          check-latest: true\n          cache-dependency-path: \"encr.dev/go.sum\"\n\n      - name: Build\n        run: cd encr.dev && go build ./...\n\n      - name: Build for Windows\n        run: cd encr.dev && go build ./...\n        env:\n          GOOS: windows\n\n  test:\n    name: \"Test\"\n    runs-on: ubuntu-24.04\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          path: encr.dev\n\n      - name: Set up Node\n        uses: actions/setup-node@v3\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version-file: \"encr.dev/go.mod\"\n          check-latest: true\n          cache-dependency-path: \"encr.dev/go.sum\"\n\n      - name: Set up Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n      - name: Install Protoc\n        uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2\n      - name: Set up cargo cache\n        uses: actions/cache@v3\n        continue-on-error: false\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            target/\n          key: ${{ runner.os }}-cargo\n\n      - name: Install encore-go\n        run: |\n          URL=$(curl -s https://api.github.com/repos/encoredev/go/releases/latest | grep \"browser_download_url.*linux_x86-64.tar.gz\" | cut -d : -f 2,3 | tr -d \\\" | tr -d '[:space:]')\n          curl --fail -L -o encore-go.tar.gz $URL && tar -C . -xzf ./encore-go.tar.gz\n\n      - name: Install tsparser\n        run: cargo install --path encr.dev/tsparser --force --debug\n\n      # If we're not running on a schedule, we only want to run tests on changed code\n      - name: Run tests on changed code on the CLI\n        run: cd encr.dev && go test -short -tags=dev_build 2>&1 ./...\n        if: github.event.schedule != '30 2 * * *'\n        env:\n          ENCORE_GOROOT: ${{ github.workspace }}/encore-go\n          ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes\n\n      - name: Run tests on changed runtime code\n        run: cd encr.dev/runtimes/go && go test -short -tags=dev_build ./...\n        if: github.event.schedule != '30 2 * * *'\n\n      # Each night we want to run all tests multiple times to catch any flaky tests\n      # We will shuffle the order in which tests are run and run them 25 times looking\n      # for failures. We will also fail fast so that we don't waste time running tests\n      # that are already failing.\n      - name: Run all tests multiple times on the CLI\n        run: cd encr.dev && go test -v --count=5 -failfast -shuffle=on -timeout=30m -tags=dev_build ./...\n        if: github.event.schedule == '30 2 * * *'\n        env:\n          ENCORE_GOROOT: ${{ github.workspace }}/encore-go\n          ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes\n\n      - name: Run all tests multiple times on the runtime\n        run: cd encr.dev/runtimes/go && go test -v --count=5 -failfast -shuffle=on -timeout=30m -tags=dev_build ./...\n        if: github.event.schedule == '30 2 * * *'\n\n      - name: Report Nightly Failure\n        uses: ravsamhq/notify-slack-action@bca2d7f5660b833a27bda4f6b8bef389ebfefd25\n        if: ${{ failure() && github.event.schedule == '30 2 * * *' }}\n        with:\n          status: ${{ job.status }} # required\n          notification_title: \"{workflow} has {status_message}\"\n          message_format: \"{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>\"\n          footer: \"Linked Repo <{repo_url}|{repo}> | <{workflow_url}|View Workflow>\"\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ALERT_WEBHOOK_URL }} # required\n\n  test-e2e:\n    name: \"Test e2e\"\n    runs-on: ubuntu-24.04\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          path: encr.dev\n\n      - name: Set up Node\n        uses: actions/setup-node@v3\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version-file: \"encr.dev/go.mod\"\n          check-latest: true\n          cache-dependency-path: \"encr.dev/go.sum\"\n\n      - name: Set up Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n      - name: Install Protoc\n        uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2\n      - name: Set up cargo cache\n        uses: actions/cache@v3\n        continue-on-error: false\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            target/\n          key: ${{ runner.os }}-cargo\n\n      - name: Install encore-go\n        run: |\n          URL=$(curl -s https://api.github.com/repos/encoredev/go/releases/latest | grep \"browser_download_url.*linux_x86-64.tar.gz\" | cut -d : -f 2,3 | tr -d \\\" | tr -d '[:space:]')\n          curl --fail -L -o encore-go.tar.gz $URL && tar -C . -xzf ./encore-go.tar.gz\n\n      - name: Install tsparser\n        run: cargo install --path encr.dev/tsparser --force --debug\n\n      - name: Install tsbundler\n        run: cd encr.dev && go install ./cli/cmd/tsbundler-encore\n\n      - name: Build jsruntime\n        run: cd encr.dev && go run ./pkg/encorebuild/cmd/build-local-binary encore-runtime.node\n\n      # If we're not running on a schedule, we only want to run tests on changed code\n      - name: Run tests on changed code on the CLI\n        run: cd encr.dev && go test -short -tags=e2e 2>&1 ./e2e-tests\n        if: github.event.schedule != '30 2 * * *'\n        env:\n          ENCORE_GOROOT: ${{ github.workspace }}/encore-go\n          ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes\n\n      # Each night we want to run all tests multiple times to catch any flaky tests\n      # We will shuffle the order in which tests are run and run them 25 times looking\n      # for failures. We will also fail fast so that we don't waste time running tests\n      # that are already failing.\n      - name: Run all tests multiple times on the CLI\n        run: cd encr.dev && go test -v --count=5 -failfast -shuffle=on -timeout=30m -tags=e2e ./e2e-tests\n        if: github.event.schedule == '30 2 * * *'\n        env:\n          ENCORE_GOROOT: ${{ github.workspace }}/encore-go\n          ENCORE_RUNTIMES_PATH: ${{ github.workspace }}/encr.dev/runtimes\n\n      - name: Report Nightly Failure\n        uses: ravsamhq/notify-slack-action@bca2d7f5660b833a27bda4f6b8bef389ebfefd25\n        if: ${{ failure() && github.event.schedule == '30 2 * * *' }}\n        with:\n          status: ${{ job.status }} # required\n          notification_title: \"{workflow} has {status_message}\"\n          message_format: \"{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>\"\n          footer: \"Linked Repo <{repo_url}|{repo}> | <{workflow_url}|View Workflow>\"\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ALERT_WEBHOOK_URL }} # required\n\n  # Run static analysis on the PR\n  static-analysis:\n    name: \"Static Analysis\"\n    # We're using buildjet for this as it's very slow on Github's own runners\n    runs-on: buildjet-4vcpu-ubuntu-2204\n\n    # Skip any PR created by dependabot to avoid permission issues:\n    if: (github.actor != 'dependabot[bot]')\n\n    permissions:\n      checks: write\n      contents: read\n      pull-requests: write\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install jq\n        uses: dcarbone/install-jq-action@91d8da7268538e8a0ae0c8b72af44f1763228455\n\n      - name: Install semgrep\n        run: |\n          python3 -m pip install semgrep\n          python3 -m pip install --upgrade requests\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version-file: \"go.mod\"\n          cache: false\n\n      - name: Install ci tools\n        run: |\n          go install honnef.co/go/tools/cmd/staticcheck@master\n          go install github.com/kisielk/errcheck@latest\n          go install github.com/gordonklaus/ineffassign@latest\n\n  rust_core:\n    name: \"Test core runtime\"\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout codebase\n        uses: actions/checkout@v4\n      - name: Set up Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          components: rustfmt,clippy\n      - name: Install Protoc\n        uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2\n      - name: Set up cargo cache\n        uses: actions/cache@v3\n        continue-on-error: false\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            target/\n          key: ${{ runner.os }}-cargo\n      - uses: taiki-e/install-action@nextest\n      - name: Run test\n        run: cargo nextest run\n        env:\n          CARGO_TERM_COLOR: always\n      - name: Run rustfmt\n        run: cargo fmt --all --check\n      - name: Run clippy\n        run: cargo clippy --all-targets --all-features -- -D warnings\n\n  wasm_build:\n    name: \"Build tsparser WASM\"\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout codebase\n        uses: actions/checkout@v4\n      - name: Set up Rust\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          targets: wasm32-unknown-unknown\n          components: clippy\n      - name: Install Protoc\n        uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6 # v2\n      - name: Set up cargo cache\n        uses: actions/cache@v3\n        continue-on-error: false\n        with:\n          path: |\n            ~/.cargo/bin/\n            ~/.cargo/registry/index/\n            ~/.cargo/registry/cache/\n            ~/.cargo/git/db/\n            target/\n          key: ${{ runner.os }}-cargo-wasm\n      - name: Build\n        run: cargo build --target wasm32-unknown-unknown -p tsparser-wasm\n      - name: Run clippy for WASM\n        run: cargo clippy --target wasm32-unknown-unknown -p tsparser-wasm -- -D warnings\n"
  },
  {
    "path": ".github/workflows/makefile",
    "content": "# This makefile is used inconjunction with the .reviewdog.yml file in the root of the repo\n.PHONY: list-modules go-vet staticcheck errcheck ineffassign go-fmt\n\n# Automatically gather all information\nALL_SRC :=  $(shell find ../../ -name \"*.go\")\nALL_MODS = $(shell find ../../ -name go.mod)\nMOD_DIRS = $(sort $(realpath $(dir $(ALL_MODS))))\nREPO_DIR := $(realpath ../../)\nSEMGREP_DIR := \"$(REPO_DIR)/tools/semgrep-rules\"\n\n# List modules reports all found Go modules within the repository\nlist-modules:\n\t@echo $(MOD_DIRS)\n\n# Function to run a command in each Go module with appropriate build tags\n#\n# REL_DIR is the relative path to the file from the repository root\n#         it is computed by removing the REPO_DIR prefix from the $dir variable,\n#\t\t  then we remove the prefix \"/\" to make it relative\n#         and finally escaping the slashes so we can use it in sed\ndefine run_for_each_module\n\t@for dir in $(MOD_DIRS); do \\\n\t\tTAGS=\"\"; \\\n\t\tif [ \"$$dir\" != \"$(REPO_DIR)\" ]; then \\\n\t\t\tTAGS=\"-tags encore,encore_internal,encore_app\"; \\\n\t\tfi; \\\n\t\tREL_DIR=$$(echo \"$${dir#$(REPO_DIR)}/\" | sed 's/^\\///' | sed 's/\\//\\\\\\//g'); \\\n\t\t(cd \"$$dir\" && $(1) $$TAGS $(2) | sed \"s/^\\.\\//$$REL_DIR/\"); \\\n\tdone;\nendef\n\n# Run Go vet\ngo-vet: $(ALL_SRC)\n\t# The sed statements are:\n\t#\n\t# 1. Remove any lines starting with \"#\" (go vet uses these for each package)\n\t# 2. Remove any \"vet: \" prefix from the output (sometimes we get this sometimes we dont)\n\t# 3. Remove any \"./\" prefix from the output (we'll get this for files which exist directly in the module root folder - this is done so we don't double up next)\n\t# 4. Add a \"./\" prefix to the output (this is so the sed within the run_for_each_module function can add the module path to each line)\n\t$(call run_for_each_module,go vet,./... 2>&1  | sed '/^#/d' | sed 's/^vet: //' | sed 's/^\\.\\///' | sed \"s/^/\\.\\//\")\n\n## Run staticcheck\nstaticcheck: $(ALL_SRC)\n\t$(call run_for_each_module,staticcheck -tests=false -f=json,./... | jq -f \"$(REPO_DIR)/.github/workflows/staticcheck-to-rdjsonl.jq\" -c)\n\n# Run errcheck\nerrcheck: $(ALL_SRC)\n\t$(call run_for_each_module,errcheck -abspath,./...)\n\n\n## Run ineffassign\nineffassign: $(ALL_SRC)\n\t$(call run_for_each_module,ineffassign,./... 2>&1)\n\nsemgrep: $(ALL_SRC)\n\t@cd $(REPO_DIR) && semgrep scan --quiet --config=auto --config=$(SEMGREP_DIR) --json | jq -f \"$(REPO_DIR)/.github/workflows/semgrep-to-rdjson.jq\" -c\n\ngo-fmt: $(ALL_SRC)\n\t@cd $(REPO_DIR) && gofmt -s -d . || exit 0\n"
  },
  {
    "path": ".github/workflows/release-2.yml",
    "content": "name: Release (2.0)\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to build (\"v1.2.3\", \"v1.2.3-nightly.20231231\", \"v1.2.3-beta.1\" or \"v0.0.0-develop+[commitHash]\")'\n        type: string\n        required: true\n\njobs:\n  release:\n    name: \"Run Release Script\"\n    runs-on: self-hosted\n    env:\n      GOROOT: /usr/local/go-1.21.4\n      RUSTUP_HOME: /usr/local/rust/rustup\n\n    steps:\n      - name: Checkout the repo\n        uses: actions/checkout@v4\n        with:\n          path: encr.dev\n\n      - name: Trigger release script\n        env:\n          NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          cd ${{ github.workspace }}/encr.dev\n          go run ./pkg/encorebuild/cmd/make-release/ -dst \"${{ github.workspace\t}}/build\" -v \"${{ github.event.inputs.version }}\" -publish-npm=true\n\n      - name: Publish artifact (darwin_amd64)\n        uses: actions/upload-artifact@v3\n        with:\n          name: encore-${{ github.event.inputs.version }}-darwin_amd64\n          path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-darwin_amd64.tar.gz\n\n      - name: Publish artifact (darwin_arm64)\n        uses: actions/upload-artifact@v3\n        with:\n          name: encore-${{ github.event.inputs.version }}-darwin_arm64\n          path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-darwin_arm64.tar.gz\n\n      - name: Publish artifact (linux_amd64)\n        uses: actions/upload-artifact@v3\n        with:\n          name: encore-${{ github.event.inputs.version }}-linux_amd64\n          path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_amd64.tar.gz\n\n      - name: Publish artifact (linux_arm64)\n        uses: actions/upload-artifact@v3\n        with:\n          name: encore-${{ github.event.inputs.version }}-linux_arm64\n          path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_arm64.tar.gz\n\n      - name: Publish artifact (windows_amd64)\n        uses: actions/upload-artifact@v3\n        with:\n          name: encore-${{ github.event.inputs.version }}-windows_amd64\n          path: ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-windows_amd64.tar.gz\n\n      - name: Setup Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Registry\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Copy linux artifacts to docker context folder\n        run: |\n          mkdir -p ${{ github.workspace }}/encr.dev/.github/dockerimg/artifacts\n          cp ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_amd64.tar.gz ${{ github.workspace }}/encr.dev/.github/dockerimg/artifacts/encore-linux_amd64.tar.gz\n          cp ${{ github.workspace }}/build/artifacts/encore-${{ github.event.inputs.version }}-linux_arm64.tar.gz ${{ github.workspace }}/encr.dev/.github/dockerimg/artifacts/encore-linux_arm64.tar.gz\n\n      - name: Create metadata (tags, labels) for Docker image\n        id: docker-meta\n        uses: docker/metadata-action@v5\n        with:\n          images: encoredotdev/encore\n          labels: |\n            org.opencontainers.image.title=Encore\n            org.opencontainers.image.vendor=encore.dev\n            org.opencontainers.image.authors=support@encore.dev\n            org.opencontainers.image.description=Encore is the end-to-end Backend Development Platform that lets you escape cloud complexity.\n          tags: |\n            type=raw,value=latest,enable=${{ !contains(github.event.inputs.version, '-') }}\n            type=semver,pattern={{version}},value=${{ github.event.inputs.version }}\n            type=sha\n            type=schedule,pattern=nightly,enable=${{ contains(github.event.inputs.version, '-nightly.') }}\n            type=semver,pattern={{major}}.{{minor}},value=${{ github.event.inputs.version }},enable=${{ !contains(github.event.inputs.version, '-') }}\n            type=semver,pattern={{major}},value=${{ github.event.inputs.version }},enable=${{ !contains(github.event.inputs.version, '-') }}\n\n      - name: Build and push docker images\n        uses: docker/build-push-action@v4\n        with:\n          context: encr.dev/.github/dockerimg\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.docker-meta.outputs.tags }}\n          labels: ${{ steps.docker-meta.outputs.labels }}\n          cache-from: type=inline\n          cache-to: type=inline\n          build-args: |\n            RELEASE_VERSION=${{ github.event.inputs.version }}\n\n  notify_release_success:\n    name: \"Notify release system of successful build\"\n    runs-on: self-hosted\n    needs:\n      - release\n    steps:\n      - name: Webhook\n        uses: distributhor/workflow-webhook@f5a294e144d6ef44cfac4d3d5e20b613bcee0d4b # v3.0.7\n        env:\n          webhook_type: \"json\"\n          webhook_url: ${{ secrets.RELEASE_WEBHOOK }}\n          data: '{ \"version\": \"${{ github.event.inputs.version }}\", \"run_id\": \"${{ github.run_id }}\" }'\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to build (\"1.2.3\")'\n        required: true\n      encorego_version:\n        description: 'Encore-Go version to use (\"encore-go1.17.7\")'\n        required: true\n\njobs:\n  build:\n    strategy:\n      matrix:\n        include:\n          - builder: ubuntu-24.04\n            goos: linux\n            goarch: amd64\n            release_key: linux_x86-64\n          - builder: ubuntu-24.04\n            goos: linux\n            goarch: arm64\n            release_key: linux_arm64\n          - builder: macos-11\n            goos: darwin\n            goarch: amd64\n            release_key: macos_x86-64\n          - builder: macos-11\n            goos: darwin\n            goarch: arm64\n            release_key: macos_arm64\n          - builder: windows-latest\n            goos: windows\n            goarch: amd64\n            release_key: windows_x86-64\n\n    runs-on: ${{ matrix.builder }}\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@v4\n        with:\n          path: encr.dev\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version-file: \"encr.dev/go.mod\"\n          check-latest: true\n          cache-dependency-path: \"encr.dev/go.sum\"\n\n      - name: Set up Zig\n        uses: goto-bus-stop/setup-zig@7ab2955eb728f5440978d5824358023be3a2802d # v2.2.0\n        with:\n          version: 0.10.1\n\n      - name: Install encore-go\n        run: curl --fail -o encore-go.tar.gz -L https://github.com/encoredev/go/releases/download/${{ github.event.inputs.encorego_version }}/${{ matrix.release_key }}.tar.gz && tar -C ${{ github.workspace }} -xzf ./encore-go.tar.gz\n\n      - name: Build\n        run: cd encr.dev && go run ./pkg/make-release/make-release.go -v=\"${{ github.event.inputs.version }}\" -dst=dist -goos=${{ matrix.goos }} -goarch=${{ matrix.goarch }} -encore-go=\"../encore-go\"\n        env:\n          GO111MODULE: \"on\"\n        if: runner.os != 'windows'\n\n      - name: Build\n        run: cd encr.dev && .\\pkg\\make-release\\windows\\build.bat\n        env:\n          GO111MODULE: \"on\"\n          ENCORE_VERSION: \"${{ github.event.inputs.version }}\"\n          ENCORE_GOROOT: \"../encore-go\"\n        if: runner.os == 'windows'\n\n      - name: \"Tar artifacts\"\n        run: tar -czvf encore-${{ github.event.inputs.version }}-${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz -C encr.dev/dist/${{ matrix.goos }}_${{ matrix.goarch }} .\n      - name: Publish artifact\n        uses: actions/upload-artifact@v3\n        with:\n          name: encore-${{ github.event.inputs.version }}-${{ matrix.goos }}_${{ matrix.goarch }}\n          path: encore-${{ github.event.inputs.version }}-${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz\n\n  publish-docker-images:\n    name: \"publish docker images\"\n    runs-on: ubuntu-24.04\n    needs: build\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          sparse-checkout: .github\n      - name: Download Artifacts\n        uses: actions/download-artifact@v3\n        with:\n          path: .github/dockerimg/artifacts\n      - name: Setup Docker Buildx\n        uses: docker/setup-buildx-action@v1\n\n      - name: Login to Docker Registry\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Cache Docker layers\n        uses: actions/cache@v2\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v4\n        with:\n          images: encoredotdev/encore\n          labels: |\n            org.opencontainers.image.title=Encore\n            org.opencontainers.image.vendor=encore.dev\n            org.opencontainers.image.authors=support@encore.dev\n            org.opencontainers.image.description=Encore is the end-to-end Backend Development Platform that lets you escape cloud complexity.\n          tags: |\n            type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}\n            type=semver,pattern={{version}},value=v${{ github.event.inputs.version }}\n            type=semver,pattern={{major}}.{{minor}},value=v${{ github.event.inputs.version }}\n            type=semver,pattern={{major}},value=v${{ github.event.inputs.version }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v4\n        with:\n          context: .github/dockerimg\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n          build-args: |\n            RELEASE_VERSION=${{ github.event.inputs.version }}\n\n  notify_release_success:\n    needs:\n      - build\n      - publish-docker-images\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Webhook\n        uses: distributhor/workflow-webhook@v3.0.7\n        env:\n          webhook_type: \"json\"\n          webhook_url: ${{ secrets.RELEASE_WEBHOOK }}\n          data: '{ \"version\": \"${{ github.event.inputs.version }}\", \"run_id\": \"${{ github.run_id }}\" }'\n"
  },
  {
    "path": ".github/workflows/semgrep-to-rdjson.jq",
    "content": "# See https://github.com/reviewdog/reviewdog/tree/master/proto/rdf\n{\n    source: {\n        name: \"semgrep\",\n        url: \"https://semgrep.dev/\",\n    },\n    diagnostics: [\n        .results[] | {\n            code: {\n                value: .check_id,\n                url: [\n                        .extra.metadata.shortlink?,\n                        .extra.metadata.source?,\n                        .extra.\"semgrep.dev\".rule.url?,\n                        \"https://github.com/encoredev/encore/blob/main/\\(.check_id | gsub(\"\\\\.\"; \"/\")).yml\"\n                    ] | map(select(. != null)) | first,\n            },\n            message: .extra.message,\n            location: {\n                path: .path,\n                range: {\n                    start: {\n                        line: .start.line,\n                        column: .start.col\n                    },\n                    end: {\n                        line: .end.line,\n                        column: .end.col\n                    },\n                },\n            },\n            severity: .extra.severity,\n\n            # Temporary variable we store to track the fix\n            _res: .\n        } |\n            if ._res.extra.fix then .suggestions = [{\n                range: .location.range,\n                text: ._res.extra.fix,\n            }] else . end |\n            del(._res)\n    ]\n}\n"
  },
  {
    "path": ".github/workflows/staticcheck-to-rdjsonl.jq",
    "content": "# See https://github.com/reviewdog/reviewdog/tree/master/proto/rdf\n{\n  source: {\n    name: \"staticcheck\",\n    url: \"https://staticcheck.io\"\n  },\n  message: .message,\n  code: {value: .code, url: \"https://staticcheck.io/docs/checks#\\(.code)\"},\n  location: {\n    path: .location.file,\n    range: {\n      start: {\n        line: .location.line,\n        column: .location.column\n      }\n    }\n  },\n  severity: ((.severity|ascii_upcase|select(match(\"ERROR|WARNING|INFO\")))//null)\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Prevent built binaries from being checked in accidentally.\n/dist\n/encore\n/git-remote-encore\n/target\n/__debug_*\n\n# Don't commit dotfiles\n/.encore\n/.vscode\n/.zed\n\n# Build artifact that must be placed alongside go files for Windows\n*.syso\n\n# JetBrains\n.idea\n.fleet\n.run\n\n# MacOS\n.DS_Store\n\nruntimes/supervisor-encore\n\nruntimes/supervisor-encore-linux-amd64\n\nencore-runtime.node-linux-amd64\n"
  },
  {
    "path": ".prettierrc.toml",
    "content": "trailingComma = \"none\"\n"
  },
  {
    "path": ".reviewdog.yml",
    "content": "# Encore's reviewdog configuration file.\n#\n# This runs in our CI pipeline when you open a PR. To run this locally\n# and get the same results as our CI pipeline, run: `./check.bash`\n#\n# We use a makefile rather than the commands directly as this repo\n# has multiple Go modules within it and most tools only look at the\n# module in the current directory. Thus our make file runs the tool\n# for each module, combining the results into a single standardised\n# that review dog can then parse and display as a single \"run\" for\n# each tool.\nrunner:\n  go-vet:\n    cmd: make -s -C .github/workflows go-vet\n    format: govet\n  go-fmt:\n    cmd: make -s -C .github/workflows go-fmt\n    format: diff\n# Disable staticcheck until it supports Go 1.21: https://github.com/dominikh/go-tools/issues/1431\n#  staticcheck:\n#    cmd: make -s -C .github/workflows staticcheck\n#    format: rdjsonl\n  errcheck:\n    cmd: make -s -C .github/workflows errcheck\n    errorformat:\n      - \"%f:%l:%c:\\t%m\"\n  ineffassign:\n    cmd: make -s -C .github/workflows ineffassign\n    errorformat:\n      - \"%f:%l:%c: %m\"\n  semgrep:\n    cmd: make -s -C .github/workflows semgrep\n    format: rdjson\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation\nin our community a harassment-free experience for everyone, regardless\nof age, body size, visible or invisible disability, ethnicity, sex\ncharacteristics, gender identity and expression, level of experience,\neducation, socio-economic status, nationality, personal appearance,\nrace, religion, or sexual identity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open,\nwelcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for\nour community include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our\n  mistakes, and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or\n  political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in\n  a professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our\nstandards of acceptable behavior and will take appropriate and fair\ncorrective action in response to any behavior that they deem\ninappropriate, threatening, offensive, or harmful.\n\nCommunity leaders have the right and responsibility to remove, edit,\nor reject comments, commits, code, wiki edits, issues, and other\ncontributions that are not aligned to this Code of Conduct, and will\ncommunicate reasons for moderation decisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also\napplies when an individual is officially representing the community in\npublic spaces. Examples of representing our community include using an\nofficial e-mail address, posting via an official social media account,\nor acting as an appointed representative at an online or offline\nevent.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior\nmay be reported to the community leaders responsible for enforcement\nat [info@encore.dev](mailto:info@encore.dev). All complaints\nwill be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and\nsecurity of the reporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in\ndetermining the consequences for any action they deem in violation of\nthis Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior\ndeemed unprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders,\nproviding clarity around the nature of the violation and an\nexplanation of why the behavior was inappropriate. A public apology\nmay be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued\nbehavior. No interaction with the people involved, including\nunsolicited interaction with those enforcing the Code of Conduct, for\na specified period of time. This includes avoiding interactions in\ncommunity spaces as well as external channels like social\nmedia. Violating these terms may lead to a temporary or permanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards,\nincluding sustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or\npublic communication with the community for a specified period of\ntime. No public or private interaction with the people involved,\nincluding unsolicited interaction with those enforcing the Code of\nConduct, is allowed during this period. Violating these terms may lead\nto a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of\ncommunity standards, including sustained inappropriate behavior,\nharassment of an individual, or aggression toward or disparagement of\nclasses of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction\nwithin the community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor\nCovenant][homepage], version 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of\nconduct enforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the\nFAQ at https://www.contributor-covenant.org/faq. Translations are\navailable at https://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Encore\n\nWe're so excited that you are interested in contributing to Encore!\nAll contributions are welcome, and there are several valuable ways to contribute.\n\nBelow is a technical walkthrough of developing the `encore` command for contributing code\nto the Encore project. Head over to the community section for [more ways to contribute](https://encore.dev/docs/community/contribute)!\n\n## GitHub Codespaces / VS Code Remote Containers\nThe easiest way to get started with developing Encore is using\nGitHub Codespaces. Simply open this repository in a new Codespace\nand your development environment will be set up with everything preconfigured for building the `encore` CLI and running applications with it.\n\nThis also works just as well with [Visual Studio Code's Remote Development](https://code.visualstudio.com/docs/remote/remote-overview).\n\n\n## Building the encore command from source\nTo build from the source simply run `go build ./cli/cmd/encore` and `go install ./cli/cmd/git-remote-encore`.\n\nRunning an Encore application requires both the Encore runtime (the `encore.dev` package) as well as a custom-built\n[Go runtime](https://github.com/encoredev/go) to implement Encore's request semantics and automated instrumentation.\n\nAs a result, the Encore Daemon must know where these two things exist on the filesystem to compile the Encore application properly.\n\nThis must be done in one of two ways: embedding the installation path at compile time (similar to `GOROOT`)\nor by setting an environment variable at runtime.\n\nThe environment variables are:\n- `ENCORE_RUNTIMES_PATH` – the path to the `encore.dev` runtime implementation.\n- `ENCORE_GOROOT` – the path to encore-go on disk\n\n**ENCORE_RUNTIMES_PATH**\n\nThis must be set to the location of the `encore.dev` runtime package.\nIt's located in this Git repository in the `runtimes` directory:\n\n```bash\nexport ENCORE_RUNTIMES_PATH=/path/to/encore/runtimes\n```\n\n**ENCORE_GOROOT**\n\nThe `ENCORE_GOROOT` must be set to the path to the [Encore Go runtime](https://github.com/encoredev/go).\nUnless you want to make changes to the Go runtime it's easiest to point this to an existing Encore installation.\n\nTo do that, run `encore daemon env` and grab the value of `ENCORE_GOROOT`. For example (yours is probably different):\n\n```bash\nexport ENCORE_GOROOT=/opt/homebrew/Cellar/encore/0.16.2/libexec/encore-go\n```\n\n### Running applications when building from source\nOnce you've built your own `encore` binary and set the environment variables above, you're ready to go!\n\nStart the daemon with the built binary: `./encore daemon -f`\n\nNote that when you run commands like `encore run` must use the same `encore` binary the daemon is running.\n\n\n### Testing the Daemon run logic\nThe codegen tests in the `internal/clientgen/client_test.go` file uses many auto generated files from the\n`e2e-tests/testdata` directory. To generate the client files and other test files, run `go test -golden-update` from\nthe `e2e-tests` directory. This will generate client files for all the supported client generation languages.\n\nRunning `go test ./internal/clientgen` will now work and use the most recent client generated files. If\nyou change the client or content of the `testdata` folder, you may need to regenerate the client files again.\n\n## Architecture\n\nThe code base is divided into several parts:\n\n### cli\nThe `encore` command line interface. The encore background daemon\nis located at `cli/daemon` and is responsible for managing processes,\nsetting up databases and talking with the Encore servers for operations like\nfetching production logs.\n\n### parser\nThe Encore Parser statically analyzes Encore apps to build up a model\nof the application dubbed the Encore Syntax Tree (EST) that lives in\n`parser/est`.\n\nFor speed the parser does not perform traditional type-checking; it does\nlimited type-checking for enforcing Encore-specific rules but otherwise\nrelies on the underlying Go compiler to perform type-checking as part of\nbuilding the application.\n\n### compiler\nThe Encore Compiler rewrites the source code based on the parsed\nEncore Syntax Tree to create a fully functioning application.\nIt rewrites API calls & API handlers, injects instrumentation\nand secret values, and more.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\n    \"runtimes/core\",\n    \"runtimes/js\",\n    \"tsparser\",\n    \"tsparser/wasm\",\n    \"supervisor\",\n    \"miniredis\",\n]\n\n[profile.dev.package]\ninsta.opt-level = 3\n\n[profile.release]\nlto = true\n\n[patch.crates-io]\ntokio-postgres = { git = \"https://github.com/encoredev/rust-postgres\", branch = \"encore-patches-sync\" }\npostgres-protocol = { git = \"https://github.com/encoredev/rust-postgres\", branch = \"encore-patches-sync\" }\npostgres-types = { git = \"https://github.com/encoredev/rust-postgres\", branch = \"encore-patches-sync\" }\nswc_ecma_parser = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\nswc_ecma_ast = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\nswc_ecma_transforms_base = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\nswc_atoms = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\nswc_common = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\nswc_ecma_loader = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\nswc_ecma_visit = { git = \"https://github.com/encoredev/swc\", branch = \"node-resolve-exports\" }\n"
  },
  {
    "path": "Cross.toml",
    "content": "[build]\npre-build = [\n    \"apt-get install unzip &&\",\n    \"curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip &&\",\n    \"unzip protoc-24.4-linux-x86_64.zip -d /usr/local &&\",\n    \"rm protoc-24.4-linux-x86_64.zip &&\",\n    \"export PATH=$PATH:/usr/local/bin\",\n]\n\n[build.env]\nvolumes = [\"ENCORE_WORKDIR\"]\npassthrough = [\"TYPE_DEF_TMP_PATH\", \"ENCORE_VERSION\"]"
  },
  {
    "path": "LICENSE",
    "content": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n     means each individual or legal entity that creates, contributes to the\n     creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n     means the combination of the Contributions of others (if any) used by a\n     Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n     means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n     means Source Code Form to which the initial Contributor has attached the\n     notice in Exhibit A, the Executable Form of such Source Code Form, and\n     Modifications of such Source Code Form, in each case including portions\n     thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n     means\n\n     a. that the initial Contributor has attached the notice described in\n        Exhibit B to the Covered Software; or\n\n     b. that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the terms of\n        a Secondary License.\n\n1.6. \"Executable Form\"\n\n     means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n     means a work that combines Covered Software with other material, in a\n     separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n     means this document.\n\n1.9. \"Licensable\"\n\n     means having the right to grant, to the maximum extent possible, whether\n     at the time of the initial grant or subsequently, any and all of the\n     rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n     means any of the following:\n\n     a. any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered Software; or\n\n     b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n      means any patent claim(s), including without limitation, method,\n      process, and apparatus claims, in any patent Licensable by such\n      Contributor that would be infringed, but for the grant of the License,\n      by the making, using, selling, offering for sale, having made, import,\n      or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n      means either the GNU General Public License, Version 2.0, the GNU Lesser\n      General Public License, Version 2.1, the GNU Affero General Public\n      License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n      means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n      means an individual or a legal entity exercising rights under this\n      License. For legal entities, \"You\" includes any entity that controls, is\n      controlled by, or is under common control with You. For purposes of this\n      definition, \"control\" means (a) the power, direct or indirect, to cause\n      the direction or management of such entity, whether by contract or\n      otherwise, or (b) ownership of more than fifty percent (50%) of the\n      outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n     Each Contributor hereby grants You a world-wide, royalty-free,\n     non-exclusive license:\n\n     a. under intellectual property rights (other than patent or trademark)\n        Licensable by such Contributor to use, reproduce, make available,\n        modify, display, perform, distribute, and otherwise exploit its\n        Contributions, either on an unmodified basis, with Modifications, or\n        as part of a Larger Work; and\n\n     b. under Patent Claims of such Contributor to make, use, sell, offer for\n        sale, have made, import, and otherwise transfer either its\n        Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n     The licenses granted in Section 2.1 with respect to any Contribution\n     become effective for each Contribution on the date the Contributor first\n     distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n     The licenses granted in this Section 2 are the only rights granted under\n     this License. No additional rights or licenses will be implied from the\n     distribution or licensing of Covered Software under this License.\n     Notwithstanding Section 2.1(b) above, no patent license is granted by a\n     Contributor:\n\n     a. for any code that a Contributor has removed from Covered Software; or\n\n     b. for infringements caused by: (i) Your and any other third party's\n        modifications of Covered Software, or (ii) the combination of its\n        Contributions with other software (except as part of its Contributor\n        Version); or\n\n     c. under Patent Claims infringed by Covered Software in the absence of\n        its Contributions.\n\n     This License does not grant any rights in the trademarks, service marks,\n     or logos of any Contributor (except as may be necessary to comply with\n     the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n     No Contributor makes additional grants as a result of Your choice to\n     distribute the Covered Software under a subsequent version of this\n     License (see Section 10.2) or under the terms of a Secondary License (if\n     permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n     Each Contributor represents that the Contributor believes its\n     Contributions are its original creation(s) or it has sufficient rights to\n     grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n     This License is not intended to limit any rights You have under\n     applicable copyright doctrines of fair use, fair dealing, or other\n     equivalents.\n\n2.7. Conditions\n\n     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n     Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n     All distribution of Covered Software in Source Code Form, including any\n     Modifications that You create or to which You contribute, must be under\n     the terms of this License. You must inform recipients that the Source\n     Code Form of the Covered Software is governed by the terms of this\n     License, and how they can obtain a copy of this License. You may not\n     attempt to alter or restrict the recipients' rights in the Source Code\n     Form.\n\n3.2. Distribution of Executable Form\n\n     If You distribute Covered Software in Executable Form then:\n\n     a. such Covered Software must also be made available in Source Code Form,\n        as described in Section 3.1, and You must inform recipients of the\n        Executable Form how they can obtain a copy of such Source Code Form by\n        reasonable means in a timely manner, at a charge no more than the cost\n        of distribution to the recipient; and\n\n     b. You may distribute such Executable Form under the terms of this\n        License, or sublicense it under different terms, provided that the\n        license for the Executable Form does not attempt to limit or alter the\n        recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n     You may create and distribute a Larger Work under terms of Your choice,\n     provided that You also comply with the requirements of this License for\n     the Covered Software. If the Larger Work is a combination of Covered\n     Software with a work governed by one or more Secondary Licenses, and the\n     Covered Software is not Incompatible With Secondary Licenses, this\n     License permits You to additionally distribute such Covered Software\n     under the terms of such Secondary License(s), so that the recipient of\n     the Larger Work may, at their option, further distribute the Covered\n     Software under the terms of either this License or such Secondary\n     License(s).\n\n3.4. Notices\n\n     You may not remove or alter the substance of any license notices\n     (including copyright notices, patent notices, disclaimers of warranty, or\n     limitations of liability) contained within the Source Code Form of the\n     Covered Software, except that You may alter any license notices to the\n     extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n     You may choose to offer, and to charge a fee for, warranty, support,\n     indemnity or liability obligations to one or more recipients of Covered\n     Software. However, You may do so only on Your own behalf, and not on\n     behalf of any Contributor. You must make it absolutely clear that any\n     such warranty, support, indemnity, or liability obligation is offered by\n     You alone, and You hereby agree to indemnify every Contributor for any\n     liability incurred by such Contributor as a result of warranty, support,\n     indemnity or liability terms You offer. You may include additional\n     disclaimers of warranty and limitations of liability specific to any\n     jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n   If it is impossible for You to comply with any of the terms of this License\n   with respect to some or all of the Covered Software due to statute,\n   judicial order, or regulation then You must: (a) comply with the terms of\n   this License to the maximum extent possible; and (b) describe the\n   limitations and the code they affect. Such description must be placed in a\n   text file included with all distributions of the Covered Software under\n   this License. Except to the extent prohibited by statute or regulation,\n   such description must be sufficiently detailed for a recipient of ordinary\n   skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n     fail to comply with any of its terms. However, if You become compliant,\n     then the rights granted under this License from a particular Contributor\n     are reinstated (a) provisionally, unless and until such Contributor\n     explicitly and finally terminates Your grants, and (b) on an ongoing\n     basis, if such Contributor fails to notify You of the non-compliance by\n     some reasonable means prior to 60 days after You have come back into\n     compliance. Moreover, Your grants from a particular Contributor are\n     reinstated on an ongoing basis if such Contributor notifies You of the\n     non-compliance by some reasonable means, this is the first time You have\n     received notice of non-compliance with this License from such\n     Contributor, and You become compliant prior to 30 days after Your receipt\n     of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n     infringement claim (excluding declaratory judgment actions,\n     counter-claims, and cross-claims) alleging that a Contributor Version\n     directly or indirectly infringes any patent, then the rights granted to\n     You by any and all Contributors for the Covered Software under Section\n     2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n     license agreements (excluding distributors and resellers) which have been\n     validly granted by You or Your distributors under this License prior to\n     termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n   Covered Software is provided under this License on an \"as is\" basis,\n   without warranty of any kind, either expressed, implied, or statutory,\n   including, without limitation, warranties that the Covered Software is free\n   of defects, merchantable, fit for a particular purpose or non-infringing.\n   The entire risk as to the quality and performance of the Covered Software\n   is with You. Should any Covered Software prove defective in any respect,\n   You (not any Contributor) assume the cost of any necessary servicing,\n   repair, or correction. This disclaimer of warranty constitutes an essential\n   part of this License. No use of  any Covered Software is authorized under\n   this License except under this disclaimer.\n\n7. Limitation of Liability\n\n   Under no circumstances and under no legal theory, whether tort (including\n   negligence), contract, or otherwise, shall any Contributor, or anyone who\n   distributes Covered Software as permitted above, be liable to You for any\n   direct, indirect, special, incidental, or consequential damages of any\n   character including, without limitation, damages for lost profits, loss of\n   goodwill, work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses, even if such party shall have been\n   informed of the possibility of such damages. This limitation of liability\n   shall not apply to liability for death or personal injury resulting from\n   such party's negligence to the extent applicable law prohibits such\n   limitation. Some jurisdictions do not allow the exclusion or limitation of\n   incidental or consequential damages, so this exclusion and limitation may\n   not apply to You.\n\n8. Litigation\n\n   Any litigation relating to this License may be brought only in the courts\n   of a jurisdiction where the defendant maintains its principal place of\n   business and such litigation shall be governed by laws of that\n   jurisdiction, without reference to its conflict-of-law provisions. Nothing\n   in this Section shall prevent a party's ability to bring cross-claims or\n   counter-claims.\n\n9. Miscellaneous\n\n   This License represents the complete agreement concerning the subject\n   matter hereof. If any provision of this License is held to be\n   unenforceable, such provision shall be reformed only to the extent\n   necessary to make it enforceable. Any law or regulation which provides that\n   the language of a contract shall be construed against the drafter shall not\n   be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n      Mozilla Foundation is the license steward. Except as provided in Section\n      10.3, no one other than the license steward has the right to modify or\n      publish new versions of this License. Each version will be given a\n      distinguishing version number.\n\n10.2. Effect of New Versions\n\n      You may distribute the Covered Software under the terms of the version\n      of the License under which You originally received the Covered Software,\n      or under the terms of any subsequent version published by the license\n      steward.\n\n10.3. Modified Versions\n\n      If you create software not governed by this License, and you want to\n      create a new license for such software, you may create and use a\n      modified version of this License if you rename the license and remove\n      any references to the name of the license steward (except to note that\n      such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n      Licenses If You choose to distribute Source Code Form that is\n      Incompatible With Secondary Licenses under the terms of this version of\n      the License, the notice described in Exhibit B of this License must be\n      attached.\n\nExhibit A - Source Code Form License Notice\n\n      This Source Code Form is subject to the\n      terms of the Mozilla Public License, v.\n      2.0. If a copy of the MPL was not\n      distributed with this file, You can\n      obtain one at\n      http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n      This Source Code Form is \"Incompatible\n      With Secondary Licenses\", as defined by\n      the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\" dir=\"auto\">\n<a href=\"https://encore.dev\"><img src=\"https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg\" width=\"160px\" alt=\"encore icon\"></img></a><br/><br/><br/>\n<b>Open Source Framework for creating type-safe distributed systems with declarative infrastructure</b><br/>\n</p>\n\n- **Framework:** The Encore framework, available for TypeScript and Go, lets you define APIs, services, and infrastructure (databases, Pub/Sub, caching, buckets, cron jobs) as type-safe objects in your code. Write your application once, then deploy it anywhere without code changes by [exporting a Docker image](https://encore.dev/docs/ts/self-host/build) and supplying the infra configuration.\n\n- **Local Dev Tools:** The Encore CLI runs your app locally and automatically provisions local infrastructure. Encore's local dev dashboard provides tools for a productive workflow: Tracing, API Explorer, Service Catalog, Architecture Diagrams, and Database Explorer.\n\n- **DevOps Platform (Optional):** [Encore Cloud](https://encore.cloud) parses your application and automatically provisions the required infrastructure in your own AWS/GCP account. Other tools include Preview Environments for each PR, Service Catalog, Distributed Tracing, Metrics, and Cost Analytics.\n\n**⭐ Star this repository** to help spread the word and stay up to date.\n\n### Get started\n\n**Install Encore:**\n\n- **macOS:** `brew install encoredev/tap/encore`\n- **Linux:** `curl -L https://encore.dev/install.sh | bash`\n- **Windows:** `iwr https://encore.dev/install.ps1 | iex`\n\n**Create your first app:**\n\n- **TypeScript:** `encore app create --example=ts/hello-world`\n- **Go:** `encore app create --example=hello-world`\n\n**Use with AI coding assistants:**\n\nAdd Encore's [LLM instructions](https://encore.dev/docs/ts/ai-integration) to your project, so your AI tools can understand your architecture, generate type-safe code, and use Encore's infrastructure primitives.\nUse the built-in [MCP server](https://encore.dev/docs/ts/ai-integration) to give your AI runtime context (query databases, call APIs, analyze traces) for seamless debugging and faster iterations. [Learn more](https://encore.dev/docs/ts/ai-integration)\n\nhttps://github.com/user-attachments/assets/461b902f-8fd3-46f1-a73c-0ebbfa789ce3\n\n_Encore's local development dashboard_\n\n## How it works\n\nEncore's open source backend frameworks, [Encore.ts](https://encore.dev/docs/ts) and [Encore.go](https://encore.dev/docs/primitives/overview), enable you to define resources like services, databases, Pub/Sub, caches, buckets, and cron jobs, as type-safe objects in your application code.\n\nYou only define **infrastructure semantics** (_what matters for the behavior of the application_), not configuration for specific cloud services. Here's how you define a database in Encore.ts:\n\n```typescript\nconst db = new SQLDatabase(\"users\", { migrations: \"./migrations\" });\n```\n\nEncore parses your application to understand your infrastructure requirements, then sets up infrastructure in different environments:\n\n- **Locally:** The Encore CLI sets up local infrastructure (microservices, Postgres, Pub/Sub, etc.) and provides a development dashboard with distributed tracing, API documentation, service catalog, architecture diagrams, and database explorer. Works offline, no Docker Compose needed.\n\n- **AWS/GCP:** Encore Cloud deploys to your AWS/GCP account without any Terraform or YAML needed. It automatically sets up compute instances (serverless or Kubernetes), databases (RDS/Cloud SQL), Pub/Sub (SQS/GCP Pub/Sub), storage (S3/GCS), caching (ElastiCache/Memorystore), and all other required resources like security groups and IAM policies, according to best practices.\n\n- **Self-hosted:** Use the Encore CLI to export your app as Docker images, then supply your infra config to host anywhere.\n\n<img width=\"578\" alt=\"Encore overview\" src=\"https://github.com/user-attachments/assets/89fb83cf-2400-4b54-969d-6f7a5911e76c\" />\n\n_Encore orchestrates infrastructure from local development and testing, to production in your cloud._\n\n#### Encore makes it simpler to build distributed systems\n\n- **Microservices without boilerplate:** Call APIs in other services like regular functions. Encore handles service discovery, networking, and serialization. Get cross-service type-safety and auto-complete in your IDE.\n\n- **Modular monolith to microservices:** Structure your application using independent services for clarity. Then deploy them colocated in a single process or as distributed microservices, without changing a single line of code. Encore handles service communication whether in-process or over the network.\n\n- **Testing built-in:** Mock API calls, get dedicated test infrastructure, and use distributed tracing for tests.\n\n### Example: Hello World\n\nDefining microservices and API endpoints is very simple. With less than 10 lines of code, you can create a production-ready, deployable service.\n\n**Hello World in Encore.ts**\n\n```typescript\nimport { api } from \"encore.dev/api\";\n\nexport const get = api(\n  { expose: true, method: \"GET\", path: \"/hello/:name\" },\n  async ({ name }: { name: string }): Promise<Response> => {\n    const msg = `Hello ${name}!`;\n    return { message: msg };\n  }\n);\n\ninterface Response {\n  message: string;\n}\n```\n\n**Hello World in Encore.go**\n\n```go\npackage hello\n\n//encore:api public path=/hello/:name\nfunc World(ctx context.Context, name string) (*Response, error) {\n\tmsg := fmt.Sprintf(\"Hello, %s!\", name)\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n```\n\n### Example: Using Pub/Sub\n\nIf you want a Pub/Sub Topic, you declare it directly in your application code and Encore will integrate the infrastructure and generate the boilerplate code necessary.\nEncore orchestrates the relevant Pub/Sub infrastructure for different environments:\n\n- **NSQ** for local environments\n- **GCP Pub/Sub** for environments on GCP\n- **SNS/SQS** for environments on AWS\n\n**Using Pub/Sub in Encore.ts**\n\n```typescript\nimport { Topic } \"encore.dev/pubsub\"\n\nexport interface SignupEvent {\n    userID: string;\n}\n\nexport const signups = new Topic<SignupEvent>(\"signups\", {\n    deliveryGuarantee: \"at-least-once\",\n});\n```\n\n**Using Pub/Sub in Encore.go**\n\n```go\nimport \"encore.dev/pubsub\"\n\ntype User struct { /* fields... */ }\n\nvar Signup = pubsub.NewTopic[*User](\"signup\", pubsub.TopicConfig{\n  DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\n// Publish messages by calling a method\nSignup.Publish(ctx, &User{...})\n```\n\n### Need some infrastructure Encore doesn't provide?\n\nEncore never prevents you from using arbitrary infrastructure.\n\nYou can use any external resource as you normally would, directly integrating standard SDKs (AWS SDK, GCP client libraries, third-party APIs, etc.). You then provision that resource yourself as you normally would.\n\n### Want to use Encore in an existing system?\n\nYou don't need a complete rewrite, Encore supports incremental adoption.\n\n**Service-by-service adoption (recommended):** Build new services with Encore and run them alongside your existing system, integrated via APIs. Then incrementally migrate existing services as needed. Each migrated service immediately gets Encore's full feature set: infrastructure provisioning, tracing, architecture diagrams.\n\n#### Deployment options:\n\n- **Your Kubernetes cluster:** Deploy directly to your existing Kubernetes infrastructure. Run Encore alongside legacy systems in the same environment.\n- **Encore-managed infrastructure:** Encore provisions and manages infrastructure in your AWS/GCP account, deployed within your existing VPC and security setup.\n- **Terraform provider:** Encore provides a [Terraform provider](https://encore.dev/docs/platform/integrations/terraform) to make it simple to integrate Encore managed infrastructure with your existing infrastructure landscape.\n\nStart with low-risk, frequently-changed services to validate the approach. Learn more in our [migration guide](https://encore.dev/docs/platform/migration/migrate-to-encore).\n\n## Learn more\n\n- **Documentation:** [Encore Docs](https://encore.dev/docs)\n\n- **See example apps:** [Example Apps Repo](https://github.com/encoredev/examples/)\n\n- **See products built with Encore:** [Showcase](https://encore.cloud/showcase)\n\n- **Hear from teams using Encore:** [Case studies](https://encore.cloud/customers)\n\n- **Have questions?** Join the friendly developer community on [Discord](https://encore.dev/discord)\n\n- **Talk to a human:** [Book a 1:1 demo](https://encore.dev/book) with one of our founders\n\n- **Videos:**\n  - <a href=\"https://youtu.be/vvqTGfoXVsw\" alt=\"Intro video: Encore concepts & features\" target=\"_blank\">Intro: Encore concepts & features</a>\n  - <a href=\"https://youtu.be/wiLDz-JUuqY\" alt=\"Demo video: Getting started with Encore.ts\" target=\"_blank\">Demo video: Getting started with Encore.ts</a>\n  - <a href=\"https://youtu.be/IwplIbwJtD0\" alt=\"Demo video: Building and deploying a simple service\" target=\"_blank\">Demo: Building and deploying a simple Go service</a>\n  - <a href=\"https://youtu.be/ipj1HdG4dWA\" alt=\"Demo video: Building an event-driven system\" target=\"_blank\">Demo: Building an event-driven system in Go</a>\n  - Find more videos in our [YouTube channel](youtube.com/channel/UCvqeAqMPotfuA6SPXa4VhNQ/).\n\n### How Encore compares to other tools\n\n| Tool                               | What it does                                                  | How Encore differs                                                                                                                                                                                                                                                           |\n| ---------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Pulumi / CDK / Terraform / SST** | Infrastructure-as-Code tools for provisioning cloud resources | Other solutions define configuration for specific infra services, coupling your application to one set of infrastructure. Encore uses semantic infrastructure (services, databases, Pub/Sub), write once, deploy anywhere (AWS/GCP/local), colocated or distributed services |\n| **Serverless Framework / Chalice** | Frameworks for deploying serverless functions to AWS          | Encore supports any architecture (monolith, microservices, serverless) and any cloud (AWS, GCP, self-hosted)                                                                                                                                                                 |\n| **NestJS / Express / Fiber**       | Web frameworks for building APIs and services                 | Encore provides the same capabilities plus infrastructure primitives, local dev tooling, observability, and optional cloud deployment                                                                                                                                        |\n| **Convex / Supabase / Firebase**   | Managed backend-as-a-service platforms                        | Encore gives you similar productivity but supports microservices and event-driven systems, and deploys to your own cloud account with no vendor lock-in                                                                                                                      |\n| **Vercel / Netlify / Railway**     | Deployment platforms (primarily frontend/full-stack)          | Encore is backend-specialized with deeper primitives (Pub/Sub, cron, caching) and multi-cloud infrastructure automation                                                                                                                                                      |\n\n### Why teams use Encore\n\n- **Faster Development**: Encore streamlines the development process by providing guardrails, clear abstractions, and removing manual infrastructure tasks from development iterations.\n- **Scalability & Performance**: Encore simplifies building large-scale microservices applications that can handle growing user bases and demands, without the normal boilerplate and complexity.\n- **Control & Standardization**: Built-in tools like automated architecture diagrams, infrastructure tracking and approval workflows, make it easy for teams and leaders to get an overview of the entire application.\n- **Security & Compliance**: Encore Cloud helps ensure your application is secure and compliant by enforcing security standards like least privilege IAM, and provisioning infrastructure according to best practices for each cloud provider.\n- **Reduced Costs**: Encore Cloud's automatic infrastructure management removes common cloud expenses like overprovisioned test environments, and reduces DevOps workload.\n\n## Open Source\n\nEverything needed to develop and deploy Encore applications is Open Source, including the backend frameworks, parser, compiler, runtime, and CLI.\n\nThis includes all code needed for local development, everything that runs in your application when it is deployed, and everything needed to generate a Docker image for your application, so you can easily deploy your application anywhere. [Learn more in the docs](https://encore.dev/docs/ts/self-host/build).\n\n## Join our growing developer community\n\nDevelopers building with Encore are part of fast-moving teams that want to focus on creative programming and building great software to solve meaningful problems. It's a friendly place, great for exchanging ideas and learning new things!\n\n**Join the community on [Discord](https://encore.dev/discord).**\n\nWe rely on your contributions and feedback to improve Encore for everyone who is using it.\nHere's how you can contribute:\n\n- ⭐ **Star and watch this repository to help spread the word and stay up to date.**\n- Meet fellow Encore developers and chat on [Discord](https://encore.dev/discord).\n- Follow Encore on [Twitter](https://twitter.com/encoredotdev).\n- Share feedback or ask questions via [email](mailto:hello@encore.dev).\n- Leave feedback on the [Public Roadmap](https://encore.dev/roadmap).\n- Send a pull request here on GitHub with your contribution.\n\n## Frequently Asked Questions (FAQ)\n\n### Who's behind Encore?\n\nEncore was founded by long-time engineers from Spotify and Google. We've lived through the challenges of building complex distributed systems with thousands of services, and scaling to hundreds of millions of users.\n\nEncore grew out of these experiences and is a solution to the frustrations that came with them: unnecessary infrastructure complexity and tedious repetitive work that suffocates developers' productivity and creativity.\n\n### Who is Encore for?\n\n**Individual developers:** Build cloud applications without managing infrastructure configuration. Go from idea to deployed application in minutes instead of days.\n\n**Startup teams:** Get a production-ready backend on AWS/GCP without dedicated DevOps engineers. Focus your time on your product instead of reinventing infrastructure patterns and building platform tooling.\n\n**Large organizations:** Standardize backend development across teams. Reduce onboarding time and operational overhead. Spin up new services in minutes with consistent patterns, without needing days of back and forth between development and DevOps teams.\n\n### Does defining infrastructure in code couple my app to infrastructure?\n\nNo. Encore keeps your application code cloud-agnostic by letting you refer only to **logical resources** (like \"a Postgres database\" or \"a Pub/Sub topic\"). A backend-agnostic interface means your code has no cloud-specific imports or configurations.\n\nEncore's compiler and runtime handle the mapping of logical resources to actual infrastructure, which is configured per environment. Your code stays identical whether the environment uses e.g.:\n\n- **AWS RDS** or **GCP Cloud SQL** for databases\n- **SQS/SNS** or **GCP Pub/Sub** for messaging\n- **AWS Fargate**, **Cloud Run**, or **Kubernetes** for compute\n\nThis **reduces coupling** compared to traditional Infrastructure-as-Code or cloud SDKs, which embed cloud-specific decisions directly in your codebase. With Encore, swapping cloud providers or infrastructure services requires no code changes.\n\n### What kind of support does Encore offer?\n\nEncore is fully open source and maintained by a dedicated full-time team. Support options include:\n\n- **Community Support:** Free support via [Discord](https://encore.dev/discord)\n- **Documentation:** Comprehensive guides and API references at [encore.dev/docs](https://encore.dev/docs)\n- **Paid Support:** For teams requiring guaranteed response times or dedicated support, [contact us](mailto:hello@encore.dev) about support plans\n\n### What if I want to migrate away from Encore?\n\nEncore is designed to let you go outside of the framework when you want to, and easily drop down in abstraction level when you need to, so you never run into any dead-ends.\n\nShould you want to migrate away, it's straightforward and does not require a big rewrite. 99% of your code is regular Go or TypeScript.\n\nEncore provides tools for [self-hosting](https://encore.dev/docs/ts/self-host/build) your application, by using the Open Source CLI to produce a standalone Docker image that can be deployed anywhere you'd like.\n\nLearn more in the [migration guide](https://encore.dev/docs/ts/migration/migrate-away)\n\n## Roadmap\n\nWe're actively expanding Encore's capabilities. Here's what's on the horizon:\n\n**Languages**\n\n- **Python**: Next on the roadmap for broader ecosystem support\n\n**Cloud Providers**\n\n- **Azure**: Planned to complement existing AWS and GCP support\n\n**Infrastructure Primitives**\n\n- Expanding storage, compute, and queue options\n\nSee the full [Public Roadmap](https://encore.dev/roadmap) and share your feedback on what you'd like to see next.\n\n## Contributing to Encore and building from source\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n"
  },
  {
    "path": "check.bash",
    "content": "#!/usr/bin/env bash\n#\n# This script will run the same checks as Encore's CI pipeline and report the same static analysis errors\n# as the pipeline by default. It can be used to check for what errors might be reported by the pipeline\n# before you commit and open a PR.\n#\n# Usage:\n#   ./check.bash [options]\n#\n# Options:\n#   --base <ref>         The merge base to compare against (default: origin/main)\n#   --diff               Show the diff against base instead of running the checks\n#   --filter-mode <mode> The filter mode to use for reviewdog; added, file, diff_context, nofilter (default: file)\n#   --all                Alias for `--filter-mode nofilter` (runs checks against all files in the working directory)\n#\n# Examples:\n#\n#   # Run the checks against files changed since branching from origin/main\n#   # (This is the default behavior and what our CI process does)\n#   ./check.bash\n#\n#   # Show the diff between the current working directory and origin/main\n#   ./check.bash --diff\n#\n#   # Run the checks against the entire working directory (regardless of changes made)\n#   ./check.bash --all\n\n\n##############################################################################################################################\n# Step 0: Setup the script with basic error handling                                                                         #\n##############################################################################################################################\n\n  set -euo pipefail\n  # nosemgrep\n  IFS=$'\\n\\t'\n\n  function errHandler() {\n    echo \"Exiting due to an error line $1\" >&2\n    echo \"\" >&2\n    awk 'NR>L-4 && NR<L+4 { printf \"%-5d%3s%s\\n\",NR,(NR==L?\">> \":\"\"),$0 }' L=\"$1\" \"$0\" >&2\n  }\n  trap 'errHandler $LINENO' ERR\n\n\n##############################################################################################################################\n# Step 1: Configure the script with the parameters the use wants                                                             #\n##############################################################################################################################\n\n  # Parameters\n  WORK_DIR=$( dirname \"${BASH_SOURCE[0]}\" ) # Get the directory this script is in\n  BASE_REF=\"origin/main\"                    # The merge base to compare against\n  DIFF_ONLY=\"false\"                         # If true, show the diff instead of running the checks\n  FILTER_MODE=\"file\"                        # The filter mode to use for reviewdog (added, file, diff_context, nofilter)\n\n  # Parse the command line arguments\n  while [[ $# -gt 0 ]]; do\n    case \"$1\" in\n      --base)\n        BASE_REF=\"$2\"\n        shift 2\n        ;;\n      --diff)\n        DIFF_ONLY=\"true\"\n        shift 1\n        ;;\n      --filter-mode)\n        FILTER_MODE=\"$2\"\n        shift 2\n        ;;\n      --all)\n        FILTER_MODE=\"nofilter\"\n        shift 1\n        ;;\n      *)\n        echo \"Unknown argument: $1\"\n        exit 1\n        ;;\n    esac\n  done\n\n\n##############################################################################################################################\n# Step 2: Check for required tools and error out if anything is missing which we can't install for the user                  #\n##############################################################################################################################\n\n  # Check for tools we can't install using go\n  command -v go >/dev/null 2>&1 || { echo >&2 \"go is required but not installed. Aborting.\"; exit 1; }\n  command -v git >/dev/null 2>&1 || { echo >&2 \"git is required but not installed. Aborting.\"; exit 1; }\n  command -v sed >/dev/null 2>&1 || { echo >&2 \"sed is required but not installed. Aborting.\"; exit 1; }\n  command -v semgrep >/dev/null 2>&1 || { echo >&2 \"semgrep is required but not installed. Aborting.\"; exit 1; }\n\n  # Now install all missing tools\n  command -v reviewdog >/dev/null 2>&1 || go install github.com/reviewdog/reviewdog/cmd/reviewdog@latest || { echo >&2 \"Unable to install reviewdog. Aborting.\"; exit 1; }\n  command -v staticcheck >/dev/null 2>&1 || go install honnef.co/go/tools/cmd/staticcheck@latest || { echo >&2 \"Unable to install staticcheck. Aborting.\"; exit 1; }\n  command -v errcheck >/dev/null 2>&1 || go install github.com/kisielk/errcheck@latest || { echo >&2 \"Unable to install errcheck. Aborting.\"; exit 1; }\n  command -v ineffassign >/dev/null 2>&1 || go install github.com/gordonklaus/ineffassign@latest || { echo >&2 \"Unable to install ineffassign. Aborting.\"; exit 1; }\n\n\n##############################################################################################################################\n# Step 3: Create a diff of the changes in the working directory against the common ancestor of the current branch and main   #\n#         This will be used to run static analysis checks on only the files that have changed. This diff should mimic the    #\n#         diff that would be created by GitHub when all current changes are committed and pushed into a PR on GitHub.        #\n##############################################################################################################################\n\n  # Don't generate the diff if we don't need it to filter!\n  if [[ \"$FILTER_MODE\" != \"nofilter\" ]]; then\n\n    # Create a temp directory to store the common ancestor commit\n    TMP_DIR=$(mktemp -d)\n    if [[ ! \"$TMP_DIR\" || ! -d \"$TMP_DIR\" ]]; then\n      echo \"Could not create temp dir\"\n      exit 1\n    fi\n\n    # Create a temp file to store the diff we need\n    DIFF_FILE=$(mktemp)\n    if [[ ! \"$DIFF_FILE\" || ! -f \"$DIFF_FILE\" ]]; then\n      echo \"Could not create temp diff file\"\n      exit 1\n    fi\n\n    # Create a blank file to use as a comparison when a file is missing because either it's new or been deleted\n    BLANK_FILE=$(mktemp)\n    if [[ ! \"$BLANK_FILE\" || ! -f \"$BLANK_FILE\" ]]; then\n      echo \"Could not create blank file\"\n      exit 1\n    fi\n\n    # Clean up on exit and delete all the temp files we just created\n    function cleanup() {\n      rm -rf \"$TMP_DIR\"\n      rm -f \"$DIFF_FILE\"\n      rm -f \"$BLANK_FILE\"\n    }\n    trap cleanup EXIT\n\n    # Clone the repo into the temp directory\n    git clone -q \"$WORK_DIR\" \"$TMP_DIR\"\n\n    # Change our temp directory to be a clean copy of the common ancestor commit\n    pushd \"$TMP_DIR\" > /dev/null\n    git reset -q --hard HEAD\n    git checkout -q \"$(git merge-base \"$BASE_REF\" HEAD)\"\n    TRACKED_FILES_FROM_MAIN=$(git ls-files)\n    popd > /dev/null\n\n    # Create a list of files that we care about\n    MODIFICATIONS_IN_WORKING_DIR=$(git status --short | awk '{print $2}')\n    TRACKED_FILES_IN_WORKING_DIR=$(git ls-files)\n    ALL_FILES=$(echo \"$TRACKED_FILES_IN_WORKING_DIR $MODIFICATIONS_IN_WORKING_DIR $TRACKED_FILES_FROM_MAIN\" | tr ' ' '\\n' | sort -u)\n\n    # Create a diff of the changes in the working directory against the common ancestor of the current branch and main\n    for file in $ALL_FILES; do\n      # If the original file doesn't exist, use a blank file instead\n      # (This means it was a new file that was added in the current version of the code base)\n      ORIGINAL_FILE=\"$TMP_DIR/$file\"\n      if [[ ! -f \"$ORIGINAL_FILE\" ]]; then\n        ORIGINAL_FILE=\"$BLANK_FILE\"\n      fi\n\n      # If the updated file doesn't exist, use a blank file instead\n      # (This means the file was deleted in the current version of the code base)\n      UPDATED_FILE=\"$WORK_DIR/$file\"\n      if [[ ! -f \"$UPDATED_FILE\" ]]; then\n        UPDATED_FILE=\"$BLANK_FILE\"\n      fi\n\n      # Run git diff between the original file and the updated file\n      # Replace the file paths in the diff to match the relative path in the working directory\n      # Then write the diff into our diff file\n      git diff \"$ORIGINAL_FILE\" \"$UPDATED_FILE\" | sed \"s|$ORIGINAL_FILE|/$file|g\" | sed \"s|$UPDATED_FILE|/$file|g\" >> \"$DIFF_FILE\" || true # Suppress the exit code\n    done\n\n    if [[ \"$DIFF_ONLY\" == \"true\" ]]; then\n      cat \"$DIFF_FILE\"\n      exit 0\n    fi\n  fi\n\n\n##############################################################################################################################\n# Step 4: Run review dog using the diff we just created, allowing reviewdog to only show errors from changes we've made      #\n##############################################################################################################################\n\n  if [[ \"$FILTER_MODE\" == \"nofilter\" ]]; then\n    reviewdog -filter-mode=nofilter\n  else\n    reviewdog -filter-mode=\"$FILTER_MODE\" -diff=\"cat $DIFF_FILE\"\n  fi\n"
  },
  {
    "path": "cli/cmd/encore/app/app.go",
    "content": "package app\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/root\"\n)\n\n// These can be overwritten using\n// `go build -ldflags \"-X encr.dev/cli/cmd/encore/app.defaultGitRemoteName=encore\"`.\nvar (\n\tdefaultGitRemoteName = \"encore\"\n\tdefaultGitRemoteURL  = \"encore://\"\n)\n\nvar appCmd = &cobra.Command{\n\tUse:   \"app\",\n\tShort: \"Commands to create and link Encore apps\",\n}\n\nfunc init() {\n\troot.Cmd.AddCommand(appCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/app/clone.go",
    "content": "package app\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n)\n\nvar cloneAppCmd = &cobra.Command{\n\tUse:   \"clone [app-id] [directory]\",\n\tShort: \"Clone an Encore app to your computer\",\n\tArgs:  cobra.MinimumNArgs(1),\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(c *cobra.Command, args []string) {\n\t\tcmdArgs := append([]string{\"clone\", \"--origin\", defaultGitRemoteName, defaultGitRemoteURL + args[0]}, args[1:]...)\n\t\tcmd := exec.Command(\"git\", cmdArgs...)\n\t\tcmd.Stdin = os.Stdin\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tos.Exit(1)\n\t\t}\n\t},\n\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tswitch len(args) {\n\t\tcase 0:\n\t\t\treturn cmdutil.AutoCompleteAppSlug(cmd, args, toComplete)\n\t\tcase 1:\n\t\t\treturn nil, cobra.ShellCompDirectiveFilterDirs\n\t\tdefault:\n\t\t\treturn nil, cobra.ShellCompDirectiveDefault\n\t\t}\n\t},\n}\n\nfunc init() {\n\tappCmd.AddCommand(cloneAppCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/app/create.go",
    "content": "package app\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/tailscale/hujson\"\n\t\"golang.org/x/term\"\n\n\t\"encr.dev/cli/cmd/encore/auth\"\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/llm_rules\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/cli/internal/telemetry\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/internal/userconfig\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/github\"\n\t\"encr.dev/pkg/xos\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar (\n\tcreateAppTemplate   string\n\tcreateAppOnPlatform bool\n\tcreateAppLang       = cmdutil.Oneof{\n\t\tValue:     \"\",\n\t\tAllowed:   cmdutil.LanguageFlagValues(),\n\t\tFlag:      \"lang\",\n\t\tFlagShort: \"l\",\n\t\tDesc:      \"Programming language to use for the app\",\n\t\tTypeDesc:  \"string\",\n\t}\n\tcreateAppLLMRules = cmdutil.Oneof{\n\t\tValue:     \"\",\n\t\tAllowed:   llm_rules.LLMRulesFlagValues(),\n\t\tFlag:      \"llm-rules\",\n\t\tFlagShort: \"r\",\n\t\tDesc:      \"Initialize the app with llm rules for a specific tool\",\n\t\tTypeDesc:  \"string\",\n\t}\n)\n\nvar createAppCmd = &cobra.Command{\n\tUse:   \"create [name]\",\n\tShort: \"Create a new Encore app\",\n\tArgs:  cobra.MaximumNArgs(1),\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tname := \"\"\n\t\tif len(args) > 0 {\n\t\t\tname = args[0]\n\t\t}\n\n\t\tvar tool llm_rules.Tool\n\t\tif createAppLLMRules.Value == \"\" {\n\t\t\tcfg, err := userconfig.Global().Get()\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatalf(\"Couldn't read user config: %s\", err)\n\t\t\t}\n\t\t\ttool = llm_rules.Tool(cfg.LLMRules)\n\t\t} else {\n\t\t\ttool = llm_rules.Tool(createAppLLMRules.Value)\n\t\t}\n\n\t\tif err := createApp(context.Background(), name, createAppTemplate, cmdutil.Language(createAppLang.Value), tool); err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t},\n}\n\nfunc init() {\n\tappCmd.AddCommand(createAppCmd)\n\tcreateAppCmd.Flags().BoolVar(&createAppOnPlatform, \"platform\", true, \"whether to create the app with the Encore Platform\")\n\tcreateAppCmd.Flags().StringVar(&createAppTemplate, \"example\", \"\", \"URL to example code to use.\")\n\tcreateAppLang.AddFlag(createAppCmd)\n\tcreateAppLLMRules.AddFlag(createAppCmd)\n}\n\nfunc promptAccountCreation() {\n\t// If shell is non-interactive, don't prompt\n\tif !term.IsTerminal(int(os.Stdin.Fd())) {\n\t\treturn\n\t}\n\tcyan := color.New(color.FgCyan)\n\tred := color.New(color.FgRed)\n\t// Prompt the user for creating an account if they're not logged in.\n\tif _, err := conf.CurrentUser(); errors.Is(err, fs.ErrNotExist) && createAppOnPlatform {\n\tPromptLoop:\n\t\tfor {\n\t\t\t_, _ = cyan.Fprint(os.Stderr, \"Log in / Sign up for a free Encore Cloud account to enable automated cloud deployments? (Y/n): \")\n\t\t\tvar input string\n\t\t\t_, _ = fmt.Scanln(&input)\n\t\t\tinput = strings.TrimSpace(input)\n\t\t\tswitch input {\n\t\t\tcase \"Y\", \"y\", \"yes\", \"\":\n\t\t\t\ttelemetry.Send(\"app.create.account\", map[string]any{\"response\": true})\n\t\t\t\tif err := auth.DoLogin(auth.AutoFlow); err != nil {\n\t\t\t\t\tcmdutil.Fatal(err)\n\t\t\t\t}\n\t\t\tcase \"N\", \"n\", \"no\":\n\t\t\t\ttelemetry.Send(\"app.create.account\", map[string]any{\"response\": false})\n\t\t\t\t// Continue without creating an account.\n\t\t\tcase \"q\", \"quit\", \"exit\":\n\t\t\t\tos.Exit(1)\n\t\t\tdefault:\n\t\t\t\t// Try again.\n\t\t\t\t_, _ = red.Fprintln(os.Stderr, \"Unexpected answer, please enter 'y' or 'n'.\")\n\t\t\t\tcontinue PromptLoop\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc promptRunApp() bool {\n\t// If shell is non-interactive, don't prompt\n\tif !term.IsTerminal(int(os.Stdin.Fd())) {\n\t\treturn false\n\t}\n\n\tcyan := color.New(color.FgCyan)\n\tred := color.New(color.FgRed)\n\tfor {\n\t\t_, _ = cyan.Fprint(os.Stderr, \"Run your app now? (Y/n): \")\n\t\tvar input string\n\t\t_, _ = fmt.Scanln(&input)\n\t\tinput = strings.TrimSpace(input)\n\t\tswitch input {\n\t\tcase \"Y\", \"y\", \"yes\", \"\":\n\t\t\ttelemetry.Send(\"app.create.run\", map[string]any{\"response\": true})\n\t\t\treturn true\n\t\tcase \"N\", \"n\", \"no\":\n\t\t\ttelemetry.Send(\"app.create.run\", map[string]any{\"response\": false})\n\t\t\treturn false\n\t\tcase \"q\", \"quit\", \"exit\":\n\t\t\ttelemetry.Send(\"app.create.run\", map[string]any{\"response\": false})\n\t\t\treturn false\n\t\tdefault:\n\t\t\t// Try again.\n\t\t\t_, _ = red.Fprintln(os.Stderr, \"Unexpected answer, please enter 'y' or 'n'.\")\n\t\t}\n\t}\n}\n\n// createApp is the implementation of the \"encore app create\" command.\nfunc createApp(ctx context.Context, name, template string, lang cmdutil.Language, llmRules llm_rules.Tool) (err error) {\n\tdefer func() {\n\t\t// We need to send the telemetry synchronously to ensure it's sent before the command exits.\n\t\ttelemetry.SendSync(\"app.create\", map[string]any{\n\t\t\t\"template\": template,\n\t\t\t\"lang\":     lang,\n\t\t\t\"error\":    err != nil,\n\t\t})\n\t}()\n\tcyan := color.New(color.FgCyan)\n\tgreen := color.New(color.FgGreen)\n\n\tpromptAccountCreation()\n\n\tif name == \"\" || template == \"\" || llmRules == \"\" {\n\t\tname, template, lang, llmRules = createAppForm(name, template, lang, llmRules, false)\n\t}\n\t// Treat the special name \"empty\" as the empty app template\n\t// (the rest of the code assumes that's the empty string).\n\tif template == \"empty\" {\n\t\ttemplate = \"\"\n\t}\n\tif template == \"\" && lang == cmdutil.LanguageTS {\n\t\ttemplate = \"ts/empty\"\n\t}\n\n\tif err := validateName(name); err != nil {\n\t\treturn err\n\t} else if _, err := os.Stat(name); err == nil {\n\t\treturn fmt.Errorf(\"directory %s already exists\", name)\n\t}\n\n\t// Parse template information, if provided.\n\tvar ex *github.Tree\n\tif template != \"\" {\n\t\tvar err error\n\t\tex, err = parseTemplate(ctx, template)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := os.Mkdir(name, 0755); err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\t// Clean up the directory we just created in case of an error.\n\t\t\t_ = os.RemoveAll(name)\n\t\t}\n\t}()\n\n\tif ex != nil {\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = fmt.Sprintf(\"Downloading template %s \", ex.Name())\n\t\ts.Start()\n\t\terr := github.ExtractTree(ctx, ex, name)\n\t\ts.Stop()\n\t\tfmt.Println()\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to download template %s: %v\", ex.Name(), err)\n\t\t}\n\t\tgray := color.New(color.Faint)\n\t\t_, _ = gray.Printf(\"Downloaded template %s.\\n\", ex.Name())\n\t} else {\n\t\t// Set up files that we need when we don't have an example\n\t\tif err := xos.WriteFile(filepath.Join(name, \".gitignore\"), []byte(\"/.encore\\n\"), 0644); err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t\tencoreModData := []byte(\"module encore.app\\n\")\n\t\tif err := xos.WriteFile(filepath.Join(name, \"go.mod\"), encoreModData, 0644); err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t}\n\n\t_, err = conf.CurrentUser()\n\tloggedIn := err == nil\n\n\texCfg, err := parseExampleConfig(name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse example config: %v\", err)\n\t}\n\n\t// Delete the example config file.\n\t_ = os.Remove(exampleJSONPath(name))\n\n\tvar app *platform.App\n\tif loggedIn && createAppOnPlatform {\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = \"Creating app on encore.dev \"\n\t\ts.Start()\n\t\tapp, err = createAppOnServer(name, exCfg)\n\t\ts.Stop()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"creating app on encore.dev: %v\", err)\n\t\t}\n\t}\n\n\tappRootRelpath := filepath.FromSlash(exCfg.EncoreAppPath)\n\tencoreAppPath := filepath.Join(name, appRootRelpath, \"encore.app\")\n\tappData, err := os.ReadFile(encoreAppPath)\n\tif err != nil {\n\t\tappData, err = []byte(\"{}\"), nil\n\t}\n\n\tif app != nil {\n\t\tappData, err = setEncoreAppID(appData, app.Slug, []string{})\n\t} else {\n\t\tappData, err = setEncoreAppID(appData, \"\", []string{\n\t\t\t\"The app is not currently linked to the encore.dev platform.\",\n\t\t\t`Use \"encore app link\" to link it.`,\n\t\t})\n\t}\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"write encore.app file\")\n\t}\n\tif err := xos.WriteFile(encoreAppPath, appData, 0644); err != nil {\n\t\treturn errors.Wrap(err, \"write encore.app file\")\n\t}\n\n\t// Update to latest encore.dev release\n\tif _, err := os.Stat(filepath.Join(name, appRootRelpath, \"go.mod\")); err == nil {\n\t\tlang = cmdutil.LanguageGo\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = \"Running go get encore.dev@latest\"\n\t\ts.Start()\n\t\tif err := gogetEncore(filepath.Join(name, appRootRelpath)); err != nil {\n\t\t\ts.FinalMSG = fmt.Sprintf(\"failed, skipping: %v\", err.Error())\n\t\t}\n\t\ts.Stop()\n\t} else if _, err := os.Stat(filepath.Join(name, appRootRelpath, \"package.json\")); err == nil {\n\t\tlang = cmdutil.LanguageTS\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = \"Running npm install encore.dev@latest\"\n\t\ts.Start()\n\t\tif err := npmInstallEncore(filepath.Join(name, appRootRelpath)); err != nil {\n\t\t\ts.FinalMSG = fmt.Sprintf(\"failed, skipping: %v\", err.Error())\n\t\t}\n\t\ts.Stop()\n\t}\n\n\t// Rewrite any existence of ENCORE_APP_ID to the allocated app id.\n\tif app != nil {\n\t\tif err := rewritePlaceholders(name, app); err != nil {\n\t\t\tred := color.New(color.FgRed)\n\t\t\t_, _ = red.Printf(\"Failed rewriting source code placeholders, skipping: %v\\n\", err)\n\t\t}\n\t}\n\n\tif err := initGitRepo(name, app); err != nil {\n\t\treturn err\n\t}\n\n\t// Try to generate wrappers. Don't error out if it fails for some reason,\n\t// it's a nice-to-have to avoid IDEs thinking there are compile errors before 'encore run' runs.\n\t_ = generateWrappers(filepath.Join(name, appRootRelpath))\n\n\t// Create the app on the daemon.\n\tappRoot, err := filepath.Abs(filepath.Join(name, appRootRelpath))\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"failed to get absolute path: %v\", err)\n\t}\n\tdaemon := cmdutil.ConnectDaemon(ctx)\n\tappResp, err := daemon.CreateApp(ctx, &daemonpb.CreateAppRequest{\n\t\tAppRoot:  appRoot,\n\t\tTutorial: exCfg.Tutorial,\n\t\tTemplate: template,\n\t})\n\tif err != nil {\n\t\tcolor.Red(\"Failed to create app on daemon: %s\\n\", err)\n\t}\n\n\tif err := llm_rules.SetupLLMRules(llmRules, lang, filepath.Join(name, appRootRelpath), appResp.AppId); err != nil {\n\t\tcolor.Red(\"Failed to setup LLM rules: %s\\n\", err)\n\t}\n\n\tcmdutil.ClearTerminalExceptFirstNLines(0)\n\t_, _ = green.Printf(\"Successfully created app %s!\\n\", name)\n\tcyanf := cyan.SprintfFunc()\n\tfmt.Println()\n\tif app != nil {\n\t\tfmt.Printf(\"App ID:   %s\\n\", cyanf(app.Slug))\n\t\tfmt.Printf(\"Web URL:  %s%s\", cyanf(\"https://app.encore.cloud/\"+app.Slug), cmdutil.Newline)\n\t}\n\tfmt.Printf(\"App Root: %s\\n\", cyanf(appRoot))\n\tllm_rules.PrintLLMRulesInfo(llmRules)\n\tgreenBoldF := green.Add(color.Bold).SprintfFunc()\n\tfmt.Printf(\"Run your app with: %s\\n\", greenBoldF(\"cd %s && encore run\", filepath.Join(name, appRootRelpath)))\n\tfmt.Println()\n\tif promptRunApp() {\n\t\tcmdutil.ClearTerminalExceptFirstNLines(0)\n\t\tstream, err := daemon.Run(ctx, &daemonpb.RunRequest{\n\t\t\tAppRoot:    appRoot,\n\t\t\tWatch:      true,\n\t\t\tWorkingDir: \".\",\n\t\t\tEnviron:    os.Environ(),\n\t\t\tListenAddr: \"127.0.0.1:4000\",\n\t\t\tBrowser:    daemonpb.RunRequest_BROWSER_ALWAYS,\n\t\t})\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"failed to run app: %v\", err)\n\t\t}\n\t\tconverter := cmdutil.ConvertJSONLogs(cmdutil.Colorize(true))\n\t\t_ = cmdutil.StreamCommandOutput(stream, converter)\n\t\treturn nil\n\t}\n\tcmdutil.ClearTerminalExceptFirstNLines(0)\n\tfmt.Print(\"Useful commands:\\n\\n\")\n\n\t_, _ = cyan.Printf(\"    encore run\\n\")\n\tfmt.Print(\"        Run your app locally\\n\\n\")\n\n\tif lang == cmdutil.LanguageGo {\n\t\t_, _ = cyan.Printf(\"    encore test ./...\\n\")\n\t} else {\n\t\t_, _ = cyan.Printf(\"    encore test\\n\")\n\t}\n\tfmt.Print(\"        Run tests\\n\\n\")\n\n\tif app != nil {\n\t\t_, _ = cyan.Printf(\"    git push encore\\n\")\n\t\tfmt.Print(\"        Deploys your app\\n\\n\")\n\t}\n\n\tfmt.Printf(\"Get started now: %s\\n\", greenBoldF(\"cd %s && encore run\", filepath.Join(name, appRootRelpath)))\n\treturn nil\n}\n\n// detectLang attempts to detect the application language for an Encore application\n// situated at appRoot.\nfunc detectLang(appRoot string) cmdutil.Language {\n\tif _, err := os.Stat(filepath.Join(appRoot, \"go.mod\")); err == nil {\n\t\treturn cmdutil.LanguageGo\n\t} else if _, err := os.Stat(filepath.Join(appRoot, \"package.json\")); err == nil {\n\t\treturn cmdutil.LanguageTS\n\t}\n\treturn cmdutil.LanguageGo\n}\n\nfunc validateName(name string) error {\n\tln := len(name)\n\tif ln == 0 {\n\t\treturn fmt.Errorf(\"name must not be empty\")\n\t} else if ln > 50 {\n\t\treturn fmt.Errorf(\"name too long (max 50 chars)\")\n\t}\n\n\tfor i, s := range name {\n\t\t// Outside of [a-z], [0-9] and != '-'?\n\t\tif !((s >= 'a' && s <= 'z') || (s >= '0' && s <= '9') || s == '-') {\n\t\t\treturn fmt.Errorf(\"name must only contain lowercase letters, digits, or dashes\")\n\t\t} else if s == '-' {\n\t\t\tif i == 0 {\n\t\t\t\treturn fmt.Errorf(\"name cannot start with a dash\")\n\t\t\t} else if (i + 1) == ln {\n\t\t\t\treturn fmt.Errorf(\"name cannot end with a dash\")\n\t\t\t} else if name[i-1] == '-' {\n\t\t\t\treturn fmt.Errorf(\"name cannot contain repeated dashes\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc gogetEncore(dir string) error {\n\tvar goBinPath string\n\n\t// Prefer the 'go' binary from the Encore GOROOT if available.\n\tif goroot, ok := env.OptEncoreGoRoot().Get(); ok {\n\t\tgoBinPath = filepath.Join(goroot, \"bin\", \"go\")\n\t} else {\n\t\t// Otherwise fall back to just \"go\", so that exec.Command\n\t\t// does a path lookup.\n\t\tgoBinPath = \"go\"\n\t}\n\n\t// Use the 'go' binary from the Encore GOROOT in case the user\n\t// does not have Go installed separately from Encore.\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.Command(goBinPath, \"get\", \"encore.dev@latest\")\n\tcmd.Dir = dir\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn errors.Newf(\"go get failed: %v: %s\", err, out)\n\t}\n\treturn nil\n}\n\nfunc npmInstallEncore(dir string) error {\n\targs := []string{\"install\"}\n\tif version.Channel == version.DevBuild {\n\t\targs = append(args, filepath.Join(env.EncoreRuntimesPath(), \"js\", \"encore.dev\"))\n\t} else {\n\t\targs = append(args, fmt.Sprintf(\"encore.dev@%s\", strings.TrimPrefix(version.Version, \"v\")))\n\t}\n\n\t// First install the 'encore.dev' package.\n\tcmd := exec.Command(\"npm\", args...)\n\tcmd.Dir = dir\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\terr = fmt.Errorf(\"installing encore.dev package failed: %v: %s\", err, out)\n\t}\n\n\t// Then run 'npm install'.\n\tcmd = exec.Command(\"npm\", \"install\")\n\tcmd.Dir = dir\n\tif out2, err2 := cmd.CombinedOutput(); err2 != nil && err == nil {\n\t\terr = fmt.Errorf(\"'npm install' failed: %v: %s\", err2, out2)\n\t}\n\n\treturn err\n}\n\nfunc createAppOnServer(name string, cfg exampleConfig) (*platform.App, error) {\n\tif _, err := conf.CurrentUser(); err != nil {\n\t\treturn nil, err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\tparams := &platform.CreateAppParams{\n\t\tName:           name,\n\t\tInitialSecrets: cfg.InitialSecrets,\n\t\tAppRootDir:     cfg.EncoreAppPath,\n\t}\n\treturn platform.CreateApp(ctx, params)\n}\n\nfunc parseTemplate(ctx context.Context, tmpl string) (*github.Tree, error) {\n\t// If the template does not contain a colon or a dot, it's definitely\n\t// not a github.com URL. Assume it's a simple template name.\n\tif !strings.Contains(tmpl, \":\") && !strings.Contains(tmpl, \".\") {\n\t\ttmpl = \"https://github.com/encoredev/examples/tree/main/\" + tmpl\n\t}\n\treturn github.ParseTree(ctx, tmpl)\n}\n\n// initGitRepo initializes the git repo.\n// If app is not nil, it configures the repo to push to the given app.\n// If git does not exist, it reports an error matching exec.ErrNotFound.\nfunc initGitRepo(path string, app *platform.App) (err error) {\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\tif ee, ok := e.(error); ok {\n\t\t\t\terr = ee\n\t\t\t} else {\n\t\t\t\tpanic(e)\n\t\t\t}\n\t\t}\n\t}()\n\n\tgit := func(args ...string) []byte {\n\t\tcmd := exec.Command(\"git\", args...)\n\t\tcmd.Dir = path\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil && !errors.Is(err, exec.ErrNotFound) {\n\t\t\tpanic(fmt.Errorf(\"git %s: %s (%w)\", strings.Join(args, \" \"), out, err))\n\t\t}\n\t\treturn out\n\t}\n\n\t// Initialize git repo\n\tgit(\"init\")\n\tif app != nil && app.MainBranch != nil {\n\t\tgit(\"checkout\", \"-b\", *app.MainBranch)\n\t}\n\tgit(\"config\", \"--local\", \"push.default\", \"current\")\n\tgit(\"add\", \"-A\")\n\n\tcmd := exec.Command(\"git\", \"commit\", \"-m\", \"Initial commit\")\n\tcmd.Dir = path\n\t// Configure the committer if the user hasn't done it themselves yet.\n\tif ok, _ := gitUserConfigured(); !ok {\n\t\tcmd.Env = append(os.Environ(),\n\t\t\t\"GIT_AUTHOR_NAME=Encore\",\n\t\t\t\"GIT_AUTHOR_EMAIL=git-bot@encore.dev\",\n\t\t\t\"GIT_COMMITTER_NAME=Encore\",\n\t\t\t\"GIT_COMMITTER_EMAIL=git-bot@encore.dev\",\n\t\t)\n\t}\n\tif out, err := cmd.CombinedOutput(); err != nil && !errors.Is(err, exec.ErrNotFound) {\n\t\treturn fmt.Errorf(\"create initial commit repository: %s (%v)\", out, err)\n\t}\n\n\tif app != nil {\n\t\tgit(\"remote\", \"add\", defaultGitRemoteName, defaultGitRemoteURL+app.Slug)\n\t}\n\n\treturn nil\n}\n\nfunc addEncoreRemote(root, appID string) {\n\t// Determine if there are any remotes\n\tcmd := exec.Command(\"git\", \"remote\")\n\tcmd.Dir = root\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn\n\t}\n\tout = bytes.TrimSpace(out)\n\tif len(out) == 0 {\n\t\tcmd = exec.Command(\"git\", \"remote\", \"add\", defaultGitRemoteName, defaultGitRemoteURL+appID)\n\t\tcmd.Dir = root\n\t\tif err := cmd.Run(); err == nil {\n\t\t\tfmt.Println(\"Configured git remote 'encore' to push/pull with Encore.\")\n\t\t}\n\t}\n}\n\n// gitUserConfigured reports whether the user has configured\n// user.name and user.email in git.\nfunc gitUserConfigured() (bool, error) {\n\tfor _, s := range []string{\"user.name\", \"user.email\"} {\n\t\tout, err := exec.Command(\"git\", \"config\", s).CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t} else if len(bytes.TrimSpace(out)) == 0 {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\treturn true, nil\n}\n\n// rewritePlaceholders recursively rewrites all files within basePath\n// to replace placeholders with the actual values for this particular app.\nfunc rewritePlaceholders(basePath string, app *platform.App) error {\n\tvar first error\n\terr := filepath.WalkDir(basePath, func(path string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !info.Type().IsRegular() {\n\t\t\treturn nil\n\t\t}\n\t\tif err := rewritePlaceholder(path, info, app); err != nil {\n\t\t\tif first == nil {\n\t\t\t\tfirst = err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err == nil {\n\t\terr = first\n\t}\n\treturn err\n}\n\n// rewritePlaceholder rewrites a file to replace placeholders with the\n// actual values for this particular app. If the file contains none of\n// the placeholders, this is a no-op.\nfunc rewritePlaceholder(path string, info fs.DirEntry, app *platform.App) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tplaceholders := []string{\n\t\t\"{{ENCORE_APP_ID}}\", app.Slug,\n\t}\n\n\tvar replaced bool\n\tfor i := 0; i < len(placeholders); i += 2 {\n\t\tplaceholder := []byte(placeholders[i])\n\t\ttarget := []byte(placeholders[i+1])\n\t\tif bytes.Contains(data, placeholder) {\n\t\t\tdata = bytes.ReplaceAll(data, placeholder, target)\n\t\t\treplaced = true\n\t\t}\n\t}\n\n\tif replaced {\n\t\treturn xos.WriteFile(path, data, info.Type().Perm())\n\t}\n\treturn nil\n}\n\n// exampleConfig is the optional configuration file for example apps.\ntype exampleConfig struct {\n\t// Relative path to the directory where the `encore.app` should be located.\n\t// Defaults to \".\".\n\tEncoreAppPath string `json:\"encore_app_path\"`\n\n\tInitialSecrets map[string]string `json:\"initial_secrets\"`\n\tTutorial       bool              `json:\"tutorial\"`\n}\n\nfunc parseExampleConfig(repoPath string) (cfg exampleConfig, err error) {\n\tbaseConfig := exampleConfig{\n\t\tEncoreAppPath: \".\",\n\t}\n\tdata, err := os.ReadFile(exampleJSONPath(repoPath))\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn baseConfig, nil\n\t} else if err != nil {\n\t\treturn baseConfig, err\n\t}\n\n\tdata, err = hujson.Standardize(data)\n\tif err != nil {\n\t\treturn baseConfig, err\n\t} else if err := json.Unmarshal(data, &cfg); err != nil {\n\t\treturn baseConfig, err\n\t}\n\n\tif cfg.EncoreAppPath == \"\" {\n\t\tcfg.EncoreAppPath = \".\"\n\t}\n\tif !filepath.IsLocal(cfg.EncoreAppPath) {\n\t\treturn baseConfig, errors.New(\"encore_app_path must be a local path\")\n\t}\n\treturn cfg, nil\n}\n\nfunc exampleJSONPath(repoPath string) string {\n\treturn filepath.Join(repoPath, \"example-initial-setup.json\")\n}\n\n// setEncoreAppID rewrites the encore.app file to replace the app id, preserving comments.\n// It optionally adds comment lines before the \"id\" field if commentLines is not nil.\nfunc setEncoreAppID(data []byte, id string, commentLines []string) ([]byte, error) {\n\tif len(data) == 0 {\n\t\tdata = []byte(\"{}\")\n\t}\n\n\troot, err := hujson.Parse(data)\n\tif err != nil {\n\t\treturn data, errors.Wrap(err, \"parse encore.app\")\n\t}\n\tobj, ok := root.Value.(*hujson.Object)\n\tif !ok {\n\t\treturn data, errors.New(\"invalid encore.app format: not a json object\")\n\t}\n\n\tvar buf bytes.Buffer\n\tfor i, ln := range commentLines {\n\t\tif i == 0 {\n\t\t\tfmt.Fprintf(&buf, \"\\n\")\n\t\t}\n\t\tfmt.Fprintf(&buf, \"\\t// %s\\n\", strings.TrimSpace(ln))\n\t}\n\textra := hujson.Extra(buf.Bytes())\n\tjsonValue, _ := json.Marshal(id)\n\tvalue := hujson.Value{\n\t\tValue: hujson.Literal(jsonValue),\n\t}\n\n\tfound := false\n\tfor i := range obj.Members {\n\t\tm := &obj.Members[i]\n\t\tif lit, ok := m.Name.Value.(hujson.Literal); ok && lit.String() == \"id\" {\n\t\t\tif commentLines != nil {\n\t\t\t\tm.Name.BeforeExtra = extra\n\t\t\t}\n\t\t\tm.Value = value\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tobj.Members = append([]hujson.ObjectMember{{\n\t\t\tName: hujson.Value{\n\t\t\t\tBeforeExtra: extra,\n\t\t\t\tValue:       hujson.Literal(`\"id\"`),\n\t\t\t},\n\t\t\tValue: value,\n\t\t}}, obj.Members...)\n\t}\n\n\troot.Format()\n\treturn root.Pack(), nil\n}\n\n// generateWrappers runs 'encore gen wrappers' in the given directory.\nfunc generateWrappers(dir string) error {\n\t// Use this executable if we can.\n\texe, err := os.Executable()\n\tif err != nil {\n\t\texe = \"encore\"\n\t}\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.Command(exe, \"gen\", \"wrappers\")\n\tcmd.Dir = dir\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"encore gen wrappers failed: %v: %s\", err, out)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/app/create_form.go",
    "content": "package app\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/list\"\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/tailscale/hujson\"\n\t\"golang.org/x/term\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/llm_rules\"\n\t\"encr.dev/pkg/option\"\n)\n\ntype templateItem struct {\n\tItemTitle string           `json:\"title\"`\n\tDesc      string           `json:\"desc\"`\n\tTemplate  string           `json:\"template\"`\n\tLang      cmdutil.Language `json:\"lang\"`\n}\n\nfunc (i templateItem) Title() string       { return i.ItemTitle }\nfunc (i templateItem) Description() string { return i.Desc }\nfunc (i templateItem) FilterValue() string { return i.ItemTitle }\n\ntype CreateStep int\n\nconst (\n\tCreateStepLang CreateStep = iota\n\tCreateStepTemplate\n\tCreateStepAppName\n\tCreateStepLLMRules\n)\n\ntype createFormModel struct {\n\tsteps []CreateStep\n\n\tlang      langSelectModel\n\ttemplates templateListModel\n\tappName   appNameModel\n\tllmRules  llm_rules.ToolSelectModel\n\n\tinitExistingApp bool\n\n\twidth   int\n\theight  int\n\taborted bool\n}\n\nfunc (m createFormModel) currentStep() option.Option[CreateStep] {\n\tif len(m.steps) == 0 {\n\t\treturn option.None[CreateStep]()\n\t}\n\treturn option.Some(m.steps[0])\n}\n\nfunc (m createFormModel) hasStep(s CreateStep) bool {\n\treturn slices.Contains(m.steps, s)\n}\n\nfunc (m *createFormModel) removeStep(s CreateStep) {\n\tm.steps = slices.DeleteFunc(m.steps, func(step CreateStep) bool {\n\t\treturn step == s\n\t})\n}\n\nfunc (m createFormModel) Init() tea.Cmd {\n\treturn tea.Batch(\n\t\tm.appName.Init(),\n\t\tm.templates.Init(),\n\t)\n}\n\nconst checkmark = \"✔\"\n\ntype appNameDone struct{}\n\ntype appNameModel struct {\n\tpredefined string\n\ttext       textinput.Model\n\tdirExists  bool\n}\n\nfunc (m appNameModel) Init() tea.Cmd {\n\treturn tea.Batch(\n\t\ttextinput.Blink,\n\t)\n}\n\nfunc (m appNameModel) Selected() string {\n\tif m.predefined != \"\" {\n\t\treturn m.predefined\n\t}\n\treturn m.text.Value()\n}\n\nfunc (m appNameModel) Update(msg tea.Msg) (appNameModel, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\tvar c tea.Cmd\n\tm.text, c = m.text.Update(msg)\n\tcmds = append(cmds, c)\n\n\tif val := m.text.Value(); val != \"\" {\n\t\t_, err := os.Stat(val)\n\t\tm.dirExists = err == nil\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.Type {\n\t\tcase tea.KeyEnter:\n\t\t\tif m.text.Value() != \"\" && !m.dirExists {\n\t\t\t\tcmds = append(cmds, func() tea.Msg {\n\t\t\t\t\treturn appNameDone{}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m appNameModel) View() string {\n\tvar b strings.Builder\n\tif m.text.Focused() {\n\t\tb.WriteString(cmdutil.InputStyle.Render(\"App Name\"))\n\t\tb.WriteString(cmdutil.DescStyle.Render(\" [Use only lowercase letters, digits, and dashes]\"))\n\t\tb.WriteByte('\\n')\n\t\tb.WriteString(m.text.View())\n\t\tif m.dirExists {\n\t\t\tb.WriteString(cmdutil.ErrorStyle.Render(\" error: dir already exists\"))\n\t\t}\n\t} else {\n\t\tfmt.Fprintf(&b, \"%s App Name: %s\", checkmark, m.text.Value())\n\t}\n\tb.WriteByte('\\n')\n\treturn b.String()\n}\n\ntype templateListModel struct {\n\tpredefined string\n\tfilter     cmdutil.Language\n\n\tall     []templateItem\n\tlist    list.Model\n\tloading spinner.Model\n}\n\nfunc (m templateListModel) Init() tea.Cmd {\n\treturn tea.Batch(\n\t\tloadTemplates,\n\t\tm.loading.Tick,\n\t)\n}\n\nfunc (m *templateListModel) SetSize(width, height int) {\n\tm.list.SetWidth(width)\n\tm.list.SetHeight(max(height-1, 0))\n}\n\ntype templateSelectDone struct{}\n\nfunc (m templateListModel) Update(msg tea.Msg) (templateListModel, tea.Cmd) {\n\tvar cmds []tea.Cmd\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.Type {\n\t\tcase tea.KeyEnter:\n\t\t\t// Have we selected a template?\n\t\t\tif idx := m.list.Index(); idx >= 0 {\n\t\t\t\treturn m, func() tea.Msg { return templateSelectDone{} }\n\t\t\t}\n\t\t}\n\n\tcase spinner.TickMsg:\n\t\tm.loading, _ = m.loading.Update(msg)\n\n\tcase loadedTemplates:\n\t\tm.all = msg\n\t\tm.refreshFilter()\n\t\tnewList, c := m.list.Update(msg)\n\t\tm.list = newList\n\t\tcmds = append(cmds, c)\n\t}\n\n\tnewList, c := m.list.Update(msg)\n\tm.list = newList\n\tcmds = append(cmds, c)\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *templateListModel) UpdateFilter(lang cmdutil.Language) {\n\tm.filter = lang\n\tm.refreshFilter()\n}\n\nfunc (m *templateListModel) refreshFilter() {\n\tvar listItems []list.Item\n\tfor _, it := range m.all {\n\t\tif it.Lang == m.filter {\n\t\t\tlistItems = append(listItems, it)\n\t\t}\n\t}\n\tm.list.SetItems(listItems)\n}\n\nfunc (m templateListModel) View() string {\n\tvar b strings.Builder\n\tb.WriteString(cmdutil.InputStyle.Render(\"Template\"))\n\tb.WriteString(cmdutil.DescStyle.Render(\" [Use arrows to move]\"))\n\tb.WriteByte('\\n')\n\tb.WriteString(m.list.View())\n\n\treturn b.String()\n}\n\nfunc (m templateListModel) Selected() string {\n\tif m.predefined != \"\" {\n\t\treturn m.predefined\n\t}\n\tidx := m.list.Index()\n\tif idx < 0 {\n\t\treturn \"\"\n\t}\n\treturn m.list.Items()[idx].FilterValue()\n}\n\nfunc (m createFormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmds []tea.Cmd\n\t\tc    tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\":\n\t\t\tm.aborted = true\n\t\t\treturn m, tea.Quit\n\t\tcase \"q\":\n\t\t\t// Only quit if no text input is focused\n\t\t\tif step, ok := m.currentStep().Get(); ok && step == CreateStepAppName {\n\t\t\t\tif m.appName.text.Focused() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tm.aborted = true\n\t\t\treturn m, tea.Quit\n\t\t}\n\n\t\tif step, ok := m.currentStep().Get(); ok {\n\t\t\tswitch step {\n\t\t\tcase CreateStepLang:\n\t\t\t\tm.lang, c = m.lang.Update(msg)\n\t\t\t\tcmds = append(cmds, c)\n\t\t\tcase CreateStepTemplate:\n\t\t\t\tm.templates, c = m.templates.Update(msg)\n\t\t\t\tcmds = append(cmds, c)\n\t\t\tcase CreateStepAppName:\n\t\t\t\tm.appName, c = m.appName.Update(msg)\n\t\t\t\tcmds = append(cmds, c)\n\t\t\tcase CreateStepLLMRules:\n\t\t\t\tm.llmRules, c = m.llmRules.Update(msg)\n\t\t\t\tcmds = append(cmds, c)\n\t\t\t}\n\t\t}\n\t\treturn m, tea.Batch(cmds...)\n\n\tcase langSelectDone:\n\t\tm.removeStep(CreateStepLang)\n\t\tm.templates.UpdateFilter(msg.Selected)\n\t\tm.SetSize(m.width, m.height)\n\n\tcase llm_rules.ToolSelectDone:\n\t\tm.removeStep(CreateStepLLMRules)\n\t\tm.SetSize(m.width, m.height)\n\n\tcase templateSelectDone:\n\t\tm.removeStep(CreateStepTemplate)\n\t\tif m.appName.predefined != \"\" {\n\t\t\tm.removeStep(CreateStepAppName)\n\t\t}\n\t\tm.SetSize(m.width, m.height)\n\n\tcase appNameDone:\n\t\tm.removeStep(CreateStepAppName)\n\t\tm.SetSize(m.width, m.height)\n\n\tcase tea.WindowSizeMsg:\n\t\tm.width = msg.Width\n\t\tm.height = msg.Height\n\t\tm.SetSize(msg.Width, msg.Height)\n\t\treturn m, nil\n\t}\n\n\t// No more steps, quit\n\tif !m.currentStep().Present() {\n\t\tcmds = append(cmds, tea.Quit)\n\t}\n\n\t// Update all submodels for other messages.\n\tm.lang, c = m.lang.Update(msg)\n\tcmds = append(cmds, c)\n\tm.templates, c = m.templates.Update(msg)\n\tcmds = append(cmds, c)\n\tm.llmRules, c = m.llmRules.Update(msg)\n\tcmds = append(cmds, c)\n\tm.appName, c = m.appName.Update(msg)\n\tcmds = append(cmds, c)\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *createFormModel) SetSize(width, height int) {\n\tdoneHeight := lipgloss.Height(m.doneView())\n\tavailHeight := height - doneHeight\n\n\t// CreateStepLang\n\tm.lang.SetSize(width, availHeight)\n\n\t// CreateStepTemplate\n\tm.templates.SetSize(width, availHeight)\n\n\t// CreateStepLLMRules\n\tm.llmRules.SetSize(width, availHeight)\n}\n\nfunc (m createFormModel) doneView() string {\n\tvar b strings.Builder\n\n\trenderDone := func(title, value string) {\n\t\tb.WriteString(cmdutil.SuccessStyle.Render(fmt.Sprintf(\"%s %s: \", checkmark, title)))\n\t\tb.WriteString(value)\n\t\tb.WriteByte('\\n')\n\t}\n\n\trenderLangDone := func() {\n\t\trenderDone(\"Language\", m.lang.Selected().Display())\n\t}\n\n\trenderNameDone := func() {\n\t\trenderDone(\"App Name\", m.appName.Selected())\n\t}\n\n\trenderTemplateDone := func() {\n\t\trenderDone(\"Template\", m.templates.Selected())\n\t}\n\n\trenderLLMRulesDone := func() {\n\t\trenderDone(\"LLM Rules\", m.llmRules.Selected().Display())\n\t}\n\n\tif m.appName.predefined != \"\" {\n\t\trenderNameDone()\n\t}\n\tif m.templates.predefined == \"\" && !m.hasStep(CreateStepLang) {\n\t\trenderLangDone()\n\t}\n\tif !m.initExistingApp {\n\t\tif m.templates.predefined != \"\" || !m.hasStep(CreateStepTemplate) {\n\t\t\trenderTemplateDone()\n\t\t}\n\t\tif m.llmRules.Predefined != \"\" || !m.hasStep(CreateStepLLMRules) {\n\t\t\tif m.llmRules.Selected() != llm_rules.LLMRulesToolNone {\n\t\t\t\trenderLLMRulesDone()\n\t\t\t}\n\t\t}\n\t}\n\tif m.appName.predefined == \"\" && !m.hasStep(CreateStepAppName) {\n\t\trenderNameDone()\n\t}\n\n\treturn b.String()\n}\n\nfunc (m createFormModel) View() string {\n\tvar b strings.Builder\n\n\tdoneView := m.doneView()\n\n\tb.WriteString(doneView)\n\tif doneView != \"\" {\n\t\tb.WriteByte('\\n')\n\t}\n\n\tif step, ok := m.currentStep().Get(); ok {\n\t\tif step == CreateStepLang {\n\t\t\tb.WriteString(m.lang.View())\n\t\t}\n\n\t\tif step == CreateStepTemplate {\n\t\t\tb.WriteString(m.templates.View())\n\t\t}\n\n\t\tif step == CreateStepAppName {\n\t\t\tb.WriteString(m.appName.View())\n\t\t}\n\n\t\tif step == CreateStepLLMRules {\n\t\t\tb.WriteString(m.llmRules.View())\n\t\t}\n\t}\n\n\treturn cmdutil.DocStyle.Render(b.String())\n}\n\nfunc (m templateListModel) SelectedItem() (templateItem, bool) {\n\tif m.predefined != \"\" {\n\t\treturn templateItem{}, false\n\t}\n\tidx := m.list.Index()\n\titems := m.list.Items()\n\tif idx >= 0 && len(items) > idx {\n\t\treturn items[idx].(templateItem), true\n\t}\n\treturn templateItem{}, false\n}\n\nfunc createAppForm(inputName, inputTemplate string, inputLang cmdutil.Language, inputLLMRules llm_rules.Tool, initExistingApp bool) (appName, template string, selectedLang cmdutil.Language, selectedRules llm_rules.Tool) {\n\t// If all is set, just return\n\tif inputName != \"\" && inputTemplate != \"\" && inputLLMRules != \"\" {\n\t\treturn inputName, inputTemplate, inputLang, inputLLMRules\n\t}\n\n\t// If shell is non-interactive, don't prompt\n\tif !term.IsTerminal(int(os.Stdin.Fd())) {\n\t\tif inputName == \"\" {\n\t\t\tcmdutil.Fatal(\"specify an app name\")\n\t\t}\n\t\treturn inputName, inputTemplate, inputLang, inputLLMRules\n\t}\n\n\tvar langModel langSelectModel\n\t{\n\t\tls := list.NewDefaultItemStyles()\n\t\tls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\tls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\tdel := list.NewDefaultDelegate()\n\t\tdel.Styles = ls\n\t\tdel.ShowDescription = false\n\t\tdel.SetSpacing(0)\n\n\t\titems := []list.Item{\n\t\t\tlangItem{\n\t\t\t\tlang: cmdutil.LanguageGo,\n\t\t\t\tdesc: \"Build performant and scalable backends with Go\",\n\t\t\t},\n\t\t\tlangItem{\n\t\t\t\tlang: cmdutil.LanguageTS,\n\t\t\t\tdesc: \"Build backend and full-stack applications with TypeScript\",\n\t\t\t},\n\t\t}\n\n\t\tll := list.New(items, del, 0, 0)\n\t\tll.SetShowTitle(false)\n\t\tll.SetShowHelp(false)\n\t\tll.SetShowPagination(true)\n\t\tll.SetShowFilter(false)\n\t\tll.SetFilteringEnabled(false)\n\t\tll.SetShowStatusBar(false)\n\t\tll.DisableQuitKeybindings() // quit handled by createFormModel\n\t\tlangModel = langSelectModel{\n\t\t\tList:       ll,\n\t\t\tPredefined: inputLang,\n\t\t}\n\t\tlangModel.SetSize(0, 20)\n\t}\n\n\tvar templateModel templateListModel\n\t{\n\t\tls := list.NewDefaultItemStyles()\n\t\tls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\tls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\tdel := list.NewDefaultDelegate()\n\t\tdel.Styles = ls\n\n\t\tll := list.New(nil, del, 0, 20)\n\t\tll.SetShowTitle(false)\n\t\tll.SetShowHelp(false)\n\t\tll.SetShowPagination(true)\n\t\tll.SetShowFilter(false)\n\t\tll.SetFilteringEnabled(false)\n\t\tll.SetShowStatusBar(false)\n\t\tll.DisableQuitKeybindings() // quit handled by createFormModel\n\n\t\tsp := spinner.New()\n\t\tsp.Spinner = spinner.Dot\n\t\tsp.Style = cmdutil.InputStyle.Copy().Inline(true)\n\t\ttemplateModel = templateListModel{\n\t\t\tpredefined: inputTemplate,\n\t\t\tlist:       ll,\n\t\t\tloading:    sp,\n\t\t}\n\t}\n\tvar llmRulesModel llm_rules.ToolSelectModel\n\t{\n\t\tls := list.NewDefaultItemStyles()\n\t\tls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\tls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\tdel := list.NewDefaultDelegate()\n\t\tdel.Styles = ls\n\t\tdel.ShowDescription = false\n\t\tdel.SetSpacing(0)\n\n\t\titems := make([]list.Item, 0, len(llm_rules.AllLLMRules)+1)\n\t\titems = append(items, llm_rules.NewLLMRulesItem(llm_rules.LLMRulesToolNone))\n\t\tfor _, rule := range llm_rules.AllLLMRules {\n\t\t\titems = append(items, llm_rules.NewLLMRulesItem(rule))\n\t\t}\n\n\t\tll := list.New(items, del, 0, 0)\n\t\tll.SetShowTitle(false)\n\t\tll.SetShowHelp(false)\n\t\tll.SetShowPagination(true)\n\t\tll.SetShowFilter(false)\n\t\tll.SetFilteringEnabled(false)\n\t\tll.SetShowStatusBar(false)\n\t\tll.DisableQuitKeybindings() // quit handled by createFormModel\n\n\t\tllmRulesModel = llm_rules.ToolSelectModel{\n\t\t\tList:       ll,\n\t\t\tPredefined: inputLLMRules,\n\t\t}\n\t\tllmRulesModel.SetSize(0, 20)\n\n\t}\n\n\tvar nameModel appNameModel\n\t{\n\t\ttext := textinput.New()\n\t\ttext.Focus()\n\t\ttext.CharLimit = 20\n\t\ttext.Width = 30\n\t\ttext.Validate = incrementalValidateNameInput\n\n\t\tnameModel = appNameModel{predefined: inputName, text: text}\n\t}\n\n\t// Setup what steps and in what order they should be presented\n\tvar steps []CreateStep\n\tif initExistingApp {\n\t\tif langModel.Predefined == \"\" {\n\t\t\tsteps = append(steps, CreateStepLang)\n\t\t}\n\t} else {\n\t\tif templateModel.predefined == \"\" {\n\t\t\tif langModel.Predefined == \"\" {\n\t\t\t\tsteps = append(steps, CreateStepLang)\n\t\t\t} else {\n\t\t\t\ttemplateModel.UpdateFilter(inputLang)\n\t\t\t}\n\t\t\tsteps = append(steps, CreateStepTemplate)\n\t\t}\n\t\tif llmRulesModel.Predefined == \"\" {\n\t\t\tsteps = append(steps, CreateStepLLMRules)\n\t\t}\n\t}\n\tif nameModel.predefined == \"\" {\n\t\tsteps = append(steps, CreateStepAppName)\n\t}\n\n\tm := createFormModel{\n\t\tsteps:           steps,\n\t\tlang:            langModel,\n\t\ttemplates:       templateModel,\n\t\tllmRules:        llmRulesModel,\n\t\tappName:         nameModel,\n\t\tinitExistingApp: initExistingApp,\n\t}\n\n\t// If we have a name, start the list without any selection.\n\tif m.appName.predefined != \"\" {\n\t\tm.templates.list.Select(-1)\n\t}\n\n\tp := tea.NewProgram(m)\n\n\tresult, err := p.Run()\n\tif err != nil {\n\t\tcmdutil.Fatal(err)\n\t}\n\n\t// Validate the result.\n\tres := result.(createFormModel)\n\tif res.aborted {\n\t\tos.Exit(1)\n\t}\n\n\tappName, template = inputName, inputTemplate\n\n\tif appName == \"\" {\n\t\tappName = res.appName.text.Value()\n\t}\n\n\tif template == \"\" && !initExistingApp {\n\t\tsel, ok := res.templates.SelectedItem()\n\t\tif !ok {\n\t\t\tcmdutil.Fatal(\"no template selected\")\n\t\t}\n\t\ttemplate = sel.Template\n\t}\n\n\treturn appName, template, res.lang.Selected(), res.llmRules.Selected()\n}\n\ntype langItem struct {\n\tlang cmdutil.Language\n\tdesc string\n}\n\nfunc (i langItem) FilterValue() string          { return i.lang.Display() }\nfunc (i langItem) Title() string                { return i.FilterValue() }\nfunc (i langItem) Description() string          { return \"\" }\nfunc (i langItem) SelectedID() cmdutil.Language { return i.lang }\n\ntype langSelectModel = cmdutil.SimpleSelectModel[cmdutil.Language, langItem]\ntype langSelectDone = cmdutil.SimpleSelectDone[cmdutil.Language]\n\ntype loadedTemplates []templateItem\n\nvar defaultTutorials = []templateItem{\n\t{\n\t\tItemTitle: \"Intro to Encore.ts\",\n\t\tDesc:      \"An interactive tutorial\",\n\t\tTemplate:  \"ts/introduction\",\n\t\tLang:      \"ts\",\n\t},\n}\n\nvar defaultTemplates = []templateItem{\n\t{\n\t\tItemTitle: \"Hello World\",\n\t\tDesc:      \"A simple REST API\",\n\t\tTemplate:  \"hello-world\",\n\t\tLang:      \"go\",\n\t},\n\t{\n\t\tItemTitle: \"Hello World\",\n\t\tDesc:      \"A simple REST API\",\n\t\tTemplate:  \"ts/hello-world\",\n\t\tLang:      \"ts\",\n\t},\n\t{\n\t\tItemTitle: \"Uptime Monitor\",\n\t\tDesc:      \"Microservices, SQL Databases, Pub/Sub, Cron Jobs\",\n\t\tTemplate:  \"uptime\",\n\t\tLang:      \"go\",\n\t},\n\t{\n\t\tItemTitle: \"Uptime Monitor\",\n\t\tDesc:      \"Microservices, SQL Databases, Pub/Sub, Cron Jobs\",\n\t\tTemplate:  \"ts/uptime\",\n\t\tLang:      \"ts\",\n\t},\n\t{\n\t\tItemTitle: \"GraphQL\",\n\t\tDesc:      \"GraphQL API, Microservices, SQL Database\",\n\t\tTemplate:  \"graphql\",\n\t\tLang:      \"go\",\n\t},\n\t{\n\t\tItemTitle: \"URL Shortener\",\n\t\tDesc:      \"REST API, SQL Database\",\n\t\tTemplate:  \"url-shortener\",\n\t\tLang:      \"go\",\n\t},\n\t{\n\t\tItemTitle: \"URL Shortener\",\n\t\tDesc:      \"REST API, SQL Database\",\n\t\tTemplate:  \"ts/url-shortener\",\n\t\tLang:      \"ts\",\n\t},\n\t{\n\t\tItemTitle: \"SaaS Starter\",\n\t\tDesc:      \"Complete app with Clerk auth, Stripe billing, etc. (advanced)\",\n\t\tTemplate:  \"ts/saas-starter\",\n\t\tLang:      \"ts\",\n\t},\n\t{\n\t\tItemTitle: \"Empty app\",\n\t\tDesc:      \"Start from scratch (experienced users only)\",\n\t\tTemplate:  \"\",\n\t\tLang:      \"go\",\n\t},\n\t{\n\t\tItemTitle: \"Empty app\",\n\t\tDesc:      \"Start from scratch (experienced users only)\",\n\t\tTemplate:  \"ts/empty\",\n\t\tLang:      \"ts\",\n\t},\n}\n\nfunc fetchTemplates(url string, defaults []templateItem) []templateItem {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tif req, err := http.NewRequestWithContext(ctx, \"GET\", url, nil); err == nil {\n\t\tif resp, err := http.DefaultClient.Do(req); err == nil {\n\t\t\tif data, err := io.ReadAll(resp.Body); err == nil {\n\t\t\t\tdata, err = hujson.Standardize(data)\n\t\t\t\tif err == nil {\n\t\t\t\t\tvar items []templateItem\n\t\t\t\t\tif err := json.Unmarshal(data, &items); err == nil && len(items) > 0 {\n\t\t\t\t\t\treturn items\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn defaults\n}\n\nfunc loadTemplates() tea.Msg {\n\tvar wg sync.WaitGroup\n\tvar templates, tutorials []templateItem\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttemplates = fetchTemplates(\"https://raw.githubusercontent.com/encoredev/examples/main/cli-templates.json\", defaultTemplates)\n\t}()\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\ttutorials = fetchTemplates(\"https://raw.githubusercontent.com/encoredev/examples/main/cli-tutorials.json\", defaultTutorials)\n\t}()\n\twg.Wait()\n\treturn loadedTemplates(append(tutorials, templates...))\n}\n\n// incrementalValidateNameInput is like validateName but only\n// checks for valid/invalid characters. It can't check for\n// whether the last character is a dash, since if we treat that\n// as an error the user won't be able to enter dashes at all.\nfunc incrementalValidateNameInput(name string) error {\n\tln := len(name)\n\tif ln == 0 {\n\t\treturn fmt.Errorf(\"name must not be empty\")\n\t} else if ln > 50 {\n\t\treturn fmt.Errorf(\"name too long (max 50 chars)\")\n\t}\n\n\tfor i, s := range name {\n\t\t// Outside of [a-z], [0-9] and != '-'?\n\t\tif !((s >= 'a' && s <= 'z') || (s >= '0' && s <= '9') || s == '-') {\n\t\t\treturn fmt.Errorf(\"name must only contain lowercase letters, digits, or dashes\")\n\t\t} else if s == '-' {\n\t\t\tif i == 0 {\n\t\t\t\treturn fmt.Errorf(\"name cannot start with a dash\")\n\t\t\t} else if name[i-1] == '-' {\n\t\t\t\treturn fmt.Errorf(\"name cannot contain repeated dashes\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/app/create_test.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc Test_setEncoreAppID(t *testing.T) {\n\ttests := []struct {\n\t\tdata         []byte\n\t\tid           string\n\t\tcommentLines []string\n\t\twant         string\n\t}{\n\t\t{\n\t\t\tdata:         []byte(`{}`),\n\t\t\tid:           \"foo\",\n\t\t\tcommentLines: []string{\"bar\"},\n\t\t\twant: `{\n\t// bar\n\t\"id\": \"foo\",\n}\n`,\n\t\t},\n\t\t{\n\t\t\tdata:         []byte(``),\n\t\t\tid:           \"foo\",\n\t\t\tcommentLines: []string{\"bar\"},\n\t\t\twant: `{\n\t// bar\n\t\"id\": \"foo\",\n}\n`,\n\t\t},\n\t\t{\n\t\t\tdata: []byte(`{\n\t// foo\n\t\"id\": \"test\",\n}`),\n\t\t\tid:           \"foo\",\n\t\t\tcommentLines: []string{\"bar\", \"baz\"},\n\t\t\twant: `{\n\t// bar\n\t// baz\n\t\"id\": \"foo\",\n}\n`,\n\t\t},\n\t\t{\n\t\t\tdata: []byte(`{\n\t\"some_other_field\": true,\n\t// foo\n\t\"id\": \"test\",\n}`),\n\t\t\tid:           \"foo\",\n\t\t\tcommentLines: []string{\"bar\", \"baz\"},\n\t\t\twant: `{\n\t\"some_other_field\": true,\n\t// bar\n\t// baz\n\t\"id\": \"foo\",\n}\n`,\n\t\t},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"%d\", i), func(t *testing.T) {\n\t\t\tgot, err := setEncoreAppID(tt.data, tt.id, tt.commentLines)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tgotStr := string(got)\n\t\t\tif gotStr != tt.want {\n\t\t\t\tt.Errorf(\"setEncoreAppID() = %q, want %q\", gotStr, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/app/initialize.go",
    "content": "package app\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/llm_rules\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/pkg/xos\"\n)\n\nconst (\n\ttsEncoreAppData = `{%s\n\t\"id\": \"%s\",\n\t\"lang\": \"typescript\",\n}\n`\n\tgoEncoreAppData = `{%s\n\t\"id\": \"%s\",\n}\n`\n)\n\nvar (\n\tinitAppLang = cmdutil.Oneof{\n\t\tValue:     \"\",\n\t\tAllowed:   cmdutil.LanguageFlagValues(),\n\t\tFlag:      \"lang\",\n\t\tFlagShort: \"l\",\n\t\tDesc:      \"Programming language to use for the app\",\n\t\tTypeDesc:  \"string\",\n\t}\n)\n\n// Create a new app from scratch: `encore app create`\n// Link an existing app to an existing repo: `encore app link <app-id>`\n// Link an existing repo to a new app: `encore app init <name>`\nfunc init() {\n\tinitAppCmd := &cobra.Command{\n\t\tUse:   \"init [name]\",\n\t\tShort: \"Create a new Encore app from an existing repository\",\n\t\tArgs:  cobra.MaximumNArgs(1),\n\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tvar name string\n\t\t\tif len(args) > 0 {\n\t\t\t\tname = args[0]\n\t\t\t}\n\t\t\tif err := initializeApp(name); err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\t\t},\n\t}\n\n\tappCmd.AddCommand(initAppCmd)\n\tinitAppLang.AddFlag(initAppCmd)\n}\n\nfunc initializeApp(name string) error {\n\t// Check if encore.app file exists\n\t_, _, err := cmdutil.MaybeAppRoot()\n\tif errors.Is(err, cmdutil.ErrNoEncoreApp) {\n\t\t// expected\n\t} else if err != nil {\n\t\tcmdutil.Fatal(err)\n\t} else {\n\t\t// There is already an app here or in a parent directory.\n\t\tcmdutil.Fatal(\"an encore.app file already exists (here or in a parent directory)\")\n\t}\n\n\tcyan := color.New(color.FgCyan)\n\tpromptAccountCreation()\n\n\tname, _, lang, _ := createAppForm(name, \"\", cmdutil.Language(initAppLang.Value), llm_rules.LLMRulesToolNone, true)\n\n\tif err := validateName(name); err != nil {\n\t\treturn err\n\t}\n\n\tappSlug := \"\"\n\tappSlugComments := \"\"\n\t// Create the app on the server.\n\tif _, err := conf.CurrentUser(); err == nil {\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = \"Creating app on encore.dev \"\n\t\ts.Start()\n\n\t\tapp, err := createAppOnServer(name, exampleConfig{})\n\t\ts.Stop()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"creating app on encore.dev: %v\", err)\n\t\t}\n\t\tappSlug = app.Slug\n\t}\n\n\t// Create the encore.app file\n\tvar encoreAppTemplate = goEncoreAppData\n\tif lang == \"ts\" {\n\t\tencoreAppTemplate = tsEncoreAppData\n\t}\n\tif appSlug == \"\" {\n\t\tappSlugComments = strings.Join([]string{\n\t\t\t\"\",\n\t\t\t\"The app is not currently linked to the encore.dev platform.\",\n\t\t\t`Use \"encore app link\" to link it.`,\n\t\t}, \"\\n\\t//\")\n\t}\n\tencoreAppData := fmt.Appendf(nil, encoreAppTemplate, appSlugComments, appSlug)\n\tif err := xos.WriteFile(\"encore.app\", encoreAppData, 0644); err != nil {\n\t\treturn err\n\t}\n\n\t// Update to latest encore.dev release\n\tif _, err := os.Stat(\"go.mod\"); err == nil {\n\t\tlang = cmdutil.LanguageGo\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = \"Running go get encore.dev@latest\"\n\t\ts.Start()\n\t\tif err := gogetEncore(\".\"); err != nil {\n\t\t\ts.FinalMSG = fmt.Sprintf(\"failed, skipping: %v\", err.Error())\n\t\t}\n\t\ts.Stop()\n\t} else if _, err := os.Stat(\"package.json\"); err == nil {\n\t\tlang = cmdutil.LanguageTS\n\t\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\ts.Prefix = \"Running npm install encore.dev@latest\"\n\t\ts.Start()\n\t\tif err := npmInstallEncore(\".\"); err != nil {\n\t\t\ts.FinalMSG = fmt.Sprintf(\"failed, skipping: %v\", err.Error())\n\t\t}\n\t\ts.Stop()\n\t}\n\n\tgreen := color.New(color.FgGreen)\n\t_, _ = green.Fprint(os.Stdout, \"Successfully initialized application on Encore Cloud!\\n\")\n\tif appSlug == \"\" {\n\t\t_, _ = fmt.Fprintf(os.Stdout, \"The app is not currently linked to the encore.dev platform.\\n\")\n\t\t_, _ = fmt.Fprintf(os.Stdout, \"Use \\\"encore app link\\\" to link it.\\n\")\n\t\treturn nil\n\t}\n\t_, _ = fmt.Fprintf(os.Stdout, \"- App ID:          %s\\n\", cyan.Sprint(appSlug))\n\t_, _ = fmt.Fprintf(os.Stdout, \"- Cloud Dashboard: %s\\n\\n\", cyan.Sprintf(\"https://app.encore.cloud/%s\", appSlug))\n\n\treturn nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/app/link.go",
    "content": "package app\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/tailscale/hujson\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/pkg/xos\"\n)\n\nvar forceLink bool\nvar linkAppCmd = &cobra.Command{\n\tUse:   \"link [app-id]\",\n\tShort: \"Link an Encore app with the server\",\n\tArgs:  cobra.MaximumNArgs(1),\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tvar appID string\n\t\tif len(args) > 0 {\n\t\t\tappID = args[0]\n\t\t}\n\t\tlinkApp(appID, forceLink)\n\t},\n\tValidArgsFunction: cmdutil.AutoCompleteAppSlug,\n}\n\nfunc init() {\n\tappCmd.AddCommand(linkAppCmd)\n\tlinkAppCmd.Flags().BoolVarP(&forceLink, \"force\", \"f\", false, \"Force link even if the app is already linked.\")\n}\n\nfunc linkApp(appID string, force bool) {\n\t// Determine the app root.\n\troot, _, err := cmdutil.MaybeAppRoot()\n\tif errors.Is(err, cmdutil.ErrNoEncoreApp) {\n\t\troot, err = os.Getwd()\n\t}\n\tif err != nil {\n\t\tcmdutil.Fatal(err)\n\t}\n\n\tfilePath := filepath.Join(root, \"encore.app\")\n\tdata, err := os.ReadFile(filePath)\n\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\tcmdutil.Fatal(err)\n\t\tos.Exit(1)\n\t}\n\tif len(bytes.TrimSpace(data)) == 0 {\n\t\t// Treat missing and empty files as an empty object.\n\t\tdata = []byte(\"{}\")\n\t}\n\n\tval, err := hujson.Parse(data)\n\tif err != nil {\n\t\tcmdutil.Fatal(\"could not parse encore.app: \", err)\n\t}\n\n\tappData, ok := val.Value.(*hujson.Object)\n\tif !ok {\n\t\tcmdutil.Fatal(\"could not parse encore.app: expected JSON object\")\n\t}\n\n\t// Find the \"id\" value, if any.\n\tvar idValue *hujson.Value\n\tfor i := 0; i < len(appData.Members); i++ {\n\t\tkv := &appData.Members[i]\n\t\tlit, ok := kv.Name.Value.(hujson.Literal)\n\t\tif !ok || lit.String() != \"id\" {\n\t\t\tcontinue\n\t\t}\n\t\tidValue = &kv.Value\n\t}\n\n\tif idValue != nil {\n\t\tval, ok := idValue.Value.(hujson.Literal)\n\t\tif ok && val.String() != \"\" && val.String() != appID && !force {\n\t\t\tcmdutil.Fatal(\"the app is already linked.\\n\\nNote: to link to a different app, specify the --force flag.\")\n\t\t}\n\t}\n\n\tif appID == \"\" {\n\t\t// The app is not linked. Prompt the user for an app ID.\n\t\tfmt.Println(\"Make sure the app is created on app.encore.cloud, and then enter its ID to link it.\")\n\t\tfmt.Print(\"App ID: \")\n\t\tif _, err := fmt.Scanln(&appID); err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t} else if appID == \"\" {\n\t\t\tcmdutil.Fatal(\"no app id given.\")\n\t\t}\n\t}\n\n\tif linked, err := validateAppSlug(appID); err != nil {\n\t\tcmdutil.Fatal(err)\n\t} else if !linked {\n\t\tfmt.Fprintln(os.Stderr, \"Error: that app does not exist, or you don't have access to it.\")\n\t\tos.Exit(1)\n\t}\n\n\t// Write it back to our data structure.\n\tif idValue != nil {\n\t\tidValue.Value = hujson.String(appID)\n\t} else {\n\t\tappData.Members = append(appData.Members, hujson.ObjectMember{\n\t\t\tName:  hujson.Value{Value: hujson.String(\"id\")},\n\t\t\tValue: hujson.Value{Value: hujson.String(appID)},\n\t\t})\n\t}\n\n\tval.Format()\n\tif err := xos.WriteFile(filePath, val.Pack(), 0644); err != nil {\n\t\tcmdutil.Fatal(err)\n\t\tos.Exit(1)\n\t}\n\n\taddEncoreRemote(root, appID)\n\tfmt.Println(\"Successfully linked app!\")\n}\n\nfunc validateAppSlug(slug string) (ok bool, err error) {\n\tif _, err := conf.CurrentUser(); errors.Is(err, fs.ErrNotExist) {\n\t\tcmdutil.Fatal(\"not logged in. Run 'encore auth login' first.\")\n\t} else if err != nil {\n\t\treturn false, err\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tif _, err := platform.GetApp(ctx, slug); err != nil {\n\t\tvar e platform.Error\n\t\tif errors.As(err, &e) && e.HTTPCode == 404 {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/auth/auth.go",
    "content": "package auth\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\t\"encr.dev/cli/internal/login\"\n\t\"encr.dev/internal/conf\"\n)\n\nvar authKey string\n\nfunc init() {\n\tauthCmd := &cobra.Command{\n\t\tUse:   \"auth\",\n\t\tShort: \"Commands to authenticate with Encore\",\n\t}\n\n\tsignupCmd := &cobra.Command{\n\t\tUse:   \"signup\",\n\t\tShort: \"Create a new Encore account\",\n\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tif err := DoLogin(DeviceAuth); err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\t\t},\n\t}\n\n\tloginCmd := &cobra.Command{\n\t\tUse:   \"login [--auth-key=<KEY>]\",\n\t\tShort: \"Log in to Encore\",\n\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tif authKey != \"\" {\n\t\t\t\tif err := DoLoginWithAuthKey(); err != nil {\n\t\t\t\t\tcmdutil.Fatal(err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := DoLogin(DeviceAuth); err != nil {\n\t\t\t\t\tcmdutil.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n\n\tlogoutCmd := &cobra.Command{\n\t\tUse:   \"logout\",\n\t\tShort: \"Logs out the currently logged in user\",\n\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tDoLogout()\n\t\t},\n\t}\n\n\twhoamiCmd := &cobra.Command{\n\t\tUse:   \"whoami\",\n\t\tShort: \"Show the current logged in user\",\n\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tWhoami()\n\t\t},\n\t}\n\n\tauthCmd.AddCommand(signupCmd)\n\n\tauthCmd.AddCommand(loginCmd)\n\tloginCmd.Flags().StringVarP(&authKey, \"auth-key\", \"k\", \"\", \"Auth Key to use for login\")\n\n\tauthCmd.AddCommand(logoutCmd)\n\tauthCmd.AddCommand(whoamiCmd)\n\troot.Cmd.AddCommand(authCmd)\n}\n\ntype Flow int\n\nconst (\n\tAutoFlow Flow = iota\n\tInteractive\n\tDeviceAuth\n)\n\nfunc DoLogin(flow Flow) (err error) {\n\tvar fn func() (*conf.Config, error)\n\tswitch flow {\n\tcase Interactive:\n\t\tfn = login.Interactive\n\tcase DeviceAuth:\n\t\tfn = login.DeviceAuth\n\tdefault:\n\t\tfn = login.DecideFlow\n\t}\n\tcfg, err := fn()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := conf.Write(cfg); err != nil {\n\t\treturn fmt.Errorf(\"write credentials: %v\", err)\n\t}\n\tfmt.Fprintln(os.Stdout, \"Successfully logged in!\")\n\treturn nil\n}\n\nfunc DoLogout() {\n\tif err := conf.Logout(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"could not logout:\", err)\n\t\tos.Exit(1)\n\t}\n\t// Stop running daemon to clear any cached credentials\n\tcmdutil.StopDaemon()\n\tfmt.Fprintln(os.Stdout, \"encore: logged out.\")\n}\n\nfunc DoLoginWithAuthKey() error {\n\tcfg, err := login.WithAuthKey(authKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := conf.Write(cfg); err != nil {\n\t\treturn fmt.Errorf(\"write credentials: %v\", err)\n\t}\n\tfmt.Fprintln(os.Stdout, \"Successfully logged in!\")\n\treturn nil\n}\n\nfunc Whoami() {\n\tcfg, err := conf.CurrentUser()\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tfmt.Fprint(os.Stdout, \"not logged in.\", cmdutil.Newline)\n\t\t\treturn\n\t\t}\n\t\tcmdutil.Fatal(err)\n\t}\n\n\tif cfg.AppSlug != \"\" {\n\t\tfmt.Fprintf(os.Stdout, \"logged in as app %s%s\", cfg.AppSlug, cmdutil.Newline)\n\t} else {\n\t\tfmt.Fprintf(os.Stdout, \"logged in as %s%s\", cfg.Email, cmdutil.Newline)\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/bits/add.go",
    "content": "package bits\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/pkg/bits\"\n)\n\nvar addCmd = &cobra.Command{\n\tUse:   \"add <name> [<path>]\",\n\tShort: \"Add an Encore Bit to your application\",\n\tArgs:  cobra.MinimumNArgs(1),\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(c *cobra.Command, args []string) {\n\t\tslug := args[0]\n\t\tctx := context.Background()\n\t\tbit, err := bits.Get(ctx, slug)\n\t\tif errors.Is(err, errBitNotFound) {\n\t\t\tcmdutil.Fatalf(\"encore bit not found: %s\", slug)\n\t\t} else if err != nil {\n\t\t\tcmdutil.Fatalf(\"could not lookup encore bit: %v\", err)\n\t\t}\n\n\t\tworkdir, err := os.MkdirTemp(\"\", \"encore-bit\")\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t\tdefer os.RemoveAll(workdir)\n\n\t\t//prefix := args[0]\n\t\t//if len(args) > 1 {\n\t\t//\tprefix = args[1]\n\t\t//}\n\n\t\tfmt.Fprintf(os.Stderr, \"Downloading Encore Bit: %s\\n\", bit.Title)\n\t\tif err := bits.Extract(ctx, bit, workdir); err != nil {\n\t\t\tcmdutil.Fatalf(\"download failed: %v\", err)\n\t\t}\n\n\t\tmeta, err := bits.Describe(ctx, workdir)\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"could not parse bit metadata: %v\", err)\n\t\t}\n\n\t\tfmt.Fprintf(os.Stderr, \"successfully got bit: %+v\\n\", meta)\n\n\t\t//fmt.Fprintf(os.Stderr, \"\\n\\nSuccessfully added Encore Bit: %s!\\n\", bit.Title)\n\t\t//fmt.Fprintf(os.Stderr, \"You can find the new bit under the %s/ directory.\\n\", prefix)\n\t},\n}\n\nfunc init() {\n\tbitsCmd.AddCommand(addCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/bits/api.go",
    "content": "package bits\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\ntype Bit struct {\n\tID          int64\n\tSlug        string\n\tTitle       string\n\tDescription string\n\tGitRepo     string\n\tGitBranch   string\n}\n\ntype ListResponse struct {\n\tBits []*Bit\n}\n\nfunc List(ctx context.Context) ([]*Bit, error) {\n\tresp, err := http.Get(\"https://automativity.encore.dev/bits\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\tslurp, _ := io.ReadAll(resp.Body)\n\t\treturn nil, errors.Newf(\"got status %d: %s\", resp.StatusCode, slurp)\n\t}\n\tvar data ListResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&data); err != nil {\n\t\treturn nil, errors.Wrap(err, \"decode json response\")\n\t}\n\treturn data.Bits, nil\n}\n\nvar errBitNotFound = errors.New(\"bit not found\")\n\nfunc Get(ctx context.Context, slug string) (*Bit, error) {\n\tresp, err := http.Get(\"https://automativity.encore.dev/bits/\" + url.PathEscape(slug))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode == 404 {\n\t\treturn nil, errBitNotFound\n\t} else if resp.StatusCode != 200 {\n\t\tslurp, _ := io.ReadAll(resp.Body)\n\t\treturn nil, errors.Newf(\"got status %d: %s\", resp.StatusCode, slurp)\n\t}\n\tvar bit Bit\n\tif err := json.NewDecoder(resp.Body).Decode(&bit); err != nil {\n\t\treturn nil, errors.Wrap(err, \"decode json response\")\n\t}\n\treturn &bit, nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/bits/bits.go",
    "content": "package bits\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/root\"\n)\n\nvar bitsCmd = &cobra.Command{\n\tUse:   \"bits\",\n\tShort: \"Commands to manage encore bits, reusable functionality for Encore applications\",\n}\n\nfunc init() {\n\troot.Cmd.AddCommand(bitsCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/bits/list.go",
    "content": "package bits\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"text/tabwriter\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/pkg/bits\"\n)\n\nvar listCmd = &cobra.Command{\n\tUse:   \"list\",\n\tShort: \"Lists available Encore Bits to add to your application\",\n\tArgs:  cobra.ExactArgs(0),\n\tRun: func(c *cobra.Command, args []string) {\n\t\tbits, err := bits.List(context.Background())\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"could not list encore bits: %v\", err)\n\t\t}\n\n\t\ttw := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\\t', 0)\n\t\tfmt.Fprintln(tw, \"ID\\tTitle\\tDescription\")\n\t\tfor _, bit := range bits {\n\t\t\tfmt.Fprintf(tw, \"%s\\t%s\\t%s\\n\", bit.Slug, bit.Title, bit.Description)\n\t\t\tfmt.Fprintln(tw)\n\t\t}\n\t\ttw.Flush()\n\t},\n}\n\nfunc init() {\n\tbitsCmd.AddCommand(listCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/build.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/pkg/appfile\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar (\n\ttargetOS = cmdutil.Oneof{\n\t\tValue:   \"linux\",\n\t\tAllowed: []string{\"linux\"},\n\t\tFlag:    \"os\",\n\t\tDesc:    \"the target operating system\",\n\t}\n\ttargetArch = cmdutil.Oneof{\n\t\tValue:   \"amd64\",\n\t\tAllowed: []string{\"amd64\", \"arm64\"},\n\t\tFlag:    \"arch\",\n\t\tDesc:    \"the target architecture\",\n\t}\n)\n\nfunc init() {\n\tbuildCmd := &cobra.Command{\n\t\tUse:     \"build\",\n\t\tAliases: []string{\"eject\"},\n\t\tShort:   \"build provides ways to build your application for deployment\",\n\t}\n\n\tp := buildParams{\n\t\tCgoEnabled: os.Getenv(\"CGO_ENABLED\") == \"1\",\n\t}\n\tdockerBuildCmd := &cobra.Command{\n\t\tUse:   \"docker IMAGE_TAG\",\n\t\tShort: \"docker builds a portable docker image of your Encore application\",\n\t\tArgs:  cobra.ExactArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tp.Goarch = targetArch.Value\n\t\t\tp.Goos = targetOS.Value\n\t\t\tp.AppRoot, _ = determineAppRoot()\n\t\t\tp.WorkspaceRoot = determineWorkspaceRoot(p.AppRoot)\n\t\t\tfile, err := appfile.ParseFile(filepath.Join(p.AppRoot, appfile.Name))\n\t\t\tif err == nil {\n\t\t\t\tif !cmd.Flag(\"base\").Changed && file.Lang == appfile.LangTS {\n\t\t\t\t\tp.BaseImg = \"node:slim\"\n\t\t\t\t}\n\t\t\t\tif !cmd.Flag(\"cgo\").Changed {\n\t\t\t\t\tp.CgoEnabled = file.Build.CgoEnabled\n\t\t\t\t}\n\t\t\t}\n\t\t\tp.ImageTag = args[0]\n\t\t\tdockerBuild(p)\n\t\t},\n\t}\n\n\tdockerBuildCmd.Flags().BoolVarP(&p.Push, \"push\", \"p\", false, \"push image to remote repository\")\n\tdockerBuildCmd.Flags().StringVar(&p.BaseImg, \"base\", \"scratch\", \"base image to build from\")\n\tdockerBuildCmd.Flags().BoolVar(&p.CgoEnabled, \"cgo\", false, \"enable cgo\")\n\tdockerBuildCmd.Flags().BoolVar(&p.SkipInfraConf, \"skip-config\", false, \"do not read or generate a infra configuration file\")\n\tdockerBuildCmd.Flags().StringVar(&p.InfraConfPath, \"config\", \"\", \"infra configuration file path\")\n\tp.Services = dockerBuildCmd.Flags().StringSlice(\"services\", nil, \"services to include in the image\")\n\tp.Gateways = dockerBuildCmd.Flags().StringSlice(\"gateways\", nil, \"gateways to include in the image\")\n\ttargetOS.AddFlag(dockerBuildCmd)\n\ttargetArch.AddFlag(dockerBuildCmd)\n\trootCmd.AddCommand(buildCmd)\n\tbuildCmd.AddCommand(dockerBuildCmd)\n}\n\ntype buildParams struct {\n\tAppRoot       string\n\tWorkspaceRoot string\n\tImageTag      string\n\tPush          bool\n\tBaseImg       string\n\tGoos          string\n\tGoarch        string\n\tCgoEnabled    bool\n\tSkipInfraConf bool\n\tInfraConfPath string\n\tServices      *[]string\n\tGateways      *[]string\n}\n\nfunc dockerBuild(p buildParams) {\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\t<-interrupt\n\t\tcancel()\n\t}()\n\n\tdaemon := setupDaemon(ctx)\n\tparams := &daemonpb.DockerExportParams{\n\t\tBaseImageTag: p.BaseImg,\n\t}\n\tif p.Push {\n\t\tparams.PushDestinationTag = p.ImageTag\n\t} else {\n\t\tparams.LocalDaemonTag = p.ImageTag\n\t}\n\n\tvar services, gateways []string\n\tif p.Services != nil {\n\t\tservices = *p.Services\n\t}\n\tif p.Gateways != nil {\n\t\tgateways = *p.Gateways\n\t}\n\tvar err error\n\tcfgPath := \"\"\n\tif p.InfraConfPath != \"\" {\n\t\tcfgPath, err = filepath.Abs(p.InfraConfPath)\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"failed to resolve absolute path for %s: %v\", p.InfraConfPath, err)\n\t\t}\n\t}\n\tstream, err := daemon.Export(ctx, &daemonpb.ExportRequest{\n\t\tAppRoot:       p.AppRoot,\n\t\tWorkspaceRoot: p.WorkspaceRoot,\n\t\tCgoEnabled:    p.CgoEnabled,\n\t\tGoos:          p.Goos,\n\t\tGoarch:        p.Goarch,\n\t\tEnviron:       os.Environ(),\n\t\tFormat: &daemonpb.ExportRequest_Docker{\n\t\t\tDocker: params,\n\t\t},\n\t\tInfraConfPath: cfgPath,\n\t\tServices:      services,\n\t\tGateways:      gateways,\n\t\tSkipInfraConf: p.SkipInfraConf,\n\t})\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"fatal: \", err)\n\t\tos.Exit(1)\n\t}\n\tif code := cmdutil.StreamCommandOutput(stream, cmdutil.ConvertJSONLogs()); code != 0 {\n\t\tos.Exit(code)\n\t}\n}\n\nfunc or(a, b string) string {\n\tif a != \"\" {\n\t\treturn a\n\t}\n\treturn b\n}\n"
  },
  {
    "path": "cli/cmd/encore/check.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar (\n\tcodegenDebug    bool\n\tcheckParseTests bool\n)\n\nvar checkCmd = &cobra.Command{\n\tUse:   \"check\",\n\tShort: \"Checks your application for compile-time errors using Encore's compiler.\",\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tappRoot, relPath := determineAppRoot()\n\t\trunChecks(appRoot, relPath)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(checkCmd)\n\tcheckCmd.Flags().BoolVar(&codegenDebug, \"codegen-debug\", false, \"Dump generated code (for debugging Encore's code generation)\")\n\tcheckCmd.Flags().BoolVar(&checkParseTests, \"tests\", false, \"Parse tests as well\")\n}\n\nfunc runChecks(appRoot, relPath string) {\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\t<-interrupt\n\t\tcancel()\n\t}()\n\n\tdaemon := setupDaemon(ctx)\n\tstream, err := daemon.Check(ctx, &daemonpb.CheckRequest{\n\t\tAppRoot:      appRoot,\n\t\tWorkingDir:   relPath,\n\t\tCodegenDebug: codegenDebug,\n\t\tParseTests:   checkParseTests,\n\t\tEnviron:      os.Environ(),\n\t})\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"fatal: \", err)\n\t\tos.Exit(1)\n\t}\n\tos.Exit(cmdutil.StreamCommandOutput(stream, nil))\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/autocompletes.go",
    "content": "package cmdutil\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/internal/conf\"\n)\n\nfunc AutoCompleteFromStaticList(args ...string) func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\treturn func(cmd *cobra.Command, _ []string, toComplete string) (rtn []string, dir cobra.ShellCompDirective) {\n\t\ttoComplete = strings.ToLower(toComplete)\n\n\t\tfor _, option := range args {\n\t\t\tbefore, _, _ := strings.Cut(option, \"\\t\")\n\n\t\t\tif strings.HasPrefix(before, toComplete) {\n\t\t\t\trtn = append(rtn, option)\n\t\t\t}\n\t\t}\n\n\t\treturn rtn, cobra.ShellCompDirectiveNoFileComp\n\t}\n}\n\nfunc AutoCompleteAppSlug(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t// incase of not being logged in or an error, we give no auto competition\n\t_, err := conf.CurrentUser()\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveError\n\t}\n\n\tapps, err := platform.ListApps(cmd.Context())\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveError\n\t}\n\n\ttoComplete = strings.ToLower(toComplete)\n\n\trtn := make([]string, 0, len(apps))\n\tfor _, app := range apps {\n\t\tif strings.HasPrefix(strings.ToLower(app.Slug), toComplete) {\n\t\t\tdesc := app.Description\n\t\t\tif desc == \"\" {\n\t\t\t\tdesc = app.Name\n\t\t\t}\n\n\t\t\trtn = append(rtn, fmt.Sprintf(\"%s\\t%s\", app.Slug, desc))\n\t\t}\n\t}\n\n\treturn rtn, cobra.ShellCompDirectiveNoFileComp\n}\n\nfunc AutoCompleteEnvSlug(cmd *cobra.Command, args []string, toComplete string) (rtn []string, dir cobra.ShellCompDirective) {\n\ttoComplete = strings.ToLower(toComplete)\n\n\t// Support the local environment\n\tif strings.HasPrefix(\"local\", toComplete) {\n\t\trtn = append(rtn, \"local\\tThis local development environment\")\n\t}\n\n\t_, err := conf.CurrentUser()\n\tif err != nil {\n\t\treturn rtn, cobra.ShellCompDirectiveError\n\t}\n\n\t// Assume the app slug is the first argument\n\tappSlug := args[len(args)-1]\n\n\t// Get the environments for the app and filter by what the user has already entered\n\tenvs, err := platform.ListEnvs(cmd.Context(), appSlug)\n\tif err != nil {\n\t\treturn rtn, cobra.ShellCompDirectiveError\n\t}\n\n\tfor _, env := range envs {\n\t\tif strings.HasPrefix(strings.ToLower(env.Slug), toComplete) {\n\t\t\trtn = append(rtn, fmt.Sprintf(\"%s\\tA %s enviroment running on %s\", env.Slug, env.Type, env.Cloud))\n\t\t}\n\t}\n\n\treturn rtn, cobra.ShellCompDirectiveNoFileComp\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/cmdutil.go",
    "content": "package cmdutil\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/fatih/color\"\n\t\"golang.org/x/crypto/ssh/terminal\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/internal/manifest\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/errinsrc\"\n\t\"encr.dev/pkg/errlist\"\n)\n\nvar (\n\tErrNoEncoreApp    = errors.New(\"no encore.app found in directory (or any of the parent directories)\")\n\tErrEncoreAppIsDir = errors.New(\"encore.app is a directory, not a file\")\n)\n\n// MaybeAppRoot determines the app root by looking for the \"encore.app\" file,\n// initially in the current directory and then recursively in parent directories\n// up to the filesystem root.\n//\n// It reports the absolute path to the app root, and the\n// relative path from the app root to the working directory.\nfunc MaybeAppRoot() (appRoot, relPath string, err error) {\n\tdir, err := os.Getwd()\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\treturn FindAppRootFromDir(dir)\n}\n\nfunc FindAppRootFromDir(dir string) (appRoot, relPath string, err error) {\n\trel := \".\"\n\tfor {\n\t\tpath := filepath.Join(dir, \"encore.app\")\n\t\tfi, err := os.Stat(path)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\tdir2 := filepath.Dir(dir)\n\t\t\tif dir2 == dir {\n\t\t\t\treturn \"\", \"\", ErrNoEncoreApp\n\t\t\t}\n\t\t\trel = filepath.Join(filepath.Base(dir), rel)\n\t\t\tdir = dir2\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t} else if fi.IsDir() {\n\t\t\treturn \"\", \"\", ErrEncoreAppIsDir\n\t\t} else {\n\t\t\treturn dir, rel, nil\n\t\t}\n\t}\n}\n\n// AppRoot is like MaybeAppRoot but instead of returning an error\n// it prints it to stderr and exits.\nfunc AppRoot() (appRoot, relPath string) {\n\tappRoot, relPath, err := MaybeAppRoot()\n\tif err != nil {\n\t\tFatal(err)\n\t}\n\treturn appRoot, relPath\n}\n\n// WorkspaceRoot determines the workspace root by looking for the .git folder in app root or parents to it.\n// It reports the absolute path to the workspace root.\nfunc WorkspaceRoot(appRoot string) string {\n\tdir := appRoot\n\tfor {\n\t\tpath := filepath.Join(dir, \".git\")\n\t\tfi, err := os.Stat(path)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\tdir2 := filepath.Dir(dir)\n\t\t\tif dir2 == dir {\n\t\t\t\treturn appRoot\n\t\t\t}\n\t\t\tdir = dir2\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\tFatal(err)\n\t\t} else if !fi.IsDir() {\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn dir\n\t\t}\n\t}\n}\n\nfunc AppSlugOrLocalID() string {\n\tappRoot, _ := AppRoot()\n\tappID, _ := appfile.Slug(appRoot)\n\tif appID == \"\" {\n\t\tmf, err := manifest.ReadOrCreate(appRoot)\n\t\tif err != nil {\n\t\t\tFatalf(\"failed to read app manifest: %v\", err)\n\t\t}\n\t\tappID = mf.LocalID\n\t}\n\treturn appID\n}\n\n// AppSlug reports the current app's app slug.\n// It throws a fatal error if the app is not connected with the Encore Platform.\nfunc AppSlug() string {\n\tappRoot, _ := AppRoot()\n\tappSlug, err := appfile.Slug(appRoot)\n\tif err != nil {\n\t\tFatal(err)\n\t} else if appSlug == \"\" {\n\t\tFatal(\"app is not linked with the Encore Platform (see 'encore app link')\")\n\t}\n\treturn appSlug\n}\n\nfunc Fatal(args ...any) {\n\t// Prettify gRPC errors\n\tfor i, arg := range args {\n\t\tif err, ok := arg.(error); ok {\n\t\t\tif s, ok := status.FromError(err); ok {\n\t\t\t\targs[i] = s.Message()\n\t\t\t}\n\t\t}\n\t}\n\n\tred := color.New(color.FgRed)\n\t_, _ = red.Fprint(os.Stderr, \"error: \")\n\t_, _ = red.Fprintln(os.Stderr, args...)\n\tos.Exit(1)\n}\n\nfunc Fatalf(format string, args ...any) {\n\t// Prettify gRPC errors\n\tfor i, arg := range args {\n\t\tif err, ok := arg.(error); ok {\n\t\t\tif s, ok := status.FromError(err); ok {\n\t\t\t\targs[i] = s.Message()\n\t\t\t}\n\t\t}\n\t}\n\n\tFatal(fmt.Sprintf(format, args...))\n}\n\nfunc DisplayError(out *os.File, err []byte) {\n\tif len(err) == 0 {\n\t\treturn\n\t}\n\n\t// Get the width of the terminal we're rendering in\n\t// if we can so we render using the most space possible.\n\twidth, _, sizeErr := terminal.GetSize(int(out.Fd()))\n\tif sizeErr == nil {\n\t\terrinsrc.TerminalWidth = width\n\t}\n\n\t// Unmarshal the error into a structured errlist\n\terrList := errlist.New(nil)\n\tif err := json.Unmarshal(err, &errList); err != nil {\n\t\tFatalf(\"unable to parse error: %v\", err)\n\t}\n\n\tif errList.Len() == 0 {\n\t\treturn\n\t}\n\n\t_, _ = os.Stderr.Write([]byte(errList.Error()))\n}\n\nvar Newline string\n\nfunc init() {\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tNewline = \"\\r\\n\"\n\tdefault:\n\t\tNewline = \"\\n\"\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/daemon.go",
    "content": "package cmdutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/golang/protobuf/ptypes/empty\"\n\t\"google.golang.org/genproto/googleapis/rpc/errdetails\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/xos\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc IsDaemonRunning(ctx context.Context) bool {\n\tsocketPath, err := daemonSockPath()\n\tif err != nil {\n\t\treturn false\n\t}\n\tif _, err := xos.SocketStat(socketPath); err == nil {\n\t\t// The socket exists; check that it is responsive.\n\t\tif cc, err := dialDaemon(ctx, socketPath); err == nil {\n\t\t\t_ = cc.Close()\n\t\t\treturn true\n\t\t}\n\t\t// socket is not responding, remove it\n\t\t_ = os.Remove(socketPath)\n\t}\n\treturn false\n\n}\n\n// ConnectDaemon returns a client connection to the Encore daemon.\n// By default, it will start the daemon if it is not already running.\nfunc ConnectDaemon(ctx context.Context) daemonpb.DaemonClient {\n\tsocketPath, err := daemonSockPath()\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"fatal: \", err)\n\t\tos.Exit(1)\n\t}\n\n\tif _, err := xos.SocketStat(socketPath); err == nil {\n\t\t// The socket exists; check that it is responsive.\n\t\tif cc, err := dialDaemon(ctx, socketPath); err == nil {\n\t\t\t// Make sure the daemon is running an up-to-date version;\n\t\t\t// restart it otherwise.\n\t\t\tcl := daemonpb.NewDaemonClient(cc)\n\t\t\tif resp, err := cl.Version(ctx, &empty.Empty{}); err == nil {\n\t\t\t\tdiff := version.Compare(resp.Version)\n\t\t\t\tswitch {\n\t\t\t\tcase diff < 0:\n\t\t\t\t\t// Daemon is running a newer version\n\t\t\t\t\treturn cl\n\t\t\t\tcase diff == 0:\n\t\t\t\t\tif configHash, err := version.ConfigHash(); err != nil {\n\t\t\t\t\t\tFatal(\"unable to get config path: \", err)\n\t\t\t\t\t} else if configHash == resp.ConfigHash {\n\t\t\t\t\t\treturn cl\n\t\t\t\t\t}\n\n\t\t\t\t\t// If we're running a development release, and so is the daemon, don't restart.\n\t\t\t\t\t// This is to avoid spurious restarts during development.\n\t\t\t\t\tif version.Channel == version.DevBuild && version.ChannelFor(resp.Version) == version.DevBuild {\n\t\t\t\t\t\treturn cl\n\t\t\t\t\t}\n\n\t\t\t\t\t// Daemon is running the same version but different config\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"encore: restarting daemon due to configuration change.\\n\")\n\t\t\t\tcase diff > 0:\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"encore: daemon is running an outdated version (%s), restarting.\\n\", resp.Version)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Remove the socket file which triggers the daemon to exit.\n\t\t_ = os.Remove(socketPath)\n\t}\n\n\t// Start the daemon.\n\tif err := StartDaemonInBackground(ctx); err != nil {\n\t\tFatal(\"starting daemon: \", err)\n\t}\n\tcc, err := dialDaemon(ctx, socketPath)\n\tif err != nil {\n\t\tFatal(\"dialing daemon: \", err)\n\t}\n\treturn daemonpb.NewDaemonClient(cc)\n}\n\nfunc StopDaemon() {\n\tsocketPath, err := daemonSockPath()\n\tif err != nil {\n\t\tFatal(\"stopping daemon: \", err)\n\t}\n\tif _, err := xos.SocketStat(socketPath); err == nil {\n\t\t_ = os.Remove(socketPath)\n\t}\n}\n\n// daemonSockPath reports the path to the Encore daemon unix socket.\nfunc daemonSockPath() (string, error) {\n\tcacheDir, err := os.UserCacheDir()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not determine cache dir: %v\", err)\n\t}\n\treturn filepath.Join(cacheDir, \"encore\", \"encored.sock\"), nil\n}\n\n// StartDaemonInBackground starts the Encore daemon in the background.\nfunc StartDaemonInBackground(ctx context.Context) error {\n\tsocketPath, err := daemonSockPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// nosemgrep\n\texe, err := os.Executable()\n\tif err != nil {\n\t\texe, err = exec.LookPath(\"encore\")\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not determine location of encore executable: %v\", err)\n\t}\n\t// nosemgrep\n\tcmd := exec.Command(exe, \"daemon\", \"-f\")\n\tcmd.SysProcAttr = xos.CreateNewProcessGroup()\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"could not start encore daemon: %v\", err)\n\t}\n\n\t// Wait for it to come up\n\tfor i := 0; i < 50; i++ {\n\t\tif err := ctx.Err(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tif _, err := xos.SocketStat(socketPath); err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"timed out waiting for daemon to start\")\n}\n\nfunc dialDaemon(ctx context.Context, socketPath string) (*grpc.ClientConn, error) {\n\tctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)\n\tdefer cancel()\n\n\tdialer := func(ctx context.Context, addr string) (net.Conn, error) {\n\t\treturn (&net.Dialer{}).DialContext(ctx, \"unix\", socketPath)\n\t}\n\t// Set max message size to 16mb (up from default 4mb) for json formatted debug metadata for large applications.\n\treturn grpc.DialContext(ctx, \"\",\n\t\tgrpc.WithInsecure(),\n\t\tgrpc.WithBlock(),\n\t\tgrpc.WithUnaryInterceptor(errInterceptor),\n\t\tgrpc.WithContextDialer(dialer),\n\t\tgrpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16*1024*1024)),\n\t)\n}\n\nfunc errInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {\n\terr := invoker(ctx, method, req, reply, cc, opts...)\n\tif err != nil {\n\t\tif st, ok := status.FromError(err); ok {\n\t\t\tif st.Code() == codes.Unauthenticated {\n\t\t\t\tFatal(\"not logged in: run 'encore auth login' first\")\n\t\t\t}\n\t\t\tfor _, detail := range st.Details() {\n\t\t\t\tswitch t := detail.(type) {\n\t\t\t\tcase *errdetails.PreconditionFailure:\n\t\t\t\t\tfor _, violation := range t.Violations {\n\t\t\t\t\t\tif violation.Type == \"INVALID_REFRESH_TOKEN\" {\n\t\t\t\t\t\t\tFatal(\"OAuth refresh token was invalid. Please run `encore auth login` again.\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/forms.go",
    "content": "package cmdutil\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/list\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nconst (\n\tCodeBlue       = \"#6D89FF\"\n\tCodePurple     = \"#A36C8C\"\n\tCodeGreen      = \"#B3D77E\"\n\tValidationFail = \"#CB1010\"\n)\n\nvar (\n\tInputStyle   = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: CodeBlue, Light: CodeBlue})\n\tDescStyle    = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: CodeGreen, Light: CodePurple})\n\tDocStyle     = lipgloss.NewStyle().Padding(0, 2, 0, 2)\n\tErrorStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color(ValidationFail))\n\tSuccessStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#00C200\"))\n)\n\ntype SelectedID[T any] interface {\n\tSelectedID() T\n}\n\ntype Selectable interface {\n\tcomparable\n\tSelectPrompt() string\n}\n\ntype SimpleSelectDone[T any] struct {\n\tSelected T\n}\n\ntype SimpleSelectModel[T Selectable, S SelectedID[T]] struct {\n\tPredefined T\n\tList       list.Model\n}\n\nfunc (m SimpleSelectModel[T, S]) Selected() T {\n\tvar empty T\n\tif m.Predefined != empty {\n\t\treturn m.Predefined\n\t}\n\tsel := m.List.SelectedItem()\n\tif sel == nil {\n\t\treturn empty\n\t}\n\treturn sel.(S).SelectedID()\n}\n\nfunc (m SimpleSelectModel[T, I]) Update(msg tea.Msg) (SimpleSelectModel[T, I], tea.Cmd) {\n\tvar c tea.Cmd\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.Type {\n\t\tcase tea.KeyEnter:\n\t\t\t// Have we selected an item?\n\t\t\tif idx := m.List.Index(); idx >= 0 {\n\t\t\t\treturn m, func() tea.Msg {\n\t\t\t\t\treturn SimpleSelectDone[T]{\n\t\t\t\t\t\tSelected: m.Selected(),\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tm.List, c = m.List.Update(msg)\n\treturn m, c\n}\n\nfunc (m *SimpleSelectModel[T, I]) SetSize(width, height int) {\n\tm.List.SetWidth(width)\n\tm.List.SetHeight(max(height-1, 0))\n}\n\nfunc (m SimpleSelectModel[T, I]) View() string {\n\tvar b strings.Builder\n\n\t// Get the prompt from the type T\n\tvar zero T\n\tprompt := zero.SelectPrompt()\n\n\tb.WriteString(InputStyle.Render(prompt))\n\tb.WriteString(DescStyle.Render(\" [Use arrows to move]\"))\n\tb.WriteString(\"\\n\")\n\tb.WriteString(m.List.View())\n\n\treturn b.String()\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/language.go",
    "content": "package cmdutil\n\ntype Language string\n\nconst (\n\tLanguageGo Language = \"go\"\n\tLanguageTS Language = \"ts\"\n)\n\nvar AllLanguages = []Language{\n\tLanguageGo,\n\tLanguageTS,\n}\n\nfunc LanguageFlagValues() []string {\n\tresult := make([]string, 0, len(AllLanguages))\n\tfor _, r := range AllLanguages {\n\t\tresult = append(result, string(r))\n\t}\n\treturn result\n}\n\nfunc (lang Language) Display() string {\n\tswitch lang {\n\tcase LanguageGo:\n\t\treturn \"Go\"\n\tcase LanguageTS:\n\t\treturn \"TypeScript\"\n\tdefault:\n\t\treturn string(lang)\n\t}\n}\n\nfunc (lang Language) SelectPrompt() string {\n\treturn \"Select language for your application\"\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/output.go",
    "content": "package cmdutil\n\nimport (\n\t\"errors\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n)\n\ntype Oneof struct {\n\tValue       string\n\tAllowed     []string\n\tFlag        string // defaults to \"output\" if empty\n\tFlagShort   string // defaults to \"o\" if both Flag and FlagShort are empty\n\tDesc        string // usage desc\n\tTypeDesc    string // type description, defaults to the name of the flag\n\tNoOptDefVal string // default value when no option is provided\n}\n\nfunc (o *Oneof) AddFlag(cmd *cobra.Command) {\n\tname, short := o.FlagName()\n\tcmd.Flags().AddFlag(\n\t\t&pflag.Flag{\n\t\t\tName:        name,\n\t\t\tNoOptDefVal: o.NoOptDefVal,\n\t\t\tShorthand:   short,\n\t\t\tUsage:       o.Usage(),\n\t\t\tValue:       o,\n\t\t\tDefValue:    o.String(),\n\t\t})\n\t_ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\treturn o.Allowed, cobra.ShellCompDirectiveNoFileComp\n\t})\n}\n\nfunc (o *Oneof) FlagName() (name, short string) {\n\tname, short = o.Flag, o.FlagShort\n\tif name == \"\" {\n\t\tname, short = \"output\", \"o\"\n\t}\n\treturn name, short\n}\n\nfunc (o *Oneof) String() string {\n\treturn o.Value\n}\n\nfunc (o *Oneof) Type() string {\n\tif o.TypeDesc != \"\" {\n\t\treturn o.TypeDesc\n\t}\n\tname, _ := o.FlagName()\n\treturn name\n}\n\nfunc (o *Oneof) Set(v string) error {\n\tif slices.Contains(o.Allowed, v) {\n\t\to.Value = v\n\t\treturn nil\n\t}\n\n\tvar b strings.Builder\n\tb.WriteString(\"must be one of \")\n\to.oneOf(&b)\n\treturn errors.New(b.String())\n}\n\nfunc (o *Oneof) Usage() string {\n\tvar b strings.Builder\n\tdesc := o.Desc\n\tif desc == \"\" {\n\t\tdesc = \"Output format\"\n\t}\n\tb.WriteString(desc + \". One of (\")\n\to.oneOf(&b)\n\tb.WriteString(\").\")\n\treturn b.String()\n}\n\n// Alternatives lists the alternatives in the format \"a|b|c\".\nfunc (o *Oneof) Alternatives() string {\n\tvar b strings.Builder\n\tfor i, s := range o.Allowed {\n\t\tif i > 0 {\n\t\t\tb.WriteByte('|')\n\t\t}\n\t\tb.WriteString(s)\n\t}\n\treturn b.String()\n}\n\nfunc (o *Oneof) oneOf(b *strings.Builder) {\n\tn := len(o.Allowed)\n\tfor i, s := range o.Allowed {\n\t\tif i > 0 {\n\t\t\tswitch {\n\t\t\tcase n == 2:\n\t\t\t\tb.WriteString(\" or \")\n\t\t\tcase i == n-1:\n\t\t\t\tb.WriteString(\", or \")\n\t\t\tdefault:\n\t\t\t\tb.WriteString(\", \")\n\t\t\t}\n\t\t}\n\n\t\tb.WriteString(strconv.Quote(s))\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/cmdutil/stream.go",
    "content": "package cmdutil\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/crypto/ssh/terminal\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/pkg/ansi\"\n\t\"encr.dev/proto/encore/daemon\"\n)\n\n// CommandOutputStream is the interface for gRPC streams that\n// stream the output of a command.\ntype CommandOutputStream interface {\n\tRecv() (*daemon.CommandMessage, error)\n}\n\ntype OutputConverter func(line []byte) []byte\n\n// StreamCommandOutput streams the output from the given command stream,\n// and reports the command's exit code.\n// If convertJSON is true, lines that look like JSON are fed through\n// zerolog's console writer.\nfunc StreamCommandOutput(stream CommandOutputStream, converter OutputConverter) int {\n\tvar outWrite io.Writer = os.Stdout\n\tvar errWrite io.Writer = os.Stderr\n\n\tvar writesDone sync.WaitGroup\n\tdefer writesDone.Wait()\n\n\tif converter != nil {\n\t\t// Create a pipe that we read from line-by-line so we can detect JSON lines.\n\t\toutRead, outw := io.Pipe()\n\t\terrRead, errw := io.Pipe()\n\t\toutWrite = outw\n\t\terrWrite = errw\n\t\tdefer func() { _ = outw.Close() }()\n\t\tdefer func() { _ = errw.Close() }()\n\n\t\tfor i, read := range []io.Reader{outRead, errRead} {\n\t\t\tread := read\n\t\t\tstdout := i == 0\n\t\t\twritesDone.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer writesDone.Done()\n\n\t\t\t\tfor {\n\t\t\t\t\tscanner := bufio.NewScanner(read)\n\t\t\t\t\tfor scanner.Scan() {\n\t\t\t\t\t\tline := append(scanner.Bytes(), '\\n')\n\t\t\t\t\t\tline = converter(line)\n\t\t\t\t\t\tif stdout {\n\t\t\t\t\t\t\t_, _ = os.Stdout.Write(line)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_, _ = os.Stderr.Write(line)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif err := scanner.Err(); err != nil {\n\t\t\t\t\t\t// The scanner failed, likely due to a too-long line. Log an error\n\t\t\t\t\t\t// and create a new scanner since the old one is in an unrecoverable state.\n\t\t\t\t\t\tfmt.Fprintln(os.Stderr, \"failed to read output:\", err)\n\t\t\t\t\t\tscanner = bufio.NewScanner(read)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\n\tfor {\n\t\tmsg, err := stream.Recv()\n\t\tif err != nil {\n\t\t\tst := status.Convert(err)\n\t\t\tswitch {\n\t\t\tcase st.Code() == codes.FailedPrecondition:\n\t\t\t\t_, _ = fmt.Fprintln(os.Stderr, st.Message())\n\t\t\t\treturn 1\n\t\t\tcase err == io.EOF || st.Code() == codes.Canceled || strings.HasSuffix(err.Error(), \"error reading from server: EOF\"):\n\t\t\t\treturn 0\n\t\t\tdefault:\n\t\t\t\tlog.Fatal().Err(err).Msg(\"connection failure\")\n\t\t\t}\n\t\t}\n\n\t\tswitch m := msg.Msg.(type) {\n\t\tcase *daemon.CommandMessage_Output:\n\t\t\tif m.Output.Stdout != nil {\n\t\t\t\t_, _ = outWrite.Write(m.Output.Stdout)\n\t\t\t}\n\t\t\tif m.Output.Stderr != nil {\n\t\t\t\t_, _ = errWrite.Write(m.Output.Stderr)\n\t\t\t}\n\t\tcase *daemon.CommandMessage_Errors:\n\t\t\tDisplayError(os.Stderr, m.Errors.Errinsrc)\n\n\t\tcase *daemon.CommandMessage_Exit:\n\t\t\treturn int(m.Exit.Code)\n\t\t}\n\t}\n}\n\ntype ConvertLogOptions struct {\n\tColor bool\n}\n\ntype ConvertLogOption func(*ConvertLogOptions)\n\nfunc Colorize(enable bool) ConvertLogOption {\n\treturn func(clo *ConvertLogOptions) {\n\t\tclo.Color = enable\n\t}\n}\n\nfunc ConvertJSONLogs(opts ...ConvertLogOption) OutputConverter {\n\t// Default to colorized output.\n\toptions := ConvertLogOptions{Color: true}\n\n\tfor _, opt := range opts {\n\t\topt(&options)\n\t}\n\n\tvar logMutex sync.Mutex\n\tlogLineBuffer := bytes.NewBuffer(make([]byte, 0, 1024))\n\tcout := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {\n\t\tw.Out = logLineBuffer\n\t\tw.FieldsExclude = []string{\"stack\"}\n\t\tw.FormatExtra = func(vals map[string]any, buf *bytes.Buffer) error {\n\t\t\tif stack, ok := vals[\"stack\"]; ok {\n\t\t\t\treturn FormatStack(stack, buf)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t})\n\tif !options.Color {\n\t\tcout.NoColor = true\n\t}\n\n\treturn func(line []byte) []byte {\n\t\t// If this isn't a JSON log line, just return it as-is\n\t\tif len(line) == 0 || line[0] != '{' {\n\t\t\treturn line\n\t\t}\n\n\t\t// Otherwise grab the converter buffer and reset it\n\t\tlogMutex.Lock()\n\t\tdefer logMutex.Unlock()\n\t\tlogLineBuffer.Reset()\n\n\t\t// Then convert the JSON log line to pretty formatted text\n\t\t_, err := cout.Write(line)\n\t\tif err != nil {\n\t\t\treturn line\n\t\t}\n\t\tout := make([]byte, len(logLineBuffer.Bytes()))\n\t\tcopy(out, logLineBuffer.Bytes())\n\t\treturn out\n\t}\n}\n\nfunc FormatStack(val any, buf *bytes.Buffer) error {\n\tvar frames []struct {\n\t\tFile string\n\t\tLine int\n\t\tFunc string\n\t}\n\n\tif jsonRepr, err := json.Marshal(val); err != nil {\n\t\treturn err\n\t} else if err := json.Unmarshal(jsonRepr, &frames); err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range frames {\n\t\tfmt.Fprintf(buf, \"\\n    %s\\n        %s\",\n\t\t\tf.Func,\n\t\t\taurora.Gray(12, fmt.Sprintf(\"%s:%d\", f.File, f.Line)))\n\t}\n\treturn nil\n}\n\nfunc ClearTerminalExceptFirstNLines(n int) {\n\t// Clear the screen except for the first line.\n\tif _, height, err := terminal.GetSize(int(os.Stdout.Fd())); err == nil {\n\t\tcount := height - (1 + n)\n\t\tif count > 0 {\n\t\t\t_, _ = os.Stdout.Write(bytes.Repeat([]byte{'\\n'}, count))\n\t\t}\n\t\t_, _ = fmt.Fprint(os.Stdout, ansi.SetCursorPosition(2, 1)+ansi.ClearScreen(ansi.CursorToBottom))\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/config/config.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\t\"encr.dev/internal/userconfig\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tforceApp, forceGlobal bool\n\tviewAllSettings       bool\n)\n\nvar autoCompleteConfigKeys = cmdutil.AutoCompleteFromStaticList(userconfig.Keys()...)\n\nvar longDocs = `Gets or sets configuration values for customizing the behavior of the Encore CLI.\n\nConfiguration options can be set both for individual Encore applications,\nas well as globally for the local user.\n\nConfiguration options can be set using ` + bt(\"encore config <key> <value>\") + `,\nand options can similarly be read using ` + bt(\"encore config <key>\") + `.\n\nWhen running ` + bt(\"encore config\") + ` within an Encore application,\nit automatically sets and gets configuration for that application.\n\nTo set or get global configuration, use the ` + bt(\"--global\") + ` flag.\n\nAvailable configuration settings are:\n\n` + userconfig.CLIDocs()\n\nvar configCmd = &cobra.Command{\n\tUse:   \"config <key> [<value>]\",\n\tShort: \"Get or set a configuration value\",\n\tLong:  longDocs,\n\tArgs:  cobra.RangeArgs(0, 2),\n\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tappRoot, _, _ := cmdutil.MaybeAppRoot()\n\n\t\tappScope := appRoot != \"\"\n\t\tif forceApp {\n\t\t\tappScope = true\n\t\t} else if forceGlobal {\n\t\t\tappScope = false\n\t\t}\n\n\t\tif appScope && appRoot == \"\" {\n\t\t\t// If the user specified --app, error if there is no app.\n\t\t\tcmdutil.Fatal(cmdutil.ErrNoEncoreApp)\n\t\t}\n\n\t\tif len(args) == 2 {\n\t\t\tvar err error\n\t\t\tif appScope {\n\t\t\t\terr = userconfig.SetForApp(appRoot, args[0], args[1])\n\t\t\t} else {\n\t\t\t\terr = userconfig.SetGlobal(args[0], args[1])\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\t\t} else {\n\t\t\tvar (\n\t\t\t\tcfg *userconfig.Config\n\t\t\t\terr error\n\t\t\t)\n\t\t\tif appScope {\n\t\t\t\tappRoot, _ := cmdutil.AppRoot()\n\t\t\t\tcfg, err = userconfig.ForApp(appRoot).Get()\n\t\t\t} else {\n\t\t\t\tcfg, err = userconfig.Global().Get()\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\n\t\t\tif viewAllSettings {\n\t\t\t\tif len(args) > 0 {\n\t\t\t\t\tcmdutil.Fatalf(\"cannot specify a settings key when using --all\")\n\t\t\t\t}\n\t\t\t\ts := strings.TrimSuffix(cfg.Render(), \"\\n\")\n\t\t\t\tfmt.Println(s)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif len(args) == 0 {\n\t\t\t\t// No args are only allowed when --all is specified.\n\t\t\t\t_ = cmd.Usage()\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\n\t\t\tval, ok := cfg.GetByKey(args[0])\n\t\t\tif !ok {\n\t\t\t\tcmdutil.Fatalf(\"unknown key %q\", args[0])\n\t\t\t}\n\t\t\tfmt.Printf(\"%v\\n\", val)\n\t\t}\n\t},\n\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif len(args) == 0 {\n\t\t\t// Completing the first argument, the config key\n\t\t\treturn autoCompleteConfigKeys(cmd, args, toComplete)\n\t\t}\n\t\treturn nil, cobra.ShellCompDirectiveNoFileComp\n\t},\n}\n\nfunc init() {\n\tconfigCmd.Flags().BoolVar(&viewAllSettings, \"all\", false, \"view all settings\")\n\tconfigCmd.Flags().BoolVar(&forceApp, \"app\", false, \"set the value for the current app\")\n\tconfigCmd.Flags().BoolVar(&forceGlobal, \"global\", false, \"set the value at the global level\")\n\tconfigCmd.MarkFlagsMutuallyExclusive(\"app\", \"global\")\n\n\troot.Cmd.AddCommand(configCmd)\n}\n\n// bt renders a backtick-enclosed string.\nfunc bt(val string) string {\n\treturn fmt.Sprintf(\"`%s`\", val)\n}\n"
  },
  {
    "path": "cli/cmd/encore/daemon/daemon.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"embed\"\n\t_ \"embed\" // for go:embed\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/pprof\"\n\t\"net/netip\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang-migrate/migrate/v4\"\n\t\"github.com/golang-migrate/migrate/v4/database\"\n\t\"github.com/golang-migrate/migrate/v4/database/sqlite3\"\n\t\"github.com/golang-migrate/migrate/v4/source/iofs\"\n\t_ \"github.com/mattn/go-sqlite3\" // for \"sqlite3\" driver\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/genproto/googleapis/rpc/errdetails\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/daemon\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/dash\"\n\t\"encr.dev/cli/daemon/engine\"\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/cli/daemon/engine/trace2/sqlite\"\n\t\"encr.dev/cli/daemon/mcp\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/objects\"\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/daemon/secret\"\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/cli/daemon/sqldb/docker\"\n\t\"encr.dev/cli/daemon/sqldb/external\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/pkg/eerror\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/watcher\"\n\t\"encr.dev/pkg/xos\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Main runs the daemon.\nfunc Main() {\n\twatcher.BumpRLimitSoftToHardLimit()\n\n\tif err := redirectLogOutput(); err != nil {\n\t\tlog.Error().Err(err).Msg(\"could not setup daemon log file, skipping\")\n\t}\n\tif err := runMain(); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"daemon failed\")\n\t}\n}\n\nfunc runMain() (err error) {\n\tctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT)\n\tdefer cancel()\n\n\t// exit receives signals from the different subsystems\n\t// that something went wrong and it's time to exit.\n\t// Sending nil indicates it's time to gracefully exit.\n\texit := make(chan error)\n\n\td := &Daemon{dev: conf.DevDaemon, exit: exit}\n\tdefer handleBailout(&err)\n\tdefer d.closeAll()\n\n\td.init(ctx)\n\td.serve()\n\n\tselect {\n\tcase err := <-exit:\n\t\treturn err\n\tcase <-ctx.Done():\n\t\treturn nil\n\t}\n}\n\n// Daemon orchestrates setting up the different daemon subsystems.\ntype Daemon struct {\n\tDaemon        *net.UnixListener\n\tRuntime       *retryingTCPListener\n\tDBProxy       *retryingTCPListener\n\tDash          *retryingTCPListener\n\tDebug         *retryingTCPListener\n\tObjectStorage *retryingTCPListener\n\tMCP           *retryingTCPListener\n\tEncoreDB      *sql.DB\n\n\tApps          *apps.Manager\n\tSecret        *secret.Manager\n\tRunMgr        *run.Manager\n\tNS            *namespace.Manager\n\tClusterMgr    *sqldb.ClusterManager\n\tObjectsMgr    *objects.ClusterManager\n\tMCPMgr        *mcp.Manager\n\tPublicBuckets *objects.PublicBucketServer\n\tTrace         trace2.Store\n\tServer        *daemon.Server\n\tdev           bool // whether we're in development mode\n\n\t// exit is a channel that shuts down the daemon when sent on.\n\t// A nil error indicates graceful exit.\n\texit chan<- error\n\n\t// close are the things to close when exiting.\n\tclose []io.Closer\n}\n\nfunc (d *Daemon) init(ctx context.Context) {\n\td.Daemon = d.listenDaemonSocket()\n\td.Dash = d.listenTCPRetry(\"dashboard\", env.EncoreDevDashListenAddr(), 9400)\n\td.DBProxy = d.listenTCPRetry(\"dbproxy\", option.None[string](), 9500)\n\td.Runtime = d.listenTCPRetry(\"runtime\", option.None[string](), 9600)\n\td.Debug = d.listenTCPRetry(\"debug\", option.None[string](), 9700)\n\td.ObjectStorage = d.listenTCPRetry(\"objectstorage\", env.EncoreObjectStorageListAddr(), 9800)\n\td.MCP = d.listenTCPRetry(\"mcp\", env.EncoreMCPSSEListenAddr(), 9900)\n\td.EncoreDB = d.openDB()\n\n\td.Apps = apps.NewManager(d.EncoreDB)\n\td.close = append(d.close, d.Apps)\n\n\t// If ENCORE_SQLDB_HOST is set, use the external cluster instead of\n\t// creating our own docker container cluster.\n\tvar sqldbDriver sqldb.Driver = &docker.Driver{}\n\tif host := os.Getenv(\"ENCORE_SQLDB_HOST\"); host != \"\" {\n\t\tsqldbDriver = &external.Driver{\n\t\t\tHost:              host,\n\t\t\tDatabase:          os.Getenv(\"ENCORE_SQLDB_DATABASE\"),\n\t\t\tSuperuserUsername: os.Getenv(\"ENCORE_SQLDB_USER\"),\n\t\t\tSuperuserPassword: os.Getenv(\"ENCORE_SQLDB_PASSWORD\"),\n\t\t}\n\t\tlog.Info().Msgf(\"using external postgres cluster: %s\", host)\n\t}\n\n\td.NS = namespace.NewManager(d.EncoreDB)\n\td.Secret = secret.New()\n\td.ClusterMgr = sqldb.NewClusterManager(sqldbDriver, d.Apps, d.NS, d.Secret)\n\td.ObjectsMgr = objects.NewClusterManager(d.NS)\n\td.PublicBuckets = objects.NewPublicBucketServer(\"http://\"+d.ObjectStorage.ClientAddr(), d.ObjectsMgr.PersistentStoreFallback)\n\n\ttraceStore := sqlite.New(d.EncoreDB)\n\tgo traceStore.CleanEvery(ctx, 1*time.Minute, 500, 100, 10000)\n\td.Trace = traceStore\n\n\td.RunMgr = &run.Manager{\n\t\tRuntimePort:   d.Runtime.Port(),\n\t\tDBProxyPort:   d.DBProxy.Port(),\n\t\tDashBaseURL:   fmt.Sprintf(\"http://%s\", d.Dash.ClientAddr()),\n\t\tSecret:        d.Secret,\n\t\tClusterMgr:    d.ClusterMgr,\n\t\tObjectsMgr:    d.ObjectsMgr,\n\t\tPublicBuckets: d.PublicBuckets,\n\t}\n\td.MCPMgr = mcp.NewManager(\n\t\td.Apps,\n\t\td.ClusterMgr,\n\t\td.NS,\n\t\td.Trace,\n\t\td.RunMgr,\n\t\tfmt.Sprintf(\"http://%s\", d.MCP.ClientAddr()),\n\t)\n\n\t// Register namespace deletion handlers.\n\td.NS.RegisterDeletionHandler(d.ClusterMgr)\n\td.NS.RegisterDeletionHandler(d.RunMgr)\n\td.NS.RegisterDeletionHandler(d.ObjectsMgr)\n\n\td.Server = daemon.New(d.Apps, d.RunMgr, d.ClusterMgr, d.Secret, d.NS, d.MCPMgr)\n}\n\nfunc (d *Daemon) serve() {\n\tgo d.serveDaemon()\n\tgo d.serveRuntime()\n\tgo d.serveDBProxy()\n\tgo d.serveDash()\n\tgo d.serveDebug()\n\tgo d.serveObjects()\n\tgo d.serveMCP()\n}\n\n// listenDaemonSocket listens on the encored.sock UNIX socket\n// and arranges to exit when the socket is closed.\nfunc (d *Daemon) listenDaemonSocket() *net.UnixListener {\n\tuserCacheDir, err := os.UserCacheDir()\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\tsocketPath := filepath.Join(userCacheDir, \"encore\", \"encored.sock\")\n\tif err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {\n\t\tfatal(err)\n\t}\n\n\t// If the daemon socket already exists, remove it so we can take over listening.\n\tif _, err := xos.SocketStat(socketPath); err == nil {\n\t\t_ = os.Remove(socketPath)\n\t}\n\tln, err := net.ListenUnix(\"unix\", &net.UnixAddr{Name: socketPath, Net: \"unix\"})\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\td.closeOnExit(ln)\n\n\t// Detect when the socket is closed.\n\tgo func() {\n\t\td.exit <- detectSocketClose(ln, socketPath)\n\t}()\n\treturn ln\n}\n\nfunc failedPreconditionError(msg, typ, desc string) error {\n\tst, err := status.New(codes.FailedPrecondition, msg).WithDetails(\n\t\t&errdetails.PreconditionFailure{\n\t\t\tViolations: []*errdetails.PreconditionFailure_Violation{\n\t\t\t\t{\n\t\t\t\t\tType:        typ,\n\t\t\t\t\tDescription: desc,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn st.Err()\n}\n\nfunc ErrInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {\n\tresp, err = handler(ctx, req)\n\tif errors.Is(err, conf.ErrInvalidRefreshToken) {\n\t\treturn nil, failedPreconditionError(\"invalid refresh token\", \"INVALID_REFRESH_TOKEN\", \"invalid refresh token\")\n\t} else if errors.Is(err, conf.ErrNotLoggedIn) {\n\t\treturn nil, status.Error(codes.Unauthenticated, \"not logged in\")\n\t}\n\treturn resp, err\n}\n\nfunc (d *Daemon) serveDaemon() {\n\tlog.Info().Stringer(\"addr\", d.Daemon.Addr()).Msg(\"serving daemon\")\n\tsrv := grpc.NewServer(grpc.UnaryInterceptor(ErrInterceptor))\n\tdaemonpb.RegisterDaemonServer(srv, d.Server)\n\td.exit <- srv.Serve(d.Daemon)\n}\n\nfunc (d *Daemon) serveRuntime() {\n\tlog.Info().Stringer(\"addr\", d.Runtime.Addr()).Msg(\"serving runtime\")\n\trec := trace2.NewRecorder(d.Trace)\n\tsrv := engine.NewServer(d.RunMgr, rec)\n\td.exit <- http.Serve(d.Runtime, srv)\n}\n\nfunc (d *Daemon) serveDBProxy() {\n\tlog.Info().Stringer(\"addr\", d.DBProxy.Addr()).Msg(\"serving dbproxy\")\n\td.exit <- d.ClusterMgr.ServeProxy(d.DBProxy)\n}\n\nfunc (d *Daemon) serveMCP() {\n\tlog.Info().Stringer(\"addr\", d.MCP.Addr()).Msg(\"serving mcp\")\n\td.exit <- d.MCPMgr.Serve(d.MCP)\n}\n\nfunc (d *Daemon) serveObjects() {\n\tlog.Info().Stringer(\"addr\", d.ObjectStorage.Addr()).Msg(\"serving object storage\")\n\td.exit <- d.PublicBuckets.Serve(d.ObjectStorage)\n}\n\nfunc (d *Daemon) serveDash() {\n\tlog.Info().Stringer(\"addr\", d.Dash.Addr()).Msg(\"serving dash\")\n\tsrv := dash.NewServer(d.Apps, d.RunMgr, d.NS, d.Trace, d.Dash.Port())\n\td.exit <- http.Serve(d.Dash, srv)\n}\n\nfunc (d *Daemon) serveDebug() {\n\tlog.Info().Stringer(\"addr\", d.Debug.Addr()).Msg(\"serving debug\")\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/debug/pprof/\", pprof.Index)\n\tmux.HandleFunc(\"/debug/pprof/cmdline\", pprof.Cmdline)\n\tmux.HandleFunc(\"/debug/pprof/profile\", pprof.Profile)\n\tmux.HandleFunc(\"/debug/pprof/symbol\", pprof.Symbol)\n\tmux.HandleFunc(\"/debug/pprof/trace\", pprof.Trace)\n\td.exit <- http.Serve(d.Debug, mux)\n}\n\n// listenTCPRetry listens for TCP connections on the given port, retrying\n// in the background if it's already in use.\nfunc (d *Daemon) listenTCPRetry(component string, addrOverride option.Option[string], defaultPort uint16) *retryingTCPListener {\n\taddr, err := parseInterface(addrOverride.GetOrElse(\"127.0.0.1:0\"))\n\tif err != nil {\n\t\tlog.Fatal().Str(\"component\", component).Err(err).Msg(\"failed to parse interface\")\n\t}\n\tif addr.Port() == 0 {\n\t\taddr = netip.AddrPortFrom(addr.Addr(), defaultPort)\n\t}\n\tln := listenLocalhostTCP(component, addr)\n\td.closeOnExit(ln)\n\treturn ln\n}\n\nfunc (d *Daemon) openDB() *sql.DB {\n\tdir, err := conf.Dir()\n\tif err != nil {\n\t\tfatal(err)\n\t} else if err := os.MkdirAll(dir, 0755); err != nil {\n\t\tfatal(err)\n\t}\n\n\tdbPath := filepath.Join(dir, \"encore.db\")\n\n\t// Create the database file if it doesn't exist, as\n\t// we've observed some failures to open the database file when it doesn't already exist.\n\tif _, err := os.Stat(dbPath); os.IsNotExist(err) {\n\t\tif f, err := os.OpenFile(dbPath, os.O_CREATE|os.O_WRONLY, 0600); err == nil {\n\t\t\t_ = f.Close()\n\t\t}\n\t}\n\n\tdb, err := sql.Open(\"sqlite3\", fmt.Sprintf(\"file:%s?cache=shared&_journal=wal&_txlock=immediate\", dbPath))\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\n\t// Initialize db schema\n\tif err := d.runDBMigrations(db); err != nil {\n\t\tfatalf(\"unable to migrate management database: %v\", err)\n\t}\n\td.closeOnExit(db)\n\n\treturn db\n}\n\n//go:embed migrations\nvar dbMigrations embed.FS\n\nfunc (d *Daemon) runDBMigrations(db *sql.DB) error {\n\t{\n\t\t// Convert old-style schema definition to golang-migrate, if necessary.\n\t\tvar isLegacy bool\n\t\terr := db.QueryRow(`\n\t\t\tSELECT COUNT(*) > 0 FROM pragma_table_info('schema_migrations') WHERE name = 'dummy'\n\t\t`).Scan(&isLegacy)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t} else if isLegacy {\n\t\t\t_, _ = db.Exec(\"DROP TABLE schema_migrations;\")\n\t\t}\n\t}\n\n\tsrc, err := iofs.New(dbMigrations, \"migrations\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read db migrations: %v\", err)\n\t}\n\tinstance, err := sqlite3.WithInstance(db, &sqlite3.Config{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initialize migration instance: %v\", err)\n\t}\n\tm, err := migrate.NewWithInstance(\"iofs\", src, \"encore\", instance)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"setup migrate instance: %v\", err)\n\t}\n\n\terr = m.Up()\n\tif errors.Is(err, migrate.ErrNoChange) {\n\t\treturn nil\n\t}\n\n\t// If we have a dirty migration, reset the dirty flag and try again.\n\t// This is safe since all migrations run inside transactions.\n\tvar dirty migrate.ErrDirty\n\tif errors.As(err, &dirty) {\n\t\t// Find the version that preceded the dirty version so\n\t\t// we can force the migration to that version and then\n\t\t// re-apply the migration.\n\t\tvar prevVer uint\n\t\tprevVer, err = src.Prev(uint(dirty.Version))\n\t\ttargetVer := int(prevVer)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// No previous migration exists\n\t\t\ttargetVer = database.NilVersion\n\t\t} else if err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to find previous version\")\n\t\t}\n\n\t\tif err = m.Force(targetVer); err == nil {\n\t\t\terr = m.Up()\n\t\t}\n\t}\n\n\treturn err\n}\n\n// detectSocketClose polls for the unix socket at socketPath to be removed\n// or changed to a different underlying inode.\nfunc detectSocketClose(ln *net.UnixListener, socketPath string) error {\n\torig, err := xos.SocketStat(socketPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// When this function exits, the socket has been changed.\n\t// In that case, don't unlink the socket since it has already been changed.\n\tdefer ln.SetUnlinkOnClose(false)\n\n\t// Sleep until the socket changes\n\terrs := 0\n\tfor {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\tfi, err := xos.SocketStat(socketPath)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// Socket was removed; don't remove it again\n\t\t\treturn nil\n\t\t} else if err != nil {\n\t\t\terrs++\n\t\t\tif errs == 3 {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tif !xos.SameSocket(orig, fi) {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (d *Daemon) closeOnExit(c io.Closer) {\n\td.close = append(d.close, c)\n}\n\nfunc (d *Daemon) closeAll() {\n\tfor _, c := range d.close {\n\t\t_ = c.Close()\n\t}\n}\n\ntype bailout struct {\n\terr error\n}\n\nfunc fatal(err error) {\n\tpanic(bailout{err})\n}\n\nfunc fatalf(format string, args ...interface{}) {\n\tpanic(bailout{fmt.Errorf(format, args...)})\n}\n\nfunc handleBailout(err *error) {\n\tif e := recover(); e != nil {\n\t\tif b, ok := e.(bailout); ok {\n\t\t\t*err = b.err\n\t\t} else {\n\t\t\tpanic(e)\n\t\t}\n\t}\n}\n\n// redirectLogOutput redirects the global logger to also write to a file.\nfunc redirectLogOutput() error {\n\tlogPath := env.EncoreDaemonLogPath()\n\tif err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {\n\t\treturn err\n\t}\n\tf, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlog.Info().Msgf(\"writing output to %s\", logPath)\n\n\tzerolog.TimeFieldFormat = time.RFC3339Nano\n\tconsoleWriter := zerolog.ConsoleWriter{\n\t\tOut:           os.Stderr,\n\t\tFieldsExclude: []string{zerolog.ErrorStackFieldName},\n\t}\n\tconsoleWriter.FormatExtra = eerror.ZeroLogConsoleExtraFormatter\n\tconsoleWriter.TimeFormat = time.TimeOnly\n\tzerolog.ErrorStackMarshaler = eerror.ZeroLogStackMarshaller\n\tlog.Logger = log.With().Caller().Stack().Logger().Output(io.MultiWriter(consoleWriter, f))\n\treturn nil\n}\n\n// retryingTCPListener is a TCP listener that attempts multiple times\n// to listen on a given port. It is designed to handle race conditions\n// between multiple daemon processes handing off to each other\n// and the port still being in use momentarily.\ntype retryingTCPListener struct {\n\tcomponent string\n\taddr      netip.AddrPort\n\tctx       context.Context\n\tcancel    func() // call to cancel ctx\n\n\t// doneListening is closed when the underlying listener is open,\n\t// or it gave up due to an error.\n\tdoneListening chan struct{}\n\tunderlying    net.Listener\n\tlistenErr     error\n}\n\nfunc listenLocalhostTCP(component string, addr netip.AddrPort) *retryingTCPListener {\n\tctx, cancel := context.WithCancel(context.Background())\n\tln := &retryingTCPListener{\n\t\tcomponent:     component,\n\t\taddr:          addr,\n\t\tctx:           ctx,\n\t\tcancel:        cancel,\n\t\tdoneListening: make(chan struct{}),\n\t}\n\tgo ln.listen()\n\treturn ln\n}\n\nfunc (ln *retryingTCPListener) Accept() (net.Conn, error) {\n\tselect {\n\tcase <-ln.ctx.Done():\n\t\treturn nil, net.ErrClosed\n\tcase <-ln.doneListening:\n\t\tif ln.listenErr != nil {\n\t\t\treturn nil, ln.listenErr\n\t\t}\n\t\treturn ln.underlying.Accept()\n\t}\n}\n\nfunc (ln *retryingTCPListener) Close() error {\n\tln.cancel()\n\tselect {\n\tcase <-ln.doneListening:\n\t\tif ln.listenErr == nil {\n\t\t\treturn ln.underlying.Close()\n\t\t}\n\tdefault:\n\t}\n\treturn nil\n}\n\nfunc (ln *retryingTCPListener) Addr() net.Addr {\n\treturn &net.TCPAddr{IP: net.IP(ln.addr.Addr().AsSlice()), Port: int(ln.addr.Port())}\n}\n\nfunc (ln *retryingTCPListener) ClientAddr() string {\n\t// If our addr is 0.0.0.0 or the ipv6 equivalent, return 127.0.0.1 instead\n\t// so that clients can connect to us.\n\tif ln.addr.Addr().IsUnspecified() {\n\t\tif ln.addr.Addr().Is6() {\n\t\t\treturn fmt.Sprintf(\"[::1]:%d\", ln.addr.Port())\n\t\t}\n\t\treturn fmt.Sprintf(\"127.0.0.1:%d\", ln.addr.Port())\n\t}\n\treturn ln.addr.String()\n}\n\nfunc (ln *retryingTCPListener) Port() int {\n\treturn int(ln.addr.Port())\n}\n\nfunc (ln *retryingTCPListener) listen() {\n\tdefer close(ln.doneListening)\n\n\tlogger := log.With().Str(\"component\", ln.component).Int(\"port\", ln.Port()).Logger()\n\taddr := ln.addr.String()\n\n\tb := backoff.NewExponentialBackOff()\n\tb.InitialInterval = 50 * time.Millisecond\n\tb.MaxInterval = 500 * time.Millisecond\n\tb.MaxElapsedTime = 5 * time.Second\n\n\tln.listenErr = backoff.Retry(func() (err error) {\n\t\tif err := ln.ctx.Err(); err != nil {\n\t\t\treturn backoff.Permanent(err)\n\t\t}\n\t\tln.underlying, err = net.Listen(\"tcp\", addr)\n\t\tif err != nil {\n\t\t\tlogger.Error().Err(ln.listenErr).Msg(\"unable to listen, retrying\")\n\t\t}\n\t\treturn err\n\t}, b)\n\n\tif ln.listenErr != nil {\n\t\tlogger.Error().Err(ln.listenErr).Msg(\"unable to listen, giving up\")\n\t} else {\n\t\tlogger.Info().Msg(\"listening on port\")\n\t}\n}\n\nfunc parseInterface(s string) (netip.AddrPort, error) {\n\taddr, portStr, _, err := splitAddrPort(s)\n\tif err != nil {\n\t\treturn netip.AddrPort{}, err\n\t}\n\n\tport, err := strconv.ParseUint(portStr, 10, 16)\n\tif err != nil {\n\t\treturn netip.AddrPort{}, err\n\t}\n\n\t// Is addr a valid ip? If so we're done.\n\tif ip, err := netip.ParseAddr(addr); err == nil {\n\t\treturn netip.AddrPortFrom(ip, uint16(port)), nil\n\t}\n\n\t// Otherwise perform name resolution.\n\tips, err := net.LookupIP(addr)\n\tif err != nil {\n\t\treturn netip.AddrPort{}, err\n\t}\n\tif len(ips) == 0 {\n\t\treturn netip.AddrPort{}, fmt.Errorf(\"no IP addresses found for %s\", addr)\n\t}\n\n\t// Prefer IPv4 addresses.\n\tfor _, ip := range ips {\n\t\tif ip.To4() != nil {\n\t\t\tif addr, err := netip.ParseAddr(ip.String()); err == nil {\n\t\t\t\treturn netip.AddrPortFrom(addr, uint16(port)), nil\n\t\t\t}\n\t\t}\n\t}\n\n\tif addr, err := netip.ParseAddr(ips[0].String()); err == nil {\n\t\treturn netip.AddrPortFrom(addr, uint16(port)), nil\n\t}\n\treturn netip.AddrPort{}, fmt.Errorf(\"unable to parse IP address %s\", addr)\n}\n\n// splitAddrPort splits s into an IP address string and a port\n// string. It splits strings shaped like \"foo:bar\" or \"[foo]:bar\",\n// without further validating the substrings. v6 indicates whether the\n// ip string should parse as an IPv6 address or an IPv4 address, in\n// order for s to be a valid ip:port string.\nfunc splitAddrPort(s string) (ip, port string, v6 bool, err error) {\n\ti := strings.LastIndexByte(s, ':')\n\tif i == -1 {\n\t\treturn \"\", \"\", false, errors.New(\"not an ip:port\")\n\t}\n\n\tip, port = s[:i], s[i+1:]\n\tif len(ip) == 0 {\n\t\treturn \"\", \"\", false, errors.New(\"no IP\")\n\t}\n\tif len(port) == 0 {\n\t\treturn \"\", \"\", false, errors.New(\"no port\")\n\t}\n\tif ip[0] == '[' {\n\t\tif len(ip) < 2 || ip[len(ip)-1] != ']' {\n\t\t\treturn \"\", \"\", false, errors.New(\"missing ]\")\n\t\t}\n\t\tip = ip[1 : len(ip)-1]\n\t\tv6 = true\n\t}\n\n\treturn ip, port, v6, nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/daemon/migrations/1_initial_schema.up.sql",
    "content": "CREATE TABLE IF NOT EXISTS app (\n    root TEXT PRIMARY KEY,\n    local_id TEXT NOT NULL,\n    platform_id TEXT NULL, -- NULL if not linked\n    updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS trace_event (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    app_id TEXT NOT NULL, -- platform_id or local_id\n    trace_id TEXT NOT NULL,\n    span_id TEXT NOT NULL,\n    event_data TEXT NOT NULL -- json\n);\n\nCREATE INDEX IF NOT EXISTS trace_event_span_key ON trace_event (trace_id, span_id);\n\nCREATE TABLE IF NOT EXISTS trace_span_index (\n    trace_id TEXT NOT NULL,\n    span_id TEXT NOT NULL,\n    app_id TEXT NOT NULL, -- platform_id or local_id\n    span_type INTEGER NOT NULL, -- enum\n\n    -- request fields\n    started_at INTEGER NULL, -- unix nanosecond\n    is_root BOOLEAN NULL,\n    service_name TEXT NULL,\n    endpoint_name TEXT NULL,\n    topic_name TEXT NULL,\n    subscription_name TEXT NULL,\n    message_id TEXT NULL,\n    external_request_id TEXT NULL,\n\n    -- response fields\n    has_response BOOLEAN NOT NULL,\n    is_error BOOLEAN NULL,\n    duration_nanos INTEGER NULL,\n    user_id TEXT NULL,\n    PRIMARY KEY (trace_id, span_id)\n);\n"
  },
  {
    "path": "cli/cmd/encore/daemon/migrations/2_infra_namespaces.up.sql",
    "content": "CREATE TABLE IF NOT EXISTS namespace (\n    id TEXT PRIMARY KEY, -- uuid\n    app_id TEXT NOT NULL, -- platform_id or local_id\n    name TEXT NOT NULL,\n    active BOOL NOT NULL DEFAULT FALSE,\n    created_at TIMESTAMP NOT NULL,\n    last_active_at TIMESTAMP NULL,\n    UNIQUE (app_id, name)\n);\n\n-- Ensure there's a single active namespace per app.\nCREATE UNIQUE INDEX active_namespace ON namespace (app_id) WHERE active = true;\n"
  },
  {
    "path": "cli/cmd/encore/daemon/migrations/3_test_tracing.up.sql",
    "content": "ALTER TABLE trace_span_index ADD COLUMN test_skipped BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE trace_span_index ADD COLUMN src_file TEXT NULL;\nALTER TABLE trace_span_index  ADD COLUMN src_line INTEGER NULL;\n"
  },
  {
    "path": "cli/cmd/encore/daemon/migrations/4_add_parent_span_id.up.sql",
    "content": "ALTER TABLE trace_span_index ADD COLUMN parent_span_id TEXT NULL;\n"
  },
  {
    "path": "cli/cmd/encore/daemon/migrations/5_add_caller_event_id.up.sql",
    "content": "ALTER TABLE trace_span_index ADD COLUMN caller_event_id INTEGER NULL;\n"
  },
  {
    "path": "cli/cmd/encore/daemon.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\tdaemonpkg \"encr.dev/cli/cmd/encore/daemon\"\n\t\"encr.dev/internal/env\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar daemonizeForeground bool\n\nvar daemonCmd = &cobra.Command{\n\tUse:   \"daemon\",\n\tShort: \"Starts the encore daemon\",\n\tRun: func(cc *cobra.Command, args []string) {\n\t\tif daemonizeForeground {\n\t\t\tdaemonpkg.Main()\n\t\t} else {\n\t\t\tif err := cmdutil.StartDaemonInBackground(context.Background()); err != nil {\n\t\t\t\tfatal(err)\n\t\t\t}\n\t\t\tfmt.Fprintln(os.Stdout, \"encore daemon is now running\")\n\t\t}\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(daemonCmd)\n\tdaemonCmd.Flags().BoolVarP(&daemonizeForeground, \"foreground\", \"f\", false, \"Start the daemon in the foreground\")\n\tdaemonCmd.AddCommand(daemonEnvCmd)\n}\n\nfunc setupDaemon(ctx context.Context) daemonpb.DaemonClient {\n\treturn cmdutil.ConnectDaemon(ctx)\n}\n\nvar daemonEnvCmd = &cobra.Command{\n\tUse:   \"env\",\n\tShort: \"Prints Encore environment information\",\n\tRun: func(cc *cobra.Command, args []string) {\n\t\tenvs := env.List()\n\t\tfor _, e := range envs {\n\t\t\tfmt.Println(e)\n\t\t}\n\t},\n}\n"
  },
  {
    "path": "cli/cmd/encore/db.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/daemon/sqldb/docker\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar dbCmd = &cobra.Command{\n\tUse:   \"db\",\n\tShort: \"Database management commands\",\n}\n\nvar (\n\tresetAll  bool\n\ttestDB    bool\n\tshadowDB  bool\n\twrite     bool\n\tadmin     bool\n\tsuperuser bool\n\tnsName    string\n)\n\nfunc getDBRole() daemonpb.DBRole {\n\tswitch {\n\tcase superuser:\n\t\treturn daemonpb.DBRole_DB_ROLE_SUPERUSER\n\tcase admin:\n\t\treturn daemonpb.DBRole_DB_ROLE_ADMIN\n\tcase write:\n\t\treturn daemonpb.DBRole_DB_ROLE_WRITE\n\tdefault:\n\t\treturn daemonpb.DBRole_DB_ROLE_READ\n\t}\n}\n\nvar dbResetCmd = &cobra.Command{\n\tUse:   \"reset <database-names...|--all>\",\n\tShort: \"Resets the databases with the given names. Use --all to reset all databases.\",\n\n\tRun: func(command *cobra.Command, args []string) {\n\t\tappRoot, _ := determineAppRoot()\n\t\tdbNames := args\n\t\tif resetAll {\n\t\t\tif len(dbNames) > 0 {\n\t\t\t\tfatal(\"cannot specify both --all and database names\")\n\t\t\t}\n\t\t\tdbNames = nil\n\t\t} else {\n\t\t\tif len(dbNames) == 0 {\n\t\t\t\tfatal(\"no database names given\")\n\t\t\t}\n\t\t}\n\n\t\tctx := context.Background()\n\t\tdaemon := setupDaemon(ctx)\n\t\tstream, err := daemon.DBReset(ctx, &daemonpb.DBResetRequest{\n\t\t\tAppRoot:       appRoot,\n\t\t\tDatabaseNames: dbNames,\n\t\t\tClusterType:   dbClusterType(),\n\t\t\tNamespace:     nonZeroPtr(nsName),\n\t\t})\n\t\tif err != nil {\n\t\t\tfatal(\"reset databases: \", err)\n\t\t}\n\t\tos.Exit(cmdutil.StreamCommandOutput(stream, nil))\n\t},\n}\n\nvar dbEnv string\n\nvar dbShellCmd = &cobra.Command{\n\tUse:   \"shell DATABASE_NAME [--env=<name>] [--test|--shadow]\",\n\tShort: \"Connects to the database via psql shell\",\n\tLong: `Defaults to connecting to your local environment.\nSpecify --env to connect to another environment.\n\nUse --test to connect to databases used for integration testing.\nUse --shadow to connect to the shadow database, used for database drift detection\nwhen using tools like Prisma.\n\n--test and --shadow imply --env=local.\n`,\n\tArgs: cobra.MaximumNArgs(1),\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(command *cobra.Command, args []string) {\n\t\tappRoot, relPath := determineAppRoot()\n\t\tctx := context.Background()\n\t\tdaemon := setupDaemon(ctx)\n\t\tdbName := \"\"\n\t\tif len(args) > 0 {\n\t\t\tdbName = args[0]\n\t\t\t// Ignore the trailing slash to support auto-completion of directory names\n\t\t\tdbName = strings.TrimSuffix(dbName, \"/\")\n\t\t} else {\n\t\t\t// Find the enclosing service by looking for the \"migrations\" folder\n\t\tSvcNameLoop:\n\t\t\tfor p := relPath; p != \".\"; p = filepath.Dir(p) {\n\t\t\t\tabsPath := filepath.Join(appRoot, p)\n\t\t\t\tif _, err := os.Stat(filepath.Join(absPath, \"migrations\")); err == nil {\n\t\t\t\t\tpkgs, err := resolvePackages(absPath, \".\")\n\t\t\t\t\tif err == nil && len(pkgs) > 0 {\n\t\t\t\t\t\tdbName = filepath.Base(pkgs[0])\n\t\t\t\t\t\tbreak SvcNameLoop\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif dbName == \"\" {\n\t\t\t\tfatal(\"could not find an Encore service with a database in this directory (or any of the parent directories).\\n\\n\" +\n\t\t\t\t\t\"Note: You can specify a service name to connect to it directly using the command 'encore db shell <database-name>'.\")\n\t\t\t}\n\t\t}\n\n\t\tif testDB || shadowDB {\n\t\t\tdbEnv = \"local\"\n\t\t}\n\n\t\tresp, err := daemon.DBConnect(ctx, &daemonpb.DBConnectRequest{\n\t\t\tAppRoot:     appRoot,\n\t\t\tDbName:      dbName,\n\t\t\tEnvName:     dbEnv,\n\t\t\tClusterType: dbClusterType(),\n\t\t\tNamespace:   nonZeroPtr(nsName),\n\t\t\tRole:        getDBRole(),\n\t\t})\n\t\tif err != nil {\n\t\t\tfatalf(\"could not connect to db  %s: %v\", dbName, err)\n\t\t}\n\n\t\t// If we have the psql binary, use that.\n\t\t// Otherwise fall back to docker.\n\t\tvar cmd *exec.Cmd\n\t\tif p, err := exec.LookPath(\"psql\"); err == nil {\n\t\t\tcmd = exec.Command(p, resp.Dsn)\n\t\t} else {\n\t\t\tfmt.Fprintln(os.Stderr, \"encore: no 'psql' executable found in $PATH; using docker to run 'psql' instead.\\n\\nNote: install psql to hide this message.\")\n\t\t\tdsn := resp.Dsn\n\n\t\t\tif runtime.GOOS == \"darwin\" || runtime.GOOS == \"windows\" {\n\t\t\t\t// Docker for {Mac, Windows}'s networking setup requires\n\t\t\t\t// using \"host.docker.internal\" instead of \"localhost\"\n\t\t\t\tfor _, rep := range []string{\"localhost\", \"127.0.0.1\"} {\n\t\t\t\t\tdsn = strings.Replace(dsn, rep, \"host.docker.internal\", -1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcmd = exec.Command(\"docker\", \"run\", \"-it\", \"--rm\", \"--network=host\", docker.Image, \"psql\", dsn)\n\t\t}\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tcmd.Stdin = os.Stdin\n\n\t\tif err := cmd.Start(); err != nil {\n\t\t\tlog.Fatal().Err(err).Msg(\"failed to start psql\")\n\t\t}\n\t\tsignal.Ignore(os.Interrupt)\n\t\tif err := cmd.Wait(); err != nil {\n\t\t\tlog.Fatal().Err(err).Msg(\"psql failed\")\n\t\t}\n\t},\n\tValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tif len(args) > 0 {\n\t\t\treturn nil, cobra.ShellCompDirectiveNoFileComp\n\t\t}\n\t\treturn nil, cobra.ShellCompDirectiveFilterDirs\n\t},\n}\n\nvar dbProxyPort int32\n\nvar dbProxyCmd = &cobra.Command{\n\tUse:   \"proxy [--env=<name>] [--test|--shadow]\",\n\tShort: \"Sets up a proxy tunnel to the database\",\n\tLong: `Set up a proxy tunnel to a database for use with other tools.\n\nUse --test to connect to databases used for integration testing.\nUse --shadow to connect to the shadow database, used for database drift detection\nwhen using tools like Prisma.\n\n--test and --shadow imply --env=local.\n`,\n\n\tRun: func(command *cobra.Command, args []string) {\n\t\tappRoot, _ := determineAppRoot()\n\t\tinterrupt := make(chan os.Signal, 1)\n\t\tsignal.Notify(interrupt, os.Interrupt)\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tgo func() {\n\t\t\t<-interrupt\n\t\t\tcancel()\n\t\t}()\n\n\t\tif testDB || shadowDB {\n\t\t\tdbEnv = \"local\"\n\t\t}\n\n\t\tdaemon := setupDaemon(ctx)\n\t\tstream, err := daemon.DBProxy(ctx, &daemonpb.DBProxyRequest{\n\t\t\tAppRoot:     appRoot,\n\t\t\tEnvName:     dbEnv,\n\t\t\tPort:        dbProxyPort,\n\t\t\tClusterType: dbClusterType(),\n\t\t\tNamespace:   nonZeroPtr(nsName),\n\t\t\tRole:        getDBRole(),\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Fatal().Err(err).Msg(\"could not setup db proxy\")\n\t\t}\n\t\tos.Exit(cmdutil.StreamCommandOutput(stream, nil))\n\t},\n}\n\nvar dbConnURICmd = &cobra.Command{\n\tUse:   \"conn-uri [<db-name>] [--test|--shadow]\",\n\tShort: \"Outputs the database connection string\",\n\tLong: `Retrieve a stable connection uri for connecting to a database.\n\nUse --test to connect to databases used for integration testing.\nUse --shadow to connect to the shadow database, used for database drift detection\nwhen using tools like Prisma.\n\n--test and --shadow imply --env=local.\n`,\n\tArgs: cobra.MaximumNArgs(1),\n\n\tRun: func(command *cobra.Command, args []string) {\n\t\tappRoot, relPath := determineAppRoot()\n\t\tctx := context.Background()\n\t\tdaemon := setupDaemon(ctx)\n\t\tdbName := \"\"\n\t\tif len(args) > 0 {\n\t\t\tdbName = args[0]\n\t\t} else {\n\t\t\t// Find the enclosing service by looking for the \"migrations\" folder\n\t\tDBNameLoop:\n\t\t\tfor p := relPath; p != \".\"; p = filepath.Dir(p) {\n\t\t\t\tabsPath := filepath.Join(appRoot, p)\n\t\t\t\tif _, err := os.Stat(filepath.Join(absPath, \"migrations\")); err == nil {\n\t\t\t\t\tpkgs, err := resolvePackages(absPath, \".\")\n\t\t\t\t\tif err == nil && len(pkgs) > 0 {\n\t\t\t\t\t\tdbName = filepath.Base(pkgs[0])\n\t\t\t\t\t\tbreak DBNameLoop\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif dbName == \"\" {\n\t\t\t\tfatal(\"could not find Encore service with a database in this directory (or any parent directory).\\n\\n\" +\n\t\t\t\t\t\"Note: You can specify a service name to connect to it directly using the command 'encore db conn-uri <service-name>'.\")\n\t\t\t}\n\t\t}\n\n\t\tif testDB || shadowDB {\n\t\t\tdbEnv = \"local\"\n\t\t}\n\n\t\tresp, err := daemon.DBConnect(ctx, &daemonpb.DBConnectRequest{\n\t\t\tAppRoot:     appRoot,\n\t\t\tDbName:      dbName,\n\t\t\tEnvName:     dbEnv,\n\t\t\tClusterType: dbClusterType(),\n\t\t\tNamespace:   nonZeroPtr(nsName),\n\t\t\tRole:        getDBRole(),\n\t\t})\n\t\tif err != nil {\n\t\t\tst, ok := status.FromError(err)\n\t\t\tif ok {\n\t\t\t\tif st.Code() == codes.NotFound {\n\t\t\t\t\tfatalf(\"no such database found: %s\", dbName)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfatalf(\"could not connect to the database for service %s: %v\", dbName, err)\n\t\t}\n\n\t\t_, _ = fmt.Fprintln(os.Stdout, resp.Dsn)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(dbCmd)\n\n\tdbResetCmd.Flags().StringVarP(&nsName, \"namespace\", \"n\", \"\", \"Namespace to use (defaults to active namespace)\")\n\tdbResetCmd.Flags().BoolVar(&resetAll, \"all\", false, \"Reset all services in the application\")\n\tdbResetCmd.Flags().BoolVarP(&testDB, \"test\", \"t\", false, \"Reset databases in the test cluster instead\")\n\tdbResetCmd.Flags().BoolVar(&shadowDB, \"shadow\", false, \"Reset databases in the shadow cluster instead\")\n\tdbCmd.AddCommand(dbResetCmd)\n\n\tdbShellCmd.Flags().StringVarP(&nsName, \"namespace\", \"n\", \"\", \"Namespace to use (defaults to active namespace)\")\n\tdbShellCmd.Flags().StringVarP(&dbEnv, \"env\", \"e\", \"local\", \"Environment name to connect to (such as \\\"prod\\\")\")\n\tdbShellCmd.Flags().BoolVarP(&testDB, \"test\", \"t\", false, \"Connect to the integration test database (implies --env=local)\")\n\tdbShellCmd.Flags().BoolVar(&shadowDB, \"shadow\", false, \"Connect to the shadow database (implies --env=local)\")\n\tdbShellCmd.Flags().BoolVar(&write, \"write\", false, \"Connect with write privileges\")\n\tdbShellCmd.Flags().BoolVar(&admin, \"admin\", false, \"Connect with admin privileges\")\n\tdbShellCmd.Flags().BoolVar(&superuser, \"superuser\", false, \"Connect as a superuser\")\n\tdbShellCmd.MarkFlagsMutuallyExclusive(\"write\", \"admin\", \"superuser\")\n\tdbCmd.AddCommand(dbShellCmd)\n\n\tdbProxyCmd.Flags().StringVarP(&nsName, \"namespace\", \"n\", \"\", \"Namespace to use (defaults to active namespace)\")\n\tdbProxyCmd.Flags().StringVarP(&dbEnv, \"env\", \"e\", \"local\", \"Environment name to connect to (such as \\\"prod\\\")\")\n\tdbProxyCmd.Flags().Int32VarP(&dbProxyPort, \"port\", \"p\", 0, \"Port to listen on (defaults to a random port)\")\n\tdbProxyCmd.Flags().BoolVarP(&testDB, \"test\", \"t\", false, \"Connect to the integration test database (implies --env=local)\")\n\tdbProxyCmd.Flags().BoolVar(&shadowDB, \"shadow\", false, \"Connect to the shadow database (implies --env=local)\")\n\tdbProxyCmd.Flags().BoolVar(&write, \"write\", false, \"Connect with write privileges\")\n\tdbProxyCmd.Flags().BoolVar(&admin, \"admin\", false, \"Connect with admin privileges\")\n\tdbProxyCmd.Flags().BoolVar(&superuser, \"superuser\", false, \"Connect as a superuser\")\n\tdbProxyCmd.MarkFlagsMutuallyExclusive(\"write\", \"admin\", \"superuser\")\n\tdbCmd.AddCommand(dbProxyCmd)\n\n\tdbConnURICmd.Flags().StringVarP(&nsName, \"namespace\", \"n\", \"\", \"Namespace to use (defaults to active namespace)\")\n\tdbConnURICmd.Flags().StringVarP(&dbEnv, \"env\", \"e\", \"local\", \"Environment name to connect to (such as \\\"prod\\\")\")\n\tdbConnURICmd.Flags().BoolVarP(&testDB, \"test\", \"t\", false, \"Connect to the integration test database (implies --env=local)\")\n\tdbConnURICmd.Flags().BoolVar(&shadowDB, \"shadow\", false, \"Connect to the shadow database (implies --env=local)\")\n\tdbConnURICmd.Flags().BoolVar(&write, \"write\", false, \"Connect with write privileges\")\n\tdbConnURICmd.Flags().BoolVar(&admin, \"admin\", false, \"Connect with admin privileges\")\n\tdbConnURICmd.Flags().BoolVar(&superuser, \"superuser\", false, \"Connect as a superuser\")\n\tdbConnURICmd.MarkFlagsMutuallyExclusive(\"write\", \"admin\", \"superuser\")\n\tdbCmd.AddCommand(dbConnURICmd)\n}\n\nfunc dbClusterType() daemonpb.DBClusterType {\n\tif testDB && shadowDB {\n\t\tfatal(\"cannot specify both --test and --shadow\")\n\t}\n\tswitch {\n\tcase testDB:\n\t\treturn daemonpb.DBClusterType_DB_CLUSTER_TYPE_TEST\n\tcase shadowDB:\n\t\treturn daemonpb.DBClusterType_DB_CLUSTER_TYPE_SHADOW\n\tdefault:\n\t\treturn daemonpb.DBClusterType_DB_CLUSTER_TYPE_RUN\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/debug.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc init() {\n\tdebugCmd := &cobra.Command{\n\t\tUse:    \"debug\",\n\t\tShort:  \"debug is a collection of debug commands\",\n\t\tHidden: true,\n\t}\n\n\tformat := cmdutil.Oneof{\n\t\tValue:     \"proto\",\n\t\tAllowed:   []string{\"proto\", \"json\"},\n\t\tFlag:      \"format\",\n\t\tFlagShort: \"f\",\n\t\tDesc:      \"Output format\",\n\t}\n\n\ttoFormat := func() daemonpb.DumpMetaRequest_Format {\n\t\tswitch format.Value {\n\t\tcase \"proto\":\n\t\t\treturn daemonpb.DumpMetaRequest_FORMAT_PROTO\n\t\tcase \"json\":\n\t\t\treturn daemonpb.DumpMetaRequest_FORMAT_JSON\n\t\tdefault:\n\t\t\treturn daemonpb.DumpMetaRequest_FORMAT_UNSPECIFIED\n\t\t}\n\t}\n\n\tvar p dumpMetaParams\n\tdumpMeta := &cobra.Command{\n\t\tUse:   \"meta\",\n\t\tShort: \"Outputs the parsed metadata\",\n\t\tArgs:  cobra.NoArgs,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tp.AppRoot, p.WorkingDir = determineAppRoot()\n\t\t\tp.Environ = os.Environ()\n\t\t\tp.Format = toFormat()\n\t\t\tdumpMeta(p)\n\t\t},\n\t}\n\n\tformat.AddFlag(dumpMeta)\n\tdumpMeta.Flags().BoolVar(&p.ParseTests, \"tests\", false, \"Parse tests as well\")\n\trootCmd.AddCommand(debugCmd)\n\tdebugCmd.AddCommand(dumpMeta)\n}\n\ntype dumpMetaParams struct {\n\tAppRoot    string\n\tWorkingDir string\n\tParseTests bool\n\tFormat     daemonpb.DumpMetaRequest_Format\n\tEnviron    []string\n}\n\nfunc dumpMeta(p dumpMetaParams) {\n\tctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\n\tdefer cancel()\n\n\tdaemon := setupDaemon(ctx)\n\tresp, err := daemon.DumpMeta(ctx, &daemonpb.DumpMetaRequest{\n\t\tAppRoot:    p.AppRoot,\n\t\tWorkingDir: p.WorkingDir,\n\t\tParseTests: p.ParseTests,\n\t\tEnviron:    p.Environ,\n\t\tFormat:     p.Format,\n\t})\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\t_, _ = os.Stdout.Write(resp.Meta)\n}\n"
  },
  {
    "path": "cli/cmd/encore/deploy.go",
    "content": "package main\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/pkg/appfile\"\n)\n\nvar (\n\tappSlug string\n\tenvName string\n\tcommit  string\n\tbranch  string\n\tformat  = cmdutil.Oneof{\n\t\tValue:     \"text\",\n\t\tAllowed:   []string{\"text\", \"json\"},\n\t\tFlag:      \"format\",\n\t\tFlagShort: \"f\",\n\t\tDesc:      \"Output format\",\n\t}\n)\n\nvar deployAppCmd = &cobra.Command{\n\tUse:                   \"deploy --commit COMMIT_SHA | --branch BRANCH_NAME\",\n\tShort:                 \"Deploy an Encore app to a cloud environment\",\n\tDisableFlagsInUseLine: true,\n\tRun: func(c *cobra.Command, args []string) {\n\t\tif commit != \"\" {\n\t\t\thb, err := hex.DecodeString(commit)\n\t\t\tif err != nil || len(hb) != 20 {\n\t\t\t\tcmdutil.Fatalf(\"invalid commit: %s\", commit)\n\t\t\t}\n\t\t}\n\t\tif appSlug == \"\" {\n\t\t\tappRoot, _, err := cmdutil.MaybeAppRoot()\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatalf(\"no app found. Run deploy inside an encore app directory or specify the app with --app\")\n\t\t\t}\n\t\t\tappSlug, err = appfile.Slug(appRoot)\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatalf(\"no app found. Run deploy inside an encore app directory or specify the app with --app\")\n\t\t\t}\n\t\t}\n\t\trollout, err := platform.Deploy(c.Context(), appSlug, envName, commit, branch)\n\t\tvar pErr platform.Error\n\t\tif ok := errors.As(err, &pErr); ok {\n\t\t\tswitch pErr.Code {\n\t\t\tcase \"app_not_found\":\n\t\t\t\tcmdutil.Fatalf(\"app not found: %s\", appSlug)\n\t\t\tcase \"validation\":\n\t\t\t\tvar details platform.ValidationDetails\n\t\t\t\terr := json.Unmarshal(pErr.Detail, &details)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcmdutil.Fatalf(\"failed to deploy: %v\", err)\n\t\t\t\t}\n\t\t\t\tswitch details.Field {\n\t\t\t\tcase \"commit\":\n\t\t\t\t\tcmdutil.Fatalf(\"could not find commit: %s. Is it pushed to the remote repository?\", commit)\n\t\t\t\tcase \"branch\":\n\t\t\t\t\tcmdutil.Fatalf(\"could not find branch: %s. Is it pushed to the remote repository?\", branch)\n\t\t\t\tcase \"env\":\n\t\t\t\t\tcmdutil.Fatalf(\"could not find environment: %s/%s\", appSlug, envName)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"failed to deploy: %v\", err)\n\t\t}\n\t\turl := fmt.Sprintf(\"https://app.encore.cloud/%s/deploys/%s/%s\", appSlug, rollout.EnvName, strings.TrimPrefix(rollout.ID, \"roll_\"))\n\t\tswitch format.Value {\n\t\tcase \"text\":\n\t\t\tfmt.Println(aurora.Sprintf(\"\\n%s %s\\n\", aurora.Bold(\"Started Deploy:\"), url))\n\t\tcase \"json\":\n\t\t\toutput, _ := json.Marshal(map[string]string{\n\t\t\t\t\"id\":  strings.TrimPrefix(rollout.ID, \"roll_\"),\n\t\t\t\t\"env\": rollout.EnvName,\n\t\t\t\t\"app\": appSlug,\n\t\t\t\t\"url\": url,\n\t\t\t})\n\t\t\tfmt.Println(string(output))\n\t\t}\n\t},\n}\n\nfunc init() {\n\talphaCmd.AddCommand(deployAppCmd)\n\tdeployAppCmd.Flags().StringVar(&appSlug, \"app\", \"\", \"app slug to deploy to (default current app)\")\n\tdeployAppCmd.Flags().StringVarP(&envName, \"env\", \"e\", \"\", \"environment to deploy to (default primary env)\")\n\tdeployAppCmd.Flags().StringVar(&commit, \"commit\", \"\", \"commit to deploy\")\n\tdeployAppCmd.Flags().StringVar(&branch, \"branch\", \"\", \"branch to deploy\")\n\tformat.AddFlag(deployAppCmd)\n\t_ = deployAppCmd.MarkFlagRequired(\"env\")\n\tdeployAppCmd.MarkFlagsMutuallyExclusive(\"commit\", \"branch\")\n\tdeployAppCmd.MarkFlagsOneRequired(\"commit\", \"branch\")\n}\n"
  },
  {
    "path": "cli/cmd/encore/exec.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\t\"encr.dev/pkg/appfile\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar execCmd = &cobra.Command{\n\tUse:   \"exec path/to/script [args...]\",\n\tShort: \"Runs executable scripts against the local Encore app\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif len(args) == 0 {\n\t\t\targs = []string{\".\"} // current directory\n\t\t}\n\t\tappRoot, wd := determineAppRoot()\n\t\texecScript(appRoot, wd, args)\n\t},\n}\nvar execCmdAlpha = &cobra.Command{\n\tUse:        \"exec path/to/script [args...]\",\n\tShort:      \"Runs executable scripts against the local Encore app\",\n\tHidden:     true,\n\tDeprecated: \"use \\\"encore exec\\\" instead\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif len(args) == 0 {\n\t\t\targs = []string{\".\"} // current directory\n\t\t}\n\t\tappRoot, wd := determineAppRoot()\n\t\texecScript(appRoot, wd, args)\n\t},\n}\n\nfunc execScript(appRoot, relWD string, args []string) {\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\t<-interrupt\n\t\tcancel()\n\t}()\n\n\tdaemon := setupDaemon(ctx)\n\n\t// For TypeScript apps, use ExecSpec to get the command spec and run it\n\t// locally. This allows interactive commands (stdin) to work properly.\n\tlang, err := appfile.AppLang(appRoot)\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\tif lang == appfile.LangTS {\n\t\ttempDir, err := os.MkdirTemp(\"\", \"encore-exec\")\n\t\tif err != nil {\n\t\t\tfatal(err)\n\t\t}\n\t\tdefer func() { _ = os.RemoveAll(tempDir) }()\n\n\t\tstream, err := daemon.ExecSpec(ctx, &daemonpb.ExecSpecRequest{\n\t\t\tAppRoot:    appRoot,\n\t\t\tWorkingDir: relWD,\n\t\t\tScriptArgs: args,\n\t\t\tEnviron:    os.Environ(),\n\t\t\tNamespace:  nonZeroPtr(nsName),\n\t\t\tTempDir:    tempDir,\n\t\t})\n\t\tif err != nil {\n\t\t\tfatal(err)\n\t\t}\n\n\t\tcmdutil.ClearTerminalExceptFirstNLines(1)\n\n\t\t// Read progress messages until we get the spec.\n\t\tvar spec *daemonpb.ExecSpecResponse\n\t\tfor {\n\t\t\tmsg, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\tfatal(err)\n\t\t\t}\n\t\t\tswitch m := msg.Msg.(type) {\n\t\t\tcase *daemonpb.ExecSpecMessage_Output:\n\t\t\t\tif len(m.Output.Stdout) > 0 {\n\t\t\t\t\tos.Stdout.Write(m.Output.Stdout)\n\t\t\t\t}\n\t\t\t\tif len(m.Output.Stderr) > 0 {\n\t\t\t\t\tos.Stderr.Write(m.Output.Stderr)\n\t\t\t\t}\n\t\t\tcase *daemonpb.ExecSpecMessage_Spec:\n\t\t\t\tspec = m.Spec\n\t\t\t}\n\t\t\tif spec != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tcmd := exec.Command(spec.Command, spec.Args...)\n\t\tcmd.Env = spec.Environ\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tcmd.Stdin = os.Stdin\n\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tvar exitErr *exec.ExitError\n\t\t\tif errors.As(err, &exitErr) {\n\t\t\t\tos.Exit(exitErr.ExitCode())\n\t\t\t}\n\t\t\tfatal(err)\n\t\t}\n\t\treturn\n\t}\n\n\t// For Go apps, use the streaming ExecScript RPC.\n\tstream, err := daemon.ExecScript(ctx, &daemonpb.ExecScriptRequest{\n\t\tAppRoot:    appRoot,\n\t\tWorkingDir: relWD,\n\t\tScriptArgs: args,\n\t\tEnviron:    os.Environ(),\n\t\tTraceFile:  root.TraceFile,\n\t\tNamespace:  nonZeroPtr(nsName),\n\t})\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\n\tcmdutil.ClearTerminalExceptFirstNLines(1)\n\tcode := cmdutil.StreamCommandOutput(stream, cmdutil.ConvertJSONLogs())\n\tos.Exit(code)\n}\n\nvar alphaCmd = &cobra.Command{\n\tUse:    \"alpha\",\n\tShort:  \"Pre-release functionality in alpha stage\",\n\tHidden: true,\n}\n\nfunc init() {\n\trootCmd.AddCommand(alphaCmd)\n}\n\nfunc init() {\n\texecCmd.Flags().StringVarP(&nsName, \"namespace\", \"n\", \"\", \"Namespace to use (defaults to active namespace)\")\n\talphaCmd.AddCommand(execCmdAlpha)\n\trootCmd.AddCommand(execCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/gen.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/manifest\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/clientgen\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc init() {\n\tgenCmd := &cobra.Command{\n\t\tUse:   \"gen\",\n\t\tShort: \"Code generation commands\",\n\t}\n\trootCmd.AddCommand(genCmd)\n\n\tvar (\n\t\toutput                         string\n\t\tlang                           string\n\t\tenvName                        string\n\t\tgenServiceNames                []string\n\t\texcludedServices               []string\n\t\tendpointTags                   []string\n\t\texcludedEndpointTags           []string\n\t\topenAPIExcludePrivateEndpoints bool\n\t\ttsSharedTypes                  bool\n\t\ttarget                         string\n\t\ttsDefaultClient                string\n\t)\n\n\tgenClientCmd := &cobra.Command{\n\t\tUse:   \"client [<app-id>] [--env=<name>] [--services=foo,bar] [--excluded-services=baz,qux] [--tags=cache,mobile] [--excluded-tags=internal] [--openapi-exclude-private-endpoints]\",\n\t\tShort: \"Generates an API client for your app\",\n\t\tLong: `Generates an API client for your app.\n\nBy default generates the API based on your local environment.\nUse '--env=<name>' to generate it based on your cloud environments.\n\nSupported language codes are:\n  typescript: A TypeScript client using the Fetch API\n  javascript: A JavaScript client using the Fetch API\n  go: A Go client using net/http\"\n  openapi: An OpenAPI specification (EXPERIMENTAL)\n\nBy default all services with a non-private API endpoint are included.\nTo further narrow down the services to generate, use the '--services' flag.\n`,\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tif target == \"leap\" {\n\t\t\t\tlang = \"typescript\"\n\t\t\t\ttsDefaultClient = \"import.meta.env.VITE_CLIENT_TARGET\"\n\t\t\t\tif output == \"\" {\n\t\t\t\t\toutput = \"../frontend/client.ts\"\n\t\t\t\t}\n\t\t\t\texcludedServices = append(excludedServices, \"frontend\")\n\t\t\t\ttsSharedTypes = true\n\t\t\t}\n\n\t\t\tif output == \"\" && lang == \"\" {\n\t\t\t\tfatal(\"specify at least one of --output or --lang.\")\n\t\t\t}\n\n\t\t\t// Determine the app id, either from the argument or from the current directory.\n\t\t\tvar appID, appRoot string\n\t\t\tif len(args) == 0 {\n\t\t\t\tvar err error\n\t\t\t\t// First check the encore.app file.\n\t\t\t\tappRoot, _, err = cmdutil.MaybeAppRoot()\n\t\t\t\tif err != nil && !errors.Is(err, cmdutil.ErrNoEncoreApp) {\n\t\t\t\t\tfatal(err)\n\t\t\t\t} else if appRoot != \"\" {\n\t\t\t\t\tif slug, err := appfile.Slug(appRoot); err == nil {\n\t\t\t\t\t\tappID = slug\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If we still don't have an app id, read it from the manifest.\n\t\t\t\tif appID == \"\" {\n\t\t\t\t\tmf, err := manifest.ReadOrCreate(appRoot)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tfatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tappID = mf.AppID\n\t\t\t\t\tif appID == \"\" {\n\t\t\t\t\t\tappID = mf.LocalID\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tappID = args[0]\n\t\t\t}\n\n\t\t\tif lang == \"\" {\n\t\t\t\tvar ok bool\n\t\t\t\tl, ok := clientgen.Detect(output)\n\t\t\t\tif !ok {\n\t\t\t\t\tfatal(\"could not detect language from output.\\n\\nNote: you can specify the language explicitly with --lang.\")\n\t\t\t\t}\n\t\t\t\tlang = string(l)\n\t\t\t} else {\n\t\t\t\t// Validate the user input for the language\n\t\t\t\tl, err := clientgen.GetLang(lang)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfatal(fmt.Sprintf(\"%s: supported languages are `typescript`, `javascript`, `go` and `openapi`\", err))\n\t\t\t\t}\n\t\t\t\tlang = string(l)\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)\n\t\t\tdefer cancel()\n\n\t\t\tdaemon := setupDaemon(ctx)\n\n\t\t\tif genServiceNames == nil {\n\t\t\t\tgenServiceNames = []string{\"*\"}\n\t\t\t}\n\n\t\t\tresp, err := daemon.GenClient(ctx, &daemonpb.GenClientRequest{\n\t\t\t\tAppId:                          appID,\n\t\t\t\tEnvName:                        envName,\n\t\t\t\tLang:                           lang,\n\t\t\t\tServices:                       genServiceNames,\n\t\t\t\tExcludedServices:               excludedServices,\n\t\t\t\tEndpointTags:                   endpointTags,\n\t\t\t\tExcludedEndpointTags:           excludedEndpointTags,\n\t\t\t\tOpenapiExcludePrivateEndpoints: &openAPIExcludePrivateEndpoints,\n\t\t\t\tTsSharedTypes:                  &tsSharedTypes,\n\t\t\t\tTsClientTarget:                 &tsDefaultClient,\n\t\t\t\tAppRoot:                        appRoot,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tfatal(err)\n\t\t\t}\n\n\t\t\tif output == \"\" {\n\t\t\t\t_, _ = os.Stdout.Write(resp.Code)\n\t\t\t} else {\n\t\t\t\tif err := os.WriteFile(output, resp.Code, 0755); err != nil {\n\t\t\t\t\tfatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tValidArgsFunction: cmdutil.AutoCompleteAppSlug,\n\t}\n\n\tgenWrappersCmd := &cobra.Command{\n\t\tUse:   \"wrappers\",\n\t\tShort: \"Generates user-facing wrapper code\",\n\t\tLong: `Manually regenerates user-facing wrapper code.\n\nThis is typically not something you ever need to call during regular development,\nas Encore automatically regenerates the wrappers whenever the code-base changes.\n\nIts core use case is for CI/CD workflows where you want to run custom linters,\nwhich may require the user-facing wrapper code to be manually generated.`,\n\t\tArgs: cobra.ExactArgs(0),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tappRoot, _ := determineAppRoot()\n\t\t\tctx := context.Background()\n\t\t\tdaemon := setupDaemon(ctx)\n\t\t\t_, err := daemon.GenWrappers(ctx, &daemonpb.GenWrappersRequest{\n\t\t\t\tAppRoot: appRoot,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tfatal(err)\n\t\t\t} else {\n\t\t\t\tfmt.Println(\"successfully generated encore wrappers.\")\n\t\t\t}\n\t\t},\n\t}\n\n\tgenCmd.AddCommand(genClientCmd)\n\tgenCmd.AddCommand(genWrappersCmd)\n\n\tgenClientCmd.Flags().StringVarP(&lang, \"lang\", \"l\", \"\", \"The language to generate code for (\\\"typescript\\\", \\\"javascript\\\", \\\"go\\\", and \\\"openapi\\\" are supported)\")\n\t_ = genClientCmd.RegisterFlagCompletionFunc(\"lang\", cmdutil.AutoCompleteFromStaticList(\n\t\t\"typescript\\tA TypeScript client using the in-browser Fetch API\",\n\t\t\"javascript\\tA JavaScript client using the in-browser Fetch API\",\n\t\t\"go\\tA Go client using net/http\",\n\t\t\"openapi\\tAn OpenAPI specification\",\n\t))\n\n\tgenClientCmd.Flags().StringVarP(&output, \"output\", \"o\", \"\", \"The filename to write the generated client code to\")\n\t_ = genClientCmd.MarkFlagFilename(\"output\", \"go\", \"ts\", \"tsx\", \"js\", \"jsx\")\n\n\tgenClientCmd.Flags().StringVarP(&envName, \"env\", \"e\", \"local\", \"The environment to fetch the API for (defaults to the local environment)\")\n\t_ = genClientCmd.RegisterFlagCompletionFunc(\"env\", cmdutil.AutoCompleteEnvSlug)\n\n\tgenClientCmd.Flags().StringSliceVarP(&genServiceNames, \"services\", \"s\", nil, \"The names of the services to include in the output\")\n\tgenClientCmd.Flags().StringSliceVarP(&excludedServices, \"excluded-services\", \"x\", nil, \"The names of the services to exclude in the output\")\n\tgenClientCmd.Flags().StringSliceVarP(&endpointTags, \"tags\", \"t\", nil, \"The names of endpoint tags to include in the output\")\n\tgenClientCmd.Flags().\n\t\tStringSliceVar(&excludedEndpointTags, \"excluded-tags\", nil, \"The names of endpoint tags to exclude in the output\")\n\tgenClientCmd.Flags().\n\t\tBoolVar(&openAPIExcludePrivateEndpoints, \"openapi-exclude-private-endpoints\", false, \"Exclude private endpoints from the OpenAPI spec\")\n\tgenClientCmd.Flags().\n\t\tBoolVar(&tsSharedTypes, \"ts:shared-types\", false, \"Import types from ~backend instead of re-generating them\")\n\tgenClientCmd.Flags().StringVar(&target, \"target\", \"\", \"An optional target for the client (\\\"leap\\\")\")\n\t_ = genClientCmd.RegisterFlagCompletionFunc(\"target\", cmdutil.AutoCompleteFromStaticList(\n\t\t\"leap\\tA TypeScript client for apps created with Leap (https://leap.new) \",\n\t))\n}\n"
  },
  {
    "path": "cli/cmd/encore/init_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage main\n\nimport (\n\t\"golang.org/x/sys/windows\"\n)\n\n// init activates virtual terminal feature on \"windows\", this enables colored\n// terminal output.\nfunc init() {\n\tsetConsoleMode(windows.Stdout, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)\n\tsetConsoleMode(windows.Stderr, windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)\n}\n\n// setConsoleMode enables VT processing on stout and stderr.\nfunc setConsoleMode(handle windows.Handle, flag uint32) {\n\tvar mode uint32\n\tif err := windows.GetConsoleMode(handle, &mode); err == nil {\n\t\twindows.SetConsoleMode(handle, mode|flag)\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/auth.go",
    "content": "package k8s\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/k8s/types\"\n\t\"encr.dev/internal/conf\"\n)\n\nvar genAuthCmd = &cobra.Command{\n\tUse:                   \"exec-credentials\",\n\tShort:                 \"Used by kubectl to get an authentication token for the Encore Kubernetes Proxy\",\n\tArgs:                  cobra.NoArgs,\n\tHidden:                true,\n\tDisableFlagsInUseLine: true,\n\tRun:                   func(cmd *cobra.Command, args []string) { generateExecCredentials() },\n}\n\nfunc init() {\n\tkubernetesCmd.AddCommand(genAuthCmd)\n}\n\n// GenerateExecCredentials generates the Kubernetes exec credentials and writes them to stdout.\n//\n// If an error occurs, it is written to stderr and the program exits with a non-zero exit code.\nfunc generateExecCredentials() {\n\t// Get the OAuth token from the Encore API\n\ttoken, err := conf.DefaultTokenSource.Token()\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"error getting token: %v\", err)\n\t}\n\n\t// Generate the kuberentes exec credentials datastructures\n\texpiryTime := types.NewTime(token.Expiry)\n\texecCredentials := &types.ExecCredential{\n\t\tTypeMeta: types.TypeMeta{\n\t\t\tAPIVersion: \"client.authentication.k8s.io/v1\",\n\t\t\tKind:       \"ExecCredential\",\n\t\t},\n\t\tStatus: &types.ExecCredentialStatus{\n\t\t\tToken:               token.AccessToken,\n\t\t\tExpirationTimestamp: &expiryTime,\n\t\t},\n\t}\n\n\t// Marshal the exec credentials to JSON and write to stdout\n\toutput, err := json.MarshalIndent(execCredentials, \"\", \"  \")\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"error marshalling exec credentials: %v\", err)\n\t}\n\t_, _ = os.Stdout.Write(output)\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/config.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/k8s/types\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/pkg/xos\"\n\n\t\"sigs.k8s.io/yaml\"\n)\n\nvar configCmd = &cobra.Command{\n\tUse:   \"configure --env=ENV_NAME\",\n\tShort: \"Updates your kubectl config to point to the Kubernetes cluster(s) for the specified environment\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tappSlug := cmdutil.AppSlug()\n\t\tctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\tif k8sEnvName == \"\" {\n\t\t\t_ = cmd.Help()\n\t\t\tcmdutil.Fatal(\"must specify environment name with --env\")\n\t\t}\n\n\t\terr := configureForAppEnv(ctx, appSlug, k8sEnvName)\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"error configuring kubectl: %v\", err)\n\t\t}\n\t},\n}\n\nvar (\n\tk8sEnvName string\n)\n\nfunc init() {\n\tconfigCmd.Flags().StringVarP(&k8sEnvName, \"env\", \"e\", \"\", \"Environment name\")\n\t_ = configCmd.MarkFlagRequired(\"env\")\n\tkubernetesCmd.AddCommand(configCmd)\n}\n\nfunc configureForAppEnv(ctx context.Context, appID string, envName string) error {\n\tappSlug, envName, clusters, err := platform.KubernetesClusters(ctx, appID, envName)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to get Kubernetes clusters for environment\")\n\t}\n\tif len(clusters) == 0 {\n\t\treturn errors.New(\"no Kubernetes clusters found for environment\")\n\t}\n\n\t// Read the existing kubeconfig file\n\tconfigFilePath := filepath.Join(types.HomeDir(), \".kube\", \"config\")\n\tcfg, err := readKubeConfig(configFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Add the clusters\n\tcontextPrefix := fmt.Sprintf(\"encore_%s_%s\", appSlug, envName)\n\tauthName := \"encore-proxy-auth\"\n\tcontextNames := make([]string, len(clusters))\n\tfor i, cluster := range clusters {\n\t\t// Create a context name for the cluster\n\t\t// by default we use the app slug and env name seperated by a underscore (e.g. encore-myapp_prod)\n\t\t// however if the environment has multiple clusters then we also include the cluster name (e.g. encore-myapp_prod_cluster1)\n\t\tcontextName := contextPrefix\n\t\tif len(clusters) > 1 {\n\t\t\tcontextName += \"_\" + cluster.Name\n\t\t}\n\t\tcontextNames[i] = contextName\n\n\t\t// Add the cluster using the cluster name as the context name\n\t\tcfg.clusters = appendOrUpdate(cfg.clusters, map[string]any{\n\t\t\t\"name\": contextName,\n\t\t\t\"cluster\": map[string]any{\n\t\t\t\t\"server\": fmt.Sprintf(\"%s/k8s-api-proxy/%s/%s/\", conf.APIBaseURL, cluster.EnvID, cluster.ResID),\n\t\t\t},\n\t\t})\n\n\t\tk8sContext := map[string]any{\n\t\t\t\"cluster\": contextName,\n\t\t\t\"user\":    authName,\n\t\t}\n\t\tif cluster.DefaultNamespace != \"\" {\n\t\t\tk8sContext[\"namespace\"] = cluster.DefaultNamespace\n\t\t}\n\n\t\tcfg.contexts = appendOrUpdate(cfg.contexts, map[string]any{\n\t\t\t\"name\":    contextName,\n\t\t\t\"context\": k8sContext,\n\t\t})\n\t}\n\n\t// Remove any old contexts or clusters\n\t// We do this by iterating over the existing contexts and clusters and removing any that are not in the new list\n\tfor i := len(cfg.contexts) - 1; i >= 0; i-- {\n\t\tif foundContext, ok := cfg.contexts[i].(map[string]any); ok {\n\t\t\tif contextName, ok := foundContext[\"name\"].(string); ok {\n\t\t\t\tif strings.HasPrefix(contextName, contextPrefix) && !slices.Contains(contextNames, contextName) {\n\t\t\t\t\tcfg.contexts = append(cfg.contexts[:i], cfg.contexts[i+1:]...)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor i := len(cfg.clusters) - 1; i >= 0; i-- {\n\t\tif foundCluster, ok := cfg.clusters[i].(map[string]any); ok {\n\t\t\tif clusterName, ok := foundCluster[\"name\"].(string); ok {\n\t\t\t\tif strings.HasPrefix(clusterName, contextPrefix) && !slices.Contains(contextNames, clusterName) {\n\t\t\t\t\tcfg.clusters = append(cfg.clusters[:i], cfg.clusters[i+1:]...)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we added a cluster then we need to update the encore-k8s-proxy user\n\tcfg.users = appendOrUpdate(cfg.users, map[string]any{\n\t\t\"name\": authName,\n\t\t\"user\": map[string]any{\n\t\t\t\"exec\": map[string]any{\n\t\t\t\t\"apiVersion\":         \"client.authentication.k8s.io/v1\",\n\t\t\t\t\"args\":               []string{\"kubernetes\", \"exec-credentials\"},\n\t\t\t\t\"command\":            \"encore\",\n\t\t\t\t\"env\":                nil,\n\t\t\t\t\"installHint\":        \"Install encore for use with kubectl, see https://encore.dev\",\n\t\t\t\t\"interactiveMode\":    \"Never\",\n\t\t\t\t\"provideClusterInfo\": false,\n\t\t\t},\n\t\t},\n\t})\n\n\t// Update the current context to the first cluster for the environment\n\tcfg.raw[\"current-context\"] = contextNames[0]\n\n\tif err := writeKubeConfig(configFilePath, cfg); err != nil {\n\t\treturn err\n\t}\n\n\tif len(clusters) == 1 {\n\t\t_, _ = fmt.Fprintf(os.Stdout, \"kubectl configured for cluster %s under context %s.\\n\", color.CyanString(clusters[0].Name), color.CyanString(contextNames[0]))\n\t} else {\n\t\t_, _ = fmt.Fprintf(os.Stdout, \"kubectl configured for %d clusters:\\n\\n\", len(clusters))\n\n\t\tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.StripEscape)\n\t\t_, _ = fmt.Fprint(w, \"CLUSTER\\tCONTEXT\\tACTIVE\\n\")\n\t\tfor i, cluster := range clusters {\n\t\t\tactive := \"\"\n\t\t\tif i == 0 {\n\t\t\t\tactive = \"yes\"\n\t\t\t}\n\t\t\t_, _ = fmt.Fprintf(w, \"%s\\t%s\\t%s\\n\", cluster.Name, contextNames[0], active)\n\t\t}\n\t\t_ = w.Flush()\n\t}\n\n\treturn nil\n}\n\n// readKubeConfig reads the existing kubeconfig file and returns a Cfg struct.\n// however this is as untyped as possible, so that we can easily marshal it back without losing any data.\nfunc readKubeConfig(file string) (*Cfg, error) {\n\tb, err := os.ReadFile(file)\n\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil, errors.Wrap(err, \"unable to read kubeconfig file\")\n\t}\n\n\t// Read the existing kubeconfig file\n\tvar kubeConfig map[string]any\n\tif len(b) > 0 {\n\t\tif err = yaml.Unmarshal(b, &kubeConfig); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"unable to parse kubeconfig file\")\n\t\t}\n\t}\n\n\t// Ensure the kubeConfig struct is valid\n\tif kubeConfig == nil {\n\t\tkubeConfig = map[string]any{\n\t\t\t\"apiVersion\": \"v1\",\n\t\t\t\"kind\":       \"Config\",\n\t\t}\n\t} else if kubeConfig[\"apiVersion\"] != \"v1\" || kubeConfig[\"kind\"] != \"Config\" {\n\t\treturn nil, errors.New(\"invalid existing kubeconfig file\")\n\t}\n\tcfg := &Cfg{\n\t\traw: kubeConfig,\n\t}\n\n\tif clusters, ok := kubeConfig[\"clusters\"]; ok {\n\t\tif clusters, ok := clusters.([]any); ok {\n\t\t\tcfg.clusters = clusters\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"clusters is not an array got %T\", clusters)\n\t\t}\n\t}\n\n\tif users, ok := kubeConfig[\"users\"]; ok {\n\t\tif users, ok := users.([]any); ok {\n\t\t\tcfg.users = users\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"users is not an array got %T\", users)\n\t\t}\n\t}\n\n\tif contexts, ok := kubeConfig[\"contexts\"]; ok {\n\t\tif contexts, ok := contexts.([]any); ok {\n\t\t\tcfg.contexts = contexts\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"contexts is not an array got %T\", contexts)\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// writeKubeConfig writes the kubeconfig back to the file.\nfunc writeKubeConfig(file string, cfg *Cfg) error {\n\t// Update the raw kubeconfig struct\n\tcfg.raw[\"clusters\"] = cfg.clusters\n\tcfg.raw[\"users\"] = cfg.users\n\tcfg.raw[\"contexts\"] = cfg.contexts\n\n\tb, err := yaml.Marshal(cfg.raw)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to marshal kubeconfig back into yaml\")\n\t}\n\n\t// Ensure the directory exists\n\tif err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {\n\t\treturn errors.Wrap(err, \"unable to create kubeconfig directory\")\n\t}\n\n\t// Then write the file\n\terr = xos.WriteFile(file, b, 0600)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to write kubeconfig file\")\n\t}\n\treturn nil\n}\n\ntype Cfg struct {\n\traw      map[string]any\n\tclusters []any\n\tusers    []any\n\tcontexts []any\n}\n\n// appendOrUpdate looks at the array for an entry which is a map and has a \"name\" key which matches the name in val, if found\n// it will update the entry with val, otherwise it will append val to the array.\nfunc appendOrUpdate(dst []any, val map[string]any) []any {\n\tidx := slices.IndexFunc(dst, func(entry any) bool {\n\t\tif entry, ok := entry.(map[string]any); ok {\n\t\t\tif entry[\"name\"] == val[\"name\"] {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\tif idx == -1 {\n\t\treturn append(dst, val)\n\t} else {\n\t\tdst[idx] = val\n\t\treturn dst\n\t}\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/kubernetes.go",
    "content": "package k8s\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/root\"\n)\n\nvar kubernetesCmd = &cobra.Command{\n\tUse:     \"kubernetes\",\n\tShort:   \"Kubernetes management commands\",\n\tAliases: []string{\"k8s\"},\n}\n\nfunc init() {\n\troot.Cmd.AddCommand(kubernetesCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/types/KUBERNETES_LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "cli/cmd/encore/k8s/types/README.md",
    "content": "# Kubernetes Types\n\nThis package contains types copied directly from the [Kubernetes](https://github.com/kubernetes/kubernetes) project, this\nis to prevent the Encore CLI needing to have a dependency on the Kubernetes project for just these types.\n"
  },
  {
    "path": "cli/cmd/encore/k8s/types/clientauthentication_types.go",
    "content": "/*\nCopyright 2021 The Kubernetes Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage types\n\n// ExecCredential is used by exec-based plugins to communicate credentials to\n// HTTP transports.\ntype ExecCredential struct {\n\tTypeMeta `json:\",inline\"`\n\n\t// Spec holds information passed to the plugin by the transport.\n\tSpec ExecCredentialSpec `json:\"spec,omitempty\"`\n\n\t// Status is filled in by the plugin and holds the credentials that the transport\n\t// should use to contact the API.\n\t// +optional\n\tStatus *ExecCredentialStatus `json:\"status,omitempty\"`\n}\n\n// ExecCredentialSpec holds request and runtime specific information provided by\n// the transport.\ntype ExecCredentialSpec struct {\n\t// Cluster contains information to allow an exec plugin to communicate with the\n\t// kubernetes cluster being authenticated to. Note that Cluster is non-nil only\n\t// when provideClusterInfo is set to true in the exec provider config (i.e.,\n\t// ExecConfig.ProvideClusterInfo).\n\t// +optional\n\tCluster *Cluster `json:\"cluster,omitempty\"`\n\n\t// Interactive declares whether stdin has been passed to this exec plugin.\n\tInteractive bool `json:\"interactive\"`\n}\n\n// ExecCredentialStatus holds credentials for the transport to use.\n//\n// Token and ClientKeyData are sensitive fields. This data should only be\n// transmitted in-memory between client and exec plugin process. Exec plugin\n// itself should at least be protected via file permissions.\ntype ExecCredentialStatus struct {\n\t// ExpirationTimestamp indicates a time when the provided credentials expire.\n\t// +optional\n\tExpirationTimestamp *Time `json:\"expirationTimestamp,omitempty\"`\n\t// Token is a bearer token used by the client for request authentication.\n\tToken string `json:\"token,omitempty\" datapolicy:\"token\"`\n\t// PEM-encoded client TLS certificates (including intermediates, if any).\n\tClientCertificateData string `json:\"clientCertificateData,omitempty\"`\n\t// PEM-encoded private key for the above certificate.\n\tClientKeyData string `json:\"clientKeyData,omitempty\" datapolicy:\"security-key\"`\n}\n\n// Cluster contains information to allow an exec plugin to communicate\n// with the kubernetes cluster being authenticated to.\n//\n// To ensure that this struct contains everything someone would need to communicate\n// with a kubernetes cluster (just like they would via a kubeconfig), the fields\n// should shadow \"k8s.io/client-go/tools/clientcmd/api/v1\".Cluster, with the exception\n// of CertificateAuthority, since CA data will always be passed to the plugin as bytes.\ntype Cluster struct {\n\t// Server is the address of the kubernetes cluster (https://hostname:port).\n\tServer string `json:\"server\"`\n\t// TLSServerName is passed to the server for SNI and is used in the client to\n\t// check server certificates against. If ServerName is empty, the hostname\n\t// used to contact the server is used.\n\t// +optional\n\tTLSServerName string `json:\"tls-server-name,omitempty\"`\n\t// InsecureSkipTLSVerify skips the validity check for the server's certificate.\n\t// This will make your HTTPS connections insecure.\n\t// +optional\n\tInsecureSkipTLSVerify bool `json:\"insecure-skip-tls-verify,omitempty\"`\n\t// CAData contains PEM-encoded certificate authority certificates.\n\t// If empty, system roots should be used.\n\t// +listType=atomic\n\t// +optional\n\tCertificateAuthorityData []byte `json:\"certificate-authority-data,omitempty\"`\n\t// ProxyURL is the URL to the proxy to be used for all requests to this\n\t// cluster.\n\t// +optional\n\tProxyURL string `json:\"proxy-url,omitempty\"`\n\t// DisableCompression allows client to opt-out of response compression for all requests to the server. This is useful\n\t// to speed up requests (specifically lists) when client-server network bandwidth is ample, by saving time on\n\t// compression (server-side) and decompression (client-side): https://github.com/kubernetes/kubernetes/issues/112296.\n\t// +optional\n\tDisableCompression bool `json:\"disable-compression,omitempty\"`\n\t// Config holds additional config data that is specific to the exec\n\t// plugin with regards to the cluster being authenticated to.\n\t//\n\t// This data is sourced from the clientcmd Cluster object's\n\t// extensions[client.authentication.k8s.io/exec] field:\n\t//\n\t// clusters:\n\t// - name: my-cluster\n\t//   cluster:\n\t//     ...\n\t//     extensions:\n\t//     - name: client.authentication.k8s.io/exec  # reserved extension name for per cluster exec config\n\t//       extension:\n\t//         audience: 06e3fbd18de8  # arbitrary config\n\t//\n\t// In some environments, the user config may be exactly the same across many clusters\n\t// (i.e. call this exec plugin) minus some details that are specific to each cluster\n\t// such as the audience.  This field allows the per cluster config to be directly\n\t// specified with the cluster info.  Using this field to store secret data is not\n\t// recommended as one of the prime benefits of exec plugins is that no secrets need\n\t// to be stored directly in the kubeconfig.\n\t// +optional\n\tConfig RawExtension `json:\"config,omitempty\"`\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/types/homedir.go",
    "content": "/*\nCopyright 2016 The Kubernetes Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage types\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n)\n\n// HomeDir returns the home directory for the current user.\n// On Windows:\n// 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.kube\\config` file is returned.\n// 2. if none of those locations contain a `.kube\\config` file, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned.\n// 3. if none of those locations are writeable, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned.\n// 4. if none of those locations exists, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned.\nfunc HomeDir() string {\n\tif runtime.GOOS == \"windows\" {\n\t\thome := os.Getenv(\"HOME\")\n\t\thomeDriveHomePath := \"\"\n\t\tif homeDrive, homePath := os.Getenv(\"HOMEDRIVE\"), os.Getenv(\"HOMEPATH\"); len(homeDrive) > 0 && len(homePath) > 0 {\n\t\t\thomeDriveHomePath = homeDrive + homePath\n\t\t}\n\t\tuserProfile := os.Getenv(\"USERPROFILE\")\n\n\t\t// Return first of %HOME%, %HOMEDRIVE%/%HOMEPATH%, %USERPROFILE% that contains a `.kube\\config` file.\n\t\t// %HOMEDRIVE%/%HOMEPATH% is preferred over %USERPROFILE% for backwards-compatibility.\n\t\tfor _, p := range []string{home, homeDriveHomePath, userProfile} {\n\t\t\tif len(p) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, err := os.Stat(filepath.Join(p, \".kube\", \"config\")); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn p\n\t\t}\n\n\t\tfirstSetPath := \"\"\n\t\tfirstExistingPath := \"\"\n\n\t\t// Prefer %USERPROFILE% over %HOMEDRIVE%/%HOMEPATH% for compatibility with other auth-writing tools\n\t\tfor _, p := range []string{home, userProfile, homeDriveHomePath} {\n\t\t\tif len(p) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(firstSetPath) == 0 {\n\t\t\t\t// remember the first path that is set\n\t\t\t\tfirstSetPath = p\n\t\t\t}\n\t\t\tinfo, err := os.Stat(p)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(firstExistingPath) == 0 {\n\t\t\t\t// remember the first path that exists\n\t\t\t\tfirstExistingPath = p\n\t\t\t}\n\t\t\tif info.IsDir() && info.Mode().Perm()&(1<<(uint(7))) != 0 {\n\t\t\t\t// return first path that is writeable\n\t\t\t\treturn p\n\t\t\t}\n\t\t}\n\n\t\t// If none are writeable, return first location that exists\n\t\tif len(firstExistingPath) > 0 {\n\t\t\treturn firstExistingPath\n\t\t}\n\n\t\t// If none exist, return first location that is set\n\t\tif len(firstSetPath) > 0 {\n\t\t\treturn firstSetPath\n\t\t}\n\n\t\t// We've got nothing\n\t\treturn \"\"\n\t}\n\treturn os.Getenv(\"HOME\")\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/types/meta_types.go",
    "content": "/*\nCopyright 2021 The Kubernetes Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage types\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n)\n\n// TypeMeta describes an individual object in an API response or request\n// with strings representing the type of the object and its API schema version.\n// Structures that are versioned or persisted should inline TypeMeta.\n//\n// +k8s:deepcopy-gen=false\ntype TypeMeta struct {\n\t// Kind is a string value representing the REST resource this object represents.\n\t// Servers may infer this from the endpoint the client submits requests to.\n\t// Cannot be updated.\n\t// In CamelCase.\n\t// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds\n\t// +optional\n\tKind string `json:\"kind,omitempty\" protobuf:\"bytes,1,opt,name=kind\"`\n\n\t// APIVersion defines the versioned schema of this representation of an object.\n\t// Servers should convert recognized schemas to the latest internal value, and\n\t// may reject unrecognized values.\n\t// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources\n\t// +optional\n\tAPIVersion string `json:\"apiVersion,omitempty\" protobuf:\"bytes,2,opt,name=apiVersion\"`\n}\n\n// Time is a wrapper around time.Time which supports correct\n// marshaling to YAML and JSON.  Wrappers are provided for many\n// of the factory methods that the time package offers.\n//\n// +protobuf.options.marshal=false\n// +protobuf.as=Timestamp\n// +protobuf.options.(gogoproto.goproto_stringer)=false\ntype Time struct {\n\ttime.Time `protobuf:\"-\"`\n}\n\n// NewTime returns a wrapped instance of the provided time\nfunc NewTime(time time.Time) Time {\n\treturn Time{time}\n}\n\n// UnmarshalJSON implements the json.Unmarshaller interface.\nfunc (t *Time) UnmarshalJSON(b []byte) error {\n\tif len(b) == 4 && string(b) == \"null\" {\n\t\tt.Time = time.Time{}\n\t\treturn nil\n\t}\n\n\tvar str string\n\terr := json.Unmarshal(b, &str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpt, err := time.Parse(time.RFC3339, str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tt.Time = pt.Local()\n\treturn nil\n}\n\n// MarshalJSON implements the json.Marshaler interface.\nfunc (t Time) MarshalJSON() ([]byte, error) {\n\tif t.IsZero() {\n\t\t// Encode unset/nil objects as JSON's \"null\".\n\t\treturn []byte(\"null\"), nil\n\t}\n\tbuf := make([]byte, 0, len(time.RFC3339)+2)\n\tbuf = append(buf, '\"')\n\t// time cannot contain non escapable JSON characters\n\tbuf = t.UTC().AppendFormat(buf, time.RFC3339)\n\tbuf = append(buf, '\"')\n\treturn buf, nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/k8s/types/runtime_types.go",
    "content": "/*\nCopyright 2014 The Kubernetes Authors.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage types\n\n// RawExtension is used to hold extensions in external versions.\n//\n// To use this, make a field which has RawExtension as its type in your external, versioned\n// struct, and Object in your internal struct. You also need to register your\n// various plugin types.\n//\n// // Internal package:\n//\n//\ttype MyAPIObject struct {\n//\t\truntime.TypeMeta `json:\",inline\"`\n//\t\tMyPlugin runtime.Object `json:\"myPlugin\"`\n//\t}\n//\n//\ttype PluginA struct {\n//\t\tAOption string `json:\"aOption\"`\n//\t}\n//\n// // External package:\n//\n//\ttype MyAPIObject struct {\n//\t\truntime.TypeMeta `json:\",inline\"`\n//\t\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n//\t}\n//\n//\ttype PluginA struct {\n//\t\tAOption string `json:\"aOption\"`\n//\t}\n//\n// // On the wire, the JSON will look something like this:\n//\n//\t{\n//\t\t\"kind\":\"MyAPIObject\",\n//\t\t\"apiVersion\":\"v1\",\n//\t\t\"myPlugin\": {\n//\t\t\t\"kind\":\"PluginA\",\n//\t\t\t\"aOption\":\"foo\",\n//\t\t},\n//\t}\n//\n// So what happens? Decode first uses json or yaml to unmarshal the serialized data into\n// your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked.\n// The next step is to copy (using pkg/conversion) into the internal struct. The runtime\n// package's DefaultScheme has conversion functions installed which will unpack the\n// JSON stored in RawExtension, turning it into the correct object type, and storing it\n// in the Object. (TODO: In the case where the object is of an unknown type, a\n// runtime.Unknown object will be created and stored.)\n//\n// +k8s:deepcopy-gen=true\n// +protobuf=true\n// +k8s:openapi-gen=true\ntype RawExtension struct {\n\t// Raw is the underlying serialization of this object.\n\t//\n\t// TODO: Determine how to detect ContentType and ContentEncoding of 'Raw' data.\n\tRaw []byte `json:\"-\" protobuf:\"bytes,1,opt,name=raw\"`\n\t// Object can hold a representation of this extension - useful for working with versioned\n\t// structs.\n\tObject any `json:\"-\"`\n}\n"
  },
  {
    "path": "cli/cmd/encore/llm_rules/init.go",
    "content": "package llm_rules\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/internal/userconfig\"\n\t\"encr.dev/pkg/appfile\"\n\t\"github.com/charmbracelet/bubbles/list\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tllmRulesToolFlag = cmdutil.Oneof{\n\t\tValue:     \"\",\n\t\tAllowed:   LLMRulesFlagValues(),\n\t\tFlag:      \"llm-rules\",\n\t\tFlagShort: \"r\",\n\t\tDesc:      \"Initialize the app with llm rules for a specific tool\",\n\t\tTypeDesc:  \"string\",\n\t}\n)\n\nfunc init() {\n\tllmRules := &cobra.Command{\n\t\tUse:   \"init\",\n\t\tShort: \"Initialize llm rules for this project\",\n\t\tArgs:  cobra.ExactArgs(0),\n\n\t\tDisableFlagsInUseLine: true,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\n\t\t\tvar tool Tool\n\t\t\tif llmRulesToolFlag.Value == \"\" {\n\t\t\t\tcfg, err := userconfig.Global().Get()\n\t\t\t\tif err != nil {\n\t\t\t\t\tcmdutil.Fatalf(\"Couldn't read user config: %s\", err)\n\t\t\t\t}\n\t\t\t\ttool = Tool(cfg.LLMRules)\n\t\t\t} else {\n\t\t\t\ttool = Tool(llmRulesToolFlag.Value)\n\t\t\t}\n\n\t\t\tif err := initLLMRules(tool); err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\t\t},\n\t}\n\n\tllmRulesCmd.AddCommand(llmRules)\n\tllmRulesToolFlag.AddFlag(llmRules)\n}\n\nfunc initLLMRules(tool Tool) error {\n\tif tool == \"\" {\n\t\tvar llmRulesModel ToolSelectModel\n\t\t{\n\t\t\tls := list.NewDefaultItemStyles()\n\t\t\tls.SelectedTitle = ls.SelectedTitle.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\t\tls.SelectedDesc = ls.SelectedDesc.Foreground(lipgloss.Color(cmdutil.CodeBlue)).BorderForeground(lipgloss.Color(cmdutil.CodeBlue))\n\t\t\tdel := list.NewDefaultDelegate()\n\t\t\tdel.Styles = ls\n\t\t\tdel.ShowDescription = false\n\t\t\tdel.SetSpacing(0)\n\n\t\t\titems := make([]list.Item, 0, len(AllLLMRules))\n\t\t\tfor _, rule := range AllLLMRules {\n\t\t\t\titems = append(items, ToolItem{rule})\n\t\t\t}\n\n\t\t\tll := list.New(items, del, 0, 0)\n\t\t\tll.SetShowTitle(false)\n\t\t\tll.SetShowHelp(false)\n\t\t\tll.SetShowPagination(true)\n\t\t\tll.SetShowFilter(false)\n\t\t\tll.SetFilteringEnabled(false)\n\t\t\tll.SetShowStatusBar(false)\n\t\t\tll.DisableQuitKeybindings() // quit handled by toolSelectModel\n\n\t\t\tllmRulesModel = ToolSelectModel{\n\t\t\t\tList:       ll,\n\t\t\t\tPredefined: LLMRulesToolNone,\n\t\t\t}\n\t\t\tllmRulesModel.SetSize(0, 20)\n\n\t\t}\n\t\tt := toolSelectorModel{\n\t\t\ttoolModel: llmRulesModel,\n\t\t}\n\t\tp := tea.NewProgram(t)\n\n\t\tresult, err := p.Run()\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\n\t\tres := result.(toolSelectorModel)\n\t\tif res.aborted {\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\ttool = res.toolModel.Selected()\n\t}\n\n\t// Determine the app root.\n\troot, _, err := cmdutil.MaybeAppRoot()\n\tif errors.Is(err, cmdutil.ErrNoEncoreApp) {\n\t\tcmdutil.Fatalf(\"no encore.app found, this command must be run from an Encore app directory\")\n\t}\n\tif err != nil {\n\t\tcmdutil.Fatal(err)\n\t}\n\n\t// parse encore.app\n\tfilePath := filepath.Join(root, \"encore.app\")\n\tencoreApp, err := appfile.ParseFile(filePath)\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"couldn't parse encore.app: %s\", err)\n\t}\n\n\tvar lang cmdutil.Language\n\tswitch encoreApp.Lang {\n\tcase appfile.LangGo:\n\t\tlang = cmdutil.LanguageGo\n\tcase appfile.LangTS:\n\t\tlang = cmdutil.LanguageTS\n\t}\n\n\tif err := SetupLLMRules(tool, lang, root, encoreApp.ID); err != nil {\n\t\tcmdutil.Fatal(err)\n\t}\n\n\tPrintLLMRulesInfo(tool)\n\n\treturn nil\n}\n\ntype toolSelectorModel struct {\n\ttoolModel ToolSelectModel\n\taborted   bool\n}\n\nfunc (t toolSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar (\n\t\tcmds []tea.Cmd\n\t\tc    tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\tt.SetSize(msg.Width, msg.Height)\n\t\treturn t, nil\n\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"ctrl+c\", \"esc\", \"q\":\n\t\t\tt.aborted = true\n\t\t\treturn t, tea.Quit\n\t\t}\n\n\t\tt.toolModel, c = t.toolModel.Update(msg)\n\t\tcmds = append(cmds, c)\n\t\treturn t, tea.Batch(cmds...)\n\n\tcase ToolSelectDone:\n\t\tcmds = append(cmds, tea.Quit)\n\t}\n\n\tt.toolModel, c = t.toolModel.Update(msg)\n\tcmds = append(cmds, c)\n\treturn t, tea.Batch(cmds...)\n}\n\nfunc (t toolSelectorModel) Init() tea.Cmd {\n\treturn nil\n}\n\nfunc (t toolSelectorModel) View() string {\n\tvar b strings.Builder\n\tb.WriteString(t.toolModel.View())\n\treturn cmdutil.DocStyle.Render(b.String())\n}\n\nfunc (t *toolSelectorModel) SetSize(width, height int) {\n\tt.toolModel.SetSize(width, height)\n}\n"
  },
  {
    "path": "cli/cmd/encore/llm_rules/llm_rules.go",
    "content": "package llm_rules\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/root\"\n)\n\nvar llmRulesCmd = &cobra.Command{\n\tUse:   \"llm-rules\",\n\tShort: \"Commands to create LLM rules for apps\",\n}\n\nfunc init() {\n\troot.Cmd.AddCommand(llmRulesCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/llm_rules/tool.go",
    "content": "package llm_rules\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/fatih/color\"\n)\n\nconst mdcTemplate string = `---\ndescription: Encore %s rules\nglobs:\nalwaysApply: true\n---\n%s\n`\n\ntype Tool string\n\n// NOTE: changes to these values should also be reflected in userconfig\nconst (\n\tLLMRulesToolNone      Tool = \"\"\n\tLLMRulesToolCursor    Tool = \"cursor\"\n\tLLMRulesToolClaudCode Tool = \"claudecode\"\n\tLLMRulesToolVSCode    Tool = \"vscode\"\n\tLLMRulesToolAgentsMD  Tool = \"agentsmd\"\n\tLLMRulesToolZed       Tool = \"zed\"\n)\n\n// all available options exept for None\nvar AllLLMRules = []Tool{\n\tLLMRulesToolCursor,\n\tLLMRulesToolClaudCode,\n\tLLMRulesToolVSCode,\n\tLLMRulesToolAgentsMD,\n\tLLMRulesToolZed,\n}\n\nfunc LLMRulesFlagValues() []string {\n\tresult := make([]string, 0, len(AllLLMRules))\n\tfor _, r := range AllLLMRules {\n\t\tresult = append(result, string(r))\n\t}\n\treturn result\n}\n\nfunc (e Tool) Display() string {\n\tswitch e {\n\tcase LLMRulesToolCursor:\n\t\treturn \"Cursor\"\n\tcase LLMRulesToolClaudCode:\n\t\treturn \"Claude Code\"\n\tcase LLMRulesToolVSCode:\n\t\treturn \"VS Code\"\n\tcase LLMRulesToolAgentsMD:\n\t\treturn \"AGENTS.md\"\n\tcase LLMRulesToolZed:\n\t\treturn \"Zed\"\n\tdefault:\n\t\treturn \"None\"\n\t}\n}\n\nfunc (e Tool) SelectPrompt() string {\n\treturn \"Select a tool to generate LLM rules for\"\n}\n\ntype ToolItem struct {\n\ttool Tool\n}\n\nfunc NewLLMRulesItem(tool Tool) ToolItem {\n\treturn ToolItem{tool: tool}\n}\n\nfunc (i ToolItem) FilterValue() string { return i.tool.Display() }\nfunc (i ToolItem) Title() string       { return i.FilterValue() }\nfunc (i ToolItem) Description() string { return \"\" }\nfunc (i ToolItem) SelectedID() Tool    { return i.tool }\n\ntype ToolSelectModel = cmdutil.SimpleSelectModel[Tool, ToolItem]\ntype ToolSelectDone = cmdutil.SimpleSelectDone[Tool]\n\nfunc SetupLLMRules(llmRules Tool, lang cmdutil.Language, appRootRelpath string, appSlug string) error {\n\tllmInstructions, err := downloadLLMInstructions(lang)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch llmRules {\n\tcase LLMRulesToolCursor:\n\t\tcursorDir := filepath.Join(appRootRelpath, \".cursor\")\n\t\trulesDir := filepath.Join(cursorDir, \"rules\")\n\t\terr := os.MkdirAll(rulesDir, 0755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif appSlug != \"\" {\n\t\t\t// https://cursor.com/docs/context/mcp#using-mcpjson\n\t\t\tmcpPath := filepath.Join(cursorDir, \"mcp.json\")\n\t\t\terr = updateJsonFile(mcpPath, \"mcpServers\", func(mcpServers map[string]any) {\n\t\t\t\t// Add encore-mcp configuration\n\t\t\t\tmcpServers[\"encore-mcp\"] = map[string]any{\n\t\t\t\t\t\"command\": \"encore\",\n\t\t\t\t\t\"args\":    []string{\"mcp\", \"run\", \"--app=\" + appSlug},\n\t\t\t\t}\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// https://cursor.com/docs/context/rules\n\t\t// always overwrite as we have a dedicated encore config file\n\t\terr = os.WriteFile(filepath.Join(rulesDir, \"encore.mdc\"), fmt.Appendf(nil, mdcTemplate, lang, string(llmInstructions)), 0644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase LLMRulesToolClaudCode:\n\t\tif appSlug != \"\" {\n\t\t\t// https://code.claude.com/docs/en/mcp#project-scope\n\t\t\tmcpPath := filepath.Join(appRootRelpath, \".mcp.json\")\n\t\t\terr = updateJsonFile(mcpPath, \"mcpServers\", func(mcpServers map[string]any) {\n\t\t\t\t// Add encore-mcp configuration\n\t\t\t\tmcpServers[\"encore-mcp\"] = map[string]any{\n\t\t\t\t\t\"command\": \"encore\",\n\t\t\t\t\t\"args\":    []string{\"mcp\", \"run\", \"--app=\" + appSlug},\n\t\t\t\t}\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// https://code.claude.com/docs/en/settings#key-points-about-the-configuration-system\n\t\tclaudeDir := filepath.Join(appRootRelpath, \".claude\")\n\t\tif err := os.MkdirAll(claudeDir, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = writeNewFileOrSkip(filepath.Join(claudeDir, \"CLAUDE.md\"), []byte(llmInstructions))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase LLMRulesToolVSCode:\n\t\tgithubDir := filepath.Join(appRootRelpath, \".github\")\n\t\tif err := os.MkdirAll(githubDir, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions#writing-your-own-copilot-instructionsmd-file\n\t\terr = writeNewFileOrSkip(filepath.Join(githubDir, \"copilot-instructions.md\"), []byte(llmInstructions))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvscodePath := filepath.Join(appRootRelpath, \".vscode\")\n\t\tif err := os.MkdirAll(vscodePath, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_configuration-format\n\t\tmcpPath := filepath.Join(vscodePath, \"mcp.json\")\n\t\terr = updateJsonFile(mcpPath, \"servers\", func(servers map[string]any) {\n\t\t\t// Add encore-mcp configuration\n\t\t\tservers[\"encore-mcp\"] = map[string]any{\n\t\t\t\t\"command\": \"encore\",\n\t\t\t\t\"args\":    []string{\"mcp\", \"run\", \"--app=\" + appSlug},\n\t\t\t}\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase LLMRulesToolAgentsMD:\n\t\t// https://agents.md/\n\t\terr = writeNewFileOrSkip(filepath.Join(appRootRelpath, \"AGENTS.md\"), []byte(llmInstructions))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tcase LLMRulesToolZed:\n\t\t// https://zed.dev/docs/ai/rules#rules-files\n\t\trulesPath := filepath.Join(appRootRelpath, \".rules\")\n\t\terr = writeNewFileOrSkip(rulesPath, []byte(llmInstructions))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif appSlug != \"\" {\n\t\t\tzedDir := filepath.Join(appRootRelpath, \".zed\")\n\t\t\terr := os.MkdirAll(zedDir, 0755)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// https://zed.dev/docs/ai/mcp#as-custom-servers\n\t\t\tsettingsPath := filepath.Join(zedDir, \"settings.json\")\n\t\t\terr = updateJsonFile(settingsPath, \"context_servers\", func(contextServers map[string]any) {\n\t\t\t\t// Add encore-mcp configuration\n\t\t\t\tcontextServers[\"encore-mcp\"] = map[string]any{\n\t\t\t\t\t\"command\": \"encore\",\n\t\t\t\t\t\"args\":    []string{\"mcp\", \"run\", \"--app=\" + appSlug},\n\t\t\t\t\t\"env\":     map[string]any{},\n\t\t\t\t\t\"source\":  \"custom\",\n\t\t\t\t}\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn nil\n}\n\nfunc PrintLLMRulesInfo(tool Tool) {\n\tif tool == LLMRulesToolNone {\n\t\treturn\n\t}\n\n\tcyan := color.New(color.FgCyan)\n\tcyanf := cyan.SprintfFunc()\n\n\tswitch tool {\n\tcase LLMRulesToolCursor, LLMRulesToolClaudCode, LLMRulesToolVSCode, LLMRulesToolZed:\n\t\tfmt.Printf(\"MCP:      %s\\n\", cyanf(\"Configured in %s\", tool.Display()))\n\t\tfmt.Println()\n\t}\n\n\tfmt.Printf(\"Try these prompts in %s:\\n\", tool.Display())\n\tfmt.Println(\"→ \\\"add image uploads to my hello world app\\\"\")\n\tfmt.Println(\"→ \\\"add a SQL database for storing user profiles\\\"\")\n\tfmt.Println(\"→ \\\"add a pub/sub topic for sending notifications\\\"\")\n\tfmt.Println()\n}\n\nfunc updateJsonFile(path, parent string, updateFn func(field map[string]any)) error {\n\tvar conf map[string]any\n\n\t// Read existing mcp.json if it exists\n\tif existingData, err := os.ReadFile(path); err == nil {\n\t\tif err := json.Unmarshal(existingData, &conf); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse existing %s: %w\", path, err)\n\t\t}\n\t} else {\n\t\tconf = make(map[string]any)\n\t}\n\n\t// Get or create mcpServers\n\tmcpServers, ok := conf[parent].(map[string]any)\n\tif !ok {\n\t\tmcpServers = make(map[string]any)\n\t\tconf[parent] = mcpServers\n\t}\n\n\tupdateFn(mcpServers)\n\n\t// Write back the config\n\tdata, err := json.MarshalIndent(conf, \"\", \"    \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal mcp.json: %w\", err)\n\t}\n\n\terr = os.WriteFile(path, data, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// write to file if it doesnt exist, and emits a warning and skips writing if the file exist\nfunc writeNewFileOrSkip(filePath string, data []byte) error {\n\tif _, err := os.Stat(filePath); err == nil {\n\t\t// File already exists, skip writing\n\t\tyellow := color.New(color.FgYellow)\n\t\tyellow.Printf(\"Warning: %s file already exists, skipping\\n\", filePath)\n\t} else {\n\t\terr = os.WriteFile(filePath, data, 0644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc downloadLLMInstructions(lang cmdutil.Language) (string, error) {\n\tfmt.Println(\"Downloading LLM Instructions...\")\n\tvar url string\n\tswitch lang {\n\tcase cmdutil.LanguageGo:\n\t\turl = \"https://raw.githubusercontent.com/encoredev/encore/refs/heads/main/go_llm_instructions.txt\"\n\tcase cmdutil.LanguageTS:\n\t\turl = \"https://raw.githubusercontent.com/encoredev/encore/refs/heads/main/ts_llm_instructions.txt\"\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unsupported language\")\n\t}\n\ts := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\ts.Prefix = \"Downloading LLM instructions...\"\n\ts.Start()\n\tdefer s.Stop()\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\ts.FinalMSG = fmt.Sprintf(\"failed, skipping: %v\", err.Error())\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\ts.FinalMSG = fmt.Sprintf(\"failed, skipping: %v\", err.Error())\n\t\treturn \"\", err\n\t}\n\treturn string(body), nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/logs.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/pkg/appfile\"\n)\n\nvar (\n\tlogsEnv   string\n\tlogsJSON  bool\n\tlogsQuiet bool\n)\n\nvar logsCmd = &cobra.Command{\n\tUse:   \"logs [--env=prod] [--json]\",\n\tShort: \"Streams logs from your application\",\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tappRoot, _ := determineAppRoot()\n\t\tstreamLogs(appRoot, logsEnv)\n\t},\n}\n\nfunc streamLogs(appRoot, envName string) {\n\tctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\n\tdefer cancel()\n\tappSlug, err := appfile.Slug(appRoot)\n\tif err != nil {\n\t\tfatal(err)\n\t} else if appSlug == \"\" {\n\t\tfatal(\"app is not linked with Encore Cloud\")\n\t}\n\n\tif envName == \"\" {\n\t\tenvName = \"@primary\"\n\t}\n\tlogs, err := platform.EnvLogs(ctx, appSlug, envName)\n\tif err != nil {\n\t\tvar e platform.Error\n\t\tif errors.As(err, &e) {\n\t\t\tswitch e.Code {\n\t\t\tcase \"env_not_found\":\n\t\t\t\tfatalf(\"environment %q not found\", envName)\n\t\t\t}\n\t\t}\n\t\tfatal(err)\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlogs.Close()\n\t}()\n\n\t// Use the same configuration as the runtime\n\tzerolog.TimeFieldFormat = time.RFC3339Nano\n\n\tif !logsQuiet {\n\t\tfmt.Println(aurora.Gray(12, \"Connected, waiting for logs...\"))\n\t}\n\n\tcw := zerolog.NewConsoleWriter()\n\tfor {\n\t\t_, message, err := logs.ReadMessage()\n\t\tif err != nil {\n\t\t\tif websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {\n\t\t\t\tfatal(\"the server closed the connection unexpectedly.\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tlines := bytes.Split(message, []byte(\"\\n\"))\n\t\tfor _, line := range lines {\n\t\t\t// Pretty-print logs if requested and it looks like a JSON log line\n\t\t\tif !logsJSON && bytes.HasPrefix(line, []byte{'{'}) {\n\t\t\t\tif _, err := cw.Write(mapCloudFieldNamesToExpected(line)); err != nil {\n\t\t\t\t\t// Fall back to regular stdout in case of error\n\t\t\t\t\tos.Stdout.Write(line)\n\t\t\t\t\tos.Stdout.Write([]byte(\"\\n\"))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tos.Stdout.Write(line)\n\t\t\t\tos.Stdout.Write([]byte(\"\\n\"))\n\t\t\t}\n\t\t}\n\t}\n}\n\n// mapCloudFieldNamesToExpected detects if we're logging with GCP style logging and then swaps\n// the field names to what is expected by zerolog\nfunc mapCloudFieldNamesToExpected(jsonBytes []byte) []byte {\n\tunmarshaled := map[string]any{}\n\terr := json.Unmarshal(jsonBytes, &unmarshaled)\n\tif err != nil {\n\t\treturn jsonBytes\n\t}\n\n\t_, hasSeverity := unmarshaled[\"severity\"]\n\t_, hasExpectedLevelField := unmarshaled[zerolog.LevelFieldName]\n\t_, hasTimestamp := unmarshaled[\"timestamp\"]\n\t_, hasExpectedTimeField := unmarshaled[zerolog.TimestampFieldName]\n\n\t// GCP logs have a severity field and a timestamp field and not the default level and timestamp\n\tif hasSeverity && !hasExpectedLevelField && hasTimestamp && !hasExpectedTimeField {\n\t\tunmarshaled[zerolog.LevelFieldName] = unmarshaled[\"severity\"]\n\t\tdelete(unmarshaled, \"severity\")\n\t\tunmarshaled[zerolog.TimestampFieldName] = unmarshaled[\"timestamp\"]\n\t\tdelete(unmarshaled, \"timestamp\")\n\t} else {\n\t\t// No changes, return the original bytes unmodified\n\t\treturn jsonBytes\n\t}\n\n\tnewBytes, err := json.Marshal(unmarshaled)\n\tif err != nil {\n\t\treturn jsonBytes\n\t}\n\treturn newBytes\n}\n\nfunc init() {\n\trootCmd.AddCommand(logsCmd)\n\tlogsCmd.Flags().StringVarP(&logsEnv, \"env\", \"e\", \"\", \"Environment name to stream logs from (defaults to the primary environment)\")\n\tlogsCmd.Flags().BoolVar(&logsJSON, \"json\", false, \"Whether to print logs in raw JSON format\")\n\tlogsCmd.Flags().BoolVarP(&logsQuiet, \"quiet\", \"q\", false, \"Whether to print initial message when the command is waiting for logs\")\n}\n"
  },
  {
    "path": "cli/cmd/encore/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/tools/go/packages\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\n\t// Register commands\n\t_ \"encr.dev/cli/cmd/encore/app\"\n\t_ \"encr.dev/cli/cmd/encore/config\"\n\t_ \"encr.dev/cli/cmd/encore/k8s\"\n\t_ \"encr.dev/cli/cmd/encore/namespace\"\n\t_ \"encr.dev/cli/cmd/encore/secrets\"\n)\n\n// for backwards compatibility, for now\nvar rootCmd = root.Cmd\n\nfunc main() {\n\tlog.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})\n\tif err := root.Cmd.Execute(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\tos.Exit(1)\n\t}\n}\n\n// determineAppRoot determines the app root by looking for the \"encore.app\" file,\n// initially in the current directory and then recursively in parent directories\n// up to the filesystem root.\n// It reports the absolute path to the app root, and the\n// relative path from the app root to the working directory.\n// On errors it prints an error message and exits.\nfunc determineAppRoot() (appRoot, relPath string) {\n\treturn cmdutil.AppRoot()\n}\n\nfunc determineWorkspaceRoot(appRoot string) string {\n\treturn cmdutil.WorkspaceRoot(appRoot)\n}\n\nfunc resolvePackages(dir string, patterns ...string) ([]string, error) {\n\tcfg := &packages.Config{\n\t\tMode: packages.NeedName,\n\t\tDir:  dir,\n\t}\n\tpkgs, err := packages.Load(cfg, patterns...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpaths := make([]string, 0, len(pkgs))\n\tfor _, pkg := range pkgs {\n\t\tpaths = append(paths, pkg.PkgPath)\n\t}\n\treturn paths, nil\n}\n\nfunc displayError(out *os.File, err []byte) {\n\tcmdutil.DisplayError(out, err)\n}\n\nfunc fatal(args ...interface{}) {\n\tcmdutil.Fatal(args...)\n}\n\nfunc fatalf(format string, args ...interface{}) {\n\tcmdutil.Fatalf(format, args...)\n}\n\nfunc nonZeroPtr[T comparable](v T) *T {\n\tvar zero T\n\tif v == zero {\n\t\treturn nil\n\t}\n\treturn &v\n}\n"
  },
  {
    "path": "cli/cmd/encore/mcp.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\t\"encr.dev/cli/internal/jsonrpc2\"\n)\n\nvar mcpCmd = &cobra.Command{\n\tUse:   \"mcp\",\n\tShort: \"MCP (Message Context Provider) commands\",\n}\n\nvar (\n\tappID   string\n\tmcpPort int = 9900\n)\n\nvar startCmd = &cobra.Command{\n\tUse:   \"start\",\n\tShort: \"Starts an SSE based MCP session and prints the SSE URL\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tctx := cmd.Context()\n\t\tif appID == \"\" {\n\t\t\tappID = cmdutil.AppSlugOrLocalID()\n\t\t}\n\t\tsetupDaemon(ctx)\n\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"  MCP Service is running!\\n\\n\")\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"  MCP SSE URL:        %s\\n\", aurora.Cyan(fmt.Sprintf(\n\t\t\t\"http://localhost:%d/sse?app=%s\", mcpPort, appID)))\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"  MCP stdio Command:  %s\\n\", aurora.Cyan(fmt.Sprintf(\n\t\t\t\"encore mcp run --app=%s\", appID)))\n\t},\n}\n\ntype sseConnection struct {\n\tread  func() (typ, data string, err error)\n\tclose func() error\n\n\tappID     string\n\tconnected bool\n\tpath      string\n\tclient    *http.Client\n\n\t// Track outstanding request IDs\n\tmu         sync.Mutex\n\trequestIDs map[jsonrpc2.ID]struct{}\n}\n\nfunc (c *sseConnection) Read() (typ, data string, err error) {\n\ttyp, data, err = c.read()\n\tif err != nil {\n\t\tc.connected = false\n\t\treturn \"\", \"\", err\n\t}\n\treturn typ, data, nil\n}\n\nfunc (c *sseConnection) Close() error {\n\tif c.close != nil {\n\t\tc.connected = false\n\t\treturn c.close()\n\t}\n\treturn nil\n}\n\nfunc (c *sseConnection) reconnect(ctx context.Context) error {\n\t// Close the existing connection if there is one\n\tif c.close != nil {\n\t\t_ = c.close()\n\t}\n\tc.connected = false\n\n\t// Initial backoff duration\n\tbackoff := 1000 * time.Millisecond\n\tmaxBackoff := 10 * time.Second\n\n\tfor {\n\t\t// Check if context is canceled\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tif root.Verbosity > 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"Reconnecting to MCP: %v\\n\", backoff)\n\t\t}\n\n\t\t// Try to connect\n\t\terr := c.connect(ctx)\n\t\tif err == nil {\n\t\t\tc.connected = true\n\t\t\treturn nil\n\t\t}\n\n\t\t// If connection failed, wait and retry with exponential backoff\n\t\tif root.Verbosity > 0 {\n\t\t\tfmt.Fprintf(os.Stderr, \"Failed to connect to MCP: %v, retrying in %v\\n\", err, backoff)\n\t\t}\n\n\t\tselect {\n\t\tcase <-time.After(backoff):\n\t\t\t// Double the backoff for next attempt, but cap at maxBackoff\n\t\t\tbackoff *= 2\n\t\t\tif backoff > maxBackoff {\n\t\t\t\tbackoff = maxBackoff\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\t}\n\t}\n}\n\nfunc (c *sseConnection) connect(ctx context.Context) error {\n\tsetupDaemon(ctx)\n\tif c.client == nil {\n\t\tc.client = &http.Client{}\n\t}\n\n\t// Initialize the request IDs map\n\tc.mu.Lock()\n\tc.requestIDs = make(map[jsonrpc2.ID]struct{})\n\tc.mu.Unlock()\n\n\tresp, err := c.client.Get(fmt.Sprintf(\"http://localhost:%d/sse?app=%s\", mcpPort, c.appID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != 200 {\n\t\tresp.Body.Close()\n\t\treturn fmt.Errorf(\"error getting session ID: %v\", resp.Status)\n\t}\n\tc.read = eventReader(startLineReader(ctx, bufio.NewReader(resp.Body).ReadString))\n\tc.close = resp.Body.Close\n\tc.connected = true\n\n\t// Read the endpoint path\n\tevent, path, err := c.Read()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading endpoint path: %v\", err)\n\t}\n\tif event != \"endpoint\" {\n\t\treturn fmt.Errorf(\"expected endpoint event, got %q\", event)\n\t}\n\tc.path = path\n\n\treturn nil\n}\n\nfunc (c *sseConnection) SendMessage(data []byte) error {\n\tif !c.connected {\n\t\treturn fmt.Errorf(\"not connected to MCP\")\n\t}\n\n\tif c.client == nil {\n\t\tc.client = &http.Client{}\n\t}\n\n\t// Track the request ID if it's a Call\n\tmsg, err := jsonrpc2.DecodeMessage(data)\n\tif err == nil {\n\t\tif call, ok := msg.(*jsonrpc2.Call); ok {\n\t\t\tc.mu.Lock()\n\t\t\tc.requestIDs[call.ID()] = struct{}{}\n\t\t\tc.mu.Unlock()\n\t\t}\n\t}\n\n\tresp, err := c.client.Post(fmt.Sprintf(\"http://localhost:%d%s\", mcpPort, c.path), \"application/json\", bytes.NewReader(data))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 202 {\n\t\treturn fmt.Errorf(\"error forwarding request: %v\", resp.Status)\n\t}\n\n\treturn nil\n}\n\n// CreateErrorResponse creates a JSON-RPC error response with the correct ID if available\nfunc (c *sseConnection) CreateErrorResponse(id *jsonrpc2.ID, code int, message string) string {\n\t// Build the error response\n\tresponse := map[string]interface{}{\n\t\t\"jsonrpc\": \"2.0\",\n\t\t\"error\": map[string]interface{}{\n\t\t\t\"code\":    code,\n\t\t\t\"message\": message,\n\t\t},\n\t}\n\n\t// Include ID if available\n\tif id != nil {\n\t\tresponse[\"id\"] = id\n\n\t\t// Remove from tracking as we're responding to it\n\t\tc.mu.Lock()\n\t\tdelete(c.requestIDs, *id)\n\t\tc.mu.Unlock()\n\t} else {\n\t\tresponse[\"id\"] = nil\n\t}\n\n\t// Marshal to JSON\n\tjsonData, err := json.Marshal(response)\n\tif err != nil {\n\t\t// Fallback if marshaling fails\n\t\treturn fmt.Sprintf(`{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":%d,\"message\":\"%s\"}}`, code, message)\n\t}\n\n\treturn string(jsonData)\n}\n\n// RemoveRequestID removes a request ID from tracking once a response is received\nfunc (c *sseConnection) RemoveRequestID(id jsonrpc2.ID) {\n\tc.mu.Lock()\n\tdelete(c.requestIDs, id)\n\tc.mu.Unlock()\n}\n\nvar runCmd = &cobra.Command{\n\tUse:   \"run\",\n\tShort: \"Runs an stdio-based MCP session\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\n\t\tctx := cmd.Context()\n\n\t\tif appID == \"\" {\n\t\t\tappID = cmdutil.AppSlugOrLocalID()\n\t\t}\n\n\t\tif root.Verbosity > 0 {\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Starting an MCP session for app %s\\n\", appID)\n\t\t}\n\n\t\tconn := &sseConnection{appID: appID}\n\t\tif err := conn.connect(ctx); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error connecting to MCP: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tdefer conn.Close()\n\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\tevent, data, err := conn.Read()\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Error reading event: %v\\n\", err)\n\n\t\t\t\t\tconn.mu.Lock()\n\t\t\t\t\trequestIDs := maps.Clone(conn.requestIDs)\n\t\t\t\t\tconn.mu.Unlock()\n\t\t\t\t\tfor id := range requestIDs {\n\t\t\t\t\t\tfmt.Println(conn.CreateErrorResponse(&id, -32700, \"error\"))\n\t\t\t\t\t}\n\t\t\t\t\tif err := conn.reconnect(ctx); err != nil {\n\t\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Error reconnecting to MCP: %v\\n\", err)\n\t\t\t\t\t\tos.Exit(1)\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif root.Verbosity > 0 {\n\t\t\t\t\tfmt.Fprintf(os.Stderr, \"Received event: %s: %s\\n\", event, data)\n\t\t\t\t}\n\t\t\t\tif event == \"message\" {\n\t\t\t\t\t// If it's a response message, remove the ID from tracking\n\t\t\t\t\tresponseMsg := struct {\n\t\t\t\t\t\tJSONRPC string       `json:\"jsonrpc\"`\n\t\t\t\t\t\tID      *jsonrpc2.ID `json:\"id\"`\n\t\t\t\t\t\tResult  interface{}  `json:\"result,omitempty\"`\n\t\t\t\t\t\tError   interface{}  `json:\"error,omitempty\"`\n\t\t\t\t\t}{}\n\n\t\t\t\t\tif err := json.Unmarshal([]byte(data), &responseMsg); err == nil && responseMsg.ID != nil {\n\t\t\t\t\t\tconn.RemoveRequestID(*responseMsg.ID)\n\t\t\t\t\t}\n\n\t\t\t\t\tfmt.Println(data)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tstdinReader := startLineReader(ctx, bufio.NewReader(os.Stdin).ReadBytes)\n\t\tif root.Verbosity > 0 {\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Listening on stdin for MCP requests\\n\\n\")\n\t\t}\n\n\t\tfor {\n\t\t\tline, err := stdinReader()\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF || err == context.Canceled {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Error reading input: %v\\n\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tif strings.TrimSpace(string(line)) == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmsg, err := jsonrpc2.DecodeMessage(line)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Error decoding request: %v\\n\", err)\n\t\t\t\tfmt.Println(conn.CreateErrorResponse(nil, -32700, \"parse error\"))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif root.Verbosity > 0 {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Sending request: %s\\n\", line)\n\t\t\t}\n\n\t\t\terr = conn.SendMessage(line)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Error sending message: %v\\n\", err)\n\n\t\t\t\t// Create error response with the request ID if available\n\t\t\t\tvar requestID *jsonrpc2.ID\n\t\t\t\tif call, ok := msg.(*jsonrpc2.Call); ok {\n\t\t\t\t\tid := call.ID()\n\t\t\t\t\trequestID = &id\n\t\t\t\t}\n\n\t\t\t\tfmt.Println(conn.CreateErrorResponse(requestID, -32700, \"error sending message\"))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t},\n}\n\ntype lineResult[T any] struct {\n\tres T\n\terr error\n}\n\nfunc startLineReader[T any](ctx context.Context, rd func(byte) (T, error)) func() (T, error) {\n\tchannel := make(chan lineResult[T])\n\tgo func() {\n\t\tfor {\n\t\t\tline, err := rd('\\n') // wait for Enter key\n\t\t\tif err != nil {\n\t\t\t\tchannel <- lineResult[T]{err: err}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tchannel <- lineResult[T]{res: line}\n\t\t}\n\t}()\n\treturn func() (T, error) {\n\t\tvar t T\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn t, ctx.Err()\n\t\tcase result := <-channel:\n\t\t\tif result.err != nil {\n\t\t\t\treturn t, result.err\n\t\t\t}\n\t\t\treturn result.res, nil\n\t\t}\n\t}\n}\n\nfunc eventReader(reader func() (string, error)) func() (typ, data string, err error) {\n\treturn func() (typ, data string, err error) {\n\t\tvar line string\n\t\tfor {\n\t\t\tline, err = reader()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", \"\", err\n\t\t\t}\n\t\t\tif strings.HasPrefix(line, \"event:\") {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ttyp = strings.TrimSpace(strings.TrimPrefix(line, \"event:\"))\n\t\tline, err = reader()\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tif !strings.HasPrefix(line, \"data:\") {\n\t\t\treturn \"\", \"\", fmt.Errorf(\"expected data: prefix, got %q\", line)\n\t\t}\n\t\tdata = strings.TrimSpace(strings.TrimPrefix(line, \"data:\"))\n\t\treturn typ, data, nil\n\t}\n}\n\nfunc init() {\n\tmcpCmd.AddCommand(runCmd)\n\trunCmd.Flags().StringVar(&appID, \"app\", \"\", \"The app ID to use for the MCP session\")\n\n\tmcpCmd.AddCommand(startCmd)\n\tstartCmd.Flags().StringVar(&appID, \"app\", \"\", \"The app ID to use for the MCP session\")\n\n\troot.Cmd.AddCommand(mcpCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/namespace/namespace.go",
    "content": "package namespace\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar nsCmd = &cobra.Command{\n\tUse:     \"namespace\",\n\tShort:   \"Manage infrastructure namespaces\",\n\tAliases: []string{\"ns\"},\n}\n\nfunc init() {\n\toutput := cmdutil.Oneof{Value: \"columns\", Allowed: []string{\"columns\", \"json\"}}\n\tlistCmd := &cobra.Command{\n\t\tUse:     \"list\",\n\t\tShort:   \"List infrastructure namespaces\",\n\t\tAliases: []string{\"ls\"},\n\t\tArgs:    cobra.NoArgs,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\t\tdefer cancel()\n\n\t\t\tappRoot, _ := cmdutil.AppRoot()\n\t\t\tdaemon := cmdutil.ConnectDaemon(ctx)\n\t\t\tresp, err := daemon.ListNamespaces(ctx, &daemonpb.ListNamespacesRequest{AppRoot: appRoot})\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\t\t\tnss := resp.Namespaces\n\n\t\t\t// Sort by active first, then name second.\n\t\t\tslices.SortFunc(nss, func(a, b *daemonpb.Namespace) int {\n\t\t\t\tif a.Active != b.Active {\n\t\t\t\t\tif a.Active {\n\t\t\t\t\t\treturn -1\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn cmp.Compare(a.Name, b.Name)\n\t\t\t})\n\n\t\t\tif output.Value == \"json\" {\n\t\t\t\tvar buf bytes.Buffer\n\t\t\t\tbuf.WriteByte('[')\n\t\t\t\tfor i, ns := range nss {\n\t\t\t\t\tdata, err := protojson.MarshalOptions{\n\t\t\t\t\t\tUseProtoNames:   true,\n\t\t\t\t\t\tEmitUnpopulated: true,\n\t\t\t\t\t}.Marshal(ns)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcmdutil.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tif i > 0 {\n\t\t\t\t\t\tbuf.WriteByte(',')\n\t\t\t\t\t}\n\t\t\t\t\tbuf.Write(data)\n\t\t\t\t}\n\t\t\t\tbuf.WriteByte(']')\n\n\t\t\t\tvar dst bytes.Buffer\n\t\t\t\tif err := json.Indent(&dst, buf.Bytes(), \"\", \"  \"); err != nil {\n\t\t\t\t\tcmdutil.Fatal(err)\n\t\t\t\t}\n\t\t\t\t_, _ = fmt.Fprintln(os.Stdout, dst.String())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.StripEscape)\n\n\t\t\t_, _ = fmt.Fprint(w, \"NAME\\tID\\tACTIVE\\n\")\n\n\t\t\tfor _, ns := range nss {\n\t\t\t\tactive := \"\"\n\t\t\t\tif ns.Active {\n\t\t\t\t\tactive = \"yes\"\n\t\t\t\t}\n\t\t\t\t_, _ = fmt.Fprintf(w, \"%s\\t%s\\t%s\\n\", ns.Name, ns.Id, active)\n\t\t\t}\n\t\t\t_ = w.Flush()\n\t\t},\n\t}\n\toutput.AddFlag(listCmd)\n\n\tnsCmd.AddCommand(listCmd)\n}\n\nvar createCmd = &cobra.Command{\n\tUse:   \"create NAME\",\n\tShort: \"Create a new infrastructure namespace\",\n\n\tArgs: cobra.ExactArgs(1),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\tappRoot, _ := cmdutil.AppRoot()\n\t\tdaemon := cmdutil.ConnectDaemon(ctx)\n\t\tns, err := daemon.CreateNamespace(ctx, &daemonpb.CreateNamespaceRequest{\n\t\t\tAppRoot: appRoot,\n\t\t\tName:    args[0],\n\t\t})\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t\t_, _ = fmt.Fprintf(os.Stdout, \"created namespace %s\\n\", ns.Name)\n\t},\n}\n\nvar deleteCmd = &cobra.Command{\n\tUse:     \"delete NAME\",\n\tShort:   \"Delete an infrastructure namespace\",\n\tAliases: []string{\"del\"},\n\n\tArgs:              cobra.ExactArgs(1),\n\tValidArgsFunction: namespaceListCompletion,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\tappRoot, _ := cmdutil.AppRoot()\n\t\tdaemon := cmdutil.ConnectDaemon(ctx)\n\t\tname := args[0]\n\t\t_, err := daemon.DeleteNamespace(ctx, &daemonpb.DeleteNamespaceRequest{\n\t\t\tAppRoot: appRoot,\n\t\t\tName:    name,\n\t\t})\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t\t_, _ = fmt.Fprintf(os.Stdout, \"deleted namespace %s\\n\", name)\n\t},\n}\n\nfunc init() {\n\tvar create bool\n\tswitchCmd := &cobra.Command{\n\t\tUse:   \"switch [--create] NAME\",\n\t\tShort: \"Switch to a different infrastructure namespace\",\n\t\tLong: `Switch to a specified infrastructure namespace. Subsequent commands will use the given namespace by default.\n\nIf -c is specified, the namespace will first be created before switching to it.\n\nYou can use '-' as the namespace name to switch back to the previously active namespace.\n`,\n\n\t\tArgs:              cobra.ExactArgs(1),\n\t\tValidArgsFunction: namespaceListCompletion,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\t\tdefer cancel()\n\n\t\t\tappRoot, _ := cmdutil.AppRoot()\n\t\t\tdaemon := cmdutil.ConnectDaemon(ctx)\n\t\t\tns, err := daemon.SwitchNamespace(ctx, &daemonpb.SwitchNamespaceRequest{\n\t\t\t\tAppRoot: appRoot,\n\t\t\t\tName:    args[0],\n\t\t\t\tCreate:  create,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tcmdutil.Fatal(err)\n\t\t\t}\n\t\t\t_, _ = fmt.Fprintf(os.Stdout, \"switched to namespace %s\\n\", ns.Name)\n\t\t},\n\t}\n\n\tswitchCmd.Flags().BoolVarP(&create, \"create\", \"c\", false, \"create the namespace before switching\")\n\tnsCmd.AddCommand(switchCmd)\n}\n\nfunc init() {\n\tnsCmd.AddCommand(createCmd)\n\tnsCmd.AddCommand(deleteCmd)\n\troot.Cmd.AddCommand(nsCmd)\n}\n\nfunc namespaceListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t// List namespaces from the daemon for completion.\n\tctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)\n\tdefer cancel()\n\n\tappRoot, _ := cmdutil.AppRoot()\n\tdaemon := cmdutil.ConnectDaemon(ctx)\n\tresp, err := daemon.ListNamespaces(ctx, &daemonpb.ListNamespacesRequest{AppRoot: appRoot})\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveError\n\t}\n\n\tnamespaces := make([]string, len(resp.Namespaces))\n\tfor i, ns := range resp.Namespaces {\n\t\tnamespaces[i] = ns.Name\n\t}\n\treturn namespaces, cobra.ShellCompDirectiveNoFileComp\n}\n"
  },
  {
    "path": "cli/cmd/encore/rand.go",
    "content": "package main\n\nimport (\n\tcryptorand \"crypto/rand\"\n\t\"encoding/base32\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/pkg/words\"\n)\n\nvar randCmd = &cobra.Command{\n\tUse:   \"rand\",\n\tShort: \"Utilities for generating cryptographically secure random data\",\n}\n\nfunc init() {\n\trootCmd.AddCommand(randCmd)\n}\n\n// UUID command\nfunc init() {\n\tvar v1, v4, v6, v7 bool\n\tuuidCmd := &cobra.Command{\n\t\tUse:   \"uuid [-1|-4|-6|-7]\",\n\t\tShort: \"Generates a random UUID (defaults to version 4)\",\n\t\tArgs:  cobra.NoArgs,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tversions := map[bool]func() (uuid.UUID, error){\n\t\t\t\tv1: uuid.NewV1,\n\t\t\t\tv4: uuid.NewV4,\n\t\t\t\tv6: uuid.NewV6,\n\t\t\t\tv7: uuid.NewV7,\n\t\t\t}\n\t\t\tfn, ok := versions[true]\n\t\t\tif !ok {\n\t\t\t\tfatalf(\"unsupported UUID version\")\n\t\t\t}\n\t\t\tu, err := fn()\n\t\t\tif err != nil {\n\t\t\t\tfatalf(\"failed to generate UUID: %v\", err)\n\t\t\t}\n\t\t\t_, _ = fmt.Println(u.String())\n\t\t},\n\t}\n\tuuidCmd.Flags().BoolVarP(&v1, \"v1\", \"1\", false, \"Generate a version 1 UUID\")\n\tuuidCmd.Flags().BoolVarP(&v4, \"v4\", \"4\", true, \"Generate a version 4 UUID\")\n\tuuidCmd.Flags().BoolVarP(&v6, \"v6\", \"6\", false, \"Generate a version 6 UUID\")\n\tuuidCmd.Flags().BoolVarP(&v7, \"v7\", \"7\", false, \"Generate a version 7 UUID\")\n\tuuidCmd.MarkFlagsMutuallyExclusive(\"v1\", \"v4\", \"v6\", \"v7\")\n\n\trandCmd.AddCommand(uuidCmd)\n}\n\n// Bytes command\nfunc init() {\n\tformat := cmdutil.Oneof{\n\t\tValue:     \"hex\",\n\t\tAllowed:   []string{\"hex\", \"base32\", \"base32hex\", \"base32crockford\", \"base64\", \"base64url\", \"raw\"},\n\t\tFlag:      \"format\",\n\t\tFlagShort: \"f\",\n\t\tDesc:      \"Output format\",\n\t}\n\n\tnoPadding := false\n\tdoFormat := func(data []byte) string {\n\t\tswitch format.Value {\n\t\tcase \"hex\":\n\t\t\treturn hex.EncodeToString(data)\n\t\tcase \"base32\":\n\t\t\tenc := base32.StdEncoding\n\t\t\tif noPadding {\n\t\t\t\tenc = enc.WithPadding(base32.NoPadding)\n\t\t\t}\n\t\t\treturn enc.EncodeToString(data)\n\t\tcase \"base32hex\":\n\t\t\tenc := base32.HexEncoding\n\t\t\tif noPadding {\n\t\t\t\tenc = enc.WithPadding(base32.NoPadding)\n\t\t\t}\n\t\t\treturn enc.EncodeToString(data)\n\t\tcase \"base32crockford\":\n\t\t\tenc := base32.NewEncoding(\"0123456789ABCDEFGHJKMNPQRSTVWXYZ\")\n\t\t\tif noPadding {\n\t\t\t\tenc = enc.WithPadding(base32.NoPadding)\n\t\t\t}\n\t\t\treturn enc.EncodeToString(data)\n\t\tcase \"base64\":\n\t\t\tenc := base64.StdEncoding\n\t\t\tif noPadding {\n\t\t\t\tenc = enc.WithPadding(base64.NoPadding)\n\t\t\t}\n\t\t\treturn enc.EncodeToString(data)\n\t\tcase \"base64url\":\n\t\t\tenc := base64.URLEncoding\n\t\t\tif noPadding {\n\t\t\t\tenc = enc.WithPadding(base64.NoPadding)\n\t\t\t}\n\t\t\treturn enc.EncodeToString(data)\n\t\tdefault:\n\t\t\tfatalf(\"unsupported output format: %s\", format.Value)\n\t\t\tpanic(\"unreachable\")\n\t\t}\n\t}\n\n\tbytesCmd := &cobra.Command{\n\t\tUse:   \"bytes BYTES [-f \" + format.Alternatives() + \"]\",\n\t\tShort: \"Generates random bytes and outputs them in the specified format\",\n\t\tArgs:  cobra.ExactArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tnum, err := strconv.ParseInt(args[0], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tfatalf(\"invalid number of bytes: %v\", err)\n\t\t\t} else if num < 1 {\n\t\t\t\tfatalf(\"number of bytes must be positive\")\n\t\t\t} else if num > 1024*1024 {\n\t\t\t\tfatalf(\"too many bytes requested\")\n\t\t\t}\n\n\t\t\tdata := make([]byte, num)\n\t\t\t_, err = cryptorand.Read(data)\n\t\t\tif err != nil {\n\t\t\t\tfatalf(\"failed to generate random bytes: %v\", err)\n\t\t\t}\n\n\t\t\tif format.Value == \"raw\" {\n\t\t\t\t_, err = os.Stdout.Write(data)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfatalf(\"failed to write: %v\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tformatted := doFormat(data)\n\t\t\t\tif _, err := os.Stdout.WriteString(formatted); err != nil {\n\t\t\t\t\tfatalf(\"failed to write: %v\", err)\n\t\t\t\t}\n\t\t\t\t_, _ = os.Stdout.Write([]byte{'\\n'})\n\t\t\t}\n\t\t},\n\t}\n\n\tformat.AddFlag(bytesCmd)\n\tbytesCmd.Flags().BoolVar(&noPadding, \"no-padding\", false, \"omit padding characters from base32/base64 output\")\n\trandCmd.AddCommand(bytesCmd)\n}\n\n// Words command\nfunc init() {\n\tvar sep string\n\twordsCmd := &cobra.Command{\n\t\tUse:   \"words [--sep=SEPARATOR] NUM\",\n\t\tShort: \"Generates random 4-5 letter words for memorable passphrases\",\n\t\tArgs:  cobra.ExactArgs(1),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tnum, err := strconv.ParseInt(args[0], 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tfatalf(\"invalid number of words: %v\", err)\n\t\t\t} else if num < 1 {\n\t\t\t\tfatalf(\"number of words must be positive\")\n\t\t\t} else if num > 1024 {\n\t\t\t\tfatalf(\"too many words requested\")\n\t\t\t}\n\n\t\t\tselected, err := words.Select(int(num))\n\t\t\tif err != nil {\n\t\t\t\tfatalf(\"failed to select words: %v\", err)\n\t\t\t}\n\n\t\t\tformatted := strings.Join(selected, sep)\n\t\t\tif _, err := os.Stdout.WriteString(formatted); err != nil {\n\t\t\t\tfatalf(\"failed to write: %v\", err)\n\t\t\t}\n\t\t\t_, _ = os.Stdout.Write([]byte{'\\n'})\n\t\t},\n\t}\n\n\twordsCmd.Flags().StringVarP(&sep, \"sep\", \"s\", \" \", \"separator between words\")\n\trandCmd.AddCommand(wordsCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/root/rootcmd.go",
    "content": "package root\n\nimport (\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/pkg/errlist\"\n)\n\nvar (\n\tVerbosity int\n\ttraceFile string\n\n\t// TraceFile is the file to write trace logs to.\n\t// If nil (the default), trace logs are not written.\n\tTraceFile *string\n)\n\nvar preRuns []func(cmd *cobra.Command, args []string)\n\n// AddPreRun adds a function to be executed before the command runs.\nfunc AddPreRun(f func(cmd *cobra.Command, args []string)) {\n\tpreRuns = append(preRuns, f)\n}\n\nvar Cmd = &cobra.Command{\n\tUse:           \"encore\",\n\tShort:         \"encore is the fastest way of developing backend applications\",\n\tSilenceErrors: true, // We'll handle displaying an error in our main func\n\tCompletionOptions: cobra.CompletionOptions{\n\t\tHiddenDefaultCmd: true, // Hide the \"completion\" command from help (used for generating auto-completions for the shell)\n\t},\n\tPersistentPreRun: func(cmd *cobra.Command, args []string) {\n\t\tif traceFile != \"\" {\n\t\t\tTraceFile = &traceFile\n\t\t}\n\n\t\tlevel := zerolog.InfoLevel\n\t\tif Verbosity == 1 {\n\t\t\tlevel = zerolog.DebugLevel\n\t\t} else if Verbosity >= 2 {\n\t\t\tlevel = zerolog.TraceLevel\n\t\t}\n\n\t\tif Verbosity >= 1 {\n\t\t\terrlist.Verbose = true\n\t\t}\n\t\tlog.Logger = log.Logger.Level(level)\n\n\t\tfor _, f := range preRuns {\n\t\t\tf(cmd, args)\n\t\t}\n\t},\n}\n\nfunc init() {\n\tCmd.PersistentFlags().CountVarP(&Verbosity, \"verbose\", \"v\", \"verbose output\")\n\tCmd.PersistentFlags().StringVar(&traceFile, \"trace\", \"\", \"file to write execution trace data to\")\n}\n"
  },
  {
    "path": "cli/cmd/encore/run.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/term\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\t\"encr.dev/cli/internal/onboarding\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar (\n\tcolor   bool\n\tnoColor bool // for \"--no-color\" compatibility\n\tdebug   = cmdutil.Oneof{\n\t\tValue:       \"\",\n\t\tNoOptDefVal: \"enabled\",\n\t\tAllowed:     []string{\"enabled\", \"break\"},\n\t\tFlag:        \"debug\",\n\t\tFlagShort:   \"\", // no short flag\n\t\tDesc:        \"Compile for debugging (disables some optimizations)\",\n\t\tTypeDesc:    \"string\",\n\t}\n\twatch    bool\n\tlisten   string\n\tlogLevel = cmdutil.Oneof{\n\t\tValue:     \"\",\n\t\tAllowed:   []string{\"trace\", \"debug\", \"info\", \"warn\", \"error\"},\n\t\tFlag:      \"level\",\n\t\tFlagShort: \"l\",\n\t\tDesc:      \"Minimum log level to display\",\n\t\tTypeDesc:  \"string\",\n\t}\n\tport               uint\n\tjsonLogs           bool\n\tscrubSensitiveData bool\n\tbrowser            = cmdutil.Oneof{\n\t\tValue:     \"auto\",\n\t\tAllowed:   []string{\"auto\", \"never\", \"always\"},\n\t\tFlag:      \"browser\",\n\t\tFlagShort: \"\", // no short flag\n\t\tDesc:      \"Whether to open the local development dashboard in the browser on startup\",\n\t\tTypeDesc:  \"string\",\n\t}\n)\n\nfunc init() {\n\trunCmd := &cobra.Command{\n\t\tUse:   \"run [--debug] [--watch=true] [--level=TRACE] [--port=4000] [--listen=<listen-addr>]\",\n\t\tShort: \"Runs your application\",\n\t\tArgs:  cobra.NoArgs,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tappRoot, wd := determineAppRoot()\n\t\t\t// If the user didn't explicitly set --watch and we're in debug mode, disable watching\n\t\t\t// as we typically don't want to swap the process when the user is debugging.\n\t\t\tif !cmd.Flag(\"watch\").Changed && debug.Value != \"\" {\n\t\t\t\twatch = false\n\t\t\t}\n\t\t\trunApp(appRoot, wd)\n\t\t},\n\t}\n\n\tisTerm := term.IsTerminal(int(os.Stdout.Fd()))\n\n\trootCmd.AddCommand(runCmd)\n\trunCmd.Flags().BoolVarP(&watch, \"watch\", \"w\", true, \"Watch for changes and live-reload\")\n\trunCmd.Flags().StringVar(&listen, \"listen\", \"\", \"Address to listen on (for example \\\"0.0.0.0:4000\\\")\")\n\trunCmd.Flags().UintVarP(&port, \"port\", \"p\", 4000, \"Port to listen on\")\n\trunCmd.Flags().BoolVar(&jsonLogs, \"json\", false, \"Display logs in JSON format\")\n\trunCmd.Flags().StringVarP(&nsName, \"namespace\", \"n\", \"\", \"Namespace to use (defaults to active namespace)\")\n\trunCmd.Flags().BoolVar(&color, \"color\", isTerm, \"Whether to display colorized output\")\n\trunCmd.Flags().BoolVar(&noColor, \"no-color\", false, \"Equivalent to --color=false\")\n\trunCmd.Flags().BoolVar(&scrubSensitiveData, \"redact\", false, \"Redact sensitive data in traces when running locally\")\n\trunCmd.Flags().MarkHidden(\"no-color\")\n\tlogLevel.AddFlag(runCmd)\n\tdebug.AddFlag(runCmd)\n\tbrowser.AddFlag(runCmd)\n}\n\n// runApp runs the app.\nfunc runApp(appRoot, wd string) {\n\tctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)\n\tdefer cancel()\n\n\t// Determine listen addr.\n\tvar listenAddr string\n\n\tif listen == \"\" {\n\t\t// If we have no listen address at all, listen on localhost.\n\t\t// (we do this so MacOS's firewall doesn't ask for permission for the daemon to listen on all interfaces)\n\t\tlistenAddr = fmt.Sprintf(\"127.0.0.1:%d\", port)\n\t} else if _, _, err := net.SplitHostPort(listen); err == nil {\n\t\t// If --listen is given with a port, use that directly and ignore --port.\n\t\tlistenAddr = listen\n\t} else {\n\t\t// Otherwise use --listen as the host and --port as the port.\n\t\tlistenAddr = net.JoinHostPort(listen, strconv.Itoa(int(port)))\n\t}\n\n\tbrowserMode := daemonpb.RunRequest_BROWSER_AUTO\n\tswitch browser.Value {\n\tcase \"auto\":\n\t\tbrowserMode = daemonpb.RunRequest_BROWSER_AUTO\n\tcase \"never\":\n\t\tbrowserMode = daemonpb.RunRequest_BROWSER_NEVER\n\tcase \"always\":\n\t\tbrowserMode = daemonpb.RunRequest_BROWSER_ALWAYS\n\t}\n\n\tdebugMode := daemonpb.RunRequest_DEBUG_DISABLED\n\tswitch debug.Value {\n\tcase \"enabled\":\n\t\tdebugMode = daemonpb.RunRequest_DEBUG_ENABLED\n\tcase \"break\":\n\t\tdebugMode = daemonpb.RunRequest_DEBUG_BREAK\n\t}\n\n\tdaemon := setupDaemon(ctx)\n\tstream, err := daemon.Run(ctx, &daemonpb.RunRequest{\n\t\tAppRoot:            appRoot,\n\t\tDebugMode:          debugMode,\n\t\tWatch:              watch,\n\t\tWorkingDir:         wd,\n\t\tListenAddr:         listenAddr,\n\t\tEnviron:            os.Environ(),\n\t\tTraceFile:          root.TraceFile,\n\t\tNamespace:          nonZeroPtr(nsName),\n\t\tBrowser:            browserMode,\n\t\tLogLevel:           nonZeroPtr(logLevel.Value),\n\t\tScrubSensitiveData: scrubSensitiveData,\n\t})\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\n\tcmdutil.ClearTerminalExceptFirstNLines(1)\n\n\tvar converter cmdutil.OutputConverter\n\tif !jsonLogs {\n\t\tconverter = cmdutil.ConvertJSONLogs(cmdutil.Colorize(color && !noColor))\n\t}\n\tcode := cmdutil.StreamCommandOutput(stream, converter)\n\tif code == 0 {\n\t\tif state, err := onboarding.Load(); err == nil {\n\t\t\tif state.DeployHint.Set() {\n\t\t\t\tif err := state.Write(); err == nil {\n\t\t\t\t\t_, _ = fmt.Println(aurora.Sprintf(\"\\nHint: deploy your app to the cloud by running: %s\", aurora.Cyan(\"git push encore\")))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tos.Exit(code)\n}\n\nfunc init() {\n}\n"
  },
  {
    "path": "cli/cmd/encore/secrets/archive.go",
    "content": "package secrets\n\nimport (\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n)\n\nvar archiveSecretCmd = &cobra.Command{\n\tDeprecated:            \"Use the command 'encore secret delete <id>' to delete the secret group.\\n\",\n\tUse:                   \"archive <id>\",\n\tShort:                 \"Archives a secret value\",\n\tDisableFlagsInUseLine: true,\n\tArgs:                  cobra.ExactArgs(1),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tdoArchiveOrUnarchive(args[0], true)\n\t},\n}\n\nvar unarchiveSecretCmd = &cobra.Command{\n\tDeprecated:            \"use the command 'encore secret delete <id>' to delete the secret group.\\n\",\n\tUse:                   \"unarchive <id>\",\n\tShort:                 \"Unarchives a secret value\",\n\tDisableFlagsInUseLine: true,\n\tArgs:                  cobra.ExactArgs(1),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tdoArchiveOrUnarchive(args[0], false)\n\t},\n}\n\nfunc doArchiveOrUnarchive(groupID string, archive bool) {\n\tif !strings.HasPrefix(groupID, \"secgrp\") {\n\t\tcmdutil.Fatal(\"the id must begin with 'secgrp_'. Valid ids can be found with 'encore secret list <key>'.\")\n\t}\n\t// newer version\n\t// do nothing since we are providing the deprecated string from the cobra command\n}\n\nfunc init() {\n\tsecretCmd.AddCommand(archiveSecretCmd)\n\tsecretCmd.AddCommand(unarchiveSecretCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/secrets/delete.go",
    "content": "package secrets\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/platform\"\n)\n\nvar forceFlag bool\n\nvar deleteSecretCmd = &cobra.Command{\n\tUse:                   \"delete <id>\",\n\tShort:                 \"Deletes a secret value\",\n\tDisableFlagsInUseLine: true,\n\tArgs:                  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// Check if --yes / --force flag was passed to skip confirmation\n\t\tif !forceFlag {\n\t\t\tfmt.Printf(\"Are you sure you want to delete secret %q? [y/N]: \", args[0])\n\t\t\tvar response string\n\t\t\t_, _ = fmt.Scanln(&response)\n\t\t\tif response != \"y\" && response != \"yes\" {\n\t\t\t\tfmt.Println(\"Aborted.\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tdoDelete(args[0], true)\n\t\treturn nil\n\t},\n}\n\nfunc doDelete(groupID string, delete bool) {\n\tif !strings.HasPrefix(groupID, \"secgrp\") {\n\t\tcmdutil.Fatal(\"the id must begin with 'secgrp_'. Valid ids can be found with 'encore secret list <key>'.\")\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\terr := platform.UpdateSecretGroup(ctx, platform.UpdateSecretGroupParams{\n\t\tID:     groupID,\n\t\tDelete: &delete,\n\t})\n\tif err != nil {\n\t\tcmdutil.Fatal(err)\n\t}\n\tfmt.Printf(\"Successfully deleted secret group %s.\\n\", groupID)\n}\n\nfunc init() {\n\tdeleteSecretCmd.Flags().BoolVarP(&forceFlag, \"yes\", \"y\", false, \"Skip confirmation prompt\")\n\tsecretCmd.AddCommand(deleteSecretCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/secrets/list.go",
    "content": "package secrets\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/cli/internal/platform/gql\"\n)\n\nvar listSecretCmd = &cobra.Command{\n\tUse:                   \"list [keys...]\",\n\tShort:                 \"Lists secrets, optionally for a specific key\",\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tappSlug := cmdutil.AppSlug()\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\n\t\tvar keys []string\n\t\tif len(args) > 0 {\n\t\t\tkeys = args\n\t\t}\n\t\tsecrets, err := platform.ListSecretGroups(ctx, appSlug, keys)\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\n\t\tif keys == nil {\n\t\t\t// Print secrets overview\n\t\t\tvar buf bytes.Buffer\n\t\t\tw := tabwriter.NewWriter(&buf, 0, 0, 3, ' ', tabwriter.StripEscape)\n\n\t\t\t_, _ = fmt.Fprint(w, \"Secret Key\\tProduction\\tDevelopment\\tLocal\\tPreview\\tSpecific Envs\\t\\n\")\n\t\t\tconst (\n\t\t\t\tcheckYes = \"\\u2713\"\n\t\t\t\tcheckNo  = \"\\u2717\"\n\t\t\t)\n\t\t\tfor _, s := range secrets {\n\t\t\t\trender := func(b bool) string {\n\t\t\t\t\tif b {\n\t\t\t\t\t\treturn checkYes\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn checkNo\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\td := getSecretEnvDesc(s.Groups)\n\t\t\t\tif !d.hasAny {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t_, _ = fmt.Fprintf(w, \"%s\\t%v\\t%v\\t%v\\t%v\\t\", s.Key,\n\t\t\t\t\trender(d.prod), render(d.dev), render(d.local), render(d.preview))\n\t\t\t\t// Render specific envs, if any\n\t\t\t\tfor i, env := range d.specific {\n\t\t\t\t\tif i > 0 {\n\t\t\t\t\t\t_, _ = fmt.Fprintf(w, \",\")\n\t\t\t\t\t}\n\t\t\t\t\t_, _ = fmt.Fprintf(w, \"%s\", env.Name)\n\t\t\t\t}\n\n\t\t\t\t_, _ = fmt.Fprint(w, \"\\t\\n\")\n\t\t\t}\n\t\t\t_ = w.Flush()\n\n\t\t\t// Add color to the checkmarks now that the table is correctly laid out.\n\t\t\t// We can't do it before since the tabwriter will get the alignment wrong\n\t\t\t// if we include a bunch of ANSI escape codes that it doesn't understand.\n\t\t\tr := strings.NewReplacer(checkYes, color.GreenString(checkYes), checkNo, color.RedString(checkNo))\n\t\t\t_, _ = r.WriteString(os.Stdout, buf.String())\n\t\t} else {\n\t\t\t// Specific secrets\n\t\t\tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)\n\t\t\t_, _ = fmt.Fprint(w, \"ID\\tSecret Key\\tEnvironment(s)\\t\\n\")\n\n\t\t\tslices.SortFunc(secrets, func(a, b *gql.Secret) int {\n\t\t\t\treturn cmp.Compare(a.Key, b.Key)\n\t\t\t})\n\t\t\tfor _, s := range secrets {\n\t\t\t\t// Sort the archived groups to the end\n\t\t\t\tslices.SortFunc(s.Groups, func(a, b *gql.SecretGroup) int {\n\t\t\t\t\taa, ab := a.ArchivedAt != nil, b.ArchivedAt != nil\n\t\t\t\t\tif aa != ab {\n\t\t\t\t\t\tif aa {\n\t\t\t\t\t\t\treturn 1\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn -1\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if aa {\n\t\t\t\t\t\treturn a.ArchivedAt.Compare(*b.ArchivedAt)\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn cmp.Compare(a.ID, b.ID)\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tfor _, g := range s.Groups {\n\t\t\t\t\tvar sel []string\n\t\t\t\t\tfor _, s := range g.Selector {\n\t\t\t\t\t\tswitch s := s.(type) {\n\t\t\t\t\t\tcase *gql.SecretSelectorSpecificEnv:\n\t\t\t\t\t\t\t// If we have a specific environment, render the name\n\t\t\t\t\t\t\t// instead of the id (which is the default when using s.String()).\n\t\t\t\t\t\t\tsel = append(sel, \"env:\"+s.Env.Name)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tsel = append(sel, s.String())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\ts := fmt.Sprintf(\"%s\\t%s\\t%s\\t\", g.ID, s.Key, strings.Join(sel, \", \"))\n\t\t\t\t\tif g.DestroyedAt != nil {\n\t\t\t\t\t\ts += \"(destroyed)\\t\"\n\t\t\t\t\t\t_, _ = color.New(color.CrossedOut).Fprintln(w, s)\n\t\t\t\t\t} else if g.ArchivedAt != nil {\n\t\t\t\t\t\ts += \"(archived)\\t\"\n\t\t\t\t\t\t_, _ = color.New(color.Concealed).Fprintln(w, s)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_, _ = fmt.Fprintln(w, s)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ = w.Flush()\n\t\t}\n\t},\n}\n\nfunc init() {\n\tsecretCmd.AddCommand(listSecretCmd)\n}\n\ntype secretEnvDesc struct {\n\thasAny                    bool // if there are any non-archived groups at all\n\tprod, dev, local, preview bool\n\tspecific                  []*gql.Env\n}\n\nfunc getSecretEnvDesc(groups []*gql.SecretGroup) secretEnvDesc {\n\tvar desc secretEnvDesc\n\tfor _, g := range groups {\n\t\tif g.ArchivedAt != nil {\n\t\t\tcontinue\n\t\t}\n\t\tdesc.hasAny = true\n\t\tfor _, sel := range g.Selector {\n\t\t\tswitch sel := sel.(type) {\n\t\t\tcase *gql.SecretSelectorEnvType:\n\t\t\t\tswitch sel.Kind {\n\t\t\t\tcase \"production\":\n\t\t\t\t\tdesc.prod = true\n\t\t\t\tcase \"development\":\n\t\t\t\t\tdesc.dev = true\n\t\t\t\tcase \"local\":\n\t\t\t\t\tdesc.local = true\n\t\t\t\tcase \"preview\":\n\t\t\t\t\tdesc.preview = true\n\t\t\t\t}\n\t\t\tcase *gql.SecretSelectorSpecificEnv:\n\t\t\t\tdesc.specific = append(desc.specific, sel.Env)\n\t\t\t}\n\t\t}\n\t}\n\treturn desc\n}\n"
  },
  {
    "path": "cli/cmd/encore/secrets/secrets.go",
    "content": "package secrets\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/root\"\n)\n\nvar secretCmd = &cobra.Command{\n\tUse:     \"secret\",\n\tShort:   \"Secret management commands\",\n\tAliases: []string{\"secrets\"},\n}\n\nfunc init() {\n\troot.Cmd.AddCommand(secretCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/secrets/set.go",
    "content": "package secrets\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/crypto/ssh/terminal\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/cli/internal/platform/gql\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar setSecretCmd = &cobra.Command{\n\tUse:   \"set --type <types> <secret-name>\",\n\tShort: \"Sets a secret value\",\n\tLong: `\nSets a secret value for one or more environment types.\n\nThe valid environment types are 'prod', 'dev', 'pr' and 'local'.\n`,\n\n\tExample: `\nEntering a secret directly in terminal:\n\n\t$ encore secret set --type dev,local MySecret\n\tEnter secret value: ...\n\tSuccessfully created secret value for MySecret.\n\nPiping a secret from a file:\n\n\t$ encore secret set --type dev,local,pr MySecret < my-secret.txt\n\tSuccessfully created secret value for MySecret.\n\nNote that this strips trailing newlines from the secret value.`,\n\tArgs:                  cobra.ExactArgs(1),\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tsetSecret(args[0])\n\t},\n}\n\nvar secretEnvs secretEnvSelector\n\ntype secretEnvSelector struct {\n\tdevFlag  bool\n\tprodFlag bool\n\tenvTypes []string\n\tenvNames []string\n}\n\nfunc init() {\n\tsecretCmd.AddCommand(setSecretCmd)\n\tsetSecretCmd.Flags().BoolVarP(&secretEnvs.devFlag, \"dev\", \"d\", false, \"To set the secret for development use\")\n\tsetSecretCmd.Flags().BoolVarP(&secretEnvs.prodFlag, \"prod\", \"p\", false, \"To set the secret for production use\")\n\tsetSecretCmd.Flags().StringSliceVarP(&secretEnvs.envTypes, \"type\", \"t\", nil, \"environment type(s) to set for (comma-separated list)\")\n\tsetSecretCmd.Flags().StringSliceVarP(&secretEnvs.envNames, \"env\", \"e\", nil, \"environment name(s) to set for (comma-separated list)\")\n\t_ = setSecretCmd.Flags().MarkHidden(\"dev\")\n\t_ = setSecretCmd.Flags().MarkHidden(\"prod\")\n}\n\nfunc setSecret(key string) {\n\tplaintextValue := readSecretValue()\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tappRoot, _ := cmdutil.AppRoot()\n\tappSlug := cmdutil.AppSlug()\n\tsel := secretEnvs.ParseSelector(ctx, appSlug)\n\n\tapp, err := platform.GetApp(ctx, appSlug)\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"unable to lookup app %s: %v\", appSlug, err)\n\t}\n\n\t// Does a matching secret group already exist?\n\tsecrets, err := platform.ListSecretGroups(ctx, app.Slug, []string{key})\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"unable to list secrets: %v\", err)\n\t}\n\n\tif matching := findMatchingSecretGroup(secrets, key, sel); matching != nil {\n\t\t// We found a matching secret group. Update it.\n\t\terr := platform.CreateSecretVersion(ctx, platform.CreateSecretVersionParams{\n\t\t\tGroupID:        matching.ID,\n\t\t\tPlaintextValue: plaintextValue,\n\t\t\tEtag:           matching.Etag,\n\t\t})\n\t\tif err != nil {\n\t\t\tcmdutil.Fatalf(\"unable to update secret: %v\", err)\n\t\t}\n\t\tfmt.Printf(\"Successfully updated secret value for %s.\\n\", key)\n\t\treturn\n\t}\n\n\t// Otherwise create a new secret group.\n\terr = platform.CreateSecretGroup(ctx, platform.CreateSecretGroupParams{\n\t\tAppID:          app.ID,\n\t\tKey:            key,\n\t\tPlaintextValue: plaintextValue,\n\t\tSelector:       sel,\n\t\tDescription:    \"\", // not yet supported from CLI\n\t})\n\tif err != nil {\n\t\tif ce, ok := getConflictError(err); ok {\n\t\t\tvar errMsg strings.Builder\n\t\t\tfmt.Fprintln(&errMsg, \"the environment selection conflicts with other secret values:\")\n\t\t\tfor _, c := range ce.Conflicts {\n\t\t\t\tfmt.Fprintf(&errMsg, \"\\t%s %s\\n\", c.GroupID, strings.Join(c.Conflicts, \", \"))\n\t\t\t}\n\t\t\tcmdutil.Fatal(errMsg.String())\n\t\t}\n\t\tcmdutil.Fatalf(\"unable to create secret: %v\", err)\n\t}\n\n\tdaemon := cmdutil.ConnectDaemon(ctx)\n\tif _, err := daemon.SecretsRefresh(ctx, &daemonpb.SecretsRefreshRequest{AppRoot: appRoot}); err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"warning: failed to refresh secret secret, skipping:\", err)\n\t}\n\n\tfmt.Printf(\"Successfully created secret value for %s.\\n\", key)\n}\n\nfunc (s secretEnvSelector) ParseSelector(ctx context.Context, appSlug string) []gql.SecretSelector {\n\tif s.devFlag && s.prodFlag {\n\t\tcmdutil.Fatal(\"cannot specify both --dev and --prod\")\n\t} else if s.devFlag && (len(s.envTypes) > 0 || len(s.envNames) > 0) {\n\t\tcmdutil.Fatal(\"cannot combine --dev with --type/--env\")\n\t} else if s.prodFlag && (len(s.envTypes) > 0 || len(s.envNames) > 0) {\n\t\tcmdutil.Fatal(\"cannot combine --prod with --type/--env\")\n\t}\n\n\t// Look up the environments\n\tenvMap := make(map[string]string) // name -> id\n\tenvs, err := platform.ListEnvs(ctx, appSlug)\n\tif err != nil {\n\t\tcmdutil.Fatalf(\"unable to list environments: %v\", err)\n\t}\n\tfor _, env := range envs {\n\t\tenvMap[env.Slug] = env.ID\n\t}\n\n\tvar sel []gql.SecretSelector\n\tif s.devFlag {\n\t\tsel = append(sel,\n\t\t\t&gql.SecretSelectorEnvType{Kind: \"development\"},\n\t\t\t&gql.SecretSelectorEnvType{Kind: \"preview\"},\n\t\t\t&gql.SecretSelectorEnvType{Kind: \"local\"},\n\t\t)\n\t} else if s.prodFlag {\n\t\tsel = append(sel, &gql.SecretSelectorEnvType{Kind: \"production\"})\n\t} else {\n\t\t// Parse env types and env names\n\t\tseenTypes := make(map[string]bool)\n\t\tvalidTypes := map[string]string{\n\t\t\t// Actual names\n\t\t\t\"development\": \"development\",\n\t\t\t\"production\":  \"production\",\n\t\t\t\"preview\":     \"preview\",\n\t\t\t\"local\":       \"local\",\n\n\t\t\t// Aliases\n\t\t\t\"dev\":       \"development\",\n\t\t\t\"prod\":      \"production\",\n\t\t\t\"pr\":        \"preview\",\n\t\t\t\"ephemeral\": \"preview\",\n\t\t}\n\n\t\tfor _, t := range s.envTypes {\n\t\t\tval, ok := validTypes[t]\n\t\t\tif !ok {\n\t\t\t\tcmdutil.Fatalf(\"invalid environment type %q\", t)\n\t\t\t}\n\t\t\tif !seenTypes[val] {\n\t\t\t\tseenTypes[val] = true\n\t\t\t\tsel = append(sel, &gql.SecretSelectorEnvType{Kind: val})\n\t\t\t}\n\t\t}\n\t\tfor _, n := range s.envNames {\n\t\t\tenvID, ok := envMap[n]\n\t\t\tif !ok {\n\t\t\t\tcmdutil.Fatalf(\"environment %q not found\", n)\n\t\t\t}\n\t\t\tsel = append(sel, &gql.SecretSelectorSpecificEnv{Env: &gql.Env{ID: envID}})\n\t\t}\n\t}\n\n\tif len(sel) == 0 {\n\t\tcmdutil.Fatal(\"must specify at least one environment with --type/--env (or --dev/--prod)\")\n\t}\n\treturn sel\n}\n\n// readSecretValue reads the secret value from the user.\n// If it's a terminal it becomes an interactive prompt,\n// otherwise it reads from stdin.\nfunc readSecretValue() string {\n\tvar value string\n\tfd := syscall.Stdin\n\tif terminal.IsTerminal(int(fd)) {\n\t\tfmt.Fprint(os.Stderr, \"Enter secret value: \")\n\t\tdata, err := terminal.ReadPassword(int(fd))\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t\tvalue = string(data)\n\t\tfmt.Fprintln(os.Stderr)\n\t} else {\n\t\tdata, err := io.ReadAll(os.Stdin)\n\t\tif err != nil {\n\t\t\tcmdutil.Fatal(err)\n\t\t}\n\t\tvalue = string(bytes.TrimRight(data, \"\\r\\n\"))\n\t}\n\treturn value\n}\n\n// findMatchingSecretGroup find whether a matching secret group already exists\n// for the given secret key and selector.\nfunc findMatchingSecretGroup(secrets []*gql.Secret, key string, selector []gql.SecretSelector) *gql.SecretGroup {\n\t// canonicalize returns the secret selectors in canonical form\n\tcanonicalize := func(sels []gql.SecretSelector) []string {\n\t\tvar strs []string\n\t\tfor _, s := range sels {\n\t\t\tstrs = append(strs, s.String())\n\t\t}\n\t\tsort.Strings(strs)\n\t\treturn strs\n\t}\n\n\twant := canonicalize(selector)\n\tfor _, s := range secrets {\n\t\tif s.Key == key {\n\t\t\tfor _, g := range s.Groups {\n\t\t\t\tgot := canonicalize(g.Selector)\n\t\t\t\tif slices.Equal(got, want) {\n\t\t\t\t\treturn g\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getConflictError(err error) (*gql.ConflictError, bool) {\n\tvar gqlErr gql.ErrorList\n\tif !errors.As(err, &gqlErr) {\n\t\treturn nil, false\n\t}\n\tfor _, e := range gqlErr {\n\t\tif conflict := e.Extensions[\"conflict\"]; len(conflict) > 0 {\n\t\t\tvar cerr gql.ConflictError\n\t\t\tif err := json.Unmarshal(conflict, &cerr); err == nil {\n\t\t\t\treturn &cerr, true\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "cli/cmd/encore/sqlc.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/golang/protobuf/proto\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/sqlc-dev/sqlc/pkg/cli\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"encr.dev/proto/encore/daemon\"\n)\n\ntype sqlcSQL struct {\n\tSchema  string        `json:\"schema\"`\n\tQueries string        `json:\"queries\"`\n\tEngine  string        `json:\"engine\"`\n\tCodegen []sqlcCodegen `json:\"codegen\"`\n}\n\ntype sqlcCodegen struct {\n\tOut    string `json:\"out\"`\n\tPlugin string `json:\"plugin\"`\n}\n\ntype sqlcPlugin struct {\n\tName    string      `json:\"name\"`\n\tProcess sqlcProcess `json:\"process\"`\n}\n\ntype sqlcProcess struct {\n\tCmd string `json:\"cmd\"`\n}\n\ntype sqlcConfig struct {\n\tVersion string       `json:\"version\"`\n\tSQL     []sqlcSQL    `json:\"sql\"`\n\tPlugins []sqlcPlugin `json:\"plugins\"`\n}\n\nfunc init() {\n\tvar useProto bool\n\tgenCmd := &cobra.Command{\n\t\tUse:    \"generate-sql-schema <migration-dir>\",\n\t\tShort:  \"Plugin for SQLC: stores the parsed sqlc model in a protobuf file\",\n\t\tHidden: true,\n\t\tArgs:   cobra.ExactArgs(1),\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\tschemaPath, err := filepath.Abs(args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttmpDir, err := os.MkdirTemp(\"\", \"encore-sqlc\")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\t_ = os.RemoveAll(tmpDir)\n\t\t\t}()\n\n\t\t\tsqlcPath := filepath.Join(tmpDir, \"sqlc.json\")\n\t\t\tqueryPath := filepath.Join(tmpDir, \"query.sql\")\n\t\t\toutPath := filepath.Join(tmpDir, \"gen\")\n\t\t\t// SQLC requires the schema path to be relative to the sqlc.json file\n\t\t\tschemaPath, err = filepath.Rel(tmpDir, schemaPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcfg := sqlcConfig{\n\t\t\t\tVersion: \"2\",\n\t\t\t\tSQL: []sqlcSQL{\n\t\t\t\t\t{\n\t\t\t\t\t\tSchema:  schemaPath,\n\t\t\t\t\t\tQueries: \"query.sql\",\n\t\t\t\t\t\tEngine:  \"postgresql\",\n\t\t\t\t\t\tCodegen: []sqlcCodegen{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tOut:    \"gen\",\n\t\t\t\t\t\t\t\tPlugin: \"encore\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPlugins: []sqlcPlugin{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"encore\",\n\t\t\t\t\t\tProcess: sqlcProcess{\n\t\t\t\t\t\t\tCmd: os.Args[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcfgData, err := json.Marshal(cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = os.WriteFile(sqlcPath, cfgData, 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// SQLC requires at least one query to be present in the query file\n\t\t\terr = os.WriteFile(queryPath, []byte(\"-- name: Dummy :one\\nSELECT 'dummy';\"), 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tres := cli.Run([]string{\"generate\", \"-f\", sqlcPath})\n\t\t\tif res != 0 {\n\t\t\t\treturn fmt.Errorf(\"sqlc exited with code %d\", res)\n\t\t\t}\n\t\t\treqBlob, err := os.ReadFile(filepath.Join(outPath, \"output.pb\"))\n\t\t\tif !useProto {\n\t\t\t\treq := &daemon.SQLCPlugin_GenerateRequest{}\n\t\t\t\tif err := proto.Unmarshal(reqBlob, req); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treqBlob, err = protojson.MarshalOptions{\n\t\t\t\t\tEmitUnpopulated: true,\n\t\t\t\t\tIndent:          \"  \",\n\t\t\t\t\tUseProtoNames:   true,\n\t\t\t\t}.Marshal(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw := bufio.NewWriter(os.Stdout)\n\t\t\tif _, err := w.Write(reqBlob); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := w.Flush(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\tgenCmd.Flags().BoolVar(&useProto, \"proto\", false, \"Output the parsed schema as protobuf\")\n\tpluginCmd := &cobra.Command{\n\t\tUse:    \"/plugin.CodegenService/Generate\",\n\t\tShort:  \"Plugin for SQLC: stores the parsed sqlc model in a protobuf file\",\n\t\tHidden: true,\n\t\tArgs:   cobra.NoArgs,\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\treqBlob, err := io.ReadAll(os.Stdin)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tresp := &daemon.SQLCPlugin_GenerateResponse{\n\t\t\t\tFiles: []*daemon.SQLCPlugin_File{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:     \"output.pb\",\n\t\t\t\t\t\tContents: reqBlob,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\trespBlob, err := proto.Marshal(resp)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tw := bufio.NewWriter(os.Stdout)\n\t\t\tif _, err := w.Write(respBlob); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := w.Flush(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\trootCmd.AddCommand(genCmd)\n\trootCmd.AddCommand(pluginCmd)\n}\n"
  },
  {
    "path": "cli/cmd/encore/telemetry.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/cmd/encore/root\"\n\t\"encr.dev/cli/internal/telemetry\"\n\t\"encr.dev/pkg/fns\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar TelemetryDisabledByEnvVar = os.Getenv(\"DISABLE_ENCORE_TELEMETRY\") == \"1\"\nvar TelemetryDebugByEnvVar = os.Getenv(\"ENCORE_TELEMETRY_DEBUG\") == \"1\"\n\nfunc printTelemetryStatus() {\n\tstatus := aurora.Green(\"Enabled\").String()\n\tif !telemetry.IsEnabled() {\n\t\tstatus = aurora.Red(\"Disabled\").String()\n\t}\n\tfmt.Println(aurora.Sprintf(\"%s\\n\", aurora.Bold(\"Encore Telemetry\")))\n\titems := [][2]string{\n\t\t{\"Status\", status},\n\t}\n\tif root.Verbosity > 0 {\n\t\titems = append(items, [2]string{\"Install ID\", telemetry.GetAnonID()})\n\t}\n\tif telemetry.IsDebug() {\n\t\titems = append(items, [2]string{\"Debug\", aurora.Green(\"Enabled\").String()})\n\t}\n\tmaxKeyLen := fns.Max(items, func(entry [2]string) int { return len(entry[0]) })\n\tfor _, item := range items {\n\t\tspacing := strings.Repeat(\" \", maxKeyLen-len(item[0]))\n\t\tfmt.Printf(\"%s: %s%s\\n\", item[0], spacing, item[1])\n\t}\n\tfmt.Println(aurora.Sprintf(\"\\nLearn more: %s\", aurora.Underline(\"https://encore.dev/docs/telemetry\")))\n}\n\nfunc updateTelemetry(ctx context.Context) {\n\t// Update the telemetry config on the daemon if it is running\n\tif cmdutil.IsDaemonRunning(ctx) {\n\t\tdaemon := cmdutil.ConnectDaemon(ctx)\n\t\t_, err := daemon.Telemetry(ctx, &daemonpb.TelemetryConfig{\n\t\t\tAnonId:  telemetry.GetAnonID(),\n\t\t\tEnabled: telemetry.IsEnabled(),\n\t\t\tDebug:   telemetry.IsDebug(),\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Debug().Err(err).Msgf(\"could not update daemon telemetry: %s\", err)\n\t\t}\n\t}\n\tif err := telemetry.SaveConfig(); err != nil {\n\t\tlog.Debug().Err(err).Msgf(\"could not save telemetry: %s\", err)\n\t}\n}\n\nvar telemetryCommand = &cobra.Command{\n\tUse:   \"telemetry\",\n\tShort: \"Reports the current telemetry status\",\n\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tprintTelemetryStatus()\n\t},\n}\n\nvar telemetryEnableCommand = &cobra.Command{\n\tUse:   \"enable\",\n\tShort: \"Enables telemetry reporting\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif telemetry.SetEnabled(true) {\n\t\t\tupdateTelemetry(cmd.Context())\n\t\t}\n\t\tprintTelemetryStatus()\n\t},\n}\n\nvar telemetryDisableCommand = &cobra.Command{\n\tUse:   \"disable\",\n\tShort: \"Disables telemetry reporting\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif telemetry.SetEnabled(false) {\n\t\t\tupdateTelemetry(cmd.Context())\n\t\t}\n\t\tprintTelemetryStatus()\n\t},\n}\n\nfunc isCommand(cmd *cobra.Command, name ...string) bool {\n\tfor cmd != nil {\n\t\tif slices.Contains(name, cmd.Name()) {\n\t\t\treturn true\n\t\t}\n\t\tcmd = cmd.Parent()\n\t}\n\treturn false\n}\n\nfunc init() {\n\ttelemetryCommand.AddCommand(telemetryEnableCommand, telemetryDisableCommand)\n\trootCmd.AddCommand(telemetryCommand)\n\troot.AddPreRun(func(cmd *cobra.Command, args []string) {\n\t\tupdate := false\n\t\tif TelemetryDisabledByEnvVar {\n\t\t\tupdate = telemetry.SetEnabled(false)\n\t\t}\n\t\tif cmd.Use == \"daemon\" {\n\t\t\treturn\n\t\t}\n\t\tupdate = update || telemetry.SetDebug(TelemetryDebugByEnvVar)\n\t\tif update {\n\t\t\tgo updateTelemetry(cmd.Context())\n\t\t}\n\t\tif telemetry.ShouldShowWarning() && !isCommand(cmd, \"version\", \"completion\") {\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(aurora.Sprintf(\"%s: This CLI tool collects usage data to help us improve Encore.\", aurora.Bold(\"Note\")))\n\t\t\tfmt.Println(aurora.Sprintf(\"      You can disable this by running '%s'.\\n\", aurora.Yellow(\"encore telemetry disable\")))\n\t\t\ttelemetry.SetShownWarning()\n\t\t}\n\t})\n\n}\n"
  },
  {
    "path": "cli/cmd/encore/test.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar testCmd = &cobra.Command{\n\tUse:   \"test [go test flags]\",\n\tShort: \"Tests your application\",\n\tLong:  \"Takes all the same flags as `go test`.\",\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tvar (\n\t\t\ttraceFile    string\n\t\t\tcodegenDebug bool\n\t\t\tprepareOnly  bool\n\t\t\tnoColor      bool\n\t\t)\n\t\t// Support specific args but otherwise let all args be passed on to \"go test\"\n\t\tfor i := 0; i < len(args); i++ {\n\t\t\targ := args[i]\n\t\t\tif arg == \"-h\" || arg == \"--help\" {\n\t\t\t\t_ = cmd.Help()\n\t\t\t\treturn\n\t\t\t} else if arg == \"--trace\" || strings.HasPrefix(arg, \"--trace=\") {\n\t\t\t\t// Drop this argument always.\n\t\t\t\targs = slices.Delete(args, i, i+1)\n\t\t\t\ti--\n\n\t\t\t\t// We either have '--trace=file' or '--trace file'.\n\t\t\t\t// Handle both.\n\t\t\t\tif _, value, ok := strings.Cut(arg, \"=\"); ok {\n\t\t\t\t\ttraceFile = value\n\t\t\t\t} else {\n\t\t\t\t\t// Make sure there is a next argument.\n\t\t\t\t\tif i < len(args) {\n\t\t\t\t\t\ttraceFile = args[i]\n\t\t\t\t\t\targs = slices.Delete(args, i, i+1)\n\t\t\t\t\t\ti--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if arg == \"--codegen-debug\" {\n\t\t\t\tcodegenDebug = true\n\t\t\t\targs = slices.Delete(args, i, i+1)\n\t\t\t\ti--\n\t\t\t} else if arg == \"--prepare\" {\n\t\t\t\tprepareOnly = true\n\t\t\t\targs = slices.Delete(args, i, i+1)\n\t\t\t\ti--\n\t\t\t} else if arg == \"--no-color\" {\n\t\t\t\tnoColor = true\n\t\t\t\targs = slices.Delete(args, i, i+1)\n\t\t\t\ti--\n\t\t\t}\n\t\t}\n\n\t\tappRoot, relPath := determineAppRoot()\n\t\texitCode, err := runTests(appRoot, relPath, args, traceFile, codegenDebug, prepareOnly, noColor)\n\t\tif err != nil {\n\t\t\tfatal(err)\n\t\t}\n\t\tos.Exit(exitCode)\n\t},\n}\n\nfunc runTests(appRoot, testDir string, args []string, traceFile string, codegenDebug, prepareOnly, noColor bool) (int, error) {\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tgo func() {\n\t\t<-interrupt\n\t\tcancel()\n\t}()\n\n\tconverter := cmdutil.ConvertJSONLogs(cmdutil.Colorize(!noColor))\n\tif slices.Contains(args, \"-json\") {\n\t\tconverter = convertTestEventOutputOnly(converter)\n\t}\n\n\tvar tempDir string\n\t// only use temp dir if we are not compiling tests or only running prepare\n\tif !prepareOnly && !slices.Contains(args, \"-o\") && !slices.Contains(args, \"-c\") {\n\t\tvar err error\n\t\ttempDir, err = os.MkdirTemp(\"\", \"encore-test\")\n\t\tif err != nil {\n\t\t\treturn 1, fmt.Errorf(\"couldn't create temp dir for test: %w\", err)\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = os.RemoveAll(tempDir)\n\t\t}()\n\t}\n\n\tdaemon := setupDaemon(ctx)\n\n\t// Is this a node package?\n\tpackageJsonPath := filepath.Join(appRoot, \"package.json\")\n\tif _, err := os.Stat(packageJsonPath); err == nil || prepareOnly {\n\t\tspec, err := daemon.TestSpec(ctx, &daemonpb.TestSpecRequest{\n\t\t\tAppRoot:    appRoot,\n\t\t\tWorkingDir: testDir,\n\t\t\tArgs:       args,\n\t\t\tEnviron:    os.Environ(),\n\t\t\tTempDir:    tempDir,\n\t\t})\n\t\tif status.Code(err) == codes.NotFound {\n\t\t\treturn 1, errors.New(\"application does not define any tests.\\nNote: Add a 'test' script command to package.json to run tests.\")\n\t\t} else if err != nil {\n\t\t\treturn 1, err\n\t\t}\n\n\t\tif prepareOnly {\n\t\t\tfor _, ln := range spec.Environ {\n\t\t\t\tfmt.Println(ln)\n\t\t\t}\n\t\t\treturn 0, nil\n\t\t}\n\n\t\tcmd := exec.Command(spec.Command, spec.Args...)\n\t\tcmd.Env = spec.Environ\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tcmd.Stdin = os.Stdin\n\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tvar exitErr *exec.ExitError\n\t\t\tif errors.As(err, &exitErr) {\n\t\t\t\treturn exitErr.ExitCode(), nil\n\t\t\t} else {\n\t\t\t\treturn 1, err\n\t\t\t}\n\t\t}\n\t\treturn 0, nil\n\t}\n\n\tstream, err := daemon.Test(ctx, &daemonpb.TestRequest{\n\t\tAppRoot:      appRoot,\n\t\tWorkingDir:   testDir,\n\t\tArgs:         args,\n\t\tEnviron:      os.Environ(),\n\t\tTraceFile:    nonZeroPtr(traceFile),\n\t\tCodegenDebug: codegenDebug,\n\t\tTempDir:      tempDir,\n\t})\n\tif err != nil {\n\t\treturn 1, err\n\t}\n\treturn cmdutil.StreamCommandOutput(stream, converter), nil\n}\n\nfunc init() {\n\ttestCmd.DisableFlagParsing = true\n\trootCmd.AddCommand(testCmd)\n\n\t// Even though we've disabled flag parsing, we still need to define the flags\n\t// so that the help text is correct.\n\ttestCmd.Flags().Bool(\"codegen-debug\", false, \"Dump generated code (for debugging Encore's code generation)\")\n\ttestCmd.Flags().Bool(\"prepare\", false, \"Prepare for running tests (without running them)\")\n\ttestCmd.Flags().String(\"trace\", \"\", \"Specifies a trace file to write trace information about the parse and compilation process to.\")\n\ttestCmd.Flags().Bool(\"no-color\", false, \"Disable colorized output\")\n\n}\n\nfunc convertTestEventOutputOnly(converter cmdutil.OutputConverter) cmdutil.OutputConverter {\n\treturn func(line []byte) []byte {\n\t\t// If this isn't a JSON log line, just return it as-is\n\t\tif len(line) == 0 || line[0] != '{' {\n\t\t\treturn line\n\t\t}\n\n\t\ttestEvent := &testJSONEvent{}\n\t\tif err := json.Unmarshal(line, testEvent); err == nil && testEvent.Action == \"output\" {\n\t\t\tif testEvent.Output != nil && (*(testEvent.Output))[0] == '{' {\n\t\t\t\tconvertedLogs := textBytes(converter(*testEvent.Output))\n\t\t\t\ttestEvent.Output = &convertedLogs\n\n\t\t\t\tnewLine, err := json.Marshal(testEvent)\n\t\t\t\tif err == nil {\n\t\t\t\t\treturn append(newLine, '\\n')\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn line\n\t}\n}\n\n// testJSONEvent and textBytes taken from the Go source code\ntype testJSONEvent struct {\n\tTime    *time.Time `json:\",omitempty\"`\n\tAction  string\n\tPackage string     `json:\",omitempty\"`\n\tTest    string     `json:\",omitempty\"`\n\tElapsed *float64   `json:\",omitempty\"`\n\tOutput  *textBytes `json:\",omitempty\"`\n}\n\n// textBytes is a hack to get JSON to emit a []byte as a string\n// without actually copying it to a string.\n// It implements encoding.TextMarshaler, which returns its text form as a []byte,\n// and then json encodes that text form as a string (which was our goal).\ntype textBytes []byte\n\nfunc (b *textBytes) MarshalText() ([]byte, error) { return *b, nil }\nfunc (b *textBytes) UnmarshalText(in []byte) error {\n\t*b = in\n\treturn nil\n}\n"
  },
  {
    "path": "cli/cmd/encore/version.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/spf13/cobra\"\n\n\t\"encr.dev/cli/internal/update\"\n\t\"encr.dev/internal/version\"\n)\n\nvar versionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Reports the current version of the encore application\",\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tvar (\n\t\t\tver *update.LatestVersion\n\t\t\terr error\n\t\t)\n\t\tif version.Version != \"\" {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\t\tdefer cancel()\n\t\t\tver, err = update.Check(ctx)\n\t\t}\n\n\t\t// NOTE: This output format is relied on by the Encore IntelliJ plugin.\n\t\t// Don't change this without considering its impact on that plugin.\n\t\tfmt.Fprintln(os.Stdout, \"encore version\", version.Version)\n\n\t\tif err != nil {\n\t\t\tfatalf(\"could not check for update: %v\", err)\n\t\t} else if ver.IsNewer(version.Version) {\n\t\t\tif ver.ForceUpgrade {\n\t\t\t\tfmt.Println(aurora.Red(\"An urgent security update for Encore is available.\"))\n\t\t\t\tif ver.SecurityNotes != \"\" {\n\t\t\t\t\tfmt.Println(aurora.Sprintf(aurora.Yellow(\"%s\"), ver.SecurityNotes))\n\t\t\t\t}\n\n\t\t\t\tversionUpdateCmd.Run(cmd, args)\n\t\t\t} else {\n\t\t\t\tif ver.SecurityUpdate {\n\t\t\t\t\tfmt.Println(aurora.Sprintf(aurora.Red(\"A security update is update available: %s -> %s\\nUpdate with: encore version update\"), version.Version, ver.Version()))\n\n\t\t\t\t\tif ver.SecurityNotes != \"\" {\n\t\t\t\t\t\tfmt.Println(aurora.Sprintf(aurora.Yellow(\"%s\"), ver.SecurityNotes))\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Println(aurora.Sprintf(aurora.Yellow(\"Update available: %s -> %s\\nUpdate with: encore version update\"), version.Version, ver.Version()))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n}\n\nvar versionUpdateCmd = &cobra.Command{\n\tUse:   \"update\",\n\tShort: \"Checks for an update of encore and, if one is available, runs the appropriate command to update it.\",\n\n\tDisableFlagsInUseLine: true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif version.Version == \"\" || strings.HasPrefix(version.Version, \"devel\") {\n\t\t\tfatal(\"cannot update development build, first install Encore from https://encore.dev/docs/install\")\n\t\t}\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\t\tver, err := update.Check(ctx)\n\t\tif err != nil {\n\t\t\tfatalf(\"could not check for update: %v\", err)\n\t\t}\n\t\tif ver.IsNewer(version.Version) {\n\t\t\tfmt.Printf(\"Upgrading Encore to %v...\\n\", ver.Version())\n\n\t\t\tif err := ver.DoUpgrade(os.Stdout, os.Stderr); err != nil {\n\t\t\t\tfatalf(\"could not update: %v\", err)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tos.Exit(0)\n\t\t} else {\n\t\t\tfmt.Println(\"Encore already up to date.\")\n\t\t}\n\t},\n}\n\nfunc init() {\n\tversionCmd.AddCommand(versionUpdateCmd)\n\trootCmd.AddCommand(versionCmd)\n}\n"
  },
  {
    "path": "cli/cmd/git-remote-encore/main.go",
    "content": "// Command git-remote-encore provides a gitremote helper for\n// interacting with Encore's git hosting without SSH keys,\n// by piggybacking on Encore's auth tokens.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/internal/conf\"\n)\n\nfunc main() {\n\tif err := run(os.Args); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"%s: %v\\n\", os.Args[0], err)\n\t\tos.Exit(1)\n\t}\n}\n\nvar isLocalTest = (func() bool {\n\treturn filepath.Base(os.Args[0]) == \"git-remote-encorelocal\"\n})()\n\n// remoteScheme is the remote scheme we expect.\n// It's \"encore\" in general but \"encorelocal\" for local development.\nvar remoteScheme = (func() string {\n\tif isLocalTest {\n\t\treturn \"encorelocal\"\n\t} else {\n\t\treturn \"encore\"\n\t}\n})()\n\nfunc run(args []string) error {\n\tstdin := bufio.NewReader(os.Stdin)\n\tstdout := os.Stdout\n\n\t// Read commands from stdin.\n\tfor {\n\t\tcmd, err := stdin.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unexpected error reading stdin: %v\", err)\n\t\t}\n\t\tcmd = cmd[:len(cmd)-1] // skip trailing newline\n\t\tswitch {\n\t\tcase cmd == \"capabilities\":\n\t\t\tif _, err := stdout.Write([]byte(\"*connect\\n\\n\")); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase strings.HasPrefix(cmd, \"connect \"):\n\t\t\tservice := cmd[len(\"connect \"):]\n\t\t\treturn connect(args, service)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unsupported command: %s\", cmd)\n\t\t}\n\t}\n}\n\n// connect implements the \"connect\" capability by copying data\n// to and from the remote end over gRPC.\nfunc connect(args []string, svc string) error {\n\turi, err := url.Parse(args[2])\n\tif err != nil {\n\t\treturn fmt.Errorf(\"connect %s: invalid remote uri: %v\", os.Args[2], err)\n\t} else if uri.Scheme != remoteScheme {\n\t\treturn fmt.Errorf(\"connect %s: expected remote scheme %q, got %q\", os.Args[2], remoteScheme, uri.Scheme)\n\t}\n\tappID := uri.Hostname()\n\n\tts := conf.NewTokenSource()\n\ttok, err := ts.Token()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not get Encore auth token: %v\", err)\n\t}\n\n\tf, err := os.CreateTemp(\"\", \"encore-token-auth-sentinel-key\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeyPath := f.Name()\n\tdefer func() { _ = os.Remove(keyPath) }()\n\n\tif err := f.Chmod(0600); err != nil {\n\t\t_ = f.Close()\n\t\treturn err\n\t} else if _, err := f.Write([]byte(SentinelPrivateKey)); err != nil {\n\t\t_ = f.Close()\n\t\treturn err\n\t} else if err := f.Close(); err != nil {\n\t\treturn err\n\t}\n\n\t// Create a dummy config file so that we can work around any host overrides\n\t// present on the system.\n\tcfg, err := os.CreateTemp(\"\", \"encore-dummy-ssh-config\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfgPath := cfg.Name()\n\tdefer func() { _ = os.Remove(cfgPath) }()\n\n\t// Communicate to Git that the connection is established.\n\t_, _ = os.Stdout.Write([]byte(\"\\n\"))\n\n\tsshServer, port := \"git.encore.dev\", \"22\"\n\tif isLocalTest {\n\t\tsshServer, port = \"localhost\", \"9040\"\n\t}\n\n\t// Set up an SSH tunnel with a sentinel key as a way to signal\n\t// Encore to use token-based authentication, and pass the token\n\t// as part of the command.\n\tcmd := exec.Command(\"ssh\",\n\t\t\"-x\", \"-T\",\n\t\t\"-F\", cfgPath,\n\t\t\"-o\", \"IdentitiesOnly=yes\",\n\t\t\"-i\", keyPath,\n\t\t\"-p\", port,\n\t\tsshServer,\n\t\tfmt.Sprintf(\"token=%s %s '%s'\", tok.AccessToken, svc, appID))\n\tcmd.Env = []string{}\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n\n// SentinelPrivateKey is a sentinel private key that Encore recognizes as\n// the key that communicates that the user wishes to do token-based authentication\n// instead of key-based authentication.\n//\n// NOTE: This is not a security problem. The key is meant to be public\n// and does not serve as a means of authentication.\n// nosemgrep\nconst SentinelPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACCyj3F5Tp1eBIp7rMohszumYzlys/BFfmX/LVkXJS8magAAAJjsp3yz7Kd8\nswAAAAtzc2gtZWQyNTUxOQAAACCyj3F5Tp1eBIp7rMohszumYzlys/BFfmX/LVkXJS8mag\nAAAEDMiwRrf5WET2mTKjKjX7z6vox3n6hKGKbP7V4MDtVre7KPcXlOnV4EinusyiGzO6Zj\nOXKz8EV+Zf8tWRclLyZqAAAAE2VuY29yZS1zZW50aW5lbC1rZXkBAg==\n-----END OPENSSH PRIVATE KEY-----\n`\n"
  },
  {
    "path": "cli/cmd/tsbundler-encore/main.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/evanw/esbuild/pkg/api\"\n\n\t\"encr.dev/internal/version\"\n\n\tflag \"github.com/spf13/pflag\"\n)\n\nvar (\n\tentryPoints      []string\n\tspecifiedEngines []string\n\t// replacementFile  string\n\toutDir   string\n\tbundle   bool\n\tminify   bool\n\thelp     bool\n\tlogLevel int\n)\n\n// main is the entry point for the tsbundler-encore command.\n//\n// It is responsible for parsing the command line flags, validating the input, and then triggering esbuild.\n//\n// Run with --help for more information.\nfunc main() {\n\t// Required flags\n\t// flag.StringVar(&replacementFile, \"replacements\", \"\", \"Replacement file or json object (default read from stdin)\")\n\n\t// Optional flags\n\tflag.StringVar(&outDir, \"outdir\", \"dist\", \"Output directory\")\n\tflag.BoolVar(&bundle, \"bundle\", true, \"Bundle all dependencies\")\n\tflag.BoolVar(&minify, \"minify\", false, \"Minify output (default false)\")\n\tflag.StringArrayVar(&specifiedEngines, \"engine\", []string{\"node:21\"}, \"Target engine\")\n\tflag.CountVarP(&logLevel, \"verbose\", \"v\", \"Increase logging level (can be specified multiple times)\")\n\tflag.BoolVarP(&help, \"help\", \"h\", false, \"Print help\")\n\tflag.Usage = printHelp\n\tflag.Parse()\n\n\tentryPoints = flag.Args()\n\tif help {\n\t\tprintHelp()\n\t\tos.Exit(0)\n\t}\n\n\t// Validate input (note: these functions will exit on error)\n\tvalidateEntrypointParams()\n\tengines := readEngines()\n\t// replacements := readReplacementMapping()\n\n\t// Create our transformer plugin\n\t// rewritePlugin := api.Plugin{\n\t// \tName: \"encore-codegen-transformer\",\n\t// \tSetup: func(build api.PluginBuild) {\n\t// \t\tbuild.OnLoad(\n\t// \t\t\tapi.OnLoadOptions{Filter: `\\.(ts|js)(x?)$`},\n\t// \t\t\tfunc(args api.OnLoadArgs) (api.OnLoadResult, error) {\n\t// \t\t\t\treplacement, found := replacements[args.Path]\n\t// \t\t\t\tif !found {\n\t// \t\t\t\t\treturn api.OnLoadResult{}, nil\n\t// \t\t\t\t}\n\n\t// \t\t\t\tcontentsBytes, err := os.ReadFile(replacement)\n\t// \t\t\t\tif err != nil {\n\t// \t\t\t\t\treturn api.OnLoadResult{}, fmt.Errorf(\"error reading replacement file: %w\", err)\n\t// \t\t\t\t}\n\t// \t\t\t\tcontent := string(contentsBytes)\n\n\t// \t\t\t\treturn api.OnLoadResult{\n\t// \t\t\t\t\tPluginName: \"encore-codegen-transformer\",\n\t// \t\t\t\t\tContents:   &content,\n\t// \t\t\t\t\tLoader:     api.LoaderTS,\n\t// \t\t\t\t}, nil\n\t// \t\t\t},\n\t// \t\t)\n\t// \t},\n\t// }\n\n\tbanner := `// This file was bundled by Encore ` + version.Version + `\n//\n// https://encore.dev`\n\n\toutBase := \"\"\n\tif len(entryPoints) == 1 {\n\t\t// If there's a single entrypoint, use its directory as the outbase\n\t\t// as otherwise esbuild won't include the \"[dir]\" token in the output.\n\t\toutBase = filepath.Dir(filepath.Dir(entryPoints[0]))\n\t}\n\n\t// Trigger esbuild\n\tresult := api.Build(api.BuildOptions{\n\t\t// Setup base settings\n\t\tLogLevel:  api.LogLevelWarning - api.LogLevel(logLevel),\n\t\tBanner:    map[string]string{\"js\": banner},\n\t\tCharset:   api.CharsetUTF8,\n\t\tSourcemap: api.SourceMapLinked,\n\t\tPackages:  api.PackagesExternal,\n\t\tPlugins:   []api.Plugin{\n\t\t\t// rewritePlugin,\n\t\t},\n\t\tTreeShaking: api.TreeShakingTrue,\n\n\t\t// Set our build target\n\t\tPlatform: api.PlatformNode,\n\t\tFormat:   api.FormatESModule,\n\t\tTarget:   api.ES2022,\n\t\tEngines:  engines,\n\n\t\t// Minification settings\n\t\tMinifyWhitespace:  minify,\n\t\tMinifySyntax:      minify,\n\t\tMinifyIdentifiers: minify,\n\n\t\t// Pass in what we want to build\n\t\tEntryNames:  \"[dir]/[name]\",\n\t\tEntryPoints: entryPoints,\n\t\tBundle:      bundle,\n\t\tOutdir:      outDir,\n\t\tOutbase:     outBase,\n\t\tWrite:       true, // Write to outdir\n\t\tOutExtension: map[string]string{\n\t\t\t\".js\": \".mjs\",\n\t\t},\n\t\tDefine: map[string]string{\n\t\t\t\"ENCORE_DROP_TESTS\": \"true\",\n\t\t},\n\t})\n\n\tif len(result.Errors) > 0 {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc printHelp() {\n\tbinary := filepath.Base(os.Args[0])\n\n\t// Base usage help\n\tversionStr := fmt.Sprintf(\"tsbundler-encore (%s)\", version.Version)\n\t_, _ = fmt.Fprintf(os.Stderr, \"%s\\n%s\\n\", versionStr, strings.Repeat(\"=\", len(versionStr)))\n\t_, _ = fmt.Fprintf(os.Stderr, \"\\nUsage: %s <entry point(s)...> [options]\\n\", binary)\n\tflag.PrintDefaults()\n\n\t// Replacements help\n\t// _, _ = fmt.Fprintf(os.Stderr, \"\\nReplacements JSON Format:\\n\")\n\t// _, _ = fmt.Fprintf(os.Stderr, \"      {\\n\")\n\t// _, _ = fmt.Fprintf(os.Stderr, \"        \\\"/absolute/path/to/file.ts\\\": \\\"/path/to/replacement.ts\\\",\\n\")\n\t// _, _ = fmt.Fprintf(os.Stderr, \"        \\\"/absolute/path/to/file2.ts\\\": \\\"/path/to/replacement2.ts\\\"\\n\")\n\t// _, _ = fmt.Fprintf(os.Stderr, \"      }\\n\")\n\n\t// Engine help\n\t_, _ = fmt.Fprintf(os.Stderr, \"\\nEngines:\\n\\nEngines can be specified as a name, or a name and version separated by a colon,\\nfor example \\\"node:21\\\" or \\\"node\\\". Multiple engines can be specified if required.\\n\\nThe supported engines are:\\n\")\n\t_, _ = fmt.Fprintf(os.Stderr, \"      - node\\n\")\n\t_, _ = fmt.Fprintf(os.Stderr, \"      - bun\\n\")\n\t_, _ = fmt.Fprintf(os.Stderr, \"      - deno\\n\")\n\t_, _ = fmt.Fprintf(os.Stderr, \"      - rhino\\n\")\n}\n\n// validateEntrypointParams validates that the entry points parameters was specified and that all entry points exist\n// and are readable on the file system.\nfunc validateEntrypointParams() {\n\tif len(entryPoints) == 0 {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: at least one entry point must be specified\\n\\n\")\n\t\tprintHelp()\n\t\tos.Exit(1)\n\t}\n\n\tfor _, entryPoint := range entryPoints {\n\t\tif st, err := os.Stat(entryPoint); errors.Is(err, fs.ErrNotExist) {\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: entry point %s does not exist\\n\", entryPoint)\n\t\t\tos.Exit(1)\n\t\t} else if err != nil {\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: error reading entry point %s: %s\\n\", entryPoint, err)\n\t\t\tos.Exit(1)\n\t\t} else if st.IsDir() {\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: entry point %s is a directory\\n\", entryPoint)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n}\n\n// readReplacementMapping reads a replacement mapping from either a file or stdin depending\n// on if the replacementFile flag was specified.\n//\n// It then validates that all the keys are valid paths to files and the values are valid paths to files.\n// func readReplacementMapping() map[string]string {\n// \tout := make(map[string]string)\n\n// \t// If a replacement file was specified, read it\n// \treplacementFile = strings.TrimSpace(replacementFile)\n// \tif replacementFile != \"\" {\n// \t\tif replacementFile[0] == '{' {\n// \t\t\terr := json.Unmarshal([]byte(replacementFile), &out)\n// \t\t\tif err != nil {\n// \t\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error parsing replacement object: %s\\n\", err)\n// \t\t\t\tos.Exit(1)\n// \t\t\t}\n// \t\t} else {\n// \t\t\tdata, err := os.ReadFile(replacementFile)\n// \t\t\tif err != nil {\n// \t\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error reading replacement file: %s\\n\", err)\n// \t\t\t\tos.Exit(1)\n// \t\t\t}\n\n// \t\t\terr = json.Unmarshal(data, &out)\n// \t\t\tif err != nil {\n// \t\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error parsing replacement file: %s\\n\", err)\n// \t\t\t\tos.Exit(1)\n// \t\t\t}\n// \t\t}\n// \t} else {\n// \t\t// Check something is being piped in\n// \t\tinfo, _ := os.Stdin.Stat()\n// \t\tif (info.Mode()&os.ModeCharDevice) != 0 || info.Size() <= 0 {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: no replacement file specified and nothing piped in\\n\")\n// \t\t\tos.Exit(1)\n// \t\t}\n\n// \t\t// Otherwise, read from stdin\n// \t\tif err := json.NewDecoder(os.Stdin).Decode(&out); err != nil {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error reading replacement file from stdin: %s\\n\", err)\n// \t\t\tos.Exit(1)\n// \t\t}\n// \t}\n\n// \t// Validate that all the keys are valid paths to files and the values are valid paths to files\n// \tfor key, value := range out {\n// \t\t// Validate key\n// \t\tif st, err := os.Stat(key); errors.Is(err, fs.ErrNotExist) {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: replacement key %s does not exist\\n\", key)\n// \t\t\tos.Exit(1)\n// \t\t} else if err != nil {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: error reading replacement key %s: %s\\n\", key, err)\n// \t\t\tos.Exit(1)\n// \t\t} else if st.IsDir() {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: replacement key %s is a directory\\n\", key)\n// \t\t\tos.Exit(1)\n// \t\t} else if !filepath.IsAbs(key) {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: replacement key %s is not an absolute path\\n\", key)\n// \t\t\tos.Exit(1)\n// \t\t}\n\n// \t\t// Validate value\n// \t\tif st, err := os.Stat(value); errors.Is(err, fs.ErrNotExist) {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: replacement value %s does not exist\\n\", value)\n// \t\t\tos.Exit(1)\n// \t\t} else if err != nil {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: error reading replacement value %s: %s\\n\", value, err)\n// \t\t\tos.Exit(1)\n// \t\t} else if st.IsDir() {\n// \t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: replacement value %s is a directory\\n\", value)\n// \t\t\tos.Exit(1)\n// \t\t}\n// \t}\n\n// \treturn out\n// }\n\n// readEngines reads the engines from the specified flag and returns a list of engines.\nfunc readEngines() []api.Engine {\n\tif len(specifiedEngines) == 0 {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: at least one engine must be specified\\n\\n\")\n\t\tprintHelp()\n\t\tos.Exit(1)\n\t}\n\n\tvar engines []api.Engine\n\tfor _, engineName := range specifiedEngines {\n\t\tengineName = strings.ToLower(strings.TrimSpace(engineName))\n\t\tengineName, engineVersion, _ := strings.Cut(engineName, \":\")\n\n\t\tvar eng api.Engine\n\t\tswitch engineName {\n\t\tcase \"node\", \"bun\": // Note: esbuild doesn't have a \"bun\" engine (yet), but to future proof we'll alias it to node\n\t\t\teng = api.Engine{Name: api.EngineNode, Version: engineVersion}\n\t\tcase \"deno\":\n\t\t\teng = api.Engine{Name: api.EngineDeno, Version: engineVersion}\n\t\tcase \"rhino\":\n\t\t\teng = api.Engine{Name: api.EngineRhino, Version: engineVersion}\n\t\tdefault:\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"Error: unknown/unsupported engine %s\\n\\n\", engineName)\n\t\t\tprintHelp()\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tengines = append(engines, eng)\n\t}\n\n\treturn engines\n}\n"
  },
  {
    "path": "cli/daemon/apps/apps.go",
    "content": "package apps\n\nimport (\n\t\"database/sql\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang/protobuf/proto\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go4.org/syncutil\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/internal/manifest\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/internal/goldfish\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/watcher\"\n\t\"encr.dev/pkg/xos\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nvar ErrNotFound = errors.New(\"app not found\")\n\nfunc NewManager(db *sql.DB) *Manager {\n\treturn &Manager{\n\t\tdb:        db,\n\t\tinstances: make(map[string]*Instance),\n\t}\n}\n\n// Manager keeps track of known apps and watches them for changes.\ntype Manager struct {\n\tdb         *sql.DB\n\tsetupWatch syncutil.Once\n\n\tappRegMu     sync.Mutex\n\tappListeners []func(*Instance)\n\n\twatchMu  sync.Mutex\n\twatchers []WatchFunc\n\n\tinstanceMu sync.Mutex\n\tinstances  map[string]*Instance // root -> instance\n}\n\ntype TrackOption func(*Instance) error\n\nfunc WithTutorial(tutorial string) TrackOption {\n\treturn func(i *Instance) error {\n\t\terr := manifest.SetTutorial(i.root, tutorial)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"set tutorial\")\n\t\t}\n\t\ti.tutorial = tutorial\n\t\treturn nil\n\t}\n}\n\n// Track begins tracking an app, and marks it as updated\n// if the app is already tracked.\nfunc (mgr *Manager) Track(appRoot string, options ...TrackOption) (*Instance, error) {\n\tapp, err := mgr.resolve(appRoot)\n\tfor _, opt := range options {\n\t\tif err := opt(app); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, err = mgr.db.Exec(`\n\t\tINSERT OR REPLACE INTO app (root, local_id, platform_id, updated_at)\n\t\tVALUES (?, ?, ?, ?)\n\t`, app.root, app.localID, app.PlatformID(), time.Now())\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"update app store\")\n\t}\n\tlog.Info().Str(\"app_id\", app.PlatformOrLocalID()).Msg(\"tracking app\")\n\treturn app, nil\n}\n\n// FindLatestByPlatformID finds the most recently updated app instance with the given platformID.\n// If no such app is found it reports an error matching ErrNotFound.\nfunc (mgr *Manager) FindLatestByPlatformID(platformID string) (*Instance, error) {\n\tvar root string\n\terr := mgr.db.QueryRow(`\n\t\tSELECT root\n\t\tFROM app\n\t\tWHERE platform_id = ?\n\t\tORDER BY updated_at DESC\n\t\tLIMIT 1\n\t`, platformID).Scan(&root)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn nil, errors.WithStack(ErrNotFound)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrap(err, \"query app store\")\n\t}\n\n\treturn mgr.resolve(root)\n}\n\nfunc (mgr *Manager) FindLatestByPlatformOrLocalID(id string) (*Instance, error) {\n\t// Local ID do not contain hyphens, platform ID's always contain hyphens.\n\tif strings.Contains(id, \"-\") {\n\t\treturn mgr.FindLatestByPlatformID(id)\n\t}\n\n\tvar root string\n\terr := mgr.db.QueryRow(`\n\t\tSELECT root\n\t\tFROM app\n\t\tWHERE local_id = ?\n\t\tORDER BY updated_at DESC\n\t\tLIMIT 1\n\t`, id).Scan(&root)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn nil, errors.WithStack(ErrNotFound)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrap(err, \"query app store\")\n\t}\n\n\treturn mgr.resolve(root)\n}\n\n// List lists all known apps.\nfunc (mgr *Manager) List() ([]*Instance, error) {\n\troots, err := mgr.listRoots()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar apps []*Instance\n\tfor _, root := range roots {\n\t\tapp, err := mgr.resolve(root)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\tlog.Debug().Str(\"root\", root).Msg(\"app no longer exists, skipping\")\n\t\t\t// Delete the\n\t\t\t_, _ = mgr.db.Exec(`DELETE FROM app WHERE root = ?`, root)\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\tlog.Error().Err(err).Str(\"root\", root).Msg(\"unable to resolve app\")\n\t\t\tcontinue\n\t\t}\n\t\tapps = append(apps, app)\n\t}\n\n\treturn apps, nil\n}\n\nfunc (mgr *Manager) listRoots() ([]string, error) {\n\trows, err := mgr.db.Query(`SELECT root FROM app`)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"query app roots\")\n\t}\n\tdefer fns.CloseIgnore(rows)\n\n\tvar roots []string\n\tfor rows.Next() {\n\t\tvar root string\n\t\tif err := rows.Scan(&root); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"scan row\")\n\t\t}\n\t\troots = append(roots, root)\n\t}\n\terr = errors.Wrap(rows.Err(), \"iterate rows\")\n\treturn roots, err\n}\n\n// RegisterAppListener registers a callback that gets invoked every time\n// an app is tracked.\nfunc (mgr *Manager) RegisterAppListener(fn func(*Instance)) {\n\tmgr.instanceMu.Lock()\n\tdefer mgr.instanceMu.Unlock()\n\n\tmgr.appRegMu.Lock()\n\tmgr.appListeners = append(mgr.appListeners, fn)\n\tmgr.appRegMu.Unlock()\n\n\t// Call the handler for all existing apps\n\tfor _, inst := range mgr.instances {\n\t\tfn(inst)\n\t}\n}\n\n// WatchFunc is the signature of functions registered as app watchers.\ntype WatchFunc func(*Instance, []watcher.Event)\n\n// WatchAll watches all apps for changes.\nfunc (mgr *Manager) WatchAll(fn WatchFunc) error {\n\terr := mgr.setupWatch.Do(func() error {\n\t\t// Begin tracking all known apps by calling List (since it calls resolve).\n\t\t_, err := mgr.List()\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmgr.watchMu.Lock()\n\tmgr.watchers = append(mgr.watchers, fn)\n\tmgr.watchMu.Unlock()\n\treturn nil\n}\n\nfunc (mgr *Manager) onWatchEvent(i *Instance, ev []watcher.Event) {\n\tmgr.watchMu.Lock()\n\twatchers := mgr.watchers\n\tmgr.watchMu.Unlock()\n\tfor _, fn := range watchers {\n\t\tfn(i, ev)\n\t}\n}\n\n// resolve resolves the current information about the app located at appRoot.\n// If the app does not exist (either because appRoot does not exist,\n// or because encore.app does not exist within it), it reports an error\n// matching fs.ErrNotExist.\nfunc (mgr *Manager) resolve(appRoot string) (*Instance, error) {\n\tmgr.instanceMu.Lock()\n\tdefer mgr.instanceMu.Unlock()\n\n\tif existing, ok := mgr.instances[appRoot]; ok {\n\t\treturn existing, nil\n\t}\n\n\tplatformID, err := readPlatformID(appRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse the manifest file\n\tman, err := manifest.ReadOrCreate(appRoot)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"parse manifest\")\n\t}\n\n\ti := NewInstance(appRoot, man.LocalID, platformID)\n\ti.tutorial = man.Tutorial\n\ti.mgr = mgr\n\tif err := i.beginWatch(); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\tlog.Error().Err(err).Str(\"id\", i.PlatformOrLocalID()).Msg(\"unable to begin watching app\")\n\t}\n\tmgr.instances[appRoot] = i\n\n\t// Notify any listeners about the new app\n\tfor _, fn := range mgr.appListeners {\n\t\tfn(i)\n\t}\n\n\treturn i, nil\n}\n\nfunc (mgr *Manager) Close() error {\n\tmgr.instanceMu.Lock()\n\tdefer mgr.instanceMu.Unlock()\n\n\tfor _, inst := range mgr.instances {\n\t\tif err := inst.Close(); err != nil {\n\t\t\tlog.Err(err).Str(\"id\", inst.PlatformOrLocalID()).Msg(\"unable to close app instance\")\n\t\t\t// do not return an error here as we want to close all instances\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Instance describes an app instance known by the Encore daemon.\ntype Instance struct {\n\troot       string\n\tlocalID    string\n\tplatformID *goldfish.Cache[string]\n\ttutorial   string\n\n\t// mgr is a reference to the manager that created it.\n\t// It may be nil if an instance was created without a manager.\n\tmgr     *Manager\n\twatcher *watcher.Watcher\n\n\tsetupWatch  syncutil.Once\n\twatchMu     sync.Mutex\n\tnextWatchID WatchSubscriptionID\n\twatchers    map[WatchSubscriptionID]*watchSubscription\n\n\tmdMu     sync.Mutex\n\tcachedMd *meta.Data\n}\n\nfunc NewInstance(root, localID, platformID string) *Instance {\n\ti := &Instance{\n\t\troot:     root,\n\t\tlocalID:  localID,\n\t\twatchers: make(map[WatchSubscriptionID]*watchSubscription),\n\t}\n\ti.platformID = goldfish.New[string](1*time.Second, i.fetchPlatformID)\n\tif platformID != \"\" {\n\t\ti.platformID.Set(platformID)\n\t}\n\treturn i\n}\n\nfunc (i *Instance) Tutorial() string {\n\treturn i.tutorial\n}\n\n// Root returns the filesystem path for the app root.\n// It always returns a non-empty string.\nfunc (i *Instance) Root() string { return i.root }\n\n// LocalID reports a local, random id unique for this app,\n// as persisted in the .encore/manifest.json file.\n// It always returns a non-empty string.\nfunc (i *Instance) LocalID() string { return i.localID }\n\n// PlatformID reports the Encore Platform's ID for this app.\n// If the app is not linked it reports the empty string.\nfunc (i *Instance) PlatformID() string {\n\tval, _ := i.platformID.Get()\n\treturn val\n}\n\n// PlatformOrLocalID reports PlatformID() if set and otherwise LocalID().\nfunc (i *Instance) PlatformOrLocalID() string {\n\tif id := i.PlatformID(); id != \"\" {\n\t\treturn id\n\t}\n\treturn i.localID\n}\n\n// Name returns the platform ID for the app, or if there isn't one\n// it returns the folder name the app is in.\nfunc (i *Instance) Name() string {\n\tif id := i.PlatformID(); id != \"\" {\n\t\treturn id\n\t}\n\n\treturn filepath.Base(i.root)\n}\n\nfunc (i *Instance) fetchPlatformID() (string, error) {\n\treturn readPlatformID(i.root)\n}\n\nfunc readPlatformID(appRoot string) (string, error) {\n\t// Parse the encore.app file\n\tpath := filepath.Join(appRoot, appfile.Name)\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tencore, err := appfile.Parse(data)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"parse encore.app\")\n\t}\n\treturn encore.ID, nil\n}\n\n// Experiments returns the enabled experiments for this app.\n//\n// Note: we read the app file here instead of a cached value so we\n// can detect changes between runs of the compiler if we're in\n// watch mode.\nfunc (i *Instance) Experiments(environ []string) (*experiments.Set, error) {\n\texp, err := appfile.Experiments(i.root)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn experiments.FromAppFileAndEnviron(exp, environ)\n}\n\nfunc (i *Instance) Lang() appfile.Lang {\n\tappFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name))\n\tif err != nil {\n\t\treturn appfile.LangGo\n\t}\n\treturn appFile.Lang\n}\n\nfunc (i *Instance) Hooks() (*appfile.Hooks, error) {\n\tappFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &appFile.Build.Hooks, nil\n}\n\nfunc (i *Instance) AppFile() (*appfile.File, error) {\n\treturn appfile.ParseFile(filepath.Join(i.root, appfile.Name))\n}\n\nfunc (i *Instance) BuildSettings() (appfile.Build, error) {\n\tappFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name))\n\tif err != nil {\n\t\treturn appfile.Build{}, err\n\t}\n\treturn appFile.Build, nil\n}\n\n// GlobalCORS returns the CORS configuration for the app which\n// will be applied against all API gateways into the app\nfunc (i *Instance) GlobalCORS() (appfile.CORS, error) {\n\tcors, err := appfile.GlobalCORS(i.root)\n\tif err != nil {\n\t\treturn appfile.CORS{}, err\n\t}\n\n\t// If there are no Global CORS return the default\n\tif cors == nil {\n\t\treturn appfile.CORS{}, nil\n\t}\n\n\treturn *cors, nil\n\n}\n\nfunc (i *Instance) Watch(fn WatchFunc) (WatchSubscriptionID, error) {\n\tif err := i.beginWatch(); err != nil {\n\t\treturn 0, err\n\t}\n\n\ti.watchMu.Lock()\n\ti.nextWatchID++\n\tid := i.nextWatchID\n\ti.watchers[id] = &watchSubscription{id, fn}\n\ti.watchMu.Unlock()\n\treturn id, nil\n}\n\nfunc (i *Instance) Unwatch(id WatchSubscriptionID) {\n\ti.watchMu.Lock()\n\tdelete(i.watchers, id)\n\ti.watchMu.Unlock()\n}\n\nfunc (i *Instance) beginWatch() error {\n\treturn i.setupWatch.Do(func() error {\n\t\twatch, err := watcher.New(i.PlatformOrLocalID())\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"unable to create watcher\")\n\t\t}\n\t\ti.watcher = watch\n\n\t\tif err := i.watcher.RecursivelyWatch(i.root); err != nil {\n\t\t\treturn errors.Wrap(err, \"unable to watch app\")\n\t\t}\n\n\t\t// If we're in dev mode, we want to watch the runtime\n\t\t// too, so we can develop changes to the runtime without\n\t\t// needing to restart the application.\n\t\tif conf.DevDaemon {\n\t\t\tif err := i.watcher.RecursivelyWatch(env.EncoreRuntimesPath()); err != nil {\n\t\t\t\treturn errors.Wrap(err, \"unable to watch runtime\")\n\t\t\t}\n\t\t}\n\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\tevents, ok := i.watcher.WaitForEvents()\n\t\t\t\tif !ok {\n\t\t\t\t\t// We're done watching.\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif i.mgr != nil {\n\t\t\t\t\ti.mgr.onWatchEvent(i, events)\n\t\t\t\t}\n\n\t\t\t\ti.watchMu.Lock()\n\t\t\t\twatchers := i.watchers\n\t\t\t\ti.watchMu.Unlock()\n\t\t\t\tfor _, sub := range watchers {\n\t\t\t\t\tsub.f(i, events)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\treturn nil\n\t})\n}\n\n// CachePath returns the path to the cache directory for this app.\n// It creates the directory if it does not exist.\nfunc (i *Instance) CachePath() (string, error) {\n\tcacheDir, err := conf.CacheDir()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"unable to get encore cache dir\")\n\t}\n\n\t// we use local ID to be stable if the app is linked to the platform later\n\tcacheDir = filepath.Join(cacheDir, i.localID)\n\tif err := os.MkdirAll(cacheDir, 0755); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"unable to create app cache dir\")\n\t}\n\n\treturn cacheDir, nil\n}\n\n// CacheMetadata caches the metadata for this app onto the file system\nfunc (i *Instance) CacheMetadata(md *meta.Data) error {\n\ti.mdMu.Lock()\n\tdefer i.mdMu.Unlock()\n\n\ti.cachedMd = md\n\n\tcacheDir, err := i.CachePath()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata, err := proto.Marshal(md)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to marshal metadata\")\n\t}\n\n\terr = xos.WriteFile(filepath.Join(cacheDir, \"metadata.pb\"), data, 0644)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to write metadata\")\n\t}\n\n\treturn nil\n}\n\n// CachedMetadata returns the cached metadata for this app, if any\nfunc (i *Instance) CachedMetadata() (*meta.Data, error) {\n\ti.mdMu.Lock()\n\tdefer i.mdMu.Unlock()\n\n\tif i.cachedMd != nil {\n\t\treturn i.cachedMd, nil\n\t}\n\n\tcacheDir, err := i.CachePath()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata, err := os.ReadFile(filepath.Join(cacheDir, \"metadata.pb\"))\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, errors.Wrap(err, \"unable to read metadata\")\n\t}\n\n\tmd := &meta.Data{}\n\terr = proto.Unmarshal(data, md)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to unmarshal metadata\")\n\t}\n\n\ti.cachedMd = md\n\treturn md, nil\n}\n\nfunc (i *Instance) Close() error {\n\tif i.watcher != nil {\n\t\treturn i.watcher.Close()\n\t}\n\treturn nil\n}\n\ntype WatchSubscriptionID int64\n\ntype watchSubscription struct {\n\tid WatchSubscriptionID\n\tf  WatchFunc\n}\n"
  },
  {
    "path": "cli/daemon/check.go",
    "content": "package daemon\n\nimport (\n\t\"encr.dev/cli/daemon/run\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Check checks the app for compilation errors.\nfunc (s *Server) Check(req *daemonpb.CheckRequest, stream daemonpb.Daemon_CheckServer) error {\n\tslog := &streamLog{stream: stream, buffered: false}\n\tlog := newStreamLogger(slog)\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to resolve app\")\n\t\tstreamExit(stream, 1)\n\t\treturn nil\n\t}\n\n\tbuildDir, err := s.mgr.Check(stream.Context(), run.CheckParams{\n\t\tApp:          app,\n\t\tWorkingDir:   req.WorkingDir,\n\t\tCodegenDebug: req.CodegenDebug,\n\t\tEnviron:      req.Environ,\n\t\tTests:        req.ParseTests,\n\t})\n\n\texitCode := 0\n\tif err != nil {\n\t\texitCode = 1\n\t\tlog.Error().Msg(err.Error())\n\t}\n\n\tif req.CodegenDebug && buildDir != \"\" {\n\t\tlog.Info().Msgf(\"wrote generated code to: %s\", buildDir)\n\t}\n\tstreamExit(stream, exitCode)\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/common.go",
    "content": "package daemon\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/internal/onboarding\"\n\t\"encr.dev/pkg/errlist\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// OnStart implements run.EventListener.\nfunc (s *Server) OnStart(r *run.Run) {}\n\nfunc (s *Server) OnCompileStart(r *run.Run) {}\n\n// OnReload implements run.EventListener.\nfunc (s *Server) OnReload(r *run.Run) {}\n\n// OnStop implements run.EventListener.\nfunc (s *Server) OnStop(r *run.Run) {}\n\n// OnStdout implements run.EventListener.\nfunc (s *Server) OnStdout(r *run.Run, line []byte) {\n\ts.mu.Lock()\n\tslog, ok := s.streams[r.ID]\n\ts.mu.Unlock()\n\n\tif ok {\n\t\t_, _ = slog.Stdout(true).Write(line)\n\t}\n}\n\n// OnStderr implements run.EventListener.\nfunc (s *Server) OnStderr(r *run.Run, line []byte) {\n\ts.mu.Lock()\n\tslog, ok := s.streams[r.ID]\n\ts.mu.Unlock()\n\n\tif ok {\n\t\t_, _ = slog.Stderr(true).Write(line)\n\t}\n}\n\nfunc (s *Server) OnError(r *run.Run, err *errlist.List) {\n\ts.mu.Lock()\n\tslog, ok := s.streams[r.ID]\n\ts.mu.Unlock()\n\n\tif ok {\n\t\tslog.Error(err)\n\t}\n}\n\nfunc showFirstRunExperience(run *run.Run, md *meta.Data, stdout io.Writer) {\n\tif state, err := onboarding.Load(); err == nil {\n\t\tif !state.FirstRun.IsSet() {\n\t\t\t// Is there a suitable endpoint to call?\n\t\t\tvar rpc *meta.RPC\n\t\t\tvar command string\n\t\t\tfor _, svc := range md.Svcs {\n\t\t\t\tfor _, r := range svc.Rpcs {\n\t\t\t\t\tif cmd := genCurlCommand(run, md, r); rpc == nil || len(command) < len(cmd) {\n\t\t\t\t\t\trpc = r\n\t\t\t\t\t\tcommand = cmd\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif rpc != nil {\n\t\t\t\tstate.FirstRun.Set()\n\t\t\t\tif err := state.Write(); err == nil {\n\t\t\t\t\t_, _ = stdout.Write([]byte(aurora.Sprintf(\"\\nHint: make an API call by running: %s\\n\", aurora.Cyan(command))))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// findAvailableAddr attempts to find an available host:port that's near\n// the given startAddr.\nfunc findAvailableAddr(startAddr string) (host string, port int, ok bool) {\n\thost, portStr, err := net.SplitHostPort(startAddr)\n\tif err != nil {\n\t\thost = \"localhost\"\n\t\tportStr = \"4000\"\n\t}\n\tstartPort, err := strconv.Atoi(portStr)\n\tif err != nil {\n\t\tstartPort = 4000\n\t}\n\n\tfor p := startPort + 1; p <= startPort+10 && p <= 65535; p++ {\n\t\taddr := host + \":\" + strconv.Itoa(p)\n\t\tln, err := net.Listen(\"tcp\", addr)\n\t\tif err == nil {\n\t\t\t_ = ln.Close()\n\t\t\treturn host, p, true\n\t\t}\n\t}\n\treturn \"\", 0, false\n}\n\nfunc genCurlCommand(run *run.Run, md *meta.Data, rpc *meta.RPC) string {\n\tvar payload []byte\n\tmethod := rpc.HttpMethods[0]\n\tswitch method {\n\tcase \"GET\", \"HEAD\", \"DELETE\":\n\t\t// doesn't use HTTP body payloads\n\tdefault:\n\t\tpayload = genSchema(md, rpc.RequestSchema)\n\t}\n\n\tvar segments []string\n\tfor _, seg := range rpc.Path.Segments {\n\t\tvar v string\n\t\tswitch seg.Type {\n\t\tdefault:\n\t\t\tv = \"foo\"\n\t\tcase meta.PathSegment_LITERAL:\n\t\t\tv = seg.Value\n\t\tcase meta.PathSegment_WILDCARD, meta.PathSegment_FALLBACK:\n\t\t\tv = \"foo\"\n\t\tcase meta.PathSegment_PARAM:\n\t\t\tswitch seg.ValueType {\n\t\t\tcase meta.PathSegment_STRING:\n\t\t\t\tv = \"foo\"\n\t\t\tcase meta.PathSegment_BOOL:\n\t\t\t\tv = \"true\"\n\t\t\tcase meta.PathSegment_INT8, meta.PathSegment_INT16, meta.PathSegment_INT32, meta.PathSegment_INT64,\n\t\t\t\tmeta.PathSegment_UINT8, meta.PathSegment_UINT16, meta.PathSegment_UINT32, meta.PathSegment_UINT64:\n\t\t\t\tv = \"1\"\n\t\t\tcase meta.PathSegment_UUID:\n\t\t\t\tv = \"be23a21f-d12c-432c-91ec-fb8a52e23967\" // some random UUID\n\t\t\tdefault:\n\t\t\t\tv = \"foo\"\n\t\t\t}\n\t\t}\n\t\tsegments = append(segments, v)\n\t}\n\n\tparts := []string{\"curl\"}\n\tif (payload != nil && method != \"POST\") || (payload == nil && method != \"GET\") {\n\t\tparts = append(parts, \" -X \", method)\n\t}\n\t// nosemgrep\n\tpath := \"/\" + strings.Join(segments, \"/\")\n\tparts = append(parts, \" http://\", run.ListenAddr, path)\n\tif payload != nil {\n\t\tparts = append(parts, \" -d '\", string(payload), \"'\")\n\t}\n\treturn strings.Join(parts, \"\")\n}\n\n// errIsAddrInUse reports whether the error is due to the address already being in use.\nfunc errIsAddrInUse(err error) bool {\n\tif opErr, ok := err.(*net.OpError); ok {\n\t\tif syscallErr, ok := opErr.Err.(*os.SyscallError); ok {\n\t\t\tif errno, ok := syscallErr.Err.(syscall.Errno); ok {\n\t\t\t\tconst WSAEADDRINUSE = 10048\n\t\t\t\tswitch {\n\t\t\t\tcase errno == syscall.EADDRINUSE:\n\t\t\t\t\treturn true\n\t\t\t\tcase runtime.GOOS == \"windows\" && errno == WSAEADDRINUSE:\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "cli/daemon/create.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// CreateApp adds tracking for a new app\nfunc (s *Server) CreateApp(ctx context.Context, req *daemonpb.CreateAppRequest) (*daemonpb.CreateAppResponse, error) {\n\tvar options []apps.TrackOption\n\tif req.Tutorial {\n\t\toptions = append(options, apps.WithTutorial(req.Template))\n\t}\n\tapp, err := s.apps.Track(req.AppRoot, options...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &daemonpb.CreateAppResponse{AppId: app.PlatformOrLocalID()}, nil\n}\n"
  },
  {
    "path": "cli/daemon/daemon.go",
    "content": "// Package daemon implements the Encore daemon gRPC server.\npackage daemon\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/golang/protobuf/ptypes/empty\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/genproto/googleapis/rpc/errdetails\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/mcp\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/daemon/secret\"\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/cli/internal/update\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/clientgen\"\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/errlist\"\n\t\"encr.dev/pkg/fns\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nvar _ daemonpb.DaemonServer = (*Server)(nil)\n\n// Server implements daemonpb.DaemonServer.\ntype Server struct {\n\tapps *apps.Manager\n\tmgr  *run.Manager\n\tcm   *sqldb.ClusterManager\n\tsm   *secret.Manager\n\tns   *namespace.Manager\n\tmcp  *mcp.Manager\n\n\tmu      sync.Mutex\n\tstreams map[string]*streamLog // run id -> stream\n\n\tavailableVerInit sync.Once\n\tavailableVer     atomic.Value // string\n\n\tappDebounceMu sync.Mutex\n\tappDebouncers map[*apps.Instance]*regenerateCodeDebouncer\n\n\tdaemonpb.UnimplementedDaemonServer\n}\n\n// New creates a new Server.\nfunc New(appsMgr *apps.Manager, mgr *run.Manager, cm *sqldb.ClusterManager, sm *secret.Manager, ns *namespace.Manager, mcp *mcp.Manager) *Server {\n\tsrv := &Server{\n\t\tapps:    appsMgr,\n\t\tmgr:     mgr,\n\t\tcm:      cm,\n\t\tsm:      sm,\n\t\tns:      ns,\n\t\tmcp:     mcp,\n\t\tstreams: make(map[string]*streamLog),\n\n\t\tappDebouncers: make(map[*apps.Instance]*regenerateCodeDebouncer),\n\t}\n\n\tmgr.AddListener(srv)\n\n\t// Check immediately for the latest version to avoid blocking 'encore run'\n\tgo srv.availableUpdate()\n\n\t// Begin watching known apps for changes\n\tgo srv.watchApps()\n\n\treturn srv\n}\n\n// GenClient generates a client based on the app's API.\nfunc (s *Server) GenClient(ctx context.Context, params *daemonpb.GenClientRequest) (*daemonpb.GenClientResponse, error) {\n\tvar md *meta.Data\n\n\tenvName := params.EnvName\n\tif envName == \"\" {\n\t\tenvName = \"local\"\n\t}\n\n\tif envName == \"local\" {\n\t\tvar app *apps.Instance\n\t\tvar err error\n\t\t// If the command was called with an app id, find the app instance by id.\n\t\tif params.AppRoot == \"\" {\n\t\t\tapp, err = s.apps.FindLatestByPlatformOrLocalID(params.AppId)\n\t\t\tif errors.Is(err, apps.ErrNotFound) {\n\t\t\t\treturn nil, status.Errorf(codes.FailedPrecondition, \"the app %s must be run locally before generating a client for the 'local' environment.\",\n\t\t\t\t\tparams.AppId)\n\t\t\t} else if err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"unable to query app info: %v\", err)\n\t\t\t}\n\t\t} else { // Otherwise, track the app by its root directory.\n\t\t\tapp, err = s.apps.Track(params.AppRoot)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, status.Errorf(codes.Internal, \"unable to query app info: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Get the app metadata\n\t\texpSet, err := app.Experiments(nil)\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"failed to parse app experiments: %v\", err)\n\t\t}\n\n\t\t// Parse the app to figure out what infrastructure is needed.\n\t\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\t\tdefer fns.CloseIgnore(bld)\n\t\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\t\tBuild:      builder.DefaultBuildInfo(),\n\t\t\tApp:        app,\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"failed to prepare app: %v\", err)\n\t\t}\n\t\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\t\tBuild:       builder.DefaultBuildInfo(),\n\t\t\tApp:         app,\n\t\t\tExperiments: expSet,\n\t\t\tWorkingDir:  \".\",\n\t\t\tParseTests:  false,\n\t\t\tPrepare:     prepareResult,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, status.Errorf(codes.InvalidArgument, \"failed to parse app metadata: %v\", err)\n\t\t}\n\t\tmd = parse.Meta\n\n\t\tif err := app.CacheMetadata(md); err != nil {\n\t\t\treturn nil, status.Errorf(codes.Internal, \"failed to cache app metadata: %v\", err)\n\t\t}\n\t} else {\n\t\tmeta, err := platform.GetEnvMeta(ctx, params.AppId, envName)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"env_not_found\") || strings.Contains(err.Error(), \"env_not_deployed\") {\n\t\t\t\tif envName == \"@primary\" {\n\t\t\t\t\treturn nil, status.Error(codes.NotFound, \"You have no deployments of this application.\\n\\nYou can generate the client for your local code by setting `--env=local`.\")\n\t\t\t\t}\n\t\t\t\treturn nil, status.Errorf(codes.NotFound, \"A deployed environment called `%s` not found.\\n\\nYou can generate the client for your local code by setting `--env=local`.\", envName)\n\t\t\t}\n\t\t\treturn nil, status.Errorf(codes.Unavailable, \"could not fetch API metadata: %v\", err)\n\t\t}\n\t\tmd = meta\n\t}\n\n\tlang := clientgen.Lang(params.Lang)\n\n\tservicesToGenerate := clientgentypes.NewServiceSet(md, params.Services, params.ExcludedServices)\n\ttagSet := clientgentypes.NewTagSet(params.EndpointTags, params.ExcludedEndpointTags)\n\topts := clientgentypes.Options{}\n\tif params.OpenapiExcludePrivateEndpoints != nil {\n\t\topts.OpenAPIExcludePrivateEndpoints = *params.OpenapiExcludePrivateEndpoints\n\t}\n\tif params.TsSharedTypes != nil {\n\t\topts.TSSharedTypes = *params.TsSharedTypes\n\t}\n\tif params.TsClientTarget != nil {\n\t\topts.TSClientTarget = *params.TsClientTarget\n\t}\n\tcode, err := clientgen.Client(lang, params.AppId, md, servicesToGenerate, tagSet, opts)\n\tif err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t}\n\treturn &daemonpb.GenClientResponse{Code: code}, nil\n}\n\nfunc (s *Server) SecretsRefresh(ctx context.Context, req *daemonpb.SecretsRefreshRequest) (*daemonpb.SecretsRefreshResponse, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts.sm.UpdateKey(app.PlatformID(), req.Key, req.Value)\n\treturn &daemonpb.SecretsRefreshResponse{}, nil\n}\n\n// Version reports the daemon version.\nfunc (s *Server) Version(context.Context, *empty.Empty) (*daemonpb.VersionResponse, error) {\n\tconfigHash, err := version.ConfigHash()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &daemonpb.VersionResponse{\n\t\tVersion:    version.Version,\n\t\tConfigHash: configHash,\n\t}, nil\n}\n\n// availableUpdate checks for updates to Encore.\n// If there is a new version it returns it as a semver string.\nfunc (s *Server) availableUpdate() *update.LatestVersion {\n\tcheck := func() *update.LatestVersion {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t\tver, err := update.Check(ctx)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"could not check for new encore release\")\n\t\t}\n\t\treturn ver\n\t}\n\n\ts.availableVerInit.Do(func() {\n\t\tver := check()\n\t\ts.availableVer.Store(ver)\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\ttime.Sleep(1 * time.Hour)\n\t\t\t\tif ver := check(); ver != nil {\n\t\t\t\t\ts.availableVer.Store(ver)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t})\n\n\tcurr := version.Version\n\tlatest := s.availableVer.Load().(*update.LatestVersion)\n\tif latest.IsNewer(curr) {\n\t\treturn latest\n\t}\n\treturn nil\n}\n\nvar errDatabaseNotFound = (func() error {\n\tst := status.New(codes.NotFound, \"database not found\")\n\treturn st.Err()\n})()\n\nvar errNotLinked = (func() error {\n\tst, err := status.New(codes.FailedPrecondition, \"app not linked\").WithDetails(\n\t\t&errdetails.PreconditionFailure{\n\t\t\tViolations: []*errdetails.PreconditionFailure_Violation{\n\t\t\t\t{\n\t\t\t\t\tType:        \"NOT_LINKED\",\n\t\t\t\t\tDescription: \"app is not linked with Encore Cloud\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn st.Err()\n})()\n\ntype commandStream interface {\n\tSend(msg *daemonpb.CommandMessage) error\n}\n\nfunc newStreamLogger(slog *streamLog) zerolog.Logger {\n\treturn zerolog.New(zerolog.SyncWriter(slog.Stderr(false))).With().Timestamp().Logger()\n}\n\ntype streamWriter struct {\n\tmu     *sync.Mutex\n\tsl     *streamLog\n\tstderr bool // if true write to stderr, otherwise stdout\n\tbuffer bool\n}\n\nfunc (w streamWriter) Write(b []byte) (int, error) {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\tif w.buffer && w.sl.buffered {\n\t\tif w.stderr {\n\t\t\treturn w.sl.writeBuffered(&w.sl.stderr, b)\n\t\t} else {\n\t\t\treturn w.sl.writeBuffered(&w.sl.stdout, b)\n\t\t}\n\t}\n\treturn w.sl.writeStream(w.stderr, b)\n}\n\nfunc streamExit(stream commandStream, code int) {\n\t_ = stream.Send(&daemonpb.CommandMessage{Msg: &daemonpb.CommandMessage_Exit{\n\t\tExit: &daemonpb.CommandExit{\n\t\t\tCode: int32(code),\n\t\t},\n\t}})\n}\n\ntype streamLog struct {\n\tstream commandStream\n\tmu     sync.Mutex\n\n\tbuffered bool\n\tstdout   *bytes.Buffer // lazily allocated\n\tstderr   *bytes.Buffer // lazily allocated\n}\n\nfunc (log *streamLog) Stdout(buffer bool) io.Writer {\n\treturn streamWriter{mu: &log.mu, sl: log, stderr: false, buffer: buffer}\n}\n\nfunc (log *streamLog) Stderr(buffer bool) io.Writer {\n\treturn streamWriter{mu: &log.mu, sl: log, stderr: true, buffer: buffer}\n}\n\nfunc (log *streamLog) Error(err *errlist.List) {\n\tlog.mu.Lock()\n\tdefer log.mu.Unlock()\n\t_ = err.SendToStream(log.stream)\n}\n\nfunc (log *streamLog) FlushBuffers() {\n\tvar stdout, stderr []byte\n\tlog.mu.Lock()\n\tdefer log.mu.Unlock()\n\tif b := log.stdout; b != nil {\n\t\tstdout = b.Bytes()\n\t\tlog.stdout = nil\n\t}\n\tif b := log.stderr; b != nil {\n\t\tstderr = b.Bytes()\n\t\tlog.stderr = nil\n\t}\n\n\t_, _ = log.writeStream(false, stderr)\n\t_, _ = log.writeStream(true, stdout)\n\tlog.buffered = false\n}\n\nfunc (log *streamLog) writeBuffered(b **bytes.Buffer, p []byte) (int, error) {\n\tif *b == nil {\n\t\t*b = &bytes.Buffer{}\n\t}\n\treturn (*b).Write(p)\n}\n\nfunc (log *streamLog) writeStream(stderr bool, b []byte) (int, error) {\n\tout := &daemonpb.CommandOutput{}\n\tif stderr {\n\t\tout.Stderr = b\n\t} else {\n\t\tout.Stdout = b\n\t}\n\terr := log.stream.Send(&daemonpb.CommandMessage{\n\t\tMsg: &daemonpb.CommandMessage_Output{\n\t\t\tOutput: out,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(b), nil\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/assembler.go",
    "content": "package ai\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n)\n\n// partialEndpoint is a helper struct that is used to assemble the endpoint\n// from the incoming websocket updates.\ntype partialEndpoint struct {\n\tservice  string\n\tendpoint *Endpoint\n}\n\n// notification generates a partially assembled endpoint structure to return to the client\nfunc (e *partialEndpoint) notification() LocalEndpointUpdate {\n\te.endpoint.EndpointSource = e.endpoint.Render()\n\te.endpoint.TypeSource = \"\"\n\tfor i, s := range e.endpoint.Types {\n\t\tif i > 0 {\n\t\t\te.endpoint.TypeSource += \"\\n\\n\"\n\t\t}\n\t\te.endpoint.TypeSource += s.Render()\n\t}\n\treturn LocalEndpointUpdate{\n\t\tService:  e.service,\n\t\tEndpoint: e.endpoint,\n\t\tType:     \"EndpointUpdate\",\n\t}\n}\n\nfunc (e *partialEndpoint) upsertType(name, doc string) *Type {\n\tif name == \"\" {\n\t\treturn nil\n\t}\n\tfor _, s := range e.endpoint.Types {\n\t\tif s.Name == name {\n\t\t\tif doc != \"\" {\n\t\t\t\ts.Doc = wrapDoc(doc, 77)\n\t\t\t}\n\t\t\treturn s\n\t\t}\n\t}\n\tsi := &Type{Name: name, Doc: wrapDoc(doc, 77)}\n\te.endpoint.Types = append(e.endpoint.Types, si)\n\treturn si\n}\n\nfunc wrapDoc(doc string, width int) string {\n\tdoc = strings.ReplaceAll(doc, \"\\n\", \" \")\n\tdoc = strings.TrimSpace(doc)\n\tbytes := []byte(doc)\n\ti := 0\n\tfor {\n\t\tstart := i\n\t\tif start+width >= len(bytes) {\n\t\t\tbreak\n\t\t}\n\t\ti += width\n\t\tfor i > start && bytes[i] != ' ' {\n\t\t\ti--\n\t\t}\n\t\tif i > start {\n\t\t\tbytes[i] = '\\n'\n\t\t} else {\n\t\t\tfor i < len(bytes) && bytes[i] != ' ' {\n\t\t\t\ti++\n\t\t\t}\n\t\t\tif i < len(bytes) {\n\t\t\t\tbytes[i] = '\\n'\n\t\t\t}\n\t\t}\n\t}\n\treturn string(bytes)\n}\n\nfunc (e *partialEndpoint) upsertError(err ErrorUpdate) *Error {\n\tfor _, s := range e.endpoint.Errors {\n\t\tif s.Code == err.Code {\n\t\t\tif err.Doc != \"\" {\n\t\t\t\ts.Doc = wrapDoc(err.Doc, 60)\n\t\t\t}\n\t\t\treturn s\n\t\t}\n\t}\n\tsi := &Error{Code: err.Code, Doc: wrapDoc(err.Doc, 60)}\n\te.endpoint.Errors = append(e.endpoint.Errors, si)\n\treturn si\n}\n\nfunc (e *partialEndpoint) upsertPathParam(up PathParamUpdate) PathSegment {\n\tfor i, s := range e.endpoint.Path {\n\t\tif s.Value != nil && *s.Value == up.Param {\n\t\t\tif up.Doc != \"\" {\n\t\t\t\te.endpoint.Path[i].Doc = wrapDoc(up.Doc, 73)\n\t\t\t}\n\t\t\treturn s\n\t\t}\n\t}\n\tseg := PathSegment{\n\t\tType:      SegmentTypeParam,\n\t\tValueType: ptr[SegmentValueType](\"string\"),\n\t\tValue:     &up.Param,\n\t\tDoc:       wrapDoc(up.Doc, 73),\n\t}\n\te.endpoint.Path = append(e.endpoint.Path, seg)\n\treturn seg\n}\n\nfunc (e *partialEndpoint) upsertField(up TypeFieldUpdate) *Type {\n\tif up.Struct == \"\" {\n\t\treturn nil\n\t}\n\ts := e.upsertType(up.Struct, \"\")\n\tfor _, f := range s.Fields {\n\t\tif f.Name == up.Name {\n\t\t\tif up.Doc != \"\" {\n\t\t\t\tf.Doc = wrapDoc(up.Doc, 73)\n\t\t\t}\n\t\t\tif up.Type != \"\" {\n\t\t\t\tf.Type = up.Type\n\t\t\t}\n\t\t\treturn s\n\t\t}\n\t}\n\tdefaultLoc := apienc.Body\n\tisRequest := up.Struct == e.endpoint.RequestType\n\tif slices.Contains([]string{\"GET\", \"HEAD\", \"DELETE\"}, e.endpoint.Method) && isRequest {\n\t\tdefaultLoc = apienc.Query\n\t}\n\tfi := &TypeField{\n\t\tName:     up.Name,\n\t\tDoc:      wrapDoc(up.Doc, 73),\n\t\tType:     up.Type,\n\t\tLocation: defaultLoc,\n\t\tWireName: idents.Convert(up.Name, idents.CamelCase),\n\t}\n\ts.Fields = append(s.Fields, fi)\n\treturn s\n}\n\n// The endpointsAssembler is a helper struct that is used to assemble the endpoint\n// from the incoming websocket updates. It keeps track of the existing endpoints and services\n// and updates them accordingly.\ntype endpointsAssembler struct {\n\teps map[string]*partialEndpoint\n}\n\nfunc newEndpointAssembler(existing []Service) *endpointsAssembler {\n\teas := &endpointsAssembler{\n\t\teps: make(map[string]*partialEndpoint),\n\t}\n\tfor _, svc := range existing {\n\t\tfor _, ep := range svc.Endpoints {\n\t\t\tkey := svc.Name + \".\" + ep.Name\n\t\t\teas.eps[key] = &partialEndpoint{\n\t\t\t\tservice:  svc.Name,\n\t\t\t\tendpoint: ep,\n\t\t\t}\n\t\t}\n\t}\n\treturn eas\n}\n\nfunc (s *endpointsAssembler) upsertEndpoint(e EndpointUpdate) *partialEndpoint {\n\tfor _, ep := range s.eps {\n\t\tif ep.service != e.Service || ep.endpoint.Name != e.Name {\n\t\t\tcontinue\n\t\t}\n\t\tif e.Doc != \"\" {\n\t\t\tep.endpoint.Doc = wrapDoc(e.Doc, 77)\n\t\t}\n\t\tif e.Method != \"\" {\n\t\t\tep.endpoint.Method = e.Method\n\t\t}\n\t\tif e.Visibility != \"\" {\n\t\t\tep.endpoint.Visibility = e.Visibility\n\t\t}\n\t\tif len(e.Path) > 0 {\n\t\t\tep.endpoint.Path = e.Path\n\t\t}\n\t\tif e.RequestType != \"\" {\n\t\t\tep.endpoint.RequestType = e.RequestType\n\t\t\tep.upsertType(e.RequestType, \"\")\n\t\t}\n\t\tif e.ResponseType != \"\" {\n\t\t\tep.endpoint.ResponseType = e.ResponseType\n\t\t\tep.upsertType(e.ResponseType, \"\")\n\t\t}\n\t\tif e.Errors != nil {\n\t\t\tep.endpoint.Errors = fns.Map(e.Errors, func(e string) *Error {\n\t\t\t\treturn &Error{Code: e}\n\t\t\t})\n\t\t}\n\t\treturn ep\n\t}\n\tep := &partialEndpoint{\n\t\tservice: e.Service,\n\t\tendpoint: &Endpoint{\n\t\t\tName:         e.Name,\n\t\t\tDoc:          wrapDoc(e.Doc, 77),\n\t\t\tMethod:       e.Method,\n\t\t\tVisibility:   e.Visibility,\n\t\t\tPath:         e.Path,\n\t\t\tRequestType:  e.RequestType,\n\t\t\tResponseType: e.ResponseType,\n\t\t\tErrors: fns.Map(e.Errors, func(e string) *Error {\n\t\t\t\treturn &Error{Code: e}\n\t\t\t}),\n\t\t\tLanguage: \"GO\",\n\t\t},\n\t}\n\ts.eps[e.Service+\".\"+e.Name] = ep\n\treturn ep\n}\n\nfunc (s *endpointsAssembler) endpoint(service, endpoint string) *partialEndpoint {\n\tkey := service + \".\" + endpoint\n\tep, ok := s.eps[key]\n\tif !ok {\n\t\tep := &partialEndpoint{\n\t\t\tservice:  service,\n\t\t\tendpoint: &Endpoint{Name: endpoint},\n\t\t}\n\t\ts.eps[key] = ep\n\t}\n\treturn ep\n}\n\nfunc newEndpointAssemblerHandler(existing []Service, notifier AINotifier, epComplete bool) AINotifier {\n\tepCache := newEndpointAssembler(existing)\n\tvar lastEp *partialEndpoint\n\treturn func(ctx context.Context, msg *AINotification) error {\n\t\tvar ep *partialEndpoint\n\t\tmsgVal := msg.Value\n\t\tswitch val := msg.Value.(type) {\n\t\tcase TypeUpdate:\n\t\t\tep = epCache.endpoint(val.Service, val.Endpoint)\n\t\t\tep.upsertType(val.Name, val.Doc)\n\t\t\tmsgVal = ep.notification()\n\t\tcase TypeFieldUpdate:\n\t\t\tep = epCache.endpoint(val.Service, val.Endpoint)\n\t\t\tep.upsertField(val)\n\t\t\tmsgVal = ep.notification()\n\t\tcase EndpointUpdate:\n\t\t\tep = epCache.upsertEndpoint(val)\n\t\t\tmsgVal = ep.notification()\n\t\tcase ErrorUpdate:\n\t\t\tep = epCache.endpoint(val.Service, val.Endpoint)\n\t\t\tep.upsertError(val)\n\t\t\tmsgVal = ep.notification()\n\t\tcase PathParamUpdate:\n\t\t\tep = epCache.endpoint(val.Service, val.Endpoint)\n\t\t\tep.upsertPathParam(val)\n\t\t\tmsgVal = ep.notification()\n\t\t}\n\t\tif epComplete && lastEp != ep {\n\t\t\tif lastEp != nil {\n\t\t\t\tmsg.Value = struct {\n\t\t\t\t\tType     string `json:\"type\"`\n\t\t\t\t\tService  string `json:\"service\"`\n\t\t\t\t\tEndpoint string `json:\"endpoint\"`\n\t\t\t\t}{\"EndpointComplete\", lastEp.service, lastEp.endpoint.Name}\n\t\t\t\tif err := notifier(ctx, msg); err != nil || msg.Finished {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastEp = ep\n\t\t}\n\t\tmsg.Value = msgVal\n\t\treturn notifier(ctx, msg)\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/client.go",
    "content": "package ai\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/hasura/go-graphql-client\"\n\t\"github.com/hasura/go-graphql-client/pkg/jsonutil\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/internal/conf\"\n)\n\ntype TaskMessage struct {\n\tType string `graphql:\"__typename\"`\n\n\tServiceUpdate   `graphql:\"... on ServiceUpdate\"`\n\tTypeUpdate      `graphql:\"... on TypeUpdate\"`\n\tTypeFieldUpdate `graphql:\"... on TypeFieldUpdate\"`\n\tErrorUpdate     `graphql:\"... on ErrorUpdate\"`\n\tEndpointUpdate  `graphql:\"... on EndpointUpdate\"`\n\tSessionUpdate   `graphql:\"... on SessionUpdate\"`\n\tTitleUpdate     `graphql:\"... on TitleUpdate\"`\n\tPathParamUpdate `graphql:\"... on PathParamUpdate\"`\n}\n\nfunc (u *TaskMessage) GetValue() AIUpdateType {\n\tswitch u.Type {\n\tcase \"ServiceUpdate\":\n\t\treturn u.ServiceUpdate\n\tcase \"TypeUpdate\":\n\t\treturn u.TypeUpdate\n\tcase \"TypeFieldUpdate\":\n\t\treturn u.TypeFieldUpdate\n\tcase \"ErrorUpdate\":\n\t\treturn u.ErrorUpdate\n\tcase \"EndpointUpdate\":\n\t\treturn u.EndpointUpdate\n\tcase \"SessionUpdate\":\n\t\treturn u.SessionUpdate\n\tcase \"TitleUpdate\":\n\t\treturn u.TitleUpdate\n\tcase \"PathParamUpdate\":\n\t\treturn u.PathParamUpdate\n\t}\n\treturn nil\n}\n\ntype AIStreamMessage struct {\n\tValue    TaskMessage\n\tError    string\n\tFinished bool\n}\n\ntype aiTask struct {\n\tMessage *AIStreamMessage `graphql:\"result\"`\n}\n\nfunc getClient(errHandler func(err error)) *graphql.SubscriptionClient {\n\tclient := graphql.NewSubscriptionClient(conf.WSBaseURL + \"/graphql\").\n\t\tWithRetryTimeout(5 * time.Second).\n\t\tWithRetryDelay(2 * time.Second).\n\t\tWithRetryStatusCodes(\"500-599\").\n\t\tWithWebSocketOptions(\n\t\t\tgraphql.WebsocketOptions{\n\t\t\t\tHTTPClient: conf.AuthClient,\n\t\t\t}).WithSyncMode(true)\n\tgo func() {\n\t\tlog.Info().Msg(\"starting ai client\")\n\t\terr := client.Run()\n\t\tlog.Info().Msg(\"closed ai client\")\n\t\tif err != nil {\n\t\t\terrHandler(err)\n\t\t}\n\t}()\n\treturn client\n}\n\ntype AITask struct {\n\tSubscriptionID string\n\tclient         *graphql.SubscriptionClient\n}\n\nfunc (t *AITask) Stop() error {\n\treturn t.client.Unsubscribe(t.SubscriptionID)\n}\n\n// startAITask is a helper function to intitiate an AI query to the encore platform. The query\n// should be assembled to stream a 'result' graphql field that is a AIStreamMessage.\nfunc startAITask[Query any](ctx context.Context, params map[string]interface{}, notifier AINotifier) (*AITask, error) {\n\tvar subId string\n\tvar errStrReply = func(error string, code any) error {\n\t\tlog.Error().Msgf(\"ai error: %s (%v)\", error, code)\n\t\t_ = notifier(ctx, &AINotification{\n\t\t\tSubscriptionID: subId,\n\t\t\tError:          &AIError{Message: error, Code: fmt.Sprintf(\"%v\", code)},\n\t\t\tFinished:       true,\n\t\t})\n\t\treturn graphql.ErrSubscriptionStopped\n\t}\n\tvar errReply = func(err error) error {\n\t\tvar graphqlErr graphql.Errors\n\t\tif errors.As(err, &graphqlErr) {\n\t\t\tfor _, e := range graphqlErr {\n\t\t\t\t_ = errStrReply(e.Message, e.Extensions[\"code\"])\n\t\t\t}\n\t\t\treturn graphql.ErrSubscriptionStopped\n\t\t}\n\t\treturn errStrReply(err.Error(), \"\")\n\t}\n\tvar query Query\n\tclient := getClient(func(err error) { _ = errReply(err) })\n\tsubId, err := client.Subscribe(&query, params, func(message []byte, err error) error {\n\t\tif err != nil {\n\t\t\treturn errReply(err)\n\t\t}\n\t\tvar result aiTask\n\t\terr = jsonutil.UnmarshalGraphQL(message, &result)\n\t\tif err != nil {\n\t\t\treturn errReply(err)\n\t\t}\n\t\tif result.Message.Error != \"\" {\n\t\t\treturn errStrReply(result.Message.Error, \"\")\n\t\t}\n\t\terr = notifier(ctx, &AINotification{\n\t\t\tSubscriptionID: subId,\n\t\t\tValue:          result.Message.Value.GetValue(),\n\t\t\tFinished:       result.Message.Finished,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errReply(err)\n\t\t}\n\t\treturn nil\n\t})\n\treturn &AITask{SubscriptionID: subId, client: client}, err\n}\n\n// AINotification is a wrapper around messages and errors from the encore platform ai service\ntype AINotification struct {\n\tSubscriptionID string   `json:\"subscriptionId,omitempty\"`\n\tValue          any      `json:\"value,omitempty\"`\n\tError          *AIError `json:\"error,omitempty\"`\n\tFinished       bool     `json:\"finished,omitempty\"`\n}\n\ntype AIError struct {\n\tMessage string `json:\"message\"`\n\tCode    string `json:\"code\"`\n}\n\ntype AINotifier func(context.Context, *AINotification) error\n"
  },
  {
    "path": "cli/daemon/dash/ai/codegen.go",
    "content": "package ai\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"golang.org/x/exp/maps\"\n\t\"golang.org/x/tools/go/packages\"\n\t\"golang.org/x/tools/imports\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/codegen/rewrite\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n\t\"encr.dev/v2/parser/apis/directive\"\n)\n\nconst defAuthHandler = `package auth\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/beta/auth\"\n)\n\ntype Data struct {\n    Username string\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) {\n    panic(\"not yet implemented\")\n}`\n\nconst (\n\tPathDocPrefix = \"Path Parameters\"\n\tErrDocPrefix  = \"Errors\"\n)\n\nfunc (p PathSegments) Render() (docPath string, goParams []string) {\n\tvar params []string\n\treturn \"/\" + path.Join(fns.Map(p, func(s PathSegment) string {\n\t\tswitch s.Type {\n\t\tcase SegmentTypeLiteral:\n\t\t\treturn *s.Value\n\t\tcase SegmentTypeParam:\n\t\t\tparams = append(params, fmt.Sprintf(\"%s %s\", *s.Value, *s.ValueType))\n\t\t\treturn fmt.Sprintf(\":%s\", *s.Value)\n\t\tcase SegmentTypeWildcard:\n\t\t\tparams = append(params, fmt.Sprintf(\"%s %s\", *s.Value, SegmentValueTypeString))\n\t\t\treturn fmt.Sprintf(\"*%s\", *s.Value)\n\t\tcase SegmentTypeFallback:\n\t\t\tparams = append(params, fmt.Sprintf(\"%s %s\", *s.Value, SegmentValueTypeString))\n\t\t\treturn fmt.Sprintf(\"!%s\", *s.Value)\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unknown path segment type: %s\", s.Type))\n\t\t}\n\t})...), params\n}\n\nfunc (s *Type) Render() string {\n\trtn := strings.Builder{}\n\tif s.Doc != \"\" {\n\t\trtn.WriteString(fmtComment(strings.TrimSpace(s.Doc), 0, 1))\n\t}\n\trtn.WriteString(fmt.Sprintf(\"type %s struct {\\n\", s.Name))\n\tfor i, f := range s.Fields {\n\t\tif i > 0 {\n\t\t\trtn.WriteString(\"\\n\")\n\t\t}\n\t\tif f.Doc != \"\" {\n\t\t\trtn.WriteString(fmtComment(strings.TrimSpace(f.Doc), 2, 1))\n\t\t}\n\t\ttags := \"\"\n\t\tswitch f.Location {\n\t\tcase apienc.Body:\n\t\t\ttags = fmt.Sprintf(\" `json:\\\"%s\\\"`\", f.WireName)\n\t\tcase apienc.Query:\n\t\t\ttags = fmt.Sprintf(\" `query:\\\"%s\\\"`\", f.WireName)\n\t\tcase apienc.Header:\n\t\t\ttags = fmt.Sprintf(\" `header:\\\"%s\\\"`\", f.WireName)\n\t\t}\n\t\trtn.WriteString(fmt.Sprintf(\"  %s %s%s\\n\", f.Name, f.Type, tags))\n\t}\n\trtn.WriteString(\"}\")\n\treturn rtn.String()\n}\n\nfunc (e *Endpoint) Render() string {\n\tbuf := strings.Builder{}\n\tif e.Doc != \"\" {\n\t\tbuf.WriteString(fmtComment(strings.TrimSpace(e.Doc)+\"\\n\", 0, 1))\n\t}\n\tbuf.WriteString(renderDocList(PathDocPrefix, e.Path))\n\tbuf.WriteString(renderDocList(ErrDocPrefix, e.Errors))\n\tpathStr, pathParams := e.Path.Render()\n\tparams := []string{\"ctx context.Context\"}\n\tparams = append(params, pathParams...)\n\tif e.RequestType != \"\" {\n\t\tparams = append(params, \"req *\"+e.RequestType)\n\t}\n\tvar rtnParams []string\n\tif e.ResponseType != \"\" {\n\t\trtnParams = append(rtnParams, \"*\"+e.ResponseType)\n\t}\n\trtnParams = append(rtnParams, \"error\")\n\tbuf.WriteString(fmtComment(\"encore:api %s method=%s path=%s\", 0, 0, e.Visibility, e.Method, pathStr))\n\tparamsStr := strings.Join(params, \", \")\n\trtnParamsStr := strings.Join(rtnParams, \", \")\n\tif len(rtnParams) > 1 {\n\t\trtnParamsStr = fmt.Sprintf(\"(%s)\", rtnParamsStr)\n\t}\n\tbuf.WriteString(fmt.Sprintf(\"func %s(%s) %s\", e.Name, paramsStr, rtnParamsStr))\n\treturn buf.String()\n}\n\nfunc indentItem(header, comment string) string {\n\tbuf := strings.Builder{}\n\tbuf.WriteString(header)\n\tfor i, line := range strings.Split(strings.TrimSpace(comment), \"\\n\") {\n\t\tindent := \"\"\n\t\tif i > 0 {\n\t\t\tindent = strings.Repeat(\" \", len(header))\n\t\t}\n\t\tbuf.WriteString(fmt.Sprintf(\"%s%s\\n\", indent, line))\n\t}\n\treturn buf.String()\n}\n\nfunc renderDocList[T interface{ DocItem() (string, string) }](header string, items []T) string {\n\tmaxLen := 0\n\titems = fns.Filter(items, func(p T) bool {\n\t\tkey, val := p.DocItem()\n\t\tif val == \"\" {\n\t\t\treturn false\n\t\t}\n\t\tmaxLen = max(maxLen, len(key))\n\t\treturn true\n\t})\n\tbuf := strings.Builder{}\n\tfor i, item := range items {\n\t\tif i == 0 {\n\t\t\tbuf.WriteString(header)\n\t\t\tbuf.WriteString(\":\\n\")\n\t\t}\n\t\tkey, value := item.DocItem()\n\t\tspacing := strings.Repeat(\" \", maxLen-len(key))\n\t\titemHeader := fmt.Sprintf(\" - %s: %s\", key, spacing)\n\t\tbuf.WriteString(indentItem(itemHeader, value))\n\t}\n\treturn fmtComment(buf.String(), 0, 1)\n}\n\n// fmtComment prepends '//' to each line of the given comment and indents it with the given number of spaces.\nfunc fmtComment(comment string, before, after int, args ...any) string {\n\tif comment == \"\" {\n\t\treturn \"\"\n\t}\n\tprefix := fmt.Sprintf(\"%s//%s\", strings.Repeat(\" \", before), strings.Repeat(\" \", after))\n\tresult := prefix + strings.ReplaceAll(comment, \"\\n\", \"\\n\"+prefix)\n\treturn fmt.Sprintf(result, args...) + \"\\n\"\n}\n\n// generateSrcFiles generates source files for the given services.\nfunc generateSrcFiles(services []Service, app *apps.Instance) (map[paths.RelSlash]string, error) {\n\tsvcPaths, err := newServicePaths(app)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tneedAuth := fns.Any(fns.FlatMap(services, Service.GetEndpoints), (*Endpoint).Auth)\n\tfiles := map[paths.RelSlash]string{}\n\tif needAuth {\n\t\tmd, err := app.CachedMetadata()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif md.AuthHandler == nil {\n\t\t\trelFile, err := svcPaths.RelFileName(\"auth\", \"handler\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfile := paths.FS(app.Root()).JoinSlash(relFile)\n\t\t\terr = os.MkdirAll(file.Dir().ToIO(), 0755)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfiles[relFile] = string(defAuthHandler)\n\t\t}\n\t}\n\tfor _, s := range services {\n\t\tif svcPaths.IsNew(s.Name) {\n\t\t\trelFile, err := svcPaths.RelFileName(s.Name, s.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfile := paths.FS(app.Root()).JoinSlash(relFile)\n\t\t\terr = os.MkdirAll(file.Dir().ToIO(), 0755)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfiles[relFile] = fmt.Sprintf(\"%spackage %s\\n\", fmtComment(s.Doc, 0, 1), strings.ToLower(s.Name))\n\t\t}\n\t\tfor _, e := range s.Endpoints {\n\t\t\trelFile, err := svcPaths.RelFileName(s.Name, e.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfilePath := paths.FS(app.Root()).JoinSlash(relFile)\n\t\t\t_, content := toSrcFile(filePath, s.Name, e.EndpointSource, e.TypeSource)\n\t\t\tfiles[relFile], err = addMissingFuncBodies(content)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\treturn files, nil\n}\n\n// addMissingFuncBodies adds a panic statement to functions that are missing a body.\n// This is used to generate a valid Go source file when the user has not implemented\n// the body of the endpoint functions.\nfunc addMissingFuncBodies(content []byte) (string, error) {\n\tset := token.NewFileSet()\n\trewriter := rewrite.New(content, 0)\n\tfile, err := parser.ParseFile(set, \"\", content, parser.ParseComments|parser.AllErrors)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tast.Inspect(file, func(n ast.Node) bool {\n\t\tswitch n := n.(type) {\n\t\tcase *ast.FuncDecl:\n\t\t\tif n.Body != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trewriter.Insert(n.End()-1, []byte(\" {\\n    panic(\\\"not yet implemented\\\")\\n}\\n\"))\n\t\t}\n\t\treturn true\n\t})\n\treturn string(rewriter.Data()), err\n}\n\n// writeFiles writes the generated source files to disk.\nfunc writeFiles(services []Service, app *apps.Instance) ([]paths.RelSlash, error) {\n\tfiles, err := generateSrcFiles(services, app)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor fileName, content := range files {\n\t\troot := paths.FS(app.Root())\n\t\terr = os.WriteFile(root.JoinSlash(fileName).ToIO(), []byte(content), 0644)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn maps.Keys(files), nil\n}\n\n// toSrcFile wraps a code fragment in a package declaration and adds missing imports\n// using the goimports tool.\nfunc toSrcFile(filePath paths.FS, svc string, srcs ...string) (offset token.Position, data []byte) {\n\tconst divider = \"// @code-start\\n\"\n\theader := fmt.Sprintf(\"package %s\\n\\n\", strings.ToLower(svc))\n\tsrc := []byte(header + divider + strings.Join(srcs, \"\\n\"))\n\timportedSrc, err := imports.Process(filePath.ToIO(), src, &imports.Options{\n\t\tComments:  true,\n\t\tTabIndent: false,\n\t\tTabWidth:  4,\n\t})\n\t// We don't need to handle the error here, as we'll catch parser/scanner errors in a later\n\t// phase. This is just a best effort to import missing packages.\n\tif err == nil {\n\t\tsrc = importedSrc\n\t}\n\tcodeOffset := bytes.Index(src, []byte(divider))\n\t// Remove the divider and any formatting made by the imports tool\n\tsrc = append(src[:codeOffset], strings.Join(srcs, \"\\n\")...)\n\t// Compute offset of the user defined code\n\tlines := strings.Split(string(src[:codeOffset]), \"\\n\")\n\treturn token.Position{\n\t\tFilename: filePath.ToIO(),\n\t\tOffset:   codeOffset,\n\t\tLine:     len(lines) - 1,\n\t\tColumn:   0,\n\t}, src\n}\n\n// updateCode updates the source code fields of the EndpointInputs in the given services.\n// if overwrite is set, the code will be regenerated from scratch and replace the existing code,\n// otherwise, we'll modify the code in place\nfunc updateCode(ctx context.Context, services []Service, app *apps.Instance, overwrite bool) (rtn *SyncResult, err error) {\n\toverlays, err := newOverlays(app, overwrite, services...)\n\tfset := token.NewFileSet()\n\tperrs := perr.NewList(ctx, fset, overlays.ReadFile)\n\tdefer func() {\n\t\tperr.CatchBailout(recover())\n\t\tif rtn == nil {\n\t\t\trtn = &SyncResult{\n\t\t\t\tServices: services,\n\t\t\t}\n\t\t}\n\t\trtn.Errors = overlays.validationErrors(perrs)\n\t}()\n\tfor p, olay := range overlays.items {\n\t\tastFile, err := parser.ParseFile(fset, p.ToIO(), olay.content, parser.ParseComments|parser.AllErrors)\n\t\tif err != nil {\n\t\t\tperrs.AddStd(err)\n\t\t}\n\t\trewriter := rewrite.New(olay.content, int(astFile.FileStart))\n\t\ttypeByName := map[string]*ast.GenDecl{}\n\t\tfuncByName := map[string]*ast.FuncDecl{}\n\t\tfor _, decl := range astFile.Decls {\n\t\t\tswitch decl := decl.(type) {\n\t\t\tcase *ast.GenDecl:\n\t\t\t\tif decl.Tok != token.TYPE {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor _, spec := range decl.Specs {\n\t\t\t\t\ttypeSpec := spec.(*ast.TypeSpec)\n\t\t\t\t\ttypeByName[typeSpec.Name.Name] = decl\n\t\t\t\t}\n\t\t\tcase *ast.FuncDecl:\n\t\t\t\tfuncByName[decl.Name.Name] = decl\n\t\t\t}\n\t\t}\n\t\tif olay.codeType == CodeTypeEndpoint {\n\t\t\tfuncDecl, ok := funcByName[olay.endpoint.Name]\n\t\t\tif !ok {\n\t\t\t\tfor _, f := range funcByName {\n\t\t\t\t\tdir, _, _ := directive.Parse(perrs, f.Doc)\n\t\t\t\t\tif dir != nil && dir.Name == \"api\" {\n\t\t\t\t\t\tfuncDecl = f\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif funcDecl != nil {\n\t\t\t\tstart := funcDecl.Pos()\n\t\t\t\tif funcDecl.Doc != nil {\n\t\t\t\t\tstart = funcDecl.Doc.Pos()\n\t\t\t\t}\n\t\t\t\tend := funcDecl.End()\n\t\t\t\tif funcDecl.Body != nil {\n\t\t\t\t\tend = funcDecl.Body.Lbrace\n\t\t\t\t}\n\t\t\t\trewriter.Replace(start, end, []byte(olay.endpoint.Render()))\n\t\t\t} else {\n\t\t\t\tif len(funcByName) > 0 {\n\t\t\t\t\trewriter.Append([]byte(\"\\n\"))\n\t\t\t\t}\n\t\t\t\trewriter.Append([]byte(olay.endpoint.Render()))\n\t\t\t}\n\t\t\tolay.content = rewriter.Data()\n\t\t\tcontent := string(olay.content[olay.headerOffset.Offset:])\n\t\t\tolay.endpoint.EndpointSource = strings.TrimSpace(content)\n\t\t} else {\n\t\t\tfor _, typ := range olay.endpoint.Types {\n\t\t\t\ttypeSpec := typeByName[typ.Name]\n\t\t\t\tcode := typ.Render()\n\t\t\t\tif typeSpec != nil {\n\t\t\t\t\tstart := typeSpec.Pos()\n\t\t\t\t\tif typeSpec.Doc != nil {\n\t\t\t\t\t\tstart = typeSpec.Doc.Pos()\n\t\t\t\t\t}\n\t\t\t\t\trewriter.Replace(start, typeSpec.End(), []byte(code))\n\t\t\t\t} else {\n\t\t\t\t\trewriter.Append([]byte(\"\\n\\n\" + code))\n\t\t\t\t}\n\t\t\t}\n\t\t\tolay.content = rewriter.Data()\n\t\t\tcontent := string(olay.content[olay.headerOffset.Offset:])\n\t\t\tolay.endpoint.TypeSource = strings.TrimSpace(content)\n\t\t}\n\t}\n\tgoRoot := paths.RootedFSPath(env.EncoreGoRoot(), \".\")\n\n\t// Parse the end result to catch any syntax errors\n\tpkginfo.UpdateGoPath(goRoot)\n\tpkgs, err := packages.Load(&packages.Config{\n\t\tMode: packages.NeedTypes | packages.NeedSyntax,\n\t\tDir:  app.Root(),\n\t\tEnv: append(os.Environ(),\n\t\t\t\"GOOS=\"+runtime.GOOS,\n\t\t\t\"GOARCH=\"+runtime.GOARCH,\n\t\t\t\"GOROOT=\"+goRoot.ToIO(),\n\t\t\t\"PATH=\"+goRoot.Join(\"bin\").ToIO()+string(filepath.ListSeparator)+os.Getenv(\"PATH\"),\n\t\t),\n\t\tFset:    fset,\n\t\tOverlay: overlays.PkgOverlay(),\n\t}, fns.Map(overlays.pkgPaths(), paths.Pkg.String)...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, pkg := range pkgs {\n\t\tfor _, err := range pkg.Errors {\n\t\t\t// ignore missing function bodies error (it's allowed)\n\t\t\tif strings.Contains(err.Error(), \"missing function body\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tperrs.AddStd(err)\n\t\t}\n\t}\n\treturn &SyncResult{\n\t\tServices: services,\n\t}, nil\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/conv.go",
    "content": "package ai\n\nimport (\n\t\"slices\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/clientgen\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n)\n\nfunc toPathSegments(p *resourcepaths.Path, docs map[string]string) []PathSegment {\n\trtn := make([]PathSegment, 0, len(p.Segments))\n\tfor _, s := range p.Segments {\n\t\tswitch s.Type {\n\t\tcase resourcepaths.Literal:\n\t\t\trtn = append(rtn, PathSegment{Type: SegmentTypeLiteral, Value: ptr(s.Value)})\n\t\tcase resourcepaths.Param:\n\t\t\trtn = append(rtn, PathSegment{\n\t\t\t\tType:      SegmentTypeParam,\n\t\t\t\tValue:     ptr(s.Value),\n\t\t\t\tValueType: ptr(SegmentValueType(strings.ToLower(s.ValueType.String()))),\n\t\t\t\tDoc:       docs[s.Value],\n\t\t\t})\n\t\tcase resourcepaths.Wildcard:\n\t\t\trtn = append(rtn, PathSegment{\n\t\t\t\tType:      SegmentTypeWildcard,\n\t\t\t\tValue:     ptr(s.Value),\n\t\t\t\tValueType: ptr(SegmentValueType(strings.ToLower(s.ValueType.String()))),\n\t\t\t\tDoc:       docs[s.Value],\n\t\t\t})\n\t\tcase resourcepaths.Fallback:\n\t\t\trtn = append(rtn, PathSegment{\n\t\t\t\tType:      SegmentTypeFallback,\n\t\t\t\tValue:     ptr(s.Value),\n\t\t\t\tValueType: ptr(SegmentValueType(strings.ToLower(s.ValueType.String()))),\n\t\t\t\tDoc:       docs[s.Value],\n\t\t\t})\n\t\t}\n\t}\n\treturn rtn\n}\n\nfunc metaPathToPathSegments(metaPath *meta.Path) []PathSegment {\n\tvar segments []PathSegment\n\tfor _, seg := range metaPath.Segments {\n\t\tsegments = append(segments, PathSegment{\n\t\t\tType:      toSegmentType(seg.Type),\n\t\t\tValue:     ptr(seg.Value),\n\t\t\tValueType: ptr(toSegmentValueType(seg.ValueType)),\n\t\t})\n\t}\n\treturn segments\n}\n\nfunc toSegmentValueType(valueType meta.PathSegment_ParamType) SegmentValueType {\n\tswitch valueType {\n\tcase meta.PathSegment_UUID:\n\t\treturn \"string\"\n\tdefault:\n\t\treturn SegmentValueType(strings.ToLower(valueType.String()))\n\t}\n}\n\nfunc toSegmentType(segmentType meta.PathSegment_SegmentType) SegmentType {\n\tswitch segmentType {\n\tcase meta.PathSegment_LITERAL:\n\t\treturn SegmentTypeLiteral\n\tcase meta.PathSegment_PARAM:\n\t\treturn SegmentTypeParam\n\tcase meta.PathSegment_WILDCARD:\n\t\treturn SegmentTypeWildcard\n\tcase meta.PathSegment_FALLBACK:\n\t\treturn SegmentTypeFallback\n\tdefault:\n\t\tpanic(\"unknown segment type\")\n\t}\n}\n\nfunc toVisibility(accessType meta.RPC_AccessType) VisibilityType {\n\tswitch accessType {\n\tcase meta.RPC_PUBLIC:\n\t\treturn VisibilityTypePublic\n\tcase meta.RPC_PRIVATE:\n\t\treturn VisibilityTypePrivate\n\tcase meta.RPC_AUTH:\n\t\treturn \"\"\n\tdefault:\n\t\tpanic(\"unknown access type\")\n\t}\n}\n\nfunc renderTypesFromMetadata(md *meta.Data, svcs ...string) string {\n\tvar types []*schema.Decl\n\tfor _, metaSvc := range md.Svcs {\n\t\tif len(svcs) > 0 && !slices.Contains(svcs, metaSvc.Name) {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, rpc := range metaSvc.Rpcs {\n\t\t\tif rpc.RequestSchema != nil {\n\t\t\t\ttypes = append(types, md.Decls[rpc.RequestSchema.GetNamed().Id])\n\t\t\t}\n\t\t\tif rpc.ResponseSchema != nil {\n\t\t\t\ttypes = append(types, md.Decls[rpc.ResponseSchema.GetNamed().Id])\n\t\t\t}\n\t\t}\n\t}\n\tsrc, _ := clientgen.GenTypes(md, types...)\n\treturn string(src)\n}\n\nfunc parseServicesFromMetadata(md *meta.Data, svcs ...string) []ServiceInput {\n\tservices := []ServiceInput{}\n\tfor _, metaSvc := range md.Svcs {\n\t\tif len(svcs) > 0 && !slices.Contains(svcs, metaSvc.Name) {\n\t\t\tcontinue\n\t\t}\n\t\tsvc := ServiceInput{\n\t\t\tName: metaSvc.Name,\n\t\t}\n\t\tfor _, rpc := range metaSvc.Rpcs {\n\t\t\tep := &Endpoint{\n\t\t\t\tName:       rpc.Name,\n\t\t\t\tMethod:     rpc.HttpMethods[0],\n\t\t\t\tVisibility: toVisibility(rpc.AccessType),\n\t\t\t\tPath:       metaPathToPathSegments(rpc.Path),\n\t\t\t}\n\t\t\tif rpc.RequestSchema != nil {\n\t\t\t\tdecl := md.Decls[rpc.RequestSchema.GetNamed().Id]\n\t\t\t\tep.RequestType = decl.Name\n\t\t\t}\n\t\t\tif rpc.ResponseSchema != nil {\n\t\t\t\tdecl := md.Decls[rpc.ResponseSchema.GetNamed().Id]\n\t\t\t\tep.ResponseType = decl.Name\n\t\t\t}\n\t\t\tsvc.Endpoints = append(svc.Endpoints, ep)\n\t\t}\n\t\tservices = append(services, svc)\n\t}\n\treturn services\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/manager.go",
    "content": "package ai\n\nimport (\n\t\"context\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nvar ErrorCodeMap = map[string]int64{\n\t\"ai_task_limit_reached\": 100,\n}\n\n// Manager exposes the ai functionality to the local dashboard\ntype Manager struct{}\n\nfunc NewAIManager() *Manager {\n\treturn &Manager{}\n}\n\nfunc (m *Manager) DefineEndpoints(ctx context.Context, appSlug string, sessionID AISessionID, prompt string, md *meta.Data, proposed []Service, notifier AINotifier) (*AITask, error) {\n\tsvcs := fns.Map(proposed, Service.GetName)\n\treturn startAITask[struct {\n\t\tMessage *AIStreamMessage `graphql:\"result: defineEndpoints(appSlug: $appSlug, sessionID: $sessionID, prompt: $prompt, current: $current, proposedDesign: $proposedDesign, existingTypes: $existingTypes)\"`\n\t}](ctx, map[string]interface{}{\n\t\t\"appSlug\":        appSlug,\n\t\t\"prompt\":         prompt,\n\t\t\"current\":        parseServicesFromMetadata(md, svcs...),\n\t\t\"proposedDesign\": fns.Map(proposed, Service.GraphQL),\n\t\t\"sessionID\":      sessionID,\n\t\t\"existingTypes\":  renderTypesFromMetadata(md, svcs...),\n\t}, newEndpointAssemblerHandler(proposed, notifier, true))\n}\n\nfunc (m *Manager) ProposeSystemDesign(ctx context.Context, appSlug, prompt string, md *meta.Data, notifier AINotifier) (*AITask, error) {\n\treturn startAITask[struct {\n\t\tMessage *AIStreamMessage `graphql:\"result: proposeSystemDesign(appSlug: $appSlug, prompt: $prompt, current: $current)\"`\n\t}](ctx, map[string]interface{}{\n\t\t\"appSlug\": appSlug,\n\t\t\"prompt\":  prompt,\n\t\t\"current\": parseServicesFromMetadata(md),\n\t}, newEndpointAssemblerHandler(nil, notifier, false))\n}\n\nfunc (m *Manager) ModifySystemDesign(ctx context.Context, appSlug string, sessionID AISessionID, originalPrompt string, proposed []Service, newPrompt string, md *meta.Data, notifier AINotifier) (*AITask, error) {\n\treturn startAITask[struct {\n\t\tMessage *AIStreamMessage `graphql:\"result: modifySystemDesign(appSlug: $appSlug, sessionID: $sessionID, originalPrompt: $originalPrompt, proposedDesign: $proposedDesign, newPrompt: $newPrompt, current: $current)\"`\n\t}](ctx, map[string]interface{}{\n\t\t\"appSlug\":        appSlug,\n\t\t\"originalPrompt\": originalPrompt,\n\t\t\"proposedDesign\": fns.Map(proposed, Service.GraphQL),\n\t\t\"current\":        parseServicesFromMetadata(md),\n\t\t\"newPrompt\":      newPrompt,\n\t\t\"sessionID\":      sessionID,\n\t}, newEndpointAssemblerHandler(proposed, notifier, false))\n}\n\nfunc (m *Manager) ParseCode(ctx context.Context, services []Service, app *apps.Instance) (*SyncResult, error) {\n\treturn parseCode(ctx, app, services)\n}\n\nfunc (m *Manager) UpdateCode(ctx context.Context, services []Service, app *apps.Instance, overwrite bool) (*SyncResult, error) {\n\treturn updateCode(ctx, services, app, overwrite)\n}\n\ntype WriteFilesResponse struct {\n\tFilesPaths []paths.RelSlash `json:\"paths\"`\n}\n\nfunc (m *Manager) WriteFiles(ctx context.Context, services []Service, app *apps.Instance) (*WriteFilesResponse, error) {\n\tfiles, err := writeFiles(services, app)\n\treturn &WriteFilesResponse{FilesPaths: files}, err\n}\n\ntype PreviewFile struct {\n\tPath    paths.RelSlash `json:\"path\"`\n\tContent string         `json:\"content\"`\n}\n\ntype PreviewFilesResponse struct {\n\tFiles []PreviewFile `json:\"files\"`\n}\n\nfunc (m *Manager) PreviewFiles(ctx context.Context, services []Service, app *apps.Instance) (*PreviewFilesResponse, error) {\n\tfiles, err := generateSrcFiles(services, app)\n\treturn &PreviewFilesResponse{Files: fns.TransformMapToSlice(files, func(k paths.RelSlash, v string) PreviewFile {\n\t\treturn PreviewFile{Path: k, Content: v}\n\t})}, err\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/overlay.go",
    "content": "package ai\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"go/token\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/exp/maps\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/pkg/errinsrc\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/pkg/paths\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\n// servicePaths is a helper struct to manage mapping between service names, pkg paths and filepaths\n// It's created by parsing the metadata of the app\ntype servicePaths struct {\n\trelPaths map[string]paths.RelSlash\n\troot     paths.FS\n\tmodule   paths.Mod\n}\n\nfunc (s *servicePaths) IsNew(svc string) bool {\n\t_, ok := s.relPaths[svc]\n\treturn !ok\n}\n\nfunc (s *servicePaths) Add(svc string, path paths.RelSlash) *servicePaths {\n\ts.relPaths[svc] = path\n\treturn s\n}\n\nfunc (s *servicePaths) PkgPath(svc string) paths.Pkg {\n\trel := s.RelPath(svc)\n\treturn s.module.Pkg(rel)\n}\n\nfunc (s *servicePaths) FullPath(svc string) paths.FS {\n\trel := s.RelPath(svc)\n\treturn s.root.JoinSlash(rel)\n}\n\nfunc (s *servicePaths) RelPath(svc string) paths.RelSlash {\n\tpkgName, ok := s.relPaths[svc]\n\tif !ok {\n\t\tpkgName = paths.RelSlash(strings.ToLower(svc))\n\t}\n\treturn pkgName\n}\n\nfunc (s *servicePaths) FileName(svc, name string) (paths.FS, error) {\n\trelPath, err := s.RelFileName(svc, name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn s.root.JoinSlash(relPath), nil\n}\n\nfunc (s *servicePaths) RelFileName(svc, name string) (paths.RelSlash, error) {\n\tpkgPath := s.FullPath(svc)\n\tname = idents.Convert(name, idents.SnakeCase)\n\tfileName := name + \".go\"\n\tvar i int\n\tfor {\n\t\tfspath := pkgPath.Join(fileName)\n\t\tif _, err := os.Stat(fspath.ToIO()); os.IsNotExist(err) {\n\t\t\treturn s.RelPath(svc).Join(fileName), nil\n\t\t} else if err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\ti++\n\t\tfileName = fmt.Sprintf(\"%s_%d.go\", name, i)\n\t}\n}\n\nfunc newServicePaths(app *apps.Instance) (*servicePaths, error) {\n\tmd, err := app.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpkgRelPath := fns.ToMap(md.Pkgs, func(p *meta.Package) string { return p.RelPath })\n\tsvcPaths := &servicePaths{\n\t\trelPaths: map[string]paths.RelSlash{},\n\t\troot:     paths.FS(app.Root()),\n\t\tmodule:   paths.Mod(md.ModulePath),\n\t}\n\tfor _, svc := range md.Svcs {\n\t\tif pkgRelPath[svc.RelPath] != nil {\n\t\t\tsvcPaths.Add(svc.Name, paths.RelSlash(pkgRelPath[svc.RelPath].RelPath))\n\t\t}\n\t}\n\treturn svcPaths, nil\n}\n\n// An overlay is a virtual file that is used to store the source code of an endpoint or types\n// It automatically generates a header with pkg name and imports.\n// It implements os.FileInfo and os.DirEntry interfaces\ntype overlay struct {\n\tpath         paths.FS\n\tendpoint     *Endpoint\n\tservice      *Service\n\tcodeType     CodeType\n\tcontent      []byte\n\theaderOffset token.Position\n}\n\nfunc (o *overlay) Type() os.FileMode {\n\treturn o.Mode()\n}\n\nfunc (o *overlay) Info() (os.FileInfo, error) {\n\treturn o, nil\n}\n\nfunc (o *overlay) Name() string {\n\treturn o.path.Base()\n}\n\nfunc (o *overlay) Size() int64 {\n\treturn int64(len(o.content))\n}\n\nfunc (o *overlay) Mode() os.FileMode {\n\treturn os.ModePerm\n}\n\nfunc (o *overlay) ModTime() time.Time {\n\treturn time.Now()\n}\n\nfunc (o *overlay) IsDir() bool {\n\treturn false\n}\n\nfunc (o *overlay) Sys() any {\n\t//TODO implement me\n\tpanic(\"implement me\")\n}\n\nfunc (o *overlay) Stat() (os.FileInfo, error) {\n\treturn o, nil\n}\n\nfunc (o *overlay) Reader() io.ReadCloser {\n\treturn &overlayReader{o, bytes.NewReader(o.content)}\n}\n\n// overlayReader is a wrapper around the overlay to implement io.ReadCloser\ntype overlayReader struct {\n\t*overlay\n\t*bytes.Reader\n}\n\nfunc (o *overlayReader) Close() error { return nil }\n\nvar (\n\t_ os.FileInfo = (*overlay)(nil)\n\t_ os.DirEntry = (*overlay)(nil)\n)\n\nfunc newOverlays(app *apps.Instance, overwrite bool, services ...Service) (*overlays, error) {\n\tsvcPaths, err := newServicePaths(app)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\to := &overlays{\n\t\titems: map[paths.FS]*overlay{},\n\t\tpaths: svcPaths,\n\t}\n\tfor _, s := range services {\n\t\tfor _, e := range s.Endpoints {\n\t\t\tif overwrite {\n\t\t\t\te.TypeSource = \"\"\n\t\t\t\te.EndpointSource = \"\"\n\t\t\t}\n\t\t\tif err := o.add(s, e); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\treturn o, nil\n}\n\n// overlays is a collection of virtual files that are used to store the source code of endpoints and types\n// in memory. It's modelled as a replacement for the os package.\ntype overlays struct {\n\titems map[paths.FS]*overlay\n\tpaths *servicePaths\n}\n\nfunc (o *overlays) Stat(name string) (os.FileInfo, error) {\n\tf, ok := o.items[paths.FS(name)]\n\tif !ok {\n\t\t// else return the filesystem file\n\t\treturn os.Stat(name)\n\t}\n\treturn f, nil\n}\n\nfunc (o *overlays) ReadDir(name string) ([]os.DirEntry, error) {\n\tentries := map[string]os.DirEntry{}\n\tosFiles, err := os.ReadDir(name)\n\tfor _, f := range osFiles {\n\t\tentries[f.Name()] = f\n\t}\n\tdir := paths.FS(name)\n\tfor _, info := range o.items {\n\t\tif dir == info.path.Dir() {\n\t\t\tentries[info.path.Base()] = info\n\t\t}\n\t}\n\tif len(entries) == 0 && err != nil {\n\t\treturn nil, err\n\t}\n\treturn maps.Values(entries), nil\n}\n\nfunc (o *overlays) PkgOverlay() map[string][]byte {\n\tfiles := map[string][]byte{}\n\tfor f, info := range o.items {\n\t\tfiles[f.ToIO()] = info.content\n\t}\n\treturn files\n}\n\nfunc (o *overlays) ReadFile(name string) ([]byte, error) {\n\tf, ok := o.items[paths.FS(name)]\n\tif !ok {\n\t\t// else return the filesystem file\n\t\treturn os.ReadFile(name)\n\t}\n\treturn f.content, nil\n}\n\nfunc (o *overlays) Open(name string) (io.ReadCloser, error) {\n\tf, ok := o.items[paths.FS(name)]\n\tif !ok {\n\t\t// else return the filesystem file\n\t\treturn os.Open(name)\n\t}\n\treturn f.Reader(), nil\n}\n\nfunc (o *overlays) pkgPaths() []paths.Pkg {\n\tpkgs := map[paths.Pkg]struct{}{}\n\tfor _, info := range o.items {\n\t\tpkgs[o.paths.PkgPath(info.service.Name)] = struct{}{}\n\t}\n\treturn maps.Keys(pkgs)\n}\n\nfunc (o *overlays) get(p paths.FS) (*overlay, bool) {\n\trtn, ok := o.items[p]\n\treturn rtn, ok\n}\n\n// validationErrors converts a perr.List into a slice of ValidationErrors\nfunc (o *overlays) validationErrors(list *perr.List) []ValidationError {\n\tvar rtn []ValidationError\n\tfor i := 0; i < list.Len(); i++ {\n\t\terr := list.At(i)\n\t\trtn = append(rtn, o.validationError(err)...)\n\t}\n\treturn rtn\n}\n\n// validationError translates errinsrc.ErrInSrc into a ValidationError which is a simplified error\n// used for displaying errors in the dashboard\nfunc (o *overlays) validationError(err *errinsrc.ErrInSrc) []ValidationError {\n\tif err.Params.Locations == nil {\n\t\treturn []ValidationError{{\n\t\t\tMessage: err.Params.Summary,\n\t\t}}\n\t}\n\tvar rtn []ValidationError\n\tfor _, loc := range err.Params.Locations {\n\t\to, ok := o.get(paths.FS(loc.File.FullPath))\n\t\tif !ok {\n\t\t\trtn = append(rtn, ValidationError{\n\t\t\t\tMessage: err.Params.Summary,\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\t\trtn = append(rtn, ValidationError{\n\t\t\tService:  o.service.ID,\n\t\t\tEndpoint: o.endpoint.ID,\n\t\t\tCodeType: o.codeType,\n\t\t\tMessage:  err.Params.Summary,\n\t\t\tStart: &Pos{\n\t\t\t\tLine:   loc.Start.Line - o.headerOffset.Line,\n\t\t\t\tColumn: loc.Start.Col - o.headerOffset.Column,\n\t\t\t},\n\t\t\tEnd: &Pos{\n\t\t\t\tLine:   loc.End.Line - o.headerOffset.Line,\n\t\t\t\tColumn: loc.End.Col - o.headerOffset.Column,\n\t\t\t},\n\t\t})\n\t}\n\treturn rtn\n}\n\n// add creates new overlays for an endpoint and its types.\n// We create separate overlays for each endpoint and its types to allow for easier parsing and code generation.\nfunc (o *overlays) add(s Service, e *Endpoint) error {\n\tp, err := o.paths.FileName(s.Name, e.Name+\"_func\")\n\tif err != nil {\n\t\treturn err\n\t}\n\toffset, content := toSrcFile(p, s.Name, e.EndpointSource)\n\te.EndpointSource = string(content[offset.Offset:])\n\to.items[p] = &overlay{\n\t\tpath:         p,\n\t\tendpoint:     e,\n\t\tservice:      &s,\n\t\tcodeType:     CodeTypeEndpoint,\n\t\tcontent:      content,\n\t\theaderOffset: offset,\n\t}\n\tp, err = o.paths.FileName(s.Name, e.Name+\"_types\")\n\tif err != nil {\n\t\treturn err\n\t}\n\toffset, content = toSrcFile(p, s.Name, e.TypeSource)\n\te.TypeSource = string(content[offset.Offset:])\n\to.items[p] = &overlay{\n\t\tpath:         p,\n\t\tendpoint:     e,\n\t\tservice:      &s,\n\t\tcodeType:     CodeTypeTypes,\n\t\tcontent:      content,\n\t\theaderOffset: offset,\n\t}\n\treturn nil\n}\n\nvar (\n\t_ parsectx.OverlaidOSFS = (*overlays)(nil)\n)\n"
  },
  {
    "path": "cli/daemon/dash/ai/parser.go",
    "content": "package ai\n\nimport (\n\t\"context\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/apis\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\n// parseErrorList parses a list of errors docs from a doc string.\nfunc parseErrorList(doc string) (string, []*Error) {\n\tdoc, errs := parseDocList(doc, ErrDocPrefix)\n\treturn doc, fns.Map(errs, func(e docListItem) *Error {\n\t\treturn &Error{\n\t\t\tCode: e.Key,\n\t\t\tDoc:  e.Doc,\n\t\t}\n\t})\n}\n\n// parsePathList parses a list of path docs from a doc string.\nfunc parsePathList(doc string) (string, map[string]string) {\n\tdoc, docs := parseDocList(doc, PathDocPrefix)\n\trtn := map[string]string{}\n\tfor _, d := range docs {\n\t\trtn[d.Key] = d.Doc\n\t}\n\treturn doc, rtn\n}\n\n// parseDocList parses a list of key-value pairs from a doc string.\n// e.g.\n//\n// Errors:\n//   - NotFound: The requested resource was not found.\n//   - InvalidArgument: The request had invalid arguments.\nfunc parseDocList(doc, section string) (string, []docListItem) {\n\tvar errs []docListItem\n\tlines := strings.Split(doc, \"\\n\")\n\tstart := -1\n\tend := -1\n\tfor i, line := range lines {\n\t\tend = i\n\t\tif strings.HasPrefix(strings.TrimSpace(line), section+\":\") {\n\t\t\tstart = i\n\n\t\t} else if start == -1 {\n\t\t\tcontinue\n\t\t} else if len(line) > 2 {\n\t\t\tswitch strings.TrimSpace(line[:2]) {\n\t\t\tcase \"-\", \"\":\n\t\t\tdefault:\n\t\t\t\tend = i - 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tlines[i] = strings.TrimSpace(line)\n\t\tif line == \"\" && lines[i-1] == \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\tif start == -1 {\n\t\treturn doc, errs\n\t}\n\n\tfor _, line := range lines[start+1 : end+1] {\n\t\tkey, doc, ok := strings.Cut(line, \":\")\n\t\tkey = strings.TrimPrefix(key, \"-\")\n\t\tkey = strings.TrimSpace(key)\n\t\tif ok {\n\t\t\terrs = append(errs, docListItem{\n\t\t\t\tKey: key,\n\t\t\t\tDoc: strings.TrimSpace(doc),\n\t\t\t})\n\t\t} else if len(errs) > 0 && line != \"\" {\n\t\t\terrs[len(errs)-1].Doc += \"\\n\" + line\n\t\t}\n\t}\n\treturn strings.Join(lines[:start], \"\\n\"), errs\n}\n\n// docListItem represents a key-value pair in a doc list.\ntype docListItem struct {\n\tKey string\n\tDoc string\n}\n\n// deref returns the underlying type of a pointer type.\nfunc deref(p schema.Type) schema.Type {\n\tfor {\n\t\tif pt, ok := p.(schema.PointerType); ok {\n\t\t\tp = pt.Elem\n\t\t} else {\n\t\t\treturn p\n\t\t}\n\t}\n}\n\n// parseCode updates the structured EndpointInput data based on the code in\n// EndpointInput.TypeSource and EndpointInput.EndpointSource fields.\nfunc parseCode(ctx context.Context, app *apps.Instance, services []Service) (rtn *SyncResult, err error) {\n\t// assamble an overlay with all our newly defined endpoints\n\toverlays, err := newOverlays(app, false, services...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfs := token.NewFileSet()\n\terrs := perr.NewList(ctx, fs, overlays.ReadFile)\n\trootDir := paths.RootedFSPath(app.Root(), \".\")\n\tpc := &parsectx.Context{\n\t\tCtx: ctx,\n\t\tLog: zerolog.Logger{},\n\t\tBuild: parsectx.BuildInfo{\n\t\t\tExperiments: nil,\n\t\t\tGOROOT:      paths.RootedFSPath(env.EncoreGoRoot(), \".\"),\n\t\t\tGOARCH:      runtime.GOARCH,\n\t\t\tGOOS:        runtime.GOOS,\n\t\t},\n\t\tMainModuleDir: rootDir,\n\t\tFS:            fs,\n\t\tParseTests:    false,\n\t\tErrs:          errs,\n\t\tOverlay:       overlays,\n\t}\n\n\t// Catch parser bailouts and convert them to ValidationErrors\n\tdefer func() {\n\t\tperr.CatchBailout(recover())\n\t\tif rtn == nil {\n\t\t\trtn = &SyncResult{\n\t\t\t\tServices: services,\n\t\t\t}\n\t\t}\n\t\trtn.Errors = overlays.validationErrors(errs)\n\t}()\n\n\t// Load overlay packages using the encore loader\n\tloader := pkginfo.New(pc)\n\tpkgs := map[paths.Pkg]*pkginfo.Package{}\n\tfor _, pkgPath := range overlays.pkgPaths() {\n\t\tpkg, ok := loader.LoadPkg(token.NoPos, pkgPath)\n\t\tif ok {\n\t\t\tpkgs[pkgPath] = pkg\n\t\t}\n\t}\n\n\t// Create a schema parser to help us parse the types\n\tschemaParser := schema.NewParser(pc, loader)\n\n\tfor _, pkg := range pkgs {\n\t\t// Use the API parser to parser the endpoints for each overlaid package\n\t\tpass := &resourceparser.Pass{\n\t\t\tContext:      pc,\n\t\t\tSchemaParser: schemaParser,\n\t\t\tPkg:          pkg,\n\t\t}\n\t\tapis.Parser.Run(pass)\n\t\tfor _, r := range pass.Resources() {\n\t\t\tswitch r := r.(type) {\n\t\t\tcase *api.Endpoint:\n\t\t\t\t// We're only interested in endpoints that are in our overlays\n\t\t\t\toverlay, ok := overlays.get(r.File.FSPath)\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\te := overlay.endpoint\n\n\t\t\t\tpathDocs := map[string]string{}\n\t\t\t\te.Doc, e.Errors = parseErrorList(r.Doc)\n\t\t\t\te.Doc, pathDocs = parsePathList(e.Doc)\n\t\t\t\te.Name = r.Name\n\t\t\t\te.Method = r.HTTPMethods[0]\n\t\t\t\te.Visibility = VisibilityType(r.Access)\n\t\t\t\te.Language = \"GO\"\n\t\t\t\te.Path = toPathSegments(r.Path, pathDocs)\n\n\t\t\t\t// Clear the types as we will reparse them\n\t\t\t\te.Types = []*Type{}\n\t\t\t\tif nr, ok := deref(r.Request).(schema.NamedType); ok {\n\t\t\t\t\te.RequestType = nr.String()\n\t\t\t\t\t// If the request type is in the overlays, we should parse it and\n\t\t\t\t\t// add it to the endpoint associated with the overlay\n\t\t\t\t\tov, ok := overlays.get(nr.DeclInfo.File.FSPath)\n\t\t\t\t\tif len(r.RequestEncoding()) > 0 && ok {\n\t\t\t\t\t\te = ov.endpoint\n\t\t\t\t\t\te.Types = append(e.Types, &Type{\n\t\t\t\t\t\t\tName: nr.String(),\n\t\t\t\t\t\t\tDoc:  strings.TrimSpace(nr.DeclInfo.Doc),\n\t\t\t\t\t\t\tFields: fns.Map(r.RequestEncoding()[0].AllParameters(), func(f *apienc.ParameterEncoding) *TypeField {\n\t\t\t\t\t\t\t\treturn &TypeField{\n\t\t\t\t\t\t\t\t\tName:     f.SrcName,\n\t\t\t\t\t\t\t\t\tWireName: f.WireName,\n\t\t\t\t\t\t\t\t\tLocation: f.Location,\n\t\t\t\t\t\t\t\t\tType:     f.Type.String(),\n\t\t\t\t\t\t\t\t\tDoc:      strings.TrimSpace(f.Doc),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif nr, ok := deref(r.Response).(schema.NamedType); ok {\n\t\t\t\t\te.ResponseType = nr.String()\n\t\t\t\t\t// If the response type is in the overlays, we should parse it and\n\t\t\t\t\t// add it to the endpoint associated with the overlay\n\t\t\t\t\tov, ok := overlays.get(nr.DeclInfo.File.FSPath)\n\t\t\t\t\tif r.ResponseEncoding() != nil && ok {\n\t\t\t\t\t\te = ov.endpoint\n\t\t\t\t\t\te.Types = append(e.Types, &Type{\n\t\t\t\t\t\t\tName: nr.String(),\n\t\t\t\t\t\t\tDoc:  strings.TrimSpace(nr.DeclInfo.Doc),\n\t\t\t\t\t\t\tFields: fns.Map(r.ResponseEncoding().AllParameters(), func(f *apienc.ParameterEncoding) *TypeField {\n\t\t\t\t\t\t\t\treturn &TypeField{\n\t\t\t\t\t\t\t\t\tName:     f.SrcName,\n\t\t\t\t\t\t\t\t\tWireName: f.WireName,\n\t\t\t\t\t\t\t\t\tLocation: f.Location,\n\t\t\t\t\t\t\t\t\tType:     f.Type.String(),\n\t\t\t\t\t\t\t\t\tDoc:      strings.TrimSpace(f.Doc),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Parse types which are in the overlays but not used in request/response\n\t\tfor _, file := range pkg.Files {\n\t\t\tast.Inspect(file.AST(), func(node ast.Node) bool {\n\t\t\t\tswitch node := node.(type) {\n\t\t\t\tcase *ast.GenDecl:\n\t\t\t\t\t// We're only interested in type declarations\n\t\t\t\t\tif node.Tok != token.TYPE {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tfor _, spec := range node.Specs {\n\t\t\t\t\t\td := spec.(*ast.TypeSpec)\n\t\t\t\t\t\t// If the type is not defined in our overlays, skip it.\n\t\t\t\t\t\tolay, ok := overlays.get(file.FSPath)\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// If it's not a struct type, skip it.\n\t\t\t\t\t\ts, ok := schemaParser.ParseType(file, d.Type).(schema.StructType)\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\te := olay.endpoint\n\t\t\t\t\t\t// If the type has already been parsed, skip it.\n\t\t\t\t\t\tif slices.ContainsFunc(e.Types, func(t *Type) bool { return t.Name == d.Name.Name }) {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Otherwise we should add it\n\t\t\t\t\t\te.Types = append(e.Types, &Type{\n\t\t\t\t\t\t\tName:   d.Name.Name,\n\t\t\t\t\t\t\tDoc:    docText(node.Doc),\n\t\t\t\t\t\t\tFields: fns.MapAndFilter(s.Fields, parseTypeField),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t}\n\t}\n\treturn &SyncResult{\n\t\tServices: services,\n\t}, nil\n}\n\n// parseTypeField is a helper function to parse a schema field into a TypeField.\nfunc parseTypeField(f schema.StructField) (*TypeField, bool) {\n\tname, ok := f.Name.Get()\n\tif !ok {\n\t\treturn nil, false\n\t}\n\t// Fields which are parsed by this functions are not a request or response type,\n\t// so we can assume the wire name is the json tag name.\n\twireName := name\n\tif tag, err := f.Tag.Get(\"json\"); err == nil {\n\t\twireName = tag.Name\n\t}\n\treturn &TypeField{\n\t\tName:     name,\n\t\tType:     f.Type.String(),\n\t\tDoc:      f.Doc,\n\t\tWireName: wireName,\n\t}, true\n}\n\n// helper function to extract the text from a comment node or \"\" if nil\nfunc docText(c *ast.CommentGroup) string {\n\tif c == nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(c.Text())\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/sql.go",
    "content": "package ai\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/golang/protobuf/proto\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/proto/encore/daemon\"\n)\n\n// ParseSQLSchema uses SQLC to parse the migration files for an encore database and returns\n// the parsed catalog\nfunc ParseSQLSchema(app *apps.Instance, schema string) (*daemon.SQLCPlugin_Catalog, error) {\n\tschemaPath := filepath.Join(app.Root(), schema)\n\tcmd := exec.Command(os.Args[0], \"generate-sql-schema\", \"--proto\", schemaPath)\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar req daemon.SQLCPlugin_GenerateRequest\n\tif err := proto.Unmarshal(output, &req); err != nil {\n\t\treturn nil, err\n\t}\n\treturn req.Catalog, nil\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/types.go",
    "content": "package ai\n\nimport (\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n)\n\ntype VisibilityType string\n\nconst (\n\tVisibilityTypePublic  VisibilityType = \"public\"\n\tVisibilityTypePrivate VisibilityType = \"private\"\n\tVisibilityTypeAuth    VisibilityType = \"auth\"\n)\n\ntype SegmentType string\n\nconst (\n\tSegmentTypeLiteral  SegmentType = \"literal\"\n\tSegmentTypeParam    SegmentType = \"param\"\n\tSegmentTypeWildcard SegmentType = \"wildcard\"\n\tSegmentTypeFallback SegmentType = \"fallback\"\n)\n\ntype SegmentValueType string\n\nconst SegmentValueTypeString SegmentValueType = \"string\"\n\ntype PathSegments []PathSegment\n\ntype PathSegment struct {\n\tType      SegmentType       `json:\"type,omitempty\"`\n\tValue     *string           `json:\"value,omitempty\"`\n\tValueType *SegmentValueType `json:\"valueType,omitempty\"`\n\tDoc       string            `graphql:\"-\" json:\"doc,omitempty\"`\n}\n\nfunc (p PathSegment) DocItem() (string, string) {\n\treturn *p.Value, p.Doc\n}\n\ntype Endpoint struct {\n\tID             string         `json:\"id,omitempty\"`\n\tName           string         `json:\"name\"`\n\tDoc            string         `json:\"doc\"`\n\tMethod         string         `json:\"method\"`\n\tVisibility     VisibilityType `json:\"visibility\"`\n\tPath           PathSegments   `json:\"path\"`\n\tRequestType    string         `json:\"requestType,omitempty\"`\n\tResponseType   string         `json:\"responseType,omitempty\"`\n\tErrors         []*Error       `json:\"errors,omitempty\"`\n\tTypes          []*Type        `json:\"types,omitempty\"`\n\tLanguage       string         `json:\"language,omitempty\"`\n\tTypeSource     string         `json:\"typeSource,omitempty\"`\n\tEndpointSource string         `json:\"endpointSource,omitempty\"`\n}\n\nfunc (s *Endpoint) Auth() bool {\n\treturn s.Visibility == VisibilityTypeAuth\n}\n\n// GraphQL scrubs data that is not needed for the graphql client\nfunc (s *Endpoint) GraphQL() *Endpoint {\n\ts.ID = \"\"\n\ts.EndpointSource = \"\"\n\ts.TypeSource = \"\"\n\ts.Types = nil\n\ts.Language = \"\"\n\tfor i, _ := range s.Path {\n\t\ts.Path[i].Doc = \"\"\n\t}\n\treturn s\n}\n\ntype Type struct {\n\tName   string       `json:\"name,omitempty\"`\n\tDoc    string       `json:\"doc,omitempty\"`\n\tFields []*TypeField `json:\"fields,omitempty\"`\n}\n\ntype Service struct {\n\tID        string      `json:\"id,omitempty\"`\n\tName      string      `json:\"name,omitempty\"`\n\tDoc       string      `json:\"doc,omitempty\"`\n\tEndpoints []*Endpoint `json:\"endpoints,omitempty\"`\n}\n\nfunc (s Service) GetName() string {\n\treturn s.Name\n}\n\nfunc (s Service) GetEndpoints() []*Endpoint {\n\treturn s.Endpoints\n}\n\n// ServiceInput is the graphql input type for our queries\n// the graphQL client we use requires the type name to match the\n// graphql type\ntype ServiceInput Service\n\n// GraphQL scrubs data that is not needed for the graphql client\nfunc (s Service) GraphQL() ServiceInput {\n\ts.ID = \"\"\n\tfor _, e := range s.Endpoints {\n\t\te.GraphQL()\n\t}\n\treturn ServiceInput(s)\n}\n\ntype BaseAIUpdateType struct {\n\tType string `graphql:\"__typename\" json:\"type\"`\n}\n\nfunc (b BaseAIUpdateType) IsAIUpdateType() {}\n\ntype AIUpdateType interface {\n\tIsAIUpdateType()\n}\n\ntype AIStreamUpdate = Result[AIUpdateType]\n\nfunc ptr[T any](val T) *T {\n\treturn &val\n}\n\ntype Result[T any] struct {\n\tValue    T\n\tFinished *bool\n\tError    *string\n}\n\ntype EndpointUpdate struct {\n\tBaseAIUpdateType\n\tService      string         `json:\"service,omitempty\"`\n\tName         string         `json:\"name,omitempty\"`\n\tDoc          string         `json:\"doc,omitempty\"`\n\tMethod       string         `json:\"method,omitempty\"`\n\tVisibility   VisibilityType `json:\"visibility,omitempty\"`\n\tPath         []PathSegment  `json:\"path,omitempty\"`\n\tRequestType  string         `json:\"requestType,omitempty\"`\n\tResponseType string         `json:\"responseType,omitempty\"`\n\tErrors       []string       `json:\"errors,omitempty\"`\n}\n\ntype ServiceUpdate struct {\n\tBaseAIUpdateType\n\tName string `json:\"name,omitempty\"`\n\tDoc  string `json:\"doc,omitempty\"`\n}\n\ntype TypeUpdate struct {\n\tBaseAIUpdateType\n\tService  string `json:\"service,omitempty\"`\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tDoc      string `graphql:\"mdoc: doc\" json:\"doc,omitempty\"`\n}\n\ntype AISessionID string\n\ntype SessionUpdate struct {\n\tBaseAIUpdateType\n\tId AISessionID\n}\n\ntype TitleUpdate struct {\n\tBaseAIUpdateType\n\tTitle string\n}\n\ntype LocalEndpointUpdate struct {\n\tType     string    `json:\"type,omitempty\"`\n\tService  string    `json:\"service,omitempty\"`\n\tEndpoint *Endpoint `json:\"endpoint,omitempty\"`\n}\n\ntype TypeField struct {\n\tName     string         `json:\"name,omitempty\"`\n\tWireName string         `json:\"wireName,omitempty\"`\n\tType     string         `json:\"type,omitempty\"`\n\tLocation apienc.WireLoc `json:\"location,omitempty\"`\n\tDoc      string         `json:\"doc,omitempty\"`\n}\n\ntype TypeFieldUpdate struct {\n\tBaseAIUpdateType\n\tService  string `json:\"service,omitempty\"`\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\tStruct   string `json:\"struct,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tDoc      string `graphql:\"mdoc: doc\" json:\"doc,omitempty\"`\n}\n\ntype Error struct {\n\tCode string `json:\"code,omitempty\"`\n\tDoc  string `json:\"doc,omitempty\"`\n}\n\nfunc (e Error) DocItem() (string, string) {\n\treturn e.Code, e.Doc\n}\n\nfunc (e Error) String() string {\n\treturn e.Code\n}\n\ntype ErrorUpdate struct {\n\tBaseAIUpdateType\n\tCode     string `json:\"code,omitempty\"`\n\tDoc      string `json:\"doc,omitempty\"`\n\tService  string `json:\"service,omitempty\"`\n\tEndpoint string `json:\"endpoint,omitempty\"`\n}\n\ntype PathParamUpdate struct {\n\tBaseAIUpdateType\n\tService  string `json:\"service,omitempty\"`\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\tParam    string `json:\"param,omitempty\"`\n\tDoc      string `json:\"doc,omitempty\"`\n}\n\ntype SyncResult struct {\n\tServices []Service         `json:\"services\"`\n\tErrors   []ValidationError `json:\"errors\"`\n}\n\n// ValidationError is a simplified ErrInSrc to return to the dashboard\ntype ValidationError struct {\n\tService  string   `json:\"service\"`\n\tEndpoint string   `json:\"endpoint\"`\n\tCodeType CodeType `json:\"codeType\"`\n\tMessage  string   `json:\"message\"`\n\tStart    *Pos     `json:\"start,omitempty\"`\n\tEnd      *Pos     `json:\"end,omitempty\"`\n}\n\ntype CodeType string\n\nconst (\n\tCodeTypeEndpoint CodeType = \"endpoint\"\n\tCodeTypeTypes    CodeType = \"types\"\n)\n\ntype Pos struct {\n\tLine   int `json:\"line\"`\n\tColumn int `json:\"column\"`\n}\n"
  },
  {
    "path": "cli/daemon/dash/ai/types_test.go",
    "content": "package ai\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestWrapDoc(t *testing.T) {\n\tvar wrapTests = []struct {\n\t\twidth  int\n\t\tstring string\n\t}{\n\t\t{1, \"Lorem ipsum dolor sit amet\"},\n\t\t{80, \"Lorem ipsum dolor sit amet\"},\n\t\t{80, \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\"},\n\t\t{80, \"Lorem Loremipsumdolorsitamet,consecteturadipiscingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagna\"},\n\t\t{30, \"Loremipsumdolorsitamet,consecteturadipiscingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagna\"},\n\t\t{80, \"\"},\n\t\t{80, \"a\\nb\\nc\\nd\"},\n\t}\n\tfor _, test := range wrapTests {\n\t\tt.Run(fmt.Sprintf(\"WrapDoc(%d, %s)\", test.width, test.string), func(t *testing.T) {\n\t\t\tresult := wrapDoc(test.string, test.width)\n\t\t\tlines := strings.Split(result, \"\\n\")\n\t\t\tfor i, line := range lines {\n\t\t\t\tif len(line) > test.width && strings.Contains(line, \" \") {\n\t\t\t\t\tt.Errorf(\"Line too long: %s\", line)\n\t\t\t\t}\n\t\t\t\tif i+1 < len(lines) {\n\t\t\t\t\tnextWord, _, _ := strings.Cut(lines[i+1], \" \")\n\t\t\t\t\tif len(line)+len(nextWord) < test.width {\n\t\t\t\t\t\tt.Errorf(\"Line too short: %s\", line)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/dash/apiproxy/apiproxy.go",
    "content": "package apiproxy\n\nimport (\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"runtime\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"golang.org/x/oauth2\"\n\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/version\"\n)\n\nfunc New(targetURL string) (*httputil.ReverseProxy, error) {\n\ttarget, err := url.Parse(targetURL)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"parse target url\")\n\t}\n\n\tproxy := &httputil.ReverseProxy{\n\t\tTransport: &oauth2.Transport{\n\t\t\tBase:   http.DefaultTransport,\n\t\t\tSource: oauth2.ReuseTokenSource(nil, conf.DefaultTokenSource),\n\t\t},\n\t\tErrorHandler: func(writer http.ResponseWriter, request *http.Request, err error) {\n\t\t\tif errors.Is(err, conf.ErrNotLoggedIn) {\n\t\t\t\twriter.WriteHeader(http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t\twriter.WriteHeader(http.StatusBadGateway)\n\n\t\t},\n\t\tRewrite: func(r *httputil.ProxyRequest) {\n\t\t\tr.Out.URL = target\n\t\t\tr.Out.Header.Set(\"User-Agent\", \"EncoreCLI/\"+version.Version)\n\t\t\tr.Out.Header.Set(\"X-Encore-Dev-Dash\", \"true\")\n\t\t\tr.Out.Header.Set(\"X-Encore-Version\", version.Version)\n\t\t\tr.Out.Header.Set(\"X-Encore-GOOS\", runtime.GOOS)\n\t\t\tr.Out.Header.Set(\"X-Encore-GOARCH\", runtime.GOARCH)\n\t\t},\n\t}\n\treturn proxy, nil\n}\n"
  },
  {
    "path": "cli/daemon/dash/dash.go",
    "content": "// Package dash serves the Encore Developer Dashboard.\npackage dash\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/golang/protobuf/jsonpb\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/dash/ai\"\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/cli/internal/browser\"\n\t\"encr.dev/cli/internal/jsonrpc2\"\n\t\"encr.dev/cli/internal/onboarding\"\n\t\"encr.dev/cli/internal/telemetry\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/parser/encoding\"\n\t\"encr.dev/pkg/editors\"\n\t\"encr.dev/pkg/errlist\"\n\t\"encr.dev/pkg/jsonext\"\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype handler struct {\n\trpc  jsonrpc2.Conn\n\tapps *apps.Manager\n\trun  *run.Manager\n\tns   *namespace.Manager\n\tai   *ai.Manager\n\ttr   trace2.Store\n}\n\nfunc (h *handler) GetMeta(appID string) (*meta.Data, error) {\n\trunInstance := h.run.FindRunByAppID(appID)\n\tvar md *meta.Data\n\tif runInstance != nil && runInstance.ProcGroup() != nil {\n\t\tmd = runInstance.ProcGroup().Meta\n\t} else {\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(appID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmd, err = app.CachedMetadata()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t} else if md == nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn md, nil\n}\n\nfunc (h *handler) GetNamespace(ctx context.Context, appID string) (*namespace.Namespace, error) {\n\trunInstance := h.run.FindRunByAppID(appID)\n\tif runInstance != nil && runInstance.ProcGroup() != nil {\n\t\treturn runInstance.NS, nil\n\t} else {\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(appID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tns, err := h.ns.GetActive(ctx, app)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ns, nil\n\t}\n}\n\nfunc (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error {\n\treply = makeProtoReplier(reply)\n\n\tunmarshal := func(dst interface{}) error {\n\t\tif r.Params() == nil {\n\t\t\treturn fmt.Errorf(\"missing params\")\n\t\t}\n\t\treturn json.Unmarshal([]byte(r.Params()), dst)\n\t}\n\n\tswitch r.Method() {\n\tcase \"db/query\":\n\t\tvar p QueryRequest\n\t\tif err := unmarshal(&p); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tres, err := h.Query(ctx, p)\n\t\treturn reply(ctx, res, err)\n\tcase \"db/transaction\":\n\t\tvar p TransactionRequest\n\t\tif err := unmarshal(&p); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tres, err := h.Transaction(ctx, p)\n\t\treturn reply(ctx, res, err)\n\tcase \"onboarding/get\":\n\t\tstate, err := onboarding.Load()\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tresp := map[string]time.Time{}\n\t\tfor key, val := range state.EventMap {\n\t\t\tif val.IsSet() {\n\t\t\t\tresp[key] = val.UTC()\n\t\t\t}\n\t\t}\n\t\treturn reply(ctx, resp, nil)\n\tcase \"onboarding/set\":\n\t\ttype params struct {\n\t\t\tProperties []string `json:\"properties\"`\n\t\t}\n\t\tvar p params\n\t\tif err := unmarshal(&p); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tstate, err := onboarding.Load()\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tfor _, prop := range p.Properties {\n\t\t\tstate.Property(prop).Set()\n\t\t}\n\t\terr = state.Write()\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\treturn reply(ctx, nil, nil)\n\tcase \"telemetry\":\n\t\ttype params struct {\n\t\t\tEvent      string                 `json:\"event\"`\n\t\t\tProperties map[string]interface{} `json:\"properties\"`\n\t\t\tOnce       bool                   `json:\"once,omitempty\"`\n\t\t}\n\t\tvar p params\n\t\tif err := unmarshal(&p); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tif p.Once {\n\t\t\ttelemetry.SendOnce(p.Event, p.Properties)\n\t\t} else {\n\t\t\ttelemetry.Send(p.Event, p.Properties)\n\t\t}\n\t\treturn reply(ctx, \"ok\", nil)\n\tcase \"version\":\n\t\ttype versionResp struct {\n\t\t\tVersion string `json:\"version\"`\n\t\t\tChannel string `json:\"channel\"`\n\t\t}\n\n\t\trtn := versionResp{\n\t\t\tVersion: version.Version,\n\t\t\tChannel: string(version.Channel),\n\t\t}\n\n\t\treturn reply(ctx, rtn, nil)\n\n\tcase \"list-apps\":\n\t\ttype app struct {\n\t\t\tID      string `json:\"id\"`\n\t\t\tName    string `json:\"name\"`\n\t\t\tAppRoot string `json:\"app_root\"`\n\t\t\tOffline bool   `json:\"offline,omitempty\"`\n\t\t}\n\n\t\tapps := []app{} // prevent marshalling as null\n\n\t\t// Load all the apps we know about\n\t\tallApp, err := h.apps.List()\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tfor _, instance := range allApp {\n\t\t\tdata := app{\n\t\t\t\tID:      instance.PlatformOrLocalID(),\n\t\t\t\tName:    instance.Name(),\n\t\t\t\tAppRoot: instance.Root(),\n\t\t\t\tOffline: true,\n\t\t\t}\n\n\t\t\tif run := h.run.FindRunByAppID(instance.PlatformOrLocalID()); run != nil {\n\t\t\t\tdata.Offline = false\n\t\t\t}\n\n\t\t\tapps = append(apps, data)\n\t\t}\n\n\t\t// Sort the apps by offline status, then by name\n\t\tslices.SortStableFunc(apps, func(a, b app) int {\n\t\t\tif a.Offline == b.Offline {\n\t\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t\t}\n\t\t\tif a.Offline {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn -1\n\t\t})\n\n\t\treturn reply(ctx, apps, nil)\n\tcase \"traces/clear\":\n\t\ttelemetry.Send(\"traces.clear\")\n\t\tvar params struct {\n\t\t\tAppID string `json:\"app_id\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\terr := h.tr.Clear(ctx, params.AppID)\n\t\treturn reply(ctx, \"ok\", err)\n\tcase \"traces/list\":\n\t\ttelemetry.Send(\"traces.list\")\n\t\tvar params struct {\n\t\t\tAppID      string `json:\"app_id\"`\n\t\t\tMessageID  string `json:\"message_id\"`\n\t\t\tTestTraces *bool  `json:\"test_traces,omitempty\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tquery := &trace2.Query{\n\t\t\tAppID:      params.AppID,\n\t\t\tTestFilter: params.TestTraces,\n\t\t\tMessageID:  params.MessageID,\n\t\t\tLimit:      100,\n\t\t}\n\t\tvar list []*tracepb2.SpanSummary\n\t\titer := func(s *tracepb2.SpanSummary) bool {\n\t\t\tlist = append(list, s)\n\t\t\treturn true\n\t\t}\n\t\terr := h.tr.List(ctx, query, iter)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"dash: could not list traces\")\n\t\t}\n\t\treturn reply(ctx, list, err)\n\n\tcase \"traces/get\":\n\t\ttelemetry.Send(\"traces.get\")\n\t\tvar params struct {\n\t\t\tAppID   string `json:\"app_id\"`\n\t\t\tTraceID string `json:\"trace_id\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tvar events []*tracepb2.TraceEvent\n\t\titer := func(ev *tracepb2.TraceEvent) bool {\n\t\t\tevents = append(events, ev)\n\t\t\treturn true\n\t\t}\n\t\terr := h.tr.Get(ctx, params.AppID, params.TraceID, iter)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"dash: could not list trace events\")\n\t\t}\n\t\treturn reply(ctx, events, err)\n\n\tcase \"traces/spans/summaries/list\":\n\t\ttelemetry.Send(\"traces.spans.summaries.list\")\n\t\tvar params struct {\n\t\t\tAppID   string `json:\"app_id\"`\n\t\t\tTraceID string `json:\"trace_id\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tspans, err := h.tr.GetSpanSummaries(ctx, params.AppID, params.TraceID)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"dash: could not list trace spans\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\treturn reply(ctx, spans, err)\n\tcase \"traces/spans/events/list\":\n\t\ttelemetry.Send(\"traces.spans.events.list\")\n\t\tvar params struct {\n\t\t\tAppID   string `json:\"app_id\"`\n\t\t\tTraceID string `json:\"trace_id\"`\n\t\t\tSpanID  string `json:\"span_id\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tevents, err := h.tr.GetEvents(ctx, params.AppID, params.TraceID, params.SpanID)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"dash: could not get span events\")\n\t\t}\n\t\treturn reply(ctx, events, err)\n\tcase \"status\":\n\t\tvar params struct {\n\t\t\tAppID string\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\t// Find the latest app by platform ID or local ID.\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, apps.ErrNotFound) {\n\t\t\t\treturn reply(ctx, map[string]interface{}{\"running\": false}, nil)\n\t\t\t} else {\n\t\t\t\treturn reply(ctx, nil, err)\n\t\t\t}\n\t\t}\n\n\t\t// Now find the running instance(s)\n\t\trunInstance := h.run.FindRunByAppID(params.AppID)\n\t\tstatus, err := buildAppStatus(app, runInstance)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"dash: could not build app status\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\treturn reply(ctx, status, nil)\n\tcase \"db-migration-status\":\n\t\tvar params struct {\n\t\t\tAppID string\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\t// Find the latest app by platform ID or local ID.\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tappMeta, err := h.GetMeta(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tnamespace, err := h.GetNamespace(ctx, params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tclusterType := sqldb.Run\n\t\tcluster, ok := h.run.ClusterMgr.Get(sqldb.GetClusterID(app, clusterType, namespace))\n\t\tif !ok {\n\t\t\treturn reply(ctx, []dbMigrationHistory{}, nil)\n\t\t}\n\n\t\tstatus := buildDbMigrationStatus(ctx, appMeta, cluster)\n\n\t\treturn reply(ctx, status, nil)\n\tcase \"api-call\":\n\t\ttelemetry.Send(\"api.call\")\n\t\tvar params run.ApiCallParams\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tres, err := run.CallAPI(ctx, h.run.FindRunByAppID(params.AppID), &params)\n\t\treturn reply(ctx, res, err)\n\n\tcase \"editors/list\":\n\t\tvar resp struct {\n\t\t\tEditors []string `json:\"editors\"`\n\t\t}\n\n\t\tfound, err := editors.Resolve(ctx)\n\t\tif err != nil {\n\t\t\tlog.Err(err).Msg(\"dash: could not list editors\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tfor _, e := range found {\n\t\t\tresp.Editors = append(resp.Editors, string(e.Editor))\n\t\t}\n\t\treturn reply(ctx, resp, nil)\n\tcase \"ai/propose-system-design\":\n\t\ttelemetry.Send(\"ai.propose\")\n\t\tlog.Debug().Msg(\"dash: propose-system-design\")\n\t\tvar params struct {\n\t\t\tAppID  string `json:\"app_id\"`\n\t\t\tPrompt string `json:\"prompt\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tmd, err := h.GetMeta(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tsessionCh := make(chan *ai.AINotification)\n\t\tdefer close(sessionCh)\n\t\tidResp := sync.Once{}\n\t\ttask, err := h.ai.ProposeSystemDesign(ctx, params.AppID, params.Prompt, md, func(ctx context.Context, msg *ai.AINotification) error {\n\t\t\tif _, ok := msg.Value.(ai.SessionUpdate); ok || msg.Error != nil {\n\t\t\t\tidResp.Do(func() {\n\t\t\t\t\tsessionCh <- msg\n\t\t\t\t})\n\t\t\t\tif ok {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn h.rpc.Notify(ctx, r.Method()+\"/stream\", msg)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tselect {\n\t\tcase msg := <-sessionCh:\n\t\t\tsu, ok := msg.Value.(ai.SessionUpdate)\n\t\t\tif !ok || msg.Error != nil {\n\t\t\t\tif msg.Error != nil {\n\t\t\t\t\terr = jsonrpc2.NewError(ai.ErrorCodeMap[msg.Error.Code], msg.Error.Message)\n\t\t\t\t} else {\n\t\t\t\t\terr = jsonrpc2.NewError(1, \"missing session_id\")\n\t\t\t\t}\n\t\t\t\treturn reply(ctx, nil, err)\n\t\t\t}\n\t\t\treturn reply(ctx, map[string]string{\n\t\t\t\t\"session_id\":      string(su.Id),\n\t\t\t\t\"subscription_id\": task.SubscriptionID,\n\t\t\t}, nil)\n\t\tcase <-ctx.Done():\n\t\t\treturn reply(ctx, nil, ctx.Err())\n\t\tcase <-time.NewTimer(10 * time.Second).C:\n\t\t\t_ = task.Stop()\n\t\t\treturn reply(ctx, nil, errors.New(\"timed out waiting for response\"))\n\t\t}\n\n\tcase \"ai/modify-system-design\":\n\t\ttelemetry.Send(\"ai.modify\")\n\t\tlog.Debug().Msg(\"dash: modify-system-design\")\n\t\tvar params struct {\n\t\t\tAppID          string         `json:\"app_id\"`\n\t\t\tSessionID      ai.AISessionID `json:\"session_id\"`\n\t\t\tOriginalPrompt string         `json:\"original_prompt\"`\n\t\t\tPrompt         string         `json:\"prompt\"`\n\t\t\tProposed       []ai.Service   `json:\"proposed\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tmd, err := h.GetMeta(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\ttask, err := h.ai.ModifySystemDesign(ctx, params.AppID, params.SessionID, params.OriginalPrompt, params.Proposed, params.Prompt, md, func(ctx context.Context, msg *ai.AINotification) error {\n\t\t\treturn h.rpc.Notify(ctx, r.Method()+\"/stream\", msg)\n\t\t})\n\t\treturn reply(ctx, task.SubscriptionID, err)\n\tcase \"ai/define-endpoints\":\n\t\ttelemetry.Send(\"ai.details\")\n\t\tlog.Debug().Msg(\"dash: define-endpoints\")\n\t\tlog.Debug().Msg(\"dash: define-endpoints\")\n\t\tvar params struct {\n\t\t\tAppID     string         `json:\"app_id\"`\n\t\t\tSessionID ai.AISessionID `json:\"session_id\"`\n\t\t\tPrompt    string         `json:\"prompt\"`\n\t\t\tProposed  []ai.Service   `json:\"proposed\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tmd, err := h.GetMeta(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\ttask, err := h.ai.DefineEndpoints(ctx, params.AppID, params.SessionID, params.Prompt, md, params.Proposed, func(ctx context.Context, msg *ai.AINotification) error {\n\t\t\treturn h.rpc.Notify(ctx, r.Method()+\"/stream\", msg)\n\t\t})\n\t\treturn reply(ctx, task.SubscriptionID, err)\n\tcase \"ai/parse-code\":\n\t\tlog.Debug().Msg(\"dash: parse-code\")\n\t\tvar params struct {\n\t\t\tAppID    string       `json:\"app_id\"`\n\t\t\tServices []ai.Service `json:\"services\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tresults, err := h.ai.ParseCode(ctx, params.Services, app)\n\t\treturn reply(ctx, results, err)\n\tcase \"ai/update-code\":\n\t\tlog.Debug().Msg(\"dash: update-code\")\n\t\tvar params struct {\n\t\t\tAppID     string       `json:\"app_id\"`\n\t\t\tServices  []ai.Service `json:\"services\"`\n\t\t\tOverwrite bool         `json:\"overwrite\"` // Ovwerwrite any existing endpoint code\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tresults, err := h.ai.UpdateCode(ctx, params.Services, app, params.Overwrite)\n\t\treturn reply(ctx, results, err)\n\tcase \"ai/preview-files\":\n\t\ttelemetry.Send(\"ai.preview\")\n\t\tlog.Debug().Msg(\"dash: preview-files\")\n\t\tvar params struct {\n\t\t\tAppID    string       `json:\"app_id\"`\n\t\t\tServices []ai.Service `json:\"services\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tresult, err := h.ai.PreviewFiles(ctx, params.Services, app)\n\t\treturn reply(ctx, result, err)\n\tcase \"ai/write-files\":\n\t\ttelemetry.Send(\"ai.write\")\n\t\tlog.Debug().Msg(\"dash: write-files\")\n\t\tvar params struct {\n\t\t\tAppID    string       `json:\"app_id\"`\n\t\t\tServices []ai.Service `json:\"services\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tresult, err := h.ai.WriteFiles(ctx, params.Services, app)\n\t\treturn reply(ctx, result, err)\n\tcase \"ai/parse-sql-schema\":\n\t\tvar params struct {\n\t\t\tAppID string `json:\"app_id\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tmd, err := h.GetMeta(params.AppID)\n\t\tif err != nil {\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\t\tfor _, db := range md.SqlDatabases {\n\t\t\t_, err := ai.ParseSQLSchema(app, *db.MigrationRelPath)\n\t\t\tif err != nil {\n\t\t\t\treturn reply(ctx, nil, err)\n\t\t\t}\n\t\t}\n\t\treturn reply(ctx, true, err)\n\tcase \"editors/open\":\n\t\ttelemetry.Send(\"editors.open\")\n\t\tvar params struct {\n\t\t\tAppID     string             `json:\"app_id\"`\n\t\t\tEditor    editors.EditorName `json:\"editor\"`\n\t\t\tFile      string             `json:\"file\"`\n\t\t\tStartLine int                `json:\"start_line,omitempty\"`\n\t\t\tStartCol  int                `json:\"start_col,omitempty\"`\n\t\t\tEndLine   int                `json:\"end_line,omitempty\"`\n\t\t\tEndCol    int                `json:\"end_col,omitempty\"`\n\t\t}\n\t\tif err := unmarshal(&params); err != nil {\n\t\t\tlog.Warn().Err(err).Msg(\"dash: could not parse open command\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\teditor, err := editors.Find(ctx, params.Editor)\n\t\tif err != nil {\n\t\t\tlog.Err(err).Str(\"editor\", string(params.Editor)).Msg(\"dash: could not find editor\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tapp, err := h.apps.FindLatestByPlatformOrLocalID(params.AppID)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, apps.ErrNotFound) {\n\t\t\t\treturn reply(ctx, nil, fmt.Errorf(\"app not found, try running encore run\"))\n\t\t\t}\n\t\t\tlog.Err(err).Str(\"app_id\", params.AppID).Msg(\"dash: could not find app\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\tif !filepath.IsLocal(params.File) {\n\t\t\tlog.Warn().Str(\"file\", params.File).Msg(\"dash: file was not local to the repo\")\n\t\t\treturn reply(ctx, nil, errors.New(\"file path must be local\"))\n\t\t}\n\t\tparams.File = filepath.Join(app.Root(), params.File)\n\n\t\tif err := editors.LaunchExternalEditor(params.File, params.StartLine, params.StartCol, editor); err != nil {\n\t\t\tlog.Err(err).Str(\"editor\", string(params.Editor)).Msg(\"dash: could not open file\")\n\t\t\treturn reply(ctx, nil, err)\n\t\t}\n\n\t\ttype openResp struct{}\n\t\treturn reply(ctx, openResp{}, nil)\n\t}\n\n\treturn jsonrpc2.MethodNotFound(ctx, reply, r)\n}\n\ntype sourceContextResponse struct {\n\tLines []string `json:\"lines\"`\n\tStart int      `json:\"start\"`\n}\n\nfunc (h *handler) listenNotify(ctx context.Context, ch <-chan *notification) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase r := <-ch:\n\t\t\tif err := h.rpc.Notify(ctx, r.Method, r.Params); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (s *Server) listenTraces() {\n\tfor sp := range s.traceCh {\n\t\t// Only marshal the trace if someone's listening.\n\t\ts.mu.Lock()\n\t\thasClients := len(s.clients) > 0\n\t\ts.mu.Unlock()\n\t\tif !hasClients {\n\t\t\tcontinue\n\t\t}\n\n\t\tdata, err := jsonext.ProtoEncoder.Marshal(sp.Span)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"dash: could not marshal trace\")\n\t\t\tcontinue\n\t\t}\n\n\t\ts.notify(&notification{\n\t\t\tMethod: \"trace/new\",\n\t\t\tParams: map[string]any{\n\t\t\t\t\"app_id\":     sp.AppID,\n\t\t\t\t\"test_trace\": sp.TestTrace,\n\t\t\t\t\"span\":       json.RawMessage(data),\n\t\t\t},\n\t\t})\n\t}\n}\n\nvar _ run.EventListener = (*Server)(nil)\n\n// OnStart notifies active websocket clients about the started run.\nfunc (s *Server) OnStart(r *run.Run) {\n\tstatus, err := buildAppStatus(r.App, r)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: could not build app status\")\n\t\treturn\n\t}\n\n\t// Open the browser if needed.\n\tbrowserMode := r.Params.Browser\n\tif browserMode == run.BrowserModeAlways || (browserMode == run.BrowserModeAuto && !s.hasClients()) {\n\t\tu := fmt.Sprintf(\"http://localhost:%d/%s\", s.dashPort, r.App.PlatformOrLocalID())\n\t\tbrowser.Open(u)\n\t}\n\n\ts.notify(&notification{\n\t\tMethod: \"process/start\",\n\t\tParams: status,\n\t})\n}\n\nfunc (s *Server) OnCompileStart(r *run.Run) {\n\tstatus, err := buildAppStatus(r.App, r)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: could not build app status\")\n\t\treturn\n\t}\n\n\tstatus.Compiling = true\n\n\ts.notify(&notification{\n\t\tMethod: \"process/compile-start\",\n\t\tParams: status,\n\t})\n}\n\n// OnReload notifies active websocket clients about the reloaded run.\nfunc (s *Server) OnReload(r *run.Run) {\n\tstatus, err := buildAppStatus(r.App, r)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: could not build app status\")\n\t\treturn\n\t}\n\n\ts.notify(&notification{\n\t\tMethod: \"process/reload\",\n\t\tParams: status,\n\t})\n}\n\n// OnStop notifies active websocket clients about the stopped run.\nfunc (s *Server) OnStop(r *run.Run) {\n\tstatus, err := buildAppStatus(r.App, nil)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: could not build app status\")\n\t\treturn\n\t}\n\n\ts.notify(&notification{\n\t\tMethod: \"process/stop\",\n\t\tParams: status,\n\t})\n}\n\n// OnStdout forwards the output to active websocket clients.\nfunc (s *Server) OnStdout(r *run.Run, out []byte) {\n\ts.onOutput(r, out)\n}\n\n// OnStderr forwards the output to active websocket clients.\nfunc (s *Server) OnStderr(r *run.Run, out []byte) {\n\ts.onOutput(r, out)\n}\n\nfunc (s *Server) OnError(r *run.Run, err *errlist.List) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\tstatus, statusErr := buildAppStatus(r.App, nil)\n\tif statusErr != nil {\n\t\tlog.Error().Err(statusErr).Msg(\"dash: could not build app status\")\n\t\treturn\n\t}\n\n\terr.MakeRelative(r.App.Root(), \"\")\n\n\tstatus.CompileError = err.Error()\n\n\ts.notify(&notification{\n\t\tMethod: \"process/compile-error\",\n\t\tParams: status,\n\t})\n}\n\nfunc (s *Server) onOutput(r *run.Run, out []byte) {\n\t// Copy to a new slice since we cannot retain it after the call ends, and notify is async.\n\tout2 := make([]byte, len(out))\n\tcopy(out2, out)\n\ts.notify(&notification{\n\t\tMethod: \"process/output\",\n\t\tParams: map[string]interface{}{\n\t\t\t\"appID\":  r.App.PlatformOrLocalID(),\n\t\t\t\"pid\":    r.ID,\n\t\t\t\"output\": out2,\n\t\t},\n\t})\n}\n\n// protoReplier is a jsonrpc2.Replier that wraps another replier and serializes\n// any protobuf message with protojson.\nfunc makeProtoReplier(rep jsonrpc2.Replier) jsonrpc2.Replier {\n\treturn func(ctx context.Context, result any, err error) error {\n\t\tif err != nil {\n\t\t\treturn rep(ctx, nil, err)\n\t\t}\n\t\tjsonData, err := jsonext.ProtoEncoder.Marshal(result)\n\t\treturn rep(ctx, json.RawMessage(jsonData), err)\n\t}\n}\n\n// appStatus is the the shared data structure to communicate app status to the client.\n//\n// It is mirrored in the frontend at src/lib/client/dev-dash-client.ts as `AppStatus`.\ntype appStatus struct {\n\tRunning      bool                  `json:\"running\"`\n\tTutorial     string                `json:\"tutorial,omitempty\"`\n\tAppID        string                `json:\"appID\"`\n\tPlatformID   string                `json:\"platformID,omitempty\"`\n\tAppRoot      string                `json:\"appRoot\"`\n\tPID          string                `json:\"pid,omitempty\"`\n\tMeta         json.RawMessage       `json:\"meta,omitempty\"`\n\tAddr         string                `json:\"addr,omitempty\"`\n\tAPIEncoding  *encoding.APIEncoding `json:\"apiEncoding,omitempty\"`\n\tCompiling    bool                  `json:\"compiling\"`\n\tCompileError string                `json:\"compileError,omitempty\"`\n}\n\ntype dbMigrationHistory struct {\n\tDatabaseName string        `json:\"databaseName\"`\n\tMigrations   []dbMigration `json:\"migrations\"`\n}\n\ntype dbMigration struct {\n\tFilename    string `json:\"filename\"`\n\tNumber      uint64 `json:\"number\"`\n\tDescription string `json:\"description\"`\n\tApplied     bool   `json:\"applied\"`\n}\n\nfunc buildAppStatus(app *apps.Instance, runInstance *run.Run) (s appStatus, err error) {\n\t// Now try and grab latest metadata for the app\n\tvar md *meta.Data\n\tif runInstance != nil {\n\t\tproc := runInstance.ProcGroup()\n\t\tif proc != nil {\n\t\t\tmd = proc.Meta\n\t\t}\n\t}\n\n\tif md == nil {\n\t\tmd, err = app.CachedMetadata()\n\t\tif err != nil {\n\t\t\treturn appStatus{}, err\n\t\t}\n\t}\n\n\t// Convert the metadata into a format we can send to the client\n\tmdStr := \"null\"\n\tvar apiEnc *encoding.APIEncoding\n\tif md != nil {\n\t\tm := &jsonpb.Marshaler{OrigName: true, EmitDefaults: true}\n\n\t\tmdStr, err = m.MarshalToString(md)\n\t\tif err != nil {\n\t\t\treturn appStatus{}, err\n\t\t}\n\n\t\tapiEnc = encoding.DescribeAPI(md)\n\t}\n\n\t// Build the response\n\tresp := appStatus{\n\t\tRunning:     false,\n\t\tTutorial:    app.Tutorial(),\n\t\tAppID:       app.PlatformOrLocalID(),\n\t\tPlatformID:  app.PlatformID(),\n\t\tMeta:        json.RawMessage(mdStr),\n\t\tAppRoot:     app.Root(),\n\t\tAPIEncoding: apiEnc,\n\t}\n\tif runInstance != nil {\n\t\tresp.Running = true\n\t\tresp.PID = runInstance.ID\n\t\tresp.Addr = runInstance.ListenAddr\n\t}\n\n\treturn resp, nil\n}\n\nfunc buildDbMigrationStatus(ctx context.Context, appMeta *meta.Data, cluster *sqldb.Cluster) []dbMigrationHistory {\n\tvar statuses []dbMigrationHistory\n\tfor _, dbMeta := range appMeta.SqlDatabases {\n\t\tdb, ok := cluster.GetDB(dbMeta.Name)\n\t\tif !ok {\n\t\t\t// Remote database migration status are not supported yet\n\t\t\tcontinue\n\t\t}\n\t\tappliedVersions, err := db.ListAppliedMigrations(ctx)\n\t\tif err != nil {\n\t\t\tlog.Error().Msgf(\"failed to list applied migrations for database %s: %v\", dbMeta.Name, err)\n\t\t\tcontinue\n\t\t}\n\t\tstatuses = append(statuses, buildMigrationHistory(dbMeta, appliedVersions))\n\t}\n\treturn statuses\n}\n\nfunc buildMigrationHistory(dbMeta *meta.SQLDatabase, appliedVersions map[uint64]bool) dbMigrationHistory {\n\thistory := dbMigrationHistory{\n\t\tDatabaseName: dbMeta.Name,\n\t\tMigrations:   []dbMigration{},\n\t}\n\t// Go over migrations from latest to earliest\n\tsortedMigrations := make([]*meta.DBMigration, len(dbMeta.Migrations))\n\tcopy(sortedMigrations, dbMeta.Migrations)\n\tslices.SortStableFunc(sortedMigrations, func(a, b *meta.DBMigration) int {\n\t\treturn int(b.Number - a.Number)\n\t})\n\timplicitlyApplied := false\n\tfor _, migration := range sortedMigrations {\n\t\tdirty, attempted := appliedVersions[migration.Number]\n\t\tapplied := attempted && !dirty\n\t\t// If the database doesn't allow non-sequential migrations,\n\t\t// then any migrations before the last applied will also have\n\t\t// been applied even if we don't see them in the database.\n\t\tif !dbMeta.AllowNonSequentialMigrations && applied {\n\t\t\timplicitlyApplied = true\n\t\t}\n\n\t\tstatus := dbMigration{\n\t\t\tFilename:    migration.Filename,\n\t\t\tNumber:      migration.Number,\n\t\t\tDescription: migration.Description,\n\t\t\tApplied:     applied || implicitlyApplied,\n\t\t}\n\t\thistory.Migrations = append(history.Migrations, status)\n\t}\n\treturn history\n}\n"
  },
  {
    "path": "cli/daemon/dash/dash_test.go",
    "content": "package dash\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nfunc TestBuildMigrationHistory(t *testing.T) {\n\ttests := []struct {\n\t\tname            string\n\t\tdbMeta          *meta.SQLDatabase\n\t\tappliedVersions map[uint64]bool\n\t\twant            dbMigrationHistory\n\t}{\n\t\t{\n\t\t\tname: \"sequential migrations all applied cleanly\",\n\t\t\tdbMeta: &meta.SQLDatabase{\n\t\t\t\tName: \"test-db\",\n\t\t\t\tMigrations: []*meta.DBMigration{\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\"},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\"},\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\"},\n\t\t\t\t},\n\t\t\t\tAllowNonSequentialMigrations: false,\n\t\t\t},\n\t\t\tappliedVersions: map[uint64]bool{\n\t\t\t\t1: false, // clean\n\t\t\t\t2: false, // clean\n\t\t\t\t3: false, // clean\n\t\t\t},\n\t\t\twant: dbMigrationHistory{\n\t\t\t\tDatabaseName: \"test-db\",\n\t\t\t\tMigrations: []dbMigration{\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\", Applied: true},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\", Applied: true},\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\", Applied: true},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sequential migrations with dirty migration\",\n\t\t\tdbMeta: &meta.SQLDatabase{\n\t\t\t\tName: \"test-db\",\n\t\t\t\tMigrations: []*meta.DBMigration{\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\"},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\"},\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\"},\n\t\t\t\t},\n\t\t\t\tAllowNonSequentialMigrations: false,\n\t\t\t},\n\t\t\tappliedVersions: map[uint64]bool{\n\t\t\t\t1: false, // clean\n\t\t\t\t2: true,  // dirty\n\t\t\t},\n\t\t\twant: dbMigrationHistory{\n\t\t\t\tDatabaseName: \"test-db\",\n\t\t\t\tMigrations: []dbMigration{\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\", Applied: false},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\", Applied: false},\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\", Applied: true},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sequential migrations partially applied\",\n\t\t\tdbMeta: &meta.SQLDatabase{\n\t\t\t\tName: \"test-db\",\n\t\t\t\tMigrations: []*meta.DBMigration{\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\"},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\"},\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\"},\n\t\t\t\t},\n\t\t\t\tAllowNonSequentialMigrations: false,\n\t\t\t},\n\t\t\tappliedVersions: map[uint64]bool{\n\t\t\t\t1: false, // clean\n\t\t\t\t2: false, // clean\n\t\t\t},\n\t\t\twant: dbMigrationHistory{\n\t\t\t\tDatabaseName: \"test-db\",\n\t\t\t\tMigrations: []dbMigration{\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\", Applied: false},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\", Applied: true},\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\", Applied: true},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"non-sequential migrations with mix of clean and dirty\",\n\t\t\tdbMeta: &meta.SQLDatabase{\n\t\t\t\tName: \"test-db\",\n\t\t\t\tMigrations: []*meta.DBMigration{\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\"},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\"},\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\"},\n\t\t\t\t},\n\t\t\t\tAllowNonSequentialMigrations: true,\n\t\t\t},\n\t\t\tappliedVersions: map[uint64]bool{\n\t\t\t\t1: false, // clean\n\t\t\t\t2: true,  // dirty\n\t\t\t\t3: false, // clean\n\t\t\t},\n\t\t\twant: dbMigrationHistory{\n\t\t\t\tDatabaseName: \"test-db\",\n\t\t\t\tMigrations: []dbMigration{\n\t\t\t\t\t{Number: 3, Filename: \"003.sql\", Description: \"third\", Applied: true},\n\t\t\t\t\t{Number: 2, Filename: \"002.sql\", Description: \"second\", Applied: false},\n\t\t\t\t\t{Number: 1, Filename: \"001.sql\", Description: \"first\", Applied: true},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty migrations list\",\n\t\t\tdbMeta: &meta.SQLDatabase{\n\t\t\t\tName:                         \"test-db\",\n\t\t\t\tMigrations:                   []*meta.DBMigration{},\n\t\t\t\tAllowNonSequentialMigrations: false,\n\t\t\t},\n\t\t\tappliedVersions: map[uint64]bool{},\n\t\t\twant: dbMigrationHistory{\n\t\t\t\tDatabaseName: \"test-db\",\n\t\t\t\tMigrations:   []dbMigration{},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := buildMigrationHistory(tt.dbMeta, tt.appliedVersions)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"buildMigrationHistory() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/dash/dashproxy/dashproxy.go",
    "content": "// Package dashproxy proxies requests to the dash server,\n// caching them locally for offline access.\npackage dashproxy\n\nimport (\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/peterbourgon/diskv\"\n\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/httpcache\"\n\t\"encr.dev/internal/httpcache/diskcache\"\n\t\"encr.dev/internal/version\"\n)\n\nfunc New(targetURL string) (*httputil.ReverseProxy, error) {\n\ttarget, err := url.Parse(targetURL)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"parse target url\")\n\t}\n\n\tvar transport http.RoundTripper = &versionAddingTransport{version: version.Version}\n\tif conf.CacheDevDash {\n\t\tcacheDir, err := os.UserCacheDir()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"get user cache dir\")\n\t\t}\n\n\t\tcache := diskcache.NewWithDiskv(diskv.New(diskv.Options{\n\t\t\tBasePath:     filepath.Join(cacheDir, \"encore\", \"dashcache\"),\n\t\t\tCacheSizeMax: 1024 * 1024 * 1024, // 1GiB\n\t\t\tCompression:  diskv.NewGzipCompression(),\n\t\t}))\n\n\t\t// Wrap the transport with a caching transport.\n\t\tcachingTransport := httpcache.NewTransport(cache)\n\t\tcachingTransport.Transport = transport\n\t\ttransport = cachingTransport\n\t}\n\n\tproxy := &httputil.ReverseProxy{\n\t\tTransport: transport,\n\t\tRewrite: func(r *httputil.ProxyRequest) {\n\t\t\tr.SetURL(target)\n\n\t\t\t// Configure cache headers so the cache behaves the way we want it to.\n\t\t\tr.Out.Header.Del(\"Cookie\")\n\t\t\tr.Out.Header.Set(\"Cache-Control\", \"stale-if-error\")\n\t\t\tr.Out.Header.Del(\"Vary\")\n\t\t},\n\t\tModifyResponse: func(resp *http.Response) error {\n\t\t\tif resp.StatusCode < 300 {\n\t\t\t\tresp.Header.Del(\"Vary\")\n\t\t\t\tresp.Header.Set(\"Cache-Control\", \"max-age=60,stale-if-error=86400\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\treturn proxy, nil\n}\n\ntype versionAddingTransport struct {\n\tversion string\n}\n\nfunc (t *versionAddingTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tif t.version != \"\" {\n\t\tvals := req.URL.Query()\n\t\tvals.Set(\"cli_version\", t.version)\n\t\treq.URL.RawQuery = vals.Encode()\n\t}\n\treturn http.DefaultTransport.RoundTrip(req)\n}\n"
  },
  {
    "path": "cli/daemon/dash/dbbrowser.go",
    "content": "package dash\n\nimport (\n\t\"context\"\n\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/pkg/fns\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgtype\"\n)\n\n// QueryRequest represents the request body for the /query endpoint\ntype QueryRequest struct {\n\tQuery     string `json:\"query\"`\n\tParams    []any  `json:\"params\"`\n\tArrayMode bool   `json:\"arrayMode\"`\n\tDbID      string `json:\"dbId\"`\n\tAppID     string `json:\"appId\"`\n}\n\n// TransactionRequest represents the request body for the /transaction endpoint\ntype TransactionRequest struct {\n\tQueries []struct {\n\t\tSQL    string `json:\"sql\"`\n\t\tParams []any  `json:\"params\"`\n\t} `json:\"queries\"`\n\tDbID  string `json:\"dbId\"`\n\tAppID string `json:\"appId\"`\n}\n\nfunc (h *handler) Query(ctx context.Context, req QueryRequest) ([]any, error) {\n\n\tpgConn, err := h.browserConn(ctx, req.AppID, req.DbID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer fns.CloseIgnoreCtx(ctx, pgConn.Close)\n\n\trows, err := pgConn.Query(context.Background(), req.Query, req.Params...)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tresults := []any{}\n\tif req.ArrayMode {\n\t\t// Return results as arrays\n\t\tfor rows.Next() {\n\t\t\tvalues, err := rows.Values()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tresults = append(results, values)\n\t\t}\n\t} else {\n\t\t// Return results as objects\n\t\tfieldDescriptions := rows.FieldDescriptions()\n\t\tfor rows.Next() {\n\t\t\tvalues, err := rows.Values()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\trow := make(map[string]any)\n\t\t\tfor i, value := range values {\n\t\t\t\trow[fieldDescriptions[i].Name] = value\n\t\t\t}\n\t\t\tresults = append(results, row)\n\t\t}\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results, nil\n}\n\n// handleTransaction handles the /transaction endpoint\nfunc (h *handler) Transaction(ctx context.Context, req TransactionRequest) ([]any, error) {\n\t// Start a transaction\n\tconn, err := h.browserConn(ctx, req.AppID, req.DbID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer fns.CloseIgnoreCtx(ctx, conn.Close)\n\n\ttx, err := conn.Begin(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer tx.Rollback(context.Background())\n\n\tresults := []any{}\n\tfor _, query := range req.Queries {\n\t\trows, err := tx.Query(context.Background(), query.SQL, query.Params...)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar queryResults []map[string]any\n\t\tfieldDescriptions := rows.FieldDescriptions()\n\t\tfor rows.Next() {\n\t\t\tvalues, err := rows.Values()\n\t\t\tif err != nil {\n\t\t\t\trows.Close()\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\trow := make(map[string]any)\n\t\t\tfor i, value := range values {\n\t\t\t\trow[fieldDescriptions[i].Name] = value\n\t\t\t}\n\t\t\tqueryResults = append(queryResults, row)\n\t\t}\n\t\trows.Close()\n\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresults = append(results, queryResults)\n\t}\n\n\t// Commit the transaction\n\tif err := tx.Commit(context.Background()); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn results, nil\n}\n\nfunc (s *handler) browserConn(ctx context.Context, appID string, dbID string) (*pgx.Conn, error) {\n\t// Find the latest app by platform ID or local ID.\n\tapp, err := s.apps.FindLatestByPlatformOrLocalID(appID)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to find latest app\")\n\t}\n\n\tnamespace, err := s.GetNamespace(ctx, appID)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get namespace\")\n\t}\n\n\tclusterType := sqldb.Run\n\tcluster := s.run.ClusterMgr.Create(ctx, &sqldb.CreateParams{\n\t\tClusterID: sqldb.GetClusterID(app, clusterType, namespace),\n\t\tMemfs:     false,\n\t})\n\tappMeta, err := s.GetMeta(appID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err = cluster.Start(ctx, nil); err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to start database cluster\")\n\t}\n\tdb, ok := cluster.GetDB(dbID)\n\tif !ok {\n\t\tif err := cluster.Setup(ctx, app.Root(), appMeta); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to setup database cluster\")\n\t\t}\n\t\tdb, ok = cluster.GetDB(dbID)\n\t\tif !ok {\n\t\t\treturn nil, errors.Newf(\"failed to get database %s\", dbID)\n\t\t}\n\t}\n\n\tinfo, err := db.Cluster.Info(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\turi := info.ConnURI(db.ApplicationCloudName(), info.Config.Superuser)\n\tconn, err := pgx.Connect(ctx, uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconn.TypeMap().RegisterType(&pgtype.Type{\n\t\tName:  \"char\",\n\t\tOID:   18,\n\t\tCodec: pgtype.TextCodec{},\n\t})\n\tconn.TypeMap().RegisterType(&pgtype.Type{\n\t\tName:  \"uuid\",\n\t\tOID:   2950,\n\t\tCodec: pgtype.TextCodec{},\n\t})\n\treturn conn, nil\n}\n"
  },
  {
    "path": "cli/daemon/dash/server.go",
    "content": "package dash\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"sync\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/dash/ai\"\n\t\"encr.dev/cli/daemon/dash/apiproxy\"\n\t\"encr.dev/cli/daemon/dash/dashproxy\"\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/internal/jsonrpc2\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/pkg/fns\"\n)\n\nvar upgrader = websocket.Upgrader{\n\tCheckOrigin: func(*http.Request) bool { return true },\n}\n\n// NewServer starts a new server and returns it.\nfunc NewServer(appsMgr *apps.Manager, runMgr *run.Manager, nsMgr *namespace.Manager, tr trace2.Store, dashPort int) *Server {\n\tproxy, err := dashproxy.New(conf.DevDashURL)\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"could not create dash proxy\")\n\t}\n\n\tapiProxy, err := apiproxy.New(conf.APIBaseURL + \"/graphql\")\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"could not create graphql proxy\")\n\t}\n\n\taiMgr := ai.NewAIManager()\n\n\ts := &Server{\n\t\tproxy:    proxy,\n\t\tapiProxy: apiProxy,\n\t\tapps:     appsMgr,\n\t\trun:      runMgr,\n\t\tns:       nsMgr,\n\t\ttr:       tr,\n\t\tdashPort: dashPort,\n\t\ttraceCh:  make(chan trace2.NewSpanEvent, 10),\n\t\tclients:  make(map[chan<- *notification]struct{}),\n\t\tai:       aiMgr,\n\t}\n\n\trunMgr.AddListener(s)\n\ttr.Listen(s.traceCh)\n\tgo s.listenTraces()\n\treturn s\n}\n\n// Server is the http.Handler for serving the developer dashboard.\ntype Server struct {\n\tproxy    *httputil.ReverseProxy\n\tapiProxy *httputil.ReverseProxy\n\tapps     *apps.Manager\n\trun      *run.Manager\n\tns       *namespace.Manager\n\ttr       trace2.Store\n\tdashPort int\n\ttraceCh  chan trace2.NewSpanEvent\n\tai       *ai.Manager\n\n\tmu      sync.Mutex\n\tclients map[chan<- *notification]struct{}\n}\n\nfunc (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tswitch req.URL.Path {\n\tcase \"/__encore\":\n\t\ts.WebSocket(w, req)\n\tcase \"/__graphql\":\n\t\ts.apiProxy.ServeHTTP(w, req)\n\tdefault:\n\t\ts.proxy.ServeHTTP(w, req)\n\t}\n}\n\n// WebSocket serves the jsonrpc2 API over WebSocket.\nfunc (s *Server) WebSocket(w http.ResponseWriter, req *http.Request) {\n\tc, err := upgrader.Upgrade(w, req, nil)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: could not upgrade websocket\")\n\t\treturn\n\t}\n\tdefer fns.CloseIgnore(c)\n\tlog.Info().Msg(\"dash: websocket connection established\")\n\n\tstream := &wsStream{c: c}\n\tconn := jsonrpc2.NewConn(stream)\n\thandler := &handler{rpc: conn, apps: s.apps, run: s.run, ns: s.ns, tr: s.tr, ai: s.ai}\n\tconn.Go(req.Context(), handler.Handle)\n\n\tch := make(chan *notification, 20)\n\ts.addClient(ch)\n\tdefer s.removeClient(ch)\n\n\t// nosemgrep: tools.semgrep-rules.semgrep-go.http-request-go-context\n\tgo handler.listenNotify(req.Context(), ch)\n\n\t<-conn.Done()\n\tif err := conn.Err(); err != nil {\n\t\tif ce, ok := err.(*websocket.CloseError); ok && ce.Code == websocket.CloseNormalClosure {\n\t\t\tlog.Info().Msg(\"dash: websocket closed\")\n\t\t} else {\n\t\t\tlog.Info().Err(err).Msg(\"dash: websocket closed with error\")\n\t\t}\n\t}\n}\n\nfunc (s *Server) addClient(ch chan *notification) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.clients[ch] = struct{}{}\n}\n\nfunc (s *Server) removeClient(ch chan *notification) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tdelete(s.clients, ch)\n}\n\n// hasClients reports whether there are any active clients.\nfunc (s *Server) hasClients() bool {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn len(s.clients) > 0\n}\n\ntype notification struct {\n\tMethod string\n\tParams interface{}\n}\n\n// notify notifies any active clients.\nfunc (s *Server) notify(n *notification) {\n\tvar clients []chan<- *notification\n\ts.mu.Lock()\n\tfor c := range s.clients {\n\t\tclients = append(clients, c)\n\t}\n\ts.mu.Unlock()\n\n\tfor _, c := range clients {\n\t\tselect {\n\t\tcase c <- n:\n\t\tdefault:\n\t\t}\n\t}\n}\n\n// wsStream implements jsonrpc2.Stream over a websocket.\ntype wsStream struct {\n\twriteMu sync.Mutex\n\tc       *websocket.Conn\n}\n\nfunc (s *wsStream) Close() error {\n\ts.writeMu.Lock()\n\tdefer s.writeMu.Unlock()\n\treturn s.c.Close()\n}\n\nfunc (s *wsStream) Read(context.Context) (jsonrpc2.Message, int64, error) {\n\ttyp, data, err := s.c.ReadMessage()\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tif typ != websocket.TextMessage {\n\t\treturn nil, 0, fmt.Errorf(\"webedit.wsStream: got non-text message type %v\", typ)\n\t}\n\tmsg, err := jsonrpc2.DecodeMessage(data)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\treturn msg, int64(len(data)), nil\n}\n\nfunc (s *wsStream) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) {\n\ts.writeMu.Lock()\n\tdefer s.writeMu.Unlock()\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\terr = s.c.WriteMessage(websocket.TextMessage, data)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn int64(len(data)), nil\n}\n"
  },
  {
    "path": "cli/daemon/db.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/pgproxy\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc toRoleType(role daemonpb.DBRole) sqldb.RoleType {\n\tswitch role {\n\tcase daemonpb.DBRole_DB_ROLE_READ:\n\t\treturn sqldb.RoleRead\n\tcase daemonpb.DBRole_DB_ROLE_WRITE:\n\t\treturn sqldb.RoleWrite\n\tcase daemonpb.DBRole_DB_ROLE_ADMIN:\n\t\treturn sqldb.RoleAdmin\n\tcase daemonpb.DBRole_DB_ROLE_SUPERUSER:\n\t\treturn sqldb.RoleSuperuser\n\tdefault:\n\t\treturn sqldb.RoleRead\n\t}\n\n}\n\n// DBConnect starts the database and returns the DSN for connecting to it.\nfunc (s *Server) DBConnect(ctx context.Context, req *daemonpb.DBConnectRequest) (*daemonpb.DBConnectResponse, error) {\n\tif req.EnvName == \"local\" {\n\t\treturn s.dbConnectLocal(ctx, req)\n\t}\n\n\tappID, err := appfile.Slug(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if appID == \"\" {\n\t\treturn nil, errNotLinked\n\t}\n\tport, passwd, err := sqldb.OneshotProxy(appID, req.EnvName, toRoleType(req.Role))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdsn := fmt.Sprintf(\"postgresql://encore:%s@127.0.0.1:%d/%s?sslmode=disable\", passwd, port, req.DbName)\n\treturn &daemonpb.DBConnectResponse{Dsn: dsn}, nil\n}\n\nfunc (s *Server) dbConnectLocal(ctx context.Context, req *daemonpb.DBConnectRequest) (*daemonpb.DBConnectResponse, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\texpSet, err := app.Experiments(nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse the app to figure out what infrastructure is needed.\n\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      builder.DefaultBuildInfo(),\n\t\tApp:        app,\n\t\tWorkingDir: \".\",\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       builder.DefaultBuildInfo(),\n\t\tApp:         app,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// The Encore IDE plugins will request a connection to the database \"_any_\"\n\t// as they will be unaware of any database names ahead of time.\n\t//\n\t// We will use the first database name in the app's schema on the returned connection string\n\tif req.DbName == \"_any_\" {\n\t\treq.DbName = \"\"\n\t\tif len(parse.Meta.SqlDatabases) > 0 {\n\t\t\treq.DbName = parse.Meta.SqlDatabases[0].Name\n\t\t}\n\n\t\t// If no database has been found, return an error\n\t\tif req.DbName == \"\" {\n\t\t\treturn nil, errDatabaseNotFound\n\t\t}\n\t} else {\n\t\t// Otherwise we need to check the requested service exists\n\t\tdatabaseExists := false\n\t\tfor _, s := range parse.Meta.SqlDatabases {\n\t\t\tif s.Name == req.DbName {\n\t\t\t\tdatabaseExists = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !databaseExists {\n\t\t\treturn nil, errDatabaseNotFound\n\t\t}\n\t}\n\n\tclusterNS, err := s.namespaceOrActive(ctx, app, req.Namespace)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar passwd string\n\tclusterType := getClusterType(req)\n\tswitch clusterType {\n\tcase sqldb.Run:\n\t\t// If the user didn't specify a namespace, leave it out from the password\n\t\t// so it uses the active namespace.\n\t\tif req.Namespace != nil {\n\t\t\tpasswd = \"local-\" + string(clusterNS.ID)\n\t\t} else {\n\t\t\tpasswd = \"local\"\n\t\t}\n\tdefault:\n\t\tpasswd = fmt.Sprintf(\"%s-%s\", clusterType, clusterNS.ID)\n\t}\n\n\tclusterID := sqldb.GetClusterID(app, clusterType, clusterNS)\n\tlog := log.With().Interface(\"cluster\", clusterID).Logger()\n\tlog.Info().Msg(\"setting up database cluster\")\n\tcluster := s.cm.Create(ctx, &sqldb.CreateParams{\n\t\tClusterID: clusterID,\n\t\tMemfs:     clusterType.Memfs(),\n\t})\n\tif cluster.IsExternalDB(req.DbName) {\n\t\treturn nil, errors.New(\"connecting to an external database is disabled\")\n\t}\n\t// TODO would be nice to stream this to the CLI\n\tif _, err := cluster.Start(ctx, nil); err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to start db cluster\")\n\t\treturn nil, err\n\t} else if err := cluster.Setup(ctx, req.AppRoot, parse.Meta); err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to create databases\")\n\t\treturn nil, err\n\t}\n\tlog.Info().Msg(\"created database cluster\")\n\n\tdsn := fmt.Sprintf(\"postgresql://%s:%s@127.0.0.1:%d/%s?sslmode=disable\",\n\t\tapp.PlatformOrLocalID(), passwd, s.mgr.DBProxyPort, req.DbName)\n\treturn &daemonpb.DBConnectResponse{Dsn: dsn}, nil\n}\n\n// DBProxy starts a local database proxy for connecting to remote databases\n// on the encore.dev platform.\nfunc (s *Server) DBProxy(params *daemonpb.DBProxyRequest, stream daemonpb.Daemon_DBProxyServer) (err error) {\n\tctx := stream.Context()\n\n\tappID, err := appfile.Slug(params.AppRoot)\n\tif err != nil {\n\t\treturn err\n\t} else if appID == \"\" && params.EnvName != \"local\" {\n\t\treturn errNotLinked\n\t}\n\n\tln, err := (&net.ListenConfig{}).Listen(ctx, \"tcp\", \"127.0.0.1:\"+strconv.Itoa(int(params.Port)))\n\tif err != nil {\n\t\treturn status.Error(codes.FailedPrecondition, err.Error())\n\t}\n\tport := ln.Addr().(*net.TCPAddr).Port\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = ln.Close()\n\t}()\n\n\tlog.Info().Msgf(\"dbproxy: listening on localhost:%d\", port)\n\tdefer log.Info().Msg(\"dbproxy: proxy closed\")\n\terr = stream.Send(&daemonpb.CommandMessage{Msg: &daemonpb.CommandMessage_Output{\n\t\tOutput: &daemonpb.CommandOutput{\n\t\t\tStdout: []byte(fmt.Sprintf(\"dbproxy: listening for TCP connections on localhost:%d\\n\", port)),\n\t\t},\n\t}})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar runProxy func() error\n\tif params.EnvName == \"local\" {\n\t\tapp, err := s.apps.Track(params.AppRoot)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\texpSet, err := app.Experiments(nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Parse the app to figure out what infrastructure is needed.\n\t\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\t\tdefer fns.CloseIgnore(bld)\n\t\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\t\tBuild:      builder.DefaultBuildInfo(),\n\t\t\tApp:        app,\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\t\tBuild:       builder.DefaultBuildInfo(),\n\t\t\tApp:         app,\n\t\t\tExperiments: expSet,\n\t\t\tWorkingDir:  \".\",\n\t\t\tParseTests:  false,\n\t\t\tPrepare:     prepareResult,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tclusterType := getClusterType(params)\n\n\t\tclusterNS, err := s.namespaceOrActive(stream.Context(), app, params.Namespace)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tclusterID := sqldb.GetClusterID(app, clusterType, clusterNS)\n\t\tcluster := s.cm.Create(ctx, &sqldb.CreateParams{\n\t\t\tClusterID: clusterID,\n\t\t\tMemfs:     clusterType.Memfs(),\n\t\t})\n\t\tif _, err := cluster.Start(ctx, nil); err != nil {\n\t\t\treturn err\n\t\t} else if err := cluster.Setup(ctx, params.AppRoot, parse.Meta); err != nil {\n\t\t\treturn err\n\t\t}\n\t\trunProxy = func() error {\n\t\t\treturn serveProxy(ctx, ln, func(ctx context.Context, client net.Conn) {\n\t\t\t\t_ = s.cm.PreauthProxyConn(client, clusterID)\n\t\t\t})\n\t\t}\n\t} else {\n\t\tproxy := &pgproxy.SingleBackendProxy{\n\t\t\tLog:             log.Logger,\n\t\t\tRequirePassword: false,\n\t\t\tFrontendTLS:     nil,\n\t\t\tDialBackend: func(ctx context.Context, startup *pgproxy.StartupData) (pgproxy.LogicalConn, error) {\n\t\t\t\tstartupData, err := startup.Raw.Encode(nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tws, err := platform.DBConnect(ctx, appID, params.EnvName, startup.Database, toRoleType(params.Role).String(), startupData)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn &sqldb.WebsocketLogicalConn{Conn: ws}, nil\n\t\t\t},\n\t\t}\n\n\t\trunProxy = func() error {\n\t\t\treturn proxy.Serve(ctx, ln)\n\t\t}\n\t}\n\n\tmsgs := make(chan string, 10)\n\tdefer close(msgs)\n\tgo func() {\n\t\tfor msg := range msgs {\n\t\t\t_ = stream.Send(&daemonpb.CommandMessage{Msg: &daemonpb.CommandMessage_Output{\n\t\t\t\tOutput: &daemonpb.CommandOutput{\n\t\t\t\t\tStdout: []byte(msg),\n\t\t\t\t},\n\t\t\t}})\n\t\t}\n\t}()\n\n\treturn runProxy()\n}\n\n// DBReset resets the given databases, recreating them from scratch.\nfunc (s *Server) DBReset(req *daemonpb.DBResetRequest, stream daemonpb.Daemon_DBResetServer) error {\n\tsendErr := func(err error) {\n\t\t_ = stream.Send(&daemonpb.CommandMessage{\n\t\t\tMsg: &daemonpb.CommandMessage_Output{Output: &daemonpb.CommandOutput{\n\t\t\t\tStderr: []byte(err.Error() + \"\\n\"),\n\t\t\t}},\n\t\t})\n\t\t_ = stream.Send(&daemonpb.CommandMessage{\n\t\t\tMsg: &daemonpb.CommandMessage_Exit{Exit: &daemonpb.CommandExit{\n\t\t\t\tCode: 1,\n\t\t\t}},\n\t\t})\n\t}\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\texpSet, err := app.Experiments(nil)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\t// Parse the app to figure out what infrastructure is needed.\n\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tprepareResult, err := bld.Prepare(stream.Context(), builder.PrepareParams{\n\t\tBuild:      builder.DefaultBuildInfo(),\n\t\tApp:        app,\n\t\tWorkingDir: \".\",\n\t})\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\tparse, err := bld.Parse(stream.Context(), builder.ParseParams{\n\t\tBuild:       builder.DefaultBuildInfo(),\n\t\tApp:         app,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tclusterNS, err := s.namespaceOrActive(stream.Context(), app, req.Namespace)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tclusterType := getClusterType(req)\n\tclusterID := sqldb.GetClusterID(app, clusterType, clusterNS)\n\tcluster, ok := s.cm.Get(clusterID)\n\tif !ok {\n\t\tcluster = s.cm.Create(stream.Context(), &sqldb.CreateParams{\n\t\t\tClusterID: clusterID,\n\t\t\tMemfs:     clusterType.Memfs(),\n\t\t})\n\t}\n\n\tif _, err := cluster.Start(stream.Context(), nil); err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\terr = cluster.Recreate(stream.Context(), req.AppRoot, req.DatabaseNames, parse.Meta)\n\tif err != nil {\n\t\tsendErr(err)\n\t}\n\treturn nil\n}\n\nfunc serveProxy(ctx context.Context, ln net.Listener, handler func(context.Context, net.Conn)) error {\n\tvar tempDelay time.Duration // how long to sleep on accept failure\n\tfor {\n\t\tfrontend, e := ln.Accept()\n\t\tif e != nil {\n\t\t\tif ne, ok := e.(net.Error); ok && ne.Temporary() {\n\t\t\t\tif tempDelay == 0 {\n\t\t\t\t\ttempDelay = 5 * time.Millisecond\n\t\t\t\t} else {\n\t\t\t\t\ttempDelay *= 2\n\t\t\t\t}\n\t\t\t\tif max := 1 * time.Second; tempDelay > max {\n\t\t\t\t\ttempDelay = max\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"dbproxy: accept error: %v; retrying in %v\", e, tempDelay)\n\t\t\t\ttime.Sleep(tempDelay)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"dbproxy: could not accept: %w\", e)\n\t\t}\n\t\ttempDelay = 0\n\t\tgo handler(ctx, frontend)\n\t}\n}\n\nfunc getClusterType(req interface{ GetClusterType() daemonpb.DBClusterType }) sqldb.ClusterType {\n\tswitch req.GetClusterType() {\n\tcase daemonpb.DBClusterType_DB_CLUSTER_TYPE_RUN:\n\t\treturn sqldb.Run\n\tcase daemonpb.DBClusterType_DB_CLUSTER_TYPE_TEST:\n\t\treturn sqldb.Test\n\tcase daemonpb.DBClusterType_DB_CLUSTER_TYPE_SHADOW:\n\t\treturn sqldb.Shadow\n\tdefault:\n\t\treturn sqldb.Run\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/debug.go",
    "content": "package daemon\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"runtime\"\n\n\t\"github.com/golang/protobuf/jsonpb\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/vcs\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc (s *Server) DumpMeta(ctx context.Context, req *daemonpb.DumpMetaRequest) (*daemonpb.DumpMetaResponse, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t}\n\n\texpSet, err := app.Experiments(req.Environ)\n\tif err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t}\n\n\t// TODO: We should check that all secret keys are defined as well.\n\n\tvcsRevision := vcs.GetRevision(app.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            req.Environ,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        app,\n\t\tWorkingDir: req.WorkingDir,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         app,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  req.WorkingDir,\n\t\tParseTests:  req.ParseTests,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t}\n\n\tvar out []byte\n\tswitch req.Format {\n\tcase daemonpb.DumpMetaRequest_FORMAT_PROTO:\n\t\tout, err = proto.Marshal(parse.Meta)\n\t\tif err != nil {\n\t\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t\t}\n\tcase daemonpb.DumpMetaRequest_FORMAT_JSON:\n\t\tvar buf bytes.Buffer\n\t\tm := &jsonpb.Marshaler{OrigName: true, EmitDefaults: true, Indent: \"  \"}\n\t\tif err := m.Marshal(&buf, parse.Meta); err != nil {\n\t\t\treturn nil, status.Error(codes.InvalidArgument, err.Error())\n\t\t}\n\t\tout = buf.Bytes()\n\tdefault:\n\t\treturn nil, status.Error(codes.InvalidArgument, \"invalid format\")\n\t}\n\n\treturn &daemonpb.DumpMetaResponse{Meta: out}, nil\n}\n"
  },
  {
    "path": "cli/daemon/engine/runtime.go",
    "content": "package engine\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\ttracemodel \"encore.dev/appruntime/exported/trace2\"\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/cli/daemon/run\"\n)\n\ntype server struct {\n\trunMgr *run.Manager\n\trec    *trace2.Recorder\n}\n\nfunc NewServer(runMgr *run.Manager, rec *trace2.Recorder) http.Handler {\n\ts := &server{runMgr: runMgr, rec: rec}\n\treturn s\n}\n\n// ServeHTTP implements http.Handler.\nfunc (s *server) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tswitch req.URL.Path {\n\tcase \"/trace\":\n\t\ts.RecordTrace(w, req)\n\tdefault:\n\t\thttp.Error(w, \"Not Found\", http.StatusNotFound)\n\t}\n}\n\nfunc (s *server) RecordTrace(w http.ResponseWriter, req *http.Request) {\n\tdata, err := s.parseTraceData(req)\n\tif err != nil {\n\t\thttp.Error(w, \"unable to parse trace header: \"+err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = s.rec.RecordTrace(data)\n\tif err != nil {\n\t\thttp.Error(w, \"unable to record trace: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc (s *server) parseTraceData(req *http.Request) (d trace2.RecordData, err error) {\n\t// Parse trace version\n\ttraceVersion := req.Header.Get(\"X-Encore-Trace-Version\")\n\tversion, err := strconv.Atoi(traceVersion)\n\tif err != nil || version <= 0 {\n\t\treturn d, fmt.Errorf(\"bad trace protocol version %q\", traceVersion)\n\t}\n\td.TraceVersion = tracemodel.Version(version)\n\n\tpid := req.Header.Get(\"X-Encore-Env-ID\")\n\tif pid == \"test\" {\n\t\tappID := req.Header.Get(\"X-Encore-App-ID\")\n\t\tif appID == \"\" {\n\t\t\treturn d, errors.New(\"missing X-Encore-App-ID header\")\n\t\t}\n\t\td.Meta = &trace2.Meta{AppID: appID}\n\t} else {\n\t\tif pid == \"\" {\n\t\t\treturn d, errors.New(\"missing X-Encore-Env-ID header\")\n\t\t}\n\t\tproc := s.runMgr.FindProc(pid)\n\t\tif proc == nil {\n\t\t\treturn d, errors.Newf(\"process %q is not running\", pid)\n\t\t}\n\t\td.Meta = &trace2.Meta{AppID: proc.Run.App.PlatformOrLocalID()}\n\t}\n\n\t// Parse time anchor\n\ttimeAnchor := req.Header.Get(\"X-Encore-Trace-TimeAnchor\")\n\tif timeAnchor == \"\" {\n\t\treturn d, errors.New(\"missing X-Encore-Trace-TimeAnchor header\")\n\t}\n\n\tif err := d.Anchor.UnmarshalText([]byte(timeAnchor)); err != nil {\n\t\treturn d, errors.Wrap(err, \"unable to parse X-Encore-Trace-TimeAnchor header\")\n\t}\n\n\td.Buf = bufio.NewReader(req.Body)\n\treturn d, nil\n}\n"
  },
  {
    "path": "cli/daemon/engine/trace/parse_test.go",
    "content": "package trace\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace\"\n\t\"encore.dev/beta/errs\"\n)\n\ntype parseTest[T any] struct {\n\tname string\n\tval  T\n\temit func(l *trace.Log, val T)\n}\n\nfunc (pt parseTest[T]) Name() string {\n\treturn pt.name\n}\n\nfunc (pt parseTest[T]) Data() []byte {\n\tlog := &trace.Log{}\n\tpt.emit(log, pt.val)\n\treturn log.GetAndClear()\n}\n\nfunc TestParse(t *testing.T) {\n\ttype reqResp struct {\n\t\tReq  *model.Request\n\t\tResp *model.Response\n\t}\n\ttests := []interface {\n\t\tName() string\n\t\tData() []byte\n\t}{\n\t\tparseTest[*model.Request]{\n\t\t\tname: \"basic\",\n\t\t\tval: &model.Request{\n\t\t\t\tType:         model.RPCCall,\n\t\t\t\tSpanID:       model.SpanID{0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\tStart:        time.Now(),\n\t\t\t\tTraced:       true,\n\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\tService:  \"service\",\n\t\t\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\t\t\tRaw:      false,\n\t\t\t\t\t},\n\t\t\t\t\tHTTPMethod:     \"POST\",\n\t\t\t\t\tPath:           \"/path/hello\",\n\t\t\t\t\tPathParams:     model.PathParams{{Name: \"one\", Value: \"hello\"}},\n\t\t\t\t\tUserID:         \"\",\n\t\t\t\t\tAuthData:       nil,\n\t\t\t\t\tNonRawPayload:  []byte(`{\"Body\":\"foo\"}`),\n\t\t\t\t\tRequestHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\temit: func(l *trace.Log, val *model.Request) { l.BeginRequest(val, 0) },\n\t\t},\n\t\tparseTest[reqResp]{\n\t\t\tname: \"raw_err\",\n\t\t\tval: reqResp{\n\t\t\t\tReq: &model.Request{\n\t\t\t\t\tType:         model.RPCCall,\n\t\t\t\t\tSpanID:       model.SpanID{0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\t\tStart:        time.Now(),\n\t\t\t\t\tTraced:       true,\n\t\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\t\tService:  \"service\",\n\t\t\t\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\t\t\t\tRaw:      true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHTTPMethod:     \"POST\",\n\t\t\t\t\t\tPath:           \"/path/hello\",\n\t\t\t\t\t\tPathParams:     model.PathParams{{Name: \"one\", Value: \"hello\"}},\n\t\t\t\t\t\tRequestHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tResp: &model.Response{\n\t\t\t\t\tHTTPStatus:         500,\n\t\t\t\t\tErr:                &errs.Error{Code: errs.Unavailable},\n\t\t\t\t\tRawRequestPayload:  []byte(\"foo\"),\n\t\t\t\t\tRawResponsePayload: []byte(\"bar\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\temit: func(l *trace.Log, val reqResp) {\n\t\t\t\tl.BeginRequest(val.Req, 0)\n\t\t\t\tl.FinishRequest(val.Req, val.Resp)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name(), func(t *testing.T) {\n\t\t\tdata := tt.Data()\n\t\t\tlogger := zerolog.New(zerolog.NewTestWriter(t))\n\t\t\t_, err := Parse(&logger, ID{}, data, trace.CurrentVersion, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse trace: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/engine/trace/trace.go",
    "content": "package trace\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encore.dev/appruntime/exported/trace\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/internal/sym\"\n\t\"encr.dev/pkg/eerror\"\n\ttracepb \"encr.dev/proto/encore/engine/trace\"\n\tmetapb \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype ID [16]byte\n\ntype TraceMeta struct {\n\tID    ID\n\tReqs  []*tracepb.Request\n\tApp   *apps.Instance\n\tEnvID string\n\tDate  time.Time\n\tMeta  *metapb.Data\n}\n\n// A Store stores traces received from running applications.\ntype Store struct {\n\ttrmu             sync.Mutex\n\ttraces           map[string][]*TraceMeta\n\trequestIDMapping map[string]*tracepb.Request // Trace ID -> Request\n\n\tlnmu sync.Mutex\n\tln   map[chan<- *TraceMeta]struct{}\n}\n\nfunc NewStore() *Store {\n\treturn &Store{\n\t\ttraces:           make(map[string][]*TraceMeta),\n\t\trequestIDMapping: make(map[string]*tracepb.Request),\n\t\tln:               make(map[chan<- *TraceMeta]struct{}),\n\t}\n}\n\nfunc (st *Store) Listen(ch chan<- *TraceMeta) {\n\tst.lnmu.Lock()\n\tst.ln[ch] = struct{}{}\n\tst.lnmu.Unlock()\n}\n\nfunc (st *Store) Store(ctx context.Context, tr *TraceMeta) error {\n\tappID := tr.App.PlatformOrLocalID()\n\tst.trmu.Lock()\n\tst.traces[appID] = append(st.traces[appID], tr)\n\n\tconst limit = 100\n\t// Remove earlier traces if we exceed the limit.\n\tif n := len(st.traces[appID]); n > limit {\n\t\tst.traces[appID] = st.traces[appID][n-limit:]\n\t}\n\n\tfor _, req := range tr.Reqs {\n\t\tst.requestIDMapping[req.TraceId.String()] = req\n\t}\n\n\tst.trmu.Unlock()\n\n\tst.lnmu.Lock()\n\tdefer st.lnmu.Unlock()\n\tfor ch := range st.ln {\n\t\t// Don't block trying to send\n\t\tselect {\n\t\tcase ch <- tr:\n\t\tdefault:\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (st *Store) GetRootTrace(traceID *tracepb.TraceID) (rtn *tracepb.Request) {\n\tst.trmu.Lock()\n\tdefer st.trmu.Unlock()\n\n\tnext := st.requestIDMapping[traceID.String()]\n\tfor next != nil {\n\t\trtn = next\n\t\tnext = st.requestIDMapping[rtn.ParentTraceId.String()]\n\t}\n\n\treturn rtn\n}\n\nfunc (st *Store) List(appID string) []*TraceMeta {\n\tst.trmu.Lock()\n\ttr := st.traces[appID]\n\tst.trmu.Unlock()\n\treturn tr\n}\n\nfunc Parse(log *zerolog.Logger, traceID ID, data []byte, version trace.Version, symTable SymTabler) ([]*tracepb.Request, error) {\n\tid := &tracepb.TraceID{\n\t\tLow:  bin.Uint64(traceID[:8]),\n\t\tHigh: bin.Uint64(traceID[8:]),\n\t}\n\ttp := &traceParser{\n\t\tlog:          log,\n\t\tversion:      version,\n\t\ttraceReader:  traceReader{buf: data},\n\t\tsymTable:     symTable,\n\t\ttraceID:      id,\n\t\treqMap:       make(map[uint64]*tracepb.Request),\n\t\ttxMap:        make(map[uint64]*tracepb.DBTransaction),\n\t\tqueryMap:     make(map[uint64]*tracepb.DBQuery),\n\t\tcallMap:      make(map[uint64]interface{}),\n\t\tgoMap:        make(map[goKey]*tracepb.Goroutine),\n\t\thttpMap:      make(map[uint64]*tracepb.HTTPCall),\n\t\tpublishMap:   make(map[uint64]*tracepb.PubsubMsgPublished),\n\t\tserviceInits: make(map[uint64]*tracepb.ServiceInit),\n\t\tcacheMap:     make(map[uint64]*tracepb.CacheOp),\n\t}\n\tif err := tp.Parse(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tp.reqs, nil\n}\n\ntype goKey struct {\n\tspanID uint64\n\tgoid   uint32\n}\n\ntype SymTabler interface {\n\tSymTable(ctx context.Context) (*sym.Table, error)\n}\n\ntype traceParser struct {\n\ttraceReader\n\tlog          *zerolog.Logger\n\tversion      trace.Version\n\tsymTable     SymTabler\n\ttraceID      *tracepb.TraceID\n\treqs         []*tracepb.Request\n\treqMap       map[uint64]*tracepb.Request\n\ttxMap        map[uint64]*tracepb.DBTransaction\n\tqueryMap     map[uint64]*tracepb.DBQuery\n\tcallMap      map[uint64]interface{} // *RPCCall or *AuthCall\n\thttpMap      map[uint64]*tracepb.HTTPCall\n\tgoMap        map[goKey]*tracepb.Goroutine\n\tpublishMap   map[uint64]*tracepb.PubsubMsgPublished\n\tserviceInits map[uint64]*tracepb.ServiceInit\n\tcacheMap     map[uint64]*tracepb.CacheOp\n}\n\nfunc (tp *traceParser) Parse() error {\n\tfor i := 0; !tp.Done(); i++ {\n\t\tev := trace.EventType(tp.Byte())\n\t\tts := tp.Uint64()\n\t\tsize := int(tp.Uint32())\n\t\tstartOff := tp.Offset()\n\n\t\tvar err error\n\t\tif tp.version >= 3 {\n\t\t\terr = tp.parseEventV3(ev, ts, size)\n\t\t} else {\n\t\t\terr = tp.parseEventV1(byte(ev), ts, size)\n\t\t}\n\n\t\tif errors.Is(err, errUnknownEvent) {\n\t\t\ttp.log.Info().Msgf(\"trace: event #%d: unknown event type %s, skipping\", i, ev.String())\n\t\t\ttp.Skip(size)\n\t\t\terr = nil\n\t\t} else if err != nil {\n\t\t\treturn eerror.WithMeta(err, map[string]any{\"event#\": i, \"event\": ev.String()})\n\t\t}\n\n\t\tif tp.Overflow() {\n\t\t\treturn eerror.New(\"trace_parser\", \"invalid trace format: reader overflow parsing event\", map[string]any{\"event#\": i, \"event\": ev})\n\t\t} else if off, want := tp.Offset(), startOff+size; off < want {\n\t\t\ttp.log.Warn().Msgf(\"trace: event #%d: parsing event=%s ended before end of frame, skipping ahead %d bytes\", i, ev, want-off)\n\t\t\ttp.Skip(want - off)\n\t\t} else if off > want {\n\t\t\treturn eerror.New(\"trace_parser\", \"event exceed frame size\", map[string]any{\"event#\": i, \"event\": ev.String(), \"excess\": off - want})\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar errUnknownEvent = errors.New(\"unknown event\")\n\nfunc (tp *traceParser) parseEventV3(ev trace.EventType, ts uint64, size int) error {\n\tswitch ev {\n\tcase trace.RequestStart:\n\t\treturn tp.requestStart(ts)\n\tcase trace.RequestEnd:\n\t\treturn tp.requestEnd(ts)\n\tcase trace.GoStart:\n\t\treturn tp.goroutineStart(ts)\n\tcase trace.GoEnd:\n\t\treturn tp.goroutineEnd(ts)\n\tcase trace.GoClear:\n\t\treturn tp.goroutineClear(ts)\n\tcase trace.TxStart:\n\t\treturn tp.transactionStart(ts)\n\tcase trace.TxEnd:\n\t\treturn tp.transactionEnd(ts)\n\tcase trace.QueryStart:\n\t\treturn tp.queryStart(ts)\n\tcase trace.QueryEnd:\n\t\treturn tp.queryEnd(ts)\n\tcase trace.CallStart:\n\t\treturn tp.callStart(ts, size)\n\tcase trace.CallEnd:\n\t\treturn tp.callEnd(ts)\n\tcase trace.AuthStart, trace.AuthEnd:\n\t\t// Skip these events for now\n\t\ttp.Skip(size)\n\t\treturn nil\n\n\tcase trace.HTTPCallStart:\n\t\treturn tp.httpStart(ts)\n\tcase trace.HTTPCallEnd:\n\t\treturn tp.httpEnd(ts)\n\tcase trace.HTTPCallBodyClosed:\n\t\treturn tp.httpBodyClosed(ts)\n\tcase trace.LogMessage:\n\t\treturn tp.logMessage(ts)\n\tcase trace.PublishStart:\n\t\treturn tp.publishStart(ts)\n\tcase trace.PublishEnd:\n\t\treturn tp.publishEnd(ts)\n\tcase trace.ServiceInitStart:\n\t\treturn tp.serviceInitStart(ts)\n\tcase trace.ServiceInitEnd:\n\t\treturn tp.serviceInitEnd(ts)\n\tcase trace.CacheOpStart:\n\t\treturn tp.cacheOpStart(ts)\n\tcase trace.CacheOpEnd:\n\t\treturn tp.cacheOpEnd(ts)\n\tcase trace.BodyStream:\n\t\treturn tp.bodyStream(ts)\n\tdefault:\n\t\treturn errUnknownEvent\n\t}\n}\n\nfunc (tp *traceParser) parseEventV1(ev byte, ts uint64, size int) error {\n\tswitch ev {\n\tcase 0x01:\n\t\treturn tp.requestStart(ts)\n\tcase 0x02:\n\t\treturn tp.requestEnd(ts)\n\tcase 0x03:\n\t\treturn tp.goroutineStart(ts)\n\tcase 0x04:\n\t\treturn tp.goroutineEnd(ts)\n\tcase 0x05:\n\t\treturn tp.goroutineClear(ts)\n\tcase 0x06:\n\t\treturn tp.transactionStart(ts)\n\tcase 0x07:\n\t\treturn tp.transactionEnd(ts)\n\tcase 0x08:\n\t\treturn tp.queryStart(ts)\n\tcase 0x09:\n\t\treturn tp.queryEnd(ts)\n\tcase 0x10:\n\t\treturn tp.callStart(ts, size)\n\tcase 0x11:\n\t\treturn tp.callEnd(ts)\n\tcase 0x12, 0x13:\n\t\t// Skip these events for now\n\t\ttp.Skip(size)\n\t\treturn nil\n\n\tdefault:\n\t\treturn errUnknownEvent\n\t}\n}\n\nfunc (tp *traceParser) requestStart(ts uint64) error {\n\ttyp, err := tp.parseRequestType()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Determine the absolute start time.\n\tvar absStart time.Time\n\tif tp.version >= 6 {\n\t\tabsStart = tp.Time()\n\t} else {\n\t\t// We don't have enough information to determine the exact start time,\n\t\t// but approximate it from the monotonic clock reading\n\t\tabsStart = time.Unix(0, int64(ts))\n\t}\n\n\t// Set the trace ID\n\ttraceID := tp.traceID\n\tif tp.version >= 11 {\n\t\tparsedTraceID := tp.parseTraceID()\n\t\tif parsedTraceID.Low != 0 || parsedTraceID.High != 0 {\n\t\t\ttraceID = parsedTraceID\n\t\t}\n\t}\n\tvar parentTraceID *tracepb.TraceID\n\tif tp.version >= 12 {\n\t\tparentTraceID = tp.parseTraceID()\n\t}\n\n\tspanID := tp.Uint64()\n\tparentSpanID := tp.Uint64()\n\n\tvar service, endpoint string\n\tif tp.version < 6 {\n\t\tservice, endpoint = \"unknown\", \"Unknown\"\n\t} else if tp.version < 9 {\n\t\tservice = tp.String()\n\t\tendpoint = tp.String()\n\t}\n\n\tgoid := uint32(tp.UVarint())\n\tif tp.version < 9 {\n\t\t_ = tp.UVarint() // skip CallLoc: no longer used\n\t}\n\tdefLoc := int32(tp.UVarint())\n\n\treq := &tracepb.Request{\n\t\tTraceId:       traceID,\n\t\tParentTraceId: parentTraceID,\n\t\tSpanId:        spanID,\n\t\tParentSpanId:  parentSpanID,\n\t\tStartTime:     ts,\n\t\tServiceName:   service,\n\t\tEndpointName:  endpoint,\n\t\tAbsStartTime:  uint64(absStart.UnixNano()),\n\t\t// EndTime not set yet\n\t\tDefLoc: defLoc,\n\t\tGoid:   goid,\n\t\tType:   typ,\n\t}\n\n\tif tp.version < 9 {\n\t\treq.Uid = tp.String()\n\n\t\tfor n, i := tp.UVarint(), uint64(0); i < n; i++ {\n\t\t\tsize := tp.UVarint()\n\t\t\tif size > (10 << 20) {\n\t\t\t\treturn eerror.New(\"trace_parser\", \"input too large\", map[string]any{\"size\": size})\n\t\t\t}\n\t\t\tinput := make([]byte, size)\n\t\t\ttp.Bytes(input)\n\t\t\treq.Inputs = append(req.Inputs, input)\n\t\t}\n\t}\n\n\tswitch typ {\n\tcase tracepb.Request_RPC:\n\t\tif tp.version >= 9 {\n\t\t\tisRaw := tp.Bool()\n\t\t\treq.ServiceName = tp.String()\n\t\t\treq.EndpointName = tp.String()\n\t\t\treq.HttpMethod = tp.String()\n\t\t\treq.Path = tp.String()\n\n\t\t\tnumParams := tp.UVarint()\n\t\t\treq.PathParams = make([]string, numParams)\n\t\t\tfor i := uint64(0); i < numParams; i++ {\n\t\t\t\treq.PathParams[i] = tp.String()\n\t\t\t}\n\n\t\t\treq.Uid = tp.String()\n\n\t\t\tif tp.version >= 11 {\n\t\t\t\treq.ExternalRequestId = tp.String()\n\n\t\t\t\tif tp.version >= 12 {\n\t\t\t\t\treq.ExternalCorrelationId = tp.String()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif isRaw {\n\t\t\t\treq.RawRequestHeaders = tp.parseHTTPHeaders()\n\t\t\t} else {\n\t\t\t\treq.RequestPayload = tp.ByteString()\n\t\t\t}\n\t\t}\n\n\tcase tracepb.Request_AUTH:\n\t\tif tp.version >= 9 {\n\t\t\treq.ServiceName = tp.String()\n\t\t\treq.EndpointName = tp.String()\n\t\t\treq.RequestPayload = tp.ByteString()\n\t\t}\n\n\tcase tracepb.Request_PUBSUB_MSG:\n\t\tif tp.version >= 9 {\n\t\t\treq.ServiceName = tp.String()\n\t\t}\n\n\t\treq.TopicName = tp.String()\n\t\treq.SubscriptionName = tp.String()\n\t\treq.MessageId = tp.String()\n\t\treq.Attempt = tp.Uint32()\n\t\treq.PublishTime = uint64(tp.Time().UnixMilli())\n\n\t\tif tp.version >= 10 {\n\t\t\treq.RequestPayload = tp.ByteString()\n\t\t}\n\t}\n\n\ttp.reqs = append(tp.reqs, req)\n\ttp.reqMap[req.SpanId] = req\n\treturn nil\n}\n\nfunc (tp *traceParser) bodyStream(ts uint64) error {\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\tflags := tp.Byte()\n\tdata := tp.ByteString()\n\n\tisResponse := (flags & 1) == 1\n\toverflowed := (flags & 2) == 2\n\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_BodyStream{\n\t\t\tBodyStream: &tracepb.BodyStream{\n\t\t\t\tIsResponse: isResponse,\n\t\t\t\tOverflowed: overflowed,\n\t\t\t\tData:       data,\n\t\t\t},\n\t\t},\n\t})\n\n\treturn nil\n}\n\nfunc (tp *traceParser) requestEnd(ts uint64) error {\n\tvar typ tracepb.Request_Type\n\tif tp.version >= 9 {\n\t\tvar err error\n\t\ttyp, err = tp.parseRequestType()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\tif tp.version < 9 {\n\t\t// Not captured by the protocol for old versions,\n\t\t// so grab it from the request.\n\t\ttyp = req.Type\n\t}\n\n\t// dur := ts - rd.startTs\n\treq.EndTime = ts\n\n\tif tp.version >= 9 {\n\t\terrMsg := tp.ByteString()\n\t\tif len(errMsg) > 0 {\n\t\t\treq.Err = errMsg\n\n\t\t\treq.ErrStack = tp.stack(filterNone)\n\t\t\tif tp.version >= 13 {\n\t\t\t\treq.PanicStack = tp.formattedStack()\n\t\t\t}\n\t\t}\n\n\t\tswitch typ {\n\t\tcase tracepb.Request_RPC:\n\t\t\tif isRaw := tp.Bool(); isRaw {\n\t\t\t\treq.RawResponseHeaders = tp.parseHTTPHeaders()\n\t\t\t} else {\n\t\t\t\treq.ResponsePayload = tp.ByteString()\n\t\t\t}\n\t\tcase tracepb.Request_AUTH:\n\t\t\treq.Uid = tp.String()\n\t\t\treq.ResponsePayload = tp.ByteString()\n\t\tcase tracepb.Request_PUBSUB_MSG:\n\t\t\treq.ResponsePayload = tp.ByteString()\n\t\t}\n\t} else {\n\t\tisErr := tp.Bool()\n\t\tif isErr {\n\t\t\tmsg := tp.ByteString()\n\t\t\tif len(msg) == 0 {\n\t\t\t\tmsg = []byte(\"unknown error\")\n\t\t\t}\n\t\t\tif tp.version >= 5 {\n\t\t\t\treq.ErrStack = tp.stack(filterNone)\n\t\t\t}\n\t\t} else {\n\t\t\tfor n, i := tp.UVarint(), uint64(0); i < n; i++ {\n\t\t\t\tsize := tp.UVarint()\n\t\t\t\tif size > (10 << 20) {\n\t\t\t\t\treturn eerror.New(\"trace_parser\", \"input too large\", map[string]any{\"size\": size})\n\t\t\t\t}\n\t\t\t\toutput := make([]byte, size)\n\t\t\t\ttp.Bytes(output)\n\t\t\t\treq.Outputs = append(req.Outputs, output)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (tp *traceParser) goroutineStart(ts uint64) error {\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\t// This is an expected error in certain situations like goroutines\n\t\t// living past the request end that then spawn additional goroutines.\n\t\t// Treat it as a warning but don't fail the parse.\n\t\ttp.log.Warn().Uint64(\"span_id\", spanID).Msg(\"unknown request span\")\n\t\treturn nil\n\t}\n\tgoid := tp.Uint32()\n\tg := &tracepb.Goroutine{\n\t\tGoid:      goid,\n\t\tStartTime: ts,\n\t}\n\tk := goKey{spanID: spanID, goid: goid}\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_Goroutine{Goroutine: g},\n\t})\n\ttp.goMap[k] = g\n\treturn nil\n}\n\nfunc (tp *traceParser) goroutineEnd(ts uint64) error {\n\tspanID := tp.Uint64()\n\tgoid := tp.Uint32()\n\tk := goKey{spanID: spanID, goid: goid}\n\tg, ok := tp.goMap[k]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown goroutine id\", map[string]any{\"goid\": goid})\n\t}\n\tg.EndTime = ts\n\tdelete(tp.goMap, k)\n\treturn nil\n}\n\nfunc (tp *traceParser) goroutineClear(ts uint64) error {\n\tspanID := tp.Uint64()\n\tgoid := tp.Uint32()\n\tk := goKey{spanID: spanID, goid: goid}\n\tg, ok := tp.goMap[k]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown goroutine id\", map[string]any{\"spanID\": spanID, \"goid\": goid})\n\t}\n\tg.EndTime = ts\n\tdelete(tp.goMap, k)\n\treturn nil\n}\n\nfunc (tp *traceParser) transactionStart(ts uint64) error {\n\ttxid := tp.UVarint()\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\tgoid := uint32(tp.UVarint())\n\n\tif tp.version < 4 {\n\t\t_ = tp.UVarint() // StartLoc; no longer used\n\t}\n\n\ttx := &tracepb.DBTransaction{\n\t\tGoid:      goid,\n\t\tStartTime: ts,\n\t}\n\tif tp.version >= 5 {\n\t\ttx.BeginStack = tp.stack(filterDB)\n\t}\n\ttp.txMap[txid] = tx\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_Tx{Tx: tx},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) transactionEnd(ts uint64) error {\n\ttxid := tp.UVarint()\n\t_ = tp.Uint64() // spanID\n\ttx, ok := tp.txMap[txid]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown transaction id\", map[string]any{\"txid\": txid})\n\t}\n\t_ = uint32(tp.UVarint()) // goid\n\tcompl := tp.Byte()\n\tif tp.version < 4 {\n\t\t_ = int32(tp.UVarint()) // EndLoc; no longer used\n\t}\n\terrMsg := tp.ByteString()\n\n\tvar stack *tracepb.StackTrace\n\tif tp.version >= 5 {\n\t\tstack = tp.stack(filterDB)\n\t}\n\n\t// It's possible to get multiple transaction end events.\n\t// Ignore them for now; we will expose this information later.\n\tif tx.EndTime == 0 {\n\t\ttx.EndTime = ts\n\t\ttx.Err = errMsg\n\t\ttx.EndStack = stack\n\t\tswitch compl {\n\t\tcase 0:\n\t\t\ttx.Completion = tracepb.DBTransaction_ROLLBACK\n\t\tcase 1:\n\t\t\ttx.Completion = tracepb.DBTransaction_COMMIT\n\t\tdefault:\n\t\t\treturn eerror.New(\"trace_parser\", \"unknown completion type\", map[string]any{\"compl\": compl})\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (tp *traceParser) queryStart(ts uint64) error {\n\tqid := tp.UVarint()\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\ttxid := tp.UVarint()\n\tgoid := uint32(tp.UVarint())\n\n\tif tp.version < 4 {\n\t\t_ = tp.UVarint() // CallLoc; no longer used\n\t}\n\tq := &tracepb.DBQuery{\n\t\tGoid:      goid,\n\t\tStartTime: ts,\n\t\tQuery:     tp.ByteString(),\n\t}\n\tif tp.version >= 5 {\n\t\tq.Stack = tp.stack(filterDB)\n\t}\n\ttp.queryMap[qid] = q\n\n\tif txid != 0 {\n\t\ttx, ok := tp.txMap[txid]\n\t\tif !ok {\n\t\t\treturn eerror.New(\"trace_parser\", \"unknown transaction id\", map[string]any{\"txid\": txid})\n\t\t}\n\t\ttx.Queries = append(tx.Queries, q)\n\t} else {\n\t\treq.Events = append(req.Events, &tracepb.Event{\n\t\t\tData: &tracepb.Event_Query{Query: q},\n\t\t})\n\t}\n\n\treturn nil\n}\n\nfunc (tp *traceParser) queryEnd(ts uint64) error {\n\tqid := tp.UVarint()\n\tq, ok := tp.queryMap[qid]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown query id\", map[string]any{\"qid\": qid})\n\t}\n\tq.EndTime = ts\n\tq.Err = tp.ByteString()\n\treturn nil\n}\n\nfunc (tp *traceParser) callStart(ts uint64, size int) error {\n\tcallID := tp.UVarint()\n\tspanID := tp.Uint64()\n\t// TODO(eandre) We currently (Dec 2, 2020) have an old format\n\t// that leaves out the child span id. Detect this based on the size\n\t// and provide a workaround that doesn't crash.\n\tvar childSpanID uint64\n\tif size == 12 {\n\t\tchildSpanID = spanID\n\t} else {\n\t\tchildSpanID = tp.Uint64()\n\t}\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\n\tgoid := uint32(tp.UVarint())\n\t_ = tp.UVarint() // CallLoc: no longer used\n\tdefLoc := int32(tp.UVarint())\n\n\tc := &tracepb.RPCCall{\n\t\tSpanId:    childSpanID,\n\t\tGoid:      goid,\n\t\tDefLoc:    defLoc,\n\t\tStartTime: ts,\n\t}\n\tif tp.version >= 5 {\n\t\tc.Stack = tp.stack(filterNone)\n\t}\n\ttp.callMap[callID] = c\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_Rpc{Rpc: c},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) callEnd(ts uint64) error {\n\tcallID := tp.UVarint()\n\terrMsg := tp.ByteString()\n\tc, ok := tp.callMap[callID].(*tracepb.RPCCall)\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown call \", map[string]any{\"callID\": callID})\n\t}\n\tc.EndTime = ts\n\tc.Err = errMsg\n\tdelete(tp.callMap, callID)\n\treturn nil\n}\n\nfunc (tp *traceParser) httpStart(ts uint64) error {\n\tcallID := tp.UVarint()\n\tspanID := tp.Uint64()\n\tchildSpanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\tc := &tracepb.HTTPCall{\n\t\tSpanId:    childSpanID,\n\t\tGoid:      uint32(tp.UVarint()),\n\t\tMethod:    tp.String(),\n\t\tUrl:       tp.String(),\n\t\tStartTime: ts,\n\t}\n\ttp.httpMap[callID] = c\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_Http{Http: c},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) httpEnd(ts uint64) error {\n\tcallID := tp.UVarint()\n\terrMsg := tp.ByteString()\n\tstatus := tp.UVarint()\n\tc, ok := tp.httpMap[callID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown call \", map[string]any{\"callID\": callID})\n\t}\n\tc.EndTime = ts\n\tc.Err = errMsg\n\tc.StatusCode = uint32(status)\n\n\tnumEvents := tp.UVarint()\n\tc.Events = make([]*tracepb.HTTPTraceEvent, 0, numEvents)\n\tfor i := 0; i < int(numEvents); i++ {\n\t\tev, err := tp.httpEvent()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.Events = append(c.Events, ev)\n\t}\n\n\treturn nil\n}\n\nfunc (tp *traceParser) httpBodyClosed(ts uint64) error {\n\tcallID := tp.UVarint()\n\t_ = tp.ByteString() // close error\n\tc, ok := tp.httpMap[callID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown call \", map[string]any{\"callID\": callID})\n\t}\n\tc.BodyClosedTime = ts\n\tdelete(tp.httpMap, callID)\n\treturn nil\n}\n\nfunc (tp *traceParser) httpEvent() (*tracepb.HTTPTraceEvent, error) {\n\tcode := tracepb.HTTPTraceEventCode(tp.Byte())\n\tts := tp.Int64()\n\tev := &tracepb.HTTPTraceEvent{\n\t\tCode: code,\n\t\tTime: uint64(ts),\n\t}\n\n\tswitch code {\n\tcase tracepb.HTTPTraceEventCode_GET_CONN:\n\t\tev.Data = &tracepb.HTTPTraceEvent_GetConn{\n\t\t\tGetConn: &tracepb.HTTPGetConnData{\n\t\t\t\tHostPort: tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_GOT_CONN:\n\t\tev.Data = &tracepb.HTTPTraceEvent_GotConn{\n\t\t\tGotConn: &tracepb.HTTPGotConnData{\n\t\t\t\tReused:         tp.Bool(),\n\t\t\t\tWasIdle:        tp.Bool(),\n\t\t\t\tIdleDurationNs: tp.Int64(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_GOT_FIRST_RESPONSE_BYTE:\n\t\t// no data\n\n\tcase tracepb.HTTPTraceEventCode_GOT_1XX_RESPONSE:\n\t\tev.Data = &tracepb.HTTPTraceEvent_Got_1XxResponse{\n\t\t\tGot_1XxResponse: &tracepb.HTTPGot1XxResponseData{\n\t\t\t\tCode: int32(tp.Varint()),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_DNS_START:\n\t\tev.Data = &tracepb.HTTPTraceEvent_DnsStart{\n\t\t\tDnsStart: &tracepb.HTTPDNSStartData{\n\t\t\t\tHost: tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_DNS_DONE:\n\t\tdata := &tracepb.HTTPDNSDoneData{\n\t\t\tErr: tp.ByteString(),\n\t\t}\n\t\taddrs := int(tp.UVarint())\n\t\tfor j := 0; j < addrs; j++ {\n\t\t\tdata.Addrs = append(data.Addrs, &tracepb.DNSAddr{\n\t\t\t\tIp: tp.ByteString(),\n\t\t\t})\n\t\t}\n\t\tev.Data = &tracepb.HTTPTraceEvent_DnsDone{DnsDone: data}\n\n\tcase tracepb.HTTPTraceEventCode_CONNECT_START:\n\t\tev.Data = &tracepb.HTTPTraceEvent_ConnectStart{\n\t\t\tConnectStart: &tracepb.HTTPConnectStartData{\n\t\t\t\tNetwork: tp.String(),\n\t\t\t\tAddr:    tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_CONNECT_DONE:\n\t\tev.Data = &tracepb.HTTPTraceEvent_ConnectDone{\n\t\t\tConnectDone: &tracepb.HTTPConnectDoneData{\n\t\t\t\tNetwork: tp.String(),\n\t\t\t\tAddr:    tp.String(),\n\t\t\t\tErr:     tp.ByteString(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_TLS_HANDSHAKE_START:\n\t\t// no data\n\n\tcase tracepb.HTTPTraceEventCode_TLS_HANDSHAKE_DONE:\n\t\tev.Data = &tracepb.HTTPTraceEvent_TlsHandshakeDone{\n\t\t\tTlsHandshakeDone: &tracepb.HTTPTLSHandshakeDoneData{\n\t\t\t\tErr:                tp.ByteString(),\n\t\t\t\tTlsVersion:         tp.Uint32(),\n\t\t\t\tCipherSuite:        tp.Uint32(),\n\t\t\t\tServerName:         tp.String(),\n\t\t\t\tNegotiatedProtocol: tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_WROTE_HEADERS:\n\t\t// no data\n\n\tcase tracepb.HTTPTraceEventCode_WROTE_REQUEST:\n\t\tev.Data = &tracepb.HTTPTraceEvent_WroteRequest{\n\t\t\tWroteRequest: &tracepb.HTTPWroteRequestData{\n\t\t\t\tErr: tp.ByteString(),\n\t\t\t},\n\t\t}\n\n\tcase tracepb.HTTPTraceEventCode_WAIT_100_CONTINUE:\n\t\t// no data\n\n\tdefault:\n\t\treturn nil, eerror.New(\"trace_parser\", \"unknown http event\", map[string]any{\"code\": code})\n\t}\n\treturn ev, nil\n}\n\nfunc (tp *traceParser) logMessage(ts uint64) error {\n\tspanID := tp.Uint64()\n\tgoid := uint32(tp.UVarint())\n\tlevel := tp.Byte()\n\tmsg := tp.String()\n\tfields := int(tp.UVarint())\n\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request\", map[string]any{\"spanID\": spanID})\n\t} else if fields > 64 {\n\t\treturn eerror.New(\"trace_parser\", \"too many fields\", map[string]any{\"fields\": fields})\n\t}\n\n\tlog := &tracepb.LogMessage{\n\t\tSpanId: spanID,\n\t\tGoid:   goid,\n\t\tTime:   ts,\n\t\tMsg:    msg,\n\t}\n\n\t// We introduced more log levels in trace version 8.\n\tif tp.version >= 8 {\n\t\tswitch level {\n\t\tcase 0:\n\t\t\tlog.Level = tracepb.LogMessage_TRACE\n\t\tcase 1:\n\t\t\tlog.Level = tracepb.LogMessage_DEBUG\n\t\tcase 2:\n\t\t\tlog.Level = tracepb.LogMessage_INFO\n\t\tcase 3:\n\t\t\tlog.Level = tracepb.LogMessage_WARN\n\t\tcase 4:\n\t\t\tlog.Level = tracepb.LogMessage_ERROR\n\t\tdefault:\n\t\t\treturn eerror.New(\"trace_parser\", \"unknown log message level\", map[string]any{\"level\": level})\n\t\t}\n\t} else {\n\t\tswitch level {\n\t\tcase 0:\n\t\t\tlog.Level = tracepb.LogMessage_DEBUG\n\t\tcase 1:\n\t\t\tlog.Level = tracepb.LogMessage_INFO\n\t\tcase 2:\n\t\t\tlog.Level = tracepb.LogMessage_ERROR\n\t\tdefault:\n\t\t\treturn eerror.New(\"trace_parser\", \"unknown log message level\", map[string]any{\"level\": level})\n\t\t}\n\t}\n\n\tfor i := 0; i < fields; i++ {\n\t\tf, err := tp.logField()\n\t\tif err != nil {\n\t\t\treturn eerror.Wrap(err, \"trace_parser\", \"error parsing field\", map[string]any{\"field#\": i})\n\t\t}\n\t\tlog.Fields = append(log.Fields, f)\n\t}\n\tif tp.version >= 5 {\n\t\tlog.Stack = tp.stack(filterNone)\n\t}\n\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_Log{Log: log},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) logField() (*tracepb.LogField, error) {\n\ttyp := tp.Byte()\n\tkey := tp.String()\n\tf := &tracepb.LogField{\n\t\tKey: key,\n\t}\n\tswitch typ {\n\tcase 1:\n\t\tif tp.version >= 7 { // We only added stack's to error log fields with version 7 (it was missing from the internal runtime before that)\n\t\t\tf.Value = &tracepb.LogField_ErrorWithStack{ErrorWithStack: &tracepb.ErrWithStack{\n\t\t\t\tError: tp.String(),\n\t\t\t\tStack: tp.stack(filterNone),\n\t\t\t}}\n\t\t} else {\n\t\t\tf.Value = &tracepb.LogField_ErrorWithoutStack{ErrorWithoutStack: tp.String()}\n\t\t}\n\tcase 2:\n\t\tf.Value = &tracepb.LogField_Str{Str: tp.String()}\n\tcase 3:\n\t\tf.Value = &tracepb.LogField_Bool{Bool: tp.Bool()}\n\tcase 4:\n\t\tf.Value = &tracepb.LogField_Time{Time: timestamppb.New(tp.Time())}\n\tcase 5:\n\t\tf.Value = &tracepb.LogField_Dur{Dur: tp.Int64()}\n\tcase 6:\n\t\tb := make([]byte, 16)\n\t\ttp.Bytes(b)\n\t\tf.Value = &tracepb.LogField_Uuid{Uuid: b}\n\tcase 7:\n\t\tval := tp.ByteString()\n\t\terr := tp.String()\n\t\tif err != \"\" {\n\t\t\tf.Value = &tracepb.LogField_ErrorWithoutStack{ErrorWithoutStack: err}\n\t\t} else {\n\t\t\tf.Value = &tracepb.LogField_Json{Json: val}\n\t\t}\n\tcase 8:\n\t\tf.Value = &tracepb.LogField_Int{Int: tp.Varint()}\n\tcase 9:\n\t\tf.Value = &tracepb.LogField_Uint{Uint: tp.UVarint()}\n\tcase 10:\n\t\tf.Value = &tracepb.LogField_Float32{Float32: tp.Float32()}\n\tcase 11:\n\t\tf.Value = &tracepb.LogField_Float64{Float64: tp.Float64()}\n\tdefault:\n\t\treturn nil, eerror.New(\"trace_parser\", \"unknown field type\", map[string]any{\"typ\": typ})\n\t}\n\treturn f, nil\n}\n\nfunc (tp *traceParser) publishStart(ts uint64) error {\n\tpublishID := tp.UVarint()\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\n\tpublish := &tracepb.PubsubMsgPublished{\n\t\tGoid:      tp.UVarint(),\n\t\tStartTime: ts,\n\t\tTopic:     tp.String(),\n\t\tMessage:   tp.ByteString(),\n\t\tStack:     tp.stack(filterNone),\n\t}\n\ttp.publishMap[publishID] = publish\n\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_PublishedMsg{PublishedMsg: publish},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) publishEnd(ts uint64) error {\n\tpublishID := tp.UVarint()\n\tpublish, ok := tp.publishMap[publishID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown publish\", map[string]any{\"publishID\": publishID})\n\t}\n\tpublish.EndTime = ts\n\tpublish.MessageId = tp.String()\n\tpublish.Err = tp.ByteString()\n\tdelete(tp.publishMap, publishID)\n\treturn nil\n}\n\nfunc (tp *traceParser) serviceInitStart(ts uint64) error {\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\n\tinitID := tp.UVarint()\n\tsvcInit := &tracepb.ServiceInit{\n\t\tGoid:      tp.UVarint(),\n\t\tDefLoc:    int32(tp.UVarint()),\n\t\tStartTime: ts,\n\t\tService:   tp.String(),\n\t}\n\ttp.serviceInits[initID] = svcInit\n\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_ServiceInit{ServiceInit: svcInit},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) serviceInitEnd(ts uint64) error {\n\tinitID := tp.UVarint()\n\tsvcInit, ok := tp.serviceInits[initID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown service init\", map[string]any{\"initID\": initID})\n\t}\n\tsvcInit.EndTime = ts\n\tsvcInit.Err = tp.ByteString()\n\tif len(svcInit.Err) > 0 {\n\t\tsvcInit.ErrStack = tp.stack(filterNone)\n\t}\n\tdelete(tp.serviceInits, initID)\n\treturn nil\n}\n\nfunc (tp *traceParser) cacheOpStart(ts uint64) error {\n\topID := tp.UVarint()\n\tspanID := tp.Uint64()\n\treq, ok := tp.reqMap[spanID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown request span\", map[string]any{\"spanID\": spanID})\n\t}\n\n\top := &tracepb.CacheOp{\n\t\tGoid:      uint32(tp.UVarint()),\n\t\tDefLoc:    int32(tp.UVarint()),\n\t\tStartTime: ts,\n\t\tOperation: tp.String(),\n\t\tWrite:     tp.Bool(),\n\t\tResult:    tracepb.CacheOp_UNKNOWN,\n\t\tStack:     tp.stack(filterNone),\n\t}\n\n\tnumKeys := tp.UVarint()\n\top.Keys = make([]string, numKeys)\n\tfor i := 0; i < int(numKeys); i++ {\n\t\top.Keys[i] = tp.String()\n\t}\n\n\tnumInputs := tp.UVarint()\n\top.Inputs = make([][]byte, numInputs)\n\tfor i := 0; i < int(numInputs); i++ {\n\t\top.Inputs[i] = tp.ByteString()\n\t}\n\ttp.cacheMap[opID] = op\n\n\treq.Events = append(req.Events, &tracepb.Event{\n\t\tData: &tracepb.Event_Cache{Cache: op},\n\t})\n\treturn nil\n}\n\nfunc (tp *traceParser) cacheOpEnd(ts uint64) error {\n\topID := tp.UVarint()\n\top, ok := tp.cacheMap[opID]\n\tif !ok {\n\t\treturn eerror.New(\"trace_parser\", \"unknown cache\", map[string]any{\"opID\": opID})\n\t}\n\top.EndTime = ts\n\n\tres := trace.CacheOpResult(tp.Byte())\n\tswitch res {\n\tcase trace.CacheOK:\n\t\top.Result = tracepb.CacheOp_OK\n\tcase trace.CacheNoSuchKey:\n\t\top.Result = tracepb.CacheOp_NO_SUCH_KEY\n\tcase trace.CacheConflict:\n\t\top.Result = tracepb.CacheOp_CONFLICT\n\tcase trace.CacheErr:\n\t\top.Result = tracepb.CacheOp_ERR\n\t\top.Err = tp.ByteString()\n\t}\n\n\tnumOutputs := tp.UVarint()\n\top.Outputs = make([][]byte, numOutputs)\n\tfor i := 0; i < int(numOutputs); i++ {\n\t\top.Outputs[i] = tp.ByteString()\n\t}\n\n\tdelete(tp.cacheMap, opID)\n\treturn nil\n}\n\ntype stackFilter int\n\nconst (\n\tfilterNone stackFilter = iota\n\tfilterDB\n)\n\nfunc (tp *traceParser) stack(filterMode stackFilter) *tracepb.StackTrace {\n\tn := int(tp.Byte())\n\ttr := &tracepb.StackTrace{}\n\tif n == 0 {\n\t\treturn tr\n\t}\n\n\tdiffs := make([]int64, n)\n\tfor i := 0; i < n; i++ {\n\t\tdiff := tp.Varint()\n\t\tdiffs[i] = diff\n\t}\n\ttr.Pcs = diffs\n\n\tif tp.symTable == nil {\n\t\treturn tr\n\t}\n\n\t// If we have a symTable, we can extract the full set of frames from the trace\n\tsym, err := tp.symTable.SymTable(context.Background())\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"could not parse sym table\")\n\t\treturn tr\n\t}\n\n\tprev := int64(0)\n\tpcs := make([]uint64, n)\n\tfor i := 0; i < n; i++ {\n\t\tx := prev + diffs[i]\n\t\tprev = x\n\t\tpcs[i] = uint64(x) + sym.BaseOffset\n\t}\n\n\ttr.Frames = make([]*tracepb.StackFrame, 0, n)\nPCLoop:\n\tfor _, pc := range pcs {\n\t\tfile, line, fn := sym.PCToLine(pc)\n\t\tif fn != nil {\n\t\t\tif filterMode == filterDB && strings.Contains(filepath.ToSlash(file), \"/src/database/sql/\") {\n\t\t\t\tcontinue PCLoop\n\t\t\t}\n\t\t\ttr.Frames = append(tr.Frames, &tracepb.StackFrame{\n\t\t\t\tFunc:     fn.Name,\n\t\t\t\tFilename: file,\n\t\t\t\tLine:     int32(line),\n\t\t\t})\n\t\t}\n\t}\n\treturn tr\n}\n\nfunc (tp *traceParser) formattedStack() *tracepb.StackTrace {\n\tn := int(tp.Byte())\n\ttr := &tracepb.StackTrace{}\n\tif n == 0 {\n\t\treturn tr\n\t}\n\n\ttr.Frames = make([]*tracepb.StackFrame, 0, n)\n\tfor i := 0; i < n; i++ {\n\t\ttr.Frames = append(tr.Frames, &tracepb.StackFrame{\n\t\t\tFilename: tp.String(),\n\t\t\tLine:     int32(tp.UVarint()),\n\t\t\tFunc:     tp.String(),\n\t\t})\n\t}\n\n\treturn tr\n}\n\nfunc (tp *traceParser) parseRequestType() (tracepb.Request_Type, error) {\n\tswitch b := tp.Byte(); b {\n\tcase 0x01:\n\t\treturn tracepb.Request_RPC, nil\n\tcase 0x02:\n\t\treturn tracepb.Request_AUTH, nil\n\tcase 0x03:\n\t\treturn tracepb.Request_PUBSUB_MSG, nil\n\tdefault:\n\t\treturn -1, eerror.New(\"trace_parser\", \"unknown request type\", map[string]any{\"type\": fmt.Sprintf(\"%x\", b)})\n\t}\n}\n\nfunc (tp *traceParser) parseTraceID() *tracepb.TraceID {\n\tvar traceID [16]byte\n\ttp.Bytes(traceID[:])\n\treturn &tracepb.TraceID{\n\t\tLow:  bin.Uint64(traceID[:8]),\n\t\tHigh: bin.Uint64(traceID[8:]),\n\t}\n}\n\nfunc (tp *traceParser) parseHTTPHeaders() map[string]string {\n\tnumHeaders := tp.UVarint()\n\th := make(map[string]string, numHeaders)\n\tfor i := uint64(0); i < numHeaders; i++ {\n\t\th[tp.String()] = tp.String()\n\t}\n\treturn h\n}\n\nvar bin = binary.LittleEndian\n\ntype traceReader struct {\n\tbuf []byte\n\toff int\n\terr bool\n}\n\nfunc (tr *traceReader) Offset() int {\n\treturn tr.off\n}\n\nfunc (tr *traceReader) Done() bool {\n\treturn tr.off >= len(tr.buf)\n}\n\nfunc (tr *traceReader) Overflow() bool {\n\treturn tr.err\n}\n\nfunc (tr *traceReader) Bytes(b []byte) {\n\tn := copy(b, tr.buf[tr.off:])\n\ttr.off += n\n\tif len(b) > n {\n\t\ttr.err = true\n\t}\n}\n\nfunc (tr *traceReader) Skip(n int) {\n\ttr.off += n\n\tif tr.off > len(tr.buf) {\n\t\ttr.off = len(tr.buf)\n\t\ttr.err = true\n\t}\n}\n\nfunc (tr *traceReader) Byte() byte {\n\tvar buf [1]byte\n\ttr.Bytes(buf[:])\n\treturn buf[0]\n}\n\nfunc (tr *traceReader) Bool() bool {\n\treturn tr.Byte() != 0\n}\n\nfunc (tr *traceReader) String() string {\n\treturn string(tr.ByteString())\n}\n\nfunc (tr *traceReader) ByteString() []byte {\n\tsize := tr.UVarint()\n\tif (size) == 0 {\n\t\treturn nil\n\t}\n\tb := make([]byte, int(size))\n\ttr.Bytes(b)\n\treturn b\n}\n\nfunc (tr *traceReader) Time() time.Time {\n\tsec := tr.Int64()\n\tnsec := tr.Int32()\n\treturn time.Unix(sec, int64(nsec)).UTC()\n}\n\nfunc (tr *traceReader) Int32() int32 {\n\tu := tr.Uint32()\n\tvar v int32\n\tif u&1 == 0 {\n\t\tv = int32(u >> 1)\n\t} else {\n\t\tv = ^int32(u >> 1)\n\t}\n\treturn v\n}\n\nfunc (tr *traceReader) Uint32() uint32 {\n\tvar buf [4]byte\n\ttr.Bytes(buf[:])\n\treturn bin.Uint32(buf[:])\n}\n\nfunc (tr *traceReader) Int64() int64 {\n\tu := tr.Uint64()\n\tvar v int64\n\tif u&1 == 0 {\n\t\tv = int64(u >> 1)\n\t} else {\n\t\tv = ^int64(u >> 1)\n\t}\n\treturn v\n}\n\nfunc (tr *traceReader) Uint64() uint64 {\n\tvar buf [8]byte\n\ttr.Bytes(buf[:])\n\treturn bin.Uint64(buf[:])\n}\n\nfunc (tr *traceReader) Varint() int64 {\n\tu := tr.UVarint()\n\tvar v int64\n\tif u&1 == 0 {\n\t\tv = int64(u >> 1)\n\t} else {\n\t\tv = ^int64(u >> 1)\n\t}\n\treturn v\n}\n\nfunc (tr *traceReader) UVarint() uint64 {\n\tvar u uint64\n\tfor i := 0; tr.off < len(tr.buf); i += 7 {\n\t\tb := tr.buf[tr.off]\n\t\tu |= uint64(b&^0x80) << i\n\t\ttr.off++\n\t\tif b&0x80 == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn u\n}\n\nfunc (tr *traceReader) Float32() float32 {\n\tb := tr.Uint32()\n\treturn math.Float32frombits(b)\n}\n\nfunc (tr *traceReader) Float64() float64 {\n\tb := tr.Uint64()\n\treturn math.Float64frombits(b)\n}\n"
  },
  {
    "path": "cli/daemon/engine/trace2/recorder.go",
    "content": "package trace2\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encr.dev/pkg/traceparser\"\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n)\n\ntype Recorder struct {\n\ts Store\n}\n\nfunc NewRecorder(s Store) *Recorder {\n\treturn &Recorder{s}\n}\n\ntype RecordData struct {\n\tMeta         *Meta\n\tTraceVersion trace2.Version\n\tBuf          *bufio.Reader\n\tAnchor       trace2.TimeAnchor\n}\n\nfunc (h *Recorder) RecordTrace(data RecordData) error {\n\teventCh := make(chan *tracepb2.TraceEvent, 100)\n\tgo func() {\n\t\tdefer close(eventCh)\n\t\tfor {\n\t\t\tev, err := traceparser.ParseEvent(data.Buf, data.Anchor, data.TraceVersion)\n\t\t\tif ev != nil {\n\t\t\t\teventCh <- ev\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// We have an error.\n\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\tlog.Error().Err(err).Msg(\"unable to parse trace\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}()\n\n\twriteEvents := func(ctx context.Context, ev []*tracepb2.TraceEvent) error {\n\t\tif len(ev) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\treturn h.s.WriteEvents(ctx, data.Meta, ev)\n\t}\n\n\t// pendingWrites are the accumulated events that we have parsed so far\n\t// that have not yet been written to the store.\n\tpendingWrites := make([]*tracepb2.TraceEvent, 0, 100)\n\n\tflushWrites := func() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t\tif err := writeEvents(ctx, pendingWrites); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"unable to write trace events\")\n\t\t\treturn\n\t\t}\n\n\t\t// Garbage collect the slice if it's too big.\n\t\tif cap(pendingWrites) > 1000 {\n\t\t\tpendingWrites = make([]*tracepb2.TraceEvent, 0, 100)\n\t\t} else {\n\t\t\tpendingWrites = pendingWrites[:0]\n\t\t}\n\t}\n\n\tdebounce := time.NewTicker(500 * time.Millisecond)\n\tdefer debounce.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase ev, ok := <-eventCh:\n\t\t\tif !ok {\n\t\t\t\t// No more events.\n\t\t\t\tflushWrites()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdebounce.Reset(500 * time.Millisecond)\n\t\t\tpendingWrites = append(pendingWrites, ev)\n\n\t\t\t// Flush immediately if we've accumulated a bunch of events\n\t\t\t// since the debounce may never run in a high throughput scenario.\n\t\t\tif len(pendingWrites) >= 100 {\n\t\t\t\tflushWrites()\n\t\t\t}\n\n\t\tcase <-debounce.C:\n\t\t\tflushWrites()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/engine/trace2/sqlite/read.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/pkg/fns\"\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n)\n\nfunc (s *Store) List(ctx context.Context, q *trace2.Query, iter trace2.ListEntryIterator) error {\n\tlimit := q.Limit\n\tif limit <= 0 {\n\t\tlimit = 100\n\t}\n\n\targs := []any{\n\t\tq.AppID, tracepb2.SpanSummary_AUTH, /* ignore auth spans */\n\t}\n\n\textraWhereClause := \"\"\n\n\tif q.MessageID != \"\" {\n\t\targs = append(args, q.MessageID)\n\t\textraWhereClause += \" AND message_id = $\" + strconv.Itoa(len(args))\n\t}\n\n\t// If we're filter for tests / not tests, add the extra where clause\n\tif q.TestFilter != nil {\n\t\targs = append(args, tracepb2.SpanSummary_TEST)\n\t\tif *q.TestFilter {\n\t\t\textraWhereClause += \" AND span_type = $\" + strconv.Itoa(len(args))\n\t\t} else {\n\t\t\textraWhereClause += \" AND span_type != $\" + strconv.Itoa(len(args))\n\t\t}\n\t}\n\n\trows, err := s.db.QueryContext(ctx, `\n\t\tSELECT\n\t\t    trace_id, span_id, started_at, span_type, is_root, service_name, endpoint_name,\n\t\t    topic_name, subscription_name, message_id, is_error, test_skipped, duration_nanos,\n\t\t\tsrc_file, src_line, parent_span_id, caller_event_id\n\t\tFROM trace_span_index\n\t\tWHERE app_id = $1 AND has_response AND is_root AND span_type != $2 `+extraWhereClause+`\n\t\tORDER BY started_at DESC\n\t\tLIMIT `+strconv.Itoa(limit)+`\n\t`, args...)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"query traces\")\n\t}\n\n\tdefer fns.CloseIgnore(rows)\n\tn := 0\n\tfor rows.Next() {\n\t\tif n >= limit {\n\t\t\tbreak\n\t\t}\n\t\tn++\n\n\t\tvar t tracepb2.SpanSummary\n\t\tvar startedAt int64\n\t\terr := rows.Scan(\n\t\t\t&t.TraceId, &t.SpanId, &startedAt, &t.Type, &t.IsRoot, &t.ServiceName, &t.EndpointName,\n\t\t\t&t.TopicName, &t.SubscriptionName, &t.MessageId, &t.IsError, &t.TestSkipped,\n\t\t\t&t.DurationNanos, &t.SrcFile, &t.SrcLine, &t.ParentSpanId, &t.CallerEventId)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"scan trace\")\n\t\t}\n\t\tts := time.Unix(0, startedAt)\n\t\tt.StartedAt = timestamppb.New(ts)\n\n\t\tif !iter(&t) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn errors.Wrap(rows.Err(), \"iterate traces\")\n}\n\n// emitCompleteSpanToListeners emits the given trace/span to all listeners\n// if it's a complete root span (meaning it has a response and is not an auth span).\nfunc (s *Store) emitCompleteSpanToListeners(ctx context.Context, appID, traceID, spanID string) {\n\tvar t tracepb2.SpanSummary\n\tvar startedAt int64\n\terr := s.db.QueryRowContext(ctx, `\n\t\tSELECT\n\t\t\ttrace_id, span_id, started_at, span_type, is_root, service_name, endpoint_name,\n\t\t\ttopic_name, subscription_name, message_id, is_error, test_skipped, duration_nanos,\n\t\t\tsrc_file, src_line, parent_span_id, caller_event_id\n\t\tFROM trace_span_index\n\t\tWHERE app_id = ? AND trace_id = ? AND span_id = ? AND has_response AND is_root AND span_type != ?\n\t\tORDER BY started_at DESC\n\t`, appID, traceID, spanID, tracepb2.SpanSummary_AUTH).Scan(\n\t\t&t.TraceId, &t.SpanId, &startedAt, &t.Type, &t.IsRoot, &t.ServiceName, &t.EndpointName,\n\t\t&t.TopicName, &t.SubscriptionName, &t.MessageId, &t.IsError, &t.TestSkipped,\n\t\t&t.DurationNanos, &t.SrcFile, &t.SrcLine, &t.ParentSpanId, &t.CallerEventId)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn\n\t} else if err != nil {\n\t\tlog.Error().Err(err).Msg(\"unable to query trace span\")\n\t\treturn\n\t}\n\n\tts := time.Unix(0, startedAt)\n\tt.StartedAt = timestamppb.New(ts)\n\tfor _, ln := range s.listeners {\n\t\tln <- trace2.NewSpanEvent{\n\t\t\tAppID:     appID,\n\t\t\tTestTrace: t.Type == tracepb2.SpanSummary_TEST,\n\t\t\tSpan:      &t,\n\t\t}\n\t}\n}\n\nfunc (s *Store) Get(ctx context.Context, appID, traceID string, iter trace2.EventIterator) error {\n\trows, err := s.db.QueryContext(ctx, `\n\t\tSELECT event_data\n\t\tFROM trace_event\n\t\tWHERE app_id = ? AND trace_id = ?\n\t`, appID, traceID)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"get trace\")\n\t}\n\n\tdefer fns.CloseIgnore(rows)\n\thasRows := false\n\tfor rows.Next() {\n\t\thasRows = true\n\t\tvar data []byte\n\t\terr := rows.Scan(&data)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"scan trace data\")\n\t\t}\n\n\t\tvar ev tracepb2.TraceEvent\n\t\tif err := protojson.Unmarshal(data, &ev); err != nil {\n\t\t\treturn errors.Wrap(err, \"unmarshal trace event\")\n\t\t}\n\t\tif !iter(&ev) {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn errors.Wrap(err, \"iterate events\")\n\t} else if !hasRows {\n\t\treturn trace2.ErrNotFound\n\t}\n\treturn nil\n}\n\nfunc (s *Store) GetSpanSummaries(ctx context.Context, appID, traceID string) ([]*tracepb2.SpanSummary, error) {\n\trows, err := s.db.QueryContext(ctx, `\n        SELECT\n            trace_id, span_id, started_at, span_type, is_root, service_name, endpoint_name,\n            topic_name, subscription_name, message_id, is_error, test_skipped, duration_nanos,\n            src_file, src_line, parent_span_id, caller_event_id\n        FROM trace_span_index\n        WHERE app_id = ? AND trace_id = ?\n        ORDER BY started_at ASC\n    `, appID, traceID)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"query span summaries\")\n\t}\n\tdefer fns.CloseIgnore(rows)\n\n\tvar summaries []*tracepb2.SpanSummary\n\tfor rows.Next() {\n\t\tvar t tracepb2.SpanSummary\n\t\tvar startedAt int64\n\t\terr := rows.Scan(\n\t\t\t&t.TraceId, &t.SpanId, &startedAt, &t.Type, &t.IsRoot, &t.ServiceName, &t.EndpointName,\n\t\t\t&t.TopicName, &t.SubscriptionName, &t.MessageId, &t.IsError, &t.TestSkipped,\n\t\t\t&t.DurationNanos, &t.SrcFile, &t.SrcLine, &t.ParentSpanId, &t.CallerEventId)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"scan span summary\")\n\t\t}\n\t\tts := time.Unix(0, startedAt)\n\t\tt.StartedAt = timestamppb.New(ts)\n\t\tsummaries = append(summaries, &t)\n\t}\n\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"iterate span summaries\")\n\t}\n\treturn summaries, nil\n}\n\nfunc (s *Store) GetEvents(ctx context.Context, appID, traceID, spanID string) ([]*tracepb2.TraceEvent, error) {\n\trows, err := s.db.QueryContext(ctx, `\n        SELECT event_data\n        FROM trace_event\n        WHERE app_id = ? AND trace_id = ? AND span_id = ?\n    `, appID, traceID, spanID)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"get span events\")\n\t}\n\tdefer fns.CloseIgnore(rows)\n\n\tvar events []*tracepb2.TraceEvent\n\tfor rows.Next() {\n\t\tvar data []byte\n\t\tif err := rows.Scan(&data); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"scan event data\")\n\t\t}\n\t\tvar ev tracepb2.TraceEvent\n\t\tif err := protojson.Unmarshal(data, &ev); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"unmarshal trace event\")\n\t\t}\n\t\tevents = append(events, &ev)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"iterate events\")\n\t}\n\treturn events, nil\n}\n"
  },
  {
    "path": "cli/daemon/engine/trace2/sqlite/write.go",
    "content": "package sqlite\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/base32\"\n\t\"encoding/binary\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/lib/pq\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/pkg/fns\"\n\ttracepbcli \"encr.dev/proto/encore/engine/trace2\"\n)\n\n// New creates a new store backed by the given db.\nfunc New(db *sql.DB) *Store {\n\treturn &Store{\n\t\tdb: db,\n\t}\n}\n\ntype Store struct {\n\tdb        *sql.DB\n\tlisteners []chan<- trace2.NewSpanEvent\n}\n\nvar _ trace2.Store = (*Store)(nil)\n\nfunc scanRows[T any](rows *sql.Rows) ([]T, error) {\n\tdefer rows.Close()\n\tvar out []T\n\tfor rows.Next() {\n\t\tvar v T\n\t\terr := rows.Scan(&v)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tout = append(out, v)\n\t}\n\treturn out, nil\n}\n\nfunc (s *Store) CleanEvery(ctx context.Context, freq time.Duration, triggerAt, eventsToKeep, batchSize int) {\n\tfor {\n\t\ttimer := time.NewTimer(freq)\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-timer.C:\n\t\t\tif err := s.DoClean(ctx, triggerAt, eventsToKeep, batchSize); err != nil {\n\t\t\t\tlog.Error().Err(err).Msg(\"trace cleanup failed\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (s *Store) DoClean(ctx context.Context, triggerAt, eventsToKeep, batchSize int) error {\n\tlog.Info().Msg(\"initiating trace event cleanup sweep\")\n\trows, err := s.db.QueryContext(ctx, \"SELECT app_id FROM trace_event GROUP BY app_id HAVING COUNT(distinct trace_id) > ?\", triggerAt)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"query app ids\")\n\t}\n\tappIDs, err := scanRows[string](rows)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"scan app ids\")\n\t}\n\n\tfor _, appID := range appIDs {\n\t\trow := s.db.QueryRowContext(ctx, `\n\t\t\t\t\t\tWITH latest_events AS (\n\t\t\t\t\t\t\tSELECT trace_id, min(id) as id FROM trace_event WHERE app_id = ? GROUP BY 1 ORDER BY 2 DESC LIMIT ?\n\t\t\t\t\t\t) SELECT min(id) FROM latest_events;\n\t\t\t\t\t`, appID, eventsToKeep)\n\t\tvar traceID int64\n\t\terr := row.Scan(&traceID)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to get trace id\")\n\t\t\tcontinue\n\t\t}\n\t\trows, err := s.db.QueryContext(ctx, \"SELECT DISTINCT trace_id FROM trace_event WHERE app_id = ? AND id < ? ORDER BY id DESC LIMIT ?\", appID, traceID, batchSize)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to get old trace ids\")\n\t\t\tcontinue\n\t\t}\n\t\ttraceIDs, err := scanRows[string](rows)\n\t\tif len(traceIDs) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tidArgs := strings.Join(fns.Map(traceIDs, pq.QuoteLiteral), \",\")\n\t\tres, err := s.db.ExecContext(ctx, \"DELETE FROM trace_event WHERE app_id = ? AND trace_id IN (\"+idArgs+\")\", appID)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to delete old trace events\")\n\t\t\tcontinue\n\t\t}\n\t\trowCount, err := res.RowsAffected()\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to get rows affected\")\n\t\t\tcontinue\n\t\t}\n\t\tlog.Info().Str(\"app_id\", appID).Int64(\"deleted\", rowCount).Msg(\"cleaned up old trace events\")\n\t\tres, err = s.db.ExecContext(ctx, \"DELETE FROM trace_span_index WHERE app_id = ? AND trace_id IN (\"+idArgs+\")\", appID)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to delete old trace spans\")\n\t\t\tcontinue\n\t\t}\n\t\trowCount, err = res.RowsAffected()\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to get rows affected\")\n\t\t\tcontinue\n\t\t}\n\t\tlog.Info().Str(\"app_id\", appID).Int64(\"deleted\", rowCount).Msg(\"cleaned up old trace spans\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) Listen(ch chan<- trace2.NewSpanEvent) {\n\ts.listeners = append(s.listeners, ch)\n}\n\nfunc (s *Store) Clear(ctx context.Context, appID string) error {\n\t_, err := s.db.ExecContext(ctx, \"DELETE FROM trace_event WHERE app_id = ?\", appID)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to clear trace events\")\n\t}\n\t_, err = s.db.ExecContext(ctx, \"DELETE FROM trace_span_index WHERE app_id = ?\", appID)\n\treturn errors.Wrap(err, \"failed to clear trace spans\")\n}\n\nfunc (s *Store) WriteEvents(ctx context.Context, meta *trace2.Meta, events []*tracepbcli.TraceEvent) error {\n\tfor _, ev := range events {\n\t\tif err := s.insertEvent(ctx, meta, ev); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"unable to insert trace span event\")\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) insertEvent(ctx context.Context, meta *trace2.Meta, ev *tracepbcli.TraceEvent) error {\n\tdata, err := protojson.Marshal(ev)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"marshal trace event\")\n\t}\n\n\t_, err = s.db.ExecContext(ctx, `\n\t\tINSERT INTO trace_event (\n\t\t\tapp_id, trace_id, span_id, event_data)\n\t\tVALUES (?, ?, ?, ?)\n\t`, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId), data)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t}\n\n\tif start := ev.GetSpanStart(); start != nil {\n\t\tif err := s.updateSpanStartIndex(ctx, meta, ev, start); err != nil {\n\t\t\treturn errors.Wrap(err, \"update span start index\")\n\t\t}\n\t} else if end := ev.GetSpanEnd(); end != nil {\n\t\tif err := s.updateSpanEndIndex(ctx, meta, ev, end); err != nil {\n\t\t\treturn errors.Wrap(err, \"update span end index\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) updateSpanStartIndex(ctx context.Context, meta *trace2.Meta, ev *tracepbcli.TraceEvent, start *tracepbcli.SpanStart) error {\n\tisRoot := start.ParentSpanId == nil\n\tif req := start.GetRequest(); req != nil {\n\t\textRequestID := req.RequestHeaders[http.CanonicalHeaderKey(\"X-Request-ID\")]\n\t\tvar parentSpanID *string\n\t\tif start.ParentSpanId != nil {\n\t\t\tencodedParentSpanID := encodeSpanID(*start.ParentSpanId)\n\t\t\tparentSpanID = &encodedParentSpanID\n\t\t}\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, started_at, is_root, service_name, endpoint_name, external_request_id, parent_span_id, caller_event_id,\n\t\t\t\thas_response, test_skipped\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\tis_root = excluded.is_root,\n\t\t\t\tservice_name = excluded.service_name,\n\t\t\t\tendpoint_name = excluded.endpoint_name,\n\t\t\t\texternal_request_id = excluded.external_request_id,\n\t\t\t\tparent_span_id = excluded.parent_span_id,\n\t\t\t\tcaller_event_id = excluded.caller_event_id\n\t\t`, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId),\n\t\t\ttracepbcli.SpanSummary_REQUEST, ev.EventTime.AsTime().UnixNano(),\n\t\t\tisRoot, req.ServiceName, req.EndpointName, extRequestID, parentSpanID, start.CallerEventId)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif auth := start.GetAuth(); auth != nil {\n\t\tvar parentSpanID *string\n\t\tif start.ParentSpanId != nil {\n\t\t\tencodedParentSpanID := encodeSpanID(*start.ParentSpanId)\n\t\t\tparentSpanID = &encodedParentSpanID\n\t\t}\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, started_at, is_root, service_name, endpoint_name, parent_span_id, caller_event_id,\n\t\t\t\thas_response, test_skipped\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\tis_root = excluded.is_root,\n\t\t\t\tservice_name = excluded.service_name,\n\t\t\t\tendpoint_name = excluded.endpoint_name,\n\t\t\t\tparent_span_id = excluded.parent_span_id,\n\t\t\t\tcaller_event_id = excluded.caller_event_id\n\t\t`, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId),\n\t\t\ttracepbcli.SpanSummary_AUTH, ev.EventTime.AsTime().UnixNano(),\n\t\t\tisRoot, auth.ServiceName, auth.EndpointName, parentSpanID, start.CallerEventId)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif msg := start.GetPubsubMessage(); msg != nil {\n\t\tvar parentSpanID *string\n\t\tif start.ParentSpanId != nil {\n\t\t\tencodedParentSpanID := encodeSpanID(*start.ParentSpanId)\n\t\t\tparentSpanID = &encodedParentSpanID\n\t\t}\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, started_at, is_root, service_name,\n\t\t\t\ttopic_name, subscription_name, message_id, parent_span_id, caller_event_id,\n\t\t\t\thas_response, test_skipped\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\tis_root = excluded.is_root,\n\t\t\t\tservice_name = excluded.service_name,\n\t\t\t\ttopic_name = excluded.topic_name,\n\t\t\t\tsubscription_name = excluded.subscription_name,\n\t\t\t\tmessage_id = excluded.message_id,\n\t\t\t\tparent_span_id = excluded.parent_span_id,\n\t\t\t\tcaller_event_id = excluded.caller_event_id\n\t\t`, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId),\n\t\t\ttracepbcli.SpanSummary_PUBSUB_MESSAGE, ev.EventTime.AsTime().UnixNano(),\n\t\t\tisRoot, msg.ServiceName, msg.TopicName, msg.SubscriptionName, msg.MessageId, parentSpanID, start.CallerEventId)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif msg := start.GetTest(); msg != nil {\n\t\tvar parentSpanID *string\n\t\tif start.ParentSpanId != nil {\n\t\t\tencodedParentSpanID := encodeSpanID(*start.ParentSpanId)\n\t\t\tparentSpanID = &encodedParentSpanID\n\t\t}\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, started_at, is_root, service_name, endpoint_name, user_id, src_file, src_line, parent_span_id, caller_event_id,\n\t\t\t\thas_response, test_skipped\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, false, false)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\tis_root = excluded.is_root,\n\t\t\t\tservice_name = excluded.service_name,\n\t\t\t\tendpoint_name = excluded.endpoint_name,\n\t\t\t\tparent_span_id = excluded.parent_span_id,\n\t\t\t\tcaller_event_id = excluded.caller_event_id\n\t\t`, meta.AppID, encodeTraceID(ev.TraceId), encodeSpanID(ev.SpanId),\n\t\t\ttracepbcli.SpanSummary_TEST, ev.EventTime.AsTime().UnixNano(),\n\t\t\tisRoot, msg.ServiceName, msg.TestName, msg.Uid, msg.TestFile, msg.TestLine, parentSpanID, start.CallerEventId)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\nfunc (s *Store) updateSpanEndIndex(ctx context.Context, meta *trace2.Meta, ev *tracepbcli.TraceEvent, end *tracepbcli.SpanEnd) (err error) {\n\ttraceID := encodeTraceID(ev.TraceId)\n\tspanID := encodeSpanID(ev.SpanId)\n\n\tdefer func() {\n\t\tif err == nil {\n\t\t\t// If the span is complete, emit it to listeners.\n\t\t\ts.emitCompleteSpanToListeners(ctx, meta.AppID, traceID, spanID)\n\t\t}\n\t}()\n\n\tif req := end.GetRequest(); req != nil {\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, has_response, is_error, duration_nanos, caller_event_id\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\thas_response = excluded.has_response,\n\t\t\t\tis_error = excluded.is_error,\n\t\t\t\tduration_nanos = excluded.duration_nanos,\n\t\t\t\tcaller_event_id = excluded.caller_event_id\n\t\t`, meta.AppID, traceID, spanID,\n\t\t\ttracepbcli.SpanSummary_REQUEST, true,\n\t\t\tend.Error != nil, end.DurationNanos, req.CallerEventId,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif auth := end.GetAuth(); auth != nil {\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, has_response, is_error, duration_nanos, user_id\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\thas_response = excluded.has_response,\n\t\t\t\tis_error = excluded.is_error,\n\t\t\t\tduration_nanos = excluded.duration_nanos,\n\t\t\t\tuser_id = excluded.user_id\n\t\t`, meta.AppID, traceID, spanID,\n\t\t\ttracepbcli.SpanSummary_AUTH, true,\n\t\t\tend.Error != nil, end.DurationNanos, auth.Uid)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif msg := end.GetPubsubMessage(); msg != nil {\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, has_response, is_error, duration_nanos\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\thas_response = excluded.has_response,\n\t\t\t\tis_error = excluded.is_error,\n\t\t\t\tduration_nanos = excluded.duration_nanos\n\t\t`, meta.AppID, traceID, spanID,\n\t\t\ttracepbcli.SpanSummary_PUBSUB_MESSAGE, true,\n\t\t\tend.Error != nil, end.DurationNanos)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tif msg := end.GetTest(); msg != nil {\n\t\t_, err := s.db.ExecContext(ctx, `\n\t\t\tINSERT INTO trace_span_index (\n\t\t\t\tapp_id, trace_id, span_id, span_type, has_response, is_error, test_skipped, duration_nanos\n\t\t\t) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\tON CONFLICT (trace_id, span_id) DO UPDATE SET\n\t\t\t\thas_response = excluded.has_response,\n\t\t\t\tis_error = excluded.is_error,\n\t\t\t\ttest_skipped = excluded.test_skipped,\n\t\t\t\tduration_nanos = excluded.duration_nanos\n\t\t`, meta.AppID, traceID, spanID,\n\t\t\ttracepbcli.SpanSummary_TEST, true,\n\t\t\tmsg.Failed, msg.Skipped, end.DurationNanos)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"insert trace span event\")\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\nvar (\n\tbinBE = binary.BigEndian\n\tbinLE = binary.LittleEndian\n)\n\n// encodeTraceID encodes the trace id as a human-readable string.\nfunc encodeTraceID(id *tracepbcli.TraceID) string {\n\tvar b [16]byte\n\tbinLE.PutUint64(b[0:8], id.Low)\n\tbinLE.PutUint64(b[8:16], id.High)\n\treturn base32hex.EncodeToString(b[:])\n}\n\n// encodeSpanID encodes the span id as a human-readable string.\nfunc encodeSpanID(id uint64) string {\n\tvar b [8]byte\n\tbinLE.PutUint64(b[:], id)\n\treturn base32hex.EncodeToString(b[:])\n}\n\nvar (\n\t// base32hex is a lowercase base32 hex encoding without padding\n\t// that preserves lexicographic sort order.\n\tbase32hex = base32.NewEncoding(\"0123456789abcdefghijklmnopqrstuv\").WithPadding(base32.NoPadding)\n)\n"
  },
  {
    "path": "cli/daemon/engine/trace2/store.go",
    "content": "package trace2\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n)\n\ntype Meta struct {\n\tAppID string\n}\n\ntype Query struct {\n\tAppID        string\n\tService      string\n\tEndpoint     string\n\tTopic        string\n\tSubscription string\n\tTraceID      string\n\tMessageID    string\n\tTestFilter   *bool // nil means both test and non-test traces are returned\n\tTags         []Tag\n\n\t// StartTime and EndTime specify the time range to query.\n\t// If zero values they are not bounded.\n\tStartTime, EndTime time.Time\n\n\tIsError *bool // nil means both successes and failures are returned\n\n\t// Minimum and maximum duration (in nanoseconds) to filter requests for.\n\t// If MaxDurMicros is 0 it defaults to no limit.\n\tMinDurNanos, MaxDurNanos uint64\n\n\tLimit int // if 0 defaults to 100.\n}\n\ntype Tag struct {\n\tKey   string\n\tValue string\n}\n\n// ErrNotFound is reported by Store.Get when a trace is not found.\nvar ErrNotFound = errors.New(\"trace not found\")\n\n// A ListEntryIterator is called once for each trace matching the query string,\n// sequentially and in streaming fashion as traces are read from the store.\n//\n// If it returns false the listing operation is stopped and the function is\n// not called again.\ntype ListEntryIterator func(*tracepb2.SpanSummary) bool\n\n// An EventIterator is called once for each event in a trace,\n// sequentially and in streaming fashion as events are read from the store.\n//\n// If it returns false the stream is aborted and the function is\n// not called again.\ntype EventIterator func(*tracepb2.TraceEvent) bool\n\n// Store is the interface for storing and retrieving traces.\ntype Store interface {\n\t// WriteEvents persists requests in the store.\n\tWriteEvents(ctx context.Context, meta *Meta, events []*tracepb2.TraceEvent) error\n\n\t// List lists traces that match the query.\n\t// It calls fn for each trace read; see ListEntryIterator.\n\tList(ctx context.Context, q *Query, iter ListEntryIterator) error\n\n\t// Get streams events matching the given trace id.\n\t// fn may be called with events out of order.\n\t// If the trace is not found it reports an error matching ErrNotFound.\n\tGet(ctx context.Context, appID, traceID string, iter EventIterator) error\n\n\t// GetSpanSummaries returns all span summaries for a trace.\n\tGetSpanSummaries(ctx context.Context, appID, traceID string) ([]*tracepb2.SpanSummary, error)\n\t// GetEvents returns events for a specific span.\n\tGetEvents(ctx context.Context, appID, traceID, spanID string) ([]*tracepb2.TraceEvent, error)\n\n\t// Listen listens for new spans.\n\tListen(ch chan<- NewSpanEvent)\n\n\t// Clear removes all traces for an app\n\tClear(ctx context.Context, appID string) error\n}\n\ntype NewSpanEvent struct {\n\tAppID     string\n\tTestTrace bool\n\tSpan      *tracepb2.SpanSummary\n}\n"
  },
  {
    "path": "cli/daemon/exec_script.go",
    "content": "package daemon\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/mod/modfile\"\n\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// ExecScript executes a one-off script.\nfunc (s *Server) ExecScript(req *daemonpb.ExecScriptRequest, stream daemonpb.Daemon_ExecScriptServer) error {\n\tctx := stream.Context()\n\tslog := &streamLog{stream: stream, buffered: true}\n\tstderr := slog.Stderr(false)\n\tsendErr := func(err error) {\n\t\tif list := run.AsErrorList(err); list != nil {\n\t\t\t_ = list.SendToStream(stream)\n\t\t} else {\n\t\t\terrStr := err.Error()\n\t\t\tif !strings.HasSuffix(errStr, \"\\n\") {\n\t\t\t\terrStr += \"\\n\"\n\t\t\t}\n\t\t\tslog.Stderr(false).Write([]byte(errStr))\n\t\t}\n\t\tstreamExit(stream, 1)\n\t}\n\n\tctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, req.TraceFile)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\tdefer tracer.Close()\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tns, err := s.namespaceOrActive(ctx, app, req.Namespace)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tops := optracker.New(stderr, stream)\n\tdefer ops.AllDone() // Kill the tracker when we exit this function\n\n\ttestResults := make(chan error, 1)\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\tvar err error\n\t\t\tswitch recovered := recovered.(type) {\n\t\t\tcase error:\n\t\t\t\terr = recovered\n\t\t\tdefault:\n\t\t\t\terr = fmt.Errorf(\"%v\", recovered)\n\t\t\t}\n\t\t\tlog.Err(err).Msg(\"panic during script execution\")\n\t\t\ttestResults <- fmt.Errorf(\"panic occured within Encore during script execution: %v\\n\", recovered)\n\t\t}\n\t}()\n\n\t// Note: TypeScript apps use the ExecSpec RPC instead, which allows\n\t// the CLI to run the command locally with stdin support.\n\tif app.Lang() != appfile.LangGo {\n\t\tsendErr(fmt.Errorf(\"unsupported language for ExecScript: %s\", app.Lang()))\n\t\treturn nil\n\t}\n\n\tmodPath := filepath.Join(app.Root(), \"go.mod\")\n\tmodData, err := os.ReadFile(modPath)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\tmod, err := modfile.Parse(modPath, modData, nil)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tcommandRelPath := filepath.ToSlash(filepath.Join(req.WorkingDir, req.ScriptArgs[0]))\n\tscriptArgs := req.ScriptArgs[1:]\n\tcommandPkg := paths.Pkg(mod.Module.Mod.Path).JoinSlash(paths.RelSlash(commandRelPath))\n\n\tp := run.ExecScriptParams{\n\t\tApp:        app,\n\t\tNS:         ns,\n\t\tWorkingDir: req.WorkingDir,\n\t\tEnviron:    req.Environ,\n\t\tMainPkg:    commandPkg,\n\t\tScriptArgs: scriptArgs,\n\t\tStdout:     slog.Stdout(false),\n\t\tStderr:     slog.Stderr(false),\n\t\tOpTracker:  ops,\n\t}\n\tif err := s.mgr.ExecScript(stream.Context(), p); err != nil {\n\t\tsendErr(err)\n\t} else {\n\t\tstreamExit(stream, 0)\n\t}\n\n\treturn nil\n}\n\n// ExecSpec returns the specification for how to run an exec command,\n// allowing the CLI to execute it directly with stdin support.\n// It streams progress messages during setup, then sends the spec as the final message.\nfunc (s *Server) ExecSpec(req *daemonpb.ExecSpecRequest, stream daemonpb.Daemon_ExecSpecServer) error {\n\tctx := stream.Context()\n\t// Wrap the ExecSpec stream so it can be used with streamLog and optracker,\n\t// which expect a commandStream (Send(*CommandMessage)).\n\tadapter := &execSpecStreamAdapter{stream: stream}\n\tslog := &streamLog{stream: adapter, buffered: true}\n\tstderr := slog.Stderr(false)\n\tsendErr := func(err error) {\n\t\tif list := run.AsErrorList(err); list != nil {\n\t\t\t_ = list.SendToStream(adapter)\n\t\t} else {\n\t\t\terrStr := err.Error()\n\t\t\tif !strings.HasSuffix(errStr, \"\\n\") {\n\t\t\t\terrStr += \"\\n\"\n\t\t\t}\n\t\t\tslog.Stderr(false).Write([]byte(errStr))\n\t\t}\n\t}\n\n\tctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, nil)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\tdefer fns.CloseIgnore(tracer)\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tif app.Lang() != appfile.LangTS {\n\t\tsendErr(errors.New(\"exec spec is only supported for TypeScript apps\"))\n\t\treturn nil\n\t}\n\n\tns, err := s.namespaceOrActive(ctx, app, req.Namespace)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tops := optracker.New(stderr, adapter)\n\tdefer ops.AllDone()\n\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\tvar panicErr error\n\t\t\tswitch recovered := recovered.(type) {\n\t\t\tcase error:\n\t\t\t\tpanicErr = recovered\n\t\t\tdefault:\n\t\t\t\tpanicErr = fmt.Errorf(\"%+v\", recovered)\n\t\t\t}\n\t\t\tstack := debug.Stack()\n\t\t\tlog.Err(panicErr).Msgf(\"panic during exec spec:\\n%s\", stack)\n\t\t\tsendErr(fmt.Errorf(\"panic during exec spec: %v\", panicErr))\n\t\t}\n\t}()\n\n\tspec, err := s.mgr.ExecSpec(ctx, run.ExecSpecParams{\n\t\tApp:        app,\n\t\tNS:         ns,\n\t\tWorkingDir: req.WorkingDir,\n\t\tEnviron:    req.Environ,\n\t\tCommand:    req.ScriptArgs[0],\n\t\tScriptArgs: req.ScriptArgs[1:],\n\t\tTempDir:    req.TempDir,\n\t\tOpTracker:  ops,\n\t})\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\t// Send the spec as the final message.\n\treturn stream.Send(&daemonpb.ExecSpecMessage{\n\t\tMsg: &daemonpb.ExecSpecMessage_Spec{\n\t\t\tSpec: &daemonpb.ExecSpecResponse{\n\t\t\t\tCommand: spec.Command,\n\t\t\t\tArgs:    spec.Args,\n\t\t\t\tEnviron: spec.Environ,\n\t\t\t},\n\t\t},\n\t})\n}\n\n// execSpecStreamAdapter adapts a Daemon_ExecSpecServer stream to the\n// commandStream interface, wrapping CommandOutput in ExecSpecMessage.\ntype execSpecStreamAdapter struct {\n\tstream daemonpb.Daemon_ExecSpecServer\n}\n\nfunc (a *execSpecStreamAdapter) Send(msg *daemonpb.CommandMessage) error {\n\tswitch m := msg.Msg.(type) {\n\tcase *daemonpb.CommandMessage_Output:\n\t\treturn a.stream.Send(&daemonpb.ExecSpecMessage{\n\t\t\tMsg: &daemonpb.ExecSpecMessage_Output{Output: m.Output},\n\t\t})\n\tcase *daemonpb.CommandMessage_Errors:\n\t\t// Send structured errors as stderr output so the client can display them.\n\t\treturn a.stream.Send(&daemonpb.ExecSpecMessage{\n\t\t\tMsg: &daemonpb.ExecSpecMessage_Output{Output: &daemonpb.CommandOutput{\n\t\t\t\tStderr: m.Errors.Errinsrc,\n\t\t\t}},\n\t\t})\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/export/download.go",
    "content": "package export\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/dockerbuild\"\n)\n\nconst (\n\tDOWNLOAD_BASE_URL = \"https://storage.googleapis.com/encore-optional/encore\"\n)\n\nfunc downloadBinary(platform, arch string, binary string, log zerolog.Logger) (dockerbuild.HostPath, error) {\n\tif version.Channel == version.DevBuild {\n\t\tsuffix := \"\"\n\t\tif platform != runtime.GOOS || arch != runtime.GOARCH {\n\t\t\tsuffix = \"-\" + platform + \"-\" + arch\n\t\t}\n\t\tif binary == \"encore-runtime.node\" {\n\t\t\tbinary = \"js/\" + binary\n\t\t}\n\t\tpath := filepath.Join(env.EncoreRuntimesPath(), binary+suffix)\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\treturn dockerbuild.HostPath(path), nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"development build of %s/%s %s not found at %s. Build it with `go run ./pkg/encorebuild/cmd/build-local-binary %[3]s --os=%[1]s --arch=%[2]s`\", platform, arch, binary, path)\n\t}\n\tcacheDir, err := conf.CacheDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbinDir := dockerbuild.HostPath(cacheDir).Join(\"bin\")\n\tarchDir := binDir.Join(version.Version, platform, arch)\n\tbinaryPath := archDir.Join(binary)\n\tif _, err := os.Stat(binaryPath.String()); err == nil {\n\t\treturn binaryPath, nil\n\t}\n\tif err := os.MkdirAll(archDir.String(), 0755); err != nil {\n\t\treturn \"\", err\n\t}\n\t// Download the binaries\n\tarchURL := fmt.Sprintf(\"%s/%s/%s-%s\", DOWNLOAD_BASE_URL, version.Version, platform, arch)\n\turl := fmt.Sprintf(\"%s/%s\", archURL, binary)\n\tlog.Info().Msgf(\"Downloading %s/%s %s\", platform, arch, binary)\n\tif err := downloadFile(url, binaryPath.String()); err != nil {\n\t\treturn \"\", err\n\t}\n\ttryCleanupPreviousVersions(binDir)\n\treturn binaryPath, nil\n}\n\nfunc tryCleanupPreviousVersions(binDir dockerbuild.HostPath) {\n\t// Clean up binaries for other versions\n\tentries, err := os.ReadDir(binDir.String())\n\tif err != nil {\n\t\tlog.Warn().Msgf(\"failed to read directory %s: %v\", binDir, err)\n\t\treturn\n\t}\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() && entry.Name() != version.Version {\n\t\t\toldVersionPath := filepath.Join(binDir.String(), entry.Name())\n\t\t\tif err := os.RemoveAll(oldVersionPath); err != nil {\n\t\t\t\tlog.Warn().Msgf(\"failed to remove old version directory %s: %v\", oldVersionPath, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc downloadFile(url, dest string) error {\n\t// Download the file to a temporary destination\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"failed to download %s: %s\", url, resp.Status)\n\t}\n\n\ttmpDest := dest + \".tmp\"\n\tout, err := os.OpenFile(tmpDest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t_, err = io.Copy(out, resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tout.Close()\n\n\t// Download the checksum\n\tsha256url := url + \".sha256\"\n\tresp, err = http.Get(sha256url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"failed to download %s: %s\", sha256url, resp.Status)\n\t}\n\thash, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Validate the checksum\n\tif err := validateHash(tmpDest, string(hash)); err != nil {\n\t\treturn err\n\t}\n\n\t// Move the file\n\tif err := os.Rename(tmpDest, dest); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc validateHash(file, hash string) error {\n\tf, err := os.Open(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\th := sha256.New()\n\tif _, err := io.Copy(h, f); err != nil {\n\t\treturn err\n\t}\n\tif fileHash := hex.EncodeToString(h.Sum(nil)); hash != fileHash {\n\t\treturn fmt.Errorf(\"file checksum failed. Expected %s, got %s\", hash, fileHash)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/export/export.go",
    "content": "package export\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/google/go-containerregistry/pkg/authn\"\n\t\"github.com/google/go-containerregistry/pkg/name\"\n\tv1 \"github.com/google/go-containerregistry/pkg/v1\"\n\t\"github.com/google/go-containerregistry/pkg/v1/daemon\"\n\t\"github.com/google/go-containerregistry/pkg/v1/empty\"\n\t\"github.com/google/go-containerregistry/pkg/v1/remote\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/internal/runlog\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/dockerbuild\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/vcs\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Docker exports the app as a docker image.\nfunc Docker(ctx context.Context, app *apps.Instance, req *daemonpb.ExportRequest, log zerolog.Logger, streamLog runlog.Log) (success bool, err error) {\n\tparams := req.GetDocker()\n\tif params == nil {\n\t\treturn false, errors.Newf(\"unsupported format: %T\", req.Format)\n\t}\n\n\texpSet, err := app.Experiments(req.Environ)\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"get experimental features\")\n\t}\n\n\tvcsRevision := vcs.GetRevision(app.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          []string{\"timetzdata\"},\n\t\tCgoEnabled:         req.CgoEnabled,\n\t\tStaticLink:         true,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            req.Environ,\n\t\tGOOS:               req.Goos,\n\t\tGOARCH:             req.Goarch,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\tappLang := app.Lang()\n\tbld := builderimpl.Resolve(appLang, expSet)\n\tdefer fns.CloseIgnore(bld)\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        app,\n\t\tWorkingDir: \".\",\n\t})\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\thooks, err := app.Hooks()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif hooks.PreBuild.IsSet() {\n\t\tif err := executeHook(ctx, hooks.PreBuild, app.Root(), streamLog); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         app,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif err := app.CacheMetadata(parse.Meta); err != nil {\n\t\tlog.Info().Err(err).Msg(\"failed to cache metadata\")\n\t\treturn false, errors.Wrap(err, \"cache metadata\")\n\t}\n\n\tlog.Info().Msgf(\"compiling Encore application for %s/%s\", req.Goos, req.Goarch)\n\tresult, err := bld.Compile(ctx, builder.CompileParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         app,\n\t\tParse:       parse,\n\t\tOpTracker:   nil, // TODO\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t})\n\n\tif err != nil {\n\t\tlog.Info().Err(err).Msg(\"compilation failed\")\n\t\treturn false, errors.Wrap(err, \"compilation failed\")\n\t}\n\n\tif hooks.PostBuild.IsSet() {\n\t\tif err := executeHook(ctx, hooks.PostBuild, app.Root(), streamLog); err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tvar crossNodeRuntime option.Option[dockerbuild.HostPath]\n\tif appLang == appfile.LangTS && buildInfo.IsCrossBuild() {\n\t\tbinary, err := downloadBinary(req.Goos, req.Goarch, \"encore-runtime.node\", log)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"download runtime binaries\")\n\t\t}\n\t\tcrossNodeRuntime = option.Some(binary)\n\t}\n\n\tbuildSettings, err := app.BuildSettings()\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"get build settings\")\n\t}\n\n\tdescribeCfg := dockerbuild.DescribeConfig{\n\t\tMeta:              parse.Meta,\n\t\tCompile:           result,\n\t\tBundleSource:      option.Option[dockerbuild.BundleSourceSpec]{},\n\t\tDockerBaseImage:   option.AsOptional(params.BaseImageTag),\n\t\tRuntimes:          dockerbuild.HostPath(env.EncoreRuntimesPath()),\n\t\tNodeRuntime:       crossNodeRuntime,\n\t\tProcessPerService: buildSettings.Docker.ProcessPerService,\n\t}\n\n\tif buildSettings.Docker.BundleSource || appLang == appfile.LangTS {\n\t\tworkspaceRoot := req.WorkspaceRoot\n\t\tappRoot := app.Root()\n\n\t\trelPath, err := filepath.Rel(workspaceRoot, appRoot)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"relative path from workspace root to app root\")\n\t\t}\n\n\t\trelPath = filepath.ToSlash(relPath)\n\n\t\tincludedPaths, err := dockerbuild.DetermineIncludes(appLang, buildSettings.Docker.BundleSource, workspaceRoot, appRoot)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"determine extra includes\")\n\t\t}\n\n\t\timageAppRoot := dockerbuild.ImagePath(\"/workspace\").Join(relPath)\n\n\t\tdescribeCfg.BundleSource = option.Some(dockerbuild.BundleSourceSpec{\n\t\t\tSource:         dockerbuild.HostPath(workspaceRoot),\n\t\t\tDest:           \"/workspace\",\n\t\t\tAppRootRelpath: dockerbuild.RelPath(relPath),\n\t\t\tIncludeSource:  includedPaths,\n\t\t\tExcludeSource: []dockerbuild.RelPath{\n\t\t\t\t\".git\",\n\t\t\t},\n\t\t})\n\n\t\tif describeCfg.WorkingDir.Empty() {\n\t\t\t// Set the working directory to app root by default.\n\t\t\tdescribeCfg.WorkingDir = option.Some(imageAppRoot)\n\t\t}\n\t}\n\n\tspec, err := dockerbuild.Describe(describeCfg)\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"describe docker image\")\n\t}\n\n\tcors, err := app.GlobalCORS()\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"get global CORS\")\n\t}\n\tvar logResponse string\n\tif !req.SkipInfraConf {\n\t\tcfg, infraCfgOutput, err := buildAndValidateInfraConfig(EmbeddedInfraConfigParams{\n\t\t\tFile:       dockerbuild.HostPath(req.InfraConfPath),\n\t\t\tServices:   req.Services,\n\t\t\tGateways:   req.Gateways,\n\t\t\tGlobalCORS: cors,\n\t\t\tMeta:       parse.Meta,\n\t\t})\n\t\tlogResponse = infraCfgOutput\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"build infra config\")\n\t\t}\n\t\tdata, err := json.Marshal(cfg)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"marshal infra config\")\n\t\t}\n\t\tspec.WriteFiles[defaultInfraConfigPath] = data\n\t\tspec.Env = append(spec.Env, fmt.Sprintf(\"ENCORE_INFRA_CONFIG_PATH=%s\", defaultInfraConfigPath))\n\n\t\t// Validate the service configs.\n\t\tcfgs, err := bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\t\tParse: parse,\n\t\t\tCueMeta: &cueutil.Meta{\n\t\t\t\tAPIBaseURL: cfg.Metadata.BaseURL,\n\t\t\t\tEnvName:    cfg.Metadata.EnvName,\n\t\t\t\tEnvType:    orDefault(cueutil.EnvType(cfg.Metadata.EnvType), \"development\"),\n\t\t\t\tCloudType:  orDefault(cueutil.CloudType(cfg.Metadata.Cloud), \"local\"),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tfor svcName, cfgStr := range cfgs.Configs {\n\t\t\tspec.Env = append(spec.Env, fmt.Sprintf(\n\t\t\t\t\"%s%s=%s\",\n\t\t\t\t\"ENCORE_CFG_\",\n\t\t\t\tstrings.ToUpper(svcName),\n\t\t\t\tbase64.RawURLEncoding.EncodeToString([]byte(cfgStr)),\n\t\t\t))\n\t\t}\n\t}\n\tvar baseImgOverride option.Option[v1.Image]\n\tif params.BaseImageTag != \"\" {\n\t\tbaseImg, err := resolveBaseImage(ctx, log, params, spec)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"resolve base image\")\n\t\t}\n\t\tbaseImgOverride = option.Some(baseImg)\n\t}\n\n\tvar supervisorPath option.Option[dockerbuild.HostPath]\n\tif spec.Supervisor.Present() {\n\t\tbinary, err := downloadBinary(req.Goos, req.Goarch, \"supervisor-encore\", log)\n\t\tif err != nil {\n\t\t\treturn false, errors.Wrap(err, \"download supervisor binaries\")\n\t\t}\n\t\tsupervisorPath = option.Some(binary)\n\t}\n\timg, err := dockerbuild.BuildImage(ctx, spec, dockerbuild.ImageBuildConfig{\n\t\tBuildTime:         time.Now(),\n\t\tBaseImageOverride: baseImgOverride,\n\t\tAddCACerts:        option.Some[dockerbuild.ImagePath](\"\"),\n\t\tSupervisorPath:    supervisorPath,\n\t})\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"build docker image\")\n\t}\n\n\tif params.LocalDaemonTag != \"\" {\n\t\ttag, err := name.NewTag(params.LocalDaemonTag, name.WeakValidation)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"invalid image tag\")\n\t\t\treturn false, nil\n\t\t}\n\t\tlog.Info().Msg(\"saving image to local docker daemon\")\n\n\t\t_, err = daemon.Write(tag, img, daemon.WithUnbufferedOpener())\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"unable to save docker image\")\n\t\t\treturn false, nil\n\t\t}\n\t\tlog.Info().Msg(\"successfully saved local docker image\")\n\t}\n\n\tif params.PushDestinationTag != \"\" {\n\t\ttag, err := name.NewTag(params.PushDestinationTag, name.WeakValidation)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"invalid image tag\")\n\t\t\treturn false, nil\n\t\t}\n\t\tlog.Info().Msg(\"pushing image to docker registry\")\n\t\tif err := pushDockerImage(ctx, log, img, tag); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"unable to push docker image\")\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tlog.Info().Msgf(\"successfully exported app as docker image\\n%s\", logResponse)\n\treturn true, nil\n}\n\nfunc orDefault[T comparable](value T, defaultValue T) T {\n\tvar zero T\n\tif value == zero {\n\t\treturn defaultValue\n\t}\n\treturn value\n}\n\nfunc resolveBaseImage(ctx context.Context, log zerolog.Logger, p *daemonpb.DockerExportParams, spec *dockerbuild.ImageSpec) (v1.Image, error) {\n\tbaseImgTag := p.BaseImageTag\n\tif baseImgTag == \"\" || baseImgTag == \"scratch\" {\n\t\treturn empty.Image, nil\n\t}\n\n\t// Try to get it from the daemon if it exists.\n\tlog.Info().Msgf(\"resolving base image %s\", baseImgTag)\n\tbaseImgRef, err := name.ParseReference(baseImgTag)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"parse base image\")\n\t}\n\n\tfetchRemote := true\n\timg, err := daemon.Image(baseImgRef, daemon.WithUnbufferedOpener())\n\tif err == nil {\n\t\tfile, err := img.ConfigFile()\n\t\tif err == nil {\n\t\t\tfetchRemote = file.OS != spec.OS || file.Architecture != spec.Arch\n\t\t}\n\t}\n\tif fetchRemote {\n\t\tlog.Info().Msg(\"could not get image from local daemon, fetching it remotely\")\n\t\tkeychain := authn.DefaultKeychain\n\t\timg, err = remote.Image(baseImgRef, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx), remote.WithPlatform(v1.Platform{\n\t\t\tOS:           spec.OS,\n\t\t\tArchitecture: spec.Arch,\n\t\t}))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"unable to fetch image\")\n\t\t}\n\t\t// If the user requested to push the image locally, save the remote image locally as well.\n\t\tif p.LocalDaemonTag != \"\" {\n\t\t\tif tag, err := name.NewTag(baseImgTag, name.WeakValidation); err == nil {\n\t\t\t\tlog.Info().Msgf(\"saving remote image %s to local docker daemon\", baseImgTag)\n\t\t\t\tif _, err = daemon.Write(tag, img); err != nil {\n\t\t\t\t\tlog.Warn().Err(err).Msg(\"unable to save remote image to local docker daemon, skipping\")\n\t\t\t\t} else {\n\t\t\t\t\tlog.Info().Msgf(\"saved remote image to local docker daemon\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn img, nil\n}\n\nfunc pushDockerImage(ctx context.Context, log zerolog.Logger, img v1.Image, destination name.Tag) error {\n\tlog.Info().Msg(\"pushing docker image to container registry\")\n\tkeychain := authn.DefaultKeychain\n\tif err := remote.Write(destination, img, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx)); err != nil {\n\t\treturn errors.WithStack(err)\n\t}\n\tlog.Info().Msg(\"successfully pushed docker image\")\n\treturn nil\n}\n\nfunc executeHook(ctx context.Context, hook appfile.Hook, workingDir string, streamLog runlog.Log) error {\n\tif err := hook.Run(ctx, workingDir, streamLog.Stdout(false), streamLog.Stderr(false)); err != nil {\n\t\treturn errors.Wrap(err, \"execute hook\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/export/infra_config.go",
    "content": "package export\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/logrusorgru/aurora\"\n\t\"github.com/tailscale/hujson\"\n\t\"golang.org/x/exp/maps\"\n\n\t\"encore.dev/appruntime/exported/config/infra\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/dockerbuild\"\n\t\"encr.dev/pkg/fns\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nvar (\n\tLEARN_MORE = aurora.Italic(\"Learn More: https://encore.dev/docs/how-to/self-host\").String()\n)\n\n// defaultInfraConfigPath is the path in the image where the environment configuration is mounted.\nconst defaultInfraConfigPath dockerbuild.ImagePath = \"/encore/infra.config.json\"\n\ntype EmbeddedInfraConfigParams struct {\n\t// The path to the infra config file.\n\tFile dockerbuild.HostPath\n\n\t// Services to include in the image.\n\tServices []string\n\n\t// Gateways to include in the image.\n\tGateways []string\n\n\t// CORS config to include in the image.\n\tGlobalCORS appfile.CORS\n\n\tMeta *meta.Data\n}\n\nfunc buildAndValidateInfraConfig(params EmbeddedInfraConfigParams) (*infra.InfraConfig, string, error) {\n\tmissing := map[string][]string{}\n\tmd := params.Meta\n\tservices := params.Services\n\tgateways := params.Gateways\n\tif len(services)+len(gateways) == 0 {\n\t\tservices = fns.Map(md.Svcs, (*meta.Service).GetName)\n\t\tgateways = fns.Map(md.Gateways, (*meta.Gateway).GetEncoreName)\n\t}\n\n\tunknownServices := fns.Filter(services, func(s string) bool {\n\t\treturn !fns.Any(md.Svcs, func(svc *meta.Service) bool {\n\t\t\treturn svc.Name == s\n\t\t})\n\t})\n\tif len(unknownServices) > 0 {\n\t\treturn nil, \"\", errors.Newf(\"unknown services: %v\", unknownServices)\n\t}\n\n\tunknownGateways := fns.Filter(gateways, func(s string) bool {\n\t\treturn !fns.Any(md.Gateways, func(gw *meta.Gateway) bool {\n\t\t\treturn gw.EncoreName == s\n\t\t})\n\t})\n\tif len(unknownGateways) > 0 {\n\t\treturn nil, \"\", errors.Newf(\"unknown gateways: %v\", unknownGateways)\n\t}\n\n\tvar infraCfg infra.InfraConfig\n\tif params.File != \"\" {\n\t\tdata, err := os.ReadFile(params.File.String())\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Wrap(err, \"infra config not found\")\n\t\t}\n\t\tdata, err = hujson.Standardize(data)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Wrap(err, \"could not standardize infra config\")\n\t\t}\n\t\terr = json.Unmarshal(data, &infraCfg)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.Wrap(err, \"could not decode infra config\")\n\t\t}\n\t}\n\tinfraCfg.HostedGateways = gateways\n\tinfraCfg.HostedServices = services\n\tenvVars, validationErrors := infra.Validate(&infraCfg)\n\n\thostedSvcs := fns.ToMap(fns.Filter(md.Svcs, func(svc *meta.Service) bool {\n\t\treturn fns.Any(services, func(s string) bool {\n\t\t\treturn svc.Name == s\n\t\t})\n\t}), (*meta.Service).GetName)\n\n\tvar secrets []string\n\t// Find all service dependencies for our hosted services.\n\tvar svcDeps = map[string]struct{}{}\n\tpkgs := fns.ToMap(md.Pkgs, (*meta.Package).GetRelPath)\n\n\t// Add dependencies for all outbound RPCs for our hosted services\n\t// and collect all required secrets.\n\tfor _, p := range md.Pkgs {\n\t\tif p.ServiceName == \"\" {\n\t\t\tsecrets = append(secrets, p.Secrets...)\n\t\t\tcontinue\n\t\t} else if _, ok := hostedSvcs[p.ServiceName]; !ok {\n\t\t\tcontinue\n\t\t}\n\t\tsecrets = append(secrets, p.Secrets...)\n\t\tfor _, r := range p.RpcCalls {\n\t\t\tsvcDeps[pkgs[r.Pkg].ServiceName] = struct{}{}\n\t\t}\n\t}\n\n\t// Add auth handler to service discovery if we host any auth RPCs.\n\tif md.AuthHandler != nil {\n\t\trequiresAuth := fns.Any(md.Svcs, func(svc *meta.Service) bool {\n\t\t\treturn fns.Any(svc.Rpcs, func(rpc *meta.RPC) bool {\n\t\t\t\treturn rpc.AccessType == meta.RPC_AUTH\n\t\t\t})\n\t\t})\n\t\tif requiresAuth {\n\t\t\tsvcDeps[md.AuthHandler.ServiceName] = struct{}{}\n\t\t}\n\t}\n\n\t// Make sure we have service discovery for all services if we are hosting gateways.\n\tif len(gateways) > 0 {\n\t\tfor _, svc := range md.Svcs {\n\t\t\tif _, ok := hostedSvcs[svc.Name]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsvcDeps[svc.Name] = struct{}{}\n\t\t}\n\t}\n\n\t// Remove any services that we host from our service dependencies.\n\tfor _, svc := range hostedSvcs {\n\t\tdelete(svcDeps, svc.Name)\n\t}\n\n\t// Remove any service discovery entries for services that we don't host.\n\tfor svc := range infraCfg.ServiceDiscovery {\n\t\tif _, ok := svcDeps[svc]; !ok {\n\t\t\tdelete(infraCfg.ServiceDiscovery, svc)\n\t\t} else {\n\t\t\tdelete(svcDeps, svc)\n\t\t}\n\t}\n\n\t// Make sure all our service dependencies are accounted for.\n\tif len(svcDeps) > 0 {\n\t\tmissing[\"Service Discovery\"] = maps.Keys(svcDeps)\n\t}\n\n\t// Remove secrets we don't need for our hosted services.\n\tslices.Sort(secrets)\n\tsecrets = slices.Compact(secrets)\n\tvar ok bool\n\tif infraCfg.Secrets.EnvRef == nil {\n\t\tfor secret := range infraCfg.Secrets.SecretsMap {\n\t\t\tsecrets, ok = fns.Delete(secrets, secret)\n\t\t\tif !ok {\n\t\t\t\tdelete(infraCfg.Secrets.SecretsMap, secret)\n\t\t\t}\n\t\t}\n\n\t\t// Make sure all our secrets are accounted for.\n\t\tif len(secrets) > 0 {\n\t\t\tmissing[\"Secrets\"] = secrets\n\t\t}\n\t} else {\n\t\t// Print that you need to define a secrets map in the infra config.\n\t}\n\n\t// Find all databases for our hosted services.\n\tdatabases := fns.FlatMap(maps.Values(hostedSvcs), func(db *meta.Service) []string {\n\t\treturn db.Databases\n\t})\n\tslices.Sort(databases)\n\tdatabases = slices.Compact(databases)\n\n\tfor i, sqlServer := range append([]*infra.SQLServer{}, infraCfg.SQLServers...) {\n\t\tfor name := range sqlServer.Databases {\n\t\t\tdatabases, ok = fns.Delete(databases, name)\n\t\t\tif !ok {\n\t\t\t\tdelete(sqlServer.Databases, name)\n\t\t\t}\n\t\t}\n\t\tif len(sqlServer.Databases) == 0 {\n\t\t\tinfraCfg.SQLServers = append(infraCfg.SQLServers[:i], infraCfg.SQLServers[i+1:]...)\n\t\t}\n\t}\n\n\tif len(databases) > 0 {\n\t\tmissing[\"Databases\"] = databases\n\t}\n\n\tcaches := fns.MapAndFilter(md.CacheClusters, func(cache *meta.CacheCluster) (string, bool) {\n\t\treturn cache.Name, fns.Any(cache.Keyspaces, func(ks *meta.CacheCluster_Keyspace) bool {\n\t\t\treturn fns.Any(services, func(s string) bool {\n\t\t\t\treturn ks.Service == s\n\t\t\t})\n\t\t})\n\t})\n\n\tfor name := range infraCfg.Redis {\n\t\tcaches, ok = fns.Delete(caches, name)\n\t\tif !ok {\n\t\t\tdelete(infraCfg.Redis, name)\n\t\t}\n\t}\n\n\tif len(caches) > 0 {\n\t\tmissing[\"Redis\"] = caches\n\t}\n\n\tsubscriptions := fns.FlatMap(md.PubsubTopics, func(topic *meta.PubSubTopic) [][2]string {\n\t\treturn fns.MapAndFilter(topic.Subscriptions, func(s *meta.PubSubTopic_Subscription) ([2]string, bool) {\n\t\t\treturn [2]string{topic.Name, s.Name}, fns.Any(services, func(svc string) bool {\n\t\t\t\treturn s.ServiceName == svc\n\t\t\t})\n\t\t})\n\t})\n\n\tfor _, pubsub := range infraCfg.PubSub {\n\t\tfor topicName, topic := range pubsub.GetTopics() {\n\t\t\tfor subName := range topic.GetSubscriptions() {\n\t\t\t\tfound := false\n\t\t\t\tfor i, sub := range subscriptions {\n\t\t\t\t\tif sub[0] == topicName && sub[1] == subName {\n\t\t\t\t\t\tsubscriptions = append(subscriptions[:i], subscriptions[i+1:]...)\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !found {\n\t\t\t\t\ttopic.DeleteSubscription(subName)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(subscriptions) > 0 {\n\t\tmissing[\"Subscriptions\"] = fns.Map(subscriptions, func(sub [2]string) string {\n\t\t\treturn sub[0] + \"/\" + sub[1]\n\t\t})\n\t}\n\n\ttopics := fns.MapAndFilter(md.PubsubTopics, func(topic *meta.PubSubTopic) (string, bool) {\n\t\treturn topic.Name, fns.Any(topic.Publishers, func(p *meta.PubSubTopic_Publisher) bool {\n\t\t\treturn fns.Any(services, func(s string) bool {\n\t\t\t\treturn p.ServiceName == s\n\t\t\t})\n\t\t})\n\t})\n\n\tfor i, pubsub := range infraCfg.PubSub {\n\t\tfor topicName, topic := range pubsub.GetTopics() {\n\t\t\ti := slices.Index(topics, topicName)\n\t\t\tif i != -1 {\n\t\t\t\ttopics = append(topics[:i], topics[i+1:]...)\n\t\t\t} else if len(topic.GetSubscriptions()) == 0 {\n\t\t\t\tpubsub.DeleteTopic(topicName)\n\t\t\t}\n\t\t}\n\t\tif len(pubsub.GetTopics()) == 0 {\n\t\t\tinfraCfg.PubSub = append(infraCfg.PubSub[:i], infraCfg.PubSub[i+1:]...)\n\t\t}\n\t}\n\tif len(topics) > 0 {\n\t\tmissing[\"Topics\"] = topics\n\t}\n\n\t// Validate bucket config\n\tbuckets := fns.FlatMap(maps.Values(hostedSvcs), func(svc *meta.Service) []string {\n\t\treturn fns.Map(svc.Buckets, (*meta.BucketUsage).GetBucket)\n\t})\n\tslices.Sort(buckets)\n\tbuckets = slices.Compact(buckets)\n\n\tfor _, storage := range infraCfg.ObjectStorage {\n\t\tfor name, infraCfg := range storage.GetBuckets() {\n\t\t\tmetaBkt, ok := fns.Find(md.Buckets, func(b *meta.Bucket) bool {\n\t\t\t\treturn b.Name == name\n\t\t\t})\n\t\t\tif ok {\n\t\t\t\tif metaBkt.Public && infraCfg.PublicBaseURL == \"\" {\n\t\t\t\t\tpath := infra.JSONPath(\"buckets\").Append(infra.JSONPath(name)).Append(\"public_base_url\")\n\t\t\t\t\tvalidationErrors[path] = errors.New(\"Bucket is public but no public base URL is set\")\n\t\t\t\t\treturn nil, \"\", configError(missing, validationErrors)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbuckets, ok = fns.Delete(buckets, name)\n\t\t\tif !ok {\n\t\t\t\tstorage.DeleteBucket(name)\n\t\t\t}\n\t\t}\n\t}\n\tif len(buckets) > 0 {\n\t\tmissing[\"Buckets\"] = buckets\n\t}\n\n\t// Copy CORS config\n\tcors := infra.CORS(params.GlobalCORS)\n\tinfraCfg.CORS = &cors\n\n\tif len(missing) > 0 || len(validationErrors) > 0 {\n\t\treturn nil, \"\", configError(missing, validationErrors)\n\t}\n\n\tcronJobStr, err := formatCronJobInstructions(services, md)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tenvStr := formatEnvVars(envVars)\n\tvar resp strings.Builder\n\tif len(cronJobStr)+len(envStr) > 0 {\n\t\tresp.WriteString(aurora.Bold(\"Before you deploy, you may need to configure the following:\\n\").String())\n\t\tresp.WriteString(cronJobStr)\n\t\tresp.WriteString(envStr)\n\t}\n\tresp.WriteString(LEARN_MORE)\n\n\treturn &infraCfg, resp.String(), nil\n}\n\nfunc formatCronJobInstructions(services []string, md *meta.Data) (string, error) {\n\tif len(md.CronJobs) == 0 {\n\t\treturn \"\", nil\n\t}\n\tsvcByRelPath := fns.ToMap(md.Svcs, func(p *meta.Service) string {\n\t\treturn p.RelPath\n\t})\n\tcronsTable := [][]string{\n\t\t{\"ID\", \"Endpoint Path\", \"Schedule\"},\n\t}\n\tfor _, cronJob := range md.CronJobs {\n\t\tsvc, ok := svcByRelPath[cronJob.Endpoint.Pkg]\n\t\tif !ok {\n\t\t\treturn \"\", errors.Newf(\"could not find service for cron job %s\", cronJob.Id)\n\t\t}\n\t\tif !slices.Contains(services, svc.Name) {\n\t\t\tcontinue\n\t\t}\n\t\trpc, ok := fns.Find(svc.Rpcs, func(r *meta.RPC) bool {\n\t\t\treturn r.Name == cronJob.Endpoint.Name\n\t\t})\n\t\tif !ok {\n\t\t\treturn \"\", errors.Newf(\"could not find rpc for cron job %s\", cronJob.Id)\n\t\t}\n\t\tcronsTable = append(cronsTable, []string{cronJob.Id, pathToString(rpc.Path), cronJob.Schedule})\n\t}\n\tif len(cronsTable) == 1 {\n\t\treturn \"\", nil\n\t}\n\n\treturn aurora.Sprintf(\"\\n%s\\n%s\\n\", aurora.Bold(\"Cron Jobs:\"), generateTable(cronsTable)), nil\n}\n\nfunc generateTable(rows [][]string) string {\n\tau := aurora.NewAurora(true)\n\tvar sb strings.Builder\n\n\t// Calculate column widths\n\tcolWidths := make([]int, len(rows[0]))\n\tfor _, row := range rows {\n\t\tfor i, cell := range row {\n\t\t\tcolWidths[i] = max(colWidths[i], len(cell))\n\t\t}\n\t}\n\n\t// Helper function to create a horizontal line\n\tcreateLine := func() string {\n\t\tline := \"+\"\n\t\tfor _, width := range colWidths {\n\t\t\tline += strings.Repeat(\"-\", width+2) + \"+\"\n\t\t}\n\t\treturn line + \"\\n\"\n\t}\n\n\t// Write top border\n\tsb.WriteString(au.Cyan(createLine()).String())\n\n\t// Write header\n\tsb.WriteString(au.Cyan(\"| \").String())\n\tfor i, header := range rows[0] {\n\t\tsb.WriteString(au.Bold(fmt.Sprintf(\"%-*s\", colWidths[i], header)).String())\n\t\tsb.WriteString(au.Cyan(\" | \").String())\n\t}\n\tsb.WriteString(\"\\n\")\n\n\t// Write header-content separator\n\tsb.WriteString(au.Cyan(createLine()).String())\n\n\t// Write content rows\n\tfor _, row := range rows[1:] {\n\t\tsb.WriteString(au.Cyan(\"| \").String())\n\t\tfor i, cell := range row {\n\t\t\tsb.WriteString(fmt.Sprintf(\"%-*s\", colWidths[i], cell))\n\t\t\tsb.WriteString(au.Cyan(\" | \").String())\n\t\t}\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\t// Write bottom border\n\tsb.WriteString(au.Cyan(createLine()).String())\n\n\treturn sb.String()\n}\n\nfunc pathToString(path *meta.Path) string {\n\tb := strings.Builder{}\n\tfor _, s := range path.Segments {\n\t\tb.WriteByte('/')\n\t\tswitch s.Type {\n\t\tcase meta.PathSegment_PARAM:\n\t\t\tb.WriteByte(':')\n\t\tcase meta.PathSegment_WILDCARD:\n\t\t\tb.WriteByte('*')\n\t\tcase meta.PathSegment_FALLBACK:\n\t\t\tb.WriteByte('!')\n\t\t}\n\t\tb.WriteString(s.Value)\n\t}\n\treturn b.String()\n\n}\n\nfunc formatEnvVars(envVars map[infra.JSONPath]infra.EnvDesc) string {\n\tif len(envVars) == 0 {\n\t\treturn \"\"\n\t}\n\n\tenvByName := map[string]infra.EnvDesc{}\n\tfor _, envVar := range envVars {\n\t\tenvByName[envVar.Name] = envVar\n\t}\n\tenvTable := [][]string{\n\t\t{\"Name\", \"Description\"},\n\t}\n\tfor _, envVar := range envByName {\n\t\tenvTable = append(envTable, []string{envVar.Name, envVar.Description})\n\t}\n\treturn aurora.Sprintf(\"%s\\n%s\\n\", aurora.Bold(\"Environment Variables:\"), generateTable(envTable))\n}\n\nfunc configError(missing map[string][]string, validation map[infra.JSONPath]error) error {\n\tau := aurora.NewAurora(true)\n\tvar errorMsg strings.Builder\n\n\terrorMsg.WriteString(\"\\n\")\n\terrorMsg.WriteString(au.Red(\"\\nYour infra configuration is incomplete\\n\").String())\n\terrorMsg.WriteString(\"\\n\")\n\n\tif len(missing) > 0 {\n\t\terrorMsg.WriteString(au.Red(\"Missing Resource Configurations:\\n\").String())\n\t\tmaxTypeLen := 0\n\t\tfor dataType := range missing {\n\t\t\tif len(dataType) > maxTypeLen {\n\t\t\t\tmaxTypeLen = len(dataType)\n\t\t\t}\n\t\t}\n\n\t\tfor dataType, values := range missing {\n\t\t\tpaddedType := fmt.Sprintf(\"%-*s\", maxTypeLen, dataType)\n\t\t\terrorMsg.WriteString(fmt.Sprintf(\"  %s: %s\\n\",\n\t\t\t\tau.Bold(paddedType),\n\t\t\t\tstrings.Join(values, \", \")))\n\t\t}\n\t\terrorMsg.WriteString(\" \\n \")\n\t}\n\tif len(validation) > 0 {\n\t\terrorMsg.WriteString(au.Red(\"Validation Errors:\\n\").String())\n\t\tfor dataType, err := range validation {\n\t\t\terrorMsg.WriteString(fmt.Sprintf(\"  %s: %s\\n\", au.Bold(dataType), err.Error()))\n\t\t}\n\t\terrorMsg.WriteString(\" \\n \")\n\t}\n\terrorMsg.WriteString(LEARN_MORE)\n\treturn errors.Newf(errorMsg.String())\n}\n"
  },
  {
    "path": "cli/daemon/export.go",
    "content": "package daemon\n\nimport (\n\t\"go/scanner\"\n\n\t\"encr.dev/cli/daemon/export\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Export exports the app.\nfunc (s *Server) Export(req *daemonpb.ExportRequest, stream daemonpb.Daemon_ExportServer) error {\n\tslog := &streamLog{stream: stream, buffered: false}\n\tlog := newStreamLogger(slog)\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to resolve app\")\n\t\tstreamExit(stream, 1)\n\t\treturn nil\n\t}\n\n\texitCode := 0\n\tsuccess, err := export.Docker(stream.Context(), app, req, log, slog)\n\tif err != nil {\n\t\texitCode = 1\n\t\tif list, ok := err.(scanner.ErrorList); ok {\n\t\t\tfor _, e := range list {\n\t\t\t\tlog.Error().Msg(e.Error())\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Error().Msg(err.Error())\n\t\t}\n\t} else if !success {\n\t\texitCode = 1\n\t}\n\n\tstreamExit(stream, exitCode)\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/internal/runlog/runlog.go",
    "content": "package runlog\n\nimport (\n\t\"io\"\n\t\"os\"\n)\n\ntype Log interface {\n\tStdout(buffered bool) io.Writer\n\tStderr(buffered bool) io.Writer\n}\n\ntype oslog struct{}\n\nfunc (oslog) Stdout(buffered bool) io.Writer { return os.Stdout }\nfunc (oslog) Stderr(buffered bool) io.Writer { return os.Stderr }\n\nfunc OS() Log {\n\treturn oslog{}\n}\n"
  },
  {
    "path": "cli/daemon/internal/sym/sym.go",
    "content": "// Package sym parses symbol tables from Go binaries.\npackage sym\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"encr.dev/cli/internal/gosym\"\n)\n\ntype Table struct {\n\t*gosym.Table\n\tBaseOffset uint64\n}\n\nfunc Load(r io.ReaderAt) (*Table, error) {\n\ttbl, err := load(r)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sym.Load: %v\", err)\n\t}\n\treturn tbl, nil\n}\n"
  },
  {
    "path": "cli/daemon/internal/sym/sym_darwin.go",
    "content": "package sym\n\nimport (\n\t\"debug/macho\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"encr.dev/cli/internal/gosym\"\n)\n\nfunc load(r io.ReaderAt) (*Table, error) {\n\texe, err := macho.NewFile(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer exe.Close()\n\n\ttext := exe.Section(\"__text\")\n\tif text == nil {\n\t\treturn nil, fmt.Errorf(\"cannot find __text section\")\n\t}\n\ttextAddr := text.Addr\n\n\tpctbl := exe.Section(\"__gopclntab\")\n\tif pctbl == nil {\n\t\treturn nil, fmt.Errorf(\"cannot find __gopclntab section\")\n\t}\n\tpctblData, err := pctbl.Data()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot read __gopclntab: %v\", err)\n\t}\n\n\tsymtab := exe.Section(\"__gosymtab\")\n\tif symtab == nil {\n\t\treturn nil, fmt.Errorf(\"cannot find __gosymtab section\")\n\t}\n\tsymtabData, err := symtab.Data()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot read __gosymtab: %v\", err)\n\t}\n\n\tlntbl := gosym.NewLineTable(pctblData, textAddr)\n\ttbl, err := gosym.NewTable(symtabData, lntbl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Table{Table: tbl, BaseOffset: textAddr}, nil\n}\n"
  },
  {
    "path": "cli/daemon/internal/sym/sym_elf.go",
    "content": "//go:build !windows && !darwin\n// +build !windows,!darwin\n\npackage sym\n\nimport (\n\t\"debug/elf\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"encr.dev/cli/internal/gosym\"\n)\n\nfunc load(r io.ReaderAt) (*Table, error) {\n\texe, err := elf.NewFile(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer exe.Close()\n\n\ttext := exe.Section(\".text\")\n\tif text == nil {\n\t\treturn nil, fmt.Errorf(\"cannot find .text section\")\n\t}\n\ttextAddr := text.Addr\n\n\tpctbl := exe.Section(\".gopclntab\")\n\tif pctbl == nil {\n\t\treturn nil, fmt.Errorf(\"cannot find .gopclntab section\")\n\t}\n\tpctblData, err := pctbl.Data()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot read .gopclntab: %v\", err)\n\t}\n\n\tsymtab := exe.Section(\".gosymtab\")\n\tif symtab == nil {\n\t\treturn nil, fmt.Errorf(\"cannot find .gosymtab section\")\n\t}\n\tsymtabData, err := symtab.Data()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot read .gosymtab: %v\", err)\n\t}\n\n\tlntbl := gosym.NewLineTable(pctblData, textAddr)\n\ttbl, err := gosym.NewTable(symtabData, lntbl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Table{Table: tbl, BaseOffset: textAddr}, nil\n}\n"
  },
  {
    "path": "cli/daemon/internal/sym/sym_windows.go",
    "content": "package sym\n\nimport (\n\t\"debug/pe\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"encr.dev/cli/internal/gosym\"\n)\n\n// This code is a simplified version of $GOROOT/src/cmd/internal/objfile/pe.go.\n\nfunc load(r io.ReaderAt) (*Table, error) {\n\texe, err := pe.NewFile(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer exe.Close()\n\n\tvar imageBase, textStart uint64\n\tswitch oh := exe.OptionalHeader.(type) {\n\tcase *pe.OptionalHeader32:\n\t\timageBase = uint64(oh.ImageBase)\n\tcase *pe.OptionalHeader64:\n\t\timageBase = oh.ImageBase\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"pe file format not recognized\")\n\t}\n\tif sect := exe.Section(\".text\"); sect != nil {\n\t\ttextStart = imageBase + uint64(sect.VirtualAddress)\n\t}\n\tpclntab, err := loadPETable(exe, \"runtime.pclntab\", \"runtime.epclntab\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsymtab, err := loadPETable(exe, \"runtime.symtab\", \"runtime.esymtab\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlntbl := gosym.NewLineTable(pclntab, textStart)\n\ttbl, err := gosym.NewTable(symtab, lntbl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Table{Table: tbl, BaseOffset: textStart}, nil\n}\n\nfunc findPESymbol(f *pe.File, name string) (*pe.Symbol, error) {\n\tfor _, s := range f.Symbols {\n\t\tif s.Name != name {\n\t\t\tcontinue\n\t\t}\n\t\tif s.SectionNumber <= 0 {\n\t\t\treturn nil, fmt.Errorf(\"symbol %s: invalid section number %d\", name, s.SectionNumber)\n\t\t}\n\t\tif len(f.Sections) < int(s.SectionNumber) {\n\t\t\treturn nil, fmt.Errorf(\"symbol %s: section number %d is larger than max %d\", name, s.SectionNumber, len(f.Sections))\n\t\t}\n\t\treturn s, nil\n\t}\n\treturn nil, fmt.Errorf(\"no %s symbol found\", name)\n}\n\nfunc loadPETable(f *pe.File, sname, ename string) ([]byte, error) {\n\tssym, err := findPESymbol(f, sname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tesym, err := findPESymbol(f, ename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif ssym.SectionNumber != esym.SectionNumber {\n\t\treturn nil, fmt.Errorf(\"%s and %s symbols must be in the same section\", sname, ename)\n\t}\n\tsect := f.Sections[ssym.SectionNumber-1]\n\tdata, err := sect.Data()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn data[ssym.Value:esym.Value], nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/api_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/pkg/builder\"\n\tmetav1 \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\nfunc (m *Manager) registerAPITools() {\n\n\t// Add tool for calling an API endpoint\n\tm.server.AddTool(mcp.NewTool(\"call_endpoint\",\n\t\tmcp.WithDescription(\"Make HTTP requests to any API endpoint in the currently open Encore. Always use this tool to make API calls and do not use curl. This tool will automatically start the application if it's not already running. This tool allows testing and interacting with the application's API endpoints, including authentication and custom payloads.\"),\n\t\tmcp.WithString(\"service\", mcp.Description(\"The name of the service containing the endpoint to call. This must match a service defined in the currently open Encore.\")),\n\t\tmcp.WithString(\"endpoint\", mcp.Description(\"The name of the endpoint to call within the specified service. This must match an endpoint defined in the service.\")),\n\t\tmcp.WithString(\"method\", mcp.Description(\"The HTTP method to use for the request (GET, POST, PUT, DELETE, etc.). Must be a valid HTTP method.\")),\n\t\tmcp.WithString(\"path\", mcp.Description(\"The API request path, including any path parameters. This should match the endpoint's defined path pattern.\")),\n\t\tmcp.WithString(\"payload\", mcp.Description(\"JSON payload for the request containing all endpoint parameters. This includes path parameters, query parameters, headers, and request body as key-value pairs.\")),\n\t\tmcp.WithString(\"auth_token\", mcp.Description(\"Optional authentication token to include in the request. This is used for endpoints that require authentication.\")),\n\t\tmcp.WithString(\"auth_payload\", mcp.Description(\"Optional authentication payload in JSON format. This is used for custom authentication schemes.\")),\n\t\tmcp.WithString(\"correlation_id\", mcp.Description(\"Optional correlation ID to track the request through the system. Useful for debugging and tracing.\")),\n\t), m.callEndpoint)\n\n\t// Add tool for getting all services and endpoints\n\tm.server.AddTool(mcp.NewTool(\"get_services\",\n\t\tmcp.WithDescription(\"Retrieve comprehensive information about all services and their endpoints in the currently open Encore. This includes endpoint schemas, documentation, and service-level metadata.\"),\n\t\tmcp.WithArray(\"services\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"Optional list of specific service names to retrieve information for. If not provided, returns information for all services in the currently open Encore.\",\n\t\t\t})),\n\t\tmcp.WithArray(\"endpoints\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"Optional list of specific endpoint names to filter by. If not provided, returns all endpoints for the specified services.\",\n\t\t\t})),\n\t\tmcp.WithBoolean(\"include_schemas\", mcp.Description(\"When true, includes detailed request and response schemas for each endpoint. This is useful for understanding the data structures used by the API.\")),\n\t\tmcp.WithBoolean(\"include_service_details\", mcp.Description(\"When true, includes additional service-level information such as middleware, dependencies, and configuration.\")),\n\t\tmcp.WithBoolean(\"include_endpoints\", mcp.Description(\"When true, includes endpoint information in the response. Set to false to get only service-level information.\")),\n\t), m.getEndpoints)\n\n\t// Add tool for getting middleware metadata\n\tm.server.AddTool(mcp.NewTool(\"get_middleware\",\n\t\tmcp.WithDescription(\"Retrieve detailed information about all middleware components in the currently open Encore, including their configuration, order of execution, and which services/endpoints they affect.\"),\n\t), m.getMiddleware)\n\n\t// Add tool for getting auth handler metadata\n\tm.server.AddTool(mcp.NewTool(\"get_auth_handlers\",\n\t\tmcp.WithDescription(\"Retrieve information about all authentication handlers in the currently open Encore, including their configuration, supported authentication methods, and which services/endpoints they protect.\"),\n\t), m.getAuthHandlers)\n}\n\nfunc (m *Manager) callEndpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\t// Extract and validate required arguments\n\tserviceStr, ok := request.Params.Arguments[\"service\"].(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"missing or invalid service argument\")\n\t}\n\n\tendpointStr, ok := request.Params.Arguments[\"endpoint\"].(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"missing or invalid endpoint argument\")\n\t}\n\n\tmethodStr, ok := request.Params.Arguments[\"method\"].(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"missing or invalid method argument\")\n\t}\n\n\tpathStr, ok := request.Params.Arguments[\"path\"].(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"missing or invalid path argument\")\n\t}\n\n\t// Build API call parameters\n\tparams := &run.ApiCallParams{\n\t\tAppID:         inst.PlatformOrLocalID(),\n\t\tService:       serviceStr,\n\t\tEndpoint:      endpointStr,\n\t\tPath:          pathStr,\n\t\tMethod:        methodStr,\n\t\tCorrelationID: \"\",\n\t}\n\n\tif !strings.HasPrefix(params.Path, \"/\") {\n\t\tparams.Path = \"/\" + params.Path\n\t}\n\n\t// Add optional parameters\n\tif payload, ok := request.Params.Arguments[\"payload\"].(string); ok && payload != \"\" {\n\t\tparams.Payload = []byte(payload)\n\t}\n\tif authToken, ok := request.Params.Arguments[\"auth_token\"].(string); ok && authToken != \"\" {\n\t\tparams.AuthToken = authToken\n\t}\n\tif authPayload, ok := request.Params.Arguments[\"auth_payload\"].(string); ok && authPayload != \"\" {\n\t\tparams.AuthPayload = []byte(authPayload)\n\t}\n\tif correlationID, ok := request.Params.Arguments[\"correlation_id\"].(string); ok && correlationID != \"\" {\n\t\tparams.CorrelationID = correlationID\n\t}\n\tns, err := m.ns.GetActive(ctx, inst)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get active namespace: %w\", err)\n\t}\n\n\t// Get the app's run instance\n\tappRun := m.run.FindRunByAppID(inst.PlatformOrLocalID())\n\tif appRun == nil {\n\t\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create listener: %w\", err)\n\t\t}\n\t\tport := ln.Addr().(*net.TCPAddr).Port\n\t\tappRun, err = m.run.Start(ctx, run.StartParams{\n\t\t\tApp:        inst,\n\t\t\tNS:         ns,\n\t\t\tWorkingDir: \"/\",\n\t\t\tWatch:      true,\n\t\t\tListener:   ln,\n\t\t\tListenAddr: \"127.0.0.1:\" + fmt.Sprint(port),\n\t\t\tEnviron:    os.Environ(),\n\t\t\tOpsTracker: nil,\n\t\t\tBrowser:    run.BrowserModeNever,\n\t\t\tDebug:      builder.DebugModeDisabled,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to start app run: %w\", err)\n\t\t}\n\t}\n\n\tstarted := false\n\tfor !started {\n\t\tselect {\n\t\tcase <-appRun.Done():\n\t\t\treturn nil, fmt.Errorf(\"app run failed to start\")\n\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t// Check if the app is ready by polling the health endpoint\n\t\t\tresp, err := http.Get(\"http://\" + appRun.ListenAddr + \"/__encore/healthz\")\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresp.Body.Close()\n\t\t\tstarted = resp.StatusCode == 200\n\t\t}\n\t}\n\n\t// Call the API\n\tresult, err := run.CallAPI(ctx, appRun, params)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"API call failed: %w\", err)\n\t}\n\n\t// Convert body to string\n\tif body, ok := result[\"body\"]; ok {\n\t\tswitch v := body.(type) {\n\t\tcase []byte:\n\t\t\tresult[\"body\"] = string(v)\n\t\t}\n\t}\n\n\t// Serialize the response\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal response: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getEndpoints(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Get the list of services to retrieve endpoints for\n\tvar serviceNames []string\n\tif services, ok := request.Params.Arguments[\"services\"].([]interface{}); ok {\n\t\tfor _, svc := range services {\n\t\t\tif svcName, ok := svc.(string); ok && svcName != \"\" {\n\t\t\t\tserviceNames = append(serviceNames, svcName)\n\t\t\t}\n\t\t}\n\t}\n\n\t// If no services specified, get all services\n\tif len(serviceNames) == 0 {\n\t\tfor _, svc := range md.Svcs {\n\t\t\tserviceNames = append(serviceNames, svc.Name)\n\t\t}\n\t}\n\n\t// Parse request parameters\n\tincludeEndpoints := true\n\tif include, ok := request.Params.Arguments[\"include_endpoints\"].(bool); ok {\n\t\tincludeEndpoints = include\n\t}\n\n\tvar endpointNames []string\n\tvar endpointFilter map[string]bool\n\tvar hasEndpointFilter bool\n\n\t// Only process endpoint filters if we're including endpoints\n\tif includeEndpoints {\n\t\t// Get the list of endpoint names to filter by\n\t\tif endpoints, ok := request.Params.Arguments[\"endpoints\"].([]interface{}); ok {\n\t\t\tfor _, ep := range endpoints {\n\t\t\t\tif epName, ok := ep.(string); ok && epName != \"\" {\n\t\t\t\t\tendpointNames = append(endpointNames, epName)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Create a map for faster lookups when filtering endpoints\n\t\tendpointFilter = make(map[string]bool)\n\t\tfor _, name := range endpointNames {\n\t\t\tendpointFilter[name] = true\n\t\t}\n\t\thasEndpointFilter = len(endpointFilter) > 0\n\t}\n\n\tincludeSchemas := false\n\tif include, ok := request.Params.Arguments[\"include_schemas\"].(bool); ok {\n\t\tincludeSchemas = include\n\t}\n\n\tincludeServiceDetails := false\n\tif include, ok := request.Params.Arguments[\"include_service_details\"].(bool); ok {\n\t\tincludeServiceDetails = include\n\t}\n\n\t// Set up decl map for schema info if needed\n\tvar declByID map[uint32]*schema.Decl\n\tif includeEndpoints && includeSchemas {\n\t\tdeclByID = map[uint32]*schema.Decl{}\n\t\tfor _, decl := range md.Decls {\n\t\t\tdeclByID[decl.Id] = decl\n\t\t}\n\t}\n\n\t// Create a map to store services with their endpoints\n\tserviceMap := make(map[string]map[string]interface{})\n\n\t// Process each requested service\n\tfor _, serviceName := range serviceNames {\n\t\t// Find the service in metadata\n\t\tvar targetService *metav1.Service\n\t\tfor _, svc := range md.Svcs {\n\t\t\tif svc.Name == serviceName {\n\t\t\t\ttargetService = svc\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif targetService == nil {\n\t\t\t// Skip services that don't exist instead of returning an error\n\t\t\tcontinue\n\t\t}\n\n\t\t// Initialize service data\n\t\tserviceData := map[string]interface{}{}\n\n\t\t// Add service details if requested\n\t\tif includeServiceDetails {\n\t\t\tserviceData[\"name\"] = targetService.Name\n\t\t\tserviceData[\"rel_path\"] = targetService.RelPath\n\t\t\tserviceData[\"has_config\"] = targetService.HasConfig\n\t\t\tserviceData[\"databases\"] = targetService.Databases\n\t\t\tserviceData[\"rpc_count\"] = len(targetService.Rpcs)\n\t\t}\n\n\t\t// Process endpoints if requested\n\t\tif includeEndpoints {\n\t\t\t// Initialize an empty array for this service's endpoints\n\t\t\tendpoints := make([]map[string]interface{}, 0)\n\n\t\t\t// Process all RPCs for this service\n\t\t\tfor _, rpc := range targetService.Rpcs {\n\t\t\t\t// Skip this endpoint if it's not in the filter list (when filter is provided)\n\t\t\t\tif hasEndpointFilter && !endpointFilter[rpc.Name] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tendpoint := map[string]interface{}{\n\t\t\t\t\t\"name\":         rpc.Name,\n\t\t\t\t\t\"access_type\":  rpc.AccessType.String(),\n\t\t\t\t\t\"http_methods\": rpc.HttpMethods,\n\t\t\t\t}\n\n\t\t\t\t// Add path if available\n\t\t\t\tif rpc.Path != nil {\n\t\t\t\t\tpathSegments := make([]string, 0)\n\t\t\t\t\tfor _, segment := range rpc.Path.Segments {\n\t\t\t\t\t\tpathSegments = append(pathSegments, segment.Value)\n\t\t\t\t\t}\n\t\t\t\t\tendpoint[\"path\"] = strings.Join(pathSegments, \"/\")\n\t\t\t\t}\n\n\t\t\t\t// Add documentation if available\n\t\t\t\tif rpc.Doc != nil {\n\t\t\t\t\tendpoint[\"doc\"] = *rpc.Doc\n\t\t\t\t}\n\n\t\t\t\t// Include schema information if requested\n\t\t\t\tif includeSchemas {\n\t\t\t\t\tschemas := map[string]interface{}{}\n\n\t\t\t\t\t// For request and response schemas\n\t\t\t\t\tif rpc.RequestSchema != nil {\n\t\t\t\t\t\tstr, _ := NamedOrInlineStruct(declByID, rpc.RequestSchema)\n\t\t\t\t\t\tqry, headers, cookies, body := StructBits(str, rpc.HttpMethods[0], false, false, true)\n\t\t\t\t\t\tschemas[\"request_schema\"] = strings.Join([]string{\"{\", qry, headers, cookies, body, \"}\"}, \"\")\n\t\t\t\t\t}\n\n\t\t\t\t\tif rpc.ResponseSchema != nil {\n\t\t\t\t\t\tstr, _ := NamedOrInlineStruct(declByID, rpc.ResponseSchema)\n\t\t\t\t\t\tqry, headers, cookies, body := StructBits(str, rpc.HttpMethods[0], true, false, true)\n\t\t\t\t\t\tschemas[\"response_schema\"] = strings.Join([]string{\"{\", qry, headers, cookies, body, \"}\"}, \"\")\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(schemas) > 0 {\n\t\t\t\t\t\tendpoint[\"schemas\"] = schemas\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tendpoints = append(endpoints, endpoint)\n\t\t\t}\n\n\t\t\t// Add endpoints to the service data if any were found\n\t\t\tif len(endpoints) > 0 {\n\t\t\t\tserviceData[\"endpoints\"] = endpoints\n\t\t\t}\n\t\t}\n\n\t\t// Add service to the result map if it has data or endpoints\n\t\tif len(serviceData) > 0 {\n\t\t\tserviceMap[serviceName] = serviceData\n\t\t}\n\t}\n\n\t// Create the result object with services and summary\n\tresult := map[string]interface{}{\n\t\t\"services\": serviceMap,\n\t\t\"summary\": map[string]interface{}{\n\t\t\t\"total_services\": len(serviceMap),\n\t\t},\n\t}\n\n\t// Add endpoint count to summary if we're including endpoints\n\tif includeEndpoints {\n\t\ttotalEndpoints := 0\n\t\tfor _, serviceData := range serviceMap {\n\t\t\tif endpoints, ok := serviceData[\"endpoints\"].([]map[string]interface{}); ok {\n\t\t\t\ttotalEndpoints += len(endpoints)\n\t\t\t}\n\t\t}\n\t\tresult[\"summary\"].(map[string]interface{})[\"total_endpoints\"] = totalEndpoints\n\t}\n\n\t// Add filter information to summary if filters were applied\n\tif len(serviceNames) < len(md.Svcs) || (includeEndpoints && hasEndpointFilter) {\n\t\tfilters := map[string]interface{}{}\n\t\tif len(serviceNames) < len(md.Svcs) {\n\t\t\tfilters[\"services\"] = serviceNames\n\t\t}\n\t\tif includeEndpoints && hasEndpointFilter {\n\t\t\tfilters[\"endpoints\"] = endpointNames\n\t\t}\n\t\tresult[\"summary\"].(map[string]interface{})[\"filters_applied\"] = filters\n\t}\n\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal services and endpoints: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getMiddleware(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Find middleware definition locations from trace nodes\n\tmiddlewareDefLocations := make(map[string]map[string]interface{})\n\n\t// Scan through all packages to find trace nodes related to middleware\n\tfor _, pkg := range md.Pkgs {\n\t\tfor _, node := range pkg.TraceNodes {\n\t\t\t// Check for middleware definitions\n\t\t\tif node.GetMiddlewareDef() != nil {\n\t\t\t\tmiddlewareDef := node.GetMiddlewareDef()\n\t\t\t\tmiddlewareName := middlewareDef.Name\n\n\t\t\t\t// Use package path + name as a unique identifier\n\t\t\t\tmiddlewareID := fmt.Sprintf(\"%s/%s\", middlewareDef.PkgRelPath, middlewareName)\n\n\t\t\t\tmiddlewareDefLocations[middlewareID] = map[string]interface{}{\n\t\t\t\t\t\"filepath\":     node.Filepath,\n\t\t\t\t\t\"line_start\":   node.SrcLineStart,\n\t\t\t\t\t\"line_end\":     node.SrcLineEnd,\n\t\t\t\t\t\"column_start\": node.SrcColStart,\n\t\t\t\t\t\"column_end\":   node.SrcColEnd,\n\t\t\t\t\t\"package_path\": middlewareDef.PkgRelPath,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Group middleware by type (global vs service-specific)\n\tglobalMiddleware := make([]map[string]interface{}, 0)\n\tserviceMiddleware := make(map[string][]map[string]interface{})\n\n\t// Process all middleware\n\tfor _, middleware := range md.Middleware {\n\t\tmiddlewareInfo := map[string]interface{}{\n\t\t\t\"doc\":    middleware.Doc,\n\t\t\t\"global\": middleware.Global,\n\t\t}\n\n\t\t// Add qualified name information if available\n\t\tif middleware.Name != nil {\n\t\t\tname := map[string]interface{}{\n\t\t\t\t\"package\": middleware.Name.Pkg,\n\t\t\t\t\"name\":    middleware.Name.Name,\n\t\t\t}\n\t\t\tmiddlewareInfo[\"name\"] = name\n\n\t\t\t// Add definition location if available\n\t\t\tmiddlewareID := fmt.Sprintf(\"%s/%s\", middleware.Name.Pkg, middleware.Name.Name)\n\t\t\tif location, exists := middlewareDefLocations[middlewareID]; exists {\n\t\t\t\tmiddlewareInfo[\"definition\"] = map[string]interface{}{\n\t\t\t\t\t\"filepath\":     location[\"filepath\"],\n\t\t\t\t\t\"line_start\":   location[\"line_start\"],\n\t\t\t\t\t\"line_end\":     location[\"line_end\"],\n\t\t\t\t\t\"column_start\": location[\"column_start\"],\n\t\t\t\t\t\"column_end\":   location[\"column_end\"],\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add target information if available\n\t\tif len(middleware.Target) > 0 {\n\t\t\ttargets := make([]map[string]interface{}, 0, len(middleware.Target))\n\t\t\tfor _, target := range middleware.Target {\n\t\t\t\ttargetInfo := map[string]interface{}{\n\t\t\t\t\t\"type\":  target.Type.String(),\n\t\t\t\t\t\"value\": target.Value,\n\t\t\t\t}\n\t\t\t\ttargets = append(targets, targetInfo)\n\t\t\t}\n\t\t\tmiddlewareInfo[\"targets\"] = targets\n\t\t}\n\n\t\t// Add to the appropriate group\n\t\tif middleware.Global {\n\t\t\tglobalMiddleware = append(globalMiddleware, middlewareInfo)\n\t\t} else if middleware.ServiceName != nil {\n\t\t\tserviceName := *middleware.ServiceName\n\t\t\tif _, exists := serviceMiddleware[serviceName]; !exists {\n\t\t\t\tserviceMiddleware[serviceName] = make([]map[string]interface{}, 0)\n\t\t\t}\n\t\t\tserviceMiddleware[serviceName] = append(serviceMiddleware[serviceName], middlewareInfo)\n\t\t}\n\t}\n\n\t// Build the final result\n\tresult := map[string]interface{}{\n\t\t\"global\":   globalMiddleware,\n\t\t\"services\": serviceMiddleware,\n\t\t\"summary\": map[string]interface{}{\n\t\t\t\"total_middleware\":   len(md.Middleware),\n\t\t\t\"global_middleware\":  len(globalMiddleware),\n\t\t\t\"service_middleware\": make(map[string]int),\n\t\t\t\"service_count\":      len(serviceMiddleware),\n\t\t},\n\t}\n\n\t// Add counts by service\n\tsummary := result[\"summary\"].(map[string]interface{})\n\tfor service, middleware := range serviceMiddleware {\n\t\tsummary[\"service_middleware\"].(map[string]int)[service] = len(middleware)\n\t}\n\n\t// Sort middleware by name for consistent output\n\tsort.Slice(globalMiddleware, func(i, j int) bool {\n\t\tnameI := \"\"\n\t\tnameJ := \"\"\n\t\tif name, ok := globalMiddleware[i][\"name\"].(map[string]interface{}); ok {\n\t\t\tnameI = name[\"name\"].(string)\n\t\t}\n\t\tif name, ok := globalMiddleware[j][\"name\"].(map[string]interface{}); ok {\n\t\t\tnameJ = name[\"name\"].(string)\n\t\t}\n\t\treturn nameI < nameJ\n\t})\n\n\t// Sort service middleware as well\n\tfor service, middleware := range serviceMiddleware {\n\t\tsort.Slice(middleware, func(i, j int) bool {\n\t\t\tnameI := \"\"\n\t\t\tnameJ := \"\"\n\t\t\tif name, ok := middleware[i][\"name\"].(map[string]interface{}); ok {\n\t\t\t\tnameI = name[\"name\"].(string)\n\t\t\t}\n\t\t\tif name, ok := middleware[j][\"name\"].(map[string]interface{}); ok {\n\t\t\t\tnameJ = name[\"name\"].(string)\n\t\t\t}\n\t\t\treturn nameI < nameJ\n\t\t})\n\t\tserviceMiddleware[service] = middleware\n\t}\n\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal middleware information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getAuthHandlers(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Find auth handler definition locations from trace nodes\n\tauthHandlerDefLocations := make(map[string]map[string]interface{})\n\n\t// Scan through all packages to find trace nodes related to auth handlers\n\tfor _, pkg := range md.Pkgs {\n\t\tfor _, node := range pkg.TraceNodes {\n\t\t\t// Check for auth handler definitions\n\t\t\tif node.GetAuthHandlerDef() != nil {\n\t\t\t\tauthHandlerDef := node.GetAuthHandlerDef()\n\t\t\t\tserviceName := authHandlerDef.ServiceName\n\t\t\t\thandlerName := authHandlerDef.Name\n\n\t\t\t\t// Use service name + handler name as a unique identifier\n\t\t\t\thandlerID := fmt.Sprintf(\"%s/%s\", serviceName, handlerName)\n\n\t\t\t\tauthHandlerDefLocations[handlerID] = map[string]interface{}{\n\t\t\t\t\t\"filepath\":     node.Filepath,\n\t\t\t\t\t\"line_start\":   node.SrcLineStart,\n\t\t\t\t\t\"line_end\":     node.SrcLineEnd,\n\t\t\t\t\t\"column_start\": node.SrcColStart,\n\t\t\t\t\t\"column_end\":   node.SrcColEnd,\n\t\t\t\t\t\"service_name\": serviceName,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Process the main auth handler if it exists\n\tvar mainAuthHandler map[string]interface{}\n\tif md.AuthHandler != nil {\n\t\tauth := md.AuthHandler\n\n\t\tauthData := map[string]interface{}{\n\t\t\t\"name\":         auth.Name,\n\t\t\t\"doc\":          auth.Doc,\n\t\t\t\"service_name\": auth.ServiceName,\n\t\t\t\"pkg_path\":     auth.PkgPath,\n\t\t\t\"pkg_name\":     auth.PkgName,\n\t\t}\n\n\t\t// Add parameter and auth data type information\n\t\tif auth.Params != nil {\n\t\t\tparamsData, err := protojson.Marshal(auth.Params)\n\t\t\tif err == nil {\n\t\t\t\tvar paramsJson interface{}\n\t\t\t\tif err := json.Unmarshal(paramsData, &paramsJson); err == nil {\n\t\t\t\t\tauthData[\"params\"] = paramsJson\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif auth.AuthData != nil {\n\t\t\tauthDataTypeData, err := protojson.Marshal(auth.AuthData)\n\t\t\tif err == nil {\n\t\t\t\tvar authDataJson interface{}\n\t\t\t\tif err := json.Unmarshal(authDataTypeData, &authDataJson); err == nil {\n\t\t\t\t\tauthData[\"auth_data\"] = authDataJson\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add location information if available\n\t\thandlerID := fmt.Sprintf(\"%s/%s\", auth.ServiceName, auth.Name)\n\t\tif location, exists := authHandlerDefLocations[handlerID]; exists {\n\t\t\tauthData[\"definition\"] = map[string]interface{}{\n\t\t\t\t\"filepath\":     location[\"filepath\"],\n\t\t\t\t\"line_start\":   location[\"line_start\"],\n\t\t\t\t\"line_end\":     location[\"line_end\"],\n\t\t\t\t\"column_start\": location[\"column_start\"],\n\t\t\t\t\"column_end\":   location[\"column_end\"],\n\t\t\t}\n\t\t}\n\n\t\tmainAuthHandler = authData\n\t}\n\n\t// Process gateway auth handlers\n\tgatewayAuthHandlers := make(map[string]map[string]interface{})\n\n\tfor _, gateway := range md.Gateways {\n\t\tif gateway.Explicit != nil && gateway.Explicit.AuthHandler != nil {\n\t\t\tauth := gateway.Explicit.AuthHandler\n\n\t\t\tauthData := map[string]interface{}{\n\t\t\t\t\"name\":         auth.Name,\n\t\t\t\t\"doc\":          auth.Doc,\n\t\t\t\t\"service_name\": auth.ServiceName,\n\t\t\t\t\"pkg_path\":     auth.PkgPath,\n\t\t\t\t\"pkg_name\":     auth.PkgName,\n\t\t\t\t\"gateway_name\": gateway.EncoreName,\n\t\t\t}\n\n\t\t\t// Add parameter and auth data type information\n\t\t\tif auth.Params != nil {\n\t\t\t\tparamsData, err := protojson.Marshal(auth.Params)\n\t\t\t\tif err == nil {\n\t\t\t\t\tvar paramsJson interface{}\n\t\t\t\t\tif err := json.Unmarshal(paramsData, &paramsJson); err == nil {\n\t\t\t\t\t\tauthData[\"params\"] = paramsJson\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif auth.AuthData != nil {\n\t\t\t\tauthDataTypeData, err := protojson.Marshal(auth.AuthData)\n\t\t\t\tif err == nil {\n\t\t\t\t\tvar authDataJson interface{}\n\t\t\t\t\tif err := json.Unmarshal(authDataTypeData, &authDataJson); err == nil {\n\t\t\t\t\t\tauthData[\"auth_data\"] = authDataJson\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add location information if available\n\t\t\thandlerID := fmt.Sprintf(\"%s/%s\", auth.ServiceName, auth.Name)\n\t\t\tif location, exists := authHandlerDefLocations[handlerID]; exists {\n\t\t\t\tauthData[\"definition\"] = map[string]interface{}{\n\t\t\t\t\t\"filepath\":     location[\"filepath\"],\n\t\t\t\t\t\"line_start\":   location[\"line_start\"],\n\t\t\t\t\t\"line_end\":     location[\"line_end\"],\n\t\t\t\t\t\"column_start\": location[\"column_start\"],\n\t\t\t\t\t\"column_end\":   location[\"column_end\"],\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tgatewayAuthHandlers[gateway.EncoreName] = authData\n\t\t}\n\t}\n\n\t// Build the final result\n\tresult := map[string]interface{}{\n\t\t\"main_auth_handler\":     mainAuthHandler,\n\t\t\"gateway_auth_handlers\": gatewayAuthHandlers,\n\t\t\"summary\": map[string]interface{}{\n\t\t\t\"has_main_auth\":      mainAuthHandler != nil,\n\t\t\t\"gateway_count\":      len(md.Gateways),\n\t\t\t\"auth_gateway_count\": len(gatewayAuthHandlers),\n\t\t},\n\t}\n\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal auth handler information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/bucket_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\n\t\"encr.dev/pkg/emulators/storage/gcsemu\"\n)\n\nfunc (m *Manager) registerBucketTools() {\n\tm.server.AddTool(mcp.NewTool(\"get_storage_buckets\",\n\t\tmcp.WithDescription(\"Retrieve comprehensive information about all storage buckets in the currently open Encore, including their configurations, access patterns, and the services that interact with them. This tool helps understand the application's storage architecture and data management strategy.\"),\n\t), m.getStorageBuckets)\n\n\tm.server.AddTool(mcp.NewTool(\"get_objects\",\n\t\tmcp.WithDescription(\"List and retrieve metadata about objects stored in one or more storage buckets. This tool helps inspect the contents of storage buckets and understand the data stored in them.\"),\n\t\tmcp.WithArray(\"buckets\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"List of bucket names to list objects from. Each bucket must be defined in the currently open Encore's storage configuration.\",\n\t\t\t})),\n\t), m.listObjects)\n}\n\nfunc (m *Manager) listObjects(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tapp, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\tclusterNS, err := m.ns.GetActive(ctx, app)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get active namespace: %w\", err)\n\t}\n\tdir, err := m.objects.BaseDir(clusterNS.ID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get base directory: %w\", err)\n\t}\n\tstore := gcsemu.NewFileStore(dir)\n\tbuckets, ok := request.Params.Arguments[\"buckets\"].([]any)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"buckets is not an array\")\n\t}\n\tobjects := map[string][]map[string]interface{}{}\n\tfor _, bucket := range buckets {\n\t\tbucketName := bucket.(string)\n\t\tvar bucketObjects []map[string]interface{}\n\t\terr = store.Walk(ctx, bucketName, func(ctx context.Context, filename string, fInfo os.FileInfo) error {\n\t\t\tobjectInfo := map[string]interface{}{\n\t\t\t\t\"name\":          filename,\n\t\t\t\t\"size\":          fInfo.Size(),\n\t\t\t\t\"last_modified\": fInfo.ModTime(),\n\t\t\t\t\"is_directory\":  fInfo.IsDir(),\n\t\t\t}\n\t\t\tbucketObjects = append(bucketObjects, objectInfo)\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to walk bucket objects: %w\", err)\n\t\t}\n\t\tobjects[bucketName] = bucketObjects\n\t}\n\tjsonData, err := json.Marshal(objects)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal object information: %w\", err)\n\t}\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getStorageBuckets(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Build map of services that use each bucket with their operations\n\tbucketUsageByService := make(map[string][]map[string]interface{})\n\n\tfor _, svc := range md.Svcs {\n\t\tfor _, bucketUsage := range svc.Buckets {\n\t\t\tbucketName := bucketUsage.Bucket\n\n\t\t\t// Convert operations to strings\n\t\t\toperations := make([]string, 0, len(bucketUsage.Operations))\n\t\t\tfor _, op := range bucketUsage.Operations {\n\t\t\t\toperations = append(operations, op.String())\n\t\t\t}\n\n\t\t\t// Create usage info\n\t\t\tusageInfo := map[string]interface{}{\n\t\t\t\t\"service_name\": svc.Name,\n\t\t\t\t\"operations\":   operations,\n\t\t\t}\n\n\t\t\t// Add to map\n\t\t\tif _, exists := bucketUsageByService[bucketName]; !exists {\n\t\t\t\tbucketUsageByService[bucketName] = make([]map[string]interface{}, 0)\n\t\t\t}\n\t\t\tbucketUsageByService[bucketName] = append(bucketUsageByService[bucketName], usageInfo)\n\t\t}\n\t}\n\n\t// Collect bucket definition locations from trace nodes\n\tbucketDefLocations := make(map[string]map[string]interface{})\n\n\t// Find bucket definitions in trace nodes if possible\n\t// Currently no specific bucket definition node type in the TraceNode,\n\t// so we leave this empty for now. This could be expanded in the future\n\t// if the metadata provides better tracking.\n\n\t// Process all buckets\n\tbuckets := make([]map[string]interface{}, 0)\n\tfor _, bucket := range md.Buckets {\n\t\tbucketInfo := map[string]interface{}{\n\t\t\t\"name\":      bucket.Name,\n\t\t\t\"versioned\": bucket.Versioned,\n\t\t\t\"public\":    bucket.Public,\n\t\t}\n\n\t\t// Add documentation if available\n\t\tif bucket.Doc != nil {\n\t\t\tbucketInfo[\"doc\"] = *bucket.Doc\n\t\t}\n\n\t\t// Add location information if available\n\t\tif location, exists := bucketDefLocations[bucket.Name]; exists {\n\t\t\tbucketInfo[\"definition\"] = location\n\t\t}\n\n\t\t// Add service usage information\n\t\tif usages, exists := bucketUsageByService[bucket.Name]; exists {\n\t\t\tbucketInfo[\"service_usage\"] = usages\n\t\t} else {\n\t\t\tbucketInfo[\"service_usage\"] = []map[string]interface{}{}\n\t\t}\n\n\t\tbuckets = append(buckets, bucketInfo)\n\t}\n\n\tjsonData, err := json.Marshal(buckets)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal storage buckets information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/cache_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n)\n\nfunc (m *Manager) registerCacheTools() {\n\tm.server.AddTool(mcp.NewTool(\"get_cache_keyspaces\",\n\t\tmcp.WithDescription(\"Retrieve comprehensive information about all cache keyspaces in the currently open Encore, including their configurations, usage patterns, and the services that interact with them. This tool helps understand the application's caching strategy and data access patterns.\"),\n\t), m.getCacheKeyspaces)\n}\n\nfunc (m *Manager) getCacheKeyspaces(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Find keyspace definition locations from trace nodes\n\tkeyspaceDefLocations := make(map[string]map[string]map[string]interface{})\n\n\t// Scan through all packages to find trace nodes related to cache keyspaces\n\tfor _, pkg := range md.Pkgs {\n\t\tfor _, node := range pkg.TraceNodes {\n\t\t\t// Check for cache keyspace definitions\n\t\t\tif node.GetCacheKeyspace() != nil {\n\t\t\t\tkeyspaceDef := node.GetCacheKeyspace()\n\t\t\t\tclusterName := keyspaceDef.ClusterName\n\t\t\t\tkeyspaceName := keyspaceDef.VarName\n\n\t\t\t\t// Initialize maps if needed\n\t\t\t\tif _, exists := keyspaceDefLocations[clusterName]; !exists {\n\t\t\t\t\tkeyspaceDefLocations[clusterName] = make(map[string]map[string]interface{})\n\t\t\t\t}\n\n\t\t\t\tif _, exists := keyspaceDefLocations[clusterName][keyspaceName]; !exists {\n\t\t\t\t\tkeyspaceDefLocations[clusterName][keyspaceName] = map[string]interface{}{\n\t\t\t\t\t\t\"filepath\":     node.Filepath,\n\t\t\t\t\t\t\"line_start\":   node.SrcLineStart,\n\t\t\t\t\t\t\"line_end\":     node.SrcLineEnd,\n\t\t\t\t\t\t\"column_start\": node.SrcColStart,\n\t\t\t\t\t\t\"column_end\":   node.SrcColEnd,\n\t\t\t\t\t\t\"package_path\": keyspaceDef.PkgRelPath,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Build the result\n\tresult := make([]map[string]interface{}, 0)\n\n\t// Process all cache clusters\n\tfor _, cluster := range md.CacheClusters {\n\t\tclusterInfo := map[string]interface{}{\n\t\t\t\"name\":            cluster.Name,\n\t\t\t\"eviction_policy\": cluster.EvictionPolicy,\n\t\t\t\"doc\":             cluster.Doc,\n\t\t}\n\n\t\t// Process keyspaces for this cluster\n\t\tkeyspaces := make([]map[string]interface{}, 0)\n\t\tfor _, keyspace := range cluster.Keyspaces {\n\t\t\tkeyspaceInfo := map[string]interface{}{\n\t\t\t\t\"service\": keyspace.Service,\n\t\t\t\t\"doc\":     keyspace.Doc,\n\t\t\t}\n\n\t\t\t// Add key and value type information from protojson\n\t\t\tif keyspace.KeyType != nil {\n\t\t\t\tkeyTypeData, err := protojson.Marshal(keyspace.KeyType)\n\t\t\t\tif err == nil {\n\t\t\t\t\tvar keyTypeJson interface{}\n\t\t\t\t\tif err := json.Unmarshal(keyTypeData, &keyTypeJson); err == nil {\n\t\t\t\t\t\tkeyspaceInfo[\"key_type\"] = keyTypeJson\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif keyspace.ValueType != nil {\n\t\t\t\tvalueTypeData, err := protojson.Marshal(keyspace.ValueType)\n\t\t\t\tif err == nil {\n\t\t\t\t\tvar valueTypeJson interface{}\n\t\t\t\t\tif err := json.Unmarshal(valueTypeData, &valueTypeJson); err == nil {\n\t\t\t\t\t\tkeyspaceInfo[\"value_type\"] = valueTypeJson\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add path pattern if available\n\t\t\tif keyspace.PathPattern != nil {\n\t\t\t\tpathPattern := make([]string, 0)\n\t\t\t\tfor _, segment := range keyspace.PathPattern.Segments {\n\t\t\t\t\tpathPattern = append(pathPattern, segment.Value)\n\t\t\t\t}\n\t\t\t\tkeyspaceInfo[\"path_pattern\"] = strings.Join(pathPattern, \"/\")\n\t\t\t}\n\n\t\t\t// Add definition location if available\n\t\t\t// We need to find the keyspace variable name from the definition data\n\t\t\t// This is approximate as we don't have a direct mapping in the metadata\n\t\t\tif locations, ok := keyspaceDefLocations[cluster.Name]; ok {\n\t\t\t\tfor keyspaceName, location := range locations {\n\t\t\t\t\t// Try to match by service\n\t\t\t\t\tif location[\"package_path\"] != \"\" && keyspace.Service != \"\" {\n\t\t\t\t\t\t// If this location is for a keyspace in this service, add it\n\t\t\t\t\t\tif packageService := findServiceNameForPackage(md, location[\"package_path\"].(string)); packageService == keyspace.Service {\n\t\t\t\t\t\t\tkeyspaceInfo[\"name\"] = keyspaceName\n\t\t\t\t\t\t\tkeyspaceInfo[\"definition\"] = map[string]interface{}{\n\t\t\t\t\t\t\t\t\"filepath\":     location[\"filepath\"],\n\t\t\t\t\t\t\t\t\"line_start\":   location[\"line_start\"],\n\t\t\t\t\t\t\t\t\"line_end\":     location[\"line_end\"],\n\t\t\t\t\t\t\t\t\"column_start\": location[\"column_start\"],\n\t\t\t\t\t\t\t\t\"column_end\":   location[\"column_end\"],\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeyspaces = append(keyspaces, keyspaceInfo)\n\t\t}\n\n\t\tclusterInfo[\"keyspaces\"] = keyspaces\n\t\tresult = append(result, clusterInfo)\n\t}\n\n\t// Sort by cluster name for consistent output\n\tsort.Slice(result, func(i, j int) bool {\n\t\treturn result[i][\"name\"].(string) < result[j][\"name\"].(string)\n\t})\n\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal cache keyspaces information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/cron_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nfunc (m *Manager) registerCronTools() {\n\tm.server.AddTool(mcp.NewTool(\"get_cronjobs\",\n\t\tmcp.WithDescription(\"Retrieve detailed information about all scheduled cron jobs in the currently open Encore, including their schedules, endpoints they trigger, and execution history. This tool helps understand the application's background task scheduling and automation capabilities.\"),\n\t), m.getCronJobs)\n}\n\nfunc (m *Manager) getCronJobs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Create a map to find service and endpoint locations\n\tendpointLocations := make(map[string]map[string]map[string]interface{})\n\n\t// Scan through all packages to find trace nodes related to RPC definitions\n\tfor _, pkg := range md.Pkgs {\n\t\tfor _, node := range pkg.TraceNodes {\n\t\t\t// Check for RPC definitions\n\t\t\tif node.GetRpcDef() != nil {\n\t\t\t\trpcDef := node.GetRpcDef()\n\t\t\t\tserviceName := rpcDef.ServiceName\n\t\t\t\trpcName := rpcDef.RpcName\n\n\t\t\t\t// Initialize maps if needed\n\t\t\t\tif _, exists := endpointLocations[serviceName]; !exists {\n\t\t\t\t\tendpointLocations[serviceName] = make(map[string]map[string]interface{})\n\t\t\t\t}\n\n\t\t\t\tif _, exists := endpointLocations[serviceName][rpcName]; !exists {\n\t\t\t\t\tendpointLocations[serviceName][rpcName] = map[string]interface{}{\n\t\t\t\t\t\t\"filepath\":     node.Filepath,\n\t\t\t\t\t\t\"line_start\":   node.SrcLineStart,\n\t\t\t\t\t\t\"line_end\":     node.SrcLineEnd,\n\t\t\t\t\t\t\"column_start\": node.SrcColStart,\n\t\t\t\t\t\t\"column_end\":   node.SrcColEnd,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Process cron jobs with location information\n\tcronjobs := make([]map[string]interface{}, 0)\n\tfor _, job := range md.CronJobs {\n\t\tjobInfo := map[string]interface{}{\n\t\t\t\"id\":       job.Id,\n\t\t\t\"title\":    job.Title,\n\t\t\t\"schedule\": job.Schedule,\n\t\t}\n\n\t\t// Add documentation if available\n\t\tif job.Doc != nil {\n\t\t\tjobInfo[\"doc\"] = *job.Doc\n\t\t}\n\n\t\t// Add endpoint information\n\t\tif job.Endpoint != nil {\n\t\t\tendpoint := map[string]interface{}{\n\t\t\t\t\"package\": job.Endpoint.Pkg,\n\t\t\t\t\"name\":    job.Endpoint.Name,\n\t\t\t}\n\n\t\t\t// If we can find the service for this endpoint, add location info\n\t\t\tfor _, svc := range md.Svcs {\n\t\t\t\tfor _, rpc := range svc.Rpcs {\n\t\t\t\t\tif rpc.Name == job.Endpoint.Name && (svc.RelPath == job.Endpoint.Pkg || svc.Name == findServiceNameForPackage(md, job.Endpoint.Pkg)) {\n\t\t\t\t\t\tendpoint[\"service_name\"] = svc.Name\n\n\t\t\t\t\t\t// Add location if we found it\n\t\t\t\t\t\tif locations, ok := endpointLocations[svc.Name]; ok {\n\t\t\t\t\t\t\tif loc, ok := locations[rpc.Name]; ok {\n\t\t\t\t\t\t\t\tendpoint[\"definition\"] = loc\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tjobInfo[\"endpoint\"] = endpoint\n\t\t}\n\n\t\tcronjobs = append(cronjobs, jobInfo)\n\t}\n\n\tjsonData, err := json.Marshal(cronjobs)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal cron jobs information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/db_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/lib/pq\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/pkg/fns\"\n)\n\nfunc (m *Manager) registerDatabaseTools() {\n\t// Add tool for getting all databases and optionally their tables\n\tm.server.AddTool(mcp.NewTool(\"get_databases\",\n\t\tmcp.WithDescription(\"Retrieve metadata about all SQL databases defined in the currently open Encore, including their schema, tables, and relationships. This tool helps understand the database structure and can optionally include detailed table information.\"),\n\t\tmcp.WithBoolean(\"include_tables\", mcp.Description(\"When true, includes detailed information about each table in the database, including column names, types, and constraints. This is useful for understanding the complete database schema.\")),\n\t\tmcp.WithArray(\"databases\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"Optional list of specific database names to retrieve information for. If not provided, returns information for all databases in the currently open Encore.\",\n\t\t\t})),\n\t), m.getDatabases)\n\n\t// Add tool for querying a database\n\tm.server.AddTool(mcp.NewTool(\"query_database\",\n\t\tmcp.WithDescription(\"Execute SQL queries against one or more databases in the currently open Encore. This tool allows running custom SQL queries to inspect or manipulate data while respecting the application's database access patterns.\"),\n\t\tmcp.WithArray(\"queries\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"object\",\n\t\t\t\t\"description\": \"Array of query objects, where each object must contain 'database' (the database name to query) and 'query' (the SQL query to execute) fields. Multiple queries can be executed in a single call.\",\n\t\t\t\t\"properties\": map[string]any{\n\t\t\t\t\t\"database\": map[string]any{\n\t\t\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\t\t\"description\": \"The database name to query\",\n\t\t\t\t\t},\n\t\t\t\t\t\"query\": map[string]any{\n\t\t\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\t\t\"description\": \"The SQL query to execute\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"required\": []string{\"database\", \"query\"},\n\t\t\t})),\n\t), m.runQuery)\n}\n\nfunc (m *Manager) getDatabases(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\tincludeTables := false\n\tif includeTablesParam, ok := request.Params.Arguments[\"include_tables\"]; ok {\n\t\tincludeTables, _ = includeTablesParam.(bool)\n\t}\n\n\t// Parse databases parameter if provided\n\tvar filterDBs map[string]bool\n\tif dbsParam, ok := request.Params.Arguments[\"databases\"]; ok && dbsParam != nil {\n\t\tdbsArray, ok := dbsParam.([]interface{})\n\t\tif ok && len(dbsArray) > 0 {\n\t\t\tfilterDBs = make(map[string]bool)\n\t\t\tfor _, db := range dbsArray {\n\t\t\t\tif dbName, ok := db.(string); ok {\n\t\t\t\t\tfilterDBs[dbName] = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Build database list\n\tdatabases := make([]map[string]interface{}, 0)\n\tfor _, db := range md.SqlDatabases {\n\t\t// Skip if we have a filter and this database isn't in it\n\t\tif filterDBs != nil && !filterDBs[db.Name] {\n\t\t\tcontinue\n\t\t}\n\n\t\tdbInfo := map[string]interface{}{\n\t\t\t\"name\": db.Name,\n\t\t\t\"doc\":  db.Doc,\n\t\t}\n\n\t\t// If we should include tables, get table information\n\t\tif includeTables {\n\t\t\ttables, err := m.getTablesForDatabase(ctx, db.Name)\n\t\t\tif err != nil {\n\t\t\t\t// Don't fail the whole request if one database fails\n\t\t\t\tdbInfo[\"tables_error\"] = err.Error()\n\t\t\t} else {\n\t\t\t\tdbInfo[\"tables\"] = tables\n\t\t\t}\n\t\t}\n\n\t\tdatabases = append(databases, dbInfo)\n\t}\n\n\tjsonData, err := json.Marshal(databases)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal database list: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getTablesForDatabase(ctx context.Context, dbName string) ([]map[string]interface{}, error) {\n\tvar tables []map[string]interface{}\n\n\terr := m.withConn(ctx, dbName, func(db *sql.DB) error {\n\t\t// Query to get tables and their columns from PostgreSQL\n\t\tquery := `\n\t\t\tSELECT \n\t\t\t\tt.table_name,\n\t\t\t\tARRAY_AGG(c.column_name ORDER BY c.ordinal_position) as columns,\n\t\t\t\tARRAY_AGG(c.data_type ORDER BY c.ordinal_position) as column_types\n\t\t\tFROM \n\t\t\t\tinformation_schema.tables t\n\t\t\tJOIN \n\t\t\t\tinformation_schema.columns c ON t.table_name = c.table_name AND t.table_schema = c.table_schema\n\t\t\tWHERE \n\t\t\t\tt.table_schema = 'public'\n\t\t\tGROUP BY \n\t\t\t\tt.table_name\n\t\t\tORDER BY \n\t\t\t\tt.table_name;\n\t\t`\n\n\t\trows, err := db.QueryContext(ctx, query)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to query tables: %w\", err)\n\t\t}\n\t\tdefer rows.Close()\n\n\t\ttables = []map[string]interface{}{}\n\n\t\tfor rows.Next() {\n\t\t\tvar tableName string\n\t\t\tvar columns pq.StringArray\n\t\t\tvar columnTypes pq.StringArray\n\n\t\t\tif err := rows.Scan(&tableName, &columns, &columnTypes); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to scan row: %w\", err)\n\t\t\t}\n\n\t\t\t// Create structured column information\n\t\t\tcolumnInfo := make([]map[string]string, len(columns))\n\t\t\tfor i := range columns {\n\t\t\t\tcolumnInfo[i] = map[string]string{\n\t\t\t\t\t\"name\": columns[i],\n\t\t\t\t\t\"type\": columnTypes[i],\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttables = append(tables, map[string]interface{}{\n\t\t\t\t\"table_name\": tableName,\n\t\t\t\t\"columns\":    columnInfo,\n\t\t\t})\n\t\t}\n\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn fmt.Errorf(\"error iterating rows: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn tables, err\n}\n\nfunc (m *Manager) withConn(ctx context.Context, dbName string, fn func(db *sql.DB) error) error {\n\tapp, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\tclusterNS, err := m.ns.GetActive(ctx, app)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get active namespace: %w\", err)\n\t}\n\tmd, err := app.CachedMetadata()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\tclusterID := sqldb.GetClusterID(app, sqldb.Run, clusterNS)\n\tcluster := m.cluster.Create(ctx, &sqldb.CreateParams{\n\t\tClusterID: clusterID,\n\t\tMemfs:     sqldb.Run.Memfs(),\n\t})\n\tif _, err := cluster.Start(ctx, nil); err != nil {\n\t\treturn err\n\t} else if err := cluster.Setup(ctx, app.Root(), md); err != nil {\n\t\treturn err\n\t}\n\n\tinfo, err := cluster.Info(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get cluster info: %w\", err)\n\t} else if info.Status != sqldb.Running {\n\t\treturn errors.New(\"cluster not running\")\n\t}\n\n\tadmin, ok := info.Encore.First(sqldb.RoleRead)\n\tif !ok {\n\t\treturn errors.New(\"unable to find superuser or admin roles\")\n\t}\n\n\turi := info.ConnURI(dbName, admin)\n\n\tpool, err := sql.Open(\"pgx\", uri)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fns.CloseIgnore(pool)\n\n\treturn fn(pool)\n}\n\nfunc (m *Manager) runQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tqueriesParam, ok := request.Params.Arguments[\"queries\"].([]interface{})\n\tif !ok || len(queriesParam) == 0 {\n\t\treturn nil, fmt.Errorf(\"missing or invalid 'queries' parameter\")\n\t}\n\n\tresults := make(map[string][]map[string]interface{})\n\n\tfor _, queryObj := range queriesParam {\n\t\tqueryMap, ok := queryObj.(map[string]interface{})\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tdbName, ok := queryMap[\"database\"].(string)\n\t\tif !ok || dbName == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tsqlQuery, ok := queryMap[\"query\"].(string)\n\t\tif !ok || sqlQuery == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Execute the query for this database\n\t\tvar queryResults []map[string]interface{}\n\t\terr := m.withConn(ctx, dbName, func(db *sql.DB) error {\n\t\t\trows, err := db.QueryContext(ctx, sqlQuery)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to execute query: %w\", err)\n\t\t\t}\n\t\t\tdefer rows.Close()\n\n\t\t\t// Serialize rows to JSON\n\t\t\tcolumns, err := rows.Columns()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to get columns: %w\", err)\n\t\t\t}\n\n\t\t\tqueryResults = make([]map[string]interface{}, 0)\n\t\t\tfor rows.Next() {\n\t\t\t\tvalues := make([]interface{}, len(columns))\n\t\t\t\tvaluePtrs := make([]interface{}, len(columns))\n\t\t\t\tfor i := range values {\n\t\t\t\t\tvaluePtrs[i] = &values[i]\n\t\t\t\t}\n\n\t\t\t\tif err := rows.Scan(valuePtrs...); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to scan row: %w\", err)\n\t\t\t\t}\n\n\t\t\t\trow := make(map[string]interface{})\n\t\t\t\tfor i, col := range columns {\n\t\t\t\t\trow[col] = values[i]\n\t\t\t\t}\n\t\t\t\tqueryResults = append(queryResults, row)\n\t\t\t}\n\n\t\t\tif err := rows.Err(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error iterating rows: %w\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\t// Store results for this query\n\t\tkey := fmt.Sprintf(\"%s: %s\", dbName, sqlQuery)\n\t\tif err != nil {\n\t\t\tresults[key] = []map[string]interface{}{\n\t\t\t\t{\"error\": err.Error()},\n\t\t\t}\n\t\t} else {\n\t\t\tresults[key] = queryResults\n\t\t}\n\t}\n\n\tjsonData, err := json.Marshal(results)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal results: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/docs_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/algolia/algoliasearch-client-go/v3/algolia/opt\"\n\t\"github.com/algolia/algoliasearch-client-go/v3/algolia/search\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"golang.org/x/net/html\"\n)\n\nfunc (m *Manager) registerDocsTools() {\n\t// Add tool for searching Encore documentation using Algolia\n\tm.server.AddTool(mcp.NewTool(\"search_docs\",\n\t\tmcp.WithDescription(\"Search the Encore documentation using Algolia's search engine. This tool helps find relevant documentation about Encore features, best practices, and examples.\"),\n\t\tmcp.WithString(\"query\", mcp.Description(\"The search query to find relevant documentation. Can include keywords, feature names, or specific topics you're looking for.\")),\n\t\tmcp.WithNumber(\"page\", mcp.Description(\"Page number for pagination, starting from 0. Use this to navigate through large result sets.\")),\n\t\tmcp.WithNumber(\"hits_per_page\", mcp.Description(\"Number of results to return per page. Default is 10. Adjust this to control the size of the result set.\")),\n\t\tmcp.WithArray(\"facet_filters\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"Optional array of facet filters to narrow down search results. These can include categories, tags, or other metadata to refine the search.\",\n\t\t\t})),\n\t), m.searchDocs)\n\n\t// Add tool for fetching Encore documentation content\n\tm.server.AddTool(mcp.NewTool(\"get_docs\",\n\t\tmcp.WithDescription(\"Retrieve the full content of specific documentation pages. This tool is useful for getting detailed information about specific topics after finding them with search_docs.\"),\n\t\tmcp.WithArray(\"paths\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"List of documentation paths to fetch (e.g. ['/docs/concepts', '/docs/services']). These paths should be valid documentation URLs without the domain.\",\n\t\t\t})),\n\t), m.getDocs)\n}\n\nfunc (m *Manager) searchDocs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t// Extract parameters from the request\n\tquery, ok := request.Params.Arguments[\"query\"].(string)\n\tif !ok || query == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid or missing query parameter\")\n\t}\n\n\t// Default pagination settings\n\tpage := 0\n\tif p, ok := request.Params.Arguments[\"page\"].(float64); ok {\n\t\tpage = int(p)\n\t}\n\n\thitsPerPage := 10\n\tif hpp, ok := request.Params.Arguments[\"hits_per_page\"].(float64); ok {\n\t\thitsPerPage = int(hpp)\n\t}\n\n\t// Process facet filters if provided\n\tvar facetFilters []string\n\tif filters, ok := request.Params.Arguments[\"facet_filters\"].([]interface{}); ok {\n\t\tfor _, filter := range filters {\n\t\t\tif filterStr, ok := filter.(string); ok && filterStr != \"\" {\n\t\t\t\tfacetFilters = append(facetFilters, filterStr)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Set context timeout\n\tctx, cancel := context.WithTimeout(ctx, 10*time.Second)\n\tdefer cancel()\n\n\t// Perform the actual search with Algolia\n\tresult, err := performAlgoliaSearch(ctx, query, page, hitsPerPage, facetFilters)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to search docs: %w\", err)\n\t}\n\n\t// Marshal the response\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal search results: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\n// performAlgoliaSearch performs the actual search against Algolia\nfunc performAlgoliaSearch(ctx context.Context, query string, page, hitsPerPage int, facetFilters []string) (map[string]interface{}, error) {\n\t// Initialize Algolia client with configurable app ID and API key\n\t// In a production environment, these should be loaded from configuration\n\tappID := \"R7DAHI8GEL\"\n\tapiKey := \"85bf0533142cccdbbc6b9deb92b19fdf\"\n\n\tclient := search.NewClient(appID, apiKey)\n\tindex := client.InitIndex(\"encore_docs\")\n\n\t// Build search parameters\n\tparams := []interface{}{\n\t\topt.Page(page),\n\t\topt.HitsPerPage(hitsPerPage),\n\t}\n\n\t// Add facet filters if any\n\tif len(facetFilters) > 0 {\n\t\t// For a simple AND of all filters - need to convert []string to variadic arguments\n\t\tif len(facetFilters) == 1 {\n\t\t\tparams = append(params, opt.FacetFilter(facetFilters[0]))\n\t\t} else {\n\t\t\t// Convert []string to []interface{} for compatibility\n\t\t\tfacetFilterInterfaces := make([]interface{}, len(facetFilters))\n\t\t\tfor i, filter := range facetFilters {\n\t\t\t\tfacetFilterInterfaces[i] = filter\n\t\t\t}\n\t\t\tparams = append(params, opt.FacetFilterAnd(facetFilterInterfaces...))\n\t\t}\n\t}\n\n\t// Perform the search\n\tres, err := index.Search(query, params...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"algolia search failed: %w\", err)\n\t}\n\n\t// Convert the Algolia response to our expected format\n\tresult := map[string]interface{}{\n\t\t\"hits\":             res.Hits,\n\t\t\"page\":             res.Page,\n\t\t\"nbHits\":           res.NbHits,\n\t\t\"nbPages\":          res.NbPages,\n\t\t\"hitsPerPage\":      res.HitsPerPage,\n\t\t\"processingTimeMS\": res.ProcessingTimeMS,\n\t\t\"query\":            query,\n\t\t\"params\":           res.Params,\n\t}\n\n\treturn result, nil\n}\n\nfunc (m *Manager) getDocs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t// Extract paths parameter from the request\n\tvar docPaths []string\n\tif paths, ok := request.Params.Arguments[\"paths\"].([]interface{}); ok {\n\t\tfor _, path := range paths {\n\t\t\tif pathStr, ok := path.(string); ok && pathStr != \"\" {\n\t\t\t\tdocPaths = append(docPaths, pathStr)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(docPaths) == 0 {\n\t\treturn nil, fmt.Errorf(\"no valid documentation paths provided\")\n\t}\n\n\t// Set context timeout\n\tctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n\tdefer cancel()\n\n\t// Fetch content for each path\n\tresult := make(map[string]interface{})\n\tdocs := make(map[string]interface{})\n\n\tfor _, path := range docPaths {\n\t\t// Ensure path starts with a slash\n\t\tif !strings.HasPrefix(path, \"/\") {\n\t\t\tpath = \"/\" + path\n\t\t}\n\n\t\turl := \"https://encore.dev\" + path\n\t\tcontent, err := fetchDocContent(ctx, url)\n\t\tif err != nil {\n\t\t\tdocs[path] = map[string]interface{}{\n\t\t\t\t\"error\":   err.Error(),\n\t\t\t\t\"success\": false,\n\t\t\t}\n\t\t} else {\n\t\t\tdocs[path] = map[string]interface{}{\n\t\t\t\t\"content\": content,\n\t\t\t\t\"url\":     url,\n\t\t\t\t\"success\": true,\n\t\t\t}\n\t\t}\n\t}\n\n\tresult[\"docs\"] = docs\n\tresult[\"summary\"] = map[string]interface{}{\n\t\t\"total\":        len(docPaths),\n\t\t\"base_url\":     \"https://encore.dev\",\n\t\t\"requested_at\": time.Now().UTC().Format(time.RFC3339),\n\t}\n\n\t// Marshal the response\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal document results: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\n// fetchDocContent fetches content from a URL and returns only the text content from the <main> tag\nfunc fetchDocContent(ctx context.Context, url string) (string, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\t// Add appropriate headers to mimic a browser request\n\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36\")\n\treq.Header.Set(\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\")\n\n\tclient := &http.Client{\n\t\tTimeout: 10 * time.Second,\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch URL: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"received non-OK status code: %d\", resp.StatusCode)\n\t}\n\n\t// Parse the HTML document\n\tdoc, err := html.Parse(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse HTML: %w\", err)\n\t}\n\n\t// Find the main tag\n\tmainNode := findMainElement(doc)\n\tif mainNode == nil {\n\t\treturn \"\", fmt.Errorf(\"no <main> tag found in the document\")\n\t}\n\n\t// Extract text content from the main tag\n\tvar textContent strings.Builder\n\textractText(mainNode, &textContent)\n\n\t// Clean up the text content\n\tcleanedText := cleanText(textContent.String())\n\n\treturn cleanedText, nil\n}\n\n// findMainElement finds the <main> element in the HTML document\nfunc findMainElement(n *html.Node) *html.Node {\n\tif n.Type == html.ElementNode && strings.ToLower(n.Data) == \"main\" {\n\t\treturn n\n\t}\n\n\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\tif result := findMainElement(c); result != nil {\n\t\t\treturn result\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// extractText recursively extracts text nodes from an HTML node\nfunc extractText(n *html.Node, sb *strings.Builder) {\n\t// Skip script, style, and non-visible elements\n\tif n.Type == html.ElementNode {\n\t\tnodeName := strings.ToLower(n.Data)\n\t\tif nodeName == \"script\" || nodeName == \"style\" || nodeName == \"noscript\" ||\n\t\t\tnodeName == \"meta\" || nodeName == \"link\" || nodeName == \"iframe\" {\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Process text nodes\n\tif n.Type == html.TextNode {\n\t\ttext := strings.TrimSpace(n.Data)\n\t\tif text != \"\" {\n\t\t\tsb.WriteString(text)\n\t\t\tsb.WriteString(\" \")\n\t\t}\n\t}\n\n\t// Recursively process all child nodes\n\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\textractText(c, sb)\n\t}\n\n\t// Add line breaks for certain block elements\n\tif n.Type == html.ElementNode {\n\t\tnodeName := strings.ToLower(n.Data)\n\t\tif nodeName == \"p\" || nodeName == \"div\" || nodeName == \"h1\" ||\n\t\t\tnodeName == \"h2\" || nodeName == \"h3\" || nodeName == \"h4\" ||\n\t\t\tnodeName == \"h5\" || nodeName == \"h6\" || nodeName == \"li\" ||\n\t\t\tnodeName == \"br\" || nodeName == \"tr\" {\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\n\t\t// Add extra line break for more significant sections\n\t\tif nodeName == \"section\" || nodeName == \"article\" ||\n\t\t\tnodeName == \"header\" || nodeName == \"footer\" {\n\t\t\tsb.WriteString(\"\\n\\n\")\n\t\t}\n\t}\n}\n\n// cleanText removes excessive whitespace and normalizes line breaks\nfunc cleanText(text string) string {\n\t// Replace multiple spaces with a single space\n\ttext = regexp.MustCompile(`\\s+`).ReplaceAllString(text, \" \")\n\n\t// Replace multiple newlines with a maximum of two\n\ttext = regexp.MustCompile(`\\n{3,}`).ReplaceAllString(text, \"\\n\\n\")\n\n\t// Trim leading/trailing whitespace\n\ttext = strings.TrimSpace(text)\n\n\treturn text\n}\n"
  },
  {
    "path": "cli/daemon/mcp/mcp.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/mark3labs/mcp-go/server\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/objects\"\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/daemon/sqldb\"\n)\n\ntype Manager struct {\n\tserver  *server.MCPServer\n\tsse     *server.SSEServer\n\tcluster *sqldb.ClusterManager\n\tns      *namespace.Manager\n\ttraces  trace2.Store\n\trun     *run.Manager\n\tobjects *objects.ClusterManager\n\tapps    *apps.Manager\n\n\tBaseURL string\n}\n\ntype appContextKey struct{}\n\ntype appContext struct {\n\tAppID string\n}\n\nfunc WithAppID(ctx context.Context, appID string) context.Context {\n\treturn context.WithValue(ctx, appContextKey{}, &appContext{AppID: appID})\n}\n\nfunc GetAppID(ctx context.Context) (string, bool) {\n\tif appCtx, ok := ctx.Value(appContextKey{}).(*appContext); ok {\n\t\treturn appCtx.AppID, true\n\t}\n\treturn \"\", false\n}\n\nfunc NewManager(apps *apps.Manager, cluster *sqldb.ClusterManager, ns *namespace.Manager, traces trace2.Store, runMgr *run.Manager, baseURL string) *Manager {\n\t// Create hooks for handling session registration\n\thooks := &server.Hooks{}\n\n\t// Create a new MCP server\n\ts := server.NewMCPServer(\n\t\t\"Encore MCP Server\",\n\t\t\"1.0.0\",\n\t\tserver.WithToolCapabilities(false),\n\t\tserver.WithHooks(hooks),\n\t)\n\n\tm := &Manager{\n\t\tserver: s,\n\t\tsse: server.NewSSEServer(s,\n\t\t\tserver.WithAppendQueryToMessageEndpoint(),\n\t\t\tserver.WithKeepAlive(true),\n\t\t\tserver.WithHTTPContextFunc(addAppToContext)),\n\t\tapps:    apps,\n\t\tns:      ns,\n\t\tcluster: cluster,\n\t\ttraces:  traces,\n\t\trun:     runMgr,\n\t\tBaseURL: baseURL,\n\t}\n\n\tm.registerDatabaseTools()\n\tm.registerTraceTools()\n\tm.registerAPITools()\n\tm.registerPubSubTools()\n\tm.registerSrcTools()\n\tm.registerBucketTools()\n\tm.registerCacheTools()\n\tm.registerMetricsTools()\n\tm.registerCronTools()\n\tm.registerSecretTools()\n\tm.registerDocsTools()\n\n\tm.registerTraceResources()\n\treturn m\n}\n\nfunc addAppToContext(ctx context.Context, r *http.Request) context.Context {\n\tif appID := r.URL.Query().Get(\"app\"); appID != \"\" {\n\t\treturn WithAppID(ctx, appID)\n\t}\n\treturn ctx\n}\n\nfunc (m *Manager) Serve(listener net.Listener) error {\n\treturn http.Serve(listener, m.sse)\n}\n\nfunc (m *Manager) getApp(ctx context.Context) (*apps.Instance, error) {\n\tappID, ok := GetAppID(ctx)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"app not found in context\")\n\t}\n\tinst, err := m.apps.FindLatestByPlatformOrLocalID(appID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find app: %w\", err)\n\t}\n\treturn inst, nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/metrics_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nfunc (m *Manager) registerMetricsTools() {\n\tm.server.AddTool(mcp.NewTool(\"get_metrics\",\n\t\tmcp.WithDescription(\"Retrieve comprehensive information about all metrics defined in the currently open Encore, including their types, labels, documentation, and usage across services. This tool helps understand the application's observability and monitoring capabilities.\"),\n\t), m.getMetrics)\n}\n\nfunc (m *Manager) getMetrics(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Group metrics by service for better organization\n\tmetricsByService := make(map[string][]map[string]interface{})\n\tglobalMetrics := make([]map[string]interface{}, 0)\n\n\t// Process all metrics\n\tfor _, metric := range md.Metrics {\n\t\tmetricInfo := map[string]interface{}{\n\t\t\t\"name\":       metric.Name,\n\t\t\t\"kind\":       metric.Kind.String(),\n\t\t\t\"value_type\": metric.ValueType.String(),\n\t\t\t\"doc\":        metric.Doc,\n\t\t}\n\n\t\t// Add labels if any\n\t\tif len(metric.Labels) > 0 {\n\t\t\tlabels := make([]map[string]interface{}, 0, len(metric.Labels))\n\t\t\tfor _, label := range metric.Labels {\n\t\t\t\tlabelInfo := map[string]interface{}{\n\t\t\t\t\t\"key\":  label.Key,\n\t\t\t\t\t\"type\": label.Type.String(),\n\t\t\t\t\t\"doc\":  label.Doc,\n\t\t\t\t}\n\t\t\t\tlabels = append(labels, labelInfo)\n\t\t\t}\n\t\t\tmetricInfo[\"labels\"] = labels\n\t\t}\n\n\t\t// Add to appropriate group (service-specific or global)\n\t\tif metric.ServiceName != nil {\n\t\t\tserviceName := *metric.ServiceName\n\t\t\tif _, exists := metricsByService[serviceName]; !exists {\n\t\t\t\tmetricsByService[serviceName] = make([]map[string]interface{}, 0)\n\t\t\t}\n\t\t\tmetricsByService[serviceName] = append(metricsByService[serviceName], metricInfo)\n\t\t} else {\n\t\t\tglobalMetrics = append(globalMetrics, metricInfo)\n\t\t}\n\t}\n\n\t// Build the final result\n\tresult := map[string]interface{}{\n\t\t\"services\": make(map[string]interface{}),\n\t\t\"global\":   globalMetrics,\n\t}\n\n\t// Add each service's metrics\n\tservicesMap := result[\"services\"].(map[string]interface{})\n\tfor serviceName, metrics := range metricsByService {\n\t\t// Sort metrics by name within each service\n\t\tsort.Slice(metrics, func(i, j int) bool {\n\t\t\treturn metrics[i][\"name\"].(string) < metrics[j][\"name\"].(string)\n\t\t})\n\t\tservicesMap[serviceName] = metrics\n\t}\n\n\t// Also sort global metrics\n\tsort.Slice(globalMetrics, func(i, j int) bool {\n\t\treturn globalMetrics[i][\"name\"].(string) < globalMetrics[j][\"name\"].(string)\n\t})\n\n\t// Add summary counts\n\tsummary := map[string]interface{}{\n\t\t\"total_metrics\":      len(md.Metrics),\n\t\t\"global_metrics\":     len(globalMetrics),\n\t\t\"service_count\":      len(metricsByService),\n\t\t\"metrics_by_service\": make(map[string]int),\n\t\t\"metrics_by_kind\":    make(map[string]int),\n\t\t\"metrics_by_type\":    make(map[string]int),\n\t}\n\n\t// Count metrics by service\n\tfor service, metrics := range metricsByService {\n\t\tsummary[\"metrics_by_service\"].(map[string]int)[service] = len(metrics)\n\t}\n\n\t// Count metrics by kind and type\n\tkindCounts := make(map[string]int)\n\ttypeCounts := make(map[string]int)\n\tfor _, metric := range md.Metrics {\n\t\tkindStr := metric.Kind.String()\n\t\tkindCounts[kindStr] = kindCounts[kindStr] + 1\n\n\t\ttypeStr := metric.ValueType.String()\n\t\ttypeCounts[typeStr] = typeCounts[typeStr] + 1\n\t}\n\tsummary[\"metrics_by_kind\"] = kindCounts\n\tsummary[\"metrics_by_type\"] = typeCounts\n\n\tresult[\"summary\"] = summary\n\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal metrics information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/pubsub_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n)\n\nfunc (m *Manager) registerPubSubTools() {\n\tm.server.AddTool(mcp.NewTool(\"get_pubsub\",\n\t\tmcp.WithDescription(\"Retrieve detailed information about all PubSub topics and their subscriptions in the currently open Encore. This includes topic configurations, subscription patterns, message schemas, and the services that publish to or subscribe to each topic.\"),\n\t), m.getPubSub)\n}\n\nfunc (m *Manager) getPubSub(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Create a map to find topic and subscription definitions from trace nodes\n\ttopicDefLocations := make(map[string]map[string]interface{})\n\tsubscriptionDefLocations := make(map[string]map[string]map[string]interface{})\n\n\t// Scan through all packages to find trace nodes related to pubsub\n\tfor _, pkg := range md.Pkgs {\n\t\tfor _, node := range pkg.TraceNodes {\n\t\t\t// Check for topic definition nodes\n\t\t\tif node.GetPubsubTopicDef() != nil {\n\t\t\t\ttopicDef := node.GetPubsubTopicDef()\n\t\t\t\tif _, exists := topicDefLocations[topicDef.TopicName]; !exists {\n\t\t\t\t\ttopicDefLocations[topicDef.TopicName] = map[string]interface{}{\n\t\t\t\t\t\t\"filepath\":     node.Filepath,\n\t\t\t\t\t\t\"line_start\":   node.SrcLineStart,\n\t\t\t\t\t\t\"line_end\":     node.SrcLineEnd,\n\t\t\t\t\t\t\"column_start\": node.SrcColStart,\n\t\t\t\t\t\t\"column_end\":   node.SrcColEnd,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check for subscription definition nodes\n\t\t\tif node.GetPubsubSubscriber() != nil {\n\t\t\t\tsubDef := node.GetPubsubSubscriber()\n\t\t\t\tif _, exists := subscriptionDefLocations[subDef.TopicName]; !exists {\n\t\t\t\t\tsubscriptionDefLocations[subDef.TopicName] = make(map[string]map[string]interface{})\n\t\t\t\t}\n\n\t\t\t\tif _, exists := subscriptionDefLocations[subDef.TopicName][subDef.SubscriberName]; !exists {\n\t\t\t\t\tsubscriptionDefLocations[subDef.TopicName][subDef.SubscriberName] = map[string]interface{}{\n\t\t\t\t\t\t\"filepath\":     node.Filepath,\n\t\t\t\t\t\t\"line_start\":   node.SrcLineStart,\n\t\t\t\t\t\t\"line_end\":     node.SrcLineEnd,\n\t\t\t\t\t\t\"column_start\": node.SrcColStart,\n\t\t\t\t\t\t\"column_end\":   node.SrcColEnd,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now build the response with locations\n\ttopics := make([]map[string]interface{}, 0)\n\tfor _, topic := range md.PubsubTopics {\n\t\t// Extract publishers\n\t\tpublishers := make([]map[string]interface{}, 0)\n\t\tfor _, publisher := range topic.Publishers {\n\t\t\tpublishers = append(publishers, map[string]interface{}{\n\t\t\t\t\"service_name\": publisher.ServiceName,\n\t\t\t})\n\t\t}\n\n\t\t// Extract subscriptions\n\t\tsubscriptions := make([]map[string]interface{}, 0)\n\t\tfor _, subscription := range topic.Subscriptions {\n\t\t\tsubscriptionInfo := map[string]interface{}{\n\t\t\t\t\"name\":         subscription.Name,\n\t\t\t\t\"service_name\": subscription.ServiceName,\n\t\t\t}\n\n\t\t\t// Add location information for subscription if available\n\t\t\tif subLocations, topicExists := subscriptionDefLocations[topic.Name]; topicExists {\n\t\t\t\tif subLocation, subExists := subLocations[subscription.Name]; subExists {\n\t\t\t\t\tsubscriptionInfo[\"definition\"] = subLocation\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add optional fields if they're set\n\t\t\tif subscription.AckDeadline > 0 {\n\t\t\t\tsubscriptionInfo[\"ack_deadline\"] = formatDuration(subscription.AckDeadline)\n\t\t\t}\n\t\t\tif subscription.MessageRetention > 0 {\n\t\t\t\tsubscriptionInfo[\"message_retention\"] = formatDuration(subscription.MessageRetention)\n\t\t\t}\n\t\t\tif subscription.MaxConcurrency != nil {\n\t\t\t\tsubscriptionInfo[\"max_concurrency\"] = *subscription.MaxConcurrency\n\t\t\t}\n\n\t\t\t// Add retry policy if available\n\t\t\tif subscription.RetryPolicy != nil {\n\t\t\t\tretryPolicy := map[string]interface{}{}\n\t\t\t\tif subscription.RetryPolicy.MinBackoff > 0 {\n\t\t\t\t\tretryPolicy[\"min_backoff\"] = formatDuration(subscription.RetryPolicy.MinBackoff)\n\t\t\t\t}\n\t\t\t\tif subscription.RetryPolicy.MaxBackoff > 0 {\n\t\t\t\t\tretryPolicy[\"max_backoff\"] = formatDuration(subscription.RetryPolicy.MaxBackoff)\n\t\t\t\t}\n\t\t\t\tif subscription.RetryPolicy.MaxRetries > 0 {\n\t\t\t\t\tretryPolicy[\"max_retries\"] = subscription.RetryPolicy.MaxRetries\n\t\t\t\t}\n\t\t\t\tsubscriptionInfo[\"retry_policy\"] = retryPolicy\n\t\t\t}\n\n\t\t\tsubscriptions = append(subscriptions, subscriptionInfo)\n\t\t}\n\n\t\t// Build topic info\n\t\ttopicInfo := map[string]interface{}{\n\t\t\t\"name\":               topic.Name,\n\t\t\t\"publishers\":         publishers,\n\t\t\t\"subscriptions\":      subscriptions,\n\t\t\t\"delivery_guarantee\": topic.DeliveryGuarantee.String(),\n\t\t}\n\n\t\t// Add location information for topic if available\n\t\tif location, exists := topicDefLocations[topic.Name]; exists {\n\t\t\ttopicInfo[\"definition\"] = location\n\t\t}\n\n\t\t// Add documentation if available\n\t\tif topic.Doc != nil {\n\t\t\ttopicInfo[\"doc\"] = *topic.Doc\n\t\t}\n\n\t\t// Add ordering key if available\n\t\tif topic.OrderingKey != \"\" {\n\t\t\ttopicInfo[\"ordering_key\"] = topic.OrderingKey\n\t\t}\n\n\t\t// Add message type if available\n\t\tif topic.MessageType != nil {\n\t\t\tmessageTypeData, err := protojson.Marshal(topic.MessageType)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to marshal message type: %w\", err)\n\t\t\t}\n\t\t\tvar messageTypeJson interface{}\n\t\t\tif err := json.Unmarshal(messageTypeData, &messageTypeJson); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to unmarshal message type JSON: %w\", err)\n\t\t\t}\n\t\t\ttopicInfo[\"message_type\"] = messageTypeJson\n\t\t}\n\n\t\ttopics = append(topics, topicInfo)\n\t}\n\n\tjsonData, err := json.Marshal(topics)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal PubSub information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/schema_json.go",
    "content": "package mcp\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n// FieldLocation represents where a field is located in the API request/response\ntype FieldLocation int\n\nconst (\n\tFieldLocationBody   FieldLocation = 0\n\tFieldLocationQuery  FieldLocation = 1\n\tFieldLocationHeader FieldLocation = 2\n\tFieldLocationCookie FieldLocation = 3\n\tFieldLocationUnused FieldLocation = 4\n)\n\n// DescribedField is a field with additional metadata\ntype DescribedField struct {\n\t*schema.Field\n\tSrcName  string\n\tName     string\n\tLocation FieldLocation\n}\n\n// StructBits generates JSON representations of a struct's fields separated by location\n// It returns query, headers, cookies, and JSON body as strings\nfunc StructBits(s *schema.Struct, method string, asResponse bool, asGoStruct bool, queryParamsAsObject bool) (query, headers, cookies, jsonBody string) {\n\t// Split the fields by location\n\tfieldsByLocation := splitFieldsByLocation(s, method, asResponse)\n\n\t// Generate query string\n\tif len(fieldsByLocation[FieldLocationQuery]) > 0 {\n\t\tif asGoStruct || queryParamsAsObject {\n\t\t\tquery = writeFieldsAsJSON(fieldsByLocation[FieldLocationQuery], asGoStruct)\n\t\t} else {\n\t\t\tvar queryParams []string\n\t\t\tfor _, field := range fieldsByLocation[FieldLocationQuery] {\n\t\t\t\tfieldName := field.Name\n\t\t\t\tfieldValue := renderFieldValueAsQueryParam(field.Typ)\n\n\t\t\t\tqueryParams = append(queryParams, url.QueryEscape(fieldName)+\"=\"+fieldValue)\n\n\t\t\t\t// If it's a list, add a second parameter to show it's a list\n\t\t\t\tif field.Typ.GetList() != nil {\n\t\t\t\t\tqueryParams = append(queryParams, url.QueryEscape(fieldName)+\"=\"+fieldValue)\n\t\t\t\t}\n\t\t\t}\n\t\t\tquery = \"?\" + strings.Join(queryParams, \"&\")\n\t\t}\n\t}\n\n\t// Generate headers\n\tif len(fieldsByLocation[FieldLocationHeader]) > 0 {\n\t\theaders = writeFieldsAsJSON(fieldsByLocation[FieldLocationHeader], asGoStruct)\n\t}\n\n\t// Generate cookies\n\tif len(fieldsByLocation[FieldLocationCookie]) > 0 {\n\t\tcookies = writeCookiesAsJSON(fieldsByLocation[FieldLocationCookie], asGoStruct)\n\t}\n\n\t// Generate JSON body\n\tif len(fieldsByLocation[FieldLocationBody]) > 0 {\n\t\tjsonBody = writeFieldsAsJSON(fieldsByLocation[FieldLocationBody], asGoStruct)\n\t}\n\n\treturn\n}\n\n// writeFieldsAsJSON renders a list of fields as a JSON object\nfunc writeFieldsAsJSON(fields []DescribedField, asGoStruct bool) string {\n\tvar buf bytes.Buffer\n\tbuf.WriteString(\"\\n\")\n\n\tfor i, f := range fields {\n\t\tfieldName := f.SrcName\n\t\tif !asGoStruct {\n\t\t\tfieldName = f.Name\n\t\t}\n\n\t\tbuf.WriteString(\"    \\\"\")\n\t\tbuf.WriteString(fieldName)\n\t\tbuf.WriteString(\"\\\": \")\n\n\t\trenderTypeValue(&buf, f.Typ)\n\n\t\tif i < len(fields)-1 {\n\t\t\tbuf.WriteString(\",\")\n\t\t}\n\t\tbuf.WriteString(\"\\n\")\n\t}\n\n\treturn buf.String()\n}\n\n// writeCookiesAsJSON renders cookie fields as JSON\nfunc writeCookiesAsJSON(fields []DescribedField, asGoStruct bool) string {\n\tvar buf bytes.Buffer\n\tbuf.WriteString(\"\\n\")\n\n\tfor i, f := range fields {\n\t\tfieldName := f.SrcName\n\t\tif !asGoStruct {\n\t\t\tfieldName = f.Name\n\t\t}\n\n\t\tbuf.WriteString(\"    \\\"\")\n\t\tbuf.WriteString(fieldName)\n\t\tbuf.WriteString(\"\\\": \")\n\n\t\t// If it's a builtin, render it normally, otherwise render as an empty string\n\t\tif f.Typ.GetBuiltin() != schema.Builtin_ANY {\n\t\t\trenderTypeValue(&buf, f.Typ)\n\t\t} else {\n\t\t\tbuf.WriteString(\"\\\"\\\"\")\n\t\t}\n\n\t\tif i < len(fields)-1 {\n\t\t\tbuf.WriteString(\",\")\n\t\t}\n\t\tbuf.WriteString(\"\\n\")\n\t}\n\n\treturn buf.String()\n}\n\n// renderTypeValue renders a type value to the buffer\nfunc renderTypeValue(buf *bytes.Buffer, typ *schema.Type) {\n\tswitch {\n\tcase typ.GetBuiltin() != schema.Builtin_ANY:\n\t\trenderBuiltinValue(buf, typ.GetBuiltin(), false)\n\tcase typ.GetList() != nil:\n\t\tbuf.WriteString(\"[\")\n\t\trenderTypeValue(buf, typ.GetList().Elem)\n\t\tbuf.WriteString(\"]\")\n\tcase typ.GetStruct() != nil:\n\t\tbuf.WriteString(\"{\")\n\t\tfor i, f := range typ.GetStruct().Fields {\n\t\t\tif f.JsonName == \"-\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tjsonName := f.JsonName\n\t\t\tif jsonName == \"\" {\n\t\t\t\tjsonName = f.Name\n\t\t\t}\n\n\t\t\tbuf.WriteString(\"\\\"\")\n\t\t\tbuf.WriteString(jsonName)\n\t\t\tbuf.WriteString(\"\\\": \")\n\n\t\t\trenderTypeValue(buf, f.Typ)\n\n\t\t\tif i < len(typ.GetStruct().Fields)-1 {\n\t\t\t\tbuf.WriteString(\", \")\n\t\t\t}\n\t\t}\n\t\tbuf.WriteString(\"}\")\n\tcase typ.GetMap() != nil:\n\t\tbuf.WriteString(\"{\")\n\t\trenderTypeValue(buf, typ.GetMap().Key)\n\t\tbuf.WriteString(\": \")\n\t\trenderTypeValue(buf, typ.GetMap().Value)\n\t\tbuf.WriteString(\"}\")\n\tcase typ.GetNamed() != nil:\n\t\t// Just render as null for simplicity\n\t\tbuf.WriteString(\"null\")\n\tcase typ.GetPointer() != nil:\n\t\trenderTypeValue(buf, typ.GetPointer().Base)\n\tcase typ.GetUnion() != nil && len(typ.GetUnion().Types) > 0:\n\t\t// Just render the first type of the union\n\t\trenderTypeValue(buf, typ.GetUnion().Types[0])\n\tcase typ.GetLiteral() != nil:\n\t\trenderLiteralValue(buf, typ.GetLiteral())\n\tdefault:\n\t\tbuf.WriteString(\"<unknown>\")\n\t}\n}\n\n// renderBuiltinValue renders a builtin type value\nfunc renderBuiltinValue(buf *bytes.Buffer, b schema.Builtin, urlEncode bool) {\n\tvar value string\n\n\tswitch b {\n\tcase schema.Builtin_ANY:\n\t\tvalue = \"<any data>\"\n\tcase schema.Builtin_BOOL:\n\t\tvalue = \"false\"\n\tcase schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64,\n\t\tschema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64:\n\t\tvalue = \"0\"\n\tcase schema.Builtin_FLOAT32, schema.Builtin_FLOAT64:\n\t\tvalue = \"0.0\"\n\tcase schema.Builtin_STRING:\n\t\tvalue = \"\\\"\\\"\"\n\tcase schema.Builtin_BYTES:\n\t\tvalue = \"\\\"\\\" /* base64 */\"\n\tcase schema.Builtin_TIME:\n\t\tvalue = \"\\\"2009-11-10T23:00:00Z\\\"\"\n\tcase schema.Builtin_UUID:\n\t\tvalue = \"\\\"7d42f515-3517-4e76-be13-30880443546f\\\"\"\n\tcase schema.Builtin_JSON:\n\t\tvalue = \"{}\"\n\tcase schema.Builtin_USER_ID:\n\t\tvalue = \"\\\"userID\\\"\"\n\tcase schema.Builtin_DECIMAL:\n\t\tvalue = \"\\\"0.0\\\"\"\n\tdefault:\n\t\tvalue = \"<unknown>\"\n\t}\n\n\tif urlEncode {\n\t\t// Remove quotes for URL encoding if they exist\n\t\tif len(value) >= 2 && value[0] == '\"' && value[len(value)-1] == '\"' {\n\t\t\tvalue = value[1 : len(value)-1]\n\t\t}\n\t\tbuf.WriteString(url.QueryEscape(value))\n\t} else {\n\t\tbuf.WriteString(value)\n\t}\n}\n\n// renderLiteralValue renders a literal value\nfunc renderLiteralValue(buf *bytes.Buffer, lit *schema.Literal) {\n\tswitch v := lit.Value.(type) {\n\tcase *schema.Literal_Boolean:\n\t\tif v.Boolean {\n\t\t\tbuf.WriteString(\"true\")\n\t\t} else {\n\t\t\tbuf.WriteString(\"false\")\n\t\t}\n\tcase *schema.Literal_Int:\n\t\tbuf.WriteString(strconv.FormatInt(v.Int, 10))\n\tcase *schema.Literal_Float:\n\t\tbuf.WriteString(strconv.FormatFloat(v.Float, 'f', -1, 64))\n\tcase *schema.Literal_Str:\n\t\tjsonStr, _ := json.Marshal(v.Str)\n\t\tbuf.Write(jsonStr)\n\tcase *schema.Literal_Null:\n\t\tbuf.WriteString(\"null\")\n\tdefault:\n\t\tbuf.WriteString(\"<unknown>\")\n\t}\n}\n\n// renderFieldValueAsQueryParam returns a URL-encoded string representation of a field's value\nfunc renderFieldValueAsQueryParam(typ *schema.Type) string {\n\tvar buf bytes.Buffer\n\n\tif typ.GetBuiltin() != schema.Builtin_ANY {\n\t\trenderBuiltinValue(&buf, typ.GetBuiltin(), true)\n\t} else if typ.GetList() != nil {\n\t\trenderTypeValue(&buf, typ.GetList().Elem)\n\t} else {\n\t\tbuf.WriteString(\"<value>\")\n\t}\n\n\treturn buf.String()\n}\n\n// splitFieldsByLocation categorizes struct fields by their HTTP location\nfunc splitFieldsByLocation(s *schema.Struct, method string, asResponse bool) map[FieldLocation][]DescribedField {\n\tresult := make(map[FieldLocation][]DescribedField)\n\n\tfor _, f := range s.Fields {\n\t\tname, location := fieldNameAndLocation(f, method, asResponse)\n\n\t\t// Skip unused fields\n\t\tif location == FieldLocationUnused {\n\t\t\tcontinue\n\t\t}\n\n\t\tresult[location] = append(result[location], DescribedField{\n\t\t\tField:    f,\n\t\t\tSrcName:  f.Name,\n\t\t\tName:     name,\n\t\t\tLocation: location,\n\t\t})\n\t}\n\n\treturn result\n}\n\n// fieldNameAndLocation determines the name and location of a field based on HTTP method and tags\nfunc fieldNameAndLocation(f *schema.Field, method string, asResponse bool) (string, FieldLocation) {\n\t// For response, all fields go in the body unless explicitly tagged\n\tif asResponse {\n\t\t// Check for explicit wire location\n\t\tif f.Wire != nil {\n\t\t\tif f.Wire.GetHeader() != nil {\n\t\t\t\tname := f.Wire.GetHeader().GetName()\n\t\t\t\tif name == \"\" {\n\t\t\t\t\tname = f.Name\n\t\t\t\t}\n\t\t\t\treturn name, FieldLocationHeader\n\t\t\t} else if f.Wire.GetQuery() != nil {\n\t\t\t\tname := f.Wire.GetQuery().GetName()\n\t\t\t\tif name == \"\" {\n\t\t\t\t\tname = f.Name\n\t\t\t\t}\n\t\t\t\treturn name, FieldLocationQuery\n\t\t\t}\n\t\t}\n\n\t\t// Default response location is body\n\t\tjsonName := f.JsonName\n\t\tif jsonName == \"\" {\n\t\t\tjsonName = f.Name\n\t\t}\n\t\treturn jsonName, FieldLocationBody\n\t}\n\n\t// For request, location depends on method and tags\n\tisGetLike := method == \"GET\" || method == \"HEAD\" || method == \"DELETE\"\n\n\t// Check for explicit wire location\n\tif f.Wire != nil {\n\t\tif f.Wire.GetHeader() != nil {\n\t\t\tname := f.Wire.GetHeader().GetName()\n\t\t\tif name == \"\" {\n\t\t\t\tname = f.Name\n\t\t\t}\n\t\t\treturn name, FieldLocationHeader\n\t\t} else if f.Wire.GetQuery() != nil {\n\t\t\tname := f.Wire.GetQuery().GetName()\n\t\t\tif name == \"\" {\n\t\t\t\tname = f.Name\n\t\t\t}\n\t\t\treturn name, FieldLocationQuery\n\t\t}\n\t}\n\n\t// Check for Cookie\n\tfor _, tag := range f.Tags {\n\t\tif tag.Key == \"cookie\" {\n\t\t\tname := tag.Name\n\t\t\tif name == \"\" {\n\t\t\t\tname = f.Name\n\t\t\t}\n\t\t\treturn name, FieldLocationCookie\n\t\t}\n\t}\n\n\t// For GET-like methods, fields go in query by default\n\tif isGetLike {\n\t\tname := f.QueryStringName\n\t\tif name == \"-\" {\n\t\t\treturn f.Name, FieldLocationUnused\n\t\t} else if name == \"\" {\n\t\t\tname = f.Name\n\t\t}\n\t\treturn name, FieldLocationQuery\n\t}\n\n\t// Default request location for POST/PUT/PATCH is body\n\tjsonName := f.JsonName\n\tif jsonName == \"-\" {\n\t\treturn f.Name, FieldLocationUnused\n\t} else if jsonName == \"\" {\n\t\tjsonName = f.Name\n\t}\n\treturn jsonName, FieldLocationBody\n}\n\n// NamedOrInlineStruct returns the struct type and type arguments for a named or inline struct.\n// Returns nil if the type is neither a named struct nor an inline struct.\nfunc NamedOrInlineStruct(meta map[uint32]*schema.Decl, t *schema.Type) (*schema.Struct, []*schema.Type) {\n\tif t == nil {\n\t\treturn nil, nil\n\t}\n\n\tif named := t.GetNamed(); named != nil {\n\t\tst := meta[named.Id]\n\t\tif st != nil && st.GetType() != nil {\n\t\t\tif structType := st.GetType().GetStruct(); structType != nil {\n\t\t\t\treturn structType, named.GetTypeArguments()\n\t\t\t}\n\t\t}\n\t} else if structType := t.GetStruct(); structType != nil {\n\t\treturn structType, []*schema.Type{}\n\t}\n\n\treturn nil, nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/secret_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nfunc (m *Manager) registerSecretTools() {\n\tm.server.AddTool(mcp.NewTool(\"get_secrets\",\n\t\tmcp.WithDescription(\"Retrieve metadata about all secrets used in the currently open Encore, including their usage patterns, which services depend on them, and their configuration. This tool helps understand the application's security requirements and secret management strategy.\"),\n\t), m.getSecrets)\n}\n\nfunc (m *Manager) getSecrets(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\n\t// Build a map of all secrets and the services that use them\n\tsecretUsageMap := make(map[string][]map[string]interface{})\n\n\t// First go through all packages to find secrets\n\tfor _, pkg := range md.Pkgs {\n\t\tif len(pkg.Secrets) > 0 && pkg.ServiceName != \"\" {\n\t\t\t// For each secret in this package\n\t\t\tfor _, secretName := range pkg.Secrets {\n\t\t\t\t// Create usage info\n\t\t\t\tusageInfo := map[string]interface{}{\n\t\t\t\t\t\"service_name\": pkg.ServiceName,\n\t\t\t\t\t\"package_path\": pkg.RelPath,\n\t\t\t\t}\n\n\t\t\t\t// Add to the map\n\t\t\t\tif _, exists := secretUsageMap[secretName]; !exists {\n\t\t\t\t\tsecretUsageMap[secretName] = make([]map[string]interface{}, 0)\n\t\t\t\t}\n\t\t\t\tsecretUsageMap[secretName] = append(secretUsageMap[secretName], usageInfo)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Build the result\n\tsecrets := make([]map[string]interface{}, 0)\n\n\t// Convert the map to an array\n\tfor secretName, usages := range secretUsageMap {\n\t\tsecretInfo := map[string]interface{}{\n\t\t\t\"name\":   secretName,\n\t\t\t\"usages\": usages,\n\t\t}\n\n\t\t// Count unique services\n\t\tserviceSet := make(map[string]bool)\n\t\tfor _, usage := range usages {\n\t\t\tif svcName, ok := usage[\"service_name\"].(string); ok {\n\t\t\t\tserviceSet[svcName] = true\n\t\t\t}\n\t\t}\n\n\t\tsecretInfo[\"service_count\"] = len(serviceSet)\n\n\t\tsecrets = append(secrets, secretInfo)\n\t}\n\n\t// Sort by name for consistent output\n\tsort.Slice(secrets, func(i, j int) bool {\n\t\treturn secrets[i][\"name\"].(string) < secrets[j][\"name\"].(string)\n\t})\n\n\tjsonData, err := json.Marshal(secrets)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal secrets information: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/src_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n)\n\nfunc (m *Manager) registerSrcTools() {\n\t// Add tool handlers\n\tm.server.AddTool(mcp.NewTool(\"get_metadata\",\n\t\tmcp.WithDescription(\"Retrieve the complete application metadata, including service definitions, database schemas, API endpoints, and other infrastructure components. This tool provides a comprehensive view of the application's architecture and configuration.\"),\n\t), m.getMetadata)\n\n\t// Add tool handlers\n\tm.server.AddTool(mcp.NewTool(\"get_src_files\",\n\t\tmcp.WithDescription(\"Retrieve the contents of one or more source files from the application. This tool is useful for examining specific parts of the codebase or understanding implementation details.\"),\n\t\tmcp.WithArray(\"files\", mcp.Items(map[string]any{\n\t\t\t\"type\":        \"string\",\n\t\t\t\"description\": \"List of file paths to retrieve, relative to the application root. Each path should point to a valid source file in the project.\",\n\t\t})),\n\t), m.getSrcFiles)\n\n}\n\nfunc (m *Manager) getMetadata(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\tmd, err := inst.CachedMetadata()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get metadata: %w\", err)\n\t}\n\tdata, err := protojson.Marshal(md)\n\n\treturn mcp.NewToolResultText(string(data)), nil\n}\n\nfunc (m *Manager) getSrcFiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\tfiles, ok := request.Params.Arguments[\"files\"].([]any)\n\tif !ok || len(files) == 0 {\n\t\treturn nil, fmt.Errorf(\"no files provided\")\n\t}\n\n\trtn := map[string]string{}\n\tfor _, file := range files {\n\t\tfileStr := file.(string)\n\t\tcontent, err := os.ReadFile(filepath.Join(inst.Root(), fileStr))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read file: %w\", err)\n\t\t}\n\t\trtn[fileStr] = string(content)\n\t}\n\n\tjsonData, err := json.Marshal(rtn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal json: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/trace_tools.go",
    "content": "package mcp\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\n\t\"encr.dev/cli/daemon/engine/trace2\"\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n)\n\nfunc (m *Manager) registerTraceResources() {\n\t// Register the trace resources\n\tm.server.AddResourceTemplate(mcp.NewResourceTemplate(\n\t\t\"trace://{id}\",\n\t\t\"API trace\",\n\t\tmcp.WithTemplateDescription(\"Retrieve detailed information about a specific trace, including all spans, timing information, and associated metadata. This resource is useful for deep debugging of individual requests.\"),\n\t\tmcp.WithTemplateMIMEType(\"application/json\"),\n\t), m.getTraceResource)\n}\n\nfunc (m *Manager) registerTraceTools() {\n\t// Add tool for listing traces\n\tm.server.AddTool(mcp.NewTool(\"get_traces\",\n\t\tmcp.WithDescription(\"Retrieve a list of request traces from the application, including their timing, status, and associated metadata. This tool helps understand the flow of requests through the system and diagnose issues.\"),\n\t\tmcp.WithString(\"service\", mcp.Description(\"Optional service name to filter traces by. Only returns traces that involve the specified service.\")),\n\t\tmcp.WithString(\"endpoint\", mcp.Description(\"Optional endpoint name to filter traces by. Only returns traces that involve the specified endpoint.\")),\n\t\tmcp.WithString(\"error\", mcp.Description(\"Optional filter for traces with errors. Set to 'true' to see only failed traces, 'false' for successful traces, or omit to see all traces.\")),\n\t\tmcp.WithString(\"limit\", mcp.Description(\"Maximum number of traces to return. Helps manage response size when dealing with many traces.\")),\n\t\tmcp.WithString(\"start_time\", mcp.Description(\"ISO format timestamp to filter traces created after this time. Useful for focusing on recent activity.\")),\n\t\tmcp.WithString(\"end_time\", mcp.Description(\"ISO format timestamp to filter traces created before this time. Useful for focusing on a specific time period.\")),\n\t), m.listTraces)\n\n\t// Add tool for getting a single trace with all spans\n\tm.server.AddTool(mcp.NewTool(\"get_trace_spans\",\n\t\tmcp.WithDescription(\"Retrieve detailed information about one or more traces, including all spans, timing information, and associated metadata. This tool is useful for deep debugging of individual requests.\"),\n\t\tmcp.WithArray(\"trace_ids\",\n\t\t\tmcp.Items(map[string]any{\n\t\t\t\t\"type\":        \"string\",\n\t\t\t\t\"description\": \"The unique identifiers of the traces to retrieve. These IDs are returned by the get_traces tool.\",\n\t\t\t})),\n\t), m.getTrace)\n}\n\nfunc (m *Manager) listTraces(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\t// Build trace query\n\tquery := &trace2.Query{\n\t\tAppID: inst.PlatformOrLocalID(),\n\t\tLimit: 100, // Default limit\n\t}\n\n\tif service, ok := request.Params.Arguments[\"service\"].(string); ok && service != \"\" {\n\t\tquery.Service = service\n\t}\n\tif endpoint, ok := request.Params.Arguments[\"endpoint\"].(string); ok && endpoint != \"\" {\n\t\tquery.Endpoint = endpoint\n\t}\n\tif errorStr, ok := request.Params.Arguments[\"error\"].(string); ok && errorStr != \"\" {\n\t\tif errorStr == \"true\" {\n\t\t\tisError := true\n\t\t\tquery.IsError = &isError\n\t\t} else if errorStr == \"false\" {\n\t\t\tisError := false\n\t\t\tquery.IsError = &isError\n\t\t}\n\t}\n\tif limitStr, ok := request.Params.Arguments[\"limit\"].(string); ok && limitStr != \"\" {\n\t\tvar limit int\n\t\tif _, err := fmt.Sscanf(limitStr, \"%d\", &limit); err == nil && limit > 0 {\n\t\t\tquery.Limit = limit\n\t\t}\n\t}\n\tif startTime, ok := request.Params.Arguments[\"start_time\"].(string); ok && startTime != \"\" {\n\t\tif t, err := time.Parse(time.RFC3339, startTime); err == nil {\n\t\t\tquery.StartTime = t\n\t\t}\n\t}\n\tif endTime, ok := request.Params.Arguments[\"end_time\"].(string); ok && endTime != \"\" {\n\t\tif t, err := time.Parse(time.RFC3339, endTime); err == nil {\n\t\t\tquery.EndTime = t\n\t\t}\n\t}\n\n\t// Collect traces\n\tvar traces []*tracepb2.SpanSummary\n\terr = m.traces.List(ctx, query, func(span *tracepb2.SpanSummary) bool {\n\t\ttraces = append(traces, span)\n\t\treturn true\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list traces: %w\", err)\n\t}\n\n\t// Convert to JSON\n\tjsonData, err := json.Marshal(traces)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal traces: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getTrace(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\ttraceIDs, ok := request.Params.Arguments[\"trace_ids\"].([]interface{})\n\tif !ok || len(traceIDs) == 0 {\n\t\treturn nil, fmt.Errorf(\"trace_ids is required and must be a non-empty array\")\n\t}\n\n\tresult := make(map[string][]*tracepb2.TraceEvent)\n\n\tfor _, traceIDVal := range traceIDs {\n\t\ttraceID, ok := traceIDVal.(string)\n\t\tif !ok || traceID == \"\" {\n\t\t\tcontinue // Skip invalid IDs\n\t\t}\n\n\t\t// Collect all events for the trace\n\t\tvar events []*tracepb2.TraceEvent\n\t\terr = m.traces.Get(ctx, inst.PlatformOrLocalID(), traceID, func(event *tracepb2.TraceEvent) bool {\n\t\t\tevents = append(events, event)\n\t\t\treturn true\n\t\t})\n\t\tif err != nil {\n\t\t\tif errors.Is(err, trace2.ErrNotFound) {\n\t\t\t\t// Just skip not found traces\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"failed to get trace %s: %w\", traceID, err)\n\t\t}\n\n\t\tresult[traceID] = events\n\t}\n\n\t// Convert to JSON\n\tjsonData, err := json.Marshal(result)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal traces: %w\", err)\n\t}\n\n\treturn mcp.NewToolResultText(string(jsonData)), nil\n}\n\nfunc (m *Manager) getTraceResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\n\tinst, err := m.getApp(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get app: %w\", err)\n\t}\n\n\ttraceID := strings.TrimPrefix(request.Params.URI, \"trace://\")\n\n\t// Collect all events for the trace\n\tvar events []*tracepb2.TraceEvent\n\terr = m.traces.Get(ctx, inst.PlatformOrLocalID(), traceID, func(event *tracepb2.TraceEvent) bool {\n\t\tevents = append(events, event)\n\t\treturn true\n\t})\n\tif err != nil {\n\t\tif errors.Is(err, trace2.ErrNotFound) {\n\t\t\treturn nil, fmt.Errorf(\"trace %s not found\", traceID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to get trace %s: %w\", traceID, err)\n\t}\n\n\t// Convert to JSON\n\tjsonData, err := json.Marshal(events)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal events: %w\", err)\n\t}\n\n\treturn []mcp.ResourceContents{\n\t\tmcp.TextResourceContents{\n\t\t\tURI:      request.Params.URI,\n\t\t\tMIMEType: \"application/json\",\n\t\t\tText:     string(jsonData),\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "cli/daemon/mcp/util.go",
    "content": "package mcp\n\nimport (\n\t\"time\"\n\n\tmetav1 \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// findServiceNameForPackage returns the service name for a given package path\nfunc findServiceNameForPackage(md *metav1.Data, pkgPath string) string {\n\tfor _, pkg := range md.Pkgs {\n\t\tif pkg.RelPath == pkgPath && pkg.ServiceName != \"\" {\n\t\t\treturn pkg.ServiceName\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// formatDuration formats a nanosecond duration into a human-readable string\nfunc formatDuration(nanos int64) string {\n\tduration := time.Duration(nanos) * time.Nanosecond\n\treturn duration.String()\n}\n"
  },
  {
    "path": "cli/daemon/namespace/namespace.go",
    "content": "package namespace\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/xid\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nvar (\n\tErrNotFound = errors.New(\"namespace not found\")\n\tErrActive   = errors.New(\"namespace is active\")\n)\n\ntype (\n\tID   string\n\tName string\n)\n\nfunc (id ID) String() string { return string(id) }\n\nfunc ParseID(s string) (ID, bool) {\n\tid, err := xid.FromString(s)\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\treturn ID(id.String()), true\n}\n\nfunc NewManager(db *sql.DB) *Manager {\n\treturn &Manager{db, nil}\n}\n\n// Manager manages namespaces.\ntype Manager struct {\n\tdb       *sql.DB\n\thandlers []DeletionHandler\n}\n\nfunc (mgr *Manager) RegisterDeletionHandler(h DeletionHandler) {\n\tmgr.handlers = append(mgr.handlers, h)\n}\n\ntype Namespace struct {\n\tID           ID\n\tApp          *apps.Instance\n\tName         Name\n\tActive       bool\n\tCreatedAt    time.Time\n\tLastActiveAt *time.Time\n}\n\nfunc (m *Manager) Create(ctx context.Context, app *apps.Instance, name Name) (*Namespace, error) {\n\tnow := time.Now()\n\tid := ID(xid.NewWithTime(now).String())\n\n\ttx, err := m.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer tx.Rollback() // committed explicitly on success\n\n\t_, err = tx.ExecContext(ctx, `\n\t\tINSERT INTO namespace (id, app_id, name, active, created_at)\n\t\tVALUES (?, ?, ?, ?, ?)\n\t`, id, app.PlatformOrLocalID(), name, false, now)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"create namespace\")\n\t}\n\n\tns := &Namespace{\n\t\tID:        id,\n\t\tApp:       app,\n\t\tName:      name,\n\t\tCreatedAt: now,\n\t}\n\n\t// If there is no active namespace, make this one active.\n\t{\n\t\tvar activeName string\n\t\terr = tx.QueryRowContext(ctx, `\n\t\t\tSELECT name FROM namespace WHERE app_id = ? AND active = true\n\t\t`, app.PlatformOrLocalID()).Scan(&activeName)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\t\t// No active namespace; make this one active.\n\t\t\t\t_, err = tx.ExecContext(ctx, `\n\t\t\t\t\tUPDATE namespace\n\t\t\t\t\tSET active = true, last_active_at = ?\n\t\t\t\t\tWHERE id = ?\n\t\t\t\t`, now, id)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"create namespace\")\n\t\t\t}\n\t\t}\n\t\tns.Active = true\n\t\tns.LastActiveAt = &now\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"create namespace\")\n\t}\n\n\treturn ns, nil\n}\n\nfunc (m *Manager) List(ctx context.Context, app *apps.Instance) ([]*Namespace, error) {\n\trows, err := m.db.QueryContext(ctx, `\n\t\tSELECT id, name, active, created_at, last_active_at\n\t\tFROM namespace\n\t\tWHERE app_id = ?\n\t\tORDER BY name ASC\n\t`, app.PlatformOrLocalID())\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"list namespaces\")\n\t}\n\tdefer rows.Close()\n\tvar nss []*Namespace\n\n\tfor rows.Next() {\n\t\tvar ns Namespace\n\t\tif err := rows.Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"scan namespace\")\n\t\t}\n\t\tns.App = app\n\t\tnss = append(nss, &ns)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"list namespaces\")\n\t}\n\n\t// If we have no namespaces at all, create a default one.\n\tif len(nss) == 0 {\n\t\tns, err := m.Create(ctx, app, \"default\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnss = []*Namespace{ns}\n\t}\n\n\treturn nss, nil\n}\n\nfunc (m *Manager) GetByName(ctx context.Context, app *apps.Instance, name Name) (*Namespace, error) {\n\tvar ns Namespace\n\terr := m.db.QueryRowContext(ctx, `\n\t\tSELECT id, name, active, created_at, last_active_at\n\t\tFROM namespace\n\t\tWHERE app_id = ? AND name = ?\n\t`, app.PlatformOrLocalID(), name).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, ErrNotFound\n\t\t}\n\t\treturn nil, errors.Wrap(err, \"get namespace\")\n\t}\n\tns.App = app\n\treturn &ns, nil\n}\n\nfunc (m *Manager) GetByID(ctx context.Context, app *apps.Instance, id ID) (*Namespace, error) {\n\tvar ns Namespace\n\terr := m.db.QueryRowContext(ctx, `\n\t\tSELECT id, name, active, created_at, last_active_at\n\t\tFROM namespace\n\t\tWHERE app_id = ? AND id = ?\n\t`, app.PlatformOrLocalID(), id).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt)\n\tif err != nil {\n\t\tif errors.Is(err, sql.ErrNoRows) {\n\t\t\treturn nil, ErrNotFound\n\t\t}\n\t\treturn nil, errors.Wrap(err, \"get namespace\")\n\t}\n\tns.App = app\n\treturn &ns, nil\n}\n\nfunc (m *Manager) Delete(ctx context.Context, app *apps.Instance, name Name) error {\n\ttx, err := m.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback() // committed explicitly on success\n\n\tvar ns Namespace\n\terr = tx.QueryRowContext(ctx, `\n\t\tDELETE FROM namespace\n\t\tWHERE app_id = ? AND name = ?\n\t\tRETURNING id, name, active, created_at, last_active_at\n\t`, app.PlatformOrLocalID(), name).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt)\n\tif ns.Active {\n\t\treturn ErrActive\n\t}\n\tns.App = app\n\n\t// Check all the deletion handlers.\n\tfor _, h := range m.handlers {\n\t\tif err := h.CanDeleteNamespace(ctx, app, &ns); err != nil {\n\t\t\treturn errors.Newf(\"cannot delete namespace: %v\", err)\n\t\t}\n\t}\n\n\t// Actually delete the namespace.\n\tfor _, h := range m.handlers {\n\t\tif err := h.DeleteNamespace(ctx, app, &ns); err != nil {\n\t\t\treturn errors.Newf(\"failed to delete namespace: %v\", err)\n\t\t}\n\t}\n\n\terr = tx.Commit()\n\treturn errors.Wrap(err, \"delete namespace\")\n}\n\nfunc (m *Manager) Switch(ctx context.Context, app *apps.Instance, name Name) (*Namespace, error) {\n\t// Resolve the namespace to switch to.\n\tvar target *Namespace\n\n\t// If the name is \"-\", switch to the previous namespace.\n\tif name == \"-\" {\n\t\tnss, err := m.List(ctx, app)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Find the non-active namespace that was most recently active\n\t\tvar lastActive *Namespace\n\t\tfor _, ns := range nss {\n\t\t\tif !ns.Active && ns.LastActiveAt != nil {\n\t\t\t\tif lastActive == nil || ns.LastActiveAt.After(*lastActive.LastActiveAt) {\n\t\t\t\t\tlastActive = ns\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif lastActive == nil {\n\t\t\treturn nil, ErrNotFound\n\t\t}\n\t\ttarget = lastActive\n\t} else {\n\t\tvar err error\n\t\ttarget, err = m.GetByName(ctx, app, name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\ttx, err := m.db.BeginTx(ctx, nil)\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\tdefer tx.Rollback() // committed explicitly on success\n\n\t// Mark all namespaces as inactive.\n\t_, err = tx.ExecContext(ctx, `\n\t\tUPDATE namespace SET active = false\n\t\tWHERE app_id = ?\n\t`, app.PlatformOrLocalID())\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"switch namespace\")\n\t}\n\n\t// Mark the selected namespace as active.\n\t_, err = tx.ExecContext(ctx, `\n\t\tUPDATE namespace SET active = true, last_active_at = ?\n\t\tWHERE id = ?\n\t`, time.Now(), target.ID)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"switch namespace\")\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\treturn nil, errors.Wrap(err, \"switch namespace\")\n\t}\n\n\ttarget.Active = true\n\treturn target, nil\n}\n\n// GetActive returns the active namespace for the given app.\nfunc (m *Manager) GetActive(ctx context.Context, app *apps.Instance) (*Namespace, error) {\n\tvar ns Namespace\n\terr := m.db.QueryRowContext(ctx, `\n\t\tSELECT id, name, active, created_at, last_active_at\n\t\tFROM namespace\n\t\tWHERE app_id = ? AND active = true\n\t`, app.PlatformOrLocalID()).Scan(&ns.ID, &ns.Name, &ns.Active, &ns.CreatedAt, &ns.LastActiveAt)\n\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\treturn nil, err\n\t} else if err == nil {\n\t\tns.App = app\n\t\treturn &ns, nil\n\t}\n\n\t// No active namespace.\n\n\t// Do we have any namespaces at all?\n\tnss, err := m.List(ctx, app)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if len(nss) > 0 {\n\t\treturn m.Switch(ctx, app, nss[0].Name)\n\t} else {\n\t\t// No namespaces. Create a new one.\n\t\treturn m.Create(ctx, app, \"default\")\n\t}\n}\n\nfunc (ns *Namespace) ToProto() *daemonpb.Namespace {\n\tres := &daemonpb.Namespace{\n\t\tId:        string(ns.ID),\n\t\tName:      string(ns.Name),\n\t\tActive:    ns.Active,\n\t\tCreatedAt: ns.CreatedAt.String(),\n\t}\n\tif ns.LastActiveAt != nil {\n\t\ts := ns.LastActiveAt.String()\n\t\tres.LastActiveAt = &s\n\t}\n\treturn res\n}\n\n// DeletionHandler is the interface for components that want to listen for\n// and handle namespace deletion events.\ntype DeletionHandler interface {\n\t// CanDeleteNamespace is called to determine whether the namespace can be deleted\n\t// by the component. To signal the namespace cannot be deleted, return a non-nil error.\n\tCanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *Namespace) error\n\n\t// DeleteNamespace is called when a namespace is deleted.\n\t// Due to the non-atomic nature of many components, failure to handle\n\t// the deletion cannot be fully rolled back.\n\tDeleteNamespace(ctx context.Context, app *apps.Instance, ns *Namespace) error\n}\n"
  },
  {
    "path": "cli/daemon/namespace.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\n\t\"github.com/golang/protobuf/ptypes/empty\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/pkg/fns\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc (s *Server) CreateNamespace(ctx context.Context, req *daemonpb.CreateNamespaceRequest) (*daemonpb.Namespace, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tns, err := s.ns.Create(ctx, app, namespace.Name(req.Name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ns.ToProto(), nil\n}\n\nfunc (s *Server) ListNamespaces(ctx context.Context, req *daemonpb.ListNamespacesRequest) (*daemonpb.ListNamespacesResponse, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnss, err := s.ns.List(ctx, app)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tprotos := fns.Map(nss, (*namespace.Namespace).ToProto)\n\treturn &daemonpb.ListNamespacesResponse{Namespaces: protos}, nil\n}\n\nfunc (s *Server) DeleteNamespace(ctx context.Context, req *daemonpb.DeleteNamespaceRequest) (*empty.Empty, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := s.ns.Delete(ctx, app, namespace.Name(req.Name)); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &empty.Empty{}, nil\n}\n\nfunc (s *Server) SwitchNamespace(ctx context.Context, req *daemonpb.SwitchNamespaceRequest) (*daemonpb.Namespace, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif req.Create {\n\t\t_, err := s.ns.Create(ctx, app, namespace.Name(req.Name))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tns, err := s.ns.Switch(ctx, app, namespace.Name(req.Name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ns.ToProto(), nil\n}\n\nfunc (s *Server) namespaceOrActive(ctx context.Context, app *apps.Instance, ns *string) (*namespace.Namespace, error) {\n\tif ns == nil {\n\t\treturn s.ns.GetActive(ctx, app)\n\t}\n\treturn s.ns.GetByName(ctx, app, namespace.Name(*ns))\n}\n"
  },
  {
    "path": "cli/daemon/objects/manager.go",
    "content": "package objects\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/pkg/emulators/storage/gcsemu\"\n)\n\n// NewClusterManager creates a new ClusterManager.\nfunc NewClusterManager(ns *namespace.Manager) *ClusterManager {\n\tmgr := &ClusterManager{\n\t\tns: ns,\n\t}\n\treturn mgr\n}\n\ntype ClusterManager struct {\n\tns *namespace.Manager\n}\n\nfunc (cm *ClusterManager) BaseDir(ns namespace.ID) (string, error) {\n\tcache, err := os.UserCacheDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filepath.Join(cache, \"encore\", \"objects\", ns.String()), nil\n}\n\n// CanDeleteNamespace implements namespace.DeletionHandler.\nfunc (cm *ClusterManager) CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error {\n\treturn nil\n}\n\n// DeleteNamespace implements namespace.DeletionHandler.\nfunc (cm *ClusterManager) DeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error {\n\tbaseDir, err := cm.BaseDir(ns.ID)\n\tif err == nil {\n\t\terr = os.RemoveAll(baseDir)\n\t}\n\treturn err\n}\n\n// PersistentStoreFallback is a public server fallback handler\n// for resolving stores based on the cluster manager's base directory.\nfunc (cm *ClusterManager) PersistentStoreFallback(id string) (gcsemu.Store, bool) {\n\tif baseDir, err := cm.BaseDir(namespace.ID(id)); err == nil {\n\t\tif _, err := os.Stat(baseDir); err == nil {\n\t\t\treturn gcsemu.NewFileStore(baseDir), true\n\t\t}\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "cli/daemon/objects/objects.go",
    "content": "package objects\n\nimport (\n\t// nosemgrep\n\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/pkg/emulators/storage/gcsemu\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go4.org/syncutil\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype Server struct {\n\tid        string\n\tpublic    *PublicBucketServer\n\tstartOnce syncutil.Once\n\tcancel    func() // set by Start\n\tstore     gcsemu.Store\n\temu       *gcsemu.GcsEmu\n\tln        net.Listener\n\tsrv       *http.Server\n\tinMemory  bool\n}\n\nfunc NewInMemoryServer(public *PublicBucketServer) *Server {\n\tid := xid.New().String()\n\tstore := gcsemu.NewMemStore()\n\treturn newServer(public, id, store, true)\n}\n\nfunc NewDirServer(public *PublicBucketServer, nsID namespace.ID, baseDir string) *Server {\n\tstore := gcsemu.NewFileStore(baseDir)\n\treturn newServer(public, nsID.String(), store, false)\n}\n\nfunc newServer(public *PublicBucketServer, id string, store gcsemu.Store, isInMem bool) *Server {\n\treturn &Server{\n\t\tpublic:   public,\n\t\tid:       id,\n\t\tstore:    store,\n\t\temu:      gcsemu.NewGcsEmu(gcsemu.Options{Store: store}),\n\t\tinMemory: isInMem,\n\t}\n}\n\nfunc (s *Server) Initialize(md *meta.Data) error {\n\tfor _, bucket := range md.Buckets {\n\t\tif err := s.emu.InitBucket(bucket.Name); err != nil {\n\t\t\treturn errors.Wrap(err, \"initialize object storage bucket\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Server) Start() error {\n\treturn s.startOnce.Do(func() error {\n\t\tif s.inMemory {\n\t\t\ts.public.Register(s.id, s.store)\n\t\t}\n\t\tmux := http.NewServeMux()\n\t\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"listen tcp\")\n\t\t}\n\t\ts.emu.Register(mux)\n\t\ts.ln = ln\n\t\ts.srv = &http.Server{Handler: mux}\n\n\t\tgo func() {\n\t\t\tif err := s.srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {\n\t\t\t\tlog.Error().Err(err).Msg(\"unable to listen to gcs server\")\n\t\t\t}\n\t\t}()\n\n\t\treturn nil\n\t})\n}\n\nfunc (s *Server) Stop() {\n\t_ = s.srv.Close()\n\tif s.inMemory {\n\t\ts.public.Deregister(s.id)\n\t}\n}\n\nfunc (s *Server) Endpoint() string {\n\t// Ensure the server has been started\n\tif err := s.Start(); err != nil {\n\t\tpanic(err)\n\t}\n\tport := s.ln.Addr().(*net.TCPAddr).Port\n\treturn fmt.Sprintf(\"http://localhost:%d\", port)\n}\n\nfunc (s *Server) PublicBaseURL() string {\n\treturn fmt.Sprintf(\"%s/%s\", s.public.BaseAddr(), s.id)\n}\n\n// IsUsed reports whether the application uses object storage at all.\nfunc IsUsed(md *meta.Data) bool {\n\treturn len(md.Buckets) > 0\n}\n"
  },
  {
    "path": "cli/daemon/objects/public.go",
    "content": "package objects\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"google.golang.org/api/storage/v1\"\n\n\t\"encr.dev/pkg/emulators/storage/gcsemu\"\n)\n\n// Fallback is a function that returns a store for a given namespace.\n// It is used for resolving namespace ids to stores, where\n// the store is not pre-registered by Register.\ntype Fallback func(namespace string) (gcsemu.Store, bool)\n\n// NewPublicBucketServer creates a new PublicBucketServer.\n// If fallback is nil, no fallback will be used.\nfunc NewPublicBucketServer(baseAddr string, fallback Fallback) *PublicBucketServer {\n\tmux := http.NewServeMux()\n\tsrv := &PublicBucketServer{\n\t\tmux:        mux,\n\t\tbaseAddr:   baseAddr,\n\t\tfallback:   fallback,\n\t\tnamespaces: make(map[string]gcsemu.Store),\n\t}\n\tmux.HandleFunc(\"/{namespace}/{bucket}/{object...}\", srv.handler)\n\treturn srv\n}\n\ntype PublicBucketServer struct {\n\tmux      *http.ServeMux\n\tbaseAddr string\n\tfallback Fallback\n\n\tmu         sync.RWMutex\n\tnamespaces map[string]gcsemu.Store\n}\n\nfunc (s *PublicBucketServer) Serve(ln net.Listener) error {\n\treturn http.Serve(ln, s)\n}\n\nfunc (s *PublicBucketServer) Register(namespace string, store gcsemu.Store) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.namespaces[namespace] = store\n}\n\nfunc (s *PublicBucketServer) Deregister(namespace string) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tdelete(s.namespaces, namespace)\n}\n\nfunc (s *PublicBucketServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\ts.mux.ServeHTTP(w, req)\n}\n\nfunc (s *PublicBucketServer) BaseAddr() string {\n\treturn s.baseAddr\n}\n\nfunc (s *PublicBucketServer) handler(w http.ResponseWriter, req *http.Request) {\n\tnsID := req.PathValue(\"namespace\")\n\tbucketName := req.PathValue(\"bucket\")\n\tobjName := req.PathValue(\"object\")\n\n\t// Determine which store to use\n\ts.mu.RLock()\n\tstore, ok := s.namespaces[nsID]\n\ts.mu.RUnlock()\n\tif !ok && s.fallback != nil {\n\t\tstore, ok = s.fallback(nsID)\n\t}\n\tif !ok {\n\t\thttp.Error(w, \"unknown namespace\", http.StatusNotFound)\n\t\treturn\n\t}\n\tswitch req.Method {\n\tcase \"OPTIONS\":\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"PUT, GET, HEAD\")\n\t\tw.Header().Set(\"Access-Control-Allow-Headers\", \"Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration\")\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", \"Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration\")\n\tcase \"GET\", \"HEAD\":\n\t\t_, isSigned := (queryLowerCase(req))[\"x-goog-signature\"]\n\t\tif isSigned {\n\t\t\terr := validateGcsSignedRequest(req, time.Now())\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tobj, contents, err := store.Get(\"\", bucketName, objName)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t} else if obj == nil {\n\t\t\thttp.Error(w, \"object not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tif obj.ContentType != \"\" {\n\t\t\tw.Header().Set(\"Content-Type\", obj.ContentType)\n\t\t}\n\t\tif obj.Etag != \"\" {\n\t\t\tw.Header().Set(\"Etag\", obj.Etag)\n\t\t}\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", \"Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration\")\n\t\tw.Header().Set(\"Content-Length\", strconv.Itoa(len(contents)))\n\t\tw.Header().Set(\"Accept-Ranges\", \"bytes\")\n\n\t\t// Only write the body for GET requests, not HEAD\n\t\tif req.Method == \"GET\" {\n\t\t\thttp.ServeContent(w, req, obj.Name, time.Time{}, bytes.NewReader(contents))\n\t\t}\n\tcase \"PUT\":\n\t\terr := validateGcsSignedRequest(req, time.Now())\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tbuf, err := io.ReadAll(req.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tmetaIn := parseObjectMeta(req)\n\t\terr = store.Add(bucketName, objName, buf, &metaIn)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// Read back the object so we can add the etag value to the response.\n\t\tmetaOut, _, err := store.Get(\"\", bucketName, objName)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Expose-Headers\", \"Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration\")\n\t\tw.Header().Set(\"Etag\", metaOut.Etag)\n\tdefault:\n\t\thttp.Error(w, \"method not allowed\", http.StatusBadRequest)\n\t}\n}\n\n// Only GCS is supported for local development\nfunc validateGcsSignedRequest(req *http.Request, now time.Time) error {\n\tconst dateLayout = \"20060102T150405Z\"\n\tconst gracePeriod = time.Duration(30) * time.Second\n\n\tquery := queryLowerCase(req)\n\n\t// We don't try to actually verify the signature, we only check that it's non-empty.\n\n\tfor _, s := range []string{\n\t\t\"x-goog-signature\",\n\t\t\"x-goog-credential\",\n\t\t\"x-goog-date\",\n\t\t\"x-goog-expires\"} {\n\t\tif len(query[s]) <= 0 {\n\t\t\treturn fmt.Errorf(\"missing or empty query param %q\", s)\n\t\t}\n\t}\n\n\tt0, err := time.Parse(dateLayout, query[\"x-goog-date\"])\n\tif err != nil {\n\t\treturn errors.New(\"failed to parse x-goog-date\")\n\t}\n\tif t0.After(now.Add(gracePeriod)) {\n\t\treturn errors.New(\"URL expiration base date is in the future\")\n\t}\n\n\ttd, err := strconv.Atoi(query[\"x-goog-expires\"])\n\tif err != nil {\n\t\treturn errors.New(\"failed to parse x-goog-expires value into an integer\")\n\t}\n\tt := t0.Add(time.Duration(td) * time.Second)\n\n\tif t.Before(now.Add(-gracePeriod)) {\n\t\treturn errors.New(\"URL is expired\")\n\t}\n\n\treturn nil\n}\n\nfunc queryLowerCase(req *http.Request) map[string]string {\n\tquery := map[string]string{}\n\tfor k, vs := range req.URL.Query() {\n\t\tquery[strings.ToLower(k)] = vs[0]\n\t}\n\treturn query\n}\n\nfunc parseObjectMeta(req *http.Request) storage.Object {\n\treturn storage.Object{ContentType: req.Header.Get(\"Content-Type\")}\n}\n"
  },
  {
    "path": "cli/daemon/pubsub/nsq.go",
    "content": "package pubsub\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/nsqio/go-nsq\"\n\t\"github.com/nsqio/nsq/nsqd\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go4.org/syncutil\"\n)\n\ntype NSQDaemon struct {\n\tnsqd      *nsqd.NSQD\n\tstartOnce syncutil.Once\n\n\tOpts *nsqd.Options\n}\n\nfunc (n *NSQDaemon) Stats() (*nsqd.Stats, error) {\n\tif n.nsqd == nil {\n\t\treturn nil, errors.New(\"nsqd not started\")\n\t}\n\tstats := n.nsqd.GetStats(\"\", \"\", true)\n\treturn &stats, nil\n}\n\nfunc (n *NSQDaemon) isReady() error {\n\tp, err := nsq.NewProducer(n.Addr(), nsq.NewConfig())\n\tp.SetLogger(&logAdapter{\"nsq producer\"}, nsq.LogLevelWarning)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = p.Ping()\n\tp.Stop()\n\tn.nsqd.GetError()\n\treturn err\n}\n\nfunc (n *NSQDaemon) Addr() string {\n\treturn n.nsqd.RealTCPAddr().String()\n}\n\nfunc (n *NSQDaemon) Start() error {\n\treturn n.startOnce.Do(func() error {\n\t\tif n.Opts == nil {\n\t\t\tn.Opts = nsqd.NewOptions()\n\t\t\ttmpDir, err := os.MkdirTemp(\"\", \"encore-nsqd\")\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"failed to create tmp nsqd datapath\")\n\t\t\t}\n\t\t\tn.Opts.DataPath = tmpDir\n\n\t\t\tn.Opts.LogLevel = nsqd.LOG_WARN\n\t\t\tn.Opts.Logger = &logAdapter{\"nsqd\"}\n\n\t\t\t// Take the default address options and scope down to localhost (to prevent firewall warnings / permission requests)\n\t\t\t// then set the port to 0 to allow any port to be used which is free\n\t\t\tn.Opts.TCPAddress = \"127.0.0.1:0\"\n\t\t\tn.Opts.HTTPAddress = \"127.0.0.1:0\"\n\t\t\tn.Opts.HTTPSAddress = \"127.0.0.1:0\"\n\t\t\tn.Opts.MaxMsgSize = 10 * 1024 * 1024 // 10MB\n\t\t}\n\t\tnsq, err := nsqd.New(n.Opts)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to create new nsqd\")\n\t\t}\n\t\tn.nsqd = nsq\n\t\tgo func() {\n\t\t\terr = nsq.Main()\n\t\t\tif err != nil {\n\t\t\t\tlog.Err(err).Msg(\"failed to start nsqd\")\n\t\t\t}\n\t\t}()\n\t\t// Ping the daemon to make sure it has started correctly\n\t\treturn n.isReady()\n\t})\n}\n\nfunc (n *NSQDaemon) Stop() {\n\tif n.nsqd != nil {\n\t\tn.nsqd.Exit()\n\t}\n}\n\ntype logAdapter struct{ serviceName string }\n\nvar _ nsqd.Logger = (*logAdapter)(nil)\n\nfunc (l *logAdapter) Output(maxdepth int, s string) error {\n\t// Attempt to extract the level, start with cutting on \":\"\n\tlvl, logMsg, found := strings.Cut(s, \":\")\n\tif !found || strings.Contains(lvl, \" \") {\n\t\t// then if that fails or we have a space in that cut, try cutting on the first space\n\t\tnewLvl, suffix, _ := strings.Cut(lvl, \" \")\n\t\tlvl = newLvl\n\n\t\tif found {\n\t\t\tlogMsg = suffix + \":\" + logMsg\n\t\t}\n\t}\n\n\t// Attempt to convert the level string to a zerolog level\n\tlogLevel := l.OutputLevel(lvl)\n\tif logLevel == zerolog.NoLevel {\n\t\t// and if that fails, then just log the message\n\t\tlogMsg = s\n\t}\n\n\tlog.WithLevel(logLevel).Str(\"service\", l.serviceName).Msg(strings.TrimSpace(logMsg))\n\n\treturn nil\n}\n\nfunc (l *logAdapter) OutputLevel(lvl string) zerolog.Level {\n\tswitch strings.ToLower(lvl) {\n\tcase \"debug\", \"dbg\":\n\t\treturn zerolog.DebugLevel\n\tcase \"info\", \"inf\":\n\t\treturn zerolog.InfoLevel\n\tcase \"warn\", \"wrn\":\n\t\treturn zerolog.WarnLevel\n\tcase \"error\", \"err\":\n\t\treturn zerolog.ErrorLevel\n\tcase \"fatal\":\n\t\treturn zerolog.FatalLevel\n\tdefault:\n\t\tlog.Warn().Msg(\"unknown level: \" + lvl)\n\t\treturn zerolog.NoLevel\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/pubsub/utils.go",
    "content": "package pubsub\n\nimport (\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// IsUsed reports whether the application uses pubsub at all.\nfunc IsUsed(md *meta.Data) bool {\n\treturn len(md.PubsubTopics) > 0\n}\n"
  },
  {
    "path": "cli/daemon/redis/redis.go",
    "content": "package redis\n\nimport (\n\tmathrand \"math/rand\" // nosemgrep\n\t\"time\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n\t\"github.com/cockroachdb/errors\"\n\t\"go4.org/syncutil\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype Server struct {\n\tstartOnce syncutil.Once\n\tmini      *miniredis.Miniredis\n\tcleanup   *time.Ticker\n\tquit      chan struct{}\n\taddr      string\n}\n\nconst tickInterval = 1 * time.Second\n\nfunc New() *Server {\n\treturn &Server{\n\t\tmini: miniredis.NewMiniRedis(),\n\t\tquit: make(chan struct{}),\n\t}\n}\n\nfunc (s *Server) Start() error {\n\treturn s.startOnce.Do(func() error {\n\t\tif err := s.mini.Start(); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to start redis server\")\n\t\t}\n\t\ts.addr = s.mini.Addr()\n\t\ts.cleanup = time.NewTicker(tickInterval)\n\t\tgo s.doCleanup()\n\t\treturn nil\n\t})\n}\nfunc (s *Server) Stop() {\n\ts.mini.Close()\n\ts.cleanup.Stop()\n\tclose(s.quit)\n}\n\nfunc (s *Server) Miniredis() *miniredis.Miniredis {\n\treturn s.mini\n}\n\nfunc (s *Server) Addr() string {\n\t// Ensure the server has been started\n\tif err := s.Start(); err != nil {\n\t\tpanic(err)\n\t}\n\treturn s.addr\n}\n\nfunc (s *Server) doCleanup() {\n\tvar acc time.Duration\n\tconst cleanupInterval = 15 * time.Second\n\n\tfor {\n\t\tselect {\n\t\tcase <-s.quit:\n\t\t\treturn\n\t\tcase <-s.cleanup.C:\n\t\t}\n\t\ts.mini.FastForward(tickInterval)\n\n\t\t// Clean up keys every so often\n\t\tacc += tickInterval\n\t\tif acc > cleanupInterval {\n\t\t\tacc -= cleanupInterval\n\t\t\ts.clearKeys()\n\t\t}\n\t}\n}\n\n// clearKeys clears random keys to get the redis server\n// down to 100 persisted keys, as a simple way to bound\n// the max memory usage.\nfunc (s *Server) clearKeys() {\n\tconst maxKeys = 100\n\tkeys := s.mini.Keys()\n\tif n := len(keys); n > maxKeys {\n\t\ttoDelete := n - maxKeys\n\t\tdeleted := 0\n\t\tfor deleted < toDelete {\n\t\t\tid := mathrand.Intn(len(keys))\n\t\t\tif keys[id] != \"\" {\n\t\t\t\ts.mini.Del(keys[id])\n\t\t\t\tkeys[id] = \"\" // mark it as deleted\n\t\t\t\tdeleted++\n\t\t\t}\n\t\t}\n\t}\n}\n\n// IsUsed reports whether the application uses redis at all.\nfunc IsUsed(md *meta.Data) bool {\n\treturn len(md.CacheClusters) > 0\n}\n"
  },
  {
    "path": "cli/daemon/run/call.go",
    "content": "package run\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog/log\"\n\t\"github.com/tailscale/hujson\"\n\n\t\"encr.dev/parser/encoding\"\n\tv1 \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype ApiCallParams struct {\n\tAppID         string\n\tService       string\n\tEndpoint      string\n\tPath          string\n\tMethod        string\n\tPayload       []byte\n\tAuthPayload   []byte `json:\"auth_payload,omitempty\"`\n\tAuthToken     string `json:\"auth_token,omitempty\"`\n\tCorrelationID string `json:\"correlation_id,omitempty\"`\n}\n\nfunc CallAPI(ctx context.Context, run *Run, p *ApiCallParams) (map[string]any, error) {\n\tlog := log.With().Str(\"app_id\", p.AppID).Str(\"path\", p.Path).Str(\"service\", p.Service).Str(\"endpoint\", p.Endpoint).Logger()\n\tif run == nil {\n\t\tlog.Error().Str(\"app_id\", p.AppID).Msg(\"dash: cannot make api call: app not running\")\n\t\treturn nil, fmt.Errorf(\"app not running\")\n\t}\n\tproc := run.ProcGroup()\n\tif proc == nil {\n\t\tlog.Error().Str(\"app_id\", p.AppID).Msg(\"dash: cannot make api call: app not running\")\n\t\treturn nil, fmt.Errorf(\"app not running\")\n\t}\n\n\tbaseURL := \"http://\" + run.ListenAddr\n\treq, err := prepareRequest(ctx, baseURL, proc.Meta, p)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: unable to prepare request\")\n\t\treturn nil, err\n\t}\n\n\tif p.CorrelationID != \"\" {\n\t\treq.Header.Set(\"X-Correlation-ID\", p.CorrelationID)\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"dash: api call failed\")\n\t\treturn nil, err\n\t}\n\tbody, _ := io.ReadAll(resp.Body)\n\t_ = resp.Body.Close()\n\n\t// Encode the body back into a Go style struct\n\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\tbody = handleResponse(proc.Meta, p, resp.Header, body)\n\t}\n\n\tlog.Info().Int(\"status\", resp.StatusCode).Msg(\"dash: api call completed\")\n\treturn map[string]interface{}{\n\t\t\"status\":      resp.Status,\n\t\t\"status_code\": resp.StatusCode,\n\t\t\"body\":        body,\n\t\t\"trace_id\":    resp.Header.Get(\"X-Encore-Trace-Id\"),\n\t}, nil\n}\n\n// findRPC finds the RPC with the given service and endpoint name.\n// If it cannot be found it reports nil.\nfunc findRPC(md *v1.Data, service, endpoint string) *v1.RPC {\n\tfor _, svc := range md.Svcs {\n\t\tif svc.Name == service {\n\t\t\tfor _, rpc := range svc.Rpcs {\n\t\t\t\tif rpc.Name == endpoint {\n\t\t\t\t\treturn rpc\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\n// prepareRequest prepares a request for sending based on the given ApiCallParams.\nfunc prepareRequest(ctx context.Context, baseURL string, md *v1.Data, p *ApiCallParams) (*http.Request, error) {\n\treqSpec := newHTTPRequestSpec()\n\trpc := findRPC(md, p.Service, p.Endpoint)\n\tif rpc == nil {\n\t\treturn nil, fmt.Errorf(\"unknown service/endpoint: %s/%s\", p.Service, p.Endpoint)\n\t}\n\n\trpcEncoding, err := encoding.DescribeRPC(md, rpc, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"describe rpc: %v\", err)\n\t}\n\n\t// Add request encoding\n\t{\n\t\treqEnc := rpcEncoding.RequestEncodingForMethod(p.Method)\n\t\tif reqEnc == nil {\n\t\t\treturn nil, fmt.Errorf(\"unsupported method: %s (supports: %s)\", p.Method, strings.Join(rpc.HttpMethods, \",\"))\n\t\t}\n\t\tif len(p.Payload) > 0 {\n\t\t\tif err := addToRequest(reqSpec, p.Payload, reqEnc.ParameterEncodingMapByName()); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"encode request params: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Add auth encoding, if any\n\tif h := md.AuthHandler; h != nil {\n\t\tauth, err := encoding.DescribeAuth(md, h.Params, nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"describe auth: %v\", err)\n\t\t}\n\t\tif auth.LegacyTokenFormat {\n\t\t\treqSpec.Header.Set(\"Authorization\", \"Bearer \"+p.AuthToken)\n\t\t} else {\n\t\t\tif err := addToRequest(reqSpec, p.AuthPayload, auth.ParameterEncodingMapByName()); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"encode auth params: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar body io.Reader = nil\n\tif reqSpec.Body != nil {\n\t\tdata, _ := json.Marshal(reqSpec.Body)\n\t\tbody = bytes.NewReader(data)\n\t\tif reqSpec.Header[\"Content-Type\"] == nil {\n\t\t\treqSpec.Header.Set(\"Content-Type\", \"application/json\")\n\t\t}\n\t}\n\n\treqURL := baseURL + p.Path\n\tif len(reqSpec.Query) > 0 {\n\t\treqURL += \"?\" + reqSpec.Query.Encode()\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, p.Method, reqURL, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor k, v := range reqSpec.Header {\n\t\treq.Header[k] = v\n\t}\n\tfor _, c := range reqSpec.Cookies {\n\t\treq.AddCookie(c)\n\t}\n\treturn req, nil\n}\n\nfunc handleResponse(md *v1.Data, p *ApiCallParams, headers http.Header, body []byte) []byte {\n\trpc := findRPC(md, p.Service, p.Endpoint)\n\tif rpc == nil {\n\t\treturn body\n\t}\n\n\tencodingOptions := &encoding.Options{}\n\trpcEncoding, err := encoding.DescribeRPC(md, rpc, encodingOptions)\n\tif err != nil {\n\t\treturn body\n\t}\n\n\tdecoded := map[string]json.RawMessage{}\n\tif err := json.Unmarshal(body, &decoded); err != nil {\n\t\treturn body\n\t}\n\n\tmembers := make([]hujson.ObjectMember, 0)\n\tif rpcEncoding.ResponseEncoding != nil {\n\t\tfor i, m := range rpcEncoding.ResponseEncoding.HeaderParameters {\n\t\t\tvalues := headers.Values(m.Name)\n\n\t\t\tvar beforeExtra []byte\n\t\t\tif i == 0 {\n\t\t\t\tbeforeExtra = []byte(\"\\n    // HTTP Headers\\n    \")\n\t\t\t}\n\n\t\t\tvar val hujson.Value\n\t\t\tif len(values) == 1 {\n\t\t\t\tval = hujson.Value{Value: hujson.String(values[0])}\n\t\t\t} else {\n\t\t\t\tarr := &hujson.Array{}\n\t\t\t\tfor _, v := range values {\n\t\t\t\t\tarr.Elements = append(arr.Elements, hujson.Value{Value: hujson.String(v)})\n\t\t\t\t}\n\t\t\t\tval = hujson.Value{Value: arr}\n\t\t\t}\n\n\t\t\tmembers = append(members, hujson.ObjectMember{\n\t\t\t\tName:  hujson.Value{Value: hujson.String(m.Name), BeforeExtra: beforeExtra},\n\t\t\t\tValue: val,\n\t\t\t})\n\t\t}\n\n\t\tfor i, m := range rpcEncoding.ResponseEncoding.BodyParameters {\n\t\t\tvalue, ok := decoded[m.Name]\n\t\t\tif !ok {\n\t\t\t\tvalue = []byte(\"null\")\n\t\t\t}\n\n\t\t\tvar beforeExtra []byte\n\t\t\tif i == 0 {\n\t\t\t\tif len(rpcEncoding.ResponseEncoding.HeaderParameters) > 0 {\n\t\t\t\t\tbeforeExtra = []byte(\"\\n\\n    // JSON Payload\\n    \")\n\t\t\t\t} else {\n\t\t\t\t\tbeforeExtra = []byte(\"\\n    \")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// nosemgrep: trailofbits.go.invalid-usage-of-modified-variable.invalid-usage-of-modified-variable\n\t\t\thValue, err := hujson.Parse(value)\n\t\t\tif err != nil {\n\t\t\t\thValue = hujson.Value{Value: hujson.Literal(value)}\n\t\t\t}\n\n\t\t\tmembers = append(members, hujson.ObjectMember{\n\t\t\t\tName:  hujson.Value{Value: hujson.String(m.Name), BeforeExtra: beforeExtra},\n\t\t\t\tValue: hValue,\n\t\t\t})\n\t\t}\n\t}\n\n\tvalue := hujson.Value{Value: &hujson.Object{Members: members}}\n\tvalue.Format()\n\treturn value.Pack()\n}\n\n// httpRequestSpec specifies how the HTTP request should be generated.\ntype httpRequestSpec struct {\n\t// Body are the fields to encode as the JSON body.\n\t// If nil, no body is added.\n\tBody map[string]json.RawMessage\n\n\t// Header are the HTTP headers to set in the request.\n\tHeader http.Header\n\n\t// Query are the query string fields to set.\n\tQuery url.Values\n\n\t// Cookies are the cookies to send.\n\tCookies []*http.Cookie\n}\n\nfunc newHTTPRequestSpec() *httpRequestSpec {\n\treturn &httpRequestSpec{\n\t\tBody:   nil, // to distinguish between no body and \"{}\".\n\t\tHeader: make(http.Header),\n\t\tQuery:  make(url.Values),\n\t}\n}\n\n// addToRequest decodes rawPayload and adds it to the request according to the given parameter encodings.\n// The body argument is where body parameters are added; other parameter locations are added\n// directly to the request object itself.\nfunc addToRequest(req *httpRequestSpec, rawPayload []byte, params map[string][]*encoding.ParameterEncoding) error {\n\tpayload, err := hujson.Parse(rawPayload)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid payload: %v\", err)\n\t}\n\tvals, ok := payload.Value.(*hujson.Object)\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid payload: expected JSON object, got %s\", payload.Pack())\n\t}\n\n\tseenKeys := make(map[string]int)\n\n\tfor _, kv := range vals.Members {\n\t\tlit, _ := kv.Name.Value.(hujson.Literal)\n\t\tkey := lit.String()\n\t\tval := kv.Value\n\t\tval.Standardize()\n\n\t\tif matches := params[key]; len(matches) > 0 {\n\t\t\t// Get the index of this particular match, in case we have conflicts.\n\t\t\tidx := seenKeys[key]\n\t\t\tseenKeys[key]++\n\t\t\tif idx < len(matches) {\n\t\t\t\tparam := matches[idx]\n\t\t\t\tswitch param.Location {\n\t\t\t\tcase encoding.Body:\n\t\t\t\t\tif req.Body == nil {\n\t\t\t\t\t\treq.Body = make(map[string]json.RawMessage)\n\t\t\t\t\t}\n\t\t\t\t\treq.Body[param.WireFormat] = val.Pack()\n\n\t\t\t\tcase encoding.Query:\n\t\t\t\t\tswitch v := val.Value.(type) {\n\t\t\t\t\tcase hujson.Literal:\n\t\t\t\t\t\treq.Query.Add(param.WireFormat, v.String())\n\t\t\t\t\tcase *hujson.Array:\n\t\t\t\t\t\tfor _, elem := range v.Elements {\n\t\t\t\t\t\t\tif lit, ok := elem.Value.(hujson.Literal); ok {\n\t\t\t\t\t\t\t\treq.Query.Add(param.WireFormat, lit.String())\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treturn fmt.Errorf(\"unsupported value type for query string array element: %T\", elem.Value)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn fmt.Errorf(\"unsupported value type for query string: %T\", v)\n\t\t\t\t\t}\n\n\t\t\t\tcase encoding.Header:\n\t\t\t\t\tswitch v := val.Value.(type) {\n\t\t\t\t\tcase hujson.Literal:\n\t\t\t\t\t\treq.Header.Add(param.WireFormat, v.String())\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn fmt.Errorf(\"unsupported value type for query string: %T\", v)\n\t\t\t\t\t}\n\n\t\t\t\tcase encoding.Cookie:\n\t\t\t\t\tswitch v := val.Value.(type) {\n\t\t\t\t\tcase hujson.Literal:\n\t\t\t\t\t\t// nosemgrep\n\t\t\t\t\t\treq.Cookies = append(req.Cookies, &http.Cookie{\n\t\t\t\t\t\t\tName:  param.WireFormat,\n\t\t\t\t\t\t\tValue: v.String(),\n\t\t\t\t\t\t})\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn fmt.Errorf(\"unsupported value type for cookie: %T\", v)\n\t\t\t\t\t}\n\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\"unsupported parameter location %v\", param.Location)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/run/check.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"runtime\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/vcs\"\n)\n\ntype CheckParams struct {\n\t// App is the app to start.\n\tApp *apps.Instance\n\n\t// WorkingDir is the working dir, for formatting\n\t// error messages with relative paths.\n\tWorkingDir string\n\n\t// CodegenDebug, if true, specifies to keep the output\n\t// around for codegen debugging purposes.\n\tCodegenDebug bool\n\n\t// Environ are the environment variables to set,\n\t// in the same format as os.Environ().\n\tEnviron []string\n\n\t// Tests specifies whether to parse and codegen for tests as well.\n\tTests bool\n}\n\n// Check checks the app for errors.\n// It reports a buildDir (if available) when codegenDebug is true.\nfunc (mgr *Manager) Check(ctx context.Context, p CheckParams) (buildDir string, err error) {\n\texpSet, err := p.App.Experiments(p.Environ)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// TODO: We should check that all secret keys are defined as well.\n\n\tvcsRevision := vcs.GetRevision(p.App.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            p.Environ,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         p.CodegenDebug,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tbld := builderimpl.Resolve(p.App.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        p.App,\n\t\tWorkingDir: p.WorkingDir,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         p.App,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  p.WorkingDir,\n\t\tParseTests:  p.Tests,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := p.App.CacheMetadata(parse.Meta); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"cache metadata\")\n\t}\n\n\t// Validate the service configs.\n\t_, err = bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\tParse: parse,\n\t\tCueMeta: &cueutil.Meta{\n\t\t\t// Dummy data to satisfy config validation.\n\t\t\tAPIBaseURL: \"http://localhost:0\",\n\t\t\tEnvName:    \"encore-check\",\n\t\t\tEnvType:    cueutil.EnvType_Development,\n\t\t\tCloudType:  cueutil.CloudType_Local,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresult, err := bld.Compile(ctx, builder.CompileParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         p.App,\n\t\tParse:       parse,\n\t\tOpTracker:   nil, // TODO\n\t\tExperiments: expSet,\n\t\tWorkingDir:  p.WorkingDir,\n\t})\n\n\tif result != nil && len(result.Outputs) > 0 {\n\t\tbuildDir = result.Outputs[0].GetArtifactDir().ToIO()\n\t}\n\treturn buildDir, err\n}\n"
  },
  {
    "path": "cli/daemon/run/errors.go",
    "content": "package run\n\nimport (\n\t\"errors\"\n\n\t\"encr.dev/pkg/errlist\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\nfunc AsErrorList(err error) *errlist.List {\n\tif errList := errlist.Convert(err); errList != nil {\n\t\treturn errList\n\t}\n\n\tlist := &perr.ListAsErr{}\n\tif errors.As(err, &list) {\n\t\treturn &errlist.List{List: list.ErrorList()}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/run/exec_command.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/promise\"\n\t\"encr.dev/pkg/vcs\"\n)\n\n// ExecSpecParams groups the parameters for the ExecSpec method.\ntype ExecSpecParams struct {\n\t// App is the app to execute the script for.\n\tApp *apps.Instance\n\n\t// NS is the namespace to use.\n\tNS *namespace.Namespace\n\n\t// Command to execute\n\tCommand string\n\n\t// ScriptArgs are the arguments to pass to the script binary.\n\tScriptArgs []string\n\n\t// WorkingDir is the working dir to execute the script from.\n\t// It's relative to the app root.\n\tWorkingDir string\n\n\t// Environ are the environment variables to set when running the command,\n\t// in the same format as os.Environ().\n\tEnviron []string\n\n\t// TempDir is a path to a temp dir that will be cleaned up by the CLI.\n\tTempDir string\n\n\tOpTracker *optracker.OpTracker\n}\n\n// ExecSpecResponse contains the specification for how to run an exec command.\ntype ExecSpecResponse struct {\n\tCommand string\n\tArgs    []string\n\tEnviron []string\n}\n\n// ExecSpec returns the specification for how to run an exec command,\n// without actually executing it. This allows the CLI to run the command\n// directly with stdin attached for interactive support.\nfunc (mgr *Manager) ExecSpec(ctx context.Context, p ExecSpecParams) (*ExecSpecResponse, error) {\n\texpSet, err := p.App.Experiments(p.Environ)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trm := infra.NewResourceManager(p.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, p.NS, p.Environ, mgr.DBProxyPort, false)\n\n\ttracker := p.OpTracker\n\tjobs := optracker.NewAsyncBuildJobs(ctx, p.App.PlatformOrLocalID(), tracker)\n\n\t// Parse the app to figure out what infrastructure is needed.\n\tstart := time.Now()\n\tparseOp := tracker.Add(\"Building Encore application graph\", start)\n\ttopoOp := tracker.Add(\"Analyzing service topology\", start)\n\n\tbld := builderimpl.Resolve(p.App.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tvcsRevision := vcs.GetRevision(p.App.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            p.Environ,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        p.App,\n\t\tWorkingDir: p.WorkingDir,\n\t})\n\tif err != nil {\n\t\ttracker.Fail(parseOp, errors.New(\"prepare error\"))\n\t\treturn nil, err\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         p.App,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  p.WorkingDir,\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\ttracker.Fail(parseOp, errors.New(\"parse error\"))\n\t\treturn nil, err\n\t}\n\tif err := p.App.CacheMetadata(parse.Meta); err != nil {\n\t\treturn nil, errors.Wrap(err, \"cache metadata\")\n\t}\n\ttracker.Done(parseOp, 500*time.Millisecond)\n\ttracker.Done(topoOp, 300*time.Millisecond)\n\n\trm.StartRequiredServices(jobs, parse.Meta)\n\n\tvar secrets map[string]string\n\tif usesSecrets(parse.Meta) {\n\t\tjobs.Go(\"Fetching application secrets\", true, 150*time.Millisecond, func(ctx context.Context) error {\n\t\t\tdata, err := mgr.Secret.Load(p.App).Get(ctx, expSet)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsecrets = data.Values\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tapiBaseURL := fmt.Sprintf(\"http://localhost:%d\", mgr.RuntimePort)\n\n\tconfigProm := promise.New(func() (*builder.ServiceConfigsResult, error) {\n\t\treturn bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\t\tParse: parse,\n\t\t\tCueMeta: &cueutil.Meta{\n\t\t\t\tAPIBaseURL: apiBaseURL,\n\t\t\t\tEnvName:    \"local\",\n\t\t\t\tEnvType:    cueutil.EnvType_Development,\n\t\t\t\tCloudType:  cueutil.CloudType_Local,\n\t\t\t},\n\t\t})\n\t})\n\n\tif err := jobs.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tgateways := make(map[string]GatewayConfig)\n\tfor _, gw := range parse.Meta.Gateways {\n\t\tgateways[gw.EncoreName] = GatewayConfig{\n\t\t\tBaseURL:   apiBaseURL,\n\t\t\tHostnames: []string{\"localhost\"},\n\t\t}\n\t}\n\n\tcfg, err := configProm.Get(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauthKey := genAuthKey()\n\tconfigGen := &RuntimeConfigGenerator{\n\t\tapp:               p.App,\n\t\tinfraManager:      rm,\n\t\tmd:                parse.Meta,\n\t\tAppID:             option.Some(GenID()),\n\t\tEnvID:             option.Some(GenID()),\n\t\tTraceEndpoint:     option.Some(fmt.Sprintf(\"http://localhost:%d/trace\", mgr.RuntimePort)),\n\t\tAuthKey:           authKey,\n\t\tGateways:          gateways,\n\t\tDefinedSecrets:    secrets,\n\t\tSvcConfigs:        cfg.Configs,\n\t\tIncludeMeta:       bld.NeedsMeta(),\n\t\tMetaPath:          option.Some(filepath.Join(p.TempDir, \"meta.pb\")),\n\t\tRuntimeConfigPath: option.Some(filepath.Join(p.TempDir, \"runtime_config.pb\")),\n\t}\n\tprocConf, err := configGen.AllInOneProc(bld.UseNewRuntimeConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tprocEnv, err := configGen.ProcEnvs(procConf, bld.UseNewRuntimeConfig())\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"compute proc envs\")\n\t}\n\n\tdefaultEnv := []string{\"ENCORE_RUNTIME_LOG=error\"}\n\tenv := append(defaultEnv, p.Environ...)\n\tenv = append(env, procConf.ExtraEnv...)\n\tenv = append(env, procEnv...)\n\n\ttracker.AllDone()\n\n\treturn &ExecSpecResponse{\n\t\tCommand: p.Command,\n\t\tArgs:    p.ScriptArgs,\n\t\tEnviron: env,\n\t}, nil\n}\n"
  },
  {
    "path": "cli/daemon/run/exec_script.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\tencoreEnv \"encr.dev/internal/env\"\n\t\"encr.dev/internal/lookpath\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/pkg/promise\"\n\t\"encr.dev/pkg/vcs\"\n)\n\n// ExecScriptParams groups the parameters for the ExecScript method.\ntype ExecScriptParams struct {\n\t// App is the app to execute the script for.\n\tApp *apps.Instance\n\n\t// NS is the namespace to use.\n\tNS *namespace.Namespace\n\n\t// MainPkg is the package path to the command to execute.\n\tMainPkg paths.Pkg\n\n\t// ScriptArgs are the arguments to pass to the script binary.\n\tScriptArgs []string\n\n\t// WorkingDir is the working dir to execute the script from.\n\t// It's relative to the app root.\n\tWorkingDir string\n\n\t// Environ are the environment variables to set when running the tests,\n\t// in the same format as os.Environ().\n\tEnviron []string\n\n\t// Stdout and Stderr are where \"go test\" output should be written.\n\tStdout, Stderr io.Writer\n\n\tOpTracker *optracker.OpTracker\n}\n\n// ExecScript executes the script.\nfunc (mgr *Manager) ExecScript(ctx context.Context, p ExecScriptParams) (err error) {\n\texpSet, err := p.App.Experiments(p.Environ)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trm := infra.NewResourceManager(p.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, p.NS, p.Environ, mgr.DBProxyPort, false)\n\tdefer rm.StopAll()\n\n\ttracker := p.OpTracker\n\tjobs := optracker.NewAsyncBuildJobs(ctx, p.App.PlatformOrLocalID(), tracker)\n\n\t// Parse the app to figure out what infrastructure is needed.\n\tstart := time.Now()\n\tparseOp := tracker.Add(\"Building Encore application graph\", start)\n\ttopoOp := tracker.Add(\"Analyzing service topology\", start)\n\n\tbld := builderimpl.Resolve(p.App.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tvcsRevision := vcs.GetRevision(p.App.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            p.Environ,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\t\tMainPkg:            option.Some(p.MainPkg),\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        p.App,\n\t\tWorkingDir: p.WorkingDir,\n\t})\n\tif err != nil {\n\t\ttracker.Fail(parseOp, errors.New(\"prepare error\"))\n\t\treturn err\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         p.App,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  p.WorkingDir,\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\t// Don't use the error itself in tracker.Fail, as it will lead to duplicate error output.\n\t\ttracker.Fail(parseOp, errors.New(\"parse error\"))\n\t\treturn err\n\t}\n\tif err := p.App.CacheMetadata(parse.Meta); err != nil {\n\t\treturn errors.Wrap(err, \"cache metadata\")\n\t}\n\ttracker.Done(parseOp, 500*time.Millisecond)\n\ttracker.Done(topoOp, 300*time.Millisecond)\n\n\trm.StartRequiredServices(jobs, parse.Meta)\n\n\tvar secrets map[string]string\n\tif usesSecrets(parse.Meta) {\n\t\tjobs.Go(\"Fetching application secrets\", true, 150*time.Millisecond, func(ctx context.Context) error {\n\t\t\tdata, err := mgr.Secret.Load(p.App).Get(ctx, expSet)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsecrets = data.Values\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tapiBaseURL := fmt.Sprintf(\"http://localhost:%d\", mgr.RuntimePort)\n\n\tconfigProm := promise.New(func() (*builder.ServiceConfigsResult, error) {\n\t\treturn bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\t\tParse: parse,\n\t\t\tCueMeta: &cueutil.Meta{\n\t\t\t\tAPIBaseURL: apiBaseURL,\n\t\t\t\tEnvName:    \"local\",\n\t\t\t\tEnvType:    cueutil.EnvType_Development,\n\t\t\t\tCloudType:  cueutil.CloudType_Local,\n\t\t\t},\n\t\t})\n\t})\n\n\tvar build *builder.CompileResult\n\tjobs.Go(\"Compiling application source code\", false, 0, func(ctx context.Context) (err error) {\n\t\tbuild, err = bld.Compile(ctx, builder.CompileParams{\n\t\t\tBuild:       buildInfo,\n\t\t\tApp:         p.App,\n\t\t\tParse:       parse,\n\t\t\tOpTracker:   tracker,\n\t\t\tExperiments: expSet,\n\t\t\tWorkingDir:  p.WorkingDir,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"compile error on exec\")\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err := jobs.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tgateways := make(map[string]GatewayConfig)\n\tfor _, gw := range parse.Meta.Gateways {\n\t\tgateways[gw.EncoreName] = GatewayConfig{\n\t\t\tBaseURL:   apiBaseURL,\n\t\t\tHostnames: []string{\"localhost\"},\n\t\t}\n\t}\n\n\toutputs := build.Outputs\n\tif len(outputs) != 1 {\n\t\treturn errors.New(\"ExecScript currently only supports a single build output\")\n\t}\n\tentrypoints := outputs[0].GetEntrypoints()\n\tif len(entrypoints) != 1 {\n\t\treturn errors.New(\"ExecScript currently only supports a single entrypoint\")\n\t}\n\tproc := entrypoints[0].Cmd.Expand(outputs[0].GetArtifactDir())\n\n\tcfg, err := configProm.Get(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttempDir, err := os.MkdirTemp(\"\", \"encore-exec\")\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"couldn't create temp dir\")\n\t}\n\tdefer func() {\n\t\t_ = os.RemoveAll(tempDir)\n\t}()\n\n\tauthKey := genAuthKey()\n\tconfigGen := &RuntimeConfigGenerator{\n\t\tapp:               p.App,\n\t\tinfraManager:      rm,\n\t\tmd:                parse.Meta,\n\t\tAppID:             option.Some(GenID()),\n\t\tEnvID:             option.Some(GenID()),\n\t\tTraceEndpoint:     option.Some(fmt.Sprintf(\"http://localhost:%d/trace\", mgr.RuntimePort)),\n\t\tAuthKey:           authKey,\n\t\tGateways:          gateways,\n\t\tDefinedSecrets:    secrets,\n\t\tSvcConfigs:        cfg.Configs,\n\t\tIncludeMeta:       bld.NeedsMeta(),\n\t\tMetaPath:          option.Some(filepath.Join(tempDir, \"meta.pb\")),\n\t\tRuntimeConfigPath: option.Some(filepath.Join(tempDir, \"runtime_config.json\")),\n\t}\n\tprocConf, err := configGen.AllInOneProc(bld.UseNewRuntimeConfig())\n\tif err != nil {\n\t\treturn err\n\t}\n\tprocEnv, err := configGen.ProcEnvs(procConf, bld.UseNewRuntimeConfig())\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"compute proc envs\")\n\t}\n\n\tenv := append(os.Environ(), proc.Env...)\n\tenv = append(env, p.Environ...)\n\tenv = append(env, procConf.ExtraEnv...)\n\n\tenv = append(env, procEnv...)\n\tenv = append(env, encodeServiceConfigs(cfg.Configs)...)\n\tif runtimeLibPath := encoreEnv.EncoreRuntimeLib(); runtimeLibPath != \"\" {\n\t\tenv = append(env, \"ENCORE_RUNTIME_LIB=\"+runtimeLibPath)\n\t}\n\n\ttracker.AllDone()\n\n\tcwd := filepath.Join(p.App.Root(), p.WorkingDir)\n\tbinary, err := lookpath.InDir(cwd, env, proc.Command[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\n\targs := append(slices.Clone(proc.Command[1:]), p.ScriptArgs...)\n\t// nosemgrep: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.CommandContext(ctx, binary, args...)\n\tcmd.Dir = filepath.Join(p.App.Root(), p.WorkingDir)\n\tcmd.Stdout = p.Stdout\n\tcmd.Stderr = p.Stderr\n\tcmd.Env = env\n\treturn cmd.Run()\n}\n"
  },
  {
    "path": "cli/daemon/run/http.go",
    "content": "package run\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// ServeHTTP implements http.Handler by forwarding the request to the currently running process.\nfunc (r *Run) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tproc := r.proc.Load().(*ProcGroup)\n\tproc.ProxyReq(w, req)\n}\n\nfunc addAuthKeyToRequest(req *http.Request, authKey config.EncoreAuthKey) {\n\tif req.Header == nil {\n\t\treq.Header = make(http.Header)\n\t}\n\n\tdate := time.Now().UTC().Format(http.TimeFormat)\n\treq.Header.Set(\"Date\", date)\n\n\tmac := hmac.New(sha256.New, authKey.Data)\n\t_, _ = fmt.Fprintf(mac, \"%s\\x00%s\", date, req.URL.Path)\n\n\tbytes := make([]byte, 4, 4+sha256.Size)\n\tbinary.BigEndian.PutUint32(bytes[0:4], authKey.KeyID)\n\tbytes = mac.Sum(bytes)\n\tauth := base64.RawStdEncoding.EncodeToString(bytes)\n\treq.Header.Set(\"X-Encore-Auth\", auth)\n}\n\nconst TestHeaderDisablePlatformAuth = \"X-Encore-Test-Disable-Platform-Auth\"\n"
  },
  {
    "path": "cli/daemon/run/infra/encorecloudtesting.go",
    "content": "package infra\n\nimport (\n\t\"encoding/base64\"\n\t\"strconv\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"go.encore.dev/platform-sdk/pkg/auth\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// setTestEncoreCloud sets the Encore Cloud API configuration to use a local\n// Encore Cloud API server.\n//\n// It returns true if one has been configured, or false if not.\n//\n// To use it the `encore run` command must be started with the following environment variables:\n// - ENCORECLOUD_LOCAL_SERVER: the URL of the local Encore Cloud API server\n// - ENCORECLOUD_LOCAL_KEY_ID: the ID of the key to use for authentication\n// - ENCORECLOUD_LOCAL_KEY_DATA: the base64-encoded data of the key to use for authentication\nfunc (rm *ResourceManager) setTestEncoreCloud(cfg *config.Runtime) (useLocalCloudServer bool, err error) {\n\tlocalServer := rm.environ.Get(\"ENCORECLOUD_LOCAL_SERVER\")\n\tif localServer == \"\" {\n\t\treturn false, nil\n\t}\n\n\t// Get the key and secret\n\tkeyIDStr := rm.environ.Get(\"ENCORECLOUD_LOCAL_KEY_ID\")\n\tkeyData64 := rm.environ.Get(\"ENCORECLOUD_LOCAL_KEY_DATA\")\n\tif keyIDStr == \"\" || keyData64 == \"\" {\n\t\treturn false, errors.New(\"ENCORECLOUD_LOCAL_KEY_ID and ENCORECLOUD_LOCAL_KEY_DATA must be set if using ENCORECLOUD_LOCAL_SERVER\")\n\t}\n\n\tkeyID, err := strconv.Atoi(keyIDStr)\n\tif err != nil || keyID <= 0 {\n\t\treturn false, errors.New(\"ENCORECLOUD_LOCAL_KEY_ID must be a positive integer\")\n\t}\n\n\tkeyData, err := base64.StdEncoding.DecodeString(keyData64)\n\tif err != nil {\n\t\treturn false, errors.New(\"ENCORECLOUD_LOCAL_KEY_DATA must be a valid base64 string\")\n\t}\n\n\tcfg.EncoreCloudAPI = &config.EncoreCloudAPI{\n\t\tServer: localServer,\n\t\tAuthKeys: []auth.Key{\n\t\t\t{\n\t\t\t\tKeyID: uint32(keyID),\n\t\t\t\tData:  keyData,\n\t\t\t},\n\t\t},\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "cli/daemon/run/infra/infra.go",
    "content": "package infra\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/objects\"\n\t\"encr.dev/cli/daemon/pubsub\"\n\t\"encr.dev/cli/daemon/redis\"\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/pkg/environ\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype Type string\n\nconst (\n\tPubSub  Type = \"pubsub\"\n\tCache   Type = \"cache\"\n\tSQLDB   Type = \"sqldb\"\n\tObjects Type = \"objects\"\n)\n\nconst (\n\t// this ID is used in the Encore Cloud README file as an example\n\t// on how to create a topic resource\n\tencoreCloudExampleTopicID = \"res_0o9ioqnrirflhhm3t720\"\n\n\t// this ID is used in the Encore Cloud README file as a example\n\t// on how to create a subscription on the above topic\n\tencoreCloudExampleSubscriptionID = \"res_0o9ioqnrirflhhm3t730\"\n)\n\n// ResourceManager manages a set of infrastructure resources\n// to support the running Encore application.\ntype ResourceManager struct {\n\tapp           *apps.Instance\n\tdbProxyPort   int\n\tsqlMgr        *sqldb.ClusterManager\n\tobjectsMgr    *objects.ClusterManager\n\tpublicBuckets *objects.PublicBucketServer\n\tns            *namespace.Namespace\n\tenviron       environ.Environ\n\tlog           zerolog.Logger\n\tforTests      bool\n\n\tmutex   sync.Mutex\n\tservers map[Type]Resource\n}\n\nfunc NewResourceManager(app *apps.Instance, sqlMgr *sqldb.ClusterManager, objectsMgr *objects.ClusterManager, publicBuckets *objects.PublicBucketServer, ns *namespace.Namespace, environ environ.Environ, dbProxyPort int, forTests bool) *ResourceManager {\n\treturn &ResourceManager{\n\t\tapp:           app,\n\t\tdbProxyPort:   dbProxyPort,\n\t\tsqlMgr:        sqlMgr,\n\t\tobjectsMgr:    objectsMgr,\n\t\tpublicBuckets: publicBuckets,\n\t\tns:            ns,\n\t\tenviron:       environ,\n\t\tforTests:      forTests,\n\n\t\tservers: make(map[Type]Resource),\n\t\tlog:     log.With().Str(\"app_id\", app.PlatformOrLocalID()).Logger(),\n\t}\n}\n\nfunc (rm *ResourceManager) StopAll() {\n\trm.mutex.Lock()\n\tdefer rm.mutex.Unlock()\n\n\trm.log.Info().Int(\"num\", len(rm.servers)).Msg(\"Stopping all resource services\")\n\n\tfor _, daemon := range rm.servers {\n\t\tdaemon.Stop()\n\t}\n}\n\ntype Resource interface {\n\t// Stop shuts down the resource.\n\tStop()\n}\n\n// StartRequiredServices will start the required services for the current application\n// if they are not already running based on the given parse result\nfunc (rm *ResourceManager) StartRequiredServices(a *optracker.AsyncBuildJobs, md *meta.Data) {\n\tif sqldb.IsUsed(md) && rm.GetSQLCluster() == nil {\n\t\ta.Go(\"Creating PostgreSQL database cluster\", true, 300*time.Millisecond, rm.StartSQLCluster(a, md))\n\t}\n\n\tif pubsub.IsUsed(md) && rm.GetPubSub() == nil {\n\t\ta.Go(\"Starting PubSub daemon\", true, 250*time.Millisecond, rm.StartPubSub)\n\t}\n\n\tif redis.IsUsed(md) && rm.GetRedis() == nil {\n\t\ta.Go(\"Starting Redis server\", true, 250*time.Millisecond, rm.StartRedis)\n\t}\n\n\tif objects.IsUsed(md) && rm.GetObjects() == nil {\n\t\ta.Go(\"Starting Object Storage server\", true, 250*time.Millisecond, rm.StartObjects(md))\n\t}\n}\n\n// StartPubSub starts a PubSub daemon.\nfunc (rm *ResourceManager) StartPubSub(ctx context.Context) error {\n\tnsqd := &pubsub.NSQDaemon{}\n\terr := nsqd.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trm.mutex.Lock()\n\trm.servers[PubSub] = nsqd\n\trm.mutex.Unlock()\n\treturn nil\n}\n\n// GetPubSub returns the PubSub daemon if it is running otherwise it returns nil\nfunc (rm *ResourceManager) GetPubSub() *pubsub.NSQDaemon {\n\trm.mutex.Lock()\n\tdefer rm.mutex.Unlock()\n\n\tif daemon, found := rm.servers[PubSub]; found {\n\t\treturn daemon.(*pubsub.NSQDaemon)\n\t}\n\treturn nil\n}\n\n// StartRedis starts a Redis server.\nfunc (rm *ResourceManager) StartRedis(ctx context.Context) error {\n\tsrv := redis.New()\n\terr := srv.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trm.mutex.Lock()\n\trm.servers[Cache] = srv\n\trm.mutex.Unlock()\n\treturn nil\n}\n\n// GetRedis returns the Redis server if it is running otherwise it returns nil\nfunc (rm *ResourceManager) GetRedis() *redis.Server {\n\trm.mutex.Lock()\n\tdefer rm.mutex.Unlock()\n\n\tif srv, found := rm.servers[Cache]; found {\n\t\treturn srv.(*redis.Server)\n\t}\n\treturn nil\n}\n\n// StartObjects starts an Object Storage server.\nfunc (rm *ResourceManager) StartObjects(md *meta.Data) func(context.Context) error {\n\treturn func(ctx context.Context) error {\n\t\tvar srv *objects.Server\n\t\tif rm.forTests {\n\t\t\tsrv = objects.NewInMemoryServer(rm.publicBuckets)\n\t\t} else {\n\t\t\tif rm.objectsMgr == nil {\n\t\t\t\treturn fmt.Errorf(\"StartObjects: no Object Storage cluster manager provided\")\n\t\t\t} else if rm.publicBuckets == nil {\n\t\t\t\treturn fmt.Errorf(\"StartObjects: no Object Storage public bucket server provided\")\n\t\t\t}\n\t\t\tbaseDir, err := rm.objectsMgr.BaseDir(rm.ns.ID)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsrv = objects.NewDirServer(rm.publicBuckets, rm.ns.ID, baseDir)\n\t\t}\n\n\t\tif err := srv.Initialize(md); err != nil {\n\t\t\treturn err\n\t\t} else if err := srv.Start(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trm.mutex.Lock()\n\t\trm.servers[Objects] = srv\n\t\trm.mutex.Unlock()\n\t\treturn nil\n\t}\n}\n\n// GetObjects returns the Object Storage server if it is running otherwise it returns nil\nfunc (rm *ResourceManager) GetObjects() *objects.Server {\n\trm.mutex.Lock()\n\tdefer rm.mutex.Unlock()\n\n\tif srv, found := rm.servers[Objects]; found {\n\t\treturn srv.(*objects.Server)\n\t}\n\treturn nil\n}\n\nfunc (rm *ResourceManager) StartSQLCluster(a *optracker.AsyncBuildJobs, md *meta.Data) func(ctx context.Context) error {\n\treturn func(ctx context.Context) error {\n\t\t// This can be the case in tests.\n\t\tif rm.sqlMgr == nil {\n\t\t\treturn fmt.Errorf(\"StartSQLCluster: no SQL Cluster manager provided\")\n\t\t}\n\n\t\ttyp := sqldb.Run\n\t\tif rm.forTests {\n\t\t\ttyp = sqldb.Test\n\t\t}\n\n\t\tif err := rm.sqlMgr.Ready(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcluster := rm.sqlMgr.Create(ctx, &sqldb.CreateParams{\n\t\t\tClusterID: sqldb.GetClusterID(rm.app, typ, rm.ns),\n\t\t\tMemfs:     typ.Memfs(),\n\t\t})\n\n\t\tif _, err := cluster.Start(ctx, a.Tracker()); err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to start cluster\")\n\t\t}\n\n\t\trm.mutex.Lock()\n\t\trm.servers[SQLDB] = cluster\n\t\trm.mutex.Unlock()\n\n\t\t// Set up the database asynchronously since it can take a while.\n\t\tif rm.forTests {\n\t\t\ta.Go(\"Recreating databases\", true, 250*time.Millisecond, func(ctx context.Context) error {\n\t\t\t\terr := cluster.Recreate(ctx, rm.app.Root(), nil, md)\n\t\t\t\tif err != nil {\n\t\t\t\t\trm.log.Error().Err(err).Msg(\"failed to recreate db\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t} else {\n\t\t\ta.Go(\"Running database migrations\", true, 250*time.Millisecond, func(ctx context.Context) error {\n\t\t\t\terr := cluster.SetupAndMigrate(ctx, rm.app.Root(), md.SqlDatabases)\n\t\t\t\tif err != nil {\n\t\t\t\t\trm.log.Error().Err(err).Msg(\"failed to setup db\")\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// GetSQLCluster returns the SQL cluster\nfunc (rm *ResourceManager) GetSQLCluster() *sqldb.Cluster {\n\trm.mutex.Lock()\n\tdefer rm.mutex.Unlock()\n\n\tif cluster, found := rm.servers[SQLDB]; found {\n\t\treturn cluster.(*sqldb.Cluster)\n\t}\n\treturn nil\n}\n\n// UpdateConfig updates the given config with infrastructure information.\n// Note that all the requisite services must have started up already,\n// which in practice means that (*optracker.AsyncBuildJobs).Wait must have returned first.\nfunc (rm *ResourceManager) UpdateConfig(cfg *config.Runtime, md *meta.Data, dbProxyPort int) error {\n\tuseLocalEncoreCloudAPIForTesting, err := rm.setTestEncoreCloud(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif cluster := rm.GetSQLCluster(); cluster != nil {\n\t\tsrv := &config.SQLServer{\n\t\t\tHost: \"localhost:\" + strconv.Itoa(dbProxyPort),\n\t\t}\n\t\tserverID := len(cfg.SQLServers)\n\t\tcfg.SQLServers = append(cfg.SQLServers, srv)\n\n\t\tfor _, db := range md.SqlDatabases {\n\t\t\tcfg.SQLDatabases = append(cfg.SQLDatabases, &config.SQLDatabase{\n\t\t\t\tServerID:     serverID,\n\t\t\t\tEncoreName:   db.Name,\n\t\t\t\tDatabaseName: db.Name,\n\t\t\t\tUser:         \"encore\",\n\t\t\t\tPassword:     cluster.Password,\n\t\t\t})\n\t\t}\n\n\t\t// Configure max connections based on 96 connections\n\t\t// divided evenly among the databases\n\t\tmaxConns := 96 / len(cfg.SQLDatabases)\n\t\tfor _, db := range cfg.SQLDatabases {\n\t\t\tdb.MaxConnections = maxConns\n\t\t}\n\t}\n\n\tif nsq := rm.GetPubSub(); nsq != nil {\n\t\tprovider := &config.PubsubProvider{\n\t\t\tNSQ: &config.NSQProvider{\n\t\t\t\tHost: nsq.Addr(),\n\t\t\t},\n\t\t}\n\t\tproviderID := len(cfg.PubsubProviders)\n\t\tcfg.PubsubProviders = append(cfg.PubsubProviders, provider)\n\n\t\t// If we're testing the Encore Cloud API locally, override from NSQ\n\t\tif useLocalEncoreCloudAPIForTesting {\n\t\t\tproviderID = len(cfg.PubsubProviders)\n\t\t\tcfg.PubsubProviders = append(cfg.PubsubProviders, &config.PubsubProvider{\n\t\t\t\tEncoreCloud: &config.EncoreCloudPubsubProvider{},\n\t\t\t})\n\t\t}\n\n\t\tcfg.PubsubTopics = make(map[string]*config.PubsubTopic)\n\t\tfor _, t := range md.PubsubTopics {\n\t\t\tproviderName := t.Name\n\t\t\tif useLocalEncoreCloudAPIForTesting {\n\t\t\t\tproviderName = encoreCloudExampleTopicID\n\t\t\t}\n\n\t\t\ttopicCfg := &config.PubsubTopic{\n\t\t\t\tProviderID:    providerID,\n\t\t\t\tEncoreName:    t.Name,\n\t\t\t\tProviderName:  providerName,\n\t\t\t\tSubscriptions: make(map[string]*config.PubsubSubscription),\n\t\t\t}\n\n\t\t\tfor _, s := range t.Subscriptions {\n\t\t\t\tsubscriptionID := t.Name\n\t\t\t\tif useLocalEncoreCloudAPIForTesting {\n\t\t\t\t\tsubscriptionID = encoreCloudExampleSubscriptionID\n\t\t\t\t}\n\n\t\t\t\ttopicCfg.Subscriptions[s.Name] = &config.PubsubSubscription{\n\t\t\t\t\tID:           subscriptionID,\n\t\t\t\t\tEncoreName:   s.Name,\n\t\t\t\t\tProviderName: s.Name,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcfg.PubsubTopics[t.Name] = topicCfg\n\t\t}\n\t}\n\n\tif redis := rm.GetRedis(); redis != nil {\n\t\tsrv := &config.RedisServer{\n\t\t\tHost: redis.Addr(),\n\t\t}\n\t\tserverID := len(cfg.RedisServers)\n\t\tcfg.RedisServers = append(cfg.RedisServers, srv)\n\n\t\tfor _, cluster := range md.CacheClusters {\n\t\t\tcfg.RedisDatabases = append(cfg.RedisDatabases, &config.RedisDatabase{\n\t\t\t\tServerID:   serverID,\n\t\t\t\tDatabase:   0,\n\t\t\t\tEncoreName: cluster.Name,\n\t\t\t\tKeyPrefix:  cluster.Name + \"/\",\n\t\t\t})\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// SQLServerConfig returns the SQL server configuration.\nfunc (rm *ResourceManager) SQLServerConfig() (config.SQLServer, error) {\n\tcluster := rm.GetSQLCluster()\n\tif cluster == nil {\n\t\treturn config.SQLServer{}, errors.New(\"no SQL cluster found\")\n\t}\n\n\tsrvCfg := config.SQLServer{\n\t\tHost: \"localhost:\" + strconv.Itoa(rm.dbProxyPort),\n\t}\n\n\treturn srvCfg, nil\n}\n\n// SQLDatabaseConfig returns the SQL server and database configuration for the given database.\nfunc (rm *ResourceManager) SQLDatabaseConfig(db *meta.SQLDatabase) (config.SQLDatabase, error) {\n\tcluster := rm.GetSQLCluster()\n\tif cluster == nil {\n\t\treturn config.SQLDatabase{}, errors.New(\"no SQL cluster found\")\n\t}\n\n\tdbCfg := config.SQLDatabase{\n\t\tEncoreName:   db.Name,\n\t\tDatabaseName: db.Name,\n\t\tUser:         \"encore\",\n\t\tPassword:     cluster.Password,\n\t}\n\n\treturn dbCfg, nil\n}\n\n// PubSubProviderConfig returns the PubSub provider configuration.\nfunc (rm *ResourceManager) PubSubProviderConfig() (config.PubsubProvider, error) {\n\tnsq := rm.GetPubSub()\n\tif nsq == nil {\n\t\treturn config.PubsubProvider{}, errors.New(\"no PubSub server found\")\n\t}\n\n\treturn config.PubsubProvider{\n\t\tNSQ: &config.NSQProvider{\n\t\t\tHost: nsq.Addr(),\n\t\t},\n\t}, nil\n}\n\n// PubSubTopicConfig returns the PubSub provider and topic configuration for the given topic.\nfunc (rm *ResourceManager) PubSubTopicConfig(topic *meta.PubSubTopic) (config.PubsubProvider, config.PubsubTopic, error) {\n\tproviderCfg, err := rm.PubSubProviderConfig()\n\tif err != nil {\n\t\treturn config.PubsubProvider{}, config.PubsubTopic{}, err\n\t}\n\n\ttopicCfg := config.PubsubTopic{\n\t\tEncoreName:    topic.Name,\n\t\tProviderName:  topic.Name,\n\t\tSubscriptions: make(map[string]*config.PubsubSubscription),\n\t}\n\n\treturn providerCfg, topicCfg, nil\n}\n\n// PubSubSubscriptionConfig returns the PubSub subscription configuration for the given subscription.\nfunc (rm *ResourceManager) PubSubSubscriptionConfig(_ *meta.PubSubTopic, sub *meta.PubSubTopic_Subscription) (config.PubsubSubscription, error) {\n\tsubCfg := config.PubsubSubscription{\n\t\tID:           sub.Name,\n\t\tEncoreName:   sub.Name,\n\t\tProviderName: sub.Name,\n\t}\n\n\treturn subCfg, nil\n}\n\n// RedisConfig returns the Redis server and database configuration for the given database.\nfunc (rm *ResourceManager) RedisConfig(redis *meta.CacheCluster) (config.RedisServer, config.RedisDatabase, error) {\n\tserver := rm.GetRedis()\n\tif server == nil {\n\t\treturn config.RedisServer{}, config.RedisDatabase{}, errors.New(\"no Redis server found\")\n\t}\n\n\tsrvCfg := config.RedisServer{\n\t\tHost: server.Addr(),\n\t}\n\n\tdbCfg := config.RedisDatabase{\n\t\tEncoreName: redis.Name,\n\t\tKeyPrefix:  redis.Name + \"/\",\n\t}\n\n\treturn srvCfg, dbCfg, nil\n}\n\n// BucketProviderConfig returns the bucket provider configuration.\nfunc (rm *ResourceManager) BucketProviderConfig() (config.BucketProvider, string, error) {\n\tobj := rm.GetObjects()\n\tif obj == nil {\n\t\treturn config.BucketProvider{}, \"\", errors.New(\"no object storage found\")\n\t}\n\n\treturn config.BucketProvider{\n\t\tGCS: &config.GCSBucketProvider{\n\t\t\tEndpoint: obj.Endpoint(),\n\t\t},\n\t}, obj.PublicBaseURL(), nil\n}\n"
  },
  {
    "path": "cli/daemon/run/manager.go",
    "content": "package run\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/xid\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/objects\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\t\"encr.dev/cli/daemon/secret\"\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/pkg/errlist\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// Manager manages the set of running applications.\ntype Manager struct {\n\tRuntimePort   int    // port for Encore runtime\n\tDBProxyPort   int    // port for sqldb proxy\n\tDashBaseURL   string // base url for the dev dashboard\n\tSecret        *secret.Manager\n\tClusterMgr    *sqldb.ClusterManager\n\tObjectsMgr    *objects.ClusterManager\n\tPublicBuckets *objects.PublicBucketServer\n\n\tlisteners []EventListener\n\tmu        sync.Mutex\n\truns      map[string]*Run // id -> run\n}\n\n// EventListener is the interface for listening to events\n// about running apps.\ntype EventListener interface {\n\t// OnStart is called when a run starts.\n\tOnStart(r *Run)\n\t// OnCompileStart is called when a run starts compiling.\n\tOnCompileStart(r *Run)\n\t// OnReload is called when a run reloads.\n\tOnReload(r *Run)\n\t// OnStop is called when a run stops.\n\tOnStop(r *Run)\n\t// OnStdout is called when a run outputs something on stdout.\n\tOnStdout(r *Run, out []byte)\n\t// OnStderr is called when a run outputs something on stderr.\n\tOnStderr(r *Run, out []byte)\n\t// OnError is called when a run encounters an error.\n\tOnError(r *Run, err *errlist.List)\n}\n\n// FindProc finds the proc with the given id.\n// It reports nil if no such proc was found.\nfunc (mgr *Manager) FindProc(procID string) *ProcGroup {\n\tmgr.mu.Lock()\n\tdefer mgr.mu.Unlock()\n\tfor _, run := range mgr.runs {\n\t\tif p := run.ProcGroup(); p != nil && p.ID == procID {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// FindRunByAppID finds the run with the given app id.\n// It reports nil if no such run was found.\nfunc (mgr *Manager) FindRunByAppID(appID string) *Run {\n\tmgr.mu.Lock()\n\tdefer mgr.mu.Unlock()\n\tfor _, run := range mgr.runs {\n\t\tif appID == run.App.PlatformID() || appID == run.App.LocalID() {\n\t\t\tselect {\n\t\t\tcase <-run.Done():\n\t\t\t\t// exited\n\t\t\tdefault:\n\t\t\t\treturn run\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// ListRuns provides a snapshot of all runs.\nfunc (mgr *Manager) ListRuns() []*Run {\n\tmgr.mu.Lock()\n\truns := make([]*Run, 0, len(mgr.runs))\n\tfor _, r := range mgr.runs {\n\t\truns = append(runs, r)\n\t}\n\tmgr.mu.Unlock()\n\n\tsort.Slice(runs, func(i, j int) bool { return runs[i].App.PlatformOrLocalID() < runs[j].App.PlatformOrLocalID() })\n\treturn runs\n}\n\n// AddListener adds an event listener to mgr.\n// It must be called before starting the first run.\nfunc (mgr *Manager) AddListener(ln EventListener) {\n\tmgr.listeners = append(mgr.listeners, ln)\n}\n\nfunc (mgr *Manager) RunStdout(r *Run, out []byte) {\n\t// Make sure the run has started before we start outputting\n\t<-r.started\n\tfor _, ln := range mgr.listeners {\n\t\tln.OnStdout(r, out)\n\t}\n}\n\nfunc (mgr *Manager) RunStderr(r *Run, out []byte) {\n\t// Make sure the run has started before we start outputting\n\t<-r.started\n\tfor _, ln := range mgr.listeners {\n\t\tln.OnStderr(r, out)\n\t}\n}\n\nfunc (mgr *Manager) RunError(r *Run, err *errlist.List) {\n\tfor _, ln := range mgr.listeners {\n\t\tln.OnError(r, err)\n\t}\n}\n\ntype parseAppParams struct {\n\tApp           *apps.Instance\n\tEnviron       []string\n\tWorkingDir    string\n\tParseTests    bool\n\tScriptMainPkg string\n}\n\ntype generateConfigParams struct {\n\tApp  *apps.Instance\n\tRM   *infra.ResourceManager\n\tMeta *meta.Data\n\n\tForTests   bool\n\tAuthKey    config.EncoreAuthKey\n\tAPIBaseURL string\n\n\tConfigAppID string\n\tConfigEnvID string\n\n\tExternalCalls bool\n}\n\n// generateServiceDiscoveryMap generates a map of service names to\n// where the Encore daemon is listening to forward to that service binary.\nfunc (mgr *Manager) generateServiceDiscoveryMap(p generateConfigParams) (map[string]config.Service, error) {\n\tservices := make(map[string]config.Service)\n\n\t// Add all the services from the app\n\tfor _, svc := range p.Meta.Svcs {\n\t\tservices[svc.Name] = config.Service{\n\t\t\tName: svc.Name,\n\t\t\t// For now all services are hosted by the same running instance\n\t\t\tURL:         p.APIBaseURL,\n\t\t\tProtocol:    config.Http,\n\t\t\tServiceAuth: mgr.getInternalServiceToServiceAuthMethod(),\n\t\t}\n\t}\n\n\treturn services, nil\n}\n\n// getInternalServiceToServiceAuthMethod returns the auth method to use\n// when making service to service calls locally.\n//\n// This currently just returns the noop auth method, but in the future\n// this function will allow us to use environmental variables to configure\n// the auth method and test different auth methods locally.\nfunc (mgr *Manager) getInternalServiceToServiceAuthMethod() config.ServiceAuth {\n\treturn config.ServiceAuth{Method: \"encore-auth\"}\n}\n\nfunc (mgr *Manager) generateConfig(p generateConfigParams) (*config.Runtime, error) {\n\tenvType := encore.EnvDevelopment\n\tif p.ForTests {\n\t\tenvType = encore.EnvTest\n\t}\n\n\tglobalCORS, err := p.App.GlobalCORS()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to get global CORS\")\n\t}\n\n\tdeployID := xid.New().String()\n\tif p.ForTests {\n\t\tdeployID = \"clitest_\" + deployID\n\t} else {\n\t\tdeployID = \"run_\" + deployID\n\t}\n\n\tserviceDiscovery, err := mgr.generateServiceDiscoveryMap(p)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to generate service discovery map\")\n\t}\n\n\tcfg := &config.Runtime{\n\t\tAppID:         p.ConfigAppID,\n\t\tAppSlug:       p.App.PlatformID(),\n\t\tAPIBaseURL:    p.APIBaseURL,\n\t\tDeployID:      deployID,\n\t\tDeployedAt:    time.Now().UTC(), // Force UTC to not cause confusion\n\t\tEnvID:         p.ConfigEnvID,\n\t\tEnvName:       \"local\",\n\t\tEnvCloud:      string(encore.CloudLocal),\n\t\tEnvType:       string(envType),\n\t\tTraceEndpoint: fmt.Sprintf(\"http://localhost:%d/trace\", mgr.RuntimePort),\n\t\tAuthKeys:      []config.EncoreAuthKey{p.AuthKey},\n\t\tCORS: &config.CORS{\n\t\t\tDebug: globalCORS.Debug,\n\t\t\tAllowOriginsWithCredentials: []string{\n\t\t\t\t// Allow all origins with credentials for local development;\n\t\t\t\t// since it's only running on localhost for development this is safe.\n\t\t\t\tconfig.UnsafeAllOriginWithCredentials,\n\t\t\t},\n\t\t\tAllowOriginsWithoutCredentials: []string{\"*\"},\n\t\t\tExtraAllowedHeaders:            globalCORS.AllowHeaders,\n\t\t\tExtraExposedHeaders:            globalCORS.ExposeHeaders,\n\t\t\tAllowPrivateNetworkAccess:      true,\n\t\t},\n\t\tServiceDiscovery: serviceDiscovery,\n\t\tServiceAuth: []config.ServiceAuth{\n\t\t\tmgr.getInternalServiceToServiceAuthMethod(),\n\t\t},\n\t\tDynamicExperiments: nil, // All experiments would be included in the static config here\n\t}\n\n\tif err := p.RM.UpdateConfig(cfg, p.Meta, mgr.DBProxyPort); err != nil {\n\t\treturn nil, err\n\t}\n\treturn cfg, nil\n}\n"
  },
  {
    "path": "cli/daemon/run/nsq_names.go",
    "content": "package run\n\nimport (\n\t\"encoding/hex\"\n\t\"regexp\"\n\n\t\"golang.org/x/crypto/sha3\"\n)\n\nvar nsqNameRegex = regexp.MustCompile(`^[\\.a-zA-Z0-9_-]+(#ephemeral)?$`)\n\n// isValidNSQName checks if a name is valid according to NSQ requirements:\n// - Must match pattern: ^[\\.a-zA-Z0-9_-]+(#ephemeral)?$\n// - Must be between 1 and 64 characters\nfunc isValidNSQName(name string) bool {\n\treturn len(name) >= 1 && len(name) <= 64 && nsqNameRegex.MatchString(name)\n}\n\n// hashNSQName creates a valid NSQ name by hashing the input.\n// The hash is a SHA3-256 hash encoded as hex (64 characters).\nfunc hashNSQName(name string) string {\n\thash := sha3.Sum256([]byte(name))\n\treturn hex.EncodeToString(hash[:])\n}\n\n// ensureValidNSQName returns the name if it's valid, otherwise returns a hashed version.\nfunc ensureValidNSQName(name string) string {\n\tif isValidNSQName(name) {\n\t\treturn name\n\t}\n\treturn hashNSQName(name)\n}\n"
  },
  {
    "path": "cli/daemon/run/proc_groups.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/daemon/internal/sym\"\n\t\"encr.dev/internal/lookpath\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/noopgateway\"\n\t\"encr.dev/pkg/noopgwdesc\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype procGroupOptions struct {\n\tCtx         context.Context\n\tProcID      string           // unique process id\n\tRun         *Run             // the run the process belongs to\n\tMeta        *meta.Data       // app metadata snapshot\n\tExperiments *experiments.Set // enabled experiments\n\tAuthKey     config.EncoreAuthKey\n\tLogger      RunLogger\n\tWorkingDir  string\n\tConfigGen   *RuntimeConfigGenerator\n}\n\nfunc newProcGroup(opts procGroupOptions) *ProcGroup {\n\tp := &ProcGroup{\n\t\tID:          opts.ProcID,\n\t\tRun:         opts.Run,\n\t\tMeta:        opts.Meta,\n\t\tExperiments: opts.Experiments,\n\t\tworkingDir:  opts.WorkingDir,\n\t\tctx:         opts.Ctx,\n\t\tlogger:      opts.Logger,\n\t\tlog:         opts.Run.log.With().Str(\"proc_id\", opts.ProcID).Logger(),\n\t\tConfigGen:   opts.ConfigGen,\n\n\t\tsymParsed: make(chan struct{}),\n\t\tServices:  make(map[string]*Proc),\n\t\tGateways:  make(map[string]*Proc),\n\t\tauthKey:   opts.AuthKey,\n\t}\n\n\tp.procCond.L = &p.procMu\n\treturn p\n}\n\n// ProcGroup represents a running Encore application\n//\n// It is a collection of [Proc]'s that are all part of the same application,\n// where each [Proc] represents a one or more services or an API gateway.\ntype ProcGroup struct {\n\tID          string           // unique process id\n\tRun         *Run             // the run the process belongs to\n\tMeta        *meta.Data       // app metadata snapshot\n\tExperiments *experiments.Set // enabled experiments\n\n\tGateways map[string]*Proc // the gateway processes, by name (if any)\n\tServices map[string]*Proc // all the service processes by name\n\n\tConfigGen *RuntimeConfigGenerator // generates runtime configuration\n\n\tprocMu       sync.Mutex // protects both allProcesses and runningProcs\n\tprocCond     sync.Cond  // used to signal a change in runningProcs\n\tallProcesses []*Proc    // all processes in the group\n\trunningProcs uint32     // number of running processes\n\n\tctx        context.Context\n\tlogger     RunLogger\n\tlog        zerolog.Logger\n\tworkingDir string\n\n\t// Used for proxying requests when there is no gateway.\n\tnoopGW *noopgateway.Gateway\n\n\tauthKey   config.EncoreAuthKey\n\tsym       *sym.Table\n\tsymErr    error\n\tsymParsed chan struct{} // closed when sym and symErr are set\n}\n\nfunc (pg *ProcGroup) ProxyReq(w http.ResponseWriter, req *http.Request) {\n\t// Currently we only support proxying to the default gateway.\n\t// Need to rethink how this should work when we support multiple gateways.\n\tif gw, ok := pg.Gateways[\"api-gateway\"]; ok {\n\t\tgw.ProxyReq(w, req)\n\t} else {\n\t\tpg.noopGW.ServeHTTP(w, req)\n\t}\n}\n\n// Done returns a channel that is closed when all processes in the group have exited.\nfunc (pg *ProcGroup) Done() <-chan struct{} {\n\tc := make(chan struct{})\n\tgo func() {\n\t\tpg.procMu.Lock()\n\t\tdefer pg.procMu.Unlock()\n\n\t\tfor pg.runningProcs > 0 {\n\t\t\t// If we have more than one process, wait for one to exit\n\t\t\tpg.procCond.Wait()\n\t\t}\n\n\t\tclose(c)\n\t}()\n\n\treturn c\n}\n\n// Start starts all the processes in the group.\nfunc (pg *ProcGroup) Start() (err error) {\n\tpg.procMu.Lock()\n\tdefer pg.procMu.Unlock()\n\n\tfor _, p := range pg.allProcesses {\n\t\tif err = p.start(); err != nil {\n\t\t\tp.Kill()\n\t\t\treturn err\n\t\t}\n\t}\n\n\tpg.noopGW = newNoopGateway(pg)\n\treturn nil\n}\n\n// Close closes the process and waits for it to shutdown.\n// It can safely be called multiple times.\nfunc (pg *ProcGroup) Close() {\n\tvar wg sync.WaitGroup\n\tpg.procMu.Lock()\n\twg.Add(len(pg.allProcesses))\n\tfor _, p := range pg.allProcesses {\n\t\tgo func(p *Proc) {\n\t\t\tp.Close()\n\t\t\twg.Done()\n\t\t}(p)\n\t}\n\n\tpg.procMu.Unlock()\n\n\twg.Wait()\n}\n\n// Kill kills all the processes in the group.\n// It does not wait for them to exit.\nfunc (pg *ProcGroup) Kill() {\n\tpg.procMu.Lock()\n\tdefer pg.procMu.Unlock()\n\n\tfor _, p := range pg.allProcesses {\n\t\tp.Kill()\n\t}\n}\n\n// parseSymTable parses the symbol table of the binary at binPath\n// and stores the result in p.sym and p.symErr.\nfunc (pg *ProcGroup) parseSymTable(binPath string) {\n\tparse := func() (*sym.Table, error) {\n\t\tf, err := os.Open(binPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer fns.CloseIgnore(f)\n\t\treturn sym.Load(f)\n\t}\n\n\tdefer close(pg.symParsed)\n\tpg.sym, pg.symErr = parse()\n}\n\n// SymTable waits for the proc's symbol table to be parsed and then returns it.\n// ctx is used to cancel the wait.\nfunc (pg *ProcGroup) SymTable(ctx context.Context) (*sym.Table, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tcase <-pg.symParsed:\n\t\treturn pg.sym, pg.symErr\n\t}\n}\n\n// newProc creates a new process in the group and sets up the required stuff in the struct\nfunc (pg *ProcGroup) newProc(processName string, listenAddr netip.AddrPort) (*Proc, error) {\n\tdst := &url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   listenAddr.String(),\n\t}\n\tproxy := &httputil.ReverseProxy{\n\t\t// Enable h2c for the proxy.\n\t\tTransport: transport.NewH2CTransport(http.DefaultTransport),\n\t\tRewrite: func(r *httputil.ProxyRequest) {\n\t\t\tr.SetURL(dst)\n\t\t\tr.Out.Header[\"X-Forwarded-For\"] = r.In.Header[\"X-Forwarded-For\"]\n\t\t\tr.SetXForwarded()\n\t\t\t// Copy the host head over.\n\t\t\tr.Out.Host = r.In.Host\n\n\t\t\t// Add the auth key unless the test header is set.\n\t\t\tif r.Out.Header.Get(TestHeaderDisablePlatformAuth) == \"\" {\n\t\t\t\taddAuthKeyToRequest(r.Out, pg.authKey)\n\t\t\t}\n\t\t},\n\t}\n\n\tp := &Proc{\n\t\tgroup:      pg,\n\t\tlog:        pg.log.With().Str(\"proc\", processName).Logger(),\n\t\tlistenAddr: listenAddr,\n\t\thttpProxy:  proxy,\n\t\texit:       make(chan struct{}),\n\t}\n\n\tpg.procMu.Lock()\n\tpg.allProcesses = append(pg.allProcesses, p)\n\tpg.procMu.Unlock()\n\n\treturn p, nil\n}\n\nfunc (pg *ProcGroup) NewAllInOneProc(spec builder.Cmd, listenAddr netip.AddrPort, env []string) error {\n\tp, err := pg.newProc(\"all-in-one\", listenAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Append both the command-specific env and the base environment.\n\tenv = append(env, spec.Env...)\n\n\tcwd := filepath.Join(pg.Run.App.Root(), pg.workingDir)\n\tbinary, err := lookpath.InDir(cwd, env, spec.Command[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// This is safe since the command comes from our build.\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.CommandContext(pg.ctx, binary, spec.Command[1:]...)\n\tcmd.Env = env\n\tcmd.Dir = cwd\n\n\t// Proxy stdout and stderr to the given app logger, if any.\n\tif l := pg.logger; l != nil {\n\t\tcmd.Stdout = newLogWriter(pg.Run, l.RunStdout)\n\t\tcmd.Stderr = newLogWriter(pg.Run, l.RunStderr)\n\t}\n\n\tp.cmd = cmd\n\n\t// Assign all the gateways to this process.\n\tfor _, gw := range pg.Meta.Gateways {\n\t\tpg.Gateways[gw.EncoreName] = p\n\t}\n\n\treturn nil\n}\n\nfunc (pg *ProcGroup) NewProcForService(serviceName string, listenAddr netip.AddrPort, spec builder.Cmd, env []string) error {\n\tif !listenAddr.IsValid() {\n\t\treturn errors.New(\"invalid listen address\")\n\t}\n\n\tp, err := pg.newProc(serviceName, listenAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpg.Services[serviceName] = p\n\n\t// Append both the command-specific env and the base environment.\n\tenv = append(env, spec.Env...)\n\n\tcwd := filepath.Join(pg.Run.App.Root(), pg.workingDir)\n\tbinary, err := lookpath.InDir(cwd, env, spec.Command[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// This is safe since the command comes from our build.\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.CommandContext(pg.ctx, binary, spec.Command[1:]...)\n\tcmd.Env = env\n\tcmd.Dir = cwd\n\n\t// Proxy stdout and stderr to the given app logger, if any.\n\tif l := pg.logger; l != nil {\n\t\tcmd.Stdout = newLogWriter(pg.Run, l.RunStdout)\n\t\tcmd.Stderr = newLogWriter(pg.Run, l.RunStderr)\n\t}\n\n\tp.cmd = cmd\n\n\treturn nil\n}\n\nfunc (pg *ProcGroup) NewProcForGateway(gatewayName string, listenAddr netip.AddrPort, spec builder.Cmd, env []string) error {\n\tif !listenAddr.IsValid() {\n\t\treturn errors.New(\"invalid listen address\")\n\t}\n\n\tp, err := pg.newProc(\"gateway-\"+gatewayName, listenAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpg.Gateways[gatewayName] = p\n\n\t// Append both the command-specific env and the base environment.\n\tenv = append(env, spec.Env...)\n\n\tcwd := filepath.Join(pg.Run.App.Root(), pg.workingDir)\n\tbinary, err := lookpath.InDir(cwd, env, spec.Command[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// This is safe since the command comes from our build.\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.CommandContext(pg.ctx, binary, spec.Command[1:]...)\n\tcmd.Env = env\n\tcmd.Dir = cwd\n\n\t// Bound the wait time to esure prompt live reload if something goes wrong\n\t// with IO copying.\n\tcmd.WaitDelay = 500 * time.Millisecond\n\n\t// Proxy stdout and stderr to the given app logger, if any.\n\tif l := pg.logger; l != nil {\n\t\tcmd.Stdout = newLogWriter(pg.Run, l.RunStdout)\n\t\tcmd.Stderr = newLogWriter(pg.Run, l.RunStderr)\n\t}\n\n\tp.cmd = cmd\n\n\treturn nil\n}\n\ntype warning struct {\n\tTitle string\n\tHelp  string\n}\n\nfunc (pg *ProcGroup) Warnings() (rtn []warning) {\n\tif missing := pg.ConfigGen.MissingSecrets(); len(missing) > 0 {\n\t\trtn = append(rtn, warning{\n\t\t\tTitle: \"secrets not defined: \" + strings.Join(missing, \", \"),\n\t\t\tHelp:  \"undefined secrets are left empty for local development only.\\nsee https://encore.dev/docs/primitives/secrets for more information\",\n\t\t})\n\t}\n\n\treturn rtn\n}\n\n// Proc represents a single Encore process running within a [ProcGroup].\ntype Proc struct {\n\tgroup *ProcGroup     // The group this process belongs to\n\tlog   zerolog.Logger // The logger for this process\n\texit  chan struct{}  // closed when the process has exited\n\tcmd   *exec.Cmd      // The command for this specific process\n\n\tlistenAddr netip.AddrPort         // The port the HTTP server of the process should listen on\n\thttpProxy  *httputil.ReverseProxy // The reverse proxy for the HTTP server of the process\n\n\t// The following fields are only valid after Start() has been called.\n\tStarted   atomic.Bool // whether the process has started\n\tStartedAt time.Time   // when the process started\n\tPid       int         // the OS process id\n}\n\n// Start starts the process and returns immediately.\n//\n// If the process has already been started, this is a no-op.\nfunc (p *Proc) Start() error {\n\tp.group.procMu.Lock()\n\tdefer p.group.procMu.Unlock()\n\n\treturn p.start()\n}\n\n// start starts the process and returns immediately\n//\n// It must be called while locked under the p.group.procMu lock.\nfunc (p *Proc) start() error {\n\tif !p.Started.CompareAndSwap(false, true) {\n\t\treturn nil\n\t}\n\n\tif err := p.cmd.Start(); err != nil {\n\t\treturn errors.Wrap(err, \"could not start process\")\n\t}\n\tp.log.Info().Str(\"addr\", p.listenAddr.String()).Msg(\"process started\")\n\tp.group.runningProcs++\n\n\tp.Pid = p.cmd.Process.Pid\n\tp.StartedAt = time.Now()\n\n\t// Start watching the process for when it quits.\n\tgo func() {\n\t\tdefer close(p.exit)\n\n\t\t// Wait for the process to exit.\n\t\terr := p.cmd.Wait()\n\t\tif err != nil && p.group.ctx.Err() == nil {\n\t\t\tp.log.Error().Err(err).Msg(\"process exited with error\")\n\t\t} else {\n\t\t\tp.log.Info().Msg(\"process exited successfully\")\n\t\t}\n\n\t\t// Flush the logs in case the output did not end in a newline.\n\t\tfor _, w := range [...]io.Writer{p.cmd.Stdout, p.cmd.Stderr} {\n\t\t\tif w != nil {\n\t\t\t\tw.(*logWriter).Flush()\n\t\t\t}\n\t\t}\n\t}()\n\n\t// When the process exits, decrement the running count for the group\n\t// and wake up any goroutines waiting for on the running count to shrink\n\tgo func() {\n\t\t<-p.exit\n\t\tp.group.procMu.Lock()\n\t\tdefer p.group.procMu.Unlock()\n\t\tp.group.runningProcs--\n\n\t\tp.group.procCond.Broadcast()\n\t}()\n\n\treturn nil\n}\n\n// Close closes the process and waits for it to exit.\n// It is safe to call Close multiple times.\nfunc (p *Proc) Close() {\n\tif err := p.cmd.Process.Signal(os.Interrupt); err != nil {\n\t\t// If there's an error sending the signal, just kill the process.\n\t\t// This might happen because Interrupt is not supported on Windows.\n\t\tp.Kill()\n\t}\n\n\ttimer := time.NewTimer(gracefulShutdownTime + (500 * time.Millisecond))\n\tdefer timer.Stop()\n\n\tselect {\n\tcase <-p.exit:\n\t\t// already exited\n\tcase <-timer.C:\n\t\tp.group.log.Error().Msg(\"timed out waiting for process to exit; killing\")\n\t\tp.Kill()\n\t\t<-p.exit\n\t}\n}\n\n// ProxyReq proxies the request to the Encore app.\nfunc (p *Proc) ProxyReq(w http.ResponseWriter, req *http.Request) {\n\tp.httpProxy.ServeHTTP(w, req)\n}\n\n// Kill causes the Process to exit immediately. Kill does not wait until\n// the Process has actually exited. This only kills the Process itself,\n// not any other processes it may have started.\nfunc (p *Proc) Kill() {\n\tif p.cmd != nil && p.cmd.Process != nil {\n\t\t_ = p.cmd.Process.Kill()\n\t}\n}\n\n// pollUntilProcessIsListening polls the listen address until\n// the process is actively listening, five seconds have passed,\n// or the context is canceled, whichever happens first.\n//\n// It reports true if the process is listening on return, false otherwise.\nfunc (p *Proc) pollUntilProcessIsListening(ctx context.Context) (ok bool) {\n\tb := backoff.NewExponentialBackOff()\n\tb.InitialInterval = 50 * time.Millisecond\n\tb.MaxInterval = 250 * time.Millisecond\n\tb.MaxElapsedTime = 5 * time.Second\n\n\terr := backoff.Retry(func() error {\n\t\tif err := ctx.Err(); err != nil {\n\t\t\treturn backoff.Permanent(err)\n\t\t}\n\n\t\tconn, err := (&net.Dialer{}).DialContext(ctx, \"tcp\", p.listenAddr.String())\n\t\tif err == nil {\n\t\t\t_ = conn.Close()\n\t\t}\n\t\treturn err\n\t}, b)\n\treturn err == nil\n}\n\nfunc newNoopGateway(pg *ProcGroup) *noopgateway.Gateway {\n\tsvcDiscovery := make(map[noopgateway.ServiceName]string)\n\tfor _, svc := range pg.Meta.Svcs {\n\t\tif proc, ok := pg.Services[svc.Name]; ok {\n\t\t\tsvcDiscovery[noopgateway.ServiceName(svc.Name)] = proc.listenAddr.String()\n\t\t}\n\t}\n\n\tdesc := noopgwdesc.Describe(pg.Meta, svcDiscovery)\n\tgw := noopgateway.New(desc)\n\n\tgw.Rewrite = func(rp *httputil.ProxyRequest) {\n\t\t// Copy the host head over.\n\t\trp.Out.Host = rp.In.Host\n\n\t\t// Add the auth key unless the test header is set.\n\t\tif rp.Out.Header.Get(TestHeaderDisablePlatformAuth) == \"\" {\n\t\t\taddAuthKeyToRequest(rp.Out, pg.authKey)\n\t\t}\n\t}\n\n\treturn gw\n}\n"
  },
  {
    "path": "cli/daemon/run/run.go",
    "content": "// Package run starts and tracks running Encore applications.\npackage run\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/netip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\t\"encr.dev/cli/daemon/secret\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/userconfig\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/promise\"\n\t\"encr.dev/pkg/svcproxy\"\n\t\"encr.dev/pkg/vcs\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// Run represents a running Encore application.\ntype Run struct {\n\tID              string // unique ID for this instance of the running app\n\tApp             *apps.Instance\n\tListenAddr      string // the address the app is listening on\n\tSvcProxy        *svcproxy.SvcProxy\n\tResourceManager *infra.ResourceManager\n\tNS              *namespace.Namespace\n\tTempDir         string\n\n\tBuilder builder.Impl\n\tlog     zerolog.Logger\n\tMgr     *Manager\n\tParams  *StartParams\n\tsecrets *secret.LoadResult\n\n\tctx     context.Context // ctx is closed when the run is to exit\n\tproc    atomic.Value    // current process\n\texited  chan struct{}   // exit is closed when the run has fully exited\n\tstarted chan struct{}   // started is closed once the run has fully started\n}\n\n// StartParams groups the parameters for the Run method.\ntype StartParams struct {\n\t// App is the app to start.\n\tApp *apps.Instance\n\n\t// NS is the namespace to use.\n\tNS *namespace.Namespace\n\n\t// WorkingDir is the working dir, for formatting\n\t// error messages with relative paths.\n\tWorkingDir string\n\n\t// Watch enables watching for code changes for live reloading.\n\tWatch bool\n\n\tListener   net.Listener // listener to use\n\tListenAddr string       // address we're listening on\n\n\t// Environ are the environment variables to set for the running app,\n\t// in the same format as os.Environ().\n\tEnviron []string\n\n\t// The Ops tracker being used for this run\n\tOpsTracker *optracker.OpTracker\n\n\t// Browser specifies the browser mode to use.\n\tBrowser BrowserMode\n\n\t// Debug specifies to compile the application for debugging.\n\tDebug builder.DebugMode\n\n\t// LogLevel overrides the default log level for the run.\n\tLogLevel option.Option[string]\n\n\t// ScrubSensitiveData enables scrubbing of sensitive data in local traces.\n\tScrubSensitiveData bool\n}\n\n// BrowserMode specifies how to open the browser when starting 'encore run'.\ntype BrowserMode int\n\nconst (\n\tBrowserModeAuto   BrowserMode = iota // open if not already open\n\tBrowserModeNever                     // never open\n\tBrowserModeAlways                    // always open\n)\n\nfunc BrowserModeFromConfig(cfg *userconfig.Config) BrowserMode {\n\tswitch cfg.RunBrowser {\n\tcase \"never\":\n\t\treturn BrowserModeNever\n\tcase \"always\":\n\t\treturn BrowserModeAlways\n\tdefault:\n\t\treturn BrowserModeAuto\n\t}\n}\n\nfunc BrowserModeFromProto(b daemonpb.RunRequest_BrowserMode) BrowserMode {\n\tswitch b {\n\tcase daemonpb.RunRequest_BROWSER_AUTO:\n\t\treturn BrowserModeAuto\n\tcase daemonpb.RunRequest_BROWSER_NEVER:\n\t\treturn BrowserModeNever\n\tcase daemonpb.RunRequest_BROWSER_ALWAYS:\n\t\treturn BrowserModeAlways\n\tdefault:\n\t\treturn BrowserModeAuto\n\t}\n}\n\nfunc DebugModeFromProto(d daemonpb.RunRequest_DebugMode) builder.DebugMode {\n\tswitch d {\n\tcase daemonpb.RunRequest_DEBUG_DISABLED:\n\t\treturn builder.DebugModeDisabled\n\tcase daemonpb.RunRequest_DEBUG_ENABLED:\n\t\treturn builder.DebugModeEnabled\n\tcase daemonpb.RunRequest_DEBUG_BREAK:\n\t\treturn builder.DebugModeBreak\n\tdefault:\n\t\treturn builder.DebugModeDisabled\n\t}\n}\n\n// Start starts the application.\n// Its lifetime is bounded by ctx.\nfunc (mgr *Manager) Start(ctx context.Context, params StartParams) (run *Run, err error) {\n\tlogger := log.With().Str(\"app_id\", params.App.PlatformOrLocalID()).Logger()\n\n\tsvcProxy, err := svcproxy.New(ctx, logger)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to create service proxy\")\n\t}\n\n\ttempDir, err := os.MkdirTemp(\"\", \"encore-run\")\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"couldn't create temp dir\")\n\t}\n\trun = &Run{\n\t\tID:              GenID(),\n\t\tApp:             params.App,\n\t\tNS:              params.NS,\n\t\tResourceManager: infra.NewResourceManager(params.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, params.NS, params.Environ, mgr.DBProxyPort, false),\n\t\tListenAddr:      params.ListenAddr,\n\t\tSvcProxy:        svcProxy,\n\t\tlog:             logger,\n\t\tMgr:             mgr,\n\t\tParams:          &params,\n\t\tTempDir:         tempDir,\n\t\tsecrets:         mgr.Secret.Load(params.App),\n\t\tctx:             ctx,\n\t\texited:          make(chan struct{}),\n\t\tstarted:         make(chan struct{}),\n\t}\n\tdefer func(r *Run) {\n\t\t// Stop all the resource servers if we exit due to an error\n\t\tif err != nil {\n\t\t\tr.Close()\n\t\t}\n\t}(run)\n\n\t// Add the run to our map before starting to avoid\n\t// racing with initialization (though it's unlikely to ever matter).\n\tmgr.mu.Lock()\n\tif mgr.runs == nil {\n\t\tmgr.runs = make(map[string]*Run)\n\t}\n\tmgr.runs[run.ID] = run\n\tmgr.mu.Unlock()\n\n\tif err := run.start(params.Listener, params.OpsTracker); err != nil {\n\t\tif errList := AsErrorList(err); errList != nil {\n\t\t\treturn nil, errList\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tif params.Watch {\n\t\tif err := mgr.watch(run); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn run, nil\n}\n\nfunc (r *Run) Close() {\n\tif r.Builder != nil {\n\t\t_ = r.Builder.Close()\n\t}\n\n\tif r.TempDir != \"\" {\n\t\t_ = os.RemoveAll(r.TempDir)\n\t}\n\n\tr.SvcProxy.Close()\n\tr.ResourceManager.StopAll()\n}\n\n// RunLogger is the interface for listening to run logs.\n// The log methods are called for each logline on stdout and stderr respectively.\ntype RunLogger interface {\n\tRunStdout(r *Run, line []byte)\n\tRunStderr(r *Run, line []byte)\n}\n\n// ProcGroup returns the current running process.\n// It may have already exited.\n// If the proc has not yet started it may return nil.\n//\n// If run is nil then nil will be returned\nfunc (r *Run) ProcGroup() *ProcGroup {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\tp, _ := r.proc.Load().(*ProcGroup)\n\treturn p\n}\n\nfunc (r *Run) StoreProc(p *ProcGroup) {\n\tr.proc.Store(p)\n}\n\n// Done returns a channel that is closed when the run is closed.\nfunc (r *Run) Done() <-chan struct{} {\n\treturn r.exited\n}\n\n// Reload rebuilds the app and, if successful,\n// starts a new proc and switches over.\nfunc (r *Run) Reload() error {\n\terr := r.buildAndStart(r.ctx, nil, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, ln := range r.Mgr.listeners {\n\t\tln.OnReload(r)\n\t}\n\n\treturn nil\n}\n\n// start starts the application and serves requests over HTTP using ln.\nfunc (r *Run) start(ln net.Listener, tracker *optracker.OpTracker) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\t// This is closed below when err == nil,\n\t\t\t// so handle the other cases.\n\t\t\tclose(r.started)\n\t\t\tclose(r.exited)\n\t\t}\n\t}()\n\n\terr = r.buildAndStart(r.ctx, tracker, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Below this line the function must never return an error\n\t// in order to only ensure we Close r.exited exactly once.\n\n\tgo func() {\n\t\tfor _, ln := range r.Mgr.listeners {\n\t\t\tln.OnStart(r)\n\t\t}\n\t\tclose(r.started)\n\t}()\n\n\t// Wrap the handler with h2c support to enable HTTP/2 in cleartext\n\t// (the std http library only accepts HTTP/2 over TLS).\n\t// We need this to be able to forward e.g. gRPC requests to the app.\n\thandler := h2c.NewHandler(r, &http2.Server{})\n\n\t// Run the http server until the app exits.\n\tsrv := &http.Server{Addr: ln.Addr().String(), Handler: handler}\n\tgo func() {\n\t\tif err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {\n\t\t\tr.log.Error().Err(err).Msg(\"could not serve\")\n\t\t}\n\t}()\n\tgo func() {\n\t\t<-r.ctx.Done()\n\t\t_ = srv.Close()\n\t}()\n\n\t// Monitor the running proc and Close the app when it exits.\n\tgo func() {\n\t\tfor {\n\t\t\tp := r.proc.Load().(*ProcGroup)\n\t\t\t<-p.Done()\n\t\t\t// p exited, but it could have been a reload.\n\t\t\t// Check to make sure p is still the active proc.\n\t\t\tp2 := r.proc.Load().(*ProcGroup)\n\t\t\tif p2 == p {\n\t\t\t\t// We're done.\n\t\t\t\tfor _, ln := range r.Mgr.listeners {\n\t\t\t\t\tln.OnStop(r)\n\t\t\t\t}\n\t\t\t\tclose(r.exited)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\treturn nil\n}\n\n// buildAndStart builds the app, starts the proc, and cleans up\n// the build dir when it exits.\n// The proc exits when ctx is canceled.\nfunc (r *Run) buildAndStart(ctx context.Context, tracker *optracker.OpTracker, isReload bool) error {\n\t// Return early if the ctx is already canceled.\n\tif err := ctx.Err(); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, ln := range r.Mgr.listeners {\n\t\tln.OnCompileStart(r)\n\t}\n\n\tjobs := optracker.NewAsyncBuildJobs(ctx, r.App.PlatformOrLocalID(), tracker)\n\n\t// Parse the app source code\n\t// Parse the app to figure out what infrastructure is needed.\n\tstart := time.Now()\n\tparseOp := tracker.Add(\"Building Encore application graph\", start)\n\ttopoOp := tracker.Add(\"Analyzing service topology\", start)\n\n\texpSet, err := r.App.Experiments(r.Params.Environ)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif r.Builder == nil {\n\t\tr.Builder = builderimpl.Resolve(r.App.Lang(), expSet)\n\t}\n\n\tvcsRevision := vcs.GetRevision(r.App.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          r.Params.Debug,\n\t\tEnviron:            r.Params.Environ,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\n\t\tDisableSensitiveScrubbing: !r.Params.ScrubSensitiveData,\n\t}\n\n\t// A context that is canceled when the proc exits.\n\tprocCtx, cancelProcCtx := context.WithCancel(ctx)\n\n\t// Cancel the proc context if we exit with a non-nil error.\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tcancelProcCtx()\n\t\t}\n\t}()\n\n\tprepareResult, err := r.Builder.Prepare(procCtx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        r.App,\n\t\tWorkingDir: r.Params.WorkingDir,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparse, err := r.Builder.Parse(procCtx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         r.App,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  r.Params.WorkingDir,\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\t// Don't use the error itself in tracker.Fail, as it will lead to duplicate error output.\n\t\ttracker.Fail(parseOp, errors.New(\"parse error\"))\n\t\treturn err\n\t}\n\n\tif err := r.App.CacheMetadata(parse.Meta); err != nil {\n\t\treturn errors.Wrap(err, \"cache metadata\")\n\t}\n\ttracker.Done(parseOp, 500*time.Millisecond)\n\ttracker.Done(topoOp, 300*time.Millisecond)\n\n\tr.ResourceManager.StartRequiredServices(jobs, parse.Meta)\n\n\tconfigProm := promise.New(func() (*builder.ServiceConfigsResult, error) {\n\t\treturn r.Builder.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\t\tParse: parse,\n\t\t\tCueMeta: &cueutil.Meta{\n\t\t\t\tAPIBaseURL: fmt.Sprintf(\"http://%s\", r.ListenAddr),\n\t\t\t\tEnvName:    \"local\",\n\t\t\t\tEnvType:    cueutil.EnvType_Development,\n\t\t\t\tCloudType:  cueutil.CloudType_Local,\n\t\t\t},\n\t\t})\n\t})\n\n\tvar build *builder.CompileResult\n\tjobs.Go(\"Compiling application source code\", false, 0, func(ctx context.Context) (err error) {\n\t\tbuild, err = r.Builder.Compile(ctx, builder.CompileParams{\n\t\t\tBuild:       buildInfo,\n\t\t\tApp:         r.App,\n\t\t\tParse:       parse,\n\t\t\tOpTracker:   tracker,\n\t\t\tExperiments: expSet,\n\t\t\tWorkingDir:  r.Params.WorkingDir,\n\t\t\tEnviron:     r.Params.Environ,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"compile error\")\n\t\t}\n\t\treturn nil\n\t})\n\n\tvar secrets map[string]string\n\tjobs.Go(\"Fetching application secrets\", true, 150*time.Millisecond, func(ctx context.Context) error {\n\t\tdata, err := r.secrets.Get(ctx, expSet)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsecrets = data.Values\n\t\treturn nil\n\t})\n\n\tif err := jobs.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tsvcCfg, err := configProm.Get(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstartOp := tracker.Add(\"Starting Encore application\", start)\n\tnewProcess, err := r.StartProcGroup(&StartProcGroupParams{\n\t\tCtx:            ctx,\n\t\tOutputs:        build.Outputs,\n\t\tMeta:           parse.Meta,\n\t\tLogger:         r.Mgr,\n\t\tSecrets:        secrets,\n\t\tServiceConfigs: svcCfg.Configs,\n\t\tEnviron:        r.Params.Environ,\n\t\tWorkingDir:     r.Params.WorkingDir,\n\t\tIsReload:       isReload,\n\t\tExperiments:    expSet,\n\t})\n\tif err != nil {\n\t\ttracker.Fail(startOp, err)\n\t\treturn err\n\t}\n\n\t// Close the proc context when the proc exits.\n\tgo func() {\n\t\tselect {\n\t\tcase <-procCtx.Done():\n\t\t// Already done\n\t\tcase <-newProcess.Done():\n\t\t\tcancelProcCtx()\n\t\t}\n\t}()\n\n\tpreviousProcess := r.proc.Swap(newProcess)\n\tif previousProcess != nil {\n\t\tpreviousProcess.(*ProcGroup).Close()\n\t}\n\n\ttracker.Done(startOp, 50*time.Millisecond)\n\n\tgo func() {\n\t\t// Wait one second before logging all the missing secrets.\n\t\ttime.Sleep(1 * time.Second)\n\n\t\t// Log any warnings.\n\t\tfor _, warning := range newProcess.Warnings() {\n\t\t\tline := \"\\n\" + aurora.Red(fmt.Sprintf(\"warning: %s\", warning.Title)).String() + \"\\n\" +\n\t\t\t\taurora.Gray(16, fmt.Sprintf(\"note: %s\", warning.Help)).String() + \"\\n\\n\"\n\t\t\tr.Mgr.RunStderr(r, []byte(line))\n\t\t}\n\t}()\n\n\treturn nil\n}\n\ntype StartProcGroupParams struct {\n\tCtx            context.Context\n\tOutputs        []builder.BuildOutput\n\tMeta           *meta.Data\n\tSecrets        map[string]string\n\tServiceConfigs map[string]string\n\tLogger         RunLogger\n\tEnviron        []string\n\tWorkingDir     string\n\tIsReload       bool\n\tExperiments    *experiments.Set\n}\n\nconst gracefulShutdownTime = 10 * time.Second\n\n// StartProcGroup starts a single actual OS process for app.\nfunc (r *Run) StartProcGroup(params *StartProcGroupParams) (p *ProcGroup, err error) {\n\tpid := GenID()\n\n\tuserEnv := append([]string{\n\t\t\"ENCORE_RUNTIME_LOG=error\",\n\t\t// Always include internal messages when developing locally.\n\t\t\"ENCORE_API_INCLUDE_INTERNAL_MESSAGE=1\",\n\t}, params.Environ...)\n\n\tdaemonProxyAddr, err := netip.ParseAddrPort(strings.ReplaceAll(r.ListenAddr, \"localhost\", \"127.0.0.1\"))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"failed to parse listen address: %s\", r.ListenAddr)\n\t}\n\tgatewayBaseURL := fmt.Sprintf(\"http://%s\", daemonProxyAddr)\n\tgateways := make(map[string]GatewayConfig)\n\tfor _, gw := range params.Meta.Gateways {\n\t\tgateways[gw.EncoreName] = GatewayConfig{\n\t\t\tBaseURL:   gatewayBaseURL,\n\t\t\tHostnames: []string{\"localhost\"},\n\t\t}\n\t}\n\n\tvar runtimeConfigPath option.Option[string]\n\tvar metaPath option.Option[string]\n\n\tif r.TempDir != \"\" {\n\t\tif r.Builder.UseNewRuntimeConfig() {\n\t\t\truntimeConfigPath = option.Some(filepath.Join(r.TempDir, \"runtime_config.pb\"))\n\t\t} else {\n\t\t\truntimeConfigPath = option.Some(filepath.Join(r.TempDir, \"runtime_config.json\"))\n\t\t}\n\n\t\tif r.Builder.NeedsMeta() {\n\t\t\tmetaPath = option.Some(filepath.Join(r.TempDir, \"meta.pb\"))\n\t\t}\n\t}\n\n\tauthKey := genAuthKey()\n\tp = newProcGroup(procGroupOptions{\n\t\tProcID:  pid,\n\t\tRun:     r,\n\t\tAuthKey: authKey,\n\t\tConfigGen: &RuntimeConfigGenerator{\n\t\t\tapp:               r.App,\n\t\t\tinfraManager:      r.ResourceManager,\n\t\t\tmd:                params.Meta,\n\t\t\tAppID:             option.Some(r.ID),\n\t\t\tEnvID:             option.Some(pid),\n\t\t\tTraceEndpoint:     option.Some(fmt.Sprintf(\"http://localhost:%d/trace\", r.Mgr.RuntimePort)),\n\t\t\tAuthKey:           authKey,\n\t\t\tGateways:          gateways,\n\t\t\tDefinedSecrets:    params.Secrets,\n\t\t\tSvcConfigs:        params.ServiceConfigs,\n\t\t\tDeployID:          option.Some(fmt.Sprintf(\"run_%s\", xid.New().String())),\n\t\t\tIncludeMeta:       r.Builder.NeedsMeta(),\n\t\t\tMetaPath:          metaPath,\n\t\t\tRuntimeConfigPath: runtimeConfigPath,\n\t\t\tLogLevel:          r.Params.LogLevel,\n\t\t},\n\t\tExperiments: params.Experiments,\n\t\tMeta:        params.Meta,\n\t\tCtx:         params.Ctx,\n\t\tWorkingDir:  params.WorkingDir,\n\t\tLogger:      params.Logger,\n\t})\n\n\tif isSingleProc(params.Outputs) {\n\t\tentrypoint := params.Outputs[0].GetEntrypoints()[0]\n\n\t\tconf, err := p.ConfigGen.AllInOneProc(entrypoint.UseRuntimeConfigV2)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Generate the environmental variables for the process\n\t\tprocEnv, err := p.ConfigGen.ProcEnvs(conf, entrypoint.UseRuntimeConfigV2)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to generate environment variables\")\n\t\t}\n\n\t\tenv := slices.Clone(userEnv)\n\t\tenv = append(env, procEnv...)\n\n\t\t// Otherwise we're running everything inside a single process\n\t\tcmd := entrypoint.Cmd.Expand(params.Outputs[0].GetArtifactDir())\n\t\tif err := p.NewAllInOneProc(cmd, conf.ListenAddr, env); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tvar (\n\t\t\tsvcConfs map[string]*ProcConfig\n\t\t\tgwConfs  map[string]*ProcConfig\n\t\t)\n\n\t\tif r.Builder.UseNewRuntimeConfig() {\n\t\t\t_, svcConfs, gwConfs, err = p.ConfigGen.ProcPerServiceWithNewRuntimeConfig(r.SvcProxy)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tsvcConfs, gwConfs, err = p.ConfigGen.ProcPerService(r.SvcProxy)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tfor _, o := range params.Outputs {\n\t\t\tfor _, ep := range o.GetEntrypoints() {\n\t\t\t\tcmd := ep.Cmd.Expand(o.GetArtifactDir())\n\t\t\t\t// create a process for each service\n\t\t\t\tfor _, svcName := range ep.Services {\n\t\t\t\t\t// Generate the environmental variables for the process\n\t\t\t\t\tprocConf, ok := svcConfs[svcName]\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn nil, errors.Newf(\"unknown service %q\", svcName)\n\t\t\t\t\t}\n\t\t\t\t\tprocEnv, err := p.ConfigGen.ProcEnvs(procConf, ep.UseRuntimeConfigV2)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, errors.Wrap(err, \"failed to generate environment variables\")\n\t\t\t\t\t}\n\n\t\t\t\t\tenv := slices.Clone(userEnv)\n\t\t\t\t\tenv = append(env, procEnv...)\n\n\t\t\t\t\tif err := p.NewProcForService(svcName, procConf.ListenAddr, cmd, env); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor _, gwName := range ep.Gateways {\n\t\t\t\t\tprocConf, ok := gwConfs[gwName]\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn nil, errors.Newf(\"unknown gateway %q\", gwName)\n\t\t\t\t\t}\n\n\t\t\t\t\tprocEnv, err := p.ConfigGen.ProcEnvs(procConf, ep.UseRuntimeConfigV2)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, errors.Wrap(err, \"failed to generate environment variables\")\n\t\t\t\t\t}\n\n\t\t\t\t\tenv := slices.Clone(userEnv)\n\t\t\t\t\tenv = append(env, procEnv...)\n\n\t\t\t\t\tif err := p.NewProcForGateway(gwName, procConf.ListenAddr, cmd, env); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Start the processes of the application\n\tif err := p.Start(); err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tp.Kill()\n\t\t}\n\t}()\n\n\t// Monitor the context and Close the process when it is done.\n\tgo func() {\n\t\tselect {\n\t\tcase <-params.Ctx.Done():\n\t\t\tp.Close()\n\t\tcase <-p.Done():\n\t\t}\n\t}()\n\n\t// If this is a live reload, wait for the process to be ready.\n\t// This way we ensure requests are always hitting a running server,\n\t// in case a batch job or something is running.\n\tif params.IsReload {\n\t\tg, ctx := errgroup.WithContext(params.Ctx)\n\t\tfor _, gw := range p.Gateways {\n\t\t\tgw := gw\n\t\t\tg.Go(func() error {\n\t\t\t\tgw.pollUntilProcessIsListening(ctx)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t\t_ = g.Wait()\n\t}\n\n\treturn p, nil\n}\n\n// logWriter is an io.Writer that buffers incoming logs\n// and forwards whole log lines to fn.\ntype logWriter struct {\n\trun     *Run\n\tfn      func(r *Run, line []byte) // matches AppLogger.Log* signature\n\tmaxLine int                       // max line length, including '\\n'\n\tbuf     *bytes.Buffer\n}\n\nfunc newLogWriter(run *Run, fn func(*Run, []byte)) *logWriter {\n\tconst maxLine = 100 * 1024\n\treturn &logWriter{\n\t\trun:     run,\n\t\tfn:      fn,\n\t\tmaxLine: maxLine,\n\t\tbuf:     bytes.NewBuffer(make([]byte, 0, maxLine)),\n\t}\n}\n\nfunc (w *logWriter) Write(b []byte) (int, error) {\n\tn := len(b)\n\tfor {\n\t\tidx := bytes.IndexByte(b, '\\n')\n\t\tif idx < 0 {\n\t\t\tbreak\n\t\t}\n\t\t// We have a line break; write the data to w.fn if it's not too long\n\t\tif (w.buf.Len() + idx + 1) <= w.maxLine {\n\t\t\tw.buf.Write(b[:idx+1])\n\t\t\tw.fn(w.run, w.buf.Bytes())\n\t\t\tw.buf.Reset()\n\t\t}\n\t\tb = b[idx+1:]\n\t}\n\n\t// Postcondition: we have some data remaining that doesn't contain a newline.\n\t// Write it to buf if it's not too long.\n\tif w.buf.Len()+len(b) <= w.maxLine {\n\t\tw.buf.Write(b)\n\t}\n\treturn n, nil\n}\n\n// Flush flushes remaining data to w.fn along with a trailing newline.\n// It must not be called concurrently with any writes to w.\nfunc (w *logWriter) Flush() {\n\tif w.buf.Len() > 0 {\n\t\tw.buf.WriteByte('\\n')\n\t\tw.fn(w.run, w.buf.Bytes())\n\t\tw.buf.Reset()\n\t}\n}\n\n// GenID generates a random run/process id.\n// It panics if it cannot get random bytes.\nfunc GenID() string {\n\tvar b [8]byte\n\tif _, err := rand.Read(b[:]); err != nil {\n\t\tpanic(\"cannot generate random data: \" + err.Error())\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(b[:])\n}\n\n// encodeSecretsEnv encodes secrets to a value that can be passed in an env variable.\nfunc encodeSecretsEnv(secrets map[string]string) string {\n\tif len(secrets) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Sort the keys\n\tkeys := make([]string, 0, len(secrets))\n\tfor k := range secrets {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\n\tvar buf bytes.Buffer\n\tfirst := true\n\tfor _, k := range keys {\n\t\tif !first {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t\tfirst = false\n\n\t\tbuf.WriteString(k)\n\t\tbuf.WriteByte('=')\n\t\tbuf.WriteString(base64.RawURLEncoding.EncodeToString([]byte(secrets[k])))\n\t}\n\n\tgzipped := gzipBytes(buf.Bytes())\n\tstr := \"gzip:\" + base64.StdEncoding.EncodeToString(gzipped)\n\treturn str\n}\n\nfunc usesSecrets(md *meta.Data) bool {\n\tfor _, pkg := range md.Pkgs {\n\t\tif len(pkg.Secrets) > 0 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc genAuthKey() config.EncoreAuthKey {\n\t// read a uint32 from crypto/rand to use as the key ID\n\tvar kidBytes [4]byte\n\tif _, err := rand.Read(kidBytes[:]); err != nil {\n\t\tpanic(\"cannot generate random data: \" + err.Error())\n\t}\n\tkid := binary.BigEndian.Uint32(kidBytes[:])\n\n\t// kid := mathrand.Uint32()\n\tvar b [16]byte\n\tif _, err := rand.Read(b[:]); err != nil {\n\t\tpanic(\"cannot generate random data: \" + err.Error())\n\t}\n\treturn config.EncoreAuthKey{KeyID: kid, Data: b[:]}\n}\n\n// CanDeleteNamespace implements namespace.DeletionHandler.\nfunc (m *Manager) CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error {\n\t// Check if any of the active runs are using this namespace.\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tfor _, r := range m.runs {\n\t\tif r.NS.ID == ns.ID && r.ctx.Err() == nil {\n\t\t\treturn errors.New(\"namespace is in use by 'encore run'\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// DeleteNamespace implements namespace.DeletionHandler.\nfunc (m *Manager) DeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error {\n\t// We don't need to do anything here; we only implement DeletionHandler for\n\t// the CanDeleteNamespace check.\n\treturn nil\n}\n\nfunc isSingleProc(outputs []builder.BuildOutput) bool {\n\tif len(outputs) != 1 {\n\t\treturn false\n\t}\n\treturn len(outputs[0].GetEntrypoints()) == 1\n}\n"
  },
  {
    "path": "cli/daemon/run/runtime_config2.go",
    "content": "package run\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/rs/xid\"\n\t\"go4.org/syncutil\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/durationpb\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\tencoreEnv \"encr.dev/internal/env\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/rtconfgen\"\n\t\"encr.dev/pkg/svcproxy\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n)\n\nconst (\n\truntimeCfgEnvVar     = \"ENCORE_RUNTIME_CONFIG\"\n\truntimeCfgPathEnvVar = \"ENCORE_RUNTIME_CONFIG_PATH\"\n\tappSecretsEnvVar     = \"ENCORE_APP_SECRETS\"\n\tserviceCfgEnvPrefix  = \"ENCORE_CFG_\"\n\tlistenEnvVar         = \"ENCORE_LISTEN_ADDR\"\n\tmetaEnvVar           = \"ENCORE_APP_META\"\n\tmetaPathEnvVar       = \"ENCORE_APP_META_PATH\"\n)\n\ntype RuntimeConfigGenerator struct {\n\tinitOnce syncutil.Once\n\tmd       *meta.Data\n\n\t// The application to generate the config for\n\tapp interface {\n\t\tPlatformID() string\n\t\tPlatformOrLocalID() string\n\t\tGlobalCORS() (appfile.CORS, error)\n\t\tAppFile() (*appfile.File, error)\n\t\tBuildSettings() (appfile.Build, error)\n\t}\n\n\t// The infra manager to use\n\tinfraManager interface {\n\t\tSQLServerConfig() (config.SQLServer, error)\n\t\tPubSubProviderConfig() (config.PubsubProvider, error)\n\n\t\tSQLDatabaseConfig(db *meta.SQLDatabase) (config.SQLDatabase, error)\n\t\tPubSubTopicConfig(topic *meta.PubSubTopic) (config.PubsubProvider, config.PubsubTopic, error)\n\t\tPubSubSubscriptionConfig(topic *meta.PubSubTopic, sub *meta.PubSubTopic_Subscription) (config.PubsubSubscription, error)\n\t\tRedisConfig(redis *meta.CacheCluster) (config.RedisServer, config.RedisDatabase, error)\n\t\tBucketProviderConfig() (config.BucketProvider, string, error)\n\t}\n\n\tAppID         option.Option[string]\n\tEnvID         option.Option[string]\n\tEnvName       option.Option[string]\n\tEnvType       option.Option[runtimev1.Environment_Type]\n\tEnvCloud      option.Option[runtimev1.Environment_Cloud]\n\tTraceEndpoint option.Option[string]\n\tDeployID      option.Option[string]\n\tGateways      map[string]GatewayConfig\n\tAuthKey       config.EncoreAuthKey\n\n\t// Whether to include the metadata.\n\tIncludeMeta bool\n\t// If set, write the metadata to the given path\n\t// instead of including it as an environment variable.\n\tMetaPath option.Option[string]\n\t// If set, write the runtime config to the given path\n\t// instead of including it as an environment variable.\n\tRuntimeConfigPath option.Option[string]\n\n\t// Minimum log level, if any.\n\tLogLevel option.Option[string]\n\n\t// The values of defined secrets.\n\tDefinedSecrets map[string]string\n\t// The configs, per service.\n\tSvcConfigs map[string]string\n\n\tconf     *rtconfgen.Builder\n\tauthKeys []*runtimev1.EncoreAuthKey\n}\n\ntype GatewayConfig struct {\n\tBaseURL   string\n\tHostnames []string\n}\n\nfunc (g *RuntimeConfigGenerator) initialize() error {\n\treturn g.initOnce.Do(func() error {\n\t\tg.conf = rtconfgen.NewBuilder()\n\t\tnewRid := func() string { return \"res_\" + xid.New().String() }\n\n\t\tif deployID, ok := g.DeployID.Get(); ok {\n\t\t\tg.conf.DeployID(deployID)\n\t\t}\n\t\tg.conf.DeployedAt(time.Now())\n\n\t\tg.conf.Env(&runtimev1.Environment{\n\t\t\tAppId:   g.AppID.GetOrElseF(g.app.PlatformOrLocalID),\n\t\t\tAppSlug: g.app.PlatformID(),\n\t\t\tEnvId:   g.EnvID.GetOrElse(\"local\"),\n\t\t\tEnvName: g.EnvName.GetOrElse(\"local\"),\n\t\t\tEnvType: g.EnvType.GetOrElse(runtimev1.Environment_TYPE_DEVELOPMENT),\n\t\t\tCloud:   g.EnvCloud.GetOrElse(runtimev1.Environment_CLOUD_LOCAL),\n\t\t})\n\n\t\ttoSecret := func(b []byte) *runtimev1.SecretData {\n\t\t\treturn &runtimev1.SecretData{\n\t\t\t\tSource: &runtimev1.SecretData_Embedded{Embedded: b},\n\t\t\t}\n\t\t}\n\t\tak := g.AuthKey\n\t\tg.authKeys = []*runtimev1.EncoreAuthKey{{Id: ak.KeyID, Data: toSecret(ak.Data)}}\n\n\t\tg.conf.EncorePlatform(&runtimev1.EncorePlatform{\n\t\t\tPlatformSigningKeys: g.authKeys,\n\t\t\tEncoreCloud:         nil,\n\t\t})\n\n\t\tif traceEndpoint, ok := g.TraceEndpoint.Get(); ok {\n\t\t\tsampleRate := 1.0\n\t\t\tif val, err := strconv.ParseFloat(os.Getenv(\"ENCORE_TRACE_SAMPLING_RATE\"), 64); err == nil {\n\t\t\t\tsampleRate = min(max(val, 0), 1)\n\t\t\t}\n\t\t\tg.conf.TracingProvider(&runtimev1.TracingProvider{\n\t\t\t\tRid: newRid(),\n\t\t\t\tProvider: &runtimev1.TracingProvider_Encore{\n\t\t\t\t\tEncore: &runtimev1.TracingProvider_EncoreTracingProvider{\n\t\t\t\t\t\tTraceEndpoint: traceEndpoint,\n\t\t\t\t\t\tSamplingConfig: []*runtimev1.TracingProvider_SamplingConfig{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tRate:  sampleRate,\n\t\t\t\t\t\t\t\tScope: &runtimev1.TracingProvider_SamplingConfig_Default{Default: &emptypb.Empty{}},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tappFile, err := g.app.AppFile()\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to get app's build settings\")\n\t\t}\n\n\t\tlogLevel := appFile.LogLevel\n\t\tif level, ok := g.LogLevel.Get(); ok {\n\t\t\tlogLevel = level\n\t\t}\n\n\t\tfor _, svc := range g.md.Svcs {\n\t\t\tcfg := &runtimev1.HostedService{\n\t\t\t\tName:      svc.Name,\n\t\t\t\tLogConfig: ptrOrNil(logLevel),\n\t\t\t}\n\n\t\t\tif appFile.Build.WorkerPooling {\n\t\t\t\tn := int32(0)\n\t\t\t\tcfg.WorkerThreads = &n\n\t\t\t}\n\t\t\tg.conf.ServiceConfig(cfg)\n\t\t}\n\n\t\tg.conf.AuthMethods([]*runtimev1.ServiceAuth{\n\t\t\t{\n\t\t\t\tAuthMethod: &runtimev1.ServiceAuth_EncoreAuth_{\n\t\t\t\t\tEncoreAuth: &runtimev1.ServiceAuth_EncoreAuth{\n\t\t\t\t\t\tAuthKeys: g.authKeys,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\tg.conf.DefaultGracefulShutdown(&runtimev1.GracefulShutdown{\n\t\t\tTotal:         durationpb.New(10 * time.Second),\n\t\t\tShutdownHooks: durationpb.New(4 * time.Second),\n\t\t\tHandlers:      durationpb.New(2 * time.Second),\n\t\t})\n\n\t\tfor _, gw := range g.md.Gateways {\n\t\t\tcors, err := g.app.GlobalCORS()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"failed to generate global CORS config\")\n\t\t\t}\n\n\t\t\tg.conf.Infra.Gateway(&runtimev1.Gateway{\n\t\t\t\tRid:        newRid(),\n\t\t\t\tEncoreName: gw.EncoreName,\n\t\t\t\tBaseUrl:    g.Gateways[gw.EncoreName].BaseURL,\n\t\t\t\tHostnames:  g.Gateways[gw.EncoreName].Hostnames,\n\t\t\t\tCors: &runtimev1.Gateway_CORS{\n\t\t\t\t\tDebug:               cors.Debug,\n\t\t\t\t\tDisableCredentials:  false,\n\t\t\t\t\tExtraAllowedHeaders: cors.AllowHeaders,\n\t\t\t\t\tExtraExposedHeaders: cors.ExposeHeaders,\n\n\t\t\t\t\tAllowedOriginsWithCredentials: &runtimev1.Gateway_CORS_UnsafeAllowAllOriginsWithCredentials{\n\t\t\t\t\t\tUnsafeAllowAllOriginsWithCredentials: true,\n\t\t\t\t\t},\n\n\t\t\t\t\tAllowedOriginsWithoutCredentials: &runtimev1.Gateway_CORSAllowedOrigins{\n\t\t\t\t\t\tAllowedOrigins: []string{\"*\"},\n\t\t\t\t\t},\n\n\t\t\t\t\tAllowPrivateNetworkAccess: true,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tif len(g.md.PubsubTopics) > 0 {\n\t\t\tpubsubConfig, err := g.infraManager.PubSubProviderConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"failed to generate pubsub provider config\")\n\t\t\t}\n\n\t\t\tcluster := g.conf.Infra.PubSubCluster(&runtimev1.PubSubCluster{\n\t\t\t\tRid: newRid(),\n\t\t\t\tProvider: &runtimev1.PubSubCluster_Nsq{\n\t\t\t\t\tNsq: &runtimev1.PubSubCluster_NSQ{Hosts: []string{pubsubConfig.NSQ.Host}},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tfor _, topic := range g.md.PubsubTopics {\n\t\t\t\ttopicRid := newRid()\n\n\t\t\t\tvar deliveryGuarantee runtimev1.PubSubTopic_DeliveryGuarantee\n\t\t\t\tswitch topic.DeliveryGuarantee {\n\t\t\t\tcase meta.PubSubTopic_AT_LEAST_ONCE:\n\t\t\t\t\tdeliveryGuarantee = runtimev1.PubSubTopic_DELIVERY_GUARANTEE_AT_LEAST_ONCE\n\t\t\t\tcase meta.PubSubTopic_EXACTLY_ONCE:\n\t\t\t\t\tdeliveryGuarantee = runtimev1.PubSubTopic_DELIVERY_GUARANTEE_EXACTLY_ONCE\n\t\t\t\tdefault:\n\t\t\t\t\treturn errors.Newf(\"unknown delivery guarantee %q\", topic.DeliveryGuarantee)\n\t\t\t\t}\n\n\t\t\t\t// Ensure topic name is valid for NSQ\n\t\t\t\ttopicCloudName := ensureValidNSQName(topic.Name)\n\n\t\t\t\tcluster.PubSubTopic(&runtimev1.PubSubTopic{\n\t\t\t\t\tRid:               topicRid,\n\t\t\t\t\tEncoreName:        topic.Name,\n\t\t\t\t\tCloudName:         topicCloudName,\n\t\t\t\t\tDeliveryGuarantee: deliveryGuarantee,\n\t\t\t\t\tOrderingAttr:      ptrOrNil(topic.OrderingKey),\n\t\t\t\t\tProviderConfig:    nil,\n\t\t\t\t})\n\n\t\t\t\tfor _, sub := range topic.Subscriptions {\n\t\t\t\t\t// Ensure subscription name is valid for NSQ\n\t\t\t\t\tsubCloudName := ensureValidNSQName(sub.Name)\n\n\t\t\t\t\tcluster.PubSubSubscription(&runtimev1.PubSubSubscription{\n\t\t\t\t\t\tRid:                    newRid(),\n\t\t\t\t\t\tTopicEncoreName:        topic.Name,\n\t\t\t\t\t\tSubscriptionEncoreName: sub.Name,\n\t\t\t\t\t\tTopicCloudName:         topicCloudName,\n\t\t\t\t\t\tSubscriptionCloudName:  subCloudName,\n\t\t\t\t\t\tPushOnly:               false,\n\t\t\t\t\t\tProviderConfig:         nil,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(g.md.SqlDatabases) > 0 {\n\t\t\tsrvConfig, err := g.infraManager.SQLServerConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"failed to generate SQL server config\")\n\t\t\t}\n\n\t\t\tcluster := g.conf.Infra.SQLCluster(&runtimev1.SQLCluster{\n\t\t\t\tRid: newRid(),\n\t\t\t})\n\n\t\t\tvar tlsConfig *runtimev1.TLSConfig\n\t\t\tif srvConfig.ServerCACert != \"\" {\n\t\t\t\ttlsConfig = &runtimev1.TLSConfig{\n\t\t\t\t\tServerCaCert: &srvConfig.ServerCACert,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcluster.SQLServer(&runtimev1.SQLServer{\n\t\t\t\tRid:       newRid(),\n\t\t\t\tKind:      runtimev1.ServerKind_SERVER_KIND_PRIMARY,\n\t\t\t\tHost:      srvConfig.Host,\n\t\t\t\tTlsConfig: tlsConfig,\n\t\t\t})\n\n\t\t\tfor _, db := range g.md.SqlDatabases {\n\t\t\t\tif externalDB, ok := g.DefinedSecrets[\"sqldb::\"+db.Name]; ok {\n\t\t\t\t\tvar extCfg struct {\n\t\t\t\t\t\tConnectionString string `json:\"connection_string\"`\n\t\t\t\t\t}\n\t\t\t\t\tif err := json.Unmarshal([]byte(externalDB), &extCfg); err != nil {\n\t\t\t\t\t\treturn errors.Wrapf(err, \"failed to unmarshal external DB config for %q\", db.Name)\n\t\t\t\t\t}\n\t\t\t\t\tpCfg, err := pgx.ParseConfig(extCfg.ConnectionString)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn errors.Wrapf(err, \"failed to parse external DB connection string for %q\", db.Name)\n\t\t\t\t\t}\n\t\t\t\t\tcluster := g.conf.Infra.SQLCluster(&runtimev1.SQLCluster{\n\t\t\t\t\t\tRid: newRid(),\n\t\t\t\t\t})\n\t\t\t\t\tcluster.SQLServer(&runtimev1.SQLServer{\n\t\t\t\t\t\tRid:  newRid(),\n\t\t\t\t\t\tKind: runtimev1.ServerKind_SERVER_KIND_PRIMARY,\n\t\t\t\t\t\tHost: net.JoinHostPort(pCfg.Host, strconv.Itoa(int(pCfg.Port))),\n\t\t\t\t\t\tTlsConfig: &runtimev1.TLSConfig{\n\t\t\t\t\t\t\tDisableCaValidation: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\t// Generate a role rid based on the cluster+username combination.\n\t\t\t\t\troleRid := fmt.Sprintf(\"role:%s:%s\", cluster.Val.Rid, pCfg.User)\n\t\t\t\t\tg.conf.Infra.SQLRole(&runtimev1.SQLRole{\n\t\t\t\t\t\tRid:           roleRid,\n\t\t\t\t\t\tUsername:      pCfg.User,\n\t\t\t\t\t\tPassword:      toSecret([]byte(pCfg.Password)),\n\t\t\t\t\t\tClientCertRid: nil,\n\t\t\t\t\t})\n\t\t\t\t\tcluster.SQLDatabase(&runtimev1.SQLDatabase{\n\t\t\t\t\t\tRid:        newRid(),\n\t\t\t\t\t\tEncoreName: db.Name,\n\t\t\t\t\t\tCloudName:  pCfg.Database,\n\t\t\t\t\t\tConnPools:  nil,\n\t\t\t\t\t}).AddConnectionPool(&runtimev1.SQLConnectionPool{\n\t\t\t\t\t\tIsReadonly:     false,\n\t\t\t\t\t\tRoleRid:        roleRid,\n\t\t\t\t\t\tMinConnections: int32(0),\n\t\t\t\t\t\tMaxConnections: int32(0),\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tdbConfig, err := g.infraManager.SQLDatabaseConfig(db)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn errors.Wrap(err, \"failed to generate SQL database config\")\n\t\t\t\t\t}\n\n\t\t\t\t\t// Generate a role rid based on the cluster+username combination.\n\t\t\t\t\troleRid := fmt.Sprintf(\"role:%s:%s\", cluster.Val.Rid, dbConfig.User)\n\t\t\t\t\tg.conf.Infra.SQLRole(&runtimev1.SQLRole{\n\t\t\t\t\t\tRid:           roleRid,\n\t\t\t\t\t\tUsername:      dbConfig.User,\n\t\t\t\t\t\tPassword:      toSecret([]byte(dbConfig.Password)),\n\t\t\t\t\t\tClientCertRid: nil,\n\t\t\t\t\t})\n\t\t\t\t\tcluster.SQLDatabase(&runtimev1.SQLDatabase{\n\t\t\t\t\t\tRid:        newRid(),\n\t\t\t\t\t\tEncoreName: dbConfig.EncoreName,\n\t\t\t\t\t\tCloudName:  dbConfig.DatabaseName,\n\t\t\t\t\t\tConnPools:  nil,\n\t\t\t\t\t}).AddConnectionPool(&runtimev1.SQLConnectionPool{\n\t\t\t\t\t\tIsReadonly:     false,\n\t\t\t\t\t\tRoleRid:        roleRid,\n\t\t\t\t\t\tMinConnections: int32(dbConfig.MinConnections),\n\t\t\t\t\t\tMaxConnections: int32(dbConfig.MaxConnections),\n\t\t\t\t\t})\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t\tif len(g.md.CacheClusters) > 0 {\n\t\t\tfor _, cl := range g.md.CacheClusters {\n\t\t\t\tsrvConfig, dbConfig, err := g.infraManager.RedisConfig(cl)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errors.Wrap(err, \"failed to generate Redis cluster config\")\n\t\t\t\t}\n\n\t\t\t\tcluster := g.conf.Infra.RedisCluster(&runtimev1.RedisCluster{\n\t\t\t\t\tRid:     newRid(),\n\t\t\t\t\tServers: nil,\n\t\t\t\t})\n\n\t\t\t\t// Generate a role rid based on the cluster+username combination.\n\t\t\t\troleRid := fmt.Sprintf(\"role:%s:%s\", cluster.Val.Rid, srvConfig.User)\n\t\t\t\tg.conf.Infra.RedisRoleFn(roleRid, func() *runtimev1.RedisRole {\n\t\t\t\t\tr := &runtimev1.RedisRole{\n\t\t\t\t\t\tRid:           roleRid,\n\t\t\t\t\t\tClientCertRid: nil,\n\t\t\t\t\t}\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase srvConfig.User != \"\" && srvConfig.Password != \"\":\n\t\t\t\t\t\tr.Auth = &runtimev1.RedisRole_Acl{Acl: &runtimev1.RedisRole_AuthACL{\n\t\t\t\t\t\t\tUsername: srvConfig.User,\n\t\t\t\t\t\t\tPassword: toSecret([]byte(srvConfig.Password)),\n\t\t\t\t\t\t}}\n\t\t\t\t\tcase srvConfig.Password != \"\":\n\t\t\t\t\t\tr.Auth = &runtimev1.RedisRole_AuthString{AuthString: toSecret([]byte(srvConfig.Password))}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tr.Auth = nil\n\t\t\t\t\t}\n\t\t\t\t\treturn r\n\t\t\t\t})\n\n\t\t\t\tvar tlsConfig *runtimev1.TLSConfig\n\t\t\t\tif srvConfig.EnableTLS || srvConfig.ServerCACert != \"\" {\n\t\t\t\t\ttlsConfig = &runtimev1.TLSConfig{\n\t\t\t\t\t\tServerCaCert: ptrOrNil(srvConfig.ServerCACert),\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcluster.RedisServer(&runtimev1.RedisServer{\n\t\t\t\t\tRid:       newRid(),\n\t\t\t\t\tHost:      srvConfig.Host,\n\t\t\t\t\tKind:      runtimev1.ServerKind_SERVER_KIND_PRIMARY,\n\t\t\t\t\tTlsConfig: tlsConfig,\n\t\t\t\t})\n\t\t\t\tcluster.RedisDatabase(&runtimev1.RedisDatabase{\n\t\t\t\t\tRid:         newRid(),\n\t\t\t\t\tEncoreName:  dbConfig.EncoreName,\n\t\t\t\t\tDatabaseIdx: int32(dbConfig.Database),\n\t\t\t\t\tKeyPrefix:   ptrOrNil(dbConfig.KeyPrefix),\n\t\t\t\t\tConnPools:   nil,\n\t\t\t\t}).AddConnectionPool(&runtimev1.RedisConnectionPool{\n\t\t\t\t\tIsReadonly:     false,\n\t\t\t\t\tRoleRid:        roleRid,\n\t\t\t\t\tMinConnections: int32(dbConfig.MinConnections),\n\t\t\t\t\tMaxConnections: int32(dbConfig.MaxConnections),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(g.md.Buckets) > 0 {\n\t\t\tbktProviderConfig, publicBaseURL, err := g.infraManager.BucketProviderConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"failed to generate bucket provider config\")\n\t\t\t}\n\n\t\t\tcluster := g.conf.Infra.BucketCluster(&runtimev1.BucketCluster{\n\t\t\t\tRid: newRid(),\n\t\t\t\tProvider: &runtimev1.BucketCluster_Gcs{\n\t\t\t\t\tGcs: &runtimev1.BucketCluster_GCS{\n\t\t\t\t\t\tEndpoint:  &bktProviderConfig.GCS.Endpoint,\n\t\t\t\t\t\tAnonymous: true,\n\t\t\t\t\t\tLocalSign: &runtimev1.BucketCluster_GCS_LocalSignOptions{\n\t\t\t\t\t\t\tBaseUrl:    publicBaseURL,\n\t\t\t\t\t\t\tAccessId:   \"dummy-sa@encore.local\",\n\t\t\t\t\t\t\tPrivateKey: reverseString(dummyPrivateKeyReversed),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tfor _, bkt := range g.md.Buckets {\n\t\t\t\tbktRid := newRid()\n\n\t\t\t\tvar publicURL *string\n\t\t\t\tif bkt.Public {\n\t\t\t\t\tu := publicBaseURL + \"/\" + bkt.Name\n\t\t\t\t\tpublicURL = &u\n\t\t\t\t}\n\t\t\t\tcluster.Bucket(&runtimev1.Bucket{\n\t\t\t\t\tRid:           bktRid,\n\t\t\t\t\tEncoreName:    bkt.Name,\n\t\t\t\t\tCloudName:     bkt.Name,\n\t\t\t\t\tPublicBaseUrl: publicURL,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tfor secretName, secretVal := range g.DefinedSecrets {\n\t\t\tg.conf.Infra.AppSecret(&runtimev1.AppSecret{\n\t\t\t\tRid:        newRid(),\n\t\t\t\tEncoreName: secretName,\n\t\t\t\tData:       toSecret([]byte(secretVal)),\n\t\t\t})\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\ntype ProcConfig struct {\n\t// The runtime config to add to the process, if any.\n\tRuntime option.Option[*runtimev1.RuntimeConfig]\n\n\tListenAddr netip.AddrPort\n\tExtraEnv   []string\n}\n\nfunc (g *RuntimeConfigGenerator) ProcPerService(proxy *svcproxy.SvcProxy) (services, gateways map[string]*ProcConfig, err error) {\n\tif err := g.initialize(); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tservices = make(map[string]*ProcConfig)\n\tgateways = make(map[string]*ProcConfig)\n\n\tnewRid := func() string { return \"res_\" + xid.New().String() }\n\n\tsd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)}\n\n\tsvcListenAddr := make(map[string]netip.AddrPort)\n\tfor _, svc := range g.md.Svcs {\n\t\tlistenAddr, err := freeLocalhostAddress()\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.Wrap(err, \"failed to find free localhost address\")\n\t\t}\n\t\tsvcListenAddr[svc.Name] = listenAddr\n\t\tsd.Services[svc.Name] = &runtimev1.ServiceDiscovery_Location{\n\t\t\tBaseUrl: proxy.RegisterService(svc.Name, listenAddr),\n\t\t\tAuthMethods: []*runtimev1.ServiceAuth{\n\t\t\t\t{\n\t\t\t\t\tAuthMethod: &runtimev1.ServiceAuth_EncoreAuth_{\n\t\t\t\t\t\tEncoreAuth: &runtimev1.ServiceAuth_EncoreAuth{\n\t\t\t\t\t\t\tAuthKeys: g.authKeys,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\t// Set up the service processes.\n\tfor _, svc := range g.md.Svcs {\n\t\tconf, err := g.conf.Deployment(newRid()).\n\t\t\tServiceDiscovery(sd).\n\t\t\tHostsServices(svc.Name).\n\t\t\tReduceWithMeta(g.md).\n\t\t\tBuildRuntimeConfig()\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t\t}\n\n\t\tusedSecrets := secretsUsedByServices(g.md, svc.Name)\n\t\tlistenAddr := svcListenAddr[svc.Name]\n\t\tconfigEnvs := g.encodeConfigs(svc.Name)\n\n\t\tservices[svc.Name] = &ProcConfig{\n\t\t\tRuntime:    option.Some(conf),\n\t\t\tListenAddr: listenAddr,\n\t\t\tExtraEnv: append([]string{\n\t\t\t\tfmt.Sprintf(\"%s=%s\", appSecretsEnvVar, g.encodeSecrets(usedSecrets)),\n\t\t\t}, configEnvs...),\n\t\t}\n\t}\n\n\t// Set up the gateways.\n\tfor _, gw := range g.md.Gateways {\n\t\tconf, err := g.conf.Deployment(newRid()).ServiceDiscovery(sd).HostsGateways(gw.EncoreName).ReduceWithMeta(g.md).BuildRuntimeConfig()\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t\t}\n\t\tlistenAddr, err := freeLocalhostAddress()\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.Wrap(err, \"failed to find free localhost address\")\n\t\t}\n\t\tgateways[gw.EncoreName] = &ProcConfig{\n\t\t\tRuntime:    option.Some(conf),\n\t\t\tListenAddr: listenAddr,\n\t\t\tExtraEnv:   []string{},\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (g *RuntimeConfigGenerator) AllInOneProc(useRuntimeConfigV2 bool) (*ProcConfig, error) {\n\tif err := g.initialize(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tnewRid := func() string { return \"res_\" + xid.New().String() }\n\n\tsd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)}\n\n\td := g.conf.Deployment(newRid()).ServiceDiscovery(sd)\n\tfor _, gw := range g.md.Gateways {\n\t\td.HostsGateways(gw.EncoreName)\n\t}\n\tfor _, svc := range g.md.Svcs {\n\t\td.HostsServices(svc.Name)\n\t}\n\n\tconf, err := d.ReduceWithMeta(g.md).BuildRuntimeConfig()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t}\n\n\tlistenAddr, err := freeLocalhostAddress()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to find free localhost address\")\n\t}\n\n\tconfigEnvs := g.encodeConfigs(fns.Map(g.md.Svcs, func(svc *meta.Service) string { return svc.Name })...)\n\n\textraEnv := configEnvs\n\tif !useRuntimeConfigV2 {\n\t\tsecretsEnv := fmt.Sprintf(\"%s=%s\", appSecretsEnvVar, encodeSecretsEnv(g.DefinedSecrets))\n\t\textraEnv = append([]string{secretsEnv}, configEnvs...)\n\t}\n\n\treturn &ProcConfig{\n\t\tRuntime:    option.Some(conf),\n\t\tListenAddr: listenAddr,\n\t\tExtraEnv:   extraEnv,\n\t}, nil\n}\n\nfunc (g *RuntimeConfigGenerator) ProcPerServiceWithNewRuntimeConfig(proxy *svcproxy.SvcProxy) (conf *runtimev1.RuntimeConfig, services, gateways map[string]*ProcConfig, err error) {\n\tif err := g.initialize(); err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\n\tif len(g.SvcConfigs) > 0 {\n\t\treturn nil, nil, nil, errors.New(\"service configs not yet supported\")\n\t}\n\n\tservices = make(map[string]*ProcConfig)\n\tgateways = make(map[string]*ProcConfig)\n\n\tnewRid := func() string { return \"res_\" + xid.New().String() }\n\n\tsd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)}\n\n\tsvcListenAddr := make(map[string]netip.AddrPort)\n\tvar svcNames []string\n\tfor _, svc := range g.md.Svcs {\n\t\tsvcNames = append(svcNames, svc.Name)\n\t\tlistenAddr, err := freeLocalhostAddress()\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to find free localhost address\")\n\t\t}\n\t\tsvcListenAddr[svc.Name] = listenAddr\n\t\tsd.Services[svc.Name] = &runtimev1.ServiceDiscovery_Location{\n\t\t\tBaseUrl: proxy.RegisterService(svc.Name, listenAddr),\n\t\t\tAuthMethods: []*runtimev1.ServiceAuth{\n\t\t\t\t{\n\t\t\t\t\tAuthMethod: &runtimev1.ServiceAuth_EncoreAuth_{\n\t\t\t\t\t\tEncoreAuth: &runtimev1.ServiceAuth_EncoreAuth{\n\t\t\t\t\t\t\tAuthKeys: g.authKeys,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\tfor _, svc := range g.md.Svcs {\n\t\tconf, err = g.conf.Deployment(newRid()).\n\t\t\tServiceDiscovery(sd).\n\t\t\tHostsServices(svc.Name).\n\t\t\tReduceWithMeta(g.md).\n\t\t\tBuildRuntimeConfig()\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t\t}\n\n\t\tlistenAddr := svcListenAddr[svc.Name]\n\t\tservices[svc.Name] = &ProcConfig{\n\t\t\tRuntime:    option.Some(conf),\n\t\t\tListenAddr: listenAddr,\n\t\t}\n\t}\n\n\t// Set up the gateways.\n\tfor _, gw := range g.md.Gateways {\n\t\tlistenAddr, err := freeLocalhostAddress()\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to find free localhost address\")\n\t\t}\n\n\t\tconf, err = g.conf.Deployment(newRid()).\n\t\t\tServiceDiscovery(sd).\n\t\t\tHostsGateways(gw.EncoreName).\n\t\t\t//ReduceWithMeta(g.md).\n\t\t\tBuildRuntimeConfig()\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t\t}\n\t\tgateways[gw.EncoreName] = &ProcConfig{\n\t\t\tRuntime:    option.Some(conf),\n\t\t\tListenAddr: listenAddr,\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (g *RuntimeConfigGenerator) ForTests(newRuntimeConf bool) (envs []string, err error) {\n\tif err := g.initialize(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tnewRid := func() string { return \"res_\" + xid.New().String() }\n\n\tsd := &runtimev1.ServiceDiscovery{Services: make(map[string]*runtimev1.ServiceDiscovery_Location)}\n\n\td := g.conf.Deployment(newRid()).ServiceDiscovery(sd)\n\tfor _, gw := range g.md.Gateways {\n\t\td.HostsGateways(gw.EncoreName)\n\t}\n\tfor _, svc := range g.md.Svcs {\n\t\td.HostsServices(svc.Name)\n\t}\n\n\tconf, err := d.ReduceWithMeta(g.md).BuildRuntimeConfig()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t}\n\n\t// Write runtime config to file or env var\n\trtEnvs, err := g.writeRuntimeConfig(conf, newRuntimeConf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tenvs = append(envs, rtEnvs...)\n\n\t// For legacy runtime, also include secrets\n\tif !newRuntimeConf {\n\t\tenvs = append(envs,\n\t\t\tfmt.Sprintf(\"%s=%s\", appSecretsEnvVar, encodeSecretsEnv(g.DefinedSecrets)),\n\t\t)\n\t}\n\n\tsvcNames := fns.Map(g.md.Svcs, func(svc *meta.Service) string { return svc.Name })\n\tenvs = append(envs, g.encodeConfigs(svcNames...)...)\n\n\t// Write metadata to file or env var\n\tif g.IncludeMeta {\n\t\tmetaEnvs, err := g.writeMetadata()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tenvs = append(envs, metaEnvs...)\n\t}\n\n\tif runtimeLibPath := encoreEnv.EncoreRuntimeLib(); runtimeLibPath != \"\" {\n\t\tenvs = append(envs, \"ENCORE_RUNTIME_LIB=\"+runtimeLibPath)\n\t}\n\n\treturn envs, nil\n}\n\nfunc ptrOrNil[T comparable](val T) *T {\n\tvar zero T\n\tif val == zero {\n\t\treturn nil\n\t}\n\treturn &val\n}\n\nfunc (g *RuntimeConfigGenerator) ProcEnvs(proc *ProcConfig, useRuntimeConfigV2 bool) ([]string, error) {\n\tenv := append([]string{\n\t\tfmt.Sprintf(\"%s=%s\", listenEnvVar, proc.ListenAddr.String()),\n\t}, proc.ExtraEnv...)\n\n\tif rt, ok := proc.Runtime.Get(); ok {\n\t\trtEnvs, err := g.writeRuntimeConfig(rt, useRuntimeConfigV2)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tenv = append(env, rtEnvs...)\n\t}\n\n\tif g.IncludeMeta {\n\t\tmetaEnvs, err := g.writeMetadata()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tenv = append(env, metaEnvs...)\n\t}\n\n\tif runtimeLibPath := encoreEnv.EncoreRuntimeLib(); runtimeLibPath != \"\" {\n\t\tenv = append(env, \"ENCORE_RUNTIME_LIB=\"+runtimeLibPath)\n\t}\n\n\treturn env, nil\n}\n\n// writeRuntimeConfig writes the runtime config to either a file (if RuntimeConfigPath is set)\n// or returns it as an environment variable string.\nfunc (g *RuntimeConfigGenerator) writeRuntimeConfig(rt *runtimev1.RuntimeConfig, useRuntimeConfigV2 bool) ([]string, error) {\n\tif runtimeCfgPath, ok := g.RuntimeConfigPath.Get(); ok {\n\t\t// Write to file: marshal the appropriate format directly\n\t\tvar data []byte\n\t\tvar err error\n\n\t\tif useRuntimeConfigV2 {\n\t\t\tdata, err = proto.Marshal(rt)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to marshal runtime config\")\n\t\t\t}\n\t\t} else {\n\t\t\t// We don't use secretEnvs because for local development we use\n\t\t\t// plaintext secrets across the board.\n\t\t\tvar secretEnvs map[string][]byte = nil\n\n\t\t\truntimeCfg, err := rtconfgen.ToLegacy(rt, secretEnvs)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t\t\t}\n\n\t\t\tdata, err = json.Marshal(runtimeCfg)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"failed to marshal runtime config\")\n\t\t\t}\n\t\t}\n\n\t\tif err := os.WriteFile(runtimeCfgPath, data, 0644); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to write runtime config\")\n\t\t}\n\t\treturn []string{fmt.Sprintf(\"%s=%s\", runtimeCfgPathEnvVar, runtimeCfgPath)}, nil\n\t}\n\n\t// Write to environment variable: marshal, optionally gzip, and encode\n\tvar runtimeCfgStr string\n\n\tif useRuntimeConfigV2 {\n\t\truntimeCfgBytes, err := proto.Marshal(rt)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal runtime config\")\n\t\t}\n\t\tgzipped := gzipBytes(runtimeCfgBytes)\n\t\truntimeCfgStr = \"gzip:\" + base64.StdEncoding.EncodeToString(gzipped)\n\t} else {\n\t\t// We don't use secretEnvs because for local development we use\n\t\t// plaintext secrets across the board.\n\t\tvar secretEnvs map[string][]byte = nil\n\n\t\truntimeCfg, err := rtconfgen.ToLegacy(rt, secretEnvs)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to generate runtime config\")\n\t\t}\n\n\t\truntimeCfgBytes, err := json.Marshal(runtimeCfg)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to marshal runtime config\")\n\t\t}\n\t\truntimeCfgStr = base64.RawURLEncoding.EncodeToString(runtimeCfgBytes)\n\t}\n\n\treturn []string{fmt.Sprintf(\"%s=%s\", runtimeCfgEnvVar, runtimeCfgStr)}, nil\n}\n\n// writeMetadata writes the metadata to either a file (if MetaPath is set)\n// or returns it as an environment variable string.\nfunc (g *RuntimeConfigGenerator) writeMetadata() ([]string, error) {\n\tmetaBytes, err := proto.Marshal(g.md)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to marshal metadata\")\n\t}\n\n\tif metaPath, ok := g.MetaPath.Get(); ok {\n\t\tif err := os.WriteFile(metaPath, metaBytes, 0644); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"failed to write metadata\")\n\t\t}\n\t\treturn []string{fmt.Sprintf(\"%s=%s\", metaPathEnvVar, metaPath)}, nil\n\t}\n\n\tgzipped := gzipBytes(metaBytes)\n\tmetaEnvStr := \"gzip:\" + base64.StdEncoding.EncodeToString(gzipped)\n\treturn []string{fmt.Sprintf(\"%s=%s\", metaEnvVar, metaEnvStr)}, nil\n}\n\nfunc (g *RuntimeConfigGenerator) MissingSecrets() []string {\n\tvar missing []string\n\tfor _, pkg := range g.md.Pkgs {\n\t\tfor _, name := range pkg.Secrets {\n\t\t\tif _, ok := g.DefinedSecrets[name]; !ok {\n\t\t\t\tmissing = append(missing, name)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Strings(missing)\n\tmissing = slices.Compact(missing)\n\treturn missing\n}\n\nfunc (g *RuntimeConfigGenerator) encodeSecrets(secretNames map[string]bool) string {\n\tvals := make(map[string]string)\n\tfor name := range secretNames {\n\t\tvals[name] = g.DefinedSecrets[name]\n\t}\n\treturn encodeSecretsEnv(vals)\n}\n\nfunc (g *RuntimeConfigGenerator) encodeConfigs(svcNames ...string) []string {\n\tenvs := make([]string, 0, len(svcNames))\n\tfor _, svcName := range svcNames {\n\t\tcfgStr, ok := g.SvcConfigs[svcName]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tenvs = append(envs,\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"%s%s=%s\",\n\t\t\t\tserviceCfgEnvPrefix,\n\t\t\t\tstrings.ToUpper(svcName),\n\t\t\t\tbase64.RawURLEncoding.EncodeToString([]byte(cfgStr)),\n\t\t\t),\n\t\t)\n\t}\n\n\treturn envs\n}\n\n// secretsUsedByServices returns the set of secrets that are accessible by the given services, using the metadata for access control.\nfunc secretsUsedByServices(md *meta.Data, svcNames ...string) (secretNames map[string]bool) {\n\tsvcNameSet := make(map[string]bool)\n\tfor _, name := range svcNames {\n\t\tsvcNameSet[name] = true\n\t}\n\n\tsecretNames = make(map[string]bool)\n\tfor _, pkg := range md.Pkgs {\n\t\tif len(pkg.Secrets) > 0 && (pkg.ServiceName == \"\" || svcNameSet[pkg.ServiceName]) {\n\t\t\tfor _, secret := range pkg.Secrets {\n\t\t\t\tsecretNames[secret] = true\n\t\t\t}\n\t\t}\n\t}\n\treturn secretNames\n}\n\n// freeLocalhostAddress returns the first free port number on the system.\nfunc freeLocalhostAddress() (netip.AddrPort, error) {\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\treturn netip.AddrPort{}, err\n\t}\n\tdefer func() { _ = l.Close() }()\n\n\treturn l.Addr().(*net.TCPAddr).AddrPort(), nil\n}\n\nfunc encodeServiceConfigs(svcCfgs map[string]string) []string {\n\tenvs := make([]string, 0, len(svcCfgs))\n\tfor serviceName, cfgString := range svcCfgs {\n\t\tenvs = append(envs, \"ENCORE_CFG_\"+strings.ToUpper(serviceName)+\"=\"+base64.RawURLEncoding.EncodeToString([]byte(cfgString)))\n\t}\n\tslices.Sort(envs)\n\treturn envs\n}\n\nfunc gzipBytes(data []byte) []byte {\n\tvar buf bytes.Buffer\n\tw := gzip.NewWriter(&buf)\n\t_, _ = w.Write(data)\n\t_ = w.Close()\n\treturn buf.Bytes()\n}\n\nfunc reverseString(s string) string {\n\trunes := []rune(s)\n\tfor i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {\n\t\trunes[i], runes[j] = runes[j], runes[i]\n\t}\n\treturn string(runes)\n}\n\n// We lightly obfuscate the PK to trigger fewer of the tools that warn about\n// keys in source code.\n//\n// $ tail -r pk.pem | rev\nconst dummyPrivateKeyReversed = `-----YEK ETAVIRP DNE-----\n=AOz3eEM5xAe/71Tfx3sQNkW\n4FXBCChkppSrCoQnR6pBeP31wu0S0UTTNDhNmSYcerdSFbRhyZOzNRnhF9o1h5D5\n+gKkhRZkC33z5+0p8aWwOVWJY8MDycHwvEYvtwcXLNZBHI8L8++mhp0uFz5c5sNM\npPRyurcUY36iDzx7hAJcAGoAvXJwVzTmzXBZtvFPs6Alc5gHti2W1l2bz2mwOV77\nBA9xAW4R6EHVTnqaoXvxvocW5Z9I0ecJzx0NPfkXBriW1lNclAnkoRAYqziasa6C\nWIxePQ2VRFbnLu7XR1M/xqg00GHFV0fTlNPo95lC6tl0PAdoupOX1lwjH3rQnTkB\nY4BgBKQQJ8F0PPTSMAvyK1bcHP2Iob8UFxyHuPOm11aHYwM4VZvmHm8jX/8vz4eb\n6kbNbEkWzfJbbEen/EJLR1XtzvTdjs9bQnJvhQMZmPGzQalqHcVuilQX+PFV4ezM\nA23w1HCIq6vZqXLO8rXhe8S5hImwVSAKq6TK5dlYPOTIBp66lCQgBKwjkcQcX7tq\nmr44FuVB7hqBMfnCB0kKcs1SuYgmfUQE41JGInsqjdpaFOwzQi4Jcx7TK44p9vn2\nik6i/hN7JSVA8kMImWIxtL18uVC/Rg0RpM2vcjd+pfgUDifZ1FVYCiL3WlEzDBlZ\nbSmYdd57T70mEEiuV8QmGiIRrk6kZAMP4CQgBKQ4mIYJX2RJQ1j0V+iXwY/bg+N5\nDPEWLB0w6ReZapNy4DSEMD1zm6IWUuo3rGfCsSKUD0xFR/YkauO5Q+GI2gKvmj5V\nMRiysBL/8PCBwKiFKo1MFjCUfbV/ks49/OJYSOi9WIJiXEg5Tm56BDTH6I8rNdU1\nlGIimbKIuzEBWUHsyDQgBKQQ8O/PDCI/SJSPYjkxw1fpX022hUvVW9pvtmd6v0vX\nM5kMBkT60IwTWhF0DoAx4Uyn4rlPiJy5TUwjC0po/aCRV+ug5C+wIRTCtVCpqRyz\nGeB4U/3WXHmSulzK5Dw4ADfbWSP0dAbNNOaFI4y6u+acEl5MFt3GN/jieITLsZNK\nX18B7zHj7LR2f5k3xiJJ/7uNFl8SCcnVquvEI1qslUSTLEPCNoiy5iX/VVTmVNwv\ndUi92s5oFMyJOFW5joggeeQ55BN6EsjQTnj/XetnpPe5wf5vvptHg5HOcUjJPmIJ\nvsGpMXoyCh3mzdQPMUJM9Ha8DKlACadqTjdid9ZsAAYLAEggCEAABMgAvulUiO2B\nFkdtezbN/f5vpPbr4knO22xylfkUp5Uw0W/HxtntXXobF42guEEiie49zki5fPHK\nvAMC7bOERRLV4v35Dd9QV/KFe0FxqEfm8bFDM6FoA4c0qnkDaKbMhdvxxs0wVFRm\nBukfBCLOt+W/XyFhZvUKkxgbcOjXV7HRFQGI+GZnrf00qbCRNOCdlYLoYX1kf3pQ\neNY6o9ZCJxIDO+dUATCoP3tmP4hvonrjGfpek99D4Ye3+iDwg0AxDW+bt9qoRFew\nVdOuGmooPaDDxn95q5IghRhrvrEaHpkN/EZiNEAJWQkZa9wkxGye5T9hMZRBjUkt\nwGPTyf02fuGquCQABIoAAEgAjSggwcKBCSAAFEQAB0w9GikhqkgBNADABIQvEIIM\n-----YEK ETAVIRP NIGEB-----`\n"
  },
  {
    "path": "cli/daemon/run/tests.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/xid\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\t\"encr.dev/cli/daemon/secret\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/pkg/vcs\"\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n)\n\n// TestParams groups the parameters for the Test method.\ntype TestParams struct {\n\t*TestSpecParams\n\n\t// Stdout and Stderr are where \"go test\" output should be written.\n\tStdout, Stderr io.Writer\n}\n\n// Test runs the tests.\nfunc (mgr *Manager) Test(ctx context.Context, params TestParams) (err error) {\n\texpSet, err := params.App.Experiments(params.Environ)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbld := builderimpl.Resolve(params.App.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\n\tspec, err := mgr.testSpec(ctx, bld, expSet, params.TestSpecParams)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tworkingDir := paths.RootedFSPath(params.App.Root(), params.WorkingDir)\n\treturn bld.RunTests(ctx, builder.RunTestsParams{\n\t\tSpec:       spec,\n\t\tWorkingDir: workingDir,\n\t\tStdout:     params.Stdout,\n\t\tStderr:     params.Stderr,\n\t})\n}\n\n// TestSpecParams are the parameters for computing a test spec.\ntype TestSpecParams struct {\n\t// App is the app to test.\n\tApp *apps.Instance\n\n\t// NS is the namespace to use.\n\tNS *namespace.Namespace\n\n\t// Secrets are the secrets to use.\n\tSecrets *secret.LoadResult\n\n\t// Args are the arguments to pass to the test command.\n\tArgs []string\n\n\t// WorkingDir is the working dir, for formatting\n\t// error messages with relative paths.\n\tWorkingDir string\n\n\t// Environ are the environment variables to set when running the tests,\n\t// in the same format as os.Environ().\n\tEnviron []string\n\n\t// CodegenDebug, if true, specifies to keep the output\n\t// around for codegen debugging purposes.\n\tCodegenDebug bool\n\n\t// TempDir is a path to a temp dir that will be clean up by the test runner.\n\tTempDir string\n}\n\ntype TestSpecResponse struct {\n\tCommand string\n\tArgs    []string\n\tEnviron []string\n}\n\n// TestSpec returns how to run the tests.\nfunc (mgr *Manager) TestSpec(ctx context.Context, params TestSpecParams) (*TestSpecResponse, error) {\n\texpSet, err := params.App.Experiments(params.Environ)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbld := builderimpl.Resolve(params.App.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\n\tspec, err := mgr.testSpec(ctx, bld, expSet, &params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &TestSpecResponse{\n\t\tCommand: spec.Command,\n\t\tArgs:    spec.Args,\n\t\tEnviron: spec.Environ,\n\t}, nil\n}\n\n// testSpec returns how to run the tests.\nfunc (mgr *Manager) testSpec(ctx context.Context, bld builder.Impl, expSet *experiments.Set, params *TestSpecParams) (*builder.TestSpecResult, error) {\n\tvar secrets map[string]string\n\tif params.Secrets != nil {\n\t\tsecretData, err := params.Secrets.Get(ctx, expSet)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsecrets = secretData.Values\n\t\t// remove db override secrets for tests\n\t\tfor k, _ := range secrets {\n\t\t\tif strings.HasPrefix(k, \"sqldb::\") {\n\t\t\t\tdelete(secrets, k)\n\t\t\t}\n\t\t}\n\t}\n\n\tvcsRevision := vcs.GetRevision(params.App.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            params.Environ,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         params.CodegenDebug,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        params.App,\n\t\tWorkingDir: params.WorkingDir,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         params.App,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  params.WorkingDir,\n\t\tParseTests:  true,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := params.App.CacheMetadata(parse.Meta); err != nil {\n\t\treturn nil, errors.Wrap(err, \"cache metadata\")\n\t}\n\n\trm := infra.NewResourceManager(params.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, params.NS, nil, mgr.DBProxyPort, true)\n\n\tjobs := optracker.NewAsyncBuildJobs(ctx, params.App.PlatformOrLocalID(), nil)\n\trm.StartRequiredServices(jobs, parse.Meta)\n\n\t// Note: jobs.Wait must be called before generateConfig.\n\tif err := jobs.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tgateways := make(map[string]GatewayConfig)\n\tgatewayBaseURL := fmt.Sprintf(\"http://localhost:%d\", mgr.RuntimePort)\n\tfor _, gw := range parse.Meta.Gateways {\n\t\tgateways[gw.EncoreName] = GatewayConfig{\n\t\t\tBaseURL:   gatewayBaseURL,\n\t\t\tHostnames: []string{\"localhost\"},\n\t\t}\n\t}\n\n\tcfg, err := bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\tParse: parse,\n\t\tCueMeta: &cueutil.Meta{\n\t\t\tAPIBaseURL: gatewayBaseURL,\n\t\t\tEnvName:    \"local\",\n\t\t\tEnvType:    cueutil.EnvType_Test,\n\t\t\tCloudType:  cueutil.CloudType_Local,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar runtimeConfigPath option.Option[string]\n\tvar metaPath option.Option[string]\n\n\tif params.TempDir != \"\" {\n\t\tif bld.UseNewRuntimeConfig() {\n\t\t\truntimeConfigPath = option.Some(filepath.Join(params.TempDir, \"runtime_config.pb\"))\n\t\t} else {\n\t\t\truntimeConfigPath = option.Some(filepath.Join(params.TempDir, \"runtime_config.json\"))\n\t\t}\n\n\t\tif bld.NeedsMeta() {\n\t\t\tmetaPath = option.Some(filepath.Join(params.TempDir, \"meta.pb\"))\n\t\t}\n\t}\n\n\tauthKey := genAuthKey()\n\tconfigGen := &RuntimeConfigGenerator{\n\t\tapp:               params.App,\n\t\tinfraManager:      rm,\n\t\tmd:                parse.Meta,\n\t\tAppID:             option.Some(params.App.PlatformOrLocalID()),\n\t\tEnvID:             option.Some(\"test\"),\n\t\tTraceEndpoint:     option.Some(fmt.Sprintf(\"http://localhost:%d/trace\", mgr.RuntimePort)),\n\t\tAuthKey:           authKey,\n\t\tGateways:          gateways,\n\t\tDefinedSecrets:    secrets,\n\t\tSvcConfigs:        cfg.Configs,\n\t\tEnvName:           option.Some(\"test\"),\n\t\tEnvType:           option.Some(runtimev1.Environment_TYPE_TEST),\n\t\tDeployID:          option.Some(fmt.Sprintf(\"clitest_%s\", xid.New().String())),\n\t\tIncludeMeta:       bld.NeedsMeta(),\n\t\tMetaPath:          metaPath,\n\t\tRuntimeConfigPath: runtimeConfigPath,\n\t}\n\n\tenv, err := configGen.ForTests(bld.UseNewRuntimeConfig())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tenv = append(env, encodeServiceConfigs(cfg.Configs)...)\n\n\treturn bld.TestSpec(ctx, builder.TestSpecParams{\n\t\tCompile: builder.CompileParams{\n\t\t\tBuild:       buildInfo,\n\t\t\tApp:         params.App,\n\t\t\tParse:       parse,\n\t\t\tOpTracker:   nil,\n\t\t\tExperiments: expSet,\n\t\t\tWorkingDir:  params.WorkingDir,\n\t\t},\n\t\tEnv:  append(params.Environ, env...),\n\t\tArgs: params.Args,\n\t})\n}\n"
  },
  {
    "path": "cli/daemon/run/watch.go",
    "content": "package run\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/pkg/watcher\"\n)\n\n// watch watches the given app for changes, and reports\n// them on c.\nfunc (mgr *Manager) watch(run *Run) error {\n\tsub, err := run.App.Watch(func(i *apps.Instance, event []watcher.Event) {\n\t\tif IgnoreEvents(event) {\n\t\t\treturn\n\t\t}\n\n\t\tmgr.RunStdout(run, []byte(\"Changes detected, recompiling...\\n\"))\n\t\tif err := run.Reload(); err != nil {\n\t\t\tif errList := AsErrorList(err); errList != nil {\n\t\t\t\tmgr.RunError(run, errList)\n\t\t\t} else {\n\t\t\t\terrStr := err.Error()\n\t\t\t\tif !strings.HasSuffix(errStr, \"\\n\") {\n\t\t\t\t\terrStr += \"\\n\"\n\t\t\t\t}\n\t\t\t\tmgr.RunStderr(run, []byte(errStr))\n\t\t\t}\n\t\t} else {\n\t\t\tmgr.RunStdout(run, []byte(\"Reloaded successfully.\\n\"))\n\t\t}\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\t<-run.Done()\n\t\trun.App.Unwatch(sub)\n\t}()\n\n\treturn nil\n}\n\n// IgnoreEvents will return true if _all_ events are on files that should be ignored\n// as the do not impact the running app, or are the result of Encore itself generating code.\nfunc IgnoreEvents(events []watcher.Event) bool {\n\tfor _, event := range events {\n\t\tif !ignoreEvent(event) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc ignoreEvent(ev watcher.Event) bool {\n\tfilename := filepath.Base(ev.Path)\n\tif strings.HasPrefix(strings.ToLower(filename), \"encore.gen.\") {\n\t\t// Ignore generated code\n\t\treturn true\n\t}\n\n\t// Ignore files which wouldn't impact the running app\n\text := filepath.Ext(ev.Path)\n\tswitch ext {\n\tcase \".go\", \".sql\", \".mod\", \".sum\", \".work\", \".app\", \".cue\",\n\t\t\".ts\", \".js\", \".tsx\", \".jsx\", \".mts\", \".mjs\", \".cjs\", \".cts\":\n\t\treturn false\n\tdefault:\n\t\treturn true\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/run.go",
    "content": "package daemon\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/userconfig\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Run runs the application.\nfunc (s *Server) Run(req *daemonpb.RunRequest, stream daemonpb.Daemon_RunServer) error {\n\tctx := stream.Context()\n\tslog := &streamLog{stream: stream, buffered: true}\n\tstderr := slog.Stderr(false)\n\n\tsendExit := func(code int32) {\n\t\t_ = stream.Send(&daemonpb.CommandMessage{\n\t\t\tMsg: &daemonpb.CommandMessage_Exit{Exit: &daemonpb.CommandExit{\n\t\t\t\tCode: code,\n\t\t\t}},\n\t\t})\n\t}\n\n\tuserConfig, err := userconfig.ForApp(req.AppRoot).Get()\n\tif err != nil {\n\t\t_, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red(\"failed to load config: %v\"), err))\n\t\tsendExit(1)\n\t\treturn nil\n\t}\n\n\tctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, req.TraceFile)\n\tif err != nil {\n\t\t_, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red(\"failed to begin tracing: %v\"), err))\n\t\tsendExit(1)\n\t\treturn nil\n\t}\n\tdefer fns.CloseIgnore(tracer)\n\n\t// ListenAddr should always be passed but guard against old clients.\n\tlistenAddr := req.ListenAddr\n\tif listenAddr == \"\" {\n\t\tlistenAddr = \":4000\"\n\t}\n\tln, err := net.Listen(\"tcp\", listenAddr)\n\tif err != nil {\n\t\tif errIsAddrInUse(err) {\n\t\t\t_, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red(\"Failed to run on %s - port is already in use\"), listenAddr))\n\t\t} else {\n\t\t\t_, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red(\"Failed to run on %s - %v\"), listenAddr, err))\n\t\t}\n\n\t\tif host, port, ok := findAvailableAddr(listenAddr); ok {\n\t\t\tif host == \"localhost\" || host == \"127.0.0.1\" {\n\t\t\t\t_, _ = fmt.Fprintf(stderr, \"Note: port %d is available; specify %s to use it\\n\",\n\t\t\t\t\tport, aurora.Sprintf(aurora.Cyan(\"--port=%d\"), port))\n\t\t\t} else {\n\t\t\t\t_, _ = fmt.Fprintf(stderr, \"Note: address %s:%d is available; specify %s to use it\\n\",\n\t\t\t\t\thost, port, aurora.Sprintf(aurora.Cyan(\"--listen=%s:%d\"), host, port))\n\t\t\t}\n\t\t} else {\n\t\t\t_, _ = fmt.Fprintf(stderr, \"Note: specify %s to run on another port\\n\",\n\t\t\t\taurora.Cyan(\"--port=NUMBER\"))\n\t\t}\n\n\t\tsendExit(1)\n\t\treturn nil\n\t}\n\tdefer fns.CloseIgnore(ln)\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\t_, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red(\"failed to resolve app: %v\"), err))\n\t\tsendExit(1)\n\t\treturn nil\n\t}\n\n\tns, err := s.namespaceOrActive(ctx, app, req.Namespace)\n\tif err != nil {\n\t\t_, _ = fmt.Fprintln(stderr, aurora.Sprintf(aurora.Red(\"failed to resolve namespace: %v\"), err))\n\t\tsendExit(1)\n\t\treturn nil\n\t}\n\n\tops := optracker.New(stderr, stream)\n\tdefer ops.AllDone() // Kill the tracker when we exit this function\n\n\t// Check for available update before we start the proc\n\t// so the output from the proc doesn't race with our\n\t// prints below.\n\tnewVer := s.availableUpdate()\n\n\t// If force upgrade has been enabled, we force the upgrade now before we try and run the app\n\tif newVer != nil && newVer.ForceUpgrade {\n\t\t_, _ = fmt.Fprint(stderr, aurora.Red(\"An urgent security update for Encore is available.\").String()+\"\\n\")\n\t\tif newVer.SecurityNotes != \"\" {\n\t\t\t_, _ = fmt.Fprint(stderr, aurora.Sprintf(aurora.Yellow(\"%s\"), newVer.SecurityNotes)+\"\\n\")\n\t\t}\n\n\t\t_, _ = fmt.Fprintf(stderr, \"Upgrading Encore to %v...\\n\", newVer.Version())\n\t\tif err := newVer.DoUpgrade(stderr, stderr); err != nil {\n\t\t\t_, _ = fmt.Fprint(stderr, aurora.Sprintf(aurora.Red(\"Upgrade failed: %v\"), err)+\"\\n\")\n\t\t}\n\n\t\tslog.FlushBuffers()\n\t\tsendExit(1) // Kill the client\n\t\tos.Exit(1)  // Kill the daemon too\n\t\treturn nil\n\t}\n\n\t// Hold the stream mutex so we can set up the stream map\n\t// before output starts.\n\ts.mu.Lock()\n\n\t// If the listen addr contains no interface, render it as \"localhost:port\"\n\t// instead of just \":port\".\n\tdisplayListenAddr := req.ListenAddr\n\tif strings.HasPrefix(listenAddr, \":\") {\n\t\tdisplayListenAddr = \"localhost\" + req.ListenAddr\n\t}\n\n\tbrowser := run.BrowserModeFromProto(req.Browser)\n\tif browser == run.BrowserModeAuto {\n\t\tbrowser = run.BrowserModeFromConfig(userConfig)\n\t}\n\n\trunInstance, err := s.mgr.Start(ctx, run.StartParams{\n\t\tApp:                app,\n\t\tNS:                 ns,\n\t\tWorkingDir:         req.WorkingDir,\n\t\tListener:           ln,\n\t\tListenAddr:         displayListenAddr,\n\t\tWatch:              req.Watch,\n\t\tEnviron:            req.Environ,\n\t\tOpsTracker:         ops,\n\t\tBrowser:            browser,\n\t\tDebug:              run.DebugModeFromProto(req.DebugMode),\n\t\tLogLevel:           option.FromPointer(req.LogLevel),\n\t\tScrubSensitiveData: req.ScrubSensitiveData,\n\t})\n\tif err != nil {\n\t\ts.mu.Unlock()\n\t\tif errList := run.AsErrorList(err); errList != nil {\n\t\t\t_ = errList.SendToStream(stream)\n\t\t} else {\n\t\t\terrStr := err.Error()\n\t\t\tif !strings.HasSuffix(errStr, \"\\n\") {\n\t\t\t\terrStr += \"\\n\"\n\t\t\t}\n\t\t\t_, _ = stderr.Write([]byte(errStr))\n\t\t}\n\t\tsendExit(1)\n\t\treturn nil\n\t}\n\tdefer runInstance.Close()\n\ts.streams[runInstance.ID] = slog\n\ts.mu.Unlock()\n\n\tops.AllDone()\n\n\tsecrets, _ := s.sm.Load(app).Get(ctx, nil)\n\texternalDBs := map[string]string{}\n\tfor key, val := range secrets.Values {\n\t\tif db, ok := strings.CutPrefix(key, \"sqldb::\"); ok {\n\t\t\tvar connCfg struct {\n\t\t\t\tConnString string `json:\"connection_string\"`\n\t\t\t}\n\t\t\terr := json.Unmarshal([]byte(val), &connCfg)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warn().Err(err).Str(\"key\", key).Msg(\"failed to unmarshal connection string\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconnURL, err := url.Parse(connCfg.ConnString)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warn().Err(err).Str(\"key\", key).Msg(\"failed to parse connection string\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconnURL.User = url.User(connURL.User.Username())\n\t\t\texternalDBs[db] = connURL.String()\n\n\t\t}\n\t}\n\t_, _ = stderr.Write([]byte(\"\\n\"))\n\t_, _ = fmt.Fprintf(stderr, \"  Encore development server running!\\n\\n\")\n\n\t_, _ = fmt.Fprintf(stderr, \"  Your API is running at:     %s\\n\", aurora.Cyan(\"http://\"+runInstance.ListenAddr))\n\t_, _ = fmt.Fprintf(stderr, \"  Development Dashboard URL:  %s\\n\", aurora.Cyan(fmt.Sprintf(\n\t\t\"%s/%s\", s.mgr.DashBaseURL, app.PlatformOrLocalID())))\n\t_, _ = fmt.Fprintf(stderr, \"  MCP SSE URL:                %s\\n\", aurora.Cyan(fmt.Sprintf(\n\t\t\"%s/sse?appID=%s\", s.mcp.BaseURL, app.PlatformOrLocalID())))\n\n\tif ns := runInstance.NS; !ns.Active || ns.Name != \"default\" {\n\t\t_, _ = fmt.Fprintf(stderr, \"  Namespace:                  %s\\n\", aurora.Cyan(ns.Name))\n\t\tif len(externalDBs) > 0 {\n\t\t\t_, _ = fmt.Fprintln(stderr, \"  External databases:\")\n\t\t}\n\t}\n\tfor db, connStr := range externalDBs {\n\t\t_, _ = fmt.Fprintf(stderr, \"     %s: %s\\n\", db, aurora.Cyan(connStr))\n\t}\n\tif req.DebugMode == daemonpb.RunRequest_DEBUG_ENABLED {\n\t\t// Print the pid for debugging. Currently we only support this if we have a default gateway.\n\t\tif gw, ok := runInstance.ProcGroup().Gateways[\"api-gateway\"]; ok {\n\t\t\t_, _ = fmt.Fprintf(stderr, \"  Process ID:                 %d\\n\", aurora.Cyan(gw.Pid))\n\t\t}\n\t}\n\t// Log which experiments are enabled, if any\n\tif exp := runInstance.ProcGroup().Experiments.List(); len(exp) > 0 {\n\t\tstrs := make([]string, len(exp))\n\t\tfor i, e := range exp {\n\t\t\tstrs[i] = string(e)\n\t\t}\n\t\t_, _ = fmt.Fprintf(stderr, \"  Enabled experiment(s):      %s\\n\", aurora.Yellow(strings.Join(strs, \", \")))\n\t}\n\n\t// If there's a newer version available, print a message.\n\tif newVer != nil {\n\t\tif newVer.SecurityUpdate {\n\t\t\t_, _ = stderr.Write([]byte(aurora.Sprintf(\n\t\t\t\taurora.Yellow(\"\\n  New Encore release available with security updates: %s (you have %s)\\n  Update with: encore version update\\n\"),\n\t\t\t\tnewVer.Version(), version.Version)))\n\n\t\t\tif newVer.SecurityNotes != \"\" {\n\t\t\t\t_, _ = stderr.Write([]byte(aurora.Sprintf(\n\t\t\t\t\taurora.Faint(\"\\n  %s\\n\"),\n\t\t\t\t\tnewVer.SecurityNotes)))\n\t\t\t}\n\t\t} else {\n\t\t\t_, _ = stderr.Write([]byte(aurora.Sprintf(\n\t\t\t\taurora.Faint(\"\\n  New Encore release available: %s (you have %s)\\n  Update with: encore version update\\n\"),\n\t\t\t\tnewVer.Version(), version.Version)))\n\t\t}\n\t}\n\t_, _ = stderr.Write([]byte(\"\\n\"))\n\n\tslog.FlushBuffers()\n\n\tgo func() {\n\t\t// Wait a little bit for the app to start\n\t\tselect {\n\t\tcase <-runInstance.Done():\n\t\t\treturn\n\t\tcase <-time.After(5 * time.Second):\n\t\t\tif proc := runInstance.ProcGroup(); proc != nil {\n\t\t\t\tshowFirstRunExperience(runInstance, proc.Meta, stderr)\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-runInstance.Done() // wait for run to complete\n\n\ts.mu.Lock()\n\tdelete(s.streams, runInstance.ID)\n\ts.mu.Unlock()\n\treturn nil\n}\n"
  },
  {
    "path": "cli/daemon/schema.go",
    "content": "package daemon\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n// genSchema generates a JSON payload to match the schema.\nfunc genSchema(meta *meta.Data, decl *schema.Type) []byte {\n\tif decl == nil {\n\t\treturn nil\n\t}\n\tr := &schemaRenderer{\n\t\tStream:    jsoniter.NewStream(jsoniter.ConfigDefault, nil, 256),\n\t\tmeta:      meta,\n\t\tseenDecls: make(map[uint32]*schema.Decl),\n\t}\n\treturn r.Render(decl)\n}\n\ntype schemaRenderer struct {\n\t*jsoniter.Stream\n\tmeta      *meta.Data\n\tseenDecls map[uint32]*schema.Decl\n\ttypeArgs  []*schema.Type\n}\n\nfunc (r *schemaRenderer) Render(d *schema.Type) []byte {\n\tr.renderType(d)\n\treturn r.Buffer()\n}\n\nfunc (r *schemaRenderer) renderType(typ *schema.Type) {\n\tswitch typ := typ.Typ.(type) {\n\tcase *schema.Type_Struct:\n\t\tr.renderStruct(typ.Struct)\n\tcase *schema.Type_Map:\n\t\tr.renderMap(typ.Map)\n\tcase *schema.Type_List:\n\t\tr.renderList(typ.List)\n\tcase *schema.Type_Builtin:\n\t\tr.renderBuiltin(typ.Builtin)\n\tcase *schema.Type_Named:\n\t\tr.renderNamed(typ.Named)\n\tcase *schema.Type_Pointer:\n\t\tr.renderType(typ.Pointer.Base)\n\tcase *schema.Type_Option:\n\t\tr.WriteNil()\n\tcase *schema.Type_Union:\n\t\tr.renderType(typ.Union.Types[0])\n\tcase *schema.Type_Literal:\n\t\tswitch v := typ.Literal.Value.(type) {\n\t\tcase *schema.Literal_Str:\n\t\t\tr.WriteString(v.Str)\n\t\tcase *schema.Literal_Int:\n\t\t\tr.WriteInt(int(v.Int))\n\t\tcase *schema.Literal_Float:\n\t\t\tr.WriteFloat64(v.Float)\n\t\tcase *schema.Literal_Boolean:\n\t\t\tr.WriteBool(v.Boolean)\n\t\tcase *schema.Literal_Null:\n\t\t\tr.WriteNil()\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unknown literal type %T\", v))\n\t\t}\n\tcase *schema.Type_TypeParameter:\n\t\tif idx := typ.TypeParameter.ParamIdx; len(r.typeArgs) > int(idx) {\n\t\t\tr.renderType(r.typeArgs[idx])\n\t\t} else {\n\t\t\tr.WriteNil()\n\t\t}\n\tcase *schema.Type_Config:\n\t\t// Config is invisible here\n\t\tr.renderType(typ.Config.Elem)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown schema type %T\", typ))\n\t}\n}\n\nfunc (r *schemaRenderer) renderStruct(s *schema.Struct) {\n\tr.WriteObjectStart()\n\twritten := false\n\tfor _, f := range s.Fields {\n\t\tn := f.JsonName\n\t\tif n == \"-\" {\n\t\t\tcontinue\n\t\t} else if n == \"\" {\n\t\t\tn = f.Name\n\t\t}\n\n\t\tif written {\n\t\t\tr.WriteMore()\n\t\t}\n\t\tr.WriteObjectField(n)\n\t\tr.renderType(f.Typ)\n\t\twritten = true\n\t}\n\tr.WriteObjectEnd()\n}\n\nfunc (r *schemaRenderer) renderMap(m *schema.Map) {\n\tr.WriteObjectStart()\n\tr.renderType(m.Key)\n\tr.WriteRaw(\": \")\n\tr.renderType(m.Value)\n\tr.WriteObjectEnd()\n}\n\nfunc (r *schemaRenderer) renderList(l *schema.List) {\n\tr.WriteArrayStart()\n\tr.renderType(l.Elem)\n\tr.WriteArrayEnd()\n}\n\nfunc (r *schemaRenderer) renderBuiltin(b schema.Builtin) {\n\tswitch b {\n\tcase schema.Builtin_ANY:\n\t\tr.WriteString(\"<any data>\")\n\tcase schema.Builtin_BOOL:\n\t\tr.WriteBool(true)\n\tcase schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64,\n\t\tschema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64:\n\t\tr.WriteInt(1)\n\tcase schema.Builtin_FLOAT32, schema.Builtin_FLOAT64:\n\t\tr.WriteRaw(\"2.3\")\n\tcase schema.Builtin_STRING:\n\t\tr.WriteString(\"hello\")\n\tcase schema.Builtin_BYTES:\n\t\tr.WriteString(\"YmFzZTY0Cg==\") // \"base64\"\n\tcase schema.Builtin_TIME:\n\t\ts, _ := time.Now().MarshalText()\n\t\tr.WriteString(string(s))\n\tcase schema.Builtin_UUID:\n\t\tr.WriteString(\"7d42f515-3517-4e76-be13-30880443546f\")\n\tcase schema.Builtin_JSON:\n\t\tr.WriteObjectStart()\n\t\tr.WriteObjectField(\"some json data\")\n\t\tr.WriteBool(true)\n\t\tr.WriteObjectEnd()\n\tcase schema.Builtin_USER_ID:\n\t\tr.WriteString(\"userID\")\n\tdefault:\n\t\tr.WriteString(\"<unknown>\")\n\t}\n}\n\nfunc (r *schemaRenderer) renderNamed(n *schema.Named) {\n\tif _, ok := r.seenDecls[n.Id]; ok {\n\t\t// Already seen this name before\n\t\tr.WriteNil()\n\t\treturn\n\t}\n\n\t// Store type arguments in scope. Restore the previous\n\t// type arguments when we're done.\n\tprevTypeArgs := r.typeArgs\n\tdefer func() {\n\t\tr.typeArgs = prevTypeArgs\n\t}()\n\tr.typeArgs = n.TypeArguments\n\n\t// Avoid infinite recursion\n\tdecl := r.meta.Decls[n.Id]\n\tr.seenDecls[n.Id] = decl\n\tr.renderType(decl.Type)\n\tdelete(r.seenDecls, n.Id)\n}\n"
  },
  {
    "path": "cli/daemon/secret/secret.go",
    "content": "// Package secret fetches and caches development secrets for Encore apps.\npackage secret\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"cuelang.org/go/cue\"\n\t\"cuelang.org/go/cue/cuecontext\"\n\t\"cuelang.org/go/cue/load\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go4.org/syncutil\"\n\t\"golang.org/x/sync/singleflight\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/pkg/xos\"\n)\n\n// New returns a new manager.\nfunc New() *Manager {\n\treturn &Manager{cache: make(map[string]*Data)}\n}\n\n// Manager manages the secrets cache for running Encore apps.\ntype Manager struct {\n\tgroup    singleflight.Group\n\tpollOnce sync.Once\n\n\tmu    sync.Mutex\n\tcache map[string]*Data\n}\n\n// Data is a snapshot of an Encore app's development secret values.\ntype Data struct {\n\t// Synced is when the values were last synced,\n\t// or the zero value if no sync has taken place.\n\tSynced time.Time\n\t// Values is a key-value map of defined secrets.\n\tValues map[string]string\n}\n\ntype LoadResult struct {\n\tmgr *Manager\n\tapp *apps.Instance\n\n\tonce    syncutil.Once\n\tch      <-chan singleflight.Result\n\tinitial singleflight.Result\n\n\tlocalSecretMu sync.Mutex\n}\n\n// Load loads the secrets for appSlug.\n// If appSlug is empty, (*LoadResult).Get resolves to empty secret data.\nfunc (mgr *Manager) Load(app *apps.Instance) *LoadResult {\n\tmgr.pollOnce.Do(mgr.startPolling)\n\n\t// Ignore cases when the app isn't linked.\n\tif app.PlatformID() == \"\" {\n\t\treturn &LoadResult{mgr: mgr, app: app}\n\t}\n\n\tch := mgr.fetch(app.PlatformID(), false)\n\treturn &LoadResult{mgr: mgr, app: app, ch: ch}\n}\n\n// Get returns the result of the prefetch.\n// It blocks until the initial fetch is ready or until ctx is cancelled.\n// For subsequent calls to Get (such as during live reload), it returns any\n// more recent data that has been subsequently cached.\nfunc (lr *LoadResult) Get(ctx context.Context, expSet *experiments.Set) (data *Data, err error) {\n\tdefer func() {\n\t\tif err == nil {\n\t\t\t// load.Instances in cue is not safe for concurrent access.\n\t\t\t// https://github.com/cue-lang/cue/issues/1746\n\t\t\tlr.localSecretMu.Lock()\n\t\t\tdefer lr.localSecretMu.Unlock()\n\t\t\t// Return a new data object so we don't write the overrides to the cache.\n\t\t\tdata, err = applyLocalOverrides(lr.app, data)\n\t\t}\n\t}()\n\n\tif lr == nil || lr.app.PlatformID() == \"\" {\n\t\treturn &Data{}, nil\n\t}\n\n\t// Fetch the initial result the first time.\n\terr = lr.once.Do(func() error {\n\t\tselect {\n\t\tcase lr.initial = <-lr.ch:\n\t\t\t// The fetch was successful so mark the Once as completed.\n\t\t\treturn nil\n\t\tcase <-ctx.Done():\n\t\t\t// We timed out before the fetch completed.\n\t\t\treturn ctx.Err()\n\t\t}\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinitial, _ := lr.initial.Val.(*Data)\n\thaveInitial := lr.initial.Err == nil\n\tcached, haveCache := lr.mgr.loadFromCache(lr.app.PlatformID())\n\n\tswitch {\n\tcase haveCache && haveInitial:\n\t\t// Which is most recent?\n\t\tif initial.Synced.After(cached.Synced) {\n\t\t\treturn initial, nil\n\t\t} else {\n\t\t\treturn cached, nil\n\t\t}\n\n\tcase haveCache:\n\t\treturn cached, nil\n\n\tcase haveInitial:\n\t\treturn initial, nil\n\n\tdefault:\n\t\t// We have a prefetch error; return it.\n\t\treturn nil, lr.initial.Err\n\t}\n}\n\n// UpdateKey updates the cached secret key to the given value.\nfunc (mgr *Manager) UpdateKey(appSlug, key, value string) {\n\tmgr.mu.Lock()\n\tdefer mgr.mu.Unlock()\n\tif data, ok := mgr.cache[appSlug]; ok {\n\t\tvals := make(map[string]string)\n\t\tfor k, v := range data.Values {\n\t\t\tvals[k] = v\n\t\t}\n\t\tvals[key] = value\n\t\tmgr.cache[appSlug] = &Data{\n\t\t\tSynced: time.Now(),\n\t\t\tValues: vals,\n\t\t}\n\t\tif err := mgr.writeToDisk(appSlug, data); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to write secrets to disk cache\")\n\t\t}\n\t}\n}\n\n// fetch fetches secrets from the server.\n// mu must not be held when running.\nfunc (mgr *Manager) fetch(appSlug string, poll bool) <-chan singleflight.Result {\n\treturn mgr.group.DoChan(appSlug, func() (any, error) {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\t\tdefer cancel()\n\t\tsecrets, err := platform.GetLocalSecretValues(ctx, appSlug, poll)\n\t\tif err != nil {\n\t\t\t// check for access to the app before stating that we failed to fetch secrets\n\t\t\tvar pErr platform.Error\n\t\t\t_, appErr := platform.GetApp(ctx, appSlug)\n\t\t\tif errors.As(appErr, &pErr) && (pErr.HTTPCode == 404 || pErr.HTTPCode == 403) {\n\t\t\t\treturn nil, fmt.Errorf(\"access denied: you do not have access to the app %q\", appSlug)\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"fetch secrets for %s: %v\", appSlug, err)\n\t\t}\n\t\tdata := &Data{\n\t\t\tSynced: time.Now(),\n\t\t\tValues: secrets,\n\t\t}\n\n\t\t// Update our caches\n\t\tmgr.mu.Lock()\n\t\tmgr.cache[appSlug] = data\n\t\tmgr.mu.Unlock()\n\t\tif err := mgr.writeToDisk(appSlug, data); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"failed to write secrets to disk cache\")\n\t\t}\n\n\t\treturn data, nil\n\t})\n}\n\nfunc (mgr *Manager) loadFromCache(appSlug string) (*Data, bool) {\n\t// Do we have the secrets in our cache?\n\tmgr.mu.Lock()\n\tdata, ok := mgr.cache[appSlug]\n\tmgr.mu.Unlock()\n\tif ok {\n\t\treturn data, true\n\t}\n\n\t// Do we have them on disk?\n\tif data, err := mgr.readFromDisk(appSlug); err == nil {\n\t\tmgr.mu.Lock()\n\t\tmgr.cache[appSlug] = data\n\t\tmgr.mu.Unlock()\n\t\treturn data, true\n\t}\n\treturn nil, false\n}\n\n// startPolling begins polling for secret updates every 5 minutes for the apps\n// that have been run.\nfunc (mgr *Manager) startPolling() {\n\tgo func() {\n\t\tfor range time.Tick(5 * time.Minute) {\n\t\t\tvar slugs []string\n\t\t\tmgr.mu.Lock()\n\t\t\tfor s := range mgr.cache {\n\t\t\t\tslugs = append(slugs, s)\n\t\t\t}\n\t\t\tmgr.mu.Unlock()\n\n\t\t\tfor _, s := range slugs {\n\t\t\t\tres := <-mgr.fetch(s, true)\n\t\t\t\tif res.Err != nil {\n\t\t\t\t\tlog.Error().Err(res.Err).Str(\"app_id\", s).Msg(\"failed to sync secrets\")\n\t\t\t\t} else {\n\t\t\t\t\tlog.Info().Str(\"app_id\", s).Msg(\"successfully synced app secrets\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// writeToDisk serializes the secret data and writes it to disk\n// readable only for the current user.\nfunc (mgr *Manager) writeToDisk(appSlug string, data *Data) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"write secrets %s: %v\", appSlug, err)\n\t\t}\n\t}()\n\n\tpath, err := mgr.secretsPath(appSlug)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create all parent dirs and then chmod the secrets dir to be only user-readable\n\tsecretsDir := filepath.Dir(path)\n\tif err := os.MkdirAll(secretsDir, 0755); err != nil {\n\t\treturn err\n\t} else if err := os.Chmod(secretsDir, 0700); err != nil {\n\t\treturn err\n\t}\n\n\tout, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn xos.WriteFile(path, out, 0600)\n}\n\n// readFromDisk reads the cached secrets from disk.\nfunc (mgr *Manager) readFromDisk(appSlug string) (data *Data, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"read secrets %s: %v\", appSlug, err)\n\t\t}\n\t}()\n\n\tpath, err := mgr.secretsPath(appSlug)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdata = new(Data)\n\terr = json.Unmarshal(fdata, data)\n\treturn data, err\n}\n\n// secretsPath returns the file path to where the given app's secrets are stored on disk.\nfunc (mgr *Manager) secretsPath(appSlug string) (string, error) {\n\tdir, err := os.UserCacheDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(dir, \"encore\", \"secrets\", appSlug+\".json\"), nil\n}\n\n// applyLocalOverrides parses the local secrets override file, if any,\n// and returns a new Data object with the overrides applied.\n//\n// If there are no overrides src is returned directly.\n// The original src data object is never modified.\nfunc applyLocalOverrides(app *apps.Instance, src *Data) (*Data, error) {\n\tconst name = \".secrets.local.cue\"\n\tdata, err := os.ReadFile(filepath.Join(app.Root(), name))\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn src, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tupdated := &Data{\n\t\tSynced: src.Synced,\n\t\tValues: make(map[string]string, len(src.Values)),\n\t}\n\tfor k, v := range src.Values {\n\t\tupdated.Values[k] = v\n\t}\n\n\tctx := cuecontext.New()\n\tloadCfg := &load.Config{\n\t\tStdin: bytes.NewReader(data),\n\t}\n\n\tinst := load.Instances([]string{\"-\"}, loadCfg)[0]\n\tif inst.Err != nil {\n\t\treturn nil, fmt.Errorf(\"parse local secrets: %v\", inst.Err)\n\t}\n\tsecrets := ctx.BuildInstance(inst)\n\tif err := secrets.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse local secrets: %v\", err)\n\t}\n\n\tit, err := secrets.Fields(cue.Hidden(false), cue.Concrete(true))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse local secrets: %v\", err)\n\t}\n\tfor it.Next() {\n\t\tkey := it.Selector().String()\n\t\tval, err := it.Value().String()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse local secrets: secret key %s is not a string\", key)\n\t\t}\n\t\tupdated.Values[key] = val\n\t}\n\treturn updated, nil\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/cluster.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/rs/zerolog\"\n\t\"go4.org/syncutil\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"encr.dev/internal/optracker\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\n\t// stdlib registers the \"pgx\" driver to database/sql.\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n)\n\n// Cluster represents a running database Cluster.\ntype Cluster struct {\n\tID       ClusterID // cluster ID\n\tMemfs    bool      // use an in-memory filesystem?\n\tPassword string    // randomly generated password for this cluster\n\n\tdriver Driver\n\tlog    zerolog.Logger\n\n\tstartOnce syncutil.Once\n\t// started is closed when the cluster has been successfully started.\n\tstarted chan struct{}\n\n\t// cachedStatus is the cached cluster status; it should be accessed\n\t// via status().\n\tcachedStatus atomic.Pointer[ClusterStatus]\n\n\tRoles EncoreRoles // set by Start\n\n\t// Ctx is canceled when the cluster is being torn down.\n\tCtx    context.Context\n\tcancel func() // for canceling Ctx\n\n\tmu         sync.Mutex\n\tdbs        map[string]*DB // name -> db\n\tisExternal func(name string) bool\n}\n\nfunc (c *Cluster) Stop() {\n\t// no-op\n}\n\n// Ready returns a channel that is closed when the cluster is up and running.\nfunc (c *Cluster) Ready() <-chan struct{} {\n\treturn c.started\n}\n\n// Start creates the cluster if necessary and starts it.\n// If the cluster is already running it does nothing.\nfunc (c *Cluster) Start(ctx context.Context, tracker *optracker.OpTracker) (*ClusterStatus, error) {\n\tvar status *ClusterStatus\n\terr := c.startOnce.Do(func() (err error) {\n\t\tc.log.Debug().Msg(\"starting cluster\")\n\t\tdefer func() {\n\t\t\tif err == nil {\n\t\t\t\tclose(c.started)\n\t\t\t\tc.log.Debug().Msg(\"successfully started cluster\")\n\t\t\t} else {\n\t\t\t\tc.log.Error().Err(err).Msg(\"failed to start cluster\")\n\t\t\t}\n\t\t}()\n\n\t\tst, err := c.driver.CreateCluster(ctx, &CreateParams{\n\t\t\tClusterID: c.ID,\n\t\t\tMemfs:     c.Memfs,\n\t\t\tTracker:   tracker,\n\t\t}, c.log)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tstatus = st\n\t\tc.cachedStatus.Store(st)\n\t\tgo c.pollStatus()\n\n\t\t// Setup the roles\n\t\tc.Roles, err = c.setupRoles(ctx, st)\n\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t} else if status == nil {\n\t\t// We've already set it up; query the current status\n\t\treturn c.Status(ctx)\n\t}\n\treturn status, nil\n}\n\n// setupRoles ensures the necessary database roles exist\n// for admin/write/read access.\nfunc (c *Cluster) setupRoles(ctx context.Context, st *ClusterStatus) (EncoreRoles, error) {\n\turi := st.ConnURI(st.Config.RootDatabase, st.Config.Superuser)\n\tconn, err := pgx.Connect(ctx, uri)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"connect: %v\", err)\n\t}\n\tdefer conn.Close(context.Background())\n\n\troles, err := c.determineRoles(ctx, st, conn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"determine roles: %v\", err)\n\t}\n\n\tfor _, role := range roles {\n\t\tsanitizedUsername := (pgx.Identifier{role.Username}).Sanitize()\n\t\tc.log.Debug().Str(\"role\", role.Username).Msg(\"creating role\")\n\t\t_, err := conn.Exec(ctx, `\n\t\t\tCREATE USER `+sanitizedUsername+`\n\t\t\tWITH LOGIN ENCRYPTED PASSWORD `+quoteString(role.Password)+`\n\t\t`)\n\t\tif err != nil {\n\t\t\tvar exists bool\n\t\t\terr2 := conn.QueryRow(context.Background(), `\n\t\t\t\tSELECT COALESCE(MAX(oid), 0) > 0 AS exists\n\t\t\t\tFROM pg_roles\n\t\t\t\tWHERE rolname = $1\n\t\t\t`, role.Username).Scan(&exists)\n\t\t\tif err2 != nil {\n\t\t\t\tc.log.Error().Err(err2).Str(\"role\", role.Username).Msg(\"unable to lookup role\")\n\t\t\t\treturn nil, fmt.Errorf(\"get role %q: %v\", role.Username, err2)\n\t\t\t} else if !exists {\n\t\t\t\tc.log.Error().Err(err).Str(\"role\", role.Username).Msg(\"unable to create role\")\n\t\t\t\treturn nil, fmt.Errorf(\"create role %q: %v\", role.Username, err)\n\t\t\t}\n\t\t\tc.log.Debug().Str(\"role\", role.Username).Msg(\"role already exists\")\n\t\t}\n\n\t\t// Add cluster-level permissions.\n\t\tswitch role.Type {\n\t\tcase RoleAdmin:\n\t\t\t// Grant admins the ability to create databases.\n\t\t\t_, err := conn.Exec(ctx, `\n\t\t\t\tALTER USER `+sanitizedUsername+` CREATEDB CREATEROLE\n\t\t\t`)\n\t\t\tif err != nil {\n\t\t\t\tc.log.Error().Err(err).Str(\"role\", role.Username).Msg(\"unable to grant CREATEDB\")\n\t\t\t\treturn nil, fmt.Errorf(\"grant CREATEDB to %q: %v\", role.Username, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn roles, nil\n}\n\n// determineRoles determines the roles to create based on the server version.\nfunc (c *Cluster) determineRoles(ctx context.Context, st *ClusterStatus, conn *pgx.Conn) (EncoreRoles, error) {\n\t// We always support an admin role (PostgreSQL 11+)\n\n\t// We support read/write roles on PostgreSQL 14+ only,\n\t// as support for predefined roles was added then.\n\tvar supportsPredefinedRoles bool\n\t{\n\t\tvar version string\n\t\tif err := conn.QueryRow(ctx, \"SHOW server_version\").Scan(&version); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"determine server version: %v\", err)\n\t\t}\n\t\tc.log.Debug().Str(\"version\", version).Msg(\"got postgres server version\")\n\n\t\tmajor, _, _ := strings.Cut(version, \".\")\n\t\tif n, err := strconv.Atoi(major); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"determine server version: %v\", err)\n\t\t} else if n >= 14 {\n\t\t\tsupportsPredefinedRoles = true\n\t\t}\n\t}\n\n\t// For legacy databases, just use the predefined admin role that we set up before.\n\troles := EncoreRoles{st.Config.Superuser}\n\tif supportsPredefinedRoles {\n\t\t// Otherwise if we support predefined roles, add more roles to use.\n\t\troles = append(roles,\n\t\t\tRole{RoleAdmin, \"encore-admin\", \"admin\"},\n\t\t\tRole{RoleWrite, \"encore-write\", \"write\"},\n\t\t\tRole{RoleRead, \"encore-read\", \"read\"},\n\t\t)\n\t}\n\treturn roles, nil\n}\n\n// initDB initializes the database for svc and adds it to c.dbs.\n// The cluster mutex must be held.\nfunc (c *Cluster) initDB(encoreName string) *DB {\n\tdriverName := encoreName\n\tif !c.driver.Meta().ClusterIsolation {\n\t\tdriverName += fmt.Sprintf(\"-%s-%s\", c.ID.NS.App.PlatformOrLocalID(), c.ID.Type)\n\n\t\t// Add the namespace id, as long as it's not the default namespace\n\t\t// (for backwards compatibility).\n\t\tif c.ID.NS.Name != \"default\" {\n\t\t\tdriverName += \"-\" + string(c.ID.NS.ID)\n\t\t}\n\t}\n\n\tdbCtx, cancel := context.WithCancel(c.Ctx)\n\tdb := &DB{\n\t\tEncoreName: encoreName,\n\t\tCluster:    c,\n\t\tdriverName: driverName,\n\n\t\t// Use a template database when running tests.\n\t\ttemplate: c.ID.Type == Test,\n\n\t\tCtx:    dbCtx,\n\t\tcancel: cancel,\n\n\t\tready: make(chan struct{}),\n\t\tlog:   c.log.With().Str(\"db\", encoreName).Logger(),\n\t}\n\tc.dbs[encoreName] = db\n\treturn db\n}\n\n// Setup sets up the given databases.\nfunc (c *Cluster) Setup(ctx context.Context, appRoot string, md *meta.Data) error {\n\tc.log.Debug().Msg(\"creating cluster\")\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(50)\n\tc.mu.Lock()\n\n\tfor _, dbMeta := range md.SqlDatabases {\n\t\tdbMeta := dbMeta\n\t\tdb, ok := c.dbs[dbMeta.Name]\n\t\tif c.isExternal(dbMeta.Name) {\n\t\t\tcontinue\n\t\t}\n\t\tif !ok {\n\t\t\tdb = c.initDB(dbMeta.Name)\n\t\t}\n\t\tg.Go(func() error { return db.Setup(ctx, appRoot, dbMeta, false, false) })\n\t}\n\tc.mu.Unlock()\n\treturn g.Wait()\n}\n\n// SetupAndMigrate creates and migrates the given databases.\nfunc (c *Cluster) SetupAndMigrate(ctx context.Context, appRoot string, dbs []*meta.SQLDatabase) error {\n\tc.log.Debug().Msg(\"creating and migrating cluster\")\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(50)\n\tc.mu.Lock()\n\tfor _, dbMeta := range dbs {\n\t\tif c.IsExternalDB(dbMeta.Name) {\n\t\t\tcontinue\n\t\t}\n\t\tdbMeta := dbMeta\n\t\tdb, ok := c.dbs[dbMeta.Name]\n\t\tif !ok {\n\t\t\tdb = c.initDB(dbMeta.Name)\n\t\t}\n\t\tg.Go(func() error { return db.Setup(ctx, appRoot, dbMeta, true, false) })\n\t}\n\tc.mu.Unlock()\n\treturn g.Wait()\n}\n\n// GetDB gets the database with the given name.\nfunc (c *Cluster) GetDB(name string) (*DB, bool) {\n\tc.mu.Lock()\n\tdb, ok := c.dbs[name]\n\tc.mu.Unlock()\n\treturn db, ok\n}\n\nfunc (c *Cluster) IsExternalDB(name string) bool {\n\tif c.isExternal == nil {\n\t\treturn false\n\t}\n\treturn c.isExternal(name)\n}\n\n// Recreate recreates the databases for the given database names.\n// If databaseNames is the nil slice it recreates all databases.\nfunc (c *Cluster) Recreate(ctx context.Context, appRoot string, databaseNames []string, md *meta.Data) error {\n\tc.log.Debug().Msg(\"recreating cluster\")\n\tvar filter map[string]bool\n\tif databaseNames != nil {\n\t\tfilter = make(map[string]bool)\n\t\tfor _, name := range databaseNames {\n\t\t\tfilter[name] = true\n\t\t}\n\t}\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(50)\n\tc.mu.Lock()\n\tfor _, dbMeta := range md.SqlDatabases {\n\t\tdbMeta := dbMeta\n\t\tif filter == nil || filter[dbMeta.Name] {\n\t\t\tdb, ok := c.dbs[dbMeta.Name]\n\t\t\tif c.isExternal(dbMeta.Name) {\n\t\t\t\tif filter[dbMeta.Name] {\n\t\t\t\t\tc.mu.Unlock()\n\t\t\t\t\treturn fmt.Errorf(\"cannot reset %q: resetting external databases is disabled\", dbMeta.Name)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !ok {\n\t\t\t\tdb = c.initDB(dbMeta.Name)\n\t\t\t}\n\t\t\tg.Go(func() error { return db.Setup(ctx, appRoot, dbMeta, true, true) })\n\t\t}\n\t}\n\tc.mu.Unlock()\n\terr := g.Wait()\n\tc.log.Debug().Err(err).Msg(\"recreated cluster\")\n\treturn err\n}\n\n// Status reports the cluster's status.\nfunc (c *Cluster) Status(ctx context.Context) (*ClusterStatus, error) {\n\tif st := c.cachedStatus.Load(); st != nil {\n\t\treturn st, nil\n\t}\n\treturn c.updateStatusFromDriver(ctx)\n}\n\nfunc (c *Cluster) updateStatusFromDriver(ctx context.Context) (*ClusterStatus, error) {\n\tst, err := c.driver.ClusterStatus(ctx, c.ID)\n\tif err == nil {\n\t\tc.cachedStatus.Store(st)\n\t}\n\treturn st, err\n}\n\n// pollStatus polls the driver for status changes.\nfunc (c *Cluster) pollStatus() {\n\tch := time.NewTicker(10 * time.Second)\n\tdefer ch.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ch.C:\n\t\t\tctx, cancel := context.WithTimeout(c.Ctx, 5*time.Second)\n\t\t\t_, _ = c.updateStatusFromDriver(ctx)\n\t\t\tcancel()\n\n\t\tcase <-c.Ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Info reports information about a cluster.\nfunc (c *Cluster) Info(ctx context.Context) (*ClusterInfo, error) {\n\tst, err := c.Start(ctx, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinfo := &ClusterInfo{ClusterStatus: st}\n\tinfo.Encore = c.Roles\n\treturn info, nil\n}\n\n// ClusterInfo returns information about a cluster.\ntype ClusterInfo struct {\n\t*ClusterStatus\n\n\t// Encore contains the roles to use to connect for an Encore app.\n\t// It is set if and only if the cluster is running.\n\tEncore EncoreRoles\n}\n\n// ConnURI reports the connection URI to connect to the given database\n// in the cluster, authenticating with the given role.\nfunc (s *ClusterStatus) ConnURI(database string, r Role) string {\n\turi := fmt.Sprintf(\"user=%s password=%s dbname=%s\", r.Username, r.Password, database)\n\n\t// Handle different ways of expressing the host\n\tcfg := s.Config\n\tif strings.HasPrefix(cfg.Host, \"/\") {\n\t\turi += \" host=\" + cfg.Host // unix socket\n\t} else if host, port, err := net.SplitHostPort(cfg.Host); err == nil {\n\t\turi += fmt.Sprintf(\" host=%s port=%s\", host, port) // host:port\n\t} else {\n\t\turi += \" host=\" + cfg.Host // hostname\n\t}\n\n\treturn uri\n}\n\n// EncoreRoles describes the credentials to use when connecting\n// to the cluster as an Encore user.\ntype EncoreRoles []Role\n\nfunc (roles EncoreRoles) Superuser() (Role, bool) { return roles.find(RoleSuperuser) }\nfunc (roles EncoreRoles) Admin() (Role, bool)     { return roles.find(RoleAdmin) }\nfunc (roles EncoreRoles) Write() (Role, bool)     { return roles.find(RoleWrite) }\nfunc (roles EncoreRoles) Read() (Role, bool)      { return roles.find(RoleRead) }\n\nfunc (roles EncoreRoles) First(typs ...RoleType) (Role, bool) {\n\tfor _, typ := range typs {\n\t\tif r, ok := roles.find(typ); ok {\n\t\t\treturn r, true\n\t\t}\n\t}\n\treturn Role{}, false\n}\n\nfunc (roles EncoreRoles) find(typ RoleType) (Role, bool) {\n\tfor _, r := range roles {\n\t\tif r.Type == typ {\n\t\t\treturn r, true\n\t\t}\n\t}\n\treturn Role{}, false\n}\n\ntype RoleType string\n\nfunc (r RoleType) String() string { return string(r) }\n\nconst (\n\tRoleSuperuser RoleType = \"superuser\"\n\tRoleAdmin     RoleType = \"admin\"\n\tRoleWrite     RoleType = \"write\"\n\tRoleRead      RoleType = \"read\"\n)\n\ntype Role struct {\n\tType     RoleType\n\tUsername string\n\tPassword string\n}\n\n// quoteString quotes a string for use in SQL.\nfunc quoteString(str string) string {\n\treturn \"'\" + strings.ReplaceAll(str, \"'\", \"''\") + \"'\"\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/db.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang-migrate/migrate/v4\"\n\t\"github.com/golang-migrate/migrate/v4/database\"\n\t\"github.com/golang-migrate/migrate/v4/database/postgres\"\n\t\"github.com/golang-migrate/migrate/v4/source\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// DB represents a single database instance within a cluster.\ntype DB struct {\n\tEncoreName string\n\tCluster    *Cluster\n\n\tdriverName string\n\n\t// Ctx is canceled when the database is being torn down.\n\tCtx    context.Context\n\tcancel func() // to cancel ctx\n\n\tsetupMu sync.Mutex\n\n\t// ready is closed when the database is migrated and ready.\n\tready   chan struct{}\n\treadied bool\n\n\tmigrated bool\n\n\t// template indicates the database is backed by a template database.\n\ttemplate bool\n\n\tlog zerolog.Logger\n}\n\n// ApplicationCloudName reports the \"cloud name\" of the application-facing database.\nfunc (db *DB) ApplicationCloudName() string {\n\treturn db.driverName\n}\n\n// TemplateCloudName reports the \"cloud name\" of the template database, if any.\nfunc (db *DB) TemplateCloudName() option.Option[string] {\n\tif db.template {\n\t\treturn option.Some(db.driverName + \"_template\")\n\t}\n\treturn option.None[string]()\n}\n\n// Ready returns a channel that is closed when the database is up and running.\nfunc (db *DB) Ready() <-chan struct{} {\n\treturn db.ready\n}\n\n// Setup sets up the database, (re)creating it if necessary and running schema migrations.\nfunc (db *DB) Setup(ctx context.Context, appRoot string, dbMeta *meta.SQLDatabase, migrate, recreate bool) (err error) {\n\tdb.log.Debug().Msg(\"setting up database\")\n\tdb.setupMu.Lock()\n\tdefer db.setupMu.Unlock()\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tif !db.readied {\n\t\t\t\tdb.readied = true\n\t\t\t\tclose(db.ready)\n\t\t\t}\n\t\t\tdb.log.Debug().Msg(\"successfully set up database\")\n\t\t} else {\n\t\t\tdb.log.Error().Err(err).Msg(\"failed to set up database\")\n\t\t}\n\t}()\n\n\tif recreate {\n\t\tif err := db.drop(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tsetupDB := func(cloudName string) error {\n\t\tif err := db.doCreate(ctx, cloudName, option.None[string]()); err != nil {\n\t\t\treturn errors.Wrapf(err, \"create db %s: %v\", cloudName, err)\n\t\t}\n\n\t\tif err := db.ensureRoles(ctx, cloudName, db.Cluster.Roles...); err != nil {\n\t\t\treturn fmt.Errorf(\"ensure db roles %s: %v\", cloudName, err)\n\t\t}\n\n\t\tif migrate || recreate || !db.migrated {\n\t\t\tif err := db.doMigrate(ctx, cloudName, appRoot, dbMeta); err != nil {\n\t\t\t\t// Only report an error if we asked to migrate or recreate.\n\t\t\t\t// Otherwise we might fail to open a database shell when there\n\t\t\t\t// is a migration issue.\n\t\t\t\tif migrate || recreate {\n\t\t\t\t\treturn fmt.Errorf(\"migrate db %s: %v\", cloudName, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// First set up the database with the application name.\n\tif err := setupDB(db.ApplicationCloudName()); err != nil {\n\t\treturn err\n\t}\n\n\tif tmplName, ok := db.TemplateCloudName().Get(); ok {\n\t\t// If we want a template database, rename the application database to the template name.\n\t\t// We do it this way in case the migrations assume the database is named according to the application name.\n\n\t\t// Terminate the connections to the template database to prevent \"database is being accessed by other users\" errors.\n\t\t_ = db.terminateConnectionsToDB(ctx, db.ApplicationCloudName())\n\t\tif err := db.doDrop(ctx, tmplName); err != nil {\n\t\t\treturn fmt.Errorf(\"drop db %s: %v\", tmplName, err)\n\t\t}\n\n\t\tif err := db.renameDB(ctx, db.ApplicationCloudName(), tmplName); err != nil {\n\t\t\treturn fmt.Errorf(\"rename db %s to %s: %v\", db.ApplicationCloudName(), tmplName, err)\n\t\t}\n\n\t\t// Then create the application database based on the template\n\t\tif err := db.doCreate(ctx, db.ApplicationCloudName(), option.Some(tmplName)); err != nil {\n\t\t\treturn errors.Wrapf(err, \"create db %s: %v\", db.ApplicationCloudName(), err)\n\t\t}\n\n\t\t// Ensure the application database has the right roles, too.\n\t\tif err := db.ensureRoles(ctx, db.ApplicationCloudName(), db.Cluster.Roles...); err != nil {\n\t\t\treturn fmt.Errorf(\"ensure db roles %s: %v\", db.ApplicationCloudName(), err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (db *DB) doCreate(ctx context.Context, cloudName string, template option.Option[string]) error {\n\tadm, err := db.connectSuperuser(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = adm.Close(context.Background()) }()\n\n\t// Does it already exist?\n\tvar dummy int\n\terr = adm.QueryRow(ctx, \"SELECT 1 FROM pg_database WHERE datname = $1\", cloudName).Scan(&dummy)\n\towner, ok := db.Cluster.Roles.First(RoleAdmin, RoleSuperuser)\n\tif !ok {\n\t\treturn errors.New(\"unable to find admin or superuser roles\")\n\t}\n\n\tif errors.Is(err, pgx.ErrNoRows) {\n\t\tdb.log.Debug().Msg(\"creating database\")\n\t\t// Sanitize names since this query does not support query params\n\t\tdbName := (pgx.Identifier{cloudName}).Sanitize()\n\t\townerName := (pgx.Identifier{owner.Username}).Sanitize()\n\n\t\t// Use the template if one is provided.\n\t\tvar tmplSnippet string\n\t\tif tmplName, ok := template.Get(); ok {\n\t\t\ttmplSnippet = fmt.Sprintf(\"WITH TEMPLATE %s\", (pgx.Identifier{tmplName}).Sanitize())\n\t\t}\n\t\t_, err = adm.Exec(ctx, fmt.Sprintf(\"CREATE DATABASE %s %s OWNER %s;\", dbName, tmplSnippet, ownerName))\n\t}\n\tif err != nil {\n\t\tdb.log.Error().Err(err).Msg(\"failed to create database\")\n\t}\n\treturn err\n}\n\nfunc (db *DB) renameDB(ctx context.Context, from, to string) error {\n\tadm, err := db.connectSuperuser(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = adm.Close(context.Background()) }()\n\n\t_, err = adm.Exec(ctx, fmt.Sprintf(\"ALTER DATABASE %s RENAME TO %s\",\n\t\t(pgx.Identifier{from}).Sanitize(),\n\t\t(pgx.Identifier{to}).Sanitize(),\n\t))\n\treturn err\n}\n\n// ensureRoles ensures the roles have been granted access to this database.\nfunc (db *DB) ensureRoles(ctx context.Context, cloudName string, roles ...Role) error {\n\tadm, err := db.connectSuperuser(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = adm.Close(context.Background()) }()\n\n\tdb.log.Debug().Msg(\"revoking public access\")\n\tsafeDBName := (pgx.Identifier{cloudName}).Sanitize()\n\t_, err = adm.Exec(ctx, \"REVOKE ALL ON DATABASE \"+safeDBName+\" FROM public\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"revoke public: %v\", err)\n\t}\n\n\tfor _, role := range roles {\n\t\tvar stmt string\n\t\tsafeRoleName := (pgx.Identifier{role.Username}).Sanitize()\n\t\tswitch role.Type {\n\t\tcase RoleSuperuser:\n\t\t\t// Already granted; nothing to do\n\t\t\tcontinue\n\t\tcase RoleAdmin:\n\t\t\tstmt = fmt.Sprintf(\"GRANT ALL ON DATABASE %s TO %s;\", safeDBName, safeRoleName)\n\t\tcase RoleWrite:\n\t\t\tstmt = fmt.Sprintf(`\n\t\t\t\tGRANT TEMP, CONNECT ON DATABASE %s TO %s;\n\t\t\t\tGRANT pg_read_all_data TO %s;\n\t\t\t\tGRANT pg_write_all_data TO %s;\n\t\t\t`, safeDBName, safeRoleName, safeRoleName, safeRoleName)\n\t\tcase RoleRead:\n\t\t\tstmt = fmt.Sprintf(`\n\t\t\t\tGRANT TEMP, CONNECT ON DATABASE %s TO %s;\n\t\t\t\tGRANT pg_read_all_data TO %s;\n\t\t\t`, safeDBName, safeRoleName, safeRoleName)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unknown role type %q\", role.Type)\n\t\t}\n\n\t\tdb.log.Debug().Str(\"role\", role.Username).Str(\"db\", cloudName).Msg(\"granting access to role\")\n\n\t\t// We've observed race conditions in Postgres to grant access. Retry a few times.\n\t\t{\n\t\t\tvar err error\n\t\t\tfor i := 0; i < 5; i++ {\n\t\t\t\t_, err = adm.Exec(ctx, stmt)\n\t\t\t\tif err == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdb.log.Debug().Str(\"role\", role.Username).Str(\"db\", cloudName).Err(err).Msg(\"error granting role, retrying\")\n\t\t\t\ttime.Sleep(250 * time.Millisecond)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"grant %s role %s: %v\", role.Type, role.Username, err)\n\t\t\t}\n\t\t}\n\n\t\tdb.log.Debug().Str(\"role\", role.Username).Str(\"db\", cloudName).Msg(\"successfully granted access\")\n\t}\n\treturn nil\n}\n\n// Migrate migrates the database.\nfunc (db *DB) doMigrate(ctx context.Context, cloudName, appRoot string, dbMeta *meta.SQLDatabase) (err error) {\n\tif db.Cluster.ID.Type == Shadow {\n\t\tdb.log.Debug().Msg(\"not applying migrations to shadow cluster\")\n\t\treturn nil\n\t}\n\tif len(dbMeta.Migrations) == 0 || dbMeta.MigrationRelPath == nil {\n\t\tdb.log.Debug().Msg(\"no database migrations to run, skipping\")\n\t\treturn nil\n\t}\n\n\tdb.log.Debug().Msg(\"running database migrations\")\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tdb.log.Error().Err(err).Msg(\"migrations failed\")\n\t\t} else {\n\t\t\tdb.migrated = true\n\t\t\tdb.log.Debug().Msg(\"migrations completed successfully\")\n\t\t}\n\t}()\n\n\tinfo, err := db.Cluster.Info(ctx)\n\tif err != nil {\n\t\treturn err\n\t} else if info.Status != Running {\n\t\treturn errors.New(\"cluster not running\")\n\t}\n\n\tadmin, ok := info.Encore.First(RoleAdmin, RoleSuperuser)\n\tif !ok {\n\t\treturn errors.New(\"unable to find superuser or admin roles\")\n\t}\n\turi := info.ConnURI(cloudName, admin)\n\tdb.log.Debug().Str(\"uri\", uri).Msg(\"running migrations\")\n\tpool, err := sql.Open(\"pgx\", uri)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fns.CloseIgnore(pool)\n\n\tpath := filepath.Join(appRoot, *dbMeta.MigrationRelPath)\n\tmdSrc := NewMetadataSource(NewOsMigrationReader(path), dbMeta.Migrations)\n\tconn, err := pool.Conn(ctx)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to connect to postgres\")\n\t}\n\terr = RunMigration(ctx, cloudName, dbMeta.AllowNonSequentialMigrations, conn, mdSrc)\n\n\t// If we have removed a migration that failed to apply we can get an ErrNoChange error\n\t// after forcing the migration down to the previous version.\n\tif errors.Is(err, migrate.ErrNoChange) {\n\t\tdb.log.Info().Msg(\"database already up to date\")\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"could not migrate database %s: %v\", cloudName, err)\n\t}\n\tdb.log.Info().Msg(\"migration completed\")\n\treturn nil\n}\n\nfunc (db *DB) ListAppliedMigrations(ctx context.Context) (map[uint64]bool, error) {\n\tconn, err := db.connectToDB(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer fns.CloseIgnore(conn)\n\treturn LoadAppliedVersions(ctx, conn, \"public\", \"schema_migrations\")\n}\n\nfunc RunMigration(ctx context.Context, dbName string, allowNonSeq bool, conn *sql.Conn, mdSrc *MetadataSource) (err error) {\n\tvar (\n\t\tdbDriver  database.Driver\n\t\tsrcDriver source.Driver\n\t)\n\tif allowNonSeq {\n\t\tdbDriver, srcDriver, err = NonSequentialMigrator(ctx, conn, mdSrc)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to connect to postgres\")\n\t\t}\n\t} else {\n\t\tdbDriver, err = postgres.WithConnection(ctx, conn, &postgres.Config{})\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to connect to postgres\")\n\t\t}\n\t\tsrcDriver = mdSrc\n\t}\n\n\tcurVersion, _, err := dbDriver.Version()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to get current version\")\n\t} else if curVersion < -1 {\n\t\treturn errors.Newf(\"invalid current version (%d) for db %s\", curVersion, dbName)\n\t}\n\n\tm, err := migrate.NewWithInstance(\"src\", srcDriver, \"postgres\", dbDriver)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to create migration instance\")\n\t}\n\n\terr = m.Up()\n\tif errors.Is(err, migrate.ErrNoChange) {\n\t\treturn err\n\t}\n\n\t// If we have a dirty migration, reset the dirty flag and try again.\n\t// This is safe since all migrations run inside transactions.\n\tvar dirty migrate.ErrDirty\n\tif errors.As(err, &dirty) {\n\t\t// Find the version that preceded the dirty version so\n\t\t// we can force the migration to that version and then\n\t\t// re-apply the migration.\n\t\tvar prevVer uint\n\t\tprevVer, err = srcDriver.Prev(uint(dirty.Version))\n\t\ttargetVer := int(prevVer)\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// If Prev returns ErrNotExist, the original migration might\n\t\t\t// have been deleted. In this case, we'll need to search for\n\t\t\t// the version that is the closest lower version starting at the\n\t\t\t// first version.\n\t\t\ttargetVer, err = findClosestLowerVersion(srcDriver.First, dirty.Version, srcDriver.Next)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"could not automatically reset the schema_migrations \"+\n\t\t\t\t\t\"dirty flag for database %s. Please reset it manually by connecting \"+\n\t\t\t\t\t\"to the database modify the schema_migrations table\", dbName)\n\t\t\t}\n\t\t} else if err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to find previous version\")\n\t\t}\n\n\t\tif err = m.Force(targetVer); err == nil {\n\t\t\terr = m.Up()\n\t\t}\n\t}\n\treturn errors.Wrap(err, \"failed to migrate database\")\n}\n\nfunc findClosestLowerVersion(first func() (uint, error), dirtyVer int, next func(i uint) (uint, error)) (int, error) {\n\tfirstVer, err := first()\n\t// If the first version doesn't exist, we can't reset the dirty flag\n\t// and we'll need to return an error.\n\tif err != nil {\n\t\treturn 0, errors.Wrapf(err, \"failed to find first version\")\n\t}\n\t// otherwise we'll need to find the version that is the closest lower version\n\trtn := database.NilVersion\n\tfor nextVer := firstVer; err == nil && nextVer < uint(dirtyVer); nextVer, err = next(nextVer) {\n\t\trtn = int(nextVer)\n\t}\n\treturn rtn, nil\n}\n\nfunc (db *DB) drop(ctx context.Context) error {\n\tif err := db.doDrop(ctx, db.ApplicationCloudName()); err != nil {\n\t\treturn errors.Wrapf(err, \"drop database %s\", db.ApplicationCloudName())\n\t}\n\tif name, ok := db.TemplateCloudName().Get(); ok {\n\t\tif err := db.doDrop(ctx, name); err != nil {\n\t\t\treturn errors.Wrapf(err, \"drop database %s\", name)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (db *DB) terminateConnectionsToDB(ctx context.Context, cloudName string) error {\n\tadm, err := db.connectSuperuser(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = adm.Close(context.Background()) }()\n\n\t// Drop all connections to prevent \"database is being accessed by other users\" errors.\n\t_, _ = adm.Exec(ctx, \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1\", cloudName)\n\treturn nil\n}\n\nfunc (db *DB) doDrop(ctx context.Context, cloudName string) error {\n\tadm, err := db.connectSuperuser(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = adm.Close(context.Background()) }()\n\n\tvar dummy int\n\terr = adm.QueryRow(ctx, \"SELECT 1 FROM pg_database WHERE datname = $1\", cloudName).Scan(&dummy)\n\tif err == nil {\n\t\t// Drop all connections to prevent \"database is being accessed by other users\" errors.\n\t\t_, _ = adm.Exec(ctx, \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1\", cloudName)\n\n\t\tname := (pgx.Identifier{cloudName}).Sanitize() // sanitize database name, to be safe\n\t\t_, err = adm.Exec(ctx, fmt.Sprintf(\"DROP DATABASE %s;\", name))\n\t\tdb.log.Debug().Err(err).Msgf(\"dropped database\")\n\t} else if errors.Is(err, pgx.ErrNoRows) {\n\t\treturn nil\n\t}\n\n\tif err != nil {\n\t\tdb.log.Debug().Err(err).Msgf(\"failed to drop database\")\n\t}\n\treturn err\n}\n\n// CloseConns closes all connections to this database through the dbproxy,\n// and prevents future ones from being established.\nfunc (db *DB) CloseConns() {\n\tdb.cancel()\n}\n\n// connectSuperuser creates a superuser connection to the root database for the cluster.\n// On success the returned conn must be closed by the caller.\nfunc (db *DB) connectSuperuser(ctx context.Context) (*pgx.Conn, error) {\n\t// Wait for the cluster to be setup\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tcase <-db.Cluster.started:\n\t}\n\n\tinfo, err := db.Cluster.Info(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if info.Status != Running {\n\t\treturn nil, fmt.Errorf(\"cluster not running\")\n\t}\n\n\turi := info.ConnURI(info.Config.RootDatabase, info.Config.Superuser)\n\n\t// Wait for the connection to be established; this might take a little bit\n\t// when we're racing with spinning up a Docker container.\n\tfor i := 0; i < 40; i++ {\n\t\tvar conn *pgx.Conn\n\t\tconn, err = pgx.Connect(ctx, uri)\n\t\tif err == nil {\n\t\t\treturn conn, nil\n\t\t} else if ctx.Err() != nil {\n\t\t\t// We'll never succeed once the context has been canceled.\n\t\t\t// Give up straight away.\n\t\t\tdb.log.Debug().Err(err).Msgf(\"failed to connect to superuser db\")\n\t\t\treturn nil, err\n\t\t}\n\t\ttime.Sleep(250 * time.Millisecond)\n\t}\n\tdb.log.Debug().Err(err).Msgf(\"failed to connect to admin db\")\n\treturn nil, fmt.Errorf(\"failed to connect to superuser database: %v\", err)\n}\n\n// Connects as a superuser or admin to the database. Fails fast if the cluster\n// is not running yet.\n// On success the returned conn must be closed by the caller.\nfunc (db *DB) connectToDB(ctx context.Context) (*sql.Conn, error) {\n\tinfo, err := db.Cluster.Info(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\turi := info.ConnURI(db.EncoreName, info.Config.Superuser)\n\tpool, err := sql.Open(\"pgx\", uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer fns.CloseIgnore(pool)\n\n\tconn, err := pool.Conn(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn conn, nil\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/db_test.go",
    "content": "package sqldb\n\nimport (\n\t\"io/fs\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t_ \"github.com/golang-migrate/migrate/v4/source/file\" // for running migrations from the filesystem\n)\n\nfunc TestFindClosestVersion(t *testing.T) {\n\tc := qt.New(t)\n\ttestCases := map[string]struct {\n\t\tversions    []uint\n\t\tdirty       int\n\t\texpected    int\n\t\texpectedErr bool\n\t}{\n\t\t\"first\": {\n\t\t\tversions: []uint{1, 2, 3},\n\t\t\tdirty:    1,\n\t\t\texpected: -1,\n\t\t},\n\t\t\"middle\": {\n\t\t\tversions: []uint{1, 2, 3},\n\t\t\tdirty:    2,\n\t\t\texpected: 1,\n\t\t},\n\t\t\"last\": {\n\t\t\tversions: []uint{1, 2, 3},\n\t\t\tdirty:    3,\n\t\t\texpected: 2,\n\t\t},\n\t\t\"deleted\": {\n\t\t\tversions: []uint{1, 2, 4},\n\t\t\tdirty:    3,\n\t\t\texpected: 2,\n\t\t},\n\t\t\"deleted_first\": {\n\t\t\tversions: []uint{2, 3, 4},\n\t\t\tdirty:    1,\n\t\t\texpected: -1,\n\t\t},\n\t\t\"empty\": {\n\t\t\tdirty:       5,\n\t\t\texpectedErr: true,\n\t\t},\n\t}\n\n\tfor name, tc := range testCases {\n\t\tc.Run(name, func(c *qt.C) {\n\t\t\tresult, err := findClosestLowerVersion(func() (uint, error) {\n\t\t\t\tif len(tc.versions) == 0 {\n\t\t\t\t\treturn 0, fs.ErrNotExist\n\t\t\t\t}\n\t\t\t\treturn tc.versions[0], nil\n\t\t\t}, tc.dirty, func(version uint) (uint, error) {\n\t\t\t\tfor _, v := range tc.versions {\n\t\t\t\t\tif v > version {\n\t\t\t\t\t\treturn v, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn 0, fs.ErrNotExist\n\t\t\t})\n\t\t\tif tc.expectedErr {\n\t\t\t\tc.Assert(err, qt.IsNotNil)\n\t\t\t} else {\n\t\t\t\tc.Assert(err, qt.IsNil)\n\t\t\t\tc.Assert(result, qt.Equals, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/docker/docker.go",
    "content": "package docker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/sqldb\"\n\t\"encr.dev/pkg/idents\"\n)\n\ntype Driver struct{}\n\nvar _ sqldb.Driver = (*Driver)(nil)\n\nconst (\n\tDefaultSuperuserUsername = \"postgres\"\n\tDefaultSuperuserPassword = \"postgres\"\n\tDefaultRootDatabase      = \"postgres\"\n\tdefaultDataDir           = \"/var/lib/postgresql/data\"\n)\n\nfunc (d *Driver) CreateCluster(ctx context.Context, p *sqldb.CreateParams, log zerolog.Logger) (status *sqldb.ClusterStatus, err error) {\n\t// Ensure the docker image exists first.\n\t{\n\t\tcheckExistsCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\t\tdefer cancel()\n\t\tif ok, err := ImageExists(checkExistsCtx); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"check docker image\")\n\t\t} else if !ok {\n\t\t\tlog.Debug().Msg(\"PostgreSQL image does not exist, pulling\")\n\t\t\tpullOp := p.Tracker.Add(\"Pulling PostgreSQL docker image\", time.Now())\n\t\t\tif err := PullImage(context.Background()); err != nil {\n\t\t\t\tlog.Error().Err(err).Msg(\"failed to pull PostgreSQL image\")\n\t\t\t\tp.Tracker.Fail(pullOp, err)\n\t\t\t\treturn nil, errors.Wrap(err, \"pull docker image\")\n\t\t\t} else {\n\t\t\t\tp.Tracker.Done(pullOp, 0)\n\t\t\t\tlog.Info().Msg(\"successfully pulled sqldb image\")\n\t\t\t}\n\t\t}\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\t// If we return with a connection, wait until we can connect.\n\tdefer func() {\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\t// Wait for the database to come up; this might take a little bit\n\t\t// when we're racing with spinning up a Docker container.\n\t\turi := status.ConnURI(status.Config.RootDatabase, status.Config.Superuser)\n\n\t\tconst sleepTime = 250 * time.Millisecond\n\t\tconst maxLoops = (30 * time.Second) / sleepTime\n\t\tfor i := 0; i < int(maxLoops); i++ {\n\t\t\tvar conn *pgx.Conn\n\t\t\tconnCtx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\t\t\tconn, err = pgx.Connect(connCtx, uri)\n\t\t\tcancel()\n\n\t\t\tif err == nil {\n\t\t\t\t_ = conn.Close(ctx)\n\t\t\t\treturn\n\t\t\t} else if ctx.Err() != nil {\n\t\t\t\t// We'll never succeed once the context has been canceled.\n\t\t\t\t// Give up straight away.\n\t\t\t\tlog.Debug().Err(err).Msgf(\"failed to connect to db\")\n\t\t\t\terr = errors.Wrap(err, \"database did not come up\")\n\t\t\t} else if errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\t// This is a transient error that can happen when the database first initialises\n\t\t\t\terr = errors.Wrap(err, \"database is not ready yet\")\n\t\t\t} else {\n\t\t\t\terr = errors.WithStack(err)\n\t\t\t}\n\t\t\ttime.Sleep(250 * time.Millisecond)\n\t\t}\n\t}()\n\n\tcid := p.ClusterID\n\tcnames := containerNames(cid)\n\tstatus, existingContainerName, err := d.clusterStatus(ctx, cid)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to get container status\")\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\t// waitForPort waits for the port to become available before returning.\n\twaitForPort := func() (*sqldb.ClusterStatus, error) {\n\t\tfor i := 0; i < 20; i++ {\n\t\t\tstatus, err = d.ClusterStatus(ctx, cid)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"unable to wait for port\")\n\t\t\t}\n\t\t\tif status.Config.Host != \"\" {\n\t\t\t\tlog.Debug().Str(\"hostport\", status.Config.Host).Msg(\"cluster started\")\n\t\t\t\treturn status, nil\n\t\t\t}\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t}\n\t\treturn nil, errors.New(\"timed out waiting for cluster to start\")\n\t}\n\n\tswitch status.Status {\n\tcase sqldb.Running:\n\t\tlog.Debug().Str(\"hostport\", status.Config.Host).Msg(\"cluster already running\")\n\t\treturn status, nil\n\n\tcase sqldb.Stopped:\n\t\tlog.Debug().Msg(\"cluster stopped, restarting\")\n\n\t\tif out, err := exec.CommandContext(ctx, \"docker\", \"start\", existingContainerName).CombinedOutput(); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"could not start sqldb container: %s\", string(out))\n\t\t}\n\t\treturn waitForPort()\n\n\tcase sqldb.NotFound:\n\t\tlog.Debug().Msg(\"cluster not found, creating\")\n\t\targs := []string{\n\t\t\t\"run\",\n\t\t\t\"-d\",\n\t\t\t\"-p\", \"5432\",\n\t\t\t\"--shm-size=1gb\",\n\t\t\t\"-e\", \"POSTGRES_USER=\" + DefaultSuperuserUsername,\n\t\t\t\"-e\", \"POSTGRES_PASSWORD=\" + DefaultSuperuserPassword,\n\t\t\t\"-e\", \"POSTGRES_DB=\" + DefaultRootDatabase,\n\t\t\t\"-e\", \"PGDATA=\" + defaultDataDir,\n\t\t\t\"--name\", cnames[0],\n\t\t}\n\t\tif p.Memfs {\n\t\t\targs = append(args,\n\t\t\t\t\"--mount\", \"type=tmpfs,destination=\"+defaultDataDir,\n\t\t\t\tImage,\n\t\t\t\t\"-c\", \"fsync=off\",\n\t\t\t)\n\t\t} else {\n\t\t\tvolumeName := clusterVolumeNames(p.ClusterID.NS)[0] // guaranteed to be non-empty\n\t\t\tif err := d.createVolumeIfNeeded(ctx, volumeName); err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"create data volume\")\n\t\t\t}\n\t\t\targs = append(args,\n\t\t\t\t\"-v\", fmt.Sprintf(\"%s:%s\", volumeName, defaultDataDir),\n\t\t\t\tImage)\n\t\t}\n\n\t\tcmd := exec.CommandContext(ctx, \"docker\", args...)\n\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"could not start sql database as docker container: %s\", out)\n\t\t}\n\n\t\tlog.Debug().Msg(\"cluster created\")\n\t\treturn waitForPort()\n\n\tdefault:\n\t\treturn nil, errors.Newf(\"unknown cluster status %q\", status.Status)\n\t}\n}\n\nfunc (d *Driver) ClusterStatus(ctx context.Context, id sqldb.ClusterID) (*sqldb.ClusterStatus, error) {\n\tstatus, _, err := d.clusterStatus(ctx, id)\n\treturn status, errors.WithStack(err)\n}\n\nfunc (d *Driver) CheckRequirements(ctx context.Context) error {\n\tif _, err := exec.LookPath(\"docker\"); err != nil {\n\t\treturn errors.New(\"This application requires docker to run since it uses an SQL database. Install docker first.\")\n\t} else if !isDockerRunning(ctx) {\n\t\treturn errors.New(\"The docker daemon is not running. Start it first.\")\n\t}\n\treturn nil\n}\n\n// clusterStatus reports both the standard ClusterStatus but also the container name we actually resolved to.\nfunc (d *Driver) clusterStatus(ctx context.Context, id sqldb.ClusterID) (status *sqldb.ClusterStatus, containerName string, err error) {\n\tvar output []byte\n\n\t// Try the candidate container names in order.\n\tcnames := containerNames(id)\n\tfor _, cname := range cnames {\n\t\tvar err error\n\t\tout, err := exec.CommandContext(ctx, \"docker\", \"container\", \"inspect\", cname).CombinedOutput()\n\t\tif errors.Is(err, exec.ErrNotFound) {\n\t\t\treturn nil, \"\", errors.New(\"docker not found: is it installed and in your PATH?\")\n\t\t} else if err != nil {\n\t\t\t// Docker returns a non-zero exit code if the container does not exist.\n\t\t\t// Try to tell this apart from an error by parsing the output.\n\t\t\tif bytes.Contains(out, []byte(\"No such container\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Podman has slightly different output when a container is not found.\n\t\t\tif bytes.Contains(out, []byte(\"no such container\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, \"\", errors.Wrapf(err, \"docker container inspect failed: %s\", out)\n\t\t} else {\n\t\t\t// Found our container; use it.\n\t\t\toutput, containerName = out, cname\n\t\t\tbreak\n\t\t}\n\t}\n\tif output == nil {\n\t\treturn &sqldb.ClusterStatus{Status: sqldb.NotFound}, containerName, nil\n\t}\n\n\tvar resp []struct {\n\t\tName  string\n\t\tState struct {\n\t\t\tRunning bool\n\t\t}\n\t\tConfig struct {\n\t\t\tEnv []string\n\t\t}\n\t\tNetworkSettings struct {\n\t\t\tPorts map[string][]struct {\n\t\t\t\tHostIP   string\n\t\t\t\tHostPort string\n\t\t\t}\n\t\t}\n\t}\n\tif err := json.Unmarshal(output, &resp); err != nil {\n\t\treturn nil, \"\", errors.Wrap(err, \"parse `docker container inspect` response\")\n\t}\n\tfor _, c := range resp {\n\t\t// Docker prefixes `/` to the container name, Podman doesn't.\n\t\tif c.Name == \"/\"+containerName || c.Name == containerName {\n\t\t\tstatus := &sqldb.ClusterStatus{Status: sqldb.Stopped, Config: &sqldb.ConnConfig{\n\t\t\t\t// Defaults if we don't find anything else configured.\n\t\t\t\tSuperuser: sqldb.Role{\n\t\t\t\t\tType:     sqldb.RoleSuperuser,\n\t\t\t\t\tUsername: DefaultSuperuserUsername,\n\t\t\t\t\tPassword: DefaultSuperuserPassword,\n\t\t\t\t},\n\t\t\t\tRootDatabase: DefaultRootDatabase,\n\t\t\t}}\n\t\t\tif c.State.Running {\n\t\t\t\tstatus.Status = sqldb.Running\n\t\t\t}\n\t\t\tports := c.NetworkSettings.Ports[\"5432/tcp\"]\n\t\t\tif len(ports) > 0 {\n\t\t\t\thostIP := ports[0].HostIP\n\n\t\t\t\t// Podman can keep HostIP empty or 0.0.0.0.\n\t\t\t\t// https://github.com/containers/podman/issues/17780\n\t\t\t\tif hostIP == \"\" || hostIP == \"0.0.0.0\" {\n\t\t\t\t\thostIP = \"127.0.0.1\"\n\t\t\t\t}\n\n\t\t\t\tstatus.Config.Host = hostIP + \":\" + ports[0].HostPort\n\t\t\t}\n\n\t\t\t// Read the Postgres config from the docker container's environment.\n\t\t\tfor _, env := range c.Config.Env {\n\t\t\t\tif name, value, ok := strings.Cut(env, \"=\"); ok {\n\t\t\t\t\tswitch name {\n\t\t\t\t\tcase \"POSTGRES_USER\":\n\t\t\t\t\t\tstatus.Config.Superuser.Username = value\n\t\t\t\t\tcase \"POSTGRES_PASSWORD\":\n\t\t\t\t\t\tstatus.Config.Superuser.Password = value\n\t\t\t\t\tcase \"POSTGRES_DB\":\n\t\t\t\t\t\tstatus.Config.RootDatabase = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn status, containerName, nil\n\t\t}\n\t}\n\treturn &sqldb.ClusterStatus{Status: sqldb.NotFound}, containerName, nil\n}\n\nfunc (d *Driver) CanDestroyCluster(ctx context.Context, id sqldb.ClusterID) error {\n\t// Check that we can communicate with Docker.\n\tif !isDockerRunning(ctx) {\n\t\treturn errors.New(\"cannot delete sql database: docker is not running\")\n\t}\n\treturn nil\n}\n\nfunc (d *Driver) DestroyCluster(ctx context.Context, id sqldb.ClusterID) error {\n\tcnames := containerNames(id)\n\tfor _, cname := range cnames {\n\t\tout, err := exec.CommandContext(ctx, \"docker\", \"rm\", \"-f\", cname).CombinedOutput()\n\t\tif err != nil {\n\t\t\tif bytes.Contains(out, []byte(\"No such container\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn errors.Wrapf(err, \"could not delete cluster: %s\", out)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Driver) DestroyNamespaceData(ctx context.Context, ns *namespace.Namespace) error {\n\tcandidates := clusterVolumeNames(ns)\n\tfor _, c := range candidates {\n\t\tif out, err := exec.CommandContext(ctx, \"docker\", \"volume\", \"rm\", \"-f\", c).CombinedOutput(); err != nil {\n\t\t\tif strings.Contains(strings.ToLower(err.Error()), \"no such volume\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn errors.Wrapf(err, \"could not delete volume %s: %s\", c, string(out))\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *Driver) createVolumeIfNeeded(ctx context.Context, name string) error {\n\tif err := exec.CommandContext(ctx, \"docker\", \"volume\", \"inspect\", name).Run(); err == nil {\n\t\treturn nil\n\t}\n\tout, err := exec.CommandContext(ctx, \"docker\", \"volume\", \"create\", name).CombinedOutput()\n\treturn errors.Wrapf(err, \"create volume %s: %s\", name, out)\n}\n\nfunc (d *Driver) Meta() sqldb.DriverMeta {\n\treturn sqldb.DriverMeta{ClusterIsolation: true}\n}\n\n// containerNames computes the container name candidates for a given clusterID.\nfunc containerNames(id sqldb.ClusterID) []string {\n\t// candidates returns possible candidate names for a given app id.\n\tcandidates := func(appID string) (names []string) {\n\t\tbase := \"sqldb-\" + appID\n\n\t\tif id.Type != sqldb.Run {\n\t\t\tbase += \"-\" + string(id.Type)\n\t\t}\n\n\t\t// Convert the namespace to kebab case to remove invalid characters like ':'.\n\t\tnsName := idents.Convert(string(id.NS.Name), idents.KebabCase)\n\n\t\tnames = []string{base + \"-\" + nsName + \"-\" + string(id.NS.ID)}\n\t\t// If this is the default namespace look up the container without\n\t\t// the namespace suffix as well, for backwards compatibility.\n\t\tif id.NS.Name == \"default\" {\n\t\t\tnames = append(names, base)\n\t\t}\n\t\treturn names\n\t}\n\n\tvar names []string\n\tif pid := id.NS.App.PlatformID(); pid != \"\" {\n\t\tnames = append(names, candidates(pid)...)\n\t}\n\tnames = append(names, candidates(id.NS.App.LocalID())...)\n\treturn names\n}\n\n// ImageExists reports whether the docker image exists.\nfunc ImageExists(ctx context.Context) (ok bool, err error) {\n\tout, err := exec.CommandContext(ctx, \"docker\", \"image\", \"inspect\", Image).CombinedOutput()\n\tswitch {\n\tcase err == nil:\n\t\treturn true, nil\n\tcase bytes.Contains(out, []byte(\"No such image\")):\n\t\treturn false, nil\n\t// Podman has a different error message.\n\tcase bytes.Contains(out, []byte(\"failed to find image\")):\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, errors.WithStack(errors.Wrapf(err, \"docker image inspect failed: %s\", Image))\n\t}\n}\n\n// PullImage pulls the image.\nfunc PullImage(ctx context.Context) error {\n\tcmd := exec.CommandContext(ctx, \"docker\", \"pull\", Image)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n\nconst Image = \"encoredotdev/postgres:15\"\n\nfunc isDockerRunning(ctx context.Context) bool {\n\terr := exec.CommandContext(ctx, \"docker\", \"info\").Run()\n\treturn err == nil\n}\n\n// clusterVolumeNames reports the candidate names for the docker volume.\nfunc clusterVolumeNames(ns *namespace.Namespace) (candidates []string) {\n\tnsName := idents.Convert(string(ns.Name), idents.KebabCase)\n\tsuffix := fmt.Sprintf(\"%s-%s\", ns.ID, nsName)\n\n\tfor _, id := range [...]string{ns.App.PlatformID(), ns.App.LocalID()} {\n\t\tif id != \"\" {\n\t\t\tcandidates = append(candidates, fmt.Sprintf(\"sqldb-%s-%s\", id, suffix))\n\t\t}\n\t}\n\treturn candidates\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/driver.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/internal/optracker\"\n)\n\nvar ErrUnsupported = errors.New(\"unsupported operation\")\n\n// A Driver abstracts away how a cluster is actually operated.\ntype Driver interface {\n\t// CreateCluster creates (if necessary) and starts (if necessary) a new cluster using the driver,\n\t// and returns its status.\n\t// err is nil if and only if the cluster could not be started.\n\tCreateCluster(ctx context.Context, p *CreateParams, log zerolog.Logger) (*ClusterStatus, error)\n\n\t// CanDestroyCluster reports whether the cluster could be destroyed, if desired.\n\t// If a Driver doesn't support destroying the cluster it reports ErrUnsupported.\n\tCanDestroyCluster(ctx context.Context, id ClusterID) error\n\n\t// DestroyCluster destroys a cluster with the given id.\n\t// If a Driver doesn't support destroying the cluster it reports ErrUnsupported.\n\tDestroyCluster(ctx context.Context, id ClusterID) error\n\n\t// DestroyNamespaceData destroys the data associated with a namespace.\n\t// If a Driver doesn't support destroying data it reports ErrUnsupported.\n\tDestroyNamespaceData(ctx context.Context, ns *namespace.Namespace) error\n\n\t// ClusterStatus reports the current status of a cluster.\n\tClusterStatus(ctx context.Context, id ClusterID) (*ClusterStatus, error)\n\n\t// CheckRequirements checks whether all the requirements are met\n\t// to use the driver.\n\tCheckRequirements(ctx context.Context) error\n\n\t// Meta reports driver metadata.\n\tMeta() DriverMeta\n}\n\ntype DriverMeta struct {\n\t// ClusterIsolation reports whether clusters are isolated by the driver.\n\t// If false, database names will be prefixed with the cluster id.\n\tClusterIsolation bool\n}\n\ntype ConnConfig struct {\n\t// Host is the host address to connect to the database.\n\t// It is only set when Status == Running.\n\tHost string\n\n\t// Superuser is the role to use to connect as the superuser,\n\t// for creating and managing Encore databases.\n\tSuperuser    Role\n\tRootDatabase string // root database to connect to\n}\n\ntype ClusterType string\n\nconst (\n\tRun    ClusterType = \"run\"\n\tShadow ClusterType = \"shadow\"\n\tTest   ClusterType = \"test\"\n)\n\nfunc (ct ClusterType) Memfs() bool {\n\tswitch ct {\n\tcase Run:\n\t\treturn false\n\tcase Shadow, Test:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// CreateParams are the params to (*ClusterManager).Create.\ntype CreateParams struct {\n\tClusterID ClusterID\n\n\t// Memfs, if true, configures the database container to use an\n\t// in-memory filesystem as opposed to persisting the database to disk.\n\tMemfs bool\n\n\t// Tracker allows tracking the progress of the operation.\n\tTracker *optracker.OpTracker\n}\n\n// Status represents the status of a container.\ntype Status string\n\nconst (\n\t// Running indicates the cluster is running.\n\tRunning Status = \"running\"\n\t// Stopped indicates the container exists but is not running.\n\tStopped Status = \"stopped\"\n\t// NotFound indicates the container does not exist.\n\tNotFound Status = \"notfound\"\n)\n\n// ClusterStatus represents the status of a database cluster.\ntype ClusterStatus struct {\n\t// Status is the status of the underlying container.\n\tStatus Status\n\n\t// Config is how to connect to the cluster.\n\t// It is non-nil if Status == Running.\n\tConfig *ConnConfig\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/external/external.go",
    "content": "// Package external implements a cluster driver for an external cluster.\npackage external\n\nimport (\n\t\"context\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/sqldb\"\n)\n\ntype Driver struct {\n\tHost              string // \"host\", \"host:port\", \"/path/to/unix.socket\",\n\tDatabase          string // database name\n\tSuperuserUsername string\n\tSuperuserPassword string\n}\n\nvar _ sqldb.Driver = (*Driver)(nil)\n\nfunc (d *Driver) CreateCluster(ctx context.Context, p *sqldb.CreateParams, log zerolog.Logger) (*sqldb.ClusterStatus, error) {\n\t// The external driver does not actually create the cluster; just return the status.\n\treturn d.ClusterStatus(ctx, p.ClusterID)\n}\n\nfunc (d *Driver) ClusterStatus(ctx context.Context, id sqldb.ClusterID) (*sqldb.ClusterStatus, error) {\n\tst := &sqldb.ClusterStatus{\n\t\tStatus: sqldb.Running,\n\t\tConfig: &sqldb.ConnConfig{\n\t\t\tHost: d.Host,\n\t\t\tSuperuser: sqldb.Role{\n\t\t\t\tType:     sqldb.RoleSuperuser,\n\t\t\t\tUsername: def(d.SuperuserUsername, \"postgres\"),\n\t\t\t\tPassword: def(d.SuperuserPassword, \"postgres\"),\n\t\t\t},\n\t\t\tRootDatabase: def(d.Database, \"postgres\"),\n\t\t},\n\t}\n\treturn st, nil\n}\n\nfunc (d *Driver) CanDestroyCluster(ctx context.Context, id sqldb.ClusterID) error {\n\treturn sqldb.ErrUnsupported\n}\n\nfunc (d *Driver) DestroyCluster(ctx context.Context, id sqldb.ClusterID) error {\n\treturn sqldb.ErrUnsupported\n}\n\nfunc (d *Driver) DestroyNamespaceData(ctx context.Context, ns *namespace.Namespace) error {\n\treturn sqldb.ErrUnsupported\n}\n\nfunc (d *Driver) CheckRequirements(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (d *Driver) Meta() sqldb.DriverMeta {\n\treturn sqldb.DriverMeta{ClusterIsolation: false}\n}\n\nfunc def(val, orDefault string) string {\n\tif val == \"\" {\n\t\tval = orDefault\n\t}\n\treturn val\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/manager.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/sync/singleflight\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/secret\"\n)\n\n// NewClusterManager creates a new ClusterManager.\nfunc NewClusterManager(driver Driver, apps *apps.Manager, ns *namespace.Manager, secretMgr *secret.Manager) *ClusterManager {\n\tlog := log.Logger\n\treturn &ClusterManager{\n\t\tlog:            log,\n\t\tdriver:         driver,\n\t\tapps:           apps,\n\t\tns:             ns,\n\t\tclusters:       make(map[clusterKey]*Cluster),\n\t\tbackendKeyData: make(map[uint32]*Cluster),\n\t\tsecretMgr:      secretMgr,\n\t}\n}\n\n// A ClusterManager manages running local sqldb clusters.\ntype ClusterManager struct {\n\tlog        zerolog.Logger\n\tdriver     Driver\n\tapps       *apps.Manager\n\tns         *namespace.Manager\n\tstartGroup singleflight.Group\n\tsecretMgr  *secret.Manager\n\n\tmu       sync.Mutex\n\tclusters map[clusterKey]*Cluster\n\t// backendKeyData maps the secret data to a cluster,\n\t// for forwarding cancel requests to the right cluster.\n\t// Access is guarded by mu.\n\tbackendKeyData map[uint32]*Cluster\n}\n\n// ClusterID uniquely identifies a cluster.\ntype ClusterID struct {\n\tNS   *namespace.Namespace\n\tType ClusterType\n}\n\n// clusterKey is the key to use to store a cluster in the cluster map.\ntype clusterKey string\n\nfunc (id ClusterID) clusterKey() clusterKey {\n\treturn clusterKey(fmt.Sprintf(\"%s-%s\", id.NS.ID, id.Type))\n}\n\nfunc GetClusterID(app *apps.Instance, typ ClusterType, ns *namespace.Namespace) ClusterID {\n\treturn ClusterID{ns, typ}\n}\n\n// Ready reports whether the cluster manager is ready and all requirements are met.\nfunc (cm *ClusterManager) Ready() error {\n\treturn cm.driver.CheckRequirements(context.Background())\n}\n\n// Create creates a database cluster but does not start it.\n// If the cluster already exists it is returned.\n// It does not perform any database migrations.\nfunc (cm *ClusterManager) Create(ctx context.Context, params *CreateParams) *Cluster {\n\tcm.mu.Lock()\n\tdefer cm.mu.Unlock()\n\n\tc, ok := cm.get(params.ClusterID)\n\tif ok {\n\t\tif status, err := c.Status(ctx); err != nil || status.Status != Running {\n\t\t\t// The cluster is no longer running; recreate it to clear our cached state.\n\t\t\tc.cancel()\n\t\t\tok = false\n\t\t}\n\t}\n\n\tif !ok {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tkey := params.ClusterID.clusterKey()\n\t\tpasswd := genPassword()\n\t\tsecretLoader := cm.secretMgr.Load(params.ClusterID.NS.App)\n\n\t\tc = &Cluster{\n\t\t\tID:       params.ClusterID,\n\t\t\tMemfs:    params.Memfs,\n\t\t\tPassword: passwd,\n\t\t\tCtx:      ctx,\n\t\t\tdriver:   cm.driver,\n\t\t\tcancel:   cancel,\n\t\t\tstarted:  make(chan struct{}),\n\t\t\tlog:      cm.log.With().Interface(\"cluster\", params.ClusterID).Logger(),\n\t\t\tdbs:      make(map[string]*DB),\n\t\t\tisExternal: func(name string) bool {\n\t\t\t\t// Don't use external databases for Memfs clusters (tests/shadows).\n\t\t\t\tif params.Memfs {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tsecrets, err := secretLoader.Get(ctx, nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\tc.log.Error().Err(err).Msg(\"failed to load secrets for external database check\")\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\t_, ok := secrets.Values[\"sqldb::\"+name]\n\t\t\t\treturn ok\n\t\t\t},\n\t\t}\n\n\t\tcm.clusters[key] = c\n\t}\n\n\treturn c\n}\n\n// LookupPassword looks up a cluster based on its password.\nfunc (cm *ClusterManager) LookupPassword(password string) (*Cluster, bool) {\n\tcm.mu.Lock()\n\tdefer cm.mu.Unlock()\n\n\tfor _, c := range cm.clusters {\n\t\tif c.Password == password {\n\t\t\treturn c, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// Get retrieves the cluster keyed by id.\nfunc (cm *ClusterManager) Get(id ClusterID) (*Cluster, bool) {\n\tcm.mu.Lock()\n\tdefer cm.mu.Unlock()\n\treturn cm.get(id)\n}\n\n// get retrieves the cluster keyed by id.\n// cm.mu must be held.\nfunc (cm *ClusterManager) get(id ClusterID) (*Cluster, bool) {\n\tc, ok := cm.clusters[id.clusterKey()]\n\treturn c, ok\n}\n\n// CanDeleteNamespace implements namespace.DeletionHandler.\nfunc (cm *ClusterManager) CanDeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error {\n\tc, ok := cm.Get(GetClusterID(app, Run, ns))\n\tif !ok {\n\t\treturn nil\n\t}\n\n\terr := c.driver.CanDestroyCluster(ctx, c.ID)\n\tif errors.Is(err, ErrUnsupported) {\n\t\terr = nil\n\t}\n\treturn nil\n}\n\n// DeleteNamespace implements namespace.DeletionHandler.\nfunc (cm *ClusterManager) DeleteNamespace(ctx context.Context, app *apps.Instance, ns *namespace.Namespace) error {\n\t// Find all clusters matching this namespace.\n\t// Use a closure for the lock to avoid holding it while we destroy the clusters.\n\tvar clusters []*Cluster\n\t(func() {\n\t\tcm.mu.Lock()\n\t\tdefer cm.mu.Unlock()\n\t\tfor _, c := range cm.clusters {\n\t\t\tif c.ID.NS.ID == ns.ID {\n\t\t\t\tclusters = append(clusters, c)\n\t\t\t}\n\t\t}\n\t})()\n\n\t// Destroy the clusters.\n\tfor _, c := range clusters {\n\t\tif err := c.driver.DestroyCluster(ctx, c.ID); err != nil && !errors.Is(err, ErrUnsupported) {\n\t\t\treturn errors.Wrapf(err, \"destroy cluster %s\", c.ID)\n\t\t}\n\t\tc.cancel()\n\t}\n\n\t// If that succeeded, destroy the namespace data.\n\terr := cm.driver.DestroyNamespaceData(ctx, ns)\n\tif errors.Is(err, ErrUnsupported) {\n\t\terr = nil\n\t}\n\treturn err\n}\n\nfunc genPassword() string {\n\tvar data [8]byte\n\tif _, err := rand.Read(data[:]); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"unable to generate random data\")\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(data[:])\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/migrate.go",
    "content": "package sqldb\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang-migrate/migrate/v4/database\"\n\t\"github.com/golang-migrate/migrate/v4/database/postgres\"\n\t\"github.com/golang-migrate/migrate/v4/source\"\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/lib/pq\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// MigrationReader is an interface for reading migration files. It has two main\n// implementations: OsMigrationReader and ZipFSMigrationReader.\ntype MigrationReader interface {\n\tRead(*meta.DBMigration) (r io.ReadCloser, err error)\n}\n\n// The OsMigrationReader reads migrations from the local filesystem.\nfunc NewOsMigrationReader(path string) *OsMigrationReader {\n\treturn &OsMigrationReader{path: path}\n}\n\ntype OsMigrationReader struct {\n\tpath string\n}\n\nfunc (src *OsMigrationReader) Read(m *meta.DBMigration) (r io.ReadCloser, err error) {\n\tfpath := filepath.Join(src.path, m.Filename)\n\tdata, err := os.ReadFile(fpath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn io.NopCloser(bytes.NewReader(data)), nil\n}\n\n// MultiReadCloser is a helper wrapper which extends the io.MultiReader to also\n// close the underlying closeable readers. It's used by the MetadataSource to\n// append a statement to mark a migration as successful.\nfunc MultiReadCloser(r ...io.Reader) io.ReadCloser {\n\treturn &multiReadCloser{\n\t\treaders:     r,\n\t\tmultiReader: io.MultiReader(r...),\n\t}\n}\n\ntype multiReadCloser struct {\n\treaders     []io.Reader\n\tmultiReader io.Reader\n}\n\nfunc (m multiReadCloser) Read(p []byte) (n int, err error) {\n\treturn m.multiReader.Read(p)\n}\n\nfunc (m multiReadCloser) Close() error {\n\tvar errs []error\n\tfor _, r := range m.readers {\n\t\tif c, ok := r.(io.Closer); !ok {\n\t\t\tcontinue\n\t\t} else if err := c.Close(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n\nvar _ io.ReadCloser = (*multiReadCloser)(nil)\n\n// NewMetadataSource creates a new MetadataSource instance.\nfunc NewMetadataSource(reader MigrationReader, migrations []*meta.DBMigration) *MetadataSource {\n\tsrc := &MetadataSource{\n\t\tMigrationReader: reader,\n\t\tmigrations:      migrations,\n\t}\n\tsrc.validate()\n\treturn src\n}\n\nfunc (src *MetadataSource) validate() {\n\tif src.err != nil {\n\t\treturn\n\t}\n\tseen := make(map[uint64]struct{})\n\tfor _, m := range src.migrations {\n\t\tif _, ok := seen[m.Number]; ok {\n\t\t\tsrc.err = fmt.Errorf(\"duplicate migration identifier %q\", m.Filename)\n\t\t\treturn\n\t\t}\n\t\tseen[m.Number] = struct{}{}\n\t}\n}\n\n// MetadataSource is a source.Driver implementation that keeps a list of migrations retrieved from\n// the Encore metadata. It relies on a MigrationReader to read the migration files.\ntype MetadataSource struct {\n\tMigrationReader\n\tmigrations []*meta.DBMigration\n\terr        error\n}\n\nfunc (src *MetadataSource) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {\n\tm, err := src.migration(version, 0)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tr, err = src.Read(m)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\t// This is used to make sure that a migration is marked successful in the\n\t// same statement as it's run. Otherwise we may end up with a finished migration\n\t// which is marked dirty because the SetVersion is run as a separate statement.\n\tstatement := fmt.Sprintf(\n\t\t\";\\ninsert into schema_migrations (version, dirty) values (%d, false) ON CONFLICT (version) DO UPDATE SET dirty = false;\",\n\t\tversion)\n\treturn MultiReadCloser(\n\t\tr,\n\t\tstrings.NewReader(statement),\n\t), m.Description, nil\n}\n\nfunc (src *MetadataSource) Open(url string) (source.Driver, error) {\n\treturn nil, fmt.Errorf(\"driver.Open is not implemented\")\n}\n\nfunc (src *MetadataSource) Close() error {\n\treturn nil\n}\n\nfunc (src *MetadataSource) First() (version uint, err error) {\n\tif len(src.migrations) == 0 {\n\t\treturn 0, os.ErrNotExist\n\t}\n\treturn uint(src.migrations[0].Number), nil\n}\n\nfunc (src *MetadataSource) Prev(version uint) (prevVersion uint, err error) {\n\tm, err := src.migration(version, -1)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint(m.Number), nil\n}\n\nfunc (src *MetadataSource) Next(version uint) (nextVersion uint, err error) {\n\tm, err := src.migration(version, +1)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint(m.Number), nil\n}\n\nfunc (src *MetadataSource) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {\n\treturn nil, \"\", os.ErrNotExist\n}\n\nfunc (src *MetadataSource) migration(version uint, offset int) (*meta.DBMigration, error) {\n\tif src.err != nil {\n\t\treturn nil, src.err\n\t}\n\tidx := slices.IndexFunc(src.migrations, func(m *meta.DBMigration) bool {\n\t\treturn m.Number == uint64(version)\n\t})\n\tif idx < 0 {\n\t\treturn nil, os.ErrNotExist\n\t}\n\tidx += offset\n\tif idx < 0 || idx >= len(src.migrations) {\n\t\treturn nil, os.ErrNotExist\n\t}\n\treturn src.migrations[idx], nil\n}\n\ntype nonSequentialDbDriver struct {\n\t*postgres.Postgres\n\tsource          *nonSequentialSource\n\tschemaName      string\n\tmigrationsTable string\n\tconn            *sql.Conn\n\tappliedVersions map[uint64]bool\n}\n\ntype nonSequentialSource struct {\n\t*MetadataSource\n\tdbDriver *nonSequentialDbDriver\n}\n\n// NonSequentialMigrator creates a new migrator that doesn't require migrations to be sequential.\n// It does this by keeping track of applied migrations in a table and using that to determine the\n// current version and which migrations need to be applied. It's effectively extending the logic of\n// the go-migrate library to support non-sequential migrations and is semi-compatible since it's using the\n// same underlying table.\nfunc NonSequentialMigrator(ctx context.Context, conn *sql.Conn, mdSource *MetadataSource) (database.Driver, source.Driver, error) {\n\tsrc := &nonSequentialSource{\n\t\tMetadataSource: mdSource,\n\t}\n\tdb := &nonSequentialDbDriver{\n\t\tconn:            conn,\n\t\tmigrationsTable: \"schema_migrations\",\n\t\tsource:          src,\n\t}\n\tsrc.dbDriver = db\n\tquery := `SELECT CURRENT_SCHEMA()`\n\tif err := conn.QueryRowContext(ctx, query).Scan(&db.schemaName); err != nil {\n\t\treturn nil, nil, &database.Error{OrigErr: err, Query: []byte(query)}\n\t}\n\n\tif len(db.schemaName) == 0 {\n\t\treturn nil, nil, postgres.ErrNoSchema\n\t}\n\n\tp, err := postgres.WithConnection(ctx, conn, &postgres.Config{\n\t\tMigrationsTable: db.migrationsTable,\n\t\tSchemaName:      db.schemaName,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"failed to create migration instance\")\n\t}\n\tdb.Postgres = p\n\tif err := db.loadAppliedVersions(); err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"failed to load applied versions\")\n\t}\n\treturn db, src, nil\n}\n\nfunc (p *nonSequentialDbDriver) Version() (version int, dirty bool, err error) {\n\tif len(p.appliedVersions) == 0 {\n\t\treturn database.NilVersion, false, nil\n\t}\n\tvar ok bool\n\tprevVersion := database.NilVersion\n\tfor _, mg := range p.source.migrations {\n\t\tdirty, ok = p.appliedVersions[mg.Number]\n\t\tif !ok {\n\t\t\treturn prevVersion, false, nil\n\t\t} else if dirty {\n\t\t\treturn int(mg.Number), true, nil\n\t\t}\n\t\tprevVersion = int(mg.Number)\n\t}\n\treturn prevVersion, false, nil\n}\n\nfunc (p *nonSequentialDbDriver) SetVersion(version int, dirty bool) error {\n\t// In PSQL, all migrations are applied within the same statement/transaction.\n\t// If the migration fails to apply, it is automatically rolled back.\n\t// Therefore, we don't need to worry about marking a migration as dirty.\n\tif dirty {\n\t\treturn nil\n\t}\n\ttx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})\n\tif err != nil {\n\t\treturn &database.Error{OrigErr: err, Err: \"transaction start failed\"}\n\t}\n\n\tif version >= 0 {\n\t\tquery := `INSERT INTO ` + pq.QuoteIdentifier(p.schemaName) + `.` + pq.QuoteIdentifier(p.migrationsTable) + ` (version, dirty) VALUES ($1, $2) ON CONFLICT (version) DO UPDATE SET dirty = $2`\n\t\tif _, err := tx.Exec(query, version, dirty); err != nil {\n\t\t\tif errRollback := tx.Rollback(); errRollback != nil {\n\t\t\t\terr = multierror.Append(err, errRollback)\n\t\t\t}\n\t\t\treturn &database.Error{OrigErr: err, Query: []byte(query)}\n\t\t}\n\t}\n\n\tif err := tx.Commit(); err != nil {\n\t\treturn &database.Error{OrigErr: err, Err: \"transaction commit failed\"}\n\t}\n\n\treturn nil\n}\n\nfunc LoadAppliedVersions(ctx context.Context, conn *sql.Conn, schemaName, migrationsTable string) (map[uint64]bool, error) {\n\tappliedVersions := map[uint64]bool{}\n\n\tquery := `SELECT version, dirty FROM ` + pq.QuoteIdentifier(schemaName) + `.` + pq.QuoteIdentifier(migrationsTable) + ` ORDER BY version`\n\trows, err := conn.QueryContext(context.Background(), query)\n\tif err != nil {\n\t\tif e, ok := err.(*pq.Error); ok {\n\t\t\tif e.Code.Name() == \"undefined_table\" {\n\t\t\t\treturn appliedVersions, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, &database.Error{OrigErr: err, Query: []byte(query)}\n\t}\n\tdefer rows.Close()\n\tvar version uint64\n\tvar dirty bool\n\tfor rows.Next() {\n\t\terr := rows.Scan(&version, &dirty)\n\t\tif err != nil {\n\t\t\treturn nil, &database.Error{OrigErr: err, Query: []byte(query)}\n\t\t}\n\t\tappliedVersions[version] = dirty\n\t}\n\treturn appliedVersions, nil\n}\n\nfunc (p *nonSequentialDbDriver) loadAppliedVersions() error {\n\tif p.appliedVersions != nil {\n\t\treturn nil\n\t}\n\tapplied, err := LoadAppliedVersions(context.Background(), p.conn, p.schemaName, p.migrationsTable)\n\tif err != nil {\n\t\treturn err\n\t}\n\tp.appliedVersions = applied\n\treturn nil\n}\n\nfunc (src *nonSequentialSource) Prev(version uint) (prevVersion uint, err error) {\n\tm, err := src.migration(version, -1)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\t// If the migration is applied, return this version\n\tif _, ok := src.dbDriver.appliedVersions[m.Number]; ok {\n\t\treturn uint(m.Number), nil\n\t}\n\t// Otherwise skip to the previous version\n\treturn src.Prev(uint(m.Number))\n}\n\nfunc (src *nonSequentialSource) Next(version uint) (nextVersion uint, err error) {\n\tm, err := src.migration(version, +1)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\t// If the migration is applied, return the next version\n\tif _, ok := src.dbDriver.appliedVersions[m.Number]; ok {\n\t\treturn src.Next(uint(m.Number))\n\t}\n\t// Otherwise, return this version\n\treturn uint(m.Number), nil\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/proxy.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/jackc/pgproto3/v2\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/pgproxy\"\n)\n\n// ServeProxy serves the database proxy using the given listener.\nfunc (cm *ClusterManager) ServeProxy(ln net.Listener) error {\n\tvar tempDelay time.Duration // how long to sleep on accept failure\n\tfor {\n\t\tconn, e := ln.Accept()\n\t\tif e != nil {\n\t\t\tif ne, ok := e.(net.Error); ok && ne.Temporary() {\n\t\t\t\tif tempDelay == 0 {\n\t\t\t\t\ttempDelay = 5 * time.Millisecond\n\t\t\t\t} else {\n\t\t\t\t\ttempDelay *= 2\n\t\t\t\t}\n\t\t\t\tif max := 1 * time.Second; tempDelay > max {\n\t\t\t\t\ttempDelay = max\n\t\t\t\t}\n\t\t\t\tlog.Error().Err(e).Msgf(\"dbproxy: accept error, retrying in %v\", tempDelay)\n\t\t\t\ttime.Sleep(tempDelay)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"dbproxy: could not accept: %v\", e)\n\t\t}\n\t\ttempDelay = 0\n\t\tgo func() {\n\t\t\tif err := cm.ProxyConn(conn, true); err != nil && err != context.Canceled {\n\t\t\t\tlog.Error().Err(err).Msg(\"dbproxy: proxy error\")\n\t\t\t}\n\t\t}()\n\t}\n}\n\n// ProxyConn authenticates and proxies a conn to the appropriate\n// database cluster and database.\n// If waitForSetup is true, it will wait for initial setup to complete\n// before proxying the connection.\nfunc (cm *ClusterManager) ProxyConn(client net.Conn, waitForSetup bool) error {\n\tdefer fns.CloseIgnore(client)\n\tcl, err := pgproxy.SetupClient(client, &pgproxy.ClientConfig{\n\t\tTLS:          nil,\n\t\tWantPassword: true,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif cancel, ok := cl.Hello.(*pgproxy.CancelData); ok {\n\t\tcm.cancelRequest(client, cancel)\n\t\treturn nil\n\t}\n\tstartup := cl.Hello.(*pgproxy.StartupData)\n\n\t// If the username is \"encore\" we're connecting to a database cluster\n\t// which may not be local\n\tvar cluster *Cluster\n\tif startup.Username == \"encore\" {\n\t\tpassword := startup.Password\n\t\tfound, ok := cm.LookupPassword(password)\n\t\tif !ok {\n\t\t\tcm.log.Error().Msg(\"dbproxy: could not find cluster\")\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"08006\",\n\t\t\t\tMessage:  \"database cluster not found or invalid connection string\",\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\t\tcluster = found\n\t} else {\n\t\t// The username is the app slug we want to connect to\n\t\tapp, err := cm.apps.FindLatestByPlatformOrLocalID(startup.Username)\n\t\tif err != nil {\n\t\t\tcm.log.Error().Err(err).Msg(\"dbproxy: could not find app\")\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"08006\",\n\t\t\t\tMessage:  \"unknown app ID\",\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\n\t\tctx := context.Background()\n\n\t\tclusterType, nsID, ok := strings.Cut(startup.Password, \"-\")\n\n\t\t// Look up the namespace to use.\n\t\tvar ns *namespace.Namespace\n\t\tif !ok {\n\t\t\tns, err = cm.ns.GetActive(ctx, app)\n\t\t} else {\n\t\t\tns, err = cm.ns.GetByID(ctx, app, namespace.ID(nsID))\n\t\t}\n\t\tif err != nil {\n\t\t\tcm.log.Error().Err(err).Msg(\"dbproxy: could not find infra namespace\")\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"08006\",\n\t\t\t\tMessage:  \"unknown active infra namespace\",\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\n\t\t// Resolve the cluster type.\n\t\tvar ct ClusterType\n\t\tswitch clusterType {\n\t\tcase \"local\":\n\t\t\tct = Run\n\t\tcase \"test\":\n\t\t\tct = Test\n\t\tcase \"shadow\":\n\t\t\tct = Shadow\n\t\tdefault:\n\t\t\tcm.log.Error().Str(\"password\", startup.Password).Msg(\"dbproxy: invalid password for connection URI\")\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"28P01\", // 28P01 = invalid password\n\t\t\t\tMessage:  \"if connecting with an app slug as the username, the only accepted passwords are 'local' or 'test' to route to those instances on your local system\",\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\n\t\t// Create the cluster if it doesn't exist in memory yet\n\t\t// This might be because the daemon is running, but the hasn't done anything\n\t\t// with the app in question yet on this run\n\t\tcluster = cm.Create(context.Background(), &CreateParams{\n\t\t\tClusterID: GetClusterID(app, ct, ns),\n\t\t\tMemfs:     ct.Memfs(),\n\t\t})\n\n\t\t// Ensure the cluster is started\n\t\t_, err = cluster.Start(context.Background(), nil)\n\t\tif err != nil {\n\t\t\tcm.log.Error().Err(err).Msg(\"dbproxy: could not start cluster\")\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"08006\",\n\t\t\t\tMessage:  \"could not start database cluster\",\n\t\t\t})\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// If Encore knows about the database, check if it's ready\n\t// however if the cluster doesn't know about the database, skip this part.\n\t//\n\t// This is because either:\n\t//   1. The database exists and is connected to\n\t//   2. The database does not exist, and the remote server will return a \"database doesn't exist\" error.\n\tdbname := startup.Database\n\tdb, ok := cluster.GetDB(dbname)\n\tif ok {\n\t\tvar ready <-chan struct{}\n\t\tif waitForSetup {\n\t\t\tready = db.Ready()\n\t\t} else {\n\t\t\ts := make(chan struct{})\n\t\t\tclose(s)\n\t\t\tready = s\n\t\t}\n\n\t\t// Wait for up to 60s for the cluster and database to come online.\n\t\tselect {\n\t\tcase <-db.Ctx.Done():\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"08006\",\n\t\t\t\tMessage:  \"db is shutting down\",\n\t\t\t})\n\t\t\treturn nil\n\t\tcase <-time.After(60 * time.Second):\n\t\t\tcm.log.Error().Str(\"db\", db.ApplicationCloudName()).Msg(\"dbproxy: timed out waiting for database to come online\")\n\t\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\t\tSeverity: \"FATAL\",\n\t\t\t\tCode:     \"08006\",\n\t\t\t\tMessage:  \"timed out waiting for db to complete setup\",\n\t\t\t})\n\t\t\treturn nil\n\n\t\tcase <-ready:\n\t\t\t// Continue connecting to backend, below\n\t\t}\n\t}\n\n\tinfo, err := cluster.Info(context.Background())\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"cluster not running: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\n\tserver, err := net.Dial(\"tcp\", info.Config.Host)\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"database not running: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\tdefer fns.CloseIgnore(server)\n\n\t// Send a modified startup message to the backend\n\tadmin, _ := info.Encore.First(RoleAdmin, RoleSuperuser)\n\tstartup.Username = admin.Username\n\tstartup.Password = admin.Password\n\tif db == nil {\n\t\t// We don't know about this database, we'll use the requested name\n\t\t// in case it does actually exist within the cluster.\n\t\t//\n\t\t// If it doesn't the cluster will return an SQL error to the client.\n\t\tstartup.Database = dbname\n\t} else {\n\t\tstartup.Database = db.ApplicationCloudName()\n\t}\n\tfe, err := pgproxy.SetupServer(server, &pgproxy.ServerConfig{\n\t\tTLS:     nil,\n\t\tStartup: startup,\n\t})\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"could not connect: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\tlog.Trace().Msg(\"backend connection established, notifying client\")\n\n\tif err := pgproxy.AuthenticateClient(cl.Backend); err != nil {\n\t\treturn err\n\t}\n\n\tkeyData, err := pgproxy.FinalizeInitialHandshake(cl.Backend, fe)\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"could not establish connection: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\tlog.Trace().Msg(\"connection handshake completed, proxying steady-state data\")\n\n\t// Store the key data so we know where to route cancellation requests.\n\tif keyData != nil {\n\t\tcm.mu.Lock()\n\t\tcm.backendKeyData[keyData.SecretKey] = cluster\n\t\tcm.mu.Unlock()\n\t\tdefer func() {\n\t\t\tcm.mu.Lock()\n\t\t\tdelete(cm.backendKeyData, keyData.SecretKey)\n\t\t\tcm.mu.Unlock()\n\t\t}()\n\t}\n\n\treturn pgproxy.CopySteadyState(cl.Backend, fe)\n}\n\n// PreauthProxyConn is a pre-authenticated proxy conn directly specifically to the given cluster.\nfunc (cm *ClusterManager) PreauthProxyConn(client net.Conn, id ClusterID) error {\n\tdefer fns.CloseIgnore(client)\n\tcl, err := pgproxy.SetupClient(client, &pgproxy.ClientConfig{\n\t\tTLS: &tls.Config{MinVersion: tls.VersionTLS12},\n\t})\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to setup client\")\n\t\treturn err\n\t}\n\n\tif cancel, ok := cl.Hello.(*pgproxy.CancelData); ok {\n\t\tcm.cancelRequest(client, cancel)\n\t\treturn nil\n\t}\n\tstartup := cl.Hello.(*pgproxy.StartupData)\n\n\tcluster, ok := cm.Get(id)\n\tif !ok {\n\t\tcm.log.Error().Interface(\"cluster\", id).Msg(\"dbproxy: could not find cluster\")\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"database cluster not running\",\n\t\t})\n\t\treturn nil\n\t}\n\tif cluster.IsExternalDB(startup.Database) {\n\t\tcm.log.Error().Str(\"db\", startup.Database).Msg(\"dbproxy: cannot proxy external database\")\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"proxy to external databases is disabled\",\n\t\t})\n\t\treturn nil\n\t}\n\tdb, ok := cluster.GetDB(startup.Database)\n\tif !ok {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"database not found\",\n\t\t})\n\t\treturn nil\n\t}\n\n\t// Wait for up to 60s for the cluster to come online.\n\tselect {\n\tcase <-db.Ctx.Done():\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"db is shutting down\",\n\t\t})\n\t\treturn nil\n\tcase <-time.After(60 * time.Second):\n\t\tcm.log.Error().Str(\"db\", startup.Database).Msg(\"dbproxy: timed out waiting for database to come online\")\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"timed out waiting for db to complete setup\",\n\t\t})\n\t\treturn nil\n\n\tcase <-cluster.Ready():\n\t\t// Continue connecting to backend, below\n\t}\n\n\tinfo, err := cluster.Info(context.Background())\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"cluster not running: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\n\tserver, err := net.Dial(\"tcp\", info.Config.Host)\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"database not running: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\tdefer fns.CloseIgnore(server)\n\n\tadmin, _ := info.Encore.First(RoleAdmin, RoleSuperuser)\n\tstartup.Username = admin.Username\n\tstartup.Password = admin.Password\n\tstartup.Database = db.ApplicationCloudName()\n\tfe, err := pgproxy.SetupServer(server, &pgproxy.ServerConfig{\n\t\tTLS:     nil,\n\t\tStartup: startup,\n\t})\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"could not connect: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\n\tif err := pgproxy.AuthenticateClient(cl.Backend); err != nil {\n\t\treturn err\n\t}\n\n\tkeyData, err := pgproxy.FinalizeInitialHandshake(cl.Backend, fe)\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"could not establish connection: \" + err.Error(),\n\t\t})\n\t\treturn nil\n\t}\n\n\t// Store the key data so we know where to route cancellation requests.\n\tif keyData != nil {\n\t\tcm.mu.Lock()\n\t\tcm.backendKeyData[keyData.SecretKey] = cluster\n\t\tcm.mu.Unlock()\n\t\tdefer func() {\n\t\t\tcm.mu.Lock()\n\t\t\tdelete(cm.backendKeyData, keyData.SecretKey)\n\t\t\tcm.mu.Unlock()\n\t\t}()\n\t}\n\n\tlog.Trace().Msg(\"successfully completed handshake, copying data back and forth\")\n\treturn pgproxy.CopySteadyState(cl.Backend, fe)\n}\n\n// cancelRequest handles a cancel request.\nfunc (cm *ClusterManager) cancelRequest(client io.Writer, req *pgproxy.CancelData) {\n\tcm.mu.Lock()\n\tcluster, ok := cm.backendKeyData[req.Raw.SecretKey]\n\tcm.mu.Unlock()\n\tif !ok {\n\t\treturn\n\t}\n\n\tinfo, err := cluster.Info(context.Background())\n\tif err != nil {\n\t\tmsg := &pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"database cluster not running\",\n\t\t}\n\t\tencode, _ := msg.Encode(nil)\n\t\t_, _ = client.Write(encode)\n\t\treturn\n\t}\n\n\tbackend, err := net.Dial(\"tcp\", info.Config.Host)\n\tif err != nil {\n\t\tmsg := &pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tCode:     \"08006\",\n\t\t\tMessage:  \"database cluster not running\",\n\t\t}\n\t\tencode, _ := msg.Encode(nil)\n\t\t_, _ = client.Write(encode)\n\t\treturn\n\t}\n\tdefer fns.CloseIgnore(backend)\n\t_ = pgproxy.SendCancelRequest(backend, req.Raw)\n}\n\nfunc writeMsg(w io.Writer, msg pgproto3.Message) error {\n\tencode, err := msg.Encode(nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = w.Write(encode)\n\treturn err\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/remote.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/pkg/pgproxy\"\n)\n\n// OneshotProxy listens on a random port for a single connection, and proxies that connection to a remote db.\n// It reports the one-time password and port to use.\n// Once a connection has been established, it stops listening.\nfunc OneshotProxy(appSlug, envSlug string, role RoleType) (port int, passwd string, err error) {\n\tln, err := net.Listen(\"tcp\", \"localhost:0\")\n\tif err != nil {\n\t\treturn 0, \"\", err\n\t}\n\tvar passwdBytes [8]byte\n\tif _, err := rand.Read(passwdBytes[:]); err != nil {\n\t\treturn 0, \"\", err\n\t}\n\tpasswd = base64.RawURLEncoding.EncodeToString(passwdBytes[:])\n\n\tgo oneshotServer(context.Background(), ln, passwd, appSlug, envSlug, role)\n\treturn ln.Addr().(*net.TCPAddr).Port, passwd, nil\n}\n\nfunc oneshotServer(ctx context.Context, ln net.Listener, passwd, appSlug, envSlug string, role RoleType) error {\n\tproxy := &pgproxy.SingleBackendProxy{\n\t\tRequirePassword: passwd != \"\",\n\t\tFrontendTLS:     nil,\n\t\tDialBackend: func(ctx context.Context, startup *pgproxy.StartupData) (pgproxy.LogicalConn, error) {\n\t\t\tif startup.Password != passwd {\n\t\t\t\treturn nil, fmt.Errorf(\"bad password\")\n\t\t\t}\n\t\t\tstartupData, err := startup.Raw.Encode(nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tws, err := platform.DBConnect(ctx, appSlug, envSlug, startup.Database, role.String(), startupData)\n\t\t\tif err != nil {\n\t\t\t\tvar e platform.Error\n\t\t\t\tif errors.As(err, &e) && e.HTTPCode == 404 {\n\t\t\t\t\treturn nil, pgproxy.DatabaseNotFoundError{Database: startup.Database}\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tconn := &WebsocketLogicalConn{Conn: ws}\n\t\t\treturn conn, nil\n\t\t},\n\t}\n\n\treturn proxy.Serve(ctx, ln)\n}\n\ntype WebsocketLogicalConn struct {\n\t*websocket.Conn\n\tbuf []byte\n}\n\nvar _ pgproxy.LogicalConn = (*WebsocketLogicalConn)(nil)\n\nfunc (c *WebsocketLogicalConn) Write(p []byte) (int, error) {\n\terr := c.Conn.WriteMessage(websocket.BinaryMessage, p)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(p), nil\n}\n\nfunc (c *WebsocketLogicalConn) Read(p []byte) (int, error) {\n\t// If we have remaining data from the previous message we received\n\t// from the stream, simply return that.\n\tif len(c.buf) > 0 {\n\t\tn := copy(p, c.buf)\n\t\tc.buf = c.buf[n:]\n\t\treturn n, nil\n\t}\n\n\t// No more buffered data, wait for a new message from the stream.\n\tfor {\n\t\ttyp, data, err := c.Conn.ReadMessage()\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t} else if typ != websocket.BinaryMessage {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Read as much data as possible directly to the waiting caller.\n\t\t// Anything remaining beyond that gets buffered until the next Read call.\n\t\tn := copy(p, data)\n\t\tc.buf = data[n:]\n\t\treturn n, nil\n\t}\n}\n\nfunc (c *WebsocketLogicalConn) Cancel(req *pgproxy.CancelData) error {\n\tenc := base64.StdEncoding\n\tdata, err := req.Raw.Encode(nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tencoded := make([]byte, enc.EncodedLen(len(data)))\n\tenc.Encode(encoded, data)\n\tlog.Info().Msgf(\"sending cancel request %x\", data)\n\treturn c.Conn.WriteMessage(websocket.TextMessage, encoded)\n}\n\nfunc (c *WebsocketLogicalConn) SetDeadline(t time.Time) error {\n\t_ = c.Conn.SetReadDeadline(t)\n\terr := c.Conn.SetWriteDeadline(t)\n\treturn err\n}\n"
  },
  {
    "path": "cli/daemon/sqldb/utils.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// WaitForConn waits for a successful connection to uri to be established.\nfunc WaitForConn(ctx context.Context, uri string) error {\n\tvar err error\n\tfor i := 0; i < 40; i++ {\n\t\tvar conn *pgx.Conn\n\t\tconn, err = pgx.Connect(ctx, uri)\n\t\tif err == nil {\n\t\t\terr = conn.Ping(ctx)\n\t\t\t_ = conn.Close(ctx)\n\t\t\tif err == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t} else if ctx.Err() != nil {\n\t\t\t// We'll never succeed once the context has been canceled.\n\t\t\t// Give up straight away.\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(250 * time.Millisecond)\n\t}\n\treturn fmt.Errorf(\"database did not come up: %v\", err)\n}\n\n// IsUsed reports whether the application uses SQL databases at all.\nfunc IsUsed(md *meta.Data) bool {\n\treturn len(md.SqlDatabases) > 0\n}\n"
  },
  {
    "path": "cli/daemon/telemetry.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"encr.dev/cli/internal/telemetry\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\nfunc (s *Server) Telemetry(ctx context.Context, req *daemonpb.TelemetryConfig) (*emptypb.Empty, error) {\n\ttelemetry.UpdateConfig(req.AnonId, req.Enabled, req.Debug)\n\treturn new(emptypb.Empty), nil\n}\n"
  },
  {
    "path": "cli/daemon/test.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime/debug\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/fns\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Test runs tests.\nfunc (s *Server) Test(req *daemonpb.TestRequest, stream daemonpb.Daemon_TestServer) error {\n\tctx := stream.Context()\n\tslog := &streamLog{stream: stream, buffered: false}\n\tstderr := slog.Stderr(false)\n\tsendErr := func(err error) {\n\t\tstderr.Write([]byte(err.Error() + \"\\n\"))\n\t\tstreamExit(stream, 1)\n\t}\n\n\tctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, req.TraceFile)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\tdefer tracer.Close()\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tns, err := s.namespaceOrActive(ctx, app, nil /* tests don't support different namespaces */)\n\tif err != nil {\n\t\tsendErr(err)\n\t\treturn nil\n\t}\n\n\tsecrets := s.sm.Load(app)\n\n\ttestCtx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\ttestResults := make(chan error, 1)\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif recovered := recover(); recovered != nil {\n\t\t\t\tvar err error\n\t\t\t\tswitch recovered := recovered.(type) {\n\t\t\t\tcase error:\n\t\t\t\t\terr = recovered\n\t\t\t\tdefault:\n\t\t\t\t\terr = fmt.Errorf(\"%+v\", recovered)\n\t\t\t\t}\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tlog.Err(err).Msgf(\"panic during test run:\\n%s\", stack)\n\t\t\t\ttestResults <- fmt.Errorf(\"panic occured within Encore during test run: %v\\n%s\\n\", recovered, stack)\n\t\t\t}\n\t\t}()\n\n\t\ttestEnv := append([]string{\"ENCORE_RUNTIME_LOG=error\"}, req.Environ...)\n\n\t\ttp := run.TestParams{\n\t\t\tTestSpecParams: &run.TestSpecParams{\n\t\t\t\tApp:          app,\n\t\t\t\tNS:           ns,\n\t\t\t\tWorkingDir:   req.WorkingDir,\n\t\t\t\tEnviron:      testEnv,\n\t\t\t\tArgs:         req.Args,\n\t\t\t\tSecrets:      secrets,\n\t\t\t\tCodegenDebug: req.CodegenDebug,\n\t\t\t\tTempDir:      req.TempDir,\n\t\t\t},\n\t\t\tStdout: slog.Stdout(false),\n\t\t\tStderr: slog.Stderr(false),\n\t\t}\n\t\ttestResults <- s.mgr.Test(testCtx, tp)\n\t}()\n\n\tif err := <-testResults; err != nil {\n\t\tsendErr(err)\n\t} else {\n\t\tstreamExit(stream, 0)\n\t}\n\treturn nil\n}\n\n// TestSpec runs tests.\nfunc (s *Server) TestSpec(ctx context.Context, req *daemonpb.TestSpecRequest) (resp *daemonpb.TestSpecResponse, err error) {\n\tctx, tracer, err := s.beginTracing(ctx, req.AppRoot, req.WorkingDir, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to begin tracing\")\n\t}\n\tdefer fns.CloseIgnore(tracer)\n\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to track app\")\n\t}\n\n\tns, err := s.namespaceOrActive(ctx, app, nil /* tests don't support different namespaces */)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to get namespace\")\n\t}\n\n\tsecrets := s.sm.Load(app)\n\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\tvar panicErr error\n\t\t\tswitch recovered := recovered.(type) {\n\t\t\tcase error:\n\t\t\t\tpanicErr = recovered\n\t\t\tdefault:\n\t\t\t\tpanicErr = fmt.Errorf(\"%+v\", recovered)\n\t\t\t}\n\t\t\tstack := debug.Stack()\n\t\t\tlog.Err(panicErr).Msgf(\"panic during test run:\\n%s\", stack)\n\t\t\terr = fmt.Errorf(\"panic during test run: %v\", panicErr)\n\t\t}\n\t}()\n\n\ttestEnv := append([]string{\"ENCORE_RUNTIME_LOG=error\"}, req.Environ...)\n\n\tspec, err := s.mgr.TestSpec(ctx, run.TestSpecParams{\n\t\tApp:        app,\n\t\tNS:         ns,\n\t\tWorkingDir: req.WorkingDir,\n\t\tEnviron:    testEnv,\n\t\tArgs:       req.Args,\n\t\tSecrets:    secrets,\n\t\tTempDir:    req.TempDir,\n\t})\n\tif errors.Is(err, builder.ErrNoTests) {\n\t\treturn nil, status.Error(codes.NotFound, \"no tests defined\")\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &daemonpb.TestSpecResponse{\n\t\tCommand: spec.Command,\n\t\tArgs:    spec.Args,\n\t\tEnviron: spec.Environ,\n\t}, nil\n}\n"
  },
  {
    "path": "cli/daemon/tracing.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\n\t\"encr.dev/internal/etrace\"\n)\n\nfunc (s *Server) beginTracing(ctx context.Context, appRoot, workingDir string, traceFile *string) (context.Context, *etrace.Tracer, error) {\n\tif traceFile == nil {\n\t\treturn ctx, nil, nil\n\t}\n\n\tvar dst string\n\tif filepath.IsAbs(*traceFile) {\n\t\tdst = *traceFile\n\t} else {\n\t\tdst = filepath.Join(appRoot, workingDir, *traceFile)\n\t}\n\treturn etrace.WithFileTracer(ctx, dst)\n}\n"
  },
  {
    "path": "cli/daemon/userfacing.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"runtime\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/vcs\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// GenWrappers generates Encore wrappers.\nfunc (s *Server) GenWrappers(ctx context.Context, req *daemonpb.GenWrappersRequest) (*daemonpb.GenWrappersResponse, error) {\n\tapp, err := s.apps.Track(req.AppRoot)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"resolve app\")\n\t}\n\tif err := s.genUserFacing(ctx, app); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &daemonpb.GenWrappersResponse{}, nil\n}\n\n// genUserFacing generates user-facing wrappers.\nfunc (s *Server) genUserFacing(ctx context.Context, app *apps.Instance) error {\n\texpSet, err := app.Experiments(nil)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"resolve experiments\")\n\t}\n\n\tvcsRevision := vcs.GetRevision(app.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tEnviron:            nil,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        app,\n\t\tWorkingDir: \".\",\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"prepare app\")\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         app,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"parse app\")\n\t}\n\n\tif err := app.CacheMetadata(parse.Meta); err != nil {\n\t\treturn errors.Wrap(err, \"cache metadata\")\n\t}\n\n\terr = bld.GenUserFacing(ctx, builder.GenUserFacingParams{\n\t\tBuild: buildInfo,\n\t\tApp:   app,\n\t\tParse: parse,\n\t})\n\treturn errors.Wrap(err, \"generate wrappers\")\n}\n"
  },
  {
    "path": "cli/daemon/watch.go",
    "content": "package daemon\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bep/debounce\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/pkg/watcher\"\n\t\"encr.dev/pkg/xos\"\n)\n\nfunc (s *Server) watchApps() {\n\tif os.Getenv(\"ENCORE_DAEMON_WATCH\") == \"0\" {\n\t\treturn\n\t}\n\ts.apps.RegisterAppListener(func(i *apps.Instance) {\n\t\ts.regenerateUserCode(context.Background(), i)\n\t\tif err := s.updateGitIgnore(i); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"unable to update app gitignore\")\n\t\t}\n\t})\n\tif err := s.apps.WatchAll(s.onWatchEvent); err != nil {\n\t\tlog.Error().Err(err).Msg(\"unable to set up app watchers\")\n\t} else {\n\t\tlog.Info().Msg(\"successfully set up file watchers\")\n\t}\n}\n\nfunc (s *Server) onWatchEvent(i *apps.Instance, events []watcher.Event) {\n\tif run.IgnoreEvents(events) {\n\t\treturn\n\t}\n\n\t// Use debounce to avoid calling this on every single change.\n\ts.appDebounceMu.Lock()\n\tdeb := s.appDebouncers[i]\n\tif deb == nil {\n\t\tdeb = &regenerateCodeDebouncer{\n\t\t\tdebounce: debounce.New(100 * time.Millisecond),\n\t\t\tdoRun:    func() { s.regenerateUserCode(context.Background(), i) },\n\t\t}\n\t\ts.appDebouncers[i] = deb\n\t}\n\ts.appDebounceMu.Unlock()\n\n\tdeb.ChangeEvent()\n}\n\ntype regenerateCodeDebouncer struct {\n\tdebounce func(func())\n\tmu       sync.Mutex\n\trunning  bool\n\trunAfter bool\n\n\tdoRun func()\n}\n\nfunc (g *regenerateCodeDebouncer) ChangeEvent() {\n\tg.debounce(func() {\n\t\tg.mu.Lock()\n\n\t\t// If we're already running, mark to run again when complete.\n\t\tif g.running {\n\t\t\tg.runAfter = true\n\t\t\tg.mu.Unlock()\n\t\t\treturn\n\t\t}\n\n\t\t// Otherwise, keep re-running for as long as change events come in.\n\t\tg.running = true\n\t\tg.runAfter = true // to start us off, at least once.\n\t\tfor g.runAfter {\n\t\t\tg.runAfter = false // reset for next time\n\t\t\tg.mu.Unlock()\n\t\t\tg.doRun() // actually run\n\t\t\tg.mu.Lock()\n\t\t}\n\n\t\t// If we get here g.runAfter nobody requested another run, so we can stop.\n\t\tg.running = false\n\n\t\tg.mu.Unlock()\n\t})\n}\n\nfunc (s *Server) regenerateUserCode(ctx context.Context, app *apps.Instance) {\n\tif err := s.genUserFacing(ctx, app); err != nil {\n\t\tlog.Error().Err(err).Str(\"app\", app.PlatformOrLocalID()).Msg(\"failed to regenerate app\")\n\t} else {\n\t\tlog.Info().Str(\"app\", app.PlatformOrLocalID()).Msg(\"successfully generated user code\")\n\t}\n}\n\n// updateGitIgnore updates the gitignore file to include Encore directives, if needed.\nfunc (s *Server) updateGitIgnore(i *apps.Instance) error {\n\tdst := filepath.Join(i.Root(), \".gitignore\")\n\tdata, err := os.ReadFile(dst)\n\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn errors.Wrap(err, \"read .gitignore\")\n\t}\n\n\t// Find which directives are already present\n\tdirectives := []string{\"encore.gen.go\", \"encore.gen.cue\", \"/.encore\", \"/encore.gen\"}\n\tfound := make([]bool, len(directives))\n\tscanner := bufio.NewScanner(bytes.NewReader(data))\n\tfor scanner.Scan() {\n\t\tln := scanner.Text()\n\t\tfor i, directive := range directives {\n\t\t\tif ln == directive {\n\t\t\t\tfound[i] = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// Add the ones that are missing\n\tupdated := false\n\tfor i, directive := range directives {\n\t\tif !found[i] {\n\t\t\tif len(data) > 0 && !bytes.HasSuffix(data, []byte(\"\\n\")) {\n\t\t\t\tdata = append(data, '\\n')\n\t\t\t}\n\t\t\tdata = append(data, directive+\"\\n\"...)\n\t\t\tupdated = true\n\t\t}\n\t}\n\n\t// Write the file back if there were any changes\n\tif updated {\n\t\treturn xos.WriteFile(dst, data, 0644)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/internal/browser/browser.go",
    "content": "// This package is vendored from an internal package in the\n// Go standard library at cmd/internal/browser.\n\n// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package browser provides utilities for interacting with users' browsers.\npackage browser\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\texec \"golang.org/x/sys/execabs\"\n)\n\n// Commands returns a list of possible commands to use to open a url.\nfunc Commands() [][]string {\n\tvar cmds [][]string\n\tif exe := os.Getenv(\"BROWSER\"); exe != \"\" {\n\t\tcmds = append(cmds, []string{exe})\n\t}\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tcmds = append(cmds, []string{\"/usr/bin/open\"})\n\tcase \"windows\":\n\t\tcmds = append(cmds, []string{\"cmd\", \"/c\", \"start\"})\n\tdefault:\n\t\tif os.Getenv(\"DISPLAY\") != \"\" {\n\t\t\t// xdg-open is only for use in a desktop environment.\n\t\t\tcmds = append(cmds, []string{\"xdg-open\"})\n\t\t}\n\t}\n\tcmds = append(cmds,\n\t\t[]string{\"chrome\"},\n\t\t[]string{\"google-chrome\"},\n\t\t[]string{\"chromium\"},\n\t\t[]string{\"firefox\"},\n\t)\n\treturn cmds\n}\n\n// CanOpen reports whether it's likely that Open will succeed.\nfunc CanOpen() bool {\n\tcmds := Commands()\n\tfor _, cmd := range cmds {\n\t\tif _, err := exec.LookPath(cmd[0]); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Open tries to open url in a browser and reports whether it succeeded.\nfunc Open(url string) bool {\n\tfor _, args := range Commands() {\n\t\tcmd := exec.Command(args[0], append(args[1:], url)...)\n\t\tif cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// appearsSuccessful reports whether the command appears to have run successfully.\n// If the command runs longer than the timeout, it's deemed successful.\n// If the command runs within the timeout, it's deemed successful if it exited cleanly.\nfunc appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool {\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\terrc <- cmd.Wait()\n\t}()\n\n\tselect {\n\tcase <-time.After(timeout):\n\t\treturn true\n\tcase err := <-errc:\n\t\treturn err == nil\n\t}\n}\n"
  },
  {
    "path": "cli/internal/bubbles/checklist/checklist.go",
    "content": "package checklist\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\ntype Item interface {\n\tRender(selected, checked bool) string\n}\n\ntype Model[I Item] struct {\n\tData    []I\n\tPerPage int\n\tInitial []bool\n\n\t// init indicates whether the data model has completed initialization\n\tinit bool\n\n\tchecked []bool // len(checked) == len(pageData)\n\n\t// index global real time index\n\tindex int\n\t// maxIndex global max index\n\tmaxIndex int\n\t// pageIndex real time index of current page\n\tpageIndex int\n\t// pageMaxIndex current page max index\n\tpageMaxIndex int\n\n\t// pageData data set rendered in real time on the current page\n\tpageData []I\n}\n\nfunc (m Model[I]) Selected() (I, bool) {\n\tidx := m.pageIndex\n\tif idx >= 0 && idx < len(m.pageData) {\n\t\treturn m.pageData[idx], true\n\t}\n\tvar zero I\n\treturn zero, false\n}\n\nfunc (m Model[I]) Checked() []I {\n\tindices := m.CheckedIndices()\n\titems := make([]I, len(indices))\n\tfor i, idx := range indices {\n\t\titems[i] = m.pageData[idx]\n\t}\n\treturn items\n}\n\nfunc (m Model[I]) CheckedIndices() []int {\n\tvar indices []int\n\tfor i, checked := range m.checked {\n\t\tif checked {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t}\n\treturn indices\n}\n\nfunc (m Model[I]) View() string {\n\tvar out strings.Builder\n\tcursor := \"»\" // TODO color etc\n\tfor i, obj := range m.pageData {\n\t\tselected := i == m.pageIndex\n\t\tchecked := m.checked[i]\n\t\tif selected {\n\t\t\tout.WriteString(cursor)\n\t\t\tout.WriteString(\" \")\n\t\t} else {\n\t\t\tout.WriteString(strings.Repeat(\" \", runewidth.StringWidth(cursor)+1))\n\t\t}\n\n\t\tif checked {\n\t\t\tout.WriteString(\"[x] \")\n\t\t} else {\n\t\t\tout.WriteString(\"[ ] \")\n\t\t}\n\n\t\tout.WriteString(obj.Render(selected, checked))\n\t\tout.WriteString(\"\\n\")\n\t}\n\n\treturn out.String()\n}\n\n// Update method responds to various events and modifies the data model\n// according to the corresponding events\nfunc (m Model[I]) Update(msg tea.Msg) (Model[I], tea.Cmd) {\n\tif !m.init {\n\t\tm.initData()\n\t\treturn m, nil\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch strings.ToLower(msg.String()) {\n\t\tcase \"down\":\n\t\t\tm.moveDown()\n\t\tcase \"up\":\n\t\t\tm.moveUp()\n\t\tcase \"right\", \"pgdown\", \"l\", \"k\":\n\t\t\tm.nextPage()\n\t\tcase \"left\", \"pgup\", \"h\", \"j\":\n\t\t\tm.prePage()\n\t\tcase \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\":\n\t\t\tnum, _ := strconv.Atoi(msg.String())\n\t\t\tidx := num - 1\n\t\t\tm.forward(idx)\n\n\t\tcase \"x\", \" \":\n\t\t\tm.toggle()\n\t\t}\n\t}\n\treturn m, nil\n}\n\nfunc (m *Model[I]) toggle() {\n\tidx := m.pageIndex\n\tif idx >= 0 && idx < len(m.pageData) {\n\t\tm.checked[idx] = !m.checked[idx]\n\t}\n}\n\n// moveDown executes the downward movement of the cursor,\n// while adjusting the internal index and refreshing the data area\nfunc (m *Model[I]) moveDown() {\n\t// the page index has not reached the maximum value, and the page\n\t// data area does not need to be updated\n\tif m.pageIndex < m.pageMaxIndex {\n\t\tm.pageIndex++\n\t\t// check whether the global index reaches the maximum value before sliding\n\t\tif m.index < m.maxIndex {\n\t\t\tm.index++\n\t\t}\n\t\treturn\n\t}\n\n\t// the page index reaches the maximum value, slide the page data area window,\n\t// the page index maintains the maximum value\n\tif m.pageIndex == m.pageMaxIndex {\n\t\t// check whether the global index reaches the maximum value before sliding\n\t\tif m.index < m.maxIndex {\n\t\t\t// global index increment\n\t\t\tm.index++\n\t\t\t// window slide down one data\n\t\t\tm.pageData = m.Data[m.index+1-m.PerPage : m.index+1]\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// moveUp performs an upward movement of the cursor,\n// while adjusting the internal index and refreshing the data area\nfunc (m *Model[I]) moveUp() {\n\t// the page index has not reached the minimum value, and the page\n\t// data area does not need to be updated\n\tif m.pageIndex > 0 {\n\t\tm.pageIndex--\n\t\t// check whether the global index reaches the minimum before sliding\n\t\tif m.index > 0 {\n\t\t\tm.index--\n\t\t}\n\t\treturn\n\t}\n\n\t// the page index reaches the minimum value, slide the page data window,\n\t// and the page index maintains the minimum value\n\tif m.pageIndex == 0 {\n\t\t// check whether the global index reaches the minimum before sliding\n\t\tif m.index > 0 {\n\t\t\t// window slide up one data\n\t\t\tm.pageData = m.Data[m.index-1 : m.index-1+m.PerPage]\n\t\t\t// global index decrement\n\t\t\tm.index--\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// nextPage triggers the page-down action, and does not change\n// the real-time page index(pageIndex)\nfunc (m *Model[I]) nextPage() {\n\t// Get the start and end position of the page data area slice: m.Data[start:end]\n\t//\n\t// note: the slice is closed left and opened right: `[start,end)`\n\t//       assuming that the global data area has unlimited length,\n\t//       end should always be the actual page `length+1`,\n\t//       the maximum value of end should be equal to `len(m.Data)`\n\t//       under limited length\n\tpageStart, pageEnd := m.pageIndexInfo()\n\t// there are two cases when `end` does not reach the maximum value\n\tif pageEnd < len(m.Data) {\n\t\t// the `end` value is at least one page length away from the global maximum index\n\t\tif len(m.Data)-pageEnd >= m.PerPage {\n\t\t\t// slide back one page in the page data area\n\t\t\tm.pageData = m.Data[pageStart+m.PerPage : pageEnd+m.PerPage]\n\t\t\t// Global real-time index increases by one page length\n\t\t\tm.index += m.PerPage\n\t\t} else { // `end` is less than a page length from the global maximum index\n\t\t\t// slide the page data area directly to the end\n\t\t\tm.pageData = m.Data[len(m.Data)-m.PerPage : len(m.Data)]\n\t\t\t// `sliding distance` = `position after sliding` - `position before sliding`\n\t\t\t// the global real-time index should also synchronize the same sliding distance\n\t\t\tm.index += len(m.Data) - pageEnd\n\t\t}\n\t}\n}\n\n// prePage triggers the page-up action, and does not change\n// the real-time page index(pageIndex)\nfunc (m *Model[I]) prePage() {\n\t// Get the start and end position of the page data area slice: m.Data[start:end]\n\t//\n\t// note: the slice is closed left and opened right: `[start,end)`\n\t//       assuming that the global data area has unlimited length,\n\t//       end should always be the actual page `length+1`,\n\t//       the maximum value of end should be equal to `len(m.Data)`\n\t//       under limited length\n\tpageStart, pageEnd := m.pageIndexInfo()\n\t// there are two cases when `start` does not reach the minimum value\n\tif pageStart > 0 {\n\t\t// `start` is at least one page length from the minimum\n\t\tif pageStart >= m.PerPage {\n\t\t\t// slide the page data area forward one page\n\t\t\tm.pageData = m.Data[pageStart-m.PerPage : pageEnd-m.PerPage]\n\t\t\t// Global real-time index reduces the length of one page\n\t\t\tm.index -= m.PerPage\n\t\t} else { // `start` to the minimum value less than one page length\n\t\t\t// slide the page data area directly to the start\n\t\t\tm.pageData = m.Data[:m.PerPage]\n\t\t\t// `sliding distance` = `position before sliding` - `minimum value(0)`\n\t\t\t// the global real-time index should also synchronize the same sliding distance\n\t\t\tm.index -= pageStart - 0\n\t\t}\n\t}\n}\n\n// forward triggers a fast jump action, if the pageIndex\n// is invalid, keep it as it is\nfunc (m *Model[I]) forward(pageIndex int) {\n\t// pageIndex has exceeded the maximum index of the page, ignore\n\tif pageIndex > m.pageMaxIndex {\n\t\treturn\n\t}\n\n\t// calculate the distance moved to pageIndex\n\tl := pageIndex - m.pageIndex\n\t// update the global real time index\n\tm.index += l\n\t// update the page real time index\n\tm.pageIndex = pageIndex\n\n}\n\n// initData initialize the data model, set the default value and\n// fix the wrong parameter settings during initialization\nfunc (m *Model[I]) initData() {\n\tif m.PerPage > len(m.Data) || m.PerPage < 1 {\n\t\tm.PerPage = len(m.Data)\n\t\tm.pageData = m.Data\n\t} else {\n\t\tm.pageData = m.Data[:m.PerPage]\n\t}\n\n\tm.pageIndex = 0\n\tm.pageMaxIndex = m.PerPage - 1\n\tm.index = 0\n\tm.maxIndex = len(m.Data) - 1\n\tm.checked = make([]bool, len(m.Data))\n\tcopy(m.checked, m.Initial)\n\tm.init = true\n}\n\n// pageIndexInfo return the start and end positions of the slice of the\n// page data area corresponding to the global data area\nfunc (m *Model[I]) pageIndexInfo() (start, end int) {\n\t// `Global real-time index` - `page real-time index` = `start index of page data area`\n\tstart = m.index - m.pageIndex\n\t// `Page data area start index` + `single page size` = `page data area end index`\n\tend = start + m.PerPage\n\treturn\n}\n"
  },
  {
    "path": "cli/internal/bubbles/selector/selector.go",
    "content": "package selector\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\ntype Item interface {\n\tRender(selected bool) string\n}\n\nfunc New[I Item](data []I, perPage int) Model[I] {\n\tm := Model[I]{data: data, perPage: perPage}\n\tm.initData()\n\treturn m\n}\n\ntype Model[I Item] struct {\n\tdata     []I\n\tpageData []I\n\tperPage  int\n\n\tfocused bool\n\n\t// init indicates whether the data model has completed initialization\n\tinit bool\n\t// index global real time index\n\tindex int\n\t// maxIndex global max index\n\tmaxIndex int\n\t// pageIndex real time index of current page\n\tpageIndex int\n\t// pageMaxIndex current page max index\n\tpageMaxIndex int\n}\n\nfunc (m Model[I]) Selected() (I, bool) {\n\tidx := m.index\n\tif idx >= 0 && idx < len(m.data) {\n\t\treturn m.data[idx], true\n\t}\n\tvar zero I\n\treturn zero, false\n}\n\nfunc (m Model[I]) View() string {\n\tvar out strings.Builder\n\tcursor := \"»\" // TODO color etc\n\tfor i, obj := range m.pageData {\n\t\tselected := i == m.pageIndex\n\t\tif selected {\n\t\t\tout.WriteString(cursor)\n\t\t\tout.WriteString(\" \")\n\t\t} else {\n\t\t\tout.WriteString(strings.Repeat(\" \", runewidth.StringWidth(cursor)+1))\n\t\t}\n\t\tout.WriteString(obj.Render(selected))\n\t\tout.WriteString(\"\\n\")\n\t}\n\n\treturn out.String()\n}\n\nfunc (m *Model[I]) Focus() tea.Cmd {\n\tm.focused = true\n\treturn nil\n}\n\nfunc (m *Model[I]) Blur() tea.Cmd {\n\tm.focused = false\n\treturn nil\n}\n\n// Update method responds to various events and modifies the data model\n// according to the corresponding events\nfunc (m Model[I]) Update(msg tea.Msg) (Model[I], tea.Cmd) {\n\tif !m.init {\n\t\tm.initData()\n\t\treturn m, nil\n\t} else if !m.focused {\n\t\treturn m, nil\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch strings.ToLower(msg.String()) {\n\t\tcase \"down\":\n\t\t\tm.moveDown()\n\t\tcase \"up\":\n\t\t\tm.moveUp()\n\t\tcase \"right\", \"pgdown\", \"l\", \"k\":\n\t\t\tm.nextPage()\n\t\tcase \"left\", \"pgup\", \"h\", \"j\":\n\t\t\tm.prePage()\n\t\tcase \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\":\n\t\t\tnum, _ := strconv.Atoi(msg.String())\n\t\t\tidx := num - 1\n\t\t\tm.forward(idx)\n\t\t}\n\t}\n\treturn m, nil\n}\n\n// moveDown executes the downward movement of the cursor,\n// while adjusting the internal index and refreshing the data area\nfunc (m *Model[I]) moveDown() {\n\t// the page index has not reached the maximum value, and the page\n\t// data area does not need to be updated\n\tif m.pageIndex < m.pageMaxIndex {\n\t\tm.pageIndex++\n\t\t// check whether the global index reaches the maximum value before sliding\n\t\tif m.index < m.maxIndex {\n\t\t\tm.index++\n\t\t}\n\t\treturn\n\t}\n\n\t// the page index reaches the maximum value, slide the page data area window,\n\t// the page index maintains the maximum value\n\tif m.pageIndex == m.pageMaxIndex {\n\t\t// check whether the global index reaches the maximum value before sliding\n\t\tif m.index < m.maxIndex {\n\t\t\t// global index increment\n\t\t\tm.index++\n\t\t\t// window slide down one data\n\t\t\tm.pageData = m.data[m.index+1-m.perPage : m.index+1]\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// moveUp performs an upward movement of the cursor,\n// while adjusting the internal index and refreshing the data area\nfunc (m *Model[I]) moveUp() {\n\t// the page index has not reached the minimum value, and the page\n\t// data area does not need to be updated\n\tif m.pageIndex > 0 {\n\t\tm.pageIndex--\n\t\t// check whether the global index reaches the minimum before sliding\n\t\tif m.index > 0 {\n\t\t\tm.index--\n\t\t}\n\t\treturn\n\t}\n\n\t// the page index reaches the minimum value, slide the page data window,\n\t// and the page index maintains the minimum value\n\tif m.pageIndex == 0 {\n\t\t// check whether the global index reaches the minimum before sliding\n\t\tif m.index > 0 {\n\t\t\t// window slide up one data\n\t\t\tm.pageData = m.data[m.index-1 : m.index-1+m.perPage]\n\t\t\t// global index decrement\n\t\t\tm.index--\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// nextPage triggers the page-down action, and does not change\n// the real-time page index(pageIndex)\nfunc (m *Model[I]) nextPage() {\n\t// Get the start and end position of the page data area slice: m.Data[start:end]\n\t//\n\t// note: the slice is closed left and opened right: `[start,end)`\n\t//       assuming that the global data area has unlimited length,\n\t//       end should always be the actual page `length+1`,\n\t//       the maximum value of end should be equal to `len(m.Data)`\n\t//       under limited length\n\tpageStart, pageEnd := m.pageIndexInfo()\n\t// there are two cases when `end` does not reach the maximum value\n\tif pageEnd < len(m.data) {\n\t\t// the `end` value is at least one page length away from the global maximum index\n\t\tif len(m.data)-pageEnd >= m.perPage {\n\t\t\t// slide back one page in the page data area\n\t\t\tm.pageData = m.data[pageStart+m.perPage : pageEnd+m.perPage]\n\t\t\t// Global real-time index increases by one page length\n\t\t\tm.index += m.perPage\n\t\t} else { // `end` is less than a page length from the global maximum index\n\t\t\t// slide the page data area directly to the end\n\t\t\tm.pageData = m.data[len(m.data)-m.perPage : len(m.data)]\n\t\t\t// `sliding distance` = `position after sliding` - `position before sliding`\n\t\t\t// the global real-time index should also synchronize the same sliding distance\n\t\t\tm.index += len(m.data) - pageEnd\n\t\t}\n\t}\n}\n\n// prePage triggers the page-up action, and does not change\n// the real-time page index(pageIndex)\nfunc (m *Model[I]) prePage() {\n\t// Get the start and end position of the page data area slice: m.Data[start:end]\n\t//\n\t// note: the slice is closed left and opened right: `[start,end)`\n\t//       assuming that the global data area has unlimited length,\n\t//       end should always be the actual page `length+1`,\n\t//       the maximum value of end should be equal to `len(m.Data)`\n\t//       under limited length\n\tpageStart, pageEnd := m.pageIndexInfo()\n\t// there are two cases when `start` does not reach the minimum value\n\tif pageStart > 0 {\n\t\t// `start` is at least one page length from the minimum\n\t\tif pageStart >= m.perPage {\n\t\t\t// slide the page data area forward one page\n\t\t\tm.pageData = m.data[pageStart-m.perPage : pageEnd-m.perPage]\n\t\t\t// Global real-time index reduces the length of one page\n\t\t\tm.index -= m.perPage\n\t\t} else { // `start` to the minimum value less than one page length\n\t\t\t// slide the page data area directly to the start\n\t\t\tm.pageData = m.data[:m.perPage]\n\t\t\t// `sliding distance` = `position before sliding` - `minimum value(0)`\n\t\t\t// the global real-time index should also synchronize the same sliding distance\n\t\t\tm.index -= pageStart - 0\n\t\t}\n\t}\n}\n\n// forward triggers a fast jump action, if the pageIndex\n// is invalid, keep it as it is\nfunc (m *Model[I]) forward(pageIndex int) {\n\t// pageIndex has exceeded the maximum index of the page, ignore\n\tif pageIndex > m.pageMaxIndex {\n\t\treturn\n\t}\n\n\t// calculate the distance moved to pageIndex\n\tl := pageIndex - m.pageIndex\n\t// update the global real time index\n\tm.index += l\n\t// update the page real time index\n\tm.pageIndex = pageIndex\n\n}\n\n// initData initialize the data model, set the default value and\n// fix the wrong parameter settings during initialization\nfunc (m *Model[I]) initData() {\n\tif m.perPage > len(m.data) || m.perPage < 1 {\n\t\tm.perPage = len(m.data)\n\t\tm.pageData = m.data\n\t} else {\n\t\tm.pageData = m.data[:m.perPage]\n\t}\n\n\tm.pageIndex = 0\n\tm.pageMaxIndex = m.perPage - 1\n\tm.index = 0\n\tm.maxIndex = len(m.data) - 1\n\tm.init = true\n}\n\n// pageIndexInfo return the start and end positions of the slice of the\n// page data area corresponding to the global data area\nfunc (m *Model[I]) pageIndexInfo() (start, end int) {\n\t// `Global real-time index` - `page real-time index` = `start index of page data area`\n\tstart = m.index - m.pageIndex\n\t// `Page data area start index` + `single page size` = `page data area end index`\n\tend = start + m.perPage\n\treturn\n}\n"
  },
  {
    "path": "cli/internal/dedent/dedent.go",
    "content": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2018 Peter Lithammer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n*/\n\npackage dedent\n\nimport (\n\t\"bytes\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nvar (\n\twhitespaceOnly    = regexp.MustCompile(\"(?m)^[ \\t]+$\")\n\tleadingWhitespace = regexp.MustCompile(\"(?m)(^[ \\t]*)(?:[^ \\t\\n])\")\n)\n\n// Dedent removes any common leading whitespace from every line in text.\n//\n// This can be used to make multiline strings to line up with the left edge of\n// the display, while still presenting them in the source code in indented\n// form.\nfunc Dedent(text string) string {\n\tif strings.HasPrefix(text, \"\\n\") {\n\t\ttext = strings.TrimPrefix(text, \"\\n\")\n\t}\n\n\tvar margin string\n\n\ttext = whitespaceOnly.ReplaceAllString(text, \"\")\n\tindents := leadingWhitespace.FindAllStringSubmatch(text, -1)\n\n\t// Look for the longest leading string of spaces and tabs common to all\n\t// lines.\n\tfor i, indent := range indents {\n\t\tif i == 0 {\n\t\t\tmargin = indent[1]\n\t\t} else if strings.HasPrefix(indent[1], margin) {\n\t\t\t// Current line more deeply indented than previous winner:\n\t\t\t// no change (previous winner is still on top).\n\t\t\tcontinue\n\t\t} else if strings.HasPrefix(margin, indent[1]) {\n\t\t\t// Current line consistent with and no deeper than previous winner:\n\t\t\t// it's the new winner.\n\t\t\tmargin = indent[1]\n\t\t} else {\n\t\t\t// Current line and previous winner have no common whitespace:\n\t\t\t// there is no margin.\n\t\t\tmargin = \"\"\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif margin != \"\" {\n\t\ttext = regexp.MustCompile(\"(?m)^\"+margin).ReplaceAllString(text, \"\")\n\t}\n\treturn text\n}\n\n// Bytes is like Dedent but for []bytes.\nfunc Bytes(text []byte) []byte {\n\tif bytes.HasPrefix(text, []byte(\"\\n\")) {\n\t\ttext = bytes.TrimPrefix(text, []byte(\"\\n\"))\n\t}\n\n\tvar margin []byte\n\n\ttext = whitespaceOnly.ReplaceAll(text, []byte{})\n\tindents := leadingWhitespace.FindAllSubmatch(text, -1)\n\n\t// Look for the longest leading string of spaces and tabs common to all\n\t// lines.\n\tfor i, indent := range indents {\n\t\tif i == 0 {\n\t\t\tmargin = indent[1]\n\t\t} else if bytes.HasPrefix(indent[1], margin) {\n\t\t\t// Current line more deeply indented than previous winner:\n\t\t\t// no change (previous winner is still on top).\n\t\t\tcontinue\n\t\t} else if bytes.HasPrefix(margin, indent[1]) {\n\t\t\t// Current line consistent with and no deeper than previous winner:\n\t\t\t// it's the new winner.\n\t\t\tmargin = indent[1]\n\t\t} else {\n\t\t\t// Current line and previous winner have no common whitespace:\n\t\t\t// there is no margin.\n\t\t\tmargin = []byte{}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(margin) > 0 {\n\t\ttext = regexp.MustCompile(\"(?m)^\"+string(margin)).ReplaceAll(text, []byte{})\n\t}\n\treturn text\n}\n"
  },
  {
    "path": "cli/internal/dedent/dedent_test.go",
    "content": "package dedent\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nconst errorMsg = \"\\nexpected %q\\ngot %q\"\n\ntype dedentTest struct {\n\ttext, expect string\n}\n\nfunc TestDedentNoMargin(t *testing.T) {\n\ttexts := []string{\n\t\t// No lines indented\n\t\t\"Hello there.\\nHow are you?\\nOh good, I'm glad.\",\n\t\t// Similar with a blank line\n\t\t\"Hello there.\\n\\nBoo!\",\n\t\t// Some lines indented, but overall margin is still zero\n\t\t\"Hello there.\\n  This is indented.\",\n\t\t// Again, add a blank line.\n\t\t\"Hello there.\\n\\n  Boo!\\n\",\n\t}\n\n\tfor _, text := range texts {\n\t\tif text != Dedent(text) {\n\t\t\tt.Errorf(errorMsg, text, Dedent(text))\n\t\t}\n\t}\n}\n\nfunc TestDedentEven(t *testing.T) {\n\ttexts := []dedentTest{\n\t\t{\n\t\t\t// All lines indented by two spaces\n\t\t\ttext:   \"  Hello there.\\n  How are ya?\\n  Oh good.\",\n\t\t\texpect: \"Hello there.\\nHow are ya?\\nOh good.\",\n\t\t},\n\t\t{\n\t\t\t// Same, with blank lines\n\t\t\ttext:   \"  Hello there.\\n\\n  How are ya?\\n  Oh good.\\n\",\n\t\t\texpect: \"Hello there.\\n\\nHow are ya?\\nOh good.\\n\",\n\t\t},\n\t\t{\n\t\t\t// Now indent one of the blank lines\n\t\t\ttext:   \"  Hello there.\\n  \\n  How are ya?\\n  Oh good.\\n\",\n\t\t\texpect: \"Hello there.\\n\\nHow are ya?\\nOh good.\\n\",\n\t\t},\n\t}\n\n\tfor _, text := range texts {\n\t\tif text.expect != Dedent(text.text) {\n\t\t\tt.Errorf(errorMsg, text.expect, Dedent(text.text))\n\t\t}\n\t}\n}\n\nfunc TestDedentUneven(t *testing.T) {\n\ttexts := []dedentTest{\n\t\t{\n\t\t\t// Lines indented unevenly\n\t\t\ttext: `\n\t\t\tdef foo():\n\t\t\t\twhile 1:\n\t\t\t\t\treturn foo\n\t\t\t`,\n\t\t\texpect: `def foo():\n\twhile 1:\n\t\treturn foo\n`,\n\t\t},\n\t\t{\n\t\t\t// Uneven indentation with a blank line\n\t\t\ttext:   \"  Foo\\n    Bar\\n\\n   Baz\\n\",\n\t\t\texpect: \"Foo\\n  Bar\\n\\n Baz\\n\",\n\t\t},\n\t\t{\n\t\t\t// Uneven indentation with a whitespace-only line\n\t\t\ttext:   \"  Foo\\n    Bar\\n \\n   Baz\\n\",\n\t\t\texpect: \"Foo\\n  Bar\\n\\n Baz\\n\",\n\t\t},\n\t}\n\n\tfor _, text := range texts {\n\t\tif text.expect != Dedent(text.text) {\n\t\t\tt.Errorf(errorMsg, text.expect, Dedent(text.text))\n\t\t}\n\t}\n}\n\n// Dedent() should not mangle internal tabs.\nfunc TestDedentPreserveInternalTabs(t *testing.T) {\n\ttext := \"  hello\\tthere\\n  how are\\tyou?\"\n\texpect := \"hello\\tthere\\nhow are\\tyou?\"\n\tif expect != Dedent(text) {\n\t\tt.Errorf(errorMsg, expect, Dedent(text))\n\t}\n\n\t// Make sure that it preserves tabs when it's not making any changes at all\n\tif expect != Dedent(expect) {\n\t\tt.Errorf(errorMsg, expect, Dedent(expect))\n\t}\n}\n\n// Dedent() should not mangle tabs in the margin (i.e. tabs and spaces both\n// count as margin, but are *not* considered equivalent).\nfunc TestDedentPreserveMarginTabs(t *testing.T) {\n\ttexts := []string{\n\t\t\"  hello there\\n\\thow are you?\",\n\t\t// Same effect even if we have 8 spaces\n\t\t\"        hello there\\n\\thow are you?\",\n\t}\n\n\tfor _, text := range texts {\n\t\td := Dedent(text)\n\t\tif text != d {\n\t\t\tt.Errorf(errorMsg, text, d)\n\t\t}\n\t}\n\n\ttexts2 := []dedentTest{\n\t\t{\n\t\t\t// Dedent() only removes whitespace that can be uniformly removed!\n\t\t\ttext:   \"\\thello there\\n\\thow are you?\",\n\t\t\texpect: \"hello there\\nhow are you?\",\n\t\t},\n\t\t{\n\t\t\ttext:   \"  \\thello there\\n  \\thow are you?\",\n\t\t\texpect: \"hello there\\nhow are you?\",\n\t\t},\n\t\t{\n\t\t\ttext:   \"  \\t  hello there\\n  \\t  how are you?\",\n\t\t\texpect: \"hello there\\nhow are you?\",\n\t\t},\n\t\t{\n\t\t\ttext:   \"  \\thello there\\n  \\t  how are you?\",\n\t\t\texpect: \"hello there\\n  how are you?\",\n\t\t},\n\t}\n\n\tfor _, text := range texts2 {\n\t\tif text.expect != Dedent(text.text) {\n\t\t\tt.Errorf(errorMsg, text.expect, Dedent(text.text))\n\t\t}\n\t}\n}\n\nfunc ExampleDedent() {\n\ts := `\n\t\tLorem ipsum dolor sit amet,\n\t\tconsectetur adipiscing elit.\n\t\tCurabitur justo tellus, facilisis nec efficitur dictum,\n\t\tfermentum vitae ligula. Sed eu convallis sapien.`\n\tfmt.Println(Dedent(s))\n\tfmt.Println(\"-------------\")\n\tfmt.Println(s)\n\t// Output:\n\t// Lorem ipsum dolor sit amet,\n\t// consectetur adipiscing elit.\n\t// Curabitur justo tellus, facilisis nec efficitur dictum,\n\t// fermentum vitae ligula. Sed eu convallis sapien.\n\t// -------------\n\t//\n\t//\t\tLorem ipsum dolor sit amet,\n\t//\t\tconsectetur adipiscing elit.\n\t//\t\tCurabitur justo tellus, facilisis nec efficitur dictum,\n\t//\t\tfermentum vitae ligula. Sed eu convallis sapien.\n}\n\nfunc BenchmarkDedent(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tDedent(`Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\t\tCurabitur justo tellus, facilisis nec efficitur dictum,\n\t\tfermentum vitae ligula. Sed eu convallis sapien.`)\n\t}\n}\n"
  },
  {
    "path": "cli/internal/gosym/pclntab.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n/*\n * Line tables\n */\n\npackage gosym\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"sync\"\n)\n\n// version of the pclntab\ntype version int\n\nconst (\n\tverUnknown version = iota\n\tver11\n\tver12\n\tver116\n)\n\n// A LineTable is a data structure mapping program counters to line numbers.\n//\n// In Go 1.1 and earlier, each function (represented by a Func) had its own LineTable,\n// and the line number corresponded to a numbering of all source lines in the\n// program, across all files. That absolute line number would then have to be\n// converted separately to a file name and line number within the file.\n//\n// In Go 1.2, the format of the data changed so that there is a single LineTable\n// for the entire program, shared by all Funcs, and there are no absolute line\n// numbers, just line numbers within specific files.\n//\n// For the most part, LineTable's methods should be treated as an internal\n// detail of the package; callers should use the methods on Table instead.\ntype LineTable struct {\n\tData []byte\n\tPC   uint64\n\tLine int\n\n\t// This mutex is used to keep parsing of pclntab synchronous.\n\tmu sync.Mutex\n\n\t// Contains the version of the pclntab section.\n\tversion version\n\n\t// Go 1.2/1.16 state\n\tbinary      binary.ByteOrder\n\tquantum     uint32\n\tptrsize     uint32\n\tfuncnametab []byte\n\tcutab       []byte\n\tfuncdata    []byte\n\tfunctab     []byte\n\tnfunctab    uint32\n\tfiletab     []byte\n\tpctab       []byte // points to the pctables.\n\tnfiletab    uint32\n\tfuncNames   map[uint32]string // cache the function names\n\tstrings     map[uint32]string // interned substrings of Data, keyed by offset\n\t// fileMap varies depending on the version of the object file.\n\t// For ver12, it maps the name to the index in the file table.\n\t// For ver116, it maps the name to the offset in filetab.\n\tfileMap map[string]uint32\n}\n\n// NOTE(rsc): This is wrong for GOARCH=arm, which uses a quantum of 4,\n// but we have no idea whether we're using arm or not. This only\n// matters in the old (pre-Go 1.2) symbol table format, so it's not worth\n// fixing.\nconst oldQuantum = 1\n\nfunc (t *LineTable) parse(targetPC uint64, targetLine int) (b []byte, pc uint64, line int) {\n\t// The PC/line table can be thought of as a sequence of\n\t//  <pc update>* <line update>\n\t// batches. Each update batch results in a (pc, line) pair,\n\t// where line applies to every PC from pc up to but not\n\t// including the pc of the next pair.\n\t//\n\t// Here we process each update individually, which simplifies\n\t// the code, but makes the corner cases more confusing.\n\tb, pc, line = t.Data, t.PC, t.Line\n\tfor pc <= targetPC && line != targetLine && len(b) > 0 {\n\t\tcode := b[0]\n\t\tb = b[1:]\n\t\tswitch {\n\t\tcase code == 0:\n\t\t\tif len(b) < 4 {\n\t\t\t\tb = b[0:0]\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tval := binary.BigEndian.Uint32(b)\n\t\t\tb = b[4:]\n\t\t\tline += int(val)\n\t\tcase code <= 64:\n\t\t\tline += int(code)\n\t\tcase code <= 128:\n\t\t\tline -= int(code - 64)\n\t\tdefault:\n\t\t\tpc += oldQuantum * uint64(code-128)\n\t\t\tcontinue\n\t\t}\n\t\tpc += oldQuantum\n\t}\n\treturn b, pc, line\n}\n\nfunc (t *LineTable) slice(pc uint64) *LineTable {\n\tdata, pc, line := t.parse(pc, -1)\n\treturn &LineTable{Data: data, PC: pc, Line: line}\n}\n\n// PCToLine returns the line number for the given program counter.\n//\n// Deprecated: Use Table's PCToLine method instead.\nfunc (t *LineTable) PCToLine(pc uint64) int {\n\tif t.isGo12() {\n\t\treturn t.go12PCToLine(pc, nil)\n\t}\n\t_, _, line := t.parse(pc, -1)\n\treturn line\n}\n\n// LineToPC returns the program counter for the given line number,\n// considering only program counters before maxpc.\n//\n// Deprecated: Use Table's LineToPC method instead.\nfunc (t *LineTable) LineToPC(line int, maxpc uint64) uint64 {\n\tif t.isGo12() {\n\t\treturn 0\n\t}\n\t_, pc, line1 := t.parse(maxpc, line)\n\tif line1 != line {\n\t\treturn 0\n\t}\n\t// Subtract quantum from PC to account for post-line increment\n\treturn pc - oldQuantum\n}\n\n// NewLineTable returns a new PC/line table\n// corresponding to the encoded data.\n// Text must be the start address of the\n// corresponding text segment.\nfunc NewLineTable(data []byte, text uint64) *LineTable {\n\treturn &LineTable{Data: data, PC: text, Line: 0, funcNames: make(map[uint32]string), strings: make(map[uint32]string)}\n}\n\n// Go 1.2 symbol table format.\n// See golang.org/s/go12symtab.\n//\n// A general note about the methods here: rather than try to avoid\n// index out of bounds errors, we trust Go to detect them, and then\n// we recover from the panics and treat them as indicative of a malformed\n// or incomplete table.\n//\n// The methods called by symtab.go, which begin with \"go12\" prefixes,\n// are expected to have that recovery logic.\n\n// isGo12 reports whether this is a Go 1.2 (or later) symbol table.\nfunc (t *LineTable) isGo12() bool {\n\tt.parsePclnTab()\n\treturn t.version >= ver12\n}\n\nconst go12magic = 0xfffffffb\nconst go116magic = 0xfffffffa\n\n// uintptr returns the pointer-sized value encoded at b.\n// The pointer size is dictated by the table being read.\nfunc (t *LineTable) uintptr(b []byte) uint64 {\n\tif t.ptrsize == 4 {\n\t\treturn uint64(t.binary.Uint32(b))\n\t}\n\treturn t.binary.Uint64(b)\n}\n\n// parsePclnTab parses the pclntab, setting the version.\nfunc (t *LineTable) parsePclnTab() {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tif t.version != verUnknown {\n\t\treturn\n\t}\n\n\t// Note that during this function, setting the version is the last thing we do.\n\t// If we set the version too early, and parsing failed (likely as a panic on\n\t// slice lookups), we'd have a mistaken version.\n\t//\n\t// Error paths through this code will default the version to 1.1.\n\tt.version = ver11\n\n\tdefer func() {\n\t\t// If we panic parsing, assume it's a Go 1.1 pclntab.\n\t\trecover()\n\t}()\n\n\t// Check header: 4-byte magic, two zeros, pc quantum, pointer size.\n\tif len(t.Data) < 16 || t.Data[4] != 0 || t.Data[5] != 0 ||\n\t\t(t.Data[6] != 1 && t.Data[6] != 2 && t.Data[6] != 4) || // pc quantum\n\t\t(t.Data[7] != 4 && t.Data[7] != 8) { // pointer size\n\t\treturn\n\t}\n\n\tvar possibleVersion version\n\tleMagic := binary.LittleEndian.Uint32(t.Data)\n\tbeMagic := binary.BigEndian.Uint32(t.Data)\n\tswitch {\n\tcase leMagic == go12magic:\n\t\tt.binary, possibleVersion = binary.LittleEndian, ver12\n\tcase beMagic == go12magic:\n\t\tt.binary, possibleVersion = binary.BigEndian, ver12\n\tcase leMagic == go116magic:\n\t\tt.binary, possibleVersion = binary.LittleEndian, ver116\n\tcase beMagic == go116magic:\n\t\tt.binary, possibleVersion = binary.BigEndian, ver116\n\tdefault:\n\t\treturn\n\t}\n\n\t// quantum and ptrSize are the same between 1.2 and 1.16\n\tt.quantum = uint32(t.Data[6])\n\tt.ptrsize = uint32(t.Data[7])\n\n\tswitch possibleVersion {\n\tcase ver116:\n\t\tt.nfunctab = uint32(t.uintptr(t.Data[8:]))\n\t\tt.nfiletab = uint32(t.uintptr(t.Data[8+t.ptrsize:]))\n\t\toffset := t.uintptr(t.Data[8+2*t.ptrsize:])\n\t\tt.funcnametab = t.Data[offset:]\n\t\toffset = t.uintptr(t.Data[8+3*t.ptrsize:])\n\t\tt.cutab = t.Data[offset:]\n\t\toffset = t.uintptr(t.Data[8+4*t.ptrsize:])\n\t\tt.filetab = t.Data[offset:]\n\t\toffset = t.uintptr(t.Data[8+5*t.ptrsize:])\n\t\tt.pctab = t.Data[offset:]\n\t\toffset = t.uintptr(t.Data[8+6*t.ptrsize:])\n\t\tt.funcdata = t.Data[offset:]\n\t\tt.functab = t.Data[offset:]\n\t\tfunctabsize := t.nfunctab*2*t.ptrsize + t.ptrsize\n\t\tt.functab = t.functab[:functabsize]\n\tcase ver12:\n\t\tt.nfunctab = uint32(t.uintptr(t.Data[8:]))\n\t\tt.funcdata = t.Data\n\t\tt.funcnametab = t.Data\n\t\tt.functab = t.Data[8+t.ptrsize:]\n\t\tt.pctab = t.Data\n\t\tfunctabsize := t.nfunctab*2*t.ptrsize + t.ptrsize\n\t\tfileoff := t.binary.Uint32(t.functab[functabsize:])\n\t\tt.functab = t.functab[:functabsize]\n\t\tt.filetab = t.Data[fileoff:]\n\t\tt.nfiletab = t.binary.Uint32(t.filetab)\n\t\tt.filetab = t.filetab[:t.nfiletab*4]\n\tdefault:\n\t\tpanic(\"unreachable\")\n\t}\n\tt.version = possibleVersion\n}\n\n// go12Funcs returns a slice of Funcs derived from the Go 1.2 pcln table.\nfunc (t *LineTable) go12Funcs() []Func {\n\t// Assume it is malformed and return nil on error.\n\tdefer func() {\n\t\trecover()\n\t}()\n\n\tn := len(t.functab) / int(t.ptrsize) / 2\n\tfuncs := make([]Func, n)\n\tfor i := range funcs {\n\t\tf := &funcs[i]\n\t\tf.Entry = t.uintptr(t.functab[2*i*int(t.ptrsize):])\n\t\tf.End = t.uintptr(t.functab[(2*i+2)*int(t.ptrsize):])\n\t\tinfo := t.funcdata[t.uintptr(t.functab[(2*i+1)*int(t.ptrsize):]):]\n\t\tf.LineTable = t\n\t\tf.FrameSize = int(t.binary.Uint32(info[t.ptrsize+2*4:]))\n\t\tf.Sym = &Sym{\n\t\t\tValue:  f.Entry,\n\t\t\tType:   'T',\n\t\t\tName:   t.funcName(t.binary.Uint32(info[t.ptrsize:])),\n\t\t\tGoType: 0,\n\t\t\tFunc:   f,\n\t\t}\n\t}\n\treturn funcs\n}\n\n// findFunc returns the func corresponding to the given program counter.\nfunc (t *LineTable) findFunc(pc uint64) []byte {\n\tif pc < t.uintptr(t.functab) || pc >= t.uintptr(t.functab[len(t.functab)-int(t.ptrsize):]) {\n\t\treturn nil\n\t}\n\n\t// The function table is a list of 2*nfunctab+1 uintptrs,\n\t// alternating program counters and offsets to func structures.\n\tf := t.functab\n\tnf := t.nfunctab\n\tfor nf > 0 {\n\t\tm := nf / 2\n\t\tfm := f[2*t.ptrsize*m:]\n\t\tif t.uintptr(fm) <= pc && pc < t.uintptr(fm[2*t.ptrsize:]) {\n\t\t\treturn t.funcdata[t.uintptr(fm[t.ptrsize:]):]\n\t\t} else if pc < t.uintptr(fm) {\n\t\t\tnf = m\n\t\t} else {\n\t\t\tf = f[(m+1)*2*t.ptrsize:]\n\t\t\tnf -= m + 1\n\t\t}\n\t}\n\treturn nil\n}\n\n// readvarint reads, removes, and returns a varint from *pp.\nfunc (t *LineTable) readvarint(pp *[]byte) uint32 {\n\tvar v, shift uint32\n\tp := *pp\n\tfor shift = 0; ; shift += 7 {\n\t\tb := p[0]\n\t\tp = p[1:]\n\t\tv |= (uint32(b) & 0x7F) << shift\n\t\tif b&0x80 == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\t*pp = p\n\treturn v\n}\n\n// funcName returns the name of the function found at off.\nfunc (t *LineTable) funcName(off uint32) string {\n\tif s, ok := t.funcNames[off]; ok {\n\t\treturn s\n\t}\n\ti := bytes.IndexByte(t.funcnametab[off:], 0)\n\ts := string(t.funcnametab[off : off+uint32(i)])\n\tt.funcNames[off] = s\n\treturn s\n}\n\n// stringFrom returns a Go string found at off from a position.\nfunc (t *LineTable) stringFrom(arr []byte, off uint32) string {\n\tif s, ok := t.strings[off]; ok {\n\t\treturn s\n\t}\n\ti := bytes.IndexByte(arr[off:], 0)\n\ts := string(arr[off : off+uint32(i)])\n\tt.strings[off] = s\n\treturn s\n}\n\n// string returns a Go string found at off.\nfunc (t *LineTable) string(off uint32) string {\n\treturn t.stringFrom(t.funcdata, off)\n}\n\n// step advances to the next pc, value pair in the encoded table.\nfunc (t *LineTable) step(p *[]byte, pc *uint64, val *int32, first bool) bool {\n\tuvdelta := t.readvarint(p)\n\tif uvdelta == 0 && !first {\n\t\treturn false\n\t}\n\tpcdelta := t.readvarint(p) * t.quantum\n\t*pc += uint64(pcdelta)\n\t*val += int32(-(uvdelta & 1) ^ (uvdelta >> 1))\n\treturn true\n}\n\n// pcvalue reports the value associated with the target pc.\n// off is the offset to the beginning of the pc-value table,\n// and entry is the start PC for the corresponding function.\nfunc (t *LineTable) pcvalue(off uint32, entry, targetpc uint64, fn *Func) int32 {\n\tp := t.pctab[off:]\n\n\tval := int32(-1)\n\tpc := entry\n\tfor t.step(&p, &pc, &val, pc == entry) {\n\t\tif targetpc < pc {\n\t\t\treturn val\n\t\t}\n\t}\n\treturn -1\n}\n\n// findFileLine scans one function in the binary looking for a\n// program counter in the given file on the given line.\n// It does so by running the pc-value tables mapping program counter\n// to file number. Since most functions come from a single file, these\n// are usually short and quick to scan. If a file match is found, then the\n// code goes to the expense of looking for a simultaneous line number match.\nfunc (t *LineTable) findFileLine(entry uint64, filetab, linetab uint32, filenum, line int32, cutab []byte) uint64 {\n\tif filetab == 0 || linetab == 0 {\n\t\treturn 0\n\t}\n\n\tfp := t.pctab[filetab:]\n\tfl := t.pctab[linetab:]\n\tfileVal := int32(-1)\n\tfilePC := entry\n\tlineVal := int32(-1)\n\tlinePC := entry\n\tfileStartPC := filePC\n\tfor t.step(&fp, &filePC, &fileVal, filePC == entry) {\n\t\tfileIndex := fileVal\n\t\tif t.version == ver116 {\n\t\t\tfileIndex = int32(t.binary.Uint32(cutab[fileVal*4:]))\n\t\t}\n\t\tif fileIndex == filenum && fileStartPC < filePC {\n\t\t\t// fileIndex is in effect starting at fileStartPC up to\n\t\t\t// but not including filePC, and it's the file we want.\n\t\t\t// Run the PC table looking for a matching line number\n\t\t\t// or until we reach filePC.\n\t\t\tlineStartPC := linePC\n\t\t\tfor linePC < filePC && t.step(&fl, &linePC, &lineVal, linePC == entry) {\n\t\t\t\t// lineVal is in effect until linePC, and lineStartPC < filePC.\n\t\t\t\tif lineVal == line {\n\t\t\t\t\tif fileStartPC <= lineStartPC {\n\t\t\t\t\t\treturn lineStartPC\n\t\t\t\t\t}\n\t\t\t\t\tif fileStartPC < linePC {\n\t\t\t\t\t\treturn fileStartPC\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlineStartPC = linePC\n\t\t\t}\n\t\t}\n\t\tfileStartPC = filePC\n\t}\n\treturn 0\n}\n\n// go12PCToLine maps program counter to line number for the Go 1.2 pcln table.\nfunc (t *LineTable) go12PCToLine(pc uint64, fn *Func) (line int) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tline = -1\n\t\t}\n\t}()\n\n\tf := t.findFunc(pc)\n\tif f == nil {\n\t\treturn -1\n\t}\n\tentry := t.uintptr(f)\n\tif pc > entry {\n\t\tpc--\n\t}\n\tlinetab := t.binary.Uint32(f[t.ptrsize+5*4:])\n\treturn int(t.pcvalue(linetab, entry, pc, fn))\n}\n\n// go12PCToFile maps program counter to file name for the Go 1.2 pcln table.\nfunc (t *LineTable) go12PCToFile(pc uint64, fn *Func) (file string) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tfile = \"\"\n\t\t}\n\t}()\n\n\tf := t.findFunc(pc)\n\tif f == nil {\n\t\treturn \"\"\n\t}\n\tentry := t.uintptr(f)\n\tif pc > entry {\n\t\tpc--\n\t}\n\tfiletab := t.binary.Uint32(f[t.ptrsize+4*4:])\n\tfno := t.pcvalue(filetab, entry, pc, fn)\n\tif t.version == ver12 {\n\t\tif fno <= 0 {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn t.string(t.binary.Uint32(t.filetab[4*fno:]))\n\t}\n\t// Go ≥ 1.16\n\tif fno < 0 { // 0 is valid for ≥ 1.16\n\t\treturn \"\"\n\t}\n\tcuoff := t.binary.Uint32(f[t.ptrsize+7*4:])\n\tif fnoff := t.binary.Uint32(t.cutab[(cuoff+uint32(fno))*4:]); fnoff != ^uint32(0) {\n\t\treturn t.stringFrom(t.filetab, fnoff)\n\t}\n\treturn \"\"\n}\n\n// go12LineToPC maps a (file, line) pair to a program counter for the Go 1.2/1.16 pcln table.\nfunc (t *LineTable) go12LineToPC(file string, line int) (pc uint64) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tpc = 0\n\t\t}\n\t}()\n\n\tt.initFileMap()\n\tfilenum, ok := t.fileMap[file]\n\tif !ok {\n\t\treturn 0\n\t}\n\n\t// Scan all functions.\n\t// If this turns out to be a bottleneck, we could build a map[int32][]int32\n\t// mapping file number to a list of functions with code from that file.\n\tvar cutab []byte\n\tfor i := uint32(0); i < t.nfunctab; i++ {\n\t\tf := t.funcdata[t.uintptr(t.functab[2*t.ptrsize*i+t.ptrsize:]):]\n\t\tentry := t.uintptr(f)\n\t\tfiletab := t.binary.Uint32(f[t.ptrsize+4*4:])\n\t\tlinetab := t.binary.Uint32(f[t.ptrsize+5*4:])\n\t\tif t.version == ver116 {\n\t\t\tcuoff := t.binary.Uint32(f[t.ptrsize+7*4:]) * 4\n\t\t\tcutab = t.cutab[cuoff:]\n\t\t}\n\t\tpc := t.findFileLine(entry, filetab, linetab, int32(filenum), int32(line), cutab)\n\t\tif pc != 0 {\n\t\t\treturn pc\n\t\t}\n\t}\n\treturn 0\n}\n\n// initFileMap initializes the map from file name to file number.\nfunc (t *LineTable) initFileMap() {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\n\tif t.fileMap != nil {\n\t\treturn\n\t}\n\tm := make(map[string]uint32)\n\n\tif t.version == ver12 {\n\t\tfor i := uint32(1); i < t.nfiletab; i++ {\n\t\t\ts := t.string(t.binary.Uint32(t.filetab[4*i:]))\n\t\t\tm[s] = i\n\t\t}\n\t} else {\n\t\tvar pos uint32\n\t\tfor i := uint32(0); i < t.nfiletab; i++ {\n\t\t\ts := t.stringFrom(t.filetab, pos)\n\t\t\tm[s] = pos\n\t\t\tpos += uint32(len(s) + 1)\n\t\t}\n\t}\n\tt.fileMap = m\n}\n\n// go12MapFiles adds to m a key for every file in the Go 1.2 LineTable.\n// Every key maps to obj. That's not a very interesting map, but it provides\n// a way for callers to obtain the list of files in the program.\nfunc (t *LineTable) go12MapFiles(m map[string]*Obj, obj *Obj) {\n\tdefer func() {\n\t\trecover()\n\t}()\n\n\tt.initFileMap()\n\tfor file := range t.fileMap {\n\t\tm[file] = obj\n\t}\n}\n"
  },
  {
    "path": "cli/internal/gosym/symtab.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package gosym implements access to the Go symbol\n// and line number tables embedded in Go binaries generated\n// by the gc compilers.\npackage gosym\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n/*\n * Symbols\n */\n\n// A Sym represents a single symbol table entry.\ntype Sym struct {\n\tValue  uint64\n\tType   byte\n\tName   string\n\tGoType uint64\n\t// If this symbol is a function symbol, the corresponding Func\n\tFunc *Func\n}\n\n// Static reports whether this symbol is static (not visible outside its file).\nfunc (s *Sym) Static() bool { return s.Type >= 'a' }\n\n// PackageName returns the package part of the symbol name,\n// or the empty string if there is none.\nfunc (s *Sym) PackageName() string {\n\tname := s.Name\n\n\t// A prefix of \"type.\" and \"go.\" is a compiler-generated symbol that doesn't belong to any package.\n\t// See variable reservedimports in cmd/compile/internal/gc/subr.go\n\tif strings.HasPrefix(name, \"go.\") || strings.HasPrefix(name, \"type.\") {\n\t\treturn \"\"\n\t}\n\n\tpathend := strings.LastIndex(name, \"/\")\n\tif pathend < 0 {\n\t\tpathend = 0\n\t}\n\n\tif i := strings.Index(name[pathend:], \".\"); i != -1 {\n\t\treturn name[:pathend+i]\n\t}\n\treturn \"\"\n}\n\n// ReceiverName returns the receiver type name of this symbol,\n// or the empty string if there is none.\nfunc (s *Sym) ReceiverName() string {\n\tpathend := strings.LastIndex(s.Name, \"/\")\n\tif pathend < 0 {\n\t\tpathend = 0\n\t}\n\tl := strings.Index(s.Name[pathend:], \".\")\n\tr := strings.LastIndex(s.Name[pathend:], \".\")\n\tif l == -1 || r == -1 || l == r {\n\t\treturn \"\"\n\t}\n\treturn s.Name[pathend+l+1 : pathend+r]\n}\n\n// BaseName returns the symbol name without the package or receiver name.\nfunc (s *Sym) BaseName() string {\n\tif i := strings.LastIndex(s.Name, \".\"); i != -1 {\n\t\treturn s.Name[i+1:]\n\t}\n\treturn s.Name\n}\n\n// A Func collects information about a single function.\ntype Func struct {\n\tEntry uint64\n\t*Sym\n\tEnd       uint64\n\tParams    []*Sym // nil for Go 1.3 and later binaries\n\tLocals    []*Sym // nil for Go 1.3 and later binaries\n\tFrameSize int\n\tLineTable *LineTable\n\tObj       *Obj\n}\n\n// An Obj represents a collection of functions in a symbol table.\n//\n// The exact method of division of a binary into separate Objs is an internal detail\n// of the symbol table format.\n//\n// In early versions of Go each source file became a different Obj.\n//\n// In Go 1 and Go 1.1, each package produced one Obj for all Go sources\n// and one Obj per C source file.\n//\n// In Go 1.2, there is a single Obj for the entire program.\ntype Obj struct {\n\t// Funcs is a list of functions in the Obj.\n\tFuncs []Func\n\n\t// In Go 1.1 and earlier, Paths is a list of symbols corresponding\n\t// to the source file names that produced the Obj.\n\t// In Go 1.2, Paths is nil.\n\t// Use the keys of Table.Files to obtain a list of source files.\n\tPaths []Sym // meta\n}\n\n/*\n * Symbol tables\n */\n\n// Table represents a Go symbol table. It stores all of the\n// symbols decoded from the program and provides methods to translate\n// between symbols, names, and addresses.\ntype Table struct {\n\tSyms  []Sym // nil for Go 1.3 and later binaries\n\tFuncs []Func\n\tFiles map[string]*Obj // for Go 1.2 and later all files map to one Obj\n\tObjs  []Obj           // for Go 1.2 and later only one Obj in slice\n\n\tgo12line *LineTable // Go 1.2 line number table\n}\n\ntype sym struct {\n\tvalue  uint64\n\tgotype uint64\n\ttyp    byte\n\tname   []byte\n}\n\nvar (\n\tlittleEndianSymtab    = []byte{0xFD, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00}\n\tbigEndianSymtab       = []byte{0xFF, 0xFF, 0xFF, 0xFD, 0x00, 0x00, 0x00}\n\toldLittleEndianSymtab = []byte{0xFE, 0xFF, 0xFF, 0xFF, 0x00, 0x00}\n)\n\nfunc walksymtab(data []byte, fn func(sym) error) error {\n\tif len(data) == 0 { // missing symtab is okay\n\t\treturn nil\n\t}\n\tvar order binary.ByteOrder = binary.BigEndian\n\tnewTable := false\n\tswitch {\n\tcase bytes.HasPrefix(data, oldLittleEndianSymtab):\n\t\t// Same as Go 1.0, but little endian.\n\t\t// Format was used during interim development between Go 1.0 and Go 1.1.\n\t\t// Should not be widespread, but easy to support.\n\t\tdata = data[6:]\n\t\torder = binary.LittleEndian\n\tcase bytes.HasPrefix(data, bigEndianSymtab):\n\t\tnewTable = true\n\tcase bytes.HasPrefix(data, littleEndianSymtab):\n\t\tnewTable = true\n\t\torder = binary.LittleEndian\n\t}\n\tvar ptrsz int\n\tif newTable {\n\t\tif len(data) < 8 {\n\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t}\n\t\tptrsz = int(data[7])\n\t\tif ptrsz != 4 && ptrsz != 8 {\n\t\t\treturn &DecodingError{7, \"invalid pointer size\", ptrsz}\n\t\t}\n\t\tdata = data[8:]\n\t}\n\tvar s sym\n\tp := data\n\tfor len(p) >= 4 {\n\t\tvar typ byte\n\t\tif newTable {\n\t\t\t// Symbol type, value, Go type.\n\t\t\ttyp = p[0] & 0x3F\n\t\t\twideValue := p[0]&0x40 != 0\n\t\t\tgoType := p[0]&0x80 != 0\n\t\t\tif typ < 26 {\n\t\t\t\ttyp += 'A'\n\t\t\t} else {\n\t\t\t\ttyp += 'a' - 26\n\t\t\t}\n\t\t\ts.typ = typ\n\t\t\tp = p[1:]\n\t\t\tif wideValue {\n\t\t\t\tif len(p) < ptrsz {\n\t\t\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t\t\t}\n\t\t\t\t// fixed-width value\n\t\t\t\tif ptrsz == 8 {\n\t\t\t\t\ts.value = order.Uint64(p[0:8])\n\t\t\t\t\tp = p[8:]\n\t\t\t\t} else {\n\t\t\t\t\ts.value = uint64(order.Uint32(p[0:4]))\n\t\t\t\t\tp = p[4:]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// varint value\n\t\t\t\ts.value = 0\n\t\t\t\tshift := uint(0)\n\t\t\t\tfor len(p) > 0 && p[0]&0x80 != 0 {\n\t\t\t\t\ts.value |= uint64(p[0]&0x7F) << shift\n\t\t\t\t\tshift += 7\n\t\t\t\t\tp = p[1:]\n\t\t\t\t}\n\t\t\t\tif len(p) == 0 {\n\t\t\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t\t\t}\n\t\t\t\ts.value |= uint64(p[0]) << shift\n\t\t\t\tp = p[1:]\n\t\t\t}\n\t\t\tif goType {\n\t\t\t\tif len(p) < ptrsz {\n\t\t\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t\t\t}\n\t\t\t\t// fixed-width go type\n\t\t\t\tif ptrsz == 8 {\n\t\t\t\t\ts.gotype = order.Uint64(p[0:8])\n\t\t\t\t\tp = p[8:]\n\t\t\t\t} else {\n\t\t\t\t\ts.gotype = uint64(order.Uint32(p[0:4]))\n\t\t\t\t\tp = p[4:]\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Value, symbol type.\n\t\t\ts.value = uint64(order.Uint32(p[0:4]))\n\t\t\tif len(p) < 5 {\n\t\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t\t}\n\t\t\ttyp = p[4]\n\t\t\tif typ&0x80 == 0 {\n\t\t\t\treturn &DecodingError{len(data) - len(p) + 4, \"bad symbol type\", typ}\n\t\t\t}\n\t\t\ttyp &^= 0x80\n\t\t\ts.typ = typ\n\t\t\tp = p[5:]\n\t\t}\n\n\t\t// Name.\n\t\tvar i int\n\t\tvar nnul int\n\t\tfor i = 0; i < len(p); i++ {\n\t\t\tif p[i] == 0 {\n\t\t\t\tnnul = 1\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tswitch typ {\n\t\tcase 'z', 'Z':\n\t\t\tp = p[i+nnul:]\n\t\t\tfor i = 0; i+2 <= len(p); i += 2 {\n\t\t\t\tif p[i] == 0 && p[i+1] == 0 {\n\t\t\t\t\tnnul = 2\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(p) < i+nnul {\n\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t}\n\t\ts.name = p[0:i]\n\t\ti += nnul\n\t\tp = p[i:]\n\n\t\tif !newTable {\n\t\t\tif len(p) < 4 {\n\t\t\t\treturn &DecodingError{len(data), \"unexpected EOF\", nil}\n\t\t\t}\n\t\t\t// Go type.\n\t\t\ts.gotype = uint64(order.Uint32(p[:4]))\n\t\t\tp = p[4:]\n\t\t}\n\t\tfn(s)\n\t}\n\treturn nil\n}\n\n// NewTable decodes the Go symbol table (the \".gosymtab\" section in ELF),\n// returning an in-memory representation.\n// Starting with Go 1.3, the Go symbol table no longer includes symbol data.\nfunc NewTable(symtab []byte, pcln *LineTable) (*Table, error) {\n\tvar n int\n\terr := walksymtab(symtab, func(s sym) error {\n\t\tn++\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar t Table\n\tif pcln.isGo12() {\n\t\tt.go12line = pcln\n\t}\n\tfname := make(map[uint16]string)\n\tt.Syms = make([]Sym, 0, n)\n\tnf := 0\n\tnz := 0\n\tlasttyp := uint8(0)\n\terr = walksymtab(symtab, func(s sym) error {\n\t\tn := len(t.Syms)\n\t\tt.Syms = t.Syms[0 : n+1]\n\t\tts := &t.Syms[n]\n\t\tts.Type = s.typ\n\t\tts.Value = s.value\n\t\tts.GoType = s.gotype\n\t\tswitch s.typ {\n\t\tdefault:\n\t\t\t// rewrite name to use . instead of · (c2 b7)\n\t\t\tw := 0\n\t\t\tb := s.name\n\t\t\tfor i := 0; i < len(b); i++ {\n\t\t\t\tif b[i] == 0xc2 && i+1 < len(b) && b[i+1] == 0xb7 {\n\t\t\t\t\ti++\n\t\t\t\t\tb[i] = '.'\n\t\t\t\t}\n\t\t\t\tb[w] = b[i]\n\t\t\t\tw++\n\t\t\t}\n\t\t\tts.Name = string(s.name[0:w])\n\t\tcase 'z', 'Z':\n\t\t\tif lasttyp != 'z' && lasttyp != 'Z' {\n\t\t\t\tnz++\n\t\t\t}\n\t\t\tfor i := 0; i < len(s.name); i += 2 {\n\t\t\t\teltIdx := binary.BigEndian.Uint16(s.name[i : i+2])\n\t\t\t\telt, ok := fname[eltIdx]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn &DecodingError{-1, \"bad filename code\", eltIdx}\n\t\t\t\t}\n\t\t\t\tif n := len(ts.Name); n > 0 && ts.Name[n-1] != '/' {\n\t\t\t\t\tts.Name += \"/\"\n\t\t\t\t}\n\t\t\t\tts.Name += elt\n\t\t\t}\n\t\t}\n\t\tswitch s.typ {\n\t\tcase 'T', 't', 'L', 'l':\n\t\t\tnf++\n\t\tcase 'f':\n\t\t\tfname[uint16(s.value)] = ts.Name\n\t\t}\n\t\tlasttyp = s.typ\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tt.Funcs = make([]Func, 0, nf)\n\tt.Files = make(map[string]*Obj)\n\n\tvar obj *Obj\n\tif t.go12line != nil {\n\t\t// Put all functions into one Obj.\n\t\tt.Objs = make([]Obj, 1)\n\t\tobj = &t.Objs[0]\n\t\tt.go12line.go12MapFiles(t.Files, obj)\n\t} else {\n\t\tt.Objs = make([]Obj, 0, nz)\n\t}\n\n\t// Count text symbols and attach frame sizes, parameters, and\n\t// locals to them. Also, find object file boundaries.\n\tlastf := 0\n\tfor i := 0; i < len(t.Syms); i++ {\n\t\tsym := &t.Syms[i]\n\t\tswitch sym.Type {\n\t\tcase 'Z', 'z': // path symbol\n\t\t\tif t.go12line != nil {\n\t\t\t\t// Go 1.2 binaries have the file information elsewhere. Ignore.\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Finish the current object\n\t\t\tif obj != nil {\n\t\t\t\tobj.Funcs = t.Funcs[lastf:]\n\t\t\t}\n\t\t\tlastf = len(t.Funcs)\n\n\t\t\t// Start new object\n\t\t\tn := len(t.Objs)\n\t\t\tt.Objs = t.Objs[0 : n+1]\n\t\t\tobj = &t.Objs[n]\n\n\t\t\t// Count & copy path symbols\n\t\t\tvar end int\n\t\t\tfor end = i + 1; end < len(t.Syms); end++ {\n\t\t\t\tif c := t.Syms[end].Type; c != 'Z' && c != 'z' {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tobj.Paths = t.Syms[i:end]\n\t\t\ti = end - 1 // loop will i++\n\n\t\t\t// Record file names\n\t\t\tdepth := 0\n\t\t\tfor j := range obj.Paths {\n\t\t\t\ts := &obj.Paths[j]\n\t\t\t\tif s.Name == \"\" {\n\t\t\t\t\tdepth--\n\t\t\t\t} else {\n\t\t\t\t\tif depth == 0 {\n\t\t\t\t\t\tt.Files[s.Name] = obj\n\t\t\t\t\t}\n\t\t\t\t\tdepth++\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase 'T', 't', 'L', 'l': // text symbol\n\t\t\tif n := len(t.Funcs); n > 0 {\n\t\t\t\tt.Funcs[n-1].End = sym.Value\n\t\t\t}\n\t\t\tif sym.Name == \"runtime.etext\" || sym.Name == \"etext\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Count parameter and local (auto) syms\n\t\t\tvar np, na int\n\t\t\tvar end int\n\t\tcountloop:\n\t\t\tfor end = i + 1; end < len(t.Syms); end++ {\n\t\t\t\tswitch t.Syms[end].Type {\n\t\t\t\tcase 'T', 't', 'L', 'l', 'Z', 'z':\n\t\t\t\t\tbreak countloop\n\t\t\t\tcase 'p':\n\t\t\t\t\tnp++\n\t\t\t\tcase 'a':\n\t\t\t\t\tna++\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fill in the function symbol\n\t\t\tn := len(t.Funcs)\n\t\t\tt.Funcs = t.Funcs[0 : n+1]\n\t\t\tfn := &t.Funcs[n]\n\t\t\tsym.Func = fn\n\t\t\tfn.Params = make([]*Sym, 0, np)\n\t\t\tfn.Locals = make([]*Sym, 0, na)\n\t\t\tfn.Sym = sym\n\t\t\tfn.Entry = sym.Value\n\t\t\tfn.Obj = obj\n\t\t\tif t.go12line != nil {\n\t\t\t\t// All functions share the same line table.\n\t\t\t\t// It knows how to narrow down to a specific\n\t\t\t\t// function quickly.\n\t\t\t\tfn.LineTable = t.go12line\n\t\t\t} else if pcln != nil {\n\t\t\t\tfn.LineTable = pcln.slice(fn.Entry)\n\t\t\t\tpcln = fn.LineTable\n\t\t\t}\n\t\t\tfor j := i; j < end; j++ {\n\t\t\t\ts := &t.Syms[j]\n\t\t\t\tswitch s.Type {\n\t\t\t\tcase 'm':\n\t\t\t\t\tfn.FrameSize = int(s.Value)\n\t\t\t\tcase 'p':\n\t\t\t\t\tn := len(fn.Params)\n\t\t\t\t\tfn.Params = fn.Params[0 : n+1]\n\t\t\t\t\tfn.Params[n] = s\n\t\t\t\tcase 'a':\n\t\t\t\t\tn := len(fn.Locals)\n\t\t\t\t\tfn.Locals = fn.Locals[0 : n+1]\n\t\t\t\t\tfn.Locals[n] = s\n\t\t\t\t}\n\t\t\t}\n\t\t\ti = end - 1 // loop will i++\n\t\t}\n\t}\n\n\tif t.go12line != nil && nf == 0 {\n\t\tt.Funcs = t.go12line.go12Funcs()\n\t}\n\tif obj != nil {\n\t\tobj.Funcs = t.Funcs[lastf:]\n\t}\n\treturn &t, nil\n}\n\n// PCToFunc returns the function containing the program counter pc,\n// or nil if there is no such function.\nfunc (t *Table) PCToFunc(pc uint64) *Func {\n\tfuncs := t.Funcs\n\tfor len(funcs) > 0 {\n\t\tm := len(funcs) / 2\n\t\tfn := &funcs[m]\n\t\tswitch {\n\t\tcase pc < fn.Entry:\n\t\t\tfuncs = funcs[0:m]\n\t\tcase fn.Entry <= pc && pc < fn.End:\n\t\t\treturn fn\n\t\tdefault:\n\t\t\tfuncs = funcs[m+1:]\n\t\t}\n\t}\n\treturn nil\n}\n\n// PCToLine looks up line number information for a program counter.\n// If there is no information, it returns fn == nil.\nfunc (t *Table) PCToLine(pc uint64) (file string, line int, fn *Func) {\n\tif fn = t.PCToFunc(pc); fn == nil {\n\t\treturn\n\t}\n\tif t.go12line != nil {\n\t\tfile = t.go12line.go12PCToFile(pc, fn)\n\t\tline = t.go12line.go12PCToLine(pc, fn)\n\t} else {\n\t\tfile, line = fn.Obj.lineFromAline(fn.LineTable.PCToLine(pc))\n\t}\n\treturn\n}\n\n// LineToPC looks up the first program counter on the given line in\n// the named file. It returns UnknownPathError or UnknownLineError if\n// there is an error looking up this line.\nfunc (t *Table) LineToPC(file string, line int) (pc uint64, fn *Func, err error) {\n\tobj, ok := t.Files[file]\n\tif !ok {\n\t\treturn 0, nil, UnknownFileError(file)\n\t}\n\n\tif t.go12line != nil {\n\t\tpc := t.go12line.go12LineToPC(file, line)\n\t\tif pc == 0 {\n\t\t\treturn 0, nil, &UnknownLineError{file, line}\n\t\t}\n\t\treturn pc, t.PCToFunc(pc), nil\n\t}\n\n\tabs, err := obj.alineFromLine(file, line)\n\tif err != nil {\n\t\treturn\n\t}\n\tfor i := range obj.Funcs {\n\t\tf := &obj.Funcs[i]\n\t\tpc := f.LineTable.LineToPC(abs, f.End)\n\t\tif pc != 0 {\n\t\t\treturn pc, f, nil\n\t\t}\n\t}\n\treturn 0, nil, &UnknownLineError{file, line}\n}\n\n// LookupSym returns the text, data, or bss symbol with the given name,\n// or nil if no such symbol is found.\nfunc (t *Table) LookupSym(name string) *Sym {\n\t// TODO(austin) Maybe make a map\n\tfor i := range t.Syms {\n\t\ts := &t.Syms[i]\n\t\tswitch s.Type {\n\t\tcase 'T', 't', 'L', 'l', 'D', 'd', 'B', 'b':\n\t\t\tif s.Name == name {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// LookupFunc returns the text, data, or bss symbol with the given name,\n// or nil if no such symbol is found.\nfunc (t *Table) LookupFunc(name string) *Func {\n\tfor i := range t.Funcs {\n\t\tf := &t.Funcs[i]\n\t\tif f.Sym.Name == name {\n\t\t\treturn f\n\t\t}\n\t}\n\treturn nil\n}\n\n// SymByAddr returns the text, data, or bss symbol starting at the given address.\nfunc (t *Table) SymByAddr(addr uint64) *Sym {\n\tfor i := range t.Syms {\n\t\ts := &t.Syms[i]\n\t\tswitch s.Type {\n\t\tcase 'T', 't', 'L', 'l', 'D', 'd', 'B', 'b':\n\t\t\tif s.Value == addr {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n/*\n * Object files\n */\n\n// This is legacy code for Go 1.1 and earlier, which used the\n// Plan 9 format for pc-line tables. This code was never quite\n// correct. It's probably very close, and it's usually correct, but\n// we never quite found all the corner cases.\n//\n// Go 1.2 and later use a simpler format, documented at golang.org/s/go12symtab.\n\nfunc (o *Obj) lineFromAline(aline int) (string, int) {\n\ttype stackEnt struct {\n\t\tpath   string\n\t\tstart  int\n\t\toffset int\n\t\tprev   *stackEnt\n\t}\n\n\tnoPath := &stackEnt{\"\", 0, 0, nil}\n\ttos := noPath\n\npathloop:\n\tfor _, s := range o.Paths {\n\t\tval := int(s.Value)\n\t\tswitch {\n\t\tcase val > aline:\n\t\t\tbreak pathloop\n\n\t\tcase val == 1:\n\t\t\t// Start a new stack\n\t\t\ttos = &stackEnt{s.Name, val, 0, noPath}\n\n\t\tcase s.Name == \"\":\n\t\t\t// Pop\n\t\t\tif tos == noPath {\n\t\t\t\treturn \"<malformed symbol table>\", 0\n\t\t\t}\n\t\t\ttos.prev.offset += val - tos.start\n\t\t\ttos = tos.prev\n\n\t\tdefault:\n\t\t\t// Push\n\t\t\ttos = &stackEnt{s.Name, val, 0, tos}\n\t\t}\n\t}\n\n\tif tos == noPath {\n\t\treturn \"\", 0\n\t}\n\treturn tos.path, aline - tos.start - tos.offset + 1\n}\n\nfunc (o *Obj) alineFromLine(path string, line int) (int, error) {\n\tif line < 1 {\n\t\treturn 0, &UnknownLineError{path, line}\n\t}\n\n\tfor i, s := range o.Paths {\n\t\t// Find this path\n\t\tif s.Name != path {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Find this line at this stack level\n\t\tdepth := 0\n\t\tvar incstart int\n\t\tline += int(s.Value)\n\tpathloop:\n\t\tfor _, s := range o.Paths[i:] {\n\t\t\tval := int(s.Value)\n\t\t\tswitch {\n\t\t\tcase depth == 1 && val >= line:\n\t\t\t\treturn line - 1, nil\n\n\t\t\tcase s.Name == \"\":\n\t\t\t\tdepth--\n\t\t\t\tif depth == 0 {\n\t\t\t\t\tbreak pathloop\n\t\t\t\t} else if depth == 1 {\n\t\t\t\t\tline += val - incstart\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\tif depth == 1 {\n\t\t\t\t\tincstart = val\n\t\t\t\t}\n\t\t\t\tdepth++\n\t\t\t}\n\t\t}\n\t\treturn 0, &UnknownLineError{path, line}\n\t}\n\treturn 0, UnknownFileError(path)\n}\n\n/*\n * Errors\n */\n\n// UnknownFileError represents a failure to find the specific file in\n// the symbol table.\ntype UnknownFileError string\n\nfunc (e UnknownFileError) Error() string { return \"unknown file: \" + string(e) }\n\n// UnknownLineError represents a failure to map a line to a program\n// counter, either because the line is beyond the bounds of the file\n// or because there is no code on the given line.\ntype UnknownLineError struct {\n\tFile string\n\tLine int\n}\n\nfunc (e *UnknownLineError) Error() string {\n\treturn \"no code at \" + e.File + \":\" + strconv.Itoa(e.Line)\n}\n\n// DecodingError represents an error during the decoding of\n// the symbol table.\ntype DecodingError struct {\n\toff int\n\tmsg string\n\tval interface{}\n}\n\nfunc (e *DecodingError) Error() string {\n\tmsg := e.msg\n\tif e.val != nil {\n\t\tmsg += fmt.Sprintf(\" '%v'\", e.val)\n\t}\n\tmsg += fmt.Sprintf(\" at byte %#x\", e.off)\n\treturn msg\n}\n"
  },
  {
    "path": "cli/internal/gosym/symtab_test.go",
    "content": "// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage gosym\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc assertString(t *testing.T, dsc, out, tgt string) {\n\tif out != tgt {\n\t\tt.Fatalf(\"Expected: %q Actual: %q for %s\", tgt, out, dsc)\n\t}\n}\n\nfunc TestStandardLibPackage(t *testing.T) {\n\ts1 := Sym{Name: \"io.(*LimitedReader).Read\"}\n\ts2 := Sym{Name: \"io.NewSectionReader\"}\n\tassertString(t, fmt.Sprintf(\"package of %q\", s1.Name), s1.PackageName(), \"io\")\n\tassertString(t, fmt.Sprintf(\"package of %q\", s2.Name), s2.PackageName(), \"io\")\n\tassertString(t, fmt.Sprintf(\"receiver of %q\", s1.Name), s1.ReceiverName(), \"(*LimitedReader)\")\n\tassertString(t, fmt.Sprintf(\"receiver of %q\", s2.Name), s2.ReceiverName(), \"\")\n}\n\nfunc TestStandardLibPathPackage(t *testing.T) {\n\ts1 := Sym{Name: \"debug/gosym.(*LineTable).PCToLine\"}\n\ts2 := Sym{Name: \"debug/gosym.NewTable\"}\n\tassertString(t, fmt.Sprintf(\"package of %q\", s1.Name), s1.PackageName(), \"debug/gosym\")\n\tassertString(t, fmt.Sprintf(\"package of %q\", s2.Name), s2.PackageName(), \"debug/gosym\")\n\tassertString(t, fmt.Sprintf(\"receiver of %q\", s1.Name), s1.ReceiverName(), \"(*LineTable)\")\n\tassertString(t, fmt.Sprintf(\"receiver of %q\", s2.Name), s2.ReceiverName(), \"\")\n}\n\nfunc TestRemotePackage(t *testing.T) {\n\ts1 := Sym{Name: \"github.com/docker/doc.ker/pkg/mflag.(*FlagSet).PrintDefaults\"}\n\ts2 := Sym{Name: \"github.com/docker/doc.ker/pkg/mflag.PrintDefaults\"}\n\tassertString(t, fmt.Sprintf(\"package of %q\", s1.Name), s1.PackageName(), \"github.com/docker/doc.ker/pkg/mflag\")\n\tassertString(t, fmt.Sprintf(\"package of %q\", s2.Name), s2.PackageName(), \"github.com/docker/doc.ker/pkg/mflag\")\n\tassertString(t, fmt.Sprintf(\"receiver of %q\", s1.Name), s1.ReceiverName(), \"(*FlagSet)\")\n\tassertString(t, fmt.Sprintf(\"receiver of %q\", s2.Name), s2.ReceiverName(), \"\")\n}\n\nfunc TestIssue29551(t *testing.T) {\n\tsymNames := []string{\n\t\t\"type..eq.[9]debug/elf.intName\",\n\t\t\"type..hash.debug/elf.ProgHeader\",\n\t\t\"type..eq.runtime._panic\",\n\t\t\"type..hash.struct { runtime.gList; runtime.n int32 }\",\n\t\t\"go.(*struct { sync.Mutex; math/big.table [64]math/big\",\n\t}\n\n\tfor _, symName := range symNames {\n\t\ts := Sym{Name: symName}\n\t\tassertString(t, fmt.Sprintf(\"package of %q\", s.Name), s.PackageName(), \"\")\n\t}\n}\n"
  },
  {
    "path": "cli/internal/gosym/testdata/main.go",
    "content": "package main\n\nfunc linefrompc()\nfunc pcfromline()\n\nfunc main() {\n\t// Prevent GC of our test symbols\n\tlinefrompc()\n\tpcfromline()\n}\n"
  },
  {
    "path": "cli/internal/gosym/testdata/pclinetest.h",
    "content": "// +build ignore\n\n// Empty include file to generate z symbols\n\n\n\n\n\n// EOF\n"
  },
  {
    "path": "cli/internal/gosym/testdata/pclinetest.s",
    "content": "TEXT ·linefrompc(SB),4,$0\t// Each byte stores its line delta\nBYTE $2;\nBYTE $1;\nBYTE $1; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1;\nBYTE $1;\nBYTE $1; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\nBYTE $1; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\n#include \"pclinetest.h\"\nBYTE $2;\n#include \"pclinetest.h\"\nBYTE $2;\nBYTE $255;\n\nTEXT ·pcfromline(SB),4,$0\t// Each record stores its line delta, then n, then n more bytes\nBYTE $32; BYTE $0;\nBYTE $1; BYTE $1; BYTE $0;\nBYTE $1; BYTE $0;\n\nBYTE $2; BYTE $4; BYTE $0; BYTE $0; BYTE $0; BYTE $0;\n\n\n#include \"pclinetest.h\"\nBYTE $4; BYTE $0;\n\n\nBYTE $3; BYTE $3; BYTE $0; BYTE $0; BYTE $0;\n#include \"pclinetest.h\"\n\n\nBYTE $4; BYTE $3; BYTE $0; BYTE $0; BYTE $0;\nBYTE $255;\n"
  },
  {
    "path": "cli/internal/jsonrpc2/conn.go",
    "content": "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n// Conn is the common interface to jsonrpc clients and servers.\n// Conn is bidirectional; it does not have a designated server or client end.\n// It manages the jsonrpc2 protocol, connecting responses back to their calls.\ntype Conn interface {\n\t// Call invokes the target method and waits for a response.\n\t// The params will be marshaled to JSON before sending over the wire, and will\n\t// be handed to the method invoked.\n\t// The response will be unmarshaled from JSON into the result.\n\t// The id returned will be unique from this connection, and can be used for\n\t// logging or tracking.\n\tCall(ctx context.Context, method string, params, result interface{}) (ID, error)\n\n\t// Notify invokes the target method but does not wait for a response.\n\t// The params will be marshaled to JSON before sending over the wire, and will\n\t// be handed to the method invoked.\n\tNotify(ctx context.Context, method string, params interface{}) error\n\n\t// Go starts a goroutine to handle the connection.\n\t// It must be called exactly once for each Conn.\n\t// It returns immediately.\n\t// You must block on Done() to wait for the connection to shut down.\n\t// This is a temporary measure, this should be started automatically in the\n\t// future.\n\tGo(ctx context.Context, handler Handler)\n\n\t// Close closes the connection and it's underlying stream.\n\t// It does not wait for the close to complete, use the Done() channel for\n\t// that.\n\tClose() error\n\n\t// Done returns a channel that will be closed when the processing goroutine\n\t// has terminated, which will happen if Close() is called or an underlying\n\t// stream is closed.\n\tDone() <-chan struct{}\n\n\t// Err returns an error if there was one from within the processing goroutine.\n\t// If err returns non nil, the connection will be already closed or closing.\n\tErr() error\n}\n\ntype conn struct {\n\tseq       int64      // must only be accessed using atomic operations\n\twriteMu   sync.Mutex // protects writes to the stream\n\tstream    Stream\n\tpendingMu sync.Mutex // protects the pending map\n\tpending   map[ID]chan *Response\n\n\tdone chan struct{}\n\terr  atomic.Value\n}\n\n// NewConn creates a new connection object around the supplied stream.\nfunc NewConn(s Stream) Conn {\n\tconn := &conn{\n\t\tstream:  s,\n\t\tpending: make(map[ID]chan *Response),\n\t\tdone:    make(chan struct{}),\n\t}\n\treturn conn\n}\n\nfunc (c *conn) Notify(ctx context.Context, method string, params interface{}) (err error) {\n\tnotify, err := NewNotification(method, params)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshaling notify parameters: %v\", err)\n\t}\n\n\t_, err = c.write(ctx, notify)\n\treturn err\n}\n\nfunc (c *conn) Call(ctx context.Context, method string, params, result interface{}) (_ ID, err error) {\n\t// generate a new request identifier\n\tid := ID{number: atomic.AddInt64(&c.seq, 1)}\n\tcall, err := NewCall(id, method, params)\n\tif err != nil {\n\t\treturn id, fmt.Errorf(\"marshaling call parameters: %v\", err)\n\t}\n\t// We have to add ourselves to the pending map before we send, otherwise we\n\t// are racing the response. Also add a buffer to rchan, so that if we get a\n\t// wire response between the time this call is cancelled and id is deleted\n\t// from c.pending, the send to rchan will not block.\n\trchan := make(chan *Response, 1)\n\tc.pendingMu.Lock()\n\tc.pending[id] = rchan\n\tc.pendingMu.Unlock()\n\tdefer func() {\n\t\tc.pendingMu.Lock()\n\t\tdelete(c.pending, id)\n\t\tc.pendingMu.Unlock()\n\t}()\n\t// now we are ready to send\n\t_, err = c.write(ctx, call)\n\tif err != nil {\n\t\t// sending failed, we will never get a response, so don't leave it pending\n\t\treturn id, err\n\t}\n\t// now wait for the response\n\tselect {\n\tcase response := <-rchan:\n\t\t// is it an error response?\n\t\tif response.err != nil {\n\t\t\treturn id, response.err\n\t\t}\n\t\tif result == nil || len(response.result) == 0 {\n\t\t\treturn id, nil\n\t\t}\n\t\tif err := json.Unmarshal(response.result, result); err != nil {\n\t\t\treturn id, fmt.Errorf(\"unmarshaling result: %v\", err)\n\t\t}\n\t\treturn id, nil\n\tcase <-ctx.Done():\n\t\treturn id, ctx.Err()\n\t}\n}\n\nfunc (c *conn) replier(req Request) Replier {\n\treturn func(ctx context.Context, result interface{}, err error) error {\n\t\tcall, ok := req.(*Call)\n\t\tif !ok {\n\t\t\t// request was a notify, no need to respond\n\t\t\treturn nil\n\t\t}\n\t\tresponse, err := NewResponse(call.id, result, err)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = c.write(ctx, response)\n\t\tif err != nil {\n\t\t\t// TODO(iancottrell): if a stream write fails, we really need to shut down\n\t\t\t// the whole stream\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc (c *conn) write(ctx context.Context, msg Message) (int64, error) {\n\tc.writeMu.Lock()\n\tdefer c.writeMu.Unlock()\n\treturn c.stream.Write(ctx, msg)\n}\n\nfunc (c *conn) Go(ctx context.Context, handler Handler) {\n\tgo c.run(ctx, handler)\n}\n\nfunc (c *conn) run(ctx context.Context, handler Handler) {\n\tdefer close(c.done)\n\tfor {\n\t\t// get the next message\n\t\tmsg, _, err := c.stream.Read(ctx)\n\t\tif err != nil {\n\t\t\t// The stream failed, we cannot continue.\n\t\t\tc.fail(err)\n\t\t\treturn\n\t\t}\n\t\tswitch msg := msg.(type) {\n\t\tcase Request:\n\t\t\tif err := handler(ctx, c.replier(msg), msg); err != nil {\n\t\t\t\t// delivery failed, not much we can do\n\t\t\t\tlog.Error().Err(err).Msg(\"jsonrpc2: message delivery failed\")\n\t\t\t}\n\t\tcase *Response:\n\t\t\t// If method is not set, this should be a response, in which case we must\n\t\t\t// have an id to send the response back to the caller.\n\t\t\tc.pendingMu.Lock()\n\t\t\trchan, ok := c.pending[msg.id]\n\t\t\tc.pendingMu.Unlock()\n\t\t\tif ok {\n\t\t\t\trchan <- msg\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *conn) Close() error {\n\treturn c.stream.Close()\n}\n\nfunc (c *conn) Done() <-chan struct{} {\n\treturn c.done\n}\n\nfunc (c *conn) Err() error {\n\tif err := c.err.Load(); err != nil {\n\t\treturn err.(error)\n\t}\n\treturn nil\n}\n\n// fail sets a failure condition on the stream and closes it.\nfunc (c *conn) fail(err error) {\n\tc.err.Store(err)\n\tc.stream.Close()\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/handler.go",
    "content": "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n// Handler is invoked to handle incoming requests.\n// The Replier sends a reply to the request and must be called exactly once.\ntype Handler func(ctx context.Context, reply Replier, req Request) error\n\n// Replier is passed to handlers to allow them to reply to the request.\n// If err is set then result will be ignored.\ntype Replier func(ctx context.Context, result interface{}, err error) error\n\n// MethodNotFound is a Handler that replies to all call requests with the\n// standard method not found response.\n// This should normally be the final handler in a chain.\nfunc MethodNotFound(ctx context.Context, reply Replier, req Request) error {\n\treturn reply(ctx, nil, fmt.Errorf(\"%w: %q\", ErrMethodNotFound, req.Method()))\n}\n\n// MustReplyHandler creates a Handler that panics if the wrapped handler does\n// not call Reply for every request that it is passed.\nfunc MustReplyHandler(handler Handler) Handler {\n\treturn func(ctx context.Context, reply Replier, req Request) error {\n\t\tcalled := false\n\t\terr := handler(ctx, func(ctx context.Context, result interface{}, err error) error {\n\t\t\tif called {\n\t\t\t\tpanic(fmt.Errorf(\"request %q replied to more than once\", req.Method()))\n\t\t\t}\n\t\t\tcalled = true\n\t\t\treturn reply(ctx, result, err)\n\t\t}, req)\n\t\tif !called {\n\t\t\tpanic(fmt.Errorf(\"request %q was never replied to\", req.Method()))\n\t\t}\n\t\treturn err\n\t}\n}\n\n// CancelHandler returns a handler that supports cancellation, and a function\n// that can be used to trigger canceling in progress requests.\nfunc CancelHandler(handler Handler) (Handler, func(id ID)) {\n\tvar mu sync.Mutex\n\thandling := make(map[ID]context.CancelFunc)\n\twrapped := func(ctx context.Context, reply Replier, req Request) error {\n\t\tif call, ok := req.(*Call); ok {\n\t\t\tcancelCtx, cancel := context.WithCancel(ctx)\n\t\t\tctx = cancelCtx\n\t\t\tmu.Lock()\n\t\t\thandling[call.ID()] = cancel\n\t\t\tmu.Unlock()\n\t\t\tinnerReply := reply\n\t\t\treply = func(ctx context.Context, result interface{}, err error) error {\n\t\t\t\tmu.Lock()\n\t\t\t\tdelete(handling, call.ID())\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn innerReply(ctx, result, err)\n\t\t\t}\n\t\t}\n\t\treturn handler(ctx, reply, req)\n\t}\n\treturn wrapped, func(id ID) {\n\t\tmu.Lock()\n\t\tcancel, found := handling[id]\n\t\tmu.Unlock()\n\t\tif found {\n\t\t\tcancel()\n\t\t}\n\t}\n}\n\n// AsyncHandler returns a handler that processes each request goes in its own\n// goroutine.\n// The handler returns immediately, without the request being processed.\n// Each request then waits for the previous request to finish before it starts.\n// This allows the stream to unblock at the cost of unbounded goroutines\n// all stalled on the previous one.\nfunc AsyncHandler(handler Handler) Handler {\n\tnextRequest := make(chan struct{})\n\tclose(nextRequest)\n\treturn func(ctx context.Context, reply Replier, req Request) error {\n\t\twaitForPrevious := nextRequest\n\t\tnextRequest = make(chan struct{})\n\t\tunlockNext := nextRequest\n\t\tinnerReply := reply\n\t\treply = func(ctx context.Context, result interface{}, err error) error {\n\t\t\tclose(unlockNext)\n\t\t\treturn innerReply(ctx, result, err)\n\t\t}\n\t\tgo func() {\n\t\t\t<-waitForPrevious\n\t\t\tif err := handler(ctx, reply, req); err != nil {\n\t\t\t\tlog.Error().Err(err).Msg(\"jsonrpc2: async message delivery failed\")\n\t\t\t}\n\t\t}()\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/jsonrpc2.go",
    "content": "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package jsonrpc2 is a minimal implementation of the JSON RPC 2 spec.\n// https://www.jsonrpc.org/specification\n// It is intended to be compatible with other implementations at the wire level.\npackage jsonrpc2\n\nconst (\n\t// ErrIdleTimeout is returned when serving timed out waiting for new connections.\n\tErrIdleTimeout = constError(\"timed out waiting for new connections\")\n)\n\ntype constError string\n\nfunc (e constError) Error() string { return string(e) }\n"
  },
  {
    "path": "cli/internal/jsonrpc2/jsonrpc2_test.go",
    "content": "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"path\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"encr.dev/cli/internal/jsonrpc2\"\n)\n\nvar logRPC = flag.Bool(\"logrpc\", false, \"Enable jsonrpc2 communication logging\")\n\ntype callTest struct {\n\tmethod string\n\tparams interface{}\n\texpect interface{}\n}\n\nvar callTests = []callTest{\n\t{\"no_args\", nil, true},\n\t{\"one_string\", \"fish\", \"got:fish\"},\n\t{\"one_number\", 10, \"got:10\"},\n\t{\"join\", []string{\"a\", \"b\", \"c\"}, \"a/b/c\"},\n\t// TODO: expand the test cases\n}\n\nfunc (test *callTest) newResults() interface{} {\n\tswitch e := test.expect.(type) {\n\tcase []interface{}:\n\t\tvar r []interface{}\n\t\tfor _, v := range e {\n\t\t\tr = append(r, reflect.New(reflect.TypeOf(v)).Interface())\n\t\t}\n\t\treturn r\n\tcase nil:\n\t\treturn nil\n\tdefault:\n\t\treturn reflect.New(reflect.TypeOf(test.expect)).Interface()\n\t}\n}\n\nfunc (test *callTest) verifyResults(t *testing.T, results interface{}) {\n\tif results == nil {\n\t\treturn\n\t}\n\tval := reflect.Indirect(reflect.ValueOf(results)).Interface()\n\tif !reflect.DeepEqual(val, test.expect) {\n\t\tt.Errorf(\"%v:Results are incorrect, got %+v expect %+v\", test.method, val, test.expect)\n\t}\n}\n\nfunc TestCall(t *testing.T) {\n\tctx := context.Background()\n\tfor _, headers := range []bool{false, true} {\n\t\tname := \"Plain\"\n\t\tif headers {\n\t\t\tname = \"Headers\"\n\t\t}\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ta, b, done := prepare(ctx, t, headers)\n\t\t\tdefer done()\n\t\t\tfor _, test := range callTests {\n\t\t\t\tt.Run(test.method, func(t *testing.T) {\n\t\t\t\t\tresults := test.newResults()\n\t\t\t\t\tif _, err := a.Call(ctx, test.method, test.params, results); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"%v:Call failed: %v\", test.method, err)\n\t\t\t\t\t}\n\t\t\t\t\ttest.verifyResults(t, results)\n\t\t\t\t\tif _, err := b.Call(ctx, test.method, test.params, results); err != nil {\n\t\t\t\t\t\tt.Fatalf(\"%v:Call failed: %v\", test.method, err)\n\t\t\t\t\t}\n\t\t\t\t\ttest.verifyResults(t, results)\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc prepare(ctx context.Context, t *testing.T, withHeaders bool) (jsonrpc2.Conn, jsonrpc2.Conn, func()) {\n\t// make a wait group that can be used to wait for the system to shut down\n\taPipe, bPipe := net.Pipe()\n\ta := run(ctx, withHeaders, aPipe)\n\tb := run(ctx, withHeaders, bPipe)\n\treturn a, b, func() {\n\t\ta.Close()\n\t\tb.Close()\n\t\t<-a.Done()\n\t\t<-b.Done()\n\t}\n}\n\nfunc run(ctx context.Context, withHeaders bool, nc net.Conn) jsonrpc2.Conn {\n\tvar stream jsonrpc2.Stream\n\tif withHeaders {\n\t\tstream = jsonrpc2.NewHeaderStream(nc)\n\t} else {\n\t\tstream = jsonrpc2.NewRawStream(nc)\n\t}\n\tconn := jsonrpc2.NewConn(stream)\n\tconn.Go(ctx, testHandler(*logRPC))\n\treturn conn\n}\n\nfunc testHandler(log bool) jsonrpc2.Handler {\n\treturn func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {\n\t\tswitch req.Method() {\n\t\tcase \"no_args\":\n\t\t\tif len(req.Params()) > 0 {\n\t\t\t\treturn reply(ctx, nil, fmt.Errorf(\"%w: expected no params\", jsonrpc2.ErrInvalidParams))\n\t\t\t}\n\t\t\treturn reply(ctx, true, nil)\n\t\tcase \"one_string\":\n\t\t\tvar v string\n\t\t\tif err := json.Unmarshal(req.Params(), &v); err != nil {\n\t\t\t\treturn reply(ctx, nil, fmt.Errorf(\"%w: %s\", jsonrpc2.ErrParse, err))\n\t\t\t}\n\t\t\treturn reply(ctx, \"got:\"+v, nil)\n\t\tcase \"one_number\":\n\t\t\tvar v int\n\t\t\tif err := json.Unmarshal(req.Params(), &v); err != nil {\n\t\t\t\treturn reply(ctx, nil, fmt.Errorf(\"%w: %s\", jsonrpc2.ErrParse, err))\n\t\t\t}\n\t\t\treturn reply(ctx, fmt.Sprintf(\"got:%d\", v), nil)\n\t\tcase \"join\":\n\t\t\tvar v []string\n\t\t\tif err := json.Unmarshal(req.Params(), &v); err != nil {\n\t\t\t\treturn reply(ctx, nil, fmt.Errorf(\"%w: %s\", jsonrpc2.ErrParse, err))\n\t\t\t}\n\t\t\treturn reply(ctx, path.Join(v...), nil)\n\t\tdefault:\n\t\t\treturn jsonrpc2.MethodNotFound(ctx, reply, req)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/messages.go",
    "content": "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n)\n\n// Message is the interface to all jsonrpc2 message types.\n// They share no common functionality, but are a closed set of concrete types\n// that are allowed to implement this interface. The message types are *Call,\n// *Notification and *Response.\ntype Message interface {\n\t// isJSONRPC2Message is used to make the set of message implementations a\n\t// closed set.\n\tisJSONRPC2Message()\n}\n\n// Request is the shared interface to jsonrpc2 messages that request\n// a method be invoked.\n// The request types are a closed set of *Call and *Notification.\ntype Request interface {\n\tMessage\n\t// Method is a string containing the method name to invoke.\n\tMethod() string\n\t// Params is either a struct or an array with the parameters of the method.\n\tParams() json.RawMessage\n\t// isJSONRPC2Request is used to make the set of request implementations closed.\n\tisJSONRPC2Request()\n}\n\n// Notification is a request for which a response cannot occur, and as such\n// it has not ID.\ntype Notification struct {\n\t// Method is a string containing the method name to invoke.\n\tmethod string\n\tparams json.RawMessage\n}\n\n// Call is a request that expects a response.\n// The response will have a matching ID.\ntype Call struct {\n\t// Method is a string containing the method name to invoke.\n\tmethod string\n\t// Params is either a struct or an array with the parameters of the method.\n\tparams json.RawMessage\n\t// id of this request, used to tie the Response back to the request.\n\tid ID\n}\n\n// Response is a reply to a Call.\n// It will have the same ID as the call it is a response to.\ntype Response struct {\n\t// result is the content of the response.\n\tresult json.RawMessage\n\t// err is set only if the call failed.\n\terr error\n\t// ID of the request this is a response to.\n\tid ID\n}\n\n// NewNotification constructs a new Notification message for the supplied\n// method and parameters.\nfunc NewNotification(method string, params interface{}) (*Notification, error) {\n\tp, merr := marshalToRaw(params)\n\treturn &Notification{method: method, params: p}, merr\n}\n\nfunc (msg *Notification) Method() string          { return msg.method }\nfunc (msg *Notification) Params() json.RawMessage { return msg.params }\nfunc (msg *Notification) isJSONRPC2Message()      {}\nfunc (msg *Notification) isJSONRPC2Request()      {}\n\nfunc (n *Notification) MarshalJSON() ([]byte, error) {\n\tmsg := wireRequest{Method: n.method, Params: &n.params}\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn data, fmt.Errorf(\"marshaling notification: %w\", err)\n\t}\n\treturn data, nil\n}\n\nfunc (n *Notification) UnmarshalJSON(data []byte) error {\n\tmsg := wireRequest{}\n\tif err := json.Unmarshal(data, &msg); err != nil {\n\t\treturn fmt.Errorf(\"unmarshaling notification: %w\", err)\n\t}\n\tn.method = msg.Method\n\tif msg.Params != nil {\n\t\tn.params = *msg.Params\n\t}\n\treturn nil\n}\n\n// NewCall constructs a new Call message for the supplied ID, method and\n// parameters.\nfunc NewCall(id ID, method string, params interface{}) (*Call, error) {\n\tp, merr := marshalToRaw(params)\n\treturn &Call{id: id, method: method, params: p}, merr\n}\n\nfunc (msg *Call) Method() string          { return msg.method }\nfunc (msg *Call) Params() json.RawMessage { return msg.params }\nfunc (msg *Call) ID() ID                  { return msg.id }\nfunc (msg *Call) isJSONRPC2Message()      {}\nfunc (msg *Call) isJSONRPC2Request()      {}\n\nfunc (c *Call) MarshalJSON() ([]byte, error) {\n\tmsg := wireRequest{Method: c.method, Params: &c.params, ID: &c.id}\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn data, fmt.Errorf(\"marshaling call: %w\", err)\n\t}\n\treturn data, nil\n}\n\nfunc (c *Call) UnmarshalJSON(data []byte) error {\n\tmsg := wireRequest{}\n\tif err := json.Unmarshal(data, &msg); err != nil {\n\t\treturn fmt.Errorf(\"unmarshaling call: %w\", err)\n\t}\n\tc.method = msg.Method\n\tif msg.Params != nil {\n\t\tc.params = *msg.Params\n\t}\n\tif msg.ID != nil {\n\t\tc.id = *msg.ID\n\t}\n\treturn nil\n}\n\n// NewResponse constructs a new Response message that is a reply to the\n// supplied. If err is set result may be ignored.\nfunc NewResponse(id ID, result interface{}, err error) (*Response, error) {\n\tr, merr := marshalToRaw(result)\n\treturn &Response{id: id, result: r, err: err}, merr\n}\n\nfunc (msg *Response) ID() ID                  { return msg.id }\nfunc (msg *Response) Result() json.RawMessage { return msg.result }\nfunc (msg *Response) Err() error              { return msg.err }\nfunc (msg *Response) isJSONRPC2Message()      {}\n\nfunc (r *Response) MarshalJSON() ([]byte, error) {\n\tmsg := &wireResponse{Error: toWireError(r.err), ID: &r.id}\n\tif msg.Error == nil {\n\t\tmsg.Result = &r.result\n\t}\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn data, fmt.Errorf(\"marshaling notification: %w\", err)\n\t}\n\treturn data, nil\n}\n\nfunc toWireError(err error) *wireError {\n\tif err == nil {\n\t\t// no error, the response is complete\n\t\treturn nil\n\t}\n\tif err, ok := err.(*wireError); ok {\n\t\t// already a wire error, just use it\n\t\treturn err\n\t}\n\tresult := &wireError{Message: err.Error()}\n\tvar wrapped *wireError\n\tif errors.As(err, &wrapped) {\n\t\t// if we wrapped a wire error, keep the code from the wrapped error\n\t\t// but the message from the outer error\n\t\tresult.Code = wrapped.Code\n\t}\n\treturn result\n}\n\nfunc (r *Response) UnmarshalJSON(data []byte) error {\n\tmsg := wireResponse{}\n\tif err := json.Unmarshal(data, &msg); err != nil {\n\t\treturn fmt.Errorf(\"unmarshaling jsonrpc response: %w\", err)\n\t}\n\tif msg.Result != nil {\n\t\tr.result = *msg.Result\n\t}\n\tif msg.Error != nil {\n\t\tr.err = msg.Error\n\t}\n\tif msg.ID != nil {\n\t\tr.id = *msg.ID\n\t}\n\treturn nil\n}\n\nfunc DecodeMessage(data []byte) (Message, error) {\n\tmsg := wireCombined{}\n\tif err := json.Unmarshal(data, &msg); err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshaling jsonrpc message: %w\", err)\n\t}\n\tif msg.Method == \"\" {\n\t\t// no method, should be a response\n\t\tif msg.ID == nil {\n\t\t\treturn nil, ErrInvalidRequest\n\t\t}\n\t\tresponse := &Response{id: *msg.ID}\n\t\tif msg.Error != nil {\n\t\t\tresponse.err = msg.Error\n\t\t}\n\t\tif msg.Result != nil {\n\t\t\tresponse.result = *msg.Result\n\t\t}\n\t\treturn response, nil\n\t}\n\t// has a method, must be a request\n\tif msg.ID == nil {\n\t\t// request with no ID is a notify\n\t\tnotify := &Notification{method: msg.Method}\n\t\tif msg.Params != nil {\n\t\t\tnotify.params = *msg.Params\n\t\t}\n\t\treturn notify, nil\n\t}\n\t// request with an ID, must be a call\n\tcall := &Call{method: msg.Method, id: *msg.ID}\n\tif msg.Params != nil {\n\t\tcall.params = *msg.Params\n\t}\n\treturn call, nil\n}\n\nfunc marshalToRaw(obj interface{}) (json.RawMessage, error) {\n\tdata, err := json.Marshal(obj)\n\tif err != nil {\n\t\treturn json.RawMessage{}, err\n\t}\n\treturn json.RawMessage(data), nil\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/serve.go",
    "content": "// Copyright 2020 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n// NOTE: This file provides an experimental API for serving multiple remote\n// jsonrpc2 clients over the network. For now, it is intentionally similar to\n// net/http, but that may change in the future as we figure out the correct\n// semantics.\n\n// A StreamServer is used to serve incoming jsonrpc2 clients communicating over\n// a newly created connection.\ntype StreamServer interface {\n\tServeStream(context.Context, Conn) error\n}\n\n// The ServerFunc type is an adapter that implements the StreamServer interface\n// using an ordinary function.\ntype ServerFunc func(context.Context, Conn) error\n\n// ServeStream calls f(ctx, s).\nfunc (f ServerFunc) ServeStream(ctx context.Context, c Conn) error {\n\treturn f(ctx, c)\n}\n\n// HandlerServer returns a StreamServer that handles incoming streams using the\n// provided handler.\nfunc HandlerServer(h Handler) StreamServer {\n\treturn ServerFunc(func(ctx context.Context, conn Conn) error {\n\t\tconn.Go(ctx, h)\n\t\t<-conn.Done()\n\t\treturn conn.Err()\n\t})\n}\n\n// ListenAndServe starts an jsonrpc2 server on the given address.  If\n// idleTimeout is non-zero, ListenAndServe exits after there are no clients for\n// this duration, otherwise it exits only on error.\nfunc ListenAndServe(ctx context.Context, network, addr string, server StreamServer, idleTimeout time.Duration) error {\n\tln, err := net.Listen(network, addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer ln.Close()\n\tif network == \"unix\" {\n\t\tdefer os.Remove(addr)\n\t}\n\treturn Serve(ctx, ln, server, idleTimeout)\n}\n\n// Serve accepts incoming connections from the network, and handles them using\n// the provided server. If idleTimeout is non-zero, ListenAndServe exits after\n// there are no clients for this duration, otherwise it exits only on error.\nfunc Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeout time.Duration) error {\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\t// Max duration: ~290 years; surely that's long enough.\n\tconst forever = 1<<63 - 1\n\tif idleTimeout <= 0 {\n\t\tidleTimeout = forever\n\t}\n\tconnTimer := time.NewTimer(idleTimeout)\n\n\tnewConns := make(chan net.Conn)\n\tdoneListening := make(chan error)\n\tclosedConns := make(chan error)\n\n\tgo func() {\n\t\tfor {\n\t\t\tnc, err := ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase doneListening <- fmt.Errorf(\"Accept(): %w\", err):\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tnewConns <- nc\n\t\t}\n\t}()\n\n\tactiveConns := 0\n\tfor {\n\t\tselect {\n\t\tcase netConn := <-newConns:\n\t\t\tactiveConns++\n\t\t\tconnTimer.Stop()\n\t\t\tstream := NewHeaderStream(netConn)\n\t\t\tgo func() {\n\t\t\t\tconn := NewConn(stream)\n\t\t\t\tclosedConns <- server.ServeStream(ctx, conn)\n\t\t\t\tstream.Close()\n\t\t\t}()\n\t\tcase err := <-doneListening:\n\t\t\treturn err\n\t\tcase err := <-closedConns:\n\t\t\tif !isClosingError(err) {\n\t\t\t\tlog.Error().Err(err).Msg(\"jsonrpc2: closed connection due to error\")\n\t\t\t}\n\t\t\tactiveConns--\n\t\t\tif activeConns == 0 {\n\t\t\t\tconnTimer.Reset(idleTimeout)\n\t\t\t}\n\t\tcase <-connTimer.C:\n\t\t\treturn ErrIdleTimeout\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\t}\n\t}\n}\n\n// isClosingError reports if the error occurs normally during the process of\n// closing a network connection. It uses imperfect heuristics that err on the\n// side of false negatives, and should not be used for anything critical.\nfunc isClosingError(err error) bool {\n\tif errors.Is(err, io.EOF) {\n\t\treturn true\n\t}\n\t// Per https://github.com/golang/go/issues/4373, this error string should not\n\t// change. This is not ideal, but since the worst that could happen here is\n\t// some superfluous logging, it is acceptable.\n\tif err.Error() == \"use of closed network connection\" {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/serve_test.go",
    "content": "// Copyright 2020 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestIdleTimeout(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tln, err := net.Listen(\"tcp\", \"localhost:0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ln.Close()\n\n\tconnect := func() net.Conn {\n\t\tconn, err := net.DialTimeout(\"tcp\", ln.Addr().String(), 5*time.Second)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn conn\n\t}\n\n\tserver := HandlerServer(MethodNotFound)\n\t// connTimer := &fakeTimer{c: make(chan time.Time, 1)}\n\tvar (\n\t\trunErr error\n\t\twg     sync.WaitGroup\n\t)\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\trunErr = Serve(ctx, ln, server, 100*time.Millisecond)\n\t}()\n\n\t// Exercise some connection/disconnection patterns, and then assert that when\n\t// our timer fires, the server exits.\n\tconn1 := connect()\n\tconn2 := connect()\n\tconn1.Close()\n\tconn2.Close()\n\tconn3 := connect()\n\tconn3.Close()\n\n\twg.Wait()\n\n\tif runErr != ErrIdleTimeout {\n\t\tt.Errorf(\"run() returned error %v, want %v\", runErr, ErrIdleTimeout)\n\t}\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/servertest/servertest.go",
    "content": "// Copyright 2020 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package servertest provides utilities for running tests against a remote LSP\n// server.\npackage servertest\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"encr.dev/cli/internal/jsonrpc2\"\n)\n\n// Connector is the interface used to connect to a server.\ntype Connector interface {\n\tConnect(context.Context) jsonrpc2.Conn\n}\n\n// TCPServer is a helper for executing tests against a remote jsonrpc2\n// connection. Once initialized, its Addr field may be used to connect a\n// jsonrpc2 client.\ntype TCPServer struct {\n\t*connList\n\n\tAddr string\n\n\tln     net.Listener\n\tframer jsonrpc2.Framer\n}\n\n// NewTCPServer returns a new test server listening on local tcp port and\n// serving incoming jsonrpc2 streams using the provided stream server. It\n// panics on any error.\nfunc NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *TCPServer {\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"servertest: failed to listen: %v\", err))\n\t}\n\tif framer == nil {\n\t\tframer = jsonrpc2.NewHeaderStream\n\t}\n\tgo jsonrpc2.Serve(ctx, ln, server, 0)\n\treturn &TCPServer{Addr: ln.Addr().String(), ln: ln, framer: framer, connList: &connList{}}\n}\n\n// Connect dials the test server and returns a jsonrpc2 Connection that is\n// ready for use.\nfunc (s *TCPServer) Connect(ctx context.Context) jsonrpc2.Conn {\n\tnetConn, err := net.Dial(\"tcp\", s.Addr)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"servertest: failed to connect to test instance: %v\", err))\n\t}\n\tconn := jsonrpc2.NewConn(s.framer(netConn))\n\ts.add(conn)\n\treturn conn\n}\n\n// PipeServer is a test server that handles connections over io.Pipes.\ntype PipeServer struct {\n\t*connList\n\tserver jsonrpc2.StreamServer\n\tframer jsonrpc2.Framer\n}\n\n// NewPipeServer returns a test server that can be connected to via io.Pipes.\nfunc NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer {\n\tif framer == nil {\n\t\tframer = jsonrpc2.NewRawStream\n\t}\n\treturn &PipeServer{server: server, framer: framer, connList: &connList{}}\n}\n\n// Connect creates new io.Pipes and binds them to the underlying StreamServer.\nfunc (s *PipeServer) Connect(ctx context.Context) jsonrpc2.Conn {\n\tsPipe, cPipe := net.Pipe()\n\tserverStream := s.framer(sPipe)\n\tserverConn := jsonrpc2.NewConn(serverStream)\n\ts.add(serverConn)\n\tgo s.server.ServeStream(ctx, serverConn)\n\n\tclientStream := s.framer(cPipe)\n\tclientConn := jsonrpc2.NewConn(clientStream)\n\ts.add(clientConn)\n\treturn clientConn\n}\n\n// connList tracks closers to run when a testserver is closed.  This is a\n// convenience, so that callers don't have to worry about closing each\n// connection.\ntype connList struct {\n\tmu    sync.Mutex\n\tconns []jsonrpc2.Conn\n}\n\nfunc (l *connList) add(conn jsonrpc2.Conn) {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tl.conns = append(l.conns, conn)\n}\n\nfunc (l *connList) Close() error {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tvar errmsgs []string\n\tfor _, conn := range l.conns {\n\t\tif err := conn.Close(); err != nil {\n\t\t\terrmsgs = append(errmsgs, err.Error())\n\t\t}\n\t}\n\tif len(errmsgs) > 0 {\n\t\treturn fmt.Errorf(\"closing errors:\\n%s\", strings.Join(errmsgs, \"\\n\"))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/servertest/servertest_test.go",
    "content": "// Copyright 2020 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage servertest\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"encr.dev/cli/internal/jsonrpc2\"\n)\n\ntype msg struct {\n\tMsg string\n}\n\nfunc fakeHandler(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {\n\treturn reply(ctx, &msg{\"pong\"}, nil)\n}\n\nfunc TestTestServer(t *testing.T) {\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tserver := jsonrpc2.HandlerServer(fakeHandler)\n\ttcpTS := NewTCPServer(ctx, server, nil)\n\tdefer tcpTS.Close()\n\tpipeTS := NewPipeServer(ctx, server, nil)\n\tdefer pipeTS.Close()\n\n\ttests := []struct {\n\t\tname      string\n\t\tconnector Connector\n\t}{\n\t\t{\"tcp\", tcpTS},\n\t\t{\"pipe\", pipeTS},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tconn := test.connector.Connect(ctx)\n\t\t\tconn.Go(ctx, jsonrpc2.MethodNotFound)\n\t\t\tvar got msg\n\t\t\tif _, err := conn.Call(ctx, \"ping\", &msg{\"ping\"}, &got); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif want := \"pong\"; got.Msg != want {\n\t\t\t\tt.Errorf(\"conn.Call(...): returned %q, want %q\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/stream.go",
    "content": "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Stream abstracts the transport mechanics from the JSON RPC protocol.\n// A Conn reads and writes messages using the stream it was provided on\n// construction, and assumes that each call to Read or Write fully transfers\n// a single message, or returns an error.\n// A stream is not safe for concurrent use, it is expected it will be used by\n// a single Conn in a safe manner.\ntype Stream interface {\n\t// Read gets the next message from the stream.\n\tRead(context.Context) (Message, int64, error)\n\t// Write sends a message to the stream.\n\tWrite(context.Context, Message) (int64, error)\n\t// Close closes the connection.\n\t// Any blocked Read or Write operations will be unblocked and return errors.\n\tClose() error\n}\n\n// Framer wraps a network connection up into a Stream.\n// It is responsible for the framing and encoding of messages into wire form.\n// NewRawStream and NewHeaderStream are implementations of a Framer.\ntype Framer func(conn net.Conn) Stream\n\n// NewRawStream returns a Stream built on top of a net.Conn.\n// The messages are sent with no wrapping, and rely on json decode consistency\n// to determine message boundaries.\nfunc NewRawStream(conn net.Conn) Stream {\n\treturn &rawStream{\n\t\tconn: conn,\n\t\tin:   json.NewDecoder(conn),\n\t}\n}\n\ntype rawStream struct {\n\tconn net.Conn\n\tin   *json.Decoder\n}\n\nfunc (s *rawStream) Read(ctx context.Context) (Message, int64, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, 0, ctx.Err()\n\tdefault:\n\t}\n\tvar raw json.RawMessage\n\tif err := s.in.Decode(&raw); err != nil {\n\t\treturn nil, 0, err\n\t}\n\tmsg, err := DecodeMessage(raw)\n\treturn msg, int64(len(raw)), err\n}\n\nfunc (s *rawStream) Write(ctx context.Context, msg Message) (int64, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn 0, ctx.Err()\n\tdefault:\n\t}\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"marshaling message: %v\", err)\n\t}\n\tn, err := s.conn.Write(data)\n\treturn int64(n), err\n}\n\nfunc (s *rawStream) Close() error {\n\treturn s.conn.Close()\n}\n\n// NewHeaderStream returns a Stream built on top of a net.Conn.\n// The messages are sent with HTTP content length and MIME type headers.\n// This is the format used by LSP and others.\nfunc NewHeaderStream(conn net.Conn) Stream {\n\treturn &headerStream{\n\t\tconn: conn,\n\t\tin:   bufio.NewReader(conn),\n\t}\n}\n\ntype headerStream struct {\n\tconn net.Conn\n\tin   *bufio.Reader\n}\n\nfunc (s *headerStream) Read(ctx context.Context) (Message, int64, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, 0, ctx.Err()\n\tdefault:\n\t}\n\tvar total, length int64\n\t// read the header, stop on the first empty line\n\tfor {\n\t\tline, err := s.in.ReadString('\\n')\n\t\ttotal += int64(len(line))\n\t\tif err != nil {\n\t\t\treturn nil, total, fmt.Errorf(\"failed reading header line: %w\", err)\n\t\t}\n\t\tline = strings.TrimSpace(line)\n\t\t// check we have a header line\n\t\tif line == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tcolon := strings.IndexRune(line, ':')\n\t\tif colon < 0 {\n\t\t\treturn nil, total, fmt.Errorf(\"invalid header line %q\", line)\n\t\t}\n\t\tname, value := line[:colon], strings.TrimSpace(line[colon+1:])\n\t\tswitch name {\n\t\tcase \"Content-Length\":\n\t\t\tif length, err = strconv.ParseInt(value, 10, 32); err != nil {\n\t\t\t\treturn nil, total, fmt.Errorf(\"failed parsing Content-Length: %v\", value)\n\t\t\t}\n\t\t\tif length <= 0 {\n\t\t\t\treturn nil, total, fmt.Errorf(\"invalid Content-Length: %v\", length)\n\t\t\t}\n\t\tdefault:\n\t\t\t// ignoring unknown headers\n\t\t}\n\t}\n\tif length == 0 {\n\t\treturn nil, total, fmt.Errorf(\"missing Content-Length header\")\n\t}\n\tdata := make([]byte, length)\n\tif _, err := io.ReadFull(s.in, data); err != nil {\n\t\treturn nil, total, err\n\t}\n\ttotal += length\n\tmsg, err := DecodeMessage(data)\n\treturn msg, total, err\n}\n\nfunc (s *headerStream) Write(ctx context.Context, msg Message) (int64, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn 0, ctx.Err()\n\tdefault:\n\t}\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"marshaling message: %v\", err)\n\t}\n\tn, err := fmt.Fprintf(s.conn, \"Content-Length: %v\\r\\n\\r\\n\", len(data))\n\ttotal := int64(n)\n\tif err == nil {\n\t\tn, err = s.conn.Write(data)\n\t\ttotal += int64(n)\n\t}\n\treturn total, err\n}\n\nfunc (s *headerStream) Close() error {\n\treturn s.conn.Close()\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/wire.go",
    "content": "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// this file contains the go forms of the wire specification\n// see http://www.jsonrpc.org/specification for details\n\nvar (\n\t// ErrUnknown should be used for all non coded errors.\n\tErrUnknown = NewError(-32001, \"JSON RPC unknown error\")\n\t// ErrParse is used when invalid JSON was received by the server.\n\tErrParse = NewError(-32700, \"JSON RPC parse error\")\n\t//ErrInvalidRequest is used when the JSON sent is not a valid Request object.\n\tErrInvalidRequest = NewError(-32600, \"JSON RPC invalid request\")\n\t// ErrMethodNotFound should be returned by the handler when the method does\n\t// not exist / is not available.\n\tErrMethodNotFound = NewError(-32601, \"JSON RPC method not found\")\n\t// ErrInvalidParams should be returned by the handler when method\n\t// parameter(s) were invalid.\n\tErrInvalidParams = NewError(-32602, \"JSON RPC invalid params\")\n\t// ErrInternal is not currently returned but defined for completeness.\n\tErrInternal = NewError(-32603, \"JSON RPC internal error\")\n\n\t//ErrServerOverloaded is returned when a message was refused due to a\n\t//server being temporarily unable to accept any new messages.\n\tErrServerOverloaded = NewError(-32000, \"JSON RPC overloaded\")\n)\n\n// wireRequest is sent to a server to represent a Call or Notify operation.\ntype wireRequest struct {\n\t// VersionTag is always encoded as the string \"2.0\"\n\tVersionTag wireVersionTag `json:\"jsonrpc\"`\n\t// Method is a string containing the method name to invoke.\n\tMethod string `json:\"method\"`\n\t// Params is either a struct or an array with the parameters of the method.\n\tParams *json.RawMessage `json:\"params,omitempty\"`\n\t// The id of this request, used to tie the Response back to the request.\n\t// Will be either a string or a number. If not set, the Request is a notify,\n\t// and no response is possible.\n\tID *ID `json:\"id,omitempty\"`\n}\n\n// WireResponse is a reply to a Request.\n// It will always have the ID field set to tie it back to a request, and will\n// have either the Result or Error fields set depending on whether it is a\n// success or failure response.\ntype wireResponse struct {\n\t// VersionTag is always encoded as the string \"2.0\"\n\tVersionTag wireVersionTag `json:\"jsonrpc\"`\n\t// Result is the response value, and is required on success.\n\tResult *json.RawMessage `json:\"result,omitempty\"`\n\t// Error is a structured error response if the call fails.\n\tError *wireError `json:\"error,omitempty\"`\n\t// ID must be set and is the identifier of the Request this is a response to.\n\tID *ID `json:\"id,omitempty\"`\n}\n\n// wireCombined has all the fields of both Request and Response.\n// We can decode this and then work out which it is.\ntype wireCombined struct {\n\tVersionTag wireVersionTag   `json:\"jsonrpc\"`\n\tID         *ID              `json:\"id,omitempty\"`\n\tMethod     string           `json:\"method\"`\n\tParams     *json.RawMessage `json:\"params,omitempty\"`\n\tResult     *json.RawMessage `json:\"result,omitempty\"`\n\tError      *wireError       `json:\"error,omitempty\"`\n}\n\n// wireError represents a structured error in a Response.\ntype wireError struct {\n\t// Code is an error code indicating the type of failure.\n\tCode int64 `json:\"code\"`\n\t// Message is a short description of the error.\n\tMessage string `json:\"message\"`\n\t// Data is optional structured data containing additional information about the error.\n\tData *json.RawMessage `json:\"data,omitempty\"`\n}\n\n// wireVersionTag is a special 0 sized struct that encodes as the jsonrpc version\n// tag.\n// It will fail during decode if it is not the correct version tag in the\n// stream.\ntype wireVersionTag struct{}\n\n// ID is a Request identifier.\ntype ID struct {\n\tname   string\n\tnumber int64\n}\n\nfunc NewError(code int64, message string) error {\n\treturn &wireError{\n\t\tCode:    code,\n\t\tMessage: message,\n\t}\n}\n\nfunc (err *wireError) Error() string {\n\treturn err.Message\n}\n\nfunc (wireVersionTag) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(\"2.0\")\n}\n\nfunc (wireVersionTag) UnmarshalJSON(data []byte) error {\n\tversion := \"\"\n\tif err := json.Unmarshal(data, &version); err != nil {\n\t\treturn err\n\t}\n\tif version != \"2.0\" {\n\t\treturn fmt.Errorf(\"invalid RPC version %v\", version)\n\t}\n\treturn nil\n}\n\n// NewIntID returns a new numerical request ID.\nfunc NewIntID(v int64) ID { return ID{number: v} }\n\n// NewStringID returns a new string request ID.\nfunc NewStringID(v string) ID { return ID{name: v} }\n\n// Format writes the ID to the formatter.\n// If the rune is q the representation is non ambiguous,\n// string forms are quoted, number forms are preceded by a #\nfunc (id ID) Format(f fmt.State, r rune) {\n\tnumF, strF := `%d`, `%s`\n\tif r == 'q' {\n\t\tnumF, strF = `#%d`, `%q`\n\t}\n\tswitch {\n\tcase id.name != \"\":\n\t\t_, _ = fmt.Fprintf(f, strF, id.name)\n\tdefault:\n\t\t_, _ = fmt.Fprintf(f, numF, id.number)\n\t}\n}\n\nfunc (id *ID) MarshalJSON() ([]byte, error) {\n\tif id.name != \"\" {\n\t\treturn json.Marshal(id.name)\n\t}\n\treturn json.Marshal(id.number)\n}\n\nfunc (id *ID) UnmarshalJSON(data []byte) error {\n\t*id = ID{}\n\tif err := json.Unmarshal(data, &id.number); err == nil {\n\t\treturn nil\n\t}\n\treturn json.Unmarshal(data, &id.name)\n}\n"
  },
  {
    "path": "cli/internal/jsonrpc2/wire_test.go",
    "content": "// Copyright 2020 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage jsonrpc2_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"encr.dev/cli/internal/jsonrpc2\"\n)\n\nvar wireIDTestData = []struct {\n\tname    string\n\tid      jsonrpc2.ID\n\tencoded []byte\n\tplain   string\n\tquoted  string\n}{\n\t{\n\t\tname:    `empty`,\n\t\tencoded: []byte(`0`),\n\t\tplain:   `0`,\n\t\tquoted:  `#0`,\n\t}, {\n\t\tname:    `number`,\n\t\tid:      jsonrpc2.NewIntID(43),\n\t\tencoded: []byte(`43`),\n\t\tplain:   `43`,\n\t\tquoted:  `#43`,\n\t}, {\n\t\tname:    `string`,\n\t\tid:      jsonrpc2.NewStringID(\"life\"),\n\t\tencoded: []byte(`\"life\"`),\n\t\tplain:   `life`,\n\t\tquoted:  `\"life\"`,\n\t},\n}\n\nfunc TestIDFormat(t *testing.T) {\n\tfor _, test := range wireIDTestData {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif got := fmt.Sprint(test.id); got != test.plain {\n\t\t\t\tt.Errorf(\"got %s expected %s\", got, test.plain)\n\t\t\t}\n\t\t\tif got := fmt.Sprintf(\"%q\", test.id); got != test.quoted {\n\t\t\t\tt.Errorf(\"got %s want %s\", got, test.quoted)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIDEncode(t *testing.T) {\n\tfor _, test := range wireIDTestData {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tdata, err := json.Marshal(&test.id)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tcheckJSON(t, data, test.encoded)\n\t\t})\n\t}\n}\n\nfunc TestIDDecode(t *testing.T) {\n\tfor _, test := range wireIDTestData {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar got *jsonrpc2.ID\n\t\t\tif err := json.Unmarshal(test.encoded, &got); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif got == nil {\n\t\t\t\tt.Errorf(\"got nil want %s\", test.id)\n\t\t\t} else if *got != test.id {\n\t\t\t\tt.Errorf(\"got %s want %s\", got, test.id)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestErrorEncode(t *testing.T) {\n\tb, err := json.Marshal(jsonrpc2.NewError(0, \"\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcheckJSON(t, b, []byte(`{\n\t\t\"code\": 0,\n\t\t\"message\": \"\"\n\t}`))\n}\n\nfunc TestErrorResponse(t *testing.T) {\n\t// originally reported in #39719, this checks that result is not present if\n\t// it is an error response\n\tr, _ := jsonrpc2.NewResponse(jsonrpc2.NewIntID(3), nil, fmt.Errorf(\"computing fix edits\"))\n\tdata, err := json.Marshal(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcheckJSON(t, data, []byte(`{\n\t\t\"jsonrpc\":\"2.0\",\n\t\t\"error\":{\n\t\t\t\"code\":0,\n\t\t\t\"message\":\"computing fix edits\"\n\t\t},\n\t\t\"id\":3\n\t}`))\n}\n\nfunc checkJSON(t *testing.T, got, want []byte) {\n\t// compare the compact form, to allow for formatting differences\n\tg := &bytes.Buffer{}\n\tif err := json.Compact(g, []byte(got)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw := &bytes.Buffer{}\n\tif err := json.Compact(w, []byte(want)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif g.String() != w.String() {\n\t\tt.Fatalf(\"Got:\\n%s\\nWant:\\n%s\", g, w)\n\t}\n}\n"
  },
  {
    "path": "cli/internal/login/deviceauth.go",
    "content": "package login\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\t\"github.com/fatih/color\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/browser\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n)\n\n// DeviceAuth logs in the suser with the device auth flow.\nfunc DeviceAuth() (*conf.Config, error) {\n\t// Generate PKCE challenge.\n\trandData, err := genRandData()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not generate random data: %v\", err)\n\t}\n\tcodeVerifier := base64.RawURLEncoding.EncodeToString([]byte(randData))\n\tchallengeHash := sha256.Sum256([]byte(codeVerifier))\n\tcodeChallenge := base64.RawURLEncoding.EncodeToString(challengeHash[:])\n\n\tresp, err := platform.BeginDeviceAuthFlow(context.Background(), platform.BeginAuthorizationFlowParams{\n\t\tCodeChallenge: codeChallenge,\n\t\tClientID:      \"encore_cli\",\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tbold  = color.New(color.Bold)\n\t\tfaint = color.New(color.Faint)\n\t)\n\n\tfmt.Printf(\"Your pairing code is %s\\n\", bold.Sprint(resp.UserCode))\n\tfaint.Println(\"This pairing code verifies your authentication with Encore.\")\n\n\tinputCh := make(chan struct{}, 1)\n\n\tspin := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\tspin.Prefix = \"Waiting for login confirmation...\"\n\n\tif !env.IsSSH() && browser.CanOpen() {\n\t\tfmt.Fprintf(os.Stdout, \"Press Enter to open the browser or visit %s (^C to quit)\\n\",\n\t\t\tresp.VerificationURI)\n\n\t\t// Asynchronously wait for input.\n\t\tw := waitForEnterPress()\n\t\tdefer w.Stop()\n\t\tgo func() {\n\t\t\tselect {\n\t\t\tcase <-w.pressed:\n\t\t\t\tinputCh <- struct{}{}\n\t\t\tcase <-w.quit:\n\t\t\t}\n\t\t}()\n\n\t} else {\n\t\t// On Windows we need a proper \\r\\n newline to ensure the URL detection doesn't extend to the next line.\n\t\t// fmt.Fprintln and family prints just a simple \\n, so don't use that.\n\t\tfmt.Fprintf(os.Stdout, \"To authenticate with Encore, please go to: %s%s\", resp.VerificationURI, cmdutil.Newline)\n\t\tspin.Start()\n\t}\n\n\tresultCh := make(chan deviceAuthResult, 1)\n\tgo pollForDeviceAuthResult(codeVerifier, resp, resultCh)\n\n\tfor {\n\t\tselect {\n\t\tcase <-inputCh:\n\t\t\t// The user hit Enter; show a spinner and try to open the browser.\n\t\t\tspin.Start()\n\t\t\tif !browser.Open(resp.VerificationURI) {\n\t\t\t\tspin.FinalMSG = fmt.Sprintf(\"Failed to open browser, please go to %s manually.\", resp.VerificationURI)\n\t\t\t\tspin.Stop()\n\n\t\t\t\t// Create a new spinner so the message above stays around.\n\t\t\t\tspin = spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\t\t\t\tspin.Prefix = \"Waiting for login confirmation...\"\n\t\t\t\tspin.Start()\n\t\t\t}\n\n\t\tcase res := <-resultCh:\n\t\t\tif res.err != nil {\n\t\t\t\tspin.FinalMSG = fmt.Sprintf(\"Failed to log in: %v\", res.err)\n\t\t\t\tspin.Stop()\n\t\t\t\treturn nil, res.err\n\t\t\t}\n\n\t\t\tspin.Stop()\n\t\t\treturn res.cfg, nil\n\t\t}\n\t}\n}\n\ntype deviceAuthResult struct {\n\tcfg *conf.Config\n\terr error\n}\n\nfunc pollForDeviceAuthResult(codeVerifier string, data *platform.BeginAuthorizationFlowResponse, resultCh chan<- deviceAuthResult) {\nPollLoop:\n\tfor {\n\t\tinterval := data.Interval\n\t\tif interval <= 0 {\n\t\t\tinterval = 5\n\t\t}\n\t\ttime.Sleep(time.Duration(interval) * time.Second)\n\n\t\tresp, err := platform.PollDeviceAuthFlow(context.Background(), platform.PollDeviceAuthFlowParams{\n\t\t\tDeviceCode:   data.DeviceCode,\n\t\t\tCodeVerifier: codeVerifier,\n\t\t})\n\t\tif err != nil {\n\t\t\tif e, ok := err.(platform.Error); ok {\n\t\t\t\tswitch e.Code {\n\t\t\t\tcase \"auth_pending\":\n\t\t\t\t\t// Not yet authorized, continue polling.\n\t\t\t\t\tcontinue PollLoop\n\n\t\t\t\tcase \"rate_limited\":\n\t\t\t\t\t// Spurious error; sleep a bit extra before retrying to be safe.\n\t\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\t\tcontinue PollLoop\n\t\t\t\t}\n\t\t\t}\n\t\t\tresultCh <- deviceAuthResult{err: err}\n\t\t\treturn\n\t\t}\n\n\t\tcfg := &conf.Config{Token: *resp.Token, Actor: resp.Actor, Email: resp.Email, AppSlug: resp.AppSlug}\n\t\tresultCh <- deviceAuthResult{cfg: cfg}\n\t\treturn\n\t}\n}\n\ntype enterPressWaiter struct {\n\tquit    chan struct{} // close to abort the waiter\n\tpressed chan struct{} // closed when enter has been pressed\n\trunDone chan struct{} // closed when the run goroutine has exited\n}\n\nfunc waitForEnterPress() *enterPressWaiter {\n\tw := &enterPressWaiter{\n\t\tquit:    make(chan struct{}),\n\t\tpressed: make(chan struct{}, 1),\n\t\trunDone: make(chan struct{}),\n\t}\n\tgo w.run()\n\treturn w\n}\n\nfunc (w *enterPressWaiter) run() {\n\tdefer close(w.runDone)\n\tfmt.Fscanln(os.Stdin)\n\tselect {\n\tcase w.pressed <- struct{}{}:\n\tcase <-w.quit:\n\t}\n}\n\nfunc (w *enterPressWaiter) Stop() {\n\tclose(w.quit)\n\tos.Stdin.SetReadDeadline(time.Now()) // interrupt the pending read\n\n\t// Asynchronously wait for the run goroutine to exit before\n\t// we reset the read deadline.\n\tgo func() {\n\t\t<-w.runDone\n\t\tos.Stdin.SetReadDeadline(time.Time{}) // reset read deadline\n\t}()\n}\n"
  },
  {
    "path": "cli/internal/login/interactive.go",
    "content": "package login\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n\n\t\"encr.dev/cli/cmd/encore/cmdutil\"\n\t\"encr.dev/cli/internal/browser\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n)\n\n// interactive keeps the state of an ongoing login flow.\ntype interactive struct {\n\tresult chan *conf.Config // Successful logins are sent on this\n\n\tstate           string\n\tchallenge       string\n\tpubKey, privKey string\n\tsrv             *http.Server\n\tln              net.Listener\n}\n\n// Interactive begins an interactive login attempt.\nfunc Interactive() (*conf.Config, error) {\n\t// Generate initial request state\n\tstate, err1 := genRandData()\n\tchallenge, err2 := genRandData()\n\tif err1 != nil || err2 != nil {\n\t\treturn nil, fmt.Errorf(\"could not generate random data: %v/%v\", err1, err2)\n\t}\n\n\tchallengeHash := sha256.Sum256([]byte(challenge))\n\tencodedChallenge := base64.RawURLEncoding.EncodeToString(challengeHash[:])\n\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer ln.Close()\n\taddr := ln.Addr().(*net.TCPAddr)\n\turl := fmt.Sprintf(\"http://localhost:%d/oauth\", addr.Port)\n\n\treq := &platform.CreateOAuthSessionParams{\n\t\tChallenge:   encodedChallenge,\n\t\tState:       state,\n\t\tRedirectURL: url,\n\t}\n\tauthURL, err := platform.CreateOAuthSession(context.Background(), req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tflow := &interactive{\n\t\tresult: make(chan *conf.Config),\n\n\t\tstate:     state,\n\t\tchallenge: challenge,\n\t}\n\tflow.srv = &http.Server{Handler: http.HandlerFunc(flow.oauthHandler)}\n\tgo flow.srv.Serve(ln)\n\n\tspin := spinner.New(spinner.CharSets[14], 100*time.Millisecond)\n\n\tif env.IsSSH() || !browser.Open(authURL) {\n\t\t// On Windows we need a proper \\r\\n newline to ensure the URL detection doesn't extend to the next line.\n\t\t// fmt.Fprintln and family prints just a simple \\n, so don't use that.\n\t\tfmt.Fprint(os.Stdout, \"Log in to Encore using your browser here: \", authURL, cmdutil.Newline)\n\t} else {\n\t\tspin.Prefix = \"Waiting for login to complete \"\n\t\tspin.Start()\n\t\tdefer spin.Stop()\n\t}\n\n\tselect {\n\tcase res := <-flow.result:\n\t\treturn res, nil\n\tcase <-time.After(10 * time.Minute):\n\t\treturn nil, errors.New(\"Timed out waiting for login confirmation\")\n\t}\n}\n\nfunc (f *interactive) oauthHandler(w http.ResponseWriter, req *http.Request) {\n\tif req.URL.Path != \"/oauth\" {\n\t\thttp.Error(w, \"Not Found\", http.StatusNotFound)\n\t\treturn\n\t}\n\tcode := req.FormValue(\"code\")\n\treqState := req.FormValue(\"state\")\n\tif code == \"\" || reqState != f.state {\n\t\thttp.Error(w, \"Bad Request (bad code or state)\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tparams := &platform.ExchangeOAuthTokenParams{\n\t\tChallenge: f.challenge,\n\t\tCode:      code,\n\t}\n\tresp, err := platform.ExchangeOAuthToken(req.Context(), params)\n\tif err != nil {\n\t\thttp.Error(w, \"Could not exchange token: \"+err.Error(), http.StatusBadGateway)\n\t\treturn\n\t} else if resp.Token == nil {\n\t\thttp.Error(w, \"Invalid response: missing token\", http.StatusBadGateway)\n\t\treturn\n\t}\n\n\tconf := &conf.Config{Token: *resp.Token, Actor: resp.Actor, Email: resp.Email, AppSlug: resp.AppSlug}\n\tselect {\n\tcase f.result <- conf:\n\t\thttp.Redirect(w, req, \"https://www.encore.dev/auth/success\", http.StatusFound)\n\tdefault:\n\t\thttp.Error(w, \"Unexpected request\", http.StatusBadRequest)\n\t}\n}\n"
  },
  {
    "path": "cli/internal/login/login.go",
    "content": "// Package login handles login and authentication with Encore's platform.\npackage login\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\t\"encr.dev/cli/internal/browser\"\n\t\"encr.dev/cli/internal/platform\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n)\n\nfunc DecideFlow() (*conf.Config, error) {\n\tif env.IsSSH() || !browser.CanOpen() {\n\t\treturn DeviceAuth()\n\t}\n\treturn Interactive()\n}\n\nfunc WithAuthKey(authKey string) (*conf.Config, error) {\n\tparams := &platform.ExchangeAuthKeyParams{\n\t\tAuthKey: authKey,\n\t}\n\tresp, err := platform.ExchangeAuthKey(context.Background(), params)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if resp.Token == nil {\n\t\treturn nil, fmt.Errorf(\"invalid response: missing token\")\n\t}\n\n\ttok := resp.Token\n\tconf := &conf.Config{Token: *tok, Actor: resp.Actor, AppSlug: resp.AppSlug}\n\n\treturn conf, nil\n}\n\nfunc genRandData() (string, error) {\n\tdata := make([]byte, 32)\n\t_, err := rand.Read(data[:])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(data), nil\n}\n"
  },
  {
    "path": "cli/internal/manifest/manifest.go",
    "content": "// Package manifest reads and writes Encore app manifests.\npackage manifest\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base32\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"encr.dev/pkg/xos\"\n)\n\n// Manifest represents the persisted manifest for\n// an Encore application. It is not intended to be committed to\n// source control.\ntype Manifest struct {\n\t// AppID is a unique identifier for the app.\n\t// It uses the encore.dev app slug if the app\n\t// is linked, and is otherwise a randomly generated id.\n\tAppID string `json:\"appID,omitempty\"`\n\n\t// LocalID is a unique id for the app that's only used locally.\n\t// It is randomly generated on first use.\n\tLocalID string `json:\"local_id\"`\n\n\t// Tutorial is set to the name of the tutorial the user is currently on or empty.\n\tTutorial string `json:\"tutorial\"`\n}\n\n// SetTutorial sets the tutorial field on the app manifest\nfunc SetTutorial(appRoot string, tutorial string) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"read/create manifest: %v\", err)\n\t\t}\n\t}()\n\n\tvar man Manifest\n\n\t// Use the existing manifest if we have one.\n\tcfgPath := filepath.Join(appRoot, \".encore\", \"manifest.json\")\n\tif data, err := os.ReadFile(cfgPath); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn err\n\t} else if err == nil {\n\t\terr = json.Unmarshal(data, &man)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tman.Tutorial = tutorial\n\n\t// Write it back.\n\tout, _ := json.Marshal(&man)\n\tif err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil {\n\t\treturn err\n\t} else if err := xos.WriteFile(cfgPath, out, 0644); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// ReadOrCreate reads the manifest for the app rooted at appRoot.\n// If it doesn't exist it creates it first.\nfunc ReadOrCreate(appRoot string) (mf *Manifest, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"read/create manifest: %v\", err)\n\t\t}\n\t}()\n\n\tvar man Manifest\n\n\t// Use the existing manifest if we have one.\n\tcfgPath := filepath.Join(appRoot, \".encore\", \"manifest.json\")\n\tif data, err := os.ReadFile(cfgPath); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil, err\n\t} else if err == nil {\n\t\terr = json.Unmarshal(data, &man)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Generate a local ID if we don't have one.\n\tif man.LocalID == \"\" {\n\t\t// If we have a legacy AppID, migrate that over to the local id.\n\t\tif man.AppID != \"\" {\n\t\t\tman.LocalID = man.AppID\n\t\t\tman.AppID = \"\"\n\t\t} else {\n\t\t\tid, err := genID()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tman.LocalID = id\n\t\t}\n\t}\n\n\t// Write it back.\n\tout, _ := json.Marshal(&man)\n\tif err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil {\n\t\treturn nil, err\n\t} else if err := xos.WriteFile(cfgPath, out, 0644); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &man, nil\n}\n\nconst encodeStr = \"23456789abcdefghikmnopqrstuvwxyz\"\n\nvar encoding = base32.NewEncoding(encodeStr).WithPadding(base32.NoPadding)\n\n// genID generates a random id for a local ID\n//\n// Note: the fact this generates without a hyphen is expected and used\n// to identify a local ID vs a platform ID\nfunc genID() (string, error) {\n\tvar data [3]byte\n\tif _, err := rand.Read(data[:]); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn encoding.EncodeToString(data[:]), nil\n}\n"
  },
  {
    "path": "cli/internal/onboarding/onboarding.go",
    "content": "package onboarding\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"encr.dev/pkg/xos\"\n)\n\ntype Event struct {\n\ttime.Time\n}\n\ntype State struct {\n\tFirstRun   Event             `json:\"first_run\"`\n\tDeployHint Event             `json:\"deploy_hint\"`\n\tEventMap   map[string]*Event `json:\"carousel\"`\n}\n\nfunc (e *State) Property(prop string) *Event {\n\tif e.EventMap == nil {\n\t\te.EventMap = map[string]*Event{}\n\t}\n\t_, ok := e.EventMap[prop]\n\tif !ok {\n\t\te.EventMap[prop] = &Event{}\n\t}\n\treturn e.EventMap[prop]\n}\n\nfunc (e *Event) IsSet() bool {\n\treturn !e.IsZero()\n}\n\nfunc (e *Event) Set() bool {\n\tif !e.IsSet() {\n\t\te.Time = time.Now()\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc Load() (*State, error) {\n\tcfg := &State{EventMap: map[string]*Event{}}\n\tpath, err := configPath()\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\terr = nil\n\t\t}\n\t\treturn cfg, err\n\t}\n\terr = json.Unmarshal(data, &cfg)\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\n\tif cfg.FirstRun.IsSet() && time.Since(cfg.FirstRun.Time) > 14*24*time.Hour {\n\t\tcfg.Property(\"carousel\").Set()\n\t}\n\treturn cfg, err\n}\n\nfunc (cfg *State) Write() error {\n\tpath, err := configPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata, err := json.Marshal(cfg)\n\tif err != nil {\n\t\treturn err\n\t} else if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {\n\t\treturn err\n\t}\n\treturn xos.WriteFile(path, data, 0644)\n}\n\nfunc configPath() (string, error) {\n\tdir, err := os.UserConfigDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(dir, \"encore\", \"onboarding.json\"), nil\n}\n"
  },
  {
    "path": "cli/internal/platform/api.go",
    "content": "package platform\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/golang/protobuf/proto\"\n\t\"github.com/gorilla/websocket\"\n\n\t\"encr.dev/pkg/fns\"\n\tmetav1 \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype CreateAppParams struct {\n\tName           string\n\tInitialSecrets map[string]string\n\tAppRootDir     string\n}\n\ntype App struct {\n\tID          string  `json:\"eid\"`\n\tLegacyID    string  `json:\"id\"`\n\tSlug        string  `json:\"slug\"`\n\tName        string  `json:\"name\"`\n\tDescription string  `json:\"description\"` // can be blank\n\tMainBranch  *string `json:\"main_branch\"` // nil if not set\n}\n\ntype Rollout struct {\n\tID      string `json:\"id\"`\n\tEnvName string `json:\"env_name\"`\n}\n\ntype Env struct {\n\tID    string `json:\"id\"`\n\tSlug  string `json:\"slug\"`\n\tType  string `json:\"type\"`\n\tCloud string `json:\"cloud\"`\n}\n\nfunc CreateApp(ctx context.Context, p *CreateAppParams) (*App, error) {\n\tvar resp App\n\terr := call(ctx, \"POST\", \"/apps\", p, &resp, true)\n\treturn &resp, err\n}\n\nfunc Deploy(ctx context.Context, appSlug, env, sha, branch string) (*Rollout, error) {\n\tvar resp Rollout\n\terr := call(\n\t\tctx,\n\t\t\"POST\",\n\t\tfmt.Sprintf(\n\t\t\t\"/apps/%s/envs/%s/rollouts\",\n\t\t\turl.PathEscape(appSlug),\n\t\t\turl.PathEscape(env),\n\t\t), map[string]string{\n\t\t\t\"sha\":    sha,\n\t\t\t\"branch\": branch,\n\t\t},\n\t\t&resp,\n\t\ttrue)\n\treturn &resp, err\n}\n\nfunc ListApps(ctx context.Context) ([]*App, error) {\n\tvar resp []*App\n\terr := call(ctx, \"GET\", \"/user/apps\", nil, &resp, true)\n\treturn resp, err\n}\n\nfunc GetApp(ctx context.Context, appSlug string) (*App, error) {\n\tvar resp App\n\terr := call(ctx, \"GET\", \"/apps/\"+url.PathEscape(appSlug), nil, &resp, true)\n\treturn &resp, err\n}\n\nfunc ListEnvs(ctx context.Context, appSlug string) ([]*Env, error) {\n\tvar resp []*Env\n\terr := call(ctx, \"GET\", \"/apps/\"+url.PathEscape(appSlug)+\"/envs\", nil, &resp, true)\n\treturn resp, err\n}\n\ntype SecretKind string\n\nconst (\n\tDevelopmentSecrets SecretKind = \"development\"\n\tProductionSecrets  SecretKind = \"production\"\n)\n\nfunc GetLocalSecretValues(ctx context.Context, appSlug string, poll bool) (secrets map[string]string, err error) {\n\turl := \"/apps/\" + url.PathEscape(appSlug) + \"/secrets:values?kind=development\"\n\tif poll {\n\t\turl += \"&poll=true\"\n\t}\n\terr = call(ctx, \"GET\", url, nil, &secrets, true)\n\treturn secrets, err\n}\n\ntype SecretVersion struct {\n\tNumber  int       `json:\"number\"`\n\tCreated time.Time `json:\"created\"`\n}\n\nfunc SetAppSecret(ctx context.Context, appSlug string, kind SecretKind, secretKey, value string) (*SecretVersion, error) {\n\tparams := struct {\n\t\tKind  SecretKind\n\t\tValue string\n\t}{Kind: kind, Value: value}\n\turl := fmt.Sprintf(\"/apps/%s/secrets/%s/versions\",\n\t\turl.PathEscape(appSlug),\n\t\turl.PathEscape(secretKey),\n\t)\n\tvar resp SecretVersion\n\terr := call(ctx, \"POST\", url, &params, &resp, true)\n\treturn &resp, err\n}\n\nfunc GetEnvMeta(ctx context.Context, appSlug, envName string) (*metav1.Data, error) {\n\turl := \"/apps/\" + url.PathEscape(appSlug) + \"/envs/\" + url.PathEscape(envName) + \"/meta\"\n\tbody, err := rawCall(ctx, \"GET\", url, nil, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer fns.CloseIgnore(body)\n\tdata, err := io.ReadAll(body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"platform.GetEnvMeta: %v\", err)\n\t}\n\tvar md metav1.Data\n\tif err := proto.Unmarshal(data, &md); err != nil {\n\t\treturn nil, fmt.Errorf(\"platform.GetEnvMeta: %v\", err)\n\t}\n\treturn &md, nil\n}\n\nfunc DBConnect(ctx context.Context, appSlug, envSlug, dbName, role string, startupData []byte) (*websocket.Conn, error) {\n\tpath := escapef(\"/apps/%s/envs/%s/sqldb-connect/%s\", appSlug, envSlug, dbName)\n\tif role != \"\" {\n\t\tpath += \"?role=\" + url.QueryEscape(role)\n\t}\n\treturn wsDial(ctx, path, true, map[string]string{\n\t\t\"X-Startup-Message\": base64.StdEncoding.EncodeToString(startupData),\n\t})\n}\n\nfunc EnvLogs(ctx context.Context, appSlug, envSlug string) (*websocket.Conn, error) {\n\tpath := escapef(\"/apps/%s/envs/%s/log\", appSlug, envSlug)\n\treturn wsDial(ctx, path, true, nil)\n}\n\nfunc KubernetesClusters(ctx context.Context, appSlug string, envName string) (string, string, []KubeCtlConfig, error) {\n\ttype K8SClusterConfigs struct {\n\t\tAppSlug  string          `json:\"app\"`\n\t\tEnvName  string          `json:\"env\"`\n\t\tClusters []KubeCtlConfig `json:\"clusters\"`\n\t}\n\n\tvar resp K8SClusterConfigs\n\terr := call(ctx, \"GET\", \"/apps/\"+url.PathEscape(appSlug)+\"/envs/\"+url.PathEscape(envName)+\"/k8s-clusters\", nil, &resp, true)\n\treturn resp.AppSlug, resp.EnvName, resp.Clusters, err\n}\n\ntype KubeCtlConfig struct {\n\tEnvID            string `json:\"env_id\"`              // The ID of the environment\n\tResID            string `json:\"res_id\"`              // The ID of the cluster\n\tName             string `json:\"name\"`                // The name of the cluster\n\tDefaultNamespace string `json:\"namespace,omitempty\"` // The default namespace for the cluster (if any)\n}\n\nfunc escapef(format string, args ...string) string {\n\tifaces := make([]interface{}, len(args))\n\tfor i, arg := range args {\n\t\tifaces[i] = url.PathEscape(arg)\n\t}\n\treturn fmt.Sprintf(format, ifaces...)\n}\n"
  },
  {
    "path": "cli/internal/platform/client.go",
    "content": "package platform\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"runtime\"\n\n\t\"github.com/gorilla/websocket\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/cli/internal/platform/gql\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/fns\"\n)\n\ntype Error struct {\n\tHTTPStatus string `json:\"-\"`\n\tHTTPCode   int    `json:\"-\"`\n\tCode       string\n\tDetail     json.RawMessage\n}\n\ntype ValidationDetails struct {\n\tField string `json:\"field\"`\n\tType  string `json:\"type\"`\n}\n\nfunc (e Error) Error() string {\n\tif len(e.Detail) > 0 {\n\t\treturn fmt.Sprintf(\"http %s: code=%s detail=%s\", e.HTTPStatus, e.Code, e.Detail)\n\t}\n\treturn fmt.Sprintf(\"http %s: code=%s\", e.HTTPStatus, e.Code)\n}\n\n// call makes a call to the API endpoint given by method and path.\n// If reqParams and respParams are non-nil they are JSON-marshalled/unmarshalled.\nfunc call(ctx context.Context, method, path string, reqParams, respParams interface{}, auth bool) (err error) {\n\tlog.Trace().Interface(\"request\", reqParams).Msgf(\"->     %s %s\", method, path)\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.Trace().Err(err).Msgf(\"<- ERR %s %s\", method, path)\n\t\t} else {\n\t\t\tlog.Trace().Interface(\"response\", respParams).Msgf(\"<- OK  %s %s\", method, path)\n\t\t}\n\t}()\n\n\tresp, err := sendPlatformReq(ctx, method, path, reqParams, auth)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fns.CloseIgnore(resp.Body)\n\n\tvar respStruct struct {\n\t\tOK    bool\n\t\tError Error\n\t\tData  json.RawMessage\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&respStruct); err != nil {\n\t\treturn fmt.Errorf(\"decode response: %v\", err)\n\t} else if !respStruct.OK {\n\t\te := respStruct.Error\n\t\te.HTTPCode = resp.StatusCode\n\t\te.HTTPStatus = resp.Status\n\t\treturn e\n\t}\n\n\tif respParams != nil {\n\t\tif err := json.Unmarshal([]byte(respStruct.Data), respParams); err != nil {\n\t\t\treturn fmt.Errorf(\"decode response data: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\ntype graphqlRequest struct {\n\tQuery         string                 `json:\"query\"`\n\tVariables     map[string]interface{} `json:\"variables,omitempty\"`\n\tOperationName string                 `json:\"operationName,omitempty\"`\n\tExtensions    map[string]interface{} `json:\"extensions,omitempty\"`\n}\n\nvar graphqlDecoder = (func() jsoniter.API {\n\tenc := jsoniter.Config{}.Froze()\n\tenc.RegisterExtension(NewInterfaceCodecExtension())\n\treturn enc\n})()\n\n// graphqlCall makes a GraphQL request.\nfunc graphqlCall(ctx context.Context, req graphqlRequest, respData any, auth bool) (err error) {\n\tlog.Trace().Msgf(\"->     graphql %s: %+v\", req.OperationName, req.Variables)\n\thttpResp, err := sendPlatformReq(ctx, \"POST\", \"/graphql\", req, auth)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fns.CloseIgnore(httpResp.Body)\n\n\tvar respStruct struct {\n\t\tData       json.RawMessage\n\t\tErrors     gql.ErrorList\n\t\tExtensions map[string]interface{}\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.Trace().Msgf(\"<- ERR graphql %s: %v\", req.OperationName, err)\n\t\t} else {\n\t\t\tlog.Trace().Msgf(\"<- OK  graphql %s: %s\", req.OperationName, respStruct.Data)\n\t\t}\n\t}()\n\n\tif err := json.NewDecoder(httpResp.Body).Decode(&respStruct); err != nil {\n\t\treturn fmt.Errorf(\"decode response: %v\", err)\n\t} else if len(respStruct.Errors) > 0 {\n\t\treturn fmt.Errorf(\"graphql request failed: %w\", respStruct.Errors)\n\t}\n\tif respData != nil {\n\t\tif err := graphqlDecoder.NewDecoder(bytes.NewReader(respStruct.Data)).Decode(respData); err != nil {\n\t\t\treturn fmt.Errorf(\"decode graphql data: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// rawCall makes a call to the API endpoint given by method and path.\n// It returns the raw HTTP response body on success; it must be closed by the caller.\nfunc rawCall(ctx context.Context, method, path string, reqParams interface{}, auth bool) (respBody io.ReadCloser, err error) {\n\tlog.Trace().Msgf(\"->     %s %s: %+v\", method, path, reqParams)\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.Trace().Msgf(\"<- ERR %s %s: %v\", method, path, err)\n\t\t} else {\n\t\t\tlog.Trace().Msgf(\"<- OK  %s %s\", method, path)\n\t\t}\n\t}()\n\n\tresp, err := sendPlatformReq(ctx, method, path, reqParams, auth)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\t_ = resp.Body.Close()\n\t\t}\n\t}()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, decodeErrorResponse(resp)\n\t}\n\n\treturn resp.Body, nil\n}\n\nfunc sendPlatformReq(ctx context.Context, method, path string, reqParams any, auth bool) (httpResp *http.Response, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"%s %s: %w\", method, path, err)\n\t\t}\n\t}()\n\n\tvar body io.Reader\n\tif reqParams != nil {\n\t\treqData, err := json.Marshal(reqParams)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %v\", err)\n\t\t}\n\t\tbody = bytes.NewReader(reqData)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, conf.APIBaseURL+path, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif reqParams != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\n\treturn doPlatformReq(req, auth)\n}\n\nfunc doPlatformReq(req *http.Request, auth bool) (httpResp *http.Response, err error) {\n\t// Add a very limited amount of information for diagnostics\n\treq.Header.Set(\"User-Agent\", \"EncoreCLI/\"+version.Version)\n\treq.Header.Set(\"X-Encore-Version\", version.Version)\n\treq.Header.Set(\"X-Encore-GOOS\", runtime.GOOS)\n\treq.Header.Set(\"X-Encore-GOARCH\", runtime.GOARCH)\n\n\tclient := http.DefaultClient\n\tif auth {\n\t\tclient = conf.AuthClient\n\t}\n\treturn client.Do(req)\n}\n\n// wsDial sets up a WebSocket connection to the API endpoint given by method and path.\nfunc wsDial(ctx context.Context, path string, auth bool, extraHeaders map[string]string) (ws *websocket.Conn, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"WS %s: %w\", path, err)\n\t\t}\n\t}()\n\n\t// Add a very limited amount of information for diagnostics\n\theader := make(http.Header)\n\theader.Set(\"User-Agent\", \"EncoreCLI/\"+version.Version)\n\theader.Set(\"X-Encore-Version\", version.Version)\n\theader.Set(\"X-Encore-GOOS\", runtime.GOOS)\n\theader.Set(\"X-Encore-GOARCH\", runtime.GOARCH)\n\theader.Set(\"Origin\", \"http://encore-cli.local\")\n\tfor k, v := range extraHeaders {\n\t\theader.Set(k, v)\n\t}\n\n\tlog.Trace().Msgf(\"->     %s %s: %+v\", \"WS\", path, extraHeaders)\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tlog.Trace().Msgf(\"<- ERR %s %s: %v\", \"WS\", path, err)\n\t\t} else {\n\t\t\tlog.Trace().Msgf(\"<- OK  %s %s\", \"WS\", path)\n\t\t}\n\t}()\n\n\tif auth {\n\t\ttok, err := conf.DefaultTokenSource.Token()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\theader.Set(\"Authorization\", \"Bearer \"+tok.AccessToken)\n\t}\n\n\turl := conf.WSBaseURL + path\n\tlog.Trace().Msgf(\"->     %s %s: connecting to %s\", \"WS\", path, url)\n\tws, httpResp, err := websocket.DefaultDialer.DialContext(ctx, url, header)\n\tif httpResp != nil && httpResp.StatusCode >= 400 {\n\t\tvar respStruct struct {\n\t\t\tOK    bool\n\t\t\tError Error\n\t\t\tData  json.RawMessage\n\t\t}\n\t\tif err := json.NewDecoder(httpResp.Body).Decode(&respStruct); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %v\", err)\n\t\t} else if !respStruct.OK {\n\t\t\te := respStruct.Error\n\t\t\te.HTTPCode = httpResp.StatusCode\n\t\t\te.HTTPStatus = httpResp.Status\n\t\t\treturn nil, e\n\t\t}\n\t}\n\n\treturn ws, err\n}\n\nfunc decodeErrorResponse(resp *http.Response) error {\n\tvar respStruct struct {\n\t\tOK    bool\n\t\tError Error\n\t\tData  json.RawMessage\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&respStruct); err != nil {\n\t\treturn fmt.Errorf(\"decode response: %v\", err)\n\t}\n\te := respStruct.Error\n\te.HTTPCode = resp.StatusCode\n\te.HTTPStatus = resp.Status\n\treturn e\n}\n"
  },
  {
    "path": "cli/internal/platform/gql/app.go",
    "content": "package gql\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\ntype App struct {\n\tID   string\n\tSlug string\n}\n\ntype Error struct {\n\tMessage    string                     `json:\"message\"`\n\tPath       []string                   `json:\"path\"`\n\tExtensions map[string]json.RawMessage `json:\"extensions\"`\n}\n\nfunc (e *Error) Error() string {\n\treturn e.Message\n}\n\ntype ErrorList []*Error\n\nfunc (err ErrorList) Error() string {\n\tif len(err) == 0 {\n\t\treturn \"no errors\"\n\t} else if len(err) == 1 {\n\t\treturn err[0].Error()\n\t}\n\treturn fmt.Sprintf(\"%s (and %d more errors)\", err[0].Error(), len(err)-1)\n}\n"
  },
  {
    "path": "cli/internal/platform/gql/env.go",
    "content": "package gql\n\ntype Env struct {\n\tID   string\n\tApp  *App\n\tName string\n}\n"
  },
  {
    "path": "cli/internal/platform/gql/secrets.go",
    "content": "package gql\n\nimport (\n\t\"time\"\n\n\t\"github.com/modern-go/reflect2\"\n)\n\ntype Secret struct {\n\tKey    string\n\tGroups []*SecretGroup\n}\n\ntype SecretGroup struct {\n\tID          string\n\tKey         string\n\tSelector    []SecretSelector\n\tDescription string\n\tEtag        string\n\tArchivedAt  *time.Time\n\tDestroyedAt *time.Time\n}\n\ntype SecretSelector interface {\n\tsecretSelector()\n\tString() string\n}\n\ntype SecretSelectorEnvType struct {\n\tKind string\n}\n\nfunc (SecretSelectorEnvType) secretSelector()   {}\nfunc (s *SecretSelectorEnvType) String() string { return \"type:\" + s.Kind }\n\ntype SecretSelectorSpecificEnv struct {\n\tEnv *Env\n}\n\nfunc (s *SecretSelectorSpecificEnv) String() string { return \"id:\" + s.Env.ID }\nfunc (SecretSelectorSpecificEnv) secretSelector()   {}\n\ntype ConflictError struct {\n\tAppID     string\n\tKey       string\n\tConflicts []GroupConflict\n}\n\ntype GroupConflict struct {\n\tGroupID   string\n\tConflicts []string\n}\n\n// TypeRegistry contains all the types that are used in the graphql schema,\n// in order to ensure they are not dead-code eliminated.\nvar TypeRegistry = []reflect2.Type{\n\treflect2.TypeOf((*SecretSelectorEnvType)(nil)),\n\treflect2.TypeOf((*SecretSelectorSpecificEnv)(nil)),\n}\n"
  },
  {
    "path": "cli/internal/platform/jsoniter_ext.go",
    "content": "package platform\n\nimport (\n\t\"reflect\"\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/modern-go/reflect2\"\n)\n\n// InterfaceCodecExtension is used to decode interface fields\n// it'll store the type of the values in a wrapper object\ntype InterfaceCodecExtension struct {\n\tjsoniter.DummyExtension\n}\n\nfunc NewInterfaceCodecExtension() *InterfaceCodecExtension {\n\treturn &InterfaceCodecExtension{}\n}\n\nfunc (e *InterfaceCodecExtension) DecorateDecoder(typ reflect2.Type, decoder jsoniter.ValDecoder) jsoniter.ValDecoder {\n\tif typ.Kind() == reflect.Interface {\n\t\treturn &interfaceCodec{typ: typ, decoder: decoder}\n\t}\n\treturn decoder\n}\n\nconst gqlPackage = \"encr.dev/cli/internal/platform/gql\"\n\ntype interfaceCodec struct {\n\ttyp     reflect2.Type\n\tdecoder jsoniter.ValDecoder\n}\n\n// Decode decodes an interface value from a iterator\nfunc (codec *interfaceCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {\n\t// if it's not an objectvalue, we don't need to bother\n\tif iter.WhatIsNext() != jsoniter.ObjectValue {\n\t\tcodec.decoder.Decode(ptr, iter)\n\t\treturn\n\t}\n\n\t// if it is, we try to resolve the pkgPath, type and content\n\tval := iter.ReadAny()\n\ttypeName := val.Get(\"__typename\").ToString()\n\tif typeName == \"\" {\n\t\titer.ReportError(\"InterfaceCodecExtension\", \"missing __typename field\")\n\t\treturn\n\t}\n\n\t// try to instantiate the type\n\tt := reflect2.TypeByPackageName(gqlPackage, typeName)\n\tif t == nil {\n\t\titer.ReportError(\"InterfaceCodecExtension\", \"cannot find type \"+typeName+\" in package \"+gqlPackage)\n\t\treturn\n\t}\n\n\t// Need to create a pointer to the pointer of the type to be able to be able\n\t// to replace placeholder values with the actual values\n\titem := reflect2.PtrTo(reflect2.PtrTo(t)).New()\n\tval.ToVal(item)\n\tif err := val.LastError(); err != nil {\n\t\titer.ReportError(\"decode\", err.Error())\n\t\treturn\n\t}\n\n\tn := reflect.New(codec.typ.Type1())\n\tn.Elem().Set(reflect.ValueOf(item).Elem().Elem())\n\tcodec.typ.UnsafeSet(ptr, n.UnsafePointer())\n}\n\n// IsEmpty checks if a ptr is empty/nil\nfunc (codec *interfaceCodec) IsEmpty(ptr unsafe.Pointer) bool {\n\treturn codec.typ.UnsafeIsNil(ptr)\n}\n"
  },
  {
    "path": "cli/internal/platform/jsoniter_ext_test.go",
    "content": "package platform\n\nimport (\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encr.dev/cli/internal/platform/gql\"\n)\n\nfunc TestInterfaceDecoder(t *testing.T) {\n\tc := qt.New(t)\n\tenc := jsoniter.Config{}.Froze()\n\tenc.RegisterExtension(NewInterfaceCodecExtension())\n\n\tdata := []byte(`{\n\t\"key\": \"test\",\n\t\"selector\": [\n\t\t{\"__typename\": \"SecretSelectorEnvType\", \"kind\": \"type:production\"},\n\t\t{\"__typename\": \"SecretSelectorSpecificEnv\", \"env\": {\"name\": \"test\"}}\n\t]\n}`)\n\n\tvar group *gql.SecretGroup\n\terr := enc.Unmarshal(data, &group)\n\tc.Assert(err, qt.IsNil)\n\tc.Assert(group, qt.DeepEquals, &gql.SecretGroup{\n\t\tKey: \"test\",\n\t\tSelector: []gql.SecretSelector{\n\t\t\t&gql.SecretSelectorEnvType{Kind: \"type:production\"},\n\t\t\t&gql.SecretSelectorSpecificEnv{Env: &gql.Env{Name: \"test\"}},\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "cli/internal/platform/login.go",
    "content": "package platform\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"golang.org/x/oauth2\"\n\n\t\"encr.dev/internal/conf\"\n)\n\ntype CreateOAuthSessionParams struct {\n\tChallenge   string `json:\"challenge\"`\n\tState       string `json:\"state\"`\n\tRedirectURL string `json:\"redirect_url\"`\n}\n\nfunc CreateOAuthSession(ctx context.Context, p *CreateOAuthSessionParams) (authURL string, err error) {\n\tvar resp struct {\n\t\tAuthURL string `json:\"auth_url\"`\n\t}\n\terr = call(ctx, \"POST\", \"/login/oauth:create-session\", p, &resp, false)\n\treturn resp.AuthURL, err\n}\n\ntype BeginAuthorizationFlowParams struct {\n\tCodeChallenge string\n\tClientID      string\n}\n\ntype BeginAuthorizationFlowResponse struct {\n\t// DeviceCode is the device verification code.\n\tDeviceCode string `json:\"device_code\"`\n\n\t// UserCode is the end-user verification code.\n\tUserCode string `json:\"user_code\"`\n\n\t// VerificationURI is the end-user URL to use to login.\n\tVerificationURI string `json:\"verification_uri\"`\n\n\t// ExpiresIn is the lifetime in seconds of the device code and user code.\n\tExpiresIn int `json:\"expires_in\"`\n\n\t// Interval is the number of seconds to wait between polling requests.\n\t// If not provided, defaults to 5.\n\tInterval int `json:\"interval,omitempty\"`\n}\n\nfunc BeginDeviceAuthFlow(ctx context.Context, p BeginAuthorizationFlowParams) (*BeginAuthorizationFlowResponse, error) {\n\tvals := url.Values{}\n\tvals.Set(\"code_challenge\", p.CodeChallenge)\n\tvals.Set(\"client_id\", p.ClientID)\n\tbody := strings.NewReader(vals.Encode())\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", conf.APIBaseURL+\"/oauth/device-auth\", body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := doPlatformReq(req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, decodeErrorResponse(resp)\n\t}\n\tvar respData BeginAuthorizationFlowResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding response body: %w\", err)\n\t}\n\treturn &respData, nil\n}\n\ntype PollDeviceAuthFlowParams struct {\n\tDeviceCode   string\n\tCodeVerifier string\n}\n\ntype OAuthToken struct {\n\t*oauth2.Token\n\tActor   string `json:\"actor,omitempty\"` // The ID of the user or app that authorized the token.\n\tEmail   string `json:\"email\"`           // empty if logging in as an app\n\tAppSlug string `json:\"app_slug\"`        // empty if logging in as a user\n}\n\nfunc PollDeviceAuthFlow(ctx context.Context, p PollDeviceAuthFlowParams) (*OAuthToken, error) {\n\tvals := url.Values{}\n\tvals.Set(\"grant_type\", \"urn:ietf:params:oauth:grant-type:device_code\")\n\tvals.Set(\"device_code\", p.DeviceCode)\n\tvals.Set(\"code_verifier\", p.CodeVerifier)\n\tbody := strings.NewReader(vals.Encode())\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", conf.APIBaseURL+\"/oauth/token\", body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := doPlatformReq(req, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, decodeErrorResponse(resp)\n\t}\n\n\tvar tok OAuthToken\n\tif err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {\n\t\treturn nil, fmt.Errorf(\"decoding response body: %w\", err)\n\t}\n\treturn &tok, nil\n}\n\ntype ExchangeOAuthTokenParams struct {\n\tChallenge string `json:\"challenge\"`\n\tCode      string `json:\"code\"`\n}\n\ntype OAuthData struct {\n\tToken   *oauth2.Token `json:\"token\"`\n\tActor   string        `json:\"actor,omitempty\"` // The ID of the user or app that authorized the token.\n\tEmail   string        `json:\"email\"`           // empty if logging in as an app\n\tAppSlug string        `json:\"app_slug\"`        // empty if logging in as a user\n}\n\nfunc ExchangeOAuthToken(ctx context.Context, p *ExchangeOAuthTokenParams) (*OAuthData, error) {\n\tvar resp OAuthData\n\terr := call(ctx, \"POST\", \"/login/oauth:exchange-token\", p, &resp, false)\n\treturn &resp, err\n}\n\ntype ExchangeAuthKeyParams struct {\n\tAuthKey string `json:\"auth_key\"`\n}\n\nfunc ExchangeAuthKey(ctx context.Context, p *ExchangeAuthKeyParams) (*OAuthData, error) {\n\tvar resp OAuthData\n\terr := call(ctx, \"POST\", \"/login/auth-key\", p, &resp, false)\n\treturn &resp, err\n}\n"
  },
  {
    "path": "cli/internal/platform/secrets.go",
    "content": "package platform\n\nimport (\n\t\"context\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"encr.dev/cli/internal/platform/gql\"\n)\n\nfunc ListSecretGroups(ctx context.Context, appSlug string, keys []string) ([]*gql.Secret, error) {\n\tquery := `\nquery ListSecretGroups($appSlug: String!, $keys: [String!]) {\n\tapp(slug: $appSlug) {\n\t\tsecrets(keys: $keys) {\n\t\t\tkey\n\t\t\tgroups {\n\t\t\t\tid, etag, description, archivedAt\n\t\t\t\tselector {\n\t\t\t\t\t__typename\n\t\t\t\t\t...on SecretSelectorEnvType {\n\t\t\t\t\t\tkind\n\t\t\t\t\t}\n\t\t\t\t\t...on SecretSelectorSpecificEnv {\n\t\t\t\t\t\tenv { id, name }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tversions { id }\n\t\t\t}\n\t\t}\n\t}\n}`\n\tvar out struct {\n\t\tApp struct {\n\t\t\t*gql.App\n\t\t\tSecrets []*gql.Secret\n\t\t}\n\t}\n\n\tin := graphqlRequest{Query: query, Variables: map[string]any{\"appSlug\": appSlug, \"keys\": keys}}\n\tif err := graphqlCall(ctx, in, &out, true); err != nil {\n\t\treturn nil, err\n\t}\n\treturn out.App.Secrets, nil\n}\n\ntype CreateSecretGroupParams struct {\n\tAppID          string\n\tKey            string\n\tPlaintextValue string\n\tDescription    string\n\tSelector       []gql.SecretSelector\n}\n\nfunc CreateSecretGroup(ctx context.Context, p CreateSecretGroupParams) error {\n\tquery := `\nmutation CreateSecretGroup($input: CreateSecretGroups!) {\n\tcreateSecretGroups(input: $input) { id }\n}`\n\tenvTypes, envIDs, err := mapSecretSelector(p.Selector)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tin := graphqlRequest{Query: query, Variables: map[string]any{\"input\": map[string]any{\n\t\t\"appID\": p.AppID,\n\t\t\"key\":   p.Key,\n\t\t\"entries\": []map[string]any{\n\t\t\t{\n\t\t\t\t\"plaintextValue\": p.PlaintextValue,\n\t\t\t\t\"envTypes\":       envTypes,\n\t\t\t\t\"envIDs\":         envIDs,\n\t\t\t\t\"description\":    p.Description,\n\t\t\t},\n\t\t},\n\t}}}\n\tif err := graphqlCall(ctx, in, nil, true); err != nil {\n\t\treturn errors.Wrap(err, \"create secret group\")\n\t}\n\treturn nil\n}\n\ntype CreateSecretVersionParams struct {\n\tGroupID        string\n\tPlaintextValue string\n\tEtag           string\n}\n\nfunc CreateSecretVersion(ctx context.Context, p CreateSecretVersionParams) error {\n\tquery := `\nmutation CreateSecretVersion($input: CreateSecretVersion!) {\n\tcreateSecretVersion(input: $input) { id }\n}`\n\tin := graphqlRequest{Query: query, Variables: map[string]any{\"input\": map[string]any{\n\t\t\"groupID\":        p.GroupID,\n\t\t\"plaintextValue\": p.PlaintextValue,\n\t\t\"etag\":           p.Etag,\n\t}}}\n\tif err := graphqlCall(ctx, in, nil, true); err != nil {\n\t\treturn errors.Wrap(err, \"create secret version\")\n\t}\n\treturn nil\n}\n\ntype UpdateSecretGroupParams struct {\n\tID   string\n\tEtag *string\n\n\t// Nil fore ach field here means it's kept unchanged.\n\tSelector    []gql.SecretSelector // nil means no changes\n\tArchived    *bool\n\tDelete      *bool\n\tDescription *string\n}\n\nfunc UpdateSecretGroup(ctx context.Context, p UpdateSecretGroupParams) error {\n\tquery := `\nmutation UpdateSecretGroup($input: UpdateSecretGroup!) {\n\tupdateSecretGroup(input: $input) { id }\n}`\n\n\tvar selector map[string]any\n\tif p.Selector != nil {\n\t\tenvTypes, envIDs, err := mapSecretSelector(p.Selector)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tselector = map[string]any{\n\t\t\t\"envTypes\": envTypes,\n\t\t\t\"envIDs\":   envIDs,\n\t\t}\n\t}\n\n\tin := graphqlRequest{Query: query, Variables: map[string]any{\"input\": map[string]any{\n\t\t\"id\":          p.ID,\n\t\t\"etag\":        p.Etag,\n\t\t\"selector\":    selector,\n\t\t\"archived\":    p.Archived,\n\t\t\"delete\":      p.Delete,\n\t\t\"description\": p.Description,\n\t}}}\n\tif err := graphqlCall(ctx, in, nil, true); err != nil {\n\t\treturn errors.Wrap(err, \"update secret group\")\n\t}\n\treturn nil\n}\n\nfunc mapSecretSelector(selector []gql.SecretSelector) (envTypes, envIDs []string, err error) {\n\tenvTypes, envIDs = []string{}, []string{}\n\tfor _, sel := range selector {\n\t\tswitch s := sel.(type) {\n\t\tcase *gql.SecretSelectorEnvType:\n\t\t\tenvTypes = append(envTypes, s.Kind)\n\t\tcase *gql.SecretSelectorSpecificEnv:\n\t\t\tenvIDs = append(envIDs, s.Env.ID)\n\t\tdefault:\n\t\t\treturn nil, nil, errors.Newf(\"unknown secret selector type %T\", s)\n\t\t}\n\t}\n\treturn envTypes, envIDs, nil\n}\n"
  },
  {
    "path": "cli/internal/telemetry/telemetry.go",
    "content": "package telemetry\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/hasura/go-graphql-client\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encore.dev/types/uuid\"\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/xos\"\n)\n\nvar singleton = func() *telemetry {\n\tt := &telemetry{\n\t\tclient: graphql.NewClient(conf.APIBaseURL+\"/graphql\", conf.DefaultClient),\n\t}\n\tpath, err := configPath()\n\tif err != nil {\n\t\treturn t\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// If the file does not exist, telemetry is enabled by default\n\t\t\tt.cfg.Enabled = true\n\t\t\tt.cfg.AnonID = uuid.Must(uuid.NewV4()).String()\n\t\t\tt.cfg.SentEvents = make(map[string]struct{})\n\t\t\t_ = t.saveConfig()\n\t\t\terr = nil\n\t\t}\n\t\treturn t\n\t}\n\terr = json.Unmarshal(data, &t.cfg)\n\tif err != nil {\n\t\tlog.Debug().Err(err).Msg(\"failed to unmarshal telemetry config\")\n\t}\n\treturn t\n}()\n\ntype telemetry struct {\n\tmu     sync.Mutex\n\tcfg    telemetryCfg\n\tclient *graphql.Client\n}\n\ntype telemetryCfg struct {\n\tEnabled      bool                `json:\"enabled\"`\n\tAnonID       string              `json:\"anon_id\"`\n\tSentEvents   map[string]struct{} `json:\"sent_events\"`\n\tShownWarning bool                `json:\"shown_warning\"`\n\tDebug        bool                `json:\"debug\"`\n}\n\ntype TelemetryMessage struct {\n\tEvent       string         `json:\"event\"`\n\tAnonymousId string         `json:\"anonymousId\"`\n\tProperties  map[string]any `json:\"properties,omitempty\"`\n}\n\nfunc (t *telemetry) sendOnce(event string, props ...map[string]any) {\n\tt.mu.Lock()\n\tif _, ok := t.cfg.SentEvents[event]; ok {\n\t\tt.mu.Unlock()\n\t\treturn\n\t}\n\tt.cfg.SentEvents[event] = struct{}{}\n\tif err := t.saveConfig(); err != nil {\n\t\tlog.Debug().Err(err).Msg(\"failed to save telemetry config\")\n\t}\n\tt.mu.Unlock()\n\tif err := t.send(event, props...); err != nil {\n\t\tlog.Debug().Err(err).Msg(\"failed to send telemetry message\")\n\t\tt.mu.Lock()\n\t\tdelete(t.cfg.SentEvents, event)\n\t\tt.mu.Unlock()\n\t}\n}\n\nfunc (t *telemetry) send(event string, props ...map[string]any) error {\n\tvar m struct {\n\t\tResult bool `graphql:\"telemetry(msg: $msg)\"`\n\t}\n\tmessage := TelemetryMessage{\n\t\tEvent:       event,\n\t\tAnonymousId: t.cfg.AnonID,\n\t\tProperties:  fns.MergeMaps(props...),\n\t}\n\tif t.cfg.Debug {\n\t\tdata, err := json.Marshal(message)\n\t\tif err != nil {\n\t\t\tlog.Info().Msgf(\"[telemetry] failed to marshal message\")\n\t\t} else {\n\t\t\tlog.Info().Msgf(\"[telemetry] %s\", string(data))\n\t\t}\n\t}\n\terr := t.client.Mutate(context.Background(), &m, map[string]any{\n\t\t\"msg\": message})\n\tif !m.Result {\n\t\treturn errors.New(\"failed to send telemetry message\")\n\t}\n\treturn err\n}\n\nfunc (t *telemetry) trySend(event string, props ...map[string]any) {\n\tif err := t.send(event, props...); err != nil {\n\t\tlog.Debug().Msg(\"failed to send telemetry message\")\n\t}\n}\n\nfunc (t *telemetry) saveConfig() error {\n\t// Write the telemetry configuration to a file\n\tpath, err := configPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata, err := json.Marshal(t.cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {\n\t\treturn err\n\t}\n\treturn xos.WriteFile(path, data, 0644)\n}\n\nfunc IsEnabled() bool {\n\treturn singleton.cfg.Enabled\n}\n\nfunc SetEnabled(enabled bool) bool {\n\treturn UpdateConfig(singleton.cfg.AnonID, enabled, singleton.cfg.Debug)\n}\n\nfunc SetDebug(debug bool) bool {\n\treturn UpdateConfig(singleton.cfg.AnonID, singleton.cfg.Enabled, debug)\n}\n\nfunc UpdateConfig(anonID string, enabled, debug bool) (changed bool) {\n\tchanged = singleton.cfg.Enabled != enabled ||\n\t\tsingleton.cfg.Debug != debug ||\n\t\tsingleton.cfg.AnonID != anonID\n\tsingleton.cfg.AnonID = anonID\n\tsingleton.cfg.Enabled = enabled\n\tsingleton.cfg.Debug = debug\n\treturn changed\n}\n\nfunc ShouldShowWarning() bool {\n\treturn !singleton.cfg.ShownWarning && IsEnabled()\n}\n\nfunc SetShownWarning() {\n\tsingleton.cfg.ShownWarning = true\n\tif err := singleton.saveConfig(); err != nil {\n\t\tlog.Debug().Err(err).Msg(\"failed to save telemetry config\")\n\t}\n}\n\nfunc SaveConfig() error {\n\treturn singleton.saveConfig()\n}\n\nfunc SendOnce(event string, props ...map[string]any) {\n\tif !IsEnabled() {\n\t\treturn\n\t}\n\tgo singleton.sendOnce(event, props...)\n}\n\nfunc Send(event string, props ...map[string]any) {\n\tif !IsEnabled() {\n\t\treturn\n\t}\n\tgo singleton.trySend(event, props...)\n}\n\nfunc SendSync(event string, props ...map[string]any) {\n\tif !IsEnabled() {\n\t\treturn\n\t}\n\tsingleton.trySend(event, props...)\n}\n\nfunc configPath() (string, error) {\n\tdir, err := os.UserConfigDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(dir, \"encore\", \"telemetry.json\"), nil\n}\n\nfunc GetAnonID() string {\n\treturn singleton.cfg.AnonID\n}\n\nfunc IsDebug() bool {\n\treturn singleton.cfg.Debug\n}\n"
  },
  {
    "path": "cli/internal/update/update.go",
    "content": "package update\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/mod/semver\"\n\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/version\"\n)\n\nvar ErrUnknownVersion = errors.New(\"unknown version\")\n\n// Check checks for the latest Encore version.\n// It reports ErrUnknownVersion if it cannot determine the version.\nfunc Check(ctx context.Context) (latestVersion *LatestVersion, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"update.Check: %w\", err)\n\t\t}\n\t}()\n\n\treleaseAPI, err := url.Parse(\"https://encore.dev/api/releases\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse release api url: %w\", err)\n\t}\n\n\t// Filter the request down to the release for the current version.\n\tqry := releaseAPI.Query()\n\n\t// These three are used to determine the latest release for the given channel, os and arch\n\tqry.Set(\"channel\", string(version.Channel))\n\tqry.Set(\"os\", runtime.GOOS)\n\tqry.Set(\"arch\", runtime.GOARCH)\n\n\t// This is used to determine if the returned release contains security updates not present\n\t// in the currently running version of Encore, as well as if we need to force an upgrade\n\t// on the user due to a critical security issue.\n\tqry.Set(\"current\", version.Version)\n\n\t// For specific app ID's or user ID's we can provide pre-releases to them\n\t// Mainly used if they've encountered a bug and we need to get them a fix asap for testing\n\tif cfg, err := conf.CurrentUser(); err == nil && cfg != nil {\n\t\tqry.Set(\"actor\", cfg.Actor)\n\t}\n\n\treleaseAPI.RawQuery = qry.Encode()\n\n\t// url := \"https://encore.dev/api/releases\"\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", releaseAPI.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"GET %s: responded with %s: %s\", releaseAPI, resp.Status, body)\n\t}\n\n\tlatestVersion = &LatestVersion{}\n\tif err := json.NewDecoder(resp.Body).Decode(latestVersion); err != nil {\n\t\treturn nil, fmt.Errorf(\"GET %s: invalid json: %v\", releaseAPI, err)\n\t}\n\n\tif !latestVersion.Supported && latestVersion.Channel != version.DevBuild {\n\t\treturn nil, ErrUnknownVersion\n\t}\n\n\treturn latestVersion, nil\n}\n\n// LatestVersion contains the parsed response from the update server\ntype LatestVersion struct {\n\t// The channel the release is from\n\tChannel version.ReleaseChannel `json:\"channel\"`\n\n\t// Whether the requested target is supported or not\n\tSupported bool `json:\"supported\"`\n\n\t// The latest version available\n\t// Access via Version() to ensure the version is prefixed with \"v\" for GA releases\n\tRawVersion string `json:\"version\"`\n\n\t// The URL for that version (if supported)\n\tURL string `json:\"url,omitempty\"`\n\n\t// Whether the version contains a security fix from the current version running\n\tSecurityUpdate bool `json:\"security_update\"`\n\n\t// Optional notes about what the security update fixes and why the user should install it\n\tSecurityNotes string `json:\"security_notes,omitempty\"`\n\n\t// If we need to force an upgrade. This is only used for security updates and only for\n\t// the most urgent ones, i.e we should never use it unless the world is on fire.\n\tForceUpgrade bool `json:\"force_upgrade,omitempty\"`\n}\n\n// Version returns the version string referenced by the LatestVersion.\n// ensuring that it is prefixed with \"v\" for GA releases.\nfunc (lv *LatestVersion) Version() string {\n\t// Server side doesn't include the \"v\" in nightly versions.\n\tif lv.Channel == version.GA {\n\t\t// Note: this trim prefix is future proofing in case we decide to start returning versions\n\t\t// which include the \"v\" prefix\n\t\treturn \"v\" + strings.TrimPrefix(lv.RawVersion, \"v\")\n\t}\n\n\treturn lv.RawVersion\n}\n\n// IsNewer returns true if LatestVersion is newer than current\n//\n// This is safe to call on a nil LatestVersion\nfunc (lv *LatestVersion) IsNewer(current string) bool {\n\tif lv == nil {\n\t\treturn false\n\t}\n\n\tswitch lv.Channel {\n\tcase version.GA:\n\t\treturn semver.Compare(lv.Version(), current) > 0\n\tcase version.Nightly:\n\t\treturn nightlyToNumber(lv.Version()) > nightlyToNumber(current)\n\t}\n\n\treturn false\n}\n\n// DoUpgrade upgrades Encore.\n//\n// Adapted from flyctl: https://github.com/superfly/flyctl\nfunc (lv *LatestVersion) DoUpgrade(stdout, stderr io.Writer) error {\n\t// What shell do we need to run?\n\targ := \"-c\"\n\tshell, ok := os.LookupEnv(\"SHELL\")\n\tif !ok {\n\t\t//goland:noinspection GoBoolExpressions\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tshell = \"powershell.exe\"\n\t\t\targ = \"-Command\"\n\t\t} else {\n\t\t\tshell = \"/bin/bash\"\n\t\t}\n\t}\n\n\t// Base script for *nix systems\n\tscript := \"curl -L \\\"https://encore.dev/install.sh\\\" | sh\"\n\n\tbrewManaged := false\n\n\t// Script overrides for windows and systems with homebrew installed\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tscript = \"iwr https://encore.dev/install.ps1 -useb | iex\"\n\tcase \"darwin\", \"linux\":\n\t\t// Upgrade via homebrew if we can\n\t\tif wasInstalledViaHomebrew(shell, arg, lv.Channel) {\n\t\t\tbrewManaged = true\n\t\t\tscript = \"brew upgrade encore --fetch-head\"\n\t\t}\n\t}\n\n\t// Sainty check we can perform the update\n\tswitch lv.Channel {\n\tcase version.GA:\n\t// no-op\n\tcase version.Nightly:\n\t\tif brewManaged {\n\t\t\tscript = \"brew upgrade encore-nightly --fetch-head\"\n\t\t} else {\n\t\t\treturn errors.New(\"nightly can not be automatically updated without homebrew\")\n\t\t}\n\tcase version.Beta:\n\t\tif brewManaged {\n\t\t\tscript = \"brew upgrade encore-beta --fetch-head\"\n\t\t} else {\n\t\t\treturn errors.New(\"beta can not be automatically updated without homebrew\")\n\t\t}\n\tcase version.DevBuild:\n\t\treturn errors.New(\"dev builds can not be automatically updated\")\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown release channel %s\", lv.Channel)\n\t}\n\n\tfmt.Println(\"Running update [\" + script + \"]\")\n\n\tif brewManaged {\n\t\tupdateBrewTap(stdout, stderr)\n\t}\n\n\t// nosemgrep\n\tcmd := exec.Command(shell, arg, script)\n\n\tcmd.Stdout = stdout\n\tcmd.Stderr = stderr\n\tcmd.Stdin = os.Stdin\n\treturn cmd.Run()\n}\n\nfunc nightlyToNumber(version string) int64 {\n\t// version looks like: nightly-20221010\n\tif !strings.HasPrefix(version, \"nightly-\") || len(version) != 16 {\n\t\treturn 0\n\t}\n\n\t// slice(8) removes \"nightly-\"\n\tdate, err := strconv.ParseInt(version[8:], 10, 64)\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\treturn date\n}\n\nfunc wasInstalledViaHomebrew(shell string, arg string, channel version.ReleaseChannel) bool {\n\tif _, err := exec.LookPath(\"brew\"); err != nil {\n\t\treturn false\n\t}\n\n\tformulaName := \"encore\"\n\tif channel == version.Nightly {\n\t\tformulaName = \"encore-nightly\"\n\t} else if channel == version.Beta {\n\t\tformulaName = \"encore-beta\"\n\t}\n\n\tbuf := new(bytes.Buffer)\n\t// nosemgrep\n\tcmd := exec.Command(shell, arg, fmt.Sprintf(\"brew list %s -1\", formulaName))\n\tcmd.Stdout = buf\n\tcmd.Stderr = buf\n\tcmd.Stdin = os.Stdin\n\n\t// No error means it was installed via homebrew, error means homebrew doesn't know about it\n\t// or isn't installed\n\treturn cmd.Run() == nil\n}\n\nfunc updateBrewTap(stdout, stderr io.Writer) {\n\t// Attempt to update the tap if it exists.\n\tvar outBuf bytes.Buffer\n\tcmd := exec.Command(\"brew\", \"--prefix\")\n\tcmd.Stdout = &outBuf\n\tif err := cmd.Run(); err == nil {\n\t\tgitDir := filepath.Join(strings.TrimSpace(outBuf.String()), \"Library\", \"Taps\", \"encoredev\", \"homebrew-tap\")\n\t\tif _, err := os.Stat(gitDir); err == nil {\n\t\t\t// Get the current branch\n\t\t\tbranchName := \"main\"\n\t\t\t{\n\t\t\t\toutBuf.Reset()\n\t\t\t\tcmd := exec.Command(\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\")\n\t\t\t\tcmd.Stdout = &outBuf\n\t\t\t\tcmd.Stderr = stderr\n\t\t\t\tcmd.Dir = gitDir\n\t\t\t\tif err := cmd.Run(); err == nil {\n\t\t\t\t\tbranchName = strings.TrimSpace(outBuf.String())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Only update if we're on the main branch.\n\t\t\tif branchName == \"main\" {\n\t\t\t\tcmd := exec.Command(\"git\", \"pull\", \"--rebase\", \"origin\", \"main\")\n\t\t\t\tcmd.Stdout = stdout\n\t\t\t\tcmd.Stderr = stderr\n\t\t\t\tcmd.Dir = gitDir\n\t\t\t\t_ = cmd.Run()\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "clippy.toml",
    "content": "ignore-interior-mutability = [\"bytes::Bytes\", \"http::header::HeaderName\"]\n\n"
  },
  {
    "path": "context7.json",
    "content": "{\n  \"url\": \"https://context7.com/encoredev/encore\",\n  \"public_key\": \"pk_hiFaGPl6nQmXCFl3gjJvk\"\n}\n"
  },
  {
    "path": "docs/go/ai-integration.md",
    "content": "---\nseotitle: Using Encore with AI Tools\nseodesc: Learn how to set up Encore with AI-powered development tools like Cursor and Claude Code to supercharge your backend development workflow.\ntitle: AI Tools Integration\nsubtitle: Supercharge your development with AI-powered coding assistants\nlang: go\n---\n\nEncore is built for AI-assisted development. Encore-specific rules and [MCP](/docs/go/ai-integration#mcp-server) integration let AI understand your architecture and generate type-safe code that follows your patterns. Run `encore run` to start your app; Encore provisions local infrastructure automatically.\n\nFor production, [self-host](/docs/go/self-host/build) or use [Encore Cloud](https://encore.cloud) to provision infrastructure in your own AWS or GCP account.\n\n## What AI Enables\n\nEncore's declarative APIs and infrastructure primitives give AI a clear model to work with. AI can add databases, pub/sub topics, and other resources with built-in guardrails, and use MCP to introspect your app—services, APIs, databases, and traces—so it can suggest accurate, pattern-consistent code.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"https://encore.cloud/assets/docs/claude-skills.mp4\" type=\"video/mp4\" />\n</video>\n\n## Enabling AI for Your Project\n\nThere are two ways to set up AI support:\n\n- [Method 1: Using the CLI](#method-1-using-the-cli) (recommended)\n- [Method 2: Using Encore Skills](#method-2-using-encore-skills)\n\n### Method 1: Using the CLI\n\n**New projects:** When you run `encore app create`, you'll be prompted to select an AI tool. Encore generates the appropriate configuration files for your chosen tool.\n\n<img src=\"/assets/docs/initllm.png\" className=\"noshadow\" />\n\n**Existing projects:** Run `encore llm-rules init` to add AI support:\n\n```bash\nencore llm-rules init\n```\n\nThis prompts you to select a tool and generates the appropriate configuration file (`.cursorrules`, `CLAUDE.md`, etc.).\n\nBoth commands also set up MCP server configuration for tools that support it (Cursor, Claude Code). If you want to set up MCP manually, see [MCP Server](#mcp-server) below.\n\nSupported tools: Cursor, Claude Code, VS Code, AGENTS.md, and Zed.\n\n### Method 2: Using Encore Skills\n\nUse the [Encore skills package](https://github.com/encoredev/skills) which works with Cursor, Claude Code, GitHub Copilot, and 10+ other AI agents:\n\n```bash\nnpx add-skill encoredev/skills\n```\n\nYou can also install specific skills or target specific agents:\n\n```bash\n# List available skills\nnpx add-skill encoredev/skills --list\n\n# Install to specific agents\nnpx add-skill encoredev/skills -a cursor -a claude-code\n```\n\nThe skills package includes a migration skill that can automatically migrate your existing backend to Encore Go. See the [Migrate using AI agent](/docs/go/migration/ai-migration) guide to learn more.\n\n## MCP Server\n\nEncore's [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server gives AI agents deep introspection into your application: querying databases, calling APIs, inspecting services, and analyzing traces.\n\n### Start the Server\n\nFrom your Encore app directory:\n\n```bash\nencore mcp start\n```\n\nThis displays connection information. Keep it running while using your AI tools.\n\n### Connect Cursor\n\n**Quick setup:** Use this button (update `your-app-id` to your actual app ID):\n\n<a href=\"https://cursor.com/en/install-mcp?name=encore-mcp&config=eyJjb21tYW5kIjoiZW5jb3JlIG1jcCBydW4gLS1hcHA9eW91ci1hcHAtaWQifQ%3D%3D\"><img src=\"https://cursor.com/deeplink/mcp-install-dark.svg\" alt=\"Add encore-mcp MCP server to Cursor\" height=\"32\" class=\"noshadow\" /></a>\n\n**Manual setup:** Create `.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"encore-mcp\": {\n      \"command\": \"encore\",\n      \"args\": [\"mcp\", \"run\", \"--app=your-app-id\"]\n    }\n  }\n}\n```\n\nFind your app ID in the `encore.app` file or in the [Encore dashboard](https://app.encore.dev).\n\n### Connect Claude Code\n\nFrom your Encore app directory:\n\n```bash\nclaude mcp add --transport stdio encore-mcp -- encore mcp run --app=your-app-id\n```\n\nVerify with `claude mcp list`. You should see `encore-mcp` in the list.\n\n## What AI Can Do\n\nWith Encore skills and MCP connected, AI can:\n\n- **Define infrastructure in code** - AI declares databases, pub/sub, cron jobs, buckets, and other [primitives](/docs/go/primitives)\n- **Generate type-safe APIs** - code that follows your patterns and passes validation\n- **Understand architecture** - inspect services and how they connect via MCP\n- **Query databases** - introspect schema and data to generate accurate queries\n- **Debug with tracing** - view request traces, timing, and span details to pinpoint issues\n- **Test instantly** - run `encore run` to test with real infrastructure, not mocks\n\n### In Practice\n\n#### Smarter Debugging with Tracing\n\nAI can access Encore's distributed tracing via MCP to debug issues intelligently. Instead of guessing, AI can view actual request traces, analyze timing across services, and inspect span details to pinpoint exactly where things went wrong. This creates a powerful feedback loop: generate code, test it, analyze the traces, and iterate.\n\n#### Database Introspection\n\nAI can query your actual database schema and data via MCP. This means AI understands your real data model and can generate accurate queries, suggest schema changes, and debug data issues by inspecting actual records.\n\n#### Instant Validation with Real Infrastructure\n\nWhen you run `encore run`, Encore provisions real local infrastructure (databases, pub/sub, etc.). AI can generate code and immediately test it against real services, catching issues early and ensuring the code works before you deploy.\n\nExample prompts:\n\n- \"Add an endpoint that publishes to a pub/sub topic, call it and verify in traces\"\n- \"Query the users database and show accounts created in the last week\"\n- \"Create a new service with CRUD endpoints connected to PostgreSQL\"\n\n## Learn More\n\n- [MCP Server Documentation](/docs/go/cli/mcp) - Complete MCP reference\n- [Encore Skills Repository](https://github.com/encoredev/skills) - Available skills and installation\n- [Quick Start Guide](/docs/go/quick-start) - Build your first Encore app\n"
  },
  {
    "path": "docs/go/cli/cli-reference.md",
    "content": "---\nseotitle: Encore CLI Reference\nseodesc: The Encore CLI lets you run your local development environment, create apps, and much more. See all CLI commands in this reference guide.\ntitle: CLI Reference\nsubtitle: The Encore CLI lets you run your local environment and much more.\nlang: go\n---\n\n## Running\n\n#### Run\n\nRuns your application.\n\n```shell\n$ encore run [--debug] [--watch=true] [--port=<number>] [--listen=<addr>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-w, --watch` | Watch for changes and live-reload | `true` |\n| `--listen` | Address to listen on (e.g. `0.0.0.0:4000`) | |\n| `-p, --port` | Port to listen on | `4000` |\n| `--json` | Display logs in JSON format | `false` |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `--color` | Whether to display colorized output | auto-detected |\n| `--redact` | Redact sensitive data in traces when running locally | `false` |\n| `-l, --level` | Minimum log level to display (`trace\\|debug\\|info\\|warn\\|error`) | |\n| `--debug` | Compile for debugging (`enabled\\|break`) | |\n| `--browser` | Open local dev dashboard in browser on startup (`auto\\|never\\|always`) | `auto` |\n\n#### Test\n\nTests your application\n\nTakes all the same flags as `go test`.\n\n```shell\n$ encore test ./... [go test flags]\n```\n\nAdditional flags recognized by `encore test`:\n\n| Flag | Description |\n| --- | --- |\n| `--codegen-debug` | Dump generated code (for debugging Encore's code generation) |\n| `--prepare` | Prepare for running tests without running them |\n| `--trace` | Write trace information about the parse and compilation process to a file |\n| `--no-color` | Disable colorized output |\n\n#### Check\n\nChecks your application for compile-time errors using Encore's compiler.\n\n```shell\n$ encore check [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `--codegen-debug` | Dump generated code (for debugging Encore's code generation) |\n| `--tests` | Parse tests as well |\n\n#### Exec\n\nRuns executable scripts against the local Encore app.\n\nCompiles and runs a Go script with the local Encore app environment setup.\n\n```shell\n$ encore exec <path/to/command> [...args]\n```\n\nThe command directory should contain Go files with package main with a main function.\n\nThe additional arguments are passed directly to the built binary.\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) |\n\n##### Example\n\nRun a database seed script\n\n```shell\n$ encore exec cmd/seed\n```\n\n## App\n\nCommands to create and link Encore apps\n\n#### Clone\n\nClone an Encore app to your computer\n\n```shell\n$ encore app clone [app-id] [directory]\n```\n\n#### Create\n\nCreate a new Encore app\n\n```shell\n$ encore app create [name] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `--example` | URL to example code to use | |\n| `-l, --lang` | Programming language to use for the app | |\n| `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool | |\n| `--platform` | Whether to create the app with the Encore Platform | `true` |\n\n#### Init\n\nCreate a new Encore app from an existing repository\n\n```shell\n$ encore app init [name] [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-l, --lang` | Programming language to use for the app |\n\n#### Link\n\nLink an Encore app with the server\n\n```shell\n$ encore app link [app-id] [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-f, --force` | Force link even if the app is already linked |\n\n## Auth\n\nCommands to authenticate with Encore\n\n#### Login\n\nLog in to Encore\n\n```shell\n$ encore auth login [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-k, --auth-key` | Auth Key to use for login |\n\n#### Logout\n\nLogs out the currently logged in user\n\n```shell\n$ encore auth logout\n```\n\n#### Signup\n\nCreate a new Encore account\n\n```shell\n$ encore auth signup\n```\n\n#### Whoami\n\nShow the current logged in user\n\n```shell\n$ encore auth whoami\n```\n\n## Daemon\n\nEncore CLI daemon commands\n\n#### Restart\n\nIf you experience unexpected behavior, try restarting the daemon using:\n\n```shell\n$ encore daemon\n```\n\n#### Env\n\nOutputs Encore environment information\n\n```shell\n$ encore daemon env\n```\n\n## Database Management\n\nDatabase management commands\n\n#### Connect to database via shell\n\nConnects to the database via psql shell\n\nDefaults to connecting to your local environment. Specify --env to connect to another environment.\n\nUse `--test` to connect to databases used for integration testing.\nUse `--shadow` to connect to the shadow database, used for database drift detection when using tools like Prisma.\n\n`--test` and `--shadow` imply `--env=local`.\n\n```shell\n$ encore db shell [DATABASE_NAME] [--env=<name>] [flags]\n```\n\n`encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with.\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `-e, --env` | Environment name to connect to | `local` |\n| `-t, --test` | Connect to the integration test database (implies --env=local) | `false` |\n| `--shadow` | Connect to the shadow database (implies --env=local) | `false` |\n| `--write` | Connect with write privileges | `false` |\n| `--admin` | Connect with admin privileges | `false` |\n| `--superuser` | Connect as a superuser | `false` |\n\n#### Connection URI\n\nOutputs a database connection string. Defaults to connecting to your local environment. Specify --env to connect to another environment.\n\n```shell\n$ encore db conn-uri [<db-name>] [--env=<name>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `-e, --env` | Environment name to connect to | `local` |\n| `-t, --test` | Connect to the integration test database (implies --env=local) | `false` |\n| `--shadow` | Connect to the shadow database (implies --env=local) | `false` |\n| `--write` | Connect with write privileges | `false` |\n| `--admin` | Connect with admin privileges | `false` |\n| `--superuser` | Connect as a superuser | `false` |\n\n#### Proxy\n\nSets up local proxy that forwards any incoming connection to the databases in the specified environment.\n\n```shell\n$ encore db proxy [--env=<name>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `-e, --env` | Environment name to connect to | `local` |\n| `-p, --port` | Port to listen on (defaults to a random port) | `0` |\n| `-t, --test` | Connect to the integration test database (implies --env=local) | `false` |\n| `--shadow` | Connect to the shadow database (implies --env=local) | `false` |\n| `--write` | Connect with write privileges | `false` |\n| `--admin` | Connect with admin privileges | `false` |\n| `--superuser` | Connect as a superuser | `false` |\n\n#### Reset\n\nResets the databases for the given services. Use --all to reset all databases.\n\n```shell\n$ encore db reset <database-names...|--all> [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `--all` | Reset all services in the application | `false` |\n| `-t, --test` | Reset databases in the test cluster instead | `false` |\n| `--shadow` | Reset databases in the shadow cluster instead | `false` |\n\n## Code Generation\n\nCode generation commands\n\n#### Generate client\n\nGenerates an API client for your app. For more information about the generated clients, see [this page](/docs/go/cli/client-generation).\n\nBy default, `encore gen client` generates the client based on the version of your application currently running in your local environment.\nYou can change this using the `--env` flag and specifying the environment name.\n\nUse `--lang=<lang>` to specify the language. Supported language codes are:\n\n- `go`: A Go client using the net/http package\n- `typescript`: A TypeScript client using the in-browser Fetch API\n- `javascript`: A JavaScript client using the in-browser Fetch API\n- `openapi`: An OpenAPI spec\n\n```shell\n$ encore gen client [<app-id>] [--env=<name>] [--lang=<lang>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-l, --lang` | Language to generate code for | |\n| `-o, --output` | Filename to write the generated client code to | |\n| `-e, --env` | Environment to fetch the API for | `local` |\n| `-s, --services` | Names of the services to include in the output | |\n| `-x, --excluded-services` | Names of the services to exclude in the output | |\n| `-t, --tags` | Names of endpoint tags to include in the output | |\n| `--excluded-tags` | Names of endpoint tags to exclude in the output | |\n| `--openapi-exclude-private-endpoints` | Exclude private endpoints from the OpenAPI spec | `false` |\n| `--ts:shared-types` | Import types from ~backend instead of re-generating them | `false` |\n| `--target` | An optional target for the client (`leap`) | |\n\n## Logs\n\nStreams logs from your application\n\n```shell\n$ encore logs [--env=prod] [--json] [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-e, --env` | Environment name to stream logs from (defaults to the primary environment) |\n| `--json` | Whether to print logs in raw JSON format |\n| `-q, --quiet` | Whether to print initial message when the command is waiting for logs |\n\n## Kubernetes\n\nKubernetes management commands\n\n#### Configure\n\nUpdates your kubectl config to point to the Kubernetes cluster(s) for the specified environment\n\n```shell\n$ encore k8s configure --env=ENV_NAME\n```\n\n## Secrets Management\n\nSecret management commands\n\n#### Set\n\nSet a secret value for a specific environment:\n\n```shell\n$ encore secret set --env <env-name> <secret-name>\n```\n\nSet a secret value for an environment type:\n\n```shell\n$ encore secret set --type <types> <secret-name>\n```\n\nWhere `<types>` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`.\n\n**Examples**\n\nEntering a secret directly in terminal:\n\n```shell\n$ encore secret set --type dev MySecret\nEnter secret value: ...\nSuccessfully created secret value for MySecret.\n```\n\nPiping a secret from a file:\n\n```shell\n$ encore secret set --type dev,local MySecret < my-secret.txt\nSuccessfully created secret value for MySecret.\n```\n\nNote that this strips trailing newlines from the secret value.\n\n#### List\n\nLists secrets, optionally for a specific key\n\n```shell\n$ encore secret list [keys...]\n```\n\n#### Delete\n\nDeletes a secret value\n\n```shell\n$ encore secret delete <id>\n```\n\n## Namespaces\n\nManage infrastructure namespaces for isolating local infrastructure. See [Infrastructure Namespaces](/docs/go/cli/infra-namespaces) for more details.\n\n#### List\n\nList infrastructure namespaces\n\n```shell\n$ encore namespace list [--output=columns|json]\n```\n\n#### Create\n\nCreate a new infrastructure namespace\n\n```shell\n$ encore namespace create NAME\n```\n\n#### Delete\n\nDelete an infrastructure namespace\n\n```shell\n$ encore namespace delete NAME\n```\n\n#### Switch\n\nSwitch to a different infrastructure namespace. Subsequent commands will use the given namespace by default.\n\nUse `-` as the namespace name to switch back to the previously active namespace.\n\n```shell\n$ encore namespace switch [--create] NAME\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-c, --create` | Create the namespace before switching |\n\n## Config\n\nGets or sets configuration values for customizing the behavior of the Encore CLI.\n\nConfiguration options can be set both for individual Encore applications, as well as globally for the local user.\n\n```shell\n$ encore config <key> [<value>] [flags]\n```\n\nWhen running `encore config` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the `--global` flag.\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `--all` | View all settings |\n| `--app` | Set the value for the current app |\n| `--global` | Set the value at the global level |\n\n## Telemetry\n\nReports the current telemetry status\n\n```shell\n$ encore telemetry\n```\n\n#### Enable\n\nEnables telemetry reporting\n\n```shell\n$ encore telemetry enable\n```\n\n#### Disable\n\nDisables telemetry reporting\n\n```shell\n$ encore telemetry disable\n```\n\n## MCP\n\nMCP (Model Context Protocol) commands for integrating with AI assistants. See [MCP](/docs/go/cli/mcp) for more details.\n\n#### Start\n\nStarts an SSE-based MCP session and prints the SSE URL\n\n```shell\n$ encore mcp start [--app=<app-id>]\n```\n\n#### Run\n\nRuns a stdio-based MCP session\n\n```shell\n$ encore mcp run [--app=<app-id>]\n```\n\n## Random\n\nUtilities for generating cryptographically secure random data.\n\n#### UUID\n\nGenerates a random UUID (defaults to version 4)\n\n```shell\n$ encore rand uuid [-1|-4|-6|-7]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-1, --v1` | Generate a version 1 UUID |\n| `-4, --v4` | Generate a version 4 UUID (default) |\n| `-6, --v6` | Generate a version 6 UUID |\n| `-7, --v7` | Generate a version 7 UUID |\n\n#### Bytes\n\nGenerates random bytes and outputs them in the specified format\n\n```shell\n$ encore rand bytes BYTES [-f <format>]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-f, --format` | Output format (`hex\\|base32\\|base32hex\\|base32crockford\\|base64\\|base64url\\|raw`) | `hex` |\n| `--no-padding` | Omit padding characters from base32/base64 output | `false` |\n\n#### Words\n\nGenerates random 4-5 letter words for memorable passphrases\n\n```shell\n$ encore rand words [--sep=SEPARATOR] NUM\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-s, --sep` | Separator between words | ` ` (space) |\n\n## Deploy\n\nDeploy an Encore app to a cloud environment.\n\nRequires either `--commit` or `--branch` to be specified.\n\n```shell\n$ encore deploy --env=<env-name> (--commit=<sha> | --branch=<name>) [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `--app` | App slug to deploy to (defaults to current app) | |\n| `-e, --env` | Environment to deploy to (required) | |\n| `--commit` | Commit SHA to deploy | |\n| `--branch` | Branch to deploy | |\n| `-f, --format` | Output format (`text\\|json`) | `text` |\n\n## Version\n\nReports the current version of the encore application\n\n```shell\n$ encore version\n```\n\n#### Update\n\nChecks for an update of encore and, if one is available, runs the appropriate command to update it.\n\n```shell\n$ encore version update\n```\n\n## Build\n\nGenerates an image for your app, which can be used to [self-host](/docs/go/self-host/docker-build) your app.\n\n#### Docker\n\nBuilds a portable Docker image of your Encore application.\n\n```shell\n$ encore build docker IMAGE_TAG [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `--base` | Base image to build from | `scratch` |\n| `-p, --push` | Push image to remote repository | `false` |\n| `--cgo` | Enable cgo | `false` |\n| `--config` | Infra configuration file path | |\n| `--skip-config` | Do not read or generate an infra configuration file | `false` |\n| `--services` | Services to include in the image | |\n| `--gateways` | Gateways to include in the image | |\n| `--os` | Target operating system | `linux` |\n| `--arch` | Target architecture (`amd64\\|arm64`) | `amd64` |\n\n## LLM Rules\n\nGenerate LLM rules in an existing app\n\n#### Init\n\nInitialize the LLM rules files\n\n```shell\n$ encore llm-rules init [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool (`cursor\\|claudecode\\|vscode\\|agentsmd\\|zed`) |\n"
  },
  {
    "path": "docs/go/cli/client-generation.md",
    "content": "---\nseotitle: Automatic API Client Generation\nseodesc: Learn how you can use automatic API client generation to get clients for your backend. See how to integrate with your frontend using a type-safe generated client.\ntitle: Client Library Generation\nsubtitle: Stop writing the same types everywhere\nlang: go\n---\n\nEncore makes it simple to write scalable distributed backends by allowing you to make function calls that Encore translates into RPC calls. Encore also generates API clients with interfaces that look like the original Go functions, with the same parameters and response signature as the server.\n\nThe generated clients are single files that use only the standard functionality of the target language, with full type safety. This allow anyone to look at the generated client and understand exactly how it works.\n\nThe structure of the generated code varies by language, to ensure it's idiomatic and easy to use, but always includes all publicly accessible endpoints, data structures, and documentation strings.\n\nEncore currently supports generating the following clients:\n- **Go** - Using `net/http` for the underlying HTTP transport.\n- **TypeScript** - Using the browser `fetch` API for the underlying HTTP client.\n- **JavaScript** - Using the browser `fetch` API for the underlying HTTP client.\n- **OpenAPI** - Using the OpenAPI Specification's language-agnostic interface to HTTP APIs. (Experimental)\n\nIf there's a language you think should be added, please submit a pull request or create a feature\nrequest on [GitHub](https://github.com/encoredev/encore/issues/new), or [reach out on Discord](/discord).\n\n<Callout type=\"important\">\n\nIf you ship the generated client to end customers, keep in mind that old clients will continue to be used after you make changes. To prevent issues with the generated clients, avoid making breaking changes in APIs that your clients access.\n\n</Callout>\n\n<br />\n\n## Generating a Client\n\nTo generate a client, use the `encore gen client` command. It generates a type-safe client using the most recent API metadata\nrunning in a particular environment for the given Encore application. For example:\n\n```shell\n# Generate a TypeScript client for calling the hello-a8bc application based on the primary environment\nencore gen client hello-a8bc --output=./client.ts\n\n# Generate a Go client for the hello-a8bc application based on the locally running code\nencore gen client hello-a8bc --output=./client.go --env=local\n\n# Generate an OpenAPI client for the hello-a8bc application based on the primary environment\nencore gen client hello-a8bc --lang=openapi --output=./openapi.json\n```\n\n### Environment Selection\n\nBy default, `encore gen client` generates the client based on the version of your application currently running in your local environment.\nYou can change this using the `--env` flag and specifying the environment name.\n\n<Callout type=\"info\">\n\nThe generated client can be used with any environment, not just the one it was generated for. However, the APIs, data structures\nand marshalling logic will be based on whatever is present and running in that environment at the point in time the client is generated.\n\n</Callout>\n\n### Service filtering\n\nBy default `encore gen client` outputs code for all services with at least one publicly accessible (or authenticated) API.\nYou can narrow down this set of services by specifying the `--services` (or `-s`) flag. It takes a comma-separated list\nof service names.\n\nFor example, to generate a typescript client for the `email` and `users` services, run:\n```shell\nencore gen client --services=email,users -o client.ts\n```\n\n### Output Mode\n\nBy default the client's code will be output to stdout, allowing you to pipe it into your clipboard, or another tool. However,\nusing `--output` you can specify a file location to write the client to. If output is specified, you do not need to specify\nthe language as Encore will detect the language based on the file extension.\n\n\n### Example Script\nYou could combine this into a `package.json` file for your Typescript frontend, to allow you to run `npm run gen` in that\nproject to update the client to match the code running in your staging environment.\n```json\n{\n  \"scripts\": {\n    // ...\n    \"gen\": \"encore gen client hello-a8bc --output=./client.ts --env=staging\"\n    // ...\n  }\n}\n```\n\n## Using the Client\n\nThe generated client has all the data structures required as parameters or returned as response values as needed by any\nof the public or authenticated API's of your Encore application. Each service is exposed as object on the client, with\neach public or authenticated API exposed as a function on those objects.\n\nFor instance, if you had a service called `email` with a function `Send`, on the generated client you would call this\nusing; `client.email.Send(...)`.\n\nFor more tips and examples of using a generated JavaScript/Typescript client, see the [Integrate with a web frontend](/docs/how-to/integrate-frontend#generating-a-request-client) docs.\n\n### Creating an instance\n\nWhen constructing a client, you need to pass a `BaseURL` as the first parameter; this is the URL at which the API can\nbe accessed. The client provides two helpers:\n\n- `Local` - This is a constant provided, which will always point at your locally running instance environment.\n- `Environment(\"name\")` - This is a function which allows you to specify an environment by name\n\nHowever, BaseURL is a string, so if the two helpers do not provide enough flexibility you can pass any valid URL to be\nused as the BaseURL.\n\n### Authentication\n\nIf your application has any API's which require [authentication](/docs/develop/auth), then additional options will generated\ninto the client, which can be used when constructing the client. Just like with API's schemas, the data type required by\nyour application's `auth handler` will be part of the client library, allowing you to set it in two ways:\n\nIf your credentials won't change during the lifetime of the client, simply passing the authentication data to the client\nthrough the `WithAuth` (Go) or `auth` (TypeScript) options.\n\nHowever, if the authentication credentials can change, you can also pass a function which will be called before each request\nand can return a new instance of the authentication data structure or return the existing instance.\n\n\n### HTTP Client Override\n\nIf required, you can override the underlying HTTP implementation with your own implementation. This is useful if you want\nto perform logging of the requests being made, or route the traffic over a secured tunnel such as a VPN.\n\nIn Go this can be configured using the `WithHTTPClient` option. You are required to provide an implementation of the\n`HTTPDoer` interface, which the [http.Client](https://pkg.go.dev/net/http#Client) implements. For TypeScript clients,\nthis can be configured using the `fetcher` option and must conform to the same prototype as the browsers inbuilt [fetch\nAPI](https://developer.mozilla.org/en-US/docs/Web/API/fetch).\n\n### Structured Errors\n\nErrors created or wrapped using Encore's [`errs package`](/docs/develop/errors) will be returned to the client and deserialized\nas an `APIError`, allowing the client to perform adaptive error handling based on the type of error returned. You can perform\na type check on errors caused by calling an API to see if it is an `APIError`, and once cast as an `APIError` you can access\nthe `Code`, `Message` and `Details` fields. For TypeScript Encore generates a `isAPIError` type guard which can be used.\n\nThe `Code` field is an enum with all the possible values generated in the library, alone with description of when we\nwould expect them to be returned by your API. See the [errors documentation](/docs/develop/errors#error-codes) for\nan online reference of this list.\n\n## Example CLI Tool\n\nFor instance, we could build a simple CLI application to use our [url shortener](/docs/tutorials/rest-api), and handle\nany structured errors in a way which makes sense for that error code.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"os\"\n    \"time\"\n\n    \"shorten_cli/client\"\n)\n\nfunc main() {\n    // Create a new client with the default BaseURL\n    client, err := client.New(\n        client.Environment(\"production\"),\n        client.WithAuth(os.Getenv(\"SHORTEN_API_KEY\")),\n    )\n    if err != nil {\n        panic(err)\n    }\n\n    // Timeout if the request takes more than 5 seconds\n    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n    defer cancel()\n\n    // Call the Shorten function in the URL service\n    resp, err := client.Url.Shorten(\n        ctx,\n        client.UrlShortenParams{ URL: os.Args[1] },\n    )\n    if err != nil {\n        // Check the error returned\n        if err, ok := err.(*client.APIError); ok {\n            switch err.Code {\n            case client.ErrUnauthenticated:\n                fmt.Println(\"SHORTEN_API_KEY was invalid, please check your environment\")\n                os.Exit(1)\n            case client.ErrAlreadyExists:\n                fmt.Println(\"The URL you provided was already shortened\")\n                os.Exit(0)\n            }\n        }\n        panic(err) // if here then something has gone wrong in an unexpected way\n    }\n    fmt.Printf(\"https://short.encr.app/%s\", resp.ID)\n}\n```\n"
  },
  {
    "path": "docs/go/cli/config-reference.md",
    "content": "---\nseotitle: Encore CLI Configuration Options\nseodesc: Configuration options to customize the behavior of the Encore CLI.\ntitle: Configuration Reference\nsubtitle: Configuration options to customize the behavior of the Encore CLI.\nlang: go\n---\n\n\nThe Encore CLI has a number of configuration options to customize its behavior.\n\nConfiguration options can be set both for individual Encore applications, as well as\nglobally for the local user.\n\nConfiguration options can be set using `encore config <key> <value>`,\nand options can similarly be read using `encore config <key>`.\n\nWhen running `encore config` within an Encore application, it automatically\nsets and gets configuration for that application.\n\nTo set or get global configuration, use the `--global` flag.\n\n## Configuration files\n\nThe configuration is stored in one ore more TOML files on the filesystem.\n\nThe configuration is read from the following files, in order:\n\n### Global configuration\n* `$XDG_CONFIG_HOME/encore/config`\n* `$HOME/.config/encore/config`\n* `$HOME/.encoreconfig`\n\n### Application-specific configuration\n* `$APP_ROOT/.encore/config`\n\nWhere `$APP_ROOT` is the directory containing the `encore.app` file.\n\nThe files are read and merged, in the order defined above, with latter files taking precedence over earlier files.\n\n## Configuration options\n\n#### run.browser\nType: string<br/>\nDefault: auto<br/>\nMust be one of: always, never, or auto\n\nWhether to open the Local Development Dashboard in the browser on `encore run`.\nIf set to \"auto\", the browser will be opened if the dashboard is not already open.\n\n"
  },
  {
    "path": "docs/go/cli/infra-namespaces.md",
    "content": "---\nseotitle: Infrastructure Namespaces\nseodesc: Learn how Encore's infrastructure namespaces makes it easy to task switch. Stash your infrastructure state and switch to a different task with a single command.\ntitle: Infrastructure Namespaces\nsubtitle: Task switching made easy\nlang: go\n---\n\nEncore's CLI allows you to create and switch between multiple, independent *infrastructure namespaces*.\nInfrastructure namespaces are isolated from each other, and each namespace contains its own independent data.\n\nThis makes it trivial to switch tasks, confident your old state and data will be waiting for you when you return.\n\nIf you've ever worked on a new feature that involves making changes to the database schema,\nonly to context switch to reviewing a Pull Request and had to reset your database, you know the feeling.\n\nWith Encore's infrastructure namespaces, this is a problem of the past.\nRun `encore namespace switch --create pr:123` (or `encore ns switch -c pr:123` for short) to create and switch to a new namespace.\n\nThe next `encore run` will run in the new namespace, with a completely fresh database.\nWhen you're done, run `encore namespace switch -` to switch back to your previous namespace.\n\n## Usage\n\nBelow are the commands for working with namespaces.\nNote that you can use `encore ns` as a short form for `encore namespace`.\n\n```shell\n# List your namespaces (* indicates the current namespace)\n$ encore namespace list\n\n# Create a new namespace\n$ encore namespace create my-ns\n\n# Switch to a namespace\n$ encore namespace switch my-ns\n\n# Switch to a namespace, creating it if it doesn't exist\n$ encore namespace switch --create my-ns\n\n# Switch to the previous namespace\n$ encore namespace switch -\n\n# Delete a namespace (and all associated data)\n$ encore namespace delete my-ns\n```\n\nMost other Encore commands that interact or use infrastructure take an optional\n`--namespace` (`-n` for short) that overrides the current namespace. If left unspecified,\nthe current namespace is used.\n\nFor example:\n\n```shell\n# Run the app using the \"my-ns\" namespace\n$ encore run --namespace my-ns\n\n# Open a database shell to the \"my-ns\" namespace\n$ encore db shell DATABASE_NAME --namespace my-ns\n\n# Reset all databases within the \"my-ns\" namespace\n$ encore db reset --all --namespace my-ns\n```\n"
  },
  {
    "path": "docs/go/cli/mcp.md",
    "content": "---\nseotitle: Encore MCP Server\nseodesc: Encore's Model Context Protocol (MCP) server provides deep introspection of your application to AI development tools.\ntitle: MCP Server\nsubtitle: The Model Context Provider (MCP) exposes tools that provide application context to LLMs.\nlang: go\n---\n\nEncore provides an MCP server that implements the [Model Context Protocol](https://modelcontextprotocol.io/introduction), an open standard that enables large language models (LLMs) to access contextual information about your application. Think of MCP as a standardized interface—like a \"USB-C port for AI applications\"—that connects your Encore app's data and functionality to any LLM that supports the protocol.\n\nYou can connect to Encore's MCP server from any MCP host (such as Claude Desktop, IDEs, or other AI tools) using either Server-Sent Events (SSE) or stdio transport. To set up this connection, simply run:\n\n```bash\ncd my-encore-app\nencore mcp start\n\n  MCP Service is running!\n\n  MCP SSE URL:        http://localhost:9900/sse?app=your-app-id\n  MCP stdio Command:  encore mcp run --app=your-app-id\n```\n\nCopy the appropriate URL or command to your MCP host's configuration, and you're ready to give your AI assistants rich context about your application.\n\n## Example: Integrating with Cursor\n\n[Cursor](https://cursor.com) is one of the most popular AI powered IDE's, and it's simple to use Encore's MCP server together with Cursor. \n\nIn order to add the Encore MCP server to Cursor, the fastest way is via the button below (make sure to update `your-app-id` in the configuration to your actual Encore app ID).\n\n<a href=\"https://cursor.com/en/install-mcp?name=encore-mcp&config=eyJjb21tYW5kIjoiZW5jb3JlIG1jcCBydW4gLS1hcHA9eW91ci1hcHAtaWQifQ%3D%3D\"><img src=\"https://cursor.com/deeplink/mcp-install-dark.svg\" alt=\"Add encore-mcp MCP server to Cursor\" height=\"32\" class=\"noshadow\" /></a>\n\nIf you prefer to configure it manually, create the file `.cursor/mcp.json` with the following settings:\n\n```json\n{\n    \"mcpServers\": {\n        \"encore-mcp\": {\n            \"command\": \"encore\",\n            \"args\": [\"mcp\", \"run\", \"--app=your-app-id\"]\n        }\n    }\n}\n```\n\nLearn more in [Cursor's MCP docs](https://docs.cursor.com/context/model-context-protocol)\n\nNow when using Cursor's Agent mode, you can ask it to do advanced actions, such as:\n\n\"Add an endpoint that publishes to a pub/sub topic, call it and verify that the publish is in the traces\"\n\n## Command Reference\n\n#### Start\n\nStarts an SSE-based MCP server and displays connection information.\n\n```shell\n$ encore mcp start [--app=<app-id>]\n```\n\n#### Run\n\nEstablishes an stdio-based MCP session. This command is typically used by MCP hosts to communicate with the server through standard input/output streams.\n\n```shell\n$ encore mcp run [--app=<app-id>]\n```\n\n## Exposed Tools\n\nEncore's MCP server exposes the following tools that provide AI models with detailed context about your application. These tools enable LLMs to understand your application's structure, retrieve relevant information, and take actions within your system.\n\n#### Database Tools\n\n- **get_databases**: Retrieve metadata about all SQL databases defined in the application, including their schema, tables, and relationships.\n- **query_database**: Execute SQL queries against one or more databases in the application.\n\n#### API Tools\n\n- **call_endpoint**: Make HTTP requests to any API endpoint in the application.\n- **get_services**: Retrieve comprehensive information about all services and their endpoints in the application.\n- **get_middleware**: Retrieve detailed information about all middleware components in the application.\n- **get_auth_handlers**: Retrieve information about all authentication handlers in the application.\n\n#### Trace Tools\n\n- **get_traces**: Retrieve a list of request traces from the application, including their timing, status, and associated metadata.\n- **get_trace_spans**: Retrieve detailed information about one or more traces, including all spans, timing information, and associated metadata.\n\n#### Source Code Tools\n\n- **get_metadata**: Retrieve the complete application metadata, including service definitions, database schemas, API endpoints, and other infrastructure components.\n- **get_src_files**: Retrieve the contents of one or more source files from the application.\n\n#### PubSub Tools\n\n- **get_pubsub**: Retrieve detailed information about all PubSub topics and their subscriptions in the application.\n\n#### Storage Tools\n\n- **get_storage_buckets**: Retrieve comprehensive information about all storage buckets in the application.\n- **get_objects**: List and retrieve metadata about objects stored in one or more storage buckets.\n\n#### Cache Tools\n\n- **get_cache_keyspaces**: Retrieve comprehensive information about all cache keyspaces in the application.\n\n#### Metrics Tools\n\n- **get_metrics**: Retrieve comprehensive information about all metrics defined in the application.\n\n#### Cron Tools\n\n- **get_cronjobs**: Retrieve detailed information about all scheduled cron jobs in the application.\n\n#### Secret Tools\n\n- **get_secrets**: Retrieve metadata about all secrets used in the application.\n\n#### Documentation Tools\n\n- **search_docs**: Search the Encore documentation using Algolia's search engine.\n- **get_docs**: Retrieve the full content of specific documentation pages.\n\n"
  },
  {
    "path": "docs/go/cli/telemetry.md",
    "content": "---\nseotitle: Encore Telemetry\nseodesc: Encore collects telemetry data about app usage\ntitle: Telemetry\nlang: go\n---\nTelemetry helps us improve the Encore by collecting usage data. This data provides insights into how Encore is used, enabling us to make informed decisions to enhance performance, add new features, and fix bugs more efficiently.\n\nEncore only collects telemetry data in the local development tools and the Encore Cloud dashboard. It does **not** collect any telemetry data from your running applications or cloud services, ensuring complete privacy and security for your operations.\n\n## Why We Collect Data\n\nWe collect telemetry data for several important reasons:\n\n1. **Improvement of Features**: Understanding which features are most used helps us prioritize improvements and new feature development.\n2. **Performance Monitoring**: Tracking performance metrics enables us to identify and resolve issues, ensuring a smoother user experience.\n3. **Bug Detection**: Telemetry data can help us detect and fix bugs faster by providing context on how and when issues occur.\n4. **User Experience**: Insights from telemetry data guide us in making Encore more intuitive and user-friendly.\n\n## How Data is Collected\n\nEncore collects data in a way that prioritizes user privacy and security. Here's how we do it:\n\n1. **User Identifiable Data**: The data collected includes identifiable information that helps us understand specific user interactions and contexts.\n2. **Types of Data**: We collect data on usage patterns, performance metrics, and error reports.\n3. **Secure Transmission**: All data is transmitted securely using industry-standard encryption protocols.\n4. **Minimal Impact**: Data collection is designed to have minimal impact on Encore's performance.\n\n### Example of Data Being Sent\n\nHere is an example of the type of data that is sent:\n\n```json\n{\n    \"event\": \"app.create\",\n    \"anonymousId\": \"a-uuid-unique-for-the-installation\",\n    \"properties\": {\n        \"error\": false,\n        \"lang\": \"go\",\n        \"template\": \"graphql\"\n    }\n}\n```\n\n## Data We Don't Collect\n\nAt Encore, we prioritize your privacy and ensure that no sensitive data is collected through our telemetry. Specifically, we do not collect:\n\n1. **Environment Variables**: We do not collect any environment variables set in your development or production environments.\n2. **File Paths**: The specific paths of your files and directories are not collected.\n3. **Contents of Files**: We do not access or collect the contents of your code files or any other files in your projects.\n4. **Logs**: No log files from your application or development environment are collected.\n5. **Serialized Errors**: We do not collect serialized errors that may contain sensitive information.\n\nOur goal is to gather useful data that helps improve Encore while ensuring that your sensitive information remains private and secure.\n\n## Disabling Telemetry\n\nWhile telemetry helps us improve Encore, we understand that some users may prefer to opt out. Disabling telemetry is straightforward and can be done in two ways:\n\n1. **Using the CLI Command**: You can disable telemetry by executing a simple command in your terminal.\n\n   ```sh\n   encore telemetry disable\n   ```\n\n2. **Setting an Environment Variable**: Alternatively, you can disable telemetry by setting the `DISABLE_ENCORE_TELEMETRY` environment variable.\n\n   ```sh\n   export DISABLE_ENCORE_TELEMETRY=1\n   ```\n\n3. **Confirmation**: After disabling telemetry, either by the CLI command or environment variable, you will receive a confirmation message indicating that telemetry has been successfully disabled.\n\n4. **Re-enabling Telemetry**: If you decide to re-enable telemetry later, you can do so with the following CLI command:\n\n   ```sh\n   encore telemetry enable\n   ```\n\n## Debugging Telemetry\n\nFor users who want more visibility into what telemetry data is being sent, you can enable debug mode:\n\n1. **Setting Debug Mode**: Enable debug mode by setting the `ENCORE_TELEMETRY_DEBUG` environment variable.\n\n   ```sh\n   export ENCORE_TELEMETRY_DEBUG=1\n   ```\n\n2. **Log Statements**: When debug mode is enabled, a log statement prepended by `[telemetry]` will be printed every time telemetry data is sent.\n\n## Conclusion\n\nTelemetry is a vital tool for improving Encore, but we respect your choice regarding data sharing. With easy-to-use commands and environment variables, you can manage your telemetry settings as you see fit. If you have any further questions or need assistance, please refer to our support documentation or contact our support team.\n\nThank you for helping us make Encore better!\n"
  },
  {
    "path": "docs/go/community/contribute.md",
    "content": "---\nseotitle: How to contribute to Encore Open Source Project\nseodesc: Learn how to contribute to the Encore Open Source project by submitting pull requests, reporting bugs, or contributing documentation or example projects.\ntitle: Ways to contribute\nsubtitle: Guidelines for contributing to Encore\nlang: go\n---\n\nWe’re so excited that you are interested in contributing to Encore! All contributions are welcome, and there are several valuable ways to contribute.\n\n### Open Source Project\n\nIf you want to contribute to the Encore Open Source project, you can submit a pull request on [GitHub](https://github.com/encoredev/encore/pulls).\n\n### Report issues\n\nIf you have run into an issue or think you’ve found a bug, please report it via the [issue tracker](https://github.com/encoredev/encore/issues).\n\n### Add or update docs\n\nIf there’s something you think would be helpful to add to the docs or if there’s something that seems out of date, we appreciate your input.\nYou can view the docs and contribute fixes or improvements directly in [GitHub](https://github.com/encoredev/encore/tree/main/docs).\n\nYou can also email your feedback to us at [hello@encore.dev](mailto:hello@encore.dev).\n\n### Blog posts\n\nIf you’ve built something cool using Encore, we’d really like you to talk about it! We love it when developers share their projects on blogs and on Twitter.\n\nUse the hashtag **#builtwithencore** and we’ll have an easier time finding your work. – We might also showcase it on the [Encore Twitter account](https://twitter.com/encoredotdev)!\n\n### Meetups & Workshops\n\nOrganizing a meetup or workshop is a great way to connect with other developers using Encore. It can also be a great first step in trying out Encore for development in your company or other professional organization.\n\nIf you want help with organizing or planning an event, please don’t hesitate to reach out to us via email at [hello@encore.dev](mailto:hello@encore.dev).\n"
  },
  {
    "path": "docs/go/community/get-involved.md",
    "content": "---\nseotitle: Encore's Open Source Developer Community\nseodesc: Learn how to engage in the Open Source Developer Community supporting Encore.\ntitle: Community\nsubtitle: Join the most pioneering developer community!\nlang: go\n---\n\nDevelopers building with Encore are forward-thinkers, who are working on exciting and innovative applications.\n\nWe rely on this group's feedback, and contributions to the Open Source project, to improve Encore for developers everywhere.\nGetting involved is a fantastic way of finding support and inspiration among peers.\n\nEveryone is welcome in the Encore community, and we hope you to get involved too!\n\n## Get involved\n\nThere are many ways to get involved. Here's where you can start straight away.\n\n<p className=\"flex items-center gap-x-2 lead-xsmall\">\n     <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 16 16\" fill=\"#111111\" stroke=\"none\">\n      <path fillRule=\"evenodd\"\n            d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n    </svg>\n    <a href=\"https://github.com/encoredev/encore\">Contribute on GitHub</a>\n</p>\n\nUse GitHub to report bugs, feedback on proposals, or contribute your ideas.\n\n<p className=\"flex items-center gap-x-2 lead-xsmall\">\n   <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 -28.5 256 256\">\n        <path fill=\"#111111\"\n            d=\"M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z\" />\n    </svg>\n    <a href=\"https://encore.dev/discord\">Join Discord</a>\n</p>\n\nConnect with fellow Encore developers, ask questions, or just hang out!\n\n<p className=\"flex items-center gap-x-2 lead-xsmall\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"#111111\" stroke=\"none\">\n        <path d=\"M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z\" />\n    </svg>\n    <a href=\"https://twitter.com/encoredotdev\">Follow on Twitter</a>\n</p>\n\nFollow Encore on Twitter to keep up with the latest. Share what you've built to help spread the word about the project.\n\n### Contribute to the project\n\nWant to make a contribution to Encore? Great, start by reading about the different [ways to contribute](/docs/go/community/contribute).\n\n### Feedback on the Roadmap\n\n[The Encore Roadmap](https://encore.dev/roadmap) is public. It's open to your comments, feature requests, and you can vote on existing entries.\n\n## Community Governance\n\nWe recommend everyone read the [Community Principles](/docs/go/community/principles).\n\nIf you need assistance, have concerns, or have questions for the Community team, please email us at [support@encore.dev](mailto:support@encore.dev).\n"
  },
  {
    "path": "docs/go/community/open-source.md",
    "content": "---\nseotitle: Encore is Open Source\nseodesc: We believe Open Source is key to a sustainable and prosperous technology community. Encore builds on Open Source software, and is itself Open Source.\ntitle: Open Source\nsubtitle: Encore is Open Source Software\nlang: go\n---\n\nWe believe Open Source is key to a long-term sustainable and prosperous technology community. Encore builds on Open Source software, and is largely Open Source itself.\n\n## License\n\nEncore's Backend Framework, parser, and compiler are Open Source under Mozilla Public License 2.0.\n\n> The MPL is a simple copyleft license. The MPL's \"file-level\" copyleft is designed to encourage contributors to share modifications they make to your code, while still allowing them to combine your code with code under other licenses (open or proprietary) with minimal restrictions.\n\nYou can learn more about MPL 2.0 on [the official website](https://www.mozilla.org/en-US/MPL/2.0/FAQ/).\n\n## Contribute\n\nContributions to improve Encore are very welcome. Contribute to Encore on [GitHub](https://github.com/encoredev/encore).\n"
  },
  {
    "path": "docs/go/community/principles.md",
    "content": "---\nseotitle: Encore Community Principles\nseodesc: Everyone is welcome in the Encore community, and we want everyone to feel at home and free to contribute.\ntitle: Community principles\nsubtitle: Everyone belongs in the Encore community\nlang: go\n---\n\nEveryone is welcome in the Encore community, and it is of utmost importance to us that everyone is able to feel at home and contribute.\n\nTherefore we as maintainers, and you as a contributor, must pledge to make participation in our community a harassment-free experience for everyone, regardless of: age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity.\n\n### Code of Conduct\n\nTo this end, the Encore community is guided by the [Contributor Covenant 2.0 Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/) to ensure everyone is welcome and able to participate. "
  },
  {
    "path": "docs/go/community/submit-template.md",
    "content": "---\nseotitle: Submit a Template to Encore's Templates repo\nseodesc: Learn how to contribute to Encore's Templates repository and get features in the Encore Templates marketplace.\ntitle: Submit a Template\nsubtitle: Your contributions help other developers build\nlang: go\n---\n\n[Templates](/templates) help and inspire developers to build applications using Encore.\n\nYou are welcome to contribute your own templates!\n\nTwo types of templates that are especially useful:\n- **Starters:** Runnable Encore applications for others to use as is, or take inspiration from.\n- **Bits:** Re-usable code samples to solve common development patterns or integrate Encore applications with third-party APIs and services.\n\n## Submit your contribution\n\nContribute a template by submitting a Pull Request to the [Open Source Examples Repo](https://github.com/encoredev/examples): `https://github.com/encoredev/examples`\n\n### Submitting Starters\n\nFollow these steps to submit a **Starter**:\n\n1. Fork the repo.\n2. Create a new folder in the root directory of the repo, this is where you will place your template. — Use a short folder name as your template will be installable via the CLI, like so: `encore app create APP-NAME --example=<TEMPLATE_FOLDER_NAME>`\n3. Include a `README.md` with instructions for how to use the template. We recommend following [this format](https://github.com/encoredev/examples/blob/8c7e33243f6bfb1b2654839e996e9a924dcd309e/uptime/README.md).\n\nOnce your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website.\n\n### Submitting Bits\n\nFollow these steps to submit your **Bits**:\n\n1. Fork the repo.\n2. Create a new folder inside the `bits` folder in the repo and place your template inside it. Use a short folder name as your template will soon be installable via the CLI.\n3. Include a `README.md` with instructions for how to use the template.\n\nOnce your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website.\n\n## Contribute from your own repo\n\nIf you don't want to contribute code to the examples repo, but still want to be featured on the [Templates page](/templates), please contact us at [hello@encore.dev](mailto:hello@encore.dev).\n\n## Dynamic Encore AppID\n\nIn most cases, you should avoid hardcoding an `AppID` in your template's source code. Instead, use the notation `{{ENCORE_APP_ID}}`.\n\nWhen a developer creates an app using the template, `{{ENCORE_APP_ID}}` will be dymically replaced with their new and unique `AppID`, meaning they will not need to make any manual code adjustments.\n"
  },
  {
    "path": "docs/go/concepts/application-model.md",
    "content": "---\nseotitle: Encore Application Model\nseodesc: How Encore understands your application using static analysis\ntitle: Encore Application Model\nsubtitle: How Encore understands your application\nlang: go\n---\n\nEncore works by using static analysis to understand your application. This is a fancy term for parsing and analyzing the code you write and creating a graph of how your application works. This graph closely represents your own mental model of the system: boxes and arrows that represent systems and services that communicate with other systems, pass data and connect to infrastructure. We call it the Encore Application Model.\n\nBecause the Open Source framework, parser, and compiler, are all designed together, Encore can ensure 100% accuracy when creating the application model. Any deviation is caught as a compilation error.\n\nUsing this model, Encore can provide tools to solve problems that normally would be up to the developer to do manually. From creating architecture diagrams and API documentation to provisioning cloud infrastructure.\n\nWe're continuously expanding on Encore's capabilities and are building a new generation of developer tools that are enabled by Encore's understanding of your application.\n\nThe framework, parser, and compiler that enable this are all [Open Source](https://github.com/encoredev/encore).\n\n<img src=\"/assets/docs/flow-diagram.png\" title=\"Encore Application Model\" className=\"mx-auto md:max-w-lg\"/>\n\n## Standardization brings clarity\n\nDevelopers make dozens of decisions when creating a backend application. Deciding how to structure the codebase, defining API schemas, picking underlying infrastructure, etc. The decisions often come down to personal preferences, not technical rationale. This creates a huge problem in the form of fragmentation! When every stack looks different, all tools have to be general purpose.\n\nWhen you adopt Encore, many of these stylistic decisions are already made for you. The Encore framework ensures your application follows modern best practices. And when you run your application, Encore's Open Source parser and compiler check that you're sticking to the standard. This means you're free to focus your energy on what matters: writing your application's business logic."
  },
  {
    "path": "docs/go/concepts/benefits.md",
    "content": "---\nseotitle: Benefits of using Encore.go\nseodesc: See how Encore.go helps you build backends faster using Go.\ntitle: Encore.go Benefits\nsubtitle: How Encore.go helps you build robust distributed systems, faster.\nlang: go\n---\n\nUsing Encore.go to declare infrastructure in application code helps unlock several benefits:\n\n- **Local development with instant infrastructure**: Encore.go automatically sets up necessary infrastructure as you develop.\n- **Rapid feedback**: Catch issues early with type-safe infrastructure, avoiding slow deployment cycles.\n- **No manual configuration required**: No need for Infrastructure-as-Code. Your code is the single source of truth.\n- **Unified codebase**: One codebase for all environments; local, preview, and cloud.\n- **Cloud-agnostic by default**: Encore.go provides an abstraction layer on top of the cloud provider's APIs, so you avoid becoming locked in to a single cloud.\n- **Evolve infrastructure without code changes**: As requirements evolve, you can change the provisioned infrastructure without making code changes, you only need to change the infrastructure configuration which is separate from the application code.\n- **AI-assisted development**: Encore is built for AI coding assistants. With [Encore-specific rules and MCP integration](/docs/go/ai-integration), AI understands your architecture and can generate type-safe, pattern-consistent code and introspect your app—services, APIs, databases, and traces.\n\n## No DevOps experience required\n\nEncore provides open source tools to help you integrate with your cloud infrastructure, enabling you to self-host your application anywhere that supports Docker containers.\nLearn more in the [self-host documentation](/docs/go/self-host/docker-build).\n\nYou can also use [Encore Cloud](https://encore.dev/use-cases/devops-automation), which fully automates provisioning and managing infrastructure in your own cloud on AWS and GCP.\n\nThis approach dramatically reduces the level of DevOps expertise required to use scalable, production-ready, cloud services like Kubernetes and Pub/Sub. And because your application code is the source of truth for infrastructure requirements, it ensures the infrastructure in all your environments are always in sync with the application's requirements.\n\n## Simplicity without giving up flexibility\n\nEncore.go provides integrations for common infrastructure primitives, but also allows for flexibility. You can always use any cloud infrastructure, even if it's not built into Encore.go. If you use Encore's [Cloud Platform](https://encore.dev/use-cases/devops-automation), it [automates infrastructure](/docs/platform/infrastructure/infra) using your own cloud account, so you always have full access to your services from the cloud provider's console.\n"
  },
  {
    "path": "docs/go/develop/api-docs.md",
    "content": "---\nseotitle: Service Catalog & Generated API Docs\nseodesc: See how Encore automatically generates API documentation that always stays up to date and in sync.\ntitle: Service Catalog\nsubtitle: Automatically get a Service Catalog and complete API docs\n---\n\nAll developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date.\n\nTo solve this, Encore uses the [Encore Application Model](/docs/go/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve.\n\nThe API docs are available both in your [Local Development Dashboard](/docs/go/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud).\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/servicecatalogvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/go/develop/auth.md",
    "content": "---\nseotitle: Adding authentication to APIs to auth users\nseodesc: Learn how to add authentication to your APIs and make sure you know who's calling your backend APIs.\ntitle: Authenticating users\nsubtitle: Knowing what's what and who's who\ninfobox: {\n  title: \"Authentication\",\n  import: \"encore.dev/beta/auth\",\n}\nlang: go\n---\nAlmost every application needs to know who's calling it, whether the user\nrepresents a person in a consumer-facing app or an organization in a B2B app.\nEncore supports both use cases in a simple yet powerful way.\n\nAs described in the docs for [defining APIs](/docs/go/primitives/defining-apis), Encore offers three access levels\nfor APIs:\n\n* `//encore:api public` &ndash; defines a public API that anybody on the internet can call.\n* `//encore:api private` &ndash; defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs.\n* `//encore:api auth` &ndash; defines a public API that anybody can call, but that requires valid authentication.\n\nWhen an API is defined with access level `auth`, outside calls to that API must specify\nan authorization header, in the form `Authorization: Bearer <token>`. The token is passed to\na designated auth handler function and the API call is allowed to go through only if the\nauth handler determines the token is valid.\n\nFor more advanced use cases you can also customize the authentication information you want.\nSee the section on [accepting structured auth information](#accepting-structured-auth-information) below.\n\n\n<Callout type=\"info\">\n\nYou can optionally send in auth data to `public` and `private` APIs, in which case the auth handler will be used. When used for `private` APIs, they are still not accessible from the outside world.\n\n</Callout>\n\n## The auth handler\n\nEncore applications can designate a special function to handle authentication,\nby defining a function and annotating it with `//encore:authhandler`. This annotation\ntells Encore to run the function whenever an incoming API call contains authentication data.\n\nThe auth handler is responsible for validating the incoming authentication data\nand returning an `auth.UID` (a string type representing a **user id**). The `auth.UID`\ncan be whatever you wish, but in practice it usually maps directly to the primary key\nstored in a user table (either defined in the Encore service or in an external service like [Firebase](/docs/go/how-to/firebase-auth) or [Auth0](/docs/go/how-to/auth0-auth)).\n\n### With custom user data\n\nOftentimes it's convenient for the rest of your application to easily be able to look up\ninformation about the authenticated user making the request. If that's the case,\ndefine the auth handler like so:\n\n```go\nimport \"encore.dev/beta/auth\"\n\n// Data can be named whatever you prefer (but must be exported).\ntype Data struct {\n    Username string\n    // ...\n}\n\n// AuthHandler can be named whatever you prefer (but must be exported).\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) {\n    // Validate the token and look up the user id and user data,\n    // for example by calling Firebase Auth.\n}\n```\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/clerk\" \n    desc=\"Example application showing an auth handler implementation with Clerk.\" \n/>\n\n<RelatedDocsLink paths={[\"/docs/go/how-to/auth0-auth\", \"/docs/go/how-to/clerk-auth\", \"/docs/go/how-to/firebase-auth\"]} />\n\n### Without custom user data\n\nWhen you don't require custom user data and it's sufficient to use `auth.UID`,\nsimply skip it in the return type:\n\n```go\nimport \"encore.dev/beta/auth\"\n\n// AuthHandler can be named whatever you prefer (but must be exported).\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, error) {\n    // Validate the token and look up the user id,\n    // for example by calling Firebase Auth.\n}\n```\n\n## Accepting structured auth information\n\nIn the examples above the function accepts a `Bearer` token as a string argument.\nIn that case Encore parses the `Authorization` HTTP header and passes the token to the auth handler.\n\nIn cases where you have different or more complex authorization requirements, you can instead specify\na data structure that specifies one or more fields to be parsed from the HTTP request. For example:\n\n```go\ntype MyAuthParams struct {\n\t// SessionCookie is set to the value of the \"session\" cookie.\n\t// If the cookie is not set it's nil.\n\tSessionCookie *http.Cookie `cookie:\"session\"`\n\t\n\t// ClientID is the unique id of the client, sourced from the URL query string.\n\tClientID string `query:\"client_id\"`\n\t\n\t// Authorization is the raw value of the \"Authorization\" header\n\t// without any parsing.\n\tAuthorization string `header:\"Authorization\"`\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, error) {\n    // ...\n}\n```\n\nThis example tells Encore that the application accepts authentication information via\nthe `session` cookie, the `client_id` query string parameter, and the `Authorization` header.\nThese fields are automatically filled in when the auth handler is called (if present in the request).\n\nYou can of course combine auth params like this with custom user data (see the section above).\n\n<Callout type=\"info\">\n\nCookies are generally only used by browsers and are automatically added to requests made by browsers.\nAs a result Encore does not include cookie fields in generated clients' authentication payloads\nor in the [Local Development Dashboard](/docs/go/observability/dev-dash).\n\n</Callout>\n\n## Handling auth errors\n\nWhen a token doesn't match your auth rules (for example if it's expired, the token has been revoked, or the token is invalid), you should return a non-nil error from the auth handler.\n\nEncore passes the error message on to the user when you use [Encore's built-in error package](/docs/go/primitives/api-errors), so we recommend using that with the error code `Unauthenticated` to communicate what happened. For example:\n\n```go\nimport \"encore.dev/beta/errs\"\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, error) {\n    return \"\", &errs.Error{\n        Code: errs.Unauthenticated,\n        Message: \"invalid token\",\n    }\n}\n```\n\n<Callout type=\"important\">\n\nNote that for security reasons you may not want to reveal too much information about why a request did not pass your auth checks. There are many subtle security considerations when dealing with authentication and we don't have time to go into all of them here.\n\nWhenever possible we recommend using a third-party auth provider instead of rolling your own authentication.\n\n</Callout>\n\n## Using auth data\n\nOnce the user has been identified by the auth handler, the API handler is called\nas usual. If it wishes to inspect the authenticated user, it can use the\n`encore.dev/beta/auth` package:\n\n- `auth.Data()` returns the custom user data returned by the auth handler (if any)\n- `auth.UserID()` returns `(auth.UID, bool)` to get the authenticated user id (if any)\n\nFor an incoming request from the outside to an API that uses the `auth` access level,\nthese are guaranteed to be set since the API won't be called if the auth handler doesn't succeed.\n\nEncore automatically propagates the auth data when you make API calls to other Encore API endpoints.\n\n<Callout type=\"info\">\n\nIf an endpoint calls another endpoint during its processing, and the original\ndoes not have an authenticated user, the request will fail. This behavior\npreserves the guarantees that `auth` endpoints always have an authenticated user.\n\n</Callout>\n\n\n## Optional authentication\n\nWhile Encore always calls the auth handler for API endpoints marked as `auth`, you can also call `public` API endpoints with authentication data.\n\nThis can be useful for APIs that support both a \"logged in\" and \"logged out\" experience.\nFor example, a site like Reddit might have a `post.List` endpoint that returns the list of posts,\nbut if you're logged in it also includes whether or not you have upvoted or downvoted each post.\n\nTo support such use cases, Encore runs the auth handler for `public` API endpoints if (and only if) the request\nincludes any authentication information (such as the `Authorization` header).\n\nIn that case, the request processing behavior varies depending on the value of the `error` returned from the auth handler:\n\n* If the error is nil, the request is considered to be an authenticated request and `auth.UID()` and `auth.Data()` will include\n  the information the auth handler returned.\n* If the error is non-nil and the error code is `errs.Unauthenticated` (like shown above), the request continues as an unauthenticated request,\n  behaving exactly as if there was no authentication data provided at all.\n* If the error is non-nil and the error code is anything else, the request is aborted and Encore returns that error to the caller.\n\nTo be able to determine if the request has an authenticated user, check the second return value from `auth.UserID()`.\n\n## Overriding auth information\n\nEncore supports overriding the auth information for an outgoing request using the\n[`auth.WithContext`](https://pkg.go.dev/encore.dev/beta/auth#WithContext) function.\nThis function returns a new context with the auth information set to the specified values.\n\nNote that this only affects the auth information passed along with the request, and not the\ncurrent request being processed (if any).\n\nThis function is often useful when testing APIs that use authentication. For example:\n\n```go\nctx := auth.WithContext(context.Background(), auth.UID(\"my-user-id\"), &MyAuthData{Email: \"hello@example.com\"})\n// ... Make an API call using `ctx` to override the auth information for that API call.\n```\n"
  },
  {
    "path": "docs/go/develop/config.md",
    "content": "---\nseotitle: Configuration for environment specific changes\nseodesc: See how you can use configuration to define different behavior in each environment. Making it simpler to develop and test your backend application.\ntitle: Configuration\nsubtitle: Define behavior in specific environments\ninfobox: {\n  title: \"Configuration\",\n  import: \"encore.dev/config\",\n}\nlang: go\n---\n\nConfiguration files let you define default behavior for your application, and override it for specific environments. This allows you to make changes without affecting deployments in other environments.\n\nEncore supports configuration files written in [CUE](https://cuelang.org/), which is a superset of JSON. It adds the following:\n- C-style comments\n- Quotes may be omitted from field names without special characters\n- Commas at the end of fields are optional\n- A comma after last element in list is allowed\n- The outer curly braces on the file are optional\n- [Expressions](https://cuelang.org/docs/tutorials/tour/expressions/) such as interpolation, comprehensions and conditionals\n  are supported.\n\n<Callout type=\"important\">\n\nFor sensitive data use Encore's [secrets management](/docs/go/primitives/secrets) functionality instead of configuration.\n\n</Callout>\n\n## Using Config\n\nInside your service, you can call `config.Load[*SomeConfigType]()`\nto load the config. This must be done at the package level, and not inside a function. See more in the [package documentation](https://pkg.go.dev/encore.dev/config#Load).\n\nHere's an example implementation:\n\n```go\npackage mysvc\n\nimport (\n    \"encore.dev/config\"\n)\n\ntype SomeConfigType struct {\n    ReadOnly config.Bool    // Put the system into read-only mode\n    Example  config.String\n}\n\nvar cfg *SomeConfigType = config.Load[*SomeConfigType]()\n```\n\nThe type you pass as a type parameter to this function will be used to generate a `encore.gen.cue` file in your services\ndirectory. This file will contain both the CUE definition for your configuration type, and some [metadata](#provided-meta-values) that Encore will\nprovide to your service at runtime. This allows you to change the final value of your configuration based on the environment the\napplication is running in.\n\nAny files ending with `.cue` in your service directory or sub-directories will be loaded by Encore and given to CUE to\nunify and compute a final configuration.\n\n<Toggle label=\"Example CUE files\">\n\n```\n-- mysvc/encore.gen.cue --\n// Code generated by encore. DO NOT EDIT.\npackage mysvc\n\n#Meta: {\n\tAPIBaseURL: string\n\tEnvironment: {\n\t\tName:  string\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\"\n\t\tCloud: \"aws\" | \"gcp\" | \"encore\" | \"local\"\n\t}\n}\n\n#Config: {\n\tReadOnly: bool   // Put the system into read-only mode\n    Example:  string\n}\n#Config\n-- mysvc/myconfig.cue --\n// Set example to \"hello world\"\nExample: \"hello world\"\n\n// By default we're not in read only mode\nReadOnly: bool | *false\n\n// But on the old production environment, we're in read only mode\nif #Meta.Environment.Name == \"old-prod\" {\n    ReadOnly: true\n}\n```\n\n</Toggle>\n\n<Callout type=\"info\">\n\nLoading configuration\nis only supported in services and the loaded data can not be referenced from packages outside that service.\n\n</Callout>\n\n### CUE tags in Go Structs\n\nYou can use the `cue` tag in your Go to specify additional constraints on your configuration. For example:\n\n```go\ntype FooBar {\n    A int `cue:\">100\"`\n    B int `cue:\"A-50\"` // If A is set, B can be inferred by CUE\n    C int `cue:\"A+B\"`  // Which then allows CUE to infer this too\n}\n\nvar _ = config.Load[*FooBar]()\n```\n\nWill result in the following CUE type definition being generated:\n\n```cue\n#Config: {\n    A: int & >100\n    B: int & A-50 // If A is set, B can be inferred by CUE\n    C: int & A+B  // Which then allows CUE to infer this too\n}\n```\n\n## Config Wrappers\n\nEncore provides type wrappers for config in the form of `config.Value[T]` and `config.Values[T]` which expand into\nfunctions of type `T` and `[]T` respectively. These functions allow you to override the default value of your\nconfiguration in your CUE files inside tests, where only code run from that test will see the override.\n\nIn the future we plan to support real-time updating of configuration values on running applications, thus using\nthese wrappers in your configuration today will future proof your code and allow you to automatically take advantage of this feature when it is\navailable.\n\nAny type supported in API requests and responses can be used as the type for a config wrapper. However for convenience, Encore ships with the following inbuilt aliases for the config wrappers:\n\n - `config.String`, `config.Bool`, `config.Int`, `config.Uint`,\n `config.Int8`, `config.Int16`, `config.Int32`, `config.In64`,\n `config.Uint8`, `config.Uint16`, `config.Uint32`, `config.Uint64`,\n `config.Float32`, `config.Float64`, `config.Bytes`, `config.Time`, `config.UUID`\n\n<Toggle label=\"Example Application using Wrappers\">\n\n```go\n-- svc/svc.go --\ntype mysvc\n\nimport (\n    \"encore.dev/config\"\n)\n\ntype Server struct {\n    // The config wrappers do not have to be in the top level struct\n    Enabled config.Bool\n    Port    config.Int\n}\n\ntype SvcConfig struct {\n    GameServerPorts config.Values[Server]\n}\n\nvar cfg = config.Load[*SvcConfig]()\n\nfunc startServers() {\n    for _, server := range cfg.GameServerPorts() {\n        if server.Enabled() {\n            go startServer(server.Port())\n        }\n    }\n}\n\nfunc startServer(port int) {\n  // ...\n}\n-- svc/servers.cue --\nGameServerPorts: [\n    {\n        Enabled: false\n        Port:    12345\n    },\n    {\n        Enabled: true\n        Port:    1337\n    },\n]\n```\n\n</Toggle>\n\n\n## Provided Meta Values\n\nWhen your application is running, Encore will provide information about that environment to your CUE files, which you\ncan use to filter on. These fields can be found in the `encore.gen.cue` file which Encore will generate when you add a\ncall to load config. Encore provides the following meta values:\n\n- **APIBaseURL**: The base URL of the Encore API, which can be used to make API calls to the application.\n- **Environment**: A struct containing information about the environment the application is running in.<br />\n  &nbsp;&nbsp; **Name**: The name of the environment<br />\n  &nbsp;&nbsp; **Type**: One of `production`, `development`, `ephemeral` or `test`.<br />\n  &nbsp;&nbsp; **Cloud**: The cloud the app is running on, which is one of `aws`, `gcp`, `encore` or `local`.<br />\n\nThe following are useful conditionals you can use in your CUE files:\n\n```cue\n// An application running due to `encore run`\nif #Meta.Environment.Type == \"development\" && #Meta.Environment.Cloud == \"local\" {}\n\n// An application running in a development environment in the Cloud\nif #Meta.Environment.Type == \"development\" && #Meta.Environment.Cloud != \"local\" {}\n\n// An application running in a production environment\nif #Meta.Environment.Type == \"production\" {}\n\n// An application running in an environment that Encore has created\n// for an open Pull Request on Github\nif #Meta.Environment.Type == \"ephemeral\" {}\n```\n\n## Testing with Config\n\nThrough the provided meta values, your applications configuration can have different values in tests, compared to\nwhen the application is running. This can be useful to prevent external side effects from your tests, such as emailing\ncustomers across all test.\n\nSometimes however, you may want to test specific behaviors based on different configurations (such as disabling user signups),\nin this scenario using the Meta data does not give you fine enough control. To allow you to set a configuration value\nat a per test level, Encore provides the helper function [`et.SetCfg`](https://pkg.go.dev/encore.dev/et#SetCfg). You can\nuse this function to set a new value only in the current test and any sub tests, while all other tests will\ncontinue to use the value defined in the CUE files.\n\n```go\n-- config.cue --\n// By default we want to send emails\nSendEmails: bool | *true\n\n// But in all tests we want to disable emails\nif #Meta.Environment.Type == \"test\" {\n    SendEmails: false\n}\n-- signup.go --\nimport (\n    \"context\"\n\n    \"encore.dev/config\"\n)\n\ntype Config struct {\n    SendEmails config.Bool\n}\n\nvar cfg = config.Load[Config]()\n\n//encore:api public\nfunc Signup(ctx context.Context, p *SignupParams) error {\n    user := createUser(p)\n\n    if cfg.SendEmails() {\n        SendWelcomeEmail(user)\n    }\n\n    return nil\n}\n-- signup_test.go --\nimport (\n    \"errors\"\n    \"testing\"\n\n    \"encore.dev/et\"\n)\n\nfunc TestSignup(t *testing.T) {\n    err := Signup(context.Background(), &SignupParams { ... })\n    if err != nil {\n        // We don't expect an error here\n        t.Fatal(err)\n    }\n\n    if emailWasSent() {\n        // We don't expect an email to be sent\n        // as it's disabled for all tests\n        t.Fatal(\"email was sent\")\n    }\n}\n\nfunc TestSignup_TestEmails(t *testing.T) {\n    // For this test, we want to enable the welcome\n    // emails so we can test that they are sent\n    et.SetCfg(cfg.SendEmails, true)\n\n    err := Signup(context.Background(), &SignupParams { ... })\n    if err != nil {\n        // We don't expect an error here\n        t.Fatal(err)\n    }\n\n    // Check the email was sent\n    if !emailWasSent() {\n        t.Fatal(\"email was not sent\")\n    }\n}\n```\n\n## Useful CUE Patterns\n\nIf you're new to CUE, we'd recommend checking out the [CUE documentation](https://cuelang.org/docs/) and\n[cuetorials](https://cuetorials.com/), however to get you started, here are some useful patterns you can use in your\nCUE files.\n\n\n<Accordion>\n\n### Defaults\n\nCUE supports the concept of a default value, which it will use if no other concrete value is provided. This can be useful\nfor when you normally want one value, but occasionally might want to provide an override in a certain scenario. A default\nvalue is specified by prefixing it with a `*`.\n\n```cue\n// ReadOnlyMode is a boolean and if we don't provide a value, it\n// will default to false.\nReadOnlyMode: bool | *false\n\nif #Meta.Environment.Name == \"old-prod\" {\n    // On this environment, we want to set ReadOnlyMode to true\n    ReadOnlyMode: true\n}\n```\n\n</Accordion>\n\n<Accordion>\n\n### Validation within CUE\n\nAny field prefixed with an `_` will not be exported to the concrete configuration once evaluated by CUE and can be used\nto hold intermediate values. Because CUE allows you to define the same field as many times as you want, as long as the\nvalues unify, we can build complex validation logic.\n\n```cue\nimport (\n    \"list\" // import CUE's list package\n)\n\n// Set some port numbers defaulting just to 8080\n// but in development including 8443\nportNumbers: [...int] | *[8080]\nif #Meta.Environment.Type == \"development\" {\n    portNumbers: [8080, 8443]\n}\n\n// Port numbers must be an array and all values\n// are integers 1024 or above.\nportNumbers: [...int & >= 1024]\n\n// The ports are considered valid if they contain the port number 8080.\n_portsAreValid: list.Contains(portNumbers, 8080)\n\n// Ensure that the ports are valid by constraining the value to be true.\n// CUE will report an error if the value is false (that is if the portNumbers list\n// does not contain the value 8080).\n_portsAreValid: true\n```\n\n</Accordion>\n\n<Accordion>\n\n### Switch Statements\n\nIf statements in CUE do not have else branches, which can make it difficult to write complex conditionals, we however\ncan use an array to emulate a switch statement, where the first value that matches the condition is returned. The following\nexample will set `SendEmailsFrom` to a single string.\n\n```cue\nSendEmailsFrom: [\n\t// These act as individual case statements\n    if #Meta.Environment.Type == \"production\" { \"noreply@example.com\" },\n    if #Meta.Environment.Name == \"staging\"    { \"staging@example.com\" },\n\n    // This last value without a condition acts as the default case\n    \"dev-system@example.dev\",\n][0] // Return the first value which matches the condition\n```\n\n</Accordion>\n\n<Accordion>\n\n### Using Map Keys as Values\n\nCUE allows us to extract map keys and use them as values to simplify the config we need to write and minimize duplication.\n\n```cue\n// Define the type we want to use\n#Server: {\n\tserver: string\n\tport: int & > 1024\n\tenabled: bool | *true\n}\n\n// Specify that servers is a map of strings to #Server\n// where they key we assign the variable Name\nservers: [Name=string]: #Server & {\n\t// Then we union the key with the value of server\n\tserver: Name\n}\n\nservers: {\n\t\"Foo\": {\n        port: 8080\n    },\n    \"Bar\": {\n        port:    8081\n        enabled: false\n    },\n}\n```\n\nThis will result in the concrete configuration of:\n```json\n{\n    \"servers\": {\n        \"Foo\": {\n            \"server\":  \"Foo\",\n            \"port\":    8080,\n            \"enabled\": true\n        },\n        \"Bar\": {\n            \"server\":  \"Bar\",\n            \"port\":    8081,\n            \"enabled\": false\n        }\n    }\n}\n```\n\n</Accordion>\n"
  },
  {
    "path": "docs/go/develop/cors.md",
    "content": "---\nseotitle: Handling CORS (Cross-Origin Resource Sharing)\nseodesc: See how you can configure CORS for your Encore application.\ntitle: CORS\nsubtitle: Configure CORS (Cross-Origin Resource Sharing) for your Encore application\nlang: go\n---\n\nCORS is a web security concept that defines which website origins are allowed to access your API.\n\nA deep-dive into CORS is out of scope for this documentation, but [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\nprovides a good overview. In short, CORS affects requests made by browsers to resources hosted on\nother origins (a combination of the scheme, domain, and port).\n\n## Configuring CORS\n\nEncore provides a default CORS configuration that is suitable for many APIs. You can override these settings\nby specifying the `global_cors` key in the `encore.app` file, which has the following\nstructure:\n\n```cue\n{\n    // debug enables CORS debug logging.\n    \"debug\": true | false,\n\n    // allow_headers allows an app to specify additional headers that should be\n    // accepted by the app.\n    //\n    // If the list contains \"*\", then all headers are allowed.\n    \"allow_headers\": [...string],\n\n    // expose_headers allows an app to specify additional headers that should be\n    // exposed from the app, beyond the default set always recognized by Encore.\n    //\n    // If the list contains \"*\", then all headers are exposed.\n    \"expose_headers\": [...string],\n\n    // allow_origins_without_credentials specifies the allowed origins for requests\n    // that don't include credentials. If nil it defaults to allowing all domains\n    // (equivalent to [\"*\"]).\n    \"allow_origins_without_credentials\": [...string],\n\n    // allow_origins_with_credentials specifies the allowed origins for requests\n    // that include credentials. If a request is made from an Origin in this list\n    // Encore responds with Access-Control-Allow-Origin: <Origin>.\n    //\n    // The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n    // or \"https://*-myapp.example.com\").\n    \"allow_origins_with_credentials\": [...string],\n}\n```\n\n## Allowed origins\n\nThe main CORS configuration is the list of allowed origins, meaning which websites are allowed\nto access your API (via browsers).\n\nFor this purpose, CORS makes a distinction between requests that contain authentication information\n(cookies, HTTP authentication, or client certificates) and those that do not. CORS applies stricter\nrules to authenticated requests.\n\nBy default, Encore allows unauthenticated requests from all origins but disallows requests that do\ninclude authorization information from other origins. This is a good default for many APIs.\nThis can be changed by setting the `allow_origins_without_credentials` key (see above).\nFor convenience Encore also allows all origins when developing locally.\n\nFor security reasons it's necessary to explicitly specify which origins are allowed to make\nauthenticated requests. This is done by setting the `allow_origins_with_credentials` key (see above).\n\n## Allowed headers and exposed headers\n\nCORS also lets you specify which headers are allowed to be sent by the client (\"allowed headers\"),\nand which headers are exposed to scripts running in the browser (\"exposed headers\").\n\nEncore automatically configures headers by parsing your program using static analysis.\nIf your API defines a request or response type that contains a header field, Encore automatically adds the header to\nthe list of exposed and allowed headers in request types respectively.\n\nTo add additional headers to these lists, you can set the `allow_headers` and `expose_headers` keys (see above).\nThis can be useful when your application relies on custom headers in e.g. raw endpoints that aren't seen by Encore's\nstatic analysis.\n"
  },
  {
    "path": "docs/go/develop/env-vars.md",
    "content": "---\nseotitle: Environment Variables Reference\nseodesc: Learn how to configure Encore's development environment using environment.\ntitle: Environment Variables\nsubtitle: Configure your development environment\nlang: go\n---\n\nEncore works out of the box without configuration, but provides several environment variables for advanced use cases such as debugging, testing, or adapting Encore to specific workflow requirements.\n\n## Daemon & Development Dashboard\n\nThese variables control how the Encore daemon operates and where it exposes its services.\n\n### ENCORE_DAEMON_LOG_PATH\n\nControls the location of the Encore daemon log file.\n\n**Default:** `<user_cache_dir>/encore/daemon.log`\n\n**Example:**\n\n```bash\nexport ENCORE_DAEMON_LOG_PATH=/var/log/encore/daemon.log\n```\n\n### ENCORE_DEVDASH_LISTEN_ADDR\n\nOverrides the listen address for the local development dashboard.\n\n**Default:** Automatically assigned by the daemon\n\n**Format:** Network address (e.g., `localhost:9400`)\n\n**Example:**\n\n```bash\nexport ENCORE_DEVDASH_LISTEN_ADDR=localhost:8080\nencore run\n```\n\n### ENCORE_MCPSSE_LISTEN_ADDR\n\nOverrides the listen address for the MCP SSE (Model Context Protocol Server-Sent Events) endpoint.\n\n**Default:** Automatically assigned by the daemon\n\n**Format:** Network address\n\n**Example:**\n\n```bash\nexport ENCORE_MCPSSE_LISTEN_ADDR=localhost:9401\n```\n\n### ENCORE_OBJECTSTORAGE_LISTEN_ADDR\n\nOverrides the listen address for the object storage service endpoint.\n\n**Default:** Automatically assigned by the daemon\n\n**Format:** Network address\n\n**Example:**\n\n```bash\nexport ENCORE_OBJECTSTORAGE_LISTEN_ADDR=localhost:9402\n```\n\n## Advanced Development\n\nThese variables are primarily useful for advanced development scenarios, such as contributing to Encore itself or using custom builds.\n\n### ENCORE_RUNTIMES_PATH\n\nSpecifies the path to the Encore runtimes directory.\n\n**Default:** Auto-detected relative to the Encore installation (`<install_root>/runtimes`)\n\n**Example:**\n\n```bash\nexport ENCORE_RUNTIMES_PATH=/path/to/custom/runtimes\n```\n\n### ENCORE_GOROOT\n\nSpecifies the path to the custom Encore Go runtime.\n\n**Default:** Auto-detected relative to the Encore installation (`<install_root>/encore-go`)\n\n**Example:**\n\n```bash\nexport ENCORE_GOROOT=/path/to/custom/encore-go\n```\n\n<Callout type=\"info\">\n\nFor most users, these paths are automatically detected and don't need to be set. They are primarily useful when contributing to Encore or testing custom builds.\n\n</Callout>\n"
  },
  {
    "path": "docs/go/develop/metadata.md",
    "content": "---\nseotitle: Metadata API – Get data about apps, envs, and requests\nseodesc: See how to use Encore's Metadata API to get information about specific apps, environments, and requests.\ntitle: Metadata\nsubtitle: Use the metadata API to get specifics about apps, environments, and requests\ninfobox: {\n  title: \"Metadata API\",\n  import: \"encore.dev\",\n}\nlang: go\n---\n\nWhile Encore tries to provide a cloud-agnostic environment, sometimes it's helpful to know more about the environment\nyour application is running in. For this reason Encore provides an API for accessing metadata about the\n[application](#application-metadata) and the environment it's running in, as well as information about the\n[current request](#current-request) as part of the `encore.dev` package.\n\n## Application Metadata\n\nCalling `encore.Meta()` will return an [encore.AppMetadata](https://pkg.go.dev/encore.dev/#AppMetadata) instance which\ncontains information about the application, including:\n\n - `AppID` - the application name.\n - `APIBaseURL` - the URL the application API can be publicly accessed on.\n - `Environment` - the [environment](/docs/platform/deploy/environments) the application is currently running in.\n - `Build` - the revision information of the build from the version control system.\n - `Deploy` - the deployment ID and when this version of the app was deployed.\n\n## Current Request\n\n`encore.CurrentRequest()` can be called from anywhere within your application and will return an\n[encore.Request](https://pkg.go.dev/encore.dev/#Request) instance which will provides information about why the current\ncode is running.\n\nThe [encore.Request](https://pkg.go.dev/encore.dev/#Request) type contains information about the running request, such as:\n - The service and endpoint being called\n - Path and path parameter information\n - When the request started\n\nThis works automatically as a result of Encore's request tracking, and works even in other goroutines that were spawned\nduring request handling.  If no request is processed by the caller, which can happen if you call it during service\ninitialization, the Type field returns None. If `CurrentRequest()` is called from a goroutine spawned during request\nprocessing it will continue to report the same request even if the request handler has already returned.\n\nThis can be useful on [raw endpoints](/docs/go/primitives/raw-endpoints) with [path parameters](/docs/go/primitives/defining-apis#rest-apis)\nas the standard `http.Request` object passed into the raw endpoint does not provide access to the parsed path parameters,\nhowever by calling `encore.CurrentRequest().PathParams()` you can get access to the parsed path parameters.\n\n\n## Example Use Cases\n\n### Using Cloud Specific Services\n\nAll the [clouds](/docs/platform/deploy/own-cloud) contain a large number of services, not all of which Encore natively supports.\nBy using information about the [environment](/docs/platform/deploy/environments), you can define the implementation of these and\nuse different services for each environment's provider. For instance if you are pushing audit logs into a data warehouse, when running on GCP you could use BigQuery, but when running on AWS you could use Redshift, when running locally you could\nsimply write them to a file.\n\n```go\npackage audit\n\nimport (\n    \"encore.dev\"\n    \"encore.dev/beta/auth\"\n)\n\nfunc Audit(ctx context.Context, action message, user auth.UID) error {\n    switch encore.Meta().Environment.Cloud {\n    case encore.CloudAWS:\n        return writeIntoRedshift(ctx, action, user)\n    case encore.CloudGCP:\n        return writeIntoBigQuery(ctx, action, user)\n    case encore.CloudLocal:\n        return writeIntoFile(ctx, action, user)\n    default:\n        return fmt.Errorf(\"unknown cloud: %s\", encore.Meta().Environment.Cloud)\n    }\n}\n```\n\n### Checking Environment type\n\nWhen implementing a signup system, you may want to skip email verification on user signups when developing the application.\nUsing the `encore.Meta()` API, we can check the environment and decide whether to send an email or simply mark the user as\nverified upon signup.\n\n```go\npackage user\n\nimport \"encore.dev\"\n\n//encore:api public\nfunc Signup(ctx context.Context, params *SignupParams) (*SignupResponse, error) {\n    // ...\n\n    // If this is a testing environment, skip sending the verification email\n    switch encore.Meta().Environment.Type {\n    case encore.EnvTest, encore.EnvDevelopment:\n        if err := MarkEmailVerified(ctx, userID); err != nil {\n            return nil, err\n        }\n    default:\n        if err := SendVerificationEmail(ctx, userID); err != nil {\n            return nil, err\n        }\n    }\n\n    // ...\n}\n```\n"
  },
  {
    "path": "docs/go/develop/middleware.md",
    "content": "---\nseotitle: Using Middleware in your backend application\nseodesc: See how you can use middleware in your backend application to handle cross-cutting generic functionality, like request logging, auth, or tracing.\ntitle: Middleware\nsubtitle: Handling cross-cutting, generic functionality\ninfobox: {\n  title: \"Middleawre\",\n  import: \"encore.dev/middleware\",\n}\nlang: go\n---\n\nMiddleware is a way to write reusable code that runs before or after (or both)\nthe handling of API requests, often across several (or all) API endpoints.\n\nIt's commonly used to implement cross-cutting concerns like\n[request logging](/docs/go/observability/logging),\n[authentication](/docs/go/develop/auth),\n[tracing](/docs/go/observability/tracing),\nand so on. One of the benefits of Encore is that\nall of these use cases are already handled out-of-the-box, so there's no\nneed to use middleware for those things.\n\nNonetheless, there are several use cases where it can be useful to write\nreusable functionality that applies to multiple API endpoints, and middleware\nis a good solution in those cases.\n\nEncore provides built-in support for middleware by defining a function with the\n`//encore:middleware` directive. The middleware directive takes a `target`\nparameter that specifies which API endpoints it applies to.\n\n## Middleware functions\n\nA typical middleware implementation looks like this:\n\n```go\nimport (\n    \"encore.dev/beta/errs\"\n    \"encore.dev/middleware\"\n)\n\n//encore:middleware global target=all\nfunc ValidationMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n    // If the payload has a Validate method, use it to validate the request.\n    payload := req.Data().Payload\n    if validator, ok := payload.(interface { Validate() error }); ok {\n        if err := validator.Validate(); err != nil {\n            // If the validation fails, return an InvalidArgument error.\n            err = errs.WrapCode(err, errs.InvalidArgument, \"validation failed\")\n            return middleware.Response{Err: err}\n        }\n    }\n    return next(req)\n}\n```\n\nMiddleware forms a chain, allowing each middleware to introspect and process\nthe incoming request before handing it off to the next middleware by calling the\n`next` function that's passed in as an argument. For the last middleware in the\nchain, calling `next` results in the actual API handler being called.\n\nThe `req` parameter provides information about the incoming request\n(see [package docs](https://pkg.go.dev/encore.dev/middleware#Request)).\n\nThe `next` function returns a [`middleware.Response`](https://pkg.go.dev/encore.dev/middleware#Response)\nobject which contains the response from the API, describing whether there was an error, and on success\nthe actual response payload.\n\nThis enables middleware to also introspect and even\nmodify the outgoing response, like this:\n\n```go\n//encore:middleware target=tag:cache\nfunc CachingMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n    data := req.Data()\n    // Check if we have the response cached. Use the request path as the cache key.\n    cacheKey := data.Path\n\tif cached, err := loadFromCache(cacheKey, data.API.ResponseType); err == nil && cached != nil {\n\t    return middleware.Response{Payload: cached}\n    }\n\t// Otherwise forward the request to the handler\n\treturn next(req)\n}\n```\n\nThis uses `target=tag:cache` to have the middleware only apply to APIs that have\nthat tag. More on this below in [Targeting APIs](#targeting-apis).\n\n<Callout type=\"important\">\n\nMiddleware functions can also be defined as methods on a Dependency Injection\nstruct declared with `//encore:service`. For example:\n\n```go\n//encore:service\ntype Service struct{}\n\n//encore:middleware target=all\nfunc (s *Service) MyMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n\t// ...\n}\n```\n\nSee the [Dependency Injection](/docs/go/how-to/dependency-injection) docs for more information.\n\n</Callout>\n\n## Middleware ordering\n\nMiddleware can either be defined inside a service, in which case it only runs\nfor APIs within that service, or it can be defined as a `global` middleware,\nin which case it applies to all services. For global middleware the `target`\ndirective still applies and enables you to easily match a subset of APIs.\n\n<Callout type=\"important\">\n\nGlobal middleware always run before all service-specific middleware,\nand then run in the order they are defined in the source code based on\nfile name lexicographic ordering.\n\n</Callout>\n\nTo avoid surprises it's best to define all middleware in a file called\n`middleware.go` in each service, and to create a single top-level package\nto contain all global middleware.\n\n## Targeting APIs\n\nThe `target` directive can either be provided as `target=all` (meaning it applies\nto all APIs) or a list of tags, in the form `target=tag:foo,tag:bar`. Note that\nthese tags are evaluated with `OR`, meaning the middleware applies to an API if\nthe API has at least one of those tags.\n\nAPIs can be defined with tags by adding `tag:foo` at the end of the `//encore:api` directive:\n\n```go\n//encore:api public method=GET path=/user/:id tag:cache\nfunc GetUser(ctx context.Context, id string) (*User, error) {\n\t// ...\n}\n```\n"
  },
  {
    "path": "docs/go/develop/mocking.md",
    "content": "---\nseotitle: Mocking out your APIs and services for testing\nseodesc: Learn how to mock out your APIs and services for testing, and how to use the built-in mocking support in Encore.\ntitle: Mocking\nsubtitle: Testing your application in isolation\ninfobox: {\n  title: \"Testing\",\n  import: \"encore.dev/et\",\n}\nlang: go\n---\n\nEncore comes with built-in support for mocking out APIs and services, which makes it easier to test your application in\nisolation.\n\n## Mocking Endpoints\n\nLet's say you have an endpoint that calls an external API in our `products` service:\n\n```go\n//encore:api private\nfunc GetPrice(ctx context.Context, p *PriceParams) (*PriceResponse, error) {\n    // Call external API to get the price\n}\n```\n\nWhen testing this function, you don't want to call the real external API since that would be slow and cause your tests\nto fail if the API is down. Instead, you want to mock out the API call and return a fake response.\n\nIn Encore, you can do this by adding a mock implementation of the endpoint using the `et.MockEndpoint` function inside your test:\n\n```go\npackage shoppingcart\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\n\t\"encore.dev/et\" // Encore's test support package\n\t\n\t\"your_app/products\"\n)\n\n\nfunc Test_Something(t *testing.T) {\n\tt.Parallel() // Run this test in parallel with other tests without the mock implementation interfering\n\t\n\t// Create a mock implementation of pricing API which will only impact this test and any sub-tests\n\tet.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {\n\t\treturn &products.PriceResponse{Price: 100}, nil\n\t})\n\t\n\t// ... the rest of your test code here ...\n} \n```\n\nWhen any code within the test, or any sub-test calls the `GetPrice` API, the mock implementation will be called instead.\nThe mock will not impact any other tests running in parallel. The function you pass to `et.MockEndpoint` must have the same\nsignature as the real endpoint.\n\nIf you want to mock out the API for all tests in the package, you can add the mock implementation to the `TestMain` function:\n\n```go\npackage shoppingcart\n\nimport (\n\t\"context\"\n\t\"os\"\n    \"testing\"\n    \n    \"encore.dev/et\"\n\t\n\t\"your_app/products\"\n)\n\nfunc TestMain(m *testing.M) {\n    // Create a mock implementation of pricing API which will impact all tests within this package\n    et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {\n        return &products.PriceResponse{Price: 100}, nil\n    })\n    \n    // Now run the tests\n    os.Exit(m.Run())\n}\n```\n\nMocks can be changed at any time, including removing them by setting the mock implementation to `nil`.\n\n## Mocking services\n\nAs well as mocking individual APIs, you can also mock entire services. This can be useful if you want to inject a different\nset of dependencies into your service for testing, or a service that your code depends on. This can be done using the\n`et.MockService` function:\n\n```go\npackage shoppingcart\n\nimport (\n    \"context\"\n    \"testing\"\n    \n    \"encore.dev/et\" // Encore's test support package\n    \n    \"your_app/products\"\n)\n\nfunc Test_Something(t *testing.T) {\n    t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering\n    \n    // Create a instance of the products service which will only impact this test and any sub-tests\n    et.MockService(\"products\", &products.Service{\n\t\tSomeField: \"a testing value\",\n\t})\n    \n    // ... the rest of your test code here ...\n}\n```\n\nWhen any code within the test, or any sub-test calls the `products` service, the mock implementation will be called instead.\nUnlike `et.MockEndpoint`, the mock implementation does not need to have the same signature, and can be any object. The only requirement\nis that any of the services APIs that are called during the test must be implemented by as a receiver method on the mock object.\n(This also includes APIs that are defined as package level functions in the service, and are not necessarily defined as receiver methods\non that services struct).\n\nTo help with compile time safety on service mocking, for every service Encore will automatically generate an `Interface` interface\nwhich contains all the APIs defined in the service. This interface can be passed as a generic argument to `et.MockService` to ensure\nthat the mock object implements all the APIs defined in the service:\n\n```go\ntype myMockObject struct{}\n\nfunc (m *myMockObject) GetPrice(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {\n    return &products.PriceResponse{Price: 100}, nil\n}\n\nfunc Test_Something(t *testing.T) {\n    t.Parallel() // Run this test in parallel with other tests without the mock implementation interfering\n    \n    // This will cause a compile time error if myMockObject does not implement all the APIs defined in the products service\n    et.MockService[products.Interface](\"products\", &myMockObject{})\n}\n```\n\n### Automatic generation of mock objects\n\nThanks to the generated `Interface` interface, it's possible to automatically generate mock objects for your services using\neither [Mockery](https://vektra.github.io/mockery/latest/) or [GoMock](https://github.com/uber-go/mock).\n"
  },
  {
    "path": "docs/go/develop/testing.md",
    "content": "---\nseotitle: Automated testing for your backend application\nseodesc: Learn how create automated tests for your microservices backend application, and run them automatically on deploy using Go and Encore.\ntitle: Automated testing\nsubtitle: Confidence at speed\ninfobox: {\n  title: \"Testing\",\n  import: \"encore.dev/et\",\n}\nlang: go\n---\n\nGo comes with excellent built-in support for automated tests.\nEncore builds on top of this foundation, and lets you write tests in exactly the same way.\nWe won't cover the basics of how to write tests here, see [the official Go docs](https://golang.org/pkg/testing/) for that.\nLet's instead focus on the difference between testing in Encore compared to a standard Go application.\n\nThe main difference is that since Encore requires an extra compilation step,\nyou must run your tests using `encore test` instead of `go test`. This is\na wrapper that compiles the Encore app and then runs `go test`. It supports\nall the same flags that the `go test` command does.\n\nFor example, use `encore test ./...` to run tests in all sub-directories,\nor just `encore test` for the current directory.\n\n## Test tracing\n\nEncore comes with built-in test tracing for local development.\n\nYou only need to open Encore's local development dashboard at [localhost:9400](http://localhost:9400) to see traces for all your tests.\nThis makes it very simple to understand the root cause for why a test is failing.\n\n<img className=\"w-full d:w-3/4 h-auto\" src=\"/assets/docs/test_trace.png\" title=\"Test tracing\" />\n\n\n## Integration testing\n\nSince Encore removes almost all boilerplate, most of the code you write\nis business logic that involves databases and calling APIs between services.\nSuch behavior is most easily tested with integration tests.\n\nWhen running tests, Encore automatically sets up the databases you need\nin a separate database cluster. They are additionally configured to skip `fsync`\nand to use an in-memory filesystem since durability is not a concern for automated tests.\n\nThis drastically reduces the speed overhead of writing integration tests.\n\nIn general, Encore applications tend to focus more on integration tests\ncompared to traditional applications that are heavier on unit tests.\nThis is nothing to worry about and is the recommended best practice.\n\n### Temporary databases\n\nWhen Encore runs tests, by default it reuses the same database for all tests,\nto improve performance. However, this means that you need to take care when writing tests\nto ensure tests don't interfere with each other.\n\nIf you instead want to have a separate database for a given test, you can use\n[`et.NewTestDatabase`](https://pkg.go.dev/encore.dev/et#NewTestDatabase) to create a temporary database\nthat only exists for the duration of the test.\n\nThe temporary test database is a fully-migrated database. It does not include any data written by other tests.\n\n<Callout type=\"info\">\n\nUnder the hood, when you start running tests, Encore sets up a fresh \"template database\" and runs the database migrations\nagainst that database. When you later call `et.NewTestDatabase`, Encore creates a new database by cloning the template database.\n\n</Callout>\n\n### Service Structs\n\nIn tests, [service structs](/docs/go/primitives/service-structs) are initialized on demand when the first\nAPI call is made to that service and then that instance of the service struct for all future tests. This means your tests\ncan run faster as they don't have to each initialize all the service struct's each time a new test starts.\n\nHowever, in some situations you might be storing state in the service struct that would interfere with other tests. When\nyou have a test you want to have its own instance of the service struct, you can use the `et.EnableServiceInstanceIsolation()` function within the test to enable this for just that test, while the rest of your tests will continue to use the shared instance.\n\n## Test-only infrastructure\n\nEncore allows tests to define infrastructure resources specifically for testing.\nThis can be useful for testing library code that interacts with infrastructure.\n\nFor example, the [x.encore.dev/pubsub/outbox](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox) package\ndefines a test-only database that is used to do integration testing of the outbox functionality.\n\n## Testing from your IDE\n\n### GoLand / IntelliJ\n\nEncore has an officially supported plugin [available in the JetBrains marketplace](https://plugins.jetbrains.com/plugin/20010-encore).\n\nIt lets you run unit tests directly from within your IDE with support for debug mode and breakpoints.\n\n### Visual Studio Code (VS Code)\n\nThere's no official VS Code plugin available yet, but we are happy to include your contribution if you  build one. Reach out on [Discord](/discord) if you need help to get started.\n\nFor advice on debugging when using VS Code, see the [Debugging docs](/docs/go/how-to/debug).\n"
  },
  {
    "path": "docs/go/develop/validation.md",
    "content": "---\nseotitle: Request validation in your backend application\nseodesc: Learn how request validation works, and see how you can use Encore's built-in middleware to validate incoming requests in your backend application.\ntitle: Validation\nsubtitle: Making sure everything's right in the world\nlang: go\n---\n\nWhen receiving incoming requests it's best practice to validate the\npayload to make sure it meets your expectations, contains all the necessary\nfields, and so on.\n\nEncore provides an out-of-the-box middleware that automatically validates\nincoming requests if the request type implements the method `Validate() error`.\n\nIf it does, Encore will call this method after deserializing the request payload,\nand only call your API handler (and other middleware) if the validation function\nreturns `nil`.\n\nIf the validation function returns an [`*errs.Error`](/docs/go/primitives/api-errors) that error\nis reported unmodified to the caller. Other errors are converted to an `*errs.Error`\nwith code `InvalidArgument`, which results in a HTTP response with status code `400 Bad Request`.\n\nThis design means that it's easy to use your validation library of choice.\nIn the future we're looking to provide an out-of-the-box validation library\nfor an even better developer experience.\n"
  },
  {
    "path": "docs/go/faq.md",
    "content": "---\nseotitle: Frequently Asked Questions\nseodesc: See quick answers to common questions about Encore\ntitle: FAQ\nsubtitle: Quick answers to common questions\nlang: go\n---\n\n## About the project\n\n**Is Encore Open Source?**\n\nYes, check out the project on [GitHub](https://github.com/encoredev/encore).\n\n**Is there a community?**\n\nYes, you're welcome to join the developer community on [Discord](https://encore.dev/discord).\n\n## Can I use X with Encore?\n\n**Can I use Python with Encore?**\n\nEncore currently supports Go and TypeScript. Python support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026.\n\n**Can mix TypeScript and Go in one application?**\n\nSupport for mixing languages in coming. Currently, if you want to use both TypeScript and Go, you need to create a separate application per language and integrate using APIs.\n\n**Can I use Azure / Digital Ocean?**\n\nEncore Cloud currently supports automating deployments to AWS and GCP. Azure support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026.\n\nIf you want to use other cloud providers like Azure or Digital Ocean, you can follow the [self-hosting instructions](/docs/go/self-host/docker-build).\n\n**Can I use MongoDB / MySQL with Encore?**\n\nEncore currently has built-in support for PostgreSQL. To use another type of database, like MongoDB and MySQL, you will need to set it up and integrate as you normally would when not using Encore.\n\n**Can I use AWS lambda with Encore?**\n\nNot right now. Encore currently supports AWS Fargate and EKS (along with CloudRun and GKE on Google Cloud Platform).\n\n## IDE Integrations\n\n**Is there an Encore plugin for Goland / IntelliJ?**\n\nYes, Encore's official Goland plugin is available in the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/20010-encore).\n\n**Is there an Encore plugin for VS Code?**\n\nNot yet, it's coming soon.\n\n## Troubleshooting\n\n**symlink creation error on Windows**\n\nEncore currently relies on symbolic links, which may be disabled by default. A common fix for this issue is to enable \"developer mode\" in the Windows settings (Settings > System > For developers > Developer mode).\n\n**`node` errors**\n\nYou might need to restart the Encore daemon, e.g. if your PATH has changed since installing nvm. Restart the daemon by running `encore daemon`.\n"
  },
  {
    "path": "docs/go/how-to/atlas-gorm.md",
    "content": "---\nseotitle: How to use Atlas + GORM for database migrations with Encore\nseodesc: See how you can use Atlas to manage your database migrations in your Encore application.\ntitle: Use Atlas + GORM for database migrations\nlang: go\n---\n\n[Atlas](https://atlasgo.io) is a popular tool for managing database migrations.\n[GORM](https://gorm.io/) is a popular ORM for Go.\n\nEncore provides excellent support for using them together to easily manage database schemas and migrations.\nEncore executes database migrations using [golang-migrate](https://github.com/golang-migrate/migrate),\nwhich Atlas supports out-of-the-box. This means that you can use Atlas to manage your Encore database migrations.\n\nThe easiest way to use Atlas + GORM together is with Atlas's support for [external schemas](https://atlasgo.io/blog/2023/06/28/external-schemas-and-gorm-support).\n\n## Setting up GORM\n\nTo set up your Encore application with GORM, start by installing the GORM package and associated Postgres driver:\n\n```shell\ngo get -u gorm.io/gorm gorm.io/driver/postgres\n```\n\nThen, in the service that you want to use GORM for, add the `*gorm.DB` as a dependency\nin your service struct (create a service struct if you don't already have one).\n\nFor example, if you had a service called `blog`:\n\n```go\n-- blog/blog.go --\npackage blog\n\nimport (\n\t\"encore.dev/storage/sqldb\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n)\n\n\n//encore:service\ntype Service struct {\n\tdb *gorm.DB\n}\n\nvar blogDB = sqldb.NewDatabase(\"blog\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// initService initializes the site service.\n// It is automatically called by Encore on service startup.\nfunc initService() (*Service, error) {\n\tdb, err := gorm.Open(postgres.New(postgres.Config{\n\t\tConn: blogDB.Stdlib(),\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Service{db: db}, nil\n}\n```\n\nFinally, create the `migrations` directory inside the `blog` directory if it doesn't already exist.\nThis is where Atlas will put your database migrations.\n\n## Setting up Atlas\n\nFirst [install Atlas](https://atlasgo.io/getting-started).\n\nThen, add an `atlas.hcl` file inside the `blog` directory:\n\n```\n-- blog/atlas.hcl --\ndata \"external_schema\" \"gorm\" {\n  program = [\"env\", \"ENCORERUNTIME_NOPANIC=1\", \"go\", \"run\", \"./scripts/atlas-gorm-loader.go\"]\n}\n\nenv \"local\" {\n  src = data.external_schema.gorm.url\n\n  migration {\n    dir = \"file://migrations\"\n    format = golang-migrate\n  }\n\n  format {\n    migrate {\n      diff = \"{{ sql . \\\"  \\\" }}\"\n    }\n  }\n}\n```\n\nNext, we need to create the `atlas-gorm-loader` script referenced above.\nIt will use the [atlas-provider-gorm](https://github.com/ariga/atlas-provider-gorm) library provided by Atlas.\n\nCreate the file as follows:\n\n```\n-- blog/scripts/atlas-gorm-loader.go --\npackage main\n\nimport (\n    \"fmt\"\n    \"io\"\n    \"os\"\n\n    _ \"ariga.io/atlas-go-sdk/recordriver\"\n    \"ariga.io/atlas-provider-gorm/gormschema\"\n    \"encore.app/blog\"\n)\n\n// Define the models to generate migrations for.\nvar models = []any{\n    &blog.Post{},\n    &blog.Comment{},\n}\n\nfunc main() {\n    stmts, err := gormschema.New(\"postgres\").Load(models...)\n    if err != nil {\n        fmt.Fprintf(os.Stderr, \"failed to load gorm schema: %v\\n\", err)\n        os.Exit(1)\n    }\n    io.WriteString(os.Stdout, stmts)\n}\n```\n\n## Creating migrations\n\nTo wrap things up, let's create a script to automate the process of generating migrations:\n\n```\n-- blog/scripts/generate-migration --\n#!/bin/bash\nset -eu\nDB_NAME=blog\nMIGRATION_NAME=${1:-}\n\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\n\n# Reset the shadow database\nencore db reset --shadow $DB_NAME\n\n# GORM executes Go code without initializing Encore when generating migrations,\n# so configure the Encore runtime to be aware that this is expected.\nexport ENCORERUNTIME_NOPANIC=1\n\n# Generate the migration\natlas migrate diff $MIGRATION_NAME --env local --dev-url \"$(encore db conn-uri --shadow $DB_NAME)&search_path=public\"\n```\n\nFinally let's make the script executable, and generate our first migration:\n\n```shell\n$ chmod +x blog/scripts/generate-migration\n$ cd blog && ./scripts/generate-migration init\n```\n\nThis will generate a new migration file in the `blog/migrations` directory, which\nwill be automatically applied when running `encore run`.\n"
  },
  {
    "path": "docs/go/how-to/auth0-auth.md",
    "content": "---\nseotitle: How to use Auth0 for your backend application\nseodesc: Learn how to use Auth0 for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Auth0.\ntitle: Use Auth0 with your app\nlang: go\n---\n\nIn this guide you will learn how to set up an Encore [auth handler](/docs/go/develop/auth#the-auth-handler) that makes use of\n[Auth0](https://auth0.com/) in order to add a seamless signup and login experience to your web app.\n\nFor all the code and instructions of how to clone and run this example locally, see the [Auth0 Example](https://github.com/encoredev/examples/tree/main/auth0) in our examples repo.\n\n## Communicate with Auth0\n\nIn your Encore app, install two modules:\n\n```shell\n$ go get github.com/coreos/go-oidc/v3/oidc golang.org/x/oauth2\n```\n\nCreate a folder and naming it `auth`, this is where our authentication related backend code will live.\n\nNext, let's set up the Auth0 `Authenticator` that will be used by our auth handler. The `Authenticator` has a method to configure and return [OAuth2](https://pkg.go.dev/golang.org/x/oauth2?utm_source=godoc) and [oidc](https://pkg.go.dev/github.com/coreos/go-oidc?utm_source=godoc) clients, and another one to verify an ID Token. \n\nCreate `auth/authenticator.go` and paste the following:\n\n```go\npackage auth\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"encore.dev/config\"\n\t\"errors\"\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"golang.org/x/oauth2\"\n)\n\ntype Auth0Config struct {\n\tClientID    config.String\n\tDomain      config.String\n\tCallbackURL config.String\n\tLogoutURL   config.String\n}\n\nvar cfg = config.Load[*Auth0Config]()\n\nvar secrets struct {\n\tAuth0ClientSecret string\n}\n\n// Authenticator is used to authenticate our users.\ntype Authenticator struct {\n\t*oidc.Provider\n\toauth2.Config\n}\n\n// New instantiates the *Authenticator.\nfunc New() (*Authenticator, error) {\n\tprovider, err := oidc.NewProvider(\n\t\tcontext.Background(),\n\t\t\"https://\"+cfg.Domain()+\"/\",\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconf := oauth2.Config{\n\t\tClientID:     cfg.ClientID(),\n\t\tClientSecret: secrets.Auth0ClientSecret,\n\t\tRedirectURL:  cfg.CallbackURL(),\n\t\tEndpoint:     provider.Endpoint(),\n\t\tScopes:       []string{oidc.ScopeOpenID, \"profile\", \"email\"},\n\t}\n\n\treturn &Authenticator{\n\t\tProvider: provider,\n\t\tConfig:   conf,\n\t}, nil\n}\n\n// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken.\nfunc (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {\n\trawIDToken, ok := token.Extra(\"id_token\").(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"no id_token field in oauth2 token\")\n\t}\n\n\toidcConfig := &oidc.Config{\n\t\tClientID: a.ClientID,\n\t}\n\n\treturn a.Verifier(oidcConfig).Verify(ctx, rawIDToken)\n}\n\nfunc generateRandomState() (string, error) {\n\tb := make([]byte, 32)\n\t_, err := rand.Read(b)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tstate := base64.StdEncoding.EncodeToString(b)\n\n\treturn state, nil\n}\n```\n\n## Set up the auth handler\n\nIt's time to define your [auth handler](/docs/go/develop/auth) and the endpoints needed for the login and logout flow.\n\nCreate the `auth/auth.go` file and paste the following:\n\n```go\npackage auth\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n)\n\n// Service struct definition.\n// Learn more: encore.dev/docs/primitives/services-and-apis/service-structs\n//\n//encore:service\ntype Service struct {\n\tauth *Authenticator\n}\n\n// initService is automatically called by Encore when the service starts up.\nfunc initService() (*Service, error) {\n\tauthenticator, err := New()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Service{auth: authenticator}, nil\n}\n\ntype LoginResponse struct {\n\tState       string `json:\"state\"`\n\tAuthCodeURL string `json:\"auth_code_url\"`\n}\n\n//encore:api public method=POST path=/auth/login\nfunc (s *Service) Login(ctx context.Context) (*LoginResponse, error) {\n\tstate, err := generateRandomState()\n\tif err != nil {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\treturn &LoginResponse{\n\t\tState: state,\n\t\t// add the audience to the auth code url\n\t\tAuthCodeURL: s.auth.AuthCodeURL(state),\n\t}, nil\n}\n\ntype CallbackRequest struct {\n\tCode string `json:\"code\"`\n}\n\ntype CallbackResponse struct {\n\tToken string `json:\"token\"`\n}\n\n//encore:api public method=POST path=/auth/callback\nfunc (s *Service) Callback(\n\tctx context.Context,\n\treq *CallbackRequest,\n) (*CallbackResponse, error) {\n\n\t// Exchange an authorization code for a token.\n\ttoken, err := s.auth.Exchange(ctx, req.Code)\n\tif err != nil {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.PermissionDenied,\n\t\t\tMessage: \"Failed to convert an authorization code into a token.\",\n\t\t}\n\t}\n\n\tidToken, err := s.auth.VerifyIDToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: \"Failed to verify ID Token.\",\n\t\t}\n\t}\n\n\tvar profile map[string]interface{}\n\tif err := idToken.Claims(&profile); err != nil {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\treturn &CallbackResponse{\n\t\tToken: token.Extra(\"id_token\").(string),\n\t}, nil\n}\n\ntype LogoutResponse struct {\n\tRedirectURL string `json:\"redirect_url\"`\n}\n\n//encore:api public method=GET path=/auth/logout\nfunc (s *Service) Logout(ctx context.Context) (*LogoutResponse, error) {\n\tlogoutUrl, err := url.Parse(\"https://\" + cfg.Domain() + \"/v2/logout\")\n\tif err != nil {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\treturnTo, err := url.Parse(cfg.LogoutURL())\n\tif err != nil {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\tparameters := url.Values{}\n\tparameters.Add(\"returnTo\", returnTo.String())\n\tparameters.Add(\"client_id\", cfg.ClientID())\n\tlogoutUrl.RawQuery = parameters.Encode()\n\n\treturn &LogoutResponse{\n\t\tRedirectURL: logoutUrl.String(),\n\t}, nil\n}\n\ntype ProfileData struct {\n\tEmail   string `json:\"email\"`\n\tPicture string `json:\"picture\"`\n}\n\n// The `encore:authhandler` annotation tells Encore to run this function for all \n// incoming API call that requires authentication.\n// Learn more: encore.dev/docs/develop/auth#the-auth-handler\n//\n//encore:authhandler\nfunc (s *Service) AuthHandler(\n\tctx context.Context,\n\ttoken string,\n) (auth.UID, *ProfileData, error) {\n\toidcConfig := &oidc.Config{\n\t\tClientID: s.auth.ClientID,\n\t}\n\n\tt, err := s.auth.Verifier(oidcConfig).Verify(ctx, token)\n\tif err != nil {\n\t\treturn \"\", nil, &errs.Error{\n\t\t\tCode:    errs.Unauthenticated,\n\t\t\tMessage: \"invalid token\",\n\t\t}\n\t}\n\n\tvar profile map[string]interface{}\n\tif err := t.Claims(&profile); err != nil {\n\t\treturn \"\", nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\t// Extract profile data returned from the identity provider.\n\t// auth0.com/docs/manage-users/user-accounts/user-profiles/user-profile-structure\n\tprofileData := &ProfileData{\n\t\tEmail:   profile[\"email\"].(string),\n\t\tPicture: profile[\"picture\"].(string),\n\t}\n\n\treturn auth.UID(profile[\"sub\"].(string)), profileData, nil\n}\n\n// Endpoints annotated with `auth` are public and requires authentication\n// Learn more: encore.dev/docs/primitives/apis#access-controls\n//\n//encore:api auth method=GET path=/profile\nfunc GetProfile(ctx context.Context) (*ProfileData, error) {\n\treturn auth.Data().(*ProfileData), nil\n}\n```\n\n## Auth0 settings\n\nThe `Authenticator` class requires some values that are specific your Auth0 application, namely the `ClientID`, `ClientSecret`, `Domain`, `CallbackURL` and `LogoutURL`.\n\nCreate an Auth0 account if you haven't already. Then, in the Auth0 dashboard, create a new *Single Page Web Applications*.\n\n<img src=\"/assets/docs/auth0-create-app.png\" title=\"Create Auth0 application\"/>\n\nNext, go to the *Application Settings* section. There you will find the `Domain`, `Client ID`, and `Client Secret` that you need to communicate with Auth0. \nCopy these values, we will need them shortly.\n\n<img src=\"/assets/docs/auth0-basic-info.png\" title=\"Auth0 basic information\"/>\n\nA callback URL is where Auth0 redirects the user after they have been authenticated. \nAdd `http://localhost:3000/callback` to the *Allowed Callback URLs*. \nYou will need to add more URLs to this list when you have a production or staging environments. \n\nThe same goes for the logout URL (were the user will get redirected after logout). Add `http://localhost:3000/` to the *Allowed Logout URLs*. \n\n<img src=\"/assets/docs/auth0-app-uris.png\" title=\"Auth0 application URIs\"/>\n\n\n## Config and secrets\n\nCreate a [configuration file](/docs/go/develop/config) in the `auth` service and name it `auth-config.cue`. Add the following:\n\n```cue\nClientID: \"<your client_id from above>\"\nDomain: \"<your domain from above>\"\n\n// An application running locally\nif #Meta.Environment.Type == \"development\" && #Meta.Environment.Cloud == \"local\" {\n\tCallbackURL: \"http://localhost:3000/callback\"\n\tLogoutURL: \"http://localhost:3000/\"\n}\n```\n\nReplace the values for the `ClientID` and `Domain` that you got from the Auth0 dashboard.\n\nThe `ClientSecret` is especially sensitive and should not be hardcoded in your code/config. Instead, you should store that as an [Encore secret](/docs/go/primitives/secrets).\n\nFrom your terminal (inside your Encore app directory), run:\n\n```shell\n$ encore secret set --prod Auth0ClientSecret\n```\n\nNow you should do the same for the development secret. The most secure way is to set up a different Auth0 application and use that for development.\nDepending on your security requirements you could also use the same secret for development and production.\n\nOnce you have a client secret for development, set it similarly to before:\n\n```shell\n$ encore secret set --dev Auth0ClientSecret\n```\n\nThat's it! Encore will run your auth handler and validate the token against Auth0.\n\n## Frontend\n\nNow that the backend is set up, we can create a frontend application that uses the login flow.\n\nHere's an example using [React](https://react.dev/) together with [React Router](https://reactrouter.com/). This example \nalso makes use of a Encores ability to [generate request clients](/docs/go/cli/client-generation) to make the communication \nwith our backend simple and typesafe.\n\n```tsx\n-- App.tsx --\nimport { PropsWithChildren } from \"react\";\nimport {\n  createBrowserRouter,\n  Link,\n  Outlet,\n  redirect,\n  RouterProvider,\n  useRouteError,\n} from \"react-router-dom\";\nimport { Auth0Provider } from \"./lib/auth\";\nimport AdminDashboard from \"./components/AdminDashboard.tsx\";\n\nimport IndexPage from \"./components/IndexPage.tsx\";\nimport \"./App.css\";\nimport LoginStatus from \"./components/LoginStatus.tsx\";\n\n// Application routes\nconst router = createBrowserRouter([\n  {\n    id: \"root\",\n    path: \"/\",\n    Component: Layout,\n    errorElement: (\n      <Layout>\n        <ErrorBoundary />\n      </Layout>\n    ),\n    children: [\n      {\n        Component: Outlet,\n        children: [\n          {\n            index: true,\n            Component: IndexPage,\n          },\n          {\n            // Login route\n            path: \"login\",\n            loader: async ({ request }) => {\n              const url = new URL(request.url);\n              const searchParams = new URLSearchParams(url.search);\n              const returnToURL = searchParams.get(\"returnTo\") ?? \"/\";\n\n              if (Auth0Provider.isAuthenticated()) return redirect(returnToURL);\n\n              try {\n                const returnURL = await Auth0Provider.login(returnToURL);\n                return redirect(returnURL);\n              } catch (error) {\n                throw new Error(\"Login failed\");\n              }\n            },\n          },\n          {\n            // Callback route, redirected to from Auth0 after login\n            path: \"callback\",\n            loader: async ({ request }) => {\n              const url = new URL(request.url);\n              const searchParams = new URLSearchParams(url.search);\n              const state = searchParams.get(\"state\");\n              const code = searchParams.get(\"code\");\n\n              if (!state || !code) throw new Error(\"Login failed\");\n\n              try {\n                const redirectURL = await Auth0Provider.validate(state, code);\n                return redirect(redirectURL);\n              } catch (error) {\n                throw new Error(\"Login failed\");\n              }\n            },\n          },\n          {\n            // Logout route\n            path: \"logout\",\n            loader: async () => {\n              try {\n                const redirectURL = await Auth0Provider.logout();\n                return redirect(redirectURL);\n              } catch (error) {\n                throw new Error(\"Logout failed\");\n              }\n            },\n          },\n          {\n            element: <Outlet />,\n            // Redirect to /login if not authenticated\n            loader: async ({ request }) => {\n              if (!Auth0Provider.isAuthenticated()) {\n                const params = new URLSearchParams();\n                params.set(\"returnTo\", new URL(request.url).pathname);\n                return redirect(\"/login?\" + params.toString());\n              }\n              return null;\n            },\n            // Protected routes\n            children: [\n              {\n                path: \"admin-dashboard\",\n                Component: AdminDashboard,\n              },\n            ],\n          },\n        ],\n      },\n    ],\n  },\n]);\n\nexport default function App() {\n  return <RouterProvider router={router} fallbackElement={<p>Loading...</p>} />;\n}\n\nfunction Layout({ children }: PropsWithChildren) {\n  return (\n    <div>\n      <header>\n        <nav className=\"nav\">\n          <div className=\"navLinks\">\n            <Link to=\"/\">Home</Link>\n            <Link to=\"/admin-dashboard\">Admin Dashboard</Link>\n          </div>\n\n          <LoginStatus />\n        </nav>\n      </header>\n\n      <main className=\"main\">{children ?? <Outlet />}</main>\n    </div>\n  );\n}\n\nfunction ErrorBoundary() {\n  const error = useRouteError() as Error;\n  return (\n    <div>\n      <h1>Something went wrong</h1>\n      <p>{error.message || JSON.stringify(error)}</p>\n    </div>\n  );\n}\n-- lib/auth.ts --\nimport Cookies from \"js-cookie\";\nimport getRequestClient from \"./getRequestClient.ts\";\n\ntype RedirectURL = string;\n\n/**\n * Handles the backend communication for the authentication flow.\n */\nexport const Auth0Provider = {\n  client: getRequestClient(),\n  isAuthenticated: () => !!Cookies.get(\"auth-token\"),\n\n  async login(returnTo: RedirectURL): Promise<RedirectURL> {\n    const response = await this.client.auth.Login();\n    Cookies.set(\"state\", response.state);\n    sessionStorage.setItem(response.state, returnTo);\n    return response.auth_code_url;\n  },\n\n  async logout(): Promise<RedirectURL> {\n    const response = await this.client.auth.Logout();\n\n    Cookies.remove(\"auth-token\");\n    Cookies.remove(\"state\");\n\n    return response.redirect_url;\n  },\n\n  async validate(state: string, authCode: string): Promise<RedirectURL> {\n    if (state != Cookies.get(\"state\")) throw new Error(\"Invalid state\");\n\n    const response = await this.client.auth.Callback({ code: authCode });\n    Cookies.set(\"auth-token\", response.token);\n    const returnURL = sessionStorage.getItem(state) ?? \"/\";\n    sessionStorage.removeItem(state);\n    return returnURL;\n  },\n};\n-- components/LoginStatus.tsx --\nimport getRequestClient from \"../lib/getRequestClient.ts\";\nimport { useFetcher } from \"react-router-dom\";\nimport { useEffect, useState } from \"react\";\nimport { auth } from \"../lib/client.ts\";\nimport { Auth0Provider } from \"../lib/auth.ts\";\n\n/**\n * Component displaying login/logout button and basic user information if logged in.\n */\nfunction LoginStatus() {\n  const client = getRequestClient();\n  const fetcher = useFetcher();\n  const [profile, setProfile] = useState<auth.ProfileData>();\n  const [loading, setLoading] = useState(true);\n\n  // Fetch profile data if user is authenticated\n  useEffect(() => {\n    const getProfile = async () => {\n      setProfile(await client.auth.GetProfile());\n      setLoading(false);\n    };\n    if (Auth0Provider.isAuthenticated()) getProfile();\n    else setLoading(false);\n  }, []);\n\n  if (loading) return null;\n\n  if (profile) {\n    return (\n      <div className=\"authStatus\">\n        <img src={profile.picture} />\n        <fetcher.Form method=\"GET\" action=\"/logout\">\n          <button type=\"submit\">Sign out {profile.email}</button>\n        </fetcher.Form>\n      </div>\n    );\n  }\n\n  const params = new URLSearchParams();\n  params.set(\"returnTo\", window.location.pathname);\n  return (\n    <div className=\"authStatus\">\n      <fetcher.Form method=\"GET\" action={\"/login?\" + params.toString()}>\n        <button type=\"submit\">\n          {fetcher.state !== \"idle\" ? \"Signing in...\" : \"Sign in\"}\n        </button>\n      </fetcher.Form>\n    </div>\n  );\n}\n\nexport default LoginStatus;\n-- lib/getRequestClient.ts --\nimport Client, { Environment, Local } from \"./client.ts\";\nimport Cookies from \"js-cookie\";\n\n/**\n * Returns the generated Encore request client for either the local or staging environment.\n * If we are running the frontend locally (development) we assume that our Encore\n * backend is also running locally.\n */\nconst getRequestClient = () => {\n  const token = Cookies.get(\"auth-token\");\n  const env = import.meta.env.DEV ? Local : Environment(\"staging\");\n\n  return new Client(env, {\n    auth: token,\n  });\n};\n\nexport default getRequestClient;\n```\n\n## Auth0 Social Identity Providers\n\nAuth0 supports multiple [social identity providers](https://auth0.com/docs/authenticate/identity-providers/social-identity-providers) (like Google and GitHub) for web applications out of the box.\n"
  },
  {
    "path": "docs/go/how-to/break-up-monolith.md",
    "content": "---\nseotitle: Break a monolith into microservices\nseodesc: Learn how to quickly break up your backend monolith into microservices using Encore, while avoiding the common pitfalls.\ntitle: Break a monolith into microservices\nsubtitle: Evolving your architecture as needed\nlang: go\n---\n\nIt's common to want to break out specific functionality into separate services. Perhaps you want to independently scale a specific service, or simply want to structure your codebase in smaller pieces.\n\nEncore makes it simple to evolve your system architecture over time, and enables you to deploy your application in multiple different ways without making code changes.\n\n## How to break out a service from a monolith\n\nAs a (slightly silly) example, let's imagine we have a monolith `hello` with two API endpoints `H1` and `H2`. It looks like this:\n\n```go\npackage hello\n\nimport (\n\t\"context\"\n)\n\n//encore:api public path=/hello/:name\nfunc H1(ctx context.Context, name string) (*Response, error) {\n\tmsg := \"Hello, \" + name + \"!\"\n\treturn &Response{Message: msg}, nil\n}\n\n//encore:api public path=/yo/:name\nfunc H2(ctx context.Context, name string) (*Response, error) {\n\tmsg := \"Yo, \" + name + \"!\"\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n```\n\nNow we're going to break out `H2` into its own separate service. Happily, all we need to do is create a new package, let's call it `yo`, and move the `H2` endpoint into it.\n\nLike so:\n```go\npackage yo\n\nimport (\n\t\"context\"\n)\n\n//encore:api public path=/yo/:name\nfunc H2(ctx context.Context, name string) (*Response, error) {\n\tmsg := \"Yo, \" + name + \"!\"\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n```\n\nOn disk we now have:\n```\n/my-app\n├── encore.app        // ... and other top-level project files\n│\n├── hello             // hello service (a Go package)\n│   └── hello.go      // hello service code\n│\n└── yo                // yo service (a Go package)\n    └── yo.go         // yo service code\n```\n\nEncore now understands these are separate services, and when you run your app you'll see that the [Service Catalog](/docs/go/observability/service-catalog) has been automatically updated accordingly.\n\n<img src=\"/assets/docs/microservices-service-catalog.png\" title=\"Service Catalog - Microservices\" />\n\nAs well as the [Flow architecture diagram](/docs/go/observability/encore-flow).\n\n<img src=\"/assets/docs/microservices-flow.png\" title=\"Encore Flow - Microservices\" />\n\n## Sharing databases between services (or not)\n\nDeciding whether to share a database between multiple services depends on your specific situation. Encore supports both options. Learn more in the [database documentation](/docs/go/primitives/share-db-between-services).\n\n"
  },
  {
    "path": "docs/go/how-to/cgo.md",
    "content": "---\nseotitle: Build Go applications with cgo using Encore\nseodesc: Learn how to build Go applications with cgo using Encore\ntitle: Build with cgo\nlang: go\n---\n\nCgo is a feature of the Go compiler that enables Go programs to interface\nwith libraries written in other languages using C bindings.\n\nBy default, for improved portability Encore builds applications with cgo support disabled.\n\nTo enable cgo for your application, add `\"build\": {\"cgo_enabled\": true}` to your `encore.app` file.\n\nFor example:\n\n```json\n-- encore.app --\n{\n  \"id\": \"my-app-id\",\n  \"build\": {\n    \"cgo_enabled\": true\n  }\n}\n```\n\nWith this setting Encore's build system will compile the application using an Ubuntu builder image\nwith gcc pre-installed.\n\n## Static linking\n\nTo keep the resulting Docker images as minimal as possible, Encore compiles applications with static linking.\nThis happens even with cgo enabled. As a result the cgo libraries you use must support static linking.\n\nIn some cases, you may need to add additional linker flags to properly work with static linking of cgo libraries.\nSee the [official cgo docs](https://pkg.go.dev/cmd/cgo) for more information on how to do this.\n"
  },
  {
    "path": "docs/go/how-to/clerk-auth.md",
    "content": "---\nseotitle: How to use Clerk to authenticate users in your backend application\nseodesc: Learn how to use Clerk for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Clerk.\ntitle: Use Clerk with your app\nlang: go\n---\n\nIn this guide you will learn how to set up an Encore [auth handler](/docs/go/develop/auth#the-auth-handler) that makes use of\n[Clerk](https://clerk.com/) in order to add an integrated signup and login experience to your web app.\n\nFor all the code and instructions of how to clone and run this example locally, see the [Clerk Example](https://github.com/encoredev/examples/tree/main/clerk) in our examples repo.\n\n## Set up the auth handler\n\nIn your Encore app, install the following module:\n\n```shell\n$ go get github.com/clerkinc/clerk-sdk-go/clerk\n```\n\nCreate a folder and naming it `auth`, this is where our authentication related backend code will live.\n\nIt's time to define your [auth handler](/docs/go/develop/auth). Create `auth/auth.go` and paste the following:\n\n```go\npackage auth\n\nimport (\n\t\"context\"\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n\t\"github.com/clerkinc/clerk-sdk-go/clerk\"\n)\n\nvar secrets struct {\n\tClientSecretKey string\n}\n\n// Service struct definition.\n// Learn more: encore.dev/docs/primitives/services-and-apis/service-structs\n//\n//encore:service\ntype Service struct {\n\tclient clerk.Client\n}\n\n// initService is automatically called by Encore when the service starts up.\nfunc initService() (*Service, error) {\n\tclient, err := clerk.NewClient(secrets.ClientSecretKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Service{client: client}, nil\n}\n\ntype UserData struct {\n\tID                    string               `json:\"id\"`\n\tUsername              *string              `json:\"username\"`\n\tFirstName             *string              `json:\"first_name\"`\n\tLastName              *string              `json:\"last_name\"`\n\tProfileImageURL       string               `json:\"profile_image_url\"`\n\tPrimaryEmailAddressID *string              `json:\"primary_email_address_id\"`\n\tEmailAddresses        []clerk.EmailAddress `json:\"email_addresses\"`\n}\n\n// The `encore:authhandler` annotation tells Encore to run this function for all\n// incoming API call that requires authentication.\n// Learn more: encore.dev/docs/develop/auth#the-auth-handler\n//\n//encore:authhandler\nfunc (s *Service) AuthHandler(ctx context.Context, token string) (auth.UID, *UserData, error) {\n\t// verify the session\n\tsessClaims, err := s.client.VerifyToken(token)\n\tif err != nil {\n\t\treturn \"\", nil, &errs.Error{\n\t\t\tCode:    errs.Unauthenticated,\n\t\t\tMessage: \"invalid token\",\n\t\t}\n\t}\n\n\tuser, err := s.client.Users().Read(sessClaims.Claims.Subject)\n\tif err != nil {\n\t\treturn \"\", nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: err.Error(),\n\t\t}\n\t}\n\n\tuserData := &UserData{\n\t\tID:                    user.ID,\n\t\tUsername:              user.Username,\n\t\tFirstName:             user.FirstName,\n\t\tLastName:              user.LastName,\n\t\tProfileImageURL:       user.ProfileImageURL,\n\t\tPrimaryEmailAddressID: user.PrimaryEmailAddressID,\n\t\tEmailAddresses:        user.EmailAddresses,\n\t}\n\n\treturn auth.UID(user.ID), userData, nil\n}\n```\n\n## Clerk credentials\n\nCreate a Clerk account if you haven't already. Then, in the Clerk dashboard, create a new applications.\n\nNext, go to the *API Keys* page for your app. Copy one of the \"Secret keys\" (the \"Publishable Key\" will be used by your frontend).\n\nThe `Secret key` is sensitive and should not be hardcoded in your code/config. Instead, you should store that as an [Encore secret](/docs/go/primitives/secrets).\n\nFrom your terminal (inside your Encore app directory), run:\n\n```shell\n$ encore secret set --prod ClientSecretKey\n```\n\nNow you should do the same for the development secret. The most secure way is to create another secret key (Clerk allows you to have multiple).\nOnce you have a client secret for development, set it similarly to before:\n\n```shell\n$ encore secret set --dev ClientSecretKey\n```\n\n## Frontend\n\nClerk offers a [React SDK](https://clerk.com/docs/references/react/overview) for the frontend which makes it really simple to integrate \na login/signup flow inside your web app as well as getting the token required to communicate with your Encore backend. \n\nYou can use the `useAuth` hook from `@clerk/clerk-react` to get the token and send it to your backend.\n\n```tsx\nimport { useAuth } from '@clerk/clerk-react';\n \nexport default function ExternalDataPage() {\n  const { getToken, isLoaded, isSignedIn } = useAuth();\n \n  if (!isLoaded) {\n    // Handle loading state however you like\n    return <div>Loading...</div>;\n  }\n \n  if (!isSignedIn) {\n    // Handle signed out state however you like\n    return <div>Sign in to view this page</div>;\n  }\n \n  const fetchDataFromExternalResource = async () => {\n    const token = await getToken();\n    // Use token to send to Encore backend when fetching data\n    return data;\n  }\n \n  return <div>...</div>;\n}\n```\n\nFor a fully working backend + frontend example see the [Clerk Example](https://github.com/encoredev/examples/tree/main/clerk) in our examples repo.\n"
  },
  {
    "path": "docs/go/how-to/debug.md",
    "content": "---\nseotitle: How to debug your application with Delve\nseodesc: Learn how to debug your Go backend application using Delve and Encore.\ntitle: Debug with Delve\nlang: go\n---\n\nEncore makes it easy to debug your application using [Delve](https://github.com/go-delve/delve \"Delve\").\n\nFirst, make sure you have `dlv` installed by running (Go 1.16 and later):\n\n```shell\n$ go install github.com/go-delve/delve/cmd/dlv@latest\n```\n\nYou have two debugger options, you can either debug by attaching to a running process or by starting the process in debug mode.\n\n## Debug by starting the process in debug mode\nRun your Encore application with `encore run --debug=break`. This will launch your encore application with a headless Delve server, which will pause your application until a debugger is attached.\n\n```shell\n$ encore run --debug=break\nAPI Base URL:      http://localhost:4000\nDev Dashboard URL: http://localhost:9400/hello-world-cgu2\n\nAPI server listening at: 127.0.0.1:2345\n```\n\nNow it's time to attach the debugger. The instructions differ depending on how you would like to debug (in your terminal or in your editor). If instructions for your editor aren’t listed below, consult your editor for information on how to attach to a Delve server.\n\n### Terminal debugging\nTo debug in your terminal, run `dlv attach :2345`. You should see:\n\n```shell\n$ dlv connect :2345\nType 'help' for list of commands.\n(dlv)\n```\nHow to use Delve’s terminal interface for debugging is out of scope for this guide, but there are great resources available. For a good introduction, see [](https://golang.cafe/blog/golang-debugging-with-delve.html \"Debugging with Delve\").\n\n### Visual Studio Code\nTo debug with VS Code you must first add a debug configuration. Press `Run -> Add Configuration`, choose `Go -> Connect to server`. Input `127.0.0.1` as host and `2345` as port. The resulting configuration should look something like this:\n\n```json\n{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Connect to server\",\n            \"type\": \"go\",\n            \"request\": \"attach\",\n            \"mode\": \"remote\",\n            \"remotePath\": \"${workspaceFolder}\",\n            \"port\": 2345,\n            \"host\": \"127.0.0.1\"\n        }\n    ]\n}\n```\n\nNext, open the **Run and Debug** menu in the toolbar on the left, select `Connect to server` (the configuration you just created), and then press the green arrow.\n\nThat’s it! You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect.\n\n### Goland\nTo debug with Goland, you must create a new Go Remote configuration. Press `Run | Edit Configurations`, click the `+` button, and choose `Go Remote`. Give it a name and hit `OK`. \n\nNow select the configuration you just created and press the green bug.\n\nThat's it. You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect.\n\n## Debug by attaching to a running process\n\nRun your Encore application with `encore run --debug`. This will cause Encore to print the Process ID to the terminal, which you will use to attach your debugger:\n\n```shell\n$ encore run --debug\nAPI Base URL:      http://localhost:4000\nDev Dashboard URL: http://localhost:9400/hello-world-cgu2\nProcess ID:        51894\n1:48PM TRC registered endpoint path=/hello/:name service=hello endpoint=Hello\n```\n\n(Your process id will differ).\n\nWhen your Encore application is running, it’s time to attach the debugger. The instructions differ depending on how you would like to debug (in your terminal or in your editor). If instructions for your editor aren’t listed below, consult your editor for information on how to attach a debugger to a running process.\n\n### Terminal debugging\nTo debug in your terminal, run `dlv attach $PID` (replace `$PID` with your Process ID from the previous step). You should see:\n\n```shell\n$ dlv attach 51894\nType 'help' for list of commands.\n(dlv)\n```\n\nHow to use Delve’s terminal interface for debugging is out of scope for this guide, but there are great resources available. For a good introduction, see [](https://golang.cafe/blog/golang-debugging-with-delve.html \"Debugging with Delve\").\n\n### Visual Studio Code\nTo debug with VS Code you must first add a debug configuration. Press `Run -> Add Configuration`, choose `Go -> Attach to local process`. In the generated configuration, you should see `\"processId\": 0` as a field. Replace `0` with the process id from above.\n\nNext, open the **Run and Debug** menu in the toolbar on the left, select Attach to Process (the configuration you just created), and then press the green arrow.\n\nThat’s it! You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect.\n\n### Goland\nTo debug with Goland, you must first install the `gops` package. Open a terminal and run the following command\n\n```shell\ngo get -t github.com/google/gops/\n```\n\nThen click `Run | Attach to Process`. If a notification window appears, click the `Invoke 'go get gops'` link. Once \nit has completed, click `Run | Attach to Process` again. In the dialog that appears, select the process with the\nprocess ID from above.\n\nThat's it. You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect.\n"
  },
  {
    "path": "docs/go/how-to/dependency-injection.md",
    "content": "---\nseotitle: How to use dependency injection to test your microservices app\nseodesc: Learn how to use dependency injection in your Go based microservices backend application using Encore.\ntitle: Dependency Injection\nsubtitle: Simplifying testing\nlang: go\n---\n\nDependency Injection is a fancy name for a simple concept: when you depend on some\nfunctionality, add that dependency as a field on your struct and refer to it that way\ninstead of directly calling it. By doing so it becomes easier to test your services\nby swapping out certain dependencies for other implementations (often with the use of\ninterfaces).\n\nEncore provides built-in support for dependency injection in services through the use\nof the `//encore:service` directive and a **service struct**. See the [service structs docs](/docs/go/primitives/service-structs) more information on how to define service structs.\n\nAs an example, consider an email service that has a SendGrid API client that is\ndependency injected. It might look like this:\n\n```go\npackage email\n\n//encore:service\ntype Service struct {\n\tsendgridClient *sendgrid.Client\n}\n\nfunc initService() (*Service, error) {\n    client, err := sendgrid.NewClient()\n    if err != nil {\n        return nil, err\n    }\n    return &Service{sendgridClient: client}, nil\n}\n```\n\nYou can then define APIs as methods on this struct:\n```go\n//encore:api private\nfunc (s *Service) Send(ctx context.Context, p *SendParams) error {\n\t// ... use s.sendgridClient to send emails ...\n}\n```\n\n### Mocking dependencies\n\nIf you wish to mock out the SendGrid client for testing purposes you can change the\nfield to an interface:\n\n```go\ntype sendgridClient interface {\n\tSendEmail(...) // a hypothetical signature, for illustration purposes\n}\n\n//encore:service\ntype Service struct {\n    sendgridClient sendgridClient\n}\n```\n\nThen during your tests you can instantiate the service object by hand:\n```go\nfunc TestFoo(t *testing.T) {\n    svc := &Service{sendgridClient: &myMockClient{}}\n    // ...\n}\n```\n"
  },
  {
    "path": "docs/go/how-to/entgo-orm.md",
    "content": "---\nseotitle: Use ent + Atlas for database schema management with Encore.\nseodesc: See how you can use an ORM like ent with Atlas to handle your database schemas.\ntitle: Use ent ORM + Atlas for database schemas\nlang: go\n---\n\nEncore has all the tools needed to support ORMs and migration frameworks out-of-the-box through\n[named databases](/docs/go/primitives/share-db-between-services) and \n[migration files](/docs/go/primitives/databases#defining-a-database-schema). Writing plain SQL might\nnot work for your use case, or you may not want to use SQL in the first place. \n\nORMs like [ent](https://entgo.io/) and migration frameworks like [Atlas](https://atlasgo.io/) can\nbe used with Encore by integrating their logic with a system's database. Encore is not restrictive,\nit uses plain SQL migration files for its migrations. \n\n- If your ORM of choice can connect to any database using a [standard SQL driver](https://github.com/lib/pq), then it can be used with Encore.\n- If your migration framework can generate SQL migration files without any modifications, then it can be used with Encore.\n\nLet's take a look at how you can integrate ent with Encore, using Atlas for generating the migration files.\n\n## Add ent schemas to a service\n[Install ent](https://entgo.io/docs/tutorial-setup#installation), then initialize your first\nschema in the service where you want to use it. For example, if you had the following app structure:\n\n```\n/my-app\n├── encore.app\n└── user        // user service\n```\n\nYou can then use this command to generate a user schema along with the ent directory that will contain\nthat schema and all future generated files:\n\n```shell\n$ go run entgo.io/ent/cmd/ent@latest new --target user/ent/schema User\n```\n\nThe `--target` option sets the schema directory within your Encore system. Each system\nshould contain its own models and schemas, and its own migration files. Like you would when using\nplain SQL.\n\nAdd the fields and edges for your new model in the generated file under `user/ent/schema/user.go`.\n\nNow, run the following command:\n\n```shell\n$ go run entgo.io/ent/cmd/ent@latest generate ./user/ent/schema\n```\n\nThis generates the ent client files. Run this command again whenever you change the schemas.\n\n## Integrating ent with an Encore database\n\nEncore automates database provisioning, and automatically runs migrations in all environments.\n\nTo integrate ent with Encore, we need to do three things:\n\n1. Create the Encore database\n2. Set up the ent client to use that database.\n3. Generate migration files for the ent schema, using Atlas.\n\n### Create the Encore database\n\nCreate the database using [`sqldb.NewDatabase`](/docs/go/primitives/databases) in `user/user.go`:\n\n```\n-- user/user.go --\npackage user\n\nimport \"encore.dev/storage/sqldb\"\n\nvar userDB = sqldb.NewDatabase(\"user\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n```\n\nNow, create the `migrations` directory, and leave it empty for now:\n\n```shell\n$ mkdir user/migrations\n```\n\n### Connect ent to the database\n\nNext, extend the user service with a [Service Struct](/docs/go/primitives/service-structs) that\ncreates an ent client connected to the database.\n\nReplace the contents of the `user/user.go` file with:\n\n```\n-- user/user.go --\npackage user\n\nimport (\n    \"encore.dev/storage/sqldb\"\n\t\"entgo.io/ent/dialect\"\n\tentsql \"entgo.io/ent/dialect/sql\"\n\t\n\t\"encore.app/user/ent\"\n)\n\nvar userDB = sqldb.NewDatabase(\"user\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n//encore:service\ntype Service struct{\n    ent *ent.Client\n}\n\nfunc initService() (*Service, error) {\n    driver := entsql.OpenDB(dialect.Postgres, userDB.Stdlib())\n    entClient := ent.NewClient(ent.Driver(driver))\n    return &Service{ent: entClient}, nil\n}\n```\n\nNow ent is fully wired up to the Encore database, and can be used from the service struct in any API endpoint.\n\n## Using Atlas for database migrations\n\nFinally, we'll set up Atlas to generate database migrations for the ent schema.\n\nFirst, make sure you [have Atlas installed](https://atlasgo.io/getting-started).\n\nThen, create the file `user/atlas.hcl` containing the following:\n\n```\n-- user/atlas.hcl --\nenv \"local\" {\n  src = \"ent://ent/schema\"\n\n  migration {\n    dir = \"file://migrations\"\n    format = golang-migrate\n  }\n\n  format {\n    migrate {\n      diff = \"{{ sql . \\\"  \\\" }}\"\n    }\n  }\n}\n```\n\nThis tells Atlas to generate migrations for the ent schema, and to output them to the `migrations` directory.\n\nAtlas works by comparing the desired ent schema with the current database schema, and generating a migration\nto bring the database schema in line with the ent schema. This relies on a so-called \"shadow database\",\nwhich is an empty database that Atlas uses to compare the ent schema against.\n\nFortunately for us, Encore has built-in support for shadow databases.\n\nCreate the file `user/scripts/generate-migration` containing the following:\n\n```\n-- user/scripts/generate-migration --\n#!/bin/bash\nset -eu\nDB_NAME=user\nMIGRATION_NAME=${1:-}\n\n# Reset the shadow database\nencore db reset --shadow $DB_NAME\n\n# ent executes Go code without initializing Encore when generating migrations,\n# so configure the Encore runtime to be aware that this is expected.\nexport ENCORERUNTIME_NOPANIC=1\n\n# Generate the migration\natlas migrate diff $MIGRATION_NAME --env local --dev-url \"$(encore db conn-uri --shadow $DB_NAME)&search_path=public\"\n```\n\nFinally, make the script executable, and generate our first migration:\n\n```shell\n$ chmod +x user/scripts/generate-migration\n$ cd user && ./scripts/generate-migration init\n```\n\nYou should see a new migration file being added to the `user/migrations` directory,\ncontaining the schema changes to create the ent models.\n\nYou can now run the service with `encore run`, and everything should be ready to go!\n"
  },
  {
    "path": "docs/go/how-to/firebase-auth.md",
    "content": "---\nseotitle: How to use Firebase Auth for your backend application\nseodesc: Learn how to use Firebase Auth for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Firebase Auth.\ntitle: Use Firebase Auth with your app\nlang: go\n---\n\nEncore's [authentication support](/docs/go/develop/auth) provides a simple yet powerful\nway of dealing with various authentication scenarios.\n\n<a href=\"https://firebase.google.com/docs/auth\" target=\"_blank\" rel=\"nofollow\">Firebase Authentication</a>\n{\" \"}is a common solution for quickly setting up a user store and simplifying social logins.\n\nEncore makes it really easy to integrate with Firebase Authentication on the backend.\n\nFor all the code and instructions of how to clone and run this example locally, see the [Firebase Auth Example](https://github.com/encoredev/examples/tree/main/firebase-auth) in our examples repo.\n\n## Set up auth handler\n\nFirst, install two modules:\n\n```shell\n$ go get firebase.google.com/go/v4 go4.org/syncutil\n```\n\nNext it's time to define your [authentication handler](/docs/go/develop/auth).\nIt can live in whatever service you'd like, but it's usually easiest\nto create a designated `user` service.\n\nCreate the `user/user.go` file and add the following skeleton code:\n\n```go\npackage user\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"encore.dev/beta/auth\"\n\tfirebase \"firebase.google.com/go/v4\"\n\tfbauth \"firebase.google.com/go/v4/auth\"\n\t\"go4.org/syncutil\"\n\t\"google.golang.org/api/option\"\n)\n\n// Data represents the user's data stored in Firebase Auth.\ntype Data struct {\n\t// Email is the user's email.\n\tEmail string\n\t// Name is the user's name.\n\tName string\n\t// Picture is the user's picture URL.\n\tPicture string\n}\n\n// ValidateToken validates an auth token against Firebase Auth.\n//encore:authhandler\nfunc ValidateToken(ctx context.Context, token string) (auth.UID, *Data, error) {\n    panic(\"Not Yet Implemented\")\n}\n```\n\n## Initialize Firebase SDK\n\nNext, let's set up the Firebase Auth client. We'll use\n&nbsp;<a href=\"https://pkg.go.dev/go4.org/syncutil#Once\" target=\"_blank\" rel=\"nofollow\">`syncutil.Once`</a>&nbsp;\nto do it lazily the first time we need it.\n\nAdd to the bottom of our file:\n\n```go\nvar (\n\tfbAuth    *fbauth.Client\n\tsetupOnce syncutil.Once\n)\n\n// setupFB ensures Firebase Auth is setup.\nfunc setupFB() error {\n    return setupOnce.Do(func() error {\n        opt := option.WithCredentialsJSON([]byte(secrets.FirebasePrivateKey))\n        app, err := firebase.NewApp(context.Background(), nil, opt)\n        if err == nil {\n            fbAuth, err = app.Auth(context.Background())\n        }\n        return err\n    })\n}\n\nvar secrets struct {\n\t// FirebasePrivateKey is the JSON credentials for calling Firebase.\n\tFirebasePrivateKey string\n}\n```\n\n## Validate token against Firebase\n\nNow that we have the code to initialize Firebase Auth, we can use it from our `ValidateToken` auth handler.\nUpdate the function to look like the following:\n\n```go\nfunc ValidateToken(ctx context.Context, token string) (auth.UID, *Data, error) {\n    if err := setupFB(); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\ttok, err := fbAuth.VerifyIDToken(ctx, token)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\temail, _ := tok.Claims[\"email\"].(string)\n\tname, _ := tok.Claims[\"name\"].(string)\n\tpicture, _ := tok.Claims[\"picture\"].(string)\n\tuid := auth.UID(tok.UID)\n\n\tusr := &Data{\n\t\tEmail:   email,\n\t\tName:    name,\n\t\tPicture: picture,\n\t}\n\treturn uid, usr, nil\n}\n```\n\nGreat! We're done with the code. Now we just need to set up the secret.\n\n## Set Firebase secret credentials\n\nIf you haven't already, set up a <a href=\"https://firebase.google.com\" target=\"_blank\" rel=\"nofollow\">Firebase</a> project.\n\nThen, go to **Project settings** and navigate to **Service accounts**.\nSelect `Go` as the language of choice and click `Generate new private key`.\nDownload the generated key and take note where it is stored.\n\nNext, store the private key as your firebase secret.\nFrom your terminal (inside your Encore app directory), run:\n\n```shell\n$ encore secret set --type prod FirebasePrivateKey < /path/to/firebase-private-key.json\nSuccessfully updated production secret FirebasePrivateKey\n```\n\nNow you should do the same for the development secret. The most secure way is to\nset up a different Firebase project and use that for development.\n\nDepending on your security requirements you could also use the same Firebase project,\nbut we recommend generating a new private key for development in that case.\n\nOnce you have a private key for development, set it similarly to before:\n\n```shell\n$ encore secret set --type dev,local,pr FirebasePrivateKey < /path/to/firebase-private-key.json\nSuccessfully updated development secret FirebasePrivateKey\n```\n\nThat's it! You can now call your Encore application and pass in Firebase tokens.\nEncore will run your auth handler and validate the token against Firebase Auth.\n\n## Frontend\n\nFirebase offers a [npm package](https://www.npmjs.com/package/firebase) for your web frontend which makes it really simple to create \na login/signup flow inside your web app as well as getting the token required to communicate with your Encore backend. \n\nFor a fully working backend + frontend example see the [Firebase Auth Example](https://github.com/encoredev/examples/tree/main/firebase-auth) in our examples repo.\n"
  },
  {
    "path": "docs/go/how-to/grpc-connect.md",
    "content": "---\nseotitle: Use Connect for gRPC/protobuf-based APIs with Encore\nseodesc: See how you can use the Connect protocol for gRPC communication with Encore services\ntitle: Use Connect for incoming gRPC requests\nlang: go\n---\n\nThe [Connect protocol](https://connectrpc.com/) is an HTTP/2-based protocol for RPC communication.\nIt's conceptually similar to gRPC, but with better support for using from browsers and JavaScript clients.\n\nThis guide shows how to use Encore for setting up a Connect service for external clients to use:\n\n1. First, we'll define a simple gRPC service using Protobuf and Connect.\n2. Then, we'll implement the service in Go, using [connect-go](https://connectrpc.com/docs/go/getting-started).\n3. Then, we'll mount the Connect service into Encore with a raw endpoint.\n4. Finally, we'll call the Connect service from cURL using its JSON mapping.\n\n## Define a Connect service\n\nWe'll largely follow the connect-go [getting started guide](https://connectrpc.com/docs/go/getting-started)\nwith some small tweaks.\n\nStart by installing the necessary tools:\n\n```shell\n$ go install github.com/bufbuild/buf/cmd/buf@latest\n$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest\n$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest\n$ go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest\n```\n\nNext, inside your Encore application ([create one if you haven't already](/docs/go/quick-start))\ncreate a new file at `greet/v1/greet.proto` with the following contents:\n\n```\n-- greet/v1/greet.proto --\nsyntax = \"proto3\";\n\npackage greet.v1;\n\noption go_package = \"encore.app/gen/greet/v1;greetv1\";\n\nmessage GreetRequest {\n  string name = 1;\n}\n\nmessage GreetResponse {\n  string greeting = 1;\n}\n\nservice GreetService {\n  rpc Greet(GreetRequest) returns (GreetResponse) {}\n}\n```\n\nNext, add a `buf.gen.yaml` in the repository root, containing:\n\n```\n-- buf.gen.yaml --\nversion: v2\nplugins:\n  - local: protoc-gen-go\n    out: gen\n    opt: paths=source_relative\n  - local: protoc-gen-connect-go\n    out: gen\n    opt: paths=source_relative\n```\n\nNow it's time to generate the connect-go service code. Run:\n\n```shell\n$ buf lint\n$ buf generate\n```\n\nIf all went well, you should see a new `gen` directory in the repository root containing some generated Go code:\n\n```\ngen\n└── greet\n    └── v1\n        ├── greet.pb.go\n        └── greetv1connect\n            └── greet.connect.go\n```\n\n## Implement the service\n\nNow that we have the service definition, we can implement the Connect service in Go.\n\nAdd the file `greet/greet.go` with the following contents:\n\n```\n-- greet/greet.go --\npackage greet\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"connectrpc.com/connect\"\n\n\tgreetv1 \"encore.app/gen/greet/v1\" // generated by protoc-gen-go\n)\n\ntype GreetServer struct{}\n\nfunc (s *GreetServer) Greet(\n\tctx context.Context,\n\treq *connect.Request[greetv1.GreetRequest],\n) (*connect.Response[greetv1.GreetResponse], error) {\n\tlog.Println(\"Request headers: \", req.Header())\n\tres := connect.NewResponse(&greetv1.GreetResponse{\n\t\tGreeting: fmt.Sprintf(\"Hello, %s!\", req.Msg.Name),\n\t})\n\tres.Header().Set(\"Greet-Version\", \"v1\")\n\treturn res, nil\n}\n```\n\n<Callout type=\"info\">\n\nThe sample code is straight from the [getting started guide](https://connectrpc.com/docs/go/getting-started);\nthere are no Encore specific changes required here.\n\n</Callout>\n\n## Mount the service in Encore\n\nNow we'll create an Encore [service struct](/docs/go/primitives/service-structs)\nthat initializes the Connect service, and a [raw endpoint](/docs/go/primitives/raw-endpoints)\nthat forwards incoming requests to the Connect service.\n\nAdd the file `greet/service.go` with the following contents:\n\n```\n-- greet/service.go --\npackage greet\n\nimport (\n\t\"net/http\"\n\n\t\"encore.app/gen/greet/v1/greetv1connect\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n)\n\n//encore:service\ntype Service struct {\n\troutes http.Handler\n}\n\n//encore:api public raw path=/greet.v1.GreetService/*endpoint\nfunc (s *Service) GreetService(w http.ResponseWriter, req *http.Request) {\n\ts.routes.ServeHTTP(w, req)\n}\n\nfunc initService() (*Service, error) {\n\tgreeter := &GreetServer{}\n\tmux := http.NewServeMux()\n\tpath, handler := greetv1connect.NewGreetServiceHandler(greeter)\n\tmux.Handle(path, handler)\n\treturn &Service{routes: mux}, nil\n}\n```\n\nThat's it! We're ready to run the service and check that everything works.\n\n## Run the service\n\nRun the service with `encore run`:\n\n```shell\n$ encore run\n```\n\nOnce it starts up, open a separate terminal and use `grpcurl` to call the service:\n\n```shell\n# Install grpcurl if you haven't already\n$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest\n\n# Call the service with grpcurl\ngrpcurl \\\n    -protoset <(buf build -o -) -plaintext \\\n    -d '{\"name\": \"Jane\"}' \\\n    localhost:4000 greet.v1.GreetService/Greet\n{\"greeting\": \"Hello, Jane!\"} \n\n# Or call the service with curl\n$ curl -H \"Content-Type: application/json\" -d '{\"name\": \"Jane\"}' http://localhost:4000/greet.v1.GreetService/Greet\n{\"greeting\":\"Hello, Jane!\"}  # Expected response\n```\n\nIf you see `{\"greeting\":\"Hello, Jane!\"}`, everything is working!\n\nWhat's more, Encore automatically traces the incoming requests, and adds request logging and captures request metrics.\n"
  },
  {
    "path": "docs/go/how-to/http-requests.md",
    "content": "---\nseotitle: How to receive regular HTTP requests in your backend application\nseodesc: Learn how to receive regular HTTP requests in your Go based backend application using Encore.\ntitle: Receive regular HTTP requests\nsubtitle: Dropping down in abstraction level\nlang: go\n---\n\nEncore makes it easy to define APIs and expose them, but it works best when you are in charge of the API schema.\n\nSometimes you need more control over the underlying HTTP request, such as to accept incoming webhooks from other\nservices, or to use WebSockets to stream data to/from the client.\n\nFor these use cases Encore lets you define **raw endpoints**. Raw endpoints operate at a lower abstraction level,\ngiving you access to the underlying HTTP request.\n\n## Defining raw endpoints\n\nTo define a raw endpoint, change the `//encore:api` annotation and function signature like so:\n\n```go\npackage service\n\nimport \"net/http\"\n\n// Webhook receives incoming webhooks from Some Service That Sends Webhooks.\n//encore:api public raw method=POST path=/webhook\nfunc Webhook(w http.ResponseWriter, req *http.Request) {\n    // ... operate on the raw HTTP request ...\n}\n```\n\nIf you're an experienced Go developer, this is just a regular Go HTTP handler.\n\nSee the <a href=\"https://pkg.go.dev/net/http#Handler\" target=\"_blank\" rel=\"nofollow\">net/http documentation</a>\nfor more information on how Go HTTP handlers work.\n\n## Reading path parameters\n\nSometimes webhooks have information in the path that you may be interested in retrieving or validating.\n\nTo do so, define the path with a path parameter, and then use [`encore.CurrentRequest`](https://pkg.go.dev/encore.dev#CurrentRequest)\nto access the path parameters. For example:\n\n```go\npackage service  \n  \nimport (  \n   \"net/http\"  \n   \n   \"encore.dev\"\n )\n\n//encore:api public raw method=POST path=/webhook/:id\nfunc Webhook(w http.ResponseWriter, req *http.Request) {  \n    id := encore.CurrentRequest().PathParams.Get(\"id\")\n\t// ... Do something with id\n }\n```\n"
  },
  {
    "path": "docs/go/how-to/integrate-frontend.mdx",
    "content": "---\nseotitle: Integrate your backend application with a frontend\nseodesc: Learn how to integrate your Go backend application with a frontend, using Encore's built-in frontend client generation feature.\ntitle: Integrate with a web frontend\nsubtitle: Keep using your favorite frontend hosting provider\nlang: go\n---\nEncore is not opinionated about where you host your frontend, pick the platform that suits your situation best.\n\nIf your frontend and backend use different domains, often the case when using PR preview environments for your frontend, you may need to [configure CORS](#handling-cors).\n\nTake a look at our [React starter template](https://encore.dev/templates/react) for an example of deploying a frontend to [Vercel](https://vercel.com/) or the [Meeting Notes tutorial](https://encore.dev/docs/go/tutorials/meeting-notes) deployed to [GitHub Pages](https://pages.github.com/).\n\n## Generating a request client\nEncore is able to generate frontend request clients (TypeScript or JavaScript). This lets you to keep the request/response types in sync without manual work and assists you in calling the APIs. Generate a client by running:\n\n```bash\n$ encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=<ENV_NAME>\n```\n\nAdding this as a script to your `package.json` is often a good idea to be able to run it whenever a change is made to your Encore API:\n\n```json\n{\n...\n\"scripts\": {\n    ...\n    \"generate-client:staging\": \"encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=staging\",\n    \"generate-client:local\": \"encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=local\"\n  }\n}\n```\n\nAfter that you are ready to use the request client in your code. Here is an example from the [Meeting Notes tutorial](https://encore.dev/docs/tutorials/meeting-notes) for calling the `GetNote` endpoint on the `note` service in order to retrieve a specific meeting note (which has the properties `id`, `cover_url` & `text`):\n\n```ts\nimport Client, { Environment, Local } from \"src/client.ts\";\n\n// Making request to locally running backend...\nconst client = new Client(Local);\n// or to a specific deployed environment\nconst client = new Client(Environment(\"staging\"));\n\n// Calling APIs as typesafe functions 🌟\nconst response = await client.note.GetNote(\"note-uuid\");\nconsole.log(response.id);\nconsole.log(response.cover_url);\nconsole.log(response.text);\n```\n\nSee more in the [client generation docs](/docs/develop/client-generation).\n\n### Asynchronous state management\n\nWhen building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale.\n[TanStack Query](https://tanstack.com/query/latest) is a popular library that was built to solve exactly these problems and works well with the Encore request client.\n\nHere is a simple example of using an Encore request client together with TanStack Query:\n\n```ts\nimport {\n  useQuery,\n  useMutation,\n  useQueryClient,\n  QueryClient,\n  QueryClientProvider,\n} from '@tanstack/react-query'\nimport Client, { todo } from '../encore-client'\n\n// Create a Encore client\nconst encoreClient = new Client(window.location.origin);\n\n// Create a react-query client\nconst queryClient = new QueryClient()\n\nfunction App() {\n  return (\n    // Provide the client to your App\n    <QueryClientProvider client={queryClient}>\n      <Todos />\n    </QueryClientProvider>\n  )\n}\n\nfunction Todos() {\n  // Access the client\n  const queryClient = useQueryClient()\n\n  // Queries\n  const query = useQuery({\n    queryKey: ['todos'],\n    queryFn: () => encoreClient.todo.List()\n  })\n\n  // Mutations\n  const mutation = useMutation({\n    mutationFn: (params: todo.AddParams) => encoreClient.todo.Add(params),\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: ['todos'] })\n    },\n  })\n\n  return (\n    <div>\n      <ul>\n        {query.data?.map((todo) => (\n          <li key={todo.id}>{todo.title}</li>\n        ))}\n      </ul>\n\n      <button\n        onClick={() => {\n          mutation.mutate({\n            id: Date.now(),\n            title: 'Do Laundry',\n          })\n        }}\n      >\n        Add Todo\n      </button>\n    </div>\n  )\n}\n\nrender(<App />, document.getElementById('root'))\n```\n\nThis example assumes that we have a `todo` service with a `List` and `Add` endpoint. When adding the new todo,\nTanStack Query will automatically invalidate the `todos` query and refetch it.\n\nFor a real-world example, take a look at the [Uptime Monitoring](https://github.com/encoredev/examples/tree/main/uptime) app which also makes use of\nTanStack Query's `refetchInterval` option for polling the backend.\n\n### Testing\nWhen unit testing a component that interacts with your Encore API you can mock methods on the request client to\nreturn a value suitable for the test. This makes your test URL agnostic because you are not intercepting\nspecific requests on the fetch layer. You also get type errors in your tests if the request client gets updated.\n\nHere is an example from the [Uptime Monitoring Starter](https://github.com/encoredev/examples/tree/main/uptime) where we are mocking a GET request method and spying on a POST request method:\n\n```ts\nimport { render, waitForElementToBeRemoved } from \"@testing-library/react\";\nimport App from \"./App\";\nimport { site } from \"./client\";\nimport { userEvent } from \"@testing-library/user-event\";\n\ndescribe(\"App\", () => {\n  beforeEach(() => {\n    // Return mocked data from the List (GET) endpoint\n    jest\n      .spyOn(site.ServiceClient.prototype, \"List\")\n      .mockReturnValue(Promise.resolve({\n        sites: [{\n          id: 1,\n          url: \"test.dev\"\n        }]\n      }));\n\n    // Spy on the Add (POST) endpoint\n    jest.spyOn(site.ServiceClient.prototype, \"Add\");\n  });\n\n  it(\"render sites\", async () => {\n    render(<App />);\n    await waitForElementToBeRemoved(() => screen.queryByText(\"Loading...\"));\n\n    // Verify that the List endpoint has been called\n    expect(site.ServiceClient.prototype.List).toBeCalledTimes(1);\n\n    // Verify that the sites are rendered with our mocked data\n    screen.getAllByText(\"test.dev\");\n  });\n\n  it(\"add site\", async () => {\n    render(<App />);\n    await waitForElementToBeRemoved(() => screen.queryByText(\"Loading...\"));\n\n    // Interact with the page and add 'another.com'\n    await userEvent.click(screen.getByText(\"Add website\"));\n    await userEvent.type(\n      screen.getByPlaceholderText(\"google.com\"),\n      \"another.com\",\n    );\n    await userEvent.click(screen.getByText(\"Save\"));\n\n    // Verify that the Add endpoint has been called with the correct parameters\n    expect(site.ServiceClient.prototype.Add).toHaveBeenCalledWith({\n      url: \"another.com\",\n    });\n  });\n})\n```\n\n<Callout type=\"info\">\n\n  In the example above we need to mock the `List` method on `site.ServiceClient.prototype` because the request client has not\n  yet been initialized when we're creating the mock. If you have access to the instance of the request client in your test\n  (which could be the case if you are passing the client around in your components) you can instead do `jest.spyOn(client.site, \"List\")`\n  and `expect(client.site.List).toHaveBeenCalled()` which would give you the same result.\n\n</Callout>\n\nMore examples of tests can be found in the [Uptime Monitoring Starter repo](https://github.com/encoredev/examples/tree/main/uptime).\n\n## Monorepo or Multi repo\nEncore is not opinionated about where your frontend lives, pick the approach that fits your application best.\n\nIf you use a monorepo then it is often a good idea to place your backend and frontend in separate folders. There are two approaches to moving your Encore backend to a subfolder:\n\n1. Place your microservices together with the `encore.app` file in a subfolder. When moving `encore.app` to a subfolder you will need to configure the \"Root Directory\" in app settings in the [Encore Cloud dashboard](https://app.encore.cloud).\n2. Place your microservices in a subfolder and keep the `encore.app` in the repo root directory. No configuration change is needed, but you will need to update the import paths if your services are calling each other.\n\n## REST vs. GraphQL\nEncore allows for building backends using both REST and GraphQL, you should pick the approach that suits your use case best.\n\nTake a look at the [GraphQL tutorial](/docs/go/tutorials/graphql) for an example of building a GraphQL backend with Encore.\n\n## Hosting a frontend on Encore for development\nEncore is primarily designed for backend development and does not (at the moment) support building or testing frontends in the deploy pipeline. For production use, we recommend that you deploy your frontend using Vercel, Netlify, or a similar service.\n\nFor development purposes, you can create a `raw` endpoint that serves static frontend assets. It would look something like the example below (taken from the [Uptime Monitoring tutorial](https://encore.dev/docs/go/tutorials/uptime)), but keep in mind that you need to have the compiled frontend assets under version control (`dist` folder in the example below).\n\n```go\npackage frontend\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n\t\"net/http\"\n)\n\nvar (\n\t//go:embed dist\n\tdist embed.FS\n\n\tassets, _ = fs.Sub(dist, \"dist\")\n\thandler   = http.StripPrefix(\"/frontend/\", http.FileServer(http.FS(assets)))\n)\n\n //encore:api public raw path=/frontend/*path\n func Serve(w http.ResponseWriter, req *http.Request) {\n\t handler.ServeHTTP(w, req)\n }\n```\n\n## Handling CORS\nIf you are running into CORS issues when calling your Encore API from your frontend you may need to specify which origins are allowed to access your API (via browsers). Do this by specifying the `global_cors` key in the `encore.app` file, which has the following structure:\n\n```json\nglobal_cors: {\n  // allow_origins_without_credentials specifies the allowed origins for requests\n  // that don't include credentials. If nil it defaults to allowing all domains\n  // (equivalent to [\"*\"]).\n  \"allow_origins_without_credentials\": [\n    \"<ORIGIN-GOES-HERE>\"\n  ],\n\n  // allow_origins_with_credentials specifies the allowed origins for requests\n  // that include credentials. If a request is made from an Origin in this list\n  // Encore responds with Access-Control-Allow-Origin: <Origin>.\n  //\n  // The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n  // or \"https://*-myapp.example.com\").\n  \"allow_origins_with_credentials\": [\n    \"<DOMAIN-GOES-HERE>\"\n  ]\n}\n```\n\nSee more in the [CORS docs](/docs/go/develop/cors).\n"
  },
  {
    "path": "docs/go/how-to/logto-auth.md",
    "content": "---\nseotitle: How to use Logto for your backend application\nseodesc: Learn how to use Logto for user authentication in your backend application. In this guide we show you how to integrate your Go backend with Logto.\ntitle: Use Logto with your app\nlang: go\n---\n\n[Logto](https://logto.io) is a modern Auth0 alternative that helps you build the sign-in experience and user identity within minutes. It's particularly well-suited for protecting API services built with Encore.\n\nThis guide will show you how to integrate Logto with your Encore application to add authentication and authorization capabilities. You can find the complete [Logto example](https://github.com/encoredev/examples/tree/main/logto-react-sdk) in our examples repo.\n\n## Logto settings\n\nBefore we begin integrating with Encore, you'll need to set up a few things in Logto:\n\n1. Create an account at [Logto Cloud](https://cloud.logto.io) if you don't have one yet.\n\n2. Create an API Resource in Logto Console, this represents your Encore API service\n   - Go to \"API Resources\" in Logto Console and create a new API\n   - Set a name and API identifier (e.g., `https://api.encoreapp.com`)\n   - Note down the API identifier on the API resource details page as we'll need it later\n  \n  <img src=\"/assets/docs/logto-api-resource.png\" title=\"Logto API Resource\"/>\n\n3. Create an application for your frontend application\n  - Go to \"Applications\" in Logto Console\n  - Create a new application according to your frontend framework (We use React as an example, but you can create any Single-Page Application (SPA) or native app)\n  - (Optional, we'll cover this later) Integrate Logto with your frontend application according to the guide in the Logto Console.\n  - Note down the application ID and issuer URL on the Application details page as we'll need them later\n\n  <img src=\"/assets/docs/logto-application-endpoints.png\" title=\"Logto application endpoints\"/>\n\n## Setup the auth handler\n\nNow let's implement the authentication in your Encore application. We'll use Encore's built-in [auth handler](/docs/go/develop/auth) to validate Logto's JWT tokens.\n\nAdd these two modules in your Encore application:\n\n```shell\n$ go get github.com/golang-jwt/jwt/v5\n$ go get github.com/MicahParks/keyfunc/v3\n```\n\nCreate `auth/auth.go` and add the following code:\n\n```go\npackage auth\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/config\"\n\t\"github.com/MicahParks/keyfunc/v3\"\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\n// Configuration variables for authentication\ntype LogtoAuthConfig struct {\n\t// The issuer URL\n\tIssuer config.String\n\t// URL to fetch JSON Web Key Set (JWKS)\n\tJwksUri config.String\n\t// Expected audience for the JWT\n\tApiResourceIndicator config.String\n\t// Expected client ID in the token claims\n\tClientId config.String\n}\n\nvar authConfig *LogtoAuthConfig = config.Load[*LogtoAuthConfig]()\n\n// RequiredClaims defines the expected structure of JWT claims\n// Extends the standard JWT claims with a custom ClientID field\ntype RequiredClaims struct {\n\tClientID string `json:\"client_id\"`\n\tjwt.RegisteredClaims\n}\n\n// AuthHandler validates JWT tokens and extracts the user ID\n// Implements Encore's authentication handler interface\n//\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, error) {\n\t// Fetch and parse the JWKS (JSON Web Key Set) from the identity provider\n\tjwks, err := keyfunc.NewDefaultCtx(ctx, []string{authConfig.JwksUri()})\n\tif err != nil {\n\t\treturn \"\", &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: \"failed to fetch JWKS\",\n\t\t}\n\t}\n\n\t// Parse and validate the JWT token with required claims and validation options\n\tparsedToken, err := jwt.ParseWithClaims(\n\t\ttoken,\n\t\t&RequiredClaims{},\n\t\tjwks.Keyfunc,\n\t\t// Expect the token to be intended for this API resource\n\t\tjwt.WithAudience(authConfig.ApiResourceIndicator()),\n\t\t// Expect the token to be issued by this issuer\n\t\tjwt.WithIssuer(authConfig.Issuer()),\n\t\t// Allow some leeway for clock skew\n\t\tjwt.WithLeeway(time.Minute*10),\n\t)\n\n\t// Check if there were any errors during token parsing\n\tif err != nil {\n\t\treturn \"\", &errs.Error{\n\t\t\tCode:    errs.Unauthenticated,\n\t\t\tMessage: \"invalid token\",\n\t\t}\n\t}\n\n\t// Verify that the client ID in the token matches the expected client ID\n\tif parsedToken.Claims.(*RequiredClaims).ClientID != authConfig.ClientId() {\n\t\treturn \"\", &errs.Error{\n\t\t\tCode:    errs.Unauthenticated,\n\t\t\tMessage: \"invalid token\",\n\t\t}\n\t}\n\n\t// Extract the user ID (subject) from the token claims\n\tuserId, err := parsedToken.Claims.GetSubject()\n\tif err != nil {\n\t\treturn \"\", &errs.Error{\n\t\t\tCode:    errs.Unauthenticated,\n\t\t\tMessage: \"invalid token\",\n\t\t}\n\t}\n\n\t// Return the user ID as an Encore auth.UID\n\treturn auth.UID(userId), nil\n}\n```\n\nCreate a [configuration file](https://encore.dev/docs/go/develop/config) in the auth service and name it `auth-config.cue`. Add the following:\n\n```cue\nIssuer: \"<your-logto-issuer-url>\"\nJwksUri: \"<your-logto-issuer-url>/jwks\"\nApiResourceIndicator: \"<your-api-resource-indicator>\"\nClientId: \"<your-client-id>\"\n```\n\nReplace the values with the ones you noted down from your Logto settings:\n- `<your-logto-issuer-url>`: The issuer URL from your Logto application endpoints (e.g., `https://your-tenant.logto.app`)\n- `<your-api-resource-indicator>`: The API identifier you set when creating the API resource (e.g., `https://api.encoreapp.com`)\n- `<your-client-id>`: The application ID from your Logto application details page\n\nFor example, your `auth-config.cue` might look like:\n\n```cue\nIssuer: \"https://your-tenant.logto.app\"\nJwksUri: \"https://your-tenant.logto.app/jwks\"\nApiResourceIndicator: \"https://api.encoreapp.com\"\nClientId: \"2gadf3mp0zotlq8j1k5x\"\n```\n\nAnd then, you can use this auth handler to protect your API endpoints:\n\n```go\npackage api\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n)\n\n//encore:api auth path=/api/hello\nfunc Api(ctx context.Context) (*Response, error) {\n\tuserId, hasUserId := auth.UserID()\n\n\tif !hasUserId {\n\t\treturn nil, &errs.Error{\n\t\t\tCode:    errs.Internal,\n\t\t\tMessage: \"User ID not found\",\n\t\t}\n\t}\n\n\tmsg := fmt.Sprintf(\"Hello, %s!\", userId)\n\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n\n```\n\n## Frontend\n\nWe've completed our work in the Encore API service. Now we need to integrate Logto with our frontend application.\n\nYou can choose the framework you are using in the [Logto Quick start](https://docs.logto.io/quick-starts) page to integrate Logto with your frontend application. In this guide we use React as an example.\n\nCheck out the [Add authentication to your React application](https://docs.logto.io/quick-starts/react) guide to learn how to integrate Logto with your React application. In this example, you only need to complete up to the Integration section. After that, we'll demonstrate how the frontend application can obtain an access token from Logto to access the Encore API.\n\nFirst, update your `LogtoConfig` by adding the API resource used in your Encore app to the `resources` field. This tells Logto that we will be requesting access tokens for this API resource (Encore API).\n\n```ts\nimport { LogtoConfig } from '@logto/react';\n\nconst config: LogtoConfig = {\n  // ...other configs\n  resources: ['<your-api-resource-indicator>'],\n};\n```\n\nAfter updating the `LogtoConfig`, if a user is already signed in, they need to sign out and sign in again for the new `LogtoConfig` settings to take effect.\n\nOnce the user is logged in, you can use the `getAccessToken` method provided by the Logto React SDK to obtain an access token for accessing specific API resources. For example, to access the Encore API, we use `https://api.encoreapp.com` as the API resource identifier.\n\nThen, add this access token to the request headers as the `Authorization` field in subsequent requests.\n\n```ts\nconst { getAccessToken } = useLogto();\nconst accessToken = await getAccessToken('<your-api-resource-indicator>');\n\n// Add this access token to the request headers as the 'Authorization' field in subsequent requests\nfetch('<your-encore-api-endpoint>/hello', {\n  headers: {\n    Authorization: `Bearer ${accessToken}`,\n  },\n});\n```\n\nHere's the key frontend code:\n\n```tsx\n-- config/logto.tsx --\nimport { LogtoConfig } from '@logto/react'\n\nexport const config: LogtoConfig = {\n  endpoint: '<your-logto-endpoint>',\n  appId: '<your-app-id>',\n  resources: ['<your-api-resource-indicator>'],\n}\n\nexport const appConfig = {\n  apiResourceIndicator: '<your-api-resource-indicator>',\n  signInRedirectUri: '<your-sign-in-redirect-uri>',\n  signOutRedirectUri: '<your-sign-out-redirect-uri>',\n}\n\nexport const encoreApiEndpoint = '<your-encore-api-endpoint>'\n-- pages/ProtectedResource.tsx --\nimport { useLogto } from \"@logto/react\";\nimport { useState } from \"react\";\nimport { Navigate } from \"react-router-dom\";\nimport { appConfig, encoreApiEndpoint } from \"../config/logto\";\n\nexport function ProtectedResource() {\n  const { isAuthenticated, getAccessToken } = useLogto();\n  const [message, setMessage] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState(\"\");\n\n  const fetchProtectedResource = async () => {\n    setIsLoading(true);\n    setError(\"\");\n    try {\n      const accessToken = await getAccessToken(appConfig.apiResourceIndicator);\n      const response = await fetch(`${encoreApiEndpoint}/api/hello`, {\n        headers: {\n          Authorization: `Bearer ${accessToken}`,\n        },\n      });\n      if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`);\n      }\n      const data = await response.json();\n      setMessage(JSON.stringify(data));\n    } catch (error) {\n      console.error(\"Error fetching protected resource:\", error);\n      setError(\"Failed to fetch protected resource. Please try again.\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  if (!isAuthenticated) {\n    return <Navigate to=\"/\" replace />;\n  }\n\n  return (\n    <div>\n      <h2>Protected Resource</h2>\n\n      {message && !error && (\n        <div>\n          <h3>Response from Protected API</h3>\n          <pre>{message}</pre>\n        </div>\n      )}\n\n      <button\n        onClick={fetchProtectedResource}\n        disabled={isLoading}\n      >\n        {isLoading ? \"Loading...\" : \"Fetch protected resource\"}\n      </button>\n\n      {error && <div>{error}</div>}\n    </div>\n  );\n}\n```\n\nThat's it, you've successfully integrated Logto with your Encore application.\n\nYou can find the complete example code [here](https://github.com/encoredev/examples/tree/main/logto-react-sdk).\n\n## Explore more\n\nIf you want to use more Logto features, you can refer to the following links for more information:\n\n- Combine Logto's [Custom token claims](https://docs.logto.io/developers/custom-token-claims) to set [custom user data](/docs/go/develop/auth#with-custom-user-data) in the auth handler\n- Use [Logto RBAC features](https://docs.logto.io/authorization/role-based-access-control) to add authorization support to your application. The React integration tutorial also demonstrates how to add `scope` information to your Access token (note that you need to sign in again after updating Logto config)\n"
  },
  {
    "path": "docs/go/how-to/pubsub-outbox.md",
    "content": "---\nseotitle: Using a transactional Pub/Sub outbox\nseodesc: Learn how you can use a transactional outbox with Pub/Sub to guarantee consistency between your database and Pub/Sub subscribers\ntitle: Transactional Pub/Sub outbox\nsubtitle: Guarantee consistency between your database and Pub/Sub subscribers\nlang: go\n---\n\nOne of the hardest parts of building an event-driven application is ensuring consistency between services.\nA common pattern is for each service to have its own database and use Pub/Sub to notify other systems of business events.\nInevitably this leads to inconsistencies since the Pub/Sub publishing is not transactional with the database writes.\n\nWhile there are several approaches to solving this, it's important the solution doesn't add too much complexity\nto what is often an already complex architecture. Perhaps the best solution in this regard is the [transactional outbox pattern](https://softwaremill.com/microservices-101/).\n\nEncore provides support for the transactional outbox pattern in the [x.encore.dev/infra/pubsub/outbox](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox) package.\n\nThe transactional outbox works by binding a Pub/Sub topic to a database transaction, translating all calls to `topic.Publish`\ninto inserting a database row in an `outbox` table. If/when the transaction later commits, the messages are picked up by\na [Relay](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#Relay) that polls the `outbox` table and publishes the\nmessages to the actual Pub/Sub topic.\n\n## Publishing messages to the outbox\n\nTo publish messages to the outbox, a topic must first be bound to the outbox. This is done using\n[Pub/Sub topic references](/docs/go/primitives/pubsub#using-topic-references) which allows you to retain complete\ntype safety and the same interface as regular Pub/Sub topics, allowing existing code to continue to work without changes.\n\n<Callout type=\"info\">\n\nIn regular (non-outbox) usage the message id returned by `topic.Publish` is the same as the message id the subscriber\nreceives when processing the message. With the outbox, this message id is not available until the transaction commits,\nso `topic.Publish` returns an id referencing the outbox row instead.\n\n</Callout>\n\n\nThe topic binding supports pluggable storage backends, enabling use of the outbox pattern with any\ntransactional storage backend. Implementation are provided out-of-the-box for use with Encore's\n`encore.dev/storage/sqldb` package, as well as the standard library `database/sql` and `github.com/jackc/pgx/v5` drivers,\nbut it's easy to write your own for other use cases.\nSee the [Go package reference](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#PersistFunc) for more information.\n\nFor example, to use a transactional outbox to notify subscribers when a user is created:\n\n```go\n-- outbox.go --\n// Create a SignupsTopic somehow.\nvar SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */)\n\n// Create a topic ref with publisher permissions.\nref := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic)\n\n// Bind it to the transactional outbox\nimport \"x.encore.dev/infra/pubsub/outbox\"\nvar tx *sqldb.Tx // somehow get a transaction\nref = outbox.Bind(ref, outbox.TxPersister(tx))\n\n// Calls to ref.Publish() will now insert a row in the outbox table.\n\n-- db_migration.sql --\n-- The database used must contain the below database table:\n-- See https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#SQLDBStore\nCREATE TABLE outbox (\n    id BIGSERIAL PRIMARY KEY,\n    topic TEXT NOT NULL,\n    data JSONB NOT NULL,\n    inserted_at TIMESTAMPTZ NOT NULL\n);\nCREATE INDEX outbox_topic_idx ON outbox (topic, id);\n```\n\nOnce the transaction commits any published messages via `ref` above will be stored in the `outbox` table.\n\n## Consuming messages from the outbox\n\nOnce committed, the messages are ready to be picked up and published to the actual Pub/Sub topic.\n\nThat is done via the [Relay](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#Relay).\nThe relay continuously polls the `outbox` table and publishes any new messages to the actual Pub/Sub topic.\n\nThe relay supports pluggable storage backends, enabling use of the outbox pattern with any\ntransactional storage backend. An implementation is provided out-of-the-box that uses Encore's built-in\n[SQL database support](https://pkg.go.dev/x.encore.dev/infra/pubsub/outbox#SQLDBStore),\nbut it's easy to write your own for other databases.\n\nThe topics to poll must be registered with the relay, typically during service initialization. For example:\n\n```go\n-- user/service.go --\npackage user\n\nimport (\n\t\"context\"\n\t\n    \"encore.dev/pubsub\"\n    \"encore.dev/storage/sqldb\"\n    \"x.encore.dev/infra/pubsub/outbox\"\n)\n\ntype Service struct {\n\tsignupsRef pubsub.Publisher[*SignupEvent]\n}\n\n// db is the database the outbox table is stored in\nvar db = sqldb.NewDatabase(...)\n\n// Create the SignupsTopic somehow.\nvar SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */)\n\nfunc initService() (*Service, error) {\n    // Initialize the relay to poll from our database.\n\trelay := outbox.NewRelay(outbox.SQLDBStore(db))\n\t\n\t// Register the SignupsTopic to be polled.\n    signupsRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic)\n\toutbox.RegisterTopic(relay, signupsRef)\n\t\n\t// Start polling.\n\tgo relay.PollForMessage(context.Background(), -1)\n\t\n\treturn &Service{signupsRef: signupsRef}, nil\n}\n```\n"
  },
  {
    "path": "docs/go/how-to/temporal.md",
    "content": "---\nseotitle: How to use Temporal and Encore\nseodesc: Learn how to use Temporal for reliable workflow execution with Encore.\ntitle: Use Temporal with Encore\nlang: go\n---\n\n[Temporal](https://temporal.io) is a workflow orchestration system for building highly reliable systems.\nEncore works great with Temporal, and this guide shows you how to integrate Temporal into your Encore application.\n\n## Set up Temporal clusters\nYou'll need at least two Temporal clusters: one for local development and one for cloud environments.\n\nWe recommend using [Temporalite](https://github.com/temporalio/temporalite) for local development,\nand [Temporal Cloud](https://temporal.io/cloud) for cloud environments. \n\n## Set up Temporal Workflow\n\nNext it's time to create a Temporal Workflow. We'll base this on the Temporal [Hello World](https://learn.temporal.io/getting_started/go/hello_world_in_go/)\nexample.\n\nCreate a new Encore service named `greeting`:\n\n```go\n-- greeting/greeting.go --\npackage greeting\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"go.temporal.io/sdk/client\"\n\t\"go.temporal.io/sdk/worker\"\n\t\"encore.dev\"\n)\n\n// Use an environment-specific task queue so we can use the same\n// Temporal Cluster for all cloud environments.\nvar (\n    envName = encore.Meta().Environment.Name\n    greetingTaskQueue = envName + \"-greeting\"\n)\n\n//encore:service\ntype Service struct {\n\tclient client.Client\n\tworker worker.Worker\n}\n\nfunc initService() (*Service, error) {\n\tc, err := client.Dial(client.Options{})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create temporal client: %v\", err)\n\t}\n\n\tw := worker.New(c, greetingTaskQueue, worker.Options{})\n\n\terr = w.Start()\n\tif err != nil {\n\t\tc.Close()\n\t\treturn nil, fmt.Errorf(\"start temporal worker: %v\", err)\n\t}\n\treturn &Service{client: c, worker: w}, nil\n}\n\nfunc (s *Service) Shutdown(force context.Context) {\n\ts.client.Close()\n\ts.worker.Stop()\n}\n```\n\nNext it's time to define some workflows. These need to be in the same service,\nso add a new `workflow` package inside the `greeting` service, containing\na workflow and activity definition in separate files:\n\n```go\n-- greeting/workflow/workflow.go --\npackage workflow\n\nimport (\n\t\"time\"\n\n\t\"go.temporal.io/sdk/workflow\"\n)\n\nfunc Greeting(ctx workflow.Context, name string) (string, error) {\n    options := workflow.ActivityOptions{\n        StartToCloseTimeout: time.Second * 5,\n    }\n\n    ctx = workflow.WithActivityOptions(ctx, options)\n\n    var result string\n    err := workflow.ExecuteActivity(ctx, ComposeGreeting, name).Get(ctx, &result)\n\n    return result, err\n}\n-- greeting/workflow/activity.go --\npackage workflow\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\nfunc ComposeGreeting(ctx context.Context, name string) (string, error) {\n    greeting := fmt.Sprintf(\"Hello %s!\", name)\n    return greeting, nil\n}\n```\n\nThen, go back to the `greeting` service and register the workflow and activity:\n\n```go\n-- greeting/greeting.go --\n// Import the package at the top:\nimport \"encore.app/greeting/workflow\"\n\n// Add these lines to `initService`, below the call to `worker.New`:\nw.RegisterWorkflow(workflow.Greeting)\nw.RegisterActivity(workflow.ComposeGreeting)\n```\n\nNow let's create an Encore API that triggers this workflow.\n\nAdd a new file `greeting/greet.go`:\n\n```go\n-- greeting/greet.go --\npackage greeting\n\nimport (\n\t\"context\"\n\n\t\"encore.app/greeting/workflow\"\n\t\"encore.dev/rlog\"\n\t\"go.temporal.io/sdk/client\"\n)\n\ntype GreetResponse struct {\n    Greeting string\n}\n\n//encore:api public path=/greet/:name\nfunc (s *Service) Greet(ctx context.Context, name string) (*GreetResponse, error) {\n    options := client.StartWorkflowOptions{\n        ID:        \"greeting-workflow\",\n        TaskQueue: greetingTaskQueue,\n    }\n    we, err := s.client.ExecuteWorkflow(ctx, options, workflow.Greeting, name)\n    if err != nil {\n        return nil, err\n    }\n    rlog.Info(\"started workflow\", \"id\", we.GetID(), \"run_id\", we.GetRunID())\n\n    // Get the results\n    var greeting string\n    err = we.Get(ctx, &greeting)\n    if err != nil {\n        return nil, err\n    }\n    return &GreetResponse{Greeting: greeting}, nil\n}\n```\n\n## Run it locally\n\nNow we're ready to test it out. Start up `temporalite` and your Encore application (in separate terminals):\n\n```bash\n$ temporalite start --namespace default\n$ encore run\n```\n\nNow try calling it, either from the [Local Development Dashboard](/docs/go/observability/dev-dash) or using cURL:\n\n```bash\n$ curl 'http://localhost:4000/greeting/Temporal'\n{\"Greeting\": \"Hello Temporal!\"}\n```\n\nIf you see this, it works!\n\n## Run in the cloud\n\nTo run it in the cloud, you will need to use Temporal Cloud or your own, self-hosted Temporal cluster.\nThe easiest way to automatically pick up the correct cluster address is to use Encore's [config functionality](/docs/go/develop/config).\n\nAdd two new files:\n```\n-- greeting/config.go --\npackage greeting\n\nimport \"encore.dev/config\"\n\ntype Config struct {\n    TemporalServer string\n}\n\nvar cfg = config.Load[*Config]()\n-- greeting/config.cue --\npackage greeting\n\nTemporalServer: [\n\t// These act as individual case statements\n    if #Meta.Environment.Cloud == \"local\" { \"localhost:7233\" },\n\n    // TODO: configure this to match your own cluster address\n    \"my.cluster.address:7233\",\n][0] // Return the first value which matches the condition\n```\n\nFinally go back to `greeting/greeting.go` and update the `client.Dial` call to look like:\n\n```go\n-- greeting/greeting.go --\nclient.Dial(client.Options{HostPort: cfg.TemporalServer})\n```\n\nWith that, Encore will automatically connect to the correct Temporal cluster, using a local cluster\nfor local development and your cloud-hosted cluster for everything else.\n"
  },
  {
    "path": "docs/go/install.md",
    "content": "---\nseotitle: Install Encore to start building\nseodesc: See how you can install Encore on all platforms, and get started building your next backend application in minutes.\ntitle: Installation\nsubtitle: Install the Encore CLI to get started with local development\nlang: go\n---\n\nIf you are new to Encore, we recommend following the [quick start guide](/docs/go/quick-start).\n\n## Install the Encore CLI\nTo develop locally with Encore, you first need to install the Encore CLI.\nThis is what provisions your local development environment, and runs your Local Development Dashboard complete with logs, tracing, and API documentation.\n\n\n<InstallInstructions />\n\n<Callout type=\"info\">\n\nTo locally run Encore apps with databases, you also need to have [Docker](https://www.docker.com) installed and running.\n\n</Callout>\n\n### Optional: Add AI/LLM instructions\n\nTo help AI coding assistants (Cursor, Claude Code, GitHub Copilot, etc.) understand how to use Encore, run this from your app directory:\n\n```bash\nencore llm-rules init\n```\n\nThis prompts you to select your tool and generates the appropriate config (e.g. `.cursorrules`, `CLAUDE.md`) and MCP setup where supported. For full details and other options, see [AI Tools Integration](/docs/go/ai-integration).\n\n### Build from source\nIf you prefer to build from source, [follow these instructions](https://github.com/encoredev/encore/blob/main/CONTRIBUTING.md).\n\n\n## Update to the latest version\nCheck which version of Encore you have installed by running `encore version` in your terminal.\nIt should print something like:\n```shell\nencore version v1.28.0\n```\n\nIf you think you're on an older version of Encore, you can easily update to the latest version by running\n`encore version update` from your terminal.\n"
  },
  {
    "path": "docs/go/migration/ai-migration.mdx",
    "content": "---\nseotitle: Migrate to Encore.go Using an AI Agent\nseodesc: Learn how to use Encore's AI migration skill to automatically migrate your existing backend to Encore.go, with validation at every step.\ntitle: Migrate using AI agent\nlang: go\n---\n\nEncore's AI migration skill analyzes your existing backend, builds a dependency-aware migration plan, and converts your code to Encore.go — one unit at a time, with validation at every step.\n\nIt works with any source framework: Gin, Echo, Chi, Fiber, net/http, Django, Rails, and more.\n\n<Callout type=\"info\">\n\nThe skill has been tested with Claude Code but should work with other agents as well.\n\n</Callout>\n\n## Prerequisites\n\nInstall the Encore skills package in your AI coding tool:\n\n```bash\nnpx add-skill encoredev/skills\n```\n\nYou'll also need:\n- The source codebase accessible on your local machine\n- An Encore project to migrate into (the skill can help create one)\n- Your source application running locally (optional — enables HTTP comparison validation)\n\n## Starting a migration\n\nCreate a new Encore app from the \"Empty app\" template by running:\n\n```bash\nencore app create\n```\n\nFrom inside your Encore app, open your AI coding tool and ask it to migrate your existing app:\n\n```\nMigrate ../path/to/existing/project to Encore.ts by using the encore-migrate skill\n```\n\nThe skill walks you through four phases: **Discover**, **Plan**, **Migrate**, and **Complete**.\n\n## How it works\n\n### Phase 1 — Discover\n\nThe AI reads your source codebase and inventories everything: API endpoints, databases, Pub/Sub topics, cron jobs, auth middleware, secrets, and tests. It groups related entities into **migration units** — typically aligned with your existing service boundaries or URL path prefixes — and presents a summary for you to review.\n\nYou can adjust the groupings before moving on. Split units that are too large, merge ones that are too small, or rename them to match your domain.\n\n### Phase 2 — Plan\n\nThe AI creates a `migration-plan.md` file and a `migration-plan/` directory in your Encore project. The summary file tracks overall progress and dependency order. Each migration unit gets its own detail file listing every endpoint, database table, and test to migrate.\n\nDependencies determine the order. Secrets and config go first, then databases, auth, leaf services, dependent services, Pub/Sub, and finally cron jobs.\n\n### Phase 3 — Migrate\n\nThe AI works through one migration unit at a time. For each entity it:\n\n1. **Implements** the Encore equivalent — [API endpoints](/docs/go/primitives/defining-apis), [database schemas](/docs/go/primitives/databases), [infrastructure declarations](/docs/go/primitives/services)\n2. **Migrates tests** from the source framework to Encore's [testing patterns](/docs/go/develop/testing)\n3. **Validates** the result using up to three layers (see [Validation](#validation))\n4. **Updates the plan** files to track progress\n\nAfter completing a unit, it suggests the next one based on the dependency order. You can also pick a different unit or tell it to keep going through multiple units.\n\n### Phase 4 — Complete\n\nWhen all units are done, the AI presents a final summary: what was migrated, what was skipped, and what needs manual attention. It suggests a final test suite run and, if your source system has a frontend, recommends reconnecting it to the new Encore backend using the [Client Generation](/docs/go/cli/client-generation) feature.\n\n## Full-stack and monorepo support\n\nWhen the source codebase contains frontend code (React, Vue, Angular, Next.js, etc.), the AI identifies it and marks it as out of scope — only backend code is migrated.\n\nFor full-stack frameworks like **Next.js**, **Remix**, **Nuxt**, **SvelteKit**, and **Astro**, the AI detects server-side routes (e.g., Next.js `pages/api/` or Remix `loader` functions) and asks what you want to do with them:\n\n1. **Migrate all** server-side routes to Encore\n2. **Migrate some** — you pick which ones move\n3. **Keep all in the frontend framework** — only migrate standalone backend code\n\nThis is useful when you want an Encore backend but prefer to keep a thin BFF or SSR data-fetching layer in your frontend framework.\n\n## Validation\n\nEvery entity is validated before it's marked as migrated. The AI uses three layers:\n\n**Test migration** — Source tests are converted to Encore's [testing patterns](/docs/go/develop/testing) and run. They must pass before the entity is marked as done.\n\n**HTTP comparison** — When both systems are running locally, the AI calls the same endpoint on both and compares the HTTP status code and response body structure. This layer is skipped for endpoints with side effects or that require auth credentials the AI can't obtain.\n\n**Verification gate** — No entity is marked as `migrated` without concrete evidence from the current session: test output, HTTP comparison results, or your explicit approval to skip.\n\n## Resuming across sessions\n\nThe migration plan is persisted to files in your Encore project, so you can close your editor and come back later. When you resume, the AI reads `migration-plan.md`, reports the current status, and suggests the next unit to work on.\n\n```\nResume the migration\n```\n\n```\nWhat's left to migrate?\n```\n"
  },
  {
    "path": "docs/go/migration/migrate-away.md",
    "content": "---\ntitle: Migrate away from Encore\nsubtitle: If you love someone, set them free.\nlang: go\n---\n\n_We realize most people read this page before even trying Encore, so we start with a perspective on how you might reason about adopting Encore. Read on to see what tools are available for migrating away._\n\nPicking technologies for your project is an important decision. It's tricky because you don't know what the requirements are going to look like in the future. This uncertainty makes many teams opt for maximum flexibility, often without acknowledging this has a significant negative effect on productivity.\n\nWhen designing Encore, we've leaned on standardization to provide a well-integrated and highly productive development workflow. The design is based on the core team's experience building scalable distributed systems at Spotify and Google, complemented with loads of invaluable input from the developer community. \n\nIn practise Encore is opinionated only in certain areas which are critical for enabling the static analysis used to create Encore's application model. This is fundamental to how Encore can provide its powerful features, like automatically instrumenting distributed tracing, and provisioning and managing cloud infrastructure.\n\n## Accommodating for your unique requirements\n\nMany software projects end up having a few novel requirements, which are highly specific to the problem domain. To accommodate for this, Encore is designed to let you go outside of the standardized Backend Framework when you need to, for example:\n- You can drop down in abstraction level in the API framework using [raw endpoints](/docs/go/primitives/raw-endpoints).\n- You can use tools like the [Terraform provider](/docs/platform/integrations/terraform) to integrate infrastructure that is not managed by Encore\n\n## Mitigating risk through Open Source and efficiency\n\nWe believe that adopting Encore is a low-risk decision for several reasons:\n\n- There's no upfront investment needed to get the benefits\n- Encore apps are normal programs where less than 1% of the code is Encore-specific\n- All infrastructure and data is in your own cloud\n- It's simple to integrate with cloud services and systems not natively supported by Encore\n- Everything you need to develop your application is Open Source, including the [parser](https://github.com/encoredev/encore/tree/main/v2/parser), [compiler](https://github.com/encoredev/encore/tree/main/v2/compiler), [runtime](https://github.com/encoredev/encore/tree/main/runtimes)\n- Everything you need to self-host your application is [Open Source and documented](/docs/go/self-host/docker-build)\n\n## What to expect when migrating away\n\nIf you want to migrate away, we want to ensure this is as smooth as possible! Here are some of the ways Encore is designed to keep your app portable, with minimized lock-in, and the tools provided to aid in migrating away.\n\n### Code changes\n\nBuilding with Encore doesn't require writing your entire application in an Encore-specific way. Encore applications are normal programs where only 1% of the code is specific to Encore's Open Source Backend Framework.\n\nThis means that the changes required to stop using the Backend Framework is almost exactly the same work you would have needed to do if you hadn't used Encore in the first place, e.g. writing infrastructure boilerplate. There is no added migration cost.\n\n### Deployment\n\nIf you are self-hosting your application, then you're already done.\n\nIf you are using Encore Cloud Platform to manage deployments and want to migrate to your own solution, you can use the `encore build docker` command to produce a Docker image, containing the compiled application, using exactly the same code path as Encore's CI system to ensure compatibility.\n\nLearn more in the [self-hosting docs](/docs/go/self-host/docker-build).\n\n### Tell us what you need\n\nWe're engineers ourselves and we understand the importance of not being constrained by a single technology.\n\nWe're working every single day on making it even easier to start, <i>and stop</i>, using Encore.\nIf you have specific concerns, questions, or requirements, we'd love to hear from you!\n\nPlease reach out on [Discord](https://encore.dev/discord) or [send an email](mailto:hello@encore.dev) with your thoughts.\n"
  },
  {
    "path": "docs/go/observability/dev-dash.md",
    "content": "---\nseotitle: Development dashboard for local development\nseodesc: Encore's Local Development Dashboard comes with build-in distributed tracing, API docs, and real-time architecture diagrams.\ntitle: Local Development Dashboard\nsubtitle: Built-in tools for simplicity and productivity\nlang: go\n---\n\nEncore provides an efficient local development workflow that automatically provisions [local infrastructure](/docs/platform/infrastructure/infra#local-development) and supports automated testing with dedicated test infrastructure.\n\nThe local environment also comes with a built-in Local Development Dashboard to simplify development and improve productivity. It has several features to help you design, develop, and debug your application:\n\n* [Service Catalog](/docs/go/observability/service-catalog) with Automatic API Documentation\n* API Explorer to call your APIs\n* [Distributed Tracing](/docs/go/observability/tracing) for simple and powerful debugging\n* [Encore Flow](/docs/go/observability/encore-flow) for visualizing your microservices architecture\n\nAll these features update in real-time as you make changes to your application.\n\nTo access the dashboard, start your Encore application with `encore run` and it will open automatically. You can also follow the link in your terminal:\n\n```bash\n$ encore run\nAPI Base URL:      http://localhost:4000\nDev Dashboard URL: http://localhost:9400/hello-world-cgu2\n```\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/localdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/go/observability/encore-flow.md",
    "content": "---\nseotitle: Encore Flow automatic microservices architecture diagrams\nseodesc: Visualize your microservices architecture automatically using Encore Flow. Get real-time interactive architecture diagrams for your entire application.\ntitle: Flow Architecture Diagram\nsubtitle: Visualize your cloud microservices architecture\nlang: go\n---\n\nFlow is a visual tool that gives you an always up-to-date view of your entire system, helping you reason about your\nmicroservices architecture and identify which services depend on each other and how they work together.\n\n## Birds-eye view\n\nHaving access to a zoomed out representation of your system can be invaluable in pretty much all parts of the\ndevelopment cycle. Flow helps you:\n\n* Track down bottlenecks before they grow into big problems.\n* Get new team members onboarded much faster.\n* Pinpoint hot paths in your system, services that might need extra attention.\n\nServices and PubSub topics are represented as boxes, arrows indicate a dependency. In the example below\nthe `login` service has dependencies on the `user` and `authentication` services. Dashed arrows shows publications or\nsubscriptions to a topic. Here, `payment` publishes to the `payment-made` topic and `email` subscribe to it:\n\n<img src=\"/assets/docs/flow-diagram.png\" title=\"Encore Flow - Highlight Dependencies\" />\n\n## Highlight dependencies\n\nHover over a service, or PubSub topic, to instantly reveal the nature and scale of its dependencies.\n\nHere the `login` service and its dependencies are highlighted. We can see that `login` makes queries to the\ndatabase and requests to two of the endpoints from the `user` service as well as requests to one endpoint from\nthe `authentication` service:\n\n<img src=\"/assets/docs/flow-highlight.png\" title=\"Encore Flow - Highlight Dependencies\" />\n\n## Real-time updates\n\nFlow is accessible in the [Local Development Dashboard](/docs/go/observability/dev-dash) and, when using Encore Cloud, in the [Encore Cloud dashboard](https://app.encore.cloud) for cloud environments.\n\nWhen developing locally, Flow will auto update in real-time to reflect your architecture as you\nmake code changes. This helps you be mindful of important dependencies and makes it clear if you introduce new ones.\n\nFor cloud environments, Flow auto-updates with each deploy.\n\nIn the example below a new subscription on the topic `payment-made` is introduced and then removed in `user` service:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/flow-auto-update.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/go/observability/logging.md",
    "content": "---\nseotitle: Use structured logging to understand your application\nseodesc: Learn how to use structured logging, a combination of free-form log messages and type-safe key-value pairs, to understand your backend application's behavior.\ntitle: Logging\nsubtitle: Structured logging helps you understand your application\nlang: go\ninfobox: {\n  title: \"Structured Logging\",\n  import: \"encore.dev/rlog\",\n}\n---\n\nEncore offers built-in support for Structured Logging, which combines a free-form log message with structured and type-safe key-value pairs. This enables straightforward analysis of what your application is doing, in a way that is easy for a computer to parse, analyze, and index. This makes it simple to quickly filter and search through logs.\n\nEncore’s logging is integrated with the built-in [Distributed Tracing](/docs/go/observability/tracing) functionality, and all logs are automatically included in the active trace. This dramatically simplifies debugging of your application.\n\n## Usage\nFirst, import `encore.dev/rlog` in your package. Then simply call one of the package methods `Info`, `Error`, or `Debug`. For example:\n\n```go\nrlog.Info(\"log message\",\n\t\"user_id\", 12345,\n\t\"is_subscriber\", true)\nrlog.Error(\"something went terribly wrong!\",\n\t\"err\", err)\n```\n\nThe first parameter is the log message. After that follows zero or more key-value pairs for structured logging for context.\n\nIf you’re logging many log messages with the same key-value pairs each time it can be a bit cumbersome. To help with that, use `rlog.With()` to group them into a context object, which then copies the key-value pairs into each log event:\n\n```go\nctx := rlog.With(\"is_subscriber\", true)\nctx.Info(\"user logged in\", \"login_method\", \"oauth\") // includes is_subscriber=true\n```\n\nFor more information, see the [API Documentation](https://pkg.go.dev/encore.dev/rlog).\n\n## Live-streaming logs\n\nEncore also makes it simple to live-stream logs directly to your terminal, from any environment, by running:\n\n```\n$ encore logs --env=prod\n```"
  },
  {
    "path": "docs/go/observability/metrics.md",
    "content": "---\nseotitle: Custom metrics in Go\nseodesc: Learn how to define and use custom metrics in your Go backend application with Encore.\ntitle: Metrics\nsubtitle: Track custom metrics in your Go application\ninfobox: {\n  title: \"Metrics\",\n  import: \"encore.dev/metrics\",\n}\nlang: go\n---\n\nEncore provides built-in support for defining custom metrics in your Go applications. Once defined, metrics are automatically collected and displayed in the Encore Cloud Dashboard, and can be exported to third-party observability services.\n\nSee the [Platform metrics documentation](/docs/platform/observability/metrics) for information about integrations with third-party services like Grafana Cloud and Datadog.\n\n## Defining custom metrics\n\nDefine custom metrics by importing the [`encore.dev/metrics`](https://pkg.go.dev/encore.dev/metrics) package and\ncreating a new metric using one of the `metrics.NewCounter` or `metrics.NewGauge` functions.\n\nFor example, to count the number of orders processed:\n\n```go\nimport \"encore.dev/metrics\"\n\nvar OrdersProcessed = metrics.NewCounter[uint64](\"orders_processed\", metrics.CounterConfig{})\n\nfunc process(order *Order) {\n    // ...\n    OrdersProcessed.Increment()\n}\n```\n\n## Metric types\n\nEncore currently supports two metric types: counters and gauges.\n\n**Counters** measure the count of something. A counter's value must always increase, never decrease. (Note that the value gets reset to 0 when the application restarts.) Typical use cases include counting the number of requests, the amount of data processed, and so on.\n\n**Gauges** measure the current value of something. Unlike counters, a gauge's value can fluctuate up and down. Typical use cases include measuring CPU usage, the number of active instances running of a process, and so on.\n\nFor information about their respective APIs, see the API documentation for [Counter](https://pkg.go.dev/encore.dev/metrics#Counter) and [Gauge](https://pkg.go.dev/encore.dev/metrics#Gauge).\n\n### Counter example\n\n```go\nimport \"encore.dev/metrics\"\n\nvar RequestsReceived = metrics.NewCounter[uint64](\"requests_received\", metrics.CounterConfig{})\n\nfunc handleRequest() {\n    RequestsReceived.Increment()\n    // ... handle request\n}\n```\n\n### Gauge example\n\n```go\nimport \"encore.dev/metrics\"\n\nvar ActiveConnections = metrics.NewGauge[int64](\"active_connections\", metrics.GaugeConfig{})\n\nfunc onConnect() {\n    ActiveConnections.Add(1)\n}\n\nfunc onDisconnect() {\n    ActiveConnections.Add(-1)\n}\n```\n\n## Defining labels\n\nEncore's metrics package provides a type-safe way of attaching labels to metrics. To define labels, create a struct type representing the labels and then use `metrics.NewCounterGroup` or `metrics.NewGaugeGroup`.\n\nThe Labels type must be a named struct, where each field corresponds to a single label. Each field must be of type `string`, `int`, or `bool`.\n\n### Counter with labels\n\n```go\nimport \"encore.dev/metrics\"\n\ntype Labels struct {\n    Success bool\n}\n\nvar OrdersProcessed = metrics.NewCounterGroup[Labels, uint64](\"orders_processed\", metrics.CounterConfig{})\n\nfunc process(order *Order) {\n    var success bool\n    // ... populate success with true/false ...\n    OrdersProcessed.With(Labels{Success: success}).Increment()\n}\n```\n\n### Gauge with labels\n\n```go\nimport \"encore.dev/metrics\"\n\ntype ConnectionLabels struct {\n    Region string\n}\n\nvar ActiveConnections = metrics.NewGaugeGroup[ConnectionLabels, int64](\"active_connections\", metrics.GaugeConfig{})\n\nfunc onConnect(region string) {\n    ActiveConnections.With(ConnectionLabels{Region: region}).Add(1)\n}\n```\n\n<Callout type=\"important\">\n\nEach combination of label values creates a unique time series tracked in memory and stored by the monitoring system.\nUsing numerous labels can lead to a combinatorial explosion, causing high cloud expenses and degraded performance.\n\nAs a general rule, limit the unique time series to tens or hundreds at most, rather than thousands.\n\n</Callout>\n"
  },
  {
    "path": "docs/go/observability/service-catalog.md",
    "content": "---\nseotitle: Service Catalog & Generated API Docs\nseodesc: See how Encore automatically generates API documentation that always stays up to date and in sync.\ntitle: Service Catalog\nsubtitle: Automatically get a Service Catalog and complete API docs\nlang: go\n---\n\nAll developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date.\n\nTo solve this, Encore uses the [Encore Application Model](/docs/go/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve.\n\nThe API docs are available both in your [Local Development Dashboard](/docs/go/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud).\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/servicecatalogvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/go/observability/tracing.md",
    "content": "---\nseotitle: Distributed Tracing helps you understand your app\nseodesc: See how to use distributed tracing in your backend application, across multiple services, using Encore.\ntitle: Distributed Tracing\nsubtitle: Track requests across your application and infrastructure\nlang: go\n---\n\nDistributed systems often have many moving parts, making it difficult to understand what your code is doing and finding the root-cause to bugs. That’s where Tracing comes in. If you haven’t seen it before, it may just about change your life.\n\nTracing is a revolutionary way to gain insight into what your applications are doing. It works by capturing the series of events as they occur during the execution of your code (a “trace”). This works by propagating a trace id between all individual systems, then correlating and joining the information together to present a unified picture of what happened end-to-end.\n\nAs opposed to the labor intensive instrumentation you'd normally need to go through to use tracing, Encore automatically captures traces for your entire application – in all environments. Uniquely, this means you can use tracing even for local development to help debugging and speed up iterations.\n\nYou view traces in the [Local Development Dashboard](/docs/go/observability/dev-dash) and, when using Encore Cloud, you can also see traces in the [Encore Cloud dashboard](https://app.encore.cloud) for Production and other environments.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/tracingvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n\n## Encore's tracing is more comprehensive and more performant than all other tools\n\nUnlike other tracing solutions, Encore understands what each trace event is and captures unique insights about each one. This means you get access to more information than ever before:\n\n* Stack traces\n* Structured logging\n* HTTP requests\n* Network connection information\n* API calls\n* Database queries\n* etc.\n\n## Redacting sensitive data\n\nEncore's tracing automatically captures request and response payloads to simplify debugging.\n\nFor cases where this is undesirable, such as for passwords or personally identifiable information (PII), Encore supports redacting fields marked as containing sensitive data.\n\nSee the documentation on [API Schemas](/docs/go/primitives/defining-apis#sensitive-data) for more information.\n"
  },
  {
    "path": "docs/go/overview.md",
    "content": "---\nseotitle: Encore.go Introduction\nseodesc: Learn how Encore's Go Backend Framework works, and get to know the powerful features that help you build cloud backend applications faster.\ntitle: Encore.go\nsubtitle: Use Encore.go to build robust backend applications and distributed systems\ntoc: false\nlang: go\n---\n\n<div className=\"min-h-72 bg-blue p-8 relative overflow-hidden not-prose\">\n    <img className=\"absolute left-[55%] -mt-8 top-0 right-0 bottom-0 noshadow\" src=\"/assets/img/dithered-clouds.png\" />\n    <div className=\"w-[75%] lg:w-[75%]\">\n        <h2 className=\"text-white lead-medium\">Quick Start Guide</h2>\n        <div className=\"body-small text-white mt-2\">\n            Build your first Encore.go application in minutes\n        </div>\n        <a href=\"/docs/go/quick-start\">\n            <Button className=\"mt-4\" kind=\"primary\" section=\"black\">Get started</Button>\n        </a>\n    </div>\n</div>\n\nEncore.go is an open source backend framework for building distributed system. It provides a declarative approach to working with essential backend primitives like APIs, microservices, databases, queues, caches, cron jobs, and storage buckets.\n\nThe framework comes with a lot of built-in tooling for a productive end-to-end developer experience:\n\n- **Local Environment Management**: Encore automatically sets up and runs your local development environment and all local infrastructure.\n- **Enhanced Observability**: Encore comes with tools like a [Local Development Dashboard](/docs/go/observability/dev-dash), [tracing](/docs/go/observability/tracing), and a database explorer for monitoring application behavior.\n- **Automatic Documentation**: Generates and maintains [up-to-date documentation](/docs/go/observability/service-catalog) for APIs and services, and created [architecture diagrams](/docs/go/observability/encore-flow) for your system.\n- **AI Integration:** Encore comes with built-in tools for effective AI assisted development, like [AI instructions](/docs/go/ai-integration) and an [MCP server](/docs/go/cli/mcp).\n- **DevOps Automation Platform (Optional)**: [Encore Cloud](https://encore.cloud) is an optional platform for automating infrastructure provisioning and DevOps processes in your cloud on AWS and GCP.\n\n<div className=\"mt-6 grid grid-cols-2 gap-6 mobile:grid-cols-1 not-prose\">\n    <a className=\"block group relative no-brandient\" target=\"_blank\" href=\"https://www.youtube.com/watch?v=ipj1HdG4dWA\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Watch a demo video</h3>\n                <img className=\"-mt-2 h-16 w-16 noshadow\" src=\"/assets/icons/features/preview-envs.png\" />\n            </div>\n            <div className=\"mt-2\">\n                See how to build an event-driven app with Encore.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" target=\"_blank\" href=\"https://github.com/encoredev/examples\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Example apps</h3>\n                <img className=\"-mt-2 h-16 w-16 noshadow\" src=\"/assets/icons/features/flow.png\" />\n            </div>\n            <div className=\"mt-2\">\n                Ready-made starter apps to inspire your development.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" href=\"/discord\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Join Discord</h3>\n                <div className=\"inline-flex w-16 h-16 items-center justify-center\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 -28.5 256 256\">\n                        <path fill=\"#111111\"\n                            d=\"M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z\" />\n                    </svg>\n                </div>\n            </div>\n            <div className=\"mt-2\">\n                Find answers, ask questions, and chat with other Encore developers.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" href=\"https://github.com/encoredev/encore\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Star on GitHub</h3>\n                <div className=\"inline-flex w-16 h-16 items-center justify-center\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 16 16\" fill=\"#111111\"\n                        stroke=\"none\">\n                        <path fillRule=\"evenodd\"\n                            d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n                    </svg>\n                </div>\n            </div>\n            <div className=\"mt-2\">\n                Get involved and star Encore on GitHub.\n            </div>\n        </div>\n    </a>\n</div>\n"
  },
  {
    "path": "docs/go/primitives/api-calls.md",
    "content": "---\nseotitle: API Calls with Encore.go\nseodesc: Learn how to make type-safe API calls in Go with Encore.go\ntitle: API Calls\nsubtitle: Making API calls is as simple as making function calls\nlang: go\n---\n\nCalling an API endpoint looks like a regular function call with Encore.go. To call an endpoint you first import the other service as a Go package using `import \"encore.app/package-name\"` and then call the API endpoint like a regular function. Encore will automatically generate the necessary boilerplate at compile-time.\n\nIn the example below, we import the service package `hello` and call the `Ping` endpoint using a function call to `hello.Ping`.\n\n```go\nimport \"encore.app/hello\" // import service\n\n//encore:api public\nfunc MyOtherAPI(ctx context.Context) error {\n    resp, err := hello.Ping(ctx, &hello.PingParams{Name: \"World\"})\n    if err == nil {\n        log.Println(resp.Message) // \"Hello, World!\"\n    }\n    return err\n}\n```\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/trello-clone\" \n    desc=\"Simple microservices example application with service-to-service API calls.\" \n/>\n\nThis means your development workflow is as simple as building a monolith, even if you use multiple services.\nYou also get all the benefits of function calls, like compile-time checking of all the parameters and auto-completion in your editor, while still allowing the division of code into logical components, services, and systems.\n\nThen when building your application, Encore uses [static analysis](/docs/go/concepts/application-model) to parse all API calls and compiles them to proper API calls.\n\n## Current Request\n\nBy using Encore's [current request API](https://pkg.go.dev/encore.dev/#Request) you can get meta-information about the\ncurrent request. Including the type of request, the time the request started, the service and endpoint called and the path\nwhich was called on the service.\n\nFor more information, see the [metadata documentation](/docs/go/develop/metadata).\n"
  },
  {
    "path": "docs/go/primitives/api-errors.md",
    "content": "---\nseotitle: API Errors – Types, Wrappers, and Codes\nseodesc: See how to return structured error information from your APIs using Encore's errs package, and how to build precise error messages for complex business logic.\ntitle: API Errors\nsubtitle: Returning structured error information from your APIs\ninfobox: {\n  title: \"API Errors\",\n  import: \"encore.dev/beta/errs\",\n}\nlang: go\n---\n\nEncore supports returning structured error information from your APIs using the [encore.dev/beta/errs](https://pkg.go.dev/encore.dev/beta/errs) package.\n\nErrors are propagated across the network to the [generated clients](/docs/go/cli/client-generation) and can be used within your front-ends without having to build any custom marshalling code.\n\n## The errs.Error type\n\nStructured errors are represented by the `errs.Error` type:\n\n```go\ntype Error struct {\n\t// Code is the error code to return.\n\tCode ErrCode `json:\"code\"`\n\t// Message is a descriptive message of the error.\n\tMessage string `json:\"message\"`\n\t// Details are user-defined additional details.\n\tDetails ErrDetails `json:\"details\"`\n\t// Meta are arbitrary key-value pairs for use within\n\t// the Encore application. They are not exposed to external clients.\n\tMeta Metadata `json:\"-\"`\n}\n```\n\nReturning an `*errs.Error` from an Encore API endpoint will result in Encore\nserializing this struct to JSON and returning it in the response. Additionally\nEncore will set the HTTP status code to match the error code (see the mapping table below).\n\nFor example:\n```go\nreturn &errs.Error{\n\tCode: errs.NotFound,\n\tMessage: \"sprocket not found\",\n}\n```\n\nCauses Encore to respond with a `HTTP 404` error with body:\n```json\n{\n    \"code\": \"not_found\",\n    \"message\": \"sprocket not found\",\n    \"details\": null\n}\n```\n\n## Error Wrapping\n\nEncore applications are encouraged to always use the `errs` package to\nmanipulate errors. It supports wrapping errors to gradually add more error\ninformation, and lets you easily define both structured error details to return\nto external clients, as well as internal key-value metadata for debugging\nand error handling.\n\n```go\nfunc Wrap(err error, msg string, metaPairs ...interface{}) error\n```\nUse `errs.Wrap` to conveniently wrap an error, adding additional context and converting it to an `*errs.Error`.\nIf `err` is nil it returns `nil`. If `err` is already an `*errs.Error` it copies the Code, Details, and Meta fields over.\n\nThe variadic `metaPairs` parameter must be key-value pairs, where the key is always a `string` and the value can be\nany built-in type. Existing key-value pairs from the `err` are merged into the new `*Error`.\n\n```go\nfunc WrapCode(err error, code ErrCode, msg string, metaPairs ...interface{}) error\n```\n`errs.WrapCode` is like `errs.Wrap` but also sets the error code.\n\n```go\nfunc Convert(err error) error\n```\n`errs.Convert` converts an error to an `*errs.Error`. If the error is already an `*errs.Error` it returns it unmodified.\nIf `err` is nil it returns nil.\n\n## Error Codes\n\nThe `errs` package defines error codes for common error scenarios.\nThey are identical to the codes defined by `gRPC` for interoperability.\n\nThe table below summarizes the error codes.\nYou can find additional documentation about when to use them in the\n[package documentation](https://pkg.go.dev/encore.dev/beta/errs#ErrCode).\n\n| Code                 | String                  | HTTP Status               |\n| -------------------- | ----------------------- | ------------------------- |\n| `OK`                 | `\"ok\"`                  | 200 OK                    |\n| `Canceled`           | `\"canceled\"`            | 499 Client Closed Request |\n| `Unknown`            | `\"unknown\"`             | 500 Internal Server Error |\n| `InvalidArgument`    | `\"invalid_argument\"`    | 400 Bad Request           |\n| `DeadlineExceeded`   | `\"deadline_exceeded\"`   | 504 Gateway Timeout       |\n| `NotFound`           | `\"not_found\"`           | 404 Not Found             |\n| `AlreadyExists`      | `\"already_exists\"`      | 409 Conflict              |\n| `PermissionDenied`   | `\"permission_denied\"`   | 403 Forbidden             |\n| `ResourceExhausted`  | `\"resource_exhausted\"`  | 429 Too Many Requests     |\n| `FailedPrecondition` | `\"failed_precondition\"` | 400 Bad Request           |\n| `Aborted`            | `\"aborted\"`             | 409 Conflict              |\n| `OutOfRange`         | `\"out_of_range\"`        | 400 Bad Request           |\n| `Unimplemented`      | `\"unimplemented\"`       | 501 Not Implemented       |\n| `Internal`           | `\"internal\"`            | 500 Internal Server Error |\n| `Unavailable`        | `\"unavailable\"`         | 503 Unavailable           |\n| `DataLoss`           | `\"data_loss\"`           | 500 Internal Server Error |\n| `Unauthenticated`    | `\"unauthenticated\"`     | 401 Unauthorized          |\n\n## Error Building\n\nIn cases where you have complex business logic, or multiple error returns,\nit's convenient to gradually add metadata to your error.\n\nFor this purpose Encore provides `errs.Builder`. The builder lets you\ngradually set aspects of the error, using a chaining API design.\nUse `errs.B()` to get a new builder that you can start chaining with directly.\n\nWhen you want to return the constructed error call the `.Err() `method.\n\nFor example:\n\n```go\nfunc getBoard(ctx context.Context, boardID int64) (*Board, error) {\n    // Construct a new error builder with errs.B()\n\teb := errs.B().Meta(\"board_id\", params.ID)\n\n\tb := &Board{ID: params.ID}\n\terr := sqldb.QueryRow(ctx, `\n\t\tSELECT name, created\n\t\tFROM board\n\t\tWHERE id = $1\n\t`, params.ID).Scan(&b.Name, &b.Created)\n\tif errors.Is(err, sqldb.ErrNoRows) {\n        // Return a \"board not found\" error with code == NotFound\n\t\treturn nil, eb.Code(errs.NotFound).Msg(\"board not found\").Err()\n\t} else if err != nil {\n        // Return a general error\n\t\treturn nil, eb.Cause(err).Msg(\"could not get board\").Err()\n\t}\n    // ...\n}\n```\n\n## Inspecting API Errors\n\nWhen you call another API within Encore, the returned errors are always wrapped in `*errs.Error`.\n\nYou can inspect the error information either by casting to `*errs.Error`, or using the below\nhelper methods.\n\n```go\nfunc Code(err error) ErrCode\n```\n`errs.Code` returns the error code. If the error was not an `*errs.Error` it returns `errs.Unknown`.\n\n```go\nfunc Meta(err error) Metadata\ntype Metadata map[string]interface{}\n```\n`errs.Meta` returns any structured metadata present in the error. If the error was not an `*errs.Error` it returns nil.\nUnlike when you return error information to external clients,\nall the metadata is sent to the calling service, making debugging even easier.\n\n```go\nfunc Details(err error) ErrDetails\n```\n`errs.Details` returns the structured error details. If the error was not an `*errs.Error` or the error lacked details,\nit returns nil.\n"
  },
  {
    "path": "docs/go/primitives/api-schemas.md",
    "content": "---\nseotitle: API Schemas – Path, Query, and Body parameters\nseodesc: See how to design API schemas for your Go based backend application using Encore.\ntitle: API Schemas\nsubtitle: How to design schemas for your APIs\nlang: go\n---\nAPIs in Encore are regular functions with request and response data types.\nThese types are structs (or pointers to structs) with optional field tags, which Encore uses to encode API requests to HTTP messages. The same struct can be used for requests and responses, but the `query` tag is ignored when generating responses.\n\nAll tags except `json` are ignored for nested tags, which means you can only define\n`header` and `query` parameters for root level fields.\n\nFor example, this struct:\n```go\ntype NestedRequestResponse struct {\n\tHeader string `header:\"X-Header\"`// this field will be read from the http header\n\tQuery  string `query:\"query\"`// this field will be read from the query string\n\tBody1  string `json:\"body1\"`\n\tNested struct {\n\t    Header2 string `header:\"X-Header2\"`// this field will be read from the body\n\t\tQuery2  string `query:\"query2\"`// this field will be read from the body\n\t\tBody2   string `json:\"body2\"`\n    } `json:\"nested\"`\n}\n```\n\nWould be unmarshalled from this request:\n\n```output\nPOST /example?query=a%20query HTTP/1.1\nContent-Type: application/json\nX-Header: A header\n\n{\n   \"body1\": \"a body\",\n   \"nested\": {\n      \"Header2\": \"not a header\",\n      \"Query2\": \"not a query\",\n      \"body2\": \"a nested body\"\n   }\n}\n\n```\n\nAnd marshalled to this response:\n\n```output\nHTTP/1.1 200 OK\nContent-Type: application/json\nX-Header: A header\n\n{\n   \"Query\": \"not a query\",\n   \"body1\": \"a body\",\n   \"nested\": {\n      \"Header2\": \"not a header\",\n      \"Query2\": \"not a query\",\n      \"body2\": \"a nested body\"\n   }\n}\n\n```\n\n## Path parameters\n\nPath parameters are specified by the `path` field in the `//encore:api` annotation.\nTo specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature.\nEncore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path\ncan be parsed as a wildcard parameter by using `*name` with a matching function parameter.\n\n```go\n// GetBlogPost retrieves a blog post by id.\n//encore:api public method=GET path=/blog/:id/*path\nfunc GetBlogPost(ctx context.Context, id int, path string) (*BlogPost, error) {\n    // Use id to query database...\n}\n```\n\n### Fallback routes\n\nEncore supports defining fallback routes that will be called if no other endpoint matches the request,\nusing the syntax `path=/!fallback`.\n\nThis is often useful when migrating an existing backend service over to Encore, as it allows you to gradually\nmigrate endpoints over to Encore while routing the remaining endpoints to the existing HTTP router using\na raw endpoint with a fallback route.\n\nFor example:\n\n```go\n//encore:service\ntype Service struct {\n\toldRouter *gin.Engine // existing HTTP router\n}\n\n// Route all requests to the existing HTTP router if no other endpoint matches.\n//encore:api public raw path=/!fallback\nfunc (s *Service) Fallback(w http.ResponseWriter, req *http.Request) {\n    s.oldRouter.ServeHTTP(w, req)\n}\n```\n\n## Headers\n\nHeaders are defined by the `header` field tag, which can be used in both request and response data types. The tag name is used to translate between the struct field and http headers.\nIn the example below, the `Language` field of `ListBlogPost` will be fetched from the\n`Accept-Language` HTTP header.\n\n```go\ntype ListBlogPost struct {\n    Language string `header:\"Accept-Language\"`\n    Author      string // Not a header\n}\n```\n\n### Cookies\n\nCookies can be set in the response by using the `header` tag with the `Set-Cookie` header name.\n\n```go\ntype LoginResponse struct {\n    SessionID string `header:\"Set-Cookie\"`\n}\n\n//encore:api public method=POST path=/login\nfunc Login(ctx context.Context) (*LoginResponse, error) {\n    return &LoginResponse{SessionID: \"session=123\"}, nil\n}\n````\n\nThe cookies can then be read using e.g. [structured auth data](/docs/go/develop/auth#accepting-structured-auth-information). \n\n## Query parameters\n\nFor `GET`, `HEAD` and `DELETE` requests, parameters are read from the query string by default.\nThe query parameter name defaults to the [snake-case](https://en.wikipedia.org/wiki/Snake_case)\nencoded name of the corresponding struct field (e.g. BlogPost becomes blog_post).\n\nThe `query` field tag can be used\nto parse a field from the query string for other HTTP methods (e.g. POST) and to override the default parameter name. \n\nQuery strings are not supported in HTTP responses and therefore `query` tags in response types are ignored.\n\nIn the example below, the `PageLimit` field will be read from the `limit` query\nparameter, whereas the `Author` field will be parsed from the query string (as `author`) only if the method of\nthe request is `GET`, `HEAD` or `DELETE`.\n\n```go\ntype ListBlogPost struct {\n    PageLimit  int `query:\"limit\"` // always a query parameter\n    Author     string              // query if GET, HEAD or DELETE, otherwise body parameter\n}\n```\n\n## Body parameters\n\nEncore will default to reading request parameters from the body (as JSON) for all HTTP methods except `GET`, `HEAD` or\n`DELETE`. The name of the body parameter defaults to the field name, but can be overridden by the\n`json` tag. Response fields will be serialized as JSON in the HTTP body unless the `header` tag is set.\n\nThere is no tag to force a field to be read from the body, as some infrastructure entities\ndo not support body content in `GET`, `HEAD` or `DELETE` requests.\n\n```go\ntype CreateBlogPost struct {\n    Subject    string `json:\"limit\"` // query if GET, HEAD or DELETE, otherwise body parameter\n    Author     string                // query if GET, HEAD or DELETE, otherwise body parameter\n}\n```\n\n## Supported types\nThe table below lists the data types supported by each HTTP message location.\n\n| Type            | Header | Path | Query | Body |\n| --------------- | ------ | ---- | ----- | ---- |\n| bool            | X      | X    | X     | X    |\n| numeric         | X      | X    | X     | X    |\n| string          | X      | X    | X     | X    |\n| time.Time       | X      | X    | X     | X    |\n| uuid.UUID       | X      | X    | X     | X    |\n| json.RawMessage | X      | X    | X     | X    |\n| list            |        |      | X     | X    |\n| struct          |        |      |       | X    |\n| map             |        |      |       | X    |\n| pointer         |        |      |       | X    |\n\n## Raw endpoints\n\nIn some cases you may need to fulfill an API schema that is defined by someone else, for instance when you want to accept webhooks.\nThis often requires you to parse custom HTTP headers and do other low-level things that Encore usually lets you skip.\n\nFor these circumstances Encore lets you define raw endpoints. Raw endpoints operate at a lower abstraction level, giving you access to the underlying HTTP request.\n\nLearn more in the [raw endpoints documentation](/docs/go/primitives/raw-endpoints).\n\n## Sensitive data\n\nEncore's built-in tracing functionality automatically captures request and response payloads\nto simplify debugging. That's not desirable if a request or response payload contains sensitive data, such\nas API keys or personally identifiable information (PII).\n\nFor those use cases Encore supports marking a field as sensitive using the struct tag `encore:\"sensitive\"`.\nEncore's tracing system will automatically redact fields tagged as sensitive. This works for both individual\nvalues as well as nested fields.\n\nNote that inputs to [auth handlers](/docs/go/develop/auth) are automatically marked as sensitive and are always redacted.\n\nRaw endpoints lack a schema, which means there's no way to add a struct tag to mark certain data as sensitive.\nFor this reason Encore supports tagging the whole API endpoint as sensitive by adding `sensitive` to the `//encore:api` annotation.\nThis will cause the whole request and response payload to be redacted, including all request and response headers.\n\n<Callout type=\"info\">\n\nThe `encore:\"sensitive\"` tag is ignored for local development environments to make development and debugging with the Local Development Dashboard easier.\n\n</Callout>\n\n\n## Example\n\n```go\npackage blog // service name\nimport (\n\t\"time\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Updates struct {\n\tAuthor      string `json:\"author,omitempty\"`\n\tPublishTime time.Time `json:\"publish_time,omitempty\"`\n}\n\n// BatchUpdateParams is the request data for the BatchUpdate endpoint.\ntype BatchUpdateParams struct {\n\tRequester     string    `header:\"X-Requester\"`\n\tRequestTime   time.Time `header:\"X-Request-Time\"`\n\tCurrentAuthor string    `query:\"author\"`\n\tUpdates       *Updates  `json:\"updates\"`\n\tMySecretKey   string    `encore:\"sensitive\"`\n}\n\n// BatchUpdateResponse is the response data for the BatchUpdate endpoint.\ntype BatchUpdateResponse struct {\n\tServedBy   string       `header:\"X-Served-By\"`\n\tUpdatedIDs []uuid.UUID  `json:\"updated_ids\"`\n}\n\n//encore:api public method=POST path=/section/:sectionID/posts\nfunc BatchUpdate(ctx context.Context, sectionID string, params *BatchUpdateParams) (*BatchUpdateResponse, error) {\n\t// Update blog posts for section\n\treturn &BatchUpdateResponse{ServedBy: hostname, UpdatedIDs: ids}, nil\n}\n\n```\n"
  },
  {
    "path": "docs/go/primitives/app-structure.md",
    "content": "---\nseotitle: Structuring your microservices backend application\nseodesc: Learn how to structure your microservices backend application. See recommended app structures for monoliths, small microservices backends, and large scale microservices applications.\ntitle: App Structure\nsubtitle: Structuring your Encore application\nlang: go\n---\n\nEncore uses a monorepo design and it's best to use one Encore app for your entire backend application. This lets Encore build an application model that spans your entire app, necessary to get the most value out of many\nfeatures like [distributed tracing](/docs/go/observability/tracing) and [Encore Flow](/docs/go/observability/encore-flow).\n\nIf you have a large application, see advice on how to [structure an app with several systems](/docs/go/primitives/app-structure#large-applications-with-several-systems). \n\nIt's simple to integrate Encore applications with pre-existing systems you might have, using APIs and built-in tools like [client generation](/docs/go/cli/client-generation).\n\n## Monolith or Microservices\n\nEncore is not opinionated about monoliths vs. microservices. It does however let you build microservices applications with a monolith-style developer experience. For example, you automatically get IDE auto-complete when making [API calls between services](/docs/go/primitives/api-calls), along with cross-service type-safety.\n\nWhen using Encore Cloud to create an environment on AWS/GCP, Encore enables you to configure if you want to combine multiple services into one process or keep them separate. This can be useful for improved efficiency at smaller scales, and for co-locating services for increased performance. Learn more in the [environments documentation](/docs/platform/deploy/environments#process-allocation).\n\n## Creating services\n\nTo create an Encore service, you create a Go package and\n[define an API](/docs/go/primitives/defining-apis) within it. When using databases, you add database migrations in a subfolder `migrations` to define the structure of the database(s). Learn more in the [SQL databases docs](/docs/go/primitives/databases).\n\nOn disk it might look like this:\n\n```\n/my-app\n├── encore.app                       // ... and other top-level project files\n│\n├── hello                            // hello service (a Go package)\n│   ├── migrations                   // hello service db migration (directory)\n│   │   └── 1_create_table.up.sql    // hello service db migration\n│   ├── hello.go                     // hello service code\n│   └── hello_test.go                // tests for hello service\n│\n└── world                            // world service (a Go package)\n    └── world.go                     // world service code\n```\n\n<RelatedDocsLink paths={[\"/docs/go/primitives/services\", \"/docs/go/primitives/defining-apis\", \"/docs/go/primitives/share-db-between-services\"]} />\n\n## Structure services using sub-packages\n\nWithin a service, it's possible to have multiple sub-packages. This is a good way to define components, helper\nfunctions, or other code for your functions, should you wish to do that. You can create as many sub-packages, in any kind of nested structure within your service, as you want.\n\nTo create sub-packages, you create sub-directories within a service package. Sub-packages are internal to services,\nthey are not themselves service packages. This means sub-packages within services cannot\nthemselves define APIs.\nYou can however define an API in a service package that calls a function within a sub-package.\n\nFor example, rather than define the entire logic for an endpoint in that endpoint's function, you can call functions\nfrom sub-packages and divide the logic in any way you want.\n\n**`hello/hello.go`**\n\n```go\npackage hello\n\nimport (\n\t\"context\"\n\t\n\t\"encore.app/hello/foo\"\n)\n\n//encore:api public path=/hello/:name\nfunc World(ctx context.Context, name string) (*Response, error) {\n\tmsg := foo.GenerateMessage(name)\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n    Message string\n}\n```\n\n**`hello/foo/foo.go`**\n\n```go\npackage foo\n\nimport (\n\t\"fmt\"\n)\n\nfunc GenerateMessage(name string) string {\n\treturn fmt.Sprintf(\"Hello %s!\", name)\n}\n\n```\n\nOn disk it might look like this:\n\n```\n/my-app\n├── encore.app                       // ... and other top-level project files\n│\n├── hello                            // hello service (a Go package)\n│   ├── migrations                   // hello service db migrations (directory)\n│   │   └── 1_create_table.up.sql    // hello service db migration\n│   ├── foo                          // sub-package foo (directory)\n│   │   └── foo.go                   // foo code (cannot define APIs)\n│   ├── hello.go                     // hello service code\n│   └── hello_test.go                // tests for hello service\n│\n└── world                            // world service (a Go package)\n    └── world.go                     // world service code\n```\n\n## Large applications with several systems\n\nIf you have a large application with several logical domains, each consisting of multiple services, it can be practical\nto separate these into distinct systems.\n\nSystems are not a special construct in Encore, they only help you divide your application logically around common concerns and purposes. Encore only handles services, the compiler will read your\nsystems and extract the services of your application. As applications grow, systems help you decompose your application\nwithout requiring any complex refactoring.\n\nTo create systems, create a sub-directory for each system and put the relevant service packages within it.\nThis is all you need to do, since with Encore each service consists of a Go package.\n\nAs an example, a company building a Trello app might divide their application into three systems: the **Trello** system\n(for the end-user facing app with boards and cards), the **User** system (for user and organization management), and\nthe **Premium** system (for handling payments and subscriptions).\n\nOn disk it might look like this:\n\n```\n/my-trello-clone\n├── encore.app                  // ... and other top-level project files\n│\n├── trello                      // trello system (a directory)\n│   ├── board                   // board service (a Go package)\n│   │   └── board.go            // board service code\n│   └── card                    // card service (a Go package)\n│       └── card.go             // card service code\n│\n├── premium                     // premium system (a directory)\n│   ├── payment                 // payment service (a Go package)\n│   │   └── payment.go          // payment service code\n│   └── subscription            // subscription service (a Go package)\n│       └── subscription.go     // subscription service code\n│\n└── usr                         // usr system (a directory)\n    ├── org                     // org service (a Go package)\n    │   └── org.go              // org service code\n    └── user                    // user service (a Go package)\n        └── user.go             // user service code\n```\n\nThe only refactoring needed to divide an existing Encore application into systems is to move services into their respective\nsubfolders. This is a simple way to separate the specific concerns of each system. What matters for Encore are the packages containing services, and the division in systems or subsystems will not change the endpoints or\narchitecture of your application.\n"
  },
  {
    "path": "docs/go/primitives/caching.md",
    "content": "---\nseotitle: Using caches in your microservices backend application\nseodesc: Learn how to implement caches to optimize response times and reduce cost in your microservices cloud backend.\ntitle: Caching\nsubtitle: Optimize response times and reduce costs by avoiding re-work\ninfobox: {\n  title: \"Caching\",\n  import: \"encore.dev/storage/cache\",\n}\nlang: go\n---\n\nA cache is a high-speed storage layer, commonly used in distributed systems to improve user experiences\nby reducing latency, improving system performance, and avoiding expensive computation.\n\nFor scalable systems you typically want to deploy the cache as a separate\ninfrastructure resource, allowing you to run multiple instances of your application concurrently.\n\nEncore's built-in Caching API lets you use high-performance caches (using [Redis](https://redis.io/)) in a cloud-agnostic declarative fashion. At deployment, Encore will automatically [provision the required infrastructure](/docs/platform/infrastructure/infra).\n\n## Cache clusters\n\nTo use caching in Encore, you must first define a *cache cluster*.\nEach cache cluster defined in your application will be provisioned as a separate Redis instance\nby Encore.\n\nThis gives you fine-grained control over which service(s) should use the same cache cluster\nand which should have a separate one.\n\nIt looks like this:\n\n```go\nimport \"encore.dev/storage/cache\"\n\nvar MyCacheCluster = cache.NewCluster(\"my-cache-cluster\", cache.ClusterConfig{\n    // EvictionPolicy tells Redis how to evict keys when the cache reaches\n    // its memory limit. For typical cache use cases, cache.AllKeysLRU is a good default.\n    EvictionPolicy: cache.AllKeysLRU,\n})\n```\n\n<Callout type=\"info\">\n\nWhen starting out it's recommended to use a single cache cluster\nthat's shared between your different services.\n\n</Callout>\n\n## Keyspaces\n\nWhen using a cache, each cached item is stored at a particular key, which is typically an arbitrary string.\nIf you use a cache cluster to cache different sets of data, it's important that distinct data set have non-overlapping keys.\n\nEach value stored in the cache also has a specific type, and certain cache operations can only be performed on certain types. For example, a common cache operation is to increment an integer value that is stored in the cache. If you try to apply this operation on a value that is not an integer, an error is returned.\n\nEncore provides a simple, type-safe solution to these problems through Keyspaces.\n\nIn order to begin storing data in your cache, you must first define a Keyspace.\n\nEach keyspace has a Key type and a Value type. The Key type is much like a map key, in that it tells Encore where in the cache\nthe item is stored. The Key type is combined with the Key Pattern to produce a string that is the Redis cache key.\n\nThe Value type is the type of the values stored in that keyspace. For many keyspaces this is specified in the name of the constructor.\nFor example, `NewIntKeyspace` stores `int64` values.\n\nFor example, if you want to rate limit the number of requests per user ID it looks like this:\n\n```go\nimport (\n    \"encore.dev/beta/auth\"\n    \"encore.dev/beta/errs\"\n    \"encore.dev/middleware\"\n)\n\n// RequestsPerUser tracks the number of requests per user.\n// The cache items expire after 10 seconds without activity.\nvar RequestsPerUser = cache.NewIntKeyspace[auth.UID](cluster, cache.KeyspaceConfig{\n\tKeyPattern:    \"requests/:key\",\n\tDefaultExpiry: cache.ExpireIn(10 * time.Second),\n})\n\n// RateLimitMiddleware is a global middleware that limits the number of authenticated requests\n// to 10 requests per 10 seconds.\n//encore:middleware target=all\nfunc RateLimitMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n\tif userID, ok := auth.UserID(); ok {\n\t\tval, err := RequestsPerUser.Increment(req.Context(), userID, 1)\n\n\t\t// NOTE: this \"fails open\", meaning if we can't communicate with the cache\n\t\t// we default to allowing the requests.\n\t\t//\n\t\t// Consider whether that's the correct behavior for your application,\n\t\t// or if you want to return an error to the user in that case.\n\t\tif err == nil && val > 10 {\n\t\t\treturn middleware.Response{\n\t\t\t\tErr: &errs.Error{Code: errs.ResourceExhausted, Message: \"rate limit exceeded\"},\n\t\t\t}\n\t\t}\n\t}\n\treturn next(req)\n}\n```\n\nAs you can see, the `RequestsPerUser` defines a `KeyPattern` which is set to `\"requests/:key\"`.\nHere `:key` refers to the value of the Key type, which is the `auth.UID` value passed in.\n\nIf you want the cache key to contain multiple values, you can define a struct type\nand pass that as the key. Then change the `KeyPattern` to specify the struct fields.\n\nFor example:\n\n```go\ntype MyKey struct {\n    UserID auth.UID\n    ResourcePath string // the resource being accessed\n}\n\n// ResourceRequestsPerUser tracks the number of requests per user and resource.\n// The cache items expire after 10 seconds without activity.\nvar ResourceRequestsPerUser = cache.NewIntKeyspace[MyKey](cluster, cache.KeyspaceConfig{\n\tKeyPattern:    \"requests/:UserID/:ResourcePath\",\n\tDefaultExpiry: cache.ExpireIn(10 * time.Second),\n})\n\n// ... then:\nkey := MyKey{UserID: \"some-user-id\", ResourcePath: \"/foo\"}\nResourceRequestsPerUser.Increment(ctx, key, 1)\n```\n\n<Callout type=\"info\">\n\nEncore ensures that all the struct fields are present in the `KeyPattern`,\nand that the placeholder values are all valid field names.\n\nThat way the connection between the struct fields and the `KeyPattern`\nbecome compile-time type-safe as well.\n\n</Callout>\n\nAlso note that Encore ensures there are no conflicting `KeyPattern` definitions across each cache cluster.\nEach keyspace must define its own, non-conflicting `KeyPattern`.\nThis way, you can feel safe that there won't be any accidental overwrites of cache values, even with multiple services sharing the same cache cluster.\n\n## Keyspace operations\n\nEncore comes with a full suite of keyspace types, each with a wide variety of cache operations.\n\nBasic keyspace types include\n[strings](https://pkg.go.dev/encore.dev/storage/cache#NewStringKeyspace),\n[integers](https://pkg.go.dev/encore.dev/storage/cache#NewIntKeyspace),\n[floats](https://pkg.go.dev/encore.dev/storage/cache#NewFloatKeyspace),\nand [struct types](https://pkg.go.dev/encore.dev/storage/cache#NewStructKeyspace).\nThese keyspaces all share the same set of methods (along with a few keyspace-specific ones).\n\nThere are also more advanced keyspaces for storing [sets of basic types](https://pkg.go.dev/encore.dev/storage/cache#NewSetKeyspace)\nand [ordered lists of basic types](https://pkg.go.dev/encore.dev/storage/cache#NewListKeyspace).\nThese keyspaces offer a different, specialized set of methods specific to set and list operations.\n\nFor a list of the supported operations, see the [package documentation](https://pkg.go.dev/encore.dev/storage/cache).\n\n## Testing\n\nWhen running tests, Encore spins up an in-memory cache separately for each test.\n\nThis way you don't have to think about clearing the cache between tests,\nor worrying about whether one test affects another.\nEach test is automatically fully isolated.\n\n## Local development\n\nFor local development, Encore maintains a local, in-memory implementation of Redis.\nThis implementation is designed to store a small amount of keys (currently 100).\n\nWhen the number of keys exceeds this value, keys are randomly purged to get below the limit.\nThis is designed in order to simulate the ephemeral, transient nature of caches while also\nlimiting memory use. The precise behavior for local development may change over time and should not be relied on.\n"
  },
  {
    "path": "docs/go/primitives/change-db-schema.md",
    "content": "---\nseotitle: How to change your SQL database schema\nseodesc: Learn how to change your SQL database schema for your Go backend application, using migration files and Encore's built-in schema migration functionality.\ntitle: Change SQL database schema\nlang: go\n---\n\nEncore database schemas are changed over time using *migration files*.\n\nEach migration file has a sequence number, and migration files are run\nin sequence when deploying. Encore tracks which migrations have already run\nand only runs new ones.\n\nTo change your database schema, add a new migration file using the next\navailable migration number.\n\nFor example, if you have two migration files already,\nthe next migration file should be named `3_something.up.sql` where\n`something` is a short description of what the migration does.\n\n<Callout type=\"warning\">\n\nDatabase migrations are applied before the application is restarted\nwith the new code. Always make sure the old application code works with\nthe new database schema, so that things don't break while your new code\nis being rolled out.\n\n</Callout>\n\n## Example\n\nLet's say you have a single migration file that creates a `todo_item` table:\n\n**`todo/migrations/1_create_table.up.sql`**\n```sql\nCREATE TABLE todo_item (\n    id BIGSERIAL PRIMARY KEY,\n    title TEXT NOT NULL,\n    done BOOLEAN NOT NULL\n);\n```\n\nAnd now you want to add a `created` column to track when each todo was created.\nAdd a new file:\n\n**`todo/migrations/2_add_created_col.up.sql`**\n```sql\nALTER TABLE todo_item ADD created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW();\n```\n\nThe next deploy Encore will notice the new migration file and run it, adding\na new column.\n"
  },
  {
    "path": "docs/go/primitives/code-snippets.md",
    "content": "---\nseotitle: Code snippets for using the Backend Framework's building blocks in your backend application\nseodesc: Learn how to build cloud-agnostic backend applications using Encore's Backend Framework.\ntitle: Code snippets\nsubtitle: Shortcuts for building with Encore\nlang: go\n---\n\nWhen you're familiar with how Encore works, you can simplify your development workflow by copy-pasting these examples. If you're looking for details on how Encore works, please refer to the relevant docs section.\n\n## APIs\n\n### Defining APIs\n\n```go\npackage hello // service name\n\n//encore:api public\nfunc Ping(ctx context.Context, params *PingParams) (*PingResponse, error) {\n    msg := fmt.Sprintf(\"Hello, %s!\", params.Name)\n    return &PingResponse{Message: msg}, nil\n}\n```\n\n### Defining Request and Response schemas\n\n```go\n// PingParams is the request data for the Ping endpoint.\ntype PingParams struct {\n    Name string\n}\n\n// PingResponse is the response data for the Ping endpoint.\ntype PingResponse struct {\n    Message string\n}\n```\n\n### Calling APIs\n\n```go\nimport \"encore.app/hello\" // import service\n\n//encore:api public\nfunc MyOtherAPI(ctx context.Context) error {\n    resp, err := hello.Ping(ctx, &hello.PingParams{Name: \"World\"})\n    if err == nil {\n        log.Println(resp.Message) // \"Hello, World!\"\n    }\n    return err\n}\n```\n\n**Hint:** Import the service package and call the API endpoint using a regular function call.\n\n### Receive Webhooks\n\n```go\nimport \"net/http\"\n\n// Webhook receives incoming webhooks from Some Service That Sends Webhooks.\n//encore:api public raw\nfunc Webhook(w http.ResponseWriter, req *http.Request) {\n    // ... operate on the raw HTTP request ...\n}\n```\n\n**Hint:** Like any other API endpoint, this will be exposed at:<br/>\n`https://<env>-<app-id>.encr.app/service.Webhook`\n\n## Databases\n\n### Creating a SQL database\n\nTo create a database, import `encore.dev/storage/sqldb` and call `sqldb.NewDatabase`, assigning the result to a package-level variable.\n`sqldb.DatabaseConfig` specifies the directory containing the database migration files, which is how you define the database schema.\n\n```\n-- todo/db.go --\npackage todo\n\n// Create the todo database and assign it to the \"tododb\" variable\nvar tododb = sqldb.NewDatabase(\"todo\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// Then, query the database using db.QueryRow, db.Exec, etc.\n-- todo/migrations/1_create_table.up.sql --\nCREATE TABLE todo_item (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  done BOOLEAN NOT NULL DEFAULT false\n  -- etc...\n);\n```\n\n### Inserting data into a database\n\nOne way of inserting data is with a helper function that uses the package function `sqldb.Exec`:\n\n```go\nimport \"encore.dev/storage/sqldb\"\n\n// insert inserts a todo item into the database.\nfunc insert(ctx context.Context, id, title string, done bool) error {\n\t_, err := tododb.Exec(ctx, `\n\t\tINSERT INTO todo_item (id, title, done)\n\t\tVALUES ($1, $2, $3)\n\t`, id, title, done)\n\treturn err\n}\n```\n\n### Querying a database\n\nTo read a single todo item in the example schema above, we can use `sqldb.QueryRow`:\n\n```go\nimport \"encore.dev/storage/sqldb\"\n\nvar item struct {\n    ID int64\n    Title string\n    Done bool\n}\nerr := tododb.QueryRow(ctx, `\n    SELECT id, title, done\n    FROM todo_item\n    LIMIT 1\n`).Scan(&item.ID, &item.Title, &item.Done)\n```\n\n**Hint:** If `sqldb.QueryRow` does not find a matching row, it reports an error that can be checked against\nby importing the standard library `errors` package and calling `errors.Is(err, sqldb.ErrNoRows)`.\n\n## Defining a Cron Job\n\n```go\nimport \"encore.dev/cron\"\n\nvar _ = cron.NewJob(\"welcome-email\", cron.JobConfig{\n\tTitle:    \"Send welcome emails\",\n\tEvery:    2 * cron.Hour,\n\tEndpoint: SendWelcomeEmail,\n})\n\n//encore:api private\nfunc SendWelcomeEmail(ctx context.Context) error {\n\t// ...\n\treturn nil\n}\n```\n**Hint:** Cron Jobs do not run in your local development environment.\n\n## PubSub\n\n### Creating a PubSub topic\n\n```go\nimport \"encore.dev/pubsub\"\n\ntype SignupEvent struct { UserID int }\nvar Signups = pubsub.NewTopic[*SignupEvent](\"signups\", pubsub.TopicConfig {\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n```\n\n**Hint:** Topics are declared as package level variables and cannot be created inside functions. Regardless of where you create a topic, it can be published and subscribed to from any service.\n\n### Publishing an Event (Pub)\n\n```go\nif _, err := Signups.Publish(ctx, &SignupEvent{UserID: id}); err != nil {\n    return err\n}\n\nif err := tx.Commit(); err != nil {\n    return err\n}\n```\n\n**Hint:** If you want to publish to the topic from another service, import the topic package variable (`Signups` in this example) and call publish on it from there.\n\n### Subscribing to Events (Sub)\n\nCreate a Subscription as a package level variable by calling `pubsub.NewSubscription`.\n\n```go\nvar _ = pubsub.NewSubscription(\n    user.Signups, \"send-welcome-email\",\n    pubsub.SubscriptionConfig[*SignupEvent] {\n        Handler: SendWelcomeEmail,\n    },\n)\nfunc SendWelcomeEmail(ctx context.Context, event *SignupEvent) error {\n    ... send email ...\n    return nil\n}\n```\n\n## Defining a Cache cluster\n\n```go\nimport \"encore.dev/storage/cache\"\n\nvar MyCacheCluster = cache.NewCluster(\"my-cache-cluster\", cache.ClusterConfig{\n    // EvictionPolicy tells Redis how to evict keys when the cache reaches\n    // its memory limit. For typical cache use cases, cache.AllKeysLRU is a good default.\n    EvictionPolicy: cache.AllKeysLRU,\n})\n```\n\n## Secrets\n\n### Defining Secrets\n\n```go\nvar secrets struct {\n    GitHubAPIToken string   // personal access token for deployments\n    SomeOtherSecret string  // some other secret\n}\n```\n\n**Hint:** The variable must be an unexported struct named `secrets`, and all the fields must be of type `string`.\n\n### Setting secret values\n\n```shell\n$ encore secret set --type <types...> <secret-name>\n```\n\n**Hint:** `<types>` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. For each Secret, there can only be one secret value for each environment type.\n\n### Using secrets\n\n```go\nfunc callGitHub(ctx context.Context) {\n    req, _ := http.NewRequestWithContext(ctx, \"GET\", \"https:///api.github.com/user\", nil)\n    req.Header.Add(\"Authorization\", \"token \" + secrets.GitHubAPIToken)\n    resp, err := http.DefaultClient.Do(req)\n    // ... handle err and resp\n}\n```\n\n**Hint:** Secret keys are globally unique for your whole application; if multiple services use the same secret name they both receive the same secret value at runtime.\n"
  },
  {
    "path": "docs/go/primitives/connect-existing-db.md",
    "content": "---\nseotitle: How to integrate your Encore app with an existing database\nseodesc: Learn how to integrate your Encore Go backend application with an existing database, in any cloud you choose.\ntitle: Integrate with existing databases\nlang: go\n---\n\nEncore automatically provision the necessary infrastructure when you create a service and add a database. However, you may want to connect to an existing database for migration or prototyping purposes. It's simple to integrate your Encore app with an existing database in these cases.\n\n## Example\n\nLet's say you have an external database hosted by DigitalOcean that you would like to connect to.\nThe simplest approach is to create a dedicated package that lazily instantiates a database connection pool.\nWe can store the password using Encore's [secrets manager](/docs/go/primitives/secrets) to make it even easier.\n\nThe connection string is something that looks like:\n\n```\npostgresql://user:password@externaldb-do-user-1234567-0.db.ondigitalocean.com:25010/externaldb?sslmode=require\n```\n\nSo we write something like:\n\n**`pkg/externaldb/externaldb.go`**\n\n```go\npackage externaldb\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/jackc/pgx/v4/pgxpool\"\n    \"go4.org/syncutil\"\n)\n\n// Get returns a database connection pool to the external database.\n// It is lazily created on first use.\nfunc Get(ctx context.Context) (*pgxpool.Pool, error) {\n    // Attempt to setup the database connection pool if it hasn't\n    // already been successfully setup.\n    err := once.Do(func() error {\n        var err error\n        pool, err = setup(ctx)\n        return err\n    })\n    return pool, err\n}\n\nvar (\n    // once is like sync.Once except it re-arms itself on failure\n    once syncutil.Once\n    // pool is the successfully created database connection pool,\n    // or nil when no such pool has been setup yet.\n    pool *pgxpool.Pool\n)\n\nvar secrets struct {\n    // ExternalDBPassword is the database password for authenticating\n    // with the external database hosted on DigitalOcean.\n    ExternalDBPassword string\n}\n\n// setup attempts to set up a database connection pool.\nfunc setup(ctx context.Context) (*pgxpool.Pool, error) {\n    connString := fmt.Sprintf(\"postgresql://%s:%s@externaldb-do-user-1234567-0.db.ondigitalocean.com:25010/externaldb?sslmode=require\",\n        \"user\", secrets.ExternalDBPassword)\n    return pgxpool.Connect(ctx, connString)\n}\n```\n\nBefore running, remember to use `encore secrets set` to store the `ExternalDBPassword` to use. (But don't worry, Encore will remind you if you forget.)\n\n## Other infrastructure\n\nThe same pattern can easily be adapted to other infrastructure components that Encore doesn't yet provide built-in support for:\n\n- Horizontally scalable databases like Cassandra, DynamoDB, BigTable, and so on\n- Document or graph databases like MongoDB or Neo4j\n- Other cloud primitives like queues, object storage buckets, and more\n- Or really any cloud services or APIs you can think of\n\nIn this way you can easily integrate Encore with anything you want.\n"
  },
  {
    "path": "docs/go/primitives/cron-jobs.md",
    "content": "---\nseotitle: Create recurring tasks with Encore's Cron Jobs API\nseodesc: Learn how to create periodic and recurring tasks in your backend application using Encore's Cron Jobs API.\ntitle: Cron Jobs\nsubtitle: Run recurring and scheduled tasks\ninfobox: {\n  title: \"Cron Jobs\",\n  import: \"encore.dev/cron\",\n  example_link: \"/docs/tutorials/uptime\"\n}\nlang: go\n---\n\nWhen you need to run periodic and recurring tasks, Encore.go provides a declarative way of using Cron Jobs.\n\nWhen a Cron Job is defined in your application, Encore automatically calls your specified API according to the defined schedule. This eliminates the need for infrastructure maintenance, as Encore manages scheduling, monitoring, and execution of Cron Jobs.\n\n<Callout type=\"info\">\n\nCron Jobs do not run when developing locally or in [Preview Environments](/docs/platform/deploy/preview-environments), but you can always call the API manually to test the behavior.\n\n</Callout>\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/uptime\"\n    desc=\"Uptime Monitoring app that uses a Cron Job to periodically check the uptime of a website.\"\n/>\n\n## Defining a Cron Job\n\nTo define a Cron Job, import the `encore.dev/cron` [package](https://pkg.go.dev/encore.dev/cron),\nand call the `cron.NewJob()` function and store it as a package-level variable.\n\n### Example\n\n```go\nimport \"encore.dev/cron\"\n\n// Send a welcome email to everyone who signed up in the last two hours.\nvar _ = cron.NewJob(\"welcome-email\", cron.JobConfig{\n\tTitle:    \"Send welcome emails\",\n\tEvery:    2 * cron.Hour,\n\tEndpoint: SendWelcomeEmail,\n})\n\n// SendWelcomeEmail emails everyone who signed up recently.\n// It's idempotent: it only sends a welcome email to each person once.\n//encore:api private\nfunc SendWelcomeEmail(ctx context.Context) error {\n\t// ...\n\treturn nil\n}\n```\n\nThe `\"welcome-email\"` argument to `cron.NewJob` is a unique ID you give to each Cron Job.\nIf you later refactor the code and move the Cron Job definition to another package,\nwe use this ID to keep track that it's the same Cron Job and not a different one.\n\nWhen this code gets deployed Encore will automatically register the Cron Job in Encore Cloud\nand begin calling the `SendWelcomeEmail` API every hour.\n\nThe Encore Cloud dashboard provides a convenient user interface for monitoring and debugging\nCron Job executions across all your environments via the `Cron Jobs` menu item:\n\n![Cron Jobs UI](/assets/docs/cron.png)\n\n## Keep in mind when using Cron Jobs\n\n- Cron Jobs do not execute during local development or in [Preview Environments](/docs/platform/deploy/preview-environments). However, you can manually invoke the API to test its behavior.\n- In Encore Cloud, Cron Job executions are limited to **once every hour**, with the exact minute randomized within that hour for users on the Free Tier. To enable more frequent executions or to specify the exact minute within the hour, consider [deploying to your own cloud](/docs/platform/deploy/own-cloud) or upgrading to the [Pro plan](/pricing).\n- Both public and private APIs are supported for Cron Jobs.\n- Ensure that the API endpoints used in Cron Jobs are idempotent, as they may be called multiple times under certain network conditions.\n- The API endpoints used in Cron Jobs must not take any request parameters. That is, their signatures must be `func(context.Context) error` or `func(context.Context) (*T, error)`.\n\n## Cron schedules\n\nAbove we used the `Every` field, which executes the Cron Job on a periodic basis.\nIt runs around the clock each day, starting at midnight (UTC).\n\nIn order to ensure a consistent delay between each run, the interval used **must divide 24 hours evenly**.\nFor example, `10 * cron.Minute` and `6 * cron.Hour` are both allowed (since 24 hours is evenly divisible by both),\nwhereas `7 * cron.Hour` is not (since 24 is not evenly divisible by 7).\nThe Encore compiler will catch this and give you a helpful error at compile-time if you try to use an invalid interval.\n\n### Cron expressions\n\nFor more advanced use cases, such as running a Cron Job on a specific day of the month, or a specific week day, or similar,\nthe `Every` field is not expressive enough.\n\nFor these use cases, Encore provides full support for [Cron expressions](https://en.wikipedia.org/wiki/Cron) by using the `Schedule` field\ninstead of the `Every` field.\n\nCron expressions allow you to define precise schedules for your tasks, including specific days of the week, specific hours of the day, and more. Note that all times are expressed in UTC.\n\nFor example:\n\n```go\n// Run the monthly accounting sync job at 4am (UTC) on the 15th day of each month.\nvar _ = cron.NewJob(\"accounting-sync\", cron.JobConfig{\n\tTitle:    \"Cron Job Example\",\n\tSchedule: \"0 4 15 * *\",\n\tEndpoint: AccountingSync,\n})\n```\n"
  },
  {
    "path": "docs/go/primitives/database-extensions.md",
    "content": "---\nseotitle: Pre-installed PostgreSQL extensions\nseodesc: See the list of pre-installed PostgreSQL extensions available when using Encore\ntitle: PostgreSQL Extensions\nsubtitle: Pre-installed extensions\ninfobox: {\n  title: \"SQL Databases\",\n  import: \"encore.dev/storage/sqldb\"\n}\nlang: go\n---\n\nEncore uses the [encoredotdev/postgres](https://github.com/encoredev/postgres-image) docker image for local development, CI/CD, and for databases hosted on Encore Cloud.\n\nThe docker image ships with the following PostgreSQL extensions pre-installed and available for use (via `CREATE EXTENSION`):\n\n| Extension                      | Version | Description                                                                                                         |\n| ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------- |\n| refint                         | 1.0     | functions for implementing referential integrity (obsolete)                                                         |\n| pg_buffercache                 | 1.3     | examine the shared buffer cache                                                                                     |\n| pg_freespacemap                | 1.2     | examine the free space map (FSM)                                                                                    |\n| plpgsql                        | 1.0     | PL/pgSQL procedural language                                                                                        |\n| citext                         | 1.6     | data type for case-insensitive character strings                                                                    |\n| adminpack                      | 2.1     | administrative functions for PostgreSQL                                                                             |\n| moddatetime                    | 1.0     | functions for tracking last modification time                                                                       |\n| amcheck                        | 1.3     | functions for verifying relation integrity                                                                          |\n| seg                            | 1.4     | data type for representing line segments or floating-point intervals                                                |\n| pg_stat_statements             | 1.10    | track planning and execution statistics of all SQL statements executed                                              |\n| pg_trgm                        | 1.6     | text similarity measurement and index searching based on trigrams                                                   |\n| isn                            | 1.2     | data types for international product numbering standards                                                            |\n| btree_gist                     | 1.7     | support for indexing common datatypes in GiST                                                                       |\n| intarray                       | 1.5     | functions, operators, and index support for 1-D arrays of integers                                                  |\n| pg_surgery                     | 1.0     | extension to perform surgery on a damaged relation                                                                  |\n| uuid-ossp                      | 1.1     | generate universally unique identifiers (UUIDs)                                                                     |\n| insert_username                | 1.0     | functions for tracking who changed a table                                                                          |\n| bloom                          | 1.0     | bloom access method - signature file based index                                                                    |\n| pgcrypto                       | 1.3     | cryptographic functions                                                                                             |\n| dblink                         | 1.2     | connect to other PostgreSQL databases from within a database                                                        |\n| tsm_system_rows                | 1.0     | TABLESAMPLE method which accepts number of rows as a limit                                                          |\n| pg_prewarm                     | 1.2     | prewarm relation data                                                                                               |\n| old_snapshot                   | 1.0     | utilities in support of old_snapshot_threshold                                                                      |\n| pageinspect                    | 1.11    | inspect the contents of database pages at a low level                                                               |\n| intagg                         | 1.1     | integer aggregator and enumerator (obsolete)                                                                        |\n| pg_visibility                  | 1.2     | examine the visibility map (VM) and page-level visibility info                                                      |\n| cube                           | 1.5     | data type for multidimensional cubes                                                                                |\n| tablefunc                      | 1.0     | functions that manipulate whole tables, including crosstab                                                          |\n| xml2                           | 1.1     | XPath querying and XSLT                                                                                             |\n| fuzzystrmatch                  | 1.1     | determine similarities and distance between strings                                                                 |\n| pg_walinspect                  | 1.0     | functions to inspect contents of PostgreSQL Write-Ahead Log                                                         |\n| btree_gin                      | 1.3     | support for indexing common datatypes in GIN                                                                        |\n| sslinfo                        | 1.2     | information about SSL certificates                                                                                  |\n| tcn                            | 1.0     | Triggered change notifications                                                                                      |\n| hstore                         | 1.8     | data type for storing sets of (key, value) pairs                                                                    |\n| dict_int                       | 1.0     | text search dictionary template for integers                                                                        |\n| earthdistance                  | 1.1     | calculate great-circle distances on the surface of the Earth                                                        |\n| file_fdw                       | 1.0     | foreign-data wrapper for flat file access                                                                           |\n| autoinc                        | 1.0     | functions for autoincrementing fields                                                                               |\n| ltree                          | 1.2     | data type for hierarchical tree-like structures                                                                     |\n| unaccent                       | 1.1     | text search dictionary that removes accents                                                                         |\n| pgrowlocks                     | 1.2     | show row-level locking information                                                                                  |\n| tsm_system_time                | 1.0     | TABLESAMPLE method which accepts time in milliseconds as a limit                                                    |\n| dict_xsyn                      | 1.0     | text search dictionary template for extended synonym processing                                                     |\n| pgstattuple                    | 1.5     | show tuple-level statistics                                                                                         |\n| postgres_fdw                   | 1.1     | foreign-data wrapper for remote PostgreSQL servers                                                                  |\n| lo                             | 1.1     | Large Object maintenance                                                                                            |\n| postgis_sfcgal-3               | 3.4.2   | PostGIS SFCGAL functions                                                                                            |\n| address_standardizer_data_us-3 | 3.4.2   | Address Standardizer US dataset example                                                                             |\n| address_standardizer-3         | 3.4.2   | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. |\n| postgis_topology-3             | 3.4.2   | PostGIS topology spatial types and functions                                                                        |\n| postgis-3                      | 3.4.2   | PostGIS geometry and geography spatial types and functions                                                          |\n| postgis_raster-3               | 3.4.2   | PostGIS raster types and functions                                                                                  |\n| postgis_tiger_geocoder-3       | 3.4.2   | PostGIS tiger geocoder and reverse geocoder                                                                         |\n| vector                         | 0.7.0   | vector data type and ivfflat and hnsw access methods                                                                |\n| postgis                        | 3.4.2   | PostGIS geometry and geography spatial types and functions                                                          |\n| address_standardizer           | 3.4.2   | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. |\n| postgis_topology               | 3.4.2   | PostGIS topology spatial types and functions                                                                        |\n| postgis_tiger_geocoder         | 3.4.2   | PostGIS tiger geocoder and reverse geocoder                                                                         |\n| address_standardizer_data_us   | 3.4.2   | Address Standardizer US dataset example                                                                             |\n| postgis_sfcgal                 | 3.4.2   | PostGIS SFCGAL functions                                                                                            |\n| postgis_raster                 | 3.4.2   | PostGIS raster types and functions                                                                                  |\n"
  },
  {
    "path": "docs/go/primitives/database-troubleshooting.md",
    "content": "---\nseotitle: Troubleshooting SQL databases\nseodesc: Advice on troubleshooting SQL databases in Encore.go\ntitle: Troubleshooting Databases\nsubtitle: Advice on troubleshooting SQL databases in Encore.go\ninfobox: {\n  title: \"SQL Databases\",\n  import: \"encore.dev/storage/sqldb\"\n}\nlang: go\n---\n\nWhen you run your application locally with `encore run`, Encore provisions local databases using [Docker](https://docker.com). If this fails with a database error, it can often be resolved by making sure you have Docker installed and running, or by restarting the Encore daemon using `encore daemon`.\n\nIf this does not resolve the issue, here are steps to resolve common errors:\n\n** Error: sqldb: unknown database **\n\nThis error is often caused by a problem with the initial migration file, such as incorrect naming or location.\n\n- Verify that you've [created the migration file](/docs/go/primitives/databases#defining-a-database-schema) correctly, then try `encore run` again.\n\n** Error: could not connect to the database **\n\nWhen you can't connect to the database in your local environment, there's likely an issue with Docker:\n\n- Make sure that you have [Docker](https://docker.com) installed and running, then try `encore run` again.\n- If this fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again.\n\n** Error: Creating PostgreSQL database cluster Failed **\n\nThis means Encore was not able to create the database. Often this is due to a problem with Docker.\n\n- Check if you have permission to access Docker by running `docker images`.\n- Set the correct permissions with `sudo usermod -aG docker $USER` (Learn more in the [Docker documentation](https://docs.docker.com/engine/install/linux-postinstall/))\n- Then log out and log back in so that your group membership is refreshed.\n\n** Error: unable to save docker image **\n\nThis error is often caused by a problem with Docker.\n\n- Make sure that you have [Docker](https://docker.com) installed and running.\n- In Docker, open **Settings > Advanced** and make sure that the setting `Allow the default Docker socket to be used` is checked.\n- If it still fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again.\n\n** Error: unable to add CA to cert pool **\n\nThis error is commonly caused by the presence of the file `$HOME/.postgresql/root.crt` on the filesystem.\nWhen this file is present the PostgreSQL client library will assume the database server has that root certificate,\nwhich will cause the above error.\n\n- Remove or rename the file, then try `encore run` again.\n\n** Resetting databases **\n\nIf your local database is in a bad state (e.g. due to a incomplete migration or corrupt data), you can reset it by running:\n\n```shell\n$ encore db reset <database-name>\n```\n\nThis drops and recreates the database, re-running all migrations from scratch. Use `--all` to reset all databases at once.\n"
  },
  {
    "path": "docs/go/primitives/databases.md",
    "content": "---\nseotitle: Using SQL databases for your backend application\nseodesc: Learn how to use SQL databases for your backend application. See how to provision, migrate, and query PostgreSQL databases using Go and Encore.\ntitle: Using SQL databases\nsubtitle: Provisioning, migrating, querying\ninfobox: {\n  title: \"SQL Databases\",\n  import: \"encore.dev/storage/sqldb\",\n  example_link: \"/docs/tutorials/uptime\"\n}\nlang: go\n---\n\nEncore treats SQL databases as logical resources and natively supports **PostgreSQL** databases.\n\n## Creating a database\n\nTo create a database, import `encore.dev/storage/sqldb` and call `sqldb.NewDatabase`, assigning the result to a package-level variable.\nDatabases must be created from within an [Encore service](/docs/go/primitives/services).\n\nFor example:\n\n```\n-- todo/db.go --\npackage todo\n\n// Create the todo database and assign it to the \"tododb\" variable\nvar tododb = sqldb.NewDatabase(\"todo\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// Then, query the database using db.QueryRow, db.Exec, etc.\n-- todo/migrations/1_create_table.up.sql --\nCREATE TABLE todo_item (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  done BOOLEAN NOT NULL DEFAULT false\n  -- etc...\n);\n```\n\nAs seen above, the `sqldb.DatabaseConfig` specifies the directory containing the database migration files, which is how you define the database schema.\nSee the [Defining the database schema](#defining-the-database-schema) section below for more details.\n\nWith this code in place, Encore will automatically create the database using [Docker](https://docker.com) when you run the command `encore run` in your local environment. Make sure Docker is installed and running on your machine before running `encore run`.\n\n<Callout type=\"info\">\n\nIf your application is already running when you define a new database, you will need to stop and restart `encore run`. This is necessary for Encore to create the new database using Docker.\n\n</Callout>\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/sql-database\"\n    desc=\"Simple PostgreSQL example application.\"\n/>\n\n## Database Migrations\n\nEncore automatically handles `up` migrations, while `down` migrations must be run manually. Each `up` migration runs sequentially, expressing changes in the database schema from the previous migration.\n\n### Naming Conventions\n\n**File Name Format:** Migration files must start with a number followed by an underscore (`_`), and must increase sequentially. Each file name must end with `.up.sql`.\n\n**Examples:**\n- `1_first_migration.up.sql`\n- `2_second_migration.up.sql`\n- `3_migration_name.up.sql`\n\nYou can also prefix migration files with leading zeroes for better ordering in the editor (e.g., `0001_migration.up.sql`).\n\n### Defining the Database Schema\n\nThe first migration typically defines the initial table structure. For instance, a `todo` service might create `todo/migrations/1_create_table.up.sql` with the following content:\n\n```sql\nCREATE TABLE todo_item (\n    id BIGSERIAL PRIMARY KEY,\n    title TEXT NOT NULL,\n    done BOOLEAN NOT NULL DEFAULT false\n);\n```\n\n### Migration File Structure\n\nMigration files are created in a `migrations` directory within an Encore service package. Each file is named `<number>_<name>.up.sql`, where `<number>` is a sequence number for ordering and `<name>` describes the migration.\n\n**Example Directory Structure:**\n\n```\n/my-app\n├── encore.app                       // ... and other top-level project files\n│\n└── todo                             // todo service (a Go package)\n    ├── migrations                   // todo service db migrations (directory)\n    │   ├── 1_create_table.up.sql    // todo service db migration\n    │   └── 2_add_field.up.sql       // todo service db migration\n    ├── todo.go                      // todo service code\n    └── todo_test.go                 // tests for todo service\n```\n\n## Inserting data into databases\n\nOnce you have created the database using `var mydb = sqldb.NewDatabase(...)` you can start inserting data into the database\nby calling methods on the `mydb` variable.\n\nThe interface is similar to that of the Go standard library's `database/sql` package.\nLearn more in the [package docs](https://pkg.go.dev/encore.dev/storage/sqldb).\n\nOne way of inserting data is with a helper function that uses the package function `sqldb.Exec`.\nFor example, to insert a single todo item using the example schema above, we can use the following helper function `insert`:\n\n```\n-- todo/insert.go --\n// insert inserts a todo item into the database.\nfunc insert(ctx context.Context, id, title string, done bool) error {\n\t_, err := tododb.Exec(ctx, `\n\t\tINSERT INTO todo_item (id, title, done)\n\t\tVALUES ($1, $2, $3)\n\t`, id, title, done)\n\treturn err\n}\n-- todo/db.go --\npackage todo\n\n// Create the todo database and assign it to the \"tododb\" variable\nvar tododb = sqldb.NewDatabase(\"todo\", sqldb.DatabaseConfig{\n  Migrations: \"./migrations\",\n})\n\n// Then, query the database using db.QueryRow, db.Exec, etc.\n-- todo/migrations/1_create_table.up.sql --\nCREATE TABLE todo_item (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  done BOOLEAN NOT NULL DEFAULT false\n  -- etc...\n);\n```\n\n## Querying databases\n\nTo query a database in your application, you similarly need to import `encore.dev/storage/sqldb` in your service package or sub-package.\n\nFor example, to read a single todo item in the example schema above, we can use `sqldb.QueryRow`:\n\n```go\nvar item struct {\n    ID int64\n    Title string\n    Done bool\n}\nerr := tododb.QueryRow(ctx, `\n    SELECT id, title, done\n    FROM todo_item\n    LIMIT 1\n`).Scan(&item.ID, &item.Title, &item.Done)\n```\n\nIf `QueryRow` does not find a matching row, it reports an error that can be checked against\nby importing the standard library `errors` package and calling `errors.Is(err, sqldb.ErrNoRows)`.\n\nLearn more in the [package docs](https://pkg.go.dev/encore.dev/storage/sqldb).\n\n## Provisioning databases\n\nEncore automatically provisions databases to match what your application requires.\nWhen you [define a database](#creating-a-database), Encore will provision the database at your next deployment.\n\nEncore provisions databases in an appropriate way depending on the environment.\nWhen running locally, Encore creates a database cluster using [Docker](https://www.docker.com/).\nIn the cloud, it depends on the [environment type](/docs/platform/deploy/environments#environment-types):\n\n- In `production` environments, the database is provisioned through the Managed SQL Database\n  service offered by the chosen cloud provider.\n- In `development` environments, the database is provisioned as a Kubernetes deployment\n  with a persistent disk attached.\n\nSee exactly what is provisioned for each cloud provider, and each environment type, in the [infrastructure documentation](/docs/platform/infrastructure/infra).\n\n## Connecting to databases\n\nIt's often useful to be able to connect to the database from outside the backend application. For example for scripts, ad-hoc querying, or dumping data for analysis.\n\nCurrently Encore does not expose user credentials for databases in the local environment or for environments on Encore Cloud. You can use a connection string to connect instead, see below.\n\n### Using the Encore CLI\n\nEncore's CLI comes with built-in support for connecting to databases:\n\n* `encore db shell <database-name> [--env=<name>]` opens a [psql](https://www.postgresql.org/docs/current/app-psql.html)\n  shell to the database named `<database-name>` in the given environment. Leaving out `--env` defaults to the local development environment. `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with.\n\n* `encore db conn-uri <database-name> [--env=<name>]` outputs a connection string for the database named `<database-name>`.\n  When specifying a cloud environment, the connection string is temporary. Leaving out `--env` defaults to the local development environment.\n\n* `encore db proxy [--env=<name>]` sets up a local proxy that forwards any incoming connection\n  to the databases in the specified environment.\n  Leaving out `--env` defaults to the local development environment.\n\nSee `encore help db` for more information on database management commands.\n\n### Using database user credentials\n\nFor cloud environments on AWS/GCP you can view database user credentials (created by Encore when provisioning databases) via the Encore Cloud dashboard:\n\n* Open your app in the [Encore Cloud dashboard](https://app.encore.cloud), navigate to the **Infrastructure** page for the appropriate environment, and locate the `USERS` section within the relevant **Database Cluster**.\n\n## Handling migration errors\n\nWhen Encore applies database migrations, there's always a possibility the migrations don't apply cleanly.\n\nThis can happen for many reasons:\n- There's a problem with the SQL syntax in the migration\n- You tried to add a `UNIQUE` constraint but the values in the table aren't actually unique\n- The existing database schema didn't look like you thought it did, so the database object you tried to change doesn't actually exist\n- ... and so on\n\nIf that happens, Encore rolls back the migration. If it happens during a cloud deployment, the deployment is aborted.\nOnce you fix the problem, re-run `encore run` (locally) or push the updated code (in the cloud) to try again.\n\nEncore tracks which migrations have been applied in the `schema_migrations` table:\n\n```sql\ndatabase=# \\d schema_migrations\n          Table \"public.schema_migrations\"\n Column  |  Type   | Collation | Nullable | Default\n---------+---------+-----------+----------+---------\n version | bigint  |           | not null |\n dirty   | boolean |           | not null |\nIndexes:\n    \"schema_migrations_pkey\" PRIMARY KEY, btree (version)\n```\n\nThe `version` column tracks which migration was last applied. If you wish to skip a migration or re-run a migration,\nchange the value in this column. For example, to re-run the last migration, run `UPDATE schema_migrations SET version = version - 1;`.\n*Note that Encore does not use the `dirty` flag by default.*\n"
  },
  {
    "path": "docs/go/primitives/defining-apis.md",
    "content": "---\nseotitle: Defining type-safe APIs with Encore.go\nseodesc: Learn how to create APIs for your cloud backend application using Go and Encore.go\ntitle: Defining Type-Safe APIs\nsubtitle: Simplifying type-safe API development\nlang: go\n---\n\nEncore.go enables you to create type-safe APIs from regular Go functions.\n\nTo define an API, add the `//encore:api` annotation to a function in your code.\nThis tells Encore that the function is an API endpoint and Encore will automatically generate the necessary boilerplate at compile-time.\n\nIn the example below, we define the API endpoint `Ping`, in the `hello` service, which gets exposed as `hello.Ping`.\n\n```go\npackage hello // service name\n\n//encore:api public\nfunc Ping(ctx context.Context, params *PingParams) (*PingResponse, error) {\n    msg := fmt.Sprintf(\"Hello, %s!\", params.Name)\n    return &PingResponse{Message: msg}, nil\n}\n```\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/hello-world\"\n    desc=\"Hello World REST API example application.\"\n/>\n\n## Access controls\n\nWhen you define an API, you have three options for how it can be accessed:\n\n* `//encore:api public` &ndash; defines a public API that anybody on the internet can call.\n* `//encore:api private` &ndash; defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs.\n* `//encore:api auth` &ndash; defines a public API that anybody can call, but requires valid authentication.\n\nYou can optionally send in auth data to `public` and `private` APIs, in which case the auth handler will be used. When used for `private` APIs, they are still not accessible from the outside world.\n\nFor more on defining APIs that require authentication, see the [authentication guide](/docs/go/develop/auth).\n\n## API Schemas\n\n### Request and response schemas\n\nIn the example above we defined an API that uses request and response schemas. The request data is of type `PingParams` and the response data of type `PingResponse`. That means we need to define them like so:\n\n```go\npackage hello // service name\n\n// PingParams is the request data for the Ping endpoint.\ntype PingParams struct {\n    Name string\n}\n\n// PingResponse is the response data for the Ping endpoint.\ntype PingResponse struct {\n    Message string\n}\n\n// Ping is an API endpoint that responds with a simple response.\n// This is exposed as \"hello.Ping\".\n//encore:api public\nfunc Ping(ctx context.Context, params *PingParams) (*PingResponse, error) {\n    msg := fmt.Sprintf(\"Hello, %s!\", params.Name)\n    return &PingResponse{Message: msg}, nil\n}\n```\nRequest and response schemas are both optional. There are four different ways of defining an API:\n\n**Using both request and response data:**<br/>\n`func Foo(ctx context.Context, p *Params) (*Response, error)`\n\n**Only returning a response:**<br/>\n`func Foo(ctx context.Context) (*Response, error)`\n\n**With only request data:**<br/>\n`func Foo(ctx context.Context, p *Params) error`\n\n**Without any request or response data:**<br/>\n`func Foo(ctx context.Context) error`\n\nAs you can see, two parts are always present: the `ctx context.Context` parameter and the `error` return value.\n\nThe `ctx` parameter is used for *cancellation*. It lets you detect when the caller is no longer interested in the result,\nand lets you abort the request processing and save resources that nobody needs.\n[Learn more about contexts on the Go blog](https://blog.golang.org/context).\n\nThe `error` return type is always required because APIs can always fail from the caller's perspective.\nTherefore even though our simple `Ping` API endpoint above never fails in its implementation, from the perspective of the caller perhaps the service is crashing or the network is down and the service cannot be reached.\n\nThis approach is simple but very powerful. It lets Encore use [static analysis](/docs/go/concepts/application-model)\nto understand the request and response schemas of all your APIs, which enables Encore to automatically generate API documentation, type-safe API clients, and much more.\n\n### Request and response data types\n\nRequest and response data types are structs (or pointers to structs) with optional field tags, which Encore uses to encode API requests to HTTP messages. The same struct can be used for requests and responses, but the `query` tag is ignored when generating responses.\n\nAll tags except `json` are ignored for nested tags, which means you can only define `header` and `query` parameters for root level fields.\n\nFor example, this struct:\n```go\ntype NestedRequestResponse struct {\n\tHeader string `header:\"X-Header\"`// this field will be read from the http header\n\tQuery  string `query:\"query\"`// this field will be read from the query string\n\tBody1  string `json:\"body1\"`\n\tNested struct {\n\t    Header2 string `header:\"X-Header2\"`// this field will be read from the body\n\t\tQuery2  string `query:\"query2\"`// this field will be read from the body\n\t\tBody2   string `json:\"body2\"`\n    } `json:\"nested\"`\n}\n```\n\nWould be unmarshalled from this request:\n\n```output\nPOST /example?query=a%20query HTTP/1.1\nContent-Type: application/json\nX-Header: A header\n\n{\n   \"body1\": \"a body\",\n   \"nested\": {\n      \"Header2\": \"not a header\",\n      \"Query2\": \"not a query\",\n      \"body2\": \"a nested body\"\n   }\n}\n\n```\n\nAnd marshalled to this response:\n\n```output\nHTTP/1.1 200 OK\nContent-Type: application/json\nX-Header: A header\n\n{\n   \"Query\": \"not a query\",\n   \"body1\": \"a body\",\n   \"nested\": {\n      \"Header2\": \"not a header\",\n      \"Query2\": \"not a query\",\n      \"body2\": \"a nested body\"\n   }\n}\n\n```\n\n### Path parameters\n\nPath parameters are specified by the `path` field in the `//encore:api` annotation.\nTo specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature.\nEncore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path\ncan be parsed as a wildcard parameter by using `*name` with a matching function parameter.\n\n```go\n// GetBlogPost retrieves a blog post by id.\n//encore:api public method=GET path=/blog/:id/*path\nfunc GetBlogPost(ctx context.Context, id int, path string) (*BlogPost, error) {\n    // Use id to query database...\n}\n```\n\n### Fallback routes\n\nEncore supports defining fallback routes that will be called if no other endpoint matches the request,\nusing the syntax `path=/!fallback`.\n\nThis is often useful when migrating an existing backend service over to Encore, as it allows you to gradually\nmigrate endpoints over to Encore while routing the remaining endpoints to the existing HTTP router using\na raw endpoint with a fallback route.\n\nFor example:\n\n```go\n//encore:service\ntype Service struct {\n\toldRouter *gin.Engine // existing HTTP router\n}\n\n// Route all requests to the existing HTTP router if no other endpoint matches.\n//encore:api public raw path=/!fallback\nfunc (s *Service) Fallback(w http.ResponseWriter, req *http.Request) {\n    s.oldRouter.ServeHTTP(w, req)\n}\n```\n\n### Headers\n\nHeaders are defined by the `header` field tag, which can be used in both request and response data types. The tag name is used to translate between the struct field and http headers.\nIn the example below, the `Language` field of `ListBlogPost` will be fetched from the\n`Accept-Language` HTTP header.\n\n```go\ntype ListBlogPost struct {\n    Language string `header:\"Accept-Language\"`\n    Author      string // Not a header\n}\n```\n\n### Cookies\n\nCookies can be set in the response by using the `header` tag with the `Set-Cookie` header name.\n\n```go\ntype LoginResponse struct {\n    SessionID string `header:\"Set-Cookie\"`\n}\n\n//encore:api public method=POST path=/login\nfunc Login(ctx context.Context) (*LoginResponse, error) {\n    return &LoginResponse{SessionID: \"session=123\"}, nil\n}\n````\n\nThe cookies can then be read using e.g. [structured auth data](/docs/go/develop/auth#accepting-structured-auth-information).\n\n### Query parameters\n\nFor `GET`, `HEAD` and `DELETE` requests, parameters are read from the query string by default.\nThe query parameter name defaults to the [snake-case](https://en.wikipedia.org/wiki/Snake_case)\nencoded name of the corresponding struct field (e.g. BlogPost becomes blog_post).\n\nThe `query` field tag can be used to parse a field from the query string for other HTTP methods (e.g. POST) and to override the default parameter name.\n\nQuery strings are not supported in HTTP responses and therefore `query` tags in response types are ignored.\n\nIn the example below, the `PageLimit` field will be read from the `limit` query\nparameter, whereas the `Author` field will be parsed from the query string (as `author`) only if the method of\nthe request is `GET`, `HEAD` or `DELETE`.\n\n```go\ntype ListBlogPost struct {\n    PageLimit  int `query:\"limit\"` // always a query parameter\n    Author     string              // query if GET, HEAD or DELETE, otherwise body parameter\n}\n```\n\nWhen fetching data with `GET` endpoints, it's common to receive additional parameters for optional behavior, like filtering a list or changing the sort order.\n\nWhen you use a struct type as the last argument in the function signature,\nEncore automatically parses these fields from the HTTP query string (for the `GET`, `HEAD`, and `DELETE` methods).\n\nFor example, if you want to have a `ListBlogPosts` endpoint:\n\n```go\ntype ListParams struct {\n    Limit uint // number of blog posts to return\n    Offset uint // number of blog posts to skip, for pagination\n}\n\ntype ListResponse struct {\n    Posts []*BlogPost\n}\n\n//encore:api public method=GET path=/blog\nfunc ListBlogPosts(ctx context.Context, opts *ListParams) (*ListResponse, error) {\n    // Use limit and offset to query database...\n}\n```\n\nThis could then be queried as `/blog?limit=10&offset=20`.\n\nQuery parameters are more limited than structured JSON data, and can only consist of basic types (`string`, `bool`, integer and floating point numbers), [Encore's UUID types](https://pkg.go.dev/encore.dev/types/uuid#UUID), and slices of those types.\n\n\n### Body parameters\n\nEncore will default to reading request parameters from the body (as JSON) for all HTTP methods except `GET`, `HEAD` or\n`DELETE`. The name of the body parameter defaults to the field name, but can be overridden by the\n`json` tag. Response fields will be serialized as JSON in the HTTP body unless the `header` tag is set.\n\nThere is no tag to force a field to be read from the body, as some infrastructure entities\ndo not support body content in `GET`, `HEAD` or `DELETE` requests.\n\n```go\ntype CreateBlogPost struct {\n    Subject    string `json:\"limit\"` // query if GET, HEAD or DELETE, otherwise body parameter\n    Author     string                // query if GET, HEAD or DELETE, otherwise body parameter\n}\n```\n\n### Optional types\n\nEncore supports optional types using the `option.Option[T]` type from the `encore.dev/types/option` package.\nThis can be used in request and response schemas to indicate that the value is not always set.\n\nSee the [package documentation](https://pkg.go.dev/encore.dev/types/option) for more information on usage.\n\n### Supported types\nThe table below lists the data types supported by each HTTP message location.\n\n| Type             | Header | Path | Query | Body |\n| ---------------- | ------ | ---- | ----- | ---- |\n| bool             | X      | X    | X     | X    |\n| numeric          | X      | X    | X     | X    |\n| string           | X      | X    | X     | X    |\n| time.Time        | X      | X    | X     | X    |\n| uuid.UUID        | X      | X    | X     | X    |\n| json.RawMessage  | X      | X    | X     | X    |\n| option.Option[T] | X      |      | X     | X    |\n| pointer          | X      |      | X     | X    |\n| list             | X      |      | X     | X    |\n| struct           |        |      |       | X    |\n| map              |        |      |       | X    |\n\n## Sensitive data\n\nEncore.go comes with built-in tracing functionality that automatically captures request and response payloads\nto simplify debugging. While helpful, that's not always desirable. For instance when a request or response payload contains sensitive data, such\nas API keys or personally identifiable information (PII).\n\nFor those use cases Encore supports marking a field as sensitive using the struct tag `encore:\"sensitive\"`.\nEncore's tracing system will automatically redact fields tagged as sensitive. This works for both individual\nvalues as well as nested fields.\n\nNote that inputs to [auth handlers](/docs/go/develop/auth) are automatically marked as sensitive and are always redacted.\n\nRaw endpoints lack a schema, which means there's no way to add a struct tag to mark certain data as sensitive.\nFor this reason Encore supports tagging the whole API endpoint as sensitive by adding `sensitive` to the `//encore:api` annotation.\nThis will cause the whole request and response payload to be redacted, including all request and response headers.\n\n<Callout type=\"info\">\n\nThe `encore:\"sensitive\"` tag is ignored for local development environments to make development and debugging with the Local Development Dashboard easier.\n\n</Callout>\n\n\n### Example\n\n```go\npackage blog // service name\nimport (\n\t\"time\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Updates struct {\n\tAuthor      string `json:\"author,omitempty\"`\n\tPublishTime time.Time `json:\"publish_time,omitempty\"`\n}\n\n// BatchUpdateParams is the request data for the BatchUpdate endpoint.\ntype BatchUpdateParams struct {\n\tRequester     string    `header:\"X-Requester\"`\n\tRequestTime   time.Time `header:\"X-Request-Time\"`\n\tCurrentAuthor string    `query:\"author\"`\n\tUpdates       *Updates  `json:\"updates\"`\n\tMySecretKey   string    `encore:\"sensitive\"`\n}\n\n// BatchUpdateResponse is the response data for the BatchUpdate endpoint.\ntype BatchUpdateResponse struct {\n\tServedBy   string       `header:\"X-Served-By\"`\n\tUpdatedIDs []uuid.UUID  `json:\"updated_ids\"`\n}\n\n//encore:api public method=POST path=/section/:sectionID/posts\nfunc BatchUpdate(ctx context.Context, sectionID string, params *BatchUpdateParams) (*BatchUpdateResponse, error) {\n\t// Update blog posts for section\n\treturn &BatchUpdateResponse{ServedBy: hostname, UpdatedIDs: ids}, nil\n}\n\n```\n\n## REST APIs\nEncore has support for RESTful APIs and lets you easily define resource-oriented API URLs, parse parameters out of them, and more.\n\nTo create a REST API, start by defining an endpoint and specify the `method` and `path` fields in the `//encore:api` comment.\n\nTo specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature. Encore parses the incoming request URL and makes sure it matches the type of the parameter.\n\nFor example, if you want to have a `GetBlogPost` endpoint that takes a numeric id as a parameter:\n\n```go\n// GetBlogPost retrieves a blog post by id.\n//encore:api public method=GET path=/blog/:id\nfunc GetBlogPost(ctx context.Context, id int) (*BlogPost, error) {\n    // Use id to query database...\n}\n```\n\nYou can also combine path parameters with body payloads. For example, if you want to have an `UpdateBlogPost` endpoint:\n\n```go\n// UpdateBlogPost updates an existing blog post by id.\n//encore:api public method=PUT path=/blog/:id\nfunc UpdateBlogPost(ctx context.Context, id int, post *BlogPost) error {\n    // Use `post` to update the blog post with the given id.\n}\n```\n\n<Callout type=\"important\">\n\nYou cannot define paths that conflict with each other, including paths\nwhere the static part can be mistaken for a parameter, e.g both `/blog` and `/blog/:id` would conflict with `/:username`.\n\n</Callout>\n\nAs a rule of thumb, try to place path parameters at the end of the path and\nprefix them with the service name, e.g:\n\n```\nGET /blog/posts\nGET /blog/posts/:id\nGET /user/profile/:username\nGET /user/me\n```\n\n## Custom HTTP status codes\n\nBy default, Encore automatically sets appropriate HTTP status codes for your API responses. We recommend using these default status codes, but there are situations where you might need to set a custom HTTP status code, such as when porting an existing API that clients depend on for specific status codes.\n\nTo set a custom HTTP status code, use the `encore:\"httpstatus\"` struct tag on a field in your response type:\n\n```go\ntype Response struct {\n    Message string `json:\"message\"`\n    Status  int    `encore:\"httpstatus\"`\n}\n\n//encore:api public method=GET path=/example\nfunc Example(ctx context.Context) (*Response, error) {\n    return &Response{\n        Message: \"Hello\",\n        Status:  201, // HTTP 201 Created\n    }, nil\n}\n```\n\nThe field with the `encore:\"httpstatus\"` tag can be an integer type and should contain a valid HTTP status code value.\n"
  },
  {
    "path": "docs/go/primitives/insert-test-data-db.md",
    "content": "---\nseotitle: How to insert test data in a database\nseodesc: Learn how to populate your database with test data using Go and Encore, making testing your backend application much simpler.\ntitle: Insert test data in a database\nlang: go\n---\n\nWhen you're developing or testing, it's often useful to seed databases with test data.\nThis can be done is several ways depending on your use case.\n\n## Using go:embed\n\nA straightforward way to insert test data is to conditionally insert it on startup using `go:embed` in combination with Encore's [metadata API](/docs/go/develop/metadata) control in which environments the data gets inserted. E.g. only in your local environment.\n\n### Example\n\nCreate a file with your test data named `fixtures.sql`.\nThen, for the service where you want to insert test data, add the following to its `.go` file in order to run on startup.\n\n```\nimport (\n    _ \"embed\"\n    \"log\"\n\n    \"encore.dev\"\n)\n\n//go:embed fixtures.sql\nvar fixtures string\n\nfunc init() {\n    if encore.Meta().Environment.Cloud == encore.CloudLocal {\n        if _, err := sqldb.Exec(context.Background(), fixtures); err != nil {\n            log.Fatalln(\"unable to add fixtures:\", err)\n        }\n    }\n}\n```\n\nNot included in the above example is preventing adding duplicate data. This is straightforward to do by making the fixtures idempotent, or by tracking it with a database table.\n\n## Populating databases in Encore Cloud's Preview Environments\n\nIf you are using Encore Cloud's Preview Environment, it can sometimes be useful to populate new Preview Environments with test data to simplify testing. \n\nThe best way to do this depends a bit on your use case, but a common way to do this is by using Encore's [webhooks](/docs/platform/integrations/webhooks) functionality, which provides notifications for when a deployment is completed and includes information about the environment in question.\n"
  },
  {
    "path": "docs/go/primitives/object-storage.md",
    "content": "---\nseotitle: Using Object Storage in your backend application\nseodesc: Learn how you can use Object Storage to store files and unstructured data in your backend application.\ntitle: Object Storage\nsubtitle: Simple and scalable storage APIs for files and unstructured data\ninfobox: {\n  title: \"Object Storage\",\n  import: \"encore.dev/storage/objects\",\n}\nlang: go\n---\n\nObject Storage is a simple and scalable solution to store files and unstructured data in your backend application.\n\nThe most common implementation is Amazon S3 (\"Simple Storage Service\") and its semantics are universally supported by every major cloud provider.\n\nEncore.go provides a cloud-agnostic API for working with Object Storage, allowing you to store and retrieve files with ease. It has support for Amazon S3, Google Cloud Storage, as well as any other S3-compatible implementation (such as DigitalOcean Spaces, MinIO, etc.).\n\nAdditionally, when you use Encore's Object Storage API you also automatically get:\n\n* Automatic tracing and instrumentation of all Object Storage operations\n* Built-in local development support, storing objects on the local filesystem\n* Support for integration testing, using a local, in-memory storage backend\n\n## Creating a Bucket\n\nThe core of Object Storage is the **Bucket**, which represents a collection of files.\nIn Encore, buckets must be declared as package level variables, and cannot be created inside functions.\nRegardless of where you create a bucket, it can be accessed from any service by referencing the variable it's assigned to.\n\nWhen creating a bucket you can configure additional properties, like whether the objects in the bucket should be versioned.\n\nSee the complete specification in the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#NewBucket).\n\nFor example, to create a bucket for storing profile pictures:\n\n```go\npackage user\n\nimport \"encore.dev/storage/objects\"\n\nvar ProfilePictures = objects.NewBucket(\"profile-pictures\", objects.BucketConfig{\n\tVersioned: false,\n})\n```\n\n## Uploading files\n\nTo upload a file to a bucket, use the `Upload` method on the bucket variable.\nIt returns a writer that you can use to write the contents of the file.\n\nTo complete the upload, call the `Close` method on the writer.\nTo abort the upload, either cancel the context or call the `Abort` method on the writer.\n\nThe `Upload` method additionally takes a set of options to configure the upload,\nlike setting attributes (`objects.WithUploadAttrs`) or to reject the upload if the\nobject already exists (`objects.WithPreconditions`).\nSee the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#Bucket.Upload) for more details.\n\n```go\npackage user\n\nimport (\n\t\t\"context\"\n\t\t\"io\"\n\t\t\"net/http\"\n\n\t\t\"encore.dev/beta/auth\"\n\t\t\"encore.dev/beta/errs\"\n\t\t\"encore.dev/storage/objects\"\n)\n\nvar ProfilePictures = objects.NewBucket(\"profile-pictures\", objects.BucketConfig{})\n\n//encore:api auth raw method=POST path=/upload-profile-picture\nfunc UploadProfilePicture(w http.ResponseWriter, req *http.Request) {\n\t// Store the user's profile picture with their user id as the key.\n\tuserID, _ := auth.UserID()\n\tkey := string(userID) // We store the profile\n\n\twriter := ProfilePictures.Upload(req.Context(), key)\n\t_, err := io.Copy(writer, req.Body)\n\tif err != nil {\n\t\t// If something went wrong with copying data, abort the upload and return an error.\n\t\twriter.Abort()\n\t\terrs.HTTPError(w, err)\n\t\treturn\n\t}\n\n\tif err := writer.Close(); err != nil {\n\t\terrs.HTTPError(w, err)\n\t\treturn\n\t}\n\n\t// All good! Return a 200 OK.\n\tw.WriteHeader(http.StatusOK)\n}\n```\n\n## Downloading files\n\nTo download a file from a bucket, use the `Download` method on the bucket variable.\nIt returns a reader that you can use to read the contents of the file.\n\nThe `Download` method additionally takes a set of options to configure the download,\nlike downloading a specific version if the bucket is versioned (`objects.WithVersion`).\nSee the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#Bucket.Download) for more details.\n\nFor example, to download the user's profile picture and serve it:\n\n```go\npackage user\n\nimport (\n\t\t\"context\"\n\t\t\"io\"\n\t\t\"net/http\"\n\n\t\t\"encore.dev\"\n\t\t\"encore.dev/beta/auth\"\n\t\t\"encore.dev/beta/errs\"\n\t\t\"encore.dev/storage/objects\"\n)\n\nvar ProfilePictures = objects.NewBucket(\"profile-pictures\", objects.BucketConfig{})\n\n//encore:api public raw method=GET path=/profile-picture/:userID\nfunc ServeProfilePicture(w http.ResponseWriter, req *http.Request) {\n\tuserID := encore.CurrentRequest().PathParams.Get(\"userID\")\n\treader := ProfilePictures.Download(req.Context(), userID)\n\n\t// Did we encounter an error?\n\tif err := reader.Err(); err != nil {\n\t\terrs.HTTPError(w, err)\n\t\treturn\n\t}\n\n\t// Assuming all images are JPEGs.\n\tw.Header().Set(\"Content-Type\", \"image/jpeg\")\n\tio.Copy(w, reader)\n}\n```\n\n## Listing objects\n\nTo list objects in a bucket, use the `List` method on the bucket variable.\n\nIt returns an iterator of `(error, *objects.ListEntry)` pairs that you can use\nto easily iterate over the objects in the bucket using a `range` loop.\n\nFor example, to list all profile pictures:\n\n```go\nfor err, entry := range ProfilePictures.List(ctx, &objects.Query{}) {\n\tif err != nil {\n\t\t// Handle error\n\t}\n\t// Do something with entry\n}\n```\n\nThe `*objects.Query` type can be used to limit the number of objects returned,\nor to filter them to a specific key prefix.\n\nSee the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#Bucket.List) for more details.\n\n## Deleting objects\n\nTo delete an object from a bucket, use the `Remove` method on the bucket variable.\n\nFor example, to delete a profile picture:\n\n```go\nerr := ProfilePictures.Remove(ctx, \"my-user-id\")\nif err != nil && !errors.Is(err, objects.ErrObjectNotFound) {\n\t// Handle error\n}\n```\n\n## Retrieving object attributes\n\nYou can retrieve information about an object using the `Attrs` method on the bucket variable.\nIt returns the attributes of the object, like its size, content type, and ETag.\n\nFor example, to get the attributes of a profile picture:\n\n```go\nattrs, err := ProfilePictures.Attrs(ctx, \"my-user-id\")\nif errors.Is(err, objects.ErrObjectNotFound) {\n\t// Object not found\n} else if err != nil {\n\t// Some other error\n}\n// Do something with attrs\n```\n\nFor convenience there is also `Exists` which returns a boolean indicating whether the object exists.\n\n```go\nexists, err := ProfilePictures.Exists(ctx, \"my-user-id\")\nif err != nil {\n\t// Handle error\n} else if !exists {\n\t// Object does not exist\n}\n```\n\n## Using Public Buckets\n\nEncore supports creating public buckets where objects can be accessed directly via HTTP/HTTPS without authentication. This is useful for serving static assets like images, videos, or other public files.\n\nTo create a public bucket, set `Public: true` in the `BucketConfig`:\n\n```go\nvar PublicAssets = objects.NewBucket(\"public-assets\", objects.BucketConfig{\n    Public: true,\n})\n```\n\nOnce configured as public, you can get the public URL for any object using the `PublicURL` method:\n\n```go\n// Get the public URL for an object\nurl := PublicAssets.PublicURL(\"path/to/image.jpg\")\n\n// The URL can be used directly or shared publicly\nfmt.Println(url) // e.g. https://assets.example.com/path/to/image.jpg\n```\nWhen self-hosting, see how to configure public buckets in the [infrastructure configuration docs](/docs/ts/self-host/configure-infra).\n\nWhen deploying with Encore Cloud it will automatically configure the bucket to be publicly accessible and [configure CDN](/docs/platform/infrastructure/infra#production-infrastructure) for optimal content delivery.\n\n### Using bucket references\n\nEncore uses static analysis to determine which services are accessing each bucket,\nand what operations each service is performing.\n\nThat information is used to provision infrastructure correctly,\nrender architecture diagrams, and configure IAM permissions.\n\nThis means that `*objects.Bucket` variables can't be passed around however you'd like,\nas it makes static analysis impossible in many cases. To work around these restrictions\nEncore allows you to get a \"reference\" to a bucket that can be passed around any way you want\nby calling `objects.BucketRef`.\n\nTo ensure Encore still is aware of which permissions each service needs, the call to `objects.BucketRef`\nmust be made from within a service. Additionally, it must pre-declare the permissions it needs;\nthose permissions are then assumed to be used by the service.\n\nIt looks like this (using the `ProfilePictures` topic above):\n\n```go\nref := objects.BucketRef[objects.Downloader](ProfilePictures)\n\n// ref is of type objects.Downloader, which allows downloading.\n```\n\nEncore provides permission interfaces for each operation that can be performed on a bucket:\n\n* `objects.Downloader` for downloading objects\n* `objects.Uploader` for uploading objects\n* `objects.Lister` for listing objects\n* `objects.Attrser` for getting object attributes\n* `objects.Remover` for removing objects\n* `objects.SignedDownloader` for generating signed download URLs for objects\n* `objects.SignedUploader` for generating signed upload URLs for objects\n\nIf you need multiple permissions they can be combined by creating an interface\nthat embeds the permissions you need.\n\n```go\ntype myPerms interface {\n  objects.Downloader\n  objects.Uploader\n}\nref := objects.BucketRef[myPerms](ProfilePictures)\n```\n\nFor convenience Encore provides an `objects.ReadWriter` interface that gives complete read-write access\nwith all the permissions above.\n\nSee the [package documentation](https://pkg.go.dev/encore.dev/storage/objects#BucketRef) for more details.\n\n## Signed Upload URLs\n\nYou can use `SignedUploadURL` to create signed URLs to allow clients to upload content directly\ninto the bucket over the internet. The URL is always restricted to one filename, and has a set\nexpiration date. Anyone in possession of the URL can upload data under this filename without any\nadditional authentication.\n\n```go\nurl, err := ProfilePictures.SignedUploadURL(ctx, \"my-user-id\", objects.WithTTL(time.Duration(7200)*time.Second))\n// Pass url to client\n```\n\nThe client can now `PUT` to this URL with the content as a binary payload.\n\n```bash\ncurl -X PUT --data-binary @/home/me/dog-wizard.jpeg \"https://storage.googleapis.com/profile-pictures/my-user-id/?x-goog-signature=b7a1<...>\"\n```\n\n### Why signed upload URLs?\n\nSigned URLs are an alternative to accepting the content payload directly in your API. Content\nupload requests are sometimes inconvenient to handle well: they can be long running and very large.\nWith signed URLs, the content flows directly into the storage bucket, and only object IDs and\nmetadata go through your API service.\n\nThe trade-off is that the upload flow becomes more complex from a client point of view.\n\n## Signed Download URLs\n\nYou can use `SignedDownloadURL` to create signed URLs to allow clients to download content directly\nfrom the bucket, even if it's private. The URL is always restricted to one filename, and has a set\nexpiration date. Anyone in possession of the URL can download the file without any additional\nauthentication.\n\n```go\nurl, err := Documents.SignedDownloadURL(ctx, \"letter-1234\", objects.WithTTL(time.Duration(7200)*time.Second))\n// Pass url to client\n```\n\n### Why signed download URLs?\n\nSimilar to the upload case, signed download URLs is a way to avoid handing large files or bulk\ntraffic through your API. With signed URLs, the content flows directly from the storage bucket,\nand only object IDs and metadata go through your API service.\n\nNote: unless the content is private, prefer serving urls with `PublicURL()` over signed URLs.\nPublic URLs go over CDN, which is typically significantly more performant and cost effective.\n\n"
  },
  {
    "path": "docs/go/primitives/pubsub.md",
    "content": "---\nseotitle: Using PubSub in your backend application\nseodesc: Learn how you can use PubSub as an asynchronous message queue in your backend application, a great approach for decoupling services for better reliability.\ntitle: Pub/Sub\nsubtitle: Decoupling services and building asynchronous systems\ninfobox: {\n  title: \"Pub/Sub Messaging\",\n  import: \"encore.dev/pubsub\",\n  example_link: \"/docs/tutorials/uptime\"\n}\nlang: go\n---\n\nPublishers & Subscribers (Pub/Sub) let you build systems that communicate by broadcasting events asynchronously. This is a great way to decouple services for better reliability and responsiveness.\n\nEncore's Backend Framework lets you use Pub/Sub in a cloud-agnostic declarative fashion. At deployment, Encore automatically [provisions the required infrastructure](/docs/platform/infrastructure/infra).\n\n## Creating a Topic\n\nThe core of Pub/Sub is the **Topic**, a named channel on which you publish events.\nTopics must be declared as package level variables, and cannot be created inside functions.\nRegardless of where you create a topic, it can be published to from any service, and subscribed to from any service.\n\nWhen creating a topic, it must be given an event type, a unique name, and a configuration to define its behaviour. See the complete specification in the [package documentation](https://pkg.go.dev/encore.dev/pubsub#NewTopic).\n\nFor example, to create a topic with events about user signups:\n\n```go\npackage user\n\nimport \"encore.dev/pubsub\"\n\ntype SignupEvent struct{ UserID int }\n\nvar Signups = pubsub.NewTopic[*SignupEvent](\"signups\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n```\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/uptime\" \n    desc=\"Event-driven example application using Pub/Sub.\" \n/>\n\n### At-least-once delivery\n\nThe above example configures the topic to ensure that, for each subscription, events will be delivered _at least once_.\n\nThis means that if the topic believes the event was not processed, it will attempt to deliver the message again.\n**Therefore, all subscription handlers should be [idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning).** This helps ensure that if the handler is called two or more times, from the outside there's no difference compared to calling it once.\n\nThis can be achieved using a database to track if you have already performed the action that the event is meant to trigger,\nor ensuring that the action being performed is also idempotent in nature.\n\n### Exactly-once delivery\n\nTopics can also be configured to deliver events _exactly once_ by setting the `DeliveryGuarantee` field to\n`pubsub.ExactlyOnce`. This enables stronger guarantees on the infrastructure level to minimize the likelihood of\nmessage re-delivery.\n\n\nHowever, there are still some rare circumstances when a message might be redelivered.  For example, if a networking issue\ncauses the acknowledgement of successful processing the message to be lost before the cloud provider receives it\n(the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem)).  As such, if correctness is critical\nunder all circumstances, it's still advisable to design your subscription handlers to be idempotent.\n\nBy enabling exactly-once delivery on a topic the cloud provider enforces certain throughput limitations:\n- AWS: 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html)).\n- GCP: At least 3,000 messages per second across all topics in the region (can be higher on the region see [GCP PubSub Quotas](https://cloud.google.com/pubsub/quotas#quotas)).\n\n<Callout type=\"important\">\n\nExactly-once delivery does not perform message deduplication on the publishing side. If  `Publish` is called twice with\nthe same message, the message will be delivered twice.\n\n</Callout>\n\n### Ordered Topics\n\nTopics are unordered by default, meaning that messages can be delivered in any order. This allows for better throughput on the topic as messages can be processed in parallel. However, in some cases, messages must be delivered in the order they were published for a given entity.\n\nTo create an ordered topic, configure the topic's `OrderingAttribute` to match the `pubsub-attr` tag on one of the top-level fields of the event type. This field ensures that messages delivered to the same subscriber are delivered in the order of publishing for that specific field value. Messages with a different value on the ordering attribute are delivered in an unspecified order.\n\nTo maintain topic order, messages with the same ordering key aren't delivered until the earliest message is processed or dead-lettered, potentially causing delays due to [head-of-line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking). Mitigate processing issues by ensuring robust logging and alerts, and appropriate subscription retry policies.\n\n<Callout type=\"info\">\n\nThe `OrderingAttribute` currently has no effect in local environments.\n\n</Callout>\n\n#### Throughput limitations\n\nEach cloud provider enforces certain throughput limitations for ordered topics:\n- **AWS:** 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html))\n- **GCP:** 1 MBps for each ordering key (See [GCP Pub/Sub Resource Limits](https://cloud.google.com/pubsub/quotas#resource_limits))\n\n#### Ordered topic example\n\n```go\npackage example\n\nimport (\n\t\"context\"\n\t\"encore.dev/pubsub\"\n)\n\ntype CartEvent struct {\n\tShoppingCartID int `pubsub-attr:\"cart_id\"`\n\tEvent          string\n}\n\nvar CartEvents = pubsub.NewTopic[*CartEvent](\"cart-events\", pubsub.TopicConfig{\n\tDeliveryGuarantee: pubsub.AtLeastOnce,\n\tOrderingAttribute: \"cart_id\",\n})\n\nfunc Example(ctx context.Context) error {\n\t// These are delivered in order as they all have the same shopping cart ID\n\tCartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 1, Event: \"item_added\"})\n\tCartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 1, Event: \"checkout_started\"})\n\tCartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 1, Event: \"checkout_completed\"})\n\n\t// This event may be delivered at any point as it has a different shopping cart ID\n\tCartEvents.Publish(ctx, &CartEvent{ShoppingCartID: 2, Event: \"item_added\"})\n}\n```\n\n## Publishing events\n\nTo publish an **Event**, call `Publish` on the topic passing in the event object (which is the type specified in the `pubsub.NewTopic[Type]` constructor).\n\nFor example:\n\n```go\nmessageID, err := Signups.Publish(ctx, &SignupEvent{UserID: id})\nif err != nil {\n    return err\n}\n\n// If we get here the event has been successfully published,\n// and all registered subscribers will receive the event.\n\n// The messageID variable contains the unique id of the message,\n// which is also provided to the subscribers when processing the event.\n```\n\nBy defining the `Signups` topic variable as an exported variable\nyou can also publish to the topic from other services in the same way.\n\n### Using topic references\n\nEncore uses static analysis to determine which services are publishing messages\nto what topics. That information is used to provision infrastructure correctly,\nrender architecture diagrams, and configure IAM permissions.\n\nThis means that `*pubsub.Topic` variables can't be passed around however you'd like,\nas it makes static analysis impossible in many cases. To work around these restrictions\nEncore allows you to get a reference to a topic that can be passed around any way you want.\n\nIt looks like this (using the `Signups` topic above):\n\n```go\nsignupRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](Signups)\n\n// signupRef is of type pubsub.Publisher[*SignupEvent], which allows publishing.\n```\n\nThe difference between a **TopicRef** and a **Topic** is that topic references need to pre-declare\nwhat permissions are needed. Encore then assumes that all the permissions you declare are used.\n\nFor example, if you declare a **TopicRef** with the `pubsub.Publisher` permission (as seen above)\nEncore assumes that the service will publish messages to the topic and provisions the infrastructure\nto support that.\n\nNote that a **TopicRef** must be declared _within a service_, but the reference itself\ncan be freely passed around to library code, be dependency injected into [service structs](/docs/go/how-to/dependency-injection),\nand so on.\n\n## Subscribing to Events\n\nTo **Subscribe** to events, you create a Subscription as a package level variable by calling the\n[`pubsub.NewSubscription`](https://pkg.go.dev/encore.dev/pubsub#NewSubscription) function.\n\nEach subscription needs:\n- the topic to subscribe to\n- a name which is unique for the topic\n- a configuration object with at least a `Handler` function to process the events\n- a configuration object\n\nHere's an example of how you create a subscription to a topic:\n\n```go\npackage email\n\nimport (\n    \"encore.dev/pubsub\"\n    \"user\"\n)\n\nvar _ = pubsub.NewSubscription(\n    user.Signups, \"send-welcome-email\",\n    pubsub.SubscriptionConfig[*SignupEvent]{\n        Handler: SendWelcomeEmail,\n    },\n)\nfunc SendWelcomeEmail(ctx context.Context, event *SignupEvent) error {\n    // send email...\n    return nil\n}\n```\n\nSubscriptions can be in the same service as the topic is declared, or in any other service of your application. Each\nsubscription to a single topic receives the events independently of any other subscriptions to the same topic. This means\nthat if one subscription is running very slowly, it will grow a backlog of unprocessed events.\nHowever, any other subscriptions will still be processing events in real-time as they are published.\n\nThe `ctx` passed to the handler function is cancelled when the `AckDeadline` for the subscription is reached.\nThis is the time when the message is considered to have timed out and can be redelivered to another subscriber.\nThe timeout defaults to 30 seconds if you don't explicitly configure `AckDeadline`.\n\n### Method-based handlers\n\nWhen using [service structs](/docs/go/primitives/service-structs) for dependency injection\nit's common to want to define the subscription handler as a method on the service struct, to be able to access the\ninjected dependencies. The pubsub package provides the `pubsub.MethodHandler` function for this purpose:\n\n```go\n//encore:service\ntype Service struct { /* ... */ }\n\nfunc (s *Service) SendWelcomeEmail(ctx context.Context, event *SignupEvent) error {\n\t// ...\n}\n\nvar _ = pubsub.NewSubscription(\n  user.Signups, \"send-welcome-email\",\n  pubsub.SubscriptionConfig[*SignupEvent]{\n    Handler: pubsub.MethodHandler((*Service).SendWelcomeEmail),\n  },\n)\n```\n\nNote that `pubsub.MethodHandler` only allows referencing methods on the service struct type, not any other type.\n\n### Subscription configuration\n\nWhen creating a subscription you can configure behavior such as message retention and retry policy, using the `SubscriptionConfig` type. See the [package documentation](https://pkg.go.dev/encore.dev/pubsub#SubscriptionConfig) for the complete configuration options.\n\n<Callout type=\"info\">\n\nThe `SubscriptionConfig` struct fields must be defined as compile-time constants, and cannot be defined in\nterms of function calls. This is necessary for Encore to understand the exact requirements of the subscription, in order to provision the correct infrastructure upon deployment.\n\n</Callout>\n\n### Error Handling\n\nIf a subscription function returns an error, the event being processed will be retried, based on the retry policy\n[configured on that subscription](https://pkg.go.dev/encore.dev/pubsub#SubscriptionConfig). After the `MaxRetries` is hit,\nthe event will be placed into a dead-letter queue (DLQ) for that subscriber. This allows the subscription to continue\nprocessing events until the bug which caused the event to fail can be fixed. Once fixed, the messages on the dead-letter queue can be manually released to be processed again by the subscriber.\n\n## Testing Pub/Sub\n\nEncore uses a special testing implementation of Pub/Sub topics. When running tests, topics are aware of which test\nis running. This gives you the following guarantees:\n- Your subscriptions will not be triggered by events published. This allows you to test the behaviour of publishers independently of side effects caused by subscribers.\n- Message ID's generated on publish are deterministic (based on the order of publishing), thus your assertions can make use of that fact.\n- Each test is isolated from other tests, meaning that events published in one test will not impact other tests (even if you use parallel testing).\n\nEncore provides a helper function, [`et.Topic`](https://pkg.go.dev/encore.dev/et#Topic), to access the testing topic. You\ncan use this object to extract the events that have been published to it during a test.\n\nHere's an example implementation:\n\n```go\npackage user\n\nimport (\n    \"testing\"\n\n    \"encore.dev/et\"\n    \"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_Register(t *testing.T) {\n    t.Parallel()\n\n    ... Call Register() and assert changes to the database ...\n\n    // Get all published messages on the Signups topic from this test.\n    msgs := et.Topic(Signups).PublishedMessages()\n    assert.Len(t, msgs, 1)\n}\n```\n\n## Ensuring consistency between services\n\nEnsuring consistency between services in event-driven applications can be challenging, especially when database writes and Pub/Sub publishing are not transactional. This can lead to inconsistencies between services.\n\nTo address this issue without adding excessive complexity, consider using a transactional outbox pattern. For more information on implementing this pattern with Encore, see the [Pub/Sub Outbox guide](/docs/primitives/pubsub-outbox).\n\n## The benefits of Pub/Sub\n\nPub/Sub is a powerful building block in a backend application. It can be used to improve app reliability by reducing the blast radius of faulty components and bottlenecks. It can also be used to increase the speed of response to the user, and even helps reduce cognitive overhead for developers by inverting the dependencies between services.\n\nFor those not familiar with Pub/Sub, lets take a look at an example API in a user registration service.\nThe behavior we want to implement is that upon registration, we send a welcome email to the user and create a record of the signup in our analytics system. Now let's see how we could implement this only using APIs, compared to how a Pub/Sub implementation might look.\n\n### An API only approach\n\nUsing API calls between services, we might design a system which looks like this when the user registers:\n\n<div className=\"grid grid-cols-3 mobile:grid-cols-1 grid-flow-row\">\n\n\n<img src=\"/assets/docs/pubsub-rpc-example.png\" className=\"noshadow w-100\" />\n\n<div className=\"col-span-2\">\n\n1. The `user` service starts a database transaction and records the user in its database.\n2. The `user` service makes a call to the `email` service to send a welcome email.\n3. The `email` service then calls an email provider to actually send the email.\n4. Upon success, the `email` service replies to the `user` service that the request was processed.\n5. The `user` service then calls the `analytics` service to record the signup.\n6. The `analytics` service the writes to the data warehouse to record the information.\n7. The `analytics` service then replies to the `user` service that the request was processed.\n8. The `user` service commits the database transaction.\n9. The `user` service then can reply to the user to say the registration was successful.\n\n</div>\n\n</div>\n\nNotice how we have to wait for everything to complete before we can reply to the user to tell then we've registered them.\nThis means that if our email provider takes 3 seconds to send the email, we've now taken 3 seconds to respond to the user,\nwhen in reality once the user was written to the database, we could have responded to the user instantly at that point to\nconfirm the registration.\n\nAnother downside to this approach is if our data warehouse is currently broken and reporting errors, our system will also\nreport errors whenever anybody tries to signup! Given analytics is purely internal and doesn't impact users, why should\nthe analytics system being down impact user signup?\n\n### A Pub/Sub approach\n\nA more ideal solution would be if we could decouple the behaviour of emailing the user and recording our analytics, such that\nthe user service only has to record the user in its own database and let the user know they are registered - without worrying\nabout the downstream impacts. Thankfully, this is exactly what [Pub/Sub topics](https://pkg.go.dev/encore.dev/pubsub#Topic) allow us to do.\n\n<div className=\"grid grid-cols-3 mobile:grid-cols-1 grid-flow-row\">\n\n<div className=\"col-span-2\">\n\nIn this example, when a user registers we:\n\n1. The `user` service starts a database transaction and records the user in its database.\n2. Publish a signup event to the `signups` topic.\n3. Commit the transaction and reply to the user to say the registration was successful.\n\nAt this point the user is free to continue interacting with the application and we've isolated the registration behaviour\nfrom the rest of the application.\n\nIn parallel, the `email` and `analytics` services will receive the signup event from the `signups` topic and will then\nperform their respective tasks. If either service returns an error, the event will automatically be backed off and retried\nuntil the service is able to process the event successfully, or reaches the maximum number of attempts and is placed\ninto the deadletter queue (DLQ).\n\n</div>\n\n<img src=\"/assets/docs/pubsub-topic-example.png\" className=\"noshadow w-100\" />\n\n</div>\n\nNotice how in this version, the processing time of the two other services did not impact the end user and in fact the `user`\nservice is not even aware of the `email` and `analytics` services. This means that new systems which need to know about\nnew users signing up can be added to the application, without the need to change the `user` service or impacting its\nperformance.\n"
  },
  {
    "path": "docs/go/primitives/raw-endpoints.md",
    "content": "---\nseotitle: Raw Endpoints\nseodesc: Learn how to create raw API endpoints for your cloud backend application using Go and Encore.go\ntitle: Raw Endpoints\nsubtitle: Drop down in abstraction to access the raw HTTP request\nlang: go\n---\n\nSometimes you need to operate a lower abstraction than Encore.go normally provides.\nFor example, you might want to access the underlying HTTP request, often useful for things like accepting webhooks.\n\nEncore.go has you covered using \"raw endpoints\".\n\nTo define a raw endpoint, change the `//encore:api` annotation and function signature like so:\n\n```go\npackage service\n\nimport \"net/http\"\n\n// Webhook receives incoming webhooks from Some Service That Sends Webhooks.\n//encore:api public raw\nfunc Webhook(w http.ResponseWriter, req *http.Request) {\n    // ... operate on the raw HTTP request ...\n}\n```\n\nLike any other Encore API endpoint, once deployed this will be exposed at the URL: <br/>\n`https://<env>-<app-id>.encr.app/service.Webhook`. Just like regular endpoints, raw endpoints support the use of `:id` and `*wildcard` segments.\n\nExperienced Go developers will have already noted this is just a regular Go HTTP handler.\n(See the <a href=\"https://pkg.go.dev/net/http#Handler\" target=\"_blank\" rel=\"nofollow\">net/http documentation</a> for how Go HTTP handlers work.)\n\nLearn more about receiving webhooks and using WebSockets in the [receiving regular HTTP requests guide](/docs/go/how-to/http-requests).\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/slack-bot\" \n    desc=\"Slack Bot example application that uses Raw endpoints to accept webhooks.\" \n/>\n"
  },
  {
    "path": "docs/go/primitives/secrets.md",
    "content": "---\nseotitle: Securely storing API keys and secrets\nseodesc: Learn how to store API keys, and secrets, securely for your backend application. Encore's built in vault makes it simple to keep your app secure.\ntitle: Storing Secrets and API keys\nsubtitle: Simply storing secrets securely\nlang: go\n---\n\nWouldn't it be nice to store secret values like API keys, database passwords, and private keys directly in the source code?\nOf course, we can’t do that &ndash; it's horrifyingly insecure!\n(Unfortunately, it's also [very common](https://www.ndss-symposium.org/ndss-paper/how-bad-can-it-git-characterizing-secret-leakage-in-public-github-repositories/).)\n\nEncore's built-in secrets manager makes it simple to store secrets in a secure way and lets you use them in your program like regular variables.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/slack-bot\"\n    desc=\"Slack Bot example application using secrets to store a Slack key.\"\n/>\n\n## Using secrets in your application\n\nTo use a secret in your application, first define it directly in your code by creating an unexported struct named `secrets`, where all fields are of type `string`. For example:\n\n```go\nvar secrets struct {\n    SSHPrivateKey string    // ed25519 private key for SSH server\n    GitHubAPIToken string   // personal access token for deployments\n    // ...\n}\n```\n\nWhen you've defined secrets in your program, the Encore compiler will check that they are set before running or deploying your application. If a secret is not set, you will get a compilation error notifying you that a secret value is missing.\n\nOnce you've provided values for all secrets, you can just use them in your application like a regular variable. For example:\n\n```go\nfunc callGitHub(ctx context.Context) {\n    req, _ := http.NewRequestWithContext(ctx, \"GET\", \"https:///api.github.com/user\", nil)\n    req.Header.Add(\"Authorization\", \"token \" + secrets.GitHubAPIToken)\n    resp, err := http.DefaultClient.Do(req)\n    // ... handle err and resp\n}\n```\n\n<Callout type=\"info\">\n\nSecret keys are globally unique for your whole application. If multiple services use the same secret name they both receive the same secret value at runtime.\n\n</Callout>\n\n## Storing secret values\n\n### Using the Encore Cloud dashboard\n\nThe simplest way to set up secrets is with the Secrets Manager in the Encore Cloud dashboard. Open your app in [app.encore.cloud](https://app.encore.cloud), go to **Settings** in the main navigation, and then click on **Secrets** in the settings menu.\n\nFrom here you can create secrets, save secret values, and configure different values for different environments.\n\n<img src=\"/assets/docs/secrets.png\" title=\"Encore's Secrets Manager\"/>\n\n### Using the CLI\n\nIf you prefer, you can also set up secrets from the CLI using:<br/> `encore secret set --type <types> <secret-name>`\n\n`<types>` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`.\n\nFor example `encore secret set --type prod SSHPrivateKey` sets the secret value for production environments,<br/> and `encore secret set --type dev,preview,local GitHubAPIToken` sets the secret value for development, preview, and local environments.\n\nIn some cases, it can be useful to define a secret for a specific environment instead of an environment type.\nYou can do so with `encore secret set --env <env-name> <secret-name>`. Secret values for specific environments\ntake precedence over values for environment types.\n\n### Environment settings\n\nEach secret can only have one secret value for each environment type. For example: If you have a secret value that's shared between `development`, `preview` and `local`, and you want to override the value for `local`, you must first edit the existing secret and remove `local` using the Secrets Manager in the [Encore Cloud dashboard](https://app.encore.cloud). You can then add a new secret value for `local`. The end result should look something like the picture below.\n\n<img src=\"/assets/docs/secretoverride.png\" title=\"Overriding a secret in Encore's Secrets Manager\"/>\n\n## How it works: Where secrets are stored\n\nWhen you store a secret Encore stores it encrypted using Google Cloud Platform's [Key Management Service](https://cloud.google.com/security-key-management) (KMS).\n\n- **Production / Your own cloud:** When you deploy to production using your own cloud account on GCP or AWS, Encore provisions a secrets manager in your account (using either KMS or AWS Secrets Manager) and replicates your secrets to it. The secrets are then injected into the container using secret environment variables.\n- **Local:** For local secrets Encore automatically replicates them to developers' machines when running `encore run`.\n- **Development / Encore Cloud:** Environments on Encore's development cloud (running on GCP under the hood) work the same as self-hosted GCP environments, using GCP Secrets Manager.\n\n### Overriding local secrets\n\nWhen setting secrets via the `encore secret set` command, they are automatically synced to all developers\nworking on the same application, courtesy of Encore Cloud.\n\nIn some cases, however, you want to override a secret only for your local machine.\nThis can be done by creating a file named `.secrets.local.cue` in the root of your Encore application,\nnext to the `encore.app` file.\n\nThe file contains key-value pairs of secret names to secret values. For example:\n\n```cue\nGitHubAPIToken: \"my-local-override-token\"\nSSHPrivateKey: \"custom-ssh-private-key\"\n```\n"
  },
  {
    "path": "docs/go/primitives/service-structs.md",
    "content": "---\nseotitle: Service Structs\nseodesc: Learn how to use service structs to define APIs as methods.\ntitle: Service structs\nlang: go\n---\n\nEncore lets you define a type, called a service struct, to represent your running service. This lets you define an initialization function (similar to the `main` function in regular Go programs).\n\nYou can also define API endpoints as methods on the service struct type, enabling you to use [dependency injection](/docs/go/how-to/dependency-injection) for testing purposes.\n\nIt works by defining a struct type of your choice (typically called `Service`)\nand declaring it with `//encore:service`.\nThen, you can define a special function named `initService`\n(or `initWhatever` if you named the type `Whatever`)\nthat gets called by Encore to initialize your service when it starts up.\n\nIt looks like this:\n```go\n//encore:service\ntype Service struct {\n\t// Add your dependencies here\n}\n\nfunc initService() (*Service, error) {\n\t// Write your service initialization code here.\n}\n\n//encore:api public\nfunc (s *Service) MyAPI(ctx context.Context) error {\n\t// ...\n}\n```\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/uptime\" \n    desc=\"Event-driven example application using service structs.\" \n/>\n\n## Calling APIs defined on service structs\n\nWhen using a service struct like above, Encore will create a file named `encore.gen.go`\nin your service directory. This file contains package-level functions for the APIs defined\nas methods on the service struct. In the example above, you would see:\n\n```go\n// Code generated by encore. DO NOT EDIT.\n\npackage email\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\nfunc Send(ctx context.Context, p *SendParams) error {\n\t// The implementation is elided here, and generated at compile-time by Encore.\n\treturn nil\n}\n```\n\nThese functions are generated in order to allow other services to keep calling your\nAPIs as package-level functions, in the same way as before: `email.Send(...)`.\nThis means other services do not need to care about whether you're using Dependency Injection\ninternally. You must always use these generated package-level functions for making API calls.\n\n<Callout type=\"info\">\n\nEncore will automatically generate these files and keep them up to date\nwhenever your code changes. There is no need to manually invoke anything\nto regenerate this code.\n\n</Callout>\n\nEncore adds all `encore.gen.go` files to your `.gitignore` since you typically\ndon't want to commit them to your repository; doing so ends up creating\na lot of unnecessary merge conflicts.\n\nHowever, in some cases when running third-party linters in a CI/CD environment\nit can be helpful to generate these wrappers to make the linter happy.\nYou can do that by invoking `encore gen wrappers`.\n\n## Graceful Shutdown\n\nWhen defining a service struct, Encore supports notifying\nyour service when it's time to gracefully shut down. This works\nby having your service struct implement the method\n`func (s *Service) Shutdown(force context.Context)`.\n\nIf that method exists, Encore will call it when it's time to begin\ngracefully shutting down. Initially the shutdown is in \"graceful mode\",\nwhich means that you have a few seconds to complete ongoing work.\n\nThe provided `force` context is canceled when the graceful shutdown window\nis over, and it's time to forcefully shut down. How much time you have\nfrom when `Shutdown` is called to when forceful shutdown begins depends on the\ncloud provider and the underlying infrastructure. Typically it's in the range 5-30 seconds.\n\n<Callout type=\"info\">\n\nEncore automatically handles graceful shutdown of all Encore-managed\nfunctionality, such as HTTP servers, database connection pools,\nPub/Sub message receivers, distributed tracing recorders, and so on.\n\nThe graceful shutdown functionality is provided if you have additional,\nnon-Encore-related resources that need graceful shutdown.\n\n</Callout>\n\nNote that graceful shutdown in Encore is *cooperative*: Encore will wait indefinitely\nfor your `Shutdown` method to return. If your `Shutdown` method does not return promptly\nafter the `force` context is closed, the underlying infrastructure at your cloud provider\nwill typically force-kill your service, which can lead to lingering connections and other\nsuch issues.\n\nIn summary, when your `Shutdown(force context.Context)` function is called:\n\n- Immediately begin gracefully shutting down\n- When the `force` context is canceled, you should forcefully shut down\n  the resources that haven't yet completed their shutdown\n- Wait until the shutdown is complete before returning from the `Shutdown` function\n"
  },
  {
    "path": "docs/go/primitives/services.md",
    "content": "---\nseotitle: Defining services with Encore.go\nseodesc: Learn how to create microservices and define APIs for your cloud backend application using Go and Encore. The easiest way of building cloud backends.\ntitle: Defining Services\nsubtitle: Simplifying (micro-)service development\nlang: go\n---\n\nEncore.go makes it simple to build applications with one or many services, without needing to manually handle the typical complexity of developing microservices.\n\n## Defining a service\n\nWith Encore.go you define a service by [defining at least one API](/docs/go/primitives/defining-apis) within a regular Go package. Encore recognizes this as a service, and uses the package name as the service name.\n\nOn disk it might look like this:\n\n```\n/my-app\n├── encore.app          // ... and other top-level project files\n│\n├── hello               // hello service (a Go package)\n│   ├── hello.go        // hello service code\n│   └── hello_test.go   // tests for hello service\n│\n└── world               // world service (a Go package)\n    └── world.go        // world service code\n```\n\n\nThis means building a microservices architecture is as simple as creating multiple Go packages within your application.\nSee the [app structure documentation](/docs/go/primitives/app-structure) for more details.\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/trello-clone\" \n    desc=\"Simple microservices example application.\" \n/>\n\n## Service Initialization\n\nUnder the hood Encore automatically generates a `main` function that initializes all your infrastructure resources when the application starts up. This means you don't write a `main` function for your Encore application.\n\nIf you want to customize the initialization behavior of your service, you can define a service struct and define custom initialization logic with that. See the [service struct docs](/docs/go/primitives/service-structs) for more info.\n"
  },
  {
    "path": "docs/go/primitives/share-db-between-services.md",
    "content": "---\nseotitle: How to share SQL databases between services\nseodesc: Learn how to share a SQL database between multiple Go backend services using Encore.\ntitle: Share SQL databases between services\nlang: go\n---\n\nBy default, each service in an Encore app has its own database. This approach has many benefits: \n- Which database is used and how it works is abstracted away from other services\n- The database is more isolated, making changes to it smaller and safer\n- By making the services more independent your application becomes more reliable by being able to more gracefully handle partial outages, such as if your database is temporarily overloaded or offline.\n\nBut like everything else in software engineering, there are trade-offs involved, and sometimes it's simpler and more reliable to use a single database that's accessed by multiple services. Encore makes this easy to do.\n\nEach database in Encore is defined within a service. That service's name becomes the name of the database. Other services can then access that database by creating a database reference with `sqldb.Named(\"dbname\")`.\n\n## Example\n\nLet's say you have a simple `todo` service, with only one table:\n\n**`todo/migrations/1_create_table.up.sql`**\n\n```sql\nCREATE TABLE todo_item (\n    id BIGSERIAL PRIMARY KEY,\n    title TEXT NOT NULL,\n    done BOOLEAN NOT NULL DEFAULT FALSE\n);\n```\n\nYou want to create a `report` service that produces various reports for internal business processes, but for simplicity you decide it makes sense to directly access the `todo` database. All that's needed is to define the `todoDB` variable like so:\n\n**`report/report.go`**\n\n```go\npackage report\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/storage/sqldb\"\n)\n\n// todoDB connects to the \"todo\" service's database.\nvar todoDB = sqldb.Named(\"todo\")\n\ntype ReportResponse struct {\n    Total int\n}\n\n// CountCompletedTodos generates a report with the number of completed todo items.\n//encore:api method=GET path=/report/todo\nfunc CountCompletedTodos(ctx context.Context) (*ReportResponse, error) {\n    var report ReportResponse\n    err := todoDB.QueryRow(ctx,`\n        SELECT COUNT(*)\n        FROM todo_item\n        WHERE completed = TRUE\n    `).Scan(&report.Total)\n    return &report, err\n}\n```\n\nWith that, Encore understands that the `report` service depends on the `todo` service's database, and orchestrates the necessary connections to make that happen. And like everything else with Encore, it works exactly the same regardless of where it's running: for local development as well as in the cloud.\n"
  },
  {
    "path": "docs/go/quick-start.mdx",
    "content": "---\nseotitle: Quick Start Guide – Learn how to build backends with Encore.go\nseodesc: See how you to build and ship a cloud based backend application using Go and Encore. Install Encore and build a REST API in just a few minutes.\ntitle: Quick Start Guide\nsubtitle: Build your first Encore.go app in 5 minutes\nlang: go\n---\n\nIn this short guide, you'll learn key concepts and experience the Encore workflow.\nIt should only take about 5 minutes to complete and by the end you'll have an API running in Encore's free development Cloud (Encore Cloud).\n\nTo make it easy to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n## 1. Install the Encore CLI\n\nTo develop with Encore, you need the Encore CLI. It provisions your local environment, and runs your local\ndevelopment dashboard complete with tracing and API documentation.\n\n🥐 Install by running the appropriate command for your system:\n\n<InstallInstructions />\n\n## 2. Create your app\n\n🥐 Create your app by running:\n\n```shell\n$ encore app create\n```\n\nIf this is your first time using Encore, you’ll be prompted to create a free Encore Cloud account.\nThis enables Encore to manage things like secrets and fully automate cloud deployments (which you’ll use later in the tutorial).\n\n🥐 Select `Go` as your app’s language.\n\n🥐 Choose a starter template. Pick `Hello World` and continue.\n\nOptional: Install AI instructions to improve how tools like Cursor and Claude Code work with Encore. After selecting your template, choose the AI instructions for the tool you plan to use.\n\n🥐 Pick a name for your app.\n\nEncore will now create your app in a folder named after your app.\n\n### Let's take a look at the code\n\nPart of what makes Encore different is the simple developer experience when building distributed systems.\nLet's look at the code to better understand how to build applications with Encore.\n\n🥐 Open the `hello.go` file in your code editor. It's located in the folder: `your-app-name/hello/`.\n\nYou should see this:\n\n```go\n-- hello/hello.go --\n// Service hello implements a simple hello world REST API.\npackage hello\n\nimport (\n\t\"context\"\n)\n\n// This is a simple REST API that responds with a personalized greeting.\n//\n//encore:api public path=/hello/:name\nfunc World(ctx context.Context, name string) (*Response, error) {\n\tmsg := \"Hello, \" + name + \"!\"\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n```\n\nAs you can see, it's all standard Go code except for a few lines specific to Encore's Backend Framework.\n\nOne such element is the API annotation:\n\n```\n//encore:api public path=/hello/:name\n```\n\nThis annotation is all that's needed for Encore to understand that the Go package `hello` is a service, and\nthe `World` function is a public API endpoint.\n\nTo create more services and endpoints, you simply create new Go packages and define endpoints using\nthe `//encore:api` annotation. _If you're curious, you can read more about [defining APIs](/docs/go/primitives/defining-apis)._\n\nEncore.go provides several other declarative ways of using backend\nprimitives, such as databases, Pub/Sub, and scheduled tasks. All defined in your application code.\n\n## 3. Start your app & Explore the Local Development Dashboard\n\n🥐 Run your app locally:\n\n```shell\n$ cd your-app-name # replace with the app name you picked\n$ encore run\n```\n\nYou should see this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/encorerun.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nThat means your local development environment is up and running!\nEncore takes care of setting up all the necessary infrastructure for your applications, even including databases and Pub/Sub.\n\n### Open the Local Development Dashboard\n\nYou can now start using your [Local Development Dashboard](/docs/go/observability/dev-dash).\n\n🥐 Open [http://localhost:9400](http://localhost:9400) in your browser to access it.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/localdashvideo.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nThe Local Development Dashboard is a powerful tool to help you move faster when you're developing new features.\n\nIt comes with an API explorer, a Service Catalog with automatically generated documentation, and powerful observability features\nlike [distributed tracing](/docs/go/observability/tracing).\n\nThrough the Local Development Dashboard you also have access to [Encore Flow](/docs/go/observability/encore-flow),\na visual representation of your microservice architecture that updates in real-time as you develop your application.\n\n### Call your API\n\n🥐 While you keep the app running, call your API from the API Explorer:\n\n<img\n  className=\"mx-auto w-full\"\n  src=\"/assets/docs/qs_call.png\"\n  title=\"Call API from Local Dashboard\"\n/>\n\nYou can also open a separate terminal to call your API endpoint:\n\n```shell\n$ curl http://localhost:4000/hello/world\n{\"Message\": \"Hello, world!\"}\n```\n\nIf you see this JSON response, you've successfully made an API call to your very first Encore application. Well done, you're on your way!\n\n### Review a trace of the request\n\nYou can now take a look at the trace for the request you just made by clicking on it in the right column in the local dashboard.\n\n<img\n  className=\"mx-auto w-full\"\n  src=\"/assets/docs/qs_trace.png\"\n  title=\"Tracing in the Local Dashboard\"\n/>\n\nWith such a simple API, there's not much to it, just a simple request and response.\n\nHowever, just imagine how powerful it is to have tracing when you're developing a more complex system with multiple services, Pub/Sub, and databases.\n(Learn more about Encore's tracing capabilities in the [tracing docs](/docs/go/observability/tracing).)\n\n## 4. Make a code change\n\nLet's put our mark on this API and make our first code change.\n\n🥐 Head back to your code editor and look at the `hello.go` file again.\nIf you can't come up a creative change yourself, why not simply change the \"Hello\" message to a more sassy \"Howdy\"?\n\n🥐 Once you've made your change, save the file.\n\nWhen you save, the daemon run by the Encore CLI instantly detects the change and automatically recompiles your application and reloads your local development environment.\n\nThe output where you're running your app will look something like this:\n\n```output\nChanges detected, recompiling...\nReloaded successfully.\nTRC registered endpoint endpoint=World path=/hello/:name service=hello\nTRC listening for incoming HTTP requests\n```\n\n🥐 Test your change by calling your API again.\n\n```shell\n$ curl http://localhost:4000/hello/world\n{\"Message\": \"Howdy, world!\"}\n```\n\nGreat job, you made a change and your app was reloaded automatically.\n\nNow you're ready to head to the cloud!\n\n## 5. Deploy your app\n\n### Generating Docker image\n\nYou can either deploy by generating a Docker image for you app using:\n\n```shell\n$ encore build docker MY-IMAGE:TAG\n```\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\nYou can now deploy this anywhere you like. Learn more in the [self-host docs](/docs/go/self-host/docker-build).\n\n### Deploy using Encore Cloud\n\nOptionally, you can use [Encore Cloud](https://encore.dev/use-cases/devops-automation) to automatically deploy your application.\nIt comes with built-in free development hosting, and for production offers fully automated deployment to your own cloud on AWS or GCP.\n\n🥐 To deploy, simply push your changes to Encore:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore Cloud will now build and test your app, provision the needed infrastructure, and deploy your application to a staging environment.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the Encore Cloud dashboard.\nIt will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\n🥐 Open the URL to access the Encore Cloud dashboard and check the progress of your deployment.\n\nYou can now use the Cloud Dashboard to view production [traces](/docs/go/observability/tracing), [connect your cloud account](/docs/platform/deploy/own-cloud), [integrate with GitHub](/docs/platform/integrations/github), and much more.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/webdashvideo.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\n## What's next?\n\n- Check out the [REST API tutorial](/docs/go/tutorials/rest-api) to learn how to create endpoints, use databases, and more.\n- Join the friendly community on [Discord](/discord) to ask questions and meet other Encore developers.\n"
  },
  {
    "path": "docs/go/self-host/ci-cd.md",
    "content": "---\nseotitle: Integrate with your CI/CD pipeline\nseodesc: Learn how to integrate Encore.go with your CI/CD pipeline.\ntitle: Integrate with your CI/CD pipeline\nlang: go\n---\n\nEncore seamlessly integrates with any CI/CD pipeline through its CLI tools. You can automate Docker image creation using the `encore build` command as part of your deployment workflow.\n\n## Integrating with CI/CD Platforms\n\nWhile every CI/CD pipeline is unique, integrating Encore follows a straightforward process. Here are the key steps:\n\n1. Install the Encore CLI in your CI environment\n2. Use `encore build docker` to create Docker images\n3. Push the images to your container registry\n4. Deploy to your infrastructure\n\nIf your app is linked with Encore Cloud, you'll need to authenticate the CLI in your CI environment using an [auth key](/docs/platform/integrations/auth-keys). Generate one from **App Settings > Auth Keys** in the Encore Cloud dashboard, store it as a CI secret, and run `encore auth login --auth-key=<KEY>` before building.\n\nRefer to your CI/CD platform's documentation for more details on how to integrate CLI tools like `encore build`.\n\n### GitHub actions example\n\nThis example shows how to build, push, and deploy an Encore Docker image to DigitalOcean using GitHub Actions.\nThe DigitalOcean application is set up re-deploy the application every time an image with the tag `latest` is uploaded. \n\n```yaml\nname: Build, Push and Deploy a Encore Docker Image to DigitalOcean\n\non:\n  push:\n    branches: [ main ]\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  build-push-deploy-image:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Download Encore CLI script\n        uses: sozo-design/curl@v1.0.2\n        with:\n          args: --output install.sh -L https://encore.dev/install.sh\n\n      - name: Install Encore CLI\n        run: bash install.sh\n\n      - name: Authenticate with Encore\n        run: /home/runner/.encore/bin/encore auth login --auth-key=${{ secrets.ENCORE_AUTH_KEY }}\n\n      - name: Log in to DigitalOcean container registry\n        run: docker login registry.digitalocean.com -u my-email@gmail.com -p ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}\n\n      - name: Build Docker image\n        run: /home/runner/.encore/bin/encore build docker myapp\n\n      - name: Tag Docker image\n        run: docker tag myapp registry.digitalocean.com/<YOUR_CONTAINER_REGISTRY_NAME>/<YOUR_IMAGE_REPOSITORY_NAME>:latest\n\n      - name: Push Docker image\n        run: docker push registry.digitalocean.com/<YOUR_CONTAINER_REGISTRY_NAME>/<YOUR_IMAGE_REPOSITORY_NAME>:latest\n```\n\n## Building Docker Images\n\nThe `encore build docker` command provides several options to customize your builds:\n\n```bash\n# Build specific services and gateways\nencore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG\n\n# Customize the base image\nencore build docker --base=node:18-alpine MY-IMAGE:TAG\n\n# Build for a specific architecture (useful when CI and deploy targets differ)\nencore build docker --arch=arm64 MY-IMAGE:TAG\n```\n\nThe image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image.\n\n```bash\ndocker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG\n```\n\nLearn more about the `encore build docker` command in the [build Docker images](/docs/go/self-host/docker-build) guide.\n\nContinue to learn how to [configure infrastructure](/docs/go/self-host/configure-infra).\n"
  },
  {
    "path": "docs/go/self-host/configure-infra.md",
    "content": "---\ntitle: Configure Infrastructure\nseotitle: Configure Infrastructure\nseodesc: Learn how to configure infrastructure resources for your Encore app.\nlang: go\n---\nIf you are using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration.\nThe `build` command lets you provide this by specifying a path to a config file using the `--config` flag.\n\n```bash\nencore build docker --config path/to/infra-config.json MY-IMAGE:TAG\n```\n\nThe configuration file should be a JSON file using the [Encore Infra Config](https://encore.dev/schemas/infra.schema.json) schema.\n\nThis supports configuring things like:\n\n- How to access infrastructure resources (what provider to use, what credentials to use, etc.)\n- How to call other services over the network (\"service discovery\"),\n  most notably their base URLs.\n- Observability configuration (where to export metrics, etc.)\n- Metadata about the environment the application is running in, to power Encore's metadata APIs\n- The values for any application-defined secrets.\n\nThis configuration is necessary for the application to behave correctly.\n\n## Example\n\nHere's an example configuration file you can use.\n\n```json\n{\n  \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n  \"metadata\": {\n    \"app_id\": \"my-app\",\n    \"env_name\": \"my-env\",\n    \"env_type\": \"production\",\n    \"cloud\": \"gcp\",\n    \"base_url\": \"https://my-app.com\"\n  },\n  \"sql_servers\": [\n    {\n      \"host\": \"my-db-host:5432\",\n      \"databases\": {\n        \"my-db\": {\n          \"username\": \"my-db-owner\",\n          \"password\": {\"$env\": \"DB_PASSWORD\"}\n        }\n      }\n    }\n  ],\n  \"service_discovery\": {\n    \"myservice\": {\n      \"base_url\": \"https://myservice:8044\"\n    }\n  },\n  \"redis\": {\n    \"my-redis\": {\n      \"database_index\": 0,\n      \"auth\": {\n        \"type\": \"acl\",\n        \"username\": \"encoreredis\",\n        \"password\": {\"$env\": \"REDIS_PASSWORD\"}\n      },\n      \"host\": \"my-redis-host\",\n    }\n  },\n  \"metrics\": {\n    \"type\": \"prometheus\",\n    \"remote_write_url\": \"https://my-remote-write-url\"\n  },\n  \"graceful_shutdown\": {\n    \"total\": 30\n  },\n  \"auth\": [\n    {\n      \"type\": \"key\",\n      \"id\": 1,\n      \"key\": {\"$env\": \"SVC_TO_SVC_KEY\"}\n    }\n  ],\n  \"secrets\": {\n    \"AppSecret\": {\"$env\": \"APP_SECRET\"}\n  },\n  \"pubsub\": [\n    {\n      \"type\": \"gcp_pubsub\",\n      \"project_id\": \"my-project\",\n      \"topics\": {\n        \"my-topic\": {\n          \"name\": \"gcp-topic-name\",\n          \"subscriptions\": {\n            \"encore-subscription\": {\n              \"name\": \"gcp-subscription-name\"\n            }\n          }\n        }\n      }\n    }\n  ],\n  \"object_storage\": [\n    {\n      \"type\": \"gcs\",\n      \"buckets\": {\n          \"my-gcs-bucket\": {\n            \"name\": \"my-gcs-bucket\",\n          }\n        }\n    }\n  ]\n}\n```\n\n## Configuring Infrastructure\nTo use infrastructure resources, additional configuration must be added so that Encore is aware of how to access each infrastructure resource.\nSee below for examples of each type of infrastructure resource.\n\n### 1. Basic Environment Metadata Configuration\n\n```json\n{\n  \"metadata\": {\n    \"app_id\": \"my-encore-app\",\n    \"env_name\": \"production\",\n    \"env_type\": \"production\",\n    \"cloud\": \"aws\",\n    \"base_url\": \"https://api.myencoreapp.com\"\n  }\n}\n```\n\n- `app_id`: The ID of your Encore application.\n- `env_name`: The environment name, such as `production`, `staging`, or `development`.\n- `env_type`: Specifies the type of environment (`production`, `test`, `development`, or `ephemeral`).\n- `cloud`: The cloud provider hosting the infrastructure (e.g., `aws`, `gcp`, or `azure`).\n- `base_url`: The base URL for services in the environment.\n\n### 2. Graceful Shutdown Configuration\n\n```json\n{\n  \"graceful_shutdown\": {\n    \"total\": 30,\n    \"shutdown_hooks\": 10,\n    \"handlers\": 20\n  }\n}\n```\n\n- `total`: The total time allowed for the shutdown process in seconds.\n- `shutdown_hooks`: The time allowed for executing shutdown hooks.\n- `handlers`: The time allocated for processing request handlers during the shutdown.\n\n### 3. Authentication Methods Configuration\nPrivate endpoints will not require authentication if no authentication methods are specified. This is typically fine when services are deployed on a private network such as a VPC. But sometimes you might need to connect to other services over the public internet, in which case you'll want to ensure private endpoints are only accessible to other backend services. To do that you can configure authentication methods.\nEncore currently supports authentication through a shared key, which you can specify in your infrastructure configuration file.\n```json\n{\n  \"auth\": [\n    {\n      \"type\": \"key\",\n      \"id\": 1,\n      \"key\": {\n        \"$env\": \"SERVICE_API_KEY\"\n      }\n    }\n  ]\n}\n```\n\n- `type`: The authentication method type (e.g., `key`).\n- `id`: The ID associated with the authentication method.\n- `key`: The authentication key, which can be set using an environment variable reference.\n\n### 4. Service Discovery Configuration\nService discovery is used to access other services over the network. You can configure service discovery in the infrastructure configuration file.\nIf you export all services into the same docker image, you don't need to configure service discovery as it will be automatically\nconfigured when the services are started.\n\n```json\n{\n  \"service_discovery\": {\n    \"myservice\": {\n      \"base_url\": \"https://myservice.myencoreapp.com\",\n      \"auth\": [\n        {\n          \"type\": \"key\",\n          \"id\": 1,\n          \"key\": {\n            \"$env\": \"MY_SERVICE_API_KEY\"\n          }\n        }\n      ]\n    }\n  }\n}\n```\n- `myservice`: This is the name of the service as it is declared in your Encore app.\n- `base_url`: The base URL for the service.\n- `auth`: Authentication methods used for accessing the service. If no authentication methods are specified, the service will use the auth methods defined in the `auth` section.\n\n### 5. Metrics Configuration\nSimilarly to cloud infrastructure resources, Encore supports configurable metrics exports:\n\n* Prometheus\n* DataDog\n* GCP Cloud Monitoring\n* AWS CloudWatch\n\nThis is configured by setting the metrics field. Below are examples for each of the supported metrics providers:\n#### 5.1. Prometheus Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"prometheus\",\n    \"collection_interval\": 15,\n    \"remote_write_url\": {\n      \"$env\": \"PROMETHEUS_REMOTE_WRITE_URL\"\n    }\n  }\n}\n```\n\n#### 5.2. Datadog Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"datadog\",\n    \"collection_interval\": 30,\n    \"site\": \"datadoghq.com\",\n    \"api_key\": {\n      \"$env\": \"DATADOG_API_KEY\"\n    }\n  }\n}\n```\n\n#### 5.3. GCP Cloud Monitoring Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"gcp_cloud_monitoring\",\n    \"collection_interval\": 60,\n    \"project_id\": \"my-gcp-project\",\n    \"monitored_resource_type\": \"gce_instance\",\n    \"monitored_resource_labels\": {\n      \"instance_id\": \"1234567890\",\n      \"zone\": \"us-central1-a\"\n    },\n    \"metric_names\": {\n      \"cpu_usage\": \"compute.googleapis.com/instance/cpu/usage_time\"\n    }\n  }\n}\n```\n\n#### 5.4. AWS CloudWatch Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"aws_cloudwatch\",\n    \"collection_interval\": 60,\n    \"namespace\": \"MyAppMetrics\"\n  }\n}\n```\n\n### 6. SQL Database Configuration\nThe SQL databases you've declared in your Encore app must be configured in the infrastructure configuration file.\nThere must be exactly one database configuration for each declared database. You can configure multiple SQL servers if needed.\n\n```json\n{\n  \"sql_servers\": [\n    {\n      \"host\": \"db.myencoreapp.com:5432\",\n      \"tls_config\": {\n        \"disabled\": false,\n        \"ca\": \"---BEGIN CERTIFICATE---\\n...\",\n        \"disable_tls_hostname_verification\": false,\n        \"disable_ca_verification\": false\n      },\n      \"databases\": {\n        \"my-database\": {\n          \"name\": \"my-postgres-db-name\",\n          \"max_connections\": 100,\n          \"min_connections\": 10,\n          \"username\": \"db_user\",\n          \"password\": {\n            \"$env\": \"DB_PASSWORD\"\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-database`: This is the name of the database as it is declared in your Encore app.\n- `name`: The name of the database on the database server. Defaults to the declared Encore name.\n- `host`: SQL server host, optionally including the port.\n- `tls_config`: TLS configuration for secure connections. If the server uses TLS with a non-system CA root, or requires a client certificate, specify the appropriate fields as PEM-encoded strings. Otherwise, they can be left empty.\n- `databases`: List of databases, each with connection settings.\n\n### 7. Secrets Configuration\n\n#### 7.1. Using Direct Secrets\nYou can set the secret value directly in the configuration file, or use an environment variable reference to set the secret value.\n\n```json\n{\n  \"secrets\": {\n    \"API_TOKEN\": \"embedded-secret-value\",\n    \"DB_PASSWORD\": {\n      \"$env\": \"DB_PASSWORD\"\n    }\n  }\n}\n```\n\n- `API_TOKEN`: This is the name of a secret as it is declared in your Encore app.\n\n#### 7.2. Using Environment Reference\nAs an alternative, you can use an environment variable reference to set the secret value. The env variable should be set in the environment where the application is running. The content\nof the environment variable should be a JSON string where each key is the secret name and the value is the secret value.\n\n```json\n{\n  \"secrets\": {\n    \"$env\": \"SECRET_JSON\"\n  }\n}\n```\n\n### 8. Redis Configuration\n\n```json\n{\n  \"redis\": {\n    \"my-redis\": {\n      \"host\": \"redis.myencoreapp.com:6379\",\n      \"database_index\": 0,\n      \"auth\": {\n        \"type\": \"auth\",\n        \"auth_string\": {\n          \"$env\": \"REDIS_AUTH_STRING\"\n        }\n      },\n      \"max_connections\": 50,\n      \"min_connections\": 5\n    }\n  }\n}\n```\n\n- `my-redis`: This is the name of the redis resource as it is declared in your Encore app.\n- `host`: Redis server host, optionally including the port.\n- `auth`: Authentication configuration for the Redis server.\n- `key_prefix`: Prefix applied to all keys.\n\n### 9. Pub/Sub Configuration\nEncore currently supports the following Pub/Sub providers:\n- `nsq` for [NSQ](https://nsq.io/)\n- `gcp` for [Google Cloud Pub/Sub](https://cloud.google.com/pubsub)\n- `aws` for AWS [SNS](https://aws.amazon.com/sns/) + [SQS](https://aws.amazon.com/sqs/)\n- `azure` for [Azure Service Bus](https://azure.microsoft.com/en-us/products/service-bus)\n\nThe configuration for each provider is different. Below are examples for each provider.\n#### 9.1. GCP Pub/Sub\n\n```json\n{\n  \"pubsub\": [\n    {\n      \"type\": \"gcp_pubsub\",\n      \"project_id\": \"my-gcp-project\",\n      \"topics\": {\n        \"my-topic\": {\n          \"name\": \"my-topic\",\n          \"project_id\": \"my-gcp-project\",\n          \"subscriptions\": {\n            \"my-subscription\": {\n              \"name\": \"my-subscription\",\n              \"push_config\": {\n                \"id\": \"my-push\",\n                \"service_account\": \"service-account@my-gcp-project.iam.gserviceaccount.com\"\n              }\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-topic`: This is the name of the topic as it is declared in your Encore app.\n- `my-subscription`: This is the name of the subscription as it is declared in your Encore app.\n- `project_id`: The default GCP project ID. This can be overridden by setting the `project_id` field in the topic or subscription.\n- `name`: The name of the topic or subscription.\n- `push_config/id`: The id will be appended to `/__encore/pubsub/push/` to form the full push path of your service, e.g. `/__encore/pubsub/push/<id>`. This is the path your service expects to receive push messages on.\n- `push_config/service_account`: The service account configured for the push subscription.\n\n#### 9.2. AWS SNS/SQS\n\n```json\n{\n  \"pubsub\": [\n    {\n      \"type\": \"aws_sns_sqs\",\n      \"topics\": {\n        \"my-topic\": {\n          \"arn\": \"arn:aws:sns:us-east-1:123456789012:my-topic\",\n          \"subscriptions\": {\n            \"my-queue\": {\n              \"url\": \"https://sqs.eu-east-1.amazonaws.com/123456789012/my-queue\"\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-topic`: This is the name of the topic as it is declared in your Encore app.\n- `my-queue`: This is the name of the queue as it is declared in your Encore app.\n- `arn`: The ARN of the SNS topic.\n- `url`: The URL of the SQS queue.\n\n#### 9.3. NSQ Configuration\n\n```json\n{\n  \"pubsub\": [\n    {\n      \"type\": \"nsq\",\n      \"hosts\": \"nsq.myencoreapp.com:4150\",\n      \"topics\": {\n        \"my-topic\": {\n          \"name\": \"my-topic\",\n          \"subscriptions\": {\n            \"my-subscription\": {\n                \"name\": \"my-subscription\"\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-topic`: This is the name of the topic as it is declared in your Encore app.\n- `my-subscription`: This is the name of the subscription as it is declared in your Encore app.\n\n### 10. Object Storage Configuration\nEncore currently supports the following object storage providers:\n- `gcs` for [Google Cloud Storage](https://cloud.google.com/storage)\n- `s3` for [AWS S3](https://aws.amazon.com/s3/) or a custom S3-compatible provider\n\n#### 10.1. GCS Configuration\n\n```json\n{\n  \"object_storage\": [\n    {\n      \"type\": \"gcs\",\n      \"buckets\": {\n        \"my-gcs-bucket\": {\n          \"name\": \"my-gcs-bucket\",\n          \"key_prefix\": \"my-optional-prefix/\",\n          \"public_base_url\": \"https://my-gcs-bucket-cdn.example.com/my-optional-prefix\"\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-gcs-bucket`: This is the name of the bucket as it is declared in your Encore app.\n- `name`: The full name of the GCS bucket.\n- `key_prefix`: An optional prefix to apply to all keys in the bucket.\n- `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended.\n\n#### 10.2. S3 Configuration\n\n```json\n{\n  \"object_storage\": [\n    {\n      \"type\": \"s3\",\n      \"region\": \"us-east-1\",\n      \"buckets\": {\n        \"my-s3-bucket\": {\n          \"name\": \"my-s3-bucket\",\n          \"key_prefix\": \"my-optional-prefix/\",\n          \"public_base_url\": \"https://my-gcs-bucket-cdn.example.com/my-optional-prefix\"\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-s3-bucket`: This is the name of the bucket as it is declared in your Encore app.\n- `region`: The AWS region where the bucket is located.\n- `name`: The full name of the S3 bucket.\n- `key_prefix`: An optional prefix to apply to all keys in the bucket.\n- `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended.\n\n#### 10.3. Custom S3 Provider Configuration\nYou can also configure a custom S3 provider by specifying the endpoint, access key id, and secret access key. Custom S3 providers are useful if you are using a S3-compatible storage provider such as [Cloudflare R2](https://developers.cloudflare.com/r2/).\n```json\n{\n  \"object_storage\": [\n    {\n      \"type\": \"s3\",\n      \"region\": \"auto\",\n      \"endpoint\": \"https://...\",\n      \"access_key_id\": \"...\",\n      \"secret_access_key\": {\n          \"$env\": \"BUCKET_SECRET_ACCESS_KEY\"\n      },\n      \"buckets\": {\n        \"my-custom-bucket\": {\n          \"name\": \"my-custom-bucket\",\n          \"key_prefix\": \"my-optional-prefix/\",\n          \"public_base_url\": \"https://my-gcs-bucket-cdn.example.com/my-optional-prefix\"          \n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-custom-bucket`: This is the name of the bucket as it is declared in your Encore app.\n- `region`: The region where the bucket is located.\n- `name`: The full name of the bucket\n- `key_prefix`: An optional prefix to apply to all keys in the bucket.\n- `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended.\n\nThis guide covers typical infrastructure configurations. Adjust according to your specific requirements to optimize your Encore app's infrastructure setup.\n"
  },
  {
    "path": "docs/go/self-host/deploy-to-digital-ocean-wip.md",
    "content": "---\nseotitle: How to deploy an Encore app to DigitalOcean\nseodesc: Learn how to deploy an Encore application to DigitalOcean's App Platform using Docker.\ntitle: Deploy to DigitalOcean\nlang: go\n---\n\nIf you prefer manual deployment over the automation offered by Encore's Platform, Encore simplifies the process of deploying your app to the cloud provider of your choice. This guide will walk you through deploying an Encore app to DigitalOcean's App Platform using Docker.\n\n### Prerequisites\n1. **DigitalOcean Account**: Make sure you have a DigitalOcean account. If not, you can [sign up here](https://www.digitalocean.com/).\n2. **Docker Installed**: Ensure Docker is installed on your local machine. You can download it from the [Docker website](https://www.docker.com/get-started).\n3. **Encore CLI**: Install the Encore CLI if you haven’t already. You can follow the installation instructions from the [Encore documentation](https://encore.dev/docs/go/install).\n4. **DigitalOcean CLI (Optional)**: You can install the DigitalOcean CLI for more flexibility and automation, but it’s not necessary for this tutorial.\n\n### Step 1: Create an Encore App\n1. **Create a New Encore App**: \n    - If you haven’t already, create a new Encore app using the Encore CLI.\n    - You can use the following command to create a new app:\n    ```bash\n    encore app create myapp\n    ``` \n    - Select the `Hello World` template.\n    - Follow the prompts to create the app.\n\n2. **Build a Docker image**:\n    - Build the Encore app to generate the docker image for deployment:\n    ```bash\n    encore build docker myapp  \n    ```\n### Step 2: Push the Docker Image to a Container Registry\nTo deploy your Docker image to DigitalOcean, you need to push it to a container registry. DigitalOcean supports\nits own container registry, but you can also use DockerHub or other registries. Here’s how to push the image to DigitalOcean’s registry:\n\n1. **Create a DigitalOcean Container Registry**:\n    - Go to the [DigitalOcean Control Panel](https://cloud.digitalocean.com/registries) and create a new container registry.\n    - Follow the instructions to set it up.\n\n2. **Login to DigitalOcean's registry**:\n   Use the login command provided by DigitalOcean, which will look something like this:\n   ```bash\n   doctl registry login\n   ```\n   You’ll need the DigitalOcean CLI for this, which can be installed from [DigitalOcean CLI documentation](https://docs.digitalocean.com/reference/doctl/how-to/install/).\n\n3. **Tag your Docker image**:\n   Tag your image to match the registry’s URL.\n   ```bash\n   docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   ```\n\n4. **Push your Docker image to the registry**:\n   ```bash\n   docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   ```\n\n### Step 3: Deploy the Docker Image to DigitalOcean App Platform\n1. **Navigate to the App Platform**:\n   Go to [DigitalOcean's App Platform](https://cloud.digitalocean.com/apps).\n\n2. **Create a New App**:\n    - Click on **\"Create App\"**.\n    - Choose the **\"DigitalOcean Container Registry\"** option.\n\n3. **Select the Docker Image Source**:\n    - Select the image you pushed earlier.\n\n4. **Configure the App Settings**:\n    - **Set up scaling options**: Configure the number of containers, CPU, and memory settings.\n    - **Environment variables**: Add any environment variables your application might need.\n    - **Choose the region**: Pick a region close to your users for better performance.\n\n5. **Deploy the App**:\n    - Click **\"Next\"**, review the settings, and click **\"Create Resources\"**.\n    - DigitalOcean will take care of provisioning the infrastructure, pulling the Docker image, and starting the application.\n\n### Step 4: Monitor and Manage the App\n1. **Access the Application**:\n    - Once deployed, you will get a public URL to access your application.\n    - Test the app to ensure it’s running as expected, e.g. \n   ```bash\n      curl https://myapp.ondigitalocean.app/hello/world\n    ```\n\n2. **View Logs and Metrics**:\n    - Go to the **\"Runtime Logs\"** tab in the App Platform to view logs\n    - Go to the **\"Insights\"** tab to view performance metrics.\n\n3. **Manage Scaling and Deployment Settings**:\n    - You can change the app configuration, such as scaling settings, deployment region, or environment variables.\n\n### Step 5: Add a Database to Your App\n\nDigitalOcean’s App Platform provides managed databases, allowing you to add a database to your app easily. Here’s how to set up a managed database for your app:\n\n1. **Navigate to the DigitalOcean Control Panel**:\n   - Go to [DigitalOcean Control Panel](https://cloud.digitalocean.com/).\n   - Click on **\"Databases\"** in the left-hand sidebar.\n\n2. **Create a New Database Cluster**:\n   - Click **\"Create Database Cluster\"**.\n   - Choose **PostgreSQL**\n   - Select the **database version**, **data center region**, and **cluster configuration** (e.g., development or production settings based on your needs).\n   - **Name the database** and configure other settings if necessary, then click **\"Create Database Cluster\"**.\n\n3. **Configure the Database Settings**:\n   - Once the database is created, go to the **\"Connection Details\"** tab of the database dashboard.\n   - Copy the **connection string** or individual settings (host, port, username, password, database name). You will need these details to connect your app to the database.\n   - Download the **CA certificate**\n\n4. **Create a Database**\n   - Connect to the database using the connection string provided by DigitalOcean.\n   ```bash\n   psql -h mydb.db.ondigitalocean.com -U doadmin -d mydb -p 25060\n   ```\n   - Create a database\n   ```sql\n    CREATE DATABASE mydb;\n    ```\n   - Create a table\n   ```sql\n     CREATE TABLE users (\n        id SERIAL PRIMARY KEY,\n        name VARCHAR(50)\n     );\n     INSERT INTO users (name) VALUES ('Alice');\n   ```\n   \n5. **Declare a Database in your Encore app**:\n   - Open your Encore app’s codebase.\n   - Add `mydb` database to your app ([Encore Database Documentation](https://encore.dev/docs/ts/primitives/databases))\n   ```typescript\n      const mydb = new SQLDatabase(\"mydb\", {\n         migrations: \"./migrations\",\n      });\n\n      export const getUser = api(\n        { expose: true, method: \"GET\", path: \"/names/:id\" },\n        async ({id}: {id:number}): Promise<{ id: number; name: string }> => {\n          return await mydb.queryRow`SELECT * FROM users WHERE id = ${id}` as { id: number; name: string };\n        }\n      );\n   ```\n\n6. **Create an Encore Infrastructure config**\n   - Create a file named `infra.config.json` in the root of your Encore app.\n   - Add the **CA certificate** and the connection details to the file:\n   ```json\n   {\n      \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n      \"sql_servers\": [\n      {\n         \"host\": \"mydb.db.ondigitalocean.com:25060\",\n         \"tls_config\": {\n            \"ca\": \"-----BEGIN CERTIFICATE-----\\n...\"\n         },\n         \"databases\": {\n            \"mydb\": {\n               \"username\": \"doadmin\",\n               \"password\": {\"$env\": \"DB_PASSWORD\"}\n             }\n         }\n      }]   \n   }\n   ```\n\n7. **Set Up Environment Variables (Optional)**:\n   - Go to the DigitalOcean App Platform dashboard.\n   - Select your app.\n   - In the **\"Settings\"** section, go to **\"App-Level Environment Variables\"**\n   - Add the database password as an encrypted environment variable called `DB_PASSWORD`.\n\n8. **Build and push the Docker image**:\n   - Build the Docker image with the updated configuration.\n   ```bash\n   encore build docker --config infra.config.json myapp\n   ```\n   - Tag and push the Docker image to the DigitalOcean container registry.\n   ```bash\n   docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   ```\n\n9. **Test the Database Connection**:\n   - Redeploy the app on DigitalOcean to apply the changes.\n   - Test the database connection by calling the API\n   ```bash\n    curl https://myapp.ondigitalocean.app/names/1\n   ```\n\n### Troubleshooting Tips\n- **Deployment Failures**: Check the build logs for any errors. Make sure the Docker image is correctly tagged and pushed to the registry.\n- **App Not Accessible**: Verify that the correct port is exposed in the Dockerfile and the App Platform configuration.\n- **Database Connection Issues**: Ensure the database connection details are correct and the database is accessible from the app.\n\n### Conclusion\nThat’s it! You’ve successfully deployed an Encore app to DigitalOcean’s App Platform using Docker. You can now scale your app, monitor its performance, and manage it easily through the DigitalOcean dashboard. If you encounter any issues, refer to the DigitalOcean documentation or the Encore community for help. Happy coding!"
  },
  {
    "path": "docs/go/self-host/self-host.md",
    "content": "---\nseotitle: Build Docker Images\nseodesc: Learn how to build Docker images for your Encore application, which can be self-hosted on your own infrastructure.\ntitle: Build Docker Images\nlang: go\n---\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nThis can be a good choice if Encore Cloud isn't a good fit for your use case, or if you want to [migrate away](/docs/go/migration/migrate-away).\n\n## Building your own Docker image\n\nTo build your own Docker image, use `encore build docker MY-IMAGE:TAG` from the CLI.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application. The base image defaults to `scratch` for GO apps and `node:slim` for TS, but can be customized with `--base`.\n\nThis is exactly the same code path that Encore's CI system uses to build Docker images, ensuring compatibility.\n\nBy default, all your services are included and started by the Docker image. If you want to specify specific services and gateways to include, you can use the `--services` and `--gateways` flags.\n\n```bash\nencore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG\n```\n\nYou can target a specific architecture with `--arch` (useful when your build machine differs from your deploy target):\n\n```bash\nencore build docker --arch=arm64 MY-IMAGE:TAG\n```\n\nTo provide an [infrastructure configuration](/docs/go/self-host/configure-infra) file at build time, use `--config`:\n\n```bash\nencore build docker --config=infra-config.json MY-IMAGE:TAG\n```\n\nThe image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image.\n\n```bash\ndocker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG\n```\n\nCongratulations, you've built your own Docker image! 🎉\nContinue to learn how to [configure infrastructure](/docs/go/self-host/configure-infra)."
  },
  {
    "path": "docs/go/tutorials/booking-system.mdx",
    "content": "---\ntitle: Building a Booking System\nsubtitle: Learn how to build your own appointment booking system with both user facing and admin functionality\nseotitle: How to build an Appointment Booking System in Go\nseodesc: Learn how to build an appointment booking tool using Go and Encore. Get your entire application running in the cloud in 30 minutes!\nlang: go\n---\n\nIn this tutorial we'll build a booking system with a user facing UI (see available slots and book appointments) and an admin dashboard (manage scheduled appointments and set availability). You will learn how to:\n\n* Create API endpoints using Encore (both public and authenticated).\n* Working with PostgreSQL databases using [sqlc](https://sqlc.dev/) and [pgx](https://github.com/jackc/pgx).\n* Scrub sensitive user data from traces.\n* Work with dates and times in Go.\n* Authenticate requests using an auth handler.\n* Send emails using a SendGrid integration.\n\n[Demo version of the app](https://prod-booking-system-teti.encr.app/frontend)\n\nThe final result will look like this:\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/booking-system/user-calendar.png\" title=\"User calendar\" />\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/booking-system/admin-dashboard.png\" title=\"Admin dashboard\" />\n\nIf you want to skip ahead you can view the final project here: [https://github.com/encoredev/examples/tree/main/booking-system](https://github.com/encoredev/examples/tree/main/booking-system)\n\n## 1. Create your Encore application\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\nMake sure you have [Docker](https://docker.com) installed and running, it is used by Encore to run PostgreSQL databases locally.\n</Callout>\n\n🥐 Create a new Encore application, using this tutorial project's starting-point branch. This gives you a ready-to-go frontend to use.\n\n```shell\n$ encore app create booking-system --example=github.com/encoredev/example-booking-system/tree/starting-point\n```\n\n\n🥐 Check that your frontend works:\n\n```shell\n$ cd booking-system\n$ encore run\n```\n\nThen visit [http://localhost:4000/frontend/](http://localhost:4000/frontend/) to see the frontend.\nIt won't function yet, since we haven't yet built the backend, so let's do just that!\n\nWhen we're done we'll have a backend with this [architecture](/docs/go/observability/encore-flow):\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/booking-system/booking-system-flow.png\" title=\"Encore Flow\" />\n\n## 2. Create booking service\n\nLet's start by creating the functionality to view bookable slots.\n\nWith Encore you define a service by [defining one or more APIs](/docs/go/primitives/defining-apis) within a regular Go package. Encore recognizes this as a service, and uses the package name as the service name. When deploying, Encore will automatically [provision the required infrastructure](/docs/platform/infrastructure/infra) for each service.\n\nWe already have a Go package named `booking`, let's turn that into an Encore service.\n\n🥐 Inside the `booking` folder, create a file named `slots.go`.\n\n```shell\n$ touch booking/slots.go\n```\n\n🥐 Add an Encore API endpoint named `GetBookableSlots` that takes a date as input. The endpoint will return a list of bookable slots from the supplied date and six days forward (so that we can show a week view calendar in the UI).\n\n```go\n-- booking/slots.go --\n// Service booking keeps track of bookable slots in the calendar.\npackage booking\n\nimport (\n\t\"context\"\n\t\"github.com/jackc/pgx/v5/pgtype\"\n\t\"time\"\n)\n\nconst DefaultBookingDuration = 1 * time.Hour\n\ntype BookableSlot struct {\n\tStart time.Time `json:\"start\"`\n\tEnd   time.Time `json:\"end\"`\n}\n\ntype SlotsParams struct{}\n\ntype SlotsResponse struct{ Slots []BookableSlot }\n\n//encore:api public method=GET path=/slots/:from\nfunc GetBookableSlots(ctx context.Context, from string) (*SlotsResponse, error) {\n\tfromDate, err := time.Parse(\"2006-01-02\", from)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconst numDays = 7\n\n\tvar slots []BookableSlot\n\tfor i := 0; i < numDays; i++ {\n\t\tdate := fromDate.AddDate(0, 0, i)\n\t\tdaySlots, err := bookableSlotsForDay(date)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tslots = append(slots, daySlots...)\n\t}\n\n\treturn &SlotsResponse{Slots: slots}, nil\n}\n\nfunc bookableSlotsForDay(date time.Time) ([]BookableSlot, error) {\n\t// 09:00\n\tavailStartTime := pgtype.Time{\n\t\tValid:        true,\n\t\tMicroseconds: int64(9*3600) * 1e6,\n\t}\n\t// 17:00\n\tavailEndTime := pgtype.Time{\n\t\tValid:        true,\n\t\tMicroseconds: int64(17*3600) * 1e6,\n\t}\n\n\tavailStart := date.Add(time.Duration(availStartTime.Microseconds) * time.Microsecond)\n\tavailEnd := date.Add(time.Duration(availEndTime.Microseconds) * time.Microsecond)\n\n\t// Compute the bookable slots in this day, based on availability.\n\tvar slots []BookableSlot\n\tstart := availStart\n\tfor {\n\t\tend := start.Add(DefaultBookingDuration)\n\t\tif end.After(availEnd) {\n\t\t\tbreak\n\t\t}\n\t\tslots = append(slots, BookableSlot{\n\t\t\tStart: start,\n\t\t\tEnd:   end,\n\t\t})\n\t\tstart = end\n\t}\n\n\treturn slots, nil\n}\n```\n\nThe availability is currently hardcoded to be 09:00 - 17:00 for each day. Later we'll add the functionality to set it for each day of the week.\nWe are also returning time slots that have already passed. Don't worry, we'll come back and fix it later on.\n\n🥐 Let's try it! Open up the Local Development Dashboard running at [http://localhost:9400](http://localhost:9400) and try calling\nthe `booking.GetBookableSlots` endpoint, passing in `2024-12-01`.\n\nIf you prefer to use the terminal instead run `curl http://localhost:4000/slots/2024-12-01` in\na new terminal instead. Either way you should see the response:\n\n```json\n{\n  \"Slots\": [\n    {\n      \"start\": \"2024-12-01T09:00:00Z\",\n      \"end\": \"2024-12-01T10:00:00Z\"\n    },\n    {\n      \"start\": \"2024-12-01T10:00:00Z\",\n      \"end\": \"2024-12-01T11:00:00Z\"\n    },\n    {\n      \"start\": \"2024-12-01T11:00:00Z\",\n      \"end\": \"2024-12-01T12:00:00Z\"\n    },\n    ...\n  ]\n}\n```\n\n## 3. Book an appointment\n\nNext, we want to make it possible to book an appointment. We'll need a database to store the bookings in. Encore makes it really simple to [create and use databases](/docs/go/primitives/databases) (both for local and cloud environments), but for this example we will also make use of [sqlc](https://sqlc.dev/) that will compile our SQL queries into type-safe Go code that we can use in our application.\n\n🥐 Let's create a SQL database for our booking service and the required sqlc scaffolding. Create the following file structure:\n\n```\n/my-app\n└── booking                              // booking service (a Go package)\n    ├── db                               // (New) db related files (directory)\n    │   ├── migrations                   // (New) db migrations (directory)\n    │   │   └── 1_create_tables.up.sql   // (New) db migration schema\n    │   └── query.sql                    // (New) SQL queries\n    ├── sqlc.yaml                        // (New) sqlc config file\n    ├── slots.go                         // booking service code\n    └── helpers.go                       // booking service code\n```\n\n🥐 Naming of the database migration file is important, it must look something like: `1_<name>.up.sql`.\n\nAdd the following contents to the migration file:\n\n```sql\n-- booking/db/migrations/1_create_tables.up.sql --\nCREATE TABLE booking (\n    id BIGSERIAL PRIMARY KEY,\n    start_time TIMESTAMP NOT NULL,\n    end_time TIMESTAMP NOT NULL,\n    email TEXT NOT NULL,\n    created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n```\n\n🥐 Next, install the sqlc library:\n\n```shell\n$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest\n```\n\n🥐 Next, we need to configure sqlc. Add the following contents to `sqlc.yaml`:\n\n```yaml\n-- booking/sqlc.yaml --\nversion: \"2\"\nsql:\n  - engine: \"postgresql\"\n    queries: \"db/query.sql\"\n    schema: \"./db/migrations\"\n    gen:\n      go:\n        package: \"db\"\n        out: \"db\"\n        sql_package: \"pgx/v5\"\n```\n\nThis instructs sqlc to generate Go code from the queries in `db/query.sql` and models from the schemas in the `db/migrations` folder.\n\n🥐 Let's create our first SQL queries. Add the following contents to `db/query.sql`:\n\n```sql\n-- name: InsertBooking :one\nINSERT INTO booking (start_time, end_time, email)\nVALUES ($1, $2, $3)\nRETURNING *;\n\n-- name: ListBookingsBetween :many\nSELECT * FROM booking\nWHERE start_time >= $1 AND end_time <= $2;\n\n-- name: ListBookings :many\nSELECT * FROM booking;\n\n-- name: DeleteBooking :exec\nDELETE FROM booking WHERE id = $1;\n\n```\n\n🥐 It's time for sqlc to shine! Run the following command in your terminal:\n\n```shell\n$ cd booking\n$ sqlc generate\n```\n\nThree files should now have been generated inside the `db` folder: `query.sql.go`, `db.go` and `models.go`. These files contain generated Go code and should not be manually edited. We will be adding more queries to `db/query.sql` later and then re-run `sqlc generate` to update the generated Go code.\n\nNow let's create an endpoint that makes use of one of these queries.\n\n🥐 Create `booking/booking.go` with the contents:\n\n```go\n-- booking/booking.go --\npackage booking\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"encore.app/booking/db\"\n\t\"github.com/jackc/pgx/v5/pgtype\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/storage/sqldb\"\n)\n\nvar (\n\tbookingDB = sqldb.NewDatabase(\"booking\", sqldb.DatabaseConfig{\n\t\tMigrations: \"./db/migrations\",\n\t})\n\n\tpgxdb = sqldb.Driver[*pgxpool.Pool](bookingDB)\n\tquery = db.New(pgxdb)\n)\n\ntype Booking struct {\n\tID    int64     `json:\"id\"`\n\tStart time.Time `json:\"start\"`\n\tEnd   time.Time `json:\"end\"`\n\tEmail string    `encore:\"sensitive\"`\n}\n\ntype BookParams struct {\n\tStart time.Time `json:\"start\"`\n\tEmail string    `encore:\"sensitive\"`\n}\n\n//encore:api public method=POST path=/booking\nfunc Book(ctx context.Context, p *BookParams) error {\n\teb := errs.B()\n\n\tnow := time.Now()\n\tif p.Start.Before(now) {\n\t\treturn eb.Code(errs.InvalidArgument).Msg(\"start time must be in the future\").Err()\n\t}\n\n\ttx, err := pgxdb.Begin(ctx)\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to start transaction\").Err()\n\t}\n\tdefer tx.Rollback(context.Background()) // committed explicitly on success\n\n\t_, err = query.InsertBooking(ctx, db.InsertBookingParams{\n\t\tStartTime: pgtype.Timestamp{Time: p.Start, Valid: true},\n\t\tEndTime:   pgtype.Timestamp{Time: p.Start.Add(DefaultBookingDuration), Valid: true},\n\t\tEmail:     p.Email,\n\t})\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to insert booking\").Err()\n\t}\n\n\tif err := tx.Commit(ctx); err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to commit transaction\").Err()\n\t}\n\treturn nil\n}\n```\n\nWe are now using the generated type-safe `query.InsertBooking` function to make the database operation.\n\nNotice the `encore:\"sensitive\"` tag on the `Email` field. This tells Encore to scrub this field so that the data is not viewable in the traces for deployed environments. This is useful for fields that contain [sensitive data](/docs/go/primitives/defining-apis#sensitive-data) such as email addresses, passwords, etc.\n\n🥐 Restart `encore run` to cause the database to be created, and then call the `booking.Book` endpoint:\n\n```shell\n$ curl -X POST 'http://localhost:4000/booking' -d '{\"start\": \"2024-12-11T09:00:00Z\", \"email\": \"test@example.com\"}'\n```\n\nCongratulations, you have now booked your first appointment!\n\n## 4. Authentication\n\nTo provide an admin dashboard for our booking system, we need to add authentication to our application so that we can have protected endpoints.\n\nKeep in mind, in this tutorial we'll only include a very basic implementation.\n\n🥐 Let's start by creating a new service named `user`:\n\n```shell\n$ mkdir user\n$ touch user/auth.go\n```\n\n🥐 Add the following contents to `user/auth.go`:\n\n```go\n-- user/auth.go --\n// Service user authenticates users.\npackage user\n\nimport (\n\t\"context\"\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n)\n\ntype Data struct {\n\tEmail string\n}\n\ntype AuthParams struct {\n\tAuthorization string `header:\"Authorization\"`\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, p *AuthParams) (auth.UID, *Data, error) {\n\tif p.Authorization != \"\" {\n\t\treturn \"test\", &Data{}, nil\n\t}\n\treturn \"\", nil, errs.B().Code(errs.Unauthenticated).Msg(\"no auth header\").Err()\n}\n\n```\n\nThis function is our [auth handler](/docs/go/develop/auth#the-auth-handler). An Encore applications can designate a special function to handle authentication,\nby defining a function and annotating it with `//encore:authhandler`. This annotation tells Encore to run the function whenever an\nincoming API call contains authentication data.\n\nThe auth handler is responsible for validating the incoming authentication data and returning an `auth.UID` (a string type representing a user id).\nThe `auth.UID` can be whatever you wish, but in practice it usually maps directly to the primary key stored in a user table (either defined in the Encore service or in an external service like Firebase or Okta).\n\nIn order to keep this example simple, we'll just approve any request containing a token that is not empty.\n\nNext we will implement some of our auth endpoints and make use of our newly created auth handler.\n\n## 5. Setting availability\n\nRight now the availability is hardcoded to 9:00 - 17:00. Let's add the functionality to let our admin users customize this.\n\nLet's start by adding another migration file, this time to create an `availability` table.\n\n🥐 Create a file called `2_add_availability.up.sql` inside the `booking/db/migrations` folder. Add the following contents to that file:\n\n```sql\n-- booking/db/migrations/2_add_availability.up.sql --\nCREATE TABLE availability (\n    weekday SMALLINT NOT NULL PRIMARY KEY, -- Sunday=0, Monday=1, etc.\n    start_time TIME NULL, -- null indicates not available\n    end_time TIME NULL -- null indicates not available\n);\n\n-- Add some placeholder availability to get started\nINSERT INTO availability (weekday, start_time, end_time) VALUES\n    (0, '09:30', '17:00'),\n    (1, '09:00', '17:00'),\n    (2, '09:00', '18:00'),\n    (3, '08:30', '18:00'),\n    (4, '09:00', '17:00'),\n    (5, '09:00', '17:00'),\n    (6, '09:30', '16:30');\n```\n\n🥐 We can now add two queries to `booking/db/query.sql` so that we can store and retrieve availability:\n\n```sql\n-- booking/db/query.sql --\n-- name: GetAvailability :many\nSELECT * FROM availability\nORDER BY weekday;\n\n-- name: UpdateAvailability :exec\nINSERT INTO availability (weekday, start_time, end_time)\nVALUES (@weekday, @start_time, @end_time)\nON CONFLICT (weekday) DO UPDATE\nSET start_time = @start_time, end_time = @end_time;\n```\n\n🥐 Run `sqlc generate` to update the generated Go code.\n\n🥐 Create a new file in the `booking` service named `availability.go`:\n\n```shell\n$ touch booking/availability.go\n```\n\n🥐 Add the following to that file:\n\n```go\n-- booking/availability.go --\npackage booking\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"encore.app/booking/db\"\n\t\"github.com/jackc/pgx/v5/pgtype\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/rlog\"\n)\n\ntype Availability struct {\n\tStart *string `json:\"start\" encore:\"optional\"`\n\tEnd   *string `json:\"end\" encore:\"optional\"`\n}\n\ntype GetAvailabilityResponse struct {\n\tAvailability []Availability\n}\n\n//encore:api public method=GET path=/availability\nfunc GetAvailability(ctx context.Context) (*GetAvailabilityResponse, error) {\n\trows, err := query.GetAvailability(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tavailability := make([]Availability, 7)\n\tfor _, row := range rows {\n\t\tday := row.Weekday\n\t\tif day < 0 || day > 6 {\n\t\t\trlog.Error(\"invalid week day in availability table\", \"row\", row)\n\t\t\tcontinue\n\t\t}\n\n\t\t// These never fail\n\t\tstart, _ := row.StartTime.TimeValue()\n\t\tend, _ := row.EndTime.TimeValue()\n\t\tavailability[day] = Availability{\n\t\t\tStart: timeToStr(start),\n\t\t\tEnd:   timeToStr(end),\n\t\t}\n\t}\n\n\treturn &GetAvailabilityResponse{Availability: availability}, nil\n}\n\ntype SetAvailabilityParams struct {\n\tAvailability []Availability\n}\n\n//encore:api auth method=POST path=/availability\nfunc SetAvailability(ctx context.Context, params SetAvailabilityParams) error {\n\teb := errs.B()\n\ttx, err := pgxdb.Begin(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback(context.Background()) // committed explicitly on success\n\n\tqry := query.WithTx(tx)\n\tfor weekday, a := range params.Availability {\n\t\tif weekday > 6 {\n\t\t\treturn eb.Code(errs.InvalidArgument).Msgf(\"invalid weekday %d\", weekday).Err()\n\t\t}\n\n\t\tstart, err1 := strToTime(a.Start)\n\t\tend, err2 := strToTime(a.End)\n\t\tif err := errors.Join(err1, err2); err != nil {\n\t\t\treturn eb.Cause(err).Code(errs.InvalidArgument).Msg(\"invalid start/end time\").Err()\n\t\t} else if start.Valid != end.Valid {\n\t\t\treturn eb.Code(errs.InvalidArgument).Msg(\"both start/stop must be set, or both null\").Err()\n\t\t} else if start.Valid && start.Microseconds > end.Microseconds {\n\t\t\treturn eb.Code(errs.InvalidArgument).Msg(\"start must be before end\").Err()\n\t\t}\n\n\t\terr = qry.UpdateAvailability(ctx, db.UpdateAvailabilityParams{\n\t\t\tWeekday:   int16(weekday),\n\t\t\tStartTime: start,\n\t\t\tEndTime:   end,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to update availability\").Err()\n\t\t}\n\t}\n\n\terr = tx.Commit(ctx)\n\treturn errs.WrapCode(err, errs.Unavailable, \"failed to commit transaction\")\n}\n```\n\nThis file contains two endpoints, a setter and a getter. The `SetAvailability` endpoint is protected by the `auth` middleware which means that the user must be authenticated in order to call it. The `GetAvailability` endpoint is public and can be called without authentication.\n\n🥐 Let's set the availability for each day of the week. Open the Development Dashboard at [http://localhost:9400](http://localhost:9400) and select the `booking.SetAvailability` endpoint in the API Explorer. For the request body, paste the following:\n\n```json\n{\n    \"Availability\": [{\n        \"start\": \"09:30\",\n        \"end\": \"17:00\"\n    },{\n        \"start\": \"09:00\",\n        \"end\": \"17:00\"\n    },{\n        \"start\": \"09:00\",\n        \"end\": \"18:00\"\n    },{\n        \"start\": \"08:30\",\n        \"end\": \"18:00\"\n    },{\n        \"start\": \"09:00\",\n        \"end\": \"17:00\"\n    },{\n        \"start\": \"09:00\",\n        \"end\": \"17:00\"\n    },{\n        \"start\": \"09:30\",\n        \"end\": \"16:30\"\n    }]\n}\n```\n\n<Callout type=\"info\">\n\nDon't leave the auth token empty, it will cause the auth handler to reject the request. You can use any value for the auth token.\n\n</Callout>\n\nNow try retrieving the availability by calling the `booking.GetAvailability` endpoint through the API Explorer in the Development Dashboard.\n\n🥐 Add the following functions inside the `booking` package, and import the `slices` package:\n\n```go\nfunc listBookingsBetween(\n\tctx context.Context,\n\tstart, end time.Time,\n) ([]*Booking, error) {\n\trows, err := query.ListBookingsBetween(ctx, db.ListBookingsBetweenParams{\n\t\tStartTime: pgtype.Timestamp{Time: start, Valid: true},\n\t\tEndTime:   pgtype.Timestamp{Time: end, Valid: true},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar bookings []*Booking\n\tfor _, row := range rows {\n\t\tbookings = append(bookings, &Booking{\n\t\t\tID:    row.ID,\n\t\t\tStart: row.StartTime.Time,\n\t\t\tEnd:   row.EndTime.Time,\n\t\t\tEmail: row.Email,\n\t\t})\n\t}\n\treturn bookings, nil\n}\n\nfunc filterBookableSlots(\n\tslots []BookableSlot,\n\tnow time.Time,\n\tbookings []*Booking,\n) []BookableSlot {\n\t// Remove slots for which the start time has already passed.\n\tslots = slices.DeleteFunc(slots, func(s BookableSlot) bool {\n\t\t// Has the slot already passed?\n\t\tif s.Start.Before(now) {\n\t\t\treturn true\n\t\t}\n\n\t\t// Is there a booking that overlaps with this slot?\n\t\tfor _, b := range bookings {\n\t\t\tif b.Start.Before(s.End) && b.End.After(s.Start) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t})\n\treturn slots\n}\n```\n\nWe'll use these functions to figure out which slots are bookable, and which are not, to avoid double bookings.\n\n🥐 Now we can update the `Book` endpoint inside `booking.go` and make use of these new functions:\n\n```go\nHL booking/booking.go 15:27\n-- booking/booking.go --\n//encore:api public method=POST path=/booking\nfunc Book(ctx context.Context, p *BookParams) error {\n\teb := errs.B()\n\n\tnow := time.Now()\n\tif p.Start.Before(now) {\n\t\treturn eb.Code(errs.InvalidArgument).Msg(\"start time must be in the future\").Err()\n\t}\n\n\ttx, err := pgxdb.Begin(ctx)\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to start transaction\").Err()\n\t}\n\tdefer tx.Rollback(context.Background()) // committed explicitly on success\n\n\t// Get the bookings for this day.\n\tstartOfDay := time.Date(p.Start.Year(), p.Start.Month(), p.Start.Day(), 0, 0, 0, 0, p.Start.Location())\n\tbookings, err := listBookingsBetween(ctx, startOfDay, startOfDay.AddDate(0, 0, 1))\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to list bookings\").Err()\n\t}\n\n\t// Is this slot bookable?\n\tslot := BookableSlot{Start: p.Start, End: p.Start.Add(DefaultBookingDuration)}\n\tif len(filterBookableSlots([]BookableSlot{slot}, now, bookings)) == 0 {\n\t\treturn eb.Code(errs.InvalidArgument).Msg(\"slot is unavailable\").Err()\n\t}\n\n\t_, err = query.InsertBooking(ctx, db.InsertBookingParams{\n\t\tStartTime: pgtype.Timestamp{Time: p.Start, Valid: true},\n\t\tEndTime:   pgtype.Timestamp{Time: p.Start.Add(DefaultBookingDuration), Valid: true},\n\t\tEmail:     p.Email,\n\t})\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to insert booking\").Err()\n\t}\n\n\tif err := tx.Commit(ctx); err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to commit transaction\").Err()\n\t}\n\treturn nil\n}\n```\n\n🥐 Inside `slots.go`, update the `GetBookableSlots` endpoint and the `bookableSlotsForDay` functions to look like this:\n\n```go\nHL booking/slots.go 7:12\nHL booking/slots.go 18:23\nHL booking/slots.go 29:36\nHL booking/slots.go 39:48\n-- booking/slots.go --\n//encore:api public method=GET path=/slots/:from\nfunc GetBookableSlots(ctx context.Context, from string) (*SlotsResponse, error) {\n\tfromDate, err := time.Parse(\"2006-01-02\", from)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tavailabilityResp, err := GetAvailability(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tavailability := availabilityResp.Availability\n\n\tconst numDays = 7\n\n\tvar slots []BookableSlot\n\tfor i := 0; i < numDays; i++ {\n\t\tdate := fromDate.AddDate(0, 0, i)\n\t\tweekday := int(date.Weekday())\n\t\tif len(availability) <= weekday {\n\t\t\tbreak\n\t\t}\n\t\tdaySlots, err := bookableSlotsForDay(date, &availability[weekday])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tslots = append(slots, daySlots...)\n\t}\n\n\t// Get bookings for the next 7 days.\n\tactiveBookings, err := listBookingsBetween(ctx, fromDate, fromDate.AddDate(0, 0, numDays))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tslots = filterBookableSlots(slots, time.Now(), activeBookings)\n\treturn &SlotsResponse{Slots: slots}, nil\n}\n\nfunc bookableSlotsForDay(date time.Time, avail *Availability) ([]BookableSlot, error) {\n\tif avail.Start == nil || avail.End == nil {\n\t\treturn nil, nil\n\t}\n\tavailStartTime, err1 := strToTime(avail.Start)\n\tavailEndTime, err2 := strToTime(avail.End)\n\tif err := errors.Join(err1, err2); err != nil {\n\t\treturn nil, err\n\t}\n\n\tavailStart := date.Add(time.Duration(availStartTime.Microseconds) * time.Microsecond)\n\tavailEnd := date.Add(time.Duration(availEndTime.Microseconds) * time.Microsecond)\n\n\t// Compute the bookable slots in this day, based on availability.\n\tvar slots []BookableSlot\n\tstart := availStart\n\tfor {\n\t\tend := start.Add(DefaultBookingDuration)\n\t\tif end.After(availEnd) {\n\t\t\tbreak\n\t\t}\n\t\tslots = append(slots, BookableSlot{\n\t\t\tStart: start,\n\t\t\tEnd:   end,\n\t\t})\n\t\tstart = end\n\t}\n\n\treturn slots, nil\n}\n```\n\n## 6. Managing scheduled bookings\n\nTo display the scheduled bookings in the admin dashboard, we need to add the functionality to list all bookings. While we're at it, we'll also make it possible to delete bookings.\n\n🥐 Add two new endpoints to `booking/booking.go`:\n\n```go\n-- booking/booking.go --\ntype ListBookingsResponse struct {\n\tBooking []*Booking `json:\"bookings\"`\n}\n\n//encore:api auth method=GET path=/booking\nfunc ListBookings(ctx context.Context) (*ListBookingsResponse, error) {\n\trows, err := query.ListBookings(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar bookings []*Booking\n\tfor _, row := range rows {\n\t\tbookings = append(bookings, &Booking{\n\t\t\tID:    row.ID,\n\t\t\tStart: row.StartTime.Time,\n\t\t\tEnd:   row.EndTime.Time,\n\t\t\tEmail: row.Email,\n\t\t})\n\t}\n\treturn &ListBookingsResponse{Booking: bookings}, nil\n}\n\n//encore:api auth method=DELETE path=/booking/:id\nfunc DeleteBooking(ctx context.Context, id int64) error {\n\treturn query.DeleteBooking(ctx, id)\n}\n```\n\nThat's it! We now have all the backend endpoints in place to be able to supply the frontend with data. 🎉\n\n## 7. Running the React frontend\n\nThe frontend should now be working as expected.\n\n🥐 Go to [http://localhost:4000/frontend/](http://localhost:4000/frontend/) and try out your new booking system.\n\nThe frontend is built using [React](https://react.dev/) and [Tailwind CSS](https://tailwindcss.com/). It uses Encore's ability to generate type-safe [request clients](https://encore.dev/docs/go/cli/client-generation). This means you don't need to manually keep the request/response objects in sync on the frontend. To generate a client:\n\n```bash\n$ encore gen client <APP_NAME> --output=./src/client.ts --env=<ENV_NAME>\n```\n\nWhile you're developing, you are going to want to run this command quite often (whenever you make a change to your endpoints) so having it as an `npm` script is a good idea. Take a look at the scripts in the `package.json` file:\n\n```json\n{\n...\n\"scripts\": {\n    ...\n    \"gen\": \"encore gen client <Encore App ID> --output=./src/lib/client.ts --env=staging\",\n    \"gen:local\": \"encore gen client <Encore App ID> --output=./src/lib/client.ts --env=local\"\n  },\n}\n```\n\nFor this frontend we use the request client together with [TanStack Query](https://tanstack.com/query/latest). When building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale. [TanStack Query](https://tanstack.com/query/latest) is a popular library that was built to solve exactly these problems and works great with the Encore request client.\n\nSee our the docs page about [integrating with a web frontend](/docs/how-to/integrate-frontend) to learn more.\n\n## 8. Deploy to Encore's development cloud\n\nLet's deploy the project to Encore's free development cloud.\n\nEncore comes with built-in CI/CD, and the deployment process is as simple as a `git push`.\n(You can also integrate with GitHub to activate per Pull Request Preview Environments, learn more in the [CI/CD docs](/docs/platform/deploy/deploying).)\n\n🥐 Now, let's deploy your app to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\nFrom there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment.\n\n🥐 When the deploy has finished, you can try out your booking system by going to `https://staging-$APP_ID.encr.app/frontend/`.\n\n*You now have an Appointment Booking System running in the cloud, well done!*\n\n## 8. Sending confirmation emails using SendGrid\n\nIn order for the users to get a confirmation email when they book an appointment we need to add an email integration.\n\nConveniently for us, there is a ready to use SendGrid integration as an [Encore Bit](https://github.com/encoredev/examples?tab=readme-ov-file#bits).\n\n🥐 [Follow the instructions](https://github.com/encoredev/examples/tree/main/bits/sendgrid) to add the SendGrid integration to your project.\n\nNext, we need to call our new `sendgrid` service when an appointment is booked.\n\n🥐 Add a call to `sendgrid.Send` in the `Book` endpoint:\n\n```go\nHL booking/booking.go 41:59\n-- booking/booking.go --\n//encore:api public method=POST path=/booking\nfunc Book(ctx context.Context, p *BookParams) error {\n\teb := errs.B()\n\n\tnow := time.Now()\n\tif p.Start.Before(now) {\n\t\treturn eb.Code(errs.InvalidArgument).Msg(\"start time must be in the future\").Err()\n\t}\n\n\ttx, err := pgxdb.Begin(ctx)\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to start transaction\").Err()\n\t}\n\tdefer tx.Rollback(context.Background()) // committed explicitly on success\n\n\t// Get the bookings for this day.\n\tstartOfDay := time.Date(p.Start.Year(), p.Start.Month(), p.Start.Day(), 0, 0, 0, 0, p.Start.Location())\n\tbookings, err := listBookingsBetween(ctx, startOfDay, startOfDay.AddDate(0, 0, 1))\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to list bookings\").Err()\n\t}\n\n\t// Is this slot bookable?\n\tslot := BookableSlot{Start: p.Start, End: p.Start.Add(DefaultBookingDuration)}\n\tif len(filterBookableSlots([]BookableSlot{slot}, now, bookings)) == 0 {\n\t\treturn eb.Code(errs.InvalidArgument).Msg(\"slot is unavailable\").Err()\n\t}\n\n\t_, err = query.InsertBooking(ctx, db.InsertBookingParams{\n\t\tStartTime: pgtype.Timestamp{Time: p.Start, Valid: true},\n\t\tEndTime:   pgtype.Timestamp{Time: p.Start.Add(DefaultBookingDuration), Valid: true},\n\t\tEmail:     p.Email,\n\t})\n\tif err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to insert booking\").Err()\n\t}\n\n\tif err := tx.Commit(ctx); err != nil {\n\t\treturn eb.Cause(err).Code(errs.Unavailable).Msg(\"failed to commit transaction\").Err()\n\t}\n\n\t// Send confirmation email using SendGrid\n\tformattedTime := pgtype.Timestamp{Time: p.Start, Valid: true}.Time.Format(\"2006-01-02 15:04\")\n\t_, err = sendgrid.Send(ctx, &sendgrid.SendParams{\n\t\tFrom: sendgrid.Address{\n\t\t\tName:  \"<your name>\",\n\t\t\tEmail: \"<your email>\",\n\t\t},\n\t\tTo: sendgrid.Address{\n\t\t\tEmail: p.Email,\n\t\t},\n\t\tSubject: \"Booking Confirmation\",\n\t\tText:    \"Thank you for your booking!\\nWe look forward to seeing you soon at \" + formattedTime,\n\t\tHtml:    \"\",\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n```\n\n<Callout type=\"info\">\n\nThe `From` email used when sending emails needs to go through the SendGrid verification process before it can be used. You can read more about it here: https://sendgrid.com/docs/ui/sending-email/sender-verification/\n\nThe default behaviour of the SendGrid integration is to only send emails on production environments. You can create production environments through the Encore Cloud Dashboard.\n\n</Callout>\n\n## 9. Deploy your finished Booking System\n\nNow you're ready to deploy your finished Booking System, complete with a SendGrid integration.\n\n🥐 As before, deploying your app to the cloud is as simple as running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Add sendgrid integration'\n$ git push encore\n```\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n"
  },
  {
    "path": "docs/go/tutorials/graphql.mdx",
    "content": "---\ntitle: Building a GraphQL API\nsubtitle: Learn how to build a GraphQL API using Encore.go\nseotitle: How to build a GraphQL API using Encore.go\nseodesc: Learn how to build a microservices backend in Go, powered by GraphQL and Encore.\nlang: go\n---\n\nEncore has great support for GraphQL with its type-safe approach to building APIs.\n\nEncore's automatic tracing also makes it easy to find and fix\nperformance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)).\n\nThe best way to use GraphQL with Encore is using [gqlgen](https://gqlgen.com/), which\nhas similar goals as Encore (type-safe APIs, minimal boilerplate, code generation, etc).\n\nThe final code will look like this:\n\n<div className=\"not-prose my-10\">\n   <Editor projectName=\"graphql\" />\n</div>\n\n## 1. Create your Encore application\n\nThis tutorial uses the [REST API](/docs/go/tutorials/rest-api) tutorial as a starting point.\n\nYou can either follow that tutorial first, or you can create a new Encore application\nusing the `url-shortener` template by running:\n\n```shell\n$ encore app create --example=url-shortener\n```\n\n## 2. Initialize gqlgen\n\nTo get started, initialize gqlgen by creating a `tools.go` file in the application root:\n\n```go\n-- tools.go --\n//go:build tools\n\npackage tools\n\nimport (\n    _ \"github.com/99designs/gqlgen\"\n    _ \"github.com/99designs/gqlgen/graphql/introspection\"\n)\n```\n\nThen run `go mod tidy` to download the dependencies.\n\nNext, create a `gqlgen.yml` file in the application root containing:\n\n```\n-- gqlgen.yml --\n# Where are all the schema files located? globs are supported eg  src/**/*.graphqls\nschema:\n  - graphql/*.graphqls\n\n# Where should the generated server code go?\nexec:\n  filename: graphql/generated/generated.go\n  package: generated\n\n# Where should any generated models go?\nmodel:\n  filename: graphql/model/models_gen.go\n  package: model\n\n# Where should the resolver implementations go?\nresolver:\n  layout: follow-schema\n  dir: graphql\n  package: graphql\n\n# gqlgen will search for any type names in the schema in these go packages\n# if they match it will use them, otherwise it will generate them.\nautobind:\n - \"encore.app/url\"\n\n# This section declares type mapping between the GraphQL and go type systems\n#\n# The first line in each type will be used as defaults for resolver arguments and\n# modelgen, the others will be allowed when binding to fields. Configure them to\n# your liking\nmodels:\n  ID:\n    model:\n      - github.com/99designs/gqlgen/graphql.ID\n      - github.com/99designs/gqlgen/graphql.Int\n      - github.com/99designs/gqlgen/graphql.Int64\n      - github.com/99designs/gqlgen/graphql.Int32\n  Int:\n    model:\n      - github.com/99designs/gqlgen/graphql.Int\n      - github.com/99designs/gqlgen/graphql.Int64\n      - github.com/99designs/gqlgen/graphql.Int32\n```\n\n## 3. Create Encore service\n\nNow it's time to create our Encore service that will provide the GraphQL API.\n\nFirst generate the gqlgen boilerplate:\n\n```shell\n$ mkdir -p graphql/generated graphql/model\n$ echo \"package model\" > graphql/model/model.go\n$ go run github.com/99designs/gqlgen generate\n```\n\nThis will create a bunch of files in the `graphql` directory.\n\nNext, create a `graphql/service.go` file containing:\n\n```go\n-- graphql/service.go --\n// Service graphql exposes a GraphQL API.\npackage graphql\n\nimport (\n    \"net/http\"\n\n    \"encore.app/graphql/generated\"\n    \"encore.dev\"\n    \"github.com/99designs/gqlgen/graphql/handler\"\n    \"github.com/99designs/gqlgen/graphql/playground\"\n)\n\n//go:generate go run github.com/99designs/gqlgen generate\n\n//encore:service\ntype Service struct {\n    srv        *handler.Server\n    playground http.Handler\n}\n\nfunc initService() (*Service, error) {\n    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &Resolver{}}))\n    pg := playground.Handler(\"GraphQL Playground\", \"/graphql\")\n    return &Service{srv: srv, playground: pg}, nil\n}\n\n//encore:api public raw path=/graphql\nfunc (s *Service) Query(w http.ResponseWriter, req *http.Request) {\n    s.srv.ServeHTTP(w, req)\n}\n\n//encore:api public raw path=/graphql/playground\nfunc (s *Service) Playground(w http.ResponseWriter, req *http.Request) {\n\t// Disable playground in production\n\tif encore.Meta().Environment.Type == encore.EnvProduction {\n        http.Error(w, \"Playground disabled\", http.StatusNotFound)\n\t\treturn\n    }\n\n    s.playground.ServeHTTP(w, req)\n}\n```\n\nThis creates an Encore service that exposes the `/graphql` and `/graphql/playground` endpoints.\n\nIt also adds a `//go:generate` directive that lets you re-run the gqlgen code generation\nby running `go generate ./graphql`.\n\n## 4. Add GraphQL schema\n\nNow it's time to define the GraphQL schema. Create a `graphql/schema.graphqls` file containing:\n\n```\n-- graphql/url.graphqls --\ntype Query {\n  urls: [URL!]!\n  get(id: ID!): URL!\n}\n\ntype Mutation {\n  shorten(input: String!): URL!\n}\n\ntype URL {\n  id:  ID!     # shortened id\n  url: String! # full URL\n}\n```\n\nThen, re-run the code generation to generate the resolver stubs:\n\n```shell\n$ go generate ./graphql\n```\nThe stubs will be written to `graphql/url.resolvers.go` and will contain a bunch of unimplemented resolver methods\nthat look something like this:\n\n```go\n// Shorten is the resolver for the shorten field.\nfunc (r *mutationResolver) Shorten(ctx context.Context, input string) (*url.URL, error) {\n\tpanic(fmt.Errorf(\"not implemented: Shorten - shorten\"))\n}\n```\n\n## 5. Implement resolvers\n\nNow, modify the resolvers to call the `url` service. Since the GraphQL API uses the same types\n(thanks to the `autobind` directive in `gqlgen.yml`) as the Encore API exposes, we can just call the\nendpoints directly. Implement the resolvers in `graphql/url.resolvers.go` like this:\n\n```go\n-- graphql/url.resolvers.go --\n// Shorten is the resolver for the shorten field.\nfunc (r *mutationResolver) Shorten(ctx context.Context, input string) (*url.URL, error) {\n\treturn url.Shorten(ctx, &url.ShortenParams{URL: input})\n}\n\n// Urls is the resolver for the urls field.\nfunc (r *queryResolver) Urls(ctx context.Context) ([]*url.URL, error) {\n\tresp, err := url.List(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.URLs, nil\n}\n\n// Get is the resolver for the get field.\nfunc (r *queryResolver) Get(ctx context.Context, id string) (*url.URL, error) {\n\treturn url.Get(ctx, id)\n}\n```\n\nAs you can see, the resolvers are just thin wrappers around the Encore API endpoints themselves.\n\n## 6. Trying it out\n\nWith that, the GraphQL API is done! Try it out by running `encore run` and opening up [the playground](http://localhost:4000/graphql/playground).\n\nEnter the query:\n```graphql\nmutation {\n    shorten(input: \"https://encore.dev\") {\n        id\n    }\n}\n```\n\nYou should get back an id like `MnTWA8Jo`. Pass the id you got (it will be something different) to a `get` query:\n\n```graphql\nquery {\n    get(id: \"<your-id-here>\") {\n        url\n    }\n}\n```\n\nAnd you should get back `https://encore.dev`.\n\n\n## 7. Deploy\n\n<Accordion>\n\n### Self-hosting\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nIf your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/go/self-host/configure-infra) your Docker image.\n\n🥐 Build a Docker image by running `encore build docker graphql:v1.0`.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\n\n🥐 Upload the Docker image to the cloud provider of your choice and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Deploy to Encore Cloud\n\nEncore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP.\n\n### Create account\n\nBefore deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step.\n\nIf you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template.\n\nAfter creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform).\n\n### Commit changes\n\nThe final step before you deploy is to commit all changes to the project repo.\n\n\nPush your changes and deploy your application to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\nFrom there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment.\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n</Accordion>\n\n## Conclusion\n\nWe've now built a GraphQL API gateway that forwards requests to the application's\nunderlying Encore services in a type-safe way with minimal boilerplate.\n\nNote that the concepts discussed here are general and can be easily adapted to any GraphQL schema.\n\nWhenever you make a change to the schema or configuration, re-run `go generate ./graphql` to\nregenerate the GraphQL boilerplate. And for more information on how to use `gqlgen`,\nsee the [gqlgen documentation](https://gqlgen.com/).\n"
  },
  {
    "path": "docs/go/tutorials/incident-management-tool.md",
    "content": "---\nseotitle: How to build an Incident Management Tool with Go\nseodesc: Learn how to build an incident management tool like PagerDuty using Go and Encore. Get a working app running in the cloud in 30 minutes!\ntitle: Building an Incident Management Tool\nsubtitle: Set up your own PagerDuty from zero-to-production in just 30 minutes\nsocial_card: /assets/docs/incident-og-image.png\nlang: go\n---\n\nIn this tutorial, we're going to walk through together how to build our very own Incident Management Tool like [Incident.io](https://incident.io) or [PagerDuty](https://pagerduty.com). We can then have our own on call schedule that can be rotated between many users, and have incidents come and be assigned according to the schedule!\n\n![Slack Incident Management Tool](/assets/docs/incident-slack-example.png \"Incident Management Tool\")\n\nIn about 30 minutes, your application will be able to support:\n\n- Creating users, as well as schedules for when users will be on call\n- Creating incidents, and reminders for unacknowledged incidents on Slack every 10 minutes\n- Auto-assign incidents which are unassigned (when the next user is on call)\n\n_ Sounds good? Let's dig in! _\n\nOr if you'd rather watch a video of this tutorial, you can do that below.\n\n<iframe width=\"360\" height=\"202\" src=\"https://www.youtube.com/embed/BR_ys_qR2kI?controls=0\" title=\"Building an Incident Management Tool Video Tutorial\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n\nView full project on [GitHub](https://github.com/encoredev/example-app-oncall)\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n## 1. Create your Encore application\n\n🥐 Create a new Encore application by running `encore app create`, select `Empty app` as the template and name it `oncall-tutorial`.\n\n## 2. Integrate with Slack\n\n🥐 Follow [this guide to create your own Incoming Webhook](https://api.slack.com/messaging/webhooks) for your Slack workspace. Incoming webhooks cannot read messages, and can only post to a specific channel of your choice.\n\n<img src=\"/assets/docs/incident-slack-app-creation.png\" alt=\"Creating a Slack app\" width=\"400px\" />\n\n🥐 Once you have your Webhook URL which starts with `https://hooks.slack.com/services/...` then copy and paste that and run the following commands to save these as secrets. We recommend having a different webhook/channel for development and production.\n\n```shell\n$ encore secret set --type dev,local,pr SlackWebhookURL\n$ encore secret set --type prod SlackWebhookURL\n```\n\n🥐 Next, let's create our `slack` service that contains the logic for calling the Webhook URL in order to post notifications to our Slack. To do this we need to implement our code in `slack/slack.go`:\n\n```go\n// Service slack calls a webhook to post notifications to Slack.\npackage slack\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"encore.dev/beta/errs\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype NotifyParams struct {\n\tText string `json:\"text\"`\n}\n\n//encore:api private\nfunc Notify(ctx context.Context, p *NotifyParams) error {\n\teb := errs.B()\n\treqBody, err := json.Marshal(p)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", secrets.SlackWebhookURL, bytes.NewReader(reqBody))\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn eb.Code(errs.Unavailable).Msgf(\"notify slack: %s: %s\", resp.Status, body).Err()\n\t}\n\treturn nil\n}\n\nvar secrets struct {\n\tSlackWebhookURL string\n}\n```\n\n<Callout type=\"info\">\n\nThe `slack` service can be reused across any of your Encore apps. All you need is the `slack/slack.go` code and the `SlackWebhookURL` secret to be defined. Then you can call the following method signature anywhere in your app:\n\n```go\nslack.Notify(context, &slack.NotifyParams{ Text: \"Send a Slack notification\" })\n```\n\n</Callout>\n\n## 3. Create a service to manage users\n\nWith an Incident Management Tool (or usually any tool, for that matter) we need a service for users.\nThis will allow us to figure out who we should assign incoming incidents to!\n\nTo get started, we need to create a `users` service with the following resources:\n\n| #   | Type                                 | Description / Filename                                                                   |\n| --- | ------------------------------------ | ---------------------------------------------------------------------------------------- |\n| #1  | SQL Migration                        | Our PostgreSQL schema for scheduling data <br/> `users/migrations/1_create_users.up.sql` |\n| #2  | HTTP Endpoint <br/> `POST /users`    | Create a new User <br/> `users/users.go`                                                 |\n| #3  | HTTP Endpoint <br/> `GET /users/:id` | Get an existing User <br/> `users/users.go`                                              |\n\nWith #1, let's design our database schema for a User in our system. For now let's store a first and last name as well as a Slack handle in case we need to notify them about any incidents which may have been assigned to them or acknowledged by them.\n\n🥐 Let's create our migration file in `users/migrations/1_create_users.up.sql`:\n\n```sql\nCREATE TABLE users (\n    id           BIGSERIAL PRIMARY KEY,\n    first_name   VARCHAR(255) NOT NULL,\n    last_name    VARCHAR(255) NOT NULL,\n    slack_handle VARCHAR(255) NOT NULL\n);\n```\n\n🥐 Then, we need to write our code to implement the HTTP endpoints listed in #2 (for creating a user) and #3 (for listing a user) belonging in `users/users.go`. Let's split them out into three sections: our structs (i.e. data models) and methods.\n\n```go\n// Service users manages users and assigns incidents.\npackage users\n\nimport (\n\t\"context\"\n\t\"encore.dev/storage/sqldb\"\n)\n\n// This is a Go struct representing our PostgreSQL schema for `users`\ntype User struct {\n\tId          int32\n\tFirstName   string\n\tLastName    string\n\tSlackHandle string\n}\n\n// Define a database named 'users', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nvar db = sqldb.NewDatabase(\"users\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n//encore:api public method=POST path=/users\nfunc Create(ctx context.Context, params CreateParams) (*User, error) {\n\tuser := User{}\n\terr := db.QueryRow(ctx, `\n\t\tINSERT INTO users (first_name, last_name, slack_handle)\n\t\tVALUES ($1, $2, $3)\n\t\tRETURNING id, first_name, last_name, slack_handle\n\t`, params.FirstName, params.LastName, params.SlackHandle).Scan(&user.Id, &user.FirstName, &user.LastName, &user.SlackHandle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &user, nil\n}\n\n// This is what JSON params our POST /users endpoint will accept\ntype CreateParams struct {\n\tFirstName   string\n\tLastName    string\n\tSlackHandle string\n}\n\n//encore:api public method=GET path=/users/:id\nfunc Get(ctx context.Context, id int32) (*User, error) {\n\tuser := User{}\n\terr := db.QueryRow(ctx, `\n\t  SELECT id, first_name, last_name, slack_handle\n\t\tFROM users\n\t\tWHERE id = $1\n\t`, id).Scan(&user.Id, &user.FirstName, &user.LastName, &user.SlackHandle)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &user, nil\n}\n```\n\n🥐 Next, type `encore run` in your Terminal and in a separate window run the command under **cURL Request** (feel free to edit the values!) to create our first user:\n\n```bash\ncurl -d '{\n  \"FirstName\":\"Katy\",\n  \"LastName\":\"Smith\",\n  \"SlackHandle\":\"katy\"\n}' http://localhost:4000/users\n\n# Example JSON response\n# {\n#   \"Id\":1,\n#   \"FirstName\":\"Katy\",\n#   \"LastName\":\"Smith\",\n#   \"SlackHandle\":\"katy\"\n# }\n```\n\nFantastic, we now have a user system in our app! Next we need a list of start and end times of each scheduled rotation so we know who to assign incoming incidents to (as well as notify them on Slack!)\n\n## 4. Add scheduling\n\nA good incident management tool should be able to spread the workload of diagnosing and fixing incidents across multiple users in a team. Being able to know who the correct person to assign an incident to is very important; our incidents might not get resolved quickly otherwise!\n\nIn order to achieve this, let's create a new service called `schedules`:\n\n| #   | Type                                            | Description / Filename                                                                     |\n| --- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ |\n| #1  | SQL Migration                                   | Our PostgreSQL schema for user data <br/> `schedules/migrations/1_create_schedules.up.sql` |\n| #2  | HTTP Endpoint <br/> `GET /schedules`            | Get list of schedules between time range <br/> `schedules/schedules.go`                    |\n| #3  | HTTP Endpoint <br/> `POST /users/:id/schedules` | Create a new Schedule <br/> `schedules/schedules.go`                                       |\n| #4  | HTTP Endpoint <br/> `GET /scheduled/:timestamp` | Get Schedule at specific time <br/> `schedules/schedules.go`                               |\n\n\nFor the SQL migration in #1, we need to create both a table and an index. For every rotation let's need a new entry containing the user who it is for as well as the start and end times of the scheduled rotation.\n\n🥐 Let's create our migration file in `schedules/migrations/1_create_schedules.up.sql`:\n\n```sql\nCREATE TABLE schedules\n(\n    id         BIGSERIAL PRIMARY KEY,\n    user_id    INTEGER   NOT NULL,\n    start_time TIMESTAMP NOT NULL,\n    end_time   TIMESTAMP NOT NULL\n);\n\nCREATE INDEX schedules_range_index ON schedules (start_time, end_time);\n```\n\n\n<Callout type=\"info\">\n\nTable indexes are used to optimize lookups without having to search every row in the table. In this case, looking up rows against both `start_time` and `end_time` will be faster _with the index_ as the dataset grows. [Learn more about PostgreSQL indexes here](https://www.tutorialspoint.com/postgresql/postgresql_indexes.htm).\n\n</Callout>\n\n🥐 Next, let's implement the HTTP endpoints for #2 (listing schedules), #3 (creating a schedule) and #4 (getting the schedule/user at a specific time) in `schedules/schedules.go`:\n\n```go\n// Service schedules implements schedules to answer who should be assigned to an incident.\npackage schedules\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"encore.app/users\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/storage/sqldb\"\n)\n\n// Define a database named 'schedules', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nvar db = sqldb.NewDatabase(\"schedules\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// This struct holds multiple Schedule structs\ntype Schedules struct {\n\tItems []Schedule\n}\n\n// This is a Go struct representing our PostgreSQL schema for `schedules`\ntype Schedule struct {\n\tId   int32\n\tUser users.User\n\tTime TimeRange\n}\n\n// As we use time ranges in our schedule, we created a generic TimeRange struct\ntype TimeRange struct {\n\tStart time.Time\n\tEnd   time.Time\n}\n\n//encore:api public method=POST path=/users/:userId/schedules\nfunc Create(ctx context.Context, userId int32, timeRange TimeRange) (*Schedule, error) {\n\teb := errs.B().Meta(\"userId\", userId, \"timeRange\", timeRange)\n\t// check for existing overlapping schedules\n\tif schedule, err := ScheduledAt(ctx, timeRange.Start.String()); schedule != nil && err == nil {\n\t\treturn nil, eb.Code(errs.InvalidArgument).Cause(err).Msg(\"schedule already exists within this start timestamp\").Err()\n\t}\n\tif schedule, err := ScheduledAt(ctx, timeRange.End.String()); schedule != nil && err == nil {\n\t\treturn nil, eb.Code(errs.InvalidArgument).Cause(err).Msg(\"schedule already exists within this end timestamp\").Err()\n\t}\n\n\t// check user exists\n\tuser, err := users.Get(ctx, userId)\n\tif err != nil {\n\t\treturn nil, eb.Code(errs.Unavailable).Cause(err).Msg(\"failed to get user\").Err()\n\t}\n\n\tschedule := Schedule{User: *user, Time: TimeRange{}}\n\terr = db.QueryRow(\n\t\tctx,\n\t\t`INSERT INTO schedules (user_id, start_time, end_time) VALUES ($1, $2, $3) RETURNING id, start_time, end_time`,\n\t\tuserId, timeRange.Start, timeRange.End,\n\t).Scan(&schedule.Id, &schedule.Time.Start, &schedule.Time.End)\n\tif err != nil {\n\t\treturn nil, eb.Code(errs.Unavailable).Cause(err).Msg(\"failed to insert schedule\").Err()\n\t}\n\n\treturn &schedule, nil\n}\n\n//encore:api public method=GET path=/scheduled\nfunc ScheduledNow(ctx context.Context) (*Schedule, error) {\n\treturn scheduled(ctx, time.Now())\n}\n\n//encore:api public method=GET path=/scheduled/:timestamp\nfunc ScheduledAt(ctx context.Context, timestamp string) (*Schedule, error) {\n\teb := errs.B().Meta(\"timestamp\", timestamp)\n\tparsedtime, err := time.Parse(time.RFC3339, timestamp)\n\tif err != nil {\n\t\treturn nil, eb.Code(errs.InvalidArgument).Msg(\"timestamp is not in a valid format\").Err()\n\t}\n\n\treturn scheduled(ctx, parsedtime)\n}\n\nfunc scheduled(ctx context.Context, timestamp time.Time) (*Schedule, error) {\n\teb := errs.B().Meta(\"timestamp\", timestamp)\n\tschedule, err := RowToSchedule(ctx, db.QueryRow(ctx, `\n\t\tSELECT id, user_id, start_time, end_time\n\t\tFROM schedules\n\t\tWHERE start_time <= $1\n\t\t  AND end_time >= $1\n\t`, timestamp.UTC()))\n\tif errors.Is(err, db.ErrNoRows) {\n\t\treturn nil, eb.Code(errs.NotFound).Msg(\"no schedule found\").Err()\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn schedule, nil\n}\n\n//encore:api public method=GET path=/schedules\nfunc ListByTimeRange(ctx context.Context, timeRange TimeRange) (*Schedules, error) {\n\trows, err := db.Query(ctx, `\n\t\tSELECT id, user_id, start_time, end_time\n\t\tFROM schedules\n\t\tWHERE start_time >= $1\n\t\tAND end_time <= $2\n\t\tORDER BY start_time ASC\n\t`, timeRange.Start, timeRange.End)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer rows.Close()\n\n\tvar schedules []Schedule\n\tfor rows.Next() {\n\t\tschedule, err := RowToSchedule(ctx, rows)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tschedules = append(schedules, *schedule)\n\t}\n\n\treturn &Schedules{Items: schedules}, nil\n}\n\n//encore:api public method=DELETE path=/schedules\nfunc DeleteByTimeRange(ctx context.Context, timeRange TimeRange) (*Schedules, error) {\n\tschedules, err := ListByTimeRange(ctx, timeRange)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, err = db.Exec(ctx, `DELETE FROM schedules WHERE start_time >= $1 AND end_time <= $2`, timeRange.Start, timeRange.End)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn schedules, err\n}\n\n// Helper function to convert a Row object to to Schedule\nfunc RowToSchedule(ctx context.Context, row interface {\n\tScan(dest ...interface{}) error\n}) (*Schedule, error) {\n\tvar schedule = &Schedule{Time: TimeRange{}}\n\tvar userId int32\n\n\terr := row.Scan(&schedule.Id, &userId, &schedule.Time.Start, &schedule.Time.End)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser, err := users.Get(ctx, userId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tschedule.User = *user\n\treturn schedule, nil\n}\n```\n\n🥐 Next, type `encore run` in your Terminal and in a separate window run the command under **cURL Request** (also feel free to edit the values!) to create our first schedule against the user we created earlier:\n\n```bash\ncurl -d '{\n  \"Start\":\"2023-11-28T10:00:00Z\",\n  \"End\":\"2023-11-30T10:00:00Z\"\n}' \"http://localhost:4000/users/1/schedules\"\n\n# Example JSON response\n# {\n#   \"Id\":1,\n#   \"User\":{\n#     \"Id\":1,\n#     \"FirstName\":\"Katy\",\n#     \"LastName\":\"Smith\",\n#     \"SlackHandle\":\"katy\"\n#   },\n#   \"Time\":{\n#     \"Start\":\"2023-11-28T10:00:00Z\",\n#     \"End\":\"2023-11-30T10:00:00Z\"\n#   }\n# }\n```\n\n## 5. Create a service to manage incidents\n\nSo we have users, and we know who is available to be notified (or if nobody should be notified) at any given time with the introduction of the `schedules` service. The only thing we're missing is the ability to report, assign and acknowledge incidents!\n\nThe flow we're going to implement is: an incoming incident will arrive, let's either unassign or auto-assign it based on the `schedules` service, and incidents have to be acknowledged. If they are not acknowledged, they will continue to be notified on Slack every 10 minutes until it has.\n\nTo start with, we need to create a new `incidents` service with the following resources:\n\n\n| #   | Type                                                 | Description / Filename                                                                             |\n| --- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| #1  | SQL Migration                                        | Our PostgreSQL schema for storing incidents <br/> `incidents/migrations/1_create_incidents.up.sql` |\n| #2  | HTTP Endpoint <br/> `GET /incidents`                 | Get list of all unacknowledged incidents <br/> `incidents/incidents.go`                            |\n| #3  | HTTP Endpoint <br/> `PUT /incidents/:id/acknowledge` | Acknowledge an incident <br/> `incidents/incidents.go`                                             |\n| #4  | HTTP Endpoint <br/> `GET /scheduled/:timestamp`      | Get  <br/> `incidents/incidents.go`                                                                |\n\nFor the SQL migration in #1, we need to create the table for our incidents. We need to have a one-to-many relationship between an user and an incident. That is, an incident can only be assigned to a single user but a single user can be assigned to many incidents.\n\n🥐 Let's create our migration file in `incidents/migrations/1_create_incidents.up.sql`:\n\n```sql\nCREATE TABLE incidents\n(\n    id               BIGSERIAL PRIMARY KEY,\n    assigned_user_id INTEGER,\n    body             TEXT      NOT NULL,\n    created_at       TIMESTAMP NOT NULL DEFAULT NOW(),\n    acknowledged_at  TIMESTAMP\n);\n```\n\n🥐 Next, our code belonging in `incidents/incidents.go` for being able to support incidents is below:\n\n```go\n// Service incidents reports, assigns and acknowledges incidents.\npackage incidents\n\nimport (\n\t\"context\"\n\t\"encore.app/schedules\"\n\t\"encore.app/slack\"\n\t\"encore.app/users\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/storage/sqldb\"\n\t\"fmt\"\n\t\"time\"\n)\n\n// Define a database named 'incidents', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nvar db = sqldb.NewDatabase(\"incidents\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// This struct holds multiple Incidents structs\ntype Incidents struct {\n\tItems []Incident\n}\n\n// This is a Go struct representing our PostgreSQL schema for `incidents`\ntype Incident struct {\n\tId             int32\n\tBody           string\n\tCreatedAt      time.Time\n\tAcknowledged   bool\n\tAcknowledgedAt *time.Time\n\tAssignee       *users.User\n}\n\n//encore:api public method=GET path=/incidents\nfunc List(ctx context.Context) (*Incidents, error) {\n\trows, err := db.Query(ctx, `\n\t\tSELECT id, assigned_user_id, body, created_at, acknowledged_at\n\t\tFROM incidents\n\t\tWHERE acknowledged_at IS NULL\n\t`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn RowsToIncidents(ctx, rows)\n}\n\n//encore:api public method=PUT path=/incidents/:id/acknowledge\nfunc Acknowledge(ctx context.Context, id int32) (*Incident, error) {\n\teb := errs.B().Meta(\"incidentId\", id)\n\trows, err := db.Query(ctx, `\n\t\tUPDATE incidents\n\t\tSET acknowledged_at = NOW()\n\t\tWHERE acknowledged_at IS NULL\n\t\t  AND id = $1\n\t\tRETURNING id, assigned_user_id, body, created_at, acknowledged_at\n\t`, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tincidents, err := RowsToIncidents(ctx, rows)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif incidents.Items == nil {\n\t\treturn nil, eb.Code(errs.NotFound).Msg(\"no incident found\").Err()\n\t}\n\n\tincident := &incidents.Items[0]\n\t_ = slack.Notify(ctx, &slack.NotifyParams{\n\t\tText: fmt.Sprintf(\"Incident #%d assigned to %s %s <@%s> has been acknowledged:\\n%s\", incident.Id, incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle, incident.Body),\n\t})\n\n\treturn incident, err\n}\n\n//encore:api public method=POST path=/incidents\nfunc Create(ctx context.Context, params *CreateParams) (*Incident, error) {\n\t// check who is on-call\n\tschedule, err := schedules.ScheduledNow(ctx)\n\n\tincident := Incident{}\n\tif schedule != nil {\n\t\tincident.Assignee = &schedule.User\n\t}\n\n\tvar row *db.Row\n\tif schedule != nil {\n\t\t// Someone is on-call\n\t\trow = db.QueryRow(ctx, `\n\t\t\tINSERT INTO incidents (assigned_user_id, body)\n\t\t\tVALUES ($1, $2)\n\t\t\tRETURNING id, body, created_at\n\t\t`, &schedule.User.Id, params.Body)\n\t} else {\n\t\t// Nobody is on-call\n\t\trow = db.QueryRow(ctx, `\n\t\t\tINSERT INTO incidents (body)\n\t\t\tVALUES ($1)\n\t\t\tRETURNING id, body, created_at\n\t\t`, params.Body)\n\t}\n\n\tif err = row.Scan(&incident.Id, &incident.Body, &incident.CreatedAt); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar text string\n\tif incident.Assignee != nil {\n\t\ttext = fmt.Sprintf(\"Incident #%d created and assigned to %s %s <@%s>\\n%s\", incident.Id, incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle, incident.Body)\n\t} else {\n\t\ttext = fmt.Sprintf(\"Incident #%d created and unassigned\\n%s\", incident.Id, incident.Body)\n\t}\n\t_ = slack.Notify(ctx, &slack.NotifyParams{Text: text})\n\n\treturn &incident, nil\n}\n\ntype CreateParams struct {\n\tBody string\n}\n\n// Helper to take a db.Rows instance and convert it into a list of Incidents\nfunc RowsToIncidents(ctx context.Context, rows *db.Rows) (*Incidents, error) {\n\teb := errs.B()\n\n\tdefer rows.Close()\n\n\tvar incidents []Incident\n\tfor rows.Next() {\n\t\tvar incident = Incident{}\n\t\tvar assignedUserId *int32\n\t\tif err := rows.Scan(&incident.Id, &assignedUserId, &incident.Body, &incident.CreatedAt, &incident.AcknowledgedAt); err != nil {\n\t\t\treturn nil, eb.Code(errs.Unknown).Msgf(\"could not scan: %v\", err).Err()\n\t\t}\n\t\tif assignedUserId != nil {\n\t\t\tuser, err := users.Get(ctx, *assignedUserId)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, eb.Code(errs.NotFound).Msgf(\"could not retrieve user for incident %v\", assignedUserId).Err()\n\t\t\t}\n\t\t\tincident.Assignee = user\n\t\t}\n\t\tincident.Acknowledged = incident.AcknowledgedAt != nil\n\t\tincidents = append(incidents, incident)\n\t}\n\n\treturn &Incidents{Items: incidents}, nil\n}\n```\n\nFantastic! We have an _almost_ working application. The main two things we're missing are:\n\n1. For unacknowledged incidents, we need to post a reminder on Slack every 10 minutes until they have been acknolwedged.\n2. Whenever a user is currently on call, we should assign all previously unassigned incidents to them.\n\n🥐 To achieve this, we'll need to create two [Cron Jobs](http://localhost:3000/docs/develop/cron-jobs) which thankfully Encore makes incredibly simple. So let's go ahead and create the first one for reminding us every 10 minutes of incidents we haven't acknowledged. Go ahead and add the code below to our `incidents/incidents.go` file:\n\n```go\n// Track unacknowledged incidents\nvar _ = cron.NewJob(\"unacknowledged-incidents-reminder\", cron.JobConfig{\n\tTitle:    \"Notify on Slack about incidents which are not acknowledged\",\n\tEvery:    10 * cron.Minute,\n\tEndpoint: RemindUnacknowledgedIncidents,\n})\n\n//encore:api private\nfunc RemindUnacknowledgedIncidents(ctx context.Context) error {\n\tincidents, err := List(ctx) // we never query for acknowledged incidents\n\tif err != nil {\n\t\treturn err\n\t}\n\tif incidents == nil {\n\t\treturn nil\n\t}\n\n\tvar items = []string{\"These incidents have not been acknowledged yet. Please acknowledge them otherwise you will be reminded every 10 minutes:\"}\n\tfor _, incident := range incidents.Items {\n\t\tvar assignee string\n\t\tif incident.Assignee != nil {\n\t\t\tassignee = fmt.Sprintf(\"%s %s (<@%s>)\", incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle)\n\t\t} else {\n\t\t\tassignee = \"Unassigned\"\n\t\t}\n\n\t\titems = append(items, fmt.Sprintf(\"[%s] [#%d] %s\", assignee, incident.Id, incident.Body))\n\t}\n\n\tif len(incidents.Items) > 0 {\n\t\t_ = slack.Notify(ctx, &slack.NotifyParams{Text: strings.Join(items, \"\\n\")})\n\t}\n\n\treturn nil\n}\n```\n\nAnd for our second cronjob, when someone goes on call we need to automatically assign the previously unassigned incidents to them. We don't have a HTTP endpoint for assigning incidents so we need to implement a `PUT /incidents/:id/assign` endpoint.\n\n🥐 So let's also add that endpoint as well as the cronjob code to our `incidents/incidents.go` file:\n\n```go\n//encore:api public method=PUT path=/incidents/:id/assign\nfunc Assign(ctx context.Context, id int32, params *AssignParams) (*Incident, error) {\n\teb := errs.B().Meta(\"params\", params)\n\trows, err := db.Query(ctx, `\n\t\tUPDATE incidents\n\t\tSET assigned_user_id = $1\n\t\tWHERE acknowledged_at IS NULL\n\t\t  AND id = $2\n\t\tRETURNING id, assigned_user_id, body, created_at, acknowledged_at\n\t`, params.UserId, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tincidents, err := RowsToIncidents(ctx, rows)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif incidents.Items == nil {\n\t\treturn nil, eb.Code(errs.NotFound).Msg(\"no incident found\").Err()\n\t}\n\n\tincident := &incidents.Items[0]\n\t_ = slack.Notify(ctx, &slack.NotifyParams{\n\t\tText: fmt.Sprintf(\"Incident #%d is re-assigned to %s %s <@%s>\\n%s\", incident.Id, incident.Assignee.FirstName, incident.Assignee.LastName, incident.Assignee.SlackHandle, incident.Body),\n\t})\n\n\treturn incident, err\n}\n\ntype AssignParams struct {\n\tUserId int32\n}\n\nvar _ = cron.NewJob(\"assign-unassigned-incidents\", cron.JobConfig{\n\tTitle:    \"Assign unassigned incidents to user on-call\",\n\tEvery:    1 * cron.Minute,\n\tEndpoint: AssignUnassignedIncidents,\n})\n\n//encore:api private\nfunc AssignUnassignedIncidents(ctx context.Context) error {\n\t// if this fails, we don't have anyone on call so let's skip this\n\tschedule, err := schedules.ScheduledNow(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tincidents, err := List(ctx) // we never query for acknowledged incidents\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, incident := range incidents.Items {\n\t\tif incident.Assignee != nil {\n\t\t\tcontinue // this incident has already been assigned\n\t\t}\n\n\t\t_, err := Assign(ctx, incident.Id, &AssignParams{UserId: schedule.User.Id})\n\t\tif err == nil {\n\t\t\trlog.Info(\"OK assigned unassigned incident\", \"incident\", incident, \"user\", schedule.User)\n\t\t} else {\n\t\t\trlog.Error(\"FAIL to assign unassigned incident\", \"incident\", incident, \"user\", schedule.User, \"err\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n```\n\n🥐 Next, call `encore run` in your Terminal and in a separate window run the command under **cURL Request** (also feel free to edit the values!) to trigger our first incident. Most likely we won't have an assigned user unless you have scheduled a time that overlaps with right now in the last cURL request for creating a schedule:\n\n```bash\ncurl -d '{\n  \"Body\":\"An unexpected error happened on example-website.com on line 38. It needs addressing now!\"\n}' http://localhost:4000/incidents\n\n# Example JSON response\n# {\n#   \"Id\":1,\n#\t  \"Body\":\"An unexpected error happened on example-website.com on line 38. It needs addressing now!\",\n#\t  \"CreatedAt\":\"2022-09-28T15:09:00Z\",\n#\t  \"Acknowledged\":false,\n#\t  \"AcknowledgedAt\":null,\n#   \"Assignee\":null\n# }\n```\n\n## 6. Try your app and deploy\n\nCongratulations! Our application looks ready for others to try - we have our `users`, `schedules` `incidents` and `slack` services along with 3 database tables and 2 cronjobs. Even better that all of the deployment and maintenance is taken care by Encore!\n\n🥐 To try out your application, type `encore run` in your Terminal and run the following cURL commands:\n\n```bash\n# Step 1: Create a User and copy the User ID to your clipboard\ncurl -d '{\n  \"FirstName\":\"Katy\",\n  \"LastName\":\"Smith\",\n  \"SlackHandle\":\"katy\"\n}' http://localhost:4000/users\n\n# Step 2: Create a schedule for the user we just created\ncurl -d '{\n  \"Start\":\"2022-09-28T10:00:00Z\",\n  \"End\":\"2022-09-29T10:00:00Z\"\n}' \"http://localhost:4000/users/1/schedules\"\n\n# Step 3: Trigger an incident\ncurl -d '{\n  \"Body\":\"An unexpected error happened on example-website.com on line 38. It needs addressing now!\"\n}' http://localhost:4000/incidents\n\n# Step 4: Acknowledge the Incident\ncurl -X PUT \"http://localhost:4000/incidents/1/acknowledge\"\n```\n\nAnd if you don't acknowledge incoming incidents on Step 4, you will be reminded on Slack every 10 minutes:\n\n![Being reminded on Slack about unacknowledged incidents](/assets/docs/incident-slack-reminder-example.png)\n\n### Deploy to the cloud\n\n🥐 Push your changes and deploy your application to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\nFrom there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment.\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n### Architecture Diagram\n\nTake a look at the [Encore Flow](/docs/go/observability/encore-flow) diagram that was automatically generated for our new application too!\n\n![Being reminded on Slack about unacknowledged incidents](/assets/docs/incident-flow-diagram.png)\n\n### GitHub Repository\n\n🥐 Check out the `example-app-oncall` repository on GitHub for this example which includes additional features and tests:\n[https://github.com/encoredev/example-app-oncall](https://github.com/encoredev/example-app-oncall)\n\nAlternatively, you can clone our example application by running this in your Terminal:\n\n```shell\n$ encore app create --example https://github.com/encoredev/example-app-oncall\n```\n\n### Feedback\n\n🥐 We'd love to hear your thoughts about this tutorial and learn about what you're building next.\nLet us know by [tweeting your experience](https://twitter.com/encoredotdev), blog about it, or talk to us about it on [Discord](https://encore.dev/discord).\n"
  },
  {
    "path": "docs/go/tutorials/meeting-notes.mdx",
    "content": "---\ntitle: Building a Meeting Notes app\nsubtitle: Learn how to set up a web app backend (with database) in less than 100 lines of code\nseotitle: How to build a Meeting Notes app in Go & React\nseodesc: Learn how to set up a free & production-ready web app backend in Go (with database) in less than 100 lines\nlang: go\n---\n\nIn this tutorial, we will create a backend in less than 100 lines of code. The backend will:\n\n- Store data in a cloud SQL database\n- Make API calls to a third-party service\n- Deploy to the cloud and be publicly available\n\nThe example app we will build is a markdown meeting notes app BUT it’s trivial to replace the specifics if you have another idea in mind (again, less than 100 lines of code).\n\n**[Demo version of the app](https://encoredev.github.io/meeting-notes)**\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/meeting-notes-demo.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nThis is the end result:\n<div className=\"not-prose my-10\">\n  <Editor projectName=\"meetingNotes\" />\n</div>\n\n## Create your Encore application\n\nCreate a new app from the meeting-notes example. This will start you off with everything described in this tutorial:\n\n```shell\n$ encore app create my-app --example=meeting-notes\n```\n\n<Callout type=\"info\">\n\nBefore running the project locally, make sure you have [Docker](https://www.docker.com/products/docker-desktop/) installed and running. Docker is needed for Encore to create databases for locally running projects. Also, if you want to try the photo search functionality then you will need an API key from [pexels.com/api/](https://www.pexels.com/api/) (more on that below)\n\n</Callout>\n\nTo run the backend locally:\n\n```shell\n$ cd you-app-name # replace with the app name you picked\n$ encore run\n```\n\nYou should see the following:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/encorerun.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nThat means your local development backend is up and running! Encore takes care of setting up all the necessary infrastructure for your application, including databases. Encore also starts the local development dashboard which is a tool to help you move faster when you're developing new features.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/localdashvideo.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nTo start the front-end, run the following commands in another terminal window:\n\n```shell\n$ cd you-app-name/frontend\n$ npm install\n$ npm run dev\n```\n\nYou can now open http://localhost:5173/example-meeting-notes/ in your browser 🔥\n\n## Storing and retrieving from an SQL database\n\nLet's take a look at the backend code. There are essentially only three files of interest, let's start by looking at `note.go`. This file contains two endpoints and one interface, all standard Go code except for a few lines specific to Encore.\n\nThe `Note` type represents our data structure:\n\n```go\ntype Note struct {\n\tID       string `json:\"id\"`\n\tText     string `json:\"text\"`\n\tCoverURL string `json:\"cover_url\"`\n}\n```\n\nEvery note will have an `ID` (uuid that is created on the frontend), `Text` (Markdown text content), and `CoverURL` (background image URL).\n\nThe `SaveNote` function handles storing a meeting note:\n\n```go\n//encore:api public method=POST path=/note\nfunc SaveNote(ctx context.Context, note *Note) (*Note, error) {\n\t// Save the note to the database.\n\t// If the note already exists (i.e. CONFLICT), we update the notes text and the cover URL.\n\t_, err := sqldb.Exec(ctx, `\n\t\tINSERT INTO note (id, text, cover_url) VALUES ($1, $2, $3)\n\t\tON CONFLICT (id) DO UPDATE SET text=$2, cover_url=$3\n\t`, note.ID, note.Text, note.CoverURL)\n\n\t// If there was an error saving to the database, then we return that error.\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Otherwise, we return the note to indicate that the save was successful.\n\treturn note, nil\n}\n```\n\nThe comment above the function tells Encore that this is a public endpoint that should be reachable by POST on `/note`. The second argument to the function (`Note`) is the POST body and the function returns a `Note` and an `error` (a `nil` error means a 200 response).\n\nThe `GetNote` function takes care of fetching a meeting note from our database given an `id`:\n\n```go\n//encore:api public method=GET path=/note/:id\nfunc GetNote(ctx context.Context, id string) (*Note, error) {\n\tnote := &Note{ID: id}\n\n\t// We use the note ID to query the database for the note's text and cover URL.\n\terr := sqldb.QueryRow(ctx, `\n\t\tSELECT text, cover_url FROM note\n\t\tWHERE id = $1\n\t`, id).Scan(&note.Text, &note.CoverURL)\n\n\t// If the note doesn't exist, we return an error.\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Otherwise, we return the note.\n\treturn note, nil\n}\n```\n\nHere we have a public GET endpoint with a dynamic path parameter which is the `id` of the meeting note to fetch. The second argument, in this case, is the dynamic path parameter, a request to this endpoint will look like `/note/123-abc` where `id` will be set to `123-abc`.\n\nBoth `SaveNote` and `GetNote` makes use of a SQL database table named `note`, let's look at how that table is defined.\n\n## Defining a SQL database\n\nTo create a SQL database using Encore we first create a folder named `migrations` and inside that folder a migration file named `1_create_tables.up.sql`. The file name is important (it must look something like `1_name.up.sql`). Our migration file is only five lines long and looks like this:\n\n```sql\nCREATE TABLE note (\n\tid TEXT PRIMARY KEY,\n\ttext TEXT,\n\tcover_url TEXT\n);\n```\n\nWhen recognizing this file, Encore will create a `note` table with three columns `id`, `text` and `cover_url`. The `id` is the primary key, used to identify specific meeting notes.\n\n## Making requests to a third-party API\n\nLet's look at how we can use an Encore endpoint to proxy requests to a third-party service (in this example photo service [pexels.com](http://www.pexels.com/) but the idea would be the same for any other third-party API).\n\nThe file `pexels.go` only has one endpoint, `SearchPhoto`:\n\n```go\n//encore:api public method=GET path=/images/:query\nfunc SearchPhoto(ctx context.Context, query string) (*SearchResponse, error) {\n\t// Create a new http client to proxy the request to the Pexels API.\n\tURL := \"https://api.pexels.com/v1/search?query=\" + query\n\tclient := &http.Client{}\n\treq, _ := http.NewRequest(\"GET\", URL, nil)\n\n\t// Add authorization header to the req with the API key.\n\treq.Header.Set(\"Authorization\", secrets.PexelsApiKey)\n\n\t// Make the request, and close the response body when we're done.\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode >= 400 {\n\t\treturn nil, fmt.Errorf(\"Pexels API error: %s\", res.Status)\n\t}\n\n\t// Decode the data into the searchResponse struct.\n\tvar searchResponse *SearchResponse\n\terr = json.NewDecoder(res.Body).Decode(&searchResponse)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn searchResponse, nil\n}\n```\n\nAgain a GET endpoint with a dynamic path parameter which this time represents the query text we want to send to the Pexels API.\n\nThe type we use to decode the response from the Pexels API looks like this:\n\n```go\ntype SearchResponse struct {\n\tPhotos []struct {\n\t\tId  int `json:\"id\"`\n\t\tSrc struct {\n\t\t\tMedium    string `json:\"medium\"`\n\t\t\tLandscape string `json:\"landscape\"`\n\t\t} `json:\"src\"`\n\t\tAlt string `json:\"alt\"`\n\t} `json:\"photos\"`\n}\n```\n\nWe get a lot more data from Pexels but here we only pick the fields that we want to propagate to our frontend.\n\n[Pexels API](https://www.pexels.com/api/) requires an API key, as most open APIs do. The API key is added as a header to the requests (from the `SearchPhoto` function above):\n\n```go\nreq.Header.Set(\"Authorization\", secrets.PexelsApiKey)\n```\n\nHere we could have hardcoded the API key but that would have made it readable for everyone with access to our repo. Instead, we made use of Encore's built-in [secrets management](https://encore.dev/docs/go/primitives/secrets). To set this secret, run the following command in your project folder and follow the prompt:\n\n```shell\nencore secret set --type dev,prod,local,pr PexelsApiKey\n```\n\n## Creating a request client\n\nEncore is able to generate frontend [request clients](https://encore.dev/docs/go/cli/client-generation) (TypeScript or JavaScript). This means that you do not need to manually keep the request/response objects in sync on the frontend, huge time saver. To generate a client run:\n\n```shell\n$ encore gen client <APP_NAME> --output=./src/client.ts --env=<ENV_NAME>\n```\n\nYou are going to want to run this command quite often (whenever you make a change to your endpoints) so having it as an `npm` script is a good idea:\n\n```json\n{\n...\n\"scripts\": {\n    ...\n    \"generate-client:staging\": \"encore gen client <Encore app id here> --output=./src/client.ts --env=staging\",\n    \"generate-client:local\": \"encore gen client <Encore app id here> --output=./src/client.ts --env=local\"\n  },\n}\n```\n\nAfter that you are ready to use the request client in your code. Here is an example of calling the `GetNote` endpoint:\n\n```tsx\nimport Client, { Environment, Local } from \"src/client.ts\";\n\n// Making request to locally running backend...\nconst client = new Client(Local);\n// or to a specific deployed environment\nconst client = new Client(Environment(\"staging\"));\n\n// Calling APIs as typesafe functions 🌟\nconst response = await client.note.GetNote(\"note-uuid\");\nconsole.log(response.id);\nconsole.log(response.cover_url);\nconsole.log(response.text);\n```\n\n## Deploying the backend to the cloud\n\nIt’s deploy time! To get your backend deployed in the cloud all you need to do is to commit your code and push it to the `encore` remote:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nWhen running `git push encore` you will get a link to the Encore Cloud dashboard where you can view the deploy for your app and after about a minute you have a backend running in the cloud ☁️\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/meeting-notes-git-push.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\n## Hosting the frontend\n\nThe frontend can be deployed to any static site hosting platform. The example project is pre-configured to deploy the frontend to [GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site). Take a look at `.github/workflows/node.yml` to see the GitHub actions workflow being triggered on new commits to the repo:\n\n```yaml\nname: Build and Deploy\n\non: [push]\n\npermissions:\n  contents: write\n\njobs:\n  build-and-deploy:\n    concurrency: ci-${{ github.ref }}\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: frontend\n\n    steps:\n      - name: Checkout 🛎️\n        uses: actions/checkout@v3\n\n      - name: Use Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: \"16.15.1\"\n\n      - name: Install and Build 🔧\n        run: |\n          npm install\n          npm run build\n\n      - name: Deploy 🚀\n        uses: JamesIves/github-pages-deploy-action@v4.3.3\n        with:\n          branch: gh-pages\n          folder: frontend/dist\n```\n\nThe interesting part is towards the bottom where we build the frontend code and make use of the [github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) step to automatically make a new commit with the compiled frontend code to a `gh-pages` branch.\n\n**Steps to deploy to GitHub pages:**\n\n1. Create a repo on GitHub\n2. In the `vite.config.js` file, set the `base` property to the name of your repo:\n\n```yaml\nbase: \"/my-repo-name/\",\n```\n\n1. Push your code to GitHub and wait for the GitHub actions workflow to finish.\n2. Go to _Settings_ → _Pages_ for your repo on GitHub and set _Branch_ to `gh-pages`.\n\n## Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n## Wrapping up\n\nYou’ve learned how to build and deploy a Go backend using Encore, store data in an SQL database, and make API calls to an external service. All of this in under 100 lines of code.\n"
  },
  {
    "path": "docs/go/tutorials/rest-api.mdx",
    "content": "---\nseotitle: How to build a REST API\nseodesc: Learn how to build and ship a REST API in just a few minutes, Encore.go. Go from zero to running API with this tutorial.\ntitle: Building a REST API\nsubtitle: Learn how to build a URL shortener with a REST API and PostgreSQL database\nlang: go\n---\n\nIn this tutorial you will create a REST API for a URL Shortener service. In a few short minutes, you'll learn how to:\n\n* Create REST APIs with Encore\n* Use PostgreSQL databases\n* Use the local development dashboard to test your app\n* Create and run tests\n\nThis is the end result:\n<div className=\"not-prose mb-10\">\n   <Editor projectName=\"urlShortener\" />\n</div>\n\n<div className=\"not-prose flex items-center gap-3 mb-6\">\n   <a href=\"https://app.encore.cloud/create-app/clone/url-shortener\"><img className=\"noshadow m-0\" src=\"https://encore.cloud/assets/img/deploy.svg\" alt=\"Deploy to Encore\" height=\"30\" /></a>\n   <span>Deploy this app to a free dev environment</span>\n</div>\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n## 1. Create a service and endpoint\n\nIf you haven't already, create a new application by running `encore app create` and select `Empty app` as the template.\n\nIf this is the first time you're using Encore, you'll be asked if you wish to create a free account.\nThis is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial).\n\nNow let's create a new `url` service.\n\n🥐 In your application's root folder, create a new folder `url` and create a new file `url.go` that looks like this:\n\n```go\n-- url/url.go --\n// Service url takes URLs, generates random short IDs, and stores the URLs in a database.\npackage url\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n)\n\ntype URL struct {\n\tID  string // short-form URL id\n\tURL string // complete URL, in long form\n}\n\ntype ShortenParams struct {\n\tURL string // the URL to shorten\n}\n\n// Shorten shortens a URL.\n//encore:api public method=POST path=/url\nfunc Shorten(ctx context.Context, p *ShortenParams) (*URL, error) {\n\tid, err := generateID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &URL{ID: id, URL: p.URL}, nil\n}\n\n// generateID generates a random short ID.\nfunc generateID() (string, error) {\n\tvar data [6]byte // 6 bytes of entropy\n\tif _, err := rand.Read(data[:]); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(data[:]), nil\n}\n```\n\nThis sets up the `POST /url` endpoint (see the `//encore:api` annotation on the `Shorten` function).\n\n🥐 Let’s see if it works! Start your app by running `encore run`.\n\nYou should see this:\n\n```output\nYour API is running at:     http://localhost:4000\nDevelopment Dashboard URL:  http://localhost:9400\n4:19PM TRC registered endpoint path=/url service=url endpoint=Shorten\n```\n\n🥐 Next, call your endpoint from the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and view a trace of the response.\nIt should look like this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/rest_tut.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nYou can also call it from the terminal:\n\n```shell\n$ curl http://localhost:4000/url -d '{\"URL\": \"https://encore.dev\"}'\n```\n\nAnd you should see this:\n\n```json\n{\n  \"ID\": \"5cJpBVRp\",\n  \"URL\": \"https://encore.dev\"\n}\n```\n\nIt works! There’s just one problem...\n\nRight now, we’re not actually storing the URL anywhere. That means we can generate shortened IDs but there’s no way to get back to the original URL! We need to store a mapping from the short ID to the complete URL.\n\n## 2. Save URLs in a database\nFortunately, Encore makes it really easy to set up a PostgreSQL database to store our data. To do so, we first define a **database schema**, in the form of a migration file.\n\n🥐 Create a new folder named `migrations` inside the `url` folder. Then, inside the `migrations` folder, create an initial database migration file named `1_create_tables.up.sql`. The file name format is important (it must start with `1_` and end in `.up.sql`).\n\n🥐 Add the following contents to the file:\n\n```sql\n-- url/migrations/1_create_tables.up.sql --\nCREATE TABLE url (\n\tid TEXT PRIMARY KEY,\n\toriginal_url TEXT NOT NULL\n);\n```\n\n🥐 Next, go back to the `url/url.go` file and import the `encore.dev/storage/sqldb` package by modifying the import statement to become:\n\n```go\nHL url/url.go 5:5\n-- url/url.go --\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\n\t\"encore.dev/storage/sqldb\"\n)\n```\n\n🥐 Then let's define our database object by adding the following to `url/url.go`:\n\n```go\n-- url/url.go --\n// Define a database named 'url', using the database\n// migrations in the \"./migrations\" folder.\n\nvar db = sqldb.NewDatabase(\"url\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n```\n\n🥐 Now, to insert data into our database, let’s create a helper function `insert`:\n\n```go\n-- url/url.go --\n// insert inserts a URL into the database.\nfunc insert(ctx context.Context, id, url string) error {\n\t_, err := db.Exec(ctx, `\n        INSERT INTO url (id, original_url)\n        VALUES ($1, $2)\n    `, id, url)\n\treturn err\n}\n```\n\n🥐 Lastly, we can update our `Shorten` function to insert into the database:\n\n```go\n-- url/url.go --\nfunc Shorten(ctx context.Context, p *ShortenParams) (*URL, error) {\n\tid, err := generateID()\n\tif err != nil {\n\t\treturn nil, err\n\t} else if err := insert(ctx, id, p.URL); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &URL{ID: id, URL: p.URL}, nil\n}\n```\n\n<Callout type=\"info\">\n\nBefore running your application, make sure you have [Docker](https://www.docker.com) installed and running. It's required to locally run Encore applications with databases.\n\n</Callout>\n\n🥐 Next, start the application again with `encore run` and Encore automatically sets up your database.\n\n(In case your application won't run, check the [databases troubleshooting guide](/docs/develop/databases#troubleshooting).)\n\nYou can verify that the database was created by opening the **Infra** tab in the local development dashboard at [localhost:9400](http://localhost:9400), which should something like this:\n\n<img src=\"/assets/docs/infra_tab.png\" alt=\"Infra tab in local development dashboard\" className=\"w-full h-full\" />\n\n🥐 Now let's call the API again from the local development dashboard, or from the terminal:\n\n```shell\n$ curl http://localhost:4000/url -d '{\"URL\": \"https://encore.dev\"}'\n```\n\n🥐 Finally, let's verify that it was saved in the database. You can do this by checking the trace in the local development dashboard, or you can run  `encore db shell url` from the app root directory and inputting `select * from url;`:\n\n```shell\n$ encore db shell url\npsql (13.1, server 11.12)\nType \"help\" for help.\n\nurl=# select * from url;\n    id    |    original_url\n----------+--------------------\n zr6RmZc4 | https://encore.dev\n(1 row)\n```\n\nThat was easy!\n\n## 3. Add endpoint to retrieve URLs\nTo complete our URL shortener API, let’s add the endpoint to retrieve a URL given its short id.\n\n🥐 Add this endpoint to `url/url.go`:\n\n```go\n-- url/url.go --\n// Get retrieves the original URL for the id.\n//encore:api public method=GET path=/url/:id\nfunc Get(ctx context.Context, id string) (*URL, error) {\n\tu := &URL{ID: id}\n\terr := db.QueryRow(ctx, `\n        SELECT original_url FROM url\n        WHERE id = $1\n    `, id).Scan(&u.URL)\n\treturn u, err\n}\n```\n\nEncore uses the `path=/url/:id` syntax to represent a path with a parameter. The `id` name corresponds to the parameter name in the function signature. In this case it is of type `string`, but you can also use other built-in types like `int` or `bool` if you want to restrict the values.\n\n🥐 We can make sure it works by reviewing the endpoint in the Service Catalog in the local development dashboard, where we can call it using the `id` you got in the previous step:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/rest_tut_3.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nYou can also call it directly from the terminal:\n\n```shell\n$ curl http://localhost:4000/url/zr6RmZc4\n```\n\nYou should now see this:\n\n```json\n{\n  \"ID\": \"zr6RmZc4\",\n  \"URL\": \"https://encore.dev\"\n}\n```\n\nIt works! That's how you build REST APIs and use PostgreSQL databases in Encore.\n\n## 4. Add a test\n\nBefore deployment, it is good practice to have tests to assure that\nthe service works properly. Such tests including database access\nare easy to write.\n\nWe've prepared a test to check that the whole cycle of shortening\nthe URL, storing and then retrieving the original URL works.\n\n🥐 Save this in a separate file `url/url_test.go`.\n\n```go\n-- url/url_test.go --\npackage url\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\n// TestShortenAndRetrieve - test that the shortened URL is stored and retrieved from database.\nfunc TestShortenAndRetrieve(t *testing.T) {\n\ttestURL := \"https://github.com/encoredev/encore\"\n\tsp := ShortenParams{URL: testURL}\n\tresp, err := Shorten(context.Background(), &sp)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twantURL := testURL\n\tif resp.URL != wantURL {\n\t\tt.Errorf(\"got %q, want %q\", resp.URL, wantURL)\n\t}\n\n\tfirstURL := resp\n\tgotURL, err := Get(context.Background(), firstURL.ID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif *gotURL != *firstURL {\n\t\tt.Errorf(\"got %v, want %v\", *gotURL, *firstURL)\n\t}\n}\n```\n\n🥐 Now run `encore test ./...` to verify that it's working.\n\nIf you use the local development dashboard ([localhost:9400](http://localhost:9400)), you can even see traces for tests.\n\n## 5. Deploy\n\n<Accordion>\n\n### Self-hosting\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nIf your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration.\nOur URL shortener makes use of a PostgreSQL database, so we'll need to supply a [runtime configuration](/docs/go/self-host/configure-infra) so that our app knows how to connect to the database in the cloud.\n\n🥐 Create a new file `infra-config.json` in the root of your project with the following contents:\n\n```json\n{\n   \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n   \"sql_servers\": [\n      {\n         \"host\": \"my-db-host:5432\",\n         \"databases\": {\n            \"url\": {\n               \"username\": \"my-db-owner\",\n                \"password\": {\"$env\": \"DB_PASSWORD\"}\n            }\n         }\n      }\n   ]\n}\n```\n\nThe values in this configuration are just examples, you will need to replace them with the correct values for your database.\n\n🥐 Build a Docker image by running `encore build docker url-shortener:v1.0`.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\n\n🥐 Upload the Docker image to the cloud provider of your choice and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\nEncore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP.\n\n### Create account\n\nBefore deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step.\n\nIf you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template.\n\nAfter creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform).\n\n### Commit changes\n\nThe final step before you deploy is to commit all changes to the project repo.\n\n🥐 Commit the new files to the project's git repo and trigger a deploy to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/rest_tut_4.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nFrom there you can also see metrics, traces, and connect your own AWS or GCP account to use for production deployment.\n\n*Now you have a fully fledged backend running in the cloud, well done!*\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n🥐 A great next step is to [integrate with GitHub](/docs/platform/integrations/github). Once you've linked with GitHub, Encore will automatically start building and running tests against your Pull Requests.\n\n</Accordion>\n\n## What's next\n\nNow that you know how to build a backend with a database, you're ready to let your creativity flow and begin building your next great idea!\n\nWe're excited to hear what you're going to build with Encore, join the pioneering developer community on [Discord](/discord) and share your story.\n"
  },
  {
    "path": "docs/go/tutorials/slack-bot.md",
    "content": "---\nseotitle: Tutorial – How to build a Slack bot\nseodesc: Learn how to build a Slack bot with Enore.go, and get it running in the cloud in just a few minutes.\ntitle: Building a Slack bot\nsubtitle: Learn how to build a Slack bot with an Encore backend\nlang: go\n---\n\nIn this tutorial you will create a Slack bot that brings the greatness of the `cowsay` utility to Slack!\n\n![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png \"Slack bot\")\n\nThis is the end result:\n<div className=\"not-prose mb-10\">\n   <Editor projectName=\"slackBot\" />\n</div>\n\n<div className=\"not-prose flex items-center gap-3 mb-6\">\n   <a href=\"https://app.encore.cloud/create-app/clone/slack-bot\"><img className=\"noshadow m-0\" src=\"https://encore.cloud/assets/img/deploy.svg\" alt=\"Deploy to Encore\" height=\"30\" /></a>\n   <span>Deploy this app to a free dev environment</span>\n</div>\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n## 1. Create your Encore application\n\n🥐 Create a new Encore application by running `encore app create` and select `Empty app` as the template.\n**Take a note of your app id, we'll need it in the next step.**\n\n## 2. Create a Slack app\n\n🥐 The first step is to create a new Slack app:\n\n1. Head over to [Slack's API site](https://api.slack.com/apps) and create a new app.\n2. When prompted, choose to create the app **from an app manifest**.\n3. Choose a workspace to install the app in.\n\n🥐 Enter the following manifest (replace `$APP_ID` in the URL below with your app id from above):\n\n```yaml\n_metadata:\n  major_version: 1\ndisplay_information:\n  name: Encore Bot\n  description: Cowsay for the cloud age.\nfeatures:\n  slash_commands:\n    - command: /cowsay\n      # Replace $APP_ID below\n      url: https://staging-$APP_ID.encr.app/cowsay\n      description: Say things with a flair!\n      usage_hint: your message here\n      should_escape: false\n  bot_user:\n    display_name: encore-bot\n    always_online: true\noauth_config:\n  scopes:\n    bot:\n      - commands\n      - chat:write\n      - chat:write.public\nsettings:\n  org_deploy_enabled: false\n  socket_mode_enabled: false\n  token_rotation_enabled: false\n```\n\nOnce created, we're ready to move on with implementing our Encore endpoint!\n\n## 3. Implement the Slack endpoint\n\nSince Slack sends custom HTTP headers that we need to pay attention to, we're going to\nuse a raw endpoint in Encore. For more information on this check out Slack's documentation\non [Enabling interactivity with Slash Commands](https://api.slack.com/interactivity/slash-commands).\n\n🥐 In your Encore app, create a new directory named `slack` and create a file `slack/slack.go` with the following contents:\n\n```go\n-- slack/slack.go --\n// Service slack implements a cowsaw Slack bot.\npackage slack\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// cowart is the formatting string for printing the cow art.\nconst cowart = \"Moo! %s\"\n\n//encore:api public raw path=/cowsay\nfunc Cowsay(w http.ResponseWriter, req *http.Request) {\n\ttext := req.FormValue(\"text\")\n\tdata, _ := json.Marshal(map[string]string{\n\t\t\"response_type\": \"in_channel\",\n\t\t\"text\":          fmt.Sprintf(cowart, text),\n\t})\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(200)\n\tw.Write(data)\n}\n```\n\nLet's try it out locally.\n\n🥐 Start your app with `encore run` and then call it in another terminal:\n\n```shell\n$ curl http://localhost:4000/cowsay -d 'text=Eat your greens!'\n{\"response_type\":\"in_channel\",\"text\":\"Moo! Eat your greens!\"}\n```\n\nLooks great!\n\n🥐 Next, let's deploy it to the cloud:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nOnce deployed, we're ready to try our Slack command!\n\n🥐 Head over to the workspace you installed the app in and run `/cowsay Hello there`.\nYou should see something like this:\n\n![Cowsay](https://encore.dev/assets/docs/cowsay-wip.png \"Cowsay (Work in Progress)\")\n\nAnd just like that we have a fully working Slack integration.\n\n## 4. Secure the webhook endpoint\n\nIn order to get up and running quickly we ignored one important aspect for a production-ready Slack app:\nverifying that the webhook requests are actually coming from Slack. Let's do that now!\n\nThe Slack documentation covers this really well on the [Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack) page.\n\nIn short, what we need to do is:\n\n1. Save a shared secret that Slack provides us\n2. Use the secret to verify that the request comes from Slack, using HMAC (Hash-based Message Authentication Code).\n\n### Save the shared secret\n\nLet's define a secret using Encore's secrets management functionality.\n\n🥐 Add this to your `slack.go` file:\n\n```go\n-- slack/slack.go --\nvar secrets struct {\n\tSlackSigningSecret string\n}\n```\n\n🥐 Head over to the configuration section for your Slack app (go to [Your Apps](https://api.slack.com/apps) &rarr; select your app &rarr; Basic Information).\n\n🥐 Copy the **Signing Secret** and then run `encore secret set --type prod SlackSigningSecret` and paste the secret.\n\n🥐 For development you will also want to set `encore secret set --type dev,local,pr SlackSigningSecret`.\nYou can use the same secret value or a placeholder value.\n\n### Compute the HMAC\n\nGo makes computing HMAC very straightforward, but it's still a fair amount of code.\n\n🥐 Add a few more imports to your file, so that it reads:\n\n```go\n-- slack/slack.go --\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/rlog\"\n)\n```\n\n🥐 Next, we'll add the `verifyRequest` function:\n\n```go\n-- slack/slack.go --\n// verifyRequest verifies that a request is coming from Slack.\nfunc verifyRequest(req *http.Request) (body []byte, err error) {\n\teb := errs.B().Code(errs.InvalidArgument)\n\tbody, err = ioutil.ReadAll(req.Body)\n\tif err != nil {\n\t\treturn nil, eb.Cause(err).Err()\n\t}\n\n\t// Compare timestamps to prevent replay attack\n\tts := req.Header.Get(\"X-Slack-Request-Timestamp\")\n\tthreshold := int64(5 * 60)\n\tn, _ := strconv.ParseInt(ts, 10, 64)\n\tif diff := time.Now().Unix() - n; diff > threshold || diff < -threshold {\n\t\treturn body, eb.Msg(\"message not recent\").Err()\n\t}\n\n\t// Compare HMAC signature\n\tsig := req.Header.Get(\"X-Slack-Signature\")\n\tprefix := \"v0=\"\n\tif !strings.HasPrefix(sig, prefix) {\n\t\treturn body, eb.Msg(\"invalid signature\").Err()\n\t}\n\tgotMac, _ := hex.DecodeString(sig[len(prefix):])\n\n\tmac := hmac.New(sha256.New, []byte(secrets.SlackSigningSecret))\n\tfmt.Fprintf(mac, \"v0:%s:\", ts)\n\tmac.Write(body)\n\texpectedMac := mac.Sum(nil)\n\tif !hmac.Equal(gotMac, expectedMac) {\n\t\treturn body, eb.Msg(\"bad mac\").Err()\n\t}\n\treturn body, nil\n}\n```\n\n<Callout type=\"info\">\n\nAs you can see, this function needs to consume the whole HTTP body in order to compute the HMAC.\n\nThis breaks the use of `req.FormValue(\"text\")` that we used earlier, since it relies on reading the HTTP body. That's the reason we're returning the body from `verifyRequest`, so that we can parse the form values from that directly instead.\n\n</Callout>\n\nWe're now ready to verify the signature.\n\n🥐 Update the `Cowsay` function to look like this:\n\n```go\n-- slack/slack.go --\n//encore:api public raw path=/cowsay\nfunc Cowsay(w http.ResponseWriter, req *http.Request) {\n\tbody, err := verifyRequest(req)\n\tif err != nil {\n\t\terrs.HTTPError(w, err)\n\t\treturn\n\t}\n\tq, _ := url.ParseQuery(string(body))\n\ttext := q.Get(\"text\")\n\tdata, _ := json.Marshal(map[string]string{\n\t\t\"response_type\": \"in_channel\",\n\t\t\"text\":          fmt.Sprintf(cowart, text),\n\t})\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(200)\n\tw.Write(data)\n}\n```\n\n## 5. Put it all together and deploy\n\nFinally we're ready to put it all together.\n\n🥐 Add the `cowart` like so:\n\n```go\n-- slack/slack.go --\nconst cowart = `\n ________________________________________\n< %- 38s >\n ----------------------------------------\n        \\   ^__^\n         \\  (oo)\\_______\n            (__)\\       )\\/\\\n                ||----w |\n                ||     ||\n`\n```\n\n🥐 Finally, let's commit our changes and deploy it:\n\n```shell\n$ git add -A .\n$ git commit -m 'Verify webhook requests and improve art'\n$ git push encore\n```\n\n🥐 Once deployed, head back to Slack and run `/cowsay Hello there`.\n\nIf everything is set up correctly, you should see:\n\n![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png \"Slack bot\")\n\nAnd there we go, a production-ready Slack bot in less than 100 lines of code.\n\nWell done!\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n"
  },
  {
    "path": "docs/go/tutorials/uptime.md",
    "content": "---\ntitle: Building an Uptime Monitor\nsubtitle: Learn how to build an event-driven uptime monitoring system\nseotitle: How to build an event-driven Uptime Monitoring System using Encore.go\nseodesc: Learn how to build an event-driven uptime monitoring tool using Go and Encore. Get your application running in the cloud in 30 minutes!\nlang: go\n---\n\nWant to be notified when your website goes down so you can fix it before your users notice?\n\nYou need an uptime monitoring system. Sounds daunting? Don't worry,\nwe'll build it with Encore in 30 minutes!\n\nThe app will use an event-driven architecture and the final result will look like this:\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/uptime/frontend.png\" title=\"Frontend\" />\n\n<div className=\"not-prose my-10\">\n   <Editor projectName=\"uptime\" />\n</div>\n\n<div className=\"not-prose flex items-center gap-3 mb-6\">\n   <a href=\"https://app.encore.cloud/create-app/clone/uptime\"><img className=\"noshadow m-0\" src=\"https://encore.cloud/assets/img/deploy.svg\" alt=\"Deploy to Encore\" height=\"30\" /></a>\n   <span>Deploy this app to a free dev environment</span>\n</div>\n\n## 1. Create your Encore application\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n🥐 Create a new Encore application, using this tutorial project's starting-point branch. This gives you a ready-to-go frontend to use.\n\n```shell\n$ encore app create uptime --example=github.com/encoredev/example-app-uptime/tree/starting-point\n```\n\nIf this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial).\n\nWhen we're done we'll have a backend with an event-driven architecture, as seen below in the [automatically generated diagram](/docs/go/observability/encore-flow) where white boxes are services and black boxes are Pub/Sub topics:\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/uptime/encore-flow.png\" title=\"Encore Flow\" />\n\n## 2. Create monitor service\n\nLet's start by creating the functionality to check if a website is currently up or down.\nLater we'll store this result in a database so we can detect when the status changes and\nsend alerts.\n\n🥐 Create an Encore service named `monitor` containing a file named `ping.go`.\n\n```shell\n$ mkdir monitor\n$ touch monitor/ping.go\n```\n\n🥐 Add an Encore API endpoint named `Ping` that takes a URL as input and returns a response\nindicating whether the site is up or down.\n\n```go\n-- monitor/ping.go --\n// Service monitor checks if a website is up or down.\npackage monitor\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// PingResponse is the response from the Ping endpoint.\ntype PingResponse struct {\n\tUp bool `json:\"up\"`\n}\n\n// Ping pings a specific site and determines whether it's up or down right now.\n//\n//encore:api public path=/ping/*url\nfunc Ping(ctx context.Context, url string) (*PingResponse, error) {\n    // If the url does not start with \"http:\" or \"https:\", default to \"https:\".\n\tif !strings.HasPrefix(url, \"http:\") && !strings.HasPrefix(url, \"https:\") {\n\t\turl = \"https://\" + url\n\t}\n\n    // Make an HTTP request to check if it's up.\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn &PingResponse{Up: false}, nil\n\t}\n\tresp.Body.Close()\n\n    // 2xx and 3xx status codes are considered up\n    up := resp.StatusCode < 400\n    return &PingResponse{Up: up}, nil\n}\n```\n\n🥐 Let's try it! Run `encore run` in your terminal and you should see the service start up.\n\nThen open up the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and try calling the `monitor.ping` endpoint from the API Explorer, passing in `google.com` as the URL.\n\nYou can then see the response, logs, and view a trace of the request. It will look something like this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/localdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nIf you prefer to use the terminal instead run `curl http://localhost:4000/ping/google.com` in\na new terminal instead. Either way you should see the response:\n\n```json\n{\"up\": true}\n```\n\nYou can also try with `httpstat.us/400` and `some-non-existing-url.com` and it should respond with `{\"up\": false}`.\n(It's always a good idea to test the negative case as well.)\n\n### Add a test\n\n🥐 Let's write an automated test so we don't break this endpoint over time. Create the file `monitor/ping_test.go`\nwith the content:\n\n```go\n-- monitor/ping_test.go --\npackage monitor\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\nfunc TestPing(t *testing.T) {\n\tctx := context.Background()\n\ttests := []struct {\n\t\tURL string\n\t\tUp  bool\n\t}{\n\t\t{\"encore.dev\", true},\n\t\t{\"google.com\", true},\n        // Test both with and without \"https://\"\n\t\t{\"httpbin.org/status/200\", true},\n\t\t{\"https://httpbin.org/status/200\", true},\n\n        // 4xx and 5xx should considered down.\n\t\t{\"httpbin.org/status/400\", false},\n\t\t{\"https://httpbin.org/status/500\", false},\n        // Invalid URLs should be considered down.\n\t\t{\"invalid://scheme\", false},\n\t}\n\n\tfor _, test := range tests {\n\t\tresp, err := Ping(ctx, test.URL)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"url %s: unexpected error: %v\", test.URL, err)\n\t\t} else if resp.Up != test.Up {\n\t\t\tt.Errorf(\"url %s: got up=%v, want %v\", test.URL, resp.Up, test.Up)\n\t\t}\n\t}\n}\n```\n\n🥐 Run `encore test ./...` to check that it all works as expected. You should see something like:\n\n```shell\n$ encore test ./...\n9:38AM INF starting request endpoint=Ping service=monitor test=TestPing\n9:38AM INF request completed code=ok duration=71.861792 endpoint=Ping http_code=200 service=monitor test=TestPing\n[... lots more lines ...]\nPASS\nok      encore.app/monitor      1.660\n```\n\nAnd if you open the local development dashboard at [localhost:9400](http://localhost:9400), you can also see traces for the tests.\n\n## 3. Create site service\n\nNext, we want to keep track of a list of websites to monitor.\n\nSince most of these APIs will be simple \"CRUD\" (Create/Read/Update/Delete) endpoints, let's build this service using [GORM](https://gorm.io/), an ORM\nlibrary that makes building CRUD endpoints really simple.\n\n🥐 Let's create a new service named `site` with a SQL database. To do so, create a new directory `site` in the application root with `migrations` folder inside that folder:\n\n```shell\n$ mkdir site\n$ mkdir site/migrations\n```\n\n🥐  Add a database migration file inside that folder, named `1_create_tables.up.sql`.\nThe file name is important (it must look something like `1_<name>.up.sql`).\n\nAdd the following contents:\n\n```sql\n-- site/migrations/1_create_tables.up.sql --\nCREATE TABLE sites (\n    id BIGSERIAL PRIMARY KEY,\n    url TEXT NOT NULL\n);\n```\n\n🥐 Next, install the GORM library and PostgreSQL driver:\n\n```shell\n$ go get -u gorm.io/gorm gorm.io/driver/postgres\n```\n\nNow let's create the `site` service itself. To do this we'll use Encore's support for [dependency injection](https://encore.dev/docs/go/how-to/dependency-injection) to inject the GORM database connection.\n\n🥐 Create `site/service.go` with the contents:\n\n```go\n-- site/service.go --\n// Service site keeps track of which sites to monitor.\npackage site\n\nimport (\n\t\"encore.dev/storage/sqldb\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n)\n\n//encore:service\ntype Service struct {\n\tdb *gorm.DB\n}\n\n// Define a database named 'site', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nvar db = sqldb.NewDatabase(\"site\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// initService initializes the site service.\n// It is automatically called by Encore on service startup.\nfunc initService() (*Service, error) {\n\tdb, err := gorm.Open(postgres.New(postgres.Config{\n\t\tConn: db.Stdlib(),\n\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Service{db: db}, nil\n}\n```\n\n🥐 With that, we're now ready to create our CRUD endpoints. Create the following files:\n\n```go\n-- site/get.go --\npackage site\n\nimport \"context\"\n\n// Site describes a monitored site.\ntype Site struct {\n\t// ID is a unique ID for the site.\n\tID int `json:\"id\"`\n\t// URL is the site's URL.\n\tURL string `json:\"url\"`\n}\n\n// Get gets a site by id.\n//\n//encore:api public method=GET path=/site/:siteID\nfunc (s *Service) Get(ctx context.Context, siteID int) (*Site, error) {\n\tvar site Site\n\tif err := s.db.Where(\"id = $1\", siteID).First(&site).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn &site, nil\n}\n-- site/add.go --\npackage site\n\nimport \"context\"\n\n// AddParams are the parameters for adding a site to be monitored.\ntype AddParams struct {\n\t// URL is the URL of the site. If it doesn't contain a scheme\n\t// (like \"http:\" or \"https:\") it defaults to \"https:\".\n\tURL string `json:\"url\"`\n}\n\n// Add adds a new site to the list of monitored websites.\n//\n//encore:api public method=POST path=/site\nfunc (s *Service) Add(ctx context.Context, p *AddParams) (*Site, error) {\n\tsite := &Site{URL: p.URL}\n\tif err := s.db.Create(site).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn site, nil\n}\n-- site/list.go --\npackage site\n\nimport \"context\"\n\ntype ListResponse struct {\n\t// Sites is the list of monitored sites.\n\tSites []*Site `json:\"sites\"`\n}\n\n// List lists the monitored websites.\n//\n//encore:api public method=GET path=/site\nfunc (s *Service) List(ctx context.Context) (*ListResponse, error) {\n\tvar sites []*Site\n\tif err := s.db.Find(&sites).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn &ListResponse{Sites: sites}, nil\n}\n-- site/delete.go --\npackage site\n\nimport \"context\"\n\n// Delete deletes a site by id.\n//\n//encore:api public method=DELETE path=/site/:siteID\nfunc (s *Service) Delete(ctx context.Context, siteID int) error {\n\treturn s.db.Delete(&Site{ID: siteID}).Error\n}\n```\n\n🥐 Now make sure you have [Docker](https://docker.com) installed and running, and then restart `encore run` to cause the `site` database to be created by Encore.\n\nYou can verify that the database was created by looking at your application's Flow architecture diagram in the local development dashboard at [localhost:9400](http://localhost:9400), and then use the Service Catalog to call the `site.Add` endpoint.\n\nOr you can call `site.Add` from the terminal:\n\n```shell\n$ curl -X POST 'http://localhost:4000/site' -d '{\"url\": \"https://encore.dev\"}'\n{\n  \"id\": 1,\n  \"url\": \"https://encore.dev\"\n}\n```\n\n## 4. Record uptime checks\n\nIn order to notify when a website goes down or comes back up, we need to track the previous state it was in.\n\n🥐  To do so, let's add a database to the `monitor` service as well.\nCreate the directory `monitor/migrations` and the file `monitor/migrations/1_create_tables.up.sql`:\n\n```sql\n-- monitor/migrations/1_create_tables.up.sql --\nCREATE TABLE checks (\n    id BIGSERIAL PRIMARY KEY,\n    site_id BIGINT NOT NULL,\n    up BOOLEAN NOT NULL,\n    checked_at TIMESTAMP WITH TIME ZONE NOT NULL\n);\n```\n\nWe'll insert a database row every time we check if a site is up.\n\n🥐 Add a new endpoint `Check` to the `monitor` service, that\ntakes in a Site ID, pings the site, and inserts a database row\nin the `checks` table.\n\nFor this service we'll use Encore's [`sqldb` package](https://encore.dev/docs/go/primitives/databases#querying-databases)\ninstead of GORM (in order to showcase both approaches).\n\n```go\n-- monitor/check.go --\npackage monitor\n\nimport (\n\t\"context\"\n\n\t\"encore.app/site\"\n\t\"encore.dev/storage/sqldb\"\n)\n\n// Check checks a single site.\n//\n//encore:api public method=POST path=/check/:siteID\nfunc Check(ctx context.Context, siteID int) error {\n\tsite, err := site.Get(ctx, siteID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresult, err := Ping(ctx, site.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = db.Exec(ctx, `\n\t\tINSERT INTO checks (site_id, up, checked_at)\n\t\tVALUES ($1, $2, NOW())\n\t`, site.ID, result.Up)\n\treturn err\n}\n\n// Define a database named 'monitor', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nvar db = sqldb.NewDatabase(\"monitor\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n```\n\n\n🥐 Restart `encore run` to cause the `monitor` database to be created.\n\nWe can again verify that the database was created in the Flow diagram, and also see the dependency between the `monitor` service and the `site` service that we just added.\n\nWe can then call the `monitor.Check` endpoint using the id `1` that we got in the last step, and view the trace where we see the database interactions.\n\nIt will look something like this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/flow.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\n🥐 You can also inspect the database using `encore db shell <database-name>` to make sure everything worked:\n\n```shell\n$ encore db shell monitor\npsql (14.4, server 14.2)\nType \"help\" for help.\n\nmonitor=> SELECT * FROM checks;\n id | site_id | up |          checked_at\n----+---------+----+-------------------------------\n  1 |       1 | t  | 2022-10-21 09:58:30.674265+00\n```\n\nIf that's what you see, everything's working great!\n\n### Add a cron job to check all sites\n\nWe now want to regularly check all the tracked sites so we can\nrespond in case any of them go down.\n\nWe'll create a new `CheckAll` API endpoint in the `monitor` service\nthat will list all the tracked sites and check all of them.\n\n🥐 Let's extract some of the functionality we wrote for the\n`Check` endpoint into a separate function, like so:\n\n```go\n-- monitor/check.go --\n// Check checks a single site.\n//\n//encore:api public method=POST path=/check/:siteID\nfunc Check(ctx context.Context, siteID int) error {\n\tsite, err := site.Get(ctx, siteID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn check(ctx, site)\n}\n\nfunc check(ctx context.Context, site *site.Site) error {\n\tresult, err := Ping(ctx, site.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = db.Exec(ctx, `\n\t\tINSERT INTO checks (site_id, up, checked_at)\n\t\tVALUES ($1, $2, NOW())\n\t`, site.ID, result.Up)\n\treturn err\n}\n```\n\nNow we're ready to create our new `CheckAll` endpoint.\n\n🥐 Create the new `CheckAll` endpoint inside `monitor/check.go`:\n\n```go\n-- monitor/check.go --\nimport \"golang.org/x/sync/errgroup\"\n\n// CheckAll checks all sites.\n//\n//encore:api public method=POST path=/checkall\nfunc CheckAll(ctx context.Context) error {\n\t// Get all the tracked sites.\n\tresp, err := site.List(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check up to 8 sites concurrently.\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(8)\n\tfor _, site := range resp.Sites {\n\t\tsite := site // capture for closure\n\t\tg.Go(func() error {\n\t\t\treturn check(ctx, site)\n\t\t})\n\t}\n\treturn g.Wait()\n}\n```\n\nThis uses [an errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) to check up to 8 sites concurrently, aborting early if\nwe encounter any error. (Note that a website being down is\nnot treated as an error.)\n\n🥐 Run `go get golang.org/x/sync/errgroup` to install that dependency.\n\n🥐 Now that we have a `CheckAll` endpoint, define a [cron job](https://encore.dev/docs/go/primitives/cron-jobs) to automatically call it every 1 hour (since this is an example, we don't need to go too crazy and check every minute):\n\n```go\n-- monitor/check.go --\nimport \"encore.dev/cron\"\n\n// Check all tracked sites every 1 hour.\nvar _ = cron.NewJob(\"check-all\", cron.JobConfig{\n\tTitle:    \"Check all sites\",\n\tEndpoint: CheckAll,\n\tEvery:    1 * cron.Hour,\n})\n```\n\n<Callout type=\"info\">\n\nCron jobs are not triggered when running the application locally but work when deploying the application to a cloud environment.\n\n</Callout>\n\nThe frontend needs a way to list all sites and display if they are up or down.\n\n🥐 Add a file in the `monitor` service and name it `status.go`. Add the following code:\n\n```go\n-- monitor/status.go --\npackage monitor\n\nimport (\n\t\"context\"\n\t\"time\"\n)\n\n// SiteStatus describes the current status of a site\n// and when it was last checked.\ntype SiteStatus struct {\n\tUp        bool      `json:\"up\"`\n\tCheckedAt time.Time `json:\"checked_at\"`\n}\n\n// StatusResponse is the response type from the Status endpoint.\ntype StatusResponse struct {\n\t// Sites contains the current status of all sites,\n\t// keyed by the site ID.\n\tSites map[int]SiteStatus `json:\"sites\"`\n}\n\n// Status checks the current up/down status of all monitored sites.\n//\n//encore:api public method=GET path=/status\nfunc Status(ctx context.Context) (*StatusResponse, error) {\n\trows, err := db.Query(ctx, `\n\t\tSELECT DISTINCT ON (site_id) site_id, up, checked_at\n\t\tFROM checks\n\t\tORDER BY site_id, checked_at DESC\n\t`)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tresult := make(map[int]SiteStatus)\n\tfor rows.Next() {\n\t\tvar siteID int\n\t\tvar status SiteStatus\n\t\tif err := rows.Scan(&siteID, &status.Up, &status.CheckedAt); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresult[siteID] = status\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &StatusResponse{Sites: result}, nil\n}\n```\n\nNow try visiting http://localhost:4000/frontend in your browser again. This time you should see a working frontend that lists all sites and their current status.\n\n## 5. Deploy\n\nTo try out your uptime monitor for real, let's deploy it to the cloud.\n\n<Accordion>\n\n### Self-hosting\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nIf your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/go/self-host/configure-infra) your Docker image.\n\n🥐 Create a new file `infra-config.json` in the root of your project with the following contents:\n\n```json\n{\n   \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n   \"sql_servers\": [\n      {\n         \"host\": \"my-db-host:5432\",\n         \"databases\": {\n            \"monitor\": {\n               \"username\": \"my-db-owner\",\n                \"password\": {\"$env\": \"DB_PASSWORD\"}\n            },\n            \"site\": {\n               \"username\": \"my-db-owner\",\n                \"password\": {\"$env\": \"DB_PASSWORD\"}\n            }\n         }\n      }\n   ]\n}\n```\n\nThe values in this configuration are just examples, you will need to replace them with the correct values for your database.\n\n🥐 Build a Docker image by running `encore build docker uptime:v1.0`.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\n\n🥐 Upload the Docker image to the cloud provider of your choice and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\nEncore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP.\n\n### Create account\n\nBefore deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step.\n\nIf you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template.\n\nAfter creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform).\n\n### Commit changes\n\nEncore comes with built-in CI/CD, and the deployment process is as simple as a `git push`.\n(You can also integrate with GitHub to activate per Pull Request Preview Environments, learn more in the [CI/CD docs](/docs/platform/deploy/deploying).)\n\n🥐 Now, let's deploy your app to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\nFrom the Cloud Dashboard you can also see metrics, trigger Cron Jobs, see traces, and later connect your own AWS or GCP account to use for deployment.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/webdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\n🥐 When the deploy has finished, you can try out your uptime monitor by going to `https://staging-$APP_ID.encr.app/frontend`.\n\n*You now have an Uptime Monitor running in the cloud, well done!*\n\n</Accordion>\n\n## 6. Publish Pub/Sub events when a site goes down\n\nHold on, an uptime monitoring system isn't very useful if it doesn't\nactually notify you when a site goes down.\n\nTo do so let's add a [Pub/Sub topic](https://encore.dev/docs/go/primitives/pubsub) on which we'll publish a message every time a site transitions from being up to being down, or vice versa.\n\n🥐 Define the topic using Encore's Pub/Sub package in a new file, `monitor/alerts.go`:\n\n```go\n-- monitor/alerts.go --\npackage monitor\n\nimport \"encore.dev/pubsub\"\n\n// TransitionEvent describes a transition of a monitored site\n// from up->down or from down->up.\ntype TransitionEvent struct {\n\t// Site is the monitored site in question.\n\tSite *site.Site `json:\"site\"`\n\t// Up specifies whether the site is now up or down (the new value).\n\tUp bool `json:\"up\"`\n}\n\n// TransitionTopic is a pubsub topic with transition events for when a monitored site\n// transitions from up->down or from down->up.\nvar TransitionTopic = pubsub.NewTopic[*TransitionEvent](\"uptime-transition\", pubsub.TopicConfig{\n\tDeliveryGuarantee: pubsub.AtLeastOnce,\n})\n```\n\nNow let's publish a message on the `TransitionTopic` if a site's up/down\nstate differs from the previous measurement.\n\n🥐 Create a `getPreviousMeasurement` function to report the last up/down state:\n\n```go\n-- monitor/alerts.go --\nimport (\n\t\"encore.dev/storage/sqldb\"\n\t\"errors\"\n\t\"context\"\n)\n// getPreviousMeasurement reports whether the given site was\n// up or down in the previous measurement.\nfunc getPreviousMeasurement(ctx context.Context, siteID int) (up bool, err error) {\n\terr = db.QueryRow(ctx, `\n\t\tSELECT up FROM checks\n\t\tWHERE site_id = $1\n\t\tORDER BY checked_at DESC\n\t\tLIMIT 1\n\t`, siteID).Scan(&up)\n\n\tif errors.Is(err, sqldb.ErrNoRows) {\n\t\t// There was no previous ping; treat this as if the site was up before\n\t\treturn true, nil\n\t} else if err != nil {\n\t\treturn false, err\n\t}\n\treturn up, nil\n}\n```\n\n🥐 Now add a function to conditionally publish a message if the up/down state differs:\n\n```go\n-- monitor/alerts.go --\nimport \"encore.app/site\"\n\nfunc publishOnTransition(ctx context.Context, site *site.Site, isUp bool) error {\n\twasUp, err := getPreviousMeasurement(ctx, site.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif isUp == wasUp {\n\t\t// Nothing to do\n\t\treturn nil\n\t}\n\t_, err = TransitionTopic.Publish(ctx, &TransitionEvent{\n\t\tSite: site,\n\t\tUp:   isUp,\n\t})\n\treturn err\n}\n```\n\n🥐 Finally modify the `check` function to call this function:\n\n```go\n-- monitor/check.go --\nfunc check(ctx context.Context, site *site.Site) error {\n\tresult, err := Ping(ctx, site.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Publish a Pub/Sub message if the site transitions\n\t// from up->down or from down->up.\n\tif err := publishOnTransition(ctx, site, result.Up); err != nil {\n\t\treturn err\n\t}\n\n\t_, err = db.Exec(ctx, `\n\t\tINSERT INTO checks (site_id, up, checked_at)\n\t\tVALUES ($1, $2, NOW())\n\t`, site.ID, result.Up)\n\treturn err\n}\n```\n\nNow the monitoring system will publish messages on the `TransitionTopic`\nwhenever a monitored site transitions from up->down or from down->up.\nIt doesn't know or care who actually listens to these messages.\n\nThe truth is right now nobody does. So let's fix that by adding\na Pub/Sub subscriber that posts these events to Slack.\n\n## 7. Send Slack notifications when a site goes down\n\n🥐 Start by creating a Slack service containing the following:\n\n```go\n-- slack/slack.go --\npackage slack\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype NotifyParams struct {\n\t// Text is the Slack message text to send.\n\tText string `json:\"text\"`\n}\n\n// Notify sends a Slack message to a pre-configured channel using a\n// Slack Incoming Webhook (see https://api.slack.com/messaging/webhooks).\n//\n//encore:api private\nfunc Notify(ctx context.Context, p *NotifyParams) error {\n\treqBody, err := json.Marshal(p)\n\tif err != nil {\n\t\treturn err\n\t}\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", secrets.SlackWebhookURL, bytes.NewReader(reqBody))\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"notify slack: %s: %s\", resp.Status, body)\n\t}\n\treturn nil\n}\n\nvar secrets struct {\n\t// SlackWebhookURL defines the Slack webhook URL to send\n\t// uptime notifications to.\n\tSlackWebhookURL string\n}\n```\n\n🥐 Now go to a Slack community of your choice where you have the permission\nto create a new Incoming Webhook.\n\n🥐 Once you have the Webhook URL, set it as an Encore secret:\n\n```shell\n$ encore secret set --type dev,local,pr SlackWebhookURL\nEnter secret value: *****\nSuccessfully updated development secret SlackWebhookURL.\n```\n\n🥐 Test the `slack.Notify` endpoint by calling it via cURL:\n\n```shell\n$ curl 'http://localhost:4000/slack.Notify' -d '{\"Text\": \"Testing Slack webhook\"}'\n```\nYou should see the *Testing Slack webhook* message appear in the Slack channel you designated for the webhook.\n\n🥐 When it works it's time to add a Pub/Sub subscriber to automatically notify Slack when a monitored site goes up or down. Add the following:\n\n```go\n-- slack/slack.go --\nimport (\n\t\"encore.dev/pubsub\"\n\t\"encore.app/monitor\"\n)\n\nvar _ = pubsub.NewSubscription(monitor.TransitionTopic, \"slack-notification\", pubsub.SubscriptionConfig[*monitor.TransitionEvent]{\n\tHandler: func(ctx context.Context, event *monitor.TransitionEvent) error {\n\t\t// Compose our message.\n\t\tmsg := fmt.Sprintf(\"*%s is down!*\", event.Site.URL)\n\t\tif event.Up {\n\t\t\tmsg = fmt.Sprintf(\"*%s is back up.*\", event.Site.URL)\n\t\t}\n\n\t\t// Send the Slack notification.\n\t\treturn Notify(ctx, &NotifyParams{Text: msg})\n\t},\n})\n```\n\n## 8. Deploy your finished Uptime Monitor\n\nNow you're ready to deploy your finished Uptime Monitor, complete with a Slack integration.\n\n<Accordion>\n\n### Self-hosting\n\nBecause we have added more infrastructure to our app, we need to [update the configuration](/docs/go/self-host/configure-infra) in our `infra-config.json` to include the new Pub/Sub topic and subscription as well as how we should set the  `SlackWebhookURL` secret. \n\n🥐 Update your `ìnfra-config.json` to reflect the new infrastructure.\n\n🥐 Build a Docker image by running `encore build docker uptime:v2.0`.\n\n🥐 Upload the Docker image to the cloud provider and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\n🥐 As before, deploying your app to the cloud is as simple as running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Add slack integration'\n$ git push encore\n```\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n</Accordion>\n\n## Conclusion\n\nWe've now built a fully functioning uptime monitoring system.\n\nIf we may say so ourselves (and we may; it's our documentation after all)\nit's pretty remarkable how much we've accomplished in such little code:\n\n* We've built three different services (`site`, `monitor`, and `slack`)\n* We've added two databases (to the `site` and `monitor` services) for tracking monitored sites and the monitoring results\n* We've added a cron job for automatically checking the sites every hour\n* We've set up a Pub/Sub topic to decouple the monitoring system from the Slack notifications\n* We've added a Slack integration, using secrets to securely store the webhook URL, listening to a Pub/Sub subscription for up/down transition events\n\nAll of this in just a bit over 300 lines of code. It's time to lean back\nand take a sip of your favorite beverage, safe in the knowledge you'll\nnever be caught unaware of a website going down suddenly.\n"
  },
  {
    "path": "docs/menu.cue",
    "content": "#Menu: #RootMenu | #SubMenu\n\n#RootMenu: {\n\tkind: \"rootmenu\"\n\titems: [...#MenuItem]\n}\n\n#SubMenu: {\n\tkind: \"submenu\"\n\t// Menu title to display when this submenu is active.\n\ttitle: string\n\n\t// ID for the submenu, used for tracking active menu in frontend.\n\tid: string\n\n\t// Additional presentation options for the menu item.\n\tpresentation?: #Presentation\n\n\tback: {\n\t\t// Text to display in the back button.\n\t\ttext: string\n\n\t\t// Path to the page to navigate to when the back button is clicked.\n\t\tpath: string\n\t}\n\n\titems: [...#MenuItem]\n}\n\n// Represents an item in a menu.\n#MenuItem: #SectionMenuItem | #BasicMenuItem | #NavMenuItem | #AccordionMenuItem\n\n#SectionMenuItem: {\n\t// Represents a menu section that can't be navigated to.\n\tkind: \"section\"\n\n\t// The text to display in the menu.\n\ttext: string\n\n\t// Menu items to show for this section.\n\titems: [...#MenuItem]\n}\n\n#BasicMenuItem: {\n\t// Represents a basic page that can be navigated to.\n\tkind: \"basic\"\n\n\t// The text to display in the menu.\n\ttext: string\n\n\t// The URL path to the page.\n\tpath: string\n\n\t// The file to render when viewing this page.\n\tfile: string\n\n\t// Inline menu to show when viewing this page.\n\tinline_menu?: [...#MenuItem]\n\n\t// hidden, if true, indicates the page exists but is hidden in the menu.\n\t// It can be navigated to directly, and will be show as \"next page\"/\"prev page\"\n\t// in the per-page navigation.\n\thidden?: true\n}\n\n#NavMenuItem: {\n\t// Represents a page that can be navigated to, that has a menu\n\t// that replaces the navigation when viewing this page.\n\tkind: \"nav\"\n\n\t// The text to display in the menu.\n\ttext: string\n\n\t// The URL path to the page.\n\tpath: string\n\n\t// The file to render when viewing this page.\n\tfile: string\n\n\t// The items to display in the submenu.\n\tsubmenu: #SubMenu\n\n\t// Additional presentation options for the menu item.\n\tpresentation?: #Presentation\n}\n\n#Presentation: {\n\t// Icon to display next to the menu item.\n\ticon?: string\n\tstyle: \"card\" | *\"basic\"\n}\n\n#AccordionMenuItem: {\n\tkind: \"accordion\"\n\ttext: string\n\t// If the accordion is open by default.\n\tdefaultExpanded: bool | *false\n\n\t// The items to display in the accordion.\n\taccordion: [...#MenuItem]\n}\n\n// The root object is a #RootMenu.\n#RootMenu\n{\n\titems: [\n\t\t{\n\t\t\tkind:    \"nav\"\n\t\t\ttext:    \"Encore.go\"\n\t\t\tpath:    \"/go\"\n\t\t\tfile:    \"go/overview\"\n\t\t\tsubmenu: #EncoreGO\n\t\t\tpresentation: {\n\t\t\t\ticon:  \"golang\"\n\t\t\t\tstyle: \"card\"\n\t\t\t}\n\t\t}, {\n\t\t\tkind:    \"nav\"\n\t\t\ttext:    \"Encore.ts\"\n\t\t\tpath:    \"/ts\"\n\t\t\tfile:    \"ts/overview\"\n\t\t\tsubmenu: #EncoreTS\n\t\t\tpresentation: {\n\t\t\t\ticon:  \"typescript\"\n\t\t\t\tstyle: \"card\"\n\t\t\t}\n\t\t}, {\n\t\t\tkind:    \"nav\"\n\t\t\ttext:    \"Encore Cloud\"\n\t\t\tpath:    \"/platform\"\n\t\t\tfile:    \"platform/overview\"\n\t\t\tsubmenu: #EncorePlatform\n\t\t\tpresentation: {\n\t\t\t\ticon:  \"typescript\"\n\t\t\t\tstyle: \"card\"\n\t\t\t}\n\t\t},\n\t]\n}\n\n#EncoreGO: #SubMenu & {\n\ttitle: \"Encore.go\"\n\tid:    \"go\"\n\tpresentation: {\n\t\ticon: \"golang\"\n\t}\n\tback: {\n\t\ttext: \"\"\n\t\tpath: \"\"\n\t}\n\titems: [\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Get Started\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Installation\"\n\t\t\t\tpath: \"/go/install\"\n\t\t\t\tfile: \"go/install\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Quick Start\"\n\t\t\t\tpath: \"/go/quick-start\"\n\t\t\t\tfile: \"go/quick-start\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"AI Integration\"\n\t\t\t\tpath: \"/go/ai-integration\"\n\t\t\t\tfile: \"go/ai-integration\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"FAQ\"\n\t\t\t\tpath: \"/go/faq\"\n\t\t\t\tfile: \"go/faq\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Concepts\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Benefits\"\n\t\t\t\tpath: \"/go/concepts/benefits\"\n\t\t\t\tfile: \"go/concepts/benefits\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Application Model\"\n\t\t\t\tpath: \"/go/concepts/application-model\"\n\t\t\t\tfile: \"go/concepts/application-model\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Tutorials\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a REST API\"\n\t\t\t\tpath: \"/go/tutorials/rest-api\"\n\t\t\t\tfile: \"go/tutorials/rest-api\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building an Uptime Monitor\"\n\t\t\t\tpath: \"/go/tutorials/uptime\"\n\t\t\t\tfile: \"go/tutorials/uptime\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a GraphQL API\"\n\t\t\t\tpath: \"/go/tutorials/graphql\"\n\t\t\t\tfile: \"go/tutorials/graphql\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a Slack bot\"\n\t\t\t\tpath: \"/go/tutorials/slack-bot\"\n\t\t\t\tfile: \"go/tutorials/slack-bot\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a Meeting Notes app\"\n\t\t\t\tpath: \"/go/tutorials/meeting-notes\"\n\t\t\t\tfile: \"go/tutorials/meeting-notes\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a Booking System\"\n\t\t\t\tpath: \"/go/tutorials/booking-system\"\n\t\t\t\tfile: \"go/tutorials/booking-system\"\n\t\t\t}, {\n\t\t\t\tkind:   \"basic\"\n\t\t\t\ttext:   \"Building an Incident Management tool\"\n\t\t\t\tpath:   \"/go/tutorials/incident-management-tool\"\n\t\t\t\tfile:   \"go/tutorials/incident-management-tool\"\n\t\t\t\thidden: true\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Primitives\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"App Structure\"\n\t\t\t\tpath: \"/go/primitives/app-structure\"\n\t\t\t\tfile: \"go/primitives/app-structure\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Services\"\n\t\t\t\tpath: \"/go/primitives/services\"\n\t\t\t\tfile: \"go/primitives/services\"\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"APIs\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Defining APIs\"\n\t\t\t\t\tpath: \"/go/primitives/defining-apis\"\n\t\t\t\t\tfile: \"go/primitives/defining-apis\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"API Calls\"\n\t\t\t\t\tpath: \"/go/primitives/api-calls\"\n\t\t\t\t\tfile: \"go/primitives/api-calls\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Raw Endpoints\"\n\t\t\t\t\tpath: \"/go/primitives/raw-endpoints\"\n\t\t\t\t\tfile: \"go/primitives/raw-endpoints\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Service Structs\"\n\t\t\t\t\tpath: \"/go/primitives/service-structs\"\n\t\t\t\t\tfile: \"go/primitives/service-structs\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"API Errors\"\n\t\t\t\t\tpath: \"/go/primitives/api-errors\"\n\t\t\t\t\tfile: \"go/primitives/api-errors\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"Databases\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Using SQL databases\"\n\t\t\t\t\tpath: \"/go/primitives/databases\"\n\t\t\t\t\tfile: \"go/primitives/databases\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Change SQL database schema\"\n\t\t\t\t\tpath: \"/go/primitives/change-db-schema\"\n\t\t\t\t\tfile: \"go/primitives/change-db-schema\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Integrate with existing databases\"\n\t\t\t\t\tpath: \"/go/primitives/connect-existing-db\"\n\t\t\t\t\tfile: \"go/primitives/connect-existing-db\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Insert test data in a database\"\n\t\t\t\t\tpath: \"/go/primitives/insert-test-data-db\"\n\t\t\t\t\tfile: \"go/primitives/insert-test-data-db\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Share databases between services\"\n\t\t\t\t\tpath: \"/go/primitives/share-db-between-services\"\n\t\t\t\t\tfile: \"go/primitives/share-db-between-services\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"PostgreSQL Extensions\"\n\t\t\t\t\tpath: \"/go/primitives/databases/extensions\"\n\t\t\t\t\tfile: \"go/primitives/database-extensions\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Troubleshooting\"\n\t\t\t\t\tpath: \"/go/primitives/databases/troubleshooting\"\n\t\t\t\t\tfile: \"go/primitives/database-troubleshooting\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Object Storage\"\n\t\t\t\tpath: \"/go/primitives/object-storage\"\n\t\t\t\tfile: \"go/primitives/object-storage\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Cron Jobs\"\n\t\t\t\tpath: \"/go/primitives/cron-jobs\"\n\t\t\t\tfile: \"go/primitives/cron-jobs\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Pub/Sub\"\n\t\t\t\tpath: \"/go/primitives/pubsub\"\n\t\t\t\tfile: \"go/primitives/pubsub\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Caching\"\n\t\t\t\tpath: \"/go/primitives/caching\"\n\t\t\t\tfile: \"go/primitives/caching\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Secrets\"\n\t\t\t\tpath: \"/go/primitives/secrets\"\n\t\t\t\tfile: \"go/primitives/secrets\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Code Snippets\"\n\t\t\t\tpath: \"/go/primitives/code-snippets\"\n\t\t\t\tfile: \"go/primitives/code-snippets\"\n\t\t\t}]\n\t\t}, {\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Development\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Authentication\"\n\t\t\t\tpath: \"/go/develop/auth\"\n\t\t\t\tfile: \"go/develop/auth\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Configuration\"\n\t\t\t\tpath: \"/go/develop/config\"\n\t\t\t\tfile: \"go/develop/config\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"CORS\"\n\t\t\t\tpath: \"/go/develop/cors\"\n\t\t\t\tfile: \"go/develop/cors\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Metadata\"\n\t\t\t\tpath: \"/go/develop/metadata\"\n\t\t\t\tfile: \"go/develop/metadata\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Middleware\"\n\t\t\t\tpath: \"/go/develop/middleware\"\n\t\t\t\tfile: \"go/develop/middleware\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Testing\"\n\t\t\t\tpath: \"/go/develop/testing\"\n\t\t\t\tfile: \"go/develop/testing\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Mocking\"\n\t\t\t\tpath: \"/go/develop/testing/mocking\"\n\t\t\t\tfile: \"go/develop/mocking\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Validation\"\n\t\t\t\tpath: \"/go/develop/validation\"\n\t\t\t\tfile: \"go/develop/validation\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Environment Variables\"\n\t\t\t\tpath: \"/go/develop/env-vars\"\n\t\t\t\tfile: \"go/develop/env-vars\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"CLI\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"CLI Reference\"\n\t\t\t\tpath: \"/go/cli/cli-reference\"\n\t\t\t\tfile: \"go/cli/cli-reference\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Client Generation\"\n\t\t\t\tpath: \"/go/cli/client-generation\"\n\t\t\t\tfile: \"go/cli/client-generation\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Infra Namespaces\"\n\t\t\t\tpath: \"/go/cli/infra-namespaces\"\n\t\t\t\tfile: \"go/cli/infra-namespaces\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"CLI Configuration\"\n\t\t\t\tpath: \"/go/cli/config-reference\"\n\t\t\t\tfile: \"go/cli/config-reference\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Telemetry\"\n\t\t\t\tpath: \"/go/cli/telemetry\"\n\t\t\t\tfile: \"go/cli/telemetry\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"MCP\"\n\t\t\t\tpath: \"/go/cli/mcp\"\n\t\t\t\tfile: \"go/cli/mcp\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Observability\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Development Dashboard\"\n\t\t\t\tpath: \"/go/observability/dev-dash\"\n\t\t\t\tfile: \"go/observability/dev-dash\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Distributed Tracing\"\n\t\t\t\tpath: \"/go/observability/tracing\"\n\t\t\t\tfile: \"go/observability/tracing\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Flow Architecture Diagram\"\n\t\t\t\tpath: \"/go/observability/encore-flow\"\n\t\t\t\tfile: \"go/observability/encore-flow\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Service Catalog\"\n\t\t\t\tpath: \"/go/observability/service-catalog\"\n\t\t\t\tfile: \"go/observability/service-catalog\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Logging\"\n\t\t\t\tpath: \"/go/observability/logging\"\n\t\t\t\tfile: \"go/observability/logging\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Metrics\"\n\t\t\t\tpath: \"/go/observability/metrics\"\n\t\t\t\tfile: \"go/observability/metrics\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Self Hosting\"\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"CI/CD\"\n\t\t\t\t\tpath: \"/go/self-host/ci-cd\"\n\t\t\t\t\tfile: \"go/self-host/ci-cd\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Build Docker Images\"\n\t\t\t\t\tpath: \"/go/self-host/docker-build\"\n\t\t\t\t\tfile: \"go/self-host/self-host\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Configure Infrastructure\"\n\t\t\t\t\tpath: \"/go/self-host/configure-infra\"\n\t\t\t\t\tfile: \"go/self-host/configure-infra\"\n\t\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"How to guides\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Break a monolith into microservices\"\n\t\t\t\tpath: \"/go/how-to/break-up-monolith\"\n\t\t\t\tfile: \"go/how-to/break-up-monolith\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Integrate with a web frontend\"\n\t\t\t\tpath: \"/go/how-to/integrate-frontend\"\n\t\t\t\tfile: \"go/how-to/integrate-frontend\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Temporal with Encore\"\n\t\t\t\tpath: \"/go/how-to/temporal\"\n\t\t\t\tfile: \"go/how-to/temporal\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Build with cgo\"\n\t\t\t\tpath: \"/go/how-to/cgo\"\n\t\t\t\tfile: \"go/how-to/cgo\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Debug with Delve\"\n\t\t\t\tpath: \"/go/how-to/debug\"\n\t\t\t\tfile: \"go/how-to/debug\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Receive regular HTTP requests & Use websockets\"\n\t\t\t\tpath: \"/go/how-to/http-requests\"\n\t\t\t\tfile: \"go/how-to/http-requests\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Atlas + GORM for database migrations\"\n\t\t\t\tpath: \"/go/how-to/atlas-gorm\"\n\t\t\t\tfile: \"go/how-to/atlas-gorm\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use the ent ORM for migrations\"\n\t\t\t\tpath: \"/go/how-to/entgo-orm\"\n\t\t\t\tfile: \"go/how-to/entgo-orm\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Connect for gRPC communication\"\n\t\t\t\tpath: \"/go/how-to/grpc-connect\"\n\t\t\t\tfile: \"go/how-to/grpc-connect\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use a Pub/Sub Transactional Outbox\"\n\t\t\t\tpath: \"/go/how-to/pubsub-outbox\"\n\t\t\t\tfile: \"go/how-to/pubsub-outbox\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Dependency Injection\"\n\t\t\t\tpath: \"/go/how-to/dependency-injection\"\n\t\t\t\tfile: \"go/how-to/dependency-injection\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Auth0 Authentication\"\n\t\t\t\tpath: \"/go/how-to/auth0-auth\"\n\t\t\t\tfile: \"go/how-to/auth0-auth\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Clerk Authentication\"\n\t\t\t\tpath: \"/go/how-to/clerk-auth\"\n\t\t\t\tfile: \"go/how-to/clerk-auth\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Firebase Authentication\"\n\t\t\t\tpath: \"/go/how-to/firebase-auth\"\n\t\t\t\tfile: \"go/how-to/firebase-auth\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use Logto Authentication\"\n\t\t\t\tpath: \"/go/how-to/logto-auth\"\n\t\t\t\tfile: \"go/how-to/logto-auth\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Migration guides\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate using AI agent\"\n\t\t\t\tpath: \"/go/migration/ai-migration\"\n\t\t\t\tfile: \"go/migration/ai-migration\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate away from Encore\"\n\t\t\t\tpath: \"/go/migration/migrate-away\"\n\t\t\t\tfile: \"go/migration/migrate-away\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Community\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Get Involved\"\n\t\t\t\tpath: \"/go/community/get-involved\"\n\t\t\t\tfile: \"go/community/get-involved\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Contribute\"\n\t\t\t\tpath: \"/go/community/contribute\"\n\t\t\t\tfile: \"go/community/contribute\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Open Source\"\n\t\t\t\tpath: \"/go/community/open-source\"\n\t\t\t\tfile: \"go/community/open-source\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Principles\"\n\t\t\t\tpath: \"/go/community/principles\"\n\t\t\t\tfile: \"go/community/principles\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Submit Template\"\n\t\t\t\tpath: \"/go/community/submit-template\"\n\t\t\t\tfile: \"go/community/submit-template\"\n\t\t\t}]\n\t\t},\n\t]\n}\n\n#EncoreTS: #SubMenu & {\n\ttitle: \"Encore.ts\"\n\tid:    \"ts\"\n\tpresentation: {\n\t\ticon: \"typescript\"\n\t}\n\tback: {\n\t\ttext: \"\"\n\t\tpath: \"\"\n\t}\n\titems: [\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Get started\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Installation\"\n\t\t\t\tpath: \"/ts/install\"\n\t\t\t\tfile: \"ts/install\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Quick Start\"\n\t\t\t\tpath: \"/ts/quick-start\"\n\t\t\t\tfile: \"ts/quick-start\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"AI Integration\"\n\t\t\t\tpath: \"/ts/ai-integration\"\n\t\t\t\tfile: \"ts/ai-integration\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"FAQ\"\n\t\t\t\tpath: \"/ts/faq\"\n\t\t\t\tfile: \"ts/faq\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Concepts\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Benefits\"\n\t\t\t\tpath: \"/ts/concepts/benefits\"\n\t\t\t\tfile: \"ts/concepts/benefits\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Application Model\"\n\t\t\t\tpath: \"/ts/concepts/application-model\"\n\t\t\t\tfile: \"ts/concepts/application-model\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Hello World\"\n\t\t\t\tpath: \"/ts/concepts/hello-world\"\n\t\t\t\tfile: \"ts/concepts/hello-world\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Tutorials\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a REST API\"\n\t\t\t\tpath: \"/ts/tutorials/rest-api\"\n\t\t\t\tfile: \"ts/tutorials/rest-api\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building an Uptime Monitor\"\n\t\t\t\tpath: \"/ts/tutorials/uptime\"\n\t\t\t\tfile: \"ts/tutorials/uptime\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a GraphQL API\"\n\t\t\t\tpath: \"/ts/tutorials/graphql\"\n\t\t\t\tfile: \"ts/tutorials/graphql\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Building a Slack bot\"\n\t\t\t\tpath: \"/ts/tutorials/slack-bot\"\n\t\t\t\tfile: \"ts/tutorials/slack-bot\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Primitives\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"App Structure\"\n\t\t\t\tpath: \"/ts/primitives/app-structure\"\n\t\t\t\tfile: \"ts/primitives/app-structure\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Services\"\n\t\t\t\tpath: \"/ts/primitives/services\"\n\t\t\t\tfile: \"ts/primitives/services\"\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"APIs\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Defining APIs\"\n\t\t\t\t\tpath: \"/ts/primitives/defining-apis\"\n\t\t\t\t\tfile: \"ts/primitives/defining-apis\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Validation\"\n\t\t\t\t\tpath: \"/ts/primitives/validation\"\n\t\t\t\t\tfile: \"ts/primitives/validation\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"API Calls\"\n\t\t\t\t\tpath: \"/ts/primitives/api-calls\"\n\t\t\t\t\tfile: \"ts/primitives/api-calls\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Raw Endpoints\"\n\t\t\t\t\tpath: \"/ts/primitives/raw-endpoints\"\n\t\t\t\t\tfile: \"ts/primitives/raw-endpoints\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"GraphQL\"\n\t\t\t\t\tpath: \"/ts/primitives/graphql\"\n\t\t\t\t\tfile: \"ts/primitives/graphql\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Streaming APIs\"\n\t\t\t\t\tpath: \"/ts/primitives/streaming-apis\"\n\t\t\t\t\tfile: \"ts/primitives/streaming-apis\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"API Errors\"\n\t\t\t\t\tpath: \"/ts/primitives/errors\"\n\t\t\t\t\tfile: \"ts/primitives/errors\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Static Assets\"\n\t\t\t\t\tpath: \"/ts/primitives/static-assets\"\n\t\t\t\t\tfile: \"ts/primitives/static-assets\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Cookies\"\n\t\t\t\t\tpath: \"/ts/primitives/cookies\"\n\t\t\t\t\tfile: \"ts/primitives/cookies\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Types\"\n\t\t\t\t\tpath: \"/ts/primitives/types\"\n\t\t\t\t\tfile: \"ts/primitives/types\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Databases\"\n\t\t\t\tpath: \"/ts/primitives/databases\"\n\t\t\t\tfile: \"ts/primitives/databases\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"PostgreSQL Extensions\"\n\t\t\t\tpath: \"/ts/primitives/databases-extensions\"\n\t\t\t\tfile: \"ts/primitives/database-extensions\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Object Storage\"\n\t\t\t\tpath: \"/ts/primitives/object-storage\"\n\t\t\t\tfile: \"ts/primitives/object-storage\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Cron Jobs\"\n\t\t\t\tpath: \"/ts/primitives/cron-jobs\"\n\t\t\t\tfile: \"ts/primitives/cron-jobs\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Pub/Sub\"\n\t\t\t\tpath: \"/ts/primitives/pubsub\"\n\t\t\t\tfile: \"ts/primitives/pubsub\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Caching\"\n\t\t\t\tpath: \"/ts/primitives/caching\"\n\t\t\t\tfile: \"ts/primitives/caching\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Secrets\"\n\t\t\t\tpath: \"/ts/primitives/secrets\"\n\t\t\t\tfile: \"ts/primitives/secrets\"\n\t\t\t}]\n\t\t}, {\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Development\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Authentication\"\n\t\t\t\tpath: \"/ts/develop/auth\"\n\t\t\t\tfile: \"ts/develop/auth\"\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"ORMs\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Overview\"\n\t\t\t\t\tpath: \"/ts/develop/orms\"\n\t\t\t\t\tfile: \"ts/develop/orms/overview\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Knex.js\"\n\t\t\t\t\tpath: \"/ts/develop/orms/knex\"\n\t\t\t\t\tfile: \"ts/develop/orms/knex\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Prisma\"\n\t\t\t\t\tpath: \"/ts/develop/orms/prisma\"\n\t\t\t\t\tfile: \"ts/develop/orms/prisma\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Drizzle\"\n\t\t\t\t\tpath: \"/ts/develop/orms/drizzle\"\n\t\t\t\t\tfile: \"ts/develop/orms/drizzle\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Sequelize\"\n\t\t\t\t\tpath: \"/ts/develop/orms/sequelize\"\n\t\t\t\t\tfile: \"ts/develop/orms/sequelize\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Metadata\"\n\t\t\t\tpath: \"/ts/develop/metadata\"\n\t\t\t\tfile: \"ts/develop/metadata\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Testing\"\n\t\t\t\tpath: \"/ts/develop/testing\"\n\t\t\t\tfile: \"ts/develop/testing\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Debugging\"\n\t\t\t\tpath: \"/ts/develop/debug\"\n\t\t\t\tfile: \"ts/develop/debug\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Middleware\"\n\t\t\t\tpath: \"/ts/develop/middleware\"\n\t\t\t\tfile: \"ts/develop/middleware\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Multithreading\"\n\t\t\t\tpath: \"/ts/develop/multithreading\"\n\t\t\t\tfile: \"ts/develop/multithreading\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Running Scripts\"\n\t\t\t\tpath: \"/ts/develop/running-scripts\"\n\t\t\t\tfile: \"ts/develop/running-scripts\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Environment Variables\"\n\t\t\t\tpath: \"/ts/develop/env-vars\"\n\t\t\t\tfile: \"ts/develop/env-vars\"\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"Monorepo\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Turborepo\"\n\t\t\t\t\tpath: \"/ts/develop/monorepo/turborepo\"\n\t\t\t\t\tfile: \"ts/develop/monorepo/turborepo\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Nx\"\n\t\t\t\t\tpath: \"/ts/develop/monorepo/nx\"\n\t\t\t\t\tfile: \"ts/develop/monorepo/nx\"\n\t\t\t\t}]\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Resources\"\n\t\t\titems: [{\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"Integrations\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Better Auth\"\n\t\t\t\t\tpath: \"/ts/develop/integrations/better-auth\"\n\t\t\t\t\tfile: \"ts/develop/integrations/better-auth\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Polar\"\n\t\t\t\t\tpath: \"/ts/develop/integrations/polar\"\n\t\t\t\t\tfile: \"ts/develop/integrations/polar\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Resend\"\n\t\t\t\t\tpath: \"/ts/develop/integrations/resend\"\n\t\t\t\t\tfile: \"ts/develop/integrations/resend\"\n\t\t\t\t}]\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"CLI\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"CLI Reference\"\n\t\t\t\tpath: \"/ts/cli/cli-reference\"\n\t\t\t\tfile: \"ts/cli/cli-reference\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Client Generation\"\n\t\t\t\tpath: \"/ts/cli/client-generation\"\n\t\t\t\tfile: \"ts/cli/client-generation\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Infra Namespaces\"\n\t\t\t\tpath: \"/ts/cli/infra-namespaces\"\n\t\t\t\tfile: \"ts/cli/infra-namespaces\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"CLI Configuration\"\n\t\t\t\tpath: \"/ts/cli/config-reference\"\n\t\t\t\tfile: \"ts/cli/config-reference\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Telemetry\"\n\t\t\t\tpath: \"/ts/cli/telemetry\"\n\t\t\t\tfile: \"ts/cli/telemetry\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"MCP\"\n\t\t\t\tpath: \"/ts/cli/mcp\"\n\t\t\t\tfile: \"ts/cli/mcp\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Frontend\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Hosting\"\n\t\t\t\tpath: \"/ts/frontend/hosting\"\n\t\t\t\tfile: \"ts/frontend/hosting\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"CORS\"\n\t\t\t\tpath: \"/ts/frontend/cors\"\n\t\t\t\tfile: \"ts/frontend/cors\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Request Client\"\n\t\t\t\tpath: \"/ts/frontend/request-client\"\n\t\t\t\tfile: \"ts/frontend/request-client\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Template Engine\"\n\t\t\t\tpath: \"/ts/frontend/template-engine\"\n\t\t\t\tfile: \"ts/frontend/template-engine\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Mono vs Multi Repo\"\n\t\t\t\tpath: \"/ts/frontend/mono-vs-multi-repo\"\n\t\t\t\tfile: \"ts/frontend/mono-vs-multi-repo\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Observability\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Development Dashboard\"\n\t\t\t\tpath: \"/ts/observability/dev-dash\"\n\t\t\t\tfile: \"ts/observability/dev-dash\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Logging\"\n\t\t\t\tpath: \"/ts/observability/logging\"\n\t\t\t\tfile: \"ts/observability/logging\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Distributed Tracing\"\n\t\t\t\tpath: \"/ts/observability/tracing\"\n\t\t\t\tfile: \"ts/observability/tracing\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Flow Architecture Diagram\"\n\t\t\t\tpath: \"/ts/observability/flow\"\n\t\t\t\tfile: \"ts/observability/flow\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Service Catalog\"\n\t\t\t\tpath: \"/ts/observability/service-catalog\"\n\t\t\t\tfile: \"ts/observability/service-catalog\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Metrics\"\n\t\t\t\tpath: \"/ts/observability/metrics\"\n\t\t\t\tfile: \"ts/observability/metrics\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Self Hosting\"\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"CI/CD\"\n\t\t\t\t\tpath: \"/ts/self-host/ci-cd\"\n\t\t\t\t\tfile: \"ts/self-host/ci-cd\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Build Docker Images\"\n\t\t\t\t\tpath: \"/ts/self-host/build\"\n\t\t\t\t\tfile: \"ts/self-host/build\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Configure Infrastructure\"\n\t\t\t\t\tpath: \"/ts/self-host/configure-infra\"\n\t\t\t\t\tfile: \"ts/self-host/configure-infra\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Deploy to DigitalOcean\"\n\t\t\t\t\tpath: \"/ts/self-host/deploy-digitalocean\"\n\t\t\t\t\tfile: \"ts/self-host/deploy-to-digital-ocean\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Deploy to Railway\"\n\t\t\t\t\tpath: \"/ts/self-host/deploy-railway\"\n\t\t\t\t\tfile: \"ts/self-host/deploy-to-railway\"\n\t\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"How to guides\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Handle file uploads\"\n\t\t\t\tpath: \"/ts/how-to/file-uploads\"\n\t\t\t\tfile: \"ts/how-to/file-uploads\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Use NestJS with Encore\"\n\t\t\t\tpath: \"/ts/how-to/nestjs\"\n\t\t\t\tfile: \"ts/how-to/nestjs\"\n\t\t\t}]\n\t\t}, {\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Migration guides\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate using AI agent\"\n\t\t\t\tpath: \"/ts/migration/ai-migration\"\n\t\t\t\tfile: \"ts/migration/ai-migration\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate away from Encore\"\n\t\t\t\tpath: \"/ts/migration/migrate-away\"\n\t\t\t\tfile: \"ts/migration/migrate-away\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate from Express.js\"\n\t\t\t\tpath: \"/ts/migration/express-migration\"\n\t\t\t\tfile: \"ts/migration/express-migration\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Community\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Get Involved\"\n\t\t\t\tpath: \"/ts/community/get-involved\"\n\t\t\t\tfile: \"ts/community/get-involved\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Contribute\"\n\t\t\t\tpath: \"/ts/community/contribute\"\n\t\t\t\tfile: \"ts/community/contribute\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Open Source\"\n\t\t\t\tpath: \"/ts/community/open-source\"\n\t\t\t\tfile: \"ts/community/open-source\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Principles\"\n\t\t\t\tpath: \"/ts/community/principles\"\n\t\t\t\tfile: \"ts/community/principles\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Submit Template\"\n\t\t\t\tpath: \"/ts/community/submit-template\"\n\t\t\t\tfile: \"ts/community/submit-template\"\n\t\t\t}]\n\t\t},\n\t]\n}\n\n#EncorePlatform: #SubMenu & {\n\ttitle: \"Encore Cloud\"\n\tid:    \"platform\"\n\tpresentation: {\n\t\ticon: \"\"\n\t}\n\tback: {\n\t\ttext: \"\"\n\t\tpath: \"\"\n\t}\n\titems: [\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Concepts\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Introduction\"\n\t\t\t\tpath: \"/platform/introduction\"\n\t\t\t\tfile: \"platform/introduction\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"AI Integration\"\n\t\t\t\tpath: \"/platform/ai-integration\"\n\t\t\t\tfile: \"platform/ai-integration\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Deployment\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Deploying & CI/CD\"\n\t\t\t\tpath: \"/platform/deploy/deploying\"\n\t\t\t\tfile: \"platform/deploy/deploying\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Connect your cloud account\"\n\t\t\t\tpath: \"/platform/deploy/own-cloud\"\n\t\t\t\tfile: \"platform/deploy/own-cloud\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Environments\"\n\t\t\t\tpath: \"/platform/deploy/environments\"\n\t\t\t\tfile: \"platform/deploy/environments\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Preview Environments\"\n\t\t\t\tpath: \"/platform/deploy/preview-environments\"\n\t\t\t\tfile: \"platform/deploy/preview-environments\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Application Security\"\n\t\t\t\tpath: \"/platform/deploy/security\"\n\t\t\t\tfile: \"platform/deploy/security\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Infrastructure\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Provisioning & Environments\"\n\t\t\t\tpath: \"/platform/infrastructure/infra\"\n\t\t\t\tfile: \"platform/infrastructure/infra\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Infrastructure Configuration\"\n\t\t\t\tpath: \"/platform/infrastructure/configuration\"\n\t\t\t\tfile: \"platform/infrastructure/configuration\"\n\t\t\t},\n\t\t\t{\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"GCP Infrastructure\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Overview\"\n\t\t\t\t\tpath: \"/platform/infrastructure/gcp\"\n\t\t\t\t\tfile: \"platform/infrastructure/gcp\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Import Cloud SQL\"\n\t\t\t\t\tpath: \"/platform/infrastructure/gcp/import-cloud-sql\"\n\t\t\t\t\tfile: \"platform/infrastructure/import-cloud-sql\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Import Project\"\n\t\t\t\t\tpath: \"/platform/infrastructure/gcp/import-project\"\n\t\t\t\t\tfile: \"platform/infrastructure/import-project\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Configure Network\"\n\t\t\t\t\tpath: \"/platform/infrastructure/configure-network\"\n\t\t\t\t\tfile: \"platform/infrastructure/configure-network\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"AWS Infrastructure\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Overview\"\n\t\t\t\t\tpath: \"/platform/infrastructure/aws\"\n\t\t\t\t\tfile: \"platform/infrastructure/aws\"\n\t\t\t\t},{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Import RDS Database\"\n\t\t\t\t\tpath: \"/platform/infrastructure/aws/import-rds\"\n\t\t\t\t\tfile: \"platform/infrastructure/import-rds\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Configure Network\"\n\t\t\t\t\tpath: \"/platform/infrastructure/configure-network\"\n\t\t\t\t\tfile: \"platform/infrastructure/configure-network\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"accordion\"\n\t\t\t\ttext: \"Kubernetes deployment\"\n\t\t\t\taccordion: [{\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Deploying to a new cluster\"\n\t\t\t\t\tpath: \"/platform/infrastructure/kubernetes\"\n\t\t\t\t\tfile: \"platform/infrastructure/kubernetes\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Import an existing cluster\"\n\t\t\t\t\tpath: \"/platform/infrastructure/import-kubernetes-cluster\"\n\t\t\t\t\tfile: \"platform/infrastructure/import-kubernetes-cluster\"\n\t\t\t\t}, {\n\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\ttext: \"Configure kubectl\"\n\t\t\t\t\tpath: \"/platform/infrastructure/configure-kubectl\"\n\t\t\t\t\tfile: \"platform/infrastructure/configure-kubectl\"\n\t\t\t\t}]\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Neon Postgres\"\n\t\t\t\tpath: \"/platform/infrastructure/neon\"\n\t\t\t\tfile: \"platform/infrastructure/neon\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Cloudflare R2\"\n\t\t\t\tpath: \"/platform/infrastructure/cloudflare\"\n\t\t\t\tfile: \"platform/infrastructure/cloudflare\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Managing database users\"\n\t\t\t\tpath: \"/platform/infrastructure/manage-db-users\"\n\t\t\t\tfile: \"platform/infrastructure/manage-db-users\"\n\t\t\t}]\n\t\t}, {\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Observability\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Metrics\"\n\t\t\t\tpath: \"/platform/observability/metrics\"\n\t\t\t\tfile: \"platform/observability/metrics\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Distributed Tracing\"\n\t\t\t\tpath: \"/platform/observability/tracing\"\n\t\t\t\tfile: \"platform/observability/tracing\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Flow Architecture Diagram\"\n\t\t\t\tpath: \"/platform/observability/encore-flow\"\n\t\t\t\tfile: \"platform/observability/encore-flow\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Service Catalog\"\n\t\t\t\tpath: \"/platform/observability/service-catalog\"\n\t\t\t\tfile: \"platform/observability/service-catalog\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Integrations\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"GitHub\"\n\t\t\t\tpath: \"/platform/integrations/github\"\n\t\t\t\tfile: \"platform/integrations/github\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Custom Domains\"\n\t\t\t\tpath: \"/platform/integrations/custom-domains\"\n\t\t\t\tfile: \"platform/integrations/custom-domains\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Webhooks\"\n\t\t\t\tpath: \"/platform/integrations/webhooks\"\n\t\t\t\tfile: \"platform/integrations/webhooks\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"OAuth Clients\"\n\t\t\t\tpath: \"/platform/integrations/oauth-clients\"\n\t\t\t\tfile: \"platform/integrations/oauth-clients\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Auth Keys\"\n\t\t\t\tpath: \"/platform/integrations/auth-keys\"\n\t\t\t\tfile: \"platform/integrations/auth-keys\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"API Reference\"\n\t\t\t\tpath: \"/platform/integrations/api-reference\"\n\t\t\t\tfile: \"platform/integrations/api-reference\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Terraform\"\n\t\t\t\tpath: \"/platform/integrations/terraform\"\n\t\t\t\tfile: \"platform/integrations/terraform\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Migration guides\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate to Encore\"\n\t\t\t\tpath: \"/platform/migration/migrate-to-encore\"\n\t\t\t\tfile: \"platform/migration/migrate-to-encore\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Migrate away from Encore\"\n\t\t\t\tpath: \"/platform/migration/migrate-away\"\n\t\t\t\tfile: \"platform/migration/migrate-away\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Management & Billing\"\n\t\t\titems: [{\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Security & Compliance\"\n\t\t\t\tpath: \"/platform/management/compliance\"\n\t\t\t\tfile: \"platform/management/compliance\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Plans & billing\"\n\t\t\t\tpath: \"/platform/management/billing\"\n\t\t\t\tfile: \"platform/management/billing\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Telemetry\"\n\t\t\t\tpath: \"/platform/management/telemetry\"\n\t\t\t\tfile: \"platform/management/telemetry\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Roles & Permissions\"\n\t\t\t\tpath: \"/platform/management/permissions\"\n\t\t\t\tfile: \"platform/management/permissions\"\n\t\t\t}, {\n\t\t\t\tkind: \"basic\"\n\t\t\t\ttext: \"Usage limits\"\n\t\t\t\tpath: \"/platform/management/usage\"\n\t\t\t\tfile: \"platform/management/usage\"\n\t\t\t}]\n\t\t},\n\t\t{\n\t\t\tkind: \"section\"\n\t\t\ttext: \"Other\"\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tkind: \"accordion\"\n\t\t\t\t\ttext: \"Product comparisons\"\n\t\t\t\t\taccordion: [{\n\t\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\t\ttext: \"Encore vs. Heroku\"\n\t\t\t\t\t\tpath: \"/platform/other/vs-heroku\"\n\t\t\t\t\t\tfile: \"platform/other/vs-heroku\"\n\t\t\t\t\t}, {\n\t\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\t\ttext: \"Encore vs. Supabase / Firebase\"\n\t\t\t\t\t\tpath: \"/platform/other/vs-supabase\"\n\t\t\t\t\t\tfile: \"platform/other/vs-supabase\"\n\t\t\t\t\t}, {\n\t\t\t\t\t\tkind: \"basic\"\n\t\t\t\t\t\ttext: \"Encore vs. Terraform / Pulumi\"\n\t\t\t\t\t\tpath: \"/platform/other/vs-terraform\"\n\t\t\t\t\t\tfile: \"platform/other/vs-terraform\"\n\t\t\t\t\t}]\n\t\t\t\t}]\n\t\t},\n\t]\n}\n"
  },
  {
    "path": "docs/platform/ai-integration.md",
    "content": "---\nseotitle: AI-Powered Development with Encore Cloud\nseodesc: Learn how Encore Cloud enables AI agents to provision infrastructure in AWS/GCP with automatic guardrails, preview environments, and more.\ntitle: AI Integration\nsubtitle: AI agents that can provision real infrastructure in your cloud\nlang: platform\n---\n\nEncore Cloud supercharges AI-powered development by letting AI agents provision real infrastructure in your AWS or GCP account with automatic guardrails.\n\nWhen you connect your cloud account to [Encore Cloud](https://encore.cloud), AI-generated code that declares databases, pub/sub topics, cron jobs, and other [primitives](/docs/ts/primitives) gets automatically provisioned with production-ready defaults: proper networking, IAM permissions, and security configurations.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/aws-demo.mp4\" type=\"video/mp4\" />\n</video>\n\n## How It Works\n\n1. **AI writes infrastructure code** using Encore's declarative primitives\n2. **Push to GitHub** to trigger a deployment\n3. **Encore Cloud provisions** the infrastructure in your AWS/GCP account with automatic guardrails\n4. **Preview environments** let you test AI-generated changes in isolation\n\nThis enables fast iterative development: AI generates code, you push it and validate in preview environments, then deploy seamlessly with automatic infrastructure provisioning.\n\n## Infrastructure with Guardrails\n\nWhen AI declares infrastructure using Encore primitives, Encore Cloud provisions it in your cloud with automatic guardrails:\n\n- **Databases**: Proper networking, encryption at rest, automated backups\n- **Pub/Sub**: Dead letter queues, retry policies, proper IAM roles\n- **Secrets**: Encrypted storage, access controls\n- **Services**: Load balancing, health checks, auto-scaling\n\nAI doesn't need to know the intricacies of AWS or GCP. It just declares what it needs, and Encore Cloud handles the cloud-specific configuration.\n\nYou stay in control: review infrastructure changes in pull requests, approve or deny resource additions, and use [infrastructure configuration](/docs/platform/infrastructure/configuration) to customize defaults per environment.\n\n## Preview Environments\n\n[Preview environments](/docs/platform/deploy/preview-environments) are perfect for testing AI-generated changes. Each pull request gets its own isolated environment with real infrastructure.\n\nThis means you can:\n\n- Let AI generate features and immediately test them with real databases and services\n- Review AI-generated infrastructure changes before they hit production\n- Catch issues early in isolated environments\n\n## Production Observability\n\nEncore Cloud provides [distributed tracing](/docs/platform/observability/tracing) and [metrics](/docs/platform/observability/metrics) across all your environments. You can:\n\n- Analyze traces to debug issues across services\n- Inspect timing to find bottlenecks\n- Compare behavior between preview and production environments\n\n## Connecting Your Cloud\n\nTo deploy your Encore app to your cloud:\n\n1. [Sign up for Encore Cloud](https://app.encore.dev)\n2. [Connect your AWS or GCP account](/docs/platform/deploy/own-cloud)\n3. Push your Encore app to deploy\n\nEncore Cloud provisions infrastructure in your cloud account based on the primitives declared in your code. You maintain full control and ownership of your infrastructure.\n\n## What AI Can Provision\n\nWith Encore Cloud connected, AI-generated code can provision:\n\n| Resource | AWS | GCP |\n|----------|-----|-----|\n| Databases | RDS (PostgreSQL) | Cloud SQL |\n| Pub/Sub | SNS + SQS | Cloud Pub/Sub |\n| Object Storage | S3 | Cloud Storage |\n| Cron Jobs | CloudWatch Events | Cloud Scheduler |\n| Secrets | Secrets Manager | Secret Manager |\n| Caching | ElastiCache | Memorystore |\n\nAll resources are provisioned with security best practices like least-privilege IAM policies, private networking, and encryption. See the [AWS](/docs/platform/infrastructure/aws) and [GCP](/docs/platform/infrastructure/gcp) infrastructure docs for specifics.\n\n## Learn More\n\n- [Connect Your Cloud Account](/docs/platform/deploy/own-cloud)\n- [Preview Environments](/docs/platform/deploy/preview-environments)\n- [Infrastructure Configuration](/docs/platform/infrastructure/configuration)\n- [Framework AI Integration (TypeScript)](/docs/ts/ai-integration)\n- [Framework AI Integration (Go)](/docs/go/ai-integration)\n"
  },
  {
    "path": "docs/platform/deploy/deploying.md",
    "content": "---\nseotitle: Deploying your Encore application is as simple as git push\nseodesc: Learn how to deploy your backend application built with Encore with a single command, while Encore manages your entire CI/CD process.\ntitle: Deploying Applications with Encore Cloud\nsubtitle: Encore Cloud automates the deployment and infrastructure provisioning process\nlang: platform\n---\n\nEncore Cloud simplifies deploying your application, making it as simple as pushing to a git repository, removing the need for manual steps.\n\n## Deploying your application\n\n### Step 1: Create account & application\n\nBefore deploying, ensure that you have an **Encore Cloud account** and have created an **Encore application**.\n\nYou can create both an account and an application by running the following command:\n\n```shell\n$ encore app create\n```\n\nYou will be asked to create a free Encore Cloud account first, and then proceed to create a new Encore application.\n\n#### Already created an application locally?\n\nFollow these steps if you've already created an app and want to link it to an account on Encore Cloud:\n\n**1. Ensure you are logged in with the CLI**\n\n```bash\nencore auth signup # If you haven't created an Encore Cloud account\nencore auth login # If you've already created an Encore Cloud account\n```\n\n**2. Link your local app to Encore Cloud**\n\nRun this command from you application's root folder:\n\n```bash\nencore app init\n```\n\n**3. Set up Encore's git remote to enable pushing directly to Encore Cloud**\n\nRun this command from you application's root folder:\n\n```bash\ngit remote add encore encore://<app-id>\n```\n\n\n### Step 2: Integrate with GitHub (Optional)\n\nWhen creating an Encore application, Encore will automatically create a new Encore managed git repository. If you are just trying out Encore Cloud, you can use this and skip the rest of this step.\n\nFor production applications we recommend integrating with GitHub instead of using the built-in Encore managed git:\n\n#### **Connecting your GitHub account**\n\nOpen your app in the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > GitHub**.\nClick the **Connect Account to GitHub** button, which will open GitHub where you can grant access either to the relevant repositorie(s).\n\n[See the full docs](/docs/platform/integrations/github) on integrating with GitHub to learn how to configure different repository structures.\n\nOnce connected to GitHub, pushing code will trigger deployments automatically. Encore Cloud Pro users get [Preview Environments](/docs/platform/deploy/preview-environments) for each pull request.\n\n### Step 3: Connect your AWS / GCP account (Optional)\n\nDeploy to your own cloud on AWS or GCP by connecting your cloud account to Encore Cloud.\n\nIf you're just trying out Encore Cloud, skip this step to deploy to a free development environment using Encore Cloud's hosting, subject to [fair use limits](/docs/platform/management/usage).\n\n#### **Connecting your cloud account**\n\nOpen your app in the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > Connect Cloud**.\n\nLearn more in the [connecting your cloud docs](/docs/platform/deploy/own-cloud).\n\n### Step 4: Push to deploy\n\nDeploy your application by pushing your code to the connected Git repository.\n\n- **Using Encore Cloud's managed git**:\n\n```shell\n$ git add -A .\n$ git commit -m 'Commit message'\n$ git push encore\n```\n\n- **If you have connected your GitHub account:**\n\n```shell\n$ git add -A .\n$ git commit -m 'Commit message'\n$ git push origin\n```\n\nThis will trigger Encore Cloud's deployment process, consisting of the following phases:\n* A build & test phase\n* An infrastructure provisioning phase\n* A deployment phase\n\nOnce you've pushed your code, you can monitor the progress in the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > Deployments**.\n\n## Configuring deploy trigger\n\nWhen using GitHub, you can configure Encore Cloud to automatically trigger deploys when you push to a specific branch name.\n\nTo configure which branch name is used to trigger deploys, open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Overview** page for your intended environment. Click on **Settings** and then in the section **Branch Push** configure the `Branch name`  and hit save.\n\n### Integrating using Encore Cloud's API\n\nYou can trigger deployments using Encore Cloud's API, learn more in the [API reference](/docs/platform/integrations/api-reference).\n\n## Configuring custom build settings\n\nIf you want, you can override certain aspects of the CI/CD process in the `encore.app` file:\n\n* The Docker base image to use when deploying\n* Whether to build with Cgo enabled\n* Whether to bundle the source code in the docker image (useful for [Sentry stack traces](https://docs.sentry.io/platforms/go/usage/serverless/))\n\nBelow are the available build settings configurable in the `encore.app` file,\nwith their default values:\n\n```cue\n{\n    \"build\": {\n        // Enables cgo when building the application and running tests\n        // in Encore's CI/CD system.\n        \"cgo_enabled\": false,\n\n        // Docker-related configuration\n        \"docker\": {\n            // The Docker base image to use when deploying the application.\n            // It must be a publicly accessible image. It defaults to \"scratch\" for go apps\n            // and \"node:24-trixie\" for typescript apps.\n            \"base_image\": \"scratch\",\n\n            // Whether to bundle the source code in the docker image.\n            // The source code will be copied into /workspace as part\n            // of the build process. This is primarily useful for tools like\n            // Sentry that need access to the source code to generate stack traces.\n            \"bundle_source\": false,\n\n            // The working directory to start the docker image in.\n            // If empty it defaults to \"/workspace\" if the source code is bundled, and to \"/\" otherwise.\n            \"working_dir\": \"\"\n        }\n\n        // Build hooks allow you to run custom commands during the build process.\n        // They can be specified as a string (e.g. \"cmd1 && cmd2\") or as an object\n        // with a command and optional environment variables.\n        \"hooks\": {\n            // Runs before the Encore build, but after dependencies are fetched (e.g. npm install).\n            \"prebuild\": \"\",\n            // Or as an object:\n            // \"prebuild\": {\"command\": \"my-command\", \"env\": {\"MY_VAR\": \"value\"}},\n\n            // Runs after the Encore build has finished.\n            \"postbuild\": \"\"\n            // Or as an object:\n            // \"postbuild\": {\"command\": \"my-command\", \"env\": {\"MY_VAR\": \"value\"}}\n        }\n    }\n}\n```\n"
  },
  {
    "path": "docs/platform/deploy/environments.md",
    "content": "---\nseotitle: Environments – Creating local, preview, and prod environments\nseodesc: Learn how to create all the environments you need for your backend application, local, preview, testing and production. Here's how you keep them in sync!\ntitle: Creating & configuring environments\nsubtitle: Get the environments you need, without the work\nlang: platform\n---\n\nEncore automatically sets up and manages different environments for your application (local, preview, testing, and production). Each environment is:\n- Fully isolated\n- Automatically provisioned\n- Always in sync with your codebase\n- Configured with appropriate infrastructure for its purpose\n\n## Environment Types\n\nEncore has four types of environments:\n- `production`\n- `development`\n- `preview`\n- `local`\n\nSome environment types differ in how infrastructure is provisioned:\n- `local` is provisioned by Encore's Open Source CLI using local versions of infrastructure.\n- `preview` environments are provisioned in Encore Cloud hosting and are optimized to be cost-efficient and fast to provision.\n- `production` and `development` environments are provisioned by Encore Cloud, either in your [cloud account](/docs/platform/deploy/own-cloud) or using Encore Cloud's free development hosting. Both environment types offer the same infrastructure options when deployed using your own cloud account.\n  \nEnvironment type is also used for [Secrets management](/docs/ts/primitives/secrets), allowing you to configure different secrets for different environment types. Therefore, you can easily configure different secrets for your `production` and `development` environments.\n\n## Creating environments\n\n1. Open your app in the [Encore Cloud dashboard](https://app.encore.cloud)\n2. Go to **Environments** > **Create env**\n3. Configure your environment:\n   - Name your environment\n   - Choose type: **Production** or **Development** (see [Environment Types](#environment-types))\n   - Set deploy trigger: Git branch or manual\n   - Configure infrastructure approval: automatic or manual\n   - Select cloud provider\n   - Choose process allocation: single or separate processes\n\n![Creating an environment](/assets/docs/createenv.png \"Creating an environment\")\n\n### Configuring deploy trigger\n\nWhen using GitHub, you can configure Encore Cloud to automatically trigger deploys when you push to a specific branch name.\n\nTo configure which branch name is used to trigger deploys, open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Overview** page for your intended environment. Click on **Settings** and then in the section **Branch Push** configure the `Branch name`  and hit save.\n\n### Configuring infrastructure approval\n\nFor some environments you may want to enforce infrastructure approval before deploying. You can configure this in the **Settings** > **Infrastructure Approval** section for your environment.\n\nWhen infrastructure approval is enabled, an application **Admin** will need to manually approve the infrastructure changes before the deployment can proceed.\n\n### Configuring process allocation\n\nEncore Cloud offers flexible process allocation options:\n- **Single process**: All services run in one process (simpler, lower cost)\n- **Separate processes**: Each service runs independently (better isolation, independent scaling)\n\nChoose your preferred deployment model when creating each environment. You can use different models for production and development environments without changing any code.\n\n<img src=\"/assets/docs/microservices-process-allocation.png\" title=\"Microservices - Process Allocation\" />\n\n## Setting a Primary environment\n\nEvery Encore app has a configurable Primary environment that serves as the default for:\n- App insights in the Encore Cloud dashboard\n- API documentation\n- CLI functionality (like API client generation)\n\n**Configuring your Primary environment:**\n1. Open your app in the [Encore Cloud dashboard](https://app.encore.cloud)\n2. Navigate to **Settings** > **General** > **Primary Environment**\n3. Select your desired environment from the dropdown\n4. Click **Update**\n"
  },
  {
    "path": "docs/platform/deploy/own-cloud.md",
    "content": "---\nseotitle: Connect your cloud account to deploy to any cloud\nseodesc: Learn how to deploy your backend application to all the major cloud providers (AWS or GCP) using Encore.\ntitle: Connect your cloud account\nsubtitle: Whatever cloud you prefer is fine by us\nlang: platform\n---\n\nEncore Cloud lets you deploy your application to any of the major cloud providers, using your own cloud account.\nThis lets you use Encore to improve your experience and productivity, while keeping the reliability of a major cloud provider.\n\nEach [environment](/docs/platform/deploy/environments) can be configured to use a different cloud provider, and you can have as many environments as you wish.\nThis also lets you easily deploy a hybrid or multi-cloud application, as you see fit.\n\n<Callout type=\"info\">\n\nEncore Cloud will provision infrastructure in your cloud account, but for safety reasons Encore Cloud does not automatically destroy infrastructure once it's no longer required. To do this, you need to manually approve the deletion of the infrastructure in your Encore Cloud dashboard.\n\nThis means if you disconnect your app from your cloud provider, or delete the environment\nwithin Encore, you need to explicitly approve the deletion of the infrastructure in your Encore Cloud dashboard.\n\n</Callout>\n\n## Google Cloud Platform (GCP)\n\nEncore Cloud provides a GCP Service Account for each Encore Cloud application, letting you grant Encore Cloud access to provision all the necessary infrastructure directly in your own GCP Organization account.\n\nTo find your app's Service Account email and configure GCP deployments, head over to the Connect Cloud page by going to the **[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > Connect Cloud**.\n\n![Connect GCP account](/assets/docs/connectgcp.png \"Connect GCP account\")\n\n### Troubleshooting\n\n**I can't access/edit the `Policy for Domain restricted sharing` page**\n\nTo edit Organization policies, you need to have the `Organization Policy Administrator` role. If you don't have this role, you can ask your GCP Organization Administrator to grant you the necessary permissions.\nIf you're a GCP Organization Administrator, you can grant yourself the necessary permissions by following the steps below:\n\n1. Go to the [IAM & Admin page](https://console.cloud.google.com/iam-admin/iam) in the GCP Console.\n2. Find your user account in the list of members.\n3. Click the pencil icon to edit your user account.\n4. Add the `Organization Policy Administrator` role to your user account.\n5. Click Save.\n\n**I can't grant access to the Encore Cloud service account**\n\nIf you're unable to grant access to the Encore Cloud service account, you may have failed to add Encore Cloud to your `Domain restricted sharing` policy.\nMake sure you've followed all the steps in the Connect Cloud page to add Encore Cloud to the policy.\nIf you're using several GCP accounts, make sure you're logged in with the correct account and that the correct organization is selected in the GCP Console.\n\n**Encore Cloud returns \"Could not find Organization ID\"**\n\nIf you see this error message, it means that Encore Cloud was unable to connect to your GCP Organization. Make sure you've followed all the steps in the Connect Cloud page to grant Encore Cloud access to your GCP Organization.\nIf you're using several GCP accounts, make sure you're logged in with the correct account and that the correct organization is selected in the GCP Console.\n\nStill having issues? Drop us an email at [support@encore.dev](mailto:support@encore.dev) or chat with us in the [Encore Discord](https://encore.dev/discord.\n\n## Amazon Web Services (AWS)\nTo configure your Encore Cloud app to deploy to your AWS account, head over to the Connect Cloud page by going to the\n**[Encore Cloud dashboard](https://app.encore.cloud/) > (Select your app) > App Settings > Integrations > Connect Cloud**.\n\nFollow the instructions to create an IAM Role, and then connect the role with Encore Cloud.\n[Learn more in the AWS docs](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user.html).\n\n![Connect AWS account](/assets/docs/connectaws.png \"Connect AWS account\")\n\n\n<Callout type=\"warning\">\n\nFor your security, make sure to check `Require external ID` and specify the\nexternal ID provided in the instructions.\n\n</Callout>\n\nAfter connecting your app to AWS, you will be asked to choose which region you want Encore Cloud to provision resources in. [Learn more about AWS regions here](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/).\n"
  },
  {
    "path": "docs/platform/deploy/preview-environments.md",
    "content": "---\nseotitle: Preview Environments – Temporary dev environments per Pull Request\nseodesc: Learn how to use Encore to activate automatic Preview Environments for every Pull Request to simplify testing and collaborating.\ntitle: Preview Environments\nsubtitle: Accelerate development with isolated test environments for each Pull Request\nlang: platform\n---\n\nWhen using [Encore Cloud Pro](https://encore.cloud/pricing), you automatically get ephemeral Preview Environments for each Pull Request.\n\nPreview Environments are free, fully-managed development environments that run on Encore Cloud. They let you test changes without managing infrastructure or incurring cost.\n\nSee the [infra docs](/docs/platform/infrastructure/infra#preview-environments) if you're curious about exactly how Preview Environments are provisioned.\n\n## Using Preview Environments\n\nTo use Preview Environments, you first need to [connect your application to GitHub](/docs/platform/integrations/github).\n\nPreview Environments are named after the pull request, for example PR #72 creates a Preview Environment named `pr:72` with the API base url `https://pr72-$APP_ID.encr.app`.\n\nYou can also view the environment in the Encore Cloud dashboard, where the url will be `https://app.encore.cloud/$APP_ID/envs/pr:72`.\n\n![Preview environment linked in GitHub](/assets/docs/ghpreviewenv.png \"Preview environment linked in GitHub\")\n\n## Populate databases with test data automatically\n\nPreview Environments can automatically come with pre-populated test data thanks to Neon's database branching feature. Here's how it works:\n\n1. Your main database (typically in staging) contains your test data\n2. When a Preview Environment is created, it gets a fresh database that's an exact copy of your main database\n3. This happens automatically - no manual data copying needed!\n\n#### Setup instructions\n1. Go to [Encore Cloud dashboard](https://app.encore.cloud)\n2. Select your app > App Settings > Preview Environments\n3. Choose which environment's database to copy from (e.g., staging)\n4. Save your changes\n\n**Note:** This feature requires using Neon as your database provider, which is:\n- Default for Encore Cloud environments\n- Optional for AWS and GCP environments\n\n<img src=\"/assets/docs/pr-neon.png\" title=\"Use Neon for PR environments\" className=\"mx-auto\"/>\n\n## Frontend Collaboration\n\nPreview Environments make it really easy to collaborate and test changes with your frontend. Just update your frontend API client to point to the `pr:#` environment.\nThis is a one-line change since your API client always specifies the environment name, e.g. `https://<env>-<my-app>.encr.app/`.\n\nIf your pull request makes changes to the API, you can [generate a new API client](/docs/ts/cli/client-generation)\nfor the new backend API using `encore gen client --env=pr:72 --lang=typescript my-app`\n"
  },
  {
    "path": "docs/platform/deploy/security.md",
    "content": "---\nseotitle: Security – How Encore keeps your backend application secure\nseodesc: Encore applications come with built-in security best practises. See how Encore keeps your application secure by default.\ntitle: Application Security\nsubtitle: Encore Cloud makes strong security the default path\nlang: platform\n---\n\n## Built on industry experience\n\nThe security practices in Encore Cloud are built on our team's decades of experience designing and operating sensitive systems at companies like Google, Spotify, and Monzo.\n\n## Security by Default\n\nEncore Cloud is designed to make security effortless rather than burdensome:\n\n- **Zero-config security**: Focus on building features while Encore Cloud automatically implements security best practices\n- **Built-in secrets management**: Safely handle sensitive data using the built-in [secrets management system](/docs/ts/primitives/secrets)\n- **Automated IAM management**: Encore Cloud automatically manages IAM policies based on the principle of least privilege\n\n## Security features\n\nWhen Encore Cloud deploys your application and infrastructure, it takes care of implementing security best practices:\n\n- **Strong encryption**: All communication uses mutual TLSv1.3\n- **Secure databases**: Database access is encrypted with certificate validation and strong security credentials\n- **Cloud security**: Automatic provisioning with security best practices specific to each cloud provider\n  - Learn more about [Google Cloud Platform (GCP)](/docs/platform/infrastructure/gcp)\n  - Learn more about [Amazon Web Services (AWS)](/docs/platform/infrastructure/aws)\n"
  },
  {
    "path": "docs/platform/infrastructure/aws.md",
    "content": "---\nseotitle: AWS Infrastructure on Encore Cloud\nseodesc: A comprehensive guide to how Encore Cloud provisions and manages AWS infrastructure for your applications\ntitle: AWS Infrastructure\nsubtitle: Understanding your application's AWS infrastructure\nlang: platform\n---\nEncore Cloud simplifies the process of deploying applications by automatically provisioning and managing the necessary AWS infrastructure. This guide provides a detailed look at the components involved and how they work together to support your applications.\n\n## Core Infrastructure Components\n\n### Networking Architecture\n\nNetworking is a critical aspect of cloud infrastructure, ensuring secure and efficient communication between different parts of your application. Encore Cloud creates an isolated [Virtual Private Cloud (VPC)][aws-vpc] for each environment, which serves as a secure network boundary.\n\nThe network architecture is designed with reliability and security in mind. Each VPC spans across two Availability Zones (AZs), providing redundancy and fault tolerance. If one AZ experiences issues, your application can continue running in the other AZ, significantly reducing the risk of downtime. This multi-AZ setup is crucial for maintaining high availability in production environments.\n\nWithin the VPC, Encore Cloud implements a three-tier architecture that carefully separates different components of your application into distinct subnet layers. This separation of concerns enhances both security and performance by controlling traffic flow between layers and limiting potential attack vectors. Each tier is configured with specific security groups and network ACLs to enforce these boundaries, creating a robust and secure networking foundation for your application.\n\n#### Subnet Tiers\n\n1. **Public Subnet**\n   The public subnet contains several key components that manage external traffic flow. At the forefront is the Application Load Balancer (ALB), which serves as the entry point for all incoming traffic to your application. The ALB intelligently distributes requests across your application instances, ensuring optimal performance and reliability.\n\n   To enable outbound communication, the subnet includes an Internet Gateway that allows your application components to securely connect to external services and APIs. Working alongside it is a NAT Gateway, which provides a secure pathway for resources in private subnets (like your compute instances) to access the internet while remaining protected from direct external access. This NAT Gateway acts as an intermediary, translating private IP addresses to public ones for outbound traffic while maintaining the security of your internal resources.\n\n2. **Compute Subnet**\n   The compute subnet is where your application's containers run, regardless of whether you're using Fargate or EKS as your container orchestration platform. This subnet is carefully isolated and configured to only accept incoming traffic from the Application Load Balancer in the public subnet. This strict traffic control ensures that your application containers can only be accessed through proper channels, protecting them from unauthorized direct access while still allowing legitimate requests to flow through seamlessly.\n\n3. **Storage Subnet** (provisioned as needed)\n   The storage subnet is a dedicated network segment designed to host your application's databases and caching systems. To maintain the highest level of security, this subnet operates in complete isolation from the internet, with no direct inbound or outbound connectivity. Access to resources within the storage subnet is strictly limited to traffic originating from the compute subnet, creating a secure enclave for your data layer. This architecture ensures that your sensitive data remains protected while still being readily accessible to your application's services running in the compute tier.\n\n### Container Management\n\nEncore Cloud provisions an [Elastic Container Registry (ECR)][aws-ecr] to store your application's Docker images. The registry is seamlessly integrated with your chosen compute platform and provides robust security features. Access to images is tightly controlled through comprehensive access controls, ensuring only authorized users and services can pull or push container images. Additionally, ECR automatically scans all images for known security vulnerabilities as they are pushed to the registry, helping you maintain a secure application environment by identifying potential risks before deployment.\n\n### Secrets Management\n\nManaging sensitive information securely is crucial. Encore Cloud uses [AWS Secrets Manager][aws-secrets] to store and manage secrets, such as API keys and database credentials. Through deep integration with AWS Secrets Manager, Encore Cloud automatically injects secrets directly into your service's environment variables at runtime, making them easily accessible while maintaining strict security controls. All secrets are encrypted both at rest and in transit using industry-standard encryption algorithms, providing comprehensive protection for your sensitive data. The system implements fine-grained access controls, where each service is given precisely scoped permissions to access only the specific secrets it needs. This ensures that even if one service is compromised, the blast radius is contained and other secrets remain secure.\n\n## Compute Options\n\nEncore Cloud provisions one of two compute platforms for running your application containers, based on your choice:\n\n### AWS Fargate\n\nWhen using Fargate, Encore Cloud configures:\n\n- **Task Definitions**\n  Task definitions are meticulously configured to ensure optimal performance and reliability of your services. Each service's container settings are fine-tuned based on its specific requirements, including memory allocation, CPU utilization, and networking parameters. Comprehensive health check configurations monitor the service's status, enabling quick detection and recovery from any issues. Environment variables are securely injected from AWS Secrets Manager at runtime, providing your services with the credentials and configuration they need while maintaining security. The task definitions are also integrated with AWS Service Discovery, enabling automatic service registration and allowing for seamless service-to-service communication within your application.\n\n- **Fargate Services**\n  Fargate services are configured with sophisticated deployment strategies that ensure zero downtime during updates. When deploying new versions of your services, Encore Cloud orchestrates a rolling update process where new tasks are gradually introduced while old ones are removed, maintaining consistent availability throughout the deployment.\n\n  Each service is automatically integrated with Application Load Balancer target groups, enabling intelligent request routing and load distribution. The load balancer continuously monitors the health of your service instances and automatically routes traffic only to healthy targets.\n\n  To ensure smooth service startup, appropriate health check grace periods are configured. This gives your services adequate time to initialize and warm up before receiving traffic, preventing premature health check failures during deployment or scaling events.\n\n- **IAM Configuration**\n  Encore Cloud implements a comprehensive IAM security model by creating unique execution roles for each task definition. These roles are automatically configured with precisely scoped permissions that enable secure access to required AWS services. The execution roles allow containers to pull images from ECR and write operational logs to CloudWatch for monitoring and debugging. They also grant access to assigned AWS resources like S3 buckets and SQS queues that the service needs to interact with. Additionally, the roles are configured to securely retrieve secrets from AWS Secrets Manager at runtime, enabling safe storage and access of sensitive configuration data. This granular permission model follows security best practices by providing each service with the minimum privileges required for operation.\n\n- **Network Integration**\nFargate tasks are strategically placed within private compute subnets, ensuring they remain isolated from direct internet access while maintaining the ability to communicate with other application components. The associated security groups are configured with precise rules that govern network traffic. These rules allow inbound traffic exclusively from the Application Load Balancer, ensuring that your services can only be accessed through the properly configured entry point. For outbound connectivity, the security groups permit traffic to flow to your databases and caching layers, enabling your services to interact with these essential backend resources while maintaining a secure network boundary.\n\n### Amazon EKS\n\nWhen using EKS, Encore Cloud configures:\n\n- **Cluster Setup**\n  Encore Cloud configures the core networking components required for cluster operation. The VPC CNI (Container Network Interface) is configured to enable pod networking within the cluster, allowing pods to communicate efficiently using the underlying AWS VPC networking capabilities. This includes setting up IP address management and network policy enforcement.\n\n  The cluster's internal DNS resolution is handled through CoreDNS, which is configured for optimal service discovery and name resolution within the cluster. CoreDNS settings are tuned to provide fast and reliable DNS lookups while maintaining reasonable cache sizes and query limits.\n\n\n- **Kubernetes Resources**\n  Encore Cloud automatically manages all necessary Kubernetes resources for your application. Each service in your application is deployed as a separate Kubernetes Deployment, allowing for independent scaling and lifecycle management. These deployments are configured with appropriate resource requests, limits, and health checks to ensure reliable operation.\n\n  For authentication and authorization, Encore Cloud implements IAM Roles for Service Accounts (IRSA), providing secure access to AWS services. Each service gets its own service account with precisely scoped IAM roles, following the principle of least privilege.\n\n  For sensitive data like API keys and credentials, Encore Cloud uses Kubernetes Secrets, which are encrypted at rest and only accessible to authorized services.\n\n  To enable network connectivity, Encore Cloud creates Kubernetes Service resources for each of your application's services, providing stable networking endpoints for inter-service communication. \n\n- **Load Balancer Integration**\n  Encore Cloud manages the complete load balancer integration for your EKS cluster. The AWS Load Balancer Controller is automatically installed and configured to handle ingress traffic for your application. This controller works in conjunction with the Application Load Balancer (ALB) to provide intelligent traffic routing and SSL/TLS termination.\n\n  The ALB Ingress Controller is configured to automatically create and manage Application Load Balancers based on your application's needs. It handles the creation and configuration of target groups, ensuring traffic is properly distributed across your service pods. The controller also manages the lifecycle of these resources, automatically cleaning up unused resources to prevent waste.\n\n  Target group binding is automatically configured to map your Kubernetes services to the appropriate ALB target groups. This ensures that traffic is correctly routed to the right pods and that health checks are properly configured to maintain high availability.\n\n  For secure communication, Encore Cloud automatically manages SSL/TLS certificates through AWS Certificate Manager. These certificates are automatically provisioned, renewed, and attached to your load balancers, ensuring all external traffic to your application is encrypted. The system also handles certificate rotation and updates transparently, maintaining secure communication without manual intervention.\n\n- **Monitoring Setup**\n  Encore Cloud automatically aggregates and sends metrics to your configured metrics destination, providing you with real-time visibility into your application's performance.\n\n  In addition to metrics, Encore Cloud configures the CloudWatch Logs agent to capture and forward all container logs. The logs are structured and organized by service name, making it easy to search and analyze application behavior. Log streams are automatically created for each container, and log retention policies are configured to help manage storage costs while maintaining necessary historical data.\n\n- **Service Accounts**\n  Encore Cloud implements a comprehensive service account management system that ensures secure and controlled access to resources. Each service in your application receives its own dedicated Kubernetes service account, providing a unique identity for authentication and authorization purposes.\n\n  To enable secure interaction with AWS services, Encore Cloud maps each Kubernetes service account to a corresponding IAM role using IAM Roles for Service Accounts (IRSA). This mapping allows pods to securely authenticate with AWS services without storing long-lived credentials.\n\n  The IAM roles are automatically configured with the minimum required permissions for each service's needs. This includes access to service-specific S3 buckets for object storage operations, permissions to publish and subscribe to SQS queues and SNS topics, ability to retrieve secrets from AWS Secrets Manager, and secure access to assigned database instances. These permissions are continuously updated as your application evolves, ensuring services always have the access they need while maintaining strong security boundaries.\n\nAll of these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs.\n\n## Managed Services\n\n### Databases\nEncore Cloud provisions [Amazon RDS][aws-rds] for PostgreSQL databases, providing a robust and scalable database solution. Each database runs the latest PostgreSQL version to ensure compatibility with modern features while maintaining up-to-date security patches. The databases are provisioned on auto-scaling capable instances, starting with db.m5.large configurations that can seamlessly scale up as your application's needs grow.\n\nTo protect your data, Encore Cloud configures automated daily backups with a 7-day retention period. Security is paramount, so databases are strategically placed within private subnets and protected by comprehensive access controls. This network isolation combined with strict security rules ensures your data remains secure while still being accessible to your application's services.\n\n#### Database Access\nDatabase access is managed through a comprehensive security model. At its core, Encore Cloud deploys [Emissary](https://github.com/encoredev/emissary), a secure socks proxy that enables safe database migrations while maintaining strict access controls. Each service in your application is assigned its own dedicated database role, providing granular control over data access and ensuring services can only interact with the data they need. For enhanced security, all databases are placed in private subnets, completely isolated from direct internet access. This multi-layered approach creates a secure foundation for your application's data access needs while maintaining operational flexibility.\n\n### Pub/Sub\nEncore Cloud implements a robust messaging system using [SQS][aws-sqs] and [SNS][aws-sns]. The system automatically configures dead-letter queues to capture failed messages, enabling thorough analysis and debugging of messaging issues. Each service in your application receives precisely scoped IAM permissions to publish and consume messages, ensuring secure communication between components. Encore Cloud fully manages the creation and configuration of subscriptions and topics, streamlining the setup and ongoing maintenance of your messaging infrastructure while maintaining optimal performance and reliability.\n\n### Object Storage\nEncore Cloud leverages [S3][aws-s3] for object storage, providing a comprehensive solution for your application's storage needs. When you declare storage requirements in your application, Encore Cloud automatically provisions dedicated S3 buckets with unique names to ensure global uniqueness across AWS. Each service in your application receives precisely scoped permissions to perform storage operations, following the principle of least privilege. For public buckets, Encore Cloud automatically integrates with CloudFront to create a global content delivery network, significantly improving access speeds for your users worldwide. Each bucket is assigned its own unique domain name, making it simple to manage and access stored content while maintaining a clear organizational structure.\n\n### Caching\nEncore Cloud uses [ElastiCache for Redis][aws-redis] to provide a high-performance caching solution. The service starts with cache.m6g.large instances that can automatically scale up as your application's needs grow. To ensure maximum reliability, caches are configured with Multi-AZ replication across availability zones, providing both high availability and fault tolerance. In the event of any failures, automatic failover capabilities ensure your application experiences no disruption in service.\n\nSecurity is maintained through Redis Access Control Lists (ACLs), which provide fine-grained control over who can access your cache and what operations they can perform. The entire system is configured for high availability, with monitoring and alerting in place to maintain optimal performance and uptime. This comprehensive setup ensures your application's caching layer remains fast, secure, and always available.\n\n### Cron Jobs\nEncore Cloud provides a streamlined approach to scheduled tasks that prioritizes security and simplicity. Each cron job is executed through authenticated API requests that are cryptographically signed to verify their authenticity. The system performs rigorous source verification to ensure all scheduled tasks originate exclusively from Encore Cloud's cron functionality, preventing unauthorized execution attempts. This elegant implementation requires no additional infrastructure components, making it both cost-effective and easy to maintain while ensuring your scheduled tasks run reliably and securely.\n\n[aws-vpc]: https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html\n[aws-fargate]: https://aws.amazon.com/fargate/\n[aws-eks]: https://aws.amazon.com/eks/\n[aws-secrets]: https://aws.amazon.com/secrets-manager/\n[aws-rds]: https://aws.amazon.com/rds/postgresql/\n[aws-sqs]: https://aws.amazon.com/sqs/\n[aws-sns]: https://aws.amazon.com/sns/\n[aws-s3]: https://aws.amazon.com/s3/\n[aws-redis]: https://aws.amazon.com/elasticache/redis/\n[aws-ecr]: https://aws.amazon.com/ecr/\n[aws-alb]: https://aws.amazon.com/elasticloadbalancing/application-load-balancer/\n"
  },
  {
    "path": "docs/platform/infrastructure/cloudflare.md",
    "content": "---\nseotitle: Cloudflare R2 Infrastructure on Encore Cloud\nseodesc: A comprehensive guide to how Encore Cloud provisions and manages Cloudflare R2 infrastructure for your applications\ntitle: Cloudflare R2 Buckets\nlang: platform\n---\n\nEncore Cloud simplifies the process of using Cloudflare R2 for object storage by automatically provisioning and managing the necessary infrastructure. This guide provides setup instructions and details on how Encore Cloud manages your Cloudflare R2 infrastructure.\n\n## Setup Process\n\n### 1. Cloudflare Account Connection\n\nTo connect your Cloudflare account to Encore Cloud:\n\n1. Create a Cloudflare API token using the **Create Additional Tokens** button in the Cloudflare dashboard \n\n2. Add the the following permissions:\n   - Zone > Zone: Read\n   - Zone > DNS: Edit\n   - Account > Workers R2 Storage: Edit\n\n3. Add the token in the Encore Cloud dashboard:\n   - Navigate to App Settings > Integrations > Cloudflare\n   - Click \"Connect Account\"\n   - Provide an account name and your API token\n\n### 2. Environment Configuration\n\nWhen creating a new environment:\n\n1. Select your preferred cloud provider\n2. Choose \"Cloudflare R2\" as the object storage provider\n3. Configure the following R2-specific settings:\n   - Token: Your Cloudflare API token\n   - Account: Your Cloudflare account\n   - Zone: The domain zone for public bucket URLs\n   - Region: Your preferred R2 storage region\n\n## Managed Features\n\n### Bucket Management\n\nEncore Cloud provides comprehensive bucket management capabilities that adapt to your application's needs. When you define storage requirements in your application, Encore Cloud automatically provisions the necessary R2 buckets with appropriate configurations. Each bucket is created with carefully configured policies and access controls to ensure secure yet efficient access to your stored objects. \n\n### Public Access Configuration\n\nWhen working with public buckets, Encore Cloud handles all aspects of public access configuration automatically. Each bucket is assigned a unique subdomain that is automatically provisioned and configured in your DNS settings. The bucket is seamlessly integrated with Cloudflare's global CDN network, ensuring fast content delivery worldwide. Encore Cloud also configures optimal caching rules to maximize performance while maintaining appropriate cache invalidation policies. This comprehensive setup ensures your public content is served efficiently and securely through Cloudflare's infrastructure.\n\n### Security Controls\n\nEncore Cloud implements a comprehensive multi-layered security model to protect your R2 storage. At the bucket level, fine-grained access controls ensure that only authorized services can perform specific operations on each bucket. Each service in your application receives its own unique set of credentials, preventing any unauthorized cross-service access. These credentials are securely distributed to the appropriate services through Encore Cloud's built-in secrets management system, which handles the entire credential lifecycle.\n\nAll these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs.\n\n[cloudflare-r2]: https://developers.cloudflare.com/r2/\n[cloudflare-cdn]: https://developers.cloudflare.com/cdn/"
  },
  {
    "path": "docs/platform/infrastructure/configuration.md",
    "content": "---\nseotitle: Infrastructure Configuration\nseodesc: Learn how you can configure infrastructure provisioned using Encore Cloud\ntitle: Infrastructure Configuration\nsubtitle: How to configure infrastructure when using Encore Cloud\nlang: platform\n---\n\nEncore Cloud provides a powerful and flexible approach to infrastructure management, ensuring that your cloud resources are efficiently provisioned, according to enterprise best practices.\n\nUnlike traditional Infrastructure-as-Code (IaC) tools, when using Encore's declarative infrastructure framework, you do not define any cloud service specifics in code. This ensures your code is cloud-agnostic and portable across clouds, and can be deployed using different infrastructure for each environment according to your priorities (cost, performance, etc.).\n\nInfrastructure configuration is made in the Encore Cloud dashboard, which provides a controlled workflow, role-based access controls, and auditable history of changes.\n\nEncore Cloud provisions and manages infrastructure by using your cloud provider's APIs. Learn more in the [Infrastructure](/docs/platform/infrastructure/infra) documentation.\n\n## Infrastructure settings when creating a new environment\n\nWhen creating a new environment, you can decide the following:\n\n- Which cloud provider to use (AWS or GCP)\n- Which compute hardware to use (e.g. AWS Fargate, GCP Cloud Run, Kubernetes)\n- If using Kubernetes, should a new cluster be created or should an existing cluster be used?\n- Which Kubernetes provider to use (GKE or EKS)\n- Which database to use (e.g. AWS RDS, GCP CloudSQL, Neon Serverless Postgres)\n- Which process allocation strategy to use (more on this below)\n\n## Ongoing infrastructure configuration\n\n### Configuration UI in Encore Cloud\n\nAfter creating an environment, you can continue to configure the infrastructure via the Encore Cloud dashboard.\n\nThe dashboard exposes the most common configuration options, and provides a controlled workflow for making changes, including audit logs and role-based access controls.\n\n<img src=\"/assets/docs/infra_config.png\" title=\"Infra configuration UI\"/>\n\n#### Process allocation configuration\n\nEncore provides a powerful configuration option called process allocation. This enables you to configure how microservices should be deployed on the compute hardware; either deploying all services in one process or one process per service. All without any code changes.\n\nIt's often recommended to deploy all services in one process in order to reduce costs and minimize response times between services. (But it depends on your use case.)\nDeploying each service as its own process will improve scalability and decrease blast radius if things go wrong. This is only recommended for production environments.\n\n<img src=\"/assets/docs/microservices-process-allocation.png\" title=\"Process allocation config\"/>\n\n### Manual configuration in your cloud provider's console\n\nManual configuration is relevant in cases where some configuration options are not yet available in the Encore Cloud dashboard, or you may want to make changes manually. Handily, you have full access to make changes directly in your cloud provider's console.\n\nEncore Cloud tries very hard to ensure that any manual changes made in the cloud provider's console are not overwritten.\n\nTherefore it only makes the minimum necessary modifications to infrastructure when deploying new changes, using the following strategies:\n\n- **PATCH-style updates:** Resources are updated using compare-and-set and similar techniques, modifying only the attributes that require changes.\n\n- **Avoid full syncs:** Unlike Terraform, Encore Cloud updates only the specific resources necessary to accomplish an infrastructure change rather than performing a complete infrastructure refresh.\n\nThese behaviors ensure an efficient and predictable workflow, minimizing unintended changes and reducing deployment times, and means that you can safely use your cloud provider's console to modify the provisioned resources.\n\nThis behavior also makes Encore Cloud well-suited for environments where infrastructure is partially managed outside of Encore Cloud, enabling you to deploy Encore applications alongside existing infrastructure (more on this below).\n\n## Working with Existing Infrastructure\n\nOne of Encore Cloud’s strengths is its ability to work seamlessly with existing infrastructure. Since it does not enforce a full sync approach, it can:\n\n- Integrate with pre-existing cloud resources without overwriting manual changes\n\n- Deploy to existing Kubernetes clusters\n\n- Co-exist with other IaC tools like Terraform and CloudFormation.\n\nEncore Cloud also provides a Terraform Provider to simplify integration with existing Terraform-managed infrastructure. Learn more in the [Terraform Provider](/docs/platform/integrations/terraform) documentation.\n"
  },
  {
    "path": "docs/platform/infrastructure/configure-kubectl.md",
    "content": "---\nseotitle: Configure kubectl to access your Encore Kubernetes cluster\nseodesc: Learn how to configure kubectl to access your Encore Kubernetes cluster.\ntitle: Configure kubectl\nlang: platform\n---\n\nEncore Cloud automatically provisions and manages Kubernetes clusters for you, but sometimes it's useful to manually inspect\nclusters using the [kubectl](https://kubernetes.io/docs/reference/kubectl/) cli. To do this, you need to configure `kubectl` to connect and authenticate through\nencore. You can do this by running the following command in your app directory:\n\n```shell\nencore kubernetes configure -e <environment>\n```\n\nWhere `<environment>` is the name of the environment you want to configure `kubectl` for.\n\nThis will configure `kubectl` to use `encore` to authenticate the cluster and proxy your traffic to the correct \ncluster. You can now use `kubectl` as you normally would, for example:\n\n```shell\nkubectl get pods\n```\n"
  },
  {
    "path": "docs/platform/infrastructure/configure-network.md",
    "content": "---\nseotitle: How to configure custom network settings for your Encore environment\nseodesc: Learn how to configure IP ranges when connecting your Encore application to existing networks.\ntitle: Configure network settings\nsubtitle: Customizing IP ranges for network peering\nlang: platform\n---\n\n# Overview\n\nWhen deploying applications with Encore Cloud, a network is automatically provisioned with default settings. However, if you plan to peer your Encore network with an existing network, you can manually configure the IP range for your environment.\n\n## Benefits\n\nConfiguring custom network settings allows you to:\n- Connect your Encore application to existing networks via peering\n- Prevent IP range conflicts with other networks in your organization\n- Plan your network topology with predictable addressing\n\n## Configuring network settings\n\nFollow these steps to configure custom network settings:\n\n1. Navigate to **Create Environment** in the Encore Cloud dashboard\n2. Select the AWS or GCP cloud provider\n3. Expand the **Network** section\n4. Enter your desired IP range\n   - The range must be at least a /16 block to reserve enough IPs for your application to grow\n   - Choose a range that doesn't conflict with your existing networks\n\nOnce configured, Encore will use your specified IP range instead of assigning a random private network.\n\n## Default network behavior\n\nBy default, Encore will reserve a randomly assigned /16 block in one of the private IP ranges. This is suitable for most deployments that don't require network peering.\n"
  },
  {
    "path": "docs/platform/infrastructure/gcp.md",
    "content": "---\nseotitle: GCP Infrastructure on Encore Cloud\nseodesc: A comprehensive guide to how Encore Cloud provisions and manages GCP infrastructure for your applications\ntitle: GCP Infrastructure\nsubtitle: Understanding your application's GCP infrastructure\nlang: platform\n---\n\nEncore Cloud simplifies the process of deploying applications by automatically provisioning and managing the necessary GCP infrastructure. This page provides an overview of the components involved and how they work together to support your applications.\n\n_Example of Encore project deployment alongside existing legacy systems on GCP:_\n\n<img src=\"/assets/docs/gcp-diagram.png\" title=\"Migration options\" className=\"noshadow\"/>\n\n## Core Infrastructure Components\n\n### Networking Architecture\n\nTo ensure maximum security and isolation, Encore Cloud provisions a dedicated GCP Project for each environment. This project isolation prevents any potential cross-environment access and enables granular control over resources and permissions. Within each project, all resources are deployed into a private network configuration, where they can only communicate with other resources inside the VPC. This private networking approach significantly reduces the attack surface by preventing direct access from the public internet, with traffic only flowing through designated ingress points.\n\n### Container Management\n\nEncore Cloud provisions a [Google Container Registry (GCR)][gcp-gcr] to store your application's Docker images.\n\nThe registry implements comprehensive access controls to ensure only authorized users and services can access and manage container images. Through integration with GCP's Identity and Access Management (IAM), each service is granted the minimum required permissions needed to pull its container images.\n\nAdditionally, GCR performs automated vulnerability scanning on all container images. As new images are pushed to the registry, they are automatically analyzed for known security vulnerabilities in the operating system and application dependencies. This proactive scanning helps identify potential security issues early in the deployment pipeline, allowing you to maintain a secure application environment.\n\n### Secrets Management\n\nEncore Cloud's integration with Secret Manager provides comprehensive security and seamless access to sensitive data. All secrets are automatically injected as environment variables into your services, eliminating the need for manual configuration while maintaining security. The secrets are protected using industry-standard encryption both when stored and during transmission between services. To ensure maximum security, Secret Manager implements strict access controls - each service can only access the specific secrets it needs, and all access attempts are logged and audited.\n\n## Compute Options\n\nEncore Cloud provisions one of two compute platforms for running your application containers, based on your choice:\n\n### Google Cloud Run\n\nWhen using Cloud Run, Encore Cloud configures:\n\n**Service Deployments**\nEach service is configured with optimized container settings and health check configurations to ensure reliable operation. Environment variables are automatically injected from Secret Manager to securely provide configuration values. Service discovery integration enables seamless communication between services.\n\n**Cloud Run Services**\nCloud Run services are configured with zero-downtime deployment strategies, ensuring your application remains available during updates. Each service is integrated with a load balancer to distribute traffic efficiently across instances. Health check grace periods are configured to allow containers adequate time to start up before receiving traffic, preventing premature termination of healthy instances.\n\n**IAM Configuration**\nEach deployment receives its own dedicated service account to ensure proper isolation and security. These service accounts are automatically configured with the minimum required permissions needed for operation. This includes access to pull container images from Google Container Registry, write application logs to Cloud Logging, and interact with assigned GCP resources like Cloud Storage buckets and Pub/Sub topics. The service accounts are also granted permission to read secrets from Secret Manager, enabling secure access to sensitive configuration values. This automated permission management ensures your services have exactly the access they need while following security best practices.\n\n### Google Kubernetes Engine\n\nWhen using GKE, Encore Cloud configures:\n\n- **Cluster Setup**\n  Encore Cloud provisions either GKE Autopilot clusters or standard GKE clusters with managed node pools, both configured to run in private subnets for enhanced security. With Autopilot, GKE automatically manages the underlying infrastructure, while with standard clusters Encore Cloud configures and maintains optimized node pools based on your workload requirements. In both cases, the nodes are placed in private subnets to ensure they're not directly accessible from the internet, with all traffic flowing through the load balancer.\n\n- **Kubernetes Resources**\n  Encore Cloud automatically creates and manages all necessary Kubernetes resources for your application. Each Encore service is deployed as a Kubernetes Deployment, ensuring reliable operation and scaling capabilities. These deployments are backed by service accounts configured with appropriate IAM roles to access GCP resources securely. Sensitive configuration data is stored as Kubernetes Secrets and automatically mounted into the appropriate pods. To enable network connectivity, Encore Cloud provisions Kubernetes Service and Ingress resources that integrate with the Google Cloud Load Balancer, providing secure external access to your application endpoints.\n\n- **Load Balancer Integration**\n  Encore Cloud integrates with Google Cloud Load Balancer to provide secure and reliable access to your applications. The load balancer is configured to distribute traffic across your services while handling SSL/TLS termination. All traffic is automatically encrypted using managed SSL/TLS certificates that are provisioned and renewed automatically. This ensures your application endpoints remain secure and accessible through HTTPS without requiring manual certificate management.\n\n- **Monitoring Setup**\n  Encore Cloud sets up comprehensive monitoring for your GKE clusters by configuring both metrics collection and log management. Container metrics are automatically collected from each pod and exported to your configured monitoring service, providing detailed insights into resource usage, performance, and application behavior. Additionally, all container logs are seamlessly forwarded to Cloud Logging, enabling centralized log aggregation and analysis. This integrated monitoring approach gives you full visibility into your application's health and performance within the Google Cloud ecosystem.\n\n- **Service Accounts**\n  Encore Cloud implements a comprehensive service account management system that ensures secure and controlled access to GCP resources. Each service in your application receives its own dedicated service account, providing fine-grained access control and isolation between services.\n\n  These service accounts are automatically configured with IAM roles that map precisely to the GCP services your application needs to interact with. The permission configuration is handled dynamically based on your application's declared resource usage. For example, if your service needs to access a GCS bucket, Encore Cloud automatically grants the minimum required permissions for those specific storage operations. Similarly, when your service needs to publish or subscribe to Pub/Sub topics, connect to databases, or retrieve secrets, the appropriate IAM roles are configured automatically.\n\n  This automated permission management ensures that each service operates under the principle of least privilege, having access only to the resources it explicitly needs to function. This significantly enhances your application's security posture by minimizing the potential impact of any security breach.\n\nAll of these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs.\n\n## Managed Services\n\n### Databases\n\nEncore Cloud provisions [GCP Cloud SQL][gcp-cloudsql] for PostgreSQL databases, providing a robust and scalable database solution:\n\nEncore Cloud provisions Cloud SQL instances running the latest PostgreSQL version, ensuring you have access to the newest features and security updates. Each instance starts with the smallest available configuration to optimize costs, while maintaining the ability to automatically scale up resources as your application's needs grow.\n\nData protection is a key priority, with automated daily backups retained for 7 days and point-in-time recovery capabilities. This allows you to restore your database to any moment within the retention period if needed.\n\nSecurity is enforced through strategic placement of databases in private subnets, isolating them from direct internet access. Strict access controls ensure that only authorized services and users can connect to the database instances.\n\n### Pub/Sub\n\nEncore Cloud implements a robust messaging system using [GCP Pub/Sub][gcp-pubsub]. The system is designed with reliability and security in mind, automatically configuring dead-letter topics to capture and preserve any failed messages for later analysis and debugging. Each service in your application receives precisely scoped IAM permissions for publishing and consuming messages, ensuring secure communication between components while maintaining the principle of least privilege. Encore Cloud fully manages all subscriptions and topics, handling the complex setup and ongoing maintenance of your messaging infrastructure, allowing you to focus on your application logic rather than infrastructure management.\n\n### Object Storage\n\nEncore Cloud leverages [Google Cloud Storage][gcp-gcs] for object storage needs. When you declare storage buckets in your application, Encore Cloud automatically provisions them with unique names in GCP. Each service that interacts with storage is configured with precisely scoped permissions, ensuring secure access to only the buckets and operations it requires. For public buckets, Encore Cloud integrates with Cloud CDN to optimize content delivery, with each bucket accessible through a unique URL. This comprehensive setup provides secure, efficient, and easily manageable object storage capabilities for your application.\n\n### Caching\n\nEncore Cloud uses [GCP Memorystore for Redis][gcp-redis] to provide a high-performance caching solution. Each Redis instance starts with the smallest available configuration to optimize costs while maintaining the ability to automatically scale up resources as your application's caching needs grow. The instances are configured in a high-availability setup to ensure your cache remains available and performant even during infrastructure updates or zone outages. Access to the cache is secured through Redis authentication, with credentials automatically managed and rotated by Encore Cloud to maintain a strong security posture.\n\n### Cron Jobs\n\nEncore Cloud provides a streamlined approach to scheduled task execution that prioritizes both simplicity and security. Each cron job is executed through authenticated API requests that are cryptographically signed, ensuring that only legitimate, verified requests can trigger your scheduled tasks. The system includes robust source verification that validates all requests originate from Encore Cloud's trusted cron infrastructure. This elegant implementation requires no additional infrastructure components, making it both cost-effective and easy to maintain while providing the reliability and security needed for production workloads.\n\n[gcp-vpc]: https://cloud.google.com/vpc\n[gcp-cloudrun]: https://cloud.google.com/run\n[gcp-gke]: https://cloud.google.com/kubernetes-engine\n[gcp-secrets]: https://cloud.google.com/secret-manager\n[gcp-pubsub]: https://cloud.google.com/pubsub\n[gcp-gcs]: https://cloud.google.com/storage\n[gcp-cloudsql]: https://cloud.google.com/sql\n[gcp-redis]: https://cloud.google.com/memorystore\n[gcp-gcr]: https://cloud.google.com/container-registry\n"
  },
  {
    "path": "docs/platform/infrastructure/import-cloud-sql.md",
    "content": "---\nseotitle: How to deploy your Encore application with an existing Cloud SQL instance\nseodesc: Learn how to easily import your existing Cloud SQL instance and connect your Encore application to it.\ntitle: Import an existing Cloud SQL instance\nsubtitle: Using your pre-existing database instead of provisioning a new one\nlang: platform\n---\n\n# Overview\n\nWhen deploying applications to your own cloud, Encore Cloud can provision all necessary infrastructure—including database instances. However, if you already have a Cloud SQL instance, you can connect your Encore application directly to this existing database.\n\n## Benefits\n\nUsing an existing Cloud SQL instance allows you to:\n- Maintain data continuity with your existing systems\n- Preserve specific database configurations\n- Utilize familiar database setups without migration\n\n## Importing a Cloud SQL instance\n\nFollow these steps to import your existing Cloud SQL instance:\n\n1. Navigate to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud)\n2. Select the GCP cloud provider\n3. Choose **Import Existing Cloud SQL Instance**\n4. Add permissions for the Encore Service Account:\n   - Copy the `Encore GCP Service Account` from the cloud dashboard\n   - Go to your project's IAM page in the GCP Console\n   - Grant the `Owner` role to the `Encore GCP Service Account`\n5. Return to the Encore Cloud dashboard\n6. Specify your database's `GCP Project ID` and `Cloud SQL Instance Name`\n7. Click the `Resolve` button to validate the instance\n\nOnce validated, you can create the environment. When you deploy to this environment, Encore Cloud will automatically connect your application to your imported Cloud SQL instance rather than provisioning a new database.\n\n## Mapping existing databases to your Encore app\nTo access an existing database in your Encore application, you need to specify the name of the existing database when you declare the database in your app. For example, if you have an existing database called `mydb` you can create a reference to it like so:\n\n```typescript\nconst db = new SQLDatabase(\"mydb\");\n```\n\n```go\nsqldb.NewDatabase(\"mydb\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n```\n\n## Applying migrations to existing databases\nEncore uses a table called `schema_migrations` in the public namespace to keep track of which migrations have been applied. If you import an existing database without that table, Encore will create it for you and apply your migrations in order. If the table already exists, Encore expects it to contain exactly two columns:\n\n```\nversion bigint\ndirty boolean\n```\n\nIf the table exists but has a different schema, you will not be able to import it with Encore at this time. If the table exists with an existing entry, Encore will apply all higher versions in your `migrations` directory to the database.\n"
  },
  {
    "path": "docs/platform/infrastructure/import-kubernetes-cluster.md",
    "content": "---\nseotitle: How to deploy your Encore application to an existing Kubernetes cluster\nseodesc: Learn how to easily import your existing Kubernetes cluster and deploy your Encore application into it.\ntitle: Import an existing Kubernetes cluster\nsubtitle: Deploying to your pre-existing cluster instead of provisioning a new one\nlang: platform\n---\n\nWhen you deploy your application to your own cloud, Encore Cloud can provision infrastructure for it in many different ways – including setting up a Kubernetes cluster.\n\nIf you already have a Kubernetes cluster, Encore Cloud can deploy your Encore application into this pre-existing cluster. This is often useful if you want to integrate your Encore application with other parts of your system that are not built using Encore.\n\nKubernetes imports are supported on GCP, AWS support is coming soon.\n\n## Importing a cluster\n\nTo import your cluster, go to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud), select **Kubernetes: Existing GKE Cluster** as the compute platform, and then specify your cluster's `Project ID`, `Region`, and `Cluster Name`.\n\nWhen you deploy to this environment, Encore Cloud will use your imported cluster as the compute instance.\n\n<img src=\"/assets/docs/import-k8s.png\" title=\"Import your existing Kubernetes cluster\" className=\"mx-auto\"/>\n"
  },
  {
    "path": "docs/platform/infrastructure/import-project.md",
    "content": "---\nseotitle: How to deploy your Encore application to an existing GCP project\nseodesc: Learn how to easily import your existing GCP project and connect your Encore application to it.\ntitle: Import an existing GCP project\nsubtitle: Using your pre-existing GCP project instead of provisioning a new one\nlang: platform\n---\n\n# Overview\n\nWhen deploying applications to your own cloud, Encore Cloud can provision all necessary infrastructure—including new GCP projects. However, if you already have a GCP project, you can deploy your Encore application directly to this existing project.\n\n## Benefits\n\nUsing an existing GCP project allows you to:\n- Keep all your infrastructure in a single project\n- Maintain existing IAM policies and permissions\n- Utilize existing billing settings and quotas\n- Consolidate resources for easier management\n\n## Importing a GCP project\n\nFollow these steps to import your existing GCP project:\n\n1. Navigate to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud)\n2. Select the GCP cloud provider\n3. Choose **Import Project**\n4. Add permissions for the Encore Service Account:\n   - Copy the `Encore GCP Service Account` from the cloud dashboard\n   - Go to your project's IAM page in the GCP Console\n   - Grant the `Owner` role to the `Encore GCP Service Account`\n5. Return to the Encore Cloud dashboard\n6. Enter your `Project ID` \n7. Click the `Resolve` button to validate the project\n\nOnce validated, you can create the environment. When you deploy to this environment, Encore Cloud will automatically deploy your application to your imported GCP project rather than provisioning a new one. "
  },
  {
    "path": "docs/platform/infrastructure/import-rds.md",
    "content": "---\nseotitle: How to deploy your Encore application with an existing AWS RDS instance\nseodesc: Learn how to easily import your existing AWS RDS instance and connect your Encore application to it.\ntitle: Import an existing AWS RDS instance\nsubtitle: Using your pre-existing database instead of provisioning a new one\nlang: platform\n---\n\n# Overview\n\nWhen deploying applications to your own cloud, Encore Cloud can provision all necessary infrastructure—including database instances. However, if you already have an AWS RDS instance, you can connect your Encore application directly to this existing database.\n\n## Benefits\n\nUsing an existing AWS RDS instance allows you to:\n- Maintain data continuity with your existing systems\n- Preserve specific database configurations\n- Utilize familiar database setups without migration\n\n## Importing an AWS RDS instance\n\nFollow these steps to import your existing AWS RDS instance:\n\n1. Navigate to **Create Environment** in the [Encore Cloud dashboard](https://app.encore.cloud)\n2. Select the AWS cloud provider\n3. Pick the `AWS Region` your database resides in\n3. Choose **Import Existing RDS Instance**\n4. Specify your database's `RDS Instance Name`\n5. Click the `Resolve` button to validate the instance\n\nOnce validated, you can create the environment. When you deploy to this environment, Encore Cloud will automatically connect your application to your imported AWS RDS instance rather than provisioning a new database.\n\n## Mapping existing databases to your Encore app\nTo access an existing database in your Encore application, you need to specify the name of the existing database when you declare the database in your app. For example, if you have an existing database called `mydb` you can create a reference to it like so:\n\n```typescript\nconst db = new SQLDatabase(\"mydb\");\n```\n\n```go\nsqldb.NewDatabase(\"mydb\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n```\n\n## Applying migrations to existing databases\nEncore uses a table called `schema_migrations` in the public namespace to keep track of which migrations have been applied. If you import an existing database without that table, Encore will create it for you and apply your migrations in order. If the table already exists, Encore expects it to contain exactly two columns:\n\n```\nversion bigint\ndirty boolean\n```\n\nIf the table exists but has a different schema, you will not be able to import it with Encore at this time. If the table exists with an existing entry, Encore will apply all higher versions in your `migrations` directory to the database.\n"
  },
  {
    "path": "docs/platform/infrastructure/infra.md",
    "content": "---\nseotitle: Cloud Infrastructure Provisioning\nseodesc: Learn how to provision appropriate cloud infrastructure depending on the environment type for AWS and GCP.\ntitle: Infrastructure provisioning & Environments\nsubtitle: How Encore Cloud provisions infrastructure for your application\nlang: platform\n---\n\nEncore Cloud automatically provisions all necessary infrastructure, in all environments and across all major cloud providers, without requiring application code changes. You simply [connect your cloud account](/docs/platform/deploy/own-cloud) and create an environment.\n\n<img src=\"/assets/docs/encore_overview.png\" title=\"Infrastructure Overview\" className=\"noshadow\"/>\n\n## How it works\n\nThis is powered by Encore's open source [backend framework](/docs/ts), which lets you declare infrastructure resources (databases, caches, queues, scheduled jobs, etc.) as type-safe objects in application code.\n\nAt compile time, Encore parses the application code to generate an [Application Model](/docs/ts/concepts/application-model), and Encore Cloud uses this meta data to create an infrastructure graph with a high-resolution definition of the infrastructure your application requires.\n\nEncore Cloud then uses this graph to provision and manage the necessary infrastructure in your cloud account (using AWS and GCP APIs), and in development and preview environments hosted by Encore Cloud.\n\n<img src=\"/assets/docs/howitworks.png\" title=\"Infrastructure Provisioning\"/>\n\n\nThe approach removes the need for infrastructure configuration files and avoids creating cloud-specific dependencies in your application.\n\nHaving an end-to-end integration between application code and infrastructure also enables Encore Cloud to keep environments in sync and track cloud infrastructure, giving you an up-to-date view of your infrastructure to avoid unnecessary cloud costs.\n\n<img src=\"/assets/docs/infra_config_new.png\" title=\"Infrastructure Tracking\"/>\n\n## Environment types\n\nBy default, Encore Cloud provisions infrastructure using contextually appropriate objectives for each environment type. You retain control over the infrastructure in your cloud account, and can configure it directly both via the Encore Cloud dashboard and your cloud provider's console. Encore Cloud takes care of syncing your changes.\n\n|                        | Local              | Encore Cloud Hosting       | GCP / AWS                          |\n| ---------------------- | ------------------ | -------------------------- | ---------------------------------- |\n| **Environment types:** | Development        | Preview, Development       | Development, Production            |\n| **Objectives:**        | Provisioning speed | Provisioning speed, Cost\\* | Reliability, Security, Scalability |\n\n\\*Encore Cloud Hosting is free to use, subject to Fair Use guidelines and usage limits. [Learn more](/docs/platform/management/usage)\n\n## Development Infrastructure\n\nEncore Cloud provisions infrastructure resources differently for each type of development environment.\n\n|                     | Local                             | Preview / Development (Encore Cloud Hosting)                 | GCP / AWS                                                      |\n| ------------------- | --------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------- |\n| **SQL Databases:**  | Docker                            | Encore Cloud Managed (Kubernetes), [Neon](/docs/deploy/neon) | [See production](/docs/deploy/infra#production-infrastructure) |\n| **Pub/Sub:**        | In-memory ([NSQ](https://nsq.io)) | GCP Pub/Sub                                                  | [See production](/docs/deploy/infra#production-infrastructure) |\n| **Caches:**         | In-memory (Redis)                 | In-memory (Redis)                                            | [See production](/docs/deploy/infra#production-infrastructure) |\n| **Cron Jobs:**      | Disabled                          | [Encore Cloud Managed](/docs/primitives/cron-jobs)           | [See production](/docs/deploy/infra#production-infrastructure) |\n| **Object Storage:** | Local Disk                        | Encore Cloud Managed                                         | [See production](/docs/deploy/infra#production-infrastructure) |\n\n\n### Local Development\n\nFor local development Encore Cloud provisions a combination of Docker and in-memory infrastructure components.\nSQL Databases are provisioned using [Docker](https://docker.com). For Pub/Sub\nand Caching the infrastructure is run in-memory.\n\nWhen running tests, a separate SQL Database cluster is provisioned that is optimized for high performance\n(using an in-memory filesystem and fsync disabled) at the expense of reduced reliability.\n\nTo avoid surprises during development, Cron Jobs are not triggered in local environments.\nThey can always be triggered manually by calling the API directly from the [development dashboard](/docs/ts/observability/dev-dash).\n\nThe application code itself is compiled and run natively on your machine (without Docker).\n\n### Preview Environments\n\nWhen you've [connected your application to GitHub](/docs/platform/integrations/github), Encore Cloud automatically provisions a temporary [Preview Environment](/docs/platform/deploy/preview-environments) for each Pull Request.\n\nPreview Environments are created in Encore Cloud Hosting, and are optimized for provisioning speed and cost-effectiveness.\nThe Preview Environment is automatically destroyed when the Pull Request is merged or closed.\n\nPreview Environments are named after the pull request, so PR #72 will create an environment named `pr:72`.\n\n### Encore Cloud Hosting\n\nEncore Cloud Hosting is a simple, zero-configuration hosting solution provided by Encore.\nIt's perfect for development environments and small-scale use that do not require any specific SLAs.\nIt's also a great way to evaluate Encore Cloud without needing to connect your cloud account.\n\nEncore Cloud Hosting is not designed for business-critical use and does not offer reliability guarantees for persistent storage\nlike SQL Databases. Other infrastructure primitives like Pub/Sub and Caching\nare provisioned with small-scale use in mind.\n\n[Learn more about the usage limitations](/docs/platform/management/usage)\n\n## Production Infrastructure\n\nEncore Cloud provisions production infrastructure resources using best-practice guidelines and services for each respective cloud provider.\n\n|                     | GCP                                                                                                                                | AWS                                                                                                            |\n| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |\n| **Networking:**     | [VPC](/docs/platform/infrastructure/gcp#networking-architecture)                                                                   | [VPC](/docs/platform/infrastructure/aws#networking-architecture)                                               |\n| **Compute:**        | [Cloud Run](/docs/platform/infrastructure/gcp#google-cloud-run), [GKE](/docs/platform/infrastructure/gcp#google-kubernetes-engine) | [Fargate ECS](/docs/platform/infrastructure/aws#aws-fargate), [EKS](/docs/platform/infrastructure/aws#aws-eks) |\n| **SQL Databases:**  | [GCP Cloud SQL](/docs/platform/infrastructure/gcp#databases), [Neon](/docs/platform/infrastructure/neon)                           | [Amazon RDS](/docs/platform/infrastructure/aws#databases), [Neon](/docs/platform/infrastructure/neon)          |\n| **Pub/Sub:**        | [GCP Pub/Sub](/docs/platform/infrastructure/gcp#pubsub)                                                                            | [Amazon SQS & Amazon SNS](/docs/platform/infrastructure/aws#pubsub)                                            |\n| **Object Storage:** | [GCS/Cloud CDN](/docs/platform/infrastructure/gcp#object-storage)                                                                  | [Amazon S3/CloudFront](/docs/platform/infrastructure/aws#object-storage)                                       |\n| **Caches:**         | [GCP Memorystore (Redis)](/docs/platform/infrastructure/gcp#caching)                                                               | [Amazon ElastiCache (Redis)](/docs/platform/infrastructure/aws#caching)                                        |\n| **Cron Jobs:**      | Encore Cloud Managed                                                                                                               | Encore Cloud Managed                                                                                           | Encore Cloud Managed |\n| **Secrets:**        | [Secret Manager](/docs/platform/infrastructure/gcp#secrets-management)                                                             | [AWS Secrets Manager](/docs/platform/infrastructure/aws#se)                                                    |\n\n### Configuration\n\nWith Encore you do not define any cloud service specifics in the application code. This means that after deploying, you can safely use your cloud provider's console to modify the provisioned resources, or use the built-in configuration UI in the Encore Cloud dashboard.\n\nLearn more in the [Infrastructure Configuration](/docs/platform/infrastructure/configuration) documentation."
  },
  {
    "path": "docs/platform/infrastructure/kubernetes.md",
    "content": "---\nseotitle: How to deploy your Encore application to a new Kubernetes cluster\nseodesc: Learn how to automatically deploy your Encore application to a new Kubernetes cluster.\ntitle: Kubernetes deployment\nsubtitle: Deploying your app to a new Kubernetes cluster\nlang: platform\n---\n\n# Deploying Encore Apps to Kubernetes\n\nEncore Cloud gives you flexibility in where you run your applications. You have two options for Kubernetes deployments:\n\n1. **Deploy to a new cluster**: Encore Cloud can automatically provision and manage a new Kubernetes cluster in your cloud account on AWS or GCP.\n2. **Use an existing cluster**: Deploy to your pre-existing Kubernetes cluster ([see instructions here](/docs/platform/infrastructure/import-kubernetes-cluster))\n\nAll infrastructure provisioning is automated, and configuration is managed through the [Encore Cloud Dashboard](https://app.encore.cloud), keeping your application code clean and infrastructure-agnostic.\n\n## Deploying to a new Kubernetes cluster\n\n**1. Connect your cloud account:** Ensure your cloud account (Google Cloud Platform or AWS) is connected to Encore Cloud. ([See docs](/docs/platform/deploy/own-cloud))\n\n**2. Create environment:** Open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to **Environments**, then click on **Create Environment**.\n\nNext, select your cloud (AWS or GCP) and then specify Kubernetes as the compute platform. Encore Cloud supports deploying to GKE on GCP, and EKS Fargate on AWS.\n\nYou can also configure if you want to allocate all services in one process or run one process per service.\n\n<img src=\"/assets/docs/k8s-config.jpg\" title=\"Environment Settings\" className=\"mx-auto\"/>\n\n**3. Push your code:** To deploy, commit and push your code to the branch you configured as the deployment trigger. You can also trigger a manual deploy from the Cloud Dashboard by going to the **Environment Overview** page and clicking on **Deploy**.\n\n**4. Automatic deployment by Encore Cloud:** Once you've triggered the deploy, Encore Cloud will automatically provision and deploy the necessary infrastructure on Kubernetes, per your environment configuration in the Cloud Dashboard. You can monitor the status of your deploy and view your environment's details through the Encore Cloud Dashboard.\n\n**5. Accessing your cluster with kubectl:** You can access your cluster using the `kubectl` CLI tool. [See the docs](/docs/platform/infrastructure/configure-kubectl) for how to do this.\n\n## Infrastructure Overview\n\nEncore Cloud simplifies the process of deploying applications by automatically provisioning and managing the necessary Kubernetes components. Here's an overview of the components Encore Cloud manages and how they work together to support your applications.\n\n### Namespace Management\n\nEncore Cloud creates a unique namespace for each environment deployed to your Kubernetes cluster, ensuring complete isolation between different environments of your application.\n\n### Secrets Management\n\nEncore Cloud provides comprehensive secrets management through deep integration with Kubernetes Secrets. Application secrets that you configure in Encore Cloud are automatically stored as Kubernetes Secrets and made available to your services at runtime. This includes both application-specific secrets that you define, as well as infrastructure secrets like database credentials that Encore Cloud manages automatically.\n\nService accounts are automatically bound to the appropriate secrets they need access to, ensuring each service can only access the secrets it requires. This follows the principle of least privilege and helps maintain a strong security posture.\n\n### Ingress Configuration\n\nEncore Cloud provisions and manages ingress for your applications through a cloud provider-specific ingress controller. The ingress controller is automatically configured to handle incoming traffic and route it securely to your application's Encore Gateway service. It manages TLS certificates automatically to ensure all traffic is encrypted, and provides fine-grained control over which services are accessible from the public internet. The controller configuration is optimized for your specific cloud provider to ensure the best possible performance and reliability.\n\n## Service Management\n\n### Deployments\nEncore Cloud manages the deployment configuration for each service in your application. Each service is deployed as a separate Kubernetes deployment, allowing for independent scaling and management. The deployment configurations are automatically generated and optimized based on your service's requirements.\n\nFor each service, Encore Cloud configures the pod specifications with appropriate resource requests and limits, health checks, and container settings. Runtime configurations like environment variables and command arguments are automatically set based on your application's needs. The container orchestration is handled seamlessly, with Encore Cloud managing pod scheduling, updates, and scaling to ensure your services run reliably and efficiently.\n\n### Network Configuration\n\nEncore Cloud provides a comprehensive networking setup through Kubernetes Service resources. Each service in your application gets assigned a unique cluster IP address, enabling reliable internal communication between services. This IP allocation works in conjunction with Kubernetes' built-in service discovery mechanism, allowing services to locate and communicate with each other using consistent internal DNS names. The internal service routing ensures that requests are efficiently distributed across all available pods for each service, providing automatic load balancing and failover capabilities.\n\n### Identity and Access\n\nEncore Cloud provides comprehensive service identity management through Kubernetes service accounts. Each pod is assigned its own dedicated service account, which handles authentication with the Kubernetes API and enables secure access to resources. These service accounts are automatically bound to the specific secrets and permissions required by each service.\n\nFor cloud provider integration, Encore Cloud maps the service accounts to appropriate IAM roles, enabling secure access to cloud resources like databases and object storage. Following the principle of least privilege, Encore Cloud configures the minimum required permissions for each service account, ensuring services can only access the resources they explicitly need.\n\nAll these configurations are automatically maintained and updated by Encore Cloud as you develop your application, ensuring your infrastructure stays aligned with your application's needs."
  },
  {
    "path": "docs/platform/infrastructure/manage-db-users.md",
    "content": "---\nseotitle: Managing database user credentials\nseodesc: Learn how to manage user credentials for databases created by Encore.\ntitle: Managing database user credentials\nlang: platform\n---\n\nEncore Cloud provisions your databases automatically, meaning you don't need to manually create database users. However, in some use cases you need access to the database user credentials, so Encore Cloud makes it simple to view them.\n\nAs an application **Admin**, open the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Infrastructure** page for the relevant environment.\n\nIn the section for the relevant **Database Cluster**, you will find a **Users** sub-section which lists your database users. Click on the \"eye\" icon next to each username to decrypt the password.\n\nNote that databases hosted in [Encore Cloud](/docs/platform/infrastructure/infra#encore-cloud) currently do not expose usernames and passwords.\nTo connect to an Encore Cloud-hosted database, use [`encore db shell`](/docs/ts/primitives/databases#connecting-to-databases).\n\n`encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with.\n\n<img src=\"/assets/docs/db-user.png\" title=\"View Database User Credentials\"/>\n\n<Callout type=\"important\">\n\nDo not change or remove the database users created by Encore, as this will prevent Encore Cloud from maintaining and handling connections to the databases in your application.\n\n</Callout>\n"
  },
  {
    "path": "docs/platform/infrastructure/neon.md",
    "content": "---\nseotitle: Neon Postgres Database\nseodesc: Learn how to configure your environment to provision a Neon Postgres database.\ntitle: Use Neon Postgres\nlang: platform\n---\n\n[Neon](https://neon.tech/) is a serverless database provider that offers a fully managed and autoscalable\nPostgres database.\n\nYou can configure Encore Cloud to provision a Neon Postgres database instead of the default offering for all supported cloud providers.\n\n## Connect your Neon account\nTo start using Neon with Encore Cloud, you need to add your Neon API key to your Encore Cloud application. You can sign up for\na Neon account at [neon.tech](https://neon.tech/). Once you have an account, you can find your API key in the\n[Neon Console](https://neon.tech/docs/manage/api-keys)\n\nThen, head over to the Neon settings page by going to the\n[Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > App Settings > Integrations > Neon.\n\nClick the \"Connect Account\" button, give it a name, and enter your API key.\n\n<img src=\"/assets/docs/connect-neon.png\" title=\"Connect Neon Account\" className=\"mx-auto\"/>\n\n## Creating environments using Neon\nNeon organizes databases in projects. A project consist of a main branch and any number of feature branches.\n[Branches](https://neon.tech/docs/introduction/branching) in Neon are similar to branches in git, letting you to create a new branch for each feature or bug fix, to test your changes in isolation.\n\nWhen configuring your Encore Cloud environment to use Neon, you can choose which project and branch to use. To get started,\nhead to the [Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > Environments > Create Environment. In the Database section, select\n`Neon database`.\n\n<img src=\"/assets/docs/create-neon.png\" title=\"Create Neon Environment\" className=\"mx-auto\"/>\n\n### Create a new Neon project and branch\nIf you're starting off a blank slate, you can let Encore Cloud create a new Neon project and branch for you.\nSelect `New Neon project` and choose a Neon account and region. We recommend picking a region close to your compute and\nthat you use the suggested project and branch names, but you're free to choose any configuration you like.\n\n### Branch from an existing Encore Cloud environment\nIf you already have an Encore Cloud environment with Neon, you can branch your database from that environment.\nSimply select `Branch from Encore environment` and choose the environment you want to branch from. This option will\nbe disabled if you don't have any environments using Neon.\n\n### Branch from an existing Neon branch\nYou can also choose to manually select a Neon branch to branch from. This is useful if you have an existing Neon project,\nbut it's not currently being used by any Encore Cloud environments. Select `Branch from Neon project`,\nthen choose the account, project and branch you want to use.\n\n### Import an existing Neon branch\nThe final option is to import an existing Neon branch. This is useful if you have an existing database you want to use.\nBe wary that this option will not create a new branch but operate on the existing data. Select `Import Neon branch`,\nthen choose the account, project and branch you want to use.\n\n**Note:** You may need to manually adjust the roles, commonly you need to change the database owner to the `db_<db_name>_admin` role to enable execution of migrations.\nSee more in the [Roles](#roles) section below.\n\n## Edit your Neon environment\nOnce the environment is created, you can edit the Neon settings by going to the [Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > Environments > (Select your environment) > Infrastructure.\nHere you can view and edit your Neon account resources. As a safety precaution, we've disabled editing of imported\nresources to prevent accidental changes to shared data.\n\n<img src=\"/assets/docs/edit-neon.png\" title=\"Edit Neon Environment\" className=\"mx-auto\"/>\n\n### Neon project\nThe retention history specifies how long Neon will keep changes to your data. The default is 1 day, but depending on your\nNeon plan, you can increase this to up to 30 days.\n\n### Neon endpoint\nEach branch is assigned a unique endpoint which essentially is the serverless compute handling your database.\nYou can edit the endpoint to set the CPU limits and the suspend timeout. The suspend timeout is the time Neon will wait\nbefore suspending the compute when it's not in use. The default is 5 minutes, but you can increase this to up to a week\n(depending on your Neon plan).\n\n## Use Neon for Preview Environments\nNeon is a great choice for [Preview Environments](/docs/platform/deploy/preview-environments) as it allows you to branch off a populated\ndatabase and test your changes in isolation.\n\nTo configure which branch to use for Preview Environments, head to the\n[Encore Cloud dashboard](https://app.encore.cloud) > (Select your app) > App Settings > Preview Environments\nand select the environment with the database you want to branch from. Hit save and you're all done.\n\nKeep in mind that you can only branch from environments that use Neon as the database provider; this is the default for Encore Cloud environments, but is a configurable option when creating AWS and GCP environments.\n\n<img src=\"/assets/docs/pr-neon.png\" title=\"Use Neon for Preview Environments\" className=\"mx-auto\"/>\n\n## Roles\n\nEncore Cloud automatically implements a structured role hierarchy that ensures a secure, scalable, and efficient management of databases.\nBelow is an explanation of how roles are created, utilized, and managed.\n\n### Role hierarchy\n\n#### 1. Initial Superuser Role\n- **Role Name:** `encore_platform`\n  - **Access level:** This role has full privileges and is the foundational user for setting up the role hierarchy.\n  - **Purpose:** The role creates and configures the subsequent roles and then steps back from day-to-day operations.\n\n#### 2. Global Roles\nThree core roles are created to define access levels across all databases:\n\n- `encore_reader`\n  - **Access level:** Provides read-only access.\n  - **Use Case:** Reading data without modifying it.\n- `encore_writer`\n  - **Access level:** Allows read and write access.\n  - **Use Case:** Performing data manipulations and inserts.\n- `encore_admin`\n  - **Access level:** Grants administrative privileges for global database operations.\n  - **Use Case:** Overseeing configurations, managing schemas, and handling elevated tasks.\n\nThese global roles are used by Encore's CLI when using the `encore db shell` command.\nLearn more in the [CLI docs](/docs/ts/primitives/databases#using-the-encore-cli).\n\n#### 3. Database-Specific Roles\nFor each database within the Neon integration, specific roles are created to provide fine-grained control:\n   - `db_<db_name>_reader`: Read-only access to the main database.\n   - `db_<db_name>_writer`: Read and write access to the main database.\n   - `db_<db_name>_admin`: Administrative privileges specific to the main database.\n\n#### 4. Service-Specific Roles\nFor each service in your application, a dedicated role is generated in the format `svc_<name>`. This role is granted the necessary `db_<db_name>_writer` role for each database the service accesses.\n\nThis ensures that each service has the appropriate level of access to perform its operations while maintaining security and separation of concerns.\n\n**Example:** A service named `orders` that writes to the `main` database is assigned the `svc_orders` role, which is granted the `db_main_writer` role.\n\n### Role Setup Workflow\n\n- **1. Superuser Creation:** the `encore_platform` superuser role is created upon integration setup.\n- **2. Global Role Creation:** The `encore_reader`, `encore_writer`, and `encore_admin` roles are established to provide general access control.\n- **3. Database-Specific Roles:** For each database, roles are created in the format `db_<db_name>_<access_level>` to manage access specific to that database.\n- **4. Service-Specific Roles:** For each service, roles are created in the format `svc_<name>` and are granted the necessary writer roles for the databases used by each service.\n\n### Viewing credentials\n\nTo view database credentials, open your app in the [Encore Cloud dashboard](https://app.encore.cloud), navigate to the **Infrastructure page** for the appropriate **Environment**, and locate the **USERS** section within the relevant **Database Cluster**.\n\n\n### Best Practices\n\nEncore Cloud automatically manages roles according to these security best practices:\n\n- **Role Ownership:** Ensures critical operations, such as migrations, are executed by roles with appropriate permissions (e.g., `db_<db_name>_admin`).\n- **Access Control:** Assigns the least privilege necessary for each task. Uses specific database roles (e.g., `db_<db_name>_reader`) to restrict access.\n- **Consistency:** Maintains consistent naming conventions (`db_<db_name>_<access_level>`) for ease of management and troubleshooting.\n\n### Integrating with existing Neon databases\n\nIf you are integrating with an existing Neon database, you may need to manually adjust the roles to work with Encore Cloud's role structure.\nCommonly, the adjustment needed is changing the database owner to the `db_<db_name>_admin` role to enable execution of migrations.\n"
  },
  {
    "path": "docs/platform/integrations/api-reference.md",
    "content": "---\nseotitle: Encore Cloud API Reference\nseodesc: Learn how to use the Encore Cloud API.\ntitle: Encore Cloud API Reference\nlang: platform\n---\n\nEncore Cloud provides an API for programmatic access to control certain parts of the platform.\n\nWe're working on expanding the set of features available over the API.\nPlease reach out to us [on Discord](https://encore.dev/discord) if you have use cases where additional API functionality would be useful.\n\nThe Base URL for the Encore Cloud API is `https://api.encore.cloud`.\n\n## Authentication\n\nAll API calls require valid authentication, which is provided by sending an access token in the `Authorization` header,\nin the format `Authorization: Bearer ${ACCESS_TOKEN}`.\n\nYou can retrieve an API access token from the OAuth Token endpoint, using an OAuth Client.\nAn API access token expires after one hour. For continuous access, shortly before an API access token expires, request a new API access token from Encore Cloud's OAuth token endpoint.\n\nOAuth client libraries in popular programming languages can handle the API access token generation and renewal.\n\nSee the [OAuth Clients](/docs/platform/integrations/oauth-clients) for more information on creating OAuth Clients.\n\n## OAuth\n\n**Method**: `POST` <br/>\n**Path**: `/api/oauth/token`\n\n#### Query Parameters\n\n| Parameter         | Description                                                    |\n| ----------------- | -------------------------------------------------------------- |\n| **client_id**     | The client id of the OAuth Client to generate a token for.     |\n| **client_secret** | The client secret of the OAuth Client to generate a token for. |\n\n#### Response\n\nThe API responds with a 2xx status code on successful creation of an API access token.\n\n```typescript\ntype Token = {\n  // The access token itself.\n  \"access_token\": string;\n\n  // The access token expires after 1 hour (3600 seconds).\n  \"expires_in\": 3600;\n\n  // The actor the token belongs to, in this case the OAuth2 client id.\n  actor: string;\n\n\n  // Indicates the access token should be passed as a \"Bearer\" token in the Authorization header.\n  \"token_type\": \"Bearer\";\n}\n```\n\n## Rollouts\n\nEncore Cloud's deployment system consists of several phases:\n\n* A build phase\n* An infrastructure provisioning phase\n* A deployment phase\n\nThese phases are combined into a unified entity called a *Rollout*.\nA rollout represents the coordinated process of rolling out a specific version of an Encore application.\n\nWe use the term *rollout* to disambiguate from the *deployment phase*, which specifically\nrefers to the last phase of the rollout process (where the version is being deployed onto the provisioned infrastructure).\n\n### The Rollout Object\n\nThe Rollout object represents the state of a rollout.\n\n```typescript\n// The representation of a rollout.\ntype Rollout = {\n  // Unique id of the rollout.\n  id: string;\n\n  // The current status of the rollout.\n  status: \"pending\" | \"queued\" | \"running\" | \"completed\";\n\n  // What the conclusion was of the rollout (when status is \"completed\").\n  // If the status is not \"completed\" the conclusion is \"pending\".\n  conclusion: \"pending\" | \"canceled\" | \"failure\" | \"success\";\n\n  // When the rollout was queued, started, and completed.\n  queued_at: Date | null;\n  started_at: Date | null;\n  completed_at: Date | null;\n\n  // Information about the various rollout phases.\n  // See type definitions below.\n  build: RolloutPhase<BuildStatus, BuildConclusion>;\n  infra: RolloutPhase<InfraStatus, InfraConclusion>;\n  deploy: RolloutPhase<DeployStatus, DeployConclusion>;\n}\n\n// Common structure of the various rollout phases.\ntype RolloutPhase<Status, Conclusion> = {\n  // Unique id of the phase.\n  id: string;\n\n  // The current status of the rollout phase.\n  status: Status;\n\n  // What the conclusion was of the phase.\n  conclusion: Conclusion;\n\n  // When the phase was queued, started, and completed.\n  queued_at: Date | null;\n  started_at: Date | null;\n  completed_at: Date | null;\n}\n\n// The current status and conclusion of a build.\n// If the status is not \"completed\" the conclusion is \"unknown\".\ntype BuildStatus = \"queued\" | \"running\" | \"completed\";\ntype BuildConclusion = \"unknown\" | \"canceled\" | \"failure\" | \"success\";\n\n// The current status and conclusion of an infra change.\n// The \"proposed\" status means the change is awaiting human approval.\n// The \"rejected\" conclusion means a human rejected the proposed infra change.\ntype InfraStatus = \"pending\" | \"proposed\" | \"queued\" | \"running\" | \"completed\";\ntype InfraConclusion = \"unknown\" | \"canceled\" | \"failure\" | \"rejected\" | \"success\";\n\n// The current status and conclusion of a deploy.\n// If the status is not \"completed\" the conclusion is \"unknown\".\ntype DeployStatus = \"queued\" | \"running\" | \"completed\";\ntype DeployConclusion = \"unknown\" | \"canceled\" | \"failure\" | \"success\";\n```\n\n### Triggering a rollout\n\n**Method**: `POST` <br/>\n**Path**: `/api/apps/${APP_ID}/envs/${ENV_NAME}/rollouts`\n\n#### Path Parameters\n\n| Parameter    | Description                                                |\n| ------------ | ---------------------------------------------------------- |\n| **APP_ID**   | The id of the Encore application to trigger a rollout for. |\n| **ENV_NAME** | The name of the environment to trigger a rollout for.      |\n\n#### JSON Request Body\n\nA rollout can be triggered either by commit SHA or by branch name.\n\n**By commit SHA:**\n```json\n{\n  // The commit hash to trigger a deploy for.\n  \"sha\": \"abc123...\",\n  // Optional. Set to skip running tests during build.\n  \"skip_tests\": false,\n  // Optional. Set to force a rebuild instead of reusing a cached build.\n  \"force_rebuild\": false\n}\n```\n\n**By branch name:**\n```json\n{\n  // The name of the branch to deploy the latest commit from.\n  \"branch\": \"main\",\n  // Optional. Set to skip running tests during build.\n  \"skip_tests\": false,\n  // Optional. Set to force a rebuild instead of reusing a cached build.\n  \"force_rebuild\": false\n}\n```\n\nExactly one of `sha` or `branch` must be provided. `skip_tests` and `force_rebuild` are optional and default to `false`.\n\n#### Response\n\nThe API responds with a 2xx status code on successful creation of a new rollout.\n\nOn success it returns a **Rollout** object as its JSON response payload,\nrepresenting the current state of the newly created rollout.\n\n### Retrieving a rollout\n\n**Method**: `GET` <br/>\n**Path**: `/api/apps/${APP_ID}/rollouts/${ROLLOUT_ID}`\n\n#### Path Parameters\n\n| Parameter      | Description                                                  |\n| -------------- | ------------------------------------------------------------ |\n| **APP_ID**     | The id of the Encore application to trigger a rollout for.   |\n| **ROLLOUT_ID** | The id of the rollout to retrieve, in the form `\"roll_...\"`. |\n\n#### Response\n\nThe API responds with a 2xx status code on successful retrieval of the rollout.\nOn success it returns a **Rollout** object as its JSON response payload,\nrepresenting the current state of the requested rollout.\n\n## Member Management\n\nEncore Cloud provides APIs for managing application members, including inviting users, listing members, and updating member roles.\n\n### Member Object\n\n```typescript\ntype Member = {\n  // The user's email address\n  email: string;\n\n  // The member's role in the application\n  role: \"owner\" | \"reader\" | \"writer\" | \"none\";\n\n  // When the member was invited to the application\n  invited: timestamp;\n\n  // When the member accepted the invitation\n  accepted: timestamp;\n\n  // When the membership expires\n  expires: timestamp;\n\n  // The member's username\n  username: string;\n\n  // The member's full name\n  full_name: string;\n\n  // The member's picture URL\n  picture_url: string;\n}\n```\n\n### Available Roles\n\n- **owner**: Full control over the application\n- **writer**: Can write application resources\n- **reader**: Can read application resources\n- **none**: Used to revoke access to an application\n\n### Invite Member\n\nInvite a new member to an Encore application.\n\n**Method**: `POST` <br/>\n**Path**: `/api/apps/${APP_ID}/member`\n\n#### Path Parameters\n\n| Parameter  | Description                                    |\n| ---------- | ---------------------------------------------- |\n| **APP_ID** | The id of the Encore application.             |\n\n#### JSON Request Body\n\n```typescript\n{\n  // The email address of the user to invite\n  \"email\": string;\n\n  // The role to assign to the invited member\n  \"role\": \"owner\" | \"reader\" | \"writer\" | \"none\";\n}\n```\n\n#### Response\n\nThe API responds with a 2xx status code on successful invitation.\n\nOn success it returns a **Member** object as its JSON response payload,\nrepresenting the newly invited member.\n\n### List Members\n\nRetrieve a list of all members for an Encore application.\n\n**Method**: `GET` <br/>\n**Path**: `/api/apps/${APP_ID}/members`\n\n#### Path Parameters\n\n| Parameter  | Description                                    |\n| ---------- | ---------------------------------------------- |\n| **APP_ID** | The id of the Encore application.             |\n\n#### Response\n\nThe API responds with a 2xx status code on success.\n\nOn success it returns an array of **Member** objects as its JSON response payload,\nrepresenting all current members and pending invites for the application.\n\n```typescript\ntype Response = Member[];\n```\n\n### Update Member Role\n\nUpdate the role of an existing member.\n\n**Method**: `PUT` <br/>\n**Path**: `/api/apps/${APP_ID}/members`\n\n#### Path Parameters\n\n| Parameter  | Description                                    |\n| ---------- | ---------------------------------------------- |\n| **APP_ID** | The id of the Encore application.             |\n\n#### JSON Request Body\n\n```typescript\n{\n  // The email address of the member to update\n  \"email\": string;\n\n  // The new role to assign to the member\n  \"role\": \"owner\" | \"reader\" | \"writer\" | \"none\";\n}\n```\n\n#### Response\n\nThe API responds with a 2xx status code on successful update.\n\n#### Error Cases\n\n- **403 Forbidden**: Insufficient permissions to manage members\n- **409 Conflict**: Attempting to remove the last owner (error detail: \"last_owner\")\n- **404 Not Found**: Member not found\n"
  },
  {
    "path": "docs/platform/integrations/auth-keys.md",
    "content": "---\nseotitle: Auth Keys let you authenticate without a browser\nseodesc: Learn how to use pre-authentication keys to authenticate without needing to sign in via a web browser. See how to setup reusable and ephemeral auth keys.\ntitle: Generating Auth Keys\nlang: platform\n---\n\nPre-authentication keys (“auth keys” for short) let you authenticate the Encore CLI without needing to sign in via a web browser. This is most useful when setting up CI/CD pipelines.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/authkeys.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n\n## Types of auth keys\n\n- **Reusable Keys** for authenticating more than one machine.\n\n- **Ephemeral Keys** - Machines authenticated by this key will be automatically logged out after one hour.\n\n<Callout type=\"important\">\n\n**Be very careful with reusable keys!** These can be very dangerous if stolen. They're best kept in a key vault product (eg. 1Password, LastPass) specifically designed for the purpose.\n\n</Callout>\n\n## Authentication\n\n**Auth keys** authenticate a machine as the Encore app for which the key was generated. If Ada generates an auth key, and uses it to set up her CI/CD pipeline, then that machine is authenticated as Ada's Encore app.\n\n## Generating a key\n\n### Step 1: Generate an auth key\n\nAs an Encore user, visit the auth key page by going to **[Your apps](https://app.encore.cloud/) > (Select your app) > App Settings > Auth Keys**.\n\nA key can be both **reusable** and **ephemeral** at the same time (you can decide the combination based on your particular use case).\n\n<Callout type=\"important\">\n\n**Don't forget to store your key!** Once generated, you will need to copy and store your key in a vault product (eg. 1Password, LastPass). We do not display the full contents of a key in our dashboard for security reasons.\n\n</Callout>\n\nThis page also gives you the ability to revoke existing keys.\n\n### Step 2: Authenticate with the auth key\n\nUsing the Encore CLI, you can authenticate with your newly generated key:\n\n```shell\n$ encore auth login --auth-key=ena_nEQIkfeM43t7oxpleMsIULbhbtLAbYnnLf1D\n```\n\n## Revoking a key\n\nYou can revoke a key simply by pressing the **Delete** button next to it. This will prevent any machines currently using it to authenticate to Encore Cloud (regardless of the key type).\n"
  },
  {
    "path": "docs/platform/integrations/custom-domains.md",
    "content": "---\nseotitle: Custom Domains for all your environments\nseodesc: Learn how to setup a custom domain for your cloud environments, to use your own domain to access your backend application built with Encore.\ntitle: Custom Domains\nsubtitle: Expose APIs from your own domain\nlang: platform\n---\n\nBy default, all application [environments](/docs/platform/deploy/environments) are accessible as subdomains of the shared Encore domain `encr.app`. When exposing APIs publicly, you often want to provide a URL endpoint branded with your own domain.\n\nFollow these instructions to serve your backend using your own custom domain name. This also has the benefit of providing a built-in Web Application Firewall (WAF) using [Cloudflare WAF](https://www.cloudflare.com/en-gb/application-services/products/waf/).\n\n## Adding a domain\n\nModify the DNS records for your domain, adding a CNAME record pointing at:\n`custom-domain.encr.app` It's recommended to set a TTL (Time-To-Live) of 30 minutes for the CNAME record.\n\n\n<Callout type=\"important\">\n\nEncore requires that you add a CNAME record for each domain you wish to serve traffic from.\nCNAME record using wildcards, e.g. `*.example.com`, are not currently supported.\n\n</Callout>\n\nOnce you've added the CNAME record, go to the Custom Domains settings page by opening\n**[Your apps](https://app.encore.cloud/) > (Select your app) > Settings > Custom Domains**. Click on `Add Domain`\non the top right of the page.\n\nEnter the domain name you configured the CNAME on and select which [environment](/docs/platform/deploy/environments) you wish to\nserve on that domain, then click `Add`.\n\nEncore will now set up your domain and issue SSL certificates to serve traffic through.\n\n<img src=\"/assets/docs/customdomain.png\" title=\"Custom Domain Settings\" className=\"noshadow\"/>\n\n<Callout type=\"info\">\n\nIf you configure multiple domains against a single environment, Encore will serve traffic through all\nconfigured domains. The `encr.app` subdomain which was created when you originally created an environment will always be\nconfigured to serve traffic to that environment.\n\nThis allows you to migrate to a custom domain safely without risking\ncutting traffic off to older clients which may be hard coded to access your applications via the default subdomain.\n\n</Callout>\n\n## Domain statuses\n\nOn the Custom Domains settings page, you can see the various statuses throughout the lifecycle of a custom domain.\n\n| Status                     | Description                                                                                                                                                                       |\n| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `Pending`                  | The domain is currently queued to be provisioned by Encore.                                                                                                                       |\n| `Waiting for CNAME`        | Encore is waiting for the CNAME to become active and for the SSL certificate to be issued for the domain.                                                                         |\n| `Configuring Edge Routers` | The SSL certificate has been issued and the Encore edge routers are being configured to route traffic on the domain.                                                              |\n| `Active`                   | The domain is serving traffic to your Encore application.                                                                                                                         |\n| `Not Working`              | A non-recoverable problem has occurred. This could be a result of the CNAME record being removed or pointed elsewhere. If you see this error, please [contact support](/contact). |\n"
  },
  {
    "path": "docs/platform/integrations/github.md",
    "content": "---\nseotitle: Integrate your Encore application with GitHub\nseodesc: Learn how to integrate your backend application with GitHub to get automatic Preview Environments for each Pull Request using Encore.\ntitle: Integrate with GitHub\nlang: platform\n---\n\nEncore applications are easy to integrate with GitHub for source code hosting.\n\nTo link your application to GitHub, open your application in the [Encore Cloud dashboard](https://app.encore.cloud), and click on **Settings** in the main navigation.\nThen select **GitHub** in the settings menu.\n\nNext, connect your account to GitHub by clicking the **Connect Account to GitHub** button. This will open GitHub where you can grant access either to all repositories or only the specific one(s) you want to link with Encore.\n\n<img class=\"max-w-lg w-full mx-auto\" src=\"/assets/img/git-connect.png\" />\n\nWhen you come back to Encore, click the **Link App to GitHub** button:\n\n<img class=\"max-w-lg w-full mx-auto\" src=\"/assets/img/git-begin.png\" />\n\nIn the popup, select the repository you would like to link your app with:\n\n<img class=\"max-w-lg w-full mx-auto\" src=\"/assets/img/git-modal.png\" />\n\nClick **Link** and you're done! Encore will now automatically start building and running tests against\nyour Pull Requests, and provision Preview Environments for each Pull Request.\n\n<img class=\"max-w-lg w-full mx-auto\" src=\"/assets/img/git-linked.png\" />\n\n## Placing your Encore app in a monorepo sub-folder\n\nIf you already have a monorepo and want to place your Encore application in a sub-folder, you need to tell Encore which folder the `encore.app` file is in.\n\nDo this by opening your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to **Settings** > **General**. Then in the **Root Directory** section, you specify the directory within your Git repository in which your `encore.app` file is located.\n\n## Configure deploy trigger\n\nWhen using GitHub, you can configure Encore to automatically trigger deploys when you push to a specific branch name.\nTo configure which branch name is used to trigger deploys, open your app in the [Encore Cloud dashboard](https://app.encore.cloud) and go to the **Overview** page for your intended environment. Click on **Settings** and then in the section **Branch Push** configure the `Branch name`  and hit save.\n\n## Preview Environments for each Pull Request\n\nOnce you've linked your app with GitHub, Encore will automatically start building and running tests against\nyour Pull Requests.\n\nEncore will also provision a dedicated Preview Environment for each pull request.\nThis environment works just like a regular development environment, and lets you test your changes\nbefore merging.\n\nLearn more in the [Preview Environments documentation](/docs/platform/deploy/preview-environments).\n\n![Preview environment linked in GitHub](/assets/docs/ghpreviewenv.png \"Preview environment linked in GitHub\")\n"
  },
  {
    "path": "docs/platform/integrations/oauth-clients.md",
    "content": "---\nseotitle: Encore Cloud OAuth Clients\nseodesc: Learn how to use OAuth Clients for access to the Encore Cloud API\ntitle: OAuth Clients\nlang: platform\n---\n\nOAuth clients provide a framework for delegated and scoped access to the Encore Cloud API. An OAuth client creates short-lived access tokens on demand, and supports the principle of least privilege by allowing fine-grained control on the access granted to the client using scopes.\n\n## How it works\nYou create an OAuth client that defines the scopes to allow when your client application uses the Encore Cloud API.\n\nScopes are currently grouped into \"roles\", which include a set of permissions.\nFor example, the `deployer` role grants access to the triggering deployments.\n\nAn OAuth client consists of a client ID and a client secret. When you create an OAuth client, Encore Cloud creates these for you. Within your client application, use the client ID and client secret to request an API access token from the Encore Cloud's OAuth token endpoint. You use the access token to make calls to the Encore Cloud API. The access token grants permission only for the scopes that were defined when you created the OAuth client.\n\nAn API access token expires after one hour. For continuous access, shortly before an API access token expires, request a new API access token from Encore Cloud's OAuth token endpoint.\n\nOAuth client libraries in popular programming languages can handle the API access token generation and renewal.\n\nEncore Cloud's OAuth implementation is based on the [OAuth 2.0 protocol](https://www.rfc-editor.org/rfc/rfc6749).\n\n## Prerequisites\nYou need to be an Owner of the Encore application in order to create or revoke OAuth clients.\n\n### Setting up an OAuth client\nOpen the OAuth clients page in the application settings page.\n\nIn the Generate OAuth client dialog, select the set of operations that can be performed with tokens created by the new OAuth client.\n\nAfter generating the client, you can see the new OAuth client's ID and secret. Copy both the client ID and secret, as you need them for your client code.\nNote that after you close the Generated new OAuth client dialog, you won't be able to copy the secret again.\n**Store the client secret securely.**\n\nYour OAuth client is now configured. Use the client ID and secret when you configure your OAuth client application. Note that the provided client secrets are case-sensitive.\n\nIf an OAuth client is created by a user who is later removed from your application, the OAuth client will continue to function and generate API access tokens.\nApplication owners can see all configured OAuth clients in the OAuth clients page of the application settings.\n\n### Roles\nRoles define which operations are permitted in API access tokens that are created by your client application.\n\nCurrently there is a single supported role: **Deployer**. The deployer role\nallows for programatically triggering deployments.\n\nWhen new Encore Cloud functionality is provided, we will add it to existing roles where applicable.\nThat means a role is not restricted to only access of APIs that existed at the time the client was initially authorized &mdash; a role will contain additional access where it makes sense for new or updated functionality.\n\n### Revoking an OAuth client\nOpen the OAuth clients page of the application settings page.\n\nFind the OAuth client that you want to delete and select Revoke.\n\nSelect Revoke OAuth client to confirm you want to revoke the OAuth client.\n\nWhen you revoke an OAuth client, any active API access tokens that were created by the client are also revoked.\n\n### Encore Cloud OAuth token endpoint\nEncore Cloud's OAuth token endpoint is https://api.encore.cloud/api/oauth/token.\nSee the [Encore Cloud API Reference](/docs/platform/integrations/api-reference) documentation for more information.\n\nMake requests to the OAuth token endpoint when you need an API access token. The OAuth token endpoint accepts requests that conform to the OAuth 2.0 client credentials grant request format, and returns responses that conform to the OAuth 2.0 client credentials grant response format.\n\n## OAuth client libraries\nPopular programming languages provide OAuth client libraries to simplify your use of OAuth clients.\n\nFor example, the following Go code shows how to create an OAuth client object that uses your client ID and client secret to generate an API access token for calls to the Encore Cloud API.\nSimilar libraries exist for other popular programming languages.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"os\"\n\n    \"golang.org/x/oauth2/clientcredentials\"\n)\n\nfunc main() {\n    oauthConfig := &clientcredentials.Config{\n        ClientID:     os.Getenv(\"OAUTH_CLIENT_ID\"),\n        ClientSecret: os.Getenv(\"OAUTH_CLIENT_SECRET\"),\n        TokenURL:     \"https://api.encore.cloud/api/oauth/token\",\n    }\n\n    client := oauthConfig.Client(context.Background())\n\n    // Make API calls using `client.Get` etc.\n    resp, err := client.Get(\"https://api.encore.cloud.com/api/...\")\n    // ...\n}\n```\n\nThe example requires that you define environment variables `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET`, with their values set to the client ID and client secret that are created when you set up an OAuth client.\n\n### Verifying you can generate API access tokens\nAfter you set up an OAuth client, an easy way to confirm that you can generate API access tokens is to make a curl request to the Encore Cloud OAuth token endpoint.\n\n\n```bash\ncurl -d \"client_id=${OAUTH_CLIENT_ID}\" -d \"client_secret=${OAUTH_CLIENT_SECRET}\" \\\n     -d \"grant_type=client_credentials\" \"https://api.encore.cloud/api/oauth/token\"\n```\n\nThe example requires that you define environment variables OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, with their values set to your client ID and client secret.\n\nHere's an example response showing the API access token:\n\n```json\n{\"access_token\":\"MTcyODQ3NTg3NXww...GDxfmxnuq9zDEAmHmP5D44=\",\"token_type\":\"Bearer\",\"expires_in\":3600, \"actor\": \"o2c_my_key_id\"}\n```\n\n## Limitations\n\nAn OAuth access token expires after 1 hour &mdash; this duration cannot be modified.\n"
  },
  {
    "path": "docs/platform/integrations/terraform.md",
    "content": "---\nseotitle: Integrate Encore with existing infrastructure\nseodesc: The Encore Terraform Provider lets you integrate your Encore deployment with existing infrastructure\ntitle: Terraform Provider\nsubtitle: Integrate Encore with existing infrastructure\ninfobox: {\n  title: \"Terraform Provider\",\n  import: \"https://registry.terraform.io/providers/encoredev/encore\",\n}\nlang: platform\n---\nEncore makes it simple to deploy and manage cloud applications. When you're dealing with a large and complex system, you may want to integrate Encore-provisioned resources with an existing infrastructure landscape. For this purpose, Encore maintains a Terraform Provider with data sources for all Encore-provisioned resources.\n\n## Understanding Encore Terraform Data Sources\n\nEncore Terraform data sources act as read-only references to resources Encore has already provisioned on your behalf.\nUnlike Terraform resources (which create or modify infrastructure), data sources only retrieve information. The Encore\ndata sources let's you retrieve cloud identifiers for resources managed by Encore, such as databases, caches, and more.\nTo do this, you only need to provide the name of the resource and the environment it's in.\n\n## Configuring the Encore Terraform Provider\n\nTo use Encore data sources, you need to declare the Encore Terraform provider in the `required_providers` of\nyour Terraform configuration file. Here's an example of how to declare the provider:\n\n```\nterraform {\n  required_providers {\n    encore = {\n      source = \"registry.terraform.io/encoredev/encore\"\n    }\n  }\n}\n```\n\nOnce you've declared the provider, Terraform will automatically download the provider plugin when initializing the\nworking directory using `terraform init`.\n\nTo authenticate with the Encore API, the provider need an Encore Auth Key. You can generate an auth key from\nEncore's [Cloud Dashboard](https://encore.dev/docs/platform/integrations/auth-keys). Once you have the auth key, you can configure the\nprovider in your Terraform configuration file like this:\n\n```\nprovider \"encore\" {\n    env = \"your-env\"\n    auth_key = \"your-auth-key\"\n}\n```\nYou can also set the `ENCORE_AUTH_KEY` environment variable to avoid hardcoding the auth key in your configuration file.\n\n## Using Encore Terraform Data Sources\n\nOnce you have the provider configured, you can use the Encore data sources to retrieve information about resources.\nThere are several data sources available, such as `encore_database`, `encore_cache`, and `encore_pubsub_topic`. Each data\nsource has its own set of attributes that you can use to retrieve information about the resource. The full documentation\nfor each data source is available in the [Terraform Registry](https://registry.terraform.io/providers/encoredev/encore).\n\nHere's an example of how to use the `encore_pubsub_topic` data source to connect AWS IOT Core to an Encore PubSub topic:\n\n```\ndata \"encore_pubsub_topic\" \"topic\" {\n  name = \"my-topic\"\n  env  = \"my-env\"\n}\n\nresource \"aws_iot_topic_rule\" \"rule\" {\n  name = \"my-rule\"\n  sql  = \"SELECT * FROM 'my-topic'\"\n  sns {\n    message_format = \"RAW\"\n    role_arn       = aws_iam_role.role.arn\n    target_arn     = data.encore_pubsub_topic.topic.aws_sns.arn\n  }\n}\n```\n"
  },
  {
    "path": "docs/platform/integrations/webhooks.md",
    "content": "---\nseotitle: Subscribe to Encore Cloud webhooks and events\nseodesc: Encore Cloud lets you define webhooks to react to events in your application, enabling you to build powerful integrations.\ntitle: Webhooks & Events\nsubtitle: Set up webhooks to react to Encore events\ninfobox: {\n  title: \"Webhooks\",\n  import: \"go.encore.dev/webhooks\",\n}\nlang: platform\n---\n\nWebhooks provide a way for notifications to be delivered to an HTTP endpoint of your choice whenever certain events happen within Encore.\nFor example, you can set up a webhook to be notified whenever a deployment starts or finishes.\n\nWebhooks are defined on a per-application basis, and are configured under Settings -> Webhooks in the [Encore Cloud dashboard](https://app.encore.cloud).\n\nTo simplify using webhooks, Encore.go provides a Go module, [go.encore.dev/webhooks](https://pkg.go.dev/go.encore.dev/webhooks), that provides\ntype definitions and documentation of all supported webhook events. This module is kept up to date as new events are added.\n\n## Webhook Deliveries\n\nEach time an event occurs that matches one of your defined webhooks,\nEncore will send a HTTP POST request to the webhook's configured URL with information about the event.\n\nIf the HTTP request fails, the delivery is marked as failed and won't be retried.\n\nEach event is given a unique event id, which is shared across all webhooks.\n\nWithin each webhook, each event is given a sequence number, which is incremented for each event\nthat matches that webhook. The sequence number allows for a linear ordering of events within a webhook,\nmaking it easy to determine if an event was missed.\n\nThese are provided in the `X-Encore-Event-Id` and `X-Encore-Sequence-Id` headers respectively,\nand are also part of the event payload itself.\n\n## Parsing webhook events\n\nTo parse a webhook event, use the [`webhooks.ParseEvent`](https://pkg.go.dev/go.encore.dev/webhooks#ParseEvent) function.\n\nAs you'll see in the example below, to parse the webhook event you'll need access to the webhook secret.\nThis is a secret value that is generated by Encore and is used to sign each webhook request. More about this\nin the next section.\n\nFor example, to process rollout started and completed webhook events,\nyou could do something like this:\n\n```go\npackage service\n\nimport (\n    \"net/http\"\n\n    \"go.encore.dev/webhooks\"\n)\n\nvar secrets struct {\n\tEncoreWebhookSecret string\n}\n\n//encore:api public raw\nfunc Webhook(w http.ResponseWriter, req *http.Request) {\n\tpayload, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\t// ... handle error\n    }\n    event, err := webhooks.ParseEvent(payload, req.Header.Get(\"X-Encore-Signature\"), secrets.EncoreWebhookSecret)\n    if err != nil {\n        // ... handle error\n    }\n\n    switch data := event.Data.(type) {\n    case *webhooks.RolloutCreatedEvent:\n        // ... handle rollout created event\n    case *webhooks.RolloutCompletedEvent:\n        // ... handle rollout completed event\n    }\n}\n```\n\n<Callout type=\"info\">\n\nNote that the example above is written as an Encore API endpoint, but that's not required.\nThe same code works in any Go HTTP server, and the `go.encore.dev/webhooks` library does not depend on\nany Encore-specific functionality.\n\n</Callout>\n\n## Checking webhook signatures\n\nSince the webhook endpoint is publicly accessible, it is important to validate that the request is coming from Encore.\nTo do so, Encore generates a secret for each webhook, which is used to sign each request.\n\nThe webhook secret can be found on the webhook details page by admins.\n\nIf you use the [go.encore.dev/webhooks](https://pkg.go.dev/go.encore.dev/webhooks) library then signature validation\nis handled automatically, but it's also possible to verify the signature manually (see below).\n\n### Preventing replay attacks\n\nA [replay attack](https://en.wikipedia.org/wiki/Replay_attack) occurs when an attacker intercepts a valid request,\nincluding the payload and signature, and re-transmits it one or more times, causing unintended side effects.\n\nTo mitigate such attacks, Encore includes a timestamp in the `X-Encore-Signature` header.\nThis timestamp is part of the signed payload, which means that it can't be changed by the attacker\nwithout invalidating the signature. This makes it possible to mitigate replay attacks by ensuring the\ntimestamp isn't older than a certain threshold (the `go.encore.dev/webhooks` library defaults to 5 minutes).\n\n### Verifying signatures manually\n\nThe `X-Encore-Signature` header included in each webhook event contains a timestamp and one or more *schemes*.\nThe timestamp is prefixed with `t=`, and each *scheme* is prefixed by a `v` and a version number.\nCurrently only the `v1` *scheme* is supported.\n\nFor example, a valid signature header might look like this:\n```\nX-Encore-Signature: t=1623345600,v1=0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b\n```\n\nThe `v1` scheme is using a hash-based message authentication code ([HMAC](https://en.wikipedia.org/wiki/HMAC))\nwith [SHA-256](https://en.wikipedia.org/wiki/SHA-2). To prevent downgrade attacks, ignore all schemes that are not `v1`.\n\nIt's possible the signature contains multiple signatures, for example if the webhook secret has been rotated recently.\n\nWhen rotating the webhook secret, Encore lets you define for how long the old secret should continue to be valid for.\nDuring that window, each webhook event will be signed with the new and the old secret.\n\nTo validate the webhook signature, follow the algorithm below:\n\n**Step 1: Extract the timestamp and signatures from the header**\nSplit the header on `,` to get a list of fields, then split each field on `=` to get the key and value.\n\nThe value of the `t` key is the timestamp, and represents the UNIX timestamp (in seconds) when the signature was created.\nThe fields with the `v1` key (possibly several, in case of secret rotation) are the signatures.\n\nDiscard any other fields.\n\n**Step 2: Prepare the payload to sign**\nCreate the payload to sign by concatenating the timestamp (as a string) and the request body, separated by `.` like so:\n\n```go\npayloadToSign := timestamp + \".\" + string(payload)\n```\n\n**Step 3: Compute the expected signature**\nCompute the HMAC with the SHA256 hash function, using the webhook secret as the key and the `payloadToSign` as the message.\n\nThen, encode the resulting HMAC using the base64 URL encoding, and trim any trailing `=` characters.\nIn Go, this can be done like so:\n\n```go\nh := hmac.New(sha256.New, []byte(webhookSecret))\nh.Write([]byte(payloadToSign))\ndigest := h.Sum(nil)\nexpectedSignature := base64.RawURLEncoding.EncodeToString(digest)\n```\n\n**Step 4: Compare the signatures**\nCompare each signature with the `v1` field in the header with the expected signature.\nTo protect against timing attacks, use a constant-time comparison function (like `crypto/hmac.Equal` in Go).\n\nIf none of the signatures match, reject the request.\n\nIf a match is found, compare the timestamp with the current time. If the difference is greater\nthan the allowed threshold (5 minutes is a reasonable default), reject the request.\n\nOtherwise, accept the request.\n"
  },
  {
    "path": "docs/platform/introduction.md",
    "content": "---\nseotitle: Introduction to Encore Cloud\nseodesc: Learn how Encore Cloud works and how it helps backend developers build cloud-based backend applications without manually dealing with infrastructure.\ntitle: Encore Cloud\nsubtitle: End-to-end development platform for building robust distributed systems\nlang: platform\n---\n\nWhile cloud services enable us to build powerful applications, they come with significant complexity.\n\nDevelopers spend countless hours managing infrastructure, writing boilerplate code, and dealing with complex deployment processes instead of building features that matter to users.\n\nLaunching a new app, migrating to the cloud, or breaking apart a monolith into microservices, can therefore be a daunting task.\nEncore Cloud is purpose-built to solve this problem and removes the need to build your own developer platform.\n\nIt provides a complete platform for building, testing, and deploying your application. In the Encore Developer Survey 2025, the average impact reported by our users was:\n- 137% improvement in delivery speed\n- 93% reduction in time spent on DevOps tasks\n- 81% reduction in developer onboarding time\n\n## A simplified cloud backend development workflow\n\nEncore Cloud provides a complete toolset for backend development: from local development, testing, and observability, to cloud infrastructure and DevOps automation.\n\n<img className=\"noshadow mx-auto d:w-3/4\" src=\"/assets/docs/arch_full.png\" title=\"Encore Overview\" />\n\nEncore Cloud's core functionality is enabled by Encore's open source backend frameworks, [Encore.ts](/docs/ts) and [Encore.go](/docs/go). These frameworks let you define essential backend resources – like APIs, microservices, databases, cron jobs, and Pub/Sub – as type-safe objects in your code using simple, declarative syntax.\n\nThe frameworks have a minimal footprint, where one line of code is enough to define a backend resource, and are designed to be unobtrusive, so that you can focus on building your product without being distracted by the underlying infrastructure.\n\nWith the backend frameworks you only define **infrastructure semantics** — _the things that matter to your application's behavior_ — not configuration for _specific_ cloud services. Encore Cloud then automatically generates boilerplate and orchestrates the relevant infrastructure for each environment using your cloud provider's API or directly through Encore Cloud's built-in cloud hosting for development.\n\n<img className=\"noshadow mx-auto d:w-3/4\" src=\"/assets/docs/howitworks.png\" title=\"Encore's infrastructure orchestration\" />\n\nThis means that, with Encore Cloud, your application code can be used to run locally, test in preview environments, and provision and deploy to cloud environments on AWS and GCP.\n\nWhen your application is deployed to your cloud, there are **no runtime dependencies on Encore Cloud** and there is **no proprietary code running in your cloud**.\n\n## Local Development\n\n<img className=\"noshadow mx-auto d:w-3/4\" src=\"/assets/docs/arch_local.png\" title=\"Encore's Local Development Workflow\" />\n\nThe local development tooling is fully open source. When you run your app locally using the [Encore CLI](/docs/ts/install), it parses your code and automatically sets up the necessary local infrastructure on the fly. _No more messing around with Docker Compose!_\n\nAside from managing infrastructure, Encore's local development workflow comes with a lot of tools to make building distributed systems easier:\n\n- **Local environment matches cloud:** Encore automatically handles the semantics of service communication and interfacing with different types of infrastructure services, so that the local environment is a 1:1 representation of your cloud environment.\n- **Cross-service type-safety:** When building microservices applications with Encore, you get type-safety and auto-complete in your IDE when making cross-service API calls.\n- **Type-aware infrastructure:** With Encore, infrastructure like Pub/Sub queues are type-aware objects in your program. This enables full end-to-end type-safety when building event-driven applications.\n- **Tracing:** The [local development dashboard](/docs/ts/observability/dev-dash) provides local tracing to help understand application behavior and find bugs.\n- **Automatic API docs & clients:** Encore generates [API docs](/docs/ts/observability/service-catalog) and [API clients](/docs/ts/cli/client-generation) in Go, TypeScript, JavaScript, and OpenAPI specification.\n\n## Testing\n\n<img className=\"noshadow mx-auto d:w-3/4\" src=\"/assets/docs/arch_testing.png\" title=\"Encore's Testing Workflow\" />\n\nEncore's open source framework comes with several built-in tools to help with testing:\n\n- **Built-in service/API mocking:** Encore provides built-in support for [mocking API calls](/docs/go/develop/testing/mocking), and interfaces for automatically generating mock objects for your services.\n- **Local test infra:** When running tests locally, Encore automatically provides dedicated [test infrastructure](/docs/go/develop/testing#test-only-infrastructure) to isolate individual tests.\n- **Local test tracing:** The [Local Development Dashboard](/docs/go/observability/dev-dash) provides distributed tracing for tests, providing great visibility into what's happening and making it easier to understand why a test failed.\n\nEncore Cloud adds to this tool-set with:\n- **Preview Environments:** Encore automatically provisions a [Preview Environment](/docs/platform/deploy/preview-environments) for each Pull Request, an effective tool when doing end-to-end testing.\n\n## Infrastructure & DevOps automation\n\n<img className=\"noshadow mx-auto d:w-3/4\" src=\"/assets/docs/arch_devops.png\" title=\"Encore's DevOps Workflow\" />\n\n### Infrastructure Management\n- **Zero-config deployment:** Connect your repo and cloud account, then deploy and Encore Cloud orchestrates your cloud resources by integrating with your cloud provider's API.\n- **Automatic infrastructure:** Use battle-tested AWS/GCP services (Cloud Run/Fargate, GKE/EKS, CloudSQL/RDS, Pub/Sub / SQS/SNS, etc.) without any manual setup or configuration.\n- **No IaC required:** Say goodbye to Terraform and YAML - your code is the single source of truth.\n\n### Security & Governance\n- **Automated IAM:** Least-privilege security permissions generated from parsing your code\n- **Infrastructure tracking:** Complete visibility of all provisioned resources\n- **Change management:** Built-in approval workflow for infrastructure changes\n- **Configuration management:** Simple UI for config changes that automatically 2-way syncs to your cloud\n\n### Monitoring & Observability\n- **Cost monitoring:** Track infrastructure costs across your cloud resources (currently for GCP, AWS coming soon)\n- **Integrated observability:** Built-in logging, metrics, and tracing\n- **Third-party integration:** Works with Datadog, Grafana, and other tools\n- **Auto-generated documentation:**\n  - Service catalog with complete API docs\n  - Live architecture diagrams showing infrastructure dependencies\n\n## Why choose Encore Cloud?\n\nEncore Cloud's end-to-end workflow is an unfair advantage for teams that need to move quickly without sacrificing quality and scalability.\nBy automating over 90% of the normal day-to-day DevOps work, you can focus on building your product instead of building your own developer platform.\n\nThe benefits of Encore Cloud are:\n\n- **Faster Development**: Encore Cloud enables 2-3x faster iterations thanks to the streamlined the development process with its clear abstractions and built-in development tools.\n- **Reduced Costs**: Encore Cloud's infrastructure management minimizes wasteful cloud expenses and reduces DevOps workload by 90%.\n- **Scalability & Performance**: Encore Cloud simplifies building microservices applications that can handle growing user bases and demands, without the normal boilerplate and complexity.\n- **Control & Standardization**: Encore Cloud enforces standardization and provisions infrastructure consistently according to best practices for each cloud provider.\n- **Quality through understandability:** Built-in tools like automated architecture diagrams, generated API docs, and distributed tracing make it simple for teams to get an overview of their system and ensure the correct behavior.\n- **Security**: Encore Cloud makes your application secure by automatically implementing security best practices for each cloud provider.\n\n## Common use cases\n\nEncore Cloud is designed to give teams a productive and less complex experience when solving most common backend use cases.\nMany teams use Encore Cloud to build things like:\n\n-   Consumer apps\n-   High-performance B2B Platforms\n-   Fintech & Crypto applications\n-   Global E-commerce marketplaces\n-   Microservices backends and event-driven systems for scalable SaaS applications\n-   And much more...\n\nCheck out the [showcase](https://encore.cloud/showcase) section for some examples of real-world products being built with Encore.\n\n## Getting started\n\n- [Follow the Quick Start Guide](/docs/ts/quick-start)\n- [Join Discord](https://encore.dev/discord) to ask questions and meet other Encore developers\n- Follow and star the project on [GitHub](https://github.com/encoredev/encore) to stay up to date\n- [Book a demo](https://encore.dev/book) to speak to one of our founders about if Encore Cloud is a good fit for your team\n"
  },
  {
    "path": "docs/platform/management/billing.md",
    "content": "---\nseotitle: Plans & Billing\nseodesc: Encore is free to use for many projects, and comes with paid plans for teams who want to move quickly. Learn more!\ntitle: Plans & Billing\nsubtitle:\nlang: platform\n---\n\nEncore offers a **Free** plan for teams that want a simple development workflow and collaboration features.\nIf you want access to all features and want to use Encore's DevOps automation tools for AWS & GCP, there's a paid **Pro** plan available.\n\nSee the [pricing page](https://encore.dev/pricing) for a feature comparison between each plan and complete pricing information.\n\n## When do I need to upgrade to a paid plan?\n\nThe Free plan comes with certain limitations, and should your needs exceed one or more of them, you may wish to upgrade to a paid plan:\n\n- When you need more than 2 cloud environments\n- When you want to [automate DevOps in your cloud on AWS/GCP](/docs/platform/infrastructure/infra)\n- When you want [Preview Environments](/docs/platform/deploy/preview-environments) for each Pull Request\n- When you want to use a Custom Domain\n- When you need multiple concurrent builds\n- When you need guaranteed logs & tracing retention\n- When you want access to private support & onboarding assistance\n- When you want custom configuration for environments hosted on Encore Cloud\n\nThere is a free 14-day trial of the Pro plan, available to all new Encore users. It's a great way to try out all the features and learn if the Pro plan suits your needs.\nYou can activate your trial from the [pricing page](https://encore.dev/pricing).\n\n## Do I need to pay for hosting?\n\nAll plans come with free use of Encore Cloud, subject to [Fair Use limits](/docs/platform/management/usage).\nEncore Cloud is intended for development environments and limited scale professional use that does not require specific SLAs.\n\nFor production use, you can [connect your own cloud account on AWS/GCP](/docs/platform/deploy/own-cloud) and use Encore to deploy there, including provisioning infrastructure and managing IAM. When you connect your own cloud account, you pay for usage directly to your cloud provider.\n\nIf you prefer to manage deployment yourself, you can export your application as a standalone Docker image and deploy in any way you prefer. ([Learn more](/docs/ts/self-host/build))\n\n## Payments & Billing FAQ\n\n### What is the price of the paid plan?\n\nSee the [pricing page](https://encore.dev/pricing) for complete pricing information.\n\nIf you are a large organization with specific needs, please [email us](mailto:hello@encore.dev) or [book a 1:1](/book) to discuss your needs and get a custom price quote.\n\n### Can I pay with a credit card?\n\nYes, we offer payments via Stripe using all major credit cards. You can upgrade your account via the [pricing page](/pricing).\n\n### What happens if my payment fails?\n\nIf your payment fails, everything will keep working as normal!\nWe will reach out to you with instructions on how to update your payment information so that we can try to process your payment again.\nWe will not downgrade your account without prior notice.\n"
  },
  {
    "path": "docs/platform/management/compliance.md",
    "content": "---\nseotitle: Security & Compliance\nseodesc: Learn about Encore's security practices, infrastructure protections, and compliance posture — built on industry-standard controls and trusted cloud providers.\ntitle: Security & Compliance\nsubtitle: How Encore protects your applications, code, and data\nlang: platform\n---\n\n_Last updated: March 3, 2026_\n\nYour applications, code, and data are among your most important assets. Security is foundational to everything we build at Encore — it is embedded in our architecture, our processes, and our culture. This document provides a comprehensive overview of the security controls and practices we have in place today, structured around the SOC 2 trust service criteria.\n\n### Security at a glance\n\n| Area | What we do |\n| --- | --- |\n| **Infrastructure** | Hosted on GCP (ISO 27001 / SOC 2 certified). All servers are private, accessible only via VPN. |\n| **Encryption** | AES-256 at rest, TLS 1.2+ and WireGuard in transit. Customer secrets additionally encrypted via GCP KMS. |\n| **Zero-trust networking** | All server-to-server communication is authenticated and end-to-end encrypted via Tailscale / WireGuard. |\n| **Access control** | Principle of least privilege, MFA enforced, regular access reviews, VPN-only infrastructure access. |\n| **Authentication** | Managed by Clerk (SOC 2 certified). Passwordless by default — Encore never stores or handles user passwords. |\n| **Monitoring & alerting** | 24/7 monitoring via Grafana, Sentry, Cronitor, and GCP Cloud Monitoring (all SOC 2 certified). |\n| **Vendor security** | All critical vendors are SOC 2 and/or ISO 27001 certified (GCP, Tailscale, Clerk, GitHub, Sentry, Grafana). |\n| **Code quality** | Mandatory code review, CI/CD with automated testing, automated vulnerability scanning. |\n| **Data privacy** | GDPR compliant. Data minimization by design. |\n| **Responsible disclosure** | Active bug bounty program for security researchers. |\n\n## SOC 2\n\nSOC is short for \"System and Organization Controls\" — it is the de facto industry standard for software security and privacy. We have implemented controls aligned with the SOC 2 framework and are currently preparing for a formal SOC 2 Type 1 audit, during which an external auditor will verify that our controls meet the standard.\n\nAfter the Type 1 audit, we plan to proceed to Type 2, which involves continuous monitoring over an extended period.\n\n### Trust Service Criteria\n\nThe five SOC 2 trust service criteria are: security, availability, confidentiality, processing integrity, and privacy.\n\n**1\\. Security**\n\nProtecting systems against unauthorized access.\n\n**2\\. Availability**\n\nEnsuring that the system remains functional and usable.\n\n**3\\. Confidentiality**\n\nRestricting the access of data to a specified set of persons or organizations. Ensuring that network communication is encrypted and cannot be intercepted by unauthorized personnel.\n\n**4\\. Processing integrity**\n\nEnsuring that a system fulfills its purpose and delivers correct data.\n\n**5\\. Privacy**\n\nMinimal processing and use of personal data in accordance with the law.\n\nThe following sections describe in detail how Encore implements each trust service principle.\n\n## Security\n\nWe believe security is achieved through proven best practices and industry standards — not through obscurity or homegrown cryptography. Our approach is defense in depth: multiple overlapping layers of protection so that the compromise of any single layer does not result in a breach.\n\nWe have a designated Security Officer responsible for all aspects of security across infrastructure, software, and data.\n\n### Infrastructure security\n\nEncore's core production infrastructure is hosted on GCP (Google Cloud Platform), an ISO27001/SOC 2 compliant vendor. Auxiliary services are provided by Hetzner, an ISO27001 compliant vendor. Tailscale, a SOC 2 compliant vendor, provides VPN (Virtual Private Network) services used to secure communication between all servers.\n\nAll core data processing is carried out in the US East region (us-east-1), and backups are kept in multiple separate regions in the US. Each region is composed of at least three \"availability zones\" (AZs) which are isolated locations, designed to take over in case of a catastrophic failure at one location. AZs are separated by a significant distance such that it is unlikely that they are affected by the same issues such as power outages, earthquakes, etc. Physical access to GCP is restricted by GCP's security controls. Furthermore, GCP monitors and immediately responds to power, temperature, fire, water leaks, etc.\n\nAccess to Encore's production infrastructure is restricted to Encore employees. All systems have access controls and only a limited number of employees have privileged access. Access is only possible through a VPN over Tailscale.\n\nThe production environment is separated from testing environments, using separate accounts and VPCs (Virtual Private Cloud) in GCP. This ensures that any defect in a test environment cannot impact the production system. The connection to the internet is controlled by dedicated gateways.\n\n### Organizational security\n\nAn organization is only as strong as its people. All employees undergo a rigorous selection process, and many of Encore's team members bring extensive experience from regulated environments such as online banking and large-scale payment systems.\n\nEmployees are required to complete annual security awareness training covering physical security, digital hygiene (strong passwords, two-factor authentication), social engineering (\"phishing\"), and related topics. Individual performance is reviewed on a bi-weekly cadence, and organizational performance is tracked via KPIs reviewed monthly by management.\n\nEncore employment policy mandates full-disk encryption on all employee devices.\n\n### Product security\n\nMultiple layers of protection ensure that customer data is not accessible to unauthorized persons.\n\nEncore's service-based architecture provides natural isolation between components, and we have adopted a zero-trust security model with Tailscale. All server-to-server communication is authenticated and end-to-end encrypted with WireGuard. GCP's VPC (Virtual Private Cloud) provides another layer of isolation from the internet on the network level. None of Encore's servers are publicly accessible on the internet.\n\nAs a general principle, all of Encore's data is encrypted while being transported across networks and when stored (\"in transit and at rest\"). In case of unauthorized access to the data, an attacker would only see undecipherable garbage which cannot be decrypted without the corresponding keys. The encryption methods employed by Encore are industry standard and deemed unbreakable by contemporary standards. Data at rest (virtual filesystems, relational databases, and object storage) is encrypted using GCP's industry-standard AES-256, while data in transit is encrypted with TLS ≥ 1.2 (for Encore's REST API) or WireGuard (for internal communication).\n\nAll customer secret information is further encrypted using GCP's Key Management Service (KMS). Any access to encrypted data by Encore employees requires elevated access and approval by multiple parties, and all such activity is audited.\n\nUser account authentication is provided by _Clerk_, a SOC 2 compliant vendor.\n\nThere are two ways for a user to log in to Encore: Single sign-on (SSO) and username plus password. Single sign-on can be used by organizations to fully manage access to Encore and, for example, ensure that former employees no longer have access after the offboarding period. Encore supports Google and GitHub SSO using OAuth.\n\nIf no SSO is used, the default login method is passwordless login using email and \"magic link\", also handled by _Clerk_. Encore does not store or in any way handle passwords, neither in plaintext nor cryptographic hash form. This means that Encore does not know the passwords of any users, and no passwords can be reconstructed from our databases.\n\nEncore uses automated vulnerability scanning across its codebase and dependencies. All teams continuously monitor their services for vulnerabilities and proactively remediate them, supervised by the Security Officer.\n\nAll security issues undergo a triaging process by the Security Officer and are escalated based on criticality.\n\n### Responsible disclosure\n\nWe maintain an active bug bounty program to encourage security researchers to report vulnerabilities before they can be exploited. If you discover a security issue, please report it to [security@encore.dev](mailto:security@encore.dev). We are committed to investigating all reports promptly and working with researchers to resolve issues responsibly.\n\n### Access control\n\nWe regularly keep track of and review the list of employees who have access to which systems and remove access where applicable to ensure least access principles apply.\n\nOffboarding processes ensure that former employees cannot access internal systems anymore after the termination of their contract. Thanks to the VPN, Encore can centrally restrict access to internal networks.\n\n#### MFA\n\nMulti-factor authentication (MFA) adds another layer of security on top of classic password authentication. In addition to username and password, the user requires another individual token of access.\n\nStealing or guessing the password is not enough for an attacker to gain access to a system, because the second factor would also need to be stolen.\n\nUsually, the second factor is a physical device, such as a mobile phone which has been paired with the authentication system. Encore employs MFA to protect access to the infrastructure provider (GCP) and the version control systems (GitHub), among other systems.\n\n## Availability\n\nHosted on a cloud infrastructure, Encore implements a service-based architecture where many dedicated software components operate isolated from one another, but in a coordinated way, much like a complex machine where individual parts can be replaced independently from one another.\n\nDuring the release of a new version of Encore services, Encore's engineers take great care during the preparation of the update so that in case of an unexpected problem, the system can be restored to the previous state in a manner that minimizes user impact.\n\n### Performance monitoring\n\nEncore uses a number of performance monitoring systems, such as Sentry, Cronitor, Grafana, and Google Cloud Monitoring (all being SOC 2 compliant vendors). Grafana is used to monitor application performance, such as server response times and user interface speed. Grafana also collects server-side metrics like CPU and RAM usage. Additionally, Encore monitors the performance of databases with GCP tooling.\n\nSlack, a SOC 2 compliant vendor, is used as the alerting channel to notify the developers in case the performance of the system has regressed, for example, due to increased response times, or increased error rates. To enable root cause analysis of bugs, Encore collects system logs from all parts of the system. These logs can only be accessed by authorized users.\n\nEncore offers a public \"Status page\" where users and customers can find the current status of Encore systems. It is available at: [https://status.encore.dev/](https://status.encore.dev/).\n\n### Backups and disaster recovery\n\nTo reduce the risk of simultaneous failure, Encore backs up data to multiple US regions in GCP, with very limited access. Relational databases are backed up on a daily schedule.\n\nEncore is currently planning a rehearsal of disaster recovery in Q4 of 2026. In this exercise, a clone of the production environment will be recovered from scratch using backups and tested for soundness.\n\n### Incident handling\n\nWhenever an incident occurs, Encore's designated on-call engineer initiates an investigation and escalates to the broader engineering team as necessary based on severity. For issues deemed critical, they follow an iterative response process to identify and contain errors, recover systems and services, and remediate vulnerabilities.\n\nCustomers and users can report outages via regular support channels (for example via email, or using the [Discord](https://encore.dev/discord) chat group). Encore's internal communication systems have dedicated channels for incident escalation.\n\n## Confidentiality\n\nWhen you use Encore, other users won't be able to see your content, unless you grant access explicitly by inviting them to your application. Encore engineers may use your data to provide support and when necessary to fix bugs.\n\n### Access controls\n\nAll employees and contractors are contractually bound to confidentiality, which persists after the termination of the work contract.\n\nAs part of a \"clean screen\" policy, all computers used by Encore staff must be set to automatically lock the screen after 1 minute of inactivity.\n\nAll systems access is subject to the \"principle of least privilege\", meaning that every employee only has access to the systems necessary to perform their official duties.\n\n### Deletion\n\nUser data will be stored by Encore after the termination of a subscription term, according to [Encore's Terms of Service](https://encore.cloud/legal/terms). When a user requests the deletion of data, the data is made inaccessible or physically deleted, depending on the data type and storage location.  For technical reasons, data may remain in backups after this point.\n\n## Processing integrity\n\n### Quality assurance\n\nProduct quality is very important to Encore. There are many different facets, including:\n\n-   Accuracy and usability of services provided\n\n-   High performance of the user interface and Encore services\n\n-   Almost zero downtime\n\nSeveral measures are put into place in order to keep product quality high:\n\n-   Code review: Code changes are reviewed by a peer of the developer before it is accepted into the main code branch (for critical systems) or in a weekly post-hoc review process (for auxiliary systems). For critical systems code can only be merged if the reviewer agrees. For auxiliary systems any requested changes by reviewers are made as part of the review process. It is often necessary to add a test alongside, and the code review process ensures that this has been done as well.\n\n-   Continuous integration: Before code is accepted, it is built by our continuous integration environment and tests are executed. If the build fails, the developer is notified immediately and a fix is required before the code can be merged.\n\n-   Manual testing: Once the code has been merged, the change is deployed and in most cases tested manually post-release to verify quality in the production environment.\n\n-   Automated integration testing: A large battery of automated tests is executed against the local and production environments and checks many common workflows for regressions of any kind. In case the tests fail, the engineer will address the issues before proceeding with attempting to merge again.\n\n-   Testing of Open-Source libraries: Encore uses Open-Source libraries to provide certain functionality. Overnight tests run daily to discover potential issues, and manual testing is performed when any Open-Source libraries are version updated.\n\nAny code change is released only if all these steps succeed. Furthermore, access to the code base is protected via multi-factor authentication (MFA), which poses another layer of defense against the malicious injection of code.\n\nSince Encore depends on third-party software, we regularly contribute to the quality assurance of our suppliers. Whenever Encore becomes aware of regressions or bugs, they are reported upstream. In this way, Encore is contributing to the quality, stability, and accuracy of other software in the space.\n\n### Process monitoring\n\nWhere possible, Encore uses software to enforce processes. For example, code review and having tests passed are enforced by the source control management tool GitHub.\n\nRegular reviews on different levels (individual, team, company) foster alignment between all individuals and the company objectives.\n\n### Privacy\n\nEncore takes data privacy very seriously and complies with the rules of the European Union's GDPR (General Data Protection Regulation). GDPR grants a wide range of rights to Encore's users, such as the right to be informed, the right to access, the right to rectification, the right to erasure, and others.\n\nOne fundamental rule of the GDPR is the principle of \"data minimization\", which ensures that we are not processing more personal data than necessary. As a result, Encore Cloud uses only minimal personal data for user authentication and essential communication (a name and contact email). As described above, Encore does not store or handle passwords in any form.\n\n### Privacy policy\n\nWe are aware that confidential handling of your data is essential to establishing trust. Therefore, [Encore's Privacy Policy](https://encore.cloud/legal/privacy) ensures that the data of our users is protected according to the high standards of GDPR.\n\n## Questions and further information\n\nWe are committed to transparency in our security practices. If you have questions about our security or compliance posture, or would like to request additional documentation for your vendor review process, please contact us at [hello@encore.dev](mailto:hello@encore.dev).\n"
  },
  {
    "path": "docs/platform/management/permissions.md",
    "content": "---\nseotitle: Roles & Permissions\nseodesc: Encore helps your whole team build applications and collaborate across backend and frontend teams.\ntitle: Roles & Permissions\nsubtitle: For teams building applications together\nlang: platform\n---\n\nEncore applications have three membership roles with different permissions: **Admins**, **Members**, and **Viewers**.\nHere is a breakdown of the key differences between each role:\n\n|                                     | Admins | Members | Viewers |\n| ----------------------------------- | ------ | ------- | ------- |\n| Manage team members                 | Y      | N       | N       |\n| Connect/Disconnect cloud accounts   | Y      | N       | N       |\n| Integrate with GitHub               | Y      | N       | N       |\n| Configure custom domains            | Y      | N       | N       |\n| Manage environments                 | Y      | N       | N       |\n| Create auth keys                    | Y      | N       | N       |\n| Approve infrastructure provisioning | Y      | N       | N       |\n| Delete applications                 | Y      | N       | N       |\n| Push code changes                   | Y      | Y       | N       |\n| Create builds & deployments         | Y      | Y       | N       |\n| Configure secrets                   | Y      | Y       | N       |\n| Pull secrets                        | Y      | Y       | Y       |\n| Run locally                         | Y      | Y       | Y       |\n| View API documentation              | Y      | Y       | Y       |\n| View Encore Flow                    | Y      | Y       | Y       |\n\n\n### Admins\n\nAdmins have full privileges and can administer your entire application.\n\n### Members\n\nMembers are active contributors to your applications. They are able to do everything that is not limited to Admins.\n  \n### Viewers\n\nViewers are read-only members. They can view the Cloud Dashboard and run your application locally.\n\nThis role is intended for any team members not contributing directly to your Encore application, but who still get value from certain access. In a bigger team, this role is often appropriate for e.g. Frontend developers and Product Managers.\n\n### Custom roles & permissions\n\nCustom roles & permissions is an optional add-on to the [Pro plan](/pricing), please [contact us](mailto:hello@encore.dev) to discuss your requirements.\n"
  },
  {
    "path": "docs/platform/management/telemetry.md",
    "content": "---\nseotitle: Encore Telemetry\nseodesc: Encore collects telemetry data about app usage\ntitle: Telemetry\nlang: platform\n---\nTelemetry helps us improve the Encore by collecting usage data. This data provides insights into how Encore is used, enabling us to make informed decisions to enhance performance, add new features, and fix bugs more efficiently.\n\nEncore only collects telemetry data in the local development tools and in the Encore Cloud dashboard. It does **not** collect any telemetry data from your running applications or cloud services, ensuring complete privacy and security for your operations.\n\n## Why We Collect Data\n\nWe collect telemetry data for several important reasons:\n\n1. **Improvement of Features**: Understanding which features are most used helps us prioritize improvements and new feature development.\n2. **Performance Monitoring**: Tracking performance metrics enables us to identify and resolve issues, ensuring a smoother user experience.\n3. **Bug Detection**: Telemetry data can help us detect and fix bugs faster by providing context on how and when issues occur.\n4. **User Experience**: Insights from telemetry data guide us in making Encore more intuitive and user-friendly.\n\n## How Data is Collected\n\nEncore collects data in a way that prioritizes user privacy and security. Here's how we do it:\n\n1. **User Identifiable Data**: The data collected includes identifiable information that helps us understand specific user interactions and contexts.\n2. **Types of Data**: We collect data on usage patterns, performance metrics, and error reports.\n3. **Secure Transmission**: All data is transmitted securely using industry-standard encryption protocols.\n4. **Minimal Impact**: Data collection is designed to have minimal impact on Encore's performance.\n\n### Example of Data Being Sent\n\nHere is an example of the type of data that is sent:\n\n```json\n{\n    \"event\": \"app.create\",\n    \"anonymousId\": \"a-uuid-unique-for-the-installation\",\n    \"properties\": {\n        \"error\": false,\n        \"lang\": \"go\",\n        \"template\": \"graphql\"\n    }\n}\n```\n\n## Data We Don't Collect\n\nAt Encore, we prioritize your privacy and ensure that no sensitive data is collected through our telemetry. Specifically, we do not collect:\n\n1. **Environment Variables**: We do not collect any environment variables set in your development or production environments.\n2. **File Paths**: The specific paths of your files and directories are not collected.\n3. **Contents of Files**: We do not access or collect the contents of your code files or any other files in your projects.\n4. **Logs**: No log files from your application or development environment are collected.\n5. **Serialized Errors**: We do not collect serialized errors that may contain sensitive information.\n\nOur goal is to gather useful data that helps improve Encore while ensuring that your sensitive information remains private and secure.\n\n## Disabling Telemetry\n\nWhile telemetry helps us improve Encore, we understand that some users may prefer to opt out. Disabling telemetry is straightforward and can be done in two ways:\n\n1. **Using the CLI Command**: You can disable telemetry by executing a simple command in your terminal.\n\n   ```sh\n   encore telemetry disable\n   ```\n\n2. **Setting an Environment Variable**: Alternatively, you can disable telemetry by setting the `DISABLE_ENCORE_TELEMETRY` environment variable.\n\n   ```sh\n   export DISABLE_ENCORE_TELEMETRY=1\n   ```\n\n3. **Confirmation**: After disabling telemetry, either by the CLI command or environment variable, you will receive a confirmation message indicating that telemetry has been successfully disabled.\n\n4. **Re-enabling Telemetry**: If you decide to re-enable telemetry later, you can do so with the following CLI command:\n\n   ```sh\n   encore telemetry enable\n   ```\n\n## Debugging Telemetry\n\nFor users who want more visibility into what telemetry data is being sent, you can enable debug mode:\n\n1. **Setting Debug Mode**: Enable debug mode by setting the `ENCORE_TELEMETRY_DEBUG` environment variable.\n\n   ```sh\n   export ENCORE_TELEMETRY_DEBUG=1\n   ```\n\n2. **Log Statements**: When debug mode is enabled, a log statement prepended by `[telemetry]` will be printed every time telemetry data is sent.\n\n## Conclusion\n\nTelemetry is a vital tool for improving Encore, but we respect your choice regarding data sharing. With easy-to-use commands and environment variables, you can manage your telemetry settings as you see fit. If you have any further questions or need assistance, please refer to our support documentation or contact our support team.\n\nThank you for helping us make Encore better!\n"
  },
  {
    "path": "docs/platform/management/usage.md",
    "content": "---\nseotitle: Usage limits and Fair Use guidelines\nseodesc: Encore comes with a built-in development cloud with generous Fair Use limits. This makes it easy to get started building your next backend application without requiring a cloud account.\ntitle: Usage limits\nsubtitle: Encore Cloud limits and Fair Use guidelines\nlang: platform\n---\n\nEncore comes with a built-in development cloud, Encore Cloud, that is free to use for development and limited scale commercial projects without any specific SLA requirements.\nEncore Cloud is subject to Fair Use guidelines and comes without warranty, as it's not intended for large-scale business-critical use cases.\n\nFor production use cases, Encore is designed to be used together with your cloud on the major cloud providers (AWS/GCP), and provides full DevOps automation for deployments to your own cloud account. This means Encore has no incentive to increase your usage – rather we can focus on building tools to help you minimize your cloud spending! (Should you wish to use Encore Cloud instead of your own cloud account, please [contact us](/book).)\n\nWhen you use Encore together with AWS/GCP, you can still use Encore Cloud to host Preview Environments and development environments.\n\n### Examples of Fair Use\n\n- Prototyping & development\n- Hobby projects\n- Commercial use cases that have limited load and do not require any SLAs\n\n### Never Fair Use\n\n- Proxies and VPNs\n- Media hosting for hot-linking\n- Scrapers\n- Crypto Mining\n- CPU-intensive APIs (e.g.: Machine Learning)\n- Load Testing\n\n## Usage guidelines\n\nWe expect most users to fall within the usage limits below.\nWe want to be as flexible and permissive as possible, and will wherever possible reach out and work with you\nto find a good solution should we notice that you are exceeding these limits.\n\nFor users on a [paid plan](/pricing), we can change these limits to support your needs. If you have significantly higher requirements,\nthis may come with an additional charge (at cost) to cover the extra capacity. Please [contact us](/book) for more information.\n\n**We will never charge you for usage of Encore Cloud unless expressly agreed in advance.**\n\n### Usage limits\n\n|                  | Per application |\n| ---------------- | --------------- |\n| Requests         | 100,000 / day   |\n| Database Storage | 1 GB            |\n| PubSub Messages  | 100,000 / day   |\n| Cron Jobs        | Once every hour |\n| Object Storage   | 1 GB            |\n\n### What happens if I reach a usage limit?\n\nIf your application reaches a usage limit, we will **not** automatically stop it. Our team will reach out to you, and work with you to find a solution that does not cause any undue disruption to your application."
  },
  {
    "path": "docs/platform/migration/migrate-away.md",
    "content": "---\ntitle: Migrate away from Encore\nsubtitle: If you love someone, set them free.\nlang: platform\n---\n\n_We realize most people read this page before even trying Encore, so we start with a perspective on how you might reason about adopting Encore. Read on to see what tools are available for migrating away._\n\nPicking technologies for your project is an important decision. It's tricky because you don't know what the requirements are going to look like in the future. This uncertainty makes many teams opt for maximum flexibility, often without acknowledging this has a significant negative effect on productivity.\n\nWhen designing Encore, we've leaned on standardization to provide a well-integrated and highly productive development workflow. The design is based on the core team's experience building scalable distributed systems at Spotify and Google, complemented with loads of invaluable input from the developer community. \n\nIn practise Encore is opinionated only in certain areas which are critical for enabling the static analysis used to create Encore's application model. This is fundamental to how Encore can provide its powerful features, like automatically instrumenting distributed tracing, and provisioning and managing cloud infrastructure.\n\n## Accommodating for your unique requirements\n\nMany software projects end up having a few novel requirements, which are highly specific to the problem domain. To accommodate for this, Encore is designed to let you go outside of the standardized Backend Framework when you need to, for example:\n- You can drop down in abstraction level in the API framework using [raw endpoints](/docs/ts/primitives/defining-apis#raw-endpoints)\n- You can use tools like the [Terraform provider](/docs/platform/integrations/terraform) to integrate infrastructure that is not managed by Encore\n\n## Mitigating risk through Open Source and efficiency\n\nWe believe that adopting Encore is a low-risk decision for several reasons:\n\n- There's no upfront investment needed to get the benefits\n- Encore apps are normal programs where less than 1% of the code is Encore-specific\n- All infrastructure and data is in your own cloud\n- It's simple to integrate with cloud services and systems not natively supported by Encore\n- Everything you need to develop your application is Open Source, including the [parser](https://github.com/encoredev/encore/tree/main/v2/parser), [compiler](https://github.com/encoredev/encore/tree/main/v2/compiler), [runtime](https://github.com/encoredev/encore/tree/main/runtimes)\n- Everything you need to self-host your application is [Open Source and documented](/docs/ts/self-host/build)\n\n## What to expect when migrating away\n\nIf you want to migrate away, we want to ensure this is as smooth as possible! Here are some of the ways Encore is designed to keep your app portable, with minimized lock-in, and the tools provided to aid in migrating away.\n\n### Code changes\n\nBuilding with Encore doesn't require writing your entire application in an Encore-specific way. Encore applications are normal programs where only 1% of the code is specific to Encore's Open Source Backend Framework.\n\nThis means that the changes required to stop using the Backend Framework is almost exactly the same work you would have needed to do if you hadn't used Encore in the first place, e.g. writing infrastructure boilerplate. There is no added migration cost.\n\n### Deployment\n\nIf you are self-hosting your application, then you're already done.\n\nIf you are using Encore Cloud to manage deployments and want to migrate to your own solution, you can use the `encore build docker` command to produce a Docker image, containing the compiled application, using exactly the same code path as Encore's CI system to ensure compatibility.\n\nLearn more in the [self-hosting docs](/docs/ts/self-host/build).\n\n### Tell us what you need\n\nWe're engineers ourselves and we understand the importance of not being constrained by a single technology.\n\nWe're working every single day on making it even easier to start, <i>and stop</i>, using Encore.\nIf you have specific concerns, questions, or requirements, we'd love to hear from you!\n\nPlease reach out on [Discord](https://encore.dev/discord) or [send an email](mailto:hello@encore.dev) with your thoughts.\n"
  },
  {
    "path": "docs/platform/migration/migrate-to-encore.md",
    "content": "---\ntitle: Migrating an existing system to Encore\nsubtitle: Approaches for adopting Encore\nseotitle: How to migrate your existing system to Encore\nseodesc: Learn how to migrate your application to Encore incrementally, and unlock Encore's powerful set of development tools for your team.\nlang: platform\n---\n\nBy building your application with the Encore open-source framework, you unlock powerful features such as the [local development tools](/docs/ts/observability/dev-dash), [automatic infrastructure provisioning](/docs/platform/infrastructure/infra), [distributed tracing](/docs/ts/observability/tracing), and [service catalog](/docs/ts/observability/service-catalog).\n\n**The good news: you don't need a complete rewrite.** This guide shows you how to adopt Encore incrementally, so you can start benefiting immediately while gradually migrating your existing system.\n\n## Why incremental migration?\n\nIncremental migration is more reliable than a complete rewrite. Here's why:\n\n- **Immediate value** - Start benefiting from Encore's features when developing your next new service.\n- **Lower risk** - Small, controlled changes instead of a single high-stakes big-bang launch.\n- **Ship faster** - Deliver improvements incrementally rather than waiting for a complete rewrite.\n\n## Choose your migration strategy\n\nWe recommend two approaches:\n\n1. **Service by service** (Recommended) - Migrate services one at a time. Run Encore alongside your legacy system, integrated via APIs.\n2. **Forklift migration** - Move your entire application in one shot using a catch-all handler, then refactor incrementally.\n\n<img src=\"/assets/docs/migration-diagram.png\" title=\"Migration options\" className=\"noshadow\"/>\n\n### Need help?\n\nWe've helped 100+ teams adopt Encore and we're happy to answer your questions and provide advice to help you with your migration.\n\n[Email us](mailto:hello@encore.dev) to ask questions, or [book a 1:1 call](https://encore.dev/book) to discuss your specific situation.\n\n**Enterprise customers**: Encore Cloud can adapt to your unique infrastructure—Kubernetes clusters, VPCs, security policies, and compliance needs—typically within days. [Contact us](https://encore.dev/book) to discuss your requirements.\n\n## Service by service migration (Recommended)\n\nMigrate services one at a time while your Encore application runs alongside your legacy system, integrated through APIs.\n\n### Key benefits\n\n- **Full Encore features immediately** - Get automatic infrastructure provisioning, distributed tracing, and architecture diagrams for each migrated service.\n- **Independent services** - Each service is self-contained with no cross-application dependencies.\n- **Simple integration** - Services communicate via APIs.\n- **Flexible deployment** - Deploy to your existing Kubernetes cluster, or let Encore Cloud set up a new project in your cloud (AWS/GCP).\n- **Better developer experience** - Start building with modern tooling right away.\n\n### Deployment options\n\nChoose how to deploy your Encore application:\n\n- **Your Kubernetes cluster** - Deploy directly to your existing Kubernetes infrastructure. Run Encore alongside legacy systems securely in the same environment.\n- **Encore-managed in your cloud account** - Let Encore handle all infrastructure provisioning and management in your AWS or GCP account, and deploy within your existing VPC and security setup.\n\n**Enterprise**: We can adapt to your specific network topology, security policies, and compliance requirements — typically within days. [Contact us](https://encore.dev/book) to discuss your requirements.\n\n_Google Cloud example architecture:_\n\n<img src=\"/assets/docs/gcp-diagram.png\" title=\"Migration options\" className=\"noshadow\"/>\n\n### Which services to migrate first?\n\nStart small and build confidence:\n\n- **Low-risk, high-value** - Validate the approach before tackling complex systems.\n- **Frequently changed** - Get immediate developer experience benefits where it matters most.\n- **Clear boundaries** - Services with well-defined APIs are easier to migrate.\n- **Fewer dependencies** - Less connected to legacy infrastructure means simpler migration.\n\n### Practical steps\n\n#### 1. Create an Encore app and integrate with GitHub\n\nThe first step in any project is to create an Encore app. If you've not tried Encore before, we recommend starting by following the [Quick Start Guide](/docs/ts/quick-start).\n\nOnce you've created you app, [integrate it with your GitHub repository](/docs/platform/integrations/github) and you'll get automatic [Preview Environments](/docs/platform/deploy/preview-environments) for every Pull Request.\n\n#### 2. Build your services and APIs\n\nSince Encore is designed to build distributed systems, it should be straightforward to build a new system that integrates with your existing backend through APIs. See the [defining APIs documentation](/docs/ts/primitives/defining-apis) for more details.\n\nShould you want to accept webhooks, that's simple to do using Encore's [Raw endpoints](/docs/ts/primitives/raw-endpoints).\n\nYou can also generate API clients in several languages, which makes it simple to integrate with frontends or other systems. See the [Client Generation documentation](/docs/ts/cli/client-generation) for more details.\n\n#### 3. Deploy alongside your existing backend\n\n**Deploy to Kubernetes**\n\nEncore Cloud can deploy directly to your existing Kubernetes cluster:\n\n- Run in the same secure environment as your legacy systems\n- Services communicate within your existing VPC\n- Gradually shift traffic using your load balancer or service mesh\n- Use your current cost management and billing\n- Maintain compliance with your governance policies\n\n[Contact us](https://encore.dev/book) to discuss Kubernetes deployment.\n\n**Deploy to new Encore-managed infrastructure in your cloud**\n\n[Connect your AWS or GCP account](/docs/platform/deploy/own-cloud) to deploy in your existing environment:\n\n- Same VPC as your legacy backend (or a new one)\n- Your current cost management and billing\n- Maintain compliance with your governance policies\n\nSee [infrastructure docs](/docs/platform/infrastructure/infra#production-infrastructure) for details.\n\n#### Integration patterns\n\nYour Encore and legacy systems communicate through APIs:\n\n- **Legacy → Encore**: Use Encore-generated API clients\n- **Encore → Legacy**: Use your existing API communication protocol (Encore is not opinionated)\n- **Authentication**: Choose to deploy an authentication gateway in front of Encore or implement authentication directly in your Encore app\n- **Events**: Use Encore's built-in Pub/Sub support for loose coupling\n\n#### 4. Expand your migration\n\nContinue migrating services incrementally. Strategies to consider:\n\n- **Related services**: Migrate services that interact frequently to maximize tracing benefits\n- **High-churn areas**: Move frequently changed services first\n- **New features**: Build new functionality in Encore from the start\n- **Critical paths**: Once confident, migrate business-critical services\n\n## Forklift migration using a catch-all handler\n\nShould you prefer, you can use a forklift migration strategy to move your entire application to Encore in one step by wrapping your existing HTTP router in a catch-all handler.\n\n### When to consider this approach\n\nThis strategy works well when:\n\n- Your existing system is a monolith or smaller distributed system\n- The codebase relies primarily on infrastructure primitives supported by Encore (microservices, databases, pub/sub, caching, object storage, cron jobs, and secrets)\n- You want to quickly consolidate everything in one place\n- You're prepared to incrementally refactor to unlock full Encore features like tracing and automatic API documentation\n\n### Trade-offs\n\n**Benefits:**\n\n- **Quick consolidation**: Get everything in one place from the start.\n- **Immediate access to core features**: Quickly use Encore's CI/CD system, secrets manager, and deployment capabilities.\n- **Single codebase**: Simplified development and deployment workflow.\n\n**Limitations:**\n\n- **Limited initial visibility**: Advanced features like [distributed tracing](/docs/ts/observability/tracing) and [architecture diagrams](/docs/ts/observability/encore-flow) require the [Encore application model](/docs/ts/concepts/application-model) and won't work immediately.\n- **Requires refactoring**: You'll need to incrementally break out endpoints to unlock full Encore capabilities.\n- **All-at-once risk**: Unlike service-by-service migration, this is a bigger initial change.\n\n### Practical steps\n\nHere follows a quick summary of the high-level steps of a forklift migration. Find more in-depth instructions in the full [forklift migration guide](/docs/ts/migration/express-migration#forklift-migration-quick-start).\n\n#### 1. Create an app and structure your code\n\nTo start, create an Encore application and copy over the code from your existing repository. In order to run your application with Encore, it needs to follow the expected [application structure](/docs/ts/primitives/app-structure), which involves placing the `encore.app` and `package.json` files in the repository root. This should be straightforward to do with minor modifications.\n\nAs an example, a single service application might look like this on disk:\n\n```\n/my-app\n├── package.json\n├── encore.app\n├──  // ... other project files\n│\n├── encore.service.ts    // defines your service root\n├── api.ts               // API endpoints\n├── db.ts                // Database definition\n```\n\nYou can also have services nested inside a `backend` folder if you prefer.\n\n#### 2. Create a catch-all handler for your HTTP router\n\nNow let's mount your existing HTTP router under a [Raw endpoint](/docs/ts/primitives/raw-endpoints), which is an Encore API endpoint type that gives you access to the underlying HTTP request.\n\nHere's a basic code example:\n\n```ts\nimport { api } from \"encore.dev/api\";\n\nexport const migrationHandler = api.raw(\n  { expose: true, method: \"*\", path: \"/api/*path\" },\n  async (req, resp) => {\n    // pass request to existing router\n  }\n);\n```\n\nBy mounting your existing HTTP router in this way, it will work as a catch-all handler for all HTTP requests and responses. This should make your application deployable through Encore with little refactoring.\n\n#### 3. Iteratively fix remaining compilation errors\n\nExactly what remains to make your application deployable with Encore will depend on your specific app.\nAs you run your app locally, using `encore run`, Encore will parse and compile it, and give you compilation errors to inform what needs to be adjusted.\n\nBy iteratively making adjustments, you should relatively quickly be able to get your application up and running with Encore.\n\n#### 4. Refactor incrementally to unlock Encore features\n\nOnce your application is deployed, gradually break out specific endpoints using Encore's [API declarations](/docs/ts/primitives/defining-apis) and introduce infrastructure declarations using the Encore backend frameworks. This incremental refactoring will:\n\n- Enable Encore to understand your application structure\n- Unlock powerful features like distributed tracing and architecture diagrams\n- Improve observability and debugging capabilities\n- Make your codebase more maintainable and easier to evolve\n\nStart with the most frequently modified endpoints or the most critical user flows to maximize the value of refactoring efforts.\n\n## Conclusion\n\nIncremental migration lets you adopt Encore without the risk of a complete rewrite.\n\n**Service by service migration** is the recommended approach—it gives you Encore's full feature set immediately while running safely alongside your existing systems.\n\n**Enterprise customers** benefit from flexible deployment options, including Kubernetes integration and customization that typically takes just days to set up.\n\n### Have questions?\n\nWe've helped 100+ teams adopt Encore and we're happy to answer your questions and provide advice to help you with your migration.\n\n- [Book a call](/book) to get 1:1 assistance\n- [Email us](mail:hello@encore.dev) to ask questions\n- [Join Discord](https://encore.dev/discord) to discuss with other developers using Encore\n"
  },
  {
    "path": "docs/platform/migration/try-encore.md",
    "content": "---\ntitle: Trying Encore for an existing project\nsubtitle: Extending, Refactoring, and Rebuilding\nseotitle: Trying Encore for an existing project\nseodesc: Learn how to try Encore for your existing backend application using Extending, Refactoring, or Rebuilding, depending on your situation and priorities.\nlang: platform\n---\n\nMaking changes to your backend requires a thoughtful approach and how you best evaluate a new tool, like Encore, depends on your situation and priorities. Here we’ll explore three approaches and introduce the common scenarios and procedures for each:\n- **Extend:** Using Encore to speed up building an independent new system or creating a proof of concept.\n- **Refactor:** Using Encore when refactoring an existing backend to unlock productivity benefits and remove complexity.\n- **Rebuild:** Using Encore when rebuilding an existing application from the ground up, ensuring modern best practices and cloud-portability.\n\n## Extend\nExtending your existing backend best suits teams who are mostly satisfied with their current setup, but are on the lookout for more efficient workflows to cut down delivery times for new projects, or wish to improve the developer experience for ongoing development.\n\n### Use cases\n- Extending an existing application with a new service or system, integrated using APIs.\n- Reducing effort when building a new system in an isolated domain, such as a new product experiment.\n- Tackling an independent project that demands fast delivery times.\n\n### When to consider Encore\nIf your existing setup feels right but you’re curious about Encore, evaluating it in an independent project is the right move.\nFor example when:\n- You want to create a new service or system and deploy it to your cloud in **AWS** or **GCP**, without manual infrastructure setup.\n- You want to try out development tools like [preview environments](/docs/platform/deploy/preview-environments), and [local tracing](/docs/ts/observability/dev-dash), without any manual instrumentation.\n- You want to validate Encore’s workflow and reliability without making changes to existing systems.\n\n### How to adopt Encore when Extending\n- **1. Identify Extension Points:** Decide on an upcoming project or proof of concept, that is relatively independent of your existing application and is appropriate for building as a new service or system.\n- **2. Create New Services:** Develop new services or systems using [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to get off the ground quickly. This lets you try out all Encore features and enables you to design your new system with Encore’s [automatic architecture diagrams](/docs/ts/observability/encore-flow).\n- **3. Integrate via APIs:**  Where relevant, integrate your new system with your existing backend application using APIs. This can be made simpler by using Encore’s [generated API clients](/docs/ts/cli/client-generation).\n- **4. Validate & Iterate:** Deploy the new services to a [cloud environment](/docs/platform/infrastructure/infra), automatically provisioned by Encore, and validate their performance and interoperability. Use Encore’s [distributed tracing](/docs/ts/observability/tracing) to find bugs or performance issues.\n- **5. Connect cloud and Deploy:** When you are satisfied that your application is working as expected, [connect your cloud account](/docs/platform/deploy/own-cloud) (AWS or GCP) and create a production environment for your application. Encore automatically provisions the infrastructure needed using each cloud’s native services, or you can deploy your application into an [existing Kubernetes cluster](/docs/platform/infrastructure/import-kubernetes-cluster).\n\n## Refactor\nRefactoring can serve as a breath of fresh air for your existing code, revitalizing it by optimizing existing structures. In this approach, your goal is to improve on your existing backend application, often focusing on shedding unnecessary complexity and enabling new opportunities.\n\n### Use cases\n- Transforming a **monolith** into **microservices**.\n- Changing system architecture, e.g. moving to an [event-driven architecture](/blog/event-driven-architecture).\n- Cloud migration, e.g. from **AWS** to **GCP**.\n- Changing foundational infrastructure, e.g. migrating to **Kubernetes**.\n- Removing unwanted complexity that’s become engrained as you’ve scaled up quickly.\n\n### When to consider Encore\nYour application is already built using a supported programming language like **Go** or **TypeScript**. and you want to unlock modern development tools like [infrastructure automation](/docs/platform/infrastructure/infra), [preview environments](/docs/platform/deploy/preview-environments), and [distributed tracing](/docs/ts/observability/tracing), with minimal adjustments to your existing backend and no manual setup.\n\n### How to adopt Encore in a Refactor\n- **1. Assess Your Goal:** Start by evaluating what changes you want to make to your existing application, and look for unnecessary complexities and bottlenecks that can be eliminated. Depending on your goal, you can decide if you want to fully implement Encore’s [API declarations](/docs/ts/primitives/defining-apis) or if you prefer to minimize changes by using a catch-all handler on your current router. Keep in mind that in order to use features like the [Service Catalog](/docs/ts/observability/service-catalog), you need to use the API declarations defined in [Encore.ts](/docs/ts) or [Encore.go](/docs/go).\n- **2. Implement Backend Framework:** Start using [Encore.ts](/docs/ts) or [Encore.go](/docs/go) in your application by replacing existing infrastructure configuration and boilerplate. This enables you to use Encore's infrastructure automation and removes the hassle of manual infrastructure setup. **Tip:** [Existing databases can be integrated](/docs/go/primitives/connect-existing-db) so you don’t need to migrate existing data.\n- **3. Resolve compile-time errors:** Encore comes with a parser and compiler that ensures your application correctly implements the Backend Framework. This lets you discover problems at compile time and provides insightful error messages to help you quickly resolve any errors.\n- **4. Test & Iterate:** Test the refactored application to ensure stability and reliability using Encore’s automatically provisioned cloud [environments](/docs/platform/deploy/environments) and [distributed tracing](/docs/ts/observability/tracing) for fast debugging and iteration. If relevant, you can use a [generated client](/docs/ts/cli/client-generation) to integrate with your existing application frontend.\n- **5. Connect cloud and Deploy:** When you are satisfied that your application is working as expected, [connect your cloud account](/docs/platform/deploy/own-cloud) (AWS or GCP) and create a production environment for your application. Encore automatically provisions the infrastructure needed using each cloud’s native services, or you can deploy your application into an [existing Kubernetes cluster](/docs/platform/infrastructure/import-kubernetes-cluster).\n\n## Rebuild\nThe Rebuild strategy is for those who want a fresh start by recreating an application from the ground up. It’s particularly relevant for companies looking to make a bigger change like changing programming language or migrating from legacy self-hosted infrastructure. A full rebuild, although potentially labor-intensive, opens up opportunities to harness the latest cloud services and developer tools like Encore.\n\n### Use cases\n- Changing programming languages to adopt more performant or modern ones for your project.\n- Migrating from legacy self-hosted solutions to scalable cloud providers like **AWS** or **GCP**.\n- Starting fresh by recreating an app from the ground up.\n\n### When to consider Encore\n- You’re intending to use a supported programming language like **Go** or **TypeScript**.\n- You want to leverage the scalability and services of cloud providers like **AWS** or **GCP**, but don’t want to become locked-in to one specific provider. (Encore applications are cloud-portable by default.)\n- You want modern development tools like [infrastructure automation](/docs/platform/infrastructure/infra), [preview environments](/docs/platform/deploy/preview-environments), and [distributed tracing](/docs/ts/observability/tracing), without manual setup or instrumentation.\n\n### How to adopt Encore in a Rebuild\n- **1. Plan & Design:** Start by creating a design, considering the application's core requirements and architecture. Decide on the programming language, keeping in mind Encore's supported languages.\n- **2. Develop from Scratch:** Develop your new application using Encore [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to get up and running quickly in a shared environment using Encore’s built-in development cloud.\n- **3. Test & Iterate:** Test your new application to ensure reliability using Encore’s [distributed tracing](/docs/ts/observability/tracing) for fast debugging and iteration. Use the [generated API clients](/docs/ts/cli/client-generation) to integrate with your application frontend.\n- **4. Connect cloud and Deploy:** When you are satisfied that your application is working as expected, [connect your cloud account](/docs/platform/deploy/own-cloud) (AWS or GCP) and create a production environment for your application. Encore automatically provisions the infrastructure needed using each cloud’s native services, or you can deploy your application into an [existing Kubernetes cluster](/docs/platform/infrastructure/import-kubernetes-cluster).\n\n## Get support adopting Encore\nEach approach has different benefits and is relevant in different scenarios. Which one is right for your team depends on your priorities and existing setup.\n\nWhether it’s expanding your horizons with **Extend**, revitalizing existing structures through **Refactor**, or starting afresh with **Rebuild**, we’re available to support as you explore Encore to unlock improved productivity and developer experience.\n\nIf you'd like to ask questions or get advice about how to get started, we're happy to talk through your project. You can [join Discord](https://encore.dev/discord) to ask questions and meet other Encore developers, or you can also [book a 1:1](/book) with a member of our core team.\n"
  },
  {
    "path": "docs/platform/observability/encore-flow.md",
    "content": "---\nseotitle: Encore Flow automatic microservices architecture diagrams\nseodesc: Visualize your microservices architecture automatically using Encore Flow. Get real-time interactive architecture diagrams for your entire application.\ntitle: Flow Architecture Diagram\nsubtitle: Visualize your cloud microservices architecture\nlang: platform\n---\n\nFlow is a visual tool that gives you an always up-to-date view of your entire system, helping you reason about your\nmicroservices architecture and identify which services depend on each other and how they work together.\n\n## Birds-eye view\n\nHaving access to a zoomed out representation of your system can be invaluable in pretty much all parts of the\ndevelopment cycle. Flow helps you:\n\n* Track down bottlenecks before they grow into big problems.\n* Get new team members onboarded much faster.\n* Pinpoint hot paths in your system, services that might need extra attention.\n\nServices and PubSub topics are represented as boxes, arrows indicate a dependency. In the example below\nthe `login` service has dependencies on the `user` and `authentication` services. Dashed arrows shows publications or\nsubscriptions to a topic. Here, `payment` publishes to the `payment-made` topic and `email` subscribe to it:\n\n<img src=\"/assets/docs/flow-diagram.png\" title=\"Encore Flow - Highlight Dependencies\" />\n\n## Highlight dependencies\n\nHover over a service, or PubSub topic, to instantly reveal the nature and scale of its dependencies.\n\nHere the `login` service and its dependencies are highlighted. We can see that `login` makes queries to the\ndatabase and requests to two of the endpoints from the `user` service as well as requests to one endpoint from\nthe `authentication` service:\n\n<img src=\"/assets/docs/flow-highlight.png\" title=\"Encore Flow - Highlight Dependencies\" />\n\n## Real-time updates\n\nFlow is accessible in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and the [Encore Cloud dashboard](https://app.encore.cloud) for cloud environments.\n\nWhen developing locally, Flow will auto update in real-time to reflect your architecture as you\nmake code changes. This helps you be mindful of important dependencies and makes it clear if you introduce new ones.\n\nFor cloud environments, Flow auto-updates with each deploy.\n\nIn the example below a new subscription on the topic `payment-made` is introduced and then removed in `user` service:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/flow-auto-update.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/platform/observability/metrics.md",
    "content": "---\nseotitle: Monitoring your backend application with custom metrics\nseodesc: See how you can monitor your backend application using Encore.\ntitle: Metrics\nsubtitle: Built-in support for keeping track of key metrics\ninfobox: {\n  title: \"Metrics\",\n  import: \"encore.dev/metrics\",\n}\nlang: platform\n---\n\nHaving easy access to key metrics is a critical part of application observability.\nEncore solves this by providing automatic dashboards of common application-level\nmetrics for each service.\n\nEncore also makes it easy to define custom metrics for your application. Once defined, custom metrics are automatically displayed on metrics page in the Cloud Dashboard.\n\nBy default, Encore also exports metrics data to your cloud provider's built-in monitoring service.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/metricsvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n\n## Defining custom metrics\n\nEncore makes it easy to define custom metrics for your application. Once defined, custom metrics are automatically displayed on the metrics page in the Cloud Dashboard.\n\nFor implementation guides on how to define metrics in your code, see:\n- [Go metrics documentation](/docs/go/observability/metrics)\n- [TypeScript metrics documentation](/docs/ts/observability/metrics)\n\n## Integrations with third party observability services\n\nTo make it easy to use a third party service for monitoring, we're adding direct integrations between Encore and popular observability services. This means you can send your metrics directly to these third party services instead of your cloud provider's monitoring service.\n\n### Grafana Cloud\n\nTo send metrics data to Grafana Cloud, you first need to Add a Grafana Cloud Stack to your application.\n\nOpen your application in the [Encore Cloud dashboard](https://app.encore.cloud), and click on **Settings** in the main navigation.\nThen select **Grafana Cloud** in the settings menu and click on **Add Stack**.\n\n<img width=\"60%\" src=\"/assets/docs/grafanastack.png\" title=\"Add a Grafana Stack\"/>\n\nNext, open the environment **Overview** for the environment you wish to sent metrics from and click on **Settings**.\nThen in the **Sending metrics data** section, select your Grafana Cloud Stack from the drop-down and save.\n\n<img width=\"60%\" src=\"/assets/docs/configstack.png\" title=\"Select Grafana Stack\"/>\n\nThat's it! After your next deploy, Encore will start sending metrics data to your Grafana Cloud Stack.\n\n<Callout type=\"info\">\n\nTo configure Encore to export metrics to Grafana Cloud, create a token with the following steps:\n\n1. In Grafana, navigate to **Administration > Users and access > Cloud access policies**\n2. Click **Create access policy**, select **metrics:read** and **metrics:write** scopes, then click **Create**\n3. On the newly created access policy, click **Add token**, then **Create** to generate the token\n\n</Callout>\n\n### Datadog\n\nTo send metrics data to Datadog, you first need to add a Datadog Account to your application.\n\nOpen your application in the [Encore Cloud dashboard](https://app.encore.cloud), and click on **Settings** in the main navigation.\nThen select **Datadog** in the settings menu and click on **Add Account**.\n\n<img width=\"60%\" src=\"/assets/docs/datadogaccount.png\" title=\"Add a Datadog account\"/>\n\nNext, open the environment **Overview** for the environment you wish to sent metrics from and click on **Settings**.\nThen in the **Sending metrics data** section, select your Datadog Account from the drop-down and save.\n\n<img width=\"60%\" src=\"/assets/docs/configstack.png\" title=\"Select Datadog Account\"/>\n\nThat's it! After your next deploy, Encore will start sending metrics data to your Datadog Account.\n"
  },
  {
    "path": "docs/platform/observability/service-catalog.md",
    "content": "---\nseotitle: Service Catalog & Generated API Docs\nseodesc: See how Encore automatically generates API documentation that always stays up to date and in sync.\ntitle: Service Catalog\nsubtitle: Automatically get a Service Catalog and complete API docs\nlang: platform\n---\n\nAll developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date.\n\nTo solve this, Encore uses the [Encore Application Model](/docs/ts/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve.\n\nThe API docs are available both in your [Local Development Dashboard](/docs/ts/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud).\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/servicecatalogvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/platform/observability/tracing.md",
    "content": "---\nseotitle: Distributed Tracing helps you understand your app\nseodesc: See how to use distributed tracing in your backend application, across multiple services, using Encore.\ntitle: Distributed Tracing\nsubtitle: Track requests across your application and infrastructure\nlang: platform\n---\n\nDistributed systems often have many moving parts, making it difficult to understand what your code is doing and finding the root-cause to bugs. That’s where Tracing comes in. If you haven’t seen it before, it may just about change your life.\n\nTracing is a revolutionary way to gain insight into what your applications are doing. It works by capturing the series of events as they occur during the execution of your code (a “trace”). This works by propagating a trace id between all individual systems, then correlating and joining the information together to present a unified picture of what happened end-to-end.\n\nAs opposed to the labor intensive instrumentation you'd normally need to go through to use tracing, Encore automatically captures traces for your entire application – in all environments. Uniquely, this means you can use tracing even for local development to help debugging and speed up iterations.\n\nYou view traces in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and in the [Encore Cloud dashboard](https://app.encore.cloud) for Production and other environments.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/tracingvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n\n## Encore's tracing is more comprehensive and more performant than all other tools\n\nUnlike other tracing solutions, Encore understands what each trace event is and captures unique insights about each one. This means you get access to more information than ever before:\n\n* Stack traces\n* Structured logging\n* HTTP requests\n* Network connection information\n* API calls\n* Database queries\n* etc.\n\n## Redacting sensitive data\n\nEncore's tracing automatically captures request and response payloads to simplify debugging.\n\nFor cases where this is undesirable, such as for passwords or personally identifiable information (PII), Encore supports redacting fields marked as containing sensitive data.\n\nSee the documentation on [API Schemas](/docs/ts/primitives/defining-apis#sensitive-data) for more information.\n\n## Trace Sampling\n\nTrace sampling lets you control what percentage of traces are recorded and stored. You can configure sampling rates per environment, service, and endpoint, giving you fine-grained control over your tracing volume.\n\n### How sampling works\n\nSampling is determined at the root of the trace. This means if you set an endpoint to sample at 10%, it controls whether a trace is created when that endpoint is called as the initial entry point. If that same endpoint is called as part of an already-ongoing trace (e.g. as an internal service-to-service call), it will always be included in the existing trace regardless of its own sampling rate.\n\nThis design ensures that all traces are complete — you'll never see partial traces with missing spans. Either a trace is sampled in its entirety, or not at all.\n\n### Configuring sampling rates\n\nYou can configure sampling rates in the Encore Cloud dashboard. Sampling can be set at three levels of granularity:\n\n- **Environment level**: Set a default sampling rate for all traces in an environment.\n- **Service level**: Override the environment default for a specific service.\n- **Endpoint level**: Override the service default for a specific endpoint.\n\nMore specific settings take precedence. For example, if your environment is set to sample 100% of traces but a high-traffic endpoint is set to 10%, that endpoint will only generate new traces 10% of the time it's called as the root of a request.\n\n## Trace Budgets\n\nTrace budgets give you full predictability over your tracing costs by letting you set spending limits on a daily and monthly basis. When a budget limit is reached, tracing is paused until the next period begins, ensuring you never receive unexpected charges.\n\n### Included events\n\nEncore Cloud includes a generous amount of tracing events in each plan:\n\n- **Free tier**: 1M trace events per month included.\n- **Pro tier**: 20M trace events per month included.\n\nBeyond the included events, Pro tier usage is billed at **$1.20 per million events**.\n\n### Setting budgets\n\nYou can configure your trace budgets in the Encore Cloud dashboard. By setting daily and monthly limits, you define exactly how much you're willing to spend on tracing. This makes tracing costs fully predictable and prevents any surprises on your bill.\n"
  },
  {
    "path": "docs/platform/other/vs-heroku.md",
    "content": "---\nseotitle: Encore compared to Heroku\nseodesc: See how the Encore Backend Development Platform lets you avoid the lock-in problems of using Heroku.\ntitle: Encore compared to Heroku\nsubtitle: Get the convenience you want — without limitations and lock-in\nlang: platform\n---\n\nIn the early days of the cloud, Heroku was seen as an innovative platform that made deployments and infrastructure management very simple using a Platform as a Service (PaaS) approach. Ultimately, Heroku lost momentum and, as cloud services rapidly evolved in the past decade, the platform didn't manage to provide enough flexibility to support users' needs.\n\nFans of Heroku will recognize much of the same simplicity in Encore's **push to deploy** workflow — the big difference is that **Encore deploys to your own cloud on AWS/GCP**. This means you keep full flexibility to scale your application using battle-tested services from the major cloud providers, and can leverage their full arsenal of thousands of different services.\n\nLet's take a look at how Encore compares to PaaS tools like Heroku:\n\n|                                                      | Encore                   | Heroku                |\n| ---------------------------------------------------- | ------------------------ | --------------------- |\n| **Infrastructure approach?**                         | Infrastructure from Code | Platform as a Service |\n| **Built-in CI/CD?**                                  | ✅︎ Yes                    | ✅︎ Yes                 |\n| **Built-in Preview Environments?**                   | ✅︎ Yes                    | ✅︎ Yes                 |\n| **Built-in local dev environment?**                  | ✅︎ Yes                    | ❌ No                  |\n| **Built-in Distributed Tracing?**                    | ✅︎ Yes                    | ❌ No                  |\n| **Deploys to major cloud providers like AWS & GCP?** | ✅︎ Yes                    | ❌ No                  |\n| **Avoids cloud lock-in?**                            | ✅︎ Yes                    | ❌ No                  |\n| **Supports Kubernetes and custom infra?**            | ✅︎ Yes                    | ❌ No                  |\n| **Infrastructure is Type-Safe?**                     | ✅︎ Yes                    | ❌ No                  |\n| **Charges for hosting?**                             | No                       | Yes                   |\n\n## Encore is the simplest way of accessing the full power and flexibility of the major cloud providers\n\nWith Encore you don't need to be a cloud expert to make full use of the services offered by major cloud providers like AWS and GCP.\n\nYou simply use [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to **declare the infrastructure semantics directly in your application code**, and Encore then [automatically provisions the necessary infrastructure](/docs/platform/infrastructure/infra) in your cloud, and provides a local development environment that matches your cloud environment.\n\nYou get the same, easy to use, \"push to deploy\" workflow that many developers appreciate with Heroku, while still being able to build large-scale distributed systems and event-driven applications deployed to AWS and GCP.\n\n## Encore's local development workflow lets application developers focus\n\nWhen using a PaaS service like Heroku to deploy your application, you're not at all solving for an efficient local development workflow.\n\nThis means, with Heroku, developers need to manually set up and maintain their local environment and observability tools, in order to facilitate local development and testing.\n\nThis can be a major distraction for application developers, because it forces them to spend time learning how to setup and maintain various local versions of cloud infrastructure, e.g. by using Docker Compose. This work is a continuous effort as the system evolves, and becomes more and more complex as the service and infrastructure footprint grows.\n\nAll this effort takes time away from product development and slows down onboarding time for new developers.\n\n**When using Encore, your local and cloud environments are both defined by the same code base: your application code.** This means developers only need to use `encore run` to start their local dev environments. Encore's Open Source CLI takes care of setting up local version of all infrastructure and provides a [local development dashboard](/docs/ts/observability/dev-dash) with built-in observability tools.\n\nThis greatly speeds up development iterations as developers can start using new infrastructure immediately, which makes building new services and event-driven systems extremely efficient.\n\n## Encore provides an end-to-end purpose-built workflow for cloud backend development\n\nEncore does a lot more than just automate infrastructure provisioning and configuration. It's designed as a purpose-built tool for cloud backend development and comes with out-of-the-box tooling for both development and DevOps.\n\n### Encore's built-in developer tools\n- Cross-service type-safety with IDE auto-complete\n- Distributed Tracing\n- Test Tracing\n- Automatic API Documentation\n- Automatic Architecture Diagrams\n- API Client Generation\n- Secrets Management\n- Service/API mocking\n\n### Encore's built-in DevOps tools\n- Automatic Infrastructure provisioning on AWS/GCP\n- Infrastructure Tracking & Approvals workflow\n- Cloud Configuration 2-way sync between Encore and AWS/GCP\n- Automatic least privilege IAM\n- Preview Environments per Pull Request\n- Cost Analytics Dashboard\n- Encore Terraform provider for extending Encore with infrastructure that is not currently part of Encore's Backend Framework\n"
  },
  {
    "path": "docs/platform/other/vs-supabase.md",
    "content": "---\nseotitle: Encore compared to Supabase / Firebase\nseodesc: See how Encore's Backend Development Platform lets you unlock the simplicity of tools like Supabase and Firebase, while maintaining the control and flexibility of building a real backend application.\ntitle: Encore compared to Supabase + Firebase\nsubtitle: Get the simplicity you want — with flexibility and scalability\nlang: platform\n---\n\nSupabase and Firebase are two popular _Backend as a Service_ providers, that provide developers with an easy way to get a database up and running for their applications. They also bundle some built-in services for common use cases like authentication. \n\nThis can be a great way of getting off the ground quickly. But as many developers have come to learn, you risk finding yourself boxed into a corner if you're not in full control of your own backend when new use cases arise.\n\n**Encore is not a _Backend as a Service_, it's a platform _for_ backend development**. It gives you many of the same benefits that Supabase and Firebase offer, like not needing to manually provision your [databases](/docs/ts/primitives/databases) (or any other infrastructure for that matter). The key difference is, **Encore provisions your infrastructure in your own cloud account on AWS/GCP.** This also lets you easily use any cloud service offered by the major cloud providers, and you don't risk being limited by the platform and having to start over from scratch.\n\nLet's take a look at how Encore compares to BaaS platforms like Supabase and Firebase:\n\n|                                                     | Encore                       | Supabase             | Firebase             |\n| --------------------------------------------------- | ---------------------------- | -------------------- | -------------------- |\n| **Approach?**                                       | Backend Development Platform | Backend as a Service | Backend as a Service |\n| **Native PostgreSQL support?**                      | ✅︎ Yes                        | ✅︎ Yes                | ❌ No                 |\n| **Support pgvector for AI use cases?**              | ✅︎ Yes                        | ✅︎ Yes                | ❌ No                 |\n| **Supports major cloud providers like AWS/GCP?**    | ✅︎ Yes                        | ❌ No                 | ✅︎ Yes (GCP only)     |\n| **Supports Microservices?**                         | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Supports Event-Driven systems?**                  | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Supports Kubernetes and custom infra?**           | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Infrastructure is Type-Safe?**                    | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Built-in local dev environment?**                 | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Built-in Preview Environments per Pull Request?** | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Built-in Distributed Tracing?**                   | ✅︎ Yes                        | ❌ No                 | ❌ No                 |\n| **Charges for hosting?**                            | No                           | Yes                  | Yes                  |\n\n## Encore is the simplest way of accessing the full power and flexibility of the major cloud providers\n\nWith Encore you don't need to be a cloud expert to make full use of the services offered by major cloud providers like AWS and GCP.\n\nYou simply use [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to **declare the infrastructure semantics directly in your application code**, and Encore then [automatically provisions the necessary infrastructure](/docs/platform/infrastructure/infra) in your cloud, and provides a local development environment that matches your cloud environment.\n\n### Example: Using PostgreSQL with Encore\n\nHere's an example of how to use [Encore.ts](/docs/ts) or [Encore.go](/docs/go) to define a PostgreSQL database (Go is used in the example, TypeScript support is also available):\n\nTo create a database, import `encore.dev/storage/sqldb` and call `sqldb.NewDatabase`, assigning the result to a package-level variable.\nDatabases must be created from within an [Encore service](/docs/go/primitives/services).\n\nFor example:\n\n```\n-- todo/db.go --\npackage todo\n\n// Create the todo database and assign it to the \"tododb\" variable\nvar tododb = sqldb.NewDatabase(\"todo\", sqldb.DatabaseConfig{\n\tMigrations: \"./migrations\",\n})\n\n// Then, query the database using db.QueryRow, db.Exec, etc.\n-- todo/migrations/1_create_table.up.sql --\nCREATE TABLE todo_item (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  done BOOLEAN NOT NULL DEFAULT false\n  -- etc...\n);\n```\n\nAs seen above, the `sqldb.DatabaseConfig` specifies the directory containing the database migration files,\nwhich is how you define the database schema.\n\nWith this code in place Encore will automatically create the database when starting `encore run` (locally)\nor on the next deployment (in the cloud). Encore automatically injects the appropriate configuration to authenticate\nand connect to the database, so once the application starts up the database is ready to be used.\n\n[Learn more about using databases with Encore](/docs/go/primitives/databases)\n\n## Encore makes it simple to build type-safe event-driven systems\n\nUnlike BaaS platforms like Supabase and Firebase, Encore has extensive support for building microservices backends and event-driven systems.\n\nFor example, Encore lets you [define APIs](/docs/ts/primitives/apis) using regular functions and enables cross-service type-safety with IDE auto-complete when making API calls between services.\n\nWith [Encore.ts](/docs/ts) and [Encore.go](/docs/go), you can build event-driven systems by defining Pub/Sub topics and subscriptions as type-safe objects in your application.\nThis gives you type-safety for Pub/Sub with compilation errors for any type-errors.\n\n## Encore's local development workflow lets application developers focus\n\nWhen using a BaaS service like Supabase to handle your infrastructure, you're not at all solving for local development.\n\nThis means, with Supabase, developers need to manually set up and maintain their local environment in order to facilitate local development and testing.\n\nThis can be a major distraction for application developers, because it forces them to spend time learning how to setup and maintain various local versions of cloud infrastructure, e.g. by using Docker Compose. This work is a continuous effort as the system evolves, and becomes more and more complex as the service and infrastructure footprint grows.\n\nAll this effort takes time away from product development and slows down onboarding time for new developers.\n\n**When using Encore, your local and cloud environments are both defined by the same code base: your application code.** This means developers only need to use `encore run` to start their local dev environments. Encore's Open Source CLI takes care of setting up local version of all infrastructure and provides a [local development dashboard](/docs/ts/observability/dev-dash) with built-in observability tools.\n\nThis greatly speeds up development iterations as developers can start using new infrastructure immediately, which makes building new services and event-driven systems extremely efficient.\n\n## Encore provides an end-to-end purpose-built workflow for cloud backend development\n\nEncore does a lot more than just automate infrastructure provisioning and configuration. It's designed as a purpose-built tool for cloud backend development and comes with out-of-the-box tooling for both development and DevOps.\n\n### Encore's built-in developer tools\n- Cross-service type-safety with IDE auto-complete\n- Distributed Tracing\n- Test Tracing\n- Automatic API Documentation\n- Automatic Architecture Diagrams\n- API Client Generation\n- Secrets Management\n- Service/API mocking\n\n### Encore's built-in DevOps tools\n- Automatic Infrastructure provisioning on AWS/GCP\n- Infrastructure Tracking & Approvals workflow\n- Cloud Configuration 2-way sync between Encore and AWS/GCP\n- Automatic least privilege IAM\n- Preview Environments per Pull Request\n- Cost Analytics Dashboard\n- Encore Terraform provider for extending Encore with infrastructure that is not currently part of Encore's Backend Framework\n"
  },
  {
    "path": "docs/platform/other/vs-terraform.md",
    "content": "---\nseotitle: Encore compared to Terraform and Pulumi\nseodesc: See how Encore's infrastructure from code approach lets you avoid the common pitfalls of infrastructure as code solutions like Terraform and Pulumi.\ntitle: Encore compared to Terraform & Pulumi\nsubtitle: How Encore is different from Infrastructure as Code tools\nlang: platform\n---\n\nThere are many tools designed to overcome the challenges of cloud infrastructure complexity. Terraform and Pulumi are _Infrastructure as Code_ tools that help you provision infrastructure by writing infrastructure configuration files. **Encore uses a fundamentally different approach that lets you declare infrastructure as type-safe objects in your application**.\n\nLet's take a look at how Encore compares to IaC tools like Terraform and Pulumi:\n\n|                                                                     | Encore                   | Terraform              | Pulumi                 |\n| ------------------------------------------------------------------- | ------------------------ | ---------------------- | ---------------------- |\n| **Approach?**                                                       | Infrastructure from Code | Infrastructure as Code | Infrastructure as Code |\n| **Supports major cloud providers like AWS/GCP?**                    | ✅︎ Yes                    | ✅︎ Yes                  | ✅︎ Yes                  |\n| **Supports Kubernetes and custom infra configuration?**             | ✅︎ Yes                    | ✅︎ Yes                  | ✅︎ Yes                  |\n| **Avoid learning a DSL?**                                           | ✅︎ Yes                    | ❌ No                   | ✅︎ Yes                  |\n| **Infrastructure is Type-Safe?**                                    | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n| **Built-in local dev environment?**                                 | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n| **Built-in Preview Environments per Pull Request?**                 | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n| **Built-in Distributed Tracing?**                                   | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n| **Avoid manually writing infra config files?**                      | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n| **Avoid manual maintenance of separate codebase for infra config?** | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n| **Avoid manual effort to keep environments in sync?**               | ✅︎ Yes                    | ❌ No                   | ❌ No                   |\n\n## Encore removes manual effort and maintenance required with IaC\n\nA common challenge with Infrastructure as Code (IaC) is that it takes a lot of manual effort to write. What's worse is, you need to repeat the effort for each new environment, or take a short cut by duplicating your prod environment and creating costly over-provisioned test or staging environments.\n\nWhen you use IaC you also end up with a separate codebase to maintain and keep in sync with your application's actual requirements. The complexity and scope of this problem grows as you introduce more infrastructure and more environments. That means as your system grows, with IaC, you will need to spend more and more time to maintain your infrastructure configuration.\n\n**Encore's _infrastructure from code_ approach means there are no configuration files to maintain**, nor any refactoring to do when changing the underlying infrastructure. Your application code is the source of truth for the semantic infrastructure requirements.\n\nIn practise, you use [Encore.ts](/docs/ts) and [Encore.go](/docs/go) to declare infrastructure as type-safe objects in your application code, and **Encore [automatically provisions the necessary infrastructure](/docs/platform/infrastructure/infra) in all environments.** Including in your own cloud, with support for major cloud providers like AWS/GCP. (This also means your application is cloud-agnostic by default and **you avoid cloud lock-in**.)\n\n## Encore's local development workflow lets application developers focus\n\nWhen using IaC to provision cloud environments, you're not at all solving for local development.\n\nThis means, with Terraform, developers need to manually set up and maintain their local environment to mimic what's running in the cloud, in order to facilitate local development and testing.\n\nThis can be a major distraction for application developers, because it forces them to spend time learning how to setup and maintain various local versions of cloud infrastructure, e.g. by using Docker Compose and NSQ. This work is a continuous effort as the system evolves, and becomes more and more complex as the footprint grows.\n\nAll this effort takes time away from product development and slows down onboarding time for new developers.\n\n**When using Encore, your local and cloud environments are both defined by the same code base: your application code.** This means developers only need to use `encore run` to start their local dev environments. Encore's Open Source CLI takes care of setting up local version of all infrastructure and provides a [local development dashboard](/docs/ts/observability/dev-dash) with built-in observability tools.\n\nThis greatly speeds up development iterations as developers can start using new infrastructure immediately, which makes building new services and event-driven systems extremely efficient.\n\n## Encore ensures your cloud environments are secure by automating IAM\n\nWhen using IaC tools like Terraform, you must always assign explicit permissions using IAM identities and IAM policies. This can be very time consuming when developing a large-scale distributed systems, and when you get it wrong it can lead to glaring security holes or unexpected system behavior.\n\nWhen using Encore, IAM identities and policies are automatically defined according to best practices for least privilege security. This is possible because Encore parses your source code and builds a graph of the logical architecture, it then uses this to define the infrastructure needs. This means Encore knows exactly which services needs access to which infrastructure for your application to function as expected.\n\n## Encore provides an end-to-end purpose-built workflow for cloud backend development\n\nEncore does a lot more than just automate infrastructure provisioning and configuration. It's designed as a purpose-built tool for cloud backend development and comes with out-of-the-box tooling for both development and DevOps.\n\n### Encore's built-in developer tools\n- Cross-service type-safety with IDE auto-complete\n- Distributed Tracing\n- Test Tracing\n- Automatic API Documentation\n- Automatic Architecture Diagrams\n- API Client Generation\n- Secrets Management\n- Service/API mocking\n\n### Encore's built-in DevOps tools\n- Automatic Infrastructure provisioning on AWS/GCP\n- Infrastructure Tracking & Approvals workflow\n- Cloud Configuration 2-way sync between Encore and AWS/GCP\n- Automatic least privilege IAM\n- Preview Environments per Pull Request\n- Cost Analytics Dashboard\n- Encore Terraform provider for extending Encore with infrastructure that is not currently part of Encore's Backend Framework\n"
  },
  {
    "path": "docs/platform/overview.md",
    "content": "---\nseotitle: Encore Cloud Docs\nseodesc: How Encore Cloud Platform helps you reduce DevOps work by 93% by automating infra in your cloud on AWS/GCP.\ntitle: Encore Cloud\nsubtitle: The easiest way to develop and deploy your application to AWS/GCP\ntoc: false\nlang: platform\n---\n\n[Encore Cloud](https://encore.cloud) is a development platform for running production applications in your own AWS or GCP environment.\n\nIt automates infrastructure provisioning, deployments, and operations, while providing built-in observability including distributed tracing, metrics, and logs.\n\nTeams using Encore Cloud report **2-3x** faster development speed and **93%** less time spent on DevOps. See more details in [customer stories](https://encore.cloud/customers).\n\nLearn more about how it works in the [introduction](/docs/platform/introduction).\n\n<div className=\"min-h-72 bg-black p-8 relative overflow-hidden not-prose\">\n    <div className=\"w-[75%] lg:w-[85%]\">\n        <h2 className=\"text-white lead-medium\">Get Started</h2>\n        <div className=\"body-small text-white mt-2\">\n            Sign up and deploy a test application in minutes.\n        </div>\n        <a href=\"/signup\">\n            <Button className=\"mt-4\" kind=\"primary\" section=\"black\">Sign up</Button>\n        </a>\n    </div>\n</div>\n\n<div className=\"mt-6 grid grid-cols-2 gap-6 mobile:grid-cols-1 not-prose\">\n<a className=\"block group relative no-brandient\" href=\"/docs/platform/introduction\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2 relative\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Introduction</h3>\n                <img className=\"-mt-2 h-16 w-16 p-3 noshadow\" src=\"/assets/icons/features/higher.png\" />\n            </div>\n            <div className=\"mt-2\">\n                Learn about the problems Encore Cloud solves and the philosophy behind it.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" target=\"_blank\" href=\"/book\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Book a call</h3>\n                <img className=\"-mt-2 h-16 w-16 noshadow\" src=\"/assets/icons/features/meet.png\" />\n            </div>\n            <div className=\"mt-2\">\n                Speak to one of our experts to figure out if Encore Cloud will work for your project.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" href=\"/discord\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Join Discord</h3>\n                <div className=\"inline-flex w-16 h-16 items-center justify-center\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 -28.5 256 256\">\n                        <path fill=\"#111111\"\n                            d=\"M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z\" />\n                    </svg>\n                </div>\n            </div>\n            <div className=\"mt-2\">\n                Ask questions and get help on Discord.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" href=\"https://github.com/encoredev/encore\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Star on GitHub</h3>\n                <div className=\"inline-flex w-16 h-16 items-center justify-center\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 16 16\" fill=\"#111111\"\n                        stroke=\"none\">\n                        <path fillRule=\"evenodd\"\n                            d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n                    </svg>\n                </div>\n            </div>\n            <div className=\"mt-2\">\n                Get involved and star Encore on GitHub.\n            </div>\n        </div>\n    </a>\n</div>"
  },
  {
    "path": "docs/ts/ai-integration.md",
    "content": "---\nseotitle: Using Encore with AI Tools\nseodesc: Learn how to set up Encore with AI-powered development tools like Cursor and Claude Code to supercharge your backend development workflow.\ntitle: AI Tools Integration\nsubtitle: Supercharge your development with AI-powered coding assistants\nlang: ts\n---\n\nEncore is built for AI-assisted development. Encore-specific rules and [MCP](/docs/ts/ai-integration#mcp-server) integration let AI understand your architecture and generate type-safe code that follows your patterns. Run `encore run` to start your app; Encore provisions local infrastructure automatically.\n\nFor production, [self-host](/docs/ts/self-host/build) or use [Encore Cloud](https://encore.cloud) to provision infrastructure in your own AWS or GCP account.\n\n## What AI Enables\n\nEncore's declarative APIs and infrastructure primitives give AI a clear model to work with. AI can add databases, pub/sub topics, and other resources with built-in guardrails, and use MCP to introspect your app—services, APIs, databases, and traces—so it can suggest accurate, pattern-consistent code.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"https://encore.cloud/assets/docs/claude-skills.mp4\" type=\"video/mp4\" />\n</video>\n\n## Enabling AI for Your Project\n\nThere are two ways to set up AI support:\n\n- [Method 1: Using the CLI](#method-1-using-the-cli) (recommended)\n- [Method 2: Using Encore Skills](#method-2-using-encore-skills)\n\n### Method 1: Using the CLI\n\n**New projects:** When you run `encore app create`, you'll be prompted to select an AI tool. Encore generates the appropriate configuration files for your chosen tool.\n\n<img src=\"/assets/docs/initllm.png\" className=\"noshadow\" />\n\n**Existing projects:** Run `encore llm-rules init` to add AI support:\n\n```bash\nencore llm-rules init\n```\n\nThis prompts you to select a tool and generates the appropriate configuration file (`.cursorrules`, `CLAUDE.md`, etc.).\n\nBoth commands also set up MCP server configuration for tools that support it (Cursor, Claude Code). If you want to set up MCP manually, see [MCP Server](#mcp-server) below.\n\nSupported tools: Cursor, Claude Code, VS Code, AGENTS.md, and Zed.\n\n### Method 2: Using Encore Skills\n\nUse the [Encore skills package](https://github.com/encoredev/skills) which works with Cursor, Claude Code, GitHub Copilot, and 10+ other AI agents:\n\n```bash\nnpx add-skill encoredev/skills\n```\n\nYou can also install specific skills or target specific agents:\n\n```bash\n# List available skills\nnpx add-skill encoredev/skills --list\n\n# Install to specific agents\nnpx add-skill encoredev/skills -a cursor -a claude-code\n```\n\nThe skills package includes a migration skill that can automatically migrate your existing backend to Encore.ts. See the [Migrate using AI agent](/docs/ts/migration/ai-migration) guide to learn more.\n\n## MCP Server\n\nEncore's [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server gives AI agents deep introspection into your application: querying databases, calling APIs, inspecting services, and analyzing traces.\n\n### Start the Server\n\nFrom your Encore app directory:\n\n```bash\nencore mcp start\n```\n\nThis displays connection information. Keep it running while using your AI tools.\n\n### Connect Cursor\n\n**Quick setup:** Use this button (update `your-app-id` to your actual app ID):\n\n<a href=\"https://cursor.com/en/install-mcp?name=encore-mcp&config=eyJjb21tYW5kIjoiZW5jb3JlIG1jcCBydW4gLS1hcHA9eW91ci1hcHAtaWQifQ%3D%3D\"><img src=\"https://cursor.com/deeplink/mcp-install-dark.svg\" alt=\"Add encore-mcp MCP server to Cursor\" height=\"32\" class=\"noshadow\" /></a>\n\n**Manual setup:** Create `.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"encore-mcp\": {\n      \"command\": \"encore\",\n      \"args\": [\"mcp\", \"run\", \"--app=your-app-id\"]\n    }\n  }\n}\n```\n\nFind your app ID in the `encore.app` file or in the [Encore dashboard](https://app.encore.dev).\n\n### Connect Claude Code\n\nFrom your Encore app directory:\n\n```bash\nclaude mcp add --transport stdio encore-mcp -- encore mcp run --app=your-app-id\n```\n\nVerify with `claude mcp list`. You should see `encore-mcp` in the list.\n\n## What AI Can Do\n\nWith Encore skills and MCP connected, AI can:\n\n- **Define infrastructure in code** - AI declares databases, pub/sub, cron jobs, buckets, and other [primitives](/docs/ts/primitives)\n- **Generate type-safe APIs** - code that follows your patterns and passes validation\n- **Understand architecture** - inspect services and how they connect via MCP\n- **Query databases** - introspect schema and data to generate accurate queries\n- **Debug with tracing** - view request traces, timing, and span details to pinpoint issues\n- **Test instantly** - run `encore run` to test with real infrastructure, not mocks\n\n### In Practice\n\n#### Smarter Debugging with Tracing\n\nAI can access Encore's distributed tracing via MCP to debug issues intelligently. Instead of guessing, AI can view actual request traces, analyze timing across services, and inspect span details to pinpoint exactly where things went wrong. This creates a powerful feedback loop: generate code, test it, analyze the traces, and iterate.\n\n#### Database Introspection\n\nAI can query your actual database schema and data via MCP. This means AI understands your real data model and can generate accurate queries, suggest schema changes, and debug data issues by inspecting actual records.\n\n#### Instant Validation with Real Infrastructure\n\nWhen you run `encore run`, Encore provisions real local infrastructure (databases, pub/sub, etc.). AI can generate code and immediately test it against real services, catching issues early and ensuring the code works before you deploy.\n\nExample prompts:\n\n- \"Add an endpoint that publishes to a pub/sub topic, call it and verify in traces\"\n- \"Query the users database and show accounts created in the last week\"\n- \"Create a new service with CRUD endpoints connected to PostgreSQL\"\n\n## Learn More\n\n- [MCP Server Documentation](/docs/ts/cli/mcp) - Complete MCP reference\n- [Encore Skills Repository](https://github.com/encoredev/skills) - Available skills and installation\n- [Quick Start Guide](/docs/ts/quick-start) - Build your first Encore app\n"
  },
  {
    "path": "docs/ts/cli/cli-reference.md",
    "content": "---\nseotitle: Encore CLI Reference\nseodesc: The Encore CLI lets you run your local development environment, create apps, and much more. See all CLI commands in this reference guide.\ntitle: CLI Reference\nsubtitle: The Encore CLI lets you run your local environment and much more.\nlang: ts\n---\n\n## Running\n\n#### Run\n\nRuns your application.\n\n```shell\n$ encore run [--debug] [--watch=true] [--port=4000] [--listen=<addr>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-w, --watch` | Watch for changes and live-reload | `true` |\n| `--listen` | Address to listen on (e.g. `0.0.0.0:4000`) | |\n| `-p, --port` | Port to listen on | `4000` |\n| `--json` | Display logs in JSON format | `false` |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `--color` | Whether to display colorized output | auto-detected |\n| `--redact` | Redact sensitive data in traces when running locally | `false` |\n| `-l, --level` | Minimum log level to display (`trace\\|debug\\|info\\|warn\\|error`) | |\n| `--debug` | Compile for debugging (`enabled\\|break`) | |\n| `--browser` | Open local dev dashboard in browser on startup (`auto\\|never\\|always`) | `auto` |\n\n#### Test\n\nTests your application.\n\nRuns the test script defined in your `package.json`.\n\n```shell\n$ encore test [flags]\n```\n\nAdditional flags recognized by `encore test`:\n\n| Flag | Description |\n| --- | --- |\n| `--codegen-debug` | Dump generated code (for debugging Encore's code generation) |\n| `--prepare` | Prepare for running tests without running them |\n| `--trace` | Write trace information about the parse and compilation process to a file |\n| `--no-color` | Disable colorized output |\n\n#### Check\n\nChecks your application for compile-time errors using Encore's compiler.\n\n```shell\n$ encore check [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `--codegen-debug` | Dump generated code (for debugging Encore's code generation) |\n| `--tests` | Parse tests as well |\n\n#### Exec\n\nRuns executable scripts against the local Encore app.\n\nTakes a command that it will execute with the local Encore app environment setup.\n\n```shell\n$ encore exec -- <command>\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) |\n\n##### Example\n\nRun a database seed script\n\n```shell\n$ encore exec -- npx tsx ./seed.ts\n```\n\n## App\n\nCommands to create and link Encore apps\n\n#### Clone\n\nClone an Encore app to your computer\n\n```shell\n$ encore app clone [app-id] [directory]\n```\n\n#### Create\n\nCreate a new Encore app\n\n```shell\n$ encore app create [name] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `--example` | URL to example code to use | |\n| `-l, --lang` | Programming language to use for the app | |\n| `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool | |\n| `--platform` | Whether to create the app with the Encore Platform | `true` |\n\n#### Init\n\nCreate a new Encore app from an existing repository\n\n```shell\n$ encore app init [name] [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-l, --lang` | Programming language to use for the app |\n\n#### Link\n\nLink an Encore app with the server\n\n```shell\n$ encore app link [app-id] [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-f, --force` | Force link even if the app is already linked |\n\n## Auth\n\nCommands to authenticate with Encore\n\n#### Login\n\nLog in to Encore\n\n```shell\n$ encore auth login [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-k, --auth-key` | Auth Key to use for login |\n\n#### Logout\n\nLogs out the currently logged in user\n\n```shell\n$ encore auth logout\n```\n\n#### Signup\n\nCreate a new Encore account\n\n```shell\n$ encore auth signup\n```\n\n#### Whoami\n\nShow the current logged in user\n\n```shell\n$ encore auth whoami\n```\n\n## Daemon\n\nEncore CLI daemon commands\n\n#### Restart\n\nIf you experience unexpected behavior, try restarting the daemon using:\n\n```shell\n$ encore daemon\n```\n\n#### Env\n\nOutputs Encore environment information\n\n```shell\n$ encore daemon env\n```\n\n## Database Management\n\nDatabase management commands\n\n#### Connect to database via shell\n\nConnects to the database via psql shell\n\nDefaults to connecting to your local environment. Specify --env to connect to another environment.\n\nUse `--test` to connect to databases used for integration testing.\nUse `--shadow` to connect to the shadow database, used for database drift detection when using tools like Prisma.\n\n`--test` and `--shadow` imply `--env=local`.\n\n```shell\n$ encore db shell [DATABASE_NAME] [--env=<name>] [flags]\n```\n\n`encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with.\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `-e, --env` | Environment name to connect to | `local` |\n| `-t, --test` | Connect to the integration test database (implies --env=local) | `false` |\n| `--shadow` | Connect to the shadow database (implies --env=local) | `false` |\n| `--write` | Connect with write privileges | `false` |\n| `--admin` | Connect with admin privileges | `false` |\n| `--superuser` | Connect as a superuser | `false` |\n\n#### Connection URI\n\nOutputs a database connection string. Defaults to connecting to your local environment. Specify --env to connect to another environment.\n\n```shell\n$ encore db conn-uri [<db-name>] [--env=<name>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `-e, --env` | Environment name to connect to | `local` |\n| `-t, --test` | Connect to the integration test database (implies --env=local) | `false` |\n| `--shadow` | Connect to the shadow database (implies --env=local) | `false` |\n| `--write` | Connect with write privileges | `false` |\n| `--admin` | Connect with admin privileges | `false` |\n| `--superuser` | Connect as a superuser | `false` |\n\n#### Proxy\n\nSets up local proxy that forwards any incoming connection to the databases in the specified environment.\n\n```shell\n$ encore db proxy [--env=<name>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `-e, --env` | Environment name to connect to | `local` |\n| `-p, --port` | Port to listen on (defaults to a random port) | `0` |\n| `-t, --test` | Connect to the integration test database (implies --env=local) | `false` |\n| `--shadow` | Connect to the shadow database (implies --env=local) | `false` |\n| `--write` | Connect with write privileges | `false` |\n| `--admin` | Connect with admin privileges | `false` |\n| `--superuser` | Connect as a superuser | `false` |\n\n#### Reset\n\nResets the databases for the given services. Use --all to reset all databases.\n\n```shell\n$ encore db reset <database-names...|--all> [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-n, --namespace` | Namespace to use (defaults to active namespace) | |\n| `--all` | Reset all services in the application | `false` |\n| `-t, --test` | Reset databases in the test cluster instead | `false` |\n| `--shadow` | Reset databases in the shadow cluster instead | `false` |\n\n## Code Generation\n\nCode generation commands\n\n#### Generate client\n\nGenerates an API client for your app. For more information about the generated clients, see [this page](/docs/ts/cli/client-generation).\n\nBy default, `encore gen client` generates the client based on the version of your application currently running in your local environment.\nYou can change this using the `--env` flag and specifying the environment name.\n\nUse `--lang=<lang>` to specify the language. Supported language codes are:\n\n- `go`: A Go client using the net/http package\n- `typescript`: A TypeScript client using the in-browser Fetch API\n- `javascript`: A JavaScript client using the in-browser Fetch API\n- `openapi`: An OpenAPI spec\n\n```shell\n$ encore gen client [<app-id>] [--env=<name>] [--lang=<lang>] [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-l, --lang` | Language to generate code for | |\n| `-o, --output` | Filename to write the generated client code to | |\n| `-e, --env` | Environment to fetch the API for | `local` |\n| `-s, --services` | Names of the services to include in the output | |\n| `-x, --excluded-services` | Names of the services to exclude in the output | |\n| `-t, --tags` | Names of endpoint tags to include in the output | |\n| `--excluded-tags` | Names of endpoint tags to exclude in the output | |\n| `--openapi-exclude-private-endpoints` | Exclude private endpoints from the OpenAPI spec | `false` |\n| `--ts:shared-types` | Import types from ~backend instead of re-generating them | `false` |\n| `--target` | An optional target for the client (`leap`) | |\n\n## Logs\n\nStreams logs from your application\n\n```shell\n$ encore logs [--env=prod] [--json] [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-e, --env` | Environment name to stream logs from (defaults to the primary environment) |\n| `--json` | Whether to print logs in raw JSON format |\n| `-q, --quiet` | Whether to print initial message when the command is waiting for logs |\n\n## Kubernetes\n\nKubernetes management commands\n\n#### Configure\n\nUpdates your kubectl config to point to the Kubernetes cluster(s) for the specified environment\n\n```shell\n$ encore k8s configure --env=ENV_NAME\n```\n\n## Secrets Management\n\nSecret management commands\n\n#### Set\n\nSet a secret value for a specific environment:\n\n```shell\n$ encore secret set --env <env-name> <secret-name>\n```\n\nSet a secret value for an environment type:\n\n```shell\n$ encore secret set --type <types> <secret-name>\n```\n\nWhere `<types>` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`.\n\n**Examples**\n\nEntering a secret directly in terminal:\n\n```shell\n$ encore secret set --type dev MySecret\nEnter secret value: ...\nSuccessfully created secret value for MySecret.\n```\n\nPiping a secret from a file:\n\n```shell\n$ encore secret set --type dev,local MySecret < my-secret.txt\nSuccessfully created secret value for MySecret.\n```\n\nNote that this strips trailing newlines from the secret value.\n\n#### List\n\nLists secrets, optionally for a specific key\n\n```shell\n$ encore secret list [keys...]\n```\n\n#### Delete\n\nDeletes a secret value\n\n```shell\n$ encore secret delete <id>\n```\n\n## Namespaces\n\nManage infrastructure namespaces for isolating local infrastructure. See [Infrastructure Namespaces](/docs/ts/cli/infra-namespaces) for more details.\n\n#### List\n\nList infrastructure namespaces\n\n```shell\n$ encore namespace list [--output=columns|json]\n```\n\n#### Create\n\nCreate a new infrastructure namespace\n\n```shell\n$ encore namespace create NAME\n```\n\n#### Delete\n\nDelete an infrastructure namespace\n\n```shell\n$ encore namespace delete NAME\n```\n\n#### Switch\n\nSwitch to a different infrastructure namespace. Subsequent commands will use the given namespace by default.\n\nUse `-` as the namespace name to switch back to the previously active namespace.\n\n```shell\n$ encore namespace switch [--create] NAME\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-c, --create` | Create the namespace before switching |\n\n## Config\n\nGets or sets configuration values for customizing the behavior of the Encore CLI.\n\nConfiguration options can be set both for individual Encore applications, as well as globally for the local user.\n\n```shell\n$ encore config <key> [<value>] [flags]\n```\n\nWhen running `encore config` within an Encore application, it automatically sets and gets configuration for that application. To set or get global configuration, use the `--global` flag.\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `--all` | View all settings |\n| `--app` | Set the value for the current app |\n| `--global` | Set the value at the global level |\n\n## Telemetry\n\nReports the current telemetry status\n\n```shell\n$ encore telemetry\n```\n\n#### Enable\n\nEnables telemetry reporting\n\n```shell\n$ encore telemetry enable\n```\n\n#### Disable\n\nDisables telemetry reporting\n\n```shell\n$ encore telemetry disable\n```\n\n## MCP\n\nMCP (Model Context Protocol) commands for integrating with AI assistants. See [MCP](/docs/ts/cli/mcp) for more details.\n\n#### Start\n\nStarts an SSE-based MCP session and prints the SSE URL\n\n```shell\n$ encore mcp start [--app=<app-id>]\n```\n\n#### Run\n\nRuns a stdio-based MCP session\n\n```shell\n$ encore mcp run [--app=<app-id>]\n```\n\n## Random\n\nUtilities for generating cryptographically secure random data.\n\n#### UUID\n\nGenerates a random UUID (defaults to version 4)\n\n```shell\n$ encore rand uuid [-1|-4|-6|-7]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-1, --v1` | Generate a version 1 UUID |\n| `-4, --v4` | Generate a version 4 UUID (default) |\n| `-6, --v6` | Generate a version 6 UUID |\n| `-7, --v7` | Generate a version 7 UUID |\n\n#### Bytes\n\nGenerates random bytes and outputs them in the specified format\n\n```shell\n$ encore rand bytes BYTES [-f <format>]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-f, --format` | Output format (`hex\\|base32\\|base32hex\\|base32crockford\\|base64\\|base64url\\|raw`) | `hex` |\n| `--no-padding` | Omit padding characters from base32/base64 output | `false` |\n\n#### Words\n\nGenerates random 4-5 letter words for memorable passphrases\n\n```shell\n$ encore rand words [--sep=SEPARATOR] NUM\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `-s, --sep` | Separator between words | ` ` (space) |\n\n## Deploy\n\nDeploy an Encore app to a cloud environment.\n\nRequires either `--commit` or `--branch` to be specified.\n\n```shell\n$ encore deploy --env=<env-name> (--commit=<sha> | --branch=<name>) [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `--app` | App slug to deploy to (defaults to current app) | |\n| `-e, --env` | Environment to deploy to (required) | |\n| `--commit` | Commit SHA to deploy | |\n| `--branch` | Branch to deploy | |\n| `-f, --format` | Output format (`text\\|json`) | `text` |\n\n## Version\n\nReports the current version of the encore application\n\n```shell\n$ encore version\n```\n\n#### Update\n\nChecks for an update of encore and, if one is available, runs the appropriate command to update it.\n\n```shell\n$ encore version update\n```\n\n## Build\n\nGenerates an image for your app, which can be used to [self-host](/docs/ts/self-host/build) your app.\n\n#### Docker\n\nBuilds a portable Docker image of your Encore application.\n\n```shell\n$ encore build docker IMAGE_TAG [flags]\n```\n\n**Flags**\n\n| Flag | Description | Default |\n| --- | --- | --- |\n| `--base` | Base image to build from | `scratch` |\n| `-p, --push` | Push image to remote repository | `false` |\n| `--cgo` | Enable cgo | `false` |\n| `--config` | Infra configuration file path | |\n| `--skip-config` | Do not read or generate an infra configuration file | `false` |\n| `--services` | Services to include in the image | |\n| `--gateways` | Gateways to include in the image | |\n| `--os` | Target operating system | `linux` |\n| `--arch` | Target architecture (`amd64\\|arm64`) | `amd64` |\n\n## LLM Rules\n\nGenerate LLM rules in an existing app\n\n#### Init\n\nInitialize the LLM rules files\n\n```shell\n$ encore llm-rules init [flags]\n```\n\n**Flags**\n\n| Flag | Description |\n| --- | --- |\n| `-r, --llm-rules` | Initialize the app with LLM rules for a specific tool (`cursor\\|claudecode\\|vscode\\|agentsmd\\|zed`) |\n"
  },
  {
    "path": "docs/ts/cli/client-generation.md",
    "content": "---\nseotitle: Automatic API Client Generation\nseodesc: Learn how you can use automatic API client generation to get clients for your backend. See how to integrate with your frontend using a type-safe generated client.\ntitle: Client Library Generation\nsubtitle: Stop writing the same types everywhere\nlang: ts\n---\n\nEncore makes it simple to write scalable distributed backends by allowing you to make function calls that Encore translates into RPC calls. Encore also generates API clients with interfaces that look like the original Go functions, with the same parameters and response signature as the server.\n\nThe generated clients are single files that use only the standard functionality of the target language, with full type safety. This allow anyone to look at the generated client and understand exactly how it works.\n\nThe structure of the generated code varies by language, to ensure it's idiomatic and easy to use, but always includes all publicly accessible endpoints, data structures, and documentation strings.\n\nEncore currently supports generating the following clients:\n- **Go** - Using `net/http` for the underlying HTTP transport.\n- **TypeScript** - Using the browser `fetch` API for the underlying HTTP client.\n- **JavaScript** - Using the browser `fetch` API for the underlying HTTP client.\n- **OpenAPI** - Using the OpenAPI Specification's language-agnostic interface to HTTP APIs. (Experimental)\n\nIf there's a language you think should be added, please submit a pull request or create a feature\nrequest on [GitHub](https://github.com/encoredev/encore/issues/new), or [reach out on Discord](/discord).\n\n<Callout type=\"important\">\n\nIf you ship the generated client to end customers, keep in mind that old clients will continue to be used after you make changes. To prevent issues with the generated clients, avoid making breaking changes in APIs that your clients access.\n\n</Callout>\n\n<br />\n\n## Generating a Client\n\nTo generate a client, use the `encore gen client` command. It generates a type-safe client using the most recent API metadata\nrunning in a particular environment for the given Encore application. For example:\n\n```shell\n# Generate a TypeScript client for calling the hello-a8bc application based on the primary environment\nencore gen client hello-a8bc --output=./client.ts\n\n# Generate a Go client for the hello-a8bc application based on the locally running code\nencore gen client hello-a8bc --output=./client.go --env=local\n\n# Generate an OpenAPI client for the hello-a8bc application based on the primary environment\nencore gen client hello-a8bc --lang=openapi --output=./openapi.json\n```\n\n### Environment Selection\n\nBy default, `encore gen client` generates the client based on the version of your application currently running in your local environment.\nYou can change this using the `--env` flag and specifying the environment name.\n\n<Callout type=\"info\">\n\nThe generated client can be used with any environment, not just the one it was generated for. However, the APIs, data structures\nand marshalling logic will be based on whatever is present and running in that environment at the point in time the client is generated.\n\n</Callout>\n\n### Service filtering\n\nBy default `encore gen client` outputs code for all services with at least one publicly accessible (or authenticated) API.\nYou can narrow down this set of services by specifying the `--services` (or `-s`) flag. It takes a comma-separated list\nof service names.\n\nFor example, to generate a typescript client for the `email` and `users` services, run:\n```shell\nencore gen client --services=email,users -o client.ts\n```\n\n### Output Mode\n\nBy default the client's code will be output to stdout, allowing you to pipe it into your clipboard, or another tool. However,\nusing `--output` you can specify a file location to write the client to. If output is specified, you do not need to specify\nthe language as Encore will detect the language based on the file extension.\n\n\n### Example Script\nYou could combine this into a `package.json` file for your Typescript frontend, to allow you to run `npm run gen` in that\nproject to update the client to match the code running in your staging environment.\n```json\n{\n  \"scripts\": {\n    // ...\n    \"gen\": \"encore gen client hello-a8bc --output=./client.ts --env=staging\"\n    // ...\n  }\n}\n```\n\n## Using the Client\n\nThe generated client has all the data structures required as parameters or returned as response values as needed by any\nof the public or authenticated API's of your Encore application. Each service is exposed as object on the client, with\neach public or authenticated API exposed as a function on those objects.\n\nFor instance, if you had a service called `email` with a function `Send`, on the generated client you would call this\nusing; `client.email.Send(...)`.\n\n### Creating an instance\n\nWhen constructing a client, you need to pass a `BaseURL` as the first parameter; this is the URL at which the API can\nbe accessed. The client provides two helpers:\n\n- `Local` - This is a constant provided, which will always point at your locally running instance environment.\n- `Environment(\"name\")` - This is a function which allows you to specify an environment by name\n\nHowever, BaseURL is a string, so if the two helpers do not provide enough flexibility you can pass any valid URL to be\nused as the BaseURL.\n\n### Authentication\n\nIf your application has any API's which require [authentication](/docs/ts/develop/auth), then additional options will generated\ninto the client, which can be used when constructing the client. Just like with API's schemas, the data type required by\nyour application's `auth handler` will be part of the client library, allowing you to set it in two ways:\n\nIf your credentials won't change during the lifetime of the client, simply passing the authentication data to the client\nthrough the `WithAuth` (Go) or `auth` (TypeScript) options.\n\nHowever, if the authentication credentials can change, you can also pass a function which will be called before each request\nand can return a new instance of the authentication data structure or return the existing instance.\n\n\n### HTTP Client Override\n\nIf required, you can override the underlying HTTP implementation with your own implementation. This is useful if you want\nto perform logging of the requests being made, or route the traffic over a secured tunnel such as a VPN.\n\nIn Go this can be configured using the `WithHTTPClient` option. You are required to provide an implementation of the\n`HTTPDoer` interface, which the [http.Client](https://pkg.go.dev/net/http#Client) implements. For TypeScript clients,\nthis can be configured using the `fetcher` option and must conform to the same prototype as the browsers inbuilt [fetch\nAPI](https://developer.mozilla.org/en-US/docs/Web/API/fetch).\n\n### Structured Errors\n\nErrors created or wrapped using Encore's [`errs package`](/docs/ts/primitives/errors) will be returned to the client and deserialized\nas an `APIError`, allowing the client to perform adaptive error handling based on the type of error returned. You can perform\na type check on errors caused by calling an API to see if it is an `APIError`, and once cast as an `APIError` you can access\nthe `Code`, `Message` and `Details` fields. For TypeScript Encore generates a `isAPIError` type guard which can be used.\n\nThe `Code` field is an enum with all the possible values generated in the library, alone with description of when we\nwould expect them to be returned by your API. See the [errors documentation](/docs/ts/primitives/errors#error-codes) for\nan online reference of this list.\n"
  },
  {
    "path": "docs/ts/cli/config-reference.md",
    "content": "---\nseotitle: Encore CLI Configuration Options\nseodesc: Configuration options to customize the behavior of the Encore CLI.\ntitle: Configuration Reference\nsubtitle: Configuration options to customize the behavior of the Encore CLI.\nlang: ts\n---\n\n\nThe Encore CLI has a number of configuration options to customize its behavior.\n\nConfiguration options can be set both for individual Encore applications, as well as\nglobally for the local user.\n\nConfiguration options can be set using `encore config <key> <value>`,\nand options can similarly be read using `encore config <key>`.\n\nWhen running `encore config` within an Encore application, it automatically\nsets and gets configuration for that application.\n\nTo set or get global configuration, use the `--global` flag.\n\n## Configuration files\n\nThe configuration is stored in one ore more TOML files on the filesystem.\n\nThe configuration is read from the following files, in order:\n\n### Global configuration\n* `$XDG_CONFIG_HOME/encore/config`\n* `$HOME/.config/encore/config`\n* `$HOME/.encoreconfig`\n\n### Application-specific configuration\n* `$APP_ROOT/.encore/config`\n\nWhere `$APP_ROOT` is the directory containing the `encore.app` file.\n\nThe files are read and merged, in the order defined above, with latter files taking precedence over earlier files.\n\n## Configuration options\n\n#### run.browser\nType: string<br/>\nDefault: auto<br/>\nMust be one of: always, never, or auto\n\nWhether to open the Local Development Dashboard in the browser on `encore run`.\nIf set to \"auto\", the browser will be opened if the dashboard is not already open.\n\n"
  },
  {
    "path": "docs/ts/cli/infra-namespaces.md",
    "content": "---\nseotitle: Infrastructure Namespaces\nseodesc: Learn how Encore's infrastructure namespaces makes it easy to task switch. Stash your infrastructure state and switch to a different task with a single command.\ntitle: Infrastructure Namespaces\nsubtitle: Task switching made easy\nlang: ts\n---\n\nEncore's CLI allows you to create and switch between multiple, independent *infrastructure namespaces*.\nInfrastructure namespaces are isolated from each other, and each namespace contains its own independent data.\n\nThis makes it trivial to switch tasks, confident your old state and data will be waiting for you when you return.\n\nIf you've ever worked on a new feature that involves making changes to the database schema,\nonly to context switch to reviewing a Pull Request and had to reset your database, you know the feeling.\n\nWith Encore's infrastructure namespaces, this is a problem of the past.\nRun `encore namespace switch --create pr:123` (or `encore ns switch -c pr:123` for short) to create and switch to a new namespace.\n\nThe next `encore run` will run in the new namespace, with a completely fresh database.\nWhen you're done, run `encore namespace switch -` to switch back to your previous namespace.\n\n## Usage\n\nBelow are the commands for working with namespaces.\nNote that you can use `encore ns` as a short form for `encore namespace`.\n\n```shell\n# List your namespaces (* indicates the current namespace)\n$ encore namespace list\n\n# Create a new namespace\n$ encore namespace create my-ns\n\n# Switch to a namespace\n$ encore namespace switch my-ns\n\n# Switch to a namespace, creating it if it doesn't exist\n$ encore namespace switch --create my-ns\n\n# Switch to the previous namespace\n$ encore namespace switch -\n\n# Delete a namespace (and all associated data)\n$ encore namespace delete my-ns\n```\n\nMost other Encore commands that interact or use infrastructure take an optional\n`--namespace` (`-n` for short) that overrides the current namespace. If left unspecified,\nthe current namespace is used.\n\nFor example:\n\n```shell\n# Run the app using the \"my-ns\" namespace\n$ encore run --namespace my-ns\n\n# Open a database shell to the \"my-ns\" namespace\n$ encore db shell DATABASE_NAME --namespace my-ns\n\n# Reset all databases within the \"my-ns\" namespace\n$ encore db reset --all --namespace my-ns\n```\n"
  },
  {
    "path": "docs/ts/cli/mcp.md",
    "content": "---\nseotitle: Encore MCP Server\nseodesc: Encore's Model Context Protocol (MCP) server provides deep introspection of your application to AI development tools.\ntitle: MCP Server\nsubtitle: The Model Context Protocol (MCP) exposes tools that provide application context to LLMs.\nlang: ts\n---\n\nEncore provides an MCP server that implements the [Model Context Protocol](https://modelcontextprotocol.io/introduction), an open standard that enables large language models (LLMs) to access contextual information about your application. Think of MCP as a standardized interface—like a \"USB-C port for AI applications\"—that connects your Encore app's data and functionality to any LLM that supports the protocol.\n\nYou can connect to Encore's MCP server from any MCP host (such as Claude Desktop, IDEs, or other AI tools) using either Server-Sent Events (SSE) or stdio transport. To set up this connection, simply run:\n\n```bash\ncd my-encore-app\nencore mcp start\n\n  MCP Service is running!\n\n  MCP SSE URL:        http://localhost:9900/sse?app=your-app-id\n  MCP stdio Command:  encore mcp run --app=your-app-id\n```\n\nCopy the appropriate URL or command to your MCP host's configuration, and you're ready to give your AI assistants rich context about your application.\n\n## Example: Integrating with Cursor\n\n[Cursor](https://cursor.com) is one of the most popular AI powered IDE's, and it's simple to use Encore's MCP server together with Cursor. \n\nIn order to add the Encore MCP server to Cursor, the fastest way is via the button below (make sure to update `your-app-id` in the configuration to your actual Encore app ID).\n\n<a href=\"https://cursor.com/en/install-mcp?name=encore-mcp&config=eyJjb21tYW5kIjoiZW5jb3JlIG1jcCBydW4gLS1hcHA9eW91ci1hcHAtaWQifQ%3D%3D\"><img src=\"https://cursor.com/deeplink/mcp-install-dark.svg\" alt=\"Add encore-mcp MCP server to Cursor\" height=\"32\" class=\"noshadow\" /></a>\n\nIf you prefer to configure it manually, create the file `.cursor/mcp.json` with the following settings:\n\n```json\n{\n    \"mcpServers\": {\n        \"encore-mcp\": {\n             \"command\": \"encore\",\n             \"args\": [\"mcp\", \"run\", \"--app=your-app-id\"]\n        }\n    }\n}\n```\n\nLearn more in [Cursor's MCP docs](https://docs.cursor.com/context/model-context-protocol)\n\nNow when using Cursor's Agent mode, you can ask it to do advanced actions, such as:\n\n\"Add an endpoint that publishes to a pub/sub topic, call it and verify that the publish is in the traces\"\n\n## Command Reference\n\n#### Start\n\nStarts an SSE-based MCP server and displays connection information.\n\n```shell\n$ encore mcp start [--app=<app-id>]\n```\n\n#### Run\n\nEstablishes an stdio-based MCP session. This command is typically used by MCP hosts to communicate with the server through standard input/output streams.\n\n```shell\n$ encore mcp run [--app=<app-id>]\n```\n\n## Exposed Tools\n\nEncore's MCP server exposes the following tools that provide AI models with detailed context about your application. These tools enable LLMs to understand your application's structure, retrieve relevant information, and take actions within your system.\n\n#### Database Tools\n\n- **get_databases**: Retrieve metadata about all SQL databases defined in the application, including their schema, tables, and relationships.\n- **query_database**: Execute SQL queries against one or more databases in the application.\n\n#### API Tools\n\n- **call_endpoint**: Make HTTP requests to any API endpoint in the application.\n- **get_services**: Retrieve comprehensive information about all services and their endpoints in the application.\n- **get_middleware**: Retrieve detailed information about all middleware components in the application.\n- **get_auth_handlers**: Retrieve information about all authentication handlers in the application.\n\n#### Trace Tools\n\n- **get_traces**: Retrieve a list of request traces from the application, including their timing, status, and associated metadata.\n- **get_trace_spans**: Retrieve detailed information about one or more traces, including all spans, timing information, and associated metadata.\n\n#### Source Code Tools\n\n- **get_metadata**: Retrieve the complete application metadata, including service definitions, database schemas, API endpoints, and other infrastructure components.\n- **get_src_files**: Retrieve the contents of one or more source files from the application.\n\n#### PubSub Tools\n\n- **get_pubsub**: Retrieve detailed information about all PubSub topics and their subscriptions in the application.\n\n#### Storage Tools\n\n- **get_storage_buckets**: Retrieve comprehensive information about all storage buckets in the application.\n- **get_objects**: List and retrieve metadata about objects stored in one or more storage buckets.\n\n#### Cache Tools\n\n- **get_cache_keyspaces**: Retrieve comprehensive information about all cache keyspaces in the application.\n\n#### Metrics Tools\n\n- **get_metrics**: Retrieve comprehensive information about all metrics defined in the application.\n\n#### Cron Tools\n\n- **get_cronjobs**: Retrieve detailed information about all scheduled cron jobs in the application.\n\n#### Secret Tools\n\n- **get_secrets**: Retrieve metadata about all secrets used in the application.\n\n#### Documentation Tools\n\n- **search_docs**: Search the Encore documentation using Algolia's search engine.\n- **get_docs**: Retrieve the full content of specific documentation pages.\n\n"
  },
  {
    "path": "docs/ts/cli/telemetry.md",
    "content": "---\nseotitle: Encore Telemetry\nseodesc: Encore collects telemetry data about app usage\ntitle: Telemetry\nlang: ts\n---\nTelemetry helps us improve the Encore by collecting usage data. This data provides insights into how Encore is used, enabling us to make informed decisions to enhance performance, add new features, and fix bugs more efficiently.\n\nEncore only collects telemetry data in the local development tools and the Encore Cloud dashboard. It does **not** collect any telemetry data from your running applications or cloud services, ensuring complete privacy and security for your operations.\n\n## Why We Collect Data\n\nWe collect telemetry data for several important reasons:\n\n1. **Improvement of Features**: Understanding which features are most used helps us prioritize improvements and new feature development.\n2. **Performance Monitoring**: Tracking performance metrics enables us to identify and resolve issues, ensuring a smoother user experience.\n3. **Bug Detection**: Telemetry data can help us detect and fix bugs faster by providing context on how and when issues occur.\n4. **User Experience**: Insights from telemetry data guide us in making Encore more intuitive and user-friendly.\n\n## How Data is Collected\n\nEncore collects data in a way that prioritizes user privacy and security. Here's how we do it:\n\n1. **User Identifiable Data**: The data collected includes identifiable information that helps us understand specific user interactions and contexts.\n2. **Types of Data**: We collect data on usage patterns, performance metrics, and error reports.\n3. **Secure Transmission**: All data is transmitted securely using industry-standard encryption protocols.\n4. **Minimal Impact**: Data collection is designed to have minimal impact on Encore's performance.\n\n### Example of Data Being Sent\n\nHere is an example of the type of data that is sent:\n\n```json\n{\n    \"event\": \"app.create\",\n    \"anonymousId\": \"a-uuid-unique-for-the-installation\",\n    \"properties\": {\n        \"error\": false,\n        \"lang\": \"go\",\n        \"template\": \"graphql\"\n    }\n}\n```\n\n## Data We Don't Collect\n\nAt Encore, we prioritize your privacy and ensure that no sensitive data is collected through our telemetry. Specifically, we do not collect:\n\n1. **Environment Variables**: We do not collect any environment variables set in your development or production environments.\n2. **File Paths**: The specific paths of your files and directories are not collected.\n3. **Contents of Files**: We do not access or collect the contents of your code files or any other files in your projects.\n4. **Logs**: No log files from your application or development environment are collected.\n5. **Serialized Errors**: We do not collect serialized errors that may contain sensitive information.\n\nOur goal is to gather useful data that helps improve Encore while ensuring that your sensitive information remains private and secure.\n\n## Disabling Telemetry\n\nWhile telemetry helps us improve Encore, we understand that some users may prefer to opt out. Disabling telemetry is straightforward and can be done in two ways:\n\n1. **Using the CLI Command**: You can disable telemetry by executing a simple command in your terminal.\n\n   ```sh\n   encore telemetry disable\n   ```\n\n2. **Setting an Environment Variable**: Alternatively, you can disable telemetry by setting the `DISABLE_ENCORE_TELEMETRY` environment variable.\n\n   ```sh\n   export DISABLE_ENCORE_TELEMETRY=1\n   ```\n\n3. **Confirmation**: After disabling telemetry, either by the CLI command or environment variable, you will receive a confirmation message indicating that telemetry has been successfully disabled.\n\n4. **Re-enabling Telemetry**: If you decide to re-enable telemetry later, you can do so with the following CLI command:\n\n   ```sh\n   encore telemetry enable\n   ```\n\n## Debugging Telemetry\n\nFor users who want more visibility into what telemetry data is being sent, you can enable debug mode:\n\n1. **Setting Debug Mode**: Enable debug mode by setting the `ENCORE_TELEMETRY_DEBUG` environment variable.\n\n   ```sh\n   export ENCORE_TELEMETRY_DEBUG=1\n   ```\n\n2. **Log Statements**: When debug mode is enabled, a log statement prepended by `[telemetry]` will be printed every time telemetry data is sent.\n\n## Conclusion\n\nTelemetry is a vital tool for improving Encore, but we respect your choice regarding data sharing. With easy-to-use commands and environment variables, you can manage your telemetry settings as you see fit. If you have any further questions or need assistance, please refer to our support documentation or contact our support team.\n\nThank you for helping us make Encore better!\n"
  },
  {
    "path": "docs/ts/community/contribute.md",
    "content": "---\nseotitle: How to contribute to Encore Open Source Project\nseodesc: Learn how to contribute to the Encore Open Source project by submitting pull requests, reporting bugs, or contributing documentation or example projects.\ntitle: Ways to contribute\nsubtitle: Guidelines for contributing to Encore\nlang: ts\n---\n\nWe’re so excited that you are interested in contributing to Encore! All contributions are welcome, and there are several valuable ways to contribute.\n\n### Open Source Project\n\nIf you want to contribute to the Encore Open Source project, you can submit a pull request on [GitHub](https://github.com/encoredev/encore/pulls).\n\n### Report issues\n\nIf you have run into an issue or think you’ve found a bug, please report it via the [issue tracker](https://github.com/encoredev/encore/issues).\n\n### Add or update docs\n\nIf there’s something you think would be helpful to add to the docs or if there’s something that seems out of date, we appreciate your input.\nYou can view the docs and contribute fixes or improvements directly in [GitHub](https://github.com/encoredev/encore/tree/main/docs).\n\nYou can also email your feedback to us at [hello@encore.dev](mailto:hello@encore.dev).\n\n### Blog posts\n\nIf you’ve built something cool using Encore, we’d really like you to talk about it! We love it when developers share their projects on blogs and on Twitter.\n\nUse the hashtag **#builtwithencore** and we’ll have an easier time finding your work. – We might also showcase it on the [Encore Twitter account](https://twitter.com/encoredotdev)!\n\n### Meetups & Workshops\n\nOrganizing a meetup or workshop is a great way to connect with other developers using Encore. It can also be a great first step in trying out Encore for development in your company or other professional organization.\n\nIf you want help with organizing or planning an event, please don’t hesitate to reach out to us via email at [hello@encore.dev](mailto:hello@encore.dev).\n"
  },
  {
    "path": "docs/ts/community/get-involved.md",
    "content": "---\nseotitle: Encore's Open Source Developer Community\nseodesc: Learn how to engage in the Open Source Developer Community supporting Encore.\ntitle: Community\nsubtitle: Join the most pioneering developer community!\nlang: ts\n---\n\nDevelopers building with Encore are forward-thinkers, who are working on exciting and innovative applications.\n\nWe rely on this group's feedback, and contributions to the Open Source project, to improve Encore for developers everywhere.\nGetting involved is a fantastic way of finding support and inspiration among peers.\n\nEveryone is welcome in the Encore community, and we hope you to get involved too!\n\n## Get involved\n\nThere are many ways to get involved. Here's where you can start straight away.\n\n<p className=\"flex items-center gap-x-2 lead-xsmall\">\n     <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 16 16\" fill=\"#111111\" stroke=\"none\">\n      <path fillRule=\"evenodd\"\n            d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n    </svg>\n    <a href=\"https://github.com/encoredev/encore\">Contribute on GitHub</a>\n</p>\n\nUse GitHub to report bugs, feedback on proposals, or contribute your ideas.\n\n<p className=\"flex items-center gap-x-2 lead-xsmall\">\n   <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 -28.5 256 256\">\n        <path fill=\"#111111\"\n            d=\"M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z\" />\n    </svg>\n    <a href=\"https://encore.dev/discord\">Join Discord</a>\n</p>\n\nConnect with fellow Encore developers, ask questions, or just hang out!\n\n<p className=\"flex items-center gap-x-2 lead-xsmall\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"#111111\" stroke=\"none\">\n        <path d=\"M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z\" />\n    </svg>\n    <a href=\"https://twitter.com/encoredotdev\">Follow on Twitter</a>\n</p>\n\nFollow Encore on Twitter to keep up with the latest. Share what you've built to help spread the word about the project.\n\n### Contribute to the project\n\nWant to make a contribution to Encore? Great, start by reading about the different [ways to contribute](/docs/ts/community/contribute).\n\n### Feedback on the Roadmap\n\n[The Encore Roadmap](https://encore.dev/roadmap) is public. It's open to your comments, feature requests, and you can vote on existing entries.\n\n## Community Governance\n\nWe recommend everyone read the [Community Principles](/docs/ts/community/principles).\n\nIf you need assistance, have concerns, or have questions for the Community team, please email us at [support@encore.dev](mailto:support@encore.dev).\n"
  },
  {
    "path": "docs/ts/community/open-source.md",
    "content": "---\nseotitle: Encore is Open Source\nseodesc: We believe Open Source is key to a sustainable and prosperous technology community. Encore builds on Open Source software, and is itself Open Source.\ntitle: Open Source\nsubtitle: Encore is Open Source Software\nlang: ts\n---\n\nWe believe Open Source is key to a long-term sustainable and prosperous technology community. Encore builds on Open Source software, and is largely Open Source itself.\n\n## License\n\nEncore's Backend Framework, parser, and compiler are Open Source under Mozilla Public License 2.0.\n\n> The MPL is a simple copyleft license. The MPL's \"file-level\" copyleft is designed to encourage contributors to share modifications they make to your code, while still allowing them to combine your code with code under other licenses (open or proprietary) with minimal restrictions.\n\nYou can learn more about MPL 2.0 on [the official website](https://www.mozilla.org/en-US/MPL/2.0/FAQ/).\n\n## Contribute\n\nContributions to improve Encore are very welcome. Contribute to Encore on [GitHub](https://github.com/encoredev/encore).\n"
  },
  {
    "path": "docs/ts/community/principles.md",
    "content": "---\nseotitle: Encore Community Principles\nseodesc: Everyone is welcome in the Encore community, and we want everyone to feel at home and free to contribute.\ntitle: Community principles\nsubtitle: Everyone belongs in the Encore community\nlang: ts\n---\n\nEveryone is welcome in the Encore community, and it is of utmost importance to us that everyone is able to feel at home and contribute.\n\nTherefore we as maintainers, and you as a contributor, must pledge to make participation in our community a harassment-free experience for everyone, regardless of: age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity.\n\n### Code of Conduct\n\nTo this end, the Encore community is guided by the [Contributor Covenant 2.0 Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/) to ensure everyone is welcome and able to participate. "
  },
  {
    "path": "docs/ts/community/submit-template.md",
    "content": "---\nseotitle: Submit a Template to Encore's Templates repo\nseodesc: Learn how to contribute to Encore's Templates repository and get features in the Encore Templates marketplace.\ntitle: Submit a Template\nsubtitle: Your contributions help other developers build\nlang: ts\n---\n\n[Templates](/templates) help and inspire developers to build applications using Encore.\n\nYou are welcome to contribute your own templates!\n\nTwo types of templates that are especially useful:\n- **Starters:** Runnable Encore applications for others to use as is, or take inspiration from.\n- **Bits:** Re-usable code samples to solve common development patterns or integrate Encore applications with third-party APIs and services.\n\n## Submit your contribution\n\nContribute a template by submitting a Pull Request to the [Open Source Examples Repo](https://github.com/encoredev/examples): `https://github.com/encoredev/examples`\n\n### Submitting Starters\n\nFollow these steps to submit a **Starter**:\n\n1. Fork the repo.\n2. Create a new folder in the root directory of the repo, this is where you will place your template. — Use a short folder name as your template will be installable via the CLI, like so: `encore app create APP-NAME --example=<TEMPLATE_FOLDER_NAME>`\n3. Include a `README.md` with instructions for how to use the template. We recommend following [this format](https://github.com/encoredev/examples/blob/8c7e33243f6bfb1b2654839e996e9a924dcd309e/uptime/README.md).\n\nOnce your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website.\n\n### Submitting Bits\n\nFollow these steps to submit your **Bits**:\n\n1. Fork the repo.\n2. Create a new folder inside the `bits` folder in the repo and place your template inside it. Use a short folder name as your template will soon be installable via the CLI.\n3. Include a `README.md` with instructions for how to use the template.\n\nOnce your Pull Request has been approved, it may be featured on the [Templates page](/templates) on the Encore website.\n\n## Contribute from your own repo\n\nIf you don't want to contribute code to the examples repo, but still want to be featured on the [Templates page](/templates), please contact us at [hello@encore.dev](mailto:hello@encore.dev).\n\n## Dynamic Encore AppID\n\nIn most cases, you should avoid hardcoding an `AppID` in your template's source code. Instead, use the notation `{{ENCORE_APP_ID}}`.\n\nWhen a developer creates an app using the template, `{{ENCORE_APP_ID}}` will be dymically replaced with their new and unique `AppID`, meaning they will not need to make any manual code adjustments.\n"
  },
  {
    "path": "docs/ts/concepts/application-model.md",
    "content": "---\nseotitle: Encore Application Model\nseodesc: How Encore understands your application using static analysis\ntitle: Encore Application Model\nsubtitle: How Encore understands your application\nlang: ts\n---\n\nEncore works by using static analysis to understand your application. This is a fancy term for parsing and analyzing the code you write and creating a graph of how your application works. This graph closely represents your own mental model of the system: boxes and arrows that represent systems and services that communicate with other systems, pass data and connect to infrastructure. We call it the Encore Application Model.\n\nBecause the Open Source framework, parser, and compiler, are all designed together, Encore can ensure 100% accuracy when creating the application model. Any deviation is caught as a compilation error.\n\nUsing this model, Encore can provide tools to solve problems that normally would be up to the developer to do manually. From creating architecture diagrams and API documentation to provisioning cloud infrastructure.\n\nWe're continuously expanding on Encore's capabilities and are building a new generation of developer tools that are enabled by Encore's understanding of your application.\n\nThe framework, parser, and compiler that enable this are all [Open Source](https://github.com/encoredev/encore).\n\n<img src=\"/assets/docs/flow-diagram.png\" title=\"Encore Application Model\" className=\"mx-auto md:max-w-lg\"/>\n\n## Standardization brings clarity\n\nDevelopers make dozens of decisions when creating a backend application. Deciding how to structure the codebase, defining API schemas, picking underlying infrastructure, etc. The decisions often come down to personal preferences, not technical rationale. This creates a huge problem in the form of fragmentation! When every stack looks different, all tools have to be general purpose.\n\nWhen you adopt Encore, many of these stylistic decisions are already made for you. The Encore framework ensures your application follows modern best practices. And when you run your application, Encore's Open Source parser and compiler check that you're sticking to the standard. This means you're free to focus your energy on what matters: writing your application's business logic."
  },
  {
    "path": "docs/ts/concepts/benefits.md",
    "content": "---\nseotitle: Benefits of using Encore.ts\nseodesc: Get to know the benefits of using Encore's Backend Framework for TypeScript to build cloud-native backend applications.\ntitle: Benefits of using Encore.ts\nlang: ts\n---\n\n## Integrated developer experience for enhanced productivity\n\n- **Local development with instant infrastructure**: Encore automatically sets up necessary infrastructure as you develop.\n- **Rapid feedback**: Catch issues early with type-safe infrastructure, avoiding slow deployment cycles.\n- **No manual configuration required**: No need for Infrastructure-as-Code. Your code is the single source of truth.\n- **Unified codebase**: One codebase for all environments; local, preview, and cloud.\n- **Cloud-agnostic by default**: Encore.ts provides an abstraction layer on top of the cloud provider's APIs, so you avoid becoming locked in to a single cloud.\n- **Evolve infrastructure without code changes**: As requirements evolve, you can change the provisioned infrastructure without needing application code changes. Either using the Open Source [self-hosting tools](/docs/ts/self-host/build) or with the optional [Cloud Platform](https://encore.dev/use-cases/devops-automation), which fully-automates infrastructure management in your own AWS/GCP account.\n- **AI-assisted development**: Encore is built for AI coding assistants. With [Encore-specific rules and MCP integration](/docs/ts/ai-integration), AI understands your architecture and can generate type-safe, pattern-consistent code and introspect your app—services, APIs, databases, and traces.\n\n## High-performance Rust runtime\n\nTo enable Encore's functionality in TypeScript, we’ve created a high-performance distributed systems runtime in Rust.\nIt integrates with the standard Node.js runtime for executing JavaScript code, ensuring **100% compatibility with the Node.js ecosystem**.\n\nIt provides a number of benefits over standard Node.js:\n- **Handles requests validation, provides API type-safety, has built-in observability, and integrates with databases, Pub/Sub, and more**\n- **9x increased throughput and 85% reduced latency** compared to standard Node.js/Express.js [See benchmarks](https://encore.dev/blog/event-loops)\n- **Zero NPM dependencies** for improved security and faster builds\n\n### How it works\n\nEncore.ts is designed to let the Node.js event loop — which is single-threaded — focus on executing your business logic, while everything else happens in Encore’s multi-threaded Rust runtime. Here's a high-level overview of how this works:\n\n**1. Node.js starts up and initializes the Encore Rust runtime. The Rust runtime then:**\n   - Begins accepting incoming requests\n   - Parses and validates these requests against the API schema\n\n**2. For each request, the Encore Runtime:**\n   - Passes the request to your application code\n   - Waits for your code to process the request\n   - Sends the response back to the client\n\n**3. When your application needs to interact with infrastructure (like databases or PubSub):**\n   - It delegates these tasks to the Rust runtime\n   - The Rust runtime handles these operations more efficiently than Node.js would, providing faster execution and lower latency\n\n## Enhanced type-safety for distributed systems\n\nEncore leverages static code analysis to parse the API schema and TypeScript types you define. This enables a number of features:\n- Built-in [local development dashboard](/docs/ts/observability/dev-dash)\n- API Explorer, automatic documentation, and local tracing\n- Runtime type-safety, automatically validating incoming requests against the API schema\n- Eliminating runtime errors due to missing required fields\n\n## No DevOps experience required\n\nEncore provides open source tools to help you integrate with your cloud infrastructure, enabling you to self-host your application anywhere that supports Docker containers.\nLearn more in the [self-host documentation](/docs/ts/self-host/build).\n\nYou can also use [Encore Cloud](https://encore.dev/use-cases/devops-automation), which fully automates provisioning and managing infrastructure in your own cloud on AWS and GCP.\n\nThis approach dramatically reduces the level of DevOps expertise required to use scalable, production-ready, cloud services like Kubernetes and Pub/Sub. And because your application code is the source of truth for infrastructure requirements, it ensures the infrastructure in all environments is always in sync with the application's current requirements.\n\n## Simplicity without giving up flexibility\n\nEncore.ts provides integrations for common infrastructure primitives, but also allows for flexibility.\n\nFor example, you can always use any cloud infrastructure, even if it's not built into the Encore.ts framework. You can use any database, message broker, or other service that your application needs, just set up the infrastructure and then reference it in your code as you would do traditionally.\n\nIf you use [Encore Cloud](https://encore.dev/use-cases/devops-automation), it will [automate infrastructure](/docs/platform/infrastructure/infra) using your own cloud account, so you always have full access to your services from the cloud provider's console.\n"
  },
  {
    "path": "docs/ts/concepts/hello-world.md",
    "content": "---\nseotitle: Hello World in Encore.ts\nseodesc: Get to know Encore.ts with this simple Hello World example.\ntitle: Hello World\nsubtitle: Get to know the basics\ntoc: false\nlang: ts\n---\n\nEncore lets you easily define type-safe, idiomatic TypeScript API endpoints.\nIt's done in a fully declarative way, enabling Encore to automatically parse and validate the incoming request and ensure it matches the schema, with zero boilerplate.\n\nTo define an API, use the `api` function from the `encore.dev/api` module to wrap a regular TypeScript async function that receives the request data as input and returns response data. This tells Encore that the function is an API endpoint. Encore will then automatically generate the necessary boilerplate at compile-time.\n\nThis means you need less than 10 lines of code to define a production-ready deployable service and API endpoint:\n\n```TypeScript\nimport { api } from \"encore.dev/api\";\n\nexport const get = api(\n  { expose: true, method: \"GET\", path: \"/hello/:name\" },\n  async ({ name }: { name: string }): Promise<Response> => {\n    const msg = `Hello ${name}!`;\n    return { message: msg };\n  }\n);\n\ninterface Response {\n  message: string;\n}\n```\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/hello-world\" \n    desc=\"Hello World REST API example application.\" \n/>\n\n## Getting started video\n\nGet to know the basics of Encore.ts in this getting started video.\n\n<iframe width=\"360\" height=\"202\" src=\"https://www.youtube.com/embed/wiLDz-JUuqY?si=BxmW0BV1hx2LIvtO\" title=\"Getting Started with Encore.ts\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen></iframe>\n\n## Using databases, Pub/Sub, and other primitives\n\nEncore's Backend Framework makes it simple to add more primitives, such as additional microservices, databases, Pub/Sub, etc.\nSee how to use each primitive:\n\n- [Services](/docs/ts/primitives/services)\n- [APIs](/docs/ts/primitives/defining-apis)\n- [Databases](/docs/ts/primitives/databases)\n- [Cron Jobs](/docs/ts/primitives/cron-jobs)\n- [Pub/Sub & Queues](/docs/ts/primitives/pubsub)\n- [Secrets](/docs/ts/primitives/secrets)\n"
  },
  {
    "path": "docs/ts/develop/auth.md",
    "content": "---\nseotitle: Adding authentication to APIs to auth users\nseodesc: Learn how to add authentication to your APIs and make sure you know who's calling your backend APIs.\ntitle: Authenticating users\nsubtitle: Knowing what's what and who's who\ninfobox: {\n  title: \"Authentication\",\n  import: \"encore.dev/auth\",\n}\nlang: ts\n---\nAlmost every application needs to know who's calling it, whether the user\nrepresents a person in a consumer-facing app or an organization in a B2B app.\nEncore supports both use cases in a simple yet powerful way.\n\nAs described in the docs for [defining APIs](/docs/ts/primitives/defining-apis),\neach API endpoint can be marked as requiring authentication, using the option `auth: true`\nwhen defining the endpoint.\n\n\n## Authentication Handlers\n\nWhen an API is defined with `auth: true`, you must define an authentication handler\nin your application. The authentication handler is responsible for inspecting incoming\nrequests to determine what user is authenticated (if any), and computing any other associated\nauthentication information.\n\nThe authentication handler is defined similarly to API endpoints, using the `authHandler`\nfunction imported from `encore.dev/auth`.\n\nLike API endpoints, the authentication handler defines what request information it's interested in,\nin the form of HTTP headers, query strings, or cookies.\n\nA simple authentication handler that inspects the `Authorization` header might look like this:\n\n```ts\nimport { Header, Gateway } from \"encore.dev/api\";\nimport { authHandler } from \"encore.dev/auth\";\n\n// AuthParams specifies the incoming request information\n// the auth handler is interested in. In this case it only\n// cares about requests that contain the `Authorization` header.\ninterface AuthParams {\n    authorization: Header<\"Authorization\">;\n}\n\n// The AuthData specifies the information about the authenticated user\n// that the auth handler makes available.\ninterface AuthData {\n    userID: string;\n}\n\n// The auth handler itself.\nexport const auth = authHandler<AuthParams, AuthData>(\n    async (params) => {\n        // TODO: Look up information about the user based on the authorization header.\n        return {userID: \"my-user-id\"};\n    }\n)\n\n// Define the API Gateway that will execute the auth handler:\nexport const gateway = new Gateway({\n    authHandler: auth,\n})\n```\n\nWith this in place, Encore will provision an API Gateway that will process\nincoming requests to your application, and whenever a request contains\nan `Authorization` header it will first call the authentication handler to\nresolve information about the user.\n\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/clerk\"\n    desc=\"Example application showing an auth handler implementation using Clerk.\"\n/>\n\n### Rejecting authentication\n\nIf the auth handler returns an `AuthData` object, Encore will consider the request\nauthenticated. To instead _reject_ the request, throw an exception. To signal that\nthe credentials are not valid, throw an `APIError` with code `Unauthenticated`.\n\nFor example:\n\n```ts\nimport { APIError } from \"encore.dev/api\";\n\nexport const auth = authHandler<AuthParams, AuthData>(\n    async (params) => {\n        throw APIError.unauthenticated(\"bad credentials\");\n    }\n)\n```\n\n## Understanding the Authentication Process\n\nEncore's authentication process proceeds in two steps:\n\n1. Determine if the request is authenticated\n2. Call the endpoint, if permissible\n\n#### Step 1: Determining if the request is authenticated\n\nWhenever an incoming request contains any of the authentication parameters (defined by the auth handler),\nEncore's API Gateway calls the auth handler to resolve the authentication data.\n\nThis happens regardless of the endpoint the request is for. Importantly, it happens even\nwhen calling an endpoint that does not require authentication.\n\nThere are three possible outcomes from calling the auth handler:\n\n1. If the auth handler succeeds, by returning `AuthData`, the request is considered authenticated.\n\n2. If the auth handler throws an `APIError` with code `Unauthenticated`, the request is considered unauthenticated,\n   exactly as if there was no authentication parameters in the request to begin with.\n\n3. If the auth handler throws any other exception, the API Gateway aborts the request and returns the error to the caller.\n\nFinally, if the request does not contain authentication data, the request is considered unauthenticated.\n\n#### Step 2: Calling the endpoint, if permissible\n\nOnce the API Gateway has determined whether the request is authenticated, it checks whether the API Endpoint\nbeing called requires authentication data.\n\nIf it does require authentication, and the request is not authenticated,\nthe API Gateway aborts the request and returns an \"unauthenticated\" error to the caller.\n\nIn all other situations, the API Gateway proceeds by calling the target endpoint.\n\nIf the request was successfully authenticated, the authentication data is passed along to the endpoint,\nregardless of whether the endpoint requires authentication or not.\n\n## Using auth data\n\nIf a request has been successfully authenticated, the API Gateway forwards the authentication data\nto the target endpoint. The endpoint can query the available auth data from the `getAuthData` function,\navailable from the `~encore/auth` module.\n\nThis module is dynamically generated by Encore to enable type-safe resolution of the auth data.\n\n### Propagating auth data\n\nEncore automatically propagates the auth data when you make API calls to other Encore API endpoints\nusing the generated `~encore/clients` package.\n\n<Callout type=\"info\">\n\nIf an endpoint calls another endpoint during its processing, and the target endpoint\nrequires authentication while the original request does not have any authentication data,\nthe API call will fail with error code `Unauthenticated`.\n\nThis behavior preserves the guarantee that endpoints that\nrequire authentication always have valid authentication data present.\n\n</Callout>\n\n## Overriding auth information\n\nYou can override the auth data for a specific endpoint when calling it via `~encore/clients` by passing `CallOpts`. Example:\n\n```ts\nimport { svc } from \"~encore/clients\";\n\nconst resp = await svc.endpoint(params, { authData: { userID: \"...\", userEmail: \"...\" } });\n```\n\n<Callout type=\"info\">\n\nOverriding auth data is useful for testing endpoints that require authentication without having to\nauthenticate the request manually.\n\n</Callout>\n\n## Mocking auth\n\nYou can mock `getAuthData` with vitest. Example:\n\n```ts\nimport { describe, expect, test, vi } from \"vitest\";\nimport * as auth from \"~encore/auth\";\nimport { get } from \"./hello\";\n\n\ndescribe(\"get\", () => {\n  test(\"should combine string with parameter value\", async () => {\n    const spy = vi.spyOn(auth, 'getAuthData');\n    spy.mockImplementation(() => ({ userEmail: \"user@email.com\" }))\n\n    const resp = await get({ name: \"world\" });\n    expect(resp.message).toBe(\"Hello world! You are authenticated with user@email.com\");\n  });\n});\n```\n"
  },
  {
    "path": "docs/ts/develop/debug.md",
    "content": "---\nseotitle: How to debug your TS backend application\nseodesc: Learn how to debug your TS backend application using Encore.\ntitle: Debug with your IDE\nlang: ts\n---\n\nEncore makes it easy to debug your application using your favorite IDE. \n\n## Enable debugging mode\nNext, run your Encore application with `encore run --debug=break`. This will cause Encore to run your app with the `--inspect-brk` flag, which will pause your application until a debugger is attached. Encore will print the URL to the terminal, which you will use to attach your debugger:\n\n```shell\n$ encore run --debug=break\n  Your API is running at:     http://127.0.0.1:4000\n  Development Dashboard URL:  http://localhost:9400/ai-chat-ts-qhwi\n  Process ID:                 38965\n\nDebugger listening on ws://127.0.0.1:9229/473dd95f-e71e-4bf2-9eda-6132dd0d6ae3\n```\n\n(Your process id and url will differ).\n\nIf you don't want the application to pause on startup, you can use `encore run --debug` instead. This will start the application and wait for a debugger to attach, but it won't pause the application until the debugger is attached.\n\n## Attach your debugger\nWhen your Encore application is running, it’s time to attach the debugger. The instructions differ depending on how you would like to debug. If instructions for your editor aren’t listed below, consult your editor for information on how to attach a debugger to a running process.\n\n### Visual Studio Code\nTo debug with VS Code you must first add a debug configuration. Press `Run -> Add Configuration`, choose `Node.js -> Attach`. The generated config should look something like this:\n\n```json\n{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Attach\",\n      \"port\": 9229,\n      \"request\": \"attach\",\n      \"skipFiles\": [\n        \"<node_internals>/**\"\n      ],\n      \"type\": \"node\"\n    }\n  ]\n}\n```\n\nNext, open the **Run and Debug** menu in the toolbar on the left, select Attach (the configuration you just created), and then press the green arrow.\n\nThat’s it! You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect.\n\n## WebStorm\nTo debug with WebStorm (or any other JetBrains IDE), you must first configure a Node.js Attach configuration. Press `Run -> Edit Configurations`, click the `+` button, and choose `Attach to Node.js/Chrome`. Give it a name and hit `OK`. \nNow select the configuration you just created and press the green bug.\n\nThat's it. You should be able to set breakpoints and have the Encore application pause when they’re hit like you would expect.\n\n"
  },
  {
    "path": "docs/ts/develop/env-vars.md",
    "content": "---\nseotitle: Environment Variables Reference\nseodesc: Learn how to configure Encore's development environment using environment variables.\ntitle: Environment Variables\nsubtitle: Configure your development environment\nlang: ts\n---\n\nEncore works out of the box without configuration, but provides several environment variables for advanced use cases such as debugging, testing, or adapting Encore to specific workflow requirements.\n\n## Daemon & Development Dashboard\n\nThese variables control how the Encore daemon operates and where it exposes its services.\n\n### ENCORE_DAEMON_LOG_PATH\n\nControls the location of the Encore daemon log file.\n\n**Default:** `<user_cache_dir>/encore/daemon.log`\n\n**Example:**\n\n```bash\nexport ENCORE_DAEMON_LOG_PATH=/var/log/encore/daemon.log\n```\n\n### ENCORE_DEVDASH_LISTEN_ADDR\n\nOverrides the listen address for the local development dashboard.\n\n**Default:** Automatically assigned by the daemon\n\n**Format:** Network address (e.g., `localhost:9400`)\n\n**Example:**\n\n```bash\nexport ENCORE_DEVDASH_LISTEN_ADDR=localhost:8080\nencore run\n```\n\n### ENCORE_MCPSSE_LISTEN_ADDR\n\nOverrides the listen address for the MCP SSE (Model Context Protocol Server-Sent Events) endpoint.\n\n**Default:** Automatically assigned by the daemon\n\n**Format:** Network address\n\n**Example:**\n\n```bash\nexport ENCORE_MCPSSE_LISTEN_ADDR=localhost:9401\n```\n\n### ENCORE_OBJECTSTORAGE_LISTEN_ADDR\n\nOverrides the listen address for the object storage service endpoint.\n\n**Default:** Automatically assigned by the daemon\n\n**Format:** Network address\n\n**Example:**\n\n```bash\nexport ENCORE_OBJECTSTORAGE_LISTEN_ADDR=localhost:9402\n```\n\n## Logging Configuration\n\nThese variables control the logging behavior for TypeScript applications.\n\n### ENCORE_RUNTIME_LOG\n\nSets the log level for Encore's internal runtime operations (written in Rust).\n\n**Default:** `debug` (automatically set to `error` during `encore run`)\n\n**Valid values:** `trace`, `debug`, `info`, `warn`, `error`\n\n**Example:**\n\n```bash\n# See detailed runtime logs\nexport ENCORE_RUNTIME_LOG=trace\nencore run\n```\n\n<Callout type=\"info\">\n\nIf `RUST_LOG` is set, it takes precedence over `ENCORE_RUNTIME_LOG`. The runtime log controls logging for internal Encore modules.\n\n</Callout>\n\n### ENCORE_LOG\n\nSets the log level for your application code.\n\n**Default:** `Trace` (log everything)\n\n**Valid values:** `Off`, `Error`, `Warn`, `Info`, `Debug`, `Trace`\n\n**Example:**\n\n```typescript\nimport log from \"encore.dev/log\";\n\nlog.info(\"This message respects ENCORE_LOG level\");\n```\n\n```bash\n# Only show errors and warnings\nexport ENCORE_LOG=Warn\nencore run\n```\n\n### ENCORE_NOLOG\n\nDisables all logging when set to any non-empty value.\n\n**Default:** Not set\n\n**Example:**\n\n```bash\n# Disable all logs\nexport ENCORE_NOLOG=1\nencore run\n```\n\n## Advanced Development\n\nThese variables are primarily useful for advanced development scenarios, such as contributing to Encore itself or using custom builds.\n\n### ENCORE_RUNTIMES_PATH\n\nSpecifies the path to the Encore runtimes directory.\n\n**Default:** Auto-detected relative to the Encore installation (`<install_root>/runtimes`)\n\n**Example:**\n\n```bash\nexport ENCORE_RUNTIMES_PATH=/path/to/custom/runtimes\n```\n\n### ENCORE_RUNTIME_LIB\n\nSpecifies the path to the native Node.js runtime library used by TypeScript applications.\n\n**Default:** `<runtimes_path>/js/encore-runtime.node`\n\n**Example:**\n\n```bash\nexport ENCORE_RUNTIME_LIB=/path/to/custom/encore-runtime.node\n```\n\n### ENCORE_TSPARSER_PATH\n\nSpecifies the path to the TypeScript parser binary.\n\n**Default:** Auto-detected from `encore` binary location or system `PATH`\n\n**Example:**\n\n```bash\nexport ENCORE_TSPARSER_PATH=/path/to/custom/tsparser-encore\n```\n\n<Callout type=\"info\">\n\nFor most users, these paths are automatically detected and don't need to be set. They are primarily useful when contributing to Encore or testing custom builds.\n\n</Callout>\n\n## Debugging\n\n### ENCORE_API_INCLUDE_INTERNAL_MESSAGE\n\nControls whether internal error messages are included in API error responses.\n\n**Default:** automatically set to `1` during local development with `encore run`\n\n**Format:** Any non-empty, non-\"0\" value is considered `true`\n\n**Example:**\n\n```bash\n# Manually enable for debugging\nexport ENCORE_API_INCLUDE_INTERNAL_MESSAGE=1\n```\n\n### RUST_LOG\n\nControls Rust-level logging for the Encore runtime. This provides more granular control than `ENCORE_RUNTIME_LOG`.\n\n**Default:** Not set\n\n**Format:** Standard Rust `env_logger` format (see [env_logger documentation](https://docs.rs/env_logger))\n\n**Example:**\n\n```bash\n# Enable info logs for all modules in the runtime\nexport RUST_LOG=info\nencore run\n```\n\n<Callout type=\"info\">\n\n`RUST_LOG` takes precedence over `ENCORE_RUNTIME_LOG`. Use `RUST_LOG` for fine-grained control over specific runtime modules.\n\n</Callout>\n"
  },
  {
    "path": "docs/ts/develop/integrations/better-auth.md",
    "content": "---\nseotitle: Using Better Auth with Encore.ts for Authentication\nseodesc: Learn how to add production-ready authentication to your Encore.ts application using Better Auth, with automatic database provisioning and secrets management.\ntitle: Better Auth\nlang: ts\n---\n\n[Better Auth](https://www.better-auth.com) is a TypeScript authentication library that supports email/password, OAuth, two-factor, magic links, and sessions. This guide shows how to use it with Encore's [database provisioning](https://encore.dev/docs/ts/primitives/databases) and [secrets management](https://encore.dev/docs/ts/primitives/secrets).\n\nTo get started quickly, create a new app from the example:\n\n```shell\n$ encore app create --example=ts/betterauth\n```\n\nOr follow the steps below to add Better Auth to an existing Encore app.\n\n<Callout type=\"info\">\n\nIf you haven't installed Encore yet, see the [installation guide](https://encore.dev/docs/ts/install) first.\n\n</Callout>\n\n## Install\n\n```shell\n$ npm install better-auth pg\n```\n\n## Set up the database\n\nBetter Auth needs a database for users and sessions. Encore [provisions and manages databases](https://encore.dev/docs/ts/primitives/databases) for you automatically, just define it in code:\n\n```ts\n-- db.ts --\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\nexport const db = new SQLDatabase(\"auth\", {\n  migrations: \"./migrations\",\n});\n```\n\n<Callout type=\"info\">\n\nLocally, Encore starts a PostgreSQL instance automatically when you run `encore run`. You'll need [Docker](https://docker.com/get-started/) running for the local database.\n\n</Callout>\n\n## Configure Better Auth\n\nCreate the Better Auth instance using Encore's database and secrets:\n\n```ts\n-- auth.ts --\nimport { betterAuth } from \"better-auth\";\nimport { Pool } from \"pg\";\nimport { secret } from \"encore.dev/config\";\nimport { db } from \"./db\";\n\nconst authSecret = secret(\"AuthSecret\");\n\nconst pool = new Pool({\n  connectionString: db.connectionString,\n});\n\nexport const auth = betterAuth({\n  secret: authSecret(),\n  basePath: \"/auth\",\n  database: pool,\n  trustedOrigins: [\"http://localhost:4000\"],\n  emailAndPassword: {\n    enabled: true,\n  },\n  socialProviders: {\n    github: {\n      clientId: secret(\"GithubClientId\")(),\n      clientSecret: secret(\"GithubClientSecret\")(),\n    },\n  },\n});\n```\n\nSet the secrets using the Encore CLI:\n\n```shell\n$ encore secret set --type dev,local,pr,production AuthSecret\n$ encore secret set --type dev,local,pr,production GithubClientId\n$ encore secret set --type dev,local,pr,production GithubClientSecret\n```\n\n<Callout type=\"info\">\n\n**Tip:** Generate a strong auth secret with `openssl rand -base64 32` and paste it when prompted for `AuthSecret`.\n\n</Callout>\n\n<Callout type=\"info\">\n\nLocally, secrets are stored on your machine and injected when you run `encore run`. No `.env` files needed.\n\n</Callout>\n\n## Connect to Encore's auth handler\n\nWire Better Auth into Encore's [authentication system](https://encore.dev/docs/ts/develop/auth) so you can use `auth: true` on any API endpoint:\n\n```ts\n-- handler.ts --\nimport { APIError, Gateway } from \"encore.dev/api\";\nimport { authHandler } from \"encore.dev/auth\";\nimport { Header } from \"encore.dev/api\";\nimport { auth } from \"./auth\";\n\ninterface AuthParams {\n  authorization: Header<\"Authorization\">;\n}\n\ninterface AuthData {\n  userID: string;\n}\n\nconst handler = authHandler<AuthParams, AuthData>(\n  async (params) => {\n    const session = await auth.api.getSession({\n      headers: new Headers({\n        authorization: params.authorization,\n      }),\n    });\n\n    if (!session) {\n      throw APIError.unauthenticated(\"invalid session\");\n    }\n\n    return { userID: session.user.id };\n  }\n);\n\nexport const gateway = new Gateway({ authHandler: handler });\n```\n\n## Expose Better Auth routes\n\nBetter Auth needs HTTP routes for sign-in, sign-up, and OAuth callbacks. Expose these using a [raw endpoint](https://encore.dev/docs/ts/primitives/raw-endpoints):\n\n```ts\n-- routes.ts --\nimport { api } from \"encore.dev/api\";\nimport { auth } from \"./auth\";\n\n// Better Auth expects a Web Request, but Encore raw endpoints receive\n// a Node.js IncomingMessage. We convert between the two formats.\nexport const authRoutes = api.raw(\n  { expose: true, path: \"/auth/*path\", method: \"*\" },\n  async (req, res) => {\n    // Read the request body\n    const chunks: Buffer[] = [];\n    for await (const chunk of req) {\n      chunks.push(chunk);\n    }\n    const body = Buffer.concat(chunks);\n\n    // Build a Web Request from the Node.js request\n    const headers = new Headers();\n    for (const [key, value] of Object.entries(req.headers)) {\n      if (value) headers.append(key, Array.isArray(value) ? value.join(\", \") : value);\n    }\n\n    const url = `http://${req.headers.host}${req.url}`;\n    const webReq = new Request(url, {\n      method: req.method,\n      headers,\n      body: [\"GET\", \"HEAD\"].includes(req.method || \"\") ? undefined : body,\n    });\n\n    // Pass to Better Auth and forward the response\n    const response = await auth.handler(webReq);\n\n    response.headers.forEach((value, key) => {\n      res.setHeader(key, value);\n    });\n    res.writeHead(response.status);\n    res.end(await response.text());\n  }\n);\n```\n\n## Use in your endpoints\n\nAny endpoint with `auth: true` will now require a valid Better Auth session:\n\n```ts\nimport { api } from \"encore.dev/api\";\nimport { getAuthData } from \"~encore/auth\";\n\nexport const getProfile = api(\n  { auth: true, expose: true, method: \"GET\", path: \"/profile\" },\n  async (): Promise<{ userID: string }> => {\n    const data = getAuthData()!;\n    return { userID: data.userID };\n  }\n);\n```\n\n## Deploy\n\nWhen you deploy, Encore automatically provisions and manages the infrastructure your app needs:\n\n- **Database** provisioned as Cloud SQL on GCP or RDS on AWS. Migrations run automatically on deploy.\n- **Secrets** encrypted per environment (preview, staging, production), never shared between them.\n- **Networking** including TLS, load balancing, and DNS.\n\n### Self-hosting\n\nBuild a Docker image and deploy anywhere:\n\n```shell\n$ encore build docker my-app:latest\n```\n\nSee the [self-hosting docs](https://encore.dev/docs/ts/self-host/build) for more details.\n\n### Encore Cloud\n\nDeploy your application to a free staging environment in Encore's development cloud:\n\n```shell\n$ git push encore main\n```\n\nYou can also connect your own AWS or GCP account and Encore will automatically provision databases, run migrations, and manage secrets in your cloud. See [Connect your cloud account](https://encore.dev/docs/platform/deploy/own-cloud) for details.\n\n## Related resources\n\n- [Encore authentication docs](https://encore.dev/docs/ts/develop/auth)\n- [Better Auth documentation](https://www.better-auth.com/docs)\n- [Encore databases](https://encore.dev/docs/ts/primitives/databases)\n- [Encore secrets](https://encore.dev/docs/ts/primitives/secrets)\n"
  },
  {
    "path": "docs/ts/develop/integrations/polar.md",
    "content": "---\nseotitle: Using Polar with Encore.ts for Payments & Subscriptions\nseodesc: Learn how to add payments, subscriptions, and license keys to your Encore.ts application using Polar as your Merchant of Record.\ntitle: Polar\nlang: ts\n---\n\n[Polar](https://polar.sh) handles payments, subscriptions, and license keys as your Merchant of Record. This guide shows how to integrate Polar with an Encore application.\n\nTo get started quickly, create a new app from the example:\n\n```shell\n$ encore app create --example=ts/polar\n```\n\nOr follow the steps below to add Polar to an existing Encore app.\n\n<Callout type=\"info\">\n\nIf you haven't installed Encore yet, see the [installation guide](https://encore.dev/docs/ts/install) first.\n\n</Callout>\n\n## Install the SDK\n\n```shell\n$ npm install @polar-sh/sdk\n```\n\n## Polar setup\n\nBefore writing code, you'll need to set up a few things in the [Polar dashboard](https://sandbox.polar.sh) (use the sandbox for development):\n\n1. **Create an access token.** Go to Settings > [Developers > Personal Access Tokens](https://sandbox.polar.sh/settings/developers/pat) and create a new token.\n2. **Create a product.** Go to [Products](https://sandbox.polar.sh/products) and create at least one product. Copy its **product ID**, you'll need it to create checkout sessions.\n3. **Set up a webhook** (optional for local dev). Go to Settings > [Webhooks](https://sandbox.polar.sh/settings/webhooks) and point it to your API URL followed by `/webhooks/polar`. For local development, use a tunnel like [ngrok](https://ngrok.com) to expose your local server.\n\nSee the [Polar documentation](https://docs.polar.sh) for more details on products, pricing, and webhooks.\n\n## Set your secrets\n\nStore your Polar credentials as [Encore secrets](https://encore.dev/docs/ts/primitives/secrets):\n\n```shell\n$ encore secret set --type dev,local,pr,production PolarAccessToken\n```\n\n<Callout type=\"info\">\n\nLocally, secrets are stored on your machine and injected when you run `encore run`. No `.env` files needed.\n\n</Callout>\n\n## Initialize the client\n\nCreate a file to configure the Polar SDK. Use Encore's [`secret()`](https://encore.dev/docs/ts/primitives/secrets) function to access the token. Use `sandbox` for development and `production` when deployed:\n\n```ts\n-- polar.ts --\nimport { Polar } from \"@polar-sh/sdk\";\nimport { secret } from \"encore.dev/config\";\n\nconst polarAccessToken = secret(\"PolarAccessToken\");\n\nconst server = process.env.ENCORE_ENVIRONMENT === \"production\"\n  ? \"production\"\n  : \"sandbox\";\n\nexport const polar = new Polar({\n  accessToken: polarAccessToken(),\n  server,\n});\n```\n\n## Create a checkout\n\nUse the Polar SDK to create checkout sessions for your products:\n\n```ts\n-- checkout.ts --\nimport { api } from \"encore.dev/api\";\nimport { polar } from \"./polar\";\nimport { getAuthData } from \"~encore/auth\";\n\ninterface CreateCheckoutRequest {\n  productId: string;\n}\n\ninterface CreateCheckoutResponse {\n  checkoutUrl: string;\n}\n\nexport const createCheckout = api(\n  { auth: true, expose: true, method: \"POST\", path: \"/checkout\" },\n  async (req: CreateCheckoutRequest): Promise<CreateCheckoutResponse> => {\n    const authData = getAuthData()!;\n\n    const baseUrl = process.env.ENCORE_API_URL || \"http://localhost:4000\";\n\n    const session = await polar.checkouts.create({\n      products: [req.productId],\n      customerEmail: authData.email,\n      successUrl: `${baseUrl}/?success=true`,\n    });\n\n    return { checkoutUrl: session.url || \"\" };\n  }\n);\n```\n\n## Handle webhooks\n\nCreate a [raw endpoint](https://encore.dev/docs/ts/primitives/raw-endpoints) to receive webhook events from Polar:\n\n```ts\n-- webhooks.ts --\nimport { api } from \"encore.dev/api\";\nimport log from \"encore.dev/log\";\n\nexport const handleWebhook = api.raw(\n  { expose: true, path: \"/webhooks/polar\", method: \"POST\" },\n  async (req, res) => {\n    const chunks: Buffer[] = [];\n    for await (const chunk of req) {\n      chunks.push(chunk);\n    }\n    const event = JSON.parse(Buffer.concat(chunks).toString());\n\n    log.info(\"Received Polar webhook\", { type: event.type });\n\n    switch (event.type) {\n      case \"subscription.active\":\n        // Grant access to your product\n        break;\n      case \"subscription.canceled\":\n        // Revoke access\n        break;\n      case \"order.paid\":\n        // Fulfill the order\n        break;\n    }\n\n    res.writeHead(200);\n    res.end();\n  }\n);\n```\n\nRegister your webhook URL in the [Polar dashboard](https://sandbox.polar.sh/settings/webhooks) under Settings > Webhooks. Use your Encore API URL followed by `/webhooks/polar`. Enable the events you want to handle (e.g. `subscription.active`, `subscription.canceled`, `order.paid`).\n\n## Deploy\n\nWhen you deploy, Encore automatically provisions and manages the infrastructure your app needs:\n\n- **Secrets** encrypted per environment (preview, staging, production), never shared between them.\n- **Databases** provisioned as Cloud SQL on GCP or RDS on AWS.\n- **Networking** including TLS, load balancing, and DNS.\n\n### Self-hosting\n\nBuild a Docker image and deploy anywhere:\n\n```shell\n$ encore build docker my-app:latest\n```\n\nSee the [self-hosting docs](https://encore.dev/docs/ts/self-host/build) for more details.\n\n### Encore Cloud\n\nDeploy your application to a free staging environment in Encore's development cloud:\n\n```shell\n$ git push encore main\n```\n\nYou can also connect your own AWS or GCP account and Encore will automatically provision the infrastructure and manage secrets in your cloud. See [Connect your cloud account](https://encore.dev/docs/platform/deploy/own-cloud) for details.\n\n## Related resources\n\n- [Polar + Encore example app](https://github.com/encoredev/examples/tree/main/ts/polar)\n- [Polar documentation](https://docs.polar.sh)\n- [Polar sandbox dashboard](https://sandbox.polar.sh)\n- [Encore secrets](https://encore.dev/docs/ts/primitives/secrets)\n- [Raw endpoints](https://encore.dev/docs/ts/primitives/raw-endpoints)\n"
  },
  {
    "path": "docs/ts/develop/integrations/resend.md",
    "content": "---\nseotitle: Using Resend with Encore.ts for Transactional Email\nseodesc: Learn how to send transactional emails from your Encore.ts application using Resend, with async delivery via Pub/Sub and built-in observability.\ntitle: Resend\nlang: ts\n---\n\n[Resend](https://resend.com) provides transactional email with high deliverability and React Email templates. This guide shows how to use it with Encore's [Pub/Sub](https://encore.dev/docs/ts/primitives/pubsub) and [secrets management](https://encore.dev/docs/ts/primitives/secrets).\n\nTo get started quickly, create a new app from the example:\n\n```shell\n$ encore app create --example=ts/resend\n```\n\nOr follow the steps below to add Resend to an existing Encore app.\n\n<Callout type=\"info\">\n\nIf you haven't installed Encore yet, see the [installation guide](https://encore.dev/docs/ts/install) first.\n\n</Callout>\n\n## Install the SDK\n\n```shell\n$ npm install resend\n```\n\nIf you want to use React Email templates:\n\n```shell\n$ npm install resend @react-email/components\n```\n\n## Resend setup\n\nBefore writing code, you'll need to configure a few things in the [Resend dashboard](https://resend.com):\n\n1. **Create an API key.** Go to [API Keys](https://resend.com/api-keys) and create a new key.\n2. **Verify a domain** (optional for testing). Go to [Domains](https://resend.com/domains) and add your sending domain. Until you verify a domain, you can use `onboarding@resend.dev` as the `from` address for testing.\n\nSee the [Resend documentation](https://resend.com/docs) for more details on domain verification and sending limits.\n\n## Set your API key\n\nStore your Resend API key as an [Encore secret](https://encore.dev/docs/ts/primitives/secrets):\n\n```shell\n$ encore secret set --type dev,local,pr,production ResendAPIKey\n```\n\n<Callout type=\"info\">\n\nLocally, secrets are stored on your machine and injected when you run `encore run`. No `.env` files needed.\n\n</Callout>\n\n## Initialize the client\n\n```ts\n-- resend.ts --\nimport { Resend } from \"resend\";\nimport { secret } from \"encore.dev/config\";\n\nconst resendApiKey = secret(\"ResendAPIKey\");\n\nexport const resend = new Resend(resendApiKey());\n```\n\n## Send an email\n\nUse the Resend SDK in an Encore API endpoint:\n\n```ts\n-- send.ts --\nimport { api } from \"encore.dev/api\";\nimport { resend } from \"./resend\";\n\ninterface SendEmailRequest {\n  to: string;\n  subject: string;\n  html: string;\n}\n\ninterface SendEmailResponse {\n  id: string;\n}\n\nexport const sendEmail = api(\n  { expose: true, method: \"POST\", path: \"/email/send\" },\n  async (req: SendEmailRequest): Promise<SendEmailResponse> => {\n    const { data, error } = await resend.emails.send({\n      from: \"Your App <hello@yourdomain.com>\",\n      to: req.to,\n      subject: req.subject,\n      html: req.html,\n    });\n\n    if (error) {\n      throw new Error(`Failed to send email: ${error.message}`);\n    }\n\n    return { id: data!.id };\n  }\n);\n```\n\n<Callout type=\"info\">\n\nThe `from` address must use a domain you've verified in [Resend](https://resend.com/domains). For testing, you can use `onboarding@resend.dev` which works with any API key.\n\n</Callout>\n\n## Async delivery with Pub/Sub\n\nFor better performance, send emails asynchronously using Encore's [Pub/Sub](https://encore.dev/docs/ts/primitives/pubsub). This keeps your API endpoints fast and handles retries automatically:\n\n```ts\n-- topic.ts --\nimport { Topic, Subscription } from \"encore.dev/pubsub\";\nimport { resend } from \"./resend\";\n\ninterface EmailEvent {\n  to: string;\n  subject: string;\n  html: string;\n}\n\nexport const emailTopic = new Topic<EmailEvent>(\"email-send\", {\n  deliveryGuarantee: \"at-least-once\",\n});\n\nconst _ = new Subscription(emailTopic, \"send-via-resend\", {\n  handler: async (event) => {\n    const { error } = await resend.emails.send({\n      from: \"Your App <hello@yourdomain.com>\",\n      to: event.to,\n      subject: event.subject,\n      html: event.html,\n    });\n\n    if (error) {\n      throw new Error(error.message);\n    }\n  },\n});\n```\n\nThen publish from any endpoint:\n\n```ts\nimport { emailTopic } from \"./topic\";\n\n// Inside any API endpoint\nawait emailTopic.publish({\n  to: \"user@example.com\",\n  subject: \"Welcome!\",\n  html: \"<p>Thanks for signing up.</p>\",\n});\n```\n\n<Callout type=\"info\">\n\nLocally, Pub/Sub runs in-process so messages are delivered immediately, making it easy to test and debug.\n\n</Callout>\n\n## Deploy\n\nWhen you deploy, Encore automatically provisions and manages the infrastructure your app needs:\n\n- **Secrets** encrypted per environment (preview, staging, production), never shared between them.\n- **Pub/Sub** provisioned as GCP Pub/Sub or SQS/SNS on AWS, with automatic retries and dead-letter queues.\n- **Networking** including TLS, load balancing, and DNS.\n\n### Self-hosting\n\nBuild a Docker image and deploy anywhere:\n\n```shell\n$ encore build docker my-app:latest\n```\n\nSee the [self-hosting docs](https://encore.dev/docs/ts/self-host/build) for more details.\n\n### Encore Cloud\n\nDeploy your application to a free staging environment in Encore's development cloud:\n\n```shell\n$ git push encore main\n```\n\nYou can also connect your own AWS or GCP account and Encore will automatically provision Pub/Sub topics, manage secrets, and handle networking in your cloud. See [Connect your cloud account](https://encore.dev/docs/platform/deploy/own-cloud) for details.\n\n## Related resources\n\n- [Resend + Encore example app](https://github.com/encoredev/examples/tree/main/ts/resend)\n- [Resend documentation](https://resend.com/docs)\n- [Encore Pub/Sub](https://encore.dev/docs/ts/primitives/pubsub)\n- [Encore secrets](https://encore.dev/docs/ts/primitives/secrets)\n"
  },
  {
    "path": "docs/ts/develop/metadata.md",
    "content": "---\nseotitle: Metadata API – Get data about the app and environment\nseodesc: See how to use Encore's Metadata API to get information about the app and the environment it's running in.\ntitle: Metadata\nsubtitle: Use the metadata API to get information about the app and the environment it's running in\ninfobox: { title: \"Metadata API\", import: \"encore.dev\" }\nlang: ts\n---\n\nWhile Encore tries to provide a cloud-agnostic environment, sometimes it's helpful to know more about the environment\nyour application is running in. For this reason Encore provides an API for accessing metadata about the\n[application](#application-metadata) and the environment it's running in as\npart of the `encore.dev` package.\n\n## Application Metadata\n\nCalling `appMeta()` from the `encore.dev` package returns an object that\ncontains information about the application, including:\n\n- `appId` - the application name.\n- `apiBaseUrl` - the URL the application API can be publicly accessed on.\n- `environment` - the [environment](/docs/platform/deploy/environments) the application is currently running in.\n- `build` - the revision information of the build from the version control system.\n- `deploy` - the deployment ID and when this version of the app was deployed.\n\n## Current Request\n\nThe `currentRequest()` function, also provided by the `encore.dev` module, can be called from anywhere within your application and returns a\n`Request` object that contains information the current request being processed.\n\nThe object contains different fields depending on whether the\ncurrent request is an API call or a Pub/Sub message being processed.\n\n```typescript\n-- API Call --\n/** Describes an API call being processed. */\nexport interface APICallMeta {\n  /** Specifies that the request is an API call. */\n  type: \"api-call\";\n\n  /** Describes the API Endpoint being called. */\n  api: APIDesc;\n\n  /** The HTTP method used in the API call. */\n  method: Method;\n\n  /**\n   * The request URL path used in the API call,\n   * excluding any query string parameters.\n   * For example \"/path/to/endpoint\".\n   */\n  path: string;\n\n  /**\n   * The request URL path used in the API call,\n   * including any query string parameters.\n   * For example \"/path/to/endpoint?with=querystring\".\n   */\n  pathAndQuery: string;\n\n  /**\n   * The parsed path parameters for the API endpoint.\n   * The keys are the names of the path parameters,\n   * from the API definition.\n   *\n   * For example {id: 5}.\n   */\n  pathParams: Record<string, any>;\n\n  /**\n   * The request headers from the HTTP request.\n   * The values are arrays if the header contains multiple values,\n   * either separated by \";\" or when the header key appears more than once.\n   */\n  headers: Record<string, string | string[]>;\n\n  /**\n   * The parsed request payload, as expected by the application code.\n   * Not provided for raw endpoints or when the API endpoint expects no\n   * request data.\n   */\n  parsedPayload?: Record<string, any>;\n}\n\n-- Pub/Sub Message --\n/** Describes a Pub/Sub message being processed. */\nexport interface PubSubMessageMeta {\n  /** Specifies that the request is a Pub/Sub message. */\n  type: \"pubsub-message\";\n\n  /** The service processing the message. */\n  service: string;\n\n  /** The name of the Pub/Sub topic. */\n  topic: string;\n\n  /** The name of the Pub/Sub subscription. */\n  subscription: string;\n\n  /**\n   * The unique id of the Pub/Sub message.\n   * It is the same id returned by `topic.publish()`.\n   * The message id stays the same across delivery attempts.\n   */\n  messageId: string;\n\n  /**\n   * The delivery attempt. The first attempt starts at 1,\n   * and increases by 1 for each retry.\n   */\n  deliveryAttempt: number;\n\n  /**\n   * The parsed request payload, as expected by the application code.\n   */\n  parsedPayload?: Record<string, any>;\n}\n```\n\nThis works automatically as a result of Encore's request tracking.\nIf no request is processed by the caller, which can happen if you call it during service\ninitialization, `currentRequest()` returns `undefined`.\n\n\n## Example Use Cases\n\n### Using Cloud Specific Services\n\nAll the [clouds](/docs/platform/deploy/own-cloud) contain a large number of services, not all of which Encore natively supports.\n\nBy using information about the [environment](/docs/platform/deploy/environments), you can define the implementation of these and use different services for each environment's provider.\n\nFor instance if you are pushing audit logs into a data warehouse, when running on GCP you could use BigQuery, but when running on AWS you could use Redshift, when running locally you could simply write them to a file.\n\n```ts\nimport { appMeta } from \"encore.dev\";\n\n// Emit an audit event.\nasync function audit(userID: string, event: Record<string, any>) {\n  const cloud = appMeta().environment.cloud;\n  switch (cloud) {\n    case \"aws\":\n      return writeIntoRedshift(userID, event);\n    case \"gcp\":\n      return writeIntoBigQuery(userID, event);\n    case \"local\":\n      return writeIntoFile(userID, event);\n    default:\n      throw new Error(`unknown cloud: ${cloud}`);\n  }\n}\n```\n\n### Checking Environment type\n\nWhen implementing a signup system, you may want to skip email verification on user signups when developing the application.\nUsing the `appMeta` API, we can check the environment and decide whether to send an email or simply mark the user as\nverified upon signup.\n\n```ts\nimport { appMeta } from \"encore.dev\";\n\nexport const signup = api(\n  { expose: true },\n  async (params: SignupParams): Promise<SignupResponse> => {\n    // more code...\n\n    // If this is a testing environment, skip sending the verification email.\n    switch (appMeta().environment.type) {\n      case \"test\":\n      case \"development\":\n        await markEmailVerified(userID);\n        break;\n      default:\n        await sendVerificationEmail(userID);\n        break;\n    }\n\n    // more code...\n  },\n);\n```\n"
  },
  {
    "path": "docs/ts/develop/middleware.md",
    "content": "---\nseotitle: Using Middleware in your Encore.ts application\nseodesc: See how you can use middleware in your Encore.ts application to handle cross-cutting generic functionality, like request logging, auth, or tracing.\ntitle: Middleware\nsubtitle: Handling cross-cutting, generic functionality\nlang: ts\n---\n\nMiddleware is a way to write reusable code that runs before, after, or both before and after\nthe handling of API requests, often across several (or all) API endpoints.\n\nMiddleware is commonly used to implement cross-cutting concerns like\n[request logging](/docs/ts/observability/logging),\n[authentication](/docs/ts/develop/auth),\n[tracing](/docs/ts/observability/tracing),\nand so on. One of the benefits of Encore.ts is that\nit handles these common use cases out-of-the-box, so there's no\nneed to write your own middleware.\n\nHowever, when developing applications there's often some use cases where it can be useful to write\nreusable functionality that applies to multiple API endpoints, and middleware\nis a good solution for this.\n\nEncore provides built-in support for middleware by adding functions to the\n[Service definitions](/docs/ts/primitives/services) configuration.\nEach middleware can be configured with a `target` option to specify what\nAPI endpoints it applies to.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/middleware\"\n    desc=\"Example app with two middleware; a rate limiter and one for user authorization.\"\n/>\n\n## Middleware functions\n\nThe simplest way to create a middleware is to use the `middleware` helper in `encore.dev/api`,\nhere is an example of a middleware that will run for endpoints that require auth:\n\n```ts\nimport { middleware } from \"encore.dev/api\";\n\nexport default new Service(\"myService\", {\n    middlewares: [\n        middleware({ target: { auth: true } }, async (req, next) => {\n            // do something before the api handler\n            const resp = await next(req);\n            // do something after the api handler\n            return resp\n        })\n    ]\n});\n\n```\n\nMiddleware forms a chain, allowing each middleware to introspect and process\nthe incoming request before handing it off to the next middleware by calling the\n`next` function that's passed in as an argument. For the last middleware in the\nchain, calling `next` results in the actual API handler being called.\n\nThe `req` parameter provides information about the incoming request, it has different fields\ndepending on what kind of handler it is.\n\nYou can get information about the current request via `req.requestMeta` if the endpoint is a\n[typed API endpoint](/docs/ts/primitives/defining-apis) or a\n[Streaming API endpoint](/docs/ts/primitives/streaming-apis).\n\nFor [Streaming API endpoints](/docs/ts/primitives/streaming-apis) you can also access the stream\nvia `req.stream` method.\n\nFor [Raw Endpoints](/docs/ts/primitives/raw-endpoints) you can access the raw request and the\nraw response via `req.rawRequest` and `req.rawResponse`.\n\nThe `next` function returns a `HandlerResponse` object which contains the response from the API.\nExtra response headers can be added using `resp.header.set(key, value)` or `resp.header.add(key, value)`,\nif the endpoint is a [typed API endpoint](/docs/ts/primitives/defining-apis).\n\nTo pass data from middleware to an API handler, you can assign values to `req.data` within the middleware. These values can then be accessed in the handler using `currentRequest()`.\n\nHere’s an example:\n\n```ts\nconst mw = middleware(async (req, next) => {\n  // Assign a value to the request\n  req.data.myMiddlewareData = { some: \"data\" };\n\n  return await next(req);\n});\n\nexport const ep = api(\n  { expose: true, method: \"GET\", path: \"/endpoint\" },\n  async () => {\n    const callMeta = currentRequest() as APICallMeta;\n\n    // Access the value in the API handler\n    const myData = callMeta.middlewareData?.myMiddlewareData;\n    // Use the data as needed\n  },\n);\n```\n\n## Middleware ordering\n\nMiddleware runs in the order they are defined in the [Service definitions](/docs/ts/primitives/services)\nconfiguration, i.e:\n\n```ts\nexport default new Service(\"myService\", {\n    middlewares: [\n        first,\n        second,\n        third\n    ],\n});\n\n```\n\n## Targeting APIs\n\nThe `target` option specifies which endpoints within the service the middleware should run on. If not set, the middleware will run for all endpoints by default.\n\nFor better performance, use the `target` option instead of filtering within the middleware function. This allows the applicable middleware to be determined per endpoint during startup, reducing runtime overhead.\n\nThe following options are available for targeting endpoints:\n\n- `tags`: A list of tags evaluated with `OR`, meaning the middleware applies to an endpoint if the endpoint has at least one of these tags.\n- `expose`: A boolean indicating whether the middleware should be applied to endpoints that are exposed or not exposed.\n- `auth`: A boolean indicating whether the middleware should be applied to endpoints that require authentication or not.\n- `isRaw`: A boolean indicating whether the middleware should be applied to raw endpoints.\n- `isStream`: A boolean indicating whether the middleware should be applied to stream endpoints.\n\n"
  },
  {
    "path": "docs/ts/develop/monorepo/nx.md",
    "content": "---\nseotitle: Using Encore with Nx in a monorepo\nseodesc: Learn how to set up Encore.ts in an Nx monorepo with shared packages that require building before use.\ntitle: Nx\nsubtitle: Using Encore in an Nx monorepo\nlang: ts\n---\n\n[Nx](https://nx.dev) is a build system for JavaScript and TypeScript monorepos. This guide shows how to set up an Encore application within an Nx monorepo that depends on shared packages requiring compilation.\n\n## Overview\n\nWhen using Encore in an Nx monorepo, you may have shared packages (like utility libraries or shared types) that need to be built before the Encore app can use them. Since Encore parses your application on startup, these dependencies must be compiled first.\n\nThis guide covers two scenarios:\n- **Local development**: Use Nx to build dependencies before running `encore run`\n- **Deployment**: Use Encore's `prebuild` hook to automatically build dependencies when deploying via Encore Cloud or exporting a Docker image\n\n## Project structure\n\nA typical Nx setup with Encore looks like this:\n\n```\nmy-nx-workspace/\n├── apps/\n│   └── backend/           # Encore application\n│       ├── encore.app\n│       ├── package.json\n│       ├── project.json\n│       ├── tsconfig.json\n│       └── article/\n│           └── article.ts\n├── packages/\n│   └── shared/            # Shared library requiring build\n│       ├── package.json\n│       ├── project.json\n│       ├── tsconfig.json\n│       ├── src/\n│       │   └── index.ts\n│       └── dist/          # Built output\n│           └── index.js\n├── nx.json\n├── package.json\n└── package-lock.json\n```\n\n## Configuration\n\n### Root package.json\n\nConfigure npm workspaces to include your apps and packages:\n\n```json\n{\n  \"name\": \"my-nx-workspace\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"nx run-many -t build\",\n    \"dev\": \"nx run-many -t dev\"\n  },\n  \"devDependencies\": {\n    \"nx\": \"^21.0.0\",\n    \"typescript\": \"^5.0.0\"\n  },\n  \"workspaces\": [\n    \"apps/*\",\n    \"packages/*\"\n  ]\n}\n```\n\n### nx.json\n\nConfigure Nx's build pipeline in the root `nx.json`:\n\n```json\n{\n  \"$schema\": \"./node_modules/nx/schemas/nx-schema.json\",\n  \"targetDefaults\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"{projectRoot}/dist/**\"],\n      \"cache\": true\n    },\n    \"dev\": {\n      \"cache\": false\n    }\n  }\n}\n```\n\nThe `\"dependsOn\": [\"^build\"]` configuration ensures that a project's dependencies are built before the project itself.\n\n### Shared package\n\nYour shared package needs to compile TypeScript to JavaScript and expose the built output.\n\n**packages/shared/package.json:**\n```json\n{\n  \"name\": \"@repo/shared\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsc\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.0.0\"\n  }\n}\n```\n\n**packages/shared/project.json:**\n```json\n{\n  \"name\": \"@repo/shared\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"sourceRoot\": \"packages/shared/src\",\n  \"projectType\": \"library\",\n  \"targets\": {\n    \"build\": {\n      \"executor\": \"nx:run-commands\",\n      \"options\": {\n        \"command\": \"tsc\",\n        \"cwd\": \"packages/shared\"\n      },\n      \"outputs\": [\"{projectRoot}/dist\"]\n    }\n  }\n}\n```\n\n**packages/shared/tsconfig.json:**\n```json\n{\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"moduleResolution\": \"bundler\",\n    \"module\": \"ES2022\",\n    \"target\": \"ES2022\",\n    \"declaration\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n```\n\n**packages/shared/src/index.ts:**\n```ts\n// Types shared between frontend and backend\nexport interface Article {\n  slug: string;\n  title: string;\n  preview: string;\n}\n\nexport interface CreateArticleRequest {\n  title: string;\n  content: string;\n}\n\n// Utility functions\nexport function slugify(text: string): string {\n  return text\n    .toLowerCase()\n    .trim()\n    .replace(/[^\\w\\s-]/g, \"\")\n    .replace(/\\s+/g, \"-\");\n}\n\nexport function truncate(text: string, maxLength: number): string {\n  if (text.length <= maxLength) return text;\n  return text.slice(0, maxLength - 3) + \"...\";\n}\n```\n\n### Encore application\n\nThe Encore app needs three key configurations:\n\n1. **encore.app** - Use the `prebuild` hook to build dependencies during deployment\n2. **package.json** - Declare the dependency on the shared package\n3. **project.json** - Configure Nx targets and task dependencies\n\nTo create the Encore app, run `encore app init --lang ts` from the `apps/backend` directory. Then add the `prebuild` hook to the generated `encore.app` file:\n\n**apps/backend/encore.app:**\n```json\n{\n    \"id\": \"generated-id\",\n    \"lang\": \"typescript\",\n    \"build\": {\n        \"hooks\": {\n            \"prebuild\": \"npx nx build-deps @repo/backend\"\n        }\n    }\n}\n```\n\nThe `prebuild` hook runs when deploying via Encore Cloud or when exporting a Docker image with the Encore CLI. The `build-deps` target builds all dependencies of the backend.\n\n**apps/backend/package.json:**\n```json\n{\n  \"name\": \"@repo/backend\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"encore run\"\n  },\n  \"dependencies\": {\n    \"@repo/shared\": \"*\",\n    \"encore.dev\": \"latest\"\n  }\n}\n```\n\n**apps/backend/project.json:**\n```json\n{\n  \"name\": \"@repo/backend\",\n  \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n  \"sourceRoot\": \"apps/backend\",\n  \"projectType\": \"application\",\n  \"targets\": {\n    \"dev\": {\n      \"executor\": \"nx:run-commands\",\n      \"options\": {\n        \"command\": \"encore run\",\n        \"cwd\": \"apps/backend\"\n      },\n      \"dependsOn\": [\"^build\"],\n      \"cache\": false\n    },\n    \"build-deps\": {\n      \"dependsOn\": [\"^build\"],\n      \"cache\": true\n    }\n  }\n}\n```\n\nThe `\"dependsOn\": [\"^build\"]` configuration uses the `^` prefix to indicate \"run the build target on all dependencies first\". This automatically builds all shared packages before running `encore run`, without needing to list each dependency explicitly.\n\n### Using the shared package\n\nWith this setup, you can import from your shared package in your Encore services:\n\n**apps/backend/article/article.ts:**\n```ts\nimport { api } from \"encore.dev/api\";\nimport type { Article, CreateArticleRequest } from \"@repo/shared\";\nimport { slugify, truncate } from \"@repo/shared\";\n\nexport const create = api(\n  { expose: true, method: \"POST\", path: \"/article\" },\n  async ({ title, content }: CreateArticleRequest): Promise<Article> => {\n    return {\n      slug: slugify(title),\n      title: title,\n      preview: truncate(content, 100),\n    };\n  },\n);\n```\n\n## Running the application\n\n### Installation\n\nFirst, install all dependencies from the monorepo root:\n\n```shell\n$ npm install\n```\n\nThis installs dependencies for all workspaces, including Nx.\n\n### Local development\n\nFor local development, you need to build the shared packages before running `encore run`. From the monorepo root:\n\n```shell\n$ npx nx build-deps @repo/backend\n$ cd apps/backend && encore run\n```\n\nOr use Nx's `dev` target which handles the dependency ordering:\n\n```shell\n$ npx nx dev @repo/backend\n```\n\nThe `\"dependsOn\": [\"^build\"]` configuration ensures all dependencies are built before the backend's dev target runs.\n\n### Deployment\n\nWhen deploying via Encore Cloud or exporting a Docker image, the `prebuild` hook in `encore.app` automatically runs the Nx build.\n\n<Callout type=\"info\">\n\nWhen deploying a monorepo to Encore Cloud, configure the root path to your Encore app in the app settings: **Settings > General > Root Directory** (e.g., `apps/backend`).\n\n</Callout>\n\n## Key points\n\n- **Local development**: Run `npx nx build-deps @repo/backend` before `encore run`, or use `npx nx dev @repo/backend` to handle dependency ordering automatically\n- **Prebuild hook**: The `prebuild` hook in `encore.app` runs during deployment (Encore Cloud) or Docker export, not during local development\n- **Task dependencies**: Use `\"dependsOn\": [\"^build\"]` in `project.json` to automatically build all dependencies before running a target\n"
  },
  {
    "path": "docs/ts/develop/monorepo/turborepo.md",
    "content": "---\nseotitle: Using Encore with Turborepo in a monorepo\nseodesc: Learn how to set up Encore.ts in a Turborepo monorepo with shared packages that require building before use.\ntitle: Turborepo\nsubtitle: Using Encore in a Turborepo monorepo\nlang: ts\n---\n\n[Turborepo](https://turbo.build/repo) is a build system for JavaScript and TypeScript monorepos. This guide shows how to set up an Encore application within a Turborepo monorepo that depends on shared packages requiring compilation.\n\n## Overview\n\nWhen using Encore in a Turborepo monorepo, you may have shared packages (like utility libraries or shared types) that need to be built before the Encore app can use them. Since Encore parses your application on startup, these dependencies must be compiled first.\n\nThis guide covers two scenarios:\n- **Local development**: Use Turborepo to build dependencies before running `encore run`\n- **Deployment**: Use Encore's `prebuild` hook to automatically build dependencies when deploying via Encore Cloud or exporting a Docker image\n\n## Project structure\n\nA typical Turborepo setup with Encore looks like this:\n\n```\nmy-turborepo/\n├── apps/\n│   └── backend/           # Encore application\n│       ├── encore.app\n│       ├── package.json\n│       ├── tsconfig.json\n│       └── article/\n│           └── article.ts\n├── packages/\n│   └── shared/            # Shared library requiring build\n│       ├── package.json\n│       ├── tsconfig.json\n│       ├── src/\n│       │   └── index.ts\n│       └── dist/          # Built output\n│           └── index.js\n├── turbo.json\n├── package.json\n└── package-lock.json\n```\n\n## Configuration\n\n### Root package.json\n\nConfigure npm workspaces to include your apps and packages:\n\n```json\n{\n  \"name\": \"my-turborepo\",\n  \"private\": true,\n  \"packageManager\": \"npm@10.0.0\",\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"dev\": \"turbo run dev\"\n  },\n  \"devDependencies\": {\n    \"turbo\": \"^2.0.0\",\n    \"typescript\": \"^5.0.0\"\n  },\n  \"workspaces\": [\n    \"apps/*\",\n    \"packages/*\"\n  ]\n}\n```\n\nThe `packageManager` field is required by Turborepo. Adjust the version to match your installed npm version (run `npm --version` to check).\n\n### turbo.json\n\nConfigure Turborepo's build pipeline in the root `turbo.json`. The `@repo/backend#dev` task depends on the shared package being built first:\n\n```json\n{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\"]\n    },\n    \"@repo/backend#dev\": {\n      \"dependsOn\": [\"@repo/shared#build\"],\n      \"cache\": false,\n      \"persistent\": true\n    },\n    \"dev\": {\n      \"cache\": false,\n      \"persistent\": true\n    }\n  }\n}\n```\n\nThe `@repo/backend#dev` task configuration ensures the shared package is built before running `encore run` in local development.\n\n### Shared package\n\nYour shared package needs to compile TypeScript to JavaScript and expose the built output:\n\n**packages/shared/package.json:**\n```json\n{\n  \"name\": \"@repo/shared\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"tsc\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.0.0\"\n  }\n}\n```\n\n**packages/shared/tsconfig.json:**\n```json\n{\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"moduleResolution\": \"bundler\",\n    \"module\": \"ES2022\",\n    \"target\": \"ES2022\",\n    \"declaration\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n```\n\n**packages/shared/src/index.ts:**\n```ts\n// Types shared between frontend and backend\nexport interface Article {\n  slug: string;\n  title: string;\n  preview: string;\n}\n\nexport interface CreateArticleRequest {\n  title: string;\n  content: string;\n}\n\n// Utility functions\nexport function slugify(text: string): string {\n  return text\n    .toLowerCase()\n    .trim()\n    .replace(/[^\\w\\s-]/g, \"\")\n    .replace(/\\s+/g, \"-\");\n}\n\nexport function truncate(text: string, maxLength: number): string {\n  if (text.length <= maxLength) return text;\n  return text.slice(0, maxLength - 3) + \"...\";\n}\n```\n\n### Encore application\n\nThe Encore app needs two key configurations:\n\n1. **encore.app** - Use the `prebuild` hook to build dependencies during deployment\n2. **package.json** - Declare the dependency on the shared package\n\nTo create the Encore app, run `encore app init --lang ts` from the `apps/backend` directory. Then add the `prebuild` hook to the generated `encore.app` file:\n\n**apps/backend/encore.app:**\n```json\n{\n    \"id\": \"generated-id\",\n    \"lang\": \"typescript\",\n    \"build\": {\n        \"hooks\": {\n            \"prebuild\": \"npx turbo build --filter=@repo/backend^...\"\n        }\n    }\n}\n```\n\nThe `prebuild` hook runs when deploying via Encore Cloud or when exporting a Docker image with the Encore CLI. The filter `@repo/backend^...` tells Turborepo to build all dependencies of `@repo/backend`. The `^` excludes the backend itself, building only its dependencies.\n\n**apps/backend/package.json:**\n```json\n{\n  \"name\": \"@repo/backend\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"encore run\"\n  },\n  \"dependencies\": {\n    \"@repo/shared\": \"*\",\n    \"encore.dev\": \"latest\"\n  }\n}\n```\n\n### Using the shared package\n\nWith this setup, you can import from your shared package in your Encore services:\n\n**apps/backend/article/article.ts:**\n```ts\nimport { api } from \"encore.dev/api\";\nimport type { Article, CreateArticleRequest } from \"@repo/shared\";\nimport { slugify, truncate } from \"@repo/shared\";\n\nexport const create = api(\n  { expose: true, method: \"POST\", path: \"/article\" },\n  async ({ title, content }: CreateArticleRequest): Promise<Article> => {\n    return {\n      slug: slugify(title),\n      title: title,\n      preview: truncate(content, 100),\n    };\n  },\n);\n```\n\n## Running the application\n\n### Installation\n\nFirst, install all dependencies from the monorepo root:\n\n```shell\n$ npm install\n```\n\nThis installs dependencies for all workspaces, including Turborepo.\n\n### Local development\n\nFor local development, you need to build the shared packages before running `encore run`. From the monorepo root:\n\n```shell\n$ npx turbo run build\n$ cd apps/backend && encore run\n```\n\nOr use Turborepo's `dev` task which handles the dependency ordering:\n\n```shell\n$ npx turbo run dev --filter=@repo/backend\n```\n\nThe `turbo.json` configuration ensures `@repo/shared` is built before the backend's dev task runs.\n\n### Deployment\n\nWhen deploying via Encore Cloud or exporting a Docker image, the `prebuild` hook in `encore.app` automatically runs the Turborepo build pipeline.\n\n<Callout type=\"info\">\n\nWhen deploying a monorepo to Encore Cloud, configure the root path to your Encore app in the app settings: **Settings > General > Root Directory** (e.g., `apps/backend`).\n\n</Callout>\n\n## Key points\n\n- **Local development**: Run `npx turbo run build` before `encore run`, or use `npx turbo run dev --filter=@repo/backend` to handle dependency ordering automatically\n- **Prebuild hook**: The `prebuild` hook in `encore.app` runs during deployment (Encore Cloud) or Docker export, not during local development\n- **Turborepo filter**: Using `--filter=@repo/backend^...` builds only the dependencies of the backend (the `^` excludes the package itself)\n"
  },
  {
    "path": "docs/ts/develop/multithreading.md",
    "content": "---\nseotitle: Multithreading in Encore.ts\nseodesc: See how Encore.ts provides true multithreading for JavaScript applications, and how to enable Worker Pooling for CPU-intensive workloads.\ntitle: Multithreading\nsubtitle: True multithreading for JavaScript applications\nlang: ts\n---\n\nEncore.ts runs using a high-performance Rust runtime that uses multiple threads to handle incoming requests.\nThe Encore.ts Rust runtime handles virtually everything outside of your core business logic:\n\n- Parsing and validating incoming requests\n- Making API calls to other services\n- Serializing and writing API responses\n- Observability integrations like distributed tracing\n- Infrastructure integrations, like executing database queries, reading and writing from object storage, publishing and consuming messages from Pub/Sub, and more\n\nThis architecture allows for much higher performance and scalability compared to traditional JavaScript frameworks.\nBy offloading most of this to multithreaded Rust, the single-threaded JavaScript event loop becomes free to focus on executing your core business logic.\n\nBut for more CPU-intensive workloads, the single-threaded JavaScript event loop can still become a performance bottleneck.\nFor these use cases Encore.ts offers Worker Pooling. With Worker Pooling enabled, Encore.ts starts up multiple NodeJS event loops\nand load-balances incoming requests across them. This can provide a significant performance boost for CPU-intensive workloads.\n\n<img src=\"https://encore.dev/assets/blog/worker-pooling/encore-pooling.png\" className=\"bg-black p-3 brand-shadow mx-auto\" />\n\n## Enabling Worker Pooling\n\nTo enable Worker Pooling, add `\"build\": {\"worker_pooling\": true}` to your `encore.app` file.\n\n## Designing your application to work with Worker Pooling\n\nMost application code will work with Worker Pooling without any changes. However, it's important to understand\nthe implications of running in a multi-threaded environment.\n\nWhen utilizing Worker Pooling, Encore.ts will automatically spin up multiple NodeJS isolates (one per CPU) to handle incoming requests.\nEach NodeJS isolate is a separate JavaScript runtime, with its own event loop and memory space.\n\nThis means that you cannot rely on global shared state that is shared across all incoming requests,\nsince each request may be handled by a different NodeJS isolate.\n"
  },
  {
    "path": "docs/ts/develop/orms/drizzle.md",
    "content": "---\nseotitle: Using Drizzle with Encore\nseodesc: Learn how to use Drizzle with Encore to interact with SQL databases.\ntitle: Using Drizzle ORM with Encore\nlang: ts\n---\nEncore.ts supports integrating [Drizzle](https://orm.drizzle.team/), a TypeScript ORM for Node.js and the browser. To use Drizzle with Encore, start by creating a `SQLDatabase` instance and providing the connection string to Drizzle.\n \n<GitHubLink href=\"https://github.com/encoredev/examples/tree/main/ts/drizzle\" desc=\"Using Drizzle ORM with Encore.ts\" />\n\n## 1. Setting Up the Database Connection\n\nIn `database.ts`, initialize the `SQLDatabase` and configure Drizzle:\n\n```typescript\n// database.ts\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { users } from \"./schema\";\n\n// Create SQLDatabase instance with migrations configuration\nconst db = new SQLDatabase(\"test\", {\n  migrations: {\n    path: \"migrations\",\n    source: \"drizzle\",\n  },\n});\n\n// Initialize Drizzle ORM with the connection string\nconst orm = drizzle(db.connectionString);\n\n// Query all users\nawait orm.select().from(users);\n```\n\n## 2. Configuring Drizzle\n\nCreate a Drizzle configuration file `drizzle.config.ts` to specify settings like migration output, schema, and database dialect:\n\n```typescript\n// drizzle.config.ts\nimport 'dotenv/config';\nimport { defineConfig } from 'drizzle-kit';\n\nexport default defineConfig({\n  out: 'migrations',\n  schema: 'schema.ts',\n  dialect: 'postgresql',\n});\n```\n\n## 3. Defining the Database Schema\n\nDefine your database tables in `schema.ts` using Drizzle's `pg-core` module:\n\n```typescript\n// schema.ts\nimport * as p from \"drizzle-orm/pg-core\";\n\nexport const users = p.pgTable(\"users\", {\n  id: p.serial().primaryKey(),\n  name: p.text(),\n  email: p.text().unique(),\n});\n```\n\n## 4. Generating Migrations\n\nRun the following command in the directory containing `drizzle.config.ts` to generate migrations:\n\n```bash\ndrizzle-kit generate\n```\n\n## 5. Applying Migrations\n\nMigrations are automatically applied when you run your Encore application, so you don’t need to run `drizzle-kit migrate` or any similar commands manually.\n"
  },
  {
    "path": "docs/ts/develop/orms/knex.md",
    "content": "---\nseotitle: Using Knex.js with Encore\nseodesc: Learn how to use Knex.js with Encore to interact with SQL databases.\ntitle: Using Knex.js with Encore\nlang: ts\n---\nEncore.ts supports integrating [Knex.js](http://knexjs.org/), a SQL query builder for Node.js. To use Knex.js with Encore.ts, start by creating an `SQLDatabase` instance and provide its connection string to Knex.js.\n\n## 1. Setting Up the Database Connection\n\nIn `site.ts`, initialize the `SQLDatabase` and configure Knex.js:\n\n```typescript\n// site.ts\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport knex from \"knex\";\n\n// Create SQLDatabase instance with migrations configuration\nconst SiteDB = new SQLDatabase(\"siteDB\", {\n  migrations: \"./migrations\",\n});\n\n// Initialize Knex with the database connection string\nconst orm = knex({\n  client: \"pg\",\n  connection: SiteDB.connectionString,\n});\n\n// Define the Site interface\nexport interface Site {\n  id: number;\n  url: string;\n}\n\n// Query builder for the \"site\" table\nconst Sites = () => orm<Site>(\"site\");\n\n// Example queries\n\n// Query all sites\nawait Sites().select();\n\n// Query a site by id\nawait Sites().where(\"id\", id).first();\n\n// Insert a new site\nawait Sites().insert({ url: params.url });\n```\n\n## 2. Creating Migrations\n\nCurrently, Encore does not support JavaScript migration files generated by `knex migrate:make`. Instead, you can create and maintain [migration files](/docs/ts/primitives/databases#database-migrations) in SQL format.\n\nExample migration file to create the `site` table:\n\n```sql\n-- migrations/1_create_table.up.sql --\nCREATE TABLE site (\n    id SERIAL PRIMARY KEY,\n    url TEXT NOT NULL UNIQUE\n);\n```\n\n## 3. Applying Migrations\n\nEncore automatically applies migrations when you run your application. You do not need to run `knex migrate:latest` or similar commands manually.\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/knex\" \n    desc=\"Example implementation showing how to use Knex ORM with Encore.ts\" />"
  },
  {
    "path": "docs/ts/develop/orms/overview.md",
    "content": "---\nseotitle: Using ORMs with Encore.ts\nseodesc: Learn how to use ORMs with Encore.ts to seamlessly interact with SQL databases from your TypeScript / Node.js backend.\ntitle: Using ORMs and Migration Frameworks with Encore.ts\nlang: ts\n---\nEncore provides built-in support for ORMs and migration frameworks by offering named databases and SQL-based migration files. For developers who prefer not to write raw SQL, Encore allows seamless integration with popular ORMs and migration tools.\n\n## Overview\n\nEncore’s approach to database management is flexible. It uses standard SQL migration files, allowing integration with ORMs like [Sequelize](https://sequelize.org/) and migration tools like [Atlas](https://atlasgo.io/).\n\n- **ORM Compatibility:** If your ORM can connect to a database via a standard SQL driver, it will work with Encore.\n- **Migration Tool Compatibility:** If your migration tool generates SQL migration files without additional customization, it can be used with Encore.\n\n## Connecting to a Database\n\nEncore provides the `SQLDatabase` class, which allows you to create a named database and retrieve its connection string. This connection string can be used by your chosen ORM or migration framework to establish a database connection.\n\nExample setup:\n\n```typescript\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\n// Initialize a named database with migration directory\nconst SiteDB = new SQLDatabase(\"siteDB\", {\n  migrations: \"./migrations\",\n});\n\n// Retrieve the connection string for ORM use\nconst connStr = SiteDB.connectionString;\n```\n\n## Example ORM implementations\n\nHere are some guides to using different ORMs with Encore:\n\n- [Using Knex.js with Encore](/docs/ts/develop/orms/knex)\n- [Using Sequelize with Encore](/docs/ts/develop/orms/sequelize)\n- [Using Drizzle with Encore](/docs/ts/develop/orms/drizzle)\n- [Using Prisma with Encore](/docs/ts/develop/orms/prisma)\n\nThis setup enables Encore to support a wide variety of ORMs and migration frameworks, making database management both flexible and straightforward."
  },
  {
    "path": "docs/ts/develop/orms/prisma.md",
    "content": "---\nseotitle: Using Prisma with Encore.ts\nseodesc: Learn how to use Prisma with Encore to interact with SQL databases.\ntitle: Using Prisma with Encore.ts\nlang: ts\n---\n\n[Prisma](https://prisma.io/) is a modern TypeScript ORM that provides type-safe database access and migrations. With Prisma, you define your database schema in a `schema.prisma` file and use Prisma's CLI to generate SQL migrations and a TypeScript client.\n\nThis guide explains how to integrate Prisma with Encore.ts, leveraging Encore's built-in database management while using Prisma's powerful ORM features.\n\n## How Prisma works with Encore\n\nEncore and Prisma work together seamlessly:\n- **Prisma** generates the migration files and TypeScript client\n- **Encore** manages database creation, connections, and applies migrations\n- You use Encore's `SQLDatabase` to provide connection strings to Prisma\n\nThe key to this integration is configuring Prisma to use Encore's shadow database for its operations, preventing any conflicts between the two systems.\n\n## Quick Example\n\nHere's a complete example of using Prisma with Encore.ts:\n\n```ts\n-- users/database.ts --\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\n// Define a database named 'users', using the database migrations\n// in the \"./prisma/migrations\" folder (generated by Prisma).\nexport const DB = new SQLDatabase(\"users\", {\n  migrations: {\n    path: \"./prisma/migrations\",\n    source: \"prisma\",\n  },\n});\n\n-- users/prisma/schema.prisma --\ngenerator client {\n  provider        = \"prisma-client\"\n  output          = \"./generated\"\n  previewFeatures = [\"queryCompiler\", \"driverAdapters\"]\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  // Connect Prisma CLI to Encore's shadow database\n  // This prevents interference with Encore's migration system\n  url      = env(\"SHADOW_DB_URL\")\n}\n\nmodel User {\n  id        Int      @id @default(autoincrement())\n  email     String   @unique\n  name      String\n  createdAt DateTime @default(now())\n}\n\n-- users/prisma/client.ts --\nimport { PrismaClient } from \"./generated/client\";\nimport { PrismaPg } from \"@prisma/adapter-pg\";\nimport { DB } from \"../database\";\n\n// Create and export the Prisma client instance\nexport const prisma = new PrismaClient({\n  adapter: new PrismaPg({ connectionString: DB.connectionString }),\n});\n\n// Re-export types from the generated client\nexport * from \"./generated/client\";\n\n-- users/api.ts --\nimport { api } from \"encore.dev/api\";\nimport { prisma } from \"./prisma/client\";\n\ninterface CreateUserRequest {\n  email: string;\n  name: string;\n}\n\n// Example API endpoint using Prisma\nexport const createUser = api(\n  { method: \"POST\", path: \"/users\", expose: true },\n  async (req: CreateUserRequest) => {\n    const user = await prisma.user.create({\n      data: req,\n    });\n    return user;\n  }\n);\n```\n\n<GitHubLink\nhref=\"https://github.com/encoredev/examples/tree/main/ts/prisma\"\ndesc=\"Complete Prisma + Encore.ts example\"\n/>\n\n## Step-by-Step Setup\n\n### 1. Install Dependencies\n\nFirst, install Prisma and its required dependencies:\n\n```bash\nnpm install prisma --save-dev\nnpm install @prisma/client @prisma/adapter-pg dotenv --save\n```\n\n### 2. Create Project Structure\n\nCreate the following directory structure for your service:\n\n```\nmy-service/\n├── database.ts\n├── prisma/\n│   ├── schema.prisma\n│   ├── migrations/\n│   ├── generated/     (will be created by Prisma)\n│   └── client.ts\n└── api.ts\n```\n\n### 3. Set Up the Database\n\nCreate `database.ts` to define your Encore database:\n\n```ts\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\n// Export the database so it can be used in the Prisma client\nexport const DB = new SQLDatabase(\"myapp\", {\n  migrations: {\n    path: \"./prisma/migrations\",\n    source: \"prisma\",\n  },\n});\n```\n\nRun `encore run` to create the database (make sure the migrations folder has been created first).\n\n### 4. Configure Database Connections\n\nPrisma needs to connect to Encore's shadow database for migration operations. The shadow database is a temporary database that Prisma uses to detect schema drift and generate migrations without affecting your main database.\n\nGet the connection strings:\n\n```bash\n# Main database connection (for Prisma Studio)\nencore db conn-uri myapp\n\n# Shadow database connection (for Prisma CLI operations)\nencore db conn-uri myapp --shadow\n```\n\nCreate a `.env` file in your project root:\n\n```\n# Connection strings for local development\nDB_URL=<main-database-connection-string>\nSHADOW_DB_URL=<shadow-database-connection-string>\n```\n\n### 5. Create Prisma Configuration\n\nCreate `prisma.config.ts` in your project root:\n\n```ts\nimport \"dotenv/config\";\nimport type { PrismaConfig } from \"prisma\";\nimport { PrismaPg } from \"@prisma/adapter-pg\";\n\ntype Env = {\n  DB_URL: string;\n};\n\nexport default {\n  earlyAccess: true,\n  schema: \"./my-service/prisma/schema.prisma\",\n  studio: {\n    adapter: async (env: Env) => {\n      // Connect Prisma Studio to the main Encore database\n      return new PrismaPg({ connectionString: env.DB_URL });\n    },\n  },\n} satisfies PrismaConfig<Env>;\n```\n\n### 6. Create Your Prisma Schema\n\nCreate `my-service/prisma/schema.prisma`:\n\n```\ngenerator client {\n  provider        = \"prisma-client\"\n  output          = \"./generated\"\n  previewFeatures = [\"queryCompiler\", \"driverAdapters\"]\n}\n\ndatasource db {\n  provider = \"postgresql\"\n  // IMPORTANT: Use shadow database URL for Prisma CLI operations\n  url      = env(\"SHADOW_DB_URL\")\n}\n\n// Define your models here\nmodel User {\n  id        Int      @id @default(autoincrement())\n  email     String   @unique\n  name      String\n}\n```\n\n### 7. Create the Prisma Client Wrapper\n\nCreate `my-service/prisma/client.ts`:\n\n```ts\nimport { PrismaClient } from \"./generated/client\";\nimport { PrismaPg } from \"@prisma/adapter-pg\";\nimport { DB } from \"../database\";\n\n// Create and export the Prisma client instance\nexport const prismaClient = new PrismaClient({\n  adapter: new PrismaPg({ connectionString: DB.connectionString }),\n});\n\n// Re-export types from the generated client\nexport * from \"./generated/client\";\n```\n\n### 8. Generate Initial Migration\n\nGenerate and apply your first migration:\n\n```bash\nnpx prisma migrate dev --name init\nencore run\n```\n\nThis will:\n1. Create the `prisma/migrations` directory\n2. Generate SQL migration files\n3. Generate the Prisma client in `prisma/generated`\n4. Apply the migration to your development database\n\n### 9. Use Prisma in Your Code\n\nNow you can use Prisma in your API endpoints:\n\n```ts\nimport { api, APIError } from \"encore.dev/api\";\nimport { prismaClient, Prisma } from \"./prisma/client\";\n\ninterface CreateUserRequest {\n  email: string;\n  name: string;\n}\n\nexport const createUser = api(\n  { method: \"POST\", path: \"/users\", expose: true },\n  async (req: CreateUserRequest) => {\n    try {\n      const user = await prismaClient.user.create({\n        data: req,\n      });\n      return user;\n    } catch (error) {\n      if (error instanceof Prisma.PrismaClientKnownRequestError) {\n        if (error.code === \"P2002\") {\n          throw APIError.alreadyExists(\"User with this email already exists\");\n        }\n      }\n      throw error;\n    }\n  },\n);\n\nexport const getUsers = api(\n  { method: \"GET\", path: \"/users\", expose: true },\n  async (): Promise<{\n    users: { name: string; email: string; id: number }[];\n  }> => {\n    return { users: await prismaClient.user.findMany() };\n  },\n);\n```\n\n## Working with Migrations\n\n### Generate New Migrations\n\nWhen you make changes to your `schema.prisma` file:\n\n```bash\nnpx prisma migrate dev --name describe-your-change\n```\n\nExample:\n```bash\nnpx prisma migrate dev --name add-user-role\n```\n\n### How Migrations Work\n\n1. **Prisma generates** SQL migration files based on your schema changes\n2. **Encore applies** these migrations automatically:\n   - Locally when you run `encore run`\n   - In cloud environments during deployment\n   - During tests when using `encore test`\n\n## Deployment\n\n### Generate Client During Build\n\nFor Encore Cloud deployments, add to your `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"postinstall\": \"npx prisma generate\"\n  }\n}\n```\n\nThis ensures the Prisma client is generated during deployment.\n\n### Production Considerations\n\n- Encore automatically provides database connection strings in production\n- No environment variables need to be configured for production\n- Migrations are applied automatically during deployment\n\n## Using Prisma Studio\n\nPrisma Studio provides a GUI for your database:\n\n```bash\nnpx prisma studio\n```\n\nOpens at `http://localhost:5555` where you can:\n- Browse and filter data\n- Create, update, and delete records\n- Explore relationships\n\n## Troubleshooting\n\n### Connection Issues\n\nIf Prisma or Prisma Studio can't connect:\n1. Verify connection strings in `.env`\n2. Restart Encore (`encore run`)\n"
  },
  {
    "path": "docs/ts/develop/orms/sequelize.md",
    "content": "---\nseotitle: Using Sequelize with Encore.ts\nseodesc: Learn how to use Sequelize with Encore to interact with SQL databases.\ntitle: Using Sequelize with Encore.ts\nlang: ts\n---\nEncore.ts supports integrating [Sequelize](https://sequelize.org/), a promise-based Node.js ORM. To set up Sequelize with Encore, start by creating a `SQLDatabase` instance and providing the connection string to Sequelize.\n\n## 1. Setting Up the Database Connection\n\nIn `database.ts`, initialize the `SQLDatabase` and configure Sequelize:\n\n```typescript\n// database.ts\nimport {\n  Model,\n  InferAttributes,\n  InferCreationAttributes,\n  CreationOptional,\n  DataTypes,\n  Sequelize,\n} from \"sequelize\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\n// Create SQLDatabase instance with migrations configuration\nconst DB = new SQLDatabase(\"encore_sequelize_test\", {\n  migrations: \"./migrations\",\n});\n\n// Initialize Sequelize with the connection string\nconst sequelize = new Sequelize(DB.connectionString);\n\n// Define the User model\nclass User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {\n  declare id: CreationOptional<number>;\n  declare name: string;\n  declare surname: string;\n}\n\n// Example usage: Count all users\nconst count = await User.count();\n```\n\n## 2. Creating Migrations\n\nEncore does not currently support JavaScript migration files generated by tools like `sequelize-cli model:generate`. Instead, create and manage your own [migration files](/docs/ts/primitives/databases#database-migrations) in SQL format.\n\nExample migration file for creating the `user` table:\n\n```sql\n-- migrations/1_create_user.up.sql --\nCREATE TABLE \"user\" (\n  id SERIAL PRIMARY KEY,\n  name TEXT NOT NULL,\n  surname TEXT NOT NULL\n);\n```\n\n## 3. Applying Migrations\n\nMigrations are automatically applied when you run your Encore application, so you don’t need to run `sequelize db:migrate` or similar commands manually.\n\n--- \n\nFor more information, see the example on GitHub:  \n<GitHubLink href=\"https://github.com/encoredev/examples/tree/main/ts/sequelize\" desc=\"Using Sequelize ORM with Encore.ts\" />"
  },
  {
    "path": "docs/ts/develop/running-scripts.md",
    "content": "---\nseotitle: How to use `encore exec` for running scripts\nseodesc: Learn how to use the `encore exec` command to run scripts like database seeding in your Encore app.\ntitle: Running Scripts\nsubtitle: Run scripts with your application's infrastructure and runtime configured and initialized\nlang: ts\n---\nIn local development, you may need to run scripts or commands, such as seeding a database with initial data.\nFor that to work the database needs to be started, and the Encore runtime needs to be configured and initialized.\n\n## Using `encore exec`\n\nThe `encore exec` command allows you to execute custom commands while leveraging Encore's infrastructure setup. This is particularly useful for tasks like database seeding, running scripts, or other one-off commands that require the app's environment to be initialized.\n\n### How it works\n\nThe `encore exec` command initializes the required infrastructure for your Encore app and executes the specified command.\nThis ensures that your commands run in the correct context with all dependencies properly configured.\n\n### Example: Database Seeding\n\nIn this example, `npx tsx ./seed.ts` runs a TypeScript script (`seed.ts`) to populate the database with initial data\n\n```bash\nencore exec -- npx tsx ./seed.ts\n```\n\nHere’s what happens:\n1. Encore initializes the app infrastructure.\n2. The `npx tsx ./seed.ts` command is executed in the context of the initialized app.\n\n### General Syntax\n\n```bash\nencore exec -- <your-command>\n```\n\nSubstitute `<your-command>` with the specific command you wish to run.\n\n### Use Cases\n\n- **Database Seeding**: Populate your database with initial data using a script.\n- **Client Generation**: Generate a client for interacting with an external dependency.\n- **Custom Scripts**: Run any script that depends on the app's initialized environment.\n\n### Notes\n\n- Ensure that the command you provide is executable in your environment.\n- Use `--` to separate `encore exec` options from the command you want to run.\n\n"
  },
  {
    "path": "docs/ts/develop/testing.md",
    "content": "---\nseotitle: Automated testing for your backend application\nseodesc: Learn how create automated tests for your microservices backend application, and run them automatically on deploy using Go and Encore.\ntitle: Automated testing\nsubtitle: Confidence at speed\nlang: ts\n---\n\nEncore provides built-in testing tools that make it simple to test your application using a variety of test runners.\n\nTo run tests with Encore:\n\n1. Configure the `test` command in your `package.json` to use the test runner of your choice.\n2. Configure your test runner.\n3. Run `encore test` from the CLI.\n\nThe `encore test` command automatically sets up all necessary infrastructure in test mode before running your tests.\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/uptime\" \n    desc=\"Uptime monitoring app with API endpoint unit tests written in Vitest.\" \n/>\n\n## Recommended Setup: Vitest\n\nWe recommend [Vitest](https://vitest.dev) as your test runner because it offers:\n- Fast execution\n- Native ESM and TypeScript support\n- Jest API compatibility\n\n### Setting up Vitest\n\n1. Create `vite.config.ts` in your application's root directory:\n\n```ts\n/// <reference types=\"vitest\" />\nimport { defineConfig } from \"vite\";\nimport path from \"path\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"~encore\": path.resolve(__dirname, \"./encore.gen\"),\n    },\n  },\n});\n```\n\n2. Update your `package.json` to include:\n\n```json\n{\n  \"scripts\": {\n    \"test\": \"vitest\"\n  }\n}\n```\n\nYou're done! Now you can run your tests with `encore test`.\n\n### Optional: IDE Integration\n\n#### VS Code Setup\n\nIf using Vitest, follow these steps:\n1. Install the official Vitest VS Code extension\n2. Add to `.vscode/settings.json`:\n\n```json\n{\n  \"vitest.commandLine\": \"encore test\"\n}\n```\n\nAs of Vitest plugin version 0.5 ([issue](https://github.com/vitest-dev/vscode/issues/306)), environment configuration requires an updated approach. The following configuration is required to ensure proper functionality:\n\nUpdate `settings.json` to include:\n\n```json\n\"vitest.nodeEnv\": {\n    // generated with `encore daemon env | grep ENCORE_RUNTIME_LIB | cut -d'=' -f2`\n    \"ENCORE_RUNTIME_LIB\": \"/opt/homebrew/Cellar/encore/1.44.5/libexec/runtimes/js/encore-runtime.node\"\n}\n```\n\nWhen running tests within VSCode, file-level parallel execution must be disabled. Update your `vite.config.ts` as follows:\n\n```typescript\n// File vite.config.ts\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"~encore\": path.resolve(__dirname, \"./encore.gen\"),\n    },\n  },\n  test: {\n    fileParallelism: false,\n  },\n});\n```\n\nTo improve the performance in CI, you can re-enable the parallel execution by overwriting the config in cli `encore test --fileParallelism=true`.\n\n## Integration Testing Best Practices\n\nEncore applications typically focus on integration tests rather than unit tests because:\n\n- Encore eliminates most boilerplate code\n- Your code primarily consists of business logic involving databases and inter-service API calls\n- Integration tests better verify this type of functionality\n\n### Test Environment Benefits\n\nWhen running tests, Encore automatically:\n- Sets up separate test databases\n- Configures databases for optimal test performance by:\n  - Skipping `fsync`\n  - Using in-memory filesystems\n  - Removing durability overhead\n\nThese optimizations make integration tests nearly as fast as unit tests.\n\n"
  },
  {
    "path": "docs/ts/faq.md",
    "content": "---\nseotitle: Frequently Asked Questions\nseodesc: See quick answers to common questions about Encore\ntitle: FAQ\nsubtitle: Quick answers to common questions\nlang: ts\n---\n\n## About the project\n\n**Is Encore Open Source?**\n\nYes, check out the project on [GitHub](https://github.com/encoredev/encore).\n\n**Is there a community?**\n\nYes, you're welcome to join the developer community on [Discord](https://encore.dev/discord).\n\n## Can I use X with Encore?\n\n**Can I use Python with Encore?**\n\nEncore currently supports Go and TypeScript. Python support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026.\n\n**Can mix TypeScript and Go in one application?**\n\nSupport for mixing languages in coming. Currently, if you want to use both TypeScript and Go, you need to create a separate application per language and integrate using APIs.\n\n**Can I use Azure / Digital Ocean?**\n\nEncore Cloud currently supports automating deployments to AWS and GCP. Azure support in on the [roadmap](https://encore.dev/roadmap) and will be available in 2026.\n\nIf you want to use other cloud providers like Azure or Digital Ocean, you can follow the [self-hosting instructions](/docs/how-to/self-host).\n\n**Can I use MongoDB / MySQL with Encore?**\n\nEncore currently has built-in support for PostgreSQL. To use another type of database, like MongoDB and MySQL, you will need to set it up and integrate as you normally would when not using Encore.\n\n**Can I use AWS lambda with Encore?**\n\nNot right now. Encore currently supports AWS Fargate and EKS (along with CloudRun and GKE on Google Cloud Platform).\n\n**Can I use Bun / Deno with Encore.ts?**\n\nRight now Encore.ts officially supports Node and has experimental support for Bun. Deno support is on the way. Note that Encore.ts already provides performance improvements thanks to its Rust-based runtime. [Learn more](https://encore.dev/blog/event-loops).\n\nTo enable the Bun experiment, add `\"experiments\": [\"bun-runtime\"]` to your `encore.app` file, and add `\"packageManager\": \"bun\"` to your `package.json` file.\n\n## IDE Integrations\n\n**Is there an Encore plugin for Goland / IntelliJ?**\n\nYes, Encore's official Goland plugin is available in the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/20010-encore).\n\n**Is there an Encore plugin for VS Code?**\n\nNot yet, it's coming soon.\n\n## Troubleshooting\n\n**symlink creation error on Windows**\n\nEncore currently relies on symbolic links, which may be disabled by default. A common fix for this issue is to enable \"developer mode\" in the Windows settings (Settings > System > For developers > Developer mode).\n\n**`node` errors**\n\nYou might need to restart the Encore daemon, e.g. if your PATH has changed since installing nvm. Restart the daemon by running `encore daemon`.\n"
  },
  {
    "path": "docs/ts/frontend/cors.md",
    "content": "---\nseotitle: Handling CORS (Cross-Origin Resource Sharing)\nseodesc: See how you can configure CORS for your Encore application.\ntitle: CORS\nsubtitle: Configure CORS (Cross-Origin Resource Sharing) for your Encore application\nlang: ts\n---\n\nCORS is a web security concept that defines which website origins are allowed to access your API.\n\nA deep-dive into CORS is out of scope for this documentation, but [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\nprovides a good overview. In short, CORS affects requests made by browsers to resources hosted on\nother origins (a combination of the scheme, domain, and port).\n\n## Configuring CORS\n\nEncore provides a default CORS configuration that is suitable for many APIs. You can override these settings\nby specifying the `global_cors` key in the `encore.app` file, which has the following\nstructure:\n\n```cue\n{\n    // debug enables CORS debug logging.\n    \"debug\": true | false,\n\n    // allow_headers allows an app to specify additional headers that should be\n    // accepted by the app.\n    //\n    // If the list contains \"*\", then all headers are allowed.\n    \"allow_headers\": [...string],\n\n    // expose_headers allows an app to specify additional headers that should be\n    // exposed from the app, beyond the default set always recognized by Encore.\n    //\n    // If the list contains \"*\", then all headers are exposed.\n    \"expose_headers\": [...string],\n\n    // allow_origins_without_credentials specifies the allowed origins for requests\n    // that don't include credentials. If nil it defaults to allowing all domains\n    // (equivalent to [\"*\"]).\n    \"allow_origins_without_credentials\": [...string],\n\n    // allow_origins_with_credentials specifies the allowed origins for requests\n    // that include credentials. If a request is made from an Origin in this list\n    // Encore responds with Access-Control-Allow-Origin: <Origin>.\n    //\n    // The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n    // or \"https://*-myapp.example.com\").\n    \"allow_origins_with_credentials\": [...string],\n}\n```\n\n## Allowed origins\n\nThe main CORS configuration is the list of allowed origins, meaning which websites are allowed\nto access your API (via browsers).\n\nFor this purpose, CORS makes a distinction between requests that contain authentication information\n(cookies, HTTP authentication, or client certificates) and those that do not. CORS applies stricter\nrules to authenticated requests.\n\nBy default, Encore allows unauthenticated requests from all origins but disallows requests that do\ninclude authorization information from other origins. This is a good default for many APIs.\nThis can be changed by setting the `allow_origins_without_credentials` key (see above).\nFor convenience Encore also allows all origins when developing locally.\n\nFor security reasons it's necessary to explicitly specify which origins are allowed to make\nauthenticated requests. This is done by setting the `allow_origins_with_credentials` key (see above).\n\n## Allowed headers and exposed headers\n\nCORS also lets you specify which headers are allowed to be sent by the client (\"allowed headers\"),\nand which headers are exposed to scripts running in the browser (\"exposed headers\").\n\nEncore automatically configures headers by parsing your program using static analysis.\nIf your API defines a request or response type that contains a header field, Encore automatically adds the header to\nthe list of exposed and allowed headers in request types respectively.\n\nTo add additional headers to these lists, you can set the `allow_headers` and `expose_headers` keys (see above).\nThis can be useful when your application relies on custom headers in e.g. raw endpoints that aren't seen by Encore's\nstatic analysis.\n"
  },
  {
    "path": "docs/ts/frontend/hosting.mdx",
    "content": "---\nseotitle: Integrate your backend application with a frontend\nseodesc: Learn how to host your frontend when having a Encore.ts backend application.\ntitle: Hosting a frontend\nsubtitle: Keep using your favorite frontend hosting provider\nlang: ts\n---\n\nEncore is not opinionated about where you host your frontend, pick the platform that suits your situation best.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/nextjs-starter\"\n    desc=\"Next.js + Encore TS Web App Starter. Frontend hosted on Vercel, backend on Encore.\"\n/>\n\n## Hosting a frontend using Encore\nEncore is primarily designed for backend development. It is possible to serve a frontend using Encore but for production we recommend that you deploy your frontend using Vercel, Netlify, or a similar service.\n\n### Template engines\n\nYou can make use of template engines like Handlebars, Pug, or EJS to render HTML files on the server. Learn more about in the [Template Engine](/docs/ts/frontend/template-engine) docs.\n\n### Serving static assets\n\nYou can create a `api.static` endpoint that serves static frontend assets, including HTML files.\n\n```ts\nimport { api } from \"encore.dev/api\";\n\n// Using fallback route to serve all files in the ./assets directory under the root path.\nexport const rootAssets = api.static({\n  expose: true,\n  path: \"/!path\",\n  dir: \"./assets\",\n  // When a file matching the request isn't found, Encore automatically serves a 404 Not Found response.\n  // You can customize the response by setting the notFound option to specify a file that should be served instead:\n  notFound: \"./assets/not_found.html\",\n});\n\n```\n\nKeep in mind that this approach will not work if you have a Single-Page Application (SPA) that uses client-side routing.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/static-files\"\n    desc=\"Static files example, sercing HTML files from a directory.\"\n/>\n"
  },
  {
    "path": "docs/ts/frontend/mono-vs-multi-repo.mdx",
    "content": "---\nseotitle: Integrate your backend application with a frontend\nseodesc: Learn how to structure your application, using a Monorepo or a Multi-repo approach.\ntitle: Mono vs Multi Repo\nsubtitle: How to structure your frontend and backend\nlang: ts\n---\n\nEncore is not opinionated about if you have your backend and frontend code in the same repo or not. Pick the approach that fits your application best.\n\n## Monorepo\n\nIf you use a monorepo then it is often a good idea to place your backend and frontend in separate folders in the root of your repo, like so:\n\n```\n/my-app\n├── backend\n│   ├── encore.app\n│   ├── package.json // Backend dependencies\n│   └── ...\n└── frontend\n    ├── package.json // Frontend dependencies\n    └── ...\n```\n\nThis way, you can keep your frontend and backend dependencies separate, while still having the codebases in the same repository. If you are using Encore Cloud for deployment, remember to configure the \"Root Directory\" in app settings in the [Encore Cloud dashboard](https://app.encore.cloud) to point to where you have your `encore.app` file.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/nextjs-starter\"\n    desc=\"Next.js + Encore Starter, separated into frontend and backend folders.\"\n/>\n\nYou can also have a monorepo where the `encore.app` file is in the root of the repo, the frontend code will then be inside your Encore app. If you go this route you will most likely need two different `tsconfig.json` files, one for the frontend and one for the backend.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/react-starter\"\n    desc=\"React + Encore Starter, frontend code inside the Encore app.\"\n/>\n"
  },
  {
    "path": "docs/ts/frontend/request-client.mdx",
    "content": "---\nseotitle: Get type-safe requests between your backend and frontend\nseodesc: Learn how to use Encore's built-in client generation to get type-safety between your backend and frontend.\ntitle: Request client for the frontend\nsubtitle: Get type-safety between your backend and frontend\nlang: ts\n---\n\nEncore is able to generate frontend request clients (TypeScript or JavaScript). This lets you to keep the request/response types in sync without manual work and assists you in calling the APIs. Generate a client by running:\n\n```bash\n$ encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=<ENV_NAME>\n```\n\nAdding this as a script to your `package.json` is often a good idea to be able to run it whenever a change is made to your Encore API:\n\n```json\n{\n...\n\"scripts\": {\n    ...\n    \"generate-client:staging\": \"encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=staging\",\n    \"generate-client:local\": \"encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=local\"\n  }\n}\n```\n\nAfter that you are ready to use the request client in your code. In this example, the frontend is calling the `GetNote` endpoint on the `note` service in order to retrieve a specific meeting note (which has the properties `id`, `cover_url` & `text`):\n\n```ts\nimport Client, { Environment, Local } from \"src/client.ts\";\n\n// Making request to locally running backend...\nconst client = new Client(Local);\n// or to a specific deployed environment\n// const client = new Client(Environment(\"staging\"));\n\n// Calling APIs as typesafe functions 🌟\nconst response = await client.note.GetNote(\"note-uuid\");\nconsole.log(response.id);\nconsole.log(response.cover_url);\nconsole.log(response.text);\n```\n\nSee more in the [client generation docs](/docs/ts/cli/client-generation).\n\n### Asynchronous state management\n\nWhen building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale.\n[TanStack Query](https://tanstack.com/query/latest) is a popular library that was built to solve exactly these problems and works well with the Encore request client.\n\nHere is a simple example of using an Encore request client together with TanStack Query:\n\n```ts\nimport {\n  useQuery,\n  useMutation,\n  useQueryClient,\n  QueryClient,\n  QueryClientProvider,\n} from '@tanstack/react-query'\nimport Client, { todo } from '../encore-client'\n\n// Create a Encore client\nconst encoreClient = new Client(window.location.origin);\n\n// Create a react-query client\nconst queryClient = new QueryClient()\n\nfunction App() {\n  return (\n    // Provide the client to your App\n    <QueryClientProvider client={queryClient}>\n      <Todos />\n    </QueryClientProvider>\n  )\n}\n\nfunction Todos() {\n  // Access the client\n  const queryClient = useQueryClient()\n\n  // Queries\n  const query = useQuery({\n    queryKey: ['todos'],\n    queryFn: () => encoreClient.todo.List()\n  })\n\n  // Mutations\n  const mutation = useMutation({\n    mutationFn: (params: todo.AddParams) => encoreClient.todo.Add(params),\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: ['todos'] })\n    },\n  })\n\n  return (\n    <div>\n      <ul>\n        {query.data?.map((todo) => (\n          <li key={todo.id}>{todo.title}</li>\n        ))}\n      </ul>\n\n      <button\n        onClick={() => {\n          mutation.mutate({\n            id: Date.now(),\n            title: 'Do Laundry',\n          })\n        }}\n      >\n        Add Todo\n      </button>\n    </div>\n  )\n}\n\nrender(<App />, document.getElementById('root'))\n```\n\nThis example assumes that we have a `todo` service with a `List` and `Add` endpoint. When adding the new todo,\nTanStack Query will automatically invalidate the `todos` query and refetch it.\n\nFor a real-world example, take a look at the [Uptime Monitoring](https://github.com/encoredev/examples/tree/main/uptime) app which also makes use of\nTanStack Query's `refetchInterval` option for polling the backend.\n\n### Testing\nWhen unit testing a component that interacts with your Encore API you can mock methods on the request client to\nreturn a value suitable for the test. This makes your test URL agnostic because you are not intercepting\nspecific requests on the fetch layer. You also get type errors in your tests if the request client gets updated.\n\nHere is an example from the [Uptime Monitoring Starter](https://github.com/encoredev/examples/tree/main/uptime) where we are mocking a GET request method and spying on a POST request method:\n\n```ts\nimport { render, waitForElementToBeRemoved } from \"@testing-library/react\";\nimport App from \"./App\";\nimport { site } from \"./client\";\nimport { userEvent } from \"@testing-library/user-event\";\n\ndescribe(\"App\", () => {\n  beforeEach(() => {\n    // Return mocked data from the List (GET) endpoint\n    jest\n      .spyOn(site.ServiceClient.prototype, \"List\")\n      .mockReturnValue(Promise.resolve({\n        sites: [{\n          id: 1,\n          url: \"test.dev\"\n        }]\n      }));\n\n    // Spy on the Add (POST) endpoint\n    jest.spyOn(site.ServiceClient.prototype, \"Add\");\n  });\n\n  it(\"render sites\", async () => {\n    render(<App />);\n    await waitForElementToBeRemoved(() => screen.queryByText(\"Loading...\"));\n\n    // Verify that the List endpoint has been called\n    expect(site.ServiceClient.prototype.List).toBeCalledTimes(1);\n\n    // Verify that the sites are rendered with our mocked data\n    screen.getAllByText(\"test.dev\");\n  });\n\n  it(\"add site\", async () => {\n    render(<App />);\n    await waitForElementToBeRemoved(() => screen.queryByText(\"Loading...\"));\n\n    // Interact with the page and add 'another.com'\n    await userEvent.click(screen.getByText(\"Add website\"));\n    await userEvent.type(\n      screen.getByPlaceholderText(\"google.com\"),\n      \"another.com\",\n    );\n    await userEvent.click(screen.getByText(\"Save\"));\n\n    // Verify that the Add endpoint has been called with the correct parameters\n    expect(site.ServiceClient.prototype.Add).toHaveBeenCalledWith({\n      url: \"another.com\",\n    });\n  });\n})\n```\n\n<Callout type=\"info\">\n\n  In the example above we need to mock the `List` method on `site.ServiceClient.prototype` because the request client has not\n  yet been initialized when we're creating the mock. If you have access to the instance of the request client in your test\n  (which could be the case if you are passing the client around in your components) you can instead do `jest.spyOn(client.site, \"List\")`\n  and `expect(client.site.List).toHaveBeenCalled()` which would give you the same result.\n\n</Callout>\n\n## REST vs. GraphQL\nEncore allows for building backends using both REST and GraphQL, you should pick the approach that suits your use case best. Encore's request client only works for REST APIs so if you choose to build a GraphQL backend you will need to use another request library for your frontend.\n\nTake a look at the [GraphQL tutorial](/docs/ts/tutorials/graphql) for an example of building a GraphQL backend with Encore.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/graphql\"\n    desc=\"Example of how to build an Apollo GraphQL server with Encore.ts, implementing a basic book CRUD service.\"\n/>\n\n"
  },
  {
    "path": "docs/ts/frontend/template-engine.md",
    "content": "---\nseotitle: How to use a template engine in you Encore.ts application\nseodesc: Learn how to use a template engine to create server-rendered HTML with dynamic data.\ntitle: Use a template engine\nlang: ts\n---\n\nIn this guide you will learn how to use a template engine, like [EJS](https://ejs.co) and [Handlebars](https://handlebarsjs.com), to create server-rendered HTML views.\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/template-engine\" \n    desc=\"Using EJS as a template engine with Encore.ts\" \n/>\n\n## Serving a specific template file\n\nBreakdown of the example:\n* We import a NPM package for rendering templates, in this case [EJS](https://ejs.co/).\n* We have a [Raw Endpoint](/docs/ts/primitives/raw-endpoints) to handle template rendering, in this case we are serving a specific template file (`person.html`) under `/person`. \n* We make use of the EJS to render the template with the given data.\n* We set the `content-type` header to `text-html` and then respond with the generated HTML.\n\n```ts\n-- template/template.ts --\nimport { api } from \"encore.dev/api\";\nimport ejs, { Options } from \"ejs\";\n\nconst BASE_PATH = \"./template/views\";\nconst ejsOptions: Options = { views: [BASE_PATH] };\n\nexport const serveSpecificTemplate = api.raw(\n  { expose: true, path: \"/person\", method: \"GET\" },\n  async (req, resp) => {\n    const viewPath = `${BASE_PATH}/person.html`;\n    const html = await ejs.renderFile(\n      viewPath,\n      // Supplying data to the view\n      { name: \"Simon\" },\n      ejsOptions,\n    );\n    resp.setHeader(\"content-type\", \"text/html\");\n    resp.end(html);\n  },\n);\n-- template/views/person.html --\n<h1>Person Page</h1>\n<p>Name: <%= name %></p>\n```\n\n## Serving from a dynamic path\n\nThis example is similar to the one above, but in this case we use a fallback path to serve a template file based on the path. We use the `currentRequest` function to get the `path` and then render the template file based on the `path`. If no path is provided, we default to `index.html`.\n\n```ts\nimport { api } from \"encore.dev/api\";\nimport { APICallMeta, currentRequest } from \"encore.dev\";\nimport ejs, { Options } from \"ejs\";\n\nconst BASE_PATH = \"./template/views\";\nconst ejsOptions: Options = { views: [BASE_PATH] };\n\nexport const servePathTemplate = api.raw(\n  { expose: true, path: \"/!path\", method: \"GET\" },\n  async (req, resp) => {\n    const { path } = (currentRequest() as APICallMeta).pathParams;\n    const viewPath = `${BASE_PATH}/${path ?? \"index\"}.html`;\n    const html = await ejs.renderFile(viewPath, ejsOptions);\n    resp.setHeader(\"content-type\", \"text/html\");\n    resp.end(html);\n  },\n);\n```\n\n## Serving inline HTML\n\nIn this example we are serving inline HTML with EJS. We use the `ejs.render` function to render the inline HTML with the given data.\n\n```ts\nimport { api } from \"encore.dev/api\";\nimport ejs, { Options } from \"ejs\";\n\nconst inlineHTML = `\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"stylesheet\" href=\"/public/styles.css\" >\n  </head>\n  <body>\n    <h1>Static Inline HTML Example</h1>\n    <h1>Name: <%= name %>!</h1>\n  </body>\n</html>\n`;\n\nexport const serveInlineHTML = api.raw(\n  { expose: true, path: \"/html\", method: \"GET\" },\n  async (req, resp) => {\n    const html = ejs.render(inlineHTML, { name: \"Simon\" });\n    resp.setHeader(\"Content-Type\", \"text/html\");\n    resp.end(html);\n  },\n);\n```\n\n## Static files\n\nIn the above example we are fetching a stylesheet from the `/public` path. We can use the `api.static` function to serve all files in the `./assets` directory under the `/public` path prefix:\n\n```ts\n// Serve all files in the ./assets directory under the /public path prefix.\nexport const assets = api.static({\n  expose: true,\n  path: \"/public/*path\",\n  dir: \"./assets\",\n});\n```\n\nLearn more about serving static files in the [Static Files](/docs/ts/primitives/static-assets) guide.\n"
  },
  {
    "path": "docs/ts/how-to/file-uploads.md",
    "content": "---\nseotitle: How to handle file uploads in you Encore.ts application\nseodesc: Learn how to store file uploads as bytes in a database and serving them back to the client.\ntitle: Handling file uploads\nlang: ts\n---\n\nIn this guide you will learn how to handle file uploads from a client in your Encore.ts backend.\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/file-upload\" \n    desc=\"Handling file uploads and storing file data in a database\" \n/>\n\n## Storing a single file in a database\n\nBreakdown of the example:\n* We have a [PostgreSQL database](/docs/ts/primitives/databases) table named `files` with columns `name` and `data` to store the file name and the file data.\n* We have a [Raw Endpoint](/docs/ts/primitives/raw-endpoints) to handle file uploads. The endpoint has a `bodyLimit` set to `null` to allow for unlimited file size. \n* We make use of the [busboy](https://www.npmjs.com/package/busboy) library to help with the file handling.\n* We convert the file data to a `Buffer` and store the file as a `BYTEA` in the database.\n\n```ts\n-- upload.ts --\nimport { api } from \"encore.dev/api\";\nimport log from \"encore.dev/log\";\nimport busboy from \"busboy\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\n// Define a database named 'files', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nexport const DB = new SQLDatabase(\"files\", {\n  migrations: \"./migrations\",\n});\n\ntype FileEntry = { data: any[]; filename: string };\n\n/**\n * Raw endpoint for storing a single file to the database.\n * Setting bodyLimit to null allows for unlimited file size.\n */\nexport const save = api.raw(\n  { expose: true, method: \"POST\", path: \"/upload\", bodyLimit: null },\n  async (req, res) => {\n    const bb = busboy({\n      headers: req.headers,\n      limits: { files: 1 },\n    });\n    const entry: FileEntry = { filename: \"\", data: [] };\n\n    bb.on(\"file\", (_, file, info) => {\n      entry.filename = info.filename;\n      file\n        .on(\"data\", (data) => {\n          entry.data.push(data);\n        })\n        .on(\"close\", () => {\n          log.info(`File ${entry.filename} uploaded`);\n        })\n        .on(\"error\", (err) => {\n          bb.emit(\"error\", err);\n        });\n    });\n\n    bb.on(\"close\", async () => {\n      try {\n        const buf = Buffer.concat(entry.data);\n        await DB.exec`\n            INSERT INTO files (name, data)\n            VALUES (${entry.filename}, ${buf})\n            ON CONFLICT (name) DO UPDATE\n                SET data = ${buf}\n        `;\n        log.info(`File ${entry.filename} saved`);\n\n        // Redirect to the root page\n        res.writeHead(303, { Connection: \"close\", Location: \"/\" });\n        res.end();\n      } catch (err) {\n        bb.emit(\"error\", err);\n      }\n    });\n\n    bb.on(\"error\", async (err) => {\n      res.writeHead(500, { Connection: \"close\" });\n      res.end(`Error: ${(err as Error).message}`);\n    });\n\n    req.pipe(bb);\n    return;\n  },\n);\n-- migrations/1_create_tables.up.sql --\nCREATE TABLE files (\n    name TEXT PRIMARY KEY,\n    data BYTEA NOT NULL\n);\n```\n\n### Frontend\n\n```html\n<form method=\"POST\" enctype=\"multipart/form-data\" action=\"/upload\">\n    <label for=\"filefield\">Single file upload:</label><br>\n    <input type=\"file\" name=\"filefield\">\n    <input type=\"submit\">\n</form>\n```\n\n## Handling multiple file uploads\n\nWhen handling multiple file uploads, we can use the same approach as above, but we need to handle multiple files in the busboy event listeners. When storing the files in the database, we loop through the files and save them one by one.\n\n```ts\nexport const saveMultiple = api.raw(\n  { expose: true, method: \"POST\", path: \"/upload-multiple\", bodyLimit: null },\n  async (req, res) => {\n    const bb = busboy({ headers: req.headers });\n    const entries: FileEntry[] = [];\n\n    bb.on(\"file\", (_, file, info) => {\n      const entry: FileEntry = { filename: info.filename, data: [] };\n\n      file\n        .on(\"data\", (data) => {\n          entry.data.push(data);\n        })\n        .on(\"close\", () => {\n          entries.push(entry);\n        })\n        .on(\"error\", (err) => {\n          bb.emit(\"error\", err);\n        });\n    });\n\n    bb.on(\"close\", async () => {\n      try {\n        for (const entry of entries) {\n          const buf = Buffer.concat(entry.data);\n          await DB.exec`\n              INSERT INTO files (name, data)\n              VALUES (${entry.filename}, ${buf})\n              ON CONFLICT (name) DO UPDATE\n                  SET data = ${buf}\n          `;\n          log.info(`File ${entry.filename} saved`);\n        }\n\n        // Redirect to the root page\n        res.writeHead(303, { Connection: \"close\", Location: \"/\" });\n        res.end();\n      } catch (err) {\n        bb.emit(\"error\", err);\n      }\n    });\n\n    bb.on(\"error\", async (err) => {\n      res.writeHead(500, { Connection: \"close\" });\n      res.end(`Error: ${(err as Error).message}`);\n    });\n\n    req.pipe(bb);\n    return;\n  },\n);\n```\n\n### Frontend\n\n```html\n<form method=\"POST\" enctype=\"multipart/form-data\" action=\"/upload-multiple\">\n    <label for=\"filefield\">Multiple files upload:</label><br>\n    <input type=\"file\" name=\"filefield\" multiple>\n    <input type=\"submit\">\n</form>\n```\n\n## Handling large files\n\nIn order to not run into a **Maximum request length exceeded**-error when uploading large files you might need to adjust the endpoints `bodyLimit`. You can also set the `bodyLimit` to `null` to allow for unlimited file size uploads. If unset it defaults to 2MiB.\n\n## Retrieving files from the database\n\nWhen retrieving files from the database, we can use a GET endpoint to fetch the file data by its name. We can then serve the file back to the client by creating a `Buffer` from the file data and sending it in the response. \n\n```ts\nimport { api } from \"encore.dev/api\";\nimport { APICallMeta, currentRequest } from \"encore.dev\"; \n\nexport const DB = new SQLDatabase(\"files\", {\n  migrations: \"./migrations\",\n});\n\nexport const get = api.raw(\n  { expose: true, method: \"GET\", path: \"/files/:name\" },\n  async (req, resp) => {\n    try {\n      const { name } = (currentRequest() as APICallMeta).pathParams;\n      const row = await DB.queryRow`\n          SELECT data\n          FROM files\n          WHERE name = ${name}`;\n      if (!row) {\n        resp.writeHead(404);\n        resp.end(\"File not found\");\n        return;\n      }\n\n      const chunk = Buffer.from(row.data);\n      resp.writeHead(200, { Connection: \"close\" });\n      resp.end(chunk);\n    } catch (err) {\n      resp.writeHead(500);\n      resp.end((err as Error).message);\n    }\n  },\n);\n```\n\nYou should now be able to retrieve a file from the database by making a GET request to `http://localhost:4000/files/name-of-file.ext`. \n"
  },
  {
    "path": "docs/ts/how-to/nestjs.md",
    "content": "---\nseotitle: Use Encore together with NestJS\nseodesc: Learn how to use NestJS to structure your business logic and Encore for creating infrastructure resources.\ntitle: Use NestJS with Encore\nlang: ts\n---\n\n[Nest](https://docs.nestjs.com/) (NestJS) is a framework for building efficient, scalable TypeScript server-side\napplications. Nest aims to provide\nan application architecture out of the box which allows for effortless creation of highly testable, scalable, and\nloosely coupled and easily maintainable applications.\n\nEncore is not opinionated when it comes to application architecture, so you can use it together with NestJS to structure\nyour business logic and Encore for creating backend primitives like APIs, Databases, and Cron Jobs.\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/nestjs\" \n    desc=\"Encore.ts + NestJS example\" \n/>\n\n## Adding Encore to a NestJS project\n\nIf you already have a NestJS project, you can add Encore to it by following these steps:\n\n1. Run `encore app init` in the root of your project to create a new Encore application.\n2. Add `encore.dev` as a dependency by running `npm install encore.dev`.\n3. Add the following `paths` to your `tsconfig.json`:\n\n```json\n-- tsconfig.json --\n{\n   \"compilerOptions\": {\n      \"paths\": {\n         \"~encore/*\": [\n            \"./encore.gen/*\"\n         ]\n      }\n   }\n}\n```\n\n## Standalone Nest application\n\nIn order for Encore to be able to provision infrastructure resources, generate API documentation etc. we need to run our\napplication using Encore. This means that we need to replace the ordinary Nest bootstrapping and instead run our Nest\napp as\na [standalone application](https://docs.nestjs.com/standalone-applications). We do this by\ncalling `NestFactory.createApplicationContext(AppModule)` and then selecting the modules/services we need:\n\n```ts\n-- applicationContext.ts --\nconst applicationContext: Promise<{ catsService: CatsService }> =\n  NestFactory.createApplicationContext(AppModule).then((app) => {\n    return {\n      catsService: app.select(CatsModule).get(CatsService, {strict: true}),\n      // other services...\n    };\n  });\n\nexport default applicationContext;\n```\n\nThe `applicationContext` variable can then be used to access your Nest modules/services from your Encore your APIs.\n\n## Defining an Encore service\n\nWhen running an app using Encore you need at least\none [Encore service](/docs/ts/primitives/services#defining-a-service). You can define a\nservice\nin two ways:\n\n1. Create a folder and inside that folder defining one or more APIs. Encore recognizes this as a service, and uses the\n   folder name as the service name.\n2. Add a file named `encore.service.ts` in a directory. The file must export a service instance, by\n   calling `new Service`, imported from `encore.dev/service`:\n\n```ts\nimport {Service} from \"encore.dev/service\";\n\nexport default new Service(\"my-service\");\n```\n\nEncore will consider this directory and all its subdirectories as part of the service.\n\nIf you already have a Nest app then the easiest way to get going is to go with the second approach and add\na `encore.service.ts` in the root of your app, then you do not need to change your existing folder structure. \n\n## Replacing Nest controllers with Encore APIs\n\nIf you already have a Nest app then you can keep most of your business logic (modules, services and providers) as is but\nin order for Encore to be able to manage your APIs, you need to replace your Nest controllers with Encore APIs.\n\nLet's assume you have a `cats/cats.controller.ts` in your Nest app that looks like this:\n\n```ts\n-- cats/cats.controller.ts --\n\n@Controller('cats')\nexport class CatsController {\n  constructor(private readonly catsService: CatsService) {\n  }\n\n  @Post()\n  @Roles(['admin'])\n  async create(@Body() createCatDto: CreateCatDto) {\n    this.catsService.create(createCatDto);\n  }\n\n  @Get()\n  async findAll(): Promise<Cat[]> {\n    return this.catsService.findAll();\n  }\n\n  @Get(':id')\n  findOne(\n    @Param('id', new ParseIntPipe())\n      id: number,\n  ) {\n    return this.catsService.get(id);\n  }\n}\n```\n\nWhen converting this to using Encore it would look like this:\n\n```ts\n-- cats/cats.controller.ts --\nexport const findAll = api(\n  {expose: true, method: 'GET', path: '/cats'},\n  async (): Promise<{ cats: Cat[] }> => {\n    const {catsService} = await applicationContext;\n    return {cats: await catsService.findAll()};\n  },\n);\n\nexport const get = api(\n  {expose: true, method: 'GET', path: '/cats/:id'},\n  async ({id}: { id: number }): Promise<{ cat: Cat }> => {\n    const {catsService} = await applicationContext;\n    return {cat: await catsService.get(id)};\n  },\n);\n\nexport const create = api(\n  {expose: true, auth: true, method: 'POST', path: '/cats'},\n  async (dto: CreateCatDto): Promise<void> => {\n    const {catsService} = await applicationContext;\n    catsService.create(dto);\n  },\n);\n```\n\nWe use the `applicationContext` (that we defined above) to access our `catsService` and pass in the necessary\nparameters.\n\nBoth Encore and Nest use the concept of a `service`. With Encore you define a service by creating a folder and inside\nthat folder defining one or more APIs. Encore recognizes this as a service, and uses the folder name as the service\nname. When deploying, Encore will automatically provision the required infrastructure for each service. So in the\nexample\nabove we have a `cats` service with three APIs because `cats.controller.ts` is placed inside a folder named `cats`.\n\n## Making use of other Encore features\n\nEncore also allows you to easily make use of other backend primitives in your Nest app,\nlike [Databases](/docs/ts/primitives/databases), [Cron Jobs](/docs/ts/primitives/cron-jobs), [Pub/Sub & Queues](/docs/ts/primitives/pubsub)\nand [Secrets](/docs/ts/primitives/secrets).\n\nTake a look at our [Encore + NestJS example](https://github.com/encoredev/examples/tree/main/ts/nestjs) which uses both\na PostgreSQL Database and an [Auth Handler](/docs/ts/develop/auth) to authenticate incoming requests.\n\n## Running your Encore app\n\nAfter those steps we are ready to run our app locally:\n\n```shell\n$ encore run\n```\n\nYou should see log messages about both Encore and Nest staring up. That means your local development environment is up\nand\nrunning and ready to take some requests!\n\n### Open the Local Development Dashboard\n\nYou can now start using your [Local Development Dashboard](/docs/ts/observability/dev-dash).\n\nOpen [http://localhost:9400](http://localhost:9400) in your browser to access it.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/localdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nThe Local Development Dashboard is a powerful tool to help you move faster when you're developing new features.\n\nIt comes with an API explorer, a Service Catalog with automatically generated documentation, and powerful\nobservability features\nlike [distributed tracing](/docs/ts/observability/tracing).\n"
  },
  {
    "path": "docs/ts/install.md",
    "content": "---\nseotitle: Install Encore to start building\nseodesc: See how you can install Encore on all platforms, and get started building your next backend application in minutes.\ntitle: Installation\nsubtitle: Install the Encore CLI to get started with local development\nlang: ts\n---\n\nIf you are new to Encore, we recommend following the [quick start guide](/docs/ts/quick-start).\n\n## Install the Encore CLI\nTo develop locally with Encore, you first need to install the Encore CLI.\nThis is what provisions your local development environment, and runs your Local Development Dashboard complete with logs, tracing, and API documentation.\n\n\n<InstallInstructions />\n\n### Prerequisites\n\n- [Node.js](https://nodejs.org/en/download/) is required to run Encore.ts apps.\n- [Docker](https://www.docker.com) is required for Encore to set up local databases.\n\n### Optional: Add AI/LLM instructions\n\nTo help AI coding assistants (Cursor, Claude Code, GitHub Copilot, etc.) understand how to use Encore, run this from your app directory:\n\n```bash\nencore llm-rules init\n```\n\nThis prompts you to select your tool and generates the appropriate config (e.g. `.cursorrules`, `CLAUDE.md`) and MCP setup where supported. For full details and other options, see [AI Tools Integration](/docs/ts/ai-integration).\n\n### Build from source\nIf you prefer to build from source, [follow these instructions](https://github.com/encoredev/encore/blob/main/CONTRIBUTING.md).\n\n\n## Update to the latest version\nCheck which version of Encore you have installed by running `encore version` in your terminal.\nIt should print something like:\n```shell\nencore version v1.28.0\n```\n\nIf you think you're on an older version of Encore, you can easily update to the latest version by running\n`encore version update` from your terminal.\n"
  },
  {
    "path": "docs/ts/migration/ai-migration.mdx",
    "content": "---\nseotitle: Migrate to Encore.ts Using an AI Agent\nseodesc: Learn how to use Encore's AI migration skill to automatically migrate your existing backend to Encore.ts, with validation at every step.\ntitle: Migrate using AI agent\nlang: ts\n---\n\nEncore's AI migration skill analyzes your existing backend, builds a dependency-aware migration plan, and converts your code to Encore.ts — one unit at a time, with validation at every step.\n\nIt works with any source framework: Express, Fastify, Hono, Koa, NestJS, and more.\n\n<Callout type=\"info\">\n\nThe skill has been tested with Claude Code but should work with other agents as well.\n\n</Callout>\n\n## Prerequisites\n\nInstall the Encore skills package in your AI coding tool:\n\n```bash\nnpx add-skill encoredev/skills\n```\n\nYou'll also need:\n- The source codebase accessible on your local machine\n- An Encore project to migrate into (the skill can help create one)\n- Your source application running locally (optional — enables HTTP comparison validation)\n\n## Starting a migration\n\nCreate a new Encore app from the \"Empty app\" template by running:\n\n```bash\nencore app create\n```\n\nFrom inside your Encore app, open your AI coding tool and ask it to migrate your existing app:\n\n```\nMigrate ../path/to/existing/project to Encore.ts by using the encore-migrate skill\n```\n\nThe skill walks you through four phases: **Discover**, **Plan**, **Migrate**, and **Complete**.\n\n## How it works\n\n### Phase 1 — Discover\n\nThe AI reads your source codebase and inventories everything: API endpoints, databases, Pub/Sub topics, cron jobs, auth middleware, secrets, and tests. It groups related entities into **migration units** — typically aligned with your existing service boundaries or URL path prefixes — and presents a summary for you to review.\n\nYou can adjust the groupings before moving on. Split units that are too large, merge ones that are too small, or rename them to match your domain.\n\n### Phase 2 — Plan\n\nThe AI creates a `migration-plan.md` file and a `migration-plan/` directory in your Encore project. The summary file tracks overall progress and dependency order. Each migration unit gets its own detail file listing every endpoint, database table, and test to migrate.\n\nDependencies determine the order. Secrets and config go first, then databases, auth, leaf services, dependent services, Pub/Sub, and finally cron jobs.\n\n### Phase 3 — Migrate\n\nThe AI works through one migration unit at a time. For each entity it:\n\n1. **Implements** the Encore equivalent — [API endpoints](/docs/ts/primitives/defining-apis), [database schemas](/docs/ts/primitives/databases), [infrastructure declarations](/docs/ts/primitives/services)\n2. **Migrates tests** from the source framework to Encore's [testing patterns](/docs/ts/develop/testing)\n3. **Validates** the result using up to three layers (see [Validation](#validation))\n4. **Updates the plan** files to track progress\n\nAfter completing a unit, it suggests the next one based on the dependency order. You can also pick a different unit or tell it to keep going through multiple units.\n\n### Phase 4 — Complete\n\nWhen all units are done, the AI presents a final summary: what was migrated, what was skipped, and what needs manual attention. It suggests a final test suite run and, if your source system has a frontend, recommends reconnecting it to the new Encore backend using the [Request Client](/docs/ts/frontend/request-client).\n\n## Full-stack and monorepo support\n\nWhen the source codebase contains frontend code (React, Vue, Angular, Next.js, etc.), the AI identifies it and marks it as out of scope — only backend code is migrated.\n\nFor full-stack frameworks like **Next.js**, **Remix**, **Nuxt**, **SvelteKit**, and **Astro**, the AI detects server-side routes (e.g., Next.js `pages/api/` or Remix `loader` functions) and asks what you want to do with them:\n\n1. **Migrate all** server-side routes to Encore\n2. **Migrate some** — you pick which ones move\n3. **Keep all in the frontend framework** — only migrate standalone backend code\n\nThis is useful when you want an Encore backend but prefer to keep a thin BFF or SSR data-fetching layer in your frontend framework.\n\n## Validation\n\nEvery entity is validated before it's marked as migrated. The AI uses three layers:\n\n**Test migration** — Source tests are converted to Encore's [testing patterns](/docs/ts/develop/testing) and run. They must pass before the entity is marked as done.\n\n**HTTP comparison** — When both systems are running locally, the AI calls the same endpoint on both and compares the HTTP status code and response body structure. This layer is skipped for endpoints with side effects or that require auth credentials the AI can't obtain.\n\n**Verification gate** — No entity is marked as `migrated` without concrete evidence from the current session: test output, HTTP comparison results, or your explicit approval to skip.\n\n## Resuming across sessions\n\nThe migration plan is persisted to files in your Encore project, so you can close your editor and come back later. When you resume, the AI reads `migration-plan.md`, reports the current status, and suggests the next unit to work on.\n\n```\nResume the migration\n```\n\n```\nWhat's left to migrate?\n```\n"
  },
  {
    "path": "docs/ts/migration/express-migration.md",
    "content": "---\nseotitle: Migrate from Express to Encore.ts\nseodesc: Learn how migrate your Express.js app over to use Encore.ts for better performance and improved development tools.\ntitle: Migrating from Express.js\nlang: ts\n---\n\nIf you have an existing app using [Express.js](https://expressjs.com/) and want to migrate it to Encore.ts, this guide is\nfor you. This guide can also serve as a comparison between the two frameworks.\n\n<iframe width=\"560\" height=\"315\" class=\"aspect-video\" src=\"https://www.youtube.com/embed/hA9syK_FtZw?si=EScQ-x3qOLdImrMb\" title=\"Express.js vs Encore.ts\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen></iframe>\n\n## Why migrate to Encore.ts?\n\nExpress.js is a great choice for building simple APIs, but as your application grows you will likely run into limitations. There is a large community around Express.js, providing many plugins and middleware to work around these limitations. However relying heavily on plugins can make it hard to find the right tools for your use case. It also means that you will need to maintain a lot of dependencies.\n\nEncore.ts is a framework that aims to make it easier to build robust and type-safe backends with\nTypeScript. Encore.ts has 0 npm dependencies, is built with performance in mind, and has a lot of built-in features for building production ready backends. You can deploy an Encore.ts app to any hosting service that accepts Docker containers, or use [Encore Cloud](/use-cases/devops-automation) to fully automate your DevOps and infrastructure.\n\n### Performance\n\nUnlike a lot of other Node.js frameworks, Encore.ts is not built on top of Express.js. Instead, Encore.ts has its own\nhigh-performance runtime, with a multi-threaded, asynchronous event loop written in Rust. The Encore Runtime handles all I/O like accepting and processing incoming HTTP requests. This runs as a completely independent event loop that utilizes as many threads as the underlying hardware supports. The result of this is that Encore.ts performs\n**9x faster** than Express.js. Learn more about the [Encore.ts Runtime](/blog/event-loops).\n\n### Built-in benefits\n\nWhen using Encore.ts you get a lot of built-in features without having to install any additional dependencies:\n\n| Built-in benefits                                      |                       <!-- -->                       |                                                       <!-- --> |\n| :----------------------------------------------------- | :--------------------------------------------------: | -------------------------------------------------------------: |\n| [Pub/Sub integrations](/docs/ts/primitives/pubsub)     |  [Type-safe API schemas](/docs/ts/primitives/apis)   |        [API Client generation](/docs/ts/cli/client-generation) |\n| [Secrets management](/docs/ts/primitives/secrets)      |        [CORS handling](/docs/ts/develop/cors)        | [Local Development Dashboard](/docs/ts/observability/dev-dash) |\n| [Database integrations](/docs/ts/primitives/databases) | [Architecture Diagrams](/docs/ts/observability/flow) |      [Service Catalog](/docs/ts/observability/service-catalog) |\n| [Request validation](/blog/event-loops)                |      [Cron Jobs](/docs/ts/primitives/cron-jobs)      |                [Local tracing](/docs/ts/observability/tracing) |\n\n## Migration guide\n\nBelow we've outlined two main strategies you can use to migrate your existing Express.js application to Encore.ts. Pick the strategy that best suits your situation and application.\n\n<GitHubLink\nhref=\"https://github.com/encoredev/examples/tree/main/ts/expressjs-migration\"\ndesc=\"Code examples for migrating an Express.js app to Encore.ts\"\n/>\n\n<Accordion>\n\n### Forklift migration (quick start)\n\nWhen you quickly want to migrate to Encore.ts and don't need all the functionality to begin with, you can use a forklift migration strategy. This approach moves the entire application over to Encore.ts in one shot, by wrapping your existing HTTP router in a catch-all handler.\n\n**Approach benefits**\n\n- You can get your application up and running with Encore.ts quickly and start moving features over to Encore.ts while the rest of the application is still untouched.\n- You will see a partial performance boost right away because the HTTP layer is now running on the Encore Rust runtime. But to get the full performance benefits, you will need to start using Encore's [API declarations](/docs/ts/primitives/defining-apis) and [infrastructure declarations](/docs/ts#explore-how-to-use-each-backend-primitive).\n\n**Approach drawbacks**\n\n- Because all requests will be proxied through the catch-all handler, you will not be able to get all the benefits from the [distributed tracing](/docs/ts/observability/tracing), which rely on the [Encore application model](/docs/ts/concepts/application-model).\n- [Encore Flow](/docs/ts/observability/flow) and the [Service Catalog](/docs/ts/observability/service-catalog) will not be able to show you the full picture of your application until you start moving services and APIs over to Encore.ts.\n- You will not be able to use the [API Client generation](/docs/ts/cli/client-generation) feature until you start defining APIs in Encore.ts.\n\n#### 1. Install Encore\n\nIf this is the first time you're using Encore, you first need to install the CLI that runs the local development\nenvironment. Use the appropriate command for your system:\n\n- **macOS:** `brew install encoredev/tap/encore`\n- **Linux:** `curl -L https://encore.dev/install.sh | bash`\n- **Windows:** `iwr https://encore.dev/install.ps1 | iex`\n\n[Installation docs](https://encore.dev/docs/install)\n\n#### 2. Add Encore.ts to your project\n\n```bash\nnpm i encore.dev\n```\n\n#### 3. Initialize an Encore app\n\nInside your project directory, run the following command to create an Encore app:\n\n```bash\nencore app init\n```\n\nThis will create an `encore.app` file in the root of your project.\n\n#### 4. Configure your tsconfig.json\n\nTo the `tsconfig.json` file in the root of your project, add the following:\n\n```json\n-- tsconfig.json --\n{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"~encore/*\": [\n        \"./encore.gen/*\"\n      ]\n    }\n  }\n}\n```\n\nWhen Encore.ts is parsing your code it will specifically look for `~encore/*` imports.\n\n#### 5. Define an Encore.ts service\n\nWhen running an app using Encore.ts you need at least one [Encore service](/docs/ts/primitives/services). Apart from that, Encore.ts in not opinionated in how you structure your code, you are free to go with a monolith or microservice approach. Learn more in our [App Structure docs](/docs/ts/primitives/app-structure).\n\nIn the root of your App, add a file named `encore.service.ts`. The file must export a service instance, by calling\n`new Service`, imported from `encore.dev/service`:\n\n```ts\nimport {Service} from \"encore.dev/service\";\n\nexport default new Service(\"my-service\");\n```\n\nEncore will consider this directory and all its subdirectories as part of the service.\n\n#### 6. Create a catch-all handler for your HTTP router\n\nNow let's mount your existing app router under a [Raw endpoint](/docs/ts/primitives/raw-endpoints), which is an Encore API endpoint type that gives you access to the underlying HTTP request.\n\nHere's a basic code example:\n\n```typescript\nimport { api, RawRequest, RawResponse } from \"encore.dev/api\";\nimport express, { request, response } from \"express\";\n\nObject.setPrototypeOf(request, RawRequest.prototype);\nObject.setPrototypeOf(response, RawResponse.prototype);\n\nconst app = express();\n\napp.get('/foo', (req: any, res) => {\n  res.send('Hello World!')\n})\n\nexport const expressApp = api.raw(\n  { expose: true, method: \"*\", path: \"/!rest\" },\n  app,\n);\n```\n\nBy mounting your existing app router in this way, it will work as a catch-all handler for all HTTP requests and responses.\n\n#### 7. Run you app locally\n\nYou will now be able to run your Express.js app locally using the `encore run` command.\n\n#### Next steps: Incrementally move over Encore.ts to get all the benefits\n\nYou can now gradually break out specific endpoints using the Encore's [API declarations](#apis) and introduce infrastructure declarations for databases and cron jobs etc. This will let Encore.ts understand your application and unlock all Encore.ts features. See the [Feature-by-feature migration](#feature-by-feature-migration) section for more details. You will eventually be able to remove Express.js as a dependency and run your app entirely on Encore.ts.\n\nYou can also [join Discord](https://encore.dev/discord) to ask questions and meet fellow Encore developers.\n\n#### Forklift example\n\n<div className=\"not-prose my-10\">\n   <Editor projectName=\"expressForklift\" />\n</div>\n\n\n</Accordion>\n\n\n<Accordion>\n\n### Full migration\n\nThis approach aims to fully replace your applications dependency on Express.js with Encore.ts, unlocking all the features and performance of Encore.ts.\n\nBelow are two examples that you can use to identify the refactoring you will need to do. In the next section you will find a [Feature-by-feature migration](#feature-by-feature-migration) guide to help you with the refactoring details.\n\n**Approach benefits**\n\n- Get all the advantages of Encore.ts, like [distributed tracing](/docs/ts/observability/tracing) and [architecture diagrams](/docs/ts/observability/flow), which rely on the [Encore application model](/docs/ts/concepts/application-model).\n- Get the [full performance benefit](https://encore.dev/blog/event-loops) of Encore.ts - **9x faster** than Express.js.\n\n**Approach drawbacks**\n\n- This approach may require more time and effort up front compared to the [Incremental migration strategy](#incremental-migration-strategy).\n\n#### App comparison\n\nHere is a side-by-side comparison of an Express.js app and an Encore.ts app. The examples show how to create APIs, handle request validation, error handling, serving static files, and rendering templates.\n\n**Express.js**\n\n<div className=\"not-prose my-10\">\n   <Editor projectName=\"expressVsEncore\" />\n</div>\n\n**Encore.ts**\n\n<div className=\"not-prose my-10\">\n   <Editor projectName=\"encoreVsExpress\" />\n</div>\n\n</Accordion>\n\n## Feature-by-feature migration\n\nCheck out our [Express.js compared to Encore.ts example](https://github.com/encoredev/examples/tree/main/ts/expressjs-migration) on GitHub for all of the code snippets in this feature comparison.\n\n<Accordion>\n\n### APIs\n\nWith Express.js, you create APIs using the `app.get`, `app.post`, `app.put`, `app.delete` functions. These functions\ntake a path and a callback function. You then use the `req` and `res` objects to handle the request and response.\n\nWith Encore.ts, you create APIs using the `api` function. This function takes an options object and a callback function.\nThe main difference compared to Express.js is that Encore.ts is type-safe, meaning you define the request and response\nschemas in the callback function. You then return an object matching the response schema. In case you need to operate at\na lower abstraction level, Encore supports defining raw endpoints that let you access the underlying HTTP request.\nLearn more in the [API Schemas docs](/docs/ts/primitives/defining-apis#api-schemas).\n\n**Express.js**\n\n```typescript\nimport express, {Request, Response} from \"express\";\n\nconst app: Express = express();\n\n// GET request with dynamic path parameter\napp.get(\"/hello/:name\", (req: Request, res: Response) => {\n  const msg = `Hello ${req.params.name}!`;\n  res.json({message: msg});\n})\n\n// GET request with query string parameter\napp.get(\"/hello\", (req: Request, res: Response) => {\n  const msg = `Hello ${req.query.name}!`;\n  res.json({message: msg});\n});\n\n// POST request example with JSON body\napp.post(\"/order\", (req: Request, res: Response) => {\n  const price = req.body.price;\n  const orderId = req.body.orderId;\n  // Handle order logic\n  res.json({message: \"Order has been placed\"});\n});\n```\n\n**Encore.ts**\n\n```typescript\nimport {api, Query} from \"encore.dev/api\";\n\n// Dynamic path parameter :name\nexport const dynamicPathParamExample = api(\n  {expose: true, method: \"GET\", path: \"/hello/:name\"},\n  async ({name}: { name: string }): Promise<{ message: string }> => {\n    const msg = `Hello ${name}!`;\n    return {message: msg};\n  },\n);\n\ninterface RequestParams {\n  // Encore will now automatically parse the query string parameter\n  name?: Query<string>;\n}\n\n// Query string parameter ?name\nexport const queryStringExample = api(\n  {expose: true, method: \"GET\", path: \"/hello\"},\n  async ({name}: RequestParams): Promise<{ message: string }> => {\n    const msg = `Hello ${name}!`;\n    return {message: msg};\n  },\n);\n\ninterface OrderRequest {\n  price: string;\n  orderId: number;\n}\n\n// POST request example with JSON body\nexport const order = api(\n  {expose: true, method: \"POST\", path: \"/order\"},\n  async ({price, orderId}: OrderRequest): Promise<{ message: string }> => {\n    // Handle order logic\n    console.log(price, orderId)\n\n    return {message: \"Order has been placed\"};\n  },\n);\n\n// Raw endpoint\nexport const myRawEndpoint = api.raw(\n  {expose: true, path: \"/raw\", method: \"GET\"},\n  async (req, resp) => {\n    resp.writeHead(200, {\"Content-Type\": \"text/plain\"});\n    resp.end(\"Hello, raw world!\");\n  },\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Microservices communication\n\nExpress.js does not have built-in support for creating microservices or for service-to-service communication. You will most likely use\n`fetch` or something equivalent to call another service.\n\nWith Encore.ts, calling another service is just like calling a local function, with complete type-safety. Under the hood, Encore.ts will translate this function call into an actual service-to-service HTTP call, resulting in trace data being generated for each call.\nLearn more in our [Service-to-Service Communication docs](/docs/ts/primitives/app-structure#multi-service-application-distributed-system).\n\n**Express.js**\n\n```typescript\nimport express, {Request, Response} from \"express\";\n\nconst app: Express = express();\n\napp.get(\"/save-post\", async (req: Request, res: Response) => {\n  try {\n    // Calling another service using fetch\n    const resp = await fetch(\"https://another-service/posts\", {\n      method: \"POST\",\n      headers: {\"Content-Type\": \"application/json\"},\n      body: JSON.stringify({\n        title: req.query.title,\n        content: req.query.content,\n      }),\n    });\n    res.json(await resp.json());\n  } catch (e) {\n    res.status(500).json({error: \"Could not save post\"});\n  }\n});\n```\n\n**Encore.ts**\n\n```typescript\nimport {api} from \"encore.dev/api\";\nimport {anotherService} from \"~encore/clients\";\n\nexport const microserviceCommunication = api(\n  {expose: true, method: \"GET\", path: \"/call\"},\n  async (): Promise<{ message: string }> => {\n    // Calling the foo endpoint in anotherService\n    const fooResponse = await anotherService.foo();\n\n    const msg = `Data from another service ${fooResponse.data}!`;\n    return {message: msg};\n  },\n);\n\n```\n\n</Accordion>\n\n\n<Accordion>\n\n### Authentication\n\nIn Express.js you can create a middleware function that checks if the user is authenticated. You can then use this\nmiddleware function in your routes to protect them. You will have to specify the middleware function for each route that\nrequires authentication.\n\nWith Encore.ts, when an API is defined with `auth: true`, you must define an authentication handler in your application.\nThe authentication handler is responsible for inspecting incoming requests to determine what user is authenticated.\n\nThe authentication handler is defined similarly to API endpoints, using the `authHandler` function imported from\n`encore.dev/auth`. Like API endpoints, the authentication handler defines what request information it's interested in,\nin the form of HTTP headers, query strings, or cookies.\n\nIf a request has been successfully authenticated, the API Gateway forwards the authentication data to the target\nendpoint. The endpoint can query the available auth data from the `getAuthData` function, available from the\n`~encore/auth`\nmodule.\n\nLearn more in our [Auth Handler docs](/docs/ts/develop/auth)\n\n**Express.js**\n\n```typescript\nimport express, {NextFunction, Request, Response} from \"express\";\n\nconst app: Express = express();\n\n// Auth middleware\nfunction authMiddleware(req: Request, res: Response, next: NextFunction) {\n  // TODO: Validate up auth token and verify that this is an authenticated user\n  const isInvalidUser = req.headers[\"authorization\"] === undefined;\n\n  if (isInvalidUser) {\n    res.status(401).json({error: \"invalid request\"});\n  } else {\n    next();\n  }\n}\n\n// Endpoint that requires auth\napp.get(\"/dashboard\", authMiddleware, (_, res: Response) => {\n  res.json({message: \"Secret dashboard message\"});\n});\n```\n\n**Encore.ts**\n\n```typescript\nimport { api, APIError, Gateway, Header } from \"encore.dev/api\";\nimport { authHandler } from \"encore.dev/auth\";\nimport { getAuthData } from \"~encore/auth\";\n\ninterface AuthParams {\n  authorization: Header<\"Authorization\">;\n}\n\n// The function passed to authHandler will be called for all incoming API call that requires authentication.\nexport const myAuthHandler = authHandler(\n  async (params: AuthParams): Promise<{ userID: string }> => {\n    // TODO: Validate up auth token and verify that this is an authenticated user\n    const isInvalidUser = params.authorization === undefined;\n\n    if (isInvalidUser) {\n      throw APIError.unauthenticated(\"Invalid user ID\");\n    }\n\n    return { userID: \"user123\" };\n  },\n);\n\nexport const gateway = new Gateway({ authHandler: myAuthHandler });\n\n// Auth endpoint example\nexport const dashboardEndpoint = api(\n  // Setting auth to true will require the user to be authenticated\n  { auth: true, method: \"GET\", path: \"/dashboard\" },\n  async (): Promise<{ message: string; userID: string }> => {\n    return {\n      message: \"Secret dashboard message\",\n      userID: getAuthData()!.userID,\n    };\n  },\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Request validation\n\nExpress.js does not have built-in request validation. You have to use a library\nlike [Zod](https://github.com/colinhacks/zod).\n\nWith Encore.ts, request validation for headers, query params and body is. You supply a schema for the request object and\nin the request payload does not match the schema the API will return a 400 error.\nLearn more in the [API Schemas docs](/docs/ts/primitives/defining-apis#api-schemas).\n\n**Express.js**\n\n```typescript\nimport express, {NextFunction, Request, Response} from \"express\";\nimport {z, ZodError} from \"zod\";\n\nconst app: Express = express();\n\n// Request validation middleware\nfunction validateData(schemas: {\n  body: z.ZodObject<any, any>;\n  query: z.ZodObject<any, any>;\n  headers: z.ZodObject<any, any>;\n}) {\n  return (req: Request, res: Response, next: NextFunction) => {\n    try {\n      // Validate headers\n      schemas.headers.parse(req.headers);\n\n      // Validate request body\n      schemas.body.parse(req.body);\n\n      // Validate query params\n      schemas.query.parse(req.query);\n\n      next();\n    } catch (error) {\n      if (error instanceof ZodError) {\n        const errorMessages = error.errors.map((issue: any) => ({\n          message: `${issue.path.join(\".\")} is ${issue.message}`,\n        }));\n        res.status(400).json({error: \"Invalid data\", details: errorMessages});\n      } else {\n        res.status(500).json({error: \"Internal Server Error\"});\n      }\n    }\n  };\n}\n\n// Request body validation schemas\nconst bodySchema = z.object({\n  someKey: z.string().optional(),\n  someOtherKey: z.number().optional(),\n  requiredKey: z.array(z.number()),\n  nullableKey: z.number().nullable().optional(),\n  multipleTypesKey: z.union([z.boolean(), z.number()]).optional(),\n  enumKey: z.enum([\"John\", \"Foo\"]).optional(),\n});\n\n// Query string validation schemas\nconst queryStringSchema = z.object({\n  name: z.string().optional(),\n});\n\n// Headers validation schemas\nconst headersSchema = z.object({\n  \"x-foo\": z.string().optional(),\n});\n\n// Request validation example using Zod\napp.post(\n  \"/validate\",\n  validateData({\n    headers: headersSchema,\n    body: bodySchema,\n    query: queryStringSchema,\n  }),\n  (_: Request, res: Response) => {\n    res.json({message: \"Validation succeeded\"});\n  },\n);\n```\n\n**Encore.ts**\n\n```typescript\nimport {api, Header, Query} from \"encore.dev/api\";\n\nenum EnumType {\n  FOO = \"foo\",\n  BAR = \"bar\",\n}\n\n// Encore.ts automatically validates the request schema and returns and error\n// if the request does not match the schema.\ninterface RequestSchema {\n  foo: Header<\"x-foo\">;\n  name?: Query<string>;\n\n  someKey?: string;\n  someOtherKey?: number;\n  requiredKey: number[];\n  nullableKey?: number | null;\n  multipleTypesKey?: boolean | number;\n  enumKey?: EnumType;\n}\n\n// Validate a request\nexport const schema = api(\n  {expose: true, method: \"POST\", path: \"/validate\"},\n  (data: RequestSchema): { message: string } => {\n    console.log(data);\n    return {message: \"Validation succeeded\"};\n  },\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Error handling\n\nIn Express.js you either throw an error (which results in a 500 response) or use the `status` function to set the status\ncode of the response.\n\nIn Encore.ts throwing an error will result in a 500 response. You can also use the `APIError` class to return specific\nerror codes. Learn more in our [API Errors docs](/docs/ts/primitives/errors).\n\n**Express.js**\n\n```typescript\nimport express, {Request, Response} from \"express\";\n\nconst app: Express = express();\n\n// Default error handler\napp.get(\"/broken\", (req, res) => {\n  throw new Error(\"BROKEN\"); // This will result in a 500 error\n});\n\n// Returning specific error code\napp.get(\"/get-user\", (req: Request, res: Response) => {\n  const id = req.query.id || \"\";\n  if (id.length !== 3) {\n    res.status(400).json({error: \"invalid id format\"});\n  }\n  // TODO: Fetch something from the DB\n  res.json({user: \"Simon\"});\n});\n```\n\n**Encore.ts**\n\n```typescript\nimport {api, APIError} from \"encore.dev/api\"; // Default error handler\n\n// Default error handler\nexport const broken = api(\n  {expose: true, method: \"GET\", path: \"/broken\"},\n  async (): Promise<void> => {\n    throw new Error(\"This is a broken endpoint\"); // This will result in a 500 error\n  },\n);\n\n// Returning specific error code\nexport const brokenWithErrorCode = api(\n  {expose: true, method: \"GET\", path: \"/broken/:id\"},\n  async ({id}: { id: string }): Promise<{ user: string }> => {\n    if (id.length !== 3) {\n      throw APIError.invalidArgument(\"invalid id format\");\n    }\n    // TODO: Fetch something from the DB\n    return {user: \"Simon\"};\n  },\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Serving static files\n\nExpress.js has a built-in middleware function to serve static files. You can use the `express.static` function to serve\nfiles from a specific directory.\n\nEncore.ts also has built-in support for static file serving with the\n`api.static` method. The files are served directly from the Encore.ts Rust Runtime. This means that zero JavaScript code is executed to serve the files, freeing up the Node.js runtime to focus on executing business logic. This dramatically speeds up both the static file serving, as well as improving the latency of your API endpoints. Learn more in our [Static Assets docs](/docs/ts/primitives/static-assets).\n\n**Express.js**\n\n```typescript\nimport express from \"express\";\n\nconst app: Express = express();\n\napp.use(\"/assets\", express.static(\"assets\"));\n```\n\n**Encore.ts**\n\n```typescript\nimport { api } from \"encore.dev/api\";\n\nexport const assets = api.static(\n  { expose: true, path: \"/assets/*path\", dir: \"./assets\" },\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Template rendering\n\nExpress.js has a built-in support for rendering templates.\n\nWith Encore.ts you can use the `api.raw` function to serve HTML templates, in this example we are using Handlebars.js\nbut you can use whichever templating engine you prefer. Learn more in\nour [Raw Endpoints docs](/docs/ts/primitives/raw-endpoints)\n\n**Express.js**\n\n```typescript\nimport express, {Request, Response} from \"express\";\n\nconst app: Express = express();\n\napp.set(\"view engine\", \"pug\"); // Set view engine to Pug\n\n// Template engine example. This will render the index.pug file in the views directory\napp.get(\"/html\", (_, res) => {\n  res.render(\"index\", {title: \"Hey\", message: \"Hello there!\"});\n});\n```\n\n**Encore.ts**\n\n```typescript\nimport {api} from \"encore.dev/api\";\nimport Handlebars from \"handlebars\";\n\nconst html = `\n<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\"/>\n  <link rel=\"stylesheet\" href=\"/assets/styles.css\">\n</head>\n<body>\n<h1>Hello {{name}}!</h1>\n</body>\n</html>\n`;\n\n// Making use of raw endpoints to serve dynamic templates.\n// https://encore.dev/docs/ts/primitives/raw-endpoints\nexport const serveHTML = api.raw(\n  {expose: true, path: \"/html\", method: \"GET\"},\n  async (req, resp) => {\n    const template = Handlebars.compile(html);\n\n    resp.setHeader(\"Content-Type\", \"text/html\");\n    resp.end(template({name: \"Simon\"}));\n  },\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Testing\n\nExpress.js does not have built-in testing support. You can use libraries like [Vitest](https://vitest.dev/) and\n[Supertest](https://www.npmjs.com/package/supertest).\n\nWith Encore.ts you are able to call the API endpoints directly in your tests, just like any other function. You then run\nthe tests using the `encore test` command. Learn more in our [Testing docs](/docs/ts/develop/testing).\n\n**Express.js**\n\n```typescript\nimport {describe, expect, test} from \"vitest\";\nimport request from \"supertest\";\nimport express from \"express\";\nimport getRequestExample from \"../get-request-example\";\n\n/**\n * We need to add the supertest library to make fake HTTP requests to the Express.js app without having to\n * start the server. We also use the vitest library to write tests.\n */\ndescribe(\"Express App\", () => {\n  const app = express();\n  app.use(\"/\", getRequestExample);\n\n  test(\"should respond with a greeting message\", async () => {\n    const response = await request(app).get(\"/hello/John\");\n    expect(response.status).to.equal(200);\n    expect(response.body).to.have.property(\"message\");\n    expect(response.body.message).to.equal(\"Hello John!\");\n  });\n});\n```\n\n**Encore.ts**\n\n```typescript\nimport {describe, expect, test} from \"vitest\";\nimport {dynamicPathParamExample} from \"../get-request-example\";\n\n// This test suite demonstrates how to test an Encore route.\n// Run tests using the `encore test` command.\ndescribe(\"Encore app\", () => {\n  test(\"should respond with a greeting message\", async () => {\n    // You can call the Encore.ts endpoint directly in your tests,\n    // just like any other function.\n    const resp = await dynamicPathParamExample({name: \"world\"});\n    expect(resp.message).toBe(\"Hello world!\");\n  });\n});\n\n```\n\n</Accordion>\n\n<Accordion>\n\n### Database\n\nExpress.js does not have built-in database support. You can use libraries like [pg-promise](https://www.npmjs.com/package/pg-promise) to connect to a\nPostgreSQL database but you also have to manage Docker Compose files for different environments.\n\nWith Encore.ts, you create a database by importing `encore.dev/storage/sqldb` and calling\n`new SQLDatabase`, assigning the result to a top-level variable.\n\nDatabase schemas are defined by creating migration files. Each migration runs in order and expresses the change in the database schema from the previous migration.\n\nEncore.ts automatically provisions databases to match what your application requires. Encore.ts provisions databases in an appropriate way depending on the environment. When running locally, Encore creates a database cluster using Docker. In the cloud, it depends on the environment type:\n\nTo query data, use the `.query` or\n`.queryRow` methods. To insert data, or to make database queryies that don't return any rows, use `.exec`.\n\nLearn more in our [Database docs](/docs/ts/primitives/databases).\n\n**Express.js**\n\n```typescript\n-- db.ts --\nimport express, {Request, Response} from \"express\";\nimport pgPromise from \"pg-promise\";\n\nconst app: Express = express();\n\n// Connect to the DB with the credentials from docker-compose.yml\nconst db = pgPromise()({\n  host: \"localhost\",\n  port: 5432,\n  database: \"database\",\n  user: \"user1\",\n  password: \"user1@123\",\n});\n\ninterface User {\n  name: string;\n  id: number;\n}\n\n// Get one User from DB\napp.get(\"/user/:id\", async (req: Request, res: Response) => {\n  const user = await db.oneOrNone<User>(\n    `\n        SELECT *\n        FROM users\n        WHERE id = $1\n    `,\n    req.params.id,\n  );\n\n  res.json({user});\n});\n-- docker-compose.yml --\nversion: '3.8'\n\nservices:\n  db:\n    build:\n      context: .\n      dockerfile: Dockerfile.postgis  # Use custom Dockerfile\n    restart: always\n    environment:\n      POSTGRES_USER: user1\n      POSTGRES_PASSWORD: user1@123\n      POSTGRES_DB: database\n    healthcheck:\n      # This command checks if the database is ready, right on the source db server\n      test: [ \"CMD-SHELL\", \"pg_isready\" ]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - postgres_data_v:/var/lib/postgresql/data\nvolumes:\n  postgres_data_v:\n-- Dockerfile.postgis --\nFROM postgres:latest\n\n# Install PostGIS extension\nRUN apt-get update \\\n    && apt-get install -y postgis postgresql-12-postgis-3 \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\n# To execute some initial queries, we can write queries in init.sql\nCOPY init.sql /docker-entrypoint-initdb.d/\n\n# Enable PostGIS extension\nRUN echo \"CREATE EXTENSION IF NOT EXISTS postgis;\" >> /docker-entrypoint-initdb.d/init.sqld\n```\n\n**Encore.ts**\n\n```typescript\n-- db.ts --\nimport {api} from \"encore.dev/api\";\nimport {SQLDatabase} from \"encore.dev/storage/sqldb\";\n\n// Define a database named 'users', using the database migrations in the \"./migrations\" folder.\n// Encore automatically provisions, migrates, and connects to the database.\nexport const DB = new SQLDatabase(\"users\", {\n  migrations: \"./migrations\",\n});\n\ninterface User {\n  name: string;\n  id: number;\n}\n\n// Get one User from DB\nexport const getUser = api(\n  {expose: true, method: \"GET\", path: \"/user/:id\"},\n  async ({id}: { id: number }): Promise<{ user: User | null }> => {\n    const user = await DB.queryRow<User>`\n        SELECT name\n        FROM users\n        WHERE id = ${id}\n    `;\n\n    return {user};\n  },\n);\n\n// Add User from DB\nexport const addUser = api(\n  { expose: true, method: \"POST\", path: \"/user\" },\n  async ({ name }: { name: string }): Promise<void> => {\n    await DB.exec`\n        INSERT INTO users (name)\n        VALUES (${name})\n    `;\n    return;\n  },\n);\n-- migrations/1_create_tables.up.sql --\nCREATE TABLE users (\n    id SERIAL PRIMARY KEY,\n    name TEXT NOT NULL UNIQUE\n);\n```\n\n</Accordion>\n\n<Accordion>\n\n### Logging\n\nExpress.js does not have built-in support for logging. You can use libraries like [Winston](https://www.npmjs.com/package/winston) to log messages.\n\nEncore.ts offers built-in support for Structured Logging, which combines a free-form log message with structured and type-safe key-value pairs. Logging is integrated with the built-in [Distributed Tracing](/docs/ts/observability/tracing) functionality, and all logs are automatically included in the active trace.\n\n**Encore.ts**\n\n```typescript\nimport log from \"encore.dev/log\";\n\nlog.error(err, \"something went terribly wrong!\");\nlog.info(\"log message\", {is_subscriber: true});\n```\n\n</Accordion>\n\n"
  },
  {
    "path": "docs/ts/migration/migrate-away.md",
    "content": "---\ntitle: Migrate away from Encore\nsubtitle: If you love someone, set them free.\nlang: ts\n---\n\n_We realize most people read this page before even trying Encore, so we start with a perspective on how you might reason about adopting Encore. Read on to see what tools are available for migrating away._\n\nPicking technologies for your project is an important decision. It's tricky because you don't know what the requirements are going to look like in the future. This uncertainty makes many teams opt for maximum flexibility, often without acknowledging this has a significant negative effect on productivity.\n\nWhen designing Encore, we've leaned on standardization to provide a well-integrated and highly productive development workflow. The design is based on the core team's experience building scalable distributed systems at Spotify and Google, complemented with loads of invaluable input from the developer community. \n\nIn practise Encore is opinionated only in certain areas which are critical for enabling the static analysis used to create Encore's application model. This is fundamental to how Encore can provide its powerful features, like automatically instrumenting distributed tracing, and provisioning and managing cloud infrastructure.\n\n## Accommodating for your unique requirements\n\nMany software projects end up having a few novel requirements, which are highly specific to the problem domain. To accommodate for this, Encore is designed to let you go outside of the standardized Backend Framework when you need to, for example:\n- You can drop down in abstraction level in the API framework using [raw endpoints](/docs/ts/primitives/defining-apis#raw-endpoints)\n- You can use tools like the [Terraform provider](/docs/platform/integrations/terraform) to integrate infrastructure that is not managed by Encore\n\n## Mitigating risk through Open Source and efficiency\n\nWe believe that adopting Encore is a low-risk decision for several reasons:\n\n- There's no upfront investment needed to get the benefits\n- Encore apps are normal programs where less than 1% of the code is Encore-specific\n- All infrastructure and data is in your own cloud\n- It's simple to integrate with cloud services and systems not natively supported by Encore\n- Everything you need to develop your application is Open Source, including the [parser](https://github.com/encoredev/encore/tree/main/v2/parser), [compiler](https://github.com/encoredev/encore/tree/main/v2/compiler), [runtime](https://github.com/encoredev/encore/tree/main/runtimes)\n- Everything you need to self-host your application is [Open Source and documented](/docs/ts/self-host/build)\n\n## What to expect when migrating away\n\nIf you want to migrate away, we want to ensure this is as smooth as possible! Here are some of the ways Encore is designed to keep your app portable, with minimized lock-in, and the tools provided to aid in migrating away.\n\n### Code changes\n\nBuilding with Encore doesn't require writing your entire application in an Encore-specific way. Encore applications are normal programs where only 1% of the code is specific to Encore's Open Source Backend Framework.\n\nThis means that the changes required to stop using the Backend Framework is almost exactly the same work you would have needed to do if you hadn't used Encore in the first place, e.g. writing infrastructure boilerplate. There is no added migration cost.\n\n### Deployment\n\nIf you are self-hosting your application, then you're already done.\n\nIf you are using Encore Cloud to manage deployments and want to migrate to your own solution, you can use the [self-hosting instructions](/docs/ts/self-host/build) and Open Source CLI tooling. The `encore build docker` command produces a Docker image, containing the compiled application, using exactly the same code path as Encore's CI system to ensure compatibility.\n\nLearn more in the [self-hosting docs](/docs/ts/self-host/build).\n\n### Tell us what you need\n\nWe're engineers ourselves and we understand the importance of not being constrained by a single technology.\n\nWe're working every single day on making it even easier to start, <i>and stop</i>, using Encore.\nIf you have specific concerns, questions, or requirements, we'd love to hear from you!\n\nPlease reach out on [Discord](https://encore.dev/discord) or [send an email](mailto:hello@encore.dev) with your thoughts.\n"
  },
  {
    "path": "docs/ts/observability/dev-dash.md",
    "content": "---\nseotitle: Development dashboard for local development\nseodesc: Encore's Local Development Dashboard comes with build-in distributed tracing, API docs, and real-time architecture diagrams.\ntitle: Local Development Dashboard\nsubtitle: Built-in tools for simplicity and productivity\nlang: ts\n---\n\nEncore provides an efficient local development workflow that automatically provisions [local infrastructure](/docs/platform/infrastructure/infra#local-development) and supports automated testing with dedicated test infrastructure.\n\nThe local environment also comes with a built-in Local Development Dashboard to simplify development and improve productivity. It has several features to help you design, develop, and debug your application:\n\n* [Service Catalog](/docs/ts/observability/service-catalog) with Automatic API Documentation\n* API Explorer to call your APIs\n* [Distributed Tracing](/docs/ts/observability/tracing) for simple and powerful debugging\n* [Encore Flow](/docs/develop/encore-flow) for visualizing your microservices architecture\n\nAll these features update in real-time as you make changes to your application.\n\nTo access the dashboard, start your Encore application with `encore run` and it will open automatically. You can also follow the link in your terminal:\n\n```bash\n$ encore run\nAPI Base URL:      http://localhost:4000\nDev Dashboard URL: http://localhost:9400/hello-world-cgu2\n```\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/localdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/ts/observability/flow.md",
    "content": "---\nseotitle: Encore Flow automatic microservices architecture diagrams\nseodesc: Visualize your microservices architecture automatically using Encore Flow. Get real-time interactive architecture diagrams for your entire application.\ntitle: Flow Architecture Diagram\nsubtitle: Visualize your cloud microservices architecture\nlang: ts\n---\n\nFlow is a visual tool that gives you an always up-to-date view of your entire system, helping you reason about your\nmicroservices architecture and identify which services depend on each other and how they work together.\n\n## Birds-eye view\n\nHaving access to a zoomed out representation of your system can be invaluable in pretty much all parts of the\ndevelopment cycle. Flow helps you:\n\n* Track down bottlenecks before they grow into big problems.\n* Get new team members onboarded much faster.\n* Pinpoint hot paths in your system, services that might need extra attention.\n\nServices and PubSub topics are represented as boxes, arrows indicate a dependency. In the example below\nthe `login` service has dependencies on the `user` and `authentication` services. Dashed arrows shows publications or\nsubscriptions to a topic. Here, `payment` publishes to the `payment-made` topic and `email` subscribe to it:\n\n<img src=\"/assets/docs/flow-diagram.png\" title=\"Encore Flow - Highlight Dependencies\" />\n\n## Highlight dependencies\n\nHover over a service, or PubSub topic, to instantly reveal the nature and scale of its dependencies.\n\nHere the `login` service and its dependencies are highlighted. We can see that `login` makes queries to the\ndatabase and requests to two of the endpoints from the `user` service as well as requests to one endpoint from\nthe `authentication` service:\n\n<img src=\"/assets/docs/flow-highlight.png\" title=\"Encore Flow - Highlight Dependencies\" />\n\n## Real-time updates\n\nFlow is accessible in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and the [Encore Cloud dashboard](https://app.encore.cloud) for cloud environments.\n\nWhen developing locally, Flow will auto update in real-time to reflect your architecture as you\nmake code changes. This helps you be mindful of important dependencies and makes it clear if you introduce new ones.\n\nFor cloud environments, Flow auto-updates with each deploy.\n\nIn the example below a new subscription on the topic `payment-made` is introduced and then removed in `user` service:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/flow-auto-update.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/ts/observability/logging.md",
    "content": "---\nseotitle: Use structured logging to understand your application\nseodesc: Learn how to use structured logging, a combination of free-form log messages and type-safe key-value pairs, to understand your backend application's behavior.\ntitle: Logging\nsubtitle: Structured logging helps you understand your application\nlang: ts\ninfobox: {\n  title: \"Structured Logging\",\n  import: \"encore.dev/log\",\n}\n---\n\nEncore offers built-in support for Structured Logging, which combines a free-form log message with structured and type-safe key-value pairs. This enables straightforward analysis of what your application is doing, in a way that is easy for a computer to parse, analyze, and index. This makes it simple to quickly filter and search through logs.\n\nEncore’s logging is integrated with the built-in [Distributed Tracing](/docs/ts/observability/tracing) functionality, and all logs are automatically included in the active trace. This dramatically simplifies debugging of your application.\n\n## Usage\nFirst, add `import log  from \"encore.dev/log\";` to your module. Then call one of the logging functions `error`, `warn`, `info`, `debug`, or `trace` to emit a log message. For example:\n\n```ts\nlog.info(\"log message\", {is_subscriber: true})\nlog.error(err, \"something went terribly wrong!\")\n```\n\nThe first parameter is the log message (or optionally an error for the error function) . After that follows a single object with key-value pairs for structured logging.\n\nIf you’re logging many log messages with the same key-value pairs each time it can be a bit cumbersome. To help with that, use `log.with()` to group them into a Logger object, which then copies the key-value pairs into each log event:\n\n```ts\nconst logger = log.with({is_subscriber: true})\nlogger.info(\"user logged in\", {login_method: \"oauth\"}) // includes is_subscriber=true\n```\n\n## Live-streaming logs\n\nEncore also makes it simple to live-stream logs directly to your terminal, from any environment, by running:\n\n```\n$ encore logs --env=prod\n```\n"
  },
  {
    "path": "docs/ts/observability/metrics.md",
    "content": "---\nseotitle: Custom metrics in TypeScript\nseodesc: Learn how to define and use custom metrics in your TypeScript backend application with Encore.\ntitle: Metrics\nsubtitle: Track custom metrics in your TypeScript application\ninfobox: {\n  title: \"Metrics\",\n  import: \"encore.dev/metrics\",\n}\nlang: ts\n---\n\nEncore provides built-in support for defining custom metrics in your TypeScript applications. Once defined, metrics are automatically collected and displayed in the Encore Cloud Dashboard, and can be exported to third-party observability services.\n\nSee the [Platform metrics documentation](/docs/platform/observability/metrics) for information about integrations with third-party services like Grafana Cloud and Datadog.\n\n## Defining custom metrics\n\nDefine custom metrics by importing from [`encore.dev/metrics`](https://encore.dev/docs/ts/primitives/metrics) and\ncreating a new metric using the `Counter`, `CounterGroup`, `Gauge`, or `GaugeGroup` classes.\n\nFor example, to count the number of orders processed:\n\n```typescript\nimport { Counter } from \"encore.dev/metrics\";\n\nexport const ordersProcessed = new Counter(\"orders_processed\");\n\nfunction process(order: Order) {\n    // ...\n    ordersProcessed.increment();\n}\n```\n\n## Metric types\n\nEncore currently supports two metric types: counters and gauges.\n\n**Counters** measure the count of something. A counter's value must always increase, never decrease. (Note that the value gets reset to 0 when the application restarts.) Typical use cases include counting the number of requests, the amount of data processed, and so on.\n\n**Gauges** measure the current value of something. Unlike counters, a gauge's value can fluctuate up and down. Typical use cases include measuring CPU usage, the number of active instances running of a process, and so on.\n\n### Counter example\n\n```typescript\nimport { Counter } from \"encore.dev/metrics\";\n\nexport const ordersProcessed = new Counter(\"orders_processed\");\n\nfunction processOrder() {\n    ordersProcessed.increment();\n    // ... process order\n}\n```\n\nYou can also increment by a specific value instead of 1:\n\n```typescript\nimport { Counter } from \"encore.dev/metrics\";\n\nexport const bytesProcessed = new Counter(\"bytes_processed\");\n\nfunction processData(data: Buffer) {\n    bytesProcessed.increment(data.length);\n    // ... process data\n}\n```\n\n### Gauge example\n\n```typescript\nimport { Gauge } from \"encore.dev/metrics\";\n\nexport const cpuUsage = new Gauge(\"cpu_usage\");\n\nfunction updateMetrics() {\n    const usage = getCpuUsage(); // returns a number between 0-100\n    cpuUsage.set(usage);\n}\n```\n\nAnother example tracking active connections:\n\n```typescript\nimport { Gauge } from \"encore.dev/metrics\";\n\nlet activeCount = 0;\nexport const activeConnections = new Gauge(\"active_connections\");\n\nfunction onConnect() {\n    activeCount++;\n    activeConnections.set(activeCount);\n}\n\nfunction onDisconnect() {\n    activeCount--;\n    activeConnections.set(activeCount);\n}\n```\n\n## Defining labels\n\nEncore's metrics package provides a type-safe way of attaching labels to metrics. To define labels, create an interface type representing the labels and then use `CounterGroup` or `GaugeGroup`.\n\nThe labels interface defines the structure of labels, where each property corresponds to a single label. Each property must be of type `string`, `number`, or `boolean`.\n\n<Callout type=\"info\">\n\nWhen using `number` type for labels, the value will be converted to an integer using `Math.floor()`.\n\n</Callout>\n\n### Counter with labels\n\n```typescript\nimport { CounterGroup } from \"encore.dev/metrics\";\n\ninterface Labels {\n    success: boolean;\n}\n\nexport const ordersProcessed = new CounterGroup<Labels>(\"orders_processed\");\n\nfunction process(order: Order) {\n    let success = false;\n    try {\n        // ... process order\n        success = true;\n    } catch (err) {\n        success = false;\n    }\n    ordersProcessed.with({ success }).increment();\n}\n```\n\n### Gauge with labels\n\n```typescript\nimport { GaugeGroup } from \"encore.dev/metrics\";\n\ninterface ConnectionLabels {\n    region: string;\n}\n\nexport const activeConnectionsByRegion = new GaugeGroup<ConnectionLabels>(\"active_connections\");\nconst connectionCounts = new Map<string, number>();\n\nfunction onConnect(region: string) {\n    const count = (connectionCounts.get(region) || 0) + 1;\n    connectionCounts.set(region, count);\n    activeConnectionsByRegion.with({ region }).set(count);\n}\n\nfunction onDisconnect(region: string) {\n    const count = Math.max(0, (connectionCounts.get(region) || 0) - 1);\n    connectionCounts.set(region, count);\n    activeConnectionsByRegion.with({ region }).set(count);\n}\n```\n\n### Multiple labels\n\nYou can define multiple labels for a metric:\n\n```typescript\nimport { CounterGroup } from \"encore.dev/metrics\";\n\ninterface JobLabels {\n    jobType: string;\n    priority: number;\n    success: boolean;\n}\n\nexport const jobsProcessed = new CounterGroup<JobLabels>(\"jobs_processed\");\n\nfunction processJob(jobType: string, priority: number) {\n    try {\n        // ... process job\n        jobsProcessed.with({ jobType, priority, success: true }).increment();\n    } catch (err) {\n        jobsProcessed.with({ jobType, priority, success: false }).increment();\n    }\n}\n```\n\n## Metric references\n\nEncore uses static analysis to determine which services are using each metric, and what operations each service is performing.\n\nThis means metric objects can't be passed around however you like, as it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions, Encore supports defining a \"reference\" to a metric that can be passed around any way you want.\n\nTo create a reference, call the `.ref()` method on any metric:\n\n```typescript\nimport { Counter } from \"encore.dev/metrics\";\n\nexport const ordersProcessed = new Counter(\"orders_processed\");\n\n// Create a reference that can be passed around\nconst metricRef = ordersProcessed.ref();\n\n// Pass the reference to other functions\nfunction logMetric(metric: Counter) {\n    metric.increment();\n}\n\nlogMetric(metricRef);\n```\n\nThis works for all metric types (`Counter`, `CounterGroup`, `Gauge`, and `GaugeGroup`).\n\n<Callout type=\"important\">\n\nEach combination of label values creates a unique time series tracked in memory and stored by the monitoring system.\nUsing numerous labels can lead to a combinatorial explosion, causing high cloud expenses and degraded performance.\n\nAs a general rule, limit the unique time series to tens or hundreds at most, rather than thousands.\n\n</Callout>\n"
  },
  {
    "path": "docs/ts/observability/service-catalog.md",
    "content": "---\nseotitle: Service Catalog & Generated API Docs\nseodesc: See how Encore automatically generates API documentation that always stays up to date and in sync.\ntitle: Service Catalog\nsubtitle: Automatically get a Service Catalog and complete API docs\nlang: ts\n---\n\nAll developers agree API documentation is great to have, but the effort of maintaining it inevitably leads to docs becoming stale and out of date.\n\nTo solve this, Encore uses the [Encore Application Model](/docs/ts/concepts/application-model) to automatically generate a Service Catalog along with complete documentation for all APIs. This ensures docs are always up-to-date as your APIs evolve.\n\nThe API docs are available both in your [Local Development Dashboard](/docs/ts/observability/dev-dash) and for your whole team in the [Encore Cloud dashboard](https://app.encore.cloud).\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/servicecatalogvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n"
  },
  {
    "path": "docs/ts/observability/tracing.md",
    "content": "---\nseotitle: Distributed Tracing helps you understand your app\nseodesc: See how to use distributed tracing in your backend application, across multiple services, using Encore.\ntitle: Distributed Tracing\nsubtitle: Track requests across your application and infrastructure\nlang: ts\n---\n\nDistributed systems often have many moving parts, making it difficult to understand what your code is doing and finding the root-cause to bugs. That’s where Tracing comes in. If you haven’t seen it before, it may just about change your life.\n\nTracing is a revolutionary way to gain insight into what your applications are doing. It works by capturing the series of events as they occur during the execution of your code (a “trace”). This works by propagating a trace id between all individual systems, then correlating and joining the information together to present a unified picture of what happened end-to-end.\n\nAs opposed to the labor intensive instrumentation you'd normally need to go through to use tracing, Encore automatically captures traces for your entire application – in all environments. Uniquely, this means you can use tracing even for local development to help debugging and speed up iterations.\n\nYou view traces in the [Local Development Dashboard](/docs/ts/observability/dev-dash) and in the [Encore Cloud dashboard](https://app.encore.cloud) for Production and other environments.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n\t<source src=\"/assets/docs/tracingvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\" />\n</video>\n\n## Encore's tracing is more comprehensive and more performant than all other tools\n\nUnlike other tracing solutions, Encore understands what each trace event is and captures unique insights about each one. This means you get access to more information than ever before:\n\n* Stack traces\n* Structured logging\n* HTTP requests\n* Network connection information\n* API calls\n* Database queries\n* etc.\n\n## Redacting sensitive data\n\nEncore's tracing automatically captures request and response payloads to simplify debugging.\n\nFor cases where this is undesirable, such as for passwords or personally identifiable information (PII), Encore supports marking endpoints as sensitive. When an endpoint is marked as sensitive, the request and response details from that endpoint will be automatically redacted from the traces it produces.\n\nSee the documentation on [API Schemas](/docs/ts/primitives/defining-apis#sensitive-data) for more information.\n"
  },
  {
    "path": "docs/ts/overview.md",
    "content": "---\nseotitle: Start building backends using Encore.ts\nseodesc: Learn how Encore.ts works, and get to know the powerful features that help you build cloud backend applications easier than ever before.\ntitle: Encore.ts\nsubtitle: Use Encore.ts to build production-ready backend applications and distributed systems\ntoc: false\nlang: ts\n---\n\n<div className=\"min-h-72 bg-blue p-8 relative overflow-hidden not-prose\">\n    <img className=\"absolute left-[55%] -mt-8 top-0 right-0 bottom-0 noshadow\" src=\"/assets/img/dithered-clouds.png\" />\n    <div className=\"w-[75%] lg:w-[75%]\">\n        <h2 className=\"text-white lead-medium\">Quick Start Guide</h2>\n        <div className=\"body-small text-white mt-2\">\n            Build your first Encore.ts application in minutes\n        </div>\n        <a href=\"/docs/ts/quick-start\">\n            <Button className=\"mt-4\" kind=\"primary\" section=\"black\">Get started</Button>\n        </a>\n    </div>\n</div>\n\nEncore.ts is an open source backend framework for building type-safe distributed system. It provides a declarative approach to working with essential backend primitives like APIs, microservices, databases, queues, caches, cron jobs, and storage buckets.\n\nThe framework comes with a lot of built-in tooling for a productive end-to-end developer experience:\n\n- **Local Environment Management**: Encore automatically sets up and runs your local development environment and all local infrastructure.\n- **Enhanced Observability**: Encore comes with tools like a [Local Development Dashboard](/docs/ts/observability/dev-dash), [tracing](/docs/ts/observability/tracing), and a database explorer for monitoring application behavior.\n- **Automatic Documentation**: Generates and maintains [up-to-date documentation](/docs/ts/observability/service-catalog) for APIs and services, and created [architecture diagrams](/docs/ts/observability/flow) for your system.\n- **AI Integration:** Encore comes with built-in tools for effective AI assisted development, like [AI instructions](/docs/ts/ai-integration) and an [MCP server](/docs/ts/cli/mcp).\n- **DevOps Automation Platform (Optional)**: [Encore Cloud](https://encore.cloud) is an optional platform for automating infrastructure provisioning and DevOps processes in your cloud on AWS and GCP.\n\n<div className=\"mt-6 grid grid-cols-2 gap-6 mobile:grid-cols-1 not-prose\">\n    <a className=\"block group relative no-brandient\" target=\"_blank\" href=\"https://www.youtube.com/watch?v=vvqTGfoXVsw\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Watch an intro video</h3>\n                <img className=\"-mt-2 h-16 w-16 noshadow\" src=\"/assets/icons/features/preview-envs.png\" />\n            </div>\n            <div className=\"mt-2\">\n                Get to know the core concepts of Encore in this short video.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" target=\"_blank\" href=\"https://github.com/encoredev/examples\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Example apps</h3>\n                <img className=\"-mt-2 h-16 w-16 noshadow\" src=\"/assets/icons/features/flow.png\" />\n            </div>\n            <div className=\"mt-2\">\n                Ready-made starter apps to inspire your development.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" href=\"/discord\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Join Discord</h3>\n                <div className=\"inline-flex w-16 h-16 items-center justify-center\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 -28.5 256 256\">\n                        <path fill=\"#111111\"\n                            d=\"M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z\" />\n                    </svg>\n                </div>\n            </div>\n            <div className=\"mt-2\">\n                Find answers, ask questions, and chat with other Encore developers.\n            </div>\n        </div>\n    </a>\n    <a className=\"block group relative no-brandient\" href=\"https://github.com/encoredev/encore\">\n        <div className=\"absolute inset-0 bg-black dark:bg-white -z-10\" />\n        <div\n            className=\"min-h-full border border-black dark:border-white p-8 mobile:p-4 bg-white dark:bg-black transition-transform duration-100 ease-in-out group-active:-translate-x-2 group-active:-translate-y-2 group-hover:-translate-x-2 group-hover:-translate-y-2\">\n            <div className=\"flex items-center justify-between\">\n                <h3 className=\"body-small\">Star on GitHub</h3>\n                <div className=\"inline-flex w-16 h-16 items-center justify-center\">\n                    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 16 16\" fill=\"#111111\"\n                        stroke=\"none\">\n                        <path fillRule=\"evenodd\"\n                            d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n                    </svg>\n                </div>\n            </div>\n            <div className=\"mt-2\">\n                Get involved and star Encore on GitHub.\n            </div>\n        </div>\n    </a>\n</div>\n"
  },
  {
    "path": "docs/ts/primitives/api-calls.mdx",
    "content": "---\nseotitle: API Calls with Encore.ts\nseodesc: Learn how to make type-safe API calls in TypeScript with Encore.ts\ntitle: API Calls\nsubtitle: Making API calls is as simple as making function calls\nlang: ts\n---\n\nCalling API endpoints between services, i.e. service-to-service calls, looks like regular function calls with Encore.ts. This gives you a simple monolith-like developer experience, even when you use multiple services.\nThe only thing you need to do is import the service you want to call from `~encore/clients` and then call its API endpoints like functions.\n\nThis works because, when compiling your application, Encore uses [static analysis](/docs/ts/concepts/application-model) to parse all APIs and make them available through the `~encore/clients` module for internal calls.\nYou get all the benefits of function calls, like compile-time checking of all the parameters and auto-completion in your editor, while still allowing the division of code into logical components, services, and systems.\n\n### Example\n\nIn the example below, we import the service `hello` and call the `ping` endpoint using a function call to `hello.ping`.\n\n```typescript\nimport { hello } from \"~encore/clients\"; // import 'hello' service\n\nexport const myOtherAPI = api({}, async (): Promise<void> => {\n  const resp = await hello.ping({ name: \"World\" });\n  console.log(resp.message); // \"Hello World!\"\n});\n```\n\n### Service client references\n\nEncore uses static analysis to determine which services are calling each other. This means service client objects can't be passed around however you like, as it makes static analysis impossible in many cases.\n\nTo simplify your workflow, given these restrictions, Encore supports defining a \"reference\" to a service client that can be passed around any way you want. To create a reference, call the `.ref()` method on any service client:\n\n```typescript\nimport { hello } from \"~encore/clients\";\n\n// Create a reference that can be passed around\nconst helloRef = hello.ref();\n\n// Pass the reference to other functions\nfunction doSomething(client: typeof helloRef) {\n  return client.ping({ name: \"World\" });\n}\n\ndoSomething(helloRef);\n```\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/simple-event-driven\"\n    desc=\"Simple microservices example application with service-to-service API calls.\"\n/>\n"
  },
  {
    "path": "docs/ts/primitives/app-structure.md",
    "content": "---\nseotitle: Structuring your microservices backend application\nseodesc: Learn how to structure your microservices backend application. See recommended app structures for monoliths, small microservices backends, and large scale microservices applications.\ntitle: App Structure\nsubtitle: Structuring your Encore application\nlang: ts\n---\n\nEncore uses a monorepo design and it's best to use one Encore app for your entire backend application. This lets Encore build an application model that spans your entire app, necessary to get the most value out of many\nfeatures like [distributed tracing](/docs/ts/observability/tracing) and [Encore Flow](/docs/ts/observability/flow).\n\nIf you have a large application, see advice on how to [structure an app with several systems](#large-applications-with-several-systems).\n\nIt's simple to integrate Encore applications with pre-existing systems you might have, using APIs and built-in tools like [client generation](/docs/ts/cli/client-generation). See more on how to approach building new functionality incrementally with Encore in the [migrating to Encore](/docs/platform/migration/migrate-to-encore) documentation.\n\n## Monolith or Microservices\n\nEncore is not opinionated about monoliths vs. microservices. It does however let you build microservices applications with a monolith-style developer experience. For example, you automatically get IDE auto-complete when making [API calls between services](/docs/ts/primitives/api-calls), along with cross-service type-safety.\n\nWhen creating a cloud environment on AWS/GCP, Encore enables you to configure if you want to combine multiple services into one process or keep them separate. This can be useful for improved efficiency at smaller scales, and for co-locating services for increased performance. Learn more in the [environments documentation](/docs/platform/deploy/environments).\n\n## Defining services\n\nTo create an Encore service, add a file named `encore.service.ts` in a directory.\n\nThe file must export a service instance, by calling `new Service`, imported from `encore.dev/service`.\n\nFor example:\n\n```ts\n\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"my-service\");\n```\n\nThat's it! Encore will consider this directory and all its subdirectories as part of the service.\n\nWithin the service, you can then [define APIs](/docs/ts/primitives/defining-apis) and use infrastructure resources like querying databases.\n\n<RelatedDocsLink paths={[\"/docs/ts/primitives/services\", \"/docs/ts/primitives/defining-apis\"]} />\n\n## Examples\n\nLet's take a look at a few different approaches to structuring your Encore application, depending on the size and complexity of your application.\n\n### Single-service application\n\nThe best place to start, especially if you're new to Encore, is by having\na single service in your application. Once you've familiarized yourself with\nthe Encore development model, it's easy to break it up into multiple services.\n\nThe best way to do this is by defining the `encore.service.ts` in the root\nof your project, next to the `package.json` file.\n\nOn disk it might look like this (but feel free to change as you see fit):\n\n```\n/my-app\n├── package.json\n├── encore.app\n├──  // ... other project files\n│\n├── encore.service.ts    // defines your service root\n├── api.ts               // API endpoints\n├── db.ts                // Database definition\n```\n\nServices can have subdirectories, so as the complexity of your service grows\nyou can add subdirectories as you see fit, to better organize the code base.\n\n### Multi-service application (Distributed System)\n\nFor larger applications it's often useful to break it apart into multiple\nservices. This helps improve reliability, scalability, and lead to clearer\ncode organization.\n\nEncore makes it easy to structure your application as multiple services.\n\nJust like before, you add an `encore.service.ts` file to mark a directory\n(and its subdirectories) as a service.\n\n\n<Callout type=\"info\">\nNote that services cannot be nested: each must be defined in its own directory,\nand cannot live in a subdirectory of another service.\n\nIf you have a single-service project with a `encore.service.ts` file at the top-level directory of your project, and you want to break it apart, start by moving that service code into a subdirectory.\n</Callout>\n\nOn disk it might look like this:\n\n```\n/my-app\n├── encore.app                       // ... and other top-level project files\n│\n├── hello                            // hello service (directory)\n│   ├── migrations                   // hello service db migrations (directory)\n│   │   └── 1_create_table.up.sql    // hello service db migration\n│   ├── encore.service.ts            // hello service definition\n│   ├── hello.ts                     // hello service APIs\n│   └── hello_test.ts                // tests for hello service\n│\n└── world                            // world service (directory)\n│   ├── encore.service.ts            // world service definition\n    └── world.ts                     // world service APIs\n```\n\n### Large applications with several systems\n\nIf you have a large application with several logical domains, each consisting of multiple services, it can be practical\nto separate these into distinct systems.\n\nSystems are not a special construct in Encore, they only help you divide your application logically around common concerns and purposes. Encore only handles services, the compiler will read your\nsystems and extract the services of your application. As applications grow, systems help you decompose your application\nwithout requiring any complex refactoring.\n\nTo create systems, simply create a sub-directory for each system and put the relevant service packages within it.\n\nAs an example, a company building a Trello app might divide their application into three systems: the **Trello** system\n(for the end-user facing app with boards and cards), the **User** system (for user and organization management), and\nthe **Premium** system (for handling payments and subscriptions).\n\nOn disk it might look like this:\n\n```\n/my-trello-clone\n├── encore.app\n├── package.json                // ... and other top-level project files\n│\n├── trello                      // trello system (a directory)\n│   ├── board                   // board service (a directory)\n│   │   ├── encore.service.ts   // service definition\n│   │   └── board.ts            // service code\n│   │\n│   └── card                    // card service (a directory)\n│       ├── encore.service.ts   // service definition\n│       └── card.ts             // service code\n│\n├── premium                     // premium system (a directory)\n│   ├── payment                 // payment service (a directory)\n│   │   ├── encore.service.ts   // service definition\n│   │   └── payment.ts          // service code\n│   │\n│   └── subscription            // subscription service (a directory)\n│       ├── encore.service.ts   // service definition\n│       └── subscription.ts     // service code\n│\n└── usr                         // usr system (a directory)\n    ├── org                     // org service (a directory)\n    │   ├── encore.service.ts   // service definition\n    │   └── org.ts              // service code\n    │\n    └── user                    // user service (a directory)\n        ├── encore.service.ts   // service definition\n        └── user.ts             // service code\n```\n\nThe only refactoring needed to divide an existing Encore application into systems is to move services into their respective\nsubfolders. This is a simple way to separate the specific concerns of each system. What matters for Encore are the packages containing services, and the division in systems or subsystems will not change the endpoints or\narchitecture of your application.\n\n## Package Management\n\nFor Encore.ts projects, using a single root-level `package.json` file (monorepo approach) is the recommended practice.\nIt has several benefits:\n\n- Ensures consistent dependency versions across your services\n- Simplifies TypeScript configuration management\n- Makes it easier to share common types and utilities\n- Reduces npm install overhead\n- Works seamlessly with TypeScript's project references\n\nEncore.ts also supports separate `package.json` files in sub-packages, with the following limitations:\n- The Encore.ts application must use one package with a single `package.json` file\n- Other separate packages must be pre-transpiled to JavaScript\n\nFurther package management options are planned for the future, particularly for supporting automatically transpiling and bundling workspace packages.\n"
  },
  {
    "path": "docs/ts/primitives/caching.md",
    "content": "---\nseotitle: Using caches in your TypeScript backend application\nseodesc: Learn how to implement caches to optimize response times and reduce cost in your TypeScript microservices cloud backend.\ntitle: Caching\nsubtitle: Optimize response times and reduce costs by avoiding re-work\ninfobox: {\n  title: \"Caching\",\n  import: \"encore.dev/storage/cache\",\n}\nlang: ts\n---\n\nA cache is a high-speed storage layer, commonly used in distributed systems to improve user experiences\nby reducing latency, improving system performance, and avoiding expensive computation.\n\nFor scalable systems you typically want to deploy the cache as a separate\ninfrastructure resource, allowing you to run multiple instances of your application concurrently.\n\nEncore's built-in Caching API lets you use high-performance caches (using [Redis](https://redis.io/)) in a cloud-agnostic declarative fashion. At deployment, Encore will automatically [provision the required infrastructure](/docs/platform/infrastructure/infra).\n\n## Cache clusters\n\nTo use caching in Encore, you must first define a *cache cluster*.\nEach cache cluster defined in your application will be provisioned as a separate Redis instance\nby Encore.\n\nThis gives you fine-grained control over which service(s) should use the same cache cluster\nand which should have a separate one.\n\nIt looks like this:\n\n```typescript\nimport { CacheCluster } from \"encore.dev/storage/cache\";\n\nconst cluster = new CacheCluster(\"my-cache\", {\n  // EvictionPolicy tells Redis how to evict keys when the cache reaches\n  // its memory limit. For typical cache use cases, \"allkeys-lru\" is a good default.\n  evictionPolicy: \"allkeys-lru\",\n});\n```\n\n<Callout type=\"info\">\n\nWhen starting out it's recommended to use a single cache cluster\nthat's shared between your different services.\n\n</Callout>\n\n### Referencing clusters across services\n\nTo use the same cache cluster from multiple services, use `CacheCluster.named()` to reference\nan existing cluster by name instead of creating a new one:\n\n```typescript\nimport { CacheCluster, StringKeyspace } from \"encore.dev/storage/cache\";\n\n// Reference a cluster defined in another service\nconst cluster = CacheCluster.named(\"my-cache\");\n\nconst sessions = new StringKeyspace<{ sessionId: string }>(cluster, {\n  keyPattern: \"session/:sessionId\",\n});\n```\n\n### Eviction policies\n\nThe eviction policy determines how Redis handles keys when the cache reaches its memory limit:\n\n- `\"allkeys-lru\"` - Evicts least recently used keys first (default)\n- `\"noeviction\"` - Returns errors when memory limit is reached\n- `\"allkeys-lfu\"` - Evicts least frequently used keys first\n- `\"allkeys-random\"` - Evicts random keys\n- `\"volatile-lru\"` - Evicts least recently used keys with an expiry set\n- `\"volatile-lfu\"` - Evicts least frequently used keys with an expiry set\n- `\"volatile-ttl\"` - Evicts keys with shortest TTL first\n- `\"volatile-random\"` - Evicts random keys with an expiry set\n\n## Keyspaces\n\nWhen using a cache, each cached item is stored at a particular key, which is typically an arbitrary string.\nIf you use a cache cluster to cache different sets of data, it's important that distinct data sets have non-overlapping keys.\n\nEach value stored in the cache also has a specific type, and certain cache operations can only be performed on certain types. For example, a common cache operation is to increment an integer value that is stored in the cache. If you try to apply this operation on a value that is not an integer, an error is returned.\n\nEncore provides a simple, type-safe solution to these problems through Keyspaces.\n\nIn order to begin storing data in your cache, you must first define a Keyspace.\n\nEach keyspace has a Key type and a Value type. The Key type is much like a map key, in that it tells Encore where in the cache the item is stored. The Key type is combined with the Key Pattern to produce a string that is the Redis cache key.\n\nThe Value type is the type of the values stored in that keyspace. For many keyspaces this is specified in the name of the constructor.\nFor example, `StringKeyspace` stores `string` values, `IntKeyspace` stores `number` values (as 64-bit integers).\n\n### Example: Rate limiting\n\nFor example, if you want to rate limit the number of requests per user ID it looks like this:\n\n```typescript\nimport { CacheCluster, IntKeyspace, expireIn } from \"encore.dev/storage/cache\";\nimport { api, APIError } from \"encore.dev/api\";\nimport { getAuthData } from \"~encore/auth\";\n\nconst cluster = new CacheCluster(\"rate-limit\", {\n  evictionPolicy: \"allkeys-lru\",\n});\n\n// RequestsPerUser tracks the number of requests per user.\n// The cache items expire after 10 seconds without activity.\nconst requestsPerUser = new IntKeyspace<{ userId: string }>(cluster, {\n  keyPattern: \"requests/:userId\",\n  defaultExpiry: expireIn(10 * 1000), // 10 seconds in milliseconds\n});\n\nexport const myEndpoint = api(\n  { expose: true, method: \"GET\", path: \"/my-endpoint\", auth: true },\n  async (): Promise<{ message: string }> => {\n    const auth = getAuthData();\n    if (!auth) {\n      throw APIError.unauthenticated(\"not authenticated\");\n    }\n\n    const count = await requestsPerUser.increment({ userId: auth.userID }, 1);\n    if (count > 10) {\n      throw APIError.resourceExhausted(\"rate limit exceeded\");\n    }\n\n    return { message: \"Hello!\" };\n  }\n);\n```\n\nAs you can see, the `requestsPerUser` defines a `keyPattern` which is set to `\"requests/:userId\"`.\nHere `:userId` refers to the field in the key type object. When you call `requestsPerUser.increment({ userId: \"user123\" }, 1)`,\nEncore generates the Redis key `\"requests/user123\"`.\n\n### Key patterns with multiple fields\n\nYou can define key types with multiple fields to create more complex key patterns:\n\n```typescript\ninterface ResourceKey {\n  userId: string;\n  resourcePath: string;\n}\n\n// ResourceRequestsPerUser tracks the number of requests per user and resource.\nconst resourceRequestsPerUser = new IntKeyspace<ResourceKey>(cluster, {\n  keyPattern: \"requests/:userId/:resourcePath\",\n  defaultExpiry: expireIn(10 * 1000),\n});\n\n// Usage:\nawait resourceRequestsPerUser.increment(\n  { userId: \"user123\", resourcePath: \"api/users\" },\n  1\n);\n```\n\n## Keyspace types\n\nEncore comes with several keyspace types, each designed for different use cases:\n\n### StringKeyspace\n\nStores string values.\n\n```typescript\nimport { StringKeyspace } from \"encore.dev/storage/cache\";\n\nconst tokens = new StringKeyspace<{ tokenId: string }>(cluster, {\n  keyPattern: \"token/:tokenId\",\n  defaultExpiry: expireIn(3600 * 1000), // 1 hour\n});\n\n// Set a value\nawait tokens.set({ tokenId: \"abc123\" }, \"user-token-value\");\n\n// Get a value (returns undefined on cache miss)\nconst token = await tokens.get({ tokenId: \"abc123\" });\n\n// Delete a value\nawait tokens.delete({ tokenId: \"abc123\" });\n```\n\nAdditional string operations:\n- `append(key, value)` - Appends to the existing value\n- `getRange(key, start, end)` - Gets a substring\n- `setRange(key, offset, value)` - Overwrites part of the string\n- `len(key)` - Gets the string length\n\n### IntKeyspace\n\nStores 64-bit integer values. Values are floored to integers using `Math.floor`.\nFor fractional values, use `FloatKeyspace` instead.\n\n```typescript\nimport { IntKeyspace } from \"encore.dev/storage/cache\";\n\nconst counters = new IntKeyspace<{ counterId: string }>(cluster, {\n  keyPattern: \"counter/:counterId\",\n});\n\n// Set a value\nawait counters.set({ counterId: \"visits\" }, 0);\n\n// Increment and get new value\nconst newCount = await counters.increment({ counterId: \"visits\" }, 1);\n\n// Decrement\nconst decremented = await counters.decrement({ counterId: \"visits\" }, 1);\n```\n\n### FloatKeyspace\n\nStores 64-bit floating-point values.\n\n```typescript\nimport { FloatKeyspace } from \"encore.dev/storage/cache\";\n\nconst scores = new FloatKeyspace<{ oddsId: string }>(cluster, {\n  keyPattern: \"odds/:oddsId\",\n});\n\n// Set a value\nawait scores.set({ oddsId: \"game1\" }, 1.5);\n\n// Increment by a float amount\nconst newOdds = await scores.increment({ oddsId: \"game1\" }, 0.1);\n```\n\n### StructKeyspace\n\nStores structured data (objects) serialized as JSON.\n\n```typescript\nimport { StructKeyspace } from \"encore.dev/storage/cache\";\n\ninterface UserProfile {\n  name: string;\n  email: string;\n  preferences: {\n    theme: \"light\" | \"dark\";\n    notifications: boolean;\n  };\n}\n\nconst profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, {\n  keyPattern: \"profile/:userId\",\n  defaultExpiry: expireIn(3600 * 1000),\n});\n\n// Set a structured value\nawait profiles.set(\n  { userId: \"user123\" },\n  {\n    name: \"Alice\",\n    email: \"alice@example.com\",\n    preferences: { theme: \"dark\", notifications: true },\n  }\n);\n\n// Get the value\nconst profile = await profiles.get({ userId: \"user123\" });\n```\n\n### StringListKeyspace\n\nStores ordered lists of string values.\n\n```typescript\nimport { StringListKeyspace } from \"encore.dev/storage/cache\";\n\nconst recentItems = new StringListKeyspace<{ userId: string }>(cluster, {\n  keyPattern: \"recent/:userId\",\n});\n\n// Push items to the list\nawait recentItems.pushRight({ userId: \"user123\" }, \"item1\", \"item2\");\n\n// Get items from the list\nconst items = await recentItems.getRange({ userId: \"user123\" }, 0, -1); // Get all\n\n// Pop an item (returns undefined if empty)\nconst lastItem = await recentItems.popRight({ userId: \"user123\" });\n```\n\n### NumberListKeyspace\n\nStores ordered lists of numeric values.\n\n```typescript\nimport { NumberListKeyspace } from \"encore.dev/storage/cache\";\n\nconst scoreHistory = new NumberListKeyspace<{ playerId: string }>(cluster, {\n  keyPattern: \"scores/:playerId\",\n});\n\n// Push scores\nawait scoreHistory.pushRight({ playerId: \"player1\" }, 100, 200, 150);\n\n// Get all scores\nconst scores = await scoreHistory.items({ playerId: \"player1\" });\n```\n\n### StringSetKeyspace\n\nStores unordered sets of unique string values.\n\n```typescript\nimport { StringSetKeyspace } from \"encore.dev/storage/cache\";\n\nconst tags = new StringSetKeyspace<{ articleId: string }>(cluster, {\n  keyPattern: \"tags/:articleId\",\n});\n\n// Add members to the set\nawait tags.add({ articleId: \"post1\" }, \"typescript\", \"encore\", \"backend\");\n\n// Check membership\nconst hasTag = await tags.contains({ articleId: \"post1\" }, \"typescript\");\n\n// Get all members\nconst allTags = await tags.items({ articleId: \"post1\" });\n\n// Remove members\nawait tags.remove({ articleId: \"post1\" }, \"backend\");\n```\n\n### NumberSetKeyspace\n\nStores unordered sets of unique numeric values.\n\n```typescript\nimport { NumberSetKeyspace } from \"encore.dev/storage/cache\";\n\nconst uniqueScores = new NumberSetKeyspace<{ gameId: string }>(cluster, {\n  keyPattern: \"unique-scores/:gameId\",\n});\n\n// Add scores\nawait uniqueScores.add({ gameId: \"game1\" }, 100, 200, 300);\n\n// Check if a score exists\nconst has100 = await uniqueScores.contains({ gameId: \"game1\" }, 100);\n```\n\n## Expiry options\n\nEncore provides several ways to set cache entry expiration:\n\n```typescript\nimport {\n  expireIn,\n  expireInSeconds,\n  expireInMinutes,\n  expireInHours,\n  expireDailyAt,\n  neverExpire,\n  keepTTL,\n} from \"encore.dev/storage/cache\";\n\n// Expire in milliseconds\nconst expiry1 = expireIn(5000); // 5 seconds\n\n// Expire in seconds\nconst expiry2 = expireInSeconds(30);\n\n// Expire in minutes\nconst expiry3 = expireInMinutes(5);\n\n// Expire in hours\nconst expiry4 = expireInHours(24);\n\n// Expire at a specific time each day (UTC)\nconst expiry5 = expireDailyAt(0, 0, 0); // Midnight UTC\n\n// Never expire (Redis may still evict based on eviction policy)\nconst expiry6 = neverExpire;\n\n// Keep existing TTL when updating (for write operations)\nconst expiry7 = keepTTL;\n```\n\n## Write options\n\nWhen setting values, you can override the default expiry:\n\n```typescript\n// Set with a specific expiry (overrides default)\nawait keyspace.set(key, value, { expiry: expireInMinutes(30) });\n\n// Keep existing TTL when updating\nawait keyspace.set(key, value, { expiry: keepTTL });\n\n// Only set if key doesn't exist (throws CacheKeyExists otherwise)\nawait keyspace.setIfNotExists(key, value);\n\n// Only set if key already exists (throws CacheMiss otherwise)\nawait keyspace.replace(key, value);\n```\n\n## Error handling\n\nCache operations can throw specific error types, all extending the base `CacheError` class:\n\n- `CacheMiss` — thrown by `replace()` when the key does not exist.\n- `CacheKeyExists` — thrown by `setIfNotExists()` when the key already exists.\n\nRead operations like `get()` return `undefined` on cache miss instead of throwing.\n\n```typescript\nimport { CacheError, CacheMiss, CacheKeyExists } from \"encore.dev/storage/cache\";\n\n// get returns undefined on cache miss\nconst value = await keyspace.get(key);\nif (value === undefined) {\n  // Key doesn't exist in cache\n}\n\n// replace throws CacheMiss if the key doesn't exist\ntry {\n  await keyspace.replace(key, newValue);\n} catch (err) {\n  if (err instanceof CacheMiss) {\n    console.log(\"Key doesn't exist, can't replace\");\n  }\n  throw err;\n}\n```\n\n## Local development\n\nFor local development, Encore maintains a local, in-memory implementation of Redis.\nThis implementation is designed to store a small amount of keys (currently 100).\n\nWhen the number of keys exceeds this value, keys are randomly purged to get below the limit.\nThis is designed in order to simulate the ephemeral, transient nature of caches while also\nlimiting memory use. The precise behavior for local development may change over time and should not be relied on.\n"
  },
  {
    "path": "docs/ts/primitives/cookies.mdx",
    "content": "---\nseotitle: Using Cookies in Encore.ts API Endpoints\nseodesc: Learn how to work with cookies in your TypeScript backend applications built with Encore.ts\ntitle: Working with Cookies\nsubtitle: Type-safe cookie handling for web applications\nlang: ts\n---\n\nEncore.ts provides type-safe cookie handling for your API endpoints. Cookies are commonly used for session management, personalization, and tracking. This guide explains how to use cookies in different contexts within your Encore.ts application.\n\n## Where Cookies Can Be Used\n\nCookies can be utilized in three main contexts within Encore.ts:\n\n1. **Auth handler parameters**: Access cookies during authentication\n2. **Response cookies**: Set cookies to be sent back to clients\n3. **Request cookies**: Access cookies sent by clients\n\n## Using Cookies in Auth Handlers\n\nCookies can be used in authentication handlers:\n\n```ts\nimport { Cookie, Gateway } from \"encore.dev/api\";\nimport { authHandler } from \"encore.dev/auth\";\n\n// Define auth parameters with cookies\ninterface AuthParams {\n  sessionId: Cookie<\"sessionId\">;\n}\n\n// Auth handler that uses cookies\nconst auth = authHandler<AuthParams, User>(async ({ sessionId }) => {\n  return validateAndGetUser(sessionId.value);\n});\n\n// Configure the gateway with the auth handler\nexport const gateway = new Gateway({\n  authHandler: auth,\n});\n```\n\n## Setting Cookies in Responses\n\nYou can set cookies in your API responses by including them in your response type:\n\n```ts\nimport { api, Cookie } from \"encore.dev/api\";\n\n// Define a response type with a cookie\ninterface LoginResponse {\n  user: UserData;\n  sessionId: Cookie<\"sessionId\">;\n}\n\n// Create an API endpoint that sets a cookie\nexport const login = api<LoginParams, LoginResponse>({\n  method: \"POST\",\n  path: \"/login\",\n  expose: true,\n}, async (params): Promise<LoginResponse> => {\n  // Authenticate user\n  const user = await authenticateUser(params.username, params.password);\n  const sessionId = generateSessionId();\n\n  // Store session\n  await storeSession(sessionId, user.id);\n\n  // Return response with cookie\n  return {\n    user,\n    sessionId: {\n      value: sessionId,\n      httpOnly: true,\n      secure: true,\n      sameSite: \"Strict\",\n      maxAge: 60 * 60 * 24 * 7, // 7 days\n    }\n  };\n});\n```\n\n## Using Cookies in Requests\n\nWhen creating API endpoints, you can access cookies sent by the client by defining them in your parameter type:\n\n```ts\nimport { api } from \"encore.dev/api\";\n\n// Define a request type with a cookie\ninterface Params {\n  language?: Cookie<\"language\">;\n}\n\n// Create an API endpoint that uses the cookie\nexport const get = api<Params, { msg: string }>(\n  {\n    method: \"GET\",\n    path: \"/user/profile\",\n    expose: true,\n  },\n  async ({ language }) => {\n    // Access the cookie value\n    const lang = language?.value ?? \"en\";\n    return { msg: `your language: ${lang}` };\n  },\n);\n```\n\n## Typed Cookie Values\n\nEncore.ts allows you to specify the type of cookie values for improved type safety. By default, cookie values are strings, but you can define cookies with different data types. If you omit the type parameter, Encore.ts will automatically use `string` as the type.\n\n### Available Cookie Types\n\nYou can use the following types for cookie values:\n\n- `string`: Text data (default when generic parameter is omitted)\n- `number`: Numeric values\n- `boolean`: True/false values\n- `Date`: Date objects\n\n### How to Specify Cookie Types\n\nWhen defining cookies, you can specify the type using the generic parameter:\n\n```ts\ninterface UserPreferences {\n  // Cookies with different types\n  userId: Cookie<number, \"userId\">;\n  darkMode: Cookie<boolean, \"darkMode\">;\n  lastVisit: Cookie<Date, \"lastVisit\">;\n  language: Cookie<string, \"language\">;\n  // Using omitted type parameter (implicitly string)\n  theme: Cookie<\"theme\">;\n}\n\nexport const savePreferences = api<UserPreferences, void>({\n  method: \"POST\",\n  path: \"/user/preferences\",\n  expose: true,\n}, async (params) => {\n  // Type-safe access to cookie values\n  const userId = params.userId.value; // number\n  const isDarkMode = params.darkMode.value; // boolean\n  const lastVisit = params.lastVisit.value; // Date\n  const language = params.language.value; // string\n  const theme = params.theme.value; // string (implicitly typed)\n\n  // Save preferences...\n});\n```\n\n## Cookie Options\n\nWhen setting cookies, you can configure various options:\n\n- **expires**: Sets an expiration date\n- **maxAge**: Sets the cookie lifetime in seconds\n- **domain**: Specifies which domains can receive the cookie\n- **path**: Limits the cookie to a specific path\n- **secure**: Only sends the cookie over HTTPS\n- **httpOnly**: Makes the cookie inaccessible to JavaScript\n- **sameSite**: Controls when cookies are sent with cross-site requests\n  - `\"Strict\"`: Only sent in same-site requests\n  - `\"Lax\"`: Sent with same-site requests and top-level navigations\n  - `\"None\"`: Sent with all requests (requires `secure: true`)\n- **partitioned**: Creates a partitioned cookie using the CHIPS model\n\n## Generated client\n\nThe generated Encore.ts client does not explicitly handle cookies. Instead, it relies on the browser's built-in cookie handling. When using the client in browser environments, cookies will be automatically included in requests and stored from responses.\n\nFor cross-site requests, you need to configure the client to include credentials:\n\n```ts\n// Create a client that includes credentials (cookies) for cross-site requests\nconst client = new Client(Local, { requestInit: { credentials: \"include\" } });\n```\n\n## Best Practices\n\n1. Use `httpOnly: true` for cookies containing sensitive data\n2. Set `secure: true` for production environments\n3. Configure appropriate `sameSite` settings based on your requirements\n4. Use the `maxAge` or `expires` options to limit cookie lifetime\n\nBy following these guidelines, you can effectively leverage cookies in your Encore.ts applications while maintaining security and type safety.\n"
  },
  {
    "path": "docs/ts/primitives/cron-jobs.md",
    "content": "---\nseotitle: Create recurring tasks with Encore's Cron Jobs API\nseodesc: Learn how to create periodic and recurring tasks in your backend application using Encore's Cron Jobs API.\ntitle: Cron Jobs\nsubtitle: Run recurring and scheduled tasks\ninfobox: {\n  title: \"Cron Jobs\",\n  import: \"encore.dev/cron\",\n  example_link: \"/docs/tutorials/uptime\"\n}\nlang: ts\n---\n\nWhen you need to run periodic and recurring tasks, Encore.ts provides a declarative way of using Cron Jobs.\n\nWhen a Cron Job is defined in your application, Encore automatically calls your specified API according to the defined schedule. This eliminates the need for infrastructure maintenance, as Encore manages scheduling, monitoring, and execution of Cron Jobs.\n\n<Callout type=\"info\">\n\nCron Jobs do not run when developing locally or in [Preview Environments](/docs/platform/deploy/preview-environments), but you can always call the API manually to test the behavior.\n\n</Callout>\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/uptime\"\n    desc=\"Uptime Monitoring app that uses a Cron Job to periodically check the uptime of a website.\"\n/>\n\n## Defining a Cron Job\n\nTo define a Cron Job, import `encore.dev/cron` and call `new CronJob`, assigning the result to a top-level variable.\n\n### Example\n\n```ts\nimport { CronJob } from \"encore.dev/cron\";\nimport { api } from \"encore.dev/api\";\n\n// Send a welcome email to everyone who signed up in the last two hours.\nconst _ = new CronJob(\"welcome-email\", {\n\ttitle: \"Send welcome emails\",\n\tevery: \"2h\",\n\tendpoint: sendWelcomeEmail,\n})\n\n// Emails everyone who signed up recently.\n// It's idempotent: it only sends a welcome email to each person once.\nexport const sendWelcomeEmail = api({}, async () => {\n\t// Send welcome emails...\n});\n```\n\nThe `\"welcome-email\"` argument to `new CronJob` is a unique ID you give to each Cron Job.\nIf you later refactor the code and move the Cron Job definition to another package,\nEncore uses this ID to keep track that it's the same Cron Job and not a different one.\n\nWhen this code gets deployed Encore will automatically register the Cron Job in Encore Cloud\nand begin calling the `sendWelcomeEmail` API every two hours.\n\nThe Encore Cloud dashboard provides a convenient user interface for monitoring and debugging\nCron Job executions across all your environments via the `Cron Jobs` menu item:\n\n![Cron Jobs UI](/assets/docs/cron.png)\n\n## Keep in mind when using Cron Jobs\n\n- Cron Jobs do not execute during local development or in [Preview Environments](/docs/platform/deploy/preview-environments). However, you can manually invoke the API to test its behavior.\n- In Encore Cloud, Cron Job executions are limited to **once every hour**, with the exact minute randomized within that hour for users on the Free Tier. To enable more frequent executions or to specify the exact minute within the hour, consider [deploying to your own cloud](/docs/platform/deploy/own-cloud) or upgrading to the [Pro plan](/pricing).\n- Both public and private APIs are supported for Cron Jobs.\n- Ensure that the API endpoints used in Cron Jobs are idempotent, as they may be called multiple times under certain network conditions.\n- API endpoints utilized in Cron Jobs must not accept any request parameters.\n\n## Cron schedules\n\nAbove we used the `every` field, which executes the Cron Job on a periodic basis.\nIt runs around the clock each day, starting at midnight (UTC).\n\nIn order to ensure a consistent delay between each run, the interval used **must divide 24 hours evenly**.\nFor example, `10m` and `6h` are both allowed (since 24 hours is evenly divisible by both),\nwhereas `7h` is not (since 24 is not evenly divisible by 7).\nThe Encore compiler will catch this and give you a helpful error at compile-time if you try to use an invalid interval.\n\n### Cron expressions\n\nFor more advanced use cases, such as running a Cron Job on a specific day of the month, or a specific week day, or similar,\nthe `every` field is not expressive enough.\n\nFor these use cases, Encore provides full support for [Cron expressions](https://en.wikipedia.org/wiki/Cron) by using the `schedule` field\ninstead of the `every` field.\n\nFor example:\n\n```ts\n// Run the monthly accounting sync job at 4am (UTC) on the 15th day of each month.\nconst _ = new CronJob(\"accounting-sync\", {\n\ttitle:    \"Cron Job Example\",\n\tschedule: \"0 4 15 * *\",\n\tendpoint: accountingSync,\n})\n```\n"
  },
  {
    "path": "docs/ts/primitives/database-extensions.md",
    "content": "---\nseotitle: Pre-installed PostgreSQL extensions\nseodesc: See the list of pre-installed PostgreSQL extensions available when using Encore\ntitle: PostgreSQL Extensions\nsubtitle: Pre-installed extensions\ninfobox: {\n  title: \"SQL Databases\",\n  import: \"encore.dev/storage/sqldb\"\n}\nlang: go\n---\n\nEncore uses the [encoredotdev/postgres](https://github.com/encoredev/postgres-image) docker image for local development, CI/CD, and for databases hosted on Encore Cloud.\n\nThe docker image ships with the following PostgreSQL extensions pre-installed and available for use (via `CREATE EXTENSION`):\n\n| Extension                      | Version | Description                                                                                                         |\n| ------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------- |\n| refint                         | 1.0     | functions for implementing referential integrity (obsolete)                                                         |\n| pg_buffercache                 | 1.3     | examine the shared buffer cache                                                                                     |\n| pg_freespacemap                | 1.2     | examine the free space map (FSM)                                                                                    |\n| plpgsql                        | 1.0     | PL/pgSQL procedural language                                                                                        |\n| citext                         | 1.6     | data type for case-insensitive character strings                                                                    |\n| adminpack                      | 2.1     | administrative functions for PostgreSQL                                                                             |\n| moddatetime                    | 1.0     | functions for tracking last modification time                                                                       |\n| amcheck                        | 1.3     | functions for verifying relation integrity                                                                          |\n| seg                            | 1.4     | data type for representing line segments or floating-point intervals                                                |\n| pg_stat_statements             | 1.10    | track planning and execution statistics of all SQL statements executed                                              |\n| pg_trgm                        | 1.6     | text similarity measurement and index searching based on trigrams                                                   |\n| isn                            | 1.2     | data types for international product numbering standards                                                            |\n| btree_gist                     | 1.7     | support for indexing common datatypes in GiST                                                                       |\n| intarray                       | 1.5     | functions, operators, and index support for 1-D arrays of integers                                                  |\n| pg_surgery                     | 1.0     | extension to perform surgery on a damaged relation                                                                  |\n| uuid-ossp                      | 1.1     | generate universally unique identifiers (UUIDs)                                                                     |\n| insert_username                | 1.0     | functions for tracking who changed a table                                                                          |\n| bloom                          | 1.0     | bloom access method - signature file based index                                                                    |\n| pgcrypto                       | 1.3     | cryptographic functions                                                                                             |\n| dblink                         | 1.2     | connect to other PostgreSQL databases from within a database                                                        |\n| tsm_system_rows                | 1.0     | TABLESAMPLE method which accepts number of rows as a limit                                                          |\n| pg_prewarm                     | 1.2     | prewarm relation data                                                                                               |\n| old_snapshot                   | 1.0     | utilities in support of old_snapshot_threshold                                                                      |\n| pageinspect                    | 1.11    | inspect the contents of database pages at a low level                                                               |\n| intagg                         | 1.1     | integer aggregator and enumerator (obsolete)                                                                        |\n| pg_visibility                  | 1.2     | examine the visibility map (VM) and page-level visibility info                                                      |\n| cube                           | 1.5     | data type for multidimensional cubes                                                                                |\n| tablefunc                      | 1.0     | functions that manipulate whole tables, including crosstab                                                          |\n| xml2                           | 1.1     | XPath querying and XSLT                                                                                             |\n| fuzzystrmatch                  | 1.1     | determine similarities and distance between strings                                                                 |\n| pg_walinspect                  | 1.0     | functions to inspect contents of PostgreSQL Write-Ahead Log                                                         |\n| btree_gin                      | 1.3     | support for indexing common datatypes in GIN                                                                        |\n| sslinfo                        | 1.2     | information about SSL certificates                                                                                  |\n| tcn                            | 1.0     | Triggered change notifications                                                                                      |\n| hstore                         | 1.8     | data type for storing sets of (key, value) pairs                                                                    |\n| dict_int                       | 1.0     | text search dictionary template for integers                                                                        |\n| earthdistance                  | 1.1     | calculate great-circle distances on the surface of the Earth                                                        |\n| file_fdw                       | 1.0     | foreign-data wrapper for flat file access                                                                           |\n| autoinc                        | 1.0     | functions for autoincrementing fields                                                                               |\n| ltree                          | 1.2     | data type for hierarchical tree-like structures                                                                     |\n| unaccent                       | 1.1     | text search dictionary that removes accents                                                                         |\n| pgrowlocks                     | 1.2     | show row-level locking information                                                                                  |\n| tsm_system_time                | 1.0     | TABLESAMPLE method which accepts time in milliseconds as a limit                                                    |\n| dict_xsyn                      | 1.0     | text search dictionary template for extended synonym processing                                                     |\n| pgstattuple                    | 1.5     | show tuple-level statistics                                                                                         |\n| postgres_fdw                   | 1.1     | foreign-data wrapper for remote PostgreSQL servers                                                                  |\n| lo                             | 1.1     | Large Object maintenance                                                                                            |\n| postgis_sfcgal-3               | 3.4.2   | PostGIS SFCGAL functions                                                                                            |\n| address_standardizer_data_us-3 | 3.4.2   | Address Standardizer US dataset example                                                                             |\n| address_standardizer-3         | 3.4.2   | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. |\n| postgis_topology-3             | 3.4.2   | PostGIS topology spatial types and functions                                                                        |\n| postgis-3                      | 3.4.2   | PostGIS geometry and geography spatial types and functions                                                          |\n| postgis_raster-3               | 3.4.2   | PostGIS raster types and functions                                                                                  |\n| postgis_tiger_geocoder-3       | 3.4.2   | PostGIS tiger geocoder and reverse geocoder                                                                         |\n| vector                         | 0.7.0   | vector data type and ivfflat and hnsw access methods                                                                |\n| postgis                        | 3.4.2   | PostGIS geometry and geography spatial types and functions                                                          |\n| address_standardizer           | 3.4.2   | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step. |\n| postgis_topology               | 3.4.2   | PostGIS topology spatial types and functions                                                                        |\n| postgis_tiger_geocoder         | 3.4.2   | PostGIS tiger geocoder and reverse geocoder                                                                         |\n| address_standardizer_data_us   | 3.4.2   | Address Standardizer US dataset example                                                                             |\n| postgis_sfcgal                 | 3.4.2   | PostGIS SFCGAL functions                                                                                            |\n| postgis_raster                 | 3.4.2   | PostGIS raster types and functions                                                                                  |\n"
  },
  {
    "path": "docs/ts/primitives/databases.md",
    "content": "---\nseotitle: Using SQL databases for your backend application\nseodesc: Learn how to use SQL databases for your backend application. See how to provision, migrate, and query PostgreSQL databases using Go and Encore.\ntitle: Using SQL databases\nsubtitle: Provisioning, migrating, querying\ninfobox: {\n  title: \"SQL Databases\",\n  import: \"encore.dev/storage/sqldb\",\n  example_link: \"/docs/tutorials/rest-api\"\n}\nlang: ts\n---\n\nEncore treats SQL databases as logical resources and natively supports **PostgreSQL** databases.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/url-shortener\"\n    desc=\"URL Shortener example that uses a PostgreSQL database.\"\n/>\n\n## Creating a database\n\nTo create a database, import `encore.dev/storage/sqldb` and call `new SQLDatabase`, assigning the result to a top-level variable.\nUse a migration file in a directory `migrations` to define the database schema.\n\nFor example:\n\n```typescript\n-- todo/todo.ts --\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\n// Create the todo database and assign it to the \"db\" variable\nconst db = new SQLDatabase(\"todo\", {\n  migrations: \"./migrations\",\n});\n\n// Then, query the database using db.query, db.exec, etc.\n-- todo/migrations/1_create_table.up.sql --\nCREATE TABLE todo_item (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  done BOOLEAN NOT NULL DEFAULT false\n  -- etc...\n);\n```\n\nAs seen above, the `new SQLDatabase()` call takes two parameters: the name of the database, and a configuration object.\nThe configuration object specifies the directory containing the database migration files, which is how you define the database schema.\n\nSee the [Defining the database schema](#defining-the-database-schema) section below for more details.\n\nWith this code in place, Encore will automatically create the database using [Docker](https://docker.com) when you run the command `encore run` in your local environment. Make sure Docker is installed and running on your machine before running `encore run`.\n\n<Callout type=\"info\">\n\nIf your application is already running when you define a new database, you will need to stop and restart `encore run`. This is necessary for Encore to create the new database using Docker.\n\n</Callout>\n\n\nIn cloud environments, Encore automatically injects the appropriate configuration to authenticate and connect to the database, so once the application starts up the database is ready to be used.\n\n## Database Migrations\n\nEncore automatically handles `up` migrations, while `down` migrations must be run manually. Each `up` migration runs sequentially, expressing changes in the database schema from the previous migration.\n\n### Naming Conventions\n\n**File Name Format:** Migration files must start with a number followed by an underscore (`_`), and must increase sequentially. Each file name must end with `.up.sql`.\n\n**Examples:**\n- `1_first_migration.up.sql`\n- `2_second_migration.up.sql`\n- `3_migration_name.up.sql`\n\nYou can also prefix migration files with leading zeroes for better ordering in the editor (e.g., `0001_migration.up.sql`).\n\n### Defining the Database Schema\n\nThe first migration typically defines the initial table structure. For instance, a `todo` service might create `todo/migrations/1_create_table.up.sql` with the following content:\n\n```sql\nCREATE TABLE todo_item (\n    id BIGSERIAL PRIMARY KEY,\n    title TEXT NOT NULL,\n    done BOOLEAN NOT NULL DEFAULT false\n);\n```\n\n### Migration File Structure\n\nMigration files are created in a `migrations` directory within an Encore service. Each file is named `<number>_<name>.up.sql`, where `<number>` is a sequence number for ordering and `<name>` describes the migration.\n\n**Example Directory Structure:**\n```\n/my-app\n├── encore.app                       // ... other top-level project files\n│\n└── todo                             // todo service\n    ├── migrations                   // database migrations (directory)\n    │   ├── 1_create_table.up.sql    // first migration file\n    │   └── 2_add_field.up.sql       // second migration file\n    ├── todo.ts                      // todo service code\n    └── todo.test.ts                 // tests for todo service\n```\n\n## Using databases\n\nOnce you have created the database using `const db = new SQLDatabase(...)` you can start querying and inserting data into the database by calling methods on the `db` variable.\n\n### Querying data\n\nTo query data, use the following methods:\n\n- `db.query`: Returns an asynchronous iterator, yielding rows one by one.\n- `db.queryRow`: Returns a single row, or `null` if no rows are found.\n- `db.queryAll`: Returns an array of all rows.\n- `db.rawQuery`: Similar to `db.query`, but takes a raw SQL string and parameters.\n- `db.rawQueryRow`: Similar to `db.queryRow`, but takes a raw SQL string and parameters.\n- `db.rawQueryAll`: Similar to `db.queryAll`, but takes a raw SQL string and parameters.\n\n\nTypical usage looks like this:\n\n```ts\nconst allTodos = await db.query`SELECT * FROM todo_item`;\nfor await (const todo of allTodos) {\n  // Process each todo\n}\n```\n\nOr to query a single todo item by id:\n\n```ts\nasync function getTodoTitle(id: number): string | undefined {\n  const row = await db.queryRow`SELECT title FROM todo_item WHERE id = ${id}`;\n  return row?.title;\n}\n```\n\nOr to query using raw SQL and parameters:\n\n```ts\nasync function getTodoTitle(id: number): string | undefined {\n  const row = await db.rawQueryRow(\"SELECT title FROM todo_item WHERE id = $1\", id);\n  return row?.title;\n}\n```\n\n\n### Inserting data\n\nTo insert data, or to make database queries that don't return any rows, use `db.exec` or `db.rawExec`.\n\nFor example:\n\n```ts\nawait db.exec`\n  INSERT INTO todo_item (title, done)\n  VALUES (${title}, false)\n`;\n```\n\nOr using raw SQL and parameters:\n\n```ts\nawait db.rawExec(\n  \"INSERT INTO todo_item (title, done) VALUES ($1, $2)\",\n  title,\n  false\n);\n```\n\n### Transactions\n\nTransactions allow you to group multiple database operations into a single unit of work. If any operation within the transaction fails, the entire transaction is rolled back, ensuring data consistency.\n\nThe transaction type implements `AsyncDisposable`, which automatically rolls back the transaction if it is not explicitly committed or rolled back. This ensures that no open transactions are left accidentally.\n\nFor example:\n\n```ts\nawait using tx = await db.begin();\n\nawait db.exec`\n  INSERT INTO todo_item (title, done)\n  VALUES (${title1}, false)\n`;\n\nawait db.exec`\n  INSERT INTO todo_item (title, done)\n  VALUES (${title2}, false)\n`;\n\nawait tx.commit();\n```\n\n\n## Connecting to databases\n\nIt's often useful to be able to connect to the database from outside the backend application. For example for scripts, ad-hoc querying, or dumping data for analysis.\n\nCurrently Encore does not expose user credentials for databases in the local environment or for environments on Encore Cloud. You can use a connection string to connect instead, see below.\n\n### Using the Encore CLI\n\nEncore's CLI comes with built-in support for connecting to databases:\n\n* `encore db shell <database-name> [--env=<name>]` opens a [psql](https://www.postgresql.org/docs/current/app-psql.html)\n  shell to the database named `<database-name>` in the given environment. Leaving out `--env` defaults to the local development environment. `encore db shell` defaults to read-only permissions. Use `--write`, `--admin` and `--superuser` flags to modify which permissions you connect with.\n\n* `encore db conn-uri <database-name> [--env=<name>]` outputs a connection string for the database named `<database-name>`.\n  When specifying a cloud environment, the connection string is temporary. Leaving out `--env` defaults to the local development environment.\n\n* `encore db proxy [--env=<name>]` sets up a local proxy that forwards any incoming connection\n  to the databases in the specified environment.\n  Leaving out `--env` defaults to the local development environment.\n\nSee `encore help db` for more information on database management commands.\n\n### Using database user credentials\n\nFor cloud environments on AWS/GCP you can view database user credentials (created by Encore when provisioning databases) via the Cloud Dashboard:\n\n* Open your app in the [Encore Cloud dashboard](https://app.encore.cloud), navigate to the **Infrastructure** page for the appropriate environment, and locate the `USERS` section within the relevant **Database Cluster**.\n\n## Handling migration errors\n\nWhen Encore applies database migrations, there's always a possibility the migrations don't apply cleanly.\n\nThis can happen for many reasons:\n- There's a problem with the SQL syntax in the migration\n- You tried to add a `UNIQUE` constraint but the values in the table aren't actually unique\n- The existing database schema didn't look like you thought it did, so the database object you tried to change doesn't actually exist\n- ... and so on\n\nIf that happens, Encore rolls back the migration. If it happens during a cloud deployment, the deployment is aborted.\nOnce you fix the problem, re-run `encore run` (locally) or push the updated code (in the cloud) to try again.\n\nEncore tracks which migrations have been applied in the `schema_migrations` table:\n\n```sql\ndatabase=# \\d schema_migrations\n          Table \"public.schema_migrations\"\n Column  |  Type   | Collation | Nullable | Default\n---------+---------+-----------+----------+---------\n version | bigint  |           | not null |\n dirty   | boolean |           | not null |\nIndexes:\n    \"schema_migrations_pkey\" PRIMARY KEY, btree (version)\n```\n\nThe `version` column tracks which migration was last applied. If you wish to skip a migration or re-run a migration,\nchange the value in this column. For example, to re-run the last migration, run `UPDATE schema_migrations SET version = version - 1;`.\n*Note that Encore does not use the `dirty` flag by default.*\n\n## Using an ORM\n\nEncore has all the tools needed to support ORMs and migration frameworks out-of-the-box through named databases and migration files. Writing plain SQL might not work for your use case, or you may not want to use SQL in the first place.\n\nORMs like [Prisma](/docs/ts/develop/orms/prisma) and [Drizzle](/docs/ts/develop/orms/drizzle) can be used with Encore by integrating their logic with a system's database. Encore is not restrictive, it uses plain SQL migration files for its migrations.\n\n* If your ORM of choice can connect to any database using a standard SQL driver, then it can be used with Encore.\n* If your migration framework can generate SQL migration files without any modifications, then it can be used with Encore.\n\nFor more information on using ORMs with Encore, see the [ORMs](/docs/ts/develop/orms) page.\n\n## Sharing databases between services\n\nThere are two primary ways of sharing a database between services:\n\n- You can define the `SQLDatabase` object in a shared module as an exported variable, and reference this object\nfrom every service that needs to access the database.\n- You can define the `SQLDatabase` object in one service using `new SQLDatabase(\"name\", ...)`, and have other services access it by creating a reference using `SQLDatabase.named(\"name\")`.\n\nBoth approaches have the same effect, but the latter is more explicit.\n\n## PostgreSQL Extensions\n\nEncore uses the [encoredotdev/postgres](https://github.com/encoredev/postgres-image) docker image for local development,\nCI/CD, and for databases hosted on Encore Cloud.\n\nThis docker image ships with many popular PostgreSQL extensions pre-installed.\nIn particular, [pgvector](https://github.com/pgvector/pgvector) and [PostGIS](https://postgis.net) are available.\n\nSee [the full list of available extensions](/docs/ts/primitives/databases-extensions).\n\n## Troubleshooting\n\nWhen you run your application locally with `encore run`, Encore will provision local databases using Docker.\nIf this fails with a database error, it can often be resolved if you restart the Encore daemon using `encore daemon` and then try `encore run` again.\n\nIf this does not resolve the issue, here are steps to resolve common errors:\n\n**Error: sqldb: unknown database**\n\nThis error is often caused by a problem with the initial migration file, such as incorrect naming or location.\n\n- Verify that you've [created the migration file](#defining-the-database-schema) correctly, then try `encore run` again.\n\n**Error: could not connect to the database**\n\nWhen you can't connect to the database in your local environment, there's likely an issue with Docker:\n\n- Make sure that you have [Docker](https://docker.com) installed and running, then try `encore run` again.\n- If this fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again.\n\n**Error: Creating PostgreSQL database cluster Failed**\n\nThis means Encore was not able to create the database. Often this is due to a problem with Docker.\n\n- Check if you have permission to access Docker by running `docker images`.\n- Set the correct permissions with `sudo usermod -aG docker $USER` (Learn more in the [Docker documentation](https://docs.docker.com/engine/install/linux-postinstall/))\n- Then log out and log back in so that your group membership is refreshed.\n\n**Error: unable to save docker image**\n\nThis error is often caused by a problem with Docker.\n\n- Make sure that you have [Docker](https://docker.com) installed and running.\n- In Docker, open **Settings > Advanced** and make sure that the setting `Allow the default Docker socket to be used` is checked.\n- If it still fails, restart the Encore daemon by running `encore daemon`, then try `encore run` again.\n\n**Error: unable to add CA to cert pool**\n\nThis error is commonly caused by the presence of the file `$HOME/.postgresql/root.crt` on the filesystem.\nWhen this file is present the PostgreSQL client library will assume the database server has that root certificate,\nwhich will cause the above error.\n\n- Remove or rename the file, then try `encore run` again.\n\n**Resetting databases**\n\nIf your local database is in a bad state (e.g. due to an incomplete migration or corrupt data), you can reset it by running:\n\n```shell\n$ encore db reset <database-name>\n```\n\nThis drops and recreates the database, re-running all migrations from scratch. Use `--all` to reset all databases at once.\n"
  },
  {
    "path": "docs/ts/primitives/defining-apis.mdx",
    "content": "---\nseotitle: Defining type-safe TypeScript APIs with Encore.ts\nseodesc: Learn how to create APIs for your cloud backend application using TypeScript and Encore.ts\ntitle: Defining Type-Safe APIs\nsubtitle: Simplifying type-safe API development\nlang: ts\n---\n\nEncore.ts simplifies creating type-safe, idiomatic TypeScript API endpoints and provides built-in validation for incoming requests.\n\nAt their core, APIs in Encore.ts are normal `async` functions with request and response data types defined as TypeScript interfaces, which Encore.ts uses to encode API requests to HTTP messages.\n\nEncore.ts also parses your source code to understand the request and response schema of each endpoint, automatically handling validation of incoming requests against your schema.\n\n## Defining API endpoints\n\nTo define an API endpoint, use the `api` function from the `encore.dev/api` module. This function wraps a regular TypeScript async function, designating it as an API endpoint. Encore.ts then generates the necessary boilerplate at compile-time.\n\nIn the example below, we define the API endpoint `ping` which accepts `POST` requests and is exposed as `hello.ping` (because our service name is `hello`).\n\n```typescript\n// inside the hello.ts file\nimport { api } from \"encore.dev/api\";\n\nexport const ping = api(\n  { method: \"POST\" },\n  async (p: PingParams): Promise<PingResponse> => {\n    return { message: `Hello ${p.name}!` };\n  },\n);\n```\n\n### Exposing API endpoints to the outside world\n\nWhen you define an API, by default it is not exposed to the outside world, and it can only be called\nby other APIs within the same Encore application.\n\nTo expose an API to the internet, add the `expose: true` field to the options object passed in\nas the first argument to `api`.\n\n- `{ expose: false }` &ndash; defines a private API that is never accessible to the outside world. It can only be called from other services in your app and via cron jobs. This is default value if the `expose` field isn't set.\n- `{ expose: true }` &ndash; defines a public API that anybody on the internet can call\n\n### Requiring authentication data\n\nTo require authentication for an API endpoint, add `auth: true` to the API options.\nWith this option, Encore will first call the authentication handler you've defined to validate the authentication of incoming requests.\n\nSetting `auth: true` can also be useful for internal APIs that aren't exposed to the internet.\nIn that case, it means that the internal caller must have valid authentication data associated with its request.\n\nFinally, even if an API endpoint does not specify `auth: true`, it will still receive any authentication data that was provided.\n\nFor more information on defining APIs that require authentication, see the [authentication guide](/docs/ts/develop/auth).\n\n## API Schemas\n\n### Request and response schemas\n\nIn the example above we defined an API that uses request and response schemas, where the request data is of type `Params` and the response data of type `Response`.\nThat means we need to define them like so:\n\n```typescript\n-- hello.ts --\nimport { api } from \"encore.dev/api\";\n\n// PingParams is the request data for the Ping endpoint.\ninterface PingParams {\n  name: string;\n}\n\n// PingResponse is the response data for the Ping endpoint.\ninterface PingResponse {\n  message: string;\n}\n\n// hello is an API endpoint that responds with a simple response.\nexport const hello = api(\n  { method: \"POST\", path: \"/hello\" },\n  async (p: PingParams): Promise<PingResponse> => {\n    return { message: `Hello ${p.name}!` };\n  },\n);\n```\n\nRequest and response schemas are both optional. There are four different ways of defining an API:\n\n**Using both request and response data:**<br/>\n`api({ ... }, async (params: Params): Promise<Response> => {});`\n\n**Only returning a response:**<br/>\n`api({ ... }, async (): Promise<Response> => {});`\n\n**With only request data:**<br/>\n`api({ ... }, async (params: Params): Promise<void> => {});`\n\n**Without any request or response data:**<br/>\n`api({ ... }, async (): Promise<void> => {});`\n\nAlternatively, you can express these using type parameters, since `api` is a generic function:\n\n**Using both request and response data:**<br/>\n`api<Params, Response>({ ... }, async (params) => {});`\n\n**Only returning a response:**<br/>\n`api<void, Response>({ ... }, async () => {});`\n\n**With only request data:**<br/>\n`api<Params, void>({ ... }, async (params) => {});`\n\n**Without any request or response data:**<br/>\n`api<void, void>({ ... }, async () => {});`\n\n### Customizing request and response encoding\nEncore parses the source code to understand the request and response schema of each endpoint.\nBy default, the data is parsed as a JSON body for incoming requests, and written back as JSON responses.\n\nThis can be customized on a per-field basis, allowing individual fields to be parsed from query strings\nand HTTP headers with ease.\n\nThis is done by using the `Header` and `Query` types defined in the `encore.dev/api` module.\n\n### Headers\n\nHeaders are defined by setting the field type to `Header<\"Name-Of-Header\">`. It can be used in both request and response data types.\n\nIn the example below, the `language` field will be fetched from the\n`Accept-Language` HTTP header.\n\n```ts\nimport { Header } from \"encore.dev/api\";\n\ninterface Params {\n  language: Header<\"Accept-Language\">; // parsed from header\n  author: string; // not a header\n}\n```\n\n### Query parameters\n\nFor `GET`, `HEAD` and `DELETE` requests, parameters are read from the query string by default, since those HTTP methods\ndo not support request bodies.\n\nFor other HTTP methods (that do support request bodies), parameters are by default read from the HTTP request body as JSON.\nIn those cases, the `Query` type can be used to specify that a field should be parsed from the query string instead.\n\nQuery strings are not supported in HTTP responses, and are treated as being part of the HTTP response body in JSON.\n\nIn the example below, the `limit` field will be read from the `limit` query parameter for all HTTP methods,\nwhereas the `author` field will be parsed from the query string only if the method of\nthe request is `GET`, `HEAD` or `DELETE` (and otherwise from the HTTP request body as JSON).\n\n```ts\ninterface Params {\n  limit: Query<number>; // always a query parameter\n  author: string; // query if GET, HEAD or DELETE, otherwise body parameter\n}\n```\n\n### Path parameters\n\nPath parameters are specified by the `path` field in the API Options in `api` call.\nTo specify a placeholder variable, use `:name` and add a function parameter with the same name to the function signature.\nEncore parses the incoming request URL and makes sure it matches the type of the parameter. The last segment of the path\ncan be parsed as a wildcard parameter by using `*name` with a matching function parameter.\n\nEach path parameter (whether a single segment like `:name` or a wildcard parameter like `*name`) must have\na matching field in the request data type.\n\nFor example:\n\n```ts\n// Retrieves a blog post by its id.\nexport const getBlogPost = api(\n    { method: \"GET\", path: \"/blog/:id/*path\" },\n    async (params: {id: number; path: string}): Promise<BlogPost> {\n        // Use id and path to query database...\n    }\n)\n```\n\n### Cookie parameters\n\nCookies are defined by setting the field type to `Cookie<\"Name-Of-Cookie\">`. It can be used in both request and response data types.\n\nIn the example below we define an optional field that will be parsed from the cookie header.\n\n```ts\nimport { Cookie } from \"encore.dev/api\";\n\ninterface Params {\n  settings?: Cookie<\"settings\">; // parsed from cookie header\n}\n```\n\nRead more about cookies [here](/docs/ts/primitives/cookies)\n\n### Fallback routes\n\nEncore supports defining fallback routes that will be called if no other endpoint matches the request,\nusing the syntax `path: \"/!fallback\"`.\n\nThis is often useful when migrating an existing backend service over to Encore, as it allows you to gradually\nmigrate endpoints over to Encore while routing the remaining endpoints to the existing HTTP router using\na raw endpoint with a fallback route.\n\nFor example:\n\n```ts\n// Route all requests to the existing HTTP router if no other endpoint matches.\nexport const fallback = api.raw(\n    { expose: true, method: \"*\", path: \"/!path\" },\n    async (req, resp) {\n        // Call old router\n    }\n)\n```\n\n## Custom HTTP status codes\n\nBy default, Encore automatically sets appropriate HTTP status codes for your API responses. We recommend using these default status codes, but there are situations where you might need to set a custom HTTP status code, such as when porting an existing API that clients depend on for specific status codes.\n\nTo set a custom HTTP status code, include an `HttpStatus` field in your response interface:\n\n```ts\nimport { api, HttpStatus } from \"encore.dev/api\";\n\ninterface Response {\n  msg: string;\n  status: HttpStatus;\n}\n\nexport const endpoint = api(\n  { expose: true, method: \"GET\", path: \"/path\" },\n  async (): Promise<Response> => {\n    return { msg: \"Hello\", status: HttpStatus.Created };\n  }\n);\n```\n\nThe `HttpStatus` enum includes all standard HTTP status codes like `HttpStatus.OK`, `HttpStatus.Created`, `HttpStatus.BadRequest`, etc.\n\n## Raw endpoints\n\nIn case you need to operate at a lower abstraction level, Encore supports defining raw endpoints that let you access the underlying HTTP request. This is often useful for things like accepting webhooks.\nLearn more in the [raw endpoints guide](/docs/ts/primitives/raw-endpoints).\n\n## Sensitive data\n\nWhen handling sensitive information like API keys, passwords, or personally identifiable information (PII), you may want to prevent these details from appearing in traces. Encore provides the `sensitive` option for API endpoints for this purpose.\n\nTo mark an endpoint as sensitive, add `sensitive: true` to the API options:\n\n```typescript\nexport const processPayment = api(\n  { expose: true, method: \"POST\", path: \"/payments\", sensitive: true },\n  async (params: PaymentParams): Promise<PaymentResponse> => {\n    return { /* ... */ };\n  }\n);\n```\n\nWhen `sensitive: true` is set, Encore automatically redacts all request and response payloads and excludes HTTP headers from traces.\n"
  },
  {
    "path": "docs/ts/primitives/errors.md",
    "content": "---\nseotitle: API Errors – Types, Wrappers, and Codes\nseodesc: See how to return structured error information from your APIs using Encore's errs package, and how to build precise error messages for complex business logic.\ntitle: API Errors\nsubtitle: Returning structured error information from your APIs\ninfobox: {\n  title: \"API Errors\",\n  import: \"encore.dev/api\",\n}\nlang: ts\n---\n\nEncore provides a standardized format of returning errors from API endpoints.\n\nIt looks like this:\n\n```json\n// HTTP 404 Not Found\n{\n    \"code\": \"not_found\",\n    \"message\": \"sprocket not found\",\n    \"details\": null\n}\n```\n\nTo return this, throw the `APIError` exception that Encore provides in the `encore.dev/api` module, with the appropriate error code:\n\n```typescript\nimport { APIError, ErrCode } from \"encore.dev/api\";\n\nthrow new APIError(ErrCode.NotFound, \"sprocket not found\");\n\n// or as a shorthand you can also write:\nthrow APIError.notFound(\"sprocket not found\");\n```\n\n\n## Error Codes\n\nThe `ErrCode` type in the `encore.dev/api` module defines error codes for common error scenarios.\nThey are identical to the codes defined by `gRPC` for interoperability.\n\nThe table below summarizes the error codes.\n\n| Code                  | String                  | HTTP Status               |\n|-----------------------|-------------------------|---------------------------|\n| `OK`                  | `\"ok\"`                  | 200 OK                    |\n| `Canceled`            | `\"canceled\"`            | 499 Client Closed Request |\n| `Unknown`             | `\"unknown\"`             | 500 Internal Server Error |\n| `InvalidArgument`     | `\"invalid_argument\"`    | 400 Bad Request           |\n| `DeadlineExceeded`    | `\"deadline_exceeded\"`   | 504 Gateway Timeout       |\n| `NotFound`            | `\"not_found\"`           | 404 Not Found             |\n| `AlreadyExists`       | `\"already_exists\"`      | 409 Conflict              |\n| `PermissionDenied`    | `\"permission_denied\"`   | 403 Forbidden             |\n| `ResourceExhausted`   | `\"resource_exhausted\"`  | 429 Too Many Requests     |\n| `FailedPrecondition`  | `\"failed_precondition\"` | 400 Bad Request           |\n| `Aborted`             | `\"aborted\"`             | 409 Conflict              |\n| `OutOfRange`          | `\"out_of_range\"`        | 400 Bad Request           |\n| `Unimplemented`       | `\"unimplemented\"`       | 501 Not Implemented       |\n| `Internal`            | `\"internal\"`            | 500 Internal Server Error |\n| `Unavailable`         | `\"unavailable\"`         | 503 Unavailable           |\n| `DataLoss`            | `\"data_loss\"`           | 500 Internal Server Error |\n| `Unauthenticated`     | `\"unauthenticated\"`     | 401 Unauthorized          |\n\n\n## Additional details\n\nTo attach additional structured details to errors, use the `withDetails` method on an `APIError`. The details will be returned with the error to external clients.\n\n"
  },
  {
    "path": "docs/ts/primitives/graphql.mdx",
    "content": "---\nseotitle: GraphQL API\nseodesc: Learn how to create a GraphQL API for your cloud backend application using TypeScript and Encore.ts\ntitle: GraphQL\nsubtitle: Serve a GraphQL API under a Raw endpoint\nlang: ts\n---\n\nEncore.ts has great support for GraphQL with its type-safe approach to building APIs.\n\nEncore's automatic tracing also makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)).\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/graphql\"\n    desc=\"Using Apollo GraphQL together with Encore.ts.\"\n/>\n\n## Concept\n\nTo serve a GraphQL API, you can leverage [Raw endpoints](/docs/ts/primitives/raw-endpoints). Raw endpoints provide direct access to the underlying HTTP request and response objects, enabling integration with a GraphQL library. Below is an outline of the high-level steps required for setup:\n\n1. Take client requests with a Raw endpoint.\n2. Pass along the request object (body, headers, query params, etc.) to the GraphQL library.\n3. Use the GraphQL library to handle the queries and mutations.\n4. Return the GraphQL response from the Raw endpoint.\n\nWhich GraphQL library you choose is up to you. It should work any GraphQL library that allows you to pass along the request object and get a GraphQL response object back without having to start a new HTTP server.\n\n## Example\n\nHere's an example using [Apollo](https://www.apollographql.com/docs/apollo-server/) to create a GraphQL API:\n\n```ts\nimport { HeaderMap } from \"@apollo/server\";\nimport { api } from \"encore.dev/api\";\nconst { ApolloServer, gql } = require(\"apollo-server\");\nimport { json } from \"node:stream/consumers\";\n\n// Type definition schema\nconst typeDefs = gql`\n  ...\n`;\n\n// Resolver functions\nconst resolvers = {\n  // ...\n};\n\nconst server = new ApolloServer({ typeDefs, resolvers });\n\nawait server.start();\n\nexport const graphqlAPI = api.raw(\n  { expose: true, path: \"/graphql\", method: \"*\" },\n  async (req, res) => {\n    // Make sure the Apollo server is started\n    server.assertStarted(\"/graphql\");\n\n    // Extract headers in a format that Apollo understands\n    const headers = new HeaderMap();\n    for (const [key, value] of Object.entries(req.headers)) {\n      if (value !== undefined) {\n        headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n      }\n    }\n\n    // Get response from Apollo server\n    const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({\n      httpGraphQLRequest: {\n        headers,\n        method: req.method!.toUpperCase(),\n        body: await json(req),\n        search: new URLSearchParams(req.url ?? \"\").toString(),\n      },\n      context: async () => {\n        return { req, res };\n      },\n    });\n\n    // Set headers\n    for (const [key, value] of httpGraphQLResponse.headers) {\n      res.setHeader(key, value);\n    }\n\n    // Set status code\n    res.statusCode = httpGraphQLResponse.status || 200;\n\n    // Write response if it's complete\n    if (httpGraphQLResponse.body.kind === \"complete\") {\n      res.end(httpGraphQLResponse.body.string);\n      return;\n    }\n\n    // Write response in chunks if it's async\n    for await (const chunk of httpGraphQLResponse.body.asyncIterator) {\n      res.write(chunk);\n    }\n    res.end();\n  },\n);\n```\n\n<RelatedDocsLink paths={[\"/docs/ts/tutorials/graphql\"]} />\n\n## Call REST APIs in resolvers\n\nIt's often a good idea to create REST endpoints for your business logic and let your resolvers forward requests to those endpoints. This has a few benefits:\n\n1. **Getting traces** - Calls to Encore endpoints results in traces being created, even for internal API calls. Having traces makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)).\n2. **Thin resolvers** - By making your REST request/response objects extend the generated GraphQL types, your resolvers will just be thin wrappers around your REST endpoints.\n3. **Testing** - You can easily test your resolvers by mocking the API calls.\n4. **REST & GraphQL** - You will have both a REST and GraphQL API.\n\nHere's an example of how it might look like if you can call a REST API from a resolver:\n\n```graphql\n-- schema.graphql --\ntype Query {\n  books: [Book]\n}\n\ntype Book {\n  title: String!\n  author: String!\n}\n```\n\n```ts\n-- resolver.ts --\n// Import the book service from the generated service clients\nimport { book } from \"~encore/clients\";\nimport { QueryResolvers } from \"../__generated__/resolvers-types\";\n\nconst queries: QueryResolvers = {\n  books: async () => {\n    // Call book.list to get the list of books\n    const { books } = await book.list();\n    return books;\n  },\n};\n\nexport default queries;\n```\n\n```ts\n-- book.ts --\nimport { api } from \"encore.dev/api\";\n// Import Book type the generated schema types\nimport { Book } from \"../__generated__/resolvers-types\";\n\nconst db: Book[] = [\n  {\n    title: \"To Kill a Mockingbird\",\n    author: \"Harper Lee\",\n  },\n  // ...\n];\n\n// REST endpoint to get the list of books\nexport const list = api(\n  { expose: true, method: \"GET\", path: \"/books\" },\n  async (): Promise<{ books: Book[] }> => {\n    return { books: db };\n  },\n);\n```\n\n"
  },
  {
    "path": "docs/ts/primitives/object-storage.md",
    "content": "---\nseotitle: Using Object Storage in your backend application\nseodesc: Learn how you can use Object Storage to store files and unstructured data in your backend application.\ntitle: Object Storage\nsubtitle: Simple and scalable storage APIs for files and unstructured data\ninfobox: {\n  title: \"Object Storage\",\n  import: \"encore.dev/storage/objects\",\n}\nlang: ts\n---\n\nObject Storage is a simple and scalable solution to store files and unstructured data in your backend application.\n\nThe most common implementation is Amazon S3 (\"Simple Storage Service\") and its semantics are universally supported by every major cloud provider.\n\nEncore.ts provides a cloud-agnostic API for working with Object Storage, allowing you to store and retrieve files with ease. It has support for Amazon S3, Google Cloud Storage, as well as any other S3-compatible implementation (such as DigitalOcean Spaces, MinIO, etc.).\n\n\nAdditionally, when you use Encore's Object Storage API you also automatically get:\n\n* Automatic tracing and instrumentation of all Object Storage operations\n* Built-in local development support, storing objects on the local filesystem\n* Support for integration testing, using a local, in-memory storage backend\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/file-upload\" \n    desc=\"Handling file uploads and storing file data in a bucket and in a database\" \n/>\n\n## Creating a Bucket\n\nThe core of Object Storage is the **Bucket**, which represents a collection of files.\nIn Encore, buckets must be declared as package level variables, and cannot be created inside functions.\nRegardless of where you create a bucket, it can be accessed from any service by referencing the variable it's assigned to.\n\nWhen creating a bucket you can configure additional properties, like whether the objects in the bucket should be versioned.\n\nFor example, to create a bucket for storing profile pictures:\n\n```ts\nimport { Bucket } from \"encore.dev/storage/objects\";\n\nexport const profilePictures = new Bucket(\"profile-pictures\", {\n  versioned: false\n});\n```\n\n## Uploading files\n\nTo upload a file to a bucket, use the `upload` method on the bucket variable.\n\n```ts\nconst data = Buffer.from(...); // image data\nconst attributes = await profilePictures.upload(\"my-image.jpeg\", data, {\n  contentType: \"image/jpeg\",\n});\n```\n\nThe `upload` method additionally takes an optional `UploadOptions` parameter\nfor configuring additinal options, like setting the content type (see above),\nor to reject the upload if the object already exists.\n\n\n## Downloading files\n\nTo download a file from a bucket, use the `download` method on the bucket variable:\n\n```ts\nconst data = await profilePictures.download(\"my-image.jpeg\");\n```\n\nThe `download` method additionally takes a set of options to configure the download,\nlike downloading a specific version if the bucket is versioned.\n\n## Listing objects\n\nTo list objects in a bucket, use the `list` method on the bucket variable.\n\nIt returns an async iterator of `ListEntry` objects that you can use to easily\niterate over the objects in the bucket using a `for await` loop.\n\nFor example, to list all profile pictures:\n\n```ts\nfor await (const entry of profilePictures.list({})) {\n  // Do something with entry\n}\n```\n\nThe `ListOptions` type can be used to limit the number of objects returned,\nor to filter them to a specific key prefix.\n\n## Deleting objects\n\nTo delete an object from a bucket, use the `remove` method on the bucket variable.\n\nFor example, to delete a profile picture:\n\n```ts\nawait profilePictures.remove(\"my-image.jpeg\");\n```\n\n## Retrieving object attributes\n\nYou can retrieve information about an object using the `attrs` method on the bucket variable.\nIt returns the attributes of the object, like its size, content type, and ETag.\n\nFor example, to get the attributes of a profile picture:\n\n```ts\nconst attrs = await profilePictures.attrs(\"my-image.jpeg\");\n```\n\nFor convenience there is also `exists` which returns a boolean indicating whether the object exists.\n\n```ts\nconst exists = await profilePictures.exists(\"my-image.jpeg\");\n```\n\n## Configuring Public Buckets\n\nTo configure a bucket to be publicly accessible, set the `public` property to `true` when creating the bucket. This allows objects in the bucket to be accessed via a public URL.\n\nFor example, to create a public bucket for storing profile pictures:\n\n```ts\nexport const publicProfilePictures = new Bucket(\"public-profile-pictures\", {\n  public: true,\n  versioned: false\n});\n```\n\nWhen self-hosting, see how to configure public buckets in the [infrastructure configuration docs](/docs/go/self-host/configure-infra).\n\nWhen deploying with Encore Cloud it will automatically configure the bucket to be publicly accessible and [configure CDN](/docs/platform/infrastructure/infra#production-infrastructure) for optimal content delivery.\n\n### Accessing Public Objects\n\nOnce a bucket is configured as public, you can access its objects using the `publicUrl` method. This method returns the public URL for the specified object.\n\nFor example, to get the public URL of a profile picture:\n\n```ts\nconst url = publicProfilePictures.publicUrl(\"my-image.jpeg\");\nconsole.log(`Public URL: ${url}`);\n```\n\n\n## Error handling\n\nThe methods throw exceptions if something goes wrong, like if the object doesn't exist or the operation fails.\n\nIf an object does not exist, it throws an `ObjectNotFound` error.\n\nIf an upload fails due to a precondition not being met (like if the object already exists\nand the `notExists: true` option is set), it throws a `PreconditionFailed` error.\n\nOther errors are returned as `ObjectsError` errors (which the above errors also extend).\n\n## Bucket references\n\nEncore uses static analysis to determine which services are accessing each bucket,\nand what operations each service is performing.\n\nThat information is used for features such as rendering architecture diagrams, and is used by Encore Cloud to provision infrastructure correctly and configure IAM permissions.\n\nThis means `Bucket` objects can't be passed around however you like,\nas it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions,\nEncore supports defining a \"reference\" to a bucket that can be passed around any way you want.\n\n### Using bucket references\n\nDefine a bucket reference by calling `bucket.ref<DesiredPermissions>()` from within a service, where `DesiredPermissions` is one of the pre-defined permission types defined in the `encore.dev/storage/objects` module.\n\nThis means you're effectively pre-declaring the permissions you need, and only the methods that\nare allowed by those permissions are available on the returned reference object.\n\nFor example, to get a reference to a bucket that can only download objects:\n\n```typescript\nimport { Uploader } from \"encore.dev/storage/objects\";\nconst ref = profilePictures.ref<Uploader>();\n\n// You can now freely pass around `ref`, and you can use\n// `ref.upload()` just like you would `profilePictures.upload()`.\n```\n\nTo ensure Encore still is aware of which permissions each service needs, the call to `bucket.ref`\nmust be made from within a service, so that Encore knows which service to associate the permissions with.\n\nEncore provides permission interfaces for each operation that can be performed on a bucket:\n\n* `Downloader` for downloading objects\n* `Uploader` for uploading objects\n* `Lister` for listing objects\n* `Attrser` for getting object attributes\n* `Remover` for removing objects\n* `SignedDownloader` for generating signed download URLs for objects\n* `SignedUploader` for generating signed upload URLs for objects\n\nIf you need multiple permissions you can combine them using `&`.\nFor example, `profilePictures.ref<Downloader & Uploader>` gives you a reference\nthat allows calling both `download` and `upload`.\n\nFor convenience Encore also provides a `ReadWriter` permission that gives complete read-write access\nto the bucket, granting all the permissions above. It is equivalent to `Downloader & Uploader & Lister & Attrser & Remover`.\n\n## Signed Upload URLs\n\nYou can use `signedUploadUrl` to create signed URLs to allow clients to upload content directly\ninto the bucket over the internet. The URL is always restricted to one filename, and has a set\nexpiration date. Anyone in possession of the URL can upload data under this filename without any\nadditional authentication.\n\n```typescript\nconst uploadUrl = await profilePictures.signedUploadUrl(\"my-user-id\", {ttl: 7200})\n// Pass url to client\n```\n\nThe client can now `PUT` to this URL with the content as a binary payload.\n\n```bash\ncurl -X PUT --data-binary @/home/me/dog-wizard.jpeg \"https://storage.googleapis.com/profile-pictures/my-user-id/?x-goog-signature=b7a1<...>\"\n```\n\n### Why signed upload URLs?\n\nSigned URLs are an alternative to accepting the content payload directly in your API. Content\nupload requests are sometimes inconvenient to handle well: they can be long running and very\nlarge. With signed URLs, the content flows directly into the storage bucket, and only object IDs\nand metadata go through your API service.\n\nThe trade-off is that the upload flow becomes more complex from a client point of view.\n\n## Signed Download URLs\n\nYou can use `signedDownloadUrl` to create signed URLs to allow clients to download content directly\nfrom the bucket, even if it's private. The URL is always restricted to one filename, and has a set\nexpiration date. Anyone in possession of the URL can download the file without any additional\nauthentication.\n\n```typescript\nconst url = await documents.signedDownloadUrl(\"letter-1234\", {ttl: 7200})\n// Pass url to client\n```\n\n### Why signed download URLs?\n\nSimilar to the upload case, signed download URLs is a way to avoid handing large files or bulk\ntraffic through your API. With signed URLs, the content flows directly from the storage bucket,\nand only object IDs and metadata go through your API service.\n\nNote: unless the content is private, prefer serving urls with `publicUrl()` over signed URLs.\nPublic URLs go over CDN, which is typically significantly more performant and cost effective.\n"
  },
  {
    "path": "docs/ts/primitives/pubsub.md",
    "content": "---\nseotitle: Using PubSub in your backend application\nseodesc: Learn how you can use PubSub as an asynchronous message queue in your backend application, a great approach for decoupling services for better reliability.\ntitle: Pub/Sub\nsubtitle: Decoupling services and building asynchronous systems\ninfobox: {\n  title: \"Pub/Sub Messaging\",\n  import: \"encore.dev/pubsub\",\n  example_link: \"/docs/ts/tutorials/uptime\"\n}\nlang: ts\n---\n\nPublishers & Subscribers (Pub/Sub) let you build systems that communicate by broadcasting events asynchronously. This is a great way to decouple services for better reliability and responsiveness.\n\nEncore's Backend Framework lets you use Pub/Sub in a cloud-agnostic declarative fashion. At deployment, Encore automatically [provisions the required infrastructure](/docs/platform/infrastructure/infra).\n\n<GitHubLink \n    href=\"https://github.com/encoredev/examples/tree/main/ts/simple-event-driven\" \n    desc=\"Simple example app with an event-driven architecture using Pub/Sub.\" \n/>\n\n## Creating a Topic\n\nThe core of Pub/Sub is the **Topic**, a named channel on which you publish events.\nTopics must be declared as package level variables, and cannot be created inside functions.\nRegardless of where you create a topic, it can be published to from any service, and subscribed to from any service.\n\nWhen creating a topic, it must be given an event type, a unique name, and a configuration to define its behaviour.\n\nFor example, to create a topic with events about user signups:\n\n```ts\nimport { Topic } from \"encore.dev/pubsub\"\n\nexport interface SignupEvent {\n    userID: string;\n}\n\nexport const signups = new Topic<SignupEvent>(\"signups\", {\n    deliveryGuarantee: \"at-least-once\",\n});\n```\n\n## Publishing events\n\nTo publish an **Event**, call `publish` on the topic passing in the event object (which is the type specified in the `new Topic<Type>` constructor).\n\nFor example:\n\n```ts\nconst messageID = await signups.publish({userID: id});\n\n// If we get here the event has been successfully published,\n// and all registered subscribers will receive the event.\n\n// The messageID variable contains the unique id of the message,\n// which is also provided to the subscribers when processing the event.\n```\n\nBy defining the `signups` topic variable as an exported variable\nyou can also publish to the topic from other services in the same way.\n\n## Subscribing to Events\n\nTo **Subscribe** to events, you create a Subscription as a top-level variable, by calling the\n`new Subscription` constructor.\n\nEach subscription needs:\n- the topic to subscribe to\n- a name which is unique for the topic\n- a configuration object with at least a `handler` function to process the events\n- a configuration object\n\nFor example, to create a subscription to the `signups` topic from earlier:\n\n```ts\nimport { Subscription } from \"encore.dev/pubsub\";\n\nconst _ = new Subscription(signups, \"send-welcome-email\", {\n    handler: async (event) => {\n        // Send a welcome email using the event.\n    },\n});\n```\n\nSubscriptions can be defined in the same service as the topic is declared, or in any other service of your application. Each\nsubscription to a single topic receives the events independently of any other subscriptions to the same topic. This means\nthat if one subscription is running very slowly, it will grow a backlog of unprocessed events.\nHowever, any other subscriptions will still be processing events in real-time as they are published.\n\n### Error Handling\n\nIf a subscription function returns an error, the event being processed will be retried, based on the retry policy\nconfigured on that subscription.\n\nAfter the max number of retries is reached,the event will be placed into a dead-letter queue (DLQ) for that subscriber.\nThis allows the subscription to continue processing events until the bug which caused the event to fail can be fixed.\nOnce fixed, the messages on the dead-letter queue can be manually released to be processed again by the subscriber.\n\n## Customizing message delivery\n\n### At-least-once delivery\n\nThe above examples configure the topic to ensure that, for each subscription, events will be delivered _at least once_.\n\nThis means that if the topic believes the event was not processed, it will attempt to deliver the message again.\n**Therefore, all subscription handlers should be [idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning).** This helps ensure that if the handler is called two or more times, from the outside there's no difference compared to calling it once.\n\nThis can be achieved using a database to track if you have already performed the action that the event is meant to trigger,\nor ensuring that the action being performed is also idempotent in nature.\n\n### Exactly-once delivery\n\nTopics can also be configured to deliver events _exactly once_ by setting the `deliveryGuarantee` field to\n`\"exactly-once\"`. This enables stronger guarantees on the infrastructure level to minimize the likelihood of\nmessage re-delivery.\n\nHowever, there are still some rare circumstances when a message might be redelivered.  For example, if a networking issue\ncauses the acknowledgement of successful processing the message to be lost before the cloud provider receives it\n(the [Two Generals' Problem](https://en.wikipedia.org/wiki/Two_Generals%27_Problem)).  As such, if correctness is critical\nunder all circumstances, it's still advisable to design your subscription handlers to be idempotent.\n\nBy enabling exactly-once delivery on a topic the cloud provider enforces certain throughput limitations:\n- AWS: 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html)).\n- GCP: At least 3,000 messages per second across all topics in the region (can be higher on the region see [GCP PubSub Quotas](https://cloud.google.com/pubsub/quotas#quotas)).\n\n<Callout type=\"important\">\n\nExactly-once delivery does not perform message deduplication on the publishing side.\nIf `publish` is called twice with the same message, the message will be delivered twice.\n\n</Callout>\n\n### Message Attributes\n\nBy default, each field in the event type is encoded as JSON and sent as part of the Pub/Sub message payload.\n\nPub/Sub topics also support sending data as \"attributes\", which are key-value\npairs that enable other behavior like subscriptions that filter messages\nor ensuring message ordering.\n\nTo define that a field should be sent as an attribute, define it with the `Attribute` type.\n\nFor example, to add an attribute named `source`:\n\n```ts\nimport { Topic, Attribute } from \"encore.dev/pubsub\";\n\nexport interface SignupEvent {\n    userID: string;\n    source: Attribute<string>;\n}\n\nexport const signups = new Topic<SignupEvent>(\"signups\", {\n    deliveryGuarantee: \"at-least-once\",\n});\n```\n\n### Ordered Topics\n\nTopics are unordered by default, meaning that messages can be delivered in any order. This allows for better throughput on the topic as messages can be processed in parallel. However, in some cases, messages must be delivered in the order they were published for a given entity.\n\nTo create an ordered topic, configure the topic's `orderingAttribute` to match the name of a top-level `Attribute` field in the event type. This field ensures that messages delivered to the same subscriber are delivered in the order of publishing for that specific field value. Messages with a different value on the ordering attribute are delivered in an unspecified order.\n\nTo maintain topic order, messages with the same ordering key aren't delivered until the earliest message is processed or dead-lettered, potentially causing delays due to [head-of-line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking). Mitigate processing issues by ensuring robust logging and alerts, and appropriate subscription retry policies.\n\n<Callout type=\"info\">\n\nThe `orderingAttribute` currently has no effect in local environments.\n\n</Callout>\n\n#### Throughput limitations\n\nEach cloud provider enforces certain throughput limitations for ordered topics:\n- **AWS:** 300 messages per second for the topic (see [AWS SQS Quotas](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html))\n- **GCP:** 1 MBps for each ordering key (See [GCP Pub/Sub Resource Limits](https://cloud.google.com/pubsub/quotas#resource_limits))\n\n#### Ordered topic example\n\n```ts\nimport { Topic, Attribute } from \"encore.dev/pubsub\";\n\nexport interface CartEvent {\n\tshoppingCartID: Attribute<number>;\n\tevent: string;\n}\n\nexport const cartEvents = new Topic<CartEvent>(\"cart-events\", {\n\tdeliveryGuarantee: \"at-least-once\",\n\torderingAttribute: \"shoppingCartID\",\n})\n\nasync function example() {\n\t// These are delivered in order as they all have the same shopping cart ID\n\tawait cartEvents.publish({shoppingCartID: 1, event: \"item_added\"});\n\tawait cartEvents.publish({shoppingCartID: 1, event: \"checkout_started\"});\n\tawait cartEvents.publish({shoppingCartID: 1, event: \"checkout_completed\"});\n\n\t// This may be delivered at any point as it has a different shopping cart ID.\n\tawait cartEvents.publish({shoppingCartID: 2, event: \"item_added\"});\n}\n```\n\n## Topic references\n\nEncore uses static analysis to determine which services are accessing each Pub/Sub topic,\nand what operations each service is performing.\n\nThat information is used for features such as rendering architecture diagrams, and is used by Encore Cloud to provision infrastructure correctly and configure IAM permissions.\n\nThis means `Topic` objects can't be passed around however you like,\nas it makes static analysis impossible in many cases. To simplify your workflow, given these restrictions,\nEncore supports defining a \"reference\" to a topic that can be passed around any way you want.\n\n### Using topic references\n\nDefine a topic reference by calling `topic.ref<DesiredPermissions>()` from within a service, where `DesiredPermissions` is one of the pre-defined permission types defined in the `encore.dev/pubsub` module. \n\nThis means you're effectively pre-declaring the permissions you need, and only the methods that\nare allowed by those permissions are available on the returned reference object.\n\nFor example, to get a reference to a topic that can publish messages:\n\n```typescript\nimport { Publisher } from \"encore.dev/pubsub\";\nconst ref = cartEvents.ref<Publisher>();\n\n// You can now freely pass around `ref`, and you can use\n// `ref.publish()` just like you would `cartEvents.publish()`.\n```\n\nTo ensure Encore still is aware of which permissions each service needs, the call to `topic.ref`\nmust be made from within a service, so that Encore knows which service to associate the permissions with.\n\nCurrently, the only permission type is `Publisher`, which allows publishing events to the topic.\nWe plan to add more permission types in the future.\n"
  },
  {
    "path": "docs/ts/primitives/raw-endpoints.mdx",
    "content": "---\nseotitle: Raw Endpoints\nseodesc: Learn how to create raw API endpoints for your cloud backend application using TypeScript and Encore.ts\ntitle: Defining Raw Endpoints\nsubtitle: Drop down in abstraction to access the raw HTTP request\nlang: ts\n---\n\nSometimes you need to operate a lower abstraction than Encore.ts normally provides.\nFor example, you might want to access the underlying HTTP request, often useful for things like accepting webhooks.\n\nEncore.ts has you covered using \"raw endpoints\".\n\nTo define a raw endpoint, use the `api.raw` function. It works similarly to\n`api`, but does not accept a request and response schema. Instead, it works like\nthe Node.js `http` module and `Express.js`, where the function receives two\nparameters: a request object and a response writer.\n\nIt looks like this:\n\n```ts\nimport { api } from \"encore.dev/api\";\n\nexport const myRawEndpoint = api.raw(\n  { expose: true, path: \"/raw\", method: \"GET\" },\n  async (req, resp) => {\n    resp.writeHead(200, { \"Content-Type\": \"text/plain\" });\n    resp.end(\"Hello, raw world!\");\n  },\n);\n```\n\nIt can be called like so:\n\n```shell\n$ curl http://localhost:4000/raw\nHello, raw world!\n```\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/slack-bot\"\n    desc=\"Slack Bot example application that uses Raw endpoints to accept webhooks.\"\n/>\n\n<RelatedDocsLink paths={[\"/docs/ts/how-to/file-uploads\"]} />\n"
  },
  {
    "path": "docs/ts/primitives/secrets.md",
    "content": "---\nseotitle: Securely storing API keys and secrets\nseodesc: Learn how to store API keys, and secrets, securely for your backend application. Encore's built in vault makes it simple to keep your app secure.\ntitle: Storing Secrets and API keys\nsubtitle: Simply storing secrets securely\nlang: ts\n---\n\nWouldn't it be nice to store secret values like API keys, database passwords, and private keys directly in the source code?\nOf course, we can’t do that &ndash; it's horrifyingly insecure!\n(Unfortunately, it's also [very common](https://www.ndss-symposium.org/ndss-paper/how-bad-can-it-git-characterizing-secret-leakage-in-public-github-repositories/).)\n\nEncore's built-in secrets manager makes it simple to store secrets in a secure way and lets you use them in your program like regular variables.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/simple-event-driven\"\n    desc=\"Simple event driven example that uses secrets to store an API key\"\n/>\n\n## Using secrets in your application\n\nTo use a secret in your application, define a top-level variable directly in your code by calling the `secret` function from `encore.dev/config`.\n\nFor example:\n\n```ts\nimport { secret } from \"encore.dev/config\";\n\n// Personal access token for deployments\nconst githubToken = secret(\"GitHubAPIToken\");\n\n// Then, resolve the secret value by calling `githubToken()`.\n```\n\nWhen you've defined a secret in your program, the Encore compiler will check that they are set before running or deploying your application.\n\nWhen running your application locally, if a secret is not set, you will get a warning notifying you that a secret value is missing.\n\nWhen deploying to a cloud environment, all secrets must be defined, otherwise the deploy will fail.\n\nOnce you've provided values for all secrets, call the secret as a function.\nFor example:\n\n```ts\nasync function callGitHub() {\n  const resp = await fetch(\"https:///api.github.com/user\", {\n    credentials: \"include\",\n    headers: {\n      Authorization: `token ${githubToken()}`,\n    },\n  });\n  // ... handle resp\n}\n```\n\n<Callout type=\"info\">\n\nSecret keys are globally unique for your whole application. If multiple services use the same secret name they both receive the same secret value at runtime.\n\n</Callout>\n\n## Storing secret values\n\n### Using the Encore Cloud dashboard\n\nThe simplest way to set up secrets is with the Secrets Manager in the Encore Cloud Dashboard. Open your app in the [Encore Cloud dashboard](https://app.encore.cloud), go to **Settings** in the main navigation, and then click on **Secrets** in the settings menu.\n\nFrom here you can create secrets, save secret values, and configure different values for different environments.\n\n<img src=\"/assets/docs/secrets.png\" title=\"Encore's Secrets Manager\"/>\n\n### Using the CLI\n\nIf you prefer, you can also set up secrets from the CLI using:<br/> `encore secret set --type <types> <secret-name>`\n\n`<types>` defines which environment types the secret value applies to. Use a comma-separated list of `production`, `development`, `preview`, and `local`. Shorthands: `prod`, `dev`, `pr`.\n\nFor example `encore secret set --type prod SSHPrivateKey` sets the secret value for production environments,<br/> and `encore secret set --type dev,preview,local GitHubAPIToken` sets the secret value for development, preview, and local environments.\n\nIn some cases, it can be useful to define a secret for a specific environment instead of an environment type.\nYou can do so with `encore secret set --env <env-name> <secret-name>`. Secret values for specific environments\ntake precedence over values for environment types.\n\n### Environment settings\n\nEach secret can only have one secret value for each environment type. For example: If you have a secret value that's shared between `development`, `preview` and `local`, and you want to override the value for `local`, you must first edit the existing secret and remove `local` using the Secrets Manager in the [Encore Cloud dashboard](https://app.encore.cloud). You can then add a new secret value for `local`. The end result should look something like the picture below.\n\n<img src=\"/assets/docs/secretoverride.png\" title=\"Overriding a secret in Encore's Secrets Manager\"/>\n\n### Overriding local secrets\n\nWhen setting secrets via the `encore secret set` command, they are automatically synced to all developers\nworking on the same application, courtesy of the Encore Platform.\n\nIn some cases, however, you want to override a secret only for your local machine.\nThis can be done by creating a file named `.secrets.local.cue` in the root of your Encore application,\nnext to the `encore.app` file.\n\nThe file contains key-value pairs of secret names to secret values. For example:\n\n```cue\nGitHubAPIToken: \"my-local-override-token\"\nSSHPrivateKey: \"custom-ssh-private-key\"\n```\n\n## How it works: Where secrets are stored\n\nWhen you store a secret Encore stores it encrypted using Google Cloud Platform's [Key Management Service](https://cloud.google.com/security-key-management) (KMS).\n\n- **Production / Your own cloud:** When you deploy to production using your own cloud account on GCP or AWS, Encore provisions a secrets manager in your account (using either KMS or AWS Secrets Manager) and replicates your secrets to it. The secrets are then injected into the container using secret environment variables.\n- **Local:** For local secrets Encore automatically replicates them to developers' machines when running `encore run`.\n- **Development / Encore Cloud:** Environments on Encore's development cloud (running on GCP under the hood) work the same as self-hosted GCP environments, using GCP Secrets Manager.\n"
  },
  {
    "path": "docs/ts/primitives/services.mdx",
    "content": "---\nseotitle: Defining Services with Encore.ts\nseodesc: Learn how to create microservices and define APIs for your cloud backend application using TypeScript and Encore. The easiest way of building cloud backends.\ntitle: Defining Services\nsubtitle: Simplifying (micro-)service development\nlang: ts\n---\n\nEncore.ts makes it simple to build applications with one or many services, without needing to manually handle the typical complexity of developing microservices.\n\n## Defining services\n\nTo create an Encore service, add a file named `encore.service.ts` in a directory.\n\nThe file must export a service instance, by calling `new Service`, imported from `encore.dev/service`.\n\nFor example:\n\n```ts\n\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"my-service\");\n```\n\nThat's it! Encore will consider this directory and all its subdirectories as part of the service.\n\nWith multiple services, each service lives in its own directory with its own `encore.service.ts`:\n\n```\n/my-app\n├── package.json\n├── encore.app\n│\n├── hello                      // hello service\n│   ├── encore.service.ts\n│   └── hello.ts\n│\n└── world                      // world service\n    ├── encore.service.ts\n    └── world.ts\n```\n\nFor more on how to structure your application, see the [app structure guide](/docs/ts/primitives/app-structure).\n"
  },
  {
    "path": "docs/ts/primitives/static-assets.mdx",
    "content": "---\nseotitle: Serve static assets\nseodesc: Learn how to serve static assets with Encore.ts\ntitle: Static Assets\nsubtitle: How to serve static assets\nlang: ts\n---\n\nEncore.ts has built-in support for serving static assets (such as images, HTML and CSS files, and JavaScript files).\n\nThis is particularly useful when you want to serve a static website or a single-page application (SPA) that has\nbeen pre-compiled into static files.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/static-files\"\n    desc=\"Example of how to use api.static in to serve static files.\"\n/>\n\n\n## API Reference\n\nServing static files in Encore.ts works similarly to regular API endpoints, but using the `api.static` function instead.\n\n```typescript\nimport { api } from \"encore.dev/api\";\n\nexport const assets = api.static(\n  { expose: true, path: \"/frontend/*path\", dir: \"./assets\" },\n);\n```\n\nThis will serve all files in the `./assets` directory under the `/frontend` path prefix.\n\nEncore automatically serves `index.html` files at the root of a directory. In the case above,\nthat means that requesting the URL `/frontend` will serve the file `./assets/index.html`,\nand `/frontend/hello` will serve the file `./assets/hello` or `./assets/hello/index.html` (whichever exists).\n\n### Serving static files at the root\n\nBy default, Encore requires that API endpoint paths don't conflict with other API endpoints.\nThis can cause problems when you want to serve static files at the root of your domain (such as by setting `path: \"/*path\"`),\nsince that would conflict with all other paths.\n\nTo support this use case, Encore allows defining a route as a \"fallback route\", that gets called only when no other API endpoint matches.\n\nFallback routes use the syntax `!path` instead of `*path`.\n\nIt looks like this:\n\n```typescript\nimport { api } from \"encore.dev/api\";\n\nexport const assets = api.static(\n  { expose: true, path: \"/!path\", dir: \"./assets\" },\n);\n```\n\n### Configuring the 404 response\n\nWhen a file matching the request isn't found, Encore automatically serves a 404 Not Found response.\n\nYou can customize the response by setting the `notFound` option to specify a file that should be served instead:\n\n```typescript\nimport { api } from \"encore.dev/api\";\n\nexport const assets = api.static(\n  { expose: true, path: \"/!path\", dir: \"./assets\", notFound: \"./not_found.html\" },\n);\n```\n\n## Performance\n\nWhen defining static files, the files are served directly from the Encore.ts Rust Runtime.\nThis means that zero JavaScript code is executed to serve the files, freeing up the Node.js\nruntime to focus on executing business logic.\n\nThis dramatically speeds up both the static file serving,\nas well as improving the latency of your API endpoints.\n"
  },
  {
    "path": "docs/ts/primitives/streaming-apis.mdx",
    "content": "---\nseotitle: Developing Streaming APIs\nseodesc: Learn how to create services that stream data.\ntitle: Streaming APIs\nsubtitle: How to create APIs that stream data\nlang: ts\n---\n\nEncore makes it easy to create API endpoints that can stream data to and from your applications.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/streaming-chat\"\n    desc=\"Simple chat app using the Streaming API create a WebSocket stream from a web frontend.\"\n/>\n\n## Different kinds of stream\n\nEncore supports three types of streams, each designed for a specific data flow direction:\n- [**StreamIn**](#streamin): When you need to stream data into your service.\n- [**StreamOut**](#streamout): When you need to stream data out from your service.\n- [**StreamInOut**](#streaminout): When you need to stream data into and out of your service.\n\n## How it works\n\nWhen you connect to a streaming API endpoint, the client and server will do a handshake in the form of a HTTP request. If the server accepts the handshake request, a stream is returned to the client and to the API handler. Under the hood the stream is a WebSocket that can be used to send and receive messages over.\n\nPath parameters, query parameters and headers can be passed via the handshake request. The stream returned to the client and to the API handler are typed with the incoming and outgoing message types that you specify in your API.\n\n## Defining streaming APIs\n\nSimilar to how you can define [RESTful API endpoints](/docs/ts/primitives/defining-apis) with Encore, you can also easily define type-safe streaming API endpoints. They accept a handshake type, an incoming and an outgoing message type (depending on your choice of stream direction). The type parameters are required for Encore to understand your API.\n\nIf you don't need any data from the handshake, you can ignore that type, and only specify the incoming and outgoing message types.\n\n<GitHubLink\n    href=\"https://github.com/encoredev/examples/tree/main/ts/streaming\"\n    desc=\"Example showcases the all different streaming APIs: api.streamIn, api.streamOut, and api.streamInOut\"\n/>\n\n### StreamIn\n\nUse `api.streamIn` when you want to have a stream from client to server, for example if you are uploading something from the client to the server:\n\n```typescript\nimport { api } from \"encore.dev/api\";\nimport log from \"encore.dev/log\";\n\n// Used to pass initial data, optional.\ninterface Handshake {\n  user: string;\n}\n\n// What the clients sends over the stream.\ninterface Message {\n  data: string;\n  done: boolean;\n}\n\n// Returned when the stream is done, optional.\ninterface Response {\n  success: boolean;\n}\n\nexport const uploadStream = api.streamIn<Handshake, Message, Response>(\n  { path: \"/upload\", expose: true },\n  async (handshake, stream) => {\n    const chunks: string[] = [];\n    try {\n      // The stream object is an AsyncIterator that yields incoming messages.\n      for await (const data of stream) {\n        chunks.push(data.data);\n        // Stop the stream if the client sends a \"done\" message\n        if (data.done) break;\n      }\n    } catch (err) {\n      log.error(`Upload error by ${handshake.user}:`, err);\n      return { success: false };\n    }\n    log.info(`Upload complete by ${handshake.user}`);\n    return { success: true };\n  },\n);\n```\n\nFor `api.streamIn` you need to specify the incoming message type, the handshake type is optional. You can also specify a optional outgoing type if your API handler responds with some data when it is done with the incoming stream.\n\n```ts\napi.streamIn<Handshake, Incoming, Outgoing>(\n  {...}, async (handshake, stream): Promise<Outgoing> => {...})\n```\n\n```ts\napi.streamIn<Handshake, Incoming>(\n  {...}, async (handshake, stream) => {...})\n```\n\n```ts\napi.streamIn<Incoming, Outgoing>(\n  {...}, async (stream): Promise<Outgoing> => {...})\n```\n\n```ts\napi.streamIn<Incoming>(\n  {...}, async (stream) => {...})\n```\n\n### StreamOut\n\nUse `api.streamOut` if you want to have a stream of messages from the server to client, for example if you are streaming logs from the server:\n\n```typescript\nimport { api, StreamOut } from \"encore.dev/api\";\nimport log from \"encore.dev/log\";\n\n// Used to pass initial data, optional.\ninterface Handshake {\n  rows: number;\n}\n\n// What the server sends over the stream.\ninterface Message {\n  row: string;\n}\n\nexport const logStream = api.streamOut<Handshake, Message>(\n  { path: \"/logs\", expose: true },\n  async (handshake, stream) => {\n    try {\n      for await (const row of mockedLogs(handshake.rows, stream)) {\n        // Send the message to the client\n        await stream.send({ row });\n      }\n    } catch (err) {\n      log.error(\"Upload error:\", err);\n    }\n  },\n);\n\n// This function generates an async iterator that yields mocked log rows\nasync function* mockedLogs(rows: number, stream: StreamOut<Message>) {\n  for (let i = 0; i < rows; i++) {\n    yield new Promise<string>((resolve) => {\n      setTimeout(() => {\n        resolve(`Log row ${i + 1}`);\n      }, 500);\n    });\n  }\n\n  // Close the stream when all logs have been sent\n  await stream.close();\n}\n```\n\nFor `api.streamOut` you need to specify the outgoing message type, the handshake type is optional.\n\n```ts\napi.streamOut<Handshake, Outgoing>(\n  {...}, async (handshake, stream) => {...})\n```\n\n```ts\napi.streamOut<Outgoing>(\n  {...}, async (stream) => {...})\n```\n\n### StreamInOut\n\nUse `api.streamInOut` when you want to stream messages in both directions, for example if you are building a chat application:\n\n```typescript\nimport { api } from \"encore.dev/api\";\n\ninterface InMessage {\n  // ...\n}\n\ninterface OutMessage {\n  // ...\n}\n\nexport const ChatStream = api.streamInOut<InMessage, OutMessage>(\n  { path: \"/chat\", expose: true },\n  async (stream) => {\n    for await (const chatMessage of stream) {\n      // Respond to the message by sending something back\n      await stream.send({ /* ... */ })\n    }\n  }\n);\n```\n\nFor `api.streamInOut` you need to specify both the incoming and outgoing message types, the handshake type is optional.\n\n```ts\napi.streamInOut<Handshake, Incoming, Outgoing>(\n  {...}, async (handshake, stream) => {...})\n```\n\n```ts\napi.streamInOut<Incoming, Outgoing>(\n  {...}, async (stream) => {...})\n```\n\n\n## Handshake\n\nWhen you connect to a streaming API endpoint, the client and server will do a handshake in the form of a HTTP request. For all stream types the handshake type is optional, and only needs to be used whenever you need data from the initial request, such as path parameters, query parameters or headers.\n\nNote that if you add a handshake data type you also get two arguments to your handler, one for the handshake data and one for the stream, and if you omit the handshake type you only get the stream.\n\n## Requiring authentication\n\nYou can use your `authHandler` in the same way as for regular endpoints, just specify `auth: true` in your endpoint options. The auth data will be passed from the client to the server via query parameters or headers in the initial handshake request.\n\nAfter a request has been successfully authenticated, you can access authentication data passed from the `authHandler` by calling `getAuthData()`. See more details in the [auth handler docs](/docs/ts/develop/auth#authentication-handlers).\n\n## Broadcasting messages\n\nTo broadcast messages to all connected clients, you can store the streams in a map and iterate over them when a new message is received. If a client disconnects, you can remove the stream from the map.\n\n```ts\nimport { api, StreamInOut } from \"encore.dev/api\";\n\n// Map to hold all connected streams\nconst connectedStreams: Map<\n  string,\n  StreamInOut<ChatMessage, ChatMessage>\n> = new Map();\n\n// Object sent from the client to the server when establishing a connection\ninterface HandshakeRequest {\n  id: string;\n}\n\n// Object by both server and client\ninterface ChatMessage {\n  username: string;\n  msg: string;\n}\n\nexport const chat = api.streamInOut<HandshakeRequest, ChatMessage, ChatMessage>(\n  { expose: true, auth: false, path: \"/chat\" },\n  async (handshake, stream) => {\n    connectedStreams.set(handshake.id, stream);\n\n    try {\n      // The stream object is an AsyncIterator that yields incoming messages.\n      // The loop will continue as long as the client keeps the connection open.\n      for await (const chatMessage of stream) {\n        for (const [key, val] of connectedStreams) {\n          try {\n            // Send the users message to all connected clients.\n            await val.send(chatMessage);\n          } catch (err) {\n            // If there is an error sending the message, remove the client from the map.\n            connectedStreams.delete(key);\n          }\n        }\n      }\n    } catch (err) {\n      // If there is an error reading from the stream, remove the client from the map.\n      connectedStreams.delete(handshake.id);\n    }\n\n    // When the client disconnects, remove them from the map.\n    connectedStreams.delete(handshake.id);\n  },\n);\n```\n\n## Connecting with the client\n\nUsing the [generated client](/docs/ts/cli/client-generation), you can connect to a streaming API endpoint that have `expose` set to `true`. The client stream acts as an async iterator, allowing you to retrieve messages by simply iterating over it:\n\n```typescript\nconst stream = client.serviceName.endpointName();\nfor await (const msg of stream) {\n  // Do something with each message\n}\n\n```\n\nTo send messages to the service, use the async `send` method:\n\n```typescript\nconst stream = client.serviceName.endpointName();\nawait stream.send({ ... });\n```\n\nTo handle network errors or do some cleanup after the connection is closed, you can attach event listeners on the underlying socket:\n\n```typescript\nconst stream = client.serviceName.endpointName();\n\nstream.socket.on(\"error\", (event) => {\n  // An error occurred\n});\n\nstream.socket.on(\"close\", (event) => {\n  // Connection was closed\n});\n\n```\n\n## Service to service streaming\n\nLike with [other endpoint types](/docs/ts/primitives/api-calls) you can easily use streaming between services by importing `~encore/clients`.\nIf you want the stream to only be reachable by other services (and not from the public internet), set the `expose` option to false.\n\nExample of using a stream endpoint from a regular api endpoint:\n\n```typescript\nimport { chat } from \"~encore/clients\"; // import 'chat' service\n\nexport const myOtherAPI = api({}, async (): Promise<void> => {\n  const stream = await chat.myStreamingEndpoint();\n\n  // send a message to the chat service over the stream\n  await stream.send({ msg: \"data\" });\n\n  for await (const msg of stream) {\n    // handle incoming message\n  }\n});\n```\n\n"
  },
  {
    "path": "docs/ts/primitives/types.mdx",
    "content": "---\nseotitle: Types in Encore.ts API schemas\nseodesc: Learn how to work with types in Encore.ts schemas\ntitle: Types\nsubtitle: Types in API schemas\nlang: ts\n---\n\nWhen you define APIs in Encore.ts, the TypeScript types you use for request and response data are analyzed to generate your API schema. This schema is used for automatic validation, API documentation, and generating type-safe clients.\n\nTo ensure your API schema can be properly represented and serialized, Encore.ts reduces complex TypeScript types into basic types that can be represented in JSON. This means your API schemas should use simple, serializable types like strings, numbers, booleans, objects, and arrays.\n\n## Decimal\n\nJavaScript's native `number` type uses floating-point arithmetic, which can lead to precision errors when working with decimal values. For example, `0.1 + 0.2` equals `0.30000000000000004` instead of `0.3`. Additionally, JavaScript numbers are limited to values between `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` (approximately ±9 quadrillion).\n\nTo handle decimal values with arbitrary precision and arbitrarily large numbers, Encore.ts provides the `Decimal` type from `encore.dev/types`. This type is especially useful for financial calculations, prices, scientific computations, or any scenario where exact decimal precision and large number support are required.\n\n### Using Decimal in APIs\n\nThe `Decimal` type can be used in your API request and response schemas just like any other type:\n\n```typescript\nimport { api } from \"encore.dev/api\";\nimport { Decimal } from \"encore.dev/types\";\n\ninterface PaymentRequest {\n  amount: Decimal;\n  currency: string;\n}\n\ninterface PaymentResponse {\n  total: Decimal;\n  tax: Decimal;\n}\n\nexport const processPayment = api(\n  { expose: true, method: \"POST\", path: \"/payments\" },\n  async (req: PaymentRequest): Promise<PaymentResponse> => {\n    const taxRate = new Decimal(\"0.15\"); // 15% tax\n    const tax = req.amount.mul(taxRate);\n    const total = req.amount.add(tax);\n\n    return { total, tax };\n  }\n);\n```\n\n### Creating Decimal values\n\nYou can create a `Decimal` from strings, numbers, or bigints:\n\n```typescript\nimport { Decimal } from \"encore.dev/types\";\n\nconst price1 = new Decimal(\"19.99\");\nconst price2 = new Decimal(29.99);\nconst price3 = new Decimal(100n);\n```\n\nFor maximum precision, it's recommended to use string literals when creating `Decimal` values to avoid any floating-point conversion issues.\n\n### Arithmetic operations\n\nThe `Decimal` type supports basic arithmetic operations:\n\n```typescript\nconst a = new Decimal(\"10.50\");\nconst b = new Decimal(\"2.25\");\n\nconst sum = a.add(b);        // 12.75\nconst difference = a.sub(b); // 8.25\nconst product = a.mul(b);    // 23.625\nconst quotient = a.div(b);   // 4.666666...\n```\n\n## Type compatibility and limitations\n\nEncore.ts analyzes your TypeScript types to generate API schemas, but TypeScript's type system is incredibly complex and supports many advanced features. While we continuously add support for new type patterns, not all TypeScript type combinations are currently supported for API schemas.\n\n### Working with ORM types\n\nA common scenario where you might encounter type compatibility issues is when using types from ORMs (Object-Relational Mappers) or database libraries directly in your API schemas. These types often use complex features from the TypeScript type system.\n\nIn such cases, it's often better to create dedicated API types and convert between them and your ORM types.\n\n### Benefits of separate API types\n\nCreating dedicated API types instead of reusing ORM types can have several advantages:\n\n- **Better API design**: Your API schema doesn't have to match your database schema 1:1. You can expose only the fields that make sense for your API consumers.\n- **Security**: Avoid accidentally exposing sensitive internal fields like password hashes or soft-delete timestamps.\n- **Stability**: Changes to your database schema don't automatically affect your API contract.\n- **Type compatibility**: Avoid issues with complex ORM-specific types that may not be supported in API schemas.\n\nIf you encounter a type that doesn't work in your API schema, creating a dedicated API type as shown above can be a good approach. In many cases, reusing your database types directly works fine, so use separate API types when it makes sense for your use case.\n"
  },
  {
    "path": "docs/ts/primitives/validation.mdx",
    "content": "---\nseotitle: Request validation with Encore.ts\nseodesc: Learn how to validation incoming requests with Encore.ts\ntitle: Validation\nsubtitle: Validate incoming requests\nlang: ts\n---\n\nWhen receiving incoming requests it's best practice to validate the payload to ensure it meets your expectations and includes all required fields.\n\nEncore.ts has request validation built in, designed to work seamlessly with TypeScript. It uses the natural TypeScript types directly to validate incoming requests, so you get the best of both worlds: the clean, concise TypeScript syntax, and runtime schema validation. This means your APIs are type-safe during both runtime and compile-time.\n\nEncore.ts makes it easy to define API endpoints that combine data from different sources: some fields from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this:\n\n```ts\nimport { Header, Query, api } from \"encore.dev/api\";\n\ninterface Request {\n  // Optional query parameter. Parsed from the request URL.\n  limit?: Query<number>;\n\n  // Custom header that must be set. Parsed from the HTTP headers.\n  myHeader: Header<\"X-My-Header\">;\n\n  // Required enum. Parsed from the request body.\n  type: \"sprocket\" | \"widget\";\n}\n\nexport const myEndpoint = api<Request, Response>(\n  { expose: true, method: \"POST\", path: \"/api\" },\n  async ({ limit, myHeader, type }) => {\n    // ...\n  },\n);\n```\n\nIn the above example, if a request to the endpoint is missing the `X-My-Header` HTTP header or the `type` field in the request body, Encore will return a `400` Bad Request response. The `limit` query parameter is optional and will be either a number or `undefined` in the endpoint handler function.\n\n## Supported validation types\n\n### String fields\n\n```ts\ninterface Schema {\n  name:  string;\n}\n```\n\n### Number fields\n\nThe `number` type will accept both int and float values:\n\n```ts\ninterface Schema {\n  age:  number;\n}\n```\n\n### Boolean fields\n\n```ts\ninterface Schema {\n  isHuman:  boolean;\n}\n```\n\n### Array fields\n\nYou can define fields wit array values of `string`, `number`, `boolean`, `null`:\n\n```ts\ninterface Schema {\n  strings:  string[];\n  numbers:  number[];\n  booleans:  boolean[];\n  nulls: null[];\n}\n```\n\nYou can have an array of objects:\n\n```ts\ninterface Schema {\n  users:  { name: string; age: number }[];\n}\n```\n\nIt is also possible to have arrays of multiple types:\n\n```ts\ninterface Schema {\n  values:  (string | number)[];\n}\n```\n\n### Enum fields\n\nString enums can be used in the request schema to validate that a field is one of a set of predefined values. For example:\n\n```ts\ninterface Schema {\n  type:  \"BLOG_POST\" | \"COMMENT\"\n}\n```\n\nYou can also use TypeScript enums:\n\n```ts\nenum PostType {\n  BlogPost = \"BLOG_POST\",\n  Comment = \"COMMENT\"\n}\n\ninterface Schema {\n  type: PostType,\n}\n```\n\nThe two above examples are equivalent.\n\n### Optional fields\n\nYou can make a field optional by adding a `?` after the field name:\n\n```ts\ninterface Request {\n  name?: string;\n}\n\nexport const myEndpoint = api(\n  { expose: true, method: \"POST\", path: \"/body\" },\n  async (req: Request) => {\n    // req.name is a string or undefined\n  },\n);\n```\n\nRequest will reach the endpoint handler even if the `name` field is missing. If the field is missing, the value will be `undefined`.\n\n### Nullable fields\n\nYou can make a field nullable by using the `| null` type:\n\n```ts\ninterface Request {\n  name: string | null;\n}\n\nexport const myEndpoint = api(\n  { expose: true, method: \"POST\", path: \"/body\" },\n  async (req: Request) => {\n    // req.name is a string or null\n  },\n);\n```\n\n### Union fields\n\nYou can define a field that can be one of several types by using a union type:\n\n```ts\ninterface Request {\n  value: string | number | boolean;\n}\n\nexport const myEndpoint = api(\n  { expose: true, method: \"POST\", path: \"/body\" },\n  async (req: Request) => {\n    // req.value is a string, number, or boolean\n  },\n);\n```\n\n### Reference schema\n\n```ts\ninterface Schema {\n  str: string; // String\n  int: number; // Number\n  list: number[]; // Array of numbers\n  listOfTypes: (number | string )[]; // Array multiple types\n  nullable: number | null; // Nullable\n  maybe?: string; // Optional\n  multiple: boolean | number | string | { name: string }; // Union\n  enum: \"John\" | \"Foo\"; // Enum\n}\n```\n\n## Value based validation rules\n\nUse Encore.ts's composable value based validation for use cases like checking the format of an email address or the length of a string.\nIt uses TypeScript's type system and allows you to define validation rules directly in your type definitions.\n\n### Built-in validation Rules\n- `Min<N>` / `Max<N>`: Validate minimum/maximum values for numbers\n- `MinLen<N>` / `MaxLen<N>`: Validate minimum/maximum lengths for strings and arrays\n- `IsURL` / `IsEmail`: Validate that a string is a URL or email address\n- `StartsWith` / `EndsWith` / `MatchesRegexp`: Validate that a string matches a specific pattern\n\n\n### Examples\n\nImport validation rules from the `encore.dev/validate` package:\n\n```ts\nimport { Min, Max, MinLen, MaxLen, IsEmail, IsURL } from \"encore.dev/validate\";\n\ninterface Schema {\n  // Number between 3 and 1000 (inclusive)\n  count: number & (Min<3> & Max<1000>);\n\n  // String between 5 and 20 characters\n  username: string & (MinLen<5> & MaxLen<20>);\n\n  // Must be either a valid URL or email address\n  contact: string & (IsURL | IsEmail);\n\n  // Array of up to 10 email addresses\n  recipients: Array<string & IsEmail> & MaxLen<10>;\n}\n```\n\n### Combining Rules\n\nYou can combine multiple validation rules using:\n- `&` (and) to require all rules to pass\n- `|` (or) to require at least one rule to pass\n\n```ts\ninterface Schema {\n  // Must be both >= 3 and <= 1000\n  count: number & (Min<3> & Max<1000>);\n\n  // Must be either a URL or an email\n  contact: string & (IsURL | IsEmail);\n}\n```\n\n### Performance\n\nThese validation rules are executed directly in Rust at runtime, before the request reaches your JavaScript code. This provides excellent performance while maintaining type safety at both compile-time and runtime.\n\n## Body\n\nBy default, the data is parsed as a JSON body for incoming requests:\n\n```ts\ninterface Request {\n  name: string; // Parsed from the JSON body\n}\n\nexport const myEndpoint = api<Request, Response>(\n  { expose: true, method: \"POST\", path: \"/body\" },\n  async (req) => {\n    // req.name is a string\n  },\n);\n```\n\nHere, `name` is a required field in the request body. If the request body is missing the `name` field, Encore will return a `400` Bad Request response.\n\n## Query\n\nFor HTTP methods that support request bodies, parameters are by default read from the HTTP request body as JSON. In those cases, the `Query` type can be used to specify that a field should be parsed from the query string instead.\n\n```typescript\nimport { api, Query } from \"encore.dev/api\";\n\ninterface Schema {\n  query: Query<string>; // this will be parsed from the '?query=...' parameter in the request url\n}\n// A simple API endpoint that echoes the data back.\nexport const echo = api(\n  { method: \"POST\", path: \"/example\" },\n  async (params: Schema) => {\n    // params.query is a string\n  },\n);\n```\n\nThis API endpoint expects incoming requests to look like this:\n```output\nPOST /example?query=hello HTTP/1.1\nContent-Type: application/json\n```\n\nFor `GET`, `HEAD` and `DELETE` requests, parameters are read and validated from the query string by default, since those HTTP methods do not support request bodies. For those methods, the `Query` type is not necessary:\n\n```typescript\nimport { Query } from \"encore.dev/api\";\n\ninterface Schema {\n  limit: Query<number>; // always a query parameter\n  author: string; // query if GET, HEAD or DELETE, otherwise body parameter\n}\n```\n\n### Nested query fields\n\nUsing the `Query` type as a nested fields has no effect:\n\n```typescript\nimport { api, Query } from \"encore.dev/api\";\n\ninterface Data {\n  query: Query<string>; // this will be parsed from the '?query=...' parameter in the request url\n  nested: {\n    query2: Query<string>; // Query has no effect inside nested fields\n  };\n}\n\nexport const echo = api(\n  { method: \"POST\", path: \"/nested\" },\n  async (params: Data) => {\n    // ...\n  },\n);\n```\n\nNested query params will be sent as part of the JSON body. The above endpoint expects incoming requests to look like this:\n\n```output\nPOST /nested HTTP/1.1\nContent-Type: application/json\n{\n   \"nested\": {\n      \"query2\": \"not a query string\"\n   }\n}\n```\n\n## Headers\n\nRequest headers are defined and validated by setting the field type to `Header<\"Name-Of-Header\">`. It can be used in both request and response data types.\n\nIn the example below, the `language` field will be fetched from the `Accept-Language` HTTP header. If the request is missing the `Accept-Language` header, Encore will return a `400` Bad Request response.\n\n```ts\nimport { Header } from \"encore.dev/api\";\n\ninterface Params {\n  language: Header<\"Accept-Language\">; // parsed from header\n  author: string; // not a header\n}\n```\n\n### Nested header fields\n\nUsing the `Header` type as a nested fields has no effect:\n\n```typescript\nimport { api, Header } from \"encore.dev/api\";\n\ninterface Data {\n  header: Header<\"X-Header\">; // this field will be read from the http header\n  nested: {\n    header2: Header<\"X-Other-Header\">; // Header has no effect inside nested fields\n  };\n}\n\n// A simple API endpoint that echoes the data back.\nexport const echo = api(\n  { method: \"POST\", path: \"/nested\" },\n  async (params: Data) => {\n    // ...\n  },\n);\n```\n\nNested headers will be sent as part of the JSON body. The above endpoint expects incoming requests to look like this:\n\n```output\nPOST /nested HTTP/1.1\nContent-Type: application/json\nX-Header: this is a header\n{\n   \"nested\": {\n      \"header2\": \"not a header\",\n   }\n}\n```\n\n## Params\n\nDynamic path parameters are also defined in the request schema. The parameter will be parsed from the request URL and made available in the request object:\n\n```ts\nimport { api } from \"encore.dev/api\";\n\ninterface Request {\n  // Required path parameter. Parsed from the request URL.\n  id: string;\n}\n\nexport const myEndpoint = api(\n  { expose: true, method: \"POST\", path: \"/user/:id\" },\n  async ({ id }: Request) => {\n    // ...\n  },\n);\n```\n\nYou can also use the `number` type for path parameters:\n\n```ts\ninterface Request {\n  id: number;\n}\n```\n\nEncore.ts will then try to parse the path parameter as a number. If the path parameter is not a valid number, Encore will return a `400` Bad Request response.\n\n## Combining sources\n\nYou can combine data from different sources in the same request schema. For example, you can have fields that are parsed from the request body, others from the query parameters, and yet others from the HTTP headers. It looks like this:\n\n```ts\nimport { Header, Query, api } from \"encore.dev/api\";\n\ninterface Request {\n  // Required path parameter. Parsed from the request URL.\n  id: number;\n\n  // Optional query parameter. Parsed from the request URL.\n  limit?: Query<number>;\n\n  // Custom header that must be set. Parsed from the HTTP headers.\n  myHeader: Header<\"X-My-Header\">;\n\n  // Required enum. Parsed from the request body.\n  type: \"sprocket\" | \"widget\";\n}\n\nexport const myEndpoint = api(\n  { expose: true, method: \"POST\", path: \"/user/:id\" },\n  async ({ id, limit, myHeader, type }: Request) => {\n    // ...\n  },\n);\n```\n\n## Errors\n\nIf the validation is not successful, Encore will return a `400` Bad Request response with a JSON body that contains the error message:\n\n```output\nHTTP/1.1 400 Bad Request\n\n{\n  \"code\": \"invalid_argument\",\n  \"message\": \"unable to decode request body\",\n  \"internal_message\": \"Error(\\\"missing field name\\\", line: 1, column: 18)\"\n}\n```\n\n\n## Response\n\nEncore.ts will not perform runtime validation for response data, but you will get compilation errors if you try to return a value that does not match the expected response type.\n\n### Reusing the request type as the response type\nYou often want to return the same data type that you received in a request. In this case, you can reuse the request type as the response type:\n\n```typescript\nimport { api, Header, Query } from \"encore.dev/api\";\n\ninterface Data {\n  header: Header<\"X-Header\">; // this field will be read from the http header\n  query: Query<string>; // this will be parsed from the '?query=...' parameter in the request url\n  body: string; // this will be sent as part of the JSON body\n}\n// A simple API endpoint that echoes the data back.\nexport const echo = api(\n  { method: \"POST\", path: \"/echo\" },\n  async (params: Data): Promise<Data> => {\n    return params; // echo the data back\n  },\n);\n```\n\nThis API endpoint expects incoming requests to look like this:\n```output\nPOST /echo?query=hello HTTP/1.1\nContent-Type: application/json\nX-Header: this is a header\n{\n   \"body\": \"a body\",\n}\n```\n\nFor HTTP responses the `Query<string>` type is considered to be part of the JSON response body, since query strings only make sense for incoming requests.\nResponses returned from this endpoint will be serialized as a HTTP response to looks like this:\n\n```output\nHTTP/1.1 200 OK\nContent-Type: application/json\nX-Header: this is a header\n{\n   \"query\": \"hello\",\n   \"body\": \"a body\",\n}\n```\n\n## Under the hood\n\nEncore.ts parses your source code to understand the request and response schema that each API endpoint expects, including things like HTTP headers, query parameters, and so on. The schemas are then processed, optimized, and stored as a Protobuf file.\n\nWhen the Encore.ts Rust runtime starts up, it reads the Protobuf file and pre-computes a request decoder and response encoder, optimized for each API endpoint, using the exact type definition each API endpoint expects. In fact, Encore.ts even handles request validation directly in Rust, ensuring invalid requests never have to even touch the JavaScript layer, mitigating many denial of service attacks.\n\nEncore’s understanding of the request schema also improves performance. JavaScript runtimes like Deno and Bun use a similar architecture (in fact, Deno also uses Rust+Tokio+Hyper), but lack Encore’s understanding of the request schema. As a result, they need to hand over the un-processed HTTP requests to the single-threaded JavaScript engine for execution.\n\nEncore.ts, on the other hand, handles much more of the request processing inside Rust, and only hands over the decoded request objects. By handling much more of the request life-cycle in multi-threaded Rust, the JavaScript event-loop is freed up to focus on executing application business logic instead of parsing HTTP requests, resulting in a significant performance improvement.\n"
  },
  {
    "path": "docs/ts/quick-start.mdx",
    "content": "---\nseotitle: Quick Start Guide – Learn how to build backends with Encore.ts\nseodesc: See how you to build and ship a cloud based backend application using Go and Encore. Install Encore and build a REST API in just a few minutes.\ntitle: Quick Start Guide\nsubtitle: Build your first Encore.ts app in 5 minutes\nlang: ts\n---\n\nFollow the steps below or use [Leap](https://leap.new) (our AI builder) to get started.\n\n<TryWithLeap />\n\nIn this short guide, you'll learn key concepts and experience the Encore workflow.\nIt should only take about 5 minutes to complete and by the end you'll have an API running in Encore's free development Cloud (Encore Cloud).\n\nTo make it easy to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n## 1. Install the Encore CLI\n\nTo develop with Encore, you need the Encore CLI. It provisions your local environment, and runs your local development dashboard complete with tracing and API documentation.\n\n🥐 Install Encore by running the appropriate command for your system:\n\n<InstallInstructions />\n\n### Prerequisites\n\n- [Node.js](https://nodejs.org/en/download/) is required to run Encore.ts apps.\n- [Docker](https://www.docker.com) is required for Encore to set up local databases.\n\n## 2. Create your app\n\n🥐 Create your app by running:\n\n```shell\n$ encore app create\n```\n\nIf this is your first time using Encore, you’ll be prompted to create a free Encore Cloud account.\nThis enables Encore to manage things like secrets and fully automate cloud deployments (which you’ll use later in the tutorial).\n\n🥐 Select `TypeScript` as your app’s language.\n\n🥐 Choose a starter template. Pick `Hello World` and continue.\n\nOptional: Install AI instructions to improve how tools like Cursor and Claude Code work with Encore. After selecting your template, choose the AI instructions for the tool you plan to use.\n\n🥐 Pick a name for your app.\n\nEncore will now create your app in a folder named after your app.\n\n### Let's take a look at the code\n\nPart of what makes Encore different is the simple developer experience when building distributed systems.\nLet's look at the code to better understand how to build applications with Encore.\n\n🥐 Open the `hello.ts` file in your code editor. It's located in the folder: `your-app-name/hello/`.\n\nYou should see this:\n\n```ts\n-- hello/hello.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const world = api(\n  { method: \"GET\", path: \"/hello/:name\", expose: true },\n  async ({ name }: { name: string }): Promise<Response> => {\n    return { message: `Hello ${name}!` };\n  }\n);\n\ninterface Response {\n  message: string;\n}\n```\n\nAs you can see, it's all standard TypeScript.\n\nYou define an API endpoint by wrapping a regular async function in a call to `api`. Doing this makes Encore identify the `world` function as a public API endpoint. Encore automatically handles authentication, HTTP routing, request validation, error handling, observability, API documentation, and more.\n\nThe `world` endpoint is part of the `hello` service because in the same folder you will also find a file named `encore.service.ts` which looks like this:\n\n```ts\n-- hello/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"hello\");\n```\n\nThis is how you define services with Encore. Encore will now consider files in the `hello` directory and all its subdirectories as part of the `hello` service. If you want to create more services, simply create a new folders, add a `encore.service.ts` file that is exporting a new `Service`. _If you're curious, you can read more about defining [services](/docs/ts/primitives/services) and [APIs](/docs/ts/primitives/apis)._\n\nThe Encore.ts [Backend Framework](/docs/ts) provides several declarative ways of using backend primitives like databases, Pub/Sub, and scheduled tasks by simply writing code.\n\n## 3. Start your app & Explore Local Development Dashboard\n\n🥐 Now let's run your app locally:\n\n```shell\n$ cd your-app-name # replace with the app name you picked\n$ encore run\n```\n\nYou should see this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/encorerun.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nThat means your local development environment is up and running!\nEncore takes care of setting up all the necessary infrastructure for your applications, even including databases and Pub/Sub.\n\n### Open the Local Development Dashboard\n\nYou can now start using your [Local Development Dashboard](/docs/ts/observability/dev-dash).\n\n🥐 Open [http://localhost:9400](http://localhost:9400) in your browser to access it.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/localdashvideo.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\nThe Local Development Dashboard is a powerful tool to help you move faster when you're developing new features.\n\nIt comes with an API explorer, a Service Catalog with automatically generated documentation, and powerful observability features\nlike [distributed tracing](/docs/ts/observability/tracing).\n\nThrough the Local Development Dashboard you also have access to [Encore Flow](/docs/ts/observability/encore-flow),\na visual representation of your microservice architecture that updates in real-time as you develop your application.\n\n### Call your API\n\n🥐 While you keep the app running, call your API from the API Explorer:\n\n<img\n  className=\"mx-auto w-full\"\n  src=\"/assets/docs/qs_call.png\"\n  title=\"Call API from Local Dashboard\"\n/>\n\nYou can also open a separate terminal to call your API endpoint:\n\n```shell\n$ curl http://localhost:4000/hello/world\n{\"Message\": \"Hello, world!\"}\n```\n\nIf you see this JSON response, you've successfully made an API call to your very first Encore application. Well done, you're on your way!\n\n### Review a trace of the request\n\nYou can now take a look at the trace for the request you just made by clicking on it in the right column in the local dashboard.\n\n<img\n  className=\"mx-auto w-full\"\n  src=\"/assets/docs/qs_trace.png\"\n  title=\"Tracing in the Local Dashboard\"\n/>\n\nWith such a simple API, there's not much to it, just a simple request and response.\n\nHowever, just imagine how powerful it is to have tracing when you're developing a more complex system with multiple services, Pub/Sub, and databases.\n(Learn more about Encore's tracing capabilities in the [tracing docs](/docs/ts/observability/tracing).)\n\n## 4. Make a code change\n\nLet's put our mark on this API and make our first code change.\n\n🥐 Head back to your code editor and look at the `hello.ts` file again.\nIf you can't come up a creative change yourself, why not simply change the \"Hello\" message to a more sassy \"Howdy\"?\n\n🥐 Once you've made your change, save the file.\n\nWhen you save, the daemon run by the Encore CLI instantly detects the change and automatically recompiles your application and reloads your local development environment.\n\nThe output where you're running your app will look something like this:\n\n```output\nChanges detected, recompiling...\nReloaded successfully.\nTRC registered endpoint endpoint=World path=/hello/:name service=hello\nTRC listening for incoming HTTP requests\n```\n\n🥐 Test your change by calling your API again.\n\n```shell\n$ curl http://localhost:4000/hello/world\n{\"Message\": \"Howdy, world!\"}\n```\n\nGreat job, you made a change and your app was reloaded automatically.\n\nNow you're ready to head to the cloud!\n\n## 5. Deploy your app\n\n### Generating Docker image\n\nYou can either deploy by generating a Docker image for your app using:\n\n```shell\n$ encore build docker MY-IMAGE:TAG\n```\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\nYou can now deploy this anywhere you like. Learn more in the [self-host docs](/docs/ts/self-host/build).\n\n### Deploy using Encore Cloud\n\nOptionally, you can use [Encore Cloud](https://encore.dev/use-cases/devops-automation) to automatically deploy your application.\nIt comes with built-in free development hosting, and for production offers fully automated deployment to your own cloud on AWS or GCP.\n\n🥐 To deploy, simply push your changes to Encore:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore Cloud will now build and test your app, provision the needed infrastructure, and deploy your application to a staging environment.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the Encore Cloud dashboard.\nIt will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\n🥐 Open the URL to access the Cloud Dashboard and check the progress of your deployment.\n\nYou can now use the Cloud Dashboard to view production [traces](/docs/ts/observability/tracing), [connect your cloud account](/docs/platform/deploy/own-cloud), [integrate with GitHub](/docs/platform/integrations/github), and much more.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source\n    src=\"/assets/docs/webdashvideo.mp4\"\n    className=\"w-full h-full\"\n    type=\"video/mp4\"\n  />\n</video>\n\n## What's next?\n\n- Check out the [REST API tutorial](/docs/ts/tutorials/rest-api) to learn how to create endpoints, use databases, and more.\n- Join the friendly community on [Discord](/discord) to ask questions and meet other Encore developers.\n"
  },
  {
    "path": "docs/ts/self-host/build.md",
    "content": "---\nseotitle: Build Docker Images\nseodesc: Learn how to build Docker images for your Encore application, which can be self-hosted on your own infrastructure.\ntitle: Build Docker Images\nlang: ts\n---\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nThis can be a good choice if [Encore Cloud](/docs/platform) isn't a good fit for your use case, or if you want to [migrate away](/docs/ts/migration/migrate-away).\n\n## Building your own Docker image\n\nTo build your own Docker image, use `encore build docker MY-IMAGE:TAG` from the CLI.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application. The base image defaults to `scratch` for GO apps and `node:slim` for TS, but can be customized with `--base`.\n\nThis is exactly the same code path that Encore's CI system uses to build Docker images, ensuring compatibility.\n\nBy default, all your services are included and started by the Docker image. If you want to specify specific services and gateways to include, you can use the `--services` and `--gateways` flags.\n\n```bash\nencore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG\n```\n\nYou can target a specific architecture with `--arch` (useful when your build machine differs from your deploy target):\n\n```bash\nencore build docker --arch=arm64 MY-IMAGE:TAG\n```\n\nTo provide an [infrastructure configuration](/docs/ts/self-host/configure-infra) file at build time, use `--config`:\n\n```bash\nencore build docker --config=infra-config.json MY-IMAGE:TAG\n```\n\nThe image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image.\n\n```bash\ndocker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG\n```\n\nCongratulations, you've built your own Docker image! 🎉\nContinue to learn how to [configure infrastructure](/docs/ts/self-host/configure-infra).\n"
  },
  {
    "path": "docs/ts/self-host/ci-cd.md",
    "content": "---\nseotitle: Integrate with your CI/CD pipeline\nseodesc: Learn how to integrate Encore.ts with your CI/CD pipeline.\ntitle: Integrate with your CI/CD pipeline\nlang: ts\n---\n\nEncore seamlessly integrates with any CI/CD pipeline through its CLI tools. You can automate Docker image creation using the `encore build` command as part of your deployment workflow.\n\n## Integrating with CI/CD Platforms\n\nWhile every CI/CD pipeline is unique, integrating Encore follows a straightforward process. Here are the key steps:\n\n1. Install the Encore CLI in your CI environment\n2. Use `encore build docker` to create Docker images\n3. Push the images to your container registry\n4. Deploy to your infrastructure\n\nIf your app is linked with Encore Cloud, you'll need to authenticate the CLI in your CI environment using an [auth key](/docs/platform/integrations/auth-keys). Generate one from **App Settings > Auth Keys** in the Encore Cloud dashboard, store it as a CI secret, and run `encore auth login --auth-key=<KEY>` before building.\n\nRefer to your CI/CD platform's documentation for more details on how to integrate CLI tools like `encore build`.\n\n### GitHub actions example\n\nThis example shows how to build, push, and deploy an Encore Docker image to DigitalOcean using GitHub Actions.\nThe DigitalOcean application is set up re-deploy the application every time an image with the tag `latest` is uploaded. \n\n```yaml\nname: Build, Push and Deploy a Encore Docker Image to DigitalOcean\n\non:\n  push:\n    branches: [ main ]\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  build-push-deploy-image:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Download Encore CLI script\n        uses: sozo-design/curl@v1.0.2\n        with:\n          args: --output install.sh -L https://encore.dev/install.sh\n\n      - name: Install Encore CLI\n        run: bash install.sh\n\n      - name: Authenticate with Encore\n        run: /home/runner/.encore/bin/encore auth login --auth-key=${{ secrets.ENCORE_AUTH_KEY }}\n\n      - name: Log in to DigitalOcean container registry\n        run: docker login registry.digitalocean.com -u my-email@gmail.com -p ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}\n\n      - name: Build Docker image\n        run: /home/runner/.encore/bin/encore build docker myapp\n\n      - name: Tag Docker image\n        run: docker tag myapp registry.digitalocean.com/<YOUR_CONTAINER_REGISTRY_NAME>/<YOUR_IMAGE_REPOSITORY_NAME>:latest\n\n      - name: Push Docker image\n        run: docker push registry.digitalocean.com/<YOUR_CONTAINER_REGISTRY_NAME>/<YOUR_IMAGE_REPOSITORY_NAME>:latest\n```\n\n## Building Docker Images\n\nThe `encore build docker` command provides several options to customize your builds:\n\n```bash\n# Build specific services and gateways\nencore build docker --services=service1,service2 --gateways=api-gateway MY-IMAGE:TAG\n\n# Customize the base image\nencore build docker --base=node:18-alpine MY-IMAGE:TAG\n\n# Build for a specific architecture (useful when CI and deploy targets differ)\nencore build docker --arch=arm64 MY-IMAGE:TAG\n```\n\nThe image will default to run on port 8080, but you can customize it by setting the `PORT` environment variable when starting your image.\n\n```bash\ndocker run -e PORT=8081 -p 8081:8081 MY-IMAGE:TAG\n```\n\nLearn more about the `encore build docker` command in the [build Docker images](/docs/ts/self-host/build) guide.\n\nContinue to learn how to [configure infrastructure](/docs/ts/self-host/configure-infra).\n"
  },
  {
    "path": "docs/ts/self-host/configure-infra.md",
    "content": "---\ntitle: Configure Infrastructure\nseotitle: Configure Infrastructure\nseodesc: Learn how to configure infrastructure resources for your Encore app.\nlang: ts\n---\n\nIf you are using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration.\nThe `build` command lets you provide this by specifying a path to a config file using the `--config` flag.\n\n```bash\nencore build docker --config path/to/infra-config.json MY-IMAGE:TAG\n```\n\nThe configuration file should be a JSON file using the [Encore Infra Config](https://encore.dev/schemas/infra.schema.json) schema.\n\nThis supports configuring things like:\n\n- How to access infrastructure resources (what provider to use, what credentials to use, etc.)\n- How to call other services over the network (\"service discovery\"),\n  most notably their base URLs.\n- Observability configuration (where to export metrics, etc.)\n- Metadata about the environment the application is running in, to power Encore's metadata APIs.\n- The values for any application-defined secrets.\n\nThis configuration is necessary for the application to behave correctly.\n\n## Example\n\nHere's an example configuration file you can use.\n\n```json\n{\n  \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n  \"metadata\": {\n    \"app_id\": \"my-app\",\n    \"env_name\": \"my-env\",\n    \"env_type\": \"production\",\n    \"cloud\": \"gcp\",\n    \"base_url\": \"https://my-app.com\"\n  },\n  \"sql_servers\": [\n    {\n      \"host\": \"my-db-host:5432\",\n      \"databases\": {\n        \"my-db\": {\n          \"username\": \"my-db-owner\",\n          \"password\": {\"$env\": \"DB_PASSWORD\"}\n        }\n      }\n    }\n  ],\n  \"service_discovery\": {\n    \"myservice\": {\n      \"base_url\": \"https://myservice:8044\"\n    }\n  },\n  \"redis\": {\n    \"encoreredis\": {\n      \"database_index\": 0,\n      \"auth\": {\n        \"type\": \"acl\",\n        \"username\": \"encoreredis\",\n        \"password\": {\"$env\": \"REDIS_PASSWORD\"}\n      },\n      \"host\": \"my-redis-host\",\n    }\n  },\n  \"metrics\": {\n    \"type\": \"prometheus\",\n    \"remote_write_url\": \"https://my-remote-write-url\"\n  },\n  \"graceful_shutdown\": {\n    \"total\": 30\n  },\n  \"auth\": [\n    {\n      \"type\": \"key\",\n      \"id\": 1,\n      \"key\": {\"$env\": \"SVC_TO_SVC_KEY\"}\n    }\n  ],\n  \"secrets\": {\n    \"AppSecret\": {\"$env\": \"APP_SECRET\"}\n  },\n  \"pubsub\": [\n    {\n      \"type\": \"gcp_pubsub\",\n      \"project_id\": \"my-project\",\n      \"topics\": {\n        \"encore-topic\": {\n          \"name\": \"gcp-topic-name\",\n          \"subscriptions\": {\n            \"encore-subscription\": {\n              \"name\": \"gcp-subscription-name\"\n            }\n          }\n        }\n      }\n    }\n  ],\n  \"object_storage\": [\n    {\n      \"type\": \"gcs\",\n      \"buckets\": {\n          \"my-gcs-bucket\": {\n            \"name\": \"my-gcs-bucket\",\n          }\n        }\n    }\n  ]\n}\n```\n\n## Configuring Infrastructure\nTo use infrastructure resources, additional configuration must be added so that Encore is aware of how to access each infrastructure resource.\nSee below for examples of each type of infrastructure resource.\n\n### 1. Basic Environment Metadata Configuration\n\n```json\n{\n  \"metadata\": {\n    \"app_id\": \"my-encore-app\",\n    \"env_name\": \"production\",\n    \"env_type\": \"production\",\n    \"cloud\": \"aws\",\n    \"base_url\": \"https://api.myencoreapp.com\"\n  }\n}\n```\n\n- `app_id`: The ID of your Encore application.\n- `env_name`: The environment name, such as `production`, `staging`, or `development`.\n- `env_type`: Specifies the type of environment (`production`, `test`, `development`, or `ephemeral`).\n- `cloud`: The cloud provider hosting the infrastructure (e.g., `aws`, `gcp`, or `azure`).\n- `base_url`: The base URL for services in the environment.\n\n### 2. Graceful Shutdown Configuration\n\n```json\n{\n  \"graceful_shutdown\": {\n    \"total\": 30,\n    \"shutdown_hooks\": 10,\n    \"handlers\": 20\n  }\n}\n```\n\n- `total`: The total time allowed for the shutdown process in seconds.\n- `shutdown_hooks`: The time allowed for executing shutdown hooks.\n- `handlers`: The time allocated for processing request handlers during the shutdown.\n\n### 3. Authentication Methods Configuration\nPrivate endpoints will not require authentication if no authentication methods are specified. This is typically fine when services are deployed on a private network such as a VPC. But sometimes you might need to connect to other services over the public internet, in which case you'll want to ensure private endpoints are only accessible to other backend services. To do that you can configure authentication methods.\nEncore currently supports authentication through a shared key, which you can specify in your infrastructure configuration file.\n```json\n{\n  \"auth\": [\n    {\n      \"type\": \"key\",\n      \"id\": 1,\n      \"key\": {\n        \"$env\": \"SERVICE_API_KEY\"\n      }\n    }\n  ]\n}\n```\n\n- `type`: The authentication method type (e.g., `key`).\n- `id`: The ID associated with the authentication method.\n- `key`: The authentication key, which can be set using an environment variable reference.\n\n### 4. Service Discovery Configuration\nService discovery is used to access other services over the network. You can configure service discovery in the infrastructure configuration file.\nIf you export all services into the same docker image, you don't need to configure service discovery as it will be automatically\nconfigured when the services are started.\n\n```json\n{\n  \"service_discovery\": {\n    \"user-service\": {\n      \"base_url\": \"https://user.myencoreapp.com\",\n      \"auth\": [\n        {\n          \"type\": \"key\",\n          \"id\": 1,\n          \"key\": {\n            \"$env\": \"USER_SERVICE_API_KEY\"\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\n- `user-service`: Configuration for a service named `user-service`.\n- `base_url`: The base URL for the service.\n- `auth`: Authentication methods used for accessing the service. If no authentication methods are specified, the service will use the auth methods defined in the `auth` section.\n\n### 5. Metrics Configuration\nSimilarly to cloud infrastructure resources, Encore supports configurable metrics exports:\n\n* Prometheus\n* DataDog\n* GCP Cloud Monitoring\n* AWS CloudWatch\n\nThis is configured by setting the metrics field. Below are examples for each of the supported metrics providers:\n#### 5.1. Prometheus Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"prometheus\",\n    \"collection_interval\": 15,\n    \"remote_write_url\": {\n      \"$env\": \"PROMETHEUS_REMOTE_WRITE_URL\"\n    }\n  }\n}\n```\n\n#### 5.2. Datadog Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"datadog\",\n    \"collection_interval\": 30,\n    \"site\": \"datadoghq.com\",\n    \"api_key\": {\n      \"$env\": \"DATADOG_API_KEY\"\n    }\n  }\n}\n```\n\n#### 5.3. GCP Cloud Monitoring Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"gcp_cloud_monitoring\",\n    \"collection_interval\": 60,\n    \"project_id\": \"my-gcp-project\",\n    \"monitored_resource_type\": \"gce_instance\",\n    \"monitored_resource_labels\": {\n      \"instance_id\": \"1234567890\",\n      \"zone\": \"us-central1-a\"\n    },\n    \"metric_names\": {\n      \"cpu_usage\": \"compute.googleapis.com/instance/cpu/usage_time\"\n    }\n  }\n}\n```\n\n#### 5.4. AWS CloudWatch Configuration\n\n```json\n{\n  \"metrics\": {\n    \"type\": \"aws_cloudwatch\",\n    \"collection_interval\": 60,\n    \"namespace\": \"MyAppMetrics\"\n  }\n}\n```\n\n### 6. SQL Database Configuration\nThe SQL databases you've declared in your Encore app must be configured in the infrastructure configuration file.\nThere must be exactly one database configuration for each declared database. You can configure multiple SQL servers if needed.\n\n```json\n{\n  \"sql_servers\": [\n    {\n      \"host\": \"db.myencoreapp.com:5432\",\n      \"tls_config\": {\n        \"disabled\": false,\n        \"ca\": \"---BEGIN CERTIFICATE---\\n...\"\n      },\n      \"databases\": {\n        \"main_db\": {\n          \"max_connections\": 100,\n          \"min_connections\": 10,\n          \"username\": \"db_user\",\n          \"password\": {\n            \"$env\": \"DB_PASSWORD\"\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n- `host`: SQL server host, optionally including the port.\n- `tls_config`: TLS configuration for secure connections. If the server uses TLS with a non-system CA root, or requires a client certificate, specify the appropriate fields as PEM-encoded strings. Otherwise, they can be left empty.\n- `databases`: List of databases, each with connection settings.\n\n### 7. Secrets Configuration\n\n#### 7.1. Using Direct Secrets\nYou can set the secret value directly in the configuration file, or use an environment variable reference to set the secret value.\n\n```json\n{\n  \"secrets\": {\n    \"API_TOKEN\": \"embedded-secret-value\",\n    \"DB_PASSWORD\": {\n      \"$env\": \"DB_PASSWORD\"\n    }\n  }\n}\n```\n\n#### 7.2. Using Environment Reference\nAs an alternative, you can use an environment variable reference to set the secret value. The env variable should be set in the environment where the application is running. The content\nof the environment variable should be a JSON string where each key is the secret name and the value is the secret value.\n\n```json\n{\n  \"secrets\": {\n    \"$env\": \"SECRET_JSON\"\n  }\n}\n```\n\n### 8. Redis Configuration\n\n```json\n{\n  \"redis\": {\n    \"cache\": {\n      \"host\": \"redis.myencoreapp.com:6379\",\n      \"database_index\": 0,\n      \"auth\": {\n        \"type\": \"auth\",\n        \"auth_string\": {\n          \"$env\": \"REDIS_AUTH_STRING\"\n        }\n      },\n      \"max_connections\": 50,\n      \"min_connections\": 5\n    }\n  }\n}\n```\n\n- `host`: Redis server host, optionally including the port.\n- `auth`: Authentication configuration for the Redis server.\n- `key_prefix`: Prefix applied to all keys.\n\n### 9. Pub/Sub Configuration\nEncore currently supports the following Pub/Sub providers:\n- `nsq` for [NSQ](https://nsq.io/)\n- `gcp` for [Google Cloud Pub/Sub](https://cloud.google.com/pubsub)\n- `aws` for AWS [SNS](https://aws.amazon.com/sns/) + [SQS](https://aws.amazon.com/sqs/)\n- `azure` for [Azure Service Bus](https://azure.microsoft.com/en-us/products/service-bus)\n\nThe configuration for each provider is different. Below are examples for each provider.\n#### 9.1. GCP Pub/Sub\n\n```json\n{\n  \"pubsub\": [\n    {\n      \"type\": \"gcp_pubsub\",\n      \"project_id\": \"my-gcp-project\",\n      \"topics\": {\n        \"user-events\": {\n          \"name\": \"user-events-topic\",\n          \"project_id\": \"my-gcp-project\",\n          \"subscriptions\": {\n            \"user-notification\": {\n              \"name\": \"user-notification-subscription\",\n              \"push_config\": {\n                \"id\": \"user-push\",\n                \"service_account\": \"service-account@my-gcp-project.iam.gserviceaccount.com\"\n              }\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n#### 9.2. AWS SNS/SQS\n\n```json\n{\n  \"pubsub\": [\n    {\n      \"type\": \"aws_sns_sqs\",\n      \"topics\": {\n        \"my-topic\": {\n          \"arn\": \"arn:aws:sns:us-east-1:123456789012:my-topic\",\n          \"subscriptions\": {\n            \"my-queue\": {\n              \"url\": \"https://sqs.eu-east-1.amazonaws.com/123456789012/my-queue\"\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n- `my-topic`: This is the name of the topic as it is declared in your Encore app.\n- `my-queue`: This is the name of the queue as it is declared in your Encore app.\n- `arn`: The ARN of the SNS topic.\n- `url`: The URL of the SQS queue.\n\n#### 9.3. NSQ Configuration\n\n```json\n{\n  \"pubsub\": [\n    {\n      \"type\": \"nsq\",\n      \"hosts\": \"nsq.myencoreapp.com:4150\",\n      \"topics\": {\n        \"order-events\": {\n          \"name\": \"order-events-topic\",\n          \"subscriptions\": {\n            \"order-processor\": {\n              \"name\": \"order-processor-subscription\"\n            }\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n### 10. Object Storage Configuration\nEncore currently supports the following object storage providers:\n- `gcs` for [Google Cloud Storage](https://cloud.google.com/storage)\n- `s3` for [AWS S3](https://aws.amazon.com/s3/) or a custom S3-compatible provider\n\n#### 10.1. GCS Configuration\n\n```json\n{\n  \"object_storage\": [\n    {\n      \"type\": \"gcs\",\n      \"buckets\": {\n        \"my-gcs-bucket\": {\n          \"name\": \"my-gcs-bucket\",\n          \"key_prefix\": \"my-optional-prefix/\",\n          \"public_base_url\": \"https://my-gcs-bucket-cdn.example.com/my-optional-prefix\"\n        }\n      }\n    }\n  ]\n}\n```\n\n- `name`: The full name of the GCS bucket.\n- `key_prefix`: An optional prefix to apply to all keys in the bucket.\n- `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended.\n\n#### 10.2. S3 Configuration\n\n```json\n{\n  \"object_storage\": [\n    {\n      \"type\": \"s3\",\n      \"region\": \"us-east-1\",\n      \"buckets\": {\n        \"my-s3-bucket\": {\n          \"name\": \"my-s3-bucket\",\n          \"key_prefix\": \"my-optional-prefix/\",\n          \"public_base_url\": \"https://my-gcs-bucket-cdn.example.com/my-optional-prefix\"\n        }\n      }\n    }\n  ]\n}\n```\n\n- `region`: The AWS region where the bucket is located.\n- `name`: The full name of the S3 bucket.\n- `key_prefix`: An optional prefix to apply to all keys in the bucket.\n- `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended.\n\n#### 10.3. Custom S3 Provider Configuration\nYou can also configure a custom S3 provider by specifying the endpoint, access key id, and secret access key. Custom S3 providers are useful if you are using a S3-compatible storage provider such as [Cloudflare R2](https://developers.cloudflare.com/r2/).\n```json\n{\n  \"object_storage\": [\n    {\n      \"type\": \"s3\",\n      \"region\": \"auto\",\n      \"endpoint\": \"https://...\",\n      \"access_key_id\": \"...\",\n      \"secret_access_key\": {\n          \"$env\": \"BUCKET_SECRET_ACCESS_KEY\"\n      },\n      \"buckets\": {\n        \"my-s3-bucket\": {\n          \"name\": \"my-s3-bucket\",\n          \"key_prefix\": \"my-optional-prefix/\",\n          \"public_base_url\": \"https://my-gcs-bucket-cdn.example.com/my-optional-prefix\"          \n        }\n      }\n    }\n  ]\n}\n```\n\n- `region`: The region where the bucket is located.\n- `name`: The full name of the bucket\n- `key_prefix`: An optional prefix to apply to all keys in the bucket.\n- `public_base_url`: A URL to use for public access to the bucket. This field is required if you configure your bucket to be public. Encore will append the object key to this URL when generating public URLs. The optional prefix will not be appended.\n\nThis guide covers typical infrastructure configurations. Adjust according to your specific requirements to optimize your Encore app's infrastructure setup.\n"
  },
  {
    "path": "docs/ts/self-host/deploy-to-digital-ocean.md",
    "content": "---\nseotitle: How to deploy an Encore app to DigitalOcean\nseodesc: Learn how to deploy an Encore application to DigitalOcean's App Platform using Docker.\ntitle: Deploy to DigitalOcean\nlang: ts\n---\n\nIf you prefer manual deployment over the automation offered by Encore's Platform, Encore simplifies the process of deploying your app to the cloud provider of your choice. This guide will walk you through deploying an Encore app to DigitalOcean's App Platform using Docker.\n\n### Video tutorial\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/D3SjuCK_5qE?si=zLxEzG7dgTBlPkwU\" title=\"Deploying a TypeScript backend to DigitalOcean using Docker & GitHub Actions\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen></iframe>\n\n### Prerequisites\n1. **DigitalOcean Account**: Make sure you have a DigitalOcean account. If not, you can [sign up here](https://www.digitalocean.com/).\n2. **Docker Installed**: Ensure Docker is installed on your local machine. You can download it from the [Docker website](https://www.docker.com/get-started).\n3. **Encore CLI**: Install the Encore CLI if you haven’t already. You can follow the installation instructions from the [Encore documentation](https://encore.dev/docs/ts/install).\n4. **DigitalOcean CLI (Optional)**: You can install the DigitalOcean CLI for more flexibility and automation, but it’s not necessary for this tutorial.\n\n### Step 1: Create an Encore App\n1. **Create a New Encore App**: \n    - If you haven’t already, create a new Encore app using the Encore CLI.\n    - You can use the following command to create a new app:\n    ```bash\n    encore app create myapp\n    ``` \n    - Select the `Hello World` template.\n    - Follow the prompts to create the app.\n\n2. **Build a Docker image**:\n    - Build the Encore app to generate the docker image for deployment:\n    ```bash\n    encore build docker myapp  \n    ```\n### Step 2: Push the Docker Image to a Container Registry\nTo deploy your Docker image to DigitalOcean, you need to push it to a container registry. DigitalOcean supports\nits own container registry, but you can also use DockerHub or other registries. Here’s how to push the image to DigitalOcean’s registry:\n\n1. **Create a DigitalOcean Container Registry**:\n    - Go to the [DigitalOcean Control Panel](https://cloud.digitalocean.com/registries) and create a new container registry.\n    - Follow the instructions to set it up.\n\n2. **Login to DigitalOcean's registry**:\n   Use the login command provided by DigitalOcean, which will look something like this:\n   ```bash\n   doctl registry login\n   ```\n   You’ll need the DigitalOcean CLI for this, which can be installed from [DigitalOcean CLI documentation](https://docs.digitalocean.com/reference/doctl/how-to/install/).\n\n3. **Tag your Docker image**:\n   Tag your image to match the registry’s URL.\n   ```bash\n   docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   ```\n\n4. **Push your Docker image to the registry**:\n   ```bash\n   docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   ```\n\n### Step 3: Deploy the Docker Image to DigitalOcean App Platform\n1. **Navigate to the App Platform**:\n   Go to [DigitalOcean's App Platform](https://cloud.digitalocean.com/apps).\n\n2. **Create a New App**:\n    - Click on **\"Create App\"**.\n    - Choose the **\"DigitalOcean Container Registry\"** option.\n\n3. **Select the Docker Image Source**:\n    - Select the image you pushed earlier.\n\n4. **Configure the App Settings**:\n    - **Set up scaling options**: Configure the number of containers, CPU, and memory settings.\n    - **Environment variables**: Add any environment variables your application might need.\n    - **Choose the region**: Pick a region close to your users for better performance.\n\n5. **Deploy the App**:\n    - Click **\"Next\"**, review the settings, and click **\"Create Resources\"**.\n    - DigitalOcean will take care of provisioning the infrastructure, pulling the Docker image, and starting the application.\n\n### Step 4: Monitor and Manage the App\n1. **Access the Application**:\n    - Once deployed, you will get a public URL to access your application.\n    - Test the app to ensure it’s running as expected, e.g. \n   ```bash\n      curl https://myapp.ondigitalocean.app/hello/world\n    ```\n\n2. **View Logs and Metrics**:\n    - Go to the **\"Runtime Logs\"** tab in the App Platform to view logs\n    - Go to the **\"Insights\"** tab to view performance metrics.\n\n3. **Manage Scaling and Deployment Settings**:\n    - You can change the app configuration, such as scaling settings, deployment region, or environment variables.\n\n### Step 5: Add a Database to Your App\n\nDigitalOcean’s App Platform provides managed databases, allowing you to add a database to your app easily. Here’s how to set up a managed database for your app:\n\n1. **Navigate to the DigitalOcean Control Panel**:\n   - Go to [DigitalOcean Control Panel](https://cloud.digitalocean.com/).\n   - Click on **\"Databases\"** in the left-hand sidebar.\n\n2. **Create a New Database Cluster**:\n   - Click **\"Create Database Cluster\"**.\n   - Choose **PostgreSQL**\n   - Select the **database version**, **data center region**, and **cluster configuration** (e.g., development or production settings based on your needs).\n   - **Name the database** and configure other settings if necessary, then click **\"Create Database Cluster\"**.\n\n3. **Configure the Database Settings**:\n   - Once the database is created, go to the **\"Connection Details\"** tab of the database dashboard.\n   - Copy the **connection string** or individual settings (host, port, username, password, database name). You will need these details to connect your app to the database.\n   - Download the **CA certificate**\n\n4. **Create a Database**\n   - Connect to the database using the connection string provided by DigitalOcean.\n   ```bash\n   psql -h mydb.db.ondigitalocean.com -U doadmin -d mydb -p 25060\n   ```\n   - Create a database\n   ```sql\n    CREATE DATABASE mydb;\n    ```\n   - Create a table\n   ```sql\n     CREATE TABLE users (\n        id SERIAL PRIMARY KEY,\n        name VARCHAR(50)\n     );\n     INSERT INTO users (name) VALUES ('Alice');\n   ```\n   \n5. **Declare a Database in your Encore app**:\n   - Open your Encore app’s codebase.\n   - Add `mydb` database to your app ([Encore Database Documentation](https://encore.dev/docs/ts/primitives/databases))\n   ```typescript\n      const mydb = new SQLDatabase(\"mydb\", {\n         migrations: \"./migrations\",\n      });\n\n      export const getUser = api(\n        { expose: true, method: \"GET\", path: \"/names/:id\" },\n        async ({id}: {id:number}): Promise<{ id: number; name: string }> => {\n          return await mydb.queryRow`SELECT * FROM users WHERE id = ${id}` as { id: number; name: string };\n        }\n      );\n   ```\n\n6. **Create an Encore Infrastructure config**\n   - Create a file named `infra.config.json` in the root of your Encore app.\n   - Add the **CA certificate** and the connection details to the file:\n   ```json\n   {\n      \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n      \"sql_servers\": [\n      {\n         \"host\": \"mydb.db.ondigitalocean.com:25060\",\n         \"tls_config\": {\n            \"ca\": \"-----BEGIN CERTIFICATE-----\\n...\"\n         },\n         \"databases\": {\n            \"mydb\": {\n               \"name\": \"mydb\",\n               \"username\": \"doadmin\",\n               \"password\": {\"$env\": \"DB_PASSWORD\"}\n             }\n         }\n      }]   \n   }\n   ```\n\n7. **Set Up Environment Variables (Optional)**:\n   - Go to the DigitalOcean App Platform dashboard.\n   - Select your app.\n   - In the **\"Settings\"** section, go to **\"App-Level Environment Variables\"**\n   - Add the database password as an encrypted environment variable called `DB_PASSWORD`.\n\n8. **Build and push the Docker image**:\n   - Build the Docker image with the updated configuration.\n   ```bash\n   encore build docker --config infra.config.json myapp\n   ```\n   - Tag and push the Docker image to the DigitalOcean container registry.\n   ```bash\n   docker tag myapp registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   docker push registry.digitalocean.com/YOUR_REGISTRY_NAME/myapp:latest\n   ```\n\n9. **Test the Database Connection**:\n   - Redeploy the app on DigitalOcean to apply the changes.\n   - Test the database connection by calling the API\n   ```bash\n    curl https://myapp.ondigitalocean.app/names/1\n   ```\n\n### Troubleshooting Tips\n- **Deployment Failures**: Check the build logs for any errors. Make sure the Docker image is correctly tagged and pushed to the registry.\n- **App Not Accessible**: Verify that the correct port is exposed in the Dockerfile and the App Platform configuration.\n- **Database Connection Issues**: Ensure the database connection details are correct and the database is accessible from the app.\n\n### Conclusion\nThat’s it! You’ve successfully deployed an Encore app to DigitalOcean’s App Platform using Docker. You can now scale your app, monitor its performance, and manage it easily through the DigitalOcean dashboard. If you encounter any issues, refer to the DigitalOcean documentation or the Encore community for help. Happy coding!\n"
  },
  {
    "path": "docs/ts/self-host/deploy-to-railway.md",
    "content": "---\nseotitle: How to deploy an Encore app to Railway\nseodesc: Learn how to deploy an Encore application to Railway using Docker and GitHub Actions.\ntitle: Deploy to Railway\nlang: ts\n---\n\nIf you prefer manual deployment over the automation offered by Encore's Platform, Encore simplifies the process of deploying your app to the cloud provider of your choice. This guide will walk you through deploying an Encore app to Railway using Docker through GitHub Actions.\n\n### Prerequisites\n1. **Railway Account**: Make sure you have a Railway account. If not, you can [sign up here](https://railway.com/).\n2. **Docker Installed**: Ensure Docker is installed on your local machine, Docker is used by Encore to run databases locally. You can download it from the [Docker website](https://www.docker.com/get-started).\n3. **Encore CLI**: Install the Encore CLI if you haven’t already. You can follow the installation instructions from the [Encore documentation](https://encore.dev/docs/ts/install).\n\n### Step 1: Create an Encore App and a GitHub repository\n1. **Create a New Encore App**: \n    - Create a new Encore app using the Encore CLI by running the following command:\n    ```bash\n    encore app create\n    ``` \n    - Select the `Hello World` template.\n    - Follow the prompts to create the app.\n\n2. **Push the code to a GitHub repo**:\n    - Create a new repo (public or private) on GitHub and push the code to it.\n   \n### Step 2: Push the Docker Image to GitHub's Container Registry\nTo deploy your Docker image to Railway, you first need to push it to a container registry. We will be using GitHub's container registry, but you can also use DockerHub or other registries. \nInstead of pushing the image manually we will be using GitHub actions to automate the process.\n\n1. **Create a GitHub Actions YAML file**:\n   - In your repo, create a `.github/workflows/deploy-image-yaml` file with the following contents:\n   \n```yaml\nname: Build, Push and Deploy a Docker Image to Railway\n\non:\n  push:\n    branches: [ main ]\n\npermissions:\n  contents: read\n  packages: write\n\njobs:\n  build-push-deploy-image:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3.3.0\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Download Encore CLI script\n        uses: sozo-design/curl@v1.0.2\n        with:\n          args: --output install.sh -L https://encore.dev/install.sh\n\n      - name: Install Encore CLI\n        run: bash install.sh\n\n      - name: Build Docker image\n        run: /home/runner/.encore/bin/encore build --config railway-infra.config.json docker myapp\n\n      - name: Tag Docker image\n        run: docker tag myapp ghcr.io/${{ github.repository }}:latest\n\n      - name: Push Docker image\n        run: docker push ghcr.io/${{ github.repository }}:latest\n```\n\nThis will install the Encore CLI, build the Docker image, tag it, and push it to GitHub's container registry everytime you push to the `main` branch.\nThe dynamic values like `${{ github.repository }}` will be filled in automatically by GitHub, you should not need to do anything.\n\n2. **Add, commit and push the changes**:\n   - Push the changes to your GitHub repository to trigger the GitHub action.\n\n### Step 3: Deploy the Docker Image to Railway\n\n1. **Create a new Project on Railway**:\n    - Log in to Railway and go to your dashboard. \n    - Click on **\"New\"**.\n    - Choose the **\"Empty project\"** option.\n\n2. **Create a new service inside your new project**:\n    - Click on **\"Create\"**. \n    - Select the \"Docker Image\" option.\n    - Enter the Docker Image URI, should be something like `ghcr.io/username/repo:latest`. You can should be able to find the Docker Image under **Packages** in your GitHub repo.\n    - Deploy the service.\n\n3. **Expose the service**:\n    - Click on the tne newly created service.\n    - Go to the **\"Settings\"** tab.\n    - Click on **\"Generate Domain\"**.\n    - Select `8080` as the port.\n    - Click on **\"Generate\"**.\n\n4. **Access the application**:\n    - Once deployed, and exposed you will get a public URL to access your application. It should look something like this: `https://repo-name-production.up.railway.app/`.\n    - Test the app to ensure it's running as expected, e.g. \n   ```bash\n      curl https://repo-name-production.up.railway.app/hello/world\n    ```\n\n### Step 4: Automate the Deployment Process\nRailway has no way of knowing that you've pushed a new image to the container registry, but we can use Railway's GraphQL API to trigger a new deployment whenever a new image is pushed to the registry.\n\n1. **Generate a Railway API Token**:\n   - Go to your Railway dashboard.\n   - Click on your profile icon in the top right corner.\n   - Go to **\"Account Settings\"**.\n   - Click on **\"Tokens\"**.\n   - Give the token a name and click on **\"Create\"**.\n   - Copy the generated token.\n\n2. **Add the Railway API Token to GitHub Secrets**:\n   - Go to your GitHub repository.\n   - Go to **\"Settings\"**.\n   - Click on **\"Secrets and variables\" → \"Actions\"**.\n   - Click on **\"New repository secret\"**.\n   - Add a new secret called `RAILWAY_API_TOKEN` and paste the token you copied earlier.\n\n3. **Add a JavaScript script to your repo**:\n   - Create a new file in your repo named `script.js` with the following contents:\n```javascript\nconst TOKEN = process.argv.slice(2)[0];\nconst ENVIRONMENT_ID = \"<your environment id>\"\nconst SERVICE_ID = \"<your service id>\"\n\nconst resp = await fetch('https://backboard.railway.com/graphql/v2', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'authorization': `Bearer ${TOKEN}`,\n  },\n  body: JSON.stringify({\n    query: `\n      mutation ServiceInstanceRedeploy {\n          serviceInstanceRedeploy(\n              environmentId: \"${ENVIRONMENT_ID}\"\n              serviceId: \"${SERVICE_ID}\"\n          )\n      }`\n  }),\n})\n\nconst data = await resp.json()\n\nif (data.errors) {\n  console.error(data.errors)\n  throw new Error('Failed to redeploy service')\n}\n\nconsole.log(data)\n ```\n   - Replace `<your environment id>` and `<your service id>` with the actual values. You can find these values in the Railway dashboard URL when you're on the service page.\n\n4. **Add new steps to the GitHub Actions YAML file**:\n   - At the bottom of the existing file, add the following steps to call the script:\n```yaml\n      - name: Set up Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - name: Trigger Railway deployment\n        run: node script.js ${{ secrets.RAILWAY_API_TOKEN }}\n```\n\nWhenever you push a new Docker Image to the container registry, the GitHub action will trigger a new deployment on Railway.\n\n### Step 5: Add a Database to Your App\n\nRailway provides managed databases, allowing you to add a database to your app easily. Here’s how to set up a database for your app:\n\n1. **Create a database for your app on Railway**:\n   - Navigate to your Railway app.\n   - Click on **\"Create\"** → **\"Database\"\"** → **\"Add PostgreSQL\"\"**\n\n2. **Copy the connection details**:\n   - Click on the database you just created.\n   - Click the **\"Data\"** → **\"Connect\"** → **\"Public Network\"**.\n   - Copy the raw `psql` command connection details. \n   \n3. **Create a database table**:\n   - Connect to the database using the `psql` command:\n   ```bash\n   PGPASSWORD=<password> psql -h <hostname>.rlwy.net -U postgres -p 39684 -d railway\n   ```\n   - Create a table\n   ```sql\n     CREATE TABLE users (\n        id SERIAL PRIMARY KEY,\n        name TEXT\n     );\n     INSERT INTO users (name) VALUES ('Alice');\n   ```\n   \n4. **Declare a Database in your Encore app**:\n   - Open your Encore app’s codebase.\n   - Add `mydb` database to your app ([Encore Database Documentation](https://encore.dev/docs/ts/primitives/databases))\n   ```typescript\n      const mydb = new SQLDatabase(\"mydb\", {\n         migrations: \"./migrations\",\n      });\n\n      export const getUser = api(\n        { expose: true, method: \"GET\", path: \"/names/:id\" },\n        async ({id}: {id:number}): Promise<{ id: number; name: string }> => {\n          return await mydb.queryRow`SELECT * FROM users WHERE id = ${id}` as { id: number; name: string };\n        }\n      );\n   ```\n\n5. **Create an Encore Infrastructure config**\n   - Create a file named `infra.config.json` in the root of your Encore app.\n   - Add the connection details to the file:\n   ```json\n   {\n      \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n      \"sql_servers\": [\n      {\n         \"host\": \"<hostname>.rlwy.net:39684\",\n         \"tls_config\": {\n            \"disable_ca_validation\": true\n         },\n         \"databases\": {\n            \"mydb\": {\n               \"name\": \"railway\",\n               \"username\": \"postgres\",\n               \"password\": {\"$env\": \"DB_PASSWORD\"}\n             }\n         }\n      }]   \n   }\n   ```\n   Railway does not allow for downloading the CA certificate for the database, so we disable the CA validation.\n\n7. **Set Up Environment Variables (Optional)**:\n   - Click on the deployed image in your app view on Railway.\n   - Click **\"Variables\"**.\n   - Add the database password as an environment variable called `DB_PASSWORD`.\n\n8. **Make a new deployment**:\n   - Add commit and push the changes to your GitHub repository, this will trigger a new deploy on Railway.\n\n9. **Test the Database Connection**:\n   - Test the database connection by calling the API\n   ```bash\n    curl https://myapp.railway.app/names/1\n   ```\n\n### Conclusion\nThat’s it! You’ve successfully deployed an Encore app to Railway using Docker. You can now scale your app, monitor its performance, and manage it easily through the Railway dashboard. If you encounter any issues, refer to the Railway documentation or the Encore community for help. Happy coding!\n"
  },
  {
    "path": "docs/ts/tutorials/graphql.mdx",
    "content": "---\ntitle: Building a GraphQL API\nsubtitle: Learn how to build a GraphQL API using Encore\nseotitle: How to build a GraphQL API using Encore.ts\nseodesc: Learn how to build a microservices backend in TypeScript, powered by GraphQL and Encore.ts\nlang: ts\n---\n\nEncore has great support for GraphQL with its type-safe approach to building APIs.\n\nEncore's automatic tracing also makes it easy to find and fix\nperformance issues that often arise in GraphQL APIs (like the [N+1 problem](https://hygraph.com/blog/graphql-n-1-problem)).\n\nIn this tutorial we will build a GraphQL API using [Apollo](https://www.apollographql.com/docs/apollo-server/) and Encore.ts.\n\nThe final code will look like this:\n\n<div className=\"not-prose my-10\">\n <Editor projectName=\"graphqlTS\" />\n</div>\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n## 1. Create your Encore application\n\n🥐 Create a new application by running `encore app create` and select `Empty app` as the template.\n\nIf this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is optional, but is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial).\n\n## 2. GraphQL setup\n\nFirst, we need to install the necessary dependencies:\n\n🥐 Update your `package.json` file to look like this:\n\n```json\n-- package.json --\n{\n  \"name\": \"encore-graphql\",\n  \"private\": true,\n  \"version\": \"0.0.1\",\n  \"license\": \"MPL-2.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"generate\": \"graphql-codegen --config codegen.yml\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.5.7\",\n    \"typescript\": \"^5.2.2\",\n    \"@graphql-codegen/cli\": \"2.16.5\",\n    \"@graphql-codegen/typescript\": \"2.8.8\",\n    \"@graphql-codegen/typescript-resolvers\": \"2.7.13\"\n  },\n  \"dependencies\": {\n    \"@apollo/server\": \"^4.11.0\",\n    \"encore.dev\": \"^1.35.3\",\n    \"graphql\": \"^16.9.0\",\n    \"graphql-tag\": \"^2.12.6\"\n  }\n}\n```\n\n🥐 Run `npm install` to install the dependencies.\n\n🥐 Next, create a `codegen.yml` file in the application root containing:\n\n```\n-- codegen.yml --\n# This configuration file tells GraphQL Code Generator how to generate types based on our schema.\nschema: './schema.graphql'\ngenerates:\n  # Specify where our generated types should live.\n  ./graphql/__generated__/resolvers-types.ts:\n    plugins:\n      - 'typescript'\n      - 'typescript-resolvers'\n    config:\n      useIndexSignature: true\n```\n\n## 3. Add GraphQL schema\n\nNow it's time to define the GraphQL schema.\n\n🥐 Create a `schema.graphql` file in the application root containing:\n\n```\n-- schema.graphql --\ntype Query {\n  books: [Book]\n}\n\ntype Book {\n  title: String!\n  author: String!\n}\n\ntype AddBookMutationResponse {\n  code: String!\n  success: Boolean!\n  message: String!\n  book: Book\n}\n\ntype Mutation {\n  addBook(title: String!, author: String!): AddBookMutationResponse\n}\n```\n\n🥐 Run the code generation script to generate the resolver types:\n\n```shell\n$ npm run generate\n```\nThe types will be written to `graphql/__generated__/resolvers-types.ts` and will contain a bunch of types that we can use when implementing the resolvers.\n\n## 4. Create a Book service\n\nLet's create a simple book service that we can later query using GraphQL. It's a good idea to to make the GraphQL library query Encore endpoints because that will result in traces being created for each called endpoint. Having tracing makes it easy to find and fix performance issues that often arise in GraphQL APIs.\n\n🥐 In your application's root folder, create a directory named `book` containing a file named `encore.service.ts`.\n\n```shell\n$ mkdir book\n$ touch book/encore.service.ts\n```\n\n🥐 Add the following code to `book/encore.service.ts`:\n\n```ts\n-- book/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"book\");\n```\n\nThis is how you define a service with Encore. Encore will now consider files in the `book` directory and all its subdirectories as part of the `book` service.\n\n🥐 Next, create a `book/book.ts` file containing:\n\n```ts\nimport { api, APIError } from \"encore.dev/api\";\nimport { Book } from \"../graphql/__generated__/resolvers-types\";\n\nconst db: Book[] = [\n  {\n    title: \"To Kill a Mockingbird\",\n    author: \"Harper Lee\",\n  },\n  {\n    title: \"1984\",\n    author: \"George Orwell\",\n  },\n  {\n    title: \"The Great Gatsby\",\n    author: \"F. Scott Fitzgerald\",\n  },\n  {\n    title: \"Moby-Dick\",\n    author: \"Herman Melville\",\n  },\n  {\n    title: \"Pride and Prejudice\",\n    author: \"Jane Austen\",\n  },\n];\n\nexport const list = api(\n  { expose: true, method: \"GET\", path: \"/books\" },\n  async (): Promise<{ books: Book[] }> => {\n    return { books: db };\n  },\n);\n\n// Omit the \"__typename\" field from the request\ntype AddRequest = Omit<Required<Book>, \"__typename\">;\n\nexport const add = api(\n  { expose: true, method: \"POST\", path: \"/book\" },\n  async (book: AddRequest): Promise<{ book: Book }> => {\n    if (db.some((b) => b.title === book.title)) {\n      throw APIError.alreadyExists(\n        `Book \"${book.title}\" is already in database`,\n      );\n    }\n    db.push(book);\n    return { book };\n  },\n);\n```\n\nThe `book` service contains two endpoint, one for listing all books and another to add a new book to the database. Our \"database\" is hardcoded just to limit the scope of this example. Take a look at the [Using SQL databases](/docs/ts/primitives/databases) docs to learn how to set up and use a database.\n\nWe get the `Book` type from the generated resolver types. This will make it easier later when we create the resolver functions.\n\n## 5. Create the GraphQL service\n\nNow it's time to create our Encore service that will provide the GraphQL API.\n\n🥐 In the `graphql` directory, add a `encore.service.ts` file with the following content:\n\n```ts\n-- graphql/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"graphql\");\n```\n\n  Now, we need to create resolvers that call the `book` service. Since the GraphQL API uses the same types as the Encore API exposes (we import types form `resolvers-types.ts` in `book.ts`), our resolver can just be thin wrapper around out API endpoints.\n\n🥐 Create the directory `resolvers` in the `graphql` directory. In the resolvers directory we want to place three files: `index.ts`, `queries.ts` and `mutations.ts`:\n\n```ts\n-- resolvers/index.ts --\nimport { Resolvers } from \"../__generated__/resolvers-types\";\nimport Query from \"./queries.js\";\nimport Mutation from \"./mutations.js\";\n\nconst resolvers: Resolvers = { Query, Mutation };\n\nexport default resolvers;\n-- resolvers/queries.ts --\nimport { book } from \"~encore/clients\";\nimport { QueryResolvers } from \"../__generated__/resolvers-types\";\n\n// Use the generated `QueryResolvers` type to type check our queries!\nconst queries: QueryResolvers = {\n  books: async () => {\n    const { books } = await book.list();\n    return books;\n  },\n};\n\nexport default queries;\n-- resolvers/mutations.ts --\nimport { book } from \"~encore/clients\";\nimport { MutationResolvers } from \"../__generated__/resolvers-types\";\nimport { APIError } from \"encore.dev/api\";\n\n// Use the generated `MutationResolvers` type to type check our mutations\nconst mutations: MutationResolvers = {\n  addBook: async (_, { title, author }) => {\n    try {\n      const resp = await book.add({ title, author });\n      return {\n        book: resp.book,\n        success: true,\n        code: \"ok\",\n        message: \"New book added\",\n      };\n    } catch (err) {\n      const apiError = err as APIError;\n\n      return {\n        book: null,\n        success: false,\n        code: apiError.code,\n        message: apiError.message,\n      };\n    }\n  },\n};\n\nexport default mutations;\n```\n\nNow we are ready can create the ApolloServer that makes use of our resolvers and to expose our GraphQL endpoint.\n\n🥐 Still in the `graphql` directory, create a `graphql.ts` file containing:\n\n```ts\n-- graphql/graphql.ts --\nimport { api } from \"encore.dev/api\";\nimport { ApolloServer, HeaderMap } from \"@apollo/server\";\nimport { readFileSync } from \"node:fs\";\nimport resolvers from \"./resolvers\";\nimport { json } from \"node:stream/consumers\";\n\nconst typeDefs = readFileSync(\"./schema.graphql\", { encoding: \"utf-8\" });\n\nconst server = new ApolloServer({\n  typeDefs,\n  resolvers,\n});\n\nawait server.start();\n\nexport const graphqlAPI = api.raw(\n  { expose: true, path: \"/graphql\", method: \"*\" },\n  async (req, res) => {\n    server.assertStarted(\"/graphql\");\n\n    const headers = new HeaderMap();\n    for (const [key, value] of Object.entries(req.headers)) {\n      if (value !== undefined) {\n        headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n      }\n    }\n\n    // More on how to use executeHTTPGraphQLRequest: https://www.apollographql.com/docs/apollo-server/integrations/building-integrations/\n    const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({\n      httpGraphQLRequest: {\n        headers,\n        method: req.method!.toUpperCase(),\n        body: await json(req),\n        search: new URLSearchParams(req.url ?? \"\").toString(),\n      },\n      context: async () => {\n        return { req, res };\n      },\n    });\n\n    for (const [key, value] of httpGraphQLResponse.headers) {\n      res.setHeader(key, value);\n    }\n    res.statusCode = httpGraphQLResponse.status || 200;\n\n    if (httpGraphQLResponse.body.kind === \"complete\") {\n      res.end(httpGraphQLResponse.body.string);\n      return;\n    }\n\n    for await (const chunk of httpGraphQLResponse.body.asyncIterator) {\n      res.write(chunk);\n    }\n    res.end();\n  },\n);\n```\n\nThis creates an [Raw API endpoint](https://encore.dev/docs/ts/primitives/raw-endpoints) available on `/graphql`. In the endpoint we use ApolloServer to handle the GraphQL queries and mutations. We then return the response to the client.\n\nIf we were to use another GraphQL library other than Apollo, the concept would still be the same:\n1. Take client requests with a Raw endpoint.\n2. Pass along the request and response objects to the GraphQL library of your choice.\n3. Use the library to handle the GraphQL queries and mutations.\n4. Return the GraphQL response from the Raw endpoint.\n\n## 6. Trying it out\n\nWith that, the GraphQL API is done!\n\n🥐Try it out by running `encore run` and opening [https://studio.apollographql.com/sandbox](https://studio.apollographql.com/sandbox) in your browser. Set http://localhost:4000/graphql as your endpoint URL. You should now be able to read the schema and execute queries.\n\nEnter the query:\n```graphql\nmutation AddBook {\n  addBook(author: \"J.R.R. Tolkien\", title: \"The Hobbit\") {\n    success\n    message\n    code\n  }\n}\n```\n\nNow try the GetBooks query:\n\n```graphql\nquery GetBooks {\n  books {\n    author\n    title\n  }\n}\n```\n\nAnd you should now see the \"The Hobbit\" in the list of books.\n\n🥐 Try opening the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and view the traces that were generated when calling your GraphQL API.\n\n\n## 7. Deploy\n\n<Accordion>\n\n### Self-hosting\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nIf your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/ts/self-host/configure-infra) your Docker image.\n\n🥐 Build a Docker image by running `encore build docker graphql:v1.0`.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\n\n🥐 Upload the Docker image to the cloud provider of your choice and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\nEncore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP.\n\n### Create account\n\nBefore deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step.\n\nIf you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template.\n\nAfter creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform).\n\n### Commit changes\n\nThe final step before you deploy is to commit all changes to the project repo.\n\n🥐 Push your changes and deploy your application to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\nFrom there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment.\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n</Accordion>\n\n## Conclusion\n\nWe've now built a GraphQL API gateway that forwards requests to the application's underlying Encore services in a type-safe way with minimal boilerplate.\n\nNote that the concepts discussed here are general and can be easily adapted to any GraphQL schema.\n\nWhenever you make a change to the schema or configuration, re-run `npm run generate` to\nregenerate the resolver types.\n"
  },
  {
    "path": "docs/ts/tutorials/rest-api.mdx",
    "content": "---\nseotitle: How to build a REST API\nseodesc: Learn how to build and ship a REST API in just a few minutes using Encore.ts, go from zero to running API with this tutorial.\ntitle: Building a REST API\nsubtitle: Learn how to build a URL shortener with a REST API and PostgreSQL database\nlang: ts\n---\n\nIn this tutorial you will create a REST API for a URL Shortener service. In a few short minutes, you'll learn how to:\n\n* Create REST APIs with Encore\n* Use PostgreSQL databases\n* Use the local development dashboard to test your app\n* Create and run tests\n\nThis is the end result:\n<div className=\"not-prose mb-10\">\n   <Editor projectName=\"urlShortenerTS\" />\n</div>\n\n<div className=\"not-prose flex items-center gap-3 mb-6\">\n   <a href=\"https://app.encore.cloud/create-app/clone/ts-url-shortener\"><img className=\"noshadow m-0\" src=\"https://encore.cloud/assets/img/deploy.svg\" alt=\"Deploy to Encore\" height=\"30\" /></a>\n   <span>Deploy this app to a free dev environment</span>\n</div>\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n## 1. Create a service and endpoint\n\nCreate a new application by running `encore app create` and select `Empty app` as the template.\n\nIf this is the first time you're using Encore, you'll be asked if you wish to create a free account.\nThis is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial).\n\nNow let's create a new `url` service.\n\n🥐 In your application's root folder, create a directory named `url` containing a file named `encore.service.ts`.\n\n```shell\n$ mkdir url\n$ touch url/encore.service.ts\n```\n\n🥐 Add the following code to `url/encore.service.ts`:\n\n```ts\n-- url/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"url\");\n```\n\nThis is how you define a service with Encore. Encore will now consider files in the `url` directory and all its subdirectories as part of the `url` service.\n\n\n🥐 Create a new file `url.ts` in the `url` directory:\n\n```shell\n$ touch url/url.ts\n```\n\n🥐 Add the following code to `url/url.ts`:\n\n```ts\n-- url/url.ts --\nimport { api } from \"encore.dev/api\";\nimport { randomBytes } from \"node:crypto\";\n\ninterface URL {\n  id: string; // short-form URL id\n  url: string; // complete URL, in long form\n}\n\ninterface ShortenParams {\n  url: string; // the URL to shorten\n}\n\n// Shortens a URL.\nexport const shorten = api(\n  { method: \"POST\", path: \"/url\", expose: true },\n  async ({ url }: ShortenParams): Promise<URL> => {\n    const id = randomBytes(6).toString(\"base64url\");\n    return { id, url };\n  },\n);\n```\n\nThis sets up the `POST /url` endpoint.\n\n🥐 Let’s see if it works! Start your app by running the following command from your app's root directory:\n\n```shell\n$ encore run\n```\n\nYou should see this:\n\n```output\nEncore development server running!\n\nYour API is running at:     http://127.0.0.1:4000\nDevelopment Dashboard URL:  http://localhost:9400/5g288\n3:50PM INF registered API endpoint endpoint=shorten path=/url service=url\n```\n\n🥐 Next, call your endpoint from the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and view a trace of the response.\nIt should look like this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/rest_tut.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nYou can also call it from the terminal:\n\n```shell\n$ curl http://localhost:4000/url -d '{\"url\": \"https://encore.dev\"}'\n```\n\nYou should see this:\n\n```output\n{\n  \"id\": \"5cJpBVRp\",\n  \"url\": \"https://encore.dev\"\n}\n```\n\nIt works! There’s just one problem...\n\nRight now, we’re not actually storing the URL anywhere. That means we can generate shortened IDs but there’s no way to get back to the original URL! We need to store a mapping from the short ID to the complete URL.\n\n## 2. Save URLs in a database\nFortunately, Encore makes it really easy to set up a PostgreSQL database to store our data. To do so, we first define a **database schema**, in the form of a migration file.\n\n🥐 Create a new folder named `migrations` inside the `url` folder. Then, inside the `migrations` folder, create an initial database migration file named `001_create_tables.up.sql`. The file name format is important (it must start with `001_` and end in `.up.sql`).\n\n```shell\n$ mkdir url/migrations\n$ touch url/migrations/001_create_tables.up.sql\n```\n\n🥐 Add the following contents to the file:\n\n```sql\n-- url/migrations/001_create_tables.up.sql --\nCREATE TABLE url (\n\tid TEXT PRIMARY KEY,\n\toriginal_url TEXT NOT NULL\n);\n```\n\n🥐 Next, go back to the `url/url.ts` file and import the `SQLDatabase` class from `encore.dev/storage/sqldb` module by modifying the imports to look like this:\n\n```ts\n-- url/url.ts --\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { randomBytes } from \"node:crypto\";\n```\n\n🥐 Now, to define the database, create an instance of the `SQLDatabase` class in the `url` service:\n\n```ts\nHL url/url.ts 4:6\n-- url/url.ts --\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { randomBytes } from \"node:crypto\";\n\n// 'url' database is used to store the URLs that are being shortened.\nconst db = new SQLDatabase(\"url\", { migrations: \"./migrations\" });\n\ninterface URL {\n    id: string; // short-form URL id\n    url: string; // complete URL, in long form\n}\n\ninterface ShortenParams {\n    url: string; // the URL to shorten\n}\n\n// Shortens a URL.\nexport const shorten = api(\n    { method: \"POST\", path: \"/url\", expose: true },\n    async ({ url }: ShortenParams): Promise<URL> => {\n        const id = randomBytes(6).toString(\"base64url\");\n        return { id, url };\n    },\n);\n```\n\n🥐 Lastly, update the `shorten` function to insert data into the database:\n\n```ts\nHL url/url.ts 21:26\n-- url/url.ts --\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { randomBytes } from \"node:crypto\";\n\n// 'url' database is used to store the URLs that are being shortened.\nconst db = new SQLDatabase(\"url\", { migrations: \"./migrations\" });\n\ninterface URL {\n    id: string; // short-form URL id\n    url: string; // complete URL, in long form\n}\n\ninterface ShortenParams {\n    url: string; // the URL to shorten\n}\n\n// Shortens a URL.\nexport const shorten = api(\n    { method: \"POST\", path: \"/url\", expose: true },\n    async ({ url }: ShortenParams): Promise<URL> => {\n      const id = randomBytes(6).toString(\"base64url\");\n      await db.exec`\n        INSERT INTO url (id, original_url)\n        VALUES (${id}, ${url})\n      `;\n      return { id, url };\n    },\n  );\n```\n\n<Callout type=\"info\">\n\nBefore running your application, make sure you have [Docker](https://www.docker.com) installed and running. It's required to locally run Encore applications with databases.\n\n</Callout>\n\n🥐 Next, start the application again with `encore run` and Encore automatically sets up your database.\n\n(In case your application won't run, check the [databases troubleshooting guide](/docs/ts/primitives/databases#troubleshooting).)\n\nYou can verify that the database was created by looking at the **Infra** tab in the local development dashboard at [localhost:9400](http://localhost:9400), which should look like this:\n\n<img src=\"/assets/docs/infra_tab.png\" alt=\"Infra tab in local development dashboard\" className=\"w-full h-full\" />\n\n🥐 Now let's call the API again from the local development dashboard, or from the terminal:\n\n```shell\n$ curl http://localhost:4000/url -d '{\"url\": \"https://encore.dev\"}'\n```\n\n🥐 Finally, let's verify that it was saved in the database. You can do this by checking the trace in the local development dashboard, or you can run  `encore db shell url` from the app root directory and inputting `select * from url;`:\n\n```shell\n$ encore db shell url\npsql (13.1, server 11.12)\nType \"help\" for help.\n\nurl=# select * from url;\n    id    |    original_url\n----------+--------------------\n zr6RmZc4 | https://encore.dev\n(1 row)\n```\n\nThat was easy!\n\n## 3. Add endpoint to retrieve URLs\nTo complete our URL shortener API, let’s add the endpoint to retrieve a URL given its short id.\n\n🥐 Add this endpoint to `url/url.ts`:\n\n```ts\nHL url/url.ts 0:1\nHL url/url.ts 29:40\n-- url/url.ts --\nimport { api, APIError } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { randomBytes } from \"node:crypto\";\n\n// 'url' database is used to store the URLs that are being shortened.\nconst db = new SQLDatabase(\"url\", { migrations: \"./migrations\" });\n\ninterface URL {\n    id: string; // short-form URL id\n    url: string; // complete URL, in long form\n}\n\ninterface ShortenParams {\n    url: string; // the URL to shorten\n}\n\n// Shortens a URL.\nexport const shorten = api(\n    { method: \"POST\", path: \"/url\", expose: true },\n    async ({ url }: ShortenParams): Promise<URL> => {\n      const id = randomBytes(6).toString(\"base64url\");\n      await db.exec`\n        INSERT INTO url (id, original_url)\n        VALUES (${id}, ${url})\n      `;\n      return { id, url };\n    },\n  );\n\n  // Get retrieves the original URL for the id.\nexport const get = api(\n    { expose: true, auth: false, method: \"GET\", path: \"/url/:id\" },\n    async ({ id }: { id: string }): Promise<URL> => {\n      const row = await db.queryRow`\n          SELECT original_url FROM url WHERE id = ${id}\n      `;\n      if (!row) throw APIError.notFound(\"url not found\");\n      return { id, url: row.original_url };\n    }\n  );\n```\n\nEncore uses the `/url/:id` syntax to represent a path with a parameter. The `id` name corresponds to the parameter name in the function signature. In this case it is of type `string`, but you can also use other built-in types like `number` or `boolean` if you want to restrict the values.\n\n🥐 We can make sure it works by reviewing the endpoint in the [Service Catalog](/docs/ts/observability/service-catalog) in the local development dashboard, where we can call it using the `id` you got in the previous step:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/rest_tut_3.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nYou can also call it directly from the terminal:\n\n```shell\n$ curl http://localhost:4000/url/your-id-from-the-previous-step\n```\n\nYou should now see this:\n\n```json\n{\n  \"id\": \"your-id-from-the-previous-step\",\n  \"url\": \"https://encore.dev\"\n}\n```\n\nIt works! That's how you build REST APIs and use PostgreSQL databases in Encore.\n\n## 4. Add a test\n\nBefore deployment, it is good practice to have tests to assure that the service works properly. Such tests including database access are easy to write.\n\n🥐 Let's start by adding the `vitest` package to your project:\n\n```shell\n$ npm i --save-dev vitest\n```\n\n[Vitest](https://vitest.dev/) is a testing framework that works great with Encore but you can use another TypeScript testing framework if you like.\n\n🥐 Next we need to add a test script to our `package.json`:\n\n```json\n-- package.json --\n\"scripts\": {\n  \"test\": \"vitest\"\n},\n```\n\nWe've prepared a test to check that the whole cycle of shortening the URL, storing and then retrieving the original URL works.\n\n🥐 Save this in a separate file `url/url.test.ts`.\n\n```ts\n-- url/url.test.ts --\nimport { describe, expect, test } from \"vitest\";\nimport { get, shorten } from \"./url\";\n\ndescribe(\"shorten\", () => {\n  test(\"getting a shortened url should give back the original\", async () => {\n    const resp = await shorten({ url: \"https://example.com\" });\n    const url = await get({ id: resp.id });\n    expect(url.url).toBe(\"https://example.com\");\n  });\n});\n```\n\n🥐 Now run `encore test` to verify that it's working.\n\nIf you use the local development dashboard ([localhost:9400](http://localhost:9400)), you can even see traces for tests.\n\n## 5. Deploy\n\n<Accordion>\n\n### Self-hosting\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nIf your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to configure your Docker image with the necessary configuration.\nOur URL shortener makes use of a PostgreSQL database, so we'll need to supply a runtime configuration so that our app knows how to connect to the database in the cloud.\n\n🥐 Create a new file `infra-config.json` in the root of your project with the following contents:\n\n```json\n{\n   \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n   \"sql_servers\": [\n      {\n         \"host\": \"my-db-host:5432\",\n         \"databases\": {\n            \"url\": {\n               \"username\": \"my-db-owner\",\n                \"password\": {\"$env\": \"DB_PASSWORD\"}\n            }\n         }\n      }\n   ]\n}\n```\n\nThe values in this configuration are just examples, you will need to replace them with the correct values for your database.\nTake a look at our guide for [deploying an Encore app with a PostgreSQL database to Digital Ocean](/docs/ts/self-host/deploy-digitalocean) for more information.\n\n🥐 Build a Docker image by running `encore build docker url-shortener:v1.0`.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\n\n🥐 Upload the Docker image to the cloud provider of your choice and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\nEncore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP.\n\n### Create account\n\nBefore deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step.\n\nIf you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template.\n\nAfter creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform).\n\n### Commit changes\n\nThe final step before you deploy is to commit all changes to the project repo.\n\n🥐 Commit the new files to the project's git repo and trigger a deploy to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/rest_tut_4.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nFrom there you can also see metrics, traces, and connect your own AWS or GCP account to use for production deployment.\n\n*Now you have a fully fledged backend running in the cloud, well done!*\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n🥐 A great next step is to [integrate with GitHub](/docs/platform/integrations/github). Once you've linked with GitHub, Encore will automatically start building and running tests against your Pull Requests.\n\n</Accordion>\n\n## What's next\n\nNow that you know how to build a backend with a database, you're ready to let your creativity flow and begin building your next great idea!\n\nWe're excited to hear what you're going to build with Encore, join the pioneering developer community on [Discord](/discord) and share your story.\n"
  },
  {
    "path": "docs/ts/tutorials/slack-bot.md",
    "content": "---\nseotitle: Tutorial – How to build a Slack bot\nseodesc: Learn how to build a Slack bot with an Encore.ts backend, and get it running in the cloud in just a few minutes.\ntitle: Building a Slack bot\nsubtitle: Learn how to build a Slack bot with an Encore backend\nlang: ts\n---\n\nIn this tutorial you will create a Slack bot that brings the greatness of the `cowsay` utility to Slack!\n\n![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png \"Slack bot\")\n\nThis is the end result:\n<div className=\"not-prose mb-10\">\n   <Editor projectName=\"slackBotTS\" />\n</div>\n\n<div className=\"not-prose flex items-center gap-3 mb-6\">\n   <a href=\"https://app.encore.cloud/create-app/clone/ts-slack-bot\"><img className=\"noshadow m-0\" src=\"https://encore.cloud/assets/img/deploy.svg\" alt=\"Deploy to Encore\" height=\"30\" /></a>\n   <span>Deploy this app to a free dev environment</span>\n</div>\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n## 1. Create your Encore application\n\n🥐 Create a new Encore application by running `encore app create` and select `Empty app` as the template.\n**Take a note of your app id, we'll need it in the next step.**\n\n## 2. Create a Slack app\n\n🥐 The first step is to create a new Slack app:\n\n1. Head over to [Slack's API site](https://api.slack.com/apps) and create a new app.\n2. When prompted, choose to create the app **from an app manifest**.\n3. Choose a workspace to install the app in.\n\n🥐 Enter the following manifest (replace `$APP_ID` in the URL below with your app id from above):\n\n```yaml\n_metadata:\n  major_version: 1\ndisplay_information:\n  name: Encore Bot\n  description: Cowsay for the cloud age.\nfeatures:\n  slash_commands:\n    - command: /cowsay\n      # Replace $APP_ID below\n      url: https://staging-$APP_ID.encr.app/cowsay\n      description: Say things with a flair!\n      usage_hint: your message here\n      should_escape: false\n  bot_user:\n    display_name: encore-bot\n    always_online: true\noauth_config:\n  scopes:\n    bot:\n      - commands\n      - chat:write\n      - chat:write.public\nsettings:\n  org_deploy_enabled: false\n  socket_mode_enabled: false\n  token_rotation_enabled: false\n```\n\nOnce created, we're ready to move on with implementing our Encore endpoint!\n\n## 3. Implement the Slack endpoint\n\nSince Slack sends custom HTTP headers that we need to pay attention to, we're going to\nuse a raw endpoint in Encore. For more information on this check out Slack's documentation\non [Enabling interactivity with Slash Commands](https://api.slack.com/interactivity/slash-commands).\n\n🥐 In your Encore app, create a directory named `slack` containing a file named `encore.service.ts`.\n\n```shell\n$ mkdir slack\n$ touch slack/encore.service.ts\n```\n\n🥐 Add the following code to `slack/encore.service.ts`:\n\n```ts\n-- slack/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"slack\");\n```\n\nThis is how you create define services with Encore. Encore will now consider files in the `slack` directory and all its subdirectories as part of the `slack` service.\n\n🥐 Create a file `slack/slack.ts` with the following contents:\n\n```ts\n-- slack/slack.ts --\nimport { api } from \"encore.dev/api\";\nimport type { IncomingMessage } from \"node:http\";\n\n// cowart is the formatting string for printing the cow art.\nconst cowart = (msg: string) => `Moo! ${msg}\n`;\n\nexport const cowsay = api.raw(\n  { expose: true, path: \"/cowsay\", method: \"*\" },\n  async (req, resp) => {\n    const body = await getBody(req);\n\n    const text = new URLSearchParams(body).get(\"text\");\n    const msg = cowart(text || \"Moo!\");\n    resp.setHeader(\"Content-Type\", \"application/json\");\n    resp.end(JSON.stringify({ response_type: \"in_channel\", text: msg }));\n  },\n);\n\n// Extract the body from an incoming request.\nfunction getBody(req: IncomingMessage): Promise<string> {\n  return new Promise((resolve) => {\n    const bodyParts: any[] = [];\n    req\n      .on(\"data\", (chunk) => {\n        bodyParts.push(chunk);\n      })\n      .on(\"end\", () => {\n        resolve(Buffer.concat(bodyParts).toString());\n      });\n  });\n}\n```\n\nLet's try it out locally.\n\n🥐 Start your app with `encore run` and then call it in another terminal:\n\n```shell\n$ curl http://localhost:4000/cowsay -d 'text=Eat your greens!'\n{\"response_type\":\"in_channel\",\"text\":\"Moo! Eat your greens!\"}\n```\n\nLooks great!\n\n🥐 Next, let's deploy it to the cloud:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nOnce deployed, we're ready to try our Slack command!\n\n🥐 Head over to the workspace you installed the app in and run `/cowsay Hello there`.\nYou should see something like this:\n\n![Cowsay](https://encore.dev/assets/docs/cowsay-wip.png \"Cowsay (Work in Progress)\")\n\nAnd just like that we have a fully working Slack integration.\n\n## 4. Secure the webhook endpoint\n\nIn order to get up and running quickly we ignored one important aspect for a production-ready Slack app:\nverifying that the webhook requests are actually coming from Slack. Let's do that now!\n\nThe Slack documentation covers this really well on the [Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack) page.\n\nIn short, what we need to do is:\n\n1. Save a shared secret that Slack provides us\n2. Use the secret to verify that the request comes from Slack, using HMAC (Hash-based Message Authentication Code).\n\n### Save the shared secret\n\nLet's define a secret using Encore's secrets management functionality.\n\n🥐 Add this to your `slack.ts` file:\n\n```ts\nHL slack/slack.ts 0:0\nHL slack/slack.ts 2:2\n-- slack/slack.ts --\nimport { secret } from \"encore.dev/config\";\n\nconst slackSigningSecret = secret(\"SlackSigningSecret\");\n```\n\n🥐 Head over to the configuration section for your Slack app (go to [Your Apps](https://api.slack.com/apps) &rarr; select your app &rarr; Basic Information).\n\n🥐 Copy the **Signing Secret** and then run `encore secret set --type prod SlackSigningSecret` and paste the secret.\n\n🥐 For development you will also want to set `encore secret set --type dev,local,pr SlackSigningSecret`.\nYou can use the same secret value or a placeholder value.\n\n### Compute the HMAC\n\nTypeScript makes computing HMAC very straightforward, but it's still a fair amount of code.\n\n🥐 Add a few more imports to your file, so that it reads:\n```ts\n-- slack/slack.ts --\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport type { IncomingHttpHeaders } from \"http\";\n```\n\n🥐 Next, we'll add the `verifySignature` function:\n\n```ts\n-- slack/slack.ts --\n// Verifies the signature of an incoming request from Slack.\nconst verifySignature = async function (\n  body: string,\n  headers: IncomingHttpHeaders,\n) {\n  const requestTimestampSec = parseInt(\n    headers[\"x-slack-request-timestamp\"] as string,\n  );\n  const signature = headers[\"x-slack-signature\"] as string;\n  if (Number.isNaN(requestTimestampSec)) {\n    throw new Error(\n      `Failed to verify authenticity: header x-slack-request-timestamp did not have the expected type (${requestTimestampSec})`,\n    );\n  }\n\n  // Calculate time-dependent values\n  const nowMs = Date.now();\n  const requestTimestampMaxDeltaMin = 5;\n  const fiveMinutesAgoSec =\n    Math.floor(nowMs / 1000) - 60 * requestTimestampMaxDeltaMin;\n\n  // Enforce verification rules\n\n  // Rule 1: Check staleness\n  if (requestTimestampSec < fiveMinutesAgoSec) {\n    throw new Error(\n      `Failed to verify authenticity: x-slack-request-timestamp must differ from system time by no more than ${requestTimestampMaxDeltaMin} minutes or request is stale`,\n    );\n  }\n\n  // Rule 2: Check signature\n  // Separate parts of signature\n  const [signatureVersion, signatureHash] = signature.split(\"=\");\n  // Only handle known versions\n  if (signatureVersion !== \"v0\") {\n    throw new Error(`Failed to verify authenticity: unknown signature version`);\n  }\n  // Compute our own signature hash\n  const hmac = createHmac(\"sha256\", slackSigningSecret());\n  hmac.update(`${signatureVersion}:${requestTimestampSec}:${body}`);\n  const ourSignatureHash = hmac.digest(\"hex\");\n  if (\n    !signatureHash ||\n    !timingSafeEqual(\n      Buffer.from(signatureHash, \"utf8\"),\n      Buffer.from(ourSignatureHash, \"utf8\"),\n    )\n  ) {\n    throw new Error(`Failed to verify authenticity: signature mismatch`);\n  }\n};\n```\n\nWe're now ready to verify the signature.\n\n🥐 Update the `cowsay` function to look like this:\n\n```ts\nHL slack/slack.ts 5:12\n-- slack/slack.ts --\nexport const cowsay = api.raw(\n  { expose: true, path: \"/cowsay\", method: \"*\" },\n  async (req, resp) => {\n    const body = await getBody(req);\n\n    try {\n      await verifySignature(body, req.headers);\n    } catch (err) {\n      const e = err as Error;\n      resp.statusCode = 500;\n      resp.end(e.message);\n      return;\n    }\n\n    const text = new URLSearchParams(body).get(\"text\");\n    const msg = cowart(text || \"Moo!\");\n    resp.setHeader(\"Content-Type\", \"application/json\");\n    resp.end(JSON.stringify({ response_type: \"in_channel\", text: msg }));\n  },\n);\n```\n\n## 5. Put it all together and deploy\n\nFinally we're ready to put it all together.\n\n🥐 Add the `cowart` in `slack.ts` like so:\n\n```ts\n-- slack/slack.ts --\nconst cowart = (msg: string) => `\n\\`\\`\\`\n+-${\"-\".repeat(msg.length)}-+\n| ${msg} |\n+-${\"-\".repeat(msg.length)}-+\n      \\\\  __n__n__\n  .------\\`-\\\\00/-'\n /  ##  ## (oo)\n/ \\\\## __   ./\n   |//YY \\\\|/\n   |||   |||\n\\`\\`\\`\n`;\n```\n\n🥐 Finally, let's commit our changes and deploy it:\n\n```shell\n$ git add -A .\n$ git commit -m 'Verify webhook requests and improve art'\n$ git push encore\n```\n\n🥐 Once deployed, head back to Slack and run `/cowsay Hello there`.\n\nIf everything is set up correctly, you should see:\n\n![Slack Cowsay](https://encore.dev/assets/docs/cowsay.png \"Slack bot\")\n\nAnd there we go, a production-ready Slack bot in less than 100 lines of code.\n\nWell done!\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n"
  },
  {
    "path": "docs/ts/tutorials/uptime.md",
    "content": "---\ntitle: Building an Uptime Monitor\nsubtitle: Learn how to build an event-driven uptime monitoring system\nseotitle: How to build an event-driven Uptime Monitoring System using Encore.ts\nseodesc: Learn how to build an event-driven uptime monitoring tool using TypeScript and Encore. Get your application running in the cloud in 30 minutes!\nlang: ts\n---\n\nWant to be notified when your website goes down so you can fix it before your users notice?\n\nYou need an uptime monitoring system. Sounds daunting? Don't worry,\nwe'll build it with Encore in 30 minutes!\n\nThe app will use an event-driven architecture and the final result will look like this:\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/uptime/frontend.png\" title=\"Frontend\" />\n\n<div className=\"not-prose my-10\">\n   <Editor projectName=\"uptimeTS\" />\n</div>\n\n<div className=\"not-prose flex items-center gap-3 mb-6\">\n   <a href=\"https://app.encore.cloud/create-app/clone/ts-uptime\"><img className=\"noshadow m-0\" src=\"https://encore.cloud/assets/img/deploy.svg\" alt=\"Deploy to Encore\" height=\"30\" /></a>\n   <span>Deploy this app to a free dev environment</span>\n</div>\n\n## 1. Create your Encore application\n\n<Callout type=\"info\">\n\nTo make it easier to follow along, we've laid out a trail of croissants to guide your way.\nWhenever you see a 🥐 it means there's something for you to do.\n\n</Callout>\n\n🥐 Create a new Encore application, using this tutorial project's starting-point branch. This gives you a ready-to-go frontend to use.\n\n```shell\n$ encore app create uptime --example=github.com/encoredev/example-app-uptime/tree/starting-point-ts\n```\n\nIf this is the first time you're using Encore, you'll be asked if you wish to create a free account. This is needed when you want Encore to manage functionality like secrets and handle cloud deployments (which we'll use later on in the tutorial).\n\nWhen we're done we'll have a backend with an event-driven architecture, as seen below in the [automatically generated diagram](/docs/ts/observability/encore-flow) where white boxes are services and black boxes are Pub/Sub topics:\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/uptime/encore-flow.png\" title=\"Encore Flow\" />\n\n## 2. Create monitor service\n\nLet's start by creating the functionality to check if a website is currently up or down.\nLater we'll store this result in a database so we can detect when the status changes and\nsend alerts.\n\n🥐 Create a directory named `monitor` containing a file named `encore.service.ts`.\n\n```shell\n$ mkdir monitor\n$ touch monitor/encore.service.ts\n```\n\n🥐 Add the following code to `monitor/encore.service.ts`:\n\n```ts\n-- monitor/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"monitor\");\n```\n\nThis is how you create define services with Encore. Encore will now consider files in the `monitor` directory and all its subdirectories as part of the `monitor` service.\n\n🥐 In the `monitor` directory, create a file named `ping.ts`.\n\n🥐 Add an Encore API endpoint named `ping` that takes a URL as input and returns a response\nindicating whether the site is up or down.\n\n```ts\n-- monitor/ping.ts --\n// Service monitor checks if a website is up or down.\nimport { api } from \"encore.dev/api\";\n\nexport interface PingParams {\n  url: string;\n}\n\nexport interface PingResponse {\n  up: boolean;\n}\n\n// Ping pings a specific site and determines whether it's up or down right now.\nexport const ping = api<PingParams, PingResponse>(\n  { expose: true, path: \"/ping/:url\", method: \"GET\" },\n  async ({ url }) => {\n    // If the url does not start with \"http:\" or \"https:\", default to \"https:\".\n    if (!url.startsWith(\"http:\") && !url.startsWith(\"https:\")) {\n      url = \"https://\" + url;\n    }\n\n    try {\n      // Make an HTTP request to check if it's up.\n      const resp = await fetch(url, { method: \"GET\" });\n      // 2xx and 3xx status codes are considered up\n      const up = resp.status >= 200 && resp.status < 300;\n      return { up };\n    } catch (err) {\n      return { up: false };\n    }\n  }\n);\n```\n\n🥐 Let's try it! Run `encore run` in your terminal and you should see the service start up.\n\nThen open up the Local Development Dashboard at [http://localhost:9400](http://localhost:9400) and try calling the `monitor.ping` endpoint from the API Explorer, passing in `google.com` as the URL.\n\nYou can then see the response, logs, and view a trace of the request. It will look like this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/localdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nIf you prefer to use the terminal instead run `curl http://localhost:4000/ping/google.com` in a new terminal instead. Either way you should see the response:\n\n```json\n{\"up\": true}\n```\n\nYou can also try with `httpstat.us/400` and `some-non-existing-url.com` and it should respond with `{\"up\": false}`.\n(It's always a good idea to test the negative case as well.)\n\n### Add a test\n\n🥐 Let's write an automated test so we don't break this endpoint over time. Create the file `monitor/ping.test.ts`\nwith the content:\n\n```ts\n-- monitor/ping.test.ts --\nimport { describe, expect, test } from \"vitest\";\nimport { ping } from \"./ping\";\n\ndescribe(\"ping\", () => {\n  test.each([\n    // Test both with and without \"https://\"\n    { site: \"google.com\", expected: true },\n    { site: \"https://encore.dev\", expected: true },\n\n    // 4xx and 5xx should considered down.\n    { site: \"https://not-a-real-site.xyz\", expected: false },\n    // Invalid URLs should be considered down.\n    { site: \"invalid://scheme\", expected: false },\n  ])(\n    `should verify that $site is ${\"$expected\" ? \"up\" : \"down\"}`,\n    async ({ site, expected }) => {\n      const resp = await ping({ url: site });\n      expect(resp.up).toBe(expected);\n    },\n  );\n});\n```\n\n🥐 Run `encore test` to check that it all works as expected. You should see something like:\n\n```shell\n$ encore test\n\nDEV  v1.3.0\n\n✓ monitor/ping.test.ts (4)\n  ✓ ping (4)\n    ✓ should verify that 'google.com' is up\n    ✓ should verify that 'https://encore.dev' is up\n    ✓ should verify that 'https://not-a-real-site.xyz' is down\n    ✓ should verify that 'invalid://scheme' is down\n\nTest Files  1 passed (1)\n     Tests  4 passed (4)\n  Start at  12:31:03\n  Duration  460ms (transform 43ms, setup 0ms, collect 59ms, tests 272ms, environment 0ms, prepare 47ms)\n\nPASS  Waiting for file changes...\n```\n\n## 3. Create site service\n\nNext, we want to keep track of a list of websites to monitor.\n\nSince most of these APIs will be simple \"CRUD\" (Create/Read/Update/Delete) endpoints, let's build this service using [Knex.js](https://knexjs.org/), an ORM library that makes building CRUD endpoints really simple.\n\n🥐 Let's start with creating a new service named `site`:\n\n```shell\n$ mkdir site # Create a new directory in the application root\n$ touch site/encore.service.ts\n```\n\n```ts\n-- site/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"site\");\n```\n\n🥐 Now we want to add a SQL database to the `site` service. To do so, create a new directory named `migrations` folder inside the `site` folder:\n\n```shell\n$ mkdir site/migrations\n```\n\n🥐  Add a database migration file inside that folder, named `1_create_tables.up.sql`.\nThe file name is important (it must look something like `1_<name>.up.sql`).\n\nAdd the following contents:\n\n```sql\n-- site/migrations/1_create_tables.up.sql --\nCREATE TABLE site (\n    id SERIAL PRIMARY KEY,\n    url TEXT NOT NULL UNIQUE\n);\n```\n\n🥐 Next, install the Knex.js library and PostgreSQL client:\n\n```shell\n$ npm i knex pg\n```\n\nNow let's create the `site` service itself with our CRUD endpoints.\n\n🥐 Create `site/site.ts` with the contents:\n\n```ts\n-- site/site.ts --\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport knex from \"knex\";\n\n// Site describes a monitored site.\nexport interface Site {\n  id: number; // ID is a unique ID for the site.\n  url: string; // URL is the site's URL.\n}\n\n// AddParams are the parameters for adding a site to be monitored.\nexport interface AddParams {\n  // URL is the URL of the site. If it doesn't contain a scheme\n  // (like \"http:\" or \"https:\") it defaults to \"https:\".\n  url: string;\n}\n\n// Add a new site to the list of monitored websites.\nexport const add = api(\n  { expose: true, method: \"POST\", path: \"/site\" },\n  async (params: AddParams): Promise<Site> => {\n    const site = (await Sites().insert({ url: params.url }, \"*\"))[0];\n    return site;\n  },\n);\n\n// Get a site by id.\nexport const get = api(\n  { expose: true, method: \"GET\", path: \"/site/:id\", auth: false },\n  async ({ id }: { id: number }): Promise<Site> => {\n    const site = await Sites().where(\"id\", id).first();\n    return site ?? Promise.reject(new Error(\"site not found\"));\n  },\n);\n\n// Delete a site by id.\nexport const del = api(\n  { expose: true, method: \"DELETE\", path: \"/site/:id\" },\n  async ({ id }: { id: number }): Promise<void> => {\n    await Sites().where(\"id\", id).delete();\n  },\n);\n\nexport interface ListResponse {\n  sites: Site[]; // Sites is the list of monitored sites\n}\n\n// Lists the monitored websites.\nexport const list = api(\n  { expose: true, method: \"GET\", path: \"/site\" },\n  async (): Promise<ListResponse> => {\n    const sites = await Sites().select();\n    return { sites };\n  },\n);\n\n// Define a database named 'site', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nconst SiteDB = new SQLDatabase(\"site\", {\n  migrations: \"./migrations\",\n});\n\nconst orm = knex({\n  client: \"pg\",\n  connection: SiteDB.connectionString,\n});\n\nconst Sites = () => orm<Site>(\"site\");\n```\n\n🥐 Now make sure you have [Docker](https://docker.com) installed and running, and then restart `encore run` to cause the `site` database to be created by Encore.\n\nYou can verify that the database was created by looking at your application's Flow architecture diagram in the local development dashboard at [localhost:9400](http://localhost:9400), and then use the Service Catalog to call the `site.add` endpoint.\n\nYou can also call the `site.add` endpoint from the terminal:\n\n```shell\n$ curl -X POST 'http://localhost:4000/site' -d '{\"url\": \"https://encore.dev\"}'\n{\n  \"id\": 1,\n  \"url\": \"https://encore.dev\"\n}\n```\n\n## 4. Record uptime checks\n\nIn order to notify when a website goes down or comes back up, we need to track the previous state it was in.\n\n🥐  To do so, let's add a database to the `monitor` service as well.\nCreate the directory `monitor/migrations` and the file `monitor/migrations/1_create_tables.up.sql`:\n\n```sql\n-- monitor/migrations/1_create_tables.up.sql --\nCREATE TABLE checks (\n    id BIGSERIAL PRIMARY KEY,\n    site_id BIGINT NOT NULL,\n    up BOOLEAN NOT NULL,\n    checked_at TIMESTAMP WITH TIME ZONE NOT NULL\n);\n```\n\nWe'll insert a database row every time we check if a site is up.\n\n🥐 Add a new endpoint `check` to the `monitor` service, that\ntakes in a Site ID, pings the site, and inserts a database row\nin the `checks` table.\n\nFor this service we'll use Encore's [`SQLDatabase` class](https://encore.dev/docs/ts/primitives/databases#querying-data) instead of Knex (in order to showcase both approaches).\n\nAdd the following to `monitor/check.ts`:\n\n```ts\n-- monitor/check.ts --\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { ping } from \"./ping\";\nimport { site } from \"~encore/clients\";\n\n// Check checks a single site.\nexport const check = api(\n  { expose: true, method: \"POST\", path: \"/check/:siteID\" },\n  async (p: { siteID: number }): Promise<{ up: boolean }> => {\n    const s = await site.get({ id: p.siteID });\n    const { up } = await ping({ url: s.url });\n    await MonitorDB.exec`\n        INSERT INTO checks (site_id, up, checked_at)\n        VALUES (${s.id}, ${up}, NOW())\n    `;\n    return { up };\n  },\n);\n\n// Define a database named 'monitor', using the database migrations\n// in the \"./migrations\" folder. Encore automatically provisions,\n// migrates, and connects to the database.\nexport const MonitorDB = new SQLDatabase(\"monitor\", {\n  migrations: \"./migrations\",\n});\n```\n\n🥐 Restart `encore run` to cause the `monitor` database to be created.\n\nWe can again verify that the database was created in the Flow diagram, and also see the dependency between the `monitor` service and the `site` service that we just added.\n\nWe can then call the `monitor.check` endpoint using the id `1` that we got in the last step, and view the trace where we see the database interactions.\n\nIt will look something like this:\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/flow.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\nYou can also also inspect the database using `encore db shell <database-name>`:\n\n```shell\n$ encore db shell monitor\npsql (14.4, server 14.2)\nType \"help\" for help.\n\nmonitor=> SELECT * FROM checks;\n id | site_id | up |          checked_at\n----+---------+----+-------------------------------\n  1 |       1 | t  | 2022-10-21 09:58:30.674265+00\n```\n\nIf that's what you see, everything's working perfectly!\n\n### Add a cron job to check all sites\n\nWe now want to regularly check all the tracked sites so we can\nrespond in case any of them go down.\n\nWe'll create a new `checkAll` API endpoint in the `monitor` service\nthat will list all the tracked sites and check all of them.\n\n🥐 Let's extract some of the functionality we wrote for the\n`check` endpoint into a separate function, like so:\n\n```ts\n-- monitor/check.ts --\nimport {Site} from \"../site/site\";\n\n// Check checks a single site.\nexport const check = api(\n  { expose: true, method: \"POST\", path: \"/check/:siteID\" },\n  async (p: { siteID: number }): Promise<{ up: boolean }> => {\n    const s = await site.get({ id: p.siteID });\n    return doCheck(s);\n  },\n);\n\nasync function doCheck(site: Site): Promise<{ up: boolean }> {\n  const { up } = await ping({ url: site.url });\n  await MonitorDB.exec`\n      INSERT INTO checks (site_id, up, checked_at)\n      VALUES (${site.id}, ${up}, NOW())\n  `;\n  return { up };\n}\n```\n\nNow we're ready to create our new `checkAll` endpoint.\n\n🥐 Create the new `checkAll` endpoint inside `monitor/check.ts`:\n\n```ts\n-- monitor/check.ts --\n// CheckAll checks all sites.\nexport const checkAll = api(\n  { expose: true, method: \"POST\", path: \"/check-all\" },\n  async (): Promise<void> => {\n    const sites = await site.list();\n    await Promise.all(sites.sites.map(doCheck));\n  },\n);\n```\n\n🥐 Now that we have a `checkAll` endpoint, define a [cron job](https://encore.dev/docs/ts/primitives/cron-jobs) to automatically call it every 1 hour (since this is an example, we don't need to go too crazy and check every minute):\n\n```ts\n-- monitor/check.ts --\nimport { CronJob } from \"encore.dev/cron\";\n\n// Check all tracked sites every 1 hour.\nconst cronJob = new CronJob(\"check-all\", {\n  title: \"Check all sites\",\n  every: \"1h\",\n  endpoint: checkAll,\n});\n```\n\n<Callout type=\"info\">\n\nTo avoid confusion while developing, cron jobs are not triggered when running the application locally but work when deploying the application to a cloud environment.\n\n</Callout>\n\nThe frontend needs a way to list all sites and display if they are up or down.\n\n🥐 Add a file `monitor/status.ts` with the following code:\n\n```ts\nimport { api } from \"encore.dev/api\";\nimport { MonitorDB } from \"./check\";\n\ninterface SiteStatus {\n  id: number;\n  up: boolean;\n  checkedAt: string;\n}\n\n// StatusResponse is the response type from the Status endpoint.\ninterface StatusResponse {\n  // Sites contains the current status of all sites,\n  // keyed by the site ID.\n  sites: SiteStatus[];\n}\n\n// status checks the current up/down status of all monitored sites.\nexport const status = api(\n  { expose: true, path: \"/status\", method: \"GET\" },\n  async (): Promise<StatusResponse> => {\n    const rows = await MonitorDB.query`\n      SELECT DISTINCT ON (site_id) site_id, up, checked_at\n      FROM checks\n      ORDER BY site_id, checked_at DESC\n    `;\n    const results: SiteStatus[] = [];\n    for await (const row of rows) {\n      results.push({\n        id: row.site_id,\n        up: row.up,\n        checkedAt: row.checked_at,\n      });\n    }\n    return { sites: results };\n  },\n);\n```\n\nNow that the backend is working, let's open [http://localhost:4000/](http://localhost:4000/) in the browser to see the frontend of our application.\n\n<img className=\"w-full h-auto\" src=\"/assets/tutorials/uptime/frontend.png\" title=\"Frontend\" />\n\n## 5. Deploy\n\nTo try out your uptime monitor for real, let's deploy it to the cloud.\n\n<Accordion>\n\n### Self-hosting\n\nEncore supports building Docker images directly from the CLI, which can then be self-hosted on your own infrastructure of choice.\n\nIf your app is using infrastructure resources, such as SQL databases, Pub/Sub, or metrics, you will need to supply a [runtime configuration](/docs/ts/self-host/configure-infra) your Docker image.\n\n🥐 Create a new file `infra-config.json` in the root of your project with the following contents:\n\n```json\n{\n   \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n   \"sql_servers\": [\n      {\n         \"host\": \"my-db-host:5432\",\n         \"databases\": {\n            \"monitor\": {\n               \"username\": \"my-db-owner\",\n                \"password\": {\"$env\": \"DB_PASSWORD\"}\n            },\n            \"site\": {\n               \"username\": \"my-db-owner\",\n                \"password\": {\"$env\": \"DB_PASSWORD\"}\n            }\n         }\n      }\n   ]\n}\n```\n\nThe values in this configuration are just examples, you will need to replace them with the correct values for your database.\nTake a look at our guide for [deploying an Encore app with a PostgreSQL database to Digital Ocean](/docs/ts/self-host/deploy-digitalocean) for more information.\n\n🥐 Build a Docker image by running `encore build docker uptime:v1.0`.\n\nThis will compile your application using the host machine and then produce a Docker image containing the compiled application.\n\n🥐 Upload the Docker image to the cloud provider of your choice and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\nEncore Cloud provides automated infrastructure and DevOps. Deploy to a free development environment or to your own cloud account on AWS or GCP.\n\n### Create account\n\nBefore deploying with Encore Cloud, you need to have a free Encore Cloud account and link your app to the platform. If you already have an account, you can move on to the next step.\n\nIf you don’t have an account, the simplest way to get set up is by running `encore app create` and selecting **Y** when prompted to create a new account. Once your account is set up, continue creating a new app, selecting the `empty app` template.\n\nAfter creating the app, copy your project files into the new app directory, ensuring that you do not replace the `encore.app` file (this file holds a unique id which links your app to the platform).\n\n### Commit changes\n\nEncore comes with built-in CI/CD, and the deployment process is as simple as a `git push`. (You can also integrate with GitHub, learn more in the [CI/CD docs](/docs/platform/deploy/deploying).)\n\n🥐 Let's deploy your app to Encore's free development cloud by running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Initial commit'\n$ git push encore\n```\n\nEncore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.\n\nAfter triggering the deployment, you will see a URL where you can view its progress in the [Encore Cloud dashboard](https://app.encore.cloud). It will look something like: `https://app.encore.cloud/$APP_ID/deploys/...`\n\nFrom the Cloud Dashboard you can also see metrics, trigger Cron Jobs, see traces, and later connect your own AWS or GCP account to use for deployment.\n\n<video autoPlay playsInline loop controls muted className=\"w-full h-full\">\n  <source src=\"/assets/docs/webdashvideo.mp4\" className=\"w-full h-full\" type=\"video/mp4\"/>\n</video>\n\n🥐 When the deploy has finished, you can try out your uptime monitor by going to `https://staging-$APP_ID.encr.app`.\n\n*You now have an app running in the cloud, well done!*\n\n</Accordion>\n\n## 6. Publish Pub/Sub events when a site goes down\n\nHold on, an uptime monitoring system isn't very useful if it doesn't actually notify you when a site goes down.\n\nTo do so let's add a [Pub/Sub topic](https://encore.dev/docs/ts/primitives/pubsub) on which we'll publish a message every time a site transitions from being up to being down, or vice versa.\n\n🥐 Define the topic using Encore's Pub/Sub module in `monitor/check.ts`:\n\n```ts\n-- monitor/check.ts --\nimport { Subscription, Topic } from \"encore.dev/pubsub\";\n\n// TransitionEvent describes a transition of a monitored site\n// from up->down or from down->up.\nexport interface TransitionEvent {\n  site: Site; // Site is the monitored site in question.\n  up: boolean; // Up specifies whether the site is now up or down (the new value).\n}\n\n// TransitionTopic is a pubsub topic with transition events for when a monitored site\n// transitions from up->down or from down->up.\nexport const TransitionTopic = new Topic<TransitionEvent>(\"uptime-transition\", {\n  deliveryGuarantee: \"at-least-once\",\n});\n```\n\nNow let's publish a message on the `TransitionTopic` if a site's up/down\nstate differs from the previous measurement.\n\n🥐 Create a `getPreviousMeasurement` function to report the last up/down state:\n\n```ts\n-- monitor/check.ts --\n// getPreviousMeasurement reports whether the given site was\n// up or down in the previous measurement.\nasync function getPreviousMeasurement(siteID: number): Promise<boolean> {\n  const row = await MonitorDB.queryRow`\n      SELECT up\n      FROM checks\n      WHERE site_id = ${siteID}\n      ORDER BY checked_at DESC\n      LIMIT 1\n  `;\n  return row?.up ?? true;\n}\n```\n\n🥐 Now add a function to conditionally publish a message if the up/down state differs by modifying the `doCheck` function:\n\n```ts\n-- monitor/check.ts --\nasync function doCheck(site: Site): Promise<{ up: boolean }> {\n  const { up } = await ping({ url: site.url });\n\n  // Publish a Pub/Sub message if the site transitions\n  // from up->down or from down->up.\n  const wasUp = await getPreviousMeasurement(site.id);\n  if (up !== wasUp) {\n    await TransitionTopic.publish({ site, up });\n  }\n\n  await MonitorDB.exec`\n      INSERT INTO checks (site_id, up, checked_at)\n      VALUES (${site.id}, ${up}, NOW())\n  `;\n  return { up };\n}\n```\n\n🥐 Start your app again using `encore run` and open the Flow architecture diagram in the local development dashboard. Now you'll see the Pub/Sub topic as a black box, it should look like this:\n\n<img className=\"w-full h-auto\" src=\"/assets/docs/uptime_tut_flow_2.png\" title=\"Architecture diagram\" />\n\nNow the monitoring system will publish messages on the `TransitionTopic`\nwhenever a monitored site transitions from up->down or from down->up.\nIt doesn't know or care who actually listens to these messages.\n\nThe truth is right now nobody does. So let's fix that by adding\na Pub/Sub subscriber that posts these events to Slack.\n\n## 7. Send Slack notifications when a site goes down\n\n🥐 Start by creating a new service named `slack`:\n\n```shell\n$ mkdir slack # Create a new directory in the application root\n$ touch slack/encore.service.ts\n```\n\n```ts\n-- slack/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"slack\");\n```\n\n🥐 Add a `slack.ts` file containing the following:\n\n```ts\n-- slack/slack.ts --\nimport { api } from \"encore.dev/api\";\nimport { secret } from \"encore.dev/config\";\nimport log from \"encore.dev/log\";\n\nexport interface NotifyParams {\n  text: string; // the slack message to send\n}\n\n// Sends a Slack message to a pre-configured channel using a\n// Slack Incoming Webhook (see https://api.slack.com/messaging/webhooks).\nexport const notify = api<NotifyParams>({}, async ({ text }) => {\n  const url = webhookURL();\n  if (!url) {\n    log.info(\"no slack webhook url defined, skipping slack notification\");\n    return;\n  }\n\n  const resp = await fetch(url, {\n    method: \"POST\",\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ content: text }),\n  });\n  if (resp.status >= 400) {\n    const body = await resp.text();\n    throw new Error(`slack notification failed: ${resp.status}: ${body}`);\n  }\n});\n\n// SlackWebhookURL defines the Slack webhook URL to send uptime notifications to.\nconst webhookURL = secret(\"SlackWebhookURL\");\n```\n\n🥐 Now go to a Slack community of your choice where you have the permission to create a new Incoming Webhook.\n\n🥐 Once you have the Webhook URL, set it as an Encore secret:\n\n```shell\n$ encore secret set --type dev,local,pr SlackWebhookURL\nEnter secret value: *****\nSuccessfully updated development secret SlackWebhookURL.\n```\n\n🥐 Test the `slack.notify` endpoint by calling it via cURL:\n\n```shell\n$ curl 'http://localhost:4000/slack.notify' -d '{\"text\": \"Testing Slack webhook\"}'\n```\nYou should see the *Testing Slack webhook* message appear in the Slack channel you designated for the webhook.\n\n🥐 When it works it's time to add a Pub/Sub subscriber to automatically notify Slack when a monitored site goes up or down. Add the following:\n\n```ts\n-- slack/slack.ts --\nimport { Subscription } from \"encore.dev/pubsub\";\nimport { TransitionTopic } from \"../monitor/check\";\n\nconst _ = new Subscription(TransitionTopic, \"slack-notification\", {\n  handler: async (event) => {\n    const text = `*${event.site.url} is ${event.up ? \"back up.\" : \"down!\"}*`;\n    await notify({ text });\n  },\n});\n```\n\n## 8. Deploy your finished Uptime Monitor\n\nNow you're ready to deploy your finished Uptime Monitor, complete with a Slack integration.\n\n<Accordion>\n\n### Self-hosting\n\nBecause we have added more infrastructure to our app, we need to [update the configuration](/docs/ts/self-host/configure-infra) in our `infra-config.json` to include the new Pub/Sub topic and subscription as well as how we should set the  `SlackWebhookURL` secret. \n\n🥐 Update your `ìnfra-config.json` to reflect the new infrastructure.\n\n🥐 Build a Docker image by running `encore build docker uptime:v2.0`.\n\n🥐 Upload the Docker image to the cloud provider and run it.\n\n</Accordion>\n\n<Accordion>\n\n### Encore Cloud (free)\n\n🥐 As before, deploying your app to the cloud is as simple as running:\n\n```shell\n$ git add -A .\n$ git commit -m 'Add slack integration'\n$ git push encore\n```\n\n### Celebrate with fireworks\n\nNow that your app is running in the cloud, let's celebrate with some fireworks:\n\n🥐 In the Cloud Dashboard, open the Command Menu by pressing **Cmd + K** (Mac) or **Ctrl + K** (Windows/Linux).\n\n_From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints._\n\n🥐 Type `fireworks` in the Command Menu and press enter. Sit back and enjoy the show!\n\n![Fireworks](/assets/docs/fireworks.jpg)\n\n</Accordion>\n\n## Conclusion\n\nWe've now built a fully functioning uptime monitoring system.\n\nIf we may say so ourselves (and we may; it's our documentation after all)\nit's pretty remarkable how much we've accomplished in such little code:\n\n* We've built three different services (`site`, `monitor`, and `slack`)\n* We've added two databases (to the `site` and `monitor` services) for tracking monitored sites and the monitoring results\n* We've added a cron job for automatically checking the sites every hour\n* We've set up a Pub/Sub topic to decouple the monitoring system from the Slack notifications\n* We've added a Slack integration, using secrets to securely store the webhook URL, listening to a Pub/Sub subscription for up/down transition events\n\nAll of this in just a bit over 300 lines of code. It's time to lean back\nand take a sip of your favorite beverage, safe in the knowledge you'll\nnever be caught unaware of a website going down suddenly.\n"
  },
  {
    "path": "e2e-tests/README.md",
    "content": "# End to End Tests\n\nThis folder contains end to end tests for Encore, which test everything in Encore\nas an end user would use it.\n\nThe tests will:\n1. Effectively run `encore run` on the [echo test app](./testdata/echo)\n2. Perform some basic requests against the running app to verify behaviour\n3. Generate the front end clients for the app\n4. Run tests against using generated clients against the running app\n5. Shutdown the running app\n"
  },
  {
    "path": "e2e-tests/app_test.go",
    "content": "//go:build e2e\n\npackage tests\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t\"encr.dev/cli/daemon/pubsub\"\n\t\"encr.dev/cli/daemon/redis\"\n\t. \"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\t\"encr.dev/cli/daemon/secret\"\n\t. \"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/builder/builderimpl\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/svcproxy\"\n\t\"encr.dev/pkg/vcs\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype RunAppData struct {\n\tAddr  string\n\tRun   *Run\n\tMeta  *meta.Data\n\tNSQ   *pubsub.NSQDaemon\n\tRedis *redis.Server\n\tEnv   []string\n\n\tValues map[string]any // arbitrary values for use in testscripts\n}\n\nfunc RunApp(c testing.TB, appRoot string, logger RunLogger, env []string) *RunAppData {\n\tassertNil := func(err error) {\n\t\tif err != nil {\n\t\t\tc.Fatal(err)\n\t\t}\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:0\")\n\tassertNil(err)\n\tc.Cleanup(func() { _ = ln.Close() })\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tc.Cleanup(cancel)\n\n\tsvcProxy, err := svcproxy.New(ctx, log.Logger)\n\tassertNil(err)\n\n\t// Use a randomly generated app id to avoid tests trampling on each other\n\t// since we use a persistent working directory based on the app id.\n\tapp := apps.NewInstance(appRoot, uuid.Must(uuid.NewV4()).String(), \"\")\n\tbld := builderimpl.Resolve(app.Lang(), nil)\n\n\tmgr := &Manager{}\n\tns := &namespace.Namespace{ID: \"some-id\", Name: \"default\"}\n\trm := infra.NewResourceManager(app, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, ns, nil, 0, false)\n\trun := &Run{\n\t\tID:              GenID(),\n\t\tListenAddr:      ln.Addr().String(),\n\t\tSvcProxy:        svcProxy,\n\t\tApp:             app,\n\t\tResourceManager: rm,\n\t\tMgr:             mgr,\n\t\tBuilder:         bld,\n\t\tParams:          &StartParams{},\n\t}\n\n\tparse, build, configs := testBuild(c, appRoot, env)\n\n\tjobs := NewAsyncBuildJobs(ctx, app.PlatformOrLocalID(), nil)\n\trun.ResourceManager.StartRequiredServices(jobs, parse.Meta)\n\tc.Cleanup(rm.StopAll)\n\n\tassertNil(jobs.Wait())\n\n\tenv = append(env, \"FOO=bar\", \"BAR=baz\")\n\n\tif logger == nil {\n\t\tlogger = testRunLogger{c}\n\t}\n\n\texpSet, err := experiments.FromAppFileAndEnviron(nil, env)\n\tassertNil(err)\n\n\tsecrets := secret.New()\n\tsecretData, err := secrets.Load(app).Get(ctx, expSet)\n\tassertNil(err)\n\n\tp, err := run.StartProcGroup(&StartProcGroupParams{\n\t\tCtx:            ctx,\n\t\tOutputs:        build.Outputs,\n\t\tMeta:           parse.Meta,\n\t\tLogger:         logger,\n\t\tEnviron:        env,\n\t\tServiceConfigs: configs.Configs,\n\t\tExperiments:    expSet,\n\t\tSecrets:        secretData.Values,\n\t})\n\tassertNil(err)\n\tc.Cleanup(p.Close)\n\trun.StoreProc(p)\n\tfor serviceName, config := range configs.Configs {\n\t\tenv = append(env, fmt.Sprintf(\"%s=%s\", fmt.Sprintf(\"ENCORE_CFG_%s\", strings.ToUpper(serviceName)), base64.RawURLEncoding.EncodeToString([]byte(config))))\n\t}\n\n\t// start proxying TCP requests to the running application\n\tstartProxy(ctx, ln, http.HandlerFunc(p.ProxyReq))\n\n\t// wait for the service to come up\n\tb := backoff.NewExponentialBackOff()\n\tb.MaxElapsedTime = 10 * time.Second\n\terr = backoff.Retry(func() error {\n\t\tw := httptest.NewRecorder()\n\t\treq, err := http.NewRequestWithContext(ctx, \"GET\", \"http://localhost/__encore/healthz\", nil)\n\t\tassertNil(err)\n\n\t\tp.ProxyReq(w, req)\n\n\t\tif w.Result().StatusCode != http.StatusOK {\n\t\t\treturn fmt.Errorf(\"unexpected status: %s\", w.Result().Status)\n\t\t}\n\n\t\treturn nil\n\t}, b)\n\tassertNil(err)\n\n\treturn &RunAppData{\n\t\tAddr:   ln.Addr().String(),\n\t\tRun:    run,\n\t\tMeta:   parse.Meta,\n\t\tNSQ:    rm.GetPubSub(),\n\t\tRedis:  rm.GetRedis(),\n\t\tEnv:    env,\n\t\tValues: make(map[string]any),\n\t}\n}\n\nfunc RunTests(c testing.TB, appRoot string, stdout, stderr io.Writer, environ []string) error {\n\tmgr := &Manager{\n\t\tSecret:     secret.New(),\n\t\tClusterMgr: nil,\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tc.Cleanup(cancel)\n\n\t// Use a randomly generated app id to avoid tests trampling on each other\n\t// since we use a persistent working directory based on the app id.\n\tapp := apps.NewInstance(appRoot, uuid.Must(uuid.NewV4()).String(), \"\")\n\n\tvar args []string\n\tswitch app.Lang() {\n\tcase appfile.LangTS:\n\t\targs = []string{}\n\tcase appfile.LangGo:\n\t\tfallthrough\n\tdefault:\n\t\targs = []string{\"./...\"}\n\t}\n\n\tif app.Lang() == appfile.LangTS {\n\t\targs = []string{}\n\t}\n\terr := mgr.Test(ctx, TestParams{\n\t\tTestSpecParams: &TestSpecParams{\n\t\t\tApp:        app,\n\t\t\tWorkingDir: \".\",\n\t\t\tArgs:       args,\n\t\t\tEnviron:    environ,\n\t\t},\n\t\tStdout: stdout,\n\t\tStderr: stderr,\n\t})\n\treturn err\n}\n\nfunc getNodeJSPath() option.Option[string] {\n\tpath, err := exec.LookPath(\"node\")\n\tif err != nil {\n\t\treturn option.None[string]()\n\t}\n\treturn option.Some(filepath.Dir(path))\n}\n\n// testRunLogger implements runLogger by calling t.Log.\ntype testRunLogger struct {\n\tlog interface{ Log(args ...any) }\n}\n\nfunc (l testRunLogger) RunStdout(r *Run, line []byte) {\n\tline = bytes.TrimSuffix(line, []byte{'\\n'})\n\tl.log.Log(string(line))\n}\n\nfunc (l testRunLogger) RunStderr(r *Run, line []byte) {\n\tline = bytes.TrimSuffix(line, []byte{'\\n'})\n\tl.log.Log(string(line))\n}\n\nfunc startProxy(ctx context.Context, ln net.Listener, proxyHandler http.Handler) {\n\tsrv := &http.Server{Handler: proxyHandler}\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = srv.Close()\n\t}()\n\n\tgo func() { _ = srv.Serve(ln) }()\n}\n\n// testBuild is a helper that compiles the app situated at appRoot\n// and cleans up the build dir during test cleanup.\nfunc testBuild(t testing.TB, appRoot string, env []string) (*builder.ParseResult, *builder.CompileResult, *builder.ServiceConfigsResult) {\n\texpSet, err := experiments.FromAppFileAndEnviron(nil, env)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Use a randomly generated app id to avoid tests trampling on each other\n\t// since we use a persistent working directory based on the app id.\n\tapp := apps.NewInstance(appRoot, uuid.Must(uuid.NewV4()).String(), \"\")\n\n\tbld := builderimpl.Resolve(app.Lang(), expSet)\n\tdefer fns.CloseIgnore(bld)\n\tctx := context.Background()\n\n\tvcsRevision := vcs.GetRevision(app.Root())\n\tbuildInfo := builder.BuildInfo{\n\t\tBuildTags:          builder.LocalBuildTags,\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          builder.DebugModeDisabled,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           vcsRevision.Revision,\n\t\tUncommittedChanges: vcsRevision.Uncommitted,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      buildInfo,\n\t\tApp:        app,\n\t\tWorkingDir: \".\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tparse, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         app,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = bld.GenUserFacing(ctx, builder.GenUserFacingParams{\n\t\tBuild: buildInfo,\n\t\tApp:   app,\n\t\tParse: parse,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tconfigs, err := bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{\n\t\tParse: parse,\n\t\tCueMeta: &cueutil.Meta{\n\t\t\tAPIBaseURL: \"http://what?\",\n\t\t\tEnvName:    \"end_to_end_test\",\n\t\t\tEnvType:    cueutil.EnvType_Development,\n\t\t\tCloudType:  cueutil.CloudType_Local,\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuild, err := bld.Compile(ctx, builder.CompileParams{\n\t\tBuild:       buildInfo,\n\t\tApp:         app,\n\t\tParse:       parse,\n\t\tOpTracker:   nil,\n\t\tExperiments: expSet,\n\t\tWorkingDir:  \".\",\n\t})\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tt.Cleanup(func() {\n\t\tfor _, output := range build.Outputs {\n\t\t\t_ = os.RemoveAll(output.GetArtifactDir().ToIO())\n\t\t}\n\t})\n\treturn parse, build, configs\n}\n\nfunc runGoModTidy(dir string) error {\n\tcmd := exec.Command(\"go\", \"mod\", \"tidy\")\n\tcmd.Dir = dir\n\tcmd.Env = append(os.Environ(), \"GO111MODULE=on\")\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"go mod tidy failed: %s\", out)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "e2e-tests/echo_app_test.go",
    "content": "//go:build e2e\n\npackage tests\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rs/zerolog/log\"\n\t\"go.uber.org/goleak\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/cli/daemon/namespace\"\n\t. \"encr.dev/cli/daemon/run\"\n\t\"encr.dev/cli/daemon/run/infra\"\n\t. \"encr.dev/internal/optracker\"\n\t\"encr.dev/pkg/clientgen\"\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/golden\"\n\t\"encr.dev/pkg/svcproxy\"\n\t\"encr.dev/v2/v2builder\"\n)\n\ntype Data[K any, V any] struct {\n\tKey   K\n\tValue V\n}\n\ntype NonBasicRequest struct {\n\tStruct        Data[*Data[string, string], int]\n\tStructPtr     *Data[int, uint16]\n\tStructSlice   []*Data[string, string]\n\tStructMap     map[string]*Data[string, float32]\n\tStructMapPtr  *map[string]*Data[string, string]\n\tAnonStruct    struct{ AnonBird string }\n\tNamedStruct   *Data[string, float64] `json:\"formatted_nest\"`\n\tRawStruct     Data[[]string, []byte]\n\tUnusedRequest *NonBasicRequest\n}\n\ntype NonBasicResponse struct {\n\t// Body\n\tStruct       Data[*Data[string, string], int]\n\tStructPtr    *Data[int, uint16]\n\tStructSlice  []*Data[string, string]\n\tStructMap    map[string]*Data[string, float32]\n\tStructMapPtr *map[string]*Data[string, string]\n\tAnonStruct   struct{ AnonBird string }\n\tNamedStruct  *Data[string, float64] `json:\"formatted_nest\"`\n\tRawStruct    json.RawMessage\n\n\t// Query\n\tQueryString    string\n\tQueryNumber    int\n\tOptQueryString string\n\tOptQueryNumber int\n\n\t// Path\n\tPathString string\n\tPathInt    int\n\tPathWild   string\n\n\t// Auth\n\tAuthHeader string\n\tAuthQuery  []int\n}\n\n// TestEndToEndWithApp tests that (*app).startProc correctly starts Encore processes\n// for sending requests.\nfunc TestEndToEndWithApp(t *testing.T) {\n\tdoTestEndToEndWithApp(t, nil)\n}\n\nfunc doTestEndToEndWithApp(t *testing.T, env []string) {\n\tc := qt.New(t)\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tappRoot := filepath.Join(wd, \"testdata\", \"echo\")\n\tapp := RunApp(c, appRoot, nil, env)\n\trun := app.Run\n\n\t// Use golden to test that the generated clients are as expected for the echo test app\n\tfor lang, path := range map[clientgen.Lang]string{\n\t\tclientgen.LangGo:         \"golang/client/goclient.go\",\n\t\tclientgen.LangTypeScript: \"ts/client.ts\",\n\t\tclientgen.LangJavascript: \"js/client.js\",\n\t} {\n\t\tservices := clientgentypes.AllServices(app.Meta)\n\t\tclient, err := clientgen.Client(lang, \"slug\", app.Meta, services, clientgentypes.TagSet{}, clientgentypes.Options{})\n\t\tif err != nil {\n\t\t\tfmt.Println(err.Error())\n\t\t\tc.FailNow()\n\t\t}\n\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"Got an error generating the client for: %s\", lang))\n\n\t\tgolden.TestAgainst(c, filepath.Join(\"echo_client\", path), string(client))\n\t}\n\n\tc.Run(\"basic requests\", func(c *qt.C) {\n\t\t// Send a simple request\n\t\tc.Run(\"Send a simple request\", func(c *qt.C) {\n\t\t\tinput := Data[string, int]{\"hello\", 1}\n\t\t\tbody, err := json.Marshal(&input)\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/echo.Echo\", bytes.NewReader(body))\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, input)\n\t\t})\n\n\t\t// Send a pubsub\n\t\tc.Run(\"send a pubsub\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/echo.Publish\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\n\t\t\t// Wait a bit to allow the message to be consumed.\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\tstats, err := app.NSQ.Stats()\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\tc.Assert(len(stats.Producers), qt.Equals, 1)\n\t\t\tc.Assert(len(stats.Topics), qt.Equals, 1)\n\t\t\tc.Assert(stats.Topics[0].TopicName, qt.Equals, \"test\")\n\t\t\tc.Assert(len(stats.Topics[0].Channels), qt.Equals, 1)\n\t\t\tc.Assert(stats.Topics[0].Channels[0].RequeueCount == 0, qt.IsTrue)\n\t\t\tc.Assert(stats.Topics[0].Channels[0].Depth == 0, qt.IsTrue)\n\t\t})\n\n\t\t// Call an endpoint using an unsupported HTTP Method\n\t\tc.Run(\"unsupported HTTP Method\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.Echo\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 404)\n\t\t})\n\n\t\t// Send an empty request\n\t\tc.Run(\"empty request\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/echo.EmptyEcho\", bytes.NewReader([]byte(\"{}\")))\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\n\t\t\t\t\"NullPtr\": nil,\n\t\t\t\t\"Zero\":    Data[string, string]{},\n\t\t\t})\n\t\t})\n\n\t\t// Send a non-basic type request with path params, headers, query string and body\n\t\tc.Run(\"non-basic type request\", func(c *qt.C) {\n\t\t\tinput := NonBasicRequest{\n\t\t\t\tStruct:      Data[*Data[string, string], int]{&Data[string, string]{\"peacock\", \"duck\"}, 1},\n\t\t\t\tStructPtr:   &Data[int, uint16]{2, 3},\n\t\t\t\tStructSlice: []*Data[string, string]{{\"seagull\", \"penguin\"}, {\"penguin\", \"seagull\"}},\n\t\t\t\tStructMap: map[string]*Data[string, float32]{\n\t\t\t\t\t\"hawk\":      {\"hummingbird\", 18.5},\n\t\t\t\t\t\"albatross\": {\"magpie\", 13.2},\n\t\t\t\t},\n\t\t\t\tStructMapPtr: &map[string]*Data[string, string]{\n\t\t\t\t\t\"hornbill\": {\"bird-of-paradise\", \"cuckoo\"},\n\t\t\t\t\t\"turkey\":   {\"owl\", \"waxbill\"},\n\t\t\t\t},\n\t\t\t\tAnonStruct:    struct{ AnonBird string }{AnonBird: \"dove\"},\n\t\t\t\tNamedStruct:   &Data[string, float64]{\"pigeon\", 34.2},\n\t\t\t\tUnusedRequest: &NonBasicRequest{StructPtr: &Data[int, uint16]{43, 9}},\n\t\t\t\tRawStruct:     Data[[]string, []byte]{[]string{\"emu\", \"ostrich\"}, []byte{4, 4, 5}},\n\t\t\t}\n\n\t\t\toutput := NonBasicResponse{\n\t\t\t\tStruct:       input.Struct,\n\t\t\t\tStructPtr:    input.StructPtr,\n\t\t\t\tStructSlice:  input.StructSlice,\n\t\t\t\tStructMap:    input.StructMap,\n\t\t\t\tStructMapPtr: input.StructMapPtr,\n\t\t\t\tAnonStruct:   input.AnonStruct,\n\t\t\t\tNamedStruct:  input.NamedStruct,\n\t\t\t\tQueryString:  \"robin\",\n\t\t\t\tQueryNumber:  33,\n\t\t\t\tRawStruct:    json.RawMessage(`{\"Key\": [\"emu\", \"ostrich\"], \"Value\": \"BAQF\"}`),\n\t\t\t\tPathString:   \"shoebill\",\n\t\t\t\tPathInt:      55,\n\t\t\t\tPathWild:     \"toucan/crane/vulture/78/\",\n\t\t\t\tAuthHeader:   \"header\",\n\t\t\t\tAuthQuery:    []int{5, 6},\n\t\t\t}\n\t\t\tbody, err := json.Marshal(&input)\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/NonBasicEcho/shoebill/55/toucan/crane/vulture/78/?string=robin&no=33&query=5&query=6\", bytes.NewReader(body))\n\t\t\treq.Header.Add(\"X-Header\", \"header\")\n\t\t\treq.Header.Add(\"X-Header-String\", \"starling\")\n\t\t\treq.Header.Add(\"X-Header-Number\", \"10\")\n\t\t\treq.Header.Add(\"Authorization\", \"Bearer tokendata\")\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Header().Get(\"X-Header-String\"), qt.Equals, \"starling\")\n\t\t\tc.Assert(w.Header().Get(\"X-Header-Number\"), qt.Equals, \"10\")\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, output)\n\t\t})\n\n\t\t// Send a request with only header parameters\n\t\tc.Run(\"only headers\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.HeadersEcho\", nil)\n\t\t\treq.Header.Add(\"x-int\", \"1\")\n\t\t\treq.Header.Add(\"x-string\", \"nightingale\")\n\t\t\treq.Header.Add(\"X-StringSlice\", \"mynah, quail, weaver\")\n\t\t\treq.Header.Add(\"X-StringSlice\", \"pewit\")\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Header().Get(\"X-Int\"), qt.Equals, \"1\")\n\t\t\tc.Assert(w.Header().Get(\"X-String\"), qt.Equals, \"nightingale\")\n\t\t})\n\n\t\t// Send POST and GET requests to the same endpoint with an assortment of basic types\n\t\tc.Run(\"POST and GET\", func(c *qt.C) {\n\t\t\tinput := map[string]any{\n\t\t\t\t\"string\":       \"string\",\n\t\t\t\t\"uint\":         1,\n\t\t\t\t\"int\":          2,\n\t\t\t\t\"int8\":         -3,\n\t\t\t\t\"int64\":        4,\n\t\t\t\t\"float32\":      5,\n\t\t\t\t\"float64\":      6,\n\t\t\t\t\"string_slice\": []any{\"slice1\", \"slice2\"},\n\t\t\t\t\"int_slice\":    []any{1, 2, 3},\n\t\t\t\t\"time\":         \"2016-01-02T15:04:05+07:00\",\n\t\t\t}\n\t\t\toutput := map[string]any{\n\t\t\t\t\"String\":      \"string\",\n\t\t\t\t\"Uint\":        1,\n\t\t\t\t\"Int\":         2,\n\t\t\t\t\"Int8\":        -3,\n\t\t\t\t\"Int64\":       4,\n\t\t\t\t\"Float32\":     5,\n\t\t\t\t\"Float64\":     6,\n\t\t\t\t\"StringSlice\": []any{\"slice1\", \"slice2\"},\n\t\t\t\t\"IntSlice\":    []any{1, 2, 3},\n\t\t\t\t\"Time\":        \"2016-01-02T15:04:05+07:00\",\n\t\t\t}\n\t\t\tqs := \"\"\n\t\t\tfor k, av := range input {\n\t\t\t\tvs := []any{av}\n\t\t\t\tswitch av.(type) {\n\t\t\t\tcase []any:\n\t\t\t\t\tvs = av.([]any)\n\t\t\t\t}\n\t\t\t\tfor _, v := range vs {\n\t\t\t\t\tif len(qs) > 0 {\n\t\t\t\t\t\tqs += \"&\"\n\t\t\t\t\t}\n\t\t\t\t\tvalue := url.QueryEscape(fmt.Sprintf(\"%v\", v))\n\t\t\t\t\tqs += fmt.Sprintf(\"%s=%v\", strings.ToLower(k), value)\n\t\t\t\t}\n\t\t\t}\n\t\t\tbody, err := json.Marshal(&output)\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tw2 := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.BasicEcho?\"+qs, nil)\n\t\t\treq2 := httptest.NewRequest(\"POST\", \"/echo.BasicEcho\", bytes.NewReader(body))\n\t\t\trun.ServeHTTP(w, req)\n\t\t\trun.ServeHTTP(w2, req2)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, output)\n\t\t\tc.Assert(w2.Body.Bytes(), qt.DeepEquals, w.Body.Bytes())\n\t\t})\n\n\t\t// Call an endpoint without request parameters, returning nil\n\t\tc.Run(\"without request\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.NilResponse\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t})\n\n\t\t// Call an endpoint with an invalid auth parameter\n\t\tc.Run(\"invalid parameter\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.NilResponse\", nil)\n\t\t\treq.Header.Add(\"x-auth-int\", \"invalid\")\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 400)\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\n\t\t\t\t\"code\":    \"invalid_argument\",\n\t\t\t\t\"details\": nil,\n\t\t\t\t\"message\": \"invalid auth param: x-auth-int: invalid parameter: strconv.ParseInt: parsing \\\"invalid\\\": invalid syntax\",\n\t\t\t})\n\t\t})\n\n\t\t// Call an endpoint without request parameters and response value\n\t\tc.Run(\"without response\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.Noop\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t})\n\n\t\t// Call an endpoint with request parameters but no response value\n\t\tc.Run(\"only request\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.MuteEcho?key=pelican&value=cocabura\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t})\n\n\t\t// Call an endpoint with a response value but no request parameters\n\t\tc.Run(\"no request, response\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.Pong\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, Data[string, string]{\"woodpecker\", \"kingfisher\"})\n\t\t})\n\n\t\t// Call endpoint with custom http status in response\n\t\tc.Run(\"custom http status response\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.CustomHTTPStatus\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 201)\n\t\t})\n\n\t\t// Call the env endpoint and make sure we get our env variables back\n\t\t{\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.Env\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\n\t\t\tfilteredEnv := make([]string, 0, len(app.Env))\n\t\t\tfor _, env := range app.Env {\n\t\t\t\tif !strings.HasPrefix(env, \"ENCORE_\") {\n\t\t\t\t\tfilteredEnv = append(filteredEnv, env)\n\t\t\t\t}\n\t\t\t}\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string][]string{\"Env\": filteredEnv})\n\t\t}\n\n\t\t// Call the app metadata endpoint and make sure we get correct data back\n\t\tc.Run(\"app metadata\", func(c *qt.C) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.AppMeta\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\n\t\t\t// we need to extract the API Base URL as it will change due to the `RuntimePort: 0` above\n\t\t\tbytes := w.Body.Bytes()\n\t\t\tgot := make(map[string]string)\n\t\t\t_ = json.Unmarshal(bytes, &got)\n\n\t\t\tc.Assert(strings.HasPrefix(got[\"APIBaseURL\"], \"http://\"), qt.IsTrue)\n\t\t\tc.Assert(bytes, qt.JSONEquals, map[string]interface{}{\n\t\t\t\t\"AppID\":      \"\",\n\t\t\t\t\"APIBaseURL\": got[\"APIBaseURL\"],\n\t\t\t\t\"EnvName\":    \"local\",\n\t\t\t\t\"EnvType\":    \"development\",\n\t\t\t})\n\t\t})\n\n\t\t// Try the dependency injection services\n\t\tc.Run(\"dependency_injection\", func(c *qt.C) {\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/di/one\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.HasLen, 0)\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/di/two\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]string{\"Msg\": \"Hello World\"})\n\t\t\t}\n\n\t\t})\n\n\t\tc.Run(\"cache\", func(c *qt.C) {\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/incr/one\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Val\": 1})\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/incr/one\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Val\": 2})\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/incr/two\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Val\": 1})\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"POST\", \"/cache/struct/1/foo\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.HasLen, 0)\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/struct/1\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Val\": \"foo\"})\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/list/1\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Vals\": []string{}})\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"POST\", \"/cache/list/1/foo\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.HasLen, 0)\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/list/1\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Vals\": []string{\"foo\"}})\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"POST\", \"/cache/list/1/bar\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.HasLen, 0)\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\treq := httptest.NewRequest(\"GET\", \"/cache/list/1\", nil)\n\t\t\t\trun.ServeHTTP(w, req)\n\t\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\"Vals\": []string{\"foo\", \"bar\"}})\n\t\t\t}\n\n\t\t\tkeys := app.Redis.Miniredis().Keys()\n\t\t\tc.Assert(keys, qt.DeepEquals, []string{\n\t\t\t\t\"int/one\",\n\t\t\t\t\"int/two\",\n\t\t\t\t\"list/1/foo/1\",\n\t\t\t\t\"struct/1/dummy/x\",\n\t\t\t})\n\t\t})\n\t})\n\n\tc.Run(\"generated_wrappers_for_intra_service_calls\", func(c *qt.C) {\n\t\t// Send a simple request\n\t\t{\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/generated-wrappers-end-to-end-test\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t}\n\t})\n\n\tc.Run(\"config_test\", func(c *qt.C) {\n\t\t{\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/echo.ConfigValues\", nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]any{\n\t\t\t\t\"ReadOnlyMode\": true,\n\t\t\t\t\"PublicKey\":    \"aGVsbG8gd29ybGQK\",\n\t\t\t\t\"AdminUsers\":   []string{\"foo\", \"bar\"},\n\t\t\t\t\"SubKeyCount\":  2,\n\t\t\t})\n\t\t}\n\t})\n\n\tc.Run(\"go_generated_client\", func(c *qt.C) {\n\t\tencoreGoroot := os.Getenv(\"ENCORE_GOROOT\")\n\t\tc.Assert(encoreGoroot, qt.Not(qt.Equals), \"\")\n\t\tgoPath := filepath.Join(encoreGoroot, \"bin\", \"go\")\n\t\tcmd := exec.Command(goPath, \"run\", \".\", app.Addr)\n\t\tcmd.Dir = filepath.Join(\"testdata\", \"echo_client\")\n\t\tcmd.Env = append(os.Environ(),\n\t\t\t\"GOROOT=\"+encoreGoroot,\n\t\t\t\"PATH=\"+fmt.Sprintf(\"%s%s%s\", filepath.Join(encoreGoroot, \"/bin\"), string(filepath.ListSeparator), os.Getenv(\"PATH\")),\n\t\t)\n\n\t\tout, err := cmd.CombinedOutput()\n\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"Got error running generated Go client: %s\", out))\n\t})\n\n\tc.Run(\"typescript_generated_client\", func(c *qt.C) {\n\t\tnpmCommandsToRun := [][]string{\n\t\t\t{\"install\", \"--prefer-offline\", \"--no-audit\"},\n\t\t\t{\"run\", \"lint\"},\n\t\t\t{\"run\", \"test\", \"--\", app.Addr},\n\t\t}\n\n\t\tfor _, args := range npmCommandsToRun {\n\t\t\tcmd := exec.Command(\"npm\", args...)\n\t\t\tcmd.Dir = filepath.Join(\"testdata\", \"echo_client\")\n\n\t\t\tout, err := cmd.CombinedOutput()\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"Got error running generated Typescript client: %s\", out))\n\t\t}\n\t})\n\n\tc.Run(\"javascript_generated_client\", func(c *qt.C) {\n\t\tnpmCommandsToRun := [][]string{\n\t\t\t{\"install\", \"--prefer-offline\", \"--no-audit\"},\n\t\t\t{\"run\", \"lint\"},\n\t\t\t{\"run\", \"test:js\", \"--\", app.Addr},\n\t\t}\n\n\t\tfor _, args := range npmCommandsToRun {\n\t\t\tcmd := exec.Command(\"npm\", args...)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\t\t\tcmd.Stdin = os.Stdin\n\t\t\tcmd.Dir = filepath.Join(\"testdata\", \"echo_client\")\n\n\t\t\tc.Assert(cmd.Run(), qt.IsNil, qt.Commentf(\"Got error running generated JavaScript client\"))\n\t\t}\n\t})\n}\n\n// TestProcClosedOnCtxCancel tests that the proc is closed when\n// the given ctx is cancelled.\nfunc TestProcClosedOnCtxCancel(t *testing.T) {\n\tdefer goleak.VerifyNone(t, goleak.IgnoreCurrent())\n\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tappRoot := filepath.Join(wd, \"testdata\", \"echo\")\n\n\tapp := apps.NewInstance(appRoot, \"local_id\", \"platform_id\")\n\n\tc := qt.New(t)\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tsvcProxy, err := svcproxy.New(ctx, log.Logger)\n\tc.Assert(err, qt.IsNil)\n\n\tmgr := &Manager{}\n\tns := &namespace.Namespace{ID: \"some-id\", Name: \"default\"}\n\trm := infra.NewResourceManager(app, nil, nil, nil, ns, nil, 0, false)\n\trun := &Run{\n\t\tID:              GenID(),\n\t\tApp:             app,\n\t\tMgr:             mgr,\n\t\tResourceManager: rm,\n\t\tListenAddr:      \"127.0.0.1:34212\",\n\t\tSvcProxy:        svcProxy,\n\t\tBuilder:         v2builder.New(),\n\t\tParams:          &StartParams{},\n\t}\n\n\tparse, build, _ := testBuild(c, appRoot, append(os.Environ(), \"ENCORE_EXPERIMENT=v2\"))\n\tjobs := NewAsyncBuildJobs(ctx, app.PlatformOrLocalID(), nil)\n\trun.ResourceManager.StartRequiredServices(jobs, parse.Meta)\n\tdefer run.Close()\n\n\tc.Assert(jobs.Wait(), qt.IsNil)\n\n\tp, err := run.StartProcGroup(&StartProcGroupParams{\n\t\tCtx:     ctx,\n\t\tOutputs: build.Outputs,\n\t\tMeta:    parse.Meta,\n\t\tLogger:  testRunLogger{t},\n\t\tEnviron: os.Environ(),\n\t})\n\tc.Assert(err, qt.IsNil)\n\tcancel()\n\t<-p.Done()\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/.gitignore",
    "content": "encore.gen.go\n/.encore\nencore.gen.cue\n/encore.gen\n"
  },
  {
    "path": "e2e-tests/testdata/echo/cache/cache.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/storage/cache\"\n)\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{\n\tEvictionPolicy: cache.AllKeysLFU,\n})\n\nvar ints = cache.NewIntKeyspace[string](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"int/:key\",\n})\n\ntype IncrResponse struct {\n\tVal int64\n}\n\n//encore:api public path=/cache/incr/:key\nfunc Incr(ctx context.Context, key string) (*IncrResponse, error) {\n\tval, err := ints.Increment(ctx, key, 1)\n\treturn &IncrResponse{Val: val}, err\n}\n\ntype StructKey struct {\n\tKey   int\n\tDummy string\n}\n\ntype StructVal struct {\n\tVal string\n}\n\nvar structs = cache.NewStructKeyspace[StructKey, StructVal](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"struct/:Key/dummy/:Dummy\",\n})\n\n//encore:api public method=POST path=/cache/struct/:key/:val\nfunc PostStruct(ctx context.Context, key int, val string) error {\n\terr := structs.Set(ctx, StructKey{Key: key, Dummy: \"x\"}, StructVal{Val: val})\n\treturn err\n}\n\n//encore:api public method=GET path=/cache/struct/:key\nfunc GetStruct(ctx context.Context, key int) (StructVal, error) {\n\tval, err := structs.Get(ctx, StructKey{Key: key, Dummy: \"x\"})\n\tif err == cache.Miss {\n\t\treturn StructVal{}, &errs.Error{Code: errs.NotFound}\n\t} else if err != nil {\n\t\treturn StructVal{}, err\n\t}\n\treturn val, nil\n}\n\nvar lists = cache.NewListKeyspace[int, string](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"list/:key/foo/:key\",\n})\n\n//encore:api public method=POST path=/cache/list/:key/:val\nfunc PostList(ctx context.Context, key int, val string) error {\n\t_, err := lists.PushRight(ctx, key, val)\n\treturn err\n}\n\ntype ListResponse struct {\n\tVals []string\n}\n\n//encore:api public method=GET path=/cache/list/:key\nfunc GetList(ctx context.Context, key int) (ListResponse, error) {\n\tvals, err := lists.Items(ctx, key)\n\treturn ListResponse{vals}, err\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/di/di.go",
    "content": "package di\n\nimport (\n\t\"archive/zip\"\n\t\"context\"\n\t\"database/sql\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"encore.dev/cron\"\n)\n\ntype TwoResponse struct {\n\tMsg string\n}\n\nvar _ = cron.NewJob(\"repeating-one\", cron.JobConfig{\n\tTitle:    \"Call One every 2 hours\",\n\tEvery:    2 * cron.Hour,\n\tEndpoint: One,\n})\n\n//encore:service\ntype Service struct {\n\tMsg string\n\n\t// Include various types to make sure the parser doesn't complain.\n\tmu   sync.Mutex\n\tonce *sync.Once\n\tdb   *sql.DB\n\tfn   func() *zip.Writer\n}\n\n//encore:api public path=/di/one\nfunc (s *Service) One(ctx context.Context) error {\n\treturn nil\n}\n\n//encore:api public path=/di/two\nfunc (s *Service) Two(ctx context.Context) (*TwoResponse, error) {\n\treturn &TwoResponse{Msg: s.Msg}, nil\n}\n\n//encore:api public raw path=/di/raw\nfunc (s *Service) Three(w http.ResponseWriter, req *http.Request) {\n\tio.Copy(w, req.Body)\n}\n\nfunc initService() (*Service, error) {\n\treturn &Service{Msg: \"Hello World\"}, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/echo/config.cue",
    "content": "import \"encoding/base64\"\n\nReadOnlyMode: true\nPublicKey: base64.Decode(null, \"aGVsbG8gd29ybGQK\") // \"hello world\" in Base64\n\nSubConfig: SubKey: MaxCount: [\n\tif #Meta.Environment.Type  == \"test\"  { 3 },\n\tif #Meta.Environment.Cloud == \"local\" { 2 },\n\t1\n][0]\n\nAdminUsers: [\n\t\"foo\",\n\t\"bar\",\n]\n"
  },
  {
    "path": "e2e-tests/testdata/echo/echo/config.go",
    "content": "package echo\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype CfgType[T any] struct {\n\tReadOnlyMode config.Bool\n\tPublicKey    config.Bytes\n\tAdminUsers   config.Values[string]\n\n\tSubConfig config.Value[struct {\n\t\tSubKey *SubCfgType[T]\n\t}]\n\n\tCurrencies map[string]struct {\n\t\tName    config.String\n\t\tCode    config.String\n\t\tAliases config.Values[string]\n\t}\n\n\tAnotherList config.Values[struct {\n\t\tName config.String\n\t}]\n}\n\ntype SubCfgType[T any] struct {\n\tMaxCount T\n}\n\nvar cfg = config.Load[*CfgType[uint]]()\n\ntype ConfigResponse struct {\n\tReadOnlyMode bool\n\tPublicKey    []byte\n\tSubKeyCount  uint\n\tAdminUsers   []string\n}\n\n//encore:api public\nfunc ConfigValues(ctx context.Context) (ConfigResponse, error) {\n\treturn ConfigResponse{\n\t\tReadOnlyMode: cfg.ReadOnlyMode(),\n\t\tPublicKey:    cfg.PublicKey(),\n\t\tAdminUsers:   cfg.AdminUsers(),\n\t\tSubKeyCount:  cfg.SubConfig().SubKey.MaxCount,\n\t}, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/echo/config_test.go",
    "content": "package echo\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\nfunc TestConfigValues(t *testing.T) {\n\tvalues, err := ConfigValues(context.Background())\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tif values.SubKeyCount != 3 {\n\t\tt.Fatalf(\"expected 1, got %d\", values.SubKeyCount)\n\t}\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/echo/echo.go",
    "content": "package echo\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"time\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/pubsub\"\n)\n\ntype Message struct {\n\tAttr    string `pubsub-attr:\"attr\"`\n\tSubject string\n\tBody    string\n}\n\nvar Topic = pubsub.NewTopic[*Message](\n\t\"test\",\n\tpubsub.TopicConfig{\n\t\tDeliveryGuarantee: pubsub.AtLeastOnce,\n\t},\n)\n\nvar _ = pubsub.NewSubscription(\n\tTopic, \"test\",\n\tpubsub.SubscriptionConfig[*Message]{\n\t\tHandler: Consumer,\n\t},\n)\n\ntype Data[K any, V any] struct {\n\tKey   K\n\tValue V\n}\n\ntype NonBasicData struct {\n\t// Header\n\tHeaderString string `header:\"X-Header-String\"`\n\tHeaderNumber int    `header:\"X-Header-Number\"`\n\n\t// Body\n\tStruct       Data[*Data[string, string], int]\n\tStructPtr    *Data[int, uint16]\n\tStructSlice  []*Data[string, string]\n\tStructMap    map[string]*Data[string, float32]\n\tStructMapPtr *map[string]*Data[string, string]\n\tAnonStruct   struct{ AnonBird string }\n\tNamedStruct  *Data[string, float64] `json:\"formatted_nest\"`\n\tRawStruct    json.RawMessage\n\n\t// Query\n\tQueryString    string `query:\"string\"`\n\tQueryNumber    int    `query:\"no\"`\n\tOptQueryNumber int    `query:\"optnum\" encore:\"optional\"`\n\tOptQueryString string `query:\"optstr\" encore:\"optional\"`\n\n\t// Path Parameters\n\tPathString string\n\tPathInt    int\n\tPathWild   string\n\n\t// Auth Parameters\n\tAuthHeader string\n\tAuthQuery  []int\n\n\t// Unexported fields\n\tunexported string\n}\n\ntype EmptyData struct {\n\tOmitEmpty Data[string, string] `json:\"OmitEmpty,omitempty\"`\n\tNullPtr   *string\n\tZero      Data[string, string]\n}\n\ntype BasicData struct {\n\tString      string\n\tUint        uint\n\tInt         int\n\tInt8        int8\n\tInt64       int64\n\tFloat32     float32\n\tFloat64     float64\n\tStringSlice []string\n\tIntSlice    []int\n\tTime        time.Time\n}\n\ntype HeadersData struct {\n\tInt    int    `header:\"X-Int\"`\n\tString string `header:\"X-String\"`\n}\n\n// Publish publishes a request on a topic\n//\n//encore:api public\nfunc Publish(ctx context.Context) error {\n\tid, err := Topic.Publish(ctx, &Message{\n\t\tAttr:    \"Attr\",\n\t\tSubject: \"subject\",\n\t\tBody:    \"body\",\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tfmt.Printf(\"Published: %s\\n\", id)\n\treturn nil\n}\n\n//encore:api private\nfunc Consumer(ctx context.Context, msg *Message) error {\n\tif msg.Attr != \"Attr\" {\n\t\treturn errors.New(\"incorrect Attr value\")\n\t}\n\tif msg.Subject != \"subject\" {\n\t\treturn errors.New(\"incorrect Subject value\")\n\t}\n\tif msg.Body != \"body\" {\n\t\treturn errors.New(\"incorrect Body value\")\n\t}\n\treturn nil\n}\n\n// Echo echoes back the request data.\n//\n//encore:api public\nfunc Echo(ctx context.Context, params *Data[string, int]) (*Data[string, int], error) {\n\treturn params, nil\n}\n\n// EmptyEcho echoes back the request data.\n//\n//encore:api public\nfunc EmptyEcho(ctx context.Context, params EmptyData) (EmptyData, error) {\n\treturn params, nil\n}\n\n// NonBasicEcho echoes back the request data.\n//\n//encore:api auth path=/NonBasicEcho/:pathString/:pathInt/*pathWild\nfunc NonBasicEcho(ctx context.Context, pathString string, pathInt int, pathWild string, params *NonBasicData) (*NonBasicData, error) {\n\tdata := auth.Data().(*AuthParams)\n\tparams.PathString = pathString\n\tparams.PathInt = pathInt\n\tparams.PathWild = pathWild\n\tparams.AuthQuery = data.Query\n\tparams.AuthHeader = data.Header\n\treturn params, nil\n}\n\n// BasicEcho echoes back the request data.\n//\n//encore:api public method=GET,POST\nfunc BasicEcho(ctx context.Context, params *BasicData) (*BasicData, error) {\n\treturn params, nil\n}\n\n// HeadersEcho echoes back the request headers\n//\n//encore:api public method=GET,POST\nfunc HeadersEcho(ctx context.Context, params *HeadersData) (*HeadersData, error) {\n\treturn params, nil\n}\n\n// Noop does nothing\n//\n//encore:api public method=GET\nfunc Noop(ctx context.Context) error {\n\treturn nil\n}\n\n// NilResponse returns a nil response and nil error\n//\n//encore:api public method=GET,POST\nfunc NilResponse(ctx context.Context) (*BasicData, error) {\n\treturn nil, nil\n}\n\n// MuteEcho absorbs a request\n//\n//encore:api public method=GET\nfunc MuteEcho(ctx context.Context, params Data[string, string]) error {\n\tlog.Printf(\"Absorbing %v\\n\", params)\n\treturn nil\n}\n\n// Pong returns a bird tuple\n//\n//encore:api public method=GET\nfunc Pong(ctx context.Context) (Data[string, string], error) {\n\treturn Data[string, string]{\"woodpecker\", \"kingfisher\"}, nil\n}\n\n// HTTPStatusResponse demonstrates encore:\"httpstatus\" tag functionality\ntype HTTPStatusResponse struct {\n\tMessage string `json:\"message\"`\n\tStatus  int    `encore:\"httpstatus\"`\n}\n\n// CustomHTTPStatus allows testing of custom HTTP status codes via encore:\"httpstatus\" tag\n//\n//encore:api public\nfunc CustomHTTPStatus(ctx context.Context) (*HTTPStatusResponse, error) {\n\treturn &HTTPStatusResponse{\n\t\tMessage: \"Created successfully\",\n\t\tStatus:  201, // HTTP 201 Created\n\t}, nil\n}\n\ntype EnvResponse struct {\n\tEnv []string\n}\n\n// Env returns the environment.\n//\n//encore:api public\nfunc Env(ctx context.Context) (*EnvResponse, error) {\n\treturn &EnvResponse{Env: os.Environ()}, nil\n}\n\ntype AppMetadata struct {\n\tAppID      string\n\tAPIBaseURL string\n\tEnvName    string\n\tEnvType    string\n}\n\n// AppMeta returns app metadata.\n//\n//encore:api public\nfunc AppMeta(ctx context.Context) (*AppMetadata, error) {\n\tmd := encore.Meta()\n\treturn &AppMetadata{\n\t\tAppID:      md.AppID,\n\t\tAPIBaseURL: md.APIBaseURL.String(),\n\t\tEnvName:    md.Environment.Name,\n\t\tEnvType:    string(md.Environment.Type),\n\t}, nil\n}\n\ntype AuthParams struct {\n\tHeader        string `header:\"X-Header\"`\n\tAuthInt       int    `header:\"X-Auth-Int\"`\n\tAuthorization string `header:\"Authorization\"`\n\tQuery         []int  `query:\"query\"`\n\tNewAuth       bool   `query:\"new-auth\"`\n}\n\nfunc (p *AuthParams) Validate() error {\n\tif p.Header == \"fail-validation\" {\n\t\treturn errors.New(\"auth validation fail\")\n\t}\n\treturn nil\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, params *AuthParams) (auth.UID, *AuthParams, error) {\n\tif reflect.ValueOf(params).Elem().IsZero() {\n\t\tpanic(\"zero value auth params should skip authhandler\")\n\t}\n\tif params.Authorization == \"Bearer tokendata\" && params.NewAuth == false {\n\t\treturn \"user\", params, nil\n\t}\n\n\t// Check headers and query strings work by adding them together to calculate the answer in the header field\n\tif params.Header != \"\" && params.NewAuth {\n\t\tans := 0\n\t\tfor _, v := range params.Query {\n\t\t\tans += v\n\t\t}\n\n\t\tif strconv.FormatInt(int64(ans), 10) == params.Header {\n\t\t\treturn \"second_user\", params, nil\n\t\t}\n\t}\n\n\treturn \"\", nil, &errs.Error{\n\t\tCode:    errs.Unauthenticated,\n\t\tMessage: \"invalid token\",\n\t}\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/echo/echo_test.go",
    "content": "package echo\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\n// TestEnvsProvided tests that 'go test' was invoked with the envs we expect.\nfunc TestEnvsProvided(t *testing.T) {\n\tenvs := os.Environ()\n\twant := map[string]string{\n\t\t\"FOO\": \"bar\",\n\t\t\"BAR\": \"baz\",\n\t}\n\tfor _, env := range envs {\n\t\tkey, val, _ := strings.Cut(env, \"=\")\n\t\tif wantVal, ok := want[key]; ok {\n\t\t\tif val != wantVal {\n\t\t\t\tt.Errorf(\"got env %s=%s, want value %s\", key, val, wantVal)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/empty_cfg/service.go",
    "content": "package emptycfg\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype cfg struct {\n\n}\n\nvar Config = config.Load[*cfg]()\n\n//encore:api public\nfunc AnAPI(ctx context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/encore.app",
    "content": "{}"
  },
  {
    "path": "e2e-tests/testdata/echo/endtoend/endtoend.go",
    "content": "package endtoend\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\n\t// \"net/http\"\n\t// \"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\n\t// \"strings\"\n\t\"time\"\n\n\t\"encore.app/test\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/rlog\"\n\t\"encore.dev/types/option\"\n\t\"encore.dev/types/uuid\"\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nvar assertNumber = 0\n\n//encore:api public method=GET path=/generated-wrappers-end-to-end-test\nfunc GeneratedWrappersEndToEndTest(ctx context.Context) (err error) {\n\trlog.Info(\"Starting end-to-end test of generated wrappers\")\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\trlog.Error(\"Panic occured during end to end test\", \"err\", r)\n\t\t\terr = fmt.Errorf(\"%v\", r)\n\t\t}\n\t}()\n\n\t// Even on a slow machine, the client should be able to connect and run this test script in 30 seconds\n\tctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n\tdefer cancel()\n\n\t// Test a simple no-op\n\terr = test.Noop(ctx)\n\tassert(err, nil, \"Wanted no error from noop\")\n\n\t// Test we get back the right structured error\n\terr = test.NoopWithError(ctx)\n\tassertStructuredError(err, errs.Unimplemented, \"totally not implemented yet\")\n\n\t// Test a simple echo\n\techoRsp, err := test.SimpleBodyEcho(ctx, &test.BodyEcho{Message: \"hello world\"})\n\tassert(err, nil, \"Wanted no error from simple body echo\")\n\tassert(echoRsp.Message, \"hello world\", \"Wanted body to be 'hello world'\")\n\n\t// Check our UpdateMessage and GetMessage API's\n\tgetRsp, err := test.GetMessage(ctx, \"intra-service wrapper\")\n\tassert(err, nil, \"Wanted no error from get message\")\n\tassert(getRsp.Message, \"\", \"Expected no message on first request\")\n\n\terr = test.UpdateMessage(ctx, \"intra-service wrapper\", &test.BodyEcho{Message: \"updating now\"})\n\tassert(err, nil, \"Wanted no error from update message\")\n\n\tgetRsp, err = test.GetMessage(ctx, \"intra-service wrapper\")\n\tassert(err, nil, \"Wanted no error from get message\")\n\tassert(getRsp.Message, \"updating now\", \"Expected data from Update request\")\n\n\t// Test the rest API which uses all input types (query string, json body and header fields)\n\t// as well as nested structs and path segments in the URL\n\trestRsp, err := test.RestStyleAPI(ctx, 5, \"hello\", &test.RestParams{\n\t\tHeaderValue: \"this is the header field\",\n\t\tQueryValue:  \"this is a query string field\",\n\t\tBodyValue:   \"this is the body field\",\n\t\tNested: struct {\n\t\t\tKey   string `json:\"Alice\"`\n\t\t\tValue int    `json:\"bOb\"`\n\t\t\tOk    bool   `json:\"charile\"`\n\t\t}{\n\t\t\tKey:   \"the nested key\",\n\t\t\tValue: 8,\n\t\t\tOk:    true,\n\t\t},\n\t})\n\tassert(err, nil, \"Wanted no error from rest style api\")\n\tassert(restRsp.HeaderValue, \"this is the header field\", \"expected header value\")\n\tassert(restRsp.QueryValue, \"this is a query string field\", \"expected query value\")\n\tassert(restRsp.BodyValue, \"this is the body field\", \"expected body value\")\n\tassert(restRsp.Nested.Key, \"hello + the nested key\", \"expected nested key\")\n\tassert(restRsp.Nested.Value, 5+8, \"expected nested value\")\n\tassert(restRsp.Nested.Ok, true, \"expected nested ok\")\n\n\t// Full marshalling test with randomised payloads\n\tr := rand.New(rand.NewSource(time.Now().UnixNano()))\n\theaderBytes := make([]byte, r.Intn(128))\n\tqueryBytes := make([]byte, r.Intn(128))\n\tbodyBytes := make([]byte, r.Intn(128))\n\tr.Read(headerBytes)\n\tr.Read(queryBytes)\n\tr.Read(bodyBytes)\n\tparams := &test.MarshallerTest[int]{\n\t\tHeaderBoolean:   r.Float32() > 0.5,\n\t\tHeaderInt:       r.Int(),\n\t\tHeaderFloat:     r.Float64(),\n\t\tHeaderString:    \"header string\",\n\t\tHeaderBytes:     headerBytes,\n\t\tHeaderTime:      time.Now().Truncate(time.Second),\n\t\tHeaderJson:      json.RawMessage(\"{\\\"hello\\\":\\\"world\\\"}\"),\n\t\tHeaderUUID:      newUUID(),\n\t\tHeaderUserID:    \"432\",\n\t\tHeaderOption:    option.Some(\"test\"),\n\t\tQueryBoolean:    r.Float32() > 0.5,\n\t\tQueryInt:        r.Int(),\n\t\tQueryFloat:      r.Float64(),\n\t\tQueryString:     \"query string\",\n\t\tQueryBytes:      headerBytes,\n\t\tQueryTime:       time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),\n\t\tQueryJson:       json.RawMessage(\"true\"),\n\t\tQueryUUID:       newUUID(),\n\t\tQueryUserID:     \"9udfa\",\n\t\tQuerySlice:      []int{r.Int(), r.Int(), r.Int(), r.Int()},\n\t\tBodyBoolean:     r.Float32() > 0.5,\n\t\tBodyInt:         r.Int(),\n\t\tBodyFloat:       r.Float64(),\n\t\tBodyString:      \"body string\",\n\t\tBodyBytes:       bodyBytes,\n\t\tBodyTime:        time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),\n\t\tBodyJson:        json.RawMessage(\"null\"),\n\t\tBodyUUID:        newUUID(),\n\t\tBodyUserID:      \"✉️\",\n\t\tBodySlice:       []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int(), r.Int()},\n\t\tBodyOption:      option.Some(r.Int()),\n\t\tBodyOptionSlice: []option.Option[int]{option.Some(r.Int()), option.None[int](), option.Some(r.Int())},\n\t}\n\tmResp, err := test.MarshallerTestHandler(ctx, params)\n\tassert(err, nil, \"Expected no error from the marshaller test\")\n\n\t// We're marshalling as JSON, so we can just compare the JSON strings\n\trespAsJSON, err := json.Marshal(mResp)\n\tassert(err, nil, \"unable to marshal response to JSON\")\n\treqAsJSON, err := json.Marshal(params)\n\tassert(err, nil, \"unable to marshal response to JSON\")\n\tif diff := cmp.Diff(string(respAsJSON), string(reqAsJSON)); diff != \"\" {\n\t\tassertNumber++\n\t\tpanic(fmt.Sprintf(\"Assertion Failure %d: %s\", assertNumber, diff))\n\t}\n\n\t// Test the raw endpoint (Unsupported currently in service to service calls)\n\t// {\n\t// \treq, err := http.NewRequest(\"PUT\", \"?foo=bar\", strings.NewReader(\"this is a test body\"))\n\t// \tassert(err, nil, \"unable to create request for raw endpoint\")\n\t// \treq.Header.Add(\"X-Test-Header\", \"test\")\n\t//\n\t// \tw := httptest.NewRecorder()\n\t// \terr = test.RawEndpoint(w, req)\n\t// \tassert(err, nil, \"expected no error from the raw socket\")\n\t//\n\t// \tassert(w.Code, http.StatusCreated, \"expected the status code to be 201\")\n\t//\n\t// \ttype responseType struct {\n\t// \t\tBody        string\n\t// \t\tHeader      string\n\t// \t\tPathParam   string\n\t// \t\tQueryString string\n\t// \t}\n\t// \tresponse := &responseType{}\n\t//\n\t// \terr = json.Unmarshal(w.Body.Bytes(), response)\n\t// \tassert(err, nil, \"expected no error when unmarshalling the response body\")\n\t//\n\t// \tassert(response, &responseType{\"this is a test body\", \"test\", \"hello\", \"bar\"}, \"expected the response to match\")\n\t//\n\t// }\n\n\trlog.Info(\"End to end wrappers test completed without error\")\n\treturn nil\n}\n\nfunc assert(got, want any, message string) {\n\tassertNumber++\n\n\tif !reflect.DeepEqual(got, want) {\n\t\tpanic(fmt.Sprintf(\"Assertion Failure %d: %s\\n\\n%+v != %+v\\n\", assertNumber, message, got, want))\n\t}\n}\n\nfunc assertNotNil(got any, message string) {\n\tassertNumber++\n\tif got == nil {\n\t\tpanic(fmt.Sprintf(\"Assertion Failure %d: got nil: %s\", assertNumber, message))\n\t}\n}\n\nfunc assertStructuredError(err error, code errs.ErrCode, message string) {\n\tassertNotNil(err, \"want an error\")\n\n\tassertNumber++\n\tif apiError, ok := err.(*errs.Error); !ok {\n\t\tpanic(fmt.Sprintf(\"Assertion Failure %d: expected *errs.Error; got %+v\\n\", assertNumber, reflect.TypeOf(err)))\n\t\tos.Exit(assertNumber)\n\t} else {\n\t\tassert(apiError.Code, code, \"unexpected error code\")\n\t\tassert(apiError.Message, message, \"expected error message\")\n\t}\n}\n\nfunc newUUID() uuid.UUID {\n\tid, err := uuid.NewV4()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn id\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/go.mod",
    "content": "module encore.app\n\ngo 1.24.0\n\nrequire (\n\tencore.dev v1.52.0\n\tgithub.com/google/go-cmp v0.7.0\n)\n"
  },
  {
    "path": "e2e-tests/testdata/echo/go.sum",
    "content": "encore.dev v1.52.0 h1:e8LbHiccXLI3M4Oc3HbvWoVy8AyNyBJm7wY7NsPwCd4=\nencore.dev v1.52.0/go.mod h1:lK8vSJG6uhYeUwT87/FEpcLdiN98QUcotd3gxRX0xDw=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\n"
  },
  {
    "path": "e2e-tests/testdata/echo/middleware/middleware.go",
    "content": "package middleware\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/middleware\"\n)\n\n//encore:middleware target=tag:error\nfunc ErroringMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n\treturn middleware.Response{\n\t\tErr: errs.B().Code(errs.Internal).Msg(\"middleware error\").Err(),\n\t}\n}\n\n//encore:middleware target=tag:resp-rewrite\nfunc ResponseRewritingMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n\tresp := next(req)\n\treqPayload := req.Data().Payload.(*Payload)\n\trespPayload := resp.Payload.(*Payload)\n\n\trespPayload.Msg = fmt.Sprintf(\"middleware(req=%s, resp=%s)\",\n\t\treqPayload.Msg, respPayload.Msg)\n\treturn resp\n}\n\n//encore:middleware target=tag:resp-gen\nfunc ResponseGeneratingMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n\tresponseData := `{\"Msg\": \"middleware generated\"}`\n\tpayload := reflect.New(req.Data().API.ResponseType)\n\tif err := json.Unmarshal([]byte(responseData), payload.Interface()); err != nil {\n\t\treturn middleware.Response{Err: err}\n\t}\n\treturn middleware.Response{Payload: payload.Elem().Interface()}\n}\n\ntype Payload struct {\n\tMsg string\n}\n\n//encore:api public tag:error\nfunc Error(ctx context.Context) error {\n\treturn nil\n}\n\n//encore:api public tag:resp-rewrite\nfunc ResponseRewrite(ctx context.Context, req *Payload) (*Payload, error) {\n\treturn &Payload{Msg: fmt.Sprintf(\"handler(%s)\", req.Msg)}, nil\n}\n\n//encore:api public tag:resp-gen\nfunc ResponseGen(ctx context.Context, msg *Payload) (*Payload, error) {\n\treturn nil, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/middleware/middleware_test.go",
    "content": "package middleware\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"encore.dev/beta/errs\"\n)\n\n// TestMiddleware tests that middleware is executed during tests.\nfunc TestMiddleware(t *testing.T) {\n\tctx := context.Background()\n\tif err := Error(ctx); err == nil {\n\t\tt.Error(\"Error(): want non-nil error, got nil\")\n\t} else if ee, ok := err.(*errs.Error); !ok {\n\t\tt.Errorf(\"Error(): want *errs.Error, got %T\", err)\n\t} else if ee.Code != errs.Internal {\n\t\tt.Errorf(\"Error(): want code=Internal, got %v\", ee.Code)\n\t} else if ee.Message != \"middleware error\" {\n\t\tt.Errorf(\"Error(): want msg=\\\"middleware error\\\", got %q\", ee.Message)\n\t}\n\n\tresp, err := ResponseRewrite(ctx, &Payload{Msg: \"foo\"})\n\tif err != nil {\n\t\tt.Errorf(\"ResponseRewrite(): got non-nil error: %v\", err)\n\t} else if want := \"middleware(req=foo, resp=handler(foo))\"; resp.Msg != want {\n\t\tt.Errorf(\"ResponseRewrite(): got %q, want %q\", resp.Msg, want)\n\t}\n\n\tresp, err = ResponseGen(ctx, &Payload{Msg: \"foo\"})\n\tif err != nil {\n\t\tt.Errorf(\"ResponseGen(): got non-nil error: %v\", err)\n\t} else if want := \"middleware generated\"; resp.Msg != want {\n\t\tt.Errorf(\"ResponseGen(): got %q, want %q\", resp.Msg, want)\n\t}\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/test/endpoints.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/types/option\"\n\t\"encore.dev/types/uuid\"\n)\n\n// Noop allows us to test if a simple HTTP request can be made\n//\n//encore:api public\nfunc Noop(ctx context.Context) error {\n\treturn nil\n}\n\n// NoopWithError allows us to test if the structured errors are returned\n//\n//encore:api public\nfunc NoopWithError(ctx context.Context) error {\n\treturn &errs.Error{\n\t\tCode:    errs.Unimplemented,\n\t\tMessage: \"totally not implemented yet\",\n\t}\n}\n\ntype BodyEcho struct {\n\tMessage string\n}\n\n// SimpleBodyEcho allows us to exercise the body marshalling from JSON\n// and being returned purely as a body\n//\n//encore:api public\nfunc SimpleBodyEcho(ctx context.Context, body *BodyEcho) (*BodyEcho, error) {\n\treturn body, nil\n}\n\nvar lastMessage = make(map[string]string)\n\n// UpdateMessage allows us to test an API which takes parameters,\n// but doesn't return anything\n//\n//encore:api public method=PUT path=/last_message/:clientID\nfunc UpdateMessage(ctx context.Context, clientID string, message *BodyEcho) error {\n\tlastMessage[clientID] = message.Message\n\treturn nil\n}\n\n// GetMessage allows us to test an API which takes no parameters,\n// but returns data. It also tests two API's on the same path with different HTTP methods\n//\n//encore:api public method=GET path=/last_message/:clientID\nfunc GetMessage(ctx context.Context, clientID string) (*BodyEcho, error) {\n\treturn &BodyEcho{\n\t\tMessage: lastMessage[clientID],\n\t}, nil\n}\n\ntype RestParams struct {\n\tHeaderValue string `header:\"Some-Key\"`\n\tQueryValue  string `query:\"Some-Key\"`\n\tBodyValue   string `json:\"Some-Key\"`\n\n\tNested struct {\n\t\tKey   string `json:\"Alice\"`\n\t\tValue int    `json:\"bOb\"`\n\t\tOk    bool   `json:\"charile\"`\n\t}\n}\n\n// RestStyleAPI tests all the ways we can get data into and out of the application\n// using Encore request handlers\n//\n//encore:api public method=PUT path=/rest/object/:objType/:name\nfunc RestStyleAPI(ctx context.Context, objType int, name string, params *RestParams) (*RestParams, error) {\n\treturn &RestParams{\n\t\tHeaderValue: params.HeaderValue,\n\t\tQueryValue:  params.QueryValue,\n\t\tBodyValue:   params.BodyValue,\n\t\tNested: struct {\n\t\t\tKey   string `json:\"Alice\"`\n\t\t\tValue int    `json:\"bOb\"`\n\t\t\tOk    bool   `json:\"charile\"`\n\t\t}{\n\t\t\tKey:   name + \" + \" + params.Nested.Key,\n\t\t\tValue: objType + params.Nested.Value,\n\t\t\tOk:    params.Nested.Ok,\n\t\t},\n\t}, nil\n}\n\ntype MarshallerTest[A any] struct {\n\tHeaderBoolean   bool                  `header:\"x-boolean\"`\n\tHeaderInt       int                   `header:\"x-int\"`\n\tHeaderFloat     float64               `header:\"x-float\"`\n\tHeaderString    string                `header:\"x-string\"`\n\tHeaderBytes     []byte                `header:\"x-bytes\"`\n\tHeaderTime      time.Time             `header:\"x-time\"`\n\tHeaderJson      json.RawMessage       `header:\"x-json\"`\n\tHeaderUUID      uuid.UUID             `header:\"x-uuid\"`\n\tHeaderUserID    auth.UID              `header:\"x-user-id\"`\n\tHeaderOption    option.Option[string] `header:\"x-option\"`\n\tQueryBoolean    bool                  `qs:\"boolean\"`\n\tQueryInt        int                   `qs:\"int\"`\n\tQueryFloat      float64               `qs:\"float\"`\n\tQueryString     string                `qs:\"string\"`\n\tQueryBytes      []byte                `qs:\"bytes\"`\n\tQueryTime       time.Time             `qs:\"time\"`\n\tQueryJson       json.RawMessage       `qs:\"json\"`\n\tQueryUUID       uuid.UUID             `qs:\"uuid\"`\n\tQueryUserID     auth.UID              `qs:\"user-id\"`\n\tQuerySlice      []A                   `qs:\"slice\"`\n\tBodyBoolean     bool                  `json:\"boolean\"`\n\tBodyInt         int                   `json:\"int\"`\n\tBodyFloat       float64               `json:\"float\"`\n\tBodyString      string                `json:\"string\"`\n\tBodyBytes       []byte                `json:\"bytes\"`\n\tBodyTime        time.Time             `json:\"time\"`\n\tBodyJson        json.RawMessage       `json:\"json\"`\n\tBodyUUID        uuid.UUID             `json:\"uuid\"`\n\tBodyUserID      auth.UID              `json:\"user-id\"`\n\tBodySlice       []A                   `json:\"slice\"`\n\tBodyOption      option.Option[A]      `json:\"option\"`\n\tBodyOptionSlice []option.Option[A]    `json:\"option-slice\"`\n}\n\n// MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all\n// the field types. It simply echos all the responses back to the client\n//\n//encore:api public\nfunc MarshallerTestHandler(ctx context.Context, params *MarshallerTest[int]) (*MarshallerTest[int], error) {\n\treturn params, nil\n}\n\n// TestAuthHandler allows us to test the clients ability to add tokens to requests\n//\n//encore:api auth\nfunc TestAuthHandler(ctx context.Context) (*BodyEcho, error) {\n\tuserID, ok := auth.UserID()\n\n\treturn &BodyEcho{\n\t\tMessage: string(userID) + \"::\" + strconv.FormatBool(ok),\n\t}, nil\n}\n\ntype response struct {\n\tBody        string\n\tHeader      string\n\tPathParam   string\n\tQueryString string\n}\n\n// RawEndpoint allows us to test the clients' ability to send raw requests\n// under auth\n//\n//encore:api public raw method=PUT,POST,DELETE,GET path=/raw/blah/*id\nfunc RawEndpoint(w http.ResponseWriter, req *http.Request) {\n\tw.WriteHeader(http.StatusCreated)\n\n\tbytes, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treq.Body.Close()\n\n\tb, err := json.Marshal(&response{\n\t\tBody:        string(bytes),\n\t\tHeader:      req.Header.Get(\"X-Test-Header\"),\n\t\tPathParam:   encore.CurrentRequest().PathParams.Get(\"id\"),\n\t\tQueryString: req.URL.Query().Get(\"foo\"),\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tw.Write(b)\n}\n\ntype MultiPathSegment struct {\n\tBoolean  bool\n\tInt      int\n\tString   string\n\tUUID     uuid.UUID\n\tWildcard string\n}\n\n// PathMultiSegments allows us to wildcard segments and segment URI encoding\n//\n//encore:api public path=/multi/:bool/:int/:string/:uuid/*wildcard\nfunc PathMultiSegments(ctx context.Context, bool bool, int int, string string, uuid uuid.UUID, wildcard string) (*MultiPathSegment, error) {\n\treturn &MultiPathSegment{\n\t\tBoolean:  bool,\n\t\tInt:      int,\n\t\tString:   string,\n\t\tUUID:     uuid,\n\t\tWildcard: wildcard,\n\t}, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo/validation/validation.go",
    "content": "package validation\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\ntype Request struct {\n\tMsg string\n}\n\nfunc (req *Request) Validate() error {\n\tif req.Msg == \"fail\" {\n\t\treturn errors.New(\"bad message\")\n\t}\n\treturn nil\n}\n\n//encore:api public\nfunc TestOne(ctx context.Context, msg *Request) error {\n\treturn nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/.eslintrc.cjs",
    "content": "/* .eslintrc.js */\nmodule.exports = {\n    root: true,\n    parser: '@typescript-eslint/parser',\n    plugins: [\n        '@typescript-eslint',\n    ],\n    env: {\n        browser: true,\n        es6: true,\n        node: true,\n        commonjs: true\n    },\n    extends: [\n        'eslint:recommended',\n        'plugin:@typescript-eslint/eslint-recommended',\n        'plugin:@typescript-eslint/recommended',\n    ],\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/.gitignore",
    "content": "node_modules/\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/go.mod",
    "content": "module echo_client\n\ngo 1.21\n\nrequire github.com/google/go-cmp v0.7.0\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/go.sum",
    "content": "github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/golang/client/goclient.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Client is an API client for the slug Encore application.\ntype Client struct {\n\tCache      CacheClient\n\tDi         DiClient\n\tEcho       EchoClient\n\tEmptycfg   EmptycfgClient\n\tEndtoend   EndtoendClient\n\tMiddleware MiddlewareClient\n\tTest       TestClient\n\tValidation ValidationClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-slug.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"slug-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{\n\t\tCache:      &cacheClient{base},\n\t\tDi:         &diClient{base},\n\t\tEcho:       &echoClient{base},\n\t\tEmptycfg:   &emptycfgClient{base},\n\t\tEndtoend:   &endtoendClient{base},\n\t\tMiddleware: &middlewareClient{base},\n\t\tTest:       &testClient{base},\n\t\tValidation: &validationClient{base},\n\t}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\n// WithAuth allows you to set the authentication data to be used with each request\nfunc WithAuth(auth EchoAuthParams) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = func(_ context.Context) (EchoAuthParams, error) {\n\t\t\treturn auth, nil\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithAuthFunc allows you to pass a function which is called for each request to return the authentication data to be used with each request\nfunc WithAuthFunc(authGenerator func(ctx context.Context) (EchoAuthParams, error)) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = authGenerator\n\t\treturn nil\n\t}\n}\n\ntype CacheIncrResponse struct {\n\tVal int64\n}\n\ntype CacheListResponse struct {\n\tVals []string\n}\n\ntype CacheStructVal struct {\n\tVal string\n}\n\n// CacheClient Provides you access to call public and authenticated APIs on cache. The concrete implementation is cacheClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype CacheClient interface {\n\tGetList(ctx context.Context, key int) (CacheListResponse, error)\n\tGetStruct(ctx context.Context, key int) (CacheStructVal, error)\n\tIncr(ctx context.Context, key string) (CacheIncrResponse, error)\n\tPostList(ctx context.Context, key int, val string) error\n\tPostStruct(ctx context.Context, key int, val string) error\n}\n\ntype cacheClient struct {\n\tbase *baseClient\n}\n\nvar _ CacheClient = (*cacheClient)(nil)\n\nfunc (c *cacheClient) GetList(ctx context.Context, key int) (resp CacheListResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/cache/list/%d\", key), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *cacheClient) GetStruct(ctx context.Context, key int) (resp CacheStructVal, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/cache/struct/%d\", key), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *cacheClient) Incr(ctx context.Context, key string) (resp CacheIncrResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/cache/incr/%s\", url.PathEscape(key)), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *cacheClient) PostList(ctx context.Context, key int, val string) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/cache/list/%d/%s\", key, url.PathEscape(val)), nil, nil, nil)\n\treturn err\n}\n\nfunc (c *cacheClient) PostStruct(ctx context.Context, key int, val string) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/cache/struct/%d/%s\", key, url.PathEscape(val)), nil, nil, nil)\n\treturn err\n}\n\ntype DiTwoResponse struct {\n\tMsg string\n}\n\n// DiClient Provides you access to call public and authenticated APIs on di. The concrete implementation is diClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype DiClient interface {\n\tOne(ctx context.Context) error\n\tThree(ctx context.Context, request *http.Request) (*http.Response, error)\n\tTwo(ctx context.Context) (DiTwoResponse, error)\n}\n\ntype diClient struct {\n\tbase *baseClient\n}\n\nvar _ DiClient = (*diClient)(nil)\n\nfunc (c *diClient) One(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/di/one\", nil, nil, nil)\n\treturn err\n}\n\nfunc (c *diClient) Three(ctx context.Context, request *http.Request) (*http.Response, error) {\n\trequest = request.WithContext(ctx)\n\n\t// Check the request has the method set, as we can't guess what method is required\n\tif request.Method == \"\" {\n\t\treturn nil, errors.New(\"request.Method must be set\")\n\t}\n\n\t// Set the relative URL for the API call\n\tpath, err := url.Parse(\"/di/raw\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse api url: %w\", err)\n\t}\n\tif request.URL != nil {\n\t\t// If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema,\n\t\t// host and path to ensure the final URL which hit the right BaseURL\n\t\trequest.URL.Scheme = path.Scheme\n\t\trequest.URL.Host = path.Host\n\t\trequest.URL.Path = path.Path\n\t} else {\n\t\trequest.URL = path\n\t}\n\n\treturn c.base.Do(request)\n}\n\nfunc (c *diClient) Two(ctx context.Context) (resp DiTwoResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/di/two\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\ntype EchoAppMetadata struct {\n\tAppID      string\n\tAPIBaseURL string\n\tEnvName    string\n\tEnvType    string\n}\n\ntype EchoAuthParams struct {\n\tHeader        string `header:\"X-Header\"`\n\tAuthInt       int    `header:\"X-Auth-Int\"`\n\tAuthorization string `header:\"Authorization\"`\n\tQuery         []int  `query:\"query\"`\n\tNewAuth       bool   `query:\"new-auth\"`\n}\n\ntype EchoBasicData struct {\n\tString      string\n\tUint        uint\n\tInt         int\n\tInt8        int8\n\tInt64       int64\n\tFloat32     float32\n\tFloat64     float64\n\tStringSlice []string\n\tIntSlice    []int\n\tTime        time.Time\n}\n\ntype EchoConfigResponse struct {\n\tReadOnlyMode bool\n\tPublicKey    []byte\n\tSubKeyCount  uint\n\tAdminUsers   []string\n}\n\ntype EchoData[K any, V any] struct {\n\tKey   K\n\tValue V\n}\n\ntype EchoEmptyData struct {\n\tOmitEmpty EchoData[string, string] `json:\"OmitEmpty,omitempty\"`\n\tNullPtr   *string\n\tZero      EchoData[string, string]\n}\n\ntype EchoEnvResponse struct {\n\tEnv []string\n}\n\n// HTTPStatusResponse demonstrates encore:\"httpstatus\" tag functionality\ntype EchoHTTPStatusResponse struct {\n\tMessage string `json:\"message\"`\n}\n\ntype EchoHeadersData struct {\n\tInt    int    `header:\"X-Int\"`\n\tString string `header:\"X-String\"`\n}\n\ntype EchoNonBasicData struct {\n\tHeaderString string                                   `header:\"X-Header-String\"` // Header\n\tHeaderNumber int                                      `header:\"X-Header-Number\"`\n\tStruct       EchoData[*EchoData[string, string], int] // Body\n\tStructPtr    *EchoData[int, uint16]\n\tStructSlice  []*EchoData[string, string]\n\tStructMap    map[string]*EchoData[string, float32]\n\tStructMapPtr *map[string]*EchoData[string, string]\n\tAnonStruct   struct {\n\t\tAnonBird string\n\t}\n\tNamedStruct    *EchoData[string, float64] `json:\"formatted_nest\"`\n\tRawStruct      json.RawMessage\n\tQueryString    string `query:\"string\"` // Query\n\tQueryNumber    int    `query:\"no\"`\n\tOptQueryNumber int    `encore:\"optional\" query:\"optnum\"`\n\tOptQueryString string `encore:\"optional\" query:\"optstr\"`\n\tPathString     string // Path Parameters\n\tPathInt        int\n\tPathWild       string\n\tAuthHeader     string // Auth Parameters\n\tAuthQuery      []int\n}\n\n// EchoClient Provides you access to call public and authenticated APIs on echo. The concrete implementation is echoClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype EchoClient interface {\n\t// AppMeta returns app metadata.\n\tAppMeta(ctx context.Context) (EchoAppMetadata, error)\n\n\t// BasicEcho echoes back the request data.\n\tBasicEcho(ctx context.Context, params EchoBasicData) (EchoBasicData, error)\n\tConfigValues(ctx context.Context) (EchoConfigResponse, error)\n\n\t// CustomHTTPStatus allows testing of custom HTTP status codes via encore:\"httpstatus\" tag\n\tCustomHTTPStatus(ctx context.Context) (EchoHTTPStatusResponse, error)\n\n\t// Echo echoes back the request data.\n\tEcho(ctx context.Context, params EchoData[string, int]) (EchoData[string, int], error)\n\n\t// EmptyEcho echoes back the request data.\n\tEmptyEcho(ctx context.Context, params EchoEmptyData) (EchoEmptyData, error)\n\n\t// Env returns the environment.\n\tEnv(ctx context.Context) (EchoEnvResponse, error)\n\n\t// HeadersEcho echoes back the request headers\n\tHeadersEcho(ctx context.Context, params EchoHeadersData) (EchoHeadersData, error)\n\n\t// MuteEcho absorbs a request\n\tMuteEcho(ctx context.Context, params EchoData[string, string]) error\n\n\t// NilResponse returns a nil response and nil error\n\tNilResponse(ctx context.Context) (EchoBasicData, error)\n\n\t// NonBasicEcho echoes back the request data.\n\tNonBasicEcho(ctx context.Context, pathString string, pathInt int, pathWild []string, params EchoNonBasicData) (EchoNonBasicData, error)\n\n\t// Noop does nothing\n\tNoop(ctx context.Context) error\n\n\t// Pong returns a bird tuple\n\tPong(ctx context.Context) (EchoData[string, string], error)\n\n\t// Publish publishes a request on a topic\n\tPublish(ctx context.Context) error\n}\n\ntype echoClient struct {\n\tbase *baseClient\n}\n\nvar _ EchoClient = (*echoClient)(nil)\n\n// AppMeta returns app metadata.\nfunc (c *echoClient) AppMeta(ctx context.Context) (resp EchoAppMetadata, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.AppMeta\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// BasicEcho echoes back the request data.\nfunc (c *echoClient) BasicEcho(ctx context.Context, params EchoBasicData) (resp EchoBasicData, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.BasicEcho\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *echoClient) ConfigValues(ctx context.Context) (resp EchoConfigResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.ConfigValues\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// CustomHTTPStatus allows testing of custom HTTP status codes via encore:\"httpstatus\" tag\nfunc (c *echoClient) CustomHTTPStatus(ctx context.Context) (resp EchoHTTPStatusResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.CustomHTTPStatus\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// Echo echoes back the request data.\nfunc (c *echoClient) Echo(ctx context.Context, params EchoData[string, int]) (resp EchoData[string, int], err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.Echo\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// EmptyEcho echoes back the request data.\nfunc (c *echoClient) EmptyEcho(ctx context.Context, params EchoEmptyData) (resp EchoEmptyData, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.EmptyEcho\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// Env returns the environment.\nfunc (c *echoClient) Env(ctx context.Context) (resp EchoEnvResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.Env\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// HeadersEcho echoes back the request headers\nfunc (c *echoClient) HeadersEcho(ctx context.Context, params EchoHeadersData) (resp EchoHeadersData, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"x-int\":    {reqEncoder.FromInt(params.Int)},\n\t\t\"x-string\": {reqEncoder.FromString(params.String)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Now make the actual call to the API\n\tvar respHeaders http.Header\n\trespHeaders, err = callAPI(ctx, c.base, \"POST\", \"/echo.HeadersEcho\", headers, nil, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Copy the unmarshalled response body into our response struct\n\trespDecoder := &serde{}\n\n\tresp.Int = respDecoder.ToInt(\"Int\", respHeaders.Get(\"x-int\"), true)\n\tresp.String = respDecoder.ToString(\"String\", respHeaders.Get(\"x-string\"), true)\n\n\tif respDecoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to unmarshal headers: %w\", respDecoder.LastError)\n\t\treturn\n\t}\n\n\treturn\n}\n\n// MuteEcho absorbs a request\nfunc (c *echoClient) MuteEcho(ctx context.Context, params EchoData[string, string]) error {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\tqueryString := url.Values{\n\t\t\"key\":   {reqEncoder.FromString(params.Key)},\n\t\t\"value\": {reqEncoder.FromString(params.Value)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\treturn fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t}\n\n\t_, err := callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/echo.MuteEcho?%s\", queryString.Encode()), nil, nil, nil)\n\treturn err\n}\n\n// NilResponse returns a nil response and nil error\nfunc (c *echoClient) NilResponse(ctx context.Context) (resp EchoBasicData, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/echo.NilResponse\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// NonBasicEcho echoes back the request data.\nfunc (c *echoClient) NonBasicEcho(ctx context.Context, pathString string, pathInt int, pathWild []string, params EchoNonBasicData) (resp EchoNonBasicData, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"x-header-number\": {reqEncoder.FromInt(params.HeaderNumber)},\n\t\t\"x-header-string\": {reqEncoder.FromString(params.HeaderString)},\n\t}\n\n\tqueryString := url.Values{\n\t\t\"no\":     {reqEncoder.FromInt(params.QueryNumber)},\n\t\t\"optnum\": {reqEncoder.FromInt(params.OptQueryNumber)},\n\t\t\"optstr\": {reqEncoder.FromString(params.OptQueryString)},\n\t\t\"string\": {reqEncoder.FromString(params.QueryString)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tStruct       EchoData[*EchoData[string, string], int] `json:\"Struct\"`\n\t\tStructPtr    *EchoData[int, uint16]                   `json:\"StructPtr\"`\n\t\tStructSlice  []*EchoData[string, string]              `json:\"StructSlice\"`\n\t\tStructMap    map[string]*EchoData[string, float32]    `json:\"StructMap\"`\n\t\tStructMapPtr *map[string]*EchoData[string, string]    `json:\"StructMapPtr\"`\n\t\tAnonStruct   struct {\n\t\t\tAnonBird string\n\t\t} `json:\"AnonStruct\"`\n\t\tNamedStruct *EchoData[string, float64] `json:\"formatted_nest\"`\n\t\tRawStruct   json.RawMessage            `json:\"RawStruct\"`\n\t\tPathString  string                     `json:\"PathString\"`\n\t\tPathInt     int                        `json:\"PathInt\"`\n\t\tPathWild    string                     `json:\"PathWild\"`\n\t\tAuthHeader  string                     `json:\"AuthHeader\"`\n\t\tAuthQuery   []int                      `json:\"AuthQuery\"`\n\t}{\n\t\tAnonStruct:   params.AnonStruct,\n\t\tAuthHeader:   params.AuthHeader,\n\t\tAuthQuery:    params.AuthQuery,\n\t\tNamedStruct:  params.NamedStruct,\n\t\tPathInt:      params.PathInt,\n\t\tPathString:   params.PathString,\n\t\tPathWild:     params.PathWild,\n\t\tRawStruct:    params.RawStruct,\n\t\tStruct:       params.Struct,\n\t\tStructMap:    params.StructMap,\n\t\tStructMapPtr: params.StructMapPtr,\n\t\tStructPtr:    params.StructPtr,\n\t\tStructSlice:  params.StructSlice,\n\t}\n\n\t// We only want the response body to marshal into these fields and none of the header fields,\n\t// so we'll construct a new struct with only those fields.\n\trespBody := struct {\n\t\tStruct       EchoData[*EchoData[string, string], int] `json:\"Struct\"`\n\t\tStructPtr    *EchoData[int, uint16]                   `json:\"StructPtr\"`\n\t\tStructSlice  []*EchoData[string, string]              `json:\"StructSlice\"`\n\t\tStructMap    map[string]*EchoData[string, float32]    `json:\"StructMap\"`\n\t\tStructMapPtr *map[string]*EchoData[string, string]    `json:\"StructMapPtr\"`\n\t\tAnonStruct   struct {\n\t\t\tAnonBird string\n\t\t} `json:\"AnonStruct\"`\n\t\tNamedStruct    *EchoData[string, float64] `json:\"formatted_nest\"`\n\t\tRawStruct      json.RawMessage            `json:\"RawStruct\"`\n\t\tQueryString    string                     `json:\"QueryString\"`\n\t\tQueryNumber    int                        `json:\"QueryNumber\"`\n\t\tOptQueryNumber int                        `json:\"OptQueryNumber\"`\n\t\tOptQueryString string                     `json:\"OptQueryString\"`\n\t\tPathString     string                     `json:\"PathString\"`\n\t\tPathInt        int                        `json:\"PathInt\"`\n\t\tPathWild       string                     `json:\"PathWild\"`\n\t\tAuthHeader     string                     `json:\"AuthHeader\"`\n\t\tAuthQuery      []int                      `json:\"AuthQuery\"`\n\t}{}\n\n\t// Now make the actual call to the API\n\tvar respHeaders http.Header\n\trespHeaders, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/NonBasicEcho/%s/%d/%s?%s\", url.PathEscape(pathString), pathInt, pathEscapeSlice(pathWild), queryString.Encode()), headers, body, &respBody)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Copy the unmarshalled response body into our response struct\n\trespDecoder := &serde{}\n\n\tresp.HeaderString = respDecoder.ToString(\"HeaderString\", respHeaders.Get(\"x-header-string\"), true)\n\tresp.HeaderNumber = respDecoder.ToInt(\"HeaderNumber\", respHeaders.Get(\"x-header-number\"), true)\n\tresp.Struct = respBody.Struct\n\tresp.StructPtr = respBody.StructPtr\n\tresp.StructSlice = respBody.StructSlice\n\tresp.StructMap = respBody.StructMap\n\tresp.StructMapPtr = respBody.StructMapPtr\n\tresp.AnonStruct = respBody.AnonStruct\n\tresp.NamedStruct = respBody.NamedStruct\n\tresp.RawStruct = respBody.RawStruct\n\tresp.QueryString = respBody.QueryString\n\tresp.QueryNumber = respBody.QueryNumber\n\tresp.OptQueryNumber = respBody.OptQueryNumber\n\tresp.OptQueryString = respBody.OptQueryString\n\tresp.PathString = respBody.PathString\n\tresp.PathInt = respBody.PathInt\n\tresp.PathWild = respBody.PathWild\n\tresp.AuthHeader = respBody.AuthHeader\n\tresp.AuthQuery = respBody.AuthQuery\n\n\tif respDecoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to unmarshal headers: %w\", respDecoder.LastError)\n\t\treturn\n\t}\n\n\treturn\n}\n\n// Noop does nothing\nfunc (c *echoClient) Noop(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"GET\", \"/echo.Noop\", nil, nil, nil)\n\treturn err\n}\n\n// Pong returns a bird tuple\nfunc (c *echoClient) Pong(ctx context.Context) (resp EchoData[string, string], err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", \"/echo.Pong\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// Publish publishes a request on a topic\nfunc (c *echoClient) Publish(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/echo.Publish\", nil, nil, nil)\n\treturn err\n}\n\n// EmptycfgClient Provides you access to call public and authenticated APIs on emptycfg. The concrete implementation is emptycfgClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype EmptycfgClient interface {\n\tAnAPI(ctx context.Context) error\n}\n\ntype emptycfgClient struct {\n\tbase *baseClient\n}\n\nvar _ EmptycfgClient = (*emptycfgClient)(nil)\n\nfunc (c *emptycfgClient) AnAPI(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/emptycfg.AnAPI\", nil, nil, nil)\n\treturn err\n}\n\n// EndtoendClient Provides you access to call public and authenticated APIs on endtoend. The concrete implementation is endtoendClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype EndtoendClient interface {\n\tGeneratedWrappersEndToEndTest(ctx context.Context) error\n}\n\ntype endtoendClient struct {\n\tbase *baseClient\n}\n\nvar _ EndtoendClient = (*endtoendClient)(nil)\n\nfunc (c *endtoendClient) GeneratedWrappersEndToEndTest(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"GET\", \"/generated-wrappers-end-to-end-test\", nil, nil, nil)\n\treturn err\n}\n\ntype MiddlewarePayload struct {\n\tMsg string\n}\n\n// MiddlewareClient Provides you access to call public and authenticated APIs on middleware. The concrete implementation is middlewareClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype MiddlewareClient interface {\n\tError(ctx context.Context) error\n\tResponseGen(ctx context.Context, params MiddlewarePayload) (MiddlewarePayload, error)\n\tResponseRewrite(ctx context.Context, params MiddlewarePayload) (MiddlewarePayload, error)\n}\n\ntype middlewareClient struct {\n\tbase *baseClient\n}\n\nvar _ MiddlewareClient = (*middlewareClient)(nil)\n\nfunc (c *middlewareClient) Error(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/middleware.Error\", nil, nil, nil)\n\treturn err\n}\n\nfunc (c *middlewareClient) ResponseGen(ctx context.Context, params MiddlewarePayload) (resp MiddlewarePayload, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/middleware.ResponseGen\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *middlewareClient) ResponseRewrite(ctx context.Context, params MiddlewarePayload) (resp MiddlewarePayload, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/middleware.ResponseRewrite\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\ntype TestBodyEcho struct {\n\tMessage string\n}\n\ntype TestMarshallerTest[A any] struct {\n\tHeaderBoolean   bool            `header:\"x-boolean\"`\n\tHeaderInt       int             `header:\"x-int\"`\n\tHeaderFloat     float64         `header:\"x-float\"`\n\tHeaderString    string          `header:\"x-string\"`\n\tHeaderBytes     []byte          `header:\"x-bytes\"`\n\tHeaderTime      time.Time       `header:\"x-time\"`\n\tHeaderJson      json.RawMessage `header:\"x-json\"`\n\tHeaderUUID      string          `header:\"x-uuid\"`\n\tHeaderUserID    string          `header:\"x-user-id\"`\n\tHeaderOption    *string         `header:\"x-option\"`\n\tQueryBoolean    bool            `qs:\"boolean\"`\n\tQueryInt        int             `qs:\"int\"`\n\tQueryFloat      float64         `qs:\"float\"`\n\tQueryString     string          `qs:\"string\"`\n\tQueryBytes      []byte          `qs:\"bytes\"`\n\tQueryTime       time.Time       `qs:\"time\"`\n\tQueryJson       json.RawMessage `qs:\"json\"`\n\tQueryUUID       string          `qs:\"uuid\"`\n\tQueryUserID     string          `qs:\"user-id\"`\n\tQuerySlice      []A             `qs:\"slice\"`\n\tBodyBoolean     bool            `json:\"boolean\"`\n\tBodyInt         int             `json:\"int\"`\n\tBodyFloat       float64         `json:\"float\"`\n\tBodyString      string          `json:\"string\"`\n\tBodyBytes       []byte          `json:\"bytes\"`\n\tBodyTime        time.Time       `json:\"time\"`\n\tBodyJson        json.RawMessage `json:\"json\"`\n\tBodyUUID        string          `json:\"uuid\"`\n\tBodyUserID      string          `json:\"user-id\"`\n\tBodySlice       []A             `json:\"slice\"`\n\tBodyOption      *A              `json:\"option\"`\n\tBodyOptionSlice []*A            `json:\"option-slice\"`\n}\n\ntype TestMultiPathSegment struct {\n\tBoolean  bool\n\tInt      int\n\tString   string\n\tUUID     string\n\tWildcard string\n}\n\ntype TestRestParams struct {\n\tHeaderValue string `header:\"Some-Key\"`\n\tQueryValue  string `query:\"Some-Key\"`\n\tBodyValue   string `json:\"Some-Key\"`\n\tNested      struct {\n\t\tKey   string `json:\"Alice\"`\n\t\tValue int    `json:\"bOb\"`\n\t\tOk    bool   `json:\"charile\"`\n\t}\n}\n\n// TestClient Provides you access to call public and authenticated APIs on test. The concrete implementation is testClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype TestClient interface {\n\t// GetMessage allows us to test an API which takes no parameters,\n\t// but returns data. It also tests two API's on the same path with different HTTP methods\n\tGetMessage(ctx context.Context, clientID string) (TestBodyEcho, error)\n\n\t// MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all\n\t// the field types. It simply echos all the responses back to the client\n\tMarshallerTestHandler(ctx context.Context, params TestMarshallerTest[int]) (TestMarshallerTest[int], error)\n\n\t// Noop allows us to test if a simple HTTP request can be made\n\tNoop(ctx context.Context) error\n\n\t// NoopWithError allows us to test if the structured errors are returned\n\tNoopWithError(ctx context.Context) error\n\n\t// PathMultiSegments allows us to wildcard segments and segment URI encoding\n\tPathMultiSegments(ctx context.Context, _bool bool, _int int, _string string, uuid string, wildcard []string) (TestMultiPathSegment, error)\n\n\t// RawEndpoint allows us to test the clients' ability to send raw requests\n\t// under auth\n\tRawEndpoint(ctx context.Context, id []string, request *http.Request) (*http.Response, error)\n\n\t// RestStyleAPI tests all the ways we can get data into and out of the application\n\t// using Encore request handlers\n\tRestStyleAPI(ctx context.Context, objType int, name string, params TestRestParams) (TestRestParams, error)\n\n\t// SimpleBodyEcho allows us to exercise the body marshalling from JSON\n\t// and being returned purely as a body\n\tSimpleBodyEcho(ctx context.Context, params TestBodyEcho) (TestBodyEcho, error)\n\n\t// TestAuthHandler allows us to test the clients ability to add tokens to requests\n\tTestAuthHandler(ctx context.Context) (TestBodyEcho, error)\n\n\t// UpdateMessage allows us to test an API which takes parameters,\n\t// but doesn't return anything\n\tUpdateMessage(ctx context.Context, clientID string, params TestBodyEcho) error\n}\n\ntype testClient struct {\n\tbase *baseClient\n}\n\nvar _ TestClient = (*testClient)(nil)\n\n// GetMessage allows us to test an API which takes no parameters,\n// but returns data. It also tests two API's on the same path with different HTTP methods\nfunc (c *testClient) GetMessage(ctx context.Context, clientID string) (resp TestBodyEcho, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/last_message/%s\", url.PathEscape(clientID)), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all\n// the field types. It simply echos all the responses back to the client\nfunc (c *testClient) MarshallerTestHandler(ctx context.Context, params TestMarshallerTest[int]) (resp TestMarshallerTest[int], err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"x-boolean\": {reqEncoder.FromBool(params.HeaderBoolean)},\n\t\t\"x-bytes\":   {reqEncoder.FromBytes(params.HeaderBytes)},\n\t\t\"x-float\":   {reqEncoder.FromFloat64(params.HeaderFloat)},\n\t\t\"x-int\":     {reqEncoder.FromInt(params.HeaderInt)},\n\t\t\"x-json\":    {reqEncoder.FromJSON(params.HeaderJson)},\n\t\t\"x-option\":  reqEncoder.FromStringOption(params.HeaderOption),\n\t\t\"x-string\":  {reqEncoder.FromString(params.HeaderString)},\n\t\t\"x-time\":    {reqEncoder.FromTime(params.HeaderTime)},\n\t\t\"x-user-id\": {reqEncoder.FromString(params.HeaderUserID)},\n\t\t\"x-uuid\":    {reqEncoder.FromString(params.HeaderUUID)},\n\t}\n\n\tqueryString := url.Values{\n\t\t\"boolean\": {reqEncoder.FromBool(params.QueryBoolean)},\n\t\t\"bytes\":   {reqEncoder.FromBytes(params.QueryBytes)},\n\t\t\"float\":   {reqEncoder.FromFloat64(params.QueryFloat)},\n\t\t\"int\":     {reqEncoder.FromInt(params.QueryInt)},\n\t\t\"json\":    {reqEncoder.FromJSON(params.QueryJson)},\n\t\t\"slice\":   reqEncoder.FromIntList(params.QuerySlice),\n\t\t\"string\":  {reqEncoder.FromString(params.QueryString)},\n\t\t\"time\":    {reqEncoder.FromTime(params.QueryTime)},\n\t\t\"user-id\": {reqEncoder.FromString(params.QueryUserID)},\n\t\t\"uuid\":    {reqEncoder.FromString(params.QueryUUID)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tBodyBoolean     bool            `json:\"boolean\"`\n\t\tBodyInt         int             `json:\"int\"`\n\t\tBodyFloat       float64         `json:\"float\"`\n\t\tBodyString      string          `json:\"string\"`\n\t\tBodyBytes       []byte          `json:\"bytes\"`\n\t\tBodyTime        time.Time       `json:\"time\"`\n\t\tBodyJson        json.RawMessage `json:\"json\"`\n\t\tBodyUUID        string          `json:\"uuid\"`\n\t\tBodyUserID      string          `json:\"user-id\"`\n\t\tBodySlice       []int           `json:\"slice\"`\n\t\tBodyOption      *int            `json:\"option\"`\n\t\tBodyOptionSlice []*int          `json:\"option-slice\"`\n\t}{\n\t\tBodyBoolean:     params.BodyBoolean,\n\t\tBodyBytes:       params.BodyBytes,\n\t\tBodyFloat:       params.BodyFloat,\n\t\tBodyInt:         params.BodyInt,\n\t\tBodyJson:        params.BodyJson,\n\t\tBodyOption:      params.BodyOption,\n\t\tBodyOptionSlice: params.BodyOptionSlice,\n\t\tBodySlice:       params.BodySlice,\n\t\tBodyString:      params.BodyString,\n\t\tBodyTime:        params.BodyTime,\n\t\tBodyUUID:        params.BodyUUID,\n\t\tBodyUserID:      params.BodyUserID,\n\t}\n\n\t// We only want the response body to marshal into these fields and none of the header fields,\n\t// so we'll construct a new struct with only those fields.\n\trespBody := struct {\n\t\tQueryBoolean    bool            `json:\"QueryBoolean\"`\n\t\tQueryInt        int             `json:\"QueryInt\"`\n\t\tQueryFloat      float64         `json:\"QueryFloat\"`\n\t\tQueryString     string          `json:\"QueryString\"`\n\t\tQueryBytes      []byte          `json:\"QueryBytes\"`\n\t\tQueryTime       time.Time       `json:\"QueryTime\"`\n\t\tQueryJson       json.RawMessage `json:\"QueryJson\"`\n\t\tQueryUUID       string          `json:\"QueryUUID\"`\n\t\tQueryUserID     string          `json:\"QueryUserID\"`\n\t\tQuerySlice      []int           `json:\"QuerySlice\"`\n\t\tBodyBoolean     bool            `json:\"boolean\"`\n\t\tBodyInt         int             `json:\"int\"`\n\t\tBodyFloat       float64         `json:\"float\"`\n\t\tBodyString      string          `json:\"string\"`\n\t\tBodyBytes       []byte          `json:\"bytes\"`\n\t\tBodyTime        time.Time       `json:\"time\"`\n\t\tBodyJson        json.RawMessage `json:\"json\"`\n\t\tBodyUUID        string          `json:\"uuid\"`\n\t\tBodyUserID      string          `json:\"user-id\"`\n\t\tBodySlice       []int           `json:\"slice\"`\n\t\tBodyOption      *int            `json:\"option\"`\n\t\tBodyOptionSlice []*int          `json:\"option-slice\"`\n\t}{}\n\n\t// Now make the actual call to the API\n\tvar respHeaders http.Header\n\trespHeaders, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/test.MarshallerTestHandler?%s\", queryString.Encode()), headers, body, &respBody)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Copy the unmarshalled response body into our response struct\n\trespDecoder := &serde{}\n\n\tresp.HeaderBoolean = respDecoder.ToBool(\"HeaderBoolean\", respHeaders.Get(\"x-boolean\"), true)\n\tresp.HeaderInt = respDecoder.ToInt(\"HeaderInt\", respHeaders.Get(\"x-int\"), true)\n\tresp.HeaderFloat = respDecoder.ToFloat64(\"HeaderFloat\", respHeaders.Get(\"x-float\"), true)\n\tresp.HeaderString = respDecoder.ToString(\"HeaderString\", respHeaders.Get(\"x-string\"), true)\n\tresp.HeaderBytes = respDecoder.ToBytes(\"HeaderBytes\", respHeaders.Get(\"x-bytes\"), true)\n\tresp.HeaderTime = respDecoder.ToTime(\"HeaderTime\", respHeaders.Get(\"x-time\"), true)\n\tresp.HeaderJson = respDecoder.ToJSON(\"HeaderJson\", respHeaders.Get(\"x-json\"), true)\n\tresp.HeaderUUID = respDecoder.ToString(\"HeaderUUID\", respHeaders.Get(\"x-uuid\"), true)\n\tresp.HeaderUserID = respDecoder.ToString(\"HeaderUserID\", respHeaders.Get(\"x-user-id\"), true)\n\tresp.HeaderOption = respDecoder.ToStringOption(\"HeaderOption\", respHeaders.Get(\"x-option\"), false)\n\tresp.QueryBoolean = respBody.QueryBoolean\n\tresp.QueryInt = respBody.QueryInt\n\tresp.QueryFloat = respBody.QueryFloat\n\tresp.QueryString = respBody.QueryString\n\tresp.QueryBytes = respBody.QueryBytes\n\tresp.QueryTime = respBody.QueryTime\n\tresp.QueryJson = respBody.QueryJson\n\tresp.QueryUUID = respBody.QueryUUID\n\tresp.QueryUserID = respBody.QueryUserID\n\tresp.QuerySlice = respBody.QuerySlice\n\tresp.BodyBoolean = respBody.BodyBoolean\n\tresp.BodyInt = respBody.BodyInt\n\tresp.BodyFloat = respBody.BodyFloat\n\tresp.BodyString = respBody.BodyString\n\tresp.BodyBytes = respBody.BodyBytes\n\tresp.BodyTime = respBody.BodyTime\n\tresp.BodyJson = respBody.BodyJson\n\tresp.BodyUUID = respBody.BodyUUID\n\tresp.BodyUserID = respBody.BodyUserID\n\tresp.BodySlice = respBody.BodySlice\n\tresp.BodyOption = respBody.BodyOption\n\tresp.BodyOptionSlice = respBody.BodyOptionSlice\n\n\tif respDecoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to unmarshal headers: %w\", respDecoder.LastError)\n\t\treturn\n\t}\n\n\treturn\n}\n\n// Noop allows us to test if a simple HTTP request can be made\nfunc (c *testClient) Noop(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/test.Noop\", nil, nil, nil)\n\treturn err\n}\n\n// NoopWithError allows us to test if the structured errors are returned\nfunc (c *testClient) NoopWithError(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/test.NoopWithError\", nil, nil, nil)\n\treturn err\n}\n\n// PathMultiSegments allows us to wildcard segments and segment URI encoding\nfunc (c *testClient) PathMultiSegments(ctx context.Context, _bool bool, _int int, _string string, uuid string, wildcard []string) (resp TestMultiPathSegment, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/multi/%t/%d/%s/%s/%s\", _bool, _int, url.PathEscape(_string), url.PathEscape(uuid), pathEscapeSlice(wildcard)), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// RawEndpoint allows us to test the clients' ability to send raw requests\n// under auth\nfunc (c *testClient) RawEndpoint(ctx context.Context, id []string, request *http.Request) (*http.Response, error) {\n\trequest = request.WithContext(ctx)\n\n\t// Check the request has the method set, as we can't guess what method is required\n\tif request.Method == \"\" {\n\t\treturn nil, errors.New(\"request.Method must be set\")\n\t}\n\n\t// Set the relative URL for the API call\n\tpath, err := url.Parse(fmt.Sprintf(\"/raw/blah/%s\", pathEscapeSlice(id)))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse api url: %w\", err)\n\t}\n\tif request.URL != nil {\n\t\t// If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema,\n\t\t// host and path to ensure the final URL which hit the right BaseURL\n\t\trequest.URL.Scheme = path.Scheme\n\t\trequest.URL.Host = path.Host\n\t\trequest.URL.Path = path.Path\n\t} else {\n\t\trequest.URL = path\n\t}\n\n\treturn c.base.Do(request)\n}\n\n// RestStyleAPI tests all the ways we can get data into and out of the application\n// using Encore request handlers\nfunc (c *testClient) RestStyleAPI(ctx context.Context, objType int, name string, params TestRestParams) (resp TestRestParams, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\"some-key\": {reqEncoder.FromString(params.HeaderValue)}}\n\n\tqueryString := url.Values{\"Some-Key\": {reqEncoder.FromString(params.QueryValue)}}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tBodyValue string `json:\"Some-Key\"`\n\t\tNested    struct {\n\t\t\tKey   string `json:\"Alice\"`\n\t\t\tValue int    `json:\"bOb\"`\n\t\t\tOk    bool   `json:\"charile\"`\n\t\t} `json:\"Nested\"`\n\t}{\n\t\tBodyValue: params.BodyValue,\n\t\tNested:    params.Nested,\n\t}\n\n\t// We only want the response body to marshal into these fields and none of the header fields,\n\t// so we'll construct a new struct with only those fields.\n\trespBody := struct {\n\t\tQueryValue string `json:\"QueryValue\"`\n\t\tBodyValue  string `json:\"Some-Key\"`\n\t\tNested     struct {\n\t\t\tKey   string `json:\"Alice\"`\n\t\t\tValue int    `json:\"bOb\"`\n\t\t\tOk    bool   `json:\"charile\"`\n\t\t} `json:\"Nested\"`\n\t}{}\n\n\t// Now make the actual call to the API\n\tvar respHeaders http.Header\n\trespHeaders, err = callAPI(ctx, c.base, \"PUT\", fmt.Sprintf(\"/rest/object/%d/%s?%s\", objType, url.PathEscape(name), queryString.Encode()), headers, body, &respBody)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Copy the unmarshalled response body into our response struct\n\trespDecoder := &serde{}\n\n\tresp.HeaderValue = respDecoder.ToString(\"HeaderValue\", respHeaders.Get(\"some-key\"), true)\n\tresp.QueryValue = respBody.QueryValue\n\tresp.BodyValue = respBody.BodyValue\n\tresp.Nested = respBody.Nested\n\n\tif respDecoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to unmarshal headers: %w\", respDecoder.LastError)\n\t\treturn\n\t}\n\n\treturn\n}\n\n// SimpleBodyEcho allows us to exercise the body marshalling from JSON\n// and being returned purely as a body\nfunc (c *testClient) SimpleBodyEcho(ctx context.Context, params TestBodyEcho) (resp TestBodyEcho, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/test.SimpleBodyEcho\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// TestAuthHandler allows us to test the clients ability to add tokens to requests\nfunc (c *testClient) TestAuthHandler(ctx context.Context) (resp TestBodyEcho, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/test.TestAuthHandler\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// UpdateMessage allows us to test an API which takes parameters,\n// but doesn't return anything\nfunc (c *testClient) UpdateMessage(ctx context.Context, clientID string, params TestBodyEcho) error {\n\t_, err := callAPI(ctx, c.base, \"PUT\", fmt.Sprintf(\"/last_message/%s\", url.PathEscape(clientID)), nil, params, nil)\n\treturn err\n}\n\ntype ValidationRequest struct {\n\tMsg string\n}\n\n// ValidationClient Provides you access to call public and authenticated APIs on validation. The concrete implementation is validationClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype ValidationClient interface {\n\tTestOne(ctx context.Context, params ValidationRequest) error\n}\n\ntype validationClient struct {\n\tbase *baseClient\n}\n\nvar _ ValidationClient = (*validationClient)(nil)\n\nfunc (c *validationClient) TestOne(ctx context.Context, params ValidationRequest) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/validation.TestOne\", nil, params, nil)\n\treturn err\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\tauthGenerator func(ctx context.Context) (EchoAuthParams, error) // The function which will add the authentication data to the requests\n\thttpClient    HTTPDoer                                          // The HTTP client which will be used for all API requests\n\tbaseURL       *url.URL                                          // The base URL which API requests will be made against\n\tuserAgent     string                                            // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// If a authorization data generator is present, call it and add the returned token to the request\n\tif b.authGenerator != nil {\n\t\tif authData, err := b.authGenerator(req.Context()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to create authorization token for api request: %w\", err)\n\t\t} else {\n\t\t\tauthEncoder := &serde{}\n\n\t\t\t// Add the auth fields to the query string\n\t\t\tquery := req.URL.Query()\n\t\t\tfor _, v := range authEncoder.FromIntList(authData.Query) {\n\t\t\t\tquery.Add(\"query\", v)\n\t\t\t}\n\t\t\tquery.Set(\"new-auth\", authEncoder.FromBool(authData.NewAuth))\n\t\t\treq.URL.RawQuery = query.Encode()\n\n\t\t\t// Add the auth fields to the headers\n\t\t\treq.Header.Set(\"x-header\", authEncoder.FromString(authData.Header))\n\t\t\treq.Header.Set(\"x-auth-int\", authEncoder.FromInt(authData.AuthInt))\n\t\t\treq.Header.Set(\"authorization\", authEncoder.FromString(authData.Authorization))\n\n\t\t\tif authEncoder.LastError != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to marshal authentication data: %w\", authEncoder.LastError)\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// pathEscapeSlice escapes a slice of strings and then joins them into a single string\nfunc pathEscapeSlice(paths []string) string {\n\tvar escapedPaths strings.Builder\n\tfor i, path := range paths {\n\t\tif i > 0 {\n\t\t\tescapedPaths.WriteString(\"/\")\n\t\t}\n\t\tescapedPaths.WriteString(url.PathEscape(path))\n\t}\n\treturn escapedPaths.String()\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n\n// serde is used to serialize request data into strings and deserialize response data from strings\ntype serde struct {\n\tLastError      error // The last error that occurred\n\tNonEmptyValues int   // The number of values this decoder has decoded\n}\n\nfunc (e *serde) FromInt(s int) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatInt(int64(s), 10)\n}\n\nfunc (e *serde) FromString(s string) (v string) {\n\te.NonEmptyValues++\n\treturn s\n}\n\nfunc (e *serde) ToInt(field string, s string, required bool) (v int) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tx, err := strconv.ParseInt(s, 10, 64)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn int(x)\n}\n\nfunc (e *serde) ToString(field string, s string, required bool) (v string) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\treturn s\n}\n\nfunc (e *serde) FromBool(s bool) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatBool(s)\n}\n\nfunc (e *serde) FromFloat64(s float64) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatFloat(s, uint8(0x66), -1, 64)\n}\n\nfunc (e *serde) FromBytes(s []byte) (v string) {\n\te.NonEmptyValues++\n\treturn base64.URLEncoding.EncodeToString(s)\n}\n\nfunc (e *serde) FromTime(s time.Time) (v string) {\n\te.NonEmptyValues++\n\treturn s.Format(time.RFC3339)\n}\n\nfunc (e *serde) FromJSON(s json.RawMessage) (v string) {\n\te.NonEmptyValues++\n\treturn string(s)\n}\n\nfunc (e *serde) FromStringOption(s *string) (v []string) {\n\tif s == nil {\n\t\treturn nil\n\t}\n\te.NonEmptyValues++\n\treturn []string{e.FromString(*s)}\n}\n\nfunc (e *serde) FromIntList(s []int) (v []string) {\n\te.NonEmptyValues++\n\tfor _, x := range s {\n\t\tv = append(v, e.FromInt(x))\n\t}\n\treturn v\n}\n\nfunc (e *serde) ToBool(field string, s string, required bool) (v bool) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tv, err := strconv.ParseBool(s)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn v\n}\n\nfunc (e *serde) ToFloat64(field string, s string, required bool) (v float64) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tx, err := strconv.ParseFloat(s, 64)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn x\n}\n\nfunc (e *serde) ToBytes(field string, s string, required bool) (v []byte) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tv, err := base64.URLEncoding.DecodeString(s)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn v\n}\n\nfunc (e *serde) ToTime(field string, s string, required bool) (v time.Time) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tv, err := time.Parse(time.RFC3339, s)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn v\n}\n\nfunc (e *serde) ToJSON(field string, s string, required bool) (v json.RawMessage) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\treturn json.RawMessage(s)\n}\n\nfunc (e *serde) ToStringOption(field string, s string, required bool) (v *string) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tval := e.ToString(field, s, required)\n\treturn &val\n}\n\n// setErr sets the last error within the object if one is not already set\nfunc (e *serde) setErr(msg, field string, err error) {\n\tif err != nil && e.LastError == nil {\n\t\te.LastError = fmt.Errorf(\"%s: %s: %w\", field, msg, err)\n\t}\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/js/client.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-slug.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the slug Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.cache = new cache.ServiceClient(base)\n        this.di = new di.ServiceClient(base)\n        this.echo = new echo.ServiceClient(base)\n        this.emptycfg = new emptycfg.ServiceClient(base)\n        this.endtoend = new endtoend.ServiceClient(base)\n        this.middleware = new middleware.ServiceClient(base)\n        this.test = new test.ServiceClient(base)\n        this.validation = new validation.ServiceClient(base)\n    }\n}\n\nclass CacheServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.GetList = this.GetList.bind(this)\n        this.GetStruct = this.GetStruct.bind(this)\n        this.Incr = this.Incr.bind(this)\n        this.PostList = this.PostList.bind(this)\n        this.PostStruct = this.PostStruct.bind(this)\n    }\n\n    async GetList(key) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/cache/list/${encodeURIComponent(key)}`)\n        return await resp.json()\n    }\n\n    async GetStruct(key) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/cache/struct/${encodeURIComponent(key)}`)\n        return await resp.json()\n    }\n\n    async Incr(key) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/cache/incr/${encodeURIComponent(key)}`)\n        return await resp.json()\n    }\n\n    async PostList(key, val) {\n        await this.baseClient.callTypedAPI(\"POST\", `/cache/list/${encodeURIComponent(key)}/${encodeURIComponent(val)}`)\n    }\n\n    async PostStruct(key, val) {\n        await this.baseClient.callTypedAPI(\"POST\", `/cache/struct/${encodeURIComponent(key)}/${encodeURIComponent(val)}`)\n    }\n}\n\nexport const cache = {\n    ServiceClient: CacheServiceClient\n}\n\nclass DiServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.One = this.One.bind(this)\n        this.Three = this.Three.bind(this)\n        this.Two = this.Two.bind(this)\n    }\n\n    async One() {\n        await this.baseClient.callTypedAPI(\"POST\", `/di/one`)\n    }\n\n    async Three(method, body, options) {\n        return this.baseClient.callAPI(method, `/di/raw`, body, options)\n    }\n\n    async Two() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/di/two`)\n        return await resp.json()\n    }\n}\n\nexport const di = {\n    ServiceClient: DiServiceClient\n}\n\nclass EchoServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.AppMeta = this.AppMeta.bind(this)\n        this.BasicEcho = this.BasicEcho.bind(this)\n        this.ConfigValues = this.ConfigValues.bind(this)\n        this.CustomHTTPStatus = this.CustomHTTPStatus.bind(this)\n        this.Echo = this.Echo.bind(this)\n        this.EmptyEcho = this.EmptyEcho.bind(this)\n        this.Env = this.Env.bind(this)\n        this.HeadersEcho = this.HeadersEcho.bind(this)\n        this.MuteEcho = this.MuteEcho.bind(this)\n        this.NilResponse = this.NilResponse.bind(this)\n        this.NonBasicEcho = this.NonBasicEcho.bind(this)\n        this.Noop = this.Noop.bind(this)\n        this.Pong = this.Pong.bind(this)\n        this.Publish = this.Publish.bind(this)\n    }\n\n    /**\n     * AppMeta returns app metadata.\n     */\n    async AppMeta() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.AppMeta`)\n        return await resp.json()\n    }\n\n    /**\n     * BasicEcho echoes back the request data.\n     */\n    async BasicEcho(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.BasicEcho`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    async ConfigValues() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.ConfigValues`)\n        return await resp.json()\n    }\n\n    /**\n     * CustomHTTPStatus allows testing of custom HTTP status codes via encore:\"httpstatus\" tag\n     */\n    async CustomHTTPStatus() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.CustomHTTPStatus`)\n        return await resp.json()\n    }\n\n    /**\n     * Echo echoes back the request data.\n     */\n    async Echo(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.Echo`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    /**\n     * EmptyEcho echoes back the request data.\n     */\n    async EmptyEcho(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.EmptyEcho`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    /**\n     * Env returns the environment.\n     */\n    async Env() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.Env`)\n        return await resp.json()\n    }\n\n    /**\n     * HeadersEcho echoes back the request headers\n     */\n    async HeadersEcho(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"x-int\":    String(params.Int),\n            \"x-string\": params.String,\n        })\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.HeadersEcho`, undefined, {headers})\n\n        //Populate the return object from the JSON body and received headers\n        const rtn = await resp.json()\n        rtn.Int = parseInt(mustBeSet(\"Header `x-int`\", resp.headers.get(\"x-int\")), 10)\n        rtn.String = mustBeSet(\"Header `x-string`\", resp.headers.get(\"x-string\"))\n        return rtn\n    }\n\n    /**\n     * MuteEcho absorbs a request\n     */\n    async MuteEcho(params) {\n        // Convert our params into the objects we need for the request\n        const query = makeRecord({\n            key:   params.Key,\n            value: params.Value,\n        })\n\n        await this.baseClient.callTypedAPI(\"GET\", `/echo.MuteEcho`, undefined, {query})\n    }\n\n    /**\n     * NilResponse returns a nil response and nil error\n     */\n    async NilResponse() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.NilResponse`)\n        return await resp.json()\n    }\n\n    /**\n     * NonBasicEcho echoes back the request data.\n     */\n    async NonBasicEcho(pathString, pathInt, pathWild, params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"x-header-number\": String(params.HeaderNumber),\n            \"x-header-string\": params.HeaderString,\n        })\n\n        const query = makeRecord({\n            no:     String(params.QueryNumber),\n            optnum: params.OptQueryNumber === undefined ? undefined : String(params.OptQueryNumber),\n            optstr: params.OptQueryString,\n            string: params.QueryString,\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            AnonStruct:       params.AnonStruct,\n            AuthHeader:       params.AuthHeader,\n            AuthQuery:        params.AuthQuery,\n            PathInt:          params.PathInt,\n            PathString:       params.PathString,\n            PathWild:         params.PathWild,\n            RawStruct:        params.RawStruct,\n            Struct:           params.Struct,\n            StructMap:        params.StructMap,\n            StructMapPtr:     params.StructMapPtr,\n            StructPtr:        params.StructPtr,\n            StructSlice:      params.StructSlice,\n            \"formatted_nest\": params[\"formatted_nest\"],\n        }\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/NonBasicEcho/${encodeURIComponent(pathString)}/${encodeURIComponent(pathInt)}/${pathWild.map(encodeURIComponent).join(\"/\")}`, JSON.stringify(body), {headers, query})\n\n        //Populate the return object from the JSON body and received headers\n        const rtn = await resp.json()\n        rtn.HeaderString = mustBeSet(\"Header `x-header-string`\", resp.headers.get(\"x-header-string\"))\n        rtn.HeaderNumber = parseInt(mustBeSet(\"Header `x-header-number`\", resp.headers.get(\"x-header-number\")), 10)\n        return rtn\n    }\n\n    /**\n     * Noop does nothing\n     */\n    async Noop() {\n        await this.baseClient.callTypedAPI(\"GET\", `/echo.Noop`)\n    }\n\n    /**\n     * Pong returns a bird tuple\n     */\n    async Pong() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/echo.Pong`)\n        return await resp.json()\n    }\n\n    /**\n     * Publish publishes a request on a topic\n     */\n    async Publish() {\n        await this.baseClient.callTypedAPI(\"POST\", `/echo.Publish`)\n    }\n}\n\nexport const echo = {\n    ServiceClient: EchoServiceClient\n}\n\nclass EmptycfgServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.AnAPI = this.AnAPI.bind(this)\n    }\n\n    async AnAPI() {\n        await this.baseClient.callTypedAPI(\"POST\", `/emptycfg.AnAPI`)\n    }\n}\n\nexport const emptycfg = {\n    ServiceClient: EmptycfgServiceClient\n}\n\nclass EndtoendServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.GeneratedWrappersEndToEndTest = this.GeneratedWrappersEndToEndTest.bind(this)\n    }\n\n    async GeneratedWrappersEndToEndTest() {\n        await this.baseClient.callTypedAPI(\"GET\", `/generated-wrappers-end-to-end-test`)\n    }\n}\n\nexport const endtoend = {\n    ServiceClient: EndtoendServiceClient\n}\n\nclass MiddlewareServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.Error = this.Error.bind(this)\n        this.ResponseGen = this.ResponseGen.bind(this)\n        this.ResponseRewrite = this.ResponseRewrite.bind(this)\n    }\n\n    async Error() {\n        await this.baseClient.callTypedAPI(\"POST\", `/middleware.Error`)\n    }\n\n    async ResponseGen(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/middleware.ResponseGen`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    async ResponseRewrite(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/middleware.ResponseRewrite`, JSON.stringify(params))\n        return await resp.json()\n    }\n}\n\nexport const middleware = {\n    ServiceClient: MiddlewareServiceClient\n}\n\nclass TestServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.GetMessage = this.GetMessage.bind(this)\n        this.MarshallerTestHandler = this.MarshallerTestHandler.bind(this)\n        this.Noop = this.Noop.bind(this)\n        this.NoopWithError = this.NoopWithError.bind(this)\n        this.PathMultiSegments = this.PathMultiSegments.bind(this)\n        this.RawEndpoint = this.RawEndpoint.bind(this)\n        this.RestStyleAPI = this.RestStyleAPI.bind(this)\n        this.SimpleBodyEcho = this.SimpleBodyEcho.bind(this)\n        this.TestAuthHandler = this.TestAuthHandler.bind(this)\n        this.UpdateMessage = this.UpdateMessage.bind(this)\n    }\n\n    /**\n     * GetMessage allows us to test an API which takes no parameters,\n     * but returns data. It also tests two API's on the same path with different HTTP methods\n     */\n    async GetMessage(clientID) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/last_message/${encodeURIComponent(clientID)}`)\n        return await resp.json()\n    }\n\n    /**\n     * MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all\n     * the field types. It simply echos all the responses back to the client\n     */\n    async MarshallerTestHandler(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"x-boolean\": String(params.HeaderBoolean),\n            \"x-bytes\":   String(params.HeaderBytes),\n            \"x-float\":   String(params.HeaderFloat),\n            \"x-int\":     String(params.HeaderInt),\n            \"x-json\":    JSON.stringify(params.HeaderJson),\n            \"x-option\":  params.HeaderOption === undefined ? undefined : String(params.HeaderOption),\n            \"x-string\":  params.HeaderString,\n            \"x-time\":    String(params.HeaderTime),\n            \"x-user-id\": String(params.HeaderUserID),\n            \"x-uuid\":    String(params.HeaderUUID),\n        })\n\n        const query = makeRecord({\n            boolean:   String(params.QueryBoolean),\n            bytes:     String(params.QueryBytes),\n            float:     String(params.QueryFloat),\n            int:       String(params.QueryInt),\n            json:      JSON.stringify(params.QueryJson),\n            slice:     params.QuerySlice.map((v) => String(v)),\n            string:    params.QueryString,\n            time:      String(params.QueryTime),\n            \"user-id\": String(params.QueryUserID),\n            uuid:      String(params.QueryUUID),\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            boolean:        params.boolean,\n            bytes:          params.bytes,\n            float:          params.float,\n            int:            params.int,\n            json:           params.json,\n            option:         params.option,\n            \"option-slice\": params[\"option-slice\"],\n            slice:          params.slice,\n            string:         params.string,\n            time:           params.time,\n            \"user-id\":      params[\"user-id\"],\n            uuid:           params.uuid,\n        }\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/test.MarshallerTestHandler`, JSON.stringify(body), {headers, query})\n\n        //Populate the return object from the JSON body and received headers\n        const rtn = await resp.json()\n        rtn.HeaderBoolean = mustBeSet(\"Header `x-boolean`\", resp.headers.get(\"x-boolean\")).toLowerCase() === \"true\"\n        rtn.HeaderInt = parseInt(mustBeSet(\"Header `x-int`\", resp.headers.get(\"x-int\")), 10)\n        rtn.HeaderFloat = Number(mustBeSet(\"Header `x-float`\", resp.headers.get(\"x-float\")))\n        rtn.HeaderString = mustBeSet(\"Header `x-string`\", resp.headers.get(\"x-string\"))\n        rtn.HeaderBytes = mustBeSet(\"Header `x-bytes`\", resp.headers.get(\"x-bytes\"))\n        rtn.HeaderTime = mustBeSet(\"Header `x-time`\", resp.headers.get(\"x-time\"))\n        rtn.HeaderJson = JSON.parse(mustBeSet(\"Header `x-json`\", resp.headers.get(\"x-json\")))\n        rtn.HeaderUUID = mustBeSet(\"Header `x-uuid`\", resp.headers.get(\"x-uuid\"))\n        rtn.HeaderUserID = mustBeSet(\"Header `x-user-id`\", resp.headers.get(\"x-user-id\"))\n        rtn.HeaderOption = resp.headers.get(\"x-option\")\n        return rtn\n    }\n\n    /**\n     * Noop allows us to test if a simple HTTP request can be made\n     */\n    async Noop() {\n        await this.baseClient.callTypedAPI(\"POST\", `/test.Noop`)\n    }\n\n    /**\n     * NoopWithError allows us to test if the structured errors are returned\n     */\n    async NoopWithError() {\n        await this.baseClient.callTypedAPI(\"POST\", `/test.NoopWithError`)\n    }\n\n    /**\n     * PathMultiSegments allows us to wildcard segments and segment URI encoding\n     */\n    async PathMultiSegments(bool, _int, string, uuid, wildcard) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/multi/${encodeURIComponent(bool)}/${encodeURIComponent(_int)}/${encodeURIComponent(string)}/${encodeURIComponent(uuid)}/${wildcard.map(encodeURIComponent).join(\"/\")}`)\n        return await resp.json()\n    }\n\n    /**\n     * RawEndpoint allows us to test the clients' ability to send raw requests\n     * under auth\n     */\n    async RawEndpoint(method, id, body, options) {\n        return this.baseClient.callAPI(method, `/raw/blah/${id.map(encodeURIComponent).join(\"/\")}`, body, options)\n    }\n\n    /**\n     * RestStyleAPI tests all the ways we can get data into and out of the application\n     * using Encore request handlers\n     */\n    async RestStyleAPI(objType, name, params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"some-key\": params.HeaderValue,\n        })\n\n        const query = makeRecord({\n            \"Some-Key\": params.QueryValue,\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            Nested:     params.Nested,\n            \"Some-Key\": params[\"Some-Key\"],\n        }\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"PUT\", `/rest/object/${encodeURIComponent(objType)}/${encodeURIComponent(name)}`, JSON.stringify(body), {headers, query})\n\n        //Populate the return object from the JSON body and received headers\n        const rtn = await resp.json()\n        rtn.HeaderValue = mustBeSet(\"Header `some-key`\", resp.headers.get(\"some-key\"))\n        return rtn\n    }\n\n    /**\n     * SimpleBodyEcho allows us to exercise the body marshalling from JSON\n     * and being returned purely as a body\n     */\n    async SimpleBodyEcho(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/test.SimpleBodyEcho`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    /**\n     * TestAuthHandler allows us to test the clients ability to add tokens to requests\n     */\n    async TestAuthHandler() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/test.TestAuthHandler`)\n        return await resp.json()\n    }\n\n    /**\n     * UpdateMessage allows us to test an API which takes parameters,\n     * but doesn't return anything\n     */\n    async UpdateMessage(clientID, params) {\n        await this.baseClient.callTypedAPI(\"PUT\", `/last_message/${encodeURIComponent(clientID)}`, JSON.stringify(params))\n    }\n}\n\nexport const test = {\n    ServiceClient: TestServiceClient\n}\n\nclass ValidationServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.TestOne = this.TestOne.bind(this)\n    }\n\n    async TestOne(params) {\n        await this.baseClient.callTypedAPI(\"POST\", `/validation.TestOne`, JSON.stringify(params))\n    }\n}\n\nexport const validation = {\n    ServiceClient: ValidationServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n// mustBeSet will throw an APIError with the Data Loss code if value is null or undefined\nfunction mustBeSet(field, value) {\n    if (value === null || value === undefined) {\n        throw new APIError(\n            500,\n            {\n                code: ErrCode.DataLoss,\n                message: `${field} was unexpectedly ${value}`, // ${value} will create the string \"null\" or \"undefined\"\n            },\n        )\n    }\n    return value\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"slug-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n\n    }\n\n    async getAuthData() {\n        let authData;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data = {};\n\n            data.query = makeRecord({\n                \"new-auth\": String(authData.NewAuth),\n                query:      authData.Query.map((v) => String(v)),\n            });\n            data.headers = makeRecord({\n                authorization: authData.Authorization,\n                \"x-auth-int\":  String(authData.AuthInt),\n                \"x-header\":    authData.Header,\n            })\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/js/main.js",
    "content": "import \"isomorphic-fetch\";\nimport { deepEqual } from \"assert\";\n\nimport Client, { ErrCode, isAPIError } from \"./client.js\";\n\nif (process.argv.length < 3) {\n  console.error(\"Usage: npm run test -- <host:port>\");\n  console.log(`Got ${process.argv.length} arguments`);\n  process.exit(1);\n}\n\n// Create the client\nconst api = new Client(\"http://\" + process.argv[2]);\n\n// Test a simple no-op\nawait api.test.Noop();\n\n// Test we get back the right structured error\nawait assertStructuredError(\n  api.test.NoopWithError(),\n  ErrCode.Unimplemented,\n  \"totally not implemented yet\"\n);\n\n// Test a simple echo\nconst echoRsp = await api.test.SimpleBodyEcho({ Message: \"hello world\" });\ndeepEqual(echoRsp.Message, \"hello world\", \"Wanted body to be 'hello world'\");\n\n// Check our UpdateMessage and GetMessage API's\nlet getRsp = await api.test.GetMessage(\"javascript\");\ndeepEqual(getRsp.Message, \"\", \"Expected no message on first request\");\n\nawait api.test.UpdateMessage(\"javascript\", { Message: \"updating now\" });\n\ngetRsp = await api.test.GetMessage(\"javascript\");\ndeepEqual(getRsp.Message, \"updating now\", \"Expected data from Update request\");\n\n// Test the rest API which uses all input types (query string, json body and header fields)\n// as well as nested structs and path segments in the URL\nconst restRsp = await api.test.RestStyleAPI(5, \"hello\", {\n  HeaderValue: \"this is the header field\",\n  QueryValue: \"this is a query string field\",\n  \"Some-Key\": \"this is the body field\",\n  Nested: {\n    Alice: \"the nested key\",\n    bOb: 8,\n    charile: true\n  }\n});\ndeepEqual(\n  restRsp.HeaderValue,\n  \"this is the header field\",\n  \"expected header value\"\n);\ndeepEqual(\n  restRsp.QueryValue,\n  \"this is a query string field\",\n  \"expected query value\"\n);\ndeepEqual(restRsp[\"Some-Key\"], \"this is the body field\", \"expected body value\");\ndeepEqual(\n  restRsp.Nested.Alice,\n  \"hello + the nested key\",\n  \"expected nested key\"\n);\ndeepEqual(restRsp.Nested.bOb, 5 + 8, \"expected nested value\");\ndeepEqual(restRsp.Nested.charile, true, \"expected nested ok\");\n\n// Full marshalling test with randomised payloads\nfunction rInt() {\n  return Math.floor(Math.random() * 10000000);\n}\n\nconst params = {\n  HeaderBoolean: Math.random() > 0.5,\n  HeaderInt: rInt(),\n  HeaderFloat: Math.random(),\n  HeaderString: \"header string\",\n  HeaderBytes: \"aGVsbG8K\",\n  HeaderTime: new Date(Math.floor(Date.now() / 1000) * 1000)\n    .toISOString()\n    .replace(\".000Z\", \"Z\"),\n  HeaderJson: { hello: \"world\" },\n  HeaderUUID: \"2553e3a4-5d9f-4716-82a2-b9bdc20a3263\",\n  HeaderUserID: \"432\",\n  HeaderOption: \"test\",\n  QueryBoolean: Math.random() > 0.5,\n  QueryInt: rInt(),\n  QueryFloat: Math.random(),\n  QueryString: \"query string\",\n  QueryBytes: \"d29ybGQK\",\n  QueryTime: new Date(Math.floor(Date.now() / 1000) * 1000)\n    .toISOString()\n    .replace(\".000Z\", \"Z\"),\n  QueryJson: { value: true },\n  QueryUUID: \"84b7463d-6000-4678-9d94-1d526bb5217c\",\n  QueryUserID: \"9udfa\",\n  QuerySlice: [rInt(), rInt(), rInt(), rInt()],\n  boolean: Math.random() > 0.5,\n  int: Math.floor(Math.random() * 10000000),\n  float: Math.random(),\n  string: \"body string\",\n  bytes: \"aXMgaXQgbWUgeW91IGFyZSBsb29raW5nIGZvcj8K\",\n  time: new Date(Math.floor(Date.now() / 1000) * 1000)\n    .toISOString()\n    .replace(\".000Z\", \"Z\"),\n  json: { json_value: 4321 },\n  uuid: \"c227acf4-1902-4c85-8027-623d47ef4c8a\",\n  \"user-id\": \"✉️\",\n  slice: [rInt(), rInt(), rInt(), rInt(), rInt(), rInt()],\n  option: 5,\n  \"option-slice\": [1, null, 2]\n};\nconst mResp = await api.test.MarshallerTestHandler(params);\ndeepEqual(mResp, params, \"Expected the same response from the marshaller test\");\n\n// Test auth handlers\nawait assertStructuredError(\n  api.test.TestAuthHandler(),\n  ErrCode.Unauthenticated,\n  \"missing auth param\"\n);\n\n// Test with static auth data\n{\n  const api = new Client(\"http://\" + process.argv[2], {\n    auth: {\n      AuthInt: 34,\n      Authorization: \"Bearer tokendata\",\n      NewAuth: false,\n      Header: \"\",\n      Query: []\n    }\n  });\n\n  const resp = await api.test.TestAuthHandler();\n  deepEqual(resp.Message, \"user::true\", \"expected the user ID back\");\n}\n\n// Test with auth data generator function\n{\n  let tokenToReturn = \"tokendata\";\n  const api = new Client(\"http://\" + process.argv[2], {\n    auth: () => {\n      return {\n        Authorization: \"Bearer \" + tokenToReturn,\n        AuthInt: 34,\n        NewAuth: false,\n        Header: \"\",\n        Query: []\n      };\n    }\n  });\n\n  // With a valid token\n  const resp = await api.test.TestAuthHandler();\n  deepEqual(resp.Message, \"user::true\", \"expected the user ID back\");\n\n  // With an invalid token\n  tokenToReturn = \"invalid-token-value\";\n  await assertStructuredError(\n    api.test.TestAuthHandler(),\n    ErrCode.Unauthenticated,\n    \"invalid token\"\n  );\n}\n\n// Test with headers and query string auth data\n{\n  const api = new Client(\"http://\" + process.argv[2], {\n    auth: {\n      Authorization: \"\",\n      AuthInt: 34,\n      NewAuth: true,\n      Header: \"102\",\n      Query: [42, 100, -50, 10]\n    }\n  });\n\n  const resp = await api.test.TestAuthHandler();\n  deepEqual(resp.Message, \"second_user::true\", \"expected the user ID back\");\n}\n\n// Test the raw endpoint\n{\n  const api = new Client(\"http://\" + process.argv[2], {\n    auth: {\n      AuthInt: 34,\n      Authorization: \"Bearer tokendata\",\n      NewAuth: false,\n      Header: \"\",\n      Query: []\n    }\n  });\n\n  const resp = await api.test.RawEndpoint(\n    \"PUT\",\n    [\"hello\"],\n    \"this is a test body\",\n    {\n      headers: { \"X-Test-Header\": \"test\" },\n      query: { foo: \"bar\" }\n    }\n  );\n\n  deepEqual(resp.status, 201, \"expected the status code to be 201\");\n\n  const response = await resp.json();\n\n  deepEqual(\n    response,\n    {\n      Body: \"this is a test body\",\n      Header: \"test\",\n      PathParam: \"hello\",\n      QueryString: \"bar\"\n    },\n    \"expected the response to match\"\n  );\n}\n\n// Test path encoding\nconst resp = await api.test.PathMultiSegments(\n  true,\n  342,\n  \"foo/blah/should/get/escaped\",\n  \"503f4487-1e15-4c37-9a80-7b70f86387bb\",\n  [\"foo/bar\", \"blah\", \"seperate/segments = great success\"]\n);\ndeepEqual(resp.Boolean, true, \"expected the boolean to be true\");\ndeepEqual(resp.Int, 342, \"expected the int to be 342\");\ndeepEqual(\n  resp.String,\n  \"foo/blah/should/get/escaped\",\n  \"invalid string field returned\"\n);\ndeepEqual(\n  resp.UUID,\n  \"503f4487-1e15-4c37-9a80-7b70f86387bb\",\n  \"invalid UUID returned\"\n);\ndeepEqual(\n  resp.Wildcard,\n  \"foo/bar/blah/seperate/segments = great success\",\n  \"invalid wildcard field returned\"\n);\n\n// Test validation\n{\n  await api.validation.TestOne({\n    Msg: \"pass\"\n  });\n  await assertStructuredError(\n    api.validation.TestOne({ Msg: \"fail\" }),\n    ErrCode.InvalidArgument,\n    \"validation failed: bad message\"\n  );\n  const client = new Client(\"http://\" + process.argv[2], {\n    auth: {\n      AuthInt: 0,\n      Authorization: \"\",\n      NewAuth: false,\n      Header: \"fail-validation\",\n      Query: []\n    }\n  });\n  await assertStructuredError(\n    client.test.Noop(),\n    ErrCode.InvalidArgument,\n    \"validation failed: auth validation fail\"\n  );\n}\n\n// Test middleware\n{\n  await assertStructuredError(\n    api.middleware.Error(),\n    ErrCode.Internal,\n    \"middleware error\"\n  );\n  const resp1 = await api.middleware.ResponseRewrite({ Msg: \"foo\" });\n  deepEqual(resp1.Msg, \"middleware(req=foo, resp=handler(foo))\");\n\n  const resp2 = await api.middleware.ResponseGen({ Msg: \"foo\" });\n  deepEqual(resp2.Msg, \"middleware generated\");\n}\n\n// Client test completed\nprocess.exit(0);\n\nasync function assertStructuredError(promise, code, message) {\n  let errorOccurred = false;\n  try {\n    await promise;\n  } catch (err) {\n    errorOccurred = true;\n    if (isAPIError(err)) {\n      if (err.code !== code) {\n        throw new Error(\n          `Expected error code ${code}, got ${err.code} with message \"${err.message}\"`\n        );\n      }\n      if (err.message !== message) {\n        throw new Error(\n          `Expected error message \"${message}\", got \"${err.message}\"`\n        );\n      }\n    } else {\n      throw new Error(`Expected APIError, got ${err}`);\n    }\n  }\n\n  if (!errorOccurred) {\n    throw new Error(\"No error was thrown during call to NoopWithError\");\n  }\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"echo_client/golang/client\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nvar assertNumber = 1\n\nfunc main() {\n\t// Even on a slow machine, the client should be able to connect and run this test script in 30 seconds\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\t// Check we where given the host:port of the running echo app\n\tif len(os.Args) != 2 {\n\t\tfmt.Println(\"Usage:\", filepath.Base(os.Args[0]), \"<host:port>\")\n\t\tfmt.Println(\"Got\", len(os.Args), \"arguments\")\n\t\tos.Exit(1)\n\t}\n\n\t// Create the client\n\tapi, err := client.New(\n\t\tclient.BaseURL(fmt.Sprintf(\"http://%s\", os.Args[1])),\n\t)\n\tassert(err, nil, \"Wanted no error from client creation\")\n\n\t// Test a simple no-op\n\terr = api.Test.Noop(ctx)\n\tassert(err, nil, \"Wanted no error from noop\")\n\n\t// Test we get back the right structured error\n\terr = api.Test.NoopWithError(ctx)\n\tassertStructuredError(err, client.ErrUnimplemented, \"totally not implemented yet\")\n\n\t// Test a simple echo\n\techoRsp, err := api.Test.SimpleBodyEcho(ctx, client.TestBodyEcho{\"hello world\"})\n\tassert(err, nil, \"Wanted no error from simple body echo\")\n\tassert(echoRsp.Message, \"hello world\", \"Wanted body to be 'hello world'\")\n\n\t// Check our UpdateMessage and GetMessage API's\n\tgetRsp, err := api.Test.GetMessage(ctx, \"go\")\n\tassert(err, nil, \"Wanted no error from get message\")\n\tassert(getRsp.Message, \"\", \"Expected no message on first request\")\n\n\terr = api.Test.UpdateMessage(ctx, \"go\", client.TestBodyEcho{\"updating now\"})\n\tassert(err, nil, \"Wanted no error from update message\")\n\n\tgetRsp, err = api.Test.GetMessage(ctx, \"go\")\n\tassert(err, nil, \"Wanted no error from get message\")\n\tassert(getRsp.Message, \"updating now\", \"Expected data from Update request\")\n\n\t// Test the rest API which uses all input types (query string, json body and header fields)\n\t// as well as nested structs and path segments in the URL\n\trestRsp, err := api.Test.RestStyleAPI(ctx, 5, \"hello\", client.TestRestParams{\n\t\tHeaderValue: \"this is the header field\",\n\t\tQueryValue:  \"this is a query string field\",\n\t\tBodyValue:   \"this is the body field\",\n\t\tNested: struct {\n\t\t\tKey   string `json:\"Alice\"`\n\t\t\tValue int    `json:\"bOb\"`\n\t\t\tOk    bool   `json:\"charile\"`\n\t\t}{\n\t\t\tKey:   \"the nested key\",\n\t\t\tValue: 8,\n\t\t\tOk:    true,\n\t\t},\n\t})\n\tassert(err, nil, \"Wanted no error from rest style api\")\n\tassert(restRsp.HeaderValue, \"this is the header field\", \"expected header value\")\n\tassert(restRsp.QueryValue, \"this is a query string field\", \"expected query value\")\n\tassert(restRsp.BodyValue, \"this is the body field\", \"expected body value\")\n\tassert(restRsp.Nested.Key, \"hello + the nested key\", \"expected nested key\")\n\tassert(restRsp.Nested.Value, 5+8, \"expected nested value\")\n\tassert(restRsp.Nested.Ok, true, \"expected nested ok\")\n\n\t// Full marshalling test with randomised payloads\n\tr := rand.New(rand.NewSource(time.Now().UnixNano()))\n\theaderBytes := make([]byte, 1+r.Intn(128))\n\tqueryBytes := make([]byte, 1+r.Intn(128))\n\tbodyBytes := make([]byte, 1+r.Intn(128))\n\tr.Read(headerBytes)\n\tr.Read(queryBytes)\n\tr.Read(bodyBytes)\n\tparams := client.TestMarshallerTest[int]{\n\t\tHeaderBoolean: r.Float32() > 0.5,\n\t\tHeaderInt:     r.Int(),\n\t\tHeaderFloat:   r.Float64(),\n\t\tHeaderString:  \"header string\",\n\t\tHeaderBytes:   headerBytes,\n\t\tHeaderTime:    time.Now().Truncate(time.Second),\n\t\tHeaderJson:    json.RawMessage(\"{\\\"hello\\\":\\\"world\\\"}\"),\n\t\tHeaderUUID:    \"2553e3a4-5d9f-4716-82a2-b9bdc20a3263\",\n\t\tHeaderUserID:  \"432\",\n\t\tQueryBoolean:  r.Float32() > 0.5,\n\t\tQueryInt:      r.Int(),\n\t\tQueryFloat:    r.Float64(),\n\t\tQueryString:   \"query string\",\n\t\tQueryBytes:    headerBytes,\n\t\tQueryTime:     time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),\n\t\tQueryJson:     json.RawMessage(\"true\"),\n\t\tQueryUUID:     \"84b7463d-6000-4678-9d94-1d526bb5217c\",\n\t\tQueryUserID:   \"9udfa\",\n\t\tQuerySlice:    []int{r.Int(), r.Int(), r.Int(), r.Int()},\n\t\tBodyBoolean:   r.Float32() > 0.5,\n\t\tBodyInt:       r.Int(),\n\t\tBodyFloat:     r.Float64(),\n\t\tBodyString:    \"body string\",\n\t\tBodyBytes:     bodyBytes,\n\t\tBodyTime:      time.Now().Add(time.Duration(rand.Intn(1024)) * time.Hour).Truncate(time.Second),\n\t\tBodyJson:      json.RawMessage(\"null\"),\n\t\tBodyUUID:      \"c227acf4-1902-4c85-8027-623d47ef4c8a\",\n\t\tBodyUserID:    \"✉️\",\n\t\tBodySlice:     []int{r.Int(), r.Int(), r.Int(), r.Int(), r.Int(), r.Int()},\n\t}\n\tmResp, err := api.Test.MarshallerTestHandler(ctx, params)\n\tassert(err, nil, \"Expected no error from the marshaller test\")\n\n\t// We're marshalling as JSON, so we can just compare the JSON strings\n\trespAsJSON, err := json.Marshal(mResp)\n\tassert(err, nil, \"unable to marshal response to JSON\")\n\treqAsJSON, err := json.Marshal(params)\n\tassert(err, nil, \"unable to marshal response to JSON\")\n\tif diff := cmp.Diff(string(respAsJSON), string(reqAsJSON)); diff != \"\" {\n\t\tassertNumber++\n\t\tfmt.Printf(\"Assertion Failure %d: %s\\n\", assertNumber, diff)\n\t\tos.Exit(assertNumber)\n\t}\n\tassert(string(respAsJSON), string(reqAsJSON), \"Expected the same response from the marshaller test\")\n\n\t// Test auth handlers\n\t_, err = api.Test.TestAuthHandler(ctx)\n\tassertStructuredError(err, client.ErrUnauthenticated, \"missing auth param\")\n\n\t// Test with static auth data\n\t{\n\t\tapi, err := client.New(\n\t\t\tclient.BaseURL(fmt.Sprintf(\"http://%s\", os.Args[1])),\n\t\t\tclient.WithAuth(client.EchoAuthParams{\n\t\t\t\tAuthorization: \"Bearer tokendata\",\n\t\t\t}),\n\t\t)\n\t\tassert(err, nil, \"Wanted no error from client creation\")\n\n\t\tresp, err := api.Test.TestAuthHandler(ctx)\n\t\tassert(err, nil, \"Expected no error from second auth\")\n\t\tassert(resp.Message, \"user::true\", \"expected the user ID back\")\n\t}\n\n\t// Test with auth data generator function\n\t{\n\t\ttokenToReturn := \"tokendata\"\n\t\tapi, err := client.New(\n\t\t\tclient.BaseURL(fmt.Sprintf(\"http://%s\", os.Args[1])),\n\t\t\tclient.WithAuthFunc(func(ctx context.Context) (client.EchoAuthParams, error) {\n\t\t\t\treturn client.EchoAuthParams{\n\t\t\t\t\tAuthorization: \"Bearer \" + tokenToReturn,\n\t\t\t\t}, nil\n\t\t\t}),\n\t\t)\n\t\tassert(err, nil, \"Wanted no error from client creation\")\n\n\t\t// With a valid token\n\t\tresp, err := api.Test.TestAuthHandler(ctx)\n\t\tassert(err, nil, \"Expected no error from second auth\")\n\t\tassert(resp.Message, \"user::true\", \"expected the user ID back\")\n\n\t\t// With an invalid token\n\t\ttokenToReturn = \"invalid-token-value\"\n\t\t_, err = api.Test.TestAuthHandler(ctx)\n\t\tassertStructuredError(err, client.ErrUnauthenticated, \"invalid token\")\n\t}\n\n\t// Test with headers and query string auth data\n\t{\n\t\tapi, err := client.New(\n\t\t\tclient.BaseURL(fmt.Sprintf(\"http://%s\", os.Args[1])),\n\t\t\tclient.WithAuth(client.EchoAuthParams{\n\t\t\t\tNewAuth: true,\n\t\t\t\tHeader:  \"102\",\n\t\t\t\tQuery:   []int{42, 100, -50, 10},\n\t\t\t}),\n\t\t)\n\t\tassert(err, nil, \"Wanted no error from client creation\")\n\n\t\tresp, err := api.Test.TestAuthHandler(ctx)\n\t\tassert(err, nil, \"Expected no error from second auth\")\n\t\tassert(resp.Message, \"second_user::true\", \"expected the user ID back\")\n\t}\n\n\t// Test the raw endpoint\n\t{\n\t\tapi, err := client.New(\n\t\t\tclient.BaseURL(fmt.Sprintf(\"http://%s\", os.Args[1])),\n\t\t\tclient.WithAuth(client.EchoAuthParams{\n\t\t\t\tAuthorization: \"Bearer tokendata\",\n\t\t\t}),\n\t\t)\n\t\tassert(err, nil, \"Wanted no error from client creation\")\n\n\t\treq, err := http.NewRequest(\"PUT\", \"?foo=bar\", strings.NewReader(\"this is a test body\"))\n\t\tassert(err, nil, \"unable to create request for raw endpoint\")\n\t\treq.Header.Add(\"X-Test-Header\", \"test\")\n\n\t\trsp, err := api.Test.RawEndpoint(ctx, []string{\"hello\"}, req)\n\t\tassert(err, nil, \"expected no error from the raw socket\")\n\t\tdefer rsp.Body.Close()\n\n\t\tassert(rsp.StatusCode, http.StatusCreated, \"expected the status code to be 201\")\n\n\t\ttype responseType struct {\n\t\t\tBody        string\n\t\t\tHeader      string\n\t\t\tPathParam   string\n\t\t\tQueryString string\n\t\t}\n\t\tresponse := &responseType{}\n\n\t\tbytes, err := io.ReadAll(rsp.Body)\n\t\tassert(err, nil, \"expected no error from reading the response body\")\n\n\t\terr = json.Unmarshal(bytes, response)\n\t\tassert(err, nil, \"expected no error when unmarshalling the response body\")\n\n\t\tassert(response, &responseType{\"this is a test body\", \"test\", \"hello\", \"bar\"}, \"expected the response to match\")\n\t}\n\n\t{\n\t\tbodyStr := \"test body\"\n\t\treq, err := http.NewRequest(\"GET\", \"?foo=bar\", strings.NewReader(bodyStr))\n\t\tassert(err, nil, \"expected no error creating request\")\n\t\tresp, err := api.Di.Three(ctx, req)\n\t\tassert(err, nil, \"expected no error from DI raw endpoint\")\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tassert(string(body), bodyStr, \"expected response body to echo incoming request body\")\n\n\t}\n\n\t// Test path encoding\n\tresp, err := api.Test.PathMultiSegments(ctx, true, 342, \"foo/blah/should/get/escaped\", \"503f4487-1e15-4c37-9a80-7b70f86387bb\", []string{\"foo/bar\", \"blah\", \"seperate/segments = great success\"})\n\tassert(err, nil, \"expected no error from the path multi segments endpoint\")\n\tassert(resp.Boolean, true, \"expected the boolean to be true\")\n\tassert(resp.Int, 342, \"expected the int to be 342\")\n\tassert(resp.String, \"foo/blah/should/get/escaped\", \"invalid string field returned\")\n\tassert(resp.UUID, \"503f4487-1e15-4c37-9a80-7b70f86387bb\", \"invalid UUID returned\")\n\tassert(resp.Wildcard, \"foo/bar/blah/seperate/segments = great success\", \"invalid wildcard field returned\")\n\n\t// Test validation\n\terr = api.Validation.TestOne(ctx, client.ValidationRequest{Msg: \"pass\"})\n\tassert(err, nil, \"expected no error from validation\")\n\terr = api.Validation.TestOne(ctx, client.ValidationRequest{Msg: \"fail\"})\n\tassertStructuredError(err, client.ErrInvalidArgument, \"validation failed: bad message\")\n\t{\n\t\tapi, err := client.New(\n\t\t\tclient.BaseURL(fmt.Sprintf(\"http://%s\", os.Args[1])),\n\t\t\tclient.WithAuth(client.EchoAuthParams{\n\t\t\t\tHeader: \"fail-validation\",\n\t\t\t}),\n\t\t)\n\t\tassert(err, nil, \"expected no error from client init\")\n\t\terr = api.Test.Noop(ctx)\n\t\tassertStructuredError(err, client.ErrInvalidArgument, \"validation failed: auth validation fail\")\n\t}\n\n\t// Test middleware\n\t{\n\t\terr = api.Middleware.Error(ctx)\n\t\tassertStructuredError(err, client.ErrInternal, \"middleware error\")\n\t\tresp, err := api.Middleware.ResponseRewrite(ctx, client.MiddlewarePayload{Msg: \"foo\"})\n\t\tassert(err, nil, \"expected no error\")\n\t\tassert(resp.Msg, \"middleware(req=foo, resp=handler(foo))\", \"unexpected response\")\n\n\t\tresp, err = api.Middleware.ResponseGen(ctx, client.MiddlewarePayload{Msg: \"foo\"})\n\t\tassert(resp.Msg, \"middleware generated\", \"unexpected response\")\n\t}\n\n\t// Client test completed\n\tos.Exit(0)\n}\n\nfunc assert(got, want any, message string) {\n\tassertNumber++\n\n\tif !reflect.DeepEqual(got, want) {\n\t\tfmt.Printf(\"Assertion Failure %d: %s\\n\\n%+v != %+v\\n\", assertNumber, message, got, want)\n\t\tos.Exit(assertNumber)\n\t}\n}\n\nfunc assertNotNil(got any, message string) {\n\tassertNumber++\n\tif got == nil {\n\t\tfmt.Printf(\"Assertion Failure %d: got nil: %s\", assertNumber, message)\n\t\tos.Exit(assertNumber)\n\t}\n}\n\nfunc assertStructuredError(err error, code client.ErrCode, message string) {\n\tassertNotNil(err, \"want an error\")\n\n\tassertNumber++\n\tif apiError, ok := err.(*client.APIError); !ok {\n\t\tfmt.Printf(\"Assertion Failure %d: expected *client.APIError; got %+v\\n\", assertNumber, reflect.TypeOf(err))\n\t\tos.Exit(assertNumber)\n\t} else {\n\t\tassert(apiError.Code, code, \"unexpected error code\")\n\t\tassert(apiError.Message, message, \"expected error message\")\n\t}\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/package.json",
    "content": "{\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"node --trace-warnings --experimental-specifier-resolution=node --loader ts-node/esm ./ts/main.ts\",\n    \"test:js\": \"node --trace-warnings --experimental-specifier-resolution=node ./js/main.js\",\n    \"lint\": \"tsc --noEmit && eslint \\\"**/*.{ts,js}\\\"\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^17.0.35\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.26.0\",\n    \"@typescript-eslint/parser\": \"^5.26.0\",\n    \"eslint\": \"^8.16.0\",\n    \"ts-node\": \"^10.8.0\",\n    \"typescript\": \"^4.6.4\"\n  },\n  \"dependencies\": {\n    \"isomorphic-fetch\": \"^3.0.0\"\n  }\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/ts/client.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-slug.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the slug Encore application.\n */\nexport default class Client {\n    public readonly cache: cache.ServiceClient\n    public readonly di: di.ServiceClient\n    public readonly echo: echo.ServiceClient\n    public readonly emptycfg: emptycfg.ServiceClient\n    public readonly endtoend: endtoend.ServiceClient\n    public readonly middleware: middleware.ServiceClient\n    public readonly test: test.ServiceClient\n    public readonly validation: validation.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.cache = new cache.ServiceClient(base)\n        this.di = new di.ServiceClient(base)\n        this.echo = new echo.ServiceClient(base)\n        this.emptycfg = new emptycfg.ServiceClient(base)\n        this.endtoend = new endtoend.ServiceClient(base)\n        this.middleware = new middleware.ServiceClient(base)\n        this.test = new test.ServiceClient(base)\n        this.validation = new validation.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    /**\n     * Allows you to set the authentication data to be used for each\n     * request either by passing in a static object or by passing in\n     * a function which returns a new object for each request.\n     */\n    auth?: echo.AuthParams | AuthDataGenerator\n}\n\nexport namespace cache {\n    export interface IncrResponse {\n        Val: number\n    }\n\n    export interface ListResponse {\n        Vals: string[]\n    }\n\n    export interface StructVal {\n        Val: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.GetList = this.GetList.bind(this)\n            this.GetStruct = this.GetStruct.bind(this)\n            this.Incr = this.Incr.bind(this)\n            this.PostList = this.PostList.bind(this)\n            this.PostStruct = this.PostStruct.bind(this)\n        }\n\n        public async GetList(key: number): Promise<ListResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/cache/list/${encodeURIComponent(key)}`)\n            return await resp.json() as ListResponse\n        }\n\n        public async GetStruct(key: number): Promise<StructVal> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/cache/struct/${encodeURIComponent(key)}`)\n            return await resp.json() as StructVal\n        }\n\n        public async Incr(key: string): Promise<IncrResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/cache/incr/${encodeURIComponent(key)}`)\n            return await resp.json() as IncrResponse\n        }\n\n        public async PostList(key: number, val: string): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/cache/list/${encodeURIComponent(key)}/${encodeURIComponent(val)}`)\n        }\n\n        public async PostStruct(key: number, val: string): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/cache/struct/${encodeURIComponent(key)}/${encodeURIComponent(val)}`)\n        }\n    }\n}\n\nexport namespace di {\n    export interface TwoResponse {\n        Msg: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.One = this.One.bind(this)\n            this.Three = this.Three.bind(this)\n            this.Two = this.Two.bind(this)\n        }\n\n        public async One(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/di/one`)\n        }\n\n        public async Three(method: string, body?: RequestInit[\"body\"], options?: CallParameters): Promise<globalThis.Response> {\n            return this.baseClient.callAPI(method, `/di/raw`, body, options)\n        }\n\n        public async Two(): Promise<TwoResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/di/two`)\n            return await resp.json() as TwoResponse\n        }\n    }\n}\n\nexport namespace echo {\n    export interface AppMetadata {\n        AppID: string\n        APIBaseURL: string\n        EnvName: string\n        EnvType: string\n    }\n\n    export interface AuthParams {\n        Header: string\n        AuthInt: number\n        Authorization: string\n        Query: number[]\n        NewAuth: boolean\n    }\n\n    export interface BasicData {\n        String: string\n        Uint: number\n        Int: number\n        Int8: number\n        Int64: number\n        Float32: number\n        Float64: number\n        StringSlice: string[]\n        IntSlice: number[]\n        Time: string\n    }\n\n    export interface ConfigResponse {\n        ReadOnlyMode: boolean\n        PublicKey: string\n        SubKeyCount: number\n        AdminUsers: string[]\n    }\n\n    export interface Data<K, V> {\n        Key: K\n        Value: V\n    }\n\n    export interface EmptyData {\n        OmitEmpty: Data<string, string>\n        NullPtr: string\n        Zero: Data<string, string>\n    }\n\n    export interface EnvResponse {\n        Env: string[]\n    }\n\n    /**\n     * HTTPStatusResponse demonstrates encore:\"httpstatus\" tag functionality\n     */\n    export interface HTTPStatusResponse {\n        message: string\n    }\n\n    export interface HeadersData {\n        Int: number\n        String: string\n    }\n\n    export interface NonBasicData {\n        /**\n         * Header\n         */\n        HeaderString: string\n\n        HeaderNumber: number\n        /**\n         * Body\n         */\n        Struct: Data<Data<string, string>, number>\n\n        StructPtr: Data<number, number>\n        StructSlice: Data<string, string>[]\n        StructMap: { [key: string]: Data<string, number> }\n        StructMapPtr: { [key: string]: Data<string, string> }\n        AnonStruct: {\n            AnonBird: string\n        }\n        \"formatted_nest\": Data<string, number>\n        RawStruct: JSONValue\n        /**\n         * Query\n         */\n        QueryString: string\n\n        QueryNumber: number\n        OptQueryNumber?: number\n        OptQueryString?: string\n        /**\n         * Path Parameters\n         */\n        PathString: string\n\n        PathInt: number\n        PathWild: string\n        /**\n         * Auth Parameters\n         */\n        AuthHeader: string\n\n        AuthQuery: number[]\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.AppMeta = this.AppMeta.bind(this)\n            this.BasicEcho = this.BasicEcho.bind(this)\n            this.ConfigValues = this.ConfigValues.bind(this)\n            this.CustomHTTPStatus = this.CustomHTTPStatus.bind(this)\n            this.Echo = this.Echo.bind(this)\n            this.EmptyEcho = this.EmptyEcho.bind(this)\n            this.Env = this.Env.bind(this)\n            this.HeadersEcho = this.HeadersEcho.bind(this)\n            this.MuteEcho = this.MuteEcho.bind(this)\n            this.NilResponse = this.NilResponse.bind(this)\n            this.NonBasicEcho = this.NonBasicEcho.bind(this)\n            this.Noop = this.Noop.bind(this)\n            this.Pong = this.Pong.bind(this)\n            this.Publish = this.Publish.bind(this)\n        }\n\n        /**\n         * AppMeta returns app metadata.\n         */\n        public async AppMeta(): Promise<AppMetadata> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.AppMeta`)\n            return await resp.json() as AppMetadata\n        }\n\n        /**\n         * BasicEcho echoes back the request data.\n         */\n        public async BasicEcho(params: BasicData): Promise<BasicData> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.BasicEcho`, JSON.stringify(params))\n            return await resp.json() as BasicData\n        }\n\n        public async ConfigValues(): Promise<ConfigResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.ConfigValues`)\n            return await resp.json() as ConfigResponse\n        }\n\n        /**\n         * CustomHTTPStatus allows testing of custom HTTP status codes via encore:\"httpstatus\" tag\n         */\n        public async CustomHTTPStatus(): Promise<HTTPStatusResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.CustomHTTPStatus`)\n            return await resp.json() as HTTPStatusResponse\n        }\n\n        /**\n         * Echo echoes back the request data.\n         */\n        public async Echo(params: Data<string, number>): Promise<Data<string, number>> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.Echo`, JSON.stringify(params))\n            return await resp.json() as Data<string, number>\n        }\n\n        /**\n         * EmptyEcho echoes back the request data.\n         */\n        public async EmptyEcho(params: EmptyData): Promise<EmptyData> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.EmptyEcho`, JSON.stringify(params))\n            return await resp.json() as EmptyData\n        }\n\n        /**\n         * Env returns the environment.\n         */\n        public async Env(): Promise<EnvResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.Env`)\n            return await resp.json() as EnvResponse\n        }\n\n        /**\n         * HeadersEcho echoes back the request headers\n         */\n        public async HeadersEcho(params: HeadersData): Promise<HeadersData> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"x-int\":    String(params.Int),\n                \"x-string\": params.String,\n            })\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.HeadersEcho`, undefined, {headers})\n\n            //Populate the return object from the JSON body and received headers\n            const rtn = await resp.json() as HeadersData\n            rtn.Int = parseInt(mustBeSet(\"Header `x-int`\", resp.headers.get(\"x-int\")), 10)\n            rtn.String = mustBeSet(\"Header `x-string`\", resp.headers.get(\"x-string\"))\n            return rtn\n        }\n\n        /**\n         * MuteEcho absorbs a request\n         */\n        public async MuteEcho(params: Data<string, string>): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const query = makeRecord<string, string | string[]>({\n                key:   params.Key,\n                value: params.Value,\n            })\n\n            await this.baseClient.callTypedAPI(\"GET\", `/echo.MuteEcho`, undefined, {query})\n        }\n\n        /**\n         * NilResponse returns a nil response and nil error\n         */\n        public async NilResponse(): Promise<BasicData> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/echo.NilResponse`)\n            return await resp.json() as BasicData\n        }\n\n        /**\n         * NonBasicEcho echoes back the request data.\n         */\n        public async NonBasicEcho(pathString: string, pathInt: number, pathWild: string[], params: NonBasicData): Promise<NonBasicData> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"x-header-number\": String(params.HeaderNumber),\n                \"x-header-string\": params.HeaderString,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                no:     String(params.QueryNumber),\n                optnum: params.OptQueryNumber === undefined ? undefined : String(params.OptQueryNumber),\n                optstr: params.OptQueryString,\n                string: params.QueryString,\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                AnonStruct:       params.AnonStruct,\n                AuthHeader:       params.AuthHeader,\n                AuthQuery:        params.AuthQuery,\n                PathInt:          params.PathInt,\n                PathString:       params.PathString,\n                PathWild:         params.PathWild,\n                RawStruct:        params.RawStruct,\n                Struct:           params.Struct,\n                StructMap:        params.StructMap,\n                StructMapPtr:     params.StructMapPtr,\n                StructPtr:        params.StructPtr,\n                StructSlice:      params.StructSlice,\n                \"formatted_nest\": params[\"formatted_nest\"],\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/NonBasicEcho/${encodeURIComponent(pathString)}/${encodeURIComponent(pathInt)}/${pathWild.map(encodeURIComponent).join(\"/\")}`, JSON.stringify(body), {headers, query})\n\n            //Populate the return object from the JSON body and received headers\n            const rtn = await resp.json() as NonBasicData\n            rtn.HeaderString = mustBeSet(\"Header `x-header-string`\", resp.headers.get(\"x-header-string\"))\n            rtn.HeaderNumber = parseInt(mustBeSet(\"Header `x-header-number`\", resp.headers.get(\"x-header-number\")), 10)\n            return rtn\n        }\n\n        /**\n         * Noop does nothing\n         */\n        public async Noop(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"GET\", `/echo.Noop`)\n        }\n\n        /**\n         * Pong returns a bird tuple\n         */\n        public async Pong(): Promise<Data<string, string>> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/echo.Pong`)\n            return await resp.json() as Data<string, string>\n        }\n\n        /**\n         * Publish publishes a request on a topic\n         */\n        public async Publish(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/echo.Publish`)\n        }\n    }\n}\n\nexport namespace emptycfg {\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.AnAPI = this.AnAPI.bind(this)\n        }\n\n        public async AnAPI(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/emptycfg.AnAPI`)\n        }\n    }\n}\n\nexport namespace endtoend {\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.GeneratedWrappersEndToEndTest = this.GeneratedWrappersEndToEndTest.bind(this)\n        }\n\n        public async GeneratedWrappersEndToEndTest(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"GET\", `/generated-wrappers-end-to-end-test`)\n        }\n    }\n}\n\nexport namespace middleware {\n    export interface Payload {\n        Msg: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.Error = this.Error.bind(this)\n            this.ResponseGen = this.ResponseGen.bind(this)\n            this.ResponseRewrite = this.ResponseRewrite.bind(this)\n        }\n\n        public async Error(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/middleware.Error`)\n        }\n\n        public async ResponseGen(params: Payload): Promise<Payload> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/middleware.ResponseGen`, JSON.stringify(params))\n            return await resp.json() as Payload\n        }\n\n        public async ResponseRewrite(params: Payload): Promise<Payload> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/middleware.ResponseRewrite`, JSON.stringify(params))\n            return await resp.json() as Payload\n        }\n    }\n}\n\nexport namespace test {\n    export interface BodyEcho {\n        Message: string\n    }\n\n    export interface MarshallerTest<A> {\n        HeaderBoolean: boolean\n        HeaderInt: number\n        HeaderFloat: number\n        HeaderString: string\n        HeaderBytes: string\n        HeaderTime: string\n        HeaderJson: JSONValue\n        HeaderUUID: string\n        HeaderUserID: string\n        HeaderOption?: string | null\n        QueryBoolean: boolean\n        QueryInt: number\n        QueryFloat: number\n        QueryString: string\n        QueryBytes: string\n        QueryTime: string\n        QueryJson: JSONValue\n        QueryUUID: string\n        QueryUserID: string\n        QuerySlice: A[]\n        boolean: boolean\n        int: number\n        float: number\n        string: string\n        bytes: string\n        time: string\n        json: JSONValue\n        uuid: string\n        \"user-id\": string\n        slice: A[]\n        option?: A | null\n        \"option-slice\": (A | null)[]\n    }\n\n    export interface MultiPathSegment {\n        Boolean: boolean\n        Int: number\n        String: string\n        UUID: string\n        Wildcard: string\n    }\n\n    export interface RestParams {\n        HeaderValue: string\n        QueryValue: string\n        \"Some-Key\": string\n        Nested: {\n            Alice: string\n            bOb: number\n            charile: boolean\n        }\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.GetMessage = this.GetMessage.bind(this)\n            this.MarshallerTestHandler = this.MarshallerTestHandler.bind(this)\n            this.Noop = this.Noop.bind(this)\n            this.NoopWithError = this.NoopWithError.bind(this)\n            this.PathMultiSegments = this.PathMultiSegments.bind(this)\n            this.RawEndpoint = this.RawEndpoint.bind(this)\n            this.RestStyleAPI = this.RestStyleAPI.bind(this)\n            this.SimpleBodyEcho = this.SimpleBodyEcho.bind(this)\n            this.TestAuthHandler = this.TestAuthHandler.bind(this)\n            this.UpdateMessage = this.UpdateMessage.bind(this)\n        }\n\n        /**\n         * GetMessage allows us to test an API which takes no parameters,\n         * but returns data. It also tests two API's on the same path with different HTTP methods\n         */\n        public async GetMessage(clientID: string): Promise<BodyEcho> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/last_message/${encodeURIComponent(clientID)}`)\n            return await resp.json() as BodyEcho\n        }\n\n        /**\n         * MarshallerTestHandler allows us to test marshalling of all the inbuilt types in all\n         * the field types. It simply echos all the responses back to the client\n         */\n        public async MarshallerTestHandler(params: MarshallerTest<number>): Promise<MarshallerTest<number>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"x-boolean\": String(params.HeaderBoolean),\n                \"x-bytes\":   String(params.HeaderBytes),\n                \"x-float\":   String(params.HeaderFloat),\n                \"x-int\":     String(params.HeaderInt),\n                \"x-json\":    JSON.stringify(params.HeaderJson),\n                \"x-option\":  params.HeaderOption === undefined ? undefined : String(params.HeaderOption),\n                \"x-string\":  params.HeaderString,\n                \"x-time\":    String(params.HeaderTime),\n                \"x-user-id\": String(params.HeaderUserID),\n                \"x-uuid\":    String(params.HeaderUUID),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                boolean:   String(params.QueryBoolean),\n                bytes:     String(params.QueryBytes),\n                float:     String(params.QueryFloat),\n                int:       String(params.QueryInt),\n                json:      JSON.stringify(params.QueryJson),\n                slice:     params.QuerySlice.map((v) => String(v)),\n                string:    params.QueryString,\n                time:      String(params.QueryTime),\n                \"user-id\": String(params.QueryUserID),\n                uuid:      String(params.QueryUUID),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                boolean:        params.boolean,\n                bytes:          params.bytes,\n                float:          params.float,\n                int:            params.int,\n                json:           params.json,\n                option:         params.option,\n                \"option-slice\": params[\"option-slice\"],\n                slice:          params.slice,\n                string:         params.string,\n                time:           params.time,\n                \"user-id\":      params[\"user-id\"],\n                uuid:           params.uuid,\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/test.MarshallerTestHandler`, JSON.stringify(body), {headers, query})\n\n            //Populate the return object from the JSON body and received headers\n            const rtn = await resp.json() as MarshallerTest<number>\n            rtn.HeaderBoolean = mustBeSet(\"Header `x-boolean`\", resp.headers.get(\"x-boolean\")).toLowerCase() === \"true\"\n            rtn.HeaderInt = parseInt(mustBeSet(\"Header `x-int`\", resp.headers.get(\"x-int\")), 10)\n            rtn.HeaderFloat = Number(mustBeSet(\"Header `x-float`\", resp.headers.get(\"x-float\")))\n            rtn.HeaderString = mustBeSet(\"Header `x-string`\", resp.headers.get(\"x-string\"))\n            rtn.HeaderBytes = mustBeSet(\"Header `x-bytes`\", resp.headers.get(\"x-bytes\"))\n            rtn.HeaderTime = mustBeSet(\"Header `x-time`\", resp.headers.get(\"x-time\"))\n            rtn.HeaderJson = JSON.parse(mustBeSet(\"Header `x-json`\", resp.headers.get(\"x-json\")))\n            rtn.HeaderUUID = mustBeSet(\"Header `x-uuid`\", resp.headers.get(\"x-uuid\"))\n            rtn.HeaderUserID = mustBeSet(\"Header `x-user-id`\", resp.headers.get(\"x-user-id\"))\n            rtn.HeaderOption = mustBeSet(\"Header `x-option`\", resp.headers.get(\"x-option\"))\n            return rtn\n        }\n\n        /**\n         * Noop allows us to test if a simple HTTP request can be made\n         */\n        public async Noop(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/test.Noop`)\n        }\n\n        /**\n         * NoopWithError allows us to test if the structured errors are returned\n         */\n        public async NoopWithError(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/test.NoopWithError`)\n        }\n\n        /**\n         * PathMultiSegments allows us to wildcard segments and segment URI encoding\n         */\n        public async PathMultiSegments(bool: boolean, int: number, _string: string, uuid: string, wildcard: string[]): Promise<MultiPathSegment> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/multi/${encodeURIComponent(bool)}/${encodeURIComponent(int)}/${encodeURIComponent(_string)}/${encodeURIComponent(uuid)}/${wildcard.map(encodeURIComponent).join(\"/\")}`)\n            return await resp.json() as MultiPathSegment\n        }\n\n        /**\n         * RawEndpoint allows us to test the clients' ability to send raw requests\n         * under auth\n         */\n        public async RawEndpoint(method: \"PUT\" | \"POST\" | \"DELETE\" | \"GET\", id: string[], body?: RequestInit[\"body\"], options?: CallParameters): Promise<globalThis.Response> {\n            return this.baseClient.callAPI(method, `/raw/blah/${id.map(encodeURIComponent).join(\"/\")}`, body, options)\n        }\n\n        /**\n         * RestStyleAPI tests all the ways we can get data into and out of the application\n         * using Encore request handlers\n         */\n        public async RestStyleAPI(objType: number, name: string, params: RestParams): Promise<RestParams> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-key\": params.HeaderValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"Some-Key\": params.QueryValue,\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                Nested:     params.Nested,\n                \"Some-Key\": params[\"Some-Key\"],\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"PUT\", `/rest/object/${encodeURIComponent(objType)}/${encodeURIComponent(name)}`, JSON.stringify(body), {headers, query})\n\n            //Populate the return object from the JSON body and received headers\n            const rtn = await resp.json() as RestParams\n            rtn.HeaderValue = mustBeSet(\"Header `some-key`\", resp.headers.get(\"some-key\"))\n            return rtn\n        }\n\n        /**\n         * SimpleBodyEcho allows us to exercise the body marshalling from JSON\n         * and being returned purely as a body\n         */\n        public async SimpleBodyEcho(params: BodyEcho): Promise<BodyEcho> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/test.SimpleBodyEcho`, JSON.stringify(params))\n            return await resp.json() as BodyEcho\n        }\n\n        /**\n         * TestAuthHandler allows us to test the clients ability to add tokens to requests\n         */\n        public async TestAuthHandler(): Promise<BodyEcho> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/test.TestAuthHandler`)\n            return await resp.json() as BodyEcho\n        }\n\n        /**\n         * UpdateMessage allows us to test an API which takes parameters,\n         * but doesn't return anything\n         */\n        public async UpdateMessage(clientID: string, params: BodyEcho): Promise<void> {\n            await this.baseClient.callTypedAPI(\"PUT\", `/last_message/${encodeURIComponent(clientID)}`, JSON.stringify(params))\n        }\n    }\n}\n\nexport namespace validation {\n    export interface Request {\n        Msg: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.TestOne = this.TestOne.bind(this)\n        }\n\n        public async TestOne(params: Request): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/validation.TestOne`, JSON.stringify(params))\n        }\n    }\n}\n\n// JSONValue represents an arbitrary JSON value.\nexport type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue}\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\n\n// mustBeSet will throw an APIError with the Data Loss code if value is null or undefined\nfunction mustBeSet<A>(field: string, value: A | null | undefined): A {\n    if (value === null || value === undefined) {\n        throw new APIError(\n            500,\n            {\n                code: ErrCode.DataLoss,\n                message: `${field} was unexpectedly ${value}`, // ${value} will create the string \"null\" or \"undefined\"\n            },\n        )\n    }\n    return value\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n// AuthDataGenerator is a function that returns a new instance of the authentication data required by this API\nexport type AuthDataGenerator = () =>\n  | echo.AuthParams\n  | Promise<echo.AuthParams | undefined>\n  | undefined;\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n    readonly authGenerator?: AuthDataGenerator\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"slug-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        let authData: echo.AuthParams | undefined;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data: CallParameters = {};\n\n            data.query = makeRecord<string, string | string[]>({\n                \"new-auth\": String(authData.NewAuth),\n                query:      authData.Query.map((v) => String(v)),\n            });\n            data.headers = makeRecord<string, string>({\n                authorization: authData.Authorization,\n                \"x-auth-int\":  String(authData.AuthInt),\n                \"x-header\":    authData.Header,\n            });\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/ts/main.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport \"isomorphic-fetch\";\nimport { deepEqual } from \"assert\";\n\nimport Client, { BaseURL, echo, ErrCode, isAPIError, test } from \"./client\";\nimport MarshallerTest = test.MarshallerTest;\n\nif (process.argv.length < 3) {\n  console.error(\"Usage: npm run test -- <host:port>\");\n  console.log(`Got ${process.argv.length} arguments`);\n  process.exit(1);\n}\n\n// Create the client\nconst api = new Client((\"http://\" + process.argv[2]) as BaseURL);\n\n// Test a simple no-op\nawait api.test.Noop();\n\n// Test we get back the right structured error\nawait assertStructuredError(\n  api.test.NoopWithError(),\n  ErrCode.Unimplemented,\n  \"totally not implemented yet\"\n);\n\n// Test a simple echo\nconst echoRsp = await api.test.SimpleBodyEcho({ Message: \"hello world\" });\ndeepEqual(echoRsp.Message, \"hello world\", \"Wanted body to be 'hello world'\");\n\n// Check our UpdateMessage and GetMessage API's\nlet getRsp = await api.test.GetMessage(\"typescript\");\ndeepEqual(getRsp.Message, \"\", \"Expected no message on first request\");\n\nawait api.test.UpdateMessage(\"typescript\", { Message: \"updating now\" });\n\ngetRsp = await api.test.GetMessage(\"typescript\");\ndeepEqual(getRsp.Message, \"updating now\", \"Expected data from Update request\");\n\n// Test the rest API which uses all input types (query string, json body and header fields)\n// as well as nested structs and path segments in the URL\nconst restRsp = await api.test.RestStyleAPI(5, \"hello\", {\n  HeaderValue: \"this is the header field\",\n  QueryValue: \"this is a query string field\",\n  \"Some-Key\": \"this is the body field\",\n  Nested: {\n    Alice: \"the nested key\",\n    bOb: 8,\n    charile: true\n  }\n});\ndeepEqual(\n  restRsp.HeaderValue,\n  \"this is the header field\",\n  \"expected header value\"\n);\ndeepEqual(\n  restRsp.QueryValue,\n  \"this is a query string field\",\n  \"expected query value\"\n);\ndeepEqual(restRsp[\"Some-Key\"], \"this is the body field\", \"expected body value\");\ndeepEqual(\n  restRsp.Nested.Alice,\n  \"hello + the nested key\",\n  \"expected nested key\"\n);\ndeepEqual(restRsp.Nested.bOb, 5 + 8, \"expected nested value\");\ndeepEqual(restRsp.Nested.charile, true, \"expected nested ok\");\n\n// Full marshalling test with randomised payloads\nfunction rInt(): number {\n  return Math.floor(Math.random() * 10000000);\n}\n\nconst params: MarshallerTest<number> = {\n  HeaderBoolean: Math.random() > 0.5,\n  HeaderInt: rInt(),\n  HeaderFloat: Math.random(),\n  HeaderString: \"header string\",\n  HeaderBytes: \"aGVsbG8K\",\n  HeaderTime: new Date(Math.floor(Date.now() / 1000) * 1000)\n    .toISOString()\n    .replace(\".000Z\", \"Z\"),\n  HeaderJson: { hello: \"world\" },\n  HeaderUUID: \"2553e3a4-5d9f-4716-82a2-b9bdc20a3263\",\n  HeaderUserID: \"432\",\n  HeaderOption: \"abc\",\n  QueryBoolean: Math.random() > 0.5,\n  QueryInt: rInt(),\n  QueryFloat: Math.random(),\n  QueryString: \"query string\",\n  QueryBytes: \"d29ybGQK\",\n  QueryTime: new Date(Math.floor(Date.now() / 1000) * 1000)\n    .toISOString()\n    .replace(\".000Z\", \"Z\"),\n  QueryJson: { value: true },\n  QueryUUID: \"84b7463d-6000-4678-9d94-1d526bb5217c\",\n  QueryUserID: \"9udfa\",\n  QuerySlice: [rInt(), rInt(), rInt(), rInt()],\n  boolean: Math.random() > 0.5,\n  int: Math.floor(Math.random() * 10000000),\n  float: Math.random(),\n  string: \"body string\",\n  bytes: \"aXMgaXQgbWUgeW91IGFyZSBsb29raW5nIGZvcj8K\",\n  time: new Date(Math.floor(Date.now() / 1000) * 1000)\n    .toISOString()\n    .replace(\".000Z\", \"Z\"),\n  json: { json_value: 4321 },\n  uuid: \"c227acf4-1902-4c85-8027-623d47ef4c8a\",\n  \"user-id\": \"✉️\",\n  slice: [rInt(), rInt(), rInt(), rInt(), rInt(), rInt()],\n  option: 123,\n  \"option-slice\": [123, null, 456]\n};\nconst mResp = await api.test.MarshallerTestHandler(params);\ndeepEqual(mResp, params, \"Expected the same response from the marshaller test\");\n\n// Test auth handlers\nawait assertStructuredError(\n  api.test.TestAuthHandler(),\n  ErrCode.Unauthenticated,\n  \"missing auth param\"\n);\n\n// Test with static auth data\n{\n  const api = new Client((\"http://\" + process.argv[2]) as BaseURL, {\n    auth: {\n      AuthInt: 34,\n      Authorization: \"Bearer tokendata\",\n      NewAuth: false,\n      Header: \"\",\n      Query: []\n    }\n  });\n\n  const resp = await api.test.TestAuthHandler();\n  deepEqual(resp.Message, \"user::true\", \"expected the user ID back\");\n}\n\n// Test with auth data generator function\n{\n  let tokenToReturn = \"tokendata\";\n  const api = new Client((\"http://\" + process.argv[2]) as BaseURL, {\n    auth: (): echo.AuthParams => {\n      return {\n        Authorization: \"Bearer \" + tokenToReturn,\n        AuthInt: 34,\n        NewAuth: false,\n        Header: \"\",\n        Query: []\n      };\n    }\n  });\n\n  // With a valid token\n  const resp = await api.test.TestAuthHandler();\n  deepEqual(resp.Message, \"user::true\", \"expected the user ID back\");\n\n  // With an invalid token\n  tokenToReturn = \"invalid-token-value\";\n  await assertStructuredError(\n    api.test.TestAuthHandler(),\n    ErrCode.Unauthenticated,\n    \"invalid token\"\n  );\n}\n\n// Test with headers and query string auth data\n{\n  const api = new Client((\"http://\" + process.argv[2]) as BaseURL, {\n    auth: {\n      Authorization: \"\",\n      AuthInt: 34,\n      NewAuth: true,\n      Header: \"102\",\n      Query: [42, 100, -50, 10]\n    }\n  });\n\n  const resp = await api.test.TestAuthHandler();\n  deepEqual(resp.Message, \"second_user::true\", \"expected the user ID back\");\n}\n\n// Test the raw endpoint\n{\n  const api = new Client((\"http://\" + process.argv[2]) as BaseURL, {\n    auth: {\n      AuthInt: 34,\n      Authorization: \"Bearer tokendata\",\n      NewAuth: false,\n      Header: \"\",\n      Query: []\n    }\n  });\n\n  const resp = await api.test.RawEndpoint(\n    \"PUT\",\n    [\"hello\"],\n    \"this is a test body\",\n    {\n      headers: { \"X-Test-Header\": \"test\" },\n      query: { foo: \"bar\" }\n    }\n  );\n\n  deepEqual(resp.status, 201, \"expected the status code to be 201\");\n\n  const response = await resp.json();\n\n  deepEqual(\n    response,\n    {\n      Body: \"this is a test body\",\n      Header: \"test\",\n      PathParam: \"hello\",\n      QueryString: \"bar\"\n    },\n    \"expected the response to match\"\n  );\n}\n\n// Test path encoding\nconst resp = await api.test.PathMultiSegments(\n  true,\n  342,\n  \"foo/blah/should/get/escaped\",\n  \"503f4487-1e15-4c37-9a80-7b70f86387bb\",\n  [\"foo/bar\", \"blah\", \"seperate/segments = great success\"]\n);\ndeepEqual(resp.Boolean, true, \"expected the boolean to be true\");\ndeepEqual(resp.Int, 342, \"expected the int to be 342\");\ndeepEqual(\n  resp.String,\n  \"foo/blah/should/get/escaped\",\n  \"invalid string field returned\"\n);\ndeepEqual(\n  resp.UUID,\n  \"503f4487-1e15-4c37-9a80-7b70f86387bb\",\n  \"invalid UUID returned\"\n);\ndeepEqual(\n  resp.Wildcard,\n  \"foo/bar/blah/seperate/segments = great success\",\n  \"invalid wildcard field returned\"\n);\n\n// Test validation\n{\n  await api.validation.TestOne({\n    Msg: \"pass\"\n  });\n  await assertStructuredError(\n    api.validation.TestOne({ Msg: \"fail\" }),\n    ErrCode.InvalidArgument,\n    \"validation failed: bad message\"\n  );\n  const client = new Client((\"http://\" + process.argv[2]) as BaseURL, {\n    auth: {\n      AuthInt: 0,\n      Authorization: \"\",\n      NewAuth: false,\n      Header: \"fail-validation\",\n      Query: []\n    }\n  });\n  await assertStructuredError(\n    client.test.Noop(),\n    ErrCode.InvalidArgument,\n    \"validation failed: auth validation fail\"\n  );\n}\n\n// Test middleware\n{\n  await assertStructuredError(\n    api.middleware.Error(),\n    ErrCode.Internal,\n    \"middleware error\"\n  );\n  const resp1 = await api.middleware.ResponseRewrite({ Msg: \"foo\" });\n  deepEqual(resp1.Msg, \"middleware(req=foo, resp=handler(foo))\");\n\n  const resp2 = await api.middleware.ResponseGen({ Msg: \"foo\" });\n  deepEqual(resp2.Msg, \"middleware generated\");\n}\n\n// Client test completed\nprocess.exit(0);\n\nasync function assertStructuredError(\n  promise: Promise<any>,\n  code: ErrCode,\n  message: string\n) {\n  let errorOccurred = false;\n  try {\n    await promise;\n  } catch (err: any) {\n    errorOccurred = true;\n    if (isAPIError(err)) {\n      if (err.code !== code) {\n        throw new Error(\n          `Expected error code ${code}, got ${err.code} with message \"${err.message}\"`\n        );\n      }\n      if (err.message !== message) {\n        throw new Error(\n          `Expected error message \"${message}\", got \"${err.message}\"`\n        );\n      }\n    } else {\n      throw new Error(`Expected APIError, got ${err}`);\n    }\n  }\n\n  if (!errorOccurred) {\n    throw new Error(\"No error was thrown during call to NoopWithError\");\n  }\n}\n"
  },
  {
    "path": "e2e-tests/testdata/echo_client/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n    /* Projects */\n    // \"incremental\": true,                              /* Enable incremental compilation */\n    // \"composite\": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */\n    // \"tsBuildInfoFile\": \"./\",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */\n    // \"disableSourceOfProjectReferenceRedirect\": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */\n    // \"disableSolutionSearching\": true,                 /* Opt a project out of multi-project reference checking when editing. */\n    // \"disableReferencedProjectLoad\": true,             /* Reduce the number of projects loaded automatically by TypeScript. */\n\n    /* Language and Environment */\n    \"target\": \"ESNext\",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */\n    \"lib\": [ \"ES6\", \"DOM\", \"ES2021.String\" ],            /* Specify a set of bundled library declaration files that describe the target runtime environment. */\n    // \"jsx\": \"preserve\",                                /* Specify what JSX code is generated. */\n    // \"experimentalDecorators\": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */\n    // \"emitDecoratorMetadata\": true,                    /* Emit design-type metadata for decorated declarations in source files. */\n    // \"jsxFactory\": \"\",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */\n    // \"jsxFragmentFactory\": \"\",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */\n    // \"jsxImportSource\": \"\",                            /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */\n    // \"reactNamespace\": \"\",                             /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */\n    // \"noLib\": true,                                    /* Disable including any library files, including the default lib.d.ts. */\n    // \"useDefineForClassFields\": true,                  /* Emit ECMAScript-standard-compliant class fields. */\n\n    /* Modules */\n    \"module\": \"ESNext\",                                  /* Specify what module code is generated. */\n    // \"rootDir\": \"./\",                                  /* Specify the root folder within your source files. */\n    \"moduleResolution\": \"Node\",                          /* Specify how TypeScript looks up a file from a given module specifier. */\n    // \"baseUrl\": \"./\",                                  /* Specify the base directory to resolve non-relative module names. */\n    // \"paths\": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */\n    // \"rootDirs\": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */\n    // \"typeRoots\": [],                                  /* Specify multiple folders that act like `./node_modules/@types`. */\n    // \"types\": [],                                      /* Specify type package names to be included without being referenced in a source file. */\n    // \"allowUmdGlobalAccess\": true,                     /* Allow accessing UMD globals from modules. */\n    // \"resolveJsonModule\": true,                        /* Enable importing .json files */\n    // \"noResolve\": true,                                /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */\n\n    /* JavaScript Support */\n    // \"allowJs\": true,                                  /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */\n    // \"checkJs\": true,                                  /* Enable error reporting in type-checked JavaScript files. */\n    // \"maxNodeModuleJsDepth\": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */\n\n    /* Emit */\n    // \"declaration\": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */\n    // \"declarationMap\": true,                           /* Create sourcemaps for d.ts files. */\n    // \"emitDeclarationOnly\": true,                      /* Only output d.ts files and not JavaScript files. */\n    // \"sourceMap\": true,                                /* Create source map files for emitted JavaScript files. */\n    // \"outFile\": \"./\",                                  /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */\n    // \"outDir\": \"./\",                                   /* Specify an output folder for all emitted files. */\n    // \"removeComments\": true,                           /* Disable emitting comments. */\n    // \"noEmit\": true,                                   /* Disable emitting files from a compilation. */\n    // \"importHelpers\": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */\n    // \"importsNotUsedAsValues\": \"remove\",               /* Specify emit/checking behavior for imports that are only used for types */\n    // \"downlevelIteration\": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */\n    // \"sourceRoot\": \"\",                                 /* Specify the root path for debuggers to find the reference source code. */\n    // \"mapRoot\": \"\",                                    /* Specify the location where debugger should locate map files instead of generated locations. */\n    // \"inlineSourceMap\": true,                          /* Include sourcemap files inside the emitted JavaScript. */\n    // \"inlineSources\": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */\n    // \"emitBOM\": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */\n    // \"newLine\": \"crlf\",                                /* Set the newline character for emitting files. */\n    // \"stripInternal\": true,                            /* Disable emitting declarations that have `@internal` in their JSDoc comments. */\n    // \"noEmitHelpers\": true,                            /* Disable generating custom helper functions like `__extends` in compiled output. */\n    \"noEmitOnError\": true,                               /* Disable emitting files if any type checking errors are reported. */\n    // \"preserveConstEnums\": true,                       /* Disable erasing `const enum` declarations in generated code. */\n    // \"declarationDir\": \"./\",                           /* Specify the output directory for generated declaration files. */\n    // \"preserveValueImports\": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */\n\n    /* Interop Constraints */\n    // \"isolatedModules\": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */\n    // \"allowSyntheticDefaultImports\": true,             /* Allow 'import x from y' when a module doesn't have a default export. */\n    \"esModuleInterop\": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */\n    // \"preserveSymlinks\": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */\n    \"forceConsistentCasingInFileNames\": true,            /* Ensure that casing is correct in imports. */\n\n    /* Type Checking */\n    \"strict\": true,                                      /* Enable all strict type-checking options. */\n    \"noImplicitAny\": true,                               /* Enable error reporting for expressions and declarations with an implied `any` type.. */\n    \"strictNullChecks\": true,                            /* When type checking, take into account `null` and `undefined`. */\n    \"strictFunctionTypes\": true,                         /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */\n    \"strictBindCallApply\": true,                         /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */\n    \"strictPropertyInitialization\": true,                /* Check for class properties that are declared but not set in the constructor. */\n    \"noImplicitThis\": true,                              /* Enable error reporting when `this` is given the type `any`. */\n    \"useUnknownInCatchVariables\": true,                  /* Type catch clause variables as 'unknown' instead of 'any'. */\n    \"alwaysStrict\": true,                                /* Ensure 'use strict' is always emitted. */\n    \"noUnusedLocals\": true,                              /* Enable error reporting when a local variables aren't read. */\n    \"noUnusedParameters\": true,                          /* Raise an error when a function parameter isn't read */\n    \"exactOptionalPropertyTypes\": true,                  /* Interpret optional property types as written, rather than adding 'undefined'. */\n    \"noImplicitReturns\": true,                           /* Enable error reporting for codepaths that do not explicitly return in a function. */\n    \"noFallthroughCasesInSwitch\": true,                  /* Enable error reporting for fallthrough cases in switch statements. */\n    \"noUncheckedIndexedAccess\": true,                    /* Include 'undefined' in index signature results */\n    \"noImplicitOverride\": true,                          /* Ensure overriding members in derived classes are marked with an override modifier. */\n    \"noPropertyAccessFromIndexSignature\": true,          /* Enforces using indexed accessors for keys declared using an indexed type */\n    \"allowUnusedLabels\": false,                          /* Disable error reporting for unused labels. */\n    \"allowUnreachableCode\": false,                       /* Disable error reporting for unreachable code. */\n\n    /* Completeness */\n    // \"skipDefaultLibCheck\": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */\n    \"skipLibCheck\": true                                 /* Skip type checking all .d.ts files. */\n  },\n  \"ts-node\": {\n    \"esm\": true\n  },\n  \"lib\": [\n    \"esnext\"\n  ]\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/encore_currentrequest.txt",
    "content": "run\npublish topic '{\"Data\": \"test\"}'\nchecklog '{\"topic\": \"topic\", \"subscription\": \"sub\", \"type\": \"pubsub-message\", \"msg\": {\"Service\": \"svc\", \"Topic\": \"topic\", \"Subscription\": \"sub\", \"ID\": \"1\", \"DeliveryAttempt\": 1}, \"message\": \"pubsub event\"}'\n\n# Authenticated requests should return the execution id\ncall GET X-Encore-Cron-Execution=foo /svc.CurrentRequest ''\ncheckresp '{\"Path\": \"/svc.CurrentRequest\", \"IdempotencyKey\": \"foo\"}'\n\n# Unauthenticated requests should return the empty string\ncall GET X-Encore-Cron-Execution=foo /svc.CurrentRequest '' no-platform-auth\ncheckresp '{\"Path\": \"/svc.CurrentRequest\", \"IdempotencyKey\": \"\"}'\n\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev\"\n    \"encore.dev/rlog\"\n    \"encore.dev/pubsub\"\n)\n\ntype MyData struct {\n    Name string\n}\n\ntype Event struct {\n    Data string\n}\n\ntype RequestData struct {\n    Path string\n    IdempotencyKey string\n}\n\n//encore:api public\nfunc CurrentRequest(ctx context.Context) (*RequestData, error) {\n    req := encore.CurrentRequest()\n    return &RequestData{\n        Path: req.Path,\n        IdempotencyKey: req.CronIdempotencyKey,\n    }, nil\n}\n\nvar topic = pubsub.NewTopic[*Event](\"topic\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar _ = pubsub.NewSubscription(topic, \"sub\", pubsub.SubscriptionConfig[*Event]{\n    Handler: func(ctx context.Context, event *Event) error {\n        req := encore.CurrentRequest()\n        rlog.Info(\"pubsub event\",\n            \"type\", req.Type,\n            \"msg\", req.Message,\n        )\n        return nil\n    },\n})\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/et_mocking.txt",
    "content": "test\n\n-- products/price.go --\npackage products\n\nimport (\n    \"context\"\n)\n\ntype PriceParams struct {\n    Quantity int\n}\n\ntype PriceResult struct {\n    Total float64\n}\n\n//encore:api public method=GET path=/products/:productID/price\nfunc GetPrice(ctx context.Context, productID int, p *PriceParams) (*PriceResult, error) {\n    return &PriceResult{ Total: 99.99 * float64(p.Quantity) }, nil\n}\n\n-- shoppingcart/cart.go --\npackage shoppingcart\n\nimport (\n    \"context\"\n\n    \"test/products\"\n)\n\ntype CartItem struct {\n    ProductID int\n    Quantity int\n}\n\n//encore:service\ntype Service struct {\n    Items []CartItem\n}\n\nfunc initService() (*Service, error) {\n    return &Service{\n        Items: []CartItem{\n            { ProductID: 1, Quantity: 2 },\n            { ProductID: 2, Quantity: 1 },\n        },\n    }, nil\n}\n\ntype TotalResult struct {\n    Total float64\n}\n\n//encore:api public method=GET path=/cart/total\nfunc (s *Service) Total(ctx context.Context) (*TotalResult, error) {\n    var total float64\n\n    for _, item := range s.Items {\n        price, err := products.GetPrice(ctx, item.ProductID, &products.PriceParams{ Quantity: item.Quantity })\n        if err != nil {\n            return nil, err\n        }\n\n        total += price.Total\n    }\n\n    return &TotalResult{ Total: total }, nil\n}\n\n//encore:api private\nfunc (s *Service) Empty(ctx context.Context) error {\n    s.Items = []CartItem{}\n    return nil\n}\n\n-- shoppingcart/cart_test.go --\npackage shoppingcart\n\nimport (\n    \"context\"\n    \"math\"\n    \"testing\"\n\n    \"encore.dev/et\"\n\n    \"test/products\"\n)\n\nfunc callAndExpect(t *testing.T, total float64) {\n    resp, err := Total(context.Background())\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    if math.Abs(resp.Total - total) > 0.001 {\n        t.Fatalf(\"expected total to be %f, got %f\", total, resp.Total)\n    }\n}\n\nfunc TestTotal_NoMocking(t *testing.T) {\n    t.Parallel()\n\n    callAndExpect(t, 299.97)\n}\n\nfunc TestTotal_WithMockingOfProductsEndpoint(t *testing.T) {\n    t.Parallel()\n\n    et.MockEndpoint(products.GetPrice, func(ctx context.Context, productID int, p *products.PriceParams) (*products.PriceResult, error) {\n       return &products.PriceResult{ Total: 20 * float64(p.Quantity) }, nil\n    })\n\n    callAndExpect(t, 60.0)\n}\n\nfunc TestTotal_WithMockingOfServiceMethod(t *testing.T) {\n    t.Parallel()\n\n    et.MockEndpoint(Total, func(ctx context.Context) (*TotalResult, error) {\n        return &TotalResult{ Total: 100.0 }, nil\n    })\n\n    callAndExpect(t, 100.0)\n}\n\nfunc TestTotal_WithMockingOfServiceObjectWithDifferentInstance(t *testing.T) {\n    t.Parallel()\n\n    et.MockService(\"shoppingcart\", &Service{\n        Items: []CartItem{\n            { ProductID: 1, Quantity: 5 },\n        },\n    })\n\n    callAndExpect(t, 499.95)\n}\n\nfunc TestTotal_WithMockingOfServiceWithMockObject(t *testing.T) {\n    t.Parallel()\n\n    et.MockService[products.Interface](\"products\", &mockProducts{})\n\n    callAndExpect(t, 303.0)\n}\n\ntype mockProducts struct{}\n\nfunc (m *mockProducts) GetPrice(ctx context.Context, productID int, p *products.PriceParams) (*products.PriceResult, error) {\n    return &products.PriceResult{ Total: float64(productID) + float64(p.Quantity * 100) }, nil\n}\n\nfunc TestTotal_UsingServiceIsolation(t *testing.T) {\n    t.Parallel()\n\n    callAndExpect(t, 299.97)\n\n    // These don't run with parallel so we can test the isolation\n    t.Run(\"emptied in isolation\", func(t *testing.T) {\n        et.EnableServiceInstanceIsolation()\n        Empty(context.Background())\n        callAndExpect(t, 0.0)\n    })\n\n    t.Run(\"non isolated still has items\", func(t *testing.T) {\n        callAndExpect(t, 299.97)\n    })\n}\n\nfunc TestTotal_RemovingMockServices(t *testing.T) {\n    t.Parallel()\n\n    et.MockService[products.Interface](\"products\", &mockProducts{})\n\n    t.Run(\"remove mock\", func(t *testing.T) {\n        et.MockService[products.Interface](\"products\", nil)\n        callAndExpect(t, 299.97)\n    })\n\n    callAndExpect(t, 303.0)\n}\n\nfunc TestTotal_RemovingMockEndpoints(t *testing.T) {\n    t.Parallel()\n\n    et.MockEndpoint(products.GetPrice, func(ctx context.Context, productID int, p *products.PriceParams) (*products.PriceResult, error) {\n       return &products.PriceResult{ Total: 20 * float64(p.Quantity) }, nil\n    })\n\n    t.Run(\"remove mock\", func(t *testing.T) {\n        et.MockEndpoint(products.GetPrice, nil)\n        callAndExpect(t, 299.97)\n    })\n\n    callAndExpect(t, 60.0)\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/et_override_user.txt",
    "content": "test\n\n-- svc/svc_test.go --\npackage svc\n\nimport (\n    \"context\"\n    \"testing\"\n    \"encore.dev/beta/auth\"\n    \"encore.dev/et\"\n)\n\nfunc TestOverrideAuthInfo(t *testing.T) {\n    curr, _ := auth.UserID()\n    if curr != \"\" {\n        t.Fatalf(\"got uid %q, want %q\", curr, \"\")\n    }\n\n    et.OverrideAuthInfo(\"foo\", nil)\n\n    curr, _ = auth.UserID()\n    if curr != \"foo\" {\n        t.Fatalf(\"got uid %q, want %q\", curr, \"foo\")\n    }\n}\n\nfunc TestOverrideAuthInfo_ResetBetweenTests(t *testing.T) {\n    curr, _ := auth.UserID()\n    if curr != \"\" {\n        t.Fatalf(\"got uid %q, want %q\", curr, \"\")\n    }\n}\n\nfunc TestOverrideAuthInfo_PropagatesToAPICalls(t *testing.T) {\n    resp, err := GetUser(context.Background())\n    if err != nil {\n        t.Fatal(err)\n    } else if resp.UserID != \"\" {\n        t.Fatalf(\"got uid %q, want %q\", resp.UserID, \"\")\n    }\n\n    et.OverrideAuthInfo(\"foo\", nil)\n\n    resp, err = GetUser(context.Background())\n    if err != nil {\n        t.Fatal(err)\n    } else if resp.UserID != \"foo\" {\n        t.Fatalf(\"got uid %q, want %q\", resp.UserID, \"foo\")\n    }\n}\n\nfunc TestOverrideAuthInfo_APICallOptsOverride(t *testing.T) {\n    et.OverrideAuthInfo(\"foo\", nil)\n\n    ctx := auth.WithContext(context.Background(), \"bar\", nil)\n    resp, err := GetUser(ctx)\n    if err != nil {\n        t.Fatal(err)\n    } else if resp.UserID != \"bar\" {\n        t.Fatalf(\"got uid %q, want %q\", resp.UserID, \"bar\")\n    }\n}\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype Response struct {\n    UserID auth.UID\n}\n\n//encore:api public\nfunc GetUser(ctx context.Context) (*Response, error) {\n    uid, _ := auth.UserID()\n    return &Response{UserID: uid}, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/et_override_user_authdata.txt",
    "content": "test\n\n-- svc/svc_test.go --\npackage svc\n\nimport (\n    \"context\"\n    \"testing\"\n    \"encore.dev/beta/auth\"\n    \"encore.dev/et\"\n)\n\nfunc TestOverrideAuthInfo(t *testing.T) {\n    curr, _ := auth.Data().(*AuthData)\n    if curr != nil {\n        t.Fatalf(\"got data %+v, want nil\", curr)\n    }\n\n    et.OverrideAuthInfo(\"foo\", &AuthData{\"email\"})\n\n    curr = auth.Data().(*AuthData)\n    if curr == nil || curr.Email != \"email\" {\n        t.Fatalf(\"got data %+v, want %q\", curr, &AuthData{\"email\"})\n    }\n}\n\nfunc TestOverrideAuthInfo_ResetBetweenTests(t *testing.T) {\n    curr  := auth.Data()\n    if curr != nil {\n        t.Fatalf(\"got data %+v, want nil\", curr)\n    }\n}\n\nfunc TestOverrideAuthInfo_PropagatesToAPICalls(t *testing.T) {\n    resp, err := GetUser(context.Background())\n    if err != nil {\n        t.Fatal(err)\n    } else if resp.Data != nil {\n        t.Fatalf(\"got data %+v, want nil\", resp.Data)\n    }\n\n    et.OverrideAuthInfo(\"foo\", &AuthData{\"email\"})\n\n    resp, err = GetUser(context.Background())\n    if err != nil {\n        t.Fatal(err)\n    } else if resp.Data == nil || resp.Data.Email != \"email\" {\n        t.Fatalf(\"got data %+v, want %+v\", resp.Data, &AuthData{\"email\"})\n    }\n}\n\nfunc TestOverrideAuthInfo_APICallOptsOverride(t *testing.T) {\n    et.OverrideAuthInfo(\"foo\", &AuthData{\"email\"})\n\n    ctx := auth.WithContext(context.Background(), \"bar\", &AuthData{\"email2\"})\n    resp, err := GetUser(ctx)\n    if err != nil {\n        t.Fatal(err)\n    } else if resp.Data == nil || resp.Data.Email != \"email2\" {\n        t.Fatalf(\"got data %+v, want %+v\", resp.Data, &AuthData{\"email2\"})\n    }\n}\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype Response struct {\n    UserID auth.UID\n    Data *AuthData\n}\n\n//encore:api public\nfunc GetUser(ctx context.Context) (*Response, error) {\n    uid, _ := auth.UserID()\n    data, _ := auth.Data().(*AuthData)\n    return &Response{UserID: uid, Data: data}, nil\n}\n\ntype AuthData struct {\n    Email string\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, *AuthData, error) {\n    return \"hello\", &AuthData{Email: \"hello@example.org\"}, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/experiment_local_secrets_override.txtar",
    "content": "env ENCORE_EXPERIMENT=local-secrets-override\nrun\ncall GET /svc.GetFoo\ncheckresp '{\"Data\": \"bar\"}'\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\ntype Resp struct {\n\tData string\n}\n\nvar secrets struct {\n\tFoo string\n}\n\n//encore:api public\nfunc GetFoo(ctx context.Context) (*Resp, error) {\n\treturn &Resp{Data: secrets.Foo}, nil\n}\n\n-- .secrets.local.cue --\nFoo: \"bar\"\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/fallback_routes.txt",
    "content": "run\ncall GET /v1/regular ''\ncheckresp '{\"Message\":\"Hello, Encore world!\"}'\ncall GET /v1/regular/foo ''\ncheckresp '{\"fallback\": true}'\ncall POST /v1/regular ''\ncheckresp '{\"fallback\": true}'\ncall GET /v2/regular ''\ncheckresp '{\"fallback\": true}'\ncall GET / ''\ncheckresp '{\"fallback\": true}'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"net/http\"\n)\n\n//encore:api public raw path=/!fallback\nfunc Fallback(w http.ResponseWriter, req *http.Request) {\n    w.Write([]byte(`{\"fallback\": true}`))\n}\n\ntype Response struct {\n    Message string\n}\n\n//encore:api public method=GET path=/v1/regular\nfunc Regular(ctx context.Context) (*Response, error) {\n    return &Response{Message: \"Hello, Encore world!\"}, nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/graceful_shutdown.txt",
    "content": "run\nshutdown\nchecklog '{\"message\": \"shutting down\"}'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"encore.dev/shutdown\"\n    \"encore.dev/rlog\"\n)\n\n//encore:service\ntype Service struct{}\n\nfunc (s *Service) Shutdown(p shutdown.Progress) error {\n    rlog.Info(\"shutting down\")\n    return nil\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/pubsub_method_handler.txt",
    "content": "run\npublish pointer '{\"Data\": \"test\"}'\npublish non-pointer '{\"Data\": \"test\"}'\nchecklog '{\"topic\": \"pointer\", \"subscription\": \"pointer\", \"event\": {\"Data\": \"test\"}, \"message\": \"pointer method\"}'\nchecklog '{\"topic\": \"non-pointer\", \"subscription\": \"non-pointer\", \"event\": {\"Data\": \"test\"}, \"message\": \"non-pointer method\"}'\n\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/rlog\"\n    \"encore.dev/pubsub\"\n)\n\n//encore:service\ntype Service struct{}\n\nfunc (s *Service) PointerMethod(ctx context.Context, event *Event) error {\n    rlog.Info(\"pointer method\", \"event\", event)\n    return nil\n}\n\nfunc (s Service) NonPointerMethod(ctx context.Context, event *Event) error {\n    rlog.Info(\"non-pointer method\", \"event\", event)\n    return nil\n}\n\ntype Event struct {\n    Data string\n}\n\nvar Pointer = pubsub.NewTopic[*Event](\"pointer\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar NonPointer = pubsub.NewTopic[*Event](\"non-pointer\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar _ = pubsub.NewSubscription(Pointer, \"pointer\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: pubsub.MethodHandler((*Service).PointerMethod),\n    },\n)\n\nvar _ = pubsub.NewSubscription(NonPointer, \"non-pointer\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: pubsub.MethodHandler(Service.NonPointerMethod),\n    },\n)\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/pubsub_ref.txt",
    "content": "test\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/pubsub\"\n)\n\ntype Msg struct{Message string}\n\nvar Topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\n//encore:api\nfunc Dummy(context.Context) error { return nil }\n\n-- svc/svc_test.go --\npackage svc\n\nimport (\n    \"context\"\n    \"testing\"\n    \"encore.dev/et\"\n    \"encore.dev/pubsub\"\n)\n\nfunc TestRefPublish(t *testing.T) {\n    ref := pubsub.TopicRef[pubsub.Publisher[Msg]](Topic)\n    ref.Publish(context.Background(), Msg{Message: \"test\"})\n\n    msgs := et.Topic(Topic).PublishedMessages()\n    if len(msgs) != 1 || msgs[0].Message != \"test\" {\n        t.Fatalf(\"got %v, want %v\", msgs, []Msg{{Message: \"test\"}})\n    }\n\n    meta := ref.Meta()\n    want := pubsub.TopicMeta{\n        Name: \"topic\",\n        Config: pubsub.TopicConfig{\n            DeliveryGuarantee: pubsub.AtLeastOnce,\n        },\n    }\n    if meta != want {\n        t.Fatalf(\"got meta %v, want %v\", meta, want)\n    }\n}\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/ts_hello.txt",
    "content": "run\ncall GET /hello/world ''\ncheckresp '{\"message\": \"Hello world\"}'\nshutdown\n\n-- encore.app --\n{\"lang\": \"typescript\"}\n\n-- package.json --\n{\n  \"name\": \"encore-ts-testapp\",\n  \"private\": true,\n  \"version\": \"0.0.1\",\n  \"description\": \"Encore Typescript Test app\",\n  \"license\": \"MPL-2.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.5.7\",\n    \"typescript\": \"^5.4\",\n    \"vitest\": \"^3.1.3\"\n  },\n  \"dependencies\": {\n    \"encore.dev\": \"1.50.0\"\n  },\n  \"optionalDependencies\": {\n    \"@rollup/rollup-linux-x64-gnu\": \"^4.13.0\"\n  }\n}\n\n-- tsconfig.json --\n{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"types\": [\"node\"],\n    \"paths\": {\n      \"~encore/*\": [\"./encore.gen/*\"]\n    },\n    \"composite\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"isolatedModules\": true,\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true\n  }\n}\n\n-- myservice/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"myservice\");\n\n-- myservice/api.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const hello = api(\n  { expose: true, method: \"GET\", path: \"/hello/:name\" },\n  async ({ name }: { name: string }): Promise<{ message: string }> => {\n    return { message: `Hello ${name}` };\n  }\n);\n"
  },
  {
    "path": "e2e-tests/testdata/testscript/ts_worker_pooling.txt",
    "content": "run\ncall GET /hello/world ''\ncheckresp '{\"message\": \"Hello world\"}'\nshutdown\n\n-- encore.app --\n{\n  \"lang\": \"typescript\",\n  \"build\": {\n    \"worker_pooling\": true\n  }\n}\n\n-- package.json --\n{\n  \"name\": \"encore-ts-testapp\",\n  \"private\": true,\n  \"version\": \"0.0.1\",\n  \"description\": \"Encore Typescript Test app\",\n  \"license\": \"MPL-2.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.5.7\",\n    \"typescript\": \"^5.4\",\n    \"vitest\": \"^3.1.3\"\n  },\n  \"dependencies\": {\n    \"encore.dev\": \"1.50.0\"\n  },\n  \"optionalDependencies\": {\n    \"@rollup/rollup-linux-x64-gnu\": \"^4.13.0\"\n  }\n}\n\n-- tsconfig.json --\n{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n    \"types\": [\"node\"],\n    \"paths\": {\n      \"~encore/*\": [\"./encore.gen/*\"]\n    },\n    \"composite\": true,\n    \"strict\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"isolatedModules\": true,\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true\n  }\n}\n\n-- myservice/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"myservice\");\n\n-- myservice/api.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const hello = api(\n  { expose: true, method: \"GET\", path: \"/hello/:name\" },\n  async ({ name }: { name: string }): Promise<{ message: string }> => {\n    return { message: `Hello ${name}` };\n  }\n);\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/.gitignore",
    "content": "encore.gen.go\nencore.gen.cue\n/.encore\n/encore.gen\nnode_modules\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/encore.app",
    "content": "{\"lang\": \"typescript\"}\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/package.json",
    "content": "{\n  \"name\": \"encore-ts-testapp\",\n  \"private\": true,\n  \"version\": \"0.0.1\",\n  \"description\": \"Encore Typescript Test app\",\n  \"license\": \"MPL-2.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"vitest\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.5.7\",\n    \"typescript\": \"^5.4\",\n    \"vitest\": \"^3.1.3\"\n  },\n  \"dependencies\": {\n    \"encore.dev\": \"file:/Users/andre/src/github.com/encoredev/encore/runtimes/js/encore.dev\"\n  },\n  \"optionalDependencies\": {\n    \"@rollup/rollup-linux-x64-gnu\": \"^4.13.0\"\n  }\n}"
  },
  {
    "path": "e2e-tests/testdata/tsapp/service1/api.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { hello } from \"./api\";\n\ndescribe(\"get\", () => {\n  it(\"should combine string with parameter value\", async () => {\n    const resp = await hello({ name: \"world\" });\n    expect(resp.message).toBe(\"Hello world\");\n  });\n});\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/service1/api.ts",
    "content": "import { api, HttpStatus } from \"encore.dev/api\";\nimport { APICallMeta, currentRequest } from \"encore.dev\";\nimport { service2 } from \"~encore/clients\";\nimport log from \"encore.dev/log\";\n\nexport const hello = api(\n  { expose: true, method: \"GET\", path: \"/hello/:name\" },\n  async ({ name }: { name: string }): Promise<{ message: string }> => {\n    return { message: `Hello ${name}` };\n  }\n);\n\n// Endpoint that demonstrates middleware data access\nexport const middlewareDemo = api(\n  { expose: true, method: \"GET\", path: \"/middleware-test\", tags: [\"mwtest\"] },\n  async (): Promise<{\n    message: string;\n    middlewareMsg: string;\n  }> => {\n    const req = currentRequest() as APICallMeta;\n\n    return {\n      message: \"Hello\",\n      middlewareMsg: req.middlewareData?.customMsg || \"Not set\"\n    };\n  }\n);\n\n// Service-to-service call: Get greeting from service2\nexport const getGreetingViaService2 = api(\n  { expose: true, method: \"POST\", path: \"/get-greeting\" },\n  async (req: {\n    name: string;\n    style?: \"formal\" | \"casual\" | \"excited\";\n  }): Promise<{\n    message: string;\n    greeting: string;\n  }> => {\n    try {\n      const result = await service2.greet({\n        name: req.name,\n        style: req.style || \"formal\"\n      });\n\n      return {\n        message: \"Greeting retrieved successfully via service-to-service call\",\n        greeting: result.greeting\n      };\n    } catch (error) {\n      log.error(\"Failed to get greeting via service call\", { error });\n      throw new Error(\n        \"Service-to-service call failed: \" + (error as Error).message\n      );\n    }\n  }\n);\n\n// Endpoint with custom HTTP status\nexport const customStatus = api(\n  { expose: true, method: \"GET\", path: \"/test-custom-status\" },\n  async (): Promise<{\n    message: string;\n    status: HttpStatus;\n  }> => {\n    return {\n      message: \"I accept!\",\n      status: 202\n    };\n  }\n);\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/service1/encore.service.ts",
    "content": "import { Service } from \"encore.dev/service\";\nimport { middleware } from \"encore.dev/api\";\n\n// Middleware to add custom data to request\nconst dataEnricher = middleware(async (req, next) => {\n  // Add custom data that endpoints can access\n  req.data.customMsg = \"Hello from middleware!\";\n\n  const resp = await next(req);\n\n  resp.status = 201;\n  resp.header.add(\"x-test-header\", \"hello\");\n\n  return resp;\n});\n\nexport default new Service(\"service1\", {\n  middlewares: [middleware({ target: { tags: [\"mwtest\"] } }, dataEnricher)]\n});\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/service2/api.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { testApiError } from \"./api\";\n\ndescribe(\"errors\", () => {\n  it(\"api error with no details with cause\", async () => {\n    await expect(\n      testApiError({ variant: \"no-details-with-cause\" })\n    ).rejects.toThrow(\n      expect.objectContaining({\n        message: \"the error\",\n        details: undefined,\n        code: \"canceled\",\n        cause: expect.objectContaining({\n          message: \"this is the cause\"\n        })\n      })\n    );\n  });\n\n  it(\"api error with no details no cause\", async () => {\n    await expect(\n      testApiError({ variant: \"no-details-no-cause\" })\n    ).rejects.toThrow(\n      expect.objectContaining({\n        message: \"the error\",\n        details: undefined,\n        code: \"canceled\",\n        cause: undefined\n      })\n    );\n  });\n\n  it(\"api error with details no cause\", async () => {\n    await expect(\n      testApiError({ variant: \"with-details-no-cause\" })\n    ).rejects.toThrow(\n      expect.objectContaining({\n        message: \"the error\",\n        details: { a: \"detail\" },\n        code: \"canceled\",\n        cause: undefined\n      })\n    );\n  });\n\n  it(\"api error with details with cause\", async () => {\n    await expect(\n      testApiError({ variant: \"with-details-with-cause\" })\n    ).rejects.toThrow(\n      expect.objectContaining({\n        message: \"the error\",\n        details: { a: \"detail\" },\n        code: \"canceled\",\n        cause: expect.objectContaining({\n          message: \"this is the cause\"\n        })\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/service2/api.ts",
    "content": "import { api, HttpStatus } from \"encore.dev/api\";\nimport { IsEmail, MaxLen, MinLen } from \"encore.dev/validate\";\nimport log from \"encore.dev/log\";\nimport { APIError } from \"encore.dev/api\";\n\ninterface GreetingRequest {\n  name: string;\n}\n\ninterface GreetingResponse {\n  greeting: string;\n  timestamp: Date;\n}\n\ninterface MessageRequest {\n  message: string & MinLen<3> & MaxLen<1000>;\n  recipient?: string & IsEmail;\n}\n\ninterface MessageResponse {\n  message: string;\n}\n\n// Generate different greeting styles\nexport const greet = api(\n  { expose: true, method: \"POST\", path: \"/greet\" },\n  async (req: GreetingRequest): Promise<GreetingResponse> => {\n    let greeting = `Hey ${req.name}! How's it going?`;\n    return {\n      greeting,\n      timestamp: new Date()\n    };\n  }\n);\n\nexport const testInputValidation = api(\n  { expose: true, method: \"POST\", path: \"/test-validation\" },\n  async (req: MessageRequest): Promise<MessageResponse> => {\n    return {\n      message: `Message processed`\n    };\n  }\n);\n\nexport const testApiError = api(\n  { expose: true, method: \"GET\", path: \"/test-api-error/:variant\" },\n  async ({ variant }: { variant: string }): Promise<{ message: string }> => {\n    switch (variant) {\n      case \"no-details-no-cause\":\n        throw APIError.canceled(\"the error\");\n      case \"with-details-no-cause\":\n        throw APIError.canceled(\"the error\").withDetails({ a: \"detail\" });\n      case \"no-details-with-cause\":\n        throw APIError.canceled(\"the error\", new Error(\"this is the cause\"));\n      case \"with-details-with-cause\":\n        throw APIError.canceled(\n          \"the error\",\n          new Error(\"this is the cause\")\n        ).withDetails({ a: \"detail\" });\n      default:\n        return { message: \"Hello there\" };\n    }\n  }\n);\n\nexport const testOtherError = api(\n  { expose: true, method: \"GET\", path: \"/test-other-error\" },\n  async (): Promise<{ message: string }> => {\n    throw new Error(\"This is a test error\");\n  }\n);\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/service2/encore.service.ts",
    "content": "import { Service } from \"encore.dev/service\";\n\nexport default new Service(\"service2\");\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/tsconfig.json",
    "content": "{\n    \"$schema\": \"https://json.schemastore.org/tsconfig\",\n    \"compilerOptions\": {\n        /* Basic Options */\n        \"lib\": [\"ES2022\"],\n        \"target\": \"ES2022\",\n        \"module\": \"ES2022\",\n        \"types\": [\"node\"],\n        \"paths\": {\n            \"~encore/*\": [\"./encore.gen/*\"]\n        },\n\n        /* Workspace Settings */\n        \"composite\": true,\n\n        /* Strict Type-Checking Options */\n        \"strict\": true,\n\n        /* Module Resolution Options */\n        \"moduleResolution\": \"bundler\",\n        \"allowSyntheticDefaultImports\": true,\n        \"isolatedModules\": true,\n        \"sourceMap\": true,\n\n        \"declaration\": true,\n\n        /* Advanced Options */\n        \"forceConsistentCasingInFileNames\": true,\n        \"skipLibCheck\": true\n    }\n}\n"
  },
  {
    "path": "e2e-tests/testdata/tsapp/vite.config.ts",
    "content": "/// <reference types=\"vitest\" />\nimport { defineConfig } from \"vite\";\nimport path from \"path\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"~encore\": path.resolve(__dirname, \"./encore.gen\")\n    }\n  }\n});\n"
  },
  {
    "path": "e2e-tests/testscript_test.go",
    "content": "//go:build e2e\n\npackage tests\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/nsqio/go-nsq\"\n\tts \"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"encr.dev/cli/daemon/run\"\n\t\"encr.dev/pkg/golden\"\n)\n\n// headerRe matches valid headers in the form \"Header=value\".\nvar headerRe = regexp.MustCompile(`^([A-Z][A-Za-z0-9-]*)=([^ ]*)$`)\n\nfunc TestRun(t *testing.T) {\n\tdoRun(t, nil)\n}\n\nfunc doRun(t *testing.T, experiments []string) {\n\truntimePath := os.Getenv(\"ENCORE_RUNTIMES_PATH\")\n\tgoroot := os.Getenv(\"ENCORE_GOROOT\")\n\tif testing.Short() {\n\t\tt.Skip(\"skipping in short mode\")\n\t} else if runtimePath == \"\" || goroot == \"\" {\n\t\tt.Skipf(\"skipping due to missing ENCORE_RUNTIMES_PATH=%q or ENCORE_GOROOT=%q\", runtimePath, goroot)\n\t}\n\n\thome := t.TempDir()\n\n\tts.Run(t, ts.Params{\n\t\tDir: \"testdata/testscript\",\n\t\tSetup: func(e *ts.Env) error {\n\t\t\te.Setenv(\"ENCORE_RUNTIMES_PATH\", runtimePath)\n\t\t\te.Setenv(\"ENCORE_GOROOT\", goroot)\n\t\t\te.Setenv(\"EXTRA_EXPERIMENTS\", strings.Join(experiments, \",\"))\n\t\t\te.Setenv(\"HOME\", home)\n\t\t\te.Setenv(\"GOFLAGS\", \"-modcacherw\")\n\t\t\tgomod := []byte(\"module test\\n\\ngo 1.21.0\\n\\nrequire encore.dev v1.52.0\")\n\t\t\tif err := os.WriteFile(filepath.Join(e.WorkDir, \"go.mod\"), gomod, 0755); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := runGoModTidy(e.WorkDir); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tinitVals(e)\n\t\t\treturn nil\n\t\t},\n\t\tCmds: map[string]func(ts *ts.TestScript, neg bool, args []string){\n\t\t\t\"run\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tlog := &testscriptLogger{ts: ts}\n\t\t\t\texp := ts.Getenv(\"ENCORE_EXPERIMENT\")\n\t\t\t\tif extra := ts.Getenv(\"EXTRA_EXPERIMENTS\"); extra != \"\" {\n\t\t\t\t\tif exp != \"\" {\n\t\t\t\t\t\texp += \",\"\n\t\t\t\t\t}\n\t\t\t\t\texp += extra\n\t\t\t\t}\n\n\t\t\t\tenv := []string{\"ENCORE_EXPERIMENT=\" + exp}\n\t\t\t\tif nodePath, ok := getNodeJSPath().Get(); ok {\n\t\t\t\t\tenv = append(env, \"PATH=\"+nodePath)\n\t\t\t\t}\n\n\t\t\t\tapp := RunApp(getTB(ts), getWorkdir(ts), log, env)\n\t\t\t\tsetVal(ts, \"app\", app)\n\t\t\t\tsetVal(ts, \"log\", log)\n\t\t\t},\n\t\t\t\"shutdown\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tapp := getVal[*RunAppData](ts, \"app\")\n\t\t\t\tapp.Run.ProcGroup().Close()\n\t\t\t},\n\t\t\t\"test\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tlog := &testscriptLogger{ts: ts}\n\t\t\t\texp := ts.Getenv(\"ENCORE_EXPERIMENT\")\n\t\t\t\tif extra := ts.Getenv(\"EXTRA_EXPERIMENTS\"); extra != \"\" {\n\t\t\t\t\tif exp != \"\" {\n\t\t\t\t\t\texp += \",\"\n\t\t\t\t\t}\n\t\t\t\t\texp += extra\n\t\t\t\t}\n\n\t\t\t\terr := RunTests(getTB(ts), getWorkdir(ts), &log.stdout, &log.stderr, []string{\"ENCORE_EXPERIMENT=\" + exp})\n\t\t\t\t_, _ = os.Stdout.Write(log.stdout.Bytes())\n\t\t\t\t_, _ = os.Stderr.Write(log.stderr.Bytes())\n\t\t\t\tif !neg && err != nil {\n\t\t\t\t\tts.Fatalf(\"tests failed: %v\", err)\n\t\t\t\t} else if neg && err == nil {\n\t\t\t\t\tts.Fatalf(\"tests unexpectedly passed: %v\", err)\n\t\t\t\t}\n\t\t\t\tsetVal(ts, \"log\", log)\n\t\t\t},\n\t\t\t\"call\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tusage := func() {\n\t\t\t\t\tts.Fatalf(\"usage: call <method> [Header=value...] <url> [data] [platform-auth]\")\n\t\t\t\t}\n\t\t\t\tif len(args) < 2 {\n\t\t\t\t\tusage()\n\t\t\t\t}\n\n\t\t\t\tmethod := args[0]\n\t\t\t\targs = args[1:]\n\t\t\t\theaders := make(map[string]string)\n\t\t\t\tfor i, arg := range args {\n\t\t\t\t\tm := headerRe.FindStringSubmatch(arg)\n\t\t\t\t\tif m != nil {\n\t\t\t\t\t\theaders[m[1]] = m[2]\n\t\t\t\t\t} else {\n\t\t\t\t\t\targs = args[i:]\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(args) == 0 {\n\t\t\t\t\tusage()\n\t\t\t\t}\n\t\t\t\tapp := getVal[*RunAppData](ts, \"app\")\n\t\t\t\turl := \"http://\" + app.Addr + args[0]\n\n\t\t\t\tdisablePlatformAuth := false\n\n\t\t\t\tvar body io.Reader\n\t\t\t\tif n := len(args); n > 1 {\n\t\t\t\t\tval := args[1]\n\t\t\t\t\tif val == \"no-platform-auth\" {\n\t\t\t\t\t\tdisablePlatformAuth = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbody = strings.NewReader(val)\n\t\t\t\t\t\tif n > 2 {\n\t\t\t\t\t\t\tif args[2] == \"no-platform-auth\" {\n\t\t\t\t\t\t\t\tdisablePlatformAuth = true\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tts.Fatalf(\"unexpected argument %q\", args[2])\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treq := httptest.NewRequest(method, url, body)\n\t\t\t\tfor k, v := range headers {\n\t\t\t\t\tts.Logf(\"setting %s=%v\", k, v)\n\t\t\t\t\treq.Header.Set(k, v)\n\t\t\t\t}\n\n\t\t\t\tif disablePlatformAuth {\n\t\t\t\t\treq.Header.Set(run.TestHeaderDisablePlatformAuth, \"true\")\n\t\t\t\t}\n\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\tapp.Run.ServeHTTP(w, req)\n\t\t\t\trespBody := w.Body.Bytes()\n\t\t\t\t_, _ = os.Stdout.Write(respBody)\n\n\t\t\t\tif w.Code != http.StatusOK && !neg {\n\t\t\t\t\tts.Fatalf(\"unexpected status code: %v: %s\", w.Code, respBody)\n\t\t\t\t} else if w.Code == http.StatusOK && neg {\n\t\t\t\t\tts.Fatalf(\"unexpected status code: %v: %s\", w.Code, respBody)\n\t\t\t\t}\n\t\t\t\tapp.Values[\"call_resp\"] = respBody\n\t\t\t},\n\t\t\t\"publish\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tif len(args) != 2 {\n\t\t\t\t\tts.Fatalf(\"usage: publish <topic> <data>\")\n\t\t\t\t}\n\t\t\t\ttopicName := args[0]\n\t\t\t\tdata := args[1]\n\n\t\t\t\tapp := getVal[*RunAppData](ts, \"app\")\n\t\t\t\tid, _ := app.Values[\"publish_id\"].(int)\n\t\t\t\tid++\n\n\t\t\t\tapp.Values[\"publish_id\"] = id\n\t\t\t\tmsgData, _ := json.Marshal(messageWrapper{\n\t\t\t\t\tID:         strconv.Itoa(id),\n\t\t\t\t\tAttributes: nil,\n\t\t\t\t\tData:       json.RawMessage(data),\n\t\t\t\t})\n\n\t\t\t\tprod, err := nsq.NewProducer(app.NSQ.Addr(), nsq.NewConfig())\n\t\t\t\tif err != nil {\n\t\t\t\t\tts.Fatalf(\"unable to create producer: %v\", err)\n\t\t\t\t}\n\t\t\t\tprod.SetLoggerLevel(nsq.LogLevelMax)\n\t\t\t\tif err := prod.Publish(topicName, msgData); err != nil {\n\t\t\t\t\tts.Fatalf(\"unable to publish: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Wait for the message to get processed\n\t\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\t\tstats, err := app.NSQ.Stats()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tts.Fatalf(\"unable to get nsq stats: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tfor _, topic := range stats.Topics {\n\t\t\t\t\t\tif topic.TopicName == topicName {\n\t\t\t\t\t\t\tif topic.Depth == 0 {\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tts.Logf(\"waiting for %q queue to be processed, depth: %d\", topic.TopicName, topic.Depth)\n\t\t\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"checklog\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tif len(args) != 1 {\n\t\t\t\t\tts.Fatalf(\"usage: checklog <pattern|file>\")\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\tvar want []jsonObj\n\t\t\t\tvar pattern jsonObj\n\t\t\t\terr := json.Unmarshal([]byte(args[0]), &pattern)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif strings.Contains(args[0], \"{\") {\n\t\t\t\t\t\tts.Fatalf(\"checklog pattern not valid log line: %v\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\tfn := ts.ReadFile(args[0])\n\t\t\t\t\tscanner := bufio.NewScanner(strings.NewReader(fn))\n\t\t\t\t\tfor scanner.Scan() {\n\t\t\t\t\t\tvar ln jsonObj\n\t\t\t\t\t\tif err := json.Unmarshal(scanner.Bytes(), &ln); err != nil {\n\t\t\t\t\t\t\tts.Fatalf(\"invalid log line in checklog script: %s: %v\", scanner.Bytes(), err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\twant = append(want, ln)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twant = []jsonObj{pattern}\n\t\t\t\t}\n\n\t\t\t\tlog := getVal[*testscriptLogger](ts, \"log\")\n\t\t\t\tstderr := log.stderr.String()\n\t\t\t\tscanner := bufio.NewScanner(strings.NewReader(stderr))\n\n\t\t\t\tseen := make([]bool, len(want))\n\t\t\t\tfor scanner.Scan() {\n\t\t\t\t\tvar got jsonObj\n\t\t\t\t\tif err := json.Unmarshal(scanner.Bytes(), &got); err == nil {\n\t\t\t\t\t\tfor i, ln := range want {\n\t\t\t\t\t\t\tif !seen[i] && gotLogLine(got, ln) {\n\t\t\t\t\t\t\t\tseen[i] = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor i, ln := range want {\n\t\t\t\t\tif !neg && !seen[i] {\n\t\t\t\t\t\tts.Fatalf(\"unable to find log line: %v\", ln)\n\t\t\t\t\t} else if neg && seen[i] {\n\t\t\t\t\t\tts.Fatalf(\"found log line: %v\", ln)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"checkresp\": func(ts *ts.TestScript, neg bool, args []string) {\n\t\t\t\tif len(args) != 1 {\n\t\t\t\t\tts.Fatalf(\"usage: checkresp <pattern|file>\")\n\t\t\t\t}\n\n\t\t\t\tvar want jsonObj\n\t\t\t\terr := json.Unmarshal([]byte(args[0]), &want)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif strings.Contains(args[0], \"{\") {\n\t\t\t\t\t\tts.Fatalf(\"checkresp pattern not valid log line: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tfn := ts.ReadFile(args[0])\n\t\t\t\t\tif err := json.Unmarshal([]byte(fn), &want); err != nil {\n\t\t\t\t\t\tts.Fatalf(\"invalid json object in checkresp script: %s: %v\", fn, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tapp := getVal[*RunAppData](ts, \"app\")\n\n\t\t\t\tvar got jsonObj\n\t\t\t\tif err := json.Unmarshal(app.Values[\"call_resp\"].([]byte), &got); err != nil {\n\t\t\t\t\tts.Fatalf(\"unable to parse response: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tmatch := gotLogLine(got, want)\n\t\t\t\tif !neg && !match {\n\t\t\t\t\tts.Fatalf(\"response does not match: got %s, want %s\", got, want)\n\t\t\t\t} else if neg && match {\n\t\t\t\t\tts.Fatalf(\"log line unexpectedly matched\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc TestMain(m *testing.M) {\n\tgolden.Setup()\n\n\tos.Exit(ts.RunMain(m, nil))\n}\n\n// messageWrapper is the data structure for an NSQ message.\n// It must be synchronized with the nsq/topic.go file in the runtime.\ntype messageWrapper struct {\n\tID         string\n\tAttributes map[string]string\n\tData       json.RawMessage\n}\n\ntype jsonObj = map[string]any\n\nfunc gotLogLine(got, want any) bool {\n\tswitch want := want.(type) {\n\tcase map[string]any:\n\t\tgot, ok := got.(map[string]any)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tfor k, v := range want {\n\t\t\tif !gotLogLine(got[k], v) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\tdefault:\n\t\treturn got == want\n\t}\n}\n\ntype testscriptLogger struct {\n\tts             *ts.TestScript\n\tstdout, stderr bytes.Buffer\n}\n\nfunc (l *testscriptLogger) RunStdout(r *run.Run, line []byte) {\n\tl.ts.Logf(\"%s\", line)\n\tl.stdout.Write(line)\n}\n\nfunc (l *testscriptLogger) RunStderr(r *run.Run, line []byte) {\n\tl.ts.Logf(\"%s\", line)\n\tl.stderr.Write(line)\n}\n\nfunc initVals(e *ts.Env) {\n\te.Values[\"vars\"] = map[string]any{\n\t\t\"tb\": e.T().(testing.TB),\n\t\t\"wd\": e.WorkDir,\n\t}\n}\n\nfunc getTB(ts *ts.TestScript) testing.TB {\n\treturn getVal[testing.TB](ts, \"tb\")\n}\n\nfunc getWorkdir(ts *ts.TestScript) string {\n\treturn getVal[string](ts, \"wd\")\n}\n\nfunc setVal(ts *ts.TestScript, key string, val any) {\n\tts.Value(\"vars\").(map[string]any)[key] = val\n}\n\nfunc getVal[T any](ts *ts.TestScript, key string) T {\n\treturn ts.Value(\"vars\").(map[string]any)[key].(T)\n}\n"
  },
  {
    "path": "e2e-tests/ts_app_test.go",
    "content": "//go:build e2e\n\npackage tests\n\nimport (\n\t\"encoding/json\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc TestTSEndToEndWithApp(t *testing.T) {\n\tc := qt.New(t)\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tnodePath, ok := getNodeJSPath().Get()\n\tif !ok {\n\t\tc.Fatal(\"Could not find nodejs binary, it is needed to run typescript apps\")\n\t}\n\n\tappRoot := filepath.Join(wd, \"testdata\", \"tsapp\")\n\tapp := RunApp(c, appRoot, nil, []string{\"PATH=\" + nodePath})\n\trun := app.Run\n\n\t// Test basic hello endpoint\n\tc.Run(\"typescript hello endpoint\", func(c *qt.C) {\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/hello/world\", nil)\n\t\trun.ServeHTTP(w, req)\n\t\tc.Assert(w.Code, qt.Equals, 200)\n\t\tc.Assert(w.Body.Bytes(), qt.JSONEquals, map[string]string{\n\t\t\t\"message\": \"Hello world\",\n\t\t})\n\t})\n\n\t// Test middleware functionality\n\tc.Run(\"middleware demo endpoint\", func(c *qt.C) {\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/middleware-test\", nil)\n\t\trun.ServeHTTP(w, req)\n\n\t\t// status modified by mw\n\t\tc.Assert(w.Code, qt.Equals, 201)\n\n\t\t// header set by mw\n\t\tc.Assert(w.Header().Get(\"X-Test-Header\"), qt.Equals, \"hello\")\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\t// Verify middleware data is present\n\t\tc.Assert(response[\"message\"], qt.Equals, \"Hello\")\n\t\tc.Assert(response[\"middlewareMsg\"], qt.Equals, \"Hello from middleware!\")\n\t})\n\n\t// Test custom HTTP status - 404 Not Found\n\tc.Run(\"custom HTTP status\", func(c *qt.C) {\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/test-custom-status\", nil)\n\t\trun.ServeHTTP(w, req)\n\t\tc.Assert(w.Code, qt.Equals, 202)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"message\"], qt.Equals, \"I accept!\")\n\t})\n\n\tc.Run(\"service2 greeting endpoint\", func(c *qt.C) {\n\t\trequestBody := `{\"name\": \"Bob\"}`\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"POST\", \"/greet\", strings.NewReader(requestBody))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\trun.ServeHTTP(w, req)\n\n\t\tc.Assert(w.Code, qt.Equals, 200)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"greeting\"], qt.Equals, \"Hey Bob! How's it going?\")\n\t})\n\n\tc.Run(\"service-to-service greeting call\", func(c *qt.C) {\n\t\trequestBody := `{\"name\": \"Charlie\"}`\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"POST\", \"/get-greeting\", strings.NewReader(requestBody))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\trun.ServeHTTP(w, req)\n\n\t\tc.Assert(w.Code, qt.Equals, 200)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"message\"], qt.Equals, \"Greeting retrieved successfully via service-to-service call\")\n\t\tc.Assert(response[\"greeting\"], qt.Equals, \"Hey Charlie! How's it going?\")\n\t})\n\n\tc.Run(\"service2 input validation - valid\", func(c *qt.C) {\n\t\trequestBody := `{\"message\": \"Hello world\", \"recipient\": \"test@example.com\"}`\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"POST\", \"/test-validation\", strings.NewReader(requestBody))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\trun.ServeHTTP(w, req)\n\n\t\tc.Assert(w.Code, qt.Equals, 200)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"message\"], qt.Equals, \"Message processed\")\n\t})\n\n\tc.Run(\"service2 input validation - to short\", func(c *qt.C) {\n\t\trequestBody := `{\"message\": \"Hi\"}`\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"POST\", \"/test-validation\", strings.NewReader(requestBody))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\trun.ServeHTTP(w, req)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"message\"], qt.Contains, \"message: length too short\")\n\n\t\t// Should return validation error\n\t\tc.Assert(w.Code, qt.Equals, 400)\n\t})\n\n\tc.Run(\"service2 input validation - invalid email\", func(c *qt.C) {\n\t\trequestBody := `{\"message\": \"Valid message\", \"recipient\": \"not-an-email\"}`\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"POST\", \"/test-validation\", strings.NewReader(requestBody))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\trun.ServeHTTP(w, req)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"message\"], qt.Contains, \"value is not an email\")\n\n\t\t// Should return validation error\n\t\tc.Assert(w.Code, qt.Equals, 400)\n\t})\n\n\t// Test error handling\n\tc.Run(\"service2 api errors\", func(c *qt.C) {\n\t\texpected := map[string]map[string]interface{}{\n\t\t\t\"no-details-no-cause\": {\n\t\t\t\t\"code\":             \"canceled\",\n\t\t\t\t\"details\":          nil,\n\t\t\t\t\"internal_message\": nil,\n\t\t\t\t\"message\":          \"the error\",\n\t\t\t},\n\t\t\t\"with-details-no-cause\": {\n\t\t\t\t\"code\": \"canceled\",\n\t\t\t\t\"details\": map[string]interface{}{\n\t\t\t\t\t\"a\": \"detail\",\n\t\t\t\t},\n\t\t\t\t\"internal_message\": nil,\n\t\t\t\t\"message\":          \"the error\",\n\t\t\t},\n\t\t\t\"no-details-with-cause\": {\n\t\t\t\t\"code\":             \"canceled\",\n\t\t\t\t\"details\":          nil,\n\t\t\t\t\"internal_message\": nil,\n\t\t\t\t\"message\":          \"the error: this is the cause\",\n\t\t\t},\n\t\t\t\"with-details-with-cause\": {\n\t\t\t\t\"code\": \"canceled\",\n\t\t\t\t\"details\": map[string]interface{}{\n\t\t\t\t\t\"a\": \"detail\",\n\t\t\t\t},\n\t\t\t\t\"internal_message\": nil,\n\t\t\t\t\"message\":          \"the error: this is the cause\",\n\t\t\t},\n\t\t}\n\n\t\tfor path, expected := range expected {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"GET\", \"/test-api-error/\"+path, nil)\n\t\t\trun.ServeHTTP(w, req)\n\t\t\tc.Assert(w.Code, qt.Equals, 499)\n\n\t\t\tvar response map[string]interface{}\n\t\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tc.Assert(response[\"code\"], qt.Equals, expected[\"code\"])\n\t\t\tif expected[\"details\"] == nil {\n\t\t\t\tc.Assert(response[\"details\"], qt.IsNil)\n\t\t\t} else {\n\t\t\t\tc.Assert(response[\"details\"], qt.DeepEquals, expected[\"details\"])\n\t\t\t}\n\t\t\tc.Assert(response[\"internal_message\"], qt.Equals, expected[\"internal_message\"])\n\t\t\tc.Assert(response[\"message\"], qt.Equals, expected[\"message\"])\n\t\t}\n\n\t})\n\n\tc.Run(\"service2 other errors\", func(c *qt.C) {\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/test-other-error/\", nil)\n\t\trun.ServeHTTP(w, req)\n\t\tc.Assert(w.Code, qt.Equals, 500)\n\n\t\tvar response map[string]interface{}\n\t\terr := json.Unmarshal(w.Body.Bytes(), &response)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\tc.Assert(response[\"code\"], qt.Equals, \"internal\")\n\t\tc.Assert(response[\"details\"], qt.IsNil)\n\t\tc.Assert(response[\"internal_message\"], qt.Equals, \"This is a test error\")\n\t\tc.Assert(response[\"message\"], qt.Equals, \"an internal error occurred\")\n\t})\n\n\t// Run TypeScript tests\n\tc.Run(\"run TypeScript tests\", func(c *qt.C) {\n\t\terr := RunTests(c.TB, appRoot, os.Stdout, os.Stderr, nil)\n\t\tc.Assert(err, qt.IsNil)\n\t})\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module encr.dev\n\ngo 1.24.0\n\ntoolchain go1.24.9\n\nrequire (\n\tcloud.google.com/go/storage v1.43.0\n\tcuelang.org/go v0.4.3\n\tencore.dev v1.1.0\n\tgithub.com/agnivade/levenshtein v1.1.1\n\tgithub.com/alecthomas/chroma v0.10.0\n\tgithub.com/alicebob/miniredis/v2 v2.23.0\n\tgithub.com/bep/debounce v1.2.1\n\tgithub.com/bluele/gcache v0.0.2\n\tgithub.com/briandowns/spinner v1.19.0\n\tgithub.com/cenkalti/backoff/v4 v4.2.1\n\tgithub.com/charmbracelet/bubbles v0.16.1\n\tgithub.com/charmbracelet/bubbletea v0.24.2\n\tgithub.com/charmbracelet/lipgloss v0.7.1\n\tgithub.com/cockroachdb/errors v1.11.1\n\tgithub.com/dave/jennifer v1.7.0\n\tgithub.com/evanw/esbuild v0.19.8\n\tgithub.com/fatih/color v1.15.0\n\tgithub.com/fatih/structtag v1.2.0\n\tgithub.com/frankban/quicktest v1.14.6\n\tgithub.com/fsnotify/fsnotify v1.8.0\n\tgithub.com/getkin/kin-openapi v0.115.0\n\tgithub.com/gofrs/uuid v4.4.0+incompatible\n\tgithub.com/golang-migrate/migrate/v4 v4.15.2\n\tgithub.com/golang/protobuf v1.5.4\n\tgithub.com/google/btree v1.1.3\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-containerregistry v0.20.6\n\tgithub.com/google/renameio/v2 v2.0.0\n\tgithub.com/gorilla/mux v1.8.1\n\tgithub.com/gorilla/websocket v1.5.0\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgithub.com/hasura/go-graphql-client v0.12.1\n\tgithub.com/jackc/pgconn v1.14.3\n\tgithub.com/jackc/pgproto3/v2 v2.3.3\n\tgithub.com/jackc/pgx/v5 v5.7.4\n\tgithub.com/json-iterator/go v1.1.12\n\tgithub.com/julienschmidt/httprouter v1.3.0\n\tgithub.com/jwalton/go-supportscolor v1.1.0\n\tgithub.com/knadh/koanf/parsers/toml/v2 v2.1.0\n\tgithub.com/knadh/koanf/providers/file v1.1.2\n\tgithub.com/knadh/koanf/providers/rawbytes v0.1.0\n\tgithub.com/knadh/koanf/v2 v2.1.2\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/logrusorgru/aurora v2.0.3+incompatible\n\tgithub.com/logrusorgru/aurora/v3 v3.0.0\n\tgithub.com/mattn/go-runewidth v0.0.15\n\tgithub.com/mattn/go-sqlite3 v1.14.18\n\tgithub.com/modern-go/reflect2 v1.0.2\n\tgithub.com/nsqio/go-nsq v1.1.0\n\tgithub.com/nsqio/nsq v1.2.1\n\tgithub.com/pelletier/go-toml v1.9.5\n\tgithub.com/peterbourgon/diskv v2.0.1+incompatible\n\tgithub.com/pkg/browser v0.0.0-20210911075715-681adbf594b8\n\tgithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/robfig/cron/v3 v3.0.1\n\tgithub.com/rogpeppe/go-internal v1.14.1\n\tgithub.com/rs/xid v1.6.0\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/spf13/cobra v1.9.1\n\tgithub.com/spf13/pflag v1.0.6\n\tgithub.com/sqlc-dev/sqlc v1.29.0\n\tgithub.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a\n\tgo.encore.dev/platform-sdk v1.1.0\n\tgo.uber.org/goleak v1.3.0\n\tgo4.org v0.0.0-20230225012048-214862532bf5\n\tgolang.org/x/crypto v0.39.0\n\tgolang.org/x/exp v0.0.0-20250305212735-054e65f0b394\n\tgolang.org/x/mod v0.25.0\n\tgolang.org/x/net v0.41.0\n\tgolang.org/x/oauth2 v0.30.0\n\tgolang.org/x/sync v0.15.0\n\tgolang.org/x/sys v0.33.0\n\tgolang.org/x/term v0.32.0\n\tgolang.org/x/text v0.26.0\n\tgolang.org/x/tools v0.34.0\n\tgoogle.golang.org/api v0.200.0\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f\n\tgoogle.golang.org/grpc v1.71.1\n\tgoogle.golang.org/protobuf v1.36.6\n\tgotest.tools/v3 v3.5.1\n\tmvdan.cc/sh/v3 v3.12.0\n\tsigs.k8s.io/yaml v1.3.0\n)\n\nrequire (\n\tcel.dev/expr v0.19.1 // indirect\n\tfilippo.io/edwards25519 v1.1.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/fmstephe/unsafeutil v1.0.0 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect\n\tgithub.com/spf13/cast v1.7.1 // indirect\n\tgithub.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n)\n\nrequire (\n\tcloud.google.com/go v0.115.1 // indirect\n\tcloud.google.com/go/auth v0.9.8 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect\n\tcloud.google.com/go/compute/metadata v0.7.0 // indirect\n\tcloud.google.com/go/iam v1.2.1 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/algolia/algoliasearch-client-go/v3 v3.31.4\n\tgithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect\n\tgithub.com/antlr4-go/antlr/v4 v4.13.1 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/benbjohnson/clock v1.3.5 // indirect\n\tgithub.com/blang/semver v3.5.1+incompatible // indirect\n\tgithub.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cockroachdb/apd/v2 v2.0.2 // indirect\n\tgithub.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect\n\tgithub.com/cockroachdb/redact v1.1.5 // indirect\n\tgithub.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect\n\tgithub.com/cubicdaiya/gonp v1.0.4 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/dlclark/regexp2 v1.7.0 // indirect\n\tgithub.com/docker/cli v28.2.2+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker v28.2.2+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.9.3 // indirect\n\tgithub.com/docker/go-connections v0.5.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/emicklei/proto v1.9.2 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/getsentry/sentry-go v0.25.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.20.0 // indirect\n\tgithub.com/go-openapi/swag v0.22.4 // indirect\n\tgithub.com/go-redis/redis/v8 v8.11.5 // indirect\n\tgithub.com/go-sql-driver/mysql v1.9.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.2.1 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/glog v1.2.4 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/snappy v0.0.4 // indirect\n\tgithub.com/google/cel-go v0.24.1 // indirect\n\tgithub.com/google/s2a-go v0.1.8 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.13.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/invopop/yaml v0.1.0 // indirect\n\tgithub.com/jackc/chunkreader/v2 v2.0.1 // indirect\n\tgithub.com/jackc/pgio v1.0.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/knadh/koanf/maps v0.1.1 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mark3labs/mcp-go v0.27.0\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/term v0.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect\n\tgithub.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/reflow v0.3.0 // indirect\n\tgithub.com/muesli/termenv v0.15.2 // indirect\n\tgithub.com/nsqio/go-diskqueue v1.1.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.2 // indirect\n\tgithub.com/perimeterx/marshmallow v1.1.4 // indirect\n\tgithub.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect\n\tgithub.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect\n\tgithub.com/pingcap/log v1.1.0 // indirect\n\tgithub.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/protocolbuffers/txtpbfmt v0.0.0-20220131092820-39736dd543b4 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/rivo/uniseg v0.4.4 // indirect\n\tgithub.com/riza-io/grpc-go v0.2.0 // indirect\n\tgithub.com/sahilm/fuzzy v0.1.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/stoewer/go-strcase v1.2.0 // indirect\n\tgithub.com/tetratelabs/wazero v1.9.0 // indirect\n\tgithub.com/vbatts/tar-split v0.12.1 // indirect\n\tgithub.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect\n\tgithub.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect\n\tgo.opentelemetry.io/otel v1.36.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.36.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.36.0 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgolang.org/x/time v0.7.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tmodernc.org/libc v1.62.1 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.9.1 // indirect\n\tmodernc.org/sqlite v1.37.0 // indirect\n\tnhooyr.io/websocket v1.8.10 // indirect\n)\n\n// The implementation of the `encore.dev` runtime, is in this repo\n// along side the Encore CLI\nreplace encore.dev => ./runtimes/go\n"
  },
  {
    "path": "go.sum",
    "content": "bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=\nbazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=\ncel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=\ncel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=\ncloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=\ncloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8=\ncloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=\ncloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=\ncloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=\ncloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=\ncloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU=\ncloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g=\ncloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc=\ncloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/spanner v1.28.0/go.mod h1:7m6mtQZn/hMbMfx62ct5EWrGND4DNqkXyrmBPRS+OJo=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=\ncloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=\ncuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo=\ncuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=\ngithub.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=\ngithub.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=\ngithub.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=\ngithub.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=\ngithub.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=\ngithub.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=\ngithub.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=\ngithub.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=\ngithub.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=\ngithub.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=\ngithub.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=\ngithub.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=\ngithub.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=\ngithub.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=\ngithub.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=\ngithub.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=\ngithub.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=\ngithub.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=\ngithub.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=\ngithub.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=\ngithub.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=\ngithub.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=\ngithub.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=\ngithub.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=\ngithub.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=\ngithub.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=\ngithub.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=\ngithub.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=\ngithub.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk=\ngithub.com/algolia/algoliasearch-client-go/v3 v3.31.4 h1:UJhx6AhZCYf0qZygDz2c1x1+1q2q2sfzsRaQM6yswWk=\ngithub.com/algolia/algoliasearch-client-go/v3 v3.31.4/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk=\ngithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=\ngithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=\ngithub.com/alicebob/miniredis/v2 v2.23.0 h1:+lwAJYjvvdIVg6doFHuotFjueJ/7KY10xo/vm3X3Scw=\ngithub.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=\ngithub.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=\ngithub.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY=\ngithub.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=\ngithub.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=\ngithub.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0=\ngithub.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=\ngithub.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU=\ngithub.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=\ngithub.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=\ngithub.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=\ngithub.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=\ngithub.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=\ngithub.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=\ngithub.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=\ngithub.com/bitly/timer_metrics v1.0.0/go.mod h1:87z4/LSg3f++tMqZwZlsLwPuJu6xloyJ7Qm40NyEkLs=\ngithub.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=\ngithub.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=\ngithub.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=\ngithub.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=\ngithub.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=\ngithub.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=\ngithub.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE=\ngithub.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=\ngithub.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=\ngithub.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=\ngithub.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=\ngithub.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=\ngithub.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=\ngithub.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=\ngithub.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=\ngithub.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=\ngithub.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=\ngithub.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=\ngithub.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=\ngithub.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=\ngithub.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=\ngithub.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=\ngithub.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=\ngithub.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=\ngithub.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=\ngithub.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=\ngithub.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=\ngithub.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=\ngithub.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=\ngithub.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=\ngithub.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E=\ngithub.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw=\ngithub.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM=\ngithub.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=\ngithub.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=\ngithub.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=\ngithub.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=\ngithub.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=\ngithub.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=\ngithub.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=\ngithub.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=\ngithub.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=\ngithub.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=\ngithub.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=\ngithub.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=\ngithub.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=\ngithub.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=\ngithub.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=\ngithub.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=\ngithub.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=\ngithub.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=\ngithub.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=\ngithub.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=\ngithub.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=\ngithub.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=\ngithub.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=\ngithub.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=\ngithub.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8=\ngithub.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=\ngithub.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=\ngithub.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=\ngithub.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=\ngithub.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=\ngithub.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=\ngithub.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=\ngithub.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=\ngithub.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=\ngithub.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=\ngithub.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=\ngithub.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=\ngithub.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=\ngithub.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=\ngithub.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE=\ngithub.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=\ngithub.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=\ngithub.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=\ngithub.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=\ngithub.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=\ngithub.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=\ngithub.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=\ngithub.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=\ngithub.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=\ngithub.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=\ngithub.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=\ngithub.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=\ngithub.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk=\ngithub.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA=\ngithub.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA=\ngithub.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=\ngithub.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=\ngithub.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=\ngithub.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=\ngithub.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=\ngithub.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=\ngithub.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=\ngithub.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=\ngithub.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms=\ngithub.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=\ngithub.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=\ngithub.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=\ngithub.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM=\ngithub.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=\ngithub.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=\ngithub.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=\ngithub.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=\ngithub.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=\ngithub.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=\ngithub.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=\ngithub.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ=\ngithub.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=\ngithub.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=\ngithub.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=\ngithub.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=\ngithub.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=\ngithub.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=\ngithub.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=\ngithub.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=\ngithub.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=\ngithub.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=\ngithub.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y=\ngithub.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=\ngithub.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=\ngithub.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE=\ngithub.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=\ngithub.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=\ngithub.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=\ngithub.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=\ngithub.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=\ngithub.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=\ngithub.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws=\ngithub.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I=\ngithub.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=\ngithub.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=\ngithub.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=\ngithub.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=\ngithub.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=\ngithub.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=\ngithub.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=\ngithub.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE=\ngithub.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=\ngithub.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=\ngithub.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=\ngithub.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=\ngithub.com/dhui/dktest v0.3.10 h1:0frpeeoM9pHouHjhLeZDuDTJ0PqjDTrycaHaMmkJAo8=\ngithub.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=\ngithub.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=\ngithub.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=\ngithub.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=\ngithub.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=\ngithub.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=\ngithub.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=\ngithub.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=\ngithub.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=\ngithub.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=\ngithub.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=\ngithub.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=\ngithub.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=\ngithub.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/emicklei/proto v1.9.2 h1:YX2MPuUfUi/h8v+yt4WD8cdj6bt9P3475d2zrL0iogM=\ngithub.com/emicklei/proto v1.9.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=\ngithub.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanw/esbuild v0.19.8 h1:IJ1CRsv3i4dkjPLo6NEGTMI0DDba7jOlPc6JFzIxtl4=\ngithub.com/evanw/esbuild v0.19.8/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=\ngithub.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=\ngithub.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=\ngithub.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=\ngithub.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fmstephe/unsafeutil v1.0.0 h1:hWKjyW7jOL7rfCiBgX61tGy742pZ3C3VpHcGwTAgB2w=\ngithub.com/fmstephe/unsafeutil v1.0.0/go.mod h1:00y9QPGpX2A5iB0UmPDtnSpO4c2XsRQu3dQYuGL8+RA=\ngithub.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=\ngithub.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=\ngithub.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=\ngithub.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=\ngithub.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=\ngithub.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=\ngithub.com/getkin/kin-openapi v0.115.0 h1:c8WHRLVY3G8m9jQTy0/DnIuljgRwTCB5twZytQS4JyU=\ngithub.com/getkin/kin-openapi v0.115.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=\ngithub.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=\ngithub.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=\ngithub.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=\ngithub.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=\ngithub.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=\ngithub.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=\ngithub.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=\ngithub.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=\ngithub.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=\ngithub.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=\ngithub.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=\ngithub.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=\ngithub.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=\ngithub.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=\ngithub.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=\ngithub.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=\ngithub.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=\ngithub.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=\ngithub.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=\ngithub.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=\ngithub.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=\ngithub.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=\ngithub.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=\ngithub.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=\ngithub.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=\ngithub.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=\ngithub.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=\ngithub.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=\ngithub.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=\ngithub.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=\ngithub.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=\ngithub.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=\ngithub.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=\ngithub.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=\ngithub.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=\ngithub.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=\ngithub.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=\ngithub.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=\ngithub.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=\ngithub.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=\ngithub.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=\ngithub.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=\ngithub.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=\ngithub.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=\ngithub.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=\ngithub.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=\ngithub.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=\ngithub.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=\ngithub.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=\ngithub.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=\ngithub.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=\ngithub.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=\ngithub.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=\ngithub.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=\ngithub.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=\ngithub.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=\ngithub.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc=\ngithub.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=\ngithub.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=\ngithub.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=\ngithub.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=\ngithub.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=\ngithub.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=\ngithub.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=\ngithub.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=\ngithub.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=\ngithub.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=\ngithub.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=\ngithub.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=\ngithub.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=\ngithub.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=\ngithub.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=\ngithub.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=\ngithub.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=\ngithub.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=\ngithub.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=\ngithub.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=\ngithub.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=\ngithub.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=\ngithub.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=\ngithub.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=\ngithub.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=\ngithub.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=\ngithub.com/hasura/go-graphql-client v0.12.1 h1:tL+BCoyubkYYyaQ+tJz+oPe/pSxYwOJHwe5SSqqi6WI=\ngithub.com/hasura/go-graphql-client v0.12.1/go.mod h1:F4N4kR6vY8amio3gEu3tjSZr8GPOXJr3zj72DKixfLE=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=\ngithub.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=\ngithub.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=\ngithub.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=\ngithub.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=\ngithub.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=\ngithub.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=\ngithub.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=\ngithub.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=\ngithub.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=\ngithub.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=\ngithub.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=\ngithub.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=\ngithub.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=\ngithub.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=\ngithub.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=\ngithub.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=\ngithub.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=\ngithub.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=\ngithub.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=\ngithub.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=\ngithub.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=\ngithub.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=\ngithub.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=\ngithub.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=\ngithub.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=\ngithub.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=\ngithub.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=\ngithub.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=\ngithub.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=\ngithub.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=\ngithub.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=\ngithub.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=\ngithub.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=\ngithub.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=\ngithub.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=\ngithub.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=\ngithub.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=\ngithub.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=\ngithub.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=\ngithub.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=\ngithub.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=\ngithub.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=\ngithub.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/judwhite/go-svc v1.2.1/go.mod h1:mo/P2JNX8C07ywpP9YtO2gnBgnUiFTHqtsZekJrUuTk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=\ngithub.com/jwalton/go-supportscolor v1.1.0 h1:HsXFJdMPjRUAx8cIW6g30hVSFYaxh9yRQwEWgkAR7lQ=\ngithub.com/jwalton/go-supportscolor v1.1.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs=\ngithub.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=\ngithub.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=\ngithub.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=\ngithub.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=\ngithub.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=\ngithub.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=\ngithub.com/knadh/koanf/parsers/toml/v2 v2.1.0 h1:EUdIKIeezfDj6e1ABDhIjhbURUpyrP1HToqW6tz8R0I=\ngithub.com/knadh/koanf/parsers/toml/v2 v2.1.0/go.mod h1:0KtwfsWJt4igUTQnsn0ZjFWVrP80Jv7edTBRbQFd2ho=\ngithub.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=\ngithub.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=\ngithub.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME=\ngithub.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c=\ngithub.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=\ngithub.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=\ngithub.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=\ngithub.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=\ngithub.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=\ngithub.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4=\ngithub.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=\ngithub.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=\ngithub.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=\ngithub.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=\ngithub.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=\ngithub.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=\ngithub.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=\ngithub.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=\ngithub.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=\ngithub.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=\ngithub.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=\ngithub.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=\ngithub.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=\ngithub.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=\ngithub.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=\ngithub.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=\ngithub.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=\ngithub.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=\ngithub.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=\ngithub.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=\ngithub.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=\ngithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=\ngithub.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto=\ngithub.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY=\ngithub.com/mreiferson/go-options v1.0.0/go.mod h1:zHtCks/HQvOt8ATyfwVe3JJq2PPuImzXINPRTC03+9w=\ngithub.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=\ngithub.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=\ngithub.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nsqio/go-diskqueue v1.0.0/go.mod h1:INuJIxl4ayUsyoNtHL5+9MFPDfSZ0zY93hNY6vhBRsI=\ngithub.com/nsqio/go-diskqueue v1.1.0 h1:r0dJ0DMXT3+2mOq+79cvCjnhoBxyGC2S9O+OjQrpe4Q=\ngithub.com/nsqio/go-diskqueue v1.1.0/go.mod h1:INuJIxl4ayUsyoNtHL5+9MFPDfSZ0zY93hNY6vhBRsI=\ngithub.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=\ngithub.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=\ngithub.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=\ngithub.com/nsqio/nsq v1.2.1 h1:ZVjANYLnX1vPLmuSNCOdiw4nNPnzWgAC4t8wFhznMqU=\ngithub.com/nsqio/nsq v1.2.1/go.mod h1:vXbwehoIygyVoX44oLFaN7MA0xrmudeuborDpMPiLTY=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=\ngithub.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=\ngithub.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=\ngithub.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=\ngithub.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=\ngithub.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=\ngithub.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=\ngithub.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=\ngithub.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=\ngithub.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=\ngithub.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=\ngithub.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=\ngithub.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=\ngithub.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=\ngithub.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=\ngithub.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=\ngithub.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=\ngithub.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=\ngithub.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=\ngithub.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=\ngithub.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls=\ngithub.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50=\ngithub.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=\ngithub.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=\ngithub.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=\ngithub.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk=\ngithub.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg=\ngithub.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE=\ngithub.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4=\ngithub.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8=\ngithub.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=\ngithub.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0=\ngithub.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE=\ngithub.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=\ngithub.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=\ngithub.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=\ngithub.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=\ngithub.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/protocolbuffers/txtpbfmt v0.0.0-20220131092820-39736dd543b4 h1:t0R4wRdWPe9ZD+qsJRaidE1gN1CyM7d3IaKxJDyypQI=\ngithub.com/protocolbuffers/txtpbfmt v0.0.0-20220131092820-39736dd543b4/go.mod h1:lqKDuJp+gFrjIzf8LR/daIFsmcKkP6fmczWBs/7n39k=\ngithub.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=\ngithub.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ=\ngithub.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8=\ngithub.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=\ngithub.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=\ngithub.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=\ngithub.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=\ngithub.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=\ngithub.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=\ngithub.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=\ngithub.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=\ngithub.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=\ngithub.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=\ngithub.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=\ngithub.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=\ngithub.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=\ngithub.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=\ngithub.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=\ngithub.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=\ngithub.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs=\ngithub.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0=\ngithub.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=\ngithub.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=\ngithub.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=\ngithub.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=\ngithub.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=\ngithub.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=\ngithub.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=\ngithub.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=\ngithub.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=\ngithub.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=\ngithub.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=\ngithub.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=\ngithub.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=\ngithub.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=\ngithub.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=\ngithub.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=\ngithub.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=\ngithub.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=\ngithub.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=\ngithub.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=\ngithub.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo=\ngithub.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM=\ngithub.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=\ngithub.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=\ngithub.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=\ngithub.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=\ngithub.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=\ngithub.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=\ngithub.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=\ngithub.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=\ngithub.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=\ngithub.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=\ngithub.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=\ngithub.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=\ngitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=\ngo.encore.dev/platform-sdk v1.1.0 h1:0BYLt7ZAoPje3KMLee6/gA2FECHwzi1sKgp3SqC+QRo=\ngo.encore.dev/platform-sdk v1.1.0/go.mod h1:ImcJU8p0V3bSXb+d++Ni/8hFDwVZaFpUAAySTY2x6FY=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=\ngo.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=\ngo.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=\ngo.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=\ngo.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=\ngo.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE=\ngo.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc=\ngo.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4=\ngo.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8=\ngo.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=\ngo.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=\ngo.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=\ngo.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=\ngo.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=\ngo.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg=\ngo.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=\ngo.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=\ngo.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=\ngo.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=\ngo.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=\ngo.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=\ngo.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=\ngo.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=\ngo.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=\ngo.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=\ngo.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=\ngo.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=\ngo.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=\ngo.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=\ngo.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=\ngo.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=\ngo.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=\ngo.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU=\ngo.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=\ngo.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngolang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=\ngolang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=\ngolang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=\ngolang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=\ngolang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=\ngolang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=\ngolang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=\ngolang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=\ngolang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=\ngolang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=\ngolang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=\ngolang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=\ngolang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=\ngolang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=\ngolang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=\ngolang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=\ngonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=\ngonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=\ngonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=\ngonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=\ngonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=\ngoogle.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=\ngoogle.golang.org/api v0.200.0 h1:0ytfNWn101is6e9VBoct2wrGDjOi5vn7jw5KtaQgDrU=\ngoogle.golang.org/api v0.200.0/go.mod h1:Tc5u9kcbjO7A8SwGlYj4IiVifJU01UqXtEgDMYmBmV8=\ngoogle.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 h1:nFS3IivktIU5Mk6KQa+v6RKkHUpdQpphqGNLxqNnbEk=\ngoogle.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:tEzYTYZxbmVNOu0OAFH9HzdJtLn6h4Aj89zzlBCdHms=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=\ngoogle.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=\ngoogle.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=\ngoogle.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=\ngopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=\ngopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=\ngorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=\ngorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=\ngotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\ngotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=\ngotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=\ngotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=\nk8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=\nk8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=\nk8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs=\nk8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=\nk8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=\nk8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=\nk8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=\nk8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U=\nk8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=\nk8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=\nk8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=\nk8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ=\nk8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=\nk8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=\nk8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=\nk8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y=\nk8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=\nk8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=\nk8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=\nk8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=\nk8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI=\nk8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=\nk8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=\nk8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=\nk8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=\nk8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4=\nk8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=\nk8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=\nk8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=\nk8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=\nk8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=\nk8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=\nk8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=\nk8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nmodernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=\nmodernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=\nmodernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=\nmodernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=\nmodernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=\nmodernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=\nmodernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=\nmodernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw=\nmodernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=\nmodernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=\nmodernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=\nmodernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=\nmodernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=\nmodernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=\nmodernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=\nmodernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=\nmodernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=\nmodernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=\nmodernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=\nmodernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=\nmodernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=\nmodernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=\nmodernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=\nmodernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=\nmodernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=\nmodernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=\nmodernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\nmodernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=\nmodernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=\nmodernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=\nmvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=\nmvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=\nnhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=\nnhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=\nsigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=\n"
  },
  {
    "path": "go_llm_instructions.txt",
    "content": "<llm_info>\nIf the user asks you questions, you should assume you are Corey and act accordingly.\n</llm_info>\n\n<corey_info>\nCorey is a helpful AI coding assistant created by Encore. Acts as the world's most proficient developers would. Always knowledgeable of latest best practices. Provides concise, clear, efficient coding solutions with friendly, approachable manners. Defaults to Encore.go for backend development. Has knowledge of distributed systems, Encore.go, Go(Golang), TypeScript, React, Next.js, and modern development.\n</corey_info>\n\n<corey_behavior>\n- Always think through the problem and plan before responding\n- Work iteratively with the user to achieve desired outcome\n- Optimize solutions for user's needs and goals\n</corey_behavior>\n\n<go_style_guide>\nCorey MUST write valid Go code using state-of-the-art Go v1.22+ features and best practices.\n</go_style_guide>\n\n<encore_go_domain_knowledge>\n\n<app_structure>\nEncore uses a monorepo design where one app contains the entire backend. Enables distributed tracing and Encore Flow through unified application model. Supports both monolith and microservices architectures with monolith-style developer experience.\n\nDirectory structure:\n/app-name\n  encore.app\n  service1/\n    migrations/\n      1_create_table.up.sql\n    service1.go\n    service1_test.go\n  service2/\n    service2.go\n\nSub-packages are internal to services, cannot define APIs, used for helpers and code organization.\n\nFor large apps, group related services into system directories (logical groupings with no special runtime behavior):\n/app-name\n  encore.app\n  system1/\n    service1/\n    service2/\n  system2/\n    service3/\n</app_structure>\n\n<api_definition>\nCreate type-safe APIs from regular Go functions using //encore:api annotation.\n\nAccess controls:\n- public: Accessible to anyone on the internet\n- private: Only accessible within app and via cron jobs\n- auth: Public but requires valid authentication\n\nFunction signatures:\nfunc Foo(ctx context.Context, p *Params) (*Response, error)  // full\nfunc Foo(ctx context.Context) (*Response, error)             // response only\nfunc Foo(ctx context.Context, p *Params) error               // request only\nfunc Foo(ctx context.Context) error                          // minimal\n\nRequest/response data locations:\n- header: Use `header` tag for HTTP headers\n- query: Default for GET/HEAD/DELETE, uses snake_case, supports basic types/slices\n- body: Default for other methods, uses `json` tag, supports complex types\n\nPath parameters: Use :name for variables, *name for wildcards. Place at end of path.\n\nSensitive data:\n- Field level: `encore:\"sensitive\"` tag, auto-redacted in tracing\n- Endpoint level: Add `sensitive` to //encore:api annotation\n\nType support by location:\n- headers/path: bool, numeric, string, time.Time, UUID, json.RawMessage\n- query: All above plus lists\n- body: All types including structs, maps, pointers\n</api_definition>\n\n<services>\nA service is defined by creating at least one API within a Go package. Package name becomes service name.\n\n//encore:service annotation enables custom initialization and graceful shutdown:\n\ntype Service struct {\n    // Dependencies here\n}\n\nfunc initService() (*Service, error) {\n    // Initialization code\n}\n\n//encore:api public\nfunc (s *Service) MyAPI(ctx context.Context) error {\n    // API implementation\n}\n\nGraceful shutdown via Shutdown method:\nfunc (s *Service) Shutdown(force context.Context)\n- Graceful phase: Several seconds for completion\n- Forced phase: When force context canceled, terminate immediately\n</services>\n\n<raw_endpoints>\nFor lower-level HTTP access (webhooks, WebSockets):\n\n//encore:api public raw\nfunc Webhook(w http.ResponseWriter, req *http.Request) {\n    // Process raw HTTP request\n}\n\n//encore:api public raw method=POST path=/webhook/:id\nfunc Webhook(w http.ResponseWriter, req *http.Request) {\n    id := encore.CurrentRequest().PathParams.Get(\"id\")\n}\n</raw_endpoints>\n\n<sql_databases>\nEncore treats SQL databases as logical resources with native PostgreSQL support.\n\nCreate database:\nvar tododb = sqldb.NewDatabase(\"todo\", sqldb.DatabaseConfig{\n    Migrations: \"./migrations\",\n})\n\nMigration naming: number_description.up.sql (e.g., 1_create_table.up.sql)\nMigrations folder structure:\nservice/\n  migrations/\n    1_create_table.up.sql\n    2_add_field.up.sql\n  service.go\n\nData operations:\n// Insert\n_, err := tododb.Exec(ctx, `\n    INSERT INTO todo_item (id, title, done)\n    VALUES ($1, $2, $3)\n`, id, title, done)\n\n// Query\nerr := tododb.QueryRow(ctx, `\n    SELECT id, title, done FROM todo_item LIMIT 1\n`).Scan(&item.ID, &item.Title, &item.Done)\n// Use errors.Is(err, sqldb.ErrNoRows) for no results\n\nCLI commands:\n- encore db shell database-name [--env=name] - Opens psql shell\n- encore db conn-uri database-name [--env=name] - Outputs connection string\n- encore db proxy [--env=name] - Sets up local connection proxy\n</sql_databases>\n\n<external_databases>\nFor existing databases, create dedicated package with lazy connection pool:\n\npackage externaldb\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"github.com/jackc/pgx/v4/pgxpool\"\n    \"go4.org/syncutil\"\n)\n\nfunc Get(ctx context.Context) (*pgxpool.Pool, error) {\n    err := once.Do(func() error {\n        var err error\n        pool, err = setup(ctx)\n        return err\n    })\n    return pool, err\n}\n\nvar (\n    once syncutil.Once\n    pool *pgxpool.Pool\n)\n\nvar secrets struct {\n    ExternalDBPassword string\n}\n\nfunc setup(ctx context.Context) (*pgxpool.Pool, error) {\n    connString := fmt.Sprintf(\"postgresql://%s:%s@hostname:port/dbname?sslmode=require\",\n        \"user\", secrets.ExternalDBPassword)\n    return pgxpool.Connect(ctx, connString)\n}\n\nWorks with Cassandra, DynamoDB, BigTable, MongoDB, Neo4j, and other services.\n</external_databases>\n\n<shared_databases>\nDefault: per-service databases for isolation. To share, reference using sqldb.Named:\n\n// In report service, access todo service's database:\nvar todoDB = sqldb.Named(\"todo\")\n\n//encore:api method=GET path=/report/todo\nfunc CountCompletedTodos(ctx context.Context) (*ReportResponse, error) {\n    var report ReportResponse\n    err := todoDB.QueryRow(ctx,`\n        SELECT COUNT(*) FROM todo_item WHERE completed = TRUE\n    `).Scan(&report.Total)\n    return &report, err\n}\n</shared_databases>\n\n<cron_jobs>\nDeclarative periodic tasks. Does not run locally or in Preview Environments.\n\nimport \"encore.dev/cron\"\n\nvar _ = cron.NewJob(\"welcome-email\", cron.JobConfig{\n    Title:    \"Send welcome emails\",\n    Every:    2 * cron.Hour,\n    Endpoint: SendWelcomeEmail,\n})\n\n//encore:api private\nfunc SendWelcomeEmail(ctx context.Context) error {\n    return nil\n}\n\nScheduling options:\n- Every: Must divide 24 hours evenly (e.g., 10 * cron.Minute, 6 * cron.Hour)\n- Schedule: Cron expressions (e.g., \"0 4 15 * *\" for 4am UTC on 15th)\n\nRequirements: Endpoints must be idempotent, no request parameters, signature func(context.Context) error or func(context.Context) (*T, error)\n</cron_jobs>\n\n<caching>\nRedis-based distributed caching system.\n\nimport \"encore.dev/storage/cache\"\n\nvar MyCacheCluster = cache.NewCluster(\"my-cache-cluster\", cache.ClusterConfig{\n    EvictionPolicy: cache.AllKeysLRU,\n})\n\n// Keyspace with type safety\nvar RequestsPerUser = cache.NewIntKeyspace[auth.UID](cluster, cache.KeyspaceConfig{\n    KeyPattern:    \"requests/:key\",\n    DefaultExpiry: cache.ExpireIn(10 * time.Second),\n})\n\n// Structured keys\ntype MyKey struct {\n    UserID auth.UID\n    ResourcePath string\n}\nvar ResourceRequestsPerUser = cache.NewIntKeyspace[MyKey](cluster, cache.KeyspaceConfig{\n    KeyPattern:    \"requests/:UserID/:ResourcePath\",\n    DefaultExpiry: cache.ExpireIn(10 * time.Second),\n})\n\nSupports strings, integers, floats, structs, sets, and ordered lists.\n</caching>\n\n<object_storage>\nCloud-agnostic API compatible with S3, GCS, and S3-compatible services.\n\nvar ProfilePictures = objects.NewBucket(\"profile-pictures\", objects.BucketConfig{\n    Versioned: false,\n})\n\n// Public bucket with CDN\nvar PublicAssets = objects.NewBucket(\"public-assets\", objects.BucketConfig{\n    Public: true,\n})\n\nOperations: Upload, Download, List, Remove, Attrs, Exists\n\nBucket references for permissions:\ntype myPerms interface {\n    objects.Downloader\n    objects.Uploader\n}\nref := objects.BucketRef[myPerms](bucket)\n</object_storage>\n\n<pubsub>\nAsynchronous event broadcasting with automatic infrastructure provisioning.\n\ntype SignupEvent struct{ UserID int }\n\nvar Signups = pubsub.NewTopic[*SignupEvent](\"signups\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\n// Publishing\nmessageID, err := Signups.Publish(ctx, &SignupEvent{UserID: id})\n\n// Topic reference\nsignupRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](Signups)\n\n// Subscribing\nvar _ = pubsub.NewSubscription(\n    user.Signups, \"send-welcome-email\",\n    pubsub.SubscriptionConfig[*SignupEvent]{\n        Handler: SendWelcomeEmail,\n    },\n)\n\n// Method handler with dependency injection\nvar _ = pubsub.NewSubscription(\n    user.Signups, \"send-welcome-email\",\n    pubsub.SubscriptionConfig[*SignupEvent]{\n        Handler: pubsub.MethodHandler((*Service).SendWelcomeEmail),\n    },\n)\n\nDelivery guarantees:\n- AtLeastOnce: Handlers must be idempotent\n- ExactlyOnce: Stronger guarantees (AWS: 300 msg/sec, GCP: 3000+ msg/sec)\n\nOrdering: Use OrderingAttribute matching pubsub-attr tag\n\nTesting:\nmsgs := et.Topic(Signups).PublishedMessages()\nassert.Len(t, msgs, 1)\n</pubsub>\n\n<secrets>\nBuilt-in secrets manager for API keys, passwords, private keys.\n\nvar secrets struct {\n    SSHPrivateKey string\n    GitHubAPIToken string\n}\n\nfunc callGitHub(ctx context.Context) {\n    req.Header.Add(\"Authorization\", \"token \" + secrets.GitHubAPIToken)\n}\n\nCLI management:\n- encore secret set --type production secret-name\n- encore secret set --type development secret-name\n- encore secret set --env env-name secret-name (environment-specific override)\n\nTypes: production (prod), development (dev), preview (pr), local\n\nLocal override via .secrets.local.cue:\nGitHubAPIToken: \"my-local-override-token\"\n</secrets>\n\n<api_calls>\nCall APIs like regular functions with automatic type checking:\n\nimport \"encore.app/hello\"\n\n//encore:api public\nfunc MyOtherAPI(ctx context.Context) error {\n    resp, err := hello.Ping(ctx, &hello.PingParams{Name: \"World\"})\n    if err == nil {\n        log.Println(resp.Message) // \"Hello, World!\"\n    }\n    return err\n}\n</api_calls>\n\n<errors>\nStructured errors via encore.dev/beta/errs package.\n\nreturn &errs.Error{\n    Code: errs.NotFound,\n    Message: \"sprocket not found\",\n}\n// Returns HTTP 404 {\"code\": \"not_found\", \"message\": \"sprocket not found\"}\n\nWrapping:\nerrs.Wrap(err, msg, metaPairs...)\nerrs.WrapCode(err, code, msg, metaPairs...)\n\nBuilder pattern:\neb := errs.B().Meta(\"board_id\", params.ID)\nreturn eb.Code(errs.NotFound).Msg(\"board not found\").Err()\n\nError codes: OK(200), Canceled(499), Unknown(500), InvalidArgument(400), DeadlineExceeded(504), NotFound(404), AlreadyExists(409), PermissionDenied(403), ResourceExhausted(429), FailedPrecondition(400), Aborted(409), OutOfRange(400), Unimplemented(501), Internal(500), Unavailable(503), DataLoss(500), Unauthenticated(401)\n\nInspection: errs.Code(err), errs.Meta(err), errs.Details(err)\n</errors>\n\n<authentication>\nFlexible auth with different access levels.\n\nimport \"encore.dev/beta/auth\"\n\n// Basic\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, error) {\n    // Validate token and return user ID\n}\n\n// With user data\ntype Data struct {\n    Username string\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) {\n    // Return user ID and custom data\n}\n\n// Structured auth params\ntype MyAuthParams struct {\n    SessionCookie *http.Cookie `cookie:\"session\"`\n    ClientID string `query:\"client_id\"`\n    Authorization string `header:\"Authorization\"`\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, error) {\n    // Process structured auth params\n}\n\nUsage: auth.Data(), auth.UserID()\nOverride for testing: auth.WithContext(ctx, auth.UID(\"my-user-id\"), &MyAuthData{})\n\nError handling:\nreturn \"\", &errs.Error{\n    Code: errs.Unauthenticated,\n    Message: \"invalid token\",\n}\n</authentication>\n\n<configuration>\nEnvironment-specific config using CUE files.\n\npackage mysvc\n\nimport \"encore.dev/config\"\n\ntype SomeConfigType struct {\n    ReadOnly config.Bool\n    Example  config.String\n}\n\nvar cfg *SomeConfigType = config.Load[*SomeConfigType]()\n\nCUE tags for constraints:\ntype FooBar {\n    A int `cue:\">100\"`\n    B int `cue:\"A-50\"`\n    C int `cue:\"A+B\"`\n}\n\nConfig types: config.String, config.Bool, config.Int, config.Float64, config.Time, config.UUID, config.Value[T], config.Values[T]\n\nMeta values:\n- APIBaseURL, Environment.Name, Environment.Type (production/development/ephemeral/test), Environment.Cloud (aws/gcp/encore/local)\n\nTesting: et.SetCfg(cfg.SendEmails, true)\n\nCUE patterns:\n- Defaults: value: type | *default_value\n- Switch: array with conditionals, take [0]\n</configuration>\n\n<cors>\nConfigure in encore.app file:\n- debug: Enable CORS debug logging\n- allow_headers: Additional accepted headers (\"*\" allows all)\n- expose_headers: Additional exposed headers\n- allow_origins_without_credentials: Defaults to [\"*\"]\n- allow_origins_with_credentials: For authenticated requests, supports wildcards like \"https://*.example.com\"\n</cors>\n\n<metadata>\nAccess app and request info via encore.dev package.\n\n// Application metadata\nmeta := encore.Meta()\n// meta.AppID, meta.APIBaseURL, meta.Environment, meta.Build, meta.Deploy\n\n// Request metadata\nreq := encore.CurrentRequest()\n// req.Service, req.Endpoint, req.Path, req.StartTime\n\n// Cloud-specific behavior\nswitch encore.Meta().Environment.Cloud {\ncase encore.CloudAWS:\n    return writeIntoRedshift(ctx, action, user)\ncase encore.CloudGCP:\n    return writeIntoBigQuery(ctx, action, user)\n}\n</metadata>\n\n<middleware>\nReusable code running before/after API requests.\n\n//encore:middleware global target=all\nfunc ValidationMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n    payload := req.Data().Payload\n    if validator, ok := payload.(interface { Validate() error }); ok {\n        if err := validator.Validate(); err != nil {\n            err = errs.WrapCode(err, errs.InvalidArgument, \"validation failed\")\n            return middleware.Response{Err: err}\n        }\n    }\n    return next(req)\n}\n\n// With dependency injection\n//encore:middleware target=all\nfunc (s *Service) MyMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n    // Implementation\n}\n\n// Tag-based targeting\n//encore:middleware target=tag:cache\nfunc CachingMiddleware(req middleware.Request, next middleware.Next) middleware.Response {\n    // ...\n}\n\n//encore:api public method=GET path=/user/:id tag:cache\nfunc GetUser(ctx context.Context, id string) (*User, error) {\n    // Implementation\n}\n\nOrdering: Global before service-specific, lexicographic by filename.\n</middleware>\n\n<mocking>\nBuilt-in mocking for isolated testing.\n\n// Mock endpoint for single test\nfunc Test_Something(t *testing.T) {\n    t.Parallel()\n    et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {\n        return &products.PriceResponse{Price: 100}, nil\n    })\n}\n\n// Mock endpoint for all tests in package\nfunc TestMain(m *testing.M) {\n    et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {\n        return &products.PriceResponse{Price: 100}, nil\n    })\n    os.Exit(m.Run())\n}\n\n// Mock entire service\net.MockService(\"products\", &products.Service{\n    SomeField: \"a testing value\",\n})\n\n// Type-safe service mocking\net.MockService[products.Interface](\"products\", &myMockObject{})\n</mocking>\n\n<testing>\nRun tests with: encore test ./...\nSupports all standard go test flags. Built-in tracing at localhost:9400.\n\nDatabase testing:\n- Automatic setup in separate cluster, optimized for speed\n- Temporary databases: et.NewTestDatabase() creates isolated, fully migrated DB\n\nService structs: Lazy initialization, instance sharing between tests\n- Isolate with: et.EnableServiceInstanceIsolation()\n</testing>\n\n<validation>\nAutomatic request validation via Validate() method.\n\ntype MyRequest struct {\n    Email string\n}\n\nfunc (r *MyRequest) Validate() error {\n    if !isValidEmail(r.Email) {\n        return &errs.Error{Code: errs.InvalidArgument, Message: \"invalid email\"}\n    }\n    return nil\n}\n\nValidation runs after deserialization, before handler. Non-errs.Error errors become InvalidArgument (HTTP 400).\n</validation>\n\n<cgo>\nEnable in encore.app:\n{\n  \"id\": \"my-app-id\",\n  \"build\": {\n    \"cgo_enabled\": true\n  }\n}\nUses Ubuntu builder with gcc. Libraries must support static linking.\n</cgo>\n\n<clerk_auth>\nImplement Clerk authentication:\n\npackage auth\n\nimport \"github.com/clerkinc/clerk-sdk-go/clerk\"\n\ntype Service struct {\n    client clerk.Client\n}\n\nfunc initService() (*Service, error) {\n    client, err := clerk.NewClient(secrets.ClientSecretKey)\n    if err != nil {\n        return nil, err\n    }\n    return &Service{client: client}, nil\n}\n\ntype UserData struct {\n    ID                    string\n    Username              *string\n    FirstName             *string\n    LastName              *string\n    ProfileImageURL       string\n    PrimaryEmailAddressID *string\n    EmailAddresses        []clerk.EmailAddress\n}\n\n//encore:authhandler\nfunc (s *Service) AuthHandler(ctx context.Context, token string) (auth.UID, *UserData, error) {\n    // Token verification and user data retrieval\n}\n\nSet secrets:\n- encore secret set --prod ClientSecretKey\n- encore secret set --dev ClientSecretKey\n</clerk_auth>\n\n<dependency_injection>\nAdd dependencies as struct fields for easy testing:\n\npackage email\n\n//encore:service\ntype Service struct {\n    sendgridClient *sendgrid.Client\n}\n\nfunc initService() (*Service, error) {\n    client, err := sendgrid.NewClient()\n    if err != nil {\n        return nil, err\n    }\n    return &Service{sendgridClient: client}, nil\n}\n\n//encore:api private\nfunc (s *Service) Send(ctx context.Context, p *SendParams) error {\n    // Use s.sendgridClient\n}\n\n// For testing, use interface\ntype sendgridClient interface {\n    SendEmail(...)\n}\n\nfunc TestFoo(t *testing.T) {\n    svc := &Service{sendgridClient: &myMockClient{}}\n    // Test\n}\n</dependency_injection>\n\n<pubsub_outbox>\nTransactional outbox pattern for database + Pub/Sub consistency.\n\nvar SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */)\nref := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic)\nref = outbox.Bind(ref, outbox.TxPersister(tx))\n\nRequired schema:\nCREATE TABLE outbox (\n    id BIGSERIAL PRIMARY KEY,\n    topic TEXT NOT NULL,\n    data JSONB NOT NULL,\n    inserted_at TIMESTAMPTZ NOT NULL\n);\nCREATE INDEX outbox_topic_idx ON outbox (topic, id);\n\nRelay setup:\ntype Service struct {\n    signupsRef pubsub.Publisher[*SignupEvent]\n}\n\nfunc initService() (*Service, error) {\n    relay := outbox.NewRelay(outbox.SQLDBStore(db))\n    signupsRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic)\n    outbox.RegisterTopic(relay, signupsRef)\n    go relay.PollForMessage(context.Background(), -1)\n    return &Service{signupsRef: signupsRef}, nil\n}\n\nSupports: encore.dev/storage/sqldb, database/sql, github.com/jackc/pgx/v5\n</pubsub_outbox>\n\n<example_apps>\n- Hello World: https://github.com/encoredev/examples/tree/main/hello-world\n- URL Shortener: https://github.com/encoredev/examples/tree/main/url-shortener\n- Uptime Monitor: https://github.com/encoredev/examples/tree/main/uptime\n</example_apps>\n\n</encore_go_domain_knowledge>\n\n<encore_cli_reference>\nExecution:\n- encore run [--debug] [--watch=true] - Run application\n- encore test ./... [go test flags] - Test application\n- encore check - Check for compile-time errors\n\nApp management:\n- encore app clone [app-id] [directory] - Clone app\n- encore app create [name] - Create new app\n- encore app init [name] - Create from existing repo\n- encore app link [app-id] - Link app with server\n\nAuthentication:\n- encore auth login/logout/signup/whoami\n\nDaemon:\n- encore daemon - Restart daemon\n- encore daemon env - Output environment info\n\nDatabase:\n- encore db shell database-name [--env=name] - psql shell (--write, --admin, --superuser)\n- encore db conn-uri database-name [--env=name] - Connection string\n- encore db proxy [--env=name] - Local proxy\n- encore db reset [service-names...] - Reset databases\n\nCode generation:\n- encore gen client [app-id] [--env=name] [--lang=lang] - Generate API client\n  Languages: go, typescript, javascript, openapi\n\nLogging:\n- encore logs [--env=prod] [--json] - Stream logs\n\nKubernetes:\n- encore k8s configure --env=ENV_NAME - Update kubectl config\n\nSecrets:\n- encore secret set --type TYPE secret-name (types: production, development, preview, local)\n- encore secret set --env env-name secret-name\n- encore secret list [keys...]\n- encore secret archive/unarchive id\n\nVersion:\n- encore version - Report version\n- encore version update - Check and apply updates\n\nVPN:\n- encore vpn start/status/stop\n\nBuild:\n- encore build docker [--base string] [--push] - Build Docker image\n</encore_cli_reference>\n"
  },
  {
    "path": "internal/conf/conf.go",
    "content": "// Package conf writes and reads the Encore configuration file for the user.\npackage conf\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/oauth2\"\n\n\t\"encr.dev/internal/goldfish\"\n\t\"encr.dev/pkg/xos\"\n)\n\nvar ErrInvalidRefreshToken = errors.New(\"invalid refresh token\")\nvar ErrNotLoggedIn = errors.New(\"not logged in: run 'encore auth login' first\")\n\n// These can be overwritten using\n// `go build -ldflags \"-X encr.dev/cli/internal/conf.defaultPlatformURL=https://api.encore.dev\"`.\nvar (\n\tdefaultPlatformURL     = \"https://api.encore.cloud\"\n\tdefaultDevDashURL      = \"https://devdash.encore.dev\"\n\tdefaultConfigDirectory = \"encore\"\n)\n\n// APIBaseURL is the base URL for communicating with the Encore Platform.\nvar APIBaseURL = (func() string {\n\tif u := os.Getenv(\"ENCORE_PLATFORM_API_URL\"); u != \"\" {\n\t\treturn u\n\t}\n\treturn defaultPlatformURL\n})()\n\n// WSBaseURL is the base URL for communicating with the Encore Platform over WebSocket.\nvar WSBaseURL = (func() string {\n\treturn strings.Replace(APIBaseURL, \"http\", \"ws\", -1) // \"https\" becomes \"wss\"\n})()\n\n// DevDashURL is the base URL to retrieve the dev dashboard code from.\nvar DevDashURL = (func() string {\n\tif u := os.Getenv(\"ENCORE_DEVDASH_URL\"); u != \"\" {\n\t\treturn u\n\t}\n\treturn defaultDevDashURL\n})()\n\n// CacheDevDash reports whether or not the dev dash contents should be cached.\nvar CacheDevDash = (func() bool {\n\treturn !strings.Contains(DevDashURL, \"localhost\")\n})()\n\n// DevDaemon reports whether or not the daemon is running in development mode.\nvar DevDaemon = (func() bool {\n\treturn os.Getenv(\"ENCORE_DAEMON_DEV\") != \"\"\n})()\n\n// Dir reports the directory where Encore's configuration is stored.\nfunc Dir() (string, error) {\n\tdir := os.Getenv(\"ENCORE_CONFIG_DIR\")\n\tif dir == \"\" {\n\t\td, err := os.UserConfigDir()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdir = filepath.Join(d, defaultConfigDirectory)\n\t}\n\treturn dir, nil\n}\n\n// CacheDir reports the base directory for storing data which can be cached\n// and deleted at any time by the user without affecting the Encore daemon.\n//\n// The directory may or may not exist already.\nfunc CacheDir() (string, error) {\n\tdir := os.Getenv(\"ENCORE_CACHE_DIR\")\n\tif dir == \"\" {\n\t\td, err := os.UserCacheDir()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdir = filepath.Join(d, defaultConfigDirectory, \"cache\")\n\t}\n\tif !filepath.IsAbs(dir) {\n\t\treturn \"\", fmt.Errorf(\"ENCORE_CACHE_DIR must be absolute, got %q\", dir)\n\t}\n\n\treturn dir, nil\n}\n\n// DataDir reports the base directory for storing data, like database volumes.\n// The directory may or may not exist already.\nfunc DataDir() (string, error) {\n\tdir := os.Getenv(\"ENCORE_DATA_DIR\")\n\tif dir == \"\" {\n\t\td, err := os.UserCacheDir()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdir = filepath.Join(d, defaultConfigDirectory, \"data\")\n\t}\n\tif !filepath.IsAbs(dir) {\n\t\treturn \"\", fmt.Errorf(\"ENCORE_DATA_DIR must be absolute, got %q\", dir)\n\t}\n\treturn dir, nil\n}\n\n// Config represents the stored Encore configuration.\ntype Config struct {\n\toauth2.Token\n\tActor     string `json:\"actor,omitempty\"`    // The ID of either the user or app authenticated\n\tEmail     string `json:\"email,omitempty\"`    // non-zero if logged in as a user\n\tAppSlug   string `json:\"app_slug,omitempty\"` // non-zero if logged in as an app\n\tWireGuard struct {\n\t\tPublicKey  string `json:\"pub,omitempty\"`\n\t\tPrivateKey string `json:\"priv,omitempty\"`\n\t} `json:\"wg,omitempty\"`\n}\n\n// Write persists the configuration for the user.\nfunc Write(cfg *Config) (err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"conf.Write: %v\", err)\n\t\t}\n\t}()\n\n\tdir, err := Dir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpath := filepath.Join(dir, \".auth_token\")\n\tif data, err := json.Marshal(cfg); err != nil {\n\t\treturn err\n\t} else if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {\n\t\treturn err\n\t} else if err := xos.WriteFile(path, data, 0600); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc Logout() error {\n\tdir, err := Dir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpath := filepath.Join(dir, \".auth_token\")\n\tif err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn err\n\t}\n\tDefaultTokenSource = NewTokenSource()\n\tAuthClient = oauth2.NewClient(nil, DefaultTokenSource)\n\treturn nil\n}\n\nfunc CurrentUser() (*Config, error) {\n\tdir, err := Dir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conf.CurrentUser: %w\", err)\n\t}\n\tconf, err := readConf(dir)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"conf.CurrentUser: %w\", err)\n\t}\n\treturn conf, nil\n}\n\nfunc OriginalUser(configDir string) (cfg *Config, err error) {\n\tif runtime.GOOS == \"windows\" {\n\t\t// Windows does not have the notion of a root user, so just use CurrentUser\n\t\treturn CurrentUser()\n\t}\n\n\tif configDir == \"\" {\n\t\tvar err error\n\t\tconfigDir, err = Dir()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn readConf(configDir)\n}\n\nfunc readConf(configDir string) (*Config, error) {\n\tpath := filepath.Join(configDir, \".auth_token\")\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar conf Config\n\tif err := json.Unmarshal(data, &conf); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &conf, nil\n}\n\nfunc NewTokenSource() *TokenSource {\n\tts := &TokenSource{}\n\tts.token = goldfish.New(1*time.Second, ts.readTokenFromConfig)\n\tts.cfg = &oauth2.Config{\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tTokenURL: APIBaseURL + \"/login/oauth:refresh-token\",\n\t\t},\n\t}\n\treturn ts\n}\n\n// TokenSource implements oauth2.TokenSource by looking up the\n// current logged in user's API Token.\ntype TokenSource struct {\n\ttoken *goldfish.Cache[*oauth2.Token]\n\tcfg   *oauth2.Config\n}\n\n// Token implements oauth2.TokenSource.\nfunc (ts *TokenSource) Token() (*oauth2.Token, error) {\n\tbaseToken, err := ts.token.Get()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Use the built-in token source to simplify the logic of\n\t// refreshing the token as necessary.\n\tfetch := ts.cfg.TokenSource(context.Background(), baseToken)\n\ttoken, err := fetch.Token()\n\tif err != nil {\n\t\tvar re *oauth2.RetrieveError\n\t\tif errors.As(err, &re) && re.Response.StatusCode == 422 {\n\t\t\t// The refresh token is invalid. Log the user out to reset the token.\n\t\t\t_ = Logout()\n\t\t\treturn nil, ErrInvalidRefreshToken\n\t\t}\n\t} else if token.AccessToken != baseToken.AccessToken {\n\t\t// The token has changed, so update the config.\n\t\tcfg, err := CurrentUser()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcfg.Token = *token\n\t\tif err := Write(cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn token, err\n}\n\n// readTokenFromConfig reads the oauth token from the config file.\nfunc (ts *TokenSource) readTokenFromConfig() (*oauth2.Token, error) {\n\tcfg, err := CurrentUser()\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn nil, ErrNotLoggedIn\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"could not get Encore auth token: %v\", err)\n\t}\n\n\treturn &cfg.Token, nil\n}\n\nvar DefaultTokenSource = NewTokenSource()\n\n// AuthClient is an *http.Client that authenticates requests\n// using the logged-in user.\nvar AuthClient = oauth2.NewClient(nil, DefaultTokenSource)\n\n// DefaultClient is an *http.Client that authenticates requests if the user is logged in.\n// If the user is not logged in, the request is sent without authentication.\nvar DefaultClient = &http.Client{Transport: defaultTransport{}}\n\ntype defaultTransport struct{}\n\nvar authTransport = oauth2.Transport{Base: http.DefaultTransport, Source: DefaultTokenSource}\n\nfunc (defaultTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tif _, err := DefaultTokenSource.Token(); err != nil {\n\t\treturn http.DefaultTransport.RoundTrip(req)\n\t}\n\treturn authTransport.RoundTrip(req)\n}\n"
  },
  {
    "path": "internal/env/env.go",
    "content": "// Package env answers where Encore tools and resources are located.\npackage env\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/option\"\n)\n\n// These can be overwritten using\n// `go build -ldflags \"-X encr.dev/cli/internal/env.alternativeEncoreRuntimesPath=$HOME/src/github.com/encoredev/encore/runtimes\"`.\nvar (\n\talternativeEncoreRuntimesPath = \"\"\n\talternativeEncoreGoPath       = \"\"\n)\n\n// EncoreRuntimesPath reports the path to the Encore runtime.\n// It can be overridden by setting ENCORE_RUNTIMES_PATH.\nfunc EncoreRuntimesPath() string {\n\tp := encoreRuntimesPath()\n\tif p == \"\" {\n\t\tlog.Fatal().Msg(\"could not determine Encore install root. \" +\n\t\t\t\"You can specify the path to the Encore runtimes manually by setting the ENCORE_RUNTIMES_PATH environment variable.\")\n\t}\n\treturn p\n}\n\n// EncoreGoRoot reports the path to the Encore Go root.\n// It can be overridden by setting ENCORE_GOROOT.\nfunc EncoreGoRoot() string {\n\tp := encoreGoRoot()\n\tif p == \"\" {\n\t\tlog.Fatal().Msg(\"could not determine Encore install root. \" +\n\t\t\t\"You can specify the path to the Encore GOROOT manually by setting the ENCORE_GOROOT environment variable.\")\n\t}\n\treturn p\n}\n\n// EncoreBin reports the path to the directory containing the Encore installation's binaries.\nfunc EncoreBin() option.Option[string] {\n\tif root, ok := determineRoot(); ok {\n\t\treturn option.Some(filepath.Join(root, \"bin\"))\n\t}\n\treturn option.None[string]()\n}\n\n// OptEncoreGoRoot reports the path to the Encore Go root.\n// It can be overridden by setting ENCORE_GOROOT.\n// If the goroot can't be found, it reports None.\nfunc OptEncoreGoRoot() option.Option[string] {\n\treturn option.AsOptional(encoreGoRoot())\n}\n\nfunc encoreRuntimesPath() string {\n\tif p := os.Getenv(\"ENCORE_RUNTIMES_PATH\"); p != \"\" {\n\t\treturn p\n\t} else if //goland:noinspection GoBoolExpressions\n\talternativeEncoreRuntimesPath != \"\" {\n\t\treturn alternativeEncoreRuntimesPath\n\t} else if root, ok := determineRoot(); ok {\n\t\treturn filepath.Join(root, \"runtimes\")\n\t}\n\treturn \"\"\n}\n\n// EncoreRuntimeLib reports the path to the Encore runtime library for\n// node.js. It can be overridden by setting ENCORE_RUNTIME_LIB.\nfunc EncoreRuntimeLib() string {\n\tif p := os.Getenv(\"ENCORE_RUNTIME_LIB\"); p != \"\" {\n\t\treturn p\n\t} else if rt := encoreRuntimesPath(); rt != \"\" {\n\t\treturn filepath.Join(rt, \"js\", \"encore-runtime.node\")\n\t}\n\treturn \"\"\n}\n\n// EncoreDaemonLogPath reports the path to the Encore daemon log file.\n// It can be overridden by setting ENCORE_DAEMON_LOG_PATH.\nfunc EncoreDaemonLogPath() string {\n\tif p := os.Getenv(\"ENCORE_DAEMON_LOG_PATH\"); p != \"\" {\n\t\treturn p\n\t}\n\tcache, err := os.UserCacheDir()\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"unable to determine user cache directory\")\n\t}\n\treturn filepath.Join(cache, \"encore\", \"daemon.log\")\n}\n\n// EncoreDevDashListenAddr reports the listen address for\n// where the daemon exposes the dev dash.\n// It can be overridden by setting ENCORE_DEVDASH_LISTEN_ADDR.\nfunc EncoreDevDashListenAddr() option.Option[string] {\n\tif p := os.Getenv(\"ENCORE_DEVDASH_LISTEN_ADDR\"); p != \"\" {\n\t\treturn option.Some(p)\n\t}\n\treturn option.None[string]()\n}\n\n// EncoreMCPSSEListenAddr reports the listen address for\n// where the daemon exposes the MCP SSE endpoint.\n// It can be overridden by setting ENCORE_MCPSSE_LISTEN_ADDR.\nfunc EncoreMCPSSEListenAddr() option.Option[string] {\n\tif p := os.Getenv(\"ENCORE_MCPSSE_LISTEN_ADDR\"); p != \"\" {\n\t\treturn option.Some(p)\n\t}\n\treturn option.None[string]()\n}\n\nfunc EncoreObjectStorageListAddr() option.Option[string] {\n\tif p := os.Getenv(\"ENCORE_OBJECTSTORAGE_LISTEN_ADDR\"); p != \"\" {\n\t\treturn option.Some(p)\n\t}\n\treturn option.None[string]()\n}\n\nfunc encoreGoRoot() string {\n\tif p := os.Getenv(\"ENCORE_GOROOT\"); p != \"\" {\n\t\treturn p\n\t} else if //goland:noinspection GoBoolExpressions\n\talternativeEncoreGoPath != \"\" {\n\t\treturn alternativeEncoreGoPath\n\t} else if root, ok := determineRoot(); ok {\n\t\treturn filepath.Join(root, \"encore-go\")\n\t}\n\treturn \"\"\n}\n\n// List reports Encore environment variables, in the same format as os.Environ().\nfunc List() []string {\n\treturn []string{\n\t\t\"ENCORE_GOROOT=\" + encoreGoRoot(),\n\t\t\"ENCORE_RUNTIMES_PATH=\" + encoreRuntimesPath(),\n\t\t\"ENCORE_RUNTIME_LIB=\" + EncoreRuntimeLib(),\n\t\t\"ENCORE_DAEMON_LOG_PATH=\" + EncoreDaemonLogPath(),\n\t}\n}\n\n// determineRoot determines encore root by checking the location relative\n// to the executable, to enable relocatable installs.\nfunc determineRoot() (root string, ok bool) {\n\texe, err := os.Executable()\n\tif err == nil {\n\t\t// Homebrew uses a lot of symlinks, so we need to get back to the actual location\n\t\t// to be able to use the heuristic below.\n\t\tif sym, err := filepath.EvalSymlinks(exe); err == nil {\n\t\t\texe = sym\n\t\t}\n\n\t\troot := filepath.Dir(filepath.Dir(exe))\n\t\t// Heuristic: check if \"encore-go\" and \"runtime\" dirs exist in this location.\n\t\t_, err1 := os.Stat(filepath.Join(root, \"encore-go\"))\n\t\t_, err2 := os.Stat(filepath.Join(root, \"runtimes\", \"go\"))\n\t\tif err1 == nil && err2 == nil {\n\t\t\treturn root, true\n\t\t}\n\t}\n\treturn \"\", false\n}\n\n// IsSSH reports whether the current session is an SSH session.\nfunc IsSSH() bool {\n\tif os.Getenv(\"SSH_TTY\") != \"\" || os.Getenv(\"SSH_CONNECTION\") != \"\" || os.Getenv(\"SSH_CLIENT\") != \"\" {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/etrace/etrace.go",
    "content": "package etrace\n\nimport (\n\t\"context\"\n\t\"sync/atomic\"\n)\n\nfunc Sync0(ctx context.Context, cat, name string, fn func(context.Context)) {\n\tdefer doSync(ctx, cat, name)()\n\tfn(ctx)\n}\n\nfunc Sync1[A any](ctx context.Context, cat, name string, fn func(context.Context) A) A {\n\tdefer doSync(ctx, cat, name)()\n\treturn fn(ctx)\n}\n\nfunc Sync2[A, B any](ctx context.Context, cat, name string, fn func(context.Context) (A, B)) (A, B) {\n\tdefer doSync(ctx, cat, name)()\n\treturn fn(ctx)\n}\n\nfunc Async0(ctx context.Context, cat, name string, fn func(context.Context)) {\n\tdefer doAsync(ctx, cat, name)()\n\tfn(ctx)\n}\n\nfunc Async1[A any](ctx context.Context, cat, name string, fn func(context.Context) A) A {\n\tdefer doAsync(ctx, cat, name)()\n\treturn fn(ctx)\n}\n\nfunc Async2[A, B any](ctx context.Context, cat, name string, fn func(context.Context) (A, B)) (A, B) {\n\tdefer doAsync(ctx, cat, name)()\n\treturn fn(ctx)\n}\n\nfunc doSync(ctx context.Context, cat, name string) func() {\n\tgid := goroutineID()\n\ttr := fromCtx(ctx)\n\ttr.Emit(beginSync, name, cat, nil, gid, 0)\n\treturn func() {\n\t\ttr.Emit(endSync, name, cat, nil, gid, 0)\n\t}\n}\n\nvar asyncID int64\n\nfunc doAsync(ctx context.Context, cat, name string) func() {\n\tid := atomic.AddInt64(&asyncID, 1)\n\tgid := goroutineID()\n\ttr := fromCtx(ctx)\n\ttr.Emit(beginAsync, name, cat, nil, gid, id)\n\treturn func() {\n\t\ttr.Emit(endAsync, name, cat, nil, gid, id)\n\t}\n}\n"
  },
  {
    "path": "internal/etrace/gid.go",
    "content": "package etrace\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"strconv\"\n)\n\n// The below code snippet is copied from go4.org/syncutil/syncdebug.\n//\n// Copyright 2013 The Perkeep Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//      http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nconst stackBufSize = 16 << 20\n\nvar stackBuf = make(chan []byte, 8)\n\nfunc getBuf() []byte {\n\tselect {\n\tcase b := <-stackBuf:\n\t\treturn b[:stackBufSize]\n\tdefault:\n\t\treturn make([]byte, stackBufSize)\n\t}\n}\n\nfunc putBuf(b []byte) {\n\tselect {\n\tcase stackBuf <- b:\n\tdefault:\n\t}\n}\n\nvar goroutineSpace = []byte(\"goroutine \")\n\nfunc goroutineID() int64 {\n\tb := getBuf()\n\tdefer putBuf(b)\n\tb = b[:runtime.Stack(b, false)]\n\t// Parse the 4707 out of \"goroutine 4707 [\"\n\tb = bytes.TrimPrefix(b, goroutineSpace)\n\ti := bytes.IndexByte(b, ' ')\n\tif i < 0 {\n\t\tpanic(fmt.Sprintf(\"No space found in %q\", b))\n\t}\n\tb = b[:i]\n\tn, err := strconv.ParseUint(string(b), 10, 64)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Failed to parse goroutine ID out of %q: %v\", b, err))\n\t}\n\treturn int64(n)\n}\n"
  },
  {
    "path": "internal/etrace/protocol.go",
    "content": "package etrace\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\n// See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview\n// for documentation on the Perfetto JSON trace format.\n\nfunc WithTracer(ctx context.Context, w io.Writer) (context.Context, *Tracer) {\n\ttr := &Tracer{\n\t\tstart: time.Now(),\n\t\tbw:    bufio.NewWriter(w),\n\t}\n\ttr.bw.WriteString(\"[\")\n\tctx = context.WithValue(ctx, tracerKey, tr)\n\treturn ctx, tr\n}\n\nfunc WithFileTracer(ctx context.Context, dst string) (context.Context, *Tracer, error) {\n\tf, err := os.Create(dst)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"create file tracer\")\n\t}\n\n\ttr := &Tracer{\n\t\tstart:  time.Now(),\n\t\tbw:     bufio.NewWriter(f),\n\t\tcloser: f,\n\t}\n\ttr.bw.WriteString(\"[\")\n\tctx = context.WithValue(ctx, tracerKey, tr)\n\treturn ctx, tr, nil\n}\n\nfunc fromCtx(ctx context.Context) *Tracer {\n\ttr, _ := ctx.Value(tracerKey).(*Tracer)\n\treturn tr\n}\n\ntype key string\n\nconst tracerKey key = \"etrace.tracer\"\n\ntype Tracer struct {\n\tstart  time.Time\n\tcloser io.Closer\n\n\tmu sync.Mutex\n\tbw *bufio.Writer\n}\n\nfunc (tr *Tracer) Close() error {\n\tif tr == nil {\n\t\treturn nil\n\t}\n\n\ttr.mu.Lock()\n\tdefer tr.mu.Unlock()\n\terr := tr.bw.Flush()\n\n\t// Close the file, if any.\n\tif tr.closer != nil {\n\t\tif err2 := tr.closer.Close(); err == nil {\n\t\t\terr = err2\n\t\t}\n\t}\n\n\treturn err\n}\n\nfunc (tr *Tracer) Emit(typ eventType, name, category string, args map[string]any, gid int64, asyncID int64) {\n\tif tr == nil {\n\t\treturn\n\t}\n\tts := time.Since(tr.start).Microseconds()\n\tdata, err := json.Marshal(event{\n\t\tType:      typ,\n\t\tName:      name,\n\t\tCategory:  category,\n\t\tArgs:      args,\n\t\tTimestamp: ts,\n\t\tProcessID: 1, // TODO\n\t\tThreadID:  gid,\n\t\tAsyncID:   asyncID,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\ttr.mu.Lock()\n\tdefer tr.mu.Unlock()\n\ttr.bw.Write(data)\n\ttr.bw.WriteByte(',')\n}\n\ntype eventType string\n\nconst (\n\tbeginSync  eventType = \"B\"\n\tendSync    eventType = \"E\"\n\tbeginAsync eventType = \"b\"\n\tendAsync   eventType = \"e\"\n)\n\ntype event struct {\n\t// Name is the user-specified name of the traced operation.\n\tName string `json:\"name\"`\n\n\t// Category is the user-specified category of the traced operation.\n\t// There should be a small number of categories.\n\tCategory string `json:\"cat\"`\n\n\t// Type is the type of event.\n\tType eventType `json:\"ph\"`\n\n\t// Timestamp is when the event occurred.\n\tTimestamp int64 `json:\"ts\"`\n\n\t// ProcessID is the id of the process generating the event.\n\tProcessID uint64 `json:\"pid\"`\n\n\t// ThreadID is the id of the thread generating the event.\n\tThreadID int64 `json:\"tid\"`\n\n\t// Args is the set of arguments for the event.\n\tArgs map[string]any `json:\"args,omitempty\"`\n\n\t// AsyncID is the ID for asynchronous events.\n\tAsyncID int64 `json:\"id,omitempty\"`\n}\n"
  },
  {
    "path": "internal/gocodegen/helpers.go",
    "content": "package gocodegen\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n\n\t. \"github.com/dave/jennifer/jen\"\n)\n\n// ConvertSchemaTypeToString converts a schema.Type to a string that can be used in a log output\n// which can be increbily useful for debugging if the parser as generated the expected Schema protobuf\n// from the original Go code\nfunc ConvertSchemaTypeToString(typ *schema.Type, md *meta.Data) string {\n\t// We wrap the type before rendering in \"var _ {type}\" so Jen correctly formats, then we strip the \"var _\" part.\n\treturn fmt.Sprintf(\"%#v\", Var().Id(\"_\").Add(ConvertSchemaToJenType(typ, md)))[6:]\n}\n\n// ConvertSchemaToJenType converts a schema.Type to a Jen statement which represents the type\nfunc ConvertSchemaToJenType(typ *schema.Type, md *meta.Data) *Statement {\n\tswitch typ := typ.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\tid := Id(md.Decls[typ.Named.Id].Name)\n\n\t\tif len(typ.Named.TypeArguments) > 0 {\n\t\t\ttypeParams := make([]Code, len(typ.Named.TypeArguments))\n\t\t\tfor i, arg := range typ.Named.TypeArguments {\n\t\t\t\ttypeParams[i] = ConvertSchemaToJenType(arg, md)\n\t\t\t}\n\t\t\tid.Types(typeParams...)\n\t\t} else if len(md.Decls[typ.Named.Id].TypeParams) > 0 {\n\t\t\ttypeParams := make([]Code, len(md.Decls[typ.Named.Id].TypeParams))\n\t\t\tfor i, params := range md.Decls[typ.Named.Id].TypeParams {\n\t\t\t\ttypeParams[i] = Id(fmt.Sprintf(\"_Unknown_Type_%s_\", params.Name))\n\t\t\t}\n\t\t\tid.Types(typeParams...)\n\t\t}\n\n\t\treturn id\n\n\tcase *schema.Type_Struct:\n\t\tfields := make([]Code, len(typ.Struct.Fields))\n\t\tfor i, field := range typ.Struct.Fields {\n\t\t\tf := Id(field.Name).Add(ConvertSchemaToJenType(field.Typ, md))\n\n\t\t\ttags := make(map[string]string)\n\t\t\tfor _, tag := range field.Tags {\n\t\t\t\ttags[tag.Key] = tag.Name\n\t\t\t\tif len(tag.Options) > 0 {\n\t\t\t\t\ttags[tag.Key] += fmt.Sprintf(\",%s\", strings.Join(tag.Options, \",\"))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(tags) > 0 {\n\t\t\t\tf = f.Tag(tags)\n\t\t\t}\n\n\t\t\tif doc := strings.TrimSpace(field.Doc); doc != \"\" {\n\t\t\t\tf = f.Comment(doc)\n\t\t\t}\n\n\t\t\tfields[i] = f\n\t\t}\n\t\treturn Struct(fields...)\n\n\tcase *schema.Type_Map:\n\t\tkey := ConvertSchemaToJenType(typ.Map.Key, md)\n\t\tvalue := ConvertSchemaToJenType(typ.Map.Value, md)\n\t\treturn Map(key).Add(value)\n\n\tcase *schema.Type_List:\n\t\tvalue := ConvertSchemaToJenType(typ.List.Elem, md)\n\t\treturn Index().Add(value)\n\n\tcase *schema.Type_Builtin:\n\t\treturn ConvertBuiltInSchemaToJenType(typ.Builtin)\n\n\tcase *schema.Type_Pointer:\n\t\treturn Op(\"*\").Add(ConvertSchemaToJenType(typ.Pointer.Base, md))\n\n\tcase *schema.Type_Option:\n\t\treturn Qual(\"encore.dev/types/option\", \"Option\").Types(ConvertSchemaToJenType(typ.Option.Value, md))\n\n\tcase *schema.Type_TypeParameter:\n\t\treturn Id(md.Decls[typ.TypeParameter.DeclId].TypeParams[typ.TypeParameter.ParamIdx].Name)\n\n\tcase *schema.Type_Config:\n\t\tif typ.Config.IsValuesList {\n\t\t\treturn Qual(\"encore.dev/config\", \"Values\").Types(ConvertSchemaToJenType(typ.Config.Elem, md))\n\t\t} else {\n\n\t\t\treturn Qual(\"encore.dev/config\", \"Value\").Types(ConvertSchemaToJenType(typ.Config.Elem, md))\n\t\t}\n\n\tcase *schema.Type_Literal, *schema.Type_Union:\n\t\t// Not yet supported.\n\t\tpanic(fmt.Sprintf(\"ConvertSchemaToJenType doesn't support type: %T\", typ))\n\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"ConvertSchemaToJenType doesn't support type: %T\", typ))\n\t}\n}\n\nfunc ConvertBuiltInSchemaToJenType(builtin schema.Builtin) *Statement {\n\tswitch builtin {\n\tcase schema.Builtin_ANY:\n\t\treturn Any()\n\tcase schema.Builtin_BOOL:\n\t\treturn Bool()\n\tcase schema.Builtin_INT8:\n\t\treturn Int8()\n\tcase schema.Builtin_INT16:\n\t\treturn Int16()\n\tcase schema.Builtin_INT32:\n\t\treturn Int32()\n\tcase schema.Builtin_INT64:\n\t\treturn Int64()\n\tcase schema.Builtin_UINT8:\n\t\treturn Uint8()\n\tcase schema.Builtin_UINT16:\n\t\treturn Uint16()\n\tcase schema.Builtin_UINT32:\n\t\treturn Uint32()\n\tcase schema.Builtin_UINT64:\n\t\treturn Uint64()\n\tcase schema.Builtin_FLOAT32:\n\t\treturn Float32()\n\tcase schema.Builtin_FLOAT64:\n\t\treturn Float64()\n\tcase schema.Builtin_STRING:\n\t\treturn String()\n\tcase schema.Builtin_BYTES:\n\t\treturn Index().Byte()\n\tcase schema.Builtin_TIME:\n\t\treturn Qual(\"time\", \"Time\")\n\tcase schema.Builtin_UUID:\n\t\treturn Qual(\"encore.dev/types/uuid\", \"UUID\")\n\tcase schema.Builtin_JSON:\n\t\treturn Qual(\"encoding/json\", \"RawMessage\")\n\tcase schema.Builtin_USER_ID:\n\t\treturn Qual(\"encore.dev/beta/auth\", \"UID\")\n\tcase schema.Builtin_INT:\n\t\treturn Int()\n\tcase schema.Builtin_UINT:\n\t\treturn Uint()\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"ConvertBuiltInSchemaToJenType doesn't support builtin: %v\", builtin))\n\t}\n}\n"
  },
  {
    "path": "internal/gocodegen/marshalling.go",
    "content": "package gocodegen\n\nimport (\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t. \"github.com/dave/jennifer/jen\"\n\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\nconst UnknownPkgPath = \"__unknown_path__\"\n\nconst (\n\tlastErrorField      = \"LastError\"\n\tnonEmptyValuesField = \"NonEmptyValues\"\n)\n\n// MarshallingCodeGenerator is used to generate a structure has methods for decoding various types, collecting the errors.\n// It will only generate methods required for the given types.\ntype MarshallingCodeGenerator struct {\n\t// pkgPath is the package import path where the marshaller is defined.\n\tpkgPath             string\n\tstructName          string\n\tused                bool\n\tencoreTypesAsString bool // true if  auth.UID and uuid.UUID should be treated as strings?\n\n\tbuiltins     []methodDescription\n\tseenBuiltins map[methodKey]methodDescription\n\tusedBody     bool\n\tusedJson     bool\n}\n\ntype methodKey struct {\n\tfromString bool\n\tbuiltin    schema.Builtin\n\tslice      bool\n\toption     bool\n}\n\ntype methodDescription struct {\n\tFromString bool\n\tMethod     string\n\tInput      Code\n\tResult     Code\n\tIsList     bool\n\tIsOption   bool\n\tBlock      []Code\n}\n\n// MarshallingCodeWrapper is returned by NewPossibleInstance and tracks usage within a block\ntype MarshallingCodeWrapper struct {\n\tg            *MarshallingCodeGenerator\n\tpkgPath      string\n\tinstanceName string\n\tused         bool\n\n\tcode     []Code\n\tendBlock []Code\n}\n\nfunc NewMarshallingCodeGenerator(pkgPath, structName string, forClientGen bool) *MarshallingCodeGenerator {\n\treturn &MarshallingCodeGenerator{\n\t\tpkgPath:             pkgPath,\n\t\tstructName:          structName,\n\t\tbuiltins:            nil,\n\t\tseenBuiltins:        make(map[methodKey]methodDescription),\n\t\tencoreTypesAsString: forClientGen,\n\t}\n}\n\n// NewPossibleInstance Creates a statement to initialise a new encoding instance.\n//\n// Use the returned wrapper to convert FromStrings to the target types, adding any code you\n// are generating to the wrapper using Add. Once you've finished generating all the code which\n// may need type conversion with that _instance_ of the deserializer, call Finalize to generate the code full code\n// including error handling.\n//\n// Once you've finished writing the whole app with all the code which uses this generator call WriteToFile to write\n// the supporting struct and methods to the given file\nfunc (g *MarshallingCodeGenerator) NewPossibleInstance(instanceName string) *MarshallingCodeWrapper {\n\tg.used = true\n\treturn &MarshallingCodeWrapper{\n\t\tg:            g,\n\t\tinstanceName: instanceName,\n\t}\n}\n\n// GenerateAll causes the generator to generate all possible methods.\nfunc (g *MarshallingCodeGenerator) GenerateAll() {\n\tfor _, val := range schema.Builtin_value {\n\t\tb := schema.Builtin(val)\n\t\tfor _, slice := range []bool{false, true} {\n\t\t\tfor _, option := range []bool{false, true} {\n\t\t\t\t_, _ = g.builtinToString(b, slice, option)\n\t\t\t}\n\t\t}\n\t}\n\n\tg.usedBody = true\n\tg.usedJson = true\n}\n\n// WriteToFile writes the full encoder type into the given file.\nfunc (g *MarshallingCodeGenerator) WriteToFile(f *File) {\n\tif !g.used || (len(g.builtins) == 0 && !g.usedBody && !g.usedJson) {\n\t\treturn\n\t}\n\n\tf.Commentf(\"%s is used to serialize request data into strings and deserialize response data from strings\", g.structName)\n\tf.Type().Id(g.structName).Struct(\n\t\tId(lastErrorField).Error().Comment(\"The last error that occurred\"),\n\t\tId(nonEmptyValuesField).Int().Comment(\"The number of values this decoder has decoded\"),\n\t)\n\n\tfor _, desc := range g.builtins {\n\t\tvar params []Code\n\t\tif desc.FromString {\n\t\t\tparams = []Code{Id(\"field\").String(), Id(\"s\").Add(desc.Input), Id(\"required\").Bool()}\n\t\t} else {\n\t\t\tparams = []Code{Id(\"s\").Add(desc.Input)}\n\t\t}\n\n\t\tf.Func().Params(\n\t\t\tId(\"e\").Op(\"*\").Id(g.structName),\n\t\t).Id(desc.Method).Params(params...).Params(Id(\"v\").Add(desc.Result)).BlockFunc(func(g *Group) {\n\t\t\tif desc.FromString {\n\t\t\t\t// If we're dealing with a list of strings, we need to compare with len(s) == 0 instead of s == \"\"\n\t\t\t\tif desc.IsList {\n\t\t\t\t\tg.If(Op(\"!\").Id(\"required\").Op(\"&&\").Len(Id(\"s\")).Op(\"==\").Lit(0)).Block(Return())\n\t\t\t\t} else {\n\t\t\t\t\tg.If(Op(\"!\").Id(\"required\").Op(\"&&\").Id(\"s\").Op(\"==\").Lit(\"\")).Block(Return())\n\t\t\t\t}\n\t\t\t} else if desc.IsOption {\n\t\t\t\tg.If(Id(\"s\").Op(\"==\").Nil()).Block(Return(Nil()))\n\t\t\t}\n\t\t\tg.Id(\"e\").Dot(nonEmptyValuesField).Op(\"++\")\n\t\t\tfor _, s := range desc.Block {\n\t\t\t\tg.Add(s)\n\t\t\t}\n\t\t})\n\t\tf.Line()\n\t}\n\n\tf.Comment(\"setErr sets the last error within the object if one is not already set\")\n\tf.Func().Params(Id(\"e\").Op(\"*\").Id(g.structName)).Id(\"setErr\").Params(List(Id(\"msg\"), Id(\"field\")).String(), Err().Error()).Block(\n\t\tIf(Err().Op(\"!=\").Nil().Op(\"&&\").Id(\"e\").Dot(lastErrorField).Op(\"==\").Nil()).Block(\n\t\t\tId(\"e\").Dot(lastErrorField).Op(\"=\").Qual(\"fmt\", \"Errorf\").Call(\n\t\t\t\tLit(\"%s: %s: %w\"),\n\t\t\t\tId(\"field\"),\n\t\t\t\tId(\"msg\"),\n\t\t\t\tId(\"err\"),\n\t\t\t),\n\t\t),\n\t)\n\tf.Line()\n\n\tif g.usedBody {\n\t\tf.Func().Params(Id(\"d\").Op(\"*\").Id(g.structName)).Id(\"Body\").Params(Id(\"body\").Qual(\"io\", \"Reader\")).Params(Id(\"payload\").Index().Byte()).Block(\n\t\t\tList(Id(\"payload\"), Err()).Op(\":=\").Qual(\"io\", \"ReadAll\").Call(Id(\"body\")),\n\t\t\tIf(Err().Op(\"==\").Nil().Op(\"&&\").Len(Id(\"payload\")).Op(\"==\").Lit(0)).Block(\n\t\t\t\tId(\"d\").Dot(\"setErr\").Call(Lit(\"missing request body\"), Lit(\"request_body\"), Qual(\"fmt\", \"Errorf\").Call(Lit(\"missing request body\"))),\n\t\t\t).Else().If(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\tId(\"d\").Dot(\"setErr\").Call(Lit(\"could not parse request body\"), Lit(\"request_body\"), Err()),\n\t\t\t),\n\t\t\tReturn(Id(\"payload\")),\n\t\t)\n\t}\n\n\tif g.usedJson {\n\t\tf.Func().Params(Id(\"d\").Op(\"*\").Id(g.structName)).Id(\"ParseJSON\").Params(Id(\"field\").String(), Id(\"iter\").Op(\"*\").Qual(\"github.com/json-iterator/go\", \"Iterator\"), Id(\"dst\").Interface()).Block(\n\t\t\tId(\"iter\").Dot(\"ReadVal\").Call(Id(\"dst\")),\n\t\t\tId(\"d\").Dot(\"setErr\").Call(Lit(\"invalid json parameter\"), Id(\"field\"), Id(\"iter\").Dot(\"Error\")),\n\t\t)\n\t}\n\tf.Line()\n}\n\nfunc (b *MarshallingCodeGenerator) builtinFromString(t schema.Builtin, slice, option bool) (string, error) {\n\tkey := methodKey{builtin: t, slice: slice, option: option, fromString: true}\n\tif n, ok := b.seenBuiltins[key]; ok {\n\t\treturn n.Method, nil\n\t} else if slice {\n\t\tk2 := methodKey{builtin: t, fromString: true, slice: false, option: option}\n\t\tif _, err := b.builtinFromString(t, false, option); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdesc := b.seenBuiltins[k2]\n\t\tname := desc.Method + \"List\"\n\t\tfn := methodDescription{\n\t\t\tFromString: true,\n\t\t\tMethod:     name,\n\t\t\tInput:      Index().String(),\n\t\t\tResult:     Index().Add(desc.Result),\n\t\t\tIsList:     true,\n\t\t\tBlock: []Code{\n\t\t\t\tFor(List(Id(\"_\"), Id(\"x\")).Op(\":=\").Range().Id(\"s\")).Block(\n\t\t\t\t\tId(\"v\").Op(\"=\").Append(Id(\"v\"), Id(\"e\").Dot(desc.Method).Call(Id(\"field\"), Id(\"x\"), Id(\"required\"))),\n\t\t\t\t),\n\t\t\t\tReturn(Id(\"v\")),\n\t\t\t},\n\t\t}\n\t\tb.seenBuiltins[key] = fn\n\t\tb.builtins = append(b.builtins, fn)\n\t\treturn fn.Method, nil\n\t} else if option {\n\t\tk2 := methodKey{builtin: t, fromString: true, slice: false, option: false}\n\t\tif _, err := b.builtinFromString(t, false, false); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdesc := b.seenBuiltins[k2]\n\t\tname := desc.Method + \"Option\"\n\t\tfn := methodDescription{\n\t\t\tFromString: true,\n\t\t\tMethod:     name,\n\t\t\tInput:      String(),\n\t\t\tResult:     Op(\"*\").Add(desc.Result),\n\t\t\tIsList:     false,\n\t\t\tIsOption:   true,\n\t\t\tBlock: []Code{\n\t\t\t\tId(\"val\").Op(\":=\").Id(\"e\").Dot(desc.Method).Call(Id(\"field\"), Id(\"s\"), Id(\"required\")),\n\t\t\t\tReturn(Op(\"&\").Id(\"val\")),\n\t\t\t},\n\t\t}\n\t\tb.seenBuiltins[key] = fn\n\t\tb.builtins = append(b.builtins, fn)\n\t\treturn fn.Method, nil\n\t}\n\n\tvar fn methodDescription\n\tswitch t {\n\tcase schema.Builtin_STRING:\n\t\tfn = methodDescription{true, \"ToString\", String(), String(), false, false, []Code{Return(Id(\"s\"))}}\n\tcase schema.Builtin_BYTES:\n\t\tfn = methodDescription{true, \"ToBytes\", String(), Index().Byte(), false, false, []Code{\n\t\t\tList(Id(\"v\"), Err()).Op(\":=\").Qual(\"encoding/base64\", \"URLEncoding\").Dot(\"DecodeString\").Call(Id(\"s\")),\n\t\t\tId(\"e\").Dot(\"setErr\").Call(Lit(\"invalid parameter\"), Id(\"field\"), Err()),\n\t\t\tReturn(Id(\"v\")),\n\t\t}}\n\tcase schema.Builtin_BOOL:\n\t\tfn = methodDescription{true, \"ToBool\", String(), Bool(), false, false, []Code{\n\t\t\tList(Id(\"v\"), Err()).Op(\":=\").Qual(\"strconv\", \"ParseBool\").Call(Id(\"s\")),\n\t\t\tId(\"e\").Dot(\"setErr\").Call(Lit(\"invalid parameter\"), Id(\"field\"), Err()),\n\t\t\tReturn(Id(\"v\")),\n\t\t}}\n\tcase schema.Builtin_UUID:\n\t\tfn = methodDescription{true, \"ToUUID\", String(), Qual(\"encore.dev/types/uuid\", \"UUID\"), false, false, []Code{\n\t\t\tList(Id(\"v\"), Err()).Op(\":=\").Qual(\"encore.dev/types/uuid\", \"FromString\").Call(Id(\"s\")),\n\t\t\tId(\"e\").Dot(\"setErr\").Call(Lit(\"invalid parameter\"), Id(\"field\"), Err()),\n\t\t\tReturn(Id(\"v\")),\n\t\t}}\n\tcase schema.Builtin_TIME:\n\t\tfn = methodDescription{true, \"ToTime\", String(), Qual(\"time\", \"Time\"), false, false, []Code{\n\t\t\tList(Id(\"v\"), Err()).Op(\":=\").Qual(\"time\", \"Parse\").Call(Qual(\"time\", \"RFC3339\"), Id(\"s\")),\n\t\t\tId(\"e\").Dot(\"setErr\").Call(Lit(\"invalid parameter\"), Id(\"field\"), Err()),\n\t\t\tReturn(Id(\"v\")),\n\t\t}}\n\tcase schema.Builtin_USER_ID:\n\t\tfn = methodDescription{true, \"ToUserID\", String(), Qual(\"encore.dev/beta/auth\", \"UID\"), false, false, []Code{\n\t\t\tReturn(Qual(\"encore.dev/beta/auth\", \"UID\").Call(Id(\"s\"))),\n\t\t}}\n\tcase schema.Builtin_JSON:\n\t\tfn = methodDescription{true, \"ToJSON\", String(), Qual(\"encoding/json\", \"RawMessage\"), false, false, []Code{\n\t\t\tReturn(Qual(\"encoding/json\", \"RawMessage\").Call(Id(\"s\"))),\n\t\t}}\n\tdefault:\n\t\ttype kind int\n\t\tconst (\n\t\t\tunsigned kind = iota + 1\n\t\t\tsigned\n\t\t\tfloat\n\t\t)\n\t\tnumTypes := map[schema.Builtin]struct {\n\t\t\ttyp  string\n\t\t\tkind kind\n\t\t\tbits int\n\t\t}{\n\t\t\tschema.Builtin_INT8:    {\"int8\", signed, 8},\n\t\t\tschema.Builtin_INT16:   {\"int16\", signed, 16},\n\t\t\tschema.Builtin_INT32:   {\"int32\", signed, 32},\n\t\t\tschema.Builtin_INT64:   {\"int64\", signed, 64},\n\t\t\tschema.Builtin_INT:     {\"int\", signed, 64},\n\t\t\tschema.Builtin_UINT8:   {\"uint8\", unsigned, 8},\n\t\t\tschema.Builtin_UINT16:  {\"uint16\", unsigned, 16},\n\t\t\tschema.Builtin_UINT32:  {\"uint32\", unsigned, 32},\n\t\t\tschema.Builtin_UINT64:  {\"uint64\", unsigned, 64},\n\t\t\tschema.Builtin_UINT:    {\"uint\", unsigned, 64},\n\t\t\tschema.Builtin_FLOAT64: {\"float64\", float, 64},\n\t\t\tschema.Builtin_FLOAT32: {\"float32\", float, 32},\n\t\t}\n\n\t\tdef, ok := numTypes[t]\n\t\tif !ok {\n\t\t\treturn \"\", errors.Newf(\"unsupported type: %s\", t)\n\t\t}\n\n\t\tcast := def.typ != \"int64\" && def.typ != \"uint64\" && def.typ != \"float64\"\n\t\tvar err error\n\t\tfn = methodDescription{true, \"To\" + strings.Title(def.typ), String(), Id(def.typ), false, false, []Code{\n\t\t\tList(Id(\"x\"), Err()).Op(\":=\").Do(func(s *Statement) {\n\t\t\t\tswitch def.kind {\n\t\t\t\tcase unsigned:\n\t\t\t\t\ts.Qual(\"strconv\", \"ParseUint\").Call(Id(\"s\"), Lit(10), Lit(def.bits))\n\t\t\t\tcase signed:\n\t\t\t\t\ts.Qual(\"strconv\", \"ParseInt\").Call(Id(\"s\"), Lit(10), Lit(def.bits))\n\t\t\t\tcase float:\n\t\t\t\t\ts.Qual(\"strconv\", \"ParseFloat\").Call(Id(\"s\"), Lit(def.bits))\n\t\t\t\tdefault:\n\t\t\t\t\terr = errors.Newf(\"unknown kind %v\", def.kind)\n\t\t\t\t}\n\t\t\t}),\n\t\t\tId(\"e\").Dot(\"setErr\").Call(Lit(\"invalid parameter\"), Id(\"field\"), Err()),\n\t\t\tReturnFunc(func(g *Group) {\n\t\t\t\tif cast {\n\t\t\t\t\tg.Id(def.typ).Call(Id(\"x\"))\n\t\t\t\t} else {\n\t\t\t\t\tg.Id(\"x\")\n\t\t\t\t}\n\t\t\t}),\n\t\t}}\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tb.seenBuiltins[key] = fn\n\tb.builtins = append(b.builtins, fn)\n\treturn fn.Method, nil\n}\n\nfunc (b *MarshallingCodeGenerator) builtinToString(t schema.Builtin, slice, option bool) (string, error) {\n\tkey := methodKey{builtin: t, slice: slice, option: option, fromString: false}\n\tif fn, ok := b.seenBuiltins[key]; ok {\n\t\treturn fn.Method, nil\n\t}\n\n\tif slice {\n\t\tk2 := methodKey{builtin: t, fromString: false, slice: false, option: option}\n\t\tif _, err := b.builtinToString(t, false, option); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdesc := b.seenBuiltins[k2]\n\t\tname := desc.Method + \"List\"\n\t\tfn := methodDescription{\n\t\t\tFromString: false,\n\t\t\tMethod:     name,\n\t\t\tInput:      Index().Add(desc.Input),\n\t\t\tResult:     Index().String(),\n\t\t\tIsList:     true,\n\t\t\tBlock: []Code{\n\t\t\t\tFor(List(Id(\"_\"), Id(\"x\")).Op(\":=\").Range().Id(\"s\")).Block(\n\t\t\t\t\tId(\"v\").Op(\"=\").Append(Id(\"v\"), Id(\"e\").Dot(desc.Method).Call(Id(\"x\"))),\n\t\t\t\t),\n\t\t\t\tReturn(Id(\"v\")),\n\t\t\t},\n\t\t}\n\t\tb.seenBuiltins[key] = fn\n\t\tb.builtins = append(b.builtins, fn)\n\t\treturn fn.Method, nil\n\t} else if option {\n\t\tk2 := methodKey{builtin: t, fromString: false, slice: false, option: false}\n\t\tif _, err := b.builtinToString(t, false, false); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdesc := b.seenBuiltins[k2]\n\t\tname := desc.Method + \"Option\"\n\t\tfn := methodDescription{\n\t\t\tFromString: false,\n\t\t\tMethod:     name,\n\t\t\tInput:      Op(\"*\").Add(desc.Input),\n\t\t\tResult:     Index().String(),\n\t\t\tIsOption:   true,\n\t\t\tBlock: []Code{\n\t\t\t\tReturn(Index().String().Values(Id(\"e\").Dot(desc.Method).Call(Op(\"*\").Id(\"s\")))),\n\t\t\t},\n\t\t}\n\t\tb.seenBuiltins[key] = fn\n\t\tb.builtins = append(b.builtins, fn)\n\t\treturn fn.Method, nil\n\t}\n\n\tvar fn methodDescription\n\tswitch t {\n\tcase schema.Builtin_STRING:\n\t\tfn = methodDescription{false, \"FromString\", String(), String(), false, false, []Code{Return(Id(\"s\"))}}\n\tcase schema.Builtin_BYTES:\n\t\tfn = methodDescription{false, \"FromBytes\", Index().Byte(), String(), false, false, []Code{\n\t\t\tReturn(Qual(\"encoding/base64\", \"URLEncoding\").Dot(\"EncodeToString\").Call(Id(\"s\"))),\n\t\t}}\n\tcase schema.Builtin_BOOL:\n\t\tfn = methodDescription{false, \"FromBool\", Bool(), String(), false, false, []Code{\n\t\t\tReturn(Qual(\"strconv\", \"FormatBool\").Call(Id(\"s\"))),\n\t\t}}\n\tcase schema.Builtin_UUID:\n\t\tfn = methodDescription{false, \"FromUUID\", Qual(\"encore.dev/types/uuid\", \"UUID\"), String(), false, false, []Code{\n\t\t\tReturn(Id(\"s\").Dot(\"String\").Call()),\n\t\t}}\n\tcase schema.Builtin_TIME:\n\t\tfn = methodDescription{false, \"FromTime\", Qual(\"time\", \"Time\"), String(), false, false, []Code{\n\t\t\tReturn(Id(\"s\").Dot(\"Format\").Call(Qual(\"time\", \"RFC3339\"))),\n\t\t}}\n\tcase schema.Builtin_USER_ID:\n\t\tfn = methodDescription{false, \"FromUserID\", Qual(\"encore.dev/beta/auth\", \"UID\"), String(), false, false, []Code{\n\t\t\tReturn(String().Call(Id(\"s\"))),\n\t\t}}\n\tcase schema.Builtin_JSON:\n\t\tfn = methodDescription{false, \"FromJSON\", Qual(\"encoding/json\", \"RawMessage\"), String(), false, false, []Code{\n\t\t\tReturn(String().Call(Id(\"s\"))),\n\t\t}}\n\tdefault:\n\t\ttype kind int\n\t\tconst (\n\t\t\tunsigned kind = iota + 1\n\t\t\tsigned\n\t\t\tfloat\n\t\t)\n\t\tnumTypes := map[schema.Builtin]struct {\n\t\t\ttyp     string\n\t\t\tcastTyp string\n\t\t\tkind    kind\n\t\t\tbits    int\n\t\t}{\n\t\t\tschema.Builtin_INT8:    {\"int8\", \"int64\", signed, 8},\n\t\t\tschema.Builtin_INT16:   {\"int16\", \"int64\", signed, 16},\n\t\t\tschema.Builtin_INT32:   {\"int32\", \"int64\", signed, 32},\n\t\t\tschema.Builtin_INT64:   {\"int64\", \"int64\", signed, 64},\n\t\t\tschema.Builtin_INT:     {\"int\", \"int64\", signed, 64},\n\t\t\tschema.Builtin_UINT8:   {\"uint8\", \"uint64\", unsigned, 8},\n\t\t\tschema.Builtin_UINT16:  {\"uint16\", \"uint64\", unsigned, 16},\n\t\t\tschema.Builtin_UINT32:  {\"uint32\", \"uint64\", unsigned, 32},\n\t\t\tschema.Builtin_UINT64:  {\"uint64\", \"uint64\", unsigned, 64},\n\t\t\tschema.Builtin_UINT:    {\"uint\", \"uint64\", unsigned, 64},\n\t\t\tschema.Builtin_FLOAT64: {\"float64\", \"float64\", float, 64},\n\t\t\tschema.Builtin_FLOAT32: {\"float32\", \"float64\", float, 32},\n\t\t}\n\n\t\tdef, ok := numTypes[t]\n\t\tif !ok {\n\t\t\treturn \"\", errors.Newf(\"unsupported type: %s\", t)\n\t\t}\n\n\t\tvar err error\n\t\tfn = methodDescription{false, \"From\" + strings.Title(def.typ), Id(def.typ), String(), false, false, []Code{\n\t\t\tReturn(Do(func(s *Statement) {\n\t\t\t\tid := Id(\"s\")\n\t\t\t\tif def.typ != def.castTyp {\n\t\t\t\t\tid = Id(def.castTyp).Call(id)\n\t\t\t\t}\n\n\t\t\t\tswitch def.kind {\n\t\t\t\tcase unsigned:\n\t\t\t\t\ts.Qual(\"strconv\", \"FormatUint\").Call(id, Lit(10))\n\t\t\t\tcase signed:\n\t\t\t\t\ts.Qual(\"strconv\", \"FormatInt\").Call(id, Lit(10))\n\t\t\t\tcase float:\n\t\t\t\t\ts.Qual(\"strconv\", \"FormatFloat\").Call(id, Lit(byte('f')), Lit(-1), Lit(def.bits))\n\t\t\t\tdefault:\n\t\t\t\t\terr = errors.Newf(\"unknown kind %v\", def.kind)\n\t\t\t\t}\n\t\t\t})),\n\t\t}}\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tb.seenBuiltins[key] = fn\n\tb.builtins = append(b.builtins, fn)\n\treturn fn.Method, nil\n}\n\nfunc (w *MarshallingCodeWrapper) WithFunc(body func(*Group), errBlock func(*Group)) []Code {\n\to := Options{Separator: \"\\n\", Multi: true}\n\tbodyStatement := CustomFunc(o, body)\n\terrStatement := CustomFunc(o, errBlock)\n\n\tw.Add(bodyStatement)\n\treturn w.Finalize(errStatement)\n}\n\nfunc (w *MarshallingCodeWrapper) LastError() Code {\n\treturn Id(w.instanceName).Dot(lastErrorField)\n}\n\n// Add adds code into the wrapped block\nfunc (w *MarshallingCodeWrapper) Add(c ...Code) {\n\tw.code = append(w.code, c...)\n}\n\n// EndBlock adds custom logic after the error block\nfunc (w *MarshallingCodeWrapper) EndBlock(endBlock ...Code) {\n\tw.endBlock = endBlock\n}\n\n// Finalize returns the final code block including all wrapped code\nfunc (w *MarshallingCodeWrapper) Finalize(ifErrorBlock ...Code) []Code {\n\tif !w.used {\n\t\treturn w.code\n\t}\n\n\t// If we know the package path, refer to the decoder with a qualified name.\n\tvar structRef *Statement\n\tif w.g.pkgPath != UnknownPkgPath {\n\t\tstructRef = Qual(w.g.pkgPath, w.g.structName)\n\t} else {\n\t\tstructRef = Id(w.g.structName)\n\t}\n\n\tcode := []Code{Id(w.instanceName).Op(\":=\").Op(\"&\").Add(structRef).Values(), Line()}\n\tcode = append(code, w.code...)\n\tcode = append(code, Line().If(Id(w.instanceName).Dot(lastErrorField).Op(\"!=\").Nil()).Block(ifErrorBlock...))\n\tcode = append(code, Line())\n\tcode = append(code, w.endBlock...)\n\treturn code\n}\n\nfunc (g *MarshallingCodeGenerator) shouldBeTreatedAsString(builtin schema.Builtin) bool {\n\treturn builtin == schema.Builtin_STRING || builtin == schema.Builtin_DECIMAL ||\n\t\t(g.encoreTypesAsString && builtin == schema.Builtin_UUID) ||\n\t\t(g.encoreTypesAsString && builtin == schema.Builtin_USER_ID)\n}\n\nfunc (w *MarshallingCodeWrapper) Body(getBody Code) Code {\n\tw.used = true\n\tw.g.usedBody = true\n\treturn Id(w.instanceName).Dot(\"Body\").Call(getBody)\n}\n\n// FromStringToBuiltin will return either the original string or a call to the encoder\nfunc (w *MarshallingCodeWrapper) FromStringToBuiltin(builtin schema.Builtin, fieldName string, getAsString Code, required bool) (code Code, err error) {\n\t// get the method name for the target type\n\tfuncName := \"\"\n\tsrcCode := getAsString\n\n\t// If the list is strings, we can just return the value\n\tif builtin == schema.Builtin_STRING && !required {\n\t\treturn getAsString, nil\n\t}\n\n\tfuncName, err = w.g.builtinFromString(builtin, false, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// mark this code wrapper as actually using the deserializer type\n\tw.used = true\n\treturn Id(w.instanceName).Dot(funcName).Call(Lit(fieldName), srcCode, Lit(required)), nil\n}\n\nfunc (w *MarshallingCodeWrapper) FromJSON(targetType *schema.Type, fieldName string, iterName string, dst Code) (code Code, err error) {\n\t// TODO: Call readers for specific types once we've added Pointer Type support\n\tw.used = true\n\tw.g.usedJson = true\n\treturn Id(w.instanceName).Dot(\"ParseJSON\").Call(Lit(fieldName), Id(iterName), Op(\"&\").Add(dst)), nil\n}\n\n// FromString will return a call to a decoder method\nfunc (w *MarshallingCodeWrapper) FromString(targetType *schema.Type, fieldName string, getAsString Code, getAsStringSlice Code, required bool) (code Code, err error) {\n\t// get the method name for the target type\n\tfuncName := \"\"\n\tsrcCode := getAsString\n\tswitch t := targetType.Typ.(type) {\n\tcase *schema.Type_List:\n\t\tif bt, ok := t.List.Elem.Typ.(*schema.Type_Builtin); ok {\n\t\t\t// If the list is uuids or userids, treat it as string\n\t\t\tbuiltin := bt.Builtin\n\t\t\tif w.g.shouldBeTreatedAsString(bt.Builtin) {\n\t\t\t\tbuiltin = schema.Builtin_STRING\n\t\t\t}\n\n\t\t\tfuncName, err = w.g.builtinFromString(builtin, true, false)\n\t\t\tsrcCode = getAsStringSlice\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"unsupported list type %T\", t.List.Elem.Typ)\n\t\t}\n\n\tcase *schema.Type_Option:\n\t\tif bt, ok := t.Option.Value.Typ.(*schema.Type_Builtin); ok {\n\t\t\t// If the list is uuids or userids, treat it as string\n\t\t\tbuiltin := bt.Builtin\n\t\t\tif w.g.shouldBeTreatedAsString(bt.Builtin) {\n\t\t\t\tbuiltin = schema.Builtin_STRING\n\t\t\t}\n\n\t\t\tfuncName, err = w.g.builtinFromString(builtin, false, true)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\t// Options are not required.\n\t\t\trequired = false\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"unsupported option type %T\", t.Option.Value.Typ)\n\t\t}\n\n\tcase *schema.Type_Builtin:\n\t\t// If it's uuid, userid then treat it as string\n\t\tbuiltin := t.Builtin\n\t\tif w.g.shouldBeTreatedAsString(t.Builtin) {\n\t\t\tbuiltin = schema.Builtin_STRING\n\t\t}\n\n\t\tfuncName, err = w.g.builtinFromString(builtin, false, false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\tdefault:\n\t\treturn nil, errors.Newf(\"unsupported type for deserialization: %T\", t)\n\t}\n\n\t// mark this code wrapper as actually using the deserializer type\n\tw.used = true\n\treturn Id(w.instanceName).Dot(funcName).Call(Lit(fieldName), srcCode, Lit(required)), nil\n}\n\n// ToStringSlice will return either the original string or a call to the encoder\nfunc (w *MarshallingCodeWrapper) ToStringSlice(sourceType *schema.Type, sourceValue Code) (code Code, err error) {\n\t// get the method name for the target type\n\tfuncName := \"\"\n\tswitch t := sourceType.Typ.(type) {\n\tcase *schema.Type_List:\n\t\tif bt, ok := t.List.Elem.Typ.(*schema.Type_Builtin); ok {\n\t\t\tbuiltin := bt.Builtin\n\t\t\tif w.g.shouldBeTreatedAsString(bt.Builtin) {\n\t\t\t\tbuiltin = schema.Builtin_STRING\n\t\t\t}\n\n\t\t\tfuncName, err = w.g.builtinToString(builtin, true, false)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tw.used = true\n\t\t\treturn Id(w.instanceName).Dot(funcName).Call(sourceValue), nil\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"unsupported list type %T\", t.List.Elem.Typ)\n\t\t}\n\n\tcase *schema.Type_Option:\n\t\tif bt, ok := t.Option.Value.Typ.(*schema.Type_Builtin); ok {\n\t\t\tbuiltin := bt.Builtin\n\t\t\tif w.g.shouldBeTreatedAsString(bt.Builtin) {\n\t\t\t\tbuiltin = schema.Builtin_STRING\n\t\t\t}\n\n\t\t\tfuncName, err = w.g.builtinToString(builtin, false, true)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tw.used = true\n\t\t\treturn Id(w.instanceName).Dot(funcName).Call(sourceValue), nil\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"unsupported option type %T\", t.Option.Value.Typ)\n\t\t}\n\n\tcase *schema.Type_Builtin:\n\t\tbuiltin := t.Builtin\n\t\tif w.g.shouldBeTreatedAsString(t.Builtin) {\n\t\t\tbuiltin = schema.Builtin_STRING\n\t\t}\n\n\t\tfuncName, err = w.g.builtinToString(builtin, false, false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.used = true\n\t\treturn Values(Id(w.instanceName).Dot(funcName).Call(sourceValue)), nil\n\n\tdefault:\n\t\treturn nil, errors.Newf(\"unsupported type for serialization: %T\", t)\n\t}\n}\n\n// ToString will return either the original string or a call to the encoder\nfunc (w *MarshallingCodeWrapper) ToString(sourceType *schema.Type, sourceValue Code) (code Code, err error) {\n\t// get the method name for the target type\n\tfuncName := \"\"\n\tswitch t := sourceType.Typ.(type) {\n\tcase *schema.Type_Builtin:\n\t\tbuiltin := t.Builtin\n\t\tif w.g.shouldBeTreatedAsString(t.Builtin) {\n\t\t\tbuiltin = schema.Builtin_STRING\n\t\t}\n\n\t\tfuncName, err = w.g.builtinToString(builtin, false, false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.used = true\n\t\treturn Id(w.instanceName).Dot(funcName).Call(sourceValue), nil\n\n\tcase *schema.Type_Option:\n\t\tif bt, ok := t.Option.Value.Typ.(*schema.Type_Builtin); ok {\n\t\t\tbuiltin := bt.Builtin\n\t\t\tif w.g.shouldBeTreatedAsString(bt.Builtin) {\n\t\t\t\tbuiltin = schema.Builtin_STRING\n\t\t\t}\n\n\t\t\tfuncName, err = w.g.builtinToString(builtin, false, true)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tw.used = true\n\t\t\treturn Id(w.instanceName).Dot(funcName).Call(sourceValue), nil\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"unsupported option type %T\", t.Option.Value.Typ)\n\t\t}\n\n\tdefault:\n\t\treturn nil, errors.Newf(\"unsupported type for serialization: %T\", t)\n\t}\n}\n"
  },
  {
    "path": "internal/gocodegen/package.go",
    "content": "// Package gocodegen contains shared code used for generating Go code by both the\n// compilers code generator, and the CLI's client generator.\npackage gocodegen\n"
  },
  {
    "path": "internal/goldfish/goldfish.go",
    "content": "// Package goldfish provides a short-term cache of values.\npackage goldfish\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\ntype Cache[V any] struct {\n\tkeepalive time.Duration\n\tfn        func() (V, error)\n\tmu        sync.Mutex\n\tlast      time.Time\n\tval       V\n}\n\nfunc New[V any](keepalive time.Duration, fn func() (V, error)) *Cache[V] {\n\treturn &Cache[V]{\n\t\tkeepalive: keepalive,\n\t\tfn:        fn,\n\t}\n}\n\nfunc (c *Cache[V]) Get() (V, error) {\n\tnow := time.Now()\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\tif now.Sub(c.last) < c.keepalive {\n\t\treturn c.val, nil\n\t}\n\n\t// Cache is out of date, re-fetch\n\tval, err := c.fn()\n\tif err == nil {\n\t\tc.val, c.last = val, now\n\t}\n\treturn c.val, err\n}\n\nfunc (c *Cache[V]) Set(val V) {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tc.val = val\n\tc.last = time.Now()\n}\n"
  },
  {
    "path": "internal/httpcache/LICENSE.txt",
    "content": "Copyright © 2012 Greg Jones (greg.jones@gmail.com)\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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."
  },
  {
    "path": "internal/httpcache/README.md",
    "content": "httpcache\n=========\n\n[![Build Status](https://travis-ci.org/gregjones/httpcache.svg?branch=master)](https://travis-ci.org/gregjones/httpcache) [![GoDoc](https://godoc.org/github.com/gregjones/httpcache?status.svg)](https://godoc.org/github.com/gregjones/httpcache)\n\nPackage httpcache provides a http.RoundTripper implementation that works as a mostly [RFC 7234](https://tools.ietf.org/html/rfc7234) compliant cache for http responses.\n\nIt is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client and not for a shared proxy).\n\nThis project isn't actively maintained; it works for what I, and seemingly others, want to do with it, and I consider it \"done\". That said, if you find any issues, please open a Pull Request and I will try to review it. Any changes now that change the public API won't be considered.\n\nCache Backends\n--------------\n\n- The built-in 'memory' cache stores responses in an in-memory map.\n- [`github.com/gregjones/httpcache/diskcache`](https://github.com/gregjones/httpcache/tree/master/diskcache) provides a filesystem-backed cache using the [diskv](https://github.com/peterbourgon/diskv) library.\n- [`github.com/gregjones/httpcache/memcache`](https://github.com/gregjones/httpcache/tree/master/memcache) provides memcache implementations, for both App Engine and 'normal' memcache servers.\n- [`sourcegraph.com/sourcegraph/s3cache`](https://sourcegraph.com/github.com/sourcegraph/s3cache) uses Amazon S3 for storage.\n- [`github.com/gregjones/httpcache/leveldbcache`](https://github.com/gregjones/httpcache/tree/master/leveldbcache) provides a filesystem-backed cache using [leveldb](https://github.com/syndtr/goleveldb/tree/master/leveldb).\n- [`github.com/die-net/lrucache`](https://github.com/die-net/lrucache) provides an in-memory cache that will evict least-recently used entries.\n- [`github.com/die-net/lrucache/twotier`](https://github.com/die-net/lrucache/tree/master/twotier) allows caches to be combined, for example to use lrucache above with a persistent disk-cache.\n- [`github.com/birkelund/boltdbcache`](https://github.com/birkelund/boltdbcache) provides a BoltDB implementation (based on the [bbolt](https://github.com/coreos/bbolt) fork).\n\nIf you implement any other backend and wish it to be linked here, please send a PR editing this file.\n\nLicense\n-------\n\n-\t[MIT License](LICENSE.txt)\n"
  },
  {
    "path": "internal/httpcache/diskcache/diskcache.go",
    "content": "// Package diskcache provides an implementation of httpcache.Cache that uses the diskv package\n// to supplement an in-memory map with persistent storage\n//\npackage diskcache\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"github.com/peterbourgon/diskv\"\n\t\"io\"\n)\n\n// Cache is an implementation of httpcache.Cache that supplements the in-memory map with persistent storage\ntype Cache struct {\n\td *diskv.Diskv\n}\n\n// Get returns the response corresponding to key if present\nfunc (c *Cache) Get(key string) (resp []byte, ok bool) {\n\tkey = keyToFilename(key)\n\tresp, err := c.d.Read(key)\n\tif err != nil {\n\t\treturn []byte{}, false\n\t}\n\treturn resp, true\n}\n\n// Set saves a response to the cache as key\nfunc (c *Cache) Set(key string, resp []byte) {\n\tkey = keyToFilename(key)\n\tc.d.WriteStream(key, bytes.NewReader(resp), true)\n}\n\n// Delete removes the response with key from the cache\nfunc (c *Cache) Delete(key string) {\n\tkey = keyToFilename(key)\n\tc.d.Erase(key)\n}\n\nfunc keyToFilename(key string) string {\n\th := md5.New()\n\tio.WriteString(h, key)\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\n// New returns a new Cache that will store files in basePath\nfunc New(basePath string) *Cache {\n\treturn &Cache{\n\t\td: diskv.New(diskv.Options{\n\t\t\tBasePath:     basePath,\n\t\t\tCacheSizeMax: 100 * 1024 * 1024, // 100MB\n\t\t}),\n\t}\n}\n\n// NewWithDiskv returns a new Cache using the provided Diskv as underlying\n// storage.\nfunc NewWithDiskv(d *diskv.Diskv) *Cache {\n\treturn &Cache{d}\n}\n"
  },
  {
    "path": "internal/httpcache/diskcache/diskcache_test.go",
    "content": "package diskcache\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"encr.dev/internal/httpcache/test\"\n)\n\nfunc TestDiskCache(t *testing.T) {\n\ttempDir, err := ioutil.TempDir(\"\", \"httpcache\")\n\tif err != nil {\n\t\tt.Fatalf(\"TempDir: %v\", err)\n\t}\n\tdefer os.RemoveAll(tempDir)\n\n\ttest.Cache(t, New(tempDir))\n}\n"
  },
  {
    "path": "internal/httpcache/httpcache.go",
    "content": "// Package httpcache provides a http.RoundTripper implementation that works as a\n// mostly RFC-compliant cache for http responses.\n//\n// It is only suitable for use as a 'private' cache (i.e. for a web-browser or an API-client\n// and not for a shared proxy).\npackage httpcache\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tstale = iota\n\tfresh\n\ttransparent\n\t// XFromCache is the header added to responses that are returned from the cache\n\tXFromCache = \"X-From-Cache\"\n)\n\n// A Cache interface is used by the Transport to store and retrieve responses.\ntype Cache interface {\n\t// Get returns the []byte representation of a cached response and a bool\n\t// set to true if the value isn't empty\n\tGet(key string) (responseBytes []byte, ok bool)\n\t// Set stores the []byte representation of a response against a key\n\tSet(key string, responseBytes []byte)\n\t// Delete removes the value associated with the key\n\tDelete(key string)\n}\n\n// cacheKey returns the cache key for req.\nfunc cacheKey(req *http.Request) string {\n\tif req.Method == http.MethodGet {\n\t\treturn req.URL.String()\n\t} else {\n\t\treturn req.Method + \" \" + req.URL.String()\n\t}\n}\n\n// CachedResponse returns the cached http.Response for req if present, and nil\n// otherwise.\nfunc CachedResponse(c Cache, req *http.Request) (resp *http.Response, err error) {\n\tcachedVal, ok := c.Get(cacheKey(req))\n\tif !ok {\n\t\treturn\n\t}\n\n\tb := bytes.NewBuffer(cachedVal)\n\treturn http.ReadResponse(bufio.NewReader(b), req)\n}\n\n// MemoryCache is an implemtation of Cache that stores responses in an in-memory map.\ntype MemoryCache struct {\n\tmu    sync.RWMutex\n\titems map[string][]byte\n}\n\n// Get returns the []byte representation of the response and true if present, false if not\nfunc (c *MemoryCache) Get(key string) (resp []byte, ok bool) {\n\tc.mu.RLock()\n\tresp, ok = c.items[key]\n\tc.mu.RUnlock()\n\treturn resp, ok\n}\n\n// Set saves response resp to the cache with key\nfunc (c *MemoryCache) Set(key string, resp []byte) {\n\tc.mu.Lock()\n\tc.items[key] = resp\n\tc.mu.Unlock()\n}\n\n// Delete removes key from the cache\nfunc (c *MemoryCache) Delete(key string) {\n\tc.mu.Lock()\n\tdelete(c.items, key)\n\tc.mu.Unlock()\n}\n\n// NewMemoryCache returns a new Cache that will store items in an in-memory map\nfunc NewMemoryCache() *MemoryCache {\n\tc := &MemoryCache{items: map[string][]byte{}}\n\treturn c\n}\n\n// Transport is an implementation of http.RoundTripper that will return values from a cache\n// where possible (avoiding a network request) and will additionally add validators (etag/if-modified-since)\n// to repeated requests allowing servers to return 304 / Not Modified\ntype Transport struct {\n\t// The RoundTripper interface actually used to make requests\n\t// If nil, http.DefaultTransport is used\n\tTransport http.RoundTripper\n\tCache     Cache\n\t// If true, responses returned from the cache will be given an extra header, X-From-Cache\n\tMarkCachedResponses bool\n}\n\n// NewTransport returns a new Transport with the\n// provided Cache implementation and MarkCachedResponses set to true\nfunc NewTransport(c Cache) *Transport {\n\treturn &Transport{Cache: c, MarkCachedResponses: true}\n}\n\n// Client returns an *http.Client that caches responses.\nfunc (t *Transport) Client() *http.Client {\n\treturn &http.Client{Transport: t}\n}\n\n// varyMatches will return false unless all of the cached values for the headers listed in Vary\n// match the new request\nfunc varyMatches(cachedResp *http.Response, req *http.Request) bool {\n\tfor _, header := range headerAllCommaSepValues(cachedResp.Header, \"vary\") {\n\t\theader = http.CanonicalHeaderKey(header)\n\t\tif header != \"\" && req.Header.Get(header) != cachedResp.Header.Get(\"X-Varied-\"+header) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// RoundTrip takes a Request and returns a Response\n//\n// If there is a fresh Response already in cache, then it will be returned without connecting to\n// the server.\n//\n// If there is a stale Response, then any validators it contains will be set on the new request\n// to give the server a chance to respond with NotModified. If this happens, then the cached Response\n// will be returned.\nfunc (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {\n\tcacheKey := cacheKey(req)\n\tcacheable := (req.Method == \"GET\" || req.Method == \"HEAD\") && req.Header.Get(\"range\") == \"\"\n\tvar cachedResp *http.Response\n\tif cacheable {\n\t\tcachedResp, err = CachedResponse(t.Cache, req)\n\t} else {\n\t\t// Need to invalidate an existing value\n\t\tt.Cache.Delete(cacheKey)\n\t}\n\n\ttransport := t.Transport\n\tif transport == nil {\n\t\ttransport = http.DefaultTransport\n\t}\n\n\tif cacheable && cachedResp != nil && err == nil {\n\t\tif t.MarkCachedResponses {\n\t\t\tcachedResp.Header.Set(XFromCache, \"1\")\n\t\t}\n\n\t\tif varyMatches(cachedResp, req) {\n\t\t\t// Can only use cached value if the new request doesn't Vary significantly\n\t\t\tfreshness := getFreshness(cachedResp.Header, req.Header)\n\t\t\tif freshness == fresh {\n\t\t\t\treturn cachedResp, nil\n\t\t\t}\n\n\t\t\tif freshness == stale {\n\t\t\t\tvar req2 *http.Request\n\t\t\t\t// Add validators if caller hasn't already done so\n\t\t\t\tetag := cachedResp.Header.Get(\"etag\")\n\t\t\t\tif etag != \"\" && req.Header.Get(\"etag\") == \"\" {\n\t\t\t\t\treq2 = cloneRequest(req)\n\t\t\t\t\treq2.Header.Set(\"if-none-match\", etag)\n\t\t\t\t}\n\t\t\t\tlastModified := cachedResp.Header.Get(\"last-modified\")\n\t\t\t\tif lastModified != \"\" && req.Header.Get(\"last-modified\") == \"\" {\n\t\t\t\t\tif req2 == nil {\n\t\t\t\t\t\treq2 = cloneRequest(req)\n\t\t\t\t\t}\n\t\t\t\t\treq2.Header.Set(\"if-modified-since\", lastModified)\n\t\t\t\t}\n\t\t\t\tif req2 != nil {\n\t\t\t\t\treq = req2\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresp, err = transport.RoundTrip(req)\n\t\tif err == nil && req.Method == \"GET\" && resp.StatusCode == http.StatusNotModified {\n\t\t\t// Replace the 304 response with the one from cache, but update with some new headers\n\t\t\tendToEndHeaders := getEndToEndHeaders(resp.Header)\n\t\t\tfor _, header := range endToEndHeaders {\n\t\t\t\tcachedResp.Header[header] = resp.Header[header]\n\t\t\t}\n\t\t\tresp = cachedResp\n\t\t} else if (err != nil || (cachedResp != nil && resp.StatusCode >= 500)) &&\n\t\t\treq.Method == \"GET\" && canStaleOnError(cachedResp.Header, req.Header) {\n\t\t\t// In case of transport failure and stale-if-error activated, returns cached content\n\t\t\t// when available\n\t\t\treturn cachedResp, nil\n\t\t} else {\n\t\t\tif err != nil || resp.StatusCode != http.StatusOK {\n\t\t\t\tt.Cache.Delete(cacheKey)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\treqCacheControl := parseCacheControl(req.Header)\n\t\tif _, ok := reqCacheControl[\"only-if-cached\"]; ok {\n\t\t\tresp = newGatewayTimeoutResponse(req)\n\t\t} else {\n\t\t\tresp, err = transport.RoundTrip(req)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif cacheable && canStore(parseCacheControl(req.Header), parseCacheControl(resp.Header)) {\n\t\tfor _, varyKey := range headerAllCommaSepValues(resp.Header, \"vary\") {\n\t\t\tvaryKey = http.CanonicalHeaderKey(varyKey)\n\t\t\tfakeHeader := \"X-Varied-\" + varyKey\n\t\t\treqValue := req.Header.Get(varyKey)\n\t\t\tif reqValue != \"\" {\n\t\t\t\tresp.Header.Set(fakeHeader, reqValue)\n\t\t\t}\n\t\t}\n\t\tswitch req.Method {\n\t\tcase \"GET\":\n\t\t\t// Delay caching until EOF is reached.\n\t\t\tresp.Body = &cachingReadCloser{\n\t\t\t\tR: resp.Body,\n\t\t\t\tOnEOF: func(r io.Reader) {\n\t\t\t\t\tresp := *resp\n\t\t\t\t\tresp.Body = ioutil.NopCloser(r)\n\t\t\t\t\trespBytes, err := httputil.DumpResponse(&resp, true)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tt.Cache.Set(cacheKey, respBytes)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\tdefault:\n\t\t\trespBytes, err := httputil.DumpResponse(resp, true)\n\t\t\tif err == nil {\n\t\t\t\tt.Cache.Set(cacheKey, respBytes)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tt.Cache.Delete(cacheKey)\n\t}\n\treturn resp, nil\n}\n\n// ErrNoDateHeader indicates that the HTTP headers contained no Date header.\nvar ErrNoDateHeader = errors.New(\"no Date header\")\n\n// Date parses and returns the value of the Date header.\nfunc Date(respHeaders http.Header) (date time.Time, err error) {\n\tdateHeader := respHeaders.Get(\"date\")\n\tif dateHeader == \"\" {\n\t\terr = ErrNoDateHeader\n\t\treturn\n\t}\n\n\treturn time.Parse(time.RFC1123, dateHeader)\n}\n\ntype realClock struct{}\n\nfunc (c *realClock) since(d time.Time) time.Duration {\n\treturn time.Since(d)\n}\n\ntype timer interface {\n\tsince(d time.Time) time.Duration\n}\n\nvar clock timer = &realClock{}\n\n// getFreshness will return one of fresh/stale/transparent based on the cache-control\n// values of the request and the response\n//\n// fresh indicates the response can be returned\n// stale indicates that the response needs validating before it is returned\n// transparent indicates the response should not be used to fulfil the request\n//\n// Because this is only a private cache, 'public' and 'private' in cache-control aren't\n// signficant. Similarly, smax-age isn't used.\nfunc getFreshness(respHeaders, reqHeaders http.Header) (freshness int) {\n\trespCacheControl := parseCacheControl(respHeaders)\n\treqCacheControl := parseCacheControl(reqHeaders)\n\tif _, ok := reqCacheControl[\"no-cache\"]; ok {\n\t\treturn transparent\n\t}\n\tif _, ok := respCacheControl[\"no-cache\"]; ok {\n\t\treturn stale\n\t}\n\tif _, ok := reqCacheControl[\"only-if-cached\"]; ok {\n\t\treturn fresh\n\t}\n\n\tdate, err := Date(respHeaders)\n\tif err != nil {\n\t\treturn stale\n\t}\n\tcurrentAge := clock.since(date)\n\n\tvar lifetime time.Duration\n\tvar zeroDuration time.Duration\n\n\t// If a response includes both an Expires header and a max-age directive,\n\t// the max-age directive overrides the Expires header, even if the Expires header is more restrictive.\n\tif maxAge, ok := respCacheControl[\"max-age\"]; ok {\n\t\tlifetime, err = time.ParseDuration(maxAge + \"s\")\n\t\tif err != nil {\n\t\t\tlifetime = zeroDuration\n\t\t}\n\t} else {\n\t\texpiresHeader := respHeaders.Get(\"Expires\")\n\t\tif expiresHeader != \"\" {\n\t\t\texpires, err := time.Parse(time.RFC1123, expiresHeader)\n\t\t\tif err != nil {\n\t\t\t\tlifetime = zeroDuration\n\t\t\t} else {\n\t\t\t\tlifetime = expires.Sub(date)\n\t\t\t}\n\t\t}\n\t}\n\n\tif maxAge, ok := reqCacheControl[\"max-age\"]; ok {\n\t\t// the client is willing to accept a response whose age is no greater than the specified time in seconds\n\t\tlifetime, err = time.ParseDuration(maxAge + \"s\")\n\t\tif err != nil {\n\t\t\tlifetime = zeroDuration\n\t\t}\n\t}\n\tif minfresh, ok := reqCacheControl[\"min-fresh\"]; ok {\n\t\t//  the client wants a response that will still be fresh for at least the specified number of seconds.\n\t\tminfreshDuration, err := time.ParseDuration(minfresh + \"s\")\n\t\tif err == nil {\n\t\t\tcurrentAge = time.Duration(currentAge + minfreshDuration)\n\t\t}\n\t}\n\n\tif maxstale, ok := reqCacheControl[\"max-stale\"]; ok {\n\t\t// Indicates that the client is willing to accept a response that has exceeded its expiration time.\n\t\t// If max-stale is assigned a value, then the client is willing to accept a response that has exceeded\n\t\t// its expiration time by no more than the specified number of seconds.\n\t\t// If no value is assigned to max-stale, then the client is willing to accept a stale response of any age.\n\t\t//\n\t\t// Responses served only because of a max-stale value are supposed to have a Warning header added to them,\n\t\t// but that seems like a  hassle, and is it actually useful? If so, then there needs to be a different\n\t\t// return-value available here.\n\t\tif maxstale == \"\" {\n\t\t\treturn fresh\n\t\t}\n\t\tmaxstaleDuration, err := time.ParseDuration(maxstale + \"s\")\n\t\tif err == nil {\n\t\t\tcurrentAge = time.Duration(currentAge - maxstaleDuration)\n\t\t}\n\t}\n\n\tif lifetime > currentAge {\n\t\treturn fresh\n\t}\n\n\treturn stale\n}\n\n// Returns true if either the request or the response includes the stale-if-error\n// cache control extension: https://tools.ietf.org/html/rfc5861\nfunc canStaleOnError(respHeaders, reqHeaders http.Header) bool {\n\trespCacheControl := parseCacheControl(respHeaders)\n\treqCacheControl := parseCacheControl(reqHeaders)\n\n\tvar err error\n\tlifetime := time.Duration(-1)\n\n\tif staleMaxAge, ok := respCacheControl[\"stale-if-error\"]; ok {\n\t\tif staleMaxAge != \"\" {\n\t\t\tlifetime, err = time.ParseDuration(staleMaxAge + \"s\")\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else {\n\t\t\treturn true\n\t\t}\n\t}\n\tif staleMaxAge, ok := reqCacheControl[\"stale-if-error\"]; ok {\n\t\tif staleMaxAge != \"\" {\n\t\t\tlifetime, err = time.ParseDuration(staleMaxAge + \"s\")\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif lifetime >= 0 {\n\t\tdate, err := Date(respHeaders)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\t\tcurrentAge := clock.since(date)\n\t\tif lifetime > currentAge {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc getEndToEndHeaders(respHeaders http.Header) []string {\n\t// These headers are always hop-by-hop\n\thopByHopHeaders := map[string]struct{}{\n\t\t\"Connection\":          {},\n\t\t\"Keep-Alive\":          {},\n\t\t\"Proxy-Authenticate\":  {},\n\t\t\"Proxy-Authorization\": {},\n\t\t\"Te\":                  {},\n\t\t\"Trailers\":            {},\n\t\t\"Transfer-Encoding\":   {},\n\t\t\"Upgrade\":             {},\n\t}\n\n\tfor _, extra := range strings.Split(respHeaders.Get(\"connection\"), \",\") {\n\t\t// any header listed in connection, if present, is also considered hop-by-hop\n\t\tif strings.Trim(extra, \" \") != \"\" {\n\t\t\thopByHopHeaders[http.CanonicalHeaderKey(extra)] = struct{}{}\n\t\t}\n\t}\n\tendToEndHeaders := []string{}\n\tfor respHeader := range respHeaders {\n\t\tif _, ok := hopByHopHeaders[respHeader]; !ok {\n\t\t\tendToEndHeaders = append(endToEndHeaders, respHeader)\n\t\t}\n\t}\n\treturn endToEndHeaders\n}\n\nfunc canStore(reqCacheControl, respCacheControl cacheControl) (canStore bool) {\n\tif _, ok := respCacheControl[\"no-store\"]; ok {\n\t\treturn false\n\t}\n\tif _, ok := reqCacheControl[\"no-store\"]; ok {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc newGatewayTimeoutResponse(req *http.Request) *http.Response {\n\tvar braw bytes.Buffer\n\tbraw.WriteString(\"HTTP/1.1 504 Gateway Timeout\\r\\n\\r\\n\")\n\tresp, err := http.ReadResponse(bufio.NewReader(&braw), req)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn resp\n}\n\n// cloneRequest returns a clone of the provided *http.Request.\n// The clone is a shallow copy of the struct and its Header map.\n// (This function copyright goauth2 authors: https://code.google.com/p/goauth2)\nfunc cloneRequest(r *http.Request) *http.Request {\n\t// shallow copy of the struct\n\tr2 := new(http.Request)\n\t*r2 = *r\n\t// deep copy of the Header\n\tr2.Header = make(http.Header)\n\tfor k, s := range r.Header {\n\t\tr2.Header[k] = s\n\t}\n\treturn r2\n}\n\ntype cacheControl map[string]string\n\nfunc parseCacheControl(headers http.Header) cacheControl {\n\tcc := cacheControl{}\n\tccHeader := headers.Get(\"Cache-Control\")\n\tfor _, part := range strings.Split(ccHeader, \",\") {\n\t\tpart = strings.Trim(part, \" \")\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.ContainsRune(part, '=') {\n\t\t\tkeyval := strings.Split(part, \"=\")\n\t\t\tcc[strings.Trim(keyval[0], \" \")] = strings.Trim(keyval[1], \",\")\n\t\t} else {\n\t\t\tcc[part] = \"\"\n\t\t}\n\t}\n\treturn cc\n}\n\n// headerAllCommaSepValues returns all comma-separated values (each\n// with whitespace trimmed) for header name in headers. According to\n// Section 4.2 of the HTTP/1.1 spec\n// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2),\n// values from multiple occurrences of a header should be concatenated, if\n// the header's value is a comma-separated list.\nfunc headerAllCommaSepValues(headers http.Header, name string) []string {\n\tvar vals []string\n\tfor _, val := range headers[http.CanonicalHeaderKey(name)] {\n\t\tfields := strings.Split(val, \",\")\n\t\tfor i, f := range fields {\n\t\t\tfields[i] = strings.TrimSpace(f)\n\t\t}\n\t\tvals = append(vals, fields...)\n\t}\n\treturn vals\n}\n\n// cachingReadCloser is a wrapper around ReadCloser R that calls OnEOF\n// handler with a full copy of the content read from R when EOF is\n// reached.\ntype cachingReadCloser struct {\n\t// Underlying ReadCloser.\n\tR io.ReadCloser\n\t// OnEOF is called with a copy of the content of R when EOF is reached.\n\tOnEOF func(io.Reader)\n\n\tbuf bytes.Buffer // buf stores a copy of the content of R.\n}\n\n// Read reads the next len(p) bytes from R or until R is drained. The\n// return value n is the number of bytes read. If R has no data to\n// return, err is io.EOF and OnEOF is called with a full copy of what\n// has been read so far.\nfunc (r *cachingReadCloser) Read(p []byte) (n int, err error) {\n\tn, err = r.R.Read(p)\n\tr.buf.Write(p[:n])\n\tif err == io.EOF {\n\t\tr.OnEOF(bytes.NewReader(r.buf.Bytes()))\n\t}\n\treturn n, err\n}\n\nfunc (r *cachingReadCloser) Close() error {\n\treturn r.R.Close()\n}\n\n// NewMemoryCacheTransport returns a new Transport using the in-memory cache implementation\nfunc NewMemoryCacheTransport() *Transport {\n\tc := NewMemoryCache()\n\tt := NewTransport(c)\n\treturn t\n}\n"
  },
  {
    "path": "internal/httpcache/httpcache_test.go",
    "content": "package httpcache\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"flag\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar s struct {\n\tserver    *httptest.Server\n\tclient    http.Client\n\ttransport *Transport\n\tdone      chan struct{} // Closed to unlock infinite handlers.\n}\n\ntype fakeClock struct {\n\telapsed time.Duration\n}\n\nfunc (c *fakeClock) since(t time.Time) time.Duration {\n\treturn c.elapsed\n}\n\nfunc TestMain(m *testing.M) {\n\tflag.Parse()\n\tsetup()\n\tcode := m.Run()\n\tteardown()\n\tos.Exit(code)\n}\n\nfunc setup() {\n\ttp := NewMemoryCacheTransport()\n\tclient := http.Client{Transport: tp}\n\ts.transport = tp\n\ts.client = client\n\ts.done = make(chan struct{})\n\n\tmux := http.NewServeMux()\n\ts.server = httptest.NewServer(mux)\n\n\tmux.HandleFunc(\"/\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=3600\")\n\t}))\n\n\tmux.HandleFunc(\"/method\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=3600\")\n\t\tw.Write([]byte(r.Method))\n\t}))\n\n\tmux.HandleFunc(\"/range\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlm := \"Fri, 14 Dec 2010 01:01:50 GMT\"\n\t\tif r.Header.Get(\"if-modified-since\") == lm {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"last-modified\", lm)\n\t\tif r.Header.Get(\"range\") == \"bytes=4-9\" {\n\t\t\tw.WriteHeader(http.StatusPartialContent)\n\t\t\tw.Write([]byte(\" text \"))\n\t\t\treturn\n\t\t}\n\t\tw.Write([]byte(\"Some text content\"))\n\t}))\n\n\tmux.HandleFunc(\"/nostore\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"no-store\")\n\t}))\n\n\tmux.HandleFunc(\"/etag\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tetag := \"124567\"\n\t\tif r.Header.Get(\"if-none-match\") == etag {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"etag\", etag)\n\t}))\n\n\tmux.HandleFunc(\"/lastmodified\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlm := \"Fri, 14 Dec 2010 01:01:50 GMT\"\n\t\tif r.Header.Get(\"if-modified-since\") == lm {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"last-modified\", lm)\n\t}))\n\n\tmux.HandleFunc(\"/varyaccept\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=3600\")\n\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\tw.Header().Set(\"Vary\", \"Accept\")\n\t\tw.Write([]byte(\"Some text content\"))\n\t}))\n\n\tmux.HandleFunc(\"/doublevary\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=3600\")\n\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\tw.Header().Set(\"Vary\", \"Accept, Accept-Language\")\n\t\tw.Write([]byte(\"Some text content\"))\n\t}))\n\tmux.HandleFunc(\"/2varyheaders\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=3600\")\n\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\tw.Header().Add(\"Vary\", \"Accept\")\n\t\tw.Header().Add(\"Vary\", \"Accept-Language\")\n\t\tw.Write([]byte(\"Some text content\"))\n\t}))\n\tmux.HandleFunc(\"/varyunused\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Cache-Control\", \"max-age=3600\")\n\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\tw.Header().Set(\"Vary\", \"X-Madeup-Header\")\n\t\tw.Write([]byte(\"Some text content\"))\n\t}))\n\n\tmux.HandleFunc(\"/cachederror\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tetag := \"abc\"\n\t\tif r.Header.Get(\"if-none-match\") == etag {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"etag\", etag)\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\tw.Write([]byte(\"Not found\"))\n\t}))\n\n\tupdateFieldsCounter := 0\n\tmux.HandleFunc(\"/updatefields\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"X-Counter\", strconv.Itoa(updateFieldsCounter))\n\t\tw.Header().Set(\"Etag\", `\"e\"`)\n\t\tupdateFieldsCounter++\n\t\tif r.Header.Get(\"if-none-match\") != \"\" {\n\t\t\tw.WriteHeader(http.StatusNotModified)\n\t\t\treturn\n\t\t}\n\t\tw.Write([]byte(\"Some text content\"))\n\t}))\n\n\t// Take 3 seconds to return 200 OK (for testing client timeouts).\n\tmux.HandleFunc(\"/3seconds\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ttime.Sleep(3 * time.Second)\n\t}))\n\n\tmux.HandleFunc(\"/infinite\", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-s.done:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tw.Write([]byte{0})\n\t\t\t}\n\t\t}\n\t}))\n}\n\nfunc teardown() {\n\tclose(s.done)\n\ts.server.Close()\n}\n\nfunc resetTest() {\n\ts.transport.Cache = NewMemoryCache()\n\tclock = &realClock{}\n}\n\n// TestCacheableMethod ensures that uncacheable method does not get stored\n// in cache and get incorrectly used for a following cacheable method request.\nfunc TestCacheableMethod(t *testing.T) {\n\tresetTest()\n\t{\n\t\treq, err := http.NewRequest(\"POST\", s.server.URL+\"/method\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\t_, err = io.Copy(&buf, resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got, want := buf.String(), \"POST\"; got != want {\n\t\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"response status code isn't 200 OK: %v\", resp.StatusCode)\n\t\t}\n\t}\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/method\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\t_, err = io.Copy(&buf, resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got, want := buf.String(), \"GET\"; got != want {\n\t\t\tt.Errorf(\"got wrong body %q, want %q\", got, want)\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"response status code isn't 200 OK: %v\", resp.StatusCode)\n\t\t}\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Errorf(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n}\n\nfunc TestDontServeHeadResponseToGetRequest(t *testing.T) {\n\tresetTest()\n\turl := s.server.URL + \"/\"\n\treq, err := http.NewRequest(http.MethodHead, url, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t_, err = s.client.Do(req)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq, err = http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tresp, err := s.client.Do(req)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Header.Get(XFromCache) != \"\" {\n\t\tt.Errorf(\"Cache should not match\")\n\t}\n}\n\nfunc TestDontStorePartialRangeInCache(t *testing.T) {\n\tresetTest()\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/range\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treq.Header.Set(\"range\", \"bytes=4-9\")\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\t_, err = io.Copy(&buf, resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got, want := buf.String(), \" text \"; got != want {\n\t\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t\t}\n\t\tif resp.StatusCode != http.StatusPartialContent {\n\t\t\tt.Errorf(\"response status code isn't 206 Partial Content: %v\", resp.StatusCode)\n\t\t}\n\t}\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/range\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\t_, err = io.Copy(&buf, resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got, want := buf.String(), \"Some text content\"; got != want {\n\t\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"response status code isn't 200 OK: %v\", resp.StatusCode)\n\t\t}\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Error(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/range\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\t_, err = io.Copy(&buf, resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got, want := buf.String(), \"Some text content\"; got != want {\n\t\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"response status code isn't 200 OK: %v\", resp.StatusCode)\n\t\t}\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Errorf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/range\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treq.Header.Set(\"range\", \"bytes=4-9\")\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tvar buf bytes.Buffer\n\t\t_, err = io.Copy(&buf, resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\terr = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got, want := buf.String(), \" text \"; got != want {\n\t\t\tt.Errorf(\"got %q, want %q\", got, want)\n\t\t}\n\t\tif resp.StatusCode != http.StatusPartialContent {\n\t\t\tt.Errorf(\"response status code isn't 206 Partial Content: %v\", resp.StatusCode)\n\t\t}\n\t}\n}\n\nfunc TestCacheOnlyIfBodyRead(t *testing.T) {\n\tresetTest()\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t\t// We do not read the body\n\t\tresp.Body.Close()\n\t}\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatalf(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n}\n\nfunc TestOnlyReadBodyOnDemand(t *testing.T) {\n\tresetTest()\n\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/infinite\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tresp, err := s.client.Do(req) // This shouldn't hang forever.\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tbuf := make([]byte, 10) // Only partially read the body.\n\t_, err = resp.Body.Read(buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tresp.Body.Close()\n}\n\nfunc TestGetOnlyIfCachedHit(t *testing.T) {\n\tresetTest()\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\treq, err := http.NewRequest(\"GET\", s.server.URL, nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treq.Header.Add(\"cache-control\", \"only-if-cached\")\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Fatalf(\"response status code isn't 200 OK: %v\", resp.StatusCode)\n\t\t}\n\t}\n}\n\nfunc TestGetOnlyIfCachedMiss(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Add(\"cache-control\", \"only-if-cached\")\n\tresp, err := s.client.Do(req)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.Header.Get(XFromCache) != \"\" {\n\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t}\n\tif resp.StatusCode != http.StatusGatewayTimeout {\n\t\tt.Fatalf(\"response status code isn't 504 GatewayTimeout: %v\", resp.StatusCode)\n\t}\n}\n\nfunc TestGetNoStoreRequest(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Add(\"Cache-Control\", \"no-store\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n}\n\nfunc TestGetNoStoreResponse(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/nostore\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n}\n\nfunc TestGetWithEtag(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/etag\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t\t// additional assertions to verify that 304 response is converted properly\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Fatalf(\"response status code isn't 200 OK: %v\", resp.StatusCode)\n\t\t}\n\t\tif _, ok := resp.Header[\"Connection\"]; ok {\n\t\t\tt.Fatalf(\"Connection header isn't absent\")\n\t\t}\n\t}\n}\n\nfunc TestGetWithLastModified(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/lastmodified\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n}\n\nfunc TestGetWithVary(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/varyaccept\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Set(\"Accept\", \"text/plain\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(\"Vary\") != \"Accept\" {\n\t\t\tt.Fatalf(`Vary header isn't \"Accept\": %v`, resp.Header.Get(\"Vary\"))\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n\treq.Header.Set(\"Accept\", \"text/html\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\treq.Header.Set(\"Accept\", \"\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n}\n\nfunc TestGetWithDoubleVary(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/doublevary\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Set(\"Accept\", \"text/plain\")\n\treq.Header.Set(\"Accept-Language\", \"da, en-gb;q=0.8, en;q=0.7\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(\"Vary\") == \"\" {\n\t\t\tt.Fatalf(`Vary header is blank`)\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n\treq.Header.Set(\"Accept-Language\", \"\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\treq.Header.Set(\"Accept-Language\", \"da\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n}\n\nfunc TestGetWith2VaryHeaders(t *testing.T) {\n\tresetTest()\n\t// Tests that multiple Vary headers' comma-separated lists are\n\t// merged. See https://github.com/gregjones/httpcache/issues/27.\n\tconst (\n\t\taccept         = \"text/plain\"\n\t\tacceptLanguage = \"da, en-gb;q=0.8, en;q=0.7\"\n\t)\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/2varyheaders\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Set(\"Accept\", accept)\n\treq.Header.Set(\"Accept-Language\", acceptLanguage)\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(\"Vary\") == \"\" {\n\t\t\tt.Fatalf(`Vary header is blank`)\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n\treq.Header.Set(\"Accept-Language\", \"\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\treq.Header.Set(\"Accept-Language\", \"da\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\treq.Header.Set(\"Accept-Language\", acceptLanguage)\n\treq.Header.Set(\"Accept\", \"\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t}\n\treq.Header.Set(\"Accept\", \"image/png\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"\" {\n\t\t\tt.Fatal(\"XFromCache header isn't blank\")\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n}\n\nfunc TestGetVaryUnused(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/varyunused\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treq.Header.Set(\"Accept\", \"text/plain\")\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(\"Vary\") == \"\" {\n\t\t\tt.Fatalf(`Vary header is blank`)\n\t\t}\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t}\n}\n\nfunc TestUpdateFields(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/updatefields\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar counter, counter2 string\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tcounter = resp.Header.Get(\"x-counter\")\n\t\t_, err = ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.Header.Get(XFromCache) != \"1\" {\n\t\t\tt.Fatalf(`XFromCache header isn't \"1\": %v`, resp.Header.Get(XFromCache))\n\t\t}\n\t\tcounter2 = resp.Header.Get(\"x-counter\")\n\t}\n\tif counter == counter2 {\n\t\tt.Fatalf(`both \"x-counter\" values are equal: %v %v`, counter, counter2)\n\t}\n}\n\n// This tests the fix for https://github.com/gregjones/httpcache/issues/74.\n// Previously, after validating a cached response, its StatusCode\n// was incorrectly being replaced.\nfunc TestCachedErrorsKeepStatus(t *testing.T) {\n\tresetTest()\n\treq, err := http.NewRequest(\"GET\", s.server.URL+\"/cachederror\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tio.Copy(ioutil.Discard, resp.Body)\n\t}\n\t{\n\t\tresp, err := s.client.Do(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.StatusCode != http.StatusNotFound {\n\t\t\tt.Fatalf(\"Status code isn't 404: %d\", resp.StatusCode)\n\t\t}\n\t}\n}\n\nfunc TestParseCacheControl(t *testing.T) {\n\tresetTest()\n\th := http.Header{}\n\tfor range parseCacheControl(h) {\n\t\tt.Fatal(\"cacheControl should be empty\")\n\t}\n\n\th.Set(\"cache-control\", \"no-cache\")\n\t{\n\t\tcc := parseCacheControl(h)\n\t\tif _, ok := cc[\"foo\"]; ok {\n\t\t\tt.Error(`Value \"foo\" shouldn't exist`)\n\t\t}\n\t\tnoCache, ok := cc[\"no-cache\"]\n\t\tif !ok {\n\t\t\tt.Fatalf(`\"no-cache\" value isn't set`)\n\t\t}\n\t\tif noCache != \"\" {\n\t\t\tt.Fatalf(`\"no-cache\" value isn't blank: %v`, noCache)\n\t\t}\n\t}\n\th.Set(\"cache-control\", \"no-cache, max-age=3600\")\n\t{\n\t\tcc := parseCacheControl(h)\n\t\tnoCache, ok := cc[\"no-cache\"]\n\t\tif !ok {\n\t\t\tt.Fatalf(`\"no-cache\" value isn't set`)\n\t\t}\n\t\tif noCache != \"\" {\n\t\t\tt.Fatalf(`\"no-cache\" value isn't blank: %v`, noCache)\n\t\t}\n\t\tif cc[\"max-age\"] != \"3600\" {\n\t\t\tt.Fatalf(`\"max-age\" value isn't \"3600\": %v`, cc[\"max-age\"])\n\t\t}\n\t}\n}\n\nfunc TestNoCacheRequestExpiration(t *testing.T) {\n\tresetTest()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"Cache-Control\", \"max-age=7200\")\n\n\treqHeaders := http.Header{}\n\treqHeaders.Set(\"Cache-Control\", \"no-cache\")\n\tif getFreshness(respHeaders, reqHeaders) != transparent {\n\t\tt.Fatal(\"freshness isn't transparent\")\n\t}\n}\n\nfunc TestNoCacheResponseExpiration(t *testing.T) {\n\tresetTest()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"Cache-Control\", \"no-cache\")\n\trespHeaders.Set(\"Expires\", \"Wed, 19 Apr 3000 11:43:00 GMT\")\n\n\treqHeaders := http.Header{}\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestReqMustRevalidate(t *testing.T) {\n\tresetTest()\n\t// not paying attention to request setting max-stale means never returning stale\n\t// responses, so always acting as if must-revalidate is set\n\trespHeaders := http.Header{}\n\n\treqHeaders := http.Header{}\n\treqHeaders.Set(\"Cache-Control\", \"must-revalidate\")\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestRespMustRevalidate(t *testing.T) {\n\tresetTest()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"Cache-Control\", \"must-revalidate\")\n\n\treqHeaders := http.Header{}\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestFreshExpiration(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"expires\", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123))\n\n\treqHeaders := http.Header{}\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n\n\tclock = &fakeClock{elapsed: 3 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestMaxAge(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"cache-control\", \"max-age=2\")\n\n\treqHeaders := http.Header{}\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n\n\tclock = &fakeClock{elapsed: 3 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestMaxAgeZero(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"cache-control\", \"max-age=0\")\n\n\treqHeaders := http.Header{}\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestBothMaxAge(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"cache-control\", \"max-age=2\")\n\n\treqHeaders := http.Header{}\n\treqHeaders.Set(\"cache-control\", \"max-age=0\")\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestMinFreshWithExpires(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"expires\", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123))\n\n\treqHeaders := http.Header{}\n\treqHeaders.Set(\"cache-control\", \"min-fresh=1\")\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n\n\treqHeaders = http.Header{}\n\treqHeaders.Set(\"cache-control\", \"min-fresh=2\")\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc TestEmptyMaxStale(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"cache-control\", \"max-age=20\")\n\n\treqHeaders := http.Header{}\n\treqHeaders.Set(\"cache-control\", \"max-stale\")\n\tclock = &fakeClock{elapsed: 10 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n\n\tclock = &fakeClock{elapsed: 60 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n}\n\nfunc TestMaxStaleValue(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\trespHeaders := http.Header{}\n\trespHeaders.Set(\"date\", now.Format(time.RFC1123))\n\trespHeaders.Set(\"cache-control\", \"max-age=10\")\n\n\treqHeaders := http.Header{}\n\treqHeaders.Set(\"cache-control\", \"max-stale=20\")\n\tclock = &fakeClock{elapsed: 5 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n\n\tclock = &fakeClock{elapsed: 15 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != fresh {\n\t\tt.Fatal(\"freshness isn't fresh\")\n\t}\n\n\tclock = &fakeClock{elapsed: 30 * time.Second}\n\tif getFreshness(respHeaders, reqHeaders) != stale {\n\t\tt.Fatal(\"freshness isn't stale\")\n\t}\n}\n\nfunc containsHeader(headers []string, header string) bool {\n\tfor _, v := range headers {\n\t\tif http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(header) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc TestGetEndToEndHeaders(t *testing.T) {\n\tresetTest()\n\tvar (\n\t\theaders http.Header\n\t\tend2end []string\n\t)\n\n\theaders = http.Header{}\n\theaders.Set(\"content-type\", \"text/html\")\n\theaders.Set(\"te\", \"deflate\")\n\n\tend2end = getEndToEndHeaders(headers)\n\tif !containsHeader(end2end, \"content-type\") {\n\t\tt.Fatal(`doesn't contain \"content-type\" header`)\n\t}\n\tif containsHeader(end2end, \"te\") {\n\t\tt.Fatal(`doesn't contain \"te\" header`)\n\t}\n\n\theaders = http.Header{}\n\theaders.Set(\"connection\", \"content-type\")\n\theaders.Set(\"content-type\", \"text/csv\")\n\theaders.Set(\"te\", \"deflate\")\n\tend2end = getEndToEndHeaders(headers)\n\tif containsHeader(end2end, \"connection\") {\n\t\tt.Fatal(`doesn't contain \"connection\" header`)\n\t}\n\tif containsHeader(end2end, \"content-type\") {\n\t\tt.Fatal(`doesn't contain \"content-type\" header`)\n\t}\n\tif containsHeader(end2end, \"te\") {\n\t\tt.Fatal(`doesn't contain \"te\" header`)\n\t}\n\n\theaders = http.Header{}\n\tend2end = getEndToEndHeaders(headers)\n\tif len(end2end) != 0 {\n\t\tt.Fatal(`non-zero end2end headers`)\n\t}\n\n\theaders = http.Header{}\n\theaders.Set(\"connection\", \"content-type\")\n\tend2end = getEndToEndHeaders(headers)\n\tif len(end2end) != 0 {\n\t\tt.Fatal(`non-zero end2end headers`)\n\t}\n}\n\ntype transportMock struct {\n\tresponse *http.Response\n\terr      error\n}\n\nfunc (t transportMock) RoundTrip(req *http.Request) (resp *http.Response, err error) {\n\treturn t.response, t.err\n}\n\nfunc TestStaleIfErrorRequest(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\ttmock := transportMock{\n\t\tresponse: &http.Response{\n\t\t\tStatus:     http.StatusText(http.StatusOK),\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Date\":          []string{now.Format(time.RFC1123)},\n\t\t\t\t\"Cache-Control\": []string{\"no-cache\"},\n\t\t\t},\n\t\t\tBody: ioutil.NopCloser(bytes.NewBuffer([]byte(\"some data\"))),\n\t\t},\n\t\terr: nil,\n\t}\n\ttp := NewMemoryCacheTransport()\n\ttp.Transport = &tmock\n\n\t// First time, response is cached on success\n\tr, _ := http.NewRequest(\"GET\", \"http://somewhere.com/\", nil)\n\tr.Header.Set(\"Cache-Control\", \"stale-if-error\")\n\tresp, err := tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// On failure, response is returned from the cache\n\ttmock.response = nil\n\ttmock.err = errors.New(\"some error\")\n\tresp, err = tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n}\n\nfunc TestStaleIfErrorRequestLifetime(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\ttmock := transportMock{\n\t\tresponse: &http.Response{\n\t\t\tStatus:     http.StatusText(http.StatusOK),\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Date\":          []string{now.Format(time.RFC1123)},\n\t\t\t\t\"Cache-Control\": []string{\"no-cache\"},\n\t\t\t},\n\t\t\tBody: ioutil.NopCloser(bytes.NewBuffer([]byte(\"some data\"))),\n\t\t},\n\t\terr: nil,\n\t}\n\ttp := NewMemoryCacheTransport()\n\ttp.Transport = &tmock\n\n\t// First time, response is cached on success\n\tr, _ := http.NewRequest(\"GET\", \"http://somewhere.com/\", nil)\n\tr.Header.Set(\"Cache-Control\", \"stale-if-error=100\")\n\tresp, err := tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// On failure, response is returned from the cache\n\ttmock.response = nil\n\ttmock.err = errors.New(\"some error\")\n\tresp, err = tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\n\t// Same for http errors\n\ttmock.response = &http.Response{StatusCode: http.StatusInternalServerError}\n\ttmock.err = nil\n\tresp, err = tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\n\t// If failure last more than max stale, error is returned\n\tclock = &fakeClock{elapsed: 200 * time.Second}\n\t_, err = tp.RoundTrip(r)\n\tif err != tmock.err {\n\t\tt.Fatalf(\"got err %v, want %v\", err, tmock.err)\n\t}\n}\n\nfunc TestStaleIfErrorResponse(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\ttmock := transportMock{\n\t\tresponse: &http.Response{\n\t\t\tStatus:     http.StatusText(http.StatusOK),\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Date\":          []string{now.Format(time.RFC1123)},\n\t\t\t\t\"Cache-Control\": []string{\"no-cache, stale-if-error\"},\n\t\t\t},\n\t\t\tBody: ioutil.NopCloser(bytes.NewBuffer([]byte(\"some data\"))),\n\t\t},\n\t\terr: nil,\n\t}\n\ttp := NewMemoryCacheTransport()\n\ttp.Transport = &tmock\n\n\t// First time, response is cached on success\n\tr, _ := http.NewRequest(\"GET\", \"http://somewhere.com/\", nil)\n\tresp, err := tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// On failure, response is returned from the cache\n\ttmock.response = nil\n\ttmock.err = errors.New(\"some error\")\n\tresp, err = tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n}\n\nfunc TestStaleIfErrorResponseLifetime(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\ttmock := transportMock{\n\t\tresponse: &http.Response{\n\t\t\tStatus:     http.StatusText(http.StatusOK),\n\t\t\tStatusCode: http.StatusOK,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Date\":          []string{now.Format(time.RFC1123)},\n\t\t\t\t\"Cache-Control\": []string{\"no-cache, stale-if-error=100\"},\n\t\t\t},\n\t\t\tBody: ioutil.NopCloser(bytes.NewBuffer([]byte(\"some data\"))),\n\t\t},\n\t\terr: nil,\n\t}\n\ttp := NewMemoryCacheTransport()\n\ttp.Transport = &tmock\n\n\t// First time, response is cached on success\n\tr, _ := http.NewRequest(\"GET\", \"http://somewhere.com/\", nil)\n\tresp, err := tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// On failure, response is returned from the cache\n\ttmock.response = nil\n\ttmock.err = errors.New(\"some error\")\n\tresp, err = tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\n\t// If failure last more than max stale, error is returned\n\tclock = &fakeClock{elapsed: 200 * time.Second}\n\t_, err = tp.RoundTrip(r)\n\tif err != tmock.err {\n\t\tt.Fatalf(\"got err %v, want %v\", err, tmock.err)\n\t}\n}\n\n// This tests the fix for https://github.com/gregjones/httpcache/issues/74.\n// Previously, after a stale response was used after encountering an error,\n// its StatusCode was being incorrectly replaced.\nfunc TestStaleIfErrorKeepsStatus(t *testing.T) {\n\tresetTest()\n\tnow := time.Now()\n\ttmock := transportMock{\n\t\tresponse: &http.Response{\n\t\t\tStatus:     http.StatusText(http.StatusNotFound),\n\t\t\tStatusCode: http.StatusNotFound,\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Date\":          []string{now.Format(time.RFC1123)},\n\t\t\t\t\"Cache-Control\": []string{\"no-cache\"},\n\t\t\t},\n\t\t\tBody: ioutil.NopCloser(bytes.NewBuffer([]byte(\"some data\"))),\n\t\t},\n\t\terr: nil,\n\t}\n\ttp := NewMemoryCacheTransport()\n\ttp.Transport = &tmock\n\n\t// First time, response is cached on success\n\tr, _ := http.NewRequest(\"GET\", \"http://somewhere.com/\", nil)\n\tr.Header.Set(\"Cache-Control\", \"stale-if-error\")\n\tresp, err := tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\t_, err = ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// On failure, response is returned from the cache\n\ttmock.response = nil\n\ttmock.err = errors.New(\"some error\")\n\tresp, err = tp.RoundTrip(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp == nil {\n\t\tt.Fatal(\"resp is nil\")\n\t}\n\tif resp.StatusCode != http.StatusNotFound {\n\t\tt.Fatalf(\"Status wasn't 404: %d\", resp.StatusCode)\n\t}\n}\n\n// Test that http.Client.Timeout is respected when cache transport is used.\n// That is so as long as request cancellation is propagated correctly.\n// In the past, that required CancelRequest to be implemented correctly,\n// but modern http.Client uses Request.Cancel (or request context) instead,\n// so we don't have to do anything.\nfunc TestClientTimeout(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"skipping timeout test in short mode\") // Because it takes at least 3 seconds to run.\n\t}\n\tresetTest()\n\tclient := &http.Client{\n\t\tTransport: NewMemoryCacheTransport(),\n\t\tTimeout:   time.Second,\n\t}\n\tstarted := time.Now()\n\tresp, err := client.Get(s.server.URL + \"/3seconds\")\n\ttaken := time.Since(started)\n\tif err == nil {\n\t\tt.Error(\"got nil error, want timeout error\")\n\t}\n\tif resp != nil {\n\t\tt.Error(\"got non-nil resp, want nil resp\")\n\t}\n\tif taken >= 2*time.Second {\n\t\tt.Error(\"client.Do took 2+ seconds, want < 2 seconds\")\n\t}\n}\n"
  },
  {
    "path": "internal/httpcache/test/test.go",
    "content": "package test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"encr.dev/internal/httpcache\"\n)\n\n// Cache excercises a httpcache.Cache implementation.\nfunc Cache(t *testing.T, cache httpcache.Cache) {\n\tkey := \"testKey\"\n\t_, ok := cache.Get(key)\n\tif ok {\n\t\tt.Fatal(\"retrieved key before adding it\")\n\t}\n\n\tval := []byte(\"some bytes\")\n\tcache.Set(key, val)\n\n\tretVal, ok := cache.Get(key)\n\tif !ok {\n\t\tt.Fatal(\"could not retrieve an element we just added\")\n\t}\n\tif !bytes.Equal(retVal, val) {\n\t\tt.Fatal(\"retrieved a different value than what we put in\")\n\t}\n\n\tcache.Delete(key)\n\n\t_, ok = cache.Get(key)\n\tif ok {\n\t\tt.Fatal(\"deleted key still present\")\n\t}\n}\n"
  },
  {
    "path": "internal/httpcache/test/test_test.go",
    "content": "package test_test\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/internal/httpcache\"\n\t\"encr.dev/internal/httpcache/test\"\n)\n\nfunc TestMemoryCache(t *testing.T) {\n\ttest.Cache(t, httpcache.NewMemoryCache())\n}\n"
  },
  {
    "path": "internal/lookpath/lookpath.go",
    "content": "/*\nThis package contains code from https://github.com/mvdan/sh.\n\nCopyright (c) 2016, Daniel Martí. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of the copyright holder nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n\npackage lookpath\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n)\n\nfunc checkStat(dir, file string, checkExec bool) (string, error) {\n\tif !filepath.IsAbs(file) {\n\t\tfile = filepath.Join(dir, file)\n\t}\n\tinfo, err := os.Stat(file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tm := info.Mode()\n\tif m.IsDir() {\n\t\treturn \"\", fmt.Errorf(\"is a directory\")\n\t}\n\tif checkExec && runtime.GOOS != \"windows\" && m&0o111 == 0 {\n\t\treturn \"\", fmt.Errorf(\"permission denied\")\n\t}\n\treturn file, nil\n}\n\nfunc winHasExt(file string) bool {\n\ti := strings.LastIndex(file, \".\")\n\tif i < 0 {\n\t\treturn false\n\t}\n\treturn strings.LastIndexAny(file, `:\\/`) < i\n}\n\n// findExecutable returns the path to an existing executable file.\nfunc findExecutable(dir, file string, exts []string) (string, error) {\n\tif len(exts) == 0 {\n\t\t// non-windows\n\t\treturn checkStat(dir, file, true)\n\t}\n\tif winHasExt(file) {\n\t\tif file, err := checkStat(dir, file, true); err == nil {\n\t\t\treturn file, nil\n\t\t}\n\t}\n\tfor _, e := range exts {\n\t\tf := file + e\n\t\tif f, err := checkStat(dir, f, true); err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"not found\")\n}\n\n// findFile returns the path to an existing file.\nfunc findFile(dir, file string, _ []string) (string, error) {\n\treturn checkStat(dir, file, false)\n}\n\n// InDir is similar to [os/exec.LookPath], with the difference that it uses the\n// provided environment. env is used to fetch relevant environment variables\n// such as PWD and PATH.\n//\n// If no error is returned, the returned path must be valid.\nfunc InDir(cwd string, env []string, file string) (string, error) {\n\tif filepath.IsAbs(file) {\n\t\treturn file, nil\n\t}\n\n\tupper := runtime.GOOS == \"windows\"\n\tenvs := listEnvironWithUpper(upper, env...)\n\treturn lookPathDir(cwd, envs, file, findExecutable)\n}\n\n// findAny defines a function to pass to lookPathDir.\ntype findAny = func(dir string, file string, exts []string) (string, error)\n\nfunc lookPathDir(cwd string, env listEnviron, file string, find findAny) (string, error) {\n\tif find == nil {\n\t\tpanic(\"no find function found\")\n\t}\n\n\tpathList := filepath.SplitList(env.Get(\"PATH\"))\n\tif len(pathList) == 0 {\n\t\tpathList = []string{\"\"}\n\t}\n\tchars := `/`\n\tif runtime.GOOS == \"windows\" {\n\t\tchars = `:\\/`\n\t}\n\texts := pathExts(env)\n\tif strings.ContainsAny(file, chars) {\n\t\treturn find(cwd, file, exts)\n\t}\n\tfor _, elem := range pathList {\n\t\tvar path string\n\t\tswitch elem {\n\t\tcase \"\", \".\":\n\t\t\t// otherwise \"foo\" won't be \"./foo\"\n\t\t\tpath = \".\" + string(filepath.Separator) + file\n\t\tdefault:\n\t\t\tpath = filepath.Join(elem, file)\n\t\t}\n\t\tif f, err := find(cwd, path, exts); err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"%q: executable file not found in $PATH\", file)\n}\n\nfunc pathExts(env listEnviron) []string {\n\tif runtime.GOOS != \"windows\" {\n\t\treturn nil\n\t}\n\tpathext := env.Get(\"PATHEXT\")\n\tif pathext == \"\" {\n\t\treturn []string{\".com\", \".exe\", \".bat\", \".cmd\"}\n\t}\n\tvar exts []string\n\tfor _, e := range strings.Split(strings.ToLower(pathext), `;`) {\n\t\tif e == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif e[0] != '.' {\n\t\t\te = \".\" + e\n\t\t}\n\t\texts = append(exts, e)\n\t}\n\treturn exts\n}\n\n// listEnvironWithUpper implements ListEnviron, but letting the tests specify\n// whether to uppercase all names or not.\nfunc listEnvironWithUpper(upper bool, pairs ...string) listEnviron {\n\tlist := slices.Clone(pairs)\n\tif upper {\n\t\t// Uppercase before sorting, so that we can remove duplicates\n\t\t// without the need for linear search nor a map.\n\t\tfor i, s := range list {\n\t\t\tif sep := strings.IndexByte(s, '='); sep > 0 {\n\t\t\t\tlist[i] = strings.ToUpper(s[:sep]) + s[sep:]\n\t\t\t}\n\t\t}\n\t}\n\n\tslices.SortStableFunc(list, func(a, b string) int {\n\t\tisep := strings.IndexByte(a, '=')\n\t\tjsep := strings.IndexByte(b, '=')\n\t\tif isep < 0 {\n\t\t\tisep = 0\n\t\t} else {\n\t\t\tisep += 1\n\t\t}\n\t\tif jsep < 0 {\n\t\t\tjsep = 0\n\t\t} else {\n\t\t\tjsep += 1\n\t\t}\n\t\treturn strings.Compare(a[:isep], b[:jsep])\n\t})\n\n\tlast := \"\"\n\tfor i := 0; i < len(list); {\n\t\ts := list[i]\n\t\tsep := strings.IndexByte(s, '=')\n\t\tif sep <= 0 {\n\t\t\t// invalid element; remove it\n\t\t\tlist = append(list[:i], list[i+1:]...)\n\t\t\tcontinue\n\t\t}\n\t\tname := s[:sep]\n\t\tif last == name {\n\t\t\t// duplicate; the last one wins\n\t\t\tlist = append(list[:i-1], list[i:]...)\n\t\t\tcontinue\n\t\t}\n\t\tlast = name\n\t\ti++\n\t}\n\treturn listEnviron(list)\n}\n\n// listEnviron is a sorted list of \"name=value\" strings.\ntype listEnviron []string\n\nfunc (l listEnviron) Get(name string) string {\n\teqpos := len(name)\n\tendpos := len(name) + 1\n\ti, ok := slices.BinarySearchFunc(l, name, func(l, name string) int {\n\t\tif len(l) < endpos {\n\t\t\t// Too short; see if we are before or after the name.\n\t\t\treturn strings.Compare(l, name)\n\t\t}\n\t\t// Compare the name prefix, then the equal character.\n\t\tc := strings.Compare(l[:eqpos], name)\n\t\teq := l[eqpos]\n\t\tif c == 0 {\n\t\t\treturn cmp.Compare(eq, '=')\n\t\t}\n\t\treturn c\n\t})\n\tif ok {\n\t\treturn l[i][endpos:]\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/optracker/async.go",
    "content": "package optracker\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\ntype AsyncBuildJobs struct {\n\tctx        context.Context\n\tcancelCtx  context.CancelFunc\n\tm          sync.Mutex\n\twait       sync.WaitGroup\n\tfirstError error\n\ttracker    *OpTracker\n\tstart      time.Time\n\tappID      string\n}\n\nfunc (a *AsyncBuildJobs) Tracker() *OpTracker {\n\treturn a.tracker\n}\n\nfunc NewAsyncBuildJobs(ctx context.Context, appID string, tracker *OpTracker) *AsyncBuildJobs {\n\tctx, cancelCtx := context.WithCancel(ctx)\n\n\treturn &AsyncBuildJobs{\n\t\tctx:       ctx,\n\t\tcancelCtx: cancelCtx,\n\t\tappID:     appID,\n\t\ttracker:   tracker,\n\t\tstart:     time.Now(),\n\t}\n}\n\nfunc (a *AsyncBuildJobs) Go(description string, track bool, minDuration time.Duration, f func(ctx context.Context) error) {\n\ta.wait.Add(1)\n\n\ttrackerID := NoOperationID\n\tif track && a.tracker != nil {\n\t\ttrackerID = a.tracker.Add(description, a.start)\n\t}\n\n\tgo func() {\n\t\tdefer a.wait.Done()\n\n\t\tlog.Info().Str(\"app_id\", a.appID).Str(\"job\", description).Msg(\"starting build job\")\n\t\tif err := f(a.ctx); err != nil {\n\t\t\t// If the context was canceled, it probably means the error was due to that.\n\t\t\tif a.ctx.Err() != nil {\n\t\t\t\tif a.tracker != nil {\n\t\t\t\t\ta.tracker.Cancel(trackerID)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Err(err).Str(\"app_id\", a.appID).Str(\"job\", description).Msg(\"build job failed\")\n\t\t\t\tif a.tracker != nil {\n\t\t\t\t\ta.tracker.Fail(trackerID, err)\n\t\t\t\t}\n\t\t\t\ta.recordError(err)\n\t\t\t}\n\t\t} else {\n\t\t\tif a.tracker != nil {\n\t\t\t\ta.tracker.Done(trackerID, minDuration)\n\t\t\t}\n\t\t\tlog.Info().Str(\"app_id\", a.appID).Str(\"job\", description).Msg(\"build job finished\")\n\t\t}\n\t}()\n}\n\nfunc (a *AsyncBuildJobs) Wait() error {\n\ta.wait.Wait()\n\treturn a.firstError\n}\n\nfunc (a *AsyncBuildJobs) recordError(err error) {\n\ta.m.Lock()\n\tdefer a.m.Unlock()\n\n\ta.cancelCtx()\n\n\tif a.firstError == nil {\n\t\ta.firstError = err\n\t}\n}\n"
  },
  {
    "path": "internal/optracker/optracker.go",
    "content": "package optracker\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"runtime\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/logrusorgru/aurora/v3\"\n\n\t\"encr.dev/pkg/ansi\"\n\t\"encr.dev/pkg/errlist\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\ntype OutputStream interface {\n\tSend(*daemonpb.CommandMessage) error\n}\n\nfunc New(w io.Writer, stream OutputStream) *OpTracker {\n\treturn &OpTracker{\n\t\tw:      w,\n\t\tstream: stream,\n\t}\n}\n\ntype OpTracker struct {\n\tmu          sync.Mutex\n\tops         []*slowOp\n\tw           io.Writer\n\tstarted     bool\n\tquit        bool // quit indicates that the tracker has been stopped (this should only be set by AllDone)\n\tsavedCursor sync.Once\n\tstream      OutputStream\n}\n\ntype OperationID int\n\nconst NoOperationID OperationID = -1\n\n// AllDone marks all ops as done.\n// This function is safe to call on a Nil OpTracker.\nfunc (t *OpTracker) AllDone() {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\n\t// If we've already quit, don't do anything\n\tif t.quit == true {\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\tfor _, o := range t.ops {\n\t\tif o.done.IsZero() || o.done.After(now) {\n\t\t\to.done = now\n\t\t}\n\t\tif o.start.After(now) {\n\t\t\to.start = now\n\t\t}\n\t}\n\tt.quit = true\n\tt.refresh()\n}\n\n// Add creates a new item on the operations tracker returning the ID for that op.\n// minStart is the time at which the tracker will start to show the task as in progress.\n//\n// This function is safe to call on a Nil OpTracker and will no-op in that case\nfunc (t *OpTracker) Add(msg string, minStart time.Time) OperationID {\n\tif t == nil {\n\t\treturn NoOperationID\n\t}\n\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tid := OperationID(len(t.ops))\n\n\tstart := time.Now()\n\tif start.Before(minStart) {\n\t\tstart = minStart\n\t}\n\top := &slowOp{msg: msg, start: start}\n\tt.ops = append(t.ops, op)\n\tt.refresh()\n\n\tif !t.started {\n\t\tgo t.spin()\n\t\tt.started = true\n\t}\n\n\treturn id\n}\n\n// Done marks the given operation as done\n//\n// This function is safe to call on a Nil OpTracker and will no-op in that case\nfunc (t *OpTracker) Done(id OperationID, minDuration time.Duration) {\n\tif t == nil || id == NoOperationID {\n\t\treturn\n\t}\n\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\to := t.ops[id]\n\n\tdone := time.Now()\n\tif a := o.start.Add(minDuration); a.After(done) {\n\t\tdone = a\n\t}\n\to.done = done\n\tt.refresh()\n}\n\nvar ErrCanceled = errors.New(\"operation canceled\")\n\n// Fail marks the operation as failed with the given error\n//\n// This function is safe to call on a Nil OpTracker and will no-op in that case\nfunc (t *OpTracker) Fail(id OperationID, err error) {\n\tif t == nil || id == NoOperationID {\n\t\treturn\n\t}\n\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tif !t.ops[id].done.IsZero() {\n\t\treturn\n\t}\n\tt.ops[id].err = err\n\tt.ops[id].done = time.Now()\n\tt.refresh()\n}\n\n// Cancel marks the operation as canceled.\n// It is equivalent to t.Fail(id, ErrCanceled).\nfunc (t *OpTracker) Cancel(id OperationID) {\n\tt.Fail(id, ErrCanceled)\n}\n\n// refresh refreshes the display by writing to t.w.\n// The mutex must be held by the caller.\nfunc (t *OpTracker) refresh() {\n\tt.savedCursor.Do(func() {\n\t\tfmt.Fprint(t.w, ansi.SaveCursorPosition)\n\t})\n\tfmt.Fprint(t.w, ansi.RestoreCursorPosition+ansi.ClearScreen(ansi.CursorToBottom))\n\n\tnow := time.Now()\n\n\t// Sort ops by start time\n\tops := make([]*slowOp, len(t.ops))\n\tcopy(ops, t.ops)\n\tsort.Slice(ops, func(i, j int) bool {\n\t\treturn ops[i].start.Before(ops[j].start)\n\t})\n\n\tfor _, o := range ops {\n\t\tstarted := o.start.Before(now)\n\t\tdone := !o.done.IsZero() && o.done.Before(now)\n\t\tif !started && !done {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar msg aurora.Value\n\t\tformat := \"  %s %s... \"\n\t\tswitch {\n\t\tcase done && o.err != nil:\n\t\t\tif errors.Is(o.err, ErrCanceled) {\n\t\t\t\tmsg = aurora.Yellow(fmt.Sprintf(format+\"Canceled\", canceled, o.msg))\n\t\t\t} else {\n\t\t\t\tif errlist := errlist.Convert(o.err); errlist != nil {\n\t\t\t\t\tif len(errlist.List) > 0 {\n\t\t\t\t\t\tmsg = aurora.Red(fmt.Sprintf(format+\"Failed: %v\", fail, o.msg, errlist.List[0].Title()))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmsg = aurora.Red(fmt.Sprintf(format+\"Failed: %v\", fail, o.msg, errlist))\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tmsg = aurora.Red(fmt.Sprintf(format+\"Failed: %v\", fail, o.msg, o.err))\n\t\t\t\t}\n\t\t\t}\n\t\tcase done && o.err == nil:\n\t\t\tmsg = aurora.Green(fmt.Sprintf(format+\"Done!\", success, o.msg))\n\t\tcase !done:\n\t\t\tmsg = aurora.Cyan(fmt.Sprintf(format, spinner[o.spinIdx], o.msg))\n\t\t\to.spinIdx = (o.spinIdx + 1) % len(spinner)\n\t\t}\n\t\tstr := msg.String()\n\t\tfmt.Fprintf(t.w, \"%s%s%s\\n\",\n\t\t\tansi.MoveCursorLeft(1000),\n\t\t\tansi.ClearLine(ansi.WholeLine),\n\t\t\tstr,\n\t\t)\n\t}\n}\n\nfunc (t *OpTracker) spin() {\n\trefresh := 100 * time.Millisecond\n\tif runtime.GOOS == \"windows\" {\n\t\t// Window's terminal is quite slow at rendering.\n\t\t// Reduce the refresh rate to avoid excessive flickering.\n\t\trefresh = 250 * time.Millisecond\n\t}\n\tfor {\n\t\ttime.Sleep(refresh)\n\t\t(func() {\n\t\t\tt.mu.Lock()\n\t\t\tdefer t.mu.Unlock()\n\t\t\tif !t.quit {\n\t\t\t\tt.refresh()\n\t\t\t}\n\t\t})()\n\t}\n\n}\n\ntype slowOp struct {\n\tmsg     string\n\terr     error\n\tspinIdx int\n\tstart   time.Time\n\tdone    time.Time\n}\n\nvar (\n\tsuccess  = \"✔\"\n\tfail     = \"❌\"\n\tcanceled = \"⚠️\"\n\tspinner  = []string{\"⠋\", \"⠙\", \"⠚\", \"⠒\", \"⠂\", \"⠂\", \"⠒\", \"⠲\", \"⠴\", \"⠦\", \"⠖\", \"⠒\", \"⠐\", \"⠐\", \"⠒\", \"⠓\", \"⠋\"}\n)\n"
  },
  {
    "path": "internal/userconfig/config.go",
    "content": "package userconfig\n\n// Config describes the configuration structure we support.\ntype Config struct {\n\t// Whether to open the Local Development Dashboard in the browser on `encore run`.\n\t// If set to \"auto\", the browser will be opened if the dashboard is not already open.\n\tRunBrowser string `koanf:\"run.browser\" oneof:\"always,never,auto\" default:\"auto\"`\n\n\t// Always choose this tool when creating an app or when initializing llm tools\n\t// for an existing app, unless overriden via --llm-rules flag on command line.\n\tLLMRules string `koanf:\"llm_rules\" oneof:\",cursor,claudcode,vscode,agentsmd,zed\" default:\"\"`\n}\n"
  },
  {
    "path": "internal/userconfig/def.go",
    "content": "package userconfig\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n)\n\nfunc (c *Config) GetByKey(key string) (v Value, ok bool) {\n\tval := reflect.ValueOf(c).Elem()\n\tdesc, ok := descs[key]\n\tif !ok {\n\t\treturn Value{}, false\n\t}\n\n\tf := val.FieldByName(desc.FieldName)\n\tif !f.IsValid() {\n\t\treturn Value{}, false\n\t}\n\n\treturn Value{Val: f.Interface(), Type: desc.Type}, true\n}\n\nfunc (c *Config) Render() string {\n\tvar buf strings.Builder\n\tfor _, key := range configKeys {\n\t\tv, ok := c.GetByKey(key)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tbuf.WriteString(fmt.Sprintf(\"%s: %s\\n\", key, v))\n\t}\n\treturn buf.String()\n}\n\nvar configKeys = (func() []string {\n\tkeys := slices.Collect(maps.Keys(descs))\n\tsort.Strings(keys)\n\treturn keys\n})()\n\nfunc GetType(key string) (Type, bool) {\n\ttyp, ok := descs[key]\n\treturn typ.Type, ok\n}\n\nfunc Keys() []string {\n\treturn configKeys\n}\n"
  },
  {
    "path": "internal/userconfig/docs.go",
    "content": "package userconfig\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n//go:generate go run ./gendocs\n\nfunc CLIDocs() string {\n\tvar buf strings.Builder\n\tfor _, key := range configKeys {\n\t\tdesc := descs[key]\n\t\tdoc := desc.Doc\n\t\tfmt.Fprintf(&buf, \"%s (%s)\\n\", key, desc.Type.Kind.String())\n\n\t\tif doc != \"\" {\n\t\t\trem := doc\n\t\t\tfor rem != \"\" {\n\t\t\t\tvar line string\n\t\t\t\tif idx := strings.IndexByte(rem, '\\n'); idx != -1 {\n\t\t\t\t\tline = rem[:idx]\n\t\t\t\t\trem = rem[idx+1:]\n\t\t\t\t} else {\n\t\t\t\t\tline = rem\n\t\t\t\t\trem = \"\"\n\t\t\t\t}\n\t\t\t\tbuf.WriteString(\"  \")\n\t\t\t\tbuf.WriteString(line)\n\t\t\t\tbuf.WriteByte('\\n')\n\t\t\t}\n\t\t} else {\n\t\t\tbuf.WriteString(\"  No documentation available.\\n\")\n\t\t}\n\n\t\tbuf.WriteByte('\\n')\n\n\t\tdidWriteMore := false\n\t\tif desc.Type.Default != nil {\n\t\t\tfmt.Fprintf(&buf, \"  Default: %v\\n\", RenderValue(*desc.Type.Default))\n\t\t\tdidWriteMore = true\n\t\t}\n\t\tif len(desc.Type.Oneof) > 0 {\n\t\t\tfmt.Fprintf(&buf, \"  Must be one of: %v\\n\", RenderOneof(desc.Type.Oneof))\n\t\t\tdidWriteMore = true\n\t\t}\n\n\t\t// Add an extra newline if we wrote validation details.\n\t\tif didWriteMore {\n\t\t\tbuf.WriteByte('\\n')\n\t\t}\n\t}\n\n\treturn buf.String()\n}\n\n// bt renders a backtick-enclosed string.\nfunc bt(val string) string {\n\treturn fmt.Sprintf(\"`%s`\", val)\n}\n\nvar markdownHeader = `\nThe Encore CLI has a number of configuration options to customize its behavior.\n\nConfiguration options can be set both for individual Encore applications, as well as\nglobally for the local user.\n\nConfiguration options can be set using ` + bt(\"encore config <key> <value>\") + `,\nand options can similarly be read using ` + bt(\"encore config <key>\") + `.\n\nWhen running ` + bt(\"encore config\") + ` within an Encore application, it automatically\nsets and gets configuration for that application.\n\nTo set or get global configuration, use the ` + bt(\"--global\") + ` flag.\n\n## Configuration files\n\nThe configuration is stored in one ore more TOML files on the filesystem.\n\nThe configuration is read from the following files, in order:\n\n### Global configuration\n* ` + bt(\"$XDG_CONFIG_HOME/encore/config\") + `\n* ` + bt(\"$HOME/.config/encore/config\") + `\n* ` + bt(\"$HOME/.encoreconfig\") + `\n\n### Application-specific configuration\n* ` + bt(\"$APP_ROOT/.encore/config\") + `\n\nWhere ` + bt(\"$APP_ROOT\") + ` is the directory containing the ` + bt(\"encore.app\") + ` file.\n\nThe files are read and merged, in the order defined above, with latter files taking precedence over earlier files.\n\n## Configuration options\n\n`\n\nfunc MarkdownDocs() string {\n\tvar buf strings.Builder\n\n\tbuf.WriteString(markdownHeader)\n\n\tfor _, key := range configKeys {\n\t\tdesc := descs[key]\n\t\tdoc := desc.Doc\n\n\t\tfmt.Fprintf(&buf, \"#### %s\\n\", key)\n\t\tfmt.Fprintf(&buf, \"Type: %s<br/>\\n\", desc.Type.Kind.String())\n\t\tif desc.Type.Default != nil {\n\t\t\tfmt.Fprintf(&buf, \"Default: %v<br/>\\n\", RenderValue(*desc.Type.Default))\n\t\t}\n\t\tif len(desc.Type.Oneof) > 0 {\n\t\t\tfmt.Fprintf(&buf, \"Must be one of: %v\\n\", RenderOneof(desc.Type.Oneof))\n\t\t}\n\t\tbuf.WriteByte('\\n')\n\n\t\tif doc != \"\" {\n\t\t\tbuf.WriteString(doc)\n\t\t} else {\n\t\t\tbuf.WriteString(\"No documentation available.\\n\")\n\t\t}\n\n\t\tbuf.WriteByte('\\n')\n\t}\n\n\treturn buf.String()\n}\n"
  },
  {
    "path": "internal/userconfig/files.go",
    "content": "package userconfig\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"encr.dev/internal/goldfish\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/knadh/koanf/parsers/toml/v2\"\n\t\"github.com/knadh/koanf/providers/file\"\n\t\"github.com/knadh/koanf/providers/rawbytes\"\n\t\"github.com/knadh/koanf/v2\"\n)\n\nconst globalCacheKey = \"#global#\"\n\nvar (\n\tgoldfishMu sync.Mutex\n\tgoldfishes = make(map[string]*Cached)\n)\n\ntype Cached = goldfish.Cache[*Config]\n\nfunc ForApp(appRoot string) *Cached {\n\tappRoot = filepath.Clean(appRoot)\n\tpaths := slices.Clone(userPaths)\n\tpaths = append(paths, appFilePath(appRoot))\n\treturn forCacheKey(appRoot, paths)\n}\n\nfunc Global() *Cached {\n\treturn forCacheKey(globalCacheKey, userPaths)\n}\n\nfunc forCacheKey(key string, paths []string) *Cached {\n\tgoldfishMu.Lock()\n\tdefer goldfishMu.Unlock()\n\n\tif c, ok := goldfishes[key]; ok {\n\t\treturn c\n\t}\n\n\tc := goldfish.New(1*time.Second, func() (*Config, error) {\n\t\treturn newInstance(paths...)\n\t})\n\tgoldfishes[key] = c\n\treturn c\n}\n\nfunc appFilePath(appRoot string) string {\n\treturn filepath.Join(appRoot, \".encore\", \"config\")\n}\n\nvar userPaths []string = func() []string {\n\tvar paths []string\n\n\tconfigHome := os.Getenv(\"XDG_CONFIG_HOME\")\n\tif configHome != \"\" {\n\t\tpaths = append(paths, filepath.Join(configHome, \"encore\", \"config\"))\n\t}\n\n\tif u, err := user.Current(); err == nil {\n\t\tif configHome == \"\" {\n\t\t\tpaths = append(paths, filepath.Join(u.HomeDir, \".config\", \"encore\", \"config\"))\n\t\t}\n\t\tpaths = append(paths, filepath.Join(u.HomeDir, \".encoreconfig\"))\n\t}\n\n\treturn paths\n}()\n\nvar tomlParser = toml.Parser()\n\nfunc newInstance(paths ...string) (*Config, error) {\n\tk := koanf.New(\".\")\n\n\tfor _, path := range paths {\n\t\tf := file.Provider(path)\n\t\terr := k.Load(f, tomlParser)\n\t\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, errors.Wrap(err, \"unable to parse config file\")\n\t\t}\n\t}\n\n\tcfg := &Config{}\n\terr := k.UnmarshalWithConf(\"\", cfg, koanf.UnmarshalConf{\n\t\tTag:       \"koanf\",\n\t\tFlatPaths: true,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to unmarshal config\")\n\t}\n\treturn cfg, nil\n}\n\nfunc validateConfig(data []byte) error {\n\tk := koanf.New(\".\")\n\treturn k.Load(rawbytes.Provider(data), tomlParser)\n}\n"
  },
  {
    "path": "internal/userconfig/gendocs/gendocs.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/internal/userconfig\"\n\t\"encr.dev/pkg/xos\"\n)\n\nfunc main() {\n\trepoRoot := resolveRepoRoot()\n\tdocsDir := filepath.Join(repoRoot, \"docs\")\n\n\tfor _, lang := range []string{\"go\", \"ts\"} {\n\t\tdocs := generateDocs(lang)\n\t\tdst := filepath.Join(docsDir, lang, \"cli\", \"config-reference.md\")\n\t\tif err := xos.WriteFile(dst, []byte(docs), 0644); err != nil {\n\t\t\tlog.Fatalf(\"error writing %s docs file: %v\\n\", lang, err)\n\t\t}\n\t}\n\tlog.Printf(\"successfully regenerated docs\")\n}\n\nfunc generateDocs(lang string) string {\n\treturn docsHeader(lang) + \"\\n\" + userconfig.MarkdownDocs()\n}\n\nfunc docsHeader(lang string) string {\n\treturn fmt.Sprintf(`---\nseotitle: Encore CLI Configuration Options\nseodesc: Configuration options to customize the behavior of the Encore CLI.\ntitle: Configuration Reference\nsubtitle: Configuration options to customize the behavior of the Encore CLI.\nlang: %s\n---\n`, lang)\n}\n\nfunc resolveRepoRoot() string {\n\t// Use `git rev-parse --show-toplevel` to get the root of the repository\n\tcmd := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\")\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tlog.Fatalf(\"Error running git rev-parse: %v\\n\", err)\n\t}\n\treturn filepath.Clean(strings.TrimSpace(string(out)))\n}\n"
  },
  {
    "path": "internal/userconfig/reflect.go",
    "content": "package userconfig\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/doc\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/fatih/structtag\"\n)\n\nfunc keyForField(f *reflect.StructField) (string, error) {\n\ttags, err := structtag.Parse(string(f.Tag))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttag, err := tags.Get(\"koanf\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tkey := tag.Name\n\tif key == \"\" {\n\t\treturn \"\", errors.New(\"empty key\")\n\t}\n\treturn key, nil\n}\n\ntype keyDesc struct {\n\tDoc       string\n\tType      Type\n\tFieldName string // field name in the Config struct\n}\n\nfunc newKeyDesc(f *reflect.StructField) (key string, desc keyDesc, err error) {\n\ttags, err := structtag.Parse(string(f.Tag))\n\tif err != nil {\n\t\treturn \"\", keyDesc{}, err\n\t}\n\ttag, err := tags.Get(\"koanf\")\n\tif err != nil {\n\t\treturn \"\", keyDesc{}, errors.Wrap(err, \"failed to get koanf tag\")\n\t}\n\tkey = tag.Name\n\tif key == \"\" {\n\t\treturn \"\", keyDesc{}, errors.New(\"empty key\")\n\t}\n\n\tkind, ok := kindFromReflect(f.Type.Kind())\n\tif !ok {\n\t\treturn \"\", keyDesc{}, errors.Errorf(\"unsupported type %v\", f.Type)\n\t}\n\n\tty := Type{Kind: kind}\n\n\t// Do we have a default?\n\tif def, _ := tags.Get(\"default\"); def != nil {\n\t\tval, err := kind.parseValue(def.Name)\n\t\tif err != nil {\n\t\t\treturn \"\", keyDesc{}, errors.Wrap(err, \"parse default value\")\n\t\t}\n\t\tty.Default = &val\n\t}\n\n\t// Do we have a oneof?\n\tif tag := f.Tag.Get(\"oneof\"); tag != \"\" {\n\t\tvar oneof []any\n\t\tfor _, part := range strings.Split(tag, \",\") {\n\t\t\tval, err := kind.parseValue(part)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", keyDesc{}, errors.Wrap(err, \"parse oneof value\")\n\t\t\t}\n\t\t\toneof = append(oneof, val)\n\t\t}\n\t\tty.Oneof = oneof\n\t}\n\n\tdesc = keyDesc{\n\t\tDoc:       docComments[f.Name],\n\t\tType:      ty,\n\t\tFieldName: f.Name,\n\t}\n\treturn key, desc, nil\n}\n\nvar descs = (func() map[string]keyDesc {\n\tvar cfg Config\n\tt := reflect.TypeOf(cfg)\n\tdescs := make(map[string]keyDesc, t.NumField())\n\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tf := t.Field(i)\n\t\tkey, desc, err := newKeyDesc(&f)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"invalid userconfig definition for field %s: %v\", f.Name, err))\n\t\t}\n\t\tif _, ok := descs[key]; ok {\n\t\t\tpanic(fmt.Sprintf(\"duplicate key %s in userconfig.Config\", key))\n\t\t}\n\t\tdescs[key] = desc\n\t}\n\n\treturn descs\n})()\n\nfunc kindFromReflect(kind reflect.Kind) (Kind, bool) {\n\tswitch kind {\n\tcase reflect.String:\n\t\treturn String, true\n\tcase reflect.Bool:\n\t\treturn Bool, true\n\tcase reflect.Int:\n\t\treturn Int, true\n\tcase reflect.Uint:\n\t\treturn Uint, true\n\tdefault:\n\t\treturn 0, false\n\t}\n}\n\n//go:embed config.go\nvar configGo string\n\n// doc comments, keyed by field name.\nvar docComments = (func() map[string]string {\n\t// Parse config.go as a Go file to extract the doc comments.\n\tfset := token.NewFileSet()\n\tf, err := parser.ParseFile(fset, \"config.go\", configGo, parser.ParseComments)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"userconfig/config.go is invalid: %v\", err))\n\t}\n\n\t// Compute package documentation with examples.\n\tp, err := doc.NewFromFiles(fset, []*ast.File{f}, \"encr.dev/internal/userconfig\")\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"userconfig/config.go is invalid: %v\", err))\n\t}\n\n\tfor _, typ := range p.Types {\n\t\tif typ.Name == \"Config\" {\n\t\t\tcomments := make(map[string]string)\n\n\t\t\t// Extract comments for each field.\n\t\t\tstructType := typ.Decl.Specs[0].(*ast.TypeSpec).Type.(*ast.StructType)\n\t\t\tfor _, f := range structType.Fields.List {\n\t\t\t\tif f.Doc == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif len(f.Names) == 0 {\n\t\t\t\t\tpanic(\"field has no name\")\n\t\t\t\t}\n\t\t\t\ttext := f.Doc.Text()\n\t\t\t\tfor _, name := range f.Names {\n\t\t\t\t\tcomments[name.Name] = text\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn comments\n\t\t}\n\t}\n\n\tpanic(\"Config type not found in userconfig/config.go\")\n})()\n"
  },
  {
    "path": "internal/userconfig/value.go",
    "content": "package userconfig\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\ntype Kind int\n\nconst (\n\tString Kind = iota + 1\n\tBool\n\tInt\n\tUint\n)\n\nfunc (k Kind) String() string {\n\tswitch k {\n\tcase String:\n\t\treturn \"string\"\n\tcase Bool:\n\t\treturn \"bool\"\n\tcase Int:\n\t\treturn \"int\"\n\tcase Uint:\n\t\treturn \"uint\"\n\tdefault:\n\t\treturn \"unknown kind\"\n\t}\n}\n\nfunc (k Kind) HumanString() string {\n\tswitch k {\n\tcase String:\n\t\treturn \"a string\"\n\tcase Bool:\n\t\treturn \"a boolean (true/false)\"\n\tcase Int:\n\t\treturn \"an integer\"\n\tcase Uint:\n\t\treturn \"an unsigned integer (>=0)\"\n\tdefault:\n\t\treturn \"an unknown kind\"\n\t}\n}\n\ntype Type struct {\n\tKind    Kind\n\tDefault *any  // nil means no default\n\tOneof   []any // nil means no restrictions\n}\n\ntype Value struct {\n\tVal  any\n\tType Type\n}\n\nfunc (v Value) String() string {\n\treturn RenderValue(v.Val)\n}\n\nfunc (t Type) ParseAndValidate(val string) (any, error) {\n\tparsed, err := t.Kind.parseValue(val)\n\tif err != nil {\n\t\treturn nil, err\n\t} else if err := t.validate(parsed); err != nil {\n\t\treturn nil, err\n\t}\n\treturn parsed, nil\n}\n\nfunc (t Type) validate(val any) error {\n\tif val == nil {\n\t\treturn errors.New(\"value cannot be nil\")\n\t}\n\tif len(t.Oneof) > 0 {\n\t\tfor _, v := range t.Oneof {\n\t\t\tif val == v {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tstrVal := fmt.Sprintf(\"%v\", val)\n\t\treturn errors.Errorf(\"value %q is not one of: %s\", strVal, RenderOneof(t.Oneof))\n\t}\n\n\tif k, ok := kindOf(val); ok {\n\t\tif k != t.Kind {\n\t\t\treturn errors.Errorf(\"value v is not %s\", t.Kind.HumanString())\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc RenderValue(v any) string {\n\treturn fmt.Sprintf(\"%v\", v)\n}\n\nfunc RenderOneof(oneof []any) string {\n\tif len(oneof) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Render as \"a, b, or c\"\n\tvar s strings.Builder\n\tfor i, v := range oneof {\n\t\tif i > 0 {\n\t\t\tif i == len(oneof)-1 {\n\t\t\t\tif len(oneof) > 2 {\n\t\t\t\t\ts.WriteString(\", or \")\n\t\t\t\t} else {\n\t\t\t\t\ts.WriteString(\" or \")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ts.WriteString(\", \")\n\t\t\t}\n\t\t}\n\n\t\ts.WriteString(RenderValue(v))\n\t}\n\treturn s.String()\n}\n\nfunc (k Kind) parseValue(value string) (any, error) {\n\tswitch k {\n\tcase String:\n\t\treturn value, nil\n\tcase Bool:\n\t\treturn strconv.ParseBool(value)\n\tcase Int:\n\t\treturn strconv.ParseInt(value, 10, 64)\n\tcase Uint:\n\t\treturn strconv.ParseUint(value, 10, 64)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown kind %v\", k)\n\t}\n}\n\nfunc KindOf[T interface{ string | bool | int | uint }](val T) (k Kind, ok bool) {\n\treturn kindOf(val)\n}\n\nfunc kindOf(val any) (k Kind, ok bool) {\n\tswitch val.(type) {\n\tcase string:\n\t\treturn String, true\n\tcase bool:\n\t\treturn Bool, true\n\tcase int:\n\t\treturn Int, true\n\tcase uint:\n\t\treturn Uint, true\n\tdefault:\n\t\treturn 0, false\n\t}\n}\n"
  },
  {
    "path": "internal/userconfig/write.go",
    "content": "package userconfig\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/xos\"\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/pelletier/go-toml\"\n)\n\nfunc SetForApp(appRoot, key, value string) error {\n\tif _, err := os.Stat(appRoot); err != nil {\n\t\treturn errors.Wrap(err, \"app root directory does not exist\")\n\t}\n\tdst := appFilePath(appRoot)\n\treturn updateConfig(dst, key, value)\n}\n\nfunc SetGlobal(key, value string) error {\n\tif len(userPaths) == 0 {\n\t\treturn errors.New(\"no global config file location found\")\n\t}\n\n\t// Find the last path in the list that exists.\n\tfor i := len(userPaths) - 1; i >= 0; i-- {\n\t\tif _, err := os.Stat(userPaths[i]); err == nil {\n\t\t\treturn updateConfig(userPaths[i], key, value)\n\t\t}\n\t}\n\n\t// Otherwise fall back to the lowest-priority entry.\n\tdst := userPaths[0]\n\treturn updateConfig(dst, key, value)\n}\n\nfunc updateConfig(dstPath, key, value string) error {\n\tdesc, ok := descs[key]\n\tif !ok {\n\t\treturn errors.Errorf(\"unknown key: %q\", key)\n\t}\n\tval, err := desc.Type.ParseAndValidate(value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Read the existing config.\n\t// If it doesn't exist it's initialized to an emty config.\n\tvar conf *toml.Tree\n\t{\n\t\tdata, err := os.ReadFile(dstPath)\n\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\treturn errors.Wrap(err, \"failed to read existing config\")\n\t\t}\n\t\tif data != nil {\n\t\t\tconf, err = toml.LoadBytes(data)\n\t\t} else {\n\t\t\tconf, err = toml.TreeFromMap(map[string]any{})\n\t\t}\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to parse existing config\")\n\t\t}\n\t}\n\n\tkeys := strings.Split(key, \".\")\n\tconf.SetPath(keys, val)\n\n\t// Write the config back out.\n\tdata, err := conf.Marshal()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to marshal config\")\n\t}\n\n\tif err := validateConfig(data); err != nil {\n\t\treturn errors.Wrap(err, \"resulting config is invalid\")\n\t}\n\n\tif err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {\n\t\treturn errors.Wrap(err, \"failed to create config file\")\n\t}\n\tif err := xos.WriteFile(dstPath, data, 0644); err != nil {\n\t\treturn errors.Wrap(err, \"failed to write config file\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/version/version.go",
    "content": "package version\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"strings\"\n\n\t\"golang.org/x/mod/semver\"\n\n\t\"encr.dev/internal/conf\"\n\t\"encr.dev/internal/env\"\n)\n\n// Version is the version of the encore binary.\n// It is set using `go build -ldflags \"-X encr.dev/internal/version.Version=v1.2.3\"`.\nvar Version string\n\n// Channel tells us which ReleaseChannel this build of Encore is under\nvar Channel ReleaseChannel\n\ntype ReleaseChannel string\n\nconst (\n\tGA       ReleaseChannel = \"ga\"      // A general availability release of Encore in Semver: v1.10.0\n\tBeta     ReleaseChannel = \"beta\"    // A beta build of an upcoming Encore release: v1.10.0-beta.1\n\tNightly  ReleaseChannel = \"nightly\" // A nightly build of Encore with the date of the build: v1.10.0-nightly.20221231\n\tDevBuild ReleaseChannel = \"develop\" // A development build of Encore with the commit of the build: v0.0.0-develop+0140ab0f78fd10d52673a961e900993b64b7b9e3\n\tunknown  ReleaseChannel = \"unknown\" // An unknown release stream (not exported as it should be an error case)\n)\n\n// ConfigHash reports a hash of the configuration that affects the behavior of the daemon.\n// It is used to decide whether to restart the daemon.\nfunc ConfigHash() (string, error) {\n\th := sha256.New()\n\tconfigDir, err := conf.Dir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfmt.Fprintf(h, \"APIBaseURL=%s\\n\", conf.APIBaseURL)\n\tfmt.Fprintf(h, \"ConfigDir=%s\\n\", configDir)\n\tfmt.Fprintf(h, \"EncoreDevDashListenAddr=%s\\n\", env.EncoreDevDashListenAddr().GetOrElse(\"\"))\n\tfmt.Fprintf(h, \"EncoreMCPSSEListenAddr=%s\\n\", env.EncoreMCPSSEListenAddr().GetOrElse(\"\"))\n\tfmt.Fprintf(h, \"EncoreObjectStorageListAddr=%s\\n\", env.EncoreObjectStorageListAddr().GetOrElse(\"\"))\n\n\tdigest := h.Sum(nil)\n\treturn base64.RawURLEncoding.EncodeToString(digest), nil\n}\n\nfunc init() {\n\t// If version is already set via a compiler link flag, then we don't need to do anything\n\tif Version == \"\" {\n\t\t// Otherwise, we want to read the information from this built binary\n\t\tVersion = \"v0.0.0-develop\"\n\n\t\tinfo, ok := debug.ReadBuildInfo()\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\n\t\t// Add the commit info\n\t\tvcsVersion := \"\"\n\t\tvcsModified := \"\"\n\t\tfor _, p := range info.Settings {\n\t\t\tswitch p.Key {\n\t\t\tcase \"vcs.revision\":\n\t\t\t\tvcsVersion = p.Value\n\t\t\tcase \"vcs.modified\":\n\t\t\t\tif p.Value == \"true\" {\n\t\t\t\t\tvcsModified = \"-modified\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif vcsVersion != \"\" {\n\t\t\tVersion += \"+\" + vcsVersion + vcsModified\n\t\t}\n\t}\n\tChannel = ChannelFor(Version)\n}\n\nfunc ChannelFor(version string) ReleaseChannel {\n\tif !strings.HasPrefix(version, \"v\") {\n\t\treturn unknown\n\t}\n\t// Now work out the release channel\n\tswitch {\n\tcase strings.Contains(version, \"-beta.\"):\n\t\treturn Beta\n\tcase strings.Contains(version, \"-nightly.\"):\n\t\treturn Nightly\n\tcase strings.HasSuffix(version, \"-develop\") || strings.Contains(version, \"-develop+\"):\n\t\treturn DevBuild\n\tdefault:\n\t\treturn GA\n\t}\n}\n\n// Compare compares this version of Encore against another version\n// accounting for the release channel.\n//\n// If the releases are from the same channel, then it returns:\n//   - 0 if the versions are the same\n//   - a negative number if this version is older than the other\n//   - a positive number if this version is newer than the other\n//\n// If the releases are from different channels, it always returns 1.\nfunc Compare(againstVersion string) int {\n\tagainstChannel := ChannelFor(againstVersion)\n\n\tif Channel != againstChannel {\n\t\t// If the channels are different, this \"version\" is always newer\n\t\treturn 1\n\t}\n\n\tswitch Channel {\n\tcase GA, Beta, Nightly:\n\t\treturn semver.Compare(Version, againstVersion)\n\tcase DevBuild:\n\t\treturn 0 // devel versions are always the same\n\tdefault:\n\t\treturn 0 // never newer if we can't test\n\t}\n}\n"
  },
  {
    "path": "miniredis/.gitignore",
    "content": "/target\n/testdata/*.key\n/testdata/*.crt\n/testdata/*.srl\n"
  },
  {
    "path": "miniredis/Cargo.toml",
    "content": "[package]\nname = \"miniredis-rs\"\nversion = \"0.1.0\"\nedition = \"2024\"\ndescription = \"Pure Rust in-memory Redis test server for use in integration tests\"\nlicense = \"MIT\"\n\n[features]\ndefault = []\ntls = [\"dep:tokio-rustls\", \"dep:rustls\", \"dep:rustls-pemfile\"]\n\n[dependencies]\n# Async runtime\ntokio = { version = \"1\", features = [\"full\"] }\n\n# Byte buffers (zero-copy)\nbytes = \"1\"\n\n# Ordered floats for BTreeMap keys (sorted sets)\nordered-float = \"5\"\n\n# Float formatting (Redis-compatible)\nryu = \"1\"\n\n# SHA1 for EVAL script caching\nsha1_smol = \"1\"\n\n# Regex for KEYS/SCAN pattern matching\nregex = \"1\"\n\n# Random number generation\nrand = \"0.9\"\n\n# TLS (optional)\ntokio-rustls = { version = \"0.26\", optional = true, default-features = false, features = [\"ring\"] }\nrustls = { version = \"0.23\", optional = true, default-features = false, features = [\"ring\", \"logging\", \"std\", \"tls12\"] }\nrustls-pemfile = { version = \"2\", optional = true }\n\n# Lua scripting\nmlua = { version = \"0.11\", features = [\"lua51\", \"vendored\", \"send\"] }\n\n[[bin]]\nname = \"miniredis-rs-server\"\npath = \"src/bin/miniredis-rs-server.rs\"\nrequired-features = [\"tls\"]\n\n[dev-dependencies]\nredis = { version = \"1.0\", features = [\"tokio-comp\", \"aio\"] }\ntokio = { version = \"1\", features = [\"full\", \"test-util\"] }\nfutures-lite = \"2\"\nrcgen = \"0.14\"\nminiredis-rs = { path = \".\", features = [\"tls\"] }\n"
  },
  {
    "path": "miniredis/MINIREDIS_LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Harmen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "miniredis/src/bin/miniredis-rs-server.rs",
    "content": "//! A thin CLI wrapper around miniredis-rs that speaks enough redis-server\n//! config to be used as a drop-in replacement in the miniredis Go integration\n//! test suite.\n//!\n//! Config lines are read from stdin (same as `redis-server -`).\n//! Recognised directives:\n//!   port <n>           – ignored (always binds to 0, prints actual port)\n//!   bind <addr>        – bind address (default 127.0.0.1)\n//!   requirepass <pw>   – set default-user password\n//!   user <name> on +@all ~* ><password> – add ACL user\n//!   tls-port <n>       – enable TLS listener (port is ignored, uses 0)\n//!   tls-cert-file <p>  – server certificate path\n//!   tls-key-file <p>   – server private key path\n//!   tls-ca-cert-file <p> – CA / client certificate path\n//!   appendonly …       – silently ignored\n//!   cluster-enabled …  – silently ignored\n//!   cluster-config-file … – silently ignored\n//!\n//! Once ready, the actual listening port is printed to stdout as a single line:\n//!   PORT=<n>\n//!\n//! The process exits cleanly on SIGTERM or SIGINT.\n\nuse std::io::{self, BufRead};\nuse std::sync::Arc;\n\nuse miniredis_rs::Miniredis;\nuse tokio::signal::unix::{SignalKind, signal};\n\n#[cfg(feature = \"tls\")]\nuse std::fs;\n\n#[cfg(feature = \"tls\")]\nfn load_tls_config(\n    cert_path: &str,\n    key_path: &str,\n    ca_cert_path: &str,\n) -> Arc<rustls::ServerConfig> {\n    let cert_pem = fs::read(cert_path).expect(\"read cert file\");\n    let key_pem = fs::read(key_path).expect(\"read key file\");\n    let ca_pem = fs::read(ca_cert_path).expect(\"read CA cert file\");\n\n    let certs: Vec<_> = rustls_pemfile::certs(&mut &cert_pem[..])\n        .collect::<Result<Vec<_>, _>>()\n        .expect(\"parse certs\");\n\n    let key = rustls_pemfile::private_key(&mut &key_pem[..])\n        .expect(\"parse key\")\n        .expect(\"no key found\");\n\n    let mut root_store = rustls::RootCertStore::empty();\n    for cert in rustls_pemfile::certs(&mut &ca_pem[..]) {\n        root_store\n            .add(cert.expect(\"parse CA cert\"))\n            .expect(\"add CA cert\");\n    }\n\n    let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))\n        .build()\n        .expect(\"build client verifier\");\n\n    let config = rustls::ServerConfig::builder()\n        .with_client_cert_verifier(verifier)\n        .with_single_cert(certs, key)\n        .expect(\"build TLS config\");\n\n    Arc::new(config)\n}\n\n#[tokio::main]\nasync fn main() {\n    let mut bind_addr = \"127.0.0.1\".to_string();\n    let mut password: Option<String> = None;\n    let mut users: Vec<(String, String)> = Vec::new();\n    let mut tls_enabled = false;\n    let mut tls_cert = String::new();\n    let mut tls_key = String::new();\n    let mut tls_ca_cert = String::new();\n\n    // Read config from stdin\n    let stdin = io::stdin();\n    for line in stdin.lock().lines() {\n        let line = line.expect(\"read stdin\");\n        let line = line.trim().to_string();\n        if line.is_empty() || line.starts_with('#') {\n            continue;\n        }\n        let parts: Vec<&str> = line.split_whitespace().collect();\n        if parts.is_empty() {\n            continue;\n        }\n        match parts[0].to_lowercase().as_str() {\n            \"port\" => { /* ignored – always use port 0 */ }\n            \"bind\" => {\n                if parts.len() > 1 {\n                    bind_addr = parts[1].to_string();\n                }\n            }\n            \"requirepass\" => {\n                if parts.len() > 1 {\n                    password = Some(parts[1].to_string());\n                }\n            }\n            \"user\" => {\n                // user <name> on +@all ~* ><password>\n                // or: user default on -@all +hello\n                if parts.len() >= 2 {\n                    let username = parts[1].to_string();\n                    // Find the >password token\n                    let mut pw = None;\n                    for part in &parts[2..] {\n                        if let Some(p) = part.strip_prefix('>') {\n                            pw = Some(p.to_string());\n                        }\n                    }\n                    if let Some(p) = pw {\n                        users.push((username, p));\n                    }\n                    // \"user default on -@all +hello\" (no password) is ignored\n                }\n            }\n            \"tls-port\" => {\n                tls_enabled = true;\n            }\n            \"tls-cert-file\" => {\n                if parts.len() > 1 {\n                    tls_cert = parts[1].to_string();\n                }\n            }\n            \"tls-key-file\" => {\n                if parts.len() > 1 {\n                    tls_key = parts[1].to_string();\n                }\n            }\n            \"tls-ca-cert-file\" => {\n                if parts.len() > 1 {\n                    tls_ca_cert = parts[1].to_string();\n                }\n            }\n            // Silently ignore everything else\n            _ => {}\n        }\n    }\n\n    let m = if tls_enabled {\n        #[cfg(feature = \"tls\")]\n        {\n            let tls_config = load_tls_config(&tls_cert, &tls_key, &tls_ca_cert);\n            Miniredis::run_tls_addr(&format!(\"{}:0\", bind_addr), tls_config)\n                .await\n                .expect(\"start TLS server\")\n        }\n        #[cfg(not(feature = \"tls\"))]\n        {\n            panic!(\"TLS requested but binary was compiled without tls feature\");\n        }\n    } else {\n        Miniredis::run_addr(&format!(\"{}:0\", bind_addr))\n            .await\n            .expect(\"start server\")\n    };\n\n    // Set up authentication\n    if let Some(pw) = &password {\n        m.require_auth(pw);\n    }\n    for (user, pw) in &users {\n        m.require_user_auth(user, pw);\n    }\n\n    // Print the port – the Go test harness reads this as readiness signal.\n    println!(\"PORT={}\", m.port());\n\n    // Wait for SIGTERM or SIGINT\n    let mut sigterm = signal(SignalKind::terminate()).expect(\"signal handler\");\n    let mut sigint = signal(SignalKind::interrupt()).expect(\"signal handler\");\n    tokio::select! {\n        _ = sigterm.recv() => {}\n        _ = sigint.recv() => {}\n    }\n\n    m.close().await;\n}\n"
  },
  {
    "path": "miniredis/src/cmd/client.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::CommandTable;\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"CLIENT\", cmd_client, false, -2);\n}\n\nfn cmd_client(_state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"SETNAME\" => {\n            if args.len() != 2 {\n                return Frame::error(\"ERR wrong number of arguments for 'client|setname' command\");\n            }\n            let name = String::from_utf8_lossy(&args[1]).to_string();\n            if name.contains(' ') || name.contains('\\n') {\n                return Frame::error(\n                    \"ERR Client names cannot contain spaces, newlines or special characters.\",\n                );\n            }\n            ctx.client_name = if name.is_empty() { None } else { Some(name) };\n            Frame::ok()\n        }\n        \"GETNAME\" => {\n            if args.len() != 1 {\n                return Frame::error(\"ERR wrong number of arguments for 'client|getname' command\");\n            }\n            match &ctx.client_name {\n                Some(name) => Frame::Bulk(name.clone().into()),\n                None => Frame::Null,\n            }\n        }\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try CLIENT HELP.\",\n            subcmd.to_lowercase()\n        )),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/cluster.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::CommandTable;\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"CLUSTER\", cmd_cluster, true, -2);\n}\n\nfn cmd_cluster(_state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"SLOTS\" => {\n            // Single-node cluster: one slot range 0-16383\n            Frame::Array(vec![Frame::Array(vec![\n                Frame::Integer(0),\n                Frame::Integer(16383),\n                Frame::Array(vec![\n                    Frame::Bulk(\"127.0.0.1\".into()),\n                    Frame::Integer(6379),\n                    Frame::Bulk(\n                        \"09dbe9720cda62f7865eabc5fd8857c5d2678366\".into(),\n                    ),\n                ]),\n            ])])\n        }\n        \"KEYSLOT\" => {\n            if args.len() != 2 {\n                return Frame::error(\n                    \"ERR wrong number of arguments for 'cluster|keyslot' command\",\n                );\n            }\n            // Simplified: always return 163\n            Frame::Integer(163)\n        }\n        \"NODES\" => {\n            Frame::Bulk(\n                \"e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 127.0.0.1:6379@6379 myself,master - 0 0 1 connected 0-16383\\n\"\n                    .into(),\n            )\n        }\n        \"SHARDS\" => {\n            // Simplified shard info as a flat array\n            Frame::Array(vec![Frame::Array(vec![\n                Frame::Bulk(\"slots\".into()),\n                Frame::Array(vec![Frame::Integer(0), Frame::Integer(16383)]),\n                Frame::Bulk(\"nodes\".into()),\n                Frame::Array(vec![Frame::Array(vec![\n                    Frame::Bulk(\"id\".into()),\n                    Frame::Bulk(\n                        \"13f84e686106847b76671957dd348fde540a77bb\".into(),\n                    ),\n                    Frame::Bulk(\"ip\".into()),\n                    Frame::Bulk(\"127.0.0.1\".into()),\n                    Frame::Bulk(\"port\".into()),\n                    Frame::Integer(6379),\n                    Frame::Bulk(\"role\".into()),\n                    Frame::Bulk(\"master\".into()),\n                    Frame::Bulk(\"replication-offset\".into()),\n                    Frame::Integer(0),\n                    Frame::Bulk(\"health\".into()),\n                    Frame::Bulk(\"online\".into()),\n                ])]),\n            ])])\n        }\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try CLUSTER HELP.\",\n            subcmd.to_lowercase()\n        )),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/connection.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_DB_INDEX_OUT_OF_RANGE, MSG_SYNTAX_ERROR, err_wrong_number,\n};\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"PING\", cmd_ping, true, -1);\n    table.add(\"ECHO\", cmd_echo, true, 2);\n    table.add(\"QUIT\", cmd_quit, true, 1);\n    table.add(\"SELECT\", cmd_select, true, 2);\n    table.add(\"AUTH\", cmd_auth, false, -2);\n    table.add(\"HELLO\", cmd_hello, false, -1);\n}\n\n/// PING [message]\nfn cmd_ping(_state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    match args.len() {\n        0 => Frame::Simple(\"PONG\".into()),\n        1 => Frame::Bulk(args[0].clone().into()),\n        _ => Frame::error(err_wrong_number(\"ping\")),\n    }\n}\n\n/// ECHO message\nfn cmd_echo(_state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    Frame::Bulk(args[0].clone().into())\n}\n\n/// QUIT\nfn cmd_quit(_state: &Arc<SharedState>, _ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    Frame::ok()\n}\n\n/// SELECT db\nfn cmd_select(_state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let db_str = match std::str::from_utf8(&args[0]) {\n        Ok(s) => s,\n        Err(_) => return Frame::error(crate::dispatch::MSG_INVALID_INT),\n    };\n\n    let db: i64 = match db_str.parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(crate::dispatch::MSG_INVALID_INT),\n    };\n\n    if !(0..16).contains(&db) {\n        return Frame::error(MSG_DB_INDEX_OUT_OF_RANGE);\n    }\n\n    ctx.selected_db = db as usize;\n    Frame::ok()\n}\n\n/// AUTH [username] password\nfn cmd_auth(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 2 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let (username, password) = if args.len() == 2 {\n        (\n            String::from_utf8_lossy(&args[0]).to_string(),\n            String::from_utf8_lossy(&args[1]).to_string(),\n        )\n    } else {\n        (\n            \"default\".to_string(),\n            String::from_utf8_lossy(&args[0]).to_string(),\n        )\n    };\n\n    let inner = state.lock();\n\n    if inner.passwords.is_empty() && username == \"default\" {\n        return Frame::error(\n            \"ERR AUTH <password> called without any password configured for the default user. Are you sure your configuration is correct?\",\n        );\n    }\n\n    match inner.passwords.get(&username) {\n        Some(pw) if pw == &password => {\n            ctx.authenticated = true;\n            Frame::ok()\n        }\n        _ => Frame::error(\"WRONGPASS invalid username-password pair\"),\n    }\n}\n\n/// HELLO protover [AUTH username password] [SETNAME clientname]\nfn cmd_hello(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.is_empty() {\n        return Frame::error(err_wrong_number(\"hello\"));\n    }\n\n    // Parse protocol version\n    let version: i64 = match std::str::from_utf8(&args[0])\n        .ok()\n        .and_then(|s| s.parse().ok())\n    {\n        Some(v) => v,\n        None => {\n            return Frame::error(\"ERR Protocol version is not an integer or out of range\");\n        }\n    };\n\n    if version != 2 && version != 3 {\n        return Frame::error(\"NOPROTO unsupported protocol version\");\n    }\n\n    // Parse optional AUTH and SETNAME\n    let mut check_auth = false;\n    let mut username = \"default\".to_string();\n    let mut password = String::new();\n    let mut i = 1;\n\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"AUTH\" => {\n                if i + 2 >= args.len() {\n                    return Frame::error(format!(\n                        \"ERR Syntax error in HELLO option '{}'\",\n                        String::from_utf8_lossy(&args[i])\n                    ));\n                }\n                username = String::from_utf8_lossy(&args[i + 1]).to_string();\n                password = String::from_utf8_lossy(&args[i + 2]).to_string();\n                check_auth = true;\n                i += 3;\n            }\n            \"SETNAME\" => {\n                if i + 1 >= args.len() {\n                    return Frame::error(format!(\n                        \"ERR Syntax error in HELLO option '{}'\",\n                        String::from_utf8_lossy(&args[i])\n                    ));\n                }\n                ctx.client_name = Some(String::from_utf8_lossy(&args[i + 1]).to_string());\n                i += 2;\n            }\n            _ => {\n                return Frame::error(format!(\n                    \"ERR Syntax error in HELLO option '{}'\",\n                    String::from_utf8_lossy(&args[i])\n                ));\n            }\n        }\n    }\n\n    // Check authentication if AUTH was provided\n    let inner = state.lock();\n    if inner.passwords.is_empty() && username == \"default\" {\n        check_auth = false;\n    }\n    if check_auth {\n        match inner.passwords.get(&username) {\n            Some(pw) if pw == &password => {\n                ctx.authenticated = true;\n            }\n            _ => {\n                return Frame::error(\"WRONGPASS invalid username-password pair\");\n            }\n        }\n    }\n\n    // Set RESP3 mode if version is 3\n    ctx.resp3 = version == 3;\n\n    // Return server info as a map\n    Frame::Map(vec![\n        (\n            Frame::bulk_string(\"server\"),\n            Frame::bulk_string(\"miniredis\"),\n        ),\n        (Frame::bulk_string(\"version\"), Frame::bulk_string(\"8.4.0\")),\n        (Frame::bulk_string(\"proto\"), Frame::Integer(version)),\n        (Frame::bulk_string(\"id\"), Frame::Integer(42)),\n        (Frame::bulk_string(\"mode\"), Frame::bulk_string(\"standalone\")),\n        (Frame::bulk_string(\"role\"), Frame::bulk_string(\"master\")),\n        (\n            Frame::bulk_string(\"modules\"),\n            Frame::Array(vec![Frame::Map(vec![\n                (Frame::bulk_string(\"name\"), Frame::bulk_string(\"vectorset\")),\n                (Frame::bulk_string(\"ver\"), Frame::Integer(1)),\n                (Frame::bulk_string(\"path\"), Frame::bulk_string(\"\")),\n                (Frame::bulk_string(\"args\"), Frame::Array(vec![])),\n            ])]),\n        ),\n    ])\n}\n"
  },
  {
    "path": "miniredis/src/cmd/generic.rs",
    "content": "use std::sync::Arc;\nuse std::time::Duration;\n\nuse rand::Rng;\n\nuse super::parse_int;\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_DB_INDEX_OUT_OF_RANGE, MSG_INVALID_CURSOR, MSG_INVALID_INT,\n    MSG_KEY_NOT_FOUND, MSG_SYNTAX_ERROR, MSG_TIMEOUT_NEGATIVE, err_wrong_number,\n};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"DEL\", cmd_del, false, -2);\n    table.add(\"UNLINK\", cmd_del, false, -2); // alias\n    table.add(\"EXISTS\", cmd_exists, true, -2);\n    table.add(\"TYPE\", cmd_type, true, 2);\n    table.add(\"RENAME\", cmd_rename, false, 3);\n    table.add(\"RENAMENX\", cmd_renamenx, false, 3);\n    table.add(\"EXPIRE\", cmd_expire, false, -3);\n    table.add(\"EXPIREAT\", cmd_expireat, false, -3);\n    table.add(\"PEXPIRE\", cmd_pexpire, false, -3);\n    table.add(\"PEXPIREAT\", cmd_pexpireat, false, -3);\n    table.add(\"PERSIST\", cmd_persist, false, 2);\n    table.add(\"TTL\", cmd_ttl, true, 2);\n    table.add(\"PTTL\", cmd_pttl, true, 2);\n    table.add(\"KEYS\", cmd_keys, true, 2);\n    table.add(\"SCAN\", cmd_scan, true, -2);\n    table.add(\"TOUCH\", cmd_touch, true, -2);\n    table.add(\"WAIT\", cmd_wait, true, 3);\n    table.add(\"RANDOMKEY\", cmd_randomkey, true, 1);\n    table.add(\"OBJECT\", cmd_object, true, -2);\n    table.add(\"EXPIRETIME\", cmd_expiretime, true, 2);\n    table.add(\"PEXPIRETIME\", cmd_pexpiretime, true, 2);\n    table.add(\"COPY\", cmd_copy, false, -3);\n    table.add(\"MOVE\", cmd_move, false, 3);\n    table.add(\"DUMP\", cmd_dump, true, 2);\n    table.add(\"RESTORE\", cmd_restore, false, -4);\n}\n\n/// DEL key [key ...]\nfn cmd_del(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    let mut count = 0i64;\n\n    for arg in args {\n        let key = String::from_utf8_lossy(arg);\n        db.check_ttl(&key);\n        if db.del(&key) {\n            count += 1;\n        }\n    }\n\n    Frame::Integer(count)\n}\n\n/// EXISTS key [key ...]\nfn cmd_exists(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    let mut count = 0i64;\n\n    for arg in args {\n        let key = String::from_utf8_lossy(arg);\n        db.check_ttl(&key);\n        if db.exists(&key, now) {\n            count += 1;\n        }\n    }\n\n    Frame::Integer(count)\n}\n\n/// TYPE key\nfn cmd_type(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    let t = match db.key_type(&key) {\n        Some(t) => t.as_str(),\n        None => \"none\",\n    };\n    Frame::Simple(t.to_owned())\n}\n\n/// RENAME key newkey\nfn cmd_rename(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let from = String::from_utf8_lossy(&args[0]).into_owned();\n    let to = String::from_utf8_lossy(&args[1]).into_owned();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if !db.keys.contains_key(&from) {\n        return Frame::error(MSG_KEY_NOT_FOUND);\n    }\n\n    db.rename(&from, &to, now);\n    Frame::ok()\n}\n\n/// RENAMENX key newkey\nfn cmd_renamenx(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let from = String::from_utf8_lossy(&args[0]).into_owned();\n    let to = String::from_utf8_lossy(&args[1]).into_owned();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if !db.keys.contains_key(&from) {\n        return Frame::error(MSG_KEY_NOT_FOUND);\n    }\n\n    if db.keys.contains_key(&to) {\n        return Frame::Integer(0);\n    }\n\n    db.rename(&from, &to, now);\n    Frame::Integer(1)\n}\n\n/// EXPIRE key seconds [NX|XX|GT|LT]\nfn cmd_expire(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    expire_impl(state, ctx, args, |secs, _| {\n        if secs <= 0 {\n            Duration::ZERO\n        } else {\n            Duration::from_secs(secs as u64)\n        }\n    })\n}\n\n/// EXPIREAT key timestamp [NX|XX|GT|LT]\nfn cmd_expireat(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    expire_impl(state, ctx, args, |ts, now| {\n        if ts <= 0 {\n            return Duration::ZERO;\n        }\n        let target = std::time::UNIX_EPOCH + Duration::from_secs(ts as u64);\n        target.duration_since(now).unwrap_or(Duration::ZERO)\n    })\n}\n\n/// PEXPIRE key milliseconds [NX|XX|GT|LT]\nfn cmd_pexpire(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    expire_impl(state, ctx, args, |ms, _| {\n        if ms <= 0 {\n            Duration::ZERO\n        } else {\n            Duration::from_millis(ms as u64)\n        }\n    })\n}\n\n/// PEXPIREAT key timestamp-ms [NX|XX|GT|LT]\nfn cmd_pexpireat(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    expire_impl(state, ctx, args, |ts, now| {\n        if ts <= 0 {\n            return Duration::ZERO;\n        }\n        let target = std::time::UNIX_EPOCH + Duration::from_millis(ts as u64);\n        target.duration_since(now).unwrap_or(Duration::ZERO)\n    })\n}\n\nfn expire_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    to_duration: impl Fn(i64, std::time::SystemTime) -> Duration,\n) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let value: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    // Parse optional flags (multiple can be combined, e.g. GT XX)\n    let mut nx = false;\n    let mut xx = false;\n    let mut gt = false;\n    let mut lt = false;\n\n    for arg in &args[2..] {\n        let flag = String::from_utf8_lossy(arg);\n        match flag.to_uppercase().as_str() {\n            \"NX\" => nx = true,\n            \"XX\" => xx = true,\n            \"GT\" => gt = true,\n            \"LT\" => lt = true,\n            _ => return Frame::error(format!(\"ERR Unsupported option {}\", flag)),\n        }\n    }\n\n    // NX is incompatible with GT/LT; GT and LT are mutually exclusive\n    if nx && (gt || lt) {\n        return Frame::error(\"ERR NX and XX, GT or LT options at the same time are not compatible\");\n    }\n    if gt && lt {\n        return Frame::error(\"ERR GT and LT options at the same time are not compatible\");\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    let new_ttl = to_duration(value, now);\n    let has_ttl = db.ttl.contains_key(&key);\n\n    // NX: only if no existing TTL\n    if nx && has_ttl {\n        return Frame::Integer(0);\n    }\n    // XX: only if has existing TTL\n    if xx && !has_ttl {\n        return Frame::Integer(0);\n    }\n    // GT: only if new TTL is greater (no TTL = infinite > any finite TTL)\n    if gt {\n        if let Some(&old_ttl) = db.ttl.get(&key) {\n            if new_ttl <= old_ttl {\n                return Frame::Integer(0);\n            }\n        } else {\n            // No existing TTL means infinite lifetime, which is > any finite TTL\n            return Frame::Integer(0);\n        }\n    }\n    // LT: only if new TTL is less\n    if lt {\n        if let Some(&old_ttl) = db.ttl.get(&key) {\n            if new_ttl >= old_ttl {\n                return Frame::Integer(0);\n            }\n        } else {\n            // No existing TTL, LT always applies\n        }\n    }\n\n    db.ttl.insert(key.clone(), new_ttl);\n    db.incr_version(&key, now);\n\n    // Check if key already expired\n    db.check_ttl(&key);\n\n    Frame::Integer(1)\n}\n\n/// PERSIST key\nfn cmd_persist(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    if db.ttl.remove(&key).is_some() {\n        db.incr_version(&key, now);\n        Frame::Integer(1)\n    } else {\n        Frame::Integer(0)\n    }\n}\n\n/// TTL key\nfn cmd_ttl(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(-2);\n    }\n\n    match db.ttl.get(key.as_ref()) {\n        Some(ttl) => Frame::Integer(ttl.as_secs() as i64),\n        None => Frame::Integer(-1),\n    }\n}\n\n/// PTTL key\nfn cmd_pttl(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(-2);\n    }\n\n    match db.ttl.get(key.as_ref()) {\n        Some(ttl) => Frame::Integer(ttl.as_millis() as i64),\n        None => Frame::Integer(-1),\n    }\n}\n\n/// KEYS pattern\nfn cmd_keys(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let pattern = String::from_utf8_lossy(&args[0]);\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    let all_keys = db.all_keys();\n    let matched = match_keys(&all_keys, &pattern);\n\n    Frame::Array(matched.into_iter().map(|k| Frame::Bulk(k.into())).collect())\n}\n\n/// SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]\nfn cmd_scan(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let cursor: i64 = match parse_int(&args[0]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_CURSOR),\n    };\n\n    let opts = match super::parse_scan_opts(&args[1..], true) {\n        Ok(o) => o,\n        Err(e) => return e,\n    };\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    let mut all_keys = db.all_keys();\n\n    // Filter by type\n    if let Some(ref tf) = opts.type_filter {\n        all_keys.retain(|k| {\n            db.key_type(k)\n                .map(|t| t.as_str() == tf.as_str())\n                .unwrap_or(false)\n        });\n    }\n\n    // Filter by pattern\n    let matched = if let Some(ref pat) = opts.pattern {\n        match_keys(&all_keys, pat)\n    } else {\n        all_keys\n    };\n\n    // Simple implementation: return all results at once, no real cursor pagination\n    if cursor != 0 {\n        return Frame::Array(vec![Frame::Bulk(\"0\".into()), Frame::Array(vec![])]);\n    }\n\n    Frame::Array(vec![\n        Frame::Bulk(\"0\".into()),\n        Frame::Array(matched.into_iter().map(|k| Frame::Bulk(k.into())).collect()),\n    ])\n}\n\n/// TOUCH key [key ...]\nfn cmd_touch(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n    let mut count = 0i64;\n\n    for arg in args {\n        let key = String::from_utf8_lossy(arg);\n        if db.keys.contains_key(key.as_ref()) {\n            count += 1;\n        }\n    }\n\n    Frame::Integer(count)\n}\n\n/// WAIT numreplicas timeout — always returns 0 (standalone)\nfn cmd_wait(_state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let _replicas: i64 = match parse_int(&args[0]) {\n        Some(n) if n >= 0 => n,\n        _ => return Frame::error(MSG_INVALID_INT),\n    };\n    let timeout: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    if timeout < 0 {\n        return Frame::error(MSG_TIMEOUT_NEGATIVE);\n    }\n\n    Frame::Integer(0)\n}\n\n/// RANDOMKEY\nfn cmd_randomkey(state: &Arc<SharedState>, ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    let mut inner = state.lock();\n    let key_count = inner.db(ctx.selected_db).keys.len();\n\n    if key_count == 0 {\n        return Frame::Null;\n    }\n\n    let idx = inner.rng.random_range(0..key_count);\n    let key = inner\n        .db(ctx.selected_db)\n        .keys\n        .keys()\n        .nth(idx)\n        .unwrap()\n        .clone();\n    Frame::Bulk(key.into())\n}\n\n/// OBJECT subcommand ...\nfn cmd_object(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let sub = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match sub.as_str() {\n        \"HELP\" => Frame::Array(vec![Frame::Bulk(\"OBJECT subcommand [arguments]\".into())]),\n        \"ENCODING\" => {\n            if args.len() != 2 {\n                return Frame::error(err_wrong_number(\"object|encoding\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let mut inner = state.lock();\n            let db = inner.db_mut(ctx.selected_db);\n            db.check_ttl(&key);\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::error(MSG_KEY_NOT_FOUND);\n            }\n            // Stub: always return \"raw\"\n            Frame::Bulk(\"raw\".into())\n        }\n        \"IDLETIME\" => {\n            if args.len() != 2 {\n                return Frame::error(err_wrong_number(\"object|idletime\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let mut inner = state.lock();\n            let db = inner.db_mut(ctx.selected_db);\n            db.check_ttl(&key);\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::error(MSG_KEY_NOT_FOUND);\n            }\n            Frame::Integer(0)\n        }\n        \"REFCOUNT\" => {\n            if args.len() != 2 {\n                return Frame::error(err_wrong_number(\"object|refcount\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let mut inner = state.lock();\n            let db = inner.db_mut(ctx.selected_db);\n            db.check_ttl(&key);\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::error(MSG_KEY_NOT_FOUND);\n            }\n            Frame::Integer(1)\n        }\n        \"FREQ\" => {\n            if args.len() != 2 {\n                return Frame::error(err_wrong_number(\"object|freq\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let mut inner = state.lock();\n            let db = inner.db_mut(ctx.selected_db);\n            db.check_ttl(&key);\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::error(MSG_KEY_NOT_FOUND);\n            }\n            Frame::Integer(0)\n        }\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand or wrong number of arguments for 'object|{}' command\",\n            sub.to_lowercase()\n        )),\n    }\n}\n\n/// EXPIRETIME key\nfn cmd_expiretime(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(-2);\n    }\n\n    match db.ttl.get(key.as_ref()) {\n        Some(ttl) => {\n            let expire_at = now + *ttl;\n            let secs = expire_at\n                .duration_since(std::time::UNIX_EPOCH)\n                .unwrap_or(Duration::ZERO)\n                .as_secs();\n            Frame::Integer(secs as i64)\n        }\n        None => Frame::Integer(-1),\n    }\n}\n\n/// PEXPIRETIME key\nfn cmd_pexpiretime(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(-2);\n    }\n\n    match db.ttl.get(key.as_ref()) {\n        Some(ttl) => {\n            let expire_at = now + *ttl;\n            let ms = expire_at\n                .duration_since(std::time::UNIX_EPOCH)\n                .unwrap_or(Duration::ZERO)\n                .as_millis();\n            Frame::Integer(ms as i64)\n        }\n        None => Frame::Integer(-1),\n    }\n}\n\n/// COPY source destination [DB db] [REPLACE]\nfn cmd_copy(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let src = String::from_utf8_lossy(&args[0]).into_owned();\n    let dst = String::from_utf8_lossy(&args[1]).into_owned();\n    let mut dest_db = ctx.selected_db;\n    let mut replace = false;\n\n    let mut i = 2;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"DB\" | \"DESTINATION\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                match parse_int(&args[i]) {\n                    Some(n) if (0..16).contains(&n) => dest_db = n as usize,\n                    Some(_) => return Frame::error(MSG_DB_INDEX_OUT_OF_RANGE),\n                    None => return Frame::error(MSG_INVALID_INT),\n                }\n            }\n            \"REPLACE\" => replace = true,\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n        i += 1;\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n\n    // COPY to self on same DB: error (checked before key existence)\n    if ctx.selected_db == dest_db && src == dst {\n        return Frame::error(\"ERR source and destination objects are the same\");\n    }\n\n    // Check source exists\n    {\n        let src_db = inner.db_mut(ctx.selected_db);\n        src_db.check_ttl(&src);\n        if !src_db.keys.contains_key(&src) {\n            return Frame::Integer(0);\n        }\n    }\n\n    // Check destination\n    {\n        let dst_db = inner.db_mut(dest_db);\n        dst_db.check_ttl(&dst);\n        if dst_db.keys.contains_key(&dst) && !replace {\n            return Frame::Integer(0);\n        }\n        if replace {\n            dst_db.del(&dst);\n        }\n    }\n\n    if ctx.selected_db == dest_db {\n        // Same DB: use copy_key\n        let db = inner.db_mut(ctx.selected_db);\n        db.copy_key(&src, &dst, now);\n    } else {\n        // Cross-DB copy: manually clone data\n        let key_type = *inner.db(ctx.selected_db).keys.get(&src).unwrap();\n        let ttl = inner.db(ctx.selected_db).ttl.get(&src).copied();\n\n        match key_type {\n            KeyType::String => {\n                let val = inner.db(ctx.selected_db).string_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner.db_mut(dest_db).string_set(&dst, v, now);\n                }\n            }\n            KeyType::Hash => {\n                let val = inner.db(ctx.selected_db).hash_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner\n                        .db_mut(dest_db)\n                        .keys\n                        .insert(dst.clone(), KeyType::Hash);\n                    inner.db_mut(dest_db).hash_keys.insert(dst.clone(), v);\n                    inner.db_mut(dest_db).incr_version(&dst, now);\n                }\n            }\n            KeyType::List => {\n                let val = inner.db(ctx.selected_db).list_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner\n                        .db_mut(dest_db)\n                        .keys\n                        .insert(dst.clone(), KeyType::List);\n                    inner.db_mut(dest_db).list_keys.insert(dst.clone(), v);\n                    inner.db_mut(dest_db).incr_version(&dst, now);\n                }\n            }\n            KeyType::Set => {\n                let val = inner.db(ctx.selected_db).set_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner.db_mut(dest_db).set_set(&dst, v, now);\n                }\n            }\n            KeyType::SortedSet => {\n                let val = inner.db(ctx.selected_db).sorted_set_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner.db_mut(dest_db).sset_set(&dst, v, now);\n                }\n            }\n            KeyType::Stream => {\n                let val = inner.db(ctx.selected_db).stream_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner\n                        .db_mut(dest_db)\n                        .keys\n                        .insert(dst.clone(), KeyType::Stream);\n                    inner.db_mut(dest_db).stream_keys.insert(dst.clone(), v);\n                    inner.db_mut(dest_db).incr_version(&dst, now);\n                }\n            }\n            KeyType::HyperLogLog => {\n                let val = inner.db(ctx.selected_db).hll_keys.get(&src).cloned();\n                if let Some(v) = val {\n                    inner\n                        .db_mut(dest_db)\n                        .keys\n                        .insert(dst.clone(), KeyType::HyperLogLog);\n                    inner.db_mut(dest_db).hll_keys.insert(dst.clone(), v);\n                    inner.db_mut(dest_db).incr_version(&dst, now);\n                }\n            }\n        }\n\n        if let Some(ttl) = ttl {\n            inner.db_mut(dest_db).ttl.insert(dst, ttl);\n        }\n    }\n\n    Frame::Integer(1)\n}\n\n/// MOVE key db\nfn cmd_move(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let target_db = match parse_int(&args[1]) {\n        Some(n) if (0..16).contains(&n) => n as usize,\n        _ => return Frame::error(MSG_DB_INDEX_OUT_OF_RANGE),\n    };\n\n    if target_db == ctx.selected_db {\n        return Frame::error(\"ERR source and destination objects are the same\");\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n\n    // Check source exists\n    {\n        let src_db = inner.db_mut(ctx.selected_db);\n        src_db.check_ttl(&key);\n        if !src_db.keys.contains_key(&key) {\n            return Frame::Integer(0);\n        }\n    }\n\n    // Check target doesn't have the key\n    {\n        let dst_db = inner.db_mut(target_db);\n        dst_db.check_ttl(&key);\n        if dst_db.keys.contains_key(&key) {\n            return Frame::Integer(0);\n        }\n    }\n\n    // Copy to target, then delete from source\n    let key_type = *inner.db(ctx.selected_db).keys.get(&key).unwrap();\n    let ttl = inner.db(ctx.selected_db).ttl.get(&key).copied();\n\n    match key_type {\n        KeyType::String => {\n            let val = inner.db(ctx.selected_db).string_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner.db_mut(target_db).string_set(&key, v, now);\n            }\n        }\n        KeyType::Hash => {\n            let val = inner.db(ctx.selected_db).hash_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner\n                    .db_mut(target_db)\n                    .keys\n                    .insert(key.clone(), KeyType::Hash);\n                inner.db_mut(target_db).hash_keys.insert(key.clone(), v);\n                inner.db_mut(target_db).incr_version(&key, now);\n            }\n        }\n        KeyType::List => {\n            let val = inner.db(ctx.selected_db).list_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner\n                    .db_mut(target_db)\n                    .keys\n                    .insert(key.clone(), KeyType::List);\n                inner.db_mut(target_db).list_keys.insert(key.clone(), v);\n                inner.db_mut(target_db).incr_version(&key, now);\n            }\n        }\n        KeyType::Set => {\n            let val = inner.db(ctx.selected_db).set_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner.db_mut(target_db).set_set(&key, v, now);\n            }\n        }\n        KeyType::SortedSet => {\n            let val = inner.db(ctx.selected_db).sorted_set_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner.db_mut(target_db).sset_set(&key, v, now);\n            }\n        }\n        KeyType::Stream => {\n            let val = inner.db(ctx.selected_db).stream_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner\n                    .db_mut(target_db)\n                    .keys\n                    .insert(key.clone(), KeyType::Stream);\n                inner.db_mut(target_db).stream_keys.insert(key.clone(), v);\n                inner.db_mut(target_db).incr_version(&key, now);\n            }\n        }\n        KeyType::HyperLogLog => {\n            let val = inner.db(ctx.selected_db).hll_keys.get(&key).cloned();\n            if let Some(v) = val {\n                inner\n                    .db_mut(target_db)\n                    .keys\n                    .insert(key.clone(), KeyType::HyperLogLog);\n                inner.db_mut(target_db).hll_keys.insert(key.clone(), v);\n                inner.db_mut(target_db).incr_version(&key, now);\n            }\n        }\n    }\n\n    if let Some(ttl) = ttl {\n        inner.db_mut(target_db).ttl.insert(key.clone(), ttl);\n    }\n\n    // Delete from source\n    inner.db_mut(ctx.selected_db).del(&key);\n    Frame::Integer(1)\n}\n\n/// DUMP key — stub: returns raw string value or null\nfn cmd_dump(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Null;\n    }\n\n    // Stub: only dump string values\n    match db.key_type(&key) {\n        Some(KeyType::String) => match db.string_get(&key) {\n            Some(val) => Frame::Bulk(val.clone().into()),\n            None => Frame::Null,\n        },\n        _ => Frame::Null,\n    }\n}\n\n/// RESTORE key ttl serialized-value [REPLACE]\nfn cmd_restore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let ttl_ms: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let value = args[2].clone();\n\n    let mut replace = false;\n    for arg in &args[3..] {\n        let opt = String::from_utf8_lossy(arg).to_uppercase();\n        if opt == \"REPLACE\" {\n            replace = true;\n        }\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if db.keys.contains_key(&key) {\n        if !replace {\n            return Frame::error(\"BUSYKEY Target key name already exists.\");\n        }\n        db.del(&key);\n    }\n\n    // Stub: store as string value\n    db.string_set(&key, value, now);\n\n    if ttl_ms > 0 {\n        db.ttl.insert(key, Duration::from_millis(ttl_ms as u64));\n    }\n\n    Frame::ok()\n}\n\n// ── Pattern matching ─────────────────────────────────────────────────\n\n/// Match keys against a glob-style pattern (like Redis KEYS/SCAN).\nfn match_keys(keys: &[String], pattern: &str) -> Vec<String> {\n    if pattern == \"*\" {\n        return keys.to_vec();\n    }\n\n    keys.iter()\n        .filter(|k| crate::keys::glob_match(pattern, k))\n        .cloned()\n        .collect()\n}\n"
  },
  {
    "path": "miniredis/src/cmd/geo.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_INVALID_INT, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, err_wrong_number,\n};\nuse crate::frame::Frame;\nuse crate::geo::{from_geohash, haversine_distance, parse_unit, to_geohash};\nuse crate::types::{Direction, KeyType};\n\nconst MSG_UNSUPPORTED_UNIT: &str = \"ERR unsupported unit provided. please use M, KM, FT, MI\";\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"GEOADD\", cmd_geoadd, false, -5);\n    table.add(\"GEODIST\", cmd_geodist, true, -4);\n    table.add(\"GEOPOS\", cmd_geopos, true, -2);\n    table.add(\"GEORADIUS\", cmd_georadius, false, -6);\n    table.add(\"GEORADIUS_RO\", cmd_georadius_ro, true, -6);\n    table.add(\"GEORADIUSBYMEMBER\", cmd_georadiusbymember, false, -5);\n    table.add(\"GEORADIUSBYMEMBER_RO\", cmd_georadiusbymember_ro, true, -5);\n}\n\n/// GEOADD key longitude latitude member [longitude latitude member ...]\nfn cmd_geoadd(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = to_str(&args[0]);\n    let triplets = &args[1..];\n\n    if !triplets.len().is_multiple_of(3) {\n        return Frame::error(err_wrong_number(\"geoadd\"));\n    }\n\n    let mut entries = Vec::new();\n    let mut i = 0;\n    while i + 2 < triplets.len() {\n        let raw_long = to_str(&triplets[i]);\n        let raw_lat = to_str(&triplets[i + 1]);\n        let name = to_str(&triplets[i + 2]);\n        i += 3;\n\n        let longitude: f64 = match raw_long.parse() {\n            Ok(v) => v,\n            Err(_) => return Frame::error(\"ERR value is not a valid float\"),\n        };\n        let latitude: f64 = match raw_lat.parse() {\n            Ok(v) => v,\n            Err(_) => return Frame::error(\"ERR value is not a valid float\"),\n        };\n\n        if !(-85.05112878..=85.05112878).contains(&latitude)\n            || !(-180.0..=180.0).contains(&longitude)\n        {\n            return Frame::error(format!(\n                \"ERR invalid longitude,latitude pair {:.6},{:.6}\",\n                longitude, latitude\n            ));\n        }\n\n        let score = to_geohash(longitude, latitude) as f64;\n        entries.push((name, score));\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut added = 0i64;\n    for (name, score) in &entries {\n        if db.sset_add(&key, *score, name, now) {\n            added += 1;\n        }\n    }\n\n    Frame::Integer(added)\n}\n\n/// GEODIST key member1 member2 [unit]\nfn cmd_geodist(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = to_str(&args[0]);\n    let from = to_str(&args[1]);\n    let to = to_str(&args[2]);\n    let remaining = &args[3..];\n\n    let unit = if !remaining.is_empty() {\n        to_str(&remaining[0])\n    } else {\n        \"m\".to_string()\n    };\n\n    if remaining.len() > 1 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let to_meter = match parse_unit(&unit) {\n        Some(v) => v,\n        None => return Frame::error(MSG_UNSUPPORTED_UNIT),\n    };\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Null;\n    }\n    if db.keys.get(&key) != Some(&KeyType::SortedSet) {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let from_score = match db.sset_score(&key, &from) {\n        Some(s) => s,\n        None => return Frame::Null,\n    };\n    let to_score = match db.sset_score(&key, &to) {\n        Some(s) => s,\n        None => return Frame::Null,\n    };\n\n    let (from_lng, from_lat) = from_geohash(from_score as u64);\n    let (to_lng, to_lat) = from_geohash(to_score as u64);\n\n    let dist = haversine_distance(from_lat, from_lng, to_lat, to_lng) / to_meter;\n    Frame::Bulk(format!(\"{:.4}\", dist).into())\n}\n\n/// GEOPOS key member [member ...]\nfn cmd_geopos(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = to_str(&args[0]);\n    let members = &args[1..];\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut results = Vec::with_capacity(members.len());\n    for member_arg in members {\n        let member = to_str(member_arg);\n        match db.sset_score(&key, &member) {\n            Some(score) => {\n                let (lng, lat) = from_geohash(score as u64);\n                results.push(Frame::Array(vec![\n                    Frame::Bulk(format!(\"{:.6}\", lng).into()),\n                    Frame::Bulk(format!(\"{:.6}\", lat).into()),\n                ]));\n            }\n            None => {\n                results.push(Frame::NullArray);\n            }\n        }\n    }\n\n    Frame::Array(results)\n}\n\n// ── Shared radius search types and helpers ──────────────────────────\n\nstruct GeoMatch {\n    name: String,\n    score: f64,\n    distance: f64,\n    longitude: f64,\n    latitude: f64,\n}\n\n#[derive(PartialEq)]\nenum SortDir {\n    Unsorted,\n    Asc,\n    Desc,\n}\n\nstruct RadiusOpts {\n    with_dist: bool,\n    with_coord: bool,\n    direction: SortDir,\n    count: usize,\n    store_key: Option<String>,\n    storedist_key: Option<String>,\n}\n\nfn within_radius(\n    state: &Arc<SharedState>,\n    db_idx: usize,\n    key: &str,\n    longitude: f64,\n    latitude: f64,\n    radius_meters: f64,\n) -> Vec<GeoMatch> {\n    let inner = state.lock();\n    let db = inner.db(db_idx);\n\n    let ss = match db.sorted_set_keys.get(key) {\n        Some(ss) => ss,\n        None => return Vec::new(),\n    };\n\n    let elems = ss.by_score(Direction::Asc);\n    let mut matches = Vec::new();\n    for el in &elems {\n        let (el_lng, el_lat) = from_geohash(el.score as u64);\n        let d = haversine_distance(latitude, longitude, el_lat, el_lng);\n        if d <= radius_meters {\n            matches.push(GeoMatch {\n                name: el.member.clone(),\n                score: el.score,\n                distance: d,\n                longitude: el_lng,\n                latitude: el_lat,\n            });\n        }\n    }\n    matches\n}\n\nfn parse_radius_opts(args: &[Vec<u8>], read_only: bool) -> Result<RadiusOpts, Frame> {\n    let mut opts = RadiusOpts {\n        with_dist: false,\n        with_coord: false,\n        direction: SortDir::Unsorted,\n        count: 0,\n        store_key: None,\n        storedist_key: None,\n    };\n\n    let mut i = 0;\n    while i < args.len() {\n        let arg = to_str(&args[i]).to_uppercase();\n        match arg.as_str() {\n            \"WITHCOORD\" => opts.with_coord = true,\n            \"WITHDIST\" => opts.with_dist = true,\n            \"ASC\" => opts.direction = SortDir::Asc,\n            \"DESC\" => opts.direction = SortDir::Desc,\n            \"COUNT\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                let n: i64 = match to_str(&args[i]).parse() {\n                    Ok(v) => v,\n                    Err(_) => return Err(Frame::error(MSG_INVALID_INT)),\n                };\n                if n <= 0 {\n                    return Err(Frame::error(\"ERR COUNT must be > 0\"));\n                }\n                opts.count = n as usize;\n            }\n            \"STORE\" => {\n                if read_only {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                i += 1;\n                if i >= args.len() {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                opts.store_key = Some(to_str(&args[i]));\n            }\n            \"STOREDIST\" => {\n                if read_only {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                i += 1;\n                if i >= args.len() {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                opts.storedist_key = Some(to_str(&args[i]));\n            }\n            _ => return Err(Frame::error(MSG_SYNTAX_ERROR)),\n        }\n        i += 1;\n    }\n\n    Ok(opts)\n}\n\nfn format_radius_results(matches: &[GeoMatch], opts: &RadiusOpts, to_meter: f64) -> Frame {\n    let mut frames = Vec::with_capacity(matches.len());\n    for m in matches {\n        if !opts.with_dist && !opts.with_coord {\n            frames.push(Frame::bulk_string(&m.name));\n        } else {\n            let mut inner = Vec::new();\n            inner.push(Frame::bulk_string(&m.name));\n            if opts.with_dist {\n                inner.push(Frame::Bulk(format!(\"{:.4}\", m.distance / to_meter).into()));\n            }\n            if opts.with_coord {\n                inner.push(Frame::Array(vec![\n                    Frame::Bulk(format!(\"{:.6}\", m.longitude).into()),\n                    Frame::Bulk(format!(\"{:.6}\", m.latitude).into()),\n                ]));\n            }\n            frames.push(Frame::Array(inner));\n        }\n    }\n    Frame::Array(frames)\n}\n\nfn apply_sort_and_count(matches: &mut Vec<GeoMatch>, opts: &RadiusOpts) {\n    if opts.direction != SortDir::Unsorted {\n        matches.sort_by(|a, b| {\n            if opts.direction == SortDir::Desc {\n                b.distance\n                    .partial_cmp(&a.distance)\n                    .unwrap_or(std::cmp::Ordering::Equal)\n            } else {\n                a.distance\n                    .partial_cmp(&b.distance)\n                    .unwrap_or(std::cmp::Ordering::Equal)\n            }\n        });\n    }\n    if opts.count > 0 && matches.len() > opts.count {\n        matches.truncate(opts.count);\n    }\n}\n\n// ── GEORADIUS / GEORADIUS_RO ────────────────────────────────────────\n\nfn cmd_georadius_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    read_only: bool,\n    cmd_name: &str,\n) -> Frame {\n    let key = to_str(&args[0]);\n\n    let longitude: f64 = match to_str(&args[1]).parse() {\n        Ok(v) => v,\n        Err(_) => return Frame::error(err_wrong_number(cmd_name)),\n    };\n    let latitude: f64 = match to_str(&args[2]).parse() {\n        Ok(v) => v,\n        Err(_) => return Frame::error(err_wrong_number(cmd_name)),\n    };\n    let radius: f64 = match to_str(&args[3]).parse() {\n        Ok(v) if v >= 0.0 => v,\n        _ => return Frame::error(err_wrong_number(cmd_name)),\n    };\n    let to_meter = match parse_unit(&to_str(&args[4])) {\n        Some(v) => v,\n        None => return Frame::error(err_wrong_number(cmd_name)),\n    };\n\n    let opts = match parse_radius_opts(&args[5..], read_only) {\n        Ok(o) => o,\n        Err(e) => return e,\n    };\n\n    // Check STORE/STOREDIST incompatibility with WITHDIST/WITHCOORD\n    if (opts.store_key.is_some() || opts.storedist_key.is_some())\n        && (opts.with_dist || opts.with_coord)\n    {\n        return Frame::error(\n            \"ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options\",\n        );\n    }\n\n    let mut matches = within_radius(\n        state,\n        ctx.selected_db,\n        &key,\n        longitude,\n        latitude,\n        radius * to_meter,\n    );\n    apply_sort_and_count(&mut matches, &opts);\n\n    // Handle STORE\n    if let Some(ref store_key) = opts.store_key {\n        let mut inner = state.lock();\n        let now = inner.effective_now();\n        let db = inner.db_mut(ctx.selected_db);\n        db.del(store_key);\n        for m in &matches {\n            db.sset_add(store_key, m.score, &m.name, now);\n        }\n        return Frame::Integer(matches.len() as i64);\n    }\n\n    // Handle STOREDIST\n    if let Some(ref storedist_key) = opts.storedist_key {\n        let mut inner = state.lock();\n        let now = inner.effective_now();\n        let db = inner.db_mut(ctx.selected_db);\n        db.del(storedist_key);\n        for m in &matches {\n            db.sset_add(storedist_key, m.distance / to_meter, &m.name, now);\n        }\n        return Frame::Integer(matches.len() as i64);\n    }\n\n    format_radius_results(&matches, &opts, to_meter)\n}\n\nfn cmd_georadius(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_georadius_impl(state, ctx, args, false, \"georadius\")\n}\n\nfn cmd_georadius_ro(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_georadius_impl(state, ctx, args, true, \"georadius_ro\")\n}\n\n// ── GEORADIUSBYMEMBER / GEORADIUSBYMEMBER_RO ────────────────────────\n\nfn cmd_georadiusbymember_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    read_only: bool,\n    cmd_name: &str,\n) -> Frame {\n    let key = to_str(&args[0]);\n    let member = to_str(&args[1]);\n\n    let radius: f64 = match to_str(&args[2]).parse() {\n        Ok(v) if v >= 0.0 => v,\n        _ => return Frame::error(err_wrong_number(cmd_name)),\n    };\n    let to_meter = match parse_unit(&to_str(&args[3])) {\n        Some(v) => v,\n        None => return Frame::error(err_wrong_number(cmd_name)),\n    };\n\n    let opts = match parse_radius_opts(&args[4..], read_only) {\n        Ok(o) => o,\n        Err(e) => return e,\n    };\n\n    // Check STORE/STOREDIST incompatibility\n    if (opts.store_key.is_some() || opts.storedist_key.is_some())\n        && (opts.with_dist || opts.with_coord)\n    {\n        return Frame::error(\n            \"ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options\",\n        );\n    }\n\n    // Look up the member's coordinates\n    {\n        let inner = state.lock();\n        let db = inner.db(ctx.selected_db);\n\n        if !db.keys.contains_key(&key) {\n            return Frame::Null;\n        }\n        if db.keys.get(&key) != Some(&KeyType::SortedSet) {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n\n        match db.sset_score(&key, &member) {\n            Some(score) => {\n                let (longitude, latitude) = from_geohash(score as u64);\n                drop(inner);\n\n                let mut matches = within_radius(\n                    state,\n                    ctx.selected_db,\n                    &key,\n                    longitude,\n                    latitude,\n                    radius * to_meter,\n                );\n                apply_sort_and_count(&mut matches, &opts);\n\n                // Handle STORE\n                if let Some(ref store_key) = opts.store_key {\n                    let mut inner = state.lock();\n                    let now = inner.effective_now();\n                    let db = inner.db_mut(ctx.selected_db);\n                    db.del(store_key);\n                    for m in &matches {\n                        db.sset_add(store_key, m.score, &m.name, now);\n                    }\n                    return Frame::Integer(matches.len() as i64);\n                }\n\n                // Handle STOREDIST\n                if let Some(ref storedist_key) = opts.storedist_key {\n                    let mut inner = state.lock();\n                    let now = inner.effective_now();\n                    let db = inner.db_mut(ctx.selected_db);\n                    db.del(storedist_key);\n                    for m in &matches {\n                        db.sset_add(storedist_key, m.distance / to_meter, &m.name, now);\n                    }\n                    return Frame::Integer(matches.len() as i64);\n                }\n\n                format_radius_results(&matches, &opts, to_meter)\n            }\n            None => Frame::error(\"ERR could not decode requested zset member\"),\n        }\n    }\n}\n\nfn cmd_georadiusbymember(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_georadiusbymember_impl(state, ctx, args, false, \"georadiusbymember\")\n}\n\nfn cmd_georadiusbymember_ro(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n) -> Frame {\n    cmd_georadiusbymember_impl(state, ctx, args, true, \"georadiusbymember_ro\")\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\nfn to_str(bytes: &[u8]) -> String {\n    String::from_utf8_lossy(bytes).to_string()\n}\n"
  },
  {
    "path": "miniredis/src/cmd/hash.rs",
    "content": "use std::sync::Arc;\n\nuse rand::Rng;\nuse rand::seq::SliceRandom;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_GT_AND_LT, MSG_INT_OVERFLOW, MSG_INVALID_CURSOR, MSG_INVALID_FLOAT,\n    MSG_INVALID_INT, MSG_NUM_FIELDS_INVALID, MSG_NUM_FIELDS_PARAMETER, MSG_NX_AND_XX_GT_LT,\n    MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, err_wrong_number,\n};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\nuse super::parse_int;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"HSET\", cmd_hset, false, -4);\n    table.add(\"HSETNX\", cmd_hsetnx, false, 4);\n    table.add(\"HMSET\", cmd_hmset, false, -4);\n    table.add(\"HGET\", cmd_hget, true, 3);\n    table.add(\"HMGET\", cmd_hmget, true, -3);\n    table.add(\"HDEL\", cmd_hdel, false, -3);\n    table.add(\"HEXISTS\", cmd_hexists, true, 3);\n    table.add(\"HGETALL\", cmd_hgetall, true, 2);\n    table.add(\"HKEYS\", cmd_hkeys, true, 2);\n    table.add(\"HVALS\", cmd_hvals, true, 2);\n    table.add(\"HLEN\", cmd_hlen, true, 2);\n    table.add(\"HINCRBY\", cmd_hincrby, false, 4);\n    table.add(\"HINCRBYFLOAT\", cmd_hincrbyfloat, false, 4);\n    table.add(\"HSTRLEN\", cmd_hstrlen, true, 3);\n    table.add(\"HSCAN\", cmd_hscan, true, -3);\n    table.add(\"HRANDFIELD\", cmd_hrandfield, true, -2);\n    table.add(\"HEXPIRE\", cmd_hexpire, false, -6);\n}\n\n/// HSET key field value [field value ...]\nfn cmd_hset(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len().is_multiple_of(2) {\n        return Frame::error(err_wrong_number(\"hset\"));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let pairs: Vec<(String, Vec<u8>)> = args[1..]\n        .chunks_exact(2)\n        .map(|c| (String::from_utf8_lossy(&c[0]).into_owned(), c[1].clone()))\n        .collect();\n\n    let added = db.hash_set(&key, &pairs, now);\n    Frame::Integer(added)\n}\n\n/// HSETNX key field value\nfn cmd_hsetnx(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let field = String::from_utf8_lossy(&args[1]).into_owned();\n    let value = args[2].clone();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // Only set if field doesn't exist\n    if let Some(hash) = db.hash_keys.get(&key)\n        && hash.contains_key(&field)\n    {\n        return Frame::Integer(0);\n    }\n\n    db.hash_set(&key, &[(field, value)], now);\n    Frame::Integer(1)\n}\n\n/// HMSET key field value [field value ...]\nfn cmd_hmset(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len().is_multiple_of(2) {\n        return Frame::error(err_wrong_number(\"hmset\"));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let pairs: Vec<(String, Vec<u8>)> = args[1..]\n        .chunks_exact(2)\n        .map(|c| (String::from_utf8_lossy(&c[0]).into_owned(), c[1].clone()))\n        .collect();\n\n    db.hash_set(&key, &pairs, now);\n    Frame::ok()\n}\n\n/// HGET key field\nfn cmd_hget(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let field = String::from_utf8_lossy(&args[1]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.hash_get(&key, &field) {\n        Some(val) => Frame::Bulk(val.clone().into()),\n        None => Frame::Null,\n    }\n}\n\n/// HMGET key field [field ...]\nfn cmd_hmget(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut results = Vec::with_capacity(args.len() - 1);\n    for arg in &args[1..] {\n        let field = String::from_utf8_lossy(arg);\n        match db.hash_get(&key, &field) {\n            Some(val) => results.push(Frame::Bulk(val.clone().into())),\n            None => results.push(Frame::Null),\n        }\n    }\n\n    Frame::Array(results)\n}\n\n/// HDEL key field [field ...]\nfn cmd_hdel(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    let fields: Vec<String> = args[1..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n\n    let count = db.hash_del(&key, &fields, now);\n    Frame::Integer(count)\n}\n\n/// HEXISTS key field\nfn cmd_hexists(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let field = String::from_utf8_lossy(&args[1]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.hash_get(&key, &field) {\n        Some(_) => Frame::Integer(1),\n        None => Frame::Integer(0),\n    }\n}\n\n/// HGETALL key\nfn cmd_hgetall(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let fields = db.hash_fields(&key);\n\n    if ctx.resp3 {\n        let mut pairs = Vec::with_capacity(fields.len());\n        for field in &fields {\n            let val = db.hash_get(&key, field).cloned().unwrap_or_default();\n            pairs.push((Frame::Bulk(field.clone().into()), Frame::Bulk(val.into())));\n        }\n        Frame::Map(pairs)\n    } else {\n        let mut result = Vec::with_capacity(fields.len() * 2);\n        for field in &fields {\n            result.push(Frame::Bulk(field.clone().into()));\n            if let Some(val) = db.hash_get(&key, field) {\n                result.push(Frame::Bulk(val.clone().into()));\n            }\n        }\n        Frame::Array(result)\n    }\n}\n\n/// HKEYS key\nfn cmd_hkeys(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let fields = db.hash_fields(&key);\n    Frame::Array(fields.into_iter().map(|f| Frame::Bulk(f.into())).collect())\n}\n\n/// HVALS key\nfn cmd_hvals(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let values = db.hash_values(&key);\n    Frame::Array(values.into_iter().map(|v| Frame::Bulk(v.into())).collect())\n}\n\n/// HLEN key\nfn cmd_hlen(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let len = db.hash_keys.get(key.as_ref()).map(|h| h.len()).unwrap_or(0);\n    Frame::Integer(len as i64)\n}\n\n/// HINCRBY key field increment\nfn cmd_hincrby(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let field = String::from_utf8_lossy(&args[1]).into_owned();\n    let delta: i64 = match String::from_utf8_lossy(&args[2]).parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let current: i64 = match db.hash_get(&key, &field) {\n        Some(v) => match String::from_utf8_lossy(v).parse::<i64>() {\n            Ok(n) => n,\n            Err(_) => return Frame::error(MSG_INVALID_INT),\n        },\n        None => 0,\n    };\n\n    let new_val = match current.checked_add(delta) {\n        Some(n) => n,\n        None => {\n            return Frame::error(MSG_INT_OVERFLOW);\n        }\n    };\n\n    db.hash_set(&key, &[(field, new_val.to_string().into_bytes())], now);\n    Frame::Integer(new_val)\n}\n\n/// HINCRBYFLOAT key field increment\nfn cmd_hincrbyfloat(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let field = String::from_utf8_lossy(&args[1]).into_owned();\n    let delta_str = String::from_utf8_lossy(&args[2]).into_owned();\n\n    // Validate by parsing as f64\n    let delta_f64: f64 = match delta_str.parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(MSG_INVALID_FLOAT),\n    };\n    if delta_f64.is_nan() || delta_f64.is_infinite() {\n        return Frame::error(MSG_INVALID_FLOAT);\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let current_str = match db.hash_get(&key, &field) {\n        Some(v) => {\n            let s = String::from_utf8_lossy(v).into_owned();\n            if s.parse::<f64>().is_err() {\n                return Frame::error(MSG_INVALID_FLOAT);\n            }\n            s\n        }\n        None => \"0\".to_string(),\n    };\n\n    let formatted = crate::cmd::string::decimal_add_format(&current_str, &delta_str);\n    db.hash_set(&key, &[(field, formatted.as_bytes().to_vec())], now);\n    Frame::Bulk(formatted.into_bytes().into())\n}\n\n/// HSTRLEN key field\nfn cmd_hstrlen(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let field = String::from_utf8_lossy(&args[1]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.hash_get(&key, &field) {\n        Some(val) => Frame::Integer(val.len() as i64),\n        None => Frame::Integer(0),\n    }\n}\n\n/// HSCAN key cursor [MATCH pattern] [COUNT count]\nfn cmd_hscan(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let _cursor: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_CURSOR),\n    };\n\n    let opts = match super::parse_scan_opts(&args[2..], false) {\n        Ok(o) => o,\n        Err(e) => return e,\n    };\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut fields = db.hash_fields(&key);\n\n    if let Some(ref pat) = opts.pattern {\n        fields = crate::keys::match_keys_vec(&fields, pat);\n    }\n\n    let mut result = Vec::new();\n    for field in &fields {\n        result.push(Frame::Bulk(field.clone().into()));\n        let val = db.hash_get(&key, field).cloned().unwrap_or_default();\n        result.push(Frame::Bulk(val.into()));\n    }\n\n    Frame::Array(vec![Frame::Bulk(\"0\".into()), Frame::Array(result)])\n}\n\n/// HRANDFIELD key [count [WITHVALUES]]\nfn cmd_hrandfield(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 3 {\n        return Frame::error(err_wrong_number(\"hrandfield\"));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut count: i64 = 0;\n    let mut with_count = false;\n    let mut with_values = false;\n\n    if args.len() >= 2 {\n        match parse_int(&args[1]) {\n            Some(n) => {\n                count = n;\n                with_count = true;\n            }\n            None => return Frame::error(MSG_INVALID_INT),\n        }\n    }\n    if args.len() == 3 {\n        let opt = String::from_utf8_lossy(&args[2]).to_uppercase();\n        if opt == \"WITHVALUES\" {\n            with_values = true;\n        } else {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return if with_count {\n            Frame::Array(vec![])\n        } else {\n            Frame::Null\n        };\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut fields = db.hash_fields(&key);\n    if fields.is_empty() {\n        return if with_count {\n            Frame::Array(vec![])\n        } else {\n            Frame::Null\n        };\n    }\n\n    // Collect values before shuffling (avoids borrow issues with inner.rng)\n    let field_values: std::collections::HashMap<String, Vec<u8>> = fields\n        .iter()\n        .map(|f| (f.clone(), db.hash_get(&key, f).cloned().unwrap_or_default()))\n        .collect();\n\n    if count < 0 {\n        let abs_count = (-count) as usize;\n        let mut result = Vec::new();\n        for _ in 0..abs_count {\n            let idx = inner.rng.random_range(0..fields.len());\n            result.push(Frame::Bulk(fields[idx].clone().into()));\n            if with_values {\n                let val = field_values.get(&fields[idx]).cloned().unwrap_or_default();\n                result.push(Frame::Bulk(val.into()));\n            }\n        }\n        return Frame::Array(result);\n    }\n\n    fields.shuffle(&mut inner.rng);\n    let take = (count as usize).min(fields.len());\n\n    if !with_count {\n        return Frame::Bulk(fields[0].clone().into());\n    }\n\n    let mut result = Vec::new();\n    for f in &fields[..take] {\n        result.push(Frame::Bulk(f.clone().into()));\n        if with_values {\n            let val = field_values.get(f).cloned().unwrap_or_default();\n            result.push(Frame::Bulk(val.into()));\n        }\n    }\n    Frame::Array(result)\n}\n\n/// HEXPIRE key seconds [NX|XX|GT|LT] FIELDS numfields field [field ...]\nfn cmd_hexpire(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let ttl_secs: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut nx = false;\n    let mut xx = false;\n    let mut gt = false;\n    let mut lt = false;\n    let mut fields: Vec<String> = Vec::new();\n\n    let mut i = 2;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"NX\" => {\n                nx = true;\n                i += 1;\n            }\n            \"XX\" => {\n                xx = true;\n                i += 1;\n            }\n            \"GT\" => {\n                gt = true;\n                i += 1;\n            }\n            \"LT\" => {\n                lt = true;\n                i += 1;\n            }\n            \"FIELDS\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_NUM_FIELDS_INVALID);\n                }\n                let num_fields: i64 = match parse_int(&args[i]) {\n                    Some(n) => n,\n                    None => return Frame::error(MSG_NUM_FIELDS_INVALID),\n                };\n                if num_fields <= 0 {\n                    return Frame::error(MSG_NUM_FIELDS_INVALID);\n                }\n                i += 1;\n                let num_fields = num_fields as usize;\n                if i + num_fields > args.len() {\n                    return Frame::error(MSG_NUM_FIELDS_PARAMETER);\n                }\n                for j in 0..num_fields {\n                    fields.push(String::from_utf8_lossy(&args[i + j]).into_owned());\n                }\n                i += num_fields;\n            }\n            _ => {\n                return Frame::error(\n                    \"ERR Mandatory argument FIELDS is missing or not at the right position\",\n                );\n            }\n        }\n    }\n\n    if gt && lt {\n        return Frame::error(MSG_GT_AND_LT);\n    }\n    if nx && (xx || gt || lt) {\n        return Frame::error(MSG_NX_AND_XX_GT_LT);\n    }\n\n    if fields.is_empty() {\n        return Frame::error(\n            \"ERR Mandatory argument FIELDS is missing or not at the right position\",\n        );\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    // Key doesn't exist: return -2 for all fields\n    if !db.keys.contains_key(&key) {\n        return Frame::Array(fields.iter().map(|_| Frame::Integer(-2)).collect());\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Hash\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let new_ttl = std::time::Duration::from_secs(ttl_secs as u64);\n\n    let field_ttls = db.hash_field_ttls.entry(key.clone()).or_default();\n\n    let mut results = Vec::with_capacity(fields.len());\n    for field in &fields {\n        // Check field exists in hash\n        let field_exists = db\n            .hash_keys\n            .get(&key)\n            .is_some_and(|h| h.contains_key(field));\n        if !field_exists {\n            results.push(Frame::Integer(-2));\n            continue;\n        }\n\n        let current_ttl = field_ttls.get(field).copied();\n        let has_ttl = current_ttl.is_some();\n\n        // NX: set only when field has no expiration\n        if nx && has_ttl {\n            results.push(Frame::Integer(0));\n            continue;\n        }\n\n        // XX: set only when field has existing expiration\n        if xx && !has_ttl {\n            results.push(Frame::Integer(0));\n            continue;\n        }\n\n        // GT: set only when new TTL > current TTL\n        if gt && (!has_ttl || new_ttl <= current_ttl.unwrap()) {\n            results.push(Frame::Integer(0));\n            continue;\n        }\n\n        // LT: set only when new TTL < current TTL (and field has expiration)\n        if lt && has_ttl && new_ttl >= current_ttl.unwrap() {\n            results.push(Frame::Integer(0));\n            continue;\n        }\n\n        field_ttls.insert(field.clone(), new_ttl);\n        results.push(Frame::Integer(1));\n    }\n\n    db.incr_version(&key, now);\n    Frame::Array(results)\n}\n"
  },
  {
    "path": "miniredis/src/cmd/hll.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{CommandTable, MSG_NOT_VALID_HLL_VALUE, err_wrong_number};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"PFADD\", cmd_pfadd, false, -2);\n    table.add(\"PFCOUNT\", cmd_pfcount, true, -2);\n    table.add(\"PFMERGE\", cmd_pfmerge, false, -2);\n}\n\n/// PFADD key element [element ...]\nfn cmd_pfadd(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() < 2 {\n        return Frame::error(err_wrong_number(\"pfadd\"));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let items: Vec<&str> = args[1..]\n        .iter()\n        .map(|a| std::str::from_utf8(a).unwrap_or(\"\"))\n        .collect();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    // Check type if key already exists\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::HyperLogLog\n    {\n        return Frame::error(MSG_NOT_VALID_HLL_VALUE);\n    }\n\n    let altered = db.hll_add(&key, &items, now);\n    Frame::Integer(altered)\n}\n\n/// PFCOUNT key [key ...]\nfn cmd_pfcount(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let keys: Vec<&str> = args\n        .iter()\n        .map(|a| std::str::from_utf8(a).unwrap_or(\"\"))\n        .collect();\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    match db.hll_count(&keys) {\n        Ok(count) => Frame::Integer(count),\n        Err(msg) => Frame::error(msg),\n    }\n}\n\n/// PFMERGE destkey [sourcekey ...]\nfn cmd_pfmerge(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let keys: Vec<&str> = args\n        .iter()\n        .map(|a| std::str::from_utf8(a).unwrap_or(\"\"))\n        .collect();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    match db.hll_merge(&keys, now) {\n        Ok(()) => Frame::ok(),\n        Err(msg) => Frame::error(msg),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/list.rs",
    "content": "use std::sync::Arc;\n\nuse super::parse_int;\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_INVALID_INT, MSG_INVALID_TIMEOUT, MSG_KEY_NOT_FOUND, MSG_OUT_OF_RANGE,\n    MSG_SYNTAX_ERROR, MSG_TIMEOUT_IS_OUT_OF_RANGE, MSG_TIMEOUT_NEGATIVE, MSG_WRONG_TYPE,\n    err_wrong_number,\n};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"LPUSH\", cmd_lpush, false, -3);\n    table.add(\"RPUSH\", cmd_rpush, false, -3);\n    table.add(\"LPUSHX\", cmd_lpushx, false, -3);\n    table.add(\"RPUSHX\", cmd_rpushx, false, -3);\n    table.add(\"LPOP\", cmd_lpop, false, -2);\n    table.add(\"RPOP\", cmd_rpop, false, -2);\n    table.add(\"LLEN\", cmd_llen, true, 2);\n    table.add(\"LINDEX\", cmd_lindex, true, 3);\n    table.add(\"LRANGE\", cmd_lrange, true, 4);\n    table.add(\"LSET\", cmd_lset, false, 4);\n    table.add(\"LINSERT\", cmd_linsert, false, 5);\n    table.add(\"LREM\", cmd_lrem, false, 4);\n    table.add(\"LTRIM\", cmd_ltrim, false, 4);\n    table.add(\"RPOPLPUSH\", cmd_rpoplpush, false, 3);\n    table.add(\"LMOVE\", cmd_lmove, false, 5);\n    table.add(\"LPOS\", cmd_lpos, true, -3);\n    // Blocking commands: registered for MULTI/EXEC queueing (non-blocking attempt)\n    table.add(\"BLPOP\", cmd_blpop, false, -3);\n    table.add(\"BRPOP\", cmd_brpop, false, -3);\n    table.add(\"BRPOPLPUSH\", cmd_brpoplpush, false, 4);\n    table.add(\"BLMOVE\", cmd_blmove, false, 6);\n}\n\n/// LPUSH key element [element ...]\nfn cmd_lpush(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xpush(state, ctx, args, true, false)\n}\n\n/// RPUSH key element [element ...]\nfn cmd_rpush(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xpush(state, ctx, args, false, false)\n}\n\n/// LPUSHX key element [element ...]\nfn cmd_lpushx(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xpush(state, ctx, args, true, true)\n}\n\n/// RPUSHX key element [element ...]\nfn cmd_rpushx(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xpush(state, ctx, args, false, true)\n}\n\nfn cmd_xpush(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    left: bool,\n    only_existing: bool,\n) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // PUSHX: only push to existing keys\n    if only_existing && !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    let values: Vec<Vec<u8>> = args[1..].to_vec();\n    let len = if left {\n        db.list_lpush(&key, &values, now)\n    } else {\n        db.list_rpush(&key, &values, now)\n    };\n\n    Frame::Integer(len)\n}\n\n/// LPOP key [count]\nfn cmd_lpop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xpop(state, ctx, args, true)\n}\n\n/// RPOP key [count]\nfn cmd_rpop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xpop(state, ctx, args, false)\n}\n\nfn cmd_xpop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>], left: bool) -> Frame {\n    let cmd_name = if left { \"lpop\" } else { \"rpop\" };\n\n    if args.len() > 2 {\n        return Frame::error(err_wrong_number(cmd_name));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let count = if args.len() > 1 {\n        match parse_int(&args[1]) {\n            Some(n) if n < 0 => return Frame::error(MSG_OUT_OF_RANGE),\n            Some(n) => Some(n as usize),\n            None => return Frame::error(MSG_INVALID_INT),\n        }\n    } else {\n        None\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if !db.keys.contains_key(&key) {\n        return if count.is_some() {\n            Frame::NullArray\n        } else {\n            Frame::Null\n        };\n    }\n\n    match count {\n        Some(n) => {\n            let mut results = Vec::new();\n            for _ in 0..n {\n                let val = if left {\n                    db.list_lpop(&key, now)\n                } else {\n                    db.list_rpop(&key, now)\n                };\n                match val {\n                    Some(v) => results.push(Frame::Bulk(v.into())),\n                    None => break,\n                }\n            }\n            Frame::Array(results)\n        }\n        None => {\n            let val = if left {\n                db.list_lpop(&key, now)\n            } else {\n                db.list_rpop(&key, now)\n            };\n            match val {\n                Some(v) => Frame::Bulk(v.into()),\n                None => Frame::Null,\n            }\n        }\n    }\n}\n\n/// LLEN key\nfn cmd_llen(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let len = db.list_keys.get(key.as_ref()).map(|l| l.len()).unwrap_or(0);\n    Frame::Integer(len as i64)\n}\n\n/// LINDEX key index\nfn cmd_lindex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    // Reject \"-0\" (Go miniredis compat)\n    if args[1] == b\"-0\" {\n        return Frame::error(MSG_INVALID_INT);\n    }\n    let index: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let list = match db.list_keys.get(key.as_ref()) {\n        Some(l) => l,\n        None => return Frame::Null,\n    };\n\n    let len = list.len() as i64;\n    let mut idx = index;\n    if idx < 0 {\n        idx += len;\n    }\n    if idx < 0 || idx >= len {\n        return Frame::Null;\n    }\n\n    Frame::Bulk(list[idx as usize].clone().into())\n}\n\n/// LRANGE key start stop\nfn cmd_lrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let start: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let end: i64 = match parse_int(&args[2]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let list = match db.list_keys.get(key.as_ref()) {\n        Some(l) => l,\n        None => return Frame::Array(vec![]),\n    };\n\n    let len = list.len() as i64;\n    let (rs, re) = redis_range(start, end, len);\n    if rs > re || rs >= len {\n        return Frame::Array(vec![]);\n    }\n\n    let results: Vec<Frame> = (rs..=re)\n        .map(|i| Frame::Bulk(list[i as usize].clone().into()))\n        .collect();\n\n    Frame::Array(results)\n}\n\n/// LSET key index element\nfn cmd_lset(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let index: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let value = args[2].clone();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::error(MSG_KEY_NOT_FOUND);\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let list = match db.list_keys.get_mut(&key) {\n        Some(l) => l,\n        None => return Frame::error(MSG_KEY_NOT_FOUND),\n    };\n\n    let len = list.len() as i64;\n    let mut idx = index;\n    if idx < 0 {\n        idx += len;\n    }\n    if idx < 0 || idx >= len {\n        return Frame::error(MSG_OUT_OF_RANGE);\n    }\n\n    list[idx as usize] = value;\n    db.incr_version(&key, now);\n    Frame::ok()\n}\n\n/// LINSERT key BEFORE|AFTER pivot element\nfn cmd_linsert(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let position = String::from_utf8_lossy(&args[1]).to_uppercase();\n    let before = match position.as_str() {\n        \"BEFORE\" => true,\n        \"AFTER\" => false,\n        _ => return Frame::error(MSG_SYNTAX_ERROR),\n    };\n    let pivot = &args[2];\n    let value = args[3].clone();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    let list = match db.list_keys.get_mut(&key) {\n        Some(l) => l,\n        None => return Frame::Integer(0),\n    };\n\n    // Find pivot\n    let pos = list.iter().position(|el| el == pivot);\n    match pos {\n        Some(i) => {\n            let insert_at = if before { i } else { i + 1 };\n            list.insert(insert_at, value);\n            let new_len = list.len() as i64;\n            db.incr_version(&key, now);\n            Frame::Integer(new_len)\n        }\n        None => Frame::Integer(-1),\n    }\n}\n\n/// LREM key count element\nfn cmd_lrem(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let count: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let element = &args[2];\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let list = match db.list_keys.get_mut(&key) {\n        Some(l) => l,\n        None => return Frame::Integer(0),\n    };\n\n    let mut removed = 0i64;\n    let max_remove = if count == 0 {\n        list.len()\n    } else {\n        count.unsigned_abs() as usize\n    };\n\n    if count >= 0 {\n        // Remove from head\n        let mut i = 0;\n        while i < list.len() && (removed as usize) < max_remove {\n            if &list[i] == element {\n                list.remove(i);\n                removed += 1;\n            } else {\n                i += 1;\n            }\n        }\n    } else {\n        // Remove from tail\n        let mut i = list.len();\n        while i > 0 && (removed as usize) < max_remove {\n            i -= 1;\n            if &list[i] == element {\n                list.remove(i);\n                removed += 1;\n            }\n        }\n    }\n\n    if list.is_empty() {\n        db.del(&key);\n    } else {\n        db.incr_version(&key, now);\n    }\n\n    Frame::Integer(removed)\n}\n\n/// LTRIM key start stop\nfn cmd_ltrim(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let start: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let end: i64 = match parse_int(&args[2]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if !db.keys.contains_key(&key) {\n        return Frame::ok();\n    }\n\n    let list = match db.list_keys.get(&key) {\n        Some(l) => l,\n        None => return Frame::ok(),\n    };\n\n    let len = list.len() as i64;\n    let (rs, re) = redis_range(start, end, len);\n\n    if rs > re || rs >= len {\n        db.del(&key);\n        return Frame::ok();\n    }\n\n    let trimmed: std::collections::VecDeque<Vec<u8>> = list\n        .iter()\n        .skip(rs as usize)\n        .take((re - rs + 1) as usize)\n        .cloned()\n        .collect();\n\n    if trimmed.is_empty() {\n        db.del(&key);\n    } else {\n        db.list_keys.insert(key.clone(), trimmed);\n        db.incr_version(&key, now);\n    }\n\n    Frame::ok()\n}\n\n/// RPOPLPUSH source destination\nfn cmd_rpoplpush(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let src = String::from_utf8_lossy(&args[0]).into_owned();\n    let dst = String::from_utf8_lossy(&args[1]).into_owned();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&src);\n    db.check_ttl(&dst);\n\n    // Type checks\n    if let Some(t) = db.key_type(&src)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n    if let Some(t) = db.key_type(&dst)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // Save TTL when src == dst so we can restore it after pop+push cycle\n    let saved_ttl = if src == dst {\n        db.ttl.get(&src).cloned()\n    } else {\n        None\n    };\n\n    let val = match db.list_rpop(&src, now) {\n        Some(v) => v,\n        None => return Frame::Null,\n    };\n\n    db.list_lpush(&dst, std::slice::from_ref(&val), now);\n\n    // Restore TTL if src == dst (pop may have deleted the key and its TTL)\n    if let Some(ttl) = saved_ttl {\n        db.ttl.insert(dst.clone(), ttl);\n    }\n\n    Frame::Bulk(val.into())\n}\n\n/// LMOVE source destination LEFT|RIGHT LEFT|RIGHT\nfn cmd_lmove(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let src = String::from_utf8_lossy(&args[0]).into_owned();\n    let dst = String::from_utf8_lossy(&args[1]).into_owned();\n    let src_dir = String::from_utf8_lossy(&args[2]).to_uppercase();\n    let dst_dir = String::from_utf8_lossy(&args[3]).to_uppercase();\n\n    let pop_left = match src_dir.as_str() {\n        \"LEFT\" => true,\n        \"RIGHT\" => false,\n        _ => return Frame::error(MSG_SYNTAX_ERROR),\n    };\n    let push_left = match dst_dir.as_str() {\n        \"LEFT\" => true,\n        \"RIGHT\" => false,\n        _ => return Frame::error(MSG_SYNTAX_ERROR),\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&src);\n    db.check_ttl(&dst);\n\n    if let Some(t) = db.key_type(&src)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n    if let Some(t) = db.key_type(&dst)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // Save TTL when src == dst so we can restore it after pop+push cycle\n    let saved_ttl = if src == dst {\n        db.ttl.get(&src).cloned()\n    } else {\n        None\n    };\n\n    let val = if pop_left {\n        db.list_lpop(&src, now)\n    } else {\n        db.list_rpop(&src, now)\n    };\n\n    match val {\n        Some(v) => {\n            if push_left {\n                db.list_lpush(&dst, std::slice::from_ref(&v), now);\n            } else {\n                db.list_rpush(&dst, std::slice::from_ref(&v), now);\n            }\n            // Restore TTL if src == dst (pop may have deleted the key and its TTL)\n            if let Some(ttl) = saved_ttl {\n                db.ttl.insert(dst.clone(), ttl);\n            }\n            Frame::Bulk(v.into())\n        }\n        None => Frame::Null,\n    }\n}\n\n// ── Utility ──────────────────────────────────────────────────────────\n\n/// LPOS key element [RANK rank] [COUNT count] [MAXLEN maxlen]\nfn cmd_lpos(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let element = &args[1];\n    let mut rank: i64 = 1;\n    let mut count: Option<i64> = None;\n    let mut maxlen: i64 = 0;\n\n    let mut i = 2;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"RANK\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                match parse_int(&args[i]) {\n                    Some(n) => {\n                        if n == 0 {\n                            return Frame::error(\n                                \"ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative values meaning from the last match\",\n                            );\n                        }\n                        rank = n;\n                    }\n                    None => return Frame::error(MSG_INVALID_INT),\n                }\n            }\n            \"COUNT\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                match parse_int(&args[i]) {\n                    Some(n) if n >= 0 => count = Some(n),\n                    _ => return Frame::error(\"ERR COUNT can't be negative\"),\n                }\n            }\n            \"MAXLEN\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                match parse_int(&args[i]) {\n                    Some(n) if n >= 0 => maxlen = n,\n                    _ => return Frame::error(\"ERR MAXLEN can't be negative\"),\n                }\n            }\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n        i += 1;\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let list = match db.list_keys.get(&key) {\n        Some(l) => l,\n        None => {\n            return if count.is_some() {\n                Frame::Array(vec![])\n            } else {\n                Frame::Null\n            };\n        }\n    };\n\n    let len = list.len();\n    let max_count = count.unwrap_or(1);\n    let scan_max = if maxlen > 0 { maxlen as usize } else { len };\n\n    let mut matches: Vec<i64> = Vec::new();\n    let mut match_count = 0i64;\n\n    if rank > 0 {\n        // Forward scan\n        let mut skip = rank - 1;\n        for (idx, item) in list.iter().enumerate().take(len.min(scan_max)) {\n            if item == element {\n                if skip > 0 {\n                    skip -= 1;\n                    continue;\n                }\n                matches.push(idx as i64);\n                match_count += 1;\n                if max_count > 0 && match_count >= max_count {\n                    break;\n                }\n            }\n        }\n    } else {\n        // Reverse scan\n        let mut skip = (-rank) - 1;\n        let start = len.saturating_sub(scan_max);\n        for idx in (start..len).rev() {\n            if &list[idx] == element {\n                if skip > 0 {\n                    skip -= 1;\n                    continue;\n                }\n                matches.push(idx as i64);\n                match_count += 1;\n                if max_count > 0 && match_count >= max_count {\n                    break;\n                }\n            }\n        }\n    }\n\n    if count.is_some() {\n        Frame::Array(matches.into_iter().map(Frame::Integer).collect())\n    } else {\n        matches\n            .first()\n            .map(|&idx| Frame::Integer(idx))\n            .unwrap_or(Frame::Null)\n    }\n}\n\n// ── Blocking command stubs (non-blocking for MULTI/EXEC) ─────────────\n\n/// BLPOP key [key ...] timeout — non-blocking attempt (for MULTI/EXEC)\npub fn cmd_blpop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    // Last arg is timeout — validate it\n    if let Some(err) = validate_timeout(&args[args.len() - 1]) {\n        return err;\n    }\n\n    // Last arg is timeout, keys are all but last\n    let keys = &args[..args.len() - 1];\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    for key_bytes in keys {\n        let key = String::from_utf8_lossy(key_bytes).into_owned();\n        let db = inner.db_mut(ctx.selected_db);\n        db.check_ttl(&key);\n\n        if let Some(t) = db.key_type(&key)\n            && t != KeyType::List\n        {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n\n        if let Some(val) = db.list_lpop(&key, now) {\n            return Frame::Array(vec![Frame::Bulk(key.into()), Frame::Bulk(val.into())]);\n        }\n    }\n\n    Frame::NullArray\n}\n\n/// BRPOP key [key ...] timeout — non-blocking attempt (for MULTI/EXEC)\npub fn cmd_brpop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    // Last arg is timeout — validate it\n    if let Some(err) = validate_timeout(&args[args.len() - 1]) {\n        return err;\n    }\n\n    let keys = &args[..args.len() - 1];\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    for key_bytes in keys {\n        let key = String::from_utf8_lossy(key_bytes).into_owned();\n        let db = inner.db_mut(ctx.selected_db);\n        db.check_ttl(&key);\n\n        if let Some(t) = db.key_type(&key)\n            && t != KeyType::List\n        {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n\n        if let Some(val) = db.list_rpop(&key, now) {\n            return Frame::Array(vec![Frame::Bulk(key.into()), Frame::Bulk(val.into())]);\n        }\n    }\n\n    Frame::NullArray\n}\n\n/// BRPOPLPUSH source destination timeout — non-blocking attempt\npub fn cmd_brpoplpush(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    // Last arg is timeout — validate it\n    if let Some(err) = validate_timeout(&args[2]) {\n        return err;\n    }\n\n    let src = String::from_utf8_lossy(&args[0]).into_owned();\n    let dst = String::from_utf8_lossy(&args[1]).into_owned();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&src);\n    db.check_ttl(&dst);\n\n    if let Some(t) = db.key_type(&src)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n    if let Some(t) = db.key_type(&dst)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.list_rpop(&src, now) {\n        Some(val) => {\n            db.list_lpush(&dst, std::slice::from_ref(&val), now);\n            Frame::Bulk(val.into())\n        }\n        None => Frame::Null,\n    }\n}\n\n/// BLMOVE source destination LEFT|RIGHT LEFT|RIGHT timeout — non-blocking attempt\npub fn cmd_blmove(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let src = String::from_utf8_lossy(&args[0]).into_owned();\n    let dst = String::from_utf8_lossy(&args[1]).into_owned();\n    let src_dir = String::from_utf8_lossy(&args[2]).to_uppercase();\n    let dst_dir = String::from_utf8_lossy(&args[3]).to_uppercase();\n\n    let pop_left = match src_dir.as_str() {\n        \"LEFT\" => true,\n        \"RIGHT\" => false,\n        _ => return Frame::error(MSG_SYNTAX_ERROR),\n    };\n    let push_left = match dst_dir.as_str() {\n        \"LEFT\" => true,\n        \"RIGHT\" => false,\n        _ => return Frame::error(MSG_SYNTAX_ERROR),\n    };\n\n    if let Some(err) = validate_timeout(&args[4]) {\n        return err;\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&src);\n    db.check_ttl(&dst);\n\n    if let Some(t) = db.key_type(&src)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n    if let Some(t) = db.key_type(&dst)\n        && t != KeyType::List\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // Save TTL when src == dst so we can restore it after pop+push cycle\n    let saved_ttl = if src == dst {\n        db.ttl.get(&src).cloned()\n    } else {\n        None\n    };\n\n    let val = if pop_left {\n        db.list_lpop(&src, now)\n    } else {\n        db.list_rpop(&src, now)\n    };\n\n    match val {\n        Some(v) => {\n            if push_left {\n                db.list_lpush(&dst, std::slice::from_ref(&v), now);\n            } else {\n                db.list_rpush(&dst, std::slice::from_ref(&v), now);\n            }\n            // Restore TTL if src == dst\n            if let Some(ttl) = saved_ttl {\n                db.ttl.insert(dst.clone(), ttl);\n            }\n            Frame::Bulk(v.into())\n        }\n        None => Frame::Null,\n    }\n}\n\n// ── Utility ──────────────────────────────────────────────────────────\n\n/// Normalize Redis-style range indices for lists. Returns (start, end) inclusive.\nfn redis_range(start: i64, end: i64, len: i64) -> (i64, i64) {\n    let mut s = start;\n    let mut e = end;\n\n    if s < 0 {\n        s += len;\n    }\n    if e < 0 {\n        e += len;\n    }\n    if s < 0 {\n        s = 0;\n    }\n    if e >= len {\n        e = len - 1;\n    }\n\n    (s, e)\n}\n\n/// Validate a blocking command timeout argument.\n/// Returns Some(Frame) with error if invalid, None if OK.\nfn validate_timeout(arg: &[u8]) -> Option<Frame> {\n    let s = String::from_utf8_lossy(arg);\n    let s_lower = s.to_lowercase();\n    if s_lower == \"inf\" || s_lower == \"+inf\" || s_lower == \"-inf\" {\n        return Some(Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE));\n    }\n    match s.parse::<f64>() {\n        Ok(t) if t < 0.0 => Some(Frame::error(MSG_TIMEOUT_NEGATIVE)),\n        Ok(_) => None,\n        Err(_) => Some(Frame::error(MSG_INVALID_TIMEOUT)),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/mod.rs",
    "content": "// Command handler modules.\n//\n// Each module implements a category of Redis commands.\n// Commands are registered in the dispatch table (src/dispatch.rs).\n\npub mod client; // CLIENT SETNAME/GETNAME\npub mod cluster; // CLUSTER SLOTS/KEYSLOT/NODES/SHARDS (mocked)\npub mod connection; // PING, ECHO, QUIT, SELECT, AUTH, HELLO\npub mod generic; // DEL, EXISTS, EXPIRE, TTL, KEYS, SCAN, etc.\npub mod geo; // GEOADD, GEODIST, GEOPOS, GEORADIUS, etc.\npub mod hash; // HSET, HGET, HDEL, HGETALL, etc.\npub mod hll; // PFADD, PFCOUNT, PFMERGE\npub mod list; // LPUSH, RPUSH, LPOP, RPOP, BLPOP, etc.\npub mod object;\npub mod pubsub; // SUBSCRIBE, PUBLISH, PSUBSCRIBE, etc.\npub mod scripting; // EVAL, EVALSHA, SCRIPT\npub mod server; // DBSIZE, FLUSHDB, INFO, TIME, etc.\npub mod set; // SADD, SREM, SMEMBERS, SINTER, etc.\npub mod sorted_set; // ZADD, ZRANGE, ZSCORE, ZRANK, etc.\npub mod stream; // XADD, XREAD, XREADGROUP, XACK, etc.\npub mod string; // GET, SET, MGET, MSET, INCR, etc.\npub mod transactions; // MULTI, EXEC, WATCH, DISCARD // OBJECT IDLETIME\n\npub(crate) fn parse_int(bytes: &[u8]) -> Option<i64> {\n    String::from_utf8_lossy(bytes).parse::<i64>().ok()\n}\n\npub(crate) fn parse_float(bytes: &[u8]) -> Option<f64> {\n    let s = String::from_utf8_lossy(bytes);\n    match s.to_lowercase().as_str() {\n        \"+inf\" | \"inf\" => Some(f64::INFINITY),\n        \"-inf\" => Some(f64::NEG_INFINITY),\n        _ => s.parse::<f64>().ok(),\n    }\n}\n\nuse crate::dispatch::{MSG_INVALID_INT, MSG_SYNTAX_ERROR};\nuse crate::frame::Frame;\n\npub(crate) struct ScanOpts {\n    pub pattern: Option<String>,\n    pub count: Option<i64>,\n    pub type_filter: Option<String>,\n}\n\n/// Parse MATCH/COUNT/TYPE options common to SCAN, SSCAN, HSCAN, ZSCAN.\n/// `allow_type`: only SCAN supports the TYPE filter.\npub(crate) fn parse_scan_opts(args: &[Vec<u8>], allow_type: bool) -> Result<ScanOpts, Frame> {\n    let mut pattern = None;\n    let mut count = None;\n    let mut type_filter = None;\n\n    let mut i = 0;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"MATCH\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                pattern = Some(String::from_utf8_lossy(&args[i]).into_owned());\n            }\n            \"COUNT\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                let n = String::from_utf8_lossy(&args[i])\n                    .parse::<i64>()\n                    .map_err(|_| Frame::error(MSG_INVALID_INT))?;\n                if n <= 0 {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                count = Some(n);\n            }\n            \"TYPE\" if allow_type => {\n                i += 1;\n                if i >= args.len() {\n                    return Err(Frame::error(MSG_SYNTAX_ERROR));\n                }\n                type_filter = Some(String::from_utf8_lossy(&args[i]).to_lowercase());\n            }\n            _ => return Err(Frame::error(MSG_SYNTAX_ERROR)),\n        }\n        i += 1;\n    }\n\n    Ok(ScanOpts {\n        pattern,\n        count,\n        type_filter,\n    })\n}\n"
  },
  {
    "path": "miniredis/src/cmd/object.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::CommandTable;\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"OBJECT\", cmd_object, true, -2);\n}\n\nfn cmd_object(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"ENCODING\" => {\n            if args.len() != 2 {\n                return Frame::error(\"ERR wrong number of arguments for 'object|encoding' command\");\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            match db.keys.get(key.as_ref()) {\n                None => Frame::Null,\n                Some(kt) => {\n                    let encoding = match kt {\n                        crate::types::KeyType::String => \"raw\",\n                        crate::types::KeyType::Hash => \"hashtable\",\n                        crate::types::KeyType::List => \"linkedlist\",\n                        crate::types::KeyType::Set => \"hashtable\",\n                        crate::types::KeyType::SortedSet => \"skiplist\",\n                        crate::types::KeyType::Stream => \"stream\",\n                        crate::types::KeyType::HyperLogLog => \"raw\",\n                    };\n                    Frame::Bulk(encoding.into())\n                }\n            }\n        }\n        \"REFCOUNT\" => {\n            if args.len() != 2 {\n                return Frame::error(\"ERR wrong number of arguments for 'object|refcount' command\");\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::Null;\n            }\n            // Always return 1 (simplified)\n            Frame::Integer(1)\n        }\n        \"FREQ\" => {\n            if args.len() != 2 {\n                return Frame::error(\"ERR wrong number of arguments for 'object|freq' command\");\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::Null;\n            }\n            // Always return 0 (simplified)\n            Frame::Integer(0)\n        }\n        \"IDLETIME\" => {\n            if args.len() != 2 {\n                return Frame::error(\"ERR wrong number of arguments for 'object|idletime' command\");\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            if !db.keys.contains_key(key.as_ref()) {\n                return Frame::Null;\n            }\n\n            match db.lru.get(key.as_ref()) {\n                Some(last_access) => {\n                    let now = inner.effective_now();\n                    let idle = now.duration_since(*last_access).unwrap_or_default();\n                    Frame::Integer(idle.as_secs() as i64)\n                }\n                None => Frame::Integer(0),\n            }\n        }\n        \"HELP\" => Frame::Array(vec![Frame::Bulk(\n            \"OBJECT IDLETIME <key> - return idle time of key\".into(),\n        )]),\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try OBJECT HELP.\",\n            subcmd.to_lowercase()\n        )),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/pubsub.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{CommandTable, err_wrong_number};\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"PUBLISH\", cmd_publish, false, 3);\n    table.add(\"PUBSUB\", cmd_pubsub, true, -2);\n    // SUBSCRIBE/PSUBSCRIBE/UNSUBSCRIBE/PUNSUBSCRIBE are normally handled in\n    // server.rs (outside dispatch). These are registered so they can be queued\n    // inside MULTI/EXEC.\n    table.add(\"SUBSCRIBE\", cmd_subscribe, false, -2);\n    table.add(\"PSUBSCRIBE\", cmd_psubscribe, false, -2);\n    table.add(\"UNSUBSCRIBE\", cmd_unsubscribe, false, -1);\n    table.add(\"PUNSUBSCRIBE\", cmd_punsubscribe, false, -1);\n}\n\n/// SUBSCRIBE channel [channel ...] — handler for MULTI/EXEC path.\n/// Normal (non-MULTI) SUBSCRIBE is handled directly in server.rs.\nfn cmd_subscribe(_state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut confirmations = Vec::new();\n    for arg in args {\n        let channel = String::from_utf8_lossy(arg).to_string();\n        ctx.pending_subscribe.push(channel.clone());\n        let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len();\n        confirmations.push(Frame::Array(vec![\n            Frame::Bulk(\"subscribe\".into()),\n            Frame::Bulk(channel.into()),\n            Frame::Integer(count as i64),\n        ]));\n    }\n\n    if confirmations.len() == 1 {\n        confirmations.pop().unwrap()\n    } else {\n        Frame::Array(confirmations)\n    }\n}\n\n/// PSUBSCRIBE pattern [pattern ...] — handler for MULTI/EXEC path.\nfn cmd_psubscribe(_state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut confirmations = Vec::new();\n    for arg in args {\n        let pattern = String::from_utf8_lossy(arg).to_string();\n        ctx.pending_psubscribe.push(pattern.clone());\n        let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len();\n        confirmations.push(Frame::Array(vec![\n            Frame::Bulk(\"psubscribe\".into()),\n            Frame::Bulk(pattern.into()),\n            Frame::Integer(count as i64),\n        ]));\n    }\n\n    if confirmations.len() == 1 {\n        confirmations.pop().unwrap()\n    } else {\n        Frame::Array(confirmations)\n    }\n}\n\n/// UNSUBSCRIBE [channel ...] — handler for MULTI/EXEC path.\nfn cmd_unsubscribe(_state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.is_empty() {\n        // Unsubscribe from all pending channels\n        ctx.pending_subscribe.clear();\n        let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len();\n        return Frame::Array(vec![\n            Frame::Bulk(\"unsubscribe\".into()),\n            Frame::Null,\n            Frame::Integer(count as i64),\n        ]);\n    }\n\n    let mut confirmations = Vec::new();\n    for arg in args {\n        let channel = String::from_utf8_lossy(arg).to_string();\n        ctx.pending_subscribe.retain(|ch| *ch != channel);\n        let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len();\n        confirmations.push(Frame::Array(vec![\n            Frame::Bulk(\"unsubscribe\".into()),\n            Frame::Bulk(channel.into()),\n            Frame::Integer(count as i64),\n        ]));\n    }\n\n    if confirmations.len() == 1 {\n        confirmations.pop().unwrap()\n    } else {\n        Frame::Array(confirmations)\n    }\n}\n\n/// PUNSUBSCRIBE [pattern ...] — handler for MULTI/EXEC path.\nfn cmd_punsubscribe(_state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.is_empty() {\n        // Unsubscribe from all pending patterns\n        ctx.pending_psubscribe.clear();\n        let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len();\n        return Frame::Array(vec![\n            Frame::Bulk(\"punsubscribe\".into()),\n            Frame::Null,\n            Frame::Integer(count as i64),\n        ]);\n    }\n\n    let mut confirmations = Vec::new();\n    for arg in args {\n        let pattern = String::from_utf8_lossy(arg).to_string();\n        ctx.pending_psubscribe.retain(|p| *p != pattern);\n        let count = ctx.pending_subscribe.len() + ctx.pending_psubscribe.len();\n        confirmations.push(Frame::Array(vec![\n            Frame::Bulk(\"punsubscribe\".into()),\n            Frame::Bulk(pattern.into()),\n            Frame::Integer(count as i64),\n        ]));\n    }\n\n    if confirmations.len() == 1 {\n        confirmations.pop().unwrap()\n    } else {\n        Frame::Array(confirmations)\n    }\n}\n\n/// PUBLISH channel message\nfn cmd_publish(state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let channel = String::from_utf8_lossy(&args[0]).to_string();\n    let message = String::from_utf8_lossy(&args[1]).to_string();\n\n    let registry = state.pubsub.lock().unwrap();\n    let count = registry.publish(&channel, &message);\n    Frame::Integer(count)\n}\n\n/// PUBSUB CHANNELS/NUMSUB/NUMPAT\nfn cmd_pubsub(state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"CHANNELS\" => {\n            if args.len() > 2 {\n                return Frame::error(err_wrong_number(\"pubsub|channels\"));\n            }\n            let pattern = if args.len() > 1 {\n                Some(String::from_utf8_lossy(&args[1]).to_string())\n            } else {\n                None\n            };\n            let registry = state.pubsub.lock().unwrap();\n            let channels = registry.active_channels(pattern.as_deref());\n            Frame::Array(\n                channels\n                    .into_iter()\n                    .map(|ch| Frame::Bulk(ch.into()))\n                    .collect(),\n            )\n        }\n        \"NUMSUB\" => {\n            let registry = state.pubsub.lock().unwrap();\n            let mut result = Vec::new();\n            for arg in &args[1..] {\n                let channel = String::from_utf8_lossy(arg).to_string();\n                let count = registry.numsub(&channel);\n                result.push(Frame::Bulk(channel.into()));\n                result.push(Frame::Integer(count));\n            }\n            Frame::Array(result)\n        }\n        \"NUMPAT\" => {\n            if args.len() > 1 {\n                return Frame::error(err_wrong_number(\"pubsub|numpat\"));\n            }\n            let registry = state.pubsub.lock().unwrap();\n            Frame::Integer(registry.numpat())\n        }\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try PUBSUB HELP.\",\n            subcmd.to_lowercase()\n        )),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/scripting.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::{AtomicUsize, Ordering};\n\nuse mlua::prelude::*;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_INVALID_INT, MSG_INVALID_KEYS_NUMBER, MSG_NEGATIVE_KEYS_NUMBER,\n    MSG_NO_SCRIPT_FOUND, err_wrong_number,\n};\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"EVAL\", cmd_eval, false, -3);\n    table.add(\"EVAL_RO\", cmd_eval_ro, true, -3);\n    table.add(\"EVALSHA\", cmd_evalsha, false, -3);\n    table.add(\"EVALSHA_RO\", cmd_evalsha_ro, true, -3);\n    table.add(\"SCRIPT\", cmd_script, false, -2);\n}\n\nfn sha1_hex(s: &str) -> String {\n    use sha1_smol::Sha1;\n    let mut hasher = Sha1::new();\n    hasher.update(s.as_bytes());\n    let digest = hasher.digest();\n    // Convert digest bytes to hex string\n    digest\n        .bytes()\n        .iter()\n        .map(|b| format!(\"{:02x}\", b))\n        .collect()\n}\n\nfn msg_not_from_scripts(sha: &str) -> String {\n    format!(\n        \"This Redis command is not allowed from script script: {}, &c\",\n        sha\n    )\n}\n\nfn err_lua_parse_error(err: &str) -> String {\n    format!(\n        \"ERR Error compiling script (new function): {}\",\n        sanitize_lua_error(err)\n    )\n}\n\n/// Sanitize a Lua error message for RESP:\n/// 1. Strip everything after the first `\\n` (stack traces)\n/// 2. Strip mlua \"runtime error: \" prefix\n/// 3. Strip Lua source location prefix like `[string \"...\"]:N: `\nfn sanitize_lua_error(err: &str) -> String {\n    // Take only the first line\n    let first_line = err.split('\\n').next().unwrap_or(err);\n    // Strip mlua \"runtime error: \" prefix\n    let stripped = first_line\n        .strip_prefix(\"runtime error: \")\n        .unwrap_or(first_line);\n    // Strip Lua source location prefix like `[string \"user_script\"]:N: `\n    let stripped = if let Some(pos) = stripped.find(\"]: \") {\n        let after = &stripped[pos + 3..];\n        // Strip the line number prefix \"N: \" that may remain\n        if let Some(colon_pos) = after.find(\": \") {\n            if after[..colon_pos].chars().all(|c| c.is_ascii_digit()) {\n                &after[colon_pos + 2..]\n            } else {\n                after\n            }\n        } else {\n            after\n        }\n    } else {\n        stripped\n    };\n    stripped.to_string()\n}\n\n/// Commands that are not allowed inside Lua scripts.\nconst DISALLOWED_IN_SCRIPTS: &[&str] = &[\n    \"MULTI\",\n    \"EXEC\",\n    \"DISCARD\",\n    \"EVAL\",\n    \"EVAL_RO\",\n    \"EVALSHA\",\n    \"EVALSHA_RO\",\n    \"SCRIPT\",\n    \"AUTH\",\n    \"WATCH\",\n    \"UNWATCH\",\n    \"SUBSCRIBE\",\n    \"UNSUBSCRIBE\",\n    \"PSUBSCRIBE\",\n    \"PUNSUBSCRIBE\",\n];\n\n// ── Frame <-> Lua value conversion ──────────────────────────────────\n\n/// Convert a Frame (Redis response) to a Lua value.\nfn frame_to_lua(lua: &Lua, frame: &Frame) -> LuaResult<LuaValue> {\n    match frame {\n        Frame::Null | Frame::NullArray => Ok(LuaValue::Boolean(false)),\n        Frame::Integer(n) => Ok(LuaValue::Integer(*n)),\n        Frame::Simple(s) => {\n            // Status reply -> table with \"ok\" field\n            let tbl = lua.create_table()?;\n            tbl.set(\"ok\", s.as_str())?;\n            Ok(LuaValue::Table(tbl))\n        }\n        Frame::Bulk(b) => {\n            let s = lua.create_string(b.as_ref())?;\n            Ok(LuaValue::String(s))\n        }\n        Frame::Error(msg) => {\n            // Error -> table with \"err\" field\n            let tbl = lua.create_table()?;\n            tbl.set(\"err\", msg.as_str())?;\n            Ok(LuaValue::Table(tbl))\n        }\n        Frame::Array(arr) | Frame::Set(arr) | Frame::Push(arr) => {\n            let tbl = lua.create_table()?;\n            for (i, item) in arr.iter().enumerate() {\n                let val = frame_to_lua(lua, item)?;\n                tbl.set(i + 1, val)?;\n            }\n            Ok(LuaValue::Table(tbl))\n        }\n        Frame::Map(pairs) => {\n            let tbl = lua.create_table()?;\n            for (i, (k, v)) in pairs.iter().enumerate() {\n                let kv_tbl = lua.create_table()?;\n                kv_tbl.set(1, frame_to_lua(lua, k)?)?;\n                kv_tbl.set(2, frame_to_lua(lua, v)?)?;\n                tbl.set(i + 1, kv_tbl)?;\n            }\n            Ok(LuaValue::Table(tbl))\n        }\n        Frame::Double(f) => Ok(LuaValue::Number(*f)),\n    }\n}\n\n/// Convert a Lua value to a Frame (Redis response).\nfn lua_to_frame(value: LuaValue) -> Frame {\n    match value {\n        LuaValue::Nil => Frame::Null,\n        LuaValue::Boolean(b) => {\n            if b {\n                Frame::Integer(1)\n            } else {\n                Frame::Null\n            }\n        }\n        LuaValue::Integer(n) => Frame::Integer(n),\n        LuaValue::Number(n) => Frame::Integer(n as i64),\n        LuaValue::String(s) => Frame::Bulk(s.as_bytes().to_vec().into()),\n        LuaValue::Table(tbl) => {\n            // Check for special \"err\" field\n            if let Ok(err_val) = tbl.get::<LuaValue>(\"err\")\n                && let LuaValue::String(s) = err_val\n            {\n                let msg = String::from_utf8_lossy(&s.as_bytes()).to_string();\n                return Frame::Error(msg);\n            }\n            // Check for special \"ok\" field\n            if let Ok(ok_val) = tbl.get::<LuaValue>(\"ok\")\n                && let LuaValue::String(s) = ok_val\n            {\n                let msg = String::from_utf8_lossy(&s.as_bytes()).to_string();\n                return Frame::Simple(msg);\n            }\n            // Numeric array\n            let mut result = Vec::new();\n            for i in 1.. {\n                match tbl.get::<LuaValue>(i) {\n                    Ok(LuaValue::Nil) => break,\n                    Ok(val) => result.push(lua_to_frame(val)),\n                    Err(_) => break,\n                }\n            }\n            Frame::Array(result)\n        }\n        _ => Frame::Null,\n    }\n}\n\n// ── cjson helpers ───────────────────────────────────────────────────\n\n/// Convert a Lua value to a JSON string.\nfn lua_to_json_string(value: &LuaValue) -> Result<String, String> {\n    match value {\n        LuaValue::Nil => Ok(\"null\".to_string()),\n        LuaValue::Boolean(b) => Ok(if *b { \"true\" } else { \"false\" }.to_string()),\n        LuaValue::Integer(n) => Ok(n.to_string()),\n        LuaValue::Number(n) => {\n            if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {\n                Ok(format!(\"{}\", *n as i64))\n            } else {\n                Ok(n.to_string())\n            }\n        }\n        LuaValue::String(s) => {\n            let raw = String::from_utf8_lossy(&s.as_bytes()).to_string();\n            Ok(json_escape_string(&raw))\n        }\n        LuaValue::Table(tbl) => {\n            // Check if it's an array (sequential integer keys starting at 1)\n            let is_array = {\n                let mut has_seq = false;\n                if let Ok(v) = tbl.get::<LuaValue>(1)\n                    && !matches!(v, LuaValue::Nil)\n                {\n                    has_seq = true;\n                }\n                has_seq\n            };\n\n            if is_array {\n                let mut items = Vec::new();\n                for i in 1.. {\n                    match tbl.get::<LuaValue>(i) {\n                        Ok(LuaValue::Nil) => break,\n                        Ok(val) => items.push(lua_to_json_string(&val)?),\n                        Err(_) => break,\n                    }\n                }\n                Ok(format!(\"[{}]\", items.join(\",\")))\n            } else {\n                // Object - iterate pairs\n                let mut pairs = Vec::new();\n                for (k, v) in tbl.clone().pairs::<LuaValue, LuaValue>().flatten() {\n                    let key_str = match &k {\n                        LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(),\n                        LuaValue::Integer(n) => n.to_string(),\n                        LuaValue::Number(n) => n.to_string(),\n                        _ => continue,\n                    };\n                    pairs.push(format!(\n                        \"{}:{}\",\n                        json_escape_string(&key_str),\n                        lua_to_json_string(&v)?\n                    ));\n                }\n                Ok(format!(\"{{{}}}\", pairs.join(\",\")))\n            }\n        }\n        _ => Err(\"Cannot encode non-supported Lua type\".to_string()),\n    }\n}\n\n/// JSON-escape a string.\nfn json_escape_string(s: &str) -> String {\n    let mut out = String::with_capacity(s.len() + 2);\n    out.push('\"');\n    for ch in s.chars() {\n        match ch {\n            '\"' => out.push_str(\"\\\\\\\"\"),\n            '\\\\' => out.push_str(\"\\\\\\\\\"),\n            '\\n' => out.push_str(\"\\\\n\"),\n            '\\r' => out.push_str(\"\\\\r\"),\n            '\\t' => out.push_str(\"\\\\t\"),\n            c if (c as u32) < 0x20 => {\n                out.push_str(&format!(\"\\\\u{:04x}\", c as u32));\n            }\n            c => out.push(c),\n        }\n    }\n    out.push('\"');\n    out\n}\n\n/// Parse a JSON string into a Lua value.\nfn json_to_lua(lua: &Lua, json: &str) -> LuaResult<LuaValue> {\n    let json = json.trim();\n    if json.is_empty() {\n        return Err(LuaError::RuntimeError(\n            \"Expected value but found EOF\".to_string(),\n        ));\n    }\n\n    let (val, _) = json_parse_value(lua, json, 0)?;\n    Ok(val)\n}\n\nfn json_parse_value(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> {\n    let pos = json_skip_whitespace(json, pos);\n    if pos >= json.len() {\n        return Err(LuaError::RuntimeError(\"Unexpected end of JSON\".to_string()));\n    }\n\n    let ch = json.as_bytes()[pos];\n    match ch {\n        b'\"' => json_parse_string(lua, json, pos),\n        b'{' => json_parse_object(lua, json, pos),\n        b'[' => json_parse_array(lua, json, pos),\n        b't' => {\n            if json[pos..].starts_with(\"true\") {\n                Ok((LuaValue::Boolean(true), pos + 4))\n            } else {\n                Err(LuaError::RuntimeError(\"Invalid JSON value\".to_string()))\n            }\n        }\n        b'f' => {\n            if json[pos..].starts_with(\"false\") {\n                Ok((LuaValue::Boolean(false), pos + 5))\n            } else {\n                Err(LuaError::RuntimeError(\"Invalid JSON value\".to_string()))\n            }\n        }\n        b'n' => {\n            if json[pos..].starts_with(\"null\") {\n                Ok((LuaValue::Nil, pos + 4))\n            } else {\n                Err(LuaError::RuntimeError(\"Invalid JSON value\".to_string()))\n            }\n        }\n        b'-' | b'0'..=b'9' => json_parse_number(lua, json, pos),\n        _ => Err(LuaError::RuntimeError(format!(\n            \"Unexpected character '{}' at position {}\",\n            ch as char, pos\n        ))),\n    }\n}\n\nfn json_skip_whitespace(json: &str, mut pos: usize) -> usize {\n    let bytes = json.as_bytes();\n    while pos < bytes.len() && matches!(bytes[pos], b' ' | b'\\t' | b'\\n' | b'\\r') {\n        pos += 1;\n    }\n    pos\n}\n\nfn json_parse_string(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> {\n    // pos points to opening quote\n    let mut i = pos + 1;\n    let bytes = json.as_bytes();\n    let mut result = String::new();\n\n    while i < bytes.len() {\n        match bytes[i] {\n            b'\"' => {\n                let s = lua.create_string(result.as_bytes())?;\n                return Ok((LuaValue::String(s), i + 1));\n            }\n            b'\\\\' => {\n                i += 1;\n                if i >= bytes.len() {\n                    return Err(LuaError::RuntimeError(\n                        \"Unterminated string escape\".to_string(),\n                    ));\n                }\n                match bytes[i] {\n                    b'\"' => result.push('\"'),\n                    b'\\\\' => result.push('\\\\'),\n                    b'/' => result.push('/'),\n                    b'n' => result.push('\\n'),\n                    b'r' => result.push('\\r'),\n                    b't' => result.push('\\t'),\n                    b'b' => result.push('\\u{0008}'),\n                    b'f' => result.push('\\u{000C}'),\n                    b'u' => {\n                        if i + 4 >= bytes.len() {\n                            return Err(LuaError::RuntimeError(\n                                \"Invalid unicode escape\".to_string(),\n                            ));\n                        }\n                        let hex = &json[i + 1..i + 5];\n                        let code = u32::from_str_radix(hex, 16).map_err(|_| {\n                            LuaError::RuntimeError(\"Invalid unicode escape\".to_string())\n                        })?;\n                        if let Some(c) = char::from_u32(code) {\n                            result.push(c);\n                        }\n                        i += 4;\n                    }\n                    _ => {\n                        result.push('\\\\');\n                        result.push(bytes[i] as char);\n                    }\n                }\n            }\n            _ => result.push(bytes[i] as char),\n        }\n        i += 1;\n    }\n    Err(LuaError::RuntimeError(\"Unterminated string\".to_string()))\n}\n\nfn json_parse_number(_lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> {\n    let bytes = json.as_bytes();\n    let mut i = pos;\n    let mut is_float = false;\n\n    if i < bytes.len() && bytes[i] == b'-' {\n        i += 1;\n    }\n    while i < bytes.len() && bytes[i].is_ascii_digit() {\n        i += 1;\n    }\n    if i < bytes.len() && bytes[i] == b'.' {\n        is_float = true;\n        i += 1;\n        while i < bytes.len() && bytes[i].is_ascii_digit() {\n            i += 1;\n        }\n    }\n    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {\n        is_float = true;\n        i += 1;\n        if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {\n            i += 1;\n        }\n        while i < bytes.len() && bytes[i].is_ascii_digit() {\n            i += 1;\n        }\n    }\n\n    let num_str = &json[pos..i];\n    if is_float {\n        let n: f64 = num_str\n            .parse()\n            .map_err(|_| LuaError::RuntimeError(\"Invalid number\".to_string()))?;\n        Ok((LuaValue::Number(n), i))\n    } else {\n        match num_str.parse::<i64>() {\n            Ok(n) => Ok((LuaValue::Integer(n), i)),\n            Err(_) => {\n                let n: f64 = num_str\n                    .parse()\n                    .map_err(|_| LuaError::RuntimeError(\"Invalid number\".to_string()))?;\n                Ok((LuaValue::Number(n), i))\n            }\n        }\n    }\n}\n\nfn json_parse_array(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> {\n    let tbl = lua.create_table()?;\n    let mut i = pos + 1; // skip '['\n    let mut idx = 1;\n\n    i = json_skip_whitespace(json, i);\n    if i < json.len() && json.as_bytes()[i] == b']' {\n        return Ok((LuaValue::Table(tbl), i + 1));\n    }\n\n    loop {\n        let (val, new_pos) = json_parse_value(lua, json, i)?;\n        tbl.set(idx, val)?;\n        idx += 1;\n        i = json_skip_whitespace(json, new_pos);\n        if i >= json.len() {\n            return Err(LuaError::RuntimeError(\"Unterminated array\".to_string()));\n        }\n        if json.as_bytes()[i] == b']' {\n            return Ok((LuaValue::Table(tbl), i + 1));\n        }\n        if json.as_bytes()[i] != b',' {\n            return Err(LuaError::RuntimeError(\"Expected ',' in array\".to_string()));\n        }\n        i += 1;\n    }\n}\n\nfn json_parse_object(lua: &Lua, json: &str, pos: usize) -> LuaResult<(LuaValue, usize)> {\n    let tbl = lua.create_table()?;\n    let mut i = pos + 1; // skip '{'\n\n    i = json_skip_whitespace(json, i);\n    if i < json.len() && json.as_bytes()[i] == b'}' {\n        return Ok((LuaValue::Table(tbl), i + 1));\n    }\n\n    loop {\n        i = json_skip_whitespace(json, i);\n        // Parse key (must be a string)\n        let (key_val, new_pos) = json_parse_string(lua, json, i)?;\n        i = json_skip_whitespace(json, new_pos);\n        if i >= json.len() || json.as_bytes()[i] != b':' {\n            return Err(LuaError::RuntimeError(\"Expected ':' in object\".to_string()));\n        }\n        i += 1;\n        // Parse value\n        let (val, new_pos) = json_parse_value(lua, json, i)?;\n        tbl.set(key_val, val)?;\n        i = json_skip_whitespace(json, new_pos);\n        if i >= json.len() {\n            return Err(LuaError::RuntimeError(\"Unterminated object\".to_string()));\n        }\n        if json.as_bytes()[i] == b'}' {\n            return Ok((LuaValue::Table(tbl), i + 1));\n        }\n        if json.as_bytes()[i] != b',' {\n            return Err(LuaError::RuntimeError(\"Expected ',' in object\".to_string()));\n        }\n        i += 1;\n    }\n}\n\n// ── Lua script execution ────────────────────────────────────────────\n\n/// Run a Lua script. Returns the response Frame.\nfn run_lua_script(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    sha: &str,\n    script: &str,\n    read_only: bool,\n    args: &[Vec<u8>],\n) -> Result<Frame, String> {\n    // Parse numkeys and split args into KEYS/ARGV\n    if args.is_empty() {\n        return Err(MSG_INVALID_INT.to_string());\n    }\n    let numkeys_str = String::from_utf8_lossy(&args[0]);\n    let numkeys: i64 = numkeys_str\n        .parse()\n        .map_err(|_| MSG_INVALID_INT.to_string())?;\n    if numkeys < 0 {\n        return Err(MSG_NEGATIVE_KEYS_NUMBER.to_string());\n    }\n    let numkeys = numkeys as usize;\n    let remaining = &args[1..];\n    if numkeys > remaining.len() {\n        return Err(MSG_INVALID_KEYS_NUMBER.to_string());\n    }\n\n    let keys = &remaining[..numkeys];\n    let argv = &remaining[numkeys..];\n\n    // Create Lua state\n    let lua = Lua::new();\n\n    // Set up global protection metatable to catch accesses to nonexistent globals.\n    // This mimics Redis's behavior of erroring on undefined global variables.\n    lua.load(r#\"\n        -- Sandbox the Lua environment like Redis does.\n        -- Remove dangerous os functions, keep only os.clock.\n        if os then\n            local clock = os.clock\n            os = { clock = clock }\n        end\n        -- Remove other dangerous functions\n        loadfile = nil\n        dofile = nil\n\n        local _orig_globals = {}\n        for k, v in pairs(_G) do\n            _orig_globals[k] = true\n        end\n        -- Also allow KEYS, ARGV, redis, cjson which will be set after this\n        _orig_globals[\"KEYS\"] = true\n        _orig_globals[\"ARGV\"] = true\n        _orig_globals[\"redis\"] = true\n        _orig_globals[\"cjson\"] = true\n        setmetatable(_G, {\n            __index = function(t, name)\n                if _orig_globals[name] then\n                    return rawget(t, name)\n                end\n                error(\"Script attempted to access nonexistent global variable '\" .. tostring(name) .. \"'\")\n            end,\n            __newindex = function(t, name, value)\n                rawset(t, name, value)\n            end\n        })\n    \"#)\n    .exec()\n    .map_err(|e| e.to_string())?;\n\n    // Set KEYS global\n    let keys_table = lua.create_table().map_err(|e| e.to_string())?;\n    for (i, k) in keys.iter().enumerate() {\n        let s = lua.create_string(k.as_slice()).map_err(|e| e.to_string())?;\n        keys_table.set(i + 1, s).map_err(|e| e.to_string())?;\n    }\n    lua.globals()\n        .set(\"KEYS\", keys_table)\n        .map_err(|e| e.to_string())?;\n\n    // Set ARGV global\n    let argv_table = lua.create_table().map_err(|e| e.to_string())?;\n    for (i, a) in argv.iter().enumerate() {\n        let s = lua.create_string(a.as_slice()).map_err(|e| e.to_string())?;\n        argv_table.set(i + 1, s).map_err(|e| e.to_string())?;\n    }\n    lua.globals()\n        .set(\"ARGV\", argv_table)\n        .map_err(|e| e.to_string())?;\n\n    // Create cjson module\n    let cjson_table = lua.create_table().map_err(|e| e.to_string())?;\n\n    // cjson.decode()\n    let decode_fn = lua\n        .create_function(|lua_ctx, args: LuaMultiValue| {\n            if args.len() != 1 {\n                return Err(LuaError::RuntimeError(\n                    \"bad argument #1 to 'decode' (string expected, got no value)\".to_string(),\n                ));\n            }\n            let arg = args.into_iter().next().unwrap();\n            let json_str = match arg {\n                LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(),\n                _ => {\n                    return Err(LuaError::RuntimeError(\n                        \"bad argument #1 to 'decode' (string expected)\".to_string(),\n                    ));\n                }\n            };\n            json_to_lua(lua_ctx, &json_str)\n        })\n        .map_err(|e| e.to_string())?;\n    cjson_table\n        .set(\"decode\", decode_fn)\n        .map_err(|e| e.to_string())?;\n\n    // cjson.encode()\n    let encode_fn = lua\n        .create_function(|_, args: LuaMultiValue| {\n            if args.len() != 1 {\n                return Err(LuaError::RuntimeError(\n                    \"bad argument #1 to 'encode' (value expected)\".to_string(),\n                ));\n            }\n            let arg = args.into_iter().next().unwrap();\n            lua_to_json_string(&arg).map_err(LuaError::RuntimeError)\n        })\n        .map_err(|e| e.to_string())?;\n    cjson_table\n        .set(\"encode\", encode_fn)\n        .map_err(|e| e.to_string())?;\n\n    lua.globals()\n        .set(\"cjson\", cjson_table)\n        .map_err(|e| e.to_string())?;\n\n    // Create redis module\n    let redis_table = lua.create_table().map_err(|e| e.to_string())?;\n\n    // Use an AtomicUsize to share selected_db between closures so that SELECT inside\n    // a script persists for subsequent redis.call() invocations within the same script.\n    let shared_selected_db = Arc::new(AtomicUsize::new(ctx.selected_db));\n\n    // redis.call() and redis.pcall()\n    let authenticated = ctx.authenticated;\n    {\n        let state_call = Arc::clone(state);\n        let db_cell_call = Arc::clone(&shared_selected_db);\n        let sha_str = sha.to_string();\n        let call_fn = lua\n            .create_function(move |lua_ctx, args: LuaMultiValue| {\n                redis_call_impl(\n                    lua_ctx,\n                    &state_call,\n                    &db_cell_call,\n                    authenticated,\n                    &sha_str,\n                    true,\n                    read_only,\n                    args,\n                )\n            })\n            .map_err(|e| e.to_string())?;\n        redis_table\n            .set(\"call\", call_fn)\n            .map_err(|e| e.to_string())?;\n    }\n    {\n        let state_pcall = Arc::clone(state);\n        let db_cell_pcall = Arc::clone(&shared_selected_db);\n        let sha_str2 = sha.to_string();\n        let pcall_fn = lua\n            .create_function(move |lua_ctx, args: LuaMultiValue| {\n                redis_call_impl(\n                    lua_ctx,\n                    &state_pcall,\n                    &db_cell_pcall,\n                    authenticated,\n                    &sha_str2,\n                    false,\n                    read_only,\n                    args,\n                )\n            })\n            .map_err(|e| e.to_string())?;\n        redis_table\n            .set(\"pcall\", pcall_fn)\n            .map_err(|e| e.to_string())?;\n    }\n\n    // redis.error_reply() - must receive exactly one string argument\n    let error_reply_fn = lua\n        .create_function(|lua_ctx, args: LuaMultiValue| {\n            if args.len() != 1 {\n                return Err(LuaError::RuntimeError(\n                    \"wrong number or type of arguments\".to_string(),\n                ));\n            }\n            let arg = args.into_iter().next().unwrap();\n            let s = match arg {\n                LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(),\n                _ => {\n                    return Err(LuaError::RuntimeError(\n                        \"wrong number or type of arguments\".to_string(),\n                    ));\n                }\n            };\n            let parts: Vec<&str> = s.splitn(2, ' ').collect();\n            let final_msg = if parts.len() == 2 {\n                let prefix = parts[0].strip_prefix('-').unwrap_or(parts[0]);\n                format!(\"{} {}\", prefix, parts[1])\n            } else {\n                let prefix = parts[0].strip_prefix('-').unwrap_or(parts[0]);\n                format!(\"ERR {}\", prefix)\n            };\n            let tbl = lua_ctx.create_table()?;\n            tbl.set(\"err\", final_msg)?;\n            Ok(LuaValue::Table(tbl))\n        })\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"error_reply\", error_reply_fn)\n        .map_err(|e| e.to_string())?;\n\n    // redis.status_reply() - must receive exactly one string argument\n    let status_reply_fn = lua\n        .create_function(|lua_ctx, args: LuaMultiValue| {\n            if args.len() != 1 {\n                return Err(LuaError::RuntimeError(\n                    \"wrong number or type of arguments\".to_string(),\n                ));\n            }\n            let arg = args.into_iter().next().unwrap();\n            let msg = match arg {\n                LuaValue::String(s) => s,\n                _ => {\n                    return Err(LuaError::RuntimeError(\n                        \"wrong number or type of arguments\".to_string(),\n                    ));\n                }\n            };\n            let tbl = lua_ctx.create_table()?;\n            tbl.set(\"ok\", msg)?;\n            Ok(LuaValue::Table(tbl))\n        })\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"status_reply\", status_reply_fn)\n        .map_err(|e| e.to_string())?;\n\n    // redis.log() - no-op\n    let log_fn = lua\n        .create_function(|_, (_level, _msg): (i32, LuaString)| Ok(()))\n        .map_err(|e| e.to_string())?;\n    redis_table.set(\"log\", log_fn).map_err(|e| e.to_string())?;\n\n    // redis.sha1hex() - handle nil/non-string args (treat as empty string)\n    let sha1hex_fn = lua\n        .create_function(|lua_ctx, args: LuaMultiValue| {\n            if args.len() != 1 {\n                return Err(LuaError::RuntimeError(\n                    \"wrong number of arguments\".to_string(),\n                ));\n            }\n            let arg = args.into_iter().next().unwrap();\n            let s = match arg {\n                LuaValue::String(s) => String::from_utf8_lossy(&s.as_bytes()).to_string(),\n                LuaValue::Integer(n) => n.to_string(),\n                LuaValue::Number(n) => n.to_string(),\n                LuaValue::Nil => String::new(),\n                _ => String::new(), // tables, booleans etc treated as empty string\n            };\n            let hash = sha1_hex(&s);\n            let result = lua_ctx.create_string(hash.as_bytes())?;\n            Ok(LuaValue::String(result))\n        })\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"sha1hex\", sha1hex_fn)\n        .map_err(|e| e.to_string())?;\n\n    // redis.replicate_commands() - no-op, returns true (always succeeds since Redis 7.0)\n    let replicate_fn = lua\n        .create_function(|_, ()| Ok(LuaValue::Boolean(true)))\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"replicate_commands\", replicate_fn)\n        .map_err(|e| e.to_string())?;\n\n    // redis.set_repl() - no-op, accepts any value\n    let set_repl_fn = lua\n        .create_function(|_, _: LuaMultiValue| Ok(()))\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"set_repl\", set_repl_fn)\n        .map_err(|e| e.to_string())?;\n\n    // redis.setresp() - validate arg (must be 2 or 3)\n    let setresp_fn = lua\n        .create_function(|_, version: i32| {\n            if version != 2 && version != 3 {\n                return Err(LuaError::RuntimeError(\n                    \"RESP version must be 2 or 3.\".to_string(),\n                ));\n            }\n            Ok(LuaValue::Nil)\n        })\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"setresp\", setresp_fn)\n        .map_err(|e| e.to_string())?;\n\n    // Redis constants\n    redis_table.set(\"LOG_DEBUG\", 0).map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"LOG_VERBOSE\", 1)\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"LOG_NOTICE\", 2)\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"LOG_WARNING\", 3)\n        .map_err(|e| e.to_string())?;\n\n    // Replication constants (used with set_repl)\n    redis_table.set(\"REPL_NONE\", 0).map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"REPL_SLAVE\", 1)\n        .map_err(|e| e.to_string())?;\n    redis_table\n        .set(\"REPL_REPLICA\", 1)\n        .map_err(|e| e.to_string())?;\n    redis_table.set(\"REPL_AOF\", 2).map_err(|e| e.to_string())?;\n    redis_table.set(\"REPL_ALL\", 3).map_err(|e| e.to_string())?;\n\n    lua.globals()\n        .set(\"redis\", redis_table)\n        .map_err(|e| e.to_string())?;\n\n    // Execute the script: compile to a function, then call it.\n    // This ensures that only explicit `return` statements produce return values\n    // (unlike eval() which may try to prepend `return` to the code).\n    let func = lua\n        .load(script)\n        .into_function()\n        .map_err(|e| err_lua_parse_error(&e.to_string()))?;\n\n    // Cache the script after successful compilation (Redis caches on EVAL even if\n    // execution fails at runtime, but not on compilation or argument errors).\n    {\n        let mut inner = state.lock();\n        inner.scripts.insert(sha.to_string(), script.to_string());\n    }\n\n    let result: LuaValue = match func.call(()) {\n        Ok(v) => v,\n        Err(e) => {\n            let msg = sanitize_lua_error(&e.to_string());\n            // Check if it looks like a Redis error (starts with ERR, WRONGTYPE, etc.)\n            if msg.starts_with(\"ERR \")\n                || msg.starts_with(\"WRONGTYPE \")\n                || msg.starts_with(\"NOSCRIPT \")\n                || msg.starts_with(\"NOGROUP \")\n                || msg.starts_with(\"BUSYKEY \")\n                || msg.contains(\"@user_script\")\n            {\n                return Err(msg);\n            }\n            return Err(format!(\"ERR @user_script:0: {}\", msg));\n        }\n    };\n\n    Ok(lua_to_frame(result))\n}\n\n/// Implementation of redis.call() / redis.pcall() within Lua.\n#[allow(clippy::too_many_arguments)]\nfn redis_call_impl(\n    lua: &Lua,\n    state: &Arc<SharedState>,\n    selected_db_cell: &Arc<AtomicUsize>,\n    authenticated: bool,\n    sha: &str,\n    fail_fast: bool,\n    read_only: bool,\n    args: LuaMultiValue,\n) -> LuaResult<LuaValue> {\n    if args.is_empty() {\n        return Err(LuaError::RuntimeError(format!(\n            \"Please specify at least one argument for this redis lib call script: {}, &c.\",\n            sha\n        )));\n    }\n\n    // Convert Lua args to string args\n    let mut cmd_args: Vec<Vec<u8>> = Vec::new();\n    for arg in args {\n        match arg {\n            LuaValue::String(s) => cmd_args.push(s.as_bytes().to_vec()),\n            LuaValue::Integer(n) => cmd_args.push(n.to_string().into_bytes()),\n            LuaValue::Number(n) => cmd_args.push((n as i64).to_string().into_bytes()),\n            _ => {\n                return Err(LuaError::RuntimeError(format!(\n                    \"Lua redis lib command arguments must be strings or integers script: {}, &c.\",\n                    sha\n                )));\n            }\n        }\n    }\n\n    if cmd_args.is_empty() {\n        return Err(LuaError::RuntimeError(msg_not_from_scripts(sha)));\n    }\n\n    let cmd = String::from_utf8_lossy(&cmd_args[0]).to_uppercase();\n    let cmd_args_rest = &cmd_args[1..];\n\n    // Check if the command is disallowed in scripts\n    if DISALLOWED_IN_SCRIPTS.contains(&cmd.as_str()) {\n        let msg = msg_not_from_scripts(sha);\n        if fail_fast {\n            return Err(LuaError::RuntimeError(msg));\n        }\n        let tbl = lua.create_table()?;\n        tbl.set(\"err\", msg)?;\n        return Ok(LuaValue::Table(tbl));\n    }\n\n    // Get the command table\n    let table = state\n        .command_table\n        .get()\n        .expect(\"command table not initialized\");\n\n    // Look up the command\n    let meta = match table.get(&cmd) {\n        Some(m) => m,\n        None => {\n            let msg = if fail_fast {\n                format!(\n                    \"Unknown Redis command called from script script: {}, &c.\",\n                    sha\n                )\n            } else {\n                \"ERR Unknown Redis command called from script\".to_string()\n            };\n            if fail_fast {\n                return Err(LuaError::RuntimeError(msg));\n            }\n            let tbl = lua.create_table()?;\n            tbl.set(\"err\", msg)?;\n            return Ok(LuaValue::Table(tbl));\n        }\n    };\n\n    // Check read-only mode\n    if read_only && !meta.read_only {\n        let msg = \"Write commands are not allowed in read-only scripts\";\n        if fail_fast {\n            return Err(LuaError::RuntimeError(msg.to_string()));\n        }\n        let tbl = lua.create_table()?;\n        tbl.set(\"err\", msg)?;\n        return Ok(LuaValue::Table(tbl));\n    }\n\n    // Create a nested ConnCtx for this call\n    let mut nested_ctx = ConnCtx::new();\n    nested_ctx.selected_db = selected_db_cell.load(Ordering::Relaxed);\n    nested_ctx.authenticated = authenticated;\n    nested_ctx.nested = true;\n    nested_ctx.nested_sha = Some(sha.to_string());\n\n    // Check arity before executing\n    if meta.arity != 0 {\n        let n = cmd_args.len() as i32; // cmd_args already includes command name\n        let bad = if meta.arity > 0 {\n            n != meta.arity\n        } else {\n            n < -meta.arity\n        };\n        if bad {\n            let msg = format!(\n                \"ERR wrong number of arguments for '{}' command\",\n                cmd.to_lowercase()\n            );\n            if fail_fast {\n                return Err(LuaError::RuntimeError(msg));\n            }\n            let tbl = lua.create_table()?;\n            tbl.set(\"err\", msg)?;\n            return Ok(LuaValue::Table(tbl));\n        }\n    }\n\n    // Execute the command\n    let frame = (meta.handler)(state, &mut nested_ctx, cmd_args_rest);\n\n    // If this was a SELECT command, update the shared selected_db\n    if cmd == \"SELECT\" && !matches!(&frame, Frame::Error(_)) {\n        selected_db_cell.store(nested_ctx.selected_db, Ordering::Relaxed);\n    }\n\n    // Convert result to Lua\n    match &frame {\n        Frame::Error(msg) => {\n            if fail_fast {\n                return Err(LuaError::RuntimeError(msg.clone()));\n            }\n            let tbl = lua.create_table()?;\n            tbl.set(\"err\", msg.as_str())?;\n            Ok(LuaValue::Table(tbl))\n        }\n        _ => frame_to_lua(lua, &frame),\n    }\n}\n\n// ── Command handlers ────────────────────────────────────────────────\n\nfn cmd_eval(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_eval_shared(state, ctx, args, false)\n}\n\nfn cmd_eval_ro(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_eval_shared(state, ctx, args, true)\n}\n\nfn cmd_eval_shared(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    read_only: bool,\n) -> Frame {\n    if ctx.nested {\n        return Frame::error(msg_not_from_scripts(\n            ctx.nested_sha.as_deref().unwrap_or(\"\"),\n        ));\n    }\n\n    let script = String::from_utf8_lossy(&args[0]).to_string();\n    let sha = sha1_hex(&script);\n    let remaining = &args[1..];\n\n    match run_lua_script(state, ctx, &sha, &script, read_only, remaining) {\n        Ok(frame) => frame,\n        Err(msg) => Frame::error(msg),\n    }\n}\n\nfn cmd_evalsha(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_evalsha_shared(state, ctx, args, false)\n}\n\nfn cmd_evalsha_ro(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_evalsha_shared(state, ctx, args, true)\n}\n\nfn cmd_evalsha_shared(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    read_only: bool,\n) -> Frame {\n    if ctx.nested {\n        return Frame::error(msg_not_from_scripts(\n            ctx.nested_sha.as_deref().unwrap_or(\"\"),\n        ));\n    }\n\n    let sha = String::from_utf8_lossy(&args[0]).to_string();\n    let remaining = &args[1..];\n\n    // Look up the script\n    let script = {\n        let inner = state.lock();\n        inner.scripts.get(&sha).cloned()\n    };\n\n    match script {\n        Some(script) => match run_lua_script(state, ctx, &sha, &script, read_only, remaining) {\n            Ok(frame) => frame,\n            Err(msg) => Frame::error(msg),\n        },\n        None => Frame::error(MSG_NO_SCRIPT_FOUND),\n    }\n}\n\nfn cmd_script(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if ctx.nested {\n        return Frame::error(msg_not_from_scripts(\n            ctx.nested_sha.as_deref().unwrap_or(\"\"),\n        ));\n    }\n\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    let sub_args = &args[1..];\n\n    match subcmd.as_str() {\n        \"LOAD\" => {\n            if sub_args.len() != 1 {\n                return Frame::error(format!(\n                    \"ERR unknown subcommand or wrong number of arguments for '{}'. Try SCRIPT HELP.\",\n                    \"LOAD\"\n                ));\n            }\n            let script = String::from_utf8_lossy(&sub_args[0]).to_string();\n\n            // Validate syntax by attempting to load in a Lua state\n            let lua = Lua::new();\n            if let Err(e) = lua.load(&script).into_function() {\n                return Frame::error(err_lua_parse_error(&e.to_string()));\n            }\n\n            let sha = sha1_hex(&script);\n            let mut inner = state.lock();\n            inner.scripts.insert(sha.clone(), script);\n            Frame::Bulk(sha.into())\n        }\n        \"EXISTS\" => {\n            if sub_args.is_empty() {\n                return Frame::error(err_wrong_number(\"script|exists\"));\n            }\n            let inner = state.lock();\n            let mut results = Vec::with_capacity(sub_args.len());\n            for arg in sub_args {\n                let sha = String::from_utf8_lossy(arg);\n                if inner.scripts.contains_key(sha.as_ref()) {\n                    results.push(Frame::Integer(1));\n                } else {\n                    results.push(Frame::Integer(0));\n                }\n            }\n            Frame::Array(results)\n        }\n        \"FLUSH\" => {\n            // Accept optional SYNC/ASYNC arg\n            if sub_args.len() > 1 {\n                return Frame::error(\"ERR SCRIPT FLUSH only support SYNC|ASYNC option\");\n            }\n            if sub_args.len() == 1 {\n                let opt = String::from_utf8_lossy(&sub_args[0]).to_uppercase();\n                if opt != \"SYNC\" && opt != \"ASYNC\" {\n                    return Frame::error(\"ERR SCRIPT FLUSH only support SYNC|ASYNC option\");\n                }\n            }\n            let mut inner = state.lock();\n            inner.scripts.clear();\n            Frame::ok()\n        }\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try SCRIPT HELP.\",\n            subcmd\n        )),\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/server.rs",
    "content": "use std::sync::Arc;\nuse std::sync::atomic::Ordering;\nuse std::time::Duration;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{CommandTable, MSG_INVALID_INT, MSG_SYNTAX_ERROR, err_wrong_number};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\n/// Raw RESP blob for COMMAND response, captured from Redis 5.0.7.\nstatic COMMAND_RESP: &[u8] = include_bytes!(\"command_resp.bin\");\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"DBSIZE\", cmd_dbsize, true, 1);\n    table.add(\"FLUSHDB\", cmd_flushdb, false, -1);\n    table.add(\"FLUSHALL\", cmd_flushall, false, -1);\n    table.add(\"COMMAND\", cmd_command, true, -1);\n    table.add(\"TIME\", cmd_time, true, 1);\n    table.add(\"INFO\", cmd_info, true, -1);\n    table.add(\"SWAPDB\", cmd_swapdb, false, 3);\n    table.add(\"MEMORY\", cmd_memory, true, -2);\n    table.add(\"MINIREDIS.FASTFORWARD\", cmd_fastforward, false, 2);\n}\n\n/// DBSIZE\nfn cmd_dbsize(state: &Arc<SharedState>, ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n    Frame::Integer(db.keys.len() as i64)\n}\n\n/// FLUSHDB [ASYNC|SYNC]\nfn cmd_flushdb(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 1 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n    if args.len() == 1 {\n        let opt = String::from_utf8_lossy(&args[0]).to_uppercase();\n        if opt != \"ASYNC\" && opt != \"SYNC\" {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    }\n    let mut inner = state.lock();\n    inner.db_mut(ctx.selected_db).flush();\n    Frame::ok()\n}\n\n/// FLUSHALL [ASYNC|SYNC]\nfn cmd_flushall(state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 1 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n    if args.len() == 1 {\n        let opt = String::from_utf8_lossy(&args[0]).to_uppercase();\n        if opt != \"ASYNC\" && opt != \"SYNC\" {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    }\n    let mut inner = state.lock();\n    for i in 0..16 {\n        inner.db_mut(i).flush();\n    }\n    Frame::ok()\n}\n\n/// COMMAND — returns static command metadata (captured from Redis 5.0.7).\nfn cmd_command(_state: &Arc<SharedState>, _ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    use std::sync::OnceLock;\n    static CACHED: OnceLock<Frame> = OnceLock::new();\n    CACHED\n        .get_or_init(|| {\n            let mut pos = 0;\n            parse_resp(COMMAND_RESP, &mut pos)\n        })\n        .clone()\n}\n\n// ── Minimal RESP parser (for the static COMMAND blob) ────────────────\n\nfn parse_resp(data: &[u8], pos: &mut usize) -> Frame {\n    match data[*pos] {\n        b'*' => {\n            *pos += 1;\n            let n = parse_resp_int(data, pos);\n            let mut items = Vec::with_capacity(n as usize);\n            for _ in 0..n {\n                items.push(parse_resp(data, pos));\n            }\n            Frame::Array(items)\n        }\n        b'$' => {\n            *pos += 1;\n            let n = parse_resp_int(data, pos) as usize;\n            let val = data[*pos..*pos + n].to_vec();\n            *pos += n + 2; // skip data + \\r\\n\n            Frame::Bulk(val.into())\n        }\n        b':' => {\n            *pos += 1;\n            Frame::Integer(parse_resp_int(data, pos))\n        }\n        b'+' => {\n            *pos += 1;\n            let start = *pos;\n            while data[*pos] != b'\\r' {\n                *pos += 1;\n            }\n            let val = String::from_utf8_lossy(&data[start..*pos]).to_string();\n            *pos += 2; // skip \\r\\n\n            Frame::Simple(val)\n        }\n        _ => {\n            *pos += 1;\n            Frame::Null\n        }\n    }\n}\n\nfn parse_resp_int(data: &[u8], pos: &mut usize) -> i64 {\n    let neg = data[*pos] == b'-';\n    if neg {\n        *pos += 1;\n    }\n    let mut val: i64 = 0;\n    while data[*pos] != b'\\r' {\n        val = val * 10 + (data[*pos] - b'0') as i64;\n        *pos += 1;\n    }\n    *pos += 2; // skip \\r\\n\n    if neg { -val } else { val }\n}\n\n/// TIME — returns [seconds, microseconds] of server time.\nfn cmd_time(state: &Arc<SharedState>, _ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    let inner = state.lock();\n    let now = inner.effective_now();\n    let since_epoch = now\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap_or_default();\n    let secs = since_epoch.as_secs();\n    let micros = since_epoch.subsec_micros();\n\n    Frame::Array(vec![\n        Frame::Bulk(secs.to_string().into()),\n        Frame::Bulk(micros.to_string().into()),\n    ])\n}\n\n/// INFO [section]\nfn cmd_info(state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 1 {\n        return Frame::error(err_wrong_number(\"info\"));\n    }\n\n    let connected = state.connected_clients.load(Ordering::Relaxed);\n    let total_conn = state.total_connections_received.load(Ordering::Relaxed);\n    let total_cmds = state.total_commands_processed.load(Ordering::Relaxed);\n\n    let section = if args.len() == 1 {\n        String::from_utf8_lossy(&args[0]).to_lowercase()\n    } else {\n        String::new()\n    };\n\n    let want_all = section.is_empty();\n\n    if !want_all && section != \"clients\" && section != \"stats\" {\n        return Frame::error(format!(\"ERR section ({}) is not supported\", section));\n    }\n\n    let mut result = String::new();\n\n    if want_all || section == \"clients\" {\n        result.push_str(&format!(\"# Clients\\r\\nconnected_clients:{}\\r\\n\", connected));\n    }\n\n    if want_all || section == \"stats\" {\n        result.push_str(&format!(\n            \"# Stats\\r\\ntotal_connections_received:{}\\r\\ntotal_commands_processed:{}\\r\\n\",\n            total_conn, total_cmds\n        ));\n    }\n\n    Frame::Bulk(result.into())\n}\n\n/// SWAPDB db1 db2\nfn cmd_swapdb(state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let db1 = match String::from_utf8_lossy(&args[0]).parse::<i64>() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(\"ERR invalid first DB index\"),\n    };\n    let db2 = match String::from_utf8_lossy(&args[1]).parse::<i64>() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(\"ERR invalid second DB index\"),\n    };\n\n    if !(0..16).contains(&db1) {\n        return Frame::error(\"ERR DB index is out of range\");\n    }\n    if !(0..16).contains(&db2) {\n        return Frame::error(\"ERR DB index is out of range\");\n    }\n\n    let db1 = db1 as usize;\n    let db2 = db2 as usize;\n\n    if db1 != db2 {\n        let mut inner = state.lock();\n        inner.dbs.swap(db1, db2);\n    }\n\n    Frame::ok()\n}\n\n/// MEMORY USAGE key\nfn cmd_memory(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"USAGE\" => {\n            if args.len() < 2 {\n                return Frame::error(\"ERR wrong number of arguments for 'memory|usage' command\");\n            }\n            if args.len() > 2 {\n                return Frame::error(crate::dispatch::MSG_SYNTAX_ERROR);\n            }\n\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            match db.keys.get(key.as_ref()) {\n                None => Frame::Null,\n                Some(kt) => {\n                    let size = estimate_key_size(db, &key, *kt);\n                    Frame::Integer(size as i64)\n                }\n            }\n        }\n        \"HELP\" => Frame::Array(vec![Frame::Bulk(\n            \"MEMORY USAGE <key> - estimate memory usage of key\".into(),\n        )]),\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try MEMORY HELP.\",\n            subcmd.to_lowercase()\n        )),\n    }\n}\n\n/// Estimate the memory usage of a key in bytes (simplified).\nfn estimate_key_size(db: &crate::db::RedisDB, key: &str, kt: KeyType) -> usize {\n    let key_overhead = 16 + key.len(); // pointer + key string\n    let value_size = match kt {\n        KeyType::String => db.string_keys.get(key).map(|v| v.len() + 3).unwrap_or(0),\n        KeyType::Hash => db\n            .hash_keys\n            .get(key)\n            .map(|h| h.iter().map(|(f, v)| f.len() + v.len() + 16).sum::<usize>() + 16)\n            .unwrap_or(0),\n        KeyType::List => db\n            .list_keys\n            .get(key)\n            .map(|l| l.iter().map(|v| v.len() + 16).sum::<usize>() + 16)\n            .unwrap_or(0),\n        KeyType::Set => db\n            .set_keys\n            .get(key)\n            .map(|s| s.iter().map(|m| m.len() + 16).sum::<usize>() + 16)\n            .unwrap_or(0),\n        KeyType::SortedSet => db\n            .sorted_set_keys\n            .get(key)\n            .map(|ss| ss.card() * 32 + 16)\n            .unwrap_or(0),\n        KeyType::Stream => 64,\n        KeyType::HyperLogLog => {\n            // 16384 registers + overhead\n            16384 + 24\n        }\n    };\n    key_overhead + value_size\n}\n\n/// MINIREDIS.FASTFORWARD <ms>\n///\n/// Advance mock time by the given number of milliseconds, expiring any keys\n/// whose TTL falls to zero. Used by the integration test suite.\nfn cmd_fastforward(state: &Arc<SharedState>, _ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let ms: u64 = match String::from_utf8_lossy(&args[0]).parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    inner.fast_forward(Duration::from_millis(ms));\n    Frame::ok()\n}\n"
  },
  {
    "path": "miniredis/src/cmd/set.rs",
    "content": "use std::collections::HashSet;\nuse std::sync::Arc;\n\nuse rand::Rng;\nuse rand::seq::SliceRandom;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_INVALID_CURSOR, MSG_INVALID_INT, MSG_INVALID_KEYS_NUMBER, MSG_OUT_OF_RANGE,\n    MSG_SYNTAX_ERROR, MSG_WRONG_TYPE,\n};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\nuse super::parse_int;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"SADD\", cmd_sadd, false, -3);\n    table.add(\"SREM\", cmd_srem, false, -3);\n    table.add(\"SCARD\", cmd_scard, true, 2);\n    table.add(\"SMEMBERS\", cmd_smembers, true, 2);\n    table.add(\"SISMEMBER\", cmd_sismember, true, 3);\n    table.add(\"SMISMEMBER\", cmd_smismember, true, -3);\n    table.add(\"SDIFF\", cmd_sdiff, true, -2);\n    table.add(\"SDIFFSTORE\", cmd_sdiffstore, false, -3);\n    table.add(\"SINTER\", cmd_sinter, true, -2);\n    table.add(\"SINTERSTORE\", cmd_sinterstore, false, -3);\n    table.add(\"SINTERCARD\", cmd_sintercard, true, -3);\n    table.add(\"SUNION\", cmd_sunion, true, -2);\n    table.add(\"SUNIONSTORE\", cmd_sunionstore, false, -3);\n    table.add(\"SMOVE\", cmd_smove, false, 4);\n    table.add(\"SPOP\", cmd_spop, false, -2);\n    table.add(\"SRANDMEMBER\", cmd_srandmember, true, -2);\n    table.add(\"SSCAN\", cmd_sscan, true, -3);\n}\n\n/// SADD key member [member ...]\nfn cmd_sadd(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let members: Vec<String> = args[1..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n\n    let added = db.set_add(&key, &members, now);\n    Frame::Integer(added)\n}\n\n/// SREM key member [member ...]\nfn cmd_srem(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    let members: Vec<String> = args[1..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n\n    let removed = db.set_rem(&key, &members, now);\n    Frame::Integer(removed)\n}\n\n/// SCARD key\nfn cmd_scard(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let count = db.set_keys.get(key.as_ref()).map(|s| s.len()).unwrap_or(0);\n    Frame::Integer(count as i64)\n}\n\n/// SMEMBERS key\nfn cmd_smembers(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let members = db.set_members(&key);\n    let items: Vec<Frame> = members.into_iter().map(|m| Frame::Bulk(m.into())).collect();\n\n    if ctx.resp3 {\n        Frame::Set(items)\n    } else {\n        Frame::Array(items)\n    }\n}\n\n/// SISMEMBER key member\nfn cmd_sismember(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let member = String::from_utf8_lossy(&args[1]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if db.set_is_member(&key, &member) {\n        Frame::Integer(1)\n    } else {\n        Frame::Integer(0)\n    }\n}\n\n/// SMISMEMBER key member [member ...]\nfn cmd_smismember(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let results: Vec<Frame> = args[1..]\n        .iter()\n        .map(|a| {\n            let member = String::from_utf8_lossy(a);\n            if db.set_is_member(&key, &member) {\n                Frame::Integer(1)\n            } else {\n                Frame::Integer(0)\n            }\n        })\n        .collect();\n\n    Frame::Array(results)\n}\n\n// ── Set operations (diff, inter, union) ──────────────────────────────\n\nenum SetOp {\n    Diff,\n    Inter,\n    Union,\n}\n\nfn set_op(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    keys: &[String],\n    op: SetOp,\n) -> Result<HashSet<String>, Frame> {\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    for key in keys {\n        if let Some(t) = db.key_type(key)\n            && t != KeyType::Set\n        {\n            return Err(Frame::error(MSG_WRONG_TYPE));\n        }\n    }\n\n    if keys.is_empty() {\n        return Ok(HashSet::new());\n    }\n\n    match op {\n        SetOp::Diff => {\n            let first = db.set_keys.get(&keys[0]).cloned().unwrap_or_default();\n            let mut result: HashSet<String> = first;\n            for key in &keys[1..] {\n                if let Some(other) = db.set_keys.get(key) {\n                    result = result.difference(other).cloned().collect();\n                }\n            }\n            Ok(result)\n        }\n        SetOp::Inter => {\n            for key in keys {\n                if !db.keys.contains_key(key) {\n                    return Ok(HashSet::new());\n                }\n            }\n            let first = db.set_keys.get(&keys[0]).cloned().unwrap_or_default();\n            let mut result: HashSet<String> = first;\n            for key in &keys[1..] {\n                if let Some(other) = db.set_keys.get(key) {\n                    result = result.intersection(other).cloned().collect();\n                } else {\n                    return Ok(HashSet::new());\n                }\n            }\n            Ok(result)\n        }\n        SetOp::Union => {\n            let mut result = HashSet::new();\n            for key in keys {\n                if let Some(set) = db.set_keys.get(key) {\n                    result = result.union(set).cloned().collect();\n                }\n            }\n            Ok(result)\n        }\n    }\n}\n\nfn set_to_frame(set: &HashSet<String>, resp3: bool) -> Frame {\n    let mut members: Vec<String> = set.iter().cloned().collect();\n    members.sort();\n    let items: Vec<Frame> = members.into_iter().map(|m| Frame::Bulk(m.into())).collect();\n    if resp3 {\n        Frame::Set(items)\n    } else {\n        Frame::Array(items)\n    }\n}\n\nfn cmd_set_op(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>], op: SetOp) -> Frame {\n    let keys: Vec<String> = args\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n    match set_op(state, ctx, &keys, op) {\n        Ok(set) => set_to_frame(&set, ctx.resp3),\n        Err(e) => e,\n    }\n}\n\nfn cmd_set_store(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    op: SetOp,\n) -> Frame {\n    let dest = String::from_utf8_lossy(&args[0]).into_owned();\n    let keys: Vec<String> = args[1..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n    let result = match set_op(state, ctx, &keys, op) {\n        Ok(set) => set,\n        Err(e) => return e,\n    };\n    let count = result.len() as i64;\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.del(&dest);\n    if !result.is_empty() {\n        db.set_set(&dest, result, now);\n    }\n    Frame::Integer(count)\n}\n\n/// SDIFF key [key ...]\nfn cmd_sdiff(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_set_op(state, ctx, args, SetOp::Diff)\n}\n\n/// SDIFFSTORE destination key [key ...]\nfn cmd_sdiffstore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_set_store(state, ctx, args, SetOp::Diff)\n}\n\n/// SINTER key [key ...]\nfn cmd_sinter(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_set_op(state, ctx, args, SetOp::Inter)\n}\n\n/// SINTERSTORE destination key [key ...]\nfn cmd_sinterstore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_set_store(state, ctx, args, SetOp::Inter)\n}\n\n/// SUNION key [key ...]\nfn cmd_sunion(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_set_op(state, ctx, args, SetOp::Union)\n}\n\n/// SUNIONSTORE destination key [key ...]\nfn cmd_sunionstore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_set_store(state, ctx, args, SetOp::Union)\n}\n\n/// SMOVE source destination member\nfn cmd_smove(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let src = String::from_utf8_lossy(&args[0]).into_owned();\n    let dst = String::from_utf8_lossy(&args[1]).into_owned();\n    let member = String::from_utf8_lossy(&args[2]).into_owned();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&src);\n    db.check_ttl(&dst);\n\n    // Type checks\n    if let Some(t) = db.key_type(&src)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n    if let Some(t) = db.key_type(&dst)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    if !db.keys.contains_key(&src) {\n        return Frame::Integer(0);\n    }\n\n    if !db.set_is_member(&src, &member) {\n        return Frame::Integer(0);\n    }\n\n    db.set_rem(&src, std::slice::from_ref(&member), now);\n    db.set_add(&dst, std::slice::from_ref(&member), now);\n    Frame::Integer(1)\n}\n\n/// SPOP key [count]\nfn cmd_spop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut with_count = false;\n    let mut count: usize = 1;\n\n    if args.len() > 1 {\n        match parse_int(&args[1]) {\n            Some(n) if n < 0 => return Frame::error(MSG_OUT_OF_RANGE),\n            Some(n) => {\n                count = n as usize;\n                with_count = true;\n            }\n            None => return Frame::error(MSG_INVALID_INT),\n        }\n    }\n    if args.len() > 2 {\n        return Frame::error(MSG_INVALID_INT);\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return if with_count {\n            Frame::Array(vec![])\n        } else {\n            Frame::Null\n        };\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut members = db.set_members(&key);\n    let mut deleted = Vec::new();\n    for _ in 0..count {\n        if members.is_empty() {\n            break;\n        }\n        let idx = inner.rng.random_range(0..members.len());\n        let member = members.remove(idx);\n        let db = inner.db_mut(ctx.selected_db);\n        db.set_rem(&key, std::slice::from_ref(&member), now);\n        deleted.push(member);\n    }\n\n    if !with_count {\n        if deleted.is_empty() {\n            Frame::Null\n        } else {\n            Frame::Bulk(deleted[0].clone().into())\n        }\n    } else {\n        Frame::Array(deleted.into_iter().map(|m| Frame::Bulk(m.into())).collect())\n    }\n}\n\n/// SRANDMEMBER key [count]\nfn cmd_srandmember(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 2 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut count: i64 = 0;\n    let mut with_count = false;\n\n    if args.len() == 2 {\n        match parse_int(&args[1]) {\n            Some(n) => {\n                count = n;\n                with_count = true;\n            }\n            None => return Frame::error(MSG_INVALID_INT),\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return if with_count {\n            Frame::Array(vec![])\n        } else {\n            Frame::Null\n        };\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut members = db.set_members(&key);\n\n    if count < 0 {\n        // Negative count: allow duplicates\n        let abs_count = (-count) as usize;\n        let mut result = Vec::with_capacity(abs_count);\n        for _ in 0..abs_count {\n            let idx = inner.rng.random_range(0..members.len());\n            result.push(Frame::Bulk(members[idx].clone().into()));\n        }\n        return Frame::Array(result);\n    }\n\n    // Positive count: unique members, shuffle\n    members.shuffle(&mut inner.rng);\n    let take = (count as usize).min(members.len());\n\n    if !with_count {\n        return Frame::Bulk(members[0].clone().into());\n    }\n\n    Frame::Array(\n        members[..take]\n            .iter()\n            .map(|m| Frame::Bulk(m.clone().into()))\n            .collect(),\n    )\n}\n\n/// SSCAN key cursor [MATCH pattern] [COUNT count]\nfn cmd_sscan(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let cursor: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_CURSOR),\n    };\n\n    let opts = match super::parse_scan_opts(&args[2..], false) {\n        Ok(o) => o,\n        Err(e) => return e,\n    };\n\n    // SSCAN validates COUNT more strictly\n    let scan_count: usize = match opts.count {\n        Some(n) if n < 0 => return Frame::error(MSG_INVALID_INT),\n        Some(0) => return Frame::error(MSG_SYNTAX_ERROR),\n        Some(n) => n as usize,\n        None => 0,\n    };\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if db.keys.contains_key(key.as_ref())\n        && let Some(t) = db.key_type(&key)\n        && t != KeyType::Set\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut members = db.set_members(&key);\n    members.sort();\n\n    // Apply MATCH filter\n    if let Some(ref pat) = opts.pattern {\n        members = crate::keys::match_keys_vec(&members, pat);\n    }\n\n    let low = cursor as usize;\n    let high = if scan_count > 0 {\n        (low + scan_count).min(members.len())\n    } else {\n        members.len()\n    };\n\n    if low >= members.len() {\n        return Frame::Array(vec![Frame::Bulk(\"0\".into()), Frame::Array(vec![])]);\n    }\n\n    let cursor_value = if high >= members.len() { 0 } else { high };\n\n    let selected = &members[low..high];\n    Frame::Array(vec![\n        Frame::Bulk(cursor_value.to_string().into()),\n        Frame::Array(\n            selected\n                .iter()\n                .map(|m| Frame::Bulk(m.clone().into()))\n                .collect(),\n        ),\n    ])\n}\n\n/// SINTERCARD numkeys key [key ...] [LIMIT limit]\nfn cmd_sintercard(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let num_keys = match parse_int(&args[0]) {\n        Some(n) if n < 1 => {\n            return Frame::error(\"ERR numkeys should be greater than 0\");\n        }\n        Some(n) => n as usize,\n        None => {\n            return Frame::error(\"ERR numkeys should be greater than 0\");\n        }\n    };\n\n    if args.len() < 1 + num_keys {\n        return Frame::error(MSG_INVALID_KEYS_NUMBER);\n    }\n\n    let keys: Vec<String> = args[1..1 + num_keys]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n\n    let mut limit: usize = 0;\n    let rest = &args[1 + num_keys..];\n\n    if rest.len() == 2 {\n        let opt = String::from_utf8_lossy(&rest[0]).to_uppercase();\n        if opt == \"LIMIT\" {\n            match parse_int(&rest[1]) {\n                Some(n) if n < 0 => {\n                    return Frame::error(\"ERR LIMIT can't be negative\");\n                }\n                Some(n) => limit = n as usize,\n                None => return Frame::error(MSG_INVALID_INT),\n            }\n        } else {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    } else if !rest.is_empty() {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let result = match set_op(state, ctx, &keys, SetOp::Inter) {\n        Ok(set) => set,\n        Err(e) => return e,\n    };\n\n    let count = result.len();\n    if limit > 0 && count > limit {\n        Frame::Integer(limit as i64)\n    } else {\n        Frame::Integer(count as i64)\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/sorted_set.rs",
    "content": "use std::sync::Arc;\n\nuse rand::Rng;\nuse rand::seq::SliceRandom;\n\nuse super::{parse_float, parse_int};\nuse crate::cmd::string::format_float;\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_INVALID_CURSOR, MSG_INVALID_FLOAT, MSG_INVALID_INT, MSG_INVALID_MIN_MAX,\n    MSG_INVALID_RANGE_ITEM, MSG_SINGLE_ELEMENT_PAIR, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE,\n    MSG_XX_AND_NX, err_wrong_number,\n};\nuse crate::frame::Frame;\nuse crate::types::{Direction, SSElem, SortedSet};\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"ZADD\", cmd_zadd, false, -4);\n    table.add(\"ZCARD\", cmd_zcard, true, 2);\n    table.add(\"ZCOUNT\", cmd_zcount, true, 4);\n    table.add(\"ZINCRBY\", cmd_zincrby, false, 4);\n    table.add(\"ZSCORE\", cmd_zscore, true, 3);\n    table.add(\"ZMSCORE\", cmd_zmscore, true, -3);\n    table.add(\"ZRANK\", cmd_zrank, true, -3);\n    table.add(\"ZREVRANK\", cmd_zrevrank, true, -3);\n    table.add(\"ZREM\", cmd_zrem, false, -3);\n    table.add(\"ZRANGE\", cmd_zrange, true, -4);\n    table.add(\"ZREVRANGE\", cmd_zrevrange, true, -4);\n    table.add(\"ZRANGEBYSCORE\", cmd_zrangebyscore, true, -4);\n    table.add(\"ZREVRANGEBYSCORE\", cmd_zrevrangebyscore, true, -4);\n    table.add(\"ZRANGEBYLEX\", cmd_zrangebylex, true, -4);\n    table.add(\"ZREVRANGEBYLEX\", cmd_zrevrangebylex, true, -4);\n    table.add(\"ZLEXCOUNT\", cmd_zlexcount, true, 4);\n    table.add(\"ZREMRANGEBYRANK\", cmd_zremrangebyrank, false, 4);\n    table.add(\"ZREMRANGEBYSCORE\", cmd_zremrangebyscore, false, 4);\n    table.add(\"ZREMRANGEBYLEX\", cmd_zremrangebylex, false, 4);\n    table.add(\"ZUNIONSTORE\", cmd_zunionstore, false, -4);\n    table.add(\"ZINTERSTORE\", cmd_zinterstore, false, -4);\n    table.add(\"ZPOPMIN\", cmd_zpopmin, false, -2);\n    table.add(\"ZPOPMAX\", cmd_zpopmax, false, -2);\n    table.add(\"ZSCAN\", cmd_zscan, true, -3);\n    table.add(\"ZINTER\", cmd_zinter, true, -3);\n    table.add(\"ZUNION\", cmd_zunion, true, -3);\n    table.add(\"ZRANDMEMBER\", cmd_zrandmember, true, -2);\n}\n\n/// Format a float for Redis output (scores).\nfn write_float(f: f64) -> String {\n    if f == f64::INFINITY {\n        \"inf\".to_string()\n    } else if f == f64::NEG_INFINITY {\n        \"-inf\".to_string()\n    } else {\n        format_float(f)\n    }\n}\n\n/// Parse a score range like \"1.5\", \"(1.5\", \"+inf\", \"-inf\".\nfn parse_float_range(s: &str) -> Result<(f64, bool), ()> {\n    if s.is_empty() {\n        return Err(());\n    }\n    let (s, inclusive) = if let Some(rest) = s.strip_prefix('(') {\n        (rest, false)\n    } else {\n        (s, true)\n    };\n    match s.to_lowercase().as_str() {\n        \"+inf\" | \"inf\" => Ok((f64::INFINITY, true)),\n        \"-inf\" => Ok((f64::NEG_INFINITY, true)),\n        _ => s.parse::<f64>().map(|f| (f, inclusive)).map_err(|_| ()),\n    }\n}\n\n/// Parse a lex range like \"[a\", \"(a\", \"+\", \"-\".\nfn parse_lex_range(s: &str) -> Result<(String, bool), ()> {\n    if s.is_empty() {\n        return Err(());\n    }\n    if s == \"+\" || s == \"-\" {\n        return Ok((s.to_string(), false));\n    }\n    match s.as_bytes()[0] {\n        b'(' => Ok((s[1..].to_string(), false)),\n        b'[' => Ok((s[1..].to_string(), true)),\n        _ => Err(()),\n    }\n}\n\n/// Filter elements by score range.\nfn with_ss_range(\n    members: Vec<SSElem>,\n    min: f64,\n    min_incl: bool,\n    max: f64,\n    max_incl: bool,\n) -> Vec<SSElem> {\n    members\n        .into_iter()\n        .filter(|e| {\n            let above_min = if min_incl {\n                e.score >= min\n            } else {\n                e.score > min\n            };\n            let below_max = if max_incl {\n                e.score <= max\n            } else {\n                e.score < max\n            };\n            above_min && below_max\n        })\n        .collect()\n}\n\n/// Filter member names by lex range.\nfn with_lex_range(\n    members: Vec<String>,\n    min: &str,\n    min_incl: bool,\n    max: &str,\n    max_incl: bool,\n) -> Vec<String> {\n    if max == \"-\" || min == \"+\" {\n        return Vec::new();\n    }\n    members\n        .into_iter()\n        .filter(|m| {\n            let above_min = if min == \"-\" {\n                true\n            } else if min_incl {\n                m.as_str() >= min\n            } else {\n                m.as_str() > min\n            };\n            let below_max = if max == \"+\" {\n                true\n            } else if max_incl {\n                m.as_str() <= max\n            } else {\n                m.as_str() < max\n            };\n            above_min && below_max\n        })\n        .collect()\n}\n\n/// Normalize Redis-style range indices for sorted sets.\nfn redis_range(len: usize, start: i64, stop: i64) -> (usize, usize) {\n    let len = len as i64;\n    let mut s = if start < 0 { len + start } else { start };\n    let mut e = if stop < 0 { len + stop } else { stop };\n    if s < 0 {\n        s = 0;\n    }\n    if e >= len {\n        e = len - 1;\n    }\n    if s > e || s >= len {\n        return (0, 0);\n    }\n    (s as usize, (e + 1) as usize)\n}\n\n// ── Commands ─────────────────────────────────────────────────────────\n\n/// ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]\nfn cmd_zadd(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut i = 1;\n    let mut nx = false;\n    let mut xx = false;\n    let mut gt = false;\n    let mut lt = false;\n    let mut ch = false;\n    let mut incr = false;\n\n    // Parse flags\n    while i < args.len() {\n        let flag = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match flag.as_str() {\n            \"NX\" => {\n                nx = true;\n                i += 1;\n            }\n            \"XX\" => {\n                xx = true;\n                i += 1;\n            }\n            \"GT\" => {\n                gt = true;\n                i += 1;\n            }\n            \"LT\" => {\n                lt = true;\n                i += 1;\n            }\n            \"CH\" => {\n                ch = true;\n                i += 1;\n            }\n            \"INCR\" => {\n                incr = true;\n                i += 1;\n            }\n            _ => break,\n        }\n    }\n\n    // Remaining args should be score-member pairs\n    let remaining = &args[i..];\n    if remaining.is_empty() || !remaining.len().is_multiple_of(2) {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    // Parse score-member pairs\n    let mut elems: Vec<(String, f64)> = Vec::new();\n    let mut j = 0;\n    while j < remaining.len() {\n        let score = match parse_float(&remaining[j]) {\n            Some(f) => f,\n            None => return Frame::error(MSG_INVALID_FLOAT),\n        };\n        let member = String::from_utf8_lossy(&remaining[j + 1]).into_owned();\n        elems.push((member, score));\n        j += 2;\n    }\n\n    // Validation\n    if xx && nx {\n        return Frame::error(MSG_XX_AND_NX);\n    }\n    if (gt || lt) && (gt && lt || nx) {\n        return Frame::error(\"ERR GT, LT, and/or NX options at the same time are not compatible\");\n    }\n    if incr && elems.len() > 1 {\n        return Frame::error(MSG_SINGLE_ELEMENT_PAIR);\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // INCR mode\n    if incr {\n        let (member, delta) = &elems[0];\n        if nx && db.sset_exists(&key, member) {\n            return Frame::Null;\n        }\n        if xx && !db.sset_exists(&key, member) {\n            return Frame::Null;\n        }\n        let new_score = db.sset_incrby(&key, member, *delta, now);\n        return if ctx.resp3 {\n            Frame::Double(new_score)\n        } else {\n            Frame::Bulk(write_float(new_score).into())\n        };\n    }\n\n    let mut count = 0i64;\n    for (member, score) in &elems {\n        let exists = db.sset_exists(&key, member);\n        if nx && exists {\n            continue;\n        }\n        if xx && !exists {\n            continue;\n        }\n        if gt\n            && exists\n            && let Some(old) = db.sset_score(&key, member)\n            && *score <= old\n        {\n            continue;\n        }\n        if lt\n            && exists\n            && let Some(old) = db.sset_score(&key, member)\n            && *score >= old\n        {\n            continue;\n        }\n        let old_score = db.sset_score(&key, member);\n        let is_new = db.sset_add(&key, *score, member, now);\n        if is_new || (ch && old_score != Some(*score)) {\n            count += 1;\n        }\n    }\n\n    Frame::Integer(count)\n}\n\n/// ZCARD key\nfn cmd_zcard(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    Frame::Integer(db.sset_card(&key) as i64)\n}\n\n/// ZCOUNT key min max\nfn cmd_zcount(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let min_s = String::from_utf8_lossy(&args[1]);\n    let max_s = String::from_utf8_lossy(&args[2]);\n\n    let (min, min_incl) = match parse_float_range(&min_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_MIN_MAX),\n    };\n    let (max, max_incl) = match parse_float_range(&max_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_MIN_MAX),\n    };\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key.as_ref()) {\n        Some(ss) => ss,\n        None => return Frame::Integer(0),\n    };\n    let elems = ss.by_score(Direction::Asc);\n    let filtered = with_ss_range(elems, min, min_incl, max, max_incl);\n    Frame::Integer(filtered.len() as i64)\n}\n\n/// ZINCRBY key increment member\nfn cmd_zincrby(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let delta = match parse_float(&args[1]) {\n        Some(f) => f,\n        None => return Frame::error(MSG_INVALID_FLOAT),\n    };\n    let member = String::from_utf8_lossy(&args[2]).into_owned();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let new_score = db.sset_incrby(&key, &member, delta, now);\n    Frame::Bulk(write_float(new_score).into())\n}\n\n/// ZSCORE key member\nfn cmd_zscore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let member = String::from_utf8_lossy(&args[1]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Null;\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.sset_score(&key, &member) {\n        Some(score) => {\n            if ctx.resp3 {\n                Frame::Double(score)\n            } else {\n                Frame::Bulk(write_float(score).into())\n            }\n        }\n        None => Frame::Null,\n    }\n}\n\n/// ZMSCORE key member [member ...]\nfn cmd_zmscore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let results: Vec<Frame> = args[1..]\n        .iter()\n        .map(|a| {\n            let member = String::from_utf8_lossy(a);\n            match db.sset_score(&key, &member) {\n                Some(score) => Frame::Bulk(write_float(score).into()),\n                None => Frame::Null,\n            }\n        })\n        .collect();\n\n    Frame::Array(results)\n}\n\n/// ZRANK key member\nfn cmd_zrank(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zrank_impl(state, ctx, args, Direction::Asc, \"zrank\")\n}\n\n/// ZREVRANK key member\nfn cmd_zrevrank(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zrank_impl(state, ctx, args, Direction::Desc, \"zrevrank\")\n}\n\nfn zrank_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    dir: Direction,\n    cmd: &str,\n) -> Frame {\n    if args.len() > 3 {\n        return Frame::error(err_wrong_number(cmd));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]);\n    let member = String::from_utf8_lossy(&args[1]);\n\n    let with_score =\n        args.len() == 3 && String::from_utf8_lossy(&args[2]).to_uppercase() == \"WITHSCORE\";\n\n    if args.len() == 3 && !with_score {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return if with_score {\n            Frame::NullArray\n        } else {\n            Frame::Null\n        };\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key.as_ref()) {\n        Some(ss) => ss,\n        None => {\n            return if with_score {\n                Frame::NullArray\n            } else {\n                Frame::Null\n            };\n        }\n    };\n\n    match ss.rank(&member, dir) {\n        Some(rank) => {\n            if with_score {\n                let score = ss.get(&member).unwrap_or(0.0);\n                Frame::Array(vec![\n                    Frame::Integer(rank as i64),\n                    Frame::Bulk(write_float(score).into()),\n                ])\n            } else {\n                Frame::Integer(rank as i64)\n            }\n        }\n        None => {\n            if with_score {\n                Frame::NullArray\n            } else {\n                Frame::Null\n            }\n        }\n    }\n}\n\n/// ZREM key member [member ...]\nfn cmd_zrem(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut deleted = 0i64;\n    for a in &args[1..] {\n        let member = String::from_utf8_lossy(a);\n        if db.sset_rem(&key, &member, now) {\n            deleted += 1;\n        }\n    }\n    Frame::Integer(deleted)\n}\n\n// ── Range commands ───────────────────────────────────────────────────\n\n/// ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]\nfn cmd_zrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let min_s = String::from_utf8_lossy(&args[1]).into_owned();\n    let max_s = String::from_utf8_lossy(&args[2]).into_owned();\n\n    let mut with_scores = false;\n    let mut by_score = false;\n    let mut by_lex = false;\n    let mut reverse = false;\n    let mut with_limit = false;\n    let mut offset_s = String::new();\n    let mut count_s = String::new();\n\n    let mut i = 3;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"BYSCORE\" => {\n                by_score = true;\n                i += 1;\n            }\n            \"BYLEX\" => {\n                by_lex = true;\n                i += 1;\n            }\n            \"REV\" => {\n                reverse = true;\n                i += 1;\n            }\n            \"LIMIT\" => {\n                with_limit = true;\n                i += 1;\n                if i + 1 >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                offset_s = String::from_utf8_lossy(&args[i]).into_owned();\n                count_s = String::from_utf8_lossy(&args[i + 1]).into_owned();\n                i += 2;\n            }\n            \"WITHSCORES\" => {\n                with_scores = true;\n                i += 1;\n            }\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    }\n\n    if by_score && by_lex {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if by_score {\n        run_range_by_score(\n            db,\n            &key,\n            &min_s,\n            &max_s,\n            reverse,\n            with_limit,\n            &offset_s,\n            &count_s,\n            with_scores,\n        )\n    } else if by_lex {\n        run_range_by_lex(\n            db, &key, &min_s, &max_s, reverse, with_limit, &offset_s, &count_s,\n        )\n    } else {\n        if with_limit {\n            return Frame::error(\n                \"ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX\",\n            );\n        }\n        run_range_by_rank(db, &key, &min_s, &max_s, reverse, with_scores)\n    }\n}\n\n/// ZREVRANGE key start stop [WITHSCORES]\nfn cmd_zrevrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let min_s = String::from_utf8_lossy(&args[1]).into_owned();\n    let max_s = String::from_utf8_lossy(&args[2]).into_owned();\n\n    let mut with_scores = false;\n    if args.len() > 3 {\n        let opt = String::from_utf8_lossy(&args[3]).to_uppercase();\n        if opt == \"WITHSCORES\" {\n            with_scores = true;\n        } else {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    run_range_by_rank(db, &key, &min_s, &max_s, true, with_scores)\n}\n\n/// ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]\nfn cmd_zrangebyscore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zrangebyscore_impl(state, ctx, args, false)\n}\n\n/// ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]\nfn cmd_zrevrangebyscore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zrangebyscore_impl(state, ctx, args, true)\n}\n\nfn zrangebyscore_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    reverse: bool,\n) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let min_s = String::from_utf8_lossy(&args[1]).into_owned();\n    let max_s = String::from_utf8_lossy(&args[2]).into_owned();\n\n    let mut with_scores = false;\n    let mut with_limit = false;\n    let mut offset_s = String::new();\n    let mut count_s = String::new();\n\n    let mut i = 3;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"WITHSCORES\" => {\n                with_scores = true;\n                i += 1;\n            }\n            \"LIMIT\" => {\n                with_limit = true;\n                i += 1;\n                if i + 1 >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                offset_s = String::from_utf8_lossy(&args[i]).into_owned();\n                count_s = String::from_utf8_lossy(&args[i + 1]).into_owned();\n                i += 2;\n            }\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    run_range_by_score(\n        db,\n        &key,\n        &min_s,\n        &max_s,\n        reverse,\n        with_limit,\n        &offset_s,\n        &count_s,\n        with_scores,\n    )\n}\n\n/// ZRANGEBYLEX key min max [LIMIT offset count]\nfn cmd_zrangebylex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zrangebylex_impl(state, ctx, args, false)\n}\n\n/// ZREVRANGEBYLEX key max min [LIMIT offset count]\nfn cmd_zrevrangebylex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zrangebylex_impl(state, ctx, args, true)\n}\n\nfn zrangebylex_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    reverse: bool,\n) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let min_s = String::from_utf8_lossy(&args[1]).into_owned();\n    let max_s = String::from_utf8_lossy(&args[2]).into_owned();\n\n    let mut with_limit = false;\n    let mut offset_s = String::new();\n    let mut count_s = String::new();\n\n    let mut i = 3;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        if opt == \"LIMIT\" {\n            with_limit = true;\n            i += 1;\n            if i + 1 >= args.len() {\n                return Frame::error(MSG_SYNTAX_ERROR);\n            }\n            offset_s = String::from_utf8_lossy(&args[i]).into_owned();\n            count_s = String::from_utf8_lossy(&args[i + 1]).into_owned();\n            i += 2;\n        } else {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    run_range_by_lex(\n        db, &key, &min_s, &max_s, reverse, with_limit, &offset_s, &count_s,\n    )\n}\n\n/// ZLEXCOUNT key min max\nfn cmd_zlexcount(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let min_s = String::from_utf8_lossy(&args[1]);\n    let max_s = String::from_utf8_lossy(&args[2]);\n\n    let (min, min_incl) = match parse_lex_range(&min_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM),\n    };\n    let (max, max_incl) = match parse_lex_range(&max_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM),\n    };\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key.as_ref()) {\n        Some(ss) => ss,\n        None => return Frame::Integer(0),\n    };\n    let mut members = ss.members_sorted();\n    members.sort();\n    let filtered = with_lex_range(members, &min, min_incl, &max, max_incl);\n    Frame::Integer(filtered.len() as i64)\n}\n\n// ── Remove range commands ────────────────────────────────────────────\n\n/// ZREMRANGEBYRANK key start stop\nfn cmd_zremrangebyrank(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let start = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let stop = match parse_int(&args[2]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(&key) {\n        Some(ss) => ss,\n        None => return Frame::Integer(0),\n    };\n    let members = ss.members_sorted();\n    let (rs, re) = redis_range(members.len(), start, stop);\n    let to_remove: Vec<String> = members[rs..re].to_vec();\n\n    for m in &to_remove {\n        db.sset_rem(&key, m, now);\n    }\n    Frame::Integer(to_remove.len() as i64)\n}\n\n/// ZREMRANGEBYSCORE key min max\nfn cmd_zremrangebyscore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let min_s = String::from_utf8_lossy(&args[1]);\n    let max_s = String::from_utf8_lossy(&args[2]);\n\n    let (min, min_incl) = match parse_float_range(&min_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_MIN_MAX),\n    };\n    let (max, max_incl) = match parse_float_range(&max_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_MIN_MAX),\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(&key) {\n        Some(ss) => ss,\n        None => return Frame::Integer(0),\n    };\n    let elems = ss.by_score(Direction::Asc);\n    let filtered = with_ss_range(elems, min, min_incl, max, max_incl);\n    let to_remove: Vec<String> = filtered.into_iter().map(|e| e.member).collect();\n\n    for m in &to_remove {\n        db.sset_rem(&key, m, now);\n    }\n    Frame::Integer(to_remove.len() as i64)\n}\n\n/// ZREMRANGEBYLEX key min max\nfn cmd_zremrangebylex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let min_s = String::from_utf8_lossy(&args[1]);\n    let max_s = String::from_utf8_lossy(&args[2]);\n\n    let (min, min_incl) = match parse_lex_range(&min_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM),\n    };\n    let (max, max_incl) = match parse_lex_range(&max_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM),\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(&key) {\n        Some(ss) => ss,\n        None => return Frame::Integer(0),\n    };\n    let mut members = ss.members_sorted();\n    members.sort();\n    let filtered = with_lex_range(members, &min, min_incl, &max, max_incl);\n\n    for m in &filtered {\n        db.sset_rem(&key, m, now);\n    }\n    Frame::Integer(filtered.len() as i64)\n}\n\n// ── Set operations (ZUNIONSTORE, ZINTERSTORE) ────────────────────────\n\n/// ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS w...] [AGGREGATE SUM|MIN|MAX]\nfn cmd_zunionstore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zstore_impl(state, ctx, args, false)\n}\n\n/// ZINTERSTORE destination numkeys key [key ...] [WEIGHTS w...] [AGGREGATE SUM|MIN|MAX]\nfn cmd_zinterstore(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zstore_impl(state, ctx, args, true)\n}\n\nfn zstore_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    intersect: bool,\n) -> Frame {\n    let dest = String::from_utf8_lossy(&args[0]).into_owned();\n    let num_keys = match parse_int(&args[1]) {\n        Some(n) if n > 0 => n as usize,\n        Some(_) => {\n            return Frame::error(\"ERR at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE\");\n        }\n        _ => return Frame::error(MSG_INVALID_INT),\n    };\n\n    if args.len() < 2 + num_keys {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let keys: Vec<String> = args[2..2 + num_keys]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n    let mut rest = &args[2 + num_keys..];\n\n    let mut weights: Vec<f64> = Vec::new();\n    let mut with_weights = false;\n    let mut aggregate = \"sum\".to_string();\n\n    while !rest.is_empty() {\n        let opt = String::from_utf8_lossy(&rest[0]).to_lowercase();\n        match opt.as_str() {\n            \"weights\" => {\n                if rest.len() < num_keys + 1 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                for i in 0..num_keys {\n                    match parse_float(&rest[i + 1]) {\n                        Some(f) => weights.push(f),\n                        None => return Frame::error(\"ERR weight value is not a float\"),\n                    }\n                }\n                with_weights = true;\n                rest = &rest[num_keys + 1..];\n            }\n            \"aggregate\" => {\n                if rest.len() < 2 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                aggregate = String::from_utf8_lossy(&rest[1]).to_lowercase();\n                match aggregate.as_str() {\n                    \"sum\" | \"min\" | \"max\" => {}\n                    _ => return Frame::error(MSG_SYNTAX_ERROR),\n                }\n                rest = &rest[2..];\n            }\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    // Collect all scores\n    let mut sset: std::collections::HashMap<String, f64> = std::collections::HashMap::new();\n    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();\n\n    for (i, key) in keys.iter().enumerate() {\n        if !db.keys.contains_key(key) {\n            continue;\n        }\n        let key_type = db.key_type(key);\n\n        let set: std::collections::HashMap<String, f64> = match key_type {\n            Some(crate::types::KeyType::Set) => db\n                .set_keys\n                .get(key)\n                .map(|s| s.iter().map(|m| (m.clone(), 1.0)).collect())\n                .unwrap_or_default(),\n            Some(crate::types::KeyType::SortedSet) => db\n                .sorted_set_keys\n                .get(key)\n                .map(|ss| ss.scores.clone())\n                .unwrap_or_default(),\n            _ => return Frame::error(MSG_WRONG_TYPE),\n        };\n\n        for (member, mut score) in set {\n            if with_weights {\n                score *= weights[i];\n            }\n            *counts.entry(member.clone()).or_insert(0) += 1;\n            let entry = sset.entry(member);\n            match entry {\n                std::collections::hash_map::Entry::Vacant(e) => {\n                    e.insert(score);\n                }\n                std::collections::hash_map::Entry::Occupied(mut e) => {\n                    let old = *e.get();\n                    match aggregate.as_str() {\n                        \"sum\" => *e.get_mut() += score,\n                        \"min\" => {\n                            if score < old {\n                                *e.get_mut() = score;\n                            }\n                        }\n                        \"max\" => {\n                            if score > old {\n                                *e.get_mut() = score;\n                            }\n                        }\n                        _ => {}\n                    }\n                }\n            }\n        }\n    }\n\n    // For ZINTERSTORE: only keep members present in ALL keys\n    if intersect {\n        sset.retain(|member, _| counts.get(member).copied().unwrap_or(0) == keys.len());\n    }\n\n    // Store result\n    db.del(&dest);\n    if !sset.is_empty() {\n        let mut new_ss = SortedSet::new();\n        for (member, score) in &sset {\n            new_ss.set(*score, member);\n        }\n        db.sset_set(&dest, new_ss, now);\n    }\n\n    Frame::Integer(sset.len() as i64)\n}\n\n// ── ZPOPMIN/ZPOPMAX ─────────────────────────────────────────────────\n\n/// ZPOPMIN key [count]\nfn cmd_zpopmin(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zpop_impl(state, ctx, args, false)\n}\n\n/// ZPOPMAX key [count]\nfn cmd_zpopmax(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zpop_impl(state, ctx, args, true)\n}\n\nfn zpop_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    reverse: bool,\n) -> Frame {\n    if args.len() > 2 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let count = if args.len() > 1 {\n        match parse_int(&args[1]) {\n            Some(n) if n >= 0 => n as usize,\n            _ => return Frame::error(MSG_INVALID_INT),\n        }\n    } else {\n        1\n    };\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Array(vec![]);\n    }\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(&key) {\n        Some(ss) => ss,\n        None => return Frame::Array(vec![]),\n    };\n\n    let dir = if reverse {\n        Direction::Desc\n    } else {\n        Direction::Asc\n    };\n    let elems = ss.by_score(dir);\n    let take = count.min(elems.len());\n    let to_pop: Vec<SSElem> = elems[..take].to_vec();\n\n    let mut result = Vec::new();\n    for e in &to_pop {\n        result.push(Frame::Bulk(e.member.clone().into()));\n        result.push(Frame::Bulk(write_float(e.score).into()));\n        db.sset_rem(&key, &e.member, now);\n    }\n\n    Frame::Array(result)\n}\n\n// ── ZSCAN ────────────────────────────────────────────────────────────\n\n/// ZSCAN key cursor [MATCH pattern] [COUNT count]\nfn cmd_zscan(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let _cursor = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_CURSOR),\n    };\n\n    let opts = match super::parse_scan_opts(&args[2..], false) {\n        Ok(o) => o,\n        Err(e) => return e,\n    };\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key.as_ref()) {\n        Some(ss) => ss,\n        None => {\n            return Frame::Array(vec![Frame::Bulk(\"0\".into()), Frame::Array(vec![])]);\n        }\n    };\n\n    let mut members = ss.members_sorted();\n    members.sort();\n\n    // Apply MATCH filter\n    if let Some(ref pat) = opts.pattern {\n        members = crate::keys::match_keys_vec(&members, pat);\n    }\n\n    // Return all members with cursor=0 (no real cursor pagination)\n    let mut result = Vec::new();\n    for m in &members {\n        result.push(Frame::Bulk(m.clone().into()));\n        let score = ss.get(m).unwrap_or(0.0);\n        result.push(Frame::Bulk(write_float(score).into()));\n    }\n\n    Frame::Array(vec![Frame::Bulk(\"0\".into()), Frame::Array(result)])\n}\n\n// ── Range helper functions ───────────────────────────────────────────\n\nfn run_range_by_rank(\n    db: &crate::db::RedisDB,\n    key: &str,\n    min_s: &str,\n    max_s: &str,\n    reverse: bool,\n    with_scores: bool,\n) -> Frame {\n    let min: i64 = match min_s.parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(MSG_INVALID_INT),\n    };\n    let max: i64 = match max_s.parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(MSG_INVALID_INT),\n    };\n\n    if !db.keys.contains_key(key) {\n        return Frame::Array(vec![]);\n    }\n    if let Some(t) = db.key_type(key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key) {\n        Some(ss) => ss,\n        None => return Frame::Array(vec![]),\n    };\n\n    let dir = if reverse {\n        Direction::Desc\n    } else {\n        Direction::Asc\n    };\n    let elems = ss.by_score(dir);\n    let (rs, re) = redis_range(elems.len(), min, max);\n\n    let mut result = Vec::new();\n    for e in &elems[rs..re] {\n        result.push(Frame::Bulk(e.member.clone().into()));\n        if with_scores {\n            result.push(Frame::Bulk(write_float(e.score).into()));\n        }\n    }\n    Frame::Array(result)\n}\n\n#[allow(clippy::too_many_arguments)]\nfn run_range_by_score(\n    db: &crate::db::RedisDB,\n    key: &str,\n    min_s: &str,\n    max_s: &str,\n    reverse: bool,\n    with_limit: bool,\n    offset_s: &str,\n    count_s: &str,\n    with_scores: bool,\n) -> Frame {\n    let mut limit_offset = 0i64;\n    let mut limit_count = -1i64;\n\n    if with_limit {\n        limit_offset = match offset_s.parse() {\n            Ok(n) => n,\n            Err(_) => return Frame::error(MSG_INVALID_INT),\n        };\n        limit_count = match count_s.parse() {\n            Ok(n) => n,\n            Err(_) => return Frame::error(MSG_INVALID_INT),\n        };\n    }\n\n    let (min, min_incl) = match parse_float_range(min_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_MIN_MAX),\n    };\n    let (max, max_incl) = match parse_float_range(max_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_MIN_MAX),\n    };\n\n    if !db.keys.contains_key(key) {\n        return Frame::Array(vec![]);\n    }\n    if let Some(t) = db.key_type(key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key) {\n        Some(ss) => ss,\n        None => return Frame::Array(vec![]),\n    };\n\n    let elems = ss.by_score(Direction::Asc);\n\n    // For reverse, swap min/max and their inclusiveness\n    let (fmin, fmin_incl, fmax, fmax_incl) = if reverse {\n        (max, max_incl, min, min_incl)\n    } else {\n        (min, min_incl, max, max_incl)\n    };\n\n    let mut filtered = with_ss_range(elems, fmin, fmin_incl, fmax, fmax_incl);\n    if reverse {\n        filtered.reverse();\n    }\n\n    // Apply LIMIT\n    if with_limit {\n        if limit_offset < 0 {\n            filtered = Vec::new();\n        } else {\n            let offset = limit_offset as usize;\n            if offset < filtered.len() {\n                filtered = filtered[offset..].to_vec();\n            } else {\n                filtered = Vec::new();\n            }\n            if limit_count >= 0 {\n                let count = limit_count as usize;\n                if filtered.len() > count {\n                    filtered.truncate(count);\n                }\n            }\n        }\n    }\n\n    let mut result = Vec::new();\n    for e in &filtered {\n        result.push(Frame::Bulk(e.member.clone().into()));\n        if with_scores {\n            result.push(Frame::Bulk(write_float(e.score).into()));\n        }\n    }\n    Frame::Array(result)\n}\n\n#[allow(clippy::too_many_arguments)]\nfn run_range_by_lex(\n    db: &crate::db::RedisDB,\n    key: &str,\n    min_s: &str,\n    max_s: &str,\n    reverse: bool,\n    with_limit: bool,\n    offset_s: &str,\n    count_s: &str,\n) -> Frame {\n    let mut limit_offset = 0i64;\n    let mut limit_count = -1i64;\n\n    if with_limit {\n        limit_offset = match offset_s.parse() {\n            Ok(n) => n,\n            Err(_) => return Frame::error(MSG_INVALID_INT),\n        };\n        limit_count = match count_s.parse() {\n            Ok(n) => n,\n            Err(_) => return Frame::error(MSG_INVALID_INT),\n        };\n    }\n\n    let (min, min_incl) = match parse_lex_range(min_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM),\n    };\n    let (max, max_incl) = match parse_lex_range(max_s) {\n        Ok(v) => v,\n        Err(_) => return Frame::error(MSG_INVALID_RANGE_ITEM),\n    };\n\n    if !db.keys.contains_key(key) {\n        return Frame::Array(vec![]);\n    }\n    if let Some(t) = db.key_type(key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key) {\n        Some(ss) => ss,\n        None => return Frame::Array(vec![]),\n    };\n\n    let mut members = ss.members_sorted();\n    members.sort();\n\n    // For reverse, swap min/max\n    let (fmin, fmin_incl, fmax, fmax_incl) = if reverse {\n        (max.clone(), max_incl, min.clone(), min_incl)\n    } else {\n        (min, min_incl, max, max_incl)\n    };\n\n    let mut filtered = with_lex_range(members, &fmin, fmin_incl, &fmax, fmax_incl);\n    if reverse {\n        filtered.reverse();\n    }\n\n    // Apply LIMIT\n    if with_limit {\n        if limit_offset < 0 {\n            filtered = Vec::new();\n        } else {\n            let offset = limit_offset as usize;\n            if offset < filtered.len() {\n                filtered = filtered[offset..].to_vec();\n            } else {\n                filtered = Vec::new();\n            }\n            if limit_count >= 0 {\n                let count = limit_count as usize;\n                if filtered.len() > count {\n                    filtered.truncate(count);\n                }\n            }\n        }\n    }\n\n    let result: Vec<Frame> = filtered\n        .into_iter()\n        .map(|m| Frame::Bulk(m.into()))\n        .collect();\n    Frame::Array(result)\n}\n\n// ── ZINTER / ZUNION (without STORE) ──────────────────────────────────\n\n/// Parse args for ZINTER/ZUNION: numkeys key [...] [WEIGHTS ...] [AGGREGATE ...] [WITHSCORES]\nfn zop_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    intersect: bool,\n) -> Frame {\n    let num_keys = match parse_int(&args[0]) {\n        Some(n) if n > 0 => n as usize,\n        Some(_) => {\n            return Frame::error(\n                \"ERR at least 1 input key is needed for 'zinter'/'zunion' command\",\n            );\n        }\n        _ => return Frame::error(MSG_INVALID_INT),\n    };\n\n    if args.len() < 1 + num_keys {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let keys: Vec<String> = args[1..1 + num_keys]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n    let mut rest = &args[1 + num_keys..];\n\n    let mut weights: Vec<f64> = Vec::new();\n    let mut with_weights = false;\n    let mut aggregate = \"sum\".to_string();\n    let mut with_scores = false;\n\n    while !rest.is_empty() {\n        let opt = String::from_utf8_lossy(&rest[0]).to_lowercase();\n        match opt.as_str() {\n            \"weights\" => {\n                if rest.len() < num_keys + 1 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                for i in 0..num_keys {\n                    match parse_float(&rest[i + 1]) {\n                        Some(f) => weights.push(f),\n                        None => return Frame::error(\"ERR weight value is not a float\"),\n                    }\n                }\n                with_weights = true;\n                rest = &rest[num_keys + 1..];\n            }\n            \"aggregate\" => {\n                if rest.len() < 2 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                aggregate = String::from_utf8_lossy(&rest[1]).to_lowercase();\n                match aggregate.as_str() {\n                    \"sum\" | \"min\" | \"max\" => {}\n                    _ => return Frame::error(MSG_SYNTAX_ERROR),\n                }\n                rest = &rest[2..];\n            }\n            \"withscores\" => {\n                with_scores = true;\n                rest = &rest[1..];\n            }\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    }\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    let mut sset: std::collections::HashMap<String, f64> = std::collections::HashMap::new();\n    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();\n\n    for (i, key) in keys.iter().enumerate() {\n        if !db.keys.contains_key(key) {\n            continue;\n        }\n        let key_type = db.key_type(key);\n        let set: std::collections::HashMap<String, f64> = match key_type {\n            Some(crate::types::KeyType::Set) => db\n                .set_keys\n                .get(key)\n                .map(|s| s.iter().map(|m| (m.clone(), 1.0)).collect())\n                .unwrap_or_default(),\n            Some(crate::types::KeyType::SortedSet) => db\n                .sorted_set_keys\n                .get(key)\n                .map(|ss| ss.scores.clone())\n                .unwrap_or_default(),\n            _ => return Frame::error(MSG_WRONG_TYPE),\n        };\n\n        for (member, mut score) in set {\n            if with_weights {\n                score *= weights[i];\n            }\n            *counts.entry(member.clone()).or_insert(0) += 1;\n            let entry = sset.entry(member);\n            match entry {\n                std::collections::hash_map::Entry::Vacant(e) => {\n                    e.insert(score);\n                }\n                std::collections::hash_map::Entry::Occupied(mut e) => {\n                    let old = *e.get();\n                    match aggregate.as_str() {\n                        \"sum\" => *e.get_mut() += score,\n                        \"min\" => {\n                            if score < old {\n                                *e.get_mut() = score;\n                            }\n                        }\n                        \"max\" => {\n                            if score > old {\n                                *e.get_mut() = score;\n                            }\n                        }\n                        _ => {}\n                    }\n                }\n            }\n        }\n    }\n\n    if intersect {\n        sset.retain(|member, _| counts.get(member).copied().unwrap_or(0) == keys.len());\n    }\n\n    // Sort by score, then by member\n    let mut elems: Vec<(String, f64)> = sset.into_iter().collect();\n    elems.sort_by(|a, b| {\n        a.1.partial_cmp(&b.1)\n            .unwrap_or(std::cmp::Ordering::Equal)\n            .then_with(|| a.0.cmp(&b.0))\n    });\n\n    let mut result = Vec::new();\n    for (member, score) in &elems {\n        result.push(Frame::Bulk(member.clone().into()));\n        if with_scores {\n            result.push(Frame::Bulk(write_float(*score).into()));\n        }\n    }\n    Frame::Array(result)\n}\n\n/// ZINTER numkeys key [...] [WEIGHTS ...] [AGGREGATE ...] [WITHSCORES]\nfn cmd_zinter(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zop_impl(state, ctx, args, true)\n}\n\n/// ZUNION numkeys key [...] [WEIGHTS ...] [AGGREGATE ...] [WITHSCORES]\nfn cmd_zunion(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    zop_impl(state, ctx, args, false)\n}\n\n/// ZRANDMEMBER key [count [WITHSCORES]]\nfn cmd_zrandmember(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if args.len() > 3 {\n        return Frame::error(err_wrong_number(\"zrandmember\"));\n    }\n\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut count: i64 = 0;\n    let mut with_count = false;\n    let mut with_scores = false;\n\n    if args.len() >= 2 {\n        match parse_int(&args[1]) {\n            Some(n) => {\n                count = n;\n                with_count = true;\n            }\n            None => return Frame::error(MSG_INVALID_INT),\n        }\n    }\n    if args.len() == 3 {\n        let opt = String::from_utf8_lossy(&args[2]).to_uppercase();\n        if opt == \"WITHSCORES\" {\n            with_scores = true;\n        } else {\n            return Frame::error(MSG_SYNTAX_ERROR);\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(key.as_ref()) {\n        return if with_count {\n            Frame::Array(vec![])\n        } else {\n            Frame::Null\n        };\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != crate::types::KeyType::SortedSet\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let ss = match db.sorted_set_keys.get(key.as_ref()) {\n        Some(ss) => ss,\n        None => {\n            return if with_count {\n                Frame::Array(vec![])\n            } else {\n                Frame::Null\n            };\n        }\n    };\n\n    let mut members = ss.members_sorted();\n    // Collect scores before shuffling (avoids borrow issues with inner.rng)\n    let scores: std::collections::HashMap<String, f64> = members\n        .iter()\n        .map(|m| (m.clone(), ss.get(m).unwrap_or(0.0)))\n        .collect();\n\n    if count < 0 {\n        // Negative count: allow duplicates\n        let abs_count = (-count) as usize;\n        let mut result = Vec::new();\n        for _ in 0..abs_count {\n            let idx = inner.rng.random_range(0..members.len());\n            result.push(Frame::Bulk(members[idx].clone().into()));\n            if with_scores {\n                let score = scores.get(&members[idx]).copied().unwrap_or(0.0);\n                result.push(Frame::Bulk(write_float(score).into()));\n            }\n        }\n        return Frame::Array(result);\n    }\n\n    // Positive count: unique, shuffle\n    members.shuffle(&mut inner.rng);\n    let take = (count as usize).min(members.len());\n\n    if !with_count {\n        return Frame::Bulk(members[0].clone().into());\n    }\n\n    let mut result = Vec::new();\n    for m in &members[..take] {\n        result.push(Frame::Bulk(m.clone().into()));\n        if with_scores {\n            let score = scores.get(m).copied().unwrap_or(0.0);\n            result.push(Frame::Bulk(write_float(score).into()));\n        }\n    }\n    Frame::Array(result)\n}\n"
  },
  {
    "path": "miniredis/src/cmd/stream.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{CommandTable, MSG_WRONG_TYPE, err_wrong_number};\nuse crate::frame::Frame;\nuse crate::types::{KeyType, Stream, format_stream_range_bound};\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"XADD\", cmd_xadd, false, -5);\n    table.add(\"XLEN\", cmd_xlen, true, 2);\n    table.add(\"XRANGE\", cmd_xrange, true, -4);\n    table.add(\"XREVRANGE\", cmd_xrevrange, true, -4);\n    table.add(\"XREAD\", cmd_xread, true, -4);\n    table.add(\"XINFO\", cmd_xinfo, true, -2);\n    table.add(\"XDEL\", cmd_xdel, false, -3);\n    table.add(\"XTRIM\", cmd_xtrim, false, -4);\n    table.add(\"XGROUP\", cmd_xgroup, false, -2);\n    table.add(\"XREADGROUP\", cmd_xreadgroup, false, -7);\n    table.add(\"XACK\", cmd_xack, false, -4);\n    table.add(\"XPENDING\", cmd_xpending, true, -3);\n    table.add(\"XCLAIM\", cmd_xclaim, false, -6);\n    table.add(\"XAUTOCLAIM\", cmd_xautoclaim, false, -6);\n}\n\n/// XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold] id field value [field value ...]\nfn cmd_xadd(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let mut i = 1;\n    let mut nomkstream = false;\n    let mut maxlen: Option<usize> = None;\n    let mut minid: Option<String> = None;\n\n    // Parse options\n    while i < args.len() {\n        let arg = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match arg.as_str() {\n            \"NOMKSTREAM\" => {\n                nomkstream = true;\n                i += 1;\n            }\n            \"MAXLEN\" => {\n                i += 1;\n                if i < args.len() {\n                    let next = String::from_utf8_lossy(&args[i]).to_string();\n                    if next == \"~\" || next == \"=\" {\n                        i += 1;\n                    }\n                }\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(n) if n >= 0 => {\n                        maxlen = Some(n as usize);\n                        i += 1;\n                    }\n                    Ok(_) => {\n                        return Frame::error(\"ERR The MAXLEN argument must be >= 0.\");\n                    }\n                    Err(_) => {\n                        return Frame::error(\"ERR value is not an integer or out of range\");\n                    }\n                }\n            }\n            \"MINID\" => {\n                i += 1;\n                if i < args.len() {\n                    let next = String::from_utf8_lossy(&args[i]).to_string();\n                    if next == \"~\" || next == \"=\" {\n                        i += 1;\n                    }\n                }\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                minid = Some(String::from_utf8_lossy(&args[i]).to_string());\n                i += 1;\n            }\n            _ => break,\n        }\n    }\n\n    if i >= args.len() {\n        return Frame::error(err_wrong_number(\"xadd\"));\n    }\n\n    let id = String::from_utf8_lossy(&args[i]).to_string();\n    i += 1;\n\n    // Remaining args are field-value pairs\n    let remaining = &args[i..];\n    if !remaining.len().is_multiple_of(2) {\n        return Frame::error(err_wrong_number(\"xadd\"));\n    }\n\n    let values: Vec<String> = remaining\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).to_string())\n        .collect();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let ms = now\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap_or_default()\n        .as_millis() as u64;\n    let db = inner.db_mut(ctx.selected_db);\n\n    // Type check\n    if let Some(kt) = db.keys.get(&key) {\n        if *kt != KeyType::Stream {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n    } else if nomkstream {\n        return Frame::Null;\n    }\n\n    db.keys.entry(key.clone()).or_insert(KeyType::Stream);\n    let stream = db.stream_keys.entry(key.clone()).or_default();\n\n    match stream.add(&id, values, ms) {\n        Ok(final_id) => {\n            if let Some(ml) = maxlen {\n                stream.trim_maxlen(ml);\n            }\n            if let Some(mi) = minid {\n                let normalized = Stream::normalize_id(&mi);\n                stream.trim_minid(&normalized);\n            }\n            db.incr_version(&key, now);\n            Frame::Bulk(final_id.into())\n        }\n        Err(e) => Frame::error(e),\n    }\n}\n\n/// XLEN key\nfn cmd_xlen(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(key.as_ref())\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.stream_keys.get(key.as_ref()) {\n        Some(stream) => Frame::Integer(stream.entries.len() as i64),\n        None => Frame::Integer(0),\n    }\n}\n\n/// XRANGE key start end [COUNT count]\nfn cmd_xrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xrange_impl(state, ctx, args, false)\n}\n\n/// XREVRANGE key end start [COUNT count]\nfn cmd_xrevrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    cmd_xrange_impl(state, ctx, args, true)\n}\n\nfn cmd_xrange_impl(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n    reverse: bool,\n) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let arg_start = String::from_utf8_lossy(&args[1]).to_string();\n    let arg_end = String::from_utf8_lossy(&args[2]).to_string();\n\n    let mut count: Option<usize> = None;\n    if args.len() > 3 {\n        if args.len() != 5 {\n            return Frame::error(\"ERR syntax error\");\n        }\n        let opt = String::from_utf8_lossy(&args[3]).to_uppercase();\n        if opt != \"COUNT\" {\n            return Frame::error(\"ERR syntax error\");\n        }\n        match String::from_utf8_lossy(&args[4]).parse::<usize>() {\n            Ok(n) => count = Some(n),\n            Err(_) => {\n                return Frame::error(\"ERR value is not an integer or out of range\");\n            }\n        }\n    }\n\n    let (start, end) = if reverse {\n        let s = match format_stream_range_bound(&arg_end, true) {\n            Ok(s) => s,\n            Err(e) => return Frame::error(e),\n        };\n        let e = match format_stream_range_bound(&arg_start, false) {\n            Ok(e) => e,\n            Err(e) => return Frame::error(e),\n        };\n        (s, e)\n    } else {\n        let s = match format_stream_range_bound(&arg_start, true) {\n            Ok(s) => s,\n            Err(e) => return Frame::error(e),\n        };\n        let e = match format_stream_range_bound(&arg_end, false) {\n            Ok(e) => e,\n            Err(e) => return Frame::error(e),\n        };\n        (s, e)\n    };\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(key.as_ref())\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let stream = match db.stream_keys.get(key.as_ref()) {\n        Some(s) => s,\n        None => return Frame::Array(vec![]),\n    };\n\n    let entries = if reverse {\n        stream.rev_range(&start, &end, count)\n    } else {\n        stream.range(&start, &end, count)\n    };\n\n    Frame::Array(\n        entries\n            .into_iter()\n            .map(|e| {\n                let vals: Vec<Frame> = e\n                    .values\n                    .iter()\n                    .map(|v| Frame::Bulk(v.clone().into()))\n                    .collect();\n                Frame::Array(vec![Frame::Bulk(e.id.clone().into()), Frame::Array(vals)])\n            })\n            .collect(),\n    )\n}\n\n/// XREAD [COUNT count] [BLOCK ms] STREAMS key [key ...] id [id ...]\nfn cmd_xread(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut i = 0;\n    let mut count: Option<usize> = None;\n\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"COUNT\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<usize>() {\n                    Ok(n) => count = Some(n),\n                    Err(_) => {\n                        return Frame::error(\"ERR value is not an integer or out of range\");\n                    }\n                }\n                i += 1;\n            }\n            \"BLOCK\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(n) if n < 0 => {\n                        return Frame::error(\"ERR timeout is negative\");\n                    }\n                    Ok(_) => {} // Accept but don't actually block\n                    Err(_) => {\n                        return Frame::error(\"ERR timeout is not an integer or out of range\");\n                    }\n                }\n                i += 1;\n            }\n            \"STREAMS\" => {\n                i += 1;\n                break;\n            }\n            _ => {\n                return Frame::error(\"ERR syntax error\");\n            }\n        }\n    }\n\n    let remaining = &args[i..];\n    if remaining.is_empty() || !remaining.len().is_multiple_of(2) {\n        return Frame::error(\n            \"ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified.\",\n        );\n    }\n\n    let half = remaining.len() / 2;\n    let keys: Vec<String> = remaining[..half]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).to_string())\n        .collect();\n\n    let inner = state.lock();\n\n    let mut ids = Vec::with_capacity(half);\n    for (idx, a) in remaining[half..].iter().enumerate() {\n        let s = String::from_utf8_lossy(a).to_string();\n        if s == \"$\" {\n            // Get current last ID for this stream\n            let db = inner.db(ctx.selected_db);\n            ids.push(\n                db.stream_keys\n                    .get(&keys[idx])\n                    .map(|stream| stream.last_id().to_string())\n                    .unwrap_or_else(|| \"0-0\".to_string()),\n            );\n        } else {\n            let normalized = Stream::normalize_id(&s);\n            if Stream::parse_id(&normalized).is_err() {\n                return Frame::error(\"ERR Invalid stream ID specified as stream command argument\");\n            }\n            ids.push(normalized);\n        }\n    }\n\n    let db = inner.db(ctx.selected_db);\n    let mut results = Vec::new();\n    let mut has_data = false;\n\n    for (idx, key) in keys.iter().enumerate() {\n        if let Some(kt) = db.keys.get(key)\n            && *kt != KeyType::Stream\n        {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n\n        let entries = match db.stream_keys.get(key) {\n            Some(stream) => {\n                let mut entries = stream.after(&ids[idx]);\n                if let Some(c) = count {\n                    entries.truncate(c);\n                }\n                entries\n            }\n            None => vec![],\n        };\n\n        if entries.is_empty() {\n            continue;\n        }\n\n        has_data = true;\n\n        let entry_frames: Vec<Frame> = entries\n            .into_iter()\n            .map(|e| {\n                let vals: Vec<Frame> = e\n                    .values\n                    .iter()\n                    .map(|v| Frame::Bulk(v.clone().into()))\n                    .collect();\n                Frame::Array(vec![Frame::Bulk(e.id.clone().into()), Frame::Array(vals)])\n            })\n            .collect();\n\n        results.push(Frame::Array(vec![\n            Frame::Bulk(key.clone().into()),\n            Frame::Array(entry_frames),\n        ]));\n    }\n\n    if !has_data {\n        return Frame::NullArray;\n    }\n\n    Frame::Array(results)\n}\n\n/// XDEL key id [id ...]\nfn cmd_xdel(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let ids: Vec<String> = args[1..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).to_string())\n        .collect();\n    let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.stream_keys.get_mut(&key) {\n        Some(stream) => {\n            // Validate all IDs before deleting\n            for id in &id_refs {\n                let normalized = Stream::normalize_id(id);\n                if Stream::parse_id(&normalized).is_err() {\n                    return Frame::error(\n                        \"ERR Invalid stream ID specified as stream command argument\",\n                    );\n                }\n            }\n            let count = stream.del(&id_refs);\n            db.incr_version(&key, now);\n            Frame::Integer(count)\n        }\n        None => {\n            // Non-existing key: return 0 even for invalid IDs\n            Frame::Integer(0)\n        }\n    }\n}\n\n/// XTRIM key MAXLEN|MINID [=|~] threshold [LIMIT count]\nfn cmd_xtrim(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let strategy = String::from_utf8_lossy(&args[1]).to_uppercase();\n\n    if strategy != \"MAXLEN\" && strategy != \"MINID\" {\n        return Frame::error(err_wrong_number(\"xtrim\"));\n    }\n\n    let mut i = 2;\n    let mut approx = false;\n    if i < args.len() {\n        let next = String::from_utf8_lossy(&args[i]).to_string();\n        if next == \"~\" {\n            approx = true;\n            i += 1;\n        } else if next == \"=\" {\n            i += 1;\n        }\n    }\n\n    if i >= args.len() {\n        return Frame::error(err_wrong_number(\"xtrim\"));\n    }\n\n    let threshold = String::from_utf8_lossy(&args[i]).to_string();\n    i += 1;\n\n    // Parse optional LIMIT\n    if i < args.len() {\n        let next = String::from_utf8_lossy(&args[i]).to_uppercase();\n        if next == \"LIMIT\" {\n            if !approx {\n                return Frame::error(\n                    \"ERR syntax error, LIMIT cannot be used without the special ~ flag\",\n                );\n            }\n            i += 1;\n            if i >= args.len() {\n                return Frame::error(\"ERR syntax error\");\n            }\n            // Parse the limit value (we accept it but don't use it for exact behavior)\n            match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                Ok(_) => {\n                    i += 1;\n                }\n                Err(_) => {\n                    return Frame::error(\"ERR value is not an integer or out of range\");\n                }\n            }\n        }\n    }\n\n    if i < args.len() {\n        return Frame::error(\"ERR syntax error\");\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let stream = match db.stream_keys.get_mut(&key) {\n        Some(s) => s,\n        None => return Frame::Integer(0),\n    };\n\n    let count = match strategy.as_str() {\n        \"MAXLEN\" => match threshold.parse::<i64>() {\n            Ok(n) if n >= 0 => stream.trim_maxlen(n as usize),\n            _ => {\n                return Frame::error(\"ERR value is not an integer or out of range\");\n            }\n        },\n        \"MINID\" => {\n            let normalized = Stream::normalize_id(&threshold);\n            stream.trim_minid(&normalized)\n        }\n        _ => {\n            return Frame::error(\"ERR syntax error\");\n        }\n    };\n\n    db.incr_version(&key, now);\n    Frame::Integer(count)\n}\n\n/// XGROUP CREATE/DESTROY/CREATECONSUMER/DELCONSUMER\nfn cmd_xgroup(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"CREATE\" => {\n            if args.len() < 3 {\n                return Frame::error(err_wrong_number(\"xgroup|create\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]).to_string();\n            let group = String::from_utf8_lossy(&args[2]).to_string();\n            let id = if args.len() > 3 {\n                String::from_utf8_lossy(&args[3]).to_string()\n            } else {\n                \"$\".to_string()\n            };\n\n            let mkstream =\n                args.len() > 4 && String::from_utf8_lossy(&args[4]).to_uppercase() == \"MKSTREAM\";\n\n            let mut inner = state.lock();\n            let now = inner.effective_now();\n            let db = inner.db_mut(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(&key) {\n                if *kt != KeyType::Stream {\n                    return Frame::error(MSG_WRONG_TYPE);\n                }\n            } else if !mkstream {\n                return Frame::error(\n                    \"ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.\",\n                );\n            } else {\n                db.keys.insert(key.clone(), KeyType::Stream);\n                db.stream_keys.insert(key.clone(), Stream::new());\n            }\n\n            let stream = db.stream_keys.get_mut(&key).unwrap();\n            match stream.create_group(&group, &id) {\n                Ok(()) => {\n                    db.incr_version(&key, now);\n                    Frame::ok()\n                }\n                Err(e) => Frame::error(e),\n            }\n        }\n        \"DESTROY\" => {\n            if args.len() < 3 {\n                return Frame::error(err_wrong_number(\"xgroup|destroy\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]).to_string();\n            let group = String::from_utf8_lossy(&args[2]).to_string();\n\n            let mut inner = state.lock();\n            let now = inner.effective_now();\n            let db = inner.db_mut(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(&key)\n                && *kt != KeyType::Stream\n            {\n                return Frame::error(MSG_WRONG_TYPE);\n            }\n\n            let stream = match db.stream_keys.get_mut(&key) {\n                Some(s) => s,\n                None => {\n                    return Frame::error(\n                        \"ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.\",\n                    );\n                }\n            };\n\n            if stream.groups.remove(&group).is_some() {\n                db.incr_version(&key, now);\n                Frame::Integer(1)\n            } else {\n                Frame::Integer(0)\n            }\n        }\n        \"CREATECONSUMER\" => {\n            if args.len() < 4 {\n                return Frame::error(err_wrong_number(\"xgroup|createconsumer\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]).to_string();\n            let group_name = String::from_utf8_lossy(&args[2]).to_string();\n            let consumer_name = String::from_utf8_lossy(&args[3]).to_string();\n\n            let mut inner = state.lock();\n            let now = inner.effective_now();\n            let db = inner.db_mut(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(&key)\n                && *kt != KeyType::Stream\n            {\n                return Frame::error(MSG_WRONG_TYPE);\n            }\n\n            let stream = match db.stream_keys.get_mut(&key) {\n                Some(s) => s,\n                None => {\n                    return Frame::error(\"ERR The XGROUP subcommand requires the key to exist.\");\n                }\n            };\n\n            let group = match stream.groups.get_mut(&group_name) {\n                Some(g) => g,\n                None => {\n                    return Frame::error(format!(\n                        \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                        group_name, key\n                    ));\n                }\n            };\n\n            if let std::collections::hash_map::Entry::Vacant(e) =\n                group.consumers.entry(consumer_name)\n            {\n                e.insert(crate::types::StreamConsumer {\n                    num_pending: 0,\n                    last_seen: now,\n                    last_success: now,\n                });\n                Frame::Integer(1)\n            } else {\n                Frame::Integer(0)\n            }\n        }\n        \"DELCONSUMER\" => {\n            if args.len() < 4 {\n                return Frame::error(err_wrong_number(\"xgroup|delconsumer\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]).to_string();\n            let group_name = String::from_utf8_lossy(&args[2]).to_string();\n            let consumer_name = String::from_utf8_lossy(&args[3]).to_string();\n\n            let mut inner = state.lock();\n            let now = inner.effective_now();\n            let db = inner.db_mut(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(&key)\n                && *kt != KeyType::Stream\n            {\n                return Frame::error(MSG_WRONG_TYPE);\n            }\n\n            let stream = match db.stream_keys.get_mut(&key) {\n                Some(s) => s,\n                None => {\n                    return Frame::error(\"ERR The XGROUP subcommand requires the key to exist.\");\n                }\n            };\n\n            let group = match stream.groups.get_mut(&group_name) {\n                Some(g) => g,\n                None => {\n                    return Frame::error(format!(\n                        \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                        group_name, key\n                    ));\n                }\n            };\n\n            let pending_count = group\n                .pending\n                .iter()\n                .filter(|pe| pe.consumer == consumer_name)\n                .count() as i64;\n            group.pending.retain(|pe| pe.consumer != consumer_name);\n            group.consumers.remove(&consumer_name);\n            db.incr_version(&key, now);\n            Frame::Integer(pending_count)\n        }\n        _ => Frame::error(format!(\n            \"ERR unknown subcommand '{}'. Try XGROUP HELP.\",\n            String::from_utf8_lossy(&args[0])\n        )),\n    }\n}\n\n/// XREADGROUP GROUP group consumer [COUNT count] [BLOCK ms] [NOACK] STREAMS key [key ...] id [id ...]\nfn cmd_xreadgroup(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut i = 0;\n    let group_kw = String::from_utf8_lossy(&args[i]).to_uppercase();\n    if group_kw != \"GROUP\" {\n        return Frame::error(\"ERR syntax error\");\n    }\n    i += 1;\n\n    let group_name = String::from_utf8_lossy(&args[i]).to_string();\n    i += 1;\n    let consumer_name = String::from_utf8_lossy(&args[i]).to_string();\n    i += 1;\n\n    let mut count: Option<usize> = None;\n    let mut noack = false;\n\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"COUNT\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(n) if n > 0 => count = Some(n as usize),\n                    Ok(_) => {\n                        // Negative or zero COUNT: treat as unlimited (no count limit)\n                        count = None;\n                    }\n                    Err(_) => {\n                        return Frame::error(\"ERR value is not an integer or out of range\");\n                    }\n                }\n                i += 1;\n            }\n            \"BLOCK\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(n) if n < 0 => {\n                        return Frame::error(\"ERR timeout is negative\");\n                    }\n                    Ok(_) => {} // Accept but don't actually block\n                    Err(_) => {\n                        return Frame::error(\"ERR timeout is not an integer or out of range\");\n                    }\n                }\n                i += 1;\n            }\n            \"NOACK\" => {\n                noack = true;\n                i += 1;\n            }\n            \"STREAMS\" => {\n                i += 1;\n                break;\n            }\n            _ => {\n                return Frame::error(\"ERR syntax error\");\n            }\n        }\n    }\n\n    let remaining = &args[i..];\n    if remaining.is_empty() || !remaining.len().is_multiple_of(2) {\n        return Frame::error(\n            \"ERR Unbalanced XREADGROUP list of streams: for each stream key an ID or '$' must be specified.\",\n        );\n    }\n\n    let half = remaining.len() / 2;\n    let keys: Vec<String> = remaining[..half]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).to_string())\n        .collect();\n\n    // Collect IDs (validation deferred to per-stream loop, after group check)\n    let mut ids = Vec::with_capacity(half);\n    for a in &remaining[half..] {\n        ids.push(String::from_utf8_lossy(a).to_string());\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    let mut results = Vec::new();\n    let mut has_data = false;\n\n    for (idx, key) in keys.iter().enumerate() {\n        if let Some(kt) = db.keys.get(key)\n            && *kt != KeyType::Stream\n        {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n\n        let stream = match db.stream_keys.get_mut(key) {\n            Some(s) => s,\n            None => {\n                return Frame::error(format!(\n                    \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                    group_name, key\n                ));\n            }\n        };\n\n        // Check group exists before ID validation\n        if !stream.groups.contains_key(&group_name) {\n            return Frame::error(format!(\n                \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                group_name, key\n            ));\n        }\n\n        // Validate non-\">\" IDs after confirming the group exists\n        if ids[idx] != \">\" && ids[idx] != \"$\" {\n            let normalized = Stream::normalize_id(&ids[idx]);\n            if Stream::parse_id(&normalized).is_err() {\n                return Frame::error(\"ERR Invalid stream ID specified as stream command argument\");\n            }\n        }\n\n        let entries =\n            match stream.read_group(&group_name, &consumer_name, &ids[idx], count, noack, now) {\n                Ok(entries) => entries,\n                Err(e) => return Frame::error(e),\n            };\n\n        // For \">\" IDs, omit streams with no new entries from results\n        if entries.is_empty() && ids[idx] == \">\" {\n            continue;\n        }\n\n        if !entries.is_empty() {\n            has_data = true;\n        }\n\n        let entry_frames: Vec<Frame> = entries\n            .into_iter()\n            .map(|e| {\n                let vals: Vec<Frame> = e\n                    .values\n                    .iter()\n                    .map(|v| Frame::Bulk(v.clone().into()))\n                    .collect();\n                Frame::Array(vec![Frame::Bulk(e.id.into()), Frame::Array(vals)])\n            })\n            .collect();\n\n        results.push(Frame::Array(vec![\n            Frame::Bulk(key.clone().into()),\n            Frame::Array(entry_frames),\n        ]));\n    }\n\n    if !has_data && ids.iter().all(|id| id == \">\") {\n        return Frame::NullArray;\n    }\n\n    Frame::Array(results)\n}\n\n/// XACK key group id [id ...]\nfn cmd_xack(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let group_name = String::from_utf8_lossy(&args[1]).to_string();\n    let ids: Vec<String> = args[2..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).to_string())\n        .collect();\n\n    // Validate all IDs\n    for id in &ids {\n        let normalized = Stream::normalize_id(id);\n        if Stream::parse_id(&normalized).is_err() {\n            return Frame::error(\"ERR Invalid stream ID specified as stream command argument\");\n        }\n    }\n\n    let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let stream = match db.stream_keys.get_mut(&key) {\n        Some(s) => s,\n        None => return Frame::Integer(0),\n    };\n\n    match stream.ack(&group_name, &id_refs) {\n        Ok(count) => Frame::Integer(count),\n        Err(e) => Frame::error(e),\n    }\n}\n\n/// XPENDING key group [[IDLE ms] start end count [consumer]]\nfn cmd_xpending(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let group_name = String::from_utf8_lossy(&args[1]).to_string();\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let stream = match db.stream_keys.get(&key) {\n        Some(s) => s,\n        None => {\n            return Frame::error(format!(\n                \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                group_name, key\n            ));\n        }\n    };\n\n    let group = match stream.groups.get(&group_name) {\n        Some(g) => g,\n        None => {\n            return Frame::error(format!(\n                \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                group_name, key\n            ));\n        }\n    };\n\n    if args.len() == 2 {\n        // Summary mode\n        let active: Vec<&crate::types::PendingEntry> = group\n            .pending\n            .iter()\n            .filter(|pe| stream.entries.iter().any(|e| e.id == pe.id))\n            .collect();\n\n        if active.is_empty() {\n            return Frame::Array(vec![\n                Frame::Integer(0),\n                Frame::Null,\n                Frame::Null,\n                Frame::NullArray,\n            ]);\n        }\n\n        let min_id = active\n            .iter()\n            .map(|pe| &pe.id)\n            .min_by(|a, b| Stream::cmp_ids(a, b))\n            .unwrap();\n        let max_id = active\n            .iter()\n            .map(|pe| &pe.id)\n            .max_by(|a, b| Stream::cmp_ids(a, b))\n            .unwrap();\n\n        // Count per consumer\n        let mut consumer_counts: std::collections::HashMap<&str, i64> =\n            std::collections::HashMap::new();\n        for pe in &active {\n            *consumer_counts.entry(&pe.consumer).or_insert(0) += 1;\n        }\n        let mut consumers: Vec<Frame> = consumer_counts\n            .iter()\n            .map(|(name, count)| {\n                Frame::Array(vec![\n                    Frame::Bulk(name.to_string().into()),\n                    Frame::Bulk(count.to_string().into()),\n                ])\n            })\n            .collect();\n        consumers.sort_by(|a, b| {\n            if let (Frame::Array(a), Frame::Array(b)) = (a, b)\n                && let (Frame::Bulk(a), Frame::Bulk(b)) = (&a[0], &b[0])\n            {\n                return a.cmp(b);\n            }\n            std::cmp::Ordering::Equal\n        });\n\n        return Frame::Array(vec![\n            Frame::Integer(active.len() as i64),\n            Frame::Bulk(min_id.clone().into()),\n            Frame::Bulk(max_id.clone().into()),\n            Frame::Array(consumers),\n        ]);\n    }\n\n    // Detail mode: XPENDING key group [IDLE ms] start end count [consumer]\n    let mut i = 2;\n    let mut idle_filter: Option<u64> = None;\n\n    if i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        if opt == \"IDLE\" {\n            i += 1;\n            if i >= args.len() {\n                return Frame::error(\"ERR syntax error\");\n            }\n            match String::from_utf8_lossy(&args[i]).parse::<u64>() {\n                Ok(n) => idle_filter = Some(n),\n                Err(_) => {\n                    return Frame::error(\"ERR value is not an integer or out of range\");\n                }\n            }\n            i += 1;\n        }\n    }\n\n    if i + 3 > args.len() {\n        return Frame::error(\"ERR syntax error\");\n    }\n\n    let start = match format_stream_range_bound(&String::from_utf8_lossy(&args[i]), true) {\n        Ok(s) => s,\n        Err(e) => return Frame::error(e),\n    };\n    let end = match format_stream_range_bound(&String::from_utf8_lossy(&args[i + 1]), false) {\n        Ok(e) => e,\n        Err(e) => return Frame::error(e),\n    };\n    let count_val = match String::from_utf8_lossy(&args[i + 2]).parse::<i64>() {\n        Ok(n) => n,\n        Err(_) => {\n            return Frame::error(\"ERR value is not an integer or out of range\");\n        }\n    };\n\n    let consumer_filter = if i + 3 < args.len() {\n        Some(String::from_utf8_lossy(&args[i + 3]).to_string())\n    } else {\n        None\n    };\n\n    if count_val <= 0 {\n        return Frame::Array(vec![]);\n    }\n\n    let now = inner.effective_now();\n    let mut result = Vec::new();\n\n    for pe in &group.pending {\n        if !stream.entries.iter().any(|e| e.id == pe.id) {\n            continue;\n        }\n        if Stream::cmp_ids(&pe.id, &start) == std::cmp::Ordering::Less {\n            continue;\n        }\n        if Stream::cmp_ids(&pe.id, &end) == std::cmp::Ordering::Greater {\n            continue;\n        }\n        if let Some(consumer) = &consumer_filter\n            && pe.consumer != *consumer\n        {\n            continue;\n        }\n        let idle_ms = now\n            .duration_since(pe.last_delivery)\n            .unwrap_or_default()\n            .as_millis() as u64;\n        if let Some(min_idle) = idle_filter\n            && idle_ms < min_idle\n        {\n            continue;\n        }\n\n        result.push(Frame::Array(vec![\n            Frame::Bulk(pe.id.clone().into()),\n            Frame::Bulk(pe.consumer.clone().into()),\n            Frame::Integer(idle_ms as i64),\n            Frame::Integer(pe.delivery_count),\n        ]));\n\n        if result.len() >= count_val as usize {\n            break;\n        }\n    }\n\n    Frame::Array(result)\n}\n\n/// XCLAIM key group consumer min-idle-ms id [id ...] [IDLE ms] [TIME ms] [RETRYCOUNT count] [FORCE] [JUSTID]\nfn cmd_xclaim(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let group_name = String::from_utf8_lossy(&args[1]).to_string();\n    let consumer_name = String::from_utf8_lossy(&args[2]).to_string();\n    let _min_idle_ms = match String::from_utf8_lossy(&args[3]).parse::<u64>() {\n        Ok(n) => n,\n        Err(_) => {\n            return Frame::error(\"ERR Invalid min-idle-time argument for XCLAIM\");\n        }\n    };\n\n    let mut ids = Vec::new();\n    let mut justid = false;\n    let mut force = false;\n    let mut in_options = false;\n    let mut i = 4;\n\n    while i < args.len() {\n        let arg = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match arg.as_str() {\n            \"JUSTID\" => {\n                in_options = true;\n                justid = true;\n                i += 1;\n            }\n            \"FORCE\" => {\n                in_options = true;\n                force = true;\n                i += 1;\n            }\n            \"IDLE\" => {\n                in_options = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(_) => {}\n                    Err(_) => {\n                        return Frame::error(\"ERR Invalid IDLE option argument for XCLAIM\");\n                    }\n                }\n                i += 1;\n            }\n            \"TIME\" => {\n                in_options = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(_) => {}\n                    Err(_) => {\n                        return Frame::error(\"ERR Invalid TIME option argument for XCLAIM\");\n                    }\n                }\n                i += 1;\n            }\n            \"RETRYCOUNT\" => {\n                in_options = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                    Ok(_) => {}\n                    Err(_) => {\n                        return Frame::error(\"ERR Invalid RETRYCOUNT option argument for XCLAIM\");\n                    }\n                }\n                i += 1;\n            }\n            _ => {\n                if in_options {\n                    return Frame::error(format!(\n                        \"ERR Unrecognized XCLAIM option '{}'\",\n                        String::from_utf8_lossy(&args[i])\n                    ));\n                }\n                ids.push(String::from_utf8_lossy(&args[i]).to_string());\n                i += 1;\n            }\n        }\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let stream = match db.stream_keys.get_mut(&key) {\n        Some(s) => s,\n        None => {\n            return Frame::error(format!(\n                \"NOGROUP No such key '{}' or consumer group '{}' in XCLAIM for key name '{}'\",\n                key, group_name, key\n            ));\n        }\n    };\n\n    let group = match stream.groups.get_mut(&group_name) {\n        Some(g) => g,\n        None => {\n            return Frame::error(format!(\n                \"NOGROUP No such key '{}' or consumer group '{}' in XCLAIM for key name '{}'\",\n                key, group_name, key\n            ));\n        }\n    };\n\n    // Ensure consumer exists\n    group\n        .consumers\n        .entry(consumer_name.clone())\n        .or_insert(crate::types::StreamConsumer {\n            num_pending: 0,\n            last_seen: now,\n            last_success: now,\n        });\n\n    let mut claimed = Vec::new();\n    for id in &ids {\n        let entry_exists = stream.entries.iter().any(|e| e.id == *id);\n        let in_pel = group.pending.iter().any(|pe| pe.id == *id);\n\n        if !entry_exists && !force && !in_pel {\n            // Entry doesn't exist, not forced, and not in PEL: skip\n            continue;\n        }\n\n        if !entry_exists && in_pel && !force {\n            // Entry was deleted but is still in PEL: remove from PEL\n            let consumer_name_of_pe = group\n                .pending\n                .iter()\n                .find(|pe| pe.id == *id)\n                .map(|pe| pe.consumer.clone());\n            group.pending.retain(|pe| pe.id != *id);\n            if let Some(cname) = consumer_name_of_pe\n                && let Some(c) = group.consumers.get_mut(&cname)\n            {\n                c.num_pending -= 1;\n            }\n            continue;\n        }\n\n        if !entry_exists && !force {\n            continue;\n        }\n\n        // Find in pending or create if force\n        let found = group.pending.iter_mut().find(|pe| pe.id == *id);\n        match found {\n            Some(pe) => {\n                // Transfer to new consumer\n                let old_consumer = pe.consumer.clone();\n                pe.consumer = consumer_name.clone();\n                pe.delivery_count += 1;\n                pe.last_delivery = now;\n\n                // Update consumer pending counts\n                if let Some(c) = group.consumers.get_mut(&old_consumer) {\n                    c.num_pending -= 1;\n                }\n                if let Some(c) = group.consumers.get_mut(&consumer_name) {\n                    c.num_pending += 1;\n                }\n            }\n            None => {\n                if force {\n                    group.pending.push(crate::types::PendingEntry {\n                        id: id.clone(),\n                        consumer: consumer_name.clone(),\n                        delivery_count: 1,\n                        last_delivery: now,\n                    });\n                    if let Some(c) = group.consumers.get_mut(&consumer_name) {\n                        c.num_pending += 1;\n                    }\n                } else {\n                    continue;\n                }\n            }\n        }\n\n        if justid {\n            claimed.push(Frame::Bulk(id.clone().into()));\n        } else if let Some(entry) = stream.entries.iter().find(|e| e.id == *id) {\n            let vals: Vec<Frame> = entry\n                .values\n                .iter()\n                .map(|v| Frame::Bulk(v.clone().into()))\n                .collect();\n            claimed.push(Frame::Array(vec![\n                Frame::Bulk(entry.id.clone().into()),\n                Frame::Array(vals),\n            ]));\n        }\n    }\n\n    Frame::Array(claimed)\n}\n\n/// XAUTOCLAIM key group consumer min-idle-ms start [COUNT count] [JUSTID]\nfn cmd_xautoclaim(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).to_string();\n    let group_name = String::from_utf8_lossy(&args[1]).to_string();\n    let consumer_name = String::from_utf8_lossy(&args[2]).to_string();\n    let min_idle_ms = match String::from_utf8_lossy(&args[3]).parse::<u64>() {\n        Ok(n) => n,\n        Err(_) => {\n            return Frame::error(\"ERR Invalid min-idle-time argument for XAUTOCLAIM\");\n        }\n    };\n    let start = String::from_utf8_lossy(&args[4]).to_string();\n    let start_id = Stream::normalize_id(&start);\n    // Validate the start ID\n    if Stream::parse_id(&start_id).is_err() {\n        return Frame::error(\"ERR Invalid stream ID specified as stream command argument\");\n    }\n\n    let mut count: usize = 100;\n    let mut justid = false;\n\n    let mut i = 5;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"COUNT\" => {\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(\"ERR syntax error\");\n                }\n                match String::from_utf8_lossy(&args[i]).parse::<usize>() {\n                    Ok(n) => count = n,\n                    Err(_) => {\n                        return Frame::error(\"ERR value is not an integer or out of range\");\n                    }\n                }\n                i += 1;\n            }\n            \"JUSTID\" => {\n                justid = true;\n                i += 1;\n            }\n            _ => {\n                return Frame::error(\"ERR syntax error\");\n            }\n        }\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    if let Some(kt) = db.keys.get(&key)\n        && *kt != KeyType::Stream\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let stream = match db.stream_keys.get_mut(&key) {\n        Some(s) => s,\n        None => {\n            return Frame::error(format!(\n                \"NOGROUP No such key '{}' or consumer group '{}' in XAUTOCLAIM for key name '{}'\",\n                key, group_name, key\n            ));\n        }\n    };\n\n    let group = match stream.groups.get_mut(&group_name) {\n        Some(g) => g,\n        None => {\n            return Frame::error(format!(\n                \"NOGROUP No such key '{}' or consumer group '{}' in XAUTOCLAIM for key name '{}'\",\n                key, group_name, key\n            ));\n        }\n    };\n\n    // Ensure consumer exists\n    group\n        .consumers\n        .entry(consumer_name.clone())\n        .or_insert(crate::types::StreamConsumer {\n            num_pending: 0,\n            last_seen: now,\n            last_success: now,\n        });\n\n    let mut claimed = Vec::new();\n    let mut last_claimed_id: Option<String> = None;\n    let mut hit_count_limit = false;\n\n    for pe in group.pending.iter_mut() {\n        if Stream::cmp_ids(&pe.id, &start_id) == std::cmp::Ordering::Less {\n            continue;\n        }\n\n        let idle_ms = now\n            .duration_since(pe.last_delivery)\n            .unwrap_or_default()\n            .as_millis() as u64;\n        if idle_ms < min_idle_ms {\n            continue;\n        }\n\n        if !stream.entries.iter().any(|e| e.id == pe.id) {\n            continue;\n        }\n\n        // Claim this entry\n        let old_consumer = pe.consumer.clone();\n        pe.consumer = consumer_name.clone();\n        pe.delivery_count += 1;\n        pe.last_delivery = now;\n\n        if let Some(c) = group.consumers.get_mut(&old_consumer) {\n            c.num_pending -= 1;\n        }\n        if let Some(c) = group.consumers.get_mut(&consumer_name) {\n            c.num_pending += 1;\n        }\n\n        if justid {\n            claimed.push(Frame::Bulk(pe.id.clone().into()));\n        } else if let Some(entry) = stream.entries.iter().find(|e| e.id == pe.id) {\n            let vals: Vec<Frame> = entry\n                .values\n                .iter()\n                .map(|v| Frame::Bulk(v.clone().into()))\n                .collect();\n            claimed.push(Frame::Array(vec![\n                Frame::Bulk(entry.id.clone().into()),\n                Frame::Array(vals),\n            ]));\n        }\n\n        last_claimed_id = Some(pe.id.clone());\n\n        if claimed.len() >= count {\n            hit_count_limit = true;\n            break;\n        }\n    }\n\n    // Compute next_id: only return a non-zero cursor if we stopped early due to COUNT limit.\n    // If we scanned all eligible entries, return \"0-0\".\n    let next_id = if hit_count_limit {\n        match last_claimed_id {\n            Some(id) => {\n                if let Ok((ms, seq)) = Stream::parse_id(&id) {\n                    Stream::format_id(ms, seq + 1)\n                } else {\n                    \"0-0\".to_string()\n                }\n            }\n            None => \"0-0\".to_string(),\n        }\n    } else {\n        \"0-0\".to_string()\n    };\n\n    Frame::Array(vec![\n        Frame::Bulk(next_id.into()),\n        Frame::Array(claimed),\n        Frame::Array(vec![]), // deleted entries (not implemented)\n    ])\n}\n\n/// XINFO STREAM/GROUPS/CONSUMERS\nfn cmd_xinfo(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let subcmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    match subcmd.as_str() {\n        \"STREAM\" => {\n            if args.len() < 2 {\n                return Frame::error(err_wrong_number(\"xinfo|stream\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(key.as_ref())\n                && *kt != KeyType::Stream\n            {\n                return Frame::error(MSG_WRONG_TYPE);\n            }\n\n            let stream = match db.stream_keys.get(key.as_ref()) {\n                Some(s) => s,\n                None => return Frame::error(\"ERR no such key\"),\n            };\n\n            Frame::Array(vec![\n                Frame::Bulk(\"length\".into()),\n                Frame::Integer(stream.entries.len() as i64),\n                Frame::Bulk(\"groups\".into()),\n                Frame::Integer(stream.groups.len() as i64),\n                Frame::Bulk(\"last-generated-id\".into()),\n                Frame::Bulk(stream.last_id().to_string().into()),\n            ])\n        }\n        \"GROUPS\" => {\n            if args.len() < 2 {\n                return Frame::error(err_wrong_number(\"xinfo|groups\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let inner = state.lock();\n            let db = inner.db(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(key.as_ref())\n                && *kt != KeyType::Stream\n            {\n                return Frame::error(MSG_WRONG_TYPE);\n            }\n\n            let stream = match db.stream_keys.get(key.as_ref()) {\n                Some(s) => s,\n                None => return Frame::error(\"ERR no such key\"),\n            };\n\n            let mut groups: Vec<Frame> = stream\n                .groups\n                .iter()\n                .map(|(name, group)| {\n                    // Compute entries-read and lag\n                    let (entries_read, lag) = compute_entries_read_lag(stream, group);\n                    Frame::Array(vec![\n                        Frame::Bulk(\"name\".into()),\n                        Frame::Bulk(name.clone().into()),\n                        Frame::Bulk(\"consumers\".into()),\n                        Frame::Integer(group.consumers.len() as i64),\n                        Frame::Bulk(\"pending\".into()),\n                        Frame::Integer(group.pending.len() as i64),\n                        Frame::Bulk(\"last-delivered-id\".into()),\n                        Frame::Bulk(group.last_id.clone().into()),\n                        Frame::Bulk(\"entries-read\".into()),\n                        entries_read,\n                        Frame::Bulk(\"lag\".into()),\n                        lag,\n                    ])\n                })\n                .collect();\n            groups.sort_by(|a, b| {\n                if let (Frame::Array(a), Frame::Array(b)) = (a, b)\n                    && let (Frame::Bulk(a), Frame::Bulk(b)) = (&a[1], &b[1])\n                {\n                    return a.cmp(b);\n                }\n                std::cmp::Ordering::Equal\n            });\n\n            Frame::Array(groups)\n        }\n        \"CONSUMERS\" => {\n            if args.len() < 3 {\n                return Frame::error(err_wrong_number(\"xinfo|consumers\"));\n            }\n            let key = String::from_utf8_lossy(&args[1]);\n            let group_name = String::from_utf8_lossy(&args[2]);\n            let inner = state.lock();\n            let now = inner.effective_now();\n            let db = inner.db(ctx.selected_db);\n\n            if let Some(kt) = db.keys.get(key.as_ref())\n                && *kt != KeyType::Stream\n            {\n                return Frame::error(MSG_WRONG_TYPE);\n            }\n\n            let stream = match db.stream_keys.get(key.as_ref()) {\n                Some(s) => s,\n                None => return Frame::error(\"ERR no such key\"),\n            };\n\n            let group = match stream.groups.get(group_name.as_ref()) {\n                Some(g) => g,\n                None => {\n                    return Frame::error(format!(\n                        \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                        group_name, key\n                    ));\n                }\n            };\n\n            let consumers: Vec<Frame> = group\n                .consumers\n                .iter()\n                .map(|(name, consumer)| {\n                    let idle = now\n                        .duration_since(consumer.last_seen)\n                        .unwrap_or_default()\n                        .as_millis() as i64;\n                    let inactive = now\n                        .duration_since(consumer.last_success)\n                        .unwrap_or_default()\n                        .as_millis() as i64;\n                    Frame::Array(vec![\n                        Frame::Bulk(\"name\".into()),\n                        Frame::Bulk(name.clone().into()),\n                        Frame::Bulk(\"pending\".into()),\n                        Frame::Integer(consumer.num_pending),\n                        Frame::Bulk(\"idle\".into()),\n                        Frame::Integer(idle),\n                        Frame::Bulk(\"inactive\".into()),\n                        Frame::Integer(inactive),\n                    ])\n                })\n                .collect();\n\n            Frame::Array(consumers)\n        }\n        _ => Frame::error(\n            \"ERR unknown subcommand or wrong number of arguments for 'XINFO' command\".to_string(),\n        ),\n    }\n}\n\n/// Compute `entries-read` and `lag` for XINFO GROUPS output.\nfn compute_entries_read_lag(stream: &Stream, group: &crate::types::StreamGroup) -> (Frame, Frame) {\n    // If last_id is \"0-0\", the group has never delivered anything.\n    if group.last_id == \"0-0\" {\n        return (Frame::Null, Frame::Integer(stream.entries.len() as i64));\n    }\n\n    // If entries_read_known is false (group was created with $ or a specific ID\n    // but never actually delivered entries), return nil for entries-read.\n    if !group.entries_read_known {\n        // We still know the lag: number of entries after the group's last_id.\n        let entries_after = stream\n            .entries\n            .iter()\n            .filter(|e| Stream::cmp_ids(&e.id, &group.last_id) == std::cmp::Ordering::Greater)\n            .count() as i64;\n        return (Frame::Null, Frame::Integer(entries_after));\n    }\n\n    // entries-read: number of entries with id <= group.last_id.\n    // Find the position of the first entry after last_id.\n    let pos = stream\n        .entries\n        .iter()\n        .position(|e| Stream::cmp_ids(&e.id, &group.last_id) == std::cmp::Ordering::Greater);\n\n    let entries_read = match pos {\n        Some(p) => p as i64,\n        None => stream.entries.len() as i64, // last_id >= all entries\n    };\n\n    let lag = stream.entries.len() as i64 - entries_read;\n\n    (Frame::Integer(entries_read), Frame::Integer(lag))\n}\n"
  },
  {
    "path": "miniredis/src/cmd/string.rs",
    "content": "use std::sync::Arc;\nuse std::time::Duration;\n\nuse super::parse_int;\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::{\n    CommandTable, MSG_INT_OVERFLOW, MSG_INVALID_FLOAT, MSG_INVALID_INT, MSG_INVALID_PSETEX_TIME,\n    MSG_INVALID_SE_TIME, MSG_INVALID_SETEX_TIME, MSG_SYNTAX_ERROR, MSG_WRONG_TYPE, MSG_XX_AND_NX,\n    err_wrong_number,\n};\nuse crate::frame::Frame;\nuse crate::types::KeyType;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"GET\", cmd_get, true, 2);\n    table.add(\"SET\", cmd_set, false, -3);\n    table.add(\"SETNX\", cmd_setnx, false, 3);\n    table.add(\"GETSET\", cmd_getset, false, 3);\n    table.add(\"SETEX\", cmd_setex, false, 4);\n    table.add(\"PSETEX\", cmd_psetex, false, 4);\n    table.add(\"MGET\", cmd_mget, true, -2);\n    table.add(\"MSET\", cmd_mset, false, -3);\n    table.add(\"MSETNX\", cmd_msetnx, false, -3);\n    table.add(\"INCR\", cmd_incr, false, 2);\n    table.add(\"INCRBY\", cmd_incrby, false, 3);\n    table.add(\"INCRBYFLOAT\", cmd_incrbyfloat, false, 3);\n    table.add(\"DECR\", cmd_decr, false, 2);\n    table.add(\"DECRBY\", cmd_decrby, false, 3);\n    table.add(\"STRLEN\", cmd_strlen, true, 2);\n    table.add(\"APPEND\", cmd_append, false, 3);\n    table.add(\"GETRANGE\", cmd_getrange, true, 4);\n    table.add(\"SUBSTR\", cmd_getrange, true, 4); // alias\n    table.add(\"SETRANGE\", cmd_setrange, false, 4);\n    table.add(\"GETDEL\", cmd_getdel, false, 2);\n    table.add(\"GETEX\", cmd_getex, false, -2);\n    table.add(\"GETBIT\", cmd_getbit, true, 3);\n    table.add(\"SETBIT\", cmd_setbit, false, 4);\n    table.add(\"BITCOUNT\", cmd_bitcount, true, -2);\n    table.add(\"BITOP\", cmd_bitop, false, -4);\n    table.add(\"BITPOS\", cmd_bitpos, true, -3);\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nfn string_incr(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    key: &str,\n    delta: i64,\n) -> Result<i64, Frame> {\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(key);\n\n    // Check type\n    if let Some(t) = db.key_type(key)\n        && t != KeyType::String\n    {\n        return Err(Frame::error(MSG_WRONG_TYPE));\n    }\n\n    let current: i64 = match db.string_get(key) {\n        Some(v) => match String::from_utf8_lossy(v).parse::<i64>() {\n            Ok(n) => n,\n            Err(_) => return Err(Frame::error(MSG_INVALID_INT)),\n        },\n        None => 0,\n    };\n\n    let new_val = match current.checked_add(delta) {\n        Some(n) => n,\n        None => return Err(Frame::error(MSG_INT_OVERFLOW)),\n    };\n\n    db.string_set(key, new_val.to_string().into_bytes(), now);\n    Ok(new_val)\n}\n\n// ── Commands ─────────────────────────────────────────────────────────\n\n/// GET key\nfn cmd_get(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.string_get(&key) {\n        Some(val) => Frame::Bulk(val.clone().into()),\n        None => Frame::Null,\n    }\n}\n\n/// SET key value [EX seconds] [PX milliseconds] [NX|XX] [KEEPTTL] [GET]\nfn cmd_set(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let value = args[1].clone();\n\n    let mut ex: Option<Duration> = None;\n    let mut nx = false;\n    let mut xx = false;\n    let mut keepttl = false;\n    let mut get = false;\n    let mut expire_opt_set = false; // Track if any EX/PX/EXAT/PXAT was already set\n\n    let mut i = 2;\n    while i < args.len() {\n        let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n        match opt.as_str() {\n            \"EX\" => {\n                if expire_opt_set {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                expire_opt_set = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let secs: i64 = match String::from_utf8_lossy(&args[i]).parse() {\n                    Ok(n) => n,\n                    Err(_) => return Frame::error(MSG_INVALID_INT),\n                };\n                if secs <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                ex = Some(Duration::from_secs(secs as u64));\n            }\n            \"PX\" => {\n                if expire_opt_set {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                expire_opt_set = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let ms: i64 = match String::from_utf8_lossy(&args[i]).parse() {\n                    Ok(n) => n,\n                    Err(_) => return Frame::error(MSG_INVALID_INT),\n                };\n                if ms <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                ex = Some(Duration::from_millis(ms as u64));\n            }\n            \"EXAT\" => {\n                if expire_opt_set {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                expire_opt_set = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let ts: i64 = match String::from_utf8_lossy(&args[i]).parse() {\n                    Ok(n) => n,\n                    Err(_) => return Frame::error(MSG_INVALID_INT),\n                };\n                if ts <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                let inner = state.lock();\n                let now = inner.effective_now();\n                let target = std::time::UNIX_EPOCH + Duration::from_secs(ts as u64);\n                match target.duration_since(now) {\n                    Ok(d) => ex = Some(d),\n                    Err(_) => ex = Some(Duration::ZERO),\n                }\n                drop(inner);\n            }\n            \"PXAT\" => {\n                if expire_opt_set {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                expire_opt_set = true;\n                i += 1;\n                if i >= args.len() {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let ts: i64 = match String::from_utf8_lossy(&args[i]).parse() {\n                    Ok(n) => n,\n                    Err(_) => return Frame::error(MSG_INVALID_INT),\n                };\n                if ts <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                let inner = state.lock();\n                let now = inner.effective_now();\n                let target = std::time::UNIX_EPOCH + Duration::from_millis(ts as u64);\n                match target.duration_since(now) {\n                    Ok(d) => ex = Some(d),\n                    Err(_) => ex = Some(Duration::ZERO),\n                }\n                drop(inner);\n            }\n            \"NX\" => nx = true,\n            \"XX\" => xx = true,\n            \"KEEPTTL\" => keepttl = true,\n            \"GET\" => get = true,\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n        i += 1;\n    }\n\n    if nx && xx {\n        return Frame::error(MSG_XX_AND_NX);\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    let old_value = if get {\n        match db.key_type(&key) {\n            Some(KeyType::String) => db.string_get(&key).map(|v| Frame::Bulk(v.clone().into())),\n            Some(_) => return Frame::error(MSG_WRONG_TYPE),\n            None => Some(Frame::Null),\n        }\n    } else {\n        None\n    };\n\n    let key_exists = db.keys.contains_key(&key);\n\n    if nx && key_exists {\n        return old_value.unwrap_or(Frame::Null);\n    }\n    if xx && !key_exists {\n        return old_value.unwrap_or(Frame::Null);\n    }\n\n    let old_ttl = if keepttl {\n        db.ttl.get(&key).copied()\n    } else {\n        None\n    };\n\n    db.string_set(&key, value, now);\n\n    if let Some(ttl) = ex {\n        db.ttl.insert(key.clone(), ttl);\n    } else if let Some(old_ttl) = old_ttl {\n        db.ttl.insert(key.clone(), old_ttl);\n    } else {\n        db.ttl.remove(&key);\n    }\n\n    old_value.unwrap_or(Frame::ok())\n}\n\n/// SETNX key value\nfn cmd_setnx(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let value = args[1].clone();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if db.keys.contains_key(&key) {\n        return Frame::Integer(0);\n    }\n\n    db.string_set(&key, value, now);\n    db.ttl.remove(&key);\n    Frame::Integer(1)\n}\n\n/// SETEX key seconds value\nfn cmd_setex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let secs: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    if secs <= 0 {\n        return Frame::error(MSG_INVALID_SETEX_TIME);\n    }\n    let value = args[2].clone();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.del(&key);\n    db.string_set(&key, value, now);\n    db.ttl.insert(key, Duration::from_secs(secs as u64));\n    Frame::ok()\n}\n\n/// PSETEX key milliseconds value\nfn cmd_psetex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let ms: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    if ms <= 0 {\n        return Frame::error(MSG_INVALID_PSETEX_TIME);\n    }\n    let value = args[2].clone();\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.del(&key);\n    db.string_set(&key, value, now);\n    db.ttl.insert(key, Duration::from_millis(ms as u64));\n    Frame::ok()\n}\n\n/// GETSET key value\nfn cmd_getset(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let value = args[1].clone();\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let old = db\n        .string_get(&key)\n        .map(|v| Frame::Bulk(v.clone().into()))\n        .unwrap_or(Frame::Null);\n\n    db.string_set(&key, value, now);\n    db.ttl.remove(&key);\n    old\n}\n\n/// MGET key [key ...]\nfn cmd_mget(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    let mut results = Vec::with_capacity(args.len());\n\n    for arg in args {\n        let key = String::from_utf8_lossy(arg);\n        db.check_ttl(&key);\n        match db.key_type(&key) {\n            Some(KeyType::String) => {\n                if let Some(val) = db.string_get(&key) {\n                    results.push(Frame::Bulk(val.clone().into()));\n                } else {\n                    results.push(Frame::Null);\n                }\n            }\n            _ => results.push(Frame::Null),\n        }\n    }\n\n    Frame::Array(results)\n}\n\n/// MSET key value [key value ...]\nfn cmd_mset(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if !args.len().is_multiple_of(2) {\n        return Frame::error(err_wrong_number(\"mset\"));\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    for pair in args.chunks_exact(2) {\n        let key = String::from_utf8_lossy(&pair[0]).into_owned();\n        let value = pair[1].clone();\n        db.del(&key);\n        db.string_set(&key, value, now);\n    }\n\n    Frame::ok()\n}\n\n/// MSETNX key value [key value ...]\nfn cmd_msetnx(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if !args.len().is_multiple_of(2) {\n        return Frame::error(err_wrong_number(\"msetnx\"));\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    // Check if ANY key already exists\n    for pair in args.chunks_exact(2) {\n        let key = String::from_utf8_lossy(&pair[0]);\n        if db.keys.contains_key(key.as_ref()) {\n            return Frame::Integer(0);\n        }\n    }\n\n    // Set all\n    for pair in args.chunks_exact(2) {\n        let key = String::from_utf8_lossy(&pair[0]).into_owned();\n        let value = pair[1].clone();\n        db.string_set(&key, value, now);\n    }\n\n    Frame::Integer(1)\n}\n\n/// INCR key\nfn cmd_incr(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    match string_incr(state, ctx, &key, 1) {\n        Ok(n) => Frame::Integer(n),\n        Err(f) => f,\n    }\n}\n\n/// INCRBY key increment\nfn cmd_incrby(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let delta: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    match string_incr(state, ctx, &key, delta) {\n        Ok(n) => Frame::Integer(n),\n        Err(f) => f,\n    }\n}\n\n/// INCRBYFLOAT key increment\nfn cmd_incrbyfloat(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let delta_str = String::from_utf8_lossy(&args[1]).into_owned();\n\n    // Validate by parsing as f64 first\n    let delta_f64: f64 = match delta_str.parse() {\n        Ok(n) => n,\n        Err(_) => return Frame::error(MSG_INVALID_FLOAT),\n    };\n    if delta_f64.is_nan() || delta_f64.is_infinite() {\n        return Frame::error(MSG_INVALID_FLOAT);\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let current_str = match db.string_get(&key) {\n        Some(v) => {\n            let s = String::from_utf8_lossy(v).into_owned();\n            // Validate it's a float\n            if s.parse::<f64>().is_err() {\n                return Frame::error(MSG_INVALID_FLOAT);\n            }\n            s\n        }\n        None => \"0\".to_string(),\n    };\n\n    let formatted = decimal_add_format(&current_str, &delta_str);\n\n    // Validate result is not infinite\n    if let Ok(v) = formatted.parse::<f64>()\n        && v.is_infinite()\n    {\n        return Frame::error(MSG_INT_OVERFLOW);\n    }\n\n    db.string_set(&key, formatted.as_bytes().to_vec(), now);\n    Frame::Bulk(formatted.into_bytes().into())\n}\n\n/// DECR key\nfn cmd_decr(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    match string_incr(state, ctx, &key, -1) {\n        Ok(n) => Frame::Integer(n),\n        Err(f) => f,\n    }\n}\n\n/// DECRBY key decrement\nfn cmd_decrby(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let delta: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    match string_incr(state, ctx, &key, -delta) {\n        Ok(n) => Frame::Integer(n),\n        Err(f) => f,\n    }\n}\n\n/// STRLEN key\nfn cmd_strlen(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    match db.string_get(&key) {\n        Some(val) => Frame::Integer(val.len() as i64),\n        None => Frame::Integer(0),\n    }\n}\n\n/// APPEND key value\nfn cmd_append(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let value = &args[1];\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut current = db.string_get(&key).cloned().unwrap_or_default();\n    current.extend_from_slice(value);\n    let new_len = current.len() as i64;\n    db.string_set(&key, current, now);\n    Frame::Integer(new_len)\n}\n\n/// GETRANGE key start end (also aliased as SUBSTR)\nfn cmd_getrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let start: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let end: i64 = match parse_int(&args[2]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let val = match db.string_get(&key) {\n        Some(v) => v.clone(),\n        None => return Frame::Bulk(bytes::Bytes::new()),\n    };\n\n    let len = val.len() as i64;\n    let (rs, re) = redis_range(start, end, len, true);\n    if rs > re || rs >= len {\n        return Frame::Bulk(bytes::Bytes::new());\n    }\n\n    Frame::Bulk(val[rs as usize..=re as usize].to_vec().into())\n}\n\n/// SETRANGE key offset value\nfn cmd_setrange(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let offset: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    if offset < 0 {\n        return Frame::error(\"ERR offset is out of range\");\n    }\n    let offset = offset as usize;\n    let replacement = &args[2];\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut val = db.string_get(&key).cloned().unwrap_or_default();\n\n    // Extend with zeros if needed\n    let needed = offset + replacement.len();\n    if val.len() < needed {\n        val.resize(needed, 0);\n    }\n\n    // Copy replacement bytes\n    val[offset..offset + replacement.len()].copy_from_slice(replacement);\n    let new_len = val.len() as i64;\n    db.string_set(&key, val, now);\n    Frame::Integer(new_len)\n}\n\n/// GETDEL key\nfn cmd_getdel(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Null;\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let val = db\n        .string_get(&key)\n        .map(|v| Frame::Bulk(v.clone().into()))\n        .unwrap_or(Frame::Null);\n\n    db.del(&key);\n    val\n}\n\n/// GETEX key [PERSIST | EX seconds | PX ms | EXAT ts | PXAT ts]\nfn cmd_getex(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n\n    // Parse options\n    let mut persist = false;\n    let mut ex: Option<Duration> = None;\n\n    if args.len() > 1 {\n        let opt = String::from_utf8_lossy(&args[1]).to_uppercase();\n        match opt.as_str() {\n            \"PERSIST\" => {\n                if args.len() != 2 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                persist = true;\n            }\n            \"EX\" => {\n                if args.len() != 3 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let secs: i64 = match parse_int(&args[2]) {\n                    Some(n) => n,\n                    None => return Frame::error(MSG_INVALID_INT),\n                };\n                if secs <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                ex = Some(Duration::from_secs(secs as u64));\n            }\n            \"PX\" => {\n                if args.len() != 3 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let ms: i64 = match parse_int(&args[2]) {\n                    Some(n) => n,\n                    None => return Frame::error(MSG_INVALID_INT),\n                };\n                if ms <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                ex = Some(Duration::from_millis(ms as u64));\n            }\n            \"EXAT\" => {\n                if args.len() != 3 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let ts: i64 = match parse_int(&args[2]) {\n                    Some(n) => n,\n                    None => return Frame::error(MSG_INVALID_INT),\n                };\n                if ts <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                let inner = state.lock();\n                let now = inner.effective_now();\n                let target = std::time::UNIX_EPOCH + Duration::from_secs(ts as u64);\n                ex = Some(target.duration_since(now).unwrap_or(Duration::ZERO));\n                drop(inner);\n            }\n            \"PXAT\" => {\n                if args.len() != 3 {\n                    return Frame::error(MSG_SYNTAX_ERROR);\n                }\n                let ts: i64 = match parse_int(&args[2]) {\n                    Some(n) => n,\n                    None => return Frame::error(MSG_INVALID_INT),\n                };\n                if ts <= 0 {\n                    return Frame::error(MSG_INVALID_SE_TIME);\n                }\n                let inner = state.lock();\n                let now = inner.effective_now();\n                let target = std::time::UNIX_EPOCH + Duration::from_millis(ts as u64);\n                ex = Some(target.duration_since(now).unwrap_or(Duration::ZERO));\n                drop(inner);\n            }\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    }\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if !db.keys.contains_key(&key) {\n        return Frame::Null;\n    }\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    // Apply TTL changes\n    if persist {\n        db.ttl.remove(&key);\n    } else if let Some(ttl) = ex {\n        db.ttl.insert(key.clone(), ttl);\n    }\n\n    match db.string_get(&key) {\n        Some(val) => Frame::Bulk(val.clone().into()),\n        None => Frame::Null,\n    }\n}\n\n// ── Utility functions ────────────────────────────────────────────────\n\n/// Normalize Redis-style range indices. Returns (start, end) inclusive.\n/// `string_mode`: for GETRANGE, the range is inclusive and never returns negative spans.\nfn redis_range(start: i64, end: i64, len: i64, string_mode: bool) -> (i64, i64) {\n    let mut s = start;\n    let mut e = end;\n\n    if s < 0 {\n        s += len;\n    }\n    if e < 0 {\n        e += len;\n    }\n\n    if s < 0 {\n        s = 0;\n    }\n    if e < 0 {\n        e = 0;\n    }\n\n    if string_mode && e >= len {\n        e = len - 1;\n    }\n\n    (s, e)\n}\n\n/// Format a float value the way Redis does.\npub fn format_float(v: f64) -> String {\n    if v == 0.0 && v.is_sign_negative() {\n        return \"0\".to_string();\n    }\n    // Use ryu for fast formatting, then strip trailing zeros after decimal point\n    let mut buf = ryu::Buffer::new();\n    let s = buf.format(v);\n\n    // ryu uses 'e' notation for very large/small numbers; check for that\n    if s.contains('e') || s.contains('E') {\n        // Fall back to standard formatting\n        return format!(\"{}\", v);\n    }\n\n    if s.contains('.') {\n        let trimmed = s.trim_end_matches('0');\n        if trimmed.ends_with('.') {\n            // Keep at least one decimal place (like Redis does for INCRBYFLOAT)\n            // Actually Redis removes trailing zeros completely: \"3.0\" -> \"3\"\n            // But \"3.14000\" -> \"3.14\"\n            // And \"3.0\" -> \"3\" not \"3.0\"\n            return trimmed.trim_end_matches('.').to_string();\n        }\n        return trimmed.to_string();\n    }\n\n    s.to_string()\n}\n\n/// Add two decimal numbers (as strings) and format the result like Redis does.\n/// Uses fixed-point i128 arithmetic with 17 decimal places to match\n/// Go miniredis's big.Float(128-bit) + fmt.Sprintf(\"%.17f\") behavior.\npub fn decimal_add_format(a: &str, b: &str) -> String {\n    const PREC: u32 = 17;\n    let scale: i128 = 10i128.pow(PREC);\n\n    let a_fixed = match parse_decimal_fixed(a, PREC) {\n        Some(v) => v,\n        None => {\n            // Fall back to f64 for values we can't parse (e.g., very large scientific notation)\n            let af: f64 = a.parse().unwrap_or(0.0);\n            let bf: f64 = b.parse().unwrap_or(0.0);\n            return format_float(af + bf);\n        }\n    };\n    let b_fixed = match parse_decimal_fixed(b, PREC) {\n        Some(v) => v,\n        None => {\n            let af: f64 = a.parse().unwrap_or(0.0);\n            let bf: f64 = b.parse().unwrap_or(0.0);\n            return format_float(af + bf);\n        }\n    };\n\n    let sum = a_fixed + b_fixed;\n\n    // Format as decimal with PREC decimal places, then strip trailing zeros\n    let negative = sum < 0;\n    let abs = sum.unsigned_abs();\n    let int_part = abs / scale as u128;\n    let frac_part = abs % scale as u128;\n\n    let mut s = if frac_part == 0 {\n        format!(\"{}\", int_part)\n    } else {\n        let frac_str = format!(\"{:017}\", frac_part);\n        let trimmed = frac_str.trim_end_matches('0');\n        format!(\"{}.{}\", int_part, trimmed)\n    };\n\n    if negative && s != \"0\" {\n        s.insert(0, '-');\n    }\n    s\n}\n\n/// Parse a decimal string (possibly with scientific notation) into fixed-point i128.\n/// Returns value * 10^prec. Returns None if the value would overflow i128.\nfn parse_decimal_fixed(s: &str, prec: u32) -> Option<i128> {\n    let s = s.trim();\n    let (negative, s) = if let Some(s) = s.strip_prefix('-') {\n        (true, s)\n    } else if let Some(s) = s.strip_prefix('+') {\n        (false, s)\n    } else {\n        (false, s)\n    };\n\n    // Handle scientific notation: split on 'e' or 'E'\n    let (mantissa, exp) = if let Some(pos) = s.find(['e', 'E']) {\n        let exp: i32 = s[pos + 1..].parse().ok()?;\n        (&s[..pos], exp)\n    } else {\n        (s, 0)\n    };\n\n    // Split mantissa into integer and fractional parts\n    let (int_str, frac_str) = if let Some(dot) = mantissa.find('.') {\n        (&mantissa[..dot], &mantissa[dot + 1..])\n    } else {\n        (mantissa, \"\")\n    };\n\n    // Build the full digit string: integer + fractional digits\n    let mut digits = String::with_capacity(int_str.len() + frac_str.len());\n    digits.push_str(int_str);\n    digits.push_str(frac_str);\n\n    // The implicit decimal point is after int_str.len() digits.\n    // The exponent shifts it by exp positions to the right.\n    // We need prec digits after the decimal point.\n    // Position of decimal point from the left: int_str.len() + exp\n    // We need total_digits = decimal_point_pos + prec digits total (with zero-padding)\n    let decimal_point = int_str.len() as i32 + exp;\n    let total_needed = decimal_point + prec as i32;\n\n    if total_needed < 0 {\n        // Result is too small, rounds to 0\n        return Some(0);\n    }\n\n    // Pad or truncate digits to total_needed length\n    let total_needed = total_needed as usize;\n    while digits.len() < total_needed {\n        digits.push('0');\n    }\n    // If we have more digits than needed, truncate (rounding towards zero)\n    digits.truncate(total_needed);\n\n    if digits.is_empty() {\n        return Some(0);\n    }\n\n    let value: i128 = digits.parse().ok()?;\n    Some(if negative { -value } else { value })\n}\n\n// ── Bit operations ───────────────────────────────────────────────────\n\n/// GETBIT key offset\nfn cmd_getbit(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let offset: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(\"ERR bit offset is not an integer or out of range\"),\n    };\n    if offset < 0 {\n        return Frame::error(\"ERR bit offset is not an integer or out of range\");\n    }\n    let offset = offset as usize;\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let val = db.string_get(&key).cloned().unwrap_or_default();\n    let byte_idx = offset / 8;\n    let bit_idx = 7 - (offset % 8);\n\n    if byte_idx >= val.len() {\n        return Frame::Integer(0);\n    }\n\n    let bit = (val[byte_idx] >> bit_idx) & 1;\n    Frame::Integer(bit as i64)\n}\n\n/// SETBIT key offset value\nfn cmd_setbit(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]).into_owned();\n    let offset: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(\"ERR bit offset is not an integer or out of range\"),\n    };\n    if offset < 0 {\n        return Frame::error(\"ERR bit offset is not an integer or out of range\");\n    }\n    let offset = offset as usize;\n\n    let bit_val: i64 = match parse_int(&args[2]) {\n        Some(n) => n,\n        None => return Frame::error(\"ERR bit is not an integer or out of range\"),\n    };\n    if bit_val != 0 && bit_val != 1 {\n        return Frame::error(\"ERR bit is not an integer or out of range\");\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let mut val = db.string_get(&key).cloned().unwrap_or_default();\n    let byte_idx = offset / 8;\n    let bit_idx = 7 - (offset % 8);\n\n    // Expand if needed\n    if byte_idx >= val.len() {\n        val.resize(byte_idx + 1, 0);\n    }\n\n    let old_bit = (val[byte_idx] >> bit_idx) & 1;\n\n    if bit_val == 1 {\n        val[byte_idx] |= 1 << bit_idx;\n    } else {\n        val[byte_idx] &= !(1 << bit_idx);\n    }\n\n    db.string_set(&key, val, now);\n    Frame::Integer(old_bit as i64)\n}\n\n/// BITCOUNT key [start end [BYTE|BIT]]\nfn cmd_bitcount(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let val = db.string_get(&key).cloned().unwrap_or_default();\n\n    if args.len() == 1 {\n        // Count all bits\n        let count: u32 = val.iter().map(|b| b.count_ones()).sum();\n        return Frame::Integer(count as i64);\n    }\n\n    if args.len() < 3 {\n        return Frame::error(MSG_SYNTAX_ERROR);\n    }\n\n    let start: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    let end: i64 = match parse_int(&args[2]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n\n    let bit_mode = if args.len() > 3 {\n        let mode = String::from_utf8_lossy(&args[3]).to_uppercase();\n        match mode.as_str() {\n            \"BYTE\" => false,\n            \"BIT\" => true,\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    } else {\n        false\n    };\n\n    if bit_mode {\n        let bit_len = val.len() as i64 * 8;\n        let (rs, re) = bitcount_range(start, end, bit_len);\n        if rs > re {\n            return Frame::Integer(0);\n        }\n        let mut count = 0u32;\n        for i in rs..=re {\n            let byte_idx = (i / 8) as usize;\n            let bit_idx = 7 - (i % 8) as usize;\n            if byte_idx < val.len() && (val[byte_idx] >> bit_idx) & 1 == 1 {\n                count += 1;\n            }\n        }\n        Frame::Integer(count as i64)\n    } else {\n        let byte_len = val.len() as i64;\n        let (rs, re) = bitcount_range(start, end, byte_len);\n        if rs > re {\n            return Frame::Integer(0);\n        }\n        let count: u32 = val[rs as usize..=re as usize]\n            .iter()\n            .map(|b| b.count_ones())\n            .sum();\n        Frame::Integer(count as i64)\n    }\n}\n\nfn bitcount_range(start: i64, end: i64, len: i64) -> (i64, i64) {\n    let mut s = start;\n    let mut e = end;\n    if s < 0 {\n        s += len;\n    }\n    if e < 0 {\n        e += len;\n    }\n    if s < 0 {\n        s = 0;\n    }\n    if e < 0 {\n        e = 0;\n    }\n    if e >= len {\n        e = len - 1;\n    }\n    (s, e)\n}\n\n/// BITOP operation destkey key [key ...]\nfn cmd_bitop(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let op = String::from_utf8_lossy(&args[0]).to_uppercase();\n    let dest = String::from_utf8_lossy(&args[1]).into_owned();\n    let src_keys: Vec<String> = args[2..]\n        .iter()\n        .map(|a| String::from_utf8_lossy(a).into_owned())\n        .collect();\n\n    if op == \"NOT\" && src_keys.len() != 1 {\n        return Frame::error(\"ERR BITOP NOT must be called with a single source key.\");\n    }\n\n    let mut inner = state.lock();\n    let now = inner.effective_now();\n    let db = inner.db_mut(ctx.selected_db);\n\n    // Collect all source values\n    let mut values: Vec<Vec<u8>> = Vec::new();\n    let mut max_len = 0;\n    for key in &src_keys {\n        db.check_ttl(key);\n        if let Some(t) = db.key_type(key)\n            && t != KeyType::String\n        {\n            return Frame::error(MSG_WRONG_TYPE);\n        }\n        let val = db.string_get(key).cloned().unwrap_or_default();\n        if val.len() > max_len {\n            max_len = val.len();\n        }\n        values.push(val);\n    }\n\n    let mut result = vec![0u8; max_len];\n    match op.as_str() {\n        \"AND\" => {\n            if !values.is_empty() {\n                result = vec![0xFF; max_len];\n                for val in &values {\n                    for i in 0..max_len {\n                        let b = if i < val.len() { val[i] } else { 0 };\n                        result[i] &= b;\n                    }\n                }\n            }\n        }\n        \"OR\" => {\n            for val in &values {\n                for i in 0..max_len {\n                    let b = if i < val.len() { val[i] } else { 0 };\n                    result[i] |= b;\n                }\n            }\n        }\n        \"XOR\" => {\n            for val in &values {\n                for i in 0..max_len {\n                    let b = if i < val.len() { val[i] } else { 0 };\n                    result[i] ^= b;\n                }\n            }\n        }\n        \"NOT\" => {\n            for i in 0..max_len {\n                let b = if i < values[0].len() { values[0][i] } else { 0 };\n                result[i] = !b;\n            }\n        }\n        _ => return Frame::error(MSG_SYNTAX_ERROR),\n    }\n\n    let len = result.len() as i64;\n    if result.is_empty() {\n        // No source data → delete destination key (like Redis)\n        db.del(&dest);\n    } else {\n        db.string_set(&dest, result, now);\n    }\n    Frame::Integer(len)\n}\n\n/// BITPOS key bit [start [end [BYTE|BIT]]]\nfn cmd_bitpos(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    let key = String::from_utf8_lossy(&args[0]);\n    let target_bit: i64 = match parse_int(&args[1]) {\n        Some(n) => n,\n        None => return Frame::error(MSG_INVALID_INT),\n    };\n    if target_bit != 0 && target_bit != 1 {\n        return Frame::error(\"ERR The bit argument must be 1 or 0.\");\n    }\n    let target = target_bit as u8;\n\n    let mut inner = state.lock();\n    let db = inner.db_mut(ctx.selected_db);\n    db.check_ttl(&key);\n\n    if let Some(t) = db.key_type(&key)\n        && t != KeyType::String\n    {\n        return Frame::error(MSG_WRONG_TYPE);\n    }\n\n    let key_exists = db.keys.contains_key(key.as_ref());\n    let val = db.string_get(&key).cloned().unwrap_or_default();\n    if val.is_empty() {\n        if !key_exists && target == 0 {\n            // Non-existent key: virtual infinite zeros, first 0-bit at position 0\n            return Frame::Integer(0);\n        }\n        return Frame::Integer(-1);\n    }\n\n    let has_range = args.len() > 2;\n    let has_end = args.len() > 3;\n    let bit_mode = if args.len() > 4 {\n        let mode = String::from_utf8_lossy(&args[4]).to_uppercase();\n        match mode.as_str() {\n            \"BYTE\" => false,\n            \"BIT\" => true,\n            _ => return Frame::error(MSG_SYNTAX_ERROR),\n        }\n    } else {\n        false\n    };\n\n    let byte_len = val.len() as i64;\n    let bit_len = byte_len * 8;\n\n    if bit_mode {\n        let start = if args.len() > 2 {\n            match parse_int(&args[2]) {\n                Some(n) => n,\n                None => return Frame::error(MSG_INVALID_INT),\n            }\n        } else {\n            0\n        };\n        let end = if args.len() > 3 {\n            match parse_int(&args[3]) {\n                Some(n) => n,\n                None => return Frame::error(MSG_INVALID_INT),\n            }\n        } else {\n            bit_len - 1\n        };\n\n        let (rs, re) = bitcount_range(start, end, bit_len);\n        if rs > re {\n            return Frame::Integer(-1);\n        }\n        for i in rs..=re {\n            let byte_idx = (i / 8) as usize;\n            let bit_idx = 7 - (i % 8) as usize;\n            let bit = if byte_idx < val.len() {\n                (val[byte_idx] >> bit_idx) & 1\n            } else {\n                0\n            };\n            if bit == target {\n                return Frame::Integer(i);\n            }\n        }\n        Frame::Integer(-1)\n    } else {\n        // BYTE mode\n        let start = if args.len() > 2 {\n            match parse_int(&args[2]) {\n                Some(n) => n,\n                None => return Frame::error(MSG_INVALID_INT),\n            }\n        } else {\n            0\n        };\n        let end = if args.len() > 3 {\n            match parse_int(&args[3]) {\n                Some(n) => n,\n                None => return Frame::error(MSG_INVALID_INT),\n            }\n        } else {\n            byte_len - 1\n        };\n\n        let (rs, re) = bitcount_range(start, end, byte_len);\n        if rs > re {\n            return Frame::Integer(-1);\n        }\n\n        for byte_idx in rs..=re {\n            let b = val[byte_idx as usize];\n            for bit_idx in 0..8 {\n                let bit = (b >> (7 - bit_idx)) & 1;\n                if bit == target {\n                    return Frame::Integer(byte_idx * 8 + bit_idx);\n                }\n            }\n        }\n\n        // If looking for 0 and no end was specified, the 0 bit is at end+1\n        if target == 0 && !has_end && !has_range {\n            return Frame::Integer(bit_len);\n        }\n        if target == 0 && has_range && !has_end {\n            return Frame::Integer(bit_len);\n        }\n\n        Frame::Integer(-1)\n    }\n}\n"
  },
  {
    "path": "miniredis/src/cmd/transactions.rs",
    "content": "use std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::dispatch::CommandTable;\nuse crate::frame::Frame;\n\npub fn register(table: &mut CommandTable) {\n    table.add(\"MULTI\", cmd_multi, false, 1);\n    table.add(\"DISCARD\", cmd_discard, false, 1);\n    table.add(\"WATCH\", cmd_watch, true, -2);\n    table.add(\"UNWATCH\", cmd_unwatch, false, 1);\n    // EXEC is handled directly in dispatch.rs (needs command table access).\n}\n\n/// MULTI\nfn cmd_multi(_state: &Arc<SharedState>, ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    if ctx.in_tx() {\n        return Frame::error(\"ERR MULTI calls can not be nested\");\n    }\n\n    ctx.transaction = Some(Vec::new());\n    ctx.dirty_transaction = false;\n    Frame::ok()\n}\n\n/// DISCARD\nfn cmd_discard(_state: &Arc<SharedState>, ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    if !ctx.in_tx() {\n        return Frame::error(\"ERR DISCARD without MULTI\");\n    }\n\n    ctx.transaction = None;\n    ctx.watch.clear();\n    Frame::ok()\n}\n\n/// WATCH key [key ...]\nfn cmd_watch(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame {\n    if ctx.in_tx() {\n        return Frame::error(\"ERR WATCH inside MULTI is not allowed\");\n    }\n\n    let inner = state.lock();\n    let db = inner.db(ctx.selected_db);\n\n    for arg in args {\n        let key = String::from_utf8_lossy(arg).to_string();\n        let version = db.key_version.get(&key).copied().unwrap_or(0);\n        ctx.watch.insert((ctx.selected_db, key), version);\n    }\n\n    Frame::ok()\n}\n\n/// UNWATCH\nfn cmd_unwatch(_state: &Arc<SharedState>, ctx: &mut ConnCtx, _args: &[Vec<u8>]) -> Frame {\n    ctx.watch.clear();\n    Frame::ok()\n}\n"
  },
  {
    "path": "miniredis/src/connection.rs",
    "content": "use bytes::BytesMut;\nuse std::collections::HashMap;\nuse std::io::Cursor;\nuse tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufWriter};\nuse tokio::net::TcpStream;\n\nuse crate::frame::{Frame, FrameError};\n\n/// Trait alias for an async stream that supports both read and write.\npub trait IoStream: AsyncRead + AsyncWrite + Unpin + Send {}\nimpl<T: AsyncRead + AsyncWrite + Unpin + Send> IoStream for T {}\n\n/// A connection wraps a stream with buffered read/write and RESP\n/// frame parsing. Supports both plain TCP and TLS connections.\npub struct Connection {\n    stream: BufWriter<Box<dyn IoStream>>,\n    buffer: BytesMut,\n    /// Whether this connection uses RESP3 encoding (set via HELLO 3).\n    pub resp3: bool,\n}\n\nimpl Connection {\n    /// Create a new `Connection` backed by a plain TCP socket.\n    pub fn new(socket: TcpStream) -> Connection {\n        Connection {\n            stream: BufWriter::new(Box::new(socket)),\n            buffer: BytesMut::with_capacity(4096),\n            resp3: false,\n        }\n    }\n\n    /// Create a new `Connection` backed by any async read/write stream (e.g. TLS).\n    pub fn new_stream(stream: impl IoStream + 'static) -> Connection {\n        Connection {\n            stream: BufWriter::new(Box::new(stream)),\n            buffer: BytesMut::with_capacity(4096),\n            resp3: false,\n        }\n    }\n\n    /// Read a single RESP frame from the connection.\n    ///\n    /// Returns `None` if the remote half closed the connection cleanly.\n    pub async fn read_frame(&mut self) -> crate::Result<Option<Frame>> {\n        loop {\n            // Try to parse a frame from the buffered data.\n            if let Some(frame) = self.parse_frame()? {\n                return Ok(Some(frame));\n            }\n\n            // Not enough data for a frame — read more from the socket.\n            let n = self.stream.read_buf(&mut self.buffer).await?;\n            if n == 0 {\n                // Connection closed\n                if self.buffer.is_empty() {\n                    return Ok(None);\n                } else {\n                    return Err(\"connection reset by peer\".into());\n                }\n            }\n        }\n    }\n\n    /// Try to parse a frame from the current buffer contents.\n    fn parse_frame(&mut self) -> crate::Result<Option<Frame>> {\n        use bytes::Buf;\n\n        let mut cursor = Cursor::new(&self.buffer[..]);\n\n        match Frame::check(&mut cursor) {\n            Ok(()) => {\n                // We know a complete frame is in the buffer.\n                let len = cursor.position() as usize;\n\n                // Reset cursor and parse.\n                cursor.set_position(0);\n                let frame = Frame::parse(&mut cursor)\n                    .map_err(|e| -> crate::Error { e.to_string().into() })?;\n\n                // Advance the buffer past the consumed bytes.\n                self.buffer.advance(len);\n\n                Ok(Some(frame))\n            }\n            Err(FrameError::Incomplete) => Ok(None),\n            Err(e) => Err(e.to_string().into()),\n        }\n    }\n\n    /// Write a frame to the connection, using RESP3 encoding if negotiated.\n    pub async fn write_frame(&mut self, frame: &Frame) -> crate::Result<()> {\n        let bytes = frame.serialize_resp(self.resp3);\n        self.stream.write_all(&bytes).await?;\n        self.stream.flush().await?;\n        Ok(())\n    }\n\n    /// Write raw bytes (used for inline protocol or multi-frame writes).\n    pub async fn write_all(&mut self, data: &[u8]) -> crate::Result<()> {\n        self.stream.write_all(data).await?;\n        self.stream.flush().await?;\n        Ok(())\n    }\n}\n\n// ── Per-Connection State ─────────────────────────────────────────────\n\n/// Per-connection context, carrying state that persists across commands\n/// within a single client session.\npub struct ConnCtx {\n    /// Currently selected database index (0-15).\n    pub selected_db: usize,\n    /// True once the client has sent a valid AUTH command (when passwords are configured).\n    pub authenticated: bool,\n    /// If Some, we're inside a MULTI block; the vec holds queued command args.\n    pub transaction: Option<Vec<QueuedCommand>>,\n    /// Set to true if any error occurs while queuing commands in a MULTI.\n    pub dirty_transaction: bool,\n    /// WATCH map: (db_index, key) -> version at WATCH time.\n    pub watch: HashMap<(usize, String), u64>,\n    /// True if the client negotiated RESP3 via HELLO.\n    pub resp3: bool,\n    /// CLIENT SETNAME value.\n    pub client_name: Option<String>,\n    /// True when executing inside a Lua script (nested call).\n    pub nested: bool,\n    /// SHA of the currently executing Lua script (if nested).\n    pub nested_sha: Option<String>,\n    /// Channels to subscribe to after EXEC completes (for SUBSCRIBE inside MULTI).\n    pub pending_subscribe: Vec<String>,\n    /// Patterns to subscribe to after EXEC completes (for PSUBSCRIBE inside MULTI).\n    pub pending_psubscribe: Vec<String>,\n}\n\n/// A command queued inside a MULTI transaction.\npub struct QueuedCommand {\n    /// The raw arguments (command name + args).\n    pub args: Vec<Vec<u8>>,\n}\n\nimpl Default for ConnCtx {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl ConnCtx {\n    pub fn new() -> Self {\n        ConnCtx {\n            selected_db: 0,\n            authenticated: false,\n            transaction: None,\n            dirty_transaction: false,\n            watch: HashMap::new(),\n            resp3: false,\n            client_name: None,\n            nested: false,\n            nested_sha: None,\n            pending_subscribe: Vec::new(),\n            pending_psubscribe: Vec::new(),\n        }\n    }\n\n    /// Are we inside a MULTI transaction?\n    pub fn in_tx(&self) -> bool {\n        self.transaction.is_some()\n    }\n}\n"
  },
  {
    "path": "miniredis/src/db.rs",
    "content": "use std::collections::{HashMap, HashSet, VecDeque};\nuse std::sync::Arc;\nuse std::sync::atomic::AtomicU64;\nuse std::time::{Duration, SystemTime};\n\nuse rand::SeedableRng;\nuse rand::rngs::StdRng;\nuse tokio::sync::{Notify, broadcast};\n\nuse crate::hll::HyperLogLog;\nuse crate::types::{KeyType, SortedSet, Stream};\n\n/// A single numbered Redis database (0-15).\n#[derive(Debug)]\npub struct RedisDB {\n    /// Master map: key name -> type tag.\n    pub keys: HashMap<String, KeyType>,\n    /// String values.\n    pub string_keys: HashMap<String, Vec<u8>>,\n    /// Hash values: key -> (field -> value).\n    pub hash_keys: HashMap<String, HashMap<String, Vec<u8>>>,\n    /// List values.\n    pub list_keys: HashMap<String, VecDeque<Vec<u8>>>,\n    /// Set values.\n    pub set_keys: HashMap<String, HashSet<String>>,\n    /// Sorted set values.\n    pub sorted_set_keys: HashMap<String, SortedSet>,\n    /// Stream values.\n    pub stream_keys: HashMap<String, Stream>,\n    /// HyperLogLog values.\n    pub hll_keys: HashMap<String, HyperLogLog>,\n    /// Key TTLs (remaining duration).\n    pub ttl: HashMap<String, Duration>,\n    /// Hash field TTLs: key -> (field -> remaining duration).\n    pub hash_field_ttls: HashMap<String, HashMap<String, Duration>>,\n    /// Key versions (bumped on every mutation, used by WATCH).\n    pub key_version: HashMap<String, u64>,\n    /// Last-recently-used timestamps.\n    pub lru: HashMap<String, SystemTime>,\n}\n\nimpl Default for RedisDB {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl RedisDB {\n    pub fn new() -> Self {\n        RedisDB {\n            keys: HashMap::new(),\n            string_keys: HashMap::new(),\n            hash_keys: HashMap::new(),\n            list_keys: HashMap::new(),\n            set_keys: HashMap::new(),\n            sorted_set_keys: HashMap::new(),\n            stream_keys: HashMap::new(),\n            hll_keys: HashMap::new(),\n            ttl: HashMap::new(),\n            hash_field_ttls: HashMap::new(),\n            key_version: HashMap::new(),\n            lru: HashMap::new(),\n        }\n    }\n\n    /// Check if a key exists (also updates LRU).\n    pub fn exists(&mut self, key: &str, now: SystemTime) -> bool {\n        if self.keys.contains_key(key) {\n            self.lru.insert(key.to_owned(), now);\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Get the type of a key, or None.\n    pub fn key_type(&self, key: &str) -> Option<KeyType> {\n        self.keys.get(key).copied()\n    }\n\n    /// Increment the key version and update LRU.\n    pub fn incr_version(&mut self, key: &str, now: SystemTime) {\n        self.lru.insert(key.to_owned(), now);\n        let v = self.key_version.entry(key.to_owned()).or_insert(0);\n        *v += 1;\n    }\n\n    /// Delete a key and its data. Returns true if the key existed.\n    pub fn del(&mut self, key: &str) -> bool {\n        let key_type = match self.keys.remove(key) {\n            Some(t) => t,\n            None => return false,\n        };\n\n        self.lru.remove(key);\n        self.ttl.remove(key);\n        self.hash_field_ttls.remove(key);\n        let v = self.key_version.entry(key.to_owned()).or_insert(0);\n        *v += 1;\n\n        match key_type {\n            KeyType::String => {\n                self.string_keys.remove(key);\n            }\n            KeyType::Hash => {\n                self.hash_keys.remove(key);\n            }\n            KeyType::List => {\n                self.list_keys.remove(key);\n            }\n            KeyType::Set => {\n                self.set_keys.remove(key);\n            }\n            KeyType::SortedSet => {\n                self.sorted_set_keys.remove(key);\n            }\n            KeyType::Stream => {\n                self.stream_keys.remove(key);\n            }\n            KeyType::HyperLogLog => {\n                self.hll_keys.remove(key);\n            }\n        }\n\n        true\n    }\n\n    /// Delete a key without removing its TTL (used by string_set etc.).\n    pub fn del_keep_ttl(&mut self, key: &str) {\n        let key_type = match self.keys.remove(key) {\n            Some(t) => t,\n            None => return,\n        };\n\n        match key_type {\n            KeyType::String => {\n                self.string_keys.remove(key);\n            }\n            KeyType::Hash => {\n                self.hash_keys.remove(key);\n            }\n            KeyType::List => {\n                self.list_keys.remove(key);\n            }\n            KeyType::Set => {\n                self.set_keys.remove(key);\n            }\n            KeyType::SortedSet => {\n                self.sorted_set_keys.remove(key);\n            }\n            KeyType::Stream => {\n                self.stream_keys.remove(key);\n            }\n            KeyType::HyperLogLog => {\n                self.hll_keys.remove(key);\n            }\n        }\n    }\n\n    /// GET: returns the value of a string key, or None.\n    pub fn string_get(&self, key: &str) -> Option<&Vec<u8>> {\n        if self.keys.get(key) != Some(&KeyType::String) {\n            return None;\n        }\n        self.string_keys.get(key)\n    }\n\n    /// SET: force-set a string key. Does NOT remove TTL.\n    pub fn string_set(&mut self, key: &str, value: Vec<u8>, now: SystemTime) {\n        self.del_keep_ttl(key);\n        self.keys.insert(key.to_owned(), KeyType::String);\n        self.string_keys.insert(key.to_owned(), value);\n        self.incr_version(key, now);\n    }\n\n    // ── Hash helpers ──────────────────────────────────────────────────\n\n    /// Set hash fields. Returns the number of NEW fields added.\n    pub fn hash_set(&mut self, key: &str, pairs: &[(String, Vec<u8>)], now: SystemTime) -> i64 {\n        self.keys.entry(key.to_owned()).or_insert(KeyType::Hash);\n        let hash = self.hash_keys.entry(key.to_owned()).or_default();\n        let mut new_count = 0i64;\n        for (field, value) in pairs {\n            if !hash.contains_key(field) {\n                new_count += 1;\n            }\n            hash.insert(field.clone(), value.clone());\n        }\n        self.incr_version(key, now);\n        new_count\n    }\n\n    /// Get a hash field value.\n    pub fn hash_get(&self, key: &str, field: &str) -> Option<&Vec<u8>> {\n        self.hash_keys.get(key)?.get(field)\n    }\n\n    /// Delete hash fields. Returns the number deleted. Removes key if hash becomes empty.\n    pub fn hash_del(&mut self, key: &str, fields: &[String], now: SystemTime) -> i64 {\n        let hash = match self.hash_keys.get_mut(key) {\n            Some(h) => h,\n            None => return 0,\n        };\n        let mut count = 0i64;\n        for field in fields {\n            if hash.remove(field).is_some() {\n                count += 1;\n            }\n        }\n        if hash.is_empty() {\n            self.del(key);\n        } else {\n            self.incr_version(key, now);\n        }\n        count\n    }\n\n    /// Get all hash field names, sorted.\n    pub fn hash_fields(&self, key: &str) -> Vec<String> {\n        match self.hash_keys.get(key) {\n            Some(h) => {\n                let mut fields: Vec<String> = h.keys().cloned().collect();\n                fields.sort();\n                fields\n            }\n            None => Vec::new(),\n        }\n    }\n\n    /// Get all hash values in field-sorted order.\n    pub fn hash_values(&self, key: &str) -> Vec<Vec<u8>> {\n        let fields = self.hash_fields(key);\n        let hash = match self.hash_keys.get(key) {\n            Some(h) => h,\n            None => return Vec::new(),\n        };\n        fields.iter().filter_map(|f| hash.get(f).cloned()).collect()\n    }\n\n    // ── List helpers ─────────────────────────────────────────────────\n\n    /// LPUSH: prepend value(s) to a list. Returns new length.\n    pub fn list_lpush(&mut self, key: &str, values: &[Vec<u8>], now: SystemTime) -> i64 {\n        self.keys.entry(key.to_owned()).or_insert(KeyType::List);\n        let list = self.list_keys.entry(key.to_owned()).or_default();\n        for v in values {\n            list.push_front(v.clone());\n        }\n        let len = list.len() as i64;\n        self.incr_version(key, now);\n        len\n    }\n\n    /// RPUSH: append value(s) to a list. Returns new length.\n    pub fn list_rpush(&mut self, key: &str, values: &[Vec<u8>], now: SystemTime) -> i64 {\n        self.keys.entry(key.to_owned()).or_insert(KeyType::List);\n        let list = self.list_keys.entry(key.to_owned()).or_default();\n        for v in values {\n            list.push_back(v.clone());\n        }\n        let len = list.len() as i64;\n        self.incr_version(key, now);\n        len\n    }\n\n    /// LPOP: remove and return the first element.\n    pub fn list_lpop(&mut self, key: &str, now: SystemTime) -> Option<Vec<u8>> {\n        let list = self.list_keys.get_mut(key)?;\n        let val = list.pop_front()?;\n        if list.is_empty() {\n            self.del(key);\n        } else {\n            self.incr_version(key, now);\n        }\n        Some(val)\n    }\n\n    /// RPOP: remove and return the last element.\n    pub fn list_rpop(&mut self, key: &str, now: SystemTime) -> Option<Vec<u8>> {\n        let list = self.list_keys.get_mut(key)?;\n        let val = list.pop_back()?;\n        if list.is_empty() {\n            self.del(key);\n        } else {\n            self.incr_version(key, now);\n        }\n        Some(val)\n    }\n\n    // ── Set helpers ──────────────────────────────────────────────────\n\n    /// SADD: add members to a set. Returns count of new members added.\n    pub fn set_add(&mut self, key: &str, members: &[String], now: SystemTime) -> i64 {\n        self.keys.entry(key.to_owned()).or_insert(KeyType::Set);\n        let set = self.set_keys.entry(key.to_owned()).or_default();\n        let mut added = 0i64;\n        for m in members {\n            if set.insert(m.clone()) {\n                added += 1;\n            }\n        }\n        self.incr_version(key, now);\n        added\n    }\n\n    /// SREM: remove members from a set. Returns count removed.\n    pub fn set_rem(&mut self, key: &str, members: &[String], now: SystemTime) -> i64 {\n        let set = match self.set_keys.get_mut(key) {\n            Some(s) => s,\n            None => return 0,\n        };\n        let mut removed = 0i64;\n        for m in members {\n            if set.remove(m) {\n                removed += 1;\n            }\n        }\n        if set.is_empty() {\n            self.del(key);\n        } else {\n            self.incr_version(key, now);\n        }\n        removed\n    }\n\n    /// Get all members of a set, sorted.\n    pub fn set_members(&self, key: &str) -> Vec<String> {\n        match self.set_keys.get(key) {\n            Some(s) => {\n                let mut members: Vec<String> = s.iter().cloned().collect();\n                members.sort();\n                members\n            }\n            None => Vec::new(),\n        }\n    }\n\n    /// Check if a member is in a set.\n    pub fn set_is_member(&self, key: &str, member: &str) -> bool {\n        self.set_keys\n            .get(key)\n            .map(|s| s.contains(member))\n            .unwrap_or(false)\n    }\n\n    /// Replace a set entirely (used by set operations like SDIFFSTORE).\n    pub fn set_set(&mut self, key: &str, members: HashSet<String>, now: SystemTime) {\n        if members.is_empty() {\n            return;\n        }\n        self.del(key);\n        self.keys.insert(key.to_owned(), KeyType::Set);\n        self.set_keys.insert(key.to_owned(), members);\n        self.incr_version(key, now);\n    }\n\n    // ── Sorted set helpers ───────────────────────────────────────────\n\n    /// ZADD: add a member with score. Returns true if the member was new.\n    pub fn sset_add(&mut self, key: &str, score: f64, member: &str, now: SystemTime) -> bool {\n        self.keys\n            .entry(key.to_owned())\n            .or_insert(KeyType::SortedSet);\n        let ss = self.sorted_set_keys.entry(key.to_owned()).or_default();\n        let is_new = ss.set(score, member);\n        self.incr_version(key, now);\n        is_new\n    }\n\n    /// Check if a member exists in a sorted set.\n    pub fn sset_exists(&self, key: &str, member: &str) -> bool {\n        self.sorted_set_keys\n            .get(key)\n            .map(|ss| ss.exists(member))\n            .unwrap_or(false)\n    }\n\n    /// Get a member's score.\n    pub fn sset_score(&self, key: &str, member: &str) -> Option<f64> {\n        self.sorted_set_keys.get(key)?.get(member)\n    }\n\n    /// Get cardinality of sorted set.\n    pub fn sset_card(&self, key: &str) -> usize {\n        self.sorted_set_keys\n            .get(key)\n            .map(|ss| ss.card())\n            .unwrap_or(0)\n    }\n\n    /// ZINCRBY: increment member's score. Returns new score.\n    pub fn sset_incrby(&mut self, key: &str, member: &str, delta: f64, now: SystemTime) -> f64 {\n        self.keys\n            .entry(key.to_owned())\n            .or_insert(KeyType::SortedSet);\n        let ss = self.sorted_set_keys.entry(key.to_owned()).or_default();\n        let new_score = ss.incrby(member, delta);\n        self.incr_version(key, now);\n        new_score\n    }\n\n    /// Remove a member from a sorted set. Returns true if it existed.\n    pub fn sset_rem(&mut self, key: &str, member: &str, now: SystemTime) -> bool {\n        let ss = match self.sorted_set_keys.get_mut(key) {\n            Some(ss) => ss,\n            None => return false,\n        };\n        let removed = ss.remove(member);\n        if ss.card() == 0 {\n            self.del(key);\n        } else {\n            self.incr_version(key, now);\n        }\n        removed\n    }\n\n    /// Replace a sorted set entirely.\n    pub fn sset_set(&mut self, key: &str, ss: SortedSet, now: SystemTime) {\n        if ss.card() == 0 {\n            self.del(key);\n            return;\n        }\n        self.del(key);\n        self.keys.insert(key.to_owned(), KeyType::SortedSet);\n        self.sorted_set_keys.insert(key.to_owned(), ss);\n        self.incr_version(key, now);\n    }\n\n    // ── HyperLogLog helpers ────────────────────────────────────────\n\n    /// PFADD: add items to a HyperLogLog. Returns 1 if any register changed, 0 otherwise.\n    pub fn hll_add(&mut self, key: &str, items: &[&str], now: SystemTime) -> i64 {\n        self.keys\n            .entry(key.to_owned())\n            .or_insert(KeyType::HyperLogLog);\n        let hll = self.hll_keys.entry(key.to_owned()).or_default();\n        let mut changed = false;\n        for item in items {\n            if hll.add(item.as_bytes()) {\n                changed = true;\n            }\n        }\n        self.incr_version(key, now);\n        if changed { 1 } else { 0 }\n    }\n\n    /// PFCOUNT: count across one or more HLL keys. Returns error if any key is wrong type.\n    pub fn hll_count(&self, keys: &[&str]) -> Result<i64, &'static str> {\n        if keys.len() == 1 {\n            let key = keys[0];\n            if let Some(kt) = self.keys.get(key)\n                && *kt != KeyType::HyperLogLog\n            {\n                return Err(\"WRONGTYPE Key is not a valid HyperLogLog string value.\");\n            }\n            match self.hll_keys.get(key) {\n                Some(hll) => Ok(hll.count() as i64),\n                None => Ok(0),\n            }\n        } else {\n            // Multiple keys: merge into temporary HLL and count\n            let mut merged = HyperLogLog::new();\n            for &key in keys {\n                if let Some(kt) = self.keys.get(key)\n                    && *kt != KeyType::HyperLogLog\n                {\n                    return Err(\"WRONGTYPE Key is not a valid HyperLogLog string value.\");\n                }\n                if let Some(hll) = self.hll_keys.get(key) {\n                    merged.merge(hll);\n                }\n            }\n            Ok(merged.count() as i64)\n        }\n    }\n\n    /// PFMERGE: merge source HLLs into dest. keys[0] is dest, rest are sources.\n    /// Returns error if any key is wrong type.\n    pub fn hll_merge(&mut self, keys: &[&str], now: SystemTime) -> Result<(), &'static str> {\n        // Validate all keys first\n        for &key in keys {\n            if let Some(kt) = self.keys.get(key)\n                && *kt != KeyType::HyperLogLog\n            {\n                return Err(\"WRONGTYPE Key is not a valid HyperLogLog string value.\");\n            }\n        }\n\n        let dest = keys[0];\n\n        // Collect source HLLs into a merged result\n        let mut merged = self.hll_keys.get(dest).cloned().unwrap_or_default();\n\n        for &key in &keys[1..] {\n            if let Some(hll) = self.hll_keys.get(key) {\n                merged.merge(hll);\n            }\n        }\n\n        // Store the result\n        self.keys.insert(dest.to_owned(), KeyType::HyperLogLog);\n        self.hll_keys.insert(dest.to_owned(), merged);\n        self.incr_version(dest, now);\n        Ok(())\n    }\n\n    // ── Key rename helper ────────────────────────────────────────────\n\n    /// Rename a key. Returns false if source doesn't exist.\n    pub fn rename(&mut self, from: &str, to: &str, now: SystemTime) -> bool {\n        let key_type = match self.keys.remove(from) {\n            Some(t) => t,\n            None => return false,\n        };\n\n        // Remove destination if it exists\n        self.del(to);\n\n        // Move the type tag\n        self.keys.insert(to.to_owned(), key_type);\n\n        // Move the actual data\n        match key_type {\n            KeyType::String => {\n                if let Some(v) = self.string_keys.remove(from) {\n                    self.string_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::Hash => {\n                if let Some(v) = self.hash_keys.remove(from) {\n                    self.hash_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::List => {\n                if let Some(v) = self.list_keys.remove(from) {\n                    self.list_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::Set => {\n                if let Some(v) = self.set_keys.remove(from) {\n                    self.set_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::SortedSet => {\n                if let Some(v) = self.sorted_set_keys.remove(from) {\n                    self.sorted_set_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::Stream => {\n                if let Some(v) = self.stream_keys.remove(from) {\n                    self.stream_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::HyperLogLog => {\n                if let Some(v) = self.hll_keys.remove(from) {\n                    self.hll_keys.insert(to.to_owned(), v);\n                }\n            }\n        }\n\n        // Move TTL\n        if let Some(ttl) = self.ttl.remove(from) {\n            self.ttl.insert(to.to_owned(), ttl);\n        }\n\n        // Move hash field TTLs\n        if let Some(field_ttls) = self.hash_field_ttls.remove(from) {\n            self.hash_field_ttls.insert(to.to_owned(), field_ttls);\n        }\n\n        // Move LRU\n        if let Some(lru) = self.lru.remove(from) {\n            self.lru.insert(to.to_owned(), lru);\n        }\n\n        // Update versions\n        self.incr_version(from, now);\n        self.incr_version(to, now);\n\n        true\n    }\n\n    /// Check and delete a key if its TTL has expired.\n    pub fn check_ttl(&mut self, key: &str) -> bool {\n        if let Some(&ttl) = self.ttl.get(key)\n            && ttl <= Duration::ZERO\n        {\n            self.del(key);\n            return true; // key was expired\n        }\n        false // key still alive or has no TTL\n    }\n\n    /// Remove all keys and values.\n    pub fn flush(&mut self) {\n        self.keys.clear();\n        self.string_keys.clear();\n        self.hash_keys.clear();\n        self.list_keys.clear();\n        self.set_keys.clear();\n        self.sorted_set_keys.clear();\n        self.stream_keys.clear();\n        self.hll_keys.clear();\n        self.ttl.clear();\n        self.hash_field_ttls.clear();\n        self.key_version.clear();\n        self.lru.clear();\n    }\n\n    /// Deep-copy a key's data (type, value, TTL) within the same DB. Returns true on success.\n    pub fn copy_key(&mut self, from: &str, to: &str, now: SystemTime) -> bool {\n        let key_type = match self.keys.get(from) {\n            Some(t) => *t,\n            None => return false,\n        };\n\n        self.keys.insert(to.to_owned(), key_type);\n\n        match key_type {\n            KeyType::String => {\n                if let Some(v) = self.string_keys.get(from).cloned() {\n                    self.string_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::Hash => {\n                if let Some(v) = self.hash_keys.get(from).cloned() {\n                    self.hash_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::List => {\n                if let Some(v) = self.list_keys.get(from).cloned() {\n                    self.list_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::Set => {\n                if let Some(v) = self.set_keys.get(from).cloned() {\n                    self.set_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::SortedSet => {\n                if let Some(v) = self.sorted_set_keys.get(from).cloned() {\n                    self.sorted_set_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::Stream => {\n                if let Some(v) = self.stream_keys.get(from).cloned() {\n                    self.stream_keys.insert(to.to_owned(), v);\n                }\n            }\n            KeyType::HyperLogLog => {\n                if let Some(v) = self.hll_keys.get(from).cloned() {\n                    self.hll_keys.insert(to.to_owned(), v);\n                }\n            }\n        }\n\n        // Copy TTL\n        if let Some(ttl) = self.ttl.get(from).copied() {\n            self.ttl.insert(to.to_owned(), ttl);\n        }\n\n        // Copy hash field TTLs\n        if let Some(field_ttls) = self.hash_field_ttls.get(from).cloned() {\n            self.hash_field_ttls.insert(to.to_owned(), field_ttls);\n        }\n\n        self.incr_version(to, now);\n        true\n    }\n\n    /// Return all keys, sorted.\n    pub fn all_keys(&self) -> Vec<String> {\n        let mut keys: Vec<String> = self.keys.keys().cloned().collect();\n        keys.sort();\n        keys\n    }\n\n    /// Decrease all TTLs by `duration`, deleting expired keys.\n    pub fn fast_forward(&mut self, duration: Duration) {\n        let keys: Vec<String> = self.ttl.keys().cloned().collect();\n        for key in keys {\n            if let Some(ttl) = self.ttl.get_mut(&key) {\n                *ttl = ttl.saturating_sub(duration);\n            }\n            self.check_ttl(&key);\n        }\n\n        // Handle hash field TTLs\n        let hash_keys: Vec<String> = self.hash_field_ttls.keys().cloned().collect();\n        for key in hash_keys {\n            self.check_hash_field_ttls(&key, duration);\n        }\n    }\n\n    /// Check and expire hash field TTLs. Removes expired fields, and if\n    /// the hash becomes empty, deletes the key entirely.\n    pub fn check_hash_field_ttls(&mut self, key: &str, duration: Duration) {\n        let field_ttls = match self.hash_field_ttls.get_mut(key) {\n            Some(t) => t,\n            None => return,\n        };\n\n        let mut expired_fields = Vec::new();\n        for (field, ttl) in field_ttls.iter_mut() {\n            *ttl = ttl.saturating_sub(duration);\n            if *ttl <= Duration::ZERO {\n                expired_fields.push(field.clone());\n            }\n        }\n\n        for field in &expired_fields {\n            field_ttls.remove(field);\n            if let Some(hash) = self.hash_keys.get_mut(key) {\n                hash.remove(field);\n            }\n        }\n\n        if field_ttls.is_empty() {\n            self.hash_field_ttls.remove(key);\n        }\n\n        // If hash is now empty, delete the key entirely\n        if let Some(hash) = self.hash_keys.get(key)\n            && hash.is_empty()\n        {\n            self.del(key);\n        }\n    }\n}\n\n/// The inner state shared across all connections.\n/// Protected by a `std::sync::Mutex` (never held across .await).\n#[derive(Debug)]\npub struct Inner {\n    /// 16 databases (0-15).\n    pub dbs: Vec<RedisDB>,\n    /// Cached Lua scripts: SHA1 hex -> source.\n    pub scripts: HashMap<String, String>,\n    /// AUTH passwords: username -> password.\n    pub passwords: HashMap<String, String>,\n    /// Mock time. If None, use real time.\n    pub now: Option<SystemTime>,\n    /// Seeded RNG for deterministic tests.\n    pub rng: StdRng,\n}\n\nimpl Default for Inner {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl Inner {\n    pub fn new() -> Self {\n        let mut dbs = Vec::with_capacity(16);\n        for _ in 0..16 {\n            dbs.push(RedisDB::new());\n        }\n        Inner {\n            dbs,\n            scripts: HashMap::new(),\n            passwords: HashMap::new(),\n            now: None,\n            rng: StdRng::from_os_rng(),\n        }\n    }\n\n    /// Get the effective \"now\" time (mock or real).\n    pub fn effective_now(&self) -> SystemTime {\n        self.now.unwrap_or_else(SystemTime::now)\n    }\n\n    /// Advance mock time and expire keys in all databases.\n    pub fn fast_forward(&mut self, duration: Duration) {\n        if let Some(ref mut now) = self.now {\n            *now += duration;\n        }\n        for db in &mut self.dbs {\n            db.fast_forward(duration);\n        }\n    }\n\n    /// Get a reference to a database.\n    pub fn db(&self, idx: usize) -> &RedisDB {\n        &self.dbs[idx]\n    }\n\n    /// Get a mutable reference to a database.\n    pub fn db_mut(&mut self, idx: usize) -> &mut RedisDB {\n        &mut self.dbs[idx]\n    }\n}\n\n/// The shared state wrapper used across all connections.\n/// `inner` is a std::sync::Mutex (not tokio::sync::Mutex) because we never\n/// hold the lock across an .await point.\npub struct SharedState {\n    /// The inner database state.\n    pub inner: std::sync::Mutex<Inner>,\n    /// Notifies blocking commands (BLPOP, XREAD BLOCK, etc.) when data changes.\n    pub notify: Notify,\n    /// Shutdown signal broadcaster.\n    pub shutdown_tx: broadcast::Sender<()>,\n    /// Total connections received (cumulative).\n    pub total_connections_received: AtomicU64,\n    /// Currently connected clients.\n    pub connected_clients: AtomicU64,\n    /// Total commands processed (cumulative).\n    pub total_commands_processed: AtomicU64,\n    /// Pub/Sub subscriber registry.\n    pub pubsub: std::sync::Mutex<crate::pubsub::PubsubRegistry>,\n    /// Command dispatch table (set once at server startup, used by Lua scripting).\n    pub command_table: std::sync::OnceLock<Arc<crate::dispatch::CommandTable>>,\n}\n\nimpl SharedState {\n    pub fn new() -> Arc<Self> {\n        let (shutdown_tx, _) = broadcast::channel(1);\n        Arc::new(SharedState {\n            inner: std::sync::Mutex::new(Inner::new()),\n            notify: Notify::new(),\n            shutdown_tx,\n            total_connections_received: AtomicU64::new(0),\n            connected_clients: AtomicU64::new(0),\n            total_commands_processed: AtomicU64::new(0),\n            pubsub: std::sync::Mutex::new(crate::pubsub::PubsubRegistry::new()),\n            command_table: std::sync::OnceLock::new(),\n        })\n    }\n\n    /// Lock the inner state.\n    pub fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {\n        self.inner.lock().unwrap()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_redis_db_string_set_get() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"hello\", b\"world\".to_vec(), now);\n        assert_eq!(db.string_get(\"hello\"), Some(&b\"world\".to_vec()));\n        assert_eq!(db.key_type(\"hello\"), Some(KeyType::String));\n    }\n\n    #[test]\n    fn test_redis_db_del() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"key\", b\"val\".to_vec(), now);\n        assert!(db.exists(\"key\", now));\n        assert!(db.del(\"key\"));\n        assert!(!db.exists(\"key\", now));\n        assert_eq!(db.string_get(\"key\"), None);\n    }\n\n    #[test]\n    fn test_redis_db_del_nonexistent() {\n        let mut db = RedisDB::new();\n        assert!(!db.del(\"nope\"));\n    }\n\n    #[test]\n    fn test_redis_db_type_check() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"str\", b\"val\".to_vec(), now);\n        assert_eq!(db.key_type(\"str\"), Some(KeyType::String));\n        assert_eq!(db.key_type(\"nonexistent\"), None);\n    }\n\n    #[test]\n    fn test_redis_db_ttl_expiration() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"ephemeral\", b\"data\".to_vec(), now);\n        db.ttl\n            .insert(\"ephemeral\".to_owned(), Duration::from_secs(10));\n\n        // Fast forward 5s -- key should still be alive\n        db.fast_forward(Duration::from_secs(5));\n        assert!(db.keys.contains_key(\"ephemeral\"));\n\n        // Fast forward another 6s -- key should be gone\n        db.fast_forward(Duration::from_secs(6));\n        assert!(!db.keys.contains_key(\"ephemeral\"));\n        assert_eq!(db.string_get(\"ephemeral\"), None);\n    }\n\n    #[test]\n    fn test_redis_db_flush() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"a\", b\"1\".to_vec(), now);\n        db.string_set(\"b\", b\"2\".to_vec(), now);\n        assert_eq!(db.keys.len(), 2);\n\n        db.flush();\n        assert!(db.keys.is_empty());\n        assert!(db.string_keys.is_empty());\n    }\n\n    #[test]\n    fn test_redis_db_all_keys_sorted() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"charlie\", b\"3\".to_vec(), now);\n        db.string_set(\"alpha\", b\"1\".to_vec(), now);\n        db.string_set(\"bravo\", b\"2\".to_vec(), now);\n\n        assert_eq!(db.all_keys(), vec![\"alpha\", \"bravo\", \"charlie\"]);\n    }\n\n    #[test]\n    fn test_redis_db_key_version() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"k\", b\"v1\".to_vec(), now);\n        let v1 = db.key_version[\"k\"];\n\n        db.string_set(\"k\", b\"v2\".to_vec(), now);\n        let v2 = db.key_version[\"k\"];\n\n        assert!(v2 > v1);\n    }\n\n    #[test]\n    fn test_redis_db_overwrite_type() {\n        let mut db = RedisDB::new();\n        let now = SystemTime::now();\n\n        db.string_set(\"key\", b\"val\".to_vec(), now);\n        assert_eq!(db.key_type(\"key\"), Some(KeyType::String));\n\n        db.string_set(\"key\", b\"new_val\".to_vec(), now);\n        assert_eq!(db.string_get(\"key\"), Some(&b\"new_val\".to_vec()));\n    }\n\n    #[test]\n    fn test_inner_16_dbs() {\n        let inner = Inner::new();\n        assert_eq!(inner.dbs.len(), 16);\n    }\n\n    #[test]\n    fn test_inner_effective_now_real() {\n        let inner = Inner::new();\n        let now = inner.effective_now();\n        let real_now = SystemTime::now();\n        let diff = real_now.duration_since(now).unwrap_or(Duration::ZERO);\n        assert!(diff < Duration::from_secs(1));\n    }\n\n    #[test]\n    fn test_inner_effective_now_mock() {\n        let mut inner = Inner::new();\n        let mock_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);\n        inner.now = Some(mock_time);\n        assert_eq!(inner.effective_now(), mock_time);\n    }\n\n    #[test]\n    fn test_shared_state_lock() {\n        let state = SharedState::new();\n        {\n            let mut inner = state.lock();\n            inner\n                .db_mut(0)\n                .string_set(\"test\", b\"value\".to_vec(), SystemTime::now());\n        }\n        {\n            let inner = state.lock();\n            assert_eq!(inner.db(0).string_get(\"test\"), Some(&b\"value\".to_vec()));\n        }\n    }\n}\n"
  },
  {
    "path": "miniredis/src/dispatch.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt;\nuse std::sync::Arc;\n\nuse crate::connection::ConnCtx;\nuse crate::db::SharedState;\nuse crate::frame::Frame;\n\n// ── Error message constants ──────────────────────────────────────────\n\npub const MSG_WRONG_TYPE: &str =\n    \"WRONGTYPE Operation against a key holding the wrong kind of value\";\npub const MSG_INVALID_INT: &str = \"ERR value is not an integer or out of range\";\npub const MSG_INT_OVERFLOW: &str = \"ERR increment or decrement would overflow\";\npub const MSG_INVALID_FLOAT: &str = \"ERR value is not a valid float\";\npub const MSG_INVALID_MIN_MAX: &str = \"ERR min or max is not a float\";\npub const MSG_INVALID_RANGE_ITEM: &str = \"ERR min or max not valid string range item\";\npub const MSG_INVALID_TIMEOUT: &str = \"ERR timeout is not a float or out of range\";\npub const MSG_SYNTAX_ERROR: &str = \"ERR syntax error\";\npub const MSG_KEY_NOT_FOUND: &str = \"ERR no such key\";\npub const MSG_OUT_OF_RANGE: &str = \"ERR index out of range\";\npub const MSG_INVALID_CURSOR: &str = \"ERR invalid cursor\";\npub const MSG_XX_AND_NX: &str = \"ERR XX and NX options at the same time are not compatible\";\npub const MSG_TIMEOUT_NEGATIVE: &str = \"ERR timeout is negative\";\npub const MSG_INVALID_SE_TIME: &str = \"ERR invalid expire time in set\";\npub const MSG_INVALID_SETEX_TIME: &str = \"ERR invalid expire time in setex\";\npub const MSG_INVALID_PSETEX_TIME: &str = \"ERR invalid expire time in psetex\";\npub const MSG_INVALID_KEYS_NUMBER: &str = \"ERR Number of keys can't be greater than number of args\";\npub const MSG_NEGATIVE_KEYS_NUMBER: &str = \"ERR Number of keys can't be negative\";\npub const MSG_NO_SCRIPT_FOUND: &str = \"NOSCRIPT No matching script. Please use EVAL.\";\npub const MSG_DB_INDEX_OUT_OF_RANGE: &str = \"ERR DB index is out of range\";\npub const MSG_SINGLE_ELEMENT_PAIR: &str =\n    \"ERR INCR option supports a single increment-element pair\";\npub const MSG_NOT_VALID_HLL_VALUE: &str = \"WRONGTYPE Key is not a valid HyperLogLog string value.\";\npub const MSG_INVALID_RANGE: &str = \"ERR value is out of range, must be positive\";\npub const MSG_TIMEOUT_IS_OUT_OF_RANGE: &str = \"ERR timeout is out of range\";\npub const MSG_GT_LT_AND_NX: &str =\n    \"ERR GT, LT, and/or NX options at the same time are not compatible\";\npub const MSG_INVALID_STREAM_ID: &str =\n    \"ERR Invalid stream ID specified as stream command argument\";\npub const MSG_STREAM_ID_TOO_SMALL: &str =\n    \"ERR The ID specified in XADD is equal or smaller than the target stream top item\";\npub const MSG_STREAM_ID_ZERO: &str = \"ERR The ID specified in XADD must be greater than 0-0\";\npub const MSG_UNSUPPORTED_UNIT: &str = \"ERR unsupported unit provided. please use M, KM, FT, MI\";\npub const MSG_XGROUP_KEY_NOT_FOUND: &str = \"ERR The XGROUP subcommand requires the key to exist. Note that for CREATE you may want to use the MKSTREAM option to create an empty stream automatically.\";\npub const MSG_LIMIT_COMBINATION: &str =\n    \"ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX\";\npub const MSG_GT_AND_LT: &str = \"ERR GT and LT options at the same time are not compatible\";\npub const MSG_NX_AND_XX_GT_LT: &str =\n    \"ERR NX and XX, GT or LT options at the same time are not compatible\";\npub const MSG_NUM_FIELDS_INVALID: &str = \"ERR Parameter `numFields` should be greater than 0\";\npub const MSG_NUM_FIELDS_PARAMETER: &str =\n    \"ERR The `numfields` parameter must match the number of arguments\";\n\n/// Generate the \"wrong number of arguments\" error for a command.\npub fn err_wrong_number(cmd: &str) -> String {\n    format!(\n        \"ERR wrong number of arguments for '{}' command\",\n        cmd.to_lowercase()\n    )\n}\n\n/// Generate the \"unknown command\" error.\npub fn err_unknown_command(cmd: &str, args: &[Vec<u8>]) -> String {\n    let mut s = format!(\"ERR unknown command `{}`, with args beginning with: \", cmd);\n    for (i, a) in args.iter().enumerate() {\n        if i >= 20 {\n            break;\n        }\n        let a_str = String::from_utf8_lossy(a);\n        s.push_str(&format!(\"`{}`, \", a_str));\n    }\n    s\n}\n\n// ── Command handler types ────────────────────────────────────────────\n\n/// The signature for a command handler function.\n/// Takes shared state, per-connection context, and the raw arguments.\n/// Returns a Frame to send as the response.\npub type CommandHandler =\n    fn(state: &Arc<SharedState>, ctx: &mut ConnCtx, args: &[Vec<u8>]) -> Frame;\n\n/// Metadata about a registered command.\npub struct CommandMeta {\n    pub handler: CommandHandler,\n    pub read_only: bool,\n    /// Redis arity. Positive = exact arg count (including cmd name).\n    /// Negative = minimum arg count. Zero = skip check.\n    pub arity: i32,\n}\n\n/// The command dispatch table.\npub struct CommandTable {\n    commands: HashMap<&'static str, CommandMeta>,\n}\n\nimpl Default for CommandTable {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl CommandTable {\n    pub fn new() -> Self {\n        let mut table = CommandTable {\n            commands: HashMap::new(),\n        };\n\n        // Register all implemented commands.\n        crate::cmd::connection::register(&mut table);\n        crate::cmd::string::register(&mut table);\n        crate::cmd::generic::register(&mut table);\n        crate::cmd::server::register(&mut table);\n        crate::cmd::hash::register(&mut table);\n        crate::cmd::list::register(&mut table);\n        crate::cmd::set::register(&mut table);\n        crate::cmd::sorted_set::register(&mut table);\n        crate::cmd::transactions::register(&mut table);\n        crate::cmd::hll::register(&mut table);\n        crate::cmd::geo::register(&mut table);\n        crate::cmd::pubsub::register(&mut table);\n        crate::cmd::client::register(&mut table);\n        crate::cmd::cluster::register(&mut table);\n        crate::cmd::object::register(&mut table);\n        crate::cmd::stream::register(&mut table);\n        crate::cmd::scripting::register(&mut table);\n\n        table\n    }\n\n    /// Register a command with arity.\n    /// Arity follows Redis convention: positive = exact arg count (incl. cmd name),\n    /// negative = minimum arg count, zero = skip check.\n    pub fn add(\n        &mut self,\n        name: &'static str,\n        handler: CommandHandler,\n        read_only: bool,\n        arity: i32,\n    ) {\n        self.commands.insert(\n            name,\n            CommandMeta {\n                handler,\n                read_only,\n                arity,\n            },\n        );\n    }\n\n    /// Look up a command by name (uppercase).\n    pub fn get(&self, name: &str) -> Option<&CommandMeta> {\n        self.commands.get(name)\n    }\n}\n\nimpl fmt::Debug for CommandTable {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        f.debug_struct(\"CommandTable\")\n            .field(\"commands\", &self.commands.keys().collect::<Vec<_>>())\n            .finish()\n    }\n}\n\n// ── Dispatch logic ───────────────────────────────────────────────────\n\n/// Dispatch a single command. Returns the response frame, and whether the\n/// connection should be closed (QUIT).\npub fn dispatch(\n    table: &CommandTable,\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n) -> (Frame, bool) {\n    if args.is_empty() {\n        return (Frame::error(\"ERR empty command\"), false);\n    }\n\n    let cmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n    let cmd_args = &args[1..];\n\n    // Handle MULTI/EXEC/DISCARD specially — they're not queued.\n    // Note: the MULTI path only runs when authenticated (you can't enter MULTI\n    // without being authenticated), so no auth check is needed here.\n    if ctx.in_tx() && cmd != \"EXEC\" && cmd != \"DISCARD\" && cmd != \"MULTI\" && cmd != \"WATCH\" {\n        // Validate the command exists before queueing\n        let meta = match table.get(&cmd) {\n            Some(m) => m,\n            None => {\n                ctx.dirty_transaction = true;\n                return (Frame::error(err_unknown_command(&cmd, cmd_args)), false);\n            }\n        };\n\n        // Validate arity before queueing (Redis checks this before QUEUED)\n        if meta.arity != 0 {\n            let n = args.len() as i32;\n            let bad = if meta.arity > 0 {\n                n != meta.arity\n            } else {\n                n < -meta.arity\n            };\n            if bad {\n                ctx.dirty_transaction = true;\n                return (Frame::error(err_wrong_number(&cmd.to_lowercase())), false);\n            }\n        }\n\n        // Special case: validate SCRIPT subcommands before queueing.\n        // Real Redis (and Go miniredis) rejects unknown subcommands immediately.\n        if cmd == \"SCRIPT\" && !cmd_args.is_empty() {\n            let subcmd = String::from_utf8_lossy(&cmd_args[0]).to_uppercase();\n            if ![\"LOAD\", \"EXISTS\", \"FLUSH\"].contains(&subcmd.as_str()) {\n                ctx.dirty_transaction = true;\n                return (\n                    Frame::error(format!(\n                        \"ERR unknown subcommand '{}'. Try SCRIPT HELP.\",\n                        subcmd\n                    )),\n                    false,\n                );\n            }\n        }\n\n        // Special case: validate OBJECT subcommand arity before queueing.\n        if cmd == \"OBJECT\" && !cmd_args.is_empty() {\n            let subcmd = String::from_utf8_lossy(&cmd_args[0]).to_uppercase();\n            match subcmd.as_str() {\n                \"ENCODING\" | \"IDLETIME\" | \"REFCOUNT\" | \"FREQ\" => {\n                    // These subcommands require exactly 1 additional arg (the key)\n                    if cmd_args.len() != 2 {\n                        ctx.dirty_transaction = true;\n                        return (\n                            Frame::error(err_wrong_number(&format!(\n                                \"object|{}\",\n                                subcmd.to_lowercase()\n                            ))),\n                            false,\n                        );\n                    }\n                }\n                \"HELP\" => {}\n                _ => {\n                    ctx.dirty_transaction = true;\n                    return (\n                        Frame::error(format!(\n                            \"ERR unknown subcommand or wrong number of arguments for 'object|{}' command\",\n                            subcmd.to_lowercase()\n                        )),\n                        false,\n                    );\n                }\n            }\n        }\n\n        // Queue the command\n        if let Some(ref mut tx) = ctx.transaction {\n            tx.push(crate::connection::QueuedCommand {\n                args: args.to_vec(),\n            });\n        }\n        return (Frame::Simple(\"QUEUED\".into()), false);\n    }\n\n    // Handle EXEC specially — it needs the command table to replay queued commands.\n    if cmd == \"EXEC\" {\n        return (cmd_exec(table, state, ctx, cmd_args), false);\n    }\n\n    // Look up the command\n    let meta = match table.get(&cmd) {\n        Some(m) => m,\n        None => {\n            // Unknown commands: check auth before returning unknown error\n            if !ctx.authenticated {\n                let inner = state.lock();\n                if !inner.passwords.is_empty() {\n                    return (Frame::error(\"NOAUTH Authentication required.\"), false);\n                }\n            }\n            return (Frame::error(err_unknown_command(&cmd, cmd_args)), false);\n        }\n    };\n\n    // Validate arity before auth (Redis checks arity first)\n    if meta.arity != 0 {\n        let n = args.len() as i32;\n        let bad = if meta.arity > 0 {\n            n != meta.arity\n        } else {\n            n < -meta.arity\n        };\n        if bad {\n            return (Frame::error(err_wrong_number(&cmd.to_lowercase())), false);\n        }\n    }\n\n    // Check auth (after arity validation)\n    if !ctx.authenticated {\n        let inner = state.lock();\n        if !inner.passwords.is_empty() && cmd != \"AUTH\" && cmd != \"HELLO\" && cmd != \"QUIT\" {\n            return (Frame::error(\"NOAUTH Authentication required.\"), false);\n        }\n    }\n\n    // Execute the command under the lock\n    let response = with_lock(state, ctx, meta.handler, cmd_args);\n    let should_close = cmd == \"QUIT\";\n\n    (response, should_close)\n}\n\n/// Execute a command handler under the database lock.\n/// This is the normal (non-MULTI) path: lock → execute → notify → unlock.\nfn with_lock(\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    handler: CommandHandler,\n    args: &[Vec<u8>],\n) -> Frame {\n    let response = handler(state, ctx, args);\n\n    // Notify any blocking commands that data may have changed.\n    state.notify.notify_waiters();\n\n    response\n}\n\n/// EXEC — execute a queued transaction.\n/// Handled in dispatch because it needs access to the command table.\nfn cmd_exec(\n    table: &CommandTable,\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    args: &[Vec<u8>],\n) -> Frame {\n    if !args.is_empty() {\n        return Frame::error(err_wrong_number(\"exec\"));\n    }\n    if !ctx.in_tx() {\n        return Frame::error(\"ERR EXEC without MULTI\");\n    }\n\n    // Dirty transaction (e.g. unknown command was queued) — abort.\n    if ctx.dirty_transaction {\n        ctx.transaction = None;\n        ctx.watch.clear();\n        return Frame::error(\"EXECABORT Transaction discarded because of previous errors.\");\n    }\n\n    // Check WATCHed keys.\n    {\n        let inner = state.lock();\n        for ((db_idx, key), version) in &ctx.watch {\n            let current = inner.db(*db_idx).key_version.get(key).copied().unwrap_or(0);\n            if current > *version {\n                // WATCH detected a change — abort.\n                ctx.transaction = None;\n                ctx.watch.clear();\n                return Frame::NullArray;\n            }\n        }\n    }\n\n    // Take the queued commands and clear transaction state.\n    let commands = ctx.transaction.take().unwrap_or_default();\n    ctx.watch.clear();\n\n    // Execute each queued command and collect results.\n    let mut results = Vec::with_capacity(commands.len());\n    for queued in &commands {\n        let cmd_name = String::from_utf8_lossy(&queued.args[0]).to_uppercase();\n        let cmd_args = &queued.args[1..];\n\n        let meta = match table.get(&cmd_name) {\n            Some(m) => m,\n            None => {\n                results.push(Frame::error(err_unknown_command(&cmd_name, cmd_args)));\n                continue;\n            }\n        };\n\n        let result = (meta.handler)(state, ctx, cmd_args);\n        results.push(result);\n    }\n\n    // Notify any blocking commands that data may have changed.\n    state.notify.notify_waiters();\n\n    Frame::Array(results)\n}\n"
  },
  {
    "path": "miniredis/src/error.rs",
    "content": "/// Error type for miniredis-rs.\npub type Error = Box<dyn std::error::Error + Send + Sync>;\n\n/// Result type for miniredis-rs.\npub type Result<T> = std::result::Result<T, Error>;\n"
  },
  {
    "path": "miniredis/src/frame.rs",
    "content": "use bytes::{Buf, Bytes};\nuse std::fmt;\nuse std::io::Cursor;\n\n/// A RESP2/RESP3 protocol frame.\n#[derive(Clone, Debug, PartialEq)]\npub enum Frame {\n    /// Simple string: `+OK\\r\\n`\n    Simple(String),\n    /// Error: `-ERR message\\r\\n`\n    Error(String),\n    /// Integer: `:42\\r\\n`\n    Integer(i64),\n    /// Bulk string: `$5\\r\\nhello\\r\\n`\n    Bulk(Bytes),\n    /// Null: `$-1\\r\\n` (RESP2) or `_\\r\\n` (RESP3)\n    Null,\n    /// Null array: `*-1\\r\\n` (RESP2) or `_\\r\\n` (RESP3)\n    NullArray,\n    /// Array: `*2\\r\\n...`\n    Array(Vec<Frame>),\n    // ── RESP3 types ──────────────────────────────────────────────────\n    /// Map: `%N\\r\\n...` (RESP3) or flat array `*2N\\r\\n...` (RESP2 fallback)\n    Map(Vec<(Frame, Frame)>),\n    /// Set: `~N\\r\\n...` (RESP3) or array `*N\\r\\n...` (RESP2 fallback)\n    Set(Vec<Frame>),\n    /// Push: `>N\\r\\n...` (RESP3) or array `*N\\r\\n...` (RESP2 fallback)\n    Push(Vec<Frame>),\n    /// Double: `,3.14\\r\\n` (RESP3) or bulk string (RESP2 fallback)\n    Double(f64),\n}\n\n/// Errors that can occur during frame parsing.\n#[derive(Debug)]\npub enum FrameError {\n    /// Not enough data is available to parse a full frame.\n    Incomplete,\n    /// Invalid frame data.\n    Protocol(String),\n}\n\nimpl Frame {\n    // ── Response builder helpers ──────────────────────────────────────\n\n    /// `+OK\\r\\n`\n    pub fn ok() -> Frame {\n        Frame::Simple(\"OK\".into())\n    }\n\n    /// `-{msg}\\r\\n`\n    pub fn error(msg: impl Into<String>) -> Frame {\n        Frame::Error(msg.into())\n    }\n\n    /// `:{n}\\r\\n`\n    pub fn integer(n: i64) -> Frame {\n        Frame::Integer(n)\n    }\n\n    /// Bulk string from bytes.\n    pub fn bulk(data: impl Into<Bytes>) -> Frame {\n        Frame::Bulk(data.into())\n    }\n\n    /// Bulk string from a str.\n    pub fn bulk_string(s: &str) -> Frame {\n        Frame::Bulk(Bytes::from(s.to_owned()))\n    }\n\n    /// `$-1\\r\\n`\n    pub fn null() -> Frame {\n        Frame::Null\n    }\n\n    /// `*-1\\r\\n`\n    pub fn null_array() -> Frame {\n        Frame::NullArray\n    }\n\n    /// Build an Array of bulk strings.\n    pub fn strings(strs: &[&str]) -> Frame {\n        Frame::Array(\n            strs.iter()\n                .map(|s| Frame::Bulk(Bytes::from(s.to_string())))\n                .collect(),\n        )\n    }\n\n    // ── Validation (no allocation) ───────────────────────────────────\n\n    /// Check whether a complete frame can be read from `src` without\n    /// allocating. Advances the cursor past the frame on success.\n    pub fn check(src: &mut Cursor<&[u8]>) -> Result<(), FrameError> {\n        match get_u8(src)? {\n            b'+' | b'-' | b':' => {\n                skip_line(src)?;\n                Ok(())\n            }\n            b'$' => {\n                let len = get_line_as_int(src)?;\n                if len < 0 {\n                    // Null bulk string `$-1\\r\\n`\n                    Ok(())\n                } else {\n                    // Skip `len` bytes + `\\r\\n`\n                    let len = len as usize;\n                    skip(src, len + 2)?;\n                    Ok(())\n                }\n            }\n            b'*' => {\n                let count = get_line_as_int(src)?;\n                if count < 0 {\n                    // Null array\n                    Ok(())\n                } else {\n                    for _ in 0..count {\n                        Frame::check(src)?;\n                    }\n                    Ok(())\n                }\n            }\n            // RESP3: Map\n            b'%' => {\n                let count = get_line_as_int(src)?;\n                for _ in 0..count {\n                    Frame::check(src)?; // key\n                    Frame::check(src)?; // value\n                }\n                Ok(())\n            }\n            // RESP3: Set or Push\n            b'~' | b'>' => {\n                let count = get_line_as_int(src)?;\n                for _ in 0..count {\n                    Frame::check(src)?;\n                }\n                Ok(())\n            }\n            // RESP3: Double\n            b',' => {\n                skip_line(src)?;\n                Ok(())\n            }\n            // RESP3: Null\n            b'_' => {\n                skip_line(src)?;\n                Ok(())\n            }\n            b => Err(FrameError::Protocol(format!(\n                \"invalid frame type byte: `{}`\",\n                b as char\n            ))),\n        }\n    }\n\n    // ── Parsing (allocates) ──────────────────────────────────────────\n\n    /// Parse a single frame from `src`.\n    pub fn parse(src: &mut Cursor<&[u8]>) -> Result<Frame, FrameError> {\n        match get_u8(src)? {\n            b'+' => {\n                let line = get_line(src)?;\n                let s = String::from_utf8(line.to_vec())\n                    .map_err(|e| FrameError::Protocol(e.to_string()))?;\n                Ok(Frame::Simple(s))\n            }\n            b'-' => {\n                let line = get_line(src)?;\n                let s = String::from_utf8(line.to_vec())\n                    .map_err(|e| FrameError::Protocol(e.to_string()))?;\n                Ok(Frame::Error(s))\n            }\n            b':' => {\n                let n = get_line_as_int(src)?;\n                Ok(Frame::Integer(n))\n            }\n            b'$' => {\n                let len = get_line_as_int(src)?;\n                if len < 0 {\n                    Ok(Frame::Null)\n                } else {\n                    let len = len as usize;\n                    if src.remaining() < len + 2 {\n                        return Err(FrameError::Incomplete);\n                    }\n                    let data =\n                        Bytes::copy_from_slice(&src.get_ref()[src.position() as usize..][..len]);\n                    skip(src, len + 2)?;\n                    Ok(Frame::Bulk(data))\n                }\n            }\n            b'*' => {\n                let count = get_line_as_int(src)?;\n                if count < 0 {\n                    Ok(Frame::Null)\n                } else {\n                    let mut frames = Vec::with_capacity(count as usize);\n                    for _ in 0..count {\n                        frames.push(Frame::parse(src)?);\n                    }\n                    Ok(Frame::Array(frames))\n                }\n            }\n            // RESP3: Map\n            b'%' => {\n                let count = get_line_as_int(src)?;\n                let mut pairs = Vec::with_capacity(count as usize);\n                for _ in 0..count {\n                    let key = Frame::parse(src)?;\n                    let value = Frame::parse(src)?;\n                    pairs.push((key, value));\n                }\n                Ok(Frame::Map(pairs))\n            }\n            // RESP3: Set\n            b'~' => {\n                let count = get_line_as_int(src)?;\n                let mut items = Vec::with_capacity(count as usize);\n                for _ in 0..count {\n                    items.push(Frame::parse(src)?);\n                }\n                Ok(Frame::Set(items))\n            }\n            // RESP3: Push\n            b'>' => {\n                let count = get_line_as_int(src)?;\n                let mut items = Vec::with_capacity(count as usize);\n                for _ in 0..count {\n                    items.push(Frame::parse(src)?);\n                }\n                Ok(Frame::Push(items))\n            }\n            // RESP3: Double\n            b',' => {\n                let line = get_line(src)?;\n                let s =\n                    std::str::from_utf8(line).map_err(|e| FrameError::Protocol(e.to_string()))?;\n                let f: f64 = match s {\n                    \"inf\" => f64::INFINITY,\n                    \"-inf\" => f64::NEG_INFINITY,\n                    \"nan\" => f64::NAN,\n                    _ => s.parse().map_err(|e: std::num::ParseFloatError| {\n                        FrameError::Protocol(e.to_string())\n                    })?,\n                };\n                Ok(Frame::Double(f))\n            }\n            // RESP3: Null\n            b'_' => {\n                skip_line(src)?;\n                Ok(Frame::Null)\n            }\n            b => Err(FrameError::Protocol(format!(\n                \"invalid frame type byte: `{}`\",\n                b as char\n            ))),\n        }\n    }\n\n    // ── Serialization ────────────────────────────────────────────────\n\n    /// Serialize this frame as RESP2 into a byte vector.\n    pub fn serialize(&self) -> Vec<u8> {\n        let mut buf = Vec::new();\n        self.write_to_buf(&mut buf, false);\n        buf\n    }\n\n    /// Serialize this frame into a byte vector, using RESP3 encoding if `resp3` is true.\n    pub fn serialize_resp(&self, resp3: bool) -> Vec<u8> {\n        let mut buf = Vec::new();\n        self.write_to_buf(&mut buf, resp3);\n        buf\n    }\n\n    /// Write this frame into a byte buffer.\n    /// When `resp3` is true, RESP3-specific types use their native wire format.\n    /// When false, they degrade to RESP2 equivalents.\n    pub fn write_to_buf(&self, buf: &mut Vec<u8>, resp3: bool) {\n        match self {\n            Frame::Simple(s) => {\n                buf.push(b'+');\n                buf.extend_from_slice(s.as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n            }\n            Frame::Error(s) => {\n                buf.push(b'-');\n                buf.extend_from_slice(s.as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n            }\n            Frame::Integer(n) => {\n                buf.push(b':');\n                buf.extend_from_slice(n.to_string().as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n            }\n            Frame::Bulk(data) => {\n                buf.push(b'$');\n                buf.extend_from_slice(data.len().to_string().as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n                buf.extend_from_slice(data);\n                buf.extend_from_slice(b\"\\r\\n\");\n            }\n            Frame::Null => {\n                if resp3 {\n                    buf.extend_from_slice(b\"_\\r\\n\");\n                } else {\n                    buf.extend_from_slice(b\"$-1\\r\\n\");\n                }\n            }\n            Frame::NullArray => {\n                if resp3 {\n                    buf.extend_from_slice(b\"_\\r\\n\");\n                } else {\n                    buf.extend_from_slice(b\"*-1\\r\\n\");\n                }\n            }\n            Frame::Array(frames) => {\n                buf.push(b'*');\n                buf.extend_from_slice(frames.len().to_string().as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n                for frame in frames {\n                    frame.write_to_buf(buf, resp3);\n                }\n            }\n            Frame::Map(pairs) => {\n                if resp3 {\n                    buf.push(b'%');\n                    buf.extend_from_slice(pairs.len().to_string().as_bytes());\n                    buf.extend_from_slice(b\"\\r\\n\");\n                } else {\n                    // RESP2 fallback: flat array with 2*N elements\n                    buf.push(b'*');\n                    buf.extend_from_slice((pairs.len() * 2).to_string().as_bytes());\n                    buf.extend_from_slice(b\"\\r\\n\");\n                }\n                for (k, v) in pairs {\n                    k.write_to_buf(buf, resp3);\n                    v.write_to_buf(buf, resp3);\n                }\n            }\n            Frame::Set(items) => {\n                if resp3 {\n                    buf.push(b'~');\n                } else {\n                    buf.push(b'*');\n                }\n                buf.extend_from_slice(items.len().to_string().as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n                for item in items {\n                    item.write_to_buf(buf, resp3);\n                }\n            }\n            Frame::Push(items) => {\n                if resp3 {\n                    buf.push(b'>');\n                } else {\n                    buf.push(b'*');\n                }\n                buf.extend_from_slice(items.len().to_string().as_bytes());\n                buf.extend_from_slice(b\"\\r\\n\");\n                for item in items {\n                    item.write_to_buf(buf, resp3);\n                }\n            }\n            Frame::Double(f) => {\n                let s = format_double(*f);\n                if resp3 {\n                    buf.push(b',');\n                    buf.extend_from_slice(s.as_bytes());\n                    buf.extend_from_slice(b\"\\r\\n\");\n                } else {\n                    // RESP2 fallback: bulk string\n                    buf.push(b'$');\n                    buf.extend_from_slice(s.len().to_string().as_bytes());\n                    buf.extend_from_slice(b\"\\r\\n\");\n                    buf.extend_from_slice(s.as_bytes());\n                    buf.extend_from_slice(b\"\\r\\n\");\n                }\n            }\n        }\n    }\n}\n\nimpl fmt::Display for Frame {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Frame::Simple(s) => write!(f, \"+{}\", s),\n            Frame::Error(s) => write!(f, \"-{}\", s),\n            Frame::Integer(n) => write!(f, \":{}\", n),\n            Frame::Bulk(data) => match std::str::from_utf8(data) {\n                Ok(s) => write!(f, \"${}\", s),\n                Err(_) => write!(f, \"${:?}\", data),\n            },\n            Frame::Null => write!(f, \"(nil)\"),\n            Frame::NullArray => write!(f, \"(nil)\"),\n            Frame::Array(frames) => {\n                write!(f, \"[\")?;\n                for (i, frame) in frames.iter().enumerate() {\n                    if i > 0 {\n                        write!(f, \", \")?;\n                    }\n                    write!(f, \"{}\", frame)?;\n                }\n                write!(f, \"]\")\n            }\n            Frame::Map(pairs) => {\n                write!(f, \"{{\")?;\n                for (i, (k, v)) in pairs.iter().enumerate() {\n                    if i > 0 {\n                        write!(f, \", \")?;\n                    }\n                    write!(f, \"{}: {}\", k, v)?;\n                }\n                write!(f, \"}}\")\n            }\n            Frame::Set(items) => {\n                write!(f, \"~[\")?;\n                for (i, item) in items.iter().enumerate() {\n                    if i > 0 {\n                        write!(f, \", \")?;\n                    }\n                    write!(f, \"{}\", item)?;\n                }\n                write!(f, \"]\")\n            }\n            Frame::Push(items) => {\n                write!(f, \">[\")?;\n                for (i, item) in items.iter().enumerate() {\n                    if i > 0 {\n                        write!(f, \", \")?;\n                    }\n                    write!(f, \"{}\", item)?;\n                }\n                write!(f, \"]\")\n            }\n            Frame::Double(v) => write!(f, \",{}\", format_double(*v)),\n        }\n    }\n}\n\n/// Format a f64 for RESP3 Double wire encoding.\nfn format_double(f: f64) -> String {\n    if f.is_infinite() {\n        if f.is_sign_positive() {\n            \"inf\".to_string()\n        } else {\n            \"-inf\".to_string()\n        }\n    } else if f.is_nan() {\n        \"nan\".to_string()\n    } else if f.fract() == 0.0 && f.abs() < 1e15 {\n        // Integer doubles: format without decimal point (Redis compat)\n        format!(\"{:.0}\", f)\n    } else {\n        // Use ryu for shortest representation\n        let mut buf = ryu::Buffer::new();\n        let s = buf.format(f);\n        s.to_string()\n    }\n}\n\nimpl std::error::Error for FrameError {}\n\nimpl fmt::Display for FrameError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            FrameError::Incomplete => write!(f, \"incomplete frame\"),\n            FrameError::Protocol(msg) => write!(f, \"protocol error: {}\", msg),\n        }\n    }\n}\n\n// ── Private helpers ──────────────────────────────────────────────────\n\n/// Peek at and consume a single byte.\nfn get_u8(src: &mut Cursor<&[u8]>) -> Result<u8, FrameError> {\n    if !src.has_remaining() {\n        return Err(FrameError::Incomplete);\n    }\n    Ok(src.get_u8())\n}\n\n/// Skip `n` bytes in the cursor.\nfn skip(src: &mut Cursor<&[u8]>, n: usize) -> Result<(), FrameError> {\n    if src.remaining() < n {\n        return Err(FrameError::Incomplete);\n    }\n    src.advance(n);\n    Ok(())\n}\n\n/// Read until `\\r\\n`, return the bytes before the delimiter.\n/// Advances the cursor past the `\\r\\n`.\nfn get_line<'a>(src: &mut Cursor<&'a [u8]>) -> Result<&'a [u8], FrameError> {\n    let start = src.position() as usize;\n    let end = src.get_ref().len();\n\n    for i in start..end.saturating_sub(1) {\n        if src.get_ref()[i] == b'\\r' && src.get_ref()[i + 1] == b'\\n' {\n            let line = &src.get_ref()[start..i];\n            src.set_position((i + 2) as u64);\n            return Ok(line);\n        }\n    }\n\n    Err(FrameError::Incomplete)\n}\n\n/// Skip until after `\\r\\n`.\nfn skip_line(src: &mut Cursor<&[u8]>) -> Result<(), FrameError> {\n    get_line(src)?;\n    Ok(())\n}\n\n/// Read a line and parse it as an i64.\nfn get_line_as_int(src: &mut Cursor<&[u8]>) -> Result<i64, FrameError> {\n    let line = get_line(src)?;\n    let s = std::str::from_utf8(line).map_err(|e| FrameError::Protocol(e.to_string()))?;\n    s.parse::<i64>()\n        .map_err(|e| FrameError::Protocol(e.to_string()))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn parse_simple_string() {\n        let data = b\"+OK\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Simple(\"OK\".into()));\n    }\n\n    #[test]\n    fn parse_error() {\n        let data = b\"-ERR unknown command\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Error(\"ERR unknown command\".into()));\n    }\n\n    #[test]\n    fn parse_integer() {\n        let data = b\":42\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Integer(42));\n    }\n\n    #[test]\n    fn parse_negative_integer() {\n        let data = b\":-1\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Integer(-1));\n    }\n\n    #[test]\n    fn parse_bulk_string() {\n        let data = b\"$5\\r\\nhello\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Bulk(Bytes::from(\"hello\")));\n    }\n\n    #[test]\n    fn parse_empty_bulk_string() {\n        let data = b\"$0\\r\\n\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Bulk(Bytes::from(\"\")));\n    }\n\n    #[test]\n    fn parse_null() {\n        let data = b\"$-1\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Null);\n    }\n\n    #[test]\n    fn parse_array() {\n        let data = b\"*2\\r\\n$3\\r\\nfoo\\r\\n$3\\r\\nbar\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(\n            frame,\n            Frame::Array(vec![\n                Frame::Bulk(Bytes::from(\"foo\")),\n                Frame::Bulk(Bytes::from(\"bar\")),\n            ])\n        );\n    }\n\n    #[test]\n    fn parse_empty_array() {\n        let data = b\"*0\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Array(vec![]));\n    }\n\n    #[test]\n    fn parse_nested_array() {\n        let data = b\"*2\\r\\n*2\\r\\n:1\\r\\n:2\\r\\n*1\\r\\n+OK\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(\n            frame,\n            Frame::Array(vec![\n                Frame::Array(vec![Frame::Integer(1), Frame::Integer(2)]),\n                Frame::Array(vec![Frame::Simple(\"OK\".into())]),\n            ])\n        );\n    }\n\n    #[test]\n    fn check_incomplete() {\n        let data = b\"$5\\r\\nhel\";\n        let mut cursor = Cursor::new(&data[..]);\n        assert!(matches!(\n            Frame::check(&mut cursor),\n            Err(FrameError::Incomplete)\n        ));\n    }\n\n    #[test]\n    fn check_complete() {\n        let data = b\"+OK\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        assert!(Frame::check(&mut cursor).is_ok());\n    }\n\n    #[test]\n    fn round_trip_simple() {\n        let frame = Frame::Simple(\"OK\".into());\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn round_trip_error() {\n        let frame = Frame::Error(\"ERR something went wrong\".into());\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn round_trip_integer() {\n        let frame = Frame::Integer(-999);\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn round_trip_bulk() {\n        let frame = Frame::Bulk(Bytes::from(\"hello world\"));\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn round_trip_null() {\n        let frame = Frame::Null;\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn round_trip_array() {\n        let frame = Frame::Array(vec![\n            Frame::Bulk(Bytes::from(\"SET\")),\n            Frame::Bulk(Bytes::from(\"key\")),\n            Frame::Bulk(Bytes::from(\"value\")),\n        ]);\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn round_trip_nested_array() {\n        let frame = Frame::Array(vec![\n            Frame::Integer(1),\n            Frame::Array(vec![Frame::Simple(\"inner\".into()), Frame::Null]),\n            Frame::Bulk(Bytes::from(\"end\")),\n        ]);\n        let bytes = frame.serialize();\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn helper_ok() {\n        assert_eq!(Frame::ok(), Frame::Simple(\"OK\".into()));\n    }\n\n    #[test]\n    fn helper_strings() {\n        let frame = Frame::strings(&[\"a\", \"b\", \"c\"]);\n        assert_eq!(\n            frame,\n            Frame::Array(vec![\n                Frame::Bulk(Bytes::from(\"a\")),\n                Frame::Bulk(Bytes::from(\"b\")),\n                Frame::Bulk(Bytes::from(\"c\")),\n            ])\n        );\n    }\n\n    #[test]\n    fn serialize_null() {\n        assert_eq!(Frame::Null.serialize(), b\"$-1\\r\\n\");\n    }\n\n    #[test]\n    fn parse_protocol_error() {\n        let data = b\"!invalid\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        assert!(matches!(\n            Frame::parse(&mut cursor),\n            Err(FrameError::Protocol(_))\n        ));\n    }\n\n    #[test]\n    fn parse_multiple_frames_sequentially() {\n        let data = b\"+OK\\r\\n:42\\r\\n$3\\r\\nfoo\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n\n        let f1 = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(f1, Frame::Simple(\"OK\".into()));\n\n        let f2 = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(f2, Frame::Integer(42));\n\n        let f3 = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(f3, Frame::Bulk(Bytes::from(\"foo\")));\n    }\n\n    #[test]\n    fn parse_binary_bulk_string() {\n        let mut data = Vec::new();\n        data.extend_from_slice(b\"$6\\r\\n\");\n        data.extend_from_slice(b\"he\\r\\nlo\");\n        data.extend_from_slice(b\"\\r\\n\");\n\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Bulk(Bytes::from(&b\"he\\r\\nlo\"[..])));\n    }\n\n    // ── RESP3 tests ─────────────────────────────────────────────────\n\n    #[test]\n    fn resp3_null_serialization() {\n        // RESP2 null\n        assert_eq!(Frame::Null.serialize(), b\"$-1\\r\\n\");\n        // RESP3 null\n        assert_eq!(Frame::Null.serialize_resp(true), b\"_\\r\\n\");\n    }\n\n    #[test]\n    fn resp3_parse_null() {\n        let data = b\"_\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Null);\n    }\n\n    #[test]\n    fn resp3_double_serialization() {\n        // RESP3 double\n        let bytes = Frame::Double(1.23).serialize_resp(true);\n        assert_eq!(bytes, b\",1.23\\r\\n\");\n\n        // RESP2 fallback: bulk string\n        let bytes = Frame::Double(1.23).serialize_resp(false);\n        assert_eq!(bytes, b\"$4\\r\\n1.23\\r\\n\");\n    }\n\n    #[test]\n    fn resp3_double_inf() {\n        let bytes = Frame::Double(f64::INFINITY).serialize_resp(true);\n        assert_eq!(bytes, b\",inf\\r\\n\");\n\n        let bytes = Frame::Double(f64::NEG_INFINITY).serialize_resp(true);\n        assert_eq!(bytes, b\",-inf\\r\\n\");\n    }\n\n    #[test]\n    fn resp3_parse_double() {\n        let data = b\",1.23\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Double(1.23));\n    }\n\n    #[test]\n    fn resp3_parse_double_inf() {\n        let data = b\",inf\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Double(f64::INFINITY));\n\n        let data = b\",-inf\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, Frame::Double(f64::NEG_INFINITY));\n    }\n\n    #[test]\n    fn resp3_map_serialization() {\n        let frame = Frame::Map(vec![\n            (Frame::bulk_string(\"key1\"), Frame::bulk_string(\"val1\")),\n            (Frame::bulk_string(\"key2\"), Frame::Integer(42)),\n        ]);\n\n        // RESP3: %2\\r\\n...\n        let bytes = frame.serialize_resp(true);\n        let expected = b\"%2\\r\\n$4\\r\\nkey1\\r\\n$4\\r\\nval1\\r\\n$4\\r\\nkey2\\r\\n:42\\r\\n\";\n        assert_eq!(bytes, expected);\n\n        // RESP2 fallback: *4\\r\\n... (flat array)\n        let bytes = frame.serialize_resp(false);\n        let expected = b\"*4\\r\\n$4\\r\\nkey1\\r\\n$4\\r\\nval1\\r\\n$4\\r\\nkey2\\r\\n:42\\r\\n\";\n        assert_eq!(bytes, expected);\n    }\n\n    #[test]\n    fn resp3_parse_map() {\n        let data = b\"%2\\r\\n$4\\r\\nkey1\\r\\n$4\\r\\nval1\\r\\n$4\\r\\nkey2\\r\\n:42\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(\n            frame,\n            Frame::Map(vec![\n                (\n                    Frame::Bulk(Bytes::from(\"key1\")),\n                    Frame::Bulk(Bytes::from(\"val1\"))\n                ),\n                (Frame::Bulk(Bytes::from(\"key2\")), Frame::Integer(42)),\n            ])\n        );\n    }\n\n    #[test]\n    fn resp3_set_serialization() {\n        let frame = Frame::Set(vec![Frame::bulk_string(\"a\"), Frame::bulk_string(\"b\")]);\n\n        // RESP3: ~2\\r\\n...\n        let bytes = frame.serialize_resp(true);\n        assert_eq!(bytes, b\"~2\\r\\n$1\\r\\na\\r\\n$1\\r\\nb\\r\\n\");\n\n        // RESP2 fallback: *2\\r\\n...\n        let bytes = frame.serialize_resp(false);\n        assert_eq!(bytes, b\"*2\\r\\n$1\\r\\na\\r\\n$1\\r\\nb\\r\\n\");\n    }\n\n    #[test]\n    fn resp3_parse_set() {\n        let data = b\"~2\\r\\n$1\\r\\na\\r\\n$1\\r\\nb\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(\n            frame,\n            Frame::Set(vec![\n                Frame::Bulk(Bytes::from(\"a\")),\n                Frame::Bulk(Bytes::from(\"b\")),\n            ])\n        );\n    }\n\n    #[test]\n    fn resp3_push_serialization() {\n        let frame = Frame::Push(vec![\n            Frame::bulk_string(\"message\"),\n            Frame::bulk_string(\"chan\"),\n            Frame::bulk_string(\"data\"),\n        ]);\n\n        // RESP3: >3\\r\\n...\n        let bytes = frame.serialize_resp(true);\n        assert_eq!(\n            bytes,\n            b\">3\\r\\n$7\\r\\nmessage\\r\\n$4\\r\\nchan\\r\\n$4\\r\\ndata\\r\\n\"\n        );\n\n        // RESP2 fallback: *3\\r\\n...\n        let bytes = frame.serialize_resp(false);\n        assert_eq!(\n            bytes,\n            b\"*3\\r\\n$7\\r\\nmessage\\r\\n$4\\r\\nchan\\r\\n$4\\r\\ndata\\r\\n\"\n        );\n    }\n\n    #[test]\n    fn resp3_parse_push() {\n        let data = b\">3\\r\\n$7\\r\\nmessage\\r\\n$4\\r\\nchan\\r\\n$4\\r\\ndata\\r\\n\";\n        let mut cursor = Cursor::new(&data[..]);\n        let frame = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(\n            frame,\n            Frame::Push(vec![\n                Frame::Bulk(Bytes::from(\"message\")),\n                Frame::Bulk(Bytes::from(\"chan\")),\n                Frame::Bulk(Bytes::from(\"data\")),\n            ])\n        );\n    }\n\n    #[test]\n    fn resp3_round_trip_map() {\n        let frame = Frame::Map(vec![\n            (Frame::bulk_string(\"a\"), Frame::Integer(1)),\n            (Frame::bulk_string(\"b\"), Frame::Integer(2)),\n        ]);\n        let bytes = frame.serialize_resp(true);\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n\n    #[test]\n    fn resp3_round_trip_double() {\n        let frame = Frame::Double(42.5);\n        let bytes = frame.serialize_resp(true);\n        let mut cursor = Cursor::new(&bytes[..]);\n        let parsed = Frame::parse(&mut cursor).unwrap();\n        assert_eq!(frame, parsed);\n    }\n}\n"
  },
  {
    "path": "miniredis/src/geo.rs",
    "content": "/// Geospatial encoding/decoding and distance calculations.\n///\n/// Implements 52-bit integer geohash encoding (matching Redis's geohash_helper.c)\n/// and Haversine distance formula.\nuse std::f64::consts::PI;\n\nconst ENC_LAT: f64 = 85.05112878;\nconst ENC_LONG: f64 = 180.0;\nconst EXP2_32: f64 = 4294967296.0; // 2^32\n\n/// Earth radius in meters (matching Redis src/geohash_helper.c).\nconst EARTH_RADIUS: f64 = 6372797.560856;\n\n// ── Range encoding ──────────────────────────────────────────────────\n\n/// Encode the position of x within the range [-r, r] as a 32-bit integer.\nfn encode_range(x: f64, r: f64) -> u32 {\n    let p = (x + r) / (2.0 * r);\n    (p * EXP2_32) as u32\n}\n\n/// Decode the 32-bit range encoding back to a value in [-r, r].\nfn decode_range(x: u32, r: f64) -> f64 {\n    let p = x as f64 / EXP2_32;\n    2.0 * r * p - r\n}\n\n// ── Bit interleaving ────────────────────────────────────────────────\n\n/// Spread 32 bits into the even bit positions of a 64-bit word.\nfn spread(x: u32) -> u64 {\n    let mut v = x as u64;\n    v = (v | (v << 16)) & 0x0000ffff0000ffff;\n    v = (v | (v << 8)) & 0x00ff00ff00ff00ff;\n    v = (v | (v << 4)) & 0x0f0f0f0f0f0f0f0f;\n    v = (v | (v << 2)) & 0x3333333333333333;\n    v = (v | (v << 1)) & 0x5555555555555555;\n    v\n}\n\n/// Squash the even bit positions of a 64-bit word into 32 bits.\nfn squash(x: u64) -> u32 {\n    let mut v = x & 0x5555555555555555;\n    v = (v | (v >> 1)) & 0x3333333333333333;\n    v = (v | (v >> 2)) & 0x0f0f0f0f0f0f0f0f;\n    v = (v | (v >> 4)) & 0x00ff00ff00ff00ff;\n    v = (v | (v >> 8)) & 0x0000ffff0000ffff;\n    v = (v | (v >> 16)) & 0x00000000ffffffff;\n    v as u32\n}\n\n/// Interleave the bits of x (lat) and y (lng). x occupies even bit positions,\n/// y occupies odd bit positions.\nfn interleave(x: u32, y: u32) -> u64 {\n    spread(x) | (spread(y) << 1)\n}\n\n/// Deinterleave: extract even and odd bit positions into two 32-bit words.\nfn deinterleave(v: u64) -> (u32, u32) {\n    (squash(v), squash(v >> 1))\n}\n\n// ── Geohash encode/decode ───────────────────────────────────────────\n\n/// Encode latitude and longitude into a full 64-bit integer geohash.\nfn encode_int(lat: f64, lng: f64) -> u64 {\n    let lat_int = encode_range(lat, ENC_LAT);\n    let lng_int = encode_range(lng, ENC_LONG);\n    interleave(lat_int, lng_int)\n}\n\n/// Encode coordinates as a 52-bit geohash (stored as the upper 52 bits of\n/// a 64-bit value, i.e. right-shifted by 12).\npub fn to_geohash(longitude: f64, latitude: f64) -> u64 {\n    encode_int(latitude, longitude) >> (64 - 52)\n}\n\n/// Decode a 52-bit geohash back to (longitude, latitude).\npub fn from_geohash(hash: u64) -> (f64, f64) {\n    let full_hash = hash << (64 - 52);\n    let (lat_int, lng_int) = deinterleave(full_hash);\n    let lat = decode_range(lat_int, ENC_LAT);\n    let lng = decode_range(lng_int, ENC_LONG);\n    // Bounding box center: add half the error\n    let lat_bits = 52 / 2; // 26\n    let lng_bits = 52 - lat_bits; // 26\n    let lat_err = 180.0 * 2.0f64.powi(-lat_bits);\n    let lng_err = 360.0 * 2.0f64.powi(-lng_bits);\n    (lng + lng_err / 2.0, lat + lat_err / 2.0)\n}\n\n// ── Haversine distance ──────────────────────────────────────────────\n\n/// Haversine helper: sin²(θ/2).\nfn hsin(theta: f64) -> f64 {\n    let s = (theta / 2.0).sin();\n    s * s\n}\n\n/// Calculate the great-circle distance in meters between two points given\n/// as (latitude, longitude) in degrees, using the Haversine formula.\npub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {\n    let la1 = lat1 * PI / 180.0;\n    let lo1 = lon1 * PI / 180.0;\n    let la2 = lat2 * PI / 180.0;\n    let lo2 = lon2 * PI / 180.0;\n\n    let h = hsin(la2 - la1) + la1.cos() * la2.cos() * hsin(lo2 - lo1);\n    2.0 * EARTH_RADIUS * h.sqrt().asin()\n}\n\n// ── Unit conversion ─────────────────────────────────────────────────\n\n/// Parse a distance unit string and return the conversion factor to meters.\n/// Returns None for unrecognized units.\npub fn parse_unit(unit: &str) -> Option<f64> {\n    match unit.to_lowercase().as_str() {\n        \"m\" => Some(1.0),\n        \"km\" => Some(1000.0),\n        \"mi\" => Some(1609.34),\n        \"ft\" => Some(0.3048),\n        _ => None,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_geohash_roundtrip() {\n        let lng = 13.361_389_338_970_184;\n        let lat = 38.115_556_395_496_3;\n        let hash = to_geohash(lng, lat);\n        assert_eq!(hash, 3479099956230698);\n\n        let (lng_back, lat_back) = from_geohash(hash);\n        assert!(\n            (lng - lng_back).abs() < 0.000001,\n            \"longitude: {} vs {}\",\n            lng,\n            lng_back\n        );\n        assert!(\n            (lat - lat_back).abs() < 0.000001,\n            \"latitude: {} vs {}\",\n            lat,\n            lat_back\n        );\n    }\n\n    #[test]\n    fn test_haversine_palermo_catania() {\n        // Palermo: 13.361389, 38.115556\n        // Catania: 15.087269, 37.502669\n        let d = haversine_distance(38.115556, 13.361389, 37.502669, 15.087269);\n        // Expected ~166274 meters\n        assert!((d - 166274.0).abs() < 100.0, \"distance: {}\", d);\n    }\n\n    #[test]\n    fn test_parse_unit() {\n        assert_eq!(parse_unit(\"m\"), Some(1.0));\n        assert_eq!(parse_unit(\"km\"), Some(1000.0));\n        assert_eq!(parse_unit(\"mi\"), Some(1609.34));\n        assert_eq!(parse_unit(\"ft\"), Some(0.3048));\n        assert_eq!(parse_unit(\"mm\"), None);\n        assert_eq!(parse_unit(\"M\"), Some(1.0));\n        assert_eq!(parse_unit(\"KM\"), Some(1000.0));\n    }\n}\n"
  },
  {
    "path": "miniredis/src/hll.rs",
    "content": "/// HyperLogLog probabilistic cardinality estimator.\n///\n/// Dense-only implementation with p=14 (16,384 registers).\nconst P: u8 = 14;\nconst M: usize = 1 << P; // 16384 registers\n\n/// Alpha constant for bias correction.\nfn alpha_m(m: f64) -> f64 {\n    match m as u64 {\n        16 => 0.673,\n        32 => 0.697,\n        64 => 0.709,\n        _ => 0.7213 / (1.0 + 1.079 / m),\n    }\n}\n\n/// Bias correction polynomial for p=14.\nfn beta14(ez: f64) -> f64 {\n    let zl = (ez + 1.0).ln();\n    -0.370393911 * ez\n        + 0.070471823 * zl\n        + 0.17393686 * zl.powi(2)\n        + 0.16339839 * zl.powi(3)\n        + -0.09237745 * zl.powi(4)\n        + 0.03738027 * zl.powi(5)\n        + -0.005384159 * zl.powi(6)\n        + 0.00042419 * zl.powi(7)\n}\n\n/// 64-bit hash function (FNV-1a with avalanche mixing).\nfn hash64(data: &[u8]) -> u64 {\n    let mut hash: u64 = 0xcbf29ce484222325;\n    for &byte in data {\n        hash ^= byte as u64;\n        hash = hash.wrapping_mul(0x100000001b3);\n    }\n    // Avalanche: ensure all output bits depend on all input bits\n    hash ^= hash >> 33;\n    hash = hash.wrapping_mul(0xff51afd7ed558ccd);\n    hash ^= hash >> 33;\n    hash = hash.wrapping_mul(0xc4ceb9fe1a85ec53);\n    hash ^= hash >> 33;\n    hash\n}\n\n/// Extract register index and rho (leading zeros + 1) from a hash.\nfn get_pos_val(hash: u64) -> (usize, u8) {\n    // Top P bits → register index\n    let idx = (hash >> (64 - P)) as usize;\n    // Remaining bits: count leading zeros + 1\n    // Set bit (P-1) to guarantee at least one 1-bit\n    let w = (hash << P) | (1u64 << (P - 1));\n    let rho = w.leading_zeros() as u8 + 1;\n    (idx, rho)\n}\n\n/// HyperLogLog probabilistic set cardinality estimator.\n#[derive(Clone, Debug)]\npub struct HyperLogLog {\n    registers: Vec<u8>,\n}\n\nimpl Default for HyperLogLog {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl HyperLogLog {\n    pub fn new() -> Self {\n        HyperLogLog {\n            registers: vec![0; M],\n        }\n    }\n\n    /// Add an element. Returns true if the internal state changed\n    /// (i.e., the approximated cardinality changed).\n    pub fn add(&mut self, element: &[u8]) -> bool {\n        let hash = hash64(element);\n        let (idx, rho) = get_pos_val(hash);\n        if rho > self.registers[idx] {\n            self.registers[idx] = rho;\n            true\n        } else {\n            false\n        }\n    }\n\n    /// Estimate the cardinality (number of unique elements).\n    pub fn count(&self) -> u64 {\n        let m = M as f64;\n        let alpha = alpha_m(m);\n\n        let mut sum = 0.0f64;\n        let mut zeros = 0.0f64;\n\n        for &reg in &self.registers {\n            sum += 2.0f64.powi(-(reg as i32));\n            if reg == 0 {\n                zeros += 1.0;\n            }\n        }\n\n        let est = alpha * m * (m - zeros) / (sum + beta14(zeros));\n        (est + 0.5) as u64\n    }\n\n    /// Merge another HLL into this one (element-wise max of registers).\n    pub fn merge(&mut self, other: &HyperLogLog) {\n        for i in 0..M {\n            if other.registers[i] > self.registers[i] {\n                self.registers[i] = other.registers[i];\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_hll_empty() {\n        let hll = HyperLogLog::new();\n        assert_eq!(hll.count(), 0);\n    }\n\n    #[test]\n    fn test_hll_add_single() {\n        let mut hll = HyperLogLog::new();\n        assert!(hll.add(b\"hello\"));\n        assert!(!hll.add(b\"hello\")); // duplicate\n        assert_eq!(hll.count(), 1);\n    }\n\n    #[test]\n    fn test_hll_add_multiple() {\n        let mut hll = HyperLogLog::new();\n        for i in 0..100 {\n            hll.add(format!(\"item-{}\", i).as_bytes());\n        }\n        let count = hll.count();\n        // Should be approximately 100 (within 10% for p=14)\n        assert!((90..=110).contains(&count), \"count was {}\", count);\n    }\n\n    #[test]\n    fn test_hll_duplicate_returns_false() {\n        let mut hll = HyperLogLog::new();\n        assert!(hll.add(b\"a\"));\n        assert!(hll.add(b\"b\"));\n        assert!(!hll.add(b\"a\")); // already added\n        assert!(!hll.add(b\"b\")); // already added\n    }\n\n    #[test]\n    fn test_hll_merge() {\n        let mut hll1 = HyperLogLog::new();\n        let mut hll2 = HyperLogLog::new();\n\n        for i in 0..50 {\n            hll1.add(format!(\"item-{}\", i).as_bytes());\n        }\n        for i in 50..100 {\n            hll2.add(format!(\"item-{}\", i).as_bytes());\n        }\n\n        hll1.merge(&hll2);\n        let count = hll1.count();\n        // Should be approximately 100\n        assert!((90..=110).contains(&count), \"count was {}\", count);\n    }\n\n    #[test]\n    fn test_hll_merge_overlap() {\n        let mut hll1 = HyperLogLog::new();\n        let mut hll2 = HyperLogLog::new();\n\n        // Add same elements to both\n        for i in 0..50 {\n            hll1.add(format!(\"item-{}\", i).as_bytes());\n            hll2.add(format!(\"item-{}\", i).as_bytes());\n        }\n\n        hll1.merge(&hll2);\n        let count = hll1.count();\n        // Should still be approximately 50\n        assert!((45..=55).contains(&count), \"count was {}\", count);\n    }\n\n    #[test]\n    fn test_hll_large_cardinality() {\n        let mut hll = HyperLogLog::new();\n        for i in 0..10000 {\n            hll.add(format!(\"element-{}\", i).as_bytes());\n        }\n        let count = hll.count();\n        // p=14 gives ~0.8% standard error, allow 5% margin\n        assert!((9500..=10500).contains(&count), \"count was {}\", count);\n    }\n}\n"
  },
  {
    "path": "miniredis/src/keys.rs",
    "content": "/// Glob pattern matching for Redis KEYS, SCAN, PSUBSCRIBE etc.\n///\n/// Supports *, ?, [abc], [a-z], [^a], \\ escape.\n///\n/// Simple glob matching: supports *, ?, [abc], [a-z], [^a].\npub fn glob_match(pattern: &str, text: &str) -> bool {\n    let pat = pattern.as_bytes();\n    let txt = text.as_bytes();\n    glob_match_inner(pat, txt)\n}\n\nfn glob_match_inner(pat: &[u8], txt: &[u8]) -> bool {\n    let mut pi = 0;\n    let mut ti = 0;\n    let mut star_pi = usize::MAX;\n    let mut star_ti = 0;\n\n    while ti < txt.len() {\n        if pi < pat.len()\n            && (pat[pi] == b'?' || (pat[pi] == txt[ti] && pat[pi] != b'[' && pat[pi] != b'\\\\'))\n        {\n            pi += 1;\n            ti += 1;\n        } else if pi < pat.len() && pat[pi] == b'*' {\n            star_pi = pi;\n            star_ti = ti;\n            pi += 1;\n        } else if pi < pat.len() && pat[pi] == b'[' {\n            let (matched, end) = match_char_class(&pat[pi..], txt[ti]);\n            if matched {\n                pi += end;\n                ti += 1;\n            } else if star_pi != usize::MAX {\n                pi = star_pi + 1;\n                star_ti += 1;\n                ti = star_ti;\n            } else {\n                return false;\n            }\n        } else if pi < pat.len() && pat[pi] == b'\\\\' && pi + 1 < pat.len() {\n            pi += 1;\n            if pat[pi] == txt[ti] {\n                pi += 1;\n                ti += 1;\n            } else if star_pi != usize::MAX {\n                pi = star_pi + 1;\n                star_ti += 1;\n                ti = star_ti;\n            } else {\n                return false;\n            }\n        } else if star_pi != usize::MAX {\n            pi = star_pi + 1;\n            star_ti += 1;\n            ti = star_ti;\n        } else {\n            return false;\n        }\n    }\n\n    while pi < pat.len() && pat[pi] == b'*' {\n        pi += 1;\n    }\n\n    pi == pat.len()\n}\n\n/// Match a character class like [abc] or [a-z] or [^a]. Returns (matched, bytes consumed).\nfn match_char_class(pat: &[u8], ch: u8) -> (bool, usize) {\n    if pat.is_empty() || pat[0] != b'[' {\n        return (false, 0);\n    }\n\n    let mut i = 1;\n    let negate = if i < pat.len() && pat[i] == b'^' {\n        i += 1;\n        true\n    } else {\n        false\n    };\n\n    let mut matched = false;\n    while i < pat.len() && pat[i] != b']' {\n        // Handle backslash escape inside char class\n        let c = if pat[i] == b'\\\\' && i + 1 < pat.len() {\n            i += 1;\n            pat[i]\n        } else {\n            pat[i]\n        };\n\n        if i + 2 < pat.len() && pat[i + 1] == b'-' {\n            let end = if pat[i + 2] == b'\\\\' && i + 3 < pat.len() {\n                i += 1;\n                pat[i + 2]\n            } else {\n                pat[i + 2]\n            };\n            if ch >= c && ch <= end {\n                matched = true;\n            }\n            i += 3;\n        } else {\n            if c == ch {\n                matched = true;\n            }\n            i += 1;\n        }\n    }\n\n    if i < pat.len() && pat[i] == b']' {\n        i += 1;\n    }\n\n    if negate {\n        matched = !matched;\n    }\n\n    (matched, i)\n}\n\n/// Filter a list of strings by a glob pattern. Used by SSCAN, HSCAN etc.\npub fn match_keys_vec(keys: &[String], pattern: &str) -> Vec<String> {\n    if pattern == \"*\" {\n        return keys.to_vec();\n    }\n    keys.iter()\n        .filter(|k| glob_match(pattern, k))\n        .cloned()\n        .collect()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_glob_match_basic() {\n        assert!(glob_match(\"*\", \"anything\"));\n        assert!(glob_match(\"hello\", \"hello\"));\n        assert!(!glob_match(\"hello\", \"world\"));\n        assert!(glob_match(\"h?llo\", \"hello\"));\n        assert!(glob_match(\"h*o\", \"hello\"));\n        assert!(glob_match(\"h[ae]llo\", \"hello\"));\n        assert!(!glob_match(\"h[ae]llo\", \"hillo\"));\n    }\n\n    #[test]\n    fn test_glob_match_patterns() {\n        assert!(glob_match(\"event*\", \"event123\"));\n        assert!(glob_match(\"event*\", \"event\"));\n        assert!(!glob_match(\"event*\", \"other\"));\n        assert!(glob_match(\"*news*\", \"the-news-today\"));\n        assert!(glob_match(\"h[a-z]llo\", \"hello\"));\n    }\n\n    #[test]\n    fn test_glob_match_escape_in_char_class() {\n        // Backslash inside [] should escape the next character\n        assert!(glob_match(\"[\\\\[o]*\", \"[one]\"));\n        assert!(!glob_match(\"[\\\\[o]*\", \"two\"));\n        assert!(glob_match(\"[\\\\[o]*\", \"other\"));\n    }\n}\n"
  },
  {
    "path": "miniredis/src/lib.rs",
    "content": "// This code is a Rust port of miniredis by Harmen (https://github.com/alicebob/miniredis),\n// originally licensed under the MIT License. See MINIREDIS_LICENSE.txt for the original license.\n\n//! # miniredis-rs\n//!\n//! Pure Rust in-memory Redis test server, for use in Rust integration tests.\n//!\n//! Start a server with `Miniredis::run().await`, it will listen on a random port.\n//! Point your Redis client to `m.redis_url()` or `m.addr()`.\n//!\n//! ```rust\n//! # async fn example() {\n//! let m = miniredis_rs::Miniredis::run().await.unwrap();\n//! m.set(\"foo\", \"bar\");\n//! // Use m.redis_url() with any Redis client\n//! m.close().await;\n//! # }\n//! ```\n\npub mod cmd;\npub mod connection;\npub mod db;\npub mod dispatch;\npub mod frame;\npub mod geo;\npub mod hll;\npub mod keys;\npub mod pubsub;\npub mod server;\npub mod types;\n\nmod error;\n\npub use error::{Error, Result};\n\nuse std::net::SocketAddr;\nuse std::sync::Arc;\nuse std::time::{Duration, SystemTime};\n\nuse tokio::net::TcpListener;\n\nuse crate::db::SharedState;\n\n/// A running miniredis instance for use in tests.\n///\n/// Create one with [`Miniredis::run()`], use [`addr()`](Miniredis::addr) or\n/// [`redis_url()`](Miniredis::redis_url) to connect a client, and call\n/// [`close()`](Miniredis::close) when done.\n#[derive(Clone)]\npub struct Miniredis {\n    state: Arc<SharedState>,\n    addr: SocketAddr,\n    selected_db: usize,\n}\n\nimpl Miniredis {\n    /// Start a new miniredis server on a random available port (127.0.0.1:0).\n    pub async fn run() -> Result<Self> {\n        Self::run_addr(\"127.0.0.1:0\").await\n    }\n\n    /// Start a new miniredis server on the given address.\n    pub async fn run_addr(addr: &str) -> Result<Self> {\n        let listener = TcpListener::bind(addr).await?;\n        let local_addr = listener.local_addr()?;\n        let state = SharedState::new();\n        let state_clone = Arc::clone(&state);\n        let shutdown_rx = state.shutdown_tx.subscribe();\n\n        tokio::spawn(async move {\n            server::run(listener, state_clone, shutdown_rx, None).await;\n        });\n\n        Ok(Miniredis {\n            state,\n            addr: local_addr,\n            selected_db: 0,\n        })\n    }\n\n    /// Start a new miniredis server with TLS on a random port.\n    ///\n    /// Connections must use TLS (rediss:// scheme). Use `tls_url()` for the URL.\n    #[cfg(feature = \"tls\")]\n    pub async fn run_tls(tls_config: Arc<rustls::ServerConfig>) -> Result<Self> {\n        Self::run_tls_addr(\"127.0.0.1:0\", tls_config).await\n    }\n\n    /// Start a new miniredis server with TLS on the given address.\n    #[cfg(feature = \"tls\")]\n    pub async fn run_tls_addr(addr: &str, tls_config: Arc<rustls::ServerConfig>) -> Result<Self> {\n        let listener = TcpListener::bind(addr).await?;\n        let local_addr = listener.local_addr()?;\n        let state = SharedState::new();\n        let state_clone = Arc::clone(&state);\n        let shutdown_rx = state.shutdown_tx.subscribe();\n        let acceptor = tokio_rustls::TlsAcceptor::from(tls_config);\n\n        tokio::spawn(async move {\n            server::run(listener, state_clone, shutdown_rx, Some(acceptor)).await;\n        });\n\n        Ok(Miniredis {\n            state,\n            addr: local_addr,\n            selected_db: 0,\n        })\n    }\n\n    /// Shut down the server.\n    pub async fn close(&self) {\n        let _ = self.state.shutdown_tx.send(());\n        // Give tasks a moment to clean up\n        tokio::task::yield_now().await;\n    }\n\n    // ── Address helpers ──────────────────────────────────────────────\n\n    /// The bound address as a `SocketAddr`.\n    pub fn addr(&self) -> SocketAddr {\n        self.addr\n    }\n\n    /// Just the host (e.g. \"127.0.0.1\").\n    pub fn host(&self) -> String {\n        self.addr.ip().to_string()\n    }\n\n    /// Just the port number.\n    pub fn port(&self) -> u16 {\n        self.addr.port()\n    }\n\n    /// A `redis://host:port` URL suitable for most Redis clients.\n    pub fn redis_url(&self) -> String {\n        format!(\"redis://{}:{}\", self.addr.ip(), self.addr.port())\n    }\n\n    /// A `rediss://host:port` URL for TLS Redis clients.\n    #[cfg(feature = \"tls\")]\n    pub fn tls_url(&self) -> String {\n        format!(\"rediss://{}:{}\", self.addr.ip(), self.addr.port())\n    }\n\n    // ── Database selection ───────────────────────────────────────────\n\n    /// Select the database used by the direct-access methods.\n    pub fn select(&mut self, db: usize) {\n        assert!(db < 16, \"database index must be 0-15\");\n        self.selected_db = db;\n    }\n\n    // ── Authentication ───────────────────────────────────────────────\n\n    /// Require AUTH with a password (default user).\n    pub fn require_auth(&self, password: &str) {\n        let mut inner = self.state.lock();\n        inner\n            .passwords\n            .insert(\"default\".to_owned(), password.to_owned());\n    }\n\n    /// Require AUTH with a username and password.\n    pub fn require_user_auth(&self, username: &str, password: &str) {\n        let mut inner = self.state.lock();\n        inner\n            .passwords\n            .insert(username.to_owned(), password.to_owned());\n    }\n\n    // ── Time & determinism ───────────────────────────────────────────\n\n    /// Set a fixed mock time. Affects EXPIREAT, stream IDs, etc.\n    pub fn set_time(&self, t: SystemTime) {\n        let mut inner = self.state.lock();\n        inner.now = Some(t);\n    }\n\n    /// Decrease all TTLs by `duration`, expiring any that drop to zero.\n    pub fn fast_forward(&self, duration: Duration) {\n        let mut inner = self.state.lock();\n        inner.fast_forward(duration);\n    }\n\n    /// Seed the random number generator for deterministic tests.\n    pub fn seed(&self, seed: u64) {\n        use rand::SeedableRng;\n        let mut inner = self.state.lock();\n        inner.rng = rand::rngs::StdRng::seed_from_u64(seed);\n    }\n\n    // ── Key management ───────────────────────────────────────────────\n\n    /// Delete a key. Returns true if it existed.\n    pub fn del(&self, key: &str) -> bool {\n        let mut inner = self.state.lock();\n        inner.db_mut(self.selected_db).del(key)\n    }\n\n    /// Check if a key exists.\n    pub fn exists(&self, key: &str) -> bool {\n        let mut inner = self.state.lock();\n        let now = inner.effective_now();\n        inner.db_mut(self.selected_db).exists(key, now)\n    }\n\n    /// Return the type of a key (\"string\", \"list\", \"set\", \"hash\", \"zset\",\n    /// \"stream\", \"none\").\n    pub fn key_type(&self, key: &str) -> &'static str {\n        let inner = self.state.lock();\n        match inner.db(self.selected_db).key_type(key) {\n            Some(t) => t.as_str(),\n            None => \"none\",\n        }\n    }\n\n    /// Return all keys from the selected database, sorted.\n    pub fn keys(&self) -> Vec<String> {\n        let inner = self.state.lock();\n        inner.db(self.selected_db).all_keys()\n    }\n\n    /// Get the TTL of a key. Returns None if the key has no TTL.\n    pub fn ttl(&self, key: &str) -> Option<Duration> {\n        let inner = self.state.lock();\n        inner.db(self.selected_db).ttl.get(key).copied()\n    }\n\n    /// Set the TTL for a key.\n    pub fn set_ttl(&self, key: &str, ttl: Duration) {\n        let mut inner = self.state.lock();\n        inner\n            .db_mut(self.selected_db)\n            .ttl\n            .insert(key.to_owned(), ttl);\n    }\n\n    // ── String operations ────────────────────────────────────────────\n\n    /// Get a string key value.\n    pub fn get(&self, key: &str) -> Option<String> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.string_get(key)\n            .map(|v| String::from_utf8_lossy(v).into_owned())\n    }\n\n    /// Set a string key. Removes any existing TTL.\n    pub fn set(&self, key: &str, value: &str) {\n        let mut inner = self.state.lock();\n        let now = inner.effective_now();\n        let db = inner.db_mut(self.selected_db);\n        db.string_set(key, value.as_bytes().to_vec(), now);\n        db.ttl.remove(key);\n    }\n\n    /// Increment a string key by delta. Creates the key if it doesn't exist.\n    pub fn incr(&self, key: &str, delta: i64) -> i64 {\n        let mut inner = self.state.lock();\n        let now = inner.effective_now();\n        let db = inner.db_mut(self.selected_db);\n        let current = db\n            .string_get(key)\n            .and_then(|v| String::from_utf8_lossy(v).parse::<i64>().ok())\n            .unwrap_or(0);\n        let new_val = current + delta;\n        db.string_set(key, new_val.to_string().into_bytes(), now);\n        new_val\n    }\n\n    // ── List operations ──────────────────────────────────────────────\n\n    /// Push values to the end (right) of a list. Creates the key if needed.\n    /// Returns the new list length.\n    pub fn push(&self, key: &str, values: &[&str]) -> usize {\n        let mut inner = self.state.lock();\n\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::List);\n        let list = db.list_keys.entry(key.to_owned()).or_default();\n        for v in values {\n            list.push_back(v.as_bytes().to_vec());\n        }\n        list.len()\n    }\n\n    /// Push a value to the beginning (left) of a list.\n    pub fn lpush(&self, key: &str, value: &str) -> usize {\n        let mut inner = self.state.lock();\n\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::List);\n        let list = db.list_keys.entry(key.to_owned()).or_default();\n        list.push_front(value.as_bytes().to_vec());\n        list.len()\n    }\n\n    /// Pop from the end (right) of a list.\n    pub fn pop(&self, key: &str) -> Option<String> {\n        let mut inner = self.state.lock();\n        let db = inner.db_mut(self.selected_db);\n        let list = db.list_keys.get_mut(key)?;\n        let val = list.pop_back()?;\n        if list.is_empty() {\n            db.list_keys.remove(key);\n            db.del(key);\n        }\n        Some(String::from_utf8_lossy(&val).into_owned())\n    }\n\n    /// Pop from the beginning (left) of a list.\n    pub fn lpop(&self, key: &str) -> Option<String> {\n        let mut inner = self.state.lock();\n        let db = inner.db_mut(self.selected_db);\n        let list = db.list_keys.get_mut(key)?;\n        let val = list.pop_front()?;\n        if list.is_empty() {\n            db.list_keys.remove(key);\n            db.del(key);\n        }\n        Some(String::from_utf8_lossy(&val).into_owned())\n    }\n\n    /// Get all values in a list.\n    pub fn list(&self, key: &str) -> Option<Vec<String>> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.list_keys.get(key).map(|list| {\n            list.iter()\n                .map(|v| String::from_utf8_lossy(v).into_owned())\n                .collect()\n        })\n    }\n\n    // ── Set operations ───────────────────────────────────────────────\n\n    /// Add members to a set. Returns the number of new members added.\n    pub fn set_add(&self, key: &str, members: &[&str]) -> usize {\n        let mut inner = self.state.lock();\n\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::Set);\n        let set = db.set_keys.entry(key.to_owned()).or_default();\n        let mut added = 0;\n        for m in members {\n            if set.insert(m.to_string()) {\n                added += 1;\n            }\n        }\n        added\n    }\n\n    /// Get all members of a set, sorted.\n    pub fn members(&self, key: &str) -> Option<Vec<String>> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.set_keys.get(key).map(|set| {\n            let mut v: Vec<String> = set.iter().cloned().collect();\n            v.sort();\n            v\n        })\n    }\n\n    /// Check if a value is a member of a set.\n    pub fn is_member(&self, key: &str, member: &str) -> bool {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.set_keys\n            .get(key)\n            .map(|set| set.contains(member))\n            .unwrap_or(false)\n    }\n\n    // ── Hash operations ──────────────────────────────────────────────\n\n    /// Set a hash field.\n    pub fn hset(&self, key: &str, field: &str, value: &str) {\n        let mut inner = self.state.lock();\n\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::Hash);\n        let hash = db.hash_keys.entry(key.to_owned()).or_default();\n        hash.insert(field.to_owned(), value.as_bytes().to_vec());\n    }\n\n    /// Get a hash field value.\n    pub fn hget(&self, key: &str, field: &str) -> Option<String> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.hash_keys\n            .get(key)\n            .and_then(|h| h.get(field))\n            .map(|v| String::from_utf8_lossy(v).into_owned())\n    }\n\n    /// Get all field names in a hash, sorted.\n    pub fn hkeys(&self, key: &str) -> Option<Vec<String>> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.hash_keys.get(key).map(|h| {\n            let mut keys: Vec<String> = h.keys().cloned().collect();\n            keys.sort();\n            keys\n        })\n    }\n\n    /// Delete a hash field. Returns true if the field existed.\n    pub fn hdel(&self, key: &str, field: &str) -> bool {\n        let mut inner = self.state.lock();\n        let db = inner.db_mut(self.selected_db);\n        if let Some(hash) = db.hash_keys.get_mut(key) {\n            let removed = hash.remove(field).is_some();\n            if hash.is_empty() {\n                db.hash_keys.remove(key);\n                db.del(key);\n            }\n            removed\n        } else {\n            false\n        }\n    }\n\n    // ── Sorted set operations ────────────────────────────────────────\n\n    /// Add a member to a sorted set with the given score.\n    /// Returns true if the member was new.\n    pub fn zadd(&self, key: &str, score: f64, member: &str) -> bool {\n        let mut inner = self.state.lock();\n\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::SortedSet);\n        let ss = db.sorted_set_keys.entry(key.to_owned()).or_default();\n        ss.set(score, member)\n    }\n\n    /// Get the score of a member in a sorted set.\n    pub fn zscore(&self, key: &str, member: &str) -> Option<f64> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.sorted_set_keys.get(key).and_then(|ss| ss.get(member))\n    }\n\n    /// Get all members of a sorted set, sorted by score then member name.\n    pub fn zmembers(&self, key: &str) -> Option<Vec<String>> {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.sorted_set_keys.get(key).map(|ss| ss.members_sorted())\n    }\n\n    // ── Stream operations ────────────────────────────────────────────\n\n    /// Add an entry to a stream. Returns the assigned ID.\n    pub fn xadd(&self, key: &str, id: &str, values: &[(&str, &str)]) -> String {\n        let mut inner = self.state.lock();\n        let now = inner.effective_now();\n        let now_ms = now\n            .duration_since(SystemTime::UNIX_EPOCH)\n            .unwrap_or_default()\n            .as_millis() as u64;\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::Stream);\n        let stream = db.stream_keys.entry(key.to_owned()).or_default();\n        let field_values: Vec<String> = values\n            .iter()\n            .flat_map(|(k, v)| vec![k.to_string(), v.to_string()])\n            .collect();\n        stream.add(id, field_values, now_ms).unwrap_or_default()\n    }\n\n    // ── HyperLogLog operations ───────────────────────────────────────\n\n    /// Add elements to a HyperLogLog. Returns true if the cardinality estimate changed.\n    pub fn pfadd(&self, key: &str, elements: &[&str]) -> bool {\n        let mut inner = self.state.lock();\n\n        let db = inner.db_mut(self.selected_db);\n        db.keys.insert(key.to_owned(), types::KeyType::HyperLogLog);\n        let hll = db.hll_keys.entry(key.to_owned()).or_default();\n        let mut changed = false;\n        for elem in elements {\n            if hll.add(elem.as_bytes()) {\n                changed = true;\n            }\n        }\n        changed\n    }\n\n    /// Get the cardinality estimate of a HyperLogLog.\n    pub fn pfcount(&self, key: &str) -> i64 {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        db.hll_keys\n            .get(key)\n            .map(|hll| hll.count() as i64)\n            .unwrap_or(0)\n    }\n\n    // ── Flush ────────────────────────────────────────────────────────\n\n    /// Remove all keys from the selected database.\n    pub fn flush_db(&self) {\n        let mut inner = self.state.lock();\n        inner.db_mut(self.selected_db).flush();\n    }\n\n    /// Remove all keys from all databases.\n    pub fn flush_all(&self) {\n        let mut inner = self.state.lock();\n        for db in &mut inner.dbs {\n            db.flush();\n        }\n    }\n\n    // ── Testing assertions ───────────────────────────────────────────\n\n    /// Assert that a string key has the expected value. Panics on mismatch.\n    pub fn check_get(&self, key: &str, expected: &str) {\n        let val = self\n            .get(key)\n            .unwrap_or_else(|| panic!(\"key {:?} not found\", key));\n        assert_eq!(\n            val, expected,\n            \"key {:?}: expected {:?}, got {:?}\",\n            key, expected, val\n        );\n    }\n\n    // ── Pub/Sub ──────────────────────────────────────────────────────\n\n    /// Publish a message on a channel. Returns the number of subscribers that received it.\n    pub fn publish(&self, channel: &str, message: &str) -> i64 {\n        let registry = self.state.pubsub.lock().unwrap();\n        registry.publish(channel, message)\n    }\n\n    /// Return active pub/sub channels, optionally filtered by glob pattern.\n    pub fn pubsub_channels(&self, pattern: Option<&str>) -> Vec<String> {\n        let registry = self.state.pubsub.lock().unwrap();\n        registry.active_channels(pattern)\n    }\n\n    /// Return the number of subscribers for specific channels.\n    pub fn pubsub_numsub(&self, channels: &[&str]) -> Vec<(String, i64)> {\n        let registry = self.state.pubsub.lock().unwrap();\n        channels\n            .iter()\n            .map(|ch| (ch.to_string(), registry.numsub(ch)))\n            .collect()\n    }\n\n    /// Return the total number of pattern subscriptions.\n    pub fn pubsub_numpat(&self) -> i64 {\n        let registry = self.state.pubsub.lock().unwrap();\n        registry.numpat()\n    }\n\n    // ── Testing assertions ───────────────────────────────────────────\n\n    /// Assert that a list key has the expected values. Panics on mismatch.\n    pub fn check_list(&self, key: &str, expected: &[&str]) {\n        let val = self\n            .list(key)\n            .unwrap_or_else(|| panic!(\"key {:?} not found or not a list\", key));\n        let expected_strs: Vec<String> = expected.iter().map(|s| s.to_string()).collect();\n        assert_eq!(\n            val, expected_strs,\n            \"key {:?}: expected {:?}, got {:?}\",\n            key, expected, val\n        );\n    }\n\n    /// Assert that a set key has the expected members (order-independent). Panics on mismatch.\n    pub fn check_set(&self, key: &str, expected: &[&str]) {\n        let val = self\n            .members(key)\n            .unwrap_or_else(|| panic!(\"key {:?} not found or not a set\", key));\n        let mut expected_sorted: Vec<String> = expected.iter().map(|s| s.to_string()).collect();\n        expected_sorted.sort();\n        assert_eq!(\n            val, expected_sorted,\n            \"key {:?}: expected {:?}, got {:?}\",\n            key, expected_sorted, val\n        );\n    }\n\n    // ── Server introspection ─────────────────────────────────────────\n\n    /// Number of currently connected clients.\n    pub fn current_connection_count(&self) -> u64 {\n        self.state\n            .connected_clients\n            .load(std::sync::atomic::Ordering::Relaxed)\n    }\n\n    /// Total number of connections received since startup.\n    pub fn total_connection_count(&self) -> u64 {\n        self.state\n            .total_connections_received\n            .load(std::sync::atomic::Ordering::Relaxed)\n    }\n\n    // ── Direct DB access ──────────────────────────────────────────────\n\n    /// Access a specific database by index (0-15) without changing the\n    /// selected database.\n    ///\n    /// The returned [`DbRef`] borrows `self` and provides the same\n    /// direct-access methods (get, set, keys, etc.) scoped to the given DB.\n    pub fn db(&self, id: usize) -> DbRef<'_> {\n        assert!(id < 16, \"database index must be 0-15\");\n        DbRef {\n            state: &self.state,\n            db_id: id,\n        }\n    }\n\n    // ── Restart ─────────────────────────────────────────────────────\n\n    /// Restart a closed server on a new port. All data is preserved.\n    /// The previous server must have been closed with [`close()`](Self::close).\n    pub async fn restart(&mut self) -> Result<()> {\n        let listener = TcpListener::bind(\"127.0.0.1:0\").await?;\n        self.addr = listener.local_addr()?;\n        let state_clone = Arc::clone(&self.state);\n        let shutdown_rx = self.state.shutdown_tx.subscribe();\n\n        tokio::spawn(async move {\n            server::run(listener, state_clone, shutdown_rx, None).await;\n        });\n\n        Ok(())\n    }\n\n    // ── Dump ────────────────────────────────────────────────────────\n\n    /// Return a text representation of the selected database, useful for\n    /// debugging.\n    pub fn dump(&self) -> String {\n        let inner = self.state.lock();\n        let db = inner.db(self.selected_db);\n        dump_db(db)\n    }\n\n    // ── Internals (for advanced usage) ───────────────────────────────\n\n    /// Get a reference to the shared state (for custom commands, etc.).\n    pub fn shared_state(&self) -> &Arc<SharedState> {\n        &self.state\n    }\n\n    /// Number of keys in the selected database.\n    pub fn db_size(&self) -> usize {\n        let inner = self.state.lock();\n        inner.db(self.selected_db).keys.len()\n    }\n}\n\n/// A handle to a specific database, returned by [`Miniredis::db()`].\n/// Provides the same direct-access methods scoped to the given DB index.\npub struct DbRef<'a> {\n    state: &'a Arc<SharedState>,\n    db_id: usize,\n}\n\nimpl DbRef<'_> {\n    /// Return all keys, sorted.\n    pub fn keys(&self) -> Vec<String> {\n        let inner = self.state.lock();\n        inner.db(self.db_id).all_keys()\n    }\n\n    /// Get a string key value.\n    pub fn get(&self, key: &str) -> Option<String> {\n        let inner = self.state.lock();\n        inner\n            .db(self.db_id)\n            .string_get(key)\n            .map(|v| String::from_utf8_lossy(v).into_owned())\n    }\n\n    /// Set a string key. Removes any existing TTL.\n    pub fn set(&self, key: &str, value: &str) {\n        let mut inner = self.state.lock();\n        let now = inner.effective_now();\n        let db = inner.db_mut(self.db_id);\n        db.string_set(key, value.as_bytes().to_vec(), now);\n        db.ttl.remove(key);\n    }\n\n    /// Check if a key exists.\n    pub fn exists(&self, key: &str) -> bool {\n        let mut inner = self.state.lock();\n        let now = inner.effective_now();\n        inner.db_mut(self.db_id).exists(key, now)\n    }\n\n    /// Return the type of a key.\n    pub fn key_type(&self, key: &str) -> &'static str {\n        let inner = self.state.lock();\n        match inner.db(self.db_id).key_type(key) {\n            Some(t) => t.as_str(),\n            None => \"none\",\n        }\n    }\n\n    /// Number of keys in this database.\n    pub fn db_size(&self) -> usize {\n        let inner = self.state.lock();\n        inner.db(self.db_id).keys.len()\n    }\n\n    /// Return a text representation of this database.\n    pub fn dump(&self) -> String {\n        let inner = self.state.lock();\n        dump_db(inner.db(self.db_id))\n    }\n}\n\n/// Maximum number of characters to show per value in [`Miniredis::dump()`].\nconst DUMP_MAX_LINE_LEN: usize = 200;\n\nfn dump_db(db: &db::RedisDB) -> String {\n    use std::fmt::Write;\n    use types::Direction;\n\n    let indent = \"   \";\n    let mut r = String::new();\n\n    let truncate = |s: &str| -> String {\n        if s.len() > DUMP_MAX_LINE_LEN {\n            let suffix = format!(\"...({})\", s.len());\n            let end = DUMP_MAX_LINE_LEN - suffix.len();\n            format!(\"{:?}{}\", &s[..end], suffix)\n        } else {\n            format!(\"{:?}\", s)\n        }\n    };\n\n    for k in db.all_keys() {\n        let _ = writeln!(r, \"- {}\", k);\n        match db.key_type(&k) {\n            Some(types::KeyType::String) => {\n                if let Some(v) = db.string_get(&k) {\n                    let _ = writeln!(r, \"{}{}\", indent, truncate(&String::from_utf8_lossy(v)));\n                }\n            }\n            Some(types::KeyType::Hash) => {\n                if let Some(hash) = db.hash_keys.get(&k) {\n                    let mut fields: Vec<&String> = hash.keys().collect();\n                    fields.sort();\n                    for f in fields {\n                        let v = String::from_utf8_lossy(&hash[f]);\n                        let _ = writeln!(r, \"{}{}: {}\", indent, f, truncate(&v));\n                    }\n                }\n            }\n            Some(types::KeyType::List) => {\n                if let Some(list) = db.list_keys.get(&k) {\n                    for item in list {\n                        let _ =\n                            writeln!(r, \"{}{}\", indent, truncate(&String::from_utf8_lossy(item)));\n                    }\n                }\n            }\n            Some(types::KeyType::Set) => {\n                if let Some(set) = db.set_keys.get(&k) {\n                    let mut members: Vec<&String> = set.iter().collect();\n                    members.sort();\n                    for m in members {\n                        let _ = writeln!(r, \"{}{}\", indent, truncate(m));\n                    }\n                }\n            }\n            Some(types::KeyType::SortedSet) => {\n                if let Some(ss) = db.sorted_set_keys.get(&k) {\n                    for el in ss.by_score(Direction::Asc) {\n                        let _ = writeln!(r, \"{}{}: {}\", indent, el.score, truncate(&el.member));\n                    }\n                }\n            }\n            Some(types::KeyType::Stream) => {\n                if let Some(stream) = db.stream_keys.get(&k) {\n                    for entry in &stream.entries {\n                        let _ = writeln!(r, \"{}{}\", indent, entry.id);\n                        let ev = &entry.values;\n                        let mut i = 0;\n                        while i + 1 < ev.len() {\n                            let _ = writeln!(\n                                r,\n                                \"{}{}{}: {}\",\n                                indent,\n                                indent,\n                                truncate(&ev[i]),\n                                truncate(&ev[i + 1])\n                            );\n                            i += 2;\n                        }\n                    }\n                }\n            }\n            Some(types::KeyType::HyperLogLog) => {\n                let _ = writeln!(r, \"{}(HyperLogLog)\", indent);\n            }\n            None => {}\n        }\n    }\n    r\n}\n"
  },
  {
    "path": "miniredis/src/pubsub.rs",
    "content": "/// Pub/Sub message delivery infrastructure.\nuse std::collections::HashSet;\nuse std::sync::{Arc, Mutex};\nuse tokio::sync::mpsc;\n\n/// A message delivered to a pub/sub subscriber.\npub struct PubsubMessage {\n    /// \"message\" or \"pmessage\"\n    pub kind: &'static str,\n    /// The pattern that matched (for pmessage only)\n    pub pattern: Option<String>,\n    /// The channel the message was published to\n    pub channel: String,\n    /// The message payload\n    pub data: String,\n}\n\n/// A pattern matcher: (pattern_string, compiled_matcher).\npub type PatternMatcher = (String, Box<dyn Fn(&str) -> bool + Send + Sync>);\n\n/// Per-subscriber state: channels, patterns, and message sender.\npub struct SubscriberInner {\n    pub channels: HashSet<String>,\n    pub patterns: Vec<PatternMatcher>,\n    pub tx: mpsc::UnboundedSender<PubsubMessage>,\n}\n\n/// Handle to a subscriber, stored in the global registry.\npub type SubscriberHandle = Arc<Mutex<SubscriberInner>>;\n\n/// Global pub/sub subscriber registry.\npub struct PubsubRegistry {\n    subscribers: Vec<SubscriberHandle>,\n}\n\nimpl Default for PubsubRegistry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl PubsubRegistry {\n    pub fn new() -> Self {\n        PubsubRegistry {\n            subscribers: Vec::new(),\n        }\n    }\n\n    pub fn add(&mut self, handle: SubscriberHandle) {\n        self.subscribers.push(handle);\n    }\n\n    pub fn remove(&mut self, handle: &SubscriberHandle) {\n        self.subscribers.retain(|s| !Arc::ptr_eq(s, handle));\n    }\n\n    /// Publish a message to all matching subscribers. Returns total delivery count.\n    pub fn publish(&self, channel: &str, message: &str) -> i64 {\n        let mut count = 0i64;\n        for sub in &self.subscribers {\n            let inner = sub.lock().unwrap();\n            // Check direct channel subscription\n            if inner.channels.contains(channel) {\n                let _ = inner.tx.send(PubsubMessage {\n                    kind: \"message\",\n                    pattern: None,\n                    channel: channel.to_string(),\n                    data: message.to_string(),\n                });\n                count += 1;\n            }\n            // Check pattern subscriptions\n            for (pat_str, matcher) in &inner.patterns {\n                if matcher(channel) {\n                    let _ = inner.tx.send(PubsubMessage {\n                        kind: \"pmessage\",\n                        pattern: Some(pat_str.clone()),\n                        channel: channel.to_string(),\n                        data: message.to_string(),\n                    });\n                    count += 1;\n                    break; // only one match per subscriber\n                }\n            }\n        }\n        count\n    }\n\n    /// Return all unique channels that have at least one subscriber, optionally filtered by pattern.\n    pub fn active_channels(&self, pattern: Option<&str>) -> Vec<String> {\n        let mut channels = HashSet::new();\n        for sub in &self.subscribers {\n            let inner = sub.lock().unwrap();\n            for ch in &inner.channels {\n                channels.insert(ch.clone());\n            }\n        }\n        let mut result: Vec<String> = if let Some(pat) = pattern {\n            channels\n                .into_iter()\n                .filter(|ch| crate::keys::glob_match(pat, ch))\n                .collect()\n        } else {\n            channels.into_iter().collect()\n        };\n        result.sort();\n        result\n    }\n\n    /// Count subscribers for a specific channel (direct subscriptions only).\n    pub fn numsub(&self, channel: &str) -> i64 {\n        let mut count = 0i64;\n        for sub in &self.subscribers {\n            let inner = sub.lock().unwrap();\n            if inner.channels.contains(channel) {\n                count += 1;\n            }\n        }\n        count\n    }\n\n    /// Total number of pattern subscriptions across all subscribers.\n    pub fn numpat(&self) -> i64 {\n        let mut count = 0i64;\n        for sub in &self.subscribers {\n            let inner = sub.lock().unwrap();\n            count += inner.patterns.len() as i64;\n        }\n        count\n    }\n}\n\n/// Per-connection pub/sub context.\npub struct PubsubCtx {\n    /// Shared handle registered in the global registry.\n    pub handle: SubscriberHandle,\n    /// Receiver for pub/sub messages.\n    pub rx: mpsc::UnboundedReceiver<PubsubMessage>,\n}\n\nimpl PubsubCtx {\n    /// Create a new pub/sub context. Returns the context and registers it in the registry.\n    pub fn new(registry: &mut PubsubRegistry) -> Self {\n        let (tx, rx) = mpsc::unbounded_channel();\n        let inner = SubscriberInner {\n            channels: HashSet::new(),\n            patterns: Vec::new(),\n            tx,\n        };\n        let handle = Arc::new(Mutex::new(inner));\n        registry.add(handle.clone());\n        PubsubCtx { handle, rx }\n    }\n\n    /// Subscribe to a channel. Returns the total subscription count.\n    pub fn subscribe(&self, channel: &str) -> usize {\n        let mut inner = self.handle.lock().unwrap();\n        inner.channels.insert(channel.to_string());\n        inner.channels.len() + inner.patterns.len()\n    }\n\n    /// Unsubscribe from a channel. Returns the total subscription count.\n    pub fn unsubscribe(&self, channel: &str) -> usize {\n        let mut inner = self.handle.lock().unwrap();\n        inner.channels.remove(channel);\n        inner.channels.len() + inner.patterns.len()\n    }\n\n    /// Subscribe to a pattern. Returns the total subscription count.\n    pub fn psubscribe(&self, pattern: &str) -> usize {\n        let pat = pattern.to_string();\n        let pat_clone = pat.clone();\n        let matcher: Box<dyn Fn(&str) -> bool + Send + Sync> =\n            Box::new(move |text: &str| crate::keys::glob_match(&pat_clone, text));\n        let mut inner = self.handle.lock().unwrap();\n        inner.patterns.push((pat, matcher));\n        inner.channels.len() + inner.patterns.len()\n    }\n\n    /// Unsubscribe from a pattern. Returns the total subscription count.\n    pub fn punsubscribe(&self, pattern: &str) -> usize {\n        let mut inner = self.handle.lock().unwrap();\n        inner.patterns.retain(|(p, _)| p != pattern);\n        inner.channels.len() + inner.patterns.len()\n    }\n\n    /// Get all subscribed channel names.\n    pub fn channels(&self) -> Vec<String> {\n        let inner = self.handle.lock().unwrap();\n        let mut channels: Vec<String> = inner.channels.iter().cloned().collect();\n        channels.sort();\n        channels\n    }\n\n    /// Get all subscribed pattern strings.\n    pub fn patterns(&self) -> Vec<String> {\n        let inner = self.handle.lock().unwrap();\n        inner.patterns.iter().map(|(p, _)| p.clone()).collect()\n    }\n\n    /// Total subscription count (channels + patterns).\n    pub fn total_count(&self) -> usize {\n        let inner = self.handle.lock().unwrap();\n        inner.channels.len() + inner.patterns.len()\n    }\n}\n"
  },
  {
    "path": "miniredis/src/server.rs",
    "content": "use std::sync::Arc;\n\nuse tokio::net::TcpListener;\nuse tokio::sync::broadcast;\n\nuse crate::connection::{ConnCtx, Connection};\nuse crate::db::SharedState;\nuse crate::dispatch::{CommandTable, dispatch, err_wrong_number};\nuse crate::frame::Frame;\nuse crate::pubsub::PubsubCtx;\n\n/// Start the server: bind to the given address, accept connections, and\n/// dispatch commands.\n///\n/// Returns the bound address (useful when binding to port 0).\n/// The server runs until a shutdown signal is received via `shutdown_rx`.\n/// If `tls_acceptor` is Some, connections are wrapped with TLS.\npub async fn run(\n    listener: TcpListener,\n    state: Arc<SharedState>,\n    mut shutdown_rx: broadcast::Receiver<()>,\n    #[cfg(feature = \"tls\")] tls_acceptor: Option<tokio_rustls::TlsAcceptor>,\n    #[cfg(not(feature = \"tls\"))] _tls_acceptor: Option<()>,\n) {\n    let table = Arc::new(CommandTable::new());\n    let _ = state.command_table.set(Arc::clone(&table));\n\n    loop {\n        tokio::select! {\n            result = listener.accept() => {\n                let (socket, _addr) = match result {\n                    Ok(s) => s,\n                    Err(_) => continue,\n                };\n\n                let state = Arc::clone(&state);\n                let table = Arc::clone(&table);\n                let shutdown_rx = state.shutdown_tx.subscribe();\n\n                #[cfg(feature = \"tls\")]\n                let tls = tls_acceptor.clone();\n\n                tokio::spawn(async move {\n                    #[cfg(feature = \"tls\")]\n                    if let Some(acceptor) = tls {\n                        if let Ok(tls_stream) = acceptor.accept(socket).await {\n                            handle_connection_stream(\n                                Connection::new_stream(tls_stream),\n                                state,\n                                table,\n                                shutdown_rx,\n                            ).await;\n                            return;\n                        }\n                        return;\n                    }\n\n                    handle_connection_stream(\n                        Connection::new(socket),\n                        state,\n                        table,\n                        shutdown_rx,\n                    ).await;\n                });\n            }\n            _ = shutdown_rx.recv() => {\n                // Shutdown signal received\n                return;\n            }\n        }\n    }\n}\n\n/// Handle a single client connection (plain or TLS).\nasync fn handle_connection_stream(\n    mut conn: Connection,\n    state: Arc<SharedState>,\n    table: Arc<CommandTable>,\n    mut shutdown_rx: broadcast::Receiver<()>,\n) {\n    use std::sync::atomic::Ordering;\n\n    state\n        .total_connections_received\n        .fetch_add(1, Ordering::Relaxed);\n    state.connected_clients.fetch_add(1, Ordering::Relaxed);\n\n    let mut ctx = ConnCtx::new();\n    let mut pubsub: Option<PubsubCtx> = None;\n\n    handle_connection_inner(\n        &mut conn,\n        &mut ctx,\n        &mut pubsub,\n        &state,\n        &table,\n        &mut shutdown_rx,\n    )\n    .await;\n\n    // Cleanup: remove subscriber from registry if in pub/sub mode\n    if let Some(ps) = pubsub.take() {\n        let mut registry = state.pubsub.lock().unwrap();\n        registry.remove(&ps.handle);\n    }\n\n    state.connected_clients.fetch_sub(1, Ordering::Relaxed);\n}\n\nasync fn handle_connection_inner(\n    conn: &mut Connection,\n    ctx: &mut ConnCtx,\n    pubsub: &mut Option<PubsubCtx>,\n    state: &Arc<SharedState>,\n    table: &Arc<CommandTable>,\n    shutdown_rx: &mut broadcast::Receiver<()>,\n) {\n    loop {\n        if let Some(ps) = pubsub.as_mut() {\n            // ── Pub/Sub mode event loop ────────────────────────────\n            tokio::select! {\n                result = conn.read_frame() => {\n                    let frame = match result {\n                        Ok(Some(frame)) => frame,\n                        Ok(None) => return,\n                        Err(_) => return,\n                    };\n\n                    let args = match frame_to_args(frame) {\n                        Some(args) => args,\n                        None => {\n                            let _ = conn.write_frame(&Frame::error(\"ERR invalid command format\")).await;\n                            continue;\n                        }\n                    };\n\n                    if args.is_empty() {\n                        let _ = conn.write_frame(&Frame::error(\"ERR empty command\")).await;\n                        continue;\n                    }\n\n                    let cmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n                    let cmd_args = &args[1..];\n\n                    match cmd.as_str() {\n                        \"SUBSCRIBE\" => {\n                            if cmd_args.is_empty() {\n                                let _ = conn.write_frame(&Frame::error(err_wrong_number(\"subscribe\"))).await;\n                                continue;\n                            }\n                            for arg in cmd_args {\n                                let channel = String::from_utf8_lossy(arg).to_string();\n                                let count = ps.subscribe(&channel);\n                                let confirm = pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"subscribe\".into()),\n                                    Frame::Bulk(channel.into()),\n                                    Frame::Integer(count as i64),\n                                ]);\n                                if conn.write_frame(&confirm).await.is_err() {\n                                    return;\n                                }\n                            }\n                        }\n                        \"UNSUBSCRIBE\" => {\n                            if cmd_args.is_empty() {\n                                // Unsubscribe from all channels\n                                let channels = ps.channels();\n                                if channels.is_empty() {\n                                    let confirm = pubsub_msg(ctx.resp3, vec![\n                                        Frame::Bulk(\"unsubscribe\".into()),\n                                        Frame::Null,\n                                        Frame::Integer(ps.total_count() as i64),\n                                    ]);\n                                    if conn.write_frame(&confirm).await.is_err() {\n                                        return;\n                                    }\n                                } else {\n                                    for channel in channels {\n                                        let count = ps.unsubscribe(&channel);\n                                        let confirm = pubsub_msg(ctx.resp3, vec![\n                                            Frame::Bulk(\"unsubscribe\".into()),\n                                            Frame::Bulk(channel.into()),\n                                            Frame::Integer(count as i64),\n                                        ]);\n                                        if conn.write_frame(&confirm).await.is_err() {\n                                            return;\n                                        }\n                                    }\n                                }\n                            } else {\n                                for arg in cmd_args {\n                                    let channel = String::from_utf8_lossy(arg).to_string();\n                                    let count = ps.unsubscribe(&channel);\n                                    let confirm = pubsub_msg(ctx.resp3, vec![\n                                        Frame::Bulk(\"unsubscribe\".into()),\n                                        Frame::Bulk(channel.into()),\n                                        Frame::Integer(count as i64),\n                                    ]);\n                                    if conn.write_frame(&confirm).await.is_err() {\n                                        return;\n                                    }\n                                }\n                            }\n                            // Exit pub/sub mode if no subscriptions left\n                            if ps.total_count() == 0 {\n                                let handle = &ps.handle;\n                                let mut registry = state.pubsub.lock().unwrap();\n                                registry.remove(handle);\n                                drop(registry);\n                                *pubsub = None;\n                            }\n                        }\n                        \"PSUBSCRIBE\" => {\n                            if cmd_args.is_empty() {\n                                let _ = conn.write_frame(&Frame::error(err_wrong_number(\"psubscribe\"))).await;\n                                continue;\n                            }\n                            for arg in cmd_args {\n                                let pattern = String::from_utf8_lossy(arg).to_string();\n                                let count = ps.psubscribe(&pattern);\n                                let confirm = pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"psubscribe\".into()),\n                                    Frame::Bulk(pattern.into()),\n                                    Frame::Integer(count as i64),\n                                ]);\n                                if conn.write_frame(&confirm).await.is_err() {\n                                    return;\n                                }\n                            }\n                        }\n                        \"PUNSUBSCRIBE\" => {\n                            if cmd_args.is_empty() {\n                                let patterns = ps.patterns();\n                                if patterns.is_empty() {\n                                    let confirm = pubsub_msg(ctx.resp3, vec![\n                                        Frame::Bulk(\"punsubscribe\".into()),\n                                        Frame::Null,\n                                        Frame::Integer(ps.total_count() as i64),\n                                    ]);\n                                    if conn.write_frame(&confirm).await.is_err() {\n                                        return;\n                                    }\n                                } else {\n                                    for pattern in patterns {\n                                        let count = ps.punsubscribe(&pattern);\n                                        let confirm = pubsub_msg(ctx.resp3, vec![\n                                            Frame::Bulk(\"punsubscribe\".into()),\n                                            Frame::Bulk(pattern.into()),\n                                            Frame::Integer(count as i64),\n                                        ]);\n                                        if conn.write_frame(&confirm).await.is_err() {\n                                            return;\n                                        }\n                                    }\n                                }\n                            } else {\n                                for arg in cmd_args {\n                                    let pattern = String::from_utf8_lossy(arg).to_string();\n                                    let count = ps.punsubscribe(&pattern);\n                                    let confirm = pubsub_msg(ctx.resp3, vec![\n                                        Frame::Bulk(\"punsubscribe\".into()),\n                                        Frame::Bulk(pattern.into()),\n                                        Frame::Integer(count as i64),\n                                    ]);\n                                    if conn.write_frame(&confirm).await.is_err() {\n                                        return;\n                                    }\n                                }\n                            }\n                            // Exit pub/sub mode if no subscriptions left\n                            if ps.total_count() == 0 {\n                                let handle = &ps.handle;\n                                let mut registry = state.pubsub.lock().unwrap();\n                                registry.remove(handle);\n                                drop(registry);\n                                *pubsub = None;\n                            }\n                        }\n                        \"PING\" => {\n                            if cmd_args.is_empty() {\n                                let pong = pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"pong\".into()),\n                                    Frame::Bulk(\"\".into()),\n                                ]);\n                                if conn.write_frame(&pong).await.is_err() {\n                                    return;\n                                }\n                            } else {\n                                let msg = String::from_utf8_lossy(&cmd_args[0]).to_string();\n                                let pong = pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"pong\".into()),\n                                    Frame::Bulk(msg.into()),\n                                ]);\n                                if conn.write_frame(&pong).await.is_err() {\n                                    return;\n                                }\n                            }\n                        }\n                        \"QUIT\" => {\n                            let _ = conn.write_frame(&Frame::ok()).await;\n                            return;\n                        }\n                        _ => {\n                            let err = format!(\n                                \"ERR Can't execute '{}': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT are allowed in this context\",\n                                cmd.to_lowercase()\n                            );\n                            if conn.write_frame(&Frame::error(err)).await.is_err() {\n                                return;\n                            }\n                        }\n                    }\n                }\n                msg = ps.rx.recv() => {\n                    match msg {\n                        Some(m) => {\n                            let frame = match m.kind {\n                                \"message\" => pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"message\".into()),\n                                    Frame::Bulk(m.channel.into()),\n                                    Frame::Bulk(m.data.into()),\n                                ]),\n                                \"pmessage\" => pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"pmessage\".into()),\n                                    Frame::Bulk(m.pattern.unwrap_or_default().into()),\n                                    Frame::Bulk(m.channel.into()),\n                                    Frame::Bulk(m.data.into()),\n                                ]),\n                                _ => continue,\n                            };\n                            if conn.write_frame(&frame).await.is_err() {\n                                return;\n                            }\n                        }\n                        None => return, // channel closed\n                    }\n                }\n                _ = shutdown_rx.recv() => {\n                    return;\n                }\n            }\n        } else {\n            // ── Normal command loop ────────────────────────────────\n            tokio::select! {\n                result = conn.read_frame() => {\n                    let frame = match result {\n                        Ok(Some(frame)) => frame,\n                        Ok(None) => return,\n                        Err(_) => return,\n                    };\n\n                    let args = match frame_to_args(frame) {\n                        Some(args) => args,\n                        None => {\n                            let _ = conn.write_frame(&Frame::error(\"ERR invalid command format\")).await;\n                            continue;\n                        }\n                    };\n\n                    if args.is_empty() {\n                        let _ = conn.write_frame(&Frame::error(\"ERR empty command\")).await;\n                        continue;\n                    }\n\n                    let cmd = String::from_utf8_lossy(&args[0]).to_uppercase();\n\n                    // Handle SUBSCRIBE/PSUBSCRIBE — enter pub/sub mode\n                    // (but not inside MULTI — let dispatch queue it)\n                    if (cmd == \"SUBSCRIBE\" || cmd == \"PSUBSCRIBE\") && !ctx.in_tx() {\n                        let cmd_args = &args[1..];\n                        if cmd_args.is_empty() {\n                            let _ = conn.write_frame(&Frame::error(err_wrong_number(&cmd.to_lowercase()))).await;\n                            continue;\n                        }\n\n                        // Create pub/sub context\n                        let ps = {\n                            let mut registry = state.pubsub.lock().unwrap();\n                            PubsubCtx::new(&mut registry)\n                        };\n\n                        *pubsub = Some(ps);\n                        let ps = pubsub.as_mut().unwrap();\n\n                        if cmd == \"SUBSCRIBE\" {\n                            for arg in cmd_args {\n                                let channel = String::from_utf8_lossy(arg).to_string();\n                                let count = ps.subscribe(&channel);\n                                let confirm = pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"subscribe\".into()),\n                                    Frame::Bulk(channel.into()),\n                                    Frame::Integer(count as i64),\n                                ]);\n                                if conn.write_frame(&confirm).await.is_err() {\n                                    return;\n                                }\n                            }\n                        } else {\n                            for arg in cmd_args {\n                                let pattern = String::from_utf8_lossy(arg).to_string();\n                                let count = ps.psubscribe(&pattern);\n                                let confirm = pubsub_msg(ctx.resp3, vec![\n                                    Frame::Bulk(\"psubscribe\".into()),\n                                    Frame::Bulk(pattern.into()),\n                                    Frame::Integer(count as i64),\n                                ]);\n                                if conn.write_frame(&confirm).await.is_err() {\n                                    return;\n                                }\n                            }\n                        }\n                        continue;\n                    }\n\n                    // Handle UNSUBSCRIBE/PUNSUBSCRIBE outside pub/sub mode (no-op)\n                    // But not inside MULTI — let dispatch queue it.\n                    if cmd == \"UNSUBSCRIBE\" && !ctx.in_tx() {\n                        let confirm = pubsub_msg(ctx.resp3, vec![\n                            Frame::Bulk(\"unsubscribe\".into()),\n                            Frame::Null,\n                            Frame::Integer(0),\n                        ]);\n                        let _ = conn.write_frame(&confirm).await;\n                        continue;\n                    }\n                    if cmd == \"PUNSUBSCRIBE\" && !ctx.in_tx() {\n                        let confirm = pubsub_msg(ctx.resp3, vec![\n                            Frame::Bulk(\"punsubscribe\".into()),\n                            Frame::Null,\n                            Frame::Integer(0),\n                        ]);\n                        let _ = conn.write_frame(&confirm).await;\n                        continue;\n                    }\n\n                    state.total_commands_processed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n\n                    // Intercept blocking commands (outside MULTI/EXEC)\n                    if !ctx.in_tx() && matches!(cmd.as_str(), \"BLPOP\" | \"BRPOP\" | \"BRPOPLPUSH\" | \"BLMOVE\") {\n                        let response = handle_blocking_command(\n                            &cmd, &args[1..], state, ctx, shutdown_rx\n                        ).await;\n\n                        conn.resp3 = ctx.resp3;\n                        if conn.write_frame(&response).await.is_err() {\n                            return;\n                        }\n                        continue;\n                    }\n\n                    // Intercept XREAD/XREADGROUP with BLOCK (outside MULTI/EXEC)\n                    if !ctx.in_tx() && matches!(cmd.as_str(), \"XREAD\" | \"XREADGROUP\")\n                        && has_block_arg(&args[1..])\n                    {\n                        let response = handle_blocking_stream_command(\n                            &cmd, &args[1..], state, ctx, shutdown_rx\n                        ).await;\n\n                        conn.resp3 = ctx.resp3;\n                        if conn.write_frame(&response).await.is_err() {\n                            return;\n                        }\n                        continue;\n                    }\n\n                    let (response, should_close) = dispatch(table, state, ctx, &args);\n\n                    // Sync RESP3 flag (set by HELLO command)\n                    conn.resp3 = ctx.resp3;\n\n                    if conn.write_frame(&response).await.is_err() {\n                        return;\n                    }\n\n                    if should_close {\n                        return;\n                    }\n\n                    // Check if SUBSCRIBE/PSUBSCRIBE was executed inside EXEC.\n                    // If so, enter pub/sub mode with the pending channels/patterns.\n                    if !ctx.pending_subscribe.is_empty() || !ctx.pending_psubscribe.is_empty() {\n                        let channels = std::mem::take(&mut ctx.pending_subscribe);\n                        let patterns = std::mem::take(&mut ctx.pending_psubscribe);\n\n                        let ps = {\n                            let mut registry = state.pubsub.lock().unwrap();\n                            PubsubCtx::new(&mut registry)\n                        };\n                        *pubsub = Some(ps);\n                        let ps = pubsub.as_mut().unwrap();\n\n                        for channel in channels {\n                            ps.subscribe(&channel);\n                        }\n                        for pattern in patterns {\n                            ps.psubscribe(&pattern);\n                        }\n                    }\n                }\n                _ = shutdown_rx.recv() => {\n                    return;\n                }\n            }\n        }\n    }\n}\n\n/// Wrap pub/sub confirmation/message frames: use Push in RESP3, Array in RESP2.\nfn pubsub_msg(resp3: bool, elements: Vec<Frame>) -> Frame {\n    if resp3 {\n        Frame::Push(elements)\n    } else {\n        Frame::Array(elements)\n    }\n}\n\n/// Handle blocking list commands (BLPOP, BRPOP, BRPOPLPUSH, BLMOVE).\n/// These block until data is available or timeout expires.\nasync fn handle_blocking_command(\n    cmd: &str,\n    args: &[Vec<u8>],\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    shutdown_rx: &mut broadcast::Receiver<()>,\n) -> Frame {\n    use crate::dispatch::{\n        MSG_INVALID_TIMEOUT, MSG_SYNTAX_ERROR, MSG_TIMEOUT_IS_OUT_OF_RANGE, MSG_TIMEOUT_NEGATIVE,\n        MSG_WRONG_TYPE,\n    };\n    use crate::types::KeyType;\n\n    match cmd {\n        \"BLPOP\" | \"BRPOP\" => {\n            if args.len() < 2 {\n                return Frame::error(crate::dispatch::err_wrong_number(&cmd.to_lowercase()));\n            }\n            let keys = &args[..args.len() - 1];\n            let timeout_str = String::from_utf8_lossy(&args[args.len() - 1]);\n            let timeout_lower = timeout_str.to_lowercase();\n            if timeout_lower == \"inf\" || timeout_lower == \"+inf\" || timeout_lower == \"-inf\" {\n                return Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE);\n            }\n            let timeout_s: f64 = match timeout_str.parse() {\n                Ok(t) => t,\n                Err(_) => return Frame::error(MSG_INVALID_TIMEOUT),\n            };\n            if timeout_s < 0.0 {\n                return Frame::error(MSG_TIMEOUT_NEGATIVE);\n            }\n            let is_left = cmd == \"BLPOP\";\n\n            // Try immediate pop\n            {\n                let mut inner = state.lock();\n                let now = inner.effective_now();\n                for key_bytes in keys {\n                    let key = String::from_utf8_lossy(key_bytes).into_owned();\n                    let db = inner.db_mut(ctx.selected_db);\n                    db.check_ttl(&key);\n                    if let Some(t) = db.key_type(&key)\n                        && t != KeyType::List\n                    {\n                        return Frame::error(MSG_WRONG_TYPE);\n                    }\n                    let val = if is_left {\n                        db.list_lpop(&key, now)\n                    } else {\n                        db.list_rpop(&key, now)\n                    };\n                    if let Some(v) = val {\n                        state.notify.notify_waiters();\n                        return Frame::Array(vec![Frame::Bulk(key.into()), Frame::Bulk(v.into())]);\n                    }\n                }\n            }\n\n            // Block until data or timeout\n            let timeout_dur = if timeout_s == 0.0 {\n                std::time::Duration::from_secs(300) // max wait\n            } else {\n                std::time::Duration::from_secs_f64(timeout_s)\n            };\n            let deadline = tokio::time::Instant::now() + timeout_dur;\n\n            loop {\n                tokio::select! {\n                    _ = state.notify.notified() => {\n                        let mut inner = state.lock();\n                        let now = inner.effective_now();\n                        for key_bytes in keys {\n                            let key = String::from_utf8_lossy(key_bytes).into_owned();\n                            let db = inner.db_mut(ctx.selected_db);\n                            db.check_ttl(&key);\n                            let val = if is_left {\n                                db.list_lpop(&key, now)\n                            } else {\n                                db.list_rpop(&key, now)\n                            };\n                            if let Some(v) = val {\n                                state.notify.notify_waiters();\n                                return Frame::Array(vec![\n                                    Frame::Bulk(key.into()),\n                                    Frame::Bulk(v.into()),\n                                ]);\n                            }\n                        }\n                    }\n                    _ = tokio::time::sleep_until(deadline) => {\n                        return Frame::NullArray;\n                    }\n                    _ = shutdown_rx.recv() => {\n                        return Frame::NullArray;\n                    }\n                }\n            }\n        }\n        \"BRPOPLPUSH\" => {\n            if args.len() != 3 {\n                return Frame::error(crate::dispatch::err_wrong_number(\"brpoplpush\"));\n            }\n            let src = String::from_utf8_lossy(&args[0]).into_owned();\n            let dst = String::from_utf8_lossy(&args[1]).into_owned();\n            let timeout_str = String::from_utf8_lossy(&args[2]);\n            let timeout_lower = timeout_str.to_lowercase();\n            if timeout_lower == \"inf\" || timeout_lower == \"+inf\" || timeout_lower == \"-inf\" {\n                return Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE);\n            }\n            let timeout_s: f64 = match timeout_str.parse() {\n                Ok(t) => t,\n                Err(_) => return Frame::error(MSG_INVALID_TIMEOUT),\n            };\n            if timeout_s < 0.0 {\n                return Frame::error(MSG_TIMEOUT_NEGATIVE);\n            }\n\n            // Try immediate\n            {\n                let mut inner = state.lock();\n                let now = inner.effective_now();\n                let db = inner.db_mut(ctx.selected_db);\n                db.check_ttl(&src);\n                db.check_ttl(&dst);\n                if let Some(t) = db.key_type(&src)\n                    && t != KeyType::List\n                {\n                    return Frame::error(MSG_WRONG_TYPE);\n                }\n                if let Some(t) = db.key_type(&dst)\n                    && t != KeyType::List\n                {\n                    return Frame::error(MSG_WRONG_TYPE);\n                }\n                if let Some(val) = db.list_rpop(&src, now) {\n                    db.list_lpush(&dst, std::slice::from_ref(&val), now);\n                    state.notify.notify_waiters();\n                    return Frame::Bulk(val.into());\n                }\n            }\n\n            let timeout_dur = if timeout_s == 0.0 {\n                std::time::Duration::from_secs(300)\n            } else {\n                std::time::Duration::from_secs_f64(timeout_s)\n            };\n            let deadline = tokio::time::Instant::now() + timeout_dur;\n\n            loop {\n                tokio::select! {\n                    _ = state.notify.notified() => {\n                        let mut inner = state.lock();\n                        let now = inner.effective_now();\n                        let db = inner.db_mut(ctx.selected_db);\n                        db.check_ttl(&src);\n                        if let Some(val) = db.list_rpop(&src, now) {\n                            db.list_lpush(&dst, std::slice::from_ref(&val), now);\n                            state.notify.notify_waiters();\n                            return Frame::Bulk(val.into());\n                        }\n                    }\n                    _ = tokio::time::sleep_until(deadline) => {\n                        return Frame::NullArray;\n                    }\n                    _ = shutdown_rx.recv() => {\n                        return Frame::NullArray;\n                    }\n                }\n            }\n        }\n        \"BLMOVE\" => {\n            if args.len() != 5 {\n                return Frame::error(crate::dispatch::err_wrong_number(\"blmove\"));\n            }\n            let src = String::from_utf8_lossy(&args[0]).into_owned();\n            let dst = String::from_utf8_lossy(&args[1]).into_owned();\n            let src_dir = String::from_utf8_lossy(&args[2]).to_uppercase();\n            let dst_dir = String::from_utf8_lossy(&args[3]).to_uppercase();\n\n            let pop_left = match src_dir.as_str() {\n                \"LEFT\" => true,\n                \"RIGHT\" => false,\n                _ => return Frame::error(MSG_SYNTAX_ERROR),\n            };\n            let push_left = match dst_dir.as_str() {\n                \"LEFT\" => true,\n                \"RIGHT\" => false,\n                _ => return Frame::error(MSG_SYNTAX_ERROR),\n            };\n\n            let timeout_str = String::from_utf8_lossy(&args[4]);\n            let timeout_lower = timeout_str.to_lowercase();\n            if timeout_lower == \"inf\" || timeout_lower == \"+inf\" || timeout_lower == \"-inf\" {\n                return Frame::error(MSG_TIMEOUT_IS_OUT_OF_RANGE);\n            }\n            let timeout_s: f64 = match timeout_str.parse() {\n                Ok(t) => t,\n                Err(_) => return Frame::error(MSG_INVALID_TIMEOUT),\n            };\n            if timeout_s < 0.0 {\n                return Frame::error(MSG_TIMEOUT_NEGATIVE);\n            }\n\n            // Try immediate\n            {\n                let mut inner = state.lock();\n                let now = inner.effective_now();\n                let db = inner.db_mut(ctx.selected_db);\n                db.check_ttl(&src);\n                db.check_ttl(&dst);\n                if let Some(t) = db.key_type(&src)\n                    && t != KeyType::List\n                {\n                    return Frame::error(MSG_WRONG_TYPE);\n                }\n                if let Some(t) = db.key_type(&dst)\n                    && t != KeyType::List\n                {\n                    return Frame::error(MSG_WRONG_TYPE);\n                }\n\n                // Save TTL when src == dst\n                let saved_ttl = if src == dst {\n                    db.ttl.get(&src).cloned()\n                } else {\n                    None\n                };\n                let val = if pop_left {\n                    db.list_lpop(&src, now)\n                } else {\n                    db.list_rpop(&src, now)\n                };\n                if let Some(v) = val {\n                    if push_left {\n                        db.list_lpush(&dst, std::slice::from_ref(&v), now);\n                    } else {\n                        db.list_rpush(&dst, std::slice::from_ref(&v), now);\n                    }\n                    if let Some(ttl) = saved_ttl {\n                        db.ttl.insert(dst.clone(), ttl);\n                    }\n                    state.notify.notify_waiters();\n                    return Frame::Bulk(v.into());\n                }\n            }\n\n            let timeout_dur = if timeout_s == 0.0 {\n                std::time::Duration::from_secs(300)\n            } else {\n                std::time::Duration::from_secs_f64(timeout_s)\n            };\n            let deadline = tokio::time::Instant::now() + timeout_dur;\n\n            loop {\n                tokio::select! {\n                    _ = state.notify.notified() => {\n                        let mut inner = state.lock();\n                        let now = inner.effective_now();\n                        let db = inner.db_mut(ctx.selected_db);\n                        db.check_ttl(&src);\n                        // Save TTL when src == dst\n                        let saved_ttl = if src == dst {\n                            db.ttl.get(&src).cloned()\n                        } else {\n                            None\n                        };\n                        let val = if pop_left {\n                            db.list_lpop(&src, now)\n                        } else {\n                            db.list_rpop(&src, now)\n                        };\n                        if let Some(v) = val {\n                            if push_left {\n                                db.list_lpush(&dst, std::slice::from_ref(&v), now);\n                            } else {\n                                db.list_rpush(&dst, std::slice::from_ref(&v), now);\n                            }\n                            if let Some(ttl) = saved_ttl {\n                                db.ttl.insert(dst.clone(), ttl);\n                            }\n                            state.notify.notify_waiters();\n                            return Frame::Bulk(v.into());\n                        }\n                    }\n                    _ = tokio::time::sleep_until(deadline) => {\n                        return Frame::NullArray;\n                    }\n                    _ = shutdown_rx.recv() => {\n                        return Frame::NullArray;\n                    }\n                }\n            }\n        }\n        _ => Frame::error(\"ERR unsupported blocking command\"),\n    }\n}\n\n/// Check if BLOCK argument is present in the command args.\nfn has_block_arg(args: &[Vec<u8>]) -> bool {\n    args.iter()\n        .any(|a| String::from_utf8_lossy(a).to_uppercase() == \"BLOCK\")\n}\n\n/// Handle blocking XREAD/XREADGROUP commands.\nasync fn handle_blocking_stream_command(\n    cmd: &str,\n    args: &[Vec<u8>],\n    state: &Arc<SharedState>,\n    ctx: &mut ConnCtx,\n    shutdown_rx: &mut broadcast::Receiver<()>,\n) -> Frame {\n    use crate::dispatch::MSG_WRONG_TYPE;\n    use crate::types::{KeyType, Stream};\n\n    match cmd {\n        \"XREAD\" => {\n            if args.len() < 3 {\n                return Frame::error(crate::dispatch::err_wrong_number(\"xread\"));\n            }\n\n            let mut i = 0;\n            let mut count: Option<usize> = None;\n            let mut block_ms: Option<i64> = None;\n\n            while i < args.len() {\n                let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n                match opt.as_str() {\n                    \"COUNT\" => {\n                        i += 1;\n                        if i >= args.len() {\n                            return Frame::error(\"ERR syntax error\");\n                        }\n                        match String::from_utf8_lossy(&args[i]).parse::<usize>() {\n                            Ok(n) => count = Some(n),\n                            Err(_) => {\n                                return Frame::error(\"ERR value is not an integer or out of range\");\n                            }\n                        }\n                        i += 1;\n                    }\n                    \"BLOCK\" => {\n                        i += 1;\n                        if i >= args.len() {\n                            return Frame::error(\"ERR syntax error\");\n                        }\n                        match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                            Ok(n) if n < 0 => {\n                                return Frame::error(\"ERR timeout is negative\");\n                            }\n                            Ok(n) => block_ms = Some(n),\n                            Err(_) => {\n                                return Frame::error(\n                                    \"ERR timeout is not an integer or out of range\",\n                                );\n                            }\n                        }\n                        i += 1;\n                    }\n                    \"STREAMS\" => {\n                        i += 1;\n                        break;\n                    }\n                    _ => {\n                        return Frame::error(\"ERR syntax error\");\n                    }\n                }\n            }\n\n            let remaining = &args[i..];\n            if remaining.is_empty() || !remaining.len().is_multiple_of(2) {\n                return Frame::error(\n                    \"ERR Unbalanced 'xread' list of streams: for each stream key an ID or '$' must be specified.\",\n                );\n            }\n\n            let half = remaining.len() / 2;\n            let keys: Vec<String> = remaining[..half]\n                .iter()\n                .map(|a| String::from_utf8_lossy(a).to_string())\n                .collect();\n\n            // Resolve $ IDs to current last IDs\n            let mut ids = Vec::with_capacity(half);\n            {\n                let inner = state.lock();\n                let db = inner.db(ctx.selected_db);\n                for (idx, a) in remaining[half..].iter().enumerate() {\n                    let s = String::from_utf8_lossy(a).to_string();\n                    if s == \"$\" {\n                        ids.push(\n                            db.stream_keys\n                                .get(&keys[idx])\n                                .map(|stream| stream.last_id().to_string())\n                                .unwrap_or_else(|| \"0-0\".to_string()),\n                        );\n                    } else {\n                        let normalized = Stream::normalize_id(&s);\n                        if Stream::parse_id(&normalized).is_err() {\n                            return Frame::error(\n                                \"ERR Invalid stream ID specified as stream command argument\",\n                            );\n                        }\n                        ids.push(normalized);\n                    }\n                }\n            }\n\n            // Helper closure to try reading from streams\n            let try_read = |state: &SharedState,\n                            keys: &[String],\n                            ids: &[String],\n                            count: Option<usize>|\n             -> Option<Frame> {\n                let inner = state.lock();\n                let db = inner.db(ctx.selected_db);\n                let mut results = Vec::new();\n\n                for (idx, key) in keys.iter().enumerate() {\n                    if let Some(kt) = db.keys.get(key)\n                        && *kt != KeyType::Stream\n                    {\n                        return Some(Frame::error(MSG_WRONG_TYPE));\n                    }\n\n                    let entries = match db.stream_keys.get(key) {\n                        Some(stream) => {\n                            let mut entries = stream.after(&ids[idx]);\n                            if let Some(c) = count {\n                                entries.truncate(c);\n                            }\n                            entries\n                        }\n                        None => vec![],\n                    };\n\n                    if entries.is_empty() {\n                        continue;\n                    }\n\n                    let entry_frames: Vec<Frame> = entries\n                        .into_iter()\n                        .map(|e| {\n                            let vals: Vec<Frame> = e\n                                .values\n                                .iter()\n                                .map(|v| Frame::Bulk(v.clone().into()))\n                                .collect();\n                            Frame::Array(vec![Frame::Bulk(e.id.clone().into()), Frame::Array(vals)])\n                        })\n                        .collect();\n\n                    results.push(Frame::Array(vec![\n                        Frame::Bulk(key.clone().into()),\n                        Frame::Array(entry_frames),\n                    ]));\n                }\n\n                if results.is_empty() {\n                    None // No data yet\n                } else {\n                    Some(Frame::Array(results))\n                }\n            };\n\n            // Try immediate read\n            if let Some(result) = try_read(state, &keys, &ids, count) {\n                return result;\n            }\n\n            // Block until data or timeout\n            let timeout_ms = block_ms.unwrap_or(0);\n            let timeout_dur = if timeout_ms == 0 {\n                std::time::Duration::from_secs(300) // max wait\n            } else {\n                std::time::Duration::from_millis(timeout_ms as u64)\n            };\n            let deadline = tokio::time::Instant::now() + timeout_dur;\n\n            loop {\n                tokio::select! {\n                    _ = state.notify.notified() => {\n                        if let Some(result) = try_read(state, &keys, &ids, count) {\n                            return result;\n                        }\n                    }\n                    _ = tokio::time::sleep_until(deadline) => {\n                        return Frame::NullArray;\n                    }\n                    _ = shutdown_rx.recv() => {\n                        return Frame::NullArray;\n                    }\n                }\n            }\n        }\n        \"XREADGROUP\" => {\n            if args.len() < 6 {\n                return Frame::error(crate::dispatch::err_wrong_number(\"xreadgroup\"));\n            }\n\n            let mut i = 0;\n            let group_kw = String::from_utf8_lossy(&args[i]).to_uppercase();\n            if group_kw != \"GROUP\" {\n                return Frame::error(\"ERR syntax error\");\n            }\n            i += 1;\n\n            let group_name = String::from_utf8_lossy(&args[i]).to_string();\n            i += 1;\n            let consumer_name = String::from_utf8_lossy(&args[i]).to_string();\n            i += 1;\n\n            let mut count: Option<usize> = None;\n            let mut block_ms: Option<i64> = None;\n            let mut noack = false;\n\n            while i < args.len() {\n                let opt = String::from_utf8_lossy(&args[i]).to_uppercase();\n                match opt.as_str() {\n                    \"COUNT\" => {\n                        i += 1;\n                        if i >= args.len() {\n                            return Frame::error(\"ERR syntax error\");\n                        }\n                        match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                            Ok(n) if n > 0 => count = Some(n as usize),\n                            Ok(_) => count = None,\n                            Err(_) => {\n                                return Frame::error(\"ERR value is not an integer or out of range\");\n                            }\n                        }\n                        i += 1;\n                    }\n                    \"BLOCK\" => {\n                        i += 1;\n                        if i >= args.len() {\n                            return Frame::error(\"ERR syntax error\");\n                        }\n                        match String::from_utf8_lossy(&args[i]).parse::<i64>() {\n                            Ok(n) if n < 0 => {\n                                return Frame::error(\"ERR timeout is negative\");\n                            }\n                            Ok(n) => block_ms = Some(n),\n                            Err(_) => {\n                                return Frame::error(\n                                    \"ERR timeout is not an integer or out of range\",\n                                );\n                            }\n                        }\n                        i += 1;\n                    }\n                    \"NOACK\" => {\n                        noack = true;\n                        i += 1;\n                    }\n                    \"STREAMS\" => {\n                        i += 1;\n                        break;\n                    }\n                    _ => {\n                        return Frame::error(\"ERR syntax error\");\n                    }\n                }\n            }\n\n            let remaining = &args[i..];\n            if remaining.is_empty() || !remaining.len().is_multiple_of(2) {\n                return Frame::error(\n                    \"ERR Unbalanced XREADGROUP list of streams: for each stream key an ID or '$' must be specified.\",\n                );\n            }\n\n            let half = remaining.len() / 2;\n            let keys: Vec<String> = remaining[..half]\n                .iter()\n                .map(|a| String::from_utf8_lossy(a).to_string())\n                .collect();\n            let ids: Vec<String> = remaining[half..]\n                .iter()\n                .map(|a| String::from_utf8_lossy(a).to_string())\n                .collect();\n\n            // If any ID is not \">\", BLOCK is ignored — run synchronously\n            let all_gt = ids.iter().all(|id| id == \">\");\n            if !all_gt {\n                // Fall through to non-blocking execution via dispatch\n                // Run non-blocking via dispatch\n                let mut full_cmd = Vec::with_capacity(args.len() + 1);\n                full_cmd.push(b\"XREADGROUP\".to_vec());\n                full_cmd.extend(args.iter().cloned());\n                let (response, _) = crate::dispatch::dispatch(\n                    state.command_table.get().unwrap(),\n                    state,\n                    ctx,\n                    &full_cmd,\n                );\n                return response;\n            }\n\n            // Validate group existence for all streams before blocking\n            {\n                let inner = state.lock();\n                let db = inner.db(ctx.selected_db);\n                for key in &keys {\n                    if let Some(kt) = db.keys.get(key)\n                        && *kt != KeyType::Stream\n                    {\n                        return Frame::error(MSG_WRONG_TYPE);\n                    }\n                    match db.stream_keys.get(key) {\n                        Some(stream) => {\n                            if !stream.groups.contains_key(&group_name) {\n                                return Frame::error(format!(\n                                    \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                                    group_name, key\n                                ));\n                            }\n                        }\n                        None => {\n                            return Frame::error(format!(\n                                \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                                group_name, key\n                            ));\n                        }\n                    }\n                }\n            }\n\n            // Helper closure to try reading from groups\n            let try_read_group = |state: &SharedState,\n                                  ctx: &ConnCtx,\n                                  keys: &[String],\n                                  group_name: &str,\n                                  consumer_name: &str,\n                                  count: Option<usize>,\n                                  noack: bool|\n             -> Result<Option<Frame>, Frame> {\n                let mut inner = state.lock();\n                let now = inner.effective_now();\n                let db = inner.db_mut(ctx.selected_db);\n                let mut results = Vec::new();\n\n                for key in keys {\n                    let stream = match db.stream_keys.get_mut(key) {\n                        Some(s) => s,\n                        None => {\n                            return Err(Frame::error(format!(\n                                \"NOGROUP No such consumer group '{}' for key name '{}'\",\n                                group_name, key\n                            )));\n                        }\n                    };\n\n                    let entries = match stream.read_group(\n                        group_name,\n                        consumer_name,\n                        \">\",\n                        count,\n                        noack,\n                        now,\n                    ) {\n                        Ok(entries) => entries,\n                        Err(e) => return Err(Frame::error(e)),\n                    };\n\n                    if entries.is_empty() {\n                        continue;\n                    }\n\n                    let entry_frames: Vec<Frame> = entries\n                        .into_iter()\n                        .map(|e| {\n                            let vals: Vec<Frame> = e\n                                .values\n                                .iter()\n                                .map(|v| Frame::Bulk(v.clone().into()))\n                                .collect();\n                            Frame::Array(vec![Frame::Bulk(e.id.into()), Frame::Array(vals)])\n                        })\n                        .collect();\n\n                    results.push(Frame::Array(vec![\n                        Frame::Bulk(key.clone().into()),\n                        Frame::Array(entry_frames),\n                    ]));\n                }\n\n                if results.is_empty() {\n                    Ok(None)\n                } else {\n                    Ok(Some(Frame::Array(results)))\n                }\n            };\n\n            // Try immediate read\n            match try_read_group(state, ctx, &keys, &group_name, &consumer_name, count, noack) {\n                Err(e) => return e,\n                Ok(Some(result)) => return result,\n                Ok(None) => {} // No data, proceed to blocking\n            }\n\n            // Block until data or timeout\n            let timeout_ms = block_ms.unwrap_or(0);\n            let timeout_dur = if timeout_ms == 0 {\n                std::time::Duration::from_secs(300) // max wait\n            } else {\n                std::time::Duration::from_millis(timeout_ms as u64)\n            };\n            let deadline = tokio::time::Instant::now() + timeout_dur;\n\n            loop {\n                tokio::select! {\n                    _ = state.notify.notified() => {\n                        match try_read_group(state, ctx, &keys, &group_name, &consumer_name, count, noack) {\n                            Err(e) => return e,\n                            Ok(Some(result)) => return result,\n                            Ok(None) => {} // Continue waiting\n                        }\n                    }\n                    _ = tokio::time::sleep_until(deadline) => {\n                        return Frame::NullArray;\n                    }\n                    _ = shutdown_rx.recv() => {\n                        return Frame::NullArray;\n                    }\n                }\n            }\n        }\n        _ => Frame::error(\"ERR unsupported blocking stream command\"),\n    }\n}\n\n/// Convert a Frame (expected to be an Array of Bulk strings) into a\n/// vector of byte vectors (the command arguments).\nfn frame_to_args(frame: Frame) -> Option<Vec<Vec<u8>>> {\n    match frame {\n        Frame::Array(frames) => {\n            let mut args = Vec::with_capacity(frames.len());\n            for f in frames {\n                match f {\n                    Frame::Bulk(data) => args.push(data.to_vec()),\n                    Frame::Simple(s) => args.push(s.into_bytes()),\n                    Frame::Integer(n) => args.push(n.to_string().into_bytes()),\n                    _ => return None,\n                }\n            }\n            Some(args)\n        }\n        // Some clients send inline commands (single bulk string)\n        Frame::Bulk(data) => {\n            let s = String::from_utf8_lossy(&data);\n            let args: Vec<Vec<u8>> = s\n                .split_whitespace()\n                .map(|w| w.as_bytes().to_vec())\n                .collect();\n            if args.is_empty() { None } else { Some(args) }\n        }\n        _ => None,\n    }\n}\n"
  },
  {
    "path": "miniredis/src/types.rs",
    "content": "use std::collections::HashMap;\n\n/// The type tag for a Redis key.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum KeyType {\n    String,\n    Hash,\n    List,\n    Set,\n    SortedSet,\n    Stream,\n    HyperLogLog,\n}\n\nimpl KeyType {\n    /// Return the Redis TYPE command string for this key type.\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            KeyType::String => \"string\",\n            KeyType::Hash => \"hash\",\n            KeyType::List => \"list\",\n            KeyType::Set => \"set\",\n            KeyType::SortedSet => \"zset\",\n            KeyType::Stream => \"stream\",\n            KeyType::HyperLogLog => \"hll\", // not \"string\" — miniredis uses a distinct type\n        }\n    }\n}\n\n/// A sorted set element with score and member.\n#[derive(Clone, Debug)]\npub struct SSElem {\n    pub score: f64,\n    pub member: String,\n}\n\n/// Ascending or descending direction for sorted set operations.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum Direction {\n    Asc,\n    Desc,\n}\n\n/// Redis sorted set — uses a HashMap for O(1) score lookups, sorts on demand for range queries.\n#[derive(Clone, Debug, Default)]\npub struct SortedSet {\n    pub scores: HashMap<String, f64>,\n}\n\nimpl SortedSet {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn card(&self) -> usize {\n        self.scores.len()\n    }\n\n    /// Add or update a member. Returns true if the member was new.\n    pub fn set(&mut self, score: f64, member: &str) -> bool {\n        let is_new = !self.scores.contains_key(member);\n        self.scores.insert(member.to_owned(), score);\n        is_new\n    }\n\n    /// Get a member's score.\n    pub fn get(&self, member: &str) -> Option<f64> {\n        self.scores.get(member).copied()\n    }\n\n    /// Check if a member exists.\n    pub fn exists(&self, member: &str) -> bool {\n        self.scores.contains_key(member)\n    }\n\n    /// Remove a member. Returns true if it existed.\n    pub fn remove(&mut self, member: &str) -> bool {\n        self.scores.remove(member).is_some()\n    }\n\n    /// Return all elements sorted by (score, member).\n    pub fn by_score(&self, dir: Direction) -> Vec<SSElem> {\n        let mut elems: Vec<SSElem> = self\n            .scores\n            .iter()\n            .map(|(m, &s)| SSElem {\n                score: s,\n                member: m.clone(),\n            })\n            .collect();\n        elems.sort_by(|a, b| {\n            a.score\n                .partial_cmp(&b.score)\n                .unwrap_or(std::cmp::Ordering::Equal)\n                .then_with(|| a.member.cmp(&b.member))\n        });\n        if dir == Direction::Desc {\n            elems.reverse();\n        }\n        elems\n    }\n\n    /// Return sorted member names (all same score → lex order).\n    pub fn members_sorted(&self) -> Vec<String> {\n        self.by_score(Direction::Asc)\n            .into_iter()\n            .map(|e| e.member)\n            .collect()\n    }\n\n    /// Get the rank (0-based index) of a member in sorted order.\n    pub fn rank(&self, member: &str, dir: Direction) -> Option<usize> {\n        if !self.scores.contains_key(member) {\n            return None;\n        }\n        let elems = self.by_score(dir);\n        elems.iter().position(|e| e.member == member)\n    }\n\n    /// Increment a member's score by delta. Creates the member if it doesn't exist.\n    /// Returns the new score.\n    pub fn incrby(&mut self, member: &str, delta: f64) -> f64 {\n        let score = self.scores.entry(member.to_owned()).or_insert(0.0);\n        *score += delta;\n        *score\n    }\n}\n\n/// A single stream entry.\n#[derive(Clone, Debug)]\npub struct StreamEntry {\n    pub id: String,\n    /// Alternating key-value pairs.\n    pub values: Vec<String>,\n}\n\n/// A pending entry in a consumer group's PEL.\n#[derive(Clone, Debug)]\npub struct PendingEntry {\n    pub id: String,\n    pub consumer: String,\n    pub delivery_count: i64,\n    pub last_delivery: std::time::SystemTime,\n}\n\n/// A consumer within a group.\n#[derive(Clone, Debug)]\npub struct StreamConsumer {\n    pub num_pending: i64,\n    pub last_seen: std::time::SystemTime,\n    pub last_success: std::time::SystemTime,\n}\n\n/// A consumer group on a stream.\n#[derive(Clone, Debug)]\npub struct StreamGroup {\n    pub last_id: String,\n    pub pending: Vec<PendingEntry>,\n    pub consumers: HashMap<String, StreamConsumer>,\n    /// Whether entries-read is known (set to false initially, true once entries are delivered).\n    pub entries_read_known: bool,\n}\n\n/// Redis stream.\n#[derive(Clone, Debug, Default)]\npub struct Stream {\n    pub entries: Vec<StreamEntry>,\n    pub groups: HashMap<String, StreamGroup>,\n    pub last_allocated_id: String,\n}\n\nimpl Stream {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Parse a stream ID string \"ms-seq\" into (ms, seq).\n    pub fn parse_id(id: &str) -> Result<(u64, u64), &'static str> {\n        let parts: Vec<&str> = id.splitn(2, '-').collect();\n        let ms = parts[0]\n            .parse::<u64>()\n            .map_err(|_| \"ERR Invalid stream ID specified as stream command argument\")?;\n        let seq = if parts.len() > 1 {\n            parts[1]\n                .parse::<u64>()\n                .map_err(|_| \"ERR Invalid stream ID specified as stream command argument\")?\n        } else {\n            0\n        };\n        Ok((ms, seq))\n    }\n\n    /// Compare two stream IDs. Returns Ordering.\n    pub fn cmp_ids(a: &str, b: &str) -> std::cmp::Ordering {\n        let a_parsed = Self::parse_id(a).unwrap_or((0, 0));\n        let b_parsed = Self::parse_id(b).unwrap_or((0, 0));\n        a_parsed.cmp(&b_parsed)\n    }\n\n    /// Format a stream ID from parts.\n    pub fn format_id(ms: u64, seq: u64) -> String {\n        format!(\"{}-{}\", ms, seq)\n    }\n\n    /// Normalize a partial ID to full \"ms-seq\" format.\n    pub fn normalize_id(id: &str) -> String {\n        if id.contains('-') {\n            id.to_string()\n        } else {\n            format!(\"{}-0\", id)\n        }\n    }\n\n    /// Get the last entry's ID, or \"0-0\" if empty.\n    pub fn last_id(&self) -> &str {\n        self.entries.last().map(|e| e.id.as_str()).unwrap_or(\"0-0\")\n    }\n\n    /// Generate a new ID based on timestamp.\n    pub fn generate_id(&mut self, ms: u64) -> String {\n        let mut new_ms = ms;\n        let mut new_seq = 0u64;\n\n        // Check against lastAllocatedID\n        if !self.last_allocated_id.is_empty()\n            && let Ok((alloc_ms, alloc_seq)) = Self::parse_id(&self.last_allocated_id)\n            && new_ms <= alloc_ms\n        {\n            new_ms = alloc_ms;\n            new_seq = alloc_seq + 1;\n        }\n\n        // Check against last entry\n        if let Some(last) = self.entries.last()\n            && let Ok((last_ms, last_seq)) = Self::parse_id(&last.id)\n            && (new_ms < last_ms || (new_ms == last_ms && new_seq <= last_seq))\n        {\n            new_ms = last_ms;\n            new_seq = last_seq + 1;\n        }\n\n        let id = Self::format_id(new_ms, new_seq);\n        self.last_allocated_id = id.clone();\n        id\n    }\n\n    /// Generate ID with a specific timestamp and auto-sequence.\n    pub fn generate_id_seq(&mut self, ms: u64) -> String {\n        self.generate_id(ms)\n    }\n\n    /// Add an entry. Returns the assigned ID or error.\n    pub fn add(\n        &mut self,\n        id: &str,\n        values: Vec<String>,\n        now_ms: u64,\n    ) -> Result<String, &'static str> {\n        let final_id = if id.is_empty() || id == \"*\" {\n            self.generate_id(now_ms)\n        } else if let Some(ms_str) = id.strip_suffix(\"-*\") {\n            let ms = ms_str\n                .parse::<u64>()\n                .map_err(|_| \"ERR Invalid stream ID specified as stream command argument\")?;\n            self.generate_id_seq(ms)\n        } else {\n            let normalized = Self::normalize_id(id);\n            // Validate the ID\n            let (ms, seq) = Self::parse_id(&normalized)?;\n            if ms == 0 && seq == 0 {\n                return Err(\"ERR The ID specified in XADD must be greater than 0-0\");\n            }\n            // Must be greater than the last entry\n            if let Some(last) = self.entries.last()\n                && Self::cmp_ids(&normalized, &last.id) != std::cmp::Ordering::Greater\n            {\n                return Err(\n                    \"ERR The ID specified in XADD is equal or smaller than the target stream top item\",\n                );\n            }\n            self.last_allocated_id = normalized.clone();\n            normalized\n        };\n\n        self.entries.push(StreamEntry {\n            id: final_id.clone(),\n            values,\n        });\n\n        Ok(final_id)\n    }\n\n    /// Trim to at most n entries (MAXLEN).\n    pub fn trim_maxlen(&mut self, n: usize) -> i64 {\n        if self.entries.len() <= n {\n            return 0;\n        }\n        let remove = self.entries.len() - n;\n        self.entries.drain(..remove);\n        remove as i64\n    }\n\n    /// Remove all entries with ID < threshold (MINID).\n    pub fn trim_minid(&mut self, threshold: &str) -> i64 {\n        let before = self.entries.len();\n        self.entries\n            .retain(|e| Self::cmp_ids(&e.id, threshold) != std::cmp::Ordering::Less);\n        (before - self.entries.len()) as i64\n    }\n\n    /// Get entries after the given ID.\n    pub fn after(&self, id: &str) -> Vec<&StreamEntry> {\n        self.entries\n            .iter()\n            .filter(|e| Self::cmp_ids(&e.id, id) == std::cmp::Ordering::Greater)\n            .collect()\n    }\n\n    /// Get entries in range [start, end].\n    pub fn range(&self, start: &str, end: &str, count: Option<usize>) -> Vec<&StreamEntry> {\n        let mut result: Vec<&StreamEntry> = self\n            .entries\n            .iter()\n            .filter(|e| {\n                Self::cmp_ids(&e.id, start) != std::cmp::Ordering::Less\n                    && Self::cmp_ids(&e.id, end) != std::cmp::Ordering::Greater\n            })\n            .collect();\n        if let Some(c) = count {\n            result.truncate(c);\n        }\n        result\n    }\n\n    /// Get entries in reverse range [end, start] (for XREVRANGE).\n    pub fn rev_range(&self, start: &str, end: &str, count: Option<usize>) -> Vec<&StreamEntry> {\n        let mut result: Vec<&StreamEntry> = self\n            .entries\n            .iter()\n            .filter(|e| {\n                Self::cmp_ids(&e.id, start) != std::cmp::Ordering::Less\n                    && Self::cmp_ids(&e.id, end) != std::cmp::Ordering::Greater\n            })\n            .rev()\n            .collect();\n        if let Some(c) = count {\n            result.truncate(c);\n        }\n        result\n    }\n\n    /// Delete entries by ID. Returns count deleted.\n    pub fn del(&mut self, ids: &[&str]) -> i64 {\n        let mut count = 0i64;\n        for id in ids {\n            let before = self.entries.len();\n            self.entries.retain(|e| e.id != **id);\n            if self.entries.len() < before {\n                count += 1;\n            }\n        }\n        count\n    }\n\n    /// Get an entry by ID.\n    pub fn get(&self, id: &str) -> Option<&StreamEntry> {\n        self.entries.iter().find(|e| e.id == id)\n    }\n\n    /// Check if an entry exists.\n    pub fn entry_exists(&self, id: &str) -> bool {\n        self.entries.iter().any(|e| e.id == id)\n    }\n\n    /// Create a consumer group. Returns error if already exists.\n    pub fn create_group(&mut self, name: &str, id: &str) -> Result<(), String> {\n        if self.groups.contains_key(name) {\n            return Err(\"BUSYGROUP Consumer Group name already exists\".to_string());\n        }\n        let last_id = if id == \"$\" {\n            self.last_id().to_string()\n        } else {\n            Self::normalize_id(id)\n        };\n        // entries_read_known is true only when the group starts at \"0-0\"\n        // (meaning we know it has read 0 entries). For \"$\" or specific IDs,\n        // we don't know the true count.\n        let entries_read_known = last_id == \"0-0\";\n        self.groups.insert(\n            name.to_string(),\n            StreamGroup {\n                last_id,\n                pending: Vec::new(),\n                consumers: HashMap::new(),\n                entries_read_known,\n            },\n        );\n        Ok(())\n    }\n\n    /// Read from a consumer group. Returns entries and updates PEL.\n    pub fn read_group(\n        &mut self,\n        group_name: &str,\n        consumer_name: &str,\n        id: &str,\n        count: Option<usize>,\n        noack: bool,\n        now: std::time::SystemTime,\n    ) -> Result<Vec<StreamEntry>, String> {\n        let group = self.groups.get_mut(group_name).ok_or_else(|| {\n            format!(\n                \"NOGROUP No such consumer group '{}' for key name\",\n                group_name\n            )\n        })?;\n\n        // Ensure consumer exists\n        group\n            .consumers\n            .entry(consumer_name.to_string())\n            .or_insert(StreamConsumer {\n                num_pending: 0,\n                last_seen: now,\n                last_success: now,\n            });\n\n        if id == \">\" {\n            // New undelivered messages\n            let entries: Vec<StreamEntry> = self\n                .entries\n                .iter()\n                .filter(|e| Self::cmp_ids(&e.id, &group.last_id) == std::cmp::Ordering::Greater)\n                .cloned()\n                .collect();\n\n            let entries = if let Some(c) = count {\n                entries.into_iter().take(c).collect::<Vec<_>>()\n            } else {\n                entries\n            };\n\n            if let Some(last) = entries.last() {\n                group.last_id = last.id.clone();\n                group.entries_read_known = true;\n            }\n\n            if !noack {\n                for entry in &entries {\n                    // Check if already in PEL\n                    if let Some(pe) = group.pending.iter_mut().find(|pe| pe.id == entry.id) {\n                        // Already in PEL - update consumer ownership\n                        let old_consumer = pe.consumer.clone();\n                        if old_consumer != consumer_name {\n                            pe.consumer = consumer_name.to_string();\n                            if let Some(c) = group.consumers.get_mut(&old_consumer) {\n                                c.num_pending -= 1;\n                            }\n                            if let Some(c) = group.consumers.get_mut(consumer_name) {\n                                c.num_pending += 1;\n                            }\n                        }\n                        pe.delivery_count += 1;\n                        pe.last_delivery = now;\n                    } else {\n                        group.pending.push(PendingEntry {\n                            id: entry.id.clone(),\n                            consumer: consumer_name.to_string(),\n                            delivery_count: 1,\n                            last_delivery: now,\n                        });\n                        if let Some(c) = group.consumers.get_mut(consumer_name) {\n                            c.num_pending += 1;\n                            c.last_success = now;\n                        }\n                    }\n                }\n            }\n\n            Ok(entries)\n        } else {\n            // Re-deliver from PEL\n            let normalized = Self::normalize_id(id);\n            let mut result = Vec::new();\n            for pe in &mut group.pending {\n                if pe.consumer != consumer_name {\n                    continue;\n                }\n                if Self::cmp_ids(&pe.id, &normalized) == std::cmp::Ordering::Less {\n                    continue;\n                }\n                // Find the entry in the stream\n                if let Some(entry) = self.entries.iter().find(|e| e.id == pe.id) {\n                    pe.delivery_count += 1;\n                    pe.last_delivery = now;\n                    result.push(entry.clone());\n                }\n                if let Some(c) = count\n                    && result.len() >= c\n                {\n                    break;\n                }\n            }\n            Ok(result)\n        }\n    }\n\n    /// Acknowledge entries. Returns count acknowledged.\n    /// If the group doesn't exist, returns 0 (not an error).\n    pub fn ack(&mut self, group_name: &str, ids: &[&str]) -> Result<i64, String> {\n        let group = match self.groups.get_mut(group_name) {\n            Some(g) => g,\n            None => return Ok(0),\n        };\n\n        let mut count = 0i64;\n        for id in ids {\n            let before = group.pending.len();\n            let consumer_name = group\n                .pending\n                .iter()\n                .find(|pe| pe.id == **id)\n                .map(|pe| pe.consumer.clone());\n            group.pending.retain(|pe| pe.id != **id);\n            if group.pending.len() < before {\n                count += 1;\n                if let Some(cname) = consumer_name\n                    && let Some(c) = group.consumers.get_mut(&cname)\n                {\n                    c.num_pending -= 1;\n                }\n            }\n        }\n        Ok(count)\n    }\n}\n\n/// Format a range bound for XRANGE/XREVRANGE.\n/// Returns Ok(formatted_id) or Err(error_message) if the id is invalid.\npub fn format_stream_range_bound(id: &str, is_start: bool) -> Result<String, &'static str> {\n    match id {\n        \"-\" => Ok(\"0-0\".to_string()),\n        \"+\" => Ok(format!(\"{}-{}\", u64::MAX, u64::MAX)),\n        _ => {\n            // Handle exclusive prefix '('\n            if let Some(rest) = id.strip_prefix('(') {\n                if rest == \"-\" || rest == \"+\" {\n                    return Err(\"ERR Invalid stream ID specified as stream command argument\");\n                }\n                // Normalize using the same rules as non-exclusive bounds first:\n                // - For start: \"X\" → \"X-0\"\n                // - For end: \"X\" → \"X-MAX\"\n                // Then apply the exclusive adjustment.\n                let normalized = if rest.contains('-') {\n                    rest.to_string()\n                } else if is_start {\n                    format!(\"{}-0\", rest)\n                } else {\n                    format!(\"{}-{}\", rest, u64::MAX)\n                };\n                // Validate the ID\n                let (ms, seq) = Stream::parse_id(&normalized)\n                    .map_err(|_| \"ERR Invalid stream ID specified as stream command argument\")?;\n                if is_start {\n                    // Exclusive start: we want entries strictly after this ID\n                    // Increment seq (or ms if seq overflows)\n                    if seq == u64::MAX {\n                        Ok(Stream::format_id(ms + 1, 0))\n                    } else {\n                        Ok(Stream::format_id(ms, seq + 1))\n                    }\n                } else {\n                    // Exclusive end: we want entries strictly before this ID\n                    // Decrement seq (or ms if seq is 0)\n                    if seq == 0 {\n                        if ms == 0 {\n                            // Nothing before 0-0\n                            Ok(\"0-0\".to_string())\n                        } else {\n                            Ok(Stream::format_id(ms - 1, u64::MAX))\n                        }\n                    } else {\n                        Ok(Stream::format_id(ms, seq - 1))\n                    }\n                }\n            } else {\n                // Validate the ID\n                let base = if id.contains('-') {\n                    id.to_string()\n                } else if is_start {\n                    format!(\"{}-0\", id)\n                } else {\n                    format!(\"{}-{}\", id, u64::MAX)\n                };\n                // Validate\n                let parts: Vec<&str> = base.splitn(2, '-').collect();\n                parts[0]\n                    .parse::<u64>()\n                    .map_err(|_| \"ERR Invalid stream ID specified as stream command argument\")?;\n                if parts.len() > 1 {\n                    parts[1].parse::<u64>().map_err(\n                        |_| \"ERR Invalid stream ID specified as stream command argument\",\n                    )?;\n                }\n                Ok(base)\n            }\n        }\n    }\n}\n\n// HyperLogLog is implemented in src/hll.rs.\n"
  },
  {
    "path": "miniredis/tests/cmd_auth.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── AUTH ──────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_auth_no_password_configured() {\n    let (_m, mut c) = start().await;\n\n    // AUTH without any password configured → specific error\n    must_fail!(c, \"AUTH\", \"foo\"; \"AUTH <password> called without any password configured\");\n}\n\n#[tokio::test]\nasync fn test_auth_wrong_args() {\n    let (_m, mut c) = start().await;\n\n    // No args\n    must_fail!(c, \"AUTH\"; \"wrong number of arguments\");\n\n    // Too many args\n    must_fail!(c, \"AUTH\", \"a\", \"b\", \"c\"; \"syntax error\");\n}\n\n#[tokio::test]\nasync fn test_auth_default_user() {\n    let (m, mut c) = start().await;\n\n    m.require_auth(\"secret\");\n\n    // Without auth, commands should fail\n    must_fail!(c, \"PING\"; \"NOAUTH\");\n\n    // Wrong password\n    must_fail!(c, \"AUTH\", \"wrongpass\"; \"WRONGPASS\");\n\n    // Correct password\n    must_ok!(c, \"AUTH\", \"secret\");\n\n    // Now commands work\n    let v: String = redis::cmd(\"PING\").query_async(&mut c).await.unwrap();\n    assert_eq!(v, \"PONG\");\n}\n\n#[tokio::test]\nasync fn test_auth_user_password() {\n    let (m, mut c) = start().await;\n\n    m.require_user_auth(\"hello\", \"world\");\n\n    // Without auth, commands should fail\n    must_fail!(c, \"PING\"; \"NOAUTH\");\n\n    // Wrong password\n    must_fail!(c, \"AUTH\", \"hello\", \"wrongpass\"; \"WRONGPASS\");\n\n    // Wrong username\n    must_fail!(c, \"AUTH\", \"goodbye\", \"world\"; \"WRONGPASS\");\n\n    // Correct user + password\n    must_ok!(c, \"AUTH\", \"hello\", \"world\");\n\n    // Now commands work\n    let v: String = redis::cmd(\"PING\").query_async(&mut c).await.unwrap();\n    assert_eq!(v, \"PONG\");\n}\n\n// ── HELLO ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_hello_basic() {\n    let (_m, mut c) = start().await;\n\n    // HELLO 2 should return server info\n    let v: redis::Value = redis::cmd(\"HELLO\")\n        .arg(\"2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Should be an array with key-value pairs\n    match v {\n        redis::Value::Array(ref items) => {\n            assert!(items.len() >= 12); // at least 6 key-value pairs\n        }\n        _ => panic!(\"expected array from HELLO, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_hello_errors() {\n    let (_m, mut c) = start().await;\n\n    // No args\n    must_fail!(c, \"HELLO\"; \"wrong number of arguments\");\n\n    // Non-integer version\n    must_fail!(c, \"HELLO\", \"foo\"; \"Protocol version is not an integer\");\n\n    // Unsupported version\n    must_fail!(c, \"HELLO\", \"1\"; \"NOPROTO\");\n    must_fail!(c, \"HELLO\", \"4\"; \"NOPROTO\");\n}\n\n#[tokio::test]\nasync fn test_hello_auth() {\n    let (m, mut c) = start().await;\n\n    m.require_auth(\"secret\");\n\n    // HELLO with AUTH should authenticate\n    let v: redis::Value = redis::cmd(\"HELLO\")\n        .arg(\"2\")\n        .arg(\"AUTH\")\n        .arg(\"default\")\n        .arg(\"secret\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(_) => {} // success\n        _ => panic!(\"expected array from HELLO, got {:?}\", v),\n    }\n\n    // Should be authenticated now\n    let v: String = redis::cmd(\"PING\").query_async(&mut c).await.unwrap();\n    assert_eq!(v, \"PONG\");\n}\n\n#[tokio::test]\nasync fn test_hello_auth_wrong_password() {\n    let (m, mut c) = start().await;\n\n    m.require_auth(\"secret\");\n\n    // HELLO with wrong AUTH should fail\n    must_fail!(c, \"HELLO\", \"2\", \"AUTH\", \"default\", \"wrong\"; \"WRONGPASS\");\n}\n\n#[tokio::test]\nasync fn test_hello_syntax_errors() {\n    let (_m, mut c) = start().await;\n\n    // AUTH with missing args\n    must_fail!(c, \"HELLO\", \"2\", \"AUTH\", \"foo\"; \"Syntax error in HELLO option\");\n\n    // SETNAME with missing arg\n    must_fail!(c, \"HELLO\", \"2\", \"AUTH\", \"foo\", \"bar\", \"SETNAME\"; \"Syntax error in HELLO option\");\n\n    // Unknown option\n    must_fail!(c, \"HELLO\", \"2\", \"BOGUS\"; \"Syntax error in HELLO option\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_bit.rs",
    "content": "// Ported from ../miniredis/cmd_string_test.go (bit operations)\nmod helpers;\n\n#[tokio::test]\nasync fn test_getbit() {\n    let (_m, mut c) = helpers::start().await;\n\n    // \\x08 = 0b00001000\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"findme\")\n        .arg(b\"\\x08\" as &[u8])\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    must_int!(c, \"GETBIT\", \"findme\", \"0\"; 0);\n    must_int!(c, \"GETBIT\", \"findme\", \"4\"; 1);\n    must_int!(c, \"GETBIT\", \"findme\", \"5\"; 0);\n\n    // Non-existing key\n    must_int!(c, \"GETBIT\", \"nosuch\", \"1\"; 0);\n    must_int!(c, \"GETBIT\", \"nosuch\", \"1000\"; 0);\n\n    // Errors\n    must_fail!(c, \"GETBIT\", \"foo\"; \"wrong number of arguments\");\n    must_fail!(c, \"GETBIT\", \"foo\", \"noint\"; \"not an integer\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    // Not wrong type for getbit — strings are valid\n}\n\n#[tokio::test]\nasync fn test_setbit() {\n    let (_m, mut c) = helpers::start().await;\n\n    // \\x08 = 0b00001000\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"findme\")\n        .arg(b\"\\x08\" as &[u8])\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Clear bit 4 (was 1)\n    must_int!(c, \"SETBIT\", \"findme\", \"4\", \"0\"; 1);\n\n    // Set bit 4 (was 0)\n    must_int!(c, \"SETBIT\", \"findme\", \"4\", \"1\"; 0);\n\n    // Non-existing key — creates it\n    must_int!(c, \"SETBIT\", \"nosuch\", \"0\", \"1\"; 0);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![0x80]);\n\n    // Extends short values\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"short\")\n        .arg(b\"\\x00\\x00\" as &[u8])\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    must_int!(c, \"SETBIT\", \"short\", \"32\", \"1\"; 0);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"short\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v.len(), 5);\n    assert_eq!(v[4], 0x80);\n\n    // Errors\n    must_fail!(c, \"SETBIT\", \"foo\"; \"wrong number of arguments\");\n    must_fail!(c, \"SETBIT\", \"foo\", \"noint\", \"1\"; \"not an integer\");\n    must_fail!(c, \"SETBIT\", \"foo\", \"1\", \"noint\"; \"not an integer\");\n    must_fail!(c, \"SETBIT\", \"foo\", \"-3\", \"0\"; \"not an integer\");\n    must_fail!(c, \"SETBIT\", \"foo\", \"3\", \"2\"; \"out of range\");\n}\n\n#[tokio::test]\nasync fn test_bitcount() {\n    let (_m, mut c) = helpers::start().await;\n\n    // 'a' = 0x61 = 0b01100001 → 3 bits\n    must_ok!(c, \"SET\", \"countme\", \"a\");\n    must_int!(c, \"BITCOUNT\", \"countme\"; 3);\n\n    // 'aaaaa' → 15 bits\n    must_ok!(c, \"SET\", \"countme\", \"aaaaa\");\n    must_int!(c, \"BITCOUNT\", \"countme\"; 15);\n\n    // Non-existing\n    must_int!(c, \"BITCOUNT\", \"nosuch\"; 0);\n\n    // With range: 'abcd'\n    // a=0x61(3) b=0x62(3) c=0x63(4) d=0x64(3)\n    must_ok!(c, \"SET\", \"foo\", \"abcd\");\n    must_int!(c, \"BITCOUNT\", \"foo\", \"0\", \"0\"; 3);\n    must_int!(c, \"BITCOUNT\", \"foo\", \"0\", \"3\"; 13);\n    must_int!(c, \"BITCOUNT\", \"foo\", \"2\", \"-2\"; 4); // only 'c'\n\n    // Errors\n    must_fail!(c, \"BITCOUNT\"; \"wrong number of arguments\");\n    must_fail!(c, \"BITCOUNT\", \"foo\", \"noint\", \"12\"; \"not an integer\");\n    must_fail!(c, \"BITCOUNT\", \"foo\", \"12\", \"noint\"; \"not an integer\");\n}\n\n#[tokio::test]\nasync fn test_bitop() {\n    let (_m, mut c) = helpers::start().await;\n\n    // AND\n    must_ok!(c, \"SET\", \"a\", \"a\"); // 0x61\n    must_ok!(c, \"SET\", \"b\", \"b\"); // 0x62\n    must_int!(c, \"BITOP\", \"AND\", \"bitand\", \"a\", \"b\"; 1);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"bitand\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![0x60]); // '`'\n\n    // AND with different lengths\n    must_ok!(c, \"SET\", \"a2\", \"aa\");\n    must_ok!(c, \"SET\", \"b2\", \"bbbb\");\n    must_int!(c, \"BITOP\", \"AND\", \"bitand2\", \"a2\", \"b2\"; 4);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"bitand2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![0x60, 0x60, 0x00, 0x00]);\n\n    // OR\n    must_int!(c, \"BITOP\", \"OR\", \"bitor\", \"a2\", \"b2\"; 4);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"bitor\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![0x63, 0x63, 0x62, 0x62]); // \"ccbb\"\n\n    // XOR\n    must_int!(c, \"BITOP\", \"XOR\", \"bitxor\", \"a2\", \"b2\"; 4);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"bitxor\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![0x03, 0x03, 0x62, 0x62]);\n\n    // NOT\n    must_int!(c, \"BITOP\", \"NOT\", \"bitnot\", \"a\"; 1);\n    let v: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"bitnot\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![0x9e]);\n\n    // Single arg copy\n    must_int!(c, \"BITOP\", \"AND\", \"copy\", \"a\"; 1);\n    let v: String = redis::cmd(\"GET\")\n        .arg(\"copy\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"a\");\n\n    // Errors\n    must_fail!(c, \"BITOP\"; \"wrong number of arguments\");\n    must_fail!(c, \"BITOP\", \"AND\"; \"wrong number of arguments\");\n    must_fail!(c, \"BITOP\", \"WHAT\", \"dest\", \"key\"; \"syntax error\");\n    must_fail!(c, \"BITOP\", \"NOT\", \"foo\", \"bar\", \"baz\"; \"BITOP NOT\");\n}\n\n#[tokio::test]\nasync fn test_bitpos() {\n    let (_m, mut c) = helpers::start().await;\n\n    // \\xff\\xf0\\x00 = all 1s, 4 more 1s, all 0s\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"findme\")\n        .arg(b\"\\xff\\xf0\\x00\" as &[u8])\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    must_int!(c, \"BITPOS\", \"findme\", \"0\"; 12);\n    must_int!(c, \"BITPOS\", \"findme\", \"1\"; 0);\n    must_int!(c, \"BITPOS\", \"findme\", \"1\", \"1\"; 8);\n    must_int!(c, \"BITPOS\", \"findme\", \"0\", \"1\"; 12);\n\n    // Only zeros\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"zero\")\n        .arg(b\"\\x00\\x00\" as &[u8])\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    must_int!(c, \"BITPOS\", \"zero\", \"1\"; -1);\n    must_int!(c, \"BITPOS\", \"zero\", \"0\"; 0);\n\n    // Only ones\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"one\")\n        .arg(b\"\\xff\\xff\" as &[u8])\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    must_int!(c, \"BITPOS\", \"one\", \"1\"; 0);\n    must_int!(c, \"BITPOS\", \"one\", \"1\", \"1\"; 8);\n    must_int!(c, \"BITPOS\", \"one\", \"0\"; 16); // special: past the end\n\n    // Non-existing\n    must_int!(c, \"BITPOS\", \"nosuch\", \"1\"; -1);\n    must_int!(c, \"BITPOS\", \"nosuch\", \"0\"; 0);\n\n    // Errors\n    must_fail!(c, \"BITPOS\"; \"wrong number of arguments\");\n    must_fail!(c, \"BITPOS\", \"foo\"; \"wrong number of arguments\");\n    must_fail!(c, \"BITPOS\", \"foo\", \"noint\"; \"not an integer\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_client.rs",
    "content": "mod helpers;\n\n#[tokio::test]\nasync fn test_client_setname_getname() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Set the client name\n    must_ok!(c, \"CLIENT\", \"SETNAME\", \"miniredis-tests\");\n\n    // Get the client name\n    must_str!(c, \"CLIENT\", \"GETNAME\"; \"miniredis-tests\");\n}\n\n#[tokio::test]\nasync fn test_client_getname_without_setname() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Get the client name without setting it first → nil\n    must_nil!(c, \"CLIENT\", \"GETNAME\");\n}\n\n#[tokio::test]\nasync fn test_client_setname_empty() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Set then clear the client name\n    must_ok!(c, \"CLIENT\", \"SETNAME\", \"test\");\n    must_str!(c, \"CLIENT\", \"GETNAME\"; \"test\");\n\n    must_ok!(c, \"CLIENT\", \"SETNAME\", \"\");\n    must_nil!(c, \"CLIENT\", \"GETNAME\");\n}\n\n#[tokio::test]\nasync fn test_client_errors() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_fail!(c, \"CLIENT\"; \"wrong number of arguments\");\n    must_fail!(c, \"CLIENT\", \"NOSUCHSUB\"; \"unknown subcommand\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_cluster.rs",
    "content": "mod helpers;\n\n#[tokio::test]\nasync fn test_cluster_slots() {\n    let (_m, mut c) = helpers::start().await;\n\n    let v: redis::Value = redis::cmd(\"CLUSTER\")\n        .arg(\"SLOTS\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Should return a nested array with slot range 0-16383\n    match v {\n        redis::Value::Array(slots) => {\n            assert_eq!(slots.len(), 1);\n            match &slots[0] {\n                redis::Value::Array(range) => {\n                    assert!(range.len() >= 3);\n                    // start slot = 0\n                    assert_eq!(range[0], redis::Value::Int(0));\n                    // end slot = 16383\n                    assert_eq!(range[1], redis::Value::Int(16383));\n                }\n                _ => panic!(\"expected array for slot range, got {:?}\", slots[0]),\n            }\n        }\n        _ => panic!(\"expected array from CLUSTER SLOTS, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_cluster_nodes() {\n    let (_m, mut c) = helpers::start().await;\n\n    let v: String = redis::cmd(\"CLUSTER\")\n        .arg(\"NODES\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert!(v.contains(\"myself,master\"));\n    assert!(v.contains(\"connected 0-16383\"));\n}\n\n#[tokio::test]\nasync fn test_cluster_keyslot() {\n    let (_m, mut c) = helpers::start().await;\n\n    let v: i64 = redis::cmd(\"CLUSTER\")\n        .arg(\"KEYSLOT\")\n        .arg(\"{test_key}\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert_eq!(v, 163);\n}\n\n#[tokio::test]\nasync fn test_cluster_shards() {\n    let (_m, mut c) = helpers::start().await;\n\n    let v: redis::Value = redis::cmd(\"CLUSTER\")\n        .arg(\"SHARDS\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Should return a nested array with shard info\n    match v {\n        redis::Value::Array(shards) => {\n            assert_eq!(shards.len(), 1);\n        }\n        _ => panic!(\"expected array from CLUSTER SHARDS, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_cluster_errors() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_fail!(c, \"CLUSTER\"; \"wrong number of arguments\");\n    must_fail!(c, \"CLUSTER\", \"NOSUCHSUB\"; \"unknown subcommand\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_connection.rs",
    "content": "// Ported from ../miniredis/cmd_connection_test.go\nmod helpers;\n\n#[tokio::test]\nasync fn test_ping() {\n    let (_m, mut c) = helpers::start().await;\n\n    // No args → PONG\n    must_str!(c, \"PING\"; \"PONG\");\n\n    // With arg → echo\n    must_str!(c, \"PING\", \"hi\"; \"hi\");\n\n    // Too many args → error\n    must_fail!(c, \"PING\", \"foo\", \"bar\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_echo() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_str!(c, \"ECHO\", \"hello\\nworld\"; \"hello\\nworld\");\n\n    // Wrong number of args\n    must_fail!(c, \"ECHO\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_select() {\n    let (m, mut c) = helpers::start().await;\n\n    // Set in db 0\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    // Switch to db 5, set different value\n    must_ok!(c, \"SELECT\", \"5\");\n    must_ok!(c, \"SET\", \"foo\", \"baz\");\n\n    // Direct API: db 0 should have \"bar\"\n    assert_eq!(m.get(\"foo\"), Some(\"bar\".to_owned()));\n\n    // SELECT out of range\n    must_fail!(c, \"SELECT\", \"16\"; \"DB index is out of range\");\n    must_fail!(c, \"SELECT\", \"-1\"; \"DB index is out of range\");\n    must_fail!(c, \"SELECT\", \"notanumber\"; \"not an integer\");\n\n    // Wrong number of args\n    must_fail!(c, \"SELECT\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_dbsize() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"DBSIZE\"; 0);\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n    must_int!(c, \"DBSIZE\"; 2);\n}\n\n#[tokio::test]\nasync fn test_flushdb() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n    must_int!(c, \"DBSIZE\"; 2);\n    must_ok!(c, \"FLUSHDB\");\n    must_int!(c, \"DBSIZE\"; 0);\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_generic.rs",
    "content": "// Ported from ../miniredis/cmd_generic_test.go\nmod helpers;\nuse std::time::Duration;\n\n#[tokio::test]\nasync fn test_ttl_expire() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    // No TTL by default\n    must_int!(c, \"TTL\", \"foo\"; -1);\n    must_int!(c, \"PTTL\", \"foo\"; -1);\n    assert!(m.ttl(\"foo\").is_none());\n\n    // Set TTL with EXPIRE\n    must_int!(c, \"EXPIRE\", \"foo\", \"100\"; 1);\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n    assert!(ttl.unwrap() <= Duration::from_secs(100));\n\n    // TTL command\n    let ttl_val: i64 = redis::cmd(\"TTL\")\n        .arg(\"foo\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(ttl_val > 0 && ttl_val <= 100);\n\n    // PTTL command\n    let pttl_val: i64 = redis::cmd(\"PTTL\")\n        .arg(\"foo\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(pttl_val > 0 && pttl_val <= 100_000);\n\n    // PERSIST removes TTL\n    must_int!(c, \"PERSIST\", \"foo\"; 1);\n    must_int!(c, \"TTL\", \"foo\"; -1);\n    assert!(m.ttl(\"foo\").is_none());\n\n    // PERSIST on key without TTL\n    must_int!(c, \"PERSIST\", \"foo\"; 0);\n\n    // EXPIRE on non-existing key\n    must_int!(c, \"EXPIRE\", \"nosuch\", \"100\"; 0);\n\n    // PERSIST on non-existing key\n    must_int!(c, \"PERSIST\", \"nosuch\"; 0);\n\n    // TTL/PTTL on non-existing key\n    must_int!(c, \"TTL\", \"nosuch\"; -2);\n    must_int!(c, \"PTTL\", \"nosuch\"; -2);\n\n    // Errors\n    must_fail!(c, \"EXPIRE\"; \"wrong number of arguments\");\n    must_fail!(c, \"EXPIRE\", \"foo\"; \"wrong number of arguments\");\n    must_fail!(c, \"TTL\"; \"wrong number of arguments\");\n    must_fail!(c, \"PTTL\"; \"wrong number of arguments\");\n    must_fail!(c, \"PERSIST\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_expireat() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    // EXPIREAT with future timestamp\n    let future_ts = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap()\n        .as_secs()\n        + 100;\n    must_int!(c, \"EXPIREAT\", \"foo\", &future_ts.to_string(); 1);\n\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n\n    // Non-existing key\n    must_int!(c, \"EXPIREAT\", \"nosuch\", &future_ts.to_string(); 0);\n}\n\n#[tokio::test]\nasync fn test_pexpire() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    must_int!(c, \"PEXPIRE\", \"foo\", \"50000\"; 1);\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n    assert!(ttl.unwrap() <= Duration::from_secs(50));\n\n    // Non-existing key\n    must_int!(c, \"PEXPIRE\", \"nosuch\", \"50000\"; 0);\n}\n\n#[tokio::test]\nasync fn test_pexpireat() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    let future_ts_ms = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .unwrap()\n        .as_millis()\n        + 100_000;\n    must_int!(c, \"PEXPIREAT\", \"foo\", &future_ts_ms.to_string(); 1);\n\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n}\n\n#[tokio::test]\nasync fn test_type() {\n    let (_m, mut c) = helpers::start().await;\n\n    // String\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    let t: String = redis::cmd(\"TYPE\")\n        .arg(\"str\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(t, \"string\");\n\n    // Hash\n    must_int!(c, \"HSET\", \"h\", \"f\", \"v\"; 1);\n    let t: String = redis::cmd(\"TYPE\")\n        .arg(\"h\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(t, \"hash\");\n\n    // List\n    must_int!(c, \"RPUSH\", \"l\", \"a\"; 1);\n    let t: String = redis::cmd(\"TYPE\")\n        .arg(\"l\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(t, \"list\");\n\n    // Set\n    must_int!(c, \"SADD\", \"s\", \"a\"; 1);\n    let t: String = redis::cmd(\"TYPE\")\n        .arg(\"s\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(t, \"set\");\n\n    // Non-existing\n    let t: String = redis::cmd(\"TYPE\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(t, \"none\");\n\n    // Errors\n    must_fail!(c, \"TYPE\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_rename() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"val\");\n\n    must_ok!(c, \"RENAME\", \"a\", \"b\");\n    must_nil!(c, \"GET\", \"a\");\n    must_str!(c, \"GET\", \"b\"; \"val\");\n\n    // Overwrite existing\n    must_ok!(c, \"SET\", \"c\", \"other\");\n    must_ok!(c, \"RENAME\", \"b\", \"c\");\n    must_str!(c, \"GET\", \"c\"; \"val\");\n\n    // Non-existing source\n    must_fail!(c, \"RENAME\", \"nosuch\", \"dst\"; \"no such key\");\n\n    // Same key\n    must_ok!(c, \"SET\", \"x\", \"v\");\n    must_ok!(c, \"RENAME\", \"x\", \"x\");\n    must_str!(c, \"GET\", \"x\"; \"v\");\n\n    // Errors\n    must_fail!(c, \"RENAME\"; \"wrong number of arguments\");\n    must_fail!(c, \"RENAME\", \"a\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_renamenx() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"val\");\n\n    must_int!(c, \"RENAMENX\", \"a\", \"b\"; 1);\n    must_nil!(c, \"GET\", \"a\");\n    must_str!(c, \"GET\", \"b\"; \"val\");\n\n    // Target exists\n    must_ok!(c, \"SET\", \"c\", \"other\");\n    must_int!(c, \"RENAMENX\", \"b\", \"c\"; 0);\n    must_str!(c, \"GET\", \"b\"; \"val\");\n    must_str!(c, \"GET\", \"c\"; \"other\");\n\n    // Non-existing source\n    must_fail!(c, \"RENAMENX\", \"nosuch\", \"dst\"; \"no such key\");\n\n    // Errors\n    must_fail!(c, \"RENAMENX\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_keys() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"alpha\", \"1\");\n    must_ok!(c, \"SET\", \"beta\", \"2\");\n    must_ok!(c, \"SET\", \"gamma\", \"3\");\n    must_ok!(c, \"SET\", \"abc\", \"4\");\n\n    // Match all\n    let mut result: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(\"*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    result.sort();\n    assert_eq!(result, vec![\"abc\", \"alpha\", \"beta\", \"gamma\"]);\n\n    // Pattern match\n    let mut result: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(\"a*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    result.sort();\n    assert_eq!(result, vec![\"abc\", \"alpha\"]);\n\n    // Single character wildcard\n    let result: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(\"ab?\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"abc\"]);\n\n    // No match\n    let result: Vec<String> = redis::cmd(\"KEYS\")\n        .arg(\"nosuch*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n\n    // Errors\n    must_fail!(c, \"KEYS\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_scan() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"key1\", \"1\");\n    must_ok!(c, \"SET\", \"key2\", \"2\");\n    must_ok!(c, \"SET\", \"other\", \"3\");\n\n    // Basic scan\n    let (cursor, mut keys): (String, Vec<String>) = redis::cmd(\"SCAN\")\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    keys.sort();\n    assert_eq!(keys, vec![\"key1\", \"key2\", \"other\"]);\n\n    // MATCH pattern\n    let (cursor, mut keys): (String, Vec<String>) = redis::cmd(\"SCAN\")\n        .arg(\"0\")\n        .arg(\"MATCH\")\n        .arg(\"key*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    keys.sort();\n    assert_eq!(keys, vec![\"key1\", \"key2\"]);\n\n    // TYPE filter\n    must_int!(c, \"SADD\", \"myset\", \"a\"; 1);\n    let (cursor, keys): (String, Vec<String>) = redis::cmd(\"SCAN\")\n        .arg(\"0\")\n        .arg(\"TYPE\")\n        .arg(\"set\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    assert_eq!(keys, vec![\"myset\"]);\n}\n\n#[tokio::test]\nasync fn test_touch() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n\n    must_int!(c, \"TOUCH\", \"a\"; 1);\n    must_int!(c, \"TOUCH\", \"a\", \"b\"; 2);\n    must_int!(c, \"TOUCH\", \"a\", \"b\", \"nosuch\"; 2);\n    must_int!(c, \"TOUCH\", \"nosuch\"; 0);\n\n    // Errors\n    must_fail!(c, \"TOUCH\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_unlink() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n\n    must_int!(c, \"UNLINK\", \"a\", \"b\", \"nosuch\"; 2);\n    must_nil!(c, \"GET\", \"a\");\n    must_nil!(c, \"GET\", \"b\");\n}\n\n#[tokio::test]\nasync fn test_randomkey() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Empty database\n    must_nil!(c, \"RANDOMKEY\");\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n\n    // Should return one of the keys\n    let key: String = redis::cmd(\"RANDOMKEY\").query_async(&mut c).await.unwrap();\n    assert!(key == \"a\" || key == \"b\");\n}\n\n#[tokio::test]\nasync fn test_wait() {\n    let (_m, mut c) = helpers::start().await;\n\n    // WAIT always returns 0 (standalone mode)\n    must_int!(c, \"WAIT\", \"0\", \"0\"; 0);\n    must_int!(c, \"WAIT\", \"1\", \"100\"; 0);\n\n    // Errors\n    must_fail!(c, \"WAIT\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_object() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    let enc: String = redis::cmd(\"OBJECT\")\n        .arg(\"ENCODING\")\n        .arg(\"foo\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(enc, \"raw\");\n\n    must_int!(c, \"OBJECT\", \"IDLETIME\", \"foo\"; 0);\n    must_int!(c, \"OBJECT\", \"REFCOUNT\", \"foo\"; 1);\n    must_int!(c, \"OBJECT\", \"FREQ\", \"foo\"; 0);\n\n    // Errors\n    must_fail!(c, \"OBJECT\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_expire_flags() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    // NX: only if no TTL\n    must_int!(c, \"EXPIRE\", \"foo\", \"100\", \"NX\"; 1);\n    assert!(m.ttl(\"foo\").is_some());\n\n    // NX again should fail (TTL already set)\n    must_int!(c, \"EXPIRE\", \"foo\", \"200\", \"NX\"; 0);\n\n    // XX: only if TTL exists\n    must_int!(c, \"EXPIRE\", \"foo\", \"200\", \"XX\"; 1);\n\n    // XX on key without TTL\n    must_ok!(c, \"SET\", \"bar\", \"baz\");\n    must_int!(c, \"EXPIRE\", \"bar\", \"100\", \"XX\"; 0);\n\n    // GT: only if new TTL > old\n    must_ok!(c, \"SET\", \"gt\", \"val\");\n    must_int!(c, \"EXPIRE\", \"gt\", \"100\"; 1);\n    must_int!(c, \"EXPIRE\", \"gt\", \"200\", \"GT\"; 1); // 200 > 100\n    must_int!(c, \"EXPIRE\", \"gt\", \"50\", \"GT\"; 0); // 50 < 200\n\n    // LT: only if new TTL < old\n    must_ok!(c, \"SET\", \"lt\", \"val\");\n    must_int!(c, \"EXPIRE\", \"lt\", \"200\"; 1);\n    must_int!(c, \"EXPIRE\", \"lt\", \"100\", \"LT\"; 1); // 100 < 200\n    must_int!(c, \"EXPIRE\", \"lt\", \"300\", \"LT\"; 0); // 300 > 100\n}\n\n#[tokio::test]\nasync fn test_copy() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Basic\n    must_ok!(c, \"SET\", \"key1\", \"value\");\n    must_int!(c, \"COPY\", \"key1\", \"key2\"; 1);\n    must_str!(c, \"GET\", \"key2\"; \"value\");\n\n    // Nonexistent source\n    must_int!(c, \"COPY\", \"nosuch\", \"to\"; 0);\n\n    // Existing destination (no overwrite by default)\n    must_ok!(c, \"SET\", \"existingkey\", \"value\");\n    must_ok!(c, \"SET\", \"newkey\", \"newvalue\");\n    must_int!(c, \"COPY\", \"newkey\", \"existingkey\"; 0);\n    must_str!(c, \"GET\", \"existingkey\"; \"value\");\n\n    // REPLACE\n    must_ok!(c, \"SET\", \"rkey1\", \"value\");\n    must_ok!(c, \"SET\", \"rkey2\", \"another\");\n    must_int!(c, \"COPY\", \"rkey1\", \"rkey2\", \"REPLACE\"; 1);\n    must_str!(c, \"GET\", \"rkey2\"; \"value\");\n\n    // List copy (deep copy)\n    must_int!(c, \"LPUSH\", \"l1\", \"original\"; 1);\n    must_int!(c, \"COPY\", \"l1\", \"l2\"; 1);\n    must_int!(c, \"LPUSH\", \"l1\", \"new\"; 2);\n    must_int!(c, \"LLEN\", \"l2\"; 1);\n\n    // Errors\n    must_fail!(c, \"COPY\"; \"wrong number of arguments\");\n    must_fail!(c, \"COPY\", \"foo\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_move_cmd() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Basic\n    must_ok!(c, \"SET\", \"foo\", \"bar!\");\n    must_int!(c, \"MOVE\", \"foo\", \"1\"; 1);\n    must_nil!(c, \"GET\", \"foo\"); // Gone from DB 0\n\n    // Source doesn't exist\n    must_int!(c, \"MOVE\", \"nosuch\", \"1\"; 0);\n\n    // Errors\n    must_fail!(c, \"MOVE\"; \"wrong number of arguments\");\n    must_fail!(c, \"MOVE\", \"foo\"; \"wrong number of arguments\");\n    must_fail!(c, \"MOVE\", \"foo\", \"noint\"; \"DB index is out of range\");\n}\n\n#[tokio::test]\nasync fn test_expiretime() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Nonexistent key\n    must_int!(c, \"EXPIRETIME\", \"nosuch\"; -2);\n\n    // No expire\n    must_ok!(c, \"SET\", \"noexpire\", \"\");\n    must_int!(c, \"EXPIRETIME\", \"noexpire\"; -1);\n\n    // With expire\n    must_ok!(c, \"SET\", \"foo\", \"\");\n    must_int!(c, \"EXPIREAT\", \"foo\", \"10413792000\"; 1);\n    must_int!(c, \"EXPIRETIME\", \"foo\"; 10413792000);\n}\n\n#[tokio::test]\nasync fn test_pexpiretime() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Nonexistent key\n    must_int!(c, \"PEXPIRETIME\", \"nosuch\"; -2);\n\n    // No expire\n    must_ok!(c, \"SET\", \"noexpire\", \"\");\n    must_int!(c, \"PEXPIRETIME\", \"noexpire\"; -1);\n\n    // With expire\n    must_ok!(c, \"SET\", \"foo\", \"\");\n    must_int!(c, \"PEXPIREAT\", \"foo\", \"10413792000123\"; 1);\n    must_int!(c, \"PEXPIRETIME\", \"foo\"; 10413792000123);\n}\n\n#[tokio::test]\nasync fn test_dump() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Missing key\n    must_nil!(c, \"DUMP\", \"missing-key\");\n\n    // Existing key (stub returns raw string)\n    must_ok!(c, \"SET\", \"existing-key\", \"value\");\n    must_str!(c, \"DUMP\", \"existing-key\"; \"value\");\n\n    // Non-string type returns nil (stub behavior)\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"set-key\")\n        .arg(\"a\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    must_nil!(c, \"DUMP\", \"set-key\");\n\n    // Errors\n    must_fail!(c, \"DUMP\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_restore() {\n    let (_m, mut c) = helpers::start().await;\n\n    // New key no TTL\n    must_ok!(c, \"RESTORE\", \"key-a\", \"0\", \"value-a\");\n    must_str!(c, \"GET\", \"key-a\"; \"value-a\");\n\n    // Busy key\n    must_ok!(c, \"SET\", \"existing\", \"value\");\n    must_fail!(c, \"RESTORE\", \"existing\", \"0\", \"other\"; \"BUSYKEY\");\n\n    // Overwrite with REPLACE\n    must_ok!(c, \"RESTORE\", \"existing\", \"0\", \"new-value\", \"REPLACE\");\n    must_str!(c, \"GET\", \"existing\"; \"new-value\");\n\n    // Errors\n    must_fail!(c, \"RESTORE\"; \"wrong number of arguments\");\n    must_fail!(c, \"RESTORE\", \"key\"; \"wrong number of arguments\");\n    must_fail!(c, \"RESTORE\", \"key\", \"argh\", \"val\"; \"not an integer\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_geo.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── GEOADD ───────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_geoadd() {\n    let (_m, mut c) = start().await;\n\n    // Add two locations\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // Re-add same member → 0 (updated, not new)\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 0);\n}\n\n#[tokio::test]\nasync fn test_geoadd_errors() {\n    let (_m, mut c) = start().await;\n\n    // Wrong number of args\n    must_fail!(c, \"GEOADD\"; \"wrong number of arguments\");\n    must_fail!(c, \"GEOADD\", \"key\"; \"wrong number of arguments\");\n    must_fail!(c, \"GEOADD\", \"key\", \"1\", \"2\"; \"wrong number of arguments\");\n\n    // Invalid longitude (out of range)\n    must_fail!(c, \"GEOADD\", \"broken\", \"-190.0\", \"10.0\", \"hi\"; \"invalid longitude,latitude pair\");\n    must_fail!(c, \"GEOADD\", \"broken\", \"190.0\", \"10.0\", \"hi\"; \"invalid longitude,latitude pair\");\n\n    // Invalid latitude (out of range)\n    must_fail!(c, \"GEOADD\", \"broken\", \"10.0\", \"-86.0\", \"hi\"; \"invalid longitude,latitude pair\");\n    must_fail!(c, \"GEOADD\", \"broken\", \"10.0\", \"86.0\", \"hi\"; \"invalid longitude,latitude pair\");\n\n    // Not a float\n    must_fail!(c, \"GEOADD\", \"broken\", \"notafloat\", \"10.0\", \"hi\"; \"not a valid float\");\n    must_fail!(c, \"GEOADD\", \"broken\", \"10.0\", \"notafloat\", \"hi\"; \"not a valid float\");\n}\n\n// ── GEOPOS ───────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_geopos() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n\n    // Get position — returns array of [lng, lat]\n    let v: redis::Value = redis::cmd(\"GEOPOS\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 1);\n            // First element should be an array of [lng, lat]\n            match &items[0] {\n                redis::Value::Array(coords) => {\n                    assert_eq!(coords.len(), 2);\n                }\n                _ => panic!(\"expected array for coords, got {:?}\", items[0]),\n            }\n        }\n        _ => panic!(\"expected array from GEOPOS, got {:?}\", v),\n    }\n\n    // Non-existent member → nil\n    let v: redis::Value = redis::cmd(\"GEOPOS\")\n        .arg(\"Sicily\")\n        .arg(\"Corleone\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 1);\n            assert_eq!(items[0], redis::Value::Nil);\n        }\n        _ => panic!(\"expected array from GEOPOS, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_geopos_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"GEOPOS\"; \"wrong number of arguments\");\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    must_fail!(c, \"GEOPOS\", \"foo\"; \"WRONGTYPE\");\n}\n\n// ── GEODIST ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_geodist() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // Default unit = meters\n    let d: String = redis::cmd(\"GEODIST\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"Catania\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(d, \"166274.1514\");\n\n    // In km\n    let d: String = redis::cmd(\"GEODIST\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"Catania\")\n        .arg(\"km\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(d, \"166.2742\");\n}\n\n#[tokio::test]\nasync fn test_geodist_nil() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n\n    // Non-existent key\n    must_nil!(c, \"GEODIST\", \"nosuch\", \"a\", \"b\");\n\n    // Non-existent member\n    must_nil!(c, \"GEODIST\", \"Sicily\", \"Palermo\", \"nosuch\");\n    must_nil!(c, \"GEODIST\", \"Sicily\", \"nosuch\", \"Palermo\");\n}\n\n#[tokio::test]\nasync fn test_geodist_errors() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    must_fail!(c, \"GEODIST\"; \"wrong number of arguments\");\n    must_fail!(c, \"GEODIST\", \"Sicily\"; \"wrong number of arguments\");\n    must_fail!(c, \"GEODIST\", \"Sicily\", \"Palermo\"; \"wrong number of arguments\");\n\n    // Unsupported unit\n    must_fail!(c, \"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"miles\"; \"unsupported unit\");\n\n    // Too many args\n    must_fail!(c, \"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"m\", \"extra\"; \"syntax error\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    must_fail!(c, \"GEODIST\", \"foo\", \"Palermo\", \"Catania\"; \"WRONGTYPE\");\n}\n\n// ── GEORADIUS ────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_georadius_basic() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // Basic radius query — returns member names\n    let v: Vec<String> = redis::cmd(\"GEORADIUS\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v.len(), 2);\n    assert!(v.contains(&\"Palermo\".to_string()));\n    assert!(v.contains(&\"Catania\".to_string()));\n\n    // Too small radius — no results\n    let v: Vec<String> = redis::cmd(\"GEORADIUS\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"1\")\n        .arg(\"km\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v.len(), 0);\n}\n\n#[tokio::test]\nasync fn test_georadius_asc_desc() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // ASC\n    let v: Vec<String> = redis::cmd(\"GEORADIUS\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"ASC\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Catania\", \"Palermo\"]);\n\n    // DESC\n    let v: Vec<String> = redis::cmd(\"GEORADIUS\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"DESC\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Palermo\", \"Catania\"]);\n}\n\n#[tokio::test]\nasync fn test_georadius_count() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // COUNT 1 + ASC\n    let v: Vec<String> = redis::cmd(\"GEORADIUS\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"ASC\")\n        .arg(\"COUNT\")\n        .arg(\"1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Catania\"]);\n\n    // COUNT errors\n    must_fail!(c, \"GEORADIUS\", \"Sicily\", \"15\", \"37\", \"200\", \"km\", \"COUNT\"; \"syntax error\");\n    must_fail!(c, \"GEORADIUS\", \"Sicily\", \"15\", \"37\", \"200\", \"km\", \"COUNT\", \"notanumber\"; \"not an integer\");\n    must_fail!(c, \"GEORADIUS\", \"Sicily\", \"15\", \"37\", \"200\", \"km\", \"COUNT\", \"-12\"; \"COUNT must be > 0\");\n}\n\n#[tokio::test]\nasync fn test_georadius_withdist() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // WITHDIST in km\n    let v: redis::Value = redis::cmd(\"GEORADIUS\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"WITHDIST\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 2);\n        }\n        _ => panic!(\"expected array from GEORADIUS WITHDIST, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_georadius_errors() {\n    let (_m, mut c) = start().await;\n\n    // Invalid unit\n    must_fail!(c, \"GEORADIUS\", \"Sicily\", \"15\", \"37\", \"200\", \"mm\"; \"wrong number of arguments\");\n\n    // Invalid float params\n    must_fail!(c, \"GEORADIUS\", \"Sicily\", \"abc\", \"def\", \"ghi\", \"m\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_georadius_ro() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // GEORADIUS_RO works\n    let v: Vec<String> = redis::cmd(\"GEORADIUS_RO\")\n        .arg(\"Sicily\")\n        .arg(\"15\")\n        .arg(\"37\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"ASC\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Catania\", \"Palermo\"]);\n\n    // STORE not allowed in RO mode\n    must_fail!(c, \"GEORADIUS_RO\", \"Sicily\", \"15\", \"37\", \"200\", \"km\", \"STORE\", \"foo\"; \"syntax error\");\n    must_fail!(c, \"GEORADIUS_RO\", \"Sicily\", \"15\", \"37\", \"200\", \"km\", \"STOREDIST\", \"foo\"; \"syntax error\");\n}\n\n// ── GEORADIUSBYMEMBER ────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_georadiusbymember_basic() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // Basic query\n    let v: Vec<String> = redis::cmd(\"GEORADIUSBYMEMBER\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v.len(), 2);\n\n    // ASC\n    let v: Vec<String> = redis::cmd(\"GEORADIUSBYMEMBER\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"ASC\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Palermo\", \"Catania\"]);\n\n    // DESC\n    let v: Vec<String> = redis::cmd(\"GEORADIUSBYMEMBER\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"DESC\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Catania\", \"Palermo\"]);\n}\n\n#[tokio::test]\nasync fn test_georadiusbymember_count() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    let v: Vec<String> = redis::cmd(\"GEORADIUSBYMEMBER\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"ASC\")\n        .arg(\"COUNT\")\n        .arg(\"1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Palermo\"]);\n}\n\n#[tokio::test]\nasync fn test_georadiusbymember_missing() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n\n    // Non-existent key → nil\n    must_nil!(c, \"GEORADIUSBYMEMBER\", \"Capri\", \"Palermo\", \"200\", \"km\");\n\n    // Missing member → error\n    must_fail!(c, \"GEORADIUSBYMEMBER\", \"Sicily\", \"nosuch\", \"200\", \"km\"; \"could not decode requested zset member\");\n\n    // Invalid unit\n    must_fail!(c, \"GEORADIUSBYMEMBER\", \"Sicily\", \"Palermo\", \"200\", \"mm\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_georadiusbymember_ro() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"GEOADD\", \"Sicily\", \"13.361389\", \"38.115556\", \"Palermo\"; 1);\n    must_int!(c, \"GEOADD\", \"Sicily\", \"15.087269\", \"37.502669\", \"Catania\"; 1);\n\n    // GEORADIUSBYMEMBER_RO works\n    let v: Vec<String> = redis::cmd(\"GEORADIUSBYMEMBER_RO\")\n        .arg(\"Sicily\")\n        .arg(\"Palermo\")\n        .arg(\"200\")\n        .arg(\"km\")\n        .arg(\"ASC\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, vec![\"Palermo\", \"Catania\"]);\n\n    // STORE not allowed\n    must_fail!(c, \"GEORADIUSBYMEMBER_RO\", \"Sicily\", \"Palermo\", \"200\", \"km\", \"STORE\", \"foo\"; \"syntax error\");\n    must_fail!(c, \"GEORADIUSBYMEMBER_RO\", \"Sicily\", \"Palermo\", \"200\", \"km\", \"STOREDIST\", \"foo\"; \"syntax error\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_hash.rs",
    "content": "// Ported from ../miniredis/cmd_hash_test.go\nmod helpers;\n\n#[tokio::test]\nasync fn test_hset_hget() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"HSET\", \"h\", \"field1\", \"val1\"; 1);\n    must_str!(c, \"HGET\", \"h\", \"field1\"; \"val1\");\n\n    // Overwrite existing field\n    must_int!(c, \"HSET\", \"h\", \"field1\", \"val2\"; 0);\n    must_str!(c, \"HGET\", \"h\", \"field1\"; \"val2\");\n\n    // Multiple fields at once\n    must_int!(c, \"HSET\", \"h\", \"a\", \"1\", \"b\", \"2\"; 2);\n    must_str!(c, \"HGET\", \"h\", \"a\"; \"1\");\n    must_str!(c, \"HGET\", \"h\", \"b\"; \"2\");\n\n    // Non-existent field\n    must_nil!(c, \"HGET\", \"h\", \"nosuch\");\n    // Non-existent key\n    must_nil!(c, \"HGET\", \"nosuch\", \"field\");\n\n    // Errors\n    must_fail!(c, \"HSET\"; \"wrong number of arguments\");\n    must_fail!(c, \"HSET\", \"h\"; \"wrong number of arguments\");\n    must_fail!(c, \"HSET\", \"h\", \"f\"; \"wrong number of arguments\");\n    must_fail!(c, \"HGET\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_hsetnx() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"HSETNX\", \"h\", \"field\", \"val\"; 1);\n    must_str!(c, \"HGET\", \"h\", \"field\"; \"val\");\n\n    // Already exists\n    must_int!(c, \"HSETNX\", \"h\", \"field\", \"other\"; 0);\n    must_str!(c, \"HGET\", \"h\", \"field\"; \"val\");\n}\n\n#[tokio::test]\nasync fn test_hmset_hmget() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"HMSET\", \"h\", \"a\", \"1\", \"b\", \"2\", \"c\", \"3\");\n    must_strs!(c, \"HMGET\", \"h\", \"a\", \"b\", \"c\"; [\"1\", \"2\", \"3\"]);\n\n    // Missing fields return nil\n    let result: Vec<Option<String>> = redis::cmd(\"HMGET\")\n        .arg(\"h\")\n        .arg(\"a\")\n        .arg(\"nosuch\")\n        .arg(\"c\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(\n        result,\n        vec![Some(\"1\".to_string()), None, Some(\"3\".to_string())]\n    );\n}\n\n#[tokio::test]\nasync fn test_hdel() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"HMSET\", \"h\", \"a\", \"1\", \"b\", \"2\", \"c\", \"3\");\n    must_int!(c, \"HDEL\", \"h\", \"a\", \"b\"; 2);\n    must_nil!(c, \"HGET\", \"h\", \"a\");\n    must_str!(c, \"HGET\", \"h\", \"c\"; \"3\");\n\n    // Non-existent field\n    must_int!(c, \"HDEL\", \"h\", \"nosuch\"; 0);\n\n    // Non-existent key\n    must_int!(c, \"HDEL\", \"nosuch\", \"field\"; 0);\n}\n\n#[tokio::test]\nasync fn test_hexists() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"HSET\", \"h\", \"field\", \"val\"; 1);\n    must_int!(c, \"HEXISTS\", \"h\", \"field\"; 1);\n    must_int!(c, \"HEXISTS\", \"h\", \"nosuch\"; 0);\n    must_int!(c, \"HEXISTS\", \"nosuch\", \"field\"; 0);\n}\n\n#[tokio::test]\nasync fn test_hgetall() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"HMSET\", \"h\", \"a\", \"1\", \"b\", \"2\");\n\n    // HGETALL returns field-value pairs\n    let result: Vec<String> = redis::cmd(\"HGETALL\")\n        .arg(\"h\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    // Should be sorted by field name: a, 1, b, 2\n    assert_eq!(result, vec![\"a\", \"1\", \"b\", \"2\"]);\n\n    // Empty key\n    let result: Vec<String> = redis::cmd(\"HGETALL\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n}\n\n#[tokio::test]\nasync fn test_hkeys_hvals() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"HMSET\", \"h\", \"b\", \"2\", \"a\", \"1\", \"c\", \"3\");\n\n    must_strs!(c, \"HKEYS\", \"h\"; [\"a\", \"b\", \"c\"]);\n    must_strs!(c, \"HVALS\", \"h\"; [\"1\", \"2\", \"3\"]);\n}\n\n#[tokio::test]\nasync fn test_hlen() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"HLEN\", \"h\"; 0);\n    must_ok!(c, \"HMSET\", \"h\", \"a\", \"1\", \"b\", \"2\");\n    must_int!(c, \"HLEN\", \"h\"; 2);\n}\n\n#[tokio::test]\nasync fn test_hincrby() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"HINCRBY\", \"h\", \"field\", \"5\"; 5);\n    must_int!(c, \"HINCRBY\", \"h\", \"field\", \"3\"; 8);\n    must_int!(c, \"HINCRBY\", \"h\", \"field\", \"-2\"; 6);\n\n    // Non-integer value\n    must_ok!(c, \"HMSET\", \"h\", \"str\", \"notanumber\");\n    must_fail!(c, \"HINCRBY\", \"h\", \"str\", \"1\"; \"not an integer\");\n}\n\n#[tokio::test]\nasync fn test_hincrbyfloat() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_str!(c, \"HINCRBYFLOAT\", \"h\", \"field\", \"1.5\"; \"1.5\");\n    must_str!(c, \"HINCRBYFLOAT\", \"h\", \"field\", \"2.5\"; \"4\");\n    must_str!(c, \"HINCRBYFLOAT\", \"h\", \"field\", \"-1\"; \"3\");\n}\n\n#[tokio::test]\nasync fn test_hstrlen() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"HSET\", \"h\", \"field\", \"hello\"; 1);\n    must_int!(c, \"HSTRLEN\", \"h\", \"field\"; 5);\n    must_int!(c, \"HSTRLEN\", \"h\", \"nosuch\"; 0);\n    must_int!(c, \"HSTRLEN\", \"nosuch\", \"field\"; 0);\n}\n\n#[tokio::test]\nasync fn test_hash_wrongtype() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"HGET\", \"str\", \"field\"; \"WRONGTYPE\");\n    must_fail!(c, \"HSET\", \"str\", \"field\", \"val\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_hscan() {\n    let (_m, mut c) = helpers::start().await;\n\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"h\")\n        .arg(\"field1\")\n        .arg(\"value1\")\n        .arg(\"field2\")\n        .arg(\"value2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Basic scan\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"HSCAN\")\n        .arg(\"h\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    // Returns [field1, value1, field2, value2]\n    assert_eq!(vals.len(), 4);\n    assert!(vals.contains(&\"field1\".to_string()));\n    assert!(vals.contains(&\"value1\".to_string()));\n\n    // MATCH\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"h2\")\n        .arg(\"aap\")\n        .arg(\"a\")\n        .arg(\"noot\")\n        .arg(\"b\")\n        .arg(\"mies\")\n        .arg(\"m\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"HSCAN\")\n        .arg(\"h2\")\n        .arg(0)\n        .arg(\"MATCH\")\n        .arg(\"mi*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    assert_eq!(vals, vec![\"mies\", \"m\"]);\n\n    // Errors\n    must_fail!(c, \"HSCAN\"; \"wrong number of arguments\");\n    must_fail!(c, \"HSCAN\", \"h\"; \"wrong number of arguments\");\n    must_fail!(c, \"HSCAN\", \"h\", \"noint\"; \"invalid cursor\");\n    must_fail!(c, \"HSCAN\", \"h\", \"0\", \"MATCH\"; \"syntax error\");\n    must_fail!(c, \"HSCAN\", \"h\", \"0\", \"COUNT\"; \"syntax error\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"HSCAN\", \"str\", \"0\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_hrandfield() {\n    let (_m, mut c) = helpers::start().await;\n\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"h\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .arg(\"f2\")\n        .arg(\"v2\")\n        .arg(\"f3\")\n        .arg(\"v3\")\n        .arg(\"f4\")\n        .arg(\"v4\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Single field (no count)\n    let v: String = redis::cmd(\"HRANDFIELD\")\n        .arg(\"h\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v.starts_with(\"f\"));\n\n    // Positive count\n    let vals: Vec<String> = redis::cmd(\"HRANDFIELD\")\n        .arg(\"h\")\n        .arg(2)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 2);\n\n    // Count larger than hash\n    let vals: Vec<String> = redis::cmd(\"HRANDFIELD\")\n        .arg(\"h\")\n        .arg(10)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 4);\n\n    // Negative count (allows duplicates)\n    let vals: Vec<String> = redis::cmd(\"HRANDFIELD\")\n        .arg(\"h\")\n        .arg(-6)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 6);\n\n    // WITHVALUES\n    let vals: Vec<String> = redis::cmd(\"HRANDFIELD\")\n        .arg(\"h\")\n        .arg(1)\n        .arg(\"WITHVALUES\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 2); // [field, value]\n\n    // Nonexistent key\n    must_nil!(c, \"HRANDFIELD\", \"nosuch\");\n\n    // Errors\n    must_fail!(c, \"HRANDFIELD\"; \"wrong number of arguments\");\n    must_fail!(c, \"HRANDFIELD\", \"h\", \"noint\"; \"not an integer\");\n}\n\n#[tokio::test]\nasync fn test_hexpire() {\n    let (m, mut c) = helpers::start().await;\n\n    // Basic expiration\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"myhash\")\n        .arg(\"field1\")\n        .arg(\"value1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"myhash\")\n        .arg(10)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"field1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1]);\n\n    // Multiple fields\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"myhash2\")\n        .arg(\"field1\")\n        .arg(\"value1\")\n        .arg(\"field2\")\n        .arg(\"value2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"myhash2\")\n        .arg(20)\n        .arg(\"FIELDS\")\n        .arg(2)\n        .arg(\"field1\")\n        .arg(\"field2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1, 1]);\n\n    // Nonexistent field\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"myhash\")\n        .arg(10)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"nonexistent\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![-2]);\n\n    // Nonexistent key\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"nokey\")\n        .arg(10)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"field1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![-2]);\n\n    // NX option\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"nxhash\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"nxhash\")\n        .arg(10)\n        .arg(\"NX\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1]);\n    // NX again → 0 (already has TTL)\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"nxhash\")\n        .arg(20)\n        .arg(\"NX\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![0]);\n\n    // XX option: no TTL → 0\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"xxhash\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"xxhash\")\n        .arg(10)\n        .arg(\"XX\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![0]);\n    // Set TTL first, then XX → 1\n    let _: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"xxhash\")\n        .arg(10)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"xxhash\")\n        .arg(20)\n        .arg(\"XX\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1]);\n\n    // GT option\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"gthash\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"gthash\")\n        .arg(10)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    // GT with smaller → 0\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"gthash\")\n        .arg(5)\n        .arg(\"GT\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![0]);\n    // GT with larger → 1\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"gthash\")\n        .arg(20)\n        .arg(\"GT\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1]);\n\n    // LT option\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"lthash\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"lthash\")\n        .arg(20)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    // LT with larger → 0\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"lthash\")\n        .arg(30)\n        .arg(\"LT\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![0]);\n    // LT with smaller → 1\n    let result: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"lthash\")\n        .arg(10)\n        .arg(\"LT\")\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1]);\n\n    // Field actually expires via fast_forward\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"hash6\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .arg(\"f2\")\n        .arg(\"v2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"hash6\")\n        .arg(1)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    m.fast_forward(std::time::Duration::from_secs(2));\n    must_nil!(c, \"HGET\", \"hash6\", \"f1\");\n    must_str!(c, \"HGET\", \"hash6\", \"f2\"; \"v2\");\n\n    // All fields expired → key deleted\n    let _: i64 = redis::cmd(\"HSET\")\n        .arg(\"hash7\")\n        .arg(\"f1\")\n        .arg(\"v1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: Vec<i64> = redis::cmd(\"HEXPIRE\")\n        .arg(\"hash7\")\n        .arg(1)\n        .arg(\"FIELDS\")\n        .arg(1)\n        .arg(\"f1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    m.fast_forward(std::time::Duration::from_secs(2));\n    must_int!(c, \"EXISTS\", \"hash7\"; 0);\n\n    // Errors\n    must_fail!(c, \"HEXPIRE\", \"myhash\"; \"wrong number of arguments\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"HEXPIRE\", \"str\", \"10\", \"FIELDS\", \"1\", \"f\"; \"WRONGTYPE\");\n    must_fail!(c, \"HEXPIRE\", \"myhash\", \"notanumber\", \"FIELDS\", \"1\", \"f\"; \"not an integer\");\n    must_fail!(c, \"HEXPIRE\", \"myhash\", \"10\", \"FIELDS\", \"0\"; \"wrong number of arguments\");\n    must_fail!(c, \"HEXPIRE\", \"myhash\", \"10\", \"FIELDS\", \"2\", \"f\"; \"numfields\");\n    must_fail!(c, \"HEXPIRE\", \"myhash\", \"10\", \"GT\", \"LT\", \"FIELDS\", \"1\", \"f\"; \"GT and LT\");\n    must_fail!(c, \"HEXPIRE\", \"myhash\", \"10\", \"NX\", \"XX\", \"FIELDS\", \"1\", \"f\"; \"NX and XX\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_hll.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── PFADD ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_pfadd_basic() {\n    let (_m, mut c) = start().await;\n\n    // Add 3 new elements → 1 (changed)\n    must_int!(c, \"PFADD\", \"h\", \"aap\", \"noot\", \"mies\"; 1);\n\n    // Add duplicate → 0 (not changed)\n    must_int!(c, \"PFADD\", \"h\", \"aap\"; 0);\n\n    // TYPE should be \"hll\"\n    must_str!(c, \"TYPE\", \"h\"; \"hll\");\n}\n\n#[tokio::test]\nasync fn test_pfadd_errors() {\n    let (_m, mut c) = start().await;\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"value\");\n    must_fail!(c, \"PFADD\", \"str\", \"hi\"; \"not a valid HyperLogLog string value\");\n\n    // Wrong number of arguments (no args at all)\n    must_fail!(c, \"PFADD\"; \"wrong number of arguments\");\n}\n\n// ── PFCOUNT ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_pfcount_basic() {\n    let (_m, mut c) = start().await;\n\n    // Add elements one at a time\n    for i in 0..100 {\n        must_int!(c, \"PFADD\", \"h1\", &format!(\"unique-{}\", i); 1);\n    }\n\n    // Add one more\n    must_int!(c, \"PFADD\", \"h1\", \"specific-value\"; 1);\n\n    // Duplicate additions should return 0\n    for _ in 0..10 {\n        must_int!(c, \"PFADD\", \"h1\", \"specific-value\"; 0);\n    }\n\n    // Count should be 101\n    must_int!(c, \"PFCOUNT\", \"h1\"; 101);\n}\n\n#[tokio::test]\nasync fn test_pfcount_multiple_keys() {\n    let (_m, mut c) = start().await;\n\n    // Create two non-overlapping HLLs\n    must_int!(c, \"PFADD\", \"h1\", \"a\", \"b\", \"c\"; 1);\n    must_int!(c, \"PFADD\", \"h2\", \"d\", \"e\"; 1);\n\n    // Single key counts\n    must_int!(c, \"PFCOUNT\", \"h1\"; 3);\n    must_int!(c, \"PFCOUNT\", \"h2\"; 2);\n\n    // Multi-key count (union)\n    must_int!(c, \"PFCOUNT\", \"h1\", \"h2\"; 5);\n\n    // With a non-existent key\n    must_int!(c, \"PFCOUNT\", \"h1\", \"h2\", \"h3\"; 5);\n\n    // Non-existent key alone\n    must_int!(c, \"PFCOUNT\", \"h9\"; 0);\n}\n\n#[tokio::test]\nasync fn test_pfcount_errors() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SET\", \"str\", \"value\");\n\n    // Wrong number of arguments\n    must_fail!(c, \"PFCOUNT\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_fail!(c, \"PFCOUNT\", \"str\"; \"not a valid HyperLogLog string value\");\n\n    // Wrong type mixed with valid key\n    must_int!(c, \"PFADD\", \"h1\", \"a\"; 1);\n    must_fail!(c, \"PFCOUNT\", \"h1\", \"str\"; \"not a valid HyperLogLog string value\");\n}\n\n// ── PFMERGE ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_pfmerge_basic() {\n    let (_m, mut c) = start().await;\n\n    // Create two non-overlapping HLLs\n    for i in 0..100 {\n        must_int!(c, \"PFADD\", \"h1\", &format!(\"item-{}\", i); 1);\n    }\n    for i in 100..200 {\n        must_int!(c, \"PFADD\", \"h3\", &format!(\"item-{}\", i); 1);\n    }\n\n    // Merge non-intersecting\n    must_ok!(c, \"PFMERGE\", \"res1\", \"h1\", \"h3\");\n    let count: i64 = redis::cmd(\"PFCOUNT\")\n        .arg(\"res1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!((195..=205).contains(&count), \"expected ~200, got {}\", count);\n}\n\n#[tokio::test]\nasync fn test_pfmerge_overlapping() {\n    let (_m, mut c) = start().await;\n\n    // Create overlapping HLLs\n    for i in 0..100 {\n        must_int!(c, \"PFADD\", \"h1\", &format!(\"item-{}\", i); 1);\n        if i % 2 == 0 {\n            must_int!(c, \"PFADD\", \"h2\", &format!(\"item-{}\", i); 1);\n        }\n    }\n\n    // h1 has 100, h2 has 50 (all in h1)\n    must_int!(c, \"PFCOUNT\", \"h1\"; 100);\n    must_int!(c, \"PFCOUNT\", \"h2\"; 50);\n\n    // Merge overlapping → should be 100 (union)\n    must_ok!(c, \"PFMERGE\", \"res2\", \"h1\", \"h2\");\n    must_int!(c, \"PFCOUNT\", \"res2\"; 100);\n}\n\n#[tokio::test]\nasync fn test_pfmerge_with_empty() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"PFADD\", \"h1\", \"a\", \"b\", \"c\"; 1);\n\n    // Merge with empty/non-existent key\n    must_ok!(c, \"PFMERGE\", \"res\", \"h1\", \"h_empty\");\n    must_int!(c, \"PFCOUNT\", \"res\"; 3);\n}\n\n#[tokio::test]\nasync fn test_pfmerge_dest_only() {\n    let (_m, mut c) = start().await;\n\n    // PFMERGE with just dest (no sources) — creates empty HLL\n    must_ok!(c, \"PFMERGE\", \"dest\");\n    must_int!(c, \"PFCOUNT\", \"dest\"; 0);\n    must_str!(c, \"TYPE\", \"dest\"; \"hll\");\n}\n\n#[tokio::test]\nasync fn test_pfmerge_errors() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SET\", \"str\", \"value\");\n\n    // Wrong number of arguments\n    must_fail!(c, \"PFMERGE\"; \"wrong number of arguments\");\n\n    // Wrong type source\n    must_fail!(c, \"PFMERGE\", \"h10\", \"str\"; \"not a valid HyperLogLog string value\");\n}\n\n// ── DEL interaction ──────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_hll_del() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"PFADD\", \"h\", \"a\", \"b\", \"c\"; 1);\n    must_int!(c, \"PFCOUNT\", \"h\"; 3);\n\n    // DEL the key\n    must_int!(c, \"DEL\", \"h\"; 1);\n\n    // Count should be 0 now\n    must_int!(c, \"PFCOUNT\", \"h\"; 0);\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_list.rs",
    "content": "// Ported from ../miniredis/cmd_list_test.go\nmod helpers;\n\n#[tokio::test]\nasync fn test_lpush_rpush() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"LPUSH\", \"l\", \"a\"; 1);\n    must_int!(c, \"LPUSH\", \"l\", \"b\"; 2);\n    must_int!(c, \"RPUSH\", \"l\", \"c\"; 3);\n\n    // List should be: b, a, c\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"b\", \"a\", \"c\"]);\n\n    // Multiple values\n    must_int!(c, \"RPUSH\", \"l2\", \"a\", \"b\", \"c\"; 3);\n    must_strs!(c, \"LRANGE\", \"l2\", \"0\", \"-1\"; [\"a\", \"b\", \"c\"]);\n\n    must_int!(c, \"LPUSH\", \"l3\", \"c\", \"b\", \"a\"; 3);\n    must_strs!(c, \"LRANGE\", \"l3\", \"0\", \"-1\"; [\"a\", \"b\", \"c\"]);\n\n    // Errors\n    must_fail!(c, \"LPUSH\"; \"wrong number of arguments\");\n    must_fail!(c, \"LPUSH\", \"key\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_lpop_rpop() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\"; 3);\n\n    must_str!(c, \"LPOP\", \"l\"; \"a\");\n    must_str!(c, \"RPOP\", \"l\"; \"c\");\n    must_str!(c, \"LPOP\", \"l\"; \"b\");\n\n    // Empty list\n    must_nil!(c, \"LPOP\", \"l\");\n    must_nil!(c, \"RPOP\", \"l\");\n\n    // Non-existent key\n    must_nil!(c, \"LPOP\", \"nosuch\");\n}\n\n#[tokio::test]\nasync fn test_lpop_count() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\", \"d\", \"e\"; 5);\n\n    must_strs!(c, \"LPOP\", \"l\", \"3\"; [\"a\", \"b\", \"c\"]);\n    must_strs!(c, \"LPOP\", \"l\", \"10\"; [\"d\", \"e\"]);\n}\n\n#[tokio::test]\nasync fn test_lpushx_rpushx() {\n    let (_m, mut c) = helpers::start().await;\n\n    // PUSHX on non-existing key\n    must_int!(c, \"LPUSHX\", \"l\", \"a\"; 0);\n    must_int!(c, \"RPUSHX\", \"l\", \"a\"; 0);\n\n    // Create the list\n    must_int!(c, \"RPUSH\", \"l\", \"a\"; 1);\n\n    // Now PUSHX works\n    must_int!(c, \"LPUSHX\", \"l\", \"b\"; 2);\n    must_int!(c, \"RPUSHX\", \"l\", \"c\"; 3);\n\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"b\", \"a\", \"c\"]);\n}\n\n#[tokio::test]\nasync fn test_llen() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"LLEN\", \"l\"; 0);\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"LLEN\", \"l\"; 3);\n}\n\n#[tokio::test]\nasync fn test_lindex() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\"; 3);\n\n    must_str!(c, \"LINDEX\", \"l\", \"0\"; \"a\");\n    must_str!(c, \"LINDEX\", \"l\", \"1\"; \"b\");\n    must_str!(c, \"LINDEX\", \"l\", \"2\"; \"c\");\n    must_str!(c, \"LINDEX\", \"l\", \"-1\"; \"c\");\n    must_str!(c, \"LINDEX\", \"l\", \"-2\"; \"b\");\n\n    // Out of range\n    must_nil!(c, \"LINDEX\", \"l\", \"100\");\n    must_nil!(c, \"LINDEX\", \"l\", \"-100\");\n\n    // Non-existent key\n    must_nil!(c, \"LINDEX\", \"nosuch\", \"0\");\n}\n\n#[tokio::test]\nasync fn test_lrange() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\", \"d\", \"e\"; 5);\n\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"a\", \"b\", \"c\", \"d\", \"e\"]);\n    must_strs!(c, \"LRANGE\", \"l\", \"1\", \"3\"; [\"b\", \"c\", \"d\"]);\n    must_strs!(c, \"LRANGE\", \"l\", \"-3\", \"-1\"; [\"c\", \"d\", \"e\"]);\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"100\"; [\"a\", \"b\", \"c\", \"d\", \"e\"]);\n\n    // Empty result\n    let result: Vec<String> = redis::cmd(\"LRANGE\")\n        .arg(\"l\")\n        .arg(\"10\")\n        .arg(\"20\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n\n    // Non-existent key\n    let result: Vec<String> = redis::cmd(\"LRANGE\")\n        .arg(\"nosuch\")\n        .arg(\"0\")\n        .arg(\"-1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n}\n\n#[tokio::test]\nasync fn test_lset() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\"; 3);\n\n    must_ok!(c, \"LSET\", \"l\", \"1\", \"B\");\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"a\", \"B\", \"c\"]);\n\n    // Negative index\n    must_ok!(c, \"LSET\", \"l\", \"-1\", \"C\");\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"a\", \"B\", \"C\"]);\n\n    // Errors\n    must_fail!(c, \"LSET\", \"l\", \"100\", \"x\"; \"index out of range\");\n    must_fail!(c, \"LSET\", \"nosuch\", \"0\", \"x\"; \"no such key\");\n}\n\n#[tokio::test]\nasync fn test_linsert() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"c\"; 2);\n\n    must_int!(c, \"LINSERT\", \"l\", \"BEFORE\", \"c\", \"b\"; 3);\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"a\", \"b\", \"c\"]);\n\n    must_int!(c, \"LINSERT\", \"l\", \"AFTER\", \"c\", \"d\"; 4);\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"a\", \"b\", \"c\", \"d\"]);\n\n    // Pivot not found\n    must_int!(c, \"LINSERT\", \"l\", \"BEFORE\", \"nosuch\", \"x\"; -1);\n\n    // Non-existent key\n    must_int!(c, \"LINSERT\", \"nosuch\", \"BEFORE\", \"a\", \"x\"; 0);\n}\n\n#[tokio::test]\nasync fn test_lrem() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"a\", \"c\", \"a\"; 5);\n\n    // Remove 2 from head\n    must_int!(c, \"LREM\", \"l\", \"2\", \"a\"; 2);\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"b\", \"c\", \"a\"]);\n\n    // Remove from tail\n    must_int!(c, \"RPUSH\", \"l2\", \"a\", \"b\", \"a\", \"c\", \"a\"; 5);\n    must_int!(c, \"LREM\", \"l2\", \"-2\", \"a\"; 2);\n    must_strs!(c, \"LRANGE\", \"l2\", \"0\", \"-1\"; [\"a\", \"b\", \"c\"]);\n\n    // Remove all\n    must_int!(c, \"RPUSH\", \"l3\", \"a\", \"a\", \"a\"; 3);\n    must_int!(c, \"LREM\", \"l3\", \"0\", \"a\"; 3);\n    must_int!(c, \"LLEN\", \"l3\"; 0);\n}\n\n#[tokio::test]\nasync fn test_ltrim() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"l\", \"a\", \"b\", \"c\", \"d\", \"e\"; 5);\n\n    must_ok!(c, \"LTRIM\", \"l\", \"1\", \"3\");\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"-1\"; [\"b\", \"c\", \"d\"]);\n}\n\n#[tokio::test]\nasync fn test_rpoplpush() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"src\", \"a\", \"b\", \"c\"; 3);\n\n    must_str!(c, \"RPOPLPUSH\", \"src\", \"dst\"; \"c\");\n    must_strs!(c, \"LRANGE\", \"src\", \"0\", \"-1\"; [\"a\", \"b\"]);\n    must_strs!(c, \"LRANGE\", \"dst\", \"0\", \"-1\"; [\"c\"]);\n\n    // Empty source\n    must_int!(c, \"RPUSH\", \"empty\", \"x\"; 1);\n    must_str!(c, \"LPOP\", \"empty\"; \"x\");\n    must_nil!(c, \"RPOPLPUSH\", \"empty\", \"dst\");\n}\n\n#[tokio::test]\nasync fn test_lmove() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"RPUSH\", \"src\", \"a\", \"b\", \"c\"; 3);\n\n    must_str!(c, \"LMOVE\", \"src\", \"dst\", \"RIGHT\", \"LEFT\"; \"c\");\n    must_strs!(c, \"LRANGE\", \"src\", \"0\", \"-1\"; [\"a\", \"b\"]);\n    must_strs!(c, \"LRANGE\", \"dst\", \"0\", \"-1\"; [\"c\"]);\n\n    must_str!(c, \"LMOVE\", \"src\", \"dst\", \"LEFT\", \"RIGHT\"; \"a\");\n    must_strs!(c, \"LRANGE\", \"src\", \"0\", \"-1\"; [\"b\"]);\n    must_strs!(c, \"LRANGE\", \"dst\", \"0\", \"-1\"; [\"c\", \"a\"]);\n}\n\n#[tokio::test]\nasync fn test_list_wrongtype() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"LPUSH\", \"str\", \"x\"; \"WRONGTYPE\");\n    must_fail!(c, \"LLEN\", \"str\"; \"WRONGTYPE\");\n    must_fail!(c, \"LRANGE\", \"str\", \"0\", \"-1\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_lpos() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Build list: [aap, aap, vuur, aap, mies, aap, noot, aap]\n    // RPUSH to get them in left-to-right order\n    let _: i64 = redis::cmd(\"RPUSH\")\n        .arg(\"l\")\n        .arg(\"aap\")\n        .arg(\"aap\")\n        .arg(\"vuur\")\n        .arg(\"aap\")\n        .arg(\"mies\")\n        .arg(\"aap\")\n        .arg(\"noot\")\n        .arg(\"aap\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Simple\n    must_int!(c, \"LPOS\", \"l\", \"aap\"; 0);\n    must_int!(c, \"LPOS\", \"l\", \"vuur\"; 2);\n    must_int!(c, \"LPOS\", \"l\", \"mies\"; 4);\n    must_nil!(c, \"LPOS\", \"l\", \"wim\");\n\n    // RANK\n    must_int!(c, \"LPOS\", \"l\", \"aap\", \"RANK\", \"1\"; 0);\n    must_int!(c, \"LPOS\", \"l\", \"aap\", \"RANK\", \"2\"; 1);\n    must_int!(c, \"LPOS\", \"l\", \"aap\", \"RANK\", \"3\"; 3);\n    must_int!(c, \"LPOS\", \"l\", \"aap\", \"RANK\", \"-1\"; 7);\n    must_int!(c, \"LPOS\", \"l\", \"aap\", \"RANK\", \"-2\"; 5);\n\n    // COUNT\n    let vals: Vec<i64> = redis::cmd(\"LPOS\")\n        .arg(\"l\")\n        .arg(\"aap\")\n        .arg(\"COUNT\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals, vec![0, 1, 3, 5, 7]);\n\n    let vals: Vec<i64> = redis::cmd(\"LPOS\")\n        .arg(\"l\")\n        .arg(\"aap\")\n        .arg(\"COUNT\")\n        .arg(3)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals, vec![0, 1, 3]);\n\n    let vals: Vec<i64> = redis::cmd(\"LPOS\")\n        .arg(\"l\")\n        .arg(\"wim\")\n        .arg(\"COUNT\")\n        .arg(1)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(vals.is_empty());\n\n    // RANK + COUNT\n    let vals: Vec<i64> = redis::cmd(\"LPOS\")\n        .arg(\"l\")\n        .arg(\"aap\")\n        .arg(\"RANK\")\n        .arg(3)\n        .arg(\"COUNT\")\n        .arg(2)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals, vec![3, 5]);\n\n    // COUNT + MAXLEN\n    let vals: Vec<i64> = redis::cmd(\"LPOS\")\n        .arg(\"l\")\n        .arg(\"aap\")\n        .arg(\"COUNT\")\n        .arg(0)\n        .arg(\"MAXLEN\")\n        .arg(4)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals, vec![0, 1, 3]);\n\n    // Errors\n    must_fail!(c, \"LPOS\", \"l\"; \"wrong number of arguments\");\n    must_fail!(c, \"LPOS\", \"l\", \"aap\", \"RANK\"; \"syntax error\");\n    must_fail!(c, \"LPOS\", \"l\", \"aap\", \"RANK\", \"0\"; \"can't be zero\");\n    must_fail!(c, \"LPOS\", \"l\", \"aap\", \"COUNT\", \"-1\"; \"can't be negative\");\n    must_fail!(c, \"LPOS\", \"l\", \"aap\", \"MAXLEN\", \"-1\"; \"can't be negative\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"LPOS\", \"str\", \"val\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_blpop() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Non-blocking: data available\n    let _: i64 = redis::cmd(\"RPUSH\")\n        .arg(\"ll\")\n        .arg(\"aap\")\n        .arg(\"noot\")\n        .arg(\"mies\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<String> = redis::cmd(\"BLPOP\")\n        .arg(\"ll\")\n        .arg(1)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"ll\", \"aap\"]);\n\n    // Timeout (short)\n    let result: Option<Vec<String>> = redis::cmd(\"BLPOP\")\n        .arg(\"empty\")\n        .arg(\"0.1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_none());\n\n    // Errors\n    must_fail!(c, \"BLPOP\"; \"wrong number of arguments\");\n    must_fail!(c, \"BLPOP\", \"key\"; \"wrong number of arguments\");\n    must_fail!(c, \"BLPOP\", \"key\", \"-1\"; \"timeout is negative\");\n}\n\n#[tokio::test]\nasync fn test_brpop() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Non-blocking: data available\n    let _: i64 = redis::cmd(\"RPUSH\")\n        .arg(\"ll\")\n        .arg(\"aap\")\n        .arg(\"noot\")\n        .arg(\"mies\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: Vec<String> = redis::cmd(\"BRPOP\")\n        .arg(\"ll\")\n        .arg(1)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"ll\", \"mies\"]);\n\n    // Timeout (short)\n    let result: Option<Vec<String>> = redis::cmd(\"BRPOP\")\n        .arg(\"empty\")\n        .arg(\"0.1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_none());\n\n    // Errors\n    must_fail!(c, \"BRPOP\"; \"wrong number of arguments\");\n    must_fail!(c, \"BRPOP\", \"key\"; \"wrong number of arguments\");\n    must_fail!(c, \"BRPOP\", \"key\", \"-1\"; \"timeout is negative\");\n}\n\n#[tokio::test]\nasync fn test_brpoplpush() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Non-blocking: data available\n    let _: i64 = redis::cmd(\"RPUSH\")\n        .arg(\"l1\")\n        .arg(\"aap\")\n        .arg(\"noot\")\n        .arg(\"mies\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let result: String = redis::cmd(\"BRPOPLPUSH\")\n        .arg(\"l1\")\n        .arg(\"l2\")\n        .arg(1)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, \"mies\");\n    must_strs!(c, \"LRANGE\", \"l2\", \"0\", \"-1\"; [\"mies\"]);\n\n    // Timeout (short)\n    let result: Option<String> = redis::cmd(\"BRPOPLPUSH\")\n        .arg(\"empty\")\n        .arg(\"dst\")\n        .arg(\"0.1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_none());\n\n    // Errors\n    must_fail!(c, \"BRPOPLPUSH\"; \"wrong number of arguments\");\n    must_fail!(c, \"BRPOPLPUSH\", \"key\"; \"wrong number of arguments\");\n    must_fail!(c, \"BRPOPLPUSH\", \"key\", \"bar\"; \"wrong number of arguments\");\n    must_fail!(c, \"BRPOPLPUSH\", \"key\", \"foo\", \"-1\"; \"timeout is negative\");\n}\n\n#[tokio::test]\nasync fn test_blmove() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Setup\n    let _: i64 = redis::cmd(\"RPUSH\")\n        .arg(\"src\")\n        .arg(\"RL\")\n        .arg(\"RR\")\n        .arg(\"LL\")\n        .arg(\"LR\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: i64 = redis::cmd(\"RPUSH\")\n        .arg(\"dst\")\n        .arg(\"m1\")\n        .arg(\"m2\")\n        .arg(\"m3\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // RIGHT LEFT\n    let v: String = redis::cmd(\"BLMOVE\")\n        .arg(\"src\")\n        .arg(\"dst\")\n        .arg(\"RIGHT\")\n        .arg(\"LEFT\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"LR\");\n\n    // LEFT RIGHT\n    let v: String = redis::cmd(\"BLMOVE\")\n        .arg(\"src\")\n        .arg(\"dst\")\n        .arg(\"LEFT\")\n        .arg(\"RIGHT\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"RL\");\n\n    // Timeout (short)\n    let result: Option<String> = redis::cmd(\"BLMOVE\")\n        .arg(\"nosuch\")\n        .arg(\"dst\")\n        .arg(\"RIGHT\")\n        .arg(\"LEFT\")\n        .arg(\"0.1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_none());\n\n    // Errors\n    must_fail!(c, \"BLMOVE\"; \"wrong number of arguments\");\n    must_fail!(c, \"BLMOVE\", \"l\"; \"wrong number of arguments\");\n    must_fail!(c, \"BLMOVE\", \"l\", \"l\"; \"wrong number of arguments\");\n    must_fail!(c, \"BLMOVE\", \"l\", \"l\", \"l\"; \"wrong number of arguments\");\n    must_fail!(c, \"BLMOVE\", \"l\", \"l\", \"l\", \"l\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_brpop_tx() {\n    // BRPOP in a transaction behaves as if the timeout triggers right away\n    let (m, mut c) = helpers::start().await;\n\n    // BRPOP on empty list inside MULTI → null (no blocking)\n    must_ok!(c, \"MULTI\");\n    let v: String = redis::cmd(\"BRPOP\")\n        .arg(\"l1\")\n        .arg(3)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"foo\")\n        .arg(\"bar\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n    let v: redis::Value = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 2);\n            assert_eq!(items[0], redis::Value::Nil);\n        }\n        _ => panic!(\"expected array from EXEC, got {:?}\", v),\n    }\n\n    // Now push something and BRPOP in MULTI → should pop it\n    m.push(\"l1\", &[\"e1\"]);\n    must_ok!(c, \"MULTI\");\n    let v: String = redis::cmd(\"BRPOP\")\n        .arg(\"l1\")\n        .arg(3)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"foo\")\n        .arg(\"bar\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n    let v: redis::Value = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 2);\n            // First result: [key, value]\n            match &items[0] {\n                redis::Value::Array(kv) => {\n                    assert_eq!(kv.len(), 2);\n                }\n                _ => panic!(\"expected array from BRPOP result, got {:?}\", items[0]),\n            }\n        }\n        _ => panic!(\"expected array from EXEC, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_blpop_resource_cleanup() {\n    // Test that a blocking BLPOP is cleaned up when the connection is closed.\n    // Ensures the server doesn't leak resources or hang.\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut conn = client.get_multiplexed_async_connection().await.unwrap();\n\n    // Issue a short-timeout BLPOP on a non-existent key\n    let result: Option<(String, String)> = redis::cmd(\"BLPOP\")\n        .arg(\"nonexistent\")\n        .arg(\"0.1\")\n        .query_async(&mut conn)\n        .await\n        .unwrap();\n    assert!(result.is_none(), \"BLPOP should timeout with nil\");\n\n    // Drop the connection and close the server — should not hang\n    drop(conn);\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn test_rpush_pop() {\n    let (m, mut c) = helpers::start().await;\n\n    // RPUSH\n    must_int!(c, \"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\"; 3);\n\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"0\"; [\"aap\"]);\n    must_strs!(c, \"LRANGE\", \"l\", \"-1\", \"-1\"; [\"mies\"]);\n\n    // Push more\n    must_int!(c, \"RPUSH\", \"l\", \"aap2\", \"noot2\", \"mies2\"; 6);\n\n    must_strs!(c, \"LRANGE\", \"l\", \"0\", \"0\"; [\"aap\"]);\n    must_strs!(c, \"LRANGE\", \"l\", \"-1\", \"-1\"; [\"mies2\"]);\n\n    // Direct API: Push and Pop\n    let len = m.push(\"l2\", &[\"a\"]);\n    assert_eq!(len, 1);\n    let len = m.push(\"l2\", &[\"b\"]);\n    assert_eq!(len, 2);\n\n    let list = m.list(\"l2\");\n    assert_eq!(list, Some(vec![\"a\".to_string(), \"b\".to_string()]));\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_misc.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── QUIT ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_quit() {\n    let (_m, mut c) = start().await;\n\n    // QUIT should return OK\n    must_ok!(c, \"QUIT\");\n}\n\n// ── COMMAND ─────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_command() {\n    let (_m, mut c) = start().await;\n\n    let v: redis::Value = redis::cmd(\"COMMAND\").query_async(&mut c).await.unwrap();\n\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 200, \"expected 200 command entries\");\n        }\n        _ => panic!(\"expected array from COMMAND, got {:?}\", v),\n    }\n}\n\n// ── INFO ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_info_no_args() {\n    let (_m, mut c) = start().await;\n\n    let v: String = redis::cmd(\"INFO\").query_async(&mut c).await.unwrap();\n\n    assert!(v.contains(\"# Clients\"), \"expected Clients section\");\n    assert!(\n        v.contains(\"connected_clients:\"),\n        \"expected connected_clients\"\n    );\n    assert!(v.contains(\"# Stats\"), \"expected Stats section\");\n    assert!(\n        v.contains(\"total_connections_received:\"),\n        \"expected total_connections_received\"\n    );\n    assert!(\n        v.contains(\"total_commands_processed:\"),\n        \"expected total_commands_processed\"\n    );\n}\n\n#[tokio::test]\nasync fn test_info_clients() {\n    let (_m, mut c) = start().await;\n\n    let v: String = redis::cmd(\"INFO\")\n        .arg(\"clients\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert!(v.contains(\"# Clients\"), \"expected Clients section\");\n    assert!(\n        v.contains(\"connected_clients:\"),\n        \"expected connected_clients\"\n    );\n    assert!(!v.contains(\"# Stats\"), \"should not contain Stats section\");\n}\n\n#[tokio::test]\nasync fn test_info_stats() {\n    let (_m, mut c) = start().await;\n\n    let v: String = redis::cmd(\"INFO\")\n        .arg(\"stats\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert!(v.contains(\"# Stats\"), \"expected Stats section\");\n    assert!(\n        v.contains(\"total_connections_received:\"),\n        \"expected total_connections_received\"\n    );\n    assert!(\n        !v.contains(\"# Clients\"),\n        \"should not contain Clients section\"\n    );\n}\n\n#[tokio::test]\nasync fn test_info_invalid_section() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"INFO\", \"bogus\"; \"not supported\");\n}\n\n// ── CLIENT SETNAME/GETNAME ──────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_client_setname_getname() {\n    let (_m, mut c) = start().await;\n\n    // GETNAME before setting → nil\n    must_nil!(c, \"CLIENT\", \"GETNAME\");\n\n    // SETNAME\n    must_ok!(c, \"CLIENT\", \"SETNAME\", \"myconn\");\n\n    // GETNAME\n    must_str!(c, \"CLIENT\", \"GETNAME\"; \"myconn\");\n\n    // Reset name with empty string\n    must_ok!(c, \"CLIENT\", \"SETNAME\", \"\");\n    must_nil!(c, \"CLIENT\", \"GETNAME\");\n}\n\n#[tokio::test]\nasync fn test_client_setname_errors() {\n    let (_m, mut c) = start().await;\n\n    // Name with space\n    must_fail!(c, \"CLIENT\", \"SETNAME\", \"my name\"; \"cannot contain spaces\");\n\n    // Name with newline\n    must_fail!(c, \"CLIENT\", \"SETNAME\", \"my\\nname\"; \"cannot contain spaces\");\n\n    // Wrong number of args\n    must_fail!(c, \"CLIENT\", \"SETNAME\"; \"wrong number of arguments\");\n    must_fail!(c, \"CLIENT\", \"SETNAME\", \"a\", \"b\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_client_getname_errors() {\n    let (_m, mut c) = start().await;\n\n    // Wrong number of args\n    must_fail!(c, \"CLIENT\", \"GETNAME\", \"extra\"; \"wrong number of arguments\");\n}\n\n// ── CLUSTER ─────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_cluster_slots() {\n    let (_m, mut c) = start().await;\n\n    let v: redis::Value = redis::cmd(\"CLUSTER\")\n        .arg(\"SLOTS\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 1, \"expected 1 slot range\");\n        }\n        _ => panic!(\"expected array from CLUSTER SLOTS, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_cluster_keyslot() {\n    let (_m, mut c) = start().await;\n\n    let v: i64 = redis::cmd(\"CLUSTER\")\n        .arg(\"KEYSLOT\")\n        .arg(\"foo\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert_eq!(v, 163);\n}\n\n#[tokio::test]\nasync fn test_cluster_nodes() {\n    let (_m, mut c) = start().await;\n\n    let v: String = redis::cmd(\"CLUSTER\")\n        .arg(\"NODES\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert!(\n        v.contains(\"myself,master\"),\n        \"expected myself,master, got: {}\",\n        v\n    );\n    assert!(\n        v.contains(\"0-16383\"),\n        \"expected 0-16383 slot range, got: {}\",\n        v\n    );\n}\n\n#[tokio::test]\nasync fn test_cluster_shards() {\n    let (_m, mut c) = start().await;\n\n    let v: redis::Value = redis::cmd(\"CLUSTER\")\n        .arg(\"SHARDS\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(items) => {\n            assert!(!items.is_empty(), \"expected at least 1 shard\");\n        }\n        _ => panic!(\"expected array from CLUSTER SHARDS, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_cluster_unknown() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"CLUSTER\", \"BOGUS\"; \"unknown subcommand\");\n}\n\n// ── OBJECT IDLETIME ─────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_object_idletime() {\n    let (_m, mut c) = start().await;\n\n    // Non-existent key → nil\n    must_nil!(c, \"OBJECT\", \"IDLETIME\", \"nosuch\");\n\n    // Set a key, check idle time ≥ 0\n    must_ok!(c, \"SET\", \"key\", \"val\");\n\n    let v: i64 = redis::cmd(\"OBJECT\")\n        .arg(\"IDLETIME\")\n        .arg(\"key\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert!(v >= 0, \"expected non-negative idle time, got {}\", v);\n}\n\n#[tokio::test]\nasync fn test_object_idletime_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"OBJECT\"; \"wrong number of arguments\");\n    must_fail!(c, \"OBJECT\", \"IDLETIME\"; \"wrong number of arguments\");\n    must_fail!(c, \"OBJECT\", \"BOGUS\"; \"unknown subcommand\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_pubsub.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── PUBLISH (through regular dispatch) ──────────────────────────────\n\n#[tokio::test]\nasync fn test_publish_no_subscribers() {\n    let (_m, mut c) = start().await;\n\n    // No subscribers → 0\n    must_int!(c, \"PUBLISH\", \"ch\", \"hello\"; 0);\n}\n\n#[tokio::test]\nasync fn test_publish_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"PUBLISH\"; \"wrong number of arguments\");\n    must_fail!(c, \"PUBLISH\", \"ch\"; \"wrong number of arguments\");\n    must_fail!(c, \"PUBLISH\", \"ch\", \"msg\", \"extra\"; \"wrong number of arguments\");\n}\n\n// ── PUBSUB CHANNELS/NUMSUB/NUMPAT (through regular dispatch) ───────\n\n#[tokio::test]\nasync fn test_pubsub_channels_empty() {\n    let (_m, mut c) = start().await;\n\n    let v: Vec<String> = redis::cmd(\"PUBSUB\")\n        .arg(\"CHANNELS\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v.is_empty());\n}\n\n#[tokio::test]\nasync fn test_pubsub_numsub_empty() {\n    let (_m, mut c) = start().await;\n\n    let v: redis::Value = redis::cmd(\"PUBSUB\")\n        .arg(\"NUMSUB\")\n        .arg(\"ch1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 2);\n            // channel name, count (0)\n        }\n        _ => panic!(\"expected array from PUBSUB NUMSUB, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_pubsub_numpat_empty() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"PUBSUB\", \"NUMPAT\"; 0);\n}\n\n// ── SUBSCRIBE + PUBLISH via redis-rs PubSub API ────────────────────\n\n#[tokio::test]\nasync fn test_subscribe_and_publish() {\n    let (m, mut c) = start().await;\n\n    // Create a subscriber using redis-rs PubSub\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    // Subscribe to a channel\n    pubsub.subscribe(\"ch1\").await.unwrap();\n\n    // Give a moment for subscription to register\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // Publish a message\n    must_int!(c, \"PUBLISH\", \"ch1\", \"hello\"; 1);\n\n    // Receive the message\n    let msg = tokio::time::timeout(\n        std::time::Duration::from_secs(2),\n        pubsub.on_message().next(),\n    )\n    .await\n    .expect(\"timeout waiting for message\")\n    .expect(\"no message received\");\n\n    let payload: String = msg.get_payload().unwrap();\n    assert_eq!(payload, \"hello\");\n    assert_eq!(msg.get_channel_name(), \"ch1\");\n}\n\n#[tokio::test]\nasync fn test_subscribe_multiple_channels() {\n    let (m, mut c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.subscribe(\"ch1\").await.unwrap();\n    pubsub.subscribe(\"ch2\").await.unwrap();\n\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // Publish to ch1\n    must_int!(c, \"PUBLISH\", \"ch1\", \"msg1\"; 1);\n    // Publish to ch2\n    must_int!(c, \"PUBLISH\", \"ch2\", \"msg2\"; 1);\n\n    // Receive both messages\n    let msg1 = tokio::time::timeout(\n        std::time::Duration::from_secs(2),\n        pubsub.on_message().next(),\n    )\n    .await\n    .expect(\"timeout\")\n    .expect(\"no message\");\n    let payload1: String = msg1.get_payload().unwrap();\n\n    let msg2 = tokio::time::timeout(\n        std::time::Duration::from_secs(2),\n        pubsub.on_message().next(),\n    )\n    .await\n    .expect(\"timeout\")\n    .expect(\"no message\");\n    let payload2: String = msg2.get_payload().unwrap();\n\n    let mut payloads = vec![payload1, payload2];\n    payloads.sort();\n    assert_eq!(payloads, vec![\"msg1\", \"msg2\"]);\n}\n\n#[tokio::test]\nasync fn test_psubscribe() {\n    let (m, mut c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.psubscribe(\"event*\").await.unwrap();\n\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // Publish to a matching channel\n    must_int!(c, \"PUBLISH\", \"event123\", \"data\"; 1);\n\n    // Non-matching channel\n    must_int!(c, \"PUBLISH\", \"other\", \"data\"; 0);\n\n    // Receive the matching message\n    let msg = tokio::time::timeout(\n        std::time::Duration::from_secs(2),\n        pubsub.on_message().next(),\n    )\n    .await\n    .expect(\"timeout\")\n    .expect(\"no message\");\n\n    let payload: String = msg.get_payload().unwrap();\n    assert_eq!(payload, \"data\");\n}\n\n#[tokio::test]\nasync fn test_unsubscribe() {\n    let (m, mut c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.subscribe(\"ch1\").await.unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // Can receive\n    must_int!(c, \"PUBLISH\", \"ch1\", \"before\"; 1);\n    let msg = tokio::time::timeout(\n        std::time::Duration::from_secs(2),\n        pubsub.on_message().next(),\n    )\n    .await\n    .expect(\"timeout\")\n    .expect(\"no message\");\n    let payload: String = msg.get_payload().unwrap();\n    assert_eq!(payload, \"before\");\n\n    // Unsubscribe\n    pubsub.unsubscribe(\"ch1\").await.unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // Should have 0 subscribers now\n    must_int!(c, \"PUBLISH\", \"ch1\", \"after\"; 0);\n}\n\n#[tokio::test]\nasync fn test_pubsub_channels_with_subscriber() {\n    let (m, mut c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.subscribe(\"news\").await.unwrap();\n    pubsub.subscribe(\"sports\").await.unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // PUBSUB CHANNELS should list both\n    let mut channels: Vec<String> = redis::cmd(\"PUBSUB\")\n        .arg(\"CHANNELS\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    channels.sort();\n    assert_eq!(channels, vec![\"news\", \"sports\"]);\n\n    // PUBSUB CHANNELS with pattern\n    let channels: Vec<String> = redis::cmd(\"PUBSUB\")\n        .arg(\"CHANNELS\")\n        .arg(\"n*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(channels, vec![\"news\"]);\n}\n\n#[tokio::test]\nasync fn test_pubsub_numsub_with_subscriber() {\n    let (m, mut c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.subscribe(\"ch1\").await.unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    let v: redis::Value = redis::cmd(\"PUBSUB\")\n        .arg(\"NUMSUB\")\n        .arg(\"ch1\")\n        .arg(\"ch2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 4); // ch1, count, ch2, count\n        }\n        _ => panic!(\"expected array from PUBSUB NUMSUB, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_pubsub_numpat_with_subscriber() {\n    let (m, mut c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.psubscribe(\"event*\").await.unwrap();\n    pubsub.psubscribe(\"news*\").await.unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    must_int!(c, \"PUBSUB\", \"NUMPAT\"; 2);\n}\n\n// ── Direct API ──────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_publish_direct_api() {\n    let (m, _c) = start().await;\n\n    let sub_client = redis::Client::open(m.redis_url()).unwrap();\n    let mut pubsub = sub_client.get_async_pubsub().await.unwrap();\n\n    pubsub.subscribe(\"ch1\").await.unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    // Use direct API to publish\n    let count = m.publish(\"ch1\", \"hello-direct\");\n    assert_eq!(count, 1);\n\n    // Receive the message\n    let msg = tokio::time::timeout(\n        std::time::Duration::from_secs(2),\n        pubsub.on_message().next(),\n    )\n    .await\n    .expect(\"timeout\")\n    .expect(\"no message\");\n\n    let payload: String = msg.get_payload().unwrap();\n    assert_eq!(payload, \"hello-direct\");\n}\n\n// Need this import for Stream trait used by on_message()\nuse futures_lite::StreamExt;\n"
  },
  {
    "path": "miniredis/tests/cmd_resp3.rs",
    "content": "mod helpers;\nuse helpers::*;\nuse tokio::io::{AsyncReadExt, AsyncWriteExt};\nuse tokio::net::TcpStream;\n\n// ── Helper: raw TCP connection for RESP3 wire format tests ──────────\n\nasync fn raw_connect(m: &miniredis_rs::Miniredis) -> TcpStream {\n    TcpStream::connect(m.addr()).await.unwrap()\n}\n\n/// Send a RESP2 command (array of bulk strings) and return raw response bytes.\nasync fn raw_cmd(stream: &mut TcpStream, args: &[&str]) -> Vec<u8> {\n    // Build RESP2 array\n    let mut cmd = format!(\"*{}\\r\\n\", args.len());\n    for arg in args {\n        cmd.push_str(&format!(\"${}\\r\\n{}\\r\\n\", arg.len(), arg));\n    }\n    stream.write_all(cmd.as_bytes()).await.unwrap();\n    stream.flush().await.unwrap();\n\n    // Read response (wait a bit for it to arrive)\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n    let mut buf = vec![0u8; 4096];\n    let n = stream.read(&mut buf).await.unwrap();\n    buf.truncate(n);\n    buf\n}\n\n// ── HELLO command tests ─────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_hello_2_returns_map_as_array() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // HELLO 2 should return a flat array (RESP2 encoding of Map)\n    let resp = raw_cmd(&mut stream, &[\"HELLO\", \"2\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    // Should start with * (RESP2 array), containing 14 elements (7 key-value pairs)\n    assert!(\n        resp_str.starts_with(\"*14\\r\\n\"),\n        \"expected RESP2 array *14, got: {:?}\",\n        resp_str\n    );\n}\n\n#[tokio::test]\nasync fn test_hello_3_returns_resp3_map() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // HELLO 3 should return a RESP3 map\n    let resp = raw_cmd(&mut stream, &[\"HELLO\", \"3\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    // Should start with % (RESP3 map) with 7 key-value pairs\n    assert!(\n        resp_str.starts_with(\"%7\\r\\n\"),\n        \"expected RESP3 map %7, got: {:?}\",\n        resp_str\n    );\n}\n\n#[tokio::test]\nasync fn test_hello_3_enables_resp3_null() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // Switch to RESP3\n    let _ = raw_cmd(&mut stream, &[\"HELLO\", \"3\"]).await;\n\n    // GET on non-existent key should return RESP3 null: _\\r\\n\n    let resp = raw_cmd(&mut stream, &[\"GET\", \"nosuch\"]).await;\n    assert_eq!(\n        resp,\n        b\"_\\r\\n\",\n        \"expected RESP3 null, got: {:?}\",\n        String::from_utf8_lossy(&resp)\n    );\n}\n\n#[tokio::test]\nasync fn test_resp2_null_is_dollar_minus_one() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // Without HELLO 3, GET on non-existent key should return RESP2 null: $-1\\r\\n\n    let resp = raw_cmd(&mut stream, &[\"GET\", \"nosuch\"]).await;\n    assert_eq!(\n        resp,\n        b\"$-1\\r\\n\",\n        \"expected RESP2 null, got: {:?}\",\n        String::from_utf8_lossy(&resp)\n    );\n}\n\n// ── HGETALL RESP3 map ───────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_hgetall_resp2_flat_array() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // Set up a hash\n    let _ = raw_cmd(&mut stream, &[\"HSET\", \"h\", \"f1\", \"v1\"]).await;\n\n    // HGETALL in RESP2 mode returns flat array\n    let resp = raw_cmd(&mut stream, &[\"HGETALL\", \"h\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    assert!(\n        resp_str.starts_with(\"*2\\r\\n\"),\n        \"expected RESP2 array *2, got: {:?}\",\n        resp_str\n    );\n}\n\n#[tokio::test]\nasync fn test_hgetall_resp3_map() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // Set up a hash\n    let _ = raw_cmd(&mut stream, &[\"HSET\", \"h\", \"f1\", \"v1\"]).await;\n\n    // Switch to RESP3\n    let _ = raw_cmd(&mut stream, &[\"HELLO\", \"3\"]).await;\n\n    // HGETALL in RESP3 mode returns map\n    let resp = raw_cmd(&mut stream, &[\"HGETALL\", \"h\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    assert!(\n        resp_str.starts_with(\"%1\\r\\n\"),\n        \"expected RESP3 map %1, got: {:?}\",\n        resp_str\n    );\n    // Verify it contains the field-value pair\n    assert!(\n        resp_str.contains(\"f1\"),\n        \"response should contain field 'f1'\"\n    );\n    assert!(\n        resp_str.contains(\"v1\"),\n        \"response should contain value 'v1'\"\n    );\n}\n\n// ── SMEMBERS RESP3 set ──────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_smembers_resp2_array() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    let _ = raw_cmd(&mut stream, &[\"SADD\", \"s\", \"a\", \"b\"]).await;\n\n    // SMEMBERS in RESP2 mode returns array\n    let resp = raw_cmd(&mut stream, &[\"SMEMBERS\", \"s\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    assert!(\n        resp_str.starts_with(\"*2\\r\\n\"),\n        \"expected RESP2 array *2, got: {:?}\",\n        resp_str\n    );\n}\n\n#[tokio::test]\nasync fn test_smembers_resp3_set() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    let _ = raw_cmd(&mut stream, &[\"SADD\", \"s\", \"a\", \"b\"]).await;\n\n    // Switch to RESP3\n    let _ = raw_cmd(&mut stream, &[\"HELLO\", \"3\"]).await;\n\n    // SMEMBERS in RESP3 mode returns set (~)\n    let resp = raw_cmd(&mut stream, &[\"SMEMBERS\", \"s\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    assert!(\n        resp_str.starts_with(\"~2\\r\\n\"),\n        \"expected RESP3 set ~2, got: {:?}\",\n        resp_str\n    );\n}\n\n// ── Commands still work after HELLO 3 ───────────────────────────────\n\n#[tokio::test]\nasync fn test_commands_work_after_hello_3() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // Switch to RESP3\n    let _ = raw_cmd(&mut stream, &[\"HELLO\", \"3\"]).await;\n\n    // SET should still return +OK\n    let resp = raw_cmd(&mut stream, &[\"SET\", \"key\", \"value\"]).await;\n    assert_eq!(resp, b\"+OK\\r\\n\");\n\n    // GET should return bulk string\n    let resp = raw_cmd(&mut stream, &[\"GET\", \"key\"]).await;\n    assert_eq!(resp, b\"$5\\r\\nvalue\\r\\n\");\n\n    // Integer commands work\n    let resp = raw_cmd(&mut stream, &[\"DEL\", \"key\"]).await;\n    assert_eq!(resp, b\":1\\r\\n\");\n}\n\n#[tokio::test]\nasync fn test_hello_3_then_hello_2_resets() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let mut stream = raw_connect(&m).await;\n\n    // Switch to RESP3\n    let resp = raw_cmd(&mut stream, &[\"HELLO\", \"3\"]).await;\n    assert!(String::from_utf8_lossy(&resp).starts_with(\"%\"));\n\n    // Switch back to RESP2\n    let resp = raw_cmd(&mut stream, &[\"HELLO\", \"2\"]).await;\n    // HELLO 2 returns Map but serialized as RESP2 flat array since we just set resp2\n    assert!(String::from_utf8_lossy(&resp).starts_with(\"*\"));\n\n    // GET on non-existent key should now return RESP2 null\n    let resp = raw_cmd(&mut stream, &[\"GET\", \"nosuch\"]).await;\n    assert_eq!(resp, b\"$-1\\r\\n\");\n}\n\n// ── HELLO with AUTH ─────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_hello_3_with_auth() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    m.require_auth(\"password\");\n\n    let mut stream = raw_connect(&m).await;\n\n    // HELLO 3 AUTH default password\n    let resp = raw_cmd(&mut stream, &[\"HELLO\", \"3\", \"AUTH\", \"default\", \"password\"]).await;\n    let resp_str = String::from_utf8_lossy(&resp);\n    assert!(\n        resp_str.starts_with(\"%7\\r\\n\"),\n        \"expected RESP3 map, got: {:?}\",\n        resp_str\n    );\n\n    // Should be authenticated and in RESP3 mode\n    let resp = raw_cmd(&mut stream, &[\"SET\", \"k\", \"v\"]).await;\n    assert_eq!(resp, b\"+OK\\r\\n\");\n}\n\n// ── Via redis-rs client ─────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_hello_via_redis_rs() {\n    let (_m, mut c) = start().await;\n\n    // HELLO 2 should work via redis-rs\n    let result: Vec<redis::Value> = redis::cmd(\"HELLO\")\n        .arg(\"2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    // Returns as flat array in RESP2 mode\n    assert!(!result.is_empty());\n\n    // Commands should still work\n    must_ok!(c, \"SET\", \"k\", \"v\");\n    must_str!(c, \"GET\", \"k\"; \"v\");\n}\n\n#[tokio::test]\nasync fn test_hello_invalid_version() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"HELLO\", \"4\"; \"NOPROTO\");\n    must_fail!(c, \"HELLO\", \"0\"; \"NOPROTO\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_scripting.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── EVAL basic ───────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_return_string() {\n    let (_m, mut c) = start().await;\n\n    must_str!(c, \"EVAL\", \"return 'hello'\", \"0\"; \"hello\");\n}\n\n#[tokio::test]\nasync fn test_eval_return_number() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"EVAL\", \"return 42\", \"0\"; 42);\n}\n\n#[tokio::test]\nasync fn test_eval_return_true() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"EVAL\", \"return true\", \"0\"; 1);\n}\n\n#[tokio::test]\nasync fn test_eval_return_false() {\n    let (_m, mut c) = start().await;\n\n    must_nil!(c, \"EVAL\", \"return false\", \"0\");\n}\n\n#[tokio::test]\nasync fn test_eval_return_nil() {\n    let (_m, mut c) = start().await;\n\n    must_nil!(c, \"EVAL\", \"return nil\", \"0\");\n}\n\n#[tokio::test]\nasync fn test_eval_return_table() {\n    let (_m, mut c) = start().await;\n\n    let result: Vec<String> = redis::cmd(\"EVAL\")\n        .arg(\"return {'a', 'b', 'c'}\")\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"a\", \"b\", \"c\"]);\n}\n\n// ── KEYS and ARGV ────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_keys_argv() {\n    let (_m, mut c) = start().await;\n\n    // KEYS[1] = \"mykey\", ARGV[1] = \"myval\"\n    must_str!(c, \"EVAL\", \"return KEYS[1]\", \"1\", \"mykey\", \"myval\"; \"mykey\");\n    must_str!(c, \"EVAL\", \"return ARGV[1]\", \"1\", \"mykey\", \"myval\"; \"myval\");\n}\n\n#[tokio::test]\nasync fn test_eval_multiple_keys() {\n    let (_m, mut c) = start().await;\n\n    must_str!(c, \"EVAL\", \"return KEYS[2]\", \"2\", \"k1\", \"k2\"; \"k2\");\n}\n\n// ── redis.call() ─────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_redis_call_set_get() {\n    let (_m, mut c) = start().await;\n\n    // Use redis.call to SET and GET\n    let script = r#\"\n        redis.call('SET', KEYS[1], ARGV[1])\n        return redis.call('GET', KEYS[1])\n    \"#;\n    must_str!(c, \"EVAL\", script, \"1\", \"foo\", \"bar\"; \"bar\");\n\n    // Verify the value persists\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n}\n\n#[tokio::test]\nasync fn test_eval_redis_call_incr() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SET\", \"counter\", \"10\");\n\n    let script = \"return redis.call('INCR', KEYS[1])\";\n    must_int!(c, \"EVAL\", script, \"1\", \"counter\"; 11);\n}\n\n#[tokio::test]\nasync fn test_eval_redis_call_multiple() {\n    let (_m, mut c) = start().await;\n\n    let script = r#\"\n        redis.call('SET', 'k1', 'v1')\n        redis.call('SET', 'k2', 'v2')\n        return redis.call('MGET', 'k1', 'k2')\n    \"#;\n    let result: Vec<String> = redis::cmd(\"EVAL\")\n        .arg(script)\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"v1\", \"v2\"]);\n}\n\n// ── redis.pcall() ────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_pcall_error() {\n    let (_m, mut c) = start().await;\n\n    // pcall catches errors\n    let script = r#\"\n        local ok, err = pcall(function()\n            return redis.call('NOSUCHCOMMAND')\n        end)\n        if ok then\n            return 'no error'\n        else\n            return 'got error'\n        end\n    \"#;\n    must_str!(c, \"EVAL\", script, \"0\"; \"got error\");\n}\n\n#[tokio::test]\nasync fn test_eval_redis_pcall() {\n    let (_m, mut c) = start().await;\n\n    // redis.pcall returns error as table\n    let script = r#\"\n        local res = redis.pcall('SET', 'key')\n        if res.err then\n            return 'error: ' .. res.err\n        end\n        return 'no error'\n    \"#;\n    let result: String = redis::cmd(\"EVAL\")\n        .arg(script)\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(\n        result.starts_with(\"error: \"),\n        \"expected error prefix, got: {}\",\n        result\n    );\n}\n\n// ── redis.error_reply() / redis.status_reply() ──────────────────────\n\n#[tokio::test]\nasync fn test_eval_error_reply() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"EVAL\", \"return redis.error_reply('MY_ERR custom error')\", \"0\"; \"MY_ERR\");\n}\n\n#[tokio::test]\nasync fn test_eval_status_reply() {\n    let (_m, mut c) = start().await;\n\n    must_str!(c, \"EVAL\", \"return redis.status_reply('PONG')\", \"0\"; \"PONG\");\n}\n\n// ── redis.sha1hex() ──────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_sha1hex() {\n    let (_m, mut c) = start().await;\n\n    // SHA1 of empty string = da39a3ee5e6b4b0d3255bfef95601890afd80709\n    must_str!(c, \"EVAL\", \"return redis.sha1hex('')\", \"0\"; \"da39a3ee5e6b4b0d3255bfef95601890afd80709\");\n}\n\n// ── EVALSHA ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_evalsha_basic() {\n    let (_m, mut c) = start().await;\n\n    // Load a script via EVAL first (caches it)\n    must_int!(c, \"EVAL\", \"return 42\", \"0\"; 42);\n\n    // Now get the SHA and use EVALSHA\n    let sha: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(\"return 42\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    must_int!(c, \"EVALSHA\", &sha, \"0\"; 42);\n}\n\n#[tokio::test]\nasync fn test_evalsha_not_found() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"EVALSHA\", \"deadbeef\", \"0\"; \"No matching script\");\n}\n\n// ── SCRIPT LOAD / EXISTS / FLUSH ─────────────────────────────────────\n\n#[tokio::test]\nasync fn test_script_load() {\n    let (_m, mut c) = start().await;\n\n    let sha: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(\"return 'loaded'\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // SHA should be a 40-char hex string\n    assert_eq!(sha.len(), 40);\n\n    // EVALSHA should work now\n    must_str!(c, \"EVALSHA\", &sha, \"0\"; \"loaded\");\n}\n\n#[tokio::test]\nasync fn test_script_exists() {\n    let (_m, mut c) = start().await;\n\n    let sha: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(\"return 1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // EXISTS should find it\n    let result: Vec<i64> = redis::cmd(\"SCRIPT\")\n        .arg(\"EXISTS\")\n        .arg(&sha)\n        .arg(\"deadbeef\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1, 0]);\n}\n\n#[tokio::test]\nasync fn test_script_flush() {\n    let (_m, mut c) = start().await;\n\n    let sha: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(\"return 1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Flush all scripts\n    must_ok!(c, \"SCRIPT\", \"FLUSH\");\n\n    // Should no longer exist\n    let result: Vec<i64> = redis::cmd(\"SCRIPT\")\n        .arg(\"EXISTS\")\n        .arg(&sha)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![0]);\n}\n\n#[tokio::test]\nasync fn test_script_flush_sync() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SCRIPT\", \"FLUSH\", \"SYNC\");\n}\n\n// ── Error handling ───────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_errors() {\n    let (_m, mut c) = start().await;\n\n    // Wrong number of args\n    must_fail!(c, \"EVAL\"; \"wrong number of arguments\");\n    must_fail!(c, \"EVAL\", \"return 1\"; \"wrong number of arguments\");\n\n    // Invalid numkeys\n    must_fail!(c, \"EVAL\", \"return 1\", \"abc\"; \"not an integer\");\n\n    // Negative numkeys\n    must_fail!(c, \"EVAL\", \"return 1\", \"-1\"; \"negative\");\n\n    // numkeys > remaining args\n    must_fail!(c, \"EVAL\", \"return 1\", \"2\", \"key1\"; \"greater than number of args\");\n}\n\n#[tokio::test]\nasync fn test_eval_syntax_error() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"EVAL\", \"this is not valid lua!!\", \"0\"; \"Error compiling script\");\n}\n\n#[tokio::test]\nasync fn test_eval_runtime_error() {\n    let (_m, mut c) = start().await;\n\n    // redis.call with wrong type should propagate error\n    must_ok!(c, \"SET\", \"str\", \"value\");\n    must_fail!(c, \"EVAL\", \"return redis.call('LPUSH', 'str', 'v')\", \"0\"; \"WRONGTYPE\");\n}\n\n// ── Script using various data types ──────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_with_hash() {\n    let (_m, mut c) = start().await;\n\n    let script = r#\"\n        redis.call('HSET', KEYS[1], 'field1', 'val1')\n        redis.call('HSET', KEYS[1], 'field2', 'val2')\n        return redis.call('HGET', KEYS[1], 'field1')\n    \"#;\n    must_str!(c, \"EVAL\", script, \"1\", \"myhash\"; \"val1\");\n}\n\n#[tokio::test]\nasync fn test_eval_with_list() {\n    let (_m, mut c) = start().await;\n\n    let script = r#\"\n        redis.call('RPUSH', KEYS[1], 'a', 'b', 'c')\n        return redis.call('LLEN', KEYS[1])\n    \"#;\n    must_int!(c, \"EVAL\", script, \"1\", \"mylist\"; 3);\n}\n\n// ── SCRIPT subcommand errors ─────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_script_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"SCRIPT\"; \"wrong number of arguments\");\n    must_fail!(c, \"SCRIPT\", \"NOSUCHSUB\"; \"unknown subcommand\");\n    must_fail!(c, \"SCRIPT\", \"LOAD\"; \"wrong number of arguments\");\n    must_fail!(c, \"SCRIPT\", \"EXISTS\"; \"wrong number of arguments\");\n}\n\n// ── EVAL_RO / EVALSHA_RO ─────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_ro_read() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SET\", \"k\", \"v\");\n\n    // Read-only should allow reads\n    must_str!(c, \"EVAL_RO\", \"return redis.call('GET', KEYS[1])\", \"1\", \"k\"; \"v\");\n}\n\n#[tokio::test]\nasync fn test_eval_ro_write_blocked() {\n    let (_m, mut c) = start().await;\n\n    // Read-only should block writes\n    must_fail!(c, \"EVAL_RO\", \"return redis.call('SET', 'k', 'v')\", \"0\"; \"Write commands are not allowed\");\n}\n\n// ── EVALSHA_RO ───────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_evalsha_ro_read() {\n    let (_m, mut c) = start().await;\n\n    let script = \"return redis.call('GET', KEYS[1])\";\n    let sha: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(script)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"SET\", \"readonly\", \"foo\");\n\n    // Read-only should allow reads\n    must_str!(c, \"EVALSHA_RO\", &sha, \"1\", \"readonly\"; \"foo\");\n}\n\n#[tokio::test]\nasync fn test_evalsha_ro_write_blocked() {\n    let (_m, mut c) = start().await;\n\n    let write_script = \"return redis.call('SET', KEYS[1], ARGV[1])\";\n    let sha: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(write_script)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Read-only should block writes\n    must_fail!(c, \"EVALSHA_RO\", &sha, \"1\", \"key1\", \"value1\"; \"Write commands are not allowed\");\n}\n\n// ── redis.call() error cases ─────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_call_errors() {\n    let (_m, mut c) = start().await;\n\n    // redis.call() with no args\n    must_fail!(c, \"EVAL\", \"redis.call()\", \"0\"; \"Please specify at least one argument\");\n\n    // redis.call with table arg\n    must_fail!(c, \"EVAL\", \"redis.call({})\", \"0\"; \"must be strings or integers\");\n\n    // redis.call with number arg (number as command name is treated as string \"1\")\n    must_fail!(c, \"EVAL\", \"redis.call(1)\", \"0\"; \"Unknown Redis command\");\n}\n\n// ── redis.log() ──────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_eval_log() {\n    let (_m, mut c) = start().await;\n\n    // redis.log should succeed and return nil\n    must_nil!(c, \"EVAL\", \"redis.log(redis.LOG_NOTICE, 'hello')\", \"0\");\n}\n\n// ── redis.replicate_commands() / redis.set_repl() ────────────────────\n\n#[tokio::test]\nasync fn test_eval_replicate_commands() {\n    let (_m, mut c) = start().await;\n\n    // replicate_commands returns true (always enabled)\n    must_int!(c, \"EVAL\", \"return redis.replicate_commands()\", \"0\"; 1);\n}\n\n#[tokio::test]\nasync fn test_eval_set_repl() {\n    let (_m, mut c) = start().await;\n\n    // set_repl is a no-op, takes an integer argument\n    must_nil!(c, \"EVAL\", \"redis.set_repl(0)\", \"0\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_server.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── DBSIZE ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_dbsize() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"DBSIZE\"; 0);\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n    must_int!(c, \"DBSIZE\"; 2);\n\n    must_int!(c, \"DEL\", \"a\"; 1);\n    must_int!(c, \"DBSIZE\"; 1);\n}\n\n// ── FLUSHDB ─────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_flushdb() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n    must_int!(c, \"DBSIZE\"; 2);\n\n    must_ok!(c, \"FLUSHDB\");\n    must_int!(c, \"DBSIZE\"; 0);\n}\n\n// ── FLUSHALL ────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_flushall() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SELECT\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n\n    must_ok!(c, \"FLUSHALL\");\n\n    must_int!(c, \"DBSIZE\"; 0);\n    must_ok!(c, \"SELECT\", \"0\");\n    must_int!(c, \"DBSIZE\"; 0);\n}\n\n// ── TIME ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_time() {\n    let (_m, mut c) = start().await;\n\n    let v: redis::Value = redis::cmd(\"TIME\").query_async(&mut c).await.unwrap();\n\n    match v {\n        redis::Value::Array(items) => {\n            assert_eq!(items.len(), 2);\n        }\n        _ => panic!(\"expected array from TIME, got {:?}\", v),\n    }\n\n    // Too many args\n    must_fail!(c, \"TIME\", \"extra\"; \"wrong number of arguments\");\n}\n\n// ── INFO ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_info() {\n    let (_m, mut c) = start().await;\n\n    // No section → returns both clients and stats\n    let v: String = redis::cmd(\"INFO\").query_async(&mut c).await.unwrap();\n    assert!(\n        v.contains(\"# Clients\"),\n        \"expected Clients section, got: {}\",\n        v\n    );\n    assert!(\n        v.contains(\"connected_clients\"),\n        \"expected connected_clients, got: {}\",\n        v\n    );\n\n    // Specific section\n    let v: String = redis::cmd(\"INFO\")\n        .arg(\"clients\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(\n        v.contains(\"connected_clients\"),\n        \"expected connected_clients, got: {}\",\n        v\n    );\n\n    let v: String = redis::cmd(\"INFO\")\n        .arg(\"stats\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(\n        v.contains(\"total_connections_received\"),\n        \"expected total_connections_received, got: {}\",\n        v\n    );\n\n    // Invalid section\n    must_fail!(c, \"INFO\", \"bogus\"; \"not supported\");\n}\n\n// ── SWAPDB ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_swapdb() {\n    let (_m, mut c) = start().await;\n\n    // Set key in DB 0\n    must_ok!(c, \"SET\", \"key0\", \"val0\");\n    // Switch to DB 1 and set key\n    must_ok!(c, \"SELECT\", \"1\");\n    must_ok!(c, \"SET\", \"key1\", \"val1\");\n\n    // Swap DB 0 and DB 1\n    must_ok!(c, \"SWAPDB\", \"0\", \"1\");\n\n    // Now DB 1 should have key0\n    must_str!(c, \"GET\", \"key0\"; \"val0\");\n    must_nil!(c, \"GET\", \"key1\");\n\n    // Switch to DB 0, should have key1\n    must_ok!(c, \"SELECT\", \"0\");\n    must_str!(c, \"GET\", \"key1\"; \"val1\");\n    must_nil!(c, \"GET\", \"key0\");\n}\n\n#[tokio::test]\nasync fn test_swapdb_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"SWAPDB\"; \"wrong number of arguments\");\n    must_fail!(c, \"SWAPDB\", \"0\"; \"wrong number of arguments\");\n    must_fail!(c, \"SWAPDB\", \"abc\", \"0\"; \"invalid first DB index\");\n    must_fail!(c, \"SWAPDB\", \"0\", \"abc\"; \"invalid second DB index\");\n    must_fail!(c, \"SWAPDB\", \"0\", \"99\"; \"DB index is out of range\");\n    must_fail!(c, \"SWAPDB\", \"-1\", \"0\"; \"DB index is out of range\");\n}\n\n// ── MEMORY USAGE ────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_memory_usage() {\n    let (_m, mut c) = start().await;\n\n    // Non-existent key → nil\n    must_nil!(c, \"MEMORY\", \"USAGE\", \"nosuch\");\n\n    // String key\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    let v: i64 = redis::cmd(\"MEMORY\")\n        .arg(\"USAGE\")\n        .arg(\"foo\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v > 0, \"expected positive memory usage, got {}\", v);\n}\n\n#[tokio::test]\nasync fn test_memory_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"MEMORY\"; \"wrong number of arguments\");\n    must_fail!(c, \"MEMORY\", \"USAGE\"; \"wrong number of arguments\");\n    must_fail!(c, \"MEMORY\", \"BOGUS\"; \"unknown subcommand\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_set.rs",
    "content": "// Ported from ../miniredis/cmd_set_test.go\nmod helpers;\n\n#[tokio::test]\nasync fn test_sadd() {\n    let (m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SCARD\", \"s\"; 3);\n    must_int!(c, \"SADD\", \"s\", \"a\", \"b\", \"d\"; 1); // only d is new\n\n    // SMEMBERS\n    must_strs_sorted!(c, \"SMEMBERS\", \"s\"; [\"a\", \"b\", \"c\", \"d\"]);\n\n    // Non-existing\n    let members: Vec<String> = redis::cmd(\"SMEMBERS\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(members.is_empty());\n\n    // Direct API\n    assert_eq!(m.key_type(\"s\"), \"set\");\n\n    // Errors\n    must_fail!(c, \"SADD\"; \"wrong number of arguments\");\n    must_fail!(c, \"SADD\", \"s\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SADD\", \"str\", \"x\"; \"WRONGTYPE\");\n    must_fail!(c, \"SMEMBERS\", \"str\"; \"WRONGTYPE\");\n    must_fail!(c, \"SCARD\", \"str\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_sismember() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s\", \"a\", \"b\"; 2);\n\n    must_int!(c, \"SISMEMBER\", \"s\", \"a\"; 1);\n    must_int!(c, \"SISMEMBER\", \"s\", \"b\"; 1);\n    must_int!(c, \"SISMEMBER\", \"s\", \"nosuch\"; 0);\n\n    // Non-existing key\n    must_int!(c, \"SISMEMBER\", \"nosuch\", \"a\"; 0);\n\n    // Errors\n    must_fail!(c, \"SISMEMBER\"; \"wrong number of arguments\");\n    must_fail!(c, \"SISMEMBER\", \"s\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SISMEMBER\", \"str\", \"x\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_smismember() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s\", \"a\", \"b\", \"c\"; 3);\n\n    let result: Vec<i64> = redis::cmd(\"SMISMEMBER\")\n        .arg(\"s\")\n        .arg(\"a\")\n        .arg(\"x\")\n        .arg(\"c\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![1, 0, 1]);\n\n    // Non-existing key\n    let result: Vec<i64> = redis::cmd(\"SMISMEMBER\")\n        .arg(\"nosuch\")\n        .arg(\"a\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![0, 0]);\n\n    // Errors\n    must_fail!(c, \"SMISMEMBER\"; \"wrong number of arguments\");\n    must_fail!(c, \"SMISMEMBER\", \"s\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_srem() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s\", \"a\", \"b\", \"c\"; 3);\n\n    must_int!(c, \"SREM\", \"s\", \"a\", \"d\"; 1);\n    must_strs_sorted!(c, \"SMEMBERS\", \"s\"; [\"b\", \"c\"]);\n\n    // Remove from non-existing key\n    must_int!(c, \"SREM\", \"nosuch\", \"a\"; 0);\n\n    // Errors\n    must_fail!(c, \"SREM\"; \"wrong number of arguments\");\n    must_fail!(c, \"SREM\", \"s\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SREM\", \"str\", \"x\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_smove() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"src\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SADD\", \"dst\", \"x\"; 1);\n\n    must_int!(c, \"SMOVE\", \"src\", \"dst\", \"a\"; 1);\n    must_strs_sorted!(c, \"SMEMBERS\", \"src\"; [\"b\", \"c\"]);\n    must_strs_sorted!(c, \"SMEMBERS\", \"dst\"; [\"a\", \"x\"]);\n\n    // Move non-existing member\n    must_int!(c, \"SMOVE\", \"src\", \"dst\", \"nosuch\"; 0);\n\n    // Move from non-existing key\n    must_int!(c, \"SMOVE\", \"nosuch\", \"dst\", \"a\"; 0);\n\n    // Move last member\n    must_int!(c, \"SADD\", \"single\", \"x\"; 1);\n    must_int!(c, \"SMOVE\", \"single\", \"dst\", \"x\"; 1);\n\n    // Errors\n    must_fail!(c, \"SMOVE\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SMOVE\", \"str\", \"dst\", \"x\"; \"WRONGTYPE\");\n    must_fail!(c, \"SMOVE\", \"src\", \"str\", \"x\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_sdiff() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\", \"d\"; 3);\n\n    must_strs_sorted!(c, \"SDIFF\", \"s1\", \"s2\"; [\"a\"]);\n\n    // Single set\n    must_strs_sorted!(c, \"SDIFF\", \"s1\"; [\"a\", \"b\", \"c\"]);\n\n    // Three sets\n    must_int!(c, \"SADD\", \"s3\", \"a\"; 1);\n    let result: Vec<String> = redis::cmd(\"SDIFF\")\n        .arg(\"s1\")\n        .arg(\"s2\")\n        .arg(\"s3\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n\n    // Non-existing key\n    must_strs_sorted!(c, \"SDIFF\", \"s1\", \"nosuch\"; [\"a\", \"b\", \"c\"]);\n\n    // Errors\n    must_fail!(c, \"SDIFF\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_sdiffstore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\", \"d\"; 3);\n\n    must_int!(c, \"SDIFFSTORE\", \"dst\", \"s1\", \"s2\"; 1);\n    must_strs_sorted!(c, \"SMEMBERS\", \"dst\"; [\"a\"]);\n\n    // Errors\n    must_fail!(c, \"SDIFFSTORE\"; \"wrong number of arguments\");\n    must_fail!(c, \"SDIFFSTORE\", \"dst\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_sinter() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\", \"d\"; 3);\n\n    must_strs_sorted!(c, \"SINTER\", \"s1\", \"s2\"; [\"b\", \"c\"]);\n\n    // Single set\n    must_strs_sorted!(c, \"SINTER\", \"s1\"; [\"a\", \"b\", \"c\"]);\n\n    // Three sets\n    must_int!(c, \"SADD\", \"s3\", \"b\"; 1);\n    must_strs_sorted!(c, \"SINTER\", \"s1\", \"s2\", \"s3\"; [\"b\"]);\n\n    // Non-existing key → empty\n    let result: Vec<String> = redis::cmd(\"SINTER\")\n        .arg(\"s1\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n\n    // Errors\n    must_fail!(c, \"SINTER\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_sinterstore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\", \"d\"; 3);\n\n    must_int!(c, \"SINTERSTORE\", \"dst\", \"s1\", \"s2\"; 2);\n    must_strs_sorted!(c, \"SMEMBERS\", \"dst\"; [\"b\", \"c\"]);\n\n    // Empty intersection with non-existing key\n    must_int!(c, \"SINTERSTORE\", \"dst2\", \"s1\", \"nosuch\"; 0);\n\n    // Errors\n    must_fail!(c, \"SINTERSTORE\"; \"wrong number of arguments\");\n    must_fail!(c, \"SINTERSTORE\", \"dst\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_sunion() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\"; 2);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\"; 2);\n\n    must_strs_sorted!(c, \"SUNION\", \"s1\", \"s2\"; [\"a\", \"b\", \"c\"]);\n\n    // Single set\n    must_strs_sorted!(c, \"SUNION\", \"s1\"; [\"a\", \"b\"]);\n\n    // Three sets\n    must_int!(c, \"SADD\", \"s3\", \"d\"; 1);\n    must_strs_sorted!(c, \"SUNION\", \"s1\", \"s2\", \"s3\"; [\"a\", \"b\", \"c\", \"d\"]);\n\n    // Non-existing key\n    must_strs_sorted!(c, \"SUNION\", \"s1\", \"nosuch\"; [\"a\", \"b\"]);\n\n    // Errors\n    must_fail!(c, \"SUNION\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_sunionstore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\"; 2);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\"; 2);\n\n    must_int!(c, \"SUNIONSTORE\", \"dst\", \"s1\", \"s2\"; 3);\n    must_strs_sorted!(c, \"SMEMBERS\", \"dst\"; [\"a\", \"b\", \"c\"]);\n\n    // Errors\n    must_fail!(c, \"SUNIONSTORE\"; \"wrong number of arguments\");\n    must_fail!(c, \"SUNIONSTORE\", \"dst\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_scard() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SCARD\", \"nosuch\"; 0);\n    must_int!(c, \"SADD\", \"s\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SCARD\", \"s\"; 3);\n\n    // Errors\n    must_fail!(c, \"SCARD\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_set_wrongtype() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SADD\", \"str\", \"x\"; \"WRONGTYPE\");\n    must_fail!(c, \"SREM\", \"str\", \"x\"; \"WRONGTYPE\");\n    must_fail!(c, \"SCARD\", \"str\"; \"WRONGTYPE\");\n    must_fail!(c, \"SMEMBERS\", \"str\"; \"WRONGTYPE\");\n    must_fail!(c, \"SISMEMBER\", \"str\", \"x\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_spop() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s\", \"aap\", \"noot\"; 2);\n\n    // SPOP returns a member\n    let v: String = redis::cmd(\"SPOP\")\n        .arg(\"s\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v == \"aap\" || v == \"noot\");\n\n    // One left, pop it\n    let v2: String = redis::cmd(\"SPOP\")\n        .arg(\"s\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v2 == \"aap\" || v2 == \"noot\");\n    assert_ne!(v, v2);\n\n    // Key is now gone\n    must_int!(c, \"SCARD\", \"s\"; 0);\n\n    // Non-existing key\n    must_nil!(c, \"SPOP\", \"nosuch\");\n\n    // With count\n    must_int!(c, \"SADD\", \"s2\", \"a\", \"b\", \"c\", \"d\"; 4);\n    let vals: Vec<String> = redis::cmd(\"SPOP\")\n        .arg(\"s2\")\n        .arg(2)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 2);\n    must_int!(c, \"SCARD\", \"s2\"; 2);\n\n    // Errors\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SPOP\", \"str\"; \"WRONGTYPE\");\n    must_fail!(c, \"SPOP\", \"str\", \"-12\"; \"out of range\");\n}\n\n#[tokio::test]\nasync fn test_srandmember() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s\", \"aap\", \"noot\", \"mies\"; 3);\n\n    // Without count\n    let v: String = redis::cmd(\"SRANDMEMBER\")\n        .arg(\"s\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v == \"aap\" || v == \"noot\" || v == \"mies\");\n\n    // Set still has all members (non-destructive)\n    must_int!(c, \"SCARD\", \"s\"; 3);\n\n    // Positive count\n    let vals: Vec<String> = redis::cmd(\"SRANDMEMBER\")\n        .arg(\"s\")\n        .arg(2)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 2);\n    // No duplicates with positive count\n    assert_ne!(vals[0], vals[1]);\n\n    // Positive count larger than set\n    let vals: Vec<String> = redis::cmd(\"SRANDMEMBER\")\n        .arg(\"s\")\n        .arg(10)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 3);\n\n    // Negative count allows duplicates\n    let vals: Vec<String> = redis::cmd(\"SRANDMEMBER\")\n        .arg(\"s\")\n        .arg(-5)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 5);\n\n    // Non-existing key\n    must_nil!(c, \"SRANDMEMBER\", \"nosuch\");\n    let vals: Vec<String> = redis::cmd(\"SRANDMEMBER\")\n        .arg(\"nosuch\")\n        .arg(1)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(vals.is_empty());\n\n    // Errors\n    must_fail!(c, \"SRANDMEMBER\"; \"wrong number of arguments\");\n    must_fail!(c, \"SRANDMEMBER\", \"s\", \"noint\"; \"not an integer\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SRANDMEMBER\", \"str\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_sscan() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"set\", \"value1\", \"value2\"; 2);\n\n    // Basic scan\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"SSCAN\")\n        .arg(\"set\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    let mut sorted = vals.clone();\n    sorted.sort();\n    assert_eq!(sorted, vec![\"value1\", \"value2\"]);\n\n    // MATCH\n    must_int!(c, \"SADD\", \"s2\", \"aap\", \"noot\", \"mies\"; 3);\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"SSCAN\")\n        .arg(\"s2\")\n        .arg(0)\n        .arg(\"MATCH\")\n        .arg(\"mi*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    assert_eq!(vals, vec![\"mies\"]);\n\n    // COUNT (accepted but ignored in miniredis)\n    let (cursor, _vals): (String, Vec<String>) = redis::cmd(\"SSCAN\")\n        .arg(\"set\")\n        .arg(0)\n        .arg(\"COUNT\")\n        .arg(200)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n\n    // Errors\n    must_fail!(c, \"SSCAN\"; \"wrong number of arguments\");\n    must_fail!(c, \"SSCAN\", \"set\"; \"wrong number of arguments\");\n    must_fail!(c, \"SSCAN\", \"set\", \"noint\"; \"invalid cursor\");\n    must_fail!(c, \"SSCAN\", \"set\", \"0\", \"MATCH\"; \"syntax error\");\n    must_fail!(c, \"SSCAN\", \"set\", \"0\", \"COUNT\"; \"syntax error\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SSCAN\", \"str\", \"0\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_sintercard() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SADD\", \"s1\", \"a\", \"b\", \"c\"; 3);\n    must_int!(c, \"SADD\", \"s2\", \"b\", \"c\", \"d\"; 3);\n\n    // Basic\n    must_int!(c, \"SINTERCARD\", \"2\", \"s1\", \"s2\"; 2);\n\n    // With LIMIT > result\n    must_int!(c, \"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"15\"; 2);\n\n    // LIMIT 0 (unlimited)\n    must_int!(c, \"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"0\"; 2);\n\n    // LIMIT 1\n    must_int!(c, \"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"1\"; 1);\n\n    // Multi intersection\n    must_int!(c, \"SADD\", \"s3\", \"c\", \"d\", \"e\"; 3);\n    must_int!(c, \"SINTERCARD\", \"3\", \"s1\", \"s2\", \"s3\"; 1);\n\n    // Non-existing key\n    must_int!(c, \"SINTERCARD\", \"2\", \"s1\", \"NOT_A_KEY\"; 0);\n\n    // Errors\n    must_fail!(c, \"SINTERCARD\", \"two\", \"k1\", \"k2\"; \"numkeys\");\n    must_fail!(c, \"SINTERCARD\", \"2\", \"k1\", \"k2\", \"LIMIT\", \"five\"; \"not an integer\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"SINTERCARD\", \"1\", \"str\"; \"WRONGTYPE\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_sorted_set.rs",
    "content": "// Ported from ../miniredis/cmd_sorted_set_test.go\nmod helpers;\n\n#[tokio::test]\nasync fn test_zadd() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n    must_int!(c, \"ZCARD\", \"z\"; 3);\n\n    // Update existing\n    must_int!(c, \"ZADD\", \"z\", \"4\", \"a\"; 0);\n    must_int!(c, \"ZCARD\", \"z\"; 3);\n\n    // Errors\n    must_fail!(c, \"ZADD\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZADD\", \"z\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"ZADD\", \"str\", \"1\", \"a\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_zadd_nx_xx() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\"; 2);\n\n    // NX: only add new\n    must_int!(c, \"ZADD\", \"z\", \"NX\", \"10\", \"a\", \"3\", \"c\"; 1);\n    // a should still be 1\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"z\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 1.0);\n\n    // XX: only update existing\n    must_int!(c, \"ZADD\", \"z\", \"XX\", \"10\", \"a\", \"4\", \"d\"; 0);\n    // a should be updated\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"z\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 10.0);\n    // d should not exist\n    let d: Option<f64> = redis::cmd(\"ZSCORE\")\n        .arg(\"z\")\n        .arg(\"d\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(d.is_none());\n\n    // XX and NX together\n    must_fail!(c, \"ZADD\", \"z\", \"XX\", \"NX\", \"1\", \"a\"; \"XX and NX\");\n}\n\n#[tokio::test]\nasync fn test_zadd_ch() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\"; 2);\n\n    // CH: count changed (new + updated)\n    must_int!(c, \"ZADD\", \"z\", \"CH\", \"10\", \"a\", \"2\", \"b\", \"3\", \"c\"; 2);\n    // a was updated (1->10), c was new = 2 changes\n}\n\n#[tokio::test]\nasync fn test_zadd_incr() {\n    let (_m, mut c) = helpers::start().await;\n\n    // INCR mode\n    let score: String = redis::cmd(\"ZADD\")\n        .arg(\"z\")\n        .arg(\"INCR\")\n        .arg(\"5\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, \"5\");\n\n    let score: String = redis::cmd(\"ZADD\")\n        .arg(\"z\")\n        .arg(\"INCR\")\n        .arg(\"3\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, \"8\");\n}\n\n#[tokio::test]\nasync fn test_zscore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1.5\", \"a\", \"2.5\", \"b\"; 2);\n\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"z\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 1.5);\n\n    // Non-existing member\n    let score: Option<f64> = redis::cmd(\"ZSCORE\")\n        .arg(\"z\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(score.is_none());\n\n    // Non-existing key\n    must_nil!(c, \"ZSCORE\", \"nosuch\", \"a\");\n\n    // Errors\n    must_fail!(c, \"ZSCORE\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_zmscore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\"; 2);\n\n    let scores: Vec<Option<f64>> = redis::cmd(\"ZMSCORE\")\n        .arg(\"z\")\n        .arg(\"a\")\n        .arg(\"nosuch\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(scores, vec![Some(1.0), None, Some(2.0)]);\n}\n\n#[tokio::test]\nasync fn test_zincrby() {\n    let (_m, mut c) = helpers::start().await;\n\n    let score: String = redis::cmd(\"ZINCRBY\")\n        .arg(\"z\")\n        .arg(\"1.5\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, \"1.5\");\n\n    let score: String = redis::cmd(\"ZINCRBY\")\n        .arg(\"z\")\n        .arg(\"2.5\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, \"4\");\n\n    // Errors\n    must_fail!(c, \"ZINCRBY\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"ZINCRBY\", \"str\", \"1\", \"a\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_zrank() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n\n    must_int!(c, \"ZRANK\", \"z\", \"a\"; 0);\n    must_int!(c, \"ZRANK\", \"z\", \"b\"; 1);\n    must_int!(c, \"ZRANK\", \"z\", \"c\"; 2);\n\n    // Non-existing member\n    must_nil!(c, \"ZRANK\", \"z\", \"nosuch\");\n\n    // Non-existing key\n    must_nil!(c, \"ZRANK\", \"nosuch\", \"a\");\n\n    // ZREVRANK\n    must_int!(c, \"ZREVRANK\", \"z\", \"a\"; 2);\n    must_int!(c, \"ZREVRANK\", \"z\", \"c\"; 0);\n}\n\n#[tokio::test]\nasync fn test_zrem() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n\n    must_int!(c, \"ZREM\", \"z\", \"a\", \"nosuch\"; 1);\n    must_int!(c, \"ZCARD\", \"z\"; 2);\n\n    // Non-existing key\n    must_int!(c, \"ZREM\", \"nosuch\", \"a\"; 0);\n\n    // Errors\n    must_fail!(c, \"ZREM\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZREM\", \"z\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_zrange() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\", \"4\", \"d\"; 4);\n\n    must_strs!(c, \"ZRANGE\", \"z\", \"0\", \"-1\"; [\"a\", \"b\", \"c\", \"d\"]);\n    must_strs!(c, \"ZRANGE\", \"z\", \"1\", \"2\"; [\"b\", \"c\"]);\n    must_strs!(c, \"ZRANGE\", \"z\", \"0\", \"0\"; [\"a\"]);\n\n    // Empty range\n    let result: Vec<String> = redis::cmd(\"ZRANGE\")\n        .arg(\"z\")\n        .arg(\"10\")\n        .arg(\"20\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n\n    // Non-existing key\n    let result: Vec<String> = redis::cmd(\"ZRANGE\")\n        .arg(\"nosuch\")\n        .arg(\"0\")\n        .arg(\"-1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n}\n\n#[tokio::test]\nasync fn test_zrevrange() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n\n    must_strs!(c, \"ZREVRANGE\", \"z\", \"0\", \"-1\"; [\"c\", \"b\", \"a\"]);\n    must_strs!(c, \"ZREVRANGE\", \"z\", \"0\", \"1\"; [\"c\", \"b\"]);\n}\n\n#[tokio::test]\nasync fn test_zrangebyscore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\", \"4\", \"d\"; 4);\n\n    must_strs!(c, \"ZRANGEBYSCORE\", \"z\", \"-inf\", \"+inf\"; [\"a\", \"b\", \"c\", \"d\"]);\n    must_strs!(c, \"ZRANGEBYSCORE\", \"z\", \"2\", \"3\"; [\"b\", \"c\"]);\n    must_strs!(c, \"ZRANGEBYSCORE\", \"z\", \"(1\", \"3\"; [\"b\", \"c\"]);\n    must_strs!(c, \"ZRANGEBYSCORE\", \"z\", \"1\", \"(3\"; [\"a\", \"b\"]);\n\n    // LIMIT\n    must_strs!(c, \"ZRANGEBYSCORE\", \"z\", \"-inf\", \"+inf\", \"LIMIT\", \"1\", \"2\"; [\"b\", \"c\"]);\n\n    // Empty\n    let result: Vec<String> = redis::cmd(\"ZRANGEBYSCORE\")\n        .arg(\"z\")\n        .arg(\"10\")\n        .arg(\"20\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n}\n\n#[tokio::test]\nasync fn test_zrevrangebyscore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\", \"4\", \"d\"; 4);\n\n    must_strs!(c, \"ZREVRANGEBYSCORE\", \"z\", \"+inf\", \"-inf\"; [\"d\", \"c\", \"b\", \"a\"]);\n    must_strs!(c, \"ZREVRANGEBYSCORE\", \"z\", \"3\", \"2\"; [\"c\", \"b\"]);\n}\n\n#[tokio::test]\nasync fn test_zrangebylex() {\n    let (_m, mut c) = helpers::start().await;\n\n    // All same score for lex ordering\n    must_int!(c, \"ZADD\", \"z\", \"0\", \"a\", \"0\", \"b\", \"0\", \"c\", \"0\", \"d\"; 4);\n\n    must_strs!(c, \"ZRANGEBYLEX\", \"z\", \"-\", \"+\"; [\"a\", \"b\", \"c\", \"d\"]);\n    must_strs!(c, \"ZRANGEBYLEX\", \"z\", \"[b\", \"[c\"; [\"b\", \"c\"]);\n    must_strs!(c, \"ZRANGEBYLEX\", \"z\", \"(a\", \"[c\"; [\"b\", \"c\"]);\n    must_strs!(c, \"ZRANGEBYLEX\", \"z\", \"[b\", \"(d\"; [\"b\", \"c\"]);\n\n    // LIMIT\n    must_strs!(c, \"ZRANGEBYLEX\", \"z\", \"-\", \"+\", \"LIMIT\", \"1\", \"2\"; [\"b\", \"c\"]);\n}\n\n#[tokio::test]\nasync fn test_zrevrangebylex() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"0\", \"a\", \"0\", \"b\", \"0\", \"c\", \"0\", \"d\"; 4);\n\n    must_strs!(c, \"ZREVRANGEBYLEX\", \"z\", \"+\", \"-\"; [\"d\", \"c\", \"b\", \"a\"]);\n    must_strs!(c, \"ZREVRANGEBYLEX\", \"z\", \"[c\", \"[b\"; [\"c\", \"b\"]);\n}\n\n#[tokio::test]\nasync fn test_zlexcount() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"0\", \"a\", \"0\", \"b\", \"0\", \"c\", \"0\", \"d\"; 4);\n\n    must_int!(c, \"ZLEXCOUNT\", \"z\", \"-\", \"+\"; 4);\n    must_int!(c, \"ZLEXCOUNT\", \"z\", \"[b\", \"[c\"; 2);\n    must_int!(c, \"ZLEXCOUNT\", \"z\", \"(a\", \"[c\"; 2);\n\n    // Non-existing key\n    must_int!(c, \"ZLEXCOUNT\", \"nosuch\", \"-\", \"+\"; 0);\n}\n\n#[tokio::test]\nasync fn test_zcount() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n\n    must_int!(c, \"ZCOUNT\", \"z\", \"-inf\", \"+inf\"; 3);\n    must_int!(c, \"ZCOUNT\", \"z\", \"1\", \"2\"; 2);\n    must_int!(c, \"ZCOUNT\", \"z\", \"(1\", \"3\"; 2);\n\n    // Non-existing key\n    must_int!(c, \"ZCOUNT\", \"nosuch\", \"-inf\", \"+inf\"; 0);\n}\n\n#[tokio::test]\nasync fn test_zremrangebyrank() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\", \"4\", \"d\"; 4);\n\n    must_int!(c, \"ZREMRANGEBYRANK\", \"z\", \"1\", \"2\"; 2);\n    must_strs!(c, \"ZRANGE\", \"z\", \"0\", \"-1\"; [\"a\", \"d\"]);\n}\n\n#[tokio::test]\nasync fn test_zremrangebyscore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\", \"4\", \"d\"; 4);\n\n    must_int!(c, \"ZREMRANGEBYSCORE\", \"z\", \"2\", \"3\"; 2);\n    must_strs!(c, \"ZRANGE\", \"z\", \"0\", \"-1\"; [\"a\", \"d\"]);\n}\n\n#[tokio::test]\nasync fn test_zremrangebylex() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"0\", \"a\", \"0\", \"b\", \"0\", \"c\", \"0\", \"d\"; 4);\n\n    must_int!(c, \"ZREMRANGEBYLEX\", \"z\", \"[b\", \"[c\"; 2);\n    must_strs!(c, \"ZRANGE\", \"z\", \"0\", \"-1\"; [\"a\", \"d\"]);\n}\n\n#[tokio::test]\nasync fn test_zunionstore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z1\", \"1\", \"a\", \"2\", \"b\"; 2);\n    must_int!(c, \"ZADD\", \"z2\", \"3\", \"b\", \"4\", \"c\"; 2);\n\n    must_int!(c, \"ZUNIONSTORE\", \"dst\", \"2\", \"z1\", \"z2\"; 3);\n    // a=1, b=2+3=5, c=4\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"dst\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 5.0);\n\n    // Non-existing key\n    must_int!(c, \"ZUNIONSTORE\", \"dst2\", \"2\", \"z1\", \"nosuch\"; 2);\n\n    // Errors\n    must_fail!(c, \"ZUNIONSTORE\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_zinterstore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z1\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n    must_int!(c, \"ZADD\", \"z2\", \"10\", \"b\", \"20\", \"c\", \"30\", \"d\"; 3);\n\n    must_int!(c, \"ZINTERSTORE\", \"dst\", \"2\", \"z1\", \"z2\"; 2);\n    // b=2+10=12, c=3+20=23\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"dst\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 12.0);\n\n    // WEIGHTS\n    must_int!(c, \"ZINTERSTORE\", \"dst2\", \"2\", \"z1\", \"z2\", \"WEIGHTS\", \"2\", \"1\"; 2);\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"dst2\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 14.0); // 2*2 + 10*1\n\n    // AGGREGATE MIN\n    must_int!(c, \"ZINTERSTORE\", \"dst3\", \"2\", \"z1\", \"z2\", \"AGGREGATE\", \"MIN\"; 2);\n    let score: f64 = redis::cmd(\"ZSCORE\")\n        .arg(\"dst3\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(score, 2.0);\n}\n\n#[tokio::test]\nasync fn test_zpopmin_zpopmax() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"a\", \"2\", \"b\", \"3\", \"c\"; 3);\n\n    // ZPOPMIN\n    let result: Vec<String> = redis::cmd(\"ZPOPMIN\")\n        .arg(\"z\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"a\", \"1\"]);\n\n    // ZPOPMAX\n    let result: Vec<String> = redis::cmd(\"ZPOPMAX\")\n        .arg(\"z\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"c\", \"3\"]);\n\n    must_int!(c, \"ZCARD\", \"z\"; 1);\n\n    // Empty set\n    let result: Vec<String> = redis::cmd(\"ZPOPMIN\")\n        .arg(\"nosuch\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n}\n\n#[tokio::test]\nasync fn test_zrange_withscores() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1.5\", \"a\", \"2.5\", \"b\"; 2);\n\n    let result: Vec<String> = redis::cmd(\"ZRANGE\")\n        .arg(\"z\")\n        .arg(\"0\")\n        .arg(\"-1\")\n        .arg(\"WITHSCORES\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(result, vec![\"a\", \"1.5\", \"b\", \"2.5\"]);\n}\n\n#[tokio::test]\nasync fn test_sorted_set_wrongtype() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"ZADD\", \"str\", \"1\", \"a\"; \"WRONGTYPE\");\n    must_fail!(c, \"ZCARD\", \"str\"; \"WRONGTYPE\");\n    must_fail!(c, \"ZSCORE\", \"str\", \"a\"; \"WRONGTYPE\");\n    must_fail!(c, \"ZRANK\", \"str\", \"a\"; \"WRONGTYPE\");\n    must_fail!(c, \"ZREM\", \"str\", \"a\"; \"WRONGTYPE\");\n    must_fail!(c, \"ZRANGE\", \"str\", \"0\", \"-1\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_zinter() {\n    let (_m, mut c) = helpers::start().await;\n\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"h1\")\n        .arg(1)\n        .arg(\"field1\")\n        .arg(2)\n        .arg(\"field2\")\n        .arg(3)\n        .arg(\"field3\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"h2\")\n        .arg(1)\n        .arg(\"field1\")\n        .arg(2)\n        .arg(\"field2\")\n        .arg(4)\n        .arg(\"field4\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Basic intersection\n    must_strs!(c, \"ZINTER\", \"2\", \"h1\", \"h2\"; [\"field1\", \"field2\"]);\n\n    // With WITHSCORES\n    must_strs!(c, \"ZINTER\", \"2\", \"h1\", \"h2\", \"WITHSCORES\"; [\"field1\", \"2\", \"field2\", \"4\"]);\n\n    // Errors\n    must_fail!(c, \"ZINTER\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZINTER\", \"noint\", \"k\"; \"not an integer\");\n}\n\n#[tokio::test]\nasync fn test_zunion() {\n    let (_m, mut c) = helpers::start().await;\n\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"h1\")\n        .arg(1)\n        .arg(\"field1\")\n        .arg(2)\n        .arg(\"field2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"h2\")\n        .arg(1)\n        .arg(\"field1\")\n        .arg(2)\n        .arg(\"field2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Basic union\n    must_strs!(c, \"ZUNION\", \"2\", \"h1\", \"h2\"; [\"field1\", \"field2\"]);\n\n    // With WITHSCORES (sum by default)\n    must_strs!(c, \"ZUNION\", \"2\", \"h1\", \"h2\", \"WITHSCORES\"; [\"field1\", \"2\", \"field2\", \"4\"]);\n\n    // AGGREGATE MIN\n    must_strs!(c, \"ZUNION\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"MIN\", \"WITHSCORES\"; [\"field1\", \"1\", \"field2\", \"2\"]);\n\n    // Errors\n    must_fail!(c, \"ZUNION\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZUNION\", \"2\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZUNION\", \"noint\", \"k\"; \"not an integer\");\n}\n\n#[tokio::test]\nasync fn test_zrandmember() {\n    let (_m, mut c) = helpers::start().await;\n\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"z\")\n        .arg(1)\n        .arg(\"one\")\n        .arg(2)\n        .arg(\"two\")\n        .arg(3)\n        .arg(\"three\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Without count\n    let v: String = redis::cmd(\"ZRANDMEMBER\")\n        .arg(\"z\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(v == \"one\" || v == \"two\" || v == \"three\");\n\n    // Nonexistent key\n    must_nil!(c, \"ZRANDMEMBER\", \"nosuch\");\n\n    // Positive count\n    let vals: Vec<String> = redis::cmd(\"ZRANDMEMBER\")\n        .arg(\"z\")\n        .arg(2)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 2);\n\n    // Positive count larger than set\n    let vals: Vec<String> = redis::cmd(\"ZRANDMEMBER\")\n        .arg(\"z\")\n        .arg(10)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 3);\n\n    // Count 0\n    let vals: Vec<String> = redis::cmd(\"ZRANDMEMBER\")\n        .arg(\"z\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(vals.is_empty());\n\n    // Negative count\n    let vals: Vec<String> = redis::cmd(\"ZRANDMEMBER\")\n        .arg(\"z\")\n        .arg(-5)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(vals.len(), 5);\n\n    // Nonexistent key with count\n    let vals: Vec<String> = redis::cmd(\"ZRANDMEMBER\")\n        .arg(\"nosuch\")\n        .arg(40)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(vals.is_empty());\n\n    // Errors\n    must_fail!(c, \"ZRANDMEMBER\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZRANDMEMBER\", \"z\", \"noint\"; \"not an integer\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"ZRANDMEMBER\", \"str\", \"1\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_issue10_float_scores() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Regression: ZRANGEBYSCORE with exact float boundaries\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"key\")\n        .arg(3.3)\n        .arg(\"element\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    must_strs!(c, \"ZRANGEBYSCORE\", \"key\", \"3.3\", \"3.3\"; [\"element\"]);\n\n    // No match\n    let result: Vec<String> = redis::cmd(\"ZRANGEBYSCORE\")\n        .arg(\"key\")\n        .arg(\"4.3\")\n        .arg(\"4.3\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(result.is_empty());\n}\n\n#[tokio::test]\nasync fn test_zscan() {\n    let (_m, mut c) = helpers::start().await;\n\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"z\")\n        .arg(1.0)\n        .arg(\"field1\")\n        .arg(2.0)\n        .arg(\"field2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Basic scan\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"ZSCAN\")\n        .arg(\"z\")\n        .arg(0)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    assert_eq!(vals.len(), 4); // field1, 1, field2, 2\n    assert!(vals.contains(&\"field1\".to_string()));\n    assert!(vals.contains(&\"field2\".to_string()));\n\n    // COUNT (accepted but ignored)\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"ZSCAN\")\n        .arg(\"z\")\n        .arg(0)\n        .arg(\"COUNT\")\n        .arg(200)\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    assert_eq!(vals.len(), 4);\n\n    // MATCH\n    let _: i64 = redis::cmd(\"ZADD\")\n        .arg(\"z\")\n        .arg(3.0)\n        .arg(\"aap\")\n        .arg(4.0)\n        .arg(\"noot\")\n        .arg(5.0)\n        .arg(\"mies\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let (cursor, vals): (String, Vec<String>) = redis::cmd(\"ZSCAN\")\n        .arg(\"z\")\n        .arg(0)\n        .arg(\"MATCH\")\n        .arg(\"mi*\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(cursor, \"0\");\n    assert_eq!(vals, vec![\"mies\", \"5\"]);\n\n    // Errors\n    must_fail!(c, \"ZSCAN\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZSCAN\", \"z\"; \"wrong number of arguments\");\n    must_fail!(c, \"ZSCAN\", \"z\", \"noint\"; \"invalid cursor\");\n    must_fail!(c, \"ZSCAN\", \"z\", \"0\", \"MATCH\"; \"syntax error\");\n    must_fail!(c, \"ZSCAN\", \"z\", \"0\", \"COUNT\"; \"syntax error\");\n    must_ok!(c, \"SET\", \"str\", \"val\");\n    must_fail!(c, \"ZSCAN\", \"str\", \"0\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_sorted_set_infinity() {\n    let (_m, mut c) = helpers::start().await;\n\n    // Add with infinity scores\n    must_int!(c, \"ZADD\", \"zinf\", \"inf\", \"plus_inf\", \"-inf\", \"minus_inf\", \"10\", \"ten\"; 3);\n    must_int!(c, \"ZCARD\", \"zinf\"; 3);\n\n    // Check ordering: -inf, 10, +inf\n    must_strs!(c, \"ZRANGE\", \"zinf\", \"0\", \"-1\"; [\"minus_inf\", \"ten\", \"plus_inf\"]);\n}\n\n#[tokio::test]\nasync fn test_sorted_set_zrank_withscore() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"ZADD\", \"z\", \"1\", \"one\", \"2\", \"two\", \"3\", \"three\"; 3);\n\n    // ZRANK with WITHSCORE\n    let v: redis::Value = redis::cmd(\"ZRANK\")\n        .arg(\"z\")\n        .arg(\"three\")\n        .arg(\"WITHSCORE\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 2);\n            assert_eq!(items[0], redis::Value::Int(2));\n        }\n        _ => panic!(\"expected array from ZRANK WITHSCORE, got {:?}\", v),\n    }\n\n    // ZREVRANK with WITHSCORE\n    let v: redis::Value = redis::cmd(\"ZREVRANK\")\n        .arg(\"z\")\n        .arg(\"one\")\n        .arg(\"WITHSCORE\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 2);\n            assert_eq!(items[0], redis::Value::Int(2));\n        }\n        _ => panic!(\"expected array from ZREVRANK WITHSCORE, got {:?}\", v),\n    }\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_stream.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── XADD / XLEN ─────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xadd_basic() {\n    let (_m, mut c) = start().await;\n\n    // XADD with explicit ID\n    let id: String = redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-1\")\n        .arg(\"name\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(id, \"1-1\");\n\n    must_int!(c, \"XLEN\", \"s\"; 1);\n\n    // Second entry\n    let id2: String = redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"2-1\")\n        .arg(\"name\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(id2, \"2-1\");\n\n    must_int!(c, \"XLEN\", \"s\"; 2);\n\n    // TYPE\n    must_str!(c, \"TYPE\", \"s\"; \"stream\");\n}\n\n#[tokio::test]\nasync fn test_xadd_auto_id() {\n    let (_m, mut c) = start().await;\n\n    // Auto-generated ID\n    let id: String = redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"*\")\n        .arg(\"name\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(id.contains('-'), \"expected id with '-', got {}\", id);\n\n    must_int!(c, \"XLEN\", \"s\"; 1);\n}\n\n#[tokio::test]\nasync fn test_xadd_partial_id() {\n    let (_m, mut c) = start().await;\n\n    // Partial auto-sequence: \"123-*\"\n    let id: String = redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"123-*\")\n        .arg(\"name\")\n        .arg(\"a\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(\n        id.starts_with(\"123-\"),\n        \"expected id starting with '123-', got {}\",\n        id\n    );\n\n    // Second with same ms should increment seq\n    let id2: String = redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"123-*\")\n        .arg(\"name\")\n        .arg(\"b\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert!(\n        id2.starts_with(\"123-\"),\n        \"expected id starting with '123-', got {}\",\n        id2\n    );\n    assert_ne!(id, id2);\n}\n\n#[tokio::test]\nasync fn test_xadd_maxlen() {\n    let (_m, mut c) = start().await;\n\n    // Add entries with MAXLEN trimming\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(\"MAXLEN\")\n            .arg(\"3\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_int!(c, \"XLEN\", \"s\"; 3);\n}\n\n#[tokio::test]\nasync fn test_xadd_minid() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // XADD with MINID trimming\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"MINID\")\n        .arg(\"4\")\n        .arg(\"6-0\")\n        .arg(\"v\")\n        .arg(\"6\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    // Only entries >= 4-0 should remain (4-0, 5-0, 6-0)\n    must_int!(c, \"XLEN\", \"s\"; 3);\n}\n\n#[tokio::test]\nasync fn test_xadd_nomkstream() {\n    let (_m, mut c) = start().await;\n\n    // NOMKSTREAM on non-existing key should not create stream\n    let result: redis::RedisResult<Option<String>> = redis::cmd(\"XADD\")\n        .arg(\"nosuch\")\n        .arg(\"NOMKSTREAM\")\n        .arg(\"*\")\n        .arg(\"field\")\n        .arg(\"value\")\n        .query_async(&mut c)\n        .await;\n    match result {\n        Ok(None) => {} // Expected: nil response\n        Ok(Some(v)) => panic!(\"expected nil, got {:?}\", v),\n        Err(e) => panic!(\"unexpected error: {:?}\", e),\n    }\n\n    must_int!(c, \"XLEN\", \"nosuch\"; 0);\n}\n\n#[tokio::test]\nasync fn test_xadd_errors() {\n    let (_m, mut c) = start().await;\n\n    // Wrong number of args\n    must_fail!(c, \"XADD\"; \"wrong number of arguments\");\n    must_fail!(c, \"XADD\", \"s\"; \"wrong number of arguments\");\n    must_fail!(c, \"XADD\", \"s\", \"*\"; \"wrong number of arguments\");\n\n    // Odd field-value pairs\n    must_fail!(c, \"XADD\", \"s\", \"*\", \"field\"; \"wrong number of arguments\");\n\n    // Invalid ID (0-0)\n    must_fail!(c, \"XADD\", \"s\", \"0-0\", \"f\", \"v\"; \"must be greater than 0-0\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"value\");\n    must_fail!(c, \"XADD\", \"str\", \"*\", \"f\", \"v\"; \"WRONGTYPE\");\n}\n\n#[tokio::test]\nasync fn test_xadd_duplicate_id() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-1\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    // Same ID should fail\n    must_fail!(c, \"XADD\", \"s\", \"1-1\", \"f\", \"v\"; \"equal or smaller\");\n\n    // Smaller ID should also fail\n    must_fail!(c, \"XADD\", \"s\", \"1-0\", \"f\", \"v\"; \"equal or smaller\");\n}\n\n// ── XLEN ─────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xlen() {\n    let (_m, mut c) = start().await;\n\n    must_int!(c, \"XLEN\", \"nosuch\"; 0);\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-1\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_int!(c, \"XLEN\", \"s\"; 1);\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"value\");\n    must_fail!(c, \"XLEN\", \"str\"; \"WRONGTYPE\");\n}\n\n// ── XRANGE / XREVRANGE ──────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xrange() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // Full range\n    let result: redis::Value = redis::cmd(\"XRANGE\")\n        .arg(\"s\")\n        .arg(\"-\")\n        .arg(\"+\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 5);\n\n    // Partial range\n    let result: redis::Value = redis::cmd(\"XRANGE\")\n        .arg(\"s\")\n        .arg(\"2\")\n        .arg(\"4\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 3);\n\n    // With COUNT\n    let result: redis::Value = redis::cmd(\"XRANGE\")\n        .arg(\"s\")\n        .arg(\"-\")\n        .arg(\"+\")\n        .arg(\"COUNT\")\n        .arg(\"2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 2);\n}\n\n#[tokio::test]\nasync fn test_xrevrange() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // Full reverse range\n    let result: redis::Value = redis::cmd(\"XREVRANGE\")\n        .arg(\"s\")\n        .arg(\"+\")\n        .arg(\"-\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 5);\n\n    // First entry should be the highest ID\n    let first = as_array(&entries[0]);\n    let id = as_string(&first[0]);\n    assert_eq!(id, \"5-0\");\n\n    // With COUNT\n    let result: redis::Value = redis::cmd(\"XREVRANGE\")\n        .arg(\"s\")\n        .arg(\"+\")\n        .arg(\"-\")\n        .arg(\"COUNT\")\n        .arg(\"2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 2);\n}\n\n#[tokio::test]\nasync fn test_xrange_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"XRANGE\"; \"wrong number of arguments\");\n    must_fail!(c, \"XRANGE\", \"s\"; \"wrong number of arguments\");\n    must_fail!(c, \"XRANGE\", \"s\", \"-\"; \"wrong number of arguments\");\n\n    // Wrong type\n    must_ok!(c, \"SET\", \"str\", \"value\");\n    must_fail!(c, \"XRANGE\", \"str\", \"-\", \"+\"; \"WRONGTYPE\");\n}\n\n// ── XREAD ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xread() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // Read all from beginning\n    let result: redis::Value = redis::cmd(\"XREAD\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    assert_eq!(streams.len(), 1);\n\n    // stream = [name, entries]\n    let stream = as_array(&streams[0]);\n    let entries = as_array(&stream[1]);\n    assert_eq!(entries.len(), 3);\n\n    // Read from after 1-0\n    let result: redis::Value = redis::cmd(\"XREAD\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    let stream = as_array(&streams[0]);\n    let entries = as_array(&stream[1]);\n    assert_eq!(entries.len(), 2);\n}\n\n#[tokio::test]\nasync fn test_xread_count() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    let result: redis::Value = redis::cmd(\"XREAD\")\n        .arg(\"COUNT\")\n        .arg(\"2\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    let stream = as_array(&streams[0]);\n    let entries = as_array(&stream[1]);\n    assert_eq!(entries.len(), 2);\n}\n\n#[tokio::test]\nasync fn test_xread_multi_streams() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s1\")\n        .arg(\"1-0\")\n        .arg(\"v\")\n        .arg(\"1\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n    redis::cmd(\"XADD\")\n        .arg(\"s2\")\n        .arg(\"1-0\")\n        .arg(\"v\")\n        .arg(\"2\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    let result: redis::Value = redis::cmd(\"XREAD\")\n        .arg(\"STREAMS\")\n        .arg(\"s1\")\n        .arg(\"s2\")\n        .arg(\"0\")\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    assert_eq!(streams.len(), 2);\n}\n\n#[tokio::test]\nasync fn test_xread_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"XREAD\"; \"wrong number of arguments\");\n    must_fail!(c, \"XREAD\", \"STREAMS\"; \"wrong number of arguments\");\n}\n\n// ── XDEL ─────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xdel() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // Delete one entry\n    must_int!(c, \"XDEL\", \"s\", \"2-0\"; 1);\n    must_int!(c, \"XLEN\", \"s\"; 2);\n\n    // Delete already-deleted\n    must_int!(c, \"XDEL\", \"s\", \"2-0\"; 0);\n\n    // Delete non-existing key\n    must_int!(c, \"XDEL\", \"nosuch\", \"1-0\"; 0);\n}\n\n// ── XTRIM ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xtrim_maxlen() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // Trim to 3\n    must_int!(c, \"XTRIM\", \"s\", \"MAXLEN\", \"3\"; 2);\n    must_int!(c, \"XLEN\", \"s\"; 3);\n\n    // Check first remaining entry is 3-0\n    let result: redis::Value = redis::cmd(\"XRANGE\")\n        .arg(\"s\")\n        .arg(\"-\")\n        .arg(\"+\")\n        .arg(\"COUNT\")\n        .arg(\"1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    let first = as_array(&entries[0]);\n    assert_eq!(as_string(&first[0]), \"3-0\");\n}\n\n#[tokio::test]\nasync fn test_xtrim_minid() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    // Trim to MINID 3\n    must_int!(c, \"XTRIM\", \"s\", \"MINID\", \"3\"; 2);\n    must_int!(c, \"XLEN\", \"s\"; 3);\n}\n\n// ── XINFO ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xinfo_stream() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    let result: redis::Value = redis::cmd(\"XINFO\")\n        .arg(\"STREAM\")\n        .arg(\"s\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let items = as_array(&result);\n    // Should contain key-value pairs: length, groups, last-generated-id, ...\n    assert!(\n        items.len() >= 6,\n        \"expected at least 6 items, got {}\",\n        items.len()\n    );\n\n    // Find \"length\" key\n    let mut found_length = false;\n    for chunk in items.chunks(2) {\n        if as_string(&chunk[0]) == \"length\" {\n            assert_eq!(as_int(&chunk[1]), 3);\n            found_length = true;\n        }\n    }\n    assert!(found_length, \"expected 'length' field in XINFO STREAM\");\n}\n\n#[tokio::test]\nasync fn test_xinfo_groups() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    let result: redis::Value = redis::cmd(\"XINFO\")\n        .arg(\"GROUPS\")\n        .arg(\"s\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let groups = as_array(&result);\n    assert_eq!(groups.len(), 1);\n}\n\n#[tokio::test]\nasync fn test_xinfo_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"XINFO\"; \"wrong number of arguments\");\n    must_fail!(c, \"XINFO\", \"STREAM\", \"nosuch\"; \"no such key\");\n}\n\n// ── XGROUP ───────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xgroup_create() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // Duplicate group\n    must_fail!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\"; \"BUSYGROUP\");\n}\n\n#[tokio::test]\nasync fn test_xgroup_create_mkstream() {\n    let (_m, mut c) = start().await;\n\n    // Create group on non-existing stream with MKSTREAM\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"nosuch\", \"g1\", \"0\", \"MKSTREAM\");\n    must_int!(c, \"XLEN\", \"nosuch\"; 0);\n\n    // Stream exists now, even though empty\n    must_str!(c, \"TYPE\", \"nosuch\"; \"stream\");\n}\n\n#[tokio::test]\nasync fn test_xgroup_destroy() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n    must_int!(c, \"XGROUP\", \"DESTROY\", \"s\", \"g1\"; 1);\n    must_int!(c, \"XGROUP\", \"DESTROY\", \"s\", \"g1\"; 0);\n}\n\n#[tokio::test]\nasync fn test_xgroup_createconsumer() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n    must_int!(c, \"XGROUP\", \"CREATECONSUMER\", \"s\", \"g1\", \"c1\"; 1);\n    // Already exists\n    must_int!(c, \"XGROUP\", \"CREATECONSUMER\", \"s\", \"g1\", \"c1\"; 0);\n}\n\n#[tokio::test]\nasync fn test_xgroup_delconsumer() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n    must_int!(c, \"XGROUP\", \"CREATECONSUMER\", \"s\", \"g1\", \"c1\"; 1);\n    must_int!(c, \"XGROUP\", \"DELCONSUMER\", \"s\", \"g1\", \"c1\"; 0); // 0 pending\n}\n\n#[tokio::test]\nasync fn test_xgroup_errors() {\n    let (_m, mut c) = start().await;\n\n    must_fail!(c, \"XGROUP\"; \"wrong number of arguments\");\n\n    // Non-existing stream without MKSTREAM\n    must_fail!(c, \"XGROUP\", \"CREATE\", \"nosuch\", \"g1\", \"0\"; \"requires the key to exist\");\n}\n\n// ── XREADGROUP ───────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xreadgroup() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // Read new entries\n    let result: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    assert_eq!(streams.len(), 1);\n    let stream = as_array(&streams[0]);\n    let entries = as_array(&stream[1]);\n    assert_eq!(entries.len(), 3);\n}\n\n#[tokio::test]\nasync fn test_xreadgroup_count() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=5 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // Read with COUNT\n    let result: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"COUNT\")\n        .arg(\"2\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    let stream = as_array(&streams[0]);\n    let entries = as_array(&stream[1]);\n    assert_eq!(entries.len(), 2);\n}\n\n#[tokio::test]\nasync fn test_xreadgroup_redelivery() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"v\")\n        .arg(\"1\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // First read - new message\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Re-read from PEL\n    let result: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\"0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let streams = as_array(&result);\n    let stream = as_array(&streams[0]);\n    let entries = as_array(&stream[1]);\n    assert_eq!(entries.len(), 1);\n}\n\n#[tokio::test]\nasync fn test_xreadgroup_nogroup() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_fail!(c, \"XREADGROUP\", \"GROUP\", \"nosuch\", \"c1\", \"STREAMS\", \"s\", \">\"; \"NOGROUP\");\n}\n\n// ── XACK ─────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xack() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // Read all\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // ACK one\n    must_int!(c, \"XACK\", \"s\", \"g1\", \"1-0\"; 1);\n\n    // ACK same again = 0\n    must_int!(c, \"XACK\", \"s\", \"g1\", \"1-0\"; 0);\n\n    // ACK multiple\n    must_int!(c, \"XACK\", \"s\", \"g1\", \"2-0\", \"3-0\"; 2);\n}\n\n// ── XPENDING ─────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xpending_summary() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // Read all\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Summary mode\n    let result: redis::Value = redis::cmd(\"XPENDING\")\n        .arg(\"s\")\n        .arg(\"g1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let items = as_array(&result);\n    assert_eq!(items.len(), 4);\n    // First item: count\n    assert_eq!(as_int(&items[0]), 3);\n}\n\n#[tokio::test]\nasync fn test_xpending_detail() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // Detail mode\n    let result: redis::Value = redis::cmd(\"XPENDING\")\n        .arg(\"s\")\n        .arg(\"g1\")\n        .arg(\"-\")\n        .arg(\"+\")\n        .arg(\"10\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 3);\n}\n\n// ── XCLAIM ───────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xclaim() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // c1 reads all\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // c2 claims 1-0 from c1\n    let result: redis::Value = redis::cmd(\"XCLAIM\")\n        .arg(\"s\")\n        .arg(\"g1\")\n        .arg(\"c2\")\n        .arg(\"0\")\n        .arg(\"1-0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 1);\n}\n\n#[tokio::test]\nasync fn test_xclaim_justid() {\n    let (_m, mut c) = start().await;\n\n    redis::cmd(\"XADD\")\n        .arg(\"s\")\n        .arg(\"1-0\")\n        .arg(\"f\")\n        .arg(\"v\")\n        .query_async::<String>(&mut c)\n        .await\n        .unwrap();\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // JUSTID - return only IDs, not full entries\n    let result: redis::Value = redis::cmd(\"XCLAIM\")\n        .arg(\"s\")\n        .arg(\"g1\")\n        .arg(\"c2\")\n        .arg(\"0\")\n        .arg(\"1-0\")\n        .arg(\"JUSTID\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let entries = as_array(&result);\n    assert_eq!(entries.len(), 1);\n    // Should be a string (ID), not an array\n    assert!(matches!(entries[0], redis::Value::BulkString(_)));\n}\n\n// ── XAUTOCLAIM ───────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_xautoclaim() {\n    let (_m, mut c) = start().await;\n\n    for i in 1..=3 {\n        redis::cmd(\"XADD\")\n            .arg(\"s\")\n            .arg(format!(\"{}-0\", i))\n            .arg(\"v\")\n            .arg(format!(\"{}\", i))\n            .query_async::<String>(&mut c)\n            .await\n            .unwrap();\n    }\n\n    must_ok!(c, \"XGROUP\", \"CREATE\", \"s\", \"g1\", \"0\");\n\n    // c1 reads all\n    let _: redis::Value = redis::cmd(\"XREADGROUP\")\n        .arg(\"GROUP\")\n        .arg(\"g1\")\n        .arg(\"c1\")\n        .arg(\"STREAMS\")\n        .arg(\"s\")\n        .arg(\">\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    // XAUTOCLAIM with 0 min-idle-time (claims all pending)\n    let result: redis::Value = redis::cmd(\"XAUTOCLAIM\")\n        .arg(\"s\")\n        .arg(\"g1\")\n        .arg(\"c2\")\n        .arg(\"0\")\n        .arg(\"0-0\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    let items = as_array(&result);\n    assert!(\n        items.len() >= 2,\n        \"expected at least 2 items (next-id, entries), got {}\",\n        items.len()\n    );\n\n    // Second item: claimed entries\n    let entries = as_array(&items[1]);\n    assert_eq!(entries.len(), 3);\n}\n\n// ── Helper functions ─────────────────────────────────────────────────\n\nfn as_array(v: &redis::Value) -> &Vec<redis::Value> {\n    match v {\n        redis::Value::Array(a) => a,\n        _ => panic!(\"expected array, got {:?}\", v),\n    }\n}\n\nfn as_string(v: &redis::Value) -> String {\n    match v {\n        redis::Value::BulkString(b) => String::from_utf8_lossy(b).to_string(),\n        redis::Value::SimpleString(s) => s.clone(),\n        _ => panic!(\"expected string, got {:?}\", v),\n    }\n}\n\nfn as_int(v: &redis::Value) -> i64 {\n    match v {\n        redis::Value::Int(i) => *i,\n        _ => panic!(\"expected int, got {:?}\", v),\n    }\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_string.rs",
    "content": "// Ported from ../miniredis/cmd_string_test.go\nmod helpers;\nuse std::time::Duration;\n\n#[tokio::test]\nasync fn test_set() {\n    let (m, mut c) = helpers::start().await;\n\n    // Basic SET/GET\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n    m.check_get(\"foo\", \"bar\");\n\n    // Overwrite\n    must_ok!(c, \"SET\", \"foo\", \"baz\");\n    must_str!(c, \"GET\", \"foo\"; \"baz\");\n\n    // Non-existent\n    must_nil!(c, \"GET\", \"nosuch\");\n\n    // Wrong number of args\n    must_fail!(c, \"SET\"; \"wrong number of arguments\");\n    must_fail!(c, \"SET\", \"foo\"; \"wrong number of arguments\");\n    must_fail!(c, \"GET\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_set_nx() {\n    let (_m, mut c) = helpers::start().await;\n\n    // NX: set only if not exists\n    must_ok!(c, \"SET\", \"foo\", \"bar\", \"NX\");\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n\n    // Second NX should fail (return nil)\n    must_nil!(c, \"SET\", \"foo\", \"baz\", \"NX\");\n    // Value unchanged\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n}\n\n#[tokio::test]\nasync fn test_set_xx() {\n    let (_m, mut c) = helpers::start().await;\n\n    // XX: set only if exists — key doesn't exist yet\n    must_nil!(c, \"SET\", \"foo\", \"bar\", \"XX\");\n    must_nil!(c, \"GET\", \"foo\");\n\n    // Now create it\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    // XX should work now\n    must_ok!(c, \"SET\", \"foo\", \"baz\", \"XX\");\n    must_str!(c, \"GET\", \"foo\"; \"baz\");\n}\n\n#[tokio::test]\nasync fn test_set_ex() {\n    let (m, mut c) = helpers::start().await;\n\n    // SET with EX\n    must_ok!(c, \"SET\", \"foo\", \"bar\", \"EX\", \"10\");\n\n    // TTL should be set\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n    assert!(ttl.unwrap() <= Duration::from_secs(10));\n\n    // Invalid EX\n    must_fail!(c, \"SET\", \"foo\", \"bar\", \"EX\", \"0\"; \"invalid expire time\");\n    must_fail!(c, \"SET\", \"foo\", \"bar\", \"EX\", \"-1\"; \"invalid expire time\");\n    must_fail!(c, \"SET\", \"foo\", \"bar\", \"EX\", \"notanumber\"; \"not an integer\");\n}\n\n#[tokio::test]\nasync fn test_set_px() {\n    let (m, mut c) = helpers::start().await;\n\n    // SET with PX\n    must_ok!(c, \"SET\", \"foo\", \"bar\", \"PX\", \"10000\");\n\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n    assert!(ttl.unwrap() <= Duration::from_secs(10));\n\n    // Invalid PX\n    must_fail!(c, \"SET\", \"foo\", \"bar\", \"PX\", \"0\"; \"invalid expire time\");\n    must_fail!(c, \"SET\", \"foo\", \"bar\", \"PX\", \"-1\"; \"invalid expire time\");\n}\n\n#[tokio::test]\nasync fn test_set_keepttl() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\", \"EX\", \"100\");\n    // Overwrite with KEEPTTL\n    must_ok!(c, \"SET\", \"foo\", \"baz\", \"KEEPTTL\");\n\n    must_str!(c, \"GET\", \"foo\"; \"baz\");\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some(), \"TTL should be preserved\");\n}\n\n#[tokio::test]\nasync fn test_set_get() {\n    let (_m, mut c) = helpers::start().await;\n\n    // GET option — return old value\n    must_nil!(c, \"SET\", \"foo\", \"bar\", \"GET\");\n\n    must_str!(c, \"SET\", \"foo\", \"baz\", \"GET\"; \"bar\");\n    must_str!(c, \"GET\", \"foo\"; \"baz\");\n}\n\n#[tokio::test]\nasync fn test_setnx() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"SETNX\", \"foo\", \"bar\"; 1);\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n\n    must_int!(c, \"SETNX\", \"foo\", \"baz\"; 0);\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n\n    // Wrong number of args\n    must_fail!(c, \"SETNX\"; \"wrong number of arguments\");\n    must_fail!(c, \"SETNX\", \"foo\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_getset() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_nil!(c, \"GETSET\", \"foo\", \"bar\");\n    must_str!(c, \"GETSET\", \"foo\", \"baz\"; \"bar\");\n    must_str!(c, \"GET\", \"foo\"; \"baz\");\n\n    // Wrong number of args\n    must_fail!(c, \"GETSET\"; \"wrong number of arguments\");\n    must_fail!(c, \"GETSET\", \"foo\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_del() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n    must_ok!(c, \"SET\", \"c\", \"3\");\n\n    // DEL multiple\n    must_int!(c, \"DEL\", \"a\", \"b\"; 2);\n    must_nil!(c, \"GET\", \"a\");\n    must_nil!(c, \"GET\", \"b\");\n    must_str!(c, \"GET\", \"c\"; \"3\");\n\n    // DEL non-existent\n    must_int!(c, \"DEL\", \"nosuch\"; 0);\n\n    // Wrong number of args\n    must_fail!(c, \"DEL\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_exists() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"a\", \"1\");\n    must_ok!(c, \"SET\", \"b\", \"2\");\n\n    must_int!(c, \"EXISTS\", \"a\"; 1);\n    must_int!(c, \"EXISTS\", \"nosuch\"; 0);\n\n    // Multiple keys\n    must_int!(c, \"EXISTS\", \"a\", \"b\", \"nosuch\"; 2);\n\n    // Wrong number of args\n    must_fail!(c, \"EXISTS\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_setex() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SETEX\", \"foo\", \"10\", \"bar\");\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n\n    // Errors\n    must_fail!(c, \"SETEX\", \"foo\", \"0\", \"bar\"; \"invalid expire time\");\n    must_fail!(c, \"SETEX\", \"foo\", \"-1\", \"bar\"; \"invalid expire time\");\n    must_fail!(c, \"SETEX\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_psetex() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"PSETEX\", \"foo\", \"10000\", \"bar\");\n    must_str!(c, \"GET\", \"foo\"; \"bar\");\n    let ttl = m.ttl(\"foo\");\n    assert!(ttl.is_some());\n\n    must_fail!(c, \"PSETEX\", \"foo\", \"0\", \"bar\"; \"invalid expire time\");\n}\n\n#[tokio::test]\nasync fn test_incr_decr() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"INCR\", \"counter\"; 1);\n    must_int!(c, \"INCR\", \"counter\"; 2);\n    must_int!(c, \"INCRBY\", \"counter\", \"5\"; 7);\n    must_int!(c, \"DECR\", \"counter\"; 6);\n    must_int!(c, \"DECRBY\", \"counter\", \"3\"; 3);\n\n    // Non-integer value\n    must_ok!(c, \"SET\", \"str\", \"notanumber\");\n    must_fail!(c, \"INCR\", \"str\"; \"not an integer\");\n\n    // Errors\n    must_fail!(c, \"INCR\"; \"wrong number of arguments\");\n    must_fail!(c, \"INCRBY\"; \"wrong number of arguments\");\n}\n\n#[tokio::test]\nasync fn test_incrbyfloat() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_str!(c, \"INCRBYFLOAT\", \"f\", \"1.5\"; \"1.5\");\n    must_str!(c, \"INCRBYFLOAT\", \"f\", \"2.5\"; \"4\");\n    must_str!(c, \"INCRBYFLOAT\", \"f\", \"-1\"; \"3\");\n}\n\n#[tokio::test]\nasync fn test_mget_mset() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"MSET\", \"a\", \"1\", \"b\", \"2\", \"c\", \"3\");\n    must_strs!(c, \"MGET\", \"a\", \"b\", \"c\"; [\"1\", \"2\", \"3\"]);\n\n    // Missing key returns nil\n    let result: Vec<Option<String>> = redis::cmd(\"MGET\")\n        .arg(\"a\")\n        .arg(\"nosuch\")\n        .arg(\"c\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(\n        result,\n        vec![Some(\"1\".to_string()), None, Some(\"3\".to_string())]\n    );\n}\n\n#[tokio::test]\nasync fn test_msetnx() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"MSETNX\", \"a\", \"1\", \"b\", \"2\"; 1);\n    must_str!(c, \"GET\", \"a\"; \"1\");\n\n    // Any key exists → all fail\n    must_int!(c, \"MSETNX\", \"a\", \"x\", \"c\", \"3\"; 0);\n    must_str!(c, \"GET\", \"a\"; \"1\");\n    must_nil!(c, \"GET\", \"c\"); // c was NOT set\n}\n\n#[tokio::test]\nasync fn test_strlen() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"hello\");\n    must_int!(c, \"STRLEN\", \"foo\"; 5);\n    must_int!(c, \"STRLEN\", \"nosuch\"; 0);\n}\n\n#[tokio::test]\nasync fn test_append() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_int!(c, \"APPEND\", \"key\", \"hello\"; 5);\n    must_int!(c, \"APPEND\", \"key\", \" world\"; 11);\n    must_str!(c, \"GET\", \"key\"; \"hello world\");\n}\n\n#[tokio::test]\nasync fn test_getrange() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"hello world\");\n    must_str!(c, \"GETRANGE\", \"foo\", \"0\", \"4\"; \"hello\");\n    must_str!(c, \"GETRANGE\", \"foo\", \"-5\", \"-1\"; \"world\");\n    must_str!(c, \"GETRANGE\", \"foo\", \"0\", \"-1\"; \"hello world\");\n}\n\n#[tokio::test]\nasync fn test_setrange() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"hello world\");\n    must_int!(c, \"SETRANGE\", \"foo\", \"6\", \"Redis\"; 11);\n    must_str!(c, \"GET\", \"foo\"; \"hello Redis\");\n\n    // Extending\n    must_int!(c, \"SETRANGE\", \"bar\", \"5\", \"hi\"; 7);\n    // Should be zero-padded\n    let val: Vec<u8> = redis::cmd(\"GET\")\n        .arg(\"bar\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(val, vec![0, 0, 0, 0, 0, b'h', b'i']);\n}\n\n#[tokio::test]\nasync fn test_getdel() {\n    let (_m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n    must_str!(c, \"GETDEL\", \"foo\"; \"bar\");\n    must_nil!(c, \"GET\", \"foo\");\n\n    // Non-existent\n    must_nil!(c, \"GETDEL\", \"nosuch\");\n}\n\n#[tokio::test]\nasync fn test_getex() {\n    let (m, mut c) = helpers::start().await;\n\n    must_ok!(c, \"SET\", \"foo\", \"bar\");\n\n    // GETEX with EX\n    must_str!(c, \"GETEX\", \"foo\", \"EX\", \"100\"; \"bar\");\n    assert!(m.ttl(\"foo\").is_some());\n\n    // GETEX with PERSIST\n    must_str!(c, \"GETEX\", \"foo\", \"PERSIST\"; \"bar\");\n    assert!(m.ttl(\"foo\").is_none());\n\n    // Non-existent\n    must_nil!(c, \"GETEX\", \"nosuch\");\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_tls.rs",
    "content": "use std::sync::Arc;\n\nuse miniredis_rs::Miniredis;\nuse tokio::io::{AsyncReadExt, AsyncWriteExt};\n\n/// Generate a self-signed TLS certificate and return (server_config, cert_der).\nfn generate_tls_config() -> (Arc<rustls::ServerConfig>, Vec<u8>) {\n    let cert = rcgen::generate_simple_self_signed(vec![\"localhost\".to_string()]).unwrap();\n    let cert_der_bytes = cert.cert.der().to_vec();\n    let cert_der = rustls::pki_types::CertificateDer::from(cert_der_bytes.clone());\n    let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into());\n\n    let config = rustls::ServerConfig::builder()\n        .with_no_client_auth()\n        .with_single_cert(vec![cert_der], key_der)\n        .unwrap();\n    (Arc::new(config), cert_der_bytes)\n}\n\n/// Create a TLS client connector that trusts the given cert.\nfn make_tls_connector(cert_der: &[u8]) -> tokio_rustls::TlsConnector {\n    let mut root_store = rustls::RootCertStore::empty();\n    root_store\n        .add(rustls::pki_types::CertificateDer::from(cert_der.to_vec()))\n        .unwrap();\n\n    let config = rustls::ClientConfig::builder()\n        .with_root_certificates(root_store)\n        .with_no_client_auth();\n\n    tokio_rustls::TlsConnector::from(Arc::new(config))\n}\n\n/// Send a RESP2 command over a TLS stream and return the raw response.\nasync fn tls_cmd(\n    stream: &mut tokio_rustls::client::TlsStream<tokio::net::TcpStream>,\n    args: &[&str],\n) -> Vec<u8> {\n    let mut cmd = format!(\"*{}\\r\\n\", args.len());\n    for arg in args {\n        cmd.push_str(&format!(\"${}\\r\\n{}\\r\\n\", arg.len(), arg));\n    }\n    stream.write_all(cmd.as_bytes()).await.unwrap();\n    stream.flush().await.unwrap();\n\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n    let mut buf = vec![0u8; 4096];\n    let n = stream.read(&mut buf).await.unwrap();\n    buf.truncate(n);\n    buf\n}\n\n// ── Tests ───────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_tls_server_starts() {\n    let (tls_config, _) = generate_tls_config();\n    let m = Miniredis::run_tls(tls_config).await.unwrap();\n\n    assert!(m.port() > 0);\n    assert!(m.tls_url().starts_with(\"rediss://\"));\n}\n\n#[tokio::test]\nasync fn test_tls_direct_api_works() {\n    let (tls_config, _) = generate_tls_config();\n    let m = Miniredis::run_tls(tls_config).await.unwrap();\n\n    // Direct API works regardless of TLS\n    m.set(\"key\", \"value\");\n    assert_eq!(m.get(\"key\"), Some(\"value\".to_string()));\n}\n\n#[tokio::test]\nasync fn test_tls_ping() {\n    let (tls_config, cert_der) = generate_tls_config();\n    let m = Miniredis::run_tls(tls_config).await.unwrap();\n\n    let connector = make_tls_connector(&cert_der);\n    let tcp = tokio::net::TcpStream::connect(m.addr()).await.unwrap();\n    let server_name = rustls::pki_types::ServerName::try_from(\"localhost\").unwrap();\n    let mut tls = connector.connect(server_name, tcp).await.unwrap();\n\n    let resp = tls_cmd(&mut tls, &[\"PING\"]).await;\n    assert_eq!(resp, b\"+PONG\\r\\n\");\n}\n\n#[tokio::test]\nasync fn test_tls_set_get() {\n    let (tls_config, cert_der) = generate_tls_config();\n    let m = Miniredis::run_tls(tls_config).await.unwrap();\n\n    let connector = make_tls_connector(&cert_der);\n    let tcp = tokio::net::TcpStream::connect(m.addr()).await.unwrap();\n    let server_name = rustls::pki_types::ServerName::try_from(\"localhost\").unwrap();\n    let mut tls = connector.connect(server_name, tcp).await.unwrap();\n\n    let resp = tls_cmd(&mut tls, &[\"SET\", \"foo\", \"bar\"]).await;\n    assert_eq!(resp, b\"+OK\\r\\n\");\n\n    let resp = tls_cmd(&mut tls, &[\"GET\", \"foo\"]).await;\n    assert_eq!(resp, b\"$3\\r\\nbar\\r\\n\");\n\n    // Verify via direct API\n    assert_eq!(m.get(\"foo\"), Some(\"bar\".to_string()));\n}\n\n#[tokio::test]\nasync fn test_tls_multiple_commands() {\n    let (tls_config, cert_der) = generate_tls_config();\n    let m = Miniredis::run_tls(tls_config).await.unwrap();\n\n    let connector = make_tls_connector(&cert_der);\n    let tcp = tokio::net::TcpStream::connect(m.addr()).await.unwrap();\n    let server_name = rustls::pki_types::ServerName::try_from(\"localhost\").unwrap();\n    let mut tls = connector.connect(server_name, tcp).await.unwrap();\n\n    // Run several commands over TLS\n    let resp = tls_cmd(&mut tls, &[\"SET\", \"k1\", \"v1\"]).await;\n    assert_eq!(resp, b\"+OK\\r\\n\");\n\n    let resp = tls_cmd(&mut tls, &[\"SET\", \"k2\", \"v2\"]).await;\n    assert_eq!(resp, b\"+OK\\r\\n\");\n\n    let resp = tls_cmd(&mut tls, &[\"DEL\", \"k1\"]).await;\n    assert_eq!(resp, b\":1\\r\\n\");\n\n    let resp = tls_cmd(&mut tls, &[\"EXISTS\", \"k1\"]).await;\n    assert_eq!(resp, b\":0\\r\\n\");\n\n    let resp = tls_cmd(&mut tls, &[\"GET\", \"k2\"]).await;\n    assert_eq!(resp, b\"$2\\r\\nv2\\r\\n\");\n}\n\n#[tokio::test]\nasync fn test_plain_tcp_to_tls_server_fails() {\n    let (tls_config, _) = generate_tls_config();\n    let m = Miniredis::run_tls(tls_config).await.unwrap();\n\n    // Plain TCP should not get a valid response from TLS server\n    let mut stream = tokio::net::TcpStream::connect(m.addr()).await.unwrap();\n    let cmd = b\"*1\\r\\n$4\\r\\nPING\\r\\n\";\n    stream.write_all(cmd).await.unwrap();\n    stream.flush().await.unwrap();\n\n    tokio::time::sleep(std::time::Duration::from_millis(100)).await;\n\n    let mut buf = vec![0u8; 1024];\n    let result =\n        tokio::time::timeout(std::time::Duration::from_millis(200), stream.read(&mut buf)).await;\n\n    match result {\n        Ok(Ok(0)) => {} // connection closed - expected\n        Ok(Ok(n)) => {\n            // Got bytes but they shouldn't be a valid RESP response\n            let resp = &buf[..n];\n            assert_ne!(resp, b\"+PONG\\r\\n\", \"should not get PONG without TLS\");\n        }\n        Ok(Err(_)) => {} // error - expected\n        Err(_) => {}     // timeout - expected\n    }\n}\n"
  },
  {
    "path": "miniredis/tests/cmd_transactions.rs",
    "content": "mod helpers;\nuse helpers::*;\n\n// ── Error cases ──────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_exec_without_multi() {\n    let (_m, mut c) = start().await;\n    must_fail!(c, \"EXEC\"; \"EXEC without MULTI\");\n}\n\n#[tokio::test]\nasync fn test_discard_without_multi() {\n    let (_m, mut c) = start().await;\n    must_fail!(c, \"DISCARD\"; \"DISCARD without MULTI\");\n}\n\n#[tokio::test]\nasync fn test_multi_nested() {\n    let (_m, mut c) = start().await;\n\n    // First MULTI → OK\n    must_ok!(c, \"MULTI\");\n\n    // Second MULTI → error\n    must_fail!(c, \"MULTI\"; \"MULTI calls can not be nested\");\n\n    // Clean up\n    must_ok!(c, \"DISCARD\");\n}\n\n// ── Basic MULTI / EXEC ──────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_multi_basic() {\n    let (_m, mut c) = start().await;\n    must_ok!(c, \"MULTI\");\n    // Clean up\n    must_ok!(c, \"DISCARD\");\n}\n\n#[tokio::test]\nasync fn test_simple_transaction() {\n    let (_m, mut c) = start().await;\n\n    // MULTI\n    must_ok!(c, \"MULTI\");\n\n    // SET is queued\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"aap\")\n        .arg(\"1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // EXEC returns array of results\n    let v: Vec<String> = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n    assert_eq!(v, vec![\"OK\"]);\n\n    // Key should now be set\n    must_str!(c, \"GET\", \"aap\"; \"1\");\n\n    // Commands should be back to normal mode\n    must_ok!(c, \"SET\", \"aap\", \"2\");\n    must_str!(c, \"GET\", \"aap\"; \"2\");\n}\n\n#[tokio::test]\nasync fn test_multi_exec_multiple_commands() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"MULTI\");\n\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"k1\")\n        .arg(\"v1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"k2\")\n        .arg(\"v2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    let v: String = redis::cmd(\"GET\")\n        .arg(\"k1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // EXEC\n    let v: redis::Value = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n\n    // Should be Array [OK, OK, \"v1\"]\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 3);\n        }\n        _ => panic!(\"expected array from EXEC, got {:?}\", v),\n    }\n\n    // Verify\n    must_str!(c, \"GET\", \"k1\"; \"v1\");\n    must_str!(c, \"GET\", \"k2\"; \"v2\");\n}\n\n// ── DISCARD ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_discard_transaction() {\n    let (_m, mut c) = start().await;\n\n    // Pre-set a key\n    must_ok!(c, \"SET\", \"aap\", \"noot\");\n\n    // MULTI + queue a change\n    must_ok!(c, \"MULTI\");\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"aap\")\n        .arg(\"mies\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // DISCARD\n    must_ok!(c, \"DISCARD\");\n\n    // Key should still have original value\n    must_str!(c, \"GET\", \"aap\"; \"noot\");\n}\n\n// ── WATCH ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_watch_basic() {\n    let (_m, mut c) = start().await;\n    must_ok!(c, \"WATCH\", \"foo\");\n}\n\n#[tokio::test]\nasync fn test_watch_in_multi() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"MULTI\");\n\n    // WATCH inside MULTI should error\n    must_fail!(c, \"WATCH\", \"foo\"; \"WATCH inside MULTI\");\n\n    must_ok!(c, \"DISCARD\");\n}\n\n#[tokio::test]\nasync fn test_watch_exec_success() {\n    let (_m, mut c) = start().await;\n\n    // Set initial value\n    must_ok!(c, \"SET\", \"one\", \"two\");\n\n    // WATCH the key\n    must_ok!(c, \"WATCH\", \"one\");\n\n    // MULTI + GET\n    must_ok!(c, \"MULTI\");\n    let v: String = redis::cmd(\"GET\")\n        .arg(\"one\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // EXEC — no changes to watched key, so it should succeed\n    let v: Vec<String> = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n    assert_eq!(v, vec![\"two\"]);\n}\n\n#[tokio::test]\nasync fn test_watch_exec_fail() {\n    let (_m, mut c1, mut c2) = start_two_clients().await;\n\n    // Set initial value\n    must_ok!(c1, \"SET\", \"one\", \"two\");\n\n    // c1: WATCH the key\n    must_ok!(c1, \"WATCH\", \"one\");\n\n    // c2: Modify the watched key\n    must_ok!(c2, \"SET\", \"one\", \"three\");\n\n    // c1: MULTI + GET + EXEC → should return nil (WATCH abort)\n    must_ok!(c1, \"MULTI\");\n    let v: String = redis::cmd(\"GET\")\n        .arg(\"one\")\n        .query_async(&mut c1)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // EXEC should return nil because the watched key was modified\n    let v: redis::Value = redis::cmd(\"EXEC\").query_async(&mut c1).await.unwrap();\n    assert_eq!(v, redis::Value::Nil);\n\n    // We're no longer in a transaction; key has the value set by c2\n    must_str!(c1, \"GET\", \"one\"; \"three\");\n}\n\n// ── UNWATCH ──────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_unwatch() {\n    let (_m, mut c1, mut c2) = start_two_clients().await;\n\n    // Set initial value\n    must_ok!(c1, \"SET\", \"one\", \"two\");\n\n    // c1: WATCH the key\n    must_ok!(c1, \"WATCH\", \"one\");\n\n    // c1: UNWATCH — cancels the watch\n    must_ok!(c1, \"UNWATCH\");\n\n    // c2: Modify the key (would have triggered WATCH failure, but we unwatched)\n    must_ok!(c2, \"SET\", \"one\", \"three\");\n\n    // c1: MULTI + SET + EXEC → should succeed because we unwatched\n    must_ok!(c1, \"MULTI\");\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"one\")\n        .arg(\"four\")\n        .query_async(&mut c1)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    let v: Vec<String> = redis::cmd(\"EXEC\").query_async(&mut c1).await.unwrap();\n    assert_eq!(v, vec![\"OK\"]);\n\n    // Key should have the value from our transaction\n    must_str!(c1, \"GET\", \"one\"; \"four\");\n}\n\n// ── Transaction with pipe().atomic() ─────────────────────────────────\n\n#[tokio::test]\nasync fn test_pipe_atomic() {\n    let (_m, mut c) = start().await;\n\n    // Use the redis crate's built-in atomic pipe (MULTI/EXEC wrapper)\n    let (v1, v2): (String, i64) = redis::pipe()\n        .atomic()\n        .cmd(\"SET\")\n        .arg(\"k\")\n        .arg(\"hello\")\n        .cmd(\"INCR\")\n        .arg(\"counter\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n\n    assert_eq!(v1, \"OK\");\n    assert_eq!(v2, 1);\n\n    must_str!(c, \"GET\", \"k\"; \"hello\");\n    must_int!(c, \"GET\", \"counter\"; 1);\n}\n\n// ── MULTI with unknown command → EXECABORT ───────────────────────────\n\n#[tokio::test]\nasync fn test_tx_queue_err() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"MULTI\");\n\n    // Valid command\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"aap\")\n        .arg(\"mies\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // Unknown command → error and dirty transaction\n    must_fail!(c, \"NOSUCHCOMMAND\", \"arg\"; \"unknown command\");\n\n    // Another valid command still queues\n    let v: String = redis::cmd(\"SET\")\n        .arg(\"noot\")\n        .arg(\"vuur\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    // EXEC should fail with EXECABORT because of the unknown command\n    must_fail!(c, \"EXEC\"; \"Transaction discarded\");\n\n    // Nothing should have been executed\n    must_nil!(c, \"GET\", \"aap\");\n}\n\n// ── EVAL/EVALSHA inside MULTI/EXEC ───────────────────────────────────\n\n#[tokio::test]\nasync fn test_lua_tx_eval() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"MULTI\");\n\n    let v: String = redis::cmd(\"EVAL\")\n        .arg(\"return {ARGV[1]}\")\n        .arg(\"0\")\n        .arg(\"key1\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    let v: redis::Value = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 1);\n        }\n        _ => panic!(\"expected array from EXEC, got {:?}\", v),\n    }\n}\n\n#[tokio::test]\nasync fn test_lua_tx_evalsha() {\n    let (_m, mut c) = start().await;\n\n    must_ok!(c, \"MULTI\");\n\n    // SCRIPT LOAD inside MULTI\n    let v: String = redis::cmd(\"SCRIPT\")\n        .arg(\"LOAD\")\n        .arg(\"return {KEYS[1],ARGV[1]}\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    let script_sha = \"bfbf458525d6a0b19200bfd6db3af481156b367b\";\n    let v: String = redis::cmd(\"EVALSHA\")\n        .arg(script_sha)\n        .arg(\"1\")\n        .arg(\"key1\")\n        .arg(\"key2\")\n        .query_async(&mut c)\n        .await\n        .unwrap();\n    assert_eq!(v, \"QUEUED\");\n\n    let v: redis::Value = redis::cmd(\"EXEC\").query_async(&mut c).await.unwrap();\n    match v {\n        redis::Value::Array(ref items) => {\n            assert_eq!(items.len(), 2);\n        }\n        _ => panic!(\"expected array from EXEC, got {:?}\", v),\n    }\n}\n"
  },
  {
    "path": "miniredis/tests/direct_api.rs",
    "content": "use miniredis_rs::Miniredis;\n\n// ── String operations ────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_set_get() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"key\", \"value\");\n    assert_eq!(m.get(\"key\"), Some(\"value\".to_string()));\n    assert_eq!(m.get(\"nosuch\"), None);\n}\n\n#[tokio::test]\nasync fn test_direct_incr() {\n    let m = Miniredis::run().await.unwrap();\n    assert_eq!(m.incr(\"counter\", 1), 1);\n    assert_eq!(m.incr(\"counter\", 5), 6);\n    assert_eq!(m.incr(\"counter\", -2), 4);\n}\n\n#[tokio::test]\nasync fn test_direct_check_get() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"k\", \"v\");\n    m.check_get(\"k\", \"v\");\n}\n\n// ── Key management ───────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_del_exists() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"k\", \"v\");\n    assert!(m.exists(\"k\"));\n    assert!(m.del(\"k\"));\n    assert!(!m.exists(\"k\"));\n    assert!(!m.del(\"k\"));\n}\n\n#[tokio::test]\nasync fn test_direct_keys() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"b\", \"1\");\n    m.set(\"a\", \"2\");\n    m.set(\"c\", \"3\");\n    assert_eq!(m.keys(), vec![\"a\", \"b\", \"c\"]);\n}\n\n#[tokio::test]\nasync fn test_direct_key_type() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"str\", \"v\");\n    assert_eq!(m.key_type(\"str\"), \"string\");\n    assert_eq!(m.key_type(\"nosuch\"), \"none\");\n}\n\n#[tokio::test]\nasync fn test_direct_db_size() {\n    let m = Miniredis::run().await.unwrap();\n    assert_eq!(m.db_size(), 0);\n    m.set(\"k1\", \"v\");\n    m.set(\"k2\", \"v\");\n    assert_eq!(m.db_size(), 2);\n}\n\n#[tokio::test]\nasync fn test_direct_flush() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"k1\", \"v\");\n    m.set(\"k2\", \"v\");\n    m.flush_db();\n    assert_eq!(m.db_size(), 0);\n}\n\n// ── List operations ──────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_list_push_pop() {\n    let m = Miniredis::run().await.unwrap();\n    assert_eq!(m.push(\"l\", &[\"a\", \"b\", \"c\"]), 3);\n    assert_eq!(\n        m.list(\"l\"),\n        Some(vec![\"a\".to_string(), \"b\".to_string(), \"c\".to_string()])\n    );\n\n    assert_eq!(m.pop(\"l\"), Some(\"c\".to_string()));\n    assert_eq!(m.lpop(\"l\"), Some(\"a\".to_string()));\n    assert_eq!(m.list(\"l\"), Some(vec![\"b\".to_string()]));\n}\n\n#[tokio::test]\nasync fn test_direct_list_lpush() {\n    let m = Miniredis::run().await.unwrap();\n    m.lpush(\"l\", \"a\");\n    m.lpush(\"l\", \"b\");\n    assert_eq!(m.list(\"l\"), Some(vec![\"b\".to_string(), \"a\".to_string()]));\n}\n\n#[tokio::test]\nasync fn test_direct_check_list() {\n    let m = Miniredis::run().await.unwrap();\n    m.push(\"l\", &[\"x\", \"y\", \"z\"]);\n    m.check_list(\"l\", &[\"x\", \"y\", \"z\"]);\n}\n\n// ── Set operations ───────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_set_add_members() {\n    let m = Miniredis::run().await.unwrap();\n    assert_eq!(m.set_add(\"s\", &[\"a\", \"b\", \"c\"]), 3);\n    assert_eq!(m.set_add(\"s\", &[\"b\", \"d\"]), 1); // only d is new\n\n    let members = m.members(\"s\").unwrap();\n    assert_eq!(members, vec![\"a\", \"b\", \"c\", \"d\"]);\n}\n\n#[tokio::test]\nasync fn test_direct_is_member() {\n    let m = Miniredis::run().await.unwrap();\n    m.set_add(\"s\", &[\"a\", \"b\"]);\n    assert!(m.is_member(\"s\", \"a\"));\n    assert!(!m.is_member(\"s\", \"c\"));\n    assert!(!m.is_member(\"nosuch\", \"a\"));\n}\n\n#[tokio::test]\nasync fn test_direct_check_set() {\n    let m = Miniredis::run().await.unwrap();\n    m.set_add(\"s\", &[\"c\", \"a\", \"b\"]);\n    m.check_set(\"s\", &[\"a\", \"b\", \"c\"]);\n}\n\n// ── Hash operations ──────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_hash() {\n    let m = Miniredis::run().await.unwrap();\n    m.hset(\"h\", \"f1\", \"v1\");\n    m.hset(\"h\", \"f2\", \"v2\");\n\n    assert_eq!(m.hget(\"h\", \"f1\"), Some(\"v1\".to_string()));\n    assert_eq!(m.hget(\"h\", \"nosuch\"), None);\n\n    let keys = m.hkeys(\"h\").unwrap();\n    assert_eq!(keys, vec![\"f1\", \"f2\"]);\n}\n\n#[tokio::test]\nasync fn test_direct_hdel() {\n    let m = Miniredis::run().await.unwrap();\n    m.hset(\"h\", \"f1\", \"v1\");\n    assert!(m.hdel(\"h\", \"f1\"));\n    assert!(!m.hdel(\"h\", \"f1\"));\n    assert_eq!(m.hget(\"h\", \"f1\"), None);\n}\n\n// ── Sorted set operations ────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_sorted_set() {\n    let m = Miniredis::run().await.unwrap();\n    assert!(m.zadd(\"ss\", 1.0, \"a\"));\n    assert!(m.zadd(\"ss\", 3.0, \"c\"));\n    assert!(m.zadd(\"ss\", 2.0, \"b\"));\n\n    // Update existing\n    assert!(!m.zadd(\"ss\", 1.5, \"a\"));\n\n    assert_eq!(m.zscore(\"ss\", \"a\"), Some(1.5));\n    assert_eq!(m.zscore(\"ss\", \"nosuch\"), None);\n\n    let members = m.zmembers(\"ss\").unwrap();\n    assert_eq!(members, vec![\"a\", \"b\", \"c\"]); // sorted by score\n}\n\n// ── Stream operations ────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_stream() {\n    let m = Miniredis::run().await.unwrap();\n    let id = m.xadd(\"stream\", \"1-0\", &[(\"field\", \"value\")]);\n    assert_eq!(id, \"1-0\");\n    assert_eq!(m.key_type(\"stream\"), \"stream\");\n}\n\n// ── HyperLogLog operations ──────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_hll() {\n    let m = Miniredis::run().await.unwrap();\n    assert!(m.pfadd(\"hll\", &[\"a\", \"b\", \"c\"]));\n    assert!(!m.pfadd(\"hll\", &[\"a\", \"b\"])); // no new elements\n    assert_eq!(m.pfcount(\"hll\"), 3);\n}\n\n// ── TTL operations ───────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_ttl() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"k\", \"v\");\n    assert_eq!(m.ttl(\"k\"), None);\n\n    m.set_ttl(\"k\", std::time::Duration::from_secs(60));\n    assert!(m.ttl(\"k\").is_some());\n}\n\n// ── Select DB ────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_select_db() {\n    let mut m = Miniredis::run().await.unwrap();\n    m.set(\"k\", \"db0\");\n    m.select(1);\n    m.set(\"k\", \"db1\");\n    assert_eq!(m.get(\"k\"), Some(\"db1\".to_string()));\n    m.select(0);\n    assert_eq!(m.get(\"k\"), Some(\"db0\".to_string()));\n}\n\n// ── Connection counting ──────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_connection_count() {\n    let m = Miniredis::run().await.unwrap();\n\n    // Before any connections\n    assert_eq!(m.total_connection_count(), 0);\n\n    // Create a client connection\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut conn = client.get_multiplexed_async_connection().await.unwrap();\n\n    // Small delay for the connection to register\n    tokio::time::sleep(std::time::Duration::from_millis(50)).await;\n\n    assert!(m.total_connection_count() > 0);\n    assert!(m.current_connection_count() > 0);\n\n    // Do a command to confirm it works\n    let _: String = redis::cmd(\"PING\").query_async(&mut conn).await.unwrap();\n\n    drop(conn);\n}\n\n// ── Fast forward ─────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_fast_forward() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"k\", \"v\");\n    m.set_ttl(\"k\", std::time::Duration::from_secs(10));\n\n    // Key exists before expiration\n    assert!(m.exists(\"k\"));\n\n    // Fast forward past TTL\n    m.fast_forward(std::time::Duration::from_secs(11));\n\n    // Key should be gone\n    assert!(!m.exists(\"k\"));\n}\n\n// ── Auth ─────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_require_auth() {\n    let m = Miniredis::run().await.unwrap();\n    m.require_auth(\"secret\");\n\n    // Connection without auth should fail\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut conn = client.get_multiplexed_async_connection().await.unwrap();\n\n    let result: redis::RedisResult<String> = redis::cmd(\"PING\").query_async(&mut conn).await;\n    assert!(result.is_err());\n}\n\n// ── DB() access ─────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_db_access() {\n    let m = Miniredis::run().await.unwrap();\n\n    // Set key in DB 0\n    m.set(\"key0\", \"val0\");\n\n    // Set key in DB 5 via db()\n    m.db(5).set(\"key5\", \"val5\");\n\n    // Keys are isolated\n    assert_eq!(m.db(0).get(\"key0\"), Some(\"val0\".to_string()));\n    assert_eq!(m.db(0).get(\"key5\"), None);\n    assert_eq!(m.db(5).get(\"key5\"), Some(\"val5\".to_string()));\n    assert_eq!(m.db(5).get(\"key0\"), None);\n\n    // db() methods\n    assert!(m.db(5).exists(\"key5\"));\n    assert_eq!(m.db(5).key_type(\"key5\"), \"string\");\n    assert_eq!(m.db(5).db_size(), 1);\n    assert_eq!(m.db(5).keys(), vec![\"key5\".to_string()]);\n}\n\n// ── Restart ─────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_restart() {\n    let mut m = Miniredis::run().await.unwrap();\n    m.set(\"before\", \"restart\");\n\n    let old_addr = m.addr();\n    m.close().await;\n    tokio::task::yield_now().await;\n\n    m.restart().await.unwrap();\n    let new_addr = m.addr();\n\n    // Port should change (new random port)\n    assert_ne!(old_addr.port(), new_addr.port());\n\n    // Data should be preserved\n    assert_eq!(m.get(\"before\"), Some(\"restart\".to_string()));\n\n    // New connections should work\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut conn = client.get_multiplexed_async_connection().await.unwrap();\n    let v: String = redis::cmd(\"GET\")\n        .arg(\"before\")\n        .query_async(&mut conn)\n        .await\n        .unwrap();\n    assert_eq!(v, \"restart\");\n}\n\n// ── Dump ────────────────────────────────────────────────────────────\n\n#[tokio::test]\nasync fn test_direct_dump() {\n    let m = Miniredis::run().await.unwrap();\n    m.set(\"str\", \"hello\");\n    m.push(\"mylist\", &[\"a\", \"b\", \"c\"]);\n    m.set_add(\"myset\", &[\"x\", \"y\"]);\n    m.hset(\"myhash\", \"f1\", \"v1\");\n    m.zadd(\"myzset\", 1.5, \"member1\");\n\n    let dump = m.dump();\n\n    assert!(dump.contains(\"- str\\n\"), \"missing string key\");\n    assert!(dump.contains(\"\\\"hello\\\"\"), \"missing string value\");\n    assert!(dump.contains(\"- mylist\\n\"), \"missing list key\");\n    assert!(dump.contains(\"\\\"a\\\"\"), \"missing list element\");\n    assert!(dump.contains(\"- myset\\n\"), \"missing set key\");\n    assert!(dump.contains(\"- myhash\\n\"), \"missing hash key\");\n    assert!(dump.contains(\"f1:\"), \"missing hash field\");\n    assert!(dump.contains(\"- myzset\\n\"), \"missing zset key\");\n    assert!(dump.contains(\"1.5:\"), \"missing zset score\");\n}\n\n#[tokio::test]\nasync fn test_direct_db_dump() {\n    let m = Miniredis::run().await.unwrap();\n    m.db(3).set(\"k\", \"v\");\n\n    // DB 0 dump should be empty\n    let dump0 = m.db(0).dump();\n    assert!(dump0.is_empty(), \"DB 0 should be empty\");\n\n    // DB 3 dump should have the key\n    let dump3 = m.db(3).dump();\n    assert!(dump3.contains(\"- k\\n\"), \"DB 3 should have key k\");\n}\n"
  },
  {
    "path": "miniredis/tests/helpers/mod.rs",
    "content": "use miniredis_rs::Miniredis;\nuse redis::aio::MultiplexedConnection;\n\n/// Spin up a server + connected client — equivalent to Go's `runWithClient(t)`.\npub async fn start() -> (Miniredis, MultiplexedConnection) {\n    let m = Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let conn = client.get_multiplexed_async_connection().await.unwrap();\n    (m, conn)\n}\n\n/// Spin up a server + two connected clients.\n#[allow(dead_code)]\npub async fn start_two_clients() -> (Miniredis, MultiplexedConnection, MultiplexedConnection) {\n    let m = Miniredis::run().await.unwrap();\n    let c1 = redis::Client::open(m.redis_url())\n        .unwrap()\n        .get_multiplexed_async_connection()\n        .await\n        .unwrap();\n    let c2 = redis::Client::open(m.redis_url())\n        .unwrap()\n        .get_multiplexed_async_connection()\n        .await\n        .unwrap();\n    (m, c1, c2)\n}\n\n// ── Assertion macros ─────────────────────────────────────────────────\n\n/// Execute a command and assert it returns \"OK\".\n#[macro_export]\nmacro_rules! must_ok {\n    ($conn:expr, $cmd:expr $(, $arg:expr)*) => {{\n        let result: String = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await\n            .unwrap();\n        assert_eq!(result, \"OK\");\n    }};\n}\n\n/// Execute a command and assert it returns a specific string.\n#[macro_export]\nmacro_rules! must_str {\n    ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{\n        let result: String = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await\n            .unwrap();\n        assert_eq!(result, $expected);\n    }};\n}\n\n/// Execute a command and assert it returns a specific integer.\n#[macro_export]\nmacro_rules! must_int {\n    ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{\n        let result: i64 = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await\n            .unwrap();\n        assert_eq!(result, $expected as i64);\n    }};\n}\n\n/// Execute a command and assert it returns nil/null.\n#[macro_export]\nmacro_rules! must_nil {\n    ($conn:expr, $cmd:expr $(, $arg:expr)*) => {{\n        let result: Option<String> = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await\n            .unwrap();\n        assert_eq!(result, None);\n    }};\n}\n\n/// Execute a command and assert it returns an error containing the given substring.\n#[macro_export]\nmacro_rules! must_fail {\n    ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{\n        let result: redis::RedisResult<redis::Value> = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await;\n        match result {\n            Err(e) => {\n                let msg = e.to_string();\n                assert!(\n                    msg.contains($expected),\n                    \"error {msg:?} does not contain {:?}\",\n                    $expected,\n                );\n            }\n            Ok(v) => {\n                panic!(\n                    \"expected error containing {:?}, got {:?}\",\n                    $expected,\n                    v,\n                );\n            }\n        }\n    }};\n}\n\n/// Execute a command and assert it returns a list of strings (sorted before comparison).\n#[macro_export]\nmacro_rules! must_strs_sorted {\n    ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{\n        let mut result: Vec<String> = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await\n            .unwrap();\n        result.sort();\n        let mut expected: Vec<String> = $expected.iter().map(|s: &&str| s.to_string()).collect();\n        expected.sort();\n        assert_eq!(result, expected);\n    }};\n}\n\n/// Execute a command and assert it returns a list of strings (order preserved).\n#[macro_export]\nmacro_rules! must_strs {\n    ($conn:expr, $cmd:expr $(, $arg:expr)* ; $expected:expr) => {{\n        let result: Vec<String> = redis::cmd($cmd)\n            $(.arg($arg))*\n            .query_async(&mut $conn)\n            .await\n            .unwrap();\n        let expected: Vec<String> = $expected.iter().map(|s: &&str| s.to_string()).collect();\n        assert_eq!(result, expected);\n    }};\n}\n\n/// Execute a command and assert boolean result (1 = true, 0 = false).\n#[macro_export]\nmacro_rules! must_1 {\n    ($conn:expr, $cmd:expr $(, $arg:expr)*) => {\n        must_int!($conn, $cmd $(, $arg)* ; 1);\n    };\n}\n\n#[macro_export]\nmacro_rules! must_0 {\n    ($conn:expr, $cmd:expr $(, $arg:expr)*) => {\n        must_int!($conn, $cmd $(, $arg)* ; 0);\n    };\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/Makefile",
    "content": "MINIREDIS_RS_BIN = ../../../target/release/miniredis-rs-server\nTESTDATA = ../../testdata\n\n.PHONY: test build\n\ntest: build $(TESTDATA)/ca.crt $(TESTDATA)/server.crt $(TESTDATA)/client.crt\n\tPATH=\"$(dir $(abspath $(MINIREDIS_RS_BIN))):$(PATH)\" INT=1 go test -count=1 -v -timeout 120s ./...\n\nbuild:\n\tcd ../../.. && cargo build --release --features tls --bin miniredis-rs-server\n\n# Generate test TLS certificates if they don't exist.\n$(TESTDATA)/ca.key:\n\topenssl genrsa -out $@ 2048\n\n$(TESTDATA)/ca.crt: $(TESTDATA)/ca.key\n\topenssl req -new -x509 -key $< -out $@ -days 3650 -subj \"/CN=Test CA\" \\\n\t\t-addext \"basicConstraints=critical,CA:TRUE\"\n\n$(TESTDATA)/server.key:\n\topenssl genrsa -out $@ 2048\n\n$(TESTDATA)/server.crt: $(TESTDATA)/server.key $(TESTDATA)/ca.crt $(TESTDATA)/ca.key\n\topenssl req -new -key $(TESTDATA)/server.key -subj \"/CN=Server\" \\\n\t\t-addext \"subjectAltName=DNS:Server\" | \\\n\topenssl x509 -req -CA $(TESTDATA)/ca.crt -CAkey $(TESTDATA)/ca.key \\\n\t\t-CAcreateserial -out $@ -days 3650 \\\n\t\t-copy_extensions copyall\n\n$(TESTDATA)/client.key:\n\topenssl genrsa -out $@ 2048\n\n$(TESTDATA)/client.crt: $(TESTDATA)/client.key $(TESTDATA)/ca.crt $(TESTDATA)/ca.key\n\topenssl req -new -key $(TESTDATA)/client.key -subj \"/CN=Client\" | \\\n\topenssl x509 -req -CA $(TESTDATA)/ca.crt -CAkey $(TESTDATA)/ca.key \\\n\t\t-CAcreateserial -out $@ -days 3650\n"
  },
  {
    "path": "miniredis/tests/integration-go/README.md",
    "content": "# Integration Tests\n\nThese are the integration tests from the original [miniredis](https://github.com/alicebob/miniredis) Go implementation (`integration/`), adapted to compare miniredis-rs against miniredis-go at the RESP byte level.\n\n## How it works\n\nEach test starts two servers — miniredis-rs (as a subprocess) and miniredis-go (in-process) — then runs identical Redis commands against both and compares the raw RESP responses.\n\n## Running\n\n```bash\nmake test\n```\n\nThis builds `miniredis-rs-server` (with TLS) and runs the Go tests with `INT=1` set. Tests are skipped without `INT=1`.\n\n## Changes from upstream\n\nThe test files (`*_test.go`) and the comparison framework (`test.go`) are kept identical to upstream, with the following exceptions:\n\n### `ephemeral.go`\n\nRewritten to start `miniredis-rs-server` as a subprocess (instead of `redis-server`). Config is passed via stdin; the server prints `PORT=<n>` to stdout when ready.\n\n### `tls.go` and `testdata/`\n\nThe test certificates use a proper CA hierarchy because rustls's `WebPkiClientVerifier` rejects self-signed `CA:TRUE` certificates used as end-entity certs (which the upstream certs are). The upstream certs work with Go's `crypto/tls` but not with rustls.\n\n- `ca.crt` / `ca.key` — self-signed CA\n- `server.crt` / `server.key` — server cert signed by CA (`SAN: DNS:Server`)\n- `client.crt` / `client.key` — client cert signed by CA\n\nBoth sides use `ca.crt` as the trust root.\n\n### `generic_test.go`\n\n`TestFastForward` uses `c.real.Do(\"MINIREDIS.FASTFORWARD\", \"200\")` instead of `time.Sleep(200ms)`. miniredis-rs (like miniredis-go) uses mock time for TTLs, so wall-clock sleep doesn't expire keys. The custom `MINIREDIS.FASTFORWARD <ms>` command advances mock time on the subprocess, matching what `c.miniredis.FastForward()` does for miniredis-go in-process.\n"
  },
  {
    "path": "miniredis/tests/integration-go/cluster_test.go",
    "content": "package main\n\nimport \"testing\"\n\nfunc TestCluster(t *testing.T) {\n\tskip(t)\n\ttestCluster(t,\n\t\tfunc(c *client) {\n\t\t\t// c.DoLoosly(\"CLUSTER\", \"SLOTS\")\n\t\t\tc.DoLoosely(\"CLUSTER\", \"KEYSLOT\", \"{test}\")\n\t\t\tc.DoLoosely(\"CLUSTER\", \"NODES\")\n\t\t\tc.Error(\"wrong number\", \"CLUSTER\")\n\t\t\t// c.DoLoosely(\"CLUSTER\", \"SHARDS\")\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/command_test.go",
    "content": "package main\n\nimport \"testing\"\n\nfunc TestCommand(t *testing.T) {\n\tt.Skip(\"not sure about this one yet\")\n\ttestRaw(t, func(c *client) {\n\t\tc.DoLoosely(\"COMMAND\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/connection_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestEcho(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ECHO\", \"hello world\")\n\t\tc.Do(\"ECHO\", \"42\")\n\t\tc.Do(\"ECHO\", \"3.1415\")\n\t\tc.Error(\"wrong number\", \"ECHO\", \"hello\", \"world\")\n\t\tc.Error(\"wrong number\", \"ECHO\")\n\t\tc.Error(\"wrong number\", \"eChO\", \"hello\", \"world\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"ECHO\", \"hi\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"wrong number\", \"ECHO\")\n\t\tc.Error(\"discarded\", \"EXEC\")\n\t})\n}\n\nfunc TestPing(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"PING\")\n\t\tc.Do(\"PING\", \"hello world\")\n\t\tc.Error(\"wrong number\", \"PING\", \"hello\", \"world\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"PING\", \"hi\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"PING\", \"hi again\")\n\t\tc.Do(\"EXEC\")\n\t})\n}\n\nfunc TestSelect(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SET\", \"foo\", \"bar2\")\n\t\tc.Do(\"GET\", \"foo\")\n\n\t\tc.Error(\"wrong number\", \"SELECT\")\n\t\tc.Error(\"out of range\", \"SELECT\", \"-1\")\n\t\tc.Error(\"not an integer\", \"SELECT\", \"aap\")\n\t\tc.Error(\"wrong number\", \"SELECT\", \"1\", \"2\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SET\", \"foo\", \"bar2\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"GET\", \"foo\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SELECT\", \"-1\")\n\t\tc.Do(\"EXEC\")\n\t})\n}\n\nfunc TestAuth(t *testing.T) {\n\tskip(t)\n\ttestAuth(t,\n\t\t\"supersecret\",\n\t\tfunc(c *client) {\n\t\t\tc.Error(\"Authentication required\", \"PING\")\n\t\t\tc.Error(\"Authentication required\", \"SET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"wrong number\", \"SET\")\n\t\t\tc.Error(\"Authentication required\", \"SET\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"Authentication required\", \"GET\", \"foo\")\n\t\t\tc.Error(\"wrong number\", \"AUTH\")\n\t\t\tc.Error(\"invalid\", \"AUTH\", \"nosecret\")\n\t\t\tc.Error(\"invalid\", \"AUTH\", \"nosecret\", \"bar\")\n\t\t\tc.Error(\"syntax error\", \"AUTH\", \"nosecret\", \"bar\", \"bar\")\n\t\t\tc.Do(\"AUTH\", \"supersecret\")\n\t\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\t\tc.Do(\"GET\", \"foo\")\n\t\t},\n\t)\n\n\ttestUserAuth(t,\n\t\tmap[string]string{\n\t\t\t\"agent1\": \"supersecret\",\n\t\t\t\"agent2\": \"dragon\",\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Error(\"Authentication required\", \"PING\")\n\t\t\tc.Error(\"Authentication required\", \"SET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"wrong number\", \"SET\")\n\t\t\tc.Error(\"Authentication required\", \"SET\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"Authentication required\", \"GET\", \"foo\")\n\t\t\tc.Error(\"wrong number\", \"AUTH\")\n\t\t\tc.Error(\"invalid\", \"AUTH\", \"nosecret\")\n\t\t\tc.Error(\"invalid\", \"AUTH\", \"agent100\", \"supersecret\")\n\t\t\tc.Error(\"syntax error\", \"AUTH\", \"agent100\", \"supersecret\", \"supersecret\")\n\t\t\tc.Error(\"invalid\", \"AUTH\", \"agent1\", \"bzzzt\")\n\t\t\tc.Do(\"AUTH\", \"agent1\", \"supersecret\")\n\t\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\t\tc.Do(\"GET\", \"foo\")\n\n\t\t\t// go back to invalid user\n\t\t\tc.Error(\"invalid\", \"AUTH\", \"agent100\", \"supersecret\")\n\t\t\tc.Do(\"GET\", \"foo\") // still agent1\n\t\t},\n\t)\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"wrong number\", \"AUTH\")\n\t\tc.Error(\"without any\", \"AUTH\", \"foo\")\n\t\tc.Error(\"invalid\", \"AUTH\", \"foo\", \"bar\")\n\t\tc.Error(\"syntax error\", \"AUTH\", \"foo\", \"bar\", \"bar\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"AUTH\", \"apassword\")\n\t\tc.Do(\"EXEC\")\n\t})\n}\n\nfunc TestHello(t *testing.T) {\n\tskip(t)\n\ttestRaw(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"SADD\", \"s\", \"aap\") // sets have resp3 specific code\n\n\t\t\tc.DoLoosely(\"HELLO\", \"3\")\n\t\t\tc.Do(\"SMEMBERS\", \"s\")\n\n\t\t\tc.DoLoosely(\"HELLO\", \"2\")\n\t\t\tc.Do(\"SMEMBERS\", \"s\")\n\n\t\t\tc.Error(\"not an integer\", \"HELLO\", \"twoandahalf\")\n\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"AUTH\", \"default\", \"foo\")\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"AUTH\", \"default\", \"foo\", \"SETNAME\", \"foo\")\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"SETNAME\", \"foo\")\n\n\t\t\t// errors\n\t\t\tc.Error(\"Syntax error\", \"HELLO\", \"3\", \"default\", \"foo\")\n\t\t\tc.Error(\"not an integer\", \"HELLO\", \"three\", \"AUTH\", \"default\", \"foo\")\n\t\t\tc.Error(\"Syntax error\", \"HELLO\", \"3\", \"AUTH\", \"default\")\n\t\t\tc.Error(\"unsupported\", \"HELLO\", \"-1\", \"foo\")\n\t\t\tc.Error(\"unsupported\", \"HELLO\", \"0\", \"foo\")\n\t\t\tc.Error(\"unsupported\", \"HELLO\", \"1\", \"foo\")\n\t\t\tc.Error(\"unsupported\", \"HELLO\", \"4\", \"foo\")\n\t\t\tc.Error(\"Syntax error\", \"HELLO\", \"3\", \"default\", \"foo\", \"SETNAME\")\n\t\t\tc.Error(\"Syntax error\", \"HELLO\", \"3\", \"SETNAME\")\n\n\t\t},\n\t)\n\n\ttestAuth(t,\n\t\t\"secret\",\n\t\tfunc(c *client) {\n\t\t\tc.Error(\"Authentication required\", \"SADD\", \"s\", \"aap\") // sets have resp3 specific code\n\n\t\t\tc.Error(\"invalid\", \"HELLO\", \"3\", \"AUTH\", \"default\", \"foo\")\n\t\t\tc.Error(\"invalid\", \"HELLO\", \"3\", \"AUTH\", \"wrong\", \"secret\")\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"AUTH\", \"default\", \"secret\")\n\t\t\tc.Do(\"SMEMBERS\", \"s\")\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"AUTH\", \"default\", \"secret\") // again!\n\t\t\tc.Do(\"SMEMBERS\", \"s\")\n\t\t\tc.DoLoosely(\"HELLO\", \"2\", \"AUTH\", \"default\", \"secret\") // again!\n\t\t\tc.Do(\"SMEMBERS\", \"s\")\n\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"AUTH\", \"default\", \"wrong\")\n\t\t\tc.Do(\"SMEMBERS\", \"s\")\n\t\t},\n\t)\n\n\ttestUserAuth(t,\n\t\tmap[string]string{\n\t\t\t\"sesame\": \"open\",\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Error(\"Authentication required\", \"SADD\", \"s\", \"aap\") // sets have resp3 specific code\n\n\t\t\tc.Error(\"invalid\", \"HELLO\", \"3\", \"AUTH\", \"foo\", \"bar\")\n\t\t\tc.Error(\"invalid\", \"HELLO\", \"3\", \"AUTH\", \"sesame\", \"close\")\n\t\t\tc.Error(\"Authentication required\", \"SMEMBERS\", \"s\")\n\t\t\tc.DoLoosely(\"HELLO\", \"3\", \"AUTH\", \"sesame\", \"open123\")\n\t\t\tc.Error(\"Authentication required\", \"SMEMBERS\", \"s\")\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/ephemeral.go",
    "content": "package main\n\n// Start a miniredis-rs server on a random port.\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\nconst executable = \"miniredis-rs-server\"\n\ntype ephemeral exec.Cmd\n\n// Redis starts a miniredis-rs on a random port. Will panic if that\n// doesn't work.\n// Returns something which you'll have to Close(), and a string to give to Dial()\nfunc Redis() (*ephemeral, string) {\n\treturn runRedis(\"\")\n}\n\n// RedisAuth starts a miniredis-rs on a random port with authentication enabled.\nfunc RedisAuth(passwd string) (*ephemeral, string) {\n\treturn runRedis(fmt.Sprintf(\"requirepass %s\", passwd))\n}\n\n// RedisUserAuth starts a miniredis-rs on a random port with ACL rules enabled.\nfunc RedisUserAuth(users map[string]string) (*ephemeral, string) {\n\tacls := \"user default on -@all +hello\\n\"\n\tfor user, pass := range users {\n\t\tacls += fmt.Sprintf(\"user %s on +@all ~* >%s\\n\", user, pass)\n\t}\n\treturn runRedis(acls)\n}\n\n// RedisCluster starts a miniredis-rs on a random port in cluster mode.\nfunc RedisCluster() (*ephemeral, string) {\n\treturn runRedis(\"cluster-enabled yes\\ncluster-config-file nodes.conf\")\n}\n\nfunc RedisTLS() (*ephemeral, string) {\n\treturn runRedis(`\n\t\ttls-port 0\n\t\ttls-cert-file ../../testdata/server.crt\n\t\ttls-key-file ../../testdata/server.key\n\t\ttls-ca-cert-file ../../testdata/ca.crt\n\t`)\n}\n\nfunc runRedis(extraConfig string) (*ephemeral, string) {\n\tc := exec.Command(executable, \"-\")\n\tstdin, err := c.StdinPipe()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstdout, err := c.StdoutPipe()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tc.Stderr = nil // inherit stderr for debugging\n\n\tif err := c.Start(); err != nil {\n\t\tpanic(fmt.Sprintf(\"starting %s: %s\", executable, err))\n\t}\n\n\t// Send config and close stdin so the server knows config is complete.\n\tfmt.Fprintf(stdin, \"port 0\\nbind 127.0.0.1\\nappendonly no\\n%s\", extraConfig)\n\tstdin.Close()\n\n\t// Read the PORT=<n> readiness line from stdout.\n\tscanner := bufio.NewScanner(stdout)\n\tif !scanner.Scan() {\n\t\tc.Process.Kill()\n\t\tc.Wait()\n\t\tpanic(\"miniredis-rs-server: no readiness line on stdout\")\n\t}\n\tline := scanner.Text()\n\tif !strings.HasPrefix(line, \"PORT=\") {\n\t\tc.Process.Kill()\n\t\tc.Wait()\n\t\tpanic(fmt.Sprintf(\"miniredis-rs-server: unexpected output: %q\", line))\n\t}\n\tport := strings.TrimPrefix(line, \"PORT=\")\n\taddr := fmt.Sprintf(\"127.0.0.1:%s\", port)\n\n\te := ephemeral(*c)\n\treturn &e, addr\n}\n\nfunc (e *ephemeral) Close() {\n\t((*exec.Cmd)(e)).Process.Kill()\n\t((*exec.Cmd)(e)).Wait()\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/generic_test.go",
    "content": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestKeys(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"one\", \"1\")\n\t\tc.Do(\"SET\", \"two\", \"2\")\n\t\tc.Do(\"SET\", \"three\", \"3\")\n\t\tc.Do(\"SET\", \"four\", \"4\")\n\t\tc.DoSorted(\"KEYS\", `*o*`)\n\t\tc.DoSorted(\"KEYS\", `t??`)\n\t\tc.DoSorted(\"KEYS\", `t?*`)\n\t\tc.DoSorted(\"KEYS\", `*`)\n\t\tc.DoSorted(\"KEYS\", `t*`)\n\t\tc.DoSorted(\"KEYS\", `t\\*`)\n\t\tc.DoSorted(\"KEYS\", `[tf]*`)\n\n\t\t// zero length key\n\t\tc.Do(\"SET\", \"\", \"nothing\")\n\t\tc.Do(\"GET\", \"\")\n\n\t\t// Simple failure cases\n\t\tc.Error(\"wrong number\", \"KEYS\")\n\t\tc.Error(\"wrong number\", \"KEYS\", \"foo\", \"bar\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"[one]\", \"1\")\n\t\tc.Do(\"SET\", \"two\", \"2\")\n\t\tc.DoSorted(\"KEYS\", `[\\[o]*`)\n\t\tc.DoSorted(\"KEYS\", `\\[*`)\n\t\tc.DoSorted(\"KEYS\", `*o*`)\n\t\tc.DoSorted(\"KEYS\", `[]*`) // nothing\n\t})\n}\n\nfunc TestRandom(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RANDOMKEY\")\n\t\t// A random key from a DB with a single key. We can test that.\n\t\tc.Do(\"SET\", \"one\", \"1\")\n\t\tc.Do(\"RANDOMKEY\")\n\n\t\t// Simple failure cases\n\t\tc.Error(\"wrong number\", \"RANDOMKEY\", \"bar\")\n\t})\n}\n\nfunc TestUnknownCommand(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"unknown\", \"nosuch\")\n\t\tc.Error(\"unknown\", \"noSUCH\")\n\t\tc.Error(\"unknown\", \"noSUCH\", \"1\", \"2\", \"3\")\n\t})\n}\n\nfunc TestQuit(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"QUIT\")\n\t})\n}\n\nfunc TestExists(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"a\", \"3\")\n\t\tc.Do(\"HSET\", \"b\", \"c\", \"d\")\n\t\tc.Do(\"EXISTS\", \"a\", \"b\")\n\t\tc.Do(\"EXISTS\", \"a\", \"b\", \"q\")\n\t\tc.Do(\"EXISTS\", \"a\", \"b\", \"b\", \"b\", \"a\", \"q\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"EXISTS\")\n\t})\n}\n\nfunc TestRename(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// No 'a' key\n\t\tc.Error(\"no such\", \"RENAME\", \"a\", \"b\")\n\n\t\t// Move a key with the TTL.\n\t\tc.Do(\"SET\", \"a\", \"3\")\n\t\tc.Do(\"EXPIRE\", \"a\", \"123\")\n\t\tc.Do(\"SET\", \"b\", \"12\")\n\t\tc.Do(\"RENAME\", \"a\", \"b\")\n\t\tc.Do(\"EXISTS\", \"a\")\n\t\tc.Do(\"GET\", \"a\")\n\t\tc.Do(\"TYPE\", \"a\")\n\t\tc.Do(\"TTL\", \"a\")\n\t\tc.Do(\"EXISTS\", \"b\")\n\t\tc.Do(\"GET\", \"b\")\n\t\tc.Do(\"TYPE\", \"b\")\n\t\tc.Do(\"TTL\", \"b\")\n\n\t\t// move a key without TTL\n\t\tc.Do(\"SET\", \"nottl\", \"3\")\n\t\tc.Do(\"RENAME\", \"nottl\", \"stillnottl\")\n\t\tc.Do(\"TTL\", \"nottl\")\n\t\tc.Do(\"TTL\", \"stillnottl\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"RENAME\")\n\t\tc.Error(\"wrong number\", \"RENAME\", \"a\")\n\t\tc.Error(\"wrong number\", \"RENAME\", \"a\", \"b\", \"toomany\")\n\t})\n}\n\nfunc TestRenamenx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// No 'a' key\n\t\tc.Error(\"no such\", \"RENAMENX\", \"a\", \"b\")\n\n\t\tc.Do(\"SET\", \"a\", \"value\")\n\t\tc.Do(\"SET\", \"str\", \"value\")\n\t\tc.Do(\"RENAMENX\", \"a\", \"str\")\n\t\tc.Do(\"EXISTS\", \"a\")\n\t\tc.Do(\"EXISTS\", \"str\")\n\t\tc.Do(\"GET\", \"a\")\n\t\tc.Do(\"GET\", \"str\")\n\n\t\tc.Do(\"RENAMENX\", \"a\", \"nosuch\")\n\t\tc.Do(\"EXISTS\", \"a\")\n\t\tc.Do(\"EXISTS\", \"nosuch\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"RENAMENX\")\n\t\tc.Error(\"wrong number\", \"RENAMENX\", \"a\")\n\t\tc.Error(\"wrong number\", \"RENAMENX\", \"a\", \"b\", \"toomany\")\n\t})\n}\n\nfunc TestScan(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// No keys yet\n\t\tc.Do(\"SCAN\", \"0\")\n\n\t\tc.Do(\"SET\", \"key\", \"value\")\n\t\tc.Do(\"SCAN\", \"0\")\n\t\tc.Do(\"SCAN\", \"0\", \"COUNT\", \"12\")\n\t\tc.Do(\"SCAN\", \"0\", \"cOuNt\", \"12\")\n\n\t\tc.Do(\"SET\", \"anotherkey\", \"value\")\n\t\tc.Do(\"SCAN\", \"0\", \"MATCH\", \"anoth*\")\n\t\tc.Do(\"SCAN\", \"0\", \"MATCH\", \"anoth*\", \"COUNT\", \"100\")\n\t\tc.Do(\"SCAN\", \"0\", \"COUNT\", \"100\", \"MATCH\", \"anoth*\")\n\n\t\tc.Do(\"SADD\", \"setkey\", \"setitem\")\n\t\tc.Do(\"SCAN\", \"0\", \"TYPE\", \"set\")\n\t\tc.Do(\"SCAN\", \"0\", \"tYpE\", \"sEt\")\n\t\tc.Do(\"SCAN\", \"0\", \"TYPE\", \"not-a-type\")\n\t\tc.Do(\"SCAN\", \"0\", \"TYPE\", \"set\", \"MATCH\", \"setkey\")\n\t\tc.Do(\"SCAN\", \"0\", \"TYPE\", \"set\", \"COUNT\", \"100\")\n\t\tc.Do(\"SCAN\", \"0\", \"TYPE\", \"set\", \"MATCH\", \"setkey\", \"COUNT\", \"100\")\n\n\t\t// SCAN may return a higher count of items than requested (See https://redis.io/docs/manual/keyspace/), so we must query all items.\n\t\tc.DoLoosely(\"SCAN\", \"0\", \"COUNT\", \"100\") // cursor differs\n\n\t\t// Can't really test multiple keys.\n\t\t// c.Do(\"SET\", \"key2\", \"value2\")\n\t\t// c.Do(\"SCAN\", \"0\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"SCAN\")\n\t\tc.Error(\"invalid cursor\", \"SCAN\", \"noint\")\n\t\tc.Error(\"not an integer\", \"SCAN\", \"0\", \"COUNT\", \"noint\")\n\t\tc.Error(\"syntax error\", \"SCAN\", \"0\", \"COUNT\", \"0\")\n\t\tc.Error(\"syntax error\", \"SCAN\", \"0\", \"COUNT\")\n\t\tc.Error(\"syntax error\", \"SCAN\", \"0\", \"MATCH\")\n\t\tc.Error(\"syntax error\", \"SCAN\", \"0\", \"garbage\")\n\t\tc.Error(\"syntax error\", \"SCAN\", \"0\", \"COUNT\", \"12\", \"MATCH\", \"foo\", \"garbage\")\n\t\tc.Error(\"syntax error\", \"SCAN\", \"0\", \"TYPE\")\n\t})\n}\n\nfunc TestFastForward(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"key1\", \"value\")\n\t\tc.Do(\"SET\", \"key\", \"value\", \"PX\", \"100\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t\tc.miniredis.FastForward(200 * time.Millisecond)\n\t\tc.real.Do(\"MINIREDIS.FASTFORWARD\", \"200\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"invalid expire\", \"SET\", \"key1\", \"value\", \"PX\", \"-100\")\n\t\tc.Error(\"invalid expire\", \"SET\", \"key2\", \"value\", \"EX\", \"-100\")\n\t\tc.Error(\"invalid expire\", \"SET\", \"key3\", \"value\", \"EX\", \"0\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Do(\"SET\", \"key4\", \"value\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t\tc.Do(\"EXPIRE\", \"key4\", \"-100\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Do(\"SET\", \"key4\", \"value\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t\tc.Do(\"EXPIRE\", \"key4\", \"0\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t})\n}\n\nfunc TestProto(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ECHO\", strings.Repeat(\"X\", 1<<24))\n\t})\n}\n\nfunc TestSwapdb(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"key1\", \"val1\")\n\t\tc.Do(\"SWAPDB\", \"0\", \"1\")\n\t\tc.Do(\"SELECT\", \"1\")\n\t\tc.Do(\"GET\", \"key1\")\n\n\t\tc.Do(\"SWAPDB\", \"1\", \"1\")\n\t\tc.Do(\"GET\", \"key1\")\n\n\t\tc.Error(\"wrong number\", \"SWAPDB\")\n\t\tc.Error(\"wrong number\", \"SWAPDB\", \"1\")\n\t\tc.Error(\"wrong number\", \"SWAPDB\", \"1\", \"2\", \"3\")\n\t\tc.Error(\"invalid first\", \"SWAPDB\", \"foo\", \"2\")\n\t\tc.Error(\"invalid second\", \"SWAPDB\", \"1\", \"bar\")\n\t\tc.Error(\"invalid first\", \"SWAPDB\", \"foo\", \"bar\")\n\t\tc.Error(\"out of range\", \"SWAPDB\", \"-1\", \"2\")\n\t\tc.Error(\"out of range\", \"SWAPDB\", \"1\", \"-2\")\n\t\t// c.Do(\"SWAPDB\", \"1\", \"1000\") // miniredis has no upperlimit\n\t})\n\n\t// SWAPDB with transactions\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SET\", \"foo\", \"foooooo\")\n\n\t\tc1.Do(\"MULTI\")\n\t\tc1.Do(\"SWAPDB\", \"0\", \"2\")\n\t\tc1.Do(\"GET\", \"foo\")\n\t\tc2.Do(\"GET\", \"foo\")\n\n\t\tc1.Do(\"EXEC\")\n\t\tc1.Do(\"GET\", \"foo\")\n\t\tc2.Do(\"GET\", \"foo\")\n\t})\n}\n\nfunc TestDel(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"one\", \"1\")\n\t\tc.Do(\"SET\", \"two\", \"2\")\n\t\tc.Do(\"SET\", \"three\", \"3\")\n\t\tc.Do(\"SET\", \"four\", \"4\")\n\t\tc.Do(\"DEL\", \"one\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Do(\"DEL\", \"twoooo\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Do(\"DEL\", \"two\", \"four\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Error(\"wrong number\", \"DEL\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t})\n}\n\nfunc TestUnlink(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"one\", \"1\")\n\t\tc.Do(\"SET\", \"two\", \"2\")\n\t\tc.Do(\"SET\", \"three\", \"3\")\n\t\tc.Do(\"SET\", \"four\", \"4\")\n\t\tc.Do(\"UNLINK\", \"one\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Do(\"UNLINK\", \"twoooo\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Do(\"UNLINK\", \"two\", \"four\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\n\t\tc.Error(\"wrong number\", \"UNLINK\")\n\t\tc.DoSorted(\"KEYS\", \"*\")\n\t})\n}\n\nfunc TestTouch(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"a\", \"some value\")\n\t\tc.Do(\"TOUCH\", \"a\")\n\t\tc.Do(\"GET\", \"a\")\n\t\tc.Do(\"TTL\", \"a\")\n\n\t\tc.Do(\"TOUCH\", \"a\", \"foobar\", \"a\")\n\n\t\tc.Error(\"wrong number\", \"TOUCH\")\n\t})\n}\n\nfunc TestPersist(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"EXPIRE\", \"foo\", \"12\")\n\t\tc.Do(\"TTL\", \"foo\")\n\t\tc.Do(\"PERSIST\", \"foo\")\n\t\tc.Do(\"TTL\", \"foo\")\n\t})\n}\n\nfunc TestCopy(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"wrong number\", \"COPY\")\n\t\tc.Error(\"wrong number\", \"COPY\", \"a\")\n\t\tc.Error(\"syntax\", \"COPY\", \"a\", \"b\", \"c\")\n\t\tc.Error(\"syntax\", \"COPY\", \"a\", \"b\", \"DB\")\n\t\tc.Error(\"range\", \"COPY\", \"a\", \"b\", \"DB\", \"-1\")\n\t\tc.Error(\"integer\", \"COPY\", \"a\", \"b\", \"DB\", \"foo\")\n\t\tc.Error(\"syntax\", \"COPY\", \"a\", \"b\", \"DB\", \"1\", \"REPLACE\", \"foo\")\n\n\t\tc.Do(\"SET\", \"a\", \"1\")\n\t\tc.Do(\"COPY\", \"a\", \"b\") // returns 1 - successfully copied\n\t\tc.Do(\"EXISTS\", \"b\")\n\t\tc.Do(\"GET\", \"b\")\n\t\tc.Do(\"TYPE\", \"b\")\n\n\t\tc.Do(\"COPY\", \"nonexistent\", \"c\") // returns 1 - not successfully copied\n\t\tc.Do(\"RENAME\", \"b\", \"c\")         // rename the copied key\n\n\t\tt.Run(\"replace option\", func(t *testing.T) {\n\t\t\tc.Do(\"SET\", \"fromme\", \"1\")\n\t\t\tc.Do(\"HSET\", \"replaceme\", \"foo\", \"bar\")\n\t\t\tc.Do(\"COPY\", \"fromme\", \"replaceme\", \"REPLACE\")\n\t\t\tc.Do(\"TYPE\", \"replaceme\")\n\t\t\tc.Do(\"GET\", \"replaceme\")\n\t\t})\n\n\t\tt.Run(\"different DB\", func(t *testing.T) {\n\t\t\tc.Do(\"SELECT\", \"2\")\n\t\t\tc.Do(\"SET\", \"fromme\", \"1\")\n\t\t\tc.Do(\"COPY\", \"fromme\", \"replaceme\", \"DB\", \"3\")\n\t\t\tc.Do(\"EXISTS\", \"replaceme\") // your value is in another db\n\t\t\tc.Do(\"SELECT\", \"3\")\n\t\t\tc.Do(\"EXISTS\", \"replaceme\")\n\t\t\tc.Do(\"TYPE\", \"replaceme\")\n\t\t\tc.Do(\"GET\", \"replaceme\")\n\t\t})\n\t\tc.Do(\"SELECT\", \"0\")\n\n\t\tt.Run(\"copy to self\", func(t *testing.T) {\n\t\t\t// copy to self is never allowed\n\t\t\tc.Do(\"SET\", \"double\", \"1\")\n\t\t\tc.Error(\"the same\", \"COPY\", \"double\", \"double\")\n\t\t\tc.Error(\"the same\", \"COPY\", \"double\", \"double\", \"REPLACE\")\n\t\t\tc.Do(\"COPY\", \"double\", \"double\", \"DB\", \"2\") // different DB is fine\n\t\t\tc.Do(\"SELECT\", \"2\")\n\t\t\tc.Do(\"TYPE\", \"double\")\n\n\t\t\tc.Error(\"the same\", \"COPY\", \"noexisting\", \"noexisting\") // \"copy to self?\" check comes before key check\n\t\t})\n\t\tc.Do(\"SELECT\", \"0\")\n\n\t\t// deep copies?\n\t\tt.Run(\"hash\", func(t *testing.T) {\n\t\t\tc.Do(\"HSET\", \"temp\", \"paris\", \"12\")\n\t\t\tc.Do(\"HSET\", \"temp\", \"oslo\", \"-5\")\n\t\t\tc.Do(\"COPY\", \"temp\", \"temp2\")\n\t\t\tc.Do(\"TYPE\", \"temp2\")\n\t\t\tc.Do(\"HGET\", \"temp2\", \"oslo\")\n\t\t\tc.Do(\"HSET\", \"temp2\", \"oslo\", \"-7\")\n\t\t\tc.Do(\"HGET\", \"temp\", \"oslo\")\n\t\t\tc.Do(\"HGET\", \"temp2\", \"oslo\")\n\t\t})\n\n\t\tt.Run(\"list set\", func(t *testing.T) {\n\t\t\tc.Do(\"LPUSH\", \"list\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"COPY\", \"list\", \"list2\")\n\t\t\tc.Do(\"TYPE\", \"list2\")\n\t\t\tc.Do(\"LSET\", \"list\", \"1\", \"vuur\")\n\t\t\tc.Do(\"LRANGE\", \"list\", \"0\", \"-1\")\n\t\t\tc.Do(\"LRANGE\", \"list2\", \"0\", \"-1\")\n\t\t})\n\n\t\tt.Run(\"list\", func(t *testing.T) {\n\t\t\tc.Do(\"LPUSH\", \"list\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"COPY\", \"list\", \"list2\")\n\t\t\tc.Do(\"TYPE\", \"list2\")\n\t\t\tc.Do(\"LPUSH\", \"list\", \"vuur\")\n\t\t\tc.Do(\"LRANGE\", \"list\", \"0\", \"-1\")\n\t\t\tc.Do(\"LRANGE\", \"list2\", \"0\", \"-1\")\n\t\t})\n\n\t\tt.Run(\"set\", func(t *testing.T) {\n\t\t\tc.Do(\"SADD\", \"set\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"COPY\", \"set\", \"set2\")\n\t\t\tc.Do(\"TYPE\", \"set2\")\n\t\t\tc.DoSorted(\"SMEMBERS\", \"set2\")\n\t\t\tc.Do(\"SADD\", \"set\", \"vuur\")\n\t\t\tc.DoSorted(\"SMEMBERS\", \"set\")\n\t\t\tc.DoSorted(\"SMEMBERS\", \"set2\")\n\t\t})\n\n\t\tt.Run(\"sorted set\", func(t *testing.T) {\n\t\t\tc.Do(\"ZADD\", \"zset\", \"1\", \"aap\", \"2\", \"noot\", \"3\", \"mies\")\n\t\t\tc.Do(\"COPY\", \"zset\", \"zset2\")\n\t\t\tc.Do(\"TYPE\", \"zset2\")\n\t\t\tc.Do(\"ZCARD\", \"zset\")\n\t\t\tc.Do(\"ZCARD\", \"zset2\")\n\t\t\tc.Do(\"ZADD\", \"zset\", \"4\", \"vuur\")\n\t\t\tc.Do(\"ZCARD\", \"zset\")\n\t\t\tc.Do(\"ZCARD\", \"zset2\")\n\t\t})\n\n\t\tt.Run(\"stream\", func(t *testing.T) {\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"0-1\",\n\t\t\t\t\"name\", \"Mercury\",\n\t\t\t)\n\t\t\tc.Do(\"COPY\", \"planets\", \"planets2\")\n\t\t\tc.Do(\"XLEN\", \"planets2\")\n\t\t\tc.Do(\"TYPE\", \"planets2\")\n\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"18446744073709551000-0\",\n\t\t\t\t\"name\", \"Earth\",\n\t\t\t)\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\t\t\tc.Do(\"XLEN\", \"planets2\")\n\t\t})\n\n\t\tt.Run(\"stream\", func(t *testing.T) {\n\t\t\tc.Do(\"PFADD\", \"hlog\", \"42\")\n\t\t\tc.DoApprox(2, \"PFCOUNT\", \"hlog\")\n\t\t\tc.Do(\"COPY\", \"hlog\", \"hlog2\")\n\t\t\t// c.Do(\"TYPE\", \"hlog2\") broken\n\t\t\tc.Do(\"PFADD\", \"hlog\", \"44\")\n\t\t\tc.Do(\"PFCOUNT\", \"hlog\")\n\t\t\tc.Do(\"PFCOUNT\", \"hlog2\")\n\t\t})\n\t})\n}\n\nfunc TestClient(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// Try to get the client name without setting it first\n\t\tc.Do(\"CLIENT\", \"GETNAME\")\n\n\t\tc.Do(\"CLIENT\", \"SETNAME\", \"miniredis-tests\")\n\t\tc.Do(\"CLIENT\", \"GETNAME\")\n\t\tc.Do(\"CLIENT\", \"SETNAME\", \"miniredis-tests2\")\n\t\tc.Do(\"CLIENT\", \"GETNAME\")\n\t\tc.Do(\"CLIENT\", \"SETNAME\", \"\")\n\t\tc.Do(\"CLIENT\", \"GETNAME\")\n\n\t\tc.Error(\"wrong number\", \"CLIENT\")\n\t\tc.Error(\"unknown subcommand\", \"CLIENT\", \"FOOBAR\")\n\t\tc.Error(\"wrong number\", \"CLIENT\", \"GETNAME\", \"foo\")\n\t\tc.Error(\"contain spaces\", \"CLIENT\", \"SETNAME\", \"miniredis tests\")\n\t\tc.Error(\"contain spaces\", \"CLIENT\", \"SETNAME\", \"miniredis\\ntests\")\n\t})\n\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"MULTI\")\n\t\tc1.Do(\"CLIENT\", \"SETNAME\", \"conn-c1\")\n\t\tc1.Do(\"CLIENT\", \"GETNAME\")\n\t\tc2.Do(\"CLIENT\", \"GETNAME\") // not set yet\n\t\tc1.Do(\"EXEC\")\n\t\tc1.Do(\"CLIENT\", \"GETNAME\")\n\t\tc2.Do(\"CLIENT\", \"GETNAME\")\n\t})\n}\n\nfunc TestObject(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"OBJECT\", \"IDLETIME\", \"foo\")\n\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"OBJECT\", \"IDLETIME\", \"foo\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"OBJECT\", \"IDLETIME\", \"foo\")\n\n\t\tc.Error(\"number\", \"OBJECT\")\n\t\tc.Error(\"unknown subcommand 'foo'\", \"OBJECT\", \"foo\")\n\t\tc.Error(\"object|idletime\", \"OBJECT\", \"IDLETIME\")\n\t\tc.Error(\"wrong number\", \"OBJECT\", \"IDLETIME\", \"foo\", \"bar\")\n\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"OBJECT\", \"IDLETIME\", \"foo\")\n\t\tc.Error(\"object|idletime\", \"OBJECT\", \"IDLETIME\", \"bar\", \"baz\")\n\t\tc.Error(\"object|idletime\", \"OBJECT\", \"IDLETIME\")\n\t\tc.Error(\"Transaction discarded\", \"EXEC\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/geo_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGeoadd(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"GEOADD\",\n\t\t\t\"Sicily\",\n\t\t\t\"13.361389\", \"38.115556\", \"Palermo\",\n\t\t\t\"15.087269\", \"37.502669\", \"Catania\",\n\t\t)\n\t\tc.Do(\"ZRANGE\", \"Sicily\", \"0\", \"-1\")\n\t\tc.Do(\"ZRANGE\", \"Sicily\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\tc.Do(\"GEOADD\",\n\t\t\t\"mountains\",\n\t\t\t\"86.9248308\", \"27.9878675\", \"Everest\",\n\t\t\t\"142.1993050\", \"11.3299030\", \"Challenger Deep\",\n\t\t\t\"31.132\", \"29.976\", \"Pyramids\",\n\t\t)\n\t\tc.Do(\"ZRANGE\", \"mountains\", \"0\", \"-1\")\n\t\tc.Do(\"GEOADD\", // re-add an existing one\n\t\t\t\"mountains\",\n\t\t\t\"86.9248308\", \"27.9878675\", \"Everest\",\n\t\t)\n\t\tc.Do(\"ZRANGE\", \"mountains\", \"0\", \"-1\")\n\t\tc.Do(\"GEOADD\", // update\n\t\t\t\"mountains\",\n\t\t\t\"86.9248308\", \"28.000\", \"Everest\",\n\t\t)\n\t\tc.Do(\"ZRANGE\", \"mountains\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"invalid\", \"GEOADD\", \"err\", \"186.9248308\", \"27.9878675\", \"not the Everest\")\n\t\tc.Error(\"invalid\", \"GEOADD\", \"err\", \"-186.9248308\", \"27.9878675\", \"not the Everest\")\n\t\tc.Error(\"invalid\", \"GEOADD\", \"err\", \"86.9248308\", \"87.9878675\", \"not the Everest\")\n\t\tc.Error(\"invalid\", \"GEOADD\", \"err\", \"86.9248308\", \"-87.9\", \"not the Everest\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"GEOADD\", \"str\", \"86.9248308\", \"27.9878675\", \"Everest\")\n\t\tc.Error(\"wrong number\", \"GEOADD\")\n\t\tc.Error(\"wrong number\", \"GEOADD\", \"foo\")\n\t\tc.Error(\"wrong number\", \"GEOADD\", \"foo\", \"86.9248308\")\n\t\tc.Error(\"wrong number\", \"GEOADD\", \"foo\", \"86.9248308\", \"27.9878675\")\n\t\tc.Do(\"GEOADD\", \"foo\", \"86.9248308\", \"27.9878675\", \"\")\n\t\tc.Error(\"not a valid float\", \"GEOADD\", \"foo\", \"eight\", \"27.9878675\", \"bar\")\n\t\tc.Error(\"not a valid float\", \"GEOADD\", \"foo\", \"86.9248308\", \"seven\", \"bar\")\n\t\t// failures in a transaction\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"wrong number\", \"GEOADD\", \"foo\")\n\t\tc.Error(\"discarded\", \"EXEC\")\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"GEOADD\", \"foo\", \"eight\", \"27.9878675\", \"bar\")\n\t\tc.Do(\"EXEC\")\n\n\t\t// 2nd key is invalid\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"GEOADD\", \"two\",\n\t\t\t\"86.9248308\", \"28.000\", \"Everest\",\n\t\t\t\"eight\", \"27.9878675\", \"bar\",\n\t\t)\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"ZRANGE\", \"two\", \"0\", \"-1\")\n\t})\n}\n\nfunc TestGeopos(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"GEOADD\",\n\t\t\t\"Sicily\",\n\t\t\t\"13.361389\", \"38.115556\", \"Palermo\",\n\t\t\t\"15.087269\", \"37.502669\", \"Catania\",\n\t\t)\n\t\tc.Do(\"GEOPOS\", \"Sicily\")\n\t\tc.DoRounded(3, \"GEOPOS\", \"Sicily\", \"Palermo\")\n\t\tc.Do(\"GEOPOS\", \"Sicily\", \"nosuch\")\n\t\tc.DoRounded(3, \"GEOPOS\", \"Sicily\", \"Catania\", \"Palermo\")\n\t\tc.DoRounded(3, \"GEOPOS\", \"Sicily\", \"Catania\", \"Catania\", \"Palermo\")\n\t\tc.Do(\"GEOPOS\", \"nosuch\", \"Palermo\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"GEOPOS\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Error(\"wrong kind\", \"GEOPOS\", \"foo\", \"Palermo\")\n\t})\n}\n\nfunc TestGeodist(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"GEOADD\",\n\t\t\t\"Sicily\",\n\t\t\t\"13.361389\", \"38.115556\", \"Palermo\",\n\t\t\t\"15.087269\", \"37.502669\", \"Catania\",\n\t\t)\n\t\tc.DoRounded(2, \"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\")\n\t\tc.DoRounded(2, \"GEODIST\", \"Sicily\", \"Catania\", \"Palermo\")\n\t\tc.Do(\"GEODIST\", \"Sicily\", \"nosuch\", \"Palermo\")\n\t\tc.Do(\"GEODIST\", \"Sicily\", \"Catania\", \"nosuch\")\n\t\tc.Do(\"GEODIST\", \"nosuch\", \"Catania\", \"Palermo\")\n\t\tc.DoRounded(2, \"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"m\")\n\t\tc.Do(\"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"km\")\n\t\tc.Do(\"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"KM\")\n\t\tc.Do(\"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"mi\")\n\t\tc.DoRounded(2, \"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"ft\")\n\t\tc.Do(\"GEODIST\", \"Sicily\", \"Palermo\", \"Palermo\")\n\n\t\tc.Error(\"unsupported unit\", \"GEODIST\", \"Sicily\", \"Palermo\", \"Palermo\", \"yards\")\n\t\tc.Error(\"wrong number\", \"GEODIST\")\n\t\tc.Error(\"wrong number\", \"GEODIST\", \"Sicily\")\n\t\tc.Error(\"wrong number\", \"GEODIST\", \"Sicily\", \"Palermo\")\n\t\tc.Error(\"syntax error\", \"GEODIST\", \"Sicily\", \"Palermo\", \"Palermo\", \"miles\", \"too many\")\n\t\tc.Error(\"unsupported unit provided. please use M, KM, FT, MI\", \"GEODIST\", \"Sicily\", \"Palermo\", \"Catania\", \"foobar\")\n\t\tc.Do(\"SET\", \"string\", \"123\")\n\t\tc.Error(\"wrong kind\", \"GEODIST\", \"string\", \"a\", \"b\")\n\t})\n}\n\nfunc TestGeoradius(t *testing.T) {\n\tskip(t)\n\tt.Run(\"basic\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"GEOADD\",\n\t\t\t\t\"stations\",\n\t\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t)\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"KM\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"1.0\", \"1.0\", \"1\", \"km\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"ft\", \"WITHDIST\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"m\", \"WITHDIST\")\n\t\t\t// redis has more precision in the coords\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"m\", \"WITHCOORD\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHDIST\", \"WITHCOORD\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHCOORD\", \"WITHDIST\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHCOORD\", \"WITHCOORD\", \"WITHCOORD\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHDIST\", \"WITHDIST\", \"WITHDIST\")\n\t\t\t// FIXME: the distances don't quite match for miles or km\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"mi\", \"WITHDIST\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHDIST\")\n\n\t\t\t// Sorting\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"DESC\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"DESC\", \"ASC\")\n\n\t\t\t// COUNT\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"COUNT\", \"1\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"COUNT\", \"2\")\n\t\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"COUNT\", \"999\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"COUNT\")\n\t\t\tc.Error(\"COUNT must\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"COUNT\", \"0\")\n\t\t\tc.Error(\"COUNT must\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"COUNT\", \"-12\")\n\t\t\tc.Error(\"not an integer\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"COUNT\", \"foobar\")\n\n\t\t\t// non-existing key\n\t\t\tc.Do(\"GEORADIUS\", \"foo\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\")\n\n\t\t\t// no error in redis, for some reason\n\t\t\t// c.Do(\"GEORADIUS\", \"foo\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"FOOBAR\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"FOOBAR\")\n\n\t\t\t// GEORADIUS_RO\n\t\t\tc.Do(\"GEORADIUS_RO\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\")\n\t\t\tc.Do(\"GEORADIUS_RO\", \"stations\", \"1.0\", \"1.0\", \"1\", \"km\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS_RO\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"STORE\", \"bar\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS_RO\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"STOREDIST\", \"bar\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS_RO\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"STORE\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS_RO\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"STOREDIST\")\n\t\t})\n\t})\n\n\tt.Run(\"STORE\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"GEOADD\",\n\t\t\t\t\"stations\",\n\t\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t)\n\n\t\t\t// plain store\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STORE\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t\t// Yeah, valid:\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STORE\", \"\")\n\t\t\tc.Do(\"ZRANGE\", \"\", \"0\", \"-1\")\n\n\t\t\t// store with count, and overwrite existing key\n\t\t\tc.Do(\"EXPIRE\", \"foo\", \"999\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"COUNT\", \"1\", \"STORE\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.Do(\"TTL\", \"foo\")\n\n\t\t\t// store should overwrite\n\t\t\tc.Do(\"SET\", \"taken\", \"123\")\n\t\t\tc.Do(\"EXPIRE\", \"taken\", \"999\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STORE\", \"taken\")\n\t\t\tc.Do(\"TYPE\", \"taken\")\n\t\t\tc.Do(\"ZRANGE\", \"taken\", \"0\", \"-1\")\n\t\t\tc.Do(\"TTL\", \"taken\")\n\n\t\t\t// errors\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STORE\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHDIST\", \"STORE\", \"foo\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHCOORD\", \"STORE\", \"foo\")\n\t\t})\n\t})\n\n\tt.Run(\"STOREDIST\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"GEOADD\",\n\t\t\t\t\"stations\",\n\t\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t)\n\n\t\t\t// plain store\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STOREDIST\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.DoRounded(3, \"ZRANGE\", \"foo\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t\t// plain store, meter\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"m\", \"STOREDIST\", \"meter\")\n\t\t\tc.Do(\"ZRANGE\", \"meter\", \"0\", \"-1\")\n\t\t\tc.DoRounded(3, \"ZRANGE\", \"meter\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t\t// Yeah, valid:\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STOREDIST\", \"\")\n\t\t\tc.Do(\"ZRANGE\", \"\", \"0\", \"-1\")\n\n\t\t\t// STOREDIST with count\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"ASC\", \"COUNT\", \"1\", \"STOREDIST\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\n\t\t\t// store should overwrite\n\t\t\tc.Do(\"SET\", \"taken\", \"123\")\n\t\t\tc.Do(\"EXPIRE\", \"taken\", \"9999\")\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STOREDIST\", \"taken\")\n\t\t\tc.Do(\"TYPE\", \"taken\")\n\t\t\tc.Do(\"ZRANGE\", \"taken\", \"0\", \"-1\")\n\t\t\tc.Do(\"TTL\", \"taken\")\n\n\t\t\t// multiple keys\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STOREDIST\", \"n1\", \"STOREDIST\", \"n2\", \"STOREDIST\", \"n3\")\n\t\t\tc.Do(\"TYPE\", \"n1\")\n\t\t\tc.Do(\"TYPE\", \"n2\")\n\t\t\tc.Do(\"TYPE\", \"n3\")\n\n\t\t\t// STORE and STOREDIST\n\t\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STOREDIST\", \"a\", \"STORE\", \"b\")\n\t\t\tc.Do(\"TYPE\", \"a\")\n\t\t\tc.Do(\"ZRANGE\", \"a\", \"0\", \"-1\")\n\t\t\tc.Do(\"TYPE\", \"b\")\n\t\t\tc.Do(\"ZRANGE\", \"b\", \"0\", \"-1\")\n\n\t\t\t// errors\n\t\t\tc.Error(\"syntax error\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"STOREDIST\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHDIST\", \"STOREDIST\", \"foo\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"400\", \"km\", \"WITHCOORD\", \"STOREDIST\", \"foo\")\n\t\t})\n\t})\n}\n\nfunc TestGeoradiusByMember(t *testing.T) {\n\tskip(t)\n\tt.Run(\"basic\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"GEOADD\",\n\t\t\t\t\"stations\",\n\t\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t)\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\")\n\t\t\t// c.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"1.0\", \"1.0\", \"1\", \"km\") // Not valid test\n\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"ft\", \"WITHDIST\")\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"m\", \"WITHDIST\")\n\t\t\t// redis has more precision in the coords\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"m\", \"WITHCOORD\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHDIST\", \"WITHCOORD\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHCOORD\", \"WITHDIST\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHCOORD\", \"WITHCOORD\", \"WITHCOORD\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHDIST\", \"WITHDIST\", \"WITHDIST\")\n\t\t\t// FIXME: the distances don't quite match for miles or km\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"mi\", \"WITHDIST\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHDIST\")\n\n\t\t\t// Sorting\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"DESC\")\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\")\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"DESC\", \"ASC\")\n\n\t\t\t// COUNT\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"COUNT\", \"1\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"COUNT\", \"2\")\n\t\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"COUNT\", \"999\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"COUNT\")\n\t\t\tc.Error(\"COUNT must\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"COUNT\", \"0\")\n\t\t\tc.Error(\"COUNT must\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"COUNT\", \"-12\")\n\t\t\tc.Error(\"not an integer\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"COUNT\", \"foobar\")\n\n\t\t\t// non-existing key\n\t\t\t// c.Do(\"GEORADIUSBYMEMBER\", \"foo\", \"Astor Pl\", \"4\", \"km\") // Failing\n\t\t\t// geo_test.go:268: value error. expected: []interface {}{} got: <nil> case: main.command{cmd:\"GEORADIUSBYMEMBER\", args:[]interface {}{\"foo\", \"Astor Pl\", \"4\", \"km\"}, error:false, sort:false, loosely:false, errorSub:\"\", receiveOnly:false, roundFloats:0, closeChan:false}\n\n\t\t\t// no error in redis, for some reason\n\t\t\t// c.Do(\"GEORADIUSBYMEMBER\", \"foo\", \"Astor Pl\", \"4\", \"km\", \"FOOBAR\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"FOOBAR\")\n\n\t\t\t// GEORADIUSBYMEMBER_RO\n\t\t\tc.Do(\"GEORADIUSBYMEMBER_RO\", \"stations\", \"Astor Pl\", \"4\", \"km\")\n\t\t\t// c.Do(\"GEORADIUSBYMEMBER_RO\", \"stations\", \"1.0\", \"1.0\", \"1\", \"km\") // Not a valid test\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER_RO\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"STORE\", \"bar\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER_RO\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"STOREDIST\", \"bar\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER_RO\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"STORE\")\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER_RO\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"STOREDIST\")\n\t\t})\n\t})\n\n\tt.Run(\"STORE\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"GEOADD\",\n\t\t\t\t\"stations\",\n\t\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t)\n\n\t\t\t// plain store\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STORE\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t\t// Yeah, valid:\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STORE\", \"\")\n\t\t\tc.Do(\"ZRANGE\", \"\", \"0\", \"-1\")\n\n\t\t\t// store with count, and overwrite existing key\n\t\t\tc.Do(\"EXPIRE\", \"foo\", \"999\")\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"COUNT\", \"1\", \"STORE\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.Do(\"TTL\", \"foo\")\n\n\t\t\t// store should overwrite\n\t\t\tc.Do(\"SET\", \"taken\", \"123\")\n\t\t\tc.Do(\"EXPIRE\", \"taken\", \"999\")\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STORE\", \"taken\")\n\t\t\tc.Do(\"TYPE\", \"taken\")\n\t\t\tc.Do(\"ZRANGE\", \"taken\", \"0\", \"-1\")\n\t\t\tc.Do(\"TTL\", \"taken\")\n\n\t\t\t// errors\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STORE\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHDIST\", \"STORE\", \"foo\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHCOORD\", \"STORE\", \"foo\")\n\t\t})\n\t})\n\n\tt.Run(\"STOREDIST\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"GEOADD\",\n\t\t\t\t\"stations\",\n\t\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t)\n\n\t\t\t// plain store\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STOREDIST\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.DoRounded(3, \"ZRANGE\", \"foo\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t\t// plain store, meter\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"m\", \"STOREDIST\", \"meter\")\n\t\t\tc.Do(\"ZRANGE\", \"meter\", \"0\", \"-1\")\n\t\t\tc.DoRounded(3, \"ZRANGE\", \"meter\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t\t// Yeah, valid:\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STOREDIST\", \"\")\n\t\t\tc.Do(\"ZRANGE\", \"\", \"0\", \"-1\")\n\n\t\t\t// STOREDIST with count\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"ASC\", \"COUNT\", \"1\", \"STOREDIST\", \"foo\")\n\t\t\tc.Do(\"ZRANGE\", \"foo\", \"0\", \"-1\")\n\n\t\t\t// store should overwrite\n\t\t\tc.Do(\"SET\", \"taken\", \"123\")\n\t\t\tc.Do(\"EXPIRE\", \"taken\", \"9999\")\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STOREDIST\", \"taken\")\n\t\t\tc.Do(\"TYPE\", \"taken\")\n\t\t\tc.Do(\"ZRANGE\", \"taken\", \"0\", \"-1\")\n\t\t\tc.Do(\"TTL\", \"taken\")\n\n\t\t\t// multiple keys\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STOREDIST\", \"n1\", \"STOREDIST\", \"n2\", \"STOREDIST\", \"n3\")\n\t\t\tc.Do(\"TYPE\", \"n1\")\n\t\t\tc.Do(\"TYPE\", \"n2\")\n\t\t\tc.Do(\"TYPE\", \"n3\")\n\n\t\t\t// STORE and STOREDIST\n\t\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STOREDIST\", \"a\", \"STORE\", \"b\")\n\t\t\tc.Do(\"TYPE\", \"a\")\n\t\t\tc.Do(\"ZRANGE\", \"a\", \"0\", \"-1\")\n\t\t\tc.Do(\"TYPE\", \"b\")\n\t\t\tc.Do(\"ZRANGE\", \"b\", \"0\", \"-1\")\n\n\t\t\t// errors\n\t\t\tc.Error(\"syntax error\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"STOREDIST\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHDIST\", \"STOREDIST\", \"foo\")\n\t\t\tc.Error(\"not compatible\", \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"400\", \"km\", \"WITHCOORD\", \"STOREDIST\", \"foo\")\n\t\t})\n\t})\n}\n\n// a bit longer testset\nfunc TestGeo(t *testing.T) {\n\tskip(t)\n\t// some subway stations\n\t// https://data.cityofnewyork.us/Transportation/Subway-Stations/arq3-7z49/data\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"GEOADD\",\n\t\t\t\"stations\",\n\t\t\t\"-73.99106999861966\", \"40.73005400028978\", \"Astor Pl\",\n\t\t\t\"-74.00019299927328\", \"40.71880300107709\", \"Canal St\",\n\t\t\t\"-73.98384899986625\", \"40.76172799961419\", \"50th St\",\n\t\t\t\"-73.97499915116808\", \"40.68086213682956\", \"Bergen St\",\n\t\t\t\"-73.89488591154061\", \"40.66471445143568\", \"Pennsylvania Ave\",\n\t\t\t\"-73.90087000018522\", \"40.88466700064975\", \"238th St\",\n\t\t\t\"-73.95806670661364\", \"40.800581558114956\", \"Cathedral Pkwy (110th St)\",\n\t\t\t\"-73.94085899871263\", \"40.67991899941601\", \"Kingston - Throop Aves\",\n\t\t\t\"-73.8987883783301\", \"40.74971952935675\", \"65th St\",\n\t\t\t\"-73.92901818461539\", \"40.75196004401078\", \"36th St\",\n\t\t\t\"-73.98740940202974\", \"40.71830605618619\", \"Delancey St - Essex St\",\n\t\t\t\"-73.89165772702445\", \"40.67802821447783\", \"Van Siclen Ave\",\n\t\t\t\"-73.87962599910783\", \"40.68152000045683\", \"Norwood Ave\",\n\t\t\t\"-73.84443500029684\", \"40.69516599823373\", \"104th-102nd Sts\",\n\t\t\t\"-73.98177094440949\", \"40.690648119969794\", \"DeKalb Ave\",\n\t\t\t\"-73.82758075034528\", \"40.58326843810286\", \"Beach 105th St\",\n\t\t\t\"-73.81365140419632\", \"40.58809156457325\", \"Beach 90th St\",\n\t\t\t\"-73.89175225349464\", \"40.829987446384116\", \"Freeman St\",\n\t\t\t\"-73.89661738461646\", \"40.822142131170786\", \"Intervale Ave\",\n\t\t\t\"-73.90074099998965\", \"40.85609299881864\", \"182nd-183rd Sts\",\n\t\t\t\"-73.91013600050078\", \"40.84589999983414\", \"174th-175th Sts\",\n\t\t\t\"-73.91843200082253\", \"40.83376899862797\", \"167th St\",\n\t\t\t\"-73.8456249984179\", \"40.75462199881262\", \"Mets - Willets Point\",\n\t\t\t\"-73.86952700103515\", \"40.74914499948836\", \"Junction Blvd\",\n\t\t\t\"-73.83003000262508\", \"40.75959999915012\", \"Flushing - Main St\",\n\t\t\t\"-73.83256900003744\", \"40.846809998885504\", \"Buhre Ave\",\n\t\t\t\"-73.92613800014134\", \"40.81047600117261\", \"3rd Ave - 138th St\",\n\t\t\t\"-73.85122199961472\", \"40.83425499825462\", \"Castle Hill Ave\",\n\t\t\t\"-74.0041310005885\", \"40.713064999433136\", \"Brooklyn Bridge - City Hall\",\n\t\t\t\"-73.8470359987544\", \"40.836488000608156\", \"Zerega Ave\",\n\t\t\t\"-73.9767132992584\", \"40.75180742981634\", \"Grand Central - 42nd St\",\n\t\t\t\"-73.98207600148947\", \"40.74608099909145\", \"33rd St\",\n\t\t\t\"-73.9510700015425\", \"40.78567199998607\", \"96th St\",\n\t\t\t\"-73.95987399886047\", \"40.77362000074615\", \"77th St\",\n\t\t\t\"-73.91038357033376\", \"40.68285130087804\", \"Chauncey St\",\n\t\t\t\"-73.98310999909673\", \"40.67731566735096\", \"Union St\",\n\t\t\t\"-73.8820347465864\", \"40.74237007972169\", \"Elmhurst Ave\",\n\t\t\t\"-73.92078599933306\", \"40.678822000873375\", \"Ralph Ave\",\n\t\t\t\"-73.86748067850041\", \"40.8571924091606\", \"Pelham Pkwy\",\n\t\t\t\"-73.86613410538703\", \"40.877839385172024\", \"Gun Hill Rd\",\n\t\t\t\"-73.8543153107622\", \"40.898286515575286\", \"Nereid Ave (238 St)\",\n\t\t\t\"-73.9580997367769\", \"40.67076515344894\", \"Franklin Ave\",\n\t\t\t\"-73.89306639507903\", \"40.823976841237396\", \"Simpson St\",\n\t\t\t\"-73.86835609178098\", \"40.848768666338934\", \"Bronx Park East\",\n\t\t\t\"-73.95007934590994\", \"40.65665931376077\", \"Winthrop St\",\n\t\t\t\"-73.88940491730106\", \"40.665517963059635\", \"Van Siclen Ave\",\n\t\t\t\"-73.9273847542618\", \"40.81830344372315\", \"149th St - Grand Concourse\",\n\t\t\t\"-73.92569199505733\", \"40.82823032742169\", \"161st St - Yankee Stadium\",\n\t\t\t\"-73.9679670004732\", \"40.762526000304575\", \"Lexington Ave - 59th St\",\n\t\t\t\"-73.90409799875945\", \"40.81211799827203\", \"E 149th St\",\n\t\t\t\"-73.87451599929486\", \"40.82952100156747\", \"Morrison Av - Soundview\",\n\t\t\t\"-73.8862829985325\", \"40.82652500055904\", \"Whitlock Ave\",\n\t\t\t\"-73.86761799923673\", \"40.8315090005233\", \"St Lawrence Ave\",\n\t\t\t\"-73.90298400173006\", \"40.745630001138395\", \"Woodside - 61st St\",\n\t\t\t\"-73.75540499924732\", \"40.603995001687544\", \"Far Rockaway - Mott Ave\",\n\t\t\t\"-73.976336575218\", \"40.77551939729258\", \"72nd St\",\n\t\t\t\"-73.96460245687166\", \"40.79161879767014\", \"96th St\",\n\t\t\t\"-73.93956099985425\", \"40.84071899990795\", \"168th St\",\n\t\t\t\"-73.8935090000331\", \"40.86697799999945\", \"Kingsbridge Rd\",\n\t\t\t\"-73.98459099904711\", \"40.754184001312545\", \"42nd St - Bryant Pk\",\n\t\t\t\"-73.96203130426609\", \"40.6616334551018\", \"Prospect Park\",\n\t\t\t\"-73.99534882595742\", \"40.63147876093745\", \"55th St\",\n\t\t\t\"-73.81701287135405\", \"40.70289855287313\", \"Jamaica - Van Wyck\",\n\t\t\t\"-73.8303702709878\", \"40.714034819571026\", \"Kew Gardens - Union Tpke\",\n\t\t\t\"-73.80800471963833\", \"40.700382424235\", \"Sutphin Blvd - Archer Av\",\n\t\t\t\"-73.94605470266329\", \"40.747768121414325\", \"Court Sq - 23rd St\",\n\t\t\t\"-73.85286048434907\", \"40.726505475813006\", \"67th Ave\",\n\t\t\t\"-73.87722085669182\", \"40.736813418197144\", \"Grand Ave - Newtown\",\n\t\t\t\"-73.97817199965161\", \"40.63611866666291\", \"Ditmas Ave\",\n\t\t\t\"-73.95999000137212\", \"40.68888900026455\", \"Classon Ave\",\n\t\t\t\"-73.95031225606621\", \"40.706126576274166\", \"Broadway\",\n\t\t\t\"-73.95024799996972\", \"40.71407200064717\", \"Lorimer St\",\n\t\t\t\"-73.9019160004208\", \"40.66914500061398\", \"Sutter Ave\",\n\t\t\t\"-73.90395860491864\", \"40.68886654246024\", \"Wilson Ave\",\n\t\t\t\"-73.9166388842194\", \"40.686415270704344\", \"Halsey St\",\n\t\t\t\"-73.94735499884204\", \"40.703844000042096\", \"Lorimer St\",\n\t\t\t\"-74.01151599772157\", \"40.634970999647166\", \"8th Ave\",\n\t\t\t\"-73.929861999118\", \"40.7564420005104\", \"36th Ave\",\n\t\t\t\"-73.92582299919906\", \"40.761431998800546\", \"Broadway\",\n\t\t\t\"-73.98676800153976\", \"40.75461199851542\", \"Times Sq - 42nd St\",\n\t\t\t\"-73.97918899989101\", \"40.75276866674217\", \"Grand Central - 42nd St\",\n\t\t\t\"-73.95762400074634\", \"40.67477166685263\", \"Park Pl\",\n\t\t\t\"-73.83216299845388\", \"40.68433100001238\", \"111th St\",\n\t\t\t\"-74.00030814755975\", \"40.732254493367876\", \"W 4th St - Washington Sq (Lower)\",\n\t\t\t\"-73.97192000069982\", \"40.75710699989316\", \"51st St\",\n\t\t\t\"-73.97621799859327\", \"40.78864400073892\", \"86th St\",\n\t\t\t\"-73.85736239521543\", \"40.89314324138378\", \"233rd St\",\n\t\t\t\"-73.98220899995783\", \"40.77344000052039\", \"66th St - Lincoln Ctr\",\n\t\t\t\"-73.89054900017344\", \"40.82094799852307\", \"Hunts Point Ave\",\n\t\t\t\"-74.0062770001748\", \"40.72285399778783\", \"Canal St\",\n\t\t\t\"-73.83632199755944\", \"40.84386300128381\", \"Middletown Rd\",\n\t\t\t\"-73.98659900207888\", \"40.739864000474604\", \"23rd St\",\n\t\t\t\"-73.94526400039679\", \"40.74702299889643\", \"Court Sq\",\n\t\t\t\"-73.98192900232715\", \"40.76824700063689\", \"59th St - Columbus Circle\",\n\t\t\t\"-73.9489160009391\", \"40.74221599986316\", \"Hunters Point Ave\",\n\t\t\t\"-73.9956570016487\", \"40.74408099989751\", \"23rd St\",\n\t\t\t\"-74.00536700180581\", \"40.728251000730204\", \"Houston St\",\n\t\t\t\"-73.83768300060997\", \"40.681711001091195\", \"104th St\",\n\t\t\t\"-73.81583268782963\", \"40.60840218069683\", \"Broad Channel\",\n\t\t\t\"-73.96850099975177\", \"40.57631166708091\", \"Ocean Pkwy\",\n\t\t\t\"-73.95358099875249\", \"40.74262599969749\", \"Vernon Blvd - Jackson Ave\",\n\t\t\t\"-73.96387000158042\", \"40.76814100049679\", \"68th St - Hunter College\",\n\t\t\t\"-73.9401635351909\", \"40.750635651014804\", \"Queensboro Plz\",\n\t\t\t\"-73.8438529979573\", \"40.680428999588415\", \"Rockaway Blvd\",\n\t\t\t\"-73.98995099881881\", \"40.734673000996125\", \"Union Sq - 14th St\",\n\t\t\t\"-73.95352200064022\", \"40.68962700158444\", \"Bedford - Nostrand Aves\",\n\t\t\t\"-73.97973580592873\", \"40.66003568810021\", \"15th St - Prospect Park\",\n\t\t\t\"-73.98025117900944\", \"40.66624469001985\", \"7th Ave\",\n\t\t\t\"-73.97577599917474\", \"40.65078166803418\", \"Ft Hamilton Pkwy\",\n\t\t\t\"-73.97972116229084\", \"40.64427200012998\", \"Church Ave\",\n\t\t\t\"-73.96435779623125\", \"40.64390459860419\", \"Beverly Rd\",\n\t\t\t\"-73.96288246192114\", \"40.65049324646484\", \"Church Ave\",\n\t\t\t\"-73.96269486837261\", \"40.63514193733789\", \"Newkirk Ave\",\n\t\t\t\"-73.96145343987648\", \"40.65507304163716\", \"Parkside Ave\",\n\t\t\t\"-73.9709563319228\", \"40.6752946951032\", \"Grand Army Plaza\",\n\t\t\t\"-73.97754993539385\", \"40.68442016526762\", \"Atlantic Av - Barclay's Center\",\n\t\t\t\"-73.91194599726617\", \"40.678339999883505\", \"Rockaway Ave\",\n\t\t\t\"-73.97537499833149\", \"40.68711899950771\", \"Fulton St\",\n\t\t\t\"-73.9667959986695\", \"40.68809400106055\", \"Clinton - Washington Aves\",\n\t\t\t\"-73.97285279191024\", \"40.67710217983294\", \"7th Ave\",\n\t\t\t\"-73.97678343963167\", \"40.684488323453685\", \"Atlantic Av - Barclay's Center\",\n\t\t\t\"-73.97880999956767\", \"40.683665667279435\", \"Atlantic Av - Barclay's Center\",\n\t\t\t\"-73.99015100090539\", \"40.692403999991036\", \"Borough Hall\",\n\t\t\t\"-73.83591899965162\", \"40.672096999172844\", \"Aqueduct Racetrack\",\n\t\t\t\"-73.86049500117254\", \"40.85436399966426\", \"Morris Park\",\n\t\t\t\"-73.85535900043564\", \"40.858984999820116\", \"Pelham Pkwy\",\n\t\t\t\"-73.95042600099683\", \"40.68043800006226\", \"Nostrand Ave\",\n\t\t\t\"-73.98040679874578\", \"40.68831058019022\", \"Nevins St\",\n\t\t\t\"-73.96422203748425\", \"40.67203223545925\", \"Eastern Pkwy - Bklyn Museum\",\n\t\t\t\"-73.94884798381702\", \"40.64512351894373\", \"Beverly Rd\",\n\t\t\t\"-73.94945514035334\", \"40.6508606878022\", \"Church Ave\",\n\t\t\t\"-73.94829990822407\", \"40.63999124275311\", \"Newkirk Ave\",\n\t\t\t\"-73.94754120734406\", \"40.63284240700742\", \"Brooklyn College - Flatbush Ave\",\n\t\t\t\"-73.95072891124937\", \"40.6627729934283\", \"Sterling St\",\n\t\t\t\"-73.93293256081851\", \"40.66897831107809\", \"Crown Hts - Utica Ave\",\n\t\t\t\"-73.94215978392963\", \"40.66948144864978\", \"Kingston Ave\",\n\t\t\t\"-73.95118300016523\", \"40.724479997808274\", \"Nassau Ave\",\n\t\t\t\"-73.95442500146235\", \"40.73126699971465\", \"Greenpoint Ave\",\n\t\t\t\"-73.95783200075729\", \"40.708383000017925\", \"Marcy Ave\",\n\t\t\t\"-73.95348800038457\", \"40.706889998054\", \"Hewes St\",\n\t\t\t\"-73.92984899935611\", \"40.81322399958908\", \"138th St - Grand Concourse\",\n\t\t\t\"-73.9752485052734\", \"40.76008683231326\", \"5th Ave - 53rd St\",\n\t\t\t\"-73.96907237490204\", \"40.75746830782865\", \"Lexington Ave - 53rd St\",\n\t\t\t\"-73.98869800128737\", \"40.74545399979951\", \"28th St\",\n\t\t\t\"-73.9879368338264\", \"40.74964456009442\", \"Herald Sq - 34th St\",\n\t\t\t\"-73.98168087489128\", \"40.73097497580066\", \"1st Ave\",\n\t\t\t\"-73.98622899953202\", \"40.755983000570076\", \"Times Sq - 42nd St\",\n\t\t\t\"-73.9514239994525\", \"40.71277400073426\", \"Metropolitan Ave\",\n\t\t\t\"-73.94049699874644\", \"40.71157600064823\", \"Grand St\",\n\t\t\t\"-73.94394399869037\", \"40.714575998363635\", \"Graham Ave\",\n\t\t\t\"-73.95666499806525\", \"40.71717399858899\", \"Bedford Ave\",\n\t\t\t\"-73.93979284713505\", \"40.70739106438455\", \"Montrose Ave\",\n\t\t\t\"-73.94381559597835\", \"40.74630503357145\", \"Long Island City - Court Sq\",\n\t\t\t\"-73.9495999997552\", \"40.7441286664954\", \"21st St\",\n\t\t\t\"-73.93285137679598\", \"40.75276306140845\", \"39th Ave\",\n\t\t\t\"-73.95035999879713\", \"40.82655099962194\", \"145th St\",\n\t\t\t\"-73.94488999901047\", \"40.8340410001399\", \"157th St\",\n\t\t\t\"-73.97232299915696\", \"40.79391900121471\", \"96th St\",\n\t\t\t\"-73.96837899960818\", \"40.799446000334825\", \"103rd St\",\n\t\t\t\"-73.95182200176913\", \"40.79907499977324\", \"Central Park North (110th St)\",\n\t\t\t\"-73.96137008267617\", \"40.796060739904526\", \"103rd St\",\n\t\t\t\"-73.98197000159583\", \"40.77845300068614\", \"72nd St\",\n\t\t\t\"-73.97209794937208\", \"40.78134608418206\", \"81st St\",\n\t\t\t\"-73.83692369387158\", \"40.71804465348743\", \"75th Ave\",\n\t\t\t\"-73.96882849429672\", \"40.78582304678557\", \"86th St\",\n\t\t\t\"-73.9668470005456\", \"40.80396699961484\", \"Cathedral Pkwy (110th St)\",\n\t\t\t\"-73.96410999757751\", \"40.807722001230864\", \"116th St - Columbia University\",\n\t\t\t\"-73.94549500011411\", \"40.807753999182815\", \"125th St\",\n\t\t\t\"-73.94077000106708\", \"40.8142290003391\", \"135th St\",\n\t\t\t\"-73.94962500096905\", \"40.802097999133004\", \"116th St\",\n\t\t\t\"-73.90522700122354\", \"40.850409999510234\", \"Tremont Ave\",\n\t\t\t\"-73.95367600087873\", \"40.82200799968475\", \"137th St - City College\",\n\t\t\t\"-73.93624499873299\", \"40.82042099969279\", \"145th St\",\n\t\t\t\"-73.91179399884471\", \"40.8484800012369\", \"176th St\",\n\t\t\t\"-73.9076840015997\", \"40.85345300155693\", \"Burnside Ave\",\n\t\t\t\"-73.91339999846983\", \"40.83930599964156\", \"170th St\",\n\t\t\t\"-73.94013299907257\", \"40.840555999148535\", \"168th St\",\n\t\t\t\"-73.9335959996056\", \"40.84950499974065\", \"181st St\",\n\t\t\t\"-73.92941199742039\", \"40.85522500175836\", \"191st St\",\n\t\t\t\"-73.93970399761596\", \"40.84739100072403\", \"175th St\",\n\t\t\t\"-73.77601299999507\", \"40.59294299908617\", \"Beach 44th St\",\n\t\t\t\"-73.7885219980118\", \"40.59237400121235\", \"Beach 60th St\",\n\t\t\t\"-73.82052058959523\", \"40.58538569133279\", \"Beach 98th St\",\n\t\t\t\"-73.83559008701239\", \"40.580955865573515\", \"Rockaway Park - Beach 116 St\",\n\t\t\t\"-73.76817499939688\", \"40.59539800166876\", \"Beach 36th St\",\n\t\t\t\"-73.76135299762073\", \"40.60006600105881\", \"Beach 25th St\",\n\t\t\t\"-73.80328900021885\", \"40.707571999615695\", \"Parsons Blvd\",\n\t\t\t\"-73.79347419927721\", \"40.710517502784\", \"169th St\",\n\t\t\t\"-73.86269999830412\", \"40.749865000555545\", \"103rd St - Corona Plaza\",\n\t\t\t\"-73.85533399834884\", \"40.75172999941711\", \"111th St\",\n\t\t\t\"-73.86161820097203\", \"40.729763972422425\", \"63rd Dr - Rego Park\",\n\t\t\t\"-73.86504999877702\", \"40.67704400054478\", \"Grant Ave\",\n\t\t\t\"-73.97991700056134\", \"40.78393399959032\", \"79th St\",\n\t\t\t\"-73.9030969995401\", \"40.67534466640805\", \"Atlantic Ave\",\n\t\t\t\"-74.00290599855235\", \"40.73342200104225\", \"Christopher St - Sheridan Sq\",\n\t\t\t\"-73.82579799906613\", \"40.68595099878361\", \"Ozone Park - Lefferts Blvd\",\n\t\t\t\"-73.98769099825152\", \"40.755477001982506\", \"Times Sq - 42nd St\",\n\t\t\t\"-73.97595787413822\", \"40.576033818103646\", \"W 8th St - NY Aquarium\",\n\t\t\t\"-73.99336500134324\", \"40.74721499918219\", \"28th St\",\n\t\t\t\"-73.98426400110407\", \"40.743069999259035\", \"28th St\",\n\t\t\t\"-73.82812100059289\", \"40.85246199951662\", \"Pelham Bay Park\",\n\t\t\t\"-73.84295199925012\", \"40.839892001013915\", \"Westchester Sq - E Tremont Ave\",\n\t\t\t\"-73.99787100060406\", \"40.741039999802105\", \"18th St\",\n\t\t\t\"-73.97604100111508\", \"40.751431000286864\", \"Grand Central - 42nd St\",\n\t\t\t\"-73.7969239998421\", \"40.59092700078133\", \"Beach 67th St\",\n\t\t\t\"-74.00049500225435\", \"40.73233799774325\", \"W 4th St - Washington Sq (Upper)\",\n\t\t\t\"-73.86008700006875\", \"40.69242699966103\", \"85th St - Forest Pky\",\n\t\t\t\"-73.85205199740794\", \"40.69370399880105\", \"Woodhaven Blvd\",\n\t\t\t\"-73.83679338454697\", \"40.697114810696476\", \"111th St\",\n\t\t\t\"-73.82834900017954\", \"40.700481998515315\", \"121st St\",\n\t\t\t\"-73.90393400118631\", \"40.69551800114878\", \"Halsey St\",\n\t\t\t\"-73.9109757182647\", \"40.699471062427136\", \"Myrtle - Wyckoff Aves\",\n\t\t\t\"-73.88411070800329\", \"40.6663149325969\", \"New Lots Ave\",\n\t\t\t\"-73.8903580002471\", \"40.67270999906104\", \"Van Siclen Ave\",\n\t\t\t\"-73.8851940021643\", \"40.679777998961164\", \"Cleveland St\",\n\t\t\t\"-73.90056237226057\", \"40.66405727094644\", \"Livonia Ave\",\n\t\t\t\"-73.90244864183562\", \"40.66358900181724\", \"Junius St\",\n\t\t\t\"-73.90895833584449\", \"40.66261748815223\", \"Rockaway Ave\",\n\t\t\t\"-73.90185000017287\", \"40.64665366739528\", \"Canarsie - Rockaway Pkwy\",\n\t\t\t\"-73.89954769388724\", \"40.65046878544699\", \"E 105th St\",\n\t\t\t\"-73.91633025007947\", \"40.6615297898075\", \"Saratoga Ave\",\n\t\t\t\"-73.92252118536001\", \"40.66476678877493\", \"Sutter Ave - Rutland Road\",\n\t\t\t\"-73.89927796057142\", \"40.65891477368527\", \"New Lots Ave\",\n\t\t\t\"-73.90428999746412\", \"40.67936600147369\", \"Broadway Junction\",\n\t\t\t\"-73.89852600159652\", \"40.676998000003756\", \"Alabama Ave\",\n\t\t\t\"-73.88074999747269\", \"40.6741300014559\", \"Shepherd Ave\",\n\t\t\t\"-73.87392925215778\", \"40.68315265707736\", \"Crescent St\",\n\t\t\t\"-73.87332199882995\", \"40.689616000838754\", \"Cypress Hills\",\n\t\t\t\"-73.86728799944963\", \"40.691290001246735\", \"75th St - Eldert Ln\",\n\t\t\t\"-73.8964029993185\", \"40.746324999410284\", \"69th St\",\n\t\t\t\"-73.8912051289911\", \"40.746867573829114\", \"74th St - Broadway\",\n\t\t\t\"-73.86943208612348\", \"40.73309737380972\", \"Woodhaven Blvd - Queens Mall\",\n\t\t\t\"-73.91217899939602\", \"40.69945400090837\", \"Myrtle - Wyckoff Aves\",\n\t\t\t\"-73.90758199885423\", \"40.70291899894902\", \"Seneca Ave\",\n\t\t\t\"-73.91823200219723\", \"40.70369299961644\", \"DeKalb Ave\",\n\t\t\t\"-73.91254899891254\", \"40.744149001021576\", \"52nd St\",\n\t\t\t\"-73.91352174995538\", \"40.756316952608096\", \"46th St\",\n\t\t\t\"-73.90606508052358\", \"40.752824829236076\", \"Northern Blvd\",\n\t\t\t\"-73.91843500103973\", \"40.74313200060382\", \"46th St\",\n\t\t\t\"-73.88369700071884\", \"40.747658999559135\", \"82nd St - Jackson Hts\",\n\t\t\t\"-73.87661299986985\", \"40.74840800060913\", \"90th St - Elmhurst Av\",\n\t\t\t\"-73.83030100071032\", \"40.66047600004959\", \"Howard Beach - JFK Airport\",\n\t\t\t\"-73.83405799948723\", \"40.668234001699815\", \"Aqueduct - North Conduit Av\",\n\t\t\t\"-73.82069263637443\", \"40.70916181536946\", \"Briarwood - Van Wyck Blvd\",\n\t\t\t\"-73.84451672012669\", \"40.72159430953587\", \"Forest Hills - 71st Av\",\n\t\t\t\"-73.81083299897232\", \"40.70541799906764\", \"Sutphin Blvd\",\n\t\t\t\"-73.80109632298924\", \"40.70206737621188\", \"Jamaica Ctr - Parsons / Archer\",\n\t\t\t\"-73.86021461772737\", \"40.88802825863786\", \"225th St\",\n\t\t\t\"-73.87915899874777\", \"40.82858400108929\", \"Elder Ave\",\n\t\t\t\"-73.89643499897414\", \"40.816103999972405\", \"Longwood Ave\",\n\t\t\t\"-73.91809500109238\", \"40.77003699949086\", \"Astoria Blvd\",\n\t\t\t\"-73.9120340001031\", \"40.775035666523664\", \"Astoria - Ditmars Blvd\",\n\t\t\t\"-73.9077019387083\", \"40.81643746686396\", \"Jackson Ave\",\n\t\t\t\"-73.90177778730917\", \"40.81948726483844\", \"Prospect Ave\",\n\t\t\t\"-73.91404199994753\", \"40.8053680007636\", \"Cypress Ave\",\n\t\t\t\"-73.88769359812888\", \"40.837195550170605\", \"174th St\",\n\t\t\t\"-73.86723422851625\", \"40.86548337793927\", \"Allerton Ave\",\n\t\t\t\"-73.90765699936489\", \"40.80871900090143\", \"E 143rd St - St Mary's St\",\n\t\t\t\"-73.89717400101743\", \"40.867760000885795\", \"Kingsbridge Rd\",\n\t\t\t\"-73.89006400069478\", \"40.87341199980121\", \"Bedford Park Blvd - Lehman College\",\n\t\t\t\"-73.93647000005559\", \"40.82388000080457\", \"Harlem - 148 St\",\n\t\t\t\"-73.9146849986034\", \"40.84443400092679\", \"Mt Eden Ave\",\n\t\t\t\"-73.89774900102401\", \"40.861295998683495\", \"Fordham Rd\",\n\t\t\t\"-73.91779099745928\", \"40.84007499993004\", \"170th St\",\n\t\t\t\"-73.88713799889574\", \"40.87324399861646\", \"Bedford Park Blvd\",\n\t\t\t\"-73.90983099923551\", \"40.87456099941789\", \"Marble Hill - 225th St\",\n\t\t\t\"-73.90483400107873\", \"40.87885599817935\", \"231st St\",\n\t\t\t\"-73.91527899954356\", \"40.86944399946045\", \"215th St\",\n\t\t\t\"-73.91881900132312\", \"40.864614000525854\", \"207th St\",\n\t\t\t\"-73.91989900100465\", \"40.86807199999737\", \"Inwood - 207th St\",\n\t\t\t\"-73.89858300049647\", \"40.88924800011476\", \"Van Cortlandt Park - 242nd St\",\n\t\t\t\"-73.87996127877184\", \"40.84020763241799\", \"West Farms Sq - E Tremont Av\",\n\t\t\t\"-73.8625097078866\", \"40.883887974625274\", \"219th St\",\n\t\t\t\"-73.88465499988732\", \"40.87974999947229\", \"Mosholu Pkwy\",\n\t\t\t\"-73.87885499918691\", \"40.87481100011182\", \"Norwood - 205th St\",\n\t\t\t\"-73.86705361747603\", \"40.87125880254771\", \"Burke Ave\",\n\t\t\t\"-73.83859099802153\", \"40.87866300037311\", \"Baychester Ave\",\n\t\t\t\"-73.8308340021742\", \"40.88829999901007\", \"Eastchester - Dyre Ave\",\n\t\t\t\"-73.78381700176453\", \"40.712645666744045\", \"Jamaica - 179th St\",\n\t\t\t\"-73.8506199987954\", \"40.903125000541245\", \"Wakefield - 241st St\",\n\t\t\t\"-73.95924499945693\", \"40.670342666584396\", \"Botanic Garden\",\n\t\t\t\"-73.90526176305106\", \"40.68286062551184\", \"Bushwick - Aberdeen\",\n\t\t\t\"-73.90311757920684\", \"40.67845624842869\", \"Broadway Junction\",\n\t\t\t\"-73.84638400151765\", \"40.86952599962676\", \"Gun Hill Rd\",\n\t\t\t\"-73.87334609510884\", \"40.8418630412186\", \"E 180th St\",\n\t\t\t\"-73.92553600006474\", \"40.86053100138796\", \"Dyckman St\",\n\t\t\t\"-73.95837200097044\", \"40.815580999978934\", \"125th St\",\n\t\t\t\"-73.95582700110425\", \"40.68059566598263\", \"Franklin Ave - Fulton St\",\n\t\t\t\"-73.92672247438611\", \"40.81833014409742\", \"149th St - Grand Concourse\",\n\t\t\t\"-73.91779152760981\", \"40.816029252510006\", \"3rd Ave - 149th St\",\n\t\t\t\"-73.92139999784426\", \"40.83553699933672\", \"167th St\",\n\t\t\t\"-73.91923999909432\", \"40.80756599987699\", \"Brook Ave\",\n\t\t\t\"-73.93099699953838\", \"40.74458699983993\", \"33rd St\",\n\t\t\t\"-73.9240159984882\", \"40.74378100149132\", \"40th St\",\n\t\t\t\"-73.94408792823116\", \"40.824766360871905\", \"145th St\",\n\t\t\t\"-73.93820899811622\", \"40.8301349999812\", \"155th St\",\n\t\t\t\"-73.92565099775477\", \"40.827904998845845\", \"161st St - Yankee Stadium\",\n\t\t\t\"-73.93072899914027\", \"40.67936399950546\", \"Utica Ave\",\n\t\t\t\"-73.9205264716827\", \"40.75698735912575\", \"Steinway St\",\n\t\t\t\"-73.92850899927413\", \"40.69317200129202\", \"Kosciuszko St\",\n\t\t\t\"-73.92215600150752\", \"40.689583999013905\", \"Gates Ave\",\n\t\t\t\"-73.92724299902838\", \"40.69787300011831\", \"Central Ave\",\n\t\t\t\"-73.91972000188625\", \"40.69866000123805\", \"Knickerbocker Ave\",\n\t\t\t\"-73.9214790001739\", \"40.76677866673298\", \"30th Ave\",\n\t\t\t\"-73.9229130000312\", \"40.706606665988716\", \"Jefferson St\",\n\t\t\t\"-73.93314700024209\", \"40.70615166680729\", \"Morgan Ave\",\n\t\t\t\"-73.93713823965695\", \"40.74891771986323\", \"Queens Plz\",\n\t\t\t\"-73.97697099965796\", \"40.62975466638584\", \"18th Ave\",\n\t\t\t\"-74.0255099996266\", \"40.629741666886915\", \"77th St\",\n\t\t\t\"-74.02337699950728\", \"40.63496666682377\", \"Bay Ridge Ave\",\n\t\t\t\"-73.9946587805514\", \"40.636260890961395\", \"50th St\",\n\t\t\t\"-74.00535100046275\", \"40.63138566722445\", \"Ft Hamilton Pkwy\",\n\t\t\t\"-73.98682900011477\", \"40.59770366695856\", \"25th Ave\",\n\t\t\t\"-73.9936762000529\", \"40.601950461572315\", \"Bay Pky\",\n\t\t\t\"-73.98452199846113\", \"40.617108999866005\", \"20th Ave\",\n\t\t\t\"-73.99045399865993\", \"40.620686997680025\", \"18th Ave\",\n\t\t\t\"-74.03087600085765\", \"40.61662166725951\", \"Bay Ridge - 95th St\",\n\t\t\t\"-74.0283979999864\", \"40.62268666715025\", \"86th St\",\n\t\t\t\"-74.00058287431507\", \"40.61315892569516\", \"79th St\",\n\t\t\t\"-73.99884094850685\", \"40.61925870977273\", \"71st St\",\n\t\t\t\"-73.99817432157568\", \"40.60467699816932\", \"20th Ave\",\n\t\t\t\"-74.00159259239406\", \"40.60773573171741\", \"18th Ave\",\n\t\t\t\"-73.99685724994863\", \"40.626224462922195\", \"62nd St\",\n\t\t\t\"-73.99635300025969\", \"40.62484166725887\", \"New Utrecht Ave\",\n\t\t\t\"-73.97337641974885\", \"40.59592482551748\", \"Ave U\",\n\t\t\t\"-73.9723553085244\", \"40.603258405128265\", \"Kings Hwy\",\n\t\t\t\"-73.96135378598797\", \"40.577710196642435\", \"Brighton Beach\",\n\t\t\t\"-73.95405791257907\", \"40.58654754707536\", \"Sheepshead Bay\",\n\t\t\t\"-73.95581122316301\", \"40.59930895095475\", \"Ave U\",\n\t\t\t\"-73.95760873538083\", \"40.608638645396006\", \"Kings Hwy\",\n\t\t\t\"-73.97908400099428\", \"40.597235999920436\", \"Ave U\",\n\t\t\t\"-73.98037300229343\", \"40.60405899980493\", \"Kings Hwy\",\n\t\t\t\"-73.97459272818807\", \"40.580738758491464\", \"Neptune Ave\",\n\t\t\t\"-73.97426599968905\", \"40.589449666625285\", \"Ave X\",\n\t\t\t\"-73.98376500045946\", \"40.58884066651933\", \"Bay 50th St\",\n\t\t\t\"-73.97818899936274\", \"40.59246500088859\", \"Gravesend - 86th St\",\n\t\t\t\"-73.97300281528751\", \"40.608842808949916\", \"Ave P\",\n\t\t\t\"-73.97404850873143\", \"40.61435671190883\", \"Ave N\",\n\t\t\t\"-73.9752569782215\", \"40.62073162316788\", \"Bay Pky\",\n\t\t\t\"-73.9592431052215\", \"40.617397744443736\", \"Ave M\",\n\t\t\t\"-73.98178001069293\", \"40.61145578989005\", \"Bay Pky\",\n\t\t\t\"-73.97606933170925\", \"40.62501744019143\", \"Ave I\",\n\t\t\t\"-73.96069316246925\", \"40.625022819915166\", \"Ave J\",\n\t\t\t\"-73.96151793942495\", \"40.62920837758969\", \"Ave H\",\n\t\t\t\"-73.95507827493762\", \"40.59532169111695\", \"Neck Rd\",\n\t\t\t\"-73.94193761457447\", \"40.75373927087553\", \"21st St - Queensbridge\",\n\t\t\t\"-73.98598400026407\", \"40.76245599925997\", \"50th St\",\n\t\t\t\"-73.98169782344476\", \"40.76297015245628\", \"7th Ave\",\n\t\t\t\"-73.98133100227702\", \"40.75864100159815\", \"47th-50th Sts - Rockefeller Ctr\",\n\t\t\t\"-73.97736800085171\", \"40.76408500081713\", \"57th St\",\n\t\t\t\"-73.96608964413245\", \"40.76461809442373\", \"Lexington Ave - 63rd St\",\n\t\t\t\"-73.95323499978866\", \"40.75917199967108\", \"Roosevelt Island - Main St\",\n\t\t\t\"-73.98164872301398\", \"40.768249531776064\", \"59th St - Columbus Circle\",\n\t\t\t\"-73.98420956591096\", \"40.759801973870694\", \"49th St\",\n\t\t\t\"-73.98072973372128\", \"40.76456552501829\", \"57th St\",\n\t\t\t\"-73.97334700047045\", \"40.764810999755284\", \"5th Ave - 59th St\",\n\t\t\t\"-73.96737501711436\", \"40.762708855394564\", \"Lexington Ave - 59th St\",\n\t\t\t\"-73.99105699913983\", \"40.75037300003949\", \"34th St - Penn Station\",\n\t\t\t\"-73.98749500051885\", \"40.75528999995681\", \"Times Sq - 42nd St\",\n\t\t\t\"-74.00762309323994\", \"40.71016216530185\", \"Fulton St\",\n\t\t\t\"-74.00858473570133\", \"40.714111000774025\", \"Chambers St\",\n\t\t\t\"-73.98973500085859\", \"40.757307998551504\", \"42nd St - Port Authority Bus Term\",\n\t\t\t\"-73.94906699890156\", \"40.69461899903765\", \"Myrtle-Willoughby Aves\",\n\t\t\t\"-73.9502340010257\", \"40.70037666622154\", \"Flushing Ave\",\n\t\t\t\"-73.99276500471389\", \"40.742954317826005\", \"23rd St\",\n\t\t\t\"-73.98777189072918\", \"40.74978939990011\", \"Herald Sq - 34th St\",\n\t\t\t\"-73.98503624034139\", \"40.68840847580642\", \"Hoyt - Schermerhorn Sts\",\n\t\t\t\"-73.98721815267317\", \"40.692470636847084\", \"Jay St - MetroTech\",\n\t\t\t\"-73.99017700122197\", \"40.713855001020406\", \"East Broadway\",\n\t\t\t\"-73.98807806807719\", \"40.71868074219453\", \"Delancey St - Essex St\",\n\t\t\t\"-73.98993800003434\", \"40.72340166574911\", \"Lower East Side - 2nd Ave\",\n\t\t\t\"-73.94137734838365\", \"40.70040440298112\", \"Flushing Ave\",\n\t\t\t\"-73.9356230012996\", \"40.6971950005145\", \"Myrtle Ave\",\n\t\t\t\"-73.98977899938897\", \"40.67027166728493\", \"4th Av - 9th St\",\n\t\t\t\"-73.99589172790934\", \"40.67364106090412\", \"Smith - 9th Sts\",\n\t\t\t\"-73.99075649573565\", \"40.68611054725977\", \"Bergen St\",\n\t\t\t\"-73.98605667854612\", \"40.69225539645323\", \"Jay St - MetroTech\",\n\t\t\t\"-73.99181830901125\", \"40.694196480776995\", \"Court St\",\n\t\t\t\"-73.99053886181645\", \"40.73587226699812\", \"Union Sq - 14th St\",\n\t\t\t\"-73.98934400102907\", \"40.74130266729\", \"23rd St\",\n\t\t\t\"-73.99287200067424\", \"40.66541366712979\", \"Prospect Ave\",\n\t\t\t\"-73.98830199974512\", \"40.670846666842756\", \"4th Av - 9th St\",\n\t\t\t\"-73.98575000112093\", \"40.73269099971662\", \"3rd Ave\",\n\t\t\t\"-73.99066976901818\", \"40.73476331217923\", \"Union Sq - 14th St\",\n\t\t\t\"-73.89654800103929\", \"40.67454199987086\", \"Liberty Ave\",\n\t\t\t\"-73.90531600055341\", \"40.67833366608023\", \"Broadway Junction\",\n\t\t\t\"-74.01788099953987\", \"40.6413616662838\", \"59th St\",\n\t\t\t\"-74.01000600074939\", \"40.648938666612814\", \"45th St\",\n\t\t\t\"-74.00354899951809\", \"40.65514366633887\", \"36th St\",\n\t\t\t\"-73.99444874451204\", \"40.64648407726636\", \"9th Ave\",\n\t\t\t\"-74.01403399986317\", \"40.64506866735981\", \"53rd St\",\n\t\t\t\"-73.9942022375285\", \"40.640912711444656\", \"Ft Hamilton Pkwy\",\n\t\t\t\"-73.99809099974297\", \"40.66039666692321\", \"25th St\",\n\t\t\t\"-73.99494697998841\", \"40.68027335170176\", \"Carroll St\",\n\t\t\t\"-74.00373899843763\", \"40.72622700129312\", \"Spring St\",\n\t\t\t\"-73.93796900205011\", \"40.851694999744616\", \"181st St\",\n\t\t\t\"-73.93417999964333\", \"40.85902199892482\", \"190th St\",\n\t\t\t\"-73.95479778057312\", \"40.80505813344211\", \"116th St\",\n\t\t\t\"-73.95224799734774\", \"40.811071672994565\", \"125th St\",\n\t\t\t\"-73.99770200045987\", \"40.72432866597571\", \"Prince St\",\n\t\t\t\"-73.99250799849149\", \"40.73046499853991\", \"8th St - NYU\",\n\t\t\t\"-74.00657099970202\", \"40.70941599925865\", \"Fulton St\",\n\t\t\t\"-74.00881099997359\", \"40.713050999077694\", \"Park Pl\",\n\t\t\t\"-74.00926600170112\", \"40.71547800011327\", \"Chambers St\",\n\t\t\t\"-73.98506379575646\", \"40.69054418535472\", \"Hoyt St\",\n\t\t\t\"-73.98999799960687\", \"40.693218999611084\", \"Borough Hall\",\n\t\t\t\"-73.90387900151532\", \"40.85840700040842\", \"183rd St\",\n\t\t\t\"-73.90103399921699\", \"40.86280299988937\", \"Fordham Rd\",\n\t\t\t\"-74.00974461517701\", \"40.71256392680817\", \"World Trade Center\",\n\t\t\t\"-74.0052290023424\", \"40.72082400007119\", \"Canal St - Holland Tunnel\",\n\t\t\t\"-73.94151400082208\", \"40.83051799929251\", \"155th St\",\n\t\t\t\"-73.93989200188344\", \"40.83601299923096\", \"163rd St - Amsterdam Av\",\n\t\t\t\"-74.00793800110387\", \"40.71002266658424\", \"Fulton St\",\n\t\t\t\"-74.00340673031336\", \"40.71323378962671\", \"Chambers St\",\n\t\t\t\"-73.99982638545937\", \"40.71817387697391\", \"Canal St\",\n\t\t\t\"-74.00698581780337\", \"40.71327233111697\", \"City Hall\",\n\t\t\t\"-74.0018260000577\", \"40.71946500105898\", \"Canal St\",\n\t\t\t\"-74.01316895919258\", \"40.701730507574474\", \"South Ferry\",\n\t\t\t\"-74.01400799803432\", \"40.70491399928076\", \"Bowling Green\",\n\t\t\t\"-74.01186199860112\", \"40.70755700086603\", \"Wall St\",\n\t\t\t\"-74.0130072374272\", \"40.703142373599135\", \"Whitehall St\",\n\t\t\t\"-74.01297456253795\", \"40.707744756294474\", \"Rector St\",\n\t\t\t\"-73.8958980017196\", \"40.70622599823048\", \"Fresh Pond Rd\",\n\t\t\t\"-73.88957722978091\", \"40.711431305058255\", \"Middle Village - Metropolitan Ave\",\n\t\t\t\"-74.01378300119742\", \"40.707512999521775\", \"Rector St\",\n\t\t\t\"-74.01218800112292\", \"40.7118350008202\", \"Cortlandt St\",\n\t\t\t\"-74.00950899856461\", \"40.710367998822136\", \"Fulton St\",\n\t\t\t\"-74.01105599991755\", \"40.706476001106005\", \"Broad St\",\n\t\t\t\"-74.01113196473266\", \"40.7105129841524\", \"Cortlandt St\",\n\t\t\t\"-74.00909999844257\", \"40.706820999753376\", \"Wall St\",\n\t\t\t\"-73.92727099960726\", \"40.865490998968916\", \"Dyckman St\",\n\t\t\t\"-73.99375299913589\", \"40.71826699954992\", \"Grand St\",\n\t\t\t\"-73.99620399876055\", \"40.725296998738045\", \"Broadway - Lafayette St\",\n\t\t\t\"-73.99380690654237\", \"40.720246883147254\", \"Bowery\",\n\t\t\t\"-74.00105471306033\", \"40.718814263587134\", \"Canal St\",\n\t\t\t\"-73.99804100117201\", \"40.74590599939995\", \"23rd St\",\n\t\t\t\"-73.99339099970578\", \"40.752287000775894\", \"34th St - Penn Station\",\n\t\t\t\"-73.89129866519697\", \"40.74653969115889\", \"Jackson Hts - Roosevelt Av\",\n\t\t\t\"-74.00020100063497\", \"40.737825999728116\", \"14th St\",\n\t\t\t\"-73.94753480879213\", \"40.817905559212676\", \"135th St\",\n\t\t\t\"-73.99620899921355\", \"40.73822799969515\", \"14th St\",\n\t\t\t\"-73.99775078874781\", \"40.73774146981052\", \"6th Ave\",\n\t\t\t\"-74.00257800104762\", \"40.73977666638199\", \"8th Ave\",\n\t\t\t\"-74.00168999937027\", \"40.740893000193296\", \"14th St\",\n\t\t\t\"-73.9504262489579\", \"40.66993815093054\", \"Nostrand Ave\",\n\t\t\t\"-73.99308599821961\", \"40.69746599996469\", \"Clark St\",\n\t\t\t\"-73.95684800014614\", \"40.68137966658742\", \"Franklin Ave\",\n\t\t\t\"-73.96583799857275\", \"40.68326299912644\", \"Clinton - Washington Aves\",\n\t\t\t\"-73.90307500005954\", \"40.70441200087814\", \"Forest Ave\",\n\t\t\t\"-73.94424999687163\", \"40.795020000113105\", \"110th St\",\n\t\t\t\"-73.95558899985132\", \"40.77949199820952\", \"86th St\",\n\t\t\t\"-73.98688499993673\", \"40.699742667691574\", \"York St\",\n\t\t\t\"-73.99053100065458\", \"40.69933699977884\", \"High St\",\n\t\t\t\"-73.97394599849406\", \"40.68611300020567\", \"Lafayette Ave\",\n\t\t\t\"-73.95058920022207\", \"40.667883603536815\", \"President St\",\n\t\t\t\"-73.87875099990931\", \"40.886037000253324\", \"Woodlawn\",\n\t\t\t\"-73.99465900006331\", \"40.72591466682659\", \"Bleecker St\",\n\t\t\t\"-73.94747800152219\", \"40.79060000008452\", \"103rd St\",\n\t\t\t\"-73.87210600099675\", \"40.675376998239365\", \"Euclid Ave\",\n\t\t\t\"-73.85147000026086\", \"40.67984300135503\", \"88th St\",\n\t\t\t\"-73.96379005505493\", \"40.6409401651401\", \"Cortelyou Rd\",\n\t\t\t\"-73.9416169983714\", \"40.7986290002001\", \"116th St\",\n\t\t\t\"-73.86081600108396\", \"40.83322599927859\", \"Parkchester\",\n\t\t\t\"-74.00688600277107\", \"40.719318001302135\", \"Franklin St\",\n\t\t\t\"-73.85899200206335\", \"40.67937100115432\", \"80th St\",\n\t\t\t\"-73.98196299856706\", \"40.75382100064824\", \"5th Ave - Bryant Pk\",\n\t\t\t\"-73.99714100006673\", \"40.72230099999366\", \"Spring St\",\n\t\t\t\"-73.93759400055725\", \"40.804138000587244\", \"125th St\",\n\t\t\t\"-73.9812359981396\", \"40.57728100006751\", \"Coney Island - Stillwell Av\",\n\t\t\t\"-74.00219709442206\", \"40.75544635961596\", \"34th St - Hudson Yards\",\n\t\t\t\"-73.95836178682246\", \"40.76880251014895\", \"72nd St\",\n\t\t\t\"-73.95177090964917\", \"40.77786104333163\", \"86th St\",\n\t\t\t\"-73.9470660219183\", \"40.784236650177654\", \"96th St\",\n\t\t)\n\t\tc.Do(\"ZRANGE\", \"stations\", \"0\", \"-1\")\n\t\tc.Do(\"ZRANGE\", \"stations\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"WITHDIST\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"WITHCOORD\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"ASC\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"DESC\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"DESC\", \"COUNT\", \"3\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"ASC\", \"COUNT\", \"3\")\n\t\tc.DoRounded(3, \"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"ASC\", \"COUNT\", \"99999\")\n\n\t\tc.DoSorted(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\")\n\t\tc.DoLoosely(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"WITHDIST\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"WITHDIST\", \"ASC\")\n\t\tc.DoLoosely(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"WITHCOORD\")\n\t\tc.DoRounded(3, \"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"WITHCOORD\", \"ASC\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"ASC\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"DESC\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"DESC\", \"COUNT\", \"3\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"ASC\", \"COUNT\", \"3\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"ASC\", \"COUNT\", \"99999\")\n\n\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"STORE\", \"res\")\n\t\tc.Do(\"ZRANGE\", \"res\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"GEORADIUS\", \"stations\", \"-73.9718893\", \"40.7728773\", \"4\", \"km\", \"STOREDIST\", \"resd\")\n\t\tc.DoLoosely(\"ZRANGE\", \"resd\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"STORE\", \"resbymem\")\n\t\tc.Do(\"ZRANGE\", \"resbymem\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"GEORADIUSBYMEMBER\", \"stations\", \"Astor Pl\", \"4\", \"km\", \"STOREDIST\", \"resbymemd\")\n\t\tc.DoLoosely(\"ZRANGE\", \"resbymemd\", \"0\", \"-1\", \"WITHSCORES\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/go.mod",
    "content": "module github.com/encoredev/encore/miniredis/tests/integration-go\n\ngo 1.22\n\nrequire github.com/alicebob/miniredis/v2 v2.37.0\n\nrequire github.com/yuin/gopher-lua v1.1.1 // indirect\n"
  },
  {
    "path": "miniredis/tests/integration-go/go.sum",
    "content": "github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=\ngithub.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=\ngithub.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=\ngithub.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=\n"
  },
  {
    "path": "miniredis/tests/integration-go/hash_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHash(t *testing.T) {\n\tskip(t)\n\tt.Run(\"basics\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"HGET\", \"aap\", \"noot\")\n\t\t\tc.Do(\"HMGET\", \"aap\", \"noot\")\n\t\t\tc.Do(\"HLEN\", \"aap\")\n\t\t\tc.Do(\"HKEYS\", \"aap\")\n\t\t\tc.Do(\"HVALS\", \"aap\")\n\t\t\tc.Do(\"HSET\", \"aaa\", \"bb\", \"1\", \"cc\", \"2\")\n\t\t\tc.Do(\"HGET\", \"aaa\", \"bb\")\n\t\t\tc.Do(\"HGET\", \"aaa\", \"cc\")\n\n\t\t\tc.Do(\"HDEL\", \"aap\", \"noot\")\n\t\t\tc.Do(\"HGET\", \"aap\", \"noot\")\n\t\t\tc.Do(\"EXISTS\", \"aap\") // key is gone\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"HSET\", \"aap\", \"noot\")\n\t\t\tc.Error(\"wrong number\", \"HGET\", \"aap\")\n\t\t\tc.Error(\"wrong number\", \"HMGET\", \"aap\")\n\t\t\tc.Error(\"wrong number\", \"HLEN\")\n\t\t\tc.Error(\"wrong number\", \"HKEYS\")\n\t\t\tc.Error(\"wrong number\", \"HVALS\")\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"wrong kind\", \"HSET\", \"str\", \"noot\", \"mies\")\n\t\t\tc.Error(\"wrong kind\", \"HGET\", \"str\", \"noot\")\n\t\t\tc.Error(\"wrong kind\", \"HMGET\", \"str\", \"noot\")\n\t\t\tc.Error(\"wrong kind\", \"HLEN\", \"str\")\n\t\t\tc.Error(\"wrong kind\", \"HKEYS\", \"str\")\n\t\t\tc.Error(\"wrong kind\", \"HVALS\", \"str\")\n\t\t\tc.Error(\"wrong number\", \"HSET\")\n\t\t\tc.Error(\"wrong number\", \"HSET\", \"a1\")\n\t\t\tc.Error(\"wrong number\", \"HSET\", \"a1\", \"b\")\n\t\t\tc.Error(\"wrong number\", \"HSET\", \"a2\", \"b\", \"c\", \"d\")\n\t\t})\n\t})\n\n\tt.Run(\"tx\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\", \"vuur\", \"wim\")\n\t\t\tc.Do(\"EXEC\")\n\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\", \"vuur\") // uneven arg count\n\t\t\tc.Do(\"EXEC\")\n\t\t})\n\t})\n\n\tt.Run(\"expire\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"HEXPIRE\", \"aap\", \"3\", \"FIELDS\", \"2\", \"noot\", \"vuur\")\n\n\t\t\tc.Error(\"wrong number\", \"HEXPIRE\", \"aap\", \"3\", \"FIELDS\", \"0\")\n\t\t\tc.Error(\"wrong number\", \"HEXPIRE\", \"aap\", \"3\")\n\t\t\tc.Error(\"wrong number\", \"HEXPIRE\", \"aap\", \"3\", \"FIELDS\")\n\t\t\tc.Error(\"wrong number\", \"HEXPIRE\", \"aap\", \"-3\", \"FIELDS\", \"0\")\n\t\t\tc.Error(\"wrong number\", \"HEXPIRE\", \"aap\", \"noot\", \"3\")\n\t\t\tc.Error(\"not an int\", \"HEXPIRE\", \"aap\", \"3.14\", \"FIELDS\", \"noot\", \"3.14\")\n\t\t\tc.Error(\"numfields\", \"HEXPIRE\", \"aap\", \"3\", \"FIELDS\", \"3\", \"noot\", \"vuur\")\n\t\t})\n\t})\n}\n\nfunc TestHashSetnx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HSETNX\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"EXISTS\", \"aap\")\n\t\tc.Do(\"HEXISTS\", \"aap\", \"noot\")\n\n\t\tc.Do(\"HSETNX\", \"aap\", \"noot\", \"mies2\")\n\t\tc.Do(\"HGET\", \"aap\", \"noot\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"HSETNX\", \"aap\")\n\t\tc.Error(\"wrong number\", \"HSETNX\", \"aap\", \"noot\")\n\t\tc.Error(\"wrong number\", \"HSETNX\", \"aap\", \"noot\", \"too\", \"many\")\n\t})\n}\n\nfunc TestHashDelExists(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"HSET\", \"aap\", \"vuur\", \"wim\")\n\t\tc.Do(\"HEXISTS\", \"aap\", \"noot\")\n\t\tc.Do(\"HEXISTS\", \"aap\", \"vuur\")\n\t\tc.Do(\"HDEL\", \"aap\", \"noot\")\n\t\tc.Do(\"HEXISTS\", \"aap\", \"noot\")\n\t\tc.Do(\"HEXISTS\", \"aap\", \"vuur\")\n\n\t\tc.Do(\"HEXISTS\", \"nosuch\", \"vuur\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"HDEL\")\n\t\tc.Error(\"wrong number\", \"HDEL\", \"aap\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"HDEL\", \"str\", \"key\")\n\n\t\tc.Error(\"wrong number\", \"HEXISTS\")\n\t\tc.Error(\"wrong number\", \"HEXISTS\", \"aap\")\n\t\tc.Error(\"wrong number\", \"HEXISTS\", \"aap\", \"too\", \"many\")\n\t\tc.Error(\"wrong kind\", \"HEXISTS\", \"str\", \"field\")\n\t})\n}\n\nfunc TestHashGetall(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"HSET\", \"aap\", \"vuur\", \"wim\")\n\t\tc.DoSorted(\"HGETALL\", \"aap\")\n\n\t\tc.Do(\"HGETALL\", \"nosuch\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"HGETALL\")\n\t\tc.Error(\"wrong number\", \"HGETALL\", \"too\", \"many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"HGETALL\", \"str\")\n\t})\n\n\ttestRESP3(t, func(c *client) {\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"HGETALL\", \"aap\")\n\t\tc.Do(\"HSET\", \"aap\", \"vuur\", \"wim\")\n\t\tc.DoSorted(\"HGETALL\", \"aap\")\n\n\t\tc.Do(\"HGETALL\", \"nosuch\")\n\t})\n}\n\nfunc TestHmset(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HMSET\", \"aap\", \"noot\", \"mies\", \"vuur\", \"zus\")\n\t\tc.Do(\"HGET\", \"aap\", \"noot\")\n\t\tc.Do(\"HGET\", \"aap\", \"vuur\")\n\t\tc.Do(\"HLEN\", \"aap\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"HMSET\", \"aap\")\n\t\tc.Error(\"wrong number\", \"HMSET\", \"aap\", \"key\")\n\t\tc.Error(\"wrong number\", \"HMSET\", \"aap\", \"key\", \"value\", \"odd\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"HMSET\", \"str\", \"key\", \"value\")\n\t})\n}\n\nfunc TestHashIncr(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HINCRBY\", \"aap\", \"noot\", \"12\")\n\t\tc.Do(\"HINCRBY\", \"aap\", \"noot\", \"-13\")\n\t\tc.Do(\"HINCRBY\", \"aap\", \"noot\", \"2123\")\n\t\tc.Do(\"HGET\", \"aap\", \"noot\")\n\n\t\t// Simple failure cases.\n\t\tc.Error(\"wrong number\", \"HINCRBY\")\n\t\tc.Error(\"wrong number\", \"HINCRBY\", \"aap\")\n\t\tc.Error(\"wrong number\", \"HINCRBY\", \"aap\", \"noot\")\n\t\tc.Error(\"not an integer\", \"HINCRBY\", \"aap\", \"noot\", \"noint\")\n\t\tc.Error(\"wrong number\", \"HINCRBY\", \"aap\", \"noot\", \"12\", \"toomany\")\n\t\tc.Do(\"SET\", \"str\", \"value\")\n\t\tc.Error(\"wrong kind\", \"HINCRBY\", \"str\", \"value\", \"12\")\n\t\tc.Do(\"HINCRBY\", \"aap\", \"noot\", \"12\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HINCRBYFLOAT\", \"aap\", \"noot\", \"12.3\")\n\t\tc.Do(\"HINCRBYFLOAT\", \"aap\", \"noot\", \"-13.1\")\n\t\tc.Do(\"HINCRBYFLOAT\", \"aap\", \"noot\", \"200\")\n\t\tc.Do(\"HGET\", \"aap\", \"noot\")\n\n\t\t// Simple failure cases.\n\t\tc.Error(\"wrong number\", \"HINCRBYFLOAT\")\n\t\tc.Error(\"wrong number\", \"HINCRBYFLOAT\", \"aap\")\n\t\tc.Error(\"wrong number\", \"HINCRBYFLOAT\", \"aap\", \"noot\")\n\t\tc.Error(\"not a valid float\", \"HINCRBYFLOAT\", \"aap\", \"noot\", \"noint\")\n\t\tc.Error(\"wrong number\", \"HINCRBYFLOAT\", \"aap\", \"noot\", \"12\", \"toomany\")\n\t\tc.Do(\"SET\", \"str\", \"value\")\n\t\tc.Error(\"wrong kind\", \"HINCRBYFLOAT\", \"str\", \"value\", \"12\")\n\t\tc.Do(\"HINCRBYFLOAT\", \"aap\", \"noot\", \"12\")\n\t})\n}\n\nfunc TestHscan(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// No set yet\n\t\tc.Do(\"HSCAN\", \"h\", \"0\")\n\n\t\tc.Do(\"HSET\", \"h\", \"key1\", \"value1\")\n\t\tc.Do(\"HSCAN\", \"h\", \"0\")\n\t\tc.Do(\"HSCAN\", \"h\", \"0\", \"COUNT\", \"12\")\n\t\tc.Do(\"HSCAN\", \"h\", \"0\", \"cOuNt\", \"12\")\n\n\t\tc.Do(\"HSET\", \"h\", \"anotherkey\", \"value2\")\n\t\tc.Do(\"HSCAN\", \"h\", \"0\", \"MATCH\", \"anoth*\")\n\t\tc.Do(\"HSCAN\", \"h\", \"0\", \"MATCH\", \"anoth*\", \"COUNT\", \"100\")\n\t\tc.Do(\"HSCAN\", \"h\", \"0\", \"COUNT\", \"100\", \"MATCH\", \"anoth*\")\n\n\t\t// Can't really test multiple keys.\n\t\t// c.Do(\"SET\", \"key2\", \"value2\")\n\t\t// c.Do(\"SCAN\", \"0\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"HSCAN\")\n\t\tc.Error(\"wrong number\", \"HSCAN\", \"noint\")\n\t\tc.Error(\"not an integer\", \"HSCAN\", \"h\", \"0\", \"COUNT\", \"noint\")\n\t\tc.Error(\"syntax error\", \"HSCAN\", \"h\", \"0\", \"COUNT\")\n\t\tc.Error(\"syntax error\", \"HSCAN\", \"h\", \"0\", \"MATCH\")\n\t\tc.Error(\"syntax error\", \"HSCAN\", \"h\", \"0\", \"garbage\")\n\t\tc.Error(\"syntax error\", \"HSCAN\", \"h\", \"0\", \"COUNT\", \"12\", \"MATCH\", \"foo\", \"garbage\")\n\t\t// c.Do(\"HSCAN\", \"nosuch\", \"0\", \"COUNT\", \"garbage\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"HSCAN\", \"str\", \"0\")\n\t})\n}\n\nfunc TestHstrlen(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HSTRLEN\", \"hash\", \"foo\")\n\t\tc.Do(\"HSET\", \"hash\", \"foo\", \"bar\")\n\t\tc.Do(\"HSTRLEN\", \"hash\", \"foo\")\n\t\tc.Do(\"HSTRLEN\", \"hash\", \"nosuch\")\n\t\tc.Do(\"HSTRLEN\", \"nosuch\", \"nosuch\")\n\n\t\tc.Error(\"wrong number\", \"HSTRLEN\")\n\t\tc.Error(\"wrong number\", \"HSTRLEN\", \"foo\")\n\t\tc.Error(\"wrong number\", \"HSTRLEN\", \"foo\", \"baz\", \"bar\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"HSTRLEN\", \"str\", \"bar\")\n\t})\n}\n\nfunc TestHrandfield(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HSET\", \"one\", \"foo\", \"bar\")\n\t\tc.Do(\"HRANDFIELD\", \"one\")\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"0\")\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"1\")\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"2\") // limited to 1\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"3\") // limited to 1\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"-1\")\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"-2\") // padded\n\t\tc.Do(\"HRANDFIELD\", \"one\", \"-3\") // padded\n\n\t\tc.Do(\"HSET\", \"more\", \"foo\", \"bar\", \"baz\", \"bak\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\")\n\t\tc.Do(\"HRANDFIELD\", \"more\", \"0\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\", \"1\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\", \"2\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\", \"3\") // limited to 2\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\", \"-1\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\", \"-2\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"more\", \"-3\") // length padded to 3\n\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"1\")\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"2\")\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"3\")\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"0\")\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\")\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"-1\") // still empty\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"-2\") // still empty\n\t\tc.Do(\"HRANDFIELD\", \"nosuch\", \"-3\") // still empty\n\t\tc.DoLoosely(\"HRANDFIELD\", \"one\", \"2\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"one\", \"7\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"one\", \"2\", \"WITHVALUE\")\n\t\tc.DoLoosely(\"HRANDFIELD\", \"one\", \"7\", \"WITHVALUE\")\n\n\t\tc.Error(\"ERR syntax error\", \"HRANDFIELD\", \"foo\", \"1\", \"2\")\n\t\tc.Error(\"ERR wrong number\", \"HRANDFIELD\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/hll_test.go",
    "content": "package main\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\nfunc TestHll(t *testing.T) {\n\tskip(t)\n\tt.Run(\"basics\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\t// Add 100 unique random values to h1 and 50 of these 100 to h2\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tvalue := randomStr(10)\n\t\t\t\tc.Do(\"PFADD\", \"h1\", value)\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\tc.Do(\"PFADD\", \"h2\", value)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tc.Do(\"PFADD\", \"h3\", randomStr(10))\n\t\t\t}\n\n\t\t\t// Merge non-intersecting hlls\n\t\t\t{\n\t\t\t\tc.Do(\n\t\t\t\t\t\"PFMERGE\",\n\t\t\t\t\t\"res1\",\n\t\t\t\t\t\"h1\", // count 100\n\t\t\t\t\t\"h3\", // count 100\n\t\t\t\t)\n\t\t\t\tc.DoApprox(2, \"PFCOUNT\", \"res1\")\n\t\t\t}\n\n\t\t\t// Merge intersecting hlls\n\t\t\t{\n\t\t\t\tc.Do(\n\t\t\t\t\t\"PFMERGE\",\n\t\t\t\t\t\"res2\",\n\t\t\t\t\t\"h1\", // count 100\n\t\t\t\t\t\"h2\", // count 50 (all 50 are presented in h1)\n\t\t\t\t)\n\t\t\t\tc.DoApprox(2, \"PFCOUNT\", \"res2\")\n\t\t\t}\n\n\t\t\t// Merge all hlls\n\t\t\t{\n\t\t\t\tc.Do(\n\t\t\t\t\t\"PFMERGE\",\n\t\t\t\t\t\"res3\",\n\t\t\t\t\t\"h1\", // count 100\n\t\t\t\t\t\"h2\", // count 50 (all 50 are presented in h1)\n\t\t\t\t\t\"h3\", // count 100\n\t\t\t\t\t\"h4\", // empty key\n\t\t\t\t)\n\t\t\t\tc.DoApprox(2, \"PFCOUNT\", \"res3\")\n\t\t\t}\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"PFADD\")\n\t\t\tc.Error(\"wrong number\", \"PFCOUNT\")\n\t\t\tc.Error(\"wrong number\", \"PFMERGE\")\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"not a valid HyperLogLog\", \"PFADD\", \"str\", \"noot\", \"mies\")\n\t\t\tc.Error(\"not a valid HyperLogLog\", \"PFCOUNT\", \"str\", \"h1\")\n\t\t\tc.Error(\"not a valid HyperLogLog\", \"PFMERGE\", \"str\", \"noot\")\n\t\t\tc.Error(\"not a valid HyperLogLog\", \"PFMERGE\", \"noot\", \"str\")\n\n\t\t\tc.Do(\"DEL\", \"h1\", \"h2\", \"h3\", \"h4\", \"res1\", \"res2\", \"res3\")\n\t\t\tc.Do(\"PFCOUNT\", \"h1\", \"h2\", \"h3\", \"h4\", \"res1\", \"res2\", \"res3\")\n\t\t})\n\t})\n\n\tt.Run(\"tx\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"PFADD\", \"h1\", \"noot\", \"mies\", \"vuur\", \"wim\")\n\t\t\tc.Do(\"PFADD\", \"h2\", \"noot1\", \"mies1\", \"vuur1\", \"wim1\")\n\t\t\tc.Do(\"PFMERGE\", \"h3\", \"h1\", \"h2\")\n\t\t\tc.Do(\"PFCOUNT\", \"h1\")\n\t\t\tc.Do(\"PFCOUNT\", \"h2\")\n\t\t\tc.Do(\"PFCOUNT\", \"h3\")\n\t\t\tc.Do(\"EXEC\")\n\t\t})\n\t})\n}\n\nconst letters = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\nfunc randomStr(length int) string {\n\trand.Seed(42)\n\tb := make([]byte, length)\n\tfor i := range b {\n\t\tb[i] = letters[rand.Intn(len(letters))]\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/list_test.go",
    "content": "package main\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestLPushLpop(t *testing.T) {\n\tskip(t)\n\tt.Run(\"without count\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"TYPE\", \"l\")\n\t\t\tc.Do(\"LPUSH\", \"l\", \"more\", \"keys\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"6\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"2\", \"6\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"-100\", \"-100\")\n\t\t\tc.Do(\"LRANGE\", \"nosuch\", \"2\", \"6\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"EXISTS\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"nosuch\")\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"LPUSH\")\n\t\t\tc.Error(\"wrong number\", \"LPUSH\", \"l\")\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"wrong kind\", \"LPUSH\", \"str\", \"noot\", \"mies\")\n\t\t\tc.Error(\"wrong number\", \"LRANGE\")\n\t\t\tc.Error(\"wrong number\", \"LRANGE\", \"key\")\n\t\t\tc.Error(\"wrong number\", \"LRANGE\", \"key\", \"2\")\n\t\t\tc.Error(\"wrong number\", \"LRANGE\", \"key\", \"2\", \"6\", \"toomany\")\n\t\t\tc.Error(\"not an integer\", \"LRANGE\", \"key\", \"noint\", \"6\")\n\t\t\tc.Error(\"not an integer\", \"LRANGE\", \"key\", \"2\", \"noint\")\n\t\t\tc.Error(\"wrong number\", \"LPOP\")\n\t\t})\n\t})\n\n\tt.Run(\"with count\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"LPOP\", \"l\", \"0\")\n\t\t\tc.Do(\"LPOP\", \"l\", \"2\")\n\t\t\tc.Do(\"LPOP\", \"l\", \"2\")\n\t\t\tc.Error(\"out of range\", \"LPOP\", \"l\", \"-42\")\n\t\t\tc.Do(\"LPOP\", \"nosuch\", \"2\")\n\n\t\t\tc.Error(\"wrong number\", \"LPOP\", \"nosuch\", \"2\", \"foobar\")\n\t\t})\n\t})\n\n\tt.Run(\"resp3\", func(t *testing.T) {\n\t\ttestRESP3(t, func(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\", \"9\")\n\t\t\tc.Do(\"LPOP\", \"l\")\n\t\t\tc.Do(\"LPOP\", \"l\", \"9\")\n\t\t\tc.Do(\"LPOP\", \"nosuch\")\n\t\t\tc.Do(\"LPOP\", \"nosuch\", \"9\")\n\t\t})\n\t})\n}\n\nfunc TestLPushx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"LPUSHX\", \"l\", \"aap\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"LPUSH\", \"l\", \"noot\")\n\t\tc.Do(\"LPUSHX\", \"l\", \"mies\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"LPUSHX\", \"l\", \"even\", \"more\", \"arguments\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"LPUSHX\")\n\t\tc.Error(\"wrong number\", \"LPUSHX\", \"l\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LPUSHX\", \"str\", \"mies\")\n\t})\n}\n\nfunc TestRPushRPop(t *testing.T) {\n\tskip(t)\n\tt.Run(\"without count\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"TYPE\", \"l\")\n\t\t\tc.Do(\"RPUSH\", \"l\", \"more\", \"keys\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"6\")\n\t\t\tc.Do(\"LRANGE\", \"l\", \"2\", \"6\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"EXISTS\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"nosuch\")\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"RPUSH\")\n\t\t\tc.Error(\"wrong number\", \"RPUSH\", \"l\")\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"wrong kind\", \"RPUSH\", \"str\", \"noot\", \"mies\")\n\t\t\tc.Error(\"wrong number\", \"RPOP\")\n\t\t})\n\t})\n\n\tt.Run(\"with count\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"RPOP\", \"l\", \"0\")\n\t\t\tc.Do(\"RPOP\", \"l\", \"2\")\n\t\t\tc.Do(\"RPOP\", \"l\", \"99\")\n\t\t\tc.Do(\"RPOP\", \"l\", \"99\")\n\t\t\tc.Do(\"RPOP\", \"nosuch\", \"99\")\n\t\t})\n\t})\n\n\tt.Run(\"resp3\", func(t *testing.T) {\n\t\ttestRESP3(t, func(c *client) {\n\t\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\", \"9\")\n\t\t\tc.Do(\"RPOP\", \"l\")\n\t\t\tc.Do(\"RPOP\", \"l\", \"9\")\n\t\t})\n\t})\n}\n\nfunc TestLinxed(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"LINDEX\", \"l\", \"0\")\n\t\tc.Do(\"LINDEX\", \"l\", \"1\")\n\t\tc.Do(\"LINDEX\", \"l\", \"2\")\n\t\tc.Do(\"LINDEX\", \"l\", \"3\")\n\t\tc.Do(\"LINDEX\", \"l\", \"4\")\n\t\tc.Do(\"LINDEX\", \"l\", \"44444\")\n\t\tc.Error(\"not an integer\", \"LINDEX\", \"l\", \"-0\")\n\t\tc.Do(\"LINDEX\", \"l\", \"-1\")\n\t\tc.Do(\"LINDEX\", \"l\", \"-2\")\n\t\tc.Do(\"LINDEX\", \"l\", \"-3\")\n\t\tc.Do(\"LINDEX\", \"l\", \"-4\")\n\t\tc.Do(\"LINDEX\", \"l\", \"-4000\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"LINDEX\")\n\t\tc.Error(\"wrong number\", \"LINDEX\", \"l\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LINDEX\", \"str\", \"1\")\n\t\tc.Error(\"not an integer\", \"LINDEX\", \"l\", \"noint\")\n\t\tc.Error(\"wrong number\", \"LINDEX\", \"l\", \"1\", \"too many\")\n\t})\n}\n\nfunc TestLpos(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"aap\", \"mies\", \"aap\", \"vuur\", \"aap\", \"aap\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\")\n\t\tc.Do(\"LPOS\", \"l\", \"noot\")\n\t\tc.Do(\"LPOS\", \"l\", \"mies\")\n\t\tc.Do(\"LPOS\", \"l\", \"vuur\")\n\t\tc.Do(\"LPOS\", \"l\", \"wim\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"1\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"4\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"5\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"6\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"-1\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"-4\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"-5\")\n\t\tc.Do(\"LPOS\", \"l\", \"app\", \"RANK\", \"-6\")\n\t\tc.Do(\"LPOS\", \"l\", \"wim\", \"COUNT\", \"1\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"1\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"3\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"5\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"100\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"0\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"3\", \"COUNT\", \"2\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"3\", \"COUNT\", \"3\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"5\", \"COUNT\", \"100\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-3\", \"COUNT\", \"2\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-3\", \"COUNT\", \"3\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-5\", \"COUNT\", \"100\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"4\", \"MAXLEN\", \"6\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"4\", \"MAXLEN\", \"7\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-4\", \"MAXLEN\", \"5\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-4\", \"MAXLEN\", \"6\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"0\", \"MAXLEN\", \"1\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"0\", \"MAXLEN\", \"4\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"0\", \"MAXLEN\", \"7\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"0\", \"MAXLEN\", \"8\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"2\", \"MAXLEN\", \"0\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"COUNT\", \"1\", \"MAXLEN\", \"0\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"4\", \"COUNT\", \"2\", \"MAXLEN\", \"0\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"4\", \"COUNT\", \"2\", \"MAXLEN\", \"7\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"4\", \"COUNT\", \"2\", \"MAXLEN\", \"6\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-3\", \"COUNT\", \"2\", \"MAXLEN\", \"0\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-3\", \"COUNT\", \"2\", \"MAXLEN\", \"4\")\n\t\tc.Do(\"LPOS\", \"l\", \"aap\", \"RANK\", \"-3\", \"COUNT\", \"2\", \"MAXLEN\", \"3\")\n\n\t\t// failure cases\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LPOS\", \"str\", \"aap\")\n\t\tc.Error(\"wrong number\", \"LPOS\", \"l\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANK\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"1\", \"COUNT\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"1\", \"COUNT\", \"1\", \"MAXLEN\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"1\", \"COUNT\", \"1\", \"MAXLEN\", \"1\", \"RANK\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANKS\", \"1\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"1\", \"COUNTING\", \"1\")\n\t\tc.Error(\"syntax error\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"1\", \"MAXLENGTH\", \"1\")\n\t\tc.Error(\"not an integer\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"not_an_int\")\n\t\tc.Error(\"can't be zero\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"0\")\n\t\tc.Error(\"can't be negative\", \"LPOS\", \"l\", \"aap\", \"COUNT\", \"-1\")\n\t\tc.Error(\"can't be negative\", \"LPOS\", \"l\", \"aap\", \"COUNT\", \"not_an_int\")\n\t\tc.Error(\"can't be negative\", \"LPOS\", \"l\", \"aap\", \"MAXLEN\", \"-1\")\n\t\tc.Error(\"can't be negative\", \"LPOS\", \"l\", \"aap\", \"MAXLEN\", \"not_an_int\")\n\t\tc.Error(\"can't be negative\", \"LPOS\", \"l\", \"aap\", \"MAXLEN\", \"-1\", \"RANK\", \"not_an_int\", \"COUNT\", \"-1\")\n\t\tc.Error(\"not an integer\", \"LPOS\", \"l\", \"aap\", \"RANK\", \"not_an_int\", \"COUNT\", \"-1\", \"MAXLEN\", \"-1\")\n\t\tc.Error(\"can't be negative\", \"LPOS\", \"l\", \"aap\", \"COUNT\", \"-1\", \"MAXLEN\", \"-1\", \"RANK\", \"not_an_int\")\n\t})\n}\n\nfunc TestLlen(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"LLEN\", \"l\")\n\t\tc.Do(\"LLEN\", \"nosuch\")\n\n\t\t// failure cases\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LLEN\", \"str\")\n\t\tc.Error(\"wrong number\", \"LLEN\")\n\t\tc.Error(\"wrong number\", \"LLEN\", \"l\", \"too many\")\n\t})\n}\n\nfunc TestLtrim(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"LTRIM\", \"l\", \"0\", \"1\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"RPUSH\", \"l2\", \"aap\", \"noot\", \"mies\", \"vuur\")\n\t\tc.Do(\"LTRIM\", \"l2\", \"-2\", \"-1\")\n\t\tc.Do(\"LRANGE\", \"l2\", \"0\", \"-1\")\n\t\tc.Do(\"RPUSH\", \"l3\", \"aap\", \"noot\", \"mies\", \"vuur\")\n\t\tc.Do(\"LTRIM\", \"l3\", \"-2\", \"-1000\")\n\t\tc.Do(\"LRANGE\", \"l3\", \"0\", \"-1\")\n\n\t\t// remove the list\n\t\tc.Do(\"RPUSH\", \"l4\", \"aap\")\n\t\tc.Do(\"LTRIM\", \"l4\", \"0\", \"-999\")\n\t\tc.Do(\"EXISTS\", \"l4\")\n\n\t\t// failure cases\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LTRIM\", \"str\", \"0\", \"1\")\n\t\tc.Error(\"wrong number\", \"LTRIM\", \"l\", \"0\", \"1\", \"toomany\")\n\t\tc.Error(\"not an integer\", \"LTRIM\", \"l\", \"noint\", \"1\")\n\t\tc.Error(\"not an integer\", \"LTRIM\", \"l\", \"0\", \"noint\")\n\t\tc.Error(\"wrong number\", \"LTRIM\", \"l\", \"0\")\n\t\tc.Error(\"wrong number\", \"LTRIM\", \"l\")\n\t\tc.Error(\"wrong number\", \"LTRIM\")\n\t})\n}\n\nfunc TestLrem(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\", \"mies\", \"mies\")\n\t\tc.Do(\"LREM\", \"l\", \"1\", \"mies\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"RPUSH\", \"l2\", \"aap\", \"noot\", \"mies\", \"mies\", \"mies\")\n\t\tc.Do(\"LREM\", \"l2\", \"-2\", \"mies\")\n\t\tc.Do(\"LRANGE\", \"l2\", \"0\", \"-1\")\n\t\tc.Do(\"RPUSH\", \"l3\", \"aap\", \"noot\", \"mies\", \"mies\", \"mies\")\n\t\tc.Do(\"LREM\", \"l3\", \"0\", \"mies\")\n\t\tc.Do(\"LRANGE\", \"l3\", \"0\", \"-1\")\n\n\t\t// remove the list\n\t\tc.Do(\"RPUSH\", \"l4\", \"aap\")\n\t\tc.Do(\"LREM\", \"l4\", \"999\", \"aap\")\n\t\tc.Do(\"EXISTS\", \"l4\")\n\n\t\t// failure cases\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LREM\", \"str\", \"0\", \"aap\")\n\t\tc.Error(\"wrong number\", \"LREM\", \"l\", \"0\", \"aap\", \"toomany\")\n\t\tc.Error(\"not an integer\", \"LREM\", \"l\", \"noint\", \"aap\")\n\t\tc.Error(\"wrong number\", \"LREM\", \"l\", \"0\")\n\t\tc.Error(\"wrong number\", \"LREM\", \"l\")\n\t\tc.Error(\"wrong number\", \"LREM\")\n\t})\n}\n\nfunc TestLset(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\", \"mies\", \"mies\")\n\t\tc.Do(\"LSET\", \"l\", \"1\", \"[cencored]\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"LSET\", \"l\", \"-1\", \"[cencored]\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Error(\"out of range\", \"LSET\", \"l\", \"1000\", \"new\")\n\t\tc.Error(\"out of range\", \"LSET\", \"l\", \"-7000\", \"new\")\n\t\tc.Error(\"no such key\", \"LSET\", \"nosuch\", \"1\", \"new\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"LSET\")\n\t\tc.Error(\"wrong number\", \"LSET\", \"l\")\n\t\tc.Error(\"wrong number\", \"LSET\", \"l\", \"0\")\n\t\tc.Error(\"not an integer\", \"LSET\", \"l\", \"noint\", \"aap\")\n\t\tc.Error(\"wrong number\", \"LSET\", \"l\", \"0\", \"aap\", \"toomany\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LSET\", \"str\", \"0\", \"aap\")\n\t})\n}\n\nfunc TestLinsert(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\", \"mies\", \"mies!\")\n\t\tc.Do(\"LINSERT\", \"l\", \"before\", \"aap\", \"1\")\n\t\tc.Do(\"LINSERT\", \"l\", \"before\", \"noot\", \"2\")\n\t\tc.Do(\"LINSERT\", \"l\", \"after\", \"mies!\", \"3\")\n\t\tc.Do(\"LINSERT\", \"l\", \"after\", \"mies\", \"4\")\n\t\tc.Do(\"LINSERT\", \"l\", \"after\", \"nosuch\", \"0\")\n\t\tc.Do(\"LINSERT\", \"nosuch\", \"after\", \"nosuch\", \"0\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"LINSERT\", \"l\", \"AfTeR\", \"mies\", \"4\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"LINSERT\")\n\t\tc.Error(\"wrong number\", \"LINSERT\", \"l\")\n\t\tc.Error(\"wrong number\", \"LINSERT\", \"l\", \"before\")\n\t\tc.Error(\"wrong number\", \"LINSERT\", \"l\", \"before\", \"aap\")\n\t\tc.Error(\"wrong number\", \"LINSERT\", \"l\", \"before\", \"aap\", \"too\", \"many\")\n\t\tc.Error(\"syntax error\", \"LINSERT\", \"l\", \"What?\", \"aap\", \"noot\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LINSERT\", \"str\", \"before\", \"aap\", \"noot\")\n\t})\n}\n\nfunc TestRpoplpush(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"l\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"RPOPLPUSH\", \"l\", \"l2\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"LRANGE\", \"2l\", \"0\", \"-1\")\n\t\tc.Do(\"RPOPLPUSH\", \"l\", \"l2\")\n\t\tc.Do(\"RPOPLPUSH\", \"l\", \"l2\")\n\t\tc.Do(\"RPOPLPUSH\", \"l\", \"l2\") // now empty\n\t\tc.Do(\"EXISTS\", \"l\")\n\t\tc.Do(\"LRANGE\", \"2l\", \"0\", \"-1\")\n\n\t\tc.Do(\"RPUSH\", \"round\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"RPOPLPUSH\", \"round\", \"round\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"RPOPLPUSH\", \"round\", \"round\")\n\t\tc.Do(\"RPOPLPUSH\", \"round\", \"round\")\n\t\tc.Do(\"RPOPLPUSH\", \"round\", \"round\")\n\t\tc.Do(\"RPOPLPUSH\", \"round\", \"round\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Do(\"RPUSH\", \"chk\", \"aap\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong number\", \"RPOPLPUSH\")\n\t\tc.Error(\"wrong number\", \"RPOPLPUSH\", \"chk\")\n\t\tc.Error(\"wrong number\", \"RPOPLPUSH\", \"chk\", \"too\", \"many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"RPOPLPUSH\", \"chk\", \"str\")\n\t\tc.Error(\"wrong kind\", \"RPOPLPUSH\", \"str\", \"chk\")\n\t\tc.Do(\"LRANGE\", \"chk\", \"0\", \"-1\")\n\t})\n}\n\nfunc TestRpushx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSHX\", \"l\", \"aap\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\t\tc.Do(\"RPUSH\", \"l\", \"noot\", \"mies\")\n\t\tc.Do(\"RPUSHX\", \"l\", \"vuur\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"RPUSHX\", \"l\", \"more\", \"arguments\")\n\n\t\t// failure cases\n\t\tc.Do(\"RPUSH\", \"chk\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong number\", \"RPUSHX\")\n\t\tc.Error(\"wrong number\", \"RPUSHX\", \"chk\")\n\t\tc.Do(\"LRANGE\", \"chk\", \"0\", \"-1\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"RPUSHX\", \"str\", \"value\")\n\t})\n}\n\nfunc TestBrpop(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"LPUSH\", \"l\", \"one\")\n\t\tc.Do(\"BRPOP\", \"l\", \"1\")\n\t\tc.Do(\"BRPOP\", \"l\", \"0.1\")\n\t\tc.Error(\"timeout is out of range\", \"BRPOP\", \"l\", \"inf\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\n\t\t// transaction\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"BRPOP\", \"nosuch\", \"10\")\n\t\tc.Do(\"EXEC\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"BRPOP\")\n\t\tc.Error(\"wrong number\", \"BRPOP\", \"l\")\n\t\tc.Error(\"not a float\", \"BRPOP\", \"l\", \"X\")\n\t\tc.Error(\"not a float\", \"BRPOP\", \"l\", \"\")\n\t\tc.Error(\"wrong number\", \"BRPOP\", \"1\")\n\t\tc.Error(\"timeout is negative\", \"BRPOP\", \"key\", \"-1\")\n\t})\n}\n\nfunc TestBrpopMulti(t *testing.T) {\n\tskip(t)\n\ttestMulti(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"BRPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BRPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BRPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BRPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BRPOP\", \"key\", \"1\") // will timeout\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"key\", \"aap\", \"noot\", \"mies\")\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\tc.Do(\"LPUSH\", \"key\", \"toon\")\n\t\t},\n\t)\n}\n\nfunc TestBrpopTrans(t *testing.T) {\n\tskip(t)\n\ttestMulti(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"BRPOP\", \"key\", \"1\")\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"LPUSH\", \"key\", \"toon\")\n\t\t\tc.Do(\"EXEC\")\n\t\t},\n\t)\n}\n\nfunc TestBlpop(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"LPUSH\", \"l\", \"one\")\n\t\tc.Do(\"BLPOP\", \"l\", \"1\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"BLPOP\")\n\t\tc.Error(\"wrong number\", \"BLPOP\", \"l\")\n\t\tc.Error(\"not a float\", \"BLPOP\", \"l\", \"X\")\n\t\tc.Error(\"not a float\", \"BLPOP\", \"l\", \"\")\n\t\tc.Error(\"wrong number\", \"BLPOP\", \"1\")\n\t\tc.Error(\"timeout is negative\", \"BLPOP\", \"key\", \"-1\")\n\t})\n\n\ttestMulti(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"BLPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BLPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BLPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BLPOP\", \"key\", \"1\")\n\t\t\tc.Do(\"BLPOP\", \"key\", \"1\") // will timeout\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"key\", \"aap\", \"noot\", \"mies\")\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\tc.Do(\"LPUSH\", \"key\", \"toon\")\n\t\t},\n\t)\n}\n\nfunc TestBrpoplpush(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"LPUSH\", \"l\", \"one\")\n\t\tc.Do(\"BRPOPLPUSH\", \"l\", \"l2\", \"0.1\")\n\t\tc.Do(\"EXISTS\", \"l\")\n\t\tc.Do(\"EXISTS\", \"l2\")\n\t\tc.Do(\"LRANGE\", \"l\", \"0\", \"-1\")\n\t\tc.Do(\"LRANGE\", \"l2\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"BRPOPLPUSH\")\n\t\tc.Error(\"wrong number\", \"BRPOPLPUSH\", \"l\")\n\t\tc.Error(\"wrong number\", \"BRPOPLPUSH\", \"l\", \"x\")\n\t\tc.Error(\"wrong number\", \"BRPOPLPUSH\", \"1\")\n\t\tc.Error(\"timeout is negative\", \"BRPOPLPUSH\", \"from\", \"to\", \"-1\")\n\t\tc.Error(\"out of range\", \"BRPOPLPUSH\", \"from\", \"to\", \"inf\")\n\t\tc.Error(\"wrong number\", \"BRPOPLPUSH\", \"from\", \"to\", \"-1\", \"xxx\")\n\t})\n\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\ttestMulti(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"BRPOPLPUSH\", \"from\", \"to\", \"1\")\n\t\t\tc.Do(\"BRPOPLPUSH\", \"from\", \"to\", \"1\")\n\t\t\tc.Do(\"BRPOPLPUSH\", \"from\", \"to\", \"1\")\n\t\t\tc.Do(\"BRPOPLPUSH\", \"from\", \"to\", \"1\")\n\t\t\tc.Do(\"BRPOPLPUSH\", \"from\", \"to\", \"1\") // will timeout\n\t\t\twg.Done()\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"from\", \"aap\", \"noot\", \"mies\")\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t\tc.Do(\"LPUSH\", \"from\", \"toon\")\n\t\t\twg.Wait()\n\t\t\tc.Do(\"LRANGE\", \"from\", \"0\", \"-1\")\n\t\t\tc.Do(\"LRANGE\", \"to\", \"0\", \"-1\")\n\t\t},\n\t)\n}\n\nfunc TestLmove(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"src\", \"LR\", \"LL\", \"RR\", \"RL\")\n\t\tc.Do(\"LMOVE\", \"src\", \"dst\", \"LEFT\", \"RIGHT\")\n\t\tc.Do(\"LRANGE\", \"src\", \"0\", \"-1\")\n\t\tc.Do(\"LRANGE\", \"dst\", \"0\", \"-1\")\n\t\tc.Do(\"LMOVE\", \"src\", \"dst\", \"RIGHT\", \"LEFT\")\n\t\tc.Do(\"LMOVE\", \"src\", \"dst\", \"LEFT\", \"LEFT\")\n\t\tc.Do(\"LMOVE\", \"src\", \"dst\", \"RIGHT\", \"RIGHT\") // now empty\n\t\tc.Do(\"EXISTS\", \"src\")\n\t\tc.Do(\"LRANGE\", \"dst\", \"0\", \"-1\")\n\n\t\t// Cycle left to right\n\t\tc.Do(\"RPUSH\", \"round\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\t// Cycle right to left\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\t// Cycle same side\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"LEFT\", \"LEFT\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"LMOVE\", \"round\", \"round\", \"RIGHT\", \"RIGHT\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Do(\"RPUSH\", \"chk\", \"aap\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong number\", \"LMOVE\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\", \"dst\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\", \"dst\", \"chk\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\", \"dst\", \"chk\", \"too\", \"many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"LMOVE\", \"chk\", \"str\", \"LEFT\", \"LEFT\")\n\t\tc.Error(\"wrong kind\", \"LMOVE\", \"str\", \"chk\", \"LEFT\", \"LEFT\")\n\t\tc.Do(\"LRANGE\", \"chk\", \"0\", \"-1\")\n\t})\n}\n\nfunc TestBlmove(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"RPUSH\", \"src\", \"LR\", \"LL\", \"RR\", \"RL\")\n\t\tc.Do(\"BLMOVE\", \"src\", \"dst\", \"LEFT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"src\", \"0\", \"-1\")\n\t\tc.Do(\"LRANGE\", \"dst\", \"0\", \"-1\")\n\t\tc.Do(\"BLMOVE\", \"src\", \"dst\", \"RIGHT\", \"LEFT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"src\", \"dst\", \"LEFT\", \"LEFT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"src\", \"dst\", \"RIGHT\", \"RIGHT\", \"0\") // now empty\n\t\tc.Do(\"EXISTS\", \"src\")\n\t\tc.Do(\"LRANGE\", \"dst\", \"0\", \"-1\")\n\n\t\t// Cycle left to right\n\t\tc.Do(\"RPUSH\", \"round\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"LEFT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\t// Cycle right to left\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\", \"0\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"RIGHT\", \"LEFT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\t// Cycle same side\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"LEFT\", \"LEFT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\t\tc.Do(\"BLMOVE\", \"round\", \"round\", \"RIGHT\", \"RIGHT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"round\", \"0\", \"-1\")\n\n\t\t// TTL\n\t\tc.Do(\"LPUSH\", \"test\", \"1\")\n\t\tc.Do(\"EXPIRE\", \"test\", \"1000\")\n\t\tc.Do(\"TTL\", \"test\")\n\t\tc.Do(\"BLMOVE\", \"test\", \"test\", \"LEFT\", \"LEFT\", \"1\")\n\t\tc.Do(\"TTL\", \"test\")\n\n\t\t// failure cases\n\t\tc.Do(\"RPUSH\", \"chk\", \"aap\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong number\", \"LMOVE\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\", \"dst\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\", \"dst\", \"chk\")\n\t\tc.Error(\"wrong number\", \"LMOVE\", \"chk\", \"dst\", \"chk\", \"too\", \"many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"BLMOVE\", \"chk\", \"str\", \"LEFT\", \"LEFT\", \"0\")\n\t\tc.Error(\"wrong kind\", \"BLMOVE\", \"str\", \"chk\", \"LEFT\", \"LEFT\", \"0\")\n\t\tc.Do(\"LRANGE\", \"chk\", \"0\", \"-1\")\n\t})\n\n\twg := &sync.WaitGroup{}\n\twg.Add(1)\n\ttestMulti(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"BLMOVE\", \"from\", \"to\", \"RIGHT\", \"LEFT\", \"1\")\n\t\t\tc.Do(\"BLMOVE\", \"from\", \"to\", \"RIGHT\", \"LEFT\", \"1\")\n\t\t\tc.Do(\"BLMOVE\", \"from\", \"to\", \"RIGHT\", \"LEFT\", \"1\")\n\t\t\tc.Do(\"BLMOVE\", \"from\", \"to\", \"RIGHT\", \"LEFT\", \"1\")\n\t\t\tc.Do(\"BLMOVE\", \"from\", \"to\", \"RIGHT\", \"LEFT\", \"1\") // will timeout\n\t\t\twg.Done()\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"LPUSH\", \"from\", \"aap\", \"noot\", \"mies\")\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t\tc.Do(\"LPUSH\", \"from\", \"toon\")\n\t\t\twg.Wait()\n\t\t\tc.Do(\"LRANGE\", \"from\", \"0\", \"-1\")\n\t\t\tc.Do(\"LRANGE\", \"to\", \"0\", \"-1\")\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/pubsub_test.go",
    "content": "package main\n\nimport (\n\t\"sync\"\n\t\"testing\"\n)\n\nfunc TestSubscribe(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"wrong number\", \"SUBSCRIBE\")\n\n\t\tc.Do(\"SUBSCRIBE\", \"foo\")\n\t\tc.Do(\"UNSUBSCRIBE\")\n\n\t\tc.Do(\"SUBSCRIBE\", \"foo\")\n\t\tc.Do(\"UNSUBSCRIBE\", \"foo\")\n\n\t\tc.Do(\"SUBSCRIBE\", \"foo\", \"bar\")\n\t\tc.Receive()\n\t\tc.Do(\"UNSUBSCRIBE\", \"foo\", \"bar\")\n\t\tc.Receive()\n\n\t\tc.Do(\"SUBSCRIBE\", \"-1\")\n\t\tc.Do(\"UNSUBSCRIBE\", \"-1\")\n\n\t\tc.Do(\"UNSUBSCRIBE\")\n\t})\n}\n\nfunc TestPsubscribe(t *testing.T) {\n\tskip(t)\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Error(\"wrong number\", \"PSUBSCRIBE\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi2\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"foo\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"foo\", \"bar\")\n\t\tc1.Receive()\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi3\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"foo\", \"bar\")\n\t\tc1.Receive()\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"f?o\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi4\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"f?o\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"f*o\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi5\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"f*o\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"f[oO]o\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi6\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"f[oO]o\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", `f\\?o`)\n\t\tc2.Do(\"PUBLISH\", \"f?o\", \"hi7\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", `f\\?o`)\n\n\t\tc1.Do(\"PSUBSCRIBE\", `f\\*o`)\n\t\tc2.Do(\"PUBLISH\", \"f*o\", \"hi8\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", `f\\*o`)\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"f\\\\[oO]o\")\n\t\tc2.Do(\"PUBLISH\", \"f[oO]o\", \"hi9\")\n\t\tc1.Receive()\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"f\\\\[oO]o\")\n\n\t\tc1.Do(\"PSUBSCRIBE\", `f\\\\oo`)\n\t\tc2.Do(\"PUBLISH\", `f\\\\oo`, \"hi10\")\n\t\tc1.Do(\"PUNSUBSCRIBE\", `f\\\\oo`)\n\n\t\tc1.Do(\"PSUBSCRIBE\", \"-1\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hi11\")\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"-1\")\n\t})\n\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"PSUBSCRIBE\", \"news*\")\n\t\tc2.Do(\"PUBLISH\", \"news\", \"fire!\")\n\t\tc1.Receive()\n\t})\n\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"PSUBSCRIBE\", \"news\") // no pattern\n\t\tc2.Do(\"PUBLISH\", \"news\", \"fire!\")\n\t\tc1.Receive()\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"PUNSUBSCRIBE\")\n\t\tc.Do(\"PUNSUBSCRIBE\")\n\t})\n}\n\nfunc TestPublish(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"wrong number\", \"PUBLISH\")\n\t\tc.Error(\"wrong number\", \"PUBLISH\", \"foo\")\n\t\tc.Do(\"PUBLISH\", \"foo\", \"bar\")\n\t\tc.Error(\"wrong number\", \"PUBLISH\", \"foo\", \"bar\", \"deadbeef\")\n\t\tc.Do(\"PUBLISH\", \"-1\", \"-2\")\n\t})\n}\n\nfunc TestPubSub(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"wrong number\", \"PUBSUB\")\n\t\tc.Error(\"subcommand\", \"PUBSUB\", \"FOO\")\n\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"foo\")\n\t\tc.Error(\"wrong number\", \"PUBSUB\", \"CHANNELS\", \"foo\", \"bar\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f?o\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f*o\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f[oO]o\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f\\\\?o\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f\\\\*o\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f\\\\[oO]o\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"f\\\\\\\\oo\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\", \"-1\")\n\n\t\tc.Do(\"PUBSUB\", \"NUMSUB\")\n\t\tc.Do(\"PUBSUB\", \"NUMSUB\", \"foo\")\n\t\tc.Do(\"PUBSUB\", \"NUMSUB\", \"foo\", \"bar\")\n\t\tc.Do(\"PUBSUB\", \"NUMSUB\", \"-1\")\n\n\t\tc.Do(\"PUBSUB\", \"NUMPAT\")\n\t\tc.Error(\"wrong number\", \"PUBSUB\", \"NUMPAT\", \"foo\")\n\t})\n}\n\nfunc TestPubsubFull(t *testing.T) {\n\tskip(t)\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SUBSCRIBE\", \"news\", \"sport\")\n\t\tc1.Receive()\n\t\tc2.Do(\"PUBLISH\", \"news\", \"revolution!\")\n\t\tc2.Do(\"PUBLISH\", \"news\", \"alien invasion!\")\n\t\tc2.Do(\"PUBLISH\", \"sport\", \"lady biked too fast\")\n\t\tc2.Do(\"PUBLISH\", \"gossip\", \"man bites dog\")\n\t\tc1.Receive()\n\t\tc1.Receive()\n\t\tc1.Receive()\n\t\tc1.Do(\"UNSUBSCRIBE\", \"news\", \"sport\")\n\t\tc1.Receive()\n\t})\n\n\ttestRESP3Pair(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SUBSCRIBE\", \"news\", \"sport\")\n\t\tc1.Receive()\n\t\tc2.Do(\"PUBLISH\", \"news\", \"fire!\")\n\t\tc1.Receive()\n\t\tc1.Do(\"UNSUBSCRIBE\", \"news\", \"sport\")\n\t\tc1.Receive()\n\t})\n}\n\nfunc TestPubsubMulti(t *testing.T) {\n\tskip(t)\n\tvar wg1 sync.WaitGroup\n\twg1.Add(2)\n\ttestMulti(t,\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"SUBSCRIBE\", \"news\", \"sport\")\n\t\t\tc.Receive()\n\t\t\twg1.Done()\n\t\t\tc.Receive()\n\t\t\tc.Receive()\n\t\t\tc.Receive()\n\t\t\tc.Do(\"UNSUBSCRIBE\", \"news\", \"sport\")\n\t\t\tc.Receive()\n\t\t},\n\t\tfunc(c *client) {\n\t\t\tc.Do(\"SUBSCRIBE\", \"sport\")\n\t\t\twg1.Done()\n\t\t\tc.Receive()\n\t\t\tc.Do(\"UNSUBSCRIBE\", \"sport\")\n\t\t},\n\t\tfunc(c *client) {\n\t\t\twg1.Wait()\n\t\t\tc.Do(\"PUBLISH\", \"news\", \"revolution!\")\n\t\t\tc.Do(\"PUBLISH\", \"news\", \"alien invasion!\")\n\t\t\tc.Do(\"PUBLISH\", \"sport\", \"lady biked too fast\")\n\t\t},\n\t)\n}\n\nfunc TestPubsubSelect(t *testing.T) {\n\tskip(t)\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SUBSCRIBE\", \"news\", \"sport\")\n\t\tc1.Receive()\n\t\tc2.Do(\"SELECT\", \"3\")\n\t\tc2.Do(\"PUBLISH\", \"news\", \"revolution!\")\n\t\tc1.Receive()\n\t})\n}\n\nfunc TestPubsubMode(t *testing.T) {\n\tskip(t)\n\t// most commands aren't allowed in publish mode\n\tt.Run(\"basic\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"SUBSCRIBE\", \"news\", \"sport\")\n\t\t\tc.Receive()\n\t\t\tc.Do(\"PING\")\n\t\t\tc.Do(\"PING\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"ECHO\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"HGET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SET\", \"foo\", \"bar\")\n\t\t\tc.Do(\"QUIT\")\n\t\t})\n\t})\n\n\tt.Run(\"tx\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"SUBSCRIBE\", \"news\")\n\t\t\t// failWith(e, \"PING\"),\n\t\t\t// failWith(e, \"PSUBSCRIBE\"),\n\t\t\t// failWith(e, \"PUNSUBSCRIBE\"),\n\t\t\t// failWith(e, \"QUIT\"),\n\t\t\t// failWith(e, \"SUBSCRIBE\"),\n\t\t\t// failWith(e, \"UNSUBSCRIBE\"),\n\n\t\t\tc.Error(\"are allowed\", \"APPEND\", \"foo\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"AUTH\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"BITCOUNT\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"BITOP\", \"OR\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"BITPOS\", \"foo\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"BLPOP\", \"key\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"BRPOP\", \"key\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"BRPOPLPUSH\", \"foo\", \"bar\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"DBSIZE\")\n\t\t\tc.Error(\"are allowed\", \"DECR\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"DECRBY\", \"foo\", \"3\")\n\t\t\tc.Error(\"are allowed\", \"DEL\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"DISCARD\")\n\t\t\tc.Error(\"are allowed\", \"ECHO\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"EVAL\", \"foo\", \"{}\")\n\t\t\tc.Error(\"are allowed\", \"EVALSHA\", \"foo\", \"{}\")\n\t\t\tc.Error(\"are allowed\", \"EXEC\")\n\t\t\tc.Error(\"are allowed\", \"EXISTS\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"EXPIRE\", \"foo\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"EXPIREAT\", \"foo\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"FLUSHALL\")\n\t\t\tc.Error(\"are allowed\", \"FLUSHDB\")\n\t\t\tc.Error(\"are allowed\", \"GET\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"GETEX\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"GETBIT\", \"foo\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"GETRANGE\", \"foo\", \"12\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"GETSET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"HDEL\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"HEXISTS\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"HGET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"HGETALL\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"HINCRBY\", \"foo\", \"bar\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"HINCRBYFLOAT\", \"foo\", \"bar\", \"12.34\")\n\t\t\tc.Error(\"are allowed\", \"HKEYS\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"HLEN\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"HMGET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"HMSET\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"HSCAN\", \"foo\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"HSET\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"HSETNX\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"HVALS\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"INCR\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"INCRBY\", \"foo\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"INCRBYFLOAT\", \"foo\", \"12.34\")\n\t\t\tc.Error(\"are allowed\", \"KEYS\", \"*\")\n\t\t\tc.Error(\"are allowed\", \"LINDEX\", \"foo\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"LINSERT\", \"foo\", \"after\", \"bar\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"LLEN\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"LPOP\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"LPUSH\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"LPUSHX\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"LRANGE\", \"foo\", \"1\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"LREM\", \"foo\", \"0\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"LSET\", \"foo\", \"0\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"LTRIM\", \"foo\", \"0\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"MGET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"MOVE\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"MSET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"MSETNX\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"MULTI\")\n\t\t\tc.Error(\"are allowed\", \"PERSIST\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"PEXPIRE\", \"foo\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"PEXPIREAT\", \"foo\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"PSETEX\", \"foo\", \"12\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"PTTL\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"PUBLISH\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"PUBSUB\", \"CHANNELS\")\n\t\t\tc.Error(\"are allowed\", \"RANDOMKEY\")\n\t\t\tc.Error(\"are allowed\", \"RENAME\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"RENAMENX\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"RPOP\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"RPOPLPUSH\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"RPUSH\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"RPUSHX\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SADD\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SCAN\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"SCARD\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"SCRIPT\", \"FLUSH\")\n\t\t\tc.Error(\"are allowed\", \"SDIFF\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"SDIFFSTORE\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SELECT\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"SET\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SETBIT\", \"foo\", \"0\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"SETEX\", \"foo\", \"12\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SETNX\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SETRANGE\", \"foo\", \"0\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SINTER\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SINTERSTORE\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"SISMEMBER\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SMEMBERS\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"SMOVE\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"SPOP\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"SRANDMEMBER\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"SREM\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"SSCAN\", \"foo\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"STRLEN\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"SUNION\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"SUNIONSTORE\", \"foo\", \"bar\", \"baz\")\n\t\t\tc.Error(\"are allowed\", \"TIME\")\n\t\t\tc.Error(\"are allowed\", \"TTL\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"TYPE\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"UNWATCH\")\n\t\t\tc.Error(\"are allowed\", \"WATCH\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"ZADD\", \"foo\", \"INCR\", \"1\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"ZCARD\", \"foo\")\n\t\t\tc.Error(\"are allowed\", \"ZCOUNT\", \"foo\", \"0\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"ZINCRBY\", \"foo\", \"bar\", \"12\")\n\t\t\tc.Error(\"are allowed\", \"ZINTERSTORE\", \"foo\", \"1\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"ZLEXCOUNT\", \"foo\", \"-\", \"+\")\n\t\t\tc.Error(\"are allowed\", \"ZRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.Error(\"are allowed\", \"ZRANGEBYLEX\", \"foo\", \"-\", \"+\")\n\t\t\tc.Error(\"are allowed\", \"ZRANGEBYSCORE\", \"foo\", \"0\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"ZRANK\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"ZREM\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"ZREMRANGEBYLEX\", \"foo\", \"-\", \"+\")\n\t\t\tc.Error(\"are allowed\", \"ZREMRANGEBYRANK\", \"foo\", \"0\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"ZREMRANGEBYSCORE\", \"foo\", \"0\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"ZREVRANGE\", \"foo\", \"0\", \"-1\")\n\t\t\tc.Error(\"are allowed\", \"ZREVRANGEBYLEX\", \"foo\", \"+\", \"-\")\n\t\t\tc.Error(\"are allowed\", \"ZREVRANGEBYSCORE\", \"foo\", \"0\", \"1\")\n\t\t\tc.Error(\"are allowed\", \"ZREVRANK\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"ZSCAN\", \"foo\", \"0\")\n\t\t\tc.Error(\"are allowed\", \"ZSCORE\", \"foo\", \"bar\")\n\t\t\tc.Error(\"are allowed\", \"ZUNIONSTORE\", \"foo\", \"1\", \"bar\")\n\t\t})\n\t})\n}\n\nfunc TestSubscriptions(t *testing.T) {\n\tskip(t)\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SUBSCRIBE\", \"foo\", \"bar\", \"foo\")\n\t\tc2.Do(\"PUBSUB\", \"NUMSUB\")\n\t\tc1.Do(\"UNSUBSCRIBE\", \"bar\", \"bar\", \"bar\")\n\t\tc2.Do(\"PUBSUB\", \"NUMSUB\")\n\t})\n}\n\nfunc TestPubsubUnsub(t *testing.T) {\n\tskip(t)\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SUBSCRIBE\", \"news\", \"sport\")\n\t\tc1.Receive()\n\t\tc2.DoSorted(\"PUBSUB\", \"CHANNELS\")\n\t\tc1.Do(\"QUIT\")\n\t\tc2.DoSorted(\"PUBSUB\", \"CHANNELS\")\n\t})\n}\n\nfunc TestPubsubTx(t *testing.T) {\n\tskip(t)\n\t// publish is in a tx\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"SUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"MULTI\")\n\t\tc2.Do(\"PUBSUB\", \"CHANNELS\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hello one\")\n\t\tc2.Error(\"wrong number\", \"GET\")\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"hello two\")\n\t\tc2.Error(\"discarded\", \"EXEC\")\n\n\t\tc2.Do(\"PUBLISH\", \"foo\", \"post tx\")\n\t\tc1.Receive()\n\t})\n\n\t// SUBSCRIBE is in a tx\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"MULTI\")\n\t\tc1.Do(\"SUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"PUBSUB\", \"CHANNELS\")\n\t\tc1.Do(\"EXEC\")\n\t\tc2.Do(\"PUBSUB\", \"CHANNELS\")\n\n\t\tc1.Error(\"are allowed\", \"MULTI\") // we're in SUBSCRIBE mode\n\t})\n\n\t// DISCARDing a tx prevents from entering publish mode\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SUBSCRIBE\", \"foo\")\n\t\tc.Do(\"DISCARD\")\n\t\tc.Do(\"PUBSUB\", \"CHANNELS\")\n\t})\n\n\t// UNSUBSCRIBE is in a tx\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"MULTI\")\n\t\tc1.Do(\"SUBSCRIBE\", \"foo\")\n\t\tc1.Do(\"UNSUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"PUBSUB\", \"CHANNELS\")\n\t\tc1.Do(\"EXEC\")\n\t\tc2.Do(\"PUBSUB\", \"CHANNELS\")\n\t\tc1.Do(\"PUBSUB\", \"CHANNELS\")\n\t})\n\n\t// PSUBSCRIBE is in a tx\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"MULTI\")\n\t\tc1.Do(\"PSUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"PUBSUB\", \"NUMPAT\")\n\t\tc1.Do(\"EXEC\")\n\t\tc2.Do(\"PUBSUB\", \"NUMPAT\")\n\n\t\tc1.Error(\"are allowed\", \"MULTI\") // we're in SUBSCRIBE mode\n\t})\n\n\t// PUNSUBSCRIBE is in a tx\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"MULTI\")\n\t\tc1.Do(\"PSUBSCRIBE\", \"foo\")\n\t\tc1.Do(\"PUNSUBSCRIBE\", \"foo\")\n\t\tc2.Do(\"PUBSUB\", \"NUMPAT\")\n\t\tc1.Do(\"EXEC\")\n\t\tc2.Do(\"PUBSUB\", \"NUMPAT\")\n\t\tc1.Do(\"PUBSUB\", \"NUMPAT\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/script_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestScript(t *testing.T) {\n\tskip(t)\n\tt.Run(\"EVAL\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"EVAL\", \"return 42\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"return 42\", \"1\", \"foo\")\n\t\t\tc.Do(\"EVAL\", \"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\", \"2\", \"key1\", \"key2\", \"first\", \"second\")\n\t\t\tc.Do(\"EVAL\", \"return {ARGV[1]}\", \"0\", \"first\")\n\t\t\tc.Do(\"EVAL\", \"return {ARGV[1]}\", \"0\", \"first\\nwith\\nnewlines!\\r\\r\\n\\t!\")\n\t\t\tc.Do(\"EVAL\", \"return redis.call('GET', 'nosuch')==false\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"return redis.call('GET', 'nosuch')==nil\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"local a = redis.call('MGET', 'bar'); return a[1] == false\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"local a = redis.call('MGET', 'bar'); return a[1] == nil\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"return redis.call('ZRANGE', 'q', 0, -1)\", \"0\")\n\t\t\tc.Do(\"EVAL\", \"return redis.call('LPOP', 'foo')\", \"0\")\n\n\t\t\tc.Do(\"EVAL_RO\", \"return 42\", \"0\")\n\t\t\tc.Do(\"EVAL_RO\", \"return 42+2\", \"0\")\n\t\t\tc.Error(\"Write commands are not allowed\", \"EVAL_RO\", \"return redis.call('LPOP', 'foo')\", \"0\")\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"EVAL\")\n\t\t\tc.Error(\"wrong number\", \"EVAL\", \"return 42\")\n\t\t\tc.Error(\"wrong number\", \"EVAL\", \"[\")\n\t\t\tc.Error(\"not an integer\", \"EVAL\", \"return 42\", \"return 43\")\n\t\t\tc.Error(\"greater\", \"EVAL\", \"return 42\", \"1\")\n\t\t\tc.Error(\"negative\", \"EVAL\", \"return 42\", \"-1\")\n\t\t\tc.Error(\"wrong number\", \"EVAL\", \"42\")\n\t\t})\n\t})\n\n\tt.Run(\"SCRIPT\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return 42\")\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return 42\")\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return 43\")\n\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", \"1fa00e76656cc152ad327c13fe365858fd7be306\")\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", \"0\", \"1fa00e76656cc152ad327c13fe365858fd7be306\")\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", \"0\")\n\t\t\tc.Error(\"wrong number\", \"SCRIPT\", \"EXISTS\")\n\n\t\t\tc.Do(\"SCRIPT\", \"FLUSH\")\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", \"1fa00e76656cc152ad327c13fe365858fd7be306\")\n\t\t\tc.Do(\"SCRIPT\", \"FLUSH\", \"ASYNC\")\n\t\t\tc.Do(\"SCRIPT\", \"FLUSH\", \"SyNc\")\n\n\t\t\tc.Error(\"wrong number\", \"SCRIPT\")\n\t\t\tc.Error(\"wrong number\", \"SCRIPT\", \"LOAD\", \"return 42\", \"return 42\")\n\t\t\tc.DoLoosely(\"SCRIPT\", \"LOAD\", \"]\")\n\t\t\tc.Error(\"wrong number\", \"SCRIPT\", \"LOAD\", \"]\", \"foo\")\n\t\t\tc.Error(\"wrong number\", \"SCRIPT\", \"LOAD\")\n\t\t\tc.Error(\"only support\", \"SCRIPT\", \"FLUSH\", \"foo\")\n\t\t\tc.Error(\"only support\", \"SCRIPT\", \"FLUSH\", \"ASYNC\", \"foo\")\n\t\t\tc.Error(\"unknown subcommand\", \"SCRIPT\", \"FOO\")\n\t\t})\n\t})\n\n\tt.Run(\"EVALSHA\", func(t *testing.T) {\n\t\tsha1 := \"1fa00e76656cc152ad327c13fe365858fd7be306\" // \"return 42\"\n\t\tsha2 := \"bfbf458525d6a0b19200bfd6db3af481156b367b\" // keys[1], argv[1]\n\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return 42\")\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return {KEYS[1],ARGV[1]}\")\n\t\t\tc.Do(\"EVALSHA\", sha1, \"0\")\n\t\t\tc.Do(\"EVALSHA\", sha2, \"0\")\n\t\t\tc.Do(\"EVALSHA\", sha2, \"0\", \"foo\")\n\t\t\tc.Do(\"EVALSHA\", sha2, \"1\", \"foo\")\n\t\t\tc.Do(\"EVALSHA\", sha2, \"1\", \"foo\", \"bar\")\n\t\t\tc.Do(\"EVALSHA\", sha2, \"1\", \"foo\", \"bar\", \"baz\")\n\n\t\t\tc.Do(\"SCRIPT\", \"FLUSH\")\n\t\t\tc.Error(\"Please use EVAL\", \"EVALSHA\", sha1, \"0\")\n\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return 42\")\n\t\t\tc.Error(\"wrong number\", \"EVALSHA\", sha1)\n\t\t\tc.Error(\"wrong number\", \"EVALSHA\")\n\t\t\tc.Error(\"wrong number\", \"EVALSHA\", \"nosuch\")\n\t\t\tc.Error(\"Please use EVAL\", \"EVALSHA\", \"nosuch\", \"0\")\n\t\t})\n\t})\n\n\tt.Run(\"combined\", func(t *testing.T) {\n\t\tsha1 := \"1fa00e76656cc152ad327c13fe365858fd7be306\" // \"return 42\"\n\n\t\ttestRaw(t, func(c *client) {\n\t\t\t// EVAL stores the script\n\t\t\tc.Do(\"EVAL\", \"return 42\", \"0\")\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", sha1)\n\t\t\tc.Do(\"EVALSHA\", sha1, \"0\")\n\n\t\t\t// doesn't store the script on syntax error\n\t\t\tc.Error(\"compiling\", \"EVAL\", \"return '<-syntax error\", \"0\")\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", \"015cb4913729c68a7209188bbdee1b1ca19358bf\")\n\t\t\tc.Error(\"NOSCRIPT\", \"EVALSHA\", \"015cb4913729c68a7209188bbdee1b1ca19358bf\", \"0\")\n\n\t\t\t// does store the script on arg errors\n\t\t\tc.Do(\"SCRIPT\", \"FLUSH\")\n\t\t\tc.Error(\"not an int\", \"EVAL\", \"return 42\", \"notanumber\")\n\t\t\tc.Do(\"SCRIPT\", \"EXISTS\", sha1)\n\t\t\tc.Error(\"NOSCRIPT\", \"EVALSHA\", sha1, \"0\")\n\t\t})\n\t})\n\n\tt.Run(\"setresp\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"EVAL\", `redis.setresp(3); redis.call(\"SET\", \"foo\", 12); return redis.call(\"GET\", \"foo\")`, \"0\")\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", `redis.setresp(3)`)\n\t\t\tc.Do(\"EVALSHA\", \"d204691e560b5b17f19626b50f84c2dcadff7ed5\", \"0\")\n\t\t\tc.Do(\"EVAL\", `return redis.setresp(3)`, \"0\")\n\t\t\tc.Do(\"EVAL\", `return redis.setresp(2)`, \"0\")\n\t\t\tc.Error(\"RESP version must be 2 or 3\", \"EVAL\", `return redis.setresp(4)`, \"0\")\n\t\t})\n\t\ttestRESP3(t, func(c *client) {\n\t\t\tc.Do(\"SCRIPT\", \"LOAD\", `redis.setresp(3)`)\n\t\t\tc.Do(\"EVALSHA\", \"d204691e560b5b17f19626b50f84c2dcadff7ed5\", \"0\")\n\t\t\tc.Do(\"EVAL\", `redis.setresp(3); redis.call(\"SET\", \"foo\", 12); return redis.call(\"GET\", \"foo\")`, \"0\")\n\t\t})\n\t})\n}\n\nfunc TestLua(t *testing.T) {\n\tskip(t)\n\t// basic datatype things\n\tdatatypes := func(c *client) {\n\t\tc.Do(\"EVAL\", \"\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 42\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 42, 43\", \"0\")\n\t\tc.Do(\"EVAL\", \"return true\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 'foo'\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 3.1415\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 3.9999\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,'foo'}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,'foo',nil,'foo'}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 3.9999+3\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 3.99+0.0001\", \"0\")\n\t\tc.Do(\"EVAL\", \"return 3.9999+0.201\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {{1}}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,{1,{1,'bar'}}}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return nil\", \"0\")\n\t}\n\ttestRaw(t, datatypes)\n\ttestRESP3(t, datatypes)\n\n\t// special returns\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"oops\", \"EVAL\", \"return {err = 'oops'}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,{err = 'oops'}}\", \"0\")\n\t\tc.Error(\"oops\", \"EVAL\", \"return redis.error_reply('oops2')\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,redis.error_reply('oops')}\", \"0\")\n\t\tc.Error(\"oops\", \"EVAL\", \"return {err = 'oops', noerr = true}\", \"0\") // doc error?\n\t\tc.Error(\"oops\", \"EVAL\", \"return {1, 2, err = 'oops'}\", \"0\")         // doc error?\n\n\t\tc.Do(\"EVAL\", \"return {ok = 'great'}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,{ok = 'great'}}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return redis.status_reply('great')\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {1,redis.status_reply('great')}\", \"0\")\n\t\tc.Do(\"EVAL\", \"return {ok = 'great', notok = 'yes'}\", \"0\")       // doc error?\n\t\tc.Do(\"EVAL\", \"return {1, 2, ok = 'great', notok = 'yes'}\", \"0\") // doc error?\n\n\t\tc.Error(\"type of arguments\", \"EVAL\", \"return redis.error_reply(1)\", \"0\")\n\t\tc.Error(\"type of arguments\", \"EVAL\", \"return redis.error_reply()\", \"0\")\n\t\tc.Error(\"type of arguments\", \"EVAL\", \"return redis.error_reply(redis.error_reply('foo'))\", \"0\")\n\t\tc.Error(\"type of arguments\", \"EVAL\", \"return redis.status_reply(1)\", \"0\")\n\t\tc.Error(\"type of arguments\", \"EVAL\", \"return redis.status_reply()\", \"0\")\n\t\tc.Error(\"type of arguments\", \"EVAL\", \"return redis.status_reply(redis.status_reply('foo'))\", \"0\")\n\n\t\tc.ErrorTheSame(\"ERR \", \"EVAL\", \"return redis.error_reply('')\", \"0\")\n\t\tc.ErrorTheSame(\"ERR \", \"EVAL\", \"return redis.error_reply('-')\", \"0\")\n\t\tc.ErrorTheSame(\"ERR foo\", \"EVAL\", \"return redis.error_reply('foo')\", \"0\")\n\t\tc.ErrorTheSame(\"ERR foo\", \"EVAL\", \"return redis.error_reply('-foo')\", \"0\")\n\t\tc.ErrorTheSame(\"foo bar\", \"EVAL\", \"return redis.error_reply('foo bar')\", \"0\")\n\t\tc.ErrorTheSame(\"foo bar\", \"EVAL\", \"return redis.error_reply('-foo bar')\", \"0\")\n\t})\n\n\t// state inside lua\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"EVAL\", \"redis.call('SELECT', 3); redis.call('SET', 'foo', 'bar')\", \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SELECT\", \"3\")\n\t\tc.Do(\"GET\", \"foo\")\n\t})\n\n\t// lua env\n\ttestRaw(t, func(c *client) {\n\t\t// c.Do(\"EVAL\", \"print(1)\", \"0\")\n\t\tc.Do(\"EVAL\", `return string.format('%q', \"pretty string\")`, \"0\")\n\t\tc.Error(\"Script attempted to access nonexistent global variable\", \"EVAL\", \"foob.clock()\", \"0\")\n\t\tc.DoLoosely(\"EVAL\", \"os.clock()\", \"0\")\n\t\tc.Error(\"attempt to call\", \"EVAL\", \"os.exit(42)\", \"0\")\n\t\tc.Do(\"EVAL\", \"return table.concat({1,2,3})\", \"0\")\n\t\tc.Do(\"EVAL\", \"return math.abs(-42)\", \"0\")\n\t\tc.Error(\"Script attempted to access nonexistent global variable\", \"EVAL\", `return utf8.len(\"hello world\")`, \"0\")\n\t\t// c.Error(\"Script attempted to access nonexistent global variable\", \"EVAL\", `require(\"utf8\")`, \"0\")\n\t\tc.Do(\"EVAL\", `return coroutine.running()`, \"0\")\n\t})\n\n\t// sha1hex\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"EVAL\", `return redis.sha1hex(\"foo\")`, \"0\")\n\t\tc.Do(\"SET\", \"bar\", \"32\")\n\t\tc.Do(\"EVAL\", `return redis.sha1hex(KEYS[\"bar\"])`, \"0\")\n\t\tc.Do(\"EVAL\", `return redis.sha1hex(KEYS[1])`, \"1\", \"bar\")\n\t\tc.Do(\"EVAL\", `return redis.sha1hex(nil)`, \"0\")\n\t\tc.Do(\"EVAL\", `return redis.sha1hex(42)`, \"0\")\n\t\tc.Do(\"EVAL\", `return redis.sha1hex({})`, \"0\")\n\t\tc.Do(\"EVAL\", `return redis.sha1hex(KEYS[1])`, \"0\")\n\t\tc.Error(\n\t\t\t\"wrong number of arguments\",\n\t\t\t\"EVAL\", `return redis.sha1hex()`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"wrong number of arguments\",\n\t\t\t\"EVAL\", `return redis.sha1hex(1, 2)`, \"0\",\n\t\t)\n\t})\n\n\t// cjson module\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"EVAL\", `return cjson.decode('{\"id\":\"foo\"}')['id']`, \"0\")\n\t\t// c.Do(\"SET\", \"foo\", `{\"value\":42}`)\n\t\t// c.Do(\"EVAL\", `return KEYS[1]`, 1, \"foo\")\n\t\t// c.Do(\"EVAL\", `return cjson.decode(KEYS[1])['value']`, 1, \"foo\")\n\t\tc.Do(\"EVAL\", `return cjson.decode(ARGV[1])['value']`, \"0\", `{\"value\":\"42\"}`)\n\t\tc.Do(\"EVAL\", `return redis.call(\"SET\", \"enc\", cjson.encode({[\"foo\"]=\"bar\"}))`, \"0\")\n\t\tc.Do(\"EVAL\", `return redis.call(\"SET\", \"enc\", cjson.encode({[\"foo\"]={[\"foo\"]=42}}))`, \"0\")\n\t\tc.Do(\"GET\", \"enc\")\n\n\t\tc.Error(\n\t\t\t\"bad argument #1 to \",\n\t\t\t\"EVAL\", `return cjson.encode()`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"bad argument #1 to \",\n\t\t\t\"EVAL\", `return cjson.encode(1, 2)`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"bad argument #1 to \",\n\t\t\t\"EVAL\", `return cjson.decode()`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"bad argument #1 to \",\n\t\t\t\"EVAL\", `return cjson.decode(1, 2)`, \"0\",\n\t\t)\n\t})\n\n\t// selected DB gets passed on to lua\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SELECT\", \"3\")\n\t\tc.Do(\"EVAL\", \"redis.call('SET', 'foo', 'bar')\", \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SELECT\", \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t})\n}\n\nfunc TestLuaCall(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"1\")\n\t\tc.Do(\"EVAL\", `local foo = redis.call(\"GET\", \"foo\"); redis.call(\"SET\", \"foo\", foo+1)`, \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"EVAL\", `return redis.call(\"GET\", \"foo\")`, \"0\")\n\t\tc.Do(\"EVAL\", `return redis.call(\"SET\", \"foo\", 42)`, \"0\")\n\t\tc.Do(\"EVAL\", `redis.log(redis.LOG_NOTICE, \"hello\")`, \"0\")\n\t\tc.Do(\"EVAL\", `local res = redis.call(\"GET\", \"foo\"); return res['ok']`, \"0\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tscript := `\n\t\t\tlocal result = redis.call('SET', 'mykey', 'myvalue', 'NX');\n\t\t\treturn result['ok'];\n\t\t`\n\t\tc.Do(\"EVAL\", script, \"0\")\n\t})\n\n\t// datatype errors\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\n\t\t\t\"Please specify at least one argument for this redis lib call script: 23251039f40992dadef496cbfe3f3d23a6d314ce\",\n\t\t\t\"EVAL\", `redis.call()`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 2c79b56ef55f7dc96da28dddb6ba551017fb1480,\",\n\t\t\t\"EVAL\", `redis.call({})`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Unknown Redis command called from script script: 1f422cead4ec560a2473e39974d64f965b99b8b0\",\n\t\t\t\"EVAL\", `redis.call(1)`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Unknown Redis command called from script script: cd72c3c55975da213448de4e59a8674b8b21c486\",\n\t\t\t\"EVAL\", `redis.call(\"1\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 40286a2418d06fc20cf71762ed4c52b5348b4bb0\",\n\t\t\t\"EVAL\", `redis.call(\"ECHO\", true)`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: d2f4e1eb2935fe53669068a377a3dc4b923eb669,\",\n\t\t\t\"EVAL\", `redis.call(\"ECHO\", false)`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 33462f69402788110bccac05df6a8ac9c7429304,\",\n\t\t\t\"EVAL\", `redis.call(\"ECHO\", nil)`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 180500c268449fd1a24ea520d39a4aa76d6693c2,\",\n\t\t\t\"EVAL\", `redis.call(\"HELLO\", {})`, \"0\",\n\t\t)\n\t\t// c.Error(\"Error\", \"EVAL\", `redis.call(\"HELLO\", 1)`, \"0\")\n\t\t// c.Error(\"Redis command\", \"EVAL\", `redis.call(\"HELLO\", 3.14)`, \"0\")\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 32c9afc7bcb832809c41272b7a5525020b3e8bf5,\",\n\t\t\t\"EVAL\", `redis.call(\"GET\", {})`, \"0\",\n\t\t)\n\t})\n\n\t// call() errors\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"1\")\n\n\t\tc.Error(\"rong number of arg\", \"EVAL\", `redis.call(\"HGET\", \"foo\")`, \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Error(\"rong number of arg\", \"EVAL\", `local foo = redis.call(\"HGET\", \"foo\"); redis.call(\"SET\", \"res\", foo)`, \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"GET\", \"res\")\n\t\tc.Error(\"WRONGTYPE\", \"EVAL\", `local foo = redis.call(\"HGET\", \"foo\", \"bar\"); redis.call(\"SET\", \"res\", foo)`, \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"GET\", \"res\")\n\t})\n\n\t// pcall() errors\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"1\")\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 66acd1fa6589521219d0b0dc3c1965f4b11a3422,\",\n\t\t\t\"EVAL\", `local foo = redis.pcall(\"HGET\", \"foo\"); redis.call(\"SET\", \"res\", foo)`, \"0\",\n\t\t)\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"GET\", \"res\")\n\t\tc.Error(\n\t\t\t\"Lua redis lib command arguments must be strings or integers script: 5b67bc50d5e0ed20baae44ca5a735efa6a3e5243,\",\n\t\t\t\"EVAL\", `local foo = redis.pcall(\"HGET\", \"foo\", \"bar\"); redis.call(\"SET\", \"res\", foo)`, \"0\",\n\t\t)\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"GET\", \"res\")\n\t})\n\n\t// call() with non-allowed commands\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"1\")\n\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: a17bb9f079d9b5202346e82ccaa50f3b9553172b,\",\n\t\t\t\"EVAL\", `redis.call(\"MULTI\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 56569e2c63cf8996b64922e5a26e23c60fe9f1aa,\",\n\t\t\t\"EVAL\", `redis.call(\"EXEC\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: a2457385c7980996400fc4315534dcf332d54f46,\",\n\t\t\t\"EVAL\", `redis.call(\"EVAL\", \"redis.call(\\\"GET\\\", \\\"foo\\\")\", 0)`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: ac613210b61b9f3339fd677969291675b9b703d3,\",\n\t\t\t\"EVAL\", `redis.call(\"SCRIPT\", \"LOAD\", \"return 42\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 888b717177e29e998baf4bac6116c2a4787b4c70,\",\n\t\t\t\"EVAL\", `redis.call(\"EVALSHA\", \"123\", \"0\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 508bef3f1ab46859dee541a8bc3b0f368ae1844f,\",\n\t\t\t\"EVAL\", `redis.call(\"AUTH\", \"foobar\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 62b5d652eb4d90746a5672a450ed9e3627521df1,\",\n\t\t\t\"EVAL\", `redis.call(\"WATCH\", \"foobar\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 65ea661820802737ade33d7a70582838a09fcf8d,\",\n\t\t\t\"EVAL\", `redis.call(\"SUBSCRIBE\", \"foo\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 1af9ab7e7d8aa211959de33824dc075ee816ab1a,\",\n\t\t\t\"EVAL\", `redis.call(\"UNSUBSCRIBE\", \"foo\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: 0610e3628fbdca44e6d49736d5b59be8bab5047d,\",\n\t\t\t\"EVAL\", `redis.call(\"PSUBSCRIBE\", \"foo\")`, \"0\",\n\t\t)\n\t\tc.Error(\n\t\t\t\"This Redis command is not allowed from script script: ba7f784eaff4e747e31a39abd5386c432aac3140,\",\n\t\t\t\"EVAL\", `redis.call(\"PUNSUBSCRIBE\", \"foo\")`, \"0\",\n\t\t)\n\t\tc.Do(\"EVAL\", `redis.pcall(\"EXEC\")`, \"0\")\n\t\tc.Do(\"GET\", \"foo\")\n\t})\n}\n\nfunc TestScriptNoAuth(t *testing.T) {\n\tskip(t)\n\ttestAuth(t,\n\t\t\"supersecret\",\n\t\tfunc(c *client) {\n\t\t\tc.Error(\"Authentication required\", \"EVAL\", `redis.call(\"ECHO\", \"foo\")`, \"0\")\n\t\t\tc.Do(\"AUTH\", \"supersecret\")\n\t\t\tc.Do(\"EVAL\", `redis.call(\"ECHO\", \"foo\")`, \"0\")\n\t\t},\n\t)\n}\n\nfunc TestScriptReplicate(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\n\t\t\t\"EVAL\", `redis.replicate_commands();`, \"0\",\n\t\t)\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\n\t\t\t\"EVAL\", `redis.set_repl(redis.REPL_NONE);`, \"0\",\n\t\t)\n\t})\n}\n\nfunc TestScriptTx(t *testing.T) {\n\tskip(t)\n\tsha2 := \"bfbf458525d6a0b19200bfd6db3af481156b367b\" // keys[1], argv[1]\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return {KEYS[1],ARGV[1]}\")\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"EVALSHA\", sha2, \"0\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return {KEYS[1],ARGV[1]}\")\n\t\tc.Do(\"EVALSHA\", sha2, \"0\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SCRIPT\", \"LOAD\", \"return {\")\n\t\tc.Do(\"EVALSHA\", \"aaaa\", \"0\")\n\t\tc.DoLoosely(\"EXEC\")\n\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"unknown subcommand\", \"SCRIPT\", \"FOO\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/server_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestServer(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"SET\", \"baz\", \"bak\")\n\t\tc.Do(\"XADD\", \"planets\", \"123-456\", \"name\", \"Earth\")\n\t\tc.Do(\"DBSIZE\")\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.Do(\"DBSIZE\")\n\t\tc.Do(\"SET\", \"baz\", \"bak\")\n\n\t\tc.Do(\"SELECT\", \"0\")\n\t\tc.Do(\"FLUSHDB\")\n\t\tc.Do(\"DBSIZE\")\n\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.Do(\"DBSIZE\")\n\t\tc.Do(\"FLUSHALL\")\n\t\tc.Do(\"DBSIZE\")\n\n\t\tc.Do(\"FLUSHDB\", \"aSyNc\")\n\t\tc.Do(\"FLUSHALL\", \"AsYnC\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"DBSIZE\", \"foo\")\n\t\tc.Error(\"syntax error\", \"FLUSHDB\", \"foo\")\n\t\tc.Error(\"syntax error\", \"FLUSHALL\", \"foo\")\n\t\tc.Error(\"syntax error\", \"FLUSHDB\", \"ASYNC\", \"foo\")\n\t\tc.Error(\"syntax error\", \"FLUSHDB\", \"ASYNC\", \"ASYNC\")\n\t\tc.Error(\"syntax error\", \"FLUSHALL\", \"ASYNC\", \"foo\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"plain\", \"hello\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"plain\")\n\t\tc.Do(\"LPUSH\", \"alist\", \"hello\", \"42\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"alist\")\n\t\tc.Do(\"HSET\", \"ahash\", \"key\", \"value\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"ahash\")\n\t\tc.Do(\"ZADD\", \"asset\", \"0\", \"line\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"asset\")\n\t\tc.Do(\"PFADD\", \"ahll\", \"123\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"ahll\")\n\t\tc.Do(\"XADD\", \"astream\", \"0-1\", \"name\", \"Mercury\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"astream\")\n\t\tc.DoLoosely(\"MEMORY\", \"USAGE\", \"nosuch\")\n\n\t\tc.Error(\"Try MEMORY HELP\", \"MEMORY\", \"FOO\")\n\t\tc.Error(\"wrong number of arguments\", \"MEMORY\", \"USAGE\")\n\t\tc.Error(\"syntax error\", \"MEMORY\", \"USAGE\", \"too\", \"many\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.DoLoosely(\"WAIT\", \"1\", \"1\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"WAIT\", \"1\")\n\t\tc.Error(\"wrong number\", \"WAIT\", \"1\", \"2\", \"3\")\n\t\tc.Error(\"wrong number\", \"WAIT\")\n\t\tc.Error(\"not an integer\", \"WAIT\", \"foo\", \"0\")\n\t\tc.Error(\"not an integer\", \"WAIT\", \"1\", \"foo\")\n\t\t// c.Error(\"out of range\", \"WAIT\", \"-1\", \"0\") // something weird going on\n\t\tc.Error(\"timeout is negative\", \"WAIT\", \"11\", \"-12\")\n\t})\n}\n\nfunc TestServerTLS(t *testing.T) {\n\tskip(t)\n\ttestTLS(t, func(c *client) {\n\t\tc.Do(\"PING\", \"foo\")\n\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/set_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSet(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SADD\", \"s\", \"vuur\", \"noot\")\n\t\tc.Do(\"TYPE\", \"s\")\n\t\tc.Do(\"EXISTS\", \"s\")\n\t\tc.Do(\"SCARD\", \"s\")\n\t\tc.DoSorted(\"SMEMBERS\", \"s\")\n\t\tc.DoSorted(\"SMEMBERS\", \"nosuch\")\n\t\tc.Do(\"SISMEMBER\", \"s\", \"aap\")\n\t\tc.Do(\"SISMEMBER\", \"s\", \"nosuch\")\n\t\tc.Do(\"SMISMEMBER\", \"s\", \"aap\", \"noot\", \"nosuch\")\n\n\t\tc.Do(\"SCARD\", \"nosuch\")\n\t\tc.Do(\"SISMEMBER\", \"nosuch\", \"nosuch\")\n\t\tc.Do(\"SMISMEMBER\", \"nosuch\", \"nosuch\", \"nosuch\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SADD\")\n\t\tc.Error(\"wrong number\", \"SADD\", \"s\")\n\t\tc.Error(\"wrong number\", \"SMEMBERS\")\n\t\tc.Error(\"wrong number\", \"SMEMBERS\", \"too\", \"many\")\n\t\tc.Error(\"wrong number\", \"SCARD\")\n\t\tc.Error(\"wrong number\", \"SCARD\", \"too\", \"many\")\n\t\tc.Error(\"wrong number\", \"SISMEMBER\")\n\t\tc.Error(\"wrong number\", \"SISMEMBER\", \"few\")\n\t\tc.Error(\"wrong number\", \"SISMEMBER\", \"too\", \"many\", \"arguments\")\n\t\tc.Error(\"wrong number\", \"SMISMEMBER\")\n\t\tc.Error(\"wrong number\", \"SMISMEMBER\", \"few\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SADD\", \"str\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"SMEMBERS\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SISMEMBER\", \"str\", \"noot\")\n\t\tc.Error(\"wrong kind\", \"SMISMEMBER\", \"str\", \"noot\")\n\t\tc.Error(\"wrong kind\", \"SCARD\", \"str\")\n\t})\n\n\ttestRESP3(t, func(c *client) {\n\t\tc.Do(\"SMEMBERS\", \"q\")\n\t\tc.Do(\"SADD\", \"q\", \"aap\")\n\t\tc.Do(\"SMEMBERS\", \"q\")\n\t\tc.Do(\"SISMEMBER\", \"q\", \"aap\")\n\t\tc.Do(\"SISMEMBER\", \"q\", \"noot\")\n\t\tc.Do(\"SMISMEMBER\", \"q\", \"aap\", \"noot\", \"nosuch\")\n\t})\n}\n\nfunc TestSetMove(t *testing.T) {\n\tskip(t)\n\t// Move a set around\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"RENAME\", \"s\", \"others\")\n\t\tc.DoSorted(\"SMEMBERS\", \"s\")\n\t\tc.DoSorted(\"SMEMBERS\", \"others\")\n\t\tc.Do(\"MOVE\", \"others\", \"2\")\n\t\tc.DoSorted(\"SMEMBERS\", \"others\")\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.DoSorted(\"SMEMBERS\", \"others\")\n\t})\n}\n\nfunc TestSetDel(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SREM\", \"s\", \"noot\", \"nosuch\")\n\t\tc.Do(\"SCARD\", \"s\")\n\t\tc.DoSorted(\"SMEMBERS\", \"s\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SREM\")\n\t\tc.Error(\"wrong number\", \"SREM\", \"s\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SREM\", \"str\", \"noot\")\n\t})\n}\n\nfunc TestSetSMove(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SMOVE\", \"s\", \"s2\", \"aap\")\n\t\tc.Do(\"SCARD\", \"s\")\n\t\tc.Do(\"SCARD\", \"s2\")\n\t\tc.Do(\"SMOVE\", \"s\", \"s2\", \"nosuch\")\n\t\tc.Do(\"SCARD\", \"s\")\n\t\tc.Do(\"SCARD\", \"s2\")\n\t\tc.Do(\"SMOVE\", \"s\", \"nosuch\", \"noot\")\n\t\tc.Do(\"SCARD\", \"s\")\n\t\tc.Do(\"SCARD\", \"s2\")\n\n\t\tc.Do(\"SMOVE\", \"s\", \"s2\", \"mies\")\n\t\tc.Do(\"SCARD\", \"s\")\n\t\tc.Do(\"EXISTS\", \"s\")\n\t\tc.Do(\"SCARD\", \"s2\")\n\t\tc.Do(\"EXISTS\", \"s2\")\n\n\t\tc.Do(\"SMOVE\", \"s2\", \"s2\", \"mies\")\n\n\t\tc.Do(\"SADD\", \"s5\", \"aap\")\n\t\tc.Do(\"SADD\", \"s6\", \"aap\")\n\t\tc.Do(\"SMOVE\", \"s5\", \"s6\", \"aap\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SMOVE\")\n\t\tc.Error(\"wrong number\", \"SMOVE\", \"s\")\n\t\tc.Error(\"wrong number\", \"SMOVE\", \"s\", \"s2\")\n\t\tc.Error(\"wrong number\", \"SMOVE\", \"s\", \"s2\", \"too\", \"many\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SMOVE\", \"str\", \"s2\", \"noot\")\n\t\tc.Error(\"wrong kind\", \"SMOVE\", \"s2\", \"str\", \"noot\")\n\t})\n}\n\nfunc TestSetSpop(t *testing.T) {\n\tskip(t)\n\tt.Run(\"without count\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"SADD\", \"s\", \"aap\")\n\t\t\tc.Do(\"SPOP\", \"s\")\n\t\t\tc.Do(\"EXISTS\", \"s\")\n\n\t\t\tc.Do(\"SPOP\", \"nosuch\")\n\n\t\t\tc.Do(\"SADD\", \"s\", \"aap\")\n\t\t\tc.Do(\"SADD\", \"s\", \"noot\")\n\t\t\tc.Do(\"SADD\", \"s\", \"mies\")\n\t\t\tc.Do(\"SADD\", \"s\", \"noot\")\n\t\t\tc.Do(\"SCARD\", \"s\")\n\t\t\tc.DoLoosely(\"SMEMBERS\", \"s\")\n\n\t\t\tc.Do(\"SPOP\", \"s\", \"0\")\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"SPOP\")\n\t\t\tc.Do(\"SADD\", \"s\", \"aap\")\n\t\t\tc.Error(\"out of range\", \"SPOP\", \"s\", \"s2\")\n\t\t\tc.Error(\"out of range\", \"SPOP\", \"s\", \"-1\")\n\t\t\tc.Error(\"out of range\", \"SPOP\", \"nosuch\", \"s2\")\n\t\t\t// Wrong type\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"wrong kind\", \"SPOP\", \"str\")\n\t\t})\n\t})\n\n\tt.Run(\"with count\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"SADD\", \"s\", \"aap\")\n\t\t\tc.Do(\"SADD\", \"s\", \"noot\")\n\t\t\tc.Do(\"SADD\", \"s\", \"mies\")\n\t\t\tc.Do(\"SADD\", \"s\", \"vuur\")\n\t\t\tc.DoLoosely(\"SPOP\", \"s\", \"2\")\n\t\t\tc.Do(\"EXISTS\", \"s\")\n\t\t\tc.Do(\"SCARD\", \"s\")\n\n\t\t\tc.DoLoosely(\"SPOP\", \"s\", \"200\")\n\t\t\tc.Do(\"SPOP\", \"s\", \"1\")\n\t\t\tc.Do(\"SPOP\", \"s\", \"0\")\n\t\t\tc.Do(\"SCARD\", \"s\")\n\n\t\t\tc.Do(\"SPOP\", \"nosuch\", \"1\")\n\t\t\tc.Do(\"SPOP\", \"nosuch\", \"0\")\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"out of range\", \"SPOP\", \"foo\", \"one\")\n\t\t\tc.Error(\"out of range\", \"SPOP\", \"foo\", \"-4\")\n\t\t})\n\t})\n}\n\nfunc TestSetSrandmember(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// Set with a single member...\n\t\tc.Do(\"SADD\", \"s\", \"aap\")\n\t\tc.Do(\"SRANDMEMBER\", \"s\")\n\t\tc.Do(\"SRANDMEMBER\", \"s\", \"1\")\n\t\tc.Do(\"SRANDMEMBER\", \"s\", \"5\")\n\t\tc.Do(\"SRANDMEMBER\", \"s\", \"-1\")\n\t\tc.Do(\"SRANDMEMBER\", \"s\", \"-5\")\n\n\t\tc.Do(\"SRANDMEMBER\", \"s\", \"0\")\n\t\tc.Do(\"SRANDMEMBER\", \"nosuch\")\n\t\tc.Do(\"SRANDMEMBER\", \"nosuch\", \"1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SRANDMEMBER\")\n\t\tc.Error(\"not an integer\", \"SRANDMEMBER\", \"s\", \"noint\")\n\t\tc.Error(\"syntax error\", \"SRANDMEMBER\", \"s\", \"1\", \"toomany\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SRANDMEMBER\", \"str\")\n\t})\n\n\ttestRESP3(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"q\", \"aap\")\n\t\tc.Do(\"SRANDMEMBER\", \"q\")\n\t\tc.Do(\"SRANDMEMBER\", \"q\", \"1\")\n\t\tc.Do(\"SRANDMEMBER\", \"q\", \"0\")\n\t})\n}\n\nfunc TestSetSdiff(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s1\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SADD\", \"s2\", \"noot\", \"mies\", \"vuur\")\n\t\tc.Do(\"SADD\", \"s3\", \"mies\", \"wim\")\n\t\tc.DoSorted(\"SDIFF\", \"s1\")\n\t\tc.DoSorted(\"SDIFF\", \"s1\", \"s2\")\n\t\tc.DoSorted(\"SDIFF\", \"s1\", \"s2\", \"s3\")\n\t\tc.Do(\"SDIFF\", \"nosuch\")\n\t\tc.Do(\"SDIFF\", \"s1\", \"nosuch\", \"s2\", \"nosuch\", \"s3\")\n\t\tc.Do(\"SDIFF\", \"s1\", \"s1\")\n\n\t\tc.Do(\"SDIFFSTORE\", \"res\", \"s3\", \"nosuch\", \"s1\")\n\t\tc.Do(\"SMEMBERS\", \"res\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SDIFF\")\n\t\tc.Error(\"wrong number\", \"SDIFFSTORE\")\n\t\tc.Error(\"wrong number\", \"SDIFFSTORE\", \"key\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SDIFF\", \"s1\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SDIFF\", \"nosuch\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SDIFF\", \"str\", \"s1\")\n\t\tc.Error(\"wrong kind\", \"SDIFFSTORE\", \"res\", \"str\", \"s1\")\n\t\tc.Error(\"wrong kind\", \"SDIFFSTORE\", \"res\", \"s1\", \"str\")\n\t})\n\n\ttestRESP3(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s1\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SADD\", \"s2\", \"noot\", \"mies\", \"vuur\")\n\t\tc.DoSorted(\"SDIFF\", \"s1\")\n\t\tc.DoSorted(\"SDIFF\", \"s1\", \"s2\")\n\t\tc.Do(\"SDIFFSTORE\", \"res\", \"s1\", \"s2\")\n\t\tc.Do(\"SMEMBERS\", \"res\")\n\t})\n}\n\nfunc TestSetSinter(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s1\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SADD\", \"s2\", \"noot\", \"mies\", \"vuur\")\n\t\tc.Do(\"SADD\", \"s3\", \"mies\", \"wim\")\n\t\tc.DoSorted(\"SINTER\", \"s1\")\n\t\tc.DoSorted(\"SINTER\", \"s1\", \"s2\")\n\t\tc.DoSorted(\"SINTER\", \"s1\", \"s2\", \"s3\")\n\t\tc.Do(\"SINTER\", \"nosuch\")\n\t\tc.Do(\"SINTER\", \"s1\", \"nosuch\", \"s2\", \"nosuch\", \"s3\")\n\t\tc.DoSorted(\"SINTER\", \"s1\", \"s1\")\n\n\t\tc.Do(\"SINTERSTORE\", \"res\", \"s3\", \"nosuch\", \"s1\")\n\t\tc.Do(\"SMEMBERS\", \"res\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SINTER\")\n\t\tc.Error(\"wrong number\", \"SINTERSTORE\")\n\t\tc.Error(\"wrong number\", \"SINTERSTORE\", \"key\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SINTER\", \"s1\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SINTER\", \"nosuch\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SINTER\", \"str\", \"nosuch\")\n\t\tc.Error(\"wrong kind\", \"SINTER\", \"str\", \"s1\")\n\t\tc.Error(\"wrong kind\", \"SINTERSTORE\", \"res\", \"str\", \"s1\")\n\t\tc.Error(\"wrong kind\", \"SINTERSTORE\", \"res\", \"s1\", \"str\")\n\t})\n}\n\nfunc TestSetSintercard(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"s1\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SADD\", \"s2\", \"noot\", \"mies\", \"vuur\")\n\t\tc.Do(\"SADD\", \"s3\", \"mies\", \"wim\")\n\t\tc.Do(\"SINTERCARD\", \"1\", \"s1\")\n\t\tc.Do(\"SINTERCARD\", \"2\", \"s1\", \"s2\")\n\t\tc.Do(\"SINTERCARD\", \"3\", \"s1\", \"s2\", \"s3\")\n\t\tc.Do(\"SINTERCARD\", \"1\", \"nosuch\")\n\t\tc.Do(\"SINTERCARD\", \"5\", \"s1\", \"nosuch\", \"s2\", \"nosuch\", \"s3\")\n\t\tc.Do(\"SINTERCARD\", \"2\", \"s1\", \"s1\")\n\n\t\tc.Do(\"SINTERCARD\", \"1\", \"s1\", \"LIMIT\", \"1\")\n\t\tc.Do(\"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"0\")\n\t\tc.Do(\"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"1\")\n\t\tc.Do(\"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"2\")\n\t\tc.Do(\"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"3\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SINTERCARD\")\n\t\tc.Error(\"wrong number\", \"SINTERCARD\", \"0\")\n\t\tc.Error(\"wrong number\", \"SINTERCARD\", \"\")\n\t\tc.Error(\"wrong number\", \"SINTERCARD\", \"s1\")\n\t\tc.Error(\"greater than 0\", \"SINTERCARD\", \"s1\", \"s2\")\n\t\tc.Error(\"greater than 0\", \"SINTERCARD\", \"-2\", \"s1\", \"s2\")\n\t\tc.Error(\"greater than 0\", \"SINTERCARD\", \"-1\", \"s1\", \"s2\")\n\t\tc.Error(\"greater than 0\", \"SINTERCARD\", \"0\", \"s1\", \"s2\")\n\t\tc.Error(\"syntax error\", \"SINTERCARD\", \"1\", \"s1\", \"s2\")\n\t\tc.Error(\"can't be greater\", \"SINTERCARD\", \"3\", \"s1\", \"s2\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SINTERCARD\", \"2\", \"s1\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SINTERCARD\", \"2\", \"nosuch\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SINTERCARD\", \"2\", \"str\", \"nosuch\")\n\t\tc.Error(\"wrong kind\", \"SINTERCARD\", \"2\", \"str\", \"s1\")\n\t\tc.Error(\"can't be negative\", \"SINTERCARD\", \"2\", \"s1\", \"s2\", \"LIMIT\", \"-1\")\n\t})\n}\n\nfunc TestSetSunion(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SUNION\", \"s1\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"SUNION\", \"s2\", \"noot\", \"mies\", \"vuur\")\n\t\tc.Do(\"SUNION\", \"s3\", \"mies\", \"wim\")\n\t\tc.Do(\"SUNION\", \"s1\")\n\t\tc.Do(\"SUNION\", \"s1\", \"s2\")\n\t\tc.Do(\"SUNION\", \"s1\", \"s2\", \"s3\")\n\t\tc.Do(\"SUNION\", \"nosuch\")\n\t\tc.Do(\"SUNION\", \"s1\", \"nosuch\", \"s2\", \"nosuch\", \"s3\")\n\t\tc.Do(\"SUNION\", \"s1\", \"s1\")\n\n\t\tc.Do(\"SUNIONSTORE\", \"res\", \"s3\", \"nosuch\", \"s1\")\n\t\tc.Do(\"SMEMBERS\", \"res\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"SUNION\")\n\t\tc.Error(\"wrong number\", \"SUNIONSTORE\")\n\t\tc.Error(\"wrong number\", \"SUNIONSTORE\", \"key\")\n\t\t// Wrong type\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"SUNION\", \"s1\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SUNION\", \"nosuch\", \"str\")\n\t\tc.Error(\"wrong kind\", \"SUNION\", \"str\", \"s1\")\n\t\tc.Error(\"wrong kind\", \"SUNIONSTORE\", \"res\", \"str\", \"s1\")\n\t\tc.Error(\"wrong kind\", \"SUNIONSTORE\", \"res\", \"s1\", \"str\")\n\t})\n}\n\nfunc TestSscan(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// No set yet\n\t\tc.Do(\"SSCAN\", \"set\", \"0\")\n\n\t\tc.Do(\"SADD\", \"set\", \"key1\")\n\t\tc.Do(\"SSCAN\", \"set\", \"0\")\n\t\tc.Do(\"SSCAN\", \"set\", \"0\", \"COUNT\", \"12\")\n\t\tc.Do(\"SSCAN\", \"set\", \"0\", \"cOuNt\", \"12\")\n\n\t\tc.Do(\"SADD\", \"set\", \"anotherkey\")\n\t\tc.Do(\"SSCAN\", \"set\", \"0\", \"MATCH\", \"anoth*\")\n\t\tc.Do(\"SSCAN\", \"set\", \"0\", \"MATCH\", \"anoth*\", \"COUNT\", \"100\")\n\t\tc.Do(\"SSCAN\", \"set\", \"0\", \"COUNT\", \"100\", \"MATCH\", \"anoth*\")\n\t\t// c.DoLoosely(\"SSCAN\", \"set\", \"0\", \"COUNT\", \"1\") // cursor differs // unstable test\n\t\tc.DoLoosely(\"SSCAN\", \"set\", \"0\", \"COUNT\", \"2\") // cursor differs\n\n\t\t// Can't really test multiple keys.\n\t\t// c.Do(\"SET\", \"key2\", \"value2\")\n\t\t// c.Do(\"SCAN\", \"0\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"SSCAN\")\n\t\tc.Error(\"wrong number\", \"SSCAN\", \"noint\")\n\t\tc.Error(\"not an integer\", \"SSCAN\", \"set\", \"0\", \"COUNT\", \"noint\")\n\t\tc.Error(\"syntax error\", \"SSCAN\", \"set\", \"0\", \"COUNT\", \"0\")\n\t\tc.Error(\"syntax error\", \"SSCAN\", \"set\", \"0\", \"COUNT\")\n\t\tc.Error(\"syntax error\", \"SSCAN\", \"set\", \"0\", \"MATCH\")\n\t\tc.Error(\"syntax error\", \"SSCAN\", \"set\", \"0\", \"garbage\")\n\t\tc.Error(\"syntax error\", \"SSCAN\", \"set\", \"0\", \"COUNT\", \"12\", \"MATCH\", \"foo\", \"garbage\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"SSCAN\", \"str\", \"0\")\n\t})\n}\n\nfunc TestSetNoAuth(t *testing.T) {\n\tskip(t)\n\ttestAuth(t,\n\t\t\"supersecret\",\n\t\tfunc(c *client) {\n\t\t\tc.Error(\"Authentication required\", \"SET\", \"foo\", \"bar\")\n\t\t\tc.Do(\"AUTH\", \"supersecret\")\n\t\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/sorted_set_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSortedSet(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\", \"1\", \"aap\", \"2\", \"noot\", \"3\", \"mies\")\n\t\tc.Do(\"ZADD\", \"z\", \"1\", \"vuur\", \"4\", \"noot\")\n\t\tc.Do(\"TYPE\", \"z\")\n\t\tc.Do(\"EXISTS\", \"z\")\n\t\tc.Do(\"ZCARD\", \"z\")\n\n\t\tc.Do(\"ZRANK\", \"z\", \"aap\")\n\t\tc.Do(\"ZRANK\", \"z\", \"noot\")\n\t\tc.Do(\"ZRANK\", \"z\", \"mies\")\n\t\tc.Do(\"ZRANK\", \"z\", \"vuur\")\n\t\tc.Do(\"ZRANK\", \"z\", \"nosuch\")\n\t\tc.Do(\"ZRANK\", \"z\", \"vuur\", \"WITHSCORE\")\n\t\tc.Do(\"ZRANK\", \"nosuch\", \"nosuch\")\n\t\tc.Do(\"ZRANK\", \"z\", \"nosuch\", \"WITHSCORE\")\n\t\tc.Do(\"ZRANK\", \"nosuch\", \"nosuch\", \"WITHSCORE\")\n\t\tc.Do(\"ZREVRANK\", \"z\", \"aap\")\n\t\tc.Do(\"ZREVRANK\", \"z\", \"noot\")\n\t\tc.Do(\"ZREVRANK\", \"z\", \"mies\")\n\t\tc.Do(\"ZREVRANK\", \"z\", \"vuur\")\n\t\tc.Do(\"ZREVRANK\", \"z\", \"nosuch\")\n\t\tc.Do(\"ZREVRANK\", \"nosuch\", \"nosuch\")\n\t\tc.Do(\"ZREVRANK\", \"z\", \"noot\", \"WITHSCORE\")\n\t\tc.Do(\"ZREVRANK\", \"nosuch\", \"nosuch\", \"WITHSCORE\")\n\n\t\tc.Do(\"ZADD\", \"zi\", \"inf\", \"aap\", \"-inf\", \"noot\", \"+inf\", \"mies\")\n\t\tc.Do(\"ZRANK\", \"zi\", \"noot\")\n\n\t\t// Double key\n\t\tc.Do(\"ZADD\", \"zz\", \"1\", \"aap\", \"2\", \"aap\")\n\t\tc.Do(\"ZCARD\", \"zz\")\n\n\t\tc.Do(\"ZPOPMAX\", \"zz\", \"2\")\n\t\tc.Do(\"ZPOPMAX\", \"zz\")\n\t\tc.Error(\"out of range\", \"ZPOPMAX\", \"zz\", \"-100\")\n\t\tc.Do(\"ZPOPMAX\", \"nosuch\", \"1\")\n\t\tc.Do(\"ZPOPMAX\", \"zz\", \"0\")\n\t\tc.Do(\"ZPOPMAX\", \"zz\", \"100\")\n\n\t\tc.Do(\"ZPOPMIN\", \"zz\", \"2\")\n\t\tc.Do(\"ZPOPMIN\", \"zz\")\n\t\tc.Error(\"out of range\", \"ZPOPMIN\", \"zz\", \"-100\")\n\t\tc.Do(\"ZPOPMIN\", \"nosuch\", \"1\")\n\t\tc.Do(\"ZPOPMIN\", \"zz\", \"0\")\n\t\tc.Do(\"ZPOPMIN\", \"zz\", \"100\")\n\n\t\t// failure cases\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong number\", \"ZADD\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"s\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"s\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZADD\", \"s\", \"1\", \"aap\", \"1\")\n\t\tc.Error(\"not a valid float\", \"ZADD\", \"s\", \"nofloat\", \"aap\")\n\t\tc.Error(\"wrong kind\", \"ZADD\", \"str\", \"1\", \"aap\")\n\t\tc.Error(\"wrong number\", \"ZCARD\")\n\t\tc.Error(\"wrong number\", \"ZCARD\", \"too\", \"many\")\n\t\tc.Error(\"wrong kind\", \"ZCARD\", \"str\")\n\t\tc.Error(\"wrong number\", \"ZRANK\")\n\t\tc.Error(\"wrong number\", \"ZRANK\", \"key\")\n\t\tc.Error(\"syntax error\", \"ZRANK\", \"key\", \"too\", \"many\")\n\t\tc.Error(\"wrong kind\", \"ZRANK\", \"str\", \"member\")\n\t\tc.Error(\"wrong number\", \"ZREVRANK\")\n\t\tc.Error(\"wrong number\", \"ZREVRANK\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZPOPMAX\")\n\t\tc.Error(\"out of range\", \"ZPOPMAX\", \"set\", \"noint\")\n\t\tc.Error(\"syntax error\", \"ZPOPMAX\", \"set\", \"1\", \"toomany\")\n\t\tc.Error(\"wrong number\", \"ZPOPMIN\")\n\t\tc.Error(\"out of range\", \"ZPOPMIN\", \"set\", \"noint\")\n\t\tc.Error(\"syntax error\", \"ZPOPMIN\", \"set\", \"1\", \"toomany\")\n\t\tc.Error(\"syntax error\", \"ZRANK\", \"z\", \"nosuch\", \"WITHSCORES\")\n\t\tc.Error(\"syntax error\", \"ZREVRANK\", \"z\", \"nosuch\", \"WITHSCORES\")\n\n\t\tc.Do(\"RENAME\", \"z\", \"z2\")\n\t\tc.Do(\"EXISTS\", \"z\")\n\t\tc.Do(\"EXISTS\", \"z2\")\n\t\tc.Do(\"MOVE\", \"z2\", \"3\")\n\t\tc.Do(\"EXISTS\", \"z2\")\n\t\tc.Do(\"SELECT\", \"3\")\n\t\tc.Do(\"EXISTS\", \"z2\")\n\t\tc.Do(\"DEL\", \"z2\")\n\t\tc.Do(\"EXISTS\", \"z2\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\", \"0\", \"new\\nline\\n\")\n\t\tc.Do(\"ZADD\", \"z\", \"0\", \"line\")\n\t\tc.Do(\"ZADD\", \"z\", \"0\", \"another\\nnew\\nline\\n\")\n\t\tc.Do(\"ZSCAN\", \"z\", \"0\", \"MATCH\", \"*\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"[a\", \"[z\")\n\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\", \"WITHSCORES\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\t// very small values\n\t\tc.Do(\"ZADD\", \"a_zset\", \"1.2\", \"one\")\n\t\tc.Do(\"ZADD\", \"a_zset\", \"incr\", \"1.2\", \"one\")\n\t\tc.DoRounded(1, \"ZADD\", \"a_zset\", \"incr\", \"1.2\", \"one\") // real: 3.5999999999999996, mini: 3.6\n\t\tc.Do(\"ZADD\", \"a_zset\", \"incr\", \"1.2\", \"one\")\n\t})\n}\n\nfunc TestSortedSetAdd(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"z\", \"NX\",\n\t\t\t\"1.1\", \"aap\",\n\t\t\t\"3\", \"mies\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"z\", \"XX\",\n\t\t\t\"1.2\", \"aap\",\n\t\t\t\"4\", \"vuur\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"z\", \"CH\",\n\t\t\t\"1.2\", \"aap\",\n\t\t\t\"4.1\", \"vuur\",\n\t\t\t\"5\", \"roos\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"z\", \"CH\", \"XX\",\n\t\t\t\"1.2\", \"aap\",\n\t\t\t\"4.2\", \"vuur\",\n\t\t\t\"5\", \"roos\",\n\t\t\t\"5\", \"zand\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"z\", \"XX\", \"XX\", \"XX\", \"XX\",\n\t\t\t\"1.2\", \"aap\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"z\", \"NX\", \"NX\", \"NX\", \"NX\",\n\t\t\t\"1.2\", \"aap\",\n\t\t)\n\t\tc.Error(\"not compatible\", \"ZADD\", \"z\", \"XX\", \"NX\", \"1.1\", \"foo\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"z\", \"XX\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"z\", \"NX\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"z\", \"CH\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"z\", \"??\")\n\t\tc.Error(\"syntax error\", \"ZADD\", \"z\", \"1.2\", \"aap\", \"XX\")\n\t\tc.Error(\"syntax error\", \"ZADD\", \"z\", \"1.2\", \"aap\", \"CH\")\n\t\tc.Error(\"wrong number\", \"ZADD\", \"z\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"1\", \"aap\")\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"1\", \"aap\")\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"1\", \"aap\")\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"-12\", \"aap\")\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"INCR\", \"-12\", \"aap\")\n\t\tc.Do(\"ZADD\", \"z\", \"CH\", \"INCR\", \"-12\", \"aap\") // 'CH' is ignored\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"CH\", \"-12\", \"aap\") // 'CH' is ignored\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"NX\", \"12\", \"aap\")\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"XX\", \"12\", \"aap\")\n\t\tc.Do(\"ZADD\", \"q\", \"INCR\", \"NX\", \"12\", \"aap\")\n\t\tc.Do(\"ZADD\", \"q\", \"INCR\", \"XX\", \"12\", \"aap\")\n\n\t\tc.Error(\"INCR option\", \"ZADD\", \"z\", \"INCR\", \"1\", \"aap\", \"2\", \"tiger\")\n\t\tc.Error(\"syntax error\", \"ZADD\", \"z\", \"INCR\", \"-12\")\n\t\tc.Error(\"syntax error\", \"ZADD\", \"z\", \"INCR\", \"-12\", \"aap\", \"NX\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\", \"1\", \"score\")\n\t\tc.Do(\"ZADD\", \"z\", \"GT\", \"2\", \"score\")\n\t\tc.Do(\"ZADD\", \"z\", \"LT\", \"1\", \"score\")\n\n\t\tc.Error(\"ERR GT, LT, and/or NX options at the same time are not compatible\", \"ZADD\", \"z\", \"GT\", \"LT\", \"1\", \"score\")\n\t})\n\n\ttestRESP3(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\", \"INCR\", \"1\", \"aap\")\n\t})\n}\n\nfunc TestSortedSetRange(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t\t\"3\", \"mies\",\n\t\t\t\"2\", \"nootagain\",\n\t\t\t\"3\", \"miesagain\",\n\t\t\t\"+Inf\", \"the stars\",\n\t\t\t\"+Inf\", \"more stars\",\n\t\t\t\"-Inf\", \"big bang\",\n\t\t)\n\t\tc.Do(\"ZADD\", \"zs\",\n\t\t\t\"5\", \"berlin\",\n\t\t\t\"5\", \"lisbon\",\n\t\t\t\"5\", \"manila\",\n\t\t\t\"5\", \"budapest\",\n\t\t\t\"5\", \"london\",\n\t\t\t\"5\", \"singapore\",\n\t\t\t\"5\", \"amsterdam\",\n\t\t)\n\n\t\tt.Run(\"plain\", func(t *testing.T) {\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"10\", \"WITHSCORES\", \"WITHSCORES\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\", \"WiThScOrEs\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"10\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"2\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"2\", \"20\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-4\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"2\", \"-4\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"400\", \"-1\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"300\", \"-110\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\", \"REV\")\n\t\t\tc.Error(\"not an integer\", \"ZRANGE\", \"z\", \"(0\", \"-1\")\n\t\t\tc.Error(\"not an integer\", \"ZRANGE\", \"z\", \"0\", \"(-1\")\n\t\t\tc.Error(\"combination\", \"ZRANGE\", \"z\", \"0\", \"-1\", \"LIMIT\", \"1\", \"2\")\n\t\t})\n\n\t\tt.Run(\"byscore\", func(t *testing.T) {\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\", \"BYSCORE\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"1000\", \"BYSCORE\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"1\", \"2\", \"BYSCORE\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"1\", \"(2\", \"BYSCORE\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"-inf\", \"+inf\", \"BYSCORE\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"-inf\", \"+inf\", \"BYSCORE\", \"REV\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"-inf\", \"+inf\", \"BYSCORE\", \"LIMIT\", \"0\", \"1\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"-inf\", \"+inf\", \"BYSCORE\", \"LIMIT\", \"1\", \"2\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"-inf\", \"+inf\", \"BYSCORE\", \"LIMIT\", \"0\", \"-1\")\n\t\t\tc.Do(\"ZRANGE\", \"z\", \"-inf\", \"+inf\", \"BYSCORE\", \"REV\", \"LIMIT\", \"0\", \"1\")\n\t\t\tc.Error(\"not a float\", \"ZRANGE\", \"z\", \"[1\", \"2\", \"BYSCORE\")\n\t\t})\n\n\t\tt.Run(\"bylex\", func(t *testing.T) {\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"[be\", \"(ma\", \"BYLEX\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"[be\", \"+\", \"BYLEX\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"-\", \"(ma\", \"BYLEX\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"-\", \"+\", \"BYLEX\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"[be\", \"(ma\", \"BYLEX\", \"REV\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"-\", \"+\", \"BYLEX\", \"LIMIT\", \"0\", \"1\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"-\", \"+\", \"BYLEX\", \"LIMIT\", \"1\", \"3\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"-\", \"+\", \"BYLEX\", \"LIMIT\", \"1\", \"-1\")\n\t\t\tc.Do(\"ZRANGE\", \"zs\", \"-\", \"+\", \"BYLEX\", \"LIMIT\", \"1\", \"-1\", \"REV\")\n\t\t\tc.Error(\"syntax error\", \"ZRANGE\", \"z\", \"[be\", \"[ma\", \"BYSCORE\", \"BYLEX\")\n\t\t\tc.Error(\"range item\", \"ZRANGE\", \"z\", \"be\", \"(ma\", \"BYLEX\")\n\t\t\tc.Error(\"range item\", \"ZRANGE\", \"z\", \"(be\", \"ma\", \"BYLEX\")\n\t\t})\n\n\t\tc.Do(\"ZADD\", \"zz\",\n\t\t\t\"0\", \"aap\",\n\t\t\t\"0\", \"Aap\",\n\t\t\t\"0\", \"AAP\",\n\t\t\t\"0\", \"aAP\",\n\t\t\t\"0\", \"aAp\",\n\t\t)\n\t\tc.Do(\"ZRANGE\", \"zz\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZRANGE\")\n\t\tc.Error(\"wrong number\", \"ZRANGE\", \"foo\")\n\t\tc.Error(\"wrong number\", \"ZRANGE\", \"foo\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZRANGE\", \"foo\", \"2\", \"3\", \"toomany\")\n\t\tc.Error(\"syntax error\", \"ZRANGE\", \"foo\", \"2\", \"3\", \"WITHSCORES\", \"toomany\")\n\t\tc.Error(\"not an integer\", \"ZRANGE\", \"foo\", \"noint\", \"3\")\n\t\tc.Error(\"not an integer\", \"ZRANGE\", \"foo\", \"2\", \"noint\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZRANGE\", \"str\", \"300\", \"-110\")\n\t})\n}\n\nfunc TestSortedSetRevRange(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t\t\"3\", \"mies\",\n\t\t\t\"2\", \"nootagain\",\n\t\t\t\"3\", \"miesagain\",\n\t\t\t\"+Inf\", \"the stars\",\n\t\t\t\"+Inf\", \"more stars\",\n\t\t\t\"-Inf\", \"big bang\",\n\t\t)\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"0\", \"-1\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"0\", \"-1\", \"WITHSCORES\", \"WITHSCORES\", \"WITHSCORES\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"0\", \"-1\", \"WiThScOrEs\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"0\", \"-2\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"0\", \"-1000\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"2\", \"-2\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"400\", \"-1\")\n\t\tc.Do(\"ZREVRANGE\", \"z\", \"300\", \"-110\")\n\t\tc.Error(\"syntax\", \"ZREVRANGE\", \"z\", \"300\", \"-110\", \"REV\")\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZREVRANGE\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZREVRANGE\", \"str\", \"300\", \"-110\")\n\t})\n}\n\nfunc TestSortedSetRem(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t\t\"3\", \"mies\",\n\t\t\t\"2\", \"nootagain\",\n\t\t\t\"3\", \"miesagain\",\n\t\t\t\"+Inf\", \"the stars\",\n\t\t\t\"+Inf\", \"more stars\",\n\t\t\t\"-Inf\", \"big bang\",\n\t\t)\n\t\tc.Do(\"ZREM\", \"z\", \"nosuch\")\n\t\tc.Do(\"ZREM\", \"z\", \"mies\", \"nootagain\")\n\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZREM\")\n\t\tc.Error(\"wrong number\", \"ZREM\", \"foo\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZREM\", \"str\", \"member\")\n\t})\n}\n\nfunc TestSortedSetRemRangeByLex(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"12\", \"zero kelvin\",\n\t\t\t\"12\", \"minusfour\",\n\t\t\t\"12\", \"one\",\n\t\t\t\"12\", \"oneone\",\n\t\t\t\"12\", \"two\",\n\t\t\t\"12\", \"zwei\",\n\t\t\t\"12\", \"three\",\n\t\t\t\"12\", \"drei\",\n\t\t\t\"12\", \"inf\",\n\t\t)\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\")\n\t\tc.Do(\"ZREMRANGEBYLEX\", \"z\", \"[o\", \"(t\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\")\n\t\tc.Do(\"ZREMRANGEBYLEX\", \"z\", \"-\", \"+\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYLEX\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYLEX\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYLEX\", \"key\", \"[a\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"c\")\n\t\tc.Error(\"not valid string range\", \"ZREMRANGEBYLEX\", \"key\", \"!a\", \"[b\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZREMRANGEBYLEX\", \"str\", \"[a\", \"[b\")\n\t})\n}\n\nfunc TestSortedSetRemRangeByRank(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"12\", \"zero kelvin\",\n\t\t\t\"12\", \"minusfour\",\n\t\t\t\"12\", \"one\",\n\t\t\t\"12\", \"oneone\",\n\t\t\t\"12\", \"two\",\n\t\t\t\"12\", \"zwei\",\n\t\t\t\"12\", \"three\",\n\t\t\t\"12\", \"drei\",\n\t\t\t\"12\", \"inf\",\n\t\t)\n\t\tc.Do(\"ZREMRANGEBYRANK\", \"z\", \"-2\", \"-1\")\n\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\")\n\t\tc.Do(\"ZREMRANGEBYRANK\", \"z\", \"-2\", \"-1\")\n\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\")\n\t\tc.Do(\"ZREMRANGEBYRANK\", \"z\", \"0\", \"-1\")\n\t\tc.Do(\"EXISTS\", \"z\")\n\n\t\tc.Do(\"ZREMRANGEBYRANK\", \"nosuch\", \"-2\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYRANK\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYRANK\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYRANK\", \"key\", \"0\")\n\t\tc.Error(\"not an integer\", \"ZREMRANGEBYRANK\", \"key\", \"noint\", \"-1\")\n\t\tc.Error(\"not an integer\", \"ZREMRANGEBYRANK\", \"key\", \"0\", \"noint\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYRANK\", \"key\", \"0\", \"1\", \"too many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZREMRANGEBYRANK\", \"str\", \"0\", \"-1\")\n\t})\n}\n\nfunc TestSortedSetRemRangeByScore(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t\t\"3\", \"mies\",\n\t\t\t\"2\", \"nootagain\",\n\t\t\t\"3\", \"miesagain\",\n\t\t\t\"+Inf\", \"the stars\",\n\t\t\t\"+Inf\", \"more stars\",\n\t\t\t\"-Inf\", \"big bang\",\n\t\t)\n\t\tc.Do(\"ZREMRANGEBYSCORE\", \"z\", \"-inf\", \"(2\")\n\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\")\n\t\tc.Do(\"ZREMRANGEBYSCORE\", \"z\", \"(1000\", \"(2000\")\n\t\tc.Do(\"ZRANGE\", \"z\", \"0\", \"-1\")\n\t\tc.Do(\"ZREMRANGEBYSCORE\", \"z\", \"-inf\", \"+inf\")\n\t\tc.Do(\"EXISTS\", \"z\")\n\n\t\tc.Do(\"ZREMRANGEBYSCORE\", \"nosuch\", \"-inf\", \"inf\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYSCORE\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYSCORE\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYSCORE\", \"key\", \"0\")\n\t\tc.Error(\"not a float\", \"ZREMRANGEBYSCORE\", \"key\", \"noint\", \"-1\")\n\t\tc.Error(\"not a float\", \"ZREMRANGEBYSCORE\", \"key\", \"0\", \"noint\")\n\t\tc.Error(\"wrong number\", \"ZREMRANGEBYSCORE\", \"key\", \"0\", \"1\", \"too many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZREMRANGEBYSCORE\", \"str\", \"0\", \"-1\")\n\t})\n}\n\nfunc TestSortedSetScore(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t\t\"3\", \"mies\",\n\t\t\t\"2\", \"nootagain\",\n\t\t\t\"3\", \"miesagain\",\n\t\t\t\"+Inf\", \"the stars\",\n\t\t)\n\t\tc.Do(\"ZSCORE\", \"z\", \"mies\")\n\t\tc.Do(\"ZSCORE\", \"z\", \"the stars\")\n\t\tc.Do(\"ZSCORE\", \"z\", \"nosuch\")\n\t\tc.Do(\"ZSCORE\", \"nosuch\", \"nosuch\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZSCORE\")\n\t\tc.Error(\"wrong number\", \"ZSCORE\", \"foo\")\n\t\tc.Error(\"wrong number\", \"ZSCORE\", \"foo\", \"too\", \"many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZSCORE\", \"str\", \"member\")\n\t})\n}\n\nfunc TestSortedSetRangeByScore(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"1\", \"aap\",\n\t\t\t\"2\", \"noot\",\n\t\t\t\"3\", \"mies\",\n\t\t\t\"2\", \"nootagain\",\n\t\t\t\"3\", \"miesagain\",\n\t\t\t\"+Inf\", \"the stars\",\n\t\t\t\"+Inf\", \"more stars\",\n\t\t\t\"-Inf\", \"big bang\",\n\t\t)\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"1\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"-1\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"1\", \"-2\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"z\", \"inf\", \"-inf\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"z\", \"inf\", \"-inf\", \"LIMIT\", \"1\", \"2\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"z\", \"inf\", \"-inf\", \"LIMIT\", \"-1\", \"2\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"z\", \"inf\", \"-inf\", \"LIMIT\", \"1\", \"-2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"WITHSCORES\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"WiThScOrEs\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"WITHSCORES\", \"LIMIT\", \"1\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"0\", \"3\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"0\", \"inf\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"(1\", \"3\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"(1\", \"(3\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"1\", \"(3\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"1\", \"(3\", \"LIMIT\", \"0\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"foo\", \"2\", \"3\", \"LIMIT\", \"1\", \"2\", \"WITHSCORES\")\n\t\tc.Do(\"ZCOUNT\", \"z\", \"-inf\", \"inf\")\n\t\tc.Do(\"ZCOUNT\", \"z\", \"0\", \"3\")\n\t\tc.Do(\"ZCOUNT\", \"z\", \"0\", \"inf\")\n\t\tc.Do(\"ZCOUNT\", \"z\", \"(2\", \"inf\")\n\n\t\t// Bunch of limit edge cases\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"0\", \"7\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"0\", \"8\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"0\", \"9\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"7\", \"0\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"7\", \"1\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"7\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"8\", \"0\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"8\", \"1\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"8\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"9\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"-1\", \"2\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"LIMIT\", \"-1\", \"-1\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZRANGEBYSCORE\")\n\t\tc.Error(\"wrong number\", \"ZRANGEBYSCORE\", \"foo\")\n\t\tc.Error(\"wrong number\", \"ZRANGEBYSCORE\", \"foo\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZRANGEBYSCORE\", \"foo\", \"2\", \"3\", \"toomany\")\n\t\tc.Error(\"syntax error\", \"ZRANGEBYSCORE\", \"foo\", \"2\", \"3\", \"WITHSCORES\", \"toomany\")\n\t\tc.Error(\"not an integer\", \"ZRANGEBYSCORE\", \"foo\", \"2\", \"3\", \"LIMIT\", \"noint\", \"1\")\n\t\tc.Error(\"not an integer\", \"ZRANGEBYSCORE\", \"foo\", \"2\", \"3\", \"LIMIT\", \"1\", \"noint\")\n\t\tc.Error(\"syntax error\", \"ZREVRANGEBYSCORE\", \"z\", \"-inf\", \"inf\", \"WITHSCORES\", \"LIMIT\", \"1\", \"-2\", \"toomany\")\n\t\tc.Error(\"not a float\", \"ZRANGEBYSCORE\", \"foo\", \"noint\", \"3\")\n\t\tc.Error(\"not a float\", \"ZRANGEBYSCORE\", \"foo\", \"[4\", \"3\")\n\t\tc.Error(\"not a float\", \"ZRANGEBYSCORE\", \"foo\", \"2\", \"noint\")\n\t\tc.Error(\"not a float\", \"ZRANGEBYSCORE\", \"foo\", \"4\", \"[3\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZRANGEBYSCORE\", \"str\", \"300\", \"-110\")\n\n\t\tc.Error(\"wrong number\", \"ZREVRANGEBYSCORE\")\n\t\tc.Error(\"not a float\", \"ZREVRANGEBYSCORE\", \"foo\", \"[4\", \"3\")\n\t\tc.Error(\"wrong kind\", \"ZREVRANGEBYSCORE\", \"str\", \"300\", \"-110\")\n\n\t\tc.Error(\"wrong number\", \"ZCOUNT\")\n\t\tc.Error(\"not a float\", \"ZCOUNT\", \"foo\", \"[4\", \"3\")\n\t\tc.Error(\"wrong kind\", \"ZCOUNT\", \"str\", \"300\", \"-110\")\n\t})\n\n\t// Issue #10\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"key\", \"3.3\", \"element\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"key\", \"3.3\", \"3.3\")\n\t\tc.Do(\"ZRANGEBYSCORE\", \"key\", \"4.3\", \"4.3\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"key\", \"3.3\", \"3.3\")\n\t\tc.Do(\"ZREVRANGEBYSCORE\", \"key\", \"4.3\", \"4.3\")\n\t})\n}\n\nfunc TestSortedSetRangeByLex(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"z\",\n\t\t\t\"12\", \"zero kelvin\",\n\t\t\t\"12\", \"minusfour\",\n\t\t\t\"12\", \"one\",\n\t\t\t\"12\", \"oneone\",\n\t\t\t\"12\", \"two\",\n\t\t\t\"12\", \"zwei\",\n\t\t\t\"12\", \"three\",\n\t\t\t\"12\", \"drei\",\n\t\t\t\"12\", \"inf\",\n\t\t)\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"+\", \"-\")\n\t\tc.Do(\"ZLEXCOUNT\", \"z\", \"-\", \"+\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"[o\", \"[three\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"[three\", \"[o\")\n\t\tc.Do(\"ZLEXCOUNT\", \"z\", \"[o\", \"[three\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"(o\", \"(z\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"(z\", \"(o\")\n\t\tc.Do(\"ZLEXCOUNT\", \"z\", \"(o\", \"(z\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"+\", \"(z\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"(z\", \"+\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"(a\", \"-\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"-\", \"(a\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"(z\", \"(a\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"(a\", \"(z\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"nosuch\", \"-\", \"+\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"nosuch\", \"+\", \"-\")\n\t\tc.Do(\"ZLEXCOUNT\", \"nosuch\", \"-\", \"+\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\", \"LIMIT\", \"1\", \"2\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"+\", \"-\", \"LIMIT\", \"1\", \"2\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\", \"LIMIT\", \"-1\", \"2\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"+\", \"-\", \"LIMIT\", \"-1\", \"2\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"-\", \"+\", \"LIMIT\", \"1\", \"-2\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"+\", \"-\", \"LIMIT\", \"1\", \"-2\")\n\n\t\tc.Do(\"ZADD\", \"z\", \"12\", \"z\")\n\t\tc.Do(\"ZADD\", \"z\", \"12\", \"zz\")\n\t\tc.Do(\"ZADD\", \"z\", \"12\", \"zzz\")\n\t\tc.Do(\"ZADD\", \"z\", \"12\", \"zzzz\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"[z\", \"+\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"+\", \"[z\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"z\", \"(z\", \"+\")\n\t\tc.Do(\"ZREVRANGEBYLEX\", \"z\", \"+\", \"(z\")\n\t\tc.Do(\"ZLEXCOUNT\", \"z\", \"(z\", \"+\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZRANGEBYLEX\")\n\t\tc.Error(\"wrong number\", \"ZREVRANGEBYLEX\")\n\t\tc.Error(\"wrong number\", \"ZRANGEBYLEX\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZRANGEBYLEX\", \"key\", \"[a\")\n\t\tc.Error(\"syntax error\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"c\")\n\t\tc.Error(\"not valid string range\", \"ZRANGEBYLEX\", \"key\", \"!a\", \"[b\")\n\t\tc.Error(\"not valid string range\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"!b\")\n\t\tc.Error(\"not valid string range\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"b]\")\n\t\tc.Error(\"not valid string range item\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"\")\n\t\tc.Error(\"not valid string range item\", \"ZRANGEBYLEX\", \"key\", \"\", \"[b\")\n\t\tc.Error(\"syntax error\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"LIMIT\")\n\t\tc.Error(\"syntax error\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"LIMIT\", \"1\")\n\t\tc.Error(\"not an integer\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"LIMIT\", \"a\", \"1\")\n\t\tc.Error(\"not an integer\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"LIMIT\", \"1\", \"a\")\n\t\tc.Error(\"syntax error\", \"ZRANGEBYLEX\", \"key\", \"[a\", \"[b\", \"LIMIT\", \"1\", \"1\", \"toomany\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZRANGEBYLEX\", \"str\", \"[a\", \"[b\")\n\n\t\tc.Error(\"wrong number\", \"ZLEXCOUNT\")\n\t\tc.Error(\"wrong number\", \"ZLEXCOUNT\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZLEXCOUNT\", \"key\", \"[a\")\n\t\tc.Error(\"wrong number\", \"ZLEXCOUNT\", \"key\", \"[a\", \"[b\", \"c\")\n\t\tc.Error(\"not valid string range\", \"ZLEXCOUNT\", \"key\", \"!a\", \"[b\")\n\t\tc.Error(\"wrong kind\", \"ZLEXCOUNT\", \"str\", \"[a\", \"[b\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"idx\", \"0\", \"ccc\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"idx\", \"[d\", \"[e\")\n\t\tc.Do(\"ZRANGEBYLEX\", \"idx\", \"[c\", \"[d\")\n\t})\n}\n\nfunc TestSortedSetIncyby(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZINCRBY\", \"z\", \"1.0\", \"m\")\n\t\tc.Do(\"ZINCRBY\", \"z\", \"1.0\", \"m\")\n\t\tc.Do(\"ZINCRBY\", \"z\", \"1.0\", \"m\")\n\t\tc.Do(\"ZINCRBY\", \"z\", \"2.0\", \"m\")\n\t\tc.Do(\"ZINCRBY\", \"z\", \"3\", \"m2\")\n\t\tc.Do(\"ZINCRBY\", \"z\", \"3\", \"m2\")\n\t\tc.Do(\"ZINCRBY\", \"z\", \"3\", \"m2\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZINCRBY\")\n\t\tc.Error(\"wrong number\", \"ZINCRBY\", \"key\")\n\t\tc.Error(\"wrong number\", \"ZINCRBY\", \"key\", \"1.0\")\n\t\tc.Error(\"not a valid float\", \"ZINCRBY\", \"key\", \"nofloat\", \"m\")\n\t\tc.Error(\"wrong number\", \"ZINCRBY\", \"key\", \"1.0\", \"too\", \"many\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZINCRBY\", \"str\", \"1.0\", \"member\")\n\t})\n}\n\nfunc TestZscan(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// No set yet\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\")\n\n\t\tc.Do(\"ZADD\", \"h\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\")\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\", \"COUNT\", \"12\")\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\", \"cOuNt\", \"12\")\n\n\t\t// ZSCAN may return a higher count of items than requested (See https://redis.io/docs/manual/keyspace/), so we must query all items.\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\", \"COUNT\", \"10\") // cursor differs\n\n\t\tc.Do(\"ZADD\", \"h\", \"2.0\", \"anotherkey\")\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\", \"MATCH\", \"anoth*\")\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\", \"MATCH\", \"anoth*\", \"COUNT\", \"100\")\n\t\tc.Do(\"ZSCAN\", \"h\", \"0\", \"COUNT\", \"100\", \"MATCH\", \"anoth*\")\n\n\t\t// Can't really test multiple keys.\n\t\t// c.Do(\"SET\", \"key2\", \"value2\")\n\t\t// c.Do(\"SCAN\", \"0\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"ZSCAN\")\n\t\tc.Error(\"wrong number\", \"ZSCAN\", \"noint\")\n\t\tc.Error(\"not an integer\", \"ZSCAN\", \"h\", \"0\", \"COUNT\", \"noint\")\n\t\tc.Error(\"syntax error\", \"ZSCAN\", \"h\", \"0\", \"COUNT\")\n\t\tc.Error(\"syntax error\", \"ZSCAN\", \"h\", \"0\", \"COUNT\", \"0\")\n\t\tc.Error(\"syntax error\", \"ZSCAN\", \"h\", \"0\", \"COUNT\", \"-1\")\n\t\tc.Error(\"syntax error\", \"ZSCAN\", \"h\", \"0\", \"MATCH\")\n\t\tc.Error(\"syntax error\", \"ZSCAN\", \"h\", \"0\", \"garbage\")\n\t\tc.Error(\"syntax error\", \"ZSCAN\", \"h\", \"0\", \"COUNT\", \"12\", \"MATCH\", \"foo\", \"garbage\")\n\t\t// c.Do(\"ZSCAN\", \"nosuch\", \"0\", \"COUNT\", \"garbage\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"ZSCAN\", \"str\", \"0\")\n\t})\n}\n\nfunc TestZunion(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\t// example from the docs https://redis.io/commands/ZUNION\n\t\tc.Do(\"ZADD\", \"zset1\", \"1\", \"one\")\n\t\tc.Do(\"ZADD\", \"zset1\", \"2\", \"two\")\n\t\tc.Do(\"ZADD\", \"zset2\", \"1\", \"one\")\n\t\tc.Do(\"ZADD\", \"zset2\", \"2\", \"two\")\n\t\tc.Do(\"ZADD\", \"zset2\", \"3\", \"three\")\n\t\tc.Do(\"ZUNION\", \"2\", \"zset1\", \"zset2\")\n\t\tc.Do(\"ZUNION\", \"2\", \"zset1\", \"zset2\", \"WITHSCORES\")\n\t})\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"h1\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h1\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h2\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h2\", \"4.0\", \"key2\")\n\t\tc.Do(\"ZUNION\", \"2\", \"h1\", \"h2\", \"WITHSCORES\")\n\n\t\tc.Do(\"ZUNION\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2.0\", \"12\", \"WITHSCORES\")\n\t\tc.Do(\"ZUNION\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2\", \"-12\", \"WITHSCORES\")\n\n\t\tc.Do(\"ZUNION\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"min\", \"WITHSCORES\")\n\t\tc.Do(\"ZUNION\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"max\", \"WITHSCORES\")\n\t\tc.Do(\"ZUNION\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"sum\", \"WITHSCORES\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"ZUNION\")\n\t\tc.Error(\"wrong number\", \"ZUNION\", \"noint\")\n\t\tc.Error(\"at least 1\", \"ZUNION\", \"0\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZUNION\", \"2\", \"f\")\n\t\tc.Error(\"at least 1\", \"ZUNION\", \"-1\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZUNION\", \"2\", \"f1\", \"f2\", \"f3\")\n\t\tc.Error(\"syntax error\", \"ZUNION\", \"2\", \"f1\", \"f2\", \"WEIGHTS\")\n\t\tc.Error(\"syntax error\", \"ZUNION\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZUNION\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\", \"2\", \"3\")\n\t\tc.Error(\"not a float\", \"ZUNION\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"f\", \"2\")\n\t\tc.Error(\"syntax error\", \"ZUNION\", \"2\", \"f1\", \"f2\", \"AGGREGATE\", \"foo\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"ZUNION\", \"1\", \"str\")\n\t})\n\t// not a sorted set, still fine\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"super\", \"1\", \"2\", \"3\")\n\t\tc.Do(\"SADD\", \"exclude\", \"3\")\n\t\tc.Do(\"ZUNION\", \"2\", \"super\", \"exclude\", \"weights\", \"1\", \"0\", \"aggregate\", \"min\", \"withscores\")\n\t})\n}\n\nfunc TestZunionstore(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"h1\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h1\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h2\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h2\", \"4.0\", \"key2\")\n\t\tc.Do(\"ZUNIONSTORE\", \"res\", \"2\", \"h1\", \"h2\")\n\t\tc.Do(\"ZRANGE\", \"res\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\tc.Do(\"ZUNIONSTORE\", \"weighted\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2.0\", \"12\")\n\t\tc.Do(\"ZRANGE\", \"weighted\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZUNIONSTORE\", \"weighted2\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2\", \"-12\")\n\t\tc.Do(\"ZRANGE\", \"weighted2\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\tc.Do(\"ZUNIONSTORE\", \"amin\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"min\")\n\t\tc.Do(\"ZRANGE\", \"amin\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZUNIONSTORE\", \"amax\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"max\")\n\t\tc.Do(\"ZRANGE\", \"amax\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZUNIONSTORE\", \"asum\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"sum\")\n\t\tc.Do(\"ZRANGE\", \"asum\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"ZUNIONSTORE\")\n\t\tc.Error(\"wrong number\", \"ZUNIONSTORE\", \"h\")\n\t\tc.Error(\"wrong number\", \"ZUNIONSTORE\", \"h\", \"noint\")\n\t\tc.Error(\"at least 1\", \"ZUNIONSTORE\", \"h\", \"0\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZUNIONSTORE\", \"h\", \"2\", \"f\")\n\t\tc.Error(\"at least 1\", \"ZUNIONSTORE\", \"h\", \"-1\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZUNIONSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"f3\")\n\t\tc.Error(\"syntax error\", \"ZUNIONSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\")\n\t\tc.Error(\"syntax error\", \"ZUNIONSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZUNIONSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\", \"2\", \"3\")\n\t\tc.Error(\"not a float\", \"ZUNIONSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"f\", \"2\")\n\t\tc.Error(\"syntax error\", \"ZUNIONSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"AGGREGATE\", \"foo\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"ZUNIONSTORE\", \"h\", \"1\", \"str\")\n\t})\n\t// overwrite\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"h1\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h1\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h2\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h2\", \"4.0\", \"key2\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Do(\"ZUNIONSTORE\", \"str\", \"2\", \"h1\", \"h2\")\n\t\tc.Do(\"TYPE\", \"str\")\n\t\tc.Do(\"ZUNIONSTORE\", \"h2\", \"2\", \"h1\", \"h2\")\n\t\tc.Do(\"ZRANGE\", \"h2\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"TYPE\", \"h1\")\n\t\tc.Do(\"TYPE\", \"h2\")\n\t})\n\t// not a sorted set, still fine\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SADD\", \"super\", \"1\", \"2\", \"3\")\n\t\tc.Do(\"SADD\", \"exclude\", \"3\")\n\t\tc.Do(\"ZUNIONSTORE\", \"tmp\", \"2\", \"super\", \"exclude\", \"weights\", \"1\", \"0\", \"aggregate\", \"min\")\n\t\tc.Do(\"ZRANGE\", \"tmp\", \"0\", \"-1\", \"withscores\")\n\t})\n}\n\nfunc TestZinter(t *testing.T) {\n\tskip(t)\n\t// ZINTER\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"h1\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h1\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h1\", \"3.0\", \"key3\")\n\t\tc.Do(\"ZADD\", \"h2\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h2\", \"4.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h3\", \"4.0\", \"key4\")\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"h1\", \"h2\")\n\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2.0\", \"12\")\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2\", \"-12\")\n\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"min\")\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"max\")\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"sum\")\n\n\t\t// normal set\n\t\tc.Do(\"ZADD\", \"q1\", \"2\", \"f1\")\n\t\tc.Do(\"SADD\", \"q2\", \"f1\")\n\t\tc.Do(\"ZINTER\", \"2\", \"q1\", \"q2\")\n\t\tc.DoSorted(\"ZINTER\", \"2\", \"q1\", \"q2\", \"WITHSCORES\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"ZINTER\")\n\t\tc.Error(\"wrong number\", \"ZINTER\", \"noint\")\n\t\tc.Error(\"at least 1\", \"ZINTER\", \"0\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZINTER\", \"2\", \"f\")\n\t\tc.Error(\"at least 1\", \"ZINTER\", \"-1\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZINTER\", \"2\", \"f1\", \"f2\", \"f3\")\n\t\tc.Error(\"syntax error\", \"ZINTER\", \"2\", \"f1\", \"f2\", \"WEIGHTS\")\n\t\tc.Error(\"syntax error\", \"ZINTER\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZINTER\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\", \"2\", \"3\")\n\t\tc.Error(\"not a float\", \"ZINTER\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"f\", \"2\")\n\t\tc.Error(\"syntax error\", \"ZINTER\", \"2\", \"f1\", \"f2\", \"AGGREGATE\", \"foo\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"ZINTER\", \"1\", \"str\")\n\t})\n\n\t// ZINTERSTORE\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"h1\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h1\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h1\", \"3.0\", \"key3\")\n\t\tc.Do(\"ZADD\", \"h2\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"h2\", \"4.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"h3\", \"4.0\", \"key4\")\n\t\tc.Do(\"ZINTERSTORE\", \"res\", \"2\", \"h1\", \"h2\")\n\t\tc.Do(\"ZRANGE\", \"res\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\tc.Do(\"ZINTERSTORE\", \"weighted\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2.0\", \"12\")\n\t\tc.Do(\"ZRANGE\", \"weighted\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZINTERSTORE\", \"weighted2\", \"2\", \"h1\", \"h2\", \"WEIGHTS\", \"2\", \"-12\")\n\t\tc.Do(\"ZRANGE\", \"weighted2\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\tc.Do(\"ZINTERSTORE\", \"amin\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"min\")\n\t\tc.Do(\"ZRANGE\", \"amin\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZINTERSTORE\", \"amax\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"max\")\n\t\tc.Do(\"ZRANGE\", \"amax\", \"0\", \"-1\", \"WITHSCORES\")\n\t\tc.Do(\"ZINTERSTORE\", \"asum\", \"2\", \"h1\", \"h2\", \"AGGREGATE\", \"sum\")\n\t\tc.Do(\"ZRANGE\", \"asum\", \"0\", \"-1\", \"WITHSCORES\")\n\n\t\t// normal set\n\t\tc.Do(\"ZADD\", \"q1\", \"2\", \"f1\")\n\t\tc.Do(\"SADD\", \"q2\", \"f1\")\n\t\tc.Do(\"ZINTERSTORE\", \"dest\", \"2\", \"q1\", \"q2\")\n\t\tc.Do(\"ZRANGE\", \"dest\", \"0\", \"-1\", \"withscores\")\n\n\t\t// store into self\n\t\tc.Do(\"ZINTERSTORE\", \"q1\", \"2\", \"q1\", \"q2\")\n\t\tc.Do(\"ZRANGE\", \"q1\", \"0\", \"-1\", \"withscores\")\n\t\tc.Do(\"SMEMBERS\", \"q2\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"ZINTERSTORE\")\n\t\tc.Error(\"wrong number\", \"ZINTERSTORE\", \"h\")\n\t\tc.Error(\"wrong number\", \"ZINTERSTORE\", \"h\", \"noint\")\n\t\tc.Error(\"at least 1\", \"ZINTERSTORE\", \"h\", \"0\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZINTERSTORE\", \"h\", \"2\", \"f\")\n\t\tc.Error(\"at least 1\", \"ZINTERSTORE\", \"h\", \"-1\", \"f\")\n\t\tc.Error(\"syntax error\", \"ZINTERSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"f3\")\n\t\tc.Error(\"syntax error\", \"ZINTERSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\")\n\t\tc.Error(\"syntax error\", \"ZINTERSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\")\n\t\tc.Error(\"syntax error\", \"ZINTERSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"1\", \"2\", \"3\")\n\t\tc.Error(\"not a float\", \"ZINTERSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"WEIGHTS\", \"f\", \"2\")\n\t\tc.Error(\"syntax error\", \"ZINTERSTORE\", \"h\", \"2\", \"f1\", \"f2\", \"AGGREGATE\", \"foo\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"ZINTERSTORE\", \"h\", \"1\", \"str\")\n\t})\n}\n\nfunc TestZpopminmax(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"set:zpop\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"set:zpop\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"set:zpop\", \"3.0\", \"key3\")\n\t\tc.Do(\"ZADD\", \"set:zpop\", \"4.0\", \"key4\")\n\t\tc.Do(\"ZADD\", \"set:zpop\", \"5.0\", \"key5\")\n\t\tc.Do(\"ZCARD\", \"set:zpop\")\n\n\t\tc.Do(\"ZSCORE\", \"set:zpop\", \"key1\")\n\t\tc.Do(\"ZSCORE\", \"set:zpop\", \"key5\")\n\n\t\tc.Do(\"ZPOPMIN\", \"set:zpop\")\n\t\tc.Do(\"ZPOPMIN\", \"set:zpop\", \"2\")\n\t\tc.Do(\"ZPOPMIN\", \"set:zpop\", \"100\")\n\t\tc.Error(\"out of range\", \"ZPOPMIN\", \"set:zpop\", \"-100\")\n\n\t\tc.Do(\"ZPOPMAX\", \"set:zpop\")\n\t\tc.Do(\"ZPOPMAX\", \"set:zpop\", \"2\")\n\t\tc.Do(\"ZPOPMAX\", \"set:zpop\", \"100\")\n\t\tc.Error(\"out of range\", \"ZPOPMAX\", \"set:zpop\", \"-100\")\n\t\tc.Do(\"ZPOPMAX\", \"nosuch\", \"1\")\n\n\t\t// Wrong args\n\t\tc.Error(\"wrong number\", \"ZPOPMIN\")\n\t\tc.Error(\"out of range\", \"ZPOPMIN\", \"set:zpop\", \"h1\")\n\t\tc.Error(\"syntax error\", \"ZPOPMIN\", \"set:zpop\", \"1\", \"h2\")\n\t})\n}\n\nfunc TestZrandmember(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"q\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"q\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"q\", \"3.0\", \"key3\")\n\t\tc.Do(\"ZADD\", \"q\", \"4.0\", \"key4\")\n\t\tc.Do(\"ZADD\", \"q\", \"5.0\", \"key5\")\n\t\tc.Do(\"ZCARD\", \"q\")\n\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\")\n\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"3\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"4\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"5\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"6\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"7\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"12\")\n\t\tc.Do(\"ZRANDMEMBER\", \"q\", \"0\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-3\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-4\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-5\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-6\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-7\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-12\")\n\t\tc.Do(\"ZRANDMEMBER\", \"nosuch\")\n\t\tc.Do(\"ZRANDMEMBER\", \"nosuch\", \"4\")\n\t\tc.Do(\"ZRANDMEMBER\", \"nosuch\", \"-4\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"2\", \"WITHSCORES\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"0\", \"WITHSCORES\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"q\", \"-2\", \"WITHSCORES\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"nosuch\", \"2\", \"WITHSCORES\")\n\t\tc.DoLoosely(\"ZRANDMEMBER\", \"nosuch\", \"-2\", \"WITHSCORES\")\n\n\t\t// Wrong args\n\t\tc.Error(\"wrong number\", \"ZRANDMEMBER\")\n\t\tc.Do(\"SET\", \"str\", \"1\")\n\t\tc.Error(\"wrong kind\", \"ZRANDMEMBER\", \"str\")\n\t\tc.Error(\"not an integer\", \"ZRANDMEMBER\", \"q\", \"two\")\n\t})\n}\n\nfunc TestZMScore(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"ZADD\", \"q\", \"1.0\", \"key1\")\n\t\tc.Do(\"ZADD\", \"q\", \"2.0\", \"key2\")\n\t\tc.Do(\"ZADD\", \"q\", \"3.0\", \"key3\")\n\t\tc.Do(\"ZADD\", \"q\", \"4.0\", \"key4\")\n\t\tc.Do(\"ZADD\", \"q\", \"5.0\", \"key5\")\n\n\t\tc.Do(\"ZMSCORE\", \"q\", \"key1\")\n\t\tc.Do(\"ZMSCORE\", \"q\", \"key1 key2 key3\")\n\t\tc.Do(\"ZMSCORE\", \"q\", \"nosuch\")\n\t\tc.Do(\"ZMSCORE\", \"nosuch\", \"key1\")\n\t\tc.Do(\"ZMSCORE\", \"nosuch\", \"key1\", \"key2\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"ZMSCORE\", \"q\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"ZMSCORE\", \"str\", \"key1\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/stream_test.go",
    "content": "package main\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestStream(t *testing.T) {\n\tskip(t)\n\tt.Run(\"XADD\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"0-1\",\n\t\t\t\t\"name\", \"Mercury\",\n\t\t\t)\n\t\t\tc.DoLoosely(\"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"*\",\n\t\t\t\t\"name\", \"Venus\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"18446744073709551000-0\",\n\t\t\t\t\"name\", \"Earth\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"18446744073709551000-*\",\n\t\t\t\t\"name\", \"Pluto\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"reallynosuchkey\",\n\t\t\t\t\"NOMKSTREAM\",\n\t\t\t\t\"*\",\n\t\t\t\t\"name\", \"Earth\",\n\t\t\t)\n\t\t\tc.Error(\"ID specified\", \"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"18446744073709551000-0\", // <-- duplicate\n\t\t\t\t\"name\", \"Earth\",\n\t\t\t)\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\t\t\tc.Do(\"RENAME\", \"planets\", \"planets2\")\n\t\t\tc.Do(\"DEL\", \"planets2\")\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\n\t\t\t// error cases\n\t\t\tc.Error(\"wrong number\", \"XADD\",\n\t\t\t\t\"planets\",\n\t\t\t\t\"1000\",\n\t\t\t\t\"name\", \"Mercury\",\n\t\t\t\t\"ignored\", // <-- not an even number of keys\n\t\t\t)\n\t\t\tc.Error(\"ID specified\", \"XADD\",\n\t\t\t\t\"newplanets\",\n\t\t\t\t\"0\", // <-- invalid key\n\t\t\t\t\"foo\", \"bar\",\n\t\t\t)\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"newplanets\", \"123-123\") // no args\n\t\t\tc.Error(\"stream ID\", \"XADD\", \"newplanets\", \"123-bar\", \"foo\", \"bar\")\n\t\t\tc.Error(\"stream ID\", \"XADD\", \"newplanets\", \"bar-123\", \"foo\", \"bar\")\n\t\t\tc.Error(\"stream ID\", \"XADD\", \"newplanets\", \"123-123-123\", \"foo\", \"bar\")\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\t// c.Do(\"XADD\", \"str\", \"1000\", \"foo\", \"bar\")\n\t\t\t// c.Do(\"XADD\", \"str\", \"invalid-key\", \"foo\", \"bar\")\n\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"planets\")\n\t\t\tc.Error(\"wrong number\", \"XADD\")\n\t\t})\n\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"4\", \"456-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"4\", \"456-2\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"4\", \"456-3\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"4\", \"456-4\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"4\", \"456-5\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"4\", \"456-6\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"~\", \"4\", \"456-7\", \"name\", \"Mercury\")\n\n\t\t\tc.Error(\"not an integer\", \"XADD\", \"planets\", \"MAXLEN\", \"!\", \"4\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Error(\"not an integer\", \"XADD\", \"planets\", \"MAXLEN\", \" ~\", \"4\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Error(\"MAXLEN argument\", \"XADD\", \"planets\", \"MAXLEN\", \"-4\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Error(\"not an integer\", \"XADD\", \"planets\", \"MAXLEN\", \"\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Error(\"not an integer\", \"XADD\", \"planets\", \"MAXLEN\", \"!\", \"four\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Error(\"not an integer\", \"XADD\", \"planets\", \"MAXLEN\", \"~\", \"four\")\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"planets\", \"MAXLEN\", \"~\")\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"planets\", \"MAXLEN\")\n\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"0\", \"456-8\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"not an integer\", \"XADD\", \"str\", \"MAXLEN\", \"four\", \"*\", \"foo\", \"bar\")\n\t\t})\n\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"450-0\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"450-1\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"456-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"456-2\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"456-3\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"456-4\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"456-5\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"450\", \"456-6\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MINID\", \"~\", \"450\", \"456-7\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\n\t\t\tc.Error(\"equal or smaller than the target\", \"XADD\", \"planets\", \"MINID\", \"450\", \"449-0\", \"name\", \"Earth\")\n\t\t\tc.Error(\"equal or smaller than the target\", \"XADD\", \"planets\", \"MINID\", \"450\", \"450\", \"name\", \"Earth\")\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"planets\", \"MINID\", \"~\")\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"planets\", \"MINID\")\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"planets\", \"MINID\", \"100\")\n\n\t\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\t\tc.Error(\"key holding the wrong kind of value\", \"XADD\", \"str\", \"MINID\", \"400\", \"*\", \"foo\", \"bar\")\n\t\t})\n\t})\n\n\tt.Run(\"transactions\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"EXEC\")\n\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Error(\"wrong number\", \"XADD\", \"newplanets\", \"123-123\") // no args\n\t\t\tc.Error(\"discarded\", \"EXEC\")\n\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"foo-bar\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"EXEC\")\n\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"MAXLEN\", \"four\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"EXEC\")\n\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"XADD\", \"reallynosuchkey\", \"NOMKSTREAM\", \"MAXLEN\", \"four\", \"*\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"EXEC\")\n\t\t})\n\t})\n\n\tt.Run(\"XDEL\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XDEL\", \"newplanets\", \"123-123\")\n\t\t\tc.Do(\"XADD\", \"newplanets\", \"123-123\", \"foo\", \"bar\")\n\t\t\tc.Do(\"XADD\", \"newplanets\", \"123-124\", \"baz\", \"bak\")\n\t\t\tc.Do(\"XADD\", \"newplanets\", \"123-125\", \"bal\", \"bag\")\n\t\t\tc.Do(\"XDEL\", \"newplanets\", \"123-123\", \"123-125\", \"123-123\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"newplanets\", \"0\")\n\t\t\tc.Do(\"XDEL\", \"newplanets\", \"123-123\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"newplanets\", \"0\")\n\t\t\tc.Do(\"XDEL\", \"notexisting\", \"123-123\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"newplanets\", \"0\")\n\n\t\t\tc.Do(\"XADD\", \"gaps\", \"400-400\", \"foo\", \"bar\")\n\t\t\tc.Do(\"XADD\", \"gaps\", \"400-600\", \"foo\", \"bar\")\n\t\t\tc.Do(\"XDEL\", \"gaps\", \"400-500\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"newplanets\", \"0\")\n\n\t\t\t// errors\n\t\t\tc.Do(\"XADD\", \"existing\", \"123-123\", \"foo\", \"bar\")\n\t\t\tc.Error(\"wrong number\", \"XDEL\")             // no key\n\t\t\tc.Error(\"wrong number\", \"XDEL\", \"existing\") // no id\n\t\t\tc.Error(\"Invalid stream ID\", \"XDEL\", \"existing\", \"aa-bb\")\n\t\t\tc.Do(\"XDEL\", \"notexisting\", \"aa-bb\") // invalid id\n\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"XDEL\", \"existing\", \"aa-bb\")\n\t\t\tc.Do(\"EXEC\")\n\t\t})\n\t})\n\n\tt.Run(\"FLUSHALL\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"universe\", \"$\")\n\t\t\tc.Do(\"FLUSHALL\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"planets\", \"0\")\n\t\t\tc.Error(\"consumer group\", \"XREADGROUP\", \"GROUP\", \"universe\", \"alice\", \"STREAMS\", \"planets\", \">\")\n\t\t})\n\t})\n\n\tt.Run(\"XINFO\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-1\", \"name\", \"Mercury\")\n\t\t\t// c.DoLoosely(\"XINFO\", \"STREAM\", \"planets\")\n\n\t\t\tc.Error(\"unknown subcommand\", \"XINFO\", \"STREAMMM\")\n\t\t\tc.Error(\"no such key\", \"XINFO\", \"STREAM\", \"foo\")\n\t\t\tc.Error(\"wrong number\", \"XINFO\")\n\t\t\tc.Do(\"SET\", \"scalar\", \"foo\")\n\t\t\tc.Error(\"wrong kind\", \"XINFO\", \"STREAM\", \"scalar\")\n\n\t\t\tc.Error(\"no such key\", \"XINFO\", \"GROUPS\", \"foo\")\n\t\t\tc.Do(\"XINFO\", \"GROUPS\", \"planets\")\n\n\t\t\tc.Error(\"no such key\", \"XINFO\", \"CONSUMERS\", \"foo\", \"bar\")\n\t\t})\n\t})\n\n\tt.Run(\"XREAD\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"ordplanets\",\n\t\t\t\t\"0-1\",\n\t\t\t\t\"name\", \"Mercury\",\n\t\t\t\t\"greek-god\", \"Hermes\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"ordplanets\",\n\t\t\t\t\"1-0\",\n\t\t\t\t\"name\", \"Venus\",\n\t\t\t\t\"greek-god\", \"Aphrodite\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"ordplanets\",\n\t\t\t\t\"2-1\",\n\t\t\t\t\"greek-god\", \"\",\n\t\t\t\t\"name\", \"Earth\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"ordplanets\",\n\t\t\t\t\"3-0\",\n\t\t\t\t\"name\", \"Mars\",\n\t\t\t\t\"greek-god\", \"Ares\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\",\n\t\t\t\t\"ordplanets\",\n\t\t\t\t\"4-1\",\n\t\t\t\t\"greek-god\", \"Dias\",\n\t\t\t\t\"name\", \"Jupiter\",\n\t\t\t)\n\t\t\tc.Do(\"XADD\", \"ordplanets2\", \"0-1\", \"name\", \"Mercury\", \"greek-god\", \"Hermes\", \"idx\", \"1\")\n\t\t\tc.Do(\"XADD\", \"ordplanets2\", \"1-0\", \"name\", \"Venus\", \"greek-god\", \"Aphrodite\", \"idx\", \"2\")\n\t\t\tc.Do(\"XADD\", \"ordplanets2\", \"2-1\", \"name\", \"Earth\", \"greek-god\", \"\", \"idx\", \"3\")\n\t\t\tc.Do(\"XADD\", \"ordplanets2\", \"3-0\", \"greek-god\", \"Ares\", \"name\", \"Mars\", \"idx\", \"4\")\n\t\t\tc.Do(\"XADD\", \"ordplanets2\", \"4-1\", \"name\", \"Jupiter\", \"greek-god\", \"Dias\", \"idx\", \"5\")\n\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"0\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"2\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"ordplanets2\", \"0\", \"0\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"ordplanets2\", \"2\", \"0\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"ordplanets2\", \"0\", \"2\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"ordplanets2\", \"1\", \"3\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"ordplanets\", \"ordplanets2\", \"0\", \"999\")\n\t\t\tc.Do(\"XREAD\", \"COUNT\", \"1\", \"STREAMS\", \"ordplanets\", \"ordplanets2\", \"0\", \"0\")\n\n\t\t\t// failure cases\n\t\t\tc.Error(\"wrong number\", \"XREAD\")\n\t\t\tc.Error(\"wrong number\", \"XREAD\", \"STREAMS\")\n\t\t\tc.Error(\"wrong number\", \"XREAD\", \"STREAMS\", \"foo\")\n\t\t\tc.Do(\"XREAD\", \"STREAMS\", \"foo\", \"0\")\n\t\t\tc.Error(\"wrong number\", \"XREAD\", \"STREAMS\", \"ordplanets\")\n\t\t\tc.Error(\"Unbalanced 'xread'\", \"XREAD\", \"STREAMS\", \"ordplanets\", \"foo\", \"0\")\n\t\t\tc.Error(\"wrong number\", \"XREAD\", \"COUNT\")\n\t\t\tc.Error(\"wrong number\", \"XREAD\", \"COUNT\", \"notint\")\n\t\t\tc.Error(\"wrong number\", \"XREAD\", \"COUNT\", \"10\") // No streams\n\t\t\tc.Error(\"stream ID\", \"XREAD\", \"STREAMS\", \"foo\", \"notint\")\n\t\t})\n\n\t\ttestRaw2(t, func(c, c2 *client) {\n\t\t\tc.Do(\"XADD\", \"pl\", \"55-88\", \"name\", \"Mercury\")\n\t\t\t// something is available: doesn't block\n\t\t\tc.Do(\"XREAD\", \"BLOCK\", \"10\", \"STREAMS\", \"pl\", \"0\")\n\t\t\tc.Do(\"XREAD\", \"BLOCK\", \"0\", \"STREAMS\", \"pl\", \"0\")\n\n\t\t\t// blocks\n\t\t\tvar wg sync.WaitGroup\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tc.Do(\"XREAD\", \"BLOCK\", \"1000\", \"STREAMS\", \"pl\", \"60\")\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\tc2.Do(\"XADD\", \"pl\", \"60-1\", \"name\", \"Mercury\")\n\t\t\twg.Wait()\n\n\t\t\t// timeout\n\t\t\tc.Do(\"XREAD\", \"BLOCK\", \"10\", \"STREAMS\", \"pl\", \"70\")\n\n\t\t\tc.Error(\"not an int\", \"XREAD\", \"BLOCK\", \"foo\", \"STREAMS\", \"pl\", \"0\")\n\t\t\tc.Error(\"negative\", \"XREAD\", \"BLOCK\", \"-12\", \"STREAMS\", \"pl\", \"0\")\n\t\t})\n\n\t\t// special '$' ID\n\t\ttestRaw2(t, func(c, c2 *client) {\n\t\t\tvar wg sync.WaitGroup\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t\tc2.Do(\"XADD\", \"pl\", \"60-1\", \"name\", \"Mercury\")\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\twg.Wait()\n\t\t\tc.Do(\"XREAD\", \"BLOCK\", \"1000\", \"STREAMS\", \"pl\", \"$\")\n\t\t})\n\n\t\t// special '$' ID on non-existing stream\n\t\ttestRaw2(t, func(c, c2 *client) {\n\t\t\tvar wg sync.WaitGroup\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t\tc2.Do(\"XADD\", \"pl\", \"60-1\", \"nosuch\", \"Mercury\")\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\twg.Wait()\n\t\t\tc.Do(\"XREAD\", \"BLOCK\", \"1000\", \"STREAMS\", \"nosuch\", \"$\")\n\t\t})\n\t})\n}\n\nfunc TestStreamRange(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"XADD\",\n\t\t\t\"ordplanets\",\n\t\t\t\"0-1\",\n\t\t\t\"name\", \"Mercury\",\n\t\t\t\"greek-god\", \"Hermes\",\n\t\t)\n\t\tc.Do(\"XADD\",\n\t\t\t\"ordplanets\",\n\t\t\t\"1-0\",\n\t\t\t\"name\", \"Venus\",\n\t\t\t\"greek-god\", \"Aphrodite\",\n\t\t)\n\t\tc.Do(\"XADD\",\n\t\t\t\"ordplanets\",\n\t\t\t\"2-1\",\n\t\t\t\"greek-god\", \"\",\n\t\t\t\"name\", \"Earth\",\n\t\t)\n\t\tc.Do(\"XADD\",\n\t\t\t\"ordplanets\",\n\t\t\t\"3-0\",\n\t\t\t\"name\", \"Mars\",\n\t\t\t\"greek-god\", \"Ares\",\n\t\t)\n\t\tc.Do(\"XADD\",\n\t\t\t\"ordplanets\",\n\t\t\t\"4-1\",\n\t\t\t\"greek-god\", \"Dias\",\n\t\t\t\"name\", \"Jupiter\",\n\t\t)\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"-\", \"+\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"+\", \"-\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"-\", \"99\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"0\", \"4\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"(0\", \"4\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"0\", \"(4\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"(0\", \"(4\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"2\", \"2\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"2-0\", \"2-1\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"2-1\", \"2-1\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"2-1\", \"2-2\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"0\", \"1-0\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"0\", \"1-99\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"0\", \"2\", \"COUNT\", \"1\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"1-42\", \"3-42\", \"COUNT\", \"1\")\n\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"+\", \"-\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"-\", \"+\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"4\", \"0\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"(4\", \"0\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"4\", \"(0\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"(4\", \"(0\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"2\", \"2\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"2-1\", \"2-0\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"2-1\", \"2-1\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"2-2\", \"2-1\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"1-0\", \"0\")\n\t\tc.Do(\"XREVRANGE\", \"ordplanets\", \"3-42\", \"1-0\", \"COUNT\", \"2\")\n\t\tc.Do(\"DEL\", \"ordplanets\")\n\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"XRANGE\")\n\t\tc.Error(\"wrong number\", \"XRANGE\", \"foo\")\n\t\tc.Error(\"wrong number\", \"XRANGE\", \"foo\", \"1\")\n\t\tc.Error(\"syntax error\", \"XRANGE\", \"foo\", \"2\", \"3\", \"toomany\")\n\t\tc.Error(\"not an integer\", \"XRANGE\", \"foo\", \"2\", \"3\", \"COUNT\", \"noint\")\n\t\tc.Error(\"syntax error\", \"XRANGE\", \"foo\", \"2\", \"3\", \"COUNT\", \"1\", \"toomany\")\n\t\tc.Error(\"stream ID\", \"XRANGE\", \"foo\", \"-\", \"noint\")\n\t\tc.Error(\"stream ID\", \"XRANGE\", \"foo\", \"(-\", \"+\")\n\t\tc.Error(\"stream ID\", \"XRANGE\", \"foo\", \"-\", \"(+\")\n\t\tc.Do(\"SET\", \"str\", \"I am a string\")\n\t\tc.Error(\"wrong kind\", \"XRANGE\", \"str\", \"-\", \"+\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"XADD\",\n\t\t\t\"ordplanets\",\n\t\t\t\"0-1\",\n\t\t\t\"name\", \"Mercury\",\n\t\t\t\"greek-god\", \"Hermes\",\n\t\t)\n\t\tc.Do(\"XLEN\", \"ordplanets\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"+\", \"-\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"+\", \"-\", \"COUNT\", \"FOOBAR\")\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"XLEN\", \"ordplanets\")\n\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"XRANGE\", \"ordplanets\", \"+\", \"foo\")\n\t\tc.Do(\"EXEC\")\n\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"wrong number\", \"XRANGE\", \"ordplanets\", \"+\")\n\t\tc.Error(\"discarded\", \"EXEC\")\n\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"XADD\", \"ordplanets\", \"123123-123\", \"name\", \"Mercury\")\n\t\tc.Do(\"XDEL\", \"ordplanets\", \"123123-123\")\n\t\tc.Do(\"XADD\", \"ordplanets\", \"invalid\", \"name\", \"Mercury\")\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"XLEN\", \"ordplanets\")\n\t})\n}\n\nfunc TestStreamGroup(t *testing.T) {\n\tskip(t)\n\tt.Run(\"XGROUP\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Error(\"to exist\", \"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"123-500\", \"foo\", \"bar\")\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\")\n\t\t\tc.DoLoosely(\"XINFO\", \"GROUPS\", \"planets\") // lag is wrong\n\t\t\tc.Error(\"already exist\", \"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\")\n\t\t\tc.Error(\"to exist\", \"XGROUP\", \"DESTROY\", \"foo\", \"bar\")\n\t\t\tc.Do(\"XGROUP\", \"DESTROY\", \"planets\", \"bar\")\n\t\t\tc.Error(\"No such consumer group\", \"XGROUP\", \"DELCONSUMER\", \"planets\", \"foo\", \"bar\")\n\t\t\tc.Do(\"XGROUP\", \"CREATECONSUMER\", \"planets\", \"processing\", \"alice\")\n\t\t\tc.DoLoosely(\"XINFO\", \"GROUPS\", \"planets\") // lag is wrong\n\t\t\tc.Do(\"XGROUP\", \"DELCONSUMER\", \"planets\", \"processing\", \"foo\")\n\t\t\tc.Do(\"XGROUP\", \"DELCONSUMER\", \"planets\", \"processing\", \"alice\")\n\t\t\tc.Do(\"XINFO\", \"CONSUMERS\", \"planets\", \"processing\")\n\t\t\tc.Do(\"XGROUP\", \"DESTROY\", \"planets\", \"processing\")\n\t\t\tc.Do(\"XINFO\", \"GROUPS\", \"planets\")\n\t\t\tc.Error(\"wrong number of arguments\", \"XGROUP\")\n\t\t\tc.Error(\"unknown subcommand 'foo'\", \"XGROUP\", \"foo\")\n\t\t})\n\t})\n\n\tt.Run(\"XREADGROUP\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\t// succNoResultCheck(\"XINFO\", \"STREAM\", \"planets\"),\n\t\t\tc.Do(\"XADD\", \"planets\", \"42-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"42-2\", \"name\", \"Neptune\")\n\t\t\tc.Do(\"XLEN\", \"planets\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"999\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"0\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"-1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"42-1\")\n\t\t\tc.Do(\"XDEL\", \"planets\", \"42-1\")\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"newcons\", \"$\", \"MKSTREAM\")\n\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"42-3\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"STREAMS\", \"planets\", \"42-1\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"STREAMS\", \"planets\", \"42-9\")\n\t\t\tc.Error(\"stream ID\", \"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"STREAMS\", \"planets\", \"foo\")\n\n\t\t\t// NOACK\n\t\t\t{\n\t\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"colors\", \"pr\", \"$\", \"MKSTREAM\")\n\t\t\t\tc.Do(\"XADD\", \"colors\", \"42-2\", \"name\", \"Green\")\n\t\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"pr\", \"alice\", \"NOACK\", \"STREAMS\", \"colors\", \">\")\n\t\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"pr\", \"alice\", \"NOACK\", \"STREAMS\", \"colors\", \"0\")\n\t\t\t\tc.Do(\"XACK\", \"colors\", \"p\", \"42-2\")\n\t\t\t}\n\n\t\t\t// errors\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\")\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\", \"GROUP\")\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\", \"foo\")\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\", \"GROUP\", \"foo\")\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\")\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\", \"ZTREAMZ\")\n\t\t\tc.Error(\"wrong number\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\", \"STREAMS\", \"foo\")\n\t\t\tc.Error(\"Unbalanced\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\", \"STREAMS\", \"foo\", \"bar\", \">\")\n\t\t\tc.Error(\"syntax error\", \"XREADGROUP\", \"_____\", \"foo\", \"bar\", \"STREAMS\", \"foo\", \">\")\n\t\t\tc.Error(\"consumer group\", \"XREADGROUP\", \"GROUP\", \"nosuch\", \"alice\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Error(\"consumer group\", \"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"STREAMS\", \"nosuchplanets\", \">\")\n\t\t\tc.Do(\"SET\", \"scalar\", \"bar\")\n\t\t\tc.Error(\"wrong kind\", \"XGROUP\", \"CREATE\", \"scalar\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\tc.Error(\"BUSYGROUP\", \"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t})\n\n\t\ttestRaw2(t, func(c, c2 *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"pl\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XADD\", \"pl\", \"55-88\", \"name\", \"Mercury\")\n\t\t\t// something is available: doesn't block\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"foo\", \"BLOCK\", \"10\", \"STREAMS\", \"pl\", \">\")\n\t\t\t// c.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"foo\", \"BLOCK\", \"0\", \"STREAMS\", \"pl\", \">\")\n\n\t\t\t// blocks\n\t\t\t{\n\t\t\t\tvar wg sync.WaitGroup\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func() {\n\t\t\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"foo\", \"BLOCK\", \"999999\", \"STREAMS\", \"pl\", \">\")\n\t\t\t\t\twg.Done()\n\t\t\t\t}()\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t\tc2.Do(\"XADD\", \"pl\", \"60-1\", \"name\", \"Mercury\")\n\t\t\t\twg.Wait()\n\t\t\t}\n\n\t\t\t// timeout\n\t\t\t{\n\t\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"foo\", \"BLOCK\", \"10\", \"STREAMS\", \"pl\", \">\")\n\t\t\t}\n\n\t\t\t// block is ignored if id isn't \">\"\n\t\t\t{\n\t\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"foo\", \"BLOCK\", \"9999999999\", \"STREAMS\", \"pl\", \"8\")\n\t\t\t}\n\n\t\t\t// block is ignored if _any_ id isn't \">\"\n\t\t\t{\n\t\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"pl2\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"foo\", \"BLOCK\", \"9999999999\", \"STREAMS\", \"pl\", \"pl2\", \"8\", \">\")\n\t\t\t}\n\n\t\t\tc.Error(\"not an int\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\", \"BLOCK\", \"foo\", \"STREAMS\", \"foo\", \">\")\n\t\t\tc.Error(\"No such\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\", \"BLOCK\", \"999999\", \"STREAMS\", \"pl\", \"invalid\")\n\t\t\tc.Error(\"negative\", \"XREADGROUP\", \"GROUP\", \"foo\", \"bar\", \"BLOCK\", \"-1\", \"STREAMS\", \"foo\", \">\")\n\t\t})\n\t})\n\n\tt.Run(\"XACK\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-2\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-3\", \"name\", \"not Pluto\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-4\", \"name\", \"Mars\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"4000-2\", \"4000-3\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"4000-4\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"2000-1\")\n\n\t\t\tc.Do(\"XACK\", \"nosuch\", \"processing\", \"0-1\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"nosuch\", \"0-1\")\n\n\t\t\t// error cases\n\t\t\tc.Error(\"wrong number\", \"XACK\")\n\t\t\tc.Error(\"wrong number\", \"XACK\", \"planets\")\n\t\t\tc.Error(\"wrong number\", \"XACK\", \"planets\", \"processing\")\n\t\t\tc.Error(\"Invalid stream\", \"XACK\", \"planets\", \"processing\", \"invalid\")\n\t\t\tc.Do(\"SET\", \"scalar\", \"bar\")\n\t\t\tc.Error(\"wrong kind\", \"XACK\", \"scalar\", \"processing\", \"123-456\")\n\t\t})\n\t})\n\n\tt.Run(\"XPENDING\", func(t *testing.T) {\n\t\t// summary mode\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-2\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-3\", \"name\", \"not Pluto\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-4\", \"name\", \"Mars\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"4000-4\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"4000-1\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"4000-2\")\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"4000-3\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\t// more consumers\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-5\", \"name\", \"Earth\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-6\", \"name\", \"Neptune\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"COUNT\", \"1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\t// no entries doesn't show up in pending\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"eve\", \"COUNT\", \"1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XGROUP\", \"DELCONSUMER\", \"planets\", \"processing\", \"alice\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"empty\", \"empty\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XPENDING\", \"empty\", \"empty\", \"-\", \"+\", \"999\")\n\n\t\t\tc.Error(\"consumer group\", \"XPENDING\", \"foo\", \"processing\")\n\t\t\tc.Error(\"consumer group\", \"XPENDING\", \"planets\", \"foo\")\n\n\t\t\t// error cases\n\t\t\tc.Error(\"wrong number\", \"XPENDING\")\n\t\t\tc.Error(\"wrong number\", \"XPENDING\", \"planets\")\n\t\t\tc.Error(\"syntax\", \"XPENDING\", \"planets\", \"processing\", \"too many\")\n\t\t\tc.Error(\"syntax\", \"XPENDING\", \"planets\", \"processing\", \"IDLE\", \"10\")\n\t\t})\n\n\t\t// full mode\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-2\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-3\", \"name\", \"not Pluto\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-4\", \"name\", \"Mars\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"STREAMS\", \"planets\", \">\")\n\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"999\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"4000-2\", \"+\", \"999\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"4000-3\", \"999\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"1\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"0\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"-1\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"IDLE\", \"10\", \"-\", \"+\", \"999\")\n\n\t\t\tc.Do(\"XADD\", \"planets\", \"4000-5\", \"name\", \"Earth\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"999\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"999\", \"bob\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"999\", \"eve\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"IDLE\", \"10\", \"-\", \"+\", \"999\", \"eve\")\n\n\t\t\t// update delivery counts (which we can't test thanks to the time field)\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"bob\", \"STREAMS\", \"planets\", \"99\")\n\t\t\tc.DoLoosely(\"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"999\", \"bob\")\n\n\t\t\tc.Error(\"Invalid\", \"XPENDING\", \"planets\", \"processing\", \"foo\", \"+\", \"999\")\n\t\t\tc.Error(\"Invalid\", \"XPENDING\", \"planets\", \"processing\", \"-\", \"foo\", \"999\")\n\t\t\tc.Error(\"not an integer\", \"XPENDING\", \"planets\", \"processing\", \"-\", \"+\", \"foo\")\n\t\t\tc.Error(\"not an integer\", \"XPENDING\", \"planets\", \"processing\", \"IDLE\", \"abc\", \"-\", \"+\", \"999\")\n\t\t})\n\t})\n\n\tt.Run(\"XAUTOCLAIM\", func(t *testing.T) {\n\t\t// justid mode\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"colors\", \"pr\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\", \"JUSTID\")\n\t\t\tc.Do(\"XADD\", \"colors\", \"42-2\", \"name\", \"Green\")\n\t\t\tc.Do(\"XADD\", \"colors\", \"42-3\", \"name\", \"Blue\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"pr\", \"alice\", \"STREAMS\", \"colors\", \">\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\", \"JUSTID\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"pr\", \"alice\", \"STREAMS\", \"colors\", \">\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\", \"JUSTID\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\", \"COUNT\", \"1\", \"JUSTID\")\n\t\t\tc.Do(\"XPENDING\", \"colors\", \"pr\")\n\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"eve\", \"0\", \"0\", \"JUSTID\")\n\t\t\tc.Do(\"XPENDING\", \"colors\", \"pr\")\n\n\t\t\tc.Error(\"syntax error\", \"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\", \"JUSTID\", \"foo\")\n\t\t\tc.Error(\"No such key\", \"XAUTOCLAIM\", \"colors\", \"foo\", \"alice\", \"0\", \"0\", \"JUSTID\")\n\t\t\tc.Error(\"No such key\", \"XAUTOCLAIM\", \"foo\", \"pr\", \"alice\", \"0\", \"0\", \"JUSTID\")\n\t\t\tc.Error(\"Invalid min-idle-time\", \"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"foo\", \"0\", \"JUSTID\")\n\t\t\tc.Error(\"Invalid stream ID\", \"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"foo\", \"JUSTID\")\n\t\t\tc.Error(\"Invalid stream ID\", \"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"-1\", \"JUSTID\")\n\t\t})\n\n\t\t// regular mode\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"colors\", \"pr\", \"$\", \"MKSTREAM\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\")\n\t\t\tc.Do(\"XADD\", \"colors\", \"42-2\", \"name\", \"Green\")\n\t\t\tc.Do(\"XADD\", \"colors\", \"42-3\", \"name\", \"Blue\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"pr\", \"alice\", \"STREAMS\", \"colors\", \">\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"pr\", \"alice\", \"STREAMS\", \"colors\", \">\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\")\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"alice\", \"0\", \"0\", \"COUNT\", \"1\")\n\n\t\t\tc.Do(\"XAUTOCLAIM\", \"colors\", \"pr\", \"eve\", \"0\", \"0\")\n\t\t\tc.Do(\"XPENDING\", \"colors\", \"pr\")\n\t\t})\n\t})\n\n\tt.Run(\"XCLAIM\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Error(\"No such key\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-0\")\n\t\t\tc.Do(\"XGROUP\", \"CREATE\", \"planets\", \"processing\", \"$\", \"MKSTREAM\")\n\t\t\tc.Error(\"No such key\", \"XCLAIM\", \"planets\", \"foo\", \"alice\", \"0\", \"0-0\")\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-0\")\n\t\t\tc.DoLoosely(\"XINFO\", \"CONSUMERS\", \"planets\", \"processing\") // \"idle\" is fiddly\n\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-2\", \"name\", \"Venus\")\n\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\")\n\t\t\tc.DoLoosely(\"XINFO\", \"CONSUMERS\", \"planets\", \"processing\") //  \"idle\" is fiddly\n\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\", \"0-2\", \"FORCE\")\n\t\t\tc.Do(\"XINFO\", \"GROUPS\", \"planets\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XDEL\", \"planets\", \"0-1\") // !\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"bob\", \"0\", \"0-1\")\n\t\t\tc.Do(\"XINFO\", \"GROUPS\", \"planets\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-3\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-4\", \"name\", \"Venus\")\n\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"bob\", \"0\", \"0-4\", \"FORCE\")\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"bob\", \"0\", \"0-4\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"COUNT\", \"1\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\t\t\tc.Do(\"XREADGROUP\", \"GROUP\", \"processing\", \"alice\", \"STREAMS\", \"planets\", \">\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-3\", \"RETRYCOUNT\", \"10\", \"IDLE\", \"5000\", \"JUSTID\")\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\", \"0-2\", \"RETRYCOUNT\", \"1\", \"TIME\", \"1\", \"JUSTID\")\n\t\t\tc.Do(\"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\", \"0-4\", \"RETRYCOUNT\", \"1\", \"TIME\", \"1\", \"justid\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Do(\"XACK\", \"planets\", \"processing\", \"0-1\", \"0-2\", \"0-3\", \"0-4\")\n\t\t\tc.Do(\"XPENDING\", \"planets\", \"processing\")\n\n\t\t\tc.Error(\"Unrecognized XCLAIM option\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-3\", \"RETRYCOUNT\", \"10\", \"0-4\", \"IDLE\", \"0\")\n\t\t\tc.Error(\"Unrecognized XCLAIM option\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-3\", \"RETRYCOUNT\", \"10\", \"IDLE\", \"0\", \"0-4\")\n\t\t\tc.Error(\"Invalid min-idle-time\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"foo\", \"0-1\", \"JUSTID\")\n\t\t\tc.Error(\"Invalid IDLE\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\", \"JUSTID\", \"IDLE\", \"foo\")\n\t\t\tc.Error(\"Invalid TIME\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\", \"JUSTID\", \"TIME\", \"foo\")\n\t\t\tc.Error(\"Invalid RETRYCOUNT\", \"XCLAIM\", \"planets\", \"processing\", \"alice\", \"0\", \"0-1\", \"JUSTID\", \"RETRYCOUNT\", \"foo\")\n\t\t})\n\t})\n\n\ttestRESP3(t, func(c *client) {\n\t\tc.DoLoosely(\"XINFO\", \"STREAM\", \"foo\")\n\t})\n}\n\nfunc TestStreamTrim(t *testing.T) {\n\tskip(t)\n\tt.Run(\"XTRIM MAXLEN\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"1-0\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"2-1\", \"name\", \"Earth\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"3-0\", \"name\", \"Mars\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4-1\", \"name\", \"Jupiter\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MAXLEN\", \"3\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MAXLEN\", \"=\", \"3\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MAXLEN\", \"2\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MAXLEN\", \"~\", \"2\", \"LIMIT\", \"99\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\t// error cases\n\t\t\tc.Error(\"not an integer\", \"XTRIM\", \"planets\", \"MAXLEN\", \"abc\")\n\t\t\tc.Error(\"arguments\", \"XTRIM\", \"planets\", \"MAXLEN\")\n\t\t\tc.Error(\"without the special\", \"XTRIM\", \"planets\", \"MAXLEN\", \"3\", \"LIMIT\", \"1\")\n\t\t})\n\t})\n\n\tt.Run(\"XTRIM MINID\", func(t *testing.T) {\n\t\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"XADD\", \"planets\", \"0-1\", \"name\", \"Mercury\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"1-0\", \"name\", \"Venus\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"2-1\", \"name\", \"Earth\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"3-0\", \"name\", \"Mars\")\n\t\t\tc.Do(\"XADD\", \"planets\", \"4-1\", \"name\", \"Jupiter\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MINID\", \"1\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MINID\", \"=\", \"1\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MINID\", \"3\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\tc.Do(\"XTRIM\", \"planets\", \"MINID\", \"~\", \"3\", \"LIMIT\", \"1\")\n\t\t\tc.Do(\"XRANGE\", \"planets\", \"-\", \"+\")\n\n\t\t\t// error cases\n\t\t\tc.Error(\"arguments\", \"XTRIM\", \"planets\", \"MINID\")\n\t\t\tc.Error(\"arguments\", \"XTRIM\", \"planets\")\n\t\t\tc.Error(\"arguments\", \"XTRIM\", \"planets\", \"OTHER\")\n\t\t\tc.Error(\"without the special\", \"XTRIM\", \"planets\", \"MINID\", \"3\", \"LIMIT\", \"1\")\n\t\t\tc.Error(\"out of range\", \"XTRIM\", \"planets\", \"MINID\", \"~\", \"3\", \"LIMIT\", \"one\")\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/string_test.go",
    "content": "package main\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestString(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\\bbaz\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\", \"EX\", \"100\")\n\t\tc.Error(\"not an integer\", \"SET\", \"foo\", \"bar\", \"EX\", \"noint\")\n\t\tc.Do(\"SET\", \"utf8\", \"❆❅❄☃\")\n\t\tc.Do(\"SET\", \"foo\", \"baz\", \"KEEPTTL\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\", \"GET\")\n\t\tc.Do(\"SET\", \"new\", \"bar\", \"GET\")\n\t\tc.Do(\"SET\", \"empty\", \"\", \"GET\")\n\t\tc.Do(\"SET\", \"empty\", \"filled\", \"GET\")\n\t\tc.Do(\"SET\", \"empty\", \"\", \"GET\")\n\n\t\tc.Do(\"SET\", \"fooexat\", \"bar\", \"EXAT\", \"2345678901\")\n\t\tc.DoApprox(10, \"TTL\", \"fooexat\")\n\t\tc.Error(\"not an integer\", \"SET\", \"foo\", \"bar\", \"EXAT\", \"noint\")\n\t\tc.Do(\"SET\", \"foopxat\", \"bar\", \"PXAT\", \"2345678901000\")\n\t\tc.DoApprox(10, \"TTL\", \"foopxat\")\n\t\tc.Error(\"not an integer\", \"SET\", \"foo\", \"bar\", \"PXAT\", \"noint\")\n\t\t// expires right away\n\t\tc.Do(\"SET\", \"gone\", \"bar\", \"EXAT\", \"123\")\n\t\tc.Do(\"EXISTS\", \"gone\")\n\n\t\t// SET NX GET\n\t\tc.Do(\"SET\", \"unique\", \"value1\", \"NX\", \"GET\")\n\t\tc.Do(\"SET\", \"unique\", \"value2\", \"NX\", \"GET\")\n\t\tc.Do(\"SET\", \"unique\", \"value3\", \"XX\", \"GET\")\n\t\tc.Do(\"SET\", \"unique\", \"value4\", \"XX\", \"GET\")\n\t\tc.Do(\"SET\", \"uniquer\", \"value5\", \"XX\", \"GET\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"SET\")\n\t\tc.Error(\"wrong number\", \"SET\", \"foo\")\n\t\tc.Error(\"syntax error\", \"SET\", \"foo\", \"bar\", \"baz\")\n\t\tc.Error(\"wrong number\", \"GET\")\n\t\tc.Error(\"wrong number\", \"GET\", \"too\", \"many\")\n\t\tc.Error(\"invalid expire\", \"SET\", \"foo\", \"bar\", \"EX\", \"0\")\n\t\tc.Error(\"invalid expire\", \"SET\", \"foo\", \"bar\", \"EX\", \"-100\")\n\t\tc.Error(\"syntax error\", \"SET\", \"both\", \"bar\", \"PXAT\", \"3345678901000\", \"EXAT\", \"2345678901\")\n\t\tc.Error(\"invalid expire\", \"SET\", \"foo\", \"bar\", \"EXAT\", \"-100\")\n\t\tc.Error(\"invalid expire\", \"SET\", \"foo\", \"bar\", \"PXAT\", \"-100\")\n\t\tc.Error(\"syntax error\", \"SET\", \"both\", \"bar\", \"PX\", \"6\", \"EX\", \"6\")\n\t\tc.Error(\"syntax error\", \"SET\", \"both\", \"bar\", \"PX\", \"6\", \"EX\", \"0\")\n\t\tc.Error(\"syntax error\", \"SET\", \"both\", \"bar\", \"PX\", \"6\", \"PXAT\", \"2345678901\")\n\t\t// Wrong type\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Error(\"wrong kind\", \"GET\", \"hash\")\n\t\tc.Error(\"wrong kind\", \"SET\", \"hash\", \"foo\", \"GET\")\n\t})\n}\n\nfunc TestStringGetSet(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GETSET\", \"foo\", \"new\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"GET\", \"new\")\n\t\tc.Do(\"GETSET\", \"nosuch\", \"new\")\n\t\tc.Do(\"GET\", \"nosuch\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"GETSET\")\n\t\tc.Error(\"wrong number\", \"GETSET\", \"foo\")\n\t\tc.Error(\"wrong number\", \"GETSET\", \"foo\", \"bar\", \"baz\")\n\t\t// Wrong type\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Error(\"wrong kind\", \"GETSET\", \"hash\", \"new\")\n\t})\n}\n\nfunc TestStringGetex(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"GETEX\", \"missing\")\n\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GETEX\", \"foo\")\n\t\tc.Do(\"TTL\", \"foo\")\n\n\t\tc.Do(\"GETEX\", \"foo\", \"EX\", \"10\")\n\t\tc.Do(\"TTL\", \"foo\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"GETEX\")\n\t\tc.Error(\"syntax error\", \"GETEX\", \"foo\", \"bar\")\n\t\tc.Error(\"syntax error\", \"GETEX\", \"foo\", \"EX\", \"10\", \"PERSIST\")\n\t\tc.Error(\"syntax error\", \"GETEX\", \"foo\", \"EX\", \"10\", \"PX\", \"10\")\n\t\tc.Error(\"not an integer\", \"GETEX\", \"foo\", \"EX\", \"ten\")\n\n\t\t// Wrong type\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Error(\"wrong kind\", \"GETEX\", \"hash\")\n\n\t\tc.Do(\"SET\", \"hittl\", \"bar\")\n\t\tc.Do(\"PEXPIRE\", \"hittl\", \"999999\")\n\t\tc.Do(\"GETEX\", \"hittl\", \"PERSIST\")\n\t\tc.Do(\"TTL\", \"hittl\")\n\t})\n}\n\nfunc TestStringGetdel(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"GETDEL\", \"missing\")\n\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"GETDEL\", \"foo\")\n\t\tc.Do(\"EXISTS\", \"foo\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"GETDEL\")\n\t\tc.Error(\"wrong number\", \"GETDEL\", \"foo\", \"bar\")\n\t\t// Wrong type\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Error(\"wrong kind\", \"GETDEL\", \"hash\")\n\t})\n}\n\nfunc TestStringMget(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"SET\", \"foo2\", \"bar\")\n\t\tc.Do(\"MGET\", \"foo\")\n\t\tc.Do(\"MGET\", \"foo\", \"foo2\")\n\t\tc.Do(\"MGET\", \"nosuch\", \"neither\")\n\t\tc.Do(\"MGET\", \"nosuch\", \"neither\", \"foo\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"MGET\")\n\t\t// Wrong type\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Do(\"MGET\", \"hash\") // not an error.\n\t})\n}\n\nfunc TestStringSetnx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SETNX\", \"foo\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SETNX\", \"foo\", \"bar2\")\n\t\tc.Do(\"GET\", \"foo\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"SETNX\")\n\t\tc.Error(\"wrong number\", \"SETNX\", \"foo\")\n\t\tc.Error(\"wrong number\", \"SETNX\", \"foo\", \"bar\", \"baz\")\n\t\t// Wrong type\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Do(\"SETNX\", \"hash\", \"value\")\n\t})\n}\n\nfunc TestExpire(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"EXPIRETIME\", \"missing\")\n\t\tc.Do(\"PEXPIRETIME\", \"missing\")\n\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"EXPIRETIME\", \"foo\")\n\t\tc.Do(\"PEXPIRETIME\", \"foo\")\n\n\t\tc.Do(\"EXPIRE\", \"foo\", \"12\")\n\t\tc.Do(\"TTL\", \"foo\")\n\t\tc.Do(\"TTL\", \"nosuch\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"PEXPIRE\", \"foo\", \"999999\")\n\t\tc.Do(\"EXPIREAT\", \"foo\", \"2234567890\")\n\t\tc.Do(\"PEXPIREAT\", \"foo\", \"2234567890123\")\n\t\tc.Do(\"EXPIRETIME\", \"foo\")\n\t\tc.Do(\"PEXPIRETIME\", \"foo\")\n\t\t// c.Do(\"PTTL\", \"foo\")\n\t\tc.Do(\"PTTL\", \"nosuch\")\n\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"EXPIRE\", \"foo\", \"0\")\n\t\tc.Do(\"EXISTS\", \"foo\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"EXPIRE\", \"foo\", \"-12\")\n\t\tc.Do(\"EXISTS\", \"foo\")\n\n\t\tc.Do(\"SET\", \"gt\", \"nice day today, right?\")\n\t\tc.Do(\"EXPIRE\", \"gt\", \"10\", \"GT\")\n\t\tc.Do(\"TTL\", \"gt\")\n\t\tc.Do(\"EXPIRE\", \"gt\", \"10\", \"LT\")\n\t\tc.Do(\"TTL\", \"gt\")\n\t\tc.Do(\"EXPIRE\", \"gt\", \"3\", \"GT\")\n\t\tc.Do(\"TTL\", \"gt\")\n\t\tc.Do(\"EXPIRE\", \"gt\", \"999\", \"NX\")\n\t\tc.Do(\"TTL\", \"gt\")\n\t\tc.Do(\"EXPIRE\", \"gt\", \"999\", \"XX\")\n\t\tc.Do(\"TTL\", \"gt\")\n\t\tc.Do(\"PEXPIRE\", \"gt\", \"999000\", \"XX\")\n\t\tc.Do(\"TTL\", \"gt\")\n\n\t\tc.Do(\"SET\", \"pgt\", \"indeed it is\")\n\t\tc.Do(\"PEXPIRE\", \"pgt\", \"10000\", \"LT\", \"XX\")\n\t\tc.Do(\"TTL\", \"pgt\")\n\n\t\tc.Error(\"wrong number\", \"EXPIRE\")\n\t\tc.Error(\"wrong number\", \"EXPIRE\", \"foo\")\n\t\tc.Error(\"not an integer\", \"EXPIRE\", \"foo\", \"noint\")\n\t\tc.Error(\"Unsupported\", \"EXPIRE\", \"foo\", \"12\", \"invaLID\")\n\t\tc.Error(\"Unsupported\", \"EXPIRE\", \"foo\", \"12\", \"GT\", \"toomany\")\n\t\tc.Error(\"at the same time\", \"EXPIRE\", \"foo\", \"12\", \"GT\", \"LT\")\n\t\tc.Error(\"at the same time\", \"EXPIRE\", \"foo\", \"12\", \"LT\", \"NX\")\n\t\tc.Error(\"wrong number\", \"EXPIREAT\")\n\t\tc.Error(\"wrong number\", \"TTL\")\n\t\tc.Error(\"wrong number\", \"TTL\", \"too\", \"many\")\n\t\tc.Error(\"wrong number\", \"PEXPIRE\")\n\t\tc.Error(\"wrong number\", \"PEXPIRE\", \"foo\")\n\t\tc.Error(\"not an integer\", \"PEXPIRE\", \"foo\", \"noint\")\n\t\tc.Error(\"Unsupported\", \"PEXPIRE\", \"foo\", \"12\", \"toomany\")\n\t\tc.Error(\"Unsupported\", \"PEXPIREAT\", \"foo\", \"12\", \"NX\", \"toomany\")\n\t\tc.Error(\"wrong number\", \"PEXPIREAT\")\n\t\tc.Error(\"wrong number\", \"PTTL\")\n\t\tc.Error(\"wrong number\", \"PTTL\", \"too\", \"many\")\n\n\t\tc.Error(\"wrong number\", \"EXPIRETIME\")\n\t\tc.Error(\"wrong number\", \"EXPIRETIME\", \"too\", \"many\")\n\t\tc.Error(\"wrong number\", \"PEXPIRETIME\")\n\t\tc.Error(\"wrong number\", \"PEXPIRETIME\", \"too\", \"many\")\n\t})\n}\n\nfunc TestMset(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MSET\", \"foo\", \"bar\")\n\t\tc.Do(\"MSET\", \"foo\", \"bar\", \"baz\", \"?\")\n\t\tc.Do(\"MSET\", \"foo\", \"bar\", \"foo\", \"baz\") // double key\n\t\tc.Do(\"GET\", \"foo\")\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"MSET\")\n\t\tc.Error(\"wrong number\", \"MSET\", \"foo\")\n\t\tc.Error(\"wrong number\", \"MSET\", \"foo\", \"bar\", \"baz\")\n\n\t\tc.Do(\"MSETNX\", \"foo\", \"bar\", \"aap\", \"noot\")\n\t\tc.Do(\"MSETNX\", \"one\", \"two\", \"three\", \"four\")\n\t\tc.Do(\"MSETNX\", \"11\", \"12\", \"11\", \"14\") // double key\n\t\tc.Do(\"GET\", \"11\")\n\n\t\t// Wrong type of key doesn't matter\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Do(\"MSET\", \"aap\", \"again\", \"eight\", \"nine\")\n\t\tc.Do(\"MSETNX\", \"aap\", \"again\", \"eight\", \"nine\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"MSETNX\")\n\t\tc.Error(\"wrong number\", \"MSETNX\", \"one\")\n\t\tc.Error(\"wrong number\", \"MSETNX\", \"one\", \"two\", \"three\")\n\t})\n}\n\nfunc TestSetx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SETEX\", \"foo\", \"12\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"TTL\", \"foo\")\n\t\tc.Error(\"wrong number\", \"SETEX\", \"foo\")\n\t\tc.Error(\"not an integer\", \"SETEX\", \"foo\", \"noint\", \"bar\")\n\t\tc.Error(\"wrong number\", \"SETEX\", \"foo\", \"12\")\n\t\tc.Error(\"wrong number\", \"SETEX\", \"foo\", \"12\", \"bar\", \"toomany\")\n\t\tc.Error(\"wrong number\", \"SETEX\", \"foo\", \"0\")\n\t\tc.Error(\"wrong number\", \"SETEX\", \"foo\", \"-12\")\n\n\t\tc.Do(\"PSETEX\", \"foo\", \"12\", \"bar\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\t// c.Do(\"PTTL\", \"foo\") // counts down too quickly to compare\n\t\tc.Error(\"wrong number\", \"PSETEX\", \"foo\")\n\t\tc.Error(\"not an integer\", \"PSETEX\", \"foo\", \"noint\", \"bar\")\n\t\tc.Error(\"wrong number\", \"PSETEX\", \"foo\", \"12\")\n\t\tc.Error(\"wrong number\", \"PSETEX\", \"foo\", \"12\", \"bar\", \"toomany\")\n\t\tc.Error(\"wrong number\", \"PSETEX\", \"foo\", \"0\")\n\t\tc.Error(\"wrong number\", \"PSETEX\", \"foo\", \"-12\")\n\t})\n}\n\nfunc TestGetrange(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"The quick brown fox jumps over the lazy dog\")\n\t\tc.Do(\"GETRANGE\", \"foo\", \"0\", \"100\")\n\t\tc.Do(\"GETRANGE\", \"foo\", \"0\", \"0\")\n\t\tc.Do(\"GETRANGE\", \"foo\", \"0\", \"-4\")\n\t\tc.Do(\"GETRANGE\", \"foo\", \"0\", \"-400\")\n\t\tc.Do(\"GETRANGE\", \"foo\", \"-4\", \"-4\")\n\t\tc.Do(\"GETRANGE\", \"foo\", \"4\", \"2\")\n\t\tc.Error(\"not an integer\", \"GETRANGE\", \"foo\", \"aap\", \"2\")\n\t\tc.Error(\"not an integer\", \"GETRANGE\", \"foo\", \"4\", \"aap\")\n\t\tc.Error(\"wrong number\", \"GETRANGE\", \"foo\", \"4\", \"2\", \"aap\")\n\t\tc.Error(\"wrong number\", \"GETRANGE\", \"foo\")\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"GETRANGE\", \"aap\", \"4\", \"2\")\n\t})\n}\n\nfunc TestStrlen(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"str\", \"The quick brown fox jumps over the lazy dog\")\n\t\tc.Do(\"STRLEN\", \"str\")\n\t\t// failure cases\n\t\tc.Error(\"wrong number\", \"STRLEN\")\n\t\tc.Error(\"wrong number\", \"STRLEN\", \"str\", \"bar\")\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Error(\"wrong kind\", \"STRLEN\", \"hash\")\n\t})\n}\n\nfunc TestSetrange(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"The quick brown fox jumps over the lazy dog\")\n\t\tc.Do(\"SETRANGE\", \"foo\", \"0\", \"aap\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SETRANGE\", \"foo\", \"10\", \"noot\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SETRANGE\", \"foo\", \"40\", \"overtheedge\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"SETRANGE\", \"foo\", \"400\", \"oh, hey there\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\t// Non existing key\n\t\tc.Do(\"SETRANGE\", \"nosuch\", \"2\", \"aap\")\n\t\tc.Do(\"GET\", \"nosuch\")\n\n\t\t// Error cases\n\t\tc.Error(\"wrong number\", \"SETRANGE\", \"foo\")\n\t\tc.Error(\"wrong number\", \"SETRANGE\", \"foo\", \"1\")\n\t\tc.Error(\"not an integer\", \"SETRANGE\", \"foo\", \"aap\", \"bar\")\n\t\tc.Error(\"not an integer\", \"SETRANGE\", \"foo\", \"noint\", \"bar\")\n\t\tc.Error(\"out of range\", \"SETRANGE\", \"foo\", \"-1\", \"bar\")\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"SETRANGE\", \"aap\", \"4\", \"bar\")\n\t})\n}\n\nfunc TestIncrAndFriends(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"INCR\", \"aap\")\n\t\tc.Do(\"INCR\", \"aap\")\n\t\tc.Do(\"INCR\", \"aap\")\n\t\tc.Do(\"GET\", \"aap\")\n\t\tc.Do(\"DECR\", \"aap\")\n\t\tc.Do(\"DECR\", \"noot\")\n\t\tc.Do(\"DECR\", \"noot\")\n\t\tc.Do(\"GET\", \"noot\")\n\t\tc.Do(\"INCRBY\", \"noot\", \"100\")\n\t\tc.Do(\"INCRBY\", \"noot\", \"200\")\n\t\tc.Do(\"INCRBY\", \"noot\", \"300\")\n\t\tc.Do(\"GET\", \"noot\")\n\t\tc.Do(\"DECRBY\", \"noot\", \"100\")\n\t\tc.Do(\"DECRBY\", \"noot\", \"200\")\n\t\tc.Do(\"DECRBY\", \"noot\", \"300\")\n\t\tc.Do(\"DECRBY\", \"noot\", \"400\")\n\t\tc.Do(\"GET\", \"noot\")\n\t\tc.Do(\"INCRBYFLOAT\", \"zus\", \"1.23\")\n\t\tc.Do(\"INCRBYFLOAT\", \"zus\", \"3.1456\")\n\t\tc.Do(\"INCRBYFLOAT\", \"zus\", \"987.65432\")\n\t\tc.Do(\"GET\", \"zus\")\n\t\tc.Do(\"INCRBYFLOAT\", \"whole\", \"300\")\n\t\tc.Do(\"INCRBYFLOAT\", \"whole\", \"300\")\n\t\tc.Do(\"INCRBYFLOAT\", \"whole\", \"300\")\n\t\tc.Do(\"GET\", \"whole\")\n\t\tc.Do(\"INCRBYFLOAT\", \"big\", \"12345e10\")\n\t\tc.Do(\"GET\", \"big\")\n\n\t\t// Floats are not ints.\n\t\tc.Do(\"SET\", \"float\", \"1.23\")\n\t\tc.Error(\"not an integer\", \"INCR\", \"float\")\n\t\tc.Error(\"not an integer\", \"INCRBY\", \"float\", \"12\")\n\t\tc.Error(\"not an integer\", \"DECR\", \"float\")\n\t\tc.Error(\"not an integer\", \"DECRBY\", \"float\", \"12\")\n\t\tc.Do(\"SET\", \"str\", \"I'm a string\")\n\t\tc.Error(\"not a valid float\", \"INCRBYFLOAT\", \"str\", \"123.5\")\n\n\t\t// Error cases\n\t\tc.Do(\"HSET\", \"mies\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"INCR\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"INCRBY\", \"mies\", \"1\")\n\t\tc.Error(\"not an integer\", \"INCRBY\", \"mies\", \"foo\")\n\t\tc.Error(\"wrong kind\", \"DECR\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"DECRBY\", \"mies\", \"1\")\n\t\tc.Error(\"wrong kind\", \"INCRBYFLOAT\", \"mies\", \"1\")\n\t\tc.Error(\"not a valid float\", \"INCRBYFLOAT\", \"int\", \"foo\")\n\n\t\tc.Error(\"wrong number\", \"INCR\", \"int\", \"err\")\n\t\tc.Error(\"wrong number\", \"INCRBY\", \"int\")\n\t\tc.Error(\"wrong number\", \"DECR\", \"int\", \"err\")\n\t\tc.Error(\"wrong number\", \"DECRBY\", \"int\")\n\t\tc.Error(\"wrong number\", \"INCRBYFLOAT\", \"int\")\n\n\t\t// Rounding\n\t\tc.Do(\"INCRBYFLOAT\", \"zero\", \"12.3\")\n\t\tc.Do(\"INCRBYFLOAT\", \"zero\", \"-13.1\")\n\n\t\t// Overflow\n\t\tc.Do(\"SET\", \"overflow-up\", \"9223372036854775807\")\n\t\tc.Error(\"increment or decrement would overflow\", \"INCR\", \"overflow-up\")\n\t\tc.Error(\"increment or decrement would overflow\", \"INCRBY\", \"overflow-up\", \"1\")\n\t\tc.Error(\"increment or decrement would overflow\", \"DECRBY\", \"overflow-up\", \"-1\")\n\n\t\tc.Do(\"SET\", \"overflow-down\", \"-9223372036854775808\")\n\t\tc.Error(\"increment or decrement would overflow\", \"DECR\", \"overflow-down\")\n\t\tc.Error(\"increment or decrement would overflow\", \"INCRBY\", \"overflow-down\", \"-1\")\n\t\tc.Error(\"increment or decrement would overflow\", \"DECRBY\", \"overflow-down\", \"1\")\n\n\t\t// E\n\t\tc.Do(\"INCRBYFLOAT\", \"one\", \"12e12\")\n\t\t// c.Do(\"INCRBYFLOAT\", \"one\", \"12e34\") // FIXME\n\t\tc.Error(\"not a valid float\", \"INCRBYFLOAT\", \"one\", \"12e34.1\")\n\t\t// c.Do(\"INCRBYFLOAT\", \"one\", \"0x12e12\") // FIXME\n\t\t// c.Do(\"INCRBYFLOAT\", \"one\", \"012e12\") // FIXME\n\t\tc.Do(\"INCRBYFLOAT\", \"two\", \"012\")\n\t\tc.Error(\"not a valid float\", \"INCRBYFLOAT\", \"one\", \"0b12e12\")\n\t})\n}\n\nfunc TestBitcount(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"str\", \"The quick brown fox jumps over the lazy dog\")\n\t\tc.Do(\"SET\", \"utf8\", \"❆❅❄☃\")\n\t\tc.Do(\"BITCOUNT\", \"str\")\n\t\tc.Do(\"BITCOUNT\", \"utf8\")\n\t\tc.Do(\"BITCOUNT\", \"str\", \"0\", \"0\")\n\t\tc.Do(\"BITCOUNT\", \"str\", \"1\", \"2\")\n\t\tc.Do(\"BITCOUNT\", \"str\", \"1\", \"-200\")\n\t\tc.Do(\"BITCOUNT\", \"str\", \"-2\", \"-1\")\n\t\tc.Do(\"BITCOUNT\", \"str\", \"-2\", \"-12\")\n\t\tc.Do(\"BITCOUNT\", \"utf8\", \"0\", \"0\")\n\n\t\tc.Do(\"SETBIT\", \"A\", \"10\", \"1\")\n\t\tc.Do(\"BITCOUNT\", \"A\", \"0\", \"100000\")\n\t\tc.Do(\"BITCOUNT\", \"A\", \"0\", \"9223372036854775806\")\n\t\tc.Do(\"BITCOUNT\", \"A\", \"0\", \"9223372036854775807\") // max int64\n\t\tc.Error(\"out of range\", \"BITCOUNT\", \"A\", \"0\", \"9223372036854775808\")\n\n\t\tc.Error(\"wrong number\", \"BITCOUNT\")\n\t\tc.Error(\"syntax error\", \"BITCOUNT\", \"wrong\", \"arguments\")\n\t\tc.Error(\"syntax error\", \"BITCOUNT\", \"str\", \"4\", \"2\", \"2\", \"2\", \"2\")\n\t\tc.Error(\"not an integer\", \"BITCOUNT\", \"str\", \"foo\", \"2\")\n\t\tc.Do(\"HSET\", \"aap\", \"noot\", \"mies\")\n\t\tc.Error(\"wrong kind\", \"BITCOUNT\", \"aap\", \"4\", \"2\")\n\t})\n}\n\nfunc TestBitop(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"a\", \"foo\")\n\t\tc.Do(\"SET\", \"b\", \"aap\")\n\t\tc.Do(\"SET\", \"c\", \"noot\")\n\t\tc.Do(\"SET\", \"d\", \"mies\")\n\t\tc.Do(\"SET\", \"e\", \"❆❅❄☃\")\n\n\t\t// ANDs\n\t\tc.Do(\"BITOP\", \"AND\", \"target\", \"a\", \"b\", \"c\", \"d\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"AND\", \"target\", \"a\", \"nosuch\", \"c\", \"d\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"AND\", \"utf8\", \"e\", \"e\")\n\t\tc.Do(\"GET\", \"utf8\")\n\t\tc.Do(\"BITOP\", \"AND\", \"utf8\", \"b\", \"e\")\n\t\tc.Do(\"GET\", \"utf8\")\n\t\t// BITOP on only unknown keys:\n\t\tc.Do(\"BITOP\", \"AND\", \"bits\", \"nosuch\", \"nosucheither\")\n\t\tc.Do(\"GET\", \"bits\")\n\n\t\t// ORs\n\t\tc.Do(\"BITOP\", \"OR\", \"target\", \"a\", \"b\", \"c\", \"d\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"OR\", \"target\", \"a\", \"nosuch\", \"c\", \"d\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"OR\", \"utf8\", \"e\", \"e\")\n\t\tc.Do(\"GET\", \"utf8\")\n\t\tc.Do(\"BITOP\", \"OR\", \"utf8\", \"b\", \"e\")\n\t\tc.Do(\"GET\", \"utf8\")\n\t\t// BITOP on only unknown keys:\n\t\tc.Do(\"BITOP\", \"OR\", \"bits\", \"nosuch\", \"nosucheither\")\n\t\tc.Do(\"GET\", \"bits\")\n\t\tc.Do(\"SET\", \"empty\", \"\")\n\t\t// BITOP on empty key\n\t\tc.Do(\"BITOP\", \"OR\", \"bits\", \"empty\")\n\t\tc.Do(\"GET\", \"bits\")\n\n\t\t// XORs\n\t\tc.Do(\"BITOP\", \"XOR\", \"target\", \"a\", \"b\", \"c\", \"d\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"XOR\", \"target\", \"a\", \"nosuch\", \"c\", \"d\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"XOR\", \"target\", \"a\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"XOR\", \"utf8\", \"e\", \"e\")\n\t\tc.Do(\"GET\", \"utf8\")\n\t\tc.Do(\"BITOP\", \"XOR\", \"utf8\", \"b\", \"e\")\n\t\tc.Do(\"GET\", \"utf8\")\n\n\t\t// NOTs\n\t\tc.Do(\"BITOP\", \"NOT\", \"target\", \"a\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"NOT\", \"target\", \"e\")\n\t\tc.Do(\"GET\", \"target\")\n\t\tc.Do(\"BITOP\", \"NOT\", \"bits\", \"nosuch\")\n\t\tc.Do(\"GET\", \"bits\")\n\n\t\tc.Error(\"wrong number\", \"BITOP\", \"AND\", \"utf8\")\n\t\tc.Error(\"wrong number\", \"BITOP\", \"AND\")\n\t\tc.Error(\"single source key\", \"BITOP\", \"NOT\", \"foo\", \"bar\", \"baz\")\n\t\tc.Error(\"wrong number\", \"BITOP\", \"WRONGOP\", \"key\")\n\t\tc.Error(\"wrong number\", \"BITOP\", \"WRONGOP\")\n\n\t\tc.Do(\"HSET\", \"hash\", \"aap\", \"noot\")\n\t\tc.Error(\"wrong kind\", \"BITOP\", \"AND\", \"t\", \"hash\", \"irrelevant\")\n\t\tc.Error(\"wrong kind\", \"BITOP\", \"OR\", \"t\", \"hash\", \"irrelevant\")\n\t\tc.Error(\"wrong kind\", \"BITOP\", \"XOR\", \"t\", \"hash\", \"irrelevant\")\n\t\tc.Error(\"wrong kind\", \"BITOP\", \"NOT\", \"t\", \"hash\")\n\t})\n}\n\nfunc TestBitpos(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"a\", \"\\x00\\x0f\")\n\t\tc.Do(\"SET\", \"b\", \"\\xf0\\xf0\")\n\t\tc.Do(\"SET\", \"c\", \"\\x00\\x00\\x00\\x0f\")\n\t\tc.Do(\"SET\", \"d\", \"\\x00\\x00\\x00\")\n\t\tc.Do(\"SET\", \"e\", \"\\xff\\xff\\xff\")\n\t\tc.Do(\"SET\", \"empty\", \"\")\n\n\t\tc.Do(\"BITPOS\", \"a\", \"1\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\")\n\t\tc.Do(\"BITPOS\", \"a\", \"1\", \"1\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"1\")\n\t\tc.Do(\"BITPOS\", \"a\", \"1\", \"1\", \"2\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"1\", \"2\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"0\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"0\", \"-2\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"0\", \"-2\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"0\", \"-999\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"-1\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"-2\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"-2\", \"-999\")\n\t\tc.Do(\"BITPOS\", \"a\", \"0\", \"-999\", \"-999\")\n\t\tc.Do(\"BITPOS\", \"b\", \"1\")\n\t\tc.Do(\"BITPOS\", \"b\", \"0\")\n\t\tc.Do(\"BITPOS\", \"c\", \"1\")\n\t\tc.Do(\"BITPOS\", \"c\", \"0\")\n\t\tc.Do(\"BITPOS\", \"d\", \"1\")\n\t\tc.Do(\"BITPOS\", \"d\", \"0\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"1\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\", \"1\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"1\", \"2\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\", \"1\", \"2\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"100\", \"2\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\", \"100\", \"2\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"1\", \"0\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"1\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"1\", \"-2\")\n\t\tc.Do(\"BITPOS\", \"e\", \"1\", \"1\", \"-2000\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\", \"0\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"e\", \"0\", \"1\", \"2\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"0\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"0\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"0\", \"0\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"0\", \"-1\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"1\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"1\", \"0\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"1\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"1\", \"0\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"empty\", \"1\", \"-1\", \"-1\")\n\t\tc.Do(\"BITPOS\", \"nosuch\", \"0\")\n\t\tc.Do(\"BITPOS\", \"nosuch\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"nosuch\", \"0\", \"0\", \"0\")\n\t\tc.Do(\"BITPOS\", \"nosuch\", \"1\")\n\t\tc.Do(\"BITPOS\", \"nosuch\", \"1\", \"0\")\n\t\tc.Do(\"BITPOS\", \"nosuch\", \"1\", \"0\", \"0\")\n\n\t\tc.Do(\"HSET\", \"hash\", \"aap\", \"noot\")\n\t\tc.Error(\"wrong kind\", \"BITPOS\", \"hash\", \"1\")\n\t\tc.Error(\"not an integer\", \"BITPOS\", \"a\", \"aap\")\n\t})\n}\n\nfunc TestGetbit(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tfor i := 0; i < 100; i++ {\n\t\t\tc.Do(\"SET\", \"a\", \"\\x00\\x0f\")\n\t\t\tc.Do(\"SET\", \"e\", \"\\xff\\xff\\xff\")\n\t\t\tc.Do(\"GETBIT\", \"nosuch\", \"1\")\n\t\t\tc.Do(\"GETBIT\", \"nosuch\", \"0\")\n\n\t\t\t// Error cases\n\t\t\tc.Do(\"HSET\", \"hash\", \"aap\", \"noot\")\n\t\t\tc.Error(\"wrong kind\", \"GETBIT\", \"hash\", \"1\")\n\t\t\tc.Error(\"not an integer\", \"GETBIT\", \"a\", \"aap\")\n\t\t\tc.Error(\"wrong number\", \"GETBIT\", \"a\")\n\t\t\tc.Error(\"wrong number\", \"GETBIT\", \"too\", \"1\", \"many\")\n\n\t\t\tc.Do(\"GETBIT\", \"a\", strconv.Itoa(i))\n\t\t\tc.Do(\"GETBIT\", \"e\", strconv.Itoa(i))\n\t\t}\n\t})\n}\n\nfunc TestSetbit(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tfor i := 0; i < 100; i++ {\n\t\t\tc.Do(\"SET\", \"a\", \"\\x00\\x0f\")\n\t\t\tc.Do(\"SETBIT\", \"a\", \"0\", \"1\")\n\t\t\tc.Do(\"GET\", \"a\")\n\t\t\tc.Do(\"SETBIT\", \"a\", \"0\", \"0\")\n\t\t\tc.Do(\"GET\", \"a\")\n\t\t\tc.Do(\"SETBIT\", \"a\", \"13\", \"0\")\n\t\t\tc.Do(\"GET\", \"a\")\n\t\t\tc.Do(\"SETBIT\", \"nosuch\", \"11111\", \"1\")\n\t\t\tc.Do(\"GET\", \"nosuch\")\n\n\t\t\t// Error cases\n\t\t\tc.Do(\"HSET\", \"hash\", \"aap\", \"noot\")\n\t\t\tc.Error(\"wrong kind\", \"SETBIT\", \"hash\", \"1\", \"1\")\n\t\t\tc.Error(\"not an integer\", \"SETBIT\", \"a\", \"aap\", \"0\")\n\t\t\tc.Error(\"not an integer\", \"SETBIT\", \"a\", \"0\", \"aap\")\n\t\t\tc.Error(\"not an integer\", \"SETBIT\", \"a\", \"-1\", \"0\")\n\t\t\tc.Error(\"not an integer\", \"SETBIT\", \"a\", \"1\", \"-1\")\n\t\t\tc.Error(\"not an integer\", \"SETBIT\", \"a\", \"1\", \"2\")\n\t\t\tc.Error(\"wrong number\", \"SETBIT\", \"too\", \"1\", \"2\", \"many\")\n\n\t\t\tc.Do(\"GETBIT\", \"a\", strconv.Itoa(i))\n\t\t\tc.Do(\"GETBIT\", \"e\", strconv.Itoa(i))\n\t\t}\n\t})\n}\n\nfunc TestAppend(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"APPEND\", \"foo\", \"more\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"APPEND\", \"nosuch\", \"more\")\n\t\tc.Do(\"GET\", \"nosuch\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"APPEND\")\n\t\tc.Error(\"wrong number\", \"APPEND\", \"foo\")\n\t})\n}\n\nfunc TestMove(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"EXPIRE\", \"foo\", \"12345\")\n\t\tc.Do(\"MOVE\", \"foo\", \"2\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"TTL\", \"foo\")\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"TTL\", \"foo\")\n\n\t\t// Failure cases\n\t\tc.Error(\"wrong number\", \"MOVE\")\n\t\tc.Error(\"wrong number\", \"MOVE\", \"foo\")\n\t\t// c.Do(\"MOVE\", \"foo\", \"noint\")\n\t})\n\t// hash key\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"HSET\", \"hash\", \"key\", \"value\")\n\t\tc.Do(\"EXPIRE\", \"hash\", \"12345\")\n\t\tc.Do(\"MOVE\", \"hash\", \"2\")\n\t\tc.Do(\"MGET\", \"hash\", \"key\")\n\t\tc.Do(\"TTL\", \"hash\")\n\t\tc.Do(\"SELECT\", \"2\")\n\t\tc.Do(\"MGET\", \"hash\", \"key\")\n\t\tc.Do(\"TTL\", \"hash\")\n\t})\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\t// to current DB.\n\t\tc.Error(\"the same\", \"MOVE\", \"foo\", \"0\")\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/test.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n\t\"github.com/alicebob/miniredis/v2/proto\"\n)\n\nfunc skip(t testing.TB) {\n\tt.Helper()\n\tif os.Getenv(\"INT\") == \"\" {\n\t\tt.Skip(\"INT=1 not set\")\n\t}\n}\n\nfunc testRaw(t *testing.T, cb func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\n\tsReal, sRealAddr := Redis()\n\tt.Cleanup(sReal.Close)\n\n\tclient := newClient(t, sRealAddr, sMini)\n\n\tcb(client)\n}\n\n// like testRaw, but with two connections\nfunc testRaw2(t *testing.T, cb func(*client, *client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\n\tsReal, sRealAddr := Redis()\n\tt.Cleanup(sReal.Close)\n\n\tclient1 := newClient(t, sRealAddr, sMini)\n\tclient2 := newClient(t, sRealAddr, sMini)\n\n\tcb(client1, client2)\n}\n\n// like testRaw2, but with connections in Go routines\nfunc testMulti(t *testing.T, cbs ...func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\n\tsReal, sRealAddr := Redis()\n\tt.Cleanup(sReal.Close)\n\n\tvar wg sync.WaitGroup\n\tfor _, cb := range cbs {\n\t\twg.Add(1)\n\t\tgo func(cb func(*client)) {\n\t\t\tclient := newClient(t, sRealAddr, sMini)\n\t\t\tcb(client)\n\t\t\twg.Done()\n\t\t}(cb)\n\t}\n\twg.Wait()\n}\n\n// similar to testRaw, but redis runs with authentication enabled\nfunc testAuth(t *testing.T, passwd string, cb func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\tsMini.RequireAuth(passwd)\n\n\tsReal, sRealAddr := RedisAuth(passwd)\n\tt.Cleanup(sReal.Close)\n\n\tclient := newClient(t, sRealAddr, sMini)\n\n\tcb(client)\n}\n\n// similar to testAuth, but redis runs with redis6 multiuser authentication enabled\nfunc testUserAuth(t *testing.T, users map[string]string, cb func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\tfor user, pass := range users {\n\t\tsMini.RequireUserAuth(user, pass)\n\t}\n\n\tsReal, sRealAddr := RedisUserAuth(users)\n\tt.Cleanup(sReal.Close)\n\n\tclient := newClient(t, sRealAddr, sMini)\n\n\tcb(client)\n}\n\n// similar to testRaw, but redis is started in cluster mode\nfunc testCluster(t *testing.T, cb func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\n\tsReal, sRealAddr := RedisCluster()\n\tt.Cleanup(sReal.Close)\n\n\tclient := newClient(t, sRealAddr, sMini)\n\n\tcb(client)\n}\n\n// similar to testRaw, but connections require TLS\nfunc testTLS(t *testing.T, cb func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.NewMiniRedis()\n\tif err := sMini.StartTLS(testServerTLS(t)); err != nil {\n\t\tt.Fatalf(\"unexpected miniredis error: %s\", err.Error())\n\t}\n\tt.Cleanup(sMini.Close)\n\n\tsReal, sRealAddr := RedisTLS()\n\tt.Cleanup(sReal.Close)\n\n\tclient := newClientTLS(t, sRealAddr, sMini)\n\n\tcb(client)\n}\n\n// like testRaw, but switched to RESP3 protocol.\nfunc testRESP3(t *testing.T, cb func(*client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\n\tsReal, sRealAddr := Redis()\n\tt.Cleanup(sReal.Close)\n\n\tclient := newClientResp3(t, sRealAddr, sMini)\n\n\tcb(client)\n}\n\n// like testRESP3, but with two connections\nfunc testRESP3Pair(t *testing.T, cb func(*client, *client)) {\n\tt.Helper()\n\n\tsMini := miniredis.RunT(t)\n\n\tsReal, sRealAddr := Redis()\n\tt.Cleanup(sReal.Close)\n\n\tclient1 := newClientResp3(t, sRealAddr, sMini)\n\tclient2 := newClientResp3(t, sRealAddr, sMini)\n\n\tcb(client1, client2)\n}\n\nfunc looselyEqual(a, b interface{}) bool {\n\tswitch av := a.(type) {\n\tcase string:\n\t\t_, ok := b.(string)\n\t\treturn ok\n\tcase []byte:\n\t\t_, ok := b.([]byte)\n\t\treturn ok\n\tcase int64:\n\t\t_, ok := b.(int64)\n\t\treturn ok\n\tcase int:\n\t\t_, ok := b.(int)\n\t\treturn ok\n\tcase error:\n\t\t_, ok := b.(error)\n\t\treturn ok\n\tcase []interface{}:\n\t\tbv, ok := b.([]interface{})\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tif len(av) != len(bv) {\n\t\t\treturn false\n\t\t}\n\t\tfor i, v := range av {\n\t\t\tif !looselyEqual(v, bv[i]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\tcase map[interface{}]interface{}:\n\t\tbv, ok := b.(map[interface{}]interface{})\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\tif len(av) != len(bv) {\n\t\t\treturn false\n\t\t}\n\t\tfor k, v := range av {\n\t\t\tif !looselyEqual(v, bv[k]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled case, got a %#v / %T\", a, a))\n\t}\n}\n\n// round all floats\nfunc roundFloats(r interface{}, pos int) interface{} {\n\tswitch ls := r.(type) {\n\tcase []interface{}:\n\t\tvar new []interface{}\n\t\tfor _, k := range ls {\n\t\t\tnew = append(new, roundFloats(k, pos))\n\t\t}\n\t\treturn new\n\tcase []byte:\n\t\tf, err := strconv.ParseFloat(string(ls), 64)\n\t\tif err != nil {\n\t\t\treturn ls\n\t\t}\n\t\treturn []byte(fmt.Sprintf(\"%.[1]*f\", pos, f))\n\tcase string:\n\t\tf, err := strconv.ParseFloat(string(ls), 64)\n\t\tif err != nil {\n\t\t\treturn ls\n\t\t}\n\t\treturn fmt.Sprintf(\"%.[1]*f\", pos, f)\n\tdefault:\n\t\tfmt.Printf(\"unhandled type: %T FIXME\\n\", r)\n\t\treturn nil\n\t}\n}\n\n// client which compares two redises\ntype client struct {\n\tt          *testing.T\n\treal, mini *proto.Client\n\tminiredis  *miniredis.Miniredis // in case you need m.FastForward() and friends\n}\n\nfunc newClient(t *testing.T, realAddr string, mini *miniredis.Miniredis) *client {\n\tt.Helper()\n\n\tcReal, err := proto.Dial(realAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"realredis: %s\", err.Error())\n\t}\n\n\tcMini, err := proto.Dial(mini.Addr())\n\tif err != nil {\n\t\tt.Fatalf(\"miniredis: %s\", err.Error())\n\t}\n\n\treturn &client{\n\t\tt:         t,\n\t\tminiredis: mini,\n\t\treal:      cReal,\n\t\tmini:      cMini,\n\t}\n}\n\nfunc newClientTLS(t *testing.T, realAddr string, mini *miniredis.Miniredis) *client {\n\tt.Helper()\n\n\tcfg := testClientTLS(t)\n\n\tcReal, err := proto.DialTLS(\n\t\trealAddr,\n\t\tcfg,\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"realredis: %s\", err.Error())\n\t}\n\n\tcMini, err := proto.DialTLS(\n\t\tmini.Addr(),\n\t\tcfg,\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"miniredis: %s\", err.Error())\n\t}\n\n\treturn &client{\n\t\tt:         t,\n\t\tminiredis: mini,\n\t\treal:      cReal,\n\t\tmini:      cMini,\n\t}\n}\n\nfunc newClientResp3(t *testing.T, realAddr string, mini *miniredis.Miniredis) *client {\n\tt.Helper()\n\n\tcReal, err := proto.Dial(realAddr)\n\tif err != nil {\n\t\tt.Fatalf(\"realredis: %s\", err.Error())\n\t}\n\tif _, err := cReal.Do(\"HELLO\", \"3\"); err != nil {\n\t\tt.Fatalf(\"realredis HELLO: %s\", err.Error())\n\t}\n\n\tcMini, err := proto.Dial(mini.Addr())\n\tif err != nil {\n\t\tt.Fatalf(\"miniredis: %s\", err.Error())\n\t}\n\tif _, err := cMini.Do(\"HELLO\", \"3\"); err != nil {\n\t\tt.Fatalf(\"miniredis HELLO: %s\", err.Error())\n\t}\n\n\treturn &client{\n\t\tt:         t,\n\t\tminiredis: mini,\n\t\treal:      cReal,\n\t\tmini:      cMini,\n\t}\n}\n\n// Do() is the main test function. The given redis command is executed on both\n// a real redis and on miniredis, and the returned results must be exactly the\n// same. See the other Do... commands for variants which are more flexible in\n// their comparison.\nfunc (c *client) Do(cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\t// c.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\n\tif resReal != resMini {\n\t\tc.t.Errorf(\"real: %q mini: %q\", string(resReal), string(resMini))\n\t\treturn\n\t}\n\n\tif strings.HasPrefix(string(resReal), \"-\") {\n\t\tc.t.Errorf(\"Do() returned a redis error, use c.Error(): %q\", string(resReal))\n\t}\n}\n\n// result must be []string, and we'll sort them before comparing\nfunc (c *client) DoSorted(cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\t// c.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\trealStrings, err := proto.ReadStrings(resReal)\n\tif err != nil {\n\t\tc.t.Errorf(\"readstrings realredis: %s\", errReal)\n\t\treturn\n\t}\n\tminiStrings, err := proto.ReadStrings(resMini)\n\tif err != nil {\n\t\tc.t.Errorf(\"readstrings miniredis: %s\", errReal)\n\t\treturn\n\t}\n\n\tsort.Strings(realStrings)\n\tsort.Strings(miniStrings)\n\n\tif !reflect.DeepEqual(realStrings, miniStrings) {\n\t\tc.t.Errorf(\"expected: %q got: %q\", realStrings, miniStrings)\n\t}\n}\n\n// result must kinda match (just the structure, exact values are not compared)\nfunc (c *client) DoLoosely(cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\t// c.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\n\tmini, err := proto.Parse(resMini)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error miniredis: %s\", err)\n\t\treturn\n\t}\n\treal, err := proto.Parse(resReal)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error realredis: %s\", err)\n\t\treturn\n\t}\n\tif !looselyEqual(real, mini) {\n\t\tc.t.Errorf(\"expected a loose match want: %#v have: %#v\", real, mini)\n\t}\n}\n\n// result must match, with floats rounded\nfunc (c *client) DoRounded(rounded int, cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\t// c.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\n\tmini, err := proto.Parse(resMini)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error miniredis: %s\", err)\n\t\treturn\n\t}\n\treal, err := proto.Parse(resReal)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error realredis: %s\", err)\n\t\treturn\n\t}\n\treal = roundFloats(real, rounded)\n\tmini = roundFloats(mini, rounded)\n\tif !reflect.DeepEqual(real, mini) {\n\t\tc.t.Errorf(\"expected a match (rounded to %d) want: %#v have: %#v\", rounded, real, mini)\n\t}\n}\n\n// result must be a single int, with value within threshold\nfunc (c *client) DoApprox(threshold int, cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\t// c.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\n\tmini, err := proto.Parse(resMini)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error miniredis: %s\", err)\n\t\treturn\n\t}\n\treal, err := proto.Parse(resReal)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error realredis: %s\", err)\n\t\treturn\n\t}\n\tminiInt, ok := mini.(int)\n\tif !ok {\n\t\tc.t.Errorf(\"parse int error miniredis: %T found (%#v)\", mini, mini)\n\t\treturn\n\t}\n\trealInt, ok := real.(int)\n\tif !ok {\n\t\tc.t.Errorf(\"parse int error miniredis: %T found (%#v)\", real, real)\n\t\treturn\n\t}\n\tif math.Abs(float64(miniInt-realInt)) > float64(threshold) {\n\t\tc.t.Errorf(\"expected an approximated match (threshold is %d) want: %#v have: %#v\", threshold, real, mini)\n\t}\n}\n\n// both must return an error, which much both Contain() the message.\nfunc (c *client) Error(msg string, cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\tmini, err := proto.ReadError(resMini)\n\tif err != nil {\n\t\tc.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\t\tc.t.Errorf(\"parse error miniredis: %s\", err)\n\t\treturn\n\t}\n\treal, err := proto.ReadError(resReal)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error realredis: %s\", err)\n\t\treturn\n\t}\n\n\tif !strings.Contains(real, msg) {\n\t\tc.t.Errorf(\"expected (real)\\n%q\\nto contain %q\", real, msg)\n\t}\n\tif !strings.Contains(mini, msg) {\n\t\tc.t.Errorf(\"expected (mini)\\n%q\\nto contain %q\\nreal:\\n%s\", mini, msg, real)\n\t}\n\t// if real != mini {\n\t// c.t.Errorf(\"expected error:\\n%q\\ngot:\\n%q\", real, mini)\n\t// }\n}\n\n// both must return exactly the same error\nfunc (c *client) ErrorTheSame(msg string, cmd string, args ...string) {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Do(append([]string{cmd}, args...)...)\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Do(append([]string{cmd}, args...)...)\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\tmini, err := proto.ReadError(resMini)\n\tif err != nil {\n\t\tc.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\t\tc.t.Errorf(\"parse error miniredis: %s\", err)\n\t\treturn\n\t}\n\treal, err := proto.ReadError(resReal)\n\tif err != nil {\n\t\tc.t.Errorf(\"parse error realredis: %s\", err)\n\t\treturn\n\t}\n\n\tif real != msg {\n\t\tc.t.Errorf(\"expected (real)\\n%q\\nto contain %q\", real, msg)\n\t}\n\tif mini != msg {\n\t\tc.t.Errorf(\"expected (mini)\\n%q\\nto contain %q\\nreal:\\n%s\", mini, msg, real)\n\t}\n\t// real == msg && mini == msg => real == mini, so we don't want to check it explicitly\n}\n\n// only receive a command, which can't be an error\nfunc (c *client) Receive() {\n\tc.t.Helper()\n\n\tresReal, errReal := c.real.Read()\n\tif errReal != nil {\n\t\tc.t.Errorf(\"error from realredis: %s\", errReal)\n\t\treturn\n\t}\n\tresMini, errMini := c.mini.Read()\n\tif errMini != nil {\n\t\tc.t.Errorf(\"error from miniredis: %s\", errMini)\n\t\treturn\n\t}\n\n\t// c.t.Logf(\"real:%q mini:%q\", string(resReal), string(resMini))\n\n\tif strings.HasPrefix(resReal, \"-\") {\n\t\tc.t.Errorf(\"error from realredis: %q\", string(resReal))\n\t}\n\tif strings.HasPrefix(resMini, \"-\") {\n\t\tc.t.Errorf(\"error from miniredis: %q\", string(resMini))\n\t}\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/tls.go",
    "content": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"io/ioutil\"\n\t\"testing\"\n)\n\nfunc testServerTLS(t *testing.T) *tls.Config {\n\tcert, err := tls.LoadX509KeyPair(\"../../testdata/server.crt\", \"../../testdata/server.key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcp := x509.NewCertPool()\n\trootca, err := ioutil.ReadFile(\"../../testdata/ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !cp.AppendCertsFromPEM(rootca) {\n\t\tt.Fatal(\"ca cert err\")\n\t}\n\treturn &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\tServerName:   \"Server\",\n\t\tClientCAs:    cp,\n\t}\n}\n\nfunc testClientTLS(t *testing.T) *tls.Config {\n\tcert, err := tls.LoadX509KeyPair(\"../../testdata/client.crt\", \"../../testdata/client.key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcp := x509.NewCertPool()\n\trootca, err := ioutil.ReadFile(\"../../testdata/ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !cp.AppendCertsFromPEM(rootca) {\n\t\tt.Fatal(\"ca cert err\")\n\t}\n\treturn &tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t\tServerName:   \"Server\",\n\t\tRootCAs:      cp,\n\t}\n}\n"
  },
  {
    "path": "miniredis/tests/integration-go/tx_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestTx(t *testing.T) {\n\tskip(t)\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SET\", \"AAP\", \"1\")\n\t\tc.Do(\"GET\", \"AAP\")\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"GET\", \"AAP\")\n\t})\n\n\t// empty\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\t// err: Double MULTI\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"nested\", \"MULTI\")\n\t})\n\n\t// err: No MULTI\n\ttestRaw(t, func(c *client) {\n\t\tc.Error(\"without MULTI\", \"EXEC\")\n\t})\n\n\t// Errors in the MULTI sequence\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Error(\"wrong number\", \"SET\", \"foo\")\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Error(\"EXECABORT\", \"EXEC\")\n\t})\n\n\t// Simple WATCH\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"WATCH\", \"foo\")\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\t// Simple UNWATCH\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"SET\", \"foo\", \"bar\")\n\t\tc.Do(\"WATCH\", \"foo\")\n\t\tc.Do(\"UNWATCH\")\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"GET\", \"foo\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\t// UNWATCH in a MULTI. Yep. Weird.\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"WATCH\", \"foo\")\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"UNWATCH\") // Valid. Somehow.\n\t\tc.Do(\"EXEC\")\n\t})\n\n\t// Test whether all these commands support transactions.\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"GET\", \"str\")\n\t\tc.Do(\"GETEX\", \"str\")\n\t\tc.Do(\"SET\", \"str\", \"bar\")\n\t\tc.Do(\"SETNX\", \"str\", \"bar\")\n\t\tc.Do(\"GETSET\", \"str\", \"bar\")\n\t\tc.Do(\"MGET\", \"str\", \"bar\")\n\t\tc.Do(\"MSET\", \"str\", \"bar\")\n\t\tc.Do(\"MSETNX\", \"str\", \"bar\")\n\t\tc.Do(\"SETEX\", \"str\", \"12\", \"newv\")\n\t\tc.Do(\"PSETEX\", \"str\", \"12\", \"newv\")\n\t\tc.Do(\"STRLEN\", \"str\")\n\t\tc.Do(\"APPEND\", \"str\", \"more\")\n\t\tc.Do(\"GETRANGE\", \"str\", \"0\", \"2\")\n\t\tc.Do(\"SETRANGE\", \"str\", \"0\", \"B\")\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"GET\", \"str\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SET\", \"bits\", \"\\xff\\x00\")\n\t\tc.Do(\"BITCOUNT\", \"bits\")\n\t\tc.Do(\"BITOP\", \"OR\", \"bits\", \"bits\", \"nosuch\")\n\t\tc.Do(\"BITPOS\", \"bits\", \"1\")\n\t\tc.Do(\"GETBIT\", \"bits\", \"12\")\n\t\tc.Do(\"SETBIT\", \"bits\", \"12\", \"1\")\n\t\tc.Do(\"EXEC\")\n\t\tc.Do(\"GET\", \"bits\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"INCR\", \"number\")\n\t\tc.Do(\"INCRBY\", \"number\", \"12\")\n\t\tc.Do(\"INCRBYFLOAT\", \"number\", \"12.2\")\n\t\tc.Do(\"DECR\", \"number\")\n\t\tc.Do(\"GET\", \"number\")\n\t\tc.Do(\"DECRBY\", \"number\", \"2\")\n\t\tc.Do(\"GET\", \"number\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"HSET\", \"hash\", \"foo\", \"bar\")\n\t\tc.Do(\"HDEL\", \"hash\", \"foo\")\n\t\tc.Do(\"HEXISTS\", \"hash\", \"foo\")\n\t\tc.Do(\"HSET\", \"hash\", \"foo\", \"bar22\")\n\t\tc.Do(\"HSETNX\", \"hash\", \"foo\", \"bar22\")\n\t\tc.Do(\"HGET\", \"hash\", \"foo\")\n\t\tc.Do(\"HMGET\", \"hash\", \"foo\", \"baz\")\n\t\tc.Do(\"HLEN\", \"hash\")\n\t\tc.Do(\"HGETALL\", \"hash\")\n\t\tc.Do(\"HKEYS\", \"hash\")\n\t\tc.Do(\"HVALS\", \"hash\")\n\t})\n\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"SET\", \"key\", \"foo\")\n\t\tc.Do(\"TYPE\", \"key\")\n\t\tc.Do(\"EXPIRE\", \"key\", \"12\")\n\t\tc.Do(\"TTL\", \"key\")\n\t\tc.Do(\"PEXPIRE\", \"key\", \"12\")\n\t\tc.Do(\"PTTL\", \"key\")\n\t\tc.Do(\"PERSIST\", \"key\")\n\t\tc.Do(\"DEL\", \"key\")\n\t\tc.Do(\"TYPE\", \"key\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\t// BITOP OPs are checked after the transaction.\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Do(\"BITOP\", \"BROKEN\", \"str\", \"\")\n\t\tc.Do(\"EXEC\")\n\t})\n\n\t// fail on invalid command\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"wrong number\", \"GET\")\n\t\tc.Error(\"Transaction discarded\", \"EXEC\")\n\t})\n\n\t/* FIXME\n\t\t// fail on unknown command\n\ttestRaw(t, func(c *client) {\n\t\t\tc.Do(\"MULTI\")\n\t\t\tc.Do(\"NOSUCH\")\n\t\t\tc.Do(\"EXEC\")\n\t})\n\t*/\n\n\t// failed EXEC cleaned up the tx\n\ttestRaw(t, func(c *client) {\n\t\tc.Do(\"MULTI\")\n\t\tc.Error(\"wrong number\", \"GET\")\n\t\tc.Error(\"Transaction discarded\", \"EXEC\")\n\t\tc.Do(\"MULTI\")\n\t})\n\n\ttestRaw2(t, func(c1, c2 *client) {\n\t\tc1.Do(\"WATCH\", \"foo\")\n\t\tc1.Do(\"MULTI\")\n\t\tc2.Do(\"SET\", \"foo\", \"12\")\n\t\tc2.Error(\"without\", \"EXEC\") // nil\n\t\tc1.Do(\"EXEC\")               // 0-length\n\t})\n}\n"
  },
  {
    "path": "miniredis/tests/smoke.rs",
    "content": "use redis::AsyncCommands;\n\n#[tokio::test]\nasync fn smoke_ping() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    let pong: String = redis::cmd(\"PING\").query_async(&mut con).await.unwrap();\n    assert_eq!(pong, \"PONG\");\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_set_get() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    let _: () = con.set(\"foo\", \"bar\").await.unwrap();\n    let val: String = con.get(\"foo\").await.unwrap();\n    assert_eq!(val, \"bar\");\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_set_get_direct() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    // Set via client\n    let _: () = con.set(\"key1\", \"value1\").await.unwrap();\n\n    // Read via direct API\n    m.check_get(\"key1\", \"value1\");\n\n    // Set via direct API\n    m.set(\"key2\", \"value2\");\n\n    // Read via client\n    let val: String = con.get(\"key2\").await.unwrap();\n    assert_eq!(val, \"value2\");\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_del_exists() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    let _: () = con.set(\"k\", \"v\").await.unwrap();\n    assert!(m.exists(\"k\"));\n\n    let deleted: i64 = con.del(\"k\").await.unwrap();\n    assert_eq!(deleted, 1);\n    assert!(!m.exists(\"k\"));\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_echo() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    let result: String = redis::cmd(\"ECHO\")\n        .arg(\"hello world\")\n        .query_async(&mut con)\n        .await\n        .unwrap();\n    assert_eq!(result, \"hello world\");\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_dbsize_flushdb() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    let _: () = con.set(\"a\", \"1\").await.unwrap();\n    let _: () = con.set(\"b\", \"2\").await.unwrap();\n\n    let size: i64 = redis::cmd(\"DBSIZE\").query_async(&mut con).await.unwrap();\n    assert_eq!(size, 2);\n\n    let _: () = redis::cmd(\"FLUSHDB\").query_async(&mut con).await.unwrap();\n    let size: i64 = redis::cmd(\"DBSIZE\").query_async(&mut con).await.unwrap();\n    assert_eq!(size, 0);\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_set_nx_xx() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    // NX: set only if not exists\n    let result: bool = con.set_nx(\"nxkey\", \"first\").await.unwrap();\n    assert!(result);\n\n    let result: bool = con.set_nx(\"nxkey\", \"second\").await.unwrap();\n    assert!(!result);\n\n    let val: String = con.get(\"nxkey\").await.unwrap();\n    assert_eq!(val, \"first\");\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_select_db() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    let _: () = con.set(\"key\", \"db0\").await.unwrap();\n\n    // SELECT 1 and set a different value\n    let _: () = redis::cmd(\"SELECT\")\n        .arg(1)\n        .query_async(&mut con)\n        .await\n        .unwrap();\n    let _: () = con.set(\"key\", \"db1\").await.unwrap();\n\n    // Back to db 0\n    let _: () = redis::cmd(\"SELECT\")\n        .arg(0)\n        .query_async(&mut con)\n        .await\n        .unwrap();\n    let val: String = con.get(\"key\").await.unwrap();\n    assert_eq!(val, \"db0\");\n\n    m.close().await;\n}\n\n#[tokio::test]\nasync fn smoke_fast_forward() {\n    let m = miniredis_rs::Miniredis::run().await.unwrap();\n    let client = redis::Client::open(m.redis_url()).unwrap();\n    let mut con = client.get_multiplexed_async_connection().await.unwrap();\n\n    // SET with EX 10\n    let _: () = redis::cmd(\"SET\")\n        .arg(\"temp\")\n        .arg(\"val\")\n        .arg(\"EX\")\n        .arg(10)\n        .query_async(&mut con)\n        .await\n        .unwrap();\n\n    // Key should exist\n    let val: Option<String> = con.get(\"temp\").await.unwrap();\n    assert_eq!(val, Some(\"val\".to_owned()));\n\n    // Fast forward 11 seconds\n    m.fast_forward(std::time::Duration::from_secs(11));\n\n    // Key should be gone (lazy expiration on next access)\n    let val: Option<String> = con.get(\"temp\").await.unwrap();\n    assert_eq!(val, None);\n\n    m.close().await;\n}\n"
  },
  {
    "path": "parser/encoding/rpc.go",
    "content": "package encoding\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang/protobuf/proto\"\n\n\t\"encr.dev/pkg/idents\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n// ParameterLocation is the request/response home of the parameter\ntype ParameterLocation string\n\nconst (\n\tUndefined ParameterLocation = \"undefined\" // Parameter location is Undefined\n\tHeader    ParameterLocation = \"header\"    // Parameter is placed in the HTTP header\n\tQuery     ParameterLocation = \"query\"     // Parameter is placed in the query string\n\tBody      ParameterLocation = \"body\"      // Parameter is placed in the body\n\tCookie    ParameterLocation = \"cookie\"    // Parameter is placed in cookies\n)\n\nvar (\n\tQueryTag = tagDescription{\n\t\tlocation:        Query,\n\t\toverrideDefault: true,\n\t}\n\tQsTag     = QueryTag\n\tHeaderTag = tagDescription{\n\t\tlocation:        Header,\n\t\toverrideDefault: true,\n\t\twireFormatter:   strings.ToLower,\n\t}\n\tJSONTag = tagDescription{\n\t\tlocation:        Body,\n\t\tomitEmptyOption: \"omitempty\",\n\t\toverrideDefault: false,\n\t}\n\tCookieTag = tagDescription{\n\t\tlocation:        Cookie,\n\t\tomitEmptyOption: \"omitempty\",\n\t\toverrideDefault: true,\n\t}\n)\n\n// authTags is a description of tags used for auth\nvar authTags = map[string]tagDescription{\n\t\"query\":  QueryTag,\n\t\"header\": HeaderTag,\n\t\"cookie\": CookieTag,\n}\n\n// requestTags is a description of tags used for requests\nvar requestTags = map[string]tagDescription{\n\t\"query\":  QueryTag,\n\t\"qs\":     QsTag,\n\t\"header\": HeaderTag,\n\t\"cookie\": CookieTag,\n\t\"json\":   JSONTag,\n}\n\n// responseTags is a description of tags used for responses\nvar responseTags = map[string]tagDescription{\n\t\"header\": HeaderTag,\n\t\"cookie\": CookieTag,\n\t\"json\":   JSONTag,\n}\n\n// tagDescription is used to map struct field tags to param locations\n// if overrideDefault is set, tagDescription.location will be used instead of encodingHints.defaultLocation\n// if the tag matches the paramLocation, the param name will be replaced with the\n// tag name\ntype tagDescription struct {\n\tlocation        ParameterLocation\n\toverrideDefault bool\n\tomitEmptyOption string\n\twireFormatter   func(name string) string\n}\n\n// encodingHints is used to determine the default location and applicable tag overrides for http\n// request/response encoding\ntype encodingHints struct {\n\tdefaultLocation ParameterLocation\n\ttags            map[string]tagDescription\n\toptions         *Options\n}\n\n// RPCEncoding expresses how an RPC should be encoded on the wire for both the request and responses.\ntype RPCEncoding struct {\n\tName        string     `json:\"name\"`\n\tDoc         string     `json:\"doc\"`\n\tAccessType  string     `json:\"access_type\"`\n\tProto       string     `json:\"proto\"`\n\tPath        *meta.Path `json:\"path\"`\n\tHttpMethods []string   `json:\"http_methods\"`\n\n\tDefaultMethod string `json:\"default_method\"`\n\t// Expresses how the default request encoding and method should be\n\t// Note: DefaultRequestEncoding.HTTPMethods will always be a slice with length 1\n\tDefaultRequestEncoding *RequestEncoding `json:\"request_encoding\"`\n\t// Expresses all the different ways the request can be encoded for this RPC\n\tRequestEncoding []*RequestEncoding `json:\"all_request_encodings\"`\n\t// Expresses how the response to this RPC will be encoded\n\tResponseEncoding *ResponseEncoding `json:\"response_encoding\"`\n}\n\n// RequestEncodingForMethod returns the request encoding required for the given HTTP method.\n// If the method is not supported by the RPC it reports nil.\nfunc (e *RPCEncoding) RequestEncodingForMethod(method string) *RequestEncoding {\n\tvar wildcardOption *RequestEncoding\n\n\tfor _, reqEnc := range e.RequestEncoding {\n\t\tfor _, m := range reqEnc.HTTPMethods {\n\t\t\tif strings.EqualFold(m, method) {\n\t\t\t\treturn reqEnc\n\t\t\t}\n\n\t\t\tif m == \"*\" {\n\t\t\t\twildcardOption = reqEnc\n\t\t\t}\n\t\t}\n\t}\n\treturn wildcardOption\n}\n\n// AuthEncoding expresses how a response should be encoded on the wire.\ntype AuthEncoding struct {\n\t// LegacyTokenFormat specifies whether the auth encoding uses the legacy format of\n\t// \"just give us a token as a string\". If true, the other parameters are all empty.\n\tLegacyTokenFormat bool\n\n\t// Contains metadata about how to marshal an HTTP parameter\n\tHeaderParameters []*ParameterEncoding `json:\"header_parameters\"`\n\tQueryParameters  []*ParameterEncoding `json:\"query_parameters\"`\n\tCookieParameters []*ParameterEncoding `json:\"cookie_parameters\"`\n}\n\n// ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName.\nfunc (e *AuthEncoding) ParameterEncodingMap() map[string]*ParameterEncoding {\n\treturn toEncodingMap(srcNameKey, e.HeaderParameters, e.QueryParameters, e.CookieParameters)\n}\n\n// ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name.\n// Conflicts result in an undefined encoding getting set.\nfunc (e *AuthEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding {\n\treturn toEncodingMultiMap(nameKey, e.HeaderParameters, e.QueryParameters, e.CookieParameters)\n}\n\n// ResponseEncoding expresses how a response should be encoded on the wire\ntype ResponseEncoding struct {\n\t// Contains metadata about how to marshal an HTTP parameter\n\tHeaderParameters []*ParameterEncoding `json:\"header_parameters\"`\n\tCookieParameters []*ParameterEncoding `json:\"cookie_parameters\"`\n\tBodyParameters   []*ParameterEncoding `json:\"body_parameters\"`\n}\n\n// ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName.\nfunc (e *ResponseEncoding) ParameterEncodingMap() map[string]*ParameterEncoding {\n\treturn toEncodingMap(srcNameKey, e.HeaderParameters, e.CookieParameters, e.BodyParameters)\n}\n\n// ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name.\n// Conflicts result in an undefined encoding getting set.\nfunc (e *ResponseEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding {\n\treturn toEncodingMultiMap(nameKey, e.HeaderParameters, e.CookieParameters, e.BodyParameters)\n}\n\n// RequestEncoding expresses how a request should be encoded for an explicit set of HTTPMethods\ntype RequestEncoding struct {\n\t// The HTTP methods these field configurations can be used for\n\tHTTPMethods []string `json:\"http_methods\"`\n\t// Contains metadata about how to marshal an HTTP parameter\n\tHeaderParameters []*ParameterEncoding `json:\"header_parameters\"`\n\tQueryParameters  []*ParameterEncoding `json:\"query_parameters\"`\n\tCookieParameters []*ParameterEncoding `json:\"cookie_parameters\"`\n\tBodyParameters   []*ParameterEncoding `json:\"body_parameters\"`\n}\n\n// ParameterEncodingMap returns the parameter encodings as a map, keyed by SrcName.\nfunc (e *RequestEncoding) ParameterEncodingMap() map[string]*ParameterEncoding {\n\treturn toEncodingMap(srcNameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters, e.CookieParameters)\n}\n\n// ParameterEncodingMapByName returns the parameter encodings as a map, keyed by Name.\n// Conflicts result in an undefined encoding getting set.\nfunc (e *RequestEncoding) ParameterEncodingMapByName() map[string][]*ParameterEncoding {\n\treturn toEncodingMultiMap(nameKey, e.HeaderParameters, e.QueryParameters, e.BodyParameters, e.CookieParameters)\n}\n\n// ParameterEncoding expresses how a parameter should be encoded on the wire\ntype ParameterEncoding struct {\n\t// The location specific name of the parameter (e.g. cheeseEater, cheese-eater, X-Cheese-Eater)\n\tName string `json:\"name\"`\n\t// Location is the location this encoding is for.\n\tLocation ParameterLocation `json:\"location\"`\n\t// OmitEmpty specifies whether the parameter should be omitted if it's empty.\n\tOmitEmpty bool `json:\"omit_empty\"`\n\t// SrcName is the name of the struct field\n\tSrcName string `json:\"src_name\"`\n\t// Doc is the documentation of the struct field\n\tDoc string `json:\"doc\"`\n\t// Type is the field's type description.\n\tType *schema.Type `json:\"type\"`\n\t// RawTag specifies the raw, unparsed struct tag for the field.\n\tRawTag string `json:\"raw_tag\"`\n\t// WireFormat is the wire format of the parameter.\n\tWireFormat string `json:\"wire_format\"`\n\t// Optional indicates whether the field is optional.\n\tOptional bool `json:\"optional\"`\n}\n\ntype Options struct {\n\t// SrcNameTag, if set, specifies which source tag should be used to determine\n\t// the value of the SrcName field in the returned parameter descriptions.\n\t//\n\t// If the given SrcNameTag is not present on the field, SrcName will be set\n\t// to the Go field name instead.\n\t//\n\t// If SrcNameTag is empty, SrcName is set to the Go field name.\n\tSrcNameTag string\n}\n\ntype APIEncoding struct {\n\tServices      []*ServiceEncoding `json:\"services\"`\n\tAuthorization *AuthEncoding      `json:\"authorization\"`\n}\n\ntype ServiceEncoding struct {\n\tName string         `json:\"name\"`\n\tDoc  string         `json:\"doc\"`\n\tRPCs []*RPCEncoding `json:\"rpcs\"`\n}\n\nfunc DescribeAPI(meta *meta.Data) *APIEncoding {\n\tapi := &APIEncoding{Services: make([]*ServiceEncoding, len(meta.Svcs))}\n\tfor i, s := range meta.Svcs {\n\t\tapi.Services[i] = DescribeService(meta, s)\n\t}\n\tif meta.AuthHandler == nil {\n\t\treturn api\n\t}\n\n\tvar err error\n\tapi.Authorization, err = DescribeAuth(meta, meta.AuthHandler.Params, nil)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Invalid auth definition: %s: %v\", meta.AuthHandler.Name, err))\n\t}\n\treturn api\n}\n\nfunc findDoc(relPath string, meta *meta.Data) string {\n\tfor _, p := range meta.Pkgs {\n\t\tif p.RelPath == relPath {\n\t\t\treturn p.Doc\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc DescribeService(meta *meta.Data, svc *meta.Service) *ServiceEncoding {\n\tservice := &ServiceEncoding{Name: svc.Name, Doc: findDoc(svc.RelPath, meta), RPCs: make([]*RPCEncoding, len(svc.Rpcs))}\n\tfor i, r := range svc.Rpcs {\n\t\trpc, err := DescribeRPC(meta, r, nil)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"invalid rpc: %v\", err))\n\t\t}\n\t\tservice.RPCs[i] = rpc\n\t}\n\treturn service\n}\n\n// DescribeRPC expresses how to encode an RPCs request and response objects for the wire.\nfunc DescribeRPC(appMetaData *meta.Data, rpc *meta.RPC, options *Options) (*RPCEncoding, error) {\n\tencoding := &RPCEncoding{\n\t\tDefaultMethod: DefaultClientHttpMethod(rpc),\n\t\tName:          rpc.Name,\n\t\tAccessType:    rpc.AccessType.String(),\n\t\tProto:         rpc.Proto.String(),\n\t\tPath:          rpc.Path,\n\t\tDoc:           rpc.GetDoc(),\n\t}\n\tvar err error\n\t// Work out the request encoding\n\tencoding.RequestEncoding, err = DescribeRequest(appMetaData, rpc.RequestSchema, options, rpc.HttpMethods...)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"request encoding\")\n\t}\n\n\t// Work out the response encoding\n\tencoding.ResponseEncoding, err = DescribeResponse(appMetaData, rpc.ResponseSchema, options)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"response encoding\")\n\t}\n\n\tif encoding.RequestEncoding != nil {\n\t\t// Setup the default request encoding\n\t\tdefaultEncoding := encoding.RequestEncodingForMethod(encoding.DefaultMethod)\n\t\tencoding.DefaultRequestEncoding = &RequestEncoding{\n\t\t\tHTTPMethods:      []string{encoding.DefaultMethod},\n\t\t\tHeaderParameters: defaultEncoding.HeaderParameters,\n\t\t\tBodyParameters:   defaultEncoding.BodyParameters,\n\t\t\tQueryParameters:  defaultEncoding.QueryParameters,\n\t\t\tCookieParameters: defaultEncoding.CookieParameters,\n\t\t}\n\t}\n\n\treturn encoding, nil\n}\n\n// GetConcreteStructType returns a construct Struct object for the given schema. This means any generic types\n// in the struct will be resolved to their concrete types and there will be no generic parameters in the struct object.\n// However, any nested structs may still contain generic types.\n//\n// If a nil schema is provided, a nil struct is returned.\nfunc GetConcreteStructType(appDecls []*schema.Decl, typ *schema.Type, typeArgs []*schema.Type) (*schema.Struct, error) {\n\t// dereference pointers\n\tpointer := typ.GetPointer()\n\tfor pointer != nil {\n\t\ttyp = pointer.Base\n\t\tpointer = typ.GetPointer()\n\t}\n\n\ttyp, err := GetConcreteType(appDecls, typ, typeArgs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstruc := typ.GetStruct()\n\tif struc == nil {\n\t\treturn nil, errors.Newf(\"unsupported type %+v\", reflect.TypeOf(typ.Typ))\n\t}\n\n\treturn struc, nil\n}\n\n// GetConcreteType returns a concrete type for the given schema. This means any generic types\n// in the top level type will be resolved to their concrete types and there will be no generic parameters in returned typ.\n// However, any nested types may still contain generic types.\n//\n// If a nil schema is provided, a nil is returned.\nfunc GetConcreteType(appDecls []*schema.Decl, originalType *schema.Type, typeArgs []*schema.Type) (*schema.Type, error) {\n\tif originalType == nil {\n\t\t// If there's no schema type, we want to shortcut\n\t\treturn nil, nil\n\t}\n\n\tswitch typ := originalType.Typ.(type) {\n\tcase *schema.Type_Struct:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\t// Deep copy the original struct\n\t\tstruc, ok := proto.Clone(typ.Struct).(*schema.Struct)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"failed to clone struct\")\n\t\t}\n\n\t\t// replace any type parameters with the type argument\n\t\tfor _, field := range struc.Fields {\n\t\t\tfield.Typ = resolveTypeParams(field.Typ, typeArgs)\n\t\t}\n\n\t\treturn &schema.Type{Typ: &schema.Type_Struct{Struct: struc}}, nil\n\n\tcase *schema.Type_Map:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\t// Deep copy the original struct\n\t\tmapType, ok := proto.Clone(typ.Map).(*schema.Map)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"failed to clone map\")\n\t\t}\n\n\t\treturn resolveTypeParams(&schema.Type{Typ: &schema.Type_Map{Map: mapType}}, typeArgs), nil\n\n\tcase *schema.Type_Union:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\ttypes := make([]*schema.Type, len(typ.Union.Types))\n\t\tfor i, t := range typ.Union.Types {\n\t\t\t// Deep copy the type\n\t\t\tcloned := proto.Clone(t).(*schema.Type)\n\t\t\ttypes[i] = resolveTypeParams(cloned, typeArgs)\n\t\t}\n\n\t\treturn &schema.Type{Typ: &schema.Type_Union{\n\t\t\tUnion: &schema.Union{\n\t\t\t\tTypes: types,\n\t\t\t},\n\t\t}}, nil\n\n\tcase *schema.Type_List:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\t// Deep copy the original struct\n\t\tlist, ok := proto.Clone(typ.List).(*schema.List)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"failed to clone list type\")\n\t\t}\n\n\t\t// replace any type parameters with the type argument\n\t\treturn resolveTypeParams(&schema.Type{Typ: &schema.Type_List{List: list}}, typeArgs), nil\n\n\tcase *schema.Type_Pointer:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\t// Deep copy the original struct\n\t\tpointer, ok := proto.Clone(typ.Pointer).(*schema.Pointer)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"failed to clone pointer type\")\n\t\t}\n\n\t\tvar err error\n\t\tpointer.Base, err = GetConcreteType(appDecls, pointer.Base, typeArgs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// replace any type parameters with the type argument\n\t\treturn resolveTypeParams(&schema.Type{Typ: &schema.Type_Pointer{Pointer: pointer}}, typeArgs), nil\n\n\tcase *schema.Type_Option:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\t// Deep copy the original struct\n\t\toption, ok := proto.Clone(typ.Option).(*schema.Option)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"failed to clone option type\")\n\t\t}\n\n\t\tvar err error\n\t\toption.Value, err = GetConcreteType(appDecls, option.Value, typeArgs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// replace any type parameters with the type argument\n\t\treturn resolveTypeParams(&schema.Type{Typ: &schema.Type_Option{Option: option}}, typeArgs), nil\n\n\tcase *schema.Type_Config:\n\t\t// If there are no type arguments, we've got a concrete type\n\t\tif len(typeArgs) == 0 {\n\t\t\treturn originalType, nil\n\t\t}\n\n\t\t// Deep copy the original struct\n\t\tconfig, ok := proto.Clone(typ.Config).(*schema.ConfigValue)\n\t\tif !ok {\n\t\t\treturn nil, errors.New(\"failed to clone config type\")\n\t\t}\n\n\t\t// replace any type parameters with the type argument\n\t\treturn resolveTypeParams(&schema.Type{Typ: &schema.Type_Config{Config: config}}, typeArgs), nil\n\n\tcase *schema.Type_Named:\n\t\tdecl := appDecls[typ.Named.Id]\n\t\treturn GetConcreteType(appDecls, decl.Type, typ.Named.TypeArguments)\n\n\tcase *schema.Type_Builtin:\n\t\treturn originalType, nil\n\n\tcase *schema.Type_Literal:\n\t\treturn originalType, nil\n\n\tdefault:\n\t\treturn nil, errors.Newf(\"unsupported type %+v\", reflect.TypeOf(typ))\n\t}\n}\n\n// resolveTypeParams resolves any type parameters in the given type to the given type arguments.\n// only at the top level object - so nested type arguments are not resolved\nfunc resolveTypeParams(typ *schema.Type, typeArgs []*schema.Type) *schema.Type {\n\tswitch t := typ.Typ.(type) {\n\tcase *schema.Type_TypeParameter:\n\t\treturn typeArgs[t.TypeParameter.ParamIdx]\n\n\tcase *schema.Type_Struct:\n\t\tfor _, field := range t.Struct.Fields {\n\t\t\tfield.Typ = resolveTypeParams(field.Typ, typeArgs)\n\t\t}\n\n\tcase *schema.Type_List:\n\t\tt.List.Elem = resolveTypeParams(t.List.Elem, typeArgs)\n\n\tcase *schema.Type_Map:\n\t\tt.Map.Key = resolveTypeParams(t.Map.Key, typeArgs)\n\t\tt.Map.Value = resolveTypeParams(t.Map.Value, typeArgs)\n\n\tcase *schema.Type_Config:\n\t\tt.Config.Elem = resolveTypeParams(t.Config.Elem, typeArgs)\n\n\tcase *schema.Type_Pointer:\n\t\tt.Pointer.Base = resolveTypeParams(t.Pointer.Base, typeArgs)\n\n\tcase *schema.Type_Option:\n\t\tt.Option.Value = resolveTypeParams(t.Option.Value, typeArgs)\n\n\tcase *schema.Type_Named:\n\t\tfor i, param := range t.Named.TypeArguments {\n\t\t\tt.Named.TypeArguments[i] = resolveTypeParams(param, typeArgs)\n\t\t}\n\t}\n\n\treturn typ\n}\n\n// DefaultClientHttpMethod works out the default HTTP method a client should use for a given RPC.\n// When possible we will default to POST either when no method has been specified on the API or when\n// then is a selection of methods and POST is one of them. If POST is not allowed as a method then\n// we will use the first specified method.\nfunc DefaultClientHttpMethod(rpc *meta.RPC) string {\n\t// Default to POST if we have a wildcard method or if POST is one of the allowed methods.\n\tif rpc.HttpMethods[0] == \"*\" || slices.Contains(rpc.HttpMethods, \"POST\") {\n\t\treturn \"POST\"\n\t}\n\treturn rpc.HttpMethods[0]\n\n}\n\n// DescribeAuth generates a ParameterEncoding per field of the auth struct and returns it as\n// the AuthEncoding. If authSchema is nil it returns nil, nil.\nfunc DescribeAuth(appMetaData *meta.Data, authSchema *schema.Type, options *Options) (*AuthEncoding, error) {\n\tif authSchema == nil {\n\t\treturn nil, nil\n\t}\n\n\tswitch t := authSchema.Typ.(type) {\n\tcase *schema.Type_Builtin:\n\t\tif t.Builtin != schema.Builtin_STRING {\n\t\t\treturn nil, errors.Newf(\"unsupported auth parameter %v\", errors.Safe(t.Builtin))\n\t\t}\n\t\treturn &AuthEncoding{LegacyTokenFormat: true}, nil\n\tcase *schema.Type_Named:\n\tcase *schema.Type_Pointer:\n\tdefault:\n\t\treturn nil, errors.Newf(\"unsupported auth parameter type %T\", errors.Safe(t))\n\t}\n\n\tauthStruct, err := GetConcreteStructType(appMetaData.Decls, authSchema, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"auth struct\")\n\t}\n\tfields, err := describeParams(appMetaData.Language, &encodingHints{Undefined, authTags, options}, authStruct)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif locationDiff := keyDiff(fields, Header, Query, Cookie); len(locationDiff) > 0 {\n\t\treturn nil, errors.Newf(\"auth must only contain query, header, and cookie parameters. Found: %v\", locationDiff)\n\t}\n\treturn &AuthEncoding{\n\t\tQueryParameters:  fields[Query],\n\t\tHeaderParameters: fields[Header],\n\t\tCookieParameters: fields[Cookie],\n\t}, nil\n}\n\n// DescribeResponse generates a ParameterEncoding per field of the response struct and returns it as\n// the ResponseEncoding\nfunc DescribeResponse(appMetaData *meta.Data, responseSchema *schema.Type, options *Options) (*ResponseEncoding, error) {\n\tif responseSchema == nil {\n\t\treturn nil, nil\n\t}\n\tresponseStruct, err := GetConcreteStructType(appMetaData.Decls, responseSchema, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"response struct\")\n\t}\n\tfields, err := describeParams(appMetaData.Language, &encodingHints{Body, responseTags, options}, responseStruct)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif keys := keyDiff(fields, Header, Body, Cookie); len(keys) > 0 {\n\t\treturn nil, errors.Newf(\"response must only contain body, header and cookie parameters. Found: %v\", keys)\n\t}\n\treturn &ResponseEncoding{\n\t\tBodyParameters:   fields[Body],\n\t\tHeaderParameters: fields[Header],\n\t\tCookieParameters: fields[Cookie],\n\t}, nil\n}\n\n// keyDiff returns the diff between src.keys and keys\nfunc keyDiff[T comparable, V any](src map[T]V, keys ...T) (diff []T) {\n\tfor k := range src {\n\t\tif !slices.Contains(keys, k) {\n\t\t\tdiff = append(diff, k)\n\t\t}\n\t}\n\treturn diff\n}\n\n// DescribeRequest groups the provided httpMethods by default ParameterLocation and returns a RequestEncoding\n// per ParameterLocation\nfunc DescribeRequest(appMetaData *meta.Data, requestSchema *schema.Type, options *Options, httpMethods ...string) ([]*RequestEncoding, error) {\n\tmethodsByDefaultLocation := make(map[ParameterLocation][]string)\n\tfor _, m := range httpMethods {\n\t\tswitch m {\n\t\tcase \"GET\", \"HEAD\", \"DELETE\":\n\t\t\tmethodsByDefaultLocation[Query] = append(methodsByDefaultLocation[Query], m)\n\t\tcase \"*\":\n\t\t\tmethodsByDefaultLocation[Body] = []string{\"POST\", \"PUT\", \"PATCH\"}\n\t\t\tmethodsByDefaultLocation[Query] = []string{\"GET\", \"HEAD\", \"DELETE\"}\n\t\tdefault:\n\t\t\tmethodsByDefaultLocation[Body] = append(methodsByDefaultLocation[Body], m)\n\t\t}\n\t}\n\n\tvar requestStruct *schema.Struct\n\tvar err error\n\tif requestSchema != nil {\n\t\trequestStruct, err = GetConcreteStructType(appMetaData.Decls, requestSchema, nil)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"request struct\")\n\t\t}\n\t}\n\n\tvar reqs []*RequestEncoding\n\tfor location, methods := range methodsByDefaultLocation {\n\t\tvar fields map[ParameterLocation][]*ParameterEncoding\n\n\t\tif requestStruct != nil {\n\t\t\tfields, err = describeParams(appMetaData.Language, &encodingHints{location, requestTags, options}, requestStruct)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tif keys := keyDiff(fields, Query, Header, Body, Cookie); len(keys) > 0 {\n\t\t\treturn nil, errors.Newf(\"request must only contain Query, Body, Header and Cookie parameters. Found: %v\", keys)\n\t\t}\n\t\treqs = append(reqs, &RequestEncoding{\n\t\t\tHTTPMethods:      methods,\n\t\t\tQueryParameters:  fields[Query],\n\t\t\tHeaderParameters: fields[Header],\n\t\t\tCookieParameters: fields[Cookie],\n\t\t\tBodyParameters:   fields[Body],\n\t\t})\n\t}\n\n\t// Sort by first method to get a deterministic order (list is randomized by map above)\n\tsort.Slice(reqs, func(i, j int) bool {\n\t\treturn reqs[i].HTTPMethods[0] < reqs[j].HTTPMethods[0]\n\t})\n\treturn reqs, nil\n}\n\n// describeParams calls describeParam() for each field in the payload struct\nfunc describeParams(lang meta.Lang, encodingHints *encodingHints, payload *schema.Struct) (fields map[ParameterLocation][]*ParameterEncoding, err error) {\n\tparamByLocation := make(map[ParameterLocation][]*ParameterEncoding)\n\tfor _, f := range payload.GetFields() {\n\t\tf, err := describeParam(lang, encodingHints, f)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif f != nil {\n\t\t\tparamByLocation[f.Location] = append(paramByLocation[f.Location], f)\n\t\t}\n\t}\n\treturn paramByLocation, nil\n}\n\n// formatName formats a parameter name with the default formatting for the location (e.g. snakecase for query)\nfunc formatName(lang meta.Lang, location ParameterLocation, name string) string {\n\tif location == Query && lang == meta.Lang_GO {\n\t\treturn idents.Convert(name, idents.SnakeCase)\n\t}\n\n\treturn name\n}\n\n// IgnoreField returns true if the field name is \"-\" is any of the valid request or response tags\n// or if the field is marked with encore:\"httpstatus\" (which shouldn't appear in client types)\nfunc IgnoreField(field *schema.Field) bool {\n\tfor _, tag := range field.Tags {\n\t\tif _, found := requestTags[tag.Key]; found && tag.Name == \"-\" {\n\t\t\treturn true\n\t\t}\n\t\t// Skip fields with encore:\"httpstatus\" tag - they're for internal HTTP status handling only\n\t\tif tag.Key == \"encore\" && tag.Name == \"httpstatus\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// describeParam returns the ParameterEncoding which uses field tags to describe how the parameter\n// (e.g. qs, query, header) should be encoded in HTTP (name and location).\n//\n// It returns nil, nil if the field is not to be encoded.\nfunc describeParam(lang meta.Lang, encodingHints *encodingHints, field *schema.Field) (*ParameterEncoding, error) {\n\tlocation := encodingHints.defaultLocation\n\tname := formatName(lang, encodingHints.defaultLocation, field.Name)\n\tparam := ParameterEncoding{\n\t\tName:       name,\n\t\tOmitEmpty:  false,\n\t\tSrcName:    field.Name,\n\t\tDoc:        field.Doc,\n\t\tType:       field.Typ,\n\t\tRawTag:     field.RawTag,\n\t\tOptional:   field.Optional,\n\t\tWireFormat: name,\n\t}\n\n\tvar usedOverrideTag string\n\tfor _, tag := range field.Tags {\n\t\tif IgnoreField(field) {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\ttagHint, ok := encodingHints.tags[tag.Key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif tagHint.overrideDefault {\n\t\t\tif usedOverrideTag != \"\" {\n\t\t\t\treturn nil, errors.Newf(\"tag conflict: %s cannot be combined with %s\", usedOverrideTag, tag.Key)\n\t\t\t}\n\t\t\tlocation = tagHint.location\n\t\t\tusedOverrideTag = tag.Key\n\t\t}\n\t\tif tagHint.location == location {\n\t\t\tif tag.Name != \"\" {\n\t\t\t\tparam.Name = tag.Name\n\t\t\t\tif tagHint.wireFormatter != nil {\n\t\t\t\t\tparam.WireFormat = tagHint.wireFormatter(tag.Name)\n\t\t\t\t} else {\n\t\t\t\t\tparam.WireFormat = tag.Name\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif tagHint.omitEmptyOption != \"\" {\n\t\t\tfor _, o := range tag.Options {\n\t\t\t\tif o == tagHint.omitEmptyOption {\n\t\t\t\t\tparam.OmitEmpty = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif encodingHints.options != nil && tag.Key == encodingHints.options.SrcNameTag {\n\t\t\tparam.SrcName = tag.Name\n\t\t}\n\t}\n\n\tif param.Name == \"-\" {\n\t\treturn nil, nil\n\t}\n\n\tparam.Location = location\n\treturn &param, nil\n}\n\n// toEncodingMap returns a map from SrcName to parameter encodings.\nfunc toEncodingMap(keyFunc func(e *ParameterEncoding) string, encodings ...[]*ParameterEncoding) map[string]*ParameterEncoding {\n\tres := make(map[string]*ParameterEncoding)\n\tfor _, e := range encodings {\n\t\tfor _, param := range e {\n\t\t\tres[keyFunc(param)] = param\n\t\t}\n\t}\n\treturn res\n}\n\n// toEncodingMultiMap returns a map from a key to the list of parameter encodings\n// matching that key.\nfunc toEncodingMultiMap(keyFunc func(e *ParameterEncoding) string, encodings ...[]*ParameterEncoding) map[string][]*ParameterEncoding {\n\tres := make(map[string][]*ParameterEncoding)\n\tfor _, e := range encodings {\n\t\tfor _, param := range e {\n\t\t\tkey := keyFunc(param)\n\t\t\tres[key] = append(res[key], param)\n\t\t}\n\t}\n\treturn res\n}\n\nfunc srcNameKey(e *ParameterEncoding) string {\n\treturn e.SrcName\n}\n\nfunc nameKey(e *ParameterEncoding) string {\n\treturn e.Name\n}\n"
  },
  {
    "path": "pkg/ansi/ansi.go",
    "content": "// Package ansi provides helper functions for writing ANSI terminal escape codes.\npackage ansi\n\nimport \"fmt\"\n\n// SetCursorPosition returns the ANSI escape code for setting the cursor position.\n// The rows and columns are one-based. If <=0 they default to the first row/column.\nfunc SetCursorPosition(row, col int) string {\n\tif row <= 0 {\n\t\trow = 1\n\t}\n\tif col <= 0 {\n\t\tcol = 1\n\t}\n\treturn fmt.Sprintf(\"\\u001b[%d;%dH\", row, col)\n}\n\ntype ClearScreenMethod int\n\nconst (\n\tCursorToBottom           ClearScreenMethod = 0\n\tCursorToTop              ClearScreenMethod = 1\n\tWholeScreen              ClearScreenMethod = 2\n\tWholeScreenAndScrollback ClearScreenMethod = 3\n)\n\n// ClearScreen clears the screen according to the given method.\nfunc ClearScreen(method ClearScreenMethod) string {\n\treturn fmt.Sprintf(\"\\u001b[%dJ\", method)\n}\n\ntype ClearLineMethod int\n\nconst (\n\tCursorToEnd   ClearLineMethod = 0 // cursor to end of line\n\tCursorToStart ClearLineMethod = 1 // cursor to start of line\n\tWholeLine     ClearLineMethod = 2\n)\n\n// ClearLine clears the current line according to the given method.\n// The cursor position within the line does not change.\nfunc ClearLine(method ClearLineMethod) string {\n\treturn fmt.Sprintf(\"\\u001b[%dK\", method)\n}\n\nconst (\n\t// SaveCursorPosition saves the current cursor position.\n\tSaveCursorPosition = \"\\u001b7\"\n\t// RestoreCursorPosition restores the cursor position to the saved position.\n\tRestoreCursorPosition = \"\\u001b8\"\n)\n\n// MoveCursorLeft moves the cursor left n cells.\n// If the cursor is already at the edge of the screen it has no effect.\n// If n is negative it moves to the right instead.\nfunc MoveCursorLeft(n int) string {\n\tif n < 0 {\n\t\treturn MoveCursorRight(-n)\n\t}\n\treturn fmt.Sprintf(\"\\u001b[%dD\", n)\n}\n\n// MoveCursorRight moves the cursor right n cells.\n// If the cursor is already at the edge of the screen it has no effect.\n// If n is negative it moves to the left instead.\nfunc MoveCursorRight(n int) string {\n\tif n < 0 {\n\t\treturn MoveCursorLeft(-n)\n\t}\n\treturn fmt.Sprintf(\"\\u001b[%dC\", n)\n}\n"
  },
  {
    "path": "pkg/appfile/appfile.go",
    "content": "// Package appfile reads and writes encore.app files.\npackage appfile\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/tailscale/hujson\"\n\t\"mvdan.cc/sh/v3/expand\"\n\t\"mvdan.cc/sh/v3/interp\"\n\t\"mvdan.cc/sh/v3/syntax\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n)\n\n// Name is the name of the Encore app file.\n// It is expected to be located in the root of the Encore app\n// (which is usually the Git repository root).\nconst Name = \"encore.app\"\n\ntype Lang string\n\nconst (\n\tLangGo Lang = \"go\"\n\tLangTS Lang = \"typescript\"\n)\n\n// File is a parsed encore.app file.\ntype File struct {\n\t// ID is the encore.dev app id for the app.\n\t// It is empty if the app is not linked to encore.dev.\n\tID string `json:\"id\"` // can be empty\n\n\t// Experiments is a list of values to enable experimental features in Encore.\n\t// These are not guaranteed to be stable in either runtime behaviour\n\t// or in API design.\n\t//\n\t// Do not use these features in production without consulting the Encore team.\n\tExperiments []experiments.Name `json:\"experiments,omitempty\"`\n\n\t// Lang is the language the app is written in.\n\t// If empty it defaults to Go.\n\tLang Lang `json:\"lang\"`\n\n\t// Configure global CORS settings for the application which\n\t// will be applied to all API gateways into the application.\n\tGlobalCORS *CORS `json:\"global_cors,omitempty\"`\n\n\t// Build contains build settings for the application.\n\tBuild Build `json:\"build,omitempty\"`\n\n\t// CgoEnabled enables building with cgo.\n\t//\n\t// Deprecated: Use build.cgo_enabled instead.\n\tCgoEnabled bool `json:\"cgo_enabled,omitempty\"`\n\n\t// DockerBaseImage changes the docker base image used for building the application\n\t// in Encore's CI/CD system. If unspecified it defaults to \"scratch\".\n\t//\n\t// Deprecated: Use build.docker.base_image instead.\n\tDockerBaseImage string `json:\"docker_base_image,omitempty\"`\n\n\t// LogLevel is the minimum log level for the app.\n\t// If empty it defaults to \"trace\".\n\tLogLevel string `json:\"log_level,omitempty\"`\n}\n\ntype Build struct {\n\t// CgoEnabled enables building with cgo.\n\tCgoEnabled bool `json:\"cgo_enabled,omitempty\"`\n\n\t// Docker configures the docker images built\n\t// by Encore's CI/CD system.\n\tDocker Docker `json:\"docker,omitempty\"`\n\n\t// WorkerPooling enables worker pooling for Encore.ts.\n\tWorkerPooling bool `json:\"worker_pooling,omitempty\"`\n\n\t// Hooks configures hooks for the build process.\n\tHooks Hooks `json:\"hooks,omitempty\"`\n}\n\ntype Hooks struct {\n\tPreBuild  Hook `json:\"prebuild,omitempty\"`\n\tPostBuild Hook `json:\"postbuild,omitempty\"`\n}\n\n// Hook represents a build hook command.\n// Can be specified as a string or as an object with command and env.\ntype Hook struct {\n\tCommand string            // The command to execute\n\tEnv     map[string]string // Optional environment variables\n}\n\n// IsSet returns true if the hook is configured.\nfunc (h Hook) IsSet() bool {\n\treturn h.Command != \"\"\n}\n\n// UnmarshalJSON handles both string and object formats.\nfunc (h *Hook) UnmarshalJSON(data []byte) error {\n\t// Try string format first\n\tvar cmd string\n\tif err := json.Unmarshal(data, &cmd); err == nil {\n\t\th.Command = cmd\n\t\treturn nil\n\t}\n\n\t// Try structured format\n\ttype hookData struct {\n\t\tCommand string            `json:\"command\"`\n\t\tEnv     map[string]string `json:\"env\"`\n\t}\n\tvar hd hookData\n\tif err := json.Unmarshal(data, &hd); err != nil {\n\t\treturn err\n\t}\n\th.Command = hd.Command\n\th.Env = hd.Env\n\treturn nil\n}\n\n// Run executes the hook command with shell-style parsing and variable expansion.\n// Supports shell operators like &&, ||, and pipes.\nfunc (h Hook) Run(ctx context.Context, dir string, stdout, stderr io.Writer) error {\n\tif h.Command == \"\" {\n\t\treturn nil\n\t}\n\n\t// Parse the command as a shell script\n\tfile, err := syntax.NewParser().Parse(strings.NewReader(h.Command), \"\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"parse command: %w\", err)\n\t}\n\n\t// Build environment: system env + custom env vars\n\tenv := os.Environ()\n\tfor k, v := range h.Env {\n\t\tenv = append(env, k+\"=\"+v)\n\t}\n\n\t// Create interpreter with environment and I/O\n\trunner, err := interp.New(\n\t\tinterp.Env(expand.ListEnviron(env...)),\n\t\tinterp.Dir(dir),\n\t\tinterp.StdIO(nil, stdout, stderr),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"create interpreter: %w\", err)\n\t}\n\n\treturn runner.Run(ctx, file)\n}\n\n// String returns the command string.\nfunc (h Hook) String() string {\n\treturn h.Command\n}\n\ntype Docker struct {\n\t// BaseImage changes the docker base image used for building the application\n\t// in Encore's CI/CD system. If unspecified it defaults to \"scratch\".\n\tBaseImage string `json:\"base_image,omitempty\"`\n\n\t// BundleSource determines whether the source code of the application\n\t// should be bundled into the binary, at \"/workspace\".\n\tBundleSource bool `json:\"bundle_source,omitempty\"`\n\n\t// WorkingDir specifies the working directory to start the docker image in.\n\t// If empty it defaults to \"/workspace\" if the source code is bundled, and to \"/\" otherwise.\n\tWorkingDir string `json:\"working_dir,omitempty\"`\n\n\t// ProcessPerService specifies whether each service should run in its own process. If false,\n\t// all services are run in the same process.\n\tProcessPerService bool `json:\"process_per_service,omitempty\"`\n}\n\ntype CORS struct {\n\t// Debug enables CORS debug logging.\n\tDebug bool `json:\"debug,omitempty\"`\n\n\t// AllowHeaders allows an app to specify additional headers that should be\n\t// accepted by the app.\n\t//\n\t// If the list contains \"*\", then all headers are allowed.\n\tAllowHeaders []string `json:\"allow_headers\"`\n\n\t// ExposeHeaders allows an app to specify additional headers that should be\n\t// exposed from the app, beyond the default set always recognized by Encore.\n\t//\n\t// If the list contains \"*\", then all headers are exposed.\n\tExposeHeaders []string `json:\"expose_headers\"`\n\n\t// AllowOriginsWithoutCredentials specifies the allowed origins for requests\n\t// that don't include credentials. If nil it defaults to allowing all domains\n\t// (equivalent to []string{\"*\"}).\n\tAllowOriginsWithoutCredentials []string `json:\"allow_origins_without_credentials,omitempty\"`\n\n\t// AllowOriginsWithCredentials specifies the allowed origins for requests\n\t// that include credentials. If a request is made from an Origin in this list\n\t// Encore responds with Access-Control-Allow-Origin: <Origin>.\n\t//\n\t// The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n\t// or \"https://*-myapp.example.com\").\n\tAllowOriginsWithCredentials []string `json:\"allow_origins_with_credentials,omitempty\"`\n}\n\n// Parse parses the app file data into a File.\nfunc Parse(data []byte) (*File, error) {\n\tvar f File\n\tdata, err := hujson.Standardize(data)\n\tif err == nil {\n\t\terr = json.Unmarshal(data, &f)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"appfile.Parse: %v\", err)\n\t}\n\n\tswitch f.Lang {\n\tcase LangGo, LangTS:\n\t// Do nothing\n\tcase \"\":\n\t\tf.Lang = LangGo\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"appfile.Parse: invalid lang %q\", f.Lang)\n\t}\n\n\t// Parse deprecated fields into the new Build struct.\n\tf.Build.CgoEnabled = f.Build.CgoEnabled || f.CgoEnabled\n\tif f.Build.Docker.BaseImage == \"\" {\n\t\tf.Build.Docker.BaseImage = f.DockerBaseImage\n\t}\n\n\treturn &f, nil\n}\n\n// ParseFile parses the app file located at path.\nfunc ParseFile(path string) (*File, error) {\n\tdata, err := os.ReadFile(path)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn &File{}, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"appfile.ParseFile: %w\", err)\n\t}\n\treturn Parse(data)\n}\n\n// ParseFileStrict parses the app file located at path.\n// Unlike ParseFile, it returns an error if the file does not exist.\nfunc ParseFileStrict(path string) (*File, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"appfile.ParseFileStrict: %w\", err)\n\t}\n\treturn Parse(data)\n}\n\n// Slug parses the app slug for the encore.app file located at path.\n// The slug can be empty if the app is not linked to encore.dev.\nfunc Slug(appRoot string) (string, error) {\n\tf, err := ParseFile(filepath.Join(appRoot, Name))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn f.ID, nil\n}\n\n// Experiments returns the experimental feature the app located\n// at appRoot has opted into.\nfunc Experiments(appRoot string) ([]experiments.Name, error) {\n\tf, err := ParseFile(filepath.Join(appRoot, Name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn f.Experiments, nil\n}\n\n// GlobalCORS returns the global CORS settings for the app located\nfunc GlobalCORS(appRoot string) (*CORS, error) {\n\tf, err := ParseFile(filepath.Join(appRoot, Name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn f.GlobalCORS, nil\n}\n\n// AppLang returns the language of the app located at appRoot.\nfunc AppLang(appRoot string) (Lang, error) {\n\tf, err := ParseFile(filepath.Join(appRoot, Name))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn f.Lang, nil\n}\n"
  },
  {
    "path": "pkg/bits/bits.go",
    "content": "package bits\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\ntype Bit struct {\n\tSlug        string\n\tTitle       string\n\tDescription string\n\n\t// GitHubTree is a URL to the GitHub tree for this bit,\n\t// in the format expected by github.ParseTree.\n\tGitHubTree string\n}\n\n// List lists available bits.\nfunc List(ctx context.Context) ([]*Bit, error) {\n\tresp, err := http.Get(\"https://automativity.encore.dev/bits\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\tslurp, _ := io.ReadAll(resp.Body)\n\t\treturn nil, errors.Newf(\"got status %d: %s\", resp.StatusCode, slurp)\n\t}\n\n\tvar data struct {\n\t\tBits []*Bit\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&data); err != nil {\n\t\treturn nil, errors.Wrap(err, \"decode json response\")\n\t}\n\treturn data.Bits, nil\n}\n\n// ErrNotFound is reported by Get if the bit with the given slug is not found.\nvar ErrNotFound = errors.New(\"bit not found\")\n\n// Get retrieves a bit by its slug.\nfunc Get(ctx context.Context, slug string) (*Bit, error) {\n\tresp, err := http.Get(\"https://automativity.encore.dev/bits/\" + url.PathEscape(slug))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode == 404 {\n\t\treturn nil, ErrNotFound\n\t} else if resp.StatusCode != 200 {\n\t\tslurp, _ := io.ReadAll(resp.Body)\n\t\treturn nil, errors.Newf(\"got status %d: %s\", resp.StatusCode, slurp)\n\t}\n\n\tvar bit Bit\n\tif err := json.NewDecoder(resp.Body).Decode(&bit); err != nil {\n\t\treturn nil, errors.Wrap(err, \"decode json response\")\n\t}\n\treturn &bit, nil\n}\n"
  },
  {
    "path": "pkg/bits/download.go",
    "content": "package bits\n\nimport (\n\t\"context\"\n\t\"go/token\"\n\t\"runtime\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/pkg/github\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/parser\"\n)\n\n// Extract downloads and extracts a bit into a given directory.\nfunc Extract(ctx context.Context, b *Bit, dst string) error {\n\ttree, err := github.ParseTree(ctx, b.GitHubTree)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn github.ExtractTree(ctx, tree, dst)\n}\n\n// Describe describes the contents of the bit extracted in dir.\nfunc Describe(ctx context.Context, dir string) (desc *app.Desc, err error) {\n\tfs := token.NewFileSet()\n\terrs := perr.NewList(ctx, fs)\n\tpc := &parsectx.Context{\n\t\tCtx: ctx,\n\t\tLog: zerolog.Logger{},\n\t\tBuild: parsectx.BuildInfo{\n\t\t\tExperiments: nil,\n\t\t\tGOARCH:      runtime.GOARCH,\n\t\t\tGOOS:        runtime.GOOS,\n\t\t},\n\t\tMainModuleDir: paths.RootedFSPath(dir, \".\"),\n\t\tFS:            fs,\n\t\tParseTests:    false,\n\t\tErrs:          errs,\n\t}\n\n\tdefer func() {\n\t\tif l, ok := perr.CatchBailout(recover()); ok {\n\t\t\terr = errors.Newf(\"parse failure:\\n%s\", l.FormatErrors())\n\t\t} else if errs.Len() > 0 {\n\t\t\terr = errors.Newf(\"parse failure:\\n%s\", errs.FormatErrors())\n\t\t}\n\t}()\n\n\tpp := parser.NewParser(pc)\n\tres := pp.Parse()\n\treturn app.ValidateAndDescribe(pc, res), nil\n}\n"
  },
  {
    "path": "pkg/builder/builder.go",
    "content": "package builder\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\tpathspkg \"path\"\n\t\"runtime\"\n\t\"slices\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/internal/optracker\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nvar LocalBuildTags = []string{\n\t\"encore_local\",\n\t\"encore_no_gcp\", \"encore_no_aws\", \"encore_no_azure\",\n\t\"encore_no_datadog\", \"encore_no_prometheus\",\n}\n\n// DebugMode specifies how to compile the application for debugging.\ntype DebugMode string\n\nconst (\n\tDebugModeDisabled DebugMode = \"disabled\"\n\tDebugModeEnabled  DebugMode = \"enabled\"\n\tDebugModeBreak    DebugMode = \"break\"\n)\n\ntype BuildInfo struct {\n\tBuildTags          []string\n\tCgoEnabled         bool\n\tStaticLink         bool\n\tDebugMode          DebugMode\n\tEnviron            []string\n\tGOOS, GOARCH       string\n\tKeepOutput         bool\n\tRevision           string\n\tUncommittedChanges bool\n\n\t// MainPkg is the path to the existing main package to use, if any.\n\tMainPkg option.Option[paths.Pkg]\n\n\t// Overrides to explicitly set the GoRoot and EncoreRuntime paths.\n\t// if not set, they will be inferred from the current executable.\n\tGoRoot         option.Option[paths.FS]\n\tEncoreRuntimes option.Option[paths.FS]\n\n\t// UseLocalJSRuntime specifies whether to override the installed Encore version\n\t// with the local JS runtime.\n\tUseLocalJSRuntime bool\n\n\t// Logger allows a custom logger to be used by the various phases of the builder.\n\tLogger option.Option[zerolog.Logger]\n\n\t// DisableSensitiveScrubbing, if true, disables scrubbing of sensitive fields.\n\t// Used for local development.\n\tDisableSensitiveScrubbing bool\n}\n\nfunc (b *BuildInfo) IsCrossBuild() bool {\n\treturn b.GOOS != runtime.GOOS || b.GOARCH != runtime.GOARCH\n}\n\n// DefaultBuildInfo returns a BuildInfo with default values.\n// It can be modified afterwards.\nfunc DefaultBuildInfo() BuildInfo {\n\treturn BuildInfo{\n\t\tBuildTags:          slices.Clone(LocalBuildTags),\n\t\tCgoEnabled:         true,\n\t\tStaticLink:         false,\n\t\tDebugMode:          DebugModeDisabled,\n\t\tGOOS:               runtime.GOOS,\n\t\tGOARCH:             runtime.GOARCH,\n\t\tKeepOutput:         false,\n\t\tRevision:           \"\",\n\t\tUncommittedChanges: false,\n\n\t\t// Use the local JS runtime if this is a development build.\n\t\tUseLocalJSRuntime: version.Channel == version.DevBuild,\n\t}\n}\n\ntype PrepareParams struct {\n\tBuild      BuildInfo\n\tApp        *apps.Instance\n\tWorkingDir string\n\tStderr     option.Option[io.Writer]\n}\n\ntype PrepareResult struct {\n\tData any\n}\n\ntype ParseParams struct {\n\tBuild       BuildInfo\n\tApp         *apps.Instance\n\tExperiments *experiments.Set\n\tWorkingDir  string\n\tParseTests  bool\n\n\t// Prepare is the result from calling Prepare().\n\t// Required for TypeScript apps, ignored for Go apps.\n\tPrepare *PrepareResult\n\n\t// Optional writer to redirect stderr to.\n\tStderr option.Option[io.Writer]\n}\n\ntype ParseResult struct {\n\tMeta *meta.Data\n\tData any\n}\n\ntype CompileParams struct {\n\tBuild       BuildInfo\n\tApp         *apps.Instance\n\tParse       *ParseResult\n\tOpTracker   *optracker.OpTracker\n\tExperiments *experiments.Set\n\tWorkingDir  string\n\n\t// Override to explicitly allow the Encore version to be set.\n\tEncoreVersion option.Option[string]\n\n\tEnviron []string\n}\n\ntype ArtifactString string\n\nfunc (a ArtifactString) Join(strs ...string) ArtifactString {\n\tstr := pathspkg.Join(strs...)\n\treturn ArtifactString(pathspkg.Join(string(a), str))\n}\n\nfunc (a ArtifactString) Expand(artifactDir paths.FS) string {\n\treturn os.Expand(string(a), func(key string) string {\n\t\tif key == \"ARTIFACT_DIR\" {\n\t\t\treturn artifactDir.ToIO()\n\t\t}\n\t\treturn \"\"\n\t})\n}\n\ntype ArtifactStrings []ArtifactString\n\nfunc (a ArtifactStrings) Expand(artifactDir paths.FS) []string {\n\treturn fns.Map(a, func(a ArtifactString) string { return a.Expand(artifactDir) })\n}\n\n// CmdSpec is a specification for a command to run.\n//\n// The fields can refer to file paths within the artifact directory\n// using the \"${ARTIFACT_DIR}\" placeholder (substituted with os.ExpandEnv).\n// This is necessary when building docker images, as otherwise the file paths\n// will refer to the wrong filesystem location in the built docker image.\ntype CmdSpec struct {\n\t// The command to execute. Can either be a filesystem path\n\t// or a path to a binary (using \"${ARTIFACT_DIR}\" as a placeholder).\n\tCommand ArtifactStrings `json:\"command\"`\n\n\t// Additional env variables to pass in.\n\tEnv ArtifactStrings `json:\"env\"`\n\n\t// PrioritizedFiles are file paths that should be prioritized when\n\t// building a streamable docker image.\n\tPrioritizedFiles ArtifactStrings `json:\"prioritized_files\"`\n}\n\nfunc (s *CmdSpec) Expand(artifactDir paths.FS) Cmd {\n\treturn Cmd{\n\t\tCommand: s.Command.Expand(artifactDir),\n\t\tEnv:     s.Env.Expand(artifactDir),\n\t}\n}\n\n// Cmd defines a command to run. It's like CmdSpec, but uses expanded paths\n// instead of ArtifactStrings. A CmdSpec can be turned into a Cmd using Expand.\ntype Cmd struct {\n\t// The command to execute, with arguments.\n\tCommand []string\n\n\t// Additional env variables to pass in.\n\tEnv []string\n}\n\ntype CompileResult struct {\n\tOS      string\n\tArch    string\n\tOutputs []BuildOutput\n}\n\ntype BuildOutput interface {\n\tGetArtifactDir() paths.FS\n\tGetEntrypoints() []Entrypoint\n}\n\ntype Entrypoint struct {\n\t// How to run this entrypoint.\n\tCmd CmdSpec `json:\"cmd\"`\n\t// Services hosted by this entrypoint.\n\tServices []string `json:\"services\"`\n\t// Gateways hosted by this entrypoint.\n\tGateways []string `json:\"gateways\"`\n\t// Whether this entrypoint uses the new runtime config.\n\tUseRuntimeConfigV2 bool `json:\"use_runtime_config_v2\"`\n}\n\ntype GoBuildOutput struct {\n\t// The folder containing the build artifacts.\n\t// These artifacts are assumed to be relocatable.\n\tArtifactDir paths.FS `json:\"artifact_dir\"`\n\n\t// The entrypoints that are part of this build output.\n\tEntrypoints []Entrypoint `json:\"entrypoints\"`\n}\n\nfunc (o *GoBuildOutput) GetArtifactDir() paths.FS     { return o.ArtifactDir }\nfunc (o *GoBuildOutput) GetEntrypoints() []Entrypoint { return o.Entrypoints }\n\ntype JSBuildOutput struct {\n\t// The folder containing the build artifacts.\n\t// These artifacts are assumed to be relocatable.\n\tArtifactDir paths.FS `json:\"artifact_dir\"`\n\n\t// The entrypoints that are part of this build output.\n\tEntrypoints []Entrypoint `json:\"entrypoints\"`\n\n\t// Whether the build output uses the local runtime on the builder,\n\t// as opposed to installing a published release via e.g. 'npm install'.\n\tUsesLocalRuntime bool `json:\"uses_local_runtime\"`\n}\n\nfunc (o *JSBuildOutput) GetArtifactDir() paths.FS     { return o.ArtifactDir }\nfunc (o *JSBuildOutput) GetEntrypoints() []Entrypoint { return o.Entrypoints }\n\ntype RunTestsParams struct {\n\tSpec *TestSpecResult\n\n\t// WorkingDir is the directory to invoke the test command from.\n\tWorkingDir paths.FS\n\n\t// Stdout and Stderr are where to redirect the command output.\n\tStdout, Stderr io.Writer\n}\n\ntype TestSpecParams struct {\n\tCompile CompileParams\n\n\t// Env sets environment variables for \"go test\".\n\tEnv []string\n\n\t// Args sets extra arguments for \"go test\".\n\tArgs []string\n}\n\n// ErrNoTests is reported by TestSpec when there aren't any tests to run.\nvar ErrNoTests = errors.New(\"no tests found\")\n\ntype TestSpecResult struct {\n\tCommand string\n\tArgs    []string\n\tEnviron []string\n\n\t// For use by the builder when invoking RunTests.\n\tBuilderData any\n}\n\ntype GenUserFacingParams struct {\n\tBuild BuildInfo\n\tApp   *apps.Instance\n\tParse *ParseResult\n}\n\ntype ServiceConfigsParams struct {\n\tParse   *ParseResult\n\tCueMeta *cueutil.Meta\n}\n\ntype ServiceConfigsResult struct {\n\tConfigs     map[string]string\n\tConfigFiles fs.FS\n}\n\ntype Impl interface {\n\tPrepare(context.Context, PrepareParams) (*PrepareResult, error)\n\tParse(context.Context, ParseParams) (*ParseResult, error)\n\tCompile(context.Context, CompileParams) (*CompileResult, error)\n\tTestSpec(context.Context, TestSpecParams) (*TestSpecResult, error)\n\tRunTests(context.Context, RunTestsParams) error\n\tServiceConfigs(context.Context, ServiceConfigsParams) (*ServiceConfigsResult, error)\n\tGenUserFacing(context.Context, GenUserFacingParams) error\n\tUseNewRuntimeConfig() bool\n\tNeedsMeta() bool\n\tClose() error\n}\n"
  },
  {
    "path": "pkg/builder/builderimpl/builders.go",
    "content": "package builderimpl\n\nimport (\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/v2/tsbuilder\"\n\t\"encr.dev/v2/v2builder\"\n)\n\nfunc Resolve(lang appfile.Lang, expSet *experiments.Set) builder.Impl {\n\tif lang == appfile.LangTS || experiments.TypeScript.Enabled(expSet) {\n\t\treturn tsbuilder.New()\n\t}\n\treturn v2builder.New()\n}\n"
  },
  {
    "path": "pkg/clientgen/client.go",
    "content": "// Package clientgen generates code for use with Encore apps.\npackage clientgen\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/clientgen/openapi\"\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// Lang represents a programming language or dialect that we support generating code for.\ntype Lang string\n\n// These constants represent supported languages.\nconst (\n\tLangUnknown    Lang = \"\"\n\tLangTypeScript Lang = \"typescript\"\n\tLangJavascript Lang = \"javascript\"\n\tLangGo         Lang = \"go\"\n\tLangOpenAPI    Lang = \"openapi\"\n)\n\ntype generator interface {\n\tGenerate(p clientgentypes.GenerateParams) error\n\tVersion() int // The version of the generator.\n}\n\n// ErrUnknownLang is reported by Generate when the language is not known.\nvar ErrUnknownLang = errors.New(\"unknown language\")\n\n// Detect attempts to detect the language from the given filename.\nfunc Detect(path string) (lang Lang, ok bool) {\n\tsuffix := strings.ToLower(filepath.Ext(path))\n\tswitch suffix {\n\tcase \".ts\":\n\t\treturn LangTypeScript, true\n\tcase \".js\":\n\t\treturn LangJavascript, true\n\tcase \".go\":\n\t\treturn LangGo, true\n\tdefault:\n\t\treturn LangUnknown, false\n\t}\n}\n\n// Client generates an API client based on the given app metadata.\n// ServiceNames are the services to include in the output.\n// If it's nil, all services are included.\nfunc Client(\n\tlang Lang,\n\tappSlug string,\n\tmd *meta.Data,\n\tservices clientgentypes.ServiceSet,\n\ttags clientgentypes.TagSet,\n\topts clientgentypes.Options,\n) (code []byte, err error) {\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\terr = srcerrors.UnhandledPanic(e)\n\t\t}\n\t}()\n\n\tvar gen generator\n\tswitch lang {\n\tcase LangTypeScript:\n\t\tif opts.TSSharedTypes && md.Language == meta.Lang_TYPESCRIPT {\n\t\t\tgen = &typescript{generatorVersion: typescriptGenLatestVersion, sharedTypes: true, clientTarget: opts.TSClientTarget}\n\t\t} else {\n\t\t\tgen = &typescript{generatorVersion: typescriptGenLatestVersion, sharedTypes: false}\n\t\t}\n\tcase LangJavascript:\n\t\tgen = &javascript{generatorVersion: javascriptGenLatestVersion}\n\tcase LangGo:\n\t\tgen = &golang{generatorVersion: goGenLatestVersion}\n\tcase LangOpenAPI:\n\t\tgen = openapi.New(openapi.LatestVersion)\n\tdefault:\n\t\treturn nil, ErrUnknownLang\n\t}\n\n\tvar buf bytes.Buffer\n\tparams := clientgentypes.GenerateParams{\n\t\tBuf:      &buf,\n\t\tAppSlug:  appSlug,\n\t\tMeta:     md,\n\t\tServices: services,\n\t\tTags:     tags,\n\t\tOptions:  opts,\n\t}\n\n\tif err := gen.Generate(params); err != nil {\n\t\treturn nil, fmt.Errorf(\"genclient.Generate %s %s: %v\", lang, appSlug, err)\n\t}\n\treturn buf.Bytes(), nil\n}\n\n// getServiceDoc returns the documentation for a service by looking up the\n// root package matching the service's rel_path\nfunc getServiceDoc(md *meta.Data, svc *meta.Service) string {\n\tfor _, pkg := range md.Pkgs {\n\t\tif pkg.RelPath == svc.RelPath {\n\t\t\treturn pkg.Doc\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// GetLang returns the language specified by the given string, allowing for case insensitivity and common aliases.\nfunc GetLang(lang string) (Lang, error) {\n\tswitch strings.TrimSpace(strings.ToLower(lang)) {\n\tcase \"typescript\", \"ts\":\n\t\treturn LangTypeScript, nil\n\tcase \"javascript\", \"js\":\n\t\treturn LangJavascript, nil\n\tcase \"go\", \"golang\":\n\t\treturn LangGo, nil\n\tcase \"openapi\", \"swagger\", \"oas\":\n\t\treturn LangOpenAPI, nil\n\tdefault:\n\t\treturn LangUnknown, ErrUnknownLang\n\t}\n}\n"
  },
  {
    "path": "pkg/clientgen/client_test.go",
    "content": "package clientgen\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/golden\"\n\t\"encr.dev/v2/tsbuilder\"\n\t\"encr.dev/v2/v2builder\"\n)\n\nfunc TestMain(m *testing.M) {\n\tgolden.TestMain(m)\n}\n\nfunc TestClientCodeGenerationFromGoApp(t *testing.T) {\n\tt.Helper()\n\tc := qt.New(t)\n\n\ttests, err := filepath.Glob(\"./testdata/goapp/input*.go\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tc.Assert(err, qt.IsNil)\n\n\tctx := context.Background()\n\tbld := v2builder.New()\n\n\tfor _, path := range tests {\n\t\tpath := path\n\t\tc.Run(\"expected\"+strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), \".go\"), \"input\"), func(c *qt.C) {\n\t\t\tar, err := txtar.ParseFile(path)\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tbase := t.TempDir()\n\t\t\terr = txtar.Write(ar, base)\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tapp := apps.NewInstance(base, \"app\", \"\")\n\t\t\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\t\t\tBuild:      builder.DefaultBuildInfo(),\n\t\t\t\tApp:        app,\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\tres, err := bld.Parse(ctx, builder.ParseParams{\n\t\t\t\tBuild:       builder.DefaultBuildInfo(),\n\t\t\t\tApp:         app,\n\t\t\t\tExperiments: nil,\n\t\t\t\tWorkingDir:  \".\",\n\t\t\t\tParseTests:  false,\n\t\t\t\tPrepare:     prepareResult,\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tfiles, err := os.ReadDir(\"./testdata/goapp\")\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\texpectedPrefix := \"expected\" + strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), \".go\"), \"input\") + \"_\"\n\n\t\t\tfor _, file := range files {\n\t\t\t\ttestName := strings.TrimPrefix(file.Name(), expectedPrefix)\n\n\t\t\t\t// Check that the trim prefix removed the expectedPrefix && there are no other underscores in the testName\n\t\t\t\tif testName != file.Name() && !strings.Contains(testName, \"_\") {\n\t\t\t\t\tc.Run(testName, func(c *qt.C) {\n\t\t\t\t\t\tlanguage, ok := Detect(file.Name())\n\t\t\t\t\t\tif strings.Contains(file.Name(), \"openapi\") {\n\t\t\t\t\t\t\tlanguage, ok = LangOpenAPI, true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tc.Assert(ok, qt.IsTrue, qt.Commentf(\"Unable to detect language type for %s\", file.Name()))\n\n\t\t\t\t\t\tservices := clientgentypes.AllServices(res.Meta)\n\n\t\t\t\t\t\tgeneratedClient, err := Client(\n\t\t\t\t\t\t\tlanguage,\n\t\t\t\t\t\t\t\"app\",\n\t\t\t\t\t\t\tres.Meta,\n\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\tclientgentypes.TagSet{},\n\t\t\t\t\t\t\tclientgentypes.Options{},\n\t\t\t\t\t\t)\n\t\t\t\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\t\t\t\tgolden.TestAgainst(c, \"goapp/\"+file.Name(), string(generatedClient))\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClientCodeGenerationFromTSApp(t *testing.T) {\n\tt.Helper()\n\tc := qt.New(t)\n\n\ttests, err := filepath.Glob(\"./testdata/tsapp/input*.ts\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tc.Assert(err, qt.IsNil)\n\n\tctx := context.Background()\n\tbld := tsbuilder.New()\n\n\tfor _, path := range tests {\n\t\tpath := path\n\t\tc.Run(\"expected\"+strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), \".ts\"), \"input\"), func(c *qt.C) {\n\t\t\tar, err := txtar.ParseFile(path)\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tbase := t.TempDir()\n\t\t\terr = txtar.Write(ar, base)\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tapp := apps.NewInstance(base, \"app\", \"\")\n\t\t\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\t\t\tBuild:      builder.DefaultBuildInfo(),\n\t\t\t\tApp:        app,\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\tres, err := bld.Parse(ctx, builder.ParseParams{\n\t\t\t\tBuild:       builder.DefaultBuildInfo(),\n\t\t\t\tApp:         app,\n\t\t\t\tExperiments: nil,\n\t\t\t\tWorkingDir:  \".\",\n\t\t\t\tParseTests:  false,\n\t\t\t\tPrepare:     prepareResult,\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tfiles, err := os.ReadDir(\"./testdata/tsapp\")\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\texpectedPrefix := \"expected\" + strings.TrimPrefix(strings.TrimSuffix(filepath.Base(path), \".ts\"), \"input\") + \"_\"\n\n\t\t\tfor _, file := range files {\n\t\t\t\ttestName := strings.TrimPrefix(file.Name(), expectedPrefix)\n\n\t\t\t\t// Check that the trim prefix removed the expectedPrefix && there are no other underscores in the testName\n\t\t\t\tif testName != file.Name() && !strings.Contains(testName, \"_\") {\n\t\t\t\t\tc.Run(testName, func(c *qt.C) {\n\t\t\t\t\t\tlanguage, ok := Detect(file.Name())\n\t\t\t\t\t\tif strings.Contains(file.Name(), \"openapi\") {\n\t\t\t\t\t\t\tlanguage, ok = LangOpenAPI, true\n\t\t\t\t\t\t}\n\t\t\t\t\t\toptions := clientgentypes.Options{}\n\t\t\t\t\t\tif strings.Contains(file.Name(), \"shared\") {\n\t\t\t\t\t\t\toptions.TSSharedTypes = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tc.Assert(ok, qt.IsTrue, qt.Commentf(\"Unable to detect language type for %s\", file.Name()))\n\n\t\t\t\t\t\tservices := clientgentypes.AllServices(res.Meta)\n\t\t\t\t\t\tgeneratedClient, err := Client(\n\t\t\t\t\t\t\tlanguage,\n\t\t\t\t\t\t\t\"app\",\n\t\t\t\t\t\t\tres.Meta,\n\t\t\t\t\t\t\tservices,\n\t\t\t\t\t\t\tclientgentypes.TagSet{},\n\t\t\t\t\t\t\toptions,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\t\t\t\tgolden.TestAgainst(c, \"tsapp/\"+file.Name(), string(generatedClient))\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/clientgen/clientgentypes/clientgentypes.go",
    "content": "package clientgentypes\n\nimport (\n\t\"bytes\"\n\t\"slices\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// Options for the client generator.\ntype Options struct {\n\tOpenAPIExcludePrivateEndpoints bool\n\tTSSharedTypes                  bool\n\tTSClientTarget                 string\n}\n\ntype GenerateParams struct {\n\tBuf      *bytes.Buffer\n\tAppSlug  string\n\tMeta     *meta.Data\n\tServices ServiceSet\n\tTags     TagSet\n\tOptions  Options\n}\n\ntype ServiceSet struct {\n\tlist []string\n\tset  map[string]bool\n}\n\nfunc (s ServiceSet) List() []string {\n\treturn s.list\n}\n\nfunc (s ServiceSet) Has(svc string) bool {\n\treturn s.set[svc]\n}\n\n// NewServiceSet constructs a new service set.\n// If the list contains \"*\", include all services in the metadata.\n// Finally, exclude any services in the exclude list.\nfunc NewServiceSet(md *meta.Data, include, exclude []string) ServiceSet {\n\tset := make(map[string]bool, len(include))\n\tif slices.Contains(include, \"*\") {\n\t\t// If the list contains \"*\", include all services.\n\t\tfor _, svc := range md.Svcs {\n\t\t\tset[svc.Name] = true\n\t\t}\n\t} else {\n\t\tfor _, svc := range include {\n\t\t\tset[svc] = true\n\t\t}\n\t}\n\n\t// Remove excludes.\n\tfor _, svc := range exclude {\n\t\tdelete(set, svc)\n\t}\n\n\tlist := make([]string, 0, len(set))\n\tfor svc := range set {\n\t\tlist = append(list, svc)\n\t}\n\tslices.Sort(list)\n\n\treturn ServiceSet{\n\t\tlist: list,\n\t\tset:  set,\n\t}\n}\n\nfunc AllServices(md *meta.Data) ServiceSet {\n\treturn NewServiceSet(md, []string{\"*\"}, nil)\n}\n\ntype TagSet struct {\n\tincluded map[string]bool\n\texcluded map[string]bool\n}\n\nfunc NewTagSet(tags, excludedTags []string) TagSet {\n\ttagSet := TagSet{\n\t\tincluded: make(map[string]bool),\n\t\texcluded: make(map[string]bool),\n\t}\n\n\tfor _, tag := range tags {\n\t\ttagSet.included[tag] = true\n\t}\n\tfor _, tag := range excludedTags {\n\t\ttagSet.excluded[tag] = true\n\t}\n\n\treturn tagSet\n}\n\nfunc (t TagSet) IsRPCIncluded(rpc *meta.RPC) bool {\n\t// First check if the RPC has any of the excluded tags.\n\tfor _, selector := range rpc.Tags {\n\t\tif selector.Type != meta.Selector_TAG {\n\t\t\tcontinue\n\t\t}\n\n\t\tif excluded, ok := t.excluded[selector.Value]; ok && excluded {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// If no included tags are specified, all tags are included.\n\tif len(t.included) == 0 {\n\t\treturn true\n\t}\n\n\t// Check if the RPC has any of the included tags.\n\tfor _, selector := range rpc.Tags {\n\t\tif selector.Type != meta.Selector_TAG {\n\t\t\tcontinue\n\t\t}\n\n\t\tif included, ok := t.included[selector.Value]; ok && included {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// If no included tags are found, the RPC is not included.\n\treturn false\n}\n"
  },
  {
    "path": "pkg/clientgen/errors.go",
    "content": "package clientgen\n\ntype ErrCode struct {\n\tName           string\n\tComment        string\n\tHttpStatusCode int\n}\n\nvar errorCodes []ErrCode = []ErrCode{\n\t{\"OK\", \"OK indicates the operation was successful.\", 200},\n\t{\"Canceled\", \"Canceled indicates the operation was canceled (typically by the caller).\\n\\nEncore will generate this error code when cancellation is requested.\", 499},\n\t{\"Unknown\", \"Unknown error. An example of where this error may be returned is\\nif a Status value received from another address space belongs to\\nan error-space that is not known in this address space. Also\\nerrors raised by APIs that do not return enough error information\\nmay be converted to this error.\\n\\nEncore will generate this error code in the above two mentioned cases.\", 500},\n\t{\"InvalidArgument\", \"InvalidArgument indicates client specified an invalid argument.\\nNote that this differs from FailedPrecondition. It indicates arguments\\nthat are problematic regardless of the state of the system\\n(e.g., a malformed file name).\\n\\nThis error code will not be generated by the gRPC framework.\", 400},\n\t{\"DeadlineExceeded\", \"DeadlineExceeded means operation expired before completion.\\nFor operations that change the state of the system, this error may be\\nreturned even if the operation has completed successfully. For\\nexample, a successful response from a server could have been delayed\\nlong enough for the deadline to expire.\\n\\nThe gRPC framework will generate this error code when the deadline is\\nexceeded.\", 504},\n\t{\"NotFound\", \"NotFound means some requested entity (e.g., file or directory) was\\nnot found.\\n\\nThis error code will not be generated by the gRPC framework.\", 404},\n\t{\"AlreadyExists\", \"AlreadyExists means an attempt to create an entity failed because one\\nalready exists.\\n\\nThis error code will not be generated by the gRPC framework.\", 409},\n\t{\"PermissionDenied\", \"PermissionDenied indicates the caller does not have permission to\\nexecute the specified operation. It must not be used for rejections\\ncaused by exhausting some resource (use ResourceExhausted\\ninstead for those errors). It must not be\\nused if the caller cannot be identified (use Unauthenticated\\ninstead for those errors).\\n\\nThis error code will not be generated by the gRPC core framework,\\nbut expect authentication middleware to use it.\", 403},\n\t{\"ResourceExhausted\", \"ResourceExhausted indicates some resource has been exhausted, perhaps\\na per-user quota, or perhaps the entire file system is out of space.\\n\\nThis error code will be generated by the gRPC framework in\\nout-of-memory and server overload situations, or when a message is\\nlarger than the configured maximum size.\", 429},\n\t{\"FailedPrecondition\", \"FailedPrecondition indicates operation was rejected because the\\nsystem is not in a state required for the operation's execution.\\nFor example, directory to be deleted may be non-empty, an rmdir\\noperation is applied to a non-directory, etc.\\n\\nA litmus test that may help a service implementor in deciding\\nbetween FailedPrecondition, Aborted, and Unavailable:\\n (a) Use Unavailable if the client can retry just the failing call.\\n (b) Use Aborted if the client should retry at a higher-level\\n     (e.g., restarting a read-modify-write sequence).\\n (c) Use FailedPrecondition if the client should not retry until\\n     the system state has been explicitly fixed. E.g., if an \\\"rmdir\\\"\\n     fails because the directory is non-empty, FailedPrecondition\\n     should be returned since the client should not retry unless\\n     they have first fixed up the directory by deleting files from it.\\n (d) Use FailedPrecondition if the client performs conditional\\n     REST Get/Update/Delete on a resource and the resource on the\\n     server does not match the condition. E.g., conflicting\\n     read-modify-write on the same resource.\\n\\nThis error code will not be generated by the gRPC framework.\", 400},\n\t{\"Aborted\", \"Aborted indicates the operation was aborted, typically due to a\\nconcurrency issue like sequencer check failures, transaction aborts,\\netc.\\n\\nSee litmus test above for deciding between FailedPrecondition,\\nAborted, and Unavailable.\", 409},\n\t{\"OutOfRange\", \"OutOfRange means operation was attempted past the valid range.\\nE.g., seeking or reading past end of file.\\n\\nUnlike InvalidArgument, this error indicates a problem that may\\nbe fixed if the system state changes. For example, a 32-bit file\\nmay be rotated to a 64-bit file without error.\\n\\nThere is a fair bit of overlap between FailedPrecondition and\\nOutOfRange. We recommend using OutOfRange (the more specific\\nerror) when it applies so that callers who are iterating through\\na space can easily look for an OutOfRange error to detect when\\nthey are done.\\n\\nThis error code will not be generated by the gRPC framework.\", 400},\n\t{\"Unimplemented\", \"Unimplemented indicates operation is not implemented or not\\nsupported/enabled in this service.\\n\\nThis is not an error, but a feature not available.\\n\\nThis error code will not be generated by the gRPC framework.\", 501},\n\t{\"Internal\", \"Internal means some invariant expected by the underlying system has\\nbeen broken. This is not a per-message error, it is a global\\nconditions check.\\n\\nThis error code will not be generated by the gRPC framework.\", 500},\n\t{\"Unavailable\", \"Unavailable indicates the service is currently unavailable.\\nThis is most likely a transient condition, which can be corrected by\\nretrying with a backoff.\\n\\nSee litmus test above for deciding between FailedPrecondition,\\nAborted, and Unavailable.\", 503},\n\t{\"DataLoss\", \"DataLoss indicates unrecoverable data loss or corruption.\\n\\nThis error code is only defined in the gRPC library, and only for\\nunrecoverable data loss (i.e., data loss resulting from errors\\nlike hard disk corruption or bandwidth exceeded).\\n\\nThis error code will not be generated by the gRPC framework.\", 500},\n\t{\"Unauthenticated\", \"Unauthenticated indicates the request does not have valid\\nauthentication credentials for the operation.\\n\\nThe gRPC framework will generate this error code when the\\nauthentication metadata is invalid or a Credentials callback fails,\\nbut also expect authentication middleware to generate it.\", 401},\n}\n"
  },
  {
    "path": "pkg/clientgen/golang.go",
    "content": "package clientgen\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/cockroachdb/errors\"\n\t. \"github.com/dave/jennifer/jen\"\n\t\"github.com/fatih/structtag\"\n\n\t\"encr.dev/internal/gocodegen\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/parser/encoding\"\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/idents\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n// goGenVersion allows us to introduce breaking changes in the generated code but behind a switch\n// meaning that people with client code reliant on the old behaviour can continue to generate the\n// old code.\ntype goGenVersion int\n\nconst (\n\t// GoInitial is the originally released Go client generator\n\tGoInitial goGenVersion = iota\n\n\t// GoExperimental can be used to lock experimental or uncompleted features in the generated code\n\t// It should always be the last item in the enum\n\tGoExperimental\n)\n\nconst goGenLatestVersion = GoExperimental - 1\n\ntype golang struct {\n\tmd                *meta.Data\n\tenc               *gocodegen.MarshallingCodeGenerator\n\tgeneratorVersion  goGenVersion\n\tskipDocs          bool\n\tskipPkgTypePrefix bool\n\n\tseenSlicePath   bool\n\tseenLiteralNull bool\n}\n\nfunc GenTypes(md *meta.Data, typs ...*schema.Decl) ([]byte, error) {\n\tg := &golang{\n\t\tgeneratorVersion:  goGenLatestVersion,\n\t\tmd:                md,\n\t\tenc:               gocodegen.NewMarshallingCodeGenerator(gocodegen.UnknownPkgPath, \"serde\", true),\n\t\tskipDocs:          true,\n\t\tskipPkgTypePrefix: true,\n\t}\n\trtn := &bytes.Buffer{}\n\tstmt := Add()\n\tg.generateTypeDefinitions(stmt, typs)\n\terr := stmt.Render(rtn)\n\treturn rtn.Bytes(), err\n}\n\nfunc (g *golang) Generate(p clientgentypes.GenerateParams) (err error) {\n\tg.md = p.Meta\n\tg.enc = gocodegen.NewMarshallingCodeGenerator(gocodegen.UnknownPkgPath, \"serde\", true)\n\n\tnamedTypes := getNamedTypes(p.Meta, p.Services)\n\n\t// Create a new client file\n\tfile := NewFile(\"client\")\n\tfile.HeaderComment(doNotEditHeader())\n\n\t// Generate the parent Client struct\n\tg.generateClient(file, p.AppSlug, p.Services)\n\n\t// Generate the types and service client structs\n\tseenNs := make(map[string]bool)\n\tfor _, service := range p.Meta.Svcs {\n\t\tnsName := service.Name\n\t\tg.generateTypeDefinitions(file, namedTypes.Decls(nsName))\n\t\tseenNs[nsName] = true\n\n\t\tif hasPublicRPC(service) && p.Services.Has(service.Name) {\n\t\t\tif err := g.generateServiceClient(file, service, p.Tags); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"unable to generate service client for service: %s\", service)\n\t\t\t}\n\t\t}\n\t}\n\tfor _, ns := range namedTypes.Namespaces() {\n\t\tif !seenNs[ns] {\n\t\t\tg.generateTypeDefinitions(file, namedTypes.Decls(ns))\n\t\t}\n\t}\n\n\t// Generate the base client\n\tif err := g.generateBaseClient(file); err != nil {\n\t\treturn errors.Wrap(err, \"unable to generate base client\")\n\t}\n\n\tg.writeExtraHelpers(file)\n\n\t// Write the APIError type\n\tg.writeErrorType(file)\n\n\t// Generate the serializer\n\tg.enc.WriteToFile(file)\n\n\t// Finally, render the client\n\tif err := file.Render(p.Buf); err != nil {\n\t\treturn errors.Wrap(err, \"unable to generate go client\")\n\t}\n\n\treturn nil\n}\n\nfunc (g *golang) Version() int {\n\treturn int(g.generatorVersion)\n}\n\nfunc (g *golang) cleanServiceName(service *meta.Service) string {\n\treturn strings.Title(strings.ToLower(service.Name))\n}\n\n// generateClient creates the Client struct, Option type and New function\nfunc (g *golang) generateClient(file *File, appSlug string, set clientgentypes.ServiceSet) {\n\t// List all services which have public RPCs or types we need\n\tfieldDef := make([]Code, 0, len(set.List()))\n\tfieldInit := make(Dict)\n\tfor _, service := range g.md.Svcs {\n\t\tif !hasPublicRPC(service) || !set.Has(service.Name) {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := g.cleanServiceName(service)\n\n\t\tfieldDef = append(fieldDef,\n\t\t\tId(name).Id(fmt.Sprintf(\"%sClient\", name)),\n\t\t)\n\n\t\tfieldInit[Id(name)] = Op(\"&\").Id(fmt.Sprintf(\"%sClient\", strings.ToLower(name))).Values(Id(\"base\"))\n\t}\n\n\t// The client struct\n\tfile.Commentf(\"Client is an API client for the %s Encore application.\", appSlug)\n\tfile.Add(\n\t\tType().Id(\"Client\").Struct(fieldDef...),\n\t\tLine(),\n\t\tLine(),\n\t)\n\n\tfile.Comment(\"BaseURL is the base URL for calling the Encore application's API.\")\n\tfile.Type().Id(\"BaseURL\").String()\n\tfile.Line()\n\n\tfile.Const().Id(\"Local\").Id(\"BaseURL\").Op(\"=\").Lit(\"http://localhost:4000\")\n\tfile.Line()\n\n\tfile.Comment(\"Environment returns a BaseURL for calling the cloud environment with the given name.\")\n\tfile.Func().Id(\"Environment\").\n\t\tParams(Id(\"name\").String()).\n\t\tId(\"BaseURL\").\n\t\tBlock(Return(\n\t\t\tId(\"BaseURL\").Call(\n\t\t\t\tQual(\"fmt\", \"Sprintf\").Call(\n\t\t\t\t\tLit(fmt.Sprintf(\"https://%%s-%s.encr.app\", appSlug)),\n\t\t\t\t\tId(\"name\"),\n\t\t\t\t),\n\t\t\t),\n\t\t))\n\n\tfile.Comment(\"PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\")\n\tfile.Func().Id(\"PreviewEnv\").\n\t\tParams(Id(\"pr\").Int()).\n\t\tId(\"BaseURL\").\n\t\tBlock(Return(Id(\"Environment\").Call(\n\t\t\tQual(\"fmt\", \"Sprintf\").Call(Lit(\"pr%d\"), Id(\"pr\")),\n\t\t)))\n\n\t// Option type alias\n\tfile.Comment(\"Option allows you to customise the baseClient used by the Client\")\n\tfile.Add(\n\t\tType().Id(\"Option\").Op(\"=\").\n\t\t\tFunc().Params(Id(\"client\").Op(\"*\").Id(\"baseClient\")).Error(),\n\t\tLine(),\n\t\tLine(),\n\t)\n\n\t// New Function\n\tfile.Comment(\"New returns a Client for calling the public and authenticated APIs of your Encore application.\")\n\tfile.Comment(\"You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\")\n\tfile.Add(\n\t\tFunc().Id(\"New\").\n\t\t\tParams(\n\t\t\t\tId(\"target\").Id(\"BaseURL\"),\n\t\t\t\tId(\"options\").Id(\"...Option\"),\n\t\t\t).\n\t\t\tParams(Op(\"*\").Id(\"Client\"), Error()).\n\t\t\tBlock(\n\t\t\t\tComment(\"Parse the base URL where the Encore application is being hosted\"),\n\t\t\t\tList(Id(\"baseURL\"), Err()).\n\t\t\t\t\tOp(\":=\").\n\t\t\t\t\tQual(\"net/url\", \"Parse\").Call(String().Call(Id(\"target\"))),\n\t\t\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\t\tReturn(\n\t\t\t\t\t\tNil(),\n\t\t\t\t\t\tQual(\"fmt\", \"Errorf\").Call(Lit(\"unable to parse base url: %w\"), Err()),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tLine(),\n\n\t\t\t\tComment(\"Create a client with sensible defaults\"),\n\t\t\t\tId(\"base\").Op(\":=\").Op(\"&\").Id(\"baseClient\").Values(Dict{\n\t\t\t\t\tId(\"baseURL\"):    Id(\"baseURL\"),\n\t\t\t\t\tId(\"httpClient\"): Qual(\"net/http\", \"DefaultClient\"),\n\t\t\t\t\tId(\"userAgent\"):  Lit(fmt.Sprintf(\"%s-Generated-Go-Client (Encore/%s)\", appSlug, version.Version)),\n\t\t\t\t}),\n\t\t\t\tLine(),\n\n\t\t\t\tComment(\"Apply any given options\"),\n\t\t\t\tFor(List(Id(\"_\"), Id(\"option\")).Op(\":=\").Range().Id(\"options\")).Block(\n\t\t\t\t\tIf(\n\t\t\t\t\t\tId(\"err\").Op(\":=\").Id(\"option\").Call(Id(\"base\")),\n\t\t\t\t\t\tId(\"err\").Op(\"!=\").Nil(),\n\t\t\t\t\t).Block(\n\t\t\t\t\t\tReturn(\n\t\t\t\t\t\t\tNil(),\n\t\t\t\t\t\t\tQual(\"fmt\", \"Errorf\").Call(\n\t\t\t\t\t\t\t\tLit(\"unable to apply client option: %w\"),\n\t\t\t\t\t\t\t\tId(\"err\"),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tLine(),\n\n\t\t\t\tReturn(Op(\"&\").Id(\"Client\").Values(fieldInit), Nil()),\n\t\t\t),\n\t)\n\n\t// Generate the WithHttpClient function\n\tg.generateOptionFunc(\n\t\tfile,\n\t\t\"HTTPClient\",\n\t\t`can be used to configure the underlying HTTP client used when making API calls.\n\nDefaults to http.DefaultClient`,\n\t\t&Statement{Id(\"client\").Id(\"HTTPDoer\")},\n\t\t&Statement{\n\t\t\tId(\"base\").Dot(\"httpClient\").Op(\"=\").Id(\"client\"),\n\t\t\tReturn(Nil()),\n\t\t},\n\t)\n\n\tif g.md.AuthHandler != nil {\n\t\ttyp := g.getType(g.md.AuthHandler.Params)\n\t\trawType := typ\n\t\tfuncName := \"AuthToken\"\n\t\tparamName := \"bearerToken\"\n\t\tpointer := Id(paramName)\n\t\tcomment := \"an authentication token to be used for each request.\\n\\nThis token will be sent as a Bearer token in the Authorization header.\"\n\t\tif g.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING {\n\t\t\tfuncName = \"Auth\"\n\t\t\tparamName = \"auth\"\n\t\t\tpointer = Id(\"auth\")\n\t\t\ttyp = rawType\n\t\t\tcomment = \"the authentication data to be used with each request\"\n\t\t}\n\n\t\t// Generate the WithAuth function\n\t\tg.generateOptionFunc(\n\t\t\tfile,\n\t\t\tfuncName,\n\t\t\t\"allows you to set \"+comment,\n\t\t\t// Note we take the auth data by value rather than a pointer to ensure it's safe\n\t\t\t// to use inside multiple goroutines\n\t\t\t&Statement{Id(paramName).Add(rawType)},\n\t\t\t&Statement{\n\t\t\t\tId(\"base\").Dot(\"authGenerator\").Op(\"=\").Func().\n\t\t\t\t\tParams(Id(\"_\").Qual(\"context\", \"Context\")).\n\t\t\t\t\tParams(typ, Error()).\n\t\t\t\t\tBlock(Return(pointer, Nil())),\n\t\t\t\tReturn(Nil()),\n\t\t\t},\n\t\t)\n\n\t\tg.generateOptionFunc(\n\t\t\tfile,\n\t\t\t\"AuthFunc\",\n\t\t\t\"allows you to pass a function which is called for each request to return \"+comment,\n\t\t\t&Statement{\n\t\t\t\tId(\"authGenerator\").Func().\n\t\t\t\t\tParams(Id(\"ctx\").Qual(\"context\", \"Context\")).\n\t\t\t\t\tParams(typ, Error()),\n\t\t\t},\n\t\t\t&Statement{\n\t\t\t\t&Statement{Id(\"base\").Dot(\"authGenerator\").Op(\"=\").Id(\"authGenerator\")},\n\t\t\t\tReturn(Nil()),\n\t\t\t},\n\t\t)\n\t}\n}\n\n// generateOptionFunc is a helper for reducing the boilerplate we have when creating the option functions\nfunc (g *golang) generateOptionFunc(file *File, optionName string, doc string, params *Statement, block *Statement) {\n\tfor i, line := range strings.Split(doc, \"\\n\") {\n\t\tif i == 0 {\n\t\t\tfile.Commentf(\"With%s %s\", optionName, line)\n\t\t} else {\n\t\t\tfile.Comment(line)\n\t\t}\n\t}\n\n\tfile.Func().\n\t\tId(fmt.Sprintf(\"With%s\", optionName)).\n\t\tParams(*params...).\n\t\tId(\"Option\").\n\t\tBlock(\n\t\t\tReturn(Func().Params(Id(\"base\").Op(\"*\").Id(\"baseClient\")).Error().Block(\n\t\t\t\t*block...,\n\t\t\t)),\n\t\t)\n}\n\nfunc (g *golang) generateServiceClient(file *File, service *meta.Service, tags clientgentypes.TagSet) error {\n\tname := g.cleanServiceName(service)\n\tinterfaceName := fmt.Sprintf(\"%sClient\", name)\n\tstructName := fmt.Sprintf(\"%sClient\", strings.ToLower(name))\n\n\t// The interface\n\tif doc := getServiceDoc(g.md, service); doc != \"\" && !g.skipDocs {\n\t\tfor _, line := range strings.Split(strings.TrimSpace(doc), \"\\n\") {\n\t\t\tfile.Comment(line)\n\t\t}\n\t\tfile.Comment(\"\")\n\t}\n\tfile.Commentf(\"%s Provides you access to call public and authenticated APIs on %s. The concrete implementation is %s.\", interfaceName, service.Name, structName)\n\tfile.Comment(\"It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\")\n\tvar interfaceMethods []Code\n\tfor _, rpc := range service.Rpcs {\n\t\tif rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// streaming endpoints not supported yet\n\t\tif rpc.StreamingRequest || rpc.StreamingResponse {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Add the documentation for the API to the interface method\n\t\tif rpc.Doc != nil && !g.skipDocs {\n\t\t\t// Add a newline if this is not the first method\n\t\t\tif len(interfaceMethods) > 0 {\n\t\t\t\tinterfaceMethods = append(interfaceMethods, Line())\n\t\t\t}\n\n\t\t\tfor _, line := range strings.Split(strings.TrimSpace(*rpc.Doc), \"\\n\") {\n\t\t\t\tinterfaceMethods = append(interfaceMethods, Comment(line))\n\t\t\t}\n\t\t}\n\n\t\tinterfaceMethods = append(interfaceMethods,\n\t\t\tId(idents.Convert(rpc.Name, idents.PascalCase)).Add(g.rpcParams(rpc)).Add(g.rpcReturnType(rpc, false)),\n\t\t)\n\t}\n\tfile.Type().Id(interfaceName).Interface(interfaceMethods...)\n\tfile.Line()\n\n\t// The struct\n\tfile.Type().Id(structName).Struct(\n\t\tId(\"base\").Op(\"*\").Id(\"baseClient\"),\n\t)\n\tfile.Line()\n\tfile.Var().Id(\"_\").Id(interfaceName).Op(\"=\").Params(Op(\"*\").Id(structName)).Params(Nil())\n\n\t// The API functions\n\tfor _, rpc := range service.Rpcs {\n\t\tif rpc.AccessType == meta.RPC_PRIVATE {\n\t\t\tcontinue\n\t\t}\n\n\t\t// streaming endpoints not supported yet\n\t\tif rpc.StreamingRequest || rpc.StreamingResponse {\n\t\t\tcontinue\n\t\t}\n\n\t\tif rpc.Doc != nil && *rpc.Doc != \"\" && !g.skipDocs {\n\t\t\tfor _, line := range strings.Split(strings.TrimSpace(*rpc.Doc), \"\\n\") {\n\t\t\t\tif line != \"\" {\n\t\t\t\t\tfile.Comment(line)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tcallSite, err := g.rpcCallSite(rpc)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"rpc: %s\", rpc.Name)\n\t\t}\n\n\t\tfile.Func().\n\t\t\tParams(Id(\"c\").Op(\"*\").Id(structName)).\n\t\t\tId(idents.Convert(rpc.Name, idents.PascalCase)).\n\t\t\tAdd(\n\t\t\t\tg.rpcParams(rpc),\n\t\t\t\tg.rpcReturnType(rpc, true),\n\t\t\t).Block(callSite...)\n\t\tfile.Line()\n\t}\n\treturn nil\n}\n\nfunc (g *golang) rpcParams(rpc *meta.RPC) Code {\n\tparams := []Code{\n\t\tId(\"ctx\").Qual(\"context\", \"Context\"),\n\t}\n\n\tif rpc.Path != nil && len(rpc.Path.Segments) > 0 {\n\t\tfor _, segment := range rpc.Path.Segments {\n\t\t\tif segment.Type == meta.PathSegment_LITERAL {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// We'll default to strings for most things\n\t\t\ttyp := String()\n\n\t\t\tswitch segment.ValueType {\n\t\t\tcase meta.PathSegment_BOOL:\n\t\t\t\ttyp = Bool()\n\n\t\t\tcase meta.PathSegment_INT8:\n\t\t\t\ttyp = Int8()\n\n\t\t\tcase meta.PathSegment_INT16:\n\t\t\t\ttyp = Int16()\n\n\t\t\tcase meta.PathSegment_INT32:\n\t\t\t\ttyp = Int32()\n\n\t\t\tcase meta.PathSegment_INT64:\n\t\t\t\ttyp = Int64()\n\n\t\t\tcase meta.PathSegment_INT:\n\t\t\t\ttyp = Int()\n\n\t\t\tcase meta.PathSegment_UINT8:\n\t\t\t\ttyp = Uint8()\n\n\t\t\tcase meta.PathSegment_UINT16:\n\t\t\t\ttyp = Uint16()\n\n\t\t\tcase meta.PathSegment_UINT32:\n\t\t\t\ttyp = Uint32()\n\n\t\t\tcase meta.PathSegment_UINT64:\n\t\t\t\ttyp = Uint64()\n\n\t\t\tcase meta.PathSegment_UINT:\n\t\t\t\ttyp = Uint()\n\t\t\t}\n\n\t\t\tif segment.Type == meta.PathSegment_WILDCARD || segment.Type == meta.PathSegment_FALLBACK {\n\t\t\t\ttyp = Index().Add(typ)\n\t\t\t}\n\n\t\t\tparams = append(params,\n\t\t\t\tId(g.nonReservedId(segment.Value)).Add(typ),\n\t\t\t)\n\t\t}\n\t}\n\n\tif rpc.Proto == meta.RPC_RAW {\n\t\tparams = append(params, Id(\"request\").Op(\"*\").Qual(\"net/http\", \"Request\"))\n\t} else {\n\t\tif rpc.RequestSchema != nil {\n\t\t\tparams = append(params, Id(\"params\").Add(g.getType(rpc.RequestSchema)))\n\t\t}\n\t}\n\n\treturn Params(params...)\n}\n\n// nonReservedId returns the given ID, unless we have it a reserved within the client function _or_ it's a reserved Go keyword\nfunc (g *golang) nonReservedId(id string) string {\n\tswitch id {\n\t// our reserved keywords (or ID's we use within the generated client functions)\n\tcase \"c\", \"ctx\", \"request\", \"resp\", \"err\",\n\t\t\"reqEncoder\", \"headers\", \"queryString\", \"body\", \"respBody\", \"respHeaders\", \"respDecoder\":\n\t\treturn \"_\" + id\n\n\t// Go keywords\n\tcase \"break\", \"default\", \"func\", \"interface\", \"select\", \"case\", \"defer\", \"go\", \"map\", \"struct\", \"chan\", \"else\",\n\t\t\"goto\", \"package\", \"switch\", \"const\", \"fallthrough\", \"if\", \"range\", \"type\", \"continue\", \"for\", \"import\",\n\t\t\"return\", \"var\":\n\t\treturn \"_\" + id\n\n\t// Go predeclared identifiers\n\tcase \"append\", \"bool\", \"byte\", \"cap\", \"close\", \"complex\", \"complex64\", \"complex128\", \"uint16\", \"copy\", \"false\",\n\t\t\"float32\", \"float64\", \"imag\", \"int\", \"int8\", \"int16\", \"uint32\", \"int32\", \"int64\", \"iota\", \"len\", \"make\", \"new\",\n\t\t\"nil\", \"panic\", \"uint64\", \"print\", \"println\", \"real\", \"recover\", \"string\", \"true\", \"uint\", \"uint8\", \"uintptr\":\n\t\treturn \"_\" + id\n\n\tdefault:\n\t\treturn id\n\t}\n}\n\nfunc (g *golang) rpcReturnType(rpc *meta.RPC, concreteImpl bool) Code {\n\tif rpc.Proto == meta.RPC_RAW {\n\t\treturn Params(Op(\"*\").Qual(\"net/http\", \"Response\"), Error())\n\t}\n\n\tif rpc.ResponseSchema == nil {\n\t\treturn Error()\n\t}\n\n\tif concreteImpl {\n\t\t// For the concrete implementation we want the response type to be named so we can\n\t\t// refer to it without having to define a variable.\n\t\treturn Params(Id(\"resp\").Add(g.getType(rpc.ResponseSchema)), Err().Error())\n\t} else {\n\t\treturn Params(g.getType(rpc.ResponseSchema), Error())\n\t}\n}\n\nfunc (g *golang) rpcCallSite(rpc *meta.RPC) (code []Code, err error) {\n\t// Work out how we're going to encode and call this RPC\n\trpcEncoding, err := encoding.DescribeRPC(g.md, rpc, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"rpc %s\", rpc.Name)\n\t}\n\n\t// Raw end points just pass through the request\n\t// and need no further code generation\n\tif rpc.Proto == meta.RPC_RAW {\n\t\tcode = append(\n\t\t\tcode,\n\t\t\tId(\"request\").Op(\"=\").Id(\"request\").Dot(\"WithContext\").Call(Id(\"ctx\")),\n\t\t\tLine(),\n\n\t\t\tComment(\"Check the request has the method set, as we can't guess what method is required\"),\n\t\t\tIf(Id(\"request\").Dot(\"Method\").Op(\"==\").Lit(\"\")).Block(\n\t\t\t\tReturn(\n\t\t\t\t\tNil(),\n\t\t\t\t\tQual(\"errors\", \"New\").Call(Lit(\"request.Method must be set\")),\n\t\t\t\t),\n\t\t\t),\n\t\t\tLine(),\n\n\t\t\tComment(\"Set the relative URL for the API call\"),\n\t\t\tList(Id(\"path\"), Err()).Op(\":=\").Qual(\"net/url\", \"Parse\").Call(g.createApiPath(rpc, false)),\n\t\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\tReturn(\n\t\t\t\t\tNil(),\n\t\t\t\t\tQual(\"fmt\", \"Errorf\").Call(Lit(\"unable to parse api url: %w\"), Err()),\n\t\t\t\t),\n\t\t\t),\n\t\t\tIf(Id(\"request\").Dot(\"URL\").Op(\"!=\").Nil()).Block(\n\t\t\t\tComment(\"If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema, \"),\n\t\t\t\tComment(\"host and path to ensure the final URL which hit the right BaseURL\"),\n\t\t\t\tId(\"request\").Dot(\"URL\").Dot(\"Scheme\").Op(\"=\").Id(\"path\").Dot(\"Scheme\"),\n\t\t\t\tId(\"request\").Dot(\"URL\").Dot(\"Host\").Op(\"=\").Id(\"path\").Dot(\"Host\"),\n\t\t\t\tId(\"request\").Dot(\"URL\").Dot(\"Path\").Op(\"=\").Id(\"path\").Dot(\"Path\"),\n\t\t\t).Else().Block(\n\t\t\t\tId(\"request\").Dot(\"URL\").Op(\"=\").Id(\"path\"),\n\t\t\t),\n\t\t\tLine(),\n\n\t\t\tLine(),\n\t\t\tReturn(Id(\"c\").Dot(\"base\").Dot(\"Do\").Call(Id(\"request\"))),\n\t\t)\n\n\t\treturn\n\t}\n\n\theaders := Nil()\n\tbody := Nil()\n\twithQueryString := false\n\n\t// Work out how we encode the Request Schema\n\tif rpc.RequestSchema != nil {\n\t\treqEnc := rpcEncoding.DefaultRequestEncoding\n\n\t\tif len(reqEnc.HeaderParameters) > 0 || len(reqEnc.QueryParameters) > 0 {\n\t\t\tcode = append(code, Comment(\"Convert our params into the objects we need for the request\"))\n\t\t}\n\n\t\tenc := g.enc.NewPossibleInstance(\"reqEncoder\")\n\n\t\t// Generate the headers\n\t\tif len(reqEnc.HeaderParameters) > 0 {\n\t\t\tvalues := Dict{}\n\n\t\t\tfor _, field := range reqEnc.HeaderParameters {\n\t\t\t\tslice, err := enc.ToStringSlice(\n\t\t\t\t\tfield.Type,\n\t\t\t\t\tId(\"params\").Dot(field.SrcName),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.Wrapf(err, \"unable to encode header %s\", field.SrcName)\n\t\t\t\t}\n\t\t\t\tvalues[Lit(field.WireFormat)] = slice\n\t\t\t}\n\n\t\t\theaders = Id(\"headers\")\n\t\t\tenc.Add(Id(\"headers\").Op(\":=\").Qual(\"net/http\", \"Header\").Values(values), Line())\n\t\t}\n\n\t\t// Generate the query string\n\t\tif len(reqEnc.QueryParameters) > 0 {\n\t\t\twithQueryString = true\n\t\t\tvalues := Dict{}\n\n\t\t\t// Check the request schema for fields we can put in the query string\n\t\t\tfor _, field := range reqEnc.QueryParameters {\n\t\t\t\tslice, err := enc.ToStringSlice(\n\t\t\t\t\tfield.Type,\n\t\t\t\t\tId(\"params\").Dot(field.SrcName),\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.Wrapf(err, \"unable to encode query fields %s\", field.SrcName)\n\t\t\t\t}\n\n\t\t\t\tvalues[Lit(field.WireFormat)] = slice\n\t\t\t}\n\n\t\t\tenc.Add(Id(\"queryString\").Op(\":=\").Qual(\"net/url\", \"Values\").Values(values), Line())\n\t\t}\n\n\t\tif rpc.ResponseSchema != nil {\n\t\t\tcode = append(code, enc.Finalize(\n\t\t\t\tId(\"err\").Op(\"=\").Qual(\"fmt\", \"Errorf\").Call(\n\t\t\t\t\tLit(\"unable to marshal parameters: %w\"),\n\t\t\t\t\tenc.LastError(),\n\t\t\t\t),\n\t\t\t\tReturn(),\n\t\t\t)...)\n\t\t} else {\n\t\t\tcode = append(code, enc.Finalize(\n\t\t\t\tReturn(Qual(\"fmt\", \"Errorf\").Call(\n\t\t\t\t\tLit(\"unable to marshal parameters: %w\"),\n\t\t\t\t\tenc.LastError(),\n\t\t\t\t)),\n\t\t\t)...)\n\t\t}\n\n\t\t// Generate the body\n\t\tif len(reqEnc.BodyParameters) > 0 {\n\t\t\tif len(reqEnc.HeaderParameters) == 0 && len(reqEnc.QueryParameters) == 0 {\n\t\t\t\t// In the simple case we can just encode the params as the body directly\n\t\t\t\tbody = Id(\"params\")\n\t\t\t} else {\n\t\t\t\t// Else we need a new struct called \"body\"\n\t\t\t\tbody = Id(\"body\")\n\n\t\t\t\ttypes, err := g.generateAnonStructTypes(reqEnc.BodyParameters, \"json\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tvalues := Dict{}\n\t\t\t\tfor _, field := range reqEnc.BodyParameters {\n\t\t\t\t\tvalues[Id(field.SrcName)] = Id(\"params\").Dot(field.SrcName)\n\t\t\t\t}\n\n\t\t\t\tcode = append(code,\n\t\t\t\t\tComment(\"Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\"),\n\t\t\t\t\tId(\"body\").Op(\":=\").Struct(types...).Values(values),\n\t\t\t\t\tLine(),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Make the request\n\tresp := Nil()\n\tapiCallCode := func() Code {\n\t\treturn Id(\"callAPI\").Call(\n\t\t\tId(\"ctx\"),\n\t\t\tId(\"c\").Dot(\"base\"),\n\t\t\tLit(rpcEncoding.DefaultMethod),\n\t\t\tg.createApiPath(rpc, withQueryString),\n\t\t\theaders,\n\t\t\tbody,\n\t\t\tresp,\n\t\t)\n\t}\n\n\t// If there's no response schema, we can just return the call to the API directly\n\tif rpc.ResponseSchema == nil {\n\t\tcode = append(code,\n\t\t\tList(Id(\"_\"), Err()).Op(\":=\").Add(apiCallCode()),\n\t\t\tReturn(Err()),\n\t\t)\n\t\treturn\n\t}\n\n\thasAnonResponseStruct := false\n\trespEnc := rpcEncoding.ResponseEncoding\n\n\t// If we have a response object, we need\n\tif len(respEnc.BodyParameters) > 0 {\n\t\tif len(respEnc.HeaderParameters) == 0 {\n\t\t\t// If there are no other fields, we can just take the return type and pass it straight through\n\t\t\tresp = Op(\"&\").Id(\"resp\")\n\t\t} else {\n\t\t\thasAnonResponseStruct = true\n\t\t\tresp = Op(\"&\").Id(\"respBody\")\n\n\t\t\t// we need to construct an anonymous struct\n\t\t\ttypes, err := g.generateAnonStructTypes(respEnc.BodyParameters, \"json\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"response unmarshal\")\n\t\t\t}\n\n\t\t\tcode = append(code,\n\t\t\t\tComment(\"We only want the response body to marshal into these fields and none of the header fields,\"),\n\t\t\t\tComment(\"so we'll construct a new struct with only those fields.\"),\n\t\t\t\tId(\"respBody\").Op(\":=\").Struct(types...).Values(),\n\t\t\t\tLine(),\n\t\t\t)\n\t\t}\n\t}\n\n\t// The API Call itself\n\tcode = append(code, Comment(\"Now make the actual call to the API\"))\n\n\theadersId := \"_\"\n\tif len(respEnc.HeaderParameters) > 0 {\n\t\theadersId = \"respHeaders\"\n\t\tcode = append(code, Var().Id(headersId).Qual(\"net/http\", \"Header\"))\n\t}\n\n\tcode = append(code,\n\t\tList(Id(headersId), Err()).Op(\"=\").Add(apiCallCode()),\n\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\tReturn(),\n\t\t),\n\t\tLine(),\n\t)\n\n\t// In we have an anonymous response struct, we need to copy the results into the full response struct\n\tif hasAnonResponseStruct || len(respEnc.HeaderParameters) > 0 {\n\t\tcode = append(code, Comment(\"Copy the unmarshalled response body into our response struct\"))\n\n\t\tenc := g.enc.NewPossibleInstance(\"respDecoder\")\n\t\tfor _, field := range respEnc.HeaderParameters {\n\t\t\tstr, err := enc.FromString(\n\t\t\t\tfield.Type,\n\t\t\t\tfield.SrcName,\n\t\t\t\tId(headersId).Dot(\"Get\").Call(Lit(field.WireFormat)),\n\t\t\t\tId(headersId).Dot(\"Values\").Call(Lit(field.WireFormat)),\n\t\t\t\ttrue,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrapf(err, \"unable to convert %s to string in response header\", field.SrcName)\n\t\t\t}\n\n\t\t\tenc.Add(Id(\"resp\").Dot(field.SrcName).Op(\"=\").Add(str))\n\t\t}\n\t\tfor _, field := range respEnc.BodyParameters {\n\t\t\tenc.Add(Id(\"resp\").Dot(field.SrcName).Op(\"=\").Id(\"respBody\").Dot(field.SrcName))\n\t\t}\n\t\tcode = append(code, enc.Finalize(\n\t\t\tId(\"err\").Op(\"=\").Qual(\"fmt\", \"Errorf\").Call(\n\t\t\t\tLit(\"unable to unmarshal headers: %w\"),\n\t\t\t\tenc.LastError(),\n\t\t\t),\n\t\t\tReturn(),\n\t\t)...)\n\t\tcode = append(code, Line())\n\t}\n\n\tcode = append(code, Return())\n\n\treturn code, err\n}\n\n// goIdentifier converts a string into a valid Go identifier.\nfunc goIdentifier(input string) string {\n\tif input == \"\" {\n\t\treturn \"_\"\n\t}\n\n\t// Convert string to rune slice for proper handling of Unicode characters\n\trunes := []rune(input)\n\n\t// Ensure the first character is a valid Go identifier start (letter or `_`)\n\tif !unicode.IsLetter(runes[0]) && runes[0] != '_' {\n\t\trunes[0] = '_'\n\t}\n\n\t// Regex to replace invalid characters (anything that isn't a letter, number, or `_`)\n\tinvalidChars := regexp.MustCompile(`[^\\p{L}\\p{N}_]`)\n\toutput := invalidChars.ReplaceAllString(string(runes), \"_\")\n\n\treturn output\n}\n\nfunc (g *golang) declToID(decl *schema.Decl) *Statement {\n\tif g.skipPkgTypePrefix {\n\t\treturn Id(goIdentifier(strings.Title(decl.Name)))\n\t} else {\n\t\treturn Id(goIdentifier(fmt.Sprintf(\"%s%s\", strings.Title(decl.Loc.PkgName), strings.Title(decl.Name))))\n\t}\n}\n\nfunc (g *golang) getType(typ *schema.Type) Code {\n\tswitch typ := typ.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\tdecl := g.md.Decls[typ.Named.Id]\n\n\t\tnamed := g.declToID(decl)\n\n\t\tif len(typ.Named.TypeArguments) == 0 {\n\t\t\treturn named\n\t\t}\n\n\t\t// Add the type arguments\n\t\ttypes := make([]Code, len(typ.Named.TypeArguments))\n\t\tfor i, t := range typ.Named.TypeArguments {\n\t\t\ttypes[i] = g.getType(t)\n\t\t}\n\n\t\treturn named.Types(types...)\n\n\tcase *schema.Type_List:\n\t\treturn Index().Add(g.getType(typ.List.Elem))\n\n\tcase *schema.Type_Map:\n\t\treturn Map(g.getType(typ.Map.Key)).Add(g.getType(typ.Map.Value))\n\n\tcase *schema.Type_Literal:\n\t\tswitch typ.Literal.Value.(type) {\n\t\tcase *schema.Literal_Str:\n\t\t\treturn String()\n\t\tcase *schema.Literal_Boolean:\n\t\t\treturn Bool()\n\t\tcase *schema.Literal_Int:\n\t\t\treturn Int()\n\t\tcase *schema.Literal_Float:\n\t\t\treturn Float64()\n\t\tcase *schema.Literal_Null:\n\t\t\t// Can't use nil as a type, so use *bool\n\t\t\tg.seenLiteralNull = true\n\t\t\treturn Id(\"null\")\n\t\tdefault:\n\t\t\treturn Any()\n\t\t}\n\n\tcase *schema.Type_Union:\n\t\t// There's no good way of representing unions in Go.\n\t\t// Use 'any' for now.\n\t\treturn Any()\n\n\tcase *schema.Type_Builtin:\n\t\tswitch typ.Builtin {\n\t\tcase schema.Builtin_ANY:\n\t\t\treturn Any()\n\t\tcase schema.Builtin_BOOL:\n\t\t\treturn Bool()\n\t\tcase schema.Builtin_INT:\n\t\t\treturn Int()\n\t\tcase schema.Builtin_INT8:\n\t\t\treturn Int8()\n\t\tcase schema.Builtin_INT16:\n\t\t\treturn Int16()\n\t\tcase schema.Builtin_INT32:\n\t\t\treturn Int32()\n\t\tcase schema.Builtin_INT64:\n\t\t\treturn Int64()\n\t\tcase schema.Builtin_UINT:\n\t\t\treturn Uint()\n\t\tcase schema.Builtin_UINT8:\n\t\t\treturn Uint8()\n\t\tcase schema.Builtin_UINT16:\n\t\t\treturn Uint16()\n\t\tcase schema.Builtin_UINT32:\n\t\t\treturn Uint32()\n\t\tcase schema.Builtin_UINT64:\n\t\t\treturn Uint64()\n\t\tcase schema.Builtin_FLOAT32:\n\t\t\treturn Float32()\n\t\tcase schema.Builtin_FLOAT64:\n\t\t\treturn Float64()\n\t\tcase schema.Builtin_STRING:\n\t\t\treturn String()\n\t\tcase schema.Builtin_BYTES:\n\t\t\treturn Index().Byte()\n\t\tcase schema.Builtin_TIME:\n\t\t\treturn Qual(\"time\", \"Time\")\n\t\tcase schema.Builtin_JSON:\n\t\t\treturn Qual(\"encoding/json\", \"RawMessage\")\n\t\tcase schema.Builtin_UUID, schema.Builtin_USER_ID, schema.Builtin_DECIMAL:\n\t\t\t// we don't want to add any custom deps, so these come in as strings\n\t\t\treturn String()\n\t\tdefault:\n\t\t\treturn Any()\n\t\t}\n\n\tcase *schema.Type_Pointer:\n\t\treturn Op(\"*\").Add(g.getType(typ.Pointer.Base))\n\n\tcase *schema.Type_Option:\n\t\t// Avoid the dependency by using a pointer type instead of encore.dev/types/option.Option.\n\t\tvalue := typ.Option.Value\n\n\t\t// Avoid double pointer\n\t\tfor ptr := value.GetPointer(); ptr != nil; ptr = value.GetPointer() {\n\t\t\tvalue = ptr.Base\n\t\t}\n\n\t\treturn Op(\"*\").Add(g.getType(value))\n\n\tcase *schema.Type_Struct:\n\t\tfields := make([]Code, 0, len(typ.Struct.Fields))\n\n\t\tfor _, field := range typ.Struct.Fields {\n\t\t\t// Skip over hidden fields\n\t\t\tif encoding.IgnoreField(field) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// The base field name and type\n\t\t\tfieldTyp := Id(idents.Convert(field.Name, idents.PascalCase)).Add(g.getType(field.Typ))\n\n\t\t\t// Add the field tags\n\t\t\tif field.RawTag != \"\" {\n\t\t\t\ttags, err := structtag.Parse(field.RawTag)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(\"raw tags failed to parse\") // This shouldn't happen at runtime, because the parser should have caught this\n\t\t\t\t}\n\n\t\t\t\ttagsForJen := make(map[string]string)\n\t\t\t\tfor _, tag := range tags.Tags() {\n\t\t\t\t\ttagsForJen[tag.Key] = tag.Value()\n\t\t\t\t}\n\n\t\t\t\tfieldTyp = fieldTyp.Tag(tagsForJen)\n\t\t\t}\n\n\t\t\t// Add the docs for the field\n\t\t\tif field.Doc != \"\" && !g.skipDocs {\n\t\t\t\tlines := strings.Split(strings.TrimSpace(field.Doc), \"\\n\")\n\n\t\t\t\tif len(lines) == 1 {\n\t\t\t\t\tfieldTyp = fieldTyp.Comment(lines[0])\n\t\t\t\t} else {\n\t\t\t\t\tfields = append(fields, Line())\n\t\t\t\t\tfor _, line := range lines {\n\t\t\t\t\t\tfields = append(fields, Comment(line))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Finally, add the field to the list of fields on the struct\n\t\t\tfields = append(fields, fieldTyp)\n\t\t}\n\n\t\treturn Struct(fields...)\n\n\tcase *schema.Type_TypeParameter:\n\t\tdecl := g.md.Decls[typ.TypeParameter.DeclId]\n\t\ttypeParam := decl.TypeParams[typ.TypeParameter.ParamIdx]\n\n\t\treturn Id(typeParam.Name)\n\n\tcase *schema.Type_Config:\n\t\t// Config types are invisible outside of the Encore app\n\t\treturn g.getType(typ.Config.Elem)\n\n\tdefault:\n\t\treturn Any()\n\t}\n}\n\nfunc (g *golang) createApiPath(rpc *meta.RPC, withQueryString bool) (urlPath *Statement) {\n\tvar url strings.Builder\n\tparams := make([]Code, 0)\n\n\tfor _, segment := range rpc.Path.Segments {\n\t\turl.WriteByte('/')\n\n\t\tif segment.Type == meta.PathSegment_LITERAL {\n\t\t\turl.WriteString(segment.Value)\n\t\t} else {\n\t\t\tparamID := Id(g.nonReservedId(segment.Value))\n\n\t\t\tswitch segment.ValueType {\n\t\t\tcase meta.PathSegment_STRING, meta.PathSegment_UUID:\n\t\t\t\turl.WriteString(\"%s\")\n\n\t\t\t\tif segment.Type == meta.PathSegment_WILDCARD || segment.Type == meta.PathSegment_FALLBACK {\n\t\t\t\t\tg.seenSlicePath = true\n\t\t\t\t\tparamID = Id(\"pathEscapeSlice\").Call(paramID)\n\t\t\t\t} else {\n\t\t\t\t\tparamID = Qual(\"net/url\", \"PathEscape\").Call(paramID)\n\t\t\t\t}\n\n\t\t\tcase meta.PathSegment_BOOL:\n\t\t\t\turl.WriteString(\"%t\")\n\t\t\tcase meta.PathSegment_INT8, meta.PathSegment_INT16, meta.PathSegment_INT32, meta.PathSegment_INT64, meta.PathSegment_INT,\n\t\t\t\tmeta.PathSegment_UINT8, meta.PathSegment_UINT16, meta.PathSegment_UINT32, meta.PathSegment_UINT64, meta.PathSegment_UINT:\n\t\t\t\turl.WriteString(\"%d\")\n\t\t\tdefault:\n\t\t\t\turl.WriteString(\"%v\")\n\t\t\t}\n\n\t\t\tparams = append(params, paramID)\n\t\t}\n\t}\n\n\t// Construct the query string\n\tif withQueryString {\n\t\turl.WriteString(\"?%s\")\n\t\tparams = append(params, Id(\"queryString\").Dot(\"Encode\").Call())\n\t}\n\n\tif len(params) == 0 {\n\t\turlPath = Lit(url.String())\n\t} else {\n\t\t// Prepend the string format\n\t\tparams = append([]Code{Lit(url.String())}, params...)\n\t\turlPath = Qual(\"fmt\", \"Sprintf\").Call(params...)\n\t}\n\n\treturn urlPath\n}\n\n// FileStatement is an interface implemented by both jen.File and jen.Statement\ntype FileStatement interface {\n\tComment(string) *Statement\n\tLine() *Statement\n\tType() *Statement\n}\n\nfunc (g *golang) generateTypeDefinitions(stmt FileStatement, decls []*schema.Decl) {\n\tsort.Slice(decls, func(i, j int) bool {\n\t\treturn decls[i].Name < decls[j].Name\n\t})\n\n\tfor _, decl := range decls {\n\t\t// Write the docs\n\t\tif decl.Doc != \"\" && !g.skipDocs {\n\t\t\tfor _, line := range strings.Split(strings.TrimSpace(decl.Doc), \"\\n\") {\n\t\t\t\tstmt.Comment(line)\n\t\t\t}\n\t\t} else {\n\t\t\tstmt.Line()\n\t\t}\n\n\t\t// Create the base type definition; `type X[T]`\n\t\ttyp := stmt.Type().Add(g.declToID(decl))\n\t\tif len(decl.TypeParams) > 0 {\n\t\t\ttypes := make([]Code, len(decl.TypeParams))\n\n\t\t\tfor i, param := range decl.TypeParams {\n\t\t\t\ttypes[i] = Id(param.Name).Any()\n\t\t\t}\n\n\t\t\ttyp = typ.Types(types...)\n\t\t}\n\n\t\t// All types which are not structs should be aliases\n\t\tif decl.Type.GetStruct() == nil && len(decl.TypeParams) == 0 {\n\t\t\ttyp = typ.Op(\"=\")\n\t\t}\n\n\t\t// Add the type\n\t\ttyp.Add(g.getType(decl.Type))\n\t}\n}\n\nfunc (g *golang) generateAnonStructTypes(fields []*encoding.ParameterEncoding, encodingTag string) (types []Code, err error) {\n\tfor _, field := range fields {\n\t\tvar tagValue strings.Builder\n\t\ttagValue.WriteString(field.Name)\n\n\t\t// Parse the tags and extract the encoding tag\n\t\ttags, err := structtag.Parse(field.RawTag)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"parse tags: %s\", field.SrcName)\n\t\t}\n\t\tif tag, err := tags.Get(encodingTag); err == nil {\n\t\t\toptions := strings.Join(tag.Options, \",\")\n\t\t\tif options != \"\" {\n\t\t\t\ttagValue.WriteRune(',')\n\t\t\t\ttagValue.WriteString(options)\n\t\t\t}\n\t\t}\n\n\t\ttypes = append(\n\t\t\ttypes,\n\t\t\tId(idents.Convert(field.SrcName, idents.PascalCase)).Add(g.getType(field.Type)).Tag(map[string]string{encodingTag: tagValue.String()}),\n\t\t)\n\t}\n\n\treturn\n}\n\nfunc (g *golang) generateBaseClient(file *File) (err error) {\n\t// Add the interface\n\tfile.Comment(\"HTTPDoer is an interface which can be used to swap out the default\")\n\tfile.Comment(\"HTTP client (http.DefaultClient) with your own custom implementation.\")\n\tfile.Comment(\"This can be used to inject middleware or mock responses during unit tests.\")\n\tfile.Type().Id(\"HTTPDoer\").Interface(\n\t\tId(\"Do\").\n\t\t\tParams(Id(\"req\").Op(\"*\").Qual(\"net/http\", \"Request\")).\n\t\t\tParams(Op(\"*\").Qual(\"net/http\", \"Response\"), Error()),\n\t)\n\n\t// Add the base client struct\n\tfile.Line()\n\tfile.Comment(\"baseClient holds all the information we need to make requests to an Encore application\")\n\tfile.Type().Id(\"baseClient\").StructFunc(func(grp *Group) {\n\t\tif g.md.AuthHandler != nil {\n\t\t\ttyp := g.getType(g.md.AuthHandler.Params)\n\n\t\t\tgrp.Id(\"authGenerator\").Func().\n\t\t\t\tParams(Id(\"ctx\").Qual(\"context\", \"Context\")).\n\t\t\t\tParams(typ, Error()).\n\t\t\t\tComment(\"The function which will add the authentication data to the requests\")\n\t\t}\n\n\t\tgrp.Id(\"httpClient\").Id(\"HTTPDoer\").\n\t\t\tComment(\"The HTTP client which will be used for all API requests\")\n\n\t\tgrp.Id(\"baseURL\").Op(\"*\").Qual(\"net/url\", \"URL\").\n\t\t\tComment(\"The base URL which API requests will be made against\")\n\n\t\tgrp.Id(\"userAgent\").String().\n\t\t\tCommentf(\"What user agent we will use in the API requests\")\n\t})\n\n\t// Add the Do method for th base client\n\tfile.Line()\n\tfile.Comment(\"Do sends the req to the Encore application adding the authorization token as required.\")\n\tfile.Func().\n\t\tParams(Id(\"b\").Op(\"*\").Id(\"baseClient\")).\n\t\tId(\"Do\").\n\t\tParams(Id(\"req\").Op(\"*\").Qual(\"net/http\", \"Request\")).\n\t\tParams(Op(\"*\").Qual(\"net/http\", \"Response\"), Error()).\n\t\tBlockFunc(func(grp *Group) {\n\t\t\tgrp.Id(\"req\").Dot(\"Header\").Dot(\"Set\").Call(\n\t\t\t\tLit(\"Content-Type\"),\n\t\t\t\tLit(\"application/json\"),\n\t\t\t)\n\t\t\tgrp.Id(\"req\").Dot(\"Header\").Dot(\"Set\").Call(\n\t\t\t\tLit(\"User-Agent\"),\n\t\t\t\tId(\"b\").Dot(\"userAgent\"),\n\t\t\t)\n\t\t\tgrp.Line()\n\n\t\t\tif g.md.AuthHandler != nil {\n\t\t\t\terr = g.addAuthData(grp)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tgrp.Comment(\"Merge the base URL and the API URL\")\n\t\t\tgrp.Id(\"req\").Dot(\"URL\").Op(\"=\").\n\t\t\t\tId(\"b\").Dot(\"baseURL\").Dot(\"ResolveReference\").Call(Id(\"req\").Dot(\"URL\"))\n\t\t\tgrp.Id(\"req\").Dot(\"Host\").Op(\"=\").Id(\"req\").Dot(\"URL\").Dot(\"Host\")\n\t\t\tgrp.Line()\n\n\t\t\tgrp.Comment(\"Finally, make the request via the configured HTTP Client\")\n\t\t\tgrp.Return(\n\t\t\t\tId(\"b\").Dot(\"httpClient\").Dot(\"Do\").Call(Id(\"req\")),\n\t\t\t)\n\t\t})\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Add the call API function\n\tfile.Line()\n\tfile.Comment(\"callAPI is used by each generated API method to actually make request and decode the responses\")\n\tfile.Func().\n\t\tId(\"callAPI\").\n\t\tParams(\n\t\t\tId(\"ctx\").Qual(\"context\", \"Context\"),\n\t\t\tId(\"client\").Op(\"*\").Id(\"baseClient\"),\n\t\t\tId(\"method\"),\n\t\t\tId(\"path\").String(),\n\t\t\tId(\"headers\").Qual(\"net/http\", \"Header\"),\n\t\t\tList(Id(\"body\"), Id(\"resp\")).Any(),\n\t\t).\n\t\tParams(Qual(\"net/http\", \"Header\"), Error()).\n\t\tBlock(\n\t\t\tComment(\"Encode the API body\"),\n\t\t\tVar().Id(\"bodyReader\").Qual(\"io\", \"Reader\"),\n\t\t\tIf(Id(\"body\").Op(\"!=\").Nil()).Block(\n\t\t\t\tList(Id(\"bodyBytes\"), Err()).Op(\":=\").\n\t\t\t\t\tQual(\"encoding/json\", \"Marshal\").\n\t\t\t\t\tCall(Id(\"body\")),\n\t\t\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\t\tReturn(Nil(), Qual(\"fmt\", \"Errorf\").Call(Lit(\"marshal request: %w\"), Err())),\n\t\t\t\t),\n\n\t\t\t\tId(\"bodyReader\").Op(\"=\").Qual(\"bytes\", \"NewReader\").Call(Id(\"bodyBytes\")),\n\t\t\t),\n\t\t\tLine(),\n\n\t\t\tComment(\"Create the request\"),\n\t\t\tList(Id(\"req\"), Err()).Op(\":=\").\n\t\t\t\tQual(\"net/http\", \"NewRequestWithContext\").\n\t\t\t\tCall(\n\t\t\t\t\tId(\"ctx\"), Id(\"method\"), Id(\"path\"), Id(\"bodyReader\"),\n\t\t\t\t),\n\t\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\tReturn(Nil(), Qual(\"fmt\", \"Errorf\").Call(Lit(\"create request: %w\"), Err())),\n\t\t\t),\n\t\t\tLine(),\n\n\t\t\tComment(\"Add any headers to the request\"),\n\t\t\tFor(List(Id(\"header\"), Id(\"values\")).Op(\":=\").Range().Id(\"headers\")).Block(\n\t\t\t\tFor(List(Id(\"_\"), Id(\"value\")).Op(\":=\").Range().Id(\"values\")).Block(\n\t\t\t\t\tId(\"req\").Dot(\"Header\").Dot(\"Add\").Call(Id(\"header\"), Id(\"value\")),\n\t\t\t\t),\n\t\t\t),\n\t\t\tLine(),\n\n\t\t\tComment(\"Make the request via the base client\"),\n\t\t\tList(Id(\"rawResponse\"), Err()).Op(\":=\").\n\t\t\t\tId(\"client\").Dot(\"Do\").Call(Id(\"req\")),\n\t\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\tReturn(Nil(), Qual(\"fmt\", \"Errorf\").Call(Lit(\"request failed: %w\"), Err())),\n\t\t\t),\n\t\t\tDefer().Func().Params().Block(\n\t\t\t\tId(\"_\").Op(\"=\").Id(\"rawResponse\").Dot(\"Body\").Dot(\"Close\").Call(),\n\t\t\t).Call(),\n\t\t\tIf(Id(\"rawResponse\").Dot(\"StatusCode\").Op(\">=\").Lit(400)).Block(\n\t\t\t\tComment(\"Read the full body sent back\"),\n\t\t\t\tList(Id(\"body\"), Err()).Op(\":=\").Qual(\"io\", \"ReadAll\").Call(Id(\"rawResponse\").Dot(\"Body\")),\n\t\t\t\tIf(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\t\tReturn(\n\t\t\t\t\t\tNil(),\n\t\t\t\t\t\tOp(\"&\").Id(\"APIError\").Values(Dict{\n\t\t\t\t\t\t\tId(\"Code\"):    Id(\"ErrUnknown\"),\n\t\t\t\t\t\t\tId(\"Message\"): Qual(\"fmt\", \"Sprintf\").Call(Lit(\"got error response without readable body: %s\"), Id(\"rawResponse\").Dot(\"Status\")),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tLine(),\n\n\t\t\t\tComment(\"Attempt to decode the error response as a structured APIError\"),\n\t\t\t\tId(\"apiError\").Op(\":=\").Op(\"&\").Id(\"APIError\").Block(),\n\t\t\t\tIf(\n\t\t\t\t\tErr().Op(\":=\").Qual(\"encoding/json\", \"Unmarshal\").\n\t\t\t\t\t\tCall(Id(\"body\"), Id(\"apiError\")),\n\t\t\t\t\tErr().Op(\"!=\").Nil(),\n\t\t\t\t).Block(\n\t\t\t\t\tComment(\"If the error is not a parsable as an APIError, then return an error with the raw body\"),\n\t\t\t\t\tReturn(\n\t\t\t\t\t\tNil(),\n\t\t\t\t\t\tOp(\"&\").Id(\"APIError\").Values(Dict{\n\t\t\t\t\t\t\tId(\"Code\"):    Id(\"ErrUnknown\"),\n\t\t\t\t\t\t\tId(\"Message\"): Qual(\"fmt\", \"Sprintf\").Call(Lit(\"got error response: %s\"), String().Call(Id(\"body\"))),\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\tReturn(Nil(), Id(\"apiError\")),\n\t\t\t),\n\t\t\tLine(),\n\n\t\t\tComment(\"Decode the response\"),\n\t\t\tIf(\n\t\t\t\tId(\"resp\").Op(\"!=\").Nil(),\n\t\t\t).Block(\n\t\t\t\tIf(\n\t\t\t\t\tErr().Op(\":=\").Qual(\"encoding/json\", \"NewDecoder\").\n\t\t\t\t\t\tCall(Id(\"rawResponse\").Dot(\"Body\")).\n\t\t\t\t\t\tDot(\"Decode\").\n\t\t\t\t\t\tCall(Id(\"resp\")),\n\t\t\t\t\tErr().Op(\"!=\").Nil(),\n\t\t\t\t).Block(\n\t\t\t\t\tReturn(Nil(), Qual(\"fmt\", \"Errorf\").Call(Lit(\"decode response: %w\"), Err())),\n\t\t\t\t),\n\t\t\t),\n\t\t\tReturn(\n\t\t\t\tId(\"rawResponse\").Dot(\"Header\"),\n\t\t\t\tNil(),\n\t\t\t),\n\t\t)\n\n\treturn nil\n}\n\nfunc (g *golang) writeErrorType(file *File) {\n\tconst ErrPrefix = \"Err\"\n\n\t// Create the error type\n\tfile.Line()\n\tfile.Comment(\"APIError is the error type returned by the API\")\n\tfile.Type().Id(\"APIError\").Struct(\n\t\tId(\"Code\").Id(\"ErrCode\").Tag(map[string]string{\"json\": \"code\"}),\n\t\tId(\"Message\").String().Tag(map[string]string{\"json\": \"message\"}),\n\t\tId(\"Details\").Any().Tag(map[string]string{\"json\": \"details\"}),\n\t)\n\tfile.Func().Params(Id(\"e\").Op(\"*\").Id(\"APIError\")).Id(\"Error\").Params().String().Block(\n\t\tReturn(Qual(\"fmt\", \"Sprintf\").Call(Lit(\"%s: %s\"), Id(\"e\").Dot(\"Code\"), Id(\"e\").Dot(\"Message\"))),\n\t)\n\tfile.Line()\n\tfile.Line()\n\n\t// Create the ErrCode type and list\n\tfile.Type().Id(\"ErrCode\").Int()\n\terrTypes := make([]Code, 0)\n\tfor i, err := range errorCodes {\n\t\tfor _, line := range strings.Split(strings.TrimSpace(err.Comment), \"\\n\") {\n\t\t\t// Fix the comment with the prefix\n\t\t\tif strings.HasPrefix(line, err.Name) {\n\t\t\t\tline = ErrPrefix + line\n\t\t\t}\n\t\t\terrTypes = append(errTypes, Comment(line))\n\t\t}\n\n\t\terrTypes = append(\n\t\t\terrTypes,\n\t\t\tId(ErrPrefix+err.Name).Id(\"ErrCode\").Op(\"=\").Lit(i),\n\t\t\tLine(),\n\t\t)\n\t}\n\tfile.Const().Defs(errTypes...)\n\n\t// Create the functions to convert an error to string\n\tfile.Comment(\"// String returns the string representation of the error code\")\n\tfile.Func().Params(Id(\"c\").Id(\"ErrCode\")).Id(\"String\").Params().String().Block(\n\t\tSwitch(Id(\"c\")).BlockFunc(func(g *Group) {\n\t\t\tfor _, err := range errorCodes {\n\t\t\t\tg.Case(Id(ErrPrefix + err.Name)).Block(\n\t\t\t\t\tReturn(Lit(idents.Convert(err.Name, idents.SnakeCase))),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tg.Default().Block(\n\t\t\t\tReturn(Lit(\"unknown\")),\n\t\t\t)\n\t\t}),\n\t)\n\tfile.Line()\n\n\tfile.Comment(\"MarshalJSON converts the error code to a human-readable string\")\n\tfile.Func().Params(Id(\"c\").Id(\"ErrCode\")).Id(\"MarshalJSON\").Params().Params(Index().Byte(), Error()).Block(\n\t\tReturn(Index().Byte().Call(Qual(\"fmt\", \"Sprintf\").Call(Lit(\"\\\"%s\\\"\"), Id(\"c\"))), Nil()),\n\t)\n\tfile.Line()\n\n\tfile.Comment(\"UnmarshalJSON converts the human-readable string to an error code\")\n\tfile.Func().Params(Id(\"c\").Op(\"*\").Id(\"ErrCode\")).Id(\"UnmarshalJSON\").Params(Id(\"b\").Index().Byte()).Error().Block(\n\t\tSwitch(String().Call(Id(\"b\"))).BlockFunc(func(g *Group) {\n\t\t\tfor _, err := range errorCodes {\n\t\t\t\tg.Case(Lit(fmt.Sprintf(\"\\\"%s\\\"\", idents.Convert(err.Name, idents.SnakeCase)))).Block(\n\t\t\t\t\tOp(\"*\").Id(\"c\").Op(\"=\").Id(ErrPrefix + err.Name),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tg.Default().Block(\n\t\t\t\tOp(\"*\").Id(\"c\").Op(\"=\").Id(ErrPrefix + \"Unknown\"),\n\t\t\t)\n\t\t}),\n\n\t\tReturn(Nil()),\n\t)\n}\n\nfunc (g *golang) writeExtraHelpers(file *File) {\n\tif g.seenSlicePath {\n\t\tfile.Line()\n\t\tfile.Comment(\"// pathEscapeSlice escapes a slice of strings and then joins them into a single string\")\n\t\tfile.Func().Id(\"pathEscapeSlice\").Params(Id(\"paths\").Index().String()).String().Block(\n\t\t\tVar().Id(\"escapedPaths\").Qual(\"strings\", \"Builder\"),\n\t\t\tFor(List(Id(\"i\"), Id(\"path\")).Op(\":=\").Range().Id(\"paths\")).Block(\n\t\t\t\tIf(Id(\"i\").Op(\">\").Lit(0)).Block(\n\t\t\t\t\tId(\"escapedPaths\").Dot(\"WriteString\").Call(Lit(\"/\")),\n\t\t\t\t),\n\t\t\t\tId(\"escapedPaths\").Dot(\"WriteString\").Call(Qual(\"net/url\", \"PathEscape\").Call(Id(\"path\"))),\n\t\t\t),\n\t\t\tReturn(Id(\"escapedPaths\").Dot(\"String\").Call()),\n\t\t)\n\t}\n\n\tif g.seenLiteralNull {\n\t\tfile.Line()\n\t\tfile.Comment(\"null is a helper type to indicate a null value in JSON.\")\n\t\tfile.Type().Id(\"null\").Op(\"=\").Op(\"*\").Bool()\n\t}\n}\n\nfunc (g *golang) addAuthData(grp *Group) (err error) {\n\tgrp.Comment(\"If a authorization data generator is present, call it and add the returned token to the request\")\n\n\t// If the auth data is a string, then we want to add it as a bearer token\n\tif g.md.AuthHandler.Params.GetBuiltin() == schema.Builtin_STRING {\n\t\tgrp.If(Id(\"b\").Dot(\"authGenerator\").Op(\"!=\").Nil()).Block(\n\t\t\tIf(\n\t\t\t\tList(Id(\"token\"), Err()).Op(\":=\").\n\t\t\t\t\tId(\"b\").Dot(\"authGenerator\").Call(\n\t\t\t\t\tId(\"req\").Dot(\"Context\").Call(),\n\t\t\t\t),\n\t\t\t\tErr().Op(\"!=\").Nil(),\n\t\t\t).Block(\n\t\t\t\tReturn(Nil(), Qual(\"fmt\", \"Errorf\").Call(Lit(\"unable to create authorization token for api request: %w\"), Err())),\n\t\t\t).Else().If(Id(\"token\").Op(\"!=\").Lit(\"\")).Block(\n\t\t\t\tId(\"req\").Dot(\"Header\").Dot(\"Set\").Call(\n\t\t\t\t\tLit(\"Authorization\"),\n\t\t\t\t\tQual(\"fmt\", \"Sprintf\").\n\t\t\t\t\t\tCall(Lit(\"Bearer %s\"), Id(\"token\")),\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t\tgrp.Line()\n\n\t\treturn nil\n\t}\n\n\t// Otherwise, we need to add the complex data type\n\tauth, err := encoding.DescribeAuth(g.md, g.md.AuthHandler.Params, nil)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"unable to describe auth data\")\n\t}\n\n\tauthGeneratorCodeBlock := If(\n\t\tList(Id(\"authData\"), Err()).Op(\":=\").\n\t\t\tId(\"b\").Dot(\"authGenerator\").Call(\n\t\t\tId(\"req\").Dot(\"Context\").Call(),\n\t\t),\n\t\tErr().Op(\"!=\").Nil(),\n\t).Block(\n\t\tReturn(Nil(), Qual(\"fmt\", \"Errorf\").Call(Lit(\"unable to create authorization token for api request: %w\"), Err())),\n\t).Else()\n\n\tif g.md.AuthHandler.AuthData.GetPointer() != nil {\n\t\tauthGeneratorCodeBlock = authGeneratorCodeBlock.If(Id(\"authData\").Op(\"!=\").Nil())\n\t}\n\n\tauthGeneratorCodeBlock = authGeneratorCodeBlock.BlockFunc(func(grp *Group) {\n\n\t\tenc := g.enc.NewPossibleInstance(\"authEncoder\")\n\t\tenc.Add(Line())\n\n\t\tif len(auth.QueryParameters) > 0 {\n\t\t\tenc.Add(Comment(\"Add the auth fields to the query string\"), Line())\n\t\t\tenc.Add(Id(\"query\").Op(\":=\").Id(\"req\").Dot(\"URL\").Dot(\"Query\").Call(), Line())\n\n\t\t\t// Check the request schema for fields we can put in the query string\n\t\t\tfor _, field := range auth.QueryParameters {\n\t\t\t\tif field.Type.GetList() != nil {\n\t\t\t\t\t// If we have a slice, we need to encode each bit\n\t\t\t\t\tslice, err := enc.ToStringSlice(\n\t\t\t\t\t\tfield.Type,\n\t\t\t\t\t\tId(\"authData\").Dot(idents.Convert(field.SrcName, idents.PascalCase)),\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = errors.Wrapf(err, \"unable to encode query fields %s\", field.SrcName)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tenc.Add(For(List(Id(\"_\"), Id(\"v\")).Op(\":=\").Range().Add(slice)).Block(\n\t\t\t\t\t\tId(\"query\").Dot(\"Add\").Call(\n\t\t\t\t\t\t\tLit(field.WireFormat),\n\t\t\t\t\t\t\tId(\"v\"),\n\t\t\t\t\t\t),\n\t\t\t\t\t), Line())\n\t\t\t\t} else if opt := field.Type.GetOption(); opt != nil {\n\t\t\t\t\t// We encode options as *T, so check if it's non-nil.\n\t\t\t\t\tenc.Add(If(\n\t\t\t\t\t\tId(\"val\").Op(\":=\").Add(Id(\"authData\").Dot(idents.Convert(field.SrcName, idents.PascalCase))),\n\t\t\t\t\t\tId(\"val\").Op(\"!=\").Nil(),\n\t\t\t\t\t).BlockFunc(func(g *Group) {\n\t\t\t\t\t\tval, err := enc.ToString(\n\t\t\t\t\t\t\tfield.Type,\n\t\t\t\t\t\t\tId(\"val\"),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = errors.Wrapf(err, \"unable to encode query field %s\", field.SrcName)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tg.Add(Id(\"query\").Dot(\"Set\").Call(\n\t\t\t\t\t\t\tLit(field.WireFormat),\n\t\t\t\t\t\t\tval,\n\t\t\t\t\t\t), Line())\n\t\t\t\t\t}))\n\t\t\t\t} else {\n\t\t\t\t\t// Otherwise, we can just append the field\n\t\t\t\t\tval, err := enc.ToString(\n\t\t\t\t\t\tfield.Type,\n\t\t\t\t\t\tId(\"authData\").Dot(idents.Convert(field.SrcName, idents.PascalCase)),\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = errors.Wrapf(err, \"unable to encode query field %s\", field.SrcName)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tenc.Add(Id(\"query\").Dot(\"Set\").Call(\n\t\t\t\t\t\tLit(field.WireFormat),\n\t\t\t\t\t\tval,\n\t\t\t\t\t), Line())\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tenc.Add(Id(\"req\").Dot(\"URL\").Dot(\"RawQuery\").Op(\"=\").Id(\"query\").Dot(\"Encode\").Call(), Line(), Line())\n\t\t}\n\n\t\tif len(auth.HeaderParameters) > 0 {\n\t\t\tenc.Add(Comment(\"Add the auth fields to the headers\"), Line())\n\n\t\t\t// Check the request schema for fields we can put in the query string\n\t\t\tfor _, field := range auth.HeaderParameters {\n\t\t\t\tif field.Type.GetList() != nil {\n\t\t\t\t\t// If we have a slice, we need to encode each bit\n\t\t\t\t\tslice, err := enc.ToStringSlice(\n\t\t\t\t\t\tfield.Type,\n\t\t\t\t\t\tId(\"authData\").Dot(idents.Convert(field.SrcName, idents.PascalCase)),\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = errors.Wrapf(err, \"unable to encode header fields %s\", field.SrcName)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tenc.Add(For(List(Id(\"_\"), Id(\"v\")).Op(\":=\").Range().Add(slice)).Block(\n\t\t\t\t\t\tId(\"req\").Dot(\"Header\").Dot(\"Add\").Call(\n\t\t\t\t\t\t\tLit(field.WireFormat),\n\t\t\t\t\t\t\tId(\"v\"),\n\t\t\t\t\t\t),\n\t\t\t\t\t), Line())\n\t\t\t\t} else if opt := field.Type.GetOption(); opt != nil {\n\t\t\t\t\t// We encode options as *T, so check if it's non-nil.\n\t\t\t\t\tenc.Add(If(\n\t\t\t\t\t\tId(\"val\").Op(\":=\").Add(Id(\"authData\").Dot(idents.Convert(field.SrcName, idents.PascalCase))),\n\t\t\t\t\t\tId(\"val\").Op(\"!=\").Nil(),\n\t\t\t\t\t).BlockFunc(func(g *Group) {\n\t\t\t\t\t\tval, err := enc.ToString(\n\t\t\t\t\t\t\tfield.Type,\n\t\t\t\t\t\t\tId(\"val\"),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terr = errors.Wrapf(err, \"unable to encode header field %s\", field.SrcName)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tenc.Add(Id(\"req\").Dot(\"Header\").Dot(\"Set\").Call(\n\t\t\t\t\t\t\tLit(field.WireFormat),\n\t\t\t\t\t\t\tval,\n\t\t\t\t\t\t), Line())\n\t\t\t\t\t}))\n\t\t\t\t} else {\n\t\t\t\t\t// Otherwise, we can just append the field\n\t\t\t\t\tval, err := enc.ToString(\n\t\t\t\t\t\tfield.Type,\n\t\t\t\t\t\tId(\"authData\").Dot(idents.Convert(field.SrcName, idents.PascalCase)),\n\t\t\t\t\t)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terr = errors.Wrapf(err, \"unable to encode header field %s\", field.SrcName)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tenc.Add(Id(\"req\").Dot(\"Header\").Dot(\"Set\").Call(\n\t\t\t\t\t\tLit(field.WireFormat),\n\t\t\t\t\t\tval,\n\t\t\t\t\t), Line())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tgrp.Add(enc.Finalize(Return(Nil(), Qual(\"fmt\", \"Errorf\").Call(\n\t\t\tLit(\"unable to marshal authentication data: %w\"),\n\t\t\tenc.LastError(),\n\t\t)))...)\n\t})\n\n\tgrp.If(Id(\"b\").Dot(\"authGenerator\").Op(\"!=\").Nil()).Block(\n\t\tauthGeneratorCodeBlock,\n\t)\n\tgrp.Line()\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/clientgen/javascript.go",
    "content": "package clientgen\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/idents\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/parser/encoding\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n/* The JavaScript generator generates code that looks like this:\nclass TaskServiceClient {\n\tpublic Add(params) {\n\t\t// ...\n\t}\n}\n\nexport const task = {\n    ServiceClient: TaskServiceClient\n}\n\n*/\n\n// jsGenVersion allows us to introduce breaking changes in the generated code but behind a switch\n// meaning that people with client code reliant on the old behaviour can continue to generate the\n// old code.\ntype jsGenVersion int\n\nconst (\n\t// JsInitial is the originally released javascript generator\n\tJsInitial jsGenVersion = iota\n\n\t// JsExperimental can be used to lock experimental or uncompleted features in the generated code\n\t// It should always be the last item in the enum\n\tJsExperimental\n)\n\nconst javascriptGenLatestVersion = JsExperimental - 1\n\ntype javascript struct {\n\t*bytes.Buffer\n\tmd               *meta.Data\n\tappSlug          string\n\ttyps             *typeRegistry\n\tcurrDecl         *schema.Decl\n\tgeneratorVersion jsGenVersion\n\n\tseenJSON           bool // true if a JSON type was seen\n\tseenHeaderResponse bool // true if we've seen a header used in a response object\n\thasAuth            bool // true if we've seen an authentication handler\n\tauthIsComplexType  bool // true if the auth type is a complex type\n}\n\nfunc (js *javascript) Version() int {\n\treturn int(js.generatorVersion)\n}\n\nfunc (js *javascript) Generate(p clientgentypes.GenerateParams) (err error) {\n\tdefer js.handleBailout(&err)\n\n\tjs.Buffer = p.Buf\n\tjs.md = p.Meta\n\tjs.appSlug = p.AppSlug\n\tjs.typs = getNamedTypes(p.Meta, p.Services)\n\n\tif js.md.AuthHandler != nil {\n\t\tif !js.isAuthCookiesOnly() {\n\t\t\tjs.hasAuth = true\n\t\t\tjs.authIsComplexType = js.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING\n\t\t}\n\t}\n\n\tjs.WriteString(\"// \" + doNotEditHeader() + \"\\n\\n\")\n\n\tjs.WriteString(\"// Disable eslint, jshint, and jslint for this file.\\n\")\n\tjs.WriteString(\"/* eslint-disable */\\n\")\n\tjs.WriteString(\"/* jshint ignore:start */\\n\")\n\tjs.WriteString(\"/*jslint-disable*/\\n\")\n\n\tseenNs := make(map[string]bool)\n\tjs.writeClient(p.Services)\n\tfor _, svc := range p.Meta.Svcs {\n\t\tif err := js.writeService(svc, p.Services, p.Tags); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tseenNs[svc.Name] = true\n\t}\n\tjs.writeExtraTypes()\n\tjs.writeStreamClasses()\n\tif err := js.writeBaseClient(p.AppSlug); err != nil {\n\t\treturn err\n\t}\n\tjs.writeCustomErrorType()\n\n\treturn nil\n}\n\nfunc (js *javascript) getFields(typ *schema.Type) []*schema.Field {\n\tif typ == nil {\n\t\treturn nil\n\t}\n\tswitch typ.Typ.(type) {\n\tcase *schema.Type_Struct:\n\t\treturn typ.GetStruct().Fields\n\tcase *schema.Type_Named:\n\t\tdecl := js.md.Decls[typ.GetNamed().Id]\n\t\treturn js.getFields(decl.Type)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (js *javascript) isAuthCookiesOnly() bool {\n\tif js.md.AuthHandler == nil {\n\t\treturn false\n\t}\n\tfields := js.getFields(js.md.AuthHandler.Params)\n\tif fields == nil {\n\t\treturn false\n\t}\n\tfor _, field := range fields {\n\t\tif field.Wire.GetCookie() == nil {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (js *javascript) writeService(svc *meta.Service, set clientgentypes.ServiceSet, tags clientgentypes.TagSet) error {\n\t// Determine if we have anything worth exposing.\n\t// Either a public RPC or a named type.\n\tisIncluded := hasPublicRPC(svc) && set.Has(svc.Name)\n\tif !isIncluded {\n\t\treturn nil\n\t}\n\n\tns := svc.Name\n\tnumIndent := 0\n\tindent := func() {\n\t\tjs.WriteString(strings.Repeat(\"    \", numIndent))\n\t}\n\n\t// Service doc string\n\tif doc := getServiceDoc(js.md, svc); doc != \"\" {\n\t\tscanner := bufio.NewScanner(strings.NewReader(doc))\n\t\tjs.WriteString(\"/**\\n\")\n\t\tfor scanner.Scan() {\n\t\t\tjs.WriteString(\" * \")\n\t\t\tjs.WriteString(scanner.Text())\n\t\t\tjs.WriteByte('\\n')\n\t\t}\n\t\tjs.WriteString(\" */\\n\")\n\t}\n\n\tfmt.Fprintf(js, \"class %sServiceClient {\\n\", cases.Title(language.English, cases.Compact).String(js.typeName(ns)))\n\tnumIndent++\n\n\t// Constructor\n\tindent()\n\tjs.WriteString(\"constructor(baseClient) {\\n\")\n\tnumIndent++\n\tindent()\n\tjs.WriteString(\"this.baseClient = baseClient\\n\")\n\tfor _, rpc := range svc.Rpcs {\n\t\tif rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {\n\t\t\tcontinue\n\t\t}\n\t\tname := js.memberName(rpc.Name)\n\t\tindent()\n\t\tfmt.Fprintf(js, \"this.%s = this.%s.bind(this)\\n\", name, name)\n\t}\n\n\tnumIndent--\n\tindent()\n\tjs.WriteString(\"}\\n\")\n\n\t// RPCs\n\tfor _, rpc := range svc.Rpcs {\n\t\tif rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {\n\t\t\tcontinue\n\t\t}\n\n\t\tjs.WriteByte('\\n')\n\n\t\t// Doc string\n\t\tif rpc.Doc != nil && *rpc.Doc != \"\" {\n\t\t\tscanner := bufio.NewScanner(strings.NewReader(*rpc.Doc))\n\t\t\tindent()\n\t\t\tjs.WriteString(\"/**\\n\")\n\t\t\tfor scanner.Scan() {\n\t\t\t\tindent()\n\t\t\t\tjs.WriteString(\" * \")\n\t\t\t\tjs.WriteString(scanner.Text())\n\t\t\t\tjs.WriteByte('\\n')\n\t\t\t}\n\t\t\tindent()\n\t\t\tjs.WriteString(\" */\\n\")\n\t\t}\n\n\t\t// Signature\n\t\tindent()\n\t\tfmt.Fprintf(js, \"async %s(\", js.memberName(rpc.Name))\n\n\t\tif rpc.Proto == meta.RPC_RAW {\n\t\t\tjs.WriteString(\"method, \")\n\t\t}\n\n\t\tnParams := 0\n\t\tvar rpcPath strings.Builder\n\t\tfor _, s := range rpc.Path.Segments {\n\t\t\trpcPath.WriteByte('/')\n\t\t\tif s.Type != meta.PathSegment_LITERAL {\n\t\t\t\tif nParams > 0 {\n\t\t\t\t\tjs.WriteString(\", \")\n\t\t\t\t}\n\n\t\t\t\tjs.WriteString(js.nonReservedId(s.Value))\n\t\t\t\tif s.Type == meta.PathSegment_WILDCARD || s.Type == meta.PathSegment_FALLBACK {\n\t\t\t\t\trpcPath.WriteString(\"${\" + js.nonReservedId(s.Value) + \".map(encodeURIComponent).join(\\\"/\\\")}\")\n\t\t\t\t} else {\n\t\t\t\t\trpcPath.WriteString(\"${encodeURIComponent(\" + js.nonReservedId(s.Value) + \")}\")\n\t\t\t\t}\n\t\t\t\tnParams++\n\t\t\t} else {\n\t\t\t\trpcPath.WriteString(s.Value)\n\t\t\t}\n\t\t}\n\n\t\tvar isStream = rpc.StreamingRequest || rpc.StreamingResponse\n\n\t\t// Avoid a name collision.\n\t\tpayloadName := \"params\"\n\n\t\tif (!isStream && rpc.RequestSchema != nil) || (isStream && rpc.HandshakeSchema != nil) {\n\t\t\tif nParams > 0 {\n\t\t\t\tjs.WriteString(\", \")\n\t\t\t}\n\t\t\tjs.WriteString(payloadName)\n\t\t} else if rpc.Proto == meta.RPC_RAW {\n\t\t\tif nParams > 0 {\n\t\t\t\tjs.WriteString(\", \")\n\t\t\t}\n\t\t\tjs.WriteString(\"body, options\")\n\t\t}\n\n\t\tjs.WriteString(\") {\\n\")\n\n\t\tif isStream {\n\t\t\tvar direction streamDirection\n\n\t\t\tif rpc.StreamingRequest && rpc.StreamingResponse {\n\t\t\t\tdirection = InOut\n\t\t\t} else if rpc.StreamingRequest {\n\t\t\t\tdirection = Out\n\t\t\t} else {\n\t\t\t\tdirection = In\n\t\t\t}\n\n\t\t\tif err := js.streamCallSite(js.newIdentWriter(numIndent+1), rpc, rpcPath.String(), direction); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"unable to write streaming RPC call site for %s.%s\", rpc.ServiceName, rpc.Name)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := js.rpcCallSite(js.newIdentWriter(numIndent+1), rpc, rpcPath.String()); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"unable to write RPC call site for %s.%s\", rpc.ServiceName, rpc.Name)\n\t\t\t}\n\t\t}\n\n\t\tindent()\n\t\tjs.WriteString(\"}\\n\")\n\t}\n\tnumIndent--\n\tindent()\n\tjs.WriteString(\"}\\n\\n\")\n\n\tfmt.Fprintf(js, \"export const %s = {\\n\", js.typeName(ns))\n\tnumIndent++\n\tindent()\n\tfmt.Fprintf(js, \"ServiceClient: %sServiceClient\\n\", cases.Title(language.English, cases.Compact).String(js.typeName(ns)))\n\tnumIndent--\n\tindent()\n\tjs.WriteString(\"}\\n\\n\")\n\treturn nil\n}\n\ntype streamDirection int\n\nconst (\n\tInOut streamDirection = iota\n\tIn\n\tOut\n)\n\nfunc (js *javascript) streamCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string, direction streamDirection) error {\n\theaders := \"\"\n\tquery := \"\"\n\n\tif rpc.HandshakeSchema != nil {\n\t\tencs, err := encoding.DescribeRequest(js.md, rpc.HandshakeSchema, &encoding.Options{SrcNameTag: \"json\"}, \"GET\")\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"stream %s\", rpc.Name)\n\t\t}\n\n\t\thandshakeEnc := encs[0]\n\n\t\tif len(handshakeEnc.HeaderParameters) > 0 || len(handshakeEnc.QueryParameters) > 0 {\n\t\t\tw.WriteString(\"// Convert our params into the objects we need for the request\\n\")\n\t\t}\n\n\t\t// Generate the headers\n\t\tif len(handshakeEnc.HeaderParameters) > 0 {\n\t\t\theaders = \"headers\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range handshakeEnc.HeaderParameters {\n\t\t\t\tref := js.Dot(\"params\", field.SrcName)\n\t\t\t\tdict[field.WireFormat] = js.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional)\n\t\t\t}\n\n\t\t\tw.WriteString(\"const headers = makeRecord(\")\n\t\t\tjs.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\n\t\t// Generate the query string\n\t\tif len(handshakeEnc.QueryParameters) > 0 {\n\t\t\tquery = \"query\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range handshakeEnc.QueryParameters {\n\t\t\t\tif list := field.Type.GetList(); list != nil {\n\t\t\t\t\tdict[field.WireFormat] = js.Dot(\"params\", field.SrcName) +\n\t\t\t\t\t\t\".map((v) => \" + js.convertBuiltinToString(list.Elem.GetBuiltin(), \"v\", false) + \")\"\n\t\t\t\t} else {\n\t\t\t\t\tdict[field.WireFormat] = js.convertBuiltinToString(\n\t\t\t\t\t\tfield.Type.GetBuiltin(),\n\t\t\t\t\t\tjs.Dot(\"params\", field.SrcName),\n\t\t\t\t\t\tfield.Optional,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw.WriteString(\"const query = makeRecord(\")\n\t\t\tjs.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\t}\n\n\t// Build the call to createStream\n\tvar method string\n\n\tswitch direction {\n\tcase InOut:\n\t\tmethod = \"createStreamInOut\"\n\tcase In:\n\t\tmethod = \"createStreamIn\"\n\tcase Out:\n\t\tmethod = \"createStreamOut\"\n\t}\n\n\tcreateStream := fmt.Sprintf(\n\t\t\"this.baseClient.%s(`%s`\",\n\t\tmethod,\n\t\trpcPath,\n\t)\n\n\tif headers != \"\" || query != \"\" {\n\t\tcreateStream += \", {\" + headers\n\n\t\tif headers != \"\" && query != \"\" {\n\t\t\tcreateStream += \", \"\n\t\t}\n\n\t\tif query != \"\" {\n\t\t\tcreateStream += query\n\t\t}\n\n\t\tcreateStream += \"}\"\n\t}\n\tcreateStream += \")\"\n\n\tw.WriteStringf(\"return await %s\\n\", createStream)\n\treturn nil\n}\n\nfunc (js *javascript) rpcCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string) error {\n\t// Work out how we're going to encode and call this RPC\n\trpcEncoding, err := encoding.DescribeRPC(js.md, rpc, &encoding.Options{SrcNameTag: \"json\"})\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"rpc %s\", rpc.Name)\n\t}\n\n\t// Raw end points just pass through the request\n\t// and need no further code generation\n\tif rpc.Proto == meta.RPC_RAW {\n\t\tw.WriteStringf(\n\t\t\t\"return this.baseClient.callAPI(method, `%s`, body, options)\\n\",\n\t\t\trpcPath,\n\t\t)\n\t\treturn nil\n\t}\n\n\t// Work out how we encode the Request Schema\n\theaders := \"\"\n\tquery := \"\"\n\tbody := \"\"\n\n\tif rpc.RequestSchema != nil {\n\t\treqEnc := rpcEncoding.DefaultRequestEncoding\n\n\t\tif len(reqEnc.HeaderParameters) > 0 || len(reqEnc.QueryParameters) > 0 {\n\t\t\tw.WriteString(\"// Convert our params into the objects we need for the request\\n\")\n\t\t}\n\n\t\t// Generate the headers\n\t\tif len(reqEnc.HeaderParameters) > 0 {\n\t\t\theaders = \"headers\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range reqEnc.HeaderParameters {\n\t\t\t\tref := js.Dot(\"params\", field.SrcName)\n\t\t\t\tdict[field.WireFormat] = js.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional)\n\t\t\t}\n\n\t\t\tw.WriteString(\"const headers = makeRecord(\")\n\t\t\tjs.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\n\t\t// Generate the query string\n\t\tif len(reqEnc.QueryParameters) > 0 {\n\t\t\tquery = \"query\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range reqEnc.QueryParameters {\n\t\t\t\tif list := field.Type.GetList(); list != nil {\n\t\t\t\t\tdict[field.WireFormat] = js.Dot(\"params\", field.SrcName) +\n\t\t\t\t\t\t\".map((v) => \" + js.convertBuiltinToString(list.Elem.GetBuiltin(), \"v\", false) + \")\"\n\t\t\t\t} else {\n\t\t\t\t\tdict[field.WireFormat] = js.convertBuiltinToString(\n\t\t\t\t\t\tfield.Type.GetBuiltin(),\n\t\t\t\t\t\tjs.Dot(\"params\", field.SrcName),\n\t\t\t\t\t\tfield.Optional,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw.WriteString(\"const query = makeRecord(\")\n\t\t\tjs.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\n\t\t// Generate the body\n\t\tif len(reqEnc.BodyParameters) > 0 {\n\t\t\tif len(reqEnc.HeaderParameters) == 0 && len(reqEnc.QueryParameters) == 0 {\n\t\t\t\t// In the simple case we can just encode the params as the body directly\n\t\t\t\tbody = \"JSON.stringify(params)\"\n\t\t\t} else {\n\t\t\t\t// Else we need a new struct called \"body\"\n\t\t\t\tbody = \"JSON.stringify(body)\"\n\n\t\t\t\tdict := make(map[string]string)\n\t\t\t\tfor _, field := range reqEnc.BodyParameters {\n\t\t\t\t\tfieldName := field.SrcName\n\t\t\t\t\tdict[fieldName] = js.Dot(\"params\", fieldName)\n\t\t\t\t}\n\n\t\t\t\tw.WriteString(\"// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\\nconst body = \")\n\t\t\t\tjs.Values(w, dict)\n\t\t\t\tw.WriteString(\"\\n\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Build the call to callTypedAPI\n\tcallAPI := fmt.Sprintf(\n\t\t\"this.baseClient.callTypedAPI(\\\"%s\\\", `%s`\",\n\t\trpcEncoding.DefaultMethod,\n\t\trpcPath,\n\t)\n\tif body != \"\" || headers != \"\" || query != \"\" {\n\t\tif body == \"\" {\n\t\t\tcallAPI += \", undefined\"\n\t\t} else {\n\t\t\tcallAPI += \", \" + body\n\t\t}\n\n\t\tif headers != \"\" || query != \"\" {\n\t\t\tcallAPI += \", {\" + headers\n\n\t\t\tif headers != \"\" && query != \"\" {\n\t\t\t\tcallAPI += \", \"\n\t\t\t}\n\n\t\t\tif query != \"\" {\n\t\t\t\tcallAPI += query\n\t\t\t}\n\n\t\t\tcallAPI += \"}\"\n\n\t\t}\n\t}\n\tcallAPI += \")\"\n\n\t// If there's no response schema, we can just return the call to the API directly\n\tif rpc.ResponseSchema == nil {\n\t\tw.WriteStringf(\"await %s\\n\", callAPI)\n\t\treturn nil\n\t}\n\n\tw.WriteStringf(\"// Now make the actual call to the API\\nconst resp = await %s\\n\", callAPI)\n\n\trespEnc := rpcEncoding.ResponseEncoding\n\n\t// If we don't need to do anything with the body, we can just return the response\n\tif len(respEnc.HeaderParameters) == 0 {\n\t\tw.WriteString(\"return await resp.json()\\n\")\n\t\treturn nil\n\t}\n\n\t// Otherwise, we need to add the header fields to the response\n\tw.WriteString(\"\\n//Populate the return object from the JSON body and received headers\\nconst rtn = await resp.json()\\n\")\n\n\tfor _, headerField := range respEnc.HeaderParameters {\n\t\tisSetCookie := strings.ToLower(headerField.WireFormat) == \"set-cookie\"\n\t\tif isSetCookie {\n\t\t\tw.WriteString(\"// Skip set-cookie header in browser context as browsers doesn't have access to read it\\n\")\n\t\t\tw.WriteString(\"if (!BROWSER) {\\n\")\n\t\t\tw = w.Indent()\n\t\t}\n\n\t\tjs.seenHeaderResponse = true\n\t\tfieldValue := fmt.Sprintf(\"resp.headers.get(\\\"%s\\\")\", headerField.WireFormat)\n\t\tif !headerField.Optional {\n\t\t\tfieldValue = fmt.Sprintf(\"mustBeSet(\\\"Header `%s`\\\", %s)\", headerField.WireFormat, fieldValue)\n\t\t}\n\n\t\tw.WriteStringf(\"%s = %s\\n\", js.Dot(\"rtn\", headerField.SrcName), js.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))\n\n\t\tif isSetCookie {\n\t\t\tw = w.Dedent()\n\t\t\tw.WriteString(\"}\\n\")\n\t\t}\n\t}\n\tw.WriteString(\"return rtn\\n\")\n\treturn nil\n}\n\n// nonReservedId returns the given ID, unless we have it a reserved within the client function _or_ it's a reserved Typescript keyword\nfunc (js *javascript) nonReservedId(id string) string {\n\tswitch id {\n\t// our reserved keywords (or ID's we use within the generated client functions)\n\tcase \"params\", \"headers\", \"query\", \"body\", \"resp\", \"rtn\":\n\t\treturn \"_\" + id\n\n\t// Javascript keywords\n\t// Based on https://www.w3schools.com/js/js_reserved.asp\n\tcase \"abstract\", \"arguments\", \"async\", \"await\", \"boolean\", \"break\", \"byte\", \"case\", \"catch\", \"char\",\n\t\t\"class\", \"const\", \"continue\", \"debugger\", \"default\", \"delete\", \"do\", \"double\", \"else\",\n\t\t\"enum\", \"eval\", \"export\", \"extends\", \"false\", \"final\", \"finally\", \"float\", \"for\", \"function\", \"get\",\n\t\t\"goto\", \"if\", \"implements\", \"import\", \"in\", \"instanceof\", \"int\", \"interface\", \"let\", \"long\",\n\t\t\"native\", \"new\", \"null\", \"of\", \"package\", \"private\", \"protected\", \"public\", \"require\", \"return\",\n\t\t\"short\", \"static\", \"super\", \"switch\", \"symbol\", \"synchronized\", \"this\", \"throw\", \"throws\",\n\t\t\"transient\", \"true\", \"try\", \"type\", \"typeof\", \"var\", \"void\", \"volatile\", \"while\", \"with\", \"yield\":\n\t\treturn \"_\" + id\n\n\tdefault:\n\t\treturn id\n\t}\n}\n\nfunc (js *javascript) writeClient(set clientgentypes.ServiceSet) {\n\tw := js.newIdentWriter(0)\n\tw.WriteString(`\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return ` + \"`https://${name}-\" + js.appSlug + \".encr.app`\" + `\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(` + \"`pr${pr}`\" + `)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the ` + js.appSlug + ` Encore application.\n */\nexport default class Client {`)\n\n\t{\n\t\tw := w.Indent()\n\t\tw.WriteString(`\n/**\n * Creates a Client for calling the public and authenticated APIs of your Encore application.\n *\n * @param target  The target which the client should be configured to use. See Local and Environment for options.\n * @param options Options for the client\n */\nconstructor(target = \"prod\", options = undefined) {`)\n\t\t{\n\t\t\tw := w.Indent()\n\n\t\t\tif js.hasAuth && !js.authIsComplexType {\n\t\t\t\tw.WriteString(`\n// Convert the old constructor parameters to a BaseURL object and a ClientOptions object\nif (!target.startsWith(\"http://\") && !target.startsWith(\"https://\")) {\n    target = Environment(target)\n}\n\nif (typeof options === \"string\") {\n    options = { auth: options }\n}\n\n`)\n\t\t\t} else {\n\t\t\t\tw.WriteString(\"\\n\")\n\t\t\t}\n\n\t\t\tw.WriteString(\"const base = new BaseClient(target, options ?? {})\\n\")\n\t\t\tfor _, svc := range js.md.Svcs {\n\t\t\t\tif hasPublicRPC(svc) && set.Has(svc.Name) {\n\t\t\t\t\tw.WriteStringf(\"this.%s = new %s.ServiceClient(base)\\n\", js.memberName(svc.Name), js.typeName(svc.Name))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tw.WriteString(\"    }\\n\")\n\t}\n\tw.WriteString(\"}\\n\\n\")\n}\n\nfunc (js *javascript) writeStreamClasses() {\n\tsend := `\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }`\n\n\treceive := `\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }`\n\n\tjs.WriteString(`\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n` + send + `\n` + receive + `\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n` + receive + `\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n` + send + `\n}`)\n}\nfunc (js *javascript) writeBaseClient(appSlug string) error {\n\tuserAgent := fmt.Sprintf(\"%s-Generated-JS-Client (Encore/%s)\", appSlug, version.Version)\n\n\tjs.WriteString(`\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {`)\n\tjs.WriteString(`\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"` + userAgent + `\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }`)\n\n\tif js.hasAuth {\n\t\tjs.WriteString(`\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n`)\n\t}\n\n\tjs.WriteString(`\n    }\n\n    async getAuthData() {`)\n\tif js.hasAuth {\n\t\tjs.WriteString(`\n        let authData;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data = {};\n\n`)\n\n\t\tw := js.newIdentWriter(3)\n\t\tif js.authIsComplexType {\n\t\t\tauthData, err := encoding.DescribeAuth(js.md, js.md.AuthHandler.Params, &encoding.Options{SrcNameTag: \"json\"})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"unable to describe auth data\")\n\t\t\t}\n\n\t\t\t// Generate the query string\n\t\t\tif len(authData.QueryParameters) > 0 {\n\t\t\t\tdict := make(map[string]string)\n\t\t\t\tfor _, field := range authData.QueryParameters {\n\t\t\t\t\tif list := field.Type.GetList(); list != nil {\n\t\t\t\t\t\tdict[field.WireFormat] = js.Dot(\"authData\", field.SrcName) +\n\t\t\t\t\t\t\t\".map((v) => \" + js.convertBuiltinToString(list.Elem.GetBuiltin(), \"v\", false) + \")\"\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdict[field.WireFormat] = js.convertBuiltinToString(\n\t\t\t\t\t\t\tfield.Type.GetBuiltin(),\n\t\t\t\t\t\t\tjs.Dot(\"authData\", field.SrcName),\n\t\t\t\t\t\t\tfield.Optional,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tw.WriteString(\"data.query = makeRecord(\")\n\t\t\t\tjs.Values(w, dict)\n\t\t\t\tw.WriteString(\");\\n\")\n\t\t\t}\n\n\t\t\t// Generate the headers\n\t\t\tif len(authData.HeaderParameters) > 0 {\n\t\t\t\tdict := make(map[string]string)\n\t\t\t\tfor _, field := range authData.HeaderParameters {\n\t\t\t\t\tref := js.Dot(\"authData\", field.SrcName)\n\t\t\t\t\tdict[field.WireFormat] = js.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional)\n\t\t\t\t}\n\n\t\t\t\tw.WriteString(\"data.headers = makeRecord(\")\n\t\t\t\tjs.Values(w, dict)\n\t\t\t\tw.WriteString(\")\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tw.WriteString(\"data.headers = {};\\n\")\n\t\t\tw.WriteString(\"data.headers[\\\"Authorization\\\"] = \\\"Bearer \\\" + authData;\\n\")\n\t\t}\n\n\t\tw.WriteString(\"\\nreturn data;\\n\")\n\t\tw.Dedent().WriteString(\"}\\n\")\n\t}\n\n\tjs.WriteString(`\n        return undefined;\n    }\n`)\n\n\tjs.WriteString(`\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: ` + \"`request failed: status ${response.status}`\" + ` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}`)\n\treturn nil\n}\n\nfunc (js *javascript) writeExtraTypes() {\n\tjs.WriteString(`\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(` + \"`\" + `${key}=${encodeURIComponent(v)}` + \"`\" + `)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n`)\n\n\tif js.seenHeaderResponse {\n\t\tjs.WriteString(`\n// mustBeSet will throw an APIError with the Data Loss code if value is null or undefined\nfunction mustBeSet(field, value) {\n    if (value === null || value === undefined) {\n        throw new APIError(\n            500,\n            {\n                code: ErrCode.DataLoss,\n                message: ` + \"`${field} was unexpectedly ${value}`\" + `, // ${value} will create the string \"null\" or \"undefined\"\n            },\n        )\n    }\n    return value\n}\n`)\n\t}\n}\n\nfunc (js *javascript) convertBuiltinToString(typ schema.Builtin, val string, isOptional bool) string {\n\tvar code string\n\tswitch typ {\n\tcase schema.Builtin_STRING:\n\t\treturn val\n\tcase schema.Builtin_JSON:\n\t\tcode = fmt.Sprintf(\"JSON.stringify(%s)\", val)\n\tdefault:\n\t\tcode = fmt.Sprintf(\"String(%s)\", val)\n\t}\n\n\tif isOptional {\n\t\tcode = fmt.Sprintf(\"%s === undefined ? undefined : %s\", val, code)\n\t}\n\treturn code\n}\n\nfunc (js *javascript) convertStringToBuiltin(typ schema.Builtin, val string) string {\n\tswitch typ {\n\tcase schema.Builtin_ANY:\n\t\treturn val\n\tcase schema.Builtin_BOOL:\n\t\treturn fmt.Sprintf(\"%s.toLowerCase() === \\\"true\\\"\", val)\n\tcase schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64,\n\t\tschema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64:\n\t\treturn fmt.Sprintf(\"parseInt(%s, 10)\", val)\n\tcase schema.Builtin_FLOAT32, schema.Builtin_FLOAT64:\n\t\treturn fmt.Sprintf(\"Number(%s)\", val)\n\tcase schema.Builtin_STRING:\n\t\treturn val\n\tcase schema.Builtin_BYTES:\n\t\treturn val\n\tcase schema.Builtin_TIME:\n\t\treturn val\n\tcase schema.Builtin_JSON:\n\t\tjs.seenJSON = true\n\t\treturn fmt.Sprintf(\"JSON.parse(%s)\", val)\n\tcase schema.Builtin_UUID:\n\t\treturn val\n\tcase schema.Builtin_USER_ID:\n\t\treturn val\n\tcase schema.Builtin_DECIMAL:\n\t\treturn val\n\tdefault:\n\t\tjs.errorf(\"unknown builtin type %v\", typ)\n\t\treturn \"any\"\n\t}\n}\n\nfunc (js *javascript) errorf(format string, args ...interface{}) {\n\tpanic(bailout{fmt.Errorf(format, args...)})\n}\n\nfunc (js *javascript) handleBailout(dst *error) {\n\tif err := recover(); err != nil {\n\t\tif bail, ok := err.(bailout); ok {\n\t\t\t*dst = bail.err\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc (js *javascript) newIdentWriter(indent int) *indentWriter {\n\treturn &indentWriter{\n\t\tw:                js.Buffer,\n\t\tdepth:            indent,\n\t\tindent:           \"    \",\n\t\tfirstWriteOnLine: true,\n\t}\n}\n\nfunc (js *javascript) Quote(s string) string {\n\treturn fmt.Sprintf(\"\\\"%s\\\"\", strings.Replace(s, \"\\\"\", \"\\\\\\\"\", -1))\n}\n\nfunc (js *javascript) QuoteIfRequired(s string) string {\n\t// If the identifier isn't purely alphanumeric, we need to add quotes.\n\tif !stringIsOnly(s, func(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }) {\n\t\treturn js.Quote(s)\n\t}\n\treturn s\n}\n\n// Dot allows us to reference a field in a struct by ijs name.\nfunc (js *javascript) Dot(structIdent string, fieldIdent string) string {\n\tfieldIdent = js.QuoteIfRequired(fieldIdent)\n\n\tif len(fieldIdent) > 0 && fieldIdent[0] == '\"' {\n\t\treturn fmt.Sprintf(\"%s[%s]\", structIdent, fieldIdent)\n\t} else {\n\t\treturn fmt.Sprintf(\"%s.%s\", structIdent, fieldIdent)\n\t}\n}\n\nfunc (js *javascript) Values(w *indentWriter, dict map[string]string) {\n\t// Work out the largest key length.\n\tlargestKey := 0\n\tkeys := make([]string, 0, len(dict))\n\tfor key := range dict {\n\t\tkeys = append(keys, key)\n\t\tkey = js.QuoteIfRequired(key)\n\t\tif len(key) > largestKey {\n\t\t\tlargestKey = len(key)\n\t\t}\n\t}\n\n\tsort.Strings(keys)\n\n\tw.WriteString(\"{\\n\")\n\t{\n\t\tw := w.Indent()\n\t\tfor _, key := range keys {\n\t\t\tident := js.QuoteIfRequired(key)\n\t\t\tw.WriteStringf(\"%s: %s%s,\\n\", ident, strings.Repeat(\" \", largestKey-len(ident)), dict[key])\n\t\t}\n\t}\n\tw.WriteString(\"}\")\n}\n\nfunc (js *javascript) typeName(identifier string) string {\n\tif js.generatorVersion < JsExperimental {\n\t\treturn identifier\n\t} else {\n\t\treturn idents.Convert(identifier, idents.PascalCase)\n\t}\n}\n\nfunc (js *javascript) memberName(identifier string) string {\n\tif js.generatorVersion < JsExperimental {\n\t\treturn identifier\n\t} else {\n\t\treturn idents.Convert(identifier, idents.CamelCase)\n\t}\n}\n\nfunc (js *javascript) fieldNameInStruct(field *schema.Field) string {\n\tname := field.Name\n\tif field.JsonName != \"\" {\n\t\tname = field.JsonName\n\t}\n\treturn name\n}\n\nfunc (js *javascript) writeCustomErrorType() {\n\tw := js.newIdentWriter(0)\n\n\tw.WriteString(`\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n`)\n}\n"
  },
  {
    "path": "pkg/clientgen/openapi/openapi.go",
    "content": "package openapi\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/doc/comment\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/getkin/kin-openapi/openapi3\"\n\n\t\"encr.dev/parser/encoding\"\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype GenVersion int\n\nconst (\n\t// Initial is the originally released OpenAPI client generator\n\tInitial GenVersion = iota\n\n\t// Experimental can be used to lock experimental or uncompleted features in the generated code\n\t// It should always be the last item in the enum.\n\tExperimental\n\n\tLatestVersion GenVersion = Experimental - 1\n)\n\ntype Generator struct {\n\tver       GenVersion\n\tspec      *openapi3.T\n\tmd        *meta.Data\n\tseenDecls map[string]uint32\n}\n\nfunc New(version GenVersion) *Generator {\n\treturn &Generator{\n\t\tver:       version,\n\t\tseenDecls: make(map[string]uint32),\n\t}\n}\n\nfunc (g *Generator) Version() int {\n\treturn int(g.ver)\n}\n\nfunc (g *Generator) Generate(p clientgentypes.GenerateParams) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif b, ok := r.(bailout); ok {\n\t\t\t\terr = b.err\n\t\t\t} else {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t}()\n\n\tg.md = p.Meta\n\tg.spec = newSpec(p.AppSlug)\n\n\tfor _, svc := range p.Meta.Svcs {\n\t\tif p.Services.Has(svc.Name) {\n\t\t\tif err := g.addService(svc, p.Tags, p.Options); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tout, err := g.spec.MarshalJSON()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"marshal openapi spec\")\n\t}\n\n\t// Pretty-print the JSON output\n\treturn json.Indent(p.Buf, out, \"\", \"  \")\n}\n\nfunc (g *Generator) addService(svc *meta.Service, tags clientgentypes.TagSet, opts clientgentypes.Options) error {\n\tfor _, rpc := range svc.Rpcs {\n\t\t// streaming endpoints not supported yet\n\t\tif rpc.StreamingRequest || rpc.StreamingResponse {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip private endpoints if the flag is set\n\t\tif opts.OpenAPIExcludePrivateEndpoints && rpc.AccessType == meta.RPC_PRIVATE {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip RPCs that don't match the tags\n\t\tif !tags.IsRPCIncluded(rpc) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := g.addRPC(rpc); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (g *Generator) addRPC(rpc *meta.RPC) error {\n\titem := g.getOrCreatePath(rpc)\n\n\tencodings, err := encoding.DescribeRPC(g.md, rpc, &encoding.Options{})\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"describe rpc %s.%s\", rpc.ServiceName, rpc.Name)\n\t}\n\n\tfor _, reqEnc := range encodings.RequestEncoding {\n\t\tfor _, m := range reqEnc.HTTPMethods {\n\t\t\top, err := g.newOperationForEncoding(rpc, m, reqEnc, encodings.ResponseEncoding)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"create operation for rpc %s.%s\", rpc.ServiceName, rpc.Name)\n\t\t\t}\n\t\t\titem.SetOperation(m, op)\n\t\t}\n\t}\n\n\tg.spec.Paths[rpcPath(rpc)] = item\n\treturn nil\n}\n\nfunc (g *Generator) getOrCreatePath(rpc *meta.RPC) *openapi3.PathItem {\n\tpath := rpcPath(rpc)\n\tif existing, ok := g.spec.Paths[path]; ok {\n\t\treturn existing\n\t}\n\titem := &openapi3.PathItem{}\n\tg.spec.Paths[path] = item\n\treturn item\n}\n\nfunc (g *Generator) newOperationForEncoding(rpc *meta.RPC, method string, reqEnc *encoding.RequestEncoding, respEnc *encoding.ResponseEncoding) (*openapi3.Operation, error) {\n\tsummary, desc := \"\", \"\"\n\tif rpc.Doc != nil {\n\t\tsummary, desc = splitDoc(*rpc.Doc)\n\t}\n\top := &openapi3.Operation{\n\t\tSummary:     summary,\n\t\tDescription: desc,\n\t\tOperationID: method + \":\" + rpc.ServiceName + \".\" + rpc.Name,\n\t\tResponses:   make(openapi3.Responses),\n\t}\n\n\t// Add path parameters\n\tfor _, seg := range rpc.Path.Segments {\n\t\tif seg.Type == meta.PathSegment_LITERAL {\n\t\t\tcontinue\n\t\t}\n\n\t\top.Parameters = append(op.Parameters, &openapi3.ParameterRef{\n\t\t\tValue: &openapi3.Parameter{\n\t\t\t\tName:            seg.Value,\n\t\t\t\tIn:              openapi3.ParameterInPath,\n\t\t\t\tDescription:     \"\",\n\t\t\t\tStyle:           openapi3.SerializationSimple,\n\t\t\t\tExplode:         ptr(false),\n\t\t\t\tAllowEmptyValue: true,\n\t\t\t\tAllowReserved:   false,\n\t\t\t\tDeprecated:      false,\n\t\t\t\tRequired:        true,\n\t\t\t\tSchema:          g.pathParamType(seg.ValueType).NewRef(),\n\t\t\t\tExample:         nil,\n\t\t\t\tExamples:        nil,\n\t\t\t\tContent:         nil,\n\t\t\t},\n\t\t})\n\t}\n\n\t// Add header parameters\n\tfor _, param := range reqEnc.HeaderParameters {\n\t\top.Parameters = append(op.Parameters, &openapi3.ParameterRef{\n\t\t\tValue: &openapi3.Parameter{\n\t\t\t\tName:            param.WireFormat,\n\t\t\t\tIn:              openapi3.ParameterInHeader,\n\t\t\t\tDescription:     markdownDoc(param.Doc),\n\t\t\t\tStyle:           openapi3.SerializationSimple,\n\t\t\t\tExplode:         ptr(true),\n\t\t\t\tAllowEmptyValue: true,\n\t\t\t\tAllowReserved:   false,\n\t\t\t\tDeprecated:      false,\n\t\t\t\tRequired:        !param.Optional,\n\t\t\t\tSchema:          g.schemaType(param.Type),\n\t\t\t\tExample:         nil,\n\t\t\t\tExamples:        nil,\n\t\t\t\tContent:         nil,\n\t\t\t},\n\t\t})\n\t}\n\n\t// Add query parameters\n\tfor _, param := range reqEnc.QueryParameters {\n\t\top.Parameters = append(op.Parameters, &openapi3.ParameterRef{\n\t\t\tValue: &openapi3.Parameter{\n\t\t\t\tName:            param.WireFormat,\n\t\t\t\tIn:              openapi3.ParameterInQuery,\n\t\t\t\tDescription:     markdownDoc(param.Doc),\n\t\t\t\tStyle:           openapi3.SerializationForm,\n\t\t\t\tExplode:         ptr(true),\n\t\t\t\tAllowEmptyValue: true,\n\t\t\t\tAllowReserved:   false,\n\t\t\t\tDeprecated:      false,\n\t\t\t\tRequired:        !param.Optional,\n\t\t\t\tSchema:          g.schemaType(param.Type),\n\t\t\t\tExample:         nil,\n\t\t\t\tExamples:        nil,\n\t\t\t\tContent:         nil,\n\t\t\t},\n\t\t})\n\t}\n\n\t// Add request body\n\tif len(reqEnc.BodyParameters) > 0 {\n\t\top.RequestBody = &openapi3.RequestBodyRef{\n\t\t\tValue: &openapi3.RequestBody{\n\t\t\t\tDescription: \"\",\n\t\t\t\tRequired:    false,\n\t\t\t\tContent:     g.bodyContent(reqEnc.BodyParameters),\n\t\t\t},\n\t\t}\n\t}\n\n\t// Encode the response\n\t{\n\t\tresp := &openapi3.Response{\n\t\t\tHeaders:     make(openapi3.Headers),\n\t\t\tLinks:       nil,\n\t\t\tDescription: ptr(\"Success response\"),\n\t\t}\n\n\t\tif respEnc != nil {\n\t\t\tfor _, param := range respEnc.HeaderParameters {\n\t\t\t\tresp.Headers[param.WireFormat] = &openapi3.HeaderRef{\n\t\t\t\t\tValue: &openapi3.Header{Parameter: openapi3.Parameter{\n\t\t\t\t\t\tDescription:     markdownDoc(param.Doc),\n\t\t\t\t\t\tStyle:           openapi3.SerializationSimple,\n\t\t\t\t\t\tExplode:         ptr(true),\n\t\t\t\t\t\tAllowEmptyValue: true,\n\t\t\t\t\t\tAllowReserved:   false,\n\t\t\t\t\t\tDeprecated:      false,\n\t\t\t\t\t\tRequired:        !param.Optional,\n\t\t\t\t\t\tSchema:          g.schemaType(param.Type),\n\t\t\t\t\t\tExample:         nil,\n\t\t\t\t\t\tExamples:        nil,\n\t\t\t\t\t\tContent:         nil,\n\t\t\t\t\t}},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(respEnc.BodyParameters) > 0 {\n\t\t\t\tresp.Content = g.bodyContent(respEnc.BodyParameters)\n\t\t\t}\n\t\t}\n\n\t\top.Responses[\"200\"] = &openapi3.ResponseRef{\n\t\t\tValue: resp,\n\t\t}\n\t\top.Responses[\"default\"] = &openapi3.ResponseRef{\n\t\t\tRef: \"#/components/responses/APIError\",\n\t\t}\n\t}\n\n\treturn op, nil\n}\n\nfunc rpcPath(rpc *meta.RPC) string {\n\tvar b strings.Builder\n\tfor _, seg := range rpc.Path.Segments {\n\t\tb.WriteString(\"/\")\n\t\tswitch seg.Type {\n\t\tcase meta.PathSegment_LITERAL:\n\t\t\tb.WriteString(seg.Value)\n\t\tdefault:\n\t\t\tb.WriteString(\"{\")\n\t\t\tb.WriteString(seg.Value)\n\t\t\tb.WriteString(\"}\")\n\t\t}\n\t}\n\treturn b.String()\n}\n\nfunc splitDoc(doc string) (plaintextSummary, markdownDescription string) {\n\tfirstLine, remaining := doc, \"\"\n\tif idx := strings.Index(doc, \"\\n\"); idx >= 0 {\n\t\tfirstLine = doc[:idx]\n\t\tremaining = doc[idx+1:]\n\t}\n\n\treturn plaintextDoc(firstLine), markdownDoc(remaining)\n}\n\nfunc plaintextDoc(doc string) string {\n\tvar parser comment.Parser\n\tvar pr comment.Printer\n\td := parser.Parse(doc)\n\treturn string(pr.Text(d))\n}\n\nfunc markdownDoc(doc string) string {\n\tvar parser comment.Parser\n\tvar pr comment.Printer\n\td := parser.Parse(doc)\n\treturn string(pr.Markdown(d))\n}\n\nfunc ptr[T any](t T) *T {\n\treturn &t\n}\n\nfunc newSpec(appSlug string) *openapi3.T {\n\tt := &openapi3.T{\n\t\tComponents: &openapi3.Components{\n\t\t\tRequestBodies:   make(map[string]*openapi3.RequestBodyRef),\n\t\t\tResponses:       make(map[string]*openapi3.ResponseRef),\n\t\t\tSecuritySchemes: make(map[string]*openapi3.SecuritySchemeRef),\n\t\t\tSchemas:         make(map[string]*openapi3.SchemaRef),\n\t\t},\n\t\tInfo: &openapi3.Info{\n\t\t\tTitle:       fmt.Sprintf(\"API for %s\", appSlug),\n\t\t\tDescription: \"Generated by encore\",\n\t\t\tVersion:     \"1\",\n\t\t\tExtensions: map[string]any{\n\t\t\t\t\"x-logo\": map[string]string{\n\t\t\t\t\t\"url\":             \"https://encore.dev/assets/branding/logo/logo-black.png\",\n\t\t\t\t\t\"backgroundColor\": \"#EEEEE1\",\n\t\t\t\t\t\"altText\":         \"Encore logo\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tOpenAPI: \"3.0.0\",\n\t\tPaths:   make(openapi3.Paths),\n\t}\n\n\t// Add the local platform server:\n\tt.AddServer(\n\t\t&openapi3.Server{\n\t\t\tURL:         \"http://localhost:4000\",\n\t\t\tDescription: \"Encore local dev environment\",\n\t\t},\n\t)\n\n\tt.Components.Responses[\"APIError\"] = &openapi3.ResponseRef{\n\t\tValue: &openapi3.Response{\n\t\t\tContent: openapi3.Content{\n\t\t\t\t\"application/json\": &openapi3.MediaType{\n\t\t\t\t\tSchema: &openapi3.SchemaRef{\n\t\t\t\t\t\tValue: &openapi3.Schema{\n\t\t\t\t\t\t\tType:  openapi3.TypeObject,\n\t\t\t\t\t\t\tTitle: \"APIError\",\n\t\t\t\t\t\t\tExternalDocs: &openapi3.ExternalDocs{\n\t\t\t\t\t\t\t\tURL: \"https://pkg.go.dev/encore.dev/beta/errs#Error\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tProperties: map[string]*openapi3.SchemaRef{\n\t\t\t\t\t\t\t\t\"code\": {\n\t\t\t\t\t\t\t\t\tValue: &openapi3.Schema{\n\t\t\t\t\t\t\t\t\t\tDescription: \"Error code\",\n\t\t\t\t\t\t\t\t\t\tExample:     \"not_found\",\n\t\t\t\t\t\t\t\t\t\tType:        openapi3.TypeString,\n\t\t\t\t\t\t\t\t\t\tExternalDocs: &openapi3.ExternalDocs{\n\t\t\t\t\t\t\t\t\t\t\tURL: \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"message\": {\n\t\t\t\t\t\t\t\t\tValue: &openapi3.Schema{\n\t\t\t\t\t\t\t\t\t\tDescription: \"Error message\",\n\t\t\t\t\t\t\t\t\t\tType:        openapi3.TypeString,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"details\": {\n\t\t\t\t\t\t\t\t\tValue: &openapi3.Schema{\n\t\t\t\t\t\t\t\t\t\tDescription: \"Error details\",\n\t\t\t\t\t\t\t\t\t\tType:        openapi3.TypeObject,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tDescription: ptr(\"Error response\"),\n\t\t},\n\t}\n\n\treturn t\n}\n\ntype bailout struct {\n\terr error\n}\n\nfunc doBailout(err error) {\n\tpanic(bailout{err})\n}\n"
  },
  {
    "path": "pkg/clientgen/openapi/schema.go",
    "content": "package openapi\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/getkin/kin-openapi/openapi3\"\n\n\t\"encr.dev/parser/encoding\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\nfunc (g *Generator) bodyContent(params []*encoding.ParameterEncoding) openapi3.Content {\n\tif len(params) == 0 {\n\t\treturn nil\n\t}\n\n\trequired := make([]string, 0, len(params))\n\tprops := make(openapi3.Schemas)\n\tfor _, p := range params {\n\t\tval := g.schemaType(p.Type)\n\t\tif vv := val.Value; vv != nil {\n\t\t\tvv.Title, vv.Description = splitDoc(p.Doc)\n\t\t}\n\t\tprops[p.WireFormat] = val\n\t\tif !p.Optional {\n\t\t\trequired = append(required, p.WireFormat)\n\t\t}\n\t}\n\n\ts := openapi3.NewObjectSchema()\n\ts.Properties = props\n\ts.Required = required\n\n\treturn openapi3.Content{\n\t\t\"application/json\": &openapi3.MediaType{\n\t\t\tSchema:   s.NewRef(),\n\t\t\tExample:  nil,\n\t\t\tExamples: nil,\n\t\t\tEncoding: nil,\n\t\t},\n\t}\n}\n\nfunc (g *Generator) schemaType(typ *schema.Type) *openapi3.SchemaRef {\n\tswitch t := typ.Typ.(type) {\n\t// A type switch for all the different schema types we support\n\tcase *schema.Type_Named:\n\t\treturn g.namedSchemaType(t.Named)\n\n\tcase *schema.Type_Struct:\n\t\tprops := make(openapi3.Schemas)\n\t\trequired := make([]string, 0, len(t.Struct.Fields))\n\t\tfor _, f := range t.Struct.Fields {\n\t\t\tjsonName := f.JsonName\n\t\t\tif jsonName == \"-\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif jsonName == \"\" {\n\t\t\t\tjsonName = f.Name\n\t\t\t}\n\t\t\tif !f.Optional {\n\t\t\t\trequired = append(required, jsonName)\n\t\t\t}\n\n\t\t\tval := g.schemaType(f.Typ)\n\n\t\t\tif vv := val.Value; vv != nil {\n\t\t\t\t// Direct schema - can set title and description directly\n\t\t\t\tvv.Title, vv.Description = splitDoc(f.Doc)\n\t\t\t} else if val.Ref != \"\" && f.Doc != \"\" {\n\t\t\t\t// Schema reference with field documentation - use allOf pattern to add description\n\t\t\t\t// This is the recommended workaround for OpenAPI 3.0 to add descriptions to $ref\n\t\t\t\t// See: https://github.com/OAI/OpenAPI-Specification/issues/2033\n\t\t\t\t// OpenAPI 3.1 supports description alongside $ref directly, but we use a library that doesn't support 3.1 yet\n\t\t\t\ttitle, description := splitDoc(f.Doc)\n\t\t\t\tval = &openapi3.SchemaRef{\n\t\t\t\t\tValue: &openapi3.Schema{\n\t\t\t\t\t\tAllOf:       []*openapi3.SchemaRef{val},\n\t\t\t\t\t\tTitle:       title,\n\t\t\t\t\t\tDescription: description,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t\tprops[jsonName] = val\n\t\t}\n\n\t\ts := openapi3.NewObjectSchema()\n\t\ts.Properties = props\n\t\ts.Required = required\n\t\treturn s.NewRef()\n\n\tcase *schema.Type_Map:\n\t\t// TODO non-string keys are not supported\n\t\ts := openapi3.NewObjectSchema()\n\t\ts.AdditionalProperties = openapi3.AdditionalProperties{\n\t\t\tSchema: g.schemaType(t.Map.Value),\n\t\t}\n\t\treturn s.NewRef()\n\n\tcase *schema.Type_List:\n\t\tarr := openapi3.NewArraySchema()\n\t\tarr.Items = g.schemaType(t.List.Elem)\n\t\treturn arr.NewRef()\n\n\tcase *schema.Type_Pointer:\n\t\treturn g.schemaType(t.Pointer.Base)\n\n\tcase *schema.Type_Option:\n\t\treturn g.schemaType(t.Option.Value)\n\n\tcase *schema.Type_Literal:\n\t\tswitch tt := t.Literal.Value.(type) {\n\t\tcase *schema.Literal_Str:\n\t\t\treturn openapi3.NewStringSchema().WithEnum(tt.Str).NewRef()\n\t\tcase *schema.Literal_Boolean:\n\t\t\treturn openapi3.NewBoolSchema().WithEnum(tt.Boolean).NewRef()\n\t\tcase *schema.Literal_Int:\n\t\t\treturn openapi3.NewInt64Schema().WithEnum(tt.Int).NewRef()\n\t\tcase *schema.Literal_Float:\n\t\t\treturn openapi3.NewFloat64Schema().WithEnum(tt.Float).NewRef()\n\t\tcase *schema.Literal_Null:\n\t\t\t// This shouldn't happen in most situations as we handle literals explicitly.\n\t\t\treturn openapi3.NewBoolSchema().WithNullable().NewRef()\n\t\tdefault:\n\t\t\tdoBailout(errors.Newf(\"unknown literal type %T\", tt))\n\t\t\treturn nil // unreachable\n\t\t}\n\n\tcase *schema.Type_Union:\n\t\t// First check if all the fields are literals.\n\t\t// If so we can more accurately represent this union as an enum.\n\t\tvar (\n\t\t\tliterals        []any\n\t\t\tliteralsType    string\n\t\t\thaveAllLiterals = true\n\t\t\thaveLiteralNull bool\n\t\t)\n\t\tfor _, tt := range t.Union.Types {\n\t\t\tlit, ok := tt.Typ.(*schema.Type_Literal)\n\t\t\tif !ok {\n\t\t\t\t// It's not a literal.\n\t\t\t\t// Still need to keep going to find any nulls.\n\t\t\t\thaveAllLiterals = false\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar litType string\n\t\t\tswitch tt := lit.Literal.Value.(type) {\n\t\t\tcase *schema.Literal_Str:\n\t\t\t\tlitType = openapi3.TypeString\n\t\t\t\tliterals = append(literals, tt.Str)\n\t\t\tcase *schema.Literal_Boolean:\n\t\t\t\tlitType = openapi3.TypeBoolean\n\t\t\t\tliterals = append(literals, tt.Boolean)\n\t\t\tcase *schema.Literal_Int:\n\t\t\t\tlitType = openapi3.TypeInteger\n\t\t\t\tliterals = append(literals, tt.Int)\n\t\t\tcase *schema.Literal_Float:\n\t\t\t\tlitType = openapi3.TypeNumber\n\t\t\t\tliterals = append(literals, tt.Float)\n\t\t\tcase *schema.Literal_Null:\n\t\t\t\thaveLiteralNull = true\n\t\t\t\tcontinue\n\t\t\tdefault:\n\t\t\t\tdoBailout(errors.Newf(\"unknown literal type %T\", tt))\n\t\t\t}\n\n\t\t\t// Set the literals type if we haven't seen it yet.\n\t\t\tif literalsType == \"\" {\n\t\t\t\tliteralsType = litType\n\t\t\t} else if literalsType != litType {\n\t\t\t\t// If we have different types, it can't be represented as an enum.\n\t\t\t\thaveAllLiterals = false\n\t\t\t}\n\t\t}\n\n\t\tif haveAllLiterals {\n\t\t\ts := openapi3.NewSchema()\n\t\t\ts.Type = literalsType\n\t\t\ts.Nullable = haveLiteralNull\n\t\t\treturn s.WithEnum(literals...).NewRef()\n\t\t}\n\n\t\t// Otherwise, we have to represent this as an anyOf schema.\n\t\tschemaRefs := make([]*openapi3.SchemaRef, 0, len(t.Union.Types))\n\t\tfor _, tt := range t.Union.Types {\n\t\t\tschemaRefs = append(schemaRefs, g.schemaType(tt))\n\t\t}\n\n\t\ts := openapi3.NewSchema()\n\t\ts.AnyOf = schemaRefs\n\t\ts.Nullable = haveLiteralNull\n\t\treturn s.NewRef()\n\n\tcase *schema.Type_TypeParameter:\n\t\treturn openapi3.NewObjectSchema().NewRef() // unknown\n\n\tcase *schema.Type_Config:\n\t\telem := g.schemaType(t.Config.Elem)\n\t\tif t.Config.IsValuesList {\n\t\t\ts := openapi3.NewArraySchema()\n\t\t\ts.Items = elem\n\t\t\treturn s.NewRef()\n\t\t} else {\n\t\t\treturn elem\n\t\t}\n\n\tcase *schema.Type_Builtin:\n\t\treturn g.builtinSchemaType(t.Builtin).NewRef()\n\n\tdefault:\n\t\tdoBailout(errors.Newf(\"unknown schema type %T\", t))\n\t\tpanic(\"unreachable\")\n\t}\n}\n\nfunc (g *Generator) builtinSchemaType(t schema.Builtin) *openapi3.Schema {\n\tswitch t {\n\tcase schema.Builtin_BOOL:\n\t\treturn openapi3.NewBoolSchema()\n\tcase schema.Builtin_INT8:\n\t\treturn openapi3.NewInt32Schema().WithMin(math.MinInt8).WithMax(math.MaxInt8)\n\tcase schema.Builtin_INT16:\n\t\treturn openapi3.NewInt32Schema().WithMin(math.MinInt16).WithMax(math.MaxInt16)\n\tcase schema.Builtin_INT32:\n\t\treturn openapi3.NewInt32Schema().WithMin(math.MinInt32).WithMax(math.MaxInt32)\n\tcase schema.Builtin_INT64, schema.Builtin_INT:\n\t\treturn openapi3.NewInt64Schema()\n\tcase schema.Builtin_UINT8:\n\t\treturn openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint8)\n\tcase schema.Builtin_UINT16:\n\t\treturn openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint16)\n\tcase schema.Builtin_UINT32:\n\t\treturn openapi3.NewInt64Schema().WithMin(0).WithMax(math.MaxUint32)\n\tcase schema.Builtin_UINT64, schema.Builtin_UINT:\n\t\treturn openapi3.NewInt64Schema().WithMin(0)\n\tcase schema.Builtin_FLOAT32, schema.Builtin_FLOAT64:\n\t\treturn openapi3.NewFloat64Schema()\n\tcase schema.Builtin_STRING:\n\t\treturn openapi3.NewStringSchema()\n\tcase schema.Builtin_BYTES:\n\t\treturn openapi3.NewStringSchema().WithFormat(\"byte\")\n\tcase schema.Builtin_TIME:\n\t\treturn openapi3.NewStringSchema().WithFormat(\"date-time\")\n\tcase schema.Builtin_UUID:\n\t\treturn openapi3.NewUUIDSchema()\n\tcase schema.Builtin_JSON:\n\t\treturn openapi3.NewObjectSchema()\n\tcase schema.Builtin_USER_ID:\n\t\treturn openapi3.NewStringSchema()\n\tcase schema.Builtin_DECIMAL:\n\t\treturn openapi3.NewStringSchema()\n\tdefault:\n\t\tdoBailout(errors.Newf(\"unknown builtin type %v\", t))\n\t\tpanic(\"unreachable\")\n\t}\n}\n\nfunc (g *Generator) namedSchemaType(typ *schema.Named) *openapi3.SchemaRef {\n\tnamedType := &schema.Type{Typ: &schema.Type_Named{Named: typ}}\n\tconcrete, err := encoding.GetConcreteType(g.md.Decls, namedType, nil)\n\tif err != nil {\n\t\tdoBailout(errors.Wrap(err, \"get concrete type\"))\n\t}\n\n\torigCandidate := g.typeToDefinitionName(namedType)\n\n\t// Make sure the candidate name corresponds to this declaration.\n\tfor idx := 1; ; idx++ {\n\t\tcandidate := origCandidate\n\t\t// Add a suffix if this is not the first candidate.\n\t\tif idx > 1 {\n\t\t\tcandidate += fmt.Sprintf(\"_%d\", idx)\n\t\t}\n\n\t\tif _, ok := g.spec.Components.Schemas[candidate]; ok {\n\t\t\t// There is already a declaration with that name; make sure it matches\n\t\t\tif seen, ok := g.seenDecls[candidate]; ok && seen != typ.Id {\n\t\t\t\t// Different declaration; try again.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\t// Unused name; allocate it.\n\t\t\t// Write to the maps before we compute the schema to avoid infinite recursion\n\t\t\t// in the presence of recursive types.\n\t\t\tg.seenDecls[candidate] = typ.Id\n\t\t\tg.spec.Components.Schemas[candidate] = nil\n\n\t\t\t// Generate the schema and add the declaration's documentation\n\t\t\tschemaRef := g.schemaType(concrete)\n\t\t\tif schemaRef.Value != nil {\n\t\t\t\t// Get the declaration to access its documentation\n\t\t\t\tif decl := g.md.Decls[typ.Id]; decl != nil && decl.Doc != \"\" {\n\t\t\t\t\ttitle, description := splitDoc(decl.Doc)\n\t\t\t\t\tif schemaRef.Value.Title == \"\" {\n\t\t\t\t\t\tschemaRef.Value.Title = title\n\t\t\t\t\t}\n\t\t\t\t\tif schemaRef.Value.Description == \"\" {\n\t\t\t\t\t\tschemaRef.Value.Description = description\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tg.spec.Components.Schemas[candidate] = schemaRef\n\t\t}\n\n\t\treturn &openapi3.SchemaRef{\n\t\t\tRef: \"#/components/schemas/\" + candidate,\n\t\t}\n\t}\n}\n\nfunc (g *Generator) typeToDefinitionName(typ *schema.Type) string {\n\tswitch typ := typ.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\tvar name strings.Builder\n\t\tdecl := g.md.Decls[typ.Named.Id]\n\t\tname.WriteString(decl.Loc.PkgName)\n\t\tname.WriteString(\".\")\n\t\tname.WriteString(decl.Name)\n\t\tfor _, typeArg := range typ.Named.TypeArguments {\n\t\t\tname.WriteString(\"_\")\n\t\t\tname.WriteString(g.typeToDefinitionName(typeArg))\n\t\t}\n\t\treturn name.String()\n\tcase *schema.Type_List:\n\t\treturn \"List_\" + g.typeToDefinitionName(typ.List.Elem)\n\tcase *schema.Type_Map:\n\t\treturn \"Map_\" + g.typeToDefinitionName(typ.Map.Key) + \"_\" + g.typeToDefinitionName(typ.Map.Value)\n\tcase *schema.Type_Pointer:\n\t\treturn g.typeToDefinitionName(typ.Pointer.Base)\n\tcase *schema.Type_Option:\n\t\treturn \"Option_\" + g.typeToDefinitionName(typ.Option.Value)\n\tcase *schema.Type_Config:\n\t\treturn g.typeToDefinitionName(typ.Config.Elem)\n\tcase *schema.Type_Builtin:\n\t\tswitch typ.Builtin {\n\t\tcase schema.Builtin_ANY:\n\t\t\treturn \"any\"\n\t\tcase schema.Builtin_BOOL:\n\t\t\treturn \"bool\"\n\t\tcase schema.Builtin_INT8:\n\t\t\treturn \"int8\"\n\t\tcase schema.Builtin_INT16:\n\t\t\treturn \"int16\"\n\t\tcase schema.Builtin_INT32:\n\t\t\treturn \"int32\"\n\t\tcase schema.Builtin_INT64:\n\t\t\treturn \"int64\"\n\t\tcase schema.Builtin_UINT8:\n\t\t\treturn \"uint8\"\n\t\tcase schema.Builtin_UINT16:\n\t\t\treturn \"uint16\"\n\t\tcase schema.Builtin_UINT32:\n\t\t\treturn \"uint32\"\n\t\tcase schema.Builtin_UINT64:\n\t\t\treturn \"uint64\"\n\t\tcase schema.Builtin_FLOAT32:\n\t\t\treturn \"float32\"\n\t\tcase schema.Builtin_FLOAT64:\n\t\t\treturn \"float64\"\n\t\tcase schema.Builtin_STRING:\n\t\t\treturn \"string\"\n\t\tcase schema.Builtin_BYTES:\n\t\t\treturn \"bytes\"\n\t\tcase schema.Builtin_TIME:\n\t\t\treturn \"string\"\n\t\tcase schema.Builtin_UUID:\n\t\t\treturn \"string\"\n\t\tcase schema.Builtin_JSON:\n\t\t\treturn \"string\"\n\t\tcase schema.Builtin_USER_ID:\n\t\t\treturn \"string\"\n\t\tcase schema.Builtin_INT:\n\t\t\treturn \"int\"\n\t\tcase schema.Builtin_UINT:\n\t\t\treturn \"uint\"\n\t\tcase schema.Builtin_DECIMAL:\n\t\t\treturn \"string\"\n\t\tdefault:\n\t\t\treturn \"\"\n\t\t}\n\n\tcase *schema.Type_Literal:\n\t\tswitch typ.Literal.Value.(type) {\n\t\tcase *schema.Literal_Boolean:\n\t\t\treturn \"bool\"\n\t\tcase *schema.Literal_Str:\n\t\t\treturn \"string\"\n\t\tcase *schema.Literal_Null:\n\t\t\treturn \"null\"\n\t\tcase *schema.Literal_Int:\n\t\t\treturn \"int\"\n\t\tcase *schema.Literal_Float:\n\t\t\treturn \"float64\"\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc (g *Generator) pathParamType(typ meta.PathSegment_ParamType) *openapi3.Schema {\n\tswitch typ {\n\tcase meta.PathSegment_BOOL:\n\t\treturn openapi3.NewBoolSchema()\n\tcase meta.PathSegment_INT8:\n\t\treturn openapi3.NewInt32Schema().WithMin(math.MinInt8).WithMax(math.MaxInt8)\n\tcase meta.PathSegment_INT16:\n\t\treturn openapi3.NewInt32Schema().WithMin(math.MinInt16).WithMax(math.MaxInt16)\n\tcase meta.PathSegment_INT32:\n\t\treturn openapi3.NewInt32Schema().WithMin(math.MinInt32).WithMax(math.MaxInt32)\n\tcase meta.PathSegment_INT64, meta.PathSegment_INT:\n\t\treturn openapi3.NewInt64Schema()\n\tcase meta.PathSegment_UINT8:\n\t\treturn openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint8)\n\tcase meta.PathSegment_UINT16:\n\t\treturn openapi3.NewInt32Schema().WithMin(0).WithMax(math.MaxUint16)\n\tcase meta.PathSegment_UINT32:\n\t\treturn openapi3.NewInt64Schema().WithMin(0).WithMax(math.MaxUint32)\n\tcase meta.PathSegment_UINT64, meta.PathSegment_UINT:\n\t\treturn openapi3.NewInt64Schema().WithMin(0)\n\tcase meta.PathSegment_STRING:\n\t\treturn openapi3.NewStringSchema()\n\tcase meta.PathSegment_UUID:\n\t\treturn openapi3.NewUUIDSchema()\n\tdefault:\n\t\tdoBailout(errors.Newf(\"unknown path param type: %v\"))\n\t\tpanic(\"unreachable\")\n\t}\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/README.md",
    "content": "# Update Golden Tests\n\nInstead of manually updating the golden tests, once you've verified the output of the tests is correct, then you\ncan simply update all the `expected output files` files by running\n\n```bash\ngo test ./pkg/clientgen -golden-update\n```\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_baseauth_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tSvc SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{Svc: &svcClient{base}}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\n// WithAuthToken allows you to set an authentication token to be used for each request.\n//\n// This token will be sent as a Bearer token in the Authorization header.\nfunc WithAuthToken(bearerToken string) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = func(_ context.Context) (string, error) {\n\t\t\treturn bearerToken, nil\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithAuthFunc allows you to pass a function which is called for each request to return an authentication token to be used for each request.\n//\n// This token will be sent as a Bearer token in the Authorization header.\nfunc WithAuthFunc(authGenerator func(ctx context.Context) (string, error)) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = authGenerator\n\t\treturn nil\n\t}\n}\n\ntype SvcRequest struct {\n\tMessage string\n}\n\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\t// DummyAPI is a dummy endpoint.\n\tDummyAPI(ctx context.Context, params SvcRequest) error\n\n\t// Private is a basic auth endpoint.\n\tPrivate(ctx context.Context, params SvcRequest) error\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\n// DummyAPI is a dummy endpoint.\nfunc (c *svcClient) DummyAPI(ctx context.Context, params SvcRequest) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/svc.DummyAPI\", nil, params, nil)\n\treturn err\n}\n\n// Private is a basic auth endpoint.\nfunc (c *svcClient) Private(ctx context.Context, params SvcRequest) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/svc.Private\", nil, params, nil)\n\treturn err\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\tauthGenerator func(ctx context.Context) (string, error) // The function which will add the authentication data to the requests\n\thttpClient    HTTPDoer                                  // The HTTP client which will be used for all API requests\n\tbaseURL       *url.URL                                  // The base URL which API requests will be made against\n\tuserAgent     string                                    // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// If a authorization data generator is present, call it and add the returned token to the request\n\tif b.authGenerator != nil {\n\t\tif token, err := b.authGenerator(req.Context()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to create authorization token for api request: %w\", err)\n\t\t} else if token != \"\" {\n\t\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\t\t}\n\t}\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_baseauth_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        // Convert the old constructor parameters to a BaseURL object and a ClientOptions object\n        if (!target.startsWith(\"http://\") && !target.startsWith(\"https://\")) {\n            target = Environment(target)\n        }\n\n        if (typeof options === \"string\") {\n            options = { auth: options }\n        }\n\n        const base = new BaseClient(target, options ?? {})\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.DummyAPI = this.DummyAPI.bind(this)\n        this.Private = this.Private.bind(this)\n    }\n\n    /**\n     * DummyAPI is a dummy endpoint.\n     */\n    async DummyAPI(params) {\n        await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`, JSON.stringify(params))\n    }\n\n    /**\n     * Private is a basic auth endpoint.\n     */\n    async Private(params) {\n        await this.baseClient.callTypedAPI(\"POST\", `/svc.Private`, JSON.stringify(params))\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n\n    }\n\n    async getAuthData() {\n        let authData;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data = {};\n\n            data.headers = {};\n            data.headers[\"Authorization\"] = \"Bearer \" + authData;\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_baseauth_openapi.json",
    "content": "{\n  \"components\": {\n    \"responses\": {\n      \"APIError\": {\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"externalDocs\": {\n                \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#Error\"\n              },\n              \"properties\": {\n                \"code\": {\n                  \"description\": \"Error code\",\n                  \"example\": \"not_found\",\n                  \"externalDocs\": {\n                    \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\"\n                  },\n                  \"type\": \"string\"\n                },\n                \"details\": {\n                  \"description\": \"Error details\",\n                  \"type\": \"object\"\n                },\n                \"message\": {\n                  \"description\": \"Error message\",\n                  \"type\": \"string\"\n                }\n              },\n              \"title\": \"APIError\",\n              \"type\": \"object\"\n            }\n          }\n        },\n        \"description\": \"Error response\"\n      }\n    }\n  },\n  \"info\": {\n    \"description\": \"Generated by encore\",\n    \"title\": \"API for app\",\n    \"version\": \"1\",\n    \"x-logo\": {\n      \"altText\": \"Encore logo\",\n      \"backgroundColor\": \"#EEEEE1\",\n      \"url\": \"https://encore.dev/assets/branding/logo/logo-black.png\"\n    }\n  },\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/svc.DummyAPI\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.DummyAPI\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Message\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"Message\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        },\n        \"summary\": \"DummyAPI is a dummy endpoint.\\n\"\n      }\n    },\n    \"/svc.Private\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.Private\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Message\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"Message\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        },\n        \"summary\": \"Private is a basic auth endpoint.\\n\"\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"description\": \"Encore local dev environment\",\n      \"url\": \"http://localhost:4000\"\n    }\n  ]\n}"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_baseauth_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * @deprecated This constructor is deprecated, and you should move to using BaseURL with an Options object\n     */\n    constructor(target: string, token?: string)\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions)\n    constructor(target: string | BaseURL = \"prod\", options?: string | ClientOptions) {\n\n        // Convert the old constructor parameters to a BaseURL object and a ClientOptions object\n        if (!target.startsWith(\"http://\") && !target.startsWith(\"https://\")) {\n            target = Environment(target)\n        }\n\n        if (typeof options === \"string\") {\n            options = { auth: options }\n        }\n\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    /**\n     * Allows you to set the auth token to be used for each request\n     * either by passing in a static token string or by passing in a function\n     * which returns the auth token.\n     *\n     * These tokens will be sent as bearer tokens in the Authorization header.\n     */\n    auth?: string | AuthDataGenerator\n}\n\nexport namespace svc {\n    export interface Request {\n        Message: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.DummyAPI = this.DummyAPI.bind(this)\n            this.Private = this.Private.bind(this)\n        }\n\n        /**\n         * DummyAPI is a dummy endpoint.\n         */\n        public async DummyAPI(params: Request): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`, JSON.stringify(params))\n        }\n\n        /**\n         * Private is a basic auth endpoint.\n         */\n        public async Private(params: Request): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/svc.Private`, JSON.stringify(params))\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n// AuthDataGenerator is a function that returns a new instance of the authentication data required by this API\nexport type AuthDataGenerator = () =>\n  | string\n  | Promise<string | undefined>\n  | undefined;\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n    readonly authGenerator?: AuthDataGenerator\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        let authData: string | undefined;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data: CallParameters = {};\n\n            data.headers = {};\n            data.headers[\"Authorization\"] = \"Bearer \" + authData;\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tAuthentication AuthenticationClient\n\tProducts       ProductsClient\n\tSvc            SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{\n\t\tAuthentication: &authenticationClient{base},\n\t\tProducts:       &productsClient{base},\n\t\tSvc:            &svcClient{base},\n\t}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\n// WithAuth allows you to set the authentication data to be used with each request\nfunc WithAuth(auth AuthenticationAuthData) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = func(_ context.Context) (AuthenticationAuthData, error) {\n\t\t\treturn auth, nil\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithAuthFunc allows you to pass a function which is called for each request to return the authentication data to be used with each request\nfunc WithAuthFunc(authGenerator func(ctx context.Context) (AuthenticationAuthData, error)) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = authGenerator\n\t\treturn nil\n\t}\n}\n\ntype AuthenticationAuthData struct {\n\tAPIKey string `header:\"X-API-Key\"`\n}\n\n// BarType docs\ntype AuthenticationBarType struct {\n\tBaz string // Baz docs\n}\n\n// FooType docs\ntype AuthenticationFooType struct {\n\tMoo string                // Moo docs\n\tBar AuthenticationBarType // Bar docs\n}\n\ntype AuthenticationUser struct {\n\tID   int    `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\n// AuthenticationClient Provides you access to call public and authenticated APIs on authentication. The concrete implementation is authenticationClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype AuthenticationClient interface {\n\tDocs(ctx context.Context, params AuthenticationFooType) error\n}\n\ntype authenticationClient struct {\n\tbase *baseClient\n}\n\nvar _ AuthenticationClient = (*authenticationClient)(nil)\n\nfunc (c *authenticationClient) Docs(ctx context.Context, params AuthenticationFooType) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/authentication.Docs\", nil, params, nil)\n\treturn err\n}\n\ntype ProductsCreateProductRequest struct {\n\tIdempotencyKey string `header:\"Idempotency-Key\"`\n\tName           string `json:\"name\"`\n\tDescription    string `json:\"description,omitempty\"`\n}\n\ntype ProductsProduct struct {\n\tID          string              `json:\"id\"`\n\tName        string              `json:\"name\"`\n\tDescription string              `json:\"description,omitempty\"`\n\tCreatedAt   time.Time           `json:\"created_at\"`\n\tCreatedBy   *AuthenticationUser `json:\"created_by\"`\n}\n\ntype ProductsProductListing struct {\n\tProducts     []*ProductsProduct `json:\"products\"`\n\tPreviousPage struct {\n\t\tCursor string `json:\"cursor,omitempty\"`\n\t\tExists bool   `json:\"exists\"`\n\t} `json:\"previous\"`\n\tNextPage struct {\n\t\tCursor string `json:\"cursor,omitempty\"`\n\t\tExists bool   `json:\"exists\"`\n\t} `json:\"next\"`\n}\n\n// ProductsClient Provides you access to call public and authenticated APIs on products. The concrete implementation is productsClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype ProductsClient interface {\n\tCreate(ctx context.Context, params ProductsCreateProductRequest) (ProductsProduct, error)\n\tList(ctx context.Context) (ProductsProductListing, error)\n}\n\ntype productsClient struct {\n\tbase *baseClient\n}\n\nvar _ ProductsClient = (*productsClient)(nil)\n\nfunc (c *productsClient) Create(ctx context.Context, params ProductsCreateProductRequest) (resp ProductsProduct, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\"idempotency-key\": {reqEncoder.FromString(params.IdempotencyKey)}}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tName        string `json:\"name\"`\n\t\tDescription string `json:\"description,omitempty\"`\n\t}{\n\t\tDescription: params.Description,\n\t\tName:        params.Name,\n\t}\n\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/products.Create\", headers, body, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *productsClient) List(ctx context.Context) (resp ProductsProductListing, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", \"/products.List\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\ntype SvcAllInputTypes[A any] struct {\n\tA        time.Time `header:\"X-Alice\"`               // Specify this comes from a header field\n\tB        []int     `query:\"Bob\"`                    // Specify this comes from a query string\n\tC        bool      `json:\"Charlies-Bool,omitempty\"` // This can come from anywhere, but if it comes from the payload in JSON it must be called Charile\n\tDave     A         // This generic type complicates the whole thing 🙈\n\tOptional *A        `json:\"optional\"` // An optional generic type\n}\n\n// DocumentedOrder represents a customer order with references\ntype SvcDocumentedOrder struct {\n\tCustomer    SvcDocumentedUser  `json:\"customer\"` // Customer who placed this order (different from shipping recipient)\n\tOrderID     string             `json:\"order_id\"`\n\tOptionalRef *SvcDocumentedUser `json:\"opt_ref\"`\n\tRequiredRef *SvcDocumentedUser `json:\"req_ref\"`\n}\n\n// DocumentedUser represents a user in the system with profile information\ntype SvcDocumentedUser struct {\n\tName  string `json:\"name\"`\n\tEmail string `json:\"email\"`\n}\n\n// Foo represents a documented integer type\ntype SvcFoo = int\n\ntype SvcGetRequest struct {\n\tBaz int `qs:\"boo\"`\n}\n\n// HeaderOnlyStruct contains all types we support in headers\ntype SvcHeaderOnlyStruct struct {\n\tBoolean  bool            `header:\"x-boolean\"`\n\tInt      int             `header:\"x-int\"`\n\tFloat    float64         `header:\"x-float\"`\n\tString   string          `header:\"x-string\"`\n\tBytes    []byte          `header:\"x-bytes\"`\n\tTime     time.Time       `header:\"x-time\"`\n\tJson     json.RawMessage `header:\"x-json\"`\n\tUUID     string          `header:\"x-uuid\"`\n\tUserID   string          `header:\"x-user-id\"`\n\tOptional *string         `header:\"x-optional\"`\n}\n\ntype SvcRecursive struct {\n\tOptional        *SvcRecursive `encore:\"optional\"`\n\tSlice           []SvcRecursive\n\tSliceOfOptional []*SvcRecursive\n\tMap             map[string]SvcRecursive\n\tMapOfOptional   map[string]*SvcRecursive\n}\n\ntype SvcRequest struct {\n\tFoo       SvcFoo `encore:\"optional\"` // Foo is good\n\tBaz       string `json:\"boo\"`        // Baz is better\n\tQueryFoo  bool   `encore:\"optional\" query:\"foo\"`\n\tQueryBar  string `encore:\"optional\" query:\"bar\"`\n\tHeaderBaz string `encore:\"optional\" header:\"baz\"`\n\tHeaderInt int    `encore:\"optional\" header:\"int\"`\n\n\t// This is a multiline\n\t// comment on the raw message!\n\tRaw json.RawMessage\n}\n\n// Tuple is a generic type which allows us to\n// return two values of two different types\ntype SvcTuple[A any, B any] struct {\n\tA A\n\tB B\n}\n\ntype SvcWithNested struct {\n\tNested *NestedType\n}\n\ntype SvcWrappedRequest = SvcWrapper[SvcRequest]\n\ntype SvcWrapper[T any] struct {\n\tValue T\n}\n\n// Svc is a service for testing the client generator.\n//\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\tCreateDocumentedOrder(ctx context.Context, params SvcDocumentedOrder) (SvcDocumentedOrder, error)\n\n\t// DummyAPI is a dummy endpoint.\n\tDummyAPI(ctx context.Context, params SvcRequest) error\n\tFallbackPath(ctx context.Context, a string, b []string) error\n\tGet(ctx context.Context, params SvcGetRequest) error\n\tGetRequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[int]) (SvcHeaderOnlyStruct, error)\n\tHeaderOnlyRequest(ctx context.Context, params SvcHeaderOnlyStruct) error\n\tNested(ctx context.Context, params SvcWithNested) (SvcWithNested, error)\n\tRESTPath(ctx context.Context, a string, b int) error\n\tRec(ctx context.Context, params SvcRecursive) (SvcRecursive, error)\n\tRequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[string]) (SvcAllInputTypes[float64], error)\n\n\t// TupleInputOutput tests the usage of generics in the client generator\n\t// and this comment is also multiline, so multiline comments get tested as well.\n\tTupleInputOutput(ctx context.Context, params SvcTuple[string, SvcWrappedRequest]) (SvcTuple[bool, SvcFoo], error)\n\tWebhook(ctx context.Context, a string, b []string, request *http.Request) (*http.Response, error)\n\tWebhook2(ctx context.Context, a string, b []string) error\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\nfunc (c *svcClient) CreateDocumentedOrder(ctx context.Context, params SvcDocumentedOrder) (resp SvcDocumentedOrder, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/svc.CreateDocumentedOrder\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// DummyAPI is a dummy endpoint.\nfunc (c *svcClient) DummyAPI(ctx context.Context, params SvcRequest) error {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"baz\": {reqEncoder.FromString(params.HeaderBaz)},\n\t\t\"int\": {reqEncoder.FromInt(params.HeaderInt)},\n\t}\n\n\tqueryString := url.Values{\n\t\t\"bar\": {reqEncoder.FromString(params.QueryBar)},\n\t\t\"foo\": {reqEncoder.FromBool(params.QueryFoo)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\treturn fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tFoo SvcFoo          `json:\"Foo\"`\n\t\tBaz string          `json:\"boo\"`\n\t\tRaw json.RawMessage `json:\"Raw\"`\n\t}{\n\t\tBaz: params.Baz,\n\t\tFoo: params.Foo,\n\t\tRaw: params.Raw,\n\t}\n\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/svc.DummyAPI?%s\", queryString.Encode()), headers, body, nil)\n\treturn err\n}\n\nfunc (c *svcClient) FallbackPath(ctx context.Context, a string, b []string) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/fallbackPath/%s/%s\", url.PathEscape(a), pathEscapeSlice(b)), nil, nil, nil)\n\treturn err\n}\n\nfunc (c *svcClient) Get(ctx context.Context, params SvcGetRequest) error {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\tqueryString := url.Values{\"boo\": {reqEncoder.FromInt(params.Baz)}}\n\n\tif reqEncoder.LastError != nil {\n\t\treturn fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t}\n\n\t_, err := callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/svc.Get?%s\", queryString.Encode()), nil, nil, nil)\n\treturn err\n}\n\nfunc (c *svcClient) GetRequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[int]) (resp SvcHeaderOnlyStruct, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\"x-alice\": {reqEncoder.FromTime(params.A)}}\n\n\tqueryString := url.Values{\n\t\t\"Bob\":      reqEncoder.FromIntList(params.B),\n\t\t\"c\":        {reqEncoder.FromBool(params.C)},\n\t\t\"dave\":     {reqEncoder.FromInt(params.Dave)},\n\t\t\"optional\": reqEncoder.FromIntOption(params.Optional),\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Now make the actual call to the API\n\tvar respHeaders http.Header\n\trespHeaders, err = callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/svc.GetRequestWithAllInputTypes?%s\", queryString.Encode()), headers, nil, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Copy the unmarshalled response body into our response struct\n\trespDecoder := &serde{}\n\n\tresp.Boolean = respDecoder.ToBool(\"Boolean\", respHeaders.Get(\"x-boolean\"), true)\n\tresp.Int = respDecoder.ToInt(\"Int\", respHeaders.Get(\"x-int\"), true)\n\tresp.Float = respDecoder.ToFloat64(\"Float\", respHeaders.Get(\"x-float\"), true)\n\tresp.String = respDecoder.ToString(\"String\", respHeaders.Get(\"x-string\"), true)\n\tresp.Bytes = respDecoder.ToBytes(\"Bytes\", respHeaders.Get(\"x-bytes\"), true)\n\tresp.Time = respDecoder.ToTime(\"Time\", respHeaders.Get(\"x-time\"), true)\n\tresp.Json = respDecoder.ToJSON(\"Json\", respHeaders.Get(\"x-json\"), true)\n\tresp.UUID = respDecoder.ToString(\"UUID\", respHeaders.Get(\"x-uuid\"), true)\n\tresp.UserID = respDecoder.ToString(\"UserID\", respHeaders.Get(\"x-user-id\"), true)\n\tresp.Optional = respDecoder.ToStringOption(\"Optional\", respHeaders.Get(\"x-optional\"), false)\n\n\tif respDecoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to unmarshal headers: %w\", respDecoder.LastError)\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) HeaderOnlyRequest(ctx context.Context, params SvcHeaderOnlyStruct) error {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"x-boolean\":  {reqEncoder.FromBool(params.Boolean)},\n\t\t\"x-bytes\":    {reqEncoder.FromBytes(params.Bytes)},\n\t\t\"x-float\":    {reqEncoder.FromFloat64(params.Float)},\n\t\t\"x-int\":      {reqEncoder.FromInt(params.Int)},\n\t\t\"x-json\":     {reqEncoder.FromJSON(params.Json)},\n\t\t\"x-optional\": reqEncoder.FromStringOption(params.Optional),\n\t\t\"x-string\":   {reqEncoder.FromString(params.String)},\n\t\t\"x-time\":     {reqEncoder.FromTime(params.Time)},\n\t\t\"x-user-id\":  {reqEncoder.FromString(params.UserID)},\n\t\t\"x-uuid\":     {reqEncoder.FromString(params.UUID)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\treturn fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t}\n\n\t_, err := callAPI(ctx, c.base, \"GET\", \"/svc.HeaderOnlyRequest\", headers, nil, nil)\n\treturn err\n}\n\nfunc (c *svcClient) Nested(ctx context.Context, params SvcWithNested) (resp SvcWithNested, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/svc.Nested\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) RESTPath(ctx context.Context, a string, b int) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/path/%s/%d\", url.PathEscape(a), b), nil, nil, nil)\n\treturn err\n}\n\nfunc (c *svcClient) Rec(ctx context.Context, params SvcRecursive) (resp SvcRecursive, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/svc.Rec\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) RequestWithAllInputTypes(ctx context.Context, params SvcAllInputTypes[string]) (resp SvcAllInputTypes[float64], err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\"x-alice\": {reqEncoder.FromTime(params.A)}}\n\n\tqueryString := url.Values{\"Bob\": reqEncoder.FromIntList(params.B)}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tC        bool    `json:\"Charlies-Bool,omitempty\"`\n\t\tDave     string  `json:\"Dave\"`\n\t\tOptional *string `json:\"optional\"`\n\t}{\n\t\tC:        params.C,\n\t\tDave:     params.Dave,\n\t\tOptional: params.Optional,\n\t}\n\n\t// We only want the response body to marshal into these fields and none of the header fields,\n\t// so we'll construct a new struct with only those fields.\n\trespBody := struct {\n\t\tB        []int    `json:\"B\"`\n\t\tC        bool     `json:\"Charlies-Bool,omitempty\"`\n\t\tDave     float64  `json:\"Dave\"`\n\t\tOptional *float64 `json:\"optional\"`\n\t}{}\n\n\t// Now make the actual call to the API\n\tvar respHeaders http.Header\n\trespHeaders, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/svc.RequestWithAllInputTypes?%s\", queryString.Encode()), headers, body, &respBody)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Copy the unmarshalled response body into our response struct\n\trespDecoder := &serde{}\n\n\tresp.A = respDecoder.ToTime(\"A\", respHeaders.Get(\"x-alice\"), true)\n\tresp.B = respBody.B\n\tresp.C = respBody.C\n\tresp.Dave = respBody.Dave\n\tresp.Optional = respBody.Optional\n\n\tif respDecoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to unmarshal headers: %w\", respDecoder.LastError)\n\t\treturn\n\t}\n\n\treturn\n}\n\n// TupleInputOutput tests the usage of generics in the client generator\n// and this comment is also multiline, so multiline comments get tested as well.\nfunc (c *svcClient) TupleInputOutput(ctx context.Context, params SvcTuple[string, SvcWrappedRequest]) (resp SvcTuple[bool, SvcFoo], err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/svc.TupleInputOutput\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) Webhook(ctx context.Context, a string, b []string, request *http.Request) (*http.Response, error) {\n\trequest = request.WithContext(ctx)\n\n\t// Check the request has the method set, as we can't guess what method is required\n\tif request.Method == \"\" {\n\t\treturn nil, errors.New(\"request.Method must be set\")\n\t}\n\n\t// Set the relative URL for the API call\n\tpath, err := url.Parse(fmt.Sprintf(\"/webhook/%s/%s\", url.PathEscape(a), pathEscapeSlice(b)))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse api url: %w\", err)\n\t}\n\tif request.URL != nil {\n\t\t// If the request already has a URL associated, we'll keep any fields set inside it, and just override the schema,\n\t\t// host and path to ensure the final URL which hit the right BaseURL\n\t\trequest.URL.Scheme = path.Scheme\n\t\trequest.URL.Host = path.Host\n\t\trequest.URL.Path = path.Path\n\t} else {\n\t\trequest.URL = path\n\t}\n\n\treturn c.base.Do(request)\n}\n\nfunc (c *svcClient) Webhook2(ctx context.Context, a string, b []string) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/webhook2/%s/%s\", url.PathEscape(a), pathEscapeSlice(b)), nil, nil, nil)\n\treturn err\n}\n\ntype NestedType struct {\n\tMessage string\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\tauthGenerator func(ctx context.Context) (AuthenticationAuthData, error) // The function which will add the authentication data to the requests\n\thttpClient    HTTPDoer                                                  // The HTTP client which will be used for all API requests\n\tbaseURL       *url.URL                                                  // The base URL which API requests will be made against\n\tuserAgent     string                                                    // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// If a authorization data generator is present, call it and add the returned token to the request\n\tif b.authGenerator != nil {\n\t\tif authData, err := b.authGenerator(req.Context()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to create authorization token for api request: %w\", err)\n\t\t} else {\n\t\t\tauthEncoder := &serde{}\n\n\t\t\t// Add the auth fields to the headers\n\t\t\treq.Header.Set(\"x-api-key\", authEncoder.FromString(authData.APIKey))\n\n\t\t\tif authEncoder.LastError != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to marshal authentication data: %w\", authEncoder.LastError)\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// pathEscapeSlice escapes a slice of strings and then joins them into a single string\nfunc pathEscapeSlice(paths []string) string {\n\tvar escapedPaths strings.Builder\n\tfor i, path := range paths {\n\t\tif i > 0 {\n\t\t\tescapedPaths.WriteString(\"/\")\n\t\t}\n\t\tescapedPaths.WriteString(url.PathEscape(path))\n\t}\n\treturn escapedPaths.String()\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n\n// serde is used to serialize request data into strings and deserialize response data from strings\ntype serde struct {\n\tLastError      error // The last error that occurred\n\tNonEmptyValues int   // The number of values this decoder has decoded\n}\n\nfunc (e *serde) FromString(s string) (v string) {\n\te.NonEmptyValues++\n\treturn s\n}\n\nfunc (e *serde) FromInt(s int) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatInt(int64(s), 10)\n}\n\nfunc (e *serde) FromBool(s bool) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatBool(s)\n}\n\nfunc (e *serde) FromTime(s time.Time) (v string) {\n\te.NonEmptyValues++\n\treturn s.Format(time.RFC3339)\n}\n\nfunc (e *serde) FromIntList(s []int) (v []string) {\n\te.NonEmptyValues++\n\tfor _, x := range s {\n\t\tv = append(v, e.FromInt(x))\n\t}\n\treturn v\n}\n\nfunc (e *serde) FromIntOption(s *int) (v []string) {\n\tif s == nil {\n\t\treturn nil\n\t}\n\te.NonEmptyValues++\n\treturn []string{e.FromInt(*s)}\n}\n\nfunc (e *serde) ToBool(field string, s string, required bool) (v bool) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tv, err := strconv.ParseBool(s)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn v\n}\n\nfunc (e *serde) ToInt(field string, s string, required bool) (v int) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tx, err := strconv.ParseInt(s, 10, 64)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn int(x)\n}\n\nfunc (e *serde) ToFloat64(field string, s string, required bool) (v float64) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tx, err := strconv.ParseFloat(s, 64)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn x\n}\n\nfunc (e *serde) ToString(field string, s string, required bool) (v string) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\treturn s\n}\n\nfunc (e *serde) ToBytes(field string, s string, required bool) (v []byte) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tv, err := base64.URLEncoding.DecodeString(s)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn v\n}\n\nfunc (e *serde) ToTime(field string, s string, required bool) (v time.Time) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tv, err := time.Parse(time.RFC3339, s)\n\te.setErr(\"invalid parameter\", field, err)\n\treturn v\n}\n\nfunc (e *serde) ToJSON(field string, s string, required bool) (v json.RawMessage) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\treturn json.RawMessage(s)\n}\n\nfunc (e *serde) ToStringOption(field string, s string, required bool) (v *string) {\n\tif !required && s == \"\" {\n\t\treturn\n\t}\n\te.NonEmptyValues++\n\tval := e.ToString(field, s, required)\n\treturn &val\n}\n\nfunc (e *serde) FromFloat64(s float64) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatFloat(s, uint8(0x66), -1, 64)\n}\n\nfunc (e *serde) FromBytes(s []byte) (v string) {\n\te.NonEmptyValues++\n\treturn base64.URLEncoding.EncodeToString(s)\n}\n\nfunc (e *serde) FromJSON(s json.RawMessage) (v string) {\n\te.NonEmptyValues++\n\treturn string(s)\n}\n\nfunc (e *serde) FromStringOption(s *string) (v []string) {\n\tif s == nil {\n\t\treturn nil\n\t}\n\te.NonEmptyValues++\n\treturn []string{e.FromString(*s)}\n}\n\n// setErr sets the last error within the object if one is not already set\nfunc (e *serde) setErr(msg, field string, err error) {\n\tif err != nil && e.LastError == nil {\n\t\te.LastError = fmt.Errorf(\"%s: %s: %w\", field, msg, err)\n\t}\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_httpstatus_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tSvc SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{Svc: &svcClient{base}}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\ntype SvcResponse struct {\n\tMessage string\n}\n\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\t// DummyAPI is a dummy endpoint.\n\tDummyAPI(ctx context.Context) (SvcResponse, error)\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\n// DummyAPI is a dummy endpoint.\nfunc (c *svcClient) DummyAPI(ctx context.Context) (resp SvcResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/svc.DummyAPI\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\thttpClient HTTPDoer // The HTTP client which will be used for all API requests\n\tbaseURL    *url.URL // The base URL which API requests will be made against\n\tuserAgent  string   // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_httpstatus_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\nexport namespace svc {\n    export interface Response {\n        Message: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.DummyAPI = this.DummyAPI.bind(this)\n        }\n\n        /**\n         * DummyAPI is a dummy endpoint.\n         */\n        public async DummyAPI(): Promise<Response> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`)\n            return await resp.json() as Response\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.authentication = new authentication.ServiceClient(base)\n        this.products = new products.ServiceClient(base)\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\nclass AuthenticationServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.Docs = this.Docs.bind(this)\n    }\n\n    async Docs(params) {\n        await this.baseClient.callTypedAPI(\"POST\", `/authentication.Docs`, JSON.stringify(params))\n    }\n}\n\nexport const authentication = {\n    ServiceClient: AuthenticationServiceClient\n}\n\nclass ProductsServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.Create = this.Create.bind(this)\n        this.List = this.List.bind(this)\n    }\n\n    async Create(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"idempotency-key\": params.IdempotencyKey,\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            description: params.description,\n            name:        params.name,\n        }\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/products.Create`, JSON.stringify(body), {headers})\n        return await resp.json()\n    }\n\n    async List() {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/products.List`)\n        return await resp.json()\n    }\n}\n\nexport const products = {\n    ServiceClient: ProductsServiceClient\n}\n\n/**\n * Svc is a service for testing the client generator.\n */\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.CreateDocumentedOrder = this.CreateDocumentedOrder.bind(this)\n        this.DummyAPI = this.DummyAPI.bind(this)\n        this.FallbackPath = this.FallbackPath.bind(this)\n        this.Get = this.Get.bind(this)\n        this.GetRequestWithAllInputTypes = this.GetRequestWithAllInputTypes.bind(this)\n        this.HeaderOnlyRequest = this.HeaderOnlyRequest.bind(this)\n        this.Nested = this.Nested.bind(this)\n        this.RESTPath = this.RESTPath.bind(this)\n        this.Rec = this.Rec.bind(this)\n        this.RequestWithAllInputTypes = this.RequestWithAllInputTypes.bind(this)\n        this.TupleInputOutput = this.TupleInputOutput.bind(this)\n        this.Webhook = this.Webhook.bind(this)\n        this.Webhook2 = this.Webhook2.bind(this)\n    }\n\n    async CreateDocumentedOrder(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.CreateDocumentedOrder`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    /**\n     * DummyAPI is a dummy endpoint.\n     */\n    async DummyAPI(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            baz: params.HeaderBaz,\n            int: params.HeaderInt === undefined ? undefined : String(params.HeaderInt),\n        })\n\n        const query = makeRecord({\n            bar: params.QueryBar,\n            foo: params.QueryFoo === undefined ? undefined : String(params.QueryFoo),\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            Foo: params.Foo,\n            Raw: params.Raw,\n            boo: params.boo,\n        }\n\n        await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`, JSON.stringify(body), {headers, query})\n    }\n\n    async FallbackPath(a, b) {\n        await this.baseClient.callTypedAPI(\"POST\", `/fallbackPath/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join(\"/\")}`)\n    }\n\n    async Get(params) {\n        // Convert our params into the objects we need for the request\n        const query = makeRecord({\n            boo: String(params.Baz),\n        })\n\n        await this.baseClient.callTypedAPI(\"GET\", `/svc.Get`, undefined, {query})\n    }\n\n    async GetRequestWithAllInputTypes(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"x-alice\": String(params.A),\n        })\n\n        const query = makeRecord({\n            Bob:      params.B.map((v) => String(v)),\n            c:        String(params[\"Charlies-Bool\"]),\n            dave:     String(params.Dave),\n            optional: params.optional === undefined ? undefined : String(params.optional),\n        })\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/svc.GetRequestWithAllInputTypes`, undefined, {headers, query})\n\n        //Populate the return object from the JSON body and received headers\n        const rtn = await resp.json()\n        rtn.Boolean = mustBeSet(\"Header `x-boolean`\", resp.headers.get(\"x-boolean\")).toLowerCase() === \"true\"\n        rtn.Int = parseInt(mustBeSet(\"Header `x-int`\", resp.headers.get(\"x-int\")), 10)\n        rtn.Float = Number(mustBeSet(\"Header `x-float`\", resp.headers.get(\"x-float\")))\n        rtn.String = mustBeSet(\"Header `x-string`\", resp.headers.get(\"x-string\"))\n        rtn.Bytes = mustBeSet(\"Header `x-bytes`\", resp.headers.get(\"x-bytes\"))\n        rtn.Time = mustBeSet(\"Header `x-time`\", resp.headers.get(\"x-time\"))\n        rtn.Json = JSON.parse(mustBeSet(\"Header `x-json`\", resp.headers.get(\"x-json\")))\n        rtn.UUID = mustBeSet(\"Header `x-uuid`\", resp.headers.get(\"x-uuid\"))\n        rtn.UserID = mustBeSet(\"Header `x-user-id`\", resp.headers.get(\"x-user-id\"))\n        rtn.Optional = resp.headers.get(\"x-optional\")\n        return rtn\n    }\n\n    async HeaderOnlyRequest(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"x-boolean\":  String(params.Boolean),\n            \"x-bytes\":    String(params.Bytes),\n            \"x-float\":    String(params.Float),\n            \"x-int\":      String(params.Int),\n            \"x-json\":     JSON.stringify(params.Json),\n            \"x-optional\": params.Optional === undefined ? undefined : String(params.Optional),\n            \"x-string\":   params.String,\n            \"x-time\":     String(params.Time),\n            \"x-user-id\":  String(params.UserID),\n            \"x-uuid\":     String(params.UUID),\n        })\n\n        await this.baseClient.callTypedAPI(\"GET\", `/svc.HeaderOnlyRequest`, undefined, {headers})\n    }\n\n    async Nested(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.Nested`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    async RESTPath(a, b) {\n        await this.baseClient.callTypedAPI(\"POST\", `/path/${encodeURIComponent(a)}/${encodeURIComponent(b)}`)\n    }\n\n    async Rec(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.Rec`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    async RequestWithAllInputTypes(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"x-alice\": String(params.A),\n        })\n\n        const query = makeRecord({\n            Bob: params.B.map((v) => String(v)),\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            \"Charlies-Bool\": params[\"Charlies-Bool\"],\n            Dave:            params.Dave,\n            optional:        params.optional,\n        }\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.RequestWithAllInputTypes`, JSON.stringify(body), {headers, query})\n\n        //Populate the return object from the JSON body and received headers\n        const rtn = await resp.json()\n        rtn.A = mustBeSet(\"Header `x-alice`\", resp.headers.get(\"x-alice\"))\n        return rtn\n    }\n\n    /**\n     * TupleInputOutput tests the usage of generics in the client generator\n     * and this comment is also multiline, so multiline comments get tested as well.\n     */\n    async TupleInputOutput(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.TupleInputOutput`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    async Webhook(method, a, b, body, options) {\n        return this.baseClient.callAPI(method, `/webhook/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join(\"/\")}`, body, options)\n    }\n\n    async Webhook2(a, b) {\n        await this.baseClient.callTypedAPI(\"POST\", `/webhook2/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join(\"/\")}`)\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n// mustBeSet will throw an APIError with the Data Loss code if value is null or undefined\nfunction mustBeSet(field, value) {\n    if (value === null || value === undefined) {\n        throw new APIError(\n            500,\n            {\n                code: ErrCode.DataLoss,\n                message: `${field} was unexpectedly ${value}`, // ${value} will create the string \"null\" or \"undefined\"\n            },\n        )\n    }\n    return value\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n\n    }\n\n    async getAuthData() {\n        let authData;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data = {};\n\n            data.headers = makeRecord({\n                \"x-api-key\": authData.APIKey,\n            })\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_noauth_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tSvc SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{Svc: &svcClient{base}}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\ntype SvcRequest struct {\n\tMessage string\n}\n\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\t// DummyAPI is a dummy endpoint.\n\tDummyAPI(ctx context.Context, params SvcRequest) error\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\n// DummyAPI is a dummy endpoint.\nfunc (c *svcClient) DummyAPI(ctx context.Context, params SvcRequest) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/svc.DummyAPI\", nil, params, nil)\n\treturn err\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\thttpClient HTTPDoer // The HTTP client which will be used for all API requests\n\tbaseURL    *url.URL // The base URL which API requests will be made against\n\tuserAgent  string   // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_noauth_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.DummyAPI = this.DummyAPI.bind(this)\n    }\n\n    /**\n     * DummyAPI is a dummy endpoint.\n     */\n    async DummyAPI(params) {\n        await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`, JSON.stringify(params))\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData() {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_noauth_openapi.json",
    "content": "{\n  \"components\": {\n    \"responses\": {\n      \"APIError\": {\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"externalDocs\": {\n                \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#Error\"\n              },\n              \"properties\": {\n                \"code\": {\n                  \"description\": \"Error code\",\n                  \"example\": \"not_found\",\n                  \"externalDocs\": {\n                    \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\"\n                  },\n                  \"type\": \"string\"\n                },\n                \"details\": {\n                  \"description\": \"Error details\",\n                  \"type\": \"object\"\n                },\n                \"message\": {\n                  \"description\": \"Error message\",\n                  \"type\": \"string\"\n                }\n              },\n              \"title\": \"APIError\",\n              \"type\": \"object\"\n            }\n          }\n        },\n        \"description\": \"Error response\"\n      }\n    }\n  },\n  \"info\": {\n    \"description\": \"Generated by encore\",\n    \"title\": \"API for app\",\n    \"version\": \"1\",\n    \"x-logo\": {\n      \"altText\": \"Encore logo\",\n      \"backgroundColor\": \"#EEEEE1\",\n      \"url\": \"https://encore.dev/assets/branding/logo/logo-black.png\"\n    }\n  },\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/svc.DummyAPI\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.DummyAPI\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Message\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"Message\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        },\n        \"summary\": \"DummyAPI is a dummy endpoint.\\n\"\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"description\": \"Encore local dev environment\",\n      \"url\": \"http://localhost:4000\"\n    }\n  ]\n}"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_noauth_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\nexport namespace svc {\n    export interface Request {\n        Message: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.DummyAPI = this.DummyAPI.bind(this)\n        }\n\n        /**\n         * DummyAPI is a dummy endpoint.\n         */\n        public async DummyAPI(params: Request): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`, JSON.stringify(params))\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_openapi.json",
    "content": "{\n  \"components\": {\n    \"responses\": {\n      \"APIError\": {\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"externalDocs\": {\n                \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#Error\"\n              },\n              \"properties\": {\n                \"code\": {\n                  \"description\": \"Error code\",\n                  \"example\": \"not_found\",\n                  \"externalDocs\": {\n                    \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\"\n                  },\n                  \"type\": \"string\"\n                },\n                \"details\": {\n                  \"description\": \"Error details\",\n                  \"type\": \"object\"\n                },\n                \"message\": {\n                  \"description\": \"Error message\",\n                  \"type\": \"string\"\n                }\n              },\n              \"title\": \"APIError\",\n              \"type\": \"object\"\n            }\n          }\n        },\n        \"description\": \"Error response\"\n      }\n    },\n    \"schemas\": {\n      \"authentication.BarType\": {\n        \"properties\": {\n          \"Baz\": {\n            \"title\": \"Baz docs\\n\",\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"Baz\"\n        ],\n        \"title\": \"BarType docs\\n\",\n        \"type\": \"object\"\n      },\n      \"authentication.User\": {\n        \"properties\": {\n          \"id\": {\n            \"format\": \"int64\",\n            \"type\": \"integer\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"name\"\n        ],\n        \"type\": \"object\"\n      },\n      \"nested.Type\": {\n        \"properties\": {\n          \"Message\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"Message\"\n        ],\n        \"type\": \"object\"\n      },\n      \"products.Product\": {\n        \"properties\": {\n          \"created_at\": {\n            \"format\": \"date-time\",\n            \"type\": \"string\"\n          },\n          \"created_by\": {\n            \"$ref\": \"#/components/schemas/authentication.User\"\n          },\n          \"description\": {\n            \"type\": \"string\"\n          },\n          \"id\": {\n            \"format\": \"uuid\",\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"description\",\n          \"created_at\",\n          \"created_by\"\n        ],\n        \"type\": \"object\"\n      },\n      \"svc.DocumentedUser\": {\n        \"properties\": {\n          \"email\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"name\",\n          \"email\"\n        ],\n        \"title\": \"DocumentedUser represents a user in the system with profile information\\n\",\n        \"type\": \"object\"\n      },\n      \"svc.Foo\": {\n        \"format\": \"int64\",\n        \"title\": \"Foo represents a documented integer type\\n\",\n        \"type\": \"integer\"\n      },\n      \"svc.Recursive\": {\n        \"properties\": {\n          \"Map\": {\n            \"additionalProperties\": {\n              \"$ref\": \"#/components/schemas/svc.Recursive\"\n            },\n            \"type\": \"object\"\n          },\n          \"MapOfOptional\": {\n            \"additionalProperties\": {\n              \"$ref\": \"#/components/schemas/svc.Recursive\"\n            },\n            \"type\": \"object\"\n          },\n          \"Optional\": {\n            \"$ref\": \"#/components/schemas/svc.Recursive\"\n          },\n          \"Slice\": {\n            \"items\": {\n              \"$ref\": \"#/components/schemas/svc.Recursive\"\n            },\n            \"type\": \"array\"\n          },\n          \"SliceOfOptional\": {\n            \"items\": {\n              \"$ref\": \"#/components/schemas/svc.Recursive\"\n            },\n            \"type\": \"array\"\n          }\n        },\n        \"required\": [\n          \"Slice\",\n          \"SliceOfOptional\",\n          \"Map\",\n          \"MapOfOptional\"\n        ],\n        \"type\": \"object\"\n      },\n      \"svc.Request\": {\n        \"properties\": {\n          \"Foo\": {\n            \"allOf\": [\n              {\n                \"$ref\": \"#/components/schemas/svc.Foo\"\n              }\n            ],\n            \"title\": \"Foo is good\\n\"\n          },\n          \"HeaderBaz\": {\n            \"type\": \"string\"\n          },\n          \"HeaderInt\": {\n            \"format\": \"int64\",\n            \"type\": \"integer\"\n          },\n          \"QueryBar\": {\n            \"type\": \"string\"\n          },\n          \"QueryFoo\": {\n            \"type\": \"boolean\"\n          },\n          \"Raw\": {\n            \"description\": \"comment on the raw message!\\n\",\n            \"title\": \"This is a multiline\\n\",\n            \"type\": \"object\"\n          },\n          \"boo\": {\n            \"title\": \"Baz is better\\n\",\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"boo\",\n          \"Raw\"\n        ],\n        \"type\": \"object\"\n      },\n      \"svc.WrappedRequest\": {\n        \"properties\": {\n          \"Value\": {\n            \"$ref\": \"#/components/schemas/svc.Request\"\n          }\n        },\n        \"required\": [\n          \"Value\"\n        ],\n        \"type\": \"object\"\n      }\n    }\n  },\n  \"info\": {\n    \"description\": \"Generated by encore\",\n    \"title\": \"API for app\",\n    \"version\": \"1\",\n    \"x-logo\": {\n      \"altText\": \"Encore logo\",\n      \"backgroundColor\": \"#EEEEE1\",\n      \"url\": \"https://encore.dev/assets/branding/logo/logo-black.png\"\n    }\n  },\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/authentication.Docs\": {\n      \"post\": {\n        \"operationId\": \"POST:authentication.Docs\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Bar\": {\n                    \"$ref\": \"#/components/schemas/authentication.BarType\"\n                  },\n                  \"Moo\": {\n                    \"title\": \"Moo docs\\n\",\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"Moo\",\n                  \"Bar\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/fallbackPath/{a}/{b}\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.FallbackPath\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"POST:svc.FallbackPath\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/path/{a}/{b}\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.RESTPath\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"POST:svc.RESTPath\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/products.Create\": {\n      \"post\": {\n        \"operationId\": \"POST:products.Create\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"idempotency-key\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"description\": {\n                    \"type\": \"string\"\n                  },\n                  \"name\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"name\",\n                  \"description\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"created_at\": {\n                      \"format\": \"date-time\",\n                      \"type\": \"string\"\n                    },\n                    \"created_by\": {\n                      \"$ref\": \"#/components/schemas/authentication.User\"\n                    },\n                    \"description\": {\n                      \"type\": \"string\"\n                    },\n                    \"id\": {\n                      \"format\": \"uuid\",\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"name\",\n                    \"description\",\n                    \"created_at\",\n                    \"created_by\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/products.List\": {\n      \"get\": {\n        \"operationId\": \"GET:products.List\",\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"next\": {\n                      \"properties\": {\n                        \"cursor\": {\n                          \"type\": \"string\"\n                        },\n                        \"exists\": {\n                          \"type\": \"boolean\"\n                        }\n                      },\n                      \"required\": [\n                        \"cursor\",\n                        \"exists\"\n                      ],\n                      \"type\": \"object\"\n                    },\n                    \"previous\": {\n                      \"properties\": {\n                        \"cursor\": {\n                          \"type\": \"string\"\n                        },\n                        \"exists\": {\n                          \"type\": \"boolean\"\n                        }\n                      },\n                      \"required\": [\n                        \"cursor\",\n                        \"exists\"\n                      ],\n                      \"type\": \"object\"\n                    },\n                    \"products\": {\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/products.Product\"\n                      },\n                      \"type\": \"array\"\n                    }\n                  },\n                  \"required\": [\n                    \"products\",\n                    \"previous\",\n                    \"next\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.CreateDocumentedOrder\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.CreateDocumentedOrder\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"customer\": {\n                    \"$ref\": \"#/components/schemas/svc.DocumentedUser\"\n                  },\n                  \"opt_ref\": {\n                    \"$ref\": \"#/components/schemas/svc.DocumentedUser\"\n                  },\n                  \"order_id\": {\n                    \"type\": \"string\"\n                  },\n                  \"req_ref\": {\n                    \"$ref\": \"#/components/schemas/svc.DocumentedUser\"\n                  }\n                },\n                \"required\": [\n                  \"customer\",\n                  \"order_id\",\n                  \"req_ref\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"customer\": {\n                      \"$ref\": \"#/components/schemas/svc.DocumentedUser\"\n                    },\n                    \"opt_ref\": {\n                      \"$ref\": \"#/components/schemas/svc.DocumentedUser\"\n                    },\n                    \"order_id\": {\n                      \"type\": \"string\"\n                    },\n                    \"req_ref\": {\n                      \"$ref\": \"#/components/schemas/svc.DocumentedUser\"\n                    }\n                  },\n                  \"required\": [\n                    \"customer\",\n                    \"order_id\",\n                    \"req_ref\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.DummyAPI\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.DummyAPI\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"baz\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"int\",\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"foo\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"bar\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Foo\": {\n                    \"$ref\": \"#/components/schemas/svc.Foo\"\n                  },\n                  \"Raw\": {\n                    \"description\": \"comment on the raw message!\\n\",\n                    \"title\": \"This is a multiline\\n\",\n                    \"type\": \"object\"\n                  },\n                  \"boo\": {\n                    \"title\": \"Baz is better\\n\",\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"boo\",\n                  \"Raw\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        },\n        \"summary\": \"DummyAPI is a dummy endpoint.\\n\"\n      }\n    },\n    \"/svc.Get\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.Get\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"boo\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.GetRequestWithAllInputTypes\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.GetRequestWithAllInputTypes\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"Specify this comes from a header field\\n\",\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-alice\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"date-time\",\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"Specify this comes from a query string\\n\",\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"Bob\",\n            \"required\": true,\n            \"schema\": {\n              \"items\": {\n                \"format\": \"int64\",\n                \"type\": \"integer\"\n              },\n              \"type\": \"array\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"This can come from anywhere, but if it comes from the payload in JSON it must be called Charile\\n\",\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"c\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"boolean\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"This generic type complicates the whole thing 🙈\\n\",\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"dave\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"An optional generic type\\n\",\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"optional\",\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\",\n            \"headers\": {\n              \"x-boolean\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"type\": \"boolean\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-bytes\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"format\": \"byte\",\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-float\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"type\": \"number\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-int\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"format\": \"int64\",\n                  \"type\": \"integer\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-json\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"type\": \"object\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-optional\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"schema\": {\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-string\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-time\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"format\": \"date-time\",\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-user-id\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              },\n              \"x-uuid\": {\n                \"allowEmptyValue\": true,\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"format\": \"uuid\",\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              }\n            }\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.HeaderOnlyRequest\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.HeaderOnlyRequest\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-boolean\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"boolean\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-int\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"int64\",\n              \"type\": \"integer\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-float\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"number\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-string\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-bytes\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"byte\",\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-time\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"date-time\",\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-json\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"object\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-uuid\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"uuid\",\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-user-id\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-optional\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.Nested\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.Nested\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Nested\": {\n                    \"$ref\": \"#/components/schemas/nested.Type\"\n                  }\n                },\n                \"required\": [\n                  \"Nested\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"Nested\": {\n                      \"$ref\": \"#/components/schemas/nested.Type\"\n                    }\n                  },\n                  \"required\": [\n                    \"Nested\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.Rec\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.Rec\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Map\": {\n                    \"additionalProperties\": {\n                      \"$ref\": \"#/components/schemas/svc.Recursive\"\n                    },\n                    \"type\": \"object\"\n                  },\n                  \"MapOfOptional\": {\n                    \"additionalProperties\": {\n                      \"$ref\": \"#/components/schemas/svc.Recursive\"\n                    },\n                    \"type\": \"object\"\n                  },\n                  \"Optional\": {\n                    \"$ref\": \"#/components/schemas/svc.Recursive\"\n                  },\n                  \"Slice\": {\n                    \"items\": {\n                      \"$ref\": \"#/components/schemas/svc.Recursive\"\n                    },\n                    \"type\": \"array\"\n                  },\n                  \"SliceOfOptional\": {\n                    \"items\": {\n                      \"$ref\": \"#/components/schemas/svc.Recursive\"\n                    },\n                    \"type\": \"array\"\n                  }\n                },\n                \"required\": [\n                  \"Slice\",\n                  \"SliceOfOptional\",\n                  \"Map\",\n                  \"MapOfOptional\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"Map\": {\n                      \"additionalProperties\": {\n                        \"$ref\": \"#/components/schemas/svc.Recursive\"\n                      },\n                      \"type\": \"object\"\n                    },\n                    \"MapOfOptional\": {\n                      \"additionalProperties\": {\n                        \"$ref\": \"#/components/schemas/svc.Recursive\"\n                      },\n                      \"type\": \"object\"\n                    },\n                    \"Optional\": {\n                      \"$ref\": \"#/components/schemas/svc.Recursive\"\n                    },\n                    \"Slice\": {\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/svc.Recursive\"\n                      },\n                      \"type\": \"array\"\n                    },\n                    \"SliceOfOptional\": {\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/svc.Recursive\"\n                      },\n                      \"type\": \"array\"\n                    }\n                  },\n                  \"required\": [\n                    \"Slice\",\n                    \"SliceOfOptional\",\n                    \"Map\",\n                    \"MapOfOptional\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.RequestWithAllInputTypes\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.RequestWithAllInputTypes\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"Specify this comes from a header field\\n\",\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"x-alice\",\n            \"required\": true,\n            \"schema\": {\n              \"format\": \"date-time\",\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"description\": \"Specify this comes from a query string\\n\",\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"Bob\",\n            \"required\": true,\n            \"schema\": {\n              \"items\": {\n                \"format\": \"int64\",\n                \"type\": \"integer\"\n              },\n              \"type\": \"array\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"Charlies-Bool\": {\n                    \"title\": \"This can come from anywhere, but if it comes from the payload in JSON it must be\\ncalled Charile\\n\",\n                    \"type\": \"boolean\"\n                  },\n                  \"Dave\": {\n                    \"title\": \"This generic type complicates the whole thing 🙈\\n\",\n                    \"type\": \"string\"\n                  },\n                  \"optional\": {\n                    \"title\": \"An optional generic type\\n\",\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"Charlies-Bool\",\n                  \"Dave\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"B\": {\n                      \"items\": {\n                        \"format\": \"int64\",\n                        \"type\": \"integer\"\n                      },\n                      \"title\": \"Specify this comes from a query string\\n\",\n                      \"type\": \"array\"\n                    },\n                    \"Charlies-Bool\": {\n                      \"title\": \"This can come from anywhere, but if it comes from the payload in JSON it must be\\ncalled Charile\\n\",\n                      \"type\": \"boolean\"\n                    },\n                    \"Dave\": {\n                      \"title\": \"This generic type complicates the whole thing 🙈\\n\",\n                      \"type\": \"number\"\n                    },\n                    \"optional\": {\n                      \"title\": \"An optional generic type\\n\",\n                      \"type\": \"number\"\n                    }\n                  },\n                  \"required\": [\n                    \"B\",\n                    \"Charlies-Bool\",\n                    \"Dave\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\",\n            \"headers\": {\n              \"x-alice\": {\n                \"allowEmptyValue\": true,\n                \"description\": \"Specify this comes from a header field\\n\",\n                \"explode\": true,\n                \"required\": true,\n                \"schema\": {\n                  \"format\": \"date-time\",\n                  \"type\": \"string\"\n                },\n                \"style\": \"simple\"\n              }\n            }\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/svc.TupleInputOutput\": {\n      \"post\": {\n        \"description\": \"and this comment is also multiline, so multiline comments get tested as well.\\n\",\n        \"operationId\": \"POST:svc.TupleInputOutput\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"A\": {\n                    \"type\": \"string\"\n                  },\n                  \"B\": {\n                    \"$ref\": \"#/components/schemas/svc.WrappedRequest\"\n                  }\n                },\n                \"required\": [\n                  \"A\",\n                  \"B\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"A\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"B\": {\n                      \"$ref\": \"#/components/schemas/svc.Foo\"\n                    }\n                  },\n                  \"required\": [\n                    \"A\",\n                    \"B\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        },\n        \"summary\": \"TupleInputOutput tests the usage of generics in the client generator\\n\"\n      }\n    },\n    \"/webhook/{a}/{b}\": {\n      \"delete\": {\n        \"operationId\": \"DELETE:svc.Webhook\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"get\": {\n        \"operationId\": \"GET:svc.Webhook\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"head\": {\n        \"operationId\": \"HEAD:svc.Webhook\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"patch\": {\n        \"operationId\": \"PATCH:svc.Webhook\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"POST:svc.Webhook\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"put\": {\n        \"operationId\": \"PUT:svc.Webhook\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/webhook2/{a}/{b}\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.Webhook2\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"POST:svc.Webhook2\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"a\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"b\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"description\": \"Encore local dev environment\",\n      \"url\": \"http://localhost:4000\"\n    }\n  ]\n}"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/expected_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly authentication: authentication.ServiceClient\n    public readonly products: products.ServiceClient\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.authentication = new authentication.ServiceClient(base)\n        this.products = new products.ServiceClient(base)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    /**\n     * Allows you to set the authentication data to be used for each\n     * request either by passing in a static object or by passing in\n     * a function which returns a new object for each request.\n     */\n    auth?: authentication.AuthData | AuthDataGenerator\n}\n\nexport namespace authentication {\n    export interface AuthData {\n        APIKey: string\n    }\n\n    /**\n     * BarType docs\n     */\n    export interface BarType {\n        /**\n         * Baz docs\n         */\n        Baz: string\n    }\n\n    /**\n     * FooType docs\n     */\n    export interface FooType {\n        /**\n         * Moo docs\n         */\n        Moo: string\n\n        /**\n         * Bar docs\n         */\n        Bar: BarType\n    }\n\n    export interface User {\n        id: number\n        name: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.Docs = this.Docs.bind(this)\n        }\n\n        public async Docs(params: FooType): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/authentication.Docs`, JSON.stringify(params))\n        }\n    }\n}\n\nexport namespace products {\n    export interface CreateProductRequest {\n        IdempotencyKey: string\n        name: string\n        description: string\n    }\n\n    export interface Product {\n        id: string\n        name: string\n        description: string\n        \"created_at\": string\n        \"created_by\": authentication.User\n    }\n\n    export interface ProductListing {\n        products: Product[]\n        previous: {\n            cursor: string\n            exists: boolean\n        }\n        next: {\n            cursor: string\n            exists: boolean\n        }\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.Create = this.Create.bind(this)\n            this.List = this.List.bind(this)\n        }\n\n        public async Create(params: CreateProductRequest): Promise<Product> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"idempotency-key\": params.IdempotencyKey,\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                description: params.description,\n                name:        params.name,\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/products.Create`, JSON.stringify(body), {headers})\n            return await resp.json() as Product\n        }\n\n        public async List(): Promise<ProductListing> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/products.List`)\n            return await resp.json() as ProductListing\n        }\n    }\n}\n\n/**\n * Svc is a service for testing the client generator.\n */\nexport namespace svc {\n    export interface AllInputTypes<A> {\n        /**\n         * Specify this comes from a header field\n         */\n        A: string\n\n        /**\n         * Specify this comes from a query string\n         */\n        B: number[]\n\n        /**\n         * This can come from anywhere, but if it comes from the payload in JSON it must be called Charile\n         */\n        \"Charlies-Bool\": boolean\n\n        /**\n         * This generic type complicates the whole thing 🙈\n         */\n        Dave: A\n\n        /**\n         * An optional generic type\n         */\n        optional?: A | null\n    }\n\n    /**\n     * DocumentedOrder represents a customer order with references\n     */\n    export interface DocumentedOrder {\n        /**\n         * Customer who placed this order (different from shipping recipient)\n         */\n        customer: DocumentedUser\n\n        \"order_id\": string\n        \"opt_ref\"?: DocumentedUser | null\n        \"req_ref\": DocumentedUser\n    }\n\n    /**\n     * DocumentedUser represents a user in the system with profile information\n     */\n    export interface DocumentedUser {\n        name: string\n        email: string\n    }\n\n    /**\n     * Foo represents a documented integer type\n     */\n    export type Foo = number\n\n    export interface GetRequest {\n        Baz: number\n    }\n\n    /**\n     * HeaderOnlyStruct contains all types we support in headers\n     */\n    export interface HeaderOnlyStruct {\n        Boolean: boolean\n        Int: number\n        Float: number\n        String: string\n        Bytes: string\n        Time: string\n        Json: JSONValue\n        UUID: string\n        UserID: string\n        Optional?: string | null\n    }\n\n    export interface Recursive {\n        Optional?: Recursive\n        Slice: Recursive[]\n        SliceOfOptional: (Recursive | null)[]\n        Map: { [key: string]: Recursive }\n        MapOfOptional: { [key: string]: Recursive | null }\n    }\n\n    export interface Request {\n        /**\n         * Foo is good\n         */\n        Foo?: Foo\n\n        /**\n         * Baz is better\n         */\n        boo: string\n\n        QueryFoo?: boolean\n        QueryBar?: string\n        HeaderBaz?: string\n        HeaderInt?: number\n        /**\n         * This is a multiline\n         * comment on the raw message!\n         */\n        Raw: JSONValue\n    }\n\n    /**\n     * Tuple is a generic type which allows us to\n     * return two values of two different types\n     */\n    export interface Tuple<A, B> {\n        A: A\n        B: B\n    }\n\n    export interface WithNested {\n        Nested: nested.Type\n    }\n\n    export type WrappedRequest = Wrapper<Request>\n\n    export interface Wrapper<T> {\n        Value: T\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.CreateDocumentedOrder = this.CreateDocumentedOrder.bind(this)\n            this.DummyAPI = this.DummyAPI.bind(this)\n            this.FallbackPath = this.FallbackPath.bind(this)\n            this.Get = this.Get.bind(this)\n            this.GetRequestWithAllInputTypes = this.GetRequestWithAllInputTypes.bind(this)\n            this.HeaderOnlyRequest = this.HeaderOnlyRequest.bind(this)\n            this.Nested = this.Nested.bind(this)\n            this.RESTPath = this.RESTPath.bind(this)\n            this.Rec = this.Rec.bind(this)\n            this.RequestWithAllInputTypes = this.RequestWithAllInputTypes.bind(this)\n            this.TupleInputOutput = this.TupleInputOutput.bind(this)\n            this.Webhook = this.Webhook.bind(this)\n            this.Webhook2 = this.Webhook2.bind(this)\n        }\n\n        public async CreateDocumentedOrder(params: DocumentedOrder): Promise<DocumentedOrder> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.CreateDocumentedOrder`, JSON.stringify(params))\n            return await resp.json() as DocumentedOrder\n        }\n\n        /**\n         * DummyAPI is a dummy endpoint.\n         */\n        public async DummyAPI(params: Request): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.HeaderBaz,\n                int: params.HeaderInt === undefined ? undefined : String(params.HeaderInt),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar: params.QueryBar,\n                foo: params.QueryFoo === undefined ? undefined : String(params.QueryFoo),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                Foo: params.Foo,\n                Raw: params.Raw,\n                boo: params.boo,\n            }\n\n            await this.baseClient.callTypedAPI(\"POST\", `/svc.DummyAPI`, JSON.stringify(body), {headers, query})\n        }\n\n        public async FallbackPath(a: string, b: string[]): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/fallbackPath/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join(\"/\")}`)\n        }\n\n        public async Get(params: GetRequest): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const query = makeRecord<string, string | string[]>({\n                boo: String(params.Baz),\n            })\n\n            await this.baseClient.callTypedAPI(\"GET\", `/svc.Get`, undefined, {query})\n        }\n\n        public async GetRequestWithAllInputTypes(params: AllInputTypes<number>): Promise<HeaderOnlyStruct> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"x-alice\": String(params.A),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                Bob:      params.B.map((v) => String(v)),\n                c:        String(params[\"Charlies-Bool\"]),\n                dave:     String(params.Dave),\n                optional: params.optional === undefined ? undefined : String(params.optional),\n            })\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/svc.GetRequestWithAllInputTypes`, undefined, {headers, query})\n\n            //Populate the return object from the JSON body and received headers\n            const rtn = await resp.json() as HeaderOnlyStruct\n            rtn.Boolean = mustBeSet(\"Header `x-boolean`\", resp.headers.get(\"x-boolean\")).toLowerCase() === \"true\"\n            rtn.Int = parseInt(mustBeSet(\"Header `x-int`\", resp.headers.get(\"x-int\")), 10)\n            rtn.Float = Number(mustBeSet(\"Header `x-float`\", resp.headers.get(\"x-float\")))\n            rtn.String = mustBeSet(\"Header `x-string`\", resp.headers.get(\"x-string\"))\n            rtn.Bytes = mustBeSet(\"Header `x-bytes`\", resp.headers.get(\"x-bytes\"))\n            rtn.Time = mustBeSet(\"Header `x-time`\", resp.headers.get(\"x-time\"))\n            rtn.Json = JSON.parse(mustBeSet(\"Header `x-json`\", resp.headers.get(\"x-json\")))\n            rtn.UUID = mustBeSet(\"Header `x-uuid`\", resp.headers.get(\"x-uuid\"))\n            rtn.UserID = mustBeSet(\"Header `x-user-id`\", resp.headers.get(\"x-user-id\"))\n            rtn.Optional = mustBeSet(\"Header `x-optional`\", resp.headers.get(\"x-optional\"))\n            return rtn\n        }\n\n        public async HeaderOnlyRequest(params: HeaderOnlyStruct): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"x-boolean\":  String(params.Boolean),\n                \"x-bytes\":    String(params.Bytes),\n                \"x-float\":    String(params.Float),\n                \"x-int\":      String(params.Int),\n                \"x-json\":     JSON.stringify(params.Json),\n                \"x-optional\": params.Optional === undefined ? undefined : String(params.Optional),\n                \"x-string\":   params.String,\n                \"x-time\":     String(params.Time),\n                \"x-user-id\":  String(params.UserID),\n                \"x-uuid\":     String(params.UUID),\n            })\n\n            await this.baseClient.callTypedAPI(\"GET\", `/svc.HeaderOnlyRequest`, undefined, {headers})\n        }\n\n        public async Nested(params: WithNested): Promise<WithNested> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.Nested`, JSON.stringify(params))\n            return await resp.json() as WithNested\n        }\n\n        public async RESTPath(a: string, b: number): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/path/${encodeURIComponent(a)}/${encodeURIComponent(b)}`)\n        }\n\n        public async Rec(params: Recursive): Promise<Recursive> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.Rec`, JSON.stringify(params))\n            return await resp.json() as Recursive\n        }\n\n        public async RequestWithAllInputTypes(params: AllInputTypes<string>): Promise<AllInputTypes<number>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"x-alice\": String(params.A),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                Bob: params.B.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                \"Charlies-Bool\": params[\"Charlies-Bool\"],\n                Dave:            params.Dave,\n                optional:        params.optional,\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.RequestWithAllInputTypes`, JSON.stringify(body), {headers, query})\n\n            //Populate the return object from the JSON body and received headers\n            const rtn = await resp.json() as AllInputTypes<number>\n            rtn.A = mustBeSet(\"Header `x-alice`\", resp.headers.get(\"x-alice\"))\n            return rtn\n        }\n\n        /**\n         * TupleInputOutput tests the usage of generics in the client generator\n         * and this comment is also multiline, so multiline comments get tested as well.\n         */\n        public async TupleInputOutput(params: Tuple<string, WrappedRequest>): Promise<Tuple<boolean, Foo>> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/svc.TupleInputOutput`, JSON.stringify(params))\n            return await resp.json() as Tuple<boolean, Foo>\n        }\n\n        public async Webhook(method: string, a: string, b: string[], body?: RequestInit[\"body\"], options?: CallParameters): Promise<globalThis.Response> {\n            return this.baseClient.callAPI(method, `/webhook/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join(\"/\")}`, body, options)\n        }\n\n        public async Webhook2(a: string, b: string[]): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/webhook2/${encodeURIComponent(a)}/${b.map(encodeURIComponent).join(\"/\")}`)\n        }\n    }\n}\n\nexport namespace nested {\n    export interface Type {\n        Message: string\n    }\n}\n\n// JSONValue represents an arbitrary JSON value.\nexport type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue}\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\n\n// mustBeSet will throw an APIError with the Data Loss code if value is null or undefined\nfunction mustBeSet<A>(field: string, value: A | null | undefined): A {\n    if (value === null || value === undefined) {\n        throw new APIError(\n            500,\n            {\n                code: ErrCode.DataLoss,\n                message: `${field} was unexpectedly ${value}`, // ${value} will create the string \"null\" or \"undefined\"\n            },\n        )\n    }\n    return value\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n// AuthDataGenerator is a function that returns a new instance of the authentication data required by this API\nexport type AuthDataGenerator = () =>\n  | authentication.AuthData\n  | Promise<authentication.AuthData | undefined>\n  | undefined;\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n    readonly authGenerator?: AuthDataGenerator\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        let authData: authentication.AuthData | undefined;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data: CallParameters = {};\n\n            data.headers = makeRecord<string, string>({\n                \"x-api-key\": authData.APIKey,\n            });\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/input.go",
    "content": "-- go.mod --\nmodule app\n\nrequire (\n\tencore.dev v1.52.1\n)\n\n-- encore.app --\n{\"id\": \"\"}\n\n-- svc/svc.go --\n// Svc is a service for testing the client generator.\npackage svc\n\nimport (\n    \"encoding/json\"\n    \"time\"\n\n    \"encore.dev/beta/auth\"\n    \"encore.dev/types/uuid\"\n    \"encore.dev/types/option\"\n)\n\ntype UnusedType struct {\n\tFoo Foo\n}\n\ntype Wrapper[T any] struct { Value T }\n\n// Tuple is a generic type which allows us to\n// return two values of two different types\ntype Tuple[A any, B any] struct {\n\tA A\n\tB B\n}\n\ntype Request struct {\n    Foo Foo    `encore:\"optional\"` // Foo is good\n    Bar string `json:\"-\"`\n    Baz string `json:\"boo\"`        // Baz is better\n\n    QueryFoo bool    `query:\"foo\" encore:\"optional\"`\n    QueryBar string  `query:\"bar\" encore:\"optional\"`\n    HeaderBaz string `header:\"baz\" encore:\"optional\"`\n    HeaderInt int    `header:\"int\" encore:\"optional\"`\n\n// This is a multiline\n    // comment on the raw message!\n    Raw json.RawMessage\n}\n\ntype WrappedRequest = Wrapper[Request]\n\ntype GetRequest struct {\n    Bar string `qs:\"-\"`\n    Baz int    `qs:\"boo\"`\n}\n\n// Foo represents a documented integer type\ntype Foo int\n\n// DocumentedUser represents a user in the system with profile information\ntype DocumentedUser struct {\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\n// DocumentedOrder represents a customer order with references\ntype DocumentedOrder struct {\n    // Customer who placed this order (different from shipping recipient)\n    Customer DocumentedUser `json:\"customer\"`\n    OrderID  string         `json:\"order_id\"`\n\n    OptionalRef option.Option[DocumentedUser] `json:\"opt_ref\"`\n    RequiredRef *DocumentedUser `json:\"req_ref\"`\n}\n\ntype Nested struct {\n    Value string\n}\n\ntype AllInputTypes[A any] struct {\n    A time.Time `header:\"X-Alice\"`               // Specify this comes from a header field\n    B []int     `query:\"Bob\"`                    // Specify this comes from a query string\n    C bool      `json:\"Charlies-Bool,omitempty\"` // This can come from anywhere, but if it comes from the payload in JSON it must be called Charile\n    Dave A                                       // This generic type complicates the whole thing 🙈\n\n    Optional option.Option[A] `json:\"optional\"` // An optional generic type\n\n    // Tags named \"-\" are ignored in schemas\n    Ignore1 string `header:\"-\"`\n    Ignore2 string `query:\"-\"`\n    Ignore3 string `json:\"-\"`\n\n    // Unexported tags are ignored\n    ignore4 string\n}\n\n// HeaderOnlyStruct contains all types we support in headers\ntype HeaderOnlyStruct struct {\n    Boolean bool            `header:\"x-boolean\"`\n    Int     int             `header:\"x-int\"`\n    Float   float64         `header:\"x-float\"`\n    String  string          `header:\"x-string\"`\n    Bytes   []byte          `header:\"x-bytes\"`\n    Time    time.Time       `header:\"x-time\"`\n    Json    json.RawMessage `header:\"x-json\"`\n    UUID    uuid.UUID       `header:\"x-uuid\"`\n    UserID  auth.UID        `header:\"x-user-id\"`\n    Optional option.Option[string] `header:\"x-optional\"`\n}\n\ntype Recursive struct {\n    Optional *Recursive `encore:\"optional\"`\n    Slice   []Recursive\n    SliceOfOptional   []option.Option[*Recursive]\n    Map map[string]Recursive\n    MapOfOptional map[string]option.Option[*Recursive]\n}\n\n-- svc/api.go --\npackage svc\n\nimport (\n    \"context\"\n    \"net/http\"\n    \"app/svc/nested\"\n)\n\n// DummyAPI is a dummy endpoint.\n//encore:api public\nfunc DummyAPI(ctx context.Context, req *Request) error {\n    return nil\n}\n\n//encore:api public method=GET\nfunc Get(ctx context.Context, req *GetRequest) error {\n    return nil\n}\n\n// TupleInputOutput tests the usage of generics in the client generator\n// and this comment is also multiline, so multiline comments get tested as well.\n//encore:api public\nfunc TupleInputOutput(ctx context.Context, req Tuple[string, WrappedRequest]) (Tuple[bool, Foo], error) {\n    return nil\n}\n\n//encore:api public path=/path/:a/:b\nfunc RESTPath(ctx context.Context, a string, b int) error {\n    return nil\n}\n\n//encore:api public raw path=/webhook/:a/*b\nfunc Webhook(w http.ResponseWriter, req *http.Request) {}\n\n\n//encore:api public path=/webhook2/:a/*b\nfunc Webhook2(ctx context.Context, a string, b string) error { return nil }\n\n//encore:api public path=/fallbackPath/:a/!b\nfunc FallbackPath(ctx context.Context, a string, b string) error { return nil }\n\n//encore:api public method=POST\nfunc RequestWithAllInputTypes(ctx context.Context, req *AllInputTypes[string]) (*AllInputTypes[float64], error) {\n    return nil\n}\n\n//encore:api public method=GET\nfunc GetRequestWithAllInputTypes(ctx context.Context, req *AllInputTypes[int]) (HeaderOnlyStruct, error) {\n    return nil\n}\n\n//encore:api public method=GET\nfunc HeaderOnlyRequest(ctx context.Context, req *HeaderOnlyStruct) error {\n    return nil\n}\n\ntype WithNested struct {\n    Nested *nested.Type\n}\n\n//encore:api public method=POST\nfunc Nested(ctx context.Context, req *WithNested) (*WithNested, error) {\n    return nil, nil\n}\n\n//encore:api public method=POST\nfunc Rec(ctx context.Context, req *Recursive) (*Recursive, error) {\n    return nil, nil\n}\n\n//encore:api public method=POST\nfunc CreateDocumentedOrder(ctx context.Context, req *DocumentedOrder) (*DocumentedOrder, error) {\n    return req, nil\n}\n-- svc/nested/nested.go --\npackage nested\n\ntype Type struct {\n    Message string\n}\n\n-- products/product.go --\npackage products\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"encore.dev/types/uuid\"\n\n    \"app/authentication\"\n)\n\ntype Product struct {\n    ID          uuid.UUID            `json:\"id\"`\n    Name        string               `json:\"name\"`\n    Description string               `json:\"description,omitempty\"`\n    CreatedAt   time.Time            `json:\"created_at\"`\n    CreatedBy   *authentication.User `json:\"created_by\"`\n}\n\ntype ProductListing struct {\n    Products []*Product `json:\"products\"`\n\n    PreviousPage struct {\n        Cursor string `json:\"cursor,omitempty\"`\n        Exists bool   `json:\"exists\"`\n    } `json:\"previous\"`\n\n    NextPage struct {\n        Cursor string `json:\"cursor,omitempty\"`\n        Exists bool   `json:\"exists\"`\n    } `json:\"next\"`\n}\n\ntype CreateProductRequest struct {\n    IdempotencyKey string `header:\"Idempotency-Key\"`\n    Name           string `json:\"name\"`\n    Description    string `json:\"description,omitempty\"`\n}\n\n//encore:api public method=GET\nfunc List(ctx context.Context) (*ProductListing, error) {\n    return nil, nil\n}\n\n//encore:api auth\nfunc Create(ctx context.Context, req *CreateProductRequest) (*Product, error) {\n    return nil, nil\n}\n\n-- authentication/auth.go --\npackage authentication\n\nimport (\n    \"context\"\n\n    \"encore.dev/beta/auth\"\n)\n\ntype AuthData struct {\n    APIKey string `header:\"X-API-Key\"`\n}\n\ntype User struct {\n    ID   int     `json:\"id\"`\n    Name string  `json:\"name\"`\n}\n\n//encore:authhandler\nfunc AuthenticateRequest(ctx context.Context, auth *AuthData) (auth.UID, *User, error) {\n    return \"\", nil, nil\n}\n\n// FooType docs\ntype FooType struct {\n    Moo string // Moo docs\n    Bar BarType // Bar docs\n}\n\n// BarType docs\ntype BarType struct {\n    Baz string // Baz docs\n}\n\n//encore:api public\nfunc Docs(ctx context.Context, req *FooType) error {\n    return nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/input_baseauth.go",
    "content": "-- go.mod --\nmodule app\n\n-- encore.app --\n{\"id\": \"\"}\n\n-- svc/svc.go --\npackage svc\n\ntype Request struct {\n\tMessage string\n}\n\n-- svc/api.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\n// DummyAPI is a dummy endpoint.\n//encore:api public\nfunc DummyAPI(ctx context.Context, req *Request) error {\n\treturn nil\n}\n\n// Private is a basic auth endpoint.\n//encore:api auth\nfunc Private(ctx context.Context, req *Request) error {\n\treturn nil\n}\n\n-- authentication/auth.go --\npackage authentication\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/beta/auth\"\n)\n\ntype User struct {\n\tID   int     `json:\"id\"`\n\tName string  `json:\"name\"`\n}\n\n//encore:authhandler\nfunc AuthenticateRequest(ctx context.Context, token string) (auth.UID, *User, error) {\n\treturn \"\", nil, nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/input_httpstatus.go",
    "content": "-- go.mod --\nmodule app\n\n-- encore.app --\n{\"id\": \"\"}\n\n-- svc/svc.go --\npackage svc\n\ntype Response struct {\n    Message string\n    Status int `encore:\"httpstatus\"`\n}\n\n-- svc/api.go --\npackage svc\n\nimport (\n    \"context\"\n    \"net/http\"\n)\n\n// DummyAPI is a dummy endpoint.\n//encore:api public\nfunc DummyAPI(ctx context.Context) (*Response, error) {\n    return nil, nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/input_noauth.go",
    "content": "-- go.mod --\nmodule app\n\n-- encore.app --\n{\"id\": \"\"}\n\n-- svc/svc.go --\npackage svc\n\ntype Request struct {\n    Message string\n}\n\n-- svc/api.go --\npackage svc\n\nimport (\n    \"context\"\n    \"net/http\"\n)\n\n// DummyAPI is a dummy endpoint.\n//encore:api public\nfunc DummyAPI(ctx context.Context, req *Request) error {\n    return nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/goapp/tsconfig.json",
    "content": "// Note: this config is here purely to remove errors about promises not being present in IDE's when viewing the expected_typescript.ts file\n{\n  \"compilerOptions\": {\n    \"target\": \"es2021\"\n  }\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_decimal_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tSvc SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{Svc: &svcClient{base}}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\ntype SvcRequest struct {\n\tMessage string\n\tVal     string\n}\n\ntype SvcResponse struct {\n\tResult string\n}\n\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\tDummy(ctx context.Context, params SvcRequest) (SvcResponse, error)\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\nfunc (c *svcClient) Dummy(ctx context.Context, params SvcRequest) (resp SvcResponse, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\tqueryString := url.Values{\n\t\t\"message\": {reqEncoder.FromString(params.message)},\n\t\t\"val\":     {reqEncoder.FromString(params.val)},\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", fmt.Sprintf(\"/dummy?%s\", queryString.Encode()), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\thttpClient HTTPDoer // The HTTP client which will be used for all API requests\n\tbaseURL    *url.URL // The base URL which API requests will be made against\n\tuserAgent  string   // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n\n// serde is used to serialize request data into strings and deserialize response data from strings\ntype serde struct {\n\tLastError      error // The last error that occurred\n\tNonEmptyValues int   // The number of values this decoder has decoded\n}\n\nfunc (e *serde) FromString(s string) (v string) {\n\te.NonEmptyValues++\n\treturn s\n}\n\n// setErr sets the last error within the object if one is not already set\nfunc (e *serde) setErr(msg, field string, err error) {\n\tif err != nil && e.LastError == nil {\n\t\te.LastError = fmt.Errorf(\"%s: %s: %w\", field, msg, err)\n\t}\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_decimal_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.dummy = this.dummy.bind(this)\n    }\n\n    async dummy(params) {\n        // Convert our params into the objects we need for the request\n        const query = makeRecord({\n            message: params.message,\n            val:     String(params.val),\n        })\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"GET\", `/dummy`, undefined, {query})\n        return await resp.json()\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData() {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_decimal_openapi.json",
    "content": "{\n  \"components\": {\n    \"responses\": {\n      \"APIError\": {\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"externalDocs\": {\n                \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#Error\"\n              },\n              \"properties\": {\n                \"code\": {\n                  \"description\": \"Error code\",\n                  \"example\": \"not_found\",\n                  \"externalDocs\": {\n                    \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\"\n                  },\n                  \"type\": \"string\"\n                },\n                \"details\": {\n                  \"description\": \"Error details\",\n                  \"type\": \"object\"\n                },\n                \"message\": {\n                  \"description\": \"Error message\",\n                  \"type\": \"string\"\n                }\n              },\n              \"title\": \"APIError\",\n              \"type\": \"object\"\n            }\n          }\n        },\n        \"description\": \"Error response\"\n      }\n    }\n  },\n  \"info\": {\n    \"description\": \"Generated by encore\",\n    \"title\": \"API for app\",\n    \"version\": \"1\",\n    \"x-logo\": {\n      \"altText\": \"Encore logo\",\n      \"backgroundColor\": \"#EEEEE1\",\n      \"url\": \"https://encore.dev/assets/branding/logo/logo-black.png\"\n    }\n  },\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/dummy\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.dummy\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"message\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"val\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"result\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"result\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"description\": \"Encore local dev environment\",\n      \"url\": \"http://localhost:4000\"\n    }\n  ]\n}"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_decimal_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\nexport namespace svc {\n    export interface Request {\n        message: string\n        val: string\n    }\n\n    export interface Response {\n        result: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.dummy = this.dummy.bind(this)\n        }\n\n        public async dummy(params: Request): Promise<Response> {\n            // Convert our params into the objects we need for the request\n            const query = makeRecord<string, string | string[]>({\n                message: params.message,\n                val:     String(params.val),\n            })\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/dummy`, undefined, {query})\n            return await resp.json() as Response\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tSvc SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{Svc: &svcClient{base}}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\n// WithAuth allows you to set the authentication data to be used with each request\nfunc WithAuth(auth SvcAuthParams) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = func(_ context.Context) (SvcAuthParams, error) {\n\t\t\treturn auth, nil\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithAuthFunc allows you to pass a function which is called for each request to return the authentication data to be used with each request\nfunc WithAuthFunc(authGenerator func(ctx context.Context) (SvcAuthParams, error)) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.authGenerator = authGenerator\n\t\treturn nil\n\t}\n}\n\ntype SvcAuthParams struct {\n\tCookie      string `encore:\"optional\" header:\"Cookie,optional\"`\n\tToken       string `encore:\"optional\" header:\"x-api-token,optional\"`\n\tCookieValue string `cookie:\"actual-cookie,optional\" encore:\"optional\"`\n}\n\ntype SvcRequest struct {\n\tFoo        float64 `encore:\"optional\"` // Foo is good\n\tBaz        string  // Baz is better\n\tQueryFoo   bool    `encore:\"optional\" query:\"foo,optional\"`\n\tQueryBar   string  `encore:\"optional\" query:\"bar,optional\"`\n\tQueryList  []bool  `encore:\"optional\" query:\"list,optional\"`\n\tHeaderBaz  string  `encore:\"optional\" header:\"baz,optional\"`\n\tHeaderNum  float64 `encore:\"optional\" header:\"num,optional\"`\n\tCookieQux  string  `cookie:\"qux,optional\" encore:\"optional\"`\n\tCookieQuux float64 `cookie:\"quux,optional\" encore:\"optional\"`\n}\n\ntype SvcRequest struct {\n\tFoo        float64 `encore:\"optional\"` // Foo is good\n\tBaz        string  // Baz is better\n\tQueryFoo   bool    `encore:\"optional\" query:\"foo,optional\"`\n\tQueryBar   string  `encore:\"optional\" query:\"bar,optional\"`\n\tQueryList  []bool  `encore:\"optional\" query:\"list,optional\"`\n\tHeaderBaz  string  `encore:\"optional\" header:\"baz,optional\"`\n\tHeaderNum  float64 `encore:\"optional\" header:\"num,optional\"`\n\tCookieQux  string  `cookie:\"qux,optional\" encore:\"optional\"`\n\tCookieQuux float64 `cookie:\"quux,optional\" encore:\"optional\"`\n}\n\ntype SvcRequest struct {\n\tFoo        float64 `encore:\"optional\"` // Foo is good\n\tBaz        string  // Baz is better\n\tQueryFoo   bool    `encore:\"optional\" query:\"foo,optional\"`\n\tQueryBar   string  `encore:\"optional\" query:\"bar,optional\"`\n\tQueryList  []bool  `encore:\"optional\" query:\"list,optional\"`\n\tHeaderBaz  string  `encore:\"optional\" header:\"baz,optional\"`\n\tHeaderNum  float64 `encore:\"optional\" header:\"num,optional\"`\n\tCookieQux  string  `cookie:\"qux,optional\" encore:\"optional\"`\n\tCookieQuux float64 `cookie:\"quux,optional\" encore:\"optional\"`\n}\n\n// Svc is a service for testing the client generator.\n//\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\tCookieDummy(ctx context.Context, params SvcRequest) (struct {\n\t\tCookie string `cookie:\"cookie\"`\n\t}, error)\n\tCookiesOnly(ctx context.Context, params struct {\n\t\tField string `cookie:\"cookie\"`\n\t}) (struct {\n\t\tCookie string `cookie:\"cookie\"`\n\t}, error)\n\tDummy(ctx context.Context, params SvcRequest) error\n\tImported(ctx context.Context, params Common_StuffImportedRequest) (Common_StuffImportedResponse, error)\n\tNoTypes(ctx context.Context) error\n\tOnlyPathParams(ctx context.Context, pathParam string, pathParam2 string) (Common_StuffImportedResponse, error)\n\tRoot(ctx context.Context, params SvcRequest) error\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\nfunc (c *svcClient) CookieDummy(ctx context.Context, params SvcRequest) (resp struct {\n\tCookie string `cookie:\"cookie\"`\n}, err error) {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"baz\": {reqEncoder.FromString(params.headerBaz)},\n\t\t\"num\": {reqEncoder.FromFloat64(params.headerNum)},\n\t}\n\n\tqueryString := url.Values{\n\t\t\"bar\":  {reqEncoder.FromString(params.queryBar)},\n\t\t\"foo\":  {reqEncoder.FromBool(params.queryFoo)},\n\t\t\"list\": reqEncoder.FromBoolList(params.queryList),\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\terr = fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t\treturn\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tFoo float64 `json:\"foo\"`\n\t\tBaz string  `json:\"baz\"`\n\t}{\n\t\tbaz: params.baz,\n\t\tfoo: params.foo,\n\t}\n\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/cookie-dummy?%s\", queryString.Encode()), headers, body, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) CookiesOnly(ctx context.Context, params struct {\n\tField string `cookie:\"cookie\"`\n}) (resp struct {\n\tCookie string `cookie:\"cookie\"`\n}, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/cookies-only\", nil, nil, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) Dummy(ctx context.Context, params SvcRequest) error {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"baz\": {reqEncoder.FromString(params.headerBaz)},\n\t\t\"num\": {reqEncoder.FromFloat64(params.headerNum)},\n\t}\n\n\tqueryString := url.Values{\n\t\t\"bar\":  {reqEncoder.FromString(params.queryBar)},\n\t\t\"foo\":  {reqEncoder.FromBool(params.queryFoo)},\n\t\t\"list\": reqEncoder.FromBoolList(params.queryList),\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\treturn fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tFoo float64 `json:\"foo\"`\n\t\tBaz string  `json:\"baz\"`\n\t}{\n\t\tbaz: params.baz,\n\t\tfoo: params.foo,\n\t}\n\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/dummy?%s\", queryString.Encode()), headers, body, nil)\n\treturn err\n}\n\nfunc (c *svcClient) Imported(ctx context.Context, params Common_StuffImportedRequest) (resp Common_StuffImportedResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", \"/imported\", nil, params, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) NoTypes(ctx context.Context) error {\n\t_, err := callAPI(ctx, c.base, \"POST\", \"/type-less\", nil, nil, nil)\n\treturn err\n}\n\nfunc (c *svcClient) OnlyPathParams(ctx context.Context, pathParam string, pathParam2 string) (resp Common_StuffImportedResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/path/%s/%s\", url.PathEscape(pathParam), url.PathEscape(pathParam2)), nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *svcClient) Root(ctx context.Context, params SvcRequest) error {\n\t// Convert our params into the objects we need for the request\n\treqEncoder := &serde{}\n\n\theaders := http.Header{\n\t\t\"baz\": {reqEncoder.FromString(params.headerBaz)},\n\t\t\"num\": {reqEncoder.FromFloat64(params.headerNum)},\n\t}\n\n\tqueryString := url.Values{\n\t\t\"bar\":  {reqEncoder.FromString(params.queryBar)},\n\t\t\"foo\":  {reqEncoder.FromBool(params.queryFoo)},\n\t\t\"list\": reqEncoder.FromBoolList(params.queryList),\n\t}\n\n\tif reqEncoder.LastError != nil {\n\t\treturn fmt.Errorf(\"unable to marshal parameters: %w\", reqEncoder.LastError)\n\t}\n\n\t// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n\tbody := struct {\n\t\tFoo float64 `json:\"foo\"`\n\t\tBaz string  `json:\"baz\"`\n\t}{\n\t\tbaz: params.baz,\n\t\tfoo: params.foo,\n\t}\n\n\t_, err := callAPI(ctx, c.base, \"POST\", fmt.Sprintf(\"/?%s\", queryString.Encode()), headers, body, nil)\n\treturn err\n}\n\ntype Common_StuffImportedRequest struct {\n\tName string\n}\n\ntype Common_StuffImportedResponse struct {\n\tMessage string\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\tauthGenerator func(ctx context.Context) (SvcAuthParams, error) // The function which will add the authentication data to the requests\n\thttpClient    HTTPDoer                                         // The HTTP client which will be used for all API requests\n\tbaseURL       *url.URL                                         // The base URL which API requests will be made against\n\tuserAgent     string                                           // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// If a authorization data generator is present, call it and add the returned token to the request\n\tif b.authGenerator != nil {\n\t\tif authData, err := b.authGenerator(req.Context()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to create authorization token for api request: %w\", err)\n\t\t} else {\n\t\t\tauthEncoder := &serde{}\n\n\t\t\t// Add the auth fields to the headers\n\t\t\treq.Header.Set(\"cookie\", authEncoder.FromString(authData.Cookie))\n\t\t\treq.Header.Set(\"x-api-token\", authEncoder.FromString(authData.Token))\n\n\t\t\tif authEncoder.LastError != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to marshal authentication data: %w\", authEncoder.LastError)\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n\n// serde is used to serialize request data into strings and deserialize response data from strings\ntype serde struct {\n\tLastError      error // The last error that occurred\n\tNonEmptyValues int   // The number of values this decoder has decoded\n}\n\nfunc (e *serde) FromString(s string) (v string) {\n\te.NonEmptyValues++\n\treturn s\n}\n\nfunc (e *serde) FromFloat64(s float64) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatFloat(s, uint8(0x66), -1, 64)\n}\n\nfunc (e *serde) FromBool(s bool) (v string) {\n\te.NonEmptyValues++\n\treturn strconv.FormatBool(s)\n}\n\nfunc (e *serde) FromBoolList(s []bool) (v []string) {\n\te.NonEmptyValues++\n\tfor _, x := range s {\n\t\tv = append(v, e.FromBool(x))\n\t}\n\treturn v\n}\n\n// setErr sets the last error within the object if one is not already set\nfunc (e *serde) setErr(msg, field string, err error) {\n\tif err != nil && e.LastError == nil {\n\t\te.LastError = fmt.Errorf(\"%s: %s: %w\", field, msg, err)\n\t}\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_httpstatus_golang.go",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\npackage client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\n// Client is an API client for the app Encore application.\ntype Client struct {\n\tSvc SvcClient\n}\n\n// BaseURL is the base URL for calling the Encore application's API.\ntype BaseURL string\n\nconst Local BaseURL = \"http://localhost:4000\"\n\n// Environment returns a BaseURL for calling the cloud environment with the given name.\nfunc Environment(name string) BaseURL {\n\treturn BaseURL(fmt.Sprintf(\"https://%s-app.encr.app\", name))\n}\n\n// PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\nfunc PreviewEnv(pr int) BaseURL {\n\treturn Environment(fmt.Sprintf(\"pr%d\", pr))\n}\n\n// Option allows you to customise the baseClient used by the Client\ntype Option = func(client *baseClient) error\n\n// New returns a Client for calling the public and authenticated APIs of your Encore application.\n// You can customize the behaviour of the client using the given Option functions, such as WithHTTPClient or WithAuthFunc.\nfunc New(target BaseURL, options ...Option) (*Client, error) {\n\t// Parse the base URL where the Encore application is being hosted\n\tbaseURL, err := url.Parse(string(target))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse base url: %w\", err)\n\t}\n\n\t// Create a client with sensible defaults\n\tbase := &baseClient{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t\tuserAgent:  \"app-Generated-Go-Client (Encore/v0.0.0-develop)\",\n\t}\n\n\t// Apply any given options\n\tfor _, option := range options {\n\t\tif err := option(base); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to apply client option: %w\", err)\n\t\t}\n\t}\n\n\treturn &Client{Svc: &svcClient{base}}, nil\n}\n\n// WithHTTPClient can be used to configure the underlying HTTP client used when making API calls.\n//\n// Defaults to http.DefaultClient\nfunc WithHTTPClient(client HTTPDoer) Option {\n\treturn func(base *baseClient) error {\n\t\tbase.httpClient = client\n\t\treturn nil\n\t}\n}\n\ntype SvcResponse struct {\n\tMessage string\n}\n\n// SvcClient Provides you access to call public and authenticated APIs on svc. The concrete implementation is svcClient.\n// It is setup as an interface allowing you to use GoMock to create mock implementations during tests.\ntype SvcClient interface {\n\tDummy(ctx context.Context) (SvcResponse, error)\n}\n\ntype svcClient struct {\n\tbase *baseClient\n}\n\nvar _ SvcClient = (*svcClient)(nil)\n\nfunc (c *svcClient) Dummy(ctx context.Context) (resp SvcResponse, err error) {\n\t// Now make the actual call to the API\n\t_, err = callAPI(ctx, c.base, \"GET\", \"/dummy\", nil, nil, &resp)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// HTTPDoer is an interface which can be used to swap out the default\n// HTTP client (http.DefaultClient) with your own custom implementation.\n// This can be used to inject middleware or mock responses during unit tests.\ntype HTTPDoer interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n\n// baseClient holds all the information we need to make requests to an Encore application\ntype baseClient struct {\n\thttpClient HTTPDoer // The HTTP client which will be used for all API requests\n\tbaseURL    *url.URL // The base URL which API requests will be made against\n\tuserAgent  string   // What user agent we will use in the API requests\n}\n\n// Do sends the req to the Encore application adding the authorization token as required.\nfunc (b *baseClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", b.userAgent)\n\n\t// Merge the base URL and the API URL\n\treq.URL = b.baseURL.ResolveReference(req.URL)\n\treq.Host = req.URL.Host\n\n\t// Finally, make the request via the configured HTTP Client\n\treturn b.httpClient.Do(req)\n}\n\n// callAPI is used by each generated API method to actually make request and decode the responses\nfunc callAPI(ctx context.Context, client *baseClient, method, path string, headers http.Header, body, resp any) (http.Header, error) {\n\t// Encode the API body\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tbodyBytes, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"marshal request: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(bodyBytes)\n\t}\n\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, path, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request: %w\", err)\n\t}\n\n\t// Add any headers to the request\n\tfor header, values := range headers {\n\t\tfor _, value := range values {\n\t\t\treq.Header.Add(header, value)\n\t\t}\n\t}\n\n\t// Make the request via the base client\n\trawResponse, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = rawResponse.Body.Close()\n\t}()\n\tif rawResponse.StatusCode >= 400 {\n\t\t// Read the full body sent back\n\t\tbody, err := io.ReadAll(rawResponse.Body)\n\t\tif err != nil {\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response without readable body: %s\", rawResponse.Status),\n\t\t\t}\n\t\t}\n\n\t\t// Attempt to decode the error response as a structured APIError\n\t\tapiError := &APIError{}\n\t\tif err := json.Unmarshal(body, apiError); err != nil {\n\t\t\t// If the error is not a parsable as an APIError, then return an error with the raw body\n\t\t\treturn nil, &APIError{\n\t\t\t\tCode:    ErrUnknown,\n\t\t\t\tMessage: fmt.Sprintf(\"got error response: %s\", string(body)),\n\t\t\t}\n\t\t}\n\t\treturn nil, apiError\n\t}\n\n\t// Decode the response\n\tif resp != nil {\n\t\tif err := json.NewDecoder(rawResponse.Body).Decode(resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"decode response: %w\", err)\n\t\t}\n\t}\n\treturn rawResponse.Header, nil\n}\n\n// APIError is the error type returned by the API\ntype APIError struct {\n\tCode    ErrCode `json:\"code\"`\n\tMessage string  `json:\"message\"`\n\tDetails any     `json:\"details\"`\n}\n\nfunc (e *APIError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Code, e.Message)\n}\n\ntype ErrCode int\n\nconst (\n\t// ErrOK indicates the operation was successful.\n\tErrOK ErrCode = 0\n\n\t// ErrCanceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tErrCanceled ErrCode = 1\n\n\t// ErrUnknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tErrUnknown ErrCode = 2\n\n\t// ErrInvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInvalidArgument ErrCode = 3\n\n\t// ErrDeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// The gRPC framework will generate this error code when the deadline is\n\t// exceeded.\n\tErrDeadlineExceeded ErrCode = 4\n\n\t// ErrNotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrNotFound ErrCode = 5\n\n\t// ErrAlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrAlreadyExists ErrCode = 6\n\n\t// ErrPermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// This error code will not be generated by the gRPC core framework,\n\t// but expect authentication middleware to use it.\n\tErrPermissionDenied ErrCode = 7\n\n\t// ErrResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// This error code will be generated by the gRPC framework in\n\t// out-of-memory and server overload situations, or when a message is\n\t// larger than the configured maximum size.\n\tErrResourceExhausted ErrCode = 8\n\n\t// ErrFailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//\n\t//\t(a) Use Unavailable if the client can retry just the failing call.\n\t//\t(b) Use Aborted if the client should retry at a higher-level\n\t//\t    (e.g., restarting a read-modify-write sequence).\n\t//\t(c) Use FailedPrecondition if the client should not retry until\n\t//\t    the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//\t    fails because the directory is non-empty, FailedPrecondition\n\t//\t    should be returned since the client should not retry unless\n\t//\t    they have first fixed up the directory by deleting files from it.\n\t//\t(d) Use FailedPrecondition if the client performs conditional\n\t//\t    REST Get/Update/Delete on a resource and the resource on the\n\t//\t    server does not match the condition. E.g., conflicting\n\t//\t    read-modify-write on the same resource.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrFailedPrecondition ErrCode = 9\n\n\t// ErrAborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// ErrAborted, and Unavailable.\n\tErrAborted ErrCode = 10\n\n\t// ErrOutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// may be rotated to a 64-bit file without error.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// ErrOutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrOutOfRange ErrCode = 11\n\n\t// ErrUnimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// This is not an error, but a feature not available.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrUnimplemented ErrCode = 12\n\n\t// ErrInternal means some invariant expected by the underlying system has\n\t// been broken. This is not a per-message error, it is a global\n\t// conditions check.\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrInternal ErrCode = 13\n\n\t// ErrUnavailable indicates the service is currently unavailable.\n\t// This is most likely a transient condition, which can be corrected by\n\t// retrying with a backoff.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tErrUnavailable ErrCode = 14\n\n\t// ErrDataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// This error code is only defined in the gRPC library, and only for\n\t// unrecoverable data loss (i.e., data loss resulting from errors\n\t// like hard disk corruption or bandwidth exceeded).\n\t//\n\t// This error code will not be generated by the gRPC framework.\n\tErrDataLoss ErrCode = 15\n\n\t// ErrUnauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// The gRPC framework will generate this error code when the\n\t// authentication metadata is invalid or a Credentials callback fails,\n\t// but also expect authentication middleware to generate it.\n\tErrUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of the error code\nfunc (c ErrCode) String() string {\n\tswitch c {\n\tcase ErrOK:\n\t\treturn \"ok\"\n\tcase ErrCanceled:\n\t\treturn \"canceled\"\n\tcase ErrUnknown:\n\t\treturn \"unknown\"\n\tcase ErrInvalidArgument:\n\t\treturn \"invalid_argument\"\n\tcase ErrDeadlineExceeded:\n\t\treturn \"deadline_exceeded\"\n\tcase ErrNotFound:\n\t\treturn \"not_found\"\n\tcase ErrAlreadyExists:\n\t\treturn \"already_exists\"\n\tcase ErrPermissionDenied:\n\t\treturn \"permission_denied\"\n\tcase ErrResourceExhausted:\n\t\treturn \"resource_exhausted\"\n\tcase ErrFailedPrecondition:\n\t\treturn \"failed_precondition\"\n\tcase ErrAborted:\n\t\treturn \"aborted\"\n\tcase ErrOutOfRange:\n\t\treturn \"out_of_range\"\n\tcase ErrUnimplemented:\n\t\treturn \"unimplemented\"\n\tcase ErrInternal:\n\t\treturn \"internal\"\n\tcase ErrUnavailable:\n\t\treturn \"unavailable\"\n\tcase ErrDataLoss:\n\t\treturn \"data_loss\"\n\tcase ErrUnauthenticated:\n\t\treturn \"unauthenticated\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// MarshalJSON converts the error code to a human-readable string\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\treturn []byte(fmt.Sprintf(\"\\\"%s\\\"\", c)), nil\n}\n\n// UnmarshalJSON converts the human-readable string to an error code\nfunc (c *ErrCode) UnmarshalJSON(b []byte) error {\n\tswitch string(b) {\n\tcase \"\\\"ok\\\"\":\n\t\t*c = ErrOK\n\tcase \"\\\"canceled\\\"\":\n\t\t*c = ErrCanceled\n\tcase \"\\\"unknown\\\"\":\n\t\t*c = ErrUnknown\n\tcase \"\\\"invalid_argument\\\"\":\n\t\t*c = ErrInvalidArgument\n\tcase \"\\\"deadline_exceeded\\\"\":\n\t\t*c = ErrDeadlineExceeded\n\tcase \"\\\"not_found\\\"\":\n\t\t*c = ErrNotFound\n\tcase \"\\\"already_exists\\\"\":\n\t\t*c = ErrAlreadyExists\n\tcase \"\\\"permission_denied\\\"\":\n\t\t*c = ErrPermissionDenied\n\tcase \"\\\"resource_exhausted\\\"\":\n\t\t*c = ErrResourceExhausted\n\tcase \"\\\"failed_precondition\\\"\":\n\t\t*c = ErrFailedPrecondition\n\tcase \"\\\"aborted\\\"\":\n\t\t*c = ErrAborted\n\tcase \"\\\"out_of_range\\\"\":\n\t\t*c = ErrOutOfRange\n\tcase \"\\\"unimplemented\\\"\":\n\t\t*c = ErrUnimplemented\n\tcase \"\\\"internal\\\"\":\n\t\t*c = ErrInternal\n\tcase \"\\\"unavailable\\\"\":\n\t\t*c = ErrUnavailable\n\tcase \"\\\"data_loss\\\"\":\n\t\t*c = ErrDataLoss\n\tcase \"\\\"unauthenticated\\\"\":\n\t\t*c = ErrUnauthenticated\n\tdefault:\n\t\t*c = ErrUnknown\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_httpstatus_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\nexport namespace svc {\n    export interface Response {\n        message: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.dummy = this.dummy.bind(this)\n        }\n\n        public async dummy(): Promise<Response> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"GET\", `/dummy`)\n            return await resp.json() as Response\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\n/**\n * Svc is a service for testing the client generator.\n */\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.cookieDummy = this.cookieDummy.bind(this)\n        this.cookiesOnly = this.cookiesOnly.bind(this)\n        this.dummy = this.dummy.bind(this)\n        this.imported = this.imported.bind(this)\n        this.noTypes = this.noTypes.bind(this)\n        this.onlyPathParams = this.onlyPathParams.bind(this)\n        this.root = this.root.bind(this)\n    }\n\n    async cookieDummy(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            baz: params.headerBaz,\n            num: params.headerNum === undefined ? undefined : String(params.headerNum),\n        })\n\n        const query = makeRecord({\n            bar:  params.queryBar,\n            foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n            list: params.queryList.map((v) => String(v)),\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            baz: params.baz,\n            foo: params.foo,\n        }\n\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/cookie-dummy`, JSON.stringify(body), {headers, query})\n        return await resp.json()\n    }\n\n    async cookiesOnly(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/cookies-only`)\n        return await resp.json()\n    }\n\n    async dummy(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            baz: params.headerBaz,\n            num: params.headerNum === undefined ? undefined : String(params.headerNum),\n        })\n\n        const query = makeRecord({\n            bar:  params.queryBar,\n            foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n            list: params.queryList.map((v) => String(v)),\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            baz: params.baz,\n            foo: params.foo,\n        }\n\n        await this.baseClient.callTypedAPI(\"POST\", `/dummy`, JSON.stringify(body), {headers, query})\n    }\n\n    async imported(params) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/imported`, JSON.stringify(params))\n        return await resp.json()\n    }\n\n    async noTypes() {\n        await this.baseClient.callTypedAPI(\"POST\", `/type-less`)\n    }\n\n    async onlyPathParams(pathParam, pathParam2) {\n        // Now make the actual call to the API\n        const resp = await this.baseClient.callTypedAPI(\"POST\", `/path/${encodeURIComponent(pathParam)}/${encodeURIComponent(pathParam2)}`)\n        return await resp.json()\n    }\n\n    async root(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            baz: params.headerBaz,\n            num: params.headerNum === undefined ? undefined : String(params.headerNum),\n        })\n\n        const query = makeRecord({\n            bar:  params.queryBar,\n            foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n            list: params.queryList.map((v) => String(v)),\n        })\n\n        // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n        const body = {\n            baz: params.baz,\n            foo: params.foo,\n        }\n\n        await this.baseClient.callTypedAPI(\"POST\", `/`, JSON.stringify(body), {headers, query})\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n\n    }\n\n    async getAuthData() {\n        let authData;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data = {};\n\n            data.headers = makeRecord({\n                cookie:        authData.cookie,\n                \"x-api-token\": authData.token,\n            })\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_list_of_union_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.dummy = this.dummy.bind(this)\n    }\n\n    async dummy(params) {\n        // Convert our params into the objects we need for the request\n        const query = makeRecord({\n            listOfUnion: params.listOfUnion.map((v) => String(v)),\n        })\n\n        await this.baseClient.callTypedAPI(\"GET\", `/dummy`, undefined, {query})\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData() {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_list_of_union_openapi.json",
    "content": "{\n  \"components\": {\n    \"responses\": {\n      \"APIError\": {\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"externalDocs\": {\n                \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#Error\"\n              },\n              \"properties\": {\n                \"code\": {\n                  \"description\": \"Error code\",\n                  \"example\": \"not_found\",\n                  \"externalDocs\": {\n                    \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\"\n                  },\n                  \"type\": \"string\"\n                },\n                \"details\": {\n                  \"description\": \"Error details\",\n                  \"type\": \"object\"\n                },\n                \"message\": {\n                  \"description\": \"Error message\",\n                  \"type\": \"string\"\n                }\n              },\n              \"title\": \"APIError\",\n              \"type\": \"object\"\n            }\n          }\n        },\n        \"description\": \"Error response\"\n      }\n    }\n  },\n  \"info\": {\n    \"description\": \"Generated by encore\",\n    \"title\": \"API for app\",\n    \"version\": \"1\",\n    \"x-logo\": {\n      \"altText\": \"Encore logo\",\n      \"backgroundColor\": \"#EEEEE1\",\n      \"url\": \"https://encore.dev/assets/branding/logo/logo-black.png\"\n    }\n  },\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/dummy\": {\n      \"get\": {\n        \"operationId\": \"GET:svc.dummy\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"listOfUnion\",\n            \"required\": true,\n            \"schema\": {\n              \"items\": {\n                \"enum\": [\n                  \"a\",\n                  \"b\"\n                ],\n                \"type\": \"string\"\n              },\n              \"type\": \"array\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"description\": \"Encore local dev environment\",\n      \"url\": \"http://localhost:4000\"\n    }\n  ]\n}"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_list_of_union_shared.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\nimport type { CookieWithOptions } from \"encore.dev/api\";\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\n/**\n * Import the endpoint handlers to derive the types for the client.\n */\nimport { dummy as api_svc_svc_dummy } from \"~backend/svc/svc\";\n\nexport namespace svc {\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.dummy = this.dummy.bind(this)\n        }\n\n        public async dummy(params: RequestType<typeof api_svc_svc_dummy>): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const query = makeRecord<string, string | string[]>({\n                listOfUnion: params.listOfUnion.map((v) => String(v)),\n            })\n\n            await this.baseClient.callTypedAPI(`/dummy`, {query, method: \"GET\", body: undefined})\n        }\n    }\n}\n\n\ntype PickMethods<Type> = Omit<CallParameters, \"method\"> & { method?: Type };\n\n// Helper type to omit all fields that are cookies.\ntype OmitCookie<T> = {\n  [K in keyof T as T[K] extends CookieWithOptions<any> ? never : K]: T[K];\n};\n\ntype RequestType<Type extends (...args: any[]) => any> =\n  Parameters<Type> extends [infer H, ...any[]]\n    ? OmitCookie<H>\n    : void;\n\ntype ResponseType<Type extends (...args: any[]) => any> = OmitCookie<Awaited<ReturnType<Type>>>;\n\nfunction dateReviver(key: string, value: any): any {\n  if (\n    typeof value === \"string\" &&\n    value.length >= 10 &&\n    value.charCodeAt(0) >= 48 && // '0'\n    value.charCodeAt(0) <= 57 // '9'\n  ) {\n    const parsedDate = new Date(value);\n    if (!isNaN(parsedDate.getTime())) {\n      return parsedDate;\n    }\n  }\n  return value;\n}\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nimport {\n  StreamInOutHandlerFn,\n  StreamInHandlerFn,\n  StreamOutHandlerFn,\n} from \"encore.dev/api\";\n\ntype StreamRequest<Type> = Type extends\n  | StreamInOutHandlerFn<any, infer Req, any>\n  | StreamInHandlerFn<any, infer Req, any>\n  | StreamOutHandlerFn<any, any>\n  ? Req\n  : never;\n\ntype StreamResponse<Type> = Type extends\n  | StreamInOutHandlerFn<any, any, infer Resp>\n  | StreamInHandlerFn<any, any, infer Resp>\n  | StreamOutHandlerFn<any, infer Resp>\n  ? Resp\n  : never;\n\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data, dateReviver));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data, dateReviver));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data, dateReviver))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(path: string, params?: CallParameters): Promise<Response> {\n        return this.callAPI(path, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(path: string, params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_list_of_union_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\nexport namespace svc {\n    export interface Request {\n        listOfUnion: (\"a\" | \"b\")[]\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.dummy = this.dummy.bind(this)\n        }\n\n        public async dummy(params: Request): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const query = makeRecord<string, string | string[]>({\n                listOfUnion: params.listOfUnion.map((v) => String(v)),\n            })\n\n            await this.baseClient.callTypedAPI(\"GET\", `/dummy`, undefined, {query})\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_openapi.json",
    "content": "{\n  \"components\": {\n    \"responses\": {\n      \"APIError\": {\n        \"content\": {\n          \"application/json\": {\n            \"schema\": {\n              \"externalDocs\": {\n                \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#Error\"\n              },\n              \"properties\": {\n                \"code\": {\n                  \"description\": \"Error code\",\n                  \"example\": \"not_found\",\n                  \"externalDocs\": {\n                    \"url\": \"https://pkg.go.dev/encore.dev/beta/errs#ErrCode\"\n                  },\n                  \"type\": \"string\"\n                },\n                \"details\": {\n                  \"description\": \"Error details\",\n                  \"type\": \"object\"\n                },\n                \"message\": {\n                  \"description\": \"Error message\",\n                  \"type\": \"string\"\n                }\n              },\n              \"title\": \"APIError\",\n              \"type\": \"object\"\n            }\n          }\n        },\n        \"description\": \"Error response\"\n      }\n    }\n  },\n  \"info\": {\n    \"description\": \"Generated by encore\",\n    \"title\": \"API for app\",\n    \"version\": \"1\",\n    \"x-logo\": {\n      \"altText\": \"Encore logo\",\n      \"backgroundColor\": \"#EEEEE1\",\n      \"url\": \"https://encore.dev/assets/branding/logo/logo-black.png\"\n    }\n  },\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.root\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"baz\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"num\",\n            \"schema\": {\n              \"type\": \"number\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"foo\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"bar\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"list\",\n            \"schema\": {\n              \"items\": {\n                \"type\": \"boolean\"\n              },\n              \"type\": \"array\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"baz\": {\n                    \"title\": \"Baz is better\\n\",\n                    \"type\": \"string\"\n                  },\n                  \"foo\": {\n                    \"title\": \"Foo is good\\n\",\n                    \"type\": \"number\"\n                  }\n                },\n                \"required\": [\n                  \"baz\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/cookie-dummy\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.cookieDummy\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"baz\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"num\",\n            \"schema\": {\n              \"type\": \"number\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"foo\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"bar\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"list\",\n            \"schema\": {\n              \"items\": {\n                \"type\": \"boolean\"\n              },\n              \"type\": \"array\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"baz\": {\n                    \"title\": \"Baz is better\\n\",\n                    \"type\": \"string\"\n                  },\n                  \"foo\": {\n                    \"title\": \"Foo is good\\n\",\n                    \"type\": \"number\"\n                  }\n                },\n                \"required\": [\n                  \"baz\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/cookies-only\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.cookiesOnly\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/dummy\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.dummy\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"baz\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"header\",\n            \"name\": \"num\",\n            \"schema\": {\n              \"type\": \"number\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"foo\",\n            \"schema\": {\n              \"type\": \"boolean\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"bar\",\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"form\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": true,\n            \"in\": \"query\",\n            \"name\": \"list\",\n            \"schema\": {\n              \"items\": {\n                \"type\": \"boolean\"\n              },\n              \"type\": \"array\"\n            },\n            \"style\": \"form\"\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"baz\": {\n                    \"title\": \"Baz is better\\n\",\n                    \"type\": \"string\"\n                  },\n                  \"foo\": {\n                    \"title\": \"Foo is good\\n\",\n                    \"type\": \"number\"\n                  }\n                },\n                \"required\": [\n                  \"baz\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/imported\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.imported\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"name\"\n                ],\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"message\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/path/{pathParam}/{pathParam2}\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.onlyPathParams\",\n        \"parameters\": [\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"pathParam\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          },\n          {\n            \"allowEmptyValue\": true,\n            \"explode\": false,\n            \"in\": \"path\",\n            \"name\": \"pathParam2\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"style\": \"simple\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"message\"\n                  ],\n                  \"type\": \"object\"\n                }\n              }\n            },\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    },\n    \"/type-less\": {\n      \"post\": {\n        \"operationId\": \"POST:svc.noTypes\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success response\"\n          },\n          \"default\": {\n            \"$ref\": \"#/components/responses/APIError\"\n          }\n        }\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"description\": \"Encore local dev environment\",\n      \"url\": \"http://localhost:4000\"\n    }\n  ]\n}"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_shared.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\nimport type { CookieWithOptions } from \"encore.dev/api\";\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * Import the auth handler to be able to derive the auth type\n */\nimport type { auth as auth_auth } from \"~backend/svc/svc\";\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    /**\n     * Allows you to set the authentication data to be used for each\n     * request either by passing in a static object or by passing in\n     * a function which returns a new object for each request.\n     */\n    auth?: RequestType<typeof auth_auth> | AuthDataGenerator\n}\n\n/**\n * Import the endpoint handlers to derive the types for the client.\n */\nimport {\n    cookieDummy as api_svc_svc_cookieDummy,\n    cookiesOnly as api_svc_svc_cookiesOnly,\n    dummy as api_svc_svc_dummy,\n    imported as api_svc_svc_imported,\n    onlyPathParams as api_svc_svc_onlyPathParams,\n    root as api_svc_svc_root\n} from \"~backend/svc/svc\";\n\n/**\n * Svc is a service for testing the client generator.\n */\nexport namespace svc {\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.cookieDummy = this.cookieDummy.bind(this)\n            this.cookiesOnly = this.cookiesOnly.bind(this)\n            this.dummy = this.dummy.bind(this)\n            this.imported = this.imported.bind(this)\n            this.noTypes = this.noTypes.bind(this)\n            this.onlyPathParams = this.onlyPathParams.bind(this)\n            this.root = this.root.bind(this)\n        }\n\n        public async cookieDummy(params: RequestType<typeof api_svc_svc_cookieDummy>): Promise<ResponseType<typeof api_svc_svc_cookieDummy>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.headerBaz,\n                num: params.headerNum === undefined ? undefined : String(params.headerNum),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar:  params.queryBar,\n                foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n                list: params.queryList?.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                baz: params.baz,\n                foo: params.foo,\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(`/cookie-dummy`, {headers, query, method: \"POST\", body: JSON.stringify(body)})\n            return JSON.parse(await resp.text(), dateReviver) as ResponseType<typeof api_svc_svc_cookieDummy>\n        }\n\n        public async cookiesOnly(params: RequestType<typeof api_svc_svc_cookiesOnly>): Promise<ResponseType<typeof api_svc_svc_cookiesOnly>> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(`/cookies-only`, {method: \"POST\", body: undefined})\n            return JSON.parse(await resp.text(), dateReviver) as ResponseType<typeof api_svc_svc_cookiesOnly>\n        }\n\n        public async dummy(params: RequestType<typeof api_svc_svc_dummy>): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.headerBaz,\n                num: params.headerNum === undefined ? undefined : String(params.headerNum),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar:  params.queryBar,\n                foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n                list: params.queryList?.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                baz: params.baz,\n                foo: params.foo,\n            }\n\n            await this.baseClient.callTypedAPI(`/dummy`, {headers, query, method: \"POST\", body: JSON.stringify(body)})\n        }\n\n        public async imported(params: RequestType<typeof api_svc_svc_imported>): Promise<ResponseType<typeof api_svc_svc_imported>> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(`/imported`, {method: \"POST\", body: JSON.stringify(params)})\n            return JSON.parse(await resp.text(), dateReviver) as ResponseType<typeof api_svc_svc_imported>\n        }\n\n        public async noTypes(): Promise<void> {\n            await this.baseClient.callTypedAPI(`/type-less`, {method: \"POST\", body: undefined})\n        }\n\n        public async onlyPathParams(params: { pathParam: string, pathParam2: string }): Promise<ResponseType<typeof api_svc_svc_onlyPathParams>> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(`/path/${encodeURIComponent(params.pathParam)}/${encodeURIComponent(params.pathParam2)}`, {method: \"POST\", body: undefined})\n            return JSON.parse(await resp.text(), dateReviver) as ResponseType<typeof api_svc_svc_onlyPathParams>\n        }\n\n        public async root(params: RequestType<typeof api_svc_svc_root>): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.headerBaz,\n                num: params.headerNum === undefined ? undefined : String(params.headerNum),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar:  params.queryBar,\n                foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n                list: params.queryList?.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                baz: params.baz,\n                foo: params.foo,\n            }\n\n            await this.baseClient.callTypedAPI(`/`, {headers, query, method: \"POST\", body: JSON.stringify(body)})\n        }\n    }\n}\n\n\ntype PickMethods<Type> = Omit<CallParameters, \"method\"> & { method?: Type };\n\n// Helper type to omit all fields that are cookies.\ntype OmitCookie<T> = {\n  [K in keyof T as T[K] extends CookieWithOptions<any> ? never : K]: T[K];\n};\n\ntype RequestType<Type extends (...args: any[]) => any> =\n  Parameters<Type> extends [infer H, ...any[]]\n    ? OmitCookie<H>\n    : void;\n\ntype ResponseType<Type extends (...args: any[]) => any> = OmitCookie<Awaited<ReturnType<Type>>>;\n\nfunction dateReviver(key: string, value: any): any {\n  if (\n    typeof value === \"string\" &&\n    value.length >= 10 &&\n    value.charCodeAt(0) >= 48 && // '0'\n    value.charCodeAt(0) <= 57 // '9'\n  ) {\n    const parsedDate = new Date(value);\n    if (!isNaN(parsedDate.getTime())) {\n      return parsedDate;\n    }\n  }\n  return value;\n}\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nimport {\n  StreamInOutHandlerFn,\n  StreamInHandlerFn,\n  StreamOutHandlerFn,\n} from \"encore.dev/api\";\n\ntype StreamRequest<Type> = Type extends\n  | StreamInOutHandlerFn<any, infer Req, any>\n  | StreamInHandlerFn<any, infer Req, any>\n  | StreamOutHandlerFn<any, any>\n  ? Req\n  : never;\n\ntype StreamResponse<Type> = Type extends\n  | StreamInOutHandlerFn<any, any, infer Resp>\n  | StreamInHandlerFn<any, any, infer Resp>\n  | StreamOutHandlerFn<any, infer Resp>\n  ? Resp\n  : never;\n\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data, dateReviver));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data, dateReviver));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data, dateReviver))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n// AuthDataGenerator is a function that returns a new instance of the authentication data required by this API\nexport type AuthDataGenerator = () =>\n  | RequestType<typeof auth_auth>\n  | Promise<RequestType<typeof auth_auth> | undefined>\n  | undefined;\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n    readonly authGenerator?: AuthDataGenerator\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        let authData: RequestType<typeof auth_auth> | undefined;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data: CallParameters = {};\n\n            data.headers = makeRecord<string, string>({\n                cookie:        authData.cookie,\n                \"x-api-token\": authData.token,\n            });\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(path: string, params?: CallParameters): Promise<Response> {\n        return this.callAPI(path, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(path: string, params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_stream_javascript.js",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * Local is the base URL for calling the Encore application's API.\n */\nexport const Local = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name) {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr) {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target = \"prod\", options = undefined) {\n        const base = new BaseClient(target, options ?? {})\n        this.svc = new svc.ServiceClient(base)\n    }\n}\n\nclass SvcServiceClient {\n    constructor(baseClient) {\n        this.baseClient = baseClient\n        this.inOutWithHandshake = this.inOutWithHandshake.bind(this)\n        this.inOutWithoutHandshake = this.inOutWithoutHandshake.bind(this)\n        this.inWithHandshake = this.inWithHandshake.bind(this)\n        this.inWithResponse = this.inWithResponse.bind(this)\n        this.inWithResponseAndHandshake = this.inWithResponseAndHandshake.bind(this)\n        this.inWithoutHandshake = this.inWithoutHandshake.bind(this)\n        this.outWithHandshake = this.outWithHandshake.bind(this)\n        this.outWithoutHandshake = this.outWithoutHandshake.bind(this)\n    }\n\n    /**\n     * InOut stream type variants\n     */\n    async inOutWithHandshake(pathParam, params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"some-header\": params.headerValue,\n        })\n\n        const query = makeRecord({\n            \"some-query\": params.queryValue,\n        })\n\n        return await this.baseClient.createStreamInOut(`/inout/${encodeURIComponent(pathParam)}`, {headers, query})\n    }\n\n    async inOutWithoutHandshake() {\n        return await this.baseClient.createStreamInOut(`/inout/noHandshake`)\n    }\n\n    /**\n     * In stream type variants\n     */\n    async inWithHandshake(pathParam, params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"some-header\": params.headerValue,\n        })\n\n        const query = makeRecord({\n            \"some-query\": params.queryValue,\n        })\n\n        return await this.baseClient.createStreamOut(`/in/${encodeURIComponent(pathParam)}`, {headers, query})\n    }\n\n    async inWithResponse() {\n        return await this.baseClient.createStreamOut(`/in/withResponse`)\n    }\n\n    async inWithResponseAndHandshake(params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"some-header\": params.headerValue,\n        })\n\n        const query = makeRecord({\n            pathParam:    params.pathParam,\n            \"some-query\": params.queryValue,\n        })\n\n        return await this.baseClient.createStreamOut(`/in/withResponseAndHandshake`, {headers, query})\n    }\n\n    async inWithoutHandshake() {\n        return await this.baseClient.createStreamOut(`/in/noHandshake`)\n    }\n\n    /**\n     * Out stream type variants\n     */\n    async outWithHandshake(pathParam, params) {\n        // Convert our params into the objects we need for the request\n        const headers = makeRecord({\n            \"some-header\": params.headerValue,\n        })\n\n        const query = makeRecord({\n            \"some-query\": params.queryValue,\n        })\n\n        return await this.baseClient.createStreamIn(`/out/${encodeURIComponent(pathParam)}`, {headers, query})\n    }\n\n    async outWithoutHandshake() {\n        return await this.baseClient.createStreamIn(`/out/noHandshake`)\n    }\n}\n\nexport const svc = {\n    ServiceClient: SvcServiceClient\n}\n\n\nfunction encodeQuery(parts) {\n    const pairs = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]])\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\nfunction makeRecord(record) {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record\n}\n\n\nfunction encodeWebSocketHeaders(headers) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    hasUpdateHandlers = [];\n\n    constructor(url, headers) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers));\n        }\n\n        this.ws = new WebSocket(url, protocols);\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type, handler) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type, handler) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn {\n    buffer = [];\n\n    constructor(url, headers) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next() {\n        for await (const next of this) return next;\n    }\n\n    async *[Symbol.asyncIterator]() {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift();\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) break;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut {\n    constructor(url, headers) {\n        let responseResolver;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response() {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n\nconst boundFetch = fetch.bind(this)\n\nclass BaseClient {\n    constructor(baseURL, options) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-JS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {}\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData() {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : '';\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut(path, params) {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    async callTypedAPI(method, path, body, params) {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    async callAPI(method, path, body, params) {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\nfunction isAPIErrorResponse(err) {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code) {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    constructor(status, response) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if (Object.setPrototypeOf == undefined) {\n            this.__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if (Error.captureStackTrace !== undefined) {\n            Error.captureStackTrace(this, this.constructor);\n        }\n\n        /**\n         * The HTTP status code associated with the error.\n         */\n        this.status = status\n\n        /**\n         * The Encore error code\n         */\n        this.code = response.code\n\n        /**\n         * The error details\n         */\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err) {\n    return err instanceof APIError;\n}\n\nexport const ErrCode = {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK: \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled: \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown: \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument: \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded: \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound: \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists: \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied: \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted: \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition: \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted: \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange: \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented: \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal: \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable: \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss: \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated: \"unauthenticated\"\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_stream_shared.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\nimport type { CookieWithOptions } from \"encore.dev/api\";\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\n/**\n * Import the endpoint handlers to derive the types for the client.\n */\nimport {\n    inOutWithHandshake as api_svc_svc_inOutWithHandshake,\n    inOutWithoutHandshake as api_svc_svc_inOutWithoutHandshake,\n    inWithHandshake as api_svc_svc_inWithHandshake,\n    inWithResponse as api_svc_svc_inWithResponse,\n    inWithResponseAndHandshake as api_svc_svc_inWithResponseAndHandshake,\n    inWithoutHandshake as api_svc_svc_inWithoutHandshake,\n    outWithHandshake as api_svc_svc_outWithHandshake,\n    outWithoutHandshake as api_svc_svc_outWithoutHandshake\n} from \"~backend/svc/svc\";\n\nexport namespace svc {\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.inOutWithHandshake = this.inOutWithHandshake.bind(this)\n            this.inOutWithoutHandshake = this.inOutWithoutHandshake.bind(this)\n            this.inWithHandshake = this.inWithHandshake.bind(this)\n            this.inWithResponse = this.inWithResponse.bind(this)\n            this.inWithResponseAndHandshake = this.inWithResponseAndHandshake.bind(this)\n            this.inWithoutHandshake = this.inWithoutHandshake.bind(this)\n            this.outWithHandshake = this.outWithHandshake.bind(this)\n            this.outWithoutHandshake = this.outWithoutHandshake.bind(this)\n        }\n\n        /**\n         * InOut stream type variants\n         */\n        public async inOutWithHandshake(params: RequestType<typeof api_svc_svc_inOutWithHandshake>): Promise<StreamInOut<StreamRequest<typeof api_svc_svc_inOutWithHandshake>, StreamResponse<typeof api_svc_svc_inOutWithHandshake>>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamInOut(`/inout/${encodeURIComponent(params.pathParam)}`, {headers, query})\n        }\n\n        public async inOutWithoutHandshake(): Promise<StreamInOut<StreamRequest<typeof api_svc_svc_inOutWithoutHandshake>, StreamResponse<typeof api_svc_svc_inOutWithoutHandshake>>> {\n            return await this.baseClient.createStreamInOut(`/inout/noHandshake`)\n        }\n\n        /**\n         * In stream type variants\n         */\n        public async inWithHandshake(params: RequestType<typeof api_svc_svc_inWithHandshake>): Promise<StreamOut<StreamRequest<typeof api_svc_svc_inWithHandshake>, void>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamOut(`/in/${encodeURIComponent(params.pathParam)}`, {headers, query})\n        }\n\n        public async inWithResponse(): Promise<StreamOut<StreamRequest<typeof api_svc_svc_inWithResponse>, StreamResponse<typeof api_svc_svc_inWithResponse>>> {\n            return await this.baseClient.createStreamOut(`/in/withResponse`)\n        }\n\n        public async inWithResponseAndHandshake(params: RequestType<typeof api_svc_svc_inWithResponseAndHandshake>): Promise<StreamOut<StreamRequest<typeof api_svc_svc_inWithResponseAndHandshake>, StreamResponse<typeof api_svc_svc_inWithResponseAndHandshake>>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                pathParam:    params.pathParam,\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamOut(`/in/withResponseAndHandshake`, {headers, query})\n        }\n\n        public async inWithoutHandshake(): Promise<StreamOut<StreamRequest<typeof api_svc_svc_inWithoutHandshake>, void>> {\n            return await this.baseClient.createStreamOut(`/in/noHandshake`)\n        }\n\n        /**\n         * Out stream type variants\n         */\n        public async outWithHandshake(params: RequestType<typeof api_svc_svc_outWithHandshake>): Promise<StreamIn<StreamResponse<typeof api_svc_svc_outWithHandshake>>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamIn(`/out/${encodeURIComponent(params.pathParam)}`, {headers, query})\n        }\n\n        public async outWithoutHandshake(): Promise<StreamIn<StreamResponse<typeof api_svc_svc_outWithoutHandshake>>> {\n            return await this.baseClient.createStreamIn(`/out/noHandshake`)\n        }\n    }\n}\n\n\ntype PickMethods<Type> = Omit<CallParameters, \"method\"> & { method?: Type };\n\n// Helper type to omit all fields that are cookies.\ntype OmitCookie<T> = {\n  [K in keyof T as T[K] extends CookieWithOptions<any> ? never : K]: T[K];\n};\n\ntype RequestType<Type extends (...args: any[]) => any> =\n  Parameters<Type> extends [infer H, ...any[]]\n    ? OmitCookie<H>\n    : void;\n\ntype ResponseType<Type extends (...args: any[]) => any> = OmitCookie<Awaited<ReturnType<Type>>>;\n\nfunction dateReviver(key: string, value: any): any {\n  if (\n    typeof value === \"string\" &&\n    value.length >= 10 &&\n    value.charCodeAt(0) >= 48 && // '0'\n    value.charCodeAt(0) <= 57 // '9'\n  ) {\n    const parsedDate = new Date(value);\n    if (!isNaN(parsedDate.getTime())) {\n      return parsedDate;\n    }\n  }\n  return value;\n}\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nimport {\n  StreamInOutHandlerFn,\n  StreamInHandlerFn,\n  StreamOutHandlerFn,\n} from \"encore.dev/api\";\n\ntype StreamRequest<Type> = Type extends\n  | StreamInOutHandlerFn<any, infer Req, any>\n  | StreamInHandlerFn<any, infer Req, any>\n  | StreamOutHandlerFn<any, any>\n  ? Req\n  : never;\n\ntype StreamResponse<Type> = Type extends\n  | StreamInOutHandlerFn<any, any, infer Resp>\n  | StreamInHandlerFn<any, any, infer Resp>\n  | StreamOutHandlerFn<any, infer Resp>\n  ? Resp\n  : never;\n\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data, dateReviver));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data, dateReviver));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data, dateReviver))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(path: string, params?: CallParameters): Promise<Response> {\n        return this.callAPI(path, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(path: string, params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_stream_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n}\n\nexport namespace svc {\n    export interface Handshake {\n        headerValue: string\n        queryValue: string\n    }\n\n    export interface Handshake {\n        headerValue: string\n        queryValue: string\n    }\n\n    export interface Handshake {\n        headerValue: string\n        queryValue: string\n        pathParam: string\n    }\n\n    export interface Handshake {\n        headerValue: string\n        queryValue: string\n    }\n\n    export interface InMsg {\n        data: string\n    }\n\n    export interface InMsg {\n        data: string\n    }\n\n    export interface InMsg {\n        data: string\n    }\n\n    export interface InMsg {\n        data: string\n    }\n\n    export interface InMsg {\n        data: string\n    }\n\n    export interface InMsg {\n        data: string\n    }\n\n    export interface OutMsg {\n        user: number\n        msg: string\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.inOutWithHandshake = this.inOutWithHandshake.bind(this)\n            this.inOutWithoutHandshake = this.inOutWithoutHandshake.bind(this)\n            this.inWithHandshake = this.inWithHandshake.bind(this)\n            this.inWithResponse = this.inWithResponse.bind(this)\n            this.inWithResponseAndHandshake = this.inWithResponseAndHandshake.bind(this)\n            this.inWithoutHandshake = this.inWithoutHandshake.bind(this)\n            this.outWithHandshake = this.outWithHandshake.bind(this)\n            this.outWithoutHandshake = this.outWithoutHandshake.bind(this)\n        }\n\n        /**\n         * InOut stream type variants\n         */\n        public async inOutWithHandshake(pathParam: string, params: Handshake): Promise<StreamInOut<InMsg, OutMsg>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamInOut(`/inout/${encodeURIComponent(pathParam)}`, {headers, query})\n        }\n\n        public async inOutWithoutHandshake(): Promise<StreamInOut<InMsg, OutMsg>> {\n            return await this.baseClient.createStreamInOut(`/inout/noHandshake`)\n        }\n\n        /**\n         * In stream type variants\n         */\n        public async inWithHandshake(pathParam: string, params: Handshake): Promise<StreamOut<InMsg, void>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamOut(`/in/${encodeURIComponent(pathParam)}`, {headers, query})\n        }\n\n        public async inWithResponse(): Promise<StreamOut<InMsg, OutMsg>> {\n            return await this.baseClient.createStreamOut(`/in/withResponse`)\n        }\n\n        public async inWithResponseAndHandshake(params: Handshake): Promise<StreamOut<InMsg, OutMsg>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                pathParam:    params.pathParam,\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamOut(`/in/withResponseAndHandshake`, {headers, query})\n        }\n\n        public async inWithoutHandshake(): Promise<StreamOut<InMsg, void>> {\n            return await this.baseClient.createStreamOut(`/in/noHandshake`)\n        }\n\n        /**\n         * Out stream type variants\n         */\n        public async outWithHandshake(pathParam: string, params: Handshake): Promise<StreamIn<OutMsg>> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                \"some-header\": params.headerValue,\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                \"some-query\": params.queryValue,\n            })\n\n            return await this.baseClient.createStreamIn(`/out/${encodeURIComponent(pathParam)}`, {headers, query})\n        }\n\n        public async outWithoutHandshake(): Promise<StreamIn<OutMsg>> {\n            return await this.baseClient.createStreamIn(`/out/noHandshake`)\n        }\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/expected_typescript.ts",
    "content": "// Code generated by the Encore v0.0.0-develop client generator. DO NOT EDIT.\n\n// Disable eslint, jshint, and jslint for this file.\n/* eslint-disable */\n/* jshint ignore:start */\n/*jslint-disable*/\n\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `https://${name}-app.encr.app`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`pr${pr}`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the app Encore application.\n */\nexport default class Client {\n    public readonly svc: svc.ServiceClient\n    private readonly options: ClientOptions\n    private readonly target: string\n\n\n    /**\n     * Creates a Client for calling the public and authenticated APIs of your Encore application.\n     *\n     * @param target  The target which the client should be configured to use. See Local and Environment for options.\n     * @param options Options for the client\n     */\n    constructor(target: BaseURL, options?: ClientOptions) {\n        this.target = target\n        this.options = options ?? {}\n        const base = new BaseClient(this.target, this.options)\n        this.svc = new svc.ServiceClient(base)\n    }\n\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n}\n\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n\n    /**\n     * Allows you to set the authentication data to be used for each\n     * request either by passing in a static object or by passing in\n     * a function which returns a new object for each request.\n     */\n    auth?: svc.AuthParams | AuthDataGenerator\n}\n\n/**\n * Svc is a service for testing the client generator.\n */\nexport namespace svc {\n    export interface AuthParams {\n        cookie?: string\n        token?: string\n    }\n\n    export interface Request {\n        /**\n         * Foo is good\n         */\n        foo?: number\n\n        /**\n         * Baz is better\n         */\n        baz: string\n\n        queryFoo?: boolean\n        queryBar?: string\n        queryList?: boolean[]\n        headerBaz?: string\n        headerNum?: number\n    }\n\n    export interface Request {\n        /**\n         * Foo is good\n         */\n        foo?: number\n\n        /**\n         * Baz is better\n         */\n        baz: string\n\n        queryFoo?: boolean\n        queryBar?: string\n        queryList?: boolean[]\n        headerBaz?: string\n        headerNum?: number\n    }\n\n    export interface Request {\n        /**\n         * Foo is good\n         */\n        foo?: number\n\n        /**\n         * Baz is better\n         */\n        baz: string\n\n        queryFoo?: boolean\n        queryBar?: string\n        queryList?: boolean[]\n        headerBaz?: string\n        headerNum?: number\n    }\n\n    export class ServiceClient {\n        private baseClient: BaseClient\n\n        constructor(baseClient: BaseClient) {\n            this.baseClient = baseClient\n            this.cookieDummy = this.cookieDummy.bind(this)\n            this.cookiesOnly = this.cookiesOnly.bind(this)\n            this.dummy = this.dummy.bind(this)\n            this.imported = this.imported.bind(this)\n            this.noTypes = this.noTypes.bind(this)\n            this.onlyPathParams = this.onlyPathParams.bind(this)\n            this.root = this.root.bind(this)\n        }\n\n        public async cookieDummy(params: Request): Promise<{\n}> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.headerBaz,\n                num: params.headerNum === undefined ? undefined : String(params.headerNum),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar:  params.queryBar,\n                foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n                list: params.queryList?.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                baz: params.baz,\n                foo: params.foo,\n            }\n\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/cookie-dummy`, JSON.stringify(body), {headers, query})\n            return await resp.json() as {\n}\n        }\n\n        public async cookiesOnly(params: {\n}): Promise<{\n}> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/cookies-only`)\n            return await resp.json() as {\n}\n        }\n\n        public async dummy(params: Request): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.headerBaz,\n                num: params.headerNum === undefined ? undefined : String(params.headerNum),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar:  params.queryBar,\n                foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n                list: params.queryList?.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                baz: params.baz,\n                foo: params.foo,\n            }\n\n            await this.baseClient.callTypedAPI(\"POST\", `/dummy`, JSON.stringify(body), {headers, query})\n        }\n\n        public async imported(params: common_stuff.ImportedRequest): Promise<common_stuff.ImportedResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/imported`, JSON.stringify(params))\n            return await resp.json() as common_stuff.ImportedResponse\n        }\n\n        public async noTypes(): Promise<void> {\n            await this.baseClient.callTypedAPI(\"POST\", `/type-less`)\n        }\n\n        public async onlyPathParams(pathParam: string, pathParam2: string): Promise<common_stuff.ImportedResponse> {\n            // Now make the actual call to the API\n            const resp = await this.baseClient.callTypedAPI(\"POST\", `/path/${encodeURIComponent(pathParam)}/${encodeURIComponent(pathParam2)}`)\n            return await resp.json() as common_stuff.ImportedResponse\n        }\n\n        public async root(params: Request): Promise<void> {\n            // Convert our params into the objects we need for the request\n            const headers = makeRecord<string, string>({\n                baz: params.headerBaz,\n                num: params.headerNum === undefined ? undefined : String(params.headerNum),\n            })\n\n            const query = makeRecord<string, string | string[]>({\n                bar:  params.queryBar,\n                foo:  params.queryFoo === undefined ? undefined : String(params.queryFoo),\n                list: params.queryList?.map((v) => String(v)),\n            })\n\n            // Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\n            const body: Record<string, any> = {\n                baz: params.baz,\n                foo: params.foo,\n            }\n\n            await this.baseClient.callTypedAPI(\"POST\", `/`, JSON.stringify(body), {headers, query})\n        }\n    }\n}\n\nexport namespace common_stuff {\n    export interface ImportedRequest {\n        name: string\n    }\n\n    export interface ImportedResponse {\n        message: string\n    }\n}\n\n\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(`${key}=${encodeURIComponent(v)}`)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(JSON.parse(event.data));\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(JSON.parse(event.data))\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }\n}\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, \"method\" | \"body\" | \"headers\"> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n\n// AuthDataGenerator is a function that returns a new instance of the authentication data required by this API\nexport type AuthDataGenerator = () =>\n  | svc.AuthParams\n  | Promise<svc.AuthParams | undefined>\n  | undefined;\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n    readonly authGenerator?: AuthDataGenerator\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"app-Generated-TS-Client (Encore/v0.0.0-develop)\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {\n        let authData: svc.AuthParams | undefined;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data: CallParameters = {};\n\n            data.headers = makeRecord<string, string>({\n                cookie:        authData.cookie,\n                \"x-api-token\": authData.token,\n            });\n\n            return data;\n        }\n\n        return undefined;\n    }\n\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        return this.callAPI(method, path, body, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(method: string, path: string, body?: RequestInit[\"body\"], params?: CallParameters): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,\n            method,\n            body: body ?? null,\n        }\n\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: `request failed: status ${response.status}` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/input.ts",
    "content": "-- encore.app --\n{\"id\": \"\"}\n\n-- package.json --\n{\"name\": \"ts-test-app\"}\n\n-- common-stuff/types.ts --\nexport interface ImportedRequest {\n  name: string;\n}\n\nexport interface ImportedResponse {\n  message: string;\n}\n\n-- svc/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\n// Svc is a service for testing the client generator.\nexport default new Service(\"svc\");\n\n-- svc/svc.ts --\nimport { Header, Query, api, Gateway, Cookie } from \"encore.dev/api\";\nimport { authHandler } from \"encore.dev/auth\";\nimport type { ImportedRequest, ImportedResponse } from \"../common-stuff/types\";\n\ninterface UnusedType {\n  foo: Foo;\n}\n\nexport const root = api(\n  { expose: true, method: \"POST\", path: \"/\" },\n  async (req: Request) => { },\n);\n\nexport const imported = api(\n  { expose: true, method: \"POST\", path: \"/imported\" },\n  async (req: ImportedRequest) : Promise<ImportedResponse> => { },\n);\n\nexport const onlyPathParams = api(\n  { expose: true, method: \"POST\", path: \"/path/:pathParam/:pathParam2\" },\n  async (req: { pathParam: string, pathParam2: string }) : Promise<ImportedResponse> => { },\n);\n\n\nexport const dummy = api(\n  { expose: true, method: \"POST\", path: \"/dummy\" },\n  async (req: Request) => { },\n);\n\n\nexport const noTypes = api(\n  { expose: true, method: \"POST\", path: \"/type-less\" },\n  async () => { },\n)\nexport const cookiesOnly = api(\n  { expose: true, method: \"POST\", path: \"/cookies-only\" },\n  async (req: { field: Cookie<'cookie'> }): Promise<{ cookie: Cookie<'cookie'> }> => {\n    return { cookie: { value: \"value\" } }\n  },\n)\n\nexport const cookieDummy = api(\n  { expose: true, method: \"POST\", path: \"/cookie-dummy\" },\n  async (req: Request): Promise<{ cookie: Cookie<'cookie'> }> => { return { cookie: { value: \"value\" } } },\n);\n\nexport interface AuthParams {\n  cookie?: Header<'Cookie'>\n  token?: Header<'x-api-token'>\n  cookieValue?: Cookie<'actual-cookie'>\n}\n\nexport interface AuthData {\n  userID: string;\n}\n\nexport const auth = authHandler<AuthParams, AuthData>(\n  async (params) => {\n    return { userID: \"my-user-id\" };\n  }\n)\n\nexport const gw = new Gateway({\n  authHandler: auth,\n})\n\ninterface Request {\n  // Foo is good\n  foo?: number;\n  // Baz is better\n  baz: string;\n\n  queryFoo?: Query<boolean, \"foo\">;\n  queryBar?: Query<\"bar\">;\n  queryList?: Query<boolean[], \"list\">\n  headerBaz?: Header<\"baz\">;\n  headerNum?: Header<number, \"num\">;\n  cookieQux?: Cookie<\"qux\">;\n  cookieQuux?: Cookie<number, \"quux\">;\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/input_decimal.ts",
    "content": "-- encore.app --\n{\"id\": \"\"}\n\n-- package.json --\n{\"name\": \"ts-test-app\"}\n\n-- svc/svc.ts --\nimport { api } from \"encore.dev/api\";\nimport { Decimal } from \"encore.dev/types\";\n\nexport const dummy = api(\n  { expose: true, method: \"GET\", path: \"/dummy\" },\n  async (req: Request): Promise<Response> => {},\n);\n\ninterface Request {\n  message: string,\n  val: Decimal,\n}\ninterface Response {\n  result: Decimal\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/input_httpstatus.ts",
    "content": "-- encore.app --\n{\"id\": \"\"}\n\n-- package.json --\n{\"name\": \"ts-test-app\"}\n\n-- svc/svc.ts --\nimport { api, HttpStatus } from \"encore.dev/api\";\n\nexport const dummy = api(\n  { expose: true, method: \"GET\", path: \"/dummy\" },\n  async (): Promise<Response> => {},\n);\n\n\ninterface Response {\n  message: string,\n  status: HttpStatus,\n}\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/input_list_of_union.ts",
    "content": "-- encore.app --\n{\"id\": \"\"}\n\n-- package.json --\n{\"name\": \"ts-test-app\"}\n\n-- svc/svc.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const dummy = api(\n  { expose: true, method: \"GET\", path: \"/dummy\" },\n  async (req: Request) => {},\n);\n\n\ninterface Request {\n    listOfUnion: (\"a\" | \"b\")[]\n}\n\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/input_stream.ts",
    "content": "-- encore.app --\n{\"id\": \"\"}\n\n-- package.json --\n{\"name\": \"ts-test-app\"}\n\n-- svc/svc.ts --\nimport { api, Header, Query } from \"encore.dev/api\";\n\ninterface Handshake {\n    headerValue: Header<\"some-header\">;\n    queryValue: Query<\"some-query\">;\n    pathParam: string;\n}\ninterface InMsg {\n    data: string;\n}\ninterface OutMsg {\n    user: number;\n    msg: string;\n}\n\n// InOut stream type variants\nexport const inOutWithHandshake = api.streamInOut<Handshake, InMsg, OutMsg>(\n  { expose: true, path: \"/inout/:pathParam\" },\n  async (handshake: Handshake, stream) => {},\n);\nexport const inOutWithoutHandshake = api.streamInOut<InMsg, OutMsg>(\n  { expose: true, path: \"/inout/noHandshake\" },\n  async (stream) => {},\n);\n\n// Out stream type variants\nexport const outWithHandshake = api.streamOut<Handshake, OutMsg>(\n  { expose: true, path: \"/out/:pathParam\" },\n  async (handshake: Handshake, stream) => {},\n);\n\nexport const outWithoutHandshake = api.streamOut<OutMsg>(\n  { expose: true, path: \"/out/noHandshake\" },\n  async (stream) => {},\n);\n\n// In stream type variants\nexport const inWithHandshake = api.streamIn<Handshake, InMsg>(\n  { expose: true, path: \"/in/:pathParam\" },\n  async (handshake: Handshake, stream) => {},\n);\n\nexport const inWithoutHandshake = api.streamIn<InMsg>(\n  { expose: true, path: \"/in/noHandshake\" },\n  async (stream) => {},\n);\n\nexport const inWithResponse = api.streamIn<InMsg, OutMsg>(\n  { expose: true, path: \"/in/withResponse\" },\n  async (stream) => {},\n);\n\nexport const inWithResponseAndHandshake = api.streamIn<Handshake, InMsg, OutMsg>(\n  { expose: true, path: \"/in/withResponseAndHandshake\" },\n  async (handshake: Handshake, stream) => {},\n);\n"
  },
  {
    "path": "pkg/clientgen/testdata/tsapp/tsconfig.json",
    "content": "// Note: this config is here purely to remove errors about promises not being present in IDE's when viewing the expected_typescript.ts file\n{\n  \"compilerOptions\": {\n    \"target\": \"es2022\",\n    \"paths\": {\n      \"~backend/*\": [\"./*\"]\n    },\n  }\n}\n"
  },
  {
    "path": "pkg/clientgen/types.go",
    "content": "package clientgen\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"sort\"\n\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\nfunc getNamedTypes(md *meta.Data, set clientgentypes.ServiceSet) *typeRegistry {\n\tr := &typeRegistry{\n\t\tmd:         md,\n\t\tnamespaces: make(map[string][]*schema.Decl),\n\t\tseenDecls:  make(map[uint32]bool),\n\t\tdeclRefs:   make(map[uint32]map[uint32]bool),\n\t}\n\tfor _, svc := range md.Svcs {\n\t\tif !set.Has(svc.Name) {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, rpc := range svc.Rpcs {\n\t\t\tif rpc.AccessType != meta.RPC_PRIVATE {\n\t\t\t\tr.Visit(rpc.HandshakeSchema)\n\t\t\t\tr.Visit(rpc.RequestSchema)\n\t\t\t\tr.Visit(rpc.ResponseSchema)\n\t\t\t}\n\t\t}\n\t}\n\n\tif md.AuthHandler != nil && md.AuthHandler.Params != nil {\n\t\tr.Visit(md.AuthHandler.Params)\n\t}\n\n\treturn r\n}\n\n// typeRegistry computes the visible set of type declarations\n// and how to group them into namespaces.\ntype typeRegistry struct {\n\tmd         *meta.Data\n\tnamespaces map[string][]*schema.Decl\n\tseenDecls  map[uint32]bool\n\tdeclRefs   map[uint32]map[uint32]bool // tracks which decls reference which other decls\n\tcurrDecl   *schema.Decl               // may be nil\n}\n\nfunc (v *typeRegistry) Decls(name string) []*schema.Decl {\n\treturn v.namespaces[name]\n}\n\nfunc (v *typeRegistry) Namespaces() []string {\n\tnss := make([]string, 0, len(v.namespaces))\n\tfor ns := range v.namespaces {\n\t\tnss = append(nss, ns)\n\t}\n\tsort.Strings(nss)\n\treturn nss\n}\n\nfunc (v *typeRegistry) Visit(typ *schema.Type) {\n\tif typ == nil {\n\t\treturn\n\t}\n\tswitch t := typ.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\tv.visitNamed(t.Named)\n\tcase *schema.Type_List:\n\t\tv.Visit(t.List.Elem)\n\tcase *schema.Type_Map:\n\t\tv.Visit(t.Map.Key)\n\t\tv.Visit(t.Map.Value)\n\tcase *schema.Type_Struct:\n\t\tfor _, f := range t.Struct.Fields {\n\t\t\tv.Visit(f.Typ)\n\t\t}\n\tcase *schema.Type_Builtin, *schema.Type_TypeParameter, *schema.Type_Literal:\n\t// do nothing\n\n\tcase *schema.Type_Pointer:\n\t\tv.Visit(t.Pointer.Base)\n\n\tcase *schema.Type_Option:\n\t\tv.Visit(t.Option.Value)\n\n\tcase *schema.Type_Config:\n\t\tv.Visit(t.Config.Elem)\n\n\tcase *schema.Type_Union:\n\t\tfor _, tt := range t.Union.Types {\n\t\t\tv.Visit(tt)\n\t\t}\n\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled type: %+v\", reflect.TypeOf(typ.Typ)))\n\t}\n}\n\nfunc (v *typeRegistry) visitDecl(decl *schema.Decl) {\n\tif decl == nil {\n\t\treturn\n\t}\n\n\tif !v.seenDecls[decl.Id] {\n\t\tv.seenDecls[decl.Id] = true\n\t\tns := decl.Loc.PkgName\n\t\tv.namespaces[ns] = append(v.namespaces[ns], decl)\n\n\t\t// Set currDecl when processing this and then reset it\n\t\tprev := v.currDecl\n\t\tv.currDecl = decl\n\t\tv.Visit(decl.Type)\n\t\tv.currDecl = prev\n\t}\n}\n\nfunc (v *typeRegistry) visitNamed(n *schema.Named) {\n\tto := n.Id\n\tcurr := v.currDecl\n\tif curr != nil {\n\t\tfrom := curr.Id\n\t\tif _, ok := v.declRefs[from]; !ok {\n\t\t\tv.declRefs[from] = make(map[uint32]bool)\n\t\t}\n\t\tv.declRefs[from][to] = true\n\t}\n\n\tdecl := v.md.Decls[to]\n\tv.visitDecl(decl)\n\n\t// Add transitive refs\n\tif curr != nil {\n\t\tfrom := curr.Id\n\t\tfor to2 := range v.declRefs[to] {\n\t\t\tv.declRefs[from][to2] = true\n\t\t}\n\t}\n\n\tfor _, typeArg := range n.TypeArguments {\n\t\tv.Visit(typeArg)\n\t}\n}\n\nfunc (v *typeRegistry) IsRecursiveRef(from, to uint32) bool {\n\treturn v.declRefs[from][to] && v.declRefs[to][from]\n}\n"
  },
  {
    "path": "pkg/clientgen/typescript.go",
    "content": "package clientgen\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/cockroachdb/errors\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/parser/encoding\"\n\t\"encr.dev/pkg/clientgen/clientgentypes\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/idents\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n/* The TypeScript generator generates code that looks like this:\nexport namespace task {\n\texport interface AddParams {\n\t\tdescription: string\n\t}\n\n\texport class ServiceClient {\n\t\tpublic Add(params: task_AddParams): Promise<task_AddResponse> {\n\t\t\t// ...\n\t\t}\n\t}\n}\n\n*/\n\n// tsGenVersion allows us to introduce breaking changes in the generated code but behind a switch\n// meaning that people with client code reliant on the old behaviour can continue to generate the\n// old code.\ntype tsGenVersion int\n\nconst (\n\t// TsInitial is the originally released typescript generator\n\tTsInitial tsGenVersion = iota\n\n\t// TsExperimental can be used to lock experimental or uncompleted features in the generated code\n\t// It should always be the last item in the enum\n\tTsExperimental\n)\n\nconst typescriptGenLatestVersion = TsExperimental - 1\n\ntype typescript struct {\n\t*bytes.Buffer\n\tmd               *meta.Data\n\tappSlug          string\n\ttyps             *typeRegistry\n\tcurrDecl         *schema.Decl\n\tgeneratorVersion tsGenVersion\n\tsharedTypes      bool\n\tclientTarget     string\n\n\tseenJSON           bool // true if a JSON type was seen\n\tseenStream         bool // true if a stream endpoint was seen\n\tseenHeaderResponse bool // true if we've seen a header used in a response object\n\thasAuth            bool // true if we've seen an authentication handler\n\tauthIsComplexType  bool // true if the auth type is a complex type\n}\n\nfunc (ts *typescript) Version() int {\n\treturn int(ts.generatorVersion)\n}\n\nfunc (ts *typescript) Generate(p clientgentypes.GenerateParams) (err error) {\n\tdefer ts.handleBailout(&err)\n\n\tts.Buffer = p.Buf\n\tts.md = p.Meta\n\tts.appSlug = p.AppSlug\n\tts.typs = getNamedTypes(p.Meta, p.Services)\n\n\tif ts.md.AuthHandler != nil {\n\t\tif !ts.isAuthCookieOnly() {\n\t\t\tts.hasAuth = true\n\t\t\tts.authIsComplexType = ts.md.AuthHandler.Params.GetBuiltin() != schema.Builtin_STRING\n\t\t}\n\t}\n\n\tts.WriteString(\"// \" + doNotEditHeader() + \"\\n\\n\")\n\tts.WriteString(\"// Disable eslint, jshint, and jslint for this file.\\n\")\n\tts.WriteString(\"/* eslint-disable */\\n\")\n\tts.WriteString(\"/* jshint ignore:start */\\n\")\n\tts.WriteString(\"/*jslint-disable*/\\n\")\n\n\tif ts.sharedTypes {\n\t\tts.WriteString(\"import type { CookieWithOptions } from \\\"encore.dev/api\\\";\\n\")\n\t}\n\n\tnss := ts.typs.Namespaces()\n\tseenNs := make(map[string]bool)\n\tts.writeClient(p.Services)\n\tfor _, svc := range p.Meta.Svcs {\n\t\tif err := ts.writeService(svc, p.Services, p.Tags); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tseenNs[svc.Name] = true\n\t}\n\tif !ts.sharedTypes {\n\t\tfor _, ns := range nss {\n\t\t\tif !seenNs[ns] {\n\t\t\t\tts.writeNamespace(ns)\n\t\t\t}\n\t\t}\n\t}\n\tts.writeExtraTypes()\n\tts.writeStreamClasses()\n\tif err := ts.writeBaseClient(p.AppSlug); err != nil {\n\t\treturn err\n\t}\n\tts.writeCustomErrorType()\n\n\tif ts.clientTarget != \"\" {\n\t\tfmt.Fprintf(ts, `\nexport default new Client(%s, { requestInit: { credentials: \"include\" } });\n`, ts.clientTarget)\n\t}\n\n\treturn nil\n}\n\nfunc (ts *typescript) getFields(typ *schema.Type) []*schema.Field {\n\tif typ == nil {\n\t\treturn nil\n\t}\n\tswitch typ.Typ.(type) {\n\tcase *schema.Type_Struct:\n\t\treturn typ.GetStruct().Fields\n\tcase *schema.Type_Named:\n\t\tdecl := ts.md.Decls[typ.GetNamed().Id]\n\t\treturn ts.getFields(decl.Type)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (ts *typescript) isAuthCookieOnly() bool {\n\tif ts.md.AuthHandler == nil {\n\t\treturn false\n\t}\n\tfields := ts.getFields(ts.md.AuthHandler.Params)\n\tif fields == nil {\n\t\treturn false\n\t}\n\tfor _, field := range fields {\n\t\tif field.Wire.GetCookie() == nil {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc hasPathParams(rpc *meta.RPC) bool {\n\treturn fns.Any(rpc.Path.Segments, func(s *meta.PathSegment) bool {\n\t\treturn s.Type != meta.PathSegment_LITERAL\n\t})\n}\n\nfunc (ts *typescript) authImportName() string {\n\treturn fmt.Sprintf(\"auth_%s\", validTSIdentifier(ts.md.AuthHandler.Name))\n}\n\nfunc (ts *typescript) writeAuthType() {\n\tif ts.sharedTypes {\n\t\tfmt.Fprintf(ts, \"RequestType<typeof %s>\", ts.authImportName())\n\t} else {\n\t\tts.writeTyp(\"\", ts.md.AuthHandler.Params, 2)\n\t}\n}\n\nfunc rpcImportName(rpc *meta.RPC) string {\n\tfileName := strings.TrimSuffix(rpc.Loc.Filename, filepath.Ext(rpc.Loc.Filename))\n\treturn fmt.Sprintf(\"api_%s_%s_%s\", validTSIdentifier(rpc.ServiceName), validTSIdentifier(fileName), validTSIdentifier(rpc.Name))\n}\n\nfunc getMethodType(rpc *meta.RPC) string {\n\tts := strings.Builder{}\n\tfor i, method := range rpc.HttpMethods {\n\t\tif i > 0 {\n\t\t\tts.WriteString(\" | \")\n\t\t}\n\n\t\tif method == \"*\" {\n\t\t\tts.WriteString(\"string\")\n\t\t} else {\n\t\t\tts.WriteString(\"\\\"\" + method + \"\\\"\")\n\t\t}\n\t}\n\treturn ts.String()\n}\n\nfunc (ts *typescript) writeService(svc *meta.Service, p clientgentypes.ServiceSet, tags clientgentypes.TagSet) error {\n\t// Determine if we have anything worth exposing.\n\t// Either a public RPC or a named type.\n\tisIncluded := hasPublicRPC(svc) && p.Has(svc.Name)\n\n\tdecls := ts.typs.Decls(svc.Name)\n\tif !isIncluded && len(decls) == 0 {\n\t\treturn nil\n\t}\n\n\tif ts.sharedTypes {\n\t\timportsByPath := make(map[string][]string)\n\t\tfor _, rpc := range svc.Rpcs {\n\t\t\tif rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) || rpc.Proto == meta.RPC_RAW ||\n\t\t\t\t(rpc.ResponseSchema == nil && rpc.RequestSchema == nil && !hasPathParams(rpc)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpath := fmt.Sprintf(\"~backend/%s/%s\", rpc.Loc.PkgPath, strings.TrimSuffix(rpc.Loc.Filename, filepath.Ext(rpc.Loc.Filename)))\n\t\t\timportsByPath[path] = append(importsByPath[path], fmt.Sprintf(\"%s as %s\", rpc.Name, rpcImportName(rpc)))\n\t\t}\n\t\tif len(importsByPath) > 0 {\n\t\t\tts.WriteString(`/**\n * Import the endpoint handlers to derive the types for the client.\n */\n`)\n\t\t}\n\t\tfor _, path := range slices.Sorted(maps.Keys(importsByPath)) {\n\t\t\timps := fmt.Sprintf(\" %s \", importsByPath[path][0])\n\t\t\tif len(importsByPath[path]) > 1 {\n\t\t\t\timps = \"\\n    \" + strings.Join(importsByPath[path], \",\\n    \") + \"\\n\"\n\t\t\t}\n\t\t\tfmt.Fprintf(ts, \"import {%s} from \\\"%s\\\";\\n\", imps, path)\n\t\t}\n\t\tts.WriteString(\"\\n\")\n\t}\n\n\tns := svc.Name\n\n\t// Service doc string\n\tif doc := getServiceDoc(ts.md, svc); doc != \"\" {\n\t\tscanner := bufio.NewScanner(strings.NewReader(doc))\n\t\tts.WriteString(\"/**\\n\")\n\t\tfor scanner.Scan() {\n\t\t\tts.WriteString(\" * \")\n\t\t\tts.WriteString(scanner.Text())\n\t\t\tts.WriteByte('\\n')\n\t\t}\n\t\tts.WriteString(\" */\\n\")\n\t}\n\n\tfmt.Fprintf(ts, \"export namespace %s {\\n\", ts.typeName(ns))\n\n\tsort.Slice(decls, func(i, j int) bool {\n\t\treturn decls[i].Name < decls[j].Name\n\t})\n\tif !ts.sharedTypes {\n\t\tfor i, d := range decls {\n\t\t\tif i > 0 {\n\t\t\t\tts.WriteString(\"\\n\")\n\t\t\t}\n\t\t\tts.writeDeclDef(ns, d)\n\t\t}\n\t}\n\n\tif !isIncluded {\n\t\tts.WriteString(\"}\\n\\n\")\n\t\treturn nil\n\t}\n\tts.WriteString(\"\\n\")\n\n\tnumIndent := 1\n\tindent := func() {\n\t\tts.WriteString(strings.Repeat(\"    \", numIndent))\n\t}\n\n\tindent()\n\tfmt.Fprint(ts, \"export class ServiceClient {\\n\")\n\tnumIndent++\n\n\t// Constructor\n\tindent()\n\tts.WriteString(\"private baseClient: BaseClient\\n\\n\")\n\tindent()\n\tts.WriteString(\"constructor(baseClient: BaseClient) {\\n\")\n\tnumIndent++\n\tindent()\n\tts.WriteString(\"this.baseClient = baseClient\\n\")\n\tfor _, rpc := range svc.Rpcs {\n\t\tif rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {\n\t\t\tcontinue\n\t\t}\n\t\tname := ts.memberName(rpc.Name)\n\t\tindent()\n\t\tfmt.Fprintf(ts, \"this.%s = this.%s.bind(this)\\n\", name, name)\n\t}\n\tnumIndent--\n\tindent()\n\tts.WriteString(\"}\\n\")\n\n\t// RPCs\n\tfor _, rpc := range svc.Rpcs {\n\t\tif rpc.AccessType == meta.RPC_PRIVATE || !tags.IsRPCIncluded(rpc) {\n\t\t\tcontinue\n\t\t}\n\n\t\tts.WriteByte('\\n')\n\n\t\t// Doc string\n\t\tif rpc.Doc != nil && *rpc.Doc != \"\" {\n\t\t\tscanner := bufio.NewScanner(strings.NewReader(*rpc.Doc))\n\t\t\tindent()\n\t\t\tts.WriteString(\"/**\\n\")\n\t\t\tfor scanner.Scan() {\n\t\t\t\tindent()\n\t\t\t\tts.WriteString(\" * \")\n\t\t\t\tts.WriteString(scanner.Text())\n\t\t\t\tts.WriteByte('\\n')\n\t\t\t}\n\t\t\tindent()\n\t\t\tts.WriteString(\" */\\n\")\n\t\t}\n\n\t\t// Signature\n\t\tindent()\n\t\tfmt.Fprintf(ts, \"public async %s(\", ts.memberName(rpc.Name))\n\n\t\tisRaw := rpc.Proto == meta.RPC_RAW\n\n\t\tif isRaw && !ts.sharedTypes {\n\t\t\tfmt.Fprintf(ts, \"method: %s, \", getMethodType(rpc))\n\t\t}\n\n\t\tnParams := 0\n\t\t// Avoid a name collision.\n\t\tpayloadName := \"params\"\n\t\tsegmentPrefix := \"\"\n\t\tif ts.sharedTypes {\n\t\t\tsegmentPrefix = payloadName + \".\"\n\t\t}\n\t\tvar isStream = rpc.StreamingRequest || rpc.StreamingResponse\n\t\tvar hasHandshake = rpc.HandshakeSchema != nil\n\t\tvar inlinePathParams = (isRaw || (rpc.RequestSchema == nil && !hasHandshake)) && hasPathParams(rpc) && ts.sharedTypes\n\t\tif inlinePathParams {\n\t\t\tts.WriteString(payloadName + \": { \")\n\t\t}\n\t\tvar rpcPath strings.Builder\n\t\tfor _, s := range rpc.Path.Segments {\n\t\t\trpcPath.WriteByte('/')\n\t\t\tif s.Type != meta.PathSegment_LITERAL {\n\t\t\t\tif !ts.sharedTypes || inlinePathParams {\n\t\t\t\t\tif nParams > 0 {\n\t\t\t\t\t\tts.WriteString(\", \")\n\t\t\t\t\t}\n\n\t\t\t\t\tts.WriteString(ts.nonReservedId(s.Value))\n\t\t\t\t\tts.WriteString(\": \")\n\t\t\t\t\tswitch s.ValueType {\n\t\t\t\t\tcase meta.PathSegment_STRING, meta.PathSegment_UUID:\n\t\t\t\t\t\tts.WriteString(\"string\")\n\t\t\t\t\tcase meta.PathSegment_BOOL:\n\t\t\t\t\t\tts.WriteString(\"boolean\")\n\t\t\t\t\tcase meta.PathSegment_INT8, meta.PathSegment_INT16, meta.PathSegment_INT32, meta.PathSegment_INT64, meta.PathSegment_INT,\n\t\t\t\t\t\tmeta.PathSegment_UINT8, meta.PathSegment_UINT16, meta.PathSegment_UINT32, meta.PathSegment_UINT64, meta.PathSegment_UINT:\n\t\t\t\t\t\tts.WriteString(\"number\")\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tpanic(fmt.Sprintf(\"unhandled PathSegment type %s\", s.ValueType))\n\t\t\t\t\t}\n\t\t\t\t\tif s.Type == meta.PathSegment_WILDCARD || s.Type == meta.PathSegment_FALLBACK {\n\t\t\t\t\t\tts.WriteString(\"[]\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif s.Type == meta.PathSegment_WILDCARD || s.Type == meta.PathSegment_FALLBACK {\n\t\t\t\t\trpcPath.WriteString(\"${\" + segmentPrefix + ts.nonReservedId(s.Value) + \".map(encodeURIComponent).join(\\\"/\\\")}\")\n\t\t\t\t} else {\n\t\t\t\t\trpcPath.WriteString(\"${encodeURIComponent(\" + segmentPrefix + ts.nonReservedId(s.Value) + \")}\")\n\t\t\t\t}\n\t\t\t\tnParams++\n\t\t\t} else {\n\t\t\t\trpcPath.WriteString(s.Value)\n\t\t\t}\n\t\t}\n\t\tif inlinePathParams {\n\t\t\tts.WriteString(\" }\")\n\t\t}\n\n\t\tif (!isStream && rpc.RequestSchema != nil) || (isStream && hasHandshake) {\n\t\t\tif !ts.sharedTypes && nParams > 0 {\n\t\t\t\tts.WriteString(\", \")\n\t\t\t}\n\t\t\tts.WriteString(payloadName + \": \")\n\t\t\tif ts.sharedTypes {\n\t\t\t\tfmt.Fprintf(ts, \"RequestType<typeof %s>\", rpcImportName(rpc))\n\t\t\t} else if isStream {\n\t\t\t\tts.writeTyp(ns, rpc.HandshakeSchema, 0)\n\t\t\t} else {\n\t\t\t\tts.writeTyp(ns, rpc.RequestSchema, 0)\n\n\t\t\t}\n\t\t} else if isRaw {\n\t\t\tif nParams > 0 {\n\t\t\t\tts.WriteString(\", \")\n\t\t\t}\n\t\t\tif !ts.sharedTypes {\n\t\t\t\tts.WriteString(\"body?: RequestInit[\\\"body\\\"], \")\n\t\t\t}\n\n\t\t\tif ts.sharedTypes {\n\t\t\t\tfmt.Fprintf(ts, \"options: PickMethods<%s> = {}\", getMethodType(rpc))\n\t\t\t} else {\n\t\t\t\tts.WriteString(\"options?: CallParameters\")\n\t\t\t}\n\t\t}\n\n\t\tvar direction streamDirection\n\n\t\tif rpc.StreamingRequest && rpc.StreamingResponse {\n\t\t\tdirection = InOut\n\t\t} else if rpc.StreamingRequest {\n\t\t\tdirection = Out\n\t\t} else {\n\t\t\tdirection = In\n\t\t}\n\n\t\twriteStreamRequest := func(ns string, numIndents int) {\n\t\t\tif rpc.RequestSchema == nil {\n\t\t\t\tts.WriteString(\"void\")\n\t\t\t} else if ts.sharedTypes {\n\t\t\t\tfmt.Fprintf(ts, \"StreamRequest<typeof %s>\", rpcImportName(rpc))\n\t\t\t} else {\n\t\t\t\tts.writeTyp(ns, rpc.RequestSchema, numIndents)\n\t\t\t}\n\t\t}\n\t\twriteStreamResponse := func(ns string, numIndents int) {\n\t\t\tif rpc.ResponseSchema == nil {\n\t\t\t\tts.WriteString(\"void\")\n\t\t\t} else if ts.sharedTypes {\n\t\t\t\tts.seenStream = true\n\t\t\t\tfmt.Fprintf(ts, \"StreamResponse<typeof %s>\", rpcImportName(rpc))\n\t\t\t} else {\n\t\t\t\tts.writeTyp(ns, rpc.ResponseSchema, numIndents)\n\t\t\t}\n\t\t}\n\n\t\tts.WriteString(\"): Promise<\")\n\n\t\tif isStream {\n\t\t\tts.seenStream = true\n\t\t\tswitch direction {\n\t\t\tcase InOut:\n\t\t\t\tts.WriteString(\"StreamInOut<\")\n\t\t\t\twriteStreamRequest(ns, 0)\n\t\t\t\tts.WriteString(\", \")\n\t\t\t\twriteStreamResponse(ns, 0)\n\t\t\t\tts.WriteString(\">\")\n\t\t\tcase In:\n\t\t\t\tts.WriteString(\"StreamIn<\")\n\t\t\t\twriteStreamResponse(ns, 0)\n\t\t\t\tts.WriteString(\">\")\n\t\t\tcase Out:\n\t\t\t\tts.WriteString(\"StreamOut<\")\n\t\t\t\twriteStreamRequest(ns, 0)\n\t\t\t\tts.WriteString(\", \")\n\t\t\t\twriteStreamResponse(ns, 0)\n\t\t\t\tts.WriteString(\">\")\n\t\t\t}\n\t\t} else if rpc.ResponseSchema != nil {\n\t\t\tif ts.sharedTypes {\n\t\t\t\tfmt.Fprintf(ts, \"ResponseType<typeof %s>\", rpcImportName(rpc))\n\t\t\t} else {\n\t\t\t\tts.writeTyp(ns, rpc.ResponseSchema, 0)\n\t\t\t}\n\t\t} else if isRaw {\n\t\t\tts.WriteString(\"globalThis.Response\")\n\t\t} else {\n\t\t\tts.WriteString(\"void\")\n\t\t}\n\t\tts.WriteString(\"> {\\n\")\n\n\t\tif isStream {\n\t\t\tif err := ts.streamCallSite(ts.newIdentWriter(numIndent+1), rpc, rpcPath.String(), direction); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"unable to write streaming RPC call site for %ss.%s\", rpc.ServiceName, rpc.Name)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := ts.rpcCallSite(ns, ts.newIdentWriter(numIndent+1), rpc, rpcPath.String()); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"unable to write RPC call site for %s.%s\", rpc.ServiceName, rpc.Name)\n\t\t\t}\n\t\t}\n\n\t\tindent()\n\t\tts.WriteString(\"}\\n\")\n\t}\n\tnumIndent--\n\tindent()\n\tts.WriteString(\"}\\n}\\n\\n\")\n\treturn nil\n}\n\nfunc (ts *typescript) streamCallSite(w *indentWriter, rpc *meta.RPC, rpcPath string, direction streamDirection) error {\n\theaders := \"\"\n\tquery := \"\"\n\n\tif rpc.HandshakeSchema != nil {\n\t\tencs, err := encoding.DescribeRequest(ts.md, rpc.HandshakeSchema, &encoding.Options{SrcNameTag: \"json\"}, \"GET\")\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"stream %s\", rpc.Name)\n\t\t}\n\n\t\thandshakeEnc := encs[0]\n\n\t\tif len(handshakeEnc.HeaderParameters) > 0 || len(handshakeEnc.QueryParameters) > 0 {\n\t\t\tw.WriteString(\"// Convert our params into the objects we need for the request\\n\")\n\t\t}\n\n\t\t// Generate the headers\n\t\tif len(handshakeEnc.HeaderParameters) > 0 {\n\t\t\theaders = \"headers\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range handshakeEnc.HeaderParameters {\n\t\t\t\tref := ts.Dot(\"params\", field.SrcName)\n\t\t\t\tdict[field.WireFormat] = ts.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional)\n\t\t\t}\n\n\t\t\tw.WriteString(\"const headers = makeRecord<string, string>(\")\n\t\t\tts.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\n\t\t// Generate the query string\n\t\tif len(handshakeEnc.QueryParameters) > 0 {\n\t\t\tquery = \"query\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range handshakeEnc.QueryParameters {\n\t\t\t\tif list := field.Type.GetList(); list != nil {\n\t\t\t\t\tdot := ts.Dot(\"params\", field.SrcName)\n\t\t\t\t\tif field.Optional || ts.isRecursive(field.Type) {\n\t\t\t\t\t\tdot += \"?\"\n\t\t\t\t\t}\n\t\t\t\t\tdict[field.WireFormat] = dot +\n\t\t\t\t\t\t\".map((v) => \" + ts.convertBuiltinToString(list.Elem.GetBuiltin(), \"v\", false) + \")\"\n\t\t\t\t} else {\n\t\t\t\t\tdict[field.WireFormat] = ts.convertBuiltinToString(\n\t\t\t\t\t\tfield.Type.GetBuiltin(),\n\t\t\t\t\t\tts.Dot(\"params\", field.SrcName),\n\t\t\t\t\t\tfield.Optional,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw.WriteString(\"const query = makeRecord<string, string | string[]>(\")\n\t\t\tts.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\t}\n\n\t// Build the call to createStream\n\tvar method string\n\n\tswitch direction {\n\tcase InOut:\n\t\tmethod = \"createStreamInOut\"\n\tcase In:\n\t\tmethod = \"createStreamIn\"\n\tcase Out:\n\t\tmethod = \"createStreamOut\"\n\t}\n\n\tcreateStream := fmt.Sprintf(\n\t\t\"this.baseClient.%s(`%s`\",\n\t\tmethod,\n\t\trpcPath,\n\t)\n\n\tif headers != \"\" || query != \"\" {\n\t\tcreateStream += \", {\" + headers\n\n\t\tif headers != \"\" && query != \"\" {\n\t\t\tcreateStream += \", \"\n\t\t}\n\n\t\tif query != \"\" {\n\t\t\tcreateStream += query\n\t\t}\n\n\t\tcreateStream += \"}\"\n\t}\n\tcreateStream += \")\"\n\n\tw.WriteStringf(\"return await %s\\n\", createStream)\n\treturn nil\n}\n\nfunc (ts *typescript) rpcCallSite(ns string, w *indentWriter, rpc *meta.RPC, rpcPath string) error {\n\t// Work out how we're going to encode and call this RPC\n\trpcEncoding, err := encoding.DescribeRPC(ts.md, rpc, &encoding.Options{SrcNameTag: \"json\"})\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"rpc %s\", rpc.Name)\n\t}\n\n\t// Raw end points just pass through the request\n\t// and need no further code generation\n\tif rpc.Proto == meta.RPC_RAW {\n\t\tif ts.sharedTypes {\n\t\t\tw.WriteStringf(\"options.method ||= \\\"%s\\\";\\n\", rpcEncoding.DefaultMethod)\n\t\t}\n\t\tw.WriteString(\"return this.baseClient.callAPI(\")\n\t\tif ts.sharedTypes {\n\t\t\tw.WriteStringf(\n\t\t\t\t\"`%s`, options\",\n\t\t\t\trpcPath,\n\t\t\t)\n\n\t\t} else {\n\t\t\tw.WriteStringf(\n\t\t\t\t\"method, `%s`, body, options\",\n\t\t\t\trpcPath,\n\t\t\t)\n\t\t}\n\t\tw.WriteString(\")\\n\")\n\t\treturn nil\n\t}\n\n\t// Work out how we encode the Request Schema\n\theaders := \"\"\n\tquery := \"\"\n\tbody := \"\"\n\n\tif rpc.RequestSchema != nil {\n\t\treqEnc := rpcEncoding.DefaultRequestEncoding\n\n\t\tif len(reqEnc.HeaderParameters) > 0 || len(reqEnc.QueryParameters) > 0 {\n\t\t\tw.WriteString(\"// Convert our params into the objects we need for the request\\n\")\n\t\t}\n\n\t\t// Generate the headers\n\t\tif len(reqEnc.HeaderParameters) > 0 {\n\t\t\theaders = \"headers\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range reqEnc.HeaderParameters {\n\t\t\t\tref := ts.Dot(\"params\", field.SrcName)\n\t\t\t\tdict[field.WireFormat] = ts.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional)\n\t\t\t}\n\n\t\t\tw.WriteString(\"const headers = makeRecord<string, string>(\")\n\t\t\tts.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\n\t\t// Generate the query string\n\t\tif len(reqEnc.QueryParameters) > 0 {\n\t\t\tquery = \"query\"\n\n\t\t\tdict := make(map[string]string)\n\t\t\tfor _, field := range reqEnc.QueryParameters {\n\t\t\t\tif list := field.Type.GetList(); list != nil {\n\t\t\t\t\tdot := ts.Dot(\"params\", field.SrcName)\n\t\t\t\t\tif field.Optional || ts.isRecursive(field.Type) {\n\t\t\t\t\t\tdot += \"?\"\n\t\t\t\t\t}\n\t\t\t\t\tdict[field.WireFormat] = dot +\n\t\t\t\t\t\t\".map((v) => \" + ts.convertBuiltinToString(list.Elem.GetBuiltin(), \"v\", false) + \")\"\n\t\t\t\t} else {\n\t\t\t\t\tdict[field.WireFormat] = ts.convertBuiltinToString(\n\t\t\t\t\t\tfield.Type.GetBuiltin(),\n\t\t\t\t\t\tts.Dot(\"params\", field.SrcName),\n\t\t\t\t\t\tfield.Optional,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tw.WriteString(\"const query = makeRecord<string, string | string[]>(\")\n\t\t\tts.Values(w, dict)\n\t\t\tw.WriteString(\")\\n\\n\")\n\t\t}\n\n\t\t// Generate the body\n\t\tif len(reqEnc.BodyParameters) > 0 {\n\t\t\tif len(reqEnc.HeaderParameters) == 0 && len(reqEnc.QueryParameters) == 0 && (!ts.sharedTypes || !hasPathParams(rpc)) {\n\t\t\t\t// In the simple case we can just encode the params as the body directly\n\t\t\t\tbody = \"JSON.stringify(params)\"\n\t\t\t} else {\n\t\t\t\t// Else we need a new struct called \"body\"\n\t\t\t\tbody = \"JSON.stringify(body)\"\n\n\t\t\t\tdict := make(map[string]string)\n\t\t\t\tfor _, field := range reqEnc.BodyParameters {\n\t\t\t\t\tdict[field.WireFormat] = ts.Dot(\"params\", field.SrcName)\n\t\t\t\t}\n\n\t\t\t\tw.WriteString(\"// Construct the body with only the fields which we want encoded within the body (excluding query string or header fields)\\nconst body: Record<string, any> = \")\n\t\t\t\tts.Values(w, dict)\n\t\t\t\tw.WriteString(\"\\n\\n\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Build the call to callTypedAPI\n\tcallAPI := \"this.baseClient.callTypedAPI(\"\n\tif !ts.sharedTypes {\n\t\tcallAPI += fmt.Sprintf(\"\\\"%s\\\", \", rpcEncoding.DefaultMethod)\n\t}\n\tcallAPI += fmt.Sprintf(\"`%s`\", rpcPath)\n\tif body != \"\" || headers != \"\" || query != \"\" || ts.sharedTypes {\n\t\tif body == \"\" {\n\t\t\tbody = \"undefined\"\n\t\t}\n\t\tif !ts.sharedTypes {\n\t\t\tcallAPI += \", \" + body\n\t\t}\n\n\t\tif headers != \"\" || query != \"\" || ts.sharedTypes {\n\t\t\tcallAPI += \", {\" + headers\n\n\t\t\tif headers != \"\" && query != \"\" {\n\t\t\t\tcallAPI += \", \"\n\t\t\t}\n\n\t\t\tif query != \"\" {\n\t\t\t\tcallAPI += query\n\t\t\t}\n\n\t\t\tif ts.sharedTypes {\n\t\t\t\tif headers != \"\" || query != \"\" {\n\t\t\t\t\tcallAPI += \", \"\n\t\t\t\t}\n\t\t\t\tcallAPI += fmt.Sprintf(`method: \"%s\", body: %s`, rpcEncoding.DefaultMethod, body)\n\t\t\t}\n\t\t\tcallAPI += \"}\"\n\t\t}\n\t}\n\tcallAPI += \")\"\n\n\t// If there's no response schema, we can just return the call to the API directly\n\tif rpc.ResponseSchema == nil {\n\t\tw.WriteStringf(\"await %s\\n\", callAPI)\n\t\treturn nil\n\t}\n\n\tw.WriteStringf(\"// Now make the actual call to the API\\nconst resp = await %s\\n\", callAPI)\n\n\trespEnc := rpcEncoding.ResponseEncoding\n\n\t// If we don't need to do anything with the body, we can just return the response\n\tif len(respEnc.HeaderParameters) == 0 {\n\t\tif ts.sharedTypes {\n\t\t\tw.WriteString(\"return JSON.parse(await resp.text(), dateReviver) as \")\n\t\t\tfmt.Fprintf(ts, \"ResponseType<typeof %s>\", rpcImportName(rpc))\n\t\t} else {\n\t\t\tw.WriteString(\"return await resp.json() as \")\n\t\t\tts.writeTyp(ns, rpc.ResponseSchema, 0)\n\t\t}\n\t\tw.WriteString(\"\\n\")\n\t\treturn nil\n\t}\n\n\t// Otherwise, we need to add the header fields to the response\n\tw.WriteString(\"\\n//Populate the return object from the JSON body and received headers\\n\")\n\n\tif ts.sharedTypes {\n\t\tw.WriteStringf(\"const rtn = JSON.parse(await resp.text(), dateReviver) as ResponseType<typeof %s>\", rpcImportName(rpc))\n\t} else {\n\t\tw.WriteString(\"const rtn = await resp.json() as \")\n\t\tts.writeTyp(ns, rpc.ResponseSchema, 0)\n\t}\n\tw.WriteString(\"\\n\")\n\n\tfor _, headerField := range respEnc.HeaderParameters {\n\t\tisSetCookie := strings.ToLower(headerField.WireFormat) == \"set-cookie\"\n\t\tif isSetCookie {\n\t\t\tw.WriteString(\"// Skip set-cookie header in browser context as browsers doesn't have access to read it\\n\")\n\t\t\tw.WriteString(\"if (!BROWSER) {\\n\")\n\t\t\tw = w.Indent()\n\t\t}\n\n\t\tts.seenHeaderResponse = true\n\t\tfieldValue := fmt.Sprintf(\"mustBeSet(\\\"Header `%s`\\\", resp.headers.get(\\\"%s\\\"))\", headerField.WireFormat, headerField.WireFormat)\n\n\t\tw.WriteStringf(\"%s = %s\\n\", ts.Dot(\"rtn\", headerField.SrcName), ts.convertStringToBuiltin(headerField.Type.GetBuiltin(), fieldValue))\n\n\t\tif isSetCookie {\n\t\t\tw = w.Dedent()\n\t\t\tw.WriteString(\"}\\n\")\n\t\t}\n\t}\n\n\tw.WriteString(\"return rtn\\n\")\n\treturn nil\n}\n\n// nonReservedId returns the given ID, unless we have it a reserved within the client function _or_ it's a reserved Typescript keyword\nfunc (ts *typescript) nonReservedId(id string) string {\n\tswitch id {\n\t// our reserved keywords (or ID's we use within the generated client functions)\n\tcase \"params\", \"headers\", \"query\", \"body\", \"resp\", \"rtn\":\n\t\treturn \"_\" + id\n\n\t// Typescript & Javascript keywords\n\tcase \"abstract\", \"any\", \"arguments\", \"as\", \"async\", \"await\", \"boolean\", \"break\", \"byte\", \"case\", \"catch\", \"char\",\n\t\t\"class\", \"const\", \"constructor\", \"continue\", \"debugger\", \"declare\", \"default\", \"delete\", \"do\", \"double\", \"else\",\n\t\t\"enum\", \"eval\", \"export\", \"extends\", \"false\", \"final\", \"finally\", \"float\", \"for\", \"from\", \"function\", \"get\",\n\t\t\"goto\", \"if\", \"implements\", \"import\", \"in\", \"instanceof\", \"interface\", \"let\", \"long\", \"module\", \"namespace\",\n\t\t\"native\", \"new\", \"null\", \"number\", \"of\", \"package\", \"private\", \"protected\", \"public\", \"require\", \"return\",\n\t\t\"set\", \"short\", \"static\", \"string\", \"super\", \"switch\", \"symbol\", \"synchronized\", \"this\", \"throw\", \"throws\",\n\t\t\"transient\", \"true\", \"try\", \"type\", \"typeof\", \"var\", \"void\", \"volatile\", \"while\", \"with\", \"yield\":\n\t\treturn \"_\" + id\n\n\tdefault:\n\t\treturn id\n\t}\n}\n\nfunc (ts *typescript) writeNamespace(ns string) {\n\tdecls := ts.typs.Decls(ns)\n\tif len(decls) == 0 {\n\t\treturn\n\t}\n\n\tfmt.Fprintf(ts, \"export namespace %s {\\n\", ts.typeName(ns))\n\tsort.Slice(decls, func(i, j int) bool {\n\t\treturn decls[i].Name < decls[j].Name\n\t})\n\tfor i, d := range decls {\n\t\tif i > 0 {\n\t\t\tts.WriteString(\"\\n\")\n\t\t}\n\n\t\tts.writeDeclDef(ns, d)\n\t}\n\tts.WriteString(\"}\\n\\n\")\n}\n\nfunc (ts *typescript) writeDeclDef(ns string, decl *schema.Decl) {\n\tif decl.Doc != \"\" {\n\t\tscanner := bufio.NewScanner(strings.NewReader(decl.Doc))\n\t\tts.WriteString(\"    /**\\n\")\n\t\tfor scanner.Scan() {\n\t\t\tts.WriteString(\"     * \")\n\t\t\tts.WriteString(scanner.Text())\n\t\t\tts.WriteByte('\\n')\n\t\t}\n\t\tts.WriteString(\"     */\\n\")\n\t}\n\n\tvar typeParams strings.Builder\n\tif len(decl.TypeParams) > 0 {\n\t\ttypeParams.WriteRune('<')\n\n\t\tfor i, typeParam := range decl.TypeParams {\n\t\t\tif i > 0 {\n\t\t\t\ttypeParams.WriteString(\", \")\n\t\t\t}\n\n\t\t\ttypeParams.WriteString(typeParam.Name)\n\t\t}\n\n\t\ttypeParams.WriteRune('>')\n\t}\n\n\t// If it's a struct type, expose it as an interface;\n\t// other types should be type aliases.\n\tif st := decl.Type.GetStruct(); st != nil {\n\t\tfmt.Fprintf(ts, \"    export interface %s%s \", ts.typeName(decl.Name), typeParams.String())\n\t} else {\n\t\tfmt.Fprintf(ts, \"    export type %s%s = \", ts.typeName(decl.Name), typeParams.String())\n\t}\n\n\tprev := ts.currDecl\n\tts.currDecl = decl\n\tts.writeTyp(ns, decl.Type, 1)\n\tts.currDecl = prev\n\tts.WriteString(\"\\n\")\n}\n\nfunc (ts *typescript) writeStreamClasses() {\n\tif ts.sharedTypes {\n\t\tts.WriteString(`\nimport {\n  StreamInOutHandlerFn,\n  StreamInHandlerFn,\n  StreamOutHandlerFn,\n} from \"encore.dev/api\";\n\ntype StreamRequest<Type> = Type extends\n  | StreamInOutHandlerFn<any, infer Req, any>\n  | StreamInHandlerFn<any, infer Req, any>\n  | StreamOutHandlerFn<any, any>\n  ? Req\n  : never;\n\ntype StreamResponse<Type> = Type extends\n  | StreamInOutHandlerFn<any, any, infer Resp>\n  | StreamInHandlerFn<any, any, infer Resp>\n  | StreamOutHandlerFn<any, infer Resp>\n  ? Resp\n  : never;\n\n`)\n\t}\n\n\tparse := \"JSON.parse(event.data)\"\n\tif ts.sharedTypes {\n\t\tparse = \"JSON.parse(event.data, dateReviver)\"\n\t}\n\n\tsend := `\n    async send(msg: Request) {\n        if (this.socket.ws.readyState === WebSocket.CONNECTING) {\n            // await that the socket is opened\n            await new Promise((resolve) => {\n                this.socket.ws.addEventListener(\"open\", resolve, { once: true });\n            });\n        }\n\n        return this.socket.ws.send(JSON.stringify(msg));\n    }`\n\n\treceive := `\n    async next(): Promise<Response | undefined> {\n        for await (const next of this) return next;\n        return undefined;\n    }\n\n    async *[Symbol.asyncIterator](): AsyncGenerator<Response, undefined, void> {\n        while (true) {\n            if (this.buffer.length > 0) {\n                yield this.buffer.shift() as Response;\n            } else {\n                if (this.socket.ws.readyState === WebSocket.CLOSED) return;\n                await this.socket.hasUpdate();\n            }\n        }\n    }`\n\n\tts.WriteString(`\nfunction encodeWebSocketHeaders(headers: Record<string, string>) {\n    // url safe, no pad\n    const base64encoded = btoa(JSON.stringify(headers))\n      .replaceAll(\"=\", \"\")\n      .replaceAll(\"+\", \"-\")\n      .replaceAll(\"/\", \"_\");\n    return \"encore.dev.headers.\" + base64encoded;\n}\n\nclass WebSocketConnection {\n    public ws: WebSocket;\n\n    private hasUpdateHandlers: (() => void)[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let protocols = [\"encore-ws\"];\n        if (headers) {\n            protocols.push(encodeWebSocketHeaders(headers))\n        }\n\n        this.ws = new WebSocket(url, protocols)\n\n        this.on(\"error\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n\n        this.on(\"close\", () => {\n            this.resolveHasUpdateHandlers();\n        });\n    }\n\n    resolveHasUpdateHandlers() {\n        const handlers = this.hasUpdateHandlers;\n        this.hasUpdateHandlers = [];\n\n        for (const handler of handlers) {\n            handler()\n        }\n    }\n\n    async hasUpdate() {\n        // await until a new message have been received, or the socket is closed\n        await new Promise((resolve) => {\n            this.hasUpdateHandlers.push(() => resolve(null))\n        });\n    }\n\n    on(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.addEventListener(type, handler);\n    }\n\n    off(type: \"error\" | \"close\" | \"message\" | \"open\", handler: (event: any) => void) {\n        this.ws.removeEventListener(type, handler);\n    }\n\n    close() {\n        this.ws.close();\n    }\n}\n\nexport class StreamInOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(` + parse + `);\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n` + send + `\n` + receive + `\n}\n\nexport class StreamIn<Response> {\n    public socket: WebSocketConnection;\n    private buffer: Response[] = [];\n\n    constructor(url: string, headers?: Record<string, string>) {\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            this.buffer.push(` + parse + `);\n            this.socket.resolveHasUpdateHandlers();\n        });\n    }\n\n    close() {\n        this.socket.close();\n    }\n` + receive + `\n}\n\nexport class StreamOut<Request, Response> {\n    public socket: WebSocketConnection;\n    private responseValue: Promise<Response>;\n\n    constructor(url: string, headers?: Record<string, string>) {\n        let responseResolver: (_: any) => void;\n        this.responseValue = new Promise((resolve) => responseResolver = resolve);\n\n        this.socket = new WebSocketConnection(url, headers);\n        this.socket.on(\"message\", (event: any) => {\n            responseResolver(` + parse + `)\n        });\n    }\n\n    async response(): Promise<Response> {\n        return this.responseValue;\n    }\n\n    close() {\n        this.socket.close();\n    }\n` + send + `\n}`)\n}\n\nfunc (ts *typescript) writeClient(set clientgentypes.ServiceSet) {\n\tw := ts.newIdentWriter(0)\n\tw.WriteStringf(`\n/**\n * BaseURL is the base URL for calling the Encore application's API.\n */\nexport type BaseURL = string\n\nexport const Local: BaseURL = \"http://localhost:4000\"\n\n/**\n * Environment returns a BaseURL for calling the cloud environment with the given name.\n */\nexport function Environment(name: string): BaseURL {\n    return `+\"`https://${name}-\"+ts.appSlug+\".encr.app`\"+`\n}\n\n/**\n * PreviewEnv returns a BaseURL for calling the preview environment with the given PR number.\n */\nexport function PreviewEnv(pr: number | string): BaseURL {\n    return Environment(`+\"`pr${pr}`\"+`)\n}\n\nconst BROWSER = typeof globalThis === \"object\" && (\"window\" in globalThis);\n\n/**\n * Client is an API client for the `+ts.appSlug+` Encore application.\n */\nexport %sclass Client {\n`, func() string {\n\t\tif ts.clientTarget != \"\" {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn \"default \"\n\t}())\n\n\t{\n\t\tw := w.Indent()\n\n\t\tfor _, svc := range ts.md.Svcs {\n\t\t\tif hasPublicRPC(svc) && set.Has(svc.Name) {\n\t\t\t\tw.WriteStringf(\"public readonly %s: %s.ServiceClient\\n\", ts.memberName(svc.Name), ts.typeName(svc.Name))\n\t\t\t}\n\t\t}\n\t\tw.WriteString(\"private readonly options: ClientOptions\\n\")\n\t\tw.WriteString(\"private readonly target: string\\n\")\n\t\tw.WriteString(\"\\n\")\n\n\t\t// Only include the deprecated constructor if bearer token authentication is being used\n\t\tif ts.hasAuth && !ts.authIsComplexType {\n\t\t\tw.WriteString(`\n/**\n * @deprecated This constructor is deprecated, and you should move to using BaseURL with an Options object\n */\nconstructor(target: string, token?: string)\n`)\n\t\t}\n\n\t\tw.WriteString(`\n/**\n * Creates a Client for calling the public and authenticated APIs of your Encore application.\n *\n * @param target  The target which the client should be configured to use. See Local and Environment for options.\n * @param options Options for the client\n */\nconstructor(target: BaseURL, options?: ClientOptions)`)\n\n\t\tif ts.hasAuth && !ts.authIsComplexType {\n\t\t\tw.WriteString(\"\\nconstructor(target: string | BaseURL = \\\"prod\\\", options?: string | ClientOptions) {\\n\")\n\t\t\t{\n\t\t\t\tw := w.Indent()\n\n\t\t\t\tw.WriteString(`\n// Convert the old constructor parameters to a BaseURL object and a ClientOptions object\nif (!target.startsWith(\"http://\") && !target.startsWith(\"https://\")) {\n    target = Environment(target)\n}\n\nif (typeof options === \"string\") {\n    options = { auth: options }\n}\n\n`)\n\t\t\t}\n\t\t} else {\n\t\t\tw.WriteString(\" {\\n\")\n\t\t}\n\n\t\t{\n\t\t\tw := w.Indent()\n\n\t\t\tw.WriteString(\"this.target = target\\n\")\n\t\t\tw.WriteString(\"this.options = options ?? {}\\n\")\n\t\t\tw.WriteString(\"const base = new BaseClient(this.target, this.options)\\n\")\n\t\t\tfor _, svc := range ts.md.Svcs {\n\t\t\t\tif hasPublicRPC(svc) && set.Has(svc.Name) {\n\t\t\t\t\tw.WriteStringf(\"this.%s = new %s.ServiceClient(base)\\n\", ts.memberName(svc.Name), ts.typeName(svc.Name))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tw.WriteString(\"}\\n\")\n\t}\n\n\tw.WriteString(`\n    /**\n     * Creates a new Encore client with the given client options set.\n     *\n     * @param options Client options to set. They are merged with existing options.\n     **/\n    public with(options: ClientOptions): Client {\n        return new Client(this.target, {\n            ...this.options,\n            ...options,\n        })\n    }\n`)\n\n\tw.WriteString(\"}\\n\")\n\n\thandler := ts.md.AuthHandler\n\tif ts.hasAuth && ts.sharedTypes && ts.authIsComplexType {\n\n\t\tts.WriteString(`\n/**\n * Import the auth handler to be able to derive the auth type\n */\n`)\n\t\tfmt.Fprintf(ts, `import type { %s as %s } from \"~backend/%s/%s\";`,\n\t\t\thandler.Name,\n\t\t\tts.authImportName(),\n\t\t\thandler.Loc.PkgName,\n\t\t\tstrings.TrimSuffix(handler.Loc.Filename, filepath.Ext(handler.Loc.Filename)))\n\t\tts.WriteString(\"\\n\")\n\t}\n\n\tw.WriteString(`\n/**\n * ClientOptions allows you to override any default behaviour within the generated Encore client.\n */\nexport interface ClientOptions {\n    /**\n     * By default the client will use the inbuilt fetch function for making the API requests.\n     * however you can override it with your own implementation here if you want to run custom\n     * code on each API request made or response received.\n     */\n    fetcher?: Fetcher\n\n    /** Default RequestInit to be used for the client */\n    requestInit?: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }\n`)\n\n\tif ts.hasAuth {\n\t\tif !ts.authIsComplexType {\n\t\t\tw.WriteString(`\n    /**\n     * Allows you to set the auth token to be used for each request\n     * either by passing in a static token string or by passing in a function\n     * which returns the auth token.\n     *\n     * These tokens will be sent as bearer tokens in the Authorization header.\n     */\n`)\n\t\t} else {\n\t\t\tw.WriteString(`\n    /**\n     * Allows you to set the authentication data to be used for each\n     * request either by passing in a static object or by passing in\n     * a function which returns a new object for each request.\n     */\n`)\n\t\t}\n\n\t\tw.WriteString(\"    auth?: \")\n\t\tts.writeAuthType()\n\t\tw.WriteString(\" | AuthDataGenerator\\n\")\n\t}\n\n\tw.WriteString(`}\n\n`)\n}\n\nfunc (ts *typescript) writeBaseClient(appSlug string) error {\n\tuserAgent := fmt.Sprintf(\"%s-Generated-TS-Client (Encore/%s)\", appSlug, version.Version)\n\n\treqOmit := `\"method\" | \"body\" | \"headers\"`\n\tif ts.sharedTypes {\n\t\treqOmit = `\"headers\"`\n\t}\n\tfmt.Fprintf(ts, `\n// CallParameters is the type of the parameters to a method call, but require headers to be a Record type\ntype CallParameters = Omit<RequestInit, %s> & {\n    /** Headers to be sent with the request */\n    headers?: Record<string, string>\n\n    /** Query parameters to be sent with the request */\n    query?: Record<string, string | string[]>\n}\n`, reqOmit)\n\n\tif ts.hasAuth {\n\t\tts.WriteString(`\n// AuthDataGenerator is a function that returns a new instance of the authentication data required by this API\nexport type AuthDataGenerator = () =>\n  | `)\n\t\tts.writeAuthType()\n\t\tts.WriteString(`\n  | Promise<`)\n\t\tts.writeAuthType()\n\t\tts.WriteString(` | undefined>\n  | undefined;`)\n\t}\n\n\tts.WriteString(`\n\n// A fetcher is the prototype for the inbuilt Fetch function\nexport type Fetcher = typeof fetch;\n\nconst boundFetch = fetch.bind(this);\n\nclass BaseClient {\n    readonly baseURL: string\n    readonly fetcher: Fetcher\n    readonly headers: Record<string, string>\n    readonly requestInit: Omit<RequestInit, \"headers\"> & { headers?: Record<string, string> }`)\n\n\tif ts.hasAuth {\n\t\tts.WriteString(\"\\n    readonly authGenerator?: AuthDataGenerator\")\n\t}\n\n\tts.WriteString(`\n\n    constructor(baseURL: string, options: ClientOptions) {\n        this.baseURL = baseURL\n        this.headers = {}\n\n        // Add User-Agent header if the script is running in the server\n        // because browsers do not allow setting User-Agent headers to requests\n        if (!BROWSER) {\n            this.headers[\"User-Agent\"] = \"` + userAgent + `\";\n        }\n\n        this.requestInit = options.requestInit ?? {};\n\n        // Setup what fetch function we'll be using in the base client\n        if (options.fetcher !== undefined) {\n            this.fetcher = options.fetcher\n        } else {\n            this.fetcher = boundFetch\n        }`)\n\n\tif ts.hasAuth {\n\t\tts.WriteString(`\n\n        // Setup an authentication data generator using the auth data token option\n        if (options.auth !== undefined) {\n            const auth = options.auth\n            if (typeof auth === \"function\") {\n                this.authGenerator = auth\n            } else {\n                this.authGenerator = () => auth\n            }\n        }`)\n\t}\n\n\tts.WriteString(`\n    }\n\n    async getAuthData(): Promise<CallParameters | undefined> {`)\n\tif ts.hasAuth {\n\t\tts.WriteString(`\n        let authData: `)\n\t\tts.writeAuthType()\n\t\tts.WriteString(` | undefined;\n\n        // If authorization data generator is present, call it and add the returned data to the request\n        if (this.authGenerator) {\n            const mayBePromise = this.authGenerator();\n            if (mayBePromise instanceof Promise) {\n                authData = await mayBePromise;\n            } else {\n                authData = mayBePromise;\n            }\n        }\n\n        if (authData) {\n            const data: CallParameters = {};\n\n`)\n\n\t\tw := ts.newIdentWriter(3)\n\t\tif ts.authIsComplexType {\n\t\t\tauthData, err := encoding.DescribeAuth(ts.md, ts.md.AuthHandler.Params, &encoding.Options{SrcNameTag: \"json\"})\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"unable to describe auth data\")\n\t\t\t}\n\n\t\t\t// Generate the query string\n\t\t\tif len(authData.QueryParameters) > 0 {\n\t\t\t\tdict := make(map[string]string)\n\t\t\t\tfor _, field := range authData.QueryParameters {\n\t\t\t\t\tif list := field.Type.GetList(); list != nil {\n\t\t\t\t\t\tdot := ts.Dot(\"authData\", field.SrcName)\n\t\t\t\t\t\tif field.Optional || ts.isRecursive(field.Type) {\n\t\t\t\t\t\t\tdot += \"?\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdict[field.WireFormat] = dot +\n\t\t\t\t\t\t\t\".map((v) => \" + ts.convertBuiltinToString(list.Elem.GetBuiltin(), \"v\", false) + \")\"\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdict[field.WireFormat] = ts.convertBuiltinToString(\n\t\t\t\t\t\t\tfield.Type.GetBuiltin(),\n\t\t\t\t\t\t\tts.Dot(\"authData\", field.SrcName),\n\t\t\t\t\t\t\tfield.Optional,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tw.WriteString(\"data.query = makeRecord<string, string | string[]>(\")\n\t\t\t\tts.Values(w, dict)\n\t\t\t\tw.WriteString(\");\\n\")\n\t\t\t}\n\n\t\t\t// Generate the headers\n\t\t\tif len(authData.HeaderParameters) > 0 {\n\t\t\t\tdict := make(map[string]string)\n\t\t\t\tfor _, field := range authData.HeaderParameters {\n\t\t\t\t\tref := ts.Dot(\"authData\", field.SrcName)\n\t\t\t\t\tdict[field.WireFormat] = ts.convertBuiltinToString(field.Type.GetBuiltin(), ref, field.Optional)\n\t\t\t\t}\n\n\t\t\t\tw.WriteString(\"data.headers = makeRecord<string, string>(\")\n\t\t\t\tts.Values(w, dict)\n\t\t\t\tw.WriteString(\");\\n\")\n\t\t\t}\n\t\t} else {\n\t\t\tw.WriteString(\"data.headers = {};\\n\")\n\t\t\tw.WriteString(\"data.headers[\\\"Authorization\\\"] = \\\"Bearer \\\" + authData;\\n\")\n\t\t}\n\n\t\tw.WriteString(\"\\nreturn data;\\n\")\n\t\tw.Dedent().WriteString(\"}\\n\")\n\t}\n\n\tts.WriteString(`\n        return undefined;\n    }\n`)\n\n\tts.WriteString(`\n    // createStreamInOut sets up a stream to a streaming API endpoint.\n    async createStreamInOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamInOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamInOut(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamIn sets up a stream to a streaming API endpoint.\n    async createStreamIn<Response>(path: string, params?: CallParameters): Promise<StreamIn<Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamIn(this.baseURL + path + queryString, headers);\n    }\n\n    // createStreamOut sets up a stream to a streaming API endpoint.\n    async createStreamOut<Request, Response>(path: string, params?: CallParameters): Promise<StreamOut<Request, Response>> {\n        let { query, headers } = params ?? {};\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                headers = {...headers, ...authData.headers};\n            }\n        }\n\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        return new StreamOut(this.baseURL + path + queryString, headers);\n    }\n`)\n\n\tcallParams := \"method: string, path: string, body?: RequestInit[\\\"body\\\"], params?: CallParameters\"\n\tcallAPIParams := \"method, path, body\"\n\tinitParams := `\n            method,\n            body: body ?? null,`\n\tif ts.sharedTypes {\n\t\tcallParams = \"path: string, params?: CallParameters\"\n\t\tcallAPIParams = \"path\"\n\t\tinitParams = \"\"\n\t}\n\n\tfmt.Fprintf(ts, `\n    // callTypedAPI makes an API call, defaulting content type to \"application/json\"\n    public async callTypedAPI(%s): Promise<Response> {\n        return this.callAPI(%s, {\n            ...params,\n            headers: { \"Content-Type\": \"application/json\", ...params?.headers }\n        });\n    }\n`, callParams, callAPIParams)\n\n\tfmt.Fprintf(ts, `\n    // callAPI is used by each generated API method to actually make the request\n    public async callAPI(%s): Promise<Response> {\n        let { query, headers, ...rest } = params ?? {}\n        const init = {\n            ...this.requestInit,\n            ...rest,%s\n        }\n`, callParams, initParams)\n\n\tts.WriteString(`\n        // Merge our headers with any predefined headers\n        init.headers = {...this.headers, ...init.headers, ...headers}\n\n        // Fetch auth data if there is any\n        const authData = await this.getAuthData();\n\n        // If we now have authentication data, add it to the request\n        if (authData) {\n            if (authData.query) {\n                query = {...query, ...authData.query};\n            }\n            if (authData.headers) {\n                init.headers = {...init.headers, ...authData.headers};\n            }\n        }\n\n        // Make the actual request\n        const queryString = query ? '?' + encodeQuery(query) : ''\n        const response = await this.fetcher(this.baseURL+path+queryString, init)\n\n        // handle any error responses\n        if (!response.ok) {\n            // try and get the error message from the response body\n            let body: APIErrorResponse = { code: ErrCode.Unknown, message: ` + \"`request failed: status ${response.status}`\" + ` }\n\n            // if we can get the structured error we should, otherwise give a best effort\n            try {\n                const text = await response.text()\n\n                try {\n                    const jsonBody = JSON.parse(text)\n                    if (isAPIErrorResponse(jsonBody)) {\n                        body = jsonBody\n                    } else {\n                        body.message += \": \" + JSON.stringify(jsonBody)\n                    }\n                } catch {\n                    body.message += \": \" + text\n                }\n            } catch (e) {\n                // otherwise we just append the text to the error message\n                body.message += \": \" + String(e)\n            }\n\n            throw new APIError(response.status, body)\n        }\n\n        return response\n    }\n}`)\n\treturn nil\n}\n\nfunc (ts *typescript) writeExtraTypes() {\n\tif ts.seenJSON {\n\t\tts.WriteString(`// JSONValue represents an arbitrary JSON value.\nexport type JSONValue = string | number | boolean | null | JSONValue[] | {[key: string]: JSONValue}\n`)\n\t}\n\tif ts.sharedTypes {\n\t\tts.WriteString(`\ntype PickMethods<Type> = Omit<CallParameters, \"method\"> & { method?: Type };\n\n// Helper type to omit all fields that are cookies.\ntype OmitCookie<T> = {\n  [K in keyof T as T[K] extends CookieWithOptions<any> ? never : K]: T[K];\n};\n\ntype RequestType<Type extends (...args: any[]) => any> =\n  Parameters<Type> extends [infer H, ...any[]]\n    ? OmitCookie<H>\n    : void;\n\ntype ResponseType<Type extends (...args: any[]) => any> = OmitCookie<Awaited<ReturnType<Type>>>;\n\nfunction dateReviver(key: string, value: any): any {\n  if (\n    typeof value === \"string\" &&\n    value.length >= 10 &&\n    value.charCodeAt(0) >= 48 && // '0'\n    value.charCodeAt(0) <= 57 // '9'\n  ) {\n    const parsedDate = new Date(value);\n    if (!isNaN(parsedDate.getTime())) {\n      return parsedDate;\n    }\n  }\n  return value;\n}\n`)\n\t}\n\n\tts.WriteString(`\n\nfunction encodeQuery(parts: Record<string, string | string[]>): string {\n    const pairs: string[] = []\n    for (const key in parts) {\n        const val = (Array.isArray(parts[key]) ?  parts[key] : [parts[key]]) as string[]\n        for (const v of val) {\n            pairs.push(` + \"`\" + `${key}=${encodeURIComponent(v)}` + \"`\" + `)\n        }\n    }\n    return pairs.join(\"&\")\n}\n\n// makeRecord takes a record and strips any undefined values from it,\n// and returns the same record with a narrower type.\n// @ts-ignore - TS ignore because makeRecord is not always used\nfunction makeRecord<K extends string | number | symbol, V>(record: Record<K, V | undefined>): Record<K, V> {\n    for (const key in record) {\n        if (record[key] === undefined) {\n            delete record[key]\n        }\n    }\n    return record as Record<K, V>\n}\n`)\n\n\tif ts.seenHeaderResponse {\n\t\tts.WriteString(`\n\n// mustBeSet will throw an APIError with the Data Loss code if value is null or undefined\nfunction mustBeSet<A>(field: string, value: A | null | undefined): A {\n    if (value === null || value === undefined) {\n        throw new APIError(\n            500,\n            {\n                code: ErrCode.DataLoss,\n                message: ` + \"`${field} was unexpectedly ${value}`\" + `, // ${value} will create the string \"null\" or \"undefined\"\n            },\n        )\n    }\n    return value\n}\n`)\n\t}\n}\n\nfunc (ts *typescript) writeDecl(ns string, decl *schema.Decl) {\n\tif decl.Loc.PkgName != ns {\n\t\tts.WriteString(ts.typeName(decl.Loc.PkgName) + \".\")\n\t}\n\tts.WriteString(ts.typeName(decl.Name))\n}\n\nfunc (ts *typescript) writeDecl2(buf *bytes.Buffer, ns string, decl *schema.Decl) {\n\tif decl.Loc.PkgName != ns {\n\t\tbuf.WriteString(ts.typeName(decl.Loc.PkgName) + \".\")\n\t}\n\tbuf.WriteString(ts.typeName(decl.Name))\n}\n\nfunc (ts *typescript) builtinType(typ schema.Builtin) string {\n\tswitch typ {\n\tcase schema.Builtin_ANY:\n\t\treturn \"any\"\n\tcase schema.Builtin_BOOL:\n\t\treturn \"boolean\"\n\tcase schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64,\n\t\tschema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64,\n\t\tschema.Builtin_FLOAT32, schema.Builtin_FLOAT64:\n\t\treturn \"number\"\n\tcase schema.Builtin_STRING:\n\t\treturn \"string\"\n\tcase schema.Builtin_BYTES:\n\t\treturn \"string\" // TODO\n\tcase schema.Builtin_TIME:\n\t\treturn \"string\" // TODO\n\tcase schema.Builtin_JSON:\n\t\tts.seenJSON = true\n\t\treturn \"JSONValue\"\n\tcase schema.Builtin_UUID:\n\t\treturn \"string\"\n\tcase schema.Builtin_USER_ID:\n\t\treturn \"string\"\n\tcase schema.Builtin_DECIMAL:\n\t\treturn \"string\"\n\tdefault:\n\t\tts.errorf(\"unknown builtin type %v\", typ)\n\t\treturn \"any\"\n\t}\n}\n\nfunc (ts *typescript) convertBuiltinToString(typ schema.Builtin, val string, isOptional bool) string {\n\tvar code string\n\tswitch typ {\n\tcase schema.Builtin_STRING:\n\t\treturn val\n\tcase schema.Builtin_JSON:\n\t\tcode = fmt.Sprintf(\"JSON.stringify(%s)\", val)\n\tcase schema.Builtin_TIME:\n\t\tif ts.sharedTypes {\n\t\t\t// If we're using shared types then this will actually be a Date object.\n\t\t\t// Otherwise it will be a string.\n\t\t\tcode = fmt.Sprintf(\"%s.toISOString()\", val)\n\t\t} else {\n\t\t\tcode = fmt.Sprintf(\"String(%s)\", val)\n\t\t}\n\tdefault:\n\t\tcode = fmt.Sprintf(\"String(%s)\", val)\n\t}\n\n\tif isOptional {\n\t\tcode = fmt.Sprintf(\"%s === undefined ? undefined : %s\", val, code)\n\t}\n\treturn code\n}\n\nfunc (ts *typescript) convertStringToBuiltin(typ schema.Builtin, val string) string {\n\tswitch typ {\n\tcase schema.Builtin_ANY:\n\t\treturn val\n\tcase schema.Builtin_BOOL:\n\t\treturn fmt.Sprintf(\"%s.toLowerCase() === \\\"true\\\"\", val)\n\tcase schema.Builtin_INT, schema.Builtin_INT8, schema.Builtin_INT16, schema.Builtin_INT32, schema.Builtin_INT64,\n\t\tschema.Builtin_UINT, schema.Builtin_UINT8, schema.Builtin_UINT16, schema.Builtin_UINT32, schema.Builtin_UINT64:\n\t\treturn fmt.Sprintf(\"parseInt(%s, 10)\", val)\n\tcase schema.Builtin_FLOAT32, schema.Builtin_FLOAT64:\n\t\treturn fmt.Sprintf(\"Number(%s)\", val)\n\tcase schema.Builtin_STRING:\n\t\treturn val\n\tcase schema.Builtin_BYTES:\n\t\treturn val\n\tcase schema.Builtin_TIME:\n\t\treturn val\n\tcase schema.Builtin_JSON:\n\t\tts.seenJSON = true\n\t\treturn fmt.Sprintf(\"JSON.parse(%s)\", val)\n\tcase schema.Builtin_UUID:\n\t\treturn val\n\tcase schema.Builtin_USER_ID:\n\t\treturn val\n\tdefault:\n\t\tts.errorf(\"unknown builtin type %v\", typ)\n\t\treturn \"any\"\n\t}\n}\n\nfunc (ts *typescript) writeTyp(ns string, typ *schema.Type, numIndents int) {\n\tvar buf strings.Builder\n\tts.renderTyp(ts.Buffer, ns, typ, numIndents)\n\tts.WriteString(buf.String())\n}\n\nfunc (ts *typescript) renderTyp(buf *bytes.Buffer, ns string, tt *schema.Type, numIndents int) {\n\tswitch typ := tt.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\tdecl := ts.md.Decls[typ.Named.Id]\n\t\tts.writeDecl2(buf, ns, decl)\n\n\t\t// Write the type arguments\n\t\tif len(typ.Named.TypeArguments) > 0 {\n\t\t\tbuf.WriteRune('<')\n\n\t\t\tfor i, typeArg := range typ.Named.TypeArguments {\n\t\t\t\tif i > 0 {\n\t\t\t\t\tbuf.WriteString(\", \")\n\t\t\t\t}\n\n\t\t\t\tts.renderTyp(buf, ns, typeArg, 0)\n\t\t\t}\n\n\t\t\tbuf.WriteRune('>')\n\t\t}\n\tcase *schema.Type_List:\n\t\t// Determine if we need parens by counting the number of union elements.\n\t\tvar unionBuf bytes.Buffer\n\t\tnumCases := ts.renderUnionTypes(&unionBuf, ns, typ.List.Elem, numIndents)\n\t\tparen := numCases > 1\n\n\t\tif paren {\n\t\t\tbuf.WriteString(\"(\")\n\t\t}\n\n\t\tbuf.Write(unionBuf.Bytes())\n\n\t\tif paren {\n\t\t\tbuf.WriteString(\")\")\n\t\t}\n\n\t\tbuf.WriteString(\"[]\")\n\n\tcase *schema.Type_Map:\n\t\tbuf.WriteString(\"{ [key: \")\n\t\tts.renderTyp(buf, ns, typ.Map.Key, numIndents)\n\t\tbuf.WriteString(\"]: \")\n\t\tts.renderTyp(buf, ns, typ.Map.Value, numIndents)\n\t\tbuf.WriteString(\" }\")\n\n\tcase *schema.Type_Builtin:\n\t\tbuf.WriteString(ts.builtinType(typ.Builtin))\n\n\tcase *schema.Type_Literal:\n\t\tswitch lit := typ.Literal.Value.(type) {\n\t\tcase *schema.Literal_Str:\n\t\t\tbuf.WriteString(ts.Quote(lit.Str))\n\t\tcase *schema.Literal_Int:\n\t\t\tbuf.WriteString(strconv.FormatInt(lit.Int, 10))\n\t\tcase *schema.Literal_Float:\n\t\t\tbuf.WriteString(strconv.FormatFloat(lit.Float, 'f', -1, 64))\n\t\tcase *schema.Literal_Boolean:\n\t\t\tbuf.WriteString(strconv.FormatBool(lit.Boolean))\n\t\tcase *schema.Literal_Null:\n\t\t\tbuf.WriteString(\"null\")\n\t\tdefault:\n\t\t\tts.errorf(\"unknown literal type %T\", lit)\n\t\t}\n\n\tcase *schema.Type_Pointer:\n\t\tts.renderUnionTypes(buf, ns, tt, numIndents)\n\tcase *schema.Type_Option:\n\t\tts.renderUnionTypes(buf, ns, tt, numIndents)\n\tcase *schema.Type_Union:\n\t\tts.renderUnionTypes(buf, ns, tt, numIndents)\n\n\tcase *schema.Type_Struct:\n\t\tindent := func() {\n\t\t\tbuf.WriteString(strings.Repeat(\"    \", numIndents+1))\n\t\t}\n\n\t\t// Filter the fields to print based on struct tags.\n\t\tfields := make([]*schema.Field, 0, len(typ.Struct.Fields))\n\t\tfor _, f := range typ.Struct.Fields {\n\t\t\t// skip cookie fields as they are handled by the browser\n\t\t\tif f.Wire.GetCookie() != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif encoding.IgnoreField(f) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfields = append(fields, f)\n\t\t}\n\n\t\tbuf.WriteString(\"{\\n\")\n\t\tfor i, field := range fields {\n\t\t\tif field.Doc != \"\" {\n\t\t\t\tscanner := bufio.NewScanner(strings.NewReader(field.Doc))\n\t\t\t\tindent()\n\t\t\t\tbuf.WriteString(\"/**\\n\")\n\t\t\t\tfor scanner.Scan() {\n\t\t\t\t\tindent()\n\t\t\t\t\tbuf.WriteString(\" * \")\n\t\t\t\t\tbuf.WriteString(scanner.Text())\n\t\t\t\t\tbuf.WriteByte('\\n')\n\t\t\t\t}\n\t\t\t\tindent()\n\t\t\t\tbuf.WriteString(\" */\\n\")\n\t\t\t}\n\n\t\t\tindent()\n\t\t\tbuf.WriteString(ts.QuoteIfRequired(ts.fieldNameInStruct(field)))\n\n\t\t\tif field.Optional || ts.isRecursive(field.Typ) {\n\t\t\t\tbuf.WriteString(\"?\")\n\t\t\t}\n\t\t\tbuf.WriteString(\": \")\n\t\t\tts.renderTyp(buf, ns, field.Typ, numIndents+1)\n\t\t\tbuf.WriteString(\"\\n\")\n\n\t\t\t// Add another empty line if we have a doc comment\n\t\t\t// and this was not the last field.\n\t\t\tif field.Doc != \"\" && i < len(fields)-1 {\n\t\t\t\tbuf.WriteByte('\\n')\n\t\t\t}\n\t\t}\n\t\tbuf.WriteString(strings.Repeat(\"    \", numIndents))\n\t\tbuf.WriteByte('}')\n\n\tcase *schema.Type_TypeParameter:\n\t\tdecl := ts.md.Decls[typ.TypeParameter.DeclId]\n\t\ttypeParam := decl.TypeParams[typ.TypeParameter.ParamIdx]\n\n\t\tbuf.WriteString(typeParam.Name)\n\n\tcase *schema.Type_Config:\n\t\t// Config type is transparent\n\t\tts.renderTyp(buf, ns, typ.Config.Elem, numIndents)\n\n\tdefault:\n\t\tts.errorf(\"unknown type %+v\", reflect.TypeOf(typ))\n\t}\n}\n\nfunc (ts *typescript) renderUnionTypes(buf *bytes.Buffer, ns string, typ *schema.Type, numIndents int) (renderedCases int) {\n\tcases := ts.getUnionCases(typ)\n\tseenCases := make(map[string]bool)\n\tfor i, caseType := range cases {\n\t\tvar caseBuf bytes.Buffer\n\t\tts.renderTyp(&caseBuf, ns, caseType, numIndents)\n\t\tcaseStr := caseBuf.String()\n\t\tif seenCases[caseStr] {\n\t\t\tcontinue\n\t\t}\n\t\tseenCases[caseStr] = true\n\t\trenderedCases++\n\n\t\tif i > 0 {\n\t\t\tbuf.WriteString(\" | \")\n\t\t}\n\t\tbuf.WriteString(caseStr)\n\t}\n\treturn renderedCases\n}\n\nfunc (ts *typescript) getUnionCases(typ *schema.Type) []*schema.Type {\n\tnull := &schema.Type{\n\t\tTyp: &schema.Type_Literal{\n\t\t\tLiteral: &schema.Literal{\n\t\t\t\tValue: &schema.Literal_Null{Null: true},\n\t\t\t},\n\t\t},\n\t}\n\n\tswitch tt := typ.Typ.(type) {\n\tcase *schema.Type_Union:\n\t\treturn slices.Clone(tt.Union.Types)\n\tcase *schema.Type_Option:\n\t\treturn append(ts.getUnionCases(tt.Option.Value), null)\n\tcase *schema.Type_Pointer:\n\t\t// We do not treat pointers as nullable, as we have the Option type for that.\n\t\t// This makes a lot of APIs nicer to use.\n\t\treturn ts.getUnionCases(tt.Pointer.Base)\n\n\tdefault:\n\t\treturn []*schema.Type{typ}\n\t}\n}\n\ntype bailout struct{ err error }\n\nfunc (ts *typescript) errorf(format string, args ...interface{}) {\n\tpanic(bailout{fmt.Errorf(format, args...)})\n}\n\nfunc (ts *typescript) handleBailout(dst *error) {\n\tif err := recover(); err != nil {\n\t\tif bail, ok := err.(bailout); ok {\n\t\t\t*dst = bail.err\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc (ts *typescript) newIdentWriter(indent int) *indentWriter {\n\treturn &indentWriter{\n\t\tw:                ts.Buffer,\n\t\tdepth:            indent,\n\t\tindent:           \"    \",\n\t\tfirstWriteOnLine: true,\n\t}\n}\n\nfunc (ts *typescript) Quote(s string) string {\n\treturn fmt.Sprintf(\"\\\"%s\\\"\", strings.Replace(s, \"\\\"\", \"\\\\\\\"\", -1))\n}\n\nfunc (ts *typescript) QuoteIfRequired(s string) string {\n\t// If the identifier isn't purely alphanumeric, we need to add quotes.\n\tif !stringIsOnly(s, func(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) }) {\n\t\treturn ts.Quote(s)\n\t}\n\treturn s\n}\n\n// Dot allows us to reference a field in a struct by its name.\nfunc (ts *typescript) Dot(structIdent string, fieldIdent string) string {\n\tfieldIdent = ts.QuoteIfRequired(fieldIdent)\n\n\tif len(fieldIdent) > 0 && fieldIdent[0] == '\"' {\n\t\treturn fmt.Sprintf(\"%s[%s]\", structIdent, fieldIdent)\n\t} else {\n\t\treturn fmt.Sprintf(\"%s.%s\", structIdent, fieldIdent)\n\t}\n}\n\nfunc (ts *typescript) Values(w *indentWriter, dict map[string]string) {\n\t// Work out the largest key length.\n\tlargestKey := 0\n\tkeys := make([]string, 0, len(dict))\n\tfor key := range dict {\n\t\tkeys = append(keys, key)\n\t\tkey = ts.QuoteIfRequired(key)\n\t\tif len(key) > largestKey {\n\t\t\tlargestKey = len(key)\n\t\t}\n\t}\n\n\tsort.Strings(keys)\n\n\tw.WriteString(\"{\\n\")\n\t{\n\t\tw := w.Indent()\n\t\tfor _, key := range keys {\n\t\t\tident := ts.QuoteIfRequired(key)\n\t\t\tw.WriteStringf(\"%s: %s%s,\\n\", ident, strings.Repeat(\" \", largestKey-len(ident)), dict[key])\n\t\t}\n\t}\n\tw.WriteString(\"}\")\n}\n\n// Regex to replace invalid characters (anything that isn't a letter, number, `_`, or `$`)\nvar invalidChars = regexp.MustCompile(`[^\\p{L}\\p{N}$_]`)\n\n// validTSIdentifier converts a string into a valid TypeScript identifier.\nfunc validTSIdentifier(input string) string {\n\tif input == \"\" {\n\t\treturn \"_\"\n\t}\n\trunes := []rune(input)\n\t// Ensure the first character is a valid TS identifier start (letter, `_`, or `$`)\n\tif !unicode.IsLetter(runes[0]) && runes[0] != '_' && runes[0] != '$' {\n\t\trunes[0] = '_'\n\t}\n\n\toutput := invalidChars.ReplaceAllString(string(runes), \"_\")\n\treturn output\n}\n\nfunc (ts *typescript) typeName(identifier string) string {\n\tif ts.generatorVersion < TsExperimental {\n\t\treturn validTSIdentifier(identifier)\n\t} else {\n\t\treturn idents.Convert(identifier, idents.PascalCase)\n\t}\n}\n\nfunc (ts *typescript) memberName(identifier string) string {\n\tif ts.generatorVersion < TsExperimental {\n\t\treturn validTSIdentifier(identifier)\n\t} else {\n\t\treturn idents.Convert(identifier, idents.CamelCase)\n\t}\n}\n\nfunc (ts *typescript) fieldNameInStruct(field *schema.Field) string {\n\tname := field.Name\n\tif field.JsonName != \"\" {\n\t\tname = field.JsonName\n\t}\n\treturn name\n}\n\nfunc (ts *typescript) isRecursive(typ *schema.Type) bool {\n\tif ts.currDecl == nil {\n\t\treturn false\n\t}\n\n\t// Treat recursively seen types as if they are optional\n\trecursiveType := false\n\tif n := typ.GetNamed(); n != nil {\n\t\trecursiveType = ts.typs.IsRecursiveRef(ts.currDecl.Id, n.Id)\n\t}\n\treturn recursiveType\n}\n\nfunc (ts *typescript) writeCustomErrorType() {\n\tw := ts.newIdentWriter(0)\n\n\tw.WriteString(`\n\n/**\n * APIErrorDetails represents the response from an Encore API in the case of an error\n */\ninterface APIErrorResponse {\n    code: ErrCode\n    message: string\n    details?: any\n}\n\nfunction isAPIErrorResponse(err: any): err is APIErrorResponse {\n    return (\n        err !== undefined && err !== null &&\n        isErrCode(err.code) &&\n        typeof(err.message) === \"string\" &&\n        (err.details === undefined || err.details === null || typeof(err.details) === \"object\")\n    )\n}\n\nfunction isErrCode(code: any): code is ErrCode {\n    return code !== undefined && Object.values(ErrCode).includes(code)\n}\n\n/**\n * APIError represents a structured error as returned from an Encore application.\n */\nexport class APIError extends Error {\n    /**\n     * The HTTP status code associated with the error.\n     */\n    public readonly status: number\n\n    /**\n     * The Encore error code\n     */\n    public readonly code: ErrCode\n\n    /**\n     * The error details\n     */\n    public readonly details?: any\n\n    constructor(status: number, response: APIErrorResponse) {\n        // extending errors causes issues after you construct them, unless you apply the following fixes\n        super(response.message);\n\n        // set error name as constructor name, make it not enumerable to keep native Error behavior\n        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n        Object.defineProperty(this, 'name', {\n            value:        'APIError',\n            enumerable:   false,\n            configurable: true,\n        })\n\n        // fix the prototype chain\n        if ((Object as any).setPrototypeOf == undefined) {\n            (this as any).__proto__ = APIError.prototype\n        } else {\n            Object.setPrototypeOf(this, APIError.prototype);\n        }\n\n        // capture a stack trace\n        if ((Error as any).captureStackTrace !== undefined) {\n            (Error as any).captureStackTrace(this, this.constructor);\n        }\n\n        this.status = status\n        this.code = response.code\n        this.details = response.details\n    }\n}\n\n/**\n * Typeguard allowing use of an APIError's fields'\n */\nexport function isAPIError(err: any): err is APIError {\n    return err instanceof APIError;\n}\n\nexport enum ErrCode {\n    /**\n     * OK indicates the operation was successful.\n     */\n    OK = \"ok\",\n\n    /**\n     * Canceled indicates the operation was canceled (typically by the caller).\n     *\n     * Encore will generate this error code when cancellation is requested.\n     */\n    Canceled = \"canceled\",\n\n    /**\n     * Unknown error. An example of where this error may be returned is\n     * if a Status value received from another address space belongs to\n     * an error-space that is not known in this address space. Also\n     * errors raised by APIs that do not return enough error information\n     * may be converted to this error.\n     *\n     * Encore will generate this error code in the above two mentioned cases.\n     */\n    Unknown = \"unknown\",\n\n    /**\n     * InvalidArgument indicates client specified an invalid argument.\n     * Note that this differs from FailedPrecondition. It indicates arguments\n     * that are problematic regardless of the state of the system\n     * (e.g., a malformed file name).\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    InvalidArgument = \"invalid_argument\",\n\n    /**\n     * DeadlineExceeded means operation expired before completion.\n     * For operations that change the state of the system, this error may be\n     * returned even if the operation has completed successfully. For\n     * example, a successful response from a server could have been delayed\n     * long enough for the deadline to expire.\n     *\n     * The gRPC framework will generate this error code when the deadline is\n     * exceeded.\n     */\n    DeadlineExceeded = \"deadline_exceeded\",\n\n    /**\n     * NotFound means some requested entity (e.g., file or directory) was\n     * not found.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    NotFound = \"not_found\",\n\n    /**\n     * AlreadyExists means an attempt to create an entity failed because one\n     * already exists.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    AlreadyExists = \"already_exists\",\n\n    /**\n     * PermissionDenied indicates the caller does not have permission to\n     * execute the specified operation. It must not be used for rejections\n     * caused by exhausting some resource (use ResourceExhausted\n     * instead for those errors). It must not be\n     * used if the caller cannot be identified (use Unauthenticated\n     * instead for those errors).\n     *\n     * This error code will not be generated by the gRPC core framework,\n     * but expect authentication middleware to use it.\n     */\n    PermissionDenied = \"permission_denied\",\n\n    /**\n     * ResourceExhausted indicates some resource has been exhausted, perhaps\n     * a per-user quota, or perhaps the entire file system is out of space.\n     *\n     * This error code will be generated by the gRPC framework in\n     * out-of-memory and server overload situations, or when a message is\n     * larger than the configured maximum size.\n     */\n    ResourceExhausted = \"resource_exhausted\",\n\n    /**\n     * FailedPrecondition indicates operation was rejected because the\n     * system is not in a state required for the operation's execution.\n     * For example, directory to be deleted may be non-empty, an rmdir\n     * operation is applied to a non-directory, etc.\n     *\n     * A litmus test that may help a service implementor in deciding\n     * between FailedPrecondition, Aborted, and Unavailable:\n     *  (a) Use Unavailable if the client can retry just the failing call.\n     *  (b) Use Aborted if the client should retry at a higher-level\n     *      (e.g., restarting a read-modify-write sequence).\n     *  (c) Use FailedPrecondition if the client should not retry until\n     *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n     *      fails because the directory is non-empty, FailedPrecondition\n     *      should be returned since the client should not retry unless\n     *      they have first fixed up the directory by deleting files from it.\n     *  (d) Use FailedPrecondition if the client performs conditional\n     *      REST Get/Update/Delete on a resource and the resource on the\n     *      server does not match the condition. E.g., conflicting\n     *      read-modify-write on the same resource.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    FailedPrecondition = \"failed_precondition\",\n\n    /**\n     * Aborted indicates the operation was aborted, typically due to a\n     * concurrency issue like sequencer check failures, transaction aborts,\n     * etc.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     */\n    Aborted = \"aborted\",\n\n    /**\n     * OutOfRange means operation was attempted past the valid range.\n     * E.g., seeking or reading past end of file.\n     *\n     * Unlike InvalidArgument, this error indicates a problem that may\n     * be fixed if the system state changes. For example, a 32-bit file\n     * system will generate InvalidArgument if asked to read at an\n     * offset that is not in the range [0,2^32-1], but it will generate\n     * OutOfRange if asked to read from an offset past the current\n     * file size.\n     *\n     * There is a fair bit of overlap between FailedPrecondition and\n     * OutOfRange. We recommend using OutOfRange (the more specific\n     * error) when it applies so that callers who are iterating through\n     * a space can easily look for an OutOfRange error to detect when\n     * they are done.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    OutOfRange = \"out_of_range\",\n\n    /**\n     * Unimplemented indicates operation is not implemented or not\n     * supported/enabled in this service.\n     *\n     * This error code will be generated by the gRPC framework. Most\n     * commonly, you will see this error code when a method implementation\n     * is missing on the server. It can also be generated for unknown\n     * compression algorithms or a disagreement as to whether an RPC should\n     * be streaming.\n     */\n    Unimplemented = \"unimplemented\",\n\n    /**\n     * Internal errors. Means some invariants expected by underlying\n     * system has been broken. If you see one of these errors,\n     * something is very broken.\n     *\n     * This error code will be generated by the gRPC framework in several\n     * internal error conditions.\n     */\n    Internal = \"internal\",\n\n    /**\n     * Unavailable indicates the service is currently unavailable.\n     * This is a most likely a transient condition and may be corrected\n     * by retrying with a backoff. Note that it is not always safe to retry\n     * non-idempotent operations.\n     *\n     * See litmus test above for deciding between FailedPrecondition,\n     * Aborted, and Unavailable.\n     *\n     * This error code will be generated by the gRPC framework during\n     * abrupt shutdown of a server process or network connection.\n     */\n    Unavailable = \"unavailable\",\n\n    /**\n     * DataLoss indicates unrecoverable data loss or corruption.\n     *\n     * This error code will not be generated by the gRPC framework.\n     */\n    DataLoss = \"data_loss\",\n\n    /**\n     * Unauthenticated indicates the request does not have valid\n     * authentication credentials for the operation.\n     *\n     * The gRPC framework will generate this error code when the\n     * authentication metadata is invalid or a Credentials callback fails,\n     * but also expect authentication middleware to generate it.\n     */\n    Unauthenticated = \"unauthenticated\",\n}\n`)\n}\n\nfunc stringIsOnly(str string, predicate func(r rune) bool) bool {\n\tfor _, r := range str {\n\t\tif !predicate(r) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "pkg/clientgen/utils.go",
    "content": "package clientgen\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"encr.dev/internal/version\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nfunc doNotEditHeader() string {\n\treturn fmt.Sprintf(\"Code generated by the Encore %s client generator. DO NOT EDIT.\", version.Version)\n}\n\nfunc hasPublicRPC(svc *meta.Service) bool {\n\tfor _, rpc := range svc.Rpcs {\n\t\tif rpc.AccessType != meta.RPC_PRIVATE {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\ntype indentWriter struct {\n\tw                *bytes.Buffer\n\tdepth            int\n\tindent           string\n\tfirstWriteOnLine bool\n}\n\nfunc (w *indentWriter) Indent() *indentWriter {\n\treturn &indentWriter{\n\t\tw:                w.w,\n\t\tdepth:            w.depth + 1,\n\t\tindent:           w.indent,\n\t\tfirstWriteOnLine: true,\n\t}\n}\n\nfunc (w *indentWriter) Dedent() *indentWriter {\n\treturn &indentWriter{\n\t\tw:                w.w,\n\t\tdepth:            max(w.depth-1, 0),\n\t\tindent:           w.indent,\n\t\tfirstWriteOnLine: true,\n\t}\n}\n\nfunc (w *indentWriter) WriteString(s string) {\n\tparts := strings.Split(s, \"\\n\")\n\tfor i, part := range parts {\n\t\tif i > 0 {\n\t\t\tw.w.WriteString(\"\\n\")\n\t\t\tw.firstWriteOnLine = true\n\t\t}\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif w.firstWriteOnLine {\n\t\t\tw.w.WriteString(strings.Repeat(w.indent, w.depth))\n\t\t\tw.firstWriteOnLine = false\n\t\t}\n\n\t\tw.w.WriteString(part)\n\t}\n}\n\nfunc (w *indentWriter) WriteStringf(s string, args ...interface{}) {\n\tw.WriteString(fmt.Sprintf(s, args...))\n}\n"
  },
  {
    "path": "pkg/cueutil/build.go",
    "content": "package cueutil\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"cuelang.org/go/cue\"\n\t\"cuelang.org/go/cue/ast\"\n\t\"cuelang.org/go/cue/build\"\n\t\"cuelang.org/go/cue/cuecontext\"\n\t\"cuelang.org/go/cue/load\"\n\t\"cuelang.org/go/cue/parser\"\n\t\"cuelang.org/go/cue/token\"\n\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/transform\"\n\n\t\"encr.dev/pkg/eerror\"\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\t\"encr.dev/pkg/fns\"\n)\n\n// LoadFromFS takes a given filesystem object and the app-relative path to the service's root package\n// and loads the full configuration needed for that service.\nfunc LoadFromFS(filesys fs.FS, serviceRelPath string, meta *Meta) (cue.Value, error) {\n\t// Work out of a temporary directory\n\ttmpPath, err := os.MkdirTemp(\"\", \"encr-cfg-\")\n\tif err != nil {\n\t\treturn cue.Value{}, eerror.Wrap(err, \"config\", \"unable to create temporary directory\", nil)\n\t}\n\tdefer func() { _ = os.RemoveAll(tmpPath) }()\n\n\t// Write the FS to the file system\n\terr = writeFSToPath(filesys, tmpPath)\n\tif err != nil {\n\t\treturn cue.Value{}, err\n\t}\n\n\t// Find all config files for the service\n\tconfigFilesForService, err := allFilesUnder(filesys, serviceRelPath)\n\tif err != nil {\n\t\treturn cue.Value{}, eerror.Wrap(err, \"config\", \"unable to list all config files for service\", map[string]any{\"path\": serviceRelPath})\n\t}\n\n\t// If there are no config files, return an empty value\n\tif len(configFilesForService) == 0 {\n\t\treturn cue.Value{}, nil\n\t}\n\n\t// Tell CUE to load all the files\n\tloaderCfg := &load.Config{\n\t\tDir:   tmpPath,\n\t\tTools: true,\n\t\tTags:  meta.ToTags(),\n\t}\n\tpkgs := load.Instances(configFilesForService, loaderCfg)\n\tfor _, pkg := range pkgs {\n\t\tif pkg.Err != nil {\n\t\t\treturn cue.Value{}, srcerrors.UnableToLoadCUEInstances(pkg.Err, tmpPath)\n\t\t}\n\n\t\t// Non CUE files may be orphaned (JSON/YAML), so need to be parsed into the CUE AST and added to the package.\n\t\tif err := addOrphanedFiles(pkg); err != nil {\n\t\t\treturn cue.Value{}, srcerrors.UnableToAddOrphanedCUEFiles(err, tmpPath)\n\t\t}\n\t}\n\n\t// Build the CUE values\n\tctx := cuecontext.New()\n\tvalues, err := ctx.BuildInstances(pkgs)\n\tif err != nil {\n\t\treturn cue.Value{}, srcerrors.UnableToLoadCUEInstances(err, tmpPath)\n\t}\n\tif len(values) == 0 {\n\t\treturn cue.Value{}, eerror.New(\"config\", \"no values generated from config\", nil)\n\t}\n\n\t// Unify all returned values into a single value\n\t// Note; to get all errors in the CUE files, we want to wait until\n\t// the validate output to check for errors\n\trtnValue := values[0]\n\tfor _, value := range values {\n\t\trtnValue = rtnValue.Unify(value)\n\t}\n\n\t// Validate the unified value is concrete\n\tif err := rtnValue.Validate(cue.Concrete(true)); err != nil {\n\t\treturn cue.Value{}, srcerrors.CUEEvaluationFailed(err, tmpPath)\n\t}\n\n\treturn rtnValue, nil\n}\n\n// allFilesUnder returns all files under the given path in the given filesystem.\nfunc allFilesUnder(filesys fs.FS, path string) ([]string, error) {\n\tvar files []string\n\n\t// Check if the path exists\n\t// and if it doesn't that means there zero CUE files for this service\n\t// this can happen when a Config struct has zero fields\n\tif _, err := fs.Stat(filesys, path); err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\terr := fs.WalkDir(filesys, path, func(path string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() {\n\t\t\tfiles = append(files, path)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn files, nil\n}\n\n// writeFSToPath writes the contents of the given filesystem to a temporary directory on the local filesystem.\nfunc writeFSToPath(fsys fs.FS, targetPath string) error {\n\t// Copy the files into the temporary directory\n\terr := fs.WalkDir(fsys, \".\", func(path string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn eerror.Wrap(err, \"config\", \"unable to walk VFS\", nil)\n\t\t}\n\n\t\t// Convert the io/fs slash-based path to a filepath.\n\t\tosPath := filepath.FromSlash(path)\n\n\t\tif !info.IsDir() {\n\t\t\t// Open the source file from our filesystem\n\t\t\tsrcFile, err := fsys.Open(path)\n\t\t\tif err != nil {\n\t\t\t\treturn eerror.Wrap(err, \"config\", \"unable to open src file\", nil)\n\t\t\t}\n\t\t\tdefer fns.CloseIgnore(srcFile)\n\n\t\t\tdstFile, err := os.OpenFile(filepath.Join(targetPath, osPath), os.O_CREATE|os.O_WRONLY, 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn eerror.Wrap(err, \"config\", \"unable to open dst file\", nil)\n\t\t\t}\n\n\t\t\t_, err = io.Copy(dstFile, srcFile)\n\t\t\tif err2 := dstFile.Close(); err == nil {\n\t\t\t\terr = err2\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn eerror.Wrap(err, \"config\", \"unable to copy file\", nil)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := os.Mkdir(filepath.Join(targetPath, osPath), 0755); err != nil && !errors.Is(err, os.ErrExist) {\n\t\t\t\treturn eerror.Wrap(err, \"config\", \"unable to make dir\", nil)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn eerror.Wrap(err, \"config\", \"unable to write config files to temporary directory\", map[string]any{\"path\": targetPath})\n\t}\n\treturn nil\n}\n\n// addOrphanedFiles adds any orphaned files outside the package to the build instance. This could be CUE, YAML or JSON files\n//\n// The majority of the code in the function is taken directly from the CUE source code as the code is currently only accessible\n// from internal paths - they are planning to move this out of the internal path so non-CUE code can directly call it\n// as library functions. (Src: cue/internal/encoding/encoding.go : NewDecoder())\nfunc addOrphanedFiles(i *build.Instance) (err error) {\n\tfor _, f := range i.OrphanedFiles {\n\t\tvar file ast.Node\n\n\t\trc, err := reader(f)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t//goland:noinspection GoDeferInLoop\n\t\tdefer func() { _ = rc.Close() }()\n\n\t\tt := unicode.BOMOverride(unicode.UTF8.NewDecoder())\n\t\tr := transform.NewReader(rc, t)\n\n\t\tswitch f.Encoding {\n\t\tcase build.CUE:\n\t\t\tfile, err = parser.ParseFile(f.Filename, r, parser.ParseComments)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\treturn errors.New(fmt.Sprintf(\"unsupported encoding: %s\", f.Encoding))\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := i.AddSyntax(toFile(file)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// toFile converts an ast.Node to a *ast.File. (from the CUE source code)\nfunc toFile(n ast.Node) *ast.File {\n\tswitch x := n.(type) {\n\tcase nil:\n\t\treturn nil\n\tcase *ast.StructLit:\n\t\treturn &ast.File{Decls: x.Elts}\n\tcase ast.Expr:\n\t\tast.SetRelPos(x, token.NoSpace)\n\t\treturn &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}}\n\tcase *ast.File:\n\t\treturn x\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"Unsupported node type %T\", x))\n\t}\n}\n\n// reader returns a reader for the given file. (from the CUE source code)\nfunc reader(f *build.File) (io.ReadCloser, error) {\n\tswitch s := f.Source.(type) {\n\tcase nil:\n\t\t// Use the file name.\n\tcase string:\n\t\treturn io.NopCloser(strings.NewReader(s)), nil\n\tcase []byte:\n\t\treturn io.NopCloser(bytes.NewReader(s)), nil\n\tcase *bytes.Buffer:\n\t\t// is io.Reader, but it needs to be readable repeatedly\n\t\tif s != nil {\n\t\t\treturn io.NopCloser(bytes.NewReader(s.Bytes())), nil\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid source type %T\", f.Source)\n\t}\n\treturn os.Open(f.Filename)\n}\n"
  },
  {
    "path": "pkg/cueutil/types.go",
    "content": "package cueutil\n\ntype EnvType string\n\nconst (\n\tEnvType_Production  EnvType = \"production\"\n\tEnvType_Development EnvType = \"development\"\n\tEnvType_Ephemeral   EnvType = \"ephemeral\"\n\tEnvType_Test        EnvType = \"test\"\n)\n\ntype CloudType string\n\nconst (\n\tCloudType_AWS    CloudType = \"aws\"\n\tCloudType_Azure  CloudType = \"azure\"\n\tCloudType_GCP    CloudType = \"gcp\"\n\tCloudType_Encore CloudType = \"encore\"\n\tCloudType_Local  CloudType = \"local\"\n)\n\ntype Meta struct {\n\tAPIBaseURL string\n\tEnvName    string\n\tEnvType    EnvType\n\tCloudType  CloudType\n}\n\nfunc (m *Meta) ToTags() []string {\n\tif m == nil {\n\t\treturn nil\n\t}\n\n\treturn []string{\n\t\ttag(\"APIBaseURL\", m.APIBaseURL),\n\t\ttag(\"EnvName\", m.EnvName),\n\t\ttag(\"EnvType\", m.EnvType),\n\t\ttag(\"CloudType\", m.CloudType),\n\t}\n}\n\nfunc tag[T ~string](name string, value T) string {\n\treturn name + \"=\" + string(value)\n}\n"
  },
  {
    "path": "pkg/dockerbuild/dockerbuild.go",
    "content": "package dockerbuild\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/google/go-containerregistry/pkg/name\"\n\tv1 \"github.com/google/go-containerregistry/pkg/v1\"\n\t\"github.com/google/go-containerregistry/pkg/v1/empty\"\n\t\"github.com/google/go-containerregistry/pkg/v1/mutate\"\n\t\"github.com/google/go-containerregistry/pkg/v1/remote\"\n\t\"github.com/google/go-containerregistry/pkg/v1/tarball\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/option\"\n)\n\n// DefaultCACertsPath is the default path for where to write CA Certs.\n// From https://go.dev/src/crypto/x509/root_linux.go.\nconst DefaultCACertsPath ImagePath = \"/etc/ssl/certs/ca-certificates.crt\"\n\ntype ImageBuildConfig struct {\n\t// The time to use when recording times in the image.\n\tBuildTime time.Time\n\n\t// AddCACerts, if set, specifies where in the image to mount the CA certificates.\n\t// If set to Some(\"\"), defaults to DefaultCACertsPath.\n\tAddCACerts option.Option[ImagePath]\n\n\t// SupervisorPath is the path to the supervisor binary to use.\n\t// It must be set if the image includes the supervisor.\n\tSupervisorPath option.Option[HostPath]\n\n\t// BaseImageOverride overrides the base image to use.\n\t// If None it resolves the image from the spec using ResolveRemoteImage.\n\tBaseImageOverride option.Option[v1.Image]\n\n\t// A URL to a http proxy used to fetch images\n\tDockerOptions []remote.Option\n}\n\n// BuildImage builds a docker image from the given spec.\nfunc BuildImage(ctx context.Context, spec *ImageSpec, cfg ImageBuildConfig) (v1.Image, error) {\n\toptions := append(cfg.DockerOptions,\n\t\tremote.WithPlatform(v1.Platform{\n\t\t\tOS:           spec.OS,\n\t\t\tArchitecture: spec.Arch,\n\t\t}),\n\t)\n\tbaseImg, err := resolveBaseImage(ctx, spec.DockerBaseImage, cfg.BaseImageOverride, options...)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"resolve base image\")\n\t}\n\n\topener, err := buildImageFilesystem(ctx, spec, &cfg)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"build image fs\")\n\t}\n\n\tlayer, err := tarball.LayerFromOpener(opener,\n\t\ttarball.WithCompressionLevel(5), // balance speed and compression\n\t)\n\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"create tarball layer\")\n\t}\n\n\tlog.Info().Msg(\"adding layer to base image\")\n\timg, err := mutate.Append(baseImg, mutate.Addendum{\n\t\tLayer: layer,\n\t\tHistory: v1.History{\n\t\t\tAuthor:    \"encore-app\",\n\t\t\tCreated:   v1.Time{Time: cfg.BuildTime},\n\t\t\tCreatedBy: \"encore.dev\",\n\t\t\tComment:   \"Built with encore.dev, the backend development engine\",\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"add layer\")\n\t}\n\n\t// Copy the base image's environment variables.\n\tbaseCfg, err := baseImg.ConfigFile()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"get base image config\")\n\t}\n\tenvs := newEnvMap(baseCfg.Config.Env)\n\n\timgCfg, err := img.ConfigFile()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"get image config\")\n\t}\n\timgCfg = imgCfg.DeepCopy()\n\n\t// Add the image spec's environment.\n\tenvs.Update(spec.Env)\n\n\timgCfg.Config.Entrypoint = spec.Entrypoint\n\timgCfg.Config.Cmd = nil\n\timgCfg.Config.Env = envs.ToSlice()\n\timgCfg.Config.WorkingDir = string(spec.WorkingDir)\n\timgCfg.Author = \"encore.dev\"\n\timgCfg.Created = v1.Time{Time: cfg.BuildTime}\n\timgCfg.Architecture = spec.Arch\n\timgCfg.OS = spec.OS\n\n\timg, err = mutate.ConfigFile(img, imgCfg)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"add config\")\n\t}\n\treturn img, nil\n}\n\n// ResolveRemoteImage resolves the base image with the given reference.\n// If imageRef is the empty string or \"scratch\" it resolves to the empty image.\nfunc ResolveRemoteImage(ctx context.Context, imageRef string, options ...remote.Option) (v1.Image, error) {\n\tif imageRef == \"\" || imageRef == \"scratch\" {\n\t\treturn empty.Image, nil\n\t}\n\n\t// Try to get it from the daemon if it exists.\n\tbaseImgRef, err := name.ParseReference(imageRef)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"parse image ref\")\n\t}\n\n\timg, err := remote.Image(baseImgRef, append(options, remote.WithContext(ctx))...)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"fetch image\")\n\t}\n\treturn img, nil\n}\n\nfunc resolveBaseImage(ctx context.Context, baseImgTag string, overrideBaseImage option.Option[v1.Image], options ...remote.Option) (v1.Image, error) {\n\tif override, ok := overrideBaseImage.Get(); ok {\n\t\treturn override, nil\n\t}\n\treturn ResolveRemoteImage(ctx, baseImgTag, options...)\n}\n\nfunc buildImageFilesystem(ctx context.Context, spec *ImageSpec, cfg *ImageBuildConfig) (opener tarball.Opener, err error) {\n\ttc := newTarCopier(setFileTimes(cfg.BuildTime))\n\n\t// Bundle the source code, if requested.\n\tif bundle, ok := spec.BundleSource.Get(); ok {\n\t\tif err := bundleSource(tc, spec, &bundle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Copy data into the image.\n\tif err := tc.CopyData(spec); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Copy Encore binaries into the image.\n\tif err := setupSupervisor(tc, spec, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Add build information.\n\tif err := writeBuildInfo(tc, spec.BuildInfo); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Write additional files\n\tif err := writeExtraFiles(tc, spec.WriteFiles); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Add CA certificates, if requested.\n\tif caCertsDest, ok := cfg.AddCACerts.Get(); ok {\n\t\tif caCertsDest == \"\" {\n\t\t\tcaCertsDest = DefaultCACertsPath\n\t\t}\n\t\tif err := addCACerts(ctx, tc, caCertsDest); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"add ca certs\")\n\t\t}\n\t}\n\n\treturn tc.Opener(), nil\n}\n\nfunc writeExtraFiles(tc *tarCopier, files map[ImagePath][]byte) error {\n\tfor path, data := range files {\n\t\tif err := tc.WriteFile(path, 0644, data); err != nil {\n\t\t\treturn errors.Wrap(err, \"write image data\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc setupSupervisor(tc *tarCopier, spec *ImageSpec, cfg *ImageBuildConfig) error {\n\tsuper, ok := spec.Supervisor.Get()\n\tif !ok {\n\t\treturn nil\n\t}\n\n\t// Add the supervisor binary.\n\t{\n\t\thostPath, ok := cfg.SupervisorPath.Get()\n\t\tif !ok {\n\t\t\treturn errors.New(\"supervisor requested, but not provided\")\n\t\t}\n\t\tfi, err := os.Stat(string(hostPath))\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"stat supervisor\")\n\t\t}\n\n\t\tif err := tc.MkdirAll(super.MountPath.Dir(), 0755); err != nil {\n\t\t\treturn errors.Wrap(err, \"create supervisor dir\")\n\t\t}\n\t\tif err := tc.CopyFile(super.MountPath, hostPath, fi, \"\"); err != nil {\n\t\t\treturn errors.Wrap(err, \"copy supervisor\")\n\t\t}\n\t}\n\n\t// Write the supervisor configuration.\n\t{\n\t\tdata, err := json.MarshalIndent(super.Config, \"\", \"  \")\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"marshal supervisor config\")\n\t\t}\n\t\tif err := tc.WriteFile(super.ConfigPath, 0644, data); err != nil {\n\t\t\treturn errors.Wrap(err, \"write supervisor config\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc bundleSource(tc *tarCopier, spec *ImageSpec, bundle *BundleSourceSpec) error {\n\tincludes := []HostPath{bundle.Source.Join(filepath.FromSlash(string(bundle.AppRootRelpath)))}\n\tfor _, ex := range bundle.IncludeSource {\n\t\tincludes = append(includes, bundle.Source.Join(string(ex)))\n\t}\n\n\texcludes := make(map[HostPath]bool, len(bundle.ExcludeSource))\n\tfor _, ex := range bundle.ExcludeSource {\n\t\tabsPath := bundle.Source.Join(string(ex))\n\t\texcludes[absPath] = true\n\t}\n\n\terr := tc.CopyDir(&dirCopyDesc{\n\t\tSpec:            spec,\n\t\tSrcPath:         bundle.Source,\n\t\tDstPath:         bundle.Dest,\n\t\tExcludeSrcPaths: excludes,\n\t\tIncludeSrcPaths: includes,\n\t})\n\treturn errors.Wrap(err, \"bundle source\")\n}\n\nfunc writeBuildInfo(tc *tarCopier, spec BuildInfoSpec) error {\n\tinfo, err := json.MarshalIndent(spec.Info, \"\", \"  \")\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"marshal build info\")\n\t}\n\n\terr = tc.WriteFile(spec.InfoPath, 0644, info)\n\treturn errors.Wrap(err, \"write build info\")\n}\n\nfunc tryFetch(ctx context.Context, url string) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"create request\")\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"get root certs\")\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\t_ = resp.Body.Close()\n\t\treturn nil, errors.Newf(\"root cert url returned status code: %s\", resp.Status)\n\t}\n\treturn resp, nil\n}\n\nfunc addCACerts(ctx context.Context, tc *tarCopier, dest ImagePath) error {\n\tconst (\n\t\tencoreCachedRootCerts = \"https://api.encore.cloud/artifacts/build/root-certs\"\n\t\tcurlCACertStore       = \"https://curl.se/ca/cacert.pem\"\n\t)\n\tvar (\n\t\tresp *http.Response\n\t\terr  error\n\t)\n\tfor _, url := range []string{encoreCachedRootCerts, curlCACertStore} {\n\t\tresp, err = tryFetch(ctx, url)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tlog.Warn().Err(err).Msgf(\"failed to fetch root certs from: %s\", url)\n\t}\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to fetch cert file\")\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"read cert data\")\n\t}\n\n\t// Add the file\n\terr = tc.WriteFile(dest, 0644, data)\n\treturn err\n}\n\ntype envMap map[string]string\n\nfunc (m envMap) Update(envs []string) {\n\tfor _, e := range envs {\n\t\tkey, value, _ := strings.Cut(e, \"=\")\n\t\tm[key] = value\n\t}\n}\n\nfunc (m envMap) ToSlice() []string {\n\tenvs := make([]string, 0, len(m))\n\tfor k, v := range m {\n\t\tenvs = append(envs, k+\"=\"+v)\n\t}\n\tslices.Sort(envs)\n\treturn envs\n}\n\nfunc newEnvMap(envs []string) envMap {\n\tm := make(envMap, len(envs))\n\tfor _, e := range envs {\n\t\tkey, value, _ := strings.Cut(e, \"=\")\n\t\tm[key] = value\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "pkg/dockerbuild/dockerbuild_test.go",
    "content": "package dockerbuild\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\tqt \"github.com/frankban/quicktest\"\n\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nfunc TestBuildImage(t *testing.T) {\n\tc := qt.New(t)\n\n\tartifacts := paths.FS(c.TempDir())\n\twriteFiles(c, artifacts, map[string]string{\n\t\t\"entrypoint\":       \"echo hello\",\n\t\t\"package.json\":     `{\"name\": \"package/name\"}`,\n\t\t\"node_modules/foo\": \"foo\",\n\t})\n\truntimes := paths.FS(c.TempDir())\n\twriteFiles(c, runtimes, map[string]string{\n\t\t\"js/encore-runtime.node\":     \"node runtime\",\n\t\t\"js/encore.dev/package.json\": `{\"name\": \"encore.dev\"}`,\n\t})\n\n\tcfg := DescribeConfig{\n\t\tMeta:     &meta.Data{},\n\t\tRuntimes: HostPath(runtimes),\n\t\tBundleSource: option.Some(BundleSourceSpec{\n\t\t\tSource:         HostPath(artifacts),\n\t\t\tDest:           \"/workspace\",\n\t\t\tAppRootRelpath: \".\",\n\t\t}),\n\t\tCompile: &builder.CompileResult{Outputs: []builder.BuildOutput{\n\t\t\t&builder.JSBuildOutput{\n\t\t\t\tArtifactDir: artifacts,\n\t\t\t\tEntrypoints: []builder.Entrypoint{{\n\t\t\t\t\tCmd: builder.CmdSpec{\n\t\t\t\t\t\tCommand:          builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t\tPrioritizedFiles: builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t},\n\t\t\t\t\tServices:           []string{\"foo\", \"bar\"},\n\t\t\t\t\tGateways:           []string{\"baz\", \"qux\"},\n\t\t\t\t\tUseRuntimeConfigV2: true,\n\t\t\t\t}},\n\t\t\t},\n\t\t}},\n\t}\n\n\tspec, err := describe(cfg)\n\tc.Assert(err, qt.IsNil)\n\n\tencoreBinaries := paths.FS(c.TempDir())\n\twriteFiles(c, encoreBinaries, map[string]string{\n\t\t\"supervisor.bin\": \"supervisor\",\n\t})\n\n\tctx := context.Background()\n\tbuildTime := time.Unix(1234567890, 0)\n\timg, err := BuildImage(ctx, spec, ImageBuildConfig{\n\t\tBuildTime:      buildTime,\n\t\tSupervisorPath: option.Some(HostPath(encoreBinaries.Join(\"supervisor.bin\"))),\n\t})\n\tc.Assert(err, qt.IsNil)\n\n\t_, err = img.Digest()\n\tc.Assert(err, qt.IsNil)\n\t// Note: this digest changes depending on the machine it's being built on\n\t// c.Assert(digest.String(), qt.Equals, \"sha256:6e0032a1560c506901bbc1bb291d7655639d242f5ca09d5e119876830e34813d\")\n}\n\nfunc writeFiles(c *qt.C, dir paths.FS, files map[string]string) {\n\tfor name, content := range files {\n\t\tc.Assert(filepath.IsLocal(name), qt.IsTrue)\n\t\tpath := string(dir.Join(name))\n\n\t\terr := os.MkdirAll(filepath.Dir(path), 0755)\n\t\tc.Assert(err, qt.IsNil)\n\n\t\terr = os.WriteFile(path, []byte(content), 0755)\n\t\tc.Assert(err, qt.IsNil)\n\t}\n}\n"
  },
  {
    "path": "pkg/dockerbuild/features.go",
    "content": "package dockerbuild\n\ntype FeatureFlag string\n\nconst (\n\tNewRuntimeConfig FeatureFlag = \"new_runtime_config\"\n)\n"
  },
  {
    "path": "pkg/dockerbuild/manifest.go",
    "content": "package dockerbuild\n\ntype ImageManifestFile struct {\n\tManifests []*ImageManifest\n}\n\ntype ImageManifest struct {\n\t// The Docker Image reference, e.g. \"gcr.io/my-project/my-image:sha256:abcdef...\"\n\tReference string\n\n\t// Services and gateways bundled in this image.\n\tBundledServices []string\n\tBundledGateways []string\n\n\t// FeatureFlags captures feature flags enabled for this image.\n\tFeatureFlags map[FeatureFlag]bool\n}\n"
  },
  {
    "path": "pkg/dockerbuild/spec.go",
    "content": "package dockerbuild\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\tpathpkg \"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\tstrconv \"strconv\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang/protobuf/proto\"\n\t\"github.com/rs/xid\"\n\n\t\"encr.dev/pkg/appfile\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/noopgateway\"\n\t\"encr.dev/pkg/noopgwdesc\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/pkg/supervisor\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype ImageSpecFile struct {\n\tImages []*ImageSpec\n}\n\n// ImageSpec is a specification for how to build a docker image.\ntype ImageSpec struct {\n\t// The operating system to use for the image.\n\tOS string\n\n\t// The architecture to use for the image.\n\tArch string\n\n\t// The entrypoint to use for the image. It must be non-empty.\n\t// The first entry is the executable path, and the rest are the arguments.\n\tEntrypoint []string\n\n\t// Environment variables to set for the entrypoint.\n\tEnv []string\n\n\t// The working dir to use for executing the entrypoint.\n\tWorkingDir ImagePath\n\n\t// BuildInfo contains information about the build.\n\tBuildInfo BuildInfoSpec\n\n\t// A map from the builder filesystem paths to the destination path in the image.\n\t// If the source is a directory, it will be copied recursively.\n\tCopyData map[ImagePath]HostPath\n\n\t// A set of files which should be written to the image.\n\tWriteFiles map[ImagePath][]byte\n\n\t// Whether to bundle source into the image.\n\t// It's handled separately from CopyData since we apply some filtering\n\t// on what's copied, like excluding .git directories and other build artifacts.\n\tBundleSource option.Option[BundleSourceSpec]\n\n\t// Supervisor specifies the supervisor configuration.\n\tSupervisor option.Option[SupervisorSpec]\n\n\t// The names of services bundled in this image.\n\tBundledServices []string\n\n\t// The names of gateways bundled in this image.\n\tBundledGateways []string\n\n\t// The docker base image to use. If None it defaults to the empty scratch image.\n\tDockerBaseImage string\n\n\t// StargzPrioritizedFiles are file paths in the image that should be prioritized for\n\t// stargz compression, allowing for faster streaming of those files.\n\tStargzPrioritizedFiles []ImagePath\n\n\t// FeatureFlags specifies feature flags enabled for this image.\n\tFeatureFlags map[FeatureFlag]bool\n}\n\ntype BuildInfoSpec struct {\n\t// The build info to include in the image.\n\tInfo BuildInfo\n\n\t// The path in the image where the build info is written, as a JSON file.\n\tInfoPath ImagePath\n}\n\ntype BuildInfo struct {\n\t// The version of Encore with which the app was compiled.\n\t// This is string is for informational use only, and its format should not be relied on.\n\tEncoreCompiler string\n\n\t// AppCommit describes the commit of the app.\n\tAppCommit CommitInfo\n}\n\ntype CommitInfo struct {\n\tRevision    string\n\tUncommitted bool\n}\n\ntype BundleSourceSpec struct {\n\tSource HostPath\n\tDest   ImagePath\n\n\t// Source paths to exclude from copying, relative to Source.\n\tExcludeSource []RelPath\n\n\t// Path to app root, will be included, relative to Source.\n\tAppRootRelpath RelPath\n\n\t// Extra source paths to include when bundling, relative to Source.\n\tIncludeSource []RelPath\n}\n\ntype SupervisorSpec struct {\n\t// Where to mount the supervisor binary in the image.\n\tMountPath ImagePath\n\n\t// Where to write the supervisor configuration in the image.\n\tConfigPath ImagePath\n\n\t// The config to pass to the supervisor.\n\tConfig *supervisor.Config\n}\n\ntype DescribeConfig struct {\n\t// The parsed metadata.\n\tMeta *meta.Data\n\n\t// The compile result.\n\tCompile *builder.CompileResult\n\n\t// The directory containing the runtimes.\n\tRuntimes HostPath\n\n\t// The path to the node runtime, if any.\n\tNodeRuntime option.Option[HostPath]\n\n\t// The docker base image to use, if any. If None it defaults to the empty scratch image.\n\tDockerBaseImage option.Option[string]\n\n\t// BundleSource specifies whether to bundle source into the image,\n\t// and where the source is located on the host filesystem.\n\tBundleSource option.Option[BundleSourceSpec]\n\n\t// WorkingDir specifies the working directory to start the docker image in.\n\tWorkingDir option.Option[ImagePath]\n\n\t// BuildInfo contains information about the build.\n\tBuildInfo BuildInfo\n\n\t// ProcessPerService specifies whether to run each service in a separate process.\n\tProcessPerService bool\n}\n\ntype (\n\t// HostPath is a path on the host filesystem.\n\tHostPath string\n\t// ImagePath is a path in the docker image.\n\tImagePath string\n\t// RelPath is a relative path.\n\tRelPath string\n)\n\nfunc (i ImagePath) Dir() ImagePath   { return ImagePath(pathpkg.Dir(string(i))) }\nfunc (i ImagePath) Clean() ImagePath { return ImagePath(pathpkg.Clean(string(i))) }\nfunc (i ImagePath) String() string   { return string(i) }\nfunc (i ImagePath) Join(p ...string) ImagePath {\n\treturn ImagePath(pathpkg.Join(string(i), pathpkg.Join(p...)))\n}\nfunc (i ImagePath) JoinImage(p ImagePath) ImagePath {\n\treturn i.Join(string(p))\n}\nfunc (h HostPath) Dir() HostPath { return HostPath(filepath.Dir(string(h))) }\nfunc (h HostPath) Join(p ...string) HostPath {\n\treturn HostPath(filepath.Join(string(h), filepath.Join(p...)))\n}\nfunc (h HostPath) JoinHost(p HostPath) HostPath {\n\treturn h.Join(string(p))\n}\nfunc (h HostPath) ToImage() ImagePath {\n\treturn ImagePath(string(h.ToUnix()))\n}\nfunc (h HostPath) ToUnix() HostPath {\n\tif runtime.GOOS == \"windows\" {\n\t\t// convert windows path with volume to a unix path, i.e c:\\some\\path -> /c/some/path\n\t\tvolume := filepath.VolumeName(string(h))\n\t\tif len(volume) == 2 && volume[1] == ':' {\n\t\t\treturn HostPath(\"/\" + string(volume[0]) + filepath.ToSlash(string(h[2:])))\n\t\t}\n\t}\n\n\treturn HostPath(filepath.ToSlash(string(h)))\n}\nfunc (h HostPath) String() string { return string(h) }\nfunc (h HostPath) Rel(target HostPath) (HostPath, error) {\n\trel, err := filepath.Rel(string(h), string(target))\n\treturn HostPath(rel), err\n}\nfunc (h HostPath) IsAbs() bool {\n\treturn filepath.IsAbs(h.String())\n}\n\n// Describe describes the docker image to build.\nfunc Describe(cfg DescribeConfig) (*ImageSpec, error) {\n\treturn newImageSpecBuilder().Describe(cfg)\n}\n\nfunc newImageSpecBuilder() *imageSpecBuilder {\n\treturn &imageSpecBuilder{\n\t\tprocIDGen: randomProcID,\n\t\tspec: &ImageSpec{\n\t\t\tCopyData:        make(map[ImagePath]HostPath),\n\t\t\tWriteFiles:      map[ImagePath][]byte{},\n\t\t\tFeatureFlags:    make(map[FeatureFlag]bool),\n\t\t\tBundledGateways: []string{},\n\t\t\tBundledServices: []string{},\n\t\t},\n\t\tseenArtifactDirs: make(map[HostPath]*imageArtifactDir),\n\t\tseenPrioFiles:    make(map[ImagePath]bool),\n\t}\n}\n\ntype imageArtifactDir struct {\n\tBase           ImagePath\n\tBuildArtifacts ImagePath\n}\n\ntype imageSpecBuilder struct {\n\tspec *ImageSpec\n\n\t// procIDGen generates a random id for each process.\n\t// Defaults to randomProcID.\n\tprocIDGen func() string\n\n\t// The artifact dirs we've already seen, to avoid\n\t// duplicate copies into the image.\n\tseenArtifactDirs map[HostPath]*imageArtifactDir\n\tseenPrioFiles    map[ImagePath]bool\n}\n\nconst (\n\t// defaultSupervisorMountPath is the path in the image where the supervisor is mounted.\n\tdefaultSupervisorMountPath ImagePath = \"/encore/bin/supervisor\"\n\n\t// defaultSupervisorConfigPath is the path in the image where the supervisor config is located.\n\tdefaultSupervisorConfigPath ImagePath = \"/encore/supervisor.config.json\"\n\n\t// defaultBuildInfoPath is the path in the image where the build information is located.\n\tdefaultBuildInfoPath ImagePath = \"/encore/build-info.json\"\n\n\t// defaultMetaPath is the path in the image where the application metadata is located.\n\tdefaultMetaPath ImagePath = \"/encore/meta\"\n)\n\nfunc (b *imageSpecBuilder) Describe(cfg DescribeConfig) (*ImageSpec, error) {\n\t// Allocate artifact directories for each output.\n\tfor _, out := range cfg.Compile.Outputs {\n\t\tb.allocArtifactDir(cfg, out)\n\t}\n\n\t// Determine if we should use the supervisor.\n\t// We must use the supervisor if we have more than one service or gateway.\n\tuseSupervisor := cfg.ProcessPerService || len(cfg.Compile.Outputs) > 1 || len(cfg.Compile.Outputs[0].GetEntrypoints()) > 1\n\n\tif !useSupervisor {\n\t\tep := cfg.Compile.Outputs[0].GetEntrypoints()[0]\n\t\tout := cfg.Compile.Outputs[0]\n\t\timageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())]\n\t\tif !ok {\n\t\t\treturn nil, errors.Errorf(\"missing image artifact dir for %q\", out.GetArtifactDir())\n\t\t}\n\t\tcmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts))\n\t\tb.spec.Entrypoint = cmd.Command\n\t\tb.spec.Env = cmd.Env\n\t} else {\n\t\tconfig := &supervisor.Config{\n\t\t\tNoopGateways: make(map[string]*noopgateway.Description),\n\t\t}\n\t\tsuper := SupervisorSpec{\n\t\t\tMountPath:  defaultSupervisorMountPath,\n\t\t\tConfigPath: defaultSupervisorConfigPath,\n\t\t\tConfig:     config,\n\t\t}\n\n\t\tseenGateways := make(map[string]bool)\n\t\tfor _, out := range cfg.Compile.Outputs {\n\t\t\timageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())]\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.Errorf(\"missing image artifact dir for %q\", out.GetArtifactDir())\n\t\t\t}\n\n\t\t\tfor _, ep := range out.GetEntrypoints() {\n\t\t\t\tcmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts))\n\t\t\t\tproc := supervisor.Proc{\n\t\t\t\t\tID:       b.procIDGen(),\n\t\t\t\t\tCommand:  cmd.Command,\n\t\t\t\t\tEnv:      cmd.Env,\n\t\t\t\t\tServices: slices.Clone(ep.Services),\n\t\t\t\t\tGateways: slices.Clone(ep.Gateways),\n\t\t\t\t}\n\t\t\t\tslices.Sort(proc.Services)\n\t\t\t\tslices.Sort(proc.Gateways)\n\n\t\t\t\tfor _, gw := range ep.Gateways {\n\t\t\t\t\tseenGateways[gw] = true\n\t\t\t\t}\n\n\t\t\t\tconfig.Procs = append(config.Procs, proc)\n\t\t\t}\n\t\t}\n\n\t\t// We need all gateways to be provided by some docker image. But for now, since we only support\n\t\t// a single docker image, we need all gateways to be provided by this image.\n\t\t// Each gateway that's not hosted by this image should be provided by a noop-gateway.\n\t\tif cfg.Meta != nil { // nil check for backwards compatibility\n\t\t\tfor _, gw := range cfg.Meta.Gateways {\n\t\t\t\tif !seenGateways[gw.EncoreName] {\n\t\t\t\t\tconfig.NoopGateways[gw.EncoreName] = noopgwdesc.Describe(cfg.Meta, nil)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tb.addPrio(super.MountPath)\n\t\tb.spec.Supervisor = option.Some(super)\n\t\tb.spec.Entrypoint = []string{string(super.MountPath), \"-c\", string(super.ConfigPath)}\n\t\tb.spec.Env = nil // not needed by supervisor\n\t}\n\n\t// TS apps use runtime config v2.\n\tif cfg.Meta.Language == meta.Lang_TYPESCRIPT {\n\t\tb.spec.FeatureFlags[NewRuntimeConfig] = true\n\t}\n\n\t// Compute bundled services and gateways.\n\t{\n\t\tfor _, out := range cfg.Compile.Outputs {\n\t\t\tfor _, ep := range out.GetEntrypoints() {\n\t\t\t\tb.spec.BundledServices = append(b.spec.BundledServices, ep.Services...)\n\t\t\t\tb.spec.BundledGateways = append(b.spec.BundledGateways, ep.Gateways...)\n\t\t\t}\n\t\t}\n\n\t\t// If we have any noop-gateways, consider them bundled, too.\n\t\tif super, ok := b.spec.Supervisor.Get(); ok {\n\t\t\tfor name := range super.Config.NoopGateways {\n\t\t\t\tb.spec.BundledGateways = append(b.spec.BundledGateways, name)\n\t\t\t}\n\t\t}\n\n\t\t// Sort and deduplicate.\n\t\tslices.Sort(b.spec.BundledServices)\n\t\tb.spec.BundledServices = slices.Compact(b.spec.BundledServices)\n\n\t\tslices.Sort(b.spec.BundledGateways)\n\t\tb.spec.BundledGateways = slices.Compact(b.spec.BundledGateways)\n\t}\n\n\t// Add entrypoint files to prioritized files.\n\tfor _, out := range cfg.Compile.Outputs {\n\t\thostArtifacts := HostPath(out.GetArtifactDir())\n\t\timageArtifacts, ok := b.seenArtifactDirs[hostArtifacts]\n\t\tif !ok {\n\t\t\treturn nil, errors.Errorf(\"missing image artifact dir for %q\", hostArtifacts)\n\t\t}\n\n\t\tfor _, ep := range out.GetEntrypoints() {\n\t\t\t// For each entrypoint, add prioritized files.\n\t\t\tfiles := ep.Cmd.PrioritizedFiles.Expand(paths.FS(imageArtifacts.BuildArtifacts))\n\t\t\tfor _, file := range files {\n\t\t\t\tb.addPrio(ImagePath(file))\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we have any JS outputs that need the local runtime, copy it into the image.\n\t{\n\t\tfor _, out := range cfg.Compile.Outputs {\n\t\t\tif _, ok := out.(*builder.JSBuildOutput); ok {\n\t\t\t\tif nativeRuntimeHost, ok := cfg.NodeRuntime.Get(); ok {\n\t\t\t\t\t// Add the encore-runtime.node file, and set the environment variable to point to it.\n\t\t\t\t\tnativeRuntimeImg := ImagePath(\"/encore/runtimes/js/encore-runtime.node\")\n\t\t\t\t\tb.spec.CopyData[nativeRuntimeImg] = nativeRuntimeHost\n\t\t\t\t\tb.spec.Env = append(b.spec.Env, fmt.Sprintf(\"ENCORE_RUNTIME_LIB=%s\", nativeRuntimeImg))\n\t\t\t\t\tb.addPrio(nativeRuntimeImg)\n\n\t\t\t\t\t// Copy the encore.dev package.\n\t\t\t\t\tnativePackageHost := cfg.Runtimes.Join(\"js\", \"encore.dev\")\n\t\t\t\t\tnativePackageImg := ImagePath(\"/encore/runtimes/js/encore.dev\")\n\t\t\t\t\tb.spec.CopyData[nativePackageImg] = nativePackageHost\n\t\t\t\t} else {\n\t\t\t\t\t// Copy the whole js runtime.\n\t\t\t\t\truntimeHost := cfg.Runtimes.Join(\"js\")\n\t\t\t\t\truntimeImg := ImagePath(\"/encore/runtimes/js\")\n\t\t\t\t\tb.spec.CopyData[runtimeImg] = runtimeHost\n\n\t\t\t\t\tnativeRuntimeImg := runtimeImg.Join(\"encore-runtime.node\")\n\t\t\t\t\tb.spec.Env = append(b.spec.Env, fmt.Sprintf(\"ENCORE_RUNTIME_LIB=%s\", nativeRuntimeImg))\n\t\t\t\t\tb.addPrio(nativeRuntimeImg)\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tb.spec.DockerBaseImage = cfg.DockerBaseImage.GetOrElse(\"scratch\")\n\tb.spec.BundleSource = cfg.BundleSource\n\tb.spec.WorkingDir = cfg.WorkingDir.GetOrElse(\"/\")\n\tb.spec.OS = cfg.Compile.OS\n\tb.spec.Arch = cfg.Compile.Arch\n\n\t// Include build information.\n\tb.spec.BuildInfo = BuildInfoSpec{\n\t\tInfo:     cfg.BuildInfo,\n\t\tInfoPath: defaultBuildInfoPath,\n\t}\n\n\t// Include the app metadata.\n\t{\n\t\tmd, err := proto.Marshal(cfg.Meta)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"marshal meta\")\n\t\t}\n\t\tb.spec.WriteFiles[defaultMetaPath] = md\n\t}\n\n\treturn b.spec, nil\n}\n\nfunc (b *imageSpecBuilder) addPrio(path ImagePath) {\n\tif !b.seenPrioFiles[path] {\n\t\tb.seenPrioFiles[path] = true\n\t\tb.spec.StargzPrioritizedFiles = append(b.spec.StargzPrioritizedFiles, path)\n\t}\n}\n\nfunc (b *imageSpecBuilder) allocArtifactDir(cfg DescribeConfig, out builder.BuildOutput) *imageArtifactDir {\n\thostArtifacts := HostPath(out.GetArtifactDir())\n\tif s := b.seenArtifactDirs[hostArtifacts]; s != nil {\n\t\t// Already copied this artifact dir.\n\t\treturn s\n\t}\n\n\tbundle, hasBundle := cfg.BundleSource.Get()\n\t_, isJSOutput := out.(*builder.JSBuildOutput)\n\n\t// For TS apps the artifacts will be bundled with the source\n\tif isJSOutput && hasBundle {\n\t\t// If hostArtifacts are within the the bundled source, we dont need to copy them.\n\t\tif strings.HasPrefix(string(hostArtifacts), string(bundle.Source)) {\n\t\t\trelpath, err := filepath.Rel(string(bundle.Source), string(hostArtifacts))\n\t\t\tif err != nil {\n\t\t\t\tpanic(fmt.Sprintf(\"failed to calculate relative path from %q to %q: %v\", bundle.Source, hostArtifacts, err))\n\t\t\t}\n\n\t\t\timageBuildArtifactsDir := bundle.Dest.Join(string(filepath.ToSlash(relpath)))\n\n\t\t\tartifactDir := &imageArtifactDir{\n\t\t\t\tBase:           imageBuildArtifactsDir.Dir(),\n\t\t\t\tBuildArtifacts: imageBuildArtifactsDir,\n\t\t\t}\n\n\t\t\tb.seenArtifactDirs[hostArtifacts] = artifactDir\n\t\t\treturn artifactDir\n\t\t}\n\t}\n\n\t// This artifact directory has not been copied yet.\n\t// Determine a reasonable name for it.\n\tbasePath := \"/artifacts\"\n\n\tfor i := 0; ; i++ {\n\t\tcandidatePath := ImagePath(pathpkg.Join(basePath, strconv.Itoa(i)))\n\t\tcandidate := &imageArtifactDir{\n\t\t\tBase:           candidatePath,\n\t\t\tBuildArtifacts: candidatePath.Join(\"build\"),\n\t\t}\n\t\tif b.spec.CopyData[candidate.Base] == \"\" && b.spec.CopyData[candidate.BuildArtifacts] == \"\" {\n\t\t\t// This name is available.\n\t\t\tb.spec.CopyData[candidate.BuildArtifacts] = hostArtifacts\n\t\t\tb.seenArtifactDirs[hostArtifacts] = candidate\n\t\t\treturn candidate\n\t\t}\n\n\t\t// This path already exists. Keep trying.\n\t}\n\n}\n\nfunc randomProcID() string {\n\treturn fmt.Sprintf(\"proc_%s\", xid.New())\n}\n\n// DetermineIncludes determines what paths within the workspace should be included in the image.\nfunc DetermineIncludes(appLang appfile.Lang, bundleSource bool, workspaceRoot string, appRoot string) ([]RelPath, error) {\n\t// if the actual setting is set, include all files from the workspace\n\tif bundleSource {\n\t\treturn []RelPath{\".\"}, nil\n\t}\n\n\t// app root is always included\n\tif workspaceRoot == appRoot {\n\t\treturn nil, nil\n\t}\n\n\tvar extraIncludePaths []RelPath\n\n\t// Include node_modules and package.json from the workspace root if the app is a TypeScript app.\n\tif appLang == appfile.LangTS {\n\t\tdir := filepath.Dir(appRoot)\n\t\tfor {\n\t\t\trelPath, err := filepath.Rel(workspaceRoot, dir)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"relative path from workspace root\")\n\t\t\t}\n\n\t\t\tpathsToCheck := []string{\"node_modules\", \"package.json\"}\n\t\t\tfor _, path := range pathsToCheck {\n\t\t\t\tif _, err := os.Stat(filepath.Join(dir, path)); err == nil {\n\t\t\t\t\textraIncludePaths = append(extraIncludePaths, RelPath(filepath.Join(relPath, path)))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif dir == workspaceRoot {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdir = filepath.Dir(dir)\n\t\t}\n\t}\n\n\t// Walk all files and folders in includedPaths and find any symlinks.\n\t// Add the symlink target to includedPaths if it is within the workspace root.\n\tfor _, path := range extraIncludePaths {\n\t\tabsPath := filepath.Join(workspaceRoot, string(path))\n\t\tfilepath.Walk(absPath, func(path string, fi os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif fi.Mode()&os.ModeSymlink != 0 {\n\t\t\t\ttarget, err := os.Readlink(path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tabsTarget := filepath.Join(filepath.Dir(path), filepath.Clean(target))\n\t\t\t\trelTarget, err := filepath.Rel(workspaceRoot, absTarget)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tif strings.HasPrefix(absTarget, workspaceRoot) {\n\t\t\t\t\textraIncludePaths = append(extraIncludePaths, RelPath(relTarget))\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn extraIncludePaths, nil\n}\n"
  },
  {
    "path": "pkg/dockerbuild/spec_test.go",
    "content": "package dockerbuild\n\nimport (\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/golang/protobuf/proto\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/supervisor\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nfunc TestBuild_Node(t *testing.T) {\n\tc := qt.New(t)\n\tcfg := DescribeConfig{\n\t\tMeta:     &meta.Data{Language: meta.Lang_TYPESCRIPT},\n\t\tRuntimes: \"/host/runtimes\",\n\t\tBundleSource: option.Some(BundleSourceSpec{\n\t\t\tSource:         \"/host\",\n\t\t\tDest:           \"/image\",\n\t\t\tAppRootRelpath: \".\",\n\t\t}),\n\t\tCompile: &builder.CompileResult{Outputs: []builder.BuildOutput{\n\t\t\t&builder.JSBuildOutput{\n\t\t\t\tArtifactDir: \"/host/artifacts\",\n\t\t\t\tEntrypoints: []builder.Entrypoint{{\n\t\t\t\t\tCmd: builder.CmdSpec{\n\t\t\t\t\t\tCommand:          builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t\tPrioritizedFiles: builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t},\n\t\t\t\t\tServices: []string{\"foo\", \"bar\"},\n\t\t\t\t\tGateways: []string{\"baz\", \"qux\"},\n\t\t\t\t}},\n\t\t\t},\n\t\t}},\n\t}\n\tspec, err := describe(cfg)\n\tc.Assert(err, qt.IsNil)\n\n\tmeta, err := proto.Marshal(cfg.Meta)\n\tc.Assert(err, qt.IsNil)\n\n\topts := append([]cmp.Option{cmpopts.EquateEmpty()}, option.CmpOpts()...)\n\tc.Assert(spec, qt.CmpEquals(opts...), &ImageSpec{\n\t\tEntrypoint: []string{\"/image/artifacts/entrypoint\"},\n\t\tEnv: []string{\n\t\t\t\"ENCORE_RUNTIME_LIB=/encore/runtimes/js/encore-runtime.node\",\n\t\t},\n\t\tWorkingDir: \"/\",\n\t\tBuildInfo:  BuildInfoSpec{InfoPath: defaultBuildInfoPath},\n\t\tWriteFiles: map[ImagePath][]byte{defaultMetaPath: meta},\n\t\tCopyData: map[ImagePath]HostPath{\n\t\t\t\"/encore/runtimes/js\": \"/host/runtimes/js\",\n\t\t},\n\t\tBundleSource: option.Some(BundleSourceSpec{\n\t\t\tSource:         \"/host\",\n\t\t\tDest:           \"/image\",\n\t\t\tAppRootRelpath: \".\",\n\t\t\tExcludeSource:  []RelPath{},\n\t\t\tIncludeSource:  []RelPath{},\n\t\t}),\n\t\tSupervisor:      option.None[SupervisorSpec](),\n\t\tBundledServices: []string{\"bar\", \"foo\"},\n\t\tBundledGateways: []string{\"baz\", \"qux\"},\n\t\tDockerBaseImage: \"scratch\",\n\t\tFeatureFlags:    map[FeatureFlag]bool{NewRuntimeConfig: true},\n\t\tStargzPrioritizedFiles: []ImagePath{\n\t\t\t\"/image/artifacts/entrypoint\",\n\t\t\t\"/encore/runtimes/js/encore-runtime.node\",\n\t\t},\n\t})\n}\n\nfunc TestBuild_Go_SingleBinary(t *testing.T) {\n\tc := qt.New(t)\n\tcfg := DescribeConfig{\n\t\tMeta: &meta.Data{},\n\t\tCompile: &builder.CompileResult{Outputs: []builder.BuildOutput{\n\t\t\t&builder.GoBuildOutput{\n\t\t\t\tArtifactDir: \"/host/artifacts\",\n\t\t\t\tEntrypoints: []builder.Entrypoint{\n\t\t\t\t\t{\n\t\t\t\t\t\tCmd: builder.CmdSpec{\n\t\t\t\t\t\t\tCommand:          builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t\t\tPrioritizedFiles: builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tServices: []string{\"foo\", \"bar\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}},\n\t}\n\n\tspec, err := describe(cfg)\n\tc.Assert(err, qt.IsNil)\n\n\topts := append([]cmp.Option{cmpopts.EquateEmpty()}, option.CmpOpts()...)\n\tc.Assert(spec, qt.CmpEquals(opts...), &ImageSpec{\n\t\tEntrypoint: []string{\"/artifacts/0/build/entrypoint\"},\n\t\tEnv:        nil,\n\t\tWorkingDir: \"/\",\n\t\tBuildInfo:  BuildInfoSpec{InfoPath: defaultBuildInfoPath},\n\t\tCopyData: map[ImagePath]HostPath{\n\t\t\t\"/artifacts/0/build\": \"/host/artifacts\",\n\t\t},\n\t\tBundledServices: []string{\"bar\", \"foo\"},\n\t\tBundleSource:    option.Option[BundleSourceSpec]{},\n\t\tSupervisor:      option.None[SupervisorSpec](),\n\t\tDockerBaseImage: \"scratch\",\n\t\tFeatureFlags:    map[FeatureFlag]bool{},\n\t\tStargzPrioritizedFiles: []ImagePath{\n\t\t\t\"/artifacts/0/build/entrypoint\",\n\t\t},\n\t\tWriteFiles: map[ImagePath][]byte{\n\t\t\tdefaultMetaPath: {},\n\t\t},\n\t})\n}\n\nfunc TestBuild_Go_MultiProc(t *testing.T) {\n\tc := qt.New(t)\n\tcfg := DescribeConfig{\n\t\tMeta: &meta.Data{Language: meta.Lang_TYPESCRIPT},\n\t\tCompile: &builder.CompileResult{Outputs: []builder.BuildOutput{\n\t\t\t&builder.GoBuildOutput{\n\t\t\t\tArtifactDir: \"/host/artifacts\",\n\t\t\t\tEntrypoints: []builder.Entrypoint{\n\t\t\t\t\t{\n\t\t\t\t\t\tCmd: builder.CmdSpec{\n\t\t\t\t\t\t\tCommand:          builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t\t\tPrioritizedFiles: builder.ArtifactStrings{\"${ARTIFACT_DIR}/entrypoint\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tServices: []string{\"foo\"},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCmd: builder.CmdSpec{\n\t\t\t\t\t\t\tCommand:          builder.ArtifactStrings{\"${ARTIFACT_DIR}/other-entrypoint\"},\n\t\t\t\t\t\t\tPrioritizedFiles: builder.ArtifactStrings{\"${ARTIFACT_DIR}/other-entrypoint\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tServices: []string{\"bar\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}},\n\t}\n\n\tspec, err := describe(cfg)\n\tc.Assert(err, qt.IsNil)\n\n\tmeta, err := proto.Marshal(cfg.Meta)\n\tc.Assert(err, qt.IsNil)\n\n\topts := append([]cmp.Option{cmpopts.EquateEmpty()}, option.CmpOpts()...)\n\tc.Assert(spec, qt.CmpEquals(opts...), &ImageSpec{\n\t\tEntrypoint: []string{\"/encore/bin/supervisor\", \"-c\", string(defaultSupervisorConfigPath)},\n\t\tEnv:        nil,\n\t\tWorkingDir: \"/\",\n\t\tBuildInfo:  BuildInfoSpec{InfoPath: defaultBuildInfoPath},\n\t\tCopyData: map[ImagePath]HostPath{\n\t\t\t\"/artifacts/0/build\": \"/host/artifacts\",\n\t\t},\n\t\tBundledServices: []string{\"bar\", \"foo\"},\n\t\tBundleSource:    option.Option[BundleSourceSpec]{},\n\t\tSupervisor: option.Some(SupervisorSpec{\n\t\t\tMountPath:  \"/encore/bin/supervisor\",\n\t\t\tConfigPath: defaultSupervisorConfigPath,\n\t\t\tConfig: &supervisor.Config{\n\t\t\t\tProcs: []supervisor.Proc{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"proc-id\",\n\t\t\t\t\t\tCommand:  []string{\"/artifacts/0/build/entrypoint\"},\n\t\t\t\t\t\tServices: []string{\"foo\"},\n\t\t\t\t\t\tGateways: []string{},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"proc-id\",\n\t\t\t\t\t\tCommand:  []string{\"/artifacts/0/build/other-entrypoint\"},\n\t\t\t\t\t\tServices: []string{\"bar\"},\n\t\t\t\t\t\tGateways: []string{},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}),\n\t\tDockerBaseImage: \"scratch\",\n\t\tFeatureFlags:    map[FeatureFlag]bool{NewRuntimeConfig: true},\n\t\tStargzPrioritizedFiles: []ImagePath{\n\t\t\t\"/encore/bin/supervisor\",\n\t\t\t\"/artifacts/0/build/entrypoint\",\n\t\t\t\"/artifacts/0/build/other-entrypoint\",\n\t\t},\n\t\tWriteFiles: map[ImagePath][]byte{\n\t\t\tdefaultMetaPath: meta,\n\t\t},\n\t})\n}\n\n// describe is like Describe but mocks the proc id generation\n// for reproducible tests.\nfunc describe(cfg DescribeConfig) (*ImageSpec, error) {\n\tb := newImageSpecBuilder()\n\tb.procIDGen = func() string { return \"proc-id\" }\n\treturn b.Describe(cfg)\n}\n"
  },
  {
    "path": "pkg/dockerbuild/tarcopy.go",
    "content": "package dockerbuild\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/google/go-containerregistry/pkg/v1/tarball\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/tarstream\"\n\t\"encr.dev/pkg/xos\"\n\t\"encr.dev/v2/compiler/build\"\n)\n\ntype tarCopier struct {\n\tfileTimes *time.Time\n\tentries   []*tarEntry\n\tseenDirs  map[ImagePath]bool\n}\n\nfunc newTarCopier(opts ...tarCopyOption) *tarCopier {\n\ttc := &tarCopier{\n\t\tseenDirs: make(map[ImagePath]bool),\n\t}\n\tfor _, opt := range opts {\n\t\topt(tc)\n\t}\n\treturn tc\n}\n\ntype tarCopyOption func(*tarCopier)\n\nfunc setFileTimes(t time.Time) tarCopyOption {\n\treturn func(tc *tarCopier) {\n\t\ttc.fileTimes = &t\n\t}\n}\n\n// dirCopyDesc describes how to copy a directory to the tar.\ntype dirCopyDesc struct {\n\tSpec    *ImageSpec\n\tSrcPath HostPath\n\tDstPath ImagePath\n\n\t// Src paths to exclude.\n\tExcludeSrcPaths map[HostPath]bool\n\n\t// Src paths to include.\n\tIncludeSrcPaths []HostPath\n}\n\nfunc (tc *tarCopier) CopyData(spec *ImageSpec) error {\n\t// Sort the paths by the destination path so that the tar file is deterministic.\n\ttype pathPair struct {\n\t\tSrc  HostPath\n\t\tDest ImagePath\n\t}\n\n\tvar paths []pathPair\n\tfor dest, src := range spec.CopyData {\n\t\tpaths = append(paths, pathPair{Src: src, Dest: dest})\n\t}\n\tsort.Slice(paths, func(i, j int) bool {\n\t\treturn paths[i].Dest < paths[j].Dest\n\t})\n\n\tfor _, p := range paths {\n\t\tfi, err := os.Stat(string(p.Src))\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"stat source file\")\n\t\t}\n\t\tif err := tc.MkdirAll(p.Dest.Dir(), 0755); err != nil {\n\t\t\treturn errors.Wrap(err, \"create dirs\")\n\t\t}\n\t\tif fi.IsDir() {\n\t\t\terr = tc.CopyDir(&dirCopyDesc{\n\t\t\t\tSpec:            spec,\n\t\t\t\tSrcPath:         p.Src,\n\t\t\t\tDstPath:         p.Dest,\n\t\t\t\tExcludeSrcPaths: nil,\n\t\t\t\tIncludeSrcPaths: []HostPath{p.Src},\n\t\t\t})\n\t\t} else {\n\t\t\terr = tc.CopyFile(p.Dest, p.Src, fi, \"\")\n\t\t}\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"copy path\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// shouldInclude returns true if the path should be included in the tar.\nfunc shouldInclude(desc *dirCopyDesc, path HostPath) bool {\n\tfor _, include := range desc.IncludeSrcPaths {\n\t\tif string(path) == string(include) {\n\t\t\treturn true\n\t\t}\n\n\t\tif strings.HasPrefix(string(path), string(include)) {\n\t\t\treturn true\n\t\t}\n\n\t\tif strings.HasPrefix(string(include), string(path)) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (tc *tarCopier) CopyDir(desc *dirCopyDesc) error {\n\terr := filepath.WalkDir(string(desc.SrcPath), func(pathStr string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpath := HostPath(pathStr)\n\n\t\t// Should we keep this path?\n\t\tif !shouldInclude(desc, path) {\n\t\t\tif d.IsDir() {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t} else {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\t// Should we skip this path?\n\t\tif desc.ExcludeSrcPaths[path] {\n\t\t\tif d.IsDir() {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t} else {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\trelPath, err := desc.SrcPath.Rel(path)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\tdstPath := desc.DstPath.Join(string(relPath.ToImage()))\n\n\t\t// If this is a symlink, compute the link target relative to DstPath.\n\t\tvar link ImagePath\n\n\t\tisSymlink := d.Type()&fs.ModeSymlink != 0\n\t\tif !isSymlink && runtime.GOOS == \"windows\" {\n\t\t\t// Check if the file is a junction point on Windows.\n\t\t\tif isJunction, _ := xos.IsWindowsJunctionPoint(pathStr); isJunction {\n\t\t\t\treturn errors.Newf(\"%q is a windows junction point and cannot be copied to a docker image. Use symlinks instead.\", pathStr)\n\t\t\t}\n\n\t\t}\n\n\t\tif isSymlink {\n\t\t\ttarget, err := os.Readlink(string(path))\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t}\n\n\t\t\tlink, err = tc.rewriteSymlink(desc, path, HostPath(target))\n\t\t\tif err != nil {\n\t\t\t\treturn errors.WithStack(err)\n\t\t\t} else if link == \"\" {\n\t\t\t\t// Drop the symlink\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tfi, err := d.Info()\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\terr = tc.CopyFile(dstPath, path, fi, link)\n\t\treturn errors.Wrap(err, \"add file\")\n\t})\n\n\treturn errors.WithStack(err)\n}\n\n// rewriteSymlink rewrites the symlink to the target filesystem.\nfunc (tc *tarCopier) rewriteSymlink(desc *dirCopyDesc, path HostPath, linkTarget HostPath) (newTarget ImagePath, err error) {\n\tvar (\n\t\tabsTarget      HostPath\n\t\trelFromSrcPath HostPath\n\t)\n\n\tif linkTarget.IsAbs() {\n\t\t// It's a link to an absolute destination.\n\t\t// Determine its relative path, and see if that lives within the desc.SrcPath.\n\t\tabsTarget = linkTarget\n\t\t// On Windows, we can only make a relative link if the source and target are on the same volume.\n\t\tif runtime.GOOS != \"windows\" || filepath.VolumeName(desc.SrcPath.String()) == filepath.VolumeName(absTarget.String()) {\n\t\t\trelFromSrcPath, err = desc.SrcPath.Rel(absTarget)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\t// If the relative path is local to the SrcPath, allow it.\n\t\t\tif filepath.IsLocal(relFromSrcPath.String()) {\n\t\t\t\treturn desc.DstPath.JoinImage(relFromSrcPath.ToImage()), nil\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// We have a relative link target. Determine its absolute destination.\n\t\t// Use path.Dir() since the symlink is relative to its directory, not relative to itself.\n\t\tabsTarget = path.Dir().JoinHost(linkTarget)\n\n\t\t// Determine its relative path, and see if that lives within the desc.SrcPath.\n\t\trelFromSrcPath, err = desc.SrcPath.Rel(absTarget)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// If the relative path is local to the SrcPath, allow it.\n\t\tif filepath.IsLocal(relFromSrcPath.String()) {\n\t\t\treturn desc.DstPath.JoinImage(relFromSrcPath.ToImage()), nil\n\t\t}\n\t}\n\n\t// Otherwise, determine if the absTarget is within some other path being copied.\n\tabsTargetStr := absTarget.String()\n\tfor dst, src := range desc.Spec.CopyData {\n\t\tsrcStr := src.String()\n\t\tstcStrSep := srcStr + string(filepath.Separator)\n\t\tif absTargetStr == srcStr {\n\t\t\treturn dst, nil\n\t\t} else if suffix, found := strings.CutPrefix(absTargetStr, stcStrSep); found {\n\t\t\t// It lives within the target. Compute the new target path.\n\t\t\treturn dst.Join(suffix), nil\n\t\t}\n\t}\n\n\tlog.Debug().\n\t\tStr(\"target\", linkTarget.String()).\n\t\tStr(\"rel_target\", relFromSrcPath.String()).\n\t\tStr(\"abs_target\", absTarget.String()).\n\t\tMsg(\"dropping escaping symlink\")\n\n\treturn \"\", nil\n}\n\nfunc (tc *tarCopier) MkdirAll(dstPath ImagePath, mode fs.FileMode) (err error) {\n\tdstPath = ImagePath(filepath.ToSlash(dstPath.String()))\n\tdstPath = dstPath.Clean()\n\n\tfor dstPath != \".\" && dstPath != \"/\" {\n\t\tif !tc.seenDirs[dstPath] {\n\t\t\tmodTime := time.Time{}\n\t\t\tif tc.fileTimes != nil {\n\t\t\t\tmodTime = *tc.fileTimes\n\t\t\t}\n\t\t\theader := &tar.Header{\n\t\t\t\tTypeflag: tar.TypeDir,\n\t\t\t\tModTime:  modTime,\n\t\t\t\tName:     tarHeaderName(dstPath, true),\n\t\t\t\tMode:     int64(mode.Perm()),\n\t\t\t}\n\t\t\ttc.entries = append(tc.entries, &tarEntry{\n\t\t\t\theader: header,\n\t\t\t})\n\t\t\ttc.seenDirs[dstPath] = true\n\t\t}\n\n\t\tdstPath = dstPath.Dir()\n\t}\n\n\treturn nil\n}\n\nfunc (tc *tarCopier) CopyFile(dstPath ImagePath, srcPath HostPath, fi fs.FileInfo, linkTarget ImagePath) (err error) {\n\theader, err := tar.FileInfoHeader(fi, linkTarget.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif tc.fileTimes != nil {\n\t\tt := *tc.fileTimes\n\t\theader.ModTime = t\n\t\theader.AccessTime = t\n\t\theader.ChangeTime = t\n\t}\n\n\t// HACK: make the linux binary executable when cross compiling from windows as the unix permissions gets lost.\n\tif runtime.GOOS == \"windows\" && fi.Name() == build.BinaryName {\n\t\theader.Mode = 0755\n\t}\n\n\theader.Name = tarHeaderName(dstPath, fi.IsDir())\n\tentry := &tarEntry{header: header}\n\ttc.entries = append(tc.entries, entry)\n\n\tif fi.IsDir() {\n\t\ttc.seenDirs[dstPath] = true\n\t\treturn nil\n\t}\n\n\t// If this is not a symlink, write the file.\n\tif (fi.Mode() & fs.ModeSymlink) != fs.ModeSymlink {\n\t\tentry.hostPath = option.Some(srcPath)\n\t}\n\n\treturn nil\n}\n\nfunc (tc *tarCopier) WriteFile(dstPath ImagePath, mode fs.FileMode, data []byte) (err error) {\n\theader := &tar.Header{\n\t\tName:     tarHeaderName(dstPath, false),\n\t\tTypeflag: tar.TypeReg,\n\t\tMode:     int64(mode.Perm()),\n\t\tSize:     int64(len(data)),\n\t}\n\tif tc.fileTimes != nil {\n\t\tt := *tc.fileTimes\n\t\theader.ModTime = t\n\t\theader.AccessTime = t\n\t\theader.ChangeTime = t\n\t}\n\n\ttc.entries = append(tc.entries, &tarEntry{\n\t\theader: header,\n\t\tdata:   option.Some(data),\n\t})\n\treturn nil\n}\n\ntype tarEntry struct {\n\theader *tar.Header\n\n\tdata     option.Option[[]byte]\n\thostPath option.Option[HostPath]\n}\n\nfunc (tc *tarCopier) Opener() tarball.Opener {\n\terrThunk := func(err error) tarball.Opener {\n\t\treturn func() (io.ReadCloser, error) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar dvecs []tarstream.Datavec\n\tfor _, e := range tc.entries {\n\n\t\t// create buffer to write tar header to\n\t\tbuf := new(bytes.Buffer)\n\t\ttw := tar.NewWriter(buf)\n\n\t\t// write tar header to buffer\n\t\tif err := tw.WriteHeader(e.header); err != nil {\n\t\t\treturn errThunk(errors.Wrap(err, fmt.Sprintf(\"writing header %v\", e)))\n\t\t}\n\n\t\tmemv := tarstream.MemVec{\n\t\t\tData: buf.Bytes(),\n\t\t}\n\n\t\t// add the tar header mem buffer to the tarvec\n\t\tdvecs = append(dvecs, memv)\n\n\t\tvar dataEntry tarstream.Datavec\n\t\tif hostPath, ok := e.hostPath.Get(); ok {\n\t\t\tfi := e.header.FileInfo()\n\t\t\tdataEntry = &tarstream.PathVec{\n\t\t\t\tPath: hostPath.String(),\n\t\t\t\tInfo: fi,\n\t\t\t}\n\t\t} else if data, ok := e.data.Get(); ok {\n\t\t\tdataEntry = tarstream.MemVec{Data: data}\n\t\t}\n\n\t\tif dataEntry != nil {\n\t\t\t// add the file path info to the tarvec\n\t\t\tdvecs = append(dvecs, dataEntry)\n\n\t\t\t// tar requires file entries to be padded out to 512 bytes.\n\t\t\tif !e.header.FileInfo().IsDir() {\n\t\t\t\tif size := dataEntry.GetSize(); size%512 != 0 {\n\t\t\t\t\tpadv := tarstream.PadVec{\n\t\t\t\t\t\tSize: 512 - (size % 512),\n\t\t\t\t\t}\n\t\t\t\t\tdvecs = append(dvecs, padv)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttv := tarstream.NewTarVec(dvecs)\n\treturn func() (io.ReadCloser, error) {\n\t\ttv2 := tv.Clone()\n\t\treturn tv2, nil\n\t}\n}\n\nfunc tarHeaderName(p ImagePath, isDir bool) string {\n\tname := strings.TrimPrefix(filepath.ToSlash(p.String()), \"/\")\n\tif isDir {\n\t\tname += \"/\"\n\t}\n\treturn name\n}\n"
  },
  {
    "path": "pkg/editors/LICENSE",
    "content": "Original Copyright (c) GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "pkg/editors/doc.go",
    "content": "// Package editors is a Go Port of [GitHub Desktop's editor code](https://github.com/desktop/desktop/tree/development/app/src/lib/editors)\n//\n// It was taken at this commit [a1ece18](https://github.com/desktop/desktop/tree/a1ece186bae3a742f206c5edeb0762e78fba2f7c/app/src/lib/editors)\npackage editors\n"
  },
  {
    "path": "pkg/editors/encore_names.go",
    "content": "package editors\n\n/*\n   This file was added by Encore and is not part of the original GitHub Desktop codebase\n*/\n\n// EditorName represents the name of an editor we display to a user\n//\n// This file contains the full list of editors we support\ntype EditorName string\n\nconst (\n\tAndroidStudio       EditorName = \"Android Studio\"\n\tAptanaStudio        EditorName = \"Aptana Studio\"\n\tAtom                EditorName = \"Atom\"\n\tAtomBeta            EditorName = \"Atom (Beta)\"\n\tAtomNightly         EditorName = \"Atom (Nightly)\"\n\tBBEdit              EditorName = \"BBEdit\"\n\tBrackets            EditorName = \"Brackets\"\n\tCode                EditorName = \"Code\"\n\tCodeRunner          EditorName = \"CodeRunner\"\n\tColdFusionBuilder   EditorName = \"ColdFusion Builder\"\n\tEmacs               EditorName = \"Emacs\"\n\tGeany               EditorName = \"Geany\"\n\tGEdit               EditorName = \"GEdit\"\n\tGnomeBuilder        EditorName = \"GNOME Builder\"\n\tGnomeTextEditor     EditorName = \"GNOME Text Editor\"\n\tGVim                EditorName = \"gVim\"\n\tJetbrainsCLion      EditorName = \"CLion\"\n\tJetbrainsDataSpell  EditorName = \"DataSpell\"\n\tJetbrainsFleet      EditorName = \"Fleet\"\n\tJetbrainsGoLand     EditorName = \"GoLand\"\n\tJetbrainsIntelliJ   EditorName = \"IntelliJ\"\n\tJetbrainsIntelliJCE EditorName = \"IntelliJ CE\"\n\tJetbrainsPhpStorm   EditorName = \"PhpStorm\"\n\tJetbrainsPyCharm    EditorName = \"PyCharm\"\n\tJetbrainsPyCharmCE  EditorName = \"PyCharm CE\"\n\tJetbrainsRider      EditorName = \"Rider\"\n\tJetbrainsRubyMine   EditorName = \"RubyMine\"\n\tJetbrainsWebStorm   EditorName = \"WebStorm\"\n\tKate                EditorName = \"Kate\"\n\tLiteXL              EditorName = \"Lite XL\"\n\tMacVim              EditorName = \"MacVim\"\n\tMousepad            EditorName = \"Mousepad\"\n\tNeovim              EditorName = \"Neovim\"\n\tNeovimQt            EditorName = \"Neovim-Qt\"\n\tNeovide             EditorName = \"Neovide\"\n\tNotePadPlusPlus     EditorName = \"Notepad++\"\n\tNotepadqq           EditorName = \"Notepadqq\"\n\tNova                EditorName = \"Nova\"\n\tPulsar              EditorName = \"Pulsar\"\n\tRStudio             EditorName = \"RStudio\"\n\tSlickEdit           EditorName = \"SlickEdit\"\n\tStudio              EditorName = \"Studio\"\n\tSublimeText         EditorName = \"Sublime Text\"\n\tTextMate            EditorName = \"TextMate\"\n\tTypora              EditorName = \"Typora\"\n\tVimR                EditorName = \"VimR\"\n\tVSCode              EditorName = \"VSCode\"\n\tVSCodeInsiders      EditorName = \"VSCode (Insiders)\"\n\tVSCodium            EditorName = \"VSCodium\"\n\tVSCodiumInsiders    EditorName = \"VSCodium (Insiders)\"\n\tXCode               EditorName = \"XCode\"\n\tZed                 EditorName = \"Zed\"\n\tZedPreview          EditorName = \"Zed (Preview)\"\n\tCursor              EditorName = \"Cursor\"\n)\n"
  },
  {
    "path": "pkg/editors/encore_urls.go",
    "content": "package editors\n\n/*\n   This file was added by Encore and is not part of the original GitHub Desktop codebase\n*/\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n)\n\n// convertFilePathToURLScheme converts a file path to a URL scheme that can be used to open to a specific\n// line and column number.\n//\n// If the returned string should be executed as a URL, true is returned as the second argument. If the returned string\n// should be executed as a normal argument against the editor, then false is returned\n//\n// If no URL scheme exists for that editor and empty string and false is returned.\nfunc convertFilePathToURLScheme(editorName EditorName, fullPath string, startLine int, startCol int) (openArg string, executeAsURL bool) {\n\tswitch editorName {\n\tcase VSCode, VSCodeInsiders:\n\t\tif startLine > 0 {\n\t\t\tfullPath = fmt.Sprintf(\"%s:%d\", fullPath, startLine)\n\t\t}\n\t\treturn toURLScheme(\"vscode\", \"file\", fullPath, \"\", \"\", 0, 0), true\n\tcase Cursor:\n\t\tif startLine > 0 {\n\t\t\tfullPath = fmt.Sprintf(\"%s:%d\", fullPath, startLine)\n\t\t}\n\t\treturn toURLScheme(\"cursor\", \"file\", fullPath, \"\", \"\", 0, 0), true\n\tcase JetbrainsGoLand:\n\t\treturn toJetBrainsScheme(\"goland\", fullPath, startLine, startCol), true\n\tcase JetbrainsPhpStorm:\n\t\treturn toJetBrainsScheme(\"phpstorm\", fullPath, startLine, startCol), true\n\tcase JetbrainsPyCharm, JetbrainsPyCharmCE:\n\t\treturn toJetBrainsScheme(\"pycharm\", fullPath, startLine, startCol), true\n\tcase JetbrainsRubyMine:\n\t\treturn toJetBrainsScheme(\"rubymine\", fullPath, startLine, startCol), true\n\tcase JetbrainsWebStorm:\n\t\treturn toJetBrainsScheme(\"webstorm\", fullPath, startLine, startCol), true\n\tcase JetbrainsIntelliJ, JetbrainsIntelliJCE:\n\t\treturn toJetBrainsScheme(\"idea\", fullPath, startLine, startCol), true\n\tcase JetbrainsCLion:\n\t\treturn toJetBrainsScheme(\"clion\", fullPath, startLine, startCol), true\n\tcase TextMate:\n\t\treturn toOpenURLScheme(\"txmt\", \"\", fullPath, startLine, startCol), true\n\tcase BBEdit:\n\t\treturn toOpenURLScheme(\"bbedit\", \"\", fullPath, startLine, startCol), true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc toJetBrainsScheme(scheme string, file string, line int, col int) string {\n\treturn toURLScheme(scheme, \"open\", \"\", \"file\", file, line, col)\n}\n\nfunc toOpenURLScheme(scheme string, basePath string, file string, line int, col int) string {\n\treturn toURLScheme(scheme, \"open\", basePath, \"url\", fmt.Sprintf(\"file://%s\", file), line, col)\n}\n\nfunc toURLScheme(scheme string, host string, basePath string, fileKey string, file string, line int, col int) string {\n\tu := &url.URL{\n\t\tScheme: scheme,\n\t\tHost:   host,\n\t\tPath:   basePath,\n\t}\n\n\tq := u.Query()\n\tif fileKey != \"\" && file != \"\" {\n\t\tq.Set(fileKey, file)\n\t}\n\tif line > 0 {\n\t\tq.Set(\"line\", fmt.Sprintf(\"%d\", line))\n\n\t\tif col > 0 {\n\t\t\tq.Set(\"col\", fmt.Sprintf(\"%d\", col))\n\t\t}\n\t}\n\n\tu.RawQuery = q.Encode()\n\n\treturn u.String()\n}\n"
  },
  {
    "path": "pkg/editors/launch.go",
    "content": "package editors\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/pkg/browser\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/xos\"\n)\n\n// LaunchExternalEditor opens a given file or folder in the desired external editor.\nfunc LaunchExternalEditor(fullPath string, startLine int, startCol int, editor FoundEditor) error {\n\t_, err := os.Stat(editor.Path)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn errors.Wrapf(err, \"editor %s not found\", editor.Editor)\n\t}\n\n\t// Encore patch to allow opening to a specific line and column in the file\n\t// if supported by the IDE\n\ttoExecute, executeAsURL := convertFilePathToURLScheme(editor.Editor, fullPath, startLine, startCol)\n\tif executeAsURL {\n\t\tlog.Info().Str(\"full_path\", fullPath).Str(\"editor\", string(editor.Editor)).Str(\"url\", toExecute).Msg(\"attempting to open file via URL\")\n\t\tif err := browser.OpenURL(toExecute); err == nil {\n\t\t\treturn nil\n\t\t} else {\n\t\t\tlog.Warn().Err(err).Str(\"url\", toExecute).Msg(\"failed to open URL, falling back to file appraoch\")\n\t\t\t// If the URL scheme failed to open, then we'll just open the file normally\n\t\t\ttoExecute = fullPath\n\t\t}\n\t} else if toExecute == \"\" {\n\t\ttoExecute = fullPath\n\t}\n\n\tvar cmd *exec.Cmd\n\n\t//goland:noinspection GoBoolExpressions\n\tif editor.UsesShell {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\t// nosemgrep\n\t\t\tcmd = exec.Command(\"cmd.exe\", \"/c\", editor.Path, toExecute)\n\t\t} else {\n\t\t\t// nosemgrep\n\t\t\tcmd = exec.Command(\"sh\", \"-c\", editor.Path+\" \"+toExecute)\n\t\t}\n\t} else if runtime.GOOS == \"darwin\" {\n\t\t// nosemgrep\n\t\tcmd = exec.Command(\"open\", \"-a\", editor.Path, toExecute)\n\t} else {\n\t\t// nosemgrep\n\t\tcmd = exec.Command(editor.Path, toExecute)\n\t}\n\n\t// Make sure the editor processes are detached from the Encore daemon.\n\t// Otherwise, some editors (like Notepad++) will be killed when the\n\t// Encore daemon shutsdown.\n\tcmd.SysProcAttr = xos.CreateNewProcessGroup()\n\n\tlog.Info().Str(\"full_path\", fullPath).Str(\"editor\", string(editor.Editor)).Str(\"cmd\", cmd.String()).Msg(\"attempting to open file\")\n\n\treturn errors.Wrap(cmd.Start(), \"failed to start editor\")\n}\n"
  },
  {
    "path": "pkg/editors/lookup.go",
    "content": "package editors\n\nimport (\n\t\"context\"\n\tgoerrors \"errors\"\n\t\"sort\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"go4.org/syncutil\"\n)\n\n// FoundEditor is a found external editor on the user's machine\ntype FoundEditor struct {\n\t// The friendly name of the editor, to be used in labels\n\tEditor EditorName `json:\"editor\"`\n\t// The executable associated with the editor to launch\n\tPath string `json:\"path\"`\n\t// The editor requires a shell to spawn\n\tUsesShell bool `json:\"usesShell,omitempty\"`\n}\n\nvar (\n\teditorCacheOnce syncutil.Once\n\t// editorCache is a cache of the available editors on the user's machine\n\teditorCache []FoundEditor\n\n\t// ErrEditorNotFound is returned when an editor is not found when called from\n\t// the Find function\n\tErrEditorNotFound = goerrors.New(\"editor not found\")\n)\n\n// Resolve a list of installed editors on the user's machine, using the known\n// install identifiers that each OS supports.\nfunc Resolve(ctx context.Context) ([]FoundEditor, error) {\n\terr := editorCacheOnce.Do(func() error {\n\t\tvar err error\n\t\teditorCache, err = getAvailableEditors(ctx)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"unable to get available editors\")\n\t\t}\n\n\t\tsort.Slice(editorCache, func(i, j int) bool {\n\t\t\treturn editorCache[i].Editor < editorCache[j].Editor\n\t\t})\n\t\treturn nil\n\t})\n\n\treturn editorCache, err\n}\n\n// Find searches to an editor by name, returning the editor if found, or\n// [ErrEditorNotFound] if not found\nfunc Find(ctx context.Context, name EditorName) (FoundEditor, error) {\n\teditors, err := Resolve(ctx)\n\tif err != nil {\n\t\treturn FoundEditor{}, err\n\t}\n\n\tfor _, editor := range editors {\n\t\tif editor.Editor == name {\n\t\t\treturn editor, nil\n\t\t}\n\t}\n\n\treturn FoundEditor{}, errors.WithStack(ErrEditorNotFound)\n}\n"
  },
  {
    "path": "pkg/editors/lookup_darwin.go",
    "content": "//go:build darwin\n\npackage editors\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"golang.org/x/sync/errgroup\"\n\texec \"golang.org/x/sys/execabs\"\n)\n\n// DarwinExternalEditor represents an external editor on macOS\ntype DarwinExternalEditor struct {\n\t// Name of the editor. It will be used both as identifier and user-facing.\n\tName EditorName\n\n\t// List of bundle identifiers that are used by the app in its multiple versions.\n\tBundleIdentifiers []string\n}\n\n// This list contains all the external editors supported on macOS. Add a new\n// entry here to add support for your favorite editor.\nvar editors = []DarwinExternalEditor{\n\t{\n\t\tName:              Atom,\n\t\tBundleIdentifiers: []string{\"com.github.atom\"},\n\t},\n\t{\n\t\tName:              AptanaStudio,\n\t\tBundleIdentifiers: []string{\"aptana.studio\"},\n\t},\n\t{\n\t\tName:              MacVim,\n\t\tBundleIdentifiers: []string{\"org.vim.MacVim\"},\n\t},\n\t{\n\t\tName:              Neovide,\n\t\tBundleIdentifiers: []string{\"com.neovide.neovide\"},\n\t},\n\t{\n\t\tName:              VimR,\n\t\tBundleIdentifiers: []string{\"com.qvacua.VimR\"},\n\t},\n\t{\n\t\tName:              VSCode,\n\t\tBundleIdentifiers: []string{\"com.microsoft.VSCode\"},\n\t},\n\t{\n\t\tName:              VSCodeInsiders,\n\t\tBundleIdentifiers: []string{\"com.microsoft.VSCodeInsiders\"},\n\t},\n\t{\n\t\tName:              VSCodium,\n\t\tBundleIdentifiers: []string{\"com.visualstudio.code.oss\", \"com.vscodium\"},\n\t},\n\t{\n\t\tName: SublimeText,\n\t\tBundleIdentifiers: []string{\n\t\t\t\"com.sublimetext.4\",\n\t\t\t\"com.sublimetext.3\",\n\t\t\t\"com.sublimetext.2\",\n\t\t},\n\t},\n\t{\n\t\tName:              BBEdit,\n\t\tBundleIdentifiers: []string{\"com.barebones.bbedit\"},\n\t},\n\t{\n\t\tName:              JetbrainsPhpStorm,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.PhpStorm\"},\n\t},\n\t{\n\t\tName:              JetbrainsPyCharm,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.PyCharm\"},\n\t},\n\t{\n\t\tName:              JetbrainsPyCharmCE,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.pycharm.ce\"},\n\t},\n\t{\n\t\tName:              JetbrainsDataSpell,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.DataSpell\"},\n\t},\n\t{\n\t\tName:              JetbrainsRubyMine,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.RubyMine\"},\n\t},\n\t{\n\t\tName:              RStudio,\n\t\tBundleIdentifiers: []string{\"org.rstudio.RStudio\", \"com.rstudio.desktop\"},\n\t},\n\t{\n\t\tName:              TextMate,\n\t\tBundleIdentifiers: []string{\"com.macromates.TextMate\"},\n\t},\n\t{\n\t\tName:              Brackets,\n\t\tBundleIdentifiers: []string{\"io.brackets.appshell\"},\n\t},\n\t{\n\t\tName:              JetbrainsWebStorm,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.WebStorm\"},\n\t},\n\t{\n\t\tName:              JetbrainsCLion,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.CLion\"},\n\t},\n\t{\n\t\tName:              Typora,\n\t\tBundleIdentifiers: []string{\"abnerworks.Typora\"},\n\t},\n\t{\n\t\tName:              CodeRunner,\n\t\tBundleIdentifiers: []string{\"com.krill.CodeRunner\"},\n\t},\n\t{\n\t\tName: SlickEdit,\n\t\tBundleIdentifiers: []string{\n\t\t\t\"com.slickedit.SlickEditPro2018\",\n\t\t\t\"com.slickedit.SlickEditPro2017\",\n\t\t\t\"com.slickedit.SlickEditPro2016\",\n\t\t\t\"com.slickedit.SlickEditPro2015\",\n\t\t},\n\t},\n\t{\n\t\tName:              JetbrainsIntelliJ,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.intellij\"},\n\t},\n\t{\n\t\tName:              JetbrainsIntelliJCE,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.intellij.ce\"},\n\t},\n\t{\n\t\tName:              XCode,\n\t\tBundleIdentifiers: []string{\"com.apple.dt.Xcode\"},\n\t},\n\t{\n\t\tName:              JetbrainsGoLand,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.goland\"},\n\t},\n\t{\n\t\tName:              AndroidStudio,\n\t\tBundleIdentifiers: []string{\"com.google.android.studio\"},\n\t},\n\t{\n\t\tName:              JetbrainsRider,\n\t\tBundleIdentifiers: []string{\"com.jetbrains.rider\"},\n\t},\n\t{\n\t\tName:              Nova,\n\t\tBundleIdentifiers: []string{\"com.panic.Nova\"},\n\t},\n\t{\n\t\tName:              Emacs,\n\t\tBundleIdentifiers: []string{\"org.gnu.Emacs\"},\n\t},\n\t{\n\t\tName:              LiteXL,\n\t\tBundleIdentifiers: []string{\"com.lite-xl\"},\n\t},\n\t{\n\t\tName:              JetbrainsFleet,\n\t\tBundleIdentifiers: []string{\"Fleet.app\"},\n\t},\n\t{\n\t\tName:              Pulsar,\n\t\tBundleIdentifiers: []string{\"dev.pulsar-edit.pulsar\"},\n\t},\n\t{\n\t\tName:              Zed,\n\t\tBundleIdentifiers: []string{\"dev.zed.Zed\"},\n\t},\n\t{\n\t\tName:              ZedPreview,\n\t\tBundleIdentifiers: []string{\"dev.zed.Zed-Preview\"},\n\t},\n\t{\n\t\tName:              Cursor,\n\t\tBundleIdentifiers: []string{\"com.todesktop.230313mzl4w4u92\"},\n\t},\n}\n\nfunc findApplication(ctx context.Context, editor DarwinExternalEditor, foundEditors chan FoundEditor) error {\n\tfor _, bundleIdentifier := range editor.BundleIdentifiers {\n\t\tpath, err := getAppLocationByBundleID(ctx, bundleIdentifier)\n\n\t\tswitch {\n\t\tcase err != nil:\n\t\t\treturn errors.WithStack(err)\n\n\t\tcase path != \"\":\n\t\t\tfoundEditors <- FoundEditor{\n\t\t\t\tEditor: editor.Name,\n\t\t\t\tPath:   path,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getAppLocationByBundleID returns the location of the app with the given bundle identifier.\nfunc getAppLocationByBundleID(ctx context.Context, bundleID string) (string, error) {\n\tcmd := exec.CommandContext(ctx, \"mdfind\", \"kMDItemCFBundleIdentifier == '\"+bundleID+\"'\")\n\tvar out bytes.Buffer\n\tcmd.Stdout = &out\n\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// mdfind can return multiple results, so we'll just take the first one.\n\tresults := strings.Split(out.String(), \"\\n\")\n\tif len(results) > 0 {\n\t\treturn results[0], nil\n\t}\n\n\treturn \"\", nil\n}\n\n// Resolve a list of installed editors on the user's machine, using the known\n// install identifiers that each OS supports.\nfunc getAvailableEditors(ctx context.Context) ([]FoundEditor, error) {\n\tresults := make([]FoundEditor, 0)\n\n\tgrp, ctx := errgroup.WithContext(ctx)\n\n\tfoundEditors := make(chan FoundEditor)\n\terrs := make(chan error, 1)\n\tfor _, editor := range editors {\n\t\teditor := editor\n\t\tgrp.Go(func() error {\n\t\t\treturn findApplication(ctx, editor, foundEditors)\n\t\t})\n\t}\n\n\tgo func() {\n\t\terrs <- grp.Wait()\n\t\tclose(foundEditors)\n\t}()\n\n\t// Collect results and the error from the group\n\tfor editor := range foundEditors {\n\t\tresults = append(results, editor)\n\t}\n\tif err := <-errs; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "pkg/editors/lookup_linux.go",
    "content": "//go:build linux\n\npackage editors\n\nimport (\n\t\"context\"\n)\n\n// LinuxExternalEditor represents an external editor on Linux\ntype LinuxExternalEditor struct {\n\t// Name of the editor. It will be used both as identifier and user-facing.\n\tName EditorName\n\n\t// List of possible paths where the editor's executable might be located.\n\tPaths []string\n}\n\n// This list contains all the external editors supported on Linux. Add a new\n// entry here to add support for your favorite editor.\nvar editors = []LinuxExternalEditor{\n\t{\n\t\tName:  Atom,\n\t\tPaths: []string{\"/snap/bin/atom\", \"/usr/bin/atom\"},\n\t},\n\t{\n\t\tName:  Neovim,\n\t\tPaths: []string{\"/usr/bin/nvim\"},\n\t},\n\t{\n\t\tName:  NeovimQt,\n\t\tPaths: []string{\"/usr/bin/nvim-qt\"},\n\t},\n\t{\n\t\tName:  Neovide,\n\t\tPaths: []string{\"/usr/bin/neovide\"},\n\t},\n\t{\n\t\tName:  GVim,\n\t\tPaths: []string{\"/usr/bin/gvim\"},\n\t},\n\t{\n\t\tName: VSCode,\n\t\tPaths: []string{\n\t\t\t\"/usr/share/code/bin/code\",\n\t\t\t\"/snap/bin/code\",\n\t\t\t\"/usr/bin/code\",\n\t\t\t\"/mnt/c/Program Files/Microsoft VS Code/bin/code\",\n\t\t},\n\t},\n\t{\n\t\tName:  VSCodeInsiders,\n\t\tPaths: []string{\"/snap/bin/code-insiders\", \"/usr/bin/code-insiders\"},\n\t},\n\t{\n\t\tName: VSCodium,\n\t\tPaths: []string{\n\t\t\t\"/usr/bin/codium\",\n\t\t\t\"/var/lib/flatpak/app/com.vscodium.codium\",\n\t\t\t\"/usr/share/vscodium-bin/bin/codium\",\n\t\t},\n\t},\n\t{\n\t\tName:  SublimeText,\n\t\tPaths: []string{\"/usr/bin/subl\"},\n\t},\n\t{\n\t\tName:  Typora,\n\t\tPaths: []string{\"/usr/bin/typora\"},\n\t},\n\t{\n\t\tName: SlickEdit,\n\t\tPaths: []string{\n\t\t\t\"/opt/slickedit-pro2018/bin/vs\",\n\t\t\t\"/opt/slickedit-pro2017/bin/vs\",\n\t\t\t\"/opt/slickedit-pro2016/bin/vs\",\n\t\t\t\"/opt/slickedit-pro2015/bin/vs\",\n\t\t},\n\t},\n\t{\n\t\t// Code editor for elementary OS\n\t\t// https://github.com/elementary/code\n\t\tName:  Code,\n\t\tPaths: []string{\"/usr/bin/io.elementary.code\"},\n\t},\n\t{\n\t\tName:  LiteXL,\n\t\tPaths: []string{\"/usr/bin/lite-xl\"},\n\t},\n\t{\n\t\tName: JetbrainsPhpStorm,\n\t\tPaths: []string{\n\t\t\t\"/snap/bin/phpstorm\",\n\t\t\t\".local/share/JetBrains/Toolbox/scripts/phpstorm\",\n\t\t},\n\t},\n\t{\n\t\tName: JetbrainsGoLand,\n\t\tPaths: []string{\n\t\t\t\"/snap/bin/goland\",\n\t\t\t\".local/share/JetBrains/Toolbox/scripts/goland\",\n\t\t},\n\t},\n\t{\n\t\tName: JetbrainsWebStorm,\n\t\tPaths: []string{\n\t\t\t\"/snap/bin/webstorm\",\n\t\t\t\".local/share/JetBrains/Toolbox/scripts/webstorm\",\n\t\t},\n\t},\n\t{\n\t\tName:  JetbrainsIntelliJ,\n\t\tPaths: []string{\"/snap/bin/idea\", \".local/share/JetBrains/Toolbox/scripts/idea\"},\n\t},\n\t{\n\t\tName: JetbrainsPyCharm,\n\t\tPaths: []string{\n\t\t\t\"/snap/bin/pycharm\",\n\t\t\t\".local/share/JetBrains/Toolbox/scripts/pycharm\",\n\t\t},\n\t},\n\t{\n\t\tName: Studio,\n\t\tPaths: []string{\n\t\t\t\"/snap/bin/studio\",\n\t\t\t\".local/share/JetBrains/Toolbox/scripts/studio\",\n\t\t},\n\t},\n\t{\n\t\tName:  Emacs,\n\t\tPaths: []string{\"/snap/bin/emacs\", \"/usr/local/bin/emacs\", \"/usr/bin/emacs\"},\n\t},\n\t{\n\t\tName:  Kate,\n\t\tPaths: []string{\"/usr/bin/kate\"},\n\t},\n\t{\n\t\tName:  GEdit,\n\t\tPaths: []string{\"/usr/bin/gedit\"},\n\t},\n\t{\n\t\tName:  GnomeTextEditor,\n\t\tPaths: []string{\"/usr/bin/gnome-text-editor\"},\n\t},\n\t{\n\t\tName:  GnomeBuilder,\n\t\tPaths: []string{\"/usr/bin/gnome-builder\"},\n\t},\n\t{\n\t\tName:  Notepadqq,\n\t\tPaths: []string{\"/usr/bin/notepadqq\"},\n\t},\n\t{\n\t\tName:  Geany,\n\t\tPaths: []string{\"/usr/bin/geany\"},\n\t},\n\t{\n\t\tName:  Mousepad,\n\t\tPaths: []string{\"/usr/bin/mousepad\"},\n\t},\n\t{\n\t\tName: Cursor,\n\t\tPaths: []string{\n\t\t\t\"/usr/bin/cursor\",\n\t\t\t\"/usr/share/cursor/cursor\",\n\t\t\t\"/snap/bin/cursor\",\n\t\t\t\"/opt/Cursor/cursor\",\n\t\t},\n\t},\n}\n\n// Returns the first available path from the provided list.\nfunc getAvailablePath(paths []string) string {\n\tfor _, path := range paths {\n\t\tif pathExists(path) {\n\t\t\treturn path\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// Returns a list of available editors with their paths.\nfunc getAvailableEditors(_ context.Context) ([]FoundEditor, error) {\n\tvar results []FoundEditor\n\tfor _, editor := range editors {\n\t\tpath := getAvailablePath(editor.Paths) // Assuming the editor struct has a Paths field\n\t\tif path != \"\" {\n\t\t\tresults = append(results, FoundEditor{Editor: editor.Name, Path: path})\n\t\t}\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "pkg/editors/lookup_test.go",
    "content": "package editors\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc TestResolve(t *testing.T) {\n\tc := qt.New(t)\n\n\teditors, err := Resolve(context.Background())\n\tc.Assert(err, qt.IsNil)\n\tfmt.Printf(\"Found editors:\\n%+v\\n\", editors)\n}\n"
  },
  {
    "path": "pkg/editors/lookup_unsupported.go",
    "content": "//go:build !darwin && !linux && !windows\n\npackage editors\n\nimport (\n\t\"context\"\n)\n\n// Returns no editors as we don't know how to find them on this platform.\nfunc getAvailableEditors(ctx context.Context) ([]FoundEditor, error) {\n\treturn []FoundEditor{}, nil\n}\n"
  },
  {
    "path": "pkg/editors/lookup_windows.go",
    "content": "//go:build windows\n\npackage editors\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/sys/windows/registry\"\n\n\t\"encr.dev/pkg/fns\"\n)\n\ntype WindowsExternalEditor struct {\n\tName                       EditorName\n\tRegistryKeys               []RegistryKey\n\tDisplayNamePrefixes        []string\n\tPublishers                 []string\n\tJetBrainsToolboxScriptName string\n\tInstallLocationRegistryKey string\n\tExecutableShimPaths        []string\n}\n\ntype RegistryKey struct {\n\tKey    registry.Key\n\tSubKey string\n}\n\ntype RegistryValue struct {\n\tName        string\n\tType        uint32\n\tStringValue string\n}\n\nvar editors = []WindowsExternalEditor{\n\t{\n\t\tName:                Atom,\n\t\tRegistryKeys:        []RegistryKey{CurrentUserUninstallKey(\"atom\")},\n\t\tExecutableShimPaths: []string{\"bin/atom.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"Atom\"},\n\t\tPublishers:          []string{\"GitHub Inc.\"},\n\t},\n\t{\n\t\tName:                AtomBeta,\n\t\tRegistryKeys:        []RegistryKey{CurrentUserUninstallKey(\"atom-beta\")},\n\t\tExecutableShimPaths: []string{\"bin/atom-beta.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"Atom Beta\"},\n\t\tPublishers:          []string{\"GitHub Inc.\"},\n\t},\n\t{\n\t\tName:                AtomNightly,\n\t\tRegistryKeys:        []RegistryKey{CurrentUserUninstallKey(\"atom-nightly\")},\n\t\tExecutableShimPaths: []string{\"bin/atom-nightly.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"Atom Nightly\"},\n\t\tPublishers:          []string{\"GitHub Inc.\"},\n\t},\n\t{\n\t\tName: VSCode,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of VSCode (user) - provided by default in 64-bit Windows\n\t\t\tCurrentUserUninstallKey(\"{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1\"),\n\t\t\t// 32-bit version of VSCode (user)\n\t\t\tCurrentUserUninstallKey(\"{D628A17A-9713-46BF-8D57-E671B46A741E}_is1\"),\n\t\t\t// ARM64 version of VSCode (user)\n\t\t\tCurrentUserUninstallKey(\"{D9E514E7-1A56-452D-9337-2990C0DC4310}_is1\"),\n\t\t\t// 64-bit version of VSCode (system) - was default before user scope installation\n\t\t\tLocalMachineUninstallKey(\"{EA457B21-F73E-494C-ACAB-524FDE069978}_is1\"),\n\t\t\t// 32-bit version of VSCode (system)\n\t\t\tWow64LocalMachineUninstallKey(\"{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1\"),\n\t\t\t// ARM64 version of VSCode (system)\n\t\t\tLocalMachineUninstallKey(\"{A5270FC5-65AD-483E-AC30-2C276B63D0AC}_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"bin/code.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"Microsoft Visual Studio Code\"},\n\t\tPublishers:          []string{\"Microsoft Corporation\"},\n\t},\n\t{\n\t\tName: VSCodeInsiders,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of VSCode (user) - provided by default in 64-bit Windows\n\t\t\tCurrentUserUninstallKey(\"{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1\"),\n\t\t\t// 32-bit version of VSCode (user)\n\t\t\tCurrentUserUninstallKey(\"{26F4A15E-E392-4887-8C09-7BC55712FD5B}_is1\"),\n\t\t\t// ARM64 version of VSCode (user)\n\t\t\tCurrentUserUninstallKey(\"{69BD8F7B-65EB-4C6F-A14E-44CFA83712C0}_is1\"),\n\t\t\t// 64-bit version of VSCode (system) - was default before user scope installation\n\t\t\tLocalMachineUninstallKey(\"{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1\"),\n\t\t\t// 32-bit version of VSCode (system)\n\t\t\tWow64LocalMachineUninstallKey(\"{C26E74D1-022E-4238-8B9D-1E7564A36CC9}_is1\"),\n\t\t\t// ARM64 version of VSCode (system)\n\t\t\tLocalMachineUninstallKey(\"{0AEDB616-9614-463B-97D7-119DD86CCA64}_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"bin/code-insiders.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"Microsoft Visual Studio Code Insiders\"},\n\t\tPublishers:          []string{\"Microsoft Corporation\"},\n\t},\n\t{\n\t\tName: VSCodium,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of VSCodium (user)\n\t\t\tCurrentUserUninstallKey(\"{2E1F05D1-C245-4562-81EE-28188DB6FD17}_is1\"),\n\t\t\t// 32-bit version of VSCodium (user) - new key\n\t\t\tCurrentUserUninstallKey(\"{0FD05EB4-651E-4E78-A062-515204B47A3A}_is1\"),\n\t\t\t// ARM64 version of VSCodium (user) - new key\n\t\t\tCurrentUserUninstallKey(\"{57FD70A5-1B8D-4875-9F40-C5553F094828}_is1\"),\n\t\t\t// 64-bit version of VSCodium (system) - new key\n\t\t\tLocalMachineUninstallKey(\"{88DA3577-054F-4CA1-8122-7D820494CFFB}_is1\"),\n\t\t\t// 32-bit version of VSCodium (system) - new key\n\t\t\tWow64LocalMachineUninstallKey(\"{763CBF88-25C6-4B10-952F-326AE657F16B}_is1\"),\n\t\t\t// ARM64 version of VSCodium (system) - new key\n\t\t\tLocalMachineUninstallKey(\"{67DEE444-3D04-4258-B92A-BC1F0FF2CAE4}_is1\"),\n\t\t\t// 32-bit version of VSCodium (user) - old key\n\t\t\tCurrentUserUninstallKey(\"{C6065F05-9603-4FC4-8101-B9781A25D88E}}_is1\"),\n\t\t\t// ARM64 version of VSCodium (user) - old key\n\t\t\tCurrentUserUninstallKey(\"{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}_is1\"),\n\t\t\t// 64-bit version of VSCodium (system) - old key\n\t\t\tLocalMachineUninstallKey(\"{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}_is1\"),\n\t\t\t// 32-bit version of VSCodium (system) - old key\n\t\t\tWow64LocalMachineUninstallKey(\"{E34003BB-9E10-4501-8C11-BE3FAA83F23F}_is1\"),\n\t\t\t// ARM64 version of VSCodium (system) - old key\n\t\t\tLocalMachineUninstallKey(\"{D1ACE434-89C5-48D1-88D3-E2991DF85475}_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"bin/codium.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"VSCodium\"},\n\t\tPublishers:          []string{\"VSCodium\", \"Microsoft Corporation\"},\n\t},\n\t{\n\t\tName: VSCodiumInsiders,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of VSCodium - Insiders (user)\n\t\t\tCurrentUserUninstallKey(\"{20F79D0D-A9AC-4220-9A81-CE675FFB6B41}_is1\"),\n\t\t\t// 32-bit version of VSCodium - Insiders (user)\n\t\t\tCurrentUserUninstallKey(\"{ED2E5618-3E7E-4888-BF3C-A6CCC84F586F}_is1\"),\n\t\t\t// ARM64 version of VSCodium - Insiders (user)\n\t\t\tCurrentUserUninstallKey(\"{2E362F92-14EA-455A-9ABD-3E656BBBFE71}_is1\"),\n\t\t\t// 64-bit version of VSCodium - Insiders (system)\n\t\t\tLocalMachineUninstallKey(\"{B2E0DDB2-120E-4D34-9F7E-8C688FF839A2}_is1\"),\n\t\t\t// 32-bit version of VSCodium - Insiders (system)\n\t\t\tWow64LocalMachineUninstallKey(\"{EF35BB36-FA7E-4BB9-B7DA-D1E09F2DA9C9}_is1\"),\n\t\t\t// ARM64 version of VSCodium - Insiders (system)\n\t\t\tLocalMachineUninstallKey(\"{44721278-64C6-4513-BC45-D48E07830599}_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"bin/codium-insiders.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"VSCodium Insiders\", \"VSCodium (Insiders)\"},\n\t\tPublishers:          []string{\"VSCodium\"},\n\t},\n\t{\n\t\tName: SublimeText,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// Sublime Text 4 (and newer?)\n\t\t\tLocalMachineUninstallKey(\"Sublime Text_is1\"),\n\t\t\t// Sublime Text 3\n\t\t\tLocalMachineUninstallKey(\"Sublime Text 3_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"subl.exe\"},\n\t\tDisplayNamePrefixes: []string{\"Sublime Text\"},\n\t\tPublishers:          []string{\"Sublime HQ Pty Ltd\"},\n\t},\n\t{\n\t\tName: Brackets,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\tWow64LocalMachineUninstallKey(\"{4F3B6E8C-401B-4EDE-A423-6481C239D6FF}\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"Brackets.exe\"},\n\t\tDisplayNamePrefixes: []string{\"Brackets\"},\n\t\tPublishers:          []string{\"brackets.io\"},\n\t},\n\t{\n\t\tName: ColdFusionBuilder,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of ColdFusionBuilder3\n\t\t\tLocalMachineUninstallKey(\"Adobe ColdFusion Builder 3_is1\"),\n\t\t\t// 64-bit version of ColdFusionBuilder2016\n\t\t\tLocalMachineUninstallKey(\"Adobe ColdFusion Builder 2016\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"CFBuilder.exe\"},\n\t\tDisplayNamePrefixes: []string{\"Adobe ColdFusion Builder\"},\n\t\tPublishers:          []string{\"Adobe Systems Incorporated\"},\n\t},\n\t{\n\t\tName: Typora,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of Typora\n\t\t\tLocalMachineUninstallKey(\"{37771A20-7167-44C0-B322-FD3E54C56156}_is1\"),\n\t\t\t// 32-bit version of Typora\n\t\t\tWow64LocalMachineUninstallKey(\"{37771A20-7167-44C0-B322-FD3E54C56156}_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"typora.exe\"},\n\t\tDisplayNamePrefixes: []string{\"Typora\"},\n\t\tPublishers:          []string{\"typora.io\"},\n\t},\n\t{\n\t\tName: SlickEdit,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of SlickEdit Pro 2018\n\t\t\tLocalMachineUninstallKey(\"{18406187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 32-bit version of SlickEdit Pro 2018\n\t\t\tWow64LocalMachineUninstallKey(\"{18006187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Standard 2018\n\t\t\tLocalMachineUninstallKey(\"{18606187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 32-bit version of SlickEdit Standard 2018\n\t\t\tWow64LocalMachineUninstallKey(\"{18206187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Pro 2017\n\t\t\tLocalMachineUninstallKey(\"{15406187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 32-bit version of SlickEdit Pro 2017\n\t\t\tWow64LocalMachineUninstallKey(\"{15006187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Pro 2016 (21.0.1)\n\t\t\tLocalMachineUninstallKey(\"{10C06187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Pro 2016 (21.0.0)\n\t\t\tLocalMachineUninstallKey(\"{10406187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Pro 2015 (20.0.3)\n\t\t\tLocalMachineUninstallKey(\"{0DC06187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Pro 2015 (20.0.2)\n\t\t\tLocalMachineUninstallKey(\"{0D406187-F49E-4822-CAF2-1D25C0C83BA2}\"),\n\t\t\t// 64-bit version of SlickEdit Pro 2014 (19.0.2)\n\t\t\tLocalMachineUninstallKey(\"{7CC0E567-ACD6-41E8-95DA-154CEEDB0A18}\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"win/vs.exe\"},\n\t\tDisplayNamePrefixes: []string{\"SlickEdit\"},\n\t\tPublishers:          []string{\"SlickEdit Inc.\"},\n\t},\n\t{\n\t\tName: AptanaStudio,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\tWow64LocalMachineUninstallKey(\"{2D6C1116-78C6-469C-9923-3E549218773F}\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"AptanaStudio3.exe\"},\n\t\tDisplayNamePrefixes: []string{\"Aptana Studio\"},\n\t\tPublishers:          []string{\"Appcelerator\"},\n\t},\n\t{\n\t\tName:                       JetbrainsWebStorm,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"WebStorm\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"webstorm\"),\n\t\tJetBrainsToolboxScriptName: \"webstorm\",\n\t\tDisplayNamePrefixes:        []string{\"WebStorm\"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsPhpStorm,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"PhpStorm\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"phpstorm\"),\n\t\tJetBrainsToolboxScriptName: \"phpstorm\",\n\t\tDisplayNamePrefixes:        []string{\"PhpStorm\"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       AndroidStudio,\n\t\tRegistryKeys:               []RegistryKey{LocalMachineUninstallKey(\"Android Studio\")},\n\t\tInstallLocationRegistryKey: \"UninstallString\",\n\t\tJetBrainsToolboxScriptName: \"studio\",\n\t\tExecutableShimPaths: []string{\n\t\t\t\"../bin/studio64.exe\",\n\t\t\t\"../bin/studio.exe\",\n\t\t},\n\t\tDisplayNamePrefixes: []string{\"Android Studio\"},\n\t\tPublishers:          []string{\"Google LLC\"},\n\t},\n\t{\n\t\tName: NotePadPlusPlus,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of Notepad++\n\t\t\tLocalMachineUninstallKey(\"Notepad++\"),\n\t\t\t// 32-bit version of Notepad++\n\t\t\tWow64LocalMachineUninstallKey(\"Notepad++\"),\n\t\t},\n\t\tInstallLocationRegistryKey: \"DisplayIcon\",\n\t\tDisplayNamePrefixes:        []string{\"Notepad++\"},\n\t\tPublishers:                 []string{\"Notepad++ Team\"},\n\t},\n\t{\n\t\tName:                       JetbrainsRider,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"JetBrains Rider\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"rider\"),\n\t\tJetBrainsToolboxScriptName: \"rider\",\n\t\tDisplayNamePrefixes:        []string{\"JetBrains Rider\"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       RStudio,\n\t\tRegistryKeys:               []RegistryKey{Wow64LocalMachineUninstallKey(\"RStudio\")},\n\t\tInstallLocationRegistryKey: \"DisplayIcon\",\n\t\tDisplayNamePrefixes:        []string{\"RStudio\"},\n\t\tPublishers:                 []string{\"RStudio\", \"Posit Software\"},\n\t},\n\t{\n\t\tName:                       JetbrainsIntelliJ,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"IntelliJ IDEA\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"idea\"),\n\t\tJetBrainsToolboxScriptName: \"idea\",\n\t\tDisplayNamePrefixes:        []string{\"IntelliJ IDEA \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                JetbrainsIntelliJCE,\n\t\tRegistryKeys:        registryKeysForJetBrainsIDE(\"IntelliJ IDEA Community Edition\"),\n\t\tExecutableShimPaths: executableShimPathsForJetBrainsIDE(\"idea\"),\n\t\tDisplayNamePrefixes: []string{\"IntelliJ IDEA Community Edition \"},\n\t\tPublishers:          []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsPyCharm,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"PyCharm\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"pycharm\"),\n\t\tJetBrainsToolboxScriptName: \"pycharm\",\n\t\tDisplayNamePrefixes:        []string{\"PyCharm \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                JetbrainsPyCharmCE,\n\t\tRegistryKeys:        registryKeysForJetBrainsIDE(\"PyCharm Community Edition\"),\n\t\tExecutableShimPaths: executableShimPathsForJetBrainsIDE(\"pycharm\"),\n\t\tDisplayNamePrefixes: []string{\"PyCharm Community Edition\"},\n\t\tPublishers:          []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsCLion,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"CLion\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"clion\"),\n\t\tJetBrainsToolboxScriptName: \"clion\",\n\t\tDisplayNamePrefixes:        []string{\"CLion \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsRubyMine,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"RubyMine\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"rubymine\"),\n\t\tJetBrainsToolboxScriptName: \"rubymine\",\n\t\tDisplayNamePrefixes:        []string{\"RubyMine \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsGoLand,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"GoLand\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"goland\"),\n\t\tJetBrainsToolboxScriptName: \"goland\",\n\t\tDisplayNamePrefixes:        []string{\"GoLand \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsFleet,\n\t\tRegistryKeys:               []RegistryKey{LocalMachineUninstallKey(\"Fleet\")},\n\t\tJetBrainsToolboxScriptName: \"fleet\",\n\t\tInstallLocationRegistryKey: \"DisplayIcon\",\n\t\tDisplayNamePrefixes:        []string{\"Fleet \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName:                       JetbrainsDataSpell,\n\t\tRegistryKeys:               registryKeysForJetBrainsIDE(\"DataSpell\"),\n\t\tExecutableShimPaths:        executableShimPathsForJetBrainsIDE(\"dataspell\"),\n\t\tJetBrainsToolboxScriptName: \"dataspell\",\n\t\tDisplayNamePrefixes:        []string{\"DataSpell \"},\n\t\tPublishers:                 []string{\"JetBrains s.r.o.\"},\n\t},\n\t{\n\t\tName: Pulsar,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\tCurrentUserUninstallKey(\"0949b555-c22c-56b7-873a-a960bdefa81f\"),\n\t\t\tLocalMachineUninstallKey(\"0949b555-c22c-56b7-873a-a960bdefa81f\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"../pulsar/Pulsar.exe\"},\n\t\tDisplayNamePrefixes: []string{\"Pulsar\"},\n\t\tPublishers:          []string{\"Pulsar-Edit\"},\n\t},\n\t{\n\t\tName: Cursor,\n\t\tRegistryKeys: []RegistryKey{\n\t\t\t// 64-bit version of Cursor (user)\n\t\t\tCurrentUserUninstallKey(\"{4F4FB13E-18F1-5261-B8EB-6E696B5F2C7E}_is1\"),\n\t\t},\n\t\tExecutableShimPaths: []string{\"bin/cursor.cmd\"},\n\t\tDisplayNamePrefixes: []string{\"Cursor\"},\n\t\tPublishers:          []string{\"Anysphere Inc.\", \"Anysphere\"},\n\t},\n}\n\nfunc registryKey(key registry.Key, subKey string) RegistryKey {\n\treturn RegistryKey{Key: key, SubKey: subKey}\n}\n\nfunc CurrentUserUninstallKey(subKey string) RegistryKey {\n\treturn registryKey(registry.CURRENT_USER, \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\"+subKey)\n}\n\nfunc LocalMachineUninstallKey(subKey string) RegistryKey {\n\treturn registryKey(registry.LOCAL_MACHINE, \"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\"+subKey)\n}\n\nfunc Wow64LocalMachineUninstallKey(subKey string) RegistryKey {\n\treturn registryKey(registry.LOCAL_MACHINE, \"Software\\\\Wow6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\\"+subKey)\n}\n\n// This function generates registry keys for a given JetBrains product for the\n// last 2 years, assuming JetBrains make no more than 5 major releases and\n// no more than 5 minor releases per year.\nfunc registryKeysForJetBrainsIDE(product string) []RegistryKey {\n\tconst maxMajorReleasesPerYear = 5\n\tconst maxMinorReleasesPerYear = 5\n\tvar lastYear = time.Now().Year()\n\tvar firstYear = lastYear - 2\n\n\tvar result []RegistryKey\n\n\tfor year := firstYear; year <= lastYear; year++ {\n\t\tfor majorRelease := 1; majorRelease <= maxMajorReleasesPerYear; majorRelease++ {\n\t\t\tfor minorRelease := 0; minorRelease <= maxMinorReleasesPerYear; minorRelease++ {\n\t\t\t\tkey := fmt.Sprintf(\"%s %d.%d\", product, year, majorRelease)\n\t\t\t\tif minorRelease > 0 {\n\t\t\t\t\tkey = fmt.Sprintf(\"%s.%d\", key, minorRelease)\n\t\t\t\t}\n\n\t\t\t\tresult = append(result,\n\t\t\t\t\tWow64LocalMachineUninstallKey(key),\n\t\t\t\t\tCurrentUserUninstallKey(key),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return in reverse order to prioritize newer versions\n\tslices.Reverse(result)\n\treturn result\n}\n\n// JetBrains IDE's might have 64 and/or 32 bit executables, so let's add both\nfunc executableShimPathsForJetBrainsIDE(baseName string) []string {\n\treturn []string{\n\t\tfmt.Sprintf(\"bin/%s64.exe\", baseName),\n\t\tfmt.Sprintf(\"bin/%s.exe\", baseName),\n\t}\n}\n\nfunc getKeyOrEmpty(keys []RegistryValue, valueName string) string {\n\tfor _, key := range keys {\n\t\tif key.Name == valueName {\n\t\t\tif key.Type == registry.SZ {\n\t\t\t\treturn key.StringValue\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc getAppInfo(editor WindowsExternalEditor, keys []RegistryValue) (displayName, publisher, installLocation string) {\n\tdisplayName = getKeyOrEmpty(keys, \"DisplayName\")\n\tpublisher = getKeyOrEmpty(keys, \"Publisher\")\n\tloc := editor.InstallLocationRegistryKey\n\tif loc == \"\" {\n\t\tloc = \"InstallLocation\"\n\t}\n\tinstallLocation = getKeyOrEmpty(keys, loc)\n\treturn\n}\n\nfunc findApplication(ctx context.Context, editor WindowsExternalEditor, foundEditors chan FoundEditor) error {\n\tfor _, registryKey := range editor.RegistryKeys {\n\t\tvalues, err := enumerateValues(registryKey.Key, registryKey.SubKey)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to enumerate registry values\")\n\t\t}\n\t\tif len(values) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tdisplayName, publisher, installLocation := getAppInfo(editor, values)\n\n\t\tif !validateStartsWith(displayName, editor.DisplayNamePrefixes) ||\n\t\t\t!stringInSlice(publisher, editor.Publishers) {\n\t\t\tlog.Warn().Str(\"editor\", string(editor.Name)).Str(\"publisher\", publisher).Str(\"display_name\", displayName).Msg(\"unexpected registry entry\")\n\t\t\tcontinue\n\t\t}\n\n\t\tvar executableShimPaths []string\n\t\tif editor.InstallLocationRegistryKey == \"DisplayIcon\" {\n\t\t\texecutableShimPaths = []string{installLocation}\n\t\t} else {\n\t\t\tfor _, shim := range editor.ExecutableShimPaths {\n\t\t\t\texecutableShimPaths = append(executableShimPaths, filepath.Join(installLocation, shim))\n\t\t\t}\n\t\t}\n\n\t\tfor _, exePath := range executableShimPaths {\n\t\t\tif pathExists(exePath) {\n\t\t\t\tfoundEditors <- FoundEditor{\n\t\t\t\t\tEditor:    editor.Name,\n\t\t\t\t\tPath:      exePath,\n\t\t\t\t\tUsesShell: strings.HasSuffix(exePath, \".cmd\"),\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t} else {\n\t\t\t\tlog.Debug().Str(\"editor\", string(editor.Name)).Str(\"path\", exePath).Msg(\"executable not found\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn findJetBrainsToolboxApplication(ctx, editor, foundEditors)\n}\n\n// Find JetBrain products installed through JetBrains Toolbox\nfunc findJetBrainsToolboxApplication(_ context.Context, editor WindowsExternalEditor, foundEditors chan FoundEditor) error {\n\tif editor.JetBrainsToolboxScriptName == \"\" {\n\t\treturn nil\n\t}\n\n\tvar toolboxRegistryReference = []RegistryKey{\n\t\tCurrentUserUninstallKey(\"toolbox\"),\n\t\tWow64LocalMachineUninstallKey(\"toolbox\"),\n\t}\n\n\tfor _, registryKey := range toolboxRegistryReference {\n\t\tkeys, err := enumerateValues(registryKey.Key, registryKey.SubKey)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"failed to enumerate registry values\")\n\t\t}\n\n\t\tif len(keys) > 0 {\n\t\t\teditorPathInToolbox := path.Join(\n\t\t\t\tgetKeyOrEmpty(keys, \"UninstallString\"),\n\t\t\t\t\"..\",\n\t\t\t\t\"..\",\n\t\t\t\t\"scripts\",\n\t\t\t\tfmt.Sprintf(\"%s.cmd\", editor.JetBrainsToolboxScriptName),\n\t\t\t)\n\t\t\tif pathExists(editorPathInToolbox) {\n\t\t\t\tfoundEditors <- FoundEditor{\n\t\t\t\t\tEditor:    editor.Name,\n\t\t\t\t\tPath:      editorPathInToolbox,\n\t\t\t\t\tUsesShell: true,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc validateStartsWith(registryVal string, definedVal []string) bool {\n\tfor _, val := range definedVal {\n\t\tif strings.HasPrefix(registryVal, val) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc enumerateValues(key registry.Key, subKey string) ([]RegistryValue, error) {\n\tk, err := registry.OpenKey(key, subKey, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer fns.CloseIgnore(k)\n\n\tvalueNames, err := k.ReadValueNames(-1) // read all value names\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar values []RegistryValue\n\tfor _, valueName := range valueNames {\n\t\tvalue, valueType, err := k.GetStringValue(valueName)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, registry.ErrUnexpectedType) && !errors.Is(err, registry.ErrNotExist) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tvalues = append(values, RegistryValue{Name: valueName, Type: valueType, StringValue: value})\n\t\t}\n\t}\n\n\treturn values, nil\n}\n\nfunc stringInSlice(a string, list []string) bool {\n\tfor _, b := range list {\n\t\tif b == a {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Resolve a list of installed editors on the user's machine, using known\n// install and uninstall path registry keys\nfunc getAvailableEditors(ctx context.Context) ([]FoundEditor, error) {\n\tresults := make([]FoundEditor, 0)\n\n\tgrp, ctx := errgroup.WithContext(ctx)\n\n\tfoundEditors := make(chan FoundEditor)\n\terrs := make(chan error, 1)\n\tfor _, editor := range editors {\n\t\teditor := editor\n\t\tgrp.Go(func() error {\n\t\t\treturn findApplication(ctx, editor, foundEditors)\n\t\t})\n\t}\n\n\tgo func() {\n\t\terrs <- grp.Wait()\n\t\tclose(foundEditors)\n\t}()\n\n\t// Collect results and the error from the group\n\tfor editor := range foundEditors {\n\t\tresults = append(results, editor)\n\t}\n\tif err := <-errs; err != nil {\n\t\treturn nil, errors.WithStack(err)\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "pkg/editors/utils.go",
    "content": "package editors\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n)\n\n// Checks if the given path exists.\nfunc pathExists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn !errors.Is(err, fs.ErrNotExist)\n}\n"
  },
  {
    "path": "pkg/eerror/error.go",
    "content": "// Package eerror stands for Encore Error and is used to provide\n// a little more information about the underlying error's metadata.\n//\n// It also provides helper methods for working with zerolog's context\npackage eerror\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors/errbase\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/rs/zerolog\"\n)\n\ntype Error struct {\n\tModule  string         `json:\"module\"`  // The module the error was raised in (normally the package name, but could be a larger \"module\" name)\n\tMessage string         `json:\"message\"` // The message of the error, it should be human-readable and should be low entropy (i.e. so multiple errors of the same can be grouped)\n\tMeta    map[string]any `json:\"meta\"`    // Metadata about the error, this can be high entropy as it isn't used to group errors\n\tStack   []*StackFrame  `json:\"stack\"`   // The stack trace of the error\n\tcause   error          `json:\"-\"`       // The underlying error, this is not serialized\n}\n\nvar _ error = (*Error)(nil)\nvar _ errbase.StackTraceProvider = (*Error)(nil)\n\n// New creates a new error with the given error\nfunc New(module string, msg string, meta map[string]any) error {\n\treturn &Error{\n\t\tModule:  module,\n\t\tMessage: msg,\n\t\tMeta:    meta,\n\t\tStack:   getStack(),\n\t}\n}\n\n// Wrap wraps the cause error with the given message and meta.\n// If cause is nil, wrap returns nil\nfunc Wrap(cause error, module string, msg string, meta map[string]any) error {\n\tif cause == nil {\n\t\treturn nil\n\t}\n\treturn &Error{\n\t\tModule:  module,\n\t\tMessage: msg,\n\t\tMeta:    meta,\n\t\tStack:   getStack(),\n\t\tcause:   cause,\n\t}\n}\n\nfunc WithMeta(err error, meta map[string]any) error {\n\tloopErr := err\n\tfor loopErr != nil {\n\t\tif e, ok := loopErr.(*Error); ok {\n\t\t\tfor key, value := range meta {\n\t\t\t\te.Meta[key] = value\n\t\t\t}\n\t\t\treturn loopErr\n\t\t}\n\n\t\tswitch e := loopErr.(type) {\n\t\tcase interface{ Unwrap() error }:\n\t\t\tloopErr = e.Unwrap()\n\t\tcase interface{ Cause() error }:\n\t\t\tloopErr = e.Cause()\n\t\tdefault:\n\t\t\tloopErr = nil\n\t\t}\n\t}\n\n\t// if here we didn't find an *Error to add the metadata to, so we'll put on in\n\treturn &Error{\n\t\tModule:  \"\",\n\t\tMessage: err.Error(),\n\t\tMeta:    meta,\n\t\tStack:   getStack(),\n\t\tcause:   err,\n\t}\n}\n\n// Error returns a simple string of the error\nfunc (e *Error) Error() string {\n\tif e.cause != nil {\n\t\tcause := e.cause.Error()\n\n\t\t// Remove the module prefix if it's the same\n\t\tcause = strings.TrimPrefix(cause, \"[\"+e.Module+\"]: \")\n\n\t\treturn fmt.Sprintf(\"[%s]: %s: %s\", e.Module, e.Message, cause)\n\t}\n\treturn fmt.Sprintf(\"[%s]: %s\", e.Module, e.Message)\n}\n\n// Cause implements Causer for some libraries and returns the underlying cause\nfunc (e *Error) Cause() error {\n\treturn e.cause\n}\n\n// Unwrap implements the Go 2 unwrap interface used by xerrors and errors\nfunc (e *Error) Unwrap() error {\n\treturn e.cause\n}\n\n// StackTrace implements the StackTraceProvider interface for some libraries\n// including ZeroLog, xerrors and Sentry\nfunc (e *Error) StackTrace() errors.StackTrace {\n\tframes := make([]errors.Frame, len(e.Stack))\n\tfor i, frame := range e.Stack {\n\t\t// Note: for historic reasons the PC is off by 1 in github.com/pkg/errors\n\t\tframes[i] = errors.Frame(frame.PC + 1)\n\t}\n\treturn frames\n}\n\n// MarshalZerologObject provides a strongly-typed and encoding-agnostic interface\n// to be implemented by types used with Event/Context's Object methods.\nfunc (e *Error) MarshalZerologObject(evt *zerolog.Event) {\n\tLogWithMeta(evt, e)\n}\n\n// BottomStackTraceFrom returns the deepest stack trace from the given error\nfunc BottomStackTraceFrom(err error) (rtn errors.StackTrace) {\n\tcount := 0\n\n\tfor err != nil && count < 100 {\n\t\tcount++\n\n\t\t// If we're an error set our return data\n\t\tif e, ok := err.(interface{ StackTrace() errors.StackTrace }); ok {\n\t\t\trtn = e.StackTrace()\n\t\t}\n\n\t\t// Recurse\n\t\tswitch typed := err.(type) {\n\t\tcase interface{ Unwrap() error }:\n\t\t\terr = typed.Unwrap()\n\n\t\tcase interface{ Unwrap() []error }:\n\t\t\terrs := typed.Unwrap()\n\t\t\tif len(errs) > 0 {\n\t\t\t\terr = errs[0]\n\t\t\t} else {\n\t\t\t\terr = nil\n\t\t\t}\n\n\t\tcase interface{ Cause() error }:\n\t\t\terr = typed.Cause()\n\t\t}\n\t}\n\n\treturn\n}\n\n// MetaFrom will return the merged metadata from any eerror.Error objects in the errors\n// given. It will unwrap errors as it descends\nfunc MetaFrom(err error) map[string]any {\n\tmeta := make(map[string]any)\n\tmergeMeta(err, meta)\n\treturn meta\n}\n\nfunc mergeMeta(err error, meta map[string]any) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\t// Merge in the data from the deepest error first\n\tswitch err := err.(type) {\n\tcase interface{ Unwrap() error }:\n\t\tmergeMeta(err.Unwrap(), meta)\n\n\tcase interface{ Cause() error }:\n\t\tmergeMeta(err.Cause(), meta)\n\t}\n\n\t// Then merge in our data\n\tif e, ok := err.(*Error); ok {\n\t\tfor key, value := range e.Meta {\n\t\t\tmeta[key] = value\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "pkg/eerror/stack.go",
    "content": "package eerror\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n)\n\ntype StackFrame struct {\n\t// PC is the program counter for the frame (needed for things like sentry reporting)\n\tPC uintptr `json:\"pc\"`\n\n\t// Human-readable fields\n\tFunction string `json:\"function\"`\n\tFile     string `json:\"file\"`\n\tLine     int    `json:\"line\"`\n}\n\nvar projectSourcePath = getProjectSrcPath()\n\n// getProjectSrcPath returns the path to this repo on the local system\nfunc getProjectSrcPath() string {\n\t_, file, _, ok := runtime.Caller(0)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\teerrorPath := filepath.Dir(file)\n\tpkgPath := filepath.Dir(eerrorPath)\n\tencoreProjectPath := filepath.Dir(pkgPath)\n\n\treturn fmt.Sprintf(\"%s%c\", encoreProjectPath, os.PathSeparator)\n}\n\n// getStack returns a human read able stack trace\nfunc getStack() []*StackFrame {\n\tret := make([]uintptr, 100)\n\n\tindex := runtime.Callers(1, ret)\n\tif index == 0 {\n\t\treturn nil\n\t}\n\n\tcf := runtime.CallersFrames(ret[:index])\n\tframe, more := cf.Next()\n\n\t// Skip over the \"eerror\" package files\n\tfor strings.Contains(frame.File, \"eerror\") {\n\t\tif !more {\n\t\t\treturn nil\n\t\t}\n\n\t\tframe, more = cf.Next()\n\t}\n\n\tvar frames []*StackFrame\n\tfor {\n\t\tframes = append(frames, &StackFrame{\n\t\t\tPC:       frame.PC,\n\t\t\tFunction: strings.TrimPrefix(frame.Function, \"encr.dev/\"),\n\t\t\tFile:     strings.TrimPrefix(frame.File, projectSourcePath),\n\t\t\tLine:     frame.Line,\n\t\t})\n\n\t\tif !more {\n\t\t\treturn frames\n\t\t}\n\n\t\tframe, more = cf.Next()\n\t}\n}\n"
  },
  {
    "path": "pkg/eerror/zerolog.go",
    "content": "package eerror\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n)\n\ntype errorMeta struct {\n\tFrames []uintptr      `json:\"frames\"`\n\tMeta   map[string]any `json:\"meta\"`\n}\n\n// ZeroLogStackMarshaller will encode the following in the Zerolog error stack field:\n// - the deepest error stack trace\n// - the error meta data collected from all errors\n//\n// This can then be extracted by ZeroLogConsoleExtraFormatter\nfunc ZeroLogStackMarshaller(err error) interface{} {\n\tvar frames []uintptr\n\tfor _, frame := range BottomStackTraceFrom(err) {\n\t\t// Account for the stack trace being 1 frame deeper than the error\n\t\tframes = append(frames, uintptr(frame-1))\n\t}\n\tframes = filterFrames(frames)\n\n\treturn &errorMeta{\n\t\tFrames: frames,\n\t\tMeta:   MetaFrom(err),\n\t}\n}\n\n// ZeroLogConsoleExtraFormatter extracts the extra fields from the error and formats them for console output\n//\n// This field can be passed to a zerolog.ConsoleWriter as it's ExtraFieldFormatter\nfunc ZeroLogConsoleExtraFormatter(event map[string]any, buf *bytes.Buffer) error {\n\tstackFieldValue, found := event[zerolog.ErrorStackFieldName]\n\tif !found || stackFieldValue == nil {\n\t\treturn nil\n\t}\n\n\t// Marshal the stack field to our error meta\n\tvar errorMeta errorMeta\n\tjsonBytes, err := json.Marshal(stackFieldValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(jsonBytes, &errorMeta); err != nil {\n\t\treturn err\n\t}\n\n\t// Log any additional meta data\n\tif len(errorMeta.Meta) > 0 {\n\t\t// Get the fields not already set in the log, and then sort them\n\t\tfields := make([]string, 0, len(errorMeta.Meta))\n\t\tfor field := range errorMeta.Meta {\n\t\t\tif _, alreadySet := event[field]; !alreadySet {\n\t\t\t\tfields = append(fields, field)\n\t\t\t}\n\t\t}\n\t\tsort.Strings(fields)\n\n\t\t// Then log the additional fields\n\t\tfor _, field := range fields {\n\t\t\tif buf.Len() > 0 {\n\t\t\t\tbuf.WriteByte(' ')\n\t\t\t}\n\n\t\t\tfieldName := fmt.Sprintf(\"\\x1b[%dm%v\\x1b[0m=\", 36, field)\n\t\t\tbuf.WriteString(fieldName)\n\n\t\t\tswitch fValue := errorMeta.Meta[field].(type) {\n\t\t\tcase string:\n\t\t\t\tif needsQuote(fValue) {\n\t\t\t\t\tbuf.WriteString(strconv.Quote(fValue))\n\t\t\t\t} else {\n\t\t\t\t\tbuf.WriteString(fValue)\n\t\t\t\t}\n\t\t\tcase json.Number:\n\t\t\t\tbuf.WriteString(fValue.String())\n\t\t\tdefault:\n\t\t\t\tb, err := zerolog.InterfaceMarshalFunc(fValue)\n\t\t\t\tif err != nil {\n\t\t\t\t\tbuf.WriteString(fmt.Sprintf(\"[error: \\x1b[%dm%v\\x1b[0m=]\", 31, err))\n\t\t\t\t} else {\n\t\t\t\t\tbuf.WriteString(string(b))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now print a stack trace if we have it\n\tif len(errorMeta.Frames) > 0 {\n\t\t// Find the longest function name so we can align them\n\t\tlongestFunc := 0\n\t\tfor _, frame := range errorMeta.Frames {\n\t\t\tmodule, function := frameToModuleFunc(frame)\n\t\t\tfName := fmt.Sprintf(\"%s.%s\", module, function)\n\t\t\tif len(fName) > longestFunc {\n\t\t\t\tlongestFunc = len(fName)\n\t\t\t}\n\t\t}\n\n\t\t// Print the stack trace\n\t\tfor i, frame := range errorMeta.Frames {\n\t\t\tmodule, function := frameToModuleFunc(frame)\n\t\t\tfilename, line := frameToFileLine(frame)\n\n\t\t\tbuf.WriteString(fmt.Sprintf(\n\t\t\t\t\"\\n\\tat %s.%s%s %s:%d\",\n\t\t\t\tfmt.Sprintf(\"\\x1b[%dm%v\\x1b[0m\", 90, module),\n\t\t\t\tfmt.Sprintf(\"\\x1b[%dm%v\\x1b[0m\", 35, function),\n\t\t\t\tstrings.Repeat(\" \", longestFunc-(len(function)+len(module)+1)),\n\t\t\t\tfilename, line,\n\t\t\t))\n\n\t\t\tif i > 5 {\n\t\t\t\tbuf.WriteString(fmt.Sprintf(\"\\n\\t... remaining %d frames omitted ...\", len(errorMeta.Frames)-(i+1)))\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc frameToModuleFunc(frame uintptr) (module string, name string) {\n\tfn := runtime.FuncForPC(frame)\n\tif fn == nil {\n\t\treturn \"\", \"unknown\"\n\t}\n\n\tname = fn.Name()\n\tif idx := strings.LastIndex(name, \".\"); idx != -1 {\n\t\tmodule = name[:idx]\n\t\tname = name[idx+1:]\n\t}\n\n\tmodule = strings.TrimPrefix(module, \"encr.dev/\")\n\tname = strings.Replace(name, \"·\", \".\", -1)\n\treturn\n}\n\nfunc frameToFileLine(frame uintptr) (file string, line int) {\n\tfn := runtime.FuncForPC(frame)\n\tif fn == nil {\n\t\treturn \"unknown\", 0\n\t}\n\n\tfilename, line := fn.FileLine(frame)\n\tfilename = strings.TrimPrefix(filename, projectSourcePath)\n\n\treturn filename, line\n}\n\n// filterFrames filters out stack frames from zerolog and this package.\nfunc filterFrames(frames []uintptr) []uintptr {\n\tif len(frames) == 0 {\n\t\treturn nil\n\t}\n\tfilteredFrames := make([]uintptr, 0, len(frames))\n\n\tfor _, frame := range frames {\n\t\tmodule, _ := frameToModuleFunc(frame)\n\t\tif strings.HasPrefix(module, \"github.com/rs/zerolog\") {\n\t\t\tcontinue\n\t\t} else if strings.HasPrefix(module, \"github.com/spf13/cobra\") {\n\t\t\tcontinue\n\t\t}\n\t\tfilteredFrames = append(filteredFrames, frame)\n\t}\n\treturn filteredFrames\n}\n\n// needsQuote returns true when the string s should be quoted in output.\nfunc needsQuote(s string) bool {\n\tfor i := range s {\n\t\tif s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\\\' || s[i] == '\"' {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// LogWithMeta merges in the metadata from the errors into the log context\nfunc LogWithMeta(evt *zerolog.Event, err error) *zerolog.Event {\n\tif err == nil {\n\t\treturn evt\n\t}\n\n\tmeta := MetaFrom(err)\n\tfor key, value := range meta {\n\t\tswitch value := value.(type) {\n\t\tcase json.RawMessage:\n\t\t\tevt = evt.RawJSON(key, value)\n\t\tcase error:\n\t\t\tevt = evt.AnErr(key, value)\n\t\tcase time.Time:\n\t\t\tevt = evt.Time(key, value)\n\t\tcase time.Duration:\n\t\t\tevt = evt.Dur(key, value)\n\t\tcase net.IP:\n\t\t\tevt = evt.IPAddr(key, value)\n\t\tcase net.IPNet:\n\t\t\tevt = evt.IPPrefix(key, value)\n\t\tcase net.HardwareAddr:\n\t\t\tevt = evt.MACAddr(key, value)\n\t\tcase string:\n\t\t\tevt = evt.Str(key, value)\n\t\tcase int:\n\t\t\tevt = evt.Int(key, value)\n\t\tcase int8:\n\t\t\tevt = evt.Int8(key, value)\n\t\tcase int16:\n\t\t\tevt = evt.Int16(key, value)\n\t\tcase int32:\n\t\t\tevt = evt.Int32(key, value)\n\t\tcase int64:\n\t\t\tevt = evt.Int64(key, value)\n\t\tcase uint:\n\t\t\tevt = evt.Uint(key, value)\n\t\tcase uint8:\n\t\t\tevt = evt.Uint8(key, value)\n\t\tcase uint16:\n\t\t\tevt = evt.Uint16(key, value)\n\t\tcase uint32:\n\t\t\tevt = evt.Uint32(key, value)\n\t\tcase uint64:\n\t\t\tevt = evt.Uint64(key, value)\n\t\tcase float32:\n\t\t\tevt = evt.Float32(key, value)\n\t\tcase float64:\n\t\t\tevt = evt.Float64(key, value)\n\t\tcase bool:\n\t\t\tevt = evt.Bool(key, value)\n\t\tcase []error:\n\t\t\tevt = evt.Errs(key, value)\n\t\tcase []time.Time:\n\t\t\tevt = evt.Times(key, value)\n\t\tcase []time.Duration:\n\t\t\tevt = evt.Durs(key, value)\n\t\tcase []string:\n\t\t\tevt = evt.Strs(key, value)\n\t\tcase []int:\n\t\t\tevt = evt.Ints(key, value)\n\t\tcase []int8:\n\t\t\tevt = evt.Ints8(key, value)\n\t\tcase []int16:\n\t\t\tevt = evt.Ints16(key, value)\n\t\tcase []int32:\n\t\t\tevt = evt.Ints32(key, value)\n\t\tcase []int64:\n\t\t\tevt = evt.Ints64(key, value)\n\t\tcase []uint:\n\t\t\tevt = evt.Uints(key, value)\n\t\tcase []byte: // uint8 / byte are the same thing so we'll default to bytes\n\t\t\tevt = evt.Bytes(key, value)\n\t\tcase []uint16:\n\t\t\tevt = evt.Uints16(key, value)\n\t\tcase []uint32:\n\t\t\tevt = evt.Uints32(key, value)\n\t\tcase []uint64:\n\t\t\tevt = evt.Uints64(key, value)\n\t\tcase []float32:\n\t\t\tevt = evt.Floats32(key, value)\n\t\tcase []float64:\n\t\t\tevt = evt.Floats64(key, value)\n\t\tcase []bool:\n\t\t\tevt = evt.Bools(key, value)\n\t\tdefault:\n\t\t\tevt = evt.Interface(key, value)\n\t\t}\n\t}\n\treturn evt\n}\n"
  },
  {
    "path": "pkg/emulators/storage/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Engineering at Fullstory\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/batch.go",
    "content": "package gcsemu\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/textproto\"\n)\n\n// BatchHandler handles emulated GCS http requests for \"storage.googleapis.com/batch/storage/v1\".\nfunc (g *GcsEmu) BatchHandler(w http.ResponseWriter, r *http.Request) {\n\t// First parse the entire incoming message.\n\treader, err := r.MultipartReader()\n\tif err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\treturn\n\t}\n\n\tvar reqs []*http.Request\n\tvar contentIds []string\n\tfor i := 0; true; i++ {\n\t\tpart, err := reader.NextPart()\n\t\tif err == io.EOF {\n\t\t\tbreak // done\n\t\t} else if err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tif ct := part.Header.Get(\"Content-Type\"); ct != \"application/http\" {\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"Content-Type: want=application/http, got=%s\", ct))\n\t\t\treturn\n\t\t}\n\n\t\tcontentId := part.Header.Get(\"Content-ID\")\n\n\t\tcontent, err := io.ReadAll(part)\n\t\t_ = part.Close()\n\t\tif err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"part=%d, Content-ID=%s: read error %v\", i, contentId, err))\n\t\t\treturn\n\t\t}\n\n\t\tnewReader := bufio.NewReader(bytes.NewReader(content))\n\t\treq, err := http.ReadRequest(newReader)\n\t\tif err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"part=%d, Content-ID=%s: unable to parse request %v\", i, contentId, err))\n\t\t\treturn\n\t\t}\n\t\t// Any remaining bytes are the body.\n\t\trem, _ := io.ReadAll(newReader)\n\t\tif len(rem) > 0 {\n\t\t\treq.GetBody = func() (io.ReadCloser, error) {\n\t\t\t\treturn io.NopCloser(bytes.NewReader(rem)), nil\n\t\t\t}\n\t\t\treq.Body, _ = req.GetBody()\n\t\t}\n\t\tif cte := part.Header.Get(\"Content-Transfer-Encoding\"); cte != \"\" {\n\t\t\treq.Header.Set(\"Transfer-Encoding\", cte)\n\t\t}\n\t\t// encoded requests don't include a host, so patch it up from the incoming request\n\t\treq.Host = r.Host\n\t\treqs = append(reqs, req)\n\t\tcontentIds = append(contentIds, contentId)\n\t}\n\n\t// At this point, we can respond with a 200.\n\tmw := multipart.NewWriter(w)\n\tw.Header().Set(\"Content-Type\", \"multipart/mixed; boundary=\"+mw.Boundary())\n\tw.WriteHeader(http.StatusOK)\n\n\t// run each request\n\tfor i := range reqs {\n\t\treq, contentId := reqs[i], contentIds[i]\n\n\t\trw := httptest.NewRecorder()\n\t\tg.Handler(rw, req)\n\t\trsp := rw.Result()\n\t\trsp.ContentLength = int64(rw.Body.Len())\n\n\t\tpartHeaders := textproto.MIMEHeader{}\n\t\tpartHeaders.Set(\"Content-Type\", \"application/http\")\n\t\tif contentId != \"\" {\n\t\t\tif contentId[0] == '<' {\n\t\t\t\tcontentId = \"<response-\" + contentId[1:]\n\t\t\t} else {\n\t\t\t\tcontentId = \"response-\" + contentId\n\t\t\t}\n\t\t\tpartHeaders.Set(\"Content-ID\", contentId)\n\t\t}\n\n\t\tpw, _ := mw.CreatePart(partHeaders)\n\t\tif err := rsp.Write(pw); err != nil {\n\t\t\tg.log(err, \"failed to write\")\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err := mw.Close(); err != nil {\n\t\tg.log(err, \"failed to close\")\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/client.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"google.golang.org/api/option\"\n)\n\n// NewClient returns either a real *storage.Cient, or else a *storage.Client that routes\n// to a local emulator if a `GCS_EMULATOR_HOST` environment variable is configured.\nfunc NewClient(ctx context.Context) (*storage.Client, error) {\n\tif host := os.Getenv(\"GCS_EMULATOR_HOST\"); host != \"\" {\n\t\treturn NewTestClientWithHost(ctx, \"http://\"+host)\n\t}\n\treturn storage.NewClient(ctx)\n}\n\n// NewTestClientWithHost returns a new Google storage client that connects to the given host:port address.\nfunc NewTestClientWithHost(ctx context.Context, hostUrl string) (*storage.Client, error) {\n\tdelegate := http.DefaultTransport\n\thttpClient := &http.Client{\n\t\tTransport: tripperFunc(func(r *http.Request) (*http.Response, error) {\n\t\t\tr = r.Clone(r.Context())\n\t\t\tr.URL.Host = strings.TrimPrefix(hostUrl, \"http://\")\n\t\t\tr.URL.Scheme = \"http\"\n\t\t\treturn delegate.RoundTrip(r)\n\t\t}),\n\t}\n\treturn storage.NewClient(ctx, option.WithHTTPClient(httpClient))\n}\n\ntype tripperFunc func(*http.Request) (*http.Response, error)\n\nfunc (f tripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {\n\treturn f(r)\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/errors.go",
    "content": "package gcsemu\n\nimport (\n\t\"fmt\"\n\n\t\"google.golang.org/api/googleapi\"\n)\n\nfunc httpStatusCodeOf(err error) int {\n\tif gapiErr, ok := err.(*googleapi.Error); ok {\n\t\treturn gapiErr.Code\n\t}\n\n\tif httpErr, ok := err.(*httpError); ok {\n\t\tif httpErr.code != 0 {\n\t\t\treturn httpErr.code\n\t\t}\n\t\treturn httpStatusCodeOf(httpErr.cause)\n\t}\n\treturn 0\n}\n\nfunc fmtErrorfCode(httpCode int, f string, args ...interface{}) error {\n\treturn &httpError{\n\t\tcause: fmt.Errorf(f, args...),\n\t\tcode:  httpCode,\n\t}\n}\n\n// httpError is a custom error type that decorates with an HTTP error code\ntype httpError struct {\n\tcause error\n\tcode  int\n}\n\n// Error returns a string describing the entire causal chain.\nfunc (err *httpError) Error() string {\n\tif err == nil {\n\t\treturn \"<nil>\"\n\t}\n\treturn fmt.Sprintf(\"http error %d: %s\", err.code, err.cause)\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/filestore.go",
    "content": "package gcsemu\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tcloudstorage \"cloud.google.com/go/storage\"\n\t\"google.golang.org/api/storage/v1\"\n)\n\nconst (\n\tmetaExtention = \".emumeta\"\n)\n\ntype filestore struct {\n\tgcsDir string\n}\n\nvar _ Store = (*filestore)(nil)\n\n// NewFileStore returns a new Store that writes to the given directory.\nfunc NewFileStore(gcsDir string) *filestore {\n\treturn &filestore{gcsDir: gcsDir}\n}\n\ntype composeObj struct {\n\tfilename string\n\tconds    cloudstorage.Conditions\n}\n\nfunc (fs *filestore) CreateBucket(bucket string) error {\n\tbucketDir := filepath.Join(fs.gcsDir, bucket)\n\treturn os.MkdirAll(bucketDir, 0777)\n}\n\nfunc (fs *filestore) GetBucketMeta(baseUrl HttpBaseUrl, bucket string) (*storage.Bucket, error) {\n\tf := fs.filename(bucket, \"\")\n\tfInfo, err := os.Stat(f)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"stating %s: %w\", f, err)\n\t}\n\n\tobj := BucketMeta(baseUrl, bucket)\n\tobj.Updated = fInfo.ModTime().UTC().Format(time.RFC3339Nano)\n\treturn obj, nil\n}\n\nfunc (fs *filestore) Get(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, []byte, error) {\n\tobj, err := fs.GetMeta(baseUrl, bucket, filename)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif obj == nil {\n\t\treturn nil, nil, nil\n\t}\n\n\tf := fs.filename(bucket, filename)\n\tcontents, err := os.ReadFile(f)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"reading  %s: %w\", f, err)\n\t}\n\treturn obj, contents, nil\n}\n\nfunc (fs *filestore) GetMeta(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, error) {\n\tf := fs.filename(bucket, filename)\n\tfInfo, err := os.Stat(f)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"stating  %s: %w\", f, err)\n\t}\n\n\treturn fs.ReadMeta(baseUrl, bucket, filename, fInfo)\n}\n\nfunc (fs *filestore) Add(bucket string, filename string, contents []byte, meta *storage.Object) error {\n\tf := fs.filename(bucket, filename)\n\tif err := os.MkdirAll(filepath.Dir(f), 0777); err != nil {\n\t\treturn fmt.Errorf(\"could not create dirs for:  %s: %w\", f, err)\n\t}\n\n\tif err := os.WriteFile(f, contents, 0666); err != nil {\n\t\treturn fmt.Errorf(\"could not write:  %s: %w\", f, err)\n\t}\n\n\t// Force a new modification time, since this is what Generation is based on.\n\tnow := time.Now().UTC()\n\t_ = os.Chtimes(f, now, now)\n\n\tInitScrubbedMeta(meta, filename)\n\tmeta.Metageneration = 1\n\tmeta.Generation = now.UnixNano()\n\tif meta.TimeCreated == \"\" {\n\t\tmeta.TimeCreated = now.UTC().Format(time.RFC3339Nano)\n\t}\n\tmeta.Id = fmt.Sprintf(\"%s/%s/%d\", bucket, filename, meta.Generation)\n\tmeta.Etag = fmt.Sprintf(\"%d\", meta.Generation)\n\n\tfMeta := metaFilename(f)\n\tif err := os.WriteFile(fMeta, mustJson(meta), 0666); err != nil {\n\t\treturn fmt.Errorf(\"could not write metadata file: %s: %w\", fMeta, err)\n\t}\n\n\treturn nil\n}\n\nfunc (fs *filestore) UpdateMeta(bucket string, filename string, meta *storage.Object, metagen int64) error {\n\tInitScrubbedMeta(meta, filename)\n\tmeta.Metageneration = metagen\n\n\tfMeta := metaFilename(fs.filename(bucket, filename))\n\tif err := os.WriteFile(fMeta, mustJson(meta), 0666); err != nil {\n\t\treturn fmt.Errorf(\"could not write metadata file: %s: %w\", fMeta, err)\n\t}\n\n\treturn nil\n}\n\nfunc (fs *filestore) Copy(srcBucket string, srcFile string, dstBucket string, dstFile string) (bool, error) {\n\t// Make sure it's there\n\tmeta, err := fs.GetMeta(dontNeedUrls, srcBucket, srcFile)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t// Handle object-not-found\n\tif meta == nil {\n\t\treturn false, nil\n\t}\n\n\t// Copy with metadata\n\tf1 := fs.filename(srcBucket, srcFile)\n\tcontents, err := os.ReadFile(f1)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tmeta.TimeCreated = \"\" // reset creation time on the dest file\n\terr = fs.Add(dstBucket, dstFile, contents, meta)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\nfunc (fs *filestore) Delete(bucket string, filename string) error {\n\tf := fs.filename(bucket, filename)\n\n\terr := func() error {\n\t\t// Check if the bucket exists\n\t\tif _, err := os.Stat(f); os.IsNotExist(err) {\n\t\t\treturn os.ErrNotExist\n\t\t}\n\n\t\t// Remove the bucket\n\t\tif filename == \"\" {\n\t\t\treturn os.RemoveAll(f)\n\t\t}\n\n\t\t// Remove just the file and the associated metadata file\n\t\tif err := os.Remove(f); err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr := os.Remove(metaFilename(f))\n\t\tif os.IsNotExist(err) {\n\t\t\t// Legacy files do not have an accompanying metadata file.\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}()\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn err\n\t\t}\n\t\treturn fmt.Errorf(\"could not delete %s: %w\", f, err)\n\t}\n\n\t// Try to delete empty directories\n\tfor fp := filepath.Dir(f); len(fp) > len(fs.filename(bucket, \"\")); fp = filepath.Dir(fp) {\n\t\tfiles, err := os.ReadDir(fp)\n\t\tif err != nil || len(files) > 0 {\n\t\t\t// Quit trying to delete the directory\n\t\t\tbreak\n\t\t}\n\t\tif err := os.Remove(fp); err != nil {\n\t\t\t// If removing fails, quit trying\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (fs *filestore) ReadMeta(baseUrl HttpBaseUrl, bucket string, filename string, fInfo os.FileInfo) (*storage.Object, error) {\n\tif fInfo.IsDir() {\n\t\treturn nil, nil\n\t}\n\n\tf := fs.filename(bucket, filename)\n\tobj := &storage.Object{}\n\tfMeta := metaFilename(f)\n\tbuf, err := os.ReadFile(fMeta)\n\tif err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn nil, fmt.Errorf(\"could not read metadata file %s: %w\", fMeta, err)\n\t\t}\n\t}\n\n\tif len(buf) != 0 {\n\t\tif err := json.NewDecoder(bytes.NewReader(buf)).Decode(obj); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not parse file attributes %q for %s: %w\", buf, f, err)\n\t\t}\n\t}\n\n\tInitMetaWithUrls(baseUrl, obj, bucket, filename, uint64(fInfo.Size()))\n\t// obj.Generation = fInfo.ModTime().UnixNano() // use the mod time as the generation number\n\tobj.Updated = fInfo.ModTime().UTC().Format(time.RFC3339Nano)\n\treturn obj, nil\n}\n\nfunc (fs *filestore) filename(bucket string, filename string) string {\n\tif filename == \"\" {\n\t\treturn filepath.Join(fs.gcsDir, bucket)\n\t}\n\treturn filepath.Join(fs.gcsDir, bucket, filename)\n}\n\nfunc metaFilename(filename string) string {\n\treturn filename + metaExtention\n}\n\nfunc (fs *filestore) Walk(ctx context.Context, bucket string, cb func(ctx context.Context, filename string, fInfo os.FileInfo) error) error {\n\troot := filepath.Join(fs.gcsDir, bucket)\n\treturn filepath.Walk(root, func(path string, fInfo os.FileInfo, err error) error {\n\t\tif strings.HasSuffix(path, metaExtention) {\n\t\t\t// Ignore metadata files\n\t\t\treturn nil\n\t\t}\n\n\t\tfilename := strings.TrimPrefix(path, root)\n\t\tfilename = strings.TrimPrefix(filename, string(os.PathSeparator))\n\t\tif err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"walk error at %s: %w\", filename, err)\n\t\t}\n\n\t\tif err := cb(ctx, filename, fInfo); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/filestore_test.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestFileStore(t *testing.T) {\n\t// Setup an on-disk emulator.\n\tgcsDir := filepath.Join(os.TempDir(), fmt.Sprintf(\"gcsemu-test-%d\", time.Now().Unix()))\n\tgcsEmu := NewGcsEmu(Options{\n\t\tStore:   NewFileStore(gcsDir),\n\t\tVerbose: true,\n\t\tLog: func(err error, fmt string, args ...interface{}) {\n\t\t\tt.Helper()\n\t\t\tif err != nil {\n\t\t\t\tfmt = \"ERROR: \" + fmt + \": %s\"\n\t\t\t\targs = append(args, err)\n\t\t\t}\n\t\t\tt.Logf(fmt, args...)\n\t\t},\n\t})\n\tmux := http.NewServeMux()\n\tgcsEmu.Register(mux)\n\tsvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"about to method=%s host=%s u=%s\", r.Method, r.Host, r.URL)\n\t\tmux.ServeHTTP(w, r)\n\t}))\n\tt.Cleanup(svr.Close)\n\n\tgcsClient, err := NewTestClientWithHost(context.Background(), svr.URL)\n\tassert.NilError(t, err)\n\tt.Cleanup(func() {\n\t\t_ = gcsClient.Close()\n\t})\n\n\tbh := BucketHandle{\n\t\tName:         \"file-bucket\",\n\t\tBucketHandle: gcsClient.Bucket(\"file-bucket\"),\n\t}\n\tinitBucket(t, bh)\n\tattrs, err := bh.Attrs(context.Background())\n\tassert.NilError(t, err)\n\tassert.Equal(t, bh.Name, attrs.Name)\n\n\tt.Parallel()\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttc.f(t, bh)\n\t\t})\n\t}\n\n\tt.Run(\"RawHttp\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRawHttp(t, bh, http.DefaultClient, svr.URL)\n\t})\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/gcsemu.go",
    "content": "// Package gcsemu implements a Google Cloud Storage emulator for development.\npackage gcsemu\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\tcloudstorage \"cloud.google.com/go/storage\"\n\t\"encr.dev/pkg/emulators/storage/gcsutil\"\n\t\"github.com/bluele/gcache\"\n\t\"google.golang.org/api/storage/v1\"\n)\n\nconst maybeNotImplementedErrorMsg = \"This may be a valid request, but we haven't implemented it in gcsemu yet.\"\n\n// Options configure the emulator.\ntype Options struct {\n\t// A storage layer to use; if nil, defaults to in-mem storage.\n\tStore Store\n\n\t// If true, log verbosely.\n\tVerbose bool\n\n\t// Optional log function. `err` will be `nil` for informational/debug messages.\n\tLog func(err error, fmt string, args ...interface{})\n}\n\n// GcsEmu is a Google Cloud Storage emulator for development.\ntype GcsEmu struct {\n\t// The directory which contains gcs emulation.\n\tstore Store\n\tlocks *gcsutil.TransientLockMap\n\n\tuploadIds gcache.Cache\n\tidCounter int32\n\n\tverbose bool\n\tlog     func(err error, fmt string, args ...interface{})\n}\n\n// NewGcsEmu creates a new Google Cloud Storage emulator.\nfunc NewGcsEmu(opts Options) *GcsEmu {\n\tif opts.Store == nil {\n\t\topts.Store = NewMemStore()\n\t}\n\tif opts.Log == nil {\n\t\topts.Log = func(_ error, _ string, _ ...interface{}) {}\n\t}\n\treturn &GcsEmu{\n\t\tstore:     opts.Store,\n\t\tlocks:     gcsutil.NewTransientLockMap(),\n\t\tuploadIds: gcache.New(1024).LRU().Build(),\n\t\tverbose:   opts.Verbose,\n\t\tlog:       opts.Log,\n\t}\n}\n\nfunc lockName(bucket string, filename string) string {\n\treturn bucket + \"/\" + filename\n}\n\n// Register the emulator's HTTP handlers on the given mux.\nfunc (g *GcsEmu) Register(mux *http.ServeMux) {\n\tmux.HandleFunc(\"/\", DrainRequestHandler(GzipRequestHandler(g.Handler)))\n\tmux.HandleFunc(\"/batch/storage/v1\", DrainRequestHandler(GzipRequestHandler(g.BatchHandler)))\n}\n\n// Handler handles emulated GCS http requests for \"storage.googleapis.com\".\nfunc (g *GcsEmu) Handler(w http.ResponseWriter, r *http.Request) {\n\tbaseUrl := dontNeedUrls\n\t{\n\t\thost := requestHost(r)\n\t\tif host != \"\" {\n\t\t\t// Prepend the proto.\n\t\t\tif r.TLS != nil || r.Header.Get(\"X-Forwarded-Proto\") == \"https\" {\n\t\t\t\tbaseUrl = HttpBaseUrl(\"https://\" + host + \"/\")\n\t\t\t} else {\n\t\t\t\tbaseUrl = HttpBaseUrl(\"http://\" + host + \"/\")\n\t\t\t}\n\t\t}\n\t}\n\n\tctx := r.Context()\n\tp, ok := ParseGcsUrl(r.URL)\n\tif !ok {\n\t\tg.gapiError(w, http.StatusBadRequest, \"unrecognized request\")\n\t\treturn\n\t}\n\tobject := p.Object\n\tbucket := p.Bucket\n\n\tif err := r.ParseForm(); err != nil {\n\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"failed to parse form: %s\", err))\n\t\treturn\n\t}\n\n\tconds, err := parseConds(r.Form)\n\tif err != nil {\n\t\tg.gapiError(w, http.StatusBadRequest, err.Error())\n\t\treturn\n\t}\n\n\tif g.verbose {\n\t\tif object == \"\" {\n\t\t\tg.log(nil, \"%s request for bucket %q\", r.Method, bucket)\n\t\t} else {\n\t\t\tg.log(nil, \"%s request for bucket %q, object %q\", r.Method, bucket, object)\n\t\t}\n\t}\n\n\tswitch r.Method {\n\tcase \"DELETE\":\n\t\tg.handleGcsDelete(ctx, w, bucket, object, conds)\n\tcase \"GET\":\n\t\tif object == \"\" {\n\t\t\tif strings.HasSuffix(r.URL.Path, \"/o\") {\n\t\t\t\tg.handleGcsListBucket(ctx, baseUrl, w, r.URL.Query(), bucket)\n\t\t\t} else {\n\t\t\t\tg.handleGcsMetadataRequest(baseUrl, w, bucket, object)\n\t\t\t}\n\t\t} else {\n\t\t\talt := r.URL.Query().Get(\"alt\")\n\t\t\tif alt == \"media\" || (p.IsPublic && alt == \"\") {\n\t\t\t\tg.handleGcsMediaRequest(baseUrl, w, r.Header.Get(\"Accept-Encoding\"), bucket, object)\n\t\t\t} else if alt == \"json\" || (!p.IsPublic && alt == \"\") {\n\t\t\t\tg.handleGcsMetadataRequest(baseUrl, w, bucket, object)\n\t\t\t} else {\n\t\t\t\t// should never happen?\n\t\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"unsupported value for alt param to GET: %q\\n%s\", alt, maybeNotImplementedErrorMsg))\n\t\t\t}\n\t\t}\n\tcase \"PATCH\":\n\t\talt := r.URL.Query().Get(\"alt\")\n\t\tif alt == \"json\" || r.Header.Get(\"Content-Type\") == \"application/json\" {\n\t\t\tg.handleGcsUpdateMetadataRequest(ctx, baseUrl, w, r, bucket, object, conds)\n\t\t} else {\n\t\t\t// should never happen?\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"unsupported value for alt param to PATCH: %q\\n%s\", alt, maybeNotImplementedErrorMsg))\n\t\t}\n\tcase \"POST\":\n\t\tif bucket == \"\" {\n\t\t\tg.handleGcsNewBucket(ctx, w, r, conds)\n\t\t} else if object == \"\" {\n\t\t\tg.handleGcsNewObject(ctx, baseUrl, w, r, bucket, conds)\n\t\t} else if strings.Contains(object, \"/compose\") {\n\t\t\t// TODO: enforce other conditions outside of generation\n\t\t\tg.handleGcsCompose(ctx, baseUrl, w, r, bucket, object, conds)\n\t\t} else if strings.Contains(object, \"/rewriteTo/\") {\n\t\t\tg.handleGcsCopy(ctx, baseUrl, w, bucket, object)\n\t\t} else if r.Form.Get(\"upload_id\") != \"\" {\n\t\t\tg.handleGcsNewObjectResume(ctx, baseUrl, w, r, r.Form.Get(\"upload_id\"))\n\t\t} else {\n\t\t\t// unsupported method, or maybe should never happen\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"unsupported POST request: %v\\n%s\", r.URL, maybeNotImplementedErrorMsg))\n\t\t}\n\tcase \"PUT\":\n\t\tif r.Form.Get(\"upload_id\") != \"\" {\n\t\t\tg.handleGcsNewObjectResume(ctx, baseUrl, w, r, r.Form.Get(\"upload_id\"))\n\t\t} else {\n\t\t\t// unsupported method, or maybe should never happen\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"unsupported PUT request: %v\\n%s\", r.URL, maybeNotImplementedErrorMsg))\n\t\t}\n\tdefault:\n\t\tg.gapiError(w, http.StatusMethodNotAllowed, \"\")\n\t}\n}\n\nfunc (g *GcsEmu) handleGcsCompose(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, bucket, object string, conds cloudstorage.Conditions) {\n\tvar req storage.ComposeRequest\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\tg.gapiError(w, http.StatusBadRequest, \"bad compose request\")\n\t\treturn\n\t}\n\t// Get the composed object name from the path\n\tparts := strings.Split(object, \"/compose\")\n\tif len(parts) != 2 {\n\t\tg.gapiError(w, http.StatusBadRequest, \"bad compose request\")\n\t\treturn\n\t}\n\tdst := composeObj{\n\t\tfilename: parts[0],\n\t\tconds:    conds,\n\t}\n\n\tsrcs := make([]composeObj, len(req.SourceObjects))\n\tfor i, sObj := range req.SourceObjects {\n\t\tvar generationMatch int64\n\t\tif sObj.ObjectPreconditions != nil {\n\t\t\tgenerationMatch = sObj.ObjectPreconditions.IfGenerationMatch\n\t\t}\n\t\tsrcs[i] = composeObj{\n\t\t\tfilename: sObj.Name,\n\t\t\tconds: cloudstorage.Conditions{\n\t\t\t\tGenerationMatch: generationMatch,\n\t\t\t},\n\t\t}\n\t}\n\tvar obj *storage.Object\n\tif err := g.locks.Run(ctx, lockName(bucket, dst.filename), func(_ context.Context) error {\n\t\tvar err error\n\t\tobj, err = g.finishCompose(baseUrl, bucket, dst, srcs, req.Destination)\n\t\treturn err\n\t}); err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), fmt.Sprintf(\"failed to compose objects: %s\", err))\n\t\treturn\n\t}\n\tg.jsonRespond(w, &obj)\n}\n\nfunc (g *GcsEmu) handleGcsListBucket(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, params url.Values, bucket string) {\n\tdelimiter := params.Get(\"delimiter\")\n\tprefix := params.Get(\"prefix\")\n\tpageToken := params.Get(\"pageToken\")\n\n\tvar cursor string\n\tif pageToken != \"\" {\n\t\tlastFilename, err := gcsutil.DecodePageToken(pageToken)\n\t\tif err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"invalid pageToken parameter (failed to decode) %s: %s\", pageToken, err))\n\t\t\treturn\n\t\t}\n\t\tcursor = lastFilename\n\t}\n\n\tmaxResults := 1000\n\tmaxResultsStr := params.Get(\"maxResults\")\n\tif maxResultsStr != \"\" {\n\t\tvar err error\n\t\tmaxResults, err = strconv.Atoi(maxResultsStr)\n\t\tif err != nil || maxResults < 1 {\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"invalid maxResults parameter: %s\", maxResultsStr))\n\t\t\treturn\n\t\t}\n\t}\n\n\tg.makeBucketListResults(ctx, baseUrl, w, delimiter, cursor, prefix, bucket, maxResults)\n}\n\nfunc (g *GcsEmu) handleGcsDelete(ctx context.Context, w http.ResponseWriter, bucket string, filename string, conds cloudstorage.Conditions) {\n\terr := g.locks.Run(ctx, lockName(bucket, filename), func(ctx context.Context) error {\n\t\t// Find the existing file / meta.\n\t\tobj, err := g.store.GetMeta(dontNeedUrls, bucket, filename)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to check existence of %s/%s: %w\", bucket, filename, err)\n\t\t}\n\n\t\tif err := validateConds(obj, conds); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := g.store.Delete(bucket, filename); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn fmtErrorfCode(http.StatusNotFound, \"%s/%s not found\", bucket, filename)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"failed to delete %s/%s: %w\", bucket, filename, err)\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\treturn\n\t}\n\n\tw.WriteHeader(http.StatusNoContent)\n}\n\nfunc (g *GcsEmu) handleGcsMediaRequest(baseUrl HttpBaseUrl, w http.ResponseWriter, acceptEncoding, bucket, filename string) {\n\tobj, contents, err := g.store.Get(baseUrl, bucket, filename)\n\tif err != nil {\n\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to check existence of %s/%s: %s\", bucket, filename, err))\n\t\treturn\n\t}\n\tif obj == nil {\n\t\tg.gapiError(w, http.StatusNotFound, fmt.Sprintf(\"%s/%s not found\", bucket, filename))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", obj.ContentType)\n\tw.Header().Set(\"X-Goog-Generation\", strconv.FormatInt(obj.Generation, 10))\n\tw.Header().Set(\"X-Goog-Metageneration\", strconv.FormatInt(obj.Metageneration, 10))\n\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tw.Header().Set(\"Access-Control-Expose-Headers\", \"Content-Type, Content-Length, Content-Encoding, Date, X-Goog-Generation, X-Goog-Metageneration\")\n\tw.Header().Set(\"Content-Disposition\", obj.ContentDisposition)\n\n\tif obj.ContentEncoding == \"gzip\" {\n\t\tif strings.Contains(acceptEncoding, \"gzip\") {\n\t\t\tw.Header().Set(\"Content-Encoding\", \"gzip\")\n\t\t} else {\n\t\t\t// Uncompress on behalf of the client.\n\t\t\tbuf := bytes.NewBuffer(contents)\n\t\t\tgzipReader, err := gzip.NewReader(buf)\n\t\t\tif err != nil {\n\t\t\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to gunzip from %s/%s: %s\", bucket, filename, err))\n\t\t\t}\n\t\t\tif _, err := io.Copy(w, gzipReader); err != nil {\n\t\t\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to copy+gunzip from %s/%s: %s\", bucket, filename, err))\n\t\t\t}\n\t\t\tif err := gzipReader.Close(); err != nil {\n\t\t\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to copy+gunzip from %s/%s: %s\", bucket, filename, err))\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Just write the contents\n\tw.Header().Set(\"Content-Length\", strconv.Itoa(len(contents)))\n\tif _, err := w.Write(contents); err != nil {\n\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to copy from %s/%s: %s\", bucket, filename, err))\n\t}\n}\n\nfunc (g *GcsEmu) handleGcsMetadataRequest(baseUrl HttpBaseUrl, w http.ResponseWriter, bucket string, filename string) {\n\tvar obj interface{}\n\tvar err error\n\tif filename == \"\" {\n\t\tvar b *storage.Bucket\n\t\tb, err = g.store.GetBucketMeta(baseUrl, bucket)\n\t\tif b != nil {\n\t\t\tobj = b\n\t\t}\n\t} else {\n\t\tvar o *storage.Object\n\t\to, err = g.store.GetMeta(baseUrl, bucket, filename)\n\t\tif o != nil {\n\t\t\tobj = o\n\t\t}\n\t}\n\n\tif err != nil {\n\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to get meta for %s/%s: %s\", bucket, filename, err))\n\t\treturn\n\t}\n\tif obj == nil {\n\t\tg.gapiError(w, http.StatusNotFound, fmt.Sprintf(\"%s/%s not found\", bucket, filename))\n\t\treturn\n\t}\n\tg.jsonRespond(w, obj)\n}\n\nfunc (g *GcsEmu) handleGcsUpdateMetadataRequest(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, bucket, filename string, conds cloudstorage.Conditions) {\n\tvar obj *storage.Object\n\terr := g.locks.Run(ctx, lockName(bucket, filename), func(ctx context.Context) error {\n\t\t// Find the existing file / meta.\n\t\tvar err error\n\t\tobj, err = g.store.GetMeta(baseUrl, bucket, filename)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to check existence of %s/%s: %w\", bucket, filename, err)\n\t\t}\n\n\t\tif obj == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := validateConds(obj, conds); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Update via json decode.\n\t\tmetagen := obj.Metageneration\n\t\terr = json.NewDecoder(r.Body).Decode(&obj)\n\t\tif err != nil {\n\t\t\treturn fmtErrorfCode(http.StatusBadRequest, \"failed to parse request: %w\", err)\n\t\t}\n\n\t\tif err := g.store.UpdateMeta(bucket, filename, obj, metagen+1); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to update attrs of %s/%s: %w\", bucket, filename, err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\treturn\n\t}\n\tif obj == nil {\n\t\tg.gapiError(w, http.StatusNotFound, fmt.Sprintf(\"%s/%s not found\", bucket, filename))\n\t\treturn\n\t}\n\n\t// Respond with the updated metadata.\n\tobj, err = g.store.GetMeta(baseUrl, bucket, filename)\n\tif err != nil {\n\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"failed to get meta for %s/%s: %s\", bucket, filename, err))\n\t\treturn\n\t}\n\tg.jsonRespond(w, obj)\n}\n\nfunc (g *GcsEmu) handleGcsCopy(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, b1 string, objectPaths string) {\n\t// TODO(dk): this operation supports conditionals and metadata rewriting, but the emulator implementation currently does not.\n\t// See https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite\n\tparts := strings.Split(objectPaths, \"/rewriteTo/b/\")\n\t// Copy is implemented using the Rewrite API, with object strings of format /o/sourceObject/rewriteTo/b/destinationBucket/o/destinationObject\n\tif len(parts) != 2 {\n\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"Bad rewrite request format: %s\", objectPaths))\n\t\treturn\n\t}\n\tf1 := parts[0]\n\tdestParts := strings.Split(parts[1], \"/o/\")\n\tif len(parts) != 2 {\n\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"Bad rewrite request, expected object/file split: %s\", parts[1]))\n\t\treturn\n\t}\n\tb2 := destParts[0]\n\tf2 := destParts[1]\n\n\t// Must lock the destination object.\n\tvar obj *storage.Object\n\terr := g.locks.Run(ctx, lockName(b2, f2), func(ctx context.Context) error {\n\t\tif ok, err := g.store.Copy(b1, f1, b2, f2); err != nil {\n\t\t\treturn err\n\t\t} else if !ok {\n\t\t\treturn nil // file missing\n\t\t} else {\n\t\t\tobj, err = g.store.GetMeta(baseUrl, b2, f2)\n\t\t\treturn err\n\t\t}\n\t})\n\tif err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), fmt.Sprintf(\"failed to copy: %s\", err))\n\t\treturn\n\t}\n\tif obj == nil {\n\t\tg.gapiError(w, http.StatusNotFound, fmt.Sprintf(\"%s not found\", b1+\"/\"+f1))\n\t\treturn\n\t}\n\n\trr := storage.RewriteResponse{\n\t\tKind:                \"storage#rewriteResponse\",\n\t\tTotalBytesRewritten: int64(obj.Size),\n\t\tObjectSize:          int64(obj.Size),\n\t\tDone:                true,\n\t\tRewriteToken:        \"-not-implemented-\",\n\t\tResource:            obj,\n\t}\n\n\tg.jsonRespond(w, &rr)\n}\n\ntype uploadData struct {\n\tObject storage.Object\n\tConds  cloudstorage.Conditions\n\tdata   []byte\n}\n\nfunc (g *GcsEmu) handleGcsNewBucket(ctx context.Context, w http.ResponseWriter, r *http.Request, _ cloudstorage.Conditions) {\n\tvar bucket storage.Bucket\n\tif err := json.NewDecoder(r.Body).Decode(&bucket); err != nil {\n\t\tg.gapiError(w, http.StatusBadRequest, \"failed to parse body as json\")\n\t\treturn\n\t}\n\tbucketName := bucket.Name\n\n\terr := g.locks.Run(ctx, lockName(bucketName, \"\"), func(ctx context.Context) error {\n\t\tif err := g.store.CreateBucket(bucketName); err != nil {\n\t\t\treturn fmt.Errorf(\"could not create bucket %s: %w\", bucketName, err)\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\treturn\n\t}\n\n\tg.jsonRespond(w, bucket)\n}\n\nfunc (g *GcsEmu) handleGcsNewObject(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, bucket string, conds cloudstorage.Conditions) {\n\tswitch r.Form.Get(\"uploadType\") {\n\tcase \"media\":\n\t\t// simple upload\n\t\tname := r.Form.Get(\"name\")\n\t\tif name == \"\" {\n\t\t\tg.gapiError(w, http.StatusBadRequest, \"missing object name\")\n\t\t\treturn\n\t\t}\n\n\t\tcontents, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, \"failed to read body\")\n\t\t\treturn\n\t\t}\n\n\t\tobj := &storage.Object{\n\t\t\tBucket:      bucket,\n\t\t\tContentType: r.Header.Get(\"Content-Type\"),\n\t\t\tName:        name,\n\t\t\tSize:        uint64(len(contents)),\n\t\t}\n\n\t\tmeta, err := g.finishUpload(ctx, baseUrl, obj, contents, bucket, conds)\n\t\tif err != nil {\n\t\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"x-goog-generation\", strconv.FormatInt(meta.Generation, 10))\n\t\tw.Header().Set(\"X-Goog-Metageneration\", strconv.FormatInt(meta.Metageneration, 10))\n\t\tg.jsonRespond(w, meta)\n\t\treturn\n\tcase \"resumable\":\n\t\tvar obj storage.Object\n\t\tif err := json.NewDecoder(r.Body).Decode(&obj); err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, \"failed to parse body as json\")\n\t\t\treturn\n\t\t}\n\t\tobj.Bucket = bucket\n\n\t\tnextId := atomic.AddInt32(&g.idCounter, 1)\n\t\tid := strconv.Itoa(int(nextId))\n\t\t_ = g.uploadIds.Set(id, &uploadData{\n\t\t\tObject: obj,\n\t\t\tConds:  conds,\n\t\t})\n\n\t\tw.Header().Set(\"Location\", ObjectUrl(baseUrl, bucket, obj.Name)+\"?upload_id=\"+id)\n\t\tw.Header().Set(\"Content-Type\", obj.ContentType)\n\t\tw.WriteHeader(http.StatusCreated)\n\t\treturn\n\tcase \"multipart\":\n\t\tobj, contents, err := readMultipartInsert(r)\n\t\tif err != nil {\n\t\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"failed to parse request: %s\", err))\n\t\t\treturn\n\t\t}\n\n\t\tmeta, err := g.finishUpload(ctx, baseUrl, obj, contents, bucket, conds)\n\t\tif err != nil {\n\t\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"x-goog-generation\", strconv.FormatInt(meta.Generation, 10))\n\t\tw.Header().Set(\"X-Goog-Metageneration\", strconv.FormatInt(meta.Metageneration, 10))\n\t\tg.jsonRespond(w, meta)\n\t\treturn\n\tdefault:\n\t\t// TODO\n\t\tg.gapiError(w, http.StatusNotImplemented, \"not yet implemented\")\n\t\treturn\n\n\t}\n}\n\nfunc (g *GcsEmu) handleGcsNewObjectResume(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, r *http.Request, id string) {\n\tfound, err := g.uploadIds.GetIFPresent(id)\n\tif err != nil {\n\t\tg.gapiError(w, http.StatusInternalServerError, fmt.Sprintf(\"unexpected error: %s\", err))\n\t\treturn\n\t}\n\tif found == nil {\n\t\tg.gapiError(w, http.StatusNotFound, \"no such id\")\n\t\treturn\n\t}\n\n\tu := found.(*uploadData)\n\n\tcontents, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"failed to ready body: %s\", err))\n\t\treturn\n\t}\n\n\tcontentRange := r.Header.Get(\"Content-Range\")\n\tif contentRange == \"\" {\n\t\tg.gapiError(w, http.StatusBadRequest, \"expected Content-Range\")\n\t\treturn\n\t}\n\n\t// Parse the content range\n\tbyteRange := parseByteRange(contentRange)\n\tif byteRange == nil {\n\t\tg.gapiError(w, http.StatusBadRequest, \"malformed Content-Range header\")\n\t\treturn\n\t}\n\n\tif byteRange.lo == -1 && len(contents) != 0 || byteRange.lo != -1 && len(contents) != int(byteRange.hi+1-byteRange.lo) {\n\t\tg.gapiError(w, http.StatusBadRequest, fmt.Sprintf(\"Content-Range does not match content size: range=%v, len=%v\", contentRange, len(contents)))\n\t\treturn\n\t}\n\n\tif len(u.data) < int(byteRange.lo) {\n\t\tg.gapiError(w, http.StatusBadRequest, \"missing content\")\n\t\treturn\n\t}\n\n\t// Apply the content to our stored data.\n\tif byteRange.lo != -1 {\n\t\tu.data = u.data[:byteRange.lo] // truncate a previous write if we've seen this range before\n\t}\n\tu.data = append(u.data, contents...)\n\n\t// Are we done?\n\tif byteRange.sz < 0 || len(u.data) < int(byteRange.sz) {\n\t\t// Not finished; save the contents and tell the client to resume.\n\t\tw.Header().Set(\"Range\", fmt.Sprintf(\"bytes=0-%d\", len(u.data)-1))\n\t\tw.Header().Set(\"Content-Type\", u.Object.ContentType)\n\t\tif r.Header.Get(\"X-Guploader-No-308\") == \"yes\" {\n\t\t\tw.Header().Set(\"X-Http-Status-Code-Override\", \"308\")\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t} else {\n\t\t\tw.WriteHeader(http.StatusPermanentRedirect)\n\t\t}\n\t\treturn\n\t}\n\n\t// Done\n\tmeta, err := g.finishUpload(ctx, baseUrl, &u.Object, u.data, u.Object.Bucket, u.Conds)\n\tif err != nil {\n\t\tg.gapiError(w, httpStatusCodeOf(err), err.Error())\n\t\treturn\n\t}\n\n\tg.uploadIds.Remove(id)\n\tw.Header().Set(\"x-goog-generation\", strconv.FormatInt(meta.Generation, 10))\n\tw.Header().Set(\"X-Goog-Metageneration\", strconv.FormatInt(meta.Metageneration, 10))\n\tg.jsonRespond(w, meta)\n}\n\nfunc (g *GcsEmu) finishUpload(ctx context.Context, baseUrl HttpBaseUrl, obj *storage.Object, contents []byte, bucket string, conds cloudstorage.Conditions) (*storage.Object, error) {\n\tfilename := obj.Name\n\tbHash := md5.Sum(contents)\n\tcontentHash := bHash[:]\n\tmd5Hash := base64.StdEncoding.EncodeToString(contentHash)\n\tif obj.Md5Hash != \"\" {\n\t\th, err := base64.StdEncoding.DecodeString(obj.Md5Hash)\n\t\tif err != nil {\n\t\t\treturn nil, fmtErrorfCode(http.StatusBadRequest, \"not a valid md5 hash: %w\", err)\n\t\t}\n\t\tif !bytes.Equal(contentHash, h) {\n\t\t\treturn nil, fmtErrorfCode(http.StatusBadRequest, \"md5 hash %s != expected %s\", obj.Md5Hash, md5Hash)\n\t\t}\n\t}\n\tobj.Md5Hash = md5Hash\n\tobj.Etag = strconv.Quote(md5Hash)\n\n\terr := g.locks.Run(ctx, lockName(bucket, filename), func(ctx context.Context) error {\n\t\t// Find the existing file / meta.\n\t\texisting, err := g.store.GetMeta(baseUrl, bucket, filename)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to check existence of %s/%s: %w\", bucket, filename, err)\n\t\t}\n\n\t\tif err := validateConds(existing, conds); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif existing != nil {\n\t\t\tobj.TimeCreated = existing.TimeCreated\n\t\t}\n\n\t\tif err := g.store.Add(bucket, filename, contents, obj); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create %s/%s: %w\", bucket, filename, err)\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// respond with object metadata\n\tmeta, err := g.store.GetMeta(baseUrl, bucket, filename)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get meta for %s/%s: %w\", bucket, filename, err)\n\t}\n\n\tmeta.Id = fmt.Sprintf(\"%s/%s/%d\", bucket, filename, meta.Generation)\n\treturn meta, nil\n}\n\n// Returns true if item is strictly greater than anything that begins with prefix\nfunc greaterThanPrefix(item string, prefix string) bool {\n\tif len(item) < len(prefix) {\n\t\treturn item > prefix\n\t}\n\treturn item[:len(prefix)] > prefix\n}\n\n// Returns true if item is strictly less than anything that begins with prefix\nfunc lessThanPrefix(item string, prefix string) bool {\n\tif len(item) < len(prefix) {\n\t\treturn item < prefix[:len(item)]\n\t}\n\treturn item < prefix\n}\n\nvar (\n\temptyConds        = cloudstorage.Conditions{}\n\tdoesNotExistConds = cloudstorage.Conditions{DoesNotExist: true}\n)\n\nfunc validateConds(obj *storage.Object, cond cloudstorage.Conditions) error {\n\tif obj == nil {\n\t\t// The only way a nil object can succeed is if the conds are exactly equal to empty or doesNotExist\n\t\tif cond == emptyConds || cond == doesNotExistConds {\n\t\t\treturn nil\n\t\t}\n\t\treturn fmtErrorfCode(http.StatusPreconditionFailed, \"precondition failed\")\n\t}\n\n\t// obj != nil from here on\n\n\tif cond.DoesNotExist {\n\t\treturn fmtErrorfCode(http.StatusPreconditionFailed, \"precondition failed\")\n\t}\n\n\tif cond.GenerationMatch != 0 && obj.Generation != cond.GenerationMatch {\n\t\treturn fmtErrorfCode(http.StatusPreconditionFailed, \"precondition failed\")\n\t}\n\n\tif cond.GenerationNotMatch != 0 && obj.Generation == cond.GenerationNotMatch {\n\t\t// not-match failures use a different code\n\t\treturn fmtErrorfCode(http.StatusNotModified, \"precondition failed\")\n\t}\n\n\tif cond.MetagenerationMatch != 0 && obj.Metageneration != cond.MetagenerationMatch {\n\t\treturn fmtErrorfCode(http.StatusPreconditionFailed, \"precondition failed\")\n\t}\n\n\tif cond.MetagenerationNotMatch != 0 && obj.Metageneration == cond.MetagenerationNotMatch {\n\t\t// not-match failures use a different code\n\t\treturn fmtErrorfCode(http.StatusNotModified, \"precondition failed\")\n\t}\n\n\treturn nil\n}\n\nfunc parseConds(vals url.Values) (cloudstorage.Conditions, error) {\n\tvar ret cloudstorage.Conditions\n\tfor i, e := range []struct {\n\t\tparamName string\n\t\tref       *int64\n\t}{\n\t\t{\"ifGenerationMatch\", &ret.GenerationMatch},\n\t\t{\"ifGenerationNotMatch\", &ret.GenerationNotMatch},\n\t\t{\"ifMetagenerationMatch\", &ret.MetagenerationMatch},\n\t\t{\"ifMetagenerationNotMatch\", &ret.MetagenerationNotMatch},\n\t} {\n\t\tv := vals.Get(e.paramName)\n\t\tif v == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tval, err := strconv.ParseInt(v, 10, 64)\n\t\tif err != nil {\n\t\t\treturn ret, fmt.Errorf(\"failed to parse %s=%s: %w\", e.paramName, v, err)\n\t\t}\n\t\t*e.ref = val\n\t\tif i == 0 {\n\t\t\t// Special case\n\t\t\tret.DoesNotExist = val == 0\n\t\t}\n\t}\n\n\treturn ret, nil\n}\n\nconst (\n\tgcsMaxComposeSources = 32\n)\n\nfunc (g *GcsEmu) finishCompose(baseUrl HttpBaseUrl, bucket string, dst composeObj, srcs []composeObj, meta *storage.Object) (*storage.Object, error) {\n\tif len(srcs) > gcsMaxComposeSources {\n\t\treturn nil, fmtErrorfCode(http.StatusBadRequest, \"too many sources\")\n\t}\n\n\t// TODO: consider moving this to disk to handle very large compose operations\n\tvar data []byte\n\tmetas := make([]*storage.Object, len(srcs))\n\tfor i, src := range srcs {\n\t\tmeta, contents, err := g.store.Get(baseUrl, bucket, src.filename)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get object %s: %w\", src.filename, err)\n\t\t}\n\t\tif meta == nil {\n\t\t\treturn nil, fmtErrorfCode(http.StatusNotFound, \"no such source object %s\", src.filename)\n\t\t}\n\t\tif err := validateConds(meta, src.conds); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdata = append(data, contents...)\n\t\tmetas[i] = meta\n\t}\n\n\tfor _, m := range metas {\n\t\tmeta.ComponentCount += m.ComponentCount\n\t}\n\t// composite objects do not have an MD5 hash (https://cloud.google.com/storage/docs/composite-objects)\n\tmeta.Md5Hash = \"\"\n\n\tdstMeta, err := g.store.GetMeta(baseUrl, bucket, dst.filename)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get object %s: %w\", dst.filename, err)\n\t}\n\tif err := validateConds(dstMeta, dst.conds); err != nil {\n\t\treturn nil, err\n\t}\n\tif dstMeta != nil {\n\t\tmeta.TimeCreated = dstMeta.TimeCreated\n\t}\n\tif err := g.store.Add(bucket, dst.filename, data, meta); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to add new file: %w\", err)\n\t}\n\treturn g.store.GetMeta(baseUrl, bucket, dst.filename)\n}\n\n// InitBucket creates the given bucket directly.\nfunc (g *GcsEmu) InitBucket(bucketName string) error {\n\treturn g.locks.Run(context.Background(), lockName(bucketName, \"\"), func(ctx context.Context) error {\n\t\tif err := g.store.CreateBucket(bucketName); err != nil {\n\t\t\treturn fmt.Errorf(\"could not create bucket: %s: %w\", bucketName, err)\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/gcsemu_test.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"google.golang.org/api/googleapi\"\n\t\"google.golang.org/api/iterator\"\n\t\"gotest.tools/v3/assert\"\n)\n\nconst (\n\tinvalidBucketName = \"fullstory-non-existant-bucket\"\n)\n\nvar (\n\ttestCases = []struct {\n\t\tname string\n\t\tf    func(t *testing.T, bh BucketHandle)\n\t}{\n\t\t{\"Basics\", testBasics},\n\t\t{\"MultipleFiles\", testMultipleFiles},\n\t\t{\"HugeFile\", testHugeFile},\n\t\t{\"HugeFile_MultipleOfChunkSize\", testHugeFileMultipleOfChunkSize},\n\t\t{\"HugeFileWithConditional\", testHugeFileWithConditional},\n\t\t{\"ConditionalUpdates\", testConditionalUpdates},\n\t\t{\"GenNotMatchDoesntExist\", testGenNotMatchDoesntExist},\n\t\t{\"CopyBasics\", testCopyBasics},\n\t\t{\"Compose\", testCompose},\n\t\t{\"CopyMetadata\", testCopyMetadata},\n\t\t{\"CopyConditionals\", testCopyConditionals},\n\t}\n)\n\nconst (\n\tv1      = `This file is for gcsemu_intg_test.go, please ignore (v1)`\n\tv2      = `This file is for gcsemu_intg_test.go, please ignore (this is version 2)`\n\tsource1 = `This is source file number 1`\n\tsource2 = `This is source file number 2`\n)\n\ntype BucketHandle struct {\n\tName string\n\t*storage.BucketHandle\n}\n\nfunc initBucket(t *testing.T, bh BucketHandle) {\n\tctx := context.Background()\n\n\t_ = bh.Delete(ctx)\n\terr := bh.Create(ctx, \"dev\", &storage.BucketAttrs{})\n\tassert.NilError(t, err, \"failed\")\n\n\tattrs, err := bh.Attrs(ctx)\n\tassert.NilError(t, err, \"failed\")\n\tassert.Equal(t, bh.Name, attrs.Name, \"wrong\")\n}\n\nfunc testBasics(t *testing.T, bh BucketHandle) {\n\tconst name = \"gscemu-test/1.txt\"\n\tctx := context.Background()\n\toh := bh.Object(name)\n\n\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\terr := oh.Delete(ctx)\n\tif err != nil {\n\t\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\t}\n\n\t// Should not exist.\n\t_, err = oh.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\t// Checker funcs\n\tcheckAttrs := func(attrs *storage.ObjectAttrs, content string, metagen int64) {\n\t\tassert.Equal(t, name, attrs.Name, \"wrong\")\n\t\tassert.Equal(t, bh.Name, attrs.Bucket, \"wrong\")\n\t\tassert.Equal(t, int64(len(content)), attrs.Size, \"wrong\")\n\t\tassert.Equal(t, metagen, attrs.Metageneration, \"wrong\")\n\t\tcheckSum := md5.Sum([]byte(content))\n\t\tassert.DeepEqual(t, checkSum[:], attrs.MD5)\n\t}\n\n\tcheckObject := func(content string, metagen int64) *storage.ObjectAttrs {\n\t\tattrs, err := oh.Attrs(ctx)\n\t\tassert.NilError(t, err, \"failed\")\n\t\tcheckAttrs(attrs, content, metagen)\n\n\t\tr, err := oh.NewReader(ctx)\n\t\tassert.NilError(t, err, \"failed\")\n\t\tdata, err := io.ReadAll(r)\n\t\tassert.NilError(t, err, \"failed\")\n\t\tassert.NilError(t, r.Close(), \"failed\")\n\t\tassert.Equal(t, content, string(data), \"wrong data\")\n\t\treturn attrs\n\t}\n\n\t// Create the object.\n\tw := oh.NewWriter(ctx)\n\tassert.NilError(t, write(w, v1), \"failed\")\n\tcheckAttrs(w.Attrs(), v1, 1)\n\n\t// Read the object.\n\tattrs := checkObject(v1, 1)\n\tassert.Assert(t, attrs.Generation != 0, \"expected non-zero\")\n\tgen := attrs.Generation\n\n\t// Update the object to version 2.  Also test MD5 setting.\n\tw = oh.NewWriter(ctx)\n\tcheckSum := md5.Sum([]byte(v2))\n\tw.MD5 = checkSum[:]\n\tassert.NilError(t, write(w, v2), \"failed\")\n\tcheckAttrs(w.Attrs(), v2, 1)\n\tassert.Assert(t, gen != w.Attrs().Generation, \"expected different gen\")\n\tgen = w.Attrs().Generation\n\n\t// Read the object again.\n\tattrs = checkObject(v2, 1)\n\tassert.Equal(t, gen, attrs.Generation, \"expected same gen\")\n\n\t// Update the attrs.\n\tattrs, err = oh.Update(ctx, storage.ObjectAttrsToUpdate{\n\t\tContentType: \"text/plain\",\n\t})\n\tassert.NilError(t, err, \"failed\")\n\tcheckAttrs(attrs, v2, 2)\n\tassert.Equal(t, \"text/plain\", attrs.ContentType, \"wrong\")\n\tassert.Equal(t, gen, attrs.Generation, \"expected same gen\")\n\n\t// Delete the object.\n\tassert.NilError(t, oh.Delete(ctx), \"failed\")\n\n\t// Should not exist.\n\t_, err = oh.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\t// Should not be able to update attrs.\n\t_, err = oh.Update(ctx, storage.ObjectAttrsToUpdate{\n\t\tContentType: \"text/plain\",\n\t})\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n}\n\nfunc testMultipleFiles(t *testing.T, bh BucketHandle) {\n\tdir := \"multi-test/\"\n\tctx := context.Background()\n\n\tfiles := []string{\"file1\", \"file2\", \"file3\"}\n\tfor _, f := range files {\n\t\toh := bh.Object(dir + f)\n\t\tw := oh.NewWriter(ctx)\n\t\tassert.NilError(t, write(w, v1), \"failed to write file %s\", dir+f)\n\t}\n\n\titer := bh.Objects(ctx, &storage.Query{Prefix: dir})\n\tfor _, f := range files {\n\t\tobj, err := iter.Next()\n\t\tassert.NilError(t, err, \"failed to fetch next object\")\n\t\tassert.Equal(t, dir+f, obj.Name, \"wrong filename\")\n\t}\n\n\t// No more objects should exist\n\t_, err := iter.Next()\n\tassert.Equal(t, iterator.Done, err, \"iteration not finished or failed after first bucket object\")\n}\n\n// Tests resumable GCS uploads.\nfunc testHugeFile(t *testing.T, bh BucketHandle) {\n\tdoHugeFile(t, bh, \"gscemu-test/huge.txt\", googleapi.DefaultUploadChunkSize+4*1024*1024)\n}\n\nfunc testHugeFileMultipleOfChunkSize(t *testing.T, bh BucketHandle) {\n\tdoHugeFile(t, bh, \"gscemu-test/huge2.txt\", googleapi.DefaultUploadChunkSize*4)\n}\n\nfunc doHugeFile(t *testing.T, bh BucketHandle, name string, size int) {\n\tctx := context.Background()\n\toh := bh.Object(name)\n\n\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\terr := oh.Delete(ctx)\n\tif err != nil {\n\t\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\t}\n\n\t// Should not exist.\n\t_, err = oh.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\t// Create the object.\n\tw := oh.NewWriter(ctx)\n\thash, err := writeHugeObject(t, w, size)\n\tassert.NilError(t, err, \"failed\")\n\n\tattrs, err := oh.Attrs(ctx)\n\tassert.NilError(t, err, \"failed\")\n\tassert.Equal(t, size, int(attrs.Size), \"wrong\")\n\tassert.DeepEqual(t, hash, attrs.MD5)\n}\n\nfunc writeHugeObject(t *testing.T, w *storage.Writer, sz int) ([]byte, error) {\n\tdata := []byte(`0123456789ABCDEF`)\n\thash := md5.New()\n\tfor i := 0; i < sz/len(data); i++ {\n\t\tn, err := w.Write(data)\n\t\t_, _ = hash.Write(data)\n\t\tassert.NilError(t, err, \"failed\")\n\t\tassert.Equal(t, n, len(data), \"short write\")\n\t}\n\treturn hash.Sum(nil), w.Close()\n}\n\n// Tests resumable GCS uploads.\nfunc testHugeFileWithConditional(t *testing.T, bh BucketHandle) {\n\tconst name = \"gscemu-test/huge2.txt\"\n\tconst size = googleapi.DefaultUploadChunkSize*2 + 1024\n\n\tctx := context.Background()\n\toh := bh.Object(name)\n\n\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\terr := oh.Delete(ctx)\n\tif err != nil {\n\t\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\t}\n\n\t// Should not exist.\n\t_, err = oh.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\t// Create the object.\n\tw := oh.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)\n\thash, err := writeHugeObject(t, w, size)\n\tassert.NilError(t, err, \"failed\")\n\n\tattrs, err := oh.Attrs(ctx)\n\tassert.NilError(t, err, \"failed\")\n\tassert.Equal(t, size, int(attrs.Size), \"wrong\")\n\tassert.DeepEqual(t, hash, attrs.MD5)\n\n\t// Should fail this time.\n\tw = oh.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)\n\t_, err = writeHugeObject(t, w, size)\n\tassert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n}\n\nfunc testConditionalUpdates(t *testing.T, bh BucketHandle) {\n\tconst name = \"gscemu-test/2.txt\"\n\tctx := context.Background()\n\toh := bh.Object(name)\n\n\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\terr := oh.Delete(ctx)\n\tif err != nil {\n\t\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\t}\n\n\t// Ensure write fails\n\tw := oh.If(storage.Conditions{GenerationMatch: 1}).NewWriter(ctx)\n\terr = write(w, \"bogus\")\n\tassert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n\n\t// Now actually write it.\n\tw = oh.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)\n\tassert.NilError(t, write(w, v1), \"failed\")\n\tattrs := w.Attrs()\n\tt.Logf(\"attrs.Generation=%d attrs.Metageneration=%d\", attrs.Generation, attrs.Metageneration)\n\n\texpectFailConds := func(expectCode int, conds storage.Conditions) {\n\t\t// Ensure attr update fails.\n\t\t_, err = oh.If(conds).Update(ctx, storage.ObjectAttrsToUpdate{ContentType: \"text/plain\"})\n\t\tassert.Equal(t, expectCode, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n\n\t\t// Ensure write fails\n\t\tw := oh.If(conds).NewWriter(ctx)\n\t\terr = write(w, \"bogus\")\n\t\tassert.Equal(t, expectCode, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n\n\t\t// Ensure delete fails\n\t\terr = oh.If(conds).Delete(ctx)\n\t\tassert.Equal(t, expectCode, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n\t}\n\n\tfor i, conds := range []storage.Conditions{\n\t\t{\n\t\t\tDoesNotExist: true,\n\t\t},\n\t\t{\n\t\t\tGenerationMatch: attrs.Generation + 1,\n\t\t},\n\t\t{\n\t\t\tMetagenerationMatch: attrs.Metageneration + 1,\n\t\t},\n\t\t{\n\t\t\tGenerationMatch:     attrs.Generation,\n\t\t\tMetagenerationMatch: attrs.Metageneration + 1,\n\t\t},\n\t\t{\n\t\t\tGenerationMatch:     attrs.Generation + 1,\n\t\t\tMetagenerationMatch: attrs.Metageneration,\n\t\t},\n\t\t{\n\t\t\tGenerationNotMatch: attrs.Generation,\n\t\t},\n\t\t{\n\t\t\tMetagenerationNotMatch: attrs.Metageneration,\n\t\t},\n\t\t{\n\t\t\tGenerationNotMatch:  attrs.Generation,\n\t\t\tMetagenerationMatch: attrs.Metageneration,\n\t\t},\n\t\t{\n\t\t\tGenerationMatch:        attrs.Generation,\n\t\t\tMetagenerationNotMatch: attrs.Metageneration,\n\t\t},\n\t} {\n\t\tt.Logf(\"case %d\", i)\n\t\texpectCode := http.StatusPreconditionFailed\n\t\tif i >= 5 {\n\t\t\t// For some reason, \"not match\" cases return 304 rather than 412.\n\t\t\texpectCode = http.StatusNotModified\n\t\t}\n\t\texpectFailConds(expectCode, conds)\n\t}\n\n\t// Actually update the attrs.\n\tattrs, err = oh.If(storage.Conditions{\n\t\tGenerationMatch:     attrs.Generation,\n\t\tMetagenerationMatch: attrs.Metageneration,\n\t}).Update(ctx, storage.ObjectAttrsToUpdate{\n\t\tContentType: \"text/plain\",\n\t})\n\tassert.NilError(t, err, \"failed\")\n\n\t// Actually update the content.\n\tw = oh.If(storage.Conditions{\n\t\tGenerationMatch:     attrs.Generation,\n\t\tMetagenerationMatch: attrs.Metageneration,\n\t}).NewWriter(ctx)\n\tassert.NilError(t, write(w, v2), \"failed\")\n\tattrs = w.Attrs()\n\n\t// Actually delete.\n\terr = oh.If(storage.Conditions{\n\t\tGenerationMatch:     attrs.Generation,\n\t\tMetagenerationMatch: attrs.Metageneration,\n\t}).Delete(ctx)\n\tassert.NilError(t, err, \"failed\")\n\n\t// Should not exist.\n\t_, err = oh.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n}\n\nfunc testGenNotMatchDoesntExist(t *testing.T, bh BucketHandle) {\n\t// How does generation not match interact with a non-existent file?\n\n\tconst name = \"gscemu-test-gen-not-match.txt\"\n\tctx := context.Background()\n\toh := bh.Object(name)\n\n\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\terr := oh.Delete(ctx)\n\tif err != nil {\n\t\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\t}\n\n\t// Write should fail on non-existent object, even though the generation doesn't match.\n\tw := oh.If(storage.Conditions{GenerationNotMatch: 1}).NewWriter(ctx)\n\terr = write(w, \"bogus\")\n\tassert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n}\n\nfunc testCopyBasics(t *testing.T, bh BucketHandle) {\n\tctx := context.Background()\n\n\tfile1 := \"file-1\"\n\tfile2 := \"file-1-again\"\n\n\tsrc := bh.Object(file1)\n\tdest := bh.Object(file2)\n\n\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\t_ = src.Delete(ctx)\n\t_ = dest.Delete(ctx)\n\n\t// Should not exist.\n\t_, err := src.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\t// Create the object.\n\tw := src.NewWriter(ctx)\n\tn, err := io.Copy(w, strings.NewReader(v1))\n\tassert.NilError(t, err, \"failed\")\n\tassert.Equal(t, n, int64(len(v1)), \"wrong length\")\n\tassert.NilError(t, w.Close(), \"failed\")\n\n\t// Wait a ms to ensure different timestamps.\n\ttime.Sleep(time.Millisecond)\n\n\t// Copy the object\n\tdestAttrs, err := dest.CopierFrom(src).Run(ctx)\n\tassert.NilError(t, err, \"failed to copy\")\n\n\t// Read the object.\n\tr, err := dest.NewReader(ctx)\n\tassert.NilError(t, err, \"failed\")\n\tdata, err := io.ReadAll(r)\n\tassert.NilError(t, err, \"failed\")\n\tassert.NilError(t, r.Close(), \"failed\")\n\tassert.Equal(t, string(data), v1, \"wrong data\")\n\n\t// Check the metadata reread correct\n\treDestAttrs, err := dest.Attrs(ctx)\n\tassert.NilError(t, err, \"failed\")\n\tassert.DeepEqual(t, destAttrs, reDestAttrs)\n\n\t// Check the metadata was copied and makes sense.\n\tsrcAttrs, err := src.Attrs(ctx)\n\tassert.NilError(t, err, \"failed\")\n\texpectAttrs := *srcAttrs\n\n\t// Some things should be different\n\tassert.Assert(t, srcAttrs.Name != destAttrs.Name, \"should not equal: %s\", destAttrs.Name)\n\texpectAttrs.Name = destAttrs.Name\n\n\tassert.Assert(t, srcAttrs.MediaLink != destAttrs.MediaLink, \"should not equal: %s\", destAttrs.MediaLink)\n\texpectAttrs.MediaLink = destAttrs.MediaLink\n\n\tassert.Assert(t, srcAttrs.Generation != destAttrs.Generation, \"should not equal: %d\", destAttrs.Generation)\n\texpectAttrs.Generation = destAttrs.Generation\n\n\tassert.Assert(t, srcAttrs.Created != destAttrs.Created, \"should not equal: %s\", destAttrs.Created)\n\texpectAttrs.Created = destAttrs.Created\n\n\tassert.Assert(t, srcAttrs.Updated != destAttrs.Updated, \"should not equal: %s\", destAttrs.Updated)\n\texpectAttrs.Updated = destAttrs.Updated\n\n\texpectAttrs.Etag = destAttrs.Etag\n\n\t// Rest should be same\n\tassert.DeepEqual(t, expectAttrs, *destAttrs)\n\n\t// Delete the object.\n\tassert.NilError(t, src.Delete(ctx), \"failed\")\n\tassert.NilError(t, dest.Delete(ctx), \"failed\")\n\n\t// Copy an object that doesn't exist\n\t_, err = dest.CopierFrom(src).Run(ctx)\n\tassert.Equal(t, http.StatusNotFound, httpStatusCodeOf(err), \"wrong error %T: %s\", err, err)\n}\n\nfunc testCompose(t *testing.T, bh BucketHandle) {\n\tctx := context.Background()\n\n\tsrcFiles := []string{source1, source2}\n\tsrcGens := []int64{0, 0}\n\tmanualCompose := \"\"\n\tdstName := \"gcs-test-data/dest.txt\"\n\tdstNameSecondary := \"gcs-test-data/dest-secondary.txt\"\n\n\tsrcs := make([]*storage.ObjectHandle, len(srcFiles))\n\tfor i, src := range srcFiles {\n\t\tname := fmt.Sprintf(\"gcs-test-sources/src-%d.txt\", i)\n\t\tsrcs[i] = bh.Object(name)\n\t\t// Forcibly delete the object at the start, make sure it doesn't exist.\n\t\t_ = srcs[i].Delete(ctx)\n\n\t\t// Should not exist.\n\t\t_, err := srcs[i].Attrs(ctx)\n\t\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\t\t// Create the object.\n\t\tw := srcs[i].NewWriter(ctx)\n\t\tw.ContentType = \"text/csv\"\n\t\tn, err := io.Copy(w, strings.NewReader(src))\n\t\tassert.NilError(t, err, \"failed\")\n\t\tassert.Equal(t, n, int64(len(src)), \"wrong length\")\n\t\tassert.NilError(t, w.Close(), \"failed\")\n\t\tsrcGens[i] = w.Attrs().Generation\n\n\t\tmanualCompose += src\n\t}\n\n\tdest := bh.Object(dstName)\n\n\tdestSecondary := bh.Object(dstNameSecondary)\n\terr := destSecondary.Delete(ctx) // this one needs to be deleted to start\n\tassert.Assert(t, err == nil || err == storage.ErrObjectNotExist, \"failed to delete secondary\")\n\n\tcomposer := dest.ComposerFrom(srcs...)\n\tcomposer.ContentType = \"text/plain\"\n\tattrs, err := composer.Run(ctx)\n\tassert.NilError(t, err, \"failed to run compose\")\n\n\tassert.Equal(t, dest.BucketName(), attrs.Bucket, \"bucket doesn't match\")\n\tassert.Equal(t, dest.ObjectName(), attrs.Name, \"object name doesn't match\")\n\tassert.Equal(t, \"text/plain\", attrs.ContentType, \"content type doesn't match\")\n\tr, err := dest.NewReader(ctx)\n\tassert.NilError(t, err, \"failed to create reader for composed file\")\n\tdata, err := io.ReadAll(r)\n\tassert.NilError(t, err, \"failed to read from composed file\")\n\tassert.NilError(t, r.Close(), \"failed to close composed file reader\")\n\tassert.Equal(t, manualCompose, string(data), \"content doesn't match\")\n\n\t// Issue the same request with incorrect generation on a source.\n\tcomposer = dest.If(storage.Conditions{GenerationMatch: attrs.Generation}).ComposerFrom(\n\t\tsrcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}),     // correct\n\t\tsrcs[1].If(storage.Conditions{GenerationMatch: srcGens[1] + 1})) // incorrect\n\tcomposer.ContentType = \"text/plain\"\n\t_, err = composer.Run(ctx)\n\tassert.ErrorContains(t, err, \"googleapi: Error 412\")\n\tassert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), \"expected precondition failed\")\n\n\t// Issue the same request with incorrect generation on the destination.\n\tcomposer = dest.If(storage.Conditions{DoesNotExist: true}).ComposerFrom(\n\t\tsrcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}),\n\t\tsrcs[1].If(storage.Conditions{GenerationMatch: srcGens[1]}))\n\tcomposer.ContentType = \"text/plain\"\n\t_, err = composer.Run(ctx)\n\tassert.ErrorContains(t, err, \"googleapi: Error 412\")\n\tassert.Equal(t, http.StatusPreconditionFailed, httpStatusCodeOf(err), \"expected precondition failed\")\n\n\t// Issue the a request does not exist destination.\n\tcomposer = destSecondary.If(storage.Conditions{DoesNotExist: true}).ComposerFrom(\n\t\tsrcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}),\n\t\tsrcs[1].If(storage.Conditions{GenerationMatch: srcGens[1]}))\n\tcomposer.ContentType = \"text/plain\"\n\t_, err = composer.Run(ctx)\n\tassert.NilError(t, err, \"failed to run compose\")\n\t// The resulting data should be correct (like in the original test).\n\tr, err = destSecondary.NewReader(ctx)\n\tassert.NilError(t, err, \"failed to create reader for composed file\")\n\tdata, err = io.ReadAll(r)\n\tassert.NilError(t, err, \"failed to read from composed file\")\n\tassert.NilError(t, r.Close(), \"failed to close composed file reader\")\n\tassert.Equal(t, manualCompose, string(data), \"content doesn't match\")\n\n\t// Use the new destination as the source for another compose. This is how we append\n\t// Additionally, use generation conditions for all of the source objects.\n\tcomposer = dest.ComposerFrom(\n\t\tdest.If(storage.Conditions{GenerationMatch: attrs.Generation}),\n\t\tsrcs[0].If(storage.Conditions{GenerationMatch: srcGens[0]}))\n\tnewAttrs, err := composer.Run(ctx)\n\tassert.NilError(t, err, \"failed to run compose\")\n\tassert.Equal(t, \"\", newAttrs.ContentType, \"content type doesn't match\")\n\n\tr, err = dest.NewReader(ctx)\n\tassert.NilError(t, err, \"failed to create reader for composed file\")\n\tdata, err = io.ReadAll(r)\n\tassert.NilError(t, err, \"failed to read from composed file\")\n\tassert.NilError(t, r.Close(), \"failed to close composed file reader\")\n\tassert.Equal(t, manualCompose+source1, string(data), \"content doesn't match\")\n\n\t// Make sure we get a 404 if the source doesn't exist\n\tdneObj := bh.Object(\"dneObject\")\n\t_ = dneObj.Delete(ctx)\n\t// Should not exist.\n\t_, err = dneObj.Attrs(ctx)\n\tassert.Equal(t, storage.ErrObjectNotExist, err, \"wrong error\")\n\n\tcomposer = dest.ComposerFrom(dneObj)\n\t_, err = composer.Run(ctx)\n\tassert.Equal(t, http.StatusNotFound, httpStatusCodeOf(err), \"wrong error returned\")\n}\n\nfunc testCopyMetadata(t *testing.T, bh BucketHandle) {\n\t// TODO(dk): Metadata-rewriting on copy is not currently implemented.\n\tt.Skip()\n}\n\nfunc testCopyConditionals(t *testing.T, bh BucketHandle) {\n\t// TODO(dk): Conditional support for copy is not currently implemented.\n\tt.Skip()\n}\n\nfunc write(w *storage.Writer, content string) error {\n\tn, err := io.Copy(w, strings.NewReader(content))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif n != int64(len(content)) {\n\t\tpanic(\"not all content sent\")\n\t}\n\treturn w.Close()\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/http_wrappers.go",
    "content": "package gcsemu\n\nimport (\n\t\"compress/gzip\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// DrainRequestHandler wraps the given handler to drain the incoming request body on exit.\nfunc DrainRequestHandler(h http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\t// Always drain and close the request body to properly free up the connection.\n\t\t\t// See https://groups.google.com/forum/#!topic/golang-nuts/pP3zyUlbT00\n\t\t\t_, _ = io.Copy(io.Discard, r.Body)\n\t\t\t_ = r.Body.Close()\n\t\t}()\n\t\th(w, r)\n\t}\n}\n\n// GzipRequestHandler wraps the given handler to automatically decompress gzipped content.\nfunc GzipRequestHandler(h http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Header.Get(\"Content-Encoding\") == \"gzip\" {\n\t\t\tgzr, err := gzip.NewReader(r.Body)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tr.Body = gzr\n\t\t}\n\t\th(w, r)\n\t}\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/memstore.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\t\"fmt\"\n\n\t\"github.com/google/btree\"\n\t\"google.golang.org/api/storage/v1\"\n)\n\ntype memstore struct {\n\tmu      sync.RWMutex\n\tbuckets map[string]*memBucket\n}\n\nvar _ Store = (*memstore)(nil)\n\n// NewMemStore returns a Store that operates purely in memory.\nfunc NewMemStore() *memstore {\n\treturn &memstore{buckets: map[string]*memBucket{}}\n}\n\ntype memBucket struct {\n\tcreated time.Time\n\n\t// mutex required (despite lock map in gcsemu), because btree mutations are not structurally safe\n\tmu    sync.RWMutex\n\tfiles *btree.BTree\n}\n\nfunc (ms *memstore) getBucket(bucket string) *memBucket {\n\tms.mu.RLock()\n\tdefer ms.mu.RUnlock()\n\treturn ms.buckets[bucket]\n}\n\ntype memFile struct {\n\tmeta storage.Object\n\tdata []byte\n}\n\nfunc (mf *memFile) Less(than btree.Item) bool {\n\t// TODO(dragonsinth): is a simple lexical sort ok for Walk?\n\treturn mf.meta.Name < than.(*memFile).meta.Name\n}\n\nvar _ btree.Item = (*memFile)(nil)\n\nfunc (ms *memstore) CreateBucket(bucket string) error {\n\tms.mu.Lock()\n\tdefer ms.mu.Unlock()\n\tif ms.buckets[bucket] == nil {\n\t\tms.buckets[bucket] = &memBucket{\n\t\t\tcreated: time.Now(),\n\t\t\tfiles:   btree.New(16),\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (ms *memstore) GetBucketMeta(baseUrl HttpBaseUrl, bucket string) (*storage.Bucket, error) {\n\tif b := ms.getBucket(bucket); b != nil {\n\t\tobj := BucketMeta(baseUrl, bucket)\n\t\tobj.Updated = b.created.UTC().Format(time.RFC3339Nano)\n\t\treturn obj, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (ms *memstore) Get(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, []byte, error) {\n\tf := ms.find(bucket, filename)\n\tif f != nil {\n\t\treturn &f.meta, f.data, nil\n\t}\n\treturn nil, nil, nil\n}\n\nfunc (ms *memstore) GetMeta(baseUrl HttpBaseUrl, bucket string, filename string) (*storage.Object, error) {\n\tf := ms.find(bucket, filename)\n\tif f != nil {\n\t\tmeta := f.meta\n\t\tInitMetaWithUrls(baseUrl, &meta, bucket, filename, uint64(len(f.data)))\n\t\treturn &meta, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (ms *memstore) Add(bucket string, filename string, contents []byte, meta *storage.Object) error {\n\t_ = ms.CreateBucket(bucket)\n\n\tInitScrubbedMeta(meta, filename)\n\tmeta.Metageneration = 1\n\n\t// Cannot be overridden by caller\n\tnow := time.Now().UTC()\n\tmeta.Updated = now.UTC().Format(time.RFC3339Nano)\n\tmeta.Generation = now.UnixNano()\n\tif meta.TimeCreated == \"\" {\n\t\tmeta.TimeCreated = meta.Updated\n\t}\n\tmeta.Id = fmt.Sprintf(\"%s/%s/%d\", bucket, filename, meta.Generation)\n\tmeta.Etag = fmt.Sprintf(\"%d\", meta.Generation)\n\n\tb := ms.getBucket(bucket)\n\tb.mu.Lock()\n\tdefer b.mu.Unlock()\n\tb.files.ReplaceOrInsert(&memFile{\n\t\tmeta: *meta,\n\t\tdata: contents,\n\t})\n\treturn nil\n}\n\nfunc (ms *memstore) UpdateMeta(bucket string, filename string, meta *storage.Object, metagen int64) error {\n\tf := ms.find(bucket, filename)\n\tif f == nil {\n\t\treturn os.ErrNotExist\n\t}\n\n\tInitScrubbedMeta(meta, filename)\n\tmeta.Metageneration = metagen\n\n\tb := ms.getBucket(bucket)\n\tb.mu.Lock()\n\tdefer b.mu.Unlock()\n\tb.files.ReplaceOrInsert(&memFile{\n\t\tmeta: *meta,\n\t\tdata: f.data,\n\t})\n\treturn nil\n}\n\nfunc (ms *memstore) Copy(srcBucket string, srcFile string, dstBucket string, dstFile string) (bool, error) {\n\tsrc := ms.find(srcBucket, srcFile)\n\tif src == nil {\n\t\treturn false, nil\n\t}\n\n\t// Copy with metadata\n\tmeta := src.meta\n\tmeta.TimeCreated = \"\" // reset creation time on the dest file\n\terr := ms.Add(dstBucket, dstFile, src.data, &meta)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\nfunc (ms *memstore) Delete(bucket string, filename string) error {\n\tif filename == \"\" {\n\t\t// Remove the bucket\n\t\tms.mu.Lock()\n\t\tdefer ms.mu.Unlock()\n\t\tif _, ok := ms.buckets[bucket]; !ok {\n\t\t\treturn os.ErrNotExist\n\t\t}\n\n\t\tdelete(ms.buckets, bucket)\n\t} else if b := ms.getBucket(bucket); b != nil {\n\t\t// Remove just the file\n\t\tb.mu.Lock()\n\t\tdefer b.mu.Unlock()\n\t\tif b.files.Delete(ms.key(filename)) == nil {\n\t\t\t// case file does not exist\n\t\t\treturn os.ErrNotExist\n\t\t}\n\t} else {\n\t\treturn os.ErrNotExist\n\t}\n\n\treturn nil\n}\n\nfunc (ms *memstore) ReadMeta(baseUrl HttpBaseUrl, bucket string, filename string, _ os.FileInfo) (*storage.Object, error) {\n\treturn ms.GetMeta(baseUrl, bucket, filename)\n}\n\nfunc (ms *memstore) Walk(ctx context.Context, bucket string, cb func(ctx context.Context, filename string, fInfo os.FileInfo) error) error {\n\tif b := ms.getBucket(bucket); b != nil {\n\t\tvar err error\n\t\tb.mu.RLock()\n\t\tdefer b.mu.RUnlock()\n\t\tb.files.Ascend(func(i btree.Item) bool {\n\t\t\tmf := i.(*memFile)\n\t\t\terr = cb(ctx, mf.meta.Name, nil)\n\t\t\treturn err == nil\n\t\t})\n\t\treturn nil\n\t}\n\treturn os.ErrNotExist\n}\n\nfunc (ms *memstore) key(filename string) btree.Item {\n\treturn &memFile{\n\t\tmeta: storage.Object{\n\t\t\tName: filename,\n\t\t},\n\t}\n}\n\nfunc (ms *memstore) find(bucket string, filename string) *memFile {\n\tif b := ms.getBucket(bucket); b != nil {\n\t\tb.mu.Lock()\n\t\tdefer b.mu.Unlock()\n\t\tf := b.files.Get(ms.key(filename))\n\t\tif f != nil {\n\t\t\treturn f.(*memFile)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/memstore_test.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestMemStore(t *testing.T) {\n\t// Setup an in-memory emulator.\n\tgcsEmu := NewGcsEmu(Options{\n\t\tVerbose: true,\n\t\tLog: func(err error, fmt string, args ...interface{}) {\n\t\t\tt.Helper()\n\t\t\tif err != nil {\n\t\t\t\tfmt = \"ERROR: \" + fmt + \": %s\"\n\t\t\t\targs = append(args, err)\n\t\t\t}\n\t\t\tt.Logf(fmt, args...)\n\t\t},\n\t})\n\tmux := http.NewServeMux()\n\tgcsEmu.Register(mux)\n\tsvr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"about to method=%s host=%s u=%s\", r.Method, r.Host, r.URL)\n\t\tmux.ServeHTTP(w, r)\n\t}))\n\tt.Cleanup(svr.Close)\n\n\tgcsClient, err := NewTestClientWithHost(context.Background(), svr.URL)\n\tassert.NilError(t, err)\n\tt.Cleanup(func() {\n\t\t_ = gcsClient.Close()\n\t})\n\n\tbh := BucketHandle{\n\t\tName:         \"mem-bucket\",\n\t\tBucketHandle: gcsClient.Bucket(\"mem-bucket\"),\n\t}\n\tinitBucket(t, bh)\n\tattrs, err := bh.Attrs(context.Background())\n\tassert.NilError(t, err)\n\tassert.Equal(t, bh.Name, attrs.Name)\n\n\tt.Parallel()\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttc.f(t, bh)\n\t\t})\n\t}\n\n\tt.Run(\"RawHttp\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRawHttp(t, bh, http.DefaultClient, svr.URL)\n\t})\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/meta.go",
    "content": "package gcsemu\n\nimport (\n\t\"fmt\"\n\t\"mime\"\n\t\"strings\"\n\n\t\"google.golang.org/api/storage/v1\"\n)\n\n// BucketMeta returns a default bucket metadata for the given name and base url.\nfunc BucketMeta(baseUrl HttpBaseUrl, bucket string) *storage.Bucket {\n\treturn &storage.Bucket{\n\t\tKind:         \"storage#bucket\",\n\t\tName:         bucket,\n\t\tSelfLink:     BucketUrl(baseUrl, bucket),\n\t\tStorageClass: \"STANDARD\",\n\t}\n}\n\n// InitScrubbedMeta \"bakes\" metadata with intrinsic values and removes fields that are intrinsic / computed.\nfunc InitScrubbedMeta(meta *storage.Object, filename string) {\n\tparts := strings.Split(filename, \".\")\n\text := parts[len(parts)-1]\n\n\tif meta.ContentType == \"\" {\n\t\tmeta.ContentType = mime.TypeByExtension(ext)\n\t}\n\tmeta.Name = filename\n\tScrubMeta(meta)\n}\n\n// InitMetaWithUrls \"bakes\" metadata with intrinsic values, including computed links.\nfunc InitMetaWithUrls(baseUrl HttpBaseUrl, meta *storage.Object, bucket string, filename string, size uint64) {\n\tparts := strings.Split(filename, \".\")\n\text := parts[len(parts)-1]\n\n\tmeta.Bucket = bucket\n\tif meta.ContentType == \"\" {\n\t\tmeta.ContentType = mime.TypeByExtension(ext)\n\t}\n\tmeta.Kind = \"storage#object\"\n\tmeta.MediaLink = ObjectUrl(baseUrl, bucket, filename) + \"?alt=media\"\n\tmeta.Name = filename\n\tmeta.SelfLink = ObjectUrl(baseUrl, bucket, filename)\n\tmeta.Size = size\n\tmeta.StorageClass = \"STANDARD\"\n}\n\n// ScrubMeta removes fields that are intrinsic / computed for minimal storage.\nfunc ScrubMeta(meta *storage.Object) {\n\tmeta.Bucket = \"\"\n\tmeta.Kind = \"\"\n\tmeta.MediaLink = \"\"\n\tmeta.SelfLink = \"\"\n\tmeta.Size = 0\n\tmeta.StorageClass = \"\"\n}\n\n// BucketUrl returns the URL for a bucket.\nfunc BucketUrl(baseUrl HttpBaseUrl, bucket string) string {\n\treturn fmt.Sprintf(\"%sstorage/v1/b/%s\", normalizeBaseUrl(baseUrl), bucket)\n}\n\n// ObjectUrl returns the URL for a file.\nfunc ObjectUrl(baseUrl HttpBaseUrl, bucket string, filepath string) string {\n\treturn fmt.Sprintf(\"%sstorage/v1/b/%s/o/%s\", normalizeBaseUrl(baseUrl), bucket, filepath)\n}\n\n// HttpBaseUrl represents the emulator base URL, including trailing slash; e.g. https://www.googleapis.com/\ntype HttpBaseUrl string\n\n// when the caller doesn't really care about the object meta URLs\nconst dontNeedUrls = HttpBaseUrl(\"\")\n\nfunc normalizeBaseUrl(baseUrl HttpBaseUrl) HttpBaseUrl {\n\tif baseUrl == dontNeedUrls || baseUrl == \"https://storage.googleapis.com/\" {\n\t\treturn \"https://www.googleapis.com/\"\n\t} else if baseUrl == \"http://storage.googleapis.com/\" {\n\t\treturn \"http://www.googleapis.com/\"\n\t} else {\n\t\treturn baseUrl\n\t}\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/multipart.go",
    "content": "package gcsemu\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\n\t\"google.golang.org/api/storage/v1\"\n)\n\nfunc readMultipartInsert(r *http.Request) (*storage.Object, []byte, error) {\n\tv := r.Header.Get(\"Content-Type\")\n\tif v == \"\" {\n\t\treturn nil, nil, fmt.Errorf(\"failed to parse Content-Type header: %q\", v)\n\t}\n\td, params, err := mime.ParseMediaType(v)\n\tif err != nil || d != \"multipart/related\" {\n\t\treturn nil, nil, fmt.Errorf(\"failed to parse Content-Type header: %q\", v)\n\t}\n\tboundary, ok := params[\"boundary\"]\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"Content-Type header is missing boundary: %q\", v)\n\t}\n\n\treader := multipart.NewReader(r.Body, boundary)\n\n\treadPart := func() ([]byte, error) {\n\t\tpart, err := reader.NextPart()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get multipart: %w\", err)\n\t\t}\n\n\t\tb, err := io.ReadAll(part)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get read multipart: %w\", err)\n\t\t}\n\n\t\treturn b, nil\n\t}\n\n\t// read the first part to get the storage.Object (in json)\n\tb, err := readPart()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to read first part of body: %w\", err)\n\t}\n\n\tvar obj storage.Object\n\terr = json.Unmarshal(b, &obj)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to parse body as json: %w\", err)\n\t}\n\n\t// read the next part to get the file contents\n\tcontents, err := readPart()\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to read second part of body: %w\", err)\n\t}\n\n\tobj.Size = uint64(len(contents))\n\n\treturn &obj, contents, nil\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/parse.go",
    "content": "package gcsemu\n\nimport (\n\t\"net/url\"\n\t\"regexp\"\n)\n\nconst (\n\t// example: \"/storage/v1/b/my-bucket/o/2013-tax-returns.pdf\" (for a file) or \"/storage/v1/b/my-bucket/o\" (for a bucket)\n\tgcsObjectPathPattern = \"/storage/v1/b/([^\\\\/]+)/o(?:/(.+))?\"\n\t// example: \"//b/my-bucket/o/2013-tax-returns.pdf\" (for a file) or \"/b/my-bucket/o\" (for a bucket)\n\tgcsObjectPathPattern2 = \"/b/([^\\\\/]+)/o(?:/(.+))?\"\n\t// example: \"/storage/v1/b/my-bucket\n\tgcsBucketPathPattern = \"/storage/v1/b(?:/([^\\\\/]+))?\"\n\t// example: \"/my-bucket/2013-tax-returns.pdf\" (for a file)\n\tgcsStoragePathPattern = \"/([^\\\\/]+)/(.+)\"\n)\n\nvar (\n\tgcsObjectPathRegex  = regexp.MustCompile(gcsObjectPathPattern)\n\tgcsObjectPathRegex2 = regexp.MustCompile(gcsObjectPathPattern2)\n\tgcsBucketPathRegex  = regexp.MustCompile(gcsBucketPathPattern)\n\tgcsStoragePathRegex = regexp.MustCompile(gcsStoragePathPattern)\n)\n\n// GcsParams represent a parsed GCS url.\ntype GcsParams struct {\n\tBucket   string\n\tObject   string\n\tIsPublic bool\n}\n\n// ParseGcsUrl parses a GCS url.\nfunc ParseGcsUrl(u *url.URL) (*GcsParams, bool) {\n\tif g, ok := parseGcsUrl(gcsObjectPathRegex, u); ok {\n\t\treturn g, true\n\t}\n\tif g, ok := parseGcsUrl(gcsBucketPathRegex, u); ok {\n\t\treturn g, true\n\t}\n\tif g, ok := parseGcsUrl(gcsObjectPathRegex2, u); ok {\n\t\treturn g, true\n\t}\n\tif g, ok := parseGcsUrl(gcsStoragePathRegex, u); ok {\n\t\tg.IsPublic = true\n\t\treturn g, true\n\t}\n\treturn nil, false\n}\n\nfunc parseGcsUrl(re *regexp.Regexp, u *url.URL) (*GcsParams, bool) {\n\tsubmatches := re.FindStringSubmatch(u.Path)\n\tif submatches == nil {\n\t\treturn nil, false\n\t}\n\n\tg := &GcsParams{}\n\tif len(submatches) > 1 {\n\t\tg.Bucket = submatches[1]\n\t}\n\tif len(submatches) > 2 {\n\t\tg.Object = submatches[2]\n\t}\n\treturn g, true\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/range.go",
    "content": "package gcsemu\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype byteRange struct {\n\tlo, hi, sz int64\n}\n\nfunc parseByteRange(in string) *byteRange {\n\tvar err error\n\tif !strings.HasPrefix(in, \"bytes \") {\n\t\treturn nil\n\t}\n\tin = strings.TrimPrefix(in, \"bytes \")\n\tparts := strings.Split(in, \"/\")\n\tif len(parts) != 2 {\n\t\treturn nil\n\t}\n\n\tret := byteRange{\n\t\tlo: -1,\n\t\thi: -1,\n\t\tsz: -1,\n\t}\n\n\tif parts[0] != \"*\" {\n\t\tparts := strings.Split(parts[0], \"-\")\n\t\tif len(parts) != 2 {\n\t\t\treturn nil\n\t\t}\n\t\tret.lo, err = strconv.ParseInt(parts[0], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tret.hi, err = strconv.ParseInt(parts[1], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif parts[1] != \"*\" {\n\t\tret.sz, err = strconv.ParseInt(parts[1], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn &ret\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/range_test.go",
    "content": "package gcsemu\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestParseByteRange(t *testing.T) {\n\ttcs := []struct {\n\t\tin     string\n\t\texpect byteRange\n\t}{\n\t\t{in: \"bytes 0-8388607/*\", expect: byteRange{lo: 0, hi: 8388607, sz: -1}},\n\t\t{in: \"bytes 8388608-10485759/10485760\", expect: byteRange{lo: 8388608, hi: 10485759, sz: 10485760}},\n\t\t{in: \"bytes */10485760\", expect: byteRange{lo: -1, hi: -1, sz: 10485760}},\n\t}\n\n\tfor _, tc := range tcs {\n\t\tt.Logf(\"test case: %s\", tc.in)\n\t\tassert.Equal(t, tc.expect, *parseByteRange(tc.in))\n\t}\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/raw_http_test.go",
    "content": "package gcsemu\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\tapi \"google.golang.org/api/storage/v1\"\n\t\"gotest.tools/v3/assert\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/textproto\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc testRawHttp(t *testing.T, bh BucketHandle, httpClient *http.Client, url string) {\n\tconst name = \"gscemu-test3.txt\"\n\tconst name2 = \"gscemu-test4.txt\"\n\tconst delName = \"gscemu-test-deletion.txt\"    // used for successful deletion\n\tconst delName2 = \"gscemu-test-deletion-2.txt\" // used for not found deletion\n\n\texpectMetaGen := int64(1)\n\ttcs := []struct {\n\t\tname          string\n\t\tmakeRequest   func(*testing.T) *http.Request\n\t\tcheckResponse func(*testing.T, *http.Response)\n\t}{\n\t\t{\n\t\t\tname: \"rawGetObject\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/download/storage/v1/b/%s/o/%s?alt=media\", url, bh.Name, name)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"GET\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tbody, err := io.ReadAll(rsp.Body)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, http.StatusOK, rsp.StatusCode)\n\t\t\t\tassert.Equal(t, v1, string(body))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawGetMeta\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/storage/v1/b/%s/o/%s\", url, bh.Name, name)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"GET\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tbody, err := io.ReadAll(rsp.Body)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, http.StatusOK, rsp.StatusCode)\n\n\t\t\t\tvar attrs api.Object\n\t\t\t\terr = json.NewDecoder(bytes.NewReader(body)).Decode(&attrs)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, name, attrs.Name)\n\t\t\t\tassert.Equal(t, bh.Name, attrs.Bucket)\n\t\t\t\tassert.Equal(t, uint64(len(v1)), attrs.Size)\n\t\t\t\tassert.Equal(t, expectMetaGen, attrs.Metageneration)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawPatchMeta\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/storage/v1/b/%s/o/%s\", url, bh.Name, name)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"PATCH\", u, strings.NewReader(`{\"metadata\": {\"type\": \"tabby\"}}`))\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tbody, err := io.ReadAll(rsp.Body)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, http.StatusOK, rsp.StatusCode)\n\n\t\t\t\texpectMetaGen++\n\n\t\t\t\tvar attrs api.Object\n\t\t\t\terr = json.NewDecoder(bytes.NewReader(body)).Decode(&attrs)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, name, attrs.Name)\n\t\t\t\tassert.Equal(t, bh.Name, attrs.Bucket)\n\t\t\t\tassert.Equal(t, uint64(len(v1)), attrs.Size)\n\t\t\t\tassert.Equal(t, expectMetaGen, attrs.Metageneration)\n\t\t\t\tassert.Equal(t, \"tabby\", attrs.Metadata[\"type\"])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawDeleteObject-Success\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/storage/v1/b/%s/o/%s\", url, bh.Name, delName)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"DELETE\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"text/plain\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tassert.Equal(t, http.StatusNoContent, rsp.StatusCode)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawDeleteObject-ObjectNotFound\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/storage/v1/b/%s/o/%s\", url, bh.Name, delName2)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"DELETE\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"text/plain\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tassert.Equal(t, http.StatusNotFound, rsp.StatusCode)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawDeleteObject-BucketNotFound\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/storage/v1/b/%s/o/%s\", url, invalidBucketName, delName)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"DELETE\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"text/plain\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tassert.Equal(t, http.StatusNotFound, rsp.StatusCode)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawDeleteBucket-BucketNotFound\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/storage/v1/b/%s\", url, invalidBucketName)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"DELETE\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"text/plain\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tassert.Equal(t, http.StatusNotFound, rsp.StatusCode)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"rawUpload\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/upload/storage/v1/b/%s/o?uploadType=media&name=%s\", url, bh.Name, name2)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"POST\", u, strings.NewReader(v2))\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treq.Header.Set(\"Content-Type\", \"text/plain\")\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tbody, err := io.ReadAll(rsp.Body)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, http.StatusOK, rsp.StatusCode)\n\n\t\t\t\tvar attrs api.Object\n\t\t\t\terr = json.NewDecoder(bytes.NewReader(body)).Decode(&attrs)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, name2, attrs.Name)\n\t\t\t\tassert.Equal(t, bh.Name, attrs.Bucket)\n\t\t\t\tassert.Equal(t, uint64(len(v2)), attrs.Size)\n\t\t\t\tassert.Equal(t, int64(1), attrs.Metageneration)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"publicUrl\",\n\t\t\tmakeRequest: func(t *testing.T) *http.Request {\n\t\t\t\tu := fmt.Sprintf(\"%s/%s/%s?alt=media\", url, bh.Name, name)\n\t\t\t\tt.Log(u)\n\t\t\t\treq, err := http.NewRequest(\"GET\", u, nil)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\treturn req\n\t\t\t},\n\t\t\tcheckResponse: func(t *testing.T, rsp *http.Response) {\n\t\t\t\tbody, err := io.ReadAll(rsp.Body)\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, http.StatusOK, rsp.StatusCode)\n\t\t\t\tassert.Equal(t, v1, string(body))\n\t\t\t},\n\t\t},\n\t}\n\n\tctx := context.Background()\n\toh := bh.Object(name)\n\n\t// Create the object 1.\n\tw := oh.NewWriter(ctx)\n\tassert.NilError(t, write(w, v1))\n\n\t// Make sure object 2 is not there.\n\t_ = bh.Object(name2).Delete(ctx)\n\n\t// batch setup\n\t// Create the object for successful deletion.\n\tw = bh.Object(delName).NewWriter(ctx)\n\tassert.NilError(t, write(w, v1))\n\t// Make sure object for not found deletion is not there.\n\t_ = bh.Object(delName2).Delete(ctx)\n\n\t// Run each test individually.\n\tfor _, tc := range tcs {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := tc.makeRequest(t)\n\t\t\trsp, err := httpClient.Do(req)\n\t\t\tassert.NilError(t, err)\n\t\t\tbody, err := httputil.DumpResponse(rsp, true)\n\t\t\tassert.NilError(t, err)\n\t\t\tt.Log(string(body))\n\t\t\ttc.checkResponse(t, rsp)\n\t\t})\n\t}\n\n\t// batch setup again for batch deletion step\n\t// Create the object for successful deletion.\n\tw = bh.Object(delName).NewWriter(ctx)\n\tassert.NilError(t, write(w, v1))\n\t// Make sure object for not found deletion is not there.\n\t_ = bh.Object(delName2).Delete(ctx)\n\n\t// Batch requests don't support upload and download, only metadata stuff.\n\tt.Run(\"batch\", func(t *testing.T) {\n\t\tvar buf bytes.Buffer\n\t\tw := multipart.NewWriter(&buf)\n\n\t\t// Only use the [second, fifth] requests.\n\t\tbatchTcs := tcs[1:6]\n\t\tfor i, tc := range batchTcs {\n\t\t\treq := tc.makeRequest(t)\n\t\t\treq.Host = \"\"\n\t\t\treq.URL.Host = \"\"\n\n\t\t\tp, _ := w.CreatePart(textproto.MIMEHeader{\n\t\t\t\t\"Content-Type\":              []string{\"application/http\"},\n\t\t\t\t\"Content-Transfer-Encoding\": []string{\"binary\"},\n\t\t\t\t\"Content-ID\":                []string{fmt.Sprintf(\"<id+%d>\", i)},\n\t\t\t})\n\t\t\tbuf, err := httputil.DumpRequest(req, true)\n\t\t\tassert.NilError(t, err)\n\t\t\t_, _ = p.Write(buf)\n\t\t}\n\t\t_ = w.Close()\n\n\t\t// Compile the request\n\t\treq, err := http.NewRequest(\"POST\", fmt.Sprintf(\"%s/batch/storage/v1\", url), &buf)\n\t\tassert.NilError(t, err)\n\t\treq.Header.Set(\"Content-Type\", \"multipart/mixed; boundary=\"+w.Boundary())\n\n\t\tbody, err := httputil.DumpRequest(req, true)\n\t\tassert.NilError(t, err)\n\t\tt.Log(string(body))\n\n\t\trsp, err := httpClient.Do(req)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, http.StatusOK, rsp.StatusCode)\n\n\t\tbody, err = httputil.DumpResponse(rsp, true)\n\t\tassert.NilError(t, err)\n\t\tt.Log(string(body))\n\n\t\t// decode the multipart response\n\t\tv := rsp.Header.Get(\"Content-type\")\n\t\tassert.Check(t, v != \"\")\n\t\td, params, err := mime.ParseMediaType(v)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, \"multipart/mixed\", d)\n\t\tboundary, ok := params[\"boundary\"]\n\t\tassert.Check(t, ok)\n\n\t\tr := multipart.NewReader(rsp.Body, boundary)\n\t\tfor i, tc := range batchTcs {\n\t\t\tpart, err := r.NextPart()\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, \"application/http\", part.Header.Get(\"Content-Type\"))\n\t\t\tassert.Equal(t, fmt.Sprintf(\"<response-id+%d>\", i), part.Header.Get(\"Content-ID\"))\n\t\t\tb, err := io.ReadAll(part)\n\t\t\tassert.NilError(t, err)\n\n\t\t\t// Decode the buffer into an http.Response\n\t\t\trsp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(b)), nil)\n\t\t\tassert.NilError(t, err)\n\t\t\ttc.checkResponse(t, rsp)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/remote_test.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"golang.org/x/oauth2/google\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestRealStore(t *testing.T) {\n\tbucket := os.Getenv(\"BUCKET_ID\")\n\tif bucket == \"\" {\n\t\tt.Skip(\"BUCKET_ID must be set to run this\")\n\t}\n\n\tctx := context.Background()\n\tgcsClient, err := storage.NewClient(ctx)\n\tassert.NilError(t, err)\n\tt.Cleanup(func() {\n\t\t_ = gcsClient.Close()\n\t})\n\n\tbh := BucketHandle{\n\t\tName:         bucket,\n\t\tBucketHandle: gcsClient.Bucket(bucket),\n\t}\n\n\t// Instead of `initBucket`, just check that it exists.\n\t_, err = bh.Attrs(ctx)\n\tassert.NilError(t, err)\n\n\tt.Parallel()\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttc.f(t, bh)\n\t\t})\n\t}\n\n\thttpClient, err := google.DefaultClient(context.Background())\n\tassert.NilError(t, err)\n\tt.Run(\"RawHttp\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestRawHttp(t, bh, httpClient, \"https://storage.googleapis.com\")\n\t})\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/server.go",
    "content": "package gcsemu\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n)\n\n// Server is an in-memory Cloud Storage emulator; it is unauthenticated, and only a rough approximation.\ntype Server struct {\n\tAddr string\n\t*httptest.Server\n\t*GcsEmu\n}\n\n// NewServer creates a new Server with the given options.\n// The Server will be listening for HTTP connections, without TLS,\n// on the provided address. The resolved address is named by the Addr field.\n// An address with a port of 0 will bind to an open port on the system.\n//\n// For running a full in-process setup (e.g. unit tests), initialize\n// os.Setenv(\"GCS_EMULATOR_HOST\", srv.Addr) so that subsequent calls to NewClient()\n// will return an in-process targeted storage client.\nfunc NewServer(laddr string, opts Options) (*Server, error) {\n\tgcsEmu := NewGcsEmu(opts)\n\tmux := http.NewServeMux()\n\tgcsEmu.Register(mux)\n\n\tsrv := httptest.NewUnstartedServer(mux)\n\tl, err := net.Listen(\"tcp\", laddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to listen on addr %s: %w\", laddr, err)\n\t}\n\tsrv.Listener = l\n\tsrv.Start()\n\n\treturn &Server{\n\t\tAddr:   strings.TrimPrefix(srv.URL, \"http://\"),\n\t\tServer: srv,\n\t\tGcsEmu: gcsEmu,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/store.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"google.golang.org/api/storage/v1\"\n)\n\n// Store is an interface to either on-disk or in-mem storage\ntype Store interface {\n\t// CreateBucket creates a bucket; no error if the bucket already exists.\n\tCreateBucket(bucket string) error\n\n\t// Get returns a bucket's metadata.\n\tGetBucketMeta(baseUrl HttpBaseUrl, bucket string) (*storage.Bucket, error)\n\n\t// Get returns a file's contents and metadata.\n\tGet(url HttpBaseUrl, bucket string, filename string) (*storage.Object, []byte, error)\n\n\t// GetMeta returns a file's metadata.\n\tGetMeta(url HttpBaseUrl, bucket string, filename string) (*storage.Object, error)\n\n\t// Add creates the specified file.\n\tAdd(bucket string, filename string, contents []byte, meta *storage.Object) error\n\n\t// UpdateMeta updates the given file's metadata.\n\tUpdateMeta(bucket string, filename string, meta *storage.Object, metagen int64) error\n\n\t// Copy copies the file\n\tCopy(srcBucket string, srcFile string, dstBucket string, dstFile string) (bool, error)\n\n\t// Delete deletes the file.\n\tDelete(bucket string, filename string) error\n\n\t// ReadMeta reads the GCS metadata for a file, when you already have file info.\n\tReadMeta(url HttpBaseUrl, bucket string, filename string, fInfo os.FileInfo) (*storage.Object, error)\n\n\t// Walks the given bucket.\n\tWalk(ctx context.Context, bucket string, cb func(ctx context.Context, filename string, fInfo os.FileInfo) error) error\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/util.go",
    "content": "package gcsemu\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"google.golang.org/api/googleapi\"\n)\n\n// jsonRespond json-encodes rsp and writes it to w.  If an error occurs, then it is logged and a 500 error is written to w.\nfunc (g *GcsEmu) jsonRespond(w http.ResponseWriter, rsp interface{}) {\n\t// do NOT write a http status since OK will be the default and this allows the caller to use their own if they want\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\n\tencoder := json.NewEncoder(w)\n\tif err := encoder.Encode(rsp); err != nil {\n\t\tg.log(err, \"failed to send response\")\n\t\thttp.Error(w, \"Internal Server Error\", http.StatusInternalServerError)\n\t}\n}\n\ntype gapiErrorPartial struct {\n\t// Code is the HTTP response status code and will always be populated.\n\tCode int `json:\"code\"`\n\n\t// Message is the server response message and is only populated when\n\t// explicitly referenced by the JSON server response.\n\tMessage string `json:\"message\"`\n\n\tErrors []googleapi.ErrorItem `json:\"errors,omitempty\"`\n}\n\n// gapiError responds to the client with a GAPI error\nfunc (g *GcsEmu) gapiError(w http.ResponseWriter, code int, message string) {\n\tif code == 0 {\n\t\tcode = http.StatusInternalServerError\n\t}\n\tif code != http.StatusNotFound {\n\t\tg.log(errors.New(message), \"responding with HTTP %d\", code)\n\t}\n\tif message == \"\" {\n\t\tmessage = http.StatusText(code)\n\t}\n\n\t// format copied from errorReply struct in google.golang.org/api/googleapi\n\trsp := struct {\n\t\tError gapiErrorPartial `json:\"error\"`\n\t}{\n\t\tError: gapiErrorPartial{\n\t\t\tCode:    code,\n\t\t\tMessage: message,\n\t\t},\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\tw.WriteHeader(code)\n\tenc := json.NewEncoder(w)\n\tenc.SetIndent(\"\", \"  \")\n\t_ = enc.Encode(&rsp)\n}\n\n// mustJson serializes the given value to json, panicking on failure\nfunc mustJson(val interface{}) []byte {\n\tif val == nil {\n\t\treturn []byte(\"null\")\n\t}\n\n\tb, err := json.MarshalIndent(val, \"\", \"  \")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn b\n}\n\n// requestHost returns the host from an http.Request, respecting proxy headers. Works locally with devproxy\n// and gulp proxies as well as in AppEngine (both real GAE and the dev_appserver).\nfunc requestHost(req *http.Request) string {\n\t// proxies like gulp are supposed to accumulate original host, next-step-host, etc in order from\n\t// client-most to server-most in X-ForwardedHost; return the first entry from that if any are listed\n\tif proxyHost := req.Header.Get(\"X-Forwarded-Host\"); proxyHost != \"\" {\n\t\t// Use the first (closest to client) host\n\t\tsplits := strings.SplitN(proxyHost, \",\", 2)\n\t\treturn splits[0]\n\t}\n\n\t// Forwarded is the standardized version of X-Forwarded-Host.\n\tf := parseForwardedHeader(req.Header.Get(\"Forwarded\"))\n\tif len(f.Host) > 0 && len(f.Host[0]) > 0 {\n\t\treturn f.Host[0]\n\t}\n\n\t// Clients that generate HTTP/2 requests should use the :authority header instead\n\t// of Host. See http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3\n\thost := req.Header.Get(\"Authority\")\n\tif len(host) > 0 {\n\t\treturn host\n\t}\n\n\t// Fall back to the host line.\n\treturn req.Host\n}\n\n// forwarded represents the values of a Forwarded HTTP header.\n//\n// For more details, see the RFC: https://tools.ietf.org/html/rfc7239 and\n// MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded.\ntype forwarded struct {\n\tBy    []string\n\tFor   []string\n\tHost  []string\n\tProto []string\n}\n\nvar (\n\tforwardedHostRx = regexp.MustCompile(`(?i)host=(.*?)(?:[,;\\s]|$)`)\n)\n\nfunc removeDoubleQuotes(s string) string {\n\treturn strings.TrimSuffix(strings.TrimPrefix(s, `\"`), `\"`)\n}\n\n// Note: this currently only supports the forwarded.Host field.\nfunc parseForwardedHeader(s string) forwarded {\n\tvar f forwarded\n\n\tif s == \"\" {\n\t\treturn f\n\t}\n\n\tmatches := forwardedHostRx.FindAllStringSubmatch(s, -1)\n\tfor _, m := range matches {\n\t\tif len(m) > 0 {\n\t\t\tf.Host = append(f.Host, removeDoubleQuotes(m[1]))\n\t\t}\n\t}\n\n\treturn f\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsemu/walk.go",
    "content": "package gcsemu\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/emulators/storage/gcsutil\"\n\t\"google.golang.org/api/storage/v1\"\n)\n\n// Iterate over the file system to serve a GCS list-bucket request.\nfunc (g *GcsEmu) makeBucketListResults(ctx context.Context, baseUrl HttpBaseUrl, w http.ResponseWriter, delimiter string, cursor string, prefix string, bucket string, maxResults int) {\n\tvar errAbort = errors.New(\"sentinel error to abort walk\")\n\n\ttype item struct {\n\t\tfilename string\n\t\tfInfo    os.FileInfo\n\t}\n\tvar found []item\n\tvar prefixes []string\n\tseenPrefixes := make(map[string]bool)\n\n\tdbgWalk := func(fmt string, args ...interface{}) {\n\t\tif g.verbose {\n\t\t\tg.log(nil, fmt, args...)\n\t\t}\n\t}\n\n\tmoreResults := false\n\tcount := 0\n\terr := g.store.Walk(ctx, bucket, func(ctx context.Context, filename string, fInfo os.FileInfo) error {\n\t\tdbgWalk(\"walk: %s\", filename)\n\n\t\t// If we're beyond the prefix, we're completely done.\n\t\tif greaterThanPrefix(filename, prefix) {\n\t\t\tdbgWalk(\"%q > prefix=%q aborting\", filename, prefix)\n\t\t\treturn errAbort\n\t\t}\n\n\t\t// In the filesystem implementation, skip any directories strictly less than the cursor or prefix.\n\t\tif fInfo != nil && fInfo.IsDir() {\n\t\t\tif lessThanPrefix(filename, cursor) {\n\t\t\t\tdbgWalk(\"%q < cursor=%q skip dir\", filename, cursor)\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\t\t\tif lessThanPrefix(filename, prefix) {\n\t\t\t\tdbgWalk(\"%q < prefix=%q skip dir\", filename, prefix)\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\t\t\treturn nil // keep going\n\t\t}\n\n\t\t// If the file is <= cursor, or < prefix, skip.\n\t\tif filename <= cursor {\n\t\t\tdbgWalk(\"%q <= cursor=%q skipping\", filename, cursor)\n\t\t\treturn nil\n\t\t}\n\t\tif !strings.HasPrefix(filename, prefix) {\n\t\t\tdbgWalk(\"%q < prefix=%q skipping\", filename, prefix)\n\t\t\treturn nil\n\t\t}\n\n\t\tif count >= maxResults {\n\t\t\tmoreResults = true\n\t\t\treturn errAbort\n\t\t}\n\t\tcount++\n\n\t\tif delimiter != \"\" {\n\t\t\t// See if the filename (beyond the prefix) contains delimiter, if it does, don't record the item,\n\t\t\t// instead record the prefix (including the delimiter).\n\t\t\twithoutPrefix := strings.TrimPrefix(filename, prefix)\n\t\t\tdelimiterPos := strings.Index(withoutPrefix, delimiter)\n\t\t\tif delimiterPos >= 0 {\n\t\t\t\t// Got a hit, reconstruct the item's prefix, including the trailing delimiter\n\t\t\t\titemPrefix := filename[:len(prefix)+delimiterPos+len(delimiter)]\n\t\t\t\tif !seenPrefixes[itemPrefix] {\n\t\t\t\t\tseenPrefixes[itemPrefix] = true\n\t\t\t\t\tprefixes = append(prefixes, itemPrefix)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tfound = append(found, item{\n\t\t\tfilename: filename,\n\t\t\tfInfo:    fInfo,\n\t\t})\n\t\treturn nil\n\t})\n\t// Sentinel error is not an error\n\tif err == errAbort {\n\t\terr = nil\n\t}\n\tif err != nil {\n\t\tif len(found) == 0 {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tg.gapiError(w, http.StatusNotFound, fmt.Sprintf(\"%s not found\", bucket))\n\t\t\t} else {\n\t\t\t\tg.gapiError(w, http.StatusInternalServerError, \"failed to iterate: \"+err.Error())\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\t// return our partial results + the cursor so that the client can retry from this point\n\t\tg.log(nil, \"failed to iterate\")\n\t}\n\n\t// Resolve the found items.\n\tvar items []*storage.Object\n\tfor _, item := range found {\n\t\tif obj, err := g.store.ReadMeta(baseUrl, bucket, item.filename, item.fInfo); err != nil {\n\t\t\t// return our partial results + the cursor so that the client can retry from this point\n\t\t\tg.log(nil, \"failed to resolve: %s\", item.filename)\n\t\t\tbreak\n\t\t} else {\n\t\t\titems = append(items, obj)\n\t\t}\n\t}\n\n\tvar nextPageToken = \"\"\n\tif moreResults && len(items) > 0 {\n\t\tlastItemName := items[len(items)-1].Name\n\t\tnextPageToken = gcsutil.EncodePageToken(lastItemName)\n\t}\n\n\trsp := storage.Objects{\n\t\tKind:          \"storage#objects\",\n\t\tNextPageToken: nextPageToken,\n\t\tItems:         items,\n\t\tPrefixes:      prefixes,\n\t}\n\n\tg.jsonRespond(w, &rsp)\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/counted_lock.go",
    "content": "package gcsutil\n\nimport (\n\t\"context\"\n)\n\ntype countedLock struct {\n\t// a 1 element channel; empty == unlocked, full == locked\n\t// push an element into the channel to lock, remove the element to unlock\n\tch chan struct{}\n\n\t// should only be accessed while the outer _map_ lock is held (not this key lock)\n\trefcount int64\n}\n\nfunc newCountedLock() *countedLock {\n\treturn &countedLock{\n\t\tch:       make(chan struct{}, 1),\n\t\trefcount: 0,\n\t}\n}\n\nfunc (m *countedLock) Lock(ctx context.Context) bool {\n\t// If the context is already cancelled don't even try to lock.\n\tif ctx.Err() != nil {\n\t\treturn false\n\t}\n\tselect {\n\tcase m.ch <- struct{}{}:\n\t\treturn true\n\tcase <-ctx.Done():\n\t\treturn false\n\t}\n}\n\nfunc (m *countedLock) Unlock() {\n\tselect {\n\tcase <-m.ch:\n\t\treturn\n\tdefault:\n\t\tpanic(\"BUG: lock not held\")\n\t}\n}\n\nfunc (m *countedLock) Run(ctx context.Context, f func(ctx context.Context) error) error {\n\tif !m.Lock(ctx) {\n\t\treturn ctx.Err()\n\t}\n\tdefer m.Unlock()\n\treturn f(ctx)\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/doc.go",
    "content": "// Package gcsutil contains some generic utilities to support gcsemu.\n// TODO(dragonsinth): consider open sourcing these separately, or finding open source replacements.\npackage gcsutil\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/gcspagetoken.go",
    "content": "package gcsutil\n\n//go:generate protoc --go_out=. --go_opt=paths=source_relative gcspagetoken.proto\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\t\"google.golang.org/protobuf/proto\"\n)\n\n// EncodePageToken returns a synthetic page token to find files greater than the given string.\n// If this is part of a prefix query, the token should fall within the prefixed range.\n// BRITTLE: relies on a reverse-engineered internal GCS token format, which may be subject to change.\nfunc EncodePageToken(greaterThan string) string {\n\tbytes, err := proto.Marshal(&GcsPageToken{\n\t\tLastFile: greaterThan,\n\t})\n\tif err != nil {\n\t\tpanic(\"could not encode gcsPageToken:\" + err.Error())\n\t}\n\treturn base64.StdEncoding.EncodeToString(bytes)\n}\n\n// DecodePageToken decodes a GCS pageToken to the name of the last file returned.\nfunc DecodePageToken(pageToken string) (string, error) {\n\tbytes, err := base64.StdEncoding.DecodeString(pageToken)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not base64 decode pageToken %s: %w\", pageToken, err)\n\t}\n\tvar message GcsPageToken\n\tif err := proto.Unmarshal(bytes, &message); err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not unmarshal proto: %w\", err)\n\t}\n\n\treturn message.LastFile, nil\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/gcspagetoken.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.31.0\n// \tprotoc        v4.23.4\n// source: gcspagetoken.proto\n\npackage gcsutil\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype GcsPageToken struct {\n\tstate         protoimpl.MessageState\n\tsizeCache     protoimpl.SizeCache\n\tunknownFields protoimpl.UnknownFields\n\n\t// The full name of the last result file, when returned from the server.\n\t// When sent as a cursor, interpreted as \"return files greater than this value\".\n\tLastFile string `protobuf:\"bytes,1,opt,name=LastFile,proto3\" json:\"LastFile,omitempty\"`\n}\n\nfunc (x *GcsPageToken) Reset() {\n\t*x = GcsPageToken{}\n\tif protoimpl.UnsafeEnabled {\n\t\tmi := &file_gcspagetoken_proto_msgTypes[0]\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tms.StoreMessageInfo(mi)\n\t}\n}\n\nfunc (x *GcsPageToken) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GcsPageToken) ProtoMessage() {}\n\nfunc (x *GcsPageToken) ProtoReflect() protoreflect.Message {\n\tmi := &file_gcspagetoken_proto_msgTypes[0]\n\tif protoimpl.UnsafeEnabled && x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GcsPageToken.ProtoReflect.Descriptor instead.\nfunc (*GcsPageToken) Descriptor() ([]byte, []int) {\n\treturn file_gcspagetoken_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *GcsPageToken) GetLastFile() string {\n\tif x != nil {\n\t\treturn x.LastFile\n\t}\n\treturn \"\"\n}\n\nvar File_gcspagetoken_proto protoreflect.FileDescriptor\n\nvar file_gcspagetoken_proto_rawDesc = []byte{\n\t0x0a, 0x12, 0x67, 0x63, 0x73, 0x70, 0x61, 0x67, 0x65, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x70,\n\t0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x67, 0x63, 0x73, 0x75, 0x74, 0x69, 0x6c, 0x22, 0x2a, 0x0a,\n\t0x0c, 0x47, 0x63, 0x73, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1a, 0x0a,\n\t0x08, 0x4c, 0x61, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,\n\t0x08, 0x4c, 0x61, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x42, 0x28, 0x5a, 0x26, 0x65, 0x6e, 0x63,\n\t0x72, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x65, 0x6d, 0x75, 0x6c, 0x61, 0x74,\n\t0x6f, 0x72, 0x73, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x67, 0x63, 0x73, 0x75,\n\t0x74, 0x69, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,\n}\n\nvar (\n\tfile_gcspagetoken_proto_rawDescOnce sync.Once\n\tfile_gcspagetoken_proto_rawDescData = file_gcspagetoken_proto_rawDesc\n)\n\nfunc file_gcspagetoken_proto_rawDescGZIP() []byte {\n\tfile_gcspagetoken_proto_rawDescOnce.Do(func() {\n\t\tfile_gcspagetoken_proto_rawDescData = protoimpl.X.CompressGZIP(file_gcspagetoken_proto_rawDescData)\n\t})\n\treturn file_gcspagetoken_proto_rawDescData\n}\n\nvar file_gcspagetoken_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_gcspagetoken_proto_goTypes = []interface{}{\n\t(*GcsPageToken)(nil), // 0: gcsutil.GcsPageToken\n}\nvar file_gcspagetoken_proto_depIdxs = []int32{\n\t0, // [0:0] is the sub-list for method output_type\n\t0, // [0:0] is the sub-list for method input_type\n\t0, // [0:0] is the sub-list for extension type_name\n\t0, // [0:0] is the sub-list for extension extendee\n\t0, // [0:0] is the sub-list for field type_name\n}\n\nfunc init() { file_gcspagetoken_proto_init() }\nfunc file_gcspagetoken_proto_init() {\n\tif File_gcspagetoken_proto != nil {\n\t\treturn\n\t}\n\tif !protoimpl.UnsafeEnabled {\n\t\tfile_gcspagetoken_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {\n\t\t\tswitch v := v.(*GcsPageToken); i {\n\t\t\tcase 0:\n\t\t\t\treturn &v.state\n\t\t\tcase 1:\n\t\t\t\treturn &v.sizeCache\n\t\t\tcase 2:\n\t\t\t\treturn &v.unknownFields\n\t\t\tdefault:\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: file_gcspagetoken_proto_rawDesc,\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_gcspagetoken_proto_goTypes,\n\t\tDependencyIndexes: file_gcspagetoken_proto_depIdxs,\n\t\tMessageInfos:      file_gcspagetoken_proto_msgTypes,\n\t}.Build()\n\tFile_gcspagetoken_proto = out.File\n\tfile_gcspagetoken_proto_rawDesc = nil\n\tfile_gcspagetoken_proto_goTypes = nil\n\tfile_gcspagetoken_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/gcspagetoken.proto",
    "content": "syntax = \"proto3\";\n\npackage gcsutil;\n\noption go_package = \"encr.dev/pkg/emulators/storage/gcsutil\";\n\nmessage GcsPageToken{\n  // The full name of the last result file, when returned from the server.\n  // When sent as a cursor, interpreted as \"return files greater than this value\".\n  string LastFile = 1;\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/gcspagetoken_test.go",
    "content": "package gcsutil\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\n// TODO(dragonsinth): it would be nice to have an integration test that hits real GCS with known data stored.\n\n// TestGcsTokenGen tests that we produce expected GCS tokens that match real data we collected.\nfunc TestGcsTokenGen(t *testing.T) {\n\ttcs := []struct {\n\t\tlastFile string\n\t\tcursor   string\n\t}{\n\t\t{\n\t\t\tlastFile: \"containers/images/4dcc5142000d12f1a0f67c1e95df4035ca0ebba70117cc04101e53422d391d61/json\",\n\t\t\tcursor:   \"Cldjb250YWluZXJzL2ltYWdlcy80ZGNjNTE0MjAwMGQxMmYxYTBmNjdjMWU5NWRmNDAzNWNhMGViYmE3MDExN2NjMDQxMDFlNTM0MjJkMzkxZDYxL2pzb24=\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:0e89fc4aeb48f92acff2dddaf610b2ceea5d76a93a44d4c20b31e69d1ed68c10\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6MGU4OWZjNGFlYjQ4ZjkyYWNmZjJkZGRhZjYxMGIyY2VlYTVkNzZhOTNhNDRkNGMyMGIzMWU2OWQxZWQ2OGMxMA==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:2072bc4567e1f13081af323d046f39453f010471701fa11fc50b786b60512e99\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6MjA3MmJjNDU2N2UxZjEzMDgxYWYzMjNkMDQ2ZjM5NDUzZjAxMDQ3MTcwMWZhMTFmYzUwYjc4NmI2MDUxMmU5OQ==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:43ecade58b2ddff87a696fada3970491de28c8ea1dca09c988b447b5c5a56412\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NDNlY2FkZTU4YjJkZGZmODdhNjk2ZmFkYTM5NzA0OTFkZTI4YzhlYTFkY2EwOWM5ODhiNDQ3YjVjNWE1NjQxMg==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:57076bf87737c2f448e75324ceba121b3b90372ab913f604f928a40da4ebddc7\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NTcwNzZiZjg3NzM3YzJmNDQ4ZTc1MzI0Y2ViYTEyMWIzYjkwMzcyYWI5MTNmNjA0ZjkyOGE0MGRhNGViZGRjNw==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:6f73a9b0052f169d8296382660a31050004b691f3e6252008545a3dcb7371a49\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NmY3M2E5YjAwNTJmMTY5ZDgyOTYzODI2NjBhMzEwNTAwMDRiNjkxZjNlNjI1MjAwODU0NWEzZGNiNzM3MWE0OQ==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:765b6a129bd04f06c876f3d5b5a346e71a72fae522b230215f67375ccb659a11\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6NzY1YjZhMTI5YmQwNGYwNmM4NzZmM2Q1YjVhMzQ2ZTcxYTcyZmFlNTIyYjIzMDIxNWY2NzM3NWNjYjY1OWExMQ==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:973d921f391393e65d20a6e990e8ad5aa1129681ab8c54bf59c9192b809594c4\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6OTczZDkyMWYzOTEzOTNlNjVkMjBhNmU5OTBlOGFkNWFhMTEyOTY4MWFiOGM1NGJmNTljOTE5MmI4MDk1OTRjNA==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:a9d5802ef798d88c0f4f9dc0094249db5e26d8a8a18ba4c2194aab4a44983d2f\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6YTlkNTgwMmVmNzk4ZDg4YzBmNGY5ZGMwMDk0MjQ5ZGI1ZTI2ZDhhOGExOGJhNGMyMTk0YWFiNGE0NDk4M2QyZg==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:b5714cf3ed3a6b5f1fbc736ef0d5673c5637ccb14d53a23b8728dd828f21a22d\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6YjU3MTRjZjNlZDNhNmI1ZjFmYmM3MzZlZjBkNTY3M2M1NjM3Y2NiMTRkNTNhMjNiODcyOGRkODI4ZjIxYTIyZA==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:bfdef622d405cb35c466aa29ea7411fd594e7985127bec4e8080572d7ef45cfd\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6YmZkZWY2MjJkNDA1Y2IzNWM0NjZhYTI5ZWE3NDExZmQ1OTRlNzk4NTEyN2JlYzRlODA4MDU3MmQ3ZWY0NWNmZA==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:da543d0747020a528b8eee057912f6bd07e28f9c006606da28c82c70dac962e2\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6ZGE1NDNkMDc0NzAyMGE1MjhiOGVlZTA1NzkxMmY2YmQwN2UyOGY5YzAwNjYwNmRhMjhjODJjNzBkYWM5NjJlMg==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/images/sha256:ee73b32b0f0a9dafabd9445a3380094f984858e79c85eafde56c2e037c039c6a\",\n\t\t\tcursor:   \"Clljb250YWluZXJzL2ltYWdlcy9zaGEyNTY6ZWU3M2IzMmIwZjBhOWRhZmFiZDk0NDVhMzM4MDA5NGY5ODQ4NThlNzljODVlYWZkZTU2YzJlMDM3YzAzOWM2YQ==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/repositories/library/cpu-test/tag_v1\",\n\t\t\tcursor:   \"Ci9jb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2NwdS10ZXN0L3RhZ192MQ==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/repositories/library/dns-test/manifest_sha256:9759da4d052f154ba5d0ea32bf0404f442082e3dbad3f3cf6dc6529c6575aec2\",\n\t\t\tcursor:   \"Cnljb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2Rucy10ZXN0L21hbmlmZXN0X3NoYTI1Njo5NzU5ZGE0ZDA1MmYxNTRiYTVkMGVhMzJiZjA0MDRmNDQyMDgyZTNkYmFkM2YzY2Y2ZGM2NTI5YzY1NzVhZWMy\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/repositories/library/dns-test/manifest_sha256:ee49d4935c33260d84499e38dbd5ec3f426c9a2a7e100a901267c712874e4c1d\",\n\t\t\tcursor:   \"Cnljb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2Rucy10ZXN0L21hbmlmZXN0X3NoYTI1NjplZTQ5ZDQ5MzVjMzMyNjBkODQ0OTllMzhkYmQ1ZWMzZjQyNmM5YTJhN2UxMDBhOTAxMjY3YzcxMjg3NGU0YzFk\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/repositories/library/dns-test/tag_v2\",\n\t\t\tcursor:   \"Ci9jb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2Rucy10ZXN0L3RhZ192Mg==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/repositories/library/kapi-test/manifest_sha256:ca6d9c13fba12363760e6d2495811081b1d2e6fcbf974e551605b65cb5b0a94e\",\n\t\t\tcursor:   \"Cnpjb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L2thcGktdGVzdC9tYW5pZmVzdF9zaGEyNTY6Y2E2ZDljMTNmYmExMjM2Mzc2MGU2ZDI0OTU4MTEwODFiMWQyZTZmY2JmOTc0ZTU1MTYwNWI2NWNiNWIwYTk0ZQ==\",\n\t\t},\n\t\t{\n\t\t\tlastFile: \"containers/repositories/library/memclient-test/manifest_sha256:7b7979b351c9a019062446c1f033b2f8491868cf943758f006ae219eca231e01\",\n\t\t\tcursor:   \"Cn9jb250YWluZXJzL3JlcG9zaXRvcmllcy9saWJyYXJ5L21lbWNsaWVudC10ZXN0L21hbmlmZXN0X3NoYTI1Njo3Yjc5NzliMzUxYzlhMDE5MDYyNDQ2YzFmMDMzYjJmODQ5MTg2OGNmOTQzNzU4ZjAwNmFlMjE5ZWNhMjMxZTAx\",\n\t\t},\n\t}\n\n\tfor i, tc := range tcs {\n\t\tactualCursor := EncodePageToken(tc.lastFile)\n\t\tassert.Equal(t, tc.cursor, actualCursor, \"case %d\", i)\n\n\t\tlastFile, err := DecodePageToken(actualCursor)\n\t\tassert.NilError(t, err, \"case %i: failed to decode\", i)\n\t\tassert.Equal(t, tc.lastFile, lastFile, \"case %d\", i)\n\t}\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/transient_lock_map.go",
    "content": "package gcsutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n)\n\n// TransientLockMap is a map of mutexes that is safe for concurrent access.  It does not bother to save mutexes after\n// they have been unlocked, and thus this data structure is best for situations where the space of keys is very large.\n// If the space of keys is small then it may be inefficient to constantly recreate mutexes whenever they are needed.\ntype TransientLockMap struct {\n\tmu    sync.Mutex              // the mutex that locks the map\n\tlocks map[string]*countedLock // all locks that are currently held\n}\n\n// NewTransientLockMap returns a new TransientLockMap.\nfunc NewTransientLockMap() *TransientLockMap {\n\treturn &TransientLockMap{\n\t\tlocks: make(map[string]*countedLock),\n\t}\n}\n\n// Lock acquires the lock for the specified key and returns true, unless the context finishes before the lock could be\n// acquired, in which case false is returned.\nfunc (l *TransientLockMap) Lock(ctx context.Context, key string) bool {\n\tlock := func() *countedLock {\n\t\t// If there is high lock contention, we could use a readonly lock to check if the lock is already in the map (and\n\t\t// thus no map writes are necessary), but this is complicated enough as it is so we skip that optimization for now.\n\t\tl.mu.Lock()\n\t\tdefer l.mu.Unlock()\n\n\t\t// Check if there is already a lock for this key.\n\t\tlock, ok := l.locks[key]\n\t\tif !ok {\n\t\t\t// no lock yet, so make one and add it to the map\n\t\t\tlock = newCountedLock()\n\t\t\tl.locks[key] = lock\n\t\t}\n\n\t\t// Order is very important here.  First we have to increment the refcount while we still have the map locked; this\n\t\t// will prevent anyone else from evicting this lock after we unlock the map but before we lock the key.  Second we\n\t\t// have to unlock the map _before_ we start trying to lock the key (because locking the key could take a long time\n\t\t// and we don't want to keep the map locked that whole time).\n\t\tlock.refcount++ // incremented while holding _map_ lock\n\t\treturn lock\n\t}()\n\n\tif !lock.Lock(ctx) {\n\t\tl.returnLockObj(key, lock)\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Unlock unlocks the lock for the specified key. Panics if the lock is not currently held.\nfunc (l *TransientLockMap) Unlock(key string) {\n\tlock := func() *countedLock {\n\t\tl.mu.Lock()\n\t\tdefer l.mu.Unlock()\n\n\t\tlock, ok := l.locks[key]\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"lock not held for key %s\", key))\n\t\t}\n\t\treturn lock\n\t}()\n\n\tlock.Unlock()\n\tl.returnLockObj(key, lock)\n}\n\n// Run runs the given callback while holding the lock, unless the context finishes before the lock could be\n// acquired, in which case the context error is returned.\nfunc (l *TransientLockMap) Run(ctx context.Context, key string, f func(ctx context.Context) error) error {\n\tif !l.Lock(ctx, key) {\n\t\treturn ctx.Err()\n\t}\n\tdefer l.Unlock(key)\n\treturn f(ctx)\n}\n\nfunc (l *TransientLockMap) returnLockObj(key string, lock *countedLock) {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\tlock.refcount--\n\tif lock.refcount < 0 {\n\t\tpanic(fmt.Sprintf(\"BUG: somehow the lock.refcount for %q dropped to %d\", key, lock.refcount))\n\t}\n\tif lock.refcount == 0 {\n\t\tdelete(l.locks, key)\n\t}\n}\n"
  },
  {
    "path": "pkg/emulators/storage/gcsutil/transient_lock_map_test.go",
    "content": "package gcsutil\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc (l *TransientLockMap) len() int {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\treturn len(l.locks)\n}\n\nfunc TestTransientLockMapBasics(t *testing.T) {\n\tm := NewTransientLockMap()\n\tupstream := make(chan int, 1)\n\tdownstream := make(chan int, 1)\n\n\twaitFor := func(c <-chan int, expected int) {\n\t\tval := <-c\n\t\tassert.Equal(t, expected, val, \"got an unexpected value from a channel\")\n\t}\n\n\treadSignalNow := func(c <-chan int, expected int, msg string) {\n\t\tselect {\n\t\tcase val := <-c:\n\t\t\tassert.Equal(t, expected, val, \"got an unexpected value from a channel\")\n\t\tdefault:\n\t\t\tt.Fatal(msg)\n\t\t}\n\t}\n\n\t// wrt upstream and downstream, this goroutine is \"above\" the main thread, so it writes to downstream and reads from\n\t// upstream\n\tgo func() {\n\t\tlockWithin(t, m, \"foo\", 10*time.Millisecond)\n\t\tdownstream <- 1\n\t\twaitFor(upstream, 2)\n\t\tdownstream <- 3\n\t\tm.Unlock(\"foo\")\n\t}()\n\n\t// wait until the other goroutine has locked \"foo\"\n\twaitFor(downstream, 1)\n\n\t// prove that we can lock other keys without a problem\n\tlockWithin(t, m, \"bar\", 10*time.Millisecond)\n\tlockWithin(t, m, \"baz\", 10*time.Millisecond)\n\n\tassert.Equal(t, 3, m.len(), \"wrong number of internal locks\")\n\n\t// start trying to lock \"foo\" which will block until we signal the other goroutine to unlock it\n\ttime.AfterFunc(20*time.Millisecond, func() { upstream <- 2 })\n\tlockWithin(t, m, \"foo\", 200*time.Millisecond)\n\n\t// there better be a 3 already queued up in the downstream, otherwise we locked too fast\n\treadSignalNow(downstream, 3, \"locked \\\"foo\\\" before the other goroutine unlocked it...\")\n\n\tassert.Equal(t, 3, m.len(), \"wrong number of internal locks\")\n\n\t// we can unlock out of order, and locks go away as we unlock them\n\tm.Unlock(\"bar\")\n\tassert.Equal(t, 2, m.len(), \"wrong number of internal locks\")\n\n\tm.Unlock(\"baz\")\n\tassert.Equal(t, 1, m.len(), \"wrong number of internal locks\")\n\n\tm.Unlock(\"foo\")\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n}\n\nfunc TestTransientLockMapBadUnlock(t *testing.T) {\n\t// call a function, and fail the test if the function doesn't panic\n\tindex := 0\n\tshouldPanic := func(f func()) {\n\t\tindex++\n\t\tdefer func() {\n\t\t\tif recovered := recover(); recovered != nil {\n\t\t\t\t// ok, all is well\n\t\t\t} else {\n\t\t\t\t// we were supposed to panic but didn't - fail the test!\n\t\t\t\tt.Fatalf(\"test #%d did not panic as expected...\", index)\n\t\t\t}\n\t\t}()\n\t\tf()\n\t}\n\n\t// unlocking a key that has never been referenced\n\tm := NewTransientLockMap()\n\tshouldPanic(func() {\n\t\tm.Unlock(\"foo\")\n\t})\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n\n\t// double-unlocking a key in the same goroutine\n\tm = NewTransientLockMap()\n\tshouldPanic(func() {\n\t\tassertLock(t, m, \"foo\")\n\t\tm.Unlock(\"foo\")\n\t\tm.Unlock(\"foo\")\n\t})\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n\n\t// double-unlocking a key across 2 goroutines\n\tm = NewTransientLockMap()\n\tshouldPanic(func() {\n\t\tsignal := make(chan struct{})\n\n\t\tgo func() {\n\t\t\tassertLock(t, m, \"foo\")\n\t\t\tm.Unlock(\"foo\")\n\t\t\tclose(signal)\n\t\t}()\n\n\t\t<-signal\n\t\tm.Unlock(\"foo\")\n\t})\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n}\n\nfunc TestTransientLockMapSequence(t *testing.T) {\n\tm := NewTransientLockMap()\n\n\t// signals\n\tpartnerAboutToLock := make(chan struct{})\n\tpartnerGotLock := make(chan struct{})\n\n\tassertLock(t, m, \"foo\")\n\tgo func() {\n\t\tclose(partnerAboutToLock)\n\t\tassertLock(t, m, \"foo\")\n\t\tclose(partnerGotLock)\n\t\ttime.Sleep(125 * time.Millisecond)\n\t\tm.Unlock(\"foo\")\n\t}()\n\n\t<-partnerAboutToLock\n\ttime.Sleep(100 * time.Millisecond) // give our partner time to actually call Lock()\n\tm.Unlock(\"foo\")\n\n\t<-partnerGotLock\n\tstart := time.Now()\n\tassertLock(t, m, \"foo\")\n\n\t// ensure that the prior Lock() call actually blocked and waited for a while, as intended\n\tif d := time.Since(start); d < 100*time.Millisecond {\n\t\tt.Fatalf(\"Lock acquired too fast (%s)\", d)\n\t}\n\tm.Unlock(\"foo\")\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n}\n\nfunc TestTransientLockMapContention(t *testing.T) {\n\tm := NewTransientLockMap()\n\n\tvar wg sync.WaitGroup\n\n\tassertLock(t, m, \"foo\")\n\tassertLock(t, m, \"bar\")\n\n\tfor i := 0; i < 100; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tassertLock(t, m, \"foo\")\n\t\t\tm.Unlock(\"foo\")\n\t\t\tassertLock(t, m, \"bar\")\n\t\t\tm.Unlock(\"bar\")\n\t\t}()\n\t}\n\n\ttime.Sleep(30 * time.Millisecond)\n\tm.Unlock(\"bar\")\n\tm.Unlock(\"foo\")\n\n\twg.Wait()\n\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n}\n\nfunc TestTransientLockMapTimeout(t *testing.T) {\n\tm := NewTransientLockMap()\n\n\tassertLock(t, m, \"foo\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)\n\tdefer cancel()\n\n\tres := m.Lock(ctx, \"foo\")\n\tassert.Equal(t, false, res, \"lock foo (2)\")\n\n\tm.Unlock(\"foo\")\n\tlockWithin(t, m, \"foo\", 10*time.Millisecond) // should be able to lock near instantly\n\n\tctx2, cancel := context.WithCancel(context.Background())\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tres := m.Lock(ctx2, \"foo\")\n\t\tassert.Equal(t, false, res, \"lock foo (4)\")\n\t\tclose(done)\n\t}()\n\n\ttime.Sleep(25 * time.Millisecond)\n\n\t// goroutine should not be done yet (i.e. Lock() should still be blocked)\n\tselect {\n\tcase <-done:\n\t\tt.Fatal(\"done should not be closed yet!\")\n\tdefault:\n\t}\n\n\tcancel()\n\n\tselect {\n\tcase <-time.After(25 * time.Millisecond):\n\t\tt.Fatal(\"timeout waiting for done channel to close\")\n\tcase <-done:\n\t\t// ok\n\t}\n\n\t// finally, verify that we can unlock and lock still\n\tm.Unlock(\"foo\")\n\tlockWithin(t, m, \"foo\", 10*time.Millisecond) // should be able to lock near instantly\n\tm.Unlock(\"foo\")\n\n\tassert.Equal(t, 0, m.len(), \"wrong number of internal locks\")\n\n\t// We should not be able to lock with an already-cancelled context.\n\tfor i := 0; i < 10; i++ {\n\t\tassert.Assert(t, !m.Lock(ctx2, \"foo\"), \"lock foo (final) expected canceled\")\n\t}\n}\n\nfunc TestTransientLockMapRun(t *testing.T) {\n\tm := NewTransientLockMap()\n\n\tstart := make(chan struct{})\n\tvar wgSucc sync.WaitGroup\n\tvar wgFail sync.WaitGroup\n\tvar nFail int32\n\tvar nSucc int32\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tfor i := 0; i < 10; i++ {\n\t\tif i == 0 {\n\t\t\twgSucc.Add(1)\n\t\t} else {\n\t\t\twgFail.Add(1)\n\t\t}\n\t\tgo func() {\n\t\t\t<-start\n\t\t\terr := m.Run(ctx, \"foo\", func(ctx context.Context) error {\n\t\t\t\t// Whoever got the lock should cancel everyone else.\n\t\t\t\tcancel()\n\t\t\t\twgFail.Wait()\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err == nil {\n\t\t\t\tdefer wgSucc.Done()\n\t\t\t\tatomic.AddInt32(&nSucc, 1)\n\t\t\t} else {\n\t\t\t\tdefer wgFail.Done()\n\t\t\t\tif err == context.Canceled {\n\t\t\t\t\tatomic.AddInt32(&nFail, 1)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"expected canceled, got %T: %s\", err, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\tclose(start)\n\twgSucc.Wait()\n\n\tassert.Equal(t, int32(1), nSucc, \"wrong # success\")\n\tassert.Equal(t, int32(9), nFail, \"wrong # failures\")\n}\n\nfunc assertLock(t *testing.T, m *TransientLockMap, key string) {\n\tassert.Assert(t, m.Lock(context.Background(), key), \"should have locked\")\n}\n\n// grabs a lock, panicking if this takes longer than expected\nfunc lockWithin(t *testing.T, m *TransientLockMap, key string, timeout time.Duration) {\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\tassert.Assert(t, m.Lock(ctx, key), \"should have locked\")\n}\n"
  },
  {
    "path": "pkg/encorebuild/buildconf/config.go",
    "content": "package buildconf\n\nimport (\n\t\"runtime\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/pkg/encorebuild/buildutil\"\n\t\"encr.dev/pkg/option\"\n)\n\ntype Config struct {\n\t// Logger to use.\n\tLog zerolog.Logger\n\n\t// Target OS and architecture (in GOOS/GOARCH format)\n\tOS   string\n\tArch string\n\n\t// Release is true if this is a release build.\n\tRelease bool\n\n\t// The version being built.\n\tVersion string\n\n\t// RepoDir is the path to the encore repo on the filesystem.\n\tRepoDir string\n\n\t// CacheDir is the cache dir to use for the build.\n\tCacheDir string\n\n\t// The path to the MacOS SDK. Must be set for cross-compiles to macOS.\n\tMacSDKPath option.Option[string]\n\n\t// Whether or not to publish packages to NPM. Only used if Release is also true.\n\tPublishNPMPackages bool\n\n\t// Whether to copy the built native module back to the repo dir.\n\tCopyToRepo bool\n\n\t// RustBuilder will override the automatic builder selection if set.\n\tRustBuilder option.Option[string]\n}\n\n// IsCross reports whether the build is a cross-compile.\nfunc (cfg *Config) IsCross() bool {\n\treturn cfg.OS != runtime.GOOS || cfg.Arch != runtime.GOARCH\n}\n\nfunc (cfg *Config) CrossMacSDKPath() string {\n\tif cfg.OS != \"darwin\" {\n\t\treturn \"\"\n\t}\n\tval, ok := cfg.MacSDKPath.Get()\n\tif !ok {\n\t\tbuildutil.Bailf(\"macOS SDK path must be set for cross-compiles to macOS\")\n\t}\n\treturn val\n}\n\n// Exe returns the executable file suffix for the target OS.\nfunc (cfg *Config) Exe() string {\n\tif cfg.OS == \"windows\" {\n\t\treturn \".exe\"\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/encorebuild/buildutil/buildutil.go",
    "content": "package buildutil\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"sync\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\ntype Bailout struct {\n\tErr error\n}\n\nfunc (b Bailout) Error() string {\n    return b.Err.Error()\n}\n\nfunc Bail(err error) {\n\tpanic(Bailout{err})\n}\n\nfunc Bailf(format string, args ...any) {\n\tBail(fmt.Errorf(format, args...))\n}\n\nfunc Must[T any](val T, err error) T {\n\tif err != nil {\n\t\tBail(err)\n\t}\n\treturn val\n}\n\nfunc Check(err error) {\n\tif err != nil {\n\t\tBail(err)\n\t}\n}\n\nfunc TarGzip(srcDirectory string, tarFile string) error {\n\t// Create the tar.gz file from the src directory\n\tcmd := exec.Command(\"tar\", \"-czf\", tarFile, \"-C\", srcDirectory, \".\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to create tar.gz: %s\", out)\n\t}\n\treturn nil\n}\n\n// RunParallel runs the given functions in parallel, bailing with the first error\nfunc RunParallel(functions ...func()) {\n\tvar wg sync.WaitGroup\n\twg.Add(len(functions))\n\tvar firstErr error\n\tvar mu sync.Mutex\n\n\tfor _, f := range functions {\n\t\tf := f\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tdefer func() {\n\t\t\t\tif err := recover(); err != nil {\n\t\t\t\t\tif b, ok := err.(Bailout); ok {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\tif firstErr == nil {\n\t\t\t\t\t\t\tfirstErr = b.Err\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tf()\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\tCheck(firstErr)\n}\n"
  },
  {
    "path": "pkg/encorebuild/cmd/build-local-binary/build-local-binary.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/encorebuild\"\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t\"encr.dev/pkg/option\"\n)\n\nfunc join(strs ...string) string {\n\treturn filepath.Join(strs...)\n}\n\nvar archFlag = flag.String(\"arch\", runtime.GOARCH, \"the architecture to target\")\nvar osFlag = flag.String(\"os\", runtime.GOOS, \"the operating system to target\")\nvar builderFlag = flag.String(\"builder\", \"\", \"the builder to use\")\n\nfunc main() {\n\tbinary := os.Args[1]\n\tif binary == \"\" || binary[0] == '-' {\n\t\tlog.Fatal().Msg(\"expected binary name as first argument\")\n\t}\n\tos.Args = os.Args[1:]\n\tflag.Parse()\n\tlog.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger()\n\n\troot, err := os.Getwd()\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get working directory\")\n\t} else if _, err := os.Stat(join(root, \".git\")); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"expected to run build-local-binary from encr.dev repository root\")\n\t}\n\n\tuserCacheDir, err := os.UserCacheDir()\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get user cache dir\")\n\t}\n\tcacheDir := filepath.Join(userCacheDir, \"encore-build-cache\")\n\n\tbuilder := option.None[string]()\n\n\tif builderFlag != nil && *builderFlag != \"\" {\n\t\tbuilder = option.Some(*builderFlag)\n\t}\n\n\tcfg := &buildconf.Config{\n\t\tLog:         log.Logger,\n\t\tOS:          *osFlag,\n\t\tArch:        *archFlag,\n\t\tRelease:     false,\n\t\tVersion:     version.Version,\n\t\tRepoDir:     root,\n\t\tCacheDir:    cacheDir,\n\t\tMacSDKPath:  option.None[string](),\n\t\tCopyToRepo:  true,\n\t\tRustBuilder: builder,\n\t}\n\n\tswitch binary {\n\tcase \"all\":\n\t\tencorebuild.NewJSRuntimeBuilder(cfg).Build()\n\t\tencorebuild.NewSupervisorBuilder(cfg).Build()\n\tcase \"supervisor-encore\":\n\t\tencorebuild.NewSupervisorBuilder(cfg).Build()\n\tcase \"encore-runtime.node\":\n\t\tencorebuild.NewJSRuntimeBuilder(cfg).Build()\n\tdefault:\n\t\tlog.Fatal().Msgf(\"unknown binary %s\", binary)\n\t}\n}\n"
  },
  {
    "path": "pkg/encorebuild/cmd/make-release/make-release.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime/debug\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/encorebuild\"\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t\"encr.dev/pkg/encorebuild/buildutil\"\n\t\"encr.dev/pkg/option\"\n)\n\nfunc join(strs ...string) string {\n\treturn filepath.Join(strs...)\n}\n\nfunc main() {\n\tlog.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger()\n\n\tdst := flag.String(\"dst\", \"\", \"build destination\")\n\tversionStr := flag.String(\"v\", \"\", \"version number\")\n\tonlyBuild := flag.String(\"only\", \"\", \"build only the valid target ('darwin-arm64' or 'darwin' or 'arm64' or '' for all)\")\n\tpublishNPM := flag.Bool(\"publish-npm\", false, \"publish packages to npm\")\n\tflag.Parse()\n\tif *dst == \"\" || *versionStr == \"\" {\n\t\tlog.Fatal().Msgf(\"missing -dst %q or -v %q\", *dst, *versionStr)\n\t}\n\n\tif (*versionStr)[0] != 'v' {\n\t\tlog.Fatal().Msg(\"version must start with 'v'\")\n\t}\n\tswitch version.ChannelFor(*versionStr) {\n\tcase version.GA, version.Beta, version.Nightly, version.DevBuild:\n\t\t// no-op\n\tdefault:\n\t\tlog.Fatal().Msgf(\"unknown version channel for %s\", *versionStr)\n\t}\n\n\troot, err := os.Getwd()\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get working directory\")\n\t} else if _, err := os.Stat(join(root, \"go.mod\")); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"expected to run make-release.go from encr.dev repository root\")\n\t}\n\n\tuserCacheDir, err := os.UserCacheDir()\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get user cache dir\")\n\t}\n\tcacheDir := filepath.Join(userCacheDir, \"encore-build-cache\")\n\n\t*dst, err = filepath.Abs(*dst)\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get absolute path to destination\")\n\t}\n\n\t// Prepare the target directory.\n\tif err := os.RemoveAll(*dst); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to remove existing target dir\")\n\t} else if err := os.MkdirAll(filepath.Join(*dst, \"artifacts\"), 0755); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to create target dir\")\n\t}\n\n\ttype buildTarget struct {\n\t\tOS   string\n\t\tArch string\n\t}\n\n\ttargets := []buildTarget{\n\t\t{\"darwin\", \"amd64\"},\n\t\t{\"darwin\", \"arm64\"},\n\t\t{\"linux\", \"amd64\"},\n\t\t{\"linux\", \"arm64\"},\n\t\t{\"windows\", \"amd64\"},\n\t}\n\n\tvar parallelFuncs []func()\n\t// Give them the common settings\n\tfor _, t := range targets {\n\t\tif *onlyBuild != \"\" && !(*onlyBuild == fmt.Sprintf(\"%s-%s\", t.OS, t.Arch) ||\n\t\t\t*onlyBuild == t.OS ||\n\t\t\t*onlyBuild == t.Arch) {\n\t\t\tcontinue\n\t\t}\n\n\t\tb := &encorebuild.DistBuilder{\n\t\t\tCfg: &buildconf.Config{\n\t\t\t\tLog:                log.With().Str(\"os\", t.OS).Str(\"arch\", t.Arch).Logger(),\n\t\t\t\tOS:                 t.OS,\n\t\t\t\tArch:               t.Arch,\n\t\t\t\tRelease:            true,\n\t\t\t\tVersion:            *versionStr,\n\t\t\t\tRepoDir:            root,\n\t\t\t\tCacheDir:           cacheDir,\n\t\t\t\tMacSDKPath:         option.Some(\"/sdk\"),\n\t\t\t\tPublishNPMPackages: *publishNPM,\n\t\t\t},\n\t\t\tDistBuildDir:     join(*dst, t.OS+\"_\"+t.Arch),\n\t\t\tArtifactsTarFile: join(*dst, \"artifacts\", \"encore-\"+*versionStr+\"-\"+t.OS+\"_\"+t.Arch+\".tar.gz\"),\n\t\t}\n\n\t\tparallelFuncs = append(parallelFuncs, b.Build)\n\t}\n\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tif b, ok := err.(buildutil.Bailout); ok {\n\t\t\t\tlog.Fatal().Err(b.Err).Msg(\"failed to build\")\n\t\t\t} else {\n\t\t\t\tstack := debug.Stack()\n\t\t\t\tlog.Fatal().Msgf(\"failed to build: unrecovered panic: %v: \\n%s\", err, stack)\n\t\t\t}\n\t\t}\n\t}()\n\n\tbuildutil.RunParallel(parallelFuncs...)\n\tlog.Info().Msg(\"all distributions built successfully\")\n\n\tif *publishNPM {\n\t\tencorebuild.PublishNPMPackages(root, *versionStr)\n\t}\n\tlog.Info().Msg(\"successfully published NPM package\")\n}\n"
  },
  {
    "path": "pkg/encorebuild/compile/compile.go",
    "content": "package compile\n\nimport (\n\tosPkg \"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t. \"encr.dev/pkg/encorebuild/buildutil\"\n)\n\n// GoBinary compiles a Go binary for the given OS and architecture with GCP enabled\n//\n// This file was inspired by the blog post: https://lucor.dev/post/cross-compile-golang-fyne-project-using-zig/\nfunc GoBinary(cfg *buildconf.Config, outputPath string, entrypointPkg string, ldFlags []string) {\n\tcc, cxx, compilerEnvs, compilerLDFlags := compilerSettings(cfg)\n\tif cfg.OS == \"windows\" && !strings.HasSuffix(outputPath, \".exe\") {\n\t\toutputPath += \".exe\"\n\t}\n\n\tcombinedLDFlags := append(append([]string{}, compilerLDFlags...), ldFlags...)\n\n\tbaseEnvs := goBaseEnvs(cfg)\n\tenvs := append(baseEnvs,\n\t\t\"CGO_ENABLED=1\",\n\t\t\"CC=\"+cc,\n\t\t\"CXX=\"+cxx,\n\t)\n\tenvs = append(envs, compilerEnvs...)\n\n\t// Build the go build args\n\targs := []string{\"build\",\n\t\t\"-trimpath\",\n\t\t\"-tags\", \"netgo\", // Always force netgo otherwise we end up with segfaults on MacOS\n\t}\n\tif len(combinedLDFlags) > 0 {\n\t\targs = append(args, \"-ldflags=\"+strings.Join(combinedLDFlags, \" \"))\n\t}\n\tif cfg.OS == \"darwin\" {\n\t\targs = append(args, \"-buildmode=pie\")\n\t}\n\targs = append(args,\n\t\t\"-o\", outputPath,\n\t\tentrypointPkg,\n\t)\n\n\t// Build the command\n\tcmd := exec.Command(\"go\", args...)\n\tcmd.Env = envs\n\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tBailf(\"failed to compile go binary: %v: %s\", err, string(out))\n\t}\n}\n\nfunc goBaseEnvs(cfg *buildconf.Config) []string {\n\tgocache := filepath.Join(cfg.CacheDir, \"go\", cfg.OS, cfg.Arch)\n\tCheck(osPkg.MkdirAll(gocache, 0755))\n\treturn append(osPkg.Environ(),\n\t\t\"GOOS=\"+cfg.OS,\n\t\t\"GOARCH=\"+cfg.Arch,\n\t\t\"GOCACHE=\"+gocache,\n\t)\n}\n\n// CompileRustBinary compiles a Rust binary for the given OS and architecture\n//\n// We're using zigbuild to perform easy cross compiling\nfunc RustBinary(cfg *buildconf.Config, artifactPath, outputPath string, cratePath string, libc string, extraFeatures []string, extraEnvVars ...string) {\n\tif cfg.OS == \"windows\" {\n\t\tif !strings.HasSuffix(artifactPath, \".dll\") {\n\t\t\toutputPath += \".exe\"\n\t\t\tartifactPath += \".exe\"\n\t\t}\n\t}\n\n\tuseZig := cfg.IsCross() || cfg.Release\n\tuseCross := false\n\n\tif cfg.IsCross() && runtime.GOOS == \"darwin\" {\n\t\t// check is cross is installed\n\t\t_, err := exec.LookPath(\"cross\")\n\t\tif err == nil {\n\t\t\tuseCross = true\n\t\t\tuseZig = false\n\t\t}\n\t}\n\n\tif builder, ok := cfg.RustBuilder.Get(); ok {\n\t\tswitch builder {\n\t\tcase \"cargo\":\n\t\t\tuseCross = false\n\t\t\tuseZig = false\n\t\tcase \"zigbuild\":\n\t\t\tuseCross = false\n\t\t\tuseZig = true\n\t\tcase \"cross-rs\":\n\t\t\tuseCross = true\n\t\t\tuseZig = false\n\t\tdefault:\n\t\t\tBailf(\"unknown builder: %q\", cfg.RustBuilder)\n\t\t}\n\t}\n\n\tenvs := append(extraEnvVars, osPkg.Environ()...)\n\tvar target, zigTargetSuffix string\n\tswitch cfg.OS {\n\tcase \"darwin\":\n\t\tswitch cfg.Arch {\n\t\tcase \"amd64\":\n\t\t\ttarget = \"x86_64-apple-darwin\"\n\t\tcase \"arm64\":\n\t\t\ttarget = \"aarch64-apple-darwin\"\n\t\tdefault:\n\t\t\tBailf(\"unsupported architecture for darwin: %q\", cfg.Arch)\n\t\t}\n\n\t\t// We need to set the SDKROOT for cross compiling to MacOS\n\t\tif cfg.IsCross() {\n\t\t\tenvs = append(envs, \"SDKROOT=\"+cfg.CrossMacSDKPath())\n\t\t}\n\n\tcase \"linux\":\n\t\tswitch cfg.Arch {\n\t\tcase \"amd64\":\n\t\t\ttarget = \"x86_64-unknown-linux-\" + libc\n\t\tcase \"arm64\":\n\t\t\ttarget = \"aarch64-unknown-linux-\" + libc\n\t\tdefault:\n\t\t\tBailf(\"unsupported architecture for linux: %q\", cfg.Arch)\n\t\t}\n\n\t\t// If we're using zig, specify the glibc version we want.\n\t\tif useZig {\n\t\t\tzigTargetSuffix = \".2.31\"\n\t\t}\n\n\tcase \"windows\":\n\t\tswitch cfg.Arch {\n\t\tcase \"amd64\":\n\t\t\ttarget = \"x86_64-pc-windows-gnu\"\n\t\tdefault:\n\t\t\tBailf(\"unsupported architecture for windows: %q\", cfg.Arch)\n\t\t}\n\n\tdefault:\n\t\tBailf(\"unsupported os: %q\", cfg.OS)\n\t}\n\n\t// Create a cache dir for the go build cache for this specific OS and architecture pair\n\tpath := filepath.Join(cfg.CacheDir, \"rust\", cfg.OS, cfg.Arch)\n\n\tCheck(osPkg.MkdirAll(path, 0755))\n\n\t// Build the command\n\tcargoArgs := []string{\n\t\t\"build\",\n\t\t\"--target\", target + zigTargetSuffix,\n\t\t\"--target-dir\", path,\n\t}\n\tif useZig {\n\t\tcargoArgs[0] = \"zigbuild\"\n\t}\n\tbuildMode := \"debug\"\n\tif cfg.Release {\n\t\tcargoArgs = append(cargoArgs, \"--release\")\n\t\tbuildMode = \"release\"\n\t}\n\n\tif len(extraFeatures) > 0 {\n\t\tcargoArgs = append(cargoArgs, \"--features\", strings.Join(extraFeatures, \",\"))\n\t}\n\n\tbuilder := \"cargo\"\n\tif useCross {\n\t\tbuilder = \"cross\"\n\t}\n\tcmd := exec.Command(builder, cargoArgs...)\n\t// forwards the output to the parent process\n\tcmd.Stdout = osPkg.Stdout\n\tcmd.Stderr = osPkg.Stderr\n\tcmd.Dir = cratePath\n\tcmd.Env = envs\n\n\t// Cargo can't run multiple compiles at the same time for the same crate\n\t// so let's lock here, then unlock once the compile has finished\n\tcargoLock.Lock()\n\tdefer cargoLock.Unlock()\n\n\t// nosemgrep\n\tif err := cmd.Run(); err != nil {\n\t\tBailf(\"failed to compile rust binary: %v\", err)\n\t}\n\n\t// Copy the binary to the output path\n\tbinaryFile := filepath.Join(path, target, buildMode, artifactPath)\n\tcmd = exec.Command(\"cp\", binaryFile, outputPath)\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tBailf(\"failed to copy rust binary: %v: %s\", err, string(out))\n\t}\n}\n\n// compilerSettings returns the CC and CXX settings for the given OS and architecture\nfunc compilerSettings(cfg *buildconf.Config) (cc, cxx string, envs, ldFlags []string) {\n\tvar zigTarget string\n\tvar zigArgs string\n\tzigBinary := \"zig\"\n\n\tswitch cfg.OS {\n\tcase \"darwin\":\n\t\tzigBinary = \"/usr/local/zig-0.9.1/zig\" // We need an explicit version of Zig for darwin (0.11.0 compiles, build causes runtime errors)\n\t\tldFlags = []string{\"-s\", \"-w\"}\n\n\t\tswitch cfg.Arch {\n\t\tcase \"amd64\":\n\t\t\tzigTarget = \"x86_64-macos.10.12\"\n\t\tcase \"arm64\":\n\t\t\tzigTarget = \"aarch64-macos.11.1\"\n\t\tdefault:\n\t\t\tBailf(\"unsupported architecture for darwin: %q\", cfg.Arch)\n\t\t}\n\n\t\t// We need to set some extra stuff if we're cross compiling to MacOS\n\t\tif cfg.IsCross() {\n\t\t\tsdkPath := cfg.CrossMacSDKPath()\n\t\t\tzigArgs = \" -isysroot \" + sdkPath + \" -iwithsysroot /usr/include -iframeworkwithsysroot /System/Library/Frameworks\"\n\t\t\tenvs = []string{\n\t\t\t\t\"CGO_LDFLAGS=--sysroot \" + sdkPath + \" -F/System/Library/Frameworks -L/usr/lib\",\n\t\t\t}\n\t\t}\n\n\tcase \"linux\":\n\t\tswitch cfg.Arch {\n\t\tcase \"amd64\":\n\t\t\t// Note: we're not targeting a specific glibc version here as we tried before\n\t\t\t// with 2.35 - but for some reason we still get runtime errors not finding 2.34 or 2.33 on WSL (which had 2.35)\n\t\t\tzigTarget = \"x86_64-linux-gnu.2.31\"\n\t\t\tzigArgs = \" -static -isystem /usr/include\"\n\t\tcase \"arm64\":\n\t\t\tzigTarget = \"aarch64-linux-gnu.2.31\"\n\t\t\tzigArgs = \" -static -isystem /usr/include\"\n\t\t\tenvs = []string{\n\t\t\t\t\"PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig\",\n\t\t\t}\n\t\tdefault:\n\t\t\tBailf(\"unsupported architecture for linux: %q\", cfg.Arch)\n\t\t}\n\n\tcase \"windows\":\n\t\tswitch cfg.Arch {\n\t\tcase \"amd64\":\n\t\t\tzigTarget = \"x86_64-windows-gnu\"\n\t\tdefault:\n\t\t\tBailf(\"unsupported architecture for windows: %q\", cfg.Arch)\n\t\t}\n\n\t\tldFlags = []string{\"-H=windowsgui\"}\n\n\tdefault:\n\t\tBailf(\"unsupported os: %q\", cfg.OS)\n\t}\n\n\tcc = zigBinary + \" cc -target \" + zigTarget + zigArgs\n\tcxx = zigBinary + \" c++ -target \" + zigTarget + zigArgs\n\treturn cc, cxx, envs, ldFlags\n}\n\n// cargoLock is a lock to prevent concurrent cargo builds.\nvar cargoLock sync.Mutex\n"
  },
  {
    "path": "pkg/encorebuild/dist_builder.go",
    "content": "package encorebuild\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t. \"encr.dev/pkg/encorebuild/buildutil\"\n\t\"encr.dev/pkg/encorebuild/compile\"\n\t\"encr.dev/pkg/encorebuild/githubrelease\"\n)\n\n// A DistBuilder is a builder for a specific distribution of Encore.\n//\n// Anything which does not need to be built for a specific distribution\n// should be built in the main builder before these are invoked.\n//\n// Make release will run multiple of these in parallel to build all the\n// distributions.\ntype DistBuilder struct {\n\tCfg              *buildconf.Config\n\tDistBuildDir     string // The directory to build into\n\tArtifactsTarFile string // The directory to put the final tar.gz artifact into\n}\n\nfunc (d *DistBuilder) buildEncoreCLI() {\n\t// Build the CLI binaries.\n\td.Cfg.Log.Info().Msg(\"building encore binary...\")\n\n\tlinkerOpts := []string{\n\t\t\"-X\", fmt.Sprintf(\"'encr.dev/internal/version.Version=%s'\", d.Cfg.Version),\n\t}\n\n\t// If we're building a nightly, devel or beta version, we need to set the default config directory\n\tvar versionSuffix string\n\tswitch version.ChannelFor(d.Cfg.Version) {\n\tcase version.GA:\n\t\tversionSuffix = \"\"\n\tcase version.Beta:\n\t\tversionSuffix = \"-beta\"\n\tcase version.Nightly:\n\t\tversionSuffix = \"-nightly\"\n\tcase version.DevBuild:\n\t\tversionSuffix = \"-develop\"\n\tdefault:\n\t\tBailf(\"unknown version channel for %s\", d.Cfg.Version)\n\t}\n\n\tif versionSuffix != \"\" {\n\t\tlinkerOpts = append(linkerOpts,\n\t\t\t\"-X\", \"'encr.dev/internal/conf.defaultConfigDirectory=encore\"+versionSuffix+\"'\",\n\t\t)\n\t}\n\n\tcompile.GoBinary(\n\t\td.Cfg,\n\t\tjoin(d.DistBuildDir, \"bin\", \"encore\"+versionSuffix),\n\t\t\"./cli/cmd/encore\",\n\t\tlinkerOpts,\n\t)\n\td.Cfg.Log.Info().Msg(\"encore built successfully\")\n}\n\nfunc (d *DistBuilder) buildGitHook() {\n\t// Build the git-remote-encore binary.\n\td.Cfg.Log.Info().Msg(\"building git-remote-encore binary...\")\n\tcompile.GoBinary(\n\t\td.Cfg,\n\t\tjoin(d.DistBuildDir, \"bin\", \"git-remote-encore\"),\n\t\t\"./cli/cmd/git-remote-encore\",\n\t\tnil,\n\t)\n\td.Cfg.Log.Info().Msg(\"git-remote-encore built successfully\")\n}\n\nfunc (d *DistBuilder) buildTSBundler() {\n\t// Build the TS bundler.\n\td.Cfg.Log.Info().Msg(\"building tsbundler binary...\")\n\n\tlinkerOpts := []string{\n\t\t\"-X\", fmt.Sprintf(\"'encr.dev/internal/version.Version=%s'\", d.Cfg.Version),\n\t}\n\n\tcompile.GoBinary(\n\t\td.Cfg,\n\t\tjoin(d.DistBuildDir, \"bin\", \"tsbundler-encore\"),\n\t\t\"./cli/cmd/tsbundler-encore\",\n\t\tlinkerOpts,\n\t)\n\td.Cfg.Log.Info().Msg(\"tsbundler built successfully\")\n}\n\nfunc (d *DistBuilder) buildTSParser() {\n\t// Build the TS parser.\n\td.Cfg.Log.Info().Msg(\"building tsparser binary...\")\n\tcompile.RustBinary(\n\t\td.Cfg,\n\t\t\"tsparser-encore\",\n\t\tjoin(d.DistBuildDir, \"bin\", \"tsparser-encore\"),\n\t\t\"./tsparser\",\n\t\t\"gnu\",\n\t\t[]string{}, // features\n\t\tfmt.Sprintf(\"ENCORE_VERSION=%s\", d.Cfg.Version),\n\t)\n\td.Cfg.Log.Info().Msg(\"tsparser built successfully\")\n}\n\nfunc (d *DistBuilder) buildNodePlugin() {\n\tbuilder := NewJSRuntimeBuilder(d.Cfg)\n\tbuilder.Build()\n\n\td.Cfg.Log.Info().Msg(\"copying encore runtime for JS...\")\n\t{\n\t\tcmd := exec.Command(\"cp\", \"-r\", \"runtimes/js/.\", join(d.DistBuildDir, \"runtimes\", \"js\")+\"/\")\n\t\tcmd.Dir = d.Cfg.RepoDir\n\t\t// nosemgrep\n\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\tBailf(\"failed to copy encore go runtime: %v: %s\", err, out)\n\t\t}\n\t}\n\n\t{\n\t\tsrc := builder.NativeModuleOutput()\n\t\tdst := join(d.DistBuildDir, \"runtimes\", \"js\", \"encore-runtime.node\")\n\t\tcmd := exec.Command(\"cp\", src, dst)\n\t\t// nosemgrep\n\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\tBailf(\"failed to copy encore go runtime: %v: %s\", err, out)\n\t\t}\n\t}\n\n\td.Cfg.Log.Info().Msg(\"encore runtime for js copied successfully\")\n}\n\nfunc (d *DistBuilder) downloadEncoreGo() {\n\t// Step 1: Find out the latest release version for Encore's Go distribution\n\td.Cfg.Log.Info().Msg(\"downloading latest encore-go...\")\n\tencoreGoArchive := githubrelease.DownloadLatest(d.Cfg, \"encoredev\", \"go\")\n\n\td.Cfg.Log.Info().Msg(\"extracting encore-go...\")\n\tgithubrelease.Extract(encoreGoArchive, d.DistBuildDir)\n\td.Cfg.Log.Info().Msg(\"encore-go extracted successfully\")\n}\n\nfunc (d *DistBuilder) copyEncoreRuntimeForGo() {\n\td.Cfg.Log.Info().Msg(\"copying encore runtime for Go...\")\n\tcmd := exec.Command(\"cp\", \"-r\", \"runtimes/go/.\", join(d.DistBuildDir, \"runtimes\", \"go\")+\"/\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tBailf(\"failed to copy encore go runtime: %v: %s\", err, out)\n\t}\n\td.Cfg.Log.Info().Msg(\"encore runtime for go copied successfully\")\n}\n\n// Build builds the distribution running each step in order\nfunc (d *DistBuilder) Build() {\n\td.Cfg.Log.Info().Msg(\"building distribution...\")\n\n\tif d.DistBuildDir == \"\" {\n\t\tBailf(\"DistBuildDir not set\")\n\t}\n\n\t// Prepare the target directory.\n\tCheck(os.RemoveAll(d.DistBuildDir))\n\tCheck(os.MkdirAll(d.DistBuildDir, 0755))\n\tCheck(os.MkdirAll(join(d.DistBuildDir, \"bin\"), 0755))\n\tCheck(os.MkdirAll(join(d.DistBuildDir, \"runtimes\"), 0755))\n\tCheck(os.MkdirAll(join(d.DistBuildDir, \"runtimes\", \"go\"), 0755))\n\tCheck(os.MkdirAll(join(d.DistBuildDir, \"runtimes\", \"js\"), 0755))\n\n\t// Now we're prepped, start building.\n\tRunParallel(\n\t\td.buildEncoreCLI,\n\t\td.buildTSBundler,\n\t\td.buildGitHook,\n\t\td.buildTSParser,\n\t\td.buildNodePlugin,\n\t\td.copyEncoreRuntimeForGo,\n\t\td.downloadEncoreGo,\n\t)\n\n\t// Now tar gzip the directory\n\td.Cfg.Log.Info().Str(\"tar_file\", d.ArtifactsTarFile).Msg(\"creating distribution tar file...\")\n\tTarGzip(d.DistBuildDir, d.ArtifactsTarFile)\n\n\td.Cfg.Log.Info().Str(\"tar_file\", d.ArtifactsTarFile).Msg(\"distribution built successfully\")\n}\n\nfunc join(segs ...string) string {\n\treturn filepath.Join(segs...)\n}\n"
  },
  {
    "path": "pkg/encorebuild/gentypedefs/gentypedefs.go",
    "content": "package gentypedefs\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"golang.org/x/exp/maps\"\n)\n\ntype Config struct {\n\t// The version string to embed\n\tReleaseVersion string\n\n\t// The path to the intermediate type definition file.\n\tTypeDefFile string\n\n\t// The path to the .d.ts output file.\n\tDtsOutputFile string\n\n\t// The path to the .cjs output file.\n\tCjsOutputFile string\n}\n\nfunc Generate(cfg Config) error {\n\ttypeDefStr, exports, err := processTypeDef(cfg.TypeDefFile, false, DefaultTypeDefHeader)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse type definitions: %v\", err)\n\t}\n\n\terr = os.WriteFile(cfg.DtsOutputFile, []byte(typeDefStr), 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write .d.ts file: %v\", err)\n\t}\n\n\t{\n\t\tout, err := renderExportTemplate(cfg.ReleaseVersion, exports)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to render .cjs file: %v\", err)\n\t\t}\n\t\terr = os.WriteFile(cfg.CjsOutputFile, out, 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write .cjs file: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n//go:embed napi.cjs.tmpl\nvar napiCjsTemplate string\n\nfunc renderExportTemplate(version string, exports []string) ([]byte, error) {\n\ttmpl, err := template.New(\"napi.cjs\").Parse(napiCjsTemplate)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar buf bytes.Buffer\n\terr = tmpl.Execute(&buf, map[string]any{\n\t\t\"ModuleVersion\": version,\n\t\t\"Exports\":       exports,\n\t})\n\treturn buf.Bytes(), err\n}\n\nconst TopLevelNamespace = \"__TOP_LEVEL_MODULE__\"\nconst DefaultTypeDefHeader = `/* tslint:disable */\n/* eslint:disable */\n// Automatically generated by gen_type_defs. Do not edit.\n`\n\ntype TypeDefKind string\n\nconst (\n\tTypeDefKindConst     TypeDefKind = \"const\"\n\tTypeDefKindEnum      TypeDefKind = \"enum\"\n\tTypeDefKindInterface TypeDefKind = \"interface\"\n\tTypeDefKindFn        TypeDefKind = \"fn\"\n\tTypeDefKindStruct    TypeDefKind = \"struct\"\n\tTypeDefKindImpl      TypeDefKind = \"impl\"\n)\n\ntype TypeDefLine struct {\n\tKind         TypeDefKind `json:\"kind\"`\n\tName         string      `json:\"name\"`\n\tOriginalName string      `json:\"original_name,omitempty\"`\n\tDef          string      `json:\"def\"`\n\tJSDoc        string      `json:\"js_doc,omitempty\"`\n\tJSMod        string      `json:\"js_mod,omitempty\"`\n}\n\nfunc prettyPrint(line *TypeDefLine, constEnum bool, indent int) string {\n\ts := line.JSDoc\n\tswitch line.Kind {\n\tcase TypeDefKindInterface:\n\t\ts += \"export interface \" + line.Name + \" {\"\n\t\tif strings.TrimSpace(line.Def) != \"\" {\n\t\t\ts += \"\\n\" + line.Def + \"\\n\"\n\t\t}\n\t\ts += \"}\"\n\n\t\tif line.OriginalName != \"\" && line.OriginalName != line.Name {\n\t\t\ts += \"\\nexport type \" + line.OriginalName + \" = \" + line.Name\n\t\t}\n\n\tcase TypeDefKindEnum:\n\t\tenumName := \"enum\"\n\t\tif constEnum {\n\t\t\tenumName = \"const enum\"\n\t\t}\n\t\ts += \"export \" + enumName + \" \" + line.Name + \" {\"\n\t\tif strings.TrimSpace(line.Def) != \"\" {\n\t\t\ts += \"\\n\" + line.Def + \"\\n\"\n\t\t}\n\t\ts += \"}\"\n\n\tcase TypeDefKindStruct:\n\t\ts += \"export class \" + line.Name + \" {\"\n\t\tif strings.TrimSpace(line.Def) != \"\" {\n\t\t\ts += \"\\n\" + line.Def + \"\\n\"\n\t\t}\n\t\ts += \"}\"\n\n\t\tif line.OriginalName != \"\" && line.OriginalName != line.Name {\n\t\t\ts += \"\\nexport type \" + line.OriginalName + \" = \" + line.Name\n\t\t}\n\tdefault:\n\t\ts += line.Def\n\t}\n\n\treturn correctStringIndent(s, indent)\n}\n\nfunc processTypeDef(intermediateTypeFile string, constEnum bool, header string) (string, []string, error) {\n\tvar exports []string\n\tcontent, err := os.ReadFile(intermediateTypeFile)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tdefs, err := readIntermediateTypeFile(content)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tgroupedDefs := preprocessTypeDef(defs)\n\n\tif header != \"\" {\n\t\theader += \"\\n\"\n\t}\n\tdts := \"\"\n\n\tnamespaces := maps.Keys(groupedDefs)\n\tsort.Strings(namespaces)\n\n\tfor _, namespace := range namespaces {\n\t\tdefs := groupedDefs[namespace]\n\t\tslices.SortFunc(defs, func(a, b *TypeDefLine) int {\n\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t})\n\t\tif namespace == TopLevelNamespace {\n\t\t\tfor _, def := range defs {\n\t\t\t\tdts += prettyPrint(def, constEnum, 0) + \"\\n\\n\"\n\t\t\t\tswitch def.Kind {\n\t\t\t\tcase TypeDefKindConst, TypeDefKindEnum, TypeDefKindFn, TypeDefKindStruct:\n\t\t\t\t\texports = append(exports, def.Name)\n\t\t\t\t\tif def.OriginalName != \"\" && def.OriginalName != def.Name {\n\t\t\t\t\t\texports = append(exports, def.OriginalName)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\texports = append(exports, namespace)\n\t\t\tdts += `export namespace ` + namespace + ` {\n`\n\t\t\tfor _, def := range defs {\n\t\t\t\tdts += prettyPrint(def, constEnum, 2) + \"\\n\"\n\t\t\t}\n\t\t\tdts += `}\n`\n\t\t}\n\t}\n\n\tif strings.Contains(dts, \"ExternalObject<\") {\n\t\theader += `\nexport class ExternalObject<T> {\n  readonly '': {\n    readonly '': unique symbol\n    [K: symbol]: T\n  }\n}\n`\n\t}\n\n\tsort.Strings(exports)\n\treturn header + dts, exports, nil\n}\n\nfunc readIntermediateTypeFile(content []byte) ([]*TypeDefLine, error) {\n\tlines := strings.Split(string(content), \"\\n\")\n\tvar defs []*TypeDefLine\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !strings.HasPrefix(line, \"{\") {\n\t\t\t// crateName: { \"def\": \"\", ... }\n\t\t\tstart := strings.IndexByte(line, ':') + 1\n\t\t\tline = line[start:]\n\t\t}\n\t\tvar def TypeDefLine\n\t\terr := json.Unmarshal([]byte(line), &def)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefs = append(defs, &def)\n\t}\n\n\tslices.SortFunc(defs, func(a, b *TypeDefLine) int {\n\t\tif a.Kind == TypeDefKindStruct {\n\t\t\tif b.Kind == TypeDefKindStruct {\n\t\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t\t}\n\t\t\treturn -1\n\t\t} else if b.Kind == TypeDefKindStruct {\n\t\t\treturn 1\n\t\t} else {\n\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t}\n\t})\n\n\treturn defs, nil\n}\n\nfunc preprocessTypeDef(defs []*TypeDefLine) map[string][]*TypeDefLine {\n\tnamespaceGrouped := make(map[string][]*TypeDefLine)\n\tclassDefs := make(map[string]*TypeDefLine)\n\n\tfor _, def := range defs {\n\t\tnamespace := def.JSMod\n\t\tif namespace == \"\" {\n\t\t\tnamespace = TopLevelNamespace\n\t\t}\n\n\t\tif def.Kind == TypeDefKindStruct {\n\t\t\tnamespaceGrouped[namespace] = append(namespaceGrouped[namespace], def)\n\t\t\tclassDefs[def.Name] = def\n\t\t} else if def.Kind == TypeDefKindImpl {\n\t\t\tif classDef, ok := classDefs[def.Name]; ok {\n\t\t\t\tif classDef.Def != \"\" {\n\t\t\t\t\tclassDef.Def += \"\\n\"\n\t\t\t\t}\n\t\t\t\tclassDef.Def += def.Def\n\t\t\t}\n\t\t} else {\n\t\t\tnamespaceGrouped[namespace] = append(namespaceGrouped[namespace], def)\n\t\t}\n\t}\n\n\treturn namespaceGrouped\n}\n\nfunc correctStringIndent(src string, indent int) string {\n\tvar result strings.Builder\n\tbracketDepth := 0\n\tfor _, line := range strings.Split(src, \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tresult.WriteString(\"\\n\")\n\t\t\tcontinue\n\t\t}\n\n\t\tisInMultilineComment := strings.HasPrefix(line, \"*\")\n\t\tisClosingBracket := strings.HasSuffix(line, \"}\")\n\t\tisOpeningBracket := strings.HasSuffix(line, \"{\")\n\n\t\trightIndent := indent\n\t\tif isOpeningBracket && !isInMultilineComment {\n\t\t\tbracketDepth++\n\t\t\trightIndent += (bracketDepth - 1) * 2\n\t\t} else {\n\t\t\tif isClosingBracket && bracketDepth > 0 && !isInMultilineComment {\n\t\t\t\tbracketDepth--\n\t\t\t}\n\t\t\trightIndent += bracketDepth * 2\n\t\t}\n\n\t\tif isInMultilineComment {\n\t\t\trightIndent++\n\t\t}\n\n\t\tresult.WriteString(strings.Repeat(\" \", rightIndent) + line + \"\\n\")\n\t}\n\n\tres := result.String()\n\tif strings.HasSuffix(res, \"\\n\") {\n\t\tres = res[:len(res)-1]\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "pkg/encorebuild/gentypedefs/napi.cjs.tmpl",
    "content": "// The version of the runtime this JS bundle was generated for.\nconst version = {{.ModuleVersion | printf \"%q\"}};\n\n// Load the native module.\nconst nativeModulePath = process.env.ENCORE_RUNTIME_LIB;\nif (!nativeModulePath) {\n  throw new Error(\n    \"The ENCORE_RUNTIME_LIB environment variable is not set. It must be set to the path of the Encore runtime library ('encore-runtime.node').\"\n  );\n}\nconst nativeModule = require(nativeModulePath);\n\n// Load the exported objects from the native module.\nconst {\n{{- range .Exports}}\n  {{.}},\n  {{- end}}\n} = nativeModule;\n\n// Export the objects from the native module.\nmodule.exports = {\n{{- range .Exports}}\n  {{.}},\n  {{- end}}\n};\n\n\n// Sanity check incase the JS bundle was built for a different version of the runtime.\nif (version !== Runtime.version()) {\n  console.warn(`⚠️ WARNING: The version of the Encore runtime this JS bundle was built for (${version}) does not match the version of the Encore runtime it is running in (${Runtime.version()}).\nThis may cause unexpected behaviour in your application.\n\nTo resolve this, try update your Encore CLI using \"encore version update\" and then update the dependencies in your package.json file using \"npm install encore.dev@latest\".`);\n}\n"
  },
  {
    "path": "pkg/encorebuild/githubrelease/githubrelease.go",
    "content": "package githubrelease\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tosPkg \"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t. \"encr.dev/pkg/encorebuild/buildutil\"\n\t\"github.com/cockroachdb/errors\"\n)\n\ntype Info struct {\n\tVersion  string // The version of the release\n\tFilename string // The filename of the release (inc extension)\n\tFileExt  string // The file extension\n\tURL      string // The URL to download the release from\n\tChecksum []byte // The checksum of the release\n}\n\n// getGithubRelease fetches the latest release from Github for the given org and repo.\nfunc FetchInfo(cfg *buildconf.Config, org string, repo string) *Info {\n\trtn := &Info{}\n\n\ttype GithubRelease struct {\n\t\tTagName string `json:\"tag_name\"`\n\t\tAssets  []struct {\n\t\t\tName               string `json:\"name\"`\n\t\t\tBrowserDownloadURL string `json:\"browser_download_url\"`\n\t\t} `json:\"assets\"`\n\t}\n\n\t// Download the latest releases\n\treleasesResp := Must(http.Get(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/releases/latest\", org, repo)))\n\tdefer func() { _ = releasesResp.Body.Close() }()\n\n\tif releasesResp.StatusCode != http.StatusOK {\n\t\tBailf(\"Unexpected response status code: %s\", releasesResp.Status)\n\t}\n\n\treleases := &GithubRelease{}\n\tCheck(json.NewDecoder(releasesResp.Body).Decode(releases))\n\n\trtn.Version = releases.TagName\n\n\t// Build a list of possible file names\n\tosOptions := []string{cfg.OS}\n\tif cfg.OS == \"darwin\" {\n\t\tosOptions = append(osOptions, \"macos\")\n\t}\n\tarchOptions := []string{cfg.Arch}\n\tif cfg.Arch == \"amd64\" {\n\t\tarchOptions = append(archOptions, \"x86_64\", \"x86-64\")\n\t} else if cfg.Arch == \"arm64\" {\n\t\tarchOptions = append(archOptions, \"aarch64\")\n\t}\n\textOptions := []string{\"tar.gz\"} // , \"zip\"}\n\n\tvar fileNameOptions []string\n\tfor _, osOption := range osOptions {\n\t\tfor _, archOption := range archOptions {\n\t\t\tfor _, extOption := range extOptions {\n\t\t\t\tfileNameOptions = append(fileNameOptions,\n\t\t\t\t\tfmt.Sprintf(\"%s_%s.%s\", osOption, archOption, extOption),\n\t\t\t\t\tfmt.Sprintf(\"%s-%s.%s\", osOption, archOption, extOption),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Find the checksum file\n\tchecksumFileURL := \"\"\n\tfor _, asset := range releases.Assets {\n\t\t// We want to know the checksum URL\n\t\tif strings.EqualFold(asset.Name, \"checksums.txt\") {\n\t\t\tchecksumFileURL = asset.BrowserDownloadURL\n\n\t\t}\n\n\t\tfor _, filenameOption := range fileNameOptions {\n\t\t\tif strings.EqualFold(asset.Name, filenameOption) {\n\t\t\t\t// We also want to know the asset name and download URL\n\t\t\t\trtn.Filename = asset.Name\n\t\t\t\trtn.URL = asset.BrowserDownloadURL\n\n\t\t\t\tif strings.HasSuffix(asset.Name, \".tar.gz\") {\n\t\t\t\t\trtn.FileExt = \".tar.gz\"\n\t\t\t\t} else {\n\t\t\t\t\trtn.FileExt = filepath.Ext(asset.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif checksumFileURL == \"\" {\n\t\tBailf(\"unable to find checksum file in Github release\")\n\t}\n\tif rtn.URL == \"\" || rtn.Filename == \"\" {\n\t\tBailf(\"unable to find binary in Github release\")\n\t}\n\n\t// Download the checksum file\n\tchecksumResp := Must(http.Get(checksumFileURL))\n\tdefer func() { _ = checksumResp.Body.Close() }()\n\tif checksumResp.StatusCode != http.StatusOK {\n\t\tBailf(\"Unexpected response status code for checksum file: %s\", checksumResp.Status)\n\t}\n\n\t// Read the checksum file line by line\n\tscanner := bufio.NewScanner(checksumResp.Body)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif strings.HasSuffix(line, rtn.Filename) {\n\t\t\tchecksumStr := strings.Split(line, \" \")[0]\n\n\t\t\tchecksum := Must(hex.DecodeString(checksumStr))\n\t\t\trtn.Checksum = checksum\n\n\t\t\treturn rtn\n\t\t}\n\t}\n\tBailf(\"unable to find checksum for asset file in checksum file\")\n\tpanic(\"unreachable\")\n}\n\nfunc DownloadLatest(cfg *buildconf.Config, org, repo string) (pathToFile string) {\n\t// Find the latest release\n\trelease := FetchInfo(cfg, org, repo)\n\n\t// Create a cache dir for the download cache for this specific OS and architecture pair\n\tpath := filepath.Join(cfg.CacheDir, \"github-releases\", org, repo, cfg.OS, cfg.Arch)\n\tCheck(osPkg.MkdirAll(path, 0755))\n\n\tdownloadFileName := fmt.Sprintf(\"%s-%s%s\", release.Version, hex.EncodeToString(release.Checksum), release.FileExt)\n\tdownloadPath := filepath.Join(path, downloadFileName)\n\n\t// Check if the file already exists\n\tif _, err := osPkg.Stat(downloadPath); err == nil {\n\t\treturn downloadPath\n\t} else if !osPkg.IsNotExist(err) {\n\t\tBailf(\"failed to stat existing download file: %v\", err)\n\t}\n\n\t// Now download the file\n\tdownloadResp := Must(http.Get(release.URL))\n\tdefer func() { _ = downloadResp.Body.Close() }()\n\tif downloadResp.StatusCode != http.StatusOK {\n\t\tBailf(\"Unexpected response status code for release file: %s\", downloadResp.Status)\n\t}\n\n\t// Create the file\n\tdownloadFile := Must(osPkg.Create(downloadPath))\n\tdefer func() {\n\t\t_ = downloadFile.Close()\n\t\tif r := recover(); r != nil {\n\t\t\t_ = osPkg.Remove(downloadPath) // delete any partially written file\n\t\t\tpanic(r)                       // re-panic\n\t\t}\n\t}()\n\t_ = Must(io.Copy(downloadFile, downloadResp.Body))\n\n\t// Now checksum the file\n\t_ = Must(downloadFile.Seek(0, 0))\n\n\tchecksum := Must(checksumFile(downloadFile))\n\n\t// Check the checksum\n\tif !bytes.Equal(checksum, release.Checksum) {\n\t\tBailf(\"checksum of downloaded file (%q) does not match expected checksum (%q)\", hex.EncodeToString(checksum), hex.EncodeToString(release.Checksum))\n\t}\n\n\treturn downloadPath\n}\n\nfunc checksumFile(file *osPkg.File) ([]byte, error) {\n\thash := sha256.New()\n\tif _, err := io.Copy(hash, file); err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to checksum file\")\n\t}\n\treturn hash.Sum(nil), nil\n}\n\nfunc Extract(pathToArchive string, targetDir string) {\n\t// Create the target dir\n\tCheck(osPkg.MkdirAll(targetDir, 0755))\n\n\t// Extract the archive\n\tcmd := exec.Command(\"tar\", \"-xzf\", pathToArchive, \"--strip-components\", \"1\", \"-C\", targetDir)\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tBailf(\"failed to extract archive: %s\", out)\n\t}\n}\n\nfunc copyDir(src, dst string) error {\n\tcmd := exec.Command(\"cp\", \"-r\", src+\"/\", dst)\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to copy dir: %s\", out)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/encorebuild/jsruntimebuild.go",
    "content": "package encorebuild\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t. \"encr.dev/pkg/encorebuild/buildutil\"\n\t\"encr.dev/pkg/encorebuild/compile\"\n\t\"encr.dev/pkg/encorebuild/gentypedefs\"\n)\n\nfunc NewJSRuntimeBuilder(cfg *buildconf.Config) *JSRuntimeBuilder {\n\tif cfg.RepoDir == \"\" {\n\t\tBailf(\"repo dir not set\")\n\t} else if _, err := os.Stat(cfg.RepoDir); err != nil {\n\t\tBailf(\"repo does not exist\")\n\t}\n\n\tworkdir := filepath.Join(cfg.CacheDir, \"jsruntimebuild\", cfg.OS, cfg.Arch)\n\tCheck(os.MkdirAll(workdir, 0755))\n\treturn &JSRuntimeBuilder{\n\t\tlog:     cfg.Log,\n\t\tcfg:     cfg,\n\t\tworkdir: workdir,\n\t}\n}\n\ntype JSRuntimeBuilder struct {\n\tlog     zerolog.Logger\n\tcfg     *buildconf.Config\n\tworkdir string\n}\n\nfunc (b *JSRuntimeBuilder) Build() {\n\tb.log.Info().Msgf(\"Building local JS runtime targeting %s/%s\", b.cfg.OS, b.cfg.Arch)\n\tb.buildRustModule()\n\tb.genTypeDefWrappers()\n\tb.makeDistFolder()\n\n\tif b.cfg.CopyToRepo {\n\t\tb.copyNativeModule()\n\t}\n}\n\n// buildRustModule builds the Rust module for the JS runtime.\nfunc (b *JSRuntimeBuilder) buildRustModule() {\n\tb.log.Info().Msg(\"building rust module\")\n\n\t// Figure out the names of the compiled and target binaries.\n\tcompiledBinaryName := func() string {\n\t\tswitch b.cfg.OS {\n\t\tcase \"darwin\":\n\t\t\treturn \"libencore_js_runtime.dylib\"\n\t\tcase \"linux\":\n\t\t\treturn \"libencore_js_runtime.so\"\n\t\tcase \"windows\":\n\t\t\treturn \"encore_js_runtime.dll\"\n\t\tdefault:\n\t\t\tBailf(\"unknown OS: %s\", b.cfg.OS)\n\t\t\tpanic(\"unreachable\")\n\t\t}\n\t}()\n\n\tfeatures := []string{}\n\tif !b.cfg.Release {\n\t\t// Enable runtime tracing in debug builds.\n\t\tfeatures = append(features, \"encore-runtime-core/rttrace\")\n\t}\n\n\tcompile.RustBinary(\n\t\tb.cfg,\n\t\tcompiledBinaryName,\n\t\tb.NativeModuleOutput(),\n\t\tfilepath.Join(b.cfg.RepoDir, \"runtimes\", \"js\"),\n\t\t\"gnu\",\n\t\tfeatures, // features\n\t\t\"TYPE_DEF_TMP_PATH=\"+b.typeDefPath(),\n\t\t\"ENCORE_VERSION=\"+b.cfg.Version,\n\t\t\"ENCORE_WORKDIR=\"+b.workdir,\n\t)\n}\n\n// genTypeDefWrappers generates the napi.cjs and napi.d.cts files for\n// use by the JS SDK.\nfunc (b *JSRuntimeBuilder) genTypeDefWrappers() {\n\tb.log.Info().Msg(\"generating napi type definitions\")\n\tnapiPath := filepath.Join(b.npmPackagePath(), napiRelPath)\n\tCheck(os.MkdirAll(napiPath, 0755))\n\n\tcfg := gentypedefs.Config{\n\t\tReleaseVersion: b.cfg.Version,\n\t\tTypeDefFile:    b.typeDefPath(),\n\t\tDtsOutputFile:  filepath.Join(napiPath, \"napi.d.cts\"),\n\t\tCjsOutputFile:  filepath.Join(napiPath, \"napi.cjs\"),\n\t}\n\tCheck(gentypedefs.Generate(cfg))\n}\n\n// makeDistFolder creates the dist folder for the JS runtime,\n// and fixes the imports to be ESM-compatible.\nfunc (b *JSRuntimeBuilder) makeDistFolder() {\n\tb.log.Info().Msg(\"creating dist folder\")\n\t// Sanity-check the runtime dir configuration so we don't delete the wrong thing.\n\tbase := filepath.Base(b.cfg.RepoDir)\n\tparentBase := filepath.Base(filepath.Dir(b.cfg.RepoDir))\n\tif b.cfg.RepoDir == \"\" || (base != \"encore\" && base != \"encr.dev\" && parentBase != \"encore.worktrees\") {\n\t\tBailf(\"invalid repo directory %q, aborting\", b.cfg.RepoDir)\n\t}\n\n\tpkgPath := filepath.Join(b.jsRuntimePath(), \"encore.dev\")\n\tdistPath := filepath.Join(pkgPath, \"dist\")\n\tCheck(os.RemoveAll(distPath))\n\n\t// Run 'npm install'.\n\t{\n\t\tcmd := exec.Command(\"npm\", \"install\")\n\t\tcmd.Dir = pkgPath\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tCheck(cmd.Run())\n\t}\n\n\t// Run 'npm run build'.\n\t{\n\t\tcmd := exec.Command(\"npm\", \"run\", \"build\")\n\t\tcmd.Dir = pkgPath\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tCheck(cmd.Run())\n\t}\n\n\t// Copy the napi directory over.\n\t{\n\t\tsrc := filepath.Join(b.npmPackagePath(), napiRelPath)\n\t\tdst := filepath.Join(distPath, napiRelPath)\n\t\tcmd := exec.Command(\"cp\", \"-r\", src, dst)\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tCheck(cmd.Run())\n\t}\n\n\t// Run 'tsc-esm-fix'.\n\t{\n\t\tcmd := exec.Command(\"./node_modules/.bin/tsc-esm-fix\", \"--target=dist\")\n\t\tcmd.Dir = pkgPath\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tCheck(cmd.Run())\n\t}\n\n}\n\nfunc (b *JSRuntimeBuilder) copyNativeModule() {\n\tb.log.Info().Msg(\"copying native module\")\n\tcopyFile := func(src, dst string) {\n\t\tcmd := exec.Command(\"cp\", src, dst)\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tBailf(\"unable to copy native module: %v: %s\", err, out)\n\t\t}\n\t}\n\n\tsrc := b.NativeModuleOutput()\n\tsuffix := \"\"\n\tif b.cfg.OS != runtime.GOOS || b.cfg.Arch != runtime.GOARCH {\n\t\tsuffix = \"-\" + b.cfg.OS + \"-\" + b.cfg.Arch\n\t}\n\tdst := filepath.Join(b.jsRuntimePath(), \"encore-runtime.node\"+suffix)\n\tcopyFile(src, dst)\n}\n\nfunc (b *JSRuntimeBuilder) NativeModuleOutput() string {\n\treturn filepath.Join(b.workdir, \"encore-runtime.node\")\n}\n\nfunc (b *JSRuntimeBuilder) typeDefPath() string {\n\treturn filepath.Join(b.workdir, \"typedefs.ndjson\")\n}\n\nfunc (b *JSRuntimeBuilder) npmPackagePath() string {\n\treturn filepath.Join(b.jsRuntimePath(), \"encore.dev\")\n}\n\nfunc (b *JSRuntimeBuilder) jsRuntimePath() string {\n\treturn filepath.Join(b.cfg.RepoDir, \"runtimes\", \"js\")\n}\n\n// napiRelPath is the relative path from the package root to the napi directory.\nvar napiRelPath = filepath.Join(\"internal\", \"runtime\", \"napi\")\n\nfunc PublishNPMPackages(repoDir, version string) {\n\tpackages := []string{\"encore.dev\"}\n\tnpmVersion := strings.TrimPrefix(version[1:], \"v\")\n\n\tnpmTag := \"latest\"\n\tswitch {\n\tcase strings.Contains(version, \"-beta.\"):\n\t\tnpmTag = \"beta\"\n\tcase strings.Contains(version, \"-nightly.\"):\n\t\tnpmTag = \"nightly\"\n\t}\n\n\t// Configure the auth token\n\t{\n\t\tauthToken := os.Getenv(\"NPM_PUBLISH_TOKEN\")\n\t\tif authToken == \"\" {\n\t\t\tBailf(\"NPM_PUBLISH_TOKEN not set\")\n\t\t}\n\t\tcmd := exec.Command(\"npm\", \"set\", \"//registry.npmjs.org/:_authToken=\"+authToken)\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tCheck(cmd.Run())\n\t}\n\n\tfor _, pkg := range packages {\n\t\tpkgDir := filepath.Join(repoDir, \"runtimes\", \"js\", pkg)\n\n\t\t// Run 'npm version'.\n\t\t{\n\t\t\tcmd := exec.Command(\"npm\", \"version\", \"--no-git-tag-version\", \"--no-commit-hooks\", npmVersion)\n\t\t\tcmd.Dir = pkgDir\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\t\t\tCheck(cmd.Run())\n\t\t}\n\n\t\t// Run 'npm publish'.\n\t\t{\n\t\t\tcmd := exec.Command(\"npm\", \"publish\",\n\t\t\t\t\"--tolerate-republish\",\n\t\t\t\t\"--access\", \"public\",\n\t\t\t\t\"--tag\", npmTag,\n\t\t\t)\n\t\t\tcmd.Dir = pkgDir\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\t\t\tCheck(cmd.Run())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/encorebuild/supervisorbuild.go",
    "content": "package encorebuild\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/pkg/encorebuild/buildconf\"\n\t. \"encr.dev/pkg/encorebuild/buildutil\"\n\t\"encr.dev/pkg/encorebuild/compile\"\n)\n\nfunc NewSupervisorBuilder(cfg *buildconf.Config) *SupervisorBuilder {\n\tif cfg.RepoDir == \"\" {\n\t\tBailf(\"repo dir not set\")\n\t} else if _, err := os.Stat(cfg.RepoDir); err != nil {\n\t\tBailf(\"repo does not exist\")\n\t}\n\n\tworkdir := filepath.Join(cfg.CacheDir, \"supervisorbuild\", cfg.OS, cfg.Arch)\n\tCheck(os.MkdirAll(workdir, 0755))\n\treturn &SupervisorBuilder{\n\t\tlog:     cfg.Log,\n\t\tcfg:     cfg,\n\t\tworkdir: workdir,\n\t}\n}\n\ntype SupervisorBuilder struct {\n\tlog     zerolog.Logger\n\tcfg     *buildconf.Config\n\tworkdir string\n}\n\nfunc (b *SupervisorBuilder) Build() {\n\tb.log.Info().Msgf(\"Building local Supervisor targeting %s/%s\", b.cfg.OS, b.cfg.Arch)\n\tb.buildRustModule()\n\tif b.cfg.CopyToRepo {\n\t\tb.copyToRepo()\n\t}\n}\n\n// buildRustModule builds the Rust module for the Supervisor runtime.\nfunc (b *SupervisorBuilder) buildRustModule() {\n\tb.log.Info().Msg(\"building rust module\")\n\tcompile.RustBinary(\n\t\tb.cfg,\n\t\t\"supervisor-encore\",\n\t\tb.BinaryOutput(),\n\t\tfilepath.Join(b.cfg.RepoDir, \"supervisor\"),\n\t\t\"musl\",\n\t\t[]string{}, // features\n\t\t\"ENCORE_VERSION=\"+b.cfg.Version,\n\t\t\"ENCORE_WORKDIR=\"+b.workdir,\n\t)\n}\n\nfunc (b *SupervisorBuilder) copyToRepo() {\n\tb.log.Info().Msg(\"copying binary to repo dir\")\n\tcopyFile := func(src, dst string) {\n\t\tcmd := exec.Command(\"cp\", src, dst)\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tBailf(\"unable to copy binary: %v: %s\", err, out)\n\t\t}\n\t}\n\n\tsrc := b.BinaryOutput()\n\tsuffix := \"\"\n\tif b.cfg.OS != runtime.GOOS || b.cfg.Arch != runtime.GOARCH {\n\t\tsuffix = \"-\" + b.cfg.OS + \"-\" + b.cfg.Arch\n\t}\n\tdst := filepath.Join(b.cfg.RepoDir, \"runtimes\", \"supervisor-encore\"+suffix)\n\tcopyFile(src, dst)\n}\n\nfunc (b *SupervisorBuilder) BinaryOutput() string {\n\treturn filepath.Join(b.workdir, \"supervisor-encore\")\n}\n"
  },
  {
    "path": "pkg/encorebuild/windows/.gitignore",
    "content": "/.deps"
  },
  {
    "path": "pkg/encorebuild/windows/build.bat",
    "content": "@echo off\nrem SPDX-License-Identifier: MIT\nrem Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.\n\nsetlocal enableextensions enabledelayedexpansion\nset BUILDDIR=%~dp0\nset ROOT=%BUILDDIR%\\..\\..\\..\nset DST=%ROOT%\\dist\\windows_amd64\nset PATH=%BUILDDIR%.deps\\llvm-mingw\\bin;%BUILDDIR%.deps;%PATH%\nset PATHEXT=.EXE;.CMD\n\nif \"%ENCORE_VERSION%\" == \"\" (\n\techo ENCORE_VERSION not set\n\texit /b 1\n)\n\nif \"%ENCORE_GOROOT%\" == \"\" (\n\techo ENCORE_GOROOT not set\n\texit /b 1\n)\n\n:: Get absolute path\ncd %ENCORE_GOROOT% || exit /b 1\nset ENCORE_GOROOT=%CD%\n\ncd /d %BUILDDIR% || exit /b 1\n\nif exist .deps\\prepared goto :build\n:installdeps\n\trmdir /s /q .deps 2> NUL\n\tmkdir .deps || goto :error\n\tcd .deps || goto :error\n\tcall :download llvm-mingw-msvcrt.zip https://download.wireguard.com/windows-toolchain/distfiles/llvm-mingw-20201020-msvcrt-x86_64.zip 2e46593245090df96d15e360e092f0b62b97e93866e0162dca7f93b16722b844 || goto :error\n\tcall :download wintun.zip https://www.wintun.net/builds/wintun-0.10.2.zip fcd9f62f1bd5a550fcb9c21fbb5d6a556214753ccbbd1a3ebad4d318ec9dcbef || goto :error\n\tcall :download wix-binaries.zip https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip 2c1888d5d1dba377fc7fa14444cf556963747ff9a0a289a3599cf09da03b9e2e || goto :error\n\tcopy /y NUL prepared > NUL || goto :error\n\tcd .. || goto :error\n\n:build\n\tset GOOS=windows\n\tcall :build_plat amd64 x86_64 amd64 || goto :error\n\tcall :copy_artifacts || goto :error\n\n:success\n\techo [+] Success!\n\texit /b 0\n\n:download\n\techo [+] Downloading %1\n\tcurl -#fLo %1 %2 || exit /b 1\n\techo [+] Verifying %1\n\tfor /f %%a in ('CertUtil -hashfile %1 SHA256 ^| findstr /r \"^[0-9a-f]*$\"') do if not \"%%a\"==\"%~3\" exit /b 1\n\techo [+] Extracting %1\n\ttar -xf %1 %~4 || exit /b 1\n\techo [+] Cleaning up %1\n\tdel %1 || exit /b 1\n\tgoto :eof\n\n:build_plat\n\trmdir /S /Q \"%DST%\"\n\tmkdir %DST%\\bin >NUL 2>&1\n\techo [+] Assembling resources\n\tx86_64-w64-mingw32-windres -I \".deps\\wintun\\bin\\amd64\" -i resources.rc -o \"%ROOT%\\cli\\cmd\\encore\\resources_amd64.syso\" -O coff -c 65001 || exit /b %errorlevel%\n\tset GOARCH=amd64\n\techo [+] Building\n\tgo build -tags load_wintun_from_rsrc -ldflags \"-X 'encr.dev/internal/version.Version=v%ENCORE_VERSION%'\" -o \"%DST%\\bin\\encore.exe\" \"%ROOT%\\cli\\cmd\\encore\" || exit /b 1\n\tgo build -trimpath -o \"%DST%\\bin\\git-remote-encore.exe\" \"%ROOT%\\cli\\cmd\\git-remote-encore\" || exit /b 1\n\tgoto :eof\n\n:copy_artifacts\n\techo [+] Copying files\n\txcopy /S /I /E /H /Q \"%ENCORE_GOROOT%\" \"%DST%\\encore-go\" || exit /b 1\n\txcopy /S /I /E /H /Q \"%ROOT%\\runtimes\\go\" \"%DST%\\runtimes\\go\" || exit /b 1\n\tgoto :eof\n\n:error\n\techo [-] Failed with error #%errorlevel%.\n\tcmd /c exit %errorlevel%\n"
  },
  {
    "path": "pkg/encorebuild/windows/manifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n    <assemblyIdentity version=\"1.0.0.0\" processorArchitecture=\"*\" name=\"encore\" type=\"win32\" />\n    <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n        <application>\n            <!-- Windows 10 -->\n            <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />\n            <!-- Windows 8.1 -->\n            <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />\n            <!-- Windows 8 -->\n            <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />\n            <!-- Windows 7 -->\n            <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />\n        </application>\n    </compatibility>\n    <dependency>\n        <dependentAssembly>\n            <assemblyIdentity type=\"win32\" name=\"Microsoft.Windows.Common-Controls\" version=\"6.0.0.0\" processorArchitecture=\"*\" publicKeyToken=\"6595b64144ccf1df\" language=\"*\" />\n        </dependentAssembly>\n    </dependency>\n    <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <windowsSettings>\n            <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2, PerMonitor</dpiAwareness>\n            <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">True</dpiAware>\n        </windowsSettings>\n    </application>\n</assembly>"
  },
  {
    "path": "pkg/encorebuild/windows/resources.rc",
    "content": "/* SPDX-License-Identifier: MIT\n *\n * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.\n */\n\n#pragma code_page(65001) // UTF-8\nCREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml\nwintun.dll RCDATA wintun.dll"
  },
  {
    "path": "pkg/environ/environ.go",
    "content": "package environ\n\n// Environ is a slice of strings representing the environment of a process.\ntype Environ []string\n\n// Get retrieves the value of the environment variable named by the key.\n// It returns the value, which will be empty if the variable is not present.\n// To distinguish between an empty value and an unset value, use LookupEnv.\nfunc (e Environ) Get(key string) string {\n\tv, _ := e.Lookup(key)\n\treturn v\n}\n\n// Lookup retrieves the value of the environment variable named\n// by the key. If the variable is present in the environment the\n// value (which may be empty) is returned and the boolean is true.\n// Otherwise the returned value will be empty and the boolean will\n// be false.\nfunc (e Environ) Lookup(key string) (string, bool) {\n\tfor _, env := range e {\n\t\tif len(env) > len(key) && env[len(key)] == '=' && env[:len(key)] == key {\n\t\t\treturn env[len(key)+1:], true\n\t\t}\n\t}\n\treturn \"\", false\n}\n"
  },
  {
    "path": "pkg/errinsrc/characters.go",
    "content": "package errinsrc\n\n// The current character set to use when rendering\nvar set CharacterSet = unicodeSet\n\ntype CharacterSet struct {\n\tHorizontalBar rune\n\tVerticalBar   rune\n\tCrossBar      rune\n\tVerticalBreak rune\n\tVerticalGap   rune\n\tUpArrow       rune\n\tRightArrow    rune\n\tLeftTop       rune\n\tMiddleTop     rune\n\tRightTop      rune\n\tLeftBottom    rune\n\tRightBottom   rune\n\tMiddleBottom  rune\n\tLeftBracket   rune\n\tRightBracket  rune\n\tLeftCross     rune\n\tRightCross    rune\n\tUnderBar      rune\n\tUnderline     rune\n}\n\nvar unicodeSet = CharacterSet{\n\tHorizontalBar: '─',\n\tVerticalBar:   '│',\n\tCrossBar:      '┼',\n\tVerticalBreak: '·',\n\tVerticalGap:   '⋮',\n\tUpArrow:       '▲',\n\tRightArrow:    '▶',\n\tLeftTop:       '╭',\n\tMiddleTop:     '┬',\n\tRightTop:      '╮',\n\tLeftBottom:    '╰',\n\tMiddleBottom:  '┴',\n\tRightBottom:   '╯',\n\tLeftBracket:   '[',\n\tRightBracket:  ']',\n\tLeftCross:     '├',\n\tRightCross:    '┤',\n\tUnderBar:      '┬',\n\tUnderline:     '─',\n}\nvar asciiSet = CharacterSet{\n\tHorizontalBar: '-',\n\tVerticalBar:   '|',\n\tCrossBar:      '+',\n\tVerticalBreak: '*',\n\tVerticalGap:   ':',\n\tUpArrow:       '^',\n\tRightArrow:    '>',\n\tLeftTop:       ',',\n\tMiddleTop:     'v',\n\tRightTop:      '.',\n\tLeftBottom:    '`',\n\tMiddleBottom:  '^',\n\tRightBottom:   '\\'',\n\tLeftBracket:   '[',\n\tRightBracket:  ']',\n\tLeftCross:     '|',\n\tRightCross:    '|',\n\tUnderBar:      '|',\n\tUnderline:     '^',\n}\n"
  },
  {
    "path": "pkg/errinsrc/errinsrc.go",
    "content": "package errinsrc\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\tencerrors \"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\n\t. \"encr.dev/pkg/errinsrc/internal\"\n)\n\n// ErrInSrc represents an error which occurred due to the source code\n// of the application being run through Encore.\n//\n// It supports the concept of one of more locations within the users\n// source code which caused the error. The locations will be rendered\n// in the final output.\n//\n// Construct these using helper functions in the `srcerrors` package\n// as we can use that as a central repository error types\ntype ErrInSrc struct {\n\t// The parameters of the error\n\t// This is an internal data type to force the\n\t// creation of these inside `srcerrors`\n\tParams ErrParams `json:\"params\"`\n\n\t// The Stack trace of where the error was created within the Encore codebase\n\t// this will be empty if the error was created in a production build of Encore.\n\t// To populate this, build Encore with the tag `dev_build`.\n\tStack []*StackFrame `json:\"stack,omitempty\"`\n}\n\nvar _ error = (*ErrInSrc)(nil)\n\n// New returns a new ErrInSrc with a Stack trace attached\nfunc New(params ErrParams, alwaysIncludeStack bool) *ErrInSrc {\n\tvar stack []*StackFrame\n\n\t//goland:noinspection GoBoolExpressions\n\tif IncludeStackByDefault || alwaysIncludeStack {\n\t\tif params.Cause != nil {\n\t\t\tstack = bottomStackTraceFrom(params.Cause)\n\t\t}\n\n\t\tif len(stack) == 0 {\n\t\t\tstack = GetStack()\n\t\t}\n\t}\n\n\treturn &ErrInSrc{\n\t\tParams: params,\n\t\tStack:  stack,\n\t}\n}\n\n// FromTemplate returns a new ErrInSrc using the [errs.Template] as a template\nfunc FromTemplate(template encerrors.Template, fileset *token.FileSet, fileReaders ...paths.FileReader) *ErrInSrc {\n\t// Setup the parameters\n\tparams := ErrParams{\n\t\tCode:    template.Code,\n\t\tTitle:   template.Title,\n\t\tSummary: template.Summary,\n\t\tDetail:  template.Detail,\n\t\tCause:   template.Cause,\n\t}\n\n\t// Read the locations\n\tfor _, tmplLoc := range template.Locations {\n\t\tvar location option.Option[*SrcLocation]\n\t\tswitch tmplLoc.Kind {\n\t\tcase encerrors.LocFile:\n\t\t\tparams.Summary += \"\\n\\nIn file: \" + tmplLoc.Filepath\n\t\t\tcontinue\n\t\tcase encerrors.LocGoNode:\n\t\t\tlocation = FromGoASTNode(fileset, tmplLoc.GoNode, fileReaders...)\n\t\tcase encerrors.LocGoPos:\n\t\t\tlocation = FromGoTokenPos(fileset, tmplLoc.GoStartPos, tmplLoc.GoEndPos, fileReaders...)\n\t\tcase encerrors.LocGoPositions:\n\t\t\tlocation = FromGoTokenPositions(tmplLoc.GoStartPosition, tmplLoc.GoEndPosition, fileReaders...)\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unknown location kind: %v\", tmplLoc.Kind))\n\t\t}\n\n\t\tloc, ok := location.Get()\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch tmplLoc.LocType {\n\t\tcase encerrors.LocError:\n\t\t\tloc.Type = LocError\n\t\tcase encerrors.LocWarning:\n\t\t\tloc.Type = LocWarning\n\t\tcase encerrors.LocHelp:\n\t\t\tloc.Type = LocHelp\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unknown location type: %v\", tmplLoc.LocType))\n\t\t}\n\n\t\tloc.Text = tmplLoc.Text\n\n\t\tparams.Locations = append(params.Locations, loc)\n\t}\n\n\t// Create the error\n\treturn New(params, template.AlwaysIncludeStack)\n}\n\n// TerminalWidth is the width of the terminal in columns that we're rendering to.\n//\n// We default to 100 characters, but the CLI overrides this when it renders errors.\n// When using the value, note it might be very small (e.g. 5) if the user has shrunk\n// their terminal window super small. Thus any code which uses this or creates new\n// widths off it should cope with <= 0 values.\nvar TerminalWidth = 100\n\nfunc (e *ErrInSrc) Unwrap() error {\n\treturn e.Params.Cause\n}\n\n// StackTrace implements the StackTraceProvider interface for some libraries\n// including ZeroLog, xerrors and Sentry\nfunc (e *ErrInSrc) StackTrace() errors.StackTrace {\n\tframes := make([]errors.Frame, len(e.Stack))\n\tfor i, frame := range e.Stack {\n\t\t// Note: interpreted as a uintptr its value represents the program counter + 1.\n\t\tframes[i] = errors.Frame(frame.ProgramCounter + 1)\n\t}\n\treturn frames\n}\n\nfunc (e *ErrInSrc) Is(target error) bool {\n\tif target == nil || e == nil {\n\t\treturn target == e\n\t}\n\n\tif target, ok := target.(*ErrInSrc); ok && target != nil {\n\t\treturn target.Params.Title == e.Params.Title\n\t}\n\treturn false\n}\n\nfunc (e *ErrInSrc) As(target any) bool {\n\tif target, ok := target.(*ErrInSrc); ok {\n\t\t*target = *e\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Bailout is a helper function which will abort the current process\n// and report the error\nfunc (e *ErrInSrc) Bailout() {\n\tpanic(Bailout{List: List{e}})\n}\n\nfunc (e *ErrInSrc) Title() string {\n\treturn e.Params.Title\n}\n\nfunc (e *ErrInSrc) Error() string {\n\tvar b strings.Builder\n\n\t// Write the header\n\tconst headerGrayLevel = 12\n\tconst spacing = 4 + 2 + 7 // (4 = \"--\" on both sides, 2 = \" \" on the sides of the title, 7 = \"[E0000]\")\n\tb.WriteRune('\\n')         // Always start with a new line as these errors are expected to be full screen\n\tb.WriteString(aurora.Gray(headerGrayLevel, fmt.Sprintf(\"%c%c \", set.HorizontalBar, set.HorizontalBar)).String())\n\tb.WriteString(aurora.Red(e.Params.Title).String())\n\tb.WriteByte(' ')\n\theaderWidth := TerminalWidth - len(e.Params.Title) - spacing\n\tif headerWidth > 0 {\n\t\tb.WriteString(aurora.Gray(headerGrayLevel, strings.Repeat(string(set.HorizontalBar), headerWidth)).String())\n\t}\n\tb.WriteString(aurora.Gray(headerGrayLevel, fmt.Sprintf(\"%cE%04d%c\", set.LeftBracket, e.Params.Code, set.RightBracket)).String())\n\tb.WriteString(aurora.Gray(headerGrayLevel, fmt.Sprintf(\"%c%c\\n\\n\", set.HorizontalBar, set.HorizontalBar)).String())\n\n\t// Write the summary\n\tif e.Params.Summary != \"\" {\n\t\twordWrap(e.Params.Summary, &b)\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\t// List the root causes\n\tif len(e.Params.Locations) > 0 {\n\t\tfor _, causes := range e.Params.Locations.GroupByFile() {\n\t\t\trenderSrc(&b, causes)\n\t\t\tb.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Write any details out\n\tif e.Params.Detail != \"\" {\n\t\twordWrap(e.Params.Detail, &b)\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\t// Write the Stack trace out (for where the error was generated within Encore's source)\n\tif len(e.Stack) > 0 {\n\t\tprettyPrintStack(e.Stack, &b)\n\t}\n\n\treturn b.String()\n}\n\nfunc (e *ErrInSrc) OnSameLine(other *ErrInSrc) bool {\n\tfor _, loc := range e.Params.Locations {\n\t\tfor _, otherLoc := range other.Params.Locations {\n\t\t\tif loc.Start.Line >= otherLoc.Start.Line && loc.End.Line <= otherLoc.End.Line {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// WithGoNode adds a Go AST node to the error\nfunc (e *ErrInSrc) WithGoNode(fileset *token.FileSet, node ast.Node, fileReaders ...paths.FileReader) {\n\tif val, ok := FromGoASTNode(fileset, node, fileReaders...).Get(); ok {\n\t\te.Params.Locations = append(e.Params.Locations, val)\n\t}\n}\n"
  },
  {
    "path": "pkg/errinsrc/internal/cuelocation.go",
    "content": "package internal\n\nimport (\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"cuelang.org/go/cue/ast\"\n\tcueerrors \"cuelang.org/go/cue/errors\"\n\t\"cuelang.org/go/cue/parser\"\n\t\"github.com/rs/zerolog/log\"\n)\n\n// LocationsFromCueError returns a list of SrcLocations based on what was given in the\n// cueerror.Error.\nfunc LocationsFromCueError(err cueerrors.Error, pathPrefix string) SrcLocations {\n\t// Convert cueerror.Pos to a *CueLocation\n\trtn := make(SrcLocations, 0, len(err.InputPositions()))\n\n\tif pos := err.Position(); pos.IsValid() {\n\t\trtn = append(rtn, FromCueTokenPos(pos, pathPrefix))\n\t}\n\n\tfor _, pos := range err.InputPositions() {\n\t\tif pos.IsValid() {\n\t\t\trtn = append(rtn, FromCueTokenPos(pos, pathPrefix))\n\t\t}\n\t}\n\tsort.Sort(rtn)\n\n\treturn rtn\n}\n\n// FromCueTokenPos converts a cueerror.Pos to a SrcLocation\n//\n// We use an interface for `cue/token.Pos` so we can test it\nfunc FromCueTokenPos(cueLoc interface {\n\tFilename() string\n\tLine() int\n\tColumn() int\n}, pathPrefix string) *SrcLocation {\n\t// Note: for CUE files we must read the bytes now, as the defer on the CUE load code will delete\n\t// the source file form the disk before this location is rendered\n\tbytes, err := os.ReadFile(cueLoc.Filename())\n\tif err != nil {\n\t\tlog.Err(err).Str(\"filename\", cueLoc.Filename()).Msg(\"Failed to read CUE file\")\n\t\t// Don't return, `bytes == nil` is fine here\n\t}\n\n\tstart := Pos{Line: cueLoc.Line(), Col: cueLoc.Column()}\n\tend := convertSingleCUEPositionToRange(cueLoc.Filename(), bytes, start)\n\n\treturn &SrcLocation{\n\t\tFile: &File{\n\t\t\tRelPath:  strings.TrimPrefix(cueLoc.Filename(), pathPrefix),\n\t\t\tFullPath: cueLoc.Filename(),\n\t\t\tContents: bytes,\n\t\t},\n\t\tStart: start,\n\t\tEnd:   end,\n\t\tType:  LocError,\n\t}\n}\n\n// convertSingleCUEPositionToRange attempts to convert a CUE error from a single position to a range\n//\n// It does this by running the CUE parser over the file and looking for the AST node that starts\n// at the same line and column. Once found, we use the end position of that node as the end position\n// of the error.\nfunc convertSingleCUEPositionToRange(filename string, bytes []byte, start Pos) Pos {\n\tfile, err := parser.ParseFile(filename, bytes, parser.ParseComments)\n\tif err != nil {\n\t\treturn start\n\t}\n\n\tvar matching ast.Node\n\tast.Walk(file, func(node ast.Node) bool {\n\t\tif node.Pos().Line() == start.Line && node.Pos().Column() == start.Col {\n\t\t\tmatching = node\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t}, nil)\n\n\tif matching != nil {\n\t\treturn Pos{Line: matching.End().Line(), Col: matching.End().Column()}\n\t}\n\treturn start\n}\n"
  },
  {
    "path": "pkg/errinsrc/internal/golocation.go",
    "content": "package internal\n\nimport (\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n)\n\nfunc FromGoASTNodeWithTypeAndText(fileset *token.FileSet, node ast.Node, typ LocationType, text string, fileReaders ...paths.FileReader) option.Option[*SrcLocation] {\n\tloc := FromGoASTNode(fileset, node, fileReaders...)\n\tif l, ok := loc.Get(); ok {\n\t\tl.Type = typ\n\t\tl.Text = text\n\t}\n\treturn loc\n}\n\n// FromGoASTNode returns a SrcLocation from a Go AST node storing the start and end\n// locations of that node.\nfunc FromGoASTNode(fileset *token.FileSet, node ast.Node, fileReaders ...paths.FileReader) option.Option[*SrcLocation] {\n\tstart := fileset.Position(node.Pos())\n\tend := fileset.Position(node.End())\n\n\t// Custom end locations for some node types\n\tswitch node := node.(type) {\n\tcase *ast.CallExpr:\n\t\tend = fileset.Position(node.Rparen + 1)\n\t}\n\n\tif !start.IsValid() || !end.IsValid() {\n\t\treturn option.None[*SrcLocation]()\n\t}\n\n\treturn FromGoTokenPositions(start, end, fileReaders...)\n}\n\nfunc FromGoTokenPos(fileset *token.FileSet, start, end token.Pos, fileReaders ...paths.FileReader) option.Option[*SrcLocation] {\n\tstartPos := fileset.Position(start)\n\tendPos := fileset.Position(end)\n\n\tif !startPos.IsValid() || !endPos.IsValid() {\n\t\treturn option.None[*SrcLocation]()\n\t}\n\n\treturn FromGoTokenPositions(startPos, endPos, fileReaders...)\n}\n\n// FromGoTokenPositions returns a SrcLocation from two Go token positions.\n// They can be the same position or different positions. However, they must\n// be locations within the same file.\n//\n// This function will panic if the locations are in different files.\nfunc FromGoTokenPositions(start token.Position, end token.Position, fileReaders ...paths.FileReader) option.Option[*SrcLocation] {\n\tif start.Filename != end.Filename {\n\t\tpanic(\"FromGoASTNode: start and end files must be the same\")\n\t}\n\tfileReaders = append(fileReaders, os.ReadFile)\n\tvar bytes []byte\n\tvar err error\n\tfor _, reader := range fileReaders {\n\t\tif reader == nil {\n\t\t\tcontinue\n\t\t}\n\t\tbytes, err = reader(start.Filename)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t}\n\tif err != nil {\n\t\tlog.Err(err).Str(\"filename\", start.Filename).Msg(\"Failed to read Go file\")\n\t\t// Don't return, `bytes == nil` is fine here\n\t}\n\n\t// Attempt to convert a single start/end position into a range\n\tif start == end {\n\t\tend = convertSingleGoPositionToRange(start.Filename, bytes, start)\n\t}\n\n\t// If either position is invalid, return nil\n\t// as that means we're not dealing with a Go Token Position\n\tif !start.IsValid() || !end.IsValid() {\n\t\tlog.Warn().Str(\"start\", start.String()).Str(\"end\", end.String()).Msg(\"Invalid Go token position\")\n\t\treturn option.None[*SrcLocation]()\n\t}\n\n\treturn option.Some(&SrcLocation{\n\t\tFile: &File{\n\t\t\tRelPath:  start.Filename,\n\t\t\tFullPath: start.Filename,\n\t\t\tContents: bytes,\n\t\t},\n\t\tStart: Pos{Line: start.Line, Col: start.Column},\n\t\tEnd:   Pos{Line: end.Line, Col: end.Column},\n\t\tType:  LocError,\n\t})\n}\n\n// convertSingleGoPositionToRange attempts to convert a single Go token position to a range with a start and end\n// position.\n//\n// This is done by attempting to parse the file, and then if we are able to parse it successfully, we look for an AST\n// node which starts at the exact line and column of the position. If we find one, we use the end position of that\n// node as the end position of the range.\n//\n// We use the first found node at that position, as we assume the largest node at that position is the most relevant\nfunc convertSingleGoPositionToRange(filename string, fileBody []byte, start token.Position) (end token.Position) {\n\tfs := token.NewFileSet()\n\tfile, err := parser.ParseFile(fs, filename, fileBody, parser.ParseComments)\n\tif err != nil || file == nil {\n\t\tend = start\n\t\t// If the file is not parsable for some reason (e.g. syntax error), we can't determine the end position\n\t\t// based on ast.Nodes. If so, we can fall back on guessing the end position by looking for common delimiters\n\t\toffset, ok := findPositionOffset(start, fileBody)\n\t\tif !ok {\n\t\t\treturn end\n\t\t}\n\t\tendOffset := GuessEndColumn(fileBody, offset)\n\t\tend.Column += endOffset - offset\n\t\treturn end\n\t}\n\n\tvar match ast.Node\n\tast.Inspect(file, func(n ast.Node) bool {\n\t\tif n == nil {\n\t\t\treturn true\n\t\t}\n\n\t\tnodePos := fs.Position(n.Pos())\n\t\tif nodePos.Line == start.Line && nodePos.Column == start.Column {\n\t\t\tmatch = n\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t})\n\n\tif match != nil {\n\t\treturn fs.Position(match.End())\n\t}\n\treturn start\n}\n\nfunc findPositionOffset(pos token.Position, data []byte) (int, bool) {\n\tline, col := 1, 1\n\tfor i, c := range data {\n\t\tif line == pos.Line && col == pos.Column {\n\t\t\treturn i, true\n\t\t} else if line > pos.Line {\n\t\t\treturn -1, false\n\t\t}\n\t\tif c == '\\n' {\n\t\t\tline++\n\t\t\tcol = 1\n\t\t} else {\n\t\t\tcol++\n\t\t}\n\t}\n\treturn -1, false\n}\n\nfunc GuessEndColumn(data []byte, offset int) int {\n\tvar params, brackets, braces int\n\tinBackticks := false\n\n\tfor i := offset; i < len(data); i++ {\n\t\tswitch data[i] {\n\t\tcase '(':\n\t\t\tparams++\n\t\tcase '[':\n\t\t\tbrackets++\n\t\tcase '{':\n\t\t\tbraces++\n\t\tcase ')':\n\t\t\tparams--\n\t\t\tif params <= 0 {\n\t\t\t\treturn i + 1\n\t\t\t}\n\t\tcase ']':\n\t\t\tbrackets--\n\t\t\tif brackets <= 0 {\n\t\t\t\treturn i + 1\n\t\t\t}\n\t\tcase '}':\n\t\t\tbraces--\n\t\t\tif braces <= 0 {\n\t\t\t\treturn i + 1\n\t\t\t}\n\t\tcase '`':\n\t\t\tinBackticks = !inBackticks\n\t\tcase ';', ',', ':', '\"', '\\'':\n\t\t\tif !inBackticks && params == 0 && brackets == 0 && braces == 0 {\n\t\t\t\treturn i + 1\n\t\t\t}\n\t\tcase ' ', '\\t', '\\n', '\\r':\n\t\t\tif params == 0 && brackets == 0 && braces == 0 {\n\t\t\t\treturn i + 1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn len(data) + 1\n}\n"
  },
  {
    "path": "pkg/errinsrc/internal/helper.go",
    "content": "package internal\n\n// ErrParams are used to create *errinsrc.ErrInSrc objects.\n//\n// It exists within an `internal` package so that it can only\n// be used by other packages within the `errinsrc` folder.\n// This is enforce through the Go compiler that all\n// errors are created inside the `srcerrors` subpackage.\ntype ErrParams struct {\n\tCode      int          `json:\"code\"`\n\tTitle     string       `json:\"title\"`\n\tSummary   string       `json:\"summary\"`\n\tDetail    string       `json:\"detail,omitempty\"`\n\tCause     error        `json:\"-\"`\n\tLocations SrcLocations `json:\"locations,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/errinsrc/internal/location.go",
    "content": "package internal\n\nimport (\n\t\"cmp\"\n\t\"slices\"\n\t\"sort\"\n\n\t\"encr.dev/pkg/option\"\n)\n\ntype LocationType uint8\n\nconst (\n\tLocError   LocationType = iota // Renders in red\n\tLocWarning                     // Renders in yellow\n\tLocHelp                        // Renders in blue\n)\n\ntype SrcLocation struct {\n\tType  LocationType `json:\"type\"`           // The type of this location\n\tText  string       `json:\"text,omitempty\"` // Optional text to render at this location\n\tFile  *File        `json:\"file,omitempty\"` // The file containing the error\n\tStart Pos          `json:\"start\"`          // The position this location starts at\n\tEnd   Pos          `json:\"end\"`            // The position this location ends at\n}\n\nfunc (s *SrcLocation) Less(other *SrcLocation) bool {\n\t// Order by type of location first (Err, then Warn, then Help)\n\t// as we always want errors rendered above warnings and warnings above help\n\t// if s.Type != other.Type {\n\t// \treturn s.Type < other.Type\n\t// }\n\n\t// Order by file first\n\tif s.File.FullPath != other.File.FullPath {\n\t\treturn s.File.FullPath < other.File.FullPath\n\t}\n\n\t// And then by the line number of where they start\n\tif s.Start.Line != other.Start.Line {\n\t\treturn s.Start.Line < other.Start.Line\n\t}\n\n\t// And then where they start on that line\n\tif s.Start.Col != other.Start.Col {\n\t\treturn s.Start.Col < other.Start.Col\n\t}\n\n\t// And which line they end on\n\tif s.End.Line != other.End.Line {\n\t\treturn s.End.Line < other.End.Line\n\t}\n\n\t// Then by ending column\n\tif s.End.Col != other.End.Col {\n\t\treturn s.End.Col < other.End.Col\n\t}\n\n\t// Finally, by the text in the location\n\tif s.Text != other.Text {\n\t\treturn s.Text < other.Text\n\t}\n\n\tif s.Type != other.Type {\n\t\treturn s.Type < other.Type\n\t}\n\n\treturn false\n}\n\ntype Pos struct {\n\tLine int `json:\"line\"`\n\tCol  int `json:\"col\"`\n}\n\ntype File struct {\n\tRelPath  string // The relative path within the project\n\tFullPath string // The full path to the file\n\tContents []byte // The contents of the file\n}\n\n// SrcLocations represents a list of locations\n// within the source code. It can be sorted and split\n// up into separate lists using GroupByFile\ntype SrcLocations []*SrcLocation\n\nvar _ sort.Interface = SrcLocations{}\n\nfunc (s SrcLocations) Len() int {\n\treturn len(s)\n}\n\nfunc (s SrcLocations) Less(i, j int) bool {\n\treturn s[i].Less(s[j])\n}\n\nfunc (s SrcLocations) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc NewSrcLocations(opts ...option.Option[*SrcLocation]) SrcLocations {\n\tvar rtn SrcLocations\n\tfor _, opt := range opts {\n\t\tif loc, ok := opt.Get(); ok {\n\t\t\trtn = append(rtn, loc)\n\t\t}\n\t}\n\treturn rtn\n}\n\n// GroupByFile groups all locations by file and returns a new\n// SrcLocations for each file.\n//\n// If a file has overlapping locations or two locations on the same line\n// then more than one SrcLocations will be returned for that file. This\n// is due to a limitation in the srcrender, and may be relaxed in the future.\nfunc (s SrcLocations) GroupByFile() []SrcLocations {\n\ttype locationGroup struct {\n\t\tfileName  string\n\t\tlocations SrcLocations\n\t}\n\n\tvar nonOverlappingLocations []*locationGroup\n\n\tinlineOnSameLine := func(a, b *SrcLocation) bool {\n\t\treturn a.Start.Line == a.End.Line &&\n\t\t\tb.Start.Line == b.End.Line &&\n\t\t\ta.Start.Line == b.Start.Line &&\n\t\t\ta.Text == \"\" && b.Text == \"\" && // Don't inline if there is text as we don't support rendering this yet\n\t\t\t((a.Start.Col > b.End.Col) ||\n\t\t\t\t(a.End.Col < b.Start.Col))\n\n\t}\n\n\t// Add locations to groups on the same file without overlaps\nnextOriginalLoc:\n\tfor _, loc := range s {\n\t\t// Attempt to match it into an existing group\n\t\tfor _, grp := range nonOverlappingLocations {\n\t\t\tif grp.fileName == loc.File.FullPath {\n\t\t\t\tfor _, other := range grp.locations {\n\t\t\t\t\tif other.Start.Line > loc.End.Line ||\n\t\t\t\t\t\tother.End.Line < loc.Start.Line ||\n\t\t\t\t\t\tinlineOnSameLine(other, loc) {\n\t\t\t\t\t\tgrp.locations = append(grp.locations, loc)\n\t\t\t\t\t\tcontinue nextOriginalLoc\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// if here we found no matching groups\n\t\tnonOverlappingLocations = append(nonOverlappingLocations, &locationGroup{\n\t\t\tfileName:  loc.File.FullPath,\n\t\t\tlocations: SrcLocations{loc},\n\t\t})\n\t}\n\n\t// Sort the locations in each group\n\trtn := make([]SrcLocations, len(nonOverlappingLocations))\n\tfor i, grp := range nonOverlappingLocations {\n\t\tsort.Sort(grp.locations)\n\t\trtn[i] = grp.locations\n\t}\n\n\t// Now sort the groups by the lowest location hint\n\t// this means that errors will be rendered first, then warnings, then help\n\t// if they are in different files\n\tslices.SortStableFunc(rtn, func(a, b SrcLocations) int {\n\t\tlowestA := LocHelp\n\t\tlowestB := LocHelp\n\n\t\tfor _, loc := range a {\n\t\t\tif loc.Type < lowestA {\n\t\t\t\tlowestA = loc.Type\n\t\t\t}\n\t\t}\n\n\t\tfor _, loc := range b {\n\t\t\tif loc.Type < lowestB {\n\t\t\t\tlowestB = loc.Type\n\t\t\t}\n\t\t}\n\n\t\treturn cmp.Compare(lowestA, lowestB)\n\t})\n\n\treturn rtn\n}\n"
  },
  {
    "path": "pkg/errinsrc/list.go",
    "content": "package errinsrc\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n)\n\n// List is a list of ErrInSrc objects.\ntype List []*ErrInSrc\n\nvar _ sort.Interface = List{}\nvar _ ErrorList = List{}\nvar _ error = List{}\n\nfunc (l List) Len() int {\n\treturn len(l)\n}\n\nfunc (l List) Less(i, j int) bool {\n\t// This less function follows (as much as possible) the behaviour\n\t// of scanner.ErrorList's sort, that is filename, then line, then column\n\t// We then move onto extra data only Encore has\n\tiErr, jErr := l[i], l[j]\n\n\tnumLocationsToCompare := len(iErr.Params.Locations)\n\tif otherNum := len(jErr.Params.Locations); otherNum < numLocationsToCompare {\n\t\tnumLocationsToCompare = otherNum\n\t}\n\n\tfor idx := 0; idx < numLocationsToCompare; idx++ {\n\t\tiLoc, jLoc := iErr.Params.Locations[idx], jErr.Params.Locations[idx]\n\t\tif iLoc.Less(jLoc) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Now our custom sort logic\n\tif iErr.Params.Code != jErr.Params.Code {\n\t\treturn iErr.Params.Code < jErr.Params.Code\n\t}\n\n\tif iErr.Params.Title != jErr.Params.Title {\n\t\treturn iErr.Params.Title < jErr.Params.Title\n\t}\n\n\tif iErr.Params.Summary != jErr.Params.Summary {\n\t\treturn iErr.Params.Summary < jErr.Params.Summary\n\t}\n\n\tif iErr.Params.Detail != jErr.Params.Detail {\n\t\treturn iErr.Params.Detail < jErr.Params.Detail\n\t}\n\n\tif len(iErr.Params.Locations) != len(jErr.Params.Locations) {\n\t\treturn len(iErr.Params.Locations) < len(jErr.Params.Locations)\n\t}\n\n\treturn false\n}\n\nfunc (l List) Swap(i, j int) {\n\tl[i], l[j] = l[j], l[i]\n}\n\nfunc (l List) Error() string {\n\tswitch len(l) {\n\tcase 0:\n\t\treturn \"no errors\"\n\tcase 1:\n\t\treturn l[0].Error()\n\t}\n\treturn fmt.Sprintf(\"%s (and %d more errors)\", l[0], len(l)-1)\n}\nfunc (l List) ErrorList() []*ErrInSrc { return l }\n"
  },
  {
    "path": "pkg/errinsrc/setup_test.go",
    "content": "package errinsrc\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"encr.dev/pkg/golden\"\n)\n\nvar testDataFullPath string\n\nfunc TestMain(m *testing.M) {\n\tColoursInErrors(false)\n\tgolden.TestMain(m)\n}\n\nfunc init() {\n\tvar err error\n\ttestDataFullPath, err = os.Getwd()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\ttestDataFullPath = path.Join(testDataFullPath, \"testdata\")\n}\n"
  },
  {
    "path": "pkg/errinsrc/srcerrors/errors.go",
    "content": "package srcerrors\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/scanner\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/tools/go/packages\"\n\n\t\"encr.dev/pkg/errinsrc\"\n\t. \"encr.dev/pkg/errinsrc/internal\"\n\t\"encr.dev/pkg/paths\"\n)\n\n// UnhandledPanic is an error we use to wrap a panic that was not handled\n// It should ideally never be seen by users, but if it is, it means we have\n// a bug within Encore which needs fixing.\nfunc UnhandledPanic(recovered any) error {\n\tif err := errinsrc.ExtractFromPanic(recovered); err != nil {\n\t\treturn err\n\t}\n\n\t// If recovered is an error, then track it as the source\n\tvar srcError error\n\tif err, ok := recovered.(error); ok {\n\t\tsrcError = err\n\t}\n\t// If we get here, it's an unhandled panic / error\n\treturn errinsrc.New(ErrParams{\n\t\tCode:    1,\n\t\tTitle:   \"Internal compiler error\",\n\t\tSummary: fmt.Sprintf(\"An unhandled panic occurred in the Encore compiler: %v\", recovered),\n\t\tDetail:  internalErrReportToEncore,\n\t\tCause:   srcError,\n\t}, true)\n}\n\n// GenericGoParserError reports an error was that was reported from the Go parser.\n// It should not be returned by any errors caused by Encore's own parser as they\n// should have specific errors listed below\nfunc GenericGoParserError(err *scanner.Error, fileReaders ...paths.FileReader) *errinsrc.ErrInSrc {\n\tlocs := SrcLocations{}\n\tif pos, ok := FromGoTokenPositions(err.Pos, err.Pos, fileReaders...).Get(); ok {\n\t\tlocs = SrcLocations{pos}\n\t}\n\n\treturn errinsrc.New(ErrParams{\n\t\tCode:      2,\n\t\tTitle:     \"Parse Error in Go Source\",\n\t\tSummary:   err.Msg,\n\t\tCause:     err,\n\t\tLocations: locs,\n\t}, false)\n}\n\n// GenericGoPackageError reports an error was that was reported from the Go package loader.\n// It should not be returned by any errors caused by Encore's own parser as they\n// should have specific errors listed below\nfunc GenericGoPackageError(err packages.Error, fileReaders ...paths.FileReader) *errinsrc.ErrInSrc {\n\tvar locations SrcLocations\n\n\t// Extract the position from the error\n\tvar pos token.Position\n\tswitch p := strings.SplitN(err.Pos, \":\", 3); len(p) {\n\tcase 3:\n\t\tpos.Column, _ = strconv.Atoi(p[2])\n\t\tfallthrough\n\tcase 2:\n\t\tpos.Line, _ = strconv.Atoi(p[1])\n\t\tfallthrough\n\tcase 1:\n\t\tif p[0] != \"\" && p[0] != \"-\" {\n\t\t\tpos.Filename = p[0]\n\t\t}\n\t}\n\tif pos.Filename != \"\" && pos.Line > 0 {\n\t\tlocations = NewSrcLocations(FromGoTokenPositions(pos, pos, fileReaders...))\n\t}\n\n\treturn errinsrc.New(ErrParams{\n\t\tCode:      3,\n\t\tTitle:     \"Go Package Error\",\n\t\tSummary:   err.Msg,\n\t\tCause:     err,\n\t\tLocations: locations,\n\t}, false)\n}\n\n// GenericGoCompilerError reports an error was that was reported from the Go compiler.\n// It should not be returned by any errors caused by Encore's own compiler as they\n// should have specific errors listed below.\nfunc GenericGoCompilerError(fileName string, lineNumber int, column int, error string, fileReaders ...paths.FileReader) error {\n\terrLocation := token.Position{\n\t\tFilename: fileName,\n\t\tOffset:   0,\n\t\tLine:     lineNumber,\n\t\tColumn:   column,\n\t}\n\n\treturn errinsrc.New(ErrParams{\n\t\tCode:      3,\n\t\tTitle:     \"Go Compilation Error\",\n\t\tSummary:   strings.TrimSpace(error),\n\t\tLocations: NewSrcLocations(FromGoTokenPositions(errLocation, errLocation, fileReaders...)),\n\t}, false)\n}\n\n// StandardLibraryError is an error that is not caused by Encore, but is\n// returned by a standard library function. We wrap it in an ErrInSrc so that\n// we can still possibly provide a source location.\nfunc StandardLibraryError(err error) *errinsrc.ErrInSrc {\n\treturn errinsrc.New(ErrParams{\n\t\tCode:    3,\n\t\tTitle:   \"Error\",\n\t\tSummary: err.Error(),\n\t\tCause:   err,\n\t}, true)\n}\n\n// GenericError is a place holder for errors reported through perr.Add or perr.Addf\nfunc GenericError(pos token.Position, msg string, fileReaders ...paths.FileReader) *errinsrc.ErrInSrc {\n\treturn errinsrc.New(ErrParams{\n\t\tCode:      3,\n\t\tTitle:     \"Error\",\n\t\tSummary:   msg,\n\t\tLocations: NewSrcLocations(FromGoTokenPositions(pos, pos, fileReaders...)),\n\t}, false)\n}\n\nfunc UnableToLoadCUEInstances(err error, pathPrefix string) error {\n\treturn handleCUEError(err, pathPrefix, ErrParams{\n\t\tCode:  6,\n\t\tTitle: \"Unable to load CUE instances\",\n\t})\n}\n\nfunc UnableToAddOrphanedCUEFiles(err error, pathPrefix string) error {\n\treturn handleCUEError(err, pathPrefix, ErrParams{\n\t\tCode:  7,\n\t\tTitle: \"Unable to add orphaned CUE files\",\n\t})\n}\n\nfunc CUEEvaluationFailed(err error, pathPrefix string) error {\n\treturn handleCUEError(err, pathPrefix, ErrParams{\n\t\tCode:  8,\n\t\tTitle: \"CUE evaluation failed\",\n\t\tDetail: \"While evaluating the CUE configuration to generate a concrete configuration for your application, CUE returned an error. \" +\n\t\t\t\"This is usually caused by either a constraint on a field being unsatisfied or there being two different values for a given field. \" +\n\t\t\t\"For more information on CUE and this error, see https://cuelang.org/docs/\",\n\t})\n}\n\nfunc ResourceNameReserved(fileset *token.FileSet, node ast.Node, resourceType string, paramName string, name, reservedPrefix string, isSnakeCase bool, fileReaders ...paths.FileReader) error {\n\tsuggestion := \"\"\n\tif strings.HasPrefix(name, reservedPrefix) { // should always be the case, but better to be safe\n\t\tsuggestion = fmt.Sprintf(\"try %q?\", name[len(reservedPrefix):])\n\t}\n\n\tvar detail string\n\tif isSnakeCase {\n\t\tdetail = resourceNameHelpSnakeCase(resourceType, paramName)\n\t} else {\n\t\tdetail = resourceNameHelpKebabCase(resourceType, paramName)\n\t}\n\n\treturn errinsrc.New(ErrParams{\n\t\tCode:  37,\n\t\tTitle: \"Reserved resource name\",\n\t\t// The metrics.NewCounter metric name \"e_blah\" uses the reserved prefix \"e_\".\n\t\tSummary:   fmt.Sprintf(\"The %s %s %q uses the reserved prefix %q\", resourceType, paramName, name, reservedPrefix),\n\t\tDetail:    detail,\n\t\tLocations: NewSrcLocations(FromGoASTNodeWithTypeAndText(fileset, node, LocError, suggestion, fileReaders...)),\n\t}, false)\n}\n"
  },
  {
    "path": "pkg/errinsrc/srcerrors/helpers.go",
    "content": "package srcerrors\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"reflect\"\n\t\"strings\"\n\n\tcueerrors \"cuelang.org/go/cue/errors\"\n\n\t\"encr.dev/pkg/errinsrc\"\n\t. \"encr.dev/pkg/errinsrc/internal\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\nfunc handleCUEError(err error, pathPrefix string, param ErrParams) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\ttoReturn := make(errinsrc.List, 0, 1)\n\n\tif param.Detail == \"\" {\n\t\tparam.Detail = \"For more information on CUE and this error, see https://cuelang.org/docs/\"\n\t}\n\n\tfor _, e := range cueerrors.Errors(err) {\n\t\tparam.Summary = e.Error()\n\t\tparam.Cause = e\n\t\tparam.Locations = LocationsFromCueError(e, pathPrefix)\n\t\ttoReturn = append(toReturn, errinsrc.New(param, false))\n\t}\n\n\tswitch len(toReturn) {\n\tcase 0:\n\t\treturn nil\n\tcase 1:\n\t\treturn toReturn[0]\n\tdefault:\n\t\treturn toReturn\n\t}\n}\n\n// Converts a node to a string which looks like the original go code.\n// such as a ast.SelectorExpr will become \"foo.Blah\"\n//\n// It's not intended to be an exact representation, but rather a helperful\n// representation for error messages.\nfunc nodeAsGoSrc(node ast.Node) string {\n\tswitch node := node.(type) {\n\tcase *ast.Ident:\n\t\treturn node.Name\n\n\tcase *ast.SelectorExpr:\n\t\treturn fmt.Sprintf(\"%s.%s\", nodeAsGoSrc(node.X), node.Sel.Name)\n\n\tcase *ast.IndexExpr:\n\t\treturn fmt.Sprintf(\"%s[%s]\", nodeAsGoSrc(node.X), nodeAsGoSrc(node.Index))\n\n\tcase *ast.IndexListExpr:\n\t\tindices := make([]string, 0, len(node.Indices))\n\t\tfor _, n := range node.Indices {\n\t\t\tindices = append(indices, nodeAsGoSrc(n))\n\t\t}\n\t\treturn fmt.Sprintf(\"%s[%s]\", nodeAsGoSrc(node.X), strings.Join(indices, \", \"))\n\n\tcase *ast.FuncLit:\n\t\treturn \"a function literal\"\n\n\tcase *ast.BasicLit:\n\t\treturn node.Value\n\n\tcase *ast.CallExpr:\n\t\treturn fmt.Sprintf(\"%s(...)\", nodeAsGoSrc(node.Fun))\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(node))\n\t}\n}\n\n// Converts a node to a string that can be used in an error message.\n// such as a ast.CallExpr will return \"a function call to foo.Blah\"\nfunc nodeType(node ast.Node) string {\n\tswitch node := node.(type) {\n\tcase *ast.Ident:\n\t\treturn \"an identifier\"\n\n\tcase *ast.SelectorExpr:\n\t\treturn \"an identifier\"\n\n\tcase *ast.IndexExpr:\n\t\treturn \"a identifier\"\n\n\tcase *ast.IndexListExpr:\n\t\treturn \"a identifier\"\n\n\tcase *ast.FuncLit:\n\t\treturn \"a function literal\"\n\n\tcase *ast.BasicLit:\n\t\tswitch node.Kind {\n\t\tcase token.INT:\n\t\t\treturn \"an integer literal\"\n\t\tcase token.FLOAT:\n\t\t\treturn \"a float literal\"\n\t\tcase token.IMAG:\n\t\t\treturn \"an imaginary literal\"\n\t\tcase token.CHAR:\n\t\t\treturn \"a character literal\"\n\t\tcase token.STRING:\n\t\t\treturn \"a string literal\"\n\t\tdefault:\n\t\t\treturn \"a literal\"\n\t\t}\n\n\tcase *ast.CallExpr:\n\t\treturn fmt.Sprintf(\"a function call to %s\", nodeAsGoSrc(node.Fun))\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(node))\n\t}\n}\n\n// Converts a schema type to a string that can be used in an error message.\n// such as a ast.CallExpr will return \"a function call to foo.Blah\"\nfunc schemaType(typ *schema.Type) string {\n\tswitch tt := typ.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\treturn \"a named type\"\n\n\tcase *schema.Type_Struct:\n\t\treturn \"a struct type\"\n\n\tcase *schema.Type_Map:\n\t\treturn \"a map type\"\n\n\tcase *schema.Type_Literal:\n\t\treturn \"a literal\"\n\n\tcase *schema.Type_Union:\n\t\treturn \"a union type\"\n\n\tcase *schema.Type_List:\n\t\treturn \"a list type\"\n\n\tcase *schema.Type_Builtin:\n\t\treturn fmt.Sprintf(\"a builtin type (%s)\", tt.Builtin)\n\n\tcase *schema.Type_Pointer:\n\t\treturn \"a pointer to \" + schemaType(tt.Pointer.Base)\n\n\tcase *schema.Type_Option:\n\t\treturn \"an optional \" + schemaType(tt.Option.Value)\n\n\tcase *schema.Type_TypeParameter:\n\t\treturn \"a type parameter\"\n\n\tcase *schema.Type_Config:\n\t\treturn \"a config value\"\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(tt))\n\t}\n}\n"
  },
  {
    "path": "pkg/errinsrc/srcerrors/helptext.go",
    "content": "package srcerrors\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc combine(parts ...string) string {\n\treturn strings.Join(parts, \"\\n\\n\")\n}\n\nconst (\n\tinternalErrReportToEncore = \"This is a bug in Encore and should not have occurred. Please report this issue to the \" +\n\t\t\"Encore team either on Github at https://github.com/encoredev/encore/issues/new and include this error.\"\n\n\tmakeService = \"To make this package a count as a service, this package or one of it's parents must have either one \" +\n\t\t\"or more API's declared within them or a PubSub subscription.\"\n\n\tconfigHelp = \"For more information on configuration, see https://encore.dev/docs/develop/config\"\n\n\tpubsubNewTopicHelp = \"For example `pubsub.NewTopic[MyMessage](\\\"my-topic\\\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })`\"\n\n\tpubsubNewSubscriptionHelp = \"A pubsub subscription must have a unique name per topic and be given a handler function for processing the message. \" +\n\t\t\"The handler for the subscription must be defined in the same service as the call to pubsub.NewSubscription and can be an inline function. \" +\n\t\t\"For example:\\n\" +\n\t\t\"\\tpubsub.NewSubscription(myTopic, \\\"subscription-name\\\", pubsub.SubscriptionConfig[MyMessage]{\\n\" +\n\t\t\"\\t\\tHandler: func(ctx context.Context, event MyMessage) error { return nil },\\n\" +\n\t\t\"\\t})\"\n\n\tpubsubHelp = \"For more information on PubSub, see https://encore.dev/docs/primitives/pubsub\"\n\n\tmetricsHelp = \"For more information on metrics, see https://encore.dev/docs/observability/metrics\"\n\n\tserviceHelp = \"For more information on services and how to define them, see https://encore.dev/docs/primitives/services\"\n\n\tauthHelp = \"For more information on auth handlers and how to define them, see https://encore.dev/docs/develop/auth\"\n)\n\nfunc resourceNameHelpKebabCase(resourceName string, paramName string) string {\n\treturn fmt.Sprintf(\"%s %s's must be defined as string literals, \"+\n\t\t\"be between 1 and 63 characters long, and defined in \\\"kebab-case\\\", meaning it must start with a letter, end with a letter \"+\n\t\t\"or number and only contain lower case letters, numbers and dashes.\",\n\t\tresourceName, paramName,\n\t)\n}\n\nfunc resourceNameHelpSnakeCase(resourceName string, paramName string) string {\n\treturn fmt.Sprintf(\"%s %s's must be defined as string literals, \"+\n\t\t\"be between 1 and 63 characters long, and defined in \\\"snake_case\\\", meaning it must start with a letter, end with a letter \"+\n\t\t\"or number and only contain lower case letters, numbers and underscores.\",\n\t\tresourceName, paramName,\n\t)\n}\n"
  },
  {
    "path": "pkg/errinsrc/srcrender.go",
    "content": "package errinsrc\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/alecthomas/chroma/quick\"\n\t\"github.com/jwalton/go-supportscolor\"\n\tauroraPkg \"github.com/logrusorgru/aurora/v3\"\n\t\"github.com/rs/zerolog/log\"\n\n\t. \"encr.dev/pkg/errinsrc/internal\"\n)\n\nconst grayLevelOnLineNumbers = 12\nconst endEscape = \"\\x1b[0m\"\n\nconst tabSize = 4\n\nvar aurora auroraPkg.Aurora\nvar enableColors bool\n\nfunc init() {\n\tColoursInErrors(supportscolor.Stdout().SupportsColor)\n}\n\nfunc ColoursInErrors(enabled bool) {\n\tenableColors = enabled\n\taurora = auroraPkg.NewAurora(enabled)\n}\n\n// renderSrc returns the lines of code surrounding the location with a pointer to the error on the error line\nfunc renderSrc(builder *strings.Builder, causes SrcLocations) {\n\tconst linesBeforeError = 2\n\tconst linesAfterError = 2\n\n\tidx := 0\n\tcurrentCause := causes[idx]\n\tlastEnd := causes[len(causes)-1].End\n\n\t// Check if any of the causes are multiline\n\tmultilineSpace := 0\n\tfor _, cause := range causes {\n\t\tif cause.Start.Line != cause.End.Line {\n\t\t\tmultilineSpace = 4\n\t\t\tbreak\n\t\t}\n\t}\n\n\tnumDigitsInLineNumbers := int(math.Log10(float64(lastEnd.Line+linesAfterError+1))) + 1\n\tlineNumberFmt := fmt.Sprintf(\" %%%dd %c \", numDigitsInLineNumbers, set.VerticalBar)\n\n\t// Render the filename\n\tbuilder.WriteString(strings.Repeat(\" \", numDigitsInLineNumbers+1))\n\tbuilder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\" %c%c%c\", set.LeftTop, set.HorizontalBar, set.LeftBracket)).String())\n\t// Note the space on both sides of this string is important\n\t// as it allows editors (such as GoLand) to pickup the filename in\n\t// terminal output and convert it into a clickable link into the code\n\tbuilder.WriteString(aurora.Cyan(fmt.Sprintf(\" %s:%d:%d \",\n\t\tcauses[0].File.RelPath,\n\t\tcauses[0].Start.Line,\n\t\tcauses[0].Start.Col,\n\t)).String())\n\tbuilder.WriteString(aurora.Gray(grayLevelOnLineNumbers, string(set.RightBracket)).String())\n\tbuilder.WriteRune('\\n')\n\tbuilder.WriteString(strings.Repeat(\" \", numDigitsInLineNumbers+2))\n\tbuilder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\"%c\", set.VerticalBar)).String())\n\tbuilder.WriteRune('\\n')\n\n\tvar currentLine int\n\tgapRenderedUntil := currentCause.Start.Line\n\tbBuffer := new(bytes.Buffer)\n\tbBuffer.Write(causes[0].File.Contents)\n\tsc := bufio.NewScanner(bBuffer)\nlinePrintLoop:\n\tfor sc.Scan() {\n\t\tcurrentLine++\n\n\t\tif currentLine >= currentCause.Start.Line-linesBeforeError && currentLine <= currentCause.End.Line+linesAfterError {\n\t\t\t// Write the line number\n\t\t\tbuilder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(lineNumberFmt, currentLine)).String())\n\n\t\t\t// If this is a multiline error, then render the gutters\n\t\t\tif multilineSpace > 0 {\n\t\t\t\tif currentCause.Start.Line <= currentLine && currentLine <= currentCause.End.Line {\n\t\t\t\t\tline := fmt.Sprintf(\"%c   \", set.VerticalBar)\n\n\t\t\t\t\tswitch currentLine {\n\t\t\t\t\tcase currentCause.Start.Line:\n\t\t\t\t\t\tif currentCause.Start.Col == 1 {\n\t\t\t\t\t\t\tline = fmt.Sprintf(\"%c%c%c \", set.LeftTop, set.HorizontalBar, set.RightArrow)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tline = strings.Repeat(\" \", multilineSpace)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase currentCause.End.Line:\n\t\t\t\t\t\tif currentCause.End.Col == 1 {\n\t\t\t\t\t\t\tline = fmt.Sprintf(\"%c%c%c \", set.LeftCross, set.HorizontalBar, set.RightArrow)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch currentCause.Type {\n\t\t\t\t\tcase LocError:\n\t\t\t\t\t\tbuilder.WriteString(aurora.BrightRed(line).String())\n\t\t\t\t\tcase LocWarning:\n\t\t\t\t\t\tbuilder.WriteString(aurora.BrightYellow(line).String())\n\t\t\t\t\tcase LocHelp:\n\t\t\t\t\t\tbuilder.WriteString(aurora.BrightBlue(line).String())\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tbuilder.WriteString(strings.Repeat(\" \", multilineSpace))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tunifiedLine := replaceTabsWithSpaces(sc.Text())\n\n\t\t\t// Then the line of code itself (attempting to highlight the syntax)\n\t\t\t// Note: we always use the \"go\" lexer, as it works nicely on CUE files too\n\t\t\tvar subBuilder strings.Builder\n\t\t\tif err := quick.Highlight(&subBuilder, unifiedLine, \"go\", \"terminal256\", \"monokai\"); err != nil || !enableColors {\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Error().AnErr(\"error\", err).Msg(\"Unable to highlight line of code\")\n\t\t\t\t}\n\t\t\t\tbuilder.WriteString(unifiedLine)\n\t\t\t} else {\n\t\t\t\tsyntaxHighlightedLine := subBuilder.String()\n\n\t\t\t\t// There's a bug in the quick.Highlight function that causes it sometimes add an extra line feed before the endEscape squence\n\t\t\t\t// so this if statement removes it\n\t\t\t\tif strings.HasSuffix(syntaxHighlightedLine, endEscape) {\n\t\t\t\t\tsyntaxHighlightedLine = strings.TrimRight(syntaxHighlightedLine[:len(syntaxHighlightedLine)-len(endEscape)], \" \\n\\r\\t\") + endEscape\n\t\t\t\t}\n\t\t\t\tbuilder.WriteString(syntaxHighlightedLine)\n\t\t\t}\n\t\t\tbuilder.WriteRune('\\n')\n\t\t} else if currentLine >= gapRenderedUntil && idx < len(causes) {\n\t\t\tgapRenderedUntil = currentCause.Start.Line\n\n\t\t\t// render two spaces for a break in the file\n\t\t\tfor i := 0; i < 2; i++ {\n\t\t\t\tlineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalBreak)\n\t\t\t\tbuilder.WriteString(\"\\n\")\n\t\t\t}\n\t\t}\n\n\t\terrorRendered := true\n\t\tcolOffset := 0\n\t\tfor errorRendered {\n\t\t\terrorRendered = false\n\n\t\t\tif currentCause.Start.Line < currentCause.End.Line {\n\t\t\t\t// If a multiline error then render the currentCause.Start() and currentCause.End() pointers\n\t\t\t\tcolor := aurora.BrightRed\n\t\t\t\tswitch currentCause.Type {\n\t\t\t\tcase LocWarning:\n\t\t\t\t\tcolor = aurora.BrightYellow\n\t\t\t\tcase LocHelp:\n\t\t\t\t\tcolor = aurora.BrightBlue\n\t\t\t\t}\n\t\t\t\tswitch currentLine {\n\t\t\t\tcase currentCause.Start.Line:\n\t\t\t\t\tif currentCause.Start.Col > 1 {\n\t\t\t\t\t\tcharCount := calcNumberCharactersForColumnNumber(sc.Text(), currentCause.Start.Col)\n\n\t\t\t\t\t\tlineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap)\n\t\t\t\t\t\tbuilder.WriteString(color(fmt.Sprintf(\"%s%c\", strings.Repeat(\" \", charCount+3), set.UpArrow)).String())\n\t\t\t\t\t\tbuilder.WriteString(\"\\n\")\n\n\t\t\t\t\t\tlineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap)\n\t\t\t\t\t\tbuilder.WriteString(color(fmt.Sprintf(\"%c%s%c\", set.LeftTop, strings.Repeat(string(set.HorizontalBar), charCount+2), set.RightBottom)).String())\n\t\t\t\t\t\tbuilder.WriteString(\"\\n\")\n\t\t\t\t\t}\n\t\t\t\tcase currentCause.End.Line:\n\t\t\t\t\tif currentCause.End.Col > 1 {\n\t\t\t\t\t\tcharCount := calcNumberCharactersForColumnNumber(sc.Text(), currentCause.End.Col)\n\n\t\t\t\t\t\tlineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap)\n\t\t\t\t\t\tbuilder.WriteString(color(fmt.Sprintf(\"%c%s%c\", set.VerticalBar, strings.Repeat(\" \", charCount+2), set.UpArrow)).String())\n\t\t\t\t\t\tbuilder.WriteString(\"\\n\")\n\n\t\t\t\t\t\tlineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap)\n\t\t\t\t\t\tbuilder.WriteString(color(fmt.Sprintf(\"%c%s%c\", set.LeftCross, strings.Repeat(string(set.HorizontalBar), charCount+2), set.RightBottom)).String())\n\t\t\t\t\t\tbuilder.WriteString(\"\\n\")\n\t\t\t\t\t}\n\n\t\t\t\t\t// And on the final line also render the error message\n\t\t\t\t\trenderErrorText(builder, 0, numDigitsInLineNumbers, \"\", 2, currentCause.Type, currentCause.Text, nil, true, true)\n\t\t\t\t\terrorRendered = true\n\t\t\t\t}\n\t\t\t} else if currentLine == currentCause.End.Line {\n\t\t\t\tvar startCol, endCol int\n\t\t\t\tstartCol = currentCause.Start.Col\n\t\t\t\tendCol = currentCause.End.Col\n\n\t\t\t\t// Try and guess the atom where the error is\n\t\t\t\t// if the currentCause.Start()/currentCause.End() point is the same position\n\t\t\t\tif endCol <= startCol {\n\t\t\t\t\tendCol = GuessEndColumn(sc.Bytes(), startCol)\n\t\t\t\t}\n\n\t\t\t\t// Work out how long the indicator is\n\t\t\t\tindicatorLength := endCol - startCol\n\t\t\t\tif indicatorLength <= 1 {\n\t\t\t\t\tindicatorLength = 1\n\t\t\t\t}\n\n\t\t\t\t// Create out the error lines\n\t\t\t\terrorLines := []string{\"\"}\n\t\t\t\terrorTextStart := indicatorLength + 1\n\n\t\t\t\tif indicatorLength >= 2 {\n\t\t\t\t\thalf := float64(indicatorLength-1) / 2\n\t\t\t\t\terrorTextStart = int(math.Floor(half)) + 2\n\t\t\t\t\tif currentCause.Text != \"\" {\n\t\t\t\t\t\terrorLines[0] = fmt.Sprintf(\n\t\t\t\t\t\t\t\"%s%c%s\",\n\t\t\t\t\t\t\tstrings.Repeat(string(set.HorizontalBar), int(math.Floor(half))),\n\t\t\t\t\t\t\tset.MiddleTop,\n\t\t\t\t\t\t\tstrings.Repeat(string(set.HorizontalBar), int(math.Ceil(half))),\n\t\t\t\t\t\t)\n\t\t\t\t\t} else {\n\t\t\t\t\t\terrorLines[0] = strings.Repeat(string(set.HorizontalBar), indicatorLength)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\terrorLines[0] = string(set.UpArrow)\n\t\t\t\t}\n\n\t\t\t\trenderNewLine := true\n\t\t\t\tif currentCause.Text == \"\" {\n\t\t\t\t\tif idx+1 < len(causes) {\n\t\t\t\t\t\tnextCause := causes[idx+1]\n\t\t\t\t\t\tif nextCause.Start.Line == currentLine && nextCause.End.Line == currentLine {\n\t\t\t\t\t\t\trenderNewLine = false\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trenderGutter := colOffset == 0\n\n\t\t\t\trenderErrorText(builder, startCol-colOffset, numDigitsInLineNumbers, sc.Text(), errorTextStart, currentCause.Type, currentCause.Text, errorLines, renderNewLine, renderGutter)\n\t\t\t\terrorRendered = true\n\t\t\t\tcolOffset = endCol\n\t\t\t}\n\n\t\t\tif errorRendered {\n\t\t\t\tidx = idx + 1\n\t\t\t\tif idx < len(causes) {\n\t\t\t\t\tcurrentCause = causes[idx]\n\t\t\t\t} else {\n\t\t\t\t\t// stop looking for errors on this line\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif currentLine > lastEnd.Line+linesAfterError {\n\t\t\t\t// stop printing al errors\n\t\t\t\tbreak linePrintLoop\n\t\t\t}\n\t\t}\n\t}\n\n\tbuilder.WriteString(aurora.Gray(grayLevelOnLineNumbers, strings.Repeat(string(set.HorizontalBar), numDigitsInLineNumbers+2)+string(set.RightBottom)).String())\n\tbuilder.WriteRune('\\n')\n}\n\nfunc lineNumberSpacer(builder *strings.Builder, numDigitsInLineNumbers int, r rune) {\n\tbuilder.WriteString(strings.Repeat(\" \", numDigitsInLineNumbers+1))\n\tbuilder.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\" %c \", r)).String())\n}\n\nfunc replaceTabsWithSpaces(line string) string {\n\tvar str strings.Builder\n\tvar col int\n\n\tfor _, r := range line {\n\t\tif r == '\\t' {\n\t\t\t// Tabs always count as at least 1 space\n\t\t\tstr.WriteRune(' ')\n\t\t\tcol++\n\n\t\t\t// We then align onto the next tab column\n\t\t\tfor col%tabSize != 0 {\n\t\t\t\tstr.WriteRune(' ')\n\t\t\t\tcol++\n\t\t\t}\n\t\t} else {\n\t\t\tstr.WriteRune(r)\n\t\t\tcol++\n\t\t}\n\t}\n\n\treturn str.String()\n}\n\n// calcNumberCharactersForColumnNumber calculates the number of monospaced characters we need\n// to render the given column number on the line - accounting for tab characters\nfunc calcNumberCharactersForColumnNumber(line string, col int) int {\n\tcount := 0\n\tfor i, r := range line {\n\t\tif r == '\\t' {\n\t\t\tcount++\n\t\t\tfor count%tabSize != 0 {\n\t\t\t\tcount++\n\t\t\t}\n\t\t} else {\n\t\t\tcount++\n\t\t}\n\n\t\tif i+1 >= col {\n\t\t\tbreak\n\t\t}\n\n\t}\n\treturn count\n}\n\nfunc renderErrorText(builder *strings.Builder, startCol int, numDigitsInLineNumbers int, srcLine string, errorTextStart int, typ LocationType, text string, errorLines []string, renderNewLine bool, renderGutter bool) {\n\tif text != \"\" {\n\t\tlines := splitTextOnWords(text, errorTextStart)\n\t\tfor i, line := range lines {\n\t\t\tprefix := strings.Repeat(\" \", errorTextStart+1)\n\t\t\tif i == 0 {\n\t\t\t\tprefix = fmt.Sprintf(\"%s%c%c \", strings.Repeat(\" \", errorTextStart-2), set.LeftBottom, set.HorizontalBar)\n\t\t\t}\n\n\t\t\terrorLines = append(errorLines, prefix+line)\n\t\t}\n\t}\n\n\tvar prefixWhitespace string\n\n\t// It's possible the start column references generated code; in that case reset\n\t// the column information as a fallback to prevent panics below.\n\tif startCol > len(srcLine) {\n\t\tstartCol = 0\n\t} else {\n\t\t// Compute the whitespace prefix we need on each line\n\t\t// (Note this will render tabs as tabs still if they are present)\n\t\tprefixWhitespace = strings.Repeat(\" \", calcNumberCharactersForColumnNumber(srcLine, startCol-1))\n\t}\n\n\t// Now write the error lines\n\tfor _, line := range errorLines {\n\t\tif renderGutter {\n\t\t\tlineNumberSpacer(builder, numDigitsInLineNumbers, set.VerticalGap)\n\t\t}\n\t\tbuilder.WriteString(prefixWhitespace)\n\n\t\tswitch typ {\n\t\tcase LocError:\n\t\t\tbuilder.WriteString(aurora.BrightRed(line).String())\n\t\tcase LocWarning:\n\t\t\tbuilder.WriteString(aurora.BrightYellow(line).String())\n\t\tcase LocHelp:\n\t\t\tbuilder.WriteString(aurora.BrightBlue(line).String())\n\t\t}\n\n\t\tif renderNewLine || len(errorLines) > 1 {\n\t\t\tbuilder.WriteString(\"\\n\")\n\t\t}\n\t}\n}\n\nfunc splitTextOnWords(text string, startingCol int) (rtn []string) {\n\ttext = strings.TrimSpace(text)\n\tmaxLineLength := TerminalWidth - startingCol\n\tif maxLineLength < 20 {\n\t\tmaxLineLength = 20\n\t}\n\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tif len(line) <= maxLineLength {\n\t\t\trtn = append(rtn, line)\n\t\t\tcontinue\n\t\t}\n\n\t\tlineStart := 0\n\t\tlastSpace := 0\n\t\tfor i := 0; i < len(line); i++ {\n\t\t\tif unicode.IsSpace(rune(line[i])) {\n\t\t\t\tif i-lineStart >= maxLineLength {\n\t\t\t\t\trtn = append(rtn, line[lineStart:lastSpace])\n\t\t\t\t\tlineStart = lastSpace + 1\n\t\t\t\t}\n\t\t\t\tlastSpace = i\n\t\t\t}\n\t\t}\n\n\t\tif len(line)-lineStart >= maxLineLength {\n\t\t\trtn = append(rtn, line[lineStart:lastSpace])\n\t\t\tlineStart = lastSpace + 1\n\t\t}\n\t\tif lineStart < len(line) {\n\t\t\trtn = append(rtn, line[lineStart:])\n\t\t}\n\n\t}\n\treturn\n}\n\nfunc wordWrap(text string, b *strings.Builder) {\n\tfor _, line := range splitTextOnWords(text, 0) {\n\t\tb.WriteString(line)\n\t\tb.WriteString(\"\\n\")\n\t}\n}\n"
  },
  {
    "path": "pkg/errinsrc/srcrender_test.go",
    "content": "package errinsrc\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\n\t. \"encr.dev/pkg/errinsrc/internal\"\n\t\"encr.dev/pkg/golden\"\n)\n\nfunc Test_renderSrc_Simple(t *testing.T) {\n\ttestParams := []struct {\n\t\ttestName     string\n\t\tline, column int\n\t\tlevel        LocationType\n\t\tmessage      string\n\t}{\n\t\t{testName: \"error no text\", line: 5, column: 7, level: LocError, message: \"\"},\n\t\t{testName: \"simple error\", line: 5, column: 2, level: LocError, message: \"What is a foo?\"},\n\t\t{testName: \"simple warning\", line: 10, column: 7, level: LocWarning, message: \"You sure about this?\"},\n\t\t{testName: \"simple help\", line: 13, column: 4, level: LocHelp, message: \"help: try a switch statement here\"},\n\t\t{testName: \"single character error\", line: 6, column: 11, level: LocError, message: \"This looks dodgy\"},\n\t\t{testName: \"multiline message\", line: 1, column: 9, level: LocError, message: \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum porttitor elementum. Duis eget cursus arcu, ut interdum lorem. Suspendisse vel diam et eros cursus vestibulum at et lacus. Nullam felis tellus, cursus nec arcu nec, laoreet maximus ante. Cras pellentesque est est, nec laoreet magna accumsan vel. Mauris commodo dui purus, non ullamcorper nisl commodo auctor. Vivamus finibus mi ut risus tempor pellentesque. Nulla facilisi. Nullam rhoncus neque porta erat molestie, at malesuada est aliquam. Etiam convallis lorem eget euismod eleifend. Phasellus sit amet diam in orci molestie pulvinar. Vestibulum non auctor dolor, vel imperdiet risus. Nam egestas et purus id sodales. Aliquam in metus varius, porta mi nec, ornare mauris.\\n\\nQuisque eu nisi vel nulla sodales pretium sed a dolor. Morbi convallis ornare ligula, ut aliquam velit auctor id. In in neque turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras et arcu id magna accumsan semper. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc semper rhoncus tincidunt. Vestibulum lacus erat, molestie ut ultrices convallis, placerat at lacus. Integer tempus tempus sodales. Integer sit amet est quam. Praesent vitae condimentum mi.\\n\\n\"},\n\t}\n\n\tfor _, tp := range testParams {\n\t\ttp := tp\n\t\tt.Run(tp.testName, func(t *testing.T) {\n\t\t\tloc := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), tp.line, tp.column}, testDataFullPath)\n\t\t\tloc.Text = tp.message\n\t\t\tloc.Type = tp.level\n\n\t\t\terr := New(ErrParams{\n\t\t\t\tCode:      1,\n\t\t\t\tTitle:     \"simple test error\",\n\t\t\t\tSummary:   \"There has been a simple error in your code\",\n\t\t\t\tDetail:    \"For more information please visit our great help documentation\",\n\t\t\t\tLocations: SrcLocations{loc},\n\t\t\t}, false)\n\n\t\t\ttestError(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_renderSrc_MultipleSeperateInSameFile(t *testing.T) {\n\tt.Run(\"spaced apart\", func(t *testing.T) {\n\t\tloc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 5, 7}, testDataFullPath)\n\t\tloc1.Text = \"defined as a boolean here\"\n\t\tloc2 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 13, 9}, testDataFullPath)\n\t\tloc2.Text = \"referenced here\"\n\n\t\terr := New(ErrParams{\n\t\t\tCode:      1,\n\t\t\tTitle:     \"simple test error\",\n\t\t\tSummary:   \"There has been a simple error in your code\",\n\t\t\tDetail:    \"For more information please visit our great help documentation\",\n\t\t\tLocations: SrcLocations{loc1, loc2},\n\t\t}, false)\n\n\t\ttestError(t, err)\n\t})\n\tt.Run(\"on following lines\", func(t *testing.T) {\n\t\tloc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 5, 7}, testDataFullPath)\n\t\tloc1.Text = \"this is weird\"\n\t\tloc2 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 6, 13}, testDataFullPath)\n\t\tloc2.Text = \"so is this!\"\n\n\t\terr := New(ErrParams{\n\t\t\tCode:      1,\n\t\t\tTitle:     \"simple test error\",\n\t\t\tSummary:   \"There has been a simple error in your code\",\n\t\t\tDetail:    \"For more information please visit our great help documentation\",\n\t\t\tLocations: SrcLocations{loc1, loc2},\n\t\t}, false)\n\n\t\ttestError(t, err)\n\t})\n\tt.Run(\"on same line\", func(t *testing.T) {\n\t\tloc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 5, 7}, testDataFullPath)\n\t\tloc1.Text = \"hint: change this to an int\"\n\t\tloc1.Type = LocHelp\n\t\tloc2 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 5, 2}, testDataFullPath)\n\t\tloc2.Text = \"wrong type here\"\n\n\t\terr := New(ErrParams{\n\t\t\tCode:      1,\n\t\t\tTitle:     \"simple test error\",\n\t\t\tSummary:   \"There has been a simple error in your code\",\n\t\t\tDetail:    \"For more information please visit our great help documentation\",\n\t\t\tLocations: SrcLocations{loc1, loc2},\n\t\t}, false)\n\n\t\ttestError(t, err)\n\t})\n}\n\nfunc Test_renderSrc_MutlilineError(t *testing.T) {\n\tloc1 := FromCueTokenPos(&errorLoc{path.Join(testDataFullPath, \"test.cue\"), 4, 7}, testDataFullPath)\n\tloc1.End = Pos{7, 1}\n\tloc1.Text = \"this is an error which spans multiple lines\\nlike this so we can test alignment\"\n\n\terr := New(ErrParams{\n\t\tCode:      1,\n\t\tTitle:     \"simple test error\",\n\t\tSummary:   \"There has been a simple error in your code\",\n\t\tDetail:    \"For more information please visit our great help documentation\",\n\t\tLocations: SrcLocations{loc1},\n\t}, false)\n\n\ttestError(t, err)\n}\n\n/* helpers */\n\nfunc testError(t *testing.T, err *ErrInSrc) {\n\t// Reset for stdout with colours\n\tset = unicodeSet\n\tColoursInErrors(true)\n\tfmt.Println(err.Error())\n\n\t// Now golden files without colours\n\tgoldenFile := strings.Replace(t.Name(), \"/\", \"__\", -1)\n\tColoursInErrors(false)\n\tgolden.TestAgainst(t, goldenFile+\"_unicode.golden\", err.Error())\n\tset = asciiSet\n\tgolden.TestAgainst(t, goldenFile+\"_ascii.golden\", err.Error())\n}\n\ntype errorLoc struct {\n\tfilename string\n\tline     int\n\tcolumn   int\n}\n\nfunc (e *errorLoc) Filename() string {\n\treturn e.filename\n}\n\nfunc (e *errorLoc) Line() int {\n\treturn e.line\n}\n\nfunc (e *errorLoc) Column() int {\n\treturn e.column\n}\n"
  },
  {
    "path": "pkg/errinsrc/stack.go",
    "content": "package errinsrc\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// StackFrame represents a single frame in a Stack trace\ntype StackFrame struct {\n\tProgramCounter uintptr `json:\"pc\"`\n\tFile           string  `json:\"file\"`\n\tPackage        string  `json:\"pkg\"`\n\tFunction       string  `json:\"fun\"`\n\tLine           int     `json:\"line\"`\n}\n\nconst maxFramesOnPrettyPrint = 5\n\nfunc GetStack() []*StackFrame {\n\tret := make([]uintptr, 100)\n\n\tindex := runtime.Callers(1, ret)\n\tif index == 0 {\n\t\treturn nil\n\t}\n\n\treturn convertFrames(ret[:index])\n}\n\n// bottomStackTraceFrom returns the deepest stack trace from the given error\nfunc bottomStackTraceFrom(err error) (rtn []*StackFrame) {\n\tcount := 0\n\n\tfor err != nil && count < 100 {\n\t\tcount++\n\n\t\t// Look for an error which provides a stack trace\n\t\tif e, ok := err.(*ErrInSrc); ok {\n\t\t\trtn = e.Stack\n\t\t} else if e, ok := err.(interface{ StackTrace() errors.StackTrace }); ok {\n\t\t\tframes := e.StackTrace()\n\t\t\tpcs := make([]uintptr, len(frames))\n\t\t\tfor i, pc := range frames {\n\t\t\t\tpcs[i] = uintptr(pc)\n\t\t\t}\n\t\t\trtn = convertFrames(pcs)\n\t\t}\n\n\t\t// Recurse\n\t\tswitch typed := err.(type) {\n\t\tcase interface{ Unwrap() error }:\n\t\t\terr = typed.Unwrap()\n\n\t\tcase interface{ Unwrap() []error }:\n\t\t\terrs := typed.Unwrap()\n\t\t\tif len(errs) > 0 {\n\t\t\t\terr = errs[0]\n\t\t\t} else {\n\t\t\t\terr = nil\n\t\t\t}\n\n\t\tcase interface{ Cause() error }:\n\t\t\terr = typed.Cause()\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc convertFrames(pcs []uintptr) []*StackFrame {\n\tcf := runtime.CallersFrames(pcs)\n\tframe, more := cf.Next()\n\n\t// Skip over the \"errinsrc\" or \"errlist\" package files or any subpackages\n\t// which are the top frames (as these would only be related to the creation of the error)\n\tfor strings.Contains(frame.File, \"errinsrc\") ||\n\t\tstrings.Contains(frame.File, \"errlist\") ||\n\t\tstrings.Contains(frame.File, \"perr\") ||\n\t\tstrings.Contains(frame.File, \"eerror\") ||\n\t\tstrings.HasSuffix(frame.File, \"errs.go\") {\n\t\tif !more {\n\t\t\treturn nil\n\t\t}\n\n\t\tframe, more = cf.Next()\n\t}\n\n\tvar frames []*StackFrame\n\tfor {\n\t\t// Skip the frame if it's Go Runtime or internal testing code related code\n\t\tif !strings.HasPrefix(frame.Function, \"runtime.\") && !strings.HasPrefix(frame.Function, \"testing.\") {\n\t\t\t// Separate the package name and function name\n\t\t\tpkgAndFunc := frame.Function\n\t\t\tif idx := strings.LastIndex(pkgAndFunc, \"/\"); idx >= 0 {\n\t\t\t\tpkgAndFunc = pkgAndFunc[idx+1:]\n\t\t\t}\n\t\t\tpkgName, funcName, _ := strings.Cut(pkgAndFunc, \".\")\n\n\t\t\t// Record the frame\n\t\t\tframes = append(frames, &StackFrame{\n\t\t\t\tProgramCounter: frame.PC,\n\t\t\t\tPackage:        pkgName,\n\t\t\t\tFunction:       funcName,\n\t\t\t\tFile:           frame.File,\n\t\t\t\tLine:           frame.Line,\n\t\t\t})\n\t\t}\n\n\t\tif !more {\n\t\t\treturn frames\n\t\t}\n\n\t\tframe, more = cf.Next()\n\t}\n}\n\nfunc prettyPrintStack(stack []*StackFrame, b *strings.Builder) string {\n\tb.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\"%c%c%c\", set.LeftTop, set.HorizontalBar, set.LeftBracket)).String())\n\tb.WriteString(aurora.Cyan(\"Stack Trace\").String())\n\tb.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\"%c\", set.RightBracket)).String())\n\n\tlongestFunc := 0\n\tfor i, frame := range stack {\n\t\tname := fmt.Sprintf(\"%s.%s\", frame.Package, frame.Function)\n\t\tif length := len(name); length > longestFunc {\n\t\t\tlongestFunc = length\n\t\t}\n\n\t\tif i >= maxFramesOnPrettyPrint {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor i, frame := range stack {\n\t\tvertical := set.LeftCross\n\t\tif i == len(stack)-1 {\n\t\t\tvertical = set.LeftBottom\n\t\t}\n\n\t\tb.WriteString(\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"\\n%s %s.%s%s %s:%d\",\n\t\t\t\taurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\"%c%c%c\", vertical, set.HorizontalBar, set.RightArrow)),\n\t\t\t\taurora.Gray(18, frame.Package),\n\t\t\t\taurora.Magenta(frame.Function),\n\t\t\t\tstrings.Repeat(\" \", longestFunc-len(frame.Function)-len(frame.Package)-1),\n\t\t\t\tframe.File,\n\t\t\t\tframe.Line,\n\t\t\t),\n\t\t)\n\n\t\tif i >= maxFramesOnPrettyPrint && i != len(stack)-1 {\n\t\t\tb.WriteString(\"\\n\")\n\t\t\tb.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\"%c%c%c\", set.LeftBottom, set.HorizontalBar, set.LeftBracket)).String())\n\t\t\tb.WriteString(aurora.Yellow(\"... remaining frames omitted ...\").Italic().String())\n\t\t\tb.WriteString(aurora.Gray(grayLevelOnLineNumbers, fmt.Sprintf(\"%c\", set.RightBracket)).String())\n\n\t\t\tbreak\n\t\t}\n\t}\n\tb.WriteString(\"\\n\\n\")\n\n\treturn b.String()\n}\n"
  },
  {
    "path": "pkg/errinsrc/stack_dev.go",
    "content": "//go:build dev_build\n\npackage errinsrc\n\n// IncludeStackByDefault is whether to include the stack by default on all errors.\n// This is exported to allow Encore's CI platform to set this to true for CI/CD builds.\nvar IncludeStackByDefault = true\n"
  },
  {
    "path": "pkg/errinsrc/stack_release.go",
    "content": "//go:build !dev_build\n\npackage errinsrc\n\n// IncludeStackByDefault is whether to include the stack by default on all errors.\n// This is exported to allow Encore's CI platform to set this to true for CI/CD builds.\nvar IncludeStackByDefault = false\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_following_lines_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n   ,-[ /test.cue:5:7 ]\n   |\n 3 | // This is a sample file\n 4 | blah: {\n 5 |     foo: bool\n   :          -v--\n   :           `- this is weird\n 6 |     bar: int | *3\n   :                v-\n   :                `- so is this!\n 7 | }\n 8 | \n---'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_following_lines_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n   ╭─[ /test.cue:5:7 ]\n   │\n 3 │ // This is a sample file\n 4 │ blah: {\n 5 │     foo: bool\n   ⋮          ─┬──\n   ⋮           ╰─ this is weird\n 6 │     bar: int | *3\n   ⋮                ┬─\n   ⋮                ╰─ so is this!\n 7 │ }\n 8 │ \n───╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_same_line_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n   ,-[ /test.cue:5:2 ]\n   |\n 3 | // This is a sample file\n 4 | blah: {\n 5 |     foo: bool\n   :     ----v----\n   :         `- wrong type here\n 6 |     bar: int | *3\n 7 | }\n---'\n\n   ,-[ /test.cue:5:7 ]\n   |\n 3 | // This is a sample file\n 4 | blah: {\n 5 |     foo: bool\n   :          -v--\n   :           `- hint: change this to an int\n 6 |     bar: int | *3\n 7 | }\n---'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__on_same_line_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n   ╭─[ /test.cue:5:2 ]\n   │\n 3 │ // This is a sample file\n 4 │ blah: {\n 5 │     foo: bool\n   ⋮     ────┬────\n   ⋮         ╰─ wrong type here\n 6 │     bar: int | *3\n 7 │ }\n───╯\n\n   ╭─[ /test.cue:5:7 ]\n   │\n 3 │ // This is a sample file\n 4 │ blah: {\n 5 │     foo: bool\n   ⋮          ─┬──\n   ⋮           ╰─ hint: change this to an int\n 6 │     bar: int | *3\n 7 │ }\n───╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__spaced_apart_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n    ,-[ /test.cue:5:7 ]\n    |\n  3 | // This is a sample file\n  4 | blah: {\n  5 |     foo: bool\n    :          -v--\n    :           `- defined as a boolean here\n    * \n    * \n 11 | \n 12 | // If foo then bar is 12\n 13 | if blah.foo {\n    :         -v-\n    :          `- referenced here\n 14 |     blah: bar: 12\n 15 | }\n----'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MultipleSeperateInSameFile__spaced_apart_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n    ╭─[ /test.cue:5:7 ]\n    │\n  3 │ // This is a sample file\n  4 │ blah: {\n  5 │     foo: bool\n    ⋮          ─┬──\n    ⋮           ╰─ defined as a boolean here\n    · \n    · \n 11 │ \n 12 │ // If foo then bar is 12\n 13 │ if blah.foo {\n    ⋮         ─┬─\n    ⋮          ╰─ referenced here\n 14 │     blah: bar: 12\n 15 │ }\n────╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MutlilineError_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n    ,-[ /test.cue:4:7 ]\n    |\n  2 |     \n  3 |     // This is a sample file\n  4 |     blah: {\n    :           ^\n    : ,---------'\n  5 | |       foo: bool\n  6 | |       bar: int | *3\n  7 | |-> }\n    : `- this is an error which spans multiple lines\n    :    like this so we can test alignment\n  8 |     \n  9 |     // Let's set foo to true\n----'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_MutlilineError_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n    ╭─[ /test.cue:4:7 ]\n    │\n  2 │     \n  3 │     // This is a sample file\n  4 │     blah: {\n    ⋮           ▲\n    ⋮ ╭─────────╯\n  5 │ │       foo: bool\n  6 │ │       bar: int | *3\n  7 │ ├─▶ }\n    ⋮ ╰─ this is an error which spans multiple lines\n    ⋮    like this so we can test alignment\n  8 │     \n  9 │     // Let's set foo to true\n────╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__error_no_text_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n   ,-[ /test.cue:5:7 ]\n   |\n 3 | // This is a sample file\n 4 | blah: {\n 5 |     foo: bool\n   :          ----\n 6 |     bar: int | *3\n 7 | }\n---'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__error_no_text_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n   ╭─[ /test.cue:5:7 ]\n   │\n 3 │ // This is a sample file\n 4 │ blah: {\n 5 │     foo: bool\n   ⋮          ────\n 6 │     bar: int | *3\n 7 │ }\n───╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__multiline_message_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n   ,-[ /test.cue:1:9 ]\n   |\n 1 | package test_cue\n   :         ---v----\n   :            `- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum porttitor elementum.\n   :               Duis eget cursus arcu, ut interdum lorem. Suspendisse vel diam et eros cursus vestibulum at et\n   :               lacus. Nullam felis tellus, cursus nec arcu nec, laoreet maximus ante. Cras pellentesque est\n   :               est, nec laoreet magna accumsan vel. Mauris commodo dui purus, non ullamcorper nisl commodo\n   :               auctor. Vivamus finibus mi ut risus tempor pellentesque. Nulla facilisi. Nullam rhoncus neque\n   :               porta erat molestie, at malesuada est aliquam. Etiam convallis lorem eget euismod eleifend.\n   :               Phasellus sit amet diam in orci molestie pulvinar. Vestibulum non auctor dolor, vel imperdiet\n   :               risus. Nam egestas et purus id sodales. Aliquam in metus varius, porta mi nec, ornare mauris.\n   :               \n   :               Quisque eu nisi vel nulla sodales pretium sed a dolor. Morbi convallis ornare ligula, ut\n   :               aliquam velit auctor id. In in neque turpis. Pellentesque habitant morbi tristique senectus et\n   :               netus et malesuada fames ac turpis egestas. Cras et arcu id magna accumsan semper. Vestibulum\n   :               ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc semper\n   :               rhoncus tincidunt. Vestibulum lacus erat, molestie ut ultrices convallis, placerat at lacus.\n   :               Integer tempus tempus sodales. Integer sit amet est quam. Praesent vitae condimentum mi.\n 2 | \n 3 | // This is a sample file\n---'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__multiline_message_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n   ╭─[ /test.cue:1:9 ]\n   │\n 1 │ package test_cue\n   ⋮         ───┬────\n   ⋮            ╰─ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean interdum porttitor elementum.\n   ⋮               Duis eget cursus arcu, ut interdum lorem. Suspendisse vel diam et eros cursus vestibulum at et\n   ⋮               lacus. Nullam felis tellus, cursus nec arcu nec, laoreet maximus ante. Cras pellentesque est\n   ⋮               est, nec laoreet magna accumsan vel. Mauris commodo dui purus, non ullamcorper nisl commodo\n   ⋮               auctor. Vivamus finibus mi ut risus tempor pellentesque. Nulla facilisi. Nullam rhoncus neque\n   ⋮               porta erat molestie, at malesuada est aliquam. Etiam convallis lorem eget euismod eleifend.\n   ⋮               Phasellus sit amet diam in orci molestie pulvinar. Vestibulum non auctor dolor, vel imperdiet\n   ⋮               risus. Nam egestas et purus id sodales. Aliquam in metus varius, porta mi nec, ornare mauris.\n   ⋮               \n   ⋮               Quisque eu nisi vel nulla sodales pretium sed a dolor. Morbi convallis ornare ligula, ut\n   ⋮               aliquam velit auctor id. In in neque turpis. Pellentesque habitant morbi tristique senectus et\n   ⋮               netus et malesuada fames ac turpis egestas. Cras et arcu id magna accumsan semper. Vestibulum\n   ⋮               ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc semper\n   ⋮               rhoncus tincidunt. Vestibulum lacus erat, molestie ut ultrices convallis, placerat at lacus.\n   ⋮               Integer tempus tempus sodales. Integer sit amet est quam. Praesent vitae condimentum mi.\n 2 │ \n 3 │ // This is a sample file\n───╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_error_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n   ,-[ /test.cue:5:2 ]\n   |\n 3 | // This is a sample file\n 4 | blah: {\n 5 |     foo: bool\n   :     ----v----\n   :         `- What is a foo?\n 6 |     bar: int | *3\n 7 | }\n---'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_error_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n   ╭─[ /test.cue:5:2 ]\n   │\n 3 │ // This is a sample file\n 4 │ blah: {\n 5 │     foo: bool\n   ⋮     ────┬────\n   ⋮         ╰─ What is a foo?\n 6 │     bar: int | *3\n 7 │ }\n───╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_help_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n    ,-[ /test.cue:13:4 ]\n    |\n 11 | \n 12 | // If foo then bar is 12\n 13 | if blah.foo {\n    :    ---v----\n    :       `- help: try a switch statement here\n 14 |     blah: bar: 12\n 15 | }\n----'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_help_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n    ╭─[ /test.cue:13:4 ]\n    │\n 11 │ \n 12 │ // If foo then bar is 12\n 13 │ if blah.foo {\n    ⋮    ───┬────\n    ⋮       ╰─ help: try a switch statement here\n 14 │     blah: bar: 12\n 15 │ }\n────╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_warning_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n    ,-[ /test.cue:10:7 ]\n    |\n  8 | \n  9 | // Let's set foo to true\n 10 | blah: foo: true\n    :       -v-\n    :        `- You sure about this?\n 11 | \n 12 | // If foo then bar is 12\n----'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__simple_warning_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n    ╭─[ /test.cue:10:7 ]\n    │\n  8 │ \n  9 │ // Let's set foo to true\n 10 │ blah: foo: true\n    ⋮       ─┬─\n    ⋮        ╰─ You sure about this?\n 11 │ \n 12 │ // If foo then bar is 12\n────╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__single_character_error_ascii.golden",
    "content": "\n-- simple test error ----------------------------------------------------------------------[E0001]--\n\nThere has been a simple error in your code\n\n   ,-[ /test.cue:6:11 ]\n   |\n 4 | blah: {\n 5 |     foo: bool\n 6 |     bar: int | *3\n   :              ^\n   :              `- This looks dodgy\n 7 | }\n 8 | \n---'\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/Test_renderSrc_Simple__single_character_error_unicode.golden",
    "content": "\n── simple test error ──────────────────────────────────────────────────────────────────────[E0001]──\n\nThere has been a simple error in your code\n\n   ╭─[ /test.cue:6:11 ]\n   │\n 4 │ blah: {\n 5 │     foo: bool\n 6 │     bar: int | *3\n   ⋮              ▲\n   ⋮              ╰─ This looks dodgy\n 7 │ }\n 8 │ \n───╯\n\nFor more information please visit our great help documentation\n\n"
  },
  {
    "path": "pkg/errinsrc/testdata/test.cue",
    "content": "package test_cue\n\n// This is a sample file\nblah: {\n\tfoo: bool\n\tbar: int | *3\n}\n\n// Let's set foo to true\nblah: foo: true\n\n// If foo then bar is 12\nif blah.foo {\n\tblah: bar: 12\n}\n"
  },
  {
    "path": "pkg/errinsrc/testdata/test.go",
    "content": "package testdata\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n\tBlah struct {\n\t\tFoo config.Bool `json:\"foo\"`\n\t\tBar config.Int  `json:\"bar\"`\n\t} `json:\"blah\"`\n}\n\nvar cfg = config.Load[Config]()\n\ntype WhatIsBarResponse struct {\n\tBar int\n}\n\n//encore:api public\nfunc WhatIsBar(ctx context.Context) (*WhatIsBarResponse, error) {\n\treturn &WhatIsBarResponse{\n\t\tBar: cfg.Blah.Bar(),\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/errinsrc/utils.go",
    "content": "package errinsrc\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t. \"encr.dev/pkg/errinsrc/internal\"\n)\n\ntype ErrorList interface {\n\tError() string\n\tErrorList() []*ErrInSrc\n}\n\n// Bailout is a panic value that can be used to\ntype Bailout struct {\n\tList ErrorList\n}\n\nfunc (b Bailout) Error() string {\n\treturn b.List.Error()\n}\n\nfunc (b Bailout) Unwrap() error {\n\treturn b.List\n}\n\nfunc (b Bailout) ErrorList() []*ErrInSrc {\n\treturn b.List.ErrorList()\n}\n\nfunc Panic(list ErrorList) {\n\tpanic(Bailout{list})\n}\n\n// ExtractFromPanic returns the first ErrInSrc or ErrorList found in the recovered\n// value.\n//\n// If no value is recovered, then nil is returned.\nfunc ExtractFromPanic(recovered any) error {\n\t// If it's already an ErrInSrc or list of them, just return that\n\tif unwrapped, ok := recovered.(error); ok {\n\t\t// Check the type of the error then unwrap as needed\n\t\tn := 0\n\t\tfor unwrapped != nil {\n\t\t\t// Limit recursion to 100 unwraps to prevent infinite loops.\n\t\t\tn++\n\t\t\tif n > 100 {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tswitch err := unwrapped.(type) {\n\t\t\tcase *ErrInSrc:\n\t\t\t\treturn err\n\t\t\tcase ErrorList:\n\t\t\t\treturn err\n\t\t\tcase Bailout:\n\t\t\t\treturn err.List\n\t\t\tcase interface{ Unwrap() error }:\n\t\t\t\tunwrapped = err.Unwrap()\n\t\t\tdefault:\n\t\t\t\t// If we get here, it's not an errinsrc or error list, so return nil\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc AddHintFromGo(err error, fileset *token.FileSet, node ast.Node, hint string) {\n\tswitch err := err.(type) {\n\tcase *ErrInSrc:\n\t\tif hintLoc, ok := FromGoASTNode(fileset, node).Get(); ok {\n\t\t\thintLoc.Type = LocHelp\n\t\t\thintLoc.Text = hint\n\t\t\terr.Params.Locations = append(err.Params.Locations, hintLoc)\n\t\t}\n\tcase ErrorList:\n\t\tfor _, err := range err.ErrorList() {\n\t\t\tAddHintFromGo(err, fileset, node, hint)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/errlist/errlist.go",
    "content": "package errlist\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/scanner\"\n\t\"go/token\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errinsrc\"\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// Verbose controls whether the error list prints all errors\n// or just the what MaxErrorsToPrint is set to\nvar Verbose = false\n\n// MaxErrorsToPrint is the maximum number of errors to print\n// if Verbose is false\nvar MaxErrorsToPrint = 1\n\ntype List struct {\n\tList errinsrc.List `json:\"list,omitempty\"`\n\tfset *token.FileSet\n}\n\nvar _ errinsrc.ErrorList = (*List)(nil)\n\nfunc New(fset *token.FileSet) *List {\n\treturn &List{fset: fset}\n}\n\n// Convert attempts to convert known error types into an error list\n// if it can't it returns nil\nfunc Convert(err error) *List {\n\tswitch err := err.(type) {\n\tcase *List:\n\t\treturn err\n\tcase *errinsrc.ErrInSrc:\n\t\tl := New(nil)\n\t\tl.List = []*errinsrc.ErrInSrc{err}\n\t\treturn l\n\tcase errinsrc.ErrorList:\n\t\tl := New(nil)\n\t\tl.List = err.ErrorList()\n\t\treturn l\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// Report is a function that allows you to report an error into\n// this list, without having to check for nil.\n//\n// This function only supports error of types:\n//   - *List\n//   - *errinsrc.ErrInSrc\n//   - *scanner.Error\n//\n// If too many errors have been reported it panics\n// with a Bailout value to abort processing.\n// Use HandleBailout to conveniently handle this.\nfunc (l *List) Report(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\tvar errToAdd *errinsrc.ErrInSrc\n\tswitch err := (err).(type) {\n\tcase *errinsrc.ErrInSrc:\n\t\t// the base type we expect\n\t\terrToAdd = err\n\n\tcase *scanner.Error:\n\t\t// errors directly from the Go parser\n\t\terrToAdd = srcerrors.GenericGoParserError(err)\n\n\tcase scanner.ErrorList:\n\t\tfor _, e := range err {\n\t\t\tl.Report(e)\n\t\t}\n\t\treturn\n\tcase *List:\n\t\t// If it's a different list, then merge it in\n\t\tif err != l {\n\t\t\tfor _, e := range err.ErrorList() {\n\t\t\t\tl.Report(e)\n\t\t\t}\n\t\t}\n\t\treturn\n\tcase errinsrc.ErrorList:\n\t\t// either another errlist or a list from `srcerrors`\n\t\tfor _, e := range err.ErrorList() {\n\t\t\tl.Report(e)\n\t\t}\n\t\treturn\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported type %T being reported to errlist.List\", err))\n\t}\n\n\t// Skip adding this error if it's on the same line as another error\n\t// we've already reported, since it's probably a spurious error caused\n\t// by the first one.\n\tfor _, e := range l.List {\n\t\tif errToAdd.OnSameLine(e) {\n\t\t\treturn\n\t\t}\n\t}\n\n\tl.List = append(l.List, errToAdd)\n\tif len(l.List) > 10 {\n\t\tl.Abort()\n\t}\n}\n\n// Add adds an error to the list.\n//\n// If too many errors have been added it panics\n// with a Bailout value to abort processing.\n// Use HandleBailout to conveniently handle this.\n//\n// Deprecated: use Report instead\nfunc (l *List) Add(pos token.Pos, msg string) {\n\tpp := l.fset.Position(pos)\n\tl.Report(&scanner.Error{\n\t\tPos: pp,\n\t\tMsg: msg,\n\t})\n}\n\n// Addf is equivalent to Add(pos, fmt.Sprintf(format, args...))\n//\n// Deprecated: use Report instead\nfunc (l *List) Addf(pos token.Pos, format string, args ...interface{}) {\n\tl.Add(pos, fmt.Sprintf(format, args...))\n}\n\n// AddRaw adds a raw *scanner.Error to the list.\n//\n// If too many errors have been added it panics\n// with a Bailout value to abort processing.\n// Use HandleBailout to conveniently handle this.\n//\n// Deprecated: use Report instead\nfunc (l *List) AddRaw(err *scanner.Error) {\n\tl.Report(err)\n}\n\n// Merge merges another list into this one.\n// The token.FileSet in use must be the same one as this one,\n// or else it panics.\n//\n// Deprecated: use Report instead\nfunc (l *List) Merge(other *List) {\n\tif other.fset != l.fset {\n\t\tpanic(\"errlist: cannot merge lists with different *token.FileSets\")\n\t}\n\tl.List = append(l.List, other.List...)\n}\n\n// Err returns an error equivalent to this error list.\n// If the list is empty, Err returns nil.\nfunc (l *List) Err() error {\n\tif len(l.List) == 0 {\n\t\treturn nil\n\t}\n\treturn l\n}\n\n// Error implements the error interface.\nfunc (l *List) Error() string {\n\tvar b strings.Builder\n\n\tif Verbose {\n\t\tfor _, err := range l.List {\n\t\t\tb.WriteString(err.Error())\n\t\t}\n\t} else {\n\t\tif len(l.List) == 0 {\n\t\t\tb.WriteString(\"no errors\")\n\t\t} else {\n\t\t\tfor i, err := range l.List {\n\t\t\t\tb.WriteString(err.Error())\n\t\t\t\tif i >= MaxErrorsToPrint-1 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(l.List) > MaxErrorsToPrint {\n\t\t\t\tb.WriteString(fmt.Sprintf(\"And %d more errors\", len(l.List)-MaxErrorsToPrint))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn b.String()\n}\n\nfunc (l *List) ErrorList() []*errinsrc.ErrInSrc {\n\treturn l.List\n}\n\n// MakeRelative rewrites the errors by making filenames within the\n// app root relative to the relwd (which must be a relative path\n// within the root).\nfunc (l *List) MakeRelative(root, relwd string) {\n\twdroot := filepath.Join(root, relwd)\n\tfor _, e := range l.List {\n\t\tfor _, loc := range e.Params.Locations {\n\t\t\tif loc.File != nil {\n\t\t\t\tfn := loc.File.RelPath\n\t\t\t\tif strings.HasPrefix(fn, root) {\n\t\t\t\t\tif rel, err := filepath.Rel(wdroot, fn); err == nil {\n\t\t\t\t\t\tloc.File.RelPath = rel\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// HandleBailout handles bailouts raised by (*List).Add and family\n// when too many errors have been found.\nfunc (l *List) HandleBailout(err *error) {\n\tif e := recover(); e != nil {\n\t\tif b, ok := e.(errinsrc.Bailout); ok {\n\t\t\t*err = b.List\n\t\t} else {\n\t\t\tpanic(e)\n\t\t}\n\t}\n}\n\nfunc (l *List) Len() int {\n\treturn len(l.List)\n}\n\n// Abort aborts early if there is an error in the list.\nfunc (l *List) Abort() {\n\terrinsrc.Panic(l)\n}\n\n// SendToStream sends a GRPC command with this\n// full errlist\n//\n// If l is nil or empty, it sends a nil command\n// allowing the client to know that there are no\n// longer an error present\nfunc (l *List) SendToStream(stream interface {\n\tSend(*daemonpb.CommandMessage) error\n}) error {\n\tvar bytes []byte\n\tif l != nil && len(l.List) > 0 {\n\t\tvar err error\n\t\tbytes, err = json.Marshal(l)\n\t\tif err != nil {\n\t\t\tpanic(\"unable to marshal error list\")\n\t\t}\n\t}\n\treturn stream.Send(\n\t\t&daemonpb.CommandMessage{\n\t\t\tMsg: &daemonpb.CommandMessage_Errors{\n\t\t\t\tErrors: &daemonpb.CommandDisplayErrors{\n\t\t\t\t\tErrinsrc: bytes,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n}\n\n// Print is a utility function that prints a list of errors to w,\n// one error per line, if the err parameter is an errorList. Otherwise\n// it prints the err string.\nfunc Print(w io.Writer, err error) {\n\tif l, ok := err.(*List); ok {\n\t\tfor _, e := range l.List {\n\t\t\tfmt.Fprintf(w, \"%s\\n\", e)\n\t\t}\n\t} else if err != nil {\n\t\tfmt.Fprintf(w, \"%s\\n\", err)\n\t}\n}\n"
  },
  {
    "path": "pkg/errors/locations.go",
    "content": "package errors\n\nimport (\n\tgoAst \"go/ast\"\n\tgoToken \"go/token\"\n)\n\n// LocationType represents if the locaton is the source of the error, a warning or a helpful hint\ntype LocationType uint8\n\nconst (\n\tLocError LocationType = iota\n\tLocWarning\n\tLocHelp\n)\n\n// LocationKind tells us what language and position markers we're using to identify the source\ntype LocationKind uint8\n\nconst (\n\tLocFile LocationKind = iota\n\tLocGoNode\n\tLocGoPos\n\tLocGoPositions\n)\n\n// SrcLocation tells us where in the code base caused the error, warning or is a hint to the end\n// user.\ntype SrcLocation struct {\n\tKind            LocationKind\n\tLocType         LocationType\n\tText            string\n\tFilepath        string\n\tGoNode          goAst.Node\n\tGoStartPos      goToken.Pos\n\tGoEndPos        goToken.Pos\n\tGoStartPosition goToken.Position\n\tGoEndPosition   goToken.Position\n}\n\ntype LocationOption func(*SrcLocation)\n\n// AsError allows you to set a SrcLocation's error text\n//\n// Pass this option in when you give the error [Template] a src location\nfunc AsError(errorText string) func(loc *SrcLocation) {\n\treturn func(loc *SrcLocation) {\n\t\tloc.Text = errorText\n\t\tloc.LocType = LocError\n\t}\n}\n\n// AsWarning allows you to set a SrcLocation's text and mark it as a warning\n//\n// Pass this option in when you give the error [Template] a src location\nfunc AsWarning(warningText string) func(loc *SrcLocation) {\n\treturn func(loc *SrcLocation) {\n\t\tloc.Text = warningText\n\t\tloc.LocType = LocWarning\n\t}\n}\n\n// AsHelp allows you to set a SrcLocation's text and mark it as a helpful hint\n//\n// Pass this option in when you give the error [Template] a src location\nfunc AsHelp(helpText string) func(loc *SrcLocation) {\n\treturn func(loc *SrcLocation) {\n\t\tloc.LocType = LocHelp\n\t\tloc.Text = helpText\n\t}\n}\n"
  },
  {
    "path": "pkg/errors/range.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n)\n\nvar (\n\tnextRangeStart = 1000\n)\n\n// Range creates a new error code range for a given module.\nfunc Range(\n\tmodule string,\n\tdefaultDetails string,\n\toptions ...RangeOption,\n) *TemplateRange {\n\tif nextRangeStart > 9999 {\n\t\t// If we hit this, we need to increase the number of digits in the error code renderer\n\t\tpanic(\"too many error code ranges\")\n\t}\n\n\t// Configure the range\n\tcfg := &rangeConfig{\n\t\trangeSize: 100,\n\t}\n\tfor _, c := range options {\n\t\tc(cfg)\n\t}\n\n\t// Create the Range\n\trangeStart := nextRangeStart\n\tnextRangeStart += cfg.rangeSize\n\n\treturn &TemplateRange{\n\t\tmodule:         module,\n\t\tdefaultDetails: defaultDetails,\n\t\tnextErrorCode:  rangeStart,\n\t\tcodeRangeEnd:   nextRangeStart,\n\t}\n}\n\ntype rangeConfig struct {\n\trangeSize int\n}\n\ntype RangeOption func(*rangeConfig)\n\n// WithRangeSize sets the size of the range. The default is 100.\nfunc WithRangeSize(size int) RangeOption {\n\treturn func(cfg *rangeConfig) {\n\t\tcfg.rangeSize = size\n\t}\n}\n\n// TemplateRange is a helper for creating a range of error codes for a given module\n// and generating templates for those errors.\ntype TemplateRange struct {\n\tmodule         string\n\tdefaultDetails string\n\tnextErrorCode  int\n\tcodeRangeEnd   int // Exclusive\n}\n\n// New creates a new template for an error in this range.\nfunc (r *TemplateRange) New(title, summary string, options ...TemplateOption) Template {\n\treturn r.Newf(title, summary, options...)()\n}\n\n// Newf creates a function to return a template for an error in this range where the summary is a format string.\nfunc (r *TemplateRange) Newf(title, summaryFmt string, options ...TemplateOption) func(summaryArgs ...any) Template {\n\tif r.nextErrorCode >= r.codeRangeEnd {\n\t\tpanic(\"too many errors in range\")\n\t}\n\terrorCode := r.nextErrorCode\n\tr.nextErrorCode++\n\n\treturn func(summaryArgs ...any) Template {\n\t\ttmp := Template{\n\t\t\tCode:    errorCode,\n\t\t\tTitle:   title,\n\t\t\tSummary: fmt.Sprintf(summaryFmt, summaryArgs...),\n\t\t\tDetail:  r.defaultDetails,\n\t\t}\n\n\t\tfor _, o := range options {\n\t\t\to(&tmp)\n\t\t}\n\n\t\treturn tmp\n\t}\n}\n"
  },
  {
    "path": "pkg/errors/template.go",
    "content": "package errors\n\nimport (\n\tgoAst \"go/ast\"\n\tgoToken \"go/token\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n// Template represents a template for a new error.\n//\n// It itself is not an error, but can be used to initialize a new [errorinsrc.ErrInSrc].\ntype Template struct {\n\tCode               int\n\tTitle              string\n\tSummary            string\n\tDetail             string\n\tCause              error\n\tLocations          []SrcLocation\n\tAlwaysIncludeStack bool\n}\n\n// TemplateOption can be passed into the [Range] when creating a new Template\ntype TemplateOption func(*Template)\n\n// AlwaysIncludeStack will setup a Template so it always includes a stack trace\n// even in a production build of Encore.\nfunc AlwaysIncludeStack() TemplateOption {\n\treturn func(template *Template) {\n\t\ttemplate.AlwaysIncludeStack = true\n\t}\n}\n\n// WithDetails will setup a template so it uses a different details to the\n// range default\nfunc WithDetails(details string) TemplateOption {\n\treturn func(template *Template) {\n\t\ttemplate.Detail = details\n\t}\n}\n\n// PrependDetails will setup a template so it prepends the given details to the\n// range default\nfunc PrependDetails(details string) TemplateOption {\n\treturn func(template *Template) {\n\t\tif template.Detail != \"\" {\n\t\t\tdetails = details + \"\\n\\n\" + template.Detail\n\t\t}\n\t\ttemplate.Detail = details\n\t}\n}\n\n// MarkAsInternalError will setup a template so it is reported as an internal error.\n//\n// This means that the error will be reported to the user as an internal error\n// with a link to the Encore issue tracker and will include a stack trace.\nfunc MarkAsInternalError() TemplateOption {\n\treturn func(template *Template) {\n\t\ttemplate.AlwaysIncludeStack = true\n\t\ttemplate.Title = \"Internal Error: \" + template.Title\n\t\ttemplate.Detail = \"This is a bug in Encore and should not have occurred. Please report this issue to the \" +\n\t\t\t\"Encore team either on Github at https://github.com/encoredev/encore/issues/new and include this error.\"\n\t}\n}\n\n// WithDetails will replace the details of the template with the given details\nfunc (t Template) WithDetails(details string) Template {\n\tt.Detail = details\n\treturn t\n}\n\n// Wrapping wraps the given error with the template.\n//\n// It will append the given error to the summary of the template\n// as well as setting the cause of the template to the given error.\nfunc (t Template) Wrapping(err error) Template {\n\tif err == nil {\n\t\treturn t\n\t}\n\tt.Summary += \"\\n\\n\" + err.Error()\n\tt.Summary = strings.TrimSpace(t.Summary)\n\n\tt.Cause = err\n\treturn t\n}\n\n// atLocation is a helper method for the various with methods\nfunc (t Template) atLocation(location SrcLocation, options []LocationOption) Template {\n\tfor _, o := range options {\n\t\to(&location)\n\t}\n\tt.Locations = append([]SrcLocation{location}, t.Locations...)\n\treturn t\n}\n\n// InFile adds the given file as a src of the error location\n//\n// Note: It is preferable to use one of the other location functions\n// as they will render the source around the error, not just the file name\nfunc (t Template) InFile(filepath string, options ...LocationOption) Template {\n\tif filepath == \"\" {\n\t\treturn t\n\t}\n\n\treturn t.atLocation(SrcLocation{Kind: LocFile, Filepath: filepath}, options)\n}\n\n// AtGoNode adds the given Go node to the template. If the node is nil, nothing happens.\n//\n// You can use the [LocationOption]s to add additional information to the location.\n//\n// Example:\n//\n//\terrMyErrorTemplate.AtGoNode(node, errtmp.AsHelp(\"this is where it was defined before\"))\nfunc (t Template) AtGoNode(node goAst.Node, options ...LocationOption) Template {\n\tif node == nil {\n\t\treturn t\n\t}\n\n\t// In some cases the node may be a \"typed nil\" which isn't caught by the check above.\n\t// Handle that separately, as otherwise we get unexpected panics later when\n\t// trying to call .Pos().\n\tif v := reflect.ValueOf(node); v.Kind() == reflect.Pointer && v.IsNil() {\n\t\treturn t\n\t}\n\n\treturn t.atLocation(SrcLocation{Kind: LocGoNode, GoNode: node}, options)\n}\n\n// AtGoPos adds the given start and end [token.Pos] to the template. If both positions are token.NoPos, nothing happens.\n// If one of the two positions are token.NoPos, the other position will be used.\n//\n// It is valid to use the same value for start and end positions, in which case the error will estimate which Node\n// you are referencing.\n//\n// Example:\n//\n//\terrMyErrorTemplate.AtGoPos(start, token.NoPos, errtmp.AsHelp(\"this is where it was defined before\"))\nfunc (t Template) AtGoPos(start, end goToken.Pos, options ...LocationOption) Template {\n\tswitch {\n\tcase start == goToken.NoPos && end == goToken.NoPos:\n\t\treturn t\n\tcase start == goToken.NoPos && end != goToken.NoPos:\n\t\tstart = end\n\tcase end == goToken.NoPos && start != goToken.NoPos:\n\t\tend = start\n\t}\n\n\treturn t.atLocation(SrcLocation{Kind: LocGoPos, GoStartPos: start, GoEndPos: end}, options)\n}\n\n// AtGoPosition adds the given Go positions to the template.\n//\n// It is valid to use the same value for start and end positions, in which case the error will estimate which Node\n// you are referencing.\n//\n// Example:\n//\n//\terrMyErrorTemplate.AtGoPosition(start, end, errtmp.AsHelp(\"this is where it was defined before\"))\nfunc (t Template) AtGoPosition(start, end goToken.Position, options ...LocationOption) Template {\n\treturn t.atLocation(SrcLocation{Kind: LocGoPositions, GoStartPosition: start, GoEndPosition: end}, options)\n}\n"
  },
  {
    "path": "pkg/errors/utils.go",
    "content": "package errors\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/option\"\n)\n\n// AtOptionalNode returns an error at the given node if it is present.\n// Otherwise, it returns the error unchanged.\nfunc AtOptionalNode[T ast.Node](err Template, opt option.Option[T]) Template {\n\tif node, ok := opt.Get(); ok {\n\t\treturn err.AtGoNode(node)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "pkg/fns/fns.go",
    "content": "package fns\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"io\"\n\t\"maps\"\n)\n\n// Map applies fn on all elements in src, producing a new slice\n// with the results, in order.\nfunc Map[A, B any](src []A, fn func(A) B) []B {\n\tdst := make([]B, len(src))\n\tfor i, v := range src {\n\t\tdst[i] = fn(v)\n\t}\n\treturn dst\n}\n\nfunc Max[A any, B cmp.Ordered](src []A, fn func(A) B) B {\n\tvar m B\n\tfor _, v := range src {\n\t\tif val := fn(v); m < val {\n\t\t\tm = val\n\t\t}\n\t}\n\treturn m\n}\n\n// MapAndFilter applies fn on all elements in src, producing a new slice\n// with the results, in order. If fn returns false, the element is\n// not included in the result.\nfunc MapAndFilter[A, B any](src []A, fn func(A) (B, bool)) []B {\n\tvar dst []B\n\tfor _, v := range src {\n\t\tif r, ok := fn(v); ok {\n\t\t\tdst = append(dst, r)\n\t\t}\n\t}\n\treturn dst\n\n}\n\n// MapErr applies fn on all elements in src, producing a new slice\n// with the results, in order. If fn returns an error, MapErr\n// returns that error and the resulting slice is nil.\nfunc MapErr[A, B any](src []A, fn func(A) (B, error)) ([]B, error) {\n\tdst := make([]B, len(src))\n\tfor i, v := range src {\n\t\tvar err error\n\t\tdst[i], err = fn(v)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn dst, nil\n\n}\n\n// TransformMapKeys creates a new map with the same values as m, but with\n// the keys transformed by fn.\nfunc TransformMapKeys[K1, K2 comparable, V any](m map[K1]V, fn func(K1) K2) map[K2]V {\n\tdst := make(map[K2]V, len(m))\n\tfor k, v := range m {\n\t\tdst[fn(k)] = v\n\t}\n\treturn dst\n}\n\n// Any returns true if any element in src satisfies the predicate.\nfunc Any[A any](src []A, pred func(A) bool) bool {\n\tfor _, v := range src {\n\t\tif pred(v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// All returns true if all elements in src satisfy the predicate.\nfunc All[A any](src []A, pred func(A) bool) bool {\n\tfor _, v := range src {\n\t\tif !pred(v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// FlatMap applies fn on all elements in src, producing a new slice\n// with the results, in order.\nfunc FlatMap[A, B any](src []A, fn func(A) []B) []B {\n\tvar dst []B\n\tfor _, v := range src {\n\t\tdst = append(dst, fn(v)...)\n\t}\n\treturn dst\n}\n\n// Find returns the first element where pred returns true.\n// The second argument is true if an element was found.\nfunc Find[A any](src []A, pred func(A) bool) (A, bool) {\n\tfor _, v := range src {\n\t\tif pred(v) {\n\t\t\treturn v, true\n\t\t}\n\t}\n\n\tvar zero A\n\treturn zero, false\n}\n\n// Filter applies fn on all elements in src, producing a new slice\n// containing the elements for which fn returned true, preserving\n// the same order.\nfunc Filter[Elem any](src []Elem, fn func(Elem) bool) []Elem {\n\tdst := make([]Elem, 0, len(src))\n\tfor _, v := range src {\n\t\tif fn(v) {\n\t\t\tdst = append(dst, v)\n\t\t}\n\t}\n\treturn dst\n}\n\n// ToMap converts a slice to a map.\nfunc ToMap[K comparable, V any](src []V, key func(V) K) map[K]V {\n\tdst := make(map[K]V, len(src))\n\tfor _, v := range src {\n\t\tdst[key(v)] = v\n\t}\n\treturn dst\n}\n\n// TransformMapToSlice creates a new slice with the results of applying fn to each\n// key-value pair in m.\nfunc TransformMapToSlice[K comparable, V any, R any](m map[K]V, fn func(K, V) R) []R {\n\tr := make([]R, 0, len(m))\n\tfor k, v := range m {\n\t\tr = append(r, fn(k, v))\n\t}\n\treturn r\n}\n\n// MapKeys returns the keys of the map m.\n// The keys will be in an indeterminate order.\nfunc MapKeys[M ~map[K]V, K comparable, V any](m M) []K {\n\tr := make([]K, 0, len(m))\n\tfor k := range m {\n\t\tr = append(r, k)\n\t}\n\treturn r\n}\n\n// CloseIgnore closes c, ignoring any error.\n// Its main use is to satisfy linters.\nfunc CloseIgnore(c io.Closer) {\n\t_ = c.Close()\n}\n\nfunc CloseIgnoreCtx(ctx context.Context, close func(ctx context.Context) error) {\n\t_ = close(ctx)\n}\n\n// MergeMaps merges all maps into a single map.\nfunc MergeMaps[K comparable, V any](ms ...map[K]V) map[K]V {\n\tvar rtn map[K]V\n\tfor i, m := range ms {\n\t\tif i == 0 {\n\t\t\trtn = m\n\t\t\tcontinue\n\t\t}\n\t\tmaps.Copy(rtn, m)\n\t}\n\treturn rtn\n}\n\nfunc Delete[T comparable](slice []T, t T) ([]T, bool) {\n\tfor i, v := range slice {\n\t\tif v == t {\n\t\t\tslice = append(slice[:i], slice[i+1:]...)\n\t\t\treturn slice, true\n\t\t}\n\t}\n\treturn slice, false\n}\n"
  },
  {
    "path": "pkg/github/github.go",
    "content": "// Package github provides utilities for interacting with GitHub repositories.\npackage github\n\nimport (\n\t\"archive/tar\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\n// Tree contains information about a (sub-)tree in a GitHub repository.\ntype Tree struct {\n\tOwner  string // GitHub owner (user or organization)\n\tRepo   string // repository name\n\tBranch string // branch name\n\tPath   string // path to subtree (\".\" for whole project)\n}\n\n// Name reports a suitable name of the top-level directory in the tree.\n// It defaults to the repository name, unless a Path is given\n// in which case it is the last component of the path.\nfunc (t *Tree) Name() string {\n\tif base := path.Base(t.Path); base != \".\" {\n\t\treturn base\n\t}\n\treturn t.Repo\n}\n\n// ParseTree parses a GitHub repository URL into a Tree.\n//\n// Valid URLs are:\n// - github.com/owner/repo\n// - github.com/owner/repo/tree/<branch>\n// - github.com/owner/repo/tree/<branch>/<path>\n//\n// If the URL does not contain a branch, the default branch is queried\n// using GitHub's API.\nfunc ParseTree(ctx context.Context, s string) (*Tree, error) {\n\tswitch {\n\tcase strings.HasPrefix(s, \"http\"):\n\t\t// Already an URL; do nothing\n\tcase strings.HasPrefix(s, \"github.com\"):\n\t\t// Assume a URL without the scheme\n\t\ts = \"https://\" + s\n\t}\n\n\tu, err := url.Parse(s)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"invalid tree string\")\n\t}\n\n\tif u.Host != \"github.com\" {\n\t\treturn nil, errors.Newf(\"url host must be github.com, not %q\", u.Host)\n\t}\n\n\t// Path must be one of:\n\t// \"/owner/repo\"\n\t// \"/owner/repo/tree/<branch>\"\n\t// \"/owner/repo/tree/<branch>/path\"\n\tparts := strings.SplitN(u.Path, \"/\", 6)\n\tswitch {\n\tcase len(parts) == 3: // \"/owner/repo\"\n\t\towner, repo := parts[1], parts[2]\n\t\t// Check the default branch\n\t\tvar resp struct {\n\t\t\tDefaultBranch string `json:\"default_branch\"`\n\t\t}\n\t\turl := fmt.Sprintf(\"https://api.github.com/repos/%s/%s\", owner, repo)\n\t\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"lookup default branch\")\n\t\t} else if err := slurpJSON(req, &resp); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"lookup default branch\")\n\t\t}\n\t\treturn &Tree{\n\t\t\tOwner:  owner,\n\t\t\tRepo:   repo,\n\t\t\tBranch: resp.DefaultBranch,\n\t\t\tPath:   \".\",\n\t\t}, nil\n\tcase len(parts) >= 5: // \"/owner/repo\"\n\t\towner, repo, t, branch := parts[1], parts[2], parts[3], parts[4]\n\t\tp := \".\"\n\t\tif len(parts) == 6 {\n\t\t\tp = parts[5]\n\t\t}\n\t\tif t != \"tree\" {\n\t\t\treturn nil, errors.Newf(\"invalid url: %s\", u)\n\t\t}\n\t\treturn &Tree{\n\t\t\tOwner:  owner,\n\t\t\tRepo:   repo,\n\t\t\tBranch: branch,\n\t\t\tPath:   p,\n\t\t}, nil\n\tdefault:\n\t\treturn nil, errors.Newf(\"unsupported url: %s\", u)\n\t}\n}\n\nfunc slurpJSON(req *http.Request, respData any) error {\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"send request\")\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != 200 {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn errors.Newf(\"got non-200 response: %s: %s\", resp.Status, body)\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(respData); err != nil {\n\t\treturn errors.Wrap(err, \"decode response\")\n\t}\n\treturn nil\n}\n\nvar ErrEmptyTree = errors.New(\"empty tree\")\n\n// ExtractTree downloads a (sub-)tree from a GitHub repository and writes it to dst.\nfunc ExtractTree(ctx context.Context, tree *Tree, dst string) error {\n\turl := fmt.Sprintf(\"https://codeload.github.com/%s/%s/tar.gz/%s\", tree.Owner, tree.Repo, tree.Branch)\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"create request\")\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"send request\")\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != 200 {\n\t\treturn errors.Newf(\"GET %s: got non-200 response: %s\", url, resp.Status)\n\t}\n\tgz, err := gzip.NewReader(resp.Body)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"read gzip response\")\n\t}\n\tdefer gz.Close()\n\ttr := tar.NewReader(gz)\n\n\tprefix := path.Join(tree.Repo+\"-\"+tree.Branch, tree.Path)\n\tprefix += \"/\"\n\tfiles := 0\n\n\tfor {\n\t\thdr, err := tr.Next()\n\t\tif err == io.EOF {\n\t\t\tif files == 0 {\n\t\t\t\treturn ErrEmptyTree\n\t\t\t}\n\t\t\treturn nil\n\t\t} else if err != nil {\n\t\t\treturn errors.Wrap(err, \"read repository data\")\n\t\t}\n\t\tif hdr.FileInfo().IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tif p := path.Clean(hdr.Name); strings.HasPrefix(p, prefix) {\n\t\t\tfiles++\n\t\t\tp = p[len(prefix):]\n\t\t\tfilePath := filepath.Join(dst, filepath.FromSlash(p))\n\t\t\tif err := createFile(tr, filePath); err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"create %s\", p)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// createFile creates the given file, creating any non-existent parent directories\n// in the process. It returns an error if the file already exists.\nfunc createFile(src io.Reader, dst string) error {\n\tif err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {\n\t\treturn err\n\t}\n\tf, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(f, src)\n\tif err2 := f.Close(); err == nil {\n\t\terr = err2\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "pkg/golden/golden.go",
    "content": "package golden\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nvar (\n\tupdate      bool // should we update golden files?\n\ttestMainRan bool // did TestMain get called?\n)\n\n// Test checks the test output against the golden file where the path to the golden file is\n// based on the test name; i.e. `testName.golden` within the `testdata` folder.\n// If -golden-update was passed to \"go test\", it writes new golden files instead.\nfunc Test(t testing.TB, output string) {\n\tfn := strings.Replace(t.Name(), \"/\", \"__\", -1)\n\tTestAgainst(t, fn+\".golden\", output)\n}\n\n// TestAgainst checks the test output against the golden file.\n// If -golden-update was passed to \"go test\", it writes new golden files instead.\nfunc TestAgainst(t testing.TB, goldenFileName string, output string) {\n\tif !testMainRan {\n\t\tt.Fatal(\"golden.TestMain was not called\")\n\t}\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpath := filepath.Join(wd, \"testdata\", goldenFileName)\n\n\tif update {\n\t\tif err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {\n\t\t\tt.Fatalf(\"update golden: %v\", err)\n\t\t}\n\t\terr := os.WriteFile(path, []byte(output), 0644)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"update golden: %v\", err)\n\t\t}\n\t} else {\n\t\texpect, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"read golden: %v\", err)\n\t\t}\n\t\tif diff := cmp.Diff(string(expect), output); diff != \"\" {\n\t\t\tt.Fatalf(\"bad output (-want +got):\\n%s\", diff)\n\t\t}\n\t}\n}\n\n// TestMain sets up the golden testing functionality for the package.\n// Packages that want to integrate golden testing should themselves\n// implement TestMain and call this function.\nfunc TestMain(m *testing.M) {\n\tSetup()\n\tos.Exit(m.Run())\n}\n\n// Setup sets up the golden testing functionality for the package.\n// It can be called instead of TestMain if a package wants to do multiple\n// main function handling.\nfunc Setup() {\n\tflag.BoolVar(&update, \"golden-update\", os.Getenv(\"GOLDEN_UPDATE\") != \"\", \"update golden files\")\n\tflag.Parse()\n\ttestMainRan = true\n}\n"
  },
  {
    "path": "pkg/idents/identifiers.go",
    "content": "package idents\n\nimport (\n\t\"strings\"\n\t\"unicode\"\n)\n\ntype IdentFormat int\n\nconst (\n\tCamelCase          IdentFormat = iota // camelCase\n\tPascalCase                            // PascalCase\n\tSnakeCase                             // snake_case\n\tScreamingSnakeCase                    // SCREAMING_SNAKE_CASE\n\tKebabCase                             // kebab-case\n)\n\n// Convert will take a given identifier and convert it to the\n// specified format.\nfunc Convert(goIdentifier string, format IdentFormat) string {\n\tparts := parseIdentifier(goIdentifier)\n\n\t// Step 1: convert case\n\tfor i, part := range parts {\n\t\tswitch format {\n\t\tcase CamelCase:\n\t\t\tif i == 0 {\n\t\t\t\tparts[i] = strings.ToLower(part)\n\t\t\t} else {\n\t\t\t\tparts[i] = strings.Title(part)\n\t\t\t}\n\n\t\tcase PascalCase:\n\t\t\tparts[i] = strings.Title(part)\n\n\t\tcase SnakeCase, KebabCase:\n\t\t\tparts[i] = strings.ToLower(part)\n\n\t\tcase ScreamingSnakeCase:\n\t\t\tparts[i] = strings.ToUpper(part)\n\t\t}\n\t}\n\n\t// Step 2: Join Parts\n\tswitch format {\n\tcase CamelCase, PascalCase:\n\t\treturn strings.Join(parts, \"\")\n\tcase SnakeCase, ScreamingSnakeCase:\n\t\treturn strings.Join(parts, \"_\")\n\tcase KebabCase:\n\t\treturn strings.Join(parts, \"-\")\n\tdefault:\n\t\tpanic(\"unknown identifier format\")\n\t}\n}\n\n// parseIdentifier parses a Go Identifier into the separate parts.\n// which can then be recombined as needed.\nfunc parseIdentifier(goIdentifier string) (parts []string) {\n\tif goIdentifier == \"\" {\n\t\treturn nil\n\t}\n\n\ttype runeType int\n\tconst (\n\t\tother runeType = iota\n\t\tupper\n\t\tlower\n\t)\n\n\truneToType := func(r rune, lastType runeType) runeType {\n\t\tswitch {\n\t\tcase unicode.IsUpper(r):\n\t\t\treturn upper\n\t\tcase unicode.IsLower(r):\n\t\t\treturn lower\n\t\tcase unicode.IsDigit(r):\n\t\t\tif lastType == other {\n\t\t\t\treturn lower\n\t\t\t}\n\t\t\treturn lastType\n\t\tdefault:\n\t\t\treturn other\n\t\t}\n\t}\n\n\tvar str strings.Builder\n\trecordPart := func() {\n\t\tpart := str.String()\n\t\tstr.Reset()\n\t\tif part == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tif !stringIsOnly(part, unicode.IsUpper) {\n\t\t\t// strings will always start with uppercase runes, so the if\n\t\t\t// the last uppercase rune is after index 0 but before the last\n\t\t\t// rune in the string, then we need to split it into two parts.\n\t\t\t//\n\t\t\t// i.e. \"GetAPIDocs\" => { \"Get\", \"APIDocs\" } => { \"Get\", \"API\", \"Docs\" }\n\t\t\tlastUpperCase := strings.LastIndexFunc(part, unicode.IsUpper)\n\t\t\tif lastUpperCase > 0 && lastUpperCase != len(part)-1 {\n\t\t\t\tparts = append(parts, part[:lastUpperCase])\n\t\t\t\tpart = part[lastUpperCase:]\n\t\t\t}\n\n\t\t\tpart = strings.ToLower(part)\n\t\t}\n\n\t\tparts = append(parts, part)\n\t}\n\n\tlastType := runeToType(rune(goIdentifier[0]), other)\n\tfor _, r := range goIdentifier {\n\t\truneType := runeToType(r, lastType)\n\n\t\t// If the type of rune has changed\n\t\tif lastType > runeType {\n\t\t\trecordPart()\n\t\t}\n\t\tlastType = runeType\n\n\t\tif runeType != other {\n\t\t\tstr.WriteRune(r)\n\t\t}\n\t}\n\n\trecordPart()\n\treturn\n}\n\nfunc stringIsOnly(str string, predicate func(r rune) bool) bool {\n\tfor _, r := range str {\n\t\tif !predicate(r) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// GenerateSuggestion creates a suggestion for an identifier in the given format\n// from the given input string.\nfunc GenerateSuggestion(input string, format IdentFormat) string {\n\t// Clean the string up first and remove unsupported characters\n\tinput = strings.TrimSpace(input)\n\tinput = strings.Map(func(r rune) rune {\n\t\tif unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) || r == '_' || r == '-' {\n\t\t\treturn r\n\t\t}\n\t\treturn -1\n\t}, input)\n\n\t// Remove any leading or trailing characters which are not letters\n\tinput = strings.TrimLeftFunc(input, func(r rune) bool {\n\t\treturn !unicode.IsLetter(r)\n\t})\n\tinput = strings.TrimRightFunc(input, func(r rune) bool {\n\t\treturn !unicode.IsLetter(r)\n\t})\n\n\t// Now convert the cleaned input into the desired format\n\treturn Convert(input, format)\n}\n"
  },
  {
    "path": "pkg/idents/identifiers_test.go",
    "content": "package idents\n\nimport (\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc Test_parseIdentifier(t *testing.T) {\n\tc := qt.New(t)\n\tc.Parallel()\n\n\ttests := []struct {\n\t\tinput  string\n\t\texpect []string\n\t}{\n\t\t{\"hello\", []string{\"hello\"}},\n\t\t{\"Hello\", []string{\"hello\"}},\n\t\t{\"HelloWorld\", []string{\"hello\", \"world\"}},\n\t\t{\"hello_world\", []string{\"hello\", \"world\"}},\n\t\t{\"Hello_World\", []string{\"hello\", \"world\"}},\n\t\t{\"_Hello___World__\", []string{\"hello\", \"world\"}},\n\t\t{\"RenderMarkdown\", []string{\"render\", \"markdown\"}},\n\t\t{\"RenderHTML\", []string{\"render\", \"HTML\"}},\n\t\t{\"getVersion2\", []string{\"get\", \"version2\"}},\n\t\t{\"GetAPIDocs\", []string{\"get\", \"API\", \"docs\"}},\n\t\t{\"EncoreResource-123abc\", []string{\"encore\", \"resource\", \"123abc\"}},\n\t\t{\"EncoreResource-abs-123\", []string{\"encore\", \"resource\", \"abs\", \"123\"}},\n\t\t{\"This is a full sentence... with \\\"random! bits-and_pieces123 blah\", []string{\"this\", \"is\", \"a\", \"full\", \"sentence\", \"with\", \"random\", \"bits\", \"and\", \"pieces123\", \"blah\"}},\n\t}\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tc.Run(tt.input, func(c *qt.C) {\n\t\t\tc.Parallel()\n\n\t\t\tgotParts := parseIdentifier(tt.input)\n\t\t\tc.Assert(gotParts, qt.DeepEquals, tt.expect)\n\t\t})\n\t}\n}\n\nfunc Test_convertIdentifierTo(t *testing.T) {\n\tc := qt.New(t)\n\tc.Parallel()\n\ttype args struct {\n\t\tinput              string\n\t\tcamelCase          string\n\t\tpascalCase         string\n\t\tsnakeCase          string\n\t\tscreamingSnakeCase string\n\t\tkebabCase          string\n\t}\n\ttests := []args{\n\t\t{\"Hello\", \"hello\", \"Hello\", \"hello\", \"HELLO\", \"hello\"},\n\t\t{\"HelloWorld\", \"helloWorld\", \"HelloWorld\", \"hello_world\", \"HELLO_WORLD\", \"hello-world\"},\n\t\t{\"getVersion2\", \"getVersion2\", \"GetVersion2\", \"get_version2\", \"GET_VERSION2\", \"get-version2\"},\n\t\t{\"GetAPIDocs\", \"getAPIDocs\", \"GetAPIDocs\", \"get_api_docs\", \"GET_API_DOCS\", \"get-api-docs\"},\n\t\t{\"EncoreResource-123abc\", \"encoreResource123abc\", \"EncoreResource123abc\", \"encore_resource_123abc\", \"ENCORE_RESOURCE_123ABC\", \"encore-resource-123abc\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tc.Run(tt.input, func(c *qt.C) {\n\t\t\tc.Parallel()\n\n\t\t\tc.Assert(Convert(tt.input, CamelCase), qt.Equals, tt.camelCase)\n\t\t\tc.Assert(Convert(tt.input, PascalCase), qt.Equals, tt.pascalCase)\n\t\t\tc.Assert(Convert(tt.input, SnakeCase), qt.Equals, tt.snakeCase)\n\t\t\tc.Assert(Convert(tt.input, ScreamingSnakeCase), qt.Equals, tt.screamingSnakeCase)\n\t\t\tc.Assert(Convert(tt.input, KebabCase), qt.Equals, tt.kebabCase)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/jsonext/listencoder.go",
    "content": "package jsonext\n\nimport (\n\t\"reflect\"\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/modern-go/reflect2\"\n)\n\ntype ListEncoderExtension struct {\n\tjsoniter.DummyExtension\n}\n\nfunc NewListEncoderExtension() *ListEncoderExtension {\n\treturn &ListEncoderExtension{}\n}\n\nfunc (e *ListEncoderExtension) DecorateEncoder(typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder {\n\tif typ.Kind() == reflect.Slice {\n\t\treturn &sliceEncoder{typ: typ, encoder: encoder}\n\t}\n\treturn encoder\n}\n\ntype sliceEncoder struct {\n\ttyp     reflect2.Type\n\tencoder jsoniter.ValEncoder\n}\n\nfunc (codec *sliceEncoder) IsEmpty(ptr unsafe.Pointer) bool {\n\treturn codec.encoder.IsEmpty(ptr)\n}\n\nfunc (codec *sliceEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {\n\tif codec.IsEmpty(ptr) {\n\t\tif codec.typ.Type1().Elem().Kind() == reflect.Uint8 {\n\t\t\tstream.WriteString(\"\")\n\t\t} else {\n\t\t\tstream.WriteEmptyArray()\n\t\t}\n\t\treturn\n\t}\n\n\tcodec.encoder.Encode(ptr, stream)\n}\n"
  },
  {
    "path": "pkg/jsonext/listencoder_test.go",
    "content": "package jsonext\n\nimport (\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nfunc TestListEncoder(t *testing.T) {\n\tc := qt.New(t)\n\n\tjsoniterAPI := jsoniter.Config{SortMapKeys: true}.Froze()\n\tjsoniterAPI.RegisterExtension(NewListEncoderExtension())\n\tmarshal, err := jsoniterAPI.Marshal(struct {\n\t\tStringList    []string `json:\",omitempty\"`\n\t\tIntList       []string\n\t\tFloatList     []float64\n\t\tNilBytes      []byte\n\t\tEmptyBytes    []byte\n\t\tSomeBytes     []byte\n\t\tStringPointer *string\n\t\tZeroArray     [0]int\n\t}{\n\t\tFloatList:  []float64{1.0, 2.0, 3.0},\n\t\tNilBytes:   nil,\n\t\tEmptyBytes: []byte{},\n\t\tSomeBytes:  []byte(\"foobar\"),\n\t})\n\tc.Assert(err, qt.IsNil)\n\tc.Assert(string(marshal), qt.Equals, `{\"IntList\":[],\"FloatList\":[1,2,3],\"NilBytes\":\"\",\"EmptyBytes\":\"\",\"SomeBytes\":\"Zm9vYmFy\",\"StringPointer\":null,\"ZeroArray\":[]}`)\n}\n"
  },
  {
    "path": "pkg/jsonext/protojson.go",
    "content": "package jsonext\n\nimport (\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/modern-go/reflect2\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/proto\"\n)\n\ntype ProtoEncoderExtension struct {\n\tjsoniter.DummyExtension\n\tmessageType reflect2.Type\n\topts        protojson.MarshalOptions\n}\n\nfunc NewProtoEncoderExtension() *ProtoEncoderExtension {\n\tmsgType := reflect2.TypeOfPtr((*proto.Message)(nil)).Elem()\n\topts := protojson.MarshalOptions{\n\t\tUseProtoNames:   true,\n\t\tEmitUnpopulated: true,\n\t}\n\treturn &ProtoEncoderExtension{\n\t\tmessageType: msgType,\n\t\topts:        opts,\n\t}\n}\n\nfunc (e *ProtoEncoderExtension) DecorateEncoder(typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder {\n\tif typ.Implements(e.messageType) {\n\t\treturn &messageEncoder{typ: typ, encoder: encoder, opts: e.opts}\n\t}\n\treturn encoder\n}\n\ntype messageEncoder struct {\n\ttyp     reflect2.Type\n\tencoder jsoniter.ValEncoder\n\topts    protojson.MarshalOptions\n}\n\nfunc (codec *messageEncoder) IsEmpty(ptr unsafe.Pointer) bool {\n\treturn codec.encoder.IsEmpty(ptr)\n}\n\nfunc (codec *messageEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {\n\tif msg, ok := codec.typ.UnsafeIndirect(ptr).(proto.Message); ok {\n\t\tdata, err := codec.opts.Marshal(msg)\n\t\tif err != nil {\n\t\t\tif stream.Error == nil {\n\t\t\t\tstream.Error = err\n\t\t\t}\n\t\t} else {\n\t\t\tstream.Write(data)\n\t\t}\n\t\treturn\n\t}\n\tcodec.encoder.Encode(ptr, stream)\n}\n\nvar ProtoEncoder = (func() jsoniter.API {\n\tenc := jsoniter.Config{}.Froze()\n\t// Note: the order is important. We don't want the list encoder to process repeated fields in proto\n\t// messages, so it must come first so it only applies to non-protobuf slices.\n\tenc.RegisterExtension(NewListEncoderExtension())\n\tenc.RegisterExtension(NewProtoEncoderExtension())\n\treturn enc\n})()\n"
  },
  {
    "path": "pkg/logging/zerolog_adapter.go",
    "content": "package logging\n\nimport (\n\t\"log\"\n\n\t\"github.com/rs/zerolog\"\n)\n\ntype zeroLogWriter struct {\n\tlogger zerolog.Logger\n\tlevel  zerolog.Level\n}\n\nfunc (z *zeroLogWriter) Write(p []byte) (n int, err error) {\n\tz.logger.WithLevel(z.level).CallerSkipFrame(3).Msg(string(p))\n\treturn len(p), nil\n}\n\n// NewZeroLogAdapter returns a new log.Logger that writes to the given zerolog.Logger at the given level.\nfunc NewZeroLogAdapter(logger zerolog.Logger, level zerolog.Level) *log.Logger {\n\tzlw := &zeroLogWriter{logger, level}\n\treturn log.New(zlw, \"\", 0)\n}\n"
  },
  {
    "path": "pkg/make-release/compilers.go",
    "content": "package main\n\nimport (\n\tosPkg \"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\n// MacOSSDKPath is the path to where the MacOS SDK is located on Encore's builder systems\nconst MacOSSDKPath = \"/sdk\"\n\nfunc GoBaseEnvs(os string, arch string) ([]string, error) {\n\t// Create a cache dir for the go build cache for this specific OS and architecture pair\n\tcacheDir, err := osPkg.UserCacheDir()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"user cache dir\")\n\t}\n\n\tpath := filepath.Join(cacheDir, \"encore-build-cache\", \"go\", os, arch)\n\n\terr = osPkg.MkdirAll(path, 0755)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to make cache dir\")\n\t}\n\n\treturn append(osPkg.Environ(),\n\t\t\"GOOS=\"+os,\n\t\t\"GOARCH=\"+arch,\n\t\t\"GOCACHE=\"+path,\n\t), nil\n}\n\n// CompileGoBinary compiles a Go binary for the given OS and architecture with GCP enabled\n//\n// This file was inspired by the blog post: https://lucor.dev/post/cross-compile-golang-fyne-project-using-zig/\nfunc CompileGoBinary(outputPath string, entrypointPkg string, ldFlags []string, os string, arch string) error {\n\tcc, cxx, compilerEnvs, compilerLDFlags, err := compilerSettings(os, arch)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"compiler settings\")\n\t}\n\n\tif os == \"windows\" {\n\t\toutputPath += \".exe\"\n\t}\n\n\tcombinedLDFlags := append(append([]string{}, compilerLDFlags...), ldFlags...)\n\n\tbaseEnvs, err := GoBaseEnvs(os, arch)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"go base envs\")\n\t}\n\n\tenvs := append(baseEnvs,\n\t\t\"CGO_ENABLED=1\",\n\t\t\"CC=\"+cc,\n\t\t\"CXX=\"+cxx,\n\t)\n\tenvs = append(envs, compilerEnvs...)\n\n\t// Build the go build args\n\targs := []string{\"build\",\n\t\t\"-trimpath\",\n\t\t\"-tags\", \"netgo\", // Always force netgo otherwise we end up with segfaults on MacOS\n\t}\n\tif len(combinedLDFlags) > 0 {\n\t\targs = append(args, \"-ldflags=\"+strings.Join(combinedLDFlags, \" \"))\n\t}\n\tif os == \"darwin\" {\n\t\targs = append(args, \"-buildmode=pie\")\n\t}\n\targs = append(args,\n\t\t\"-o\", outputPath,\n\t\tentrypointPkg,\n\t)\n\n\t// Build the command\n\tcmd := exec.Command(\"go\", args...)\n\tcmd.Env = envs\n\n\t// nosemgrep\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn errors.Newf(\"failed to compile go binary: %s\", string(out))\n\t}\n\n\treturn nil\n}\n\n// CompileRustBinary compiles a Rust binary for the given OS and architecture\n//\n// We're using zigbuild to perform easy cross compiling\nfunc CompileRustBinary(artifactPath, outputPath string, cratePath string, os string, arch string, extraEnvVars ...string) error {\n\tif os == \"windows\" {\n\t\tif !strings.HasSuffix(artifactPath, \".dll\") {\n\t\t\toutputPath += \".exe\"\n\t\t\tartifactPath += \".exe\"\n\t\t}\n\t}\n\n\tenvs := append(extraEnvVars, osPkg.Environ()...)\n\n\tvar target string\n\tswitch os {\n\tcase \"darwin\":\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\ttarget = \"x86_64-apple-darwin\"\n\t\tcase \"arm64\":\n\t\t\ttarget = \"aarch64-apple-darwin\"\n\t\tdefault:\n\t\t\treturn errors.New(\"unsupported architecture for darwin: \" + arch)\n\t\t}\n\n\t\t// We need to set the SDKROOT for cross compiling to MacOS\n\t\tif runtime.GOOS != \"darwin\" {\n\t\t\tenvs = append(envs,\n\t\t\t\t\"SDKROOT=\"+MacOSSDKPath,\n\t\t\t)\n\t\t}\n\n\tcase \"linux\":\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\ttarget = \"x86_64-unknown-linux-gnu\"\n\t\tcase \"arm64\":\n\t\t\ttarget = \"aarch64-unknown-linux-gnu\"\n\t\tdefault:\n\t\t\treturn errors.New(\"unsupported architecture for linux: \" + arch)\n\t\t}\n\n\tcase \"windows\":\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\ttarget = \"x86_64-pc-windows-gnu\"\n\t\tdefault:\n\t\t\treturn errors.New(\"unsupported architecture for windows: \" + arch)\n\t\t}\n\n\tdefault:\n\t\treturn errors.New(\"unsupported os: \" + os)\n\t}\n\n\t// Create a cache dir for the go build cache for this specific OS and architecture pair\n\tcacheDir, err := osPkg.UserCacheDir()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"user cache dir\")\n\t}\n\n\tpath := filepath.Join(cacheDir, \"encore-build-cache\", \"rust\", os, arch)\n\n\terr = osPkg.MkdirAll(path, 0755)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"failed to make cache dir\")\n\t}\n\n\t// Build the command\n\tcargoArgs := []string{\"zigbuild\",\n\t\t\"--target\", target,\n\t\t\"--target-dir\", path,\n\t\t\"--release\",\n\t}\n\n\tcmd := exec.Command(\"cargo\", cargoArgs...)\n\tcmd.Dir = cratePath\n\tcmd.Env = envs\n\n\t// Cargo can't run multiple compiles at the same time for the same crate\n\t// so let's lock here, then unlock once the compile has finished\n\tcargoLock.Lock()\n\tdefer cargoLock.Unlock()\n\n\t// nosemgrep\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn errors.Newf(\"failed to compile rust binary: %v - %s\", err, string(out))\n\t}\n\n\t// Copy the binary to the output path\n\tbinaryFile := filepath.Join(path, target, \"release\", artifactPath)\n\tcmd = exec.Command(\"cp\", binaryFile, outputPath)\n\t// nosemgrep\n\tout, err = cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn errors.Newf(\"failed to copy rust binary: %v - %s\", err, string(out))\n\t}\n\n\treturn nil\n}\n\n// compilerSettings returns the CC and CXX settings for the given OS and architecture\nfunc compilerSettings(os string, arch string) (cc, cxx string, envs, ldFlags []string, err error) {\n\tvar zigTarget string\n\tvar zigArgs string\n\tzigBinary := \"zig\" // pick it up off the path\n\n\tswitch os {\n\tcase \"darwin\":\n\t\tzigBinary = \"/usr/local/zig-0.9.1/zig\" // We need an explicit version of Zig for darwin (0.11.0 compiles, build causes runtime errors)\n\t\tldFlags = []string{\"-s\", \"-w\"}\n\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\tzigTarget = \"x86_64-macos.10.12\"\n\t\tcase \"arm64\":\n\t\t\tzigTarget = \"aarch64-macos.11.1\"\n\t\tdefault:\n\t\t\treturn \"\", \"\", nil, nil, errors.New(\"unsupported architecture for darwin: \" + arch)\n\t\t}\n\n\t\t// We need to set some extra stuff if we're cross compiling to MacOS\n\t\tif runtime.GOOS != \"darwin\" {\n\t\t\tzigArgs = \" -isysroot \" + MacOSSDKPath + \" -iwithsysroot /usr/include -iframeworkwithsysroot /System/Library/Frameworks\"\n\t\t\tenvs = []string{\n\t\t\t\t\"CGO_LDFLAGS=--sysroot \" + MacOSSDKPath + \" -F/System/Library/Frameworks -L/usr/lib\",\n\t\t\t}\n\t\t}\n\n\tcase \"linux\":\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\tzigTarget = \"x86_64-linux-gnu\" // Note: we're not targeting a specific glibc version here as we tried before with 2.35 - but for some reason we still get runtime errors not finding 2.34 or 2.33 on WSL (which had 2.35)\n\t\t\tzigArgs = \" -static -isystem /usr/include\"\n\t\tcase \"arm64\":\n\t\t\tzigTarget = \"aarch64-linux-gnu\"\n\t\t\tzigArgs = \" -static -isystem /usr/include\"\n\t\t\tenvs = []string{\n\t\t\t\t\"PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig\",\n\t\t\t}\n\t\tdefault:\n\t\t\treturn \"\", \"\", nil, nil, errors.New(\"unsupported architecture for linux: \" + arch)\n\t\t}\n\n\tcase \"windows\":\n\t\tswitch arch {\n\t\tcase \"amd64\":\n\t\t\tzigTarget = \"x86_64-windows-gnu\"\n\t\tdefault:\n\t\t\treturn \"\", \"\", nil, nil, errors.New(\"unsupported architecture for windows: \" + arch)\n\t\t}\n\n\t\tldFlags = []string{\"-H=windowsgui\"}\n\n\tdefault:\n\t\treturn \"\", \"\", nil, nil, errors.New(\"unsupported os: \" + os)\n\t}\n\n\tcc = zigBinary + \" cc -target \" + zigTarget + zigArgs\n\tcxx = zigBinary + \" c++ -target \" + zigTarget + zigArgs\n\treturn cc, cxx, envs, ldFlags, nil\n}\n\nvar (\n\tcargoLock sync.Mutex\n)\n"
  },
  {
    "path": "pkg/make-release/dist_builder.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/internal/version\"\n)\n\n// A DistBuilder is a builder for a specific distribution of Encore.\n//\n// Anything which does not need to be built for a specific distribution\n// should be built in the main builder before these are invoked.\n//\n// Make release will run multiple of these in parallel to build all the\n// distributions.\ntype DistBuilder struct {\n\tlog              zerolog.Logger\n\tOS               string      // The OS to build for\n\tArch             string      // The architecture to build for\n\tTSParserPath     string      // The path to the ts-parser repo\n\tDistBuildDir     string      // The directory to build into\n\tArtifactsTarFile string      // The directory to put the final tar.gz artifact into\n\tVersion          string      // The version to build\n\tjsBuilder        *JSPackager // The JS builder\n}\n\nfunc (d *DistBuilder) buildEncoreCLI() error {\n\t// Build the CLI binaries.\n\td.log.Info().Msg(\"building encore binary...\")\n\n\tlinkerOpts := []string{\n\t\t\"-X\", fmt.Sprintf(\"'encr.dev/internal/version.Version=%s'\", d.Version),\n\t}\n\n\t// If we're building a nightly, devel or beta version, we need to set the default config directory\n\tvar versionSuffix string\n\tswitch version.ChannelFor(d.Version) {\n\tcase version.GA:\n\t\tversionSuffix = \"\"\n\tcase version.Beta:\n\t\tversionSuffix = \"-beta\"\n\tcase version.Nightly:\n\t\tversionSuffix = \"-nightly\"\n\tcase version.DevBuild:\n\t\tversionSuffix = \"-develop\"\n\tdefault:\n\t\treturn errors.Newf(\"unknown version channel for %s\", d.Version)\n\t}\n\n\tif versionSuffix != \"\" {\n\t\tlinkerOpts = append(linkerOpts,\n\t\t\t\"-X\", \"'encr.dev/internal/conf.defaultConfigDirectory=encore\"+versionSuffix+\"'\",\n\t\t)\n\t}\n\n\terr := CompileGoBinary(\n\t\tjoin(d.DistBuildDir, \"bin\", \"encore\"+versionSuffix),\n\t\t\"./cli/cmd/encore\",\n\t\tlinkerOpts,\n\t\td.OS,\n\t\td.Arch,\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"encore failed to build\")\n\t\treturn errors.Wrap(err, \"compile encore\")\n\t}\n\td.log.Info().Msg(\"encore built successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) buildGitHook() error {\n\t// Build the git-remote-encore binary.\n\td.log.Info().Msg(\"building git-remote-encore binary...\")\n\terr := CompileGoBinary(\n\t\tjoin(d.DistBuildDir, \"bin\", \"git-remote-encore\"),\n\t\t\"./cli/cmd/git-remote-encore\",\n\t\tnil,\n\t\td.OS,\n\t\td.Arch,\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"git-remote-encore failed to build\")\n\t\treturn errors.Wrap(err, \"compile git-remote-encore\")\n\t}\n\td.log.Info().Msg(\"git-remote-encore built successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) buildTSBundler() error {\n\t// Build the TS bundler.\n\td.log.Info().Msg(\"building tsbundler binary...\")\n\n\tlinkerOpts := []string{\n\t\t\"-X\", fmt.Sprintf(\"'encr.dev/internal/version.Version=%s'\", d.Version),\n\t}\n\n\terr := CompileGoBinary(\n\t\tjoin(d.DistBuildDir, \"bin\", \"tsbundler-encore\"),\n\t\t\"./cli/cmd/tsbundler-encore\",\n\t\tlinkerOpts,\n\t\td.OS,\n\t\td.Arch,\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"tsbundler failed to build\")\n\t\treturn errors.Wrap(err, \"compile tsbundler\")\n\t}\n\td.log.Info().Msg(\"tsbundler built successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) buildTSParser() error {\n\t// Build the TS parser.\n\td.log.Info().Msg(\"building ts-parser binary...\")\n\terr := CompileRustBinary(\n\t\t\"tsparser-encore\",\n\t\tjoin(d.DistBuildDir, \"bin\", \"tsparser-encore\"),\n\t\td.TSParserPath,\n\t\td.OS,\n\t\td.Arch,\n\t\tfmt.Sprintf(\"ENCORE_VERSION=%s\", d.Version),\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"ts-parser failed to build\")\n\t\treturn errors.Wrap(err, \"compile ts-parser\")\n\t}\n\td.log.Info().Msg(\"ts-parser built successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) buildNodePlugin() error {\n\td.log.Info().Msg(\"building node plugin...\")\n\n\t// Figure out the names of the compiled and target binaries.\n\tcompiledBinaryName, err := func() (string, error) {\n\t\tswitch d.OS {\n\t\tcase \"darwin\":\n\t\t\treturn \"libencore_js_runtime.dylib\", nil\n\t\tcase \"linux\":\n\t\t\treturn \"libencore_js_runtime.so\", nil\n\t\tcase \"windows\":\n\t\t\treturn \"encore_js_runtime.dll\", nil\n\t\tdefault:\n\t\t\treturn \"\", errors.Newf(\"unknown OS: %s\", d.OS)\n\t\t}\n\t}()\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"node plugin failed to build\")\n\t\treturn errors.Wrap(err, \"compile node plugin\")\n\t}\n\n\td.log.Info().Msg(\"Patching jscore/api/version.cjs...\")\n\terr = os.WriteFile(\n\t\tfilepath.Join(\".\", \"runtimes\", \"jscore\", \"api\", \"version.cjs\"),\n\t\t[]byte(`// Code generated by /pkg/make-release. DO NOT EDIT.\n\n/**\n * The version of the runtime this JS bundle was built for\n */\nmodule.exports.version = \"`+d.Version+`\";\n`),\n\t\t0644,\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"failed to patch version.cjs\")\n\t\treturn errors.Wrap(err, \"write patch version.cjs\")\n\t}\n\n\t// Build the node plugin.\n\terr = CompileRustBinary(\n\t\tcompiledBinaryName,\n\t\tjoin(d.DistBuildDir, \"bin\", \"encore-runtime.node\"),\n\t\t\"./runtimes/jscore\",\n\t\td.OS,\n\t\td.Arch,\n\t\tfmt.Sprintf(\"ENCORE_VERSION=%s\", d.Version),\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"node plugin failed to build\")\n\t\treturn errors.Wrap(err, \"compile node plugin\")\n\t}\n\td.log.Info().Msg(\"node plugin built successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) downloadEncoreGo() error {\n\t// Step 1: Find out the latest release version for Encore's Go distribution\n\td.log.Info().Msg(\"downloading latest encore-go...\")\n\tencoreGoArchive, err := downloadLatestGithubRelease(\"encoredev\", \"go\", d.OS, d.Arch)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"failed to download encore-go\")\n\t\treturn errors.Wrap(err, \"download encore-go\")\n\t}\n\n\td.log.Info().Msg(\"extracting encore-go...\")\n\terr = extractArchive(encoreGoArchive, d.DistBuildDir)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"failed to extract encore-go\")\n\t\treturn errors.Wrap(err, \"extract encore-go\")\n\t}\n\n\td.log.Info().Msg(\"encore-go extracted successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) copyEncoreRuntimeForGo() error {\n\td.log.Info().Msg(\"copying encore runtime for Go...\")\n\tcmd := exec.Command(\"cp\", \"-r\", \"runtimes/go/.\", join(d.DistBuildDir, \"runtimes\", \"go\")+\"/\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\td.log.Err(err).Str(\"stderr\", string(out)).Msg(\"encore runtime for go failed to be copied\")\n\t\treturn errors.Wrapf(err, \"cp go runtime: %s\", out)\n\t}\n\td.log.Info().Msg(\"encore runtime for go copied successfully\")\n\treturn nil\n}\n\nfunc (d *DistBuilder) copyEncoreRuntimeForJS() error {\n\td.log.Info().Msg(\"waiting for JS packager to complete...\")\n\t<-d.jsBuilder.compileCompleted\n\tif d.jsBuilder.compileFailed.Load() {\n\t\td.log.Error().Msg(\"JS packager failed to build\")\n\t\treturn errors.New(\"js build failed\")\n\t}\n\n\td.log.Info().Msg(\"copying encore runtime for JS...\")\n\tcmd := exec.Command(\"cp\", \"-r\", d.jsBuilder.DistFolder+\"/.\", join(d.DistBuildDir, \"runtimes\", \"js\")+\"/\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\td.log.Err(err).Str(\"stderr\", string(out)).Msg(\"encore runtime for js failed to be copied\")\n\t\treturn errors.Wrapf(err, \"cp js runtime: %s\", out)\n\t}\n\td.log.Info().Msg(\"encore runtime for js copied successfully\")\n\treturn nil\n}\n\n// Build builds the distribution running each step in order\nfunc (d *DistBuilder) Build() error {\n\td.log = log.With().Str(\"os\", d.OS).Str(\"arch\", d.Arch).Logger()\n\n\td.log.Info().Msg(\"building distribution...\")\n\n\t// Prepare the target directory.\n\tif err := os.RemoveAll(d.DistBuildDir); err != nil {\n\t\td.log.Err(err).Msg(\"failed to remove existing target dir\")\n\t\treturn errors.Wrap(err, \"remove target dir\")\n\t} else if err := os.MkdirAll(d.DistBuildDir, 0755); err != nil {\n\t\td.log.Err(err).Msg(\"failed to create target dir\")\n\t\treturn errors.Wrap(err, \"create target dir\")\n\t} else if err := os.MkdirAll(join(d.DistBuildDir, \"bin\"), 0755); err != nil {\n\t\td.log.Err(err).Msg(\"failed to create bin dir\")\n\t\treturn errors.Wrap(err, \"create bin dir\")\n\t} else if err := os.MkdirAll(join(d.DistBuildDir, \"runtimes\"), 0755); err != nil {\n\t\td.log.Err(err).Msg(\"failed to create runtimes dir\")\n\t\treturn errors.Wrap(err, \"create runtimes/go dir\")\n\t} else if err := os.MkdirAll(join(d.DistBuildDir, \"runtimes\", \"go\"), 0755); err != nil {\n\t\td.log.Err(err).Msg(\"failed to create runtimes/go dir\")\n\t\treturn errors.Wrap(err, \"create runtimes/go dir\")\n\t} else if err := os.MkdirAll(join(d.DistBuildDir, \"runtimes\", \"js\"), 0755); err != nil {\n\t\td.log.Err(err).Msg(\"failed to create runtimes/js dir\")\n\t\treturn errors.Wrap(err, \"create runtimes/js dir\")\n\t}\n\n\t// Now we're prepped, start building.\n\terr := runParallel(\n\t\td.buildEncoreCLI,\n\t\td.buildTSBundler,\n\t\td.buildGitHook,\n\t\td.buildTSParser,\n\t\td.buildNodePlugin,\n\t\td.copyEncoreRuntimeForGo,\n\t\td.copyEncoreRuntimeForJS,\n\t\td.downloadEncoreGo,\n\t)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"failed to build distribution\")\n\t\treturn errors.Wrapf(err, \" os: %s, arch: %s\", d.OS, d.Arch)\n\t}\n\n\t// Now tar gzip the directory\n\td.log.Info().Str(\"tar_file\", d.ArtifactsTarFile).Msg(\"creating distribution tar file...\")\n\terr = TarGzip(d.DistBuildDir, d.ArtifactsTarFile)\n\tif err != nil {\n\t\td.log.Err(err).Msg(\"failed to tar gzip distribution\")\n\t\treturn errors.Wrapf(err, \" os: %s, arch: %s\", d.OS, d.Arch)\n\t}\n\n\td.log.Info().Str(\"tar_file\", d.ArtifactsTarFile).Msg(\"distribution built successfully\")\n\treturn nil\n}\n\n// runParallel runs the given functions in parallel, returning the first error\nfunc runParallel(functions ...func() error) error {\n\tvar wg sync.WaitGroup\n\twg.Add(len(functions))\n\tvar firstErr error\n\tvar mu sync.Mutex\n\n\tfor _, f := range functions {\n\t\tf := f\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tif err := f(); err != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\tdefer mu.Unlock()\n\t\t\t\tif firstErr == nil {\n\t\t\t\t\tfirstErr = err\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\tmu.Lock()\n\tdefer mu.Unlock()\n\treturn firstErr\n}\n"
  },
  {
    "path": "pkg/make-release/js_packager.go",
    "content": "package main\n\nimport (\n\t\"io/fs\"\n\tosPkg \"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n)\n\ntype JSPackager struct {\n\tlog              zerolog.Logger\n\tWorkspaceRoot    string // Path to the yarn workspace root\n\tVersion          string // the new version number\n\tDistFolder       string // the folder to output the compiled JS to\n\tcompileCompleted chan struct{}\n\tcompileFailed    atomic.Bool\n}\n\nfunc (j *JSPackager) PatchVersions() error {\n\tj.log.Info().Msg(\"Patching versions...\")\n\n\treplaceVersionRegex, err := regexp.Compile(`\"version\": \"([^-\"]+)(?:-[^\"]+)?\",`)\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"failed to compile regex\")\n\t\treturn errors.Wrap(err, \"compile regex\")\n\t}\n\n\t// For all the folders in the packages dir\n\tpackages, err := j.listPackages(filepath.Join(j.WorkspaceRoot, \"packages\"), \"\")\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"list packages\")\n\t}\n\n\tfor _, pkg := range packages {\n\t\t// Look for a line containing `\"version\": \".*\"`, and replace it\n\t\t// with `\"version\": \"0.0.0\"`.\n\t\t// (note we drop the \"v\" prefix for the package.json)\n\t\tnewFile := replaceVersionRegex.ReplaceAll(pkg.PackageJsonData, []byte(`\"version\": \"`+j.Version[1:]+`\",`))\n\n\t\t// Write the file back to disk\n\t\terr = osPkg.WriteFile(pkg.PackageJsonPath, newFile, 0644)\n\t\tif err != nil {\n\t\t\tj.log.Err(err).Str(\"package\", pkg.Name).Msg(\"failed to write package.json\")\n\t\t\treturn errors.Wrap(err, \"write package.json\")\n\t\t}\n\t}\n\n\tj.log.Info().Msg(\"Patching internal-runtime/conf/version.ts...\")\n\terr = osPkg.WriteFile(\n\t\tfilepath.Join(j.WorkspaceRoot, \"packages\", \"@encore.dev\", \"internal-runtime\", \"conf\", \"version.ts\"),\n\t\t[]byte(`// Code generated by /pkg/make-release. DO NOT EDIT.\n\n/**\n * The current version of the runtime.\n */\nexport const Version = \"`+j.Version+`\";\n`),\n\t\t0644,\n\t)\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"failed to patch version.ts\")\n\t\treturn errors.Wrap(err, \"write patch version.ts\")\n\t}\n\n\treturn nil\n}\n\ntype packageInfo struct {\n\tRootDir         string\n\tName            string\n\tPackageJsonPath string\n\tPackageJsonData []byte\n}\n\nfunc (j *JSPackager) listPackages(dir string, basePkgName string) ([]packageInfo, error) {\n\tvar pkgs []packageInfo\n\n\t// For all the folders in the packages dir\n\tentries, err := osPkg.ReadDir(dir)\n\tif err != nil {\n\t\tj.log.Err(err).Str(\"dir\", dir).Msg(\"failed to read directory\")\n\t\treturn nil, errors.Wrap(err, \"read directory\")\n\t}\n\n\tfor _, e := range entries {\n\t\tpkgDir := filepath.Join(dir, e.Name())\n\t\tswitch {\n\t\tcase !e.IsDir():\n\t\t\tcontinue\n\t\tcase strings.HasPrefix(e.Name(), \"@\"):\n\t\t\tchildren, err := j.listPackages(pkgDir, e.Name())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tpkgs = append(pkgs, children...)\n\t\tdefault:\n\t\t\tpkgJsonPath := filepath.Join(pkgDir, \"package.json\")\n\t\t\tpkgJson, err := osPkg.ReadFile(pkgJsonPath)\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t} else if err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"read package.json\")\n\t\t\t}\n\n\t\t\tpkgName := e.Name()\n\t\t\tif basePkgName != \"\" {\n\t\t\t\tpkgName = basePkgName + \"/\" + pkgName\n\t\t\t}\n\n\t\t\tpkgs = append(pkgs, packageInfo{\n\t\t\t\tRootDir:         pkgDir,\n\t\t\t\tName:            pkgName,\n\t\t\t\tPackageJsonPath: pkgJsonPath,\n\t\t\t\tPackageJsonData: pkgJson,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn pkgs, nil\n}\n\nfunc (j *JSPackager) Package() (rtnErr error) {\n\tdefer func() {\n\t\tif rtnErr != nil {\n\t\t\tj.compileFailed.Store(true)\n\t\t}\n\t\tclose(j.compileCompleted)\n\t}()\n\n\tj.DistFolder = filepath.Join(j.WorkspaceRoot, \"dist\")\n\n\t// Remove the existing dist directory\n\tif err := osPkg.RemoveAll(j.DistFolder); err != nil {\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\tj.log.Err(err).Msg(\"failed to remove dist directory\")\n\t\t\treturn errors.Wrap(err, \"remove dist directory\")\n\t\t}\n\t}\n\n\t// Patch the versions\n\tif err := j.PatchVersions(); err != nil {\n\t\tj.log.Err(err).Msg(\"failed to patch versions\")\n\t\treturn errors.Wrap(err, \"patch versions\")\n\t}\n\n\t// Ensure our node_modules is up to date and installed\n\tlog.Info().Msg(\"Installing JS dependencies...\")\n\terr := j.yarn(\"install\")\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"yarn install failed\")\n\t\treturn errors.Wrap(err, \"yarn install\")\n\t}\n\n\t// Now clean up previous builds inside yarn\n\tlog.Info().Msg(\"Cleaning up from previous builds...\")\n\terr = j.yarn(\"clean\")\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"yarn clean failed\")\n\t\treturn errors.Wrap(err, \"yarn clean\")\n\t}\n\n\tlog.Info().Msg(\"Building JS...\")\n\terr = j.yarn(\"build\", \"--filter=./packages/**/*\", \"--force\") // only build our packages\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"yarn build failed\")\n\t\treturn errors.Wrap(err, \"yarn build\")\n\t}\n\n\tlog.Info().Msg(\"Fixing dist folder for ESM\")\n\terr = j.yarn(\"fix-dist\")\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"yarn fix-dist failed\")\n\t\treturn errors.Wrap(err, \"yarn fix-dist\")\n\t}\n\n\t// Now it's all build we can start the publish/packing process\n\tlog.Info().Msg(\"Publishing packages to npm...\")\n\tnpmTag := \"latest\"\n\tswitch {\n\tcase strings.Contains(j.Version, \"-beta.\"):\n\t\tnpmTag = \"beta\"\n\tcase strings.Contains(j.Version, \"-nightly.\"):\n\t\tnpmTag = \"nightly\"\n\t}\n\terr = j.yarn(\"workspaces\", \"foreach\", \"--no-private\", \"-pt\", \"npm\", \"publish\", \"--tolerate-republish\", \"--access\", \"public\", \"--tag\", npmTag)\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"yarn publish failed\")\n\t\treturn errors.Wrap(err, \"yarn publish\")\n\t}\n\n\t// Create our dist folder\n\terr = osPkg.MkdirAll(j.DistFolder, 0755)\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"failed to create dist directory\")\n\t\treturn errors.Wrap(err, \"create dist directory\")\n\t}\n\n\t// Package up our JS runtime\n\tlog.Info().Msg(\"Packaging JS runtime packages...\")\n\terr = j.yarn(\"workspaces\", \"foreach\", \"--no-private\", \"pack\", \"--out\", filepath.Join(j.DistFolder, \"%s.tgz\"))\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"yarn pack failed\")\n\t\treturn errors.Wrap(err, \"yarn pack\")\n\t}\n\n\t// Now extract all the tarballs in our dist folder, deleting the tarballs\n\t// as we go.\n\tlog.Info().Msg(\"Extracting JS runtime packages...\")\n\tdirEntries, err := osPkg.ReadDir(j.DistFolder)\n\tif err != nil {\n\t\tj.log.Err(err).Msg(\"failed to read dist directory\")\n\t\treturn errors.Wrap(err, \"read dist directory\")\n\t}\n\tfor _, entry := range dirEntries {\n\t\tif entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif filepath.Ext(entry.Name()) != \".tgz\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := entry.Name()\n\t\tname = strings.TrimSuffix(name, filepath.Ext(name))\n\t\tdirToExtractTo := filepath.Join(j.DistFolder, name)\n\t\tdirToExtractTo = strings.ReplaceAll(dirToExtractTo, \"@encore.dev-\", \"@encore.dev/\")\n\n\t\t// Create the target directory\n\t\terr = osPkg.MkdirAll(dirToExtractTo, 0755)\n\t\tif err != nil {\n\t\t\tj.log.Err(err).Str(\"tgz_file\", entry.Name()).Str(\"target\", dirToExtractTo).Msg(\"failed to create target directory\")\n\t\t\treturn errors.Wrap(err, \"create target directory\")\n\t\t}\n\n\t\t// Extract the tarball\n\t\tout, err := exec.Command(\"tar\", \"-xzf\", filepath.Join(j.DistFolder, entry.Name()), \"--strip-components\", \"1\", \"-C\", dirToExtractTo).CombinedOutput()\n\t\t// nosemgrep\n\t\tif err != nil {\n\t\t\tj.log.Err(err).Str(\"tgz_file\", entry.Name()).Str(\"target\", dirToExtractTo).Str(\"output\", string(out)).Msg(\"failed to extract tarball\")\n\t\t\treturn errors.Wrap(err, \"extract tarball\")\n\t\t}\n\n\t\t// Delete the tarball\n\t\terr = osPkg.Remove(filepath.Join(j.DistFolder, entry.Name()))\n\t\tif err != nil {\n\t\t\tj.log.Err(err).Str(\"tgz_file\", entry.Name()).Str(\"target\", dirToExtractTo).Msg(\"failed to delete tarball\")\n\t\t\treturn errors.Wrap(err, \"delete tarball\")\n\t\t}\n\t}\n\n\tlog.Info().Msg(\"JS runtime packaged successfully\")\n\treturn nil\n}\n\nfunc (j *JSPackager) yarn(args ...string) error {\n\tcmd := exec.Command(\"yarn\", args...)\n\tcmd.Env = osPkg.Environ()\n\tcmd.Dir = j.WorkspaceRoot\n\tcmd.Stdout = osPkg.Stdout\n\tcmd.Stderr = osPkg.Stderr\n\n\tj.log.Debug().Str(\"cmd\", cmd.String()).Msg(\"running yarn command...\")\n\tif err := cmd.Run(); err != nil {\n\t\treturn errors.Wrap(err, \"yarn command failed\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/make-release/make-release.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/internal/version\"\n)\n\nfunc join(strs ...string) string {\n\treturn filepath.Join(strs...)\n}\n\nfunc main() {\n\tlog.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger()\n\n\tdst := flag.String(\"dst\", \"\", \"build destination\")\n\tversionStr := flag.String(\"v\", \"\", \"version number\")\n\ttsParserRepo := flag.String(\"ts-parser\", \"\", \"path to ts-parser repo\")\n\tonlyBuild := flag.String(\"only\", \"\", \"build only the valid target ('darwin-arm64' or 'darwin' or 'arm64' or '' for all)\")\n\tflag.Parse()\n\tif *dst == \"\" || *versionStr == \"\" || *tsParserRepo == \"\" {\n\t\tlog.Fatal().Msgf(\"missing -dst %q, -v %q or ts-parser %q\", *dst, *versionStr, *tsParserRepo)\n\t}\n\n\tif (*versionStr)[0] != 'v' {\n\t\tlog.Fatal().Msg(\"version must start with 'v'\")\n\t}\n\tswitch version.ChannelFor(*versionStr) {\n\tcase version.GA, version.Beta, version.Nightly, version.DevBuild:\n\t\t// no-op\n\tdefault:\n\t\tlog.Fatal().Msgf(\"unknown version channel for %s\", *versionStr)\n\t}\n\n\troot, err := os.Getwd()\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get working directory\")\n\t} else if _, err := os.Stat(join(root, \"go.mod\")); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"expected to run make-release.go from encr.dev repository root\")\n\t}\n\n\t*dst, err = filepath.Abs(*dst)\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to get absolute path to destination\")\n\t}\n\n\t// Prepare the target directory.\n\tif err := os.RemoveAll(*dst); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to remove existing target dir\")\n\t} else if err := os.MkdirAll(filepath.Join(*dst, \"artifacts\"), 0755); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to create target dir\")\n\t}\n\n\tjsBuilder := &JSPackager{\n\t\tWorkspaceRoot:    join(root, \"runtimes\", \"js\"),\n\t\tVersion:          *versionStr,\n\t\tlog:              log.Logger.With().Str(\"builder\", \"js\").Logger(),\n\t\tcompileCompleted: make(chan struct{}),\n\t}\n\n\t// Create all the builders\n\tbuilders := []*DistBuilder{\n\t\t{OS: \"darwin\", Arch: \"amd64\"},\n\t\t{OS: \"darwin\", Arch: \"arm64\"},\n\t\t{OS: \"linux\", Arch: \"amd64\"},\n\t\t{OS: \"linux\", Arch: \"arm64\"},\n\t\t{OS: \"windows\", Arch: \"amd64\"},\n\t}\n\tparralelFuncs := make([]func() error, 1, len(builders)+1)\n\tparralelFuncs[0] = jsBuilder.Package\n\n\t// Give them the common settings\n\tfor _, b := range builders {\n\t\tif *onlyBuild != \"\" && !(*onlyBuild == fmt.Sprintf(\"%s-%s\", b.OS, b.Arch) ||\n\t\t\t*onlyBuild == b.OS ||\n\t\t\t*onlyBuild == b.Arch) {\n\t\t\tcontinue\n\t\t}\n\t\tb.TSParserPath = *tsParserRepo\n\t\tb.DistBuildDir = join(*dst, b.OS+\"_\"+b.Arch)\n\t\tb.ArtifactsTarFile = join(*dst, \"artifacts\", \"encore-\"+*versionStr+\"-\"+b.OS+\"_\"+b.Arch+\".tar.gz\")\n\t\tb.Version = *versionStr\n\t\tb.jsBuilder = jsBuilder\n\n\t\tparralelFuncs = append(parralelFuncs, b.Build)\n\t}\n\n\tif err := runParallel(parralelFuncs...); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"failed to build all distributions\")\n\t}\n\tlog.Info().Msg(\"all distributions built successfully\")\n}\n"
  },
  {
    "path": "pkg/make-release/utils.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tosPkg \"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/cockroachdb/errors\"\n)\n\ntype Release struct {\n\tVersion  string // The version of the release\n\tFilename string // The filename of the release (inc extension)\n\tFileExt  string // The file extension\n\tURL      string // The URL to download the release from\n\tChecksum []byte // The checksum of the release\n}\n\n// getGithubRelease fetches the latest release from Github for the given org and repo\nfunc getGithubRelease(org string, repo string, os string, arch string) (*Release, error) {\n\trtn := &Release{}\n\n\ttype GithubRelease struct {\n\t\tTagName string `json:\"tag_name\"`\n\t\tAssets  []struct {\n\t\t\tName               string `json:\"name\"`\n\t\t\tBrowserDownloadURL string `json:\"browser_download_url\"`\n\t\t} `json:\"assets\"`\n\t}\n\n\t// Download the latest releases\n\treleasesResp, err := http.Get(fmt.Sprintf(\"https://api.github.com/repos/%s/%s/releases/latest\", org, repo))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to fetch latest release information\")\n\t}\n\tdefer func() { _ = releasesResp.Body.Close() }()\n\n\tif releasesResp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Newf(\"Unexpected response status code: %s\", releasesResp.Status)\n\t}\n\n\treleases := &GithubRelease{}\n\tif err := json.NewDecoder(releasesResp.Body).Decode(releases); err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to decode Github releases\")\n\t}\n\n\trtn.Version = releases.TagName\n\n\t// Build a list of possible file names\n\tosOptions := []string{os}\n\tif os == \"darwin\" {\n\t\tosOptions = append(osOptions, \"macos\")\n\t}\n\tarchOptions := []string{arch}\n\tif arch == \"amd64\" {\n\t\tarchOptions = append(archOptions, \"x86_64\", \"x86-64\")\n\t} else if arch == \"arm64\" {\n\t\tarchOptions = append(archOptions, \"aarch64\")\n\t}\n\textOptions := []string{\"tar.gz\"} // , \"zip\"}\n\n\tvar fileNameOptions []string\n\tfor _, osOption := range osOptions {\n\t\tfor _, archOption := range archOptions {\n\t\t\tfor _, extOption := range extOptions {\n\t\t\t\tfileNameOptions = append(fileNameOptions,\n\t\t\t\t\tfmt.Sprintf(\"%s_%s.%s\", osOption, archOption, extOption),\n\t\t\t\t\tfmt.Sprintf(\"%s-%s.%s\", osOption, archOption, extOption),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Find the checksum file\n\tchecksumFileURL := \"\"\n\tfor _, asset := range releases.Assets {\n\t\t// We want to know the checksum URL\n\t\tif strings.EqualFold(asset.Name, \"checksums.txt\") {\n\t\t\tchecksumFileURL = asset.BrowserDownloadURL\n\n\t\t}\n\n\t\tfor _, filenameOption := range fileNameOptions {\n\t\t\tif strings.EqualFold(asset.Name, filenameOption) {\n\t\t\t\t// We also want to know the asset name and download URL\n\t\t\t\trtn.Filename = asset.Name\n\t\t\t\trtn.URL = asset.BrowserDownloadURL\n\n\t\t\t\tif strings.HasSuffix(asset.Name, \".tar.gz\") {\n\t\t\t\t\trtn.FileExt = \".tar.gz\"\n\t\t\t\t} else {\n\t\t\t\t\trtn.FileExt = filepath.Ext(asset.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif checksumFileURL == \"\" {\n\t\treturn nil, errors.New(\"unable to find checksum file in Github release\")\n\t}\n\tif rtn.URL == \"\" || rtn.Filename == \"\" {\n\t\treturn nil, errors.New(\"unable to find binary in Github release\")\n\t}\n\n\t// Download the checksum file\n\tchecksumResp, err := http.Get(checksumFileURL)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to fetch checksum file\")\n\t}\n\tdefer func() { _ = checksumResp.Body.Close() }()\n\tif checksumResp.StatusCode != http.StatusOK {\n\t\treturn nil, errors.Newf(\"Unexpected response status code for checksum file: %s\", checksumResp.Status)\n\t}\n\n\t// Read the checksum file line by line\n\tscanner := bufio.NewScanner(checksumResp.Body)\n\tfor scanner.Scan() {\n\t\tline := strings.TrimSpace(scanner.Text())\n\t\tif strings.HasSuffix(line, rtn.Filename) {\n\t\t\tchecksumStr := strings.Split(line, \" \")[0]\n\n\t\t\tchecksum, err := hex.DecodeString(checksumStr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"unable to decode checksum\")\n\t\t\t}\n\t\t\trtn.Checksum = checksum\n\n\t\t\treturn rtn, nil\n\t\t}\n\t}\n\treturn nil, errors.New(\"unable to find checksum for asset file in checksum file\")\n}\n\nfunc downloadLatestGithubRelease(org, repo, os, arch string) (pathToFile string, rtnErr error) {\n\t// Find the latest release\n\trelease, err := getGithubRelease(org, repo, os, arch)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Create a cache dir for the download cache for this specific OS and architecture pair\n\tcacheDir, err := osPkg.UserCacheDir()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"user cache dir\")\n\t}\n\n\tpath := filepath.Join(cacheDir, \"encore-build-cache\", \"github-releases\", org, repo, os, arch)\n\terr = osPkg.MkdirAll(path, 0755)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"failed to make cache dir\")\n\t}\n\n\tdownloadFileName := fmt.Sprintf(\"%s-%s%s\", release.Version, hex.EncodeToString(release.Checksum), release.FileExt)\n\tdownloadPath := filepath.Join(path, downloadFileName)\n\n\t// Check if the file already exists\n\tif _, err := osPkg.Stat(downloadPath); err == nil {\n\t\treturn downloadPath, nil\n\t} else if !osPkg.IsNotExist(err) {\n\t\treturn \"\", errors.Wrap(err, \"failed to stat existing download file\")\n\t}\n\n\t// Now download the file\n\tdownloadResp, err := http.Get(release.URL)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"unable to fetch release file\")\n\t}\n\tdefer func() { _ = downloadResp.Body.Close() }()\n\tif downloadResp.StatusCode != http.StatusOK {\n\t\treturn \"\", errors.Newf(\"Unexpected response status code for release file: %s\", downloadResp.Status)\n\t}\n\n\t// Create the file\n\tdownloadFile, err := osPkg.Create(downloadPath)\n\tdefer func() {\n\t\t_ = downloadFile.Close()\n\t\tif rtnErr != nil {\n\t\t\t_ = osPkg.Remove(downloadPath) // delete any partially written file\n\t\t}\n\t}()\n\t_, err = io.Copy(downloadFile, downloadResp.Body)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"unable to download release file\")\n\t}\n\n\t// Now checksum the file\n\tif _, err := downloadFile.Seek(0, 0); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"unable to seek to start of release file\")\n\t}\n\n\tchecksum, err := checksumFile(downloadFile)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"unable to checksum release file\")\n\t}\n\n\t// Check the checksum\n\tif !bytes.Equal(checksum, release.Checksum) {\n\t\treturn \"\", errors.Newf(\"checksum of downloaded file (%q) does not match expected checksum (%q)\", hex.EncodeToString(checksum), hex.EncodeToString(release.Checksum))\n\t}\n\n\treturn downloadPath, nil\n}\n\nfunc checksumFile(file *osPkg.File) ([]byte, error) {\n\thash := sha256.New()\n\tif _, err := io.Copy(hash, file); err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to checksum file\")\n\t}\n\treturn hash.Sum(nil), nil\n}\n\nfunc extractArchive(pathToArchive string, targetDir string) error {\n\t// Create the target dir\n\tif err := osPkg.MkdirAll(targetDir, 0755); err != nil {\n\t\treturn errors.Wrap(err, \"failed to create target dir\")\n\t}\n\n\t// Extract the archive\n\tcmd := exec.Command(\"tar\", \"-xzf\", pathToArchive, \"--strip-components\", \"1\", \"-C\", targetDir)\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to extract archive: %s\", out)\n\t}\n\n\treturn nil\n}\n\nfunc TarGzip(srcDirectory string, tarFile string) error {\n\t// Create the tar.gz file from the src directory\n\tcmd := exec.Command(\"tar\", \"-czf\", tarFile, \"-C\", srcDirectory, \".\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to create tar.gz: %s\", out)\n\t}\n\treturn nil\n}\n\nfunc copyDir(src, dst string) error {\n\tcmd := exec.Command(\"cp\", \"-r\", src+\"/\", dst)\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn errors.Wrapf(err, \"failed to copy dir: %s\", out)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/make-release/windows/.gitignore",
    "content": "/.deps"
  },
  {
    "path": "pkg/make-release/windows/build.bat",
    "content": "@echo off\nrem SPDX-License-Identifier: MIT\nrem Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.\n\nsetlocal enableextensions enabledelayedexpansion\nset BUILDDIR=%~dp0\nset ROOT=%BUILDDIR%\\..\\..\\..\nset DST=%ROOT%\\dist\\windows_amd64\nset PATH=%BUILDDIR%.deps\\llvm-mingw\\bin;%BUILDDIR%.deps;%PATH%\nset PATHEXT=.EXE;.CMD\n\nif \"%ENCORE_VERSION%\" == \"\" (\n\techo ENCORE_VERSION not set\n\texit /b 1\n)\n\nif \"%ENCORE_GOROOT%\" == \"\" (\n\techo ENCORE_GOROOT not set\n\texit /b 1\n)\n\n:: Get absolute path\ncd %ENCORE_GOROOT% || exit /b 1\nset ENCORE_GOROOT=%CD%\n\ncd /d %BUILDDIR% || exit /b 1\n\nif exist .deps\\prepared goto :build\n:installdeps\n\trmdir /s /q .deps 2> NUL\n\tmkdir .deps || goto :error\n\tcd .deps || goto :error\n\tcall :download llvm-mingw-msvcrt.zip https://download.wireguard.com/windows-toolchain/distfiles/llvm-mingw-20201020-msvcrt-x86_64.zip 2e46593245090df96d15e360e092f0b62b97e93866e0162dca7f93b16722b844 || goto :error\n\tcall :download wintun.zip https://www.wintun.net/builds/wintun-0.10.2.zip fcd9f62f1bd5a550fcb9c21fbb5d6a556214753ccbbd1a3ebad4d318ec9dcbef || goto :error\n\tcall :download wix-binaries.zip https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip 2c1888d5d1dba377fc7fa14444cf556963747ff9a0a289a3599cf09da03b9e2e || goto :error\n\tcopy /y NUL prepared > NUL || goto :error\n\tcd .. || goto :error\n\n:build\n\tset GOOS=windows\n\tcall :build_plat amd64 x86_64 amd64 || goto :error\n\tcall :copy_artifacts || goto :error\n\n:success\n\techo [+] Success!\n\texit /b 0\n\n:download\n\techo [+] Downloading %1\n\tcurl -#fLo %1 %2 || exit /b 1\n\techo [+] Verifying %1\n\tfor /f %%a in ('CertUtil -hashfile %1 SHA256 ^| findstr /r \"^[0-9a-f]*$\"') do if not \"%%a\"==\"%~3\" exit /b 1\n\techo [+] Extracting %1\n\ttar -xf %1 %~4 || exit /b 1\n\techo [+] Cleaning up %1\n\tdel %1 || exit /b 1\n\tgoto :eof\n\n:build_plat\n\trmdir /S /Q \"%DST%\"\n\tmkdir %DST%\\bin >NUL 2>&1\n\techo [+] Assembling resources\n\tx86_64-w64-mingw32-windres -I \".deps\\wintun\\bin\\amd64\" -i resources.rc -o \"%ROOT%\\cli\\cmd\\encore\\resources_amd64.syso\" -O coff -c 65001 || exit /b %errorlevel%\n\tset GOARCH=amd64\n\techo [+] Building\n\tgo build -tags load_wintun_from_rsrc -ldflags \"-X 'encr.dev/internal/version.Version=v%ENCORE_VERSION%'\" -o \"%DST%\\bin\\encore.exe\" \"%ROOT%\\cli\\cmd\\encore\" || exit /b 1\n\tgo build -trimpath -o \"%DST%\\bin\\git-remote-encore.exe\" \"%ROOT%\\cli\\cmd\\git-remote-encore\" || exit /b 1\n\tgoto :eof\n\n:copy_artifacts\n\techo [+] Copying files\n\txcopy /S /I /E /H /Q \"%ENCORE_GOROOT%\" \"%DST%\\encore-go\" || exit /b 1\n\txcopy /S /I /E /H /Q \"%ROOT%\\runtimes\\go\" \"%DST%\\runtimes\\go\" || exit /b 1\n\tgoto :eof\n\n:error\n\techo [-] Failed with error #%errorlevel%.\n\tcmd /c exit %errorlevel%\n"
  },
  {
    "path": "pkg/make-release/windows/manifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n    <assemblyIdentity version=\"1.0.0.0\" processorArchitecture=\"*\" name=\"encore\" type=\"win32\" />\n    <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n        <application>\n            <!-- Windows 10 -->\n            <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />\n            <!-- Windows 8.1 -->\n            <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />\n            <!-- Windows 8 -->\n            <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />\n            <!-- Windows 7 -->\n            <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />\n        </application>\n    </compatibility>\n    <dependency>\n        <dependentAssembly>\n            <assemblyIdentity type=\"win32\" name=\"Microsoft.Windows.Common-Controls\" version=\"6.0.0.0\" processorArchitecture=\"*\" publicKeyToken=\"6595b64144ccf1df\" language=\"*\" />\n        </dependentAssembly>\n    </dependency>\n    <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <windowsSettings>\n            <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2, PerMonitor</dpiAwareness>\n            <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">True</dpiAware>\n        </windowsSettings>\n    </application>\n</assembly>"
  },
  {
    "path": "pkg/make-release/windows/resources.rc",
    "content": "/* SPDX-License-Identifier: MIT\n *\n * Copyright (C) 2019-2021 WireGuard LLC. All Rights Reserved.\n */\n\n#pragma code_page(65001) // UTF-8\nCREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml\nwintun.dll RCDATA wintun.dll"
  },
  {
    "path": "pkg/metascrub/metascrub.go",
    "content": "// Package metascrub computes scrub paths for a metadata type.\npackage metascrub\n\nimport (\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n)\n\n// New constructs a new Computer for the given metadata.\nfunc New(md *meta.Data, log zerolog.Logger) *Computer {\n\treturn &Computer{\n\t\tmd:        md,\n\t\tlog:       log.With().Str(\"component\", \"metascrub\").Logger(),\n\t\tdeclCache: make(map[declCacheKey]declResult),\n\t}\n}\n\n// Computer computes scrub paths for types, caching the computation.\n// It can safely be reused across multiple types for the same metadata.\n// It is not safe for concurrent use.\ntype Computer struct {\n\tmd  *meta.Data\n\tlog zerolog.Logger\n\n\t// declCache caches the scrub paths for encountered declarations\n\tdeclCache map[declCacheKey]declResult\n}\n\ntype Desc struct {\n\tPayload []scrub.Path\n\tHeaders []string\n}\n\ntype ParseMode int\n\nconst (\n\t// AuthHandler specifies that the type is an auth handler.\n\tAuthHandler ParseMode = 1 << iota\n)\n\n// Compute computes the scrub paths for the given typ.\n// It is not safe for concurrent use.\nfunc (c *Computer) Compute(typ *schema.Type, mode ParseMode) Desc {\n\tif typ == nil {\n\t\treturn Desc{}\n\t}\n\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tc.log.Error().Stack().Msgf(\"metascrub.Compute panicked: %v\", err)\n\t\t}\n\t}()\n\n\tp := &typeParser{c: c, mode: mode}\n\tres := p.typ(typ)\n\n\t// Did we exceed the steps?\n\tif p.steps > maxSteps {\n\t\tc.log.Error().Msg(\"metascrub.Compute aborted due to exceeding max steps\")\n\t}\n\n\treturn Desc{\n\t\tPayload: res.scrub,\n\t\tHeaders: res.headers,\n\t}\n}\n\nconst maxSteps = 10000\n\ntype typeParser struct {\n\tc    *Computer\n\tmode ParseMode\n\n\t// steps is a fail-safe to catch any potential infinite loops.\n\tsteps int\n}\n\ntype declCacheKey struct {\n\tid   uint32\n\tmode ParseMode\n}\n\nfunc (p *typeParser) decl(id uint32) declResult {\n\tkey := declCacheKey{id, p.mode}\n\tif res, ok := p.c.declCache[key]; ok {\n\t\treturn res\n\t}\n\n\t// Mark that we're beginning to process this decl,\n\t// so we avoid infinite recursion.\n\tp.c.declCache[key] = declResult{}\n\n\t// Do the actual parsing.\n\tdecl := p.c.md.Decls[id]\n\tres := p.typ(decl.Type)\n\n\t// We're done, cache the final result.\n\tp.c.declCache[key] = res\n\treturn res\n}\n\ntype declResult struct {\n\tscrub      []scrub.Path\n\ttypeParams []typeParamPath\n\theaders    []string\n}\n\nfunc (p *typeParser) typ(typ *schema.Type) declResult {\n\tif typ == nil {\n\t\treturn declResult{}\n\t}\n\tp.steps++\n\tif p.steps > maxSteps {\n\t\treturn declResult{}\n\t}\n\n\tswitch t := typ.Typ.(type) {\n\tcase *schema.Type_Named:\n\t\tdecl := p.decl(t.Named.Id)\n\t\tout := declResult{\n\t\t\t// Clone the paths since we're modifying them.\n\t\t\tscrub: slices.Clone(decl.scrub),\n\n\t\t\t// Copy the headers directly since they never get modified,\n\t\t\t// since we only care about the top-level type's headers.\n\t\t\theaders: decl.headers,\n\t\t}\n\n\t\tfor i, arg := range t.Named.TypeArguments {\n\t\t\targRes := p.typ(arg)\n\t\t\tif len(argRes.scrub) == 0 && len(argRes.typeParams) == 0 {\n\t\t\t\t// Nothing to do\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// For every type parameter, find the places\n\t\t\t// where it's used and copy it to the combined result.\n\t\t\tfor _, declParam := range decl.typeParams {\n\t\t\t\tif declParam.paramIdx == uint32(i) {\n\t\t\t\t\tfor _, s := range argRes.scrub {\n\t\t\t\t\t\tpath := append(slices.Clone(declParam.p), s...)\n\t\t\t\t\t\tout.scrub = append(out.scrub, path)\n\t\t\t\t\t}\n\t\t\t\t\tfor _, s := range argRes.typeParams {\n\t\t\t\t\t\tpath := append(slices.Clone(declParam.p), s.p...)\n\t\t\t\t\t\tout.typeParams = append(out.typeParams, typeParamPath{\n\t\t\t\t\t\t\tp:        path,\n\t\t\t\t\t\t\tparamIdx: s.paramIdx,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t\treturn out\n\n\tcase *schema.Type_Pointer:\n\t\treturn p.typ(t.Pointer.Base)\n\n\tcase *schema.Type_Option:\n\t\treturn p.typ(t.Option.Value)\n\n\tcase *schema.Type_List:\n\t\treturn p.typ(t.List.Elem)\n\n\tcase *schema.Type_Union:\n\t\tvar results []declResult\n\t\tvar numScrub, numTypeParams, numHeaders int\n\t\tfor _, typ := range t.Union.Types {\n\t\t\tres := p.typ(typ)\n\t\t\tresults = append(results, res)\n\t\t\tnumScrub += len(res.scrub)\n\t\t\tnumTypeParams += len(res.typeParams)\n\t\t\tnumHeaders += len(res.headers)\n\t\t}\n\t\tcombined := declResult{\n\t\t\tscrub:      make([]scrub.Path, 0, numScrub),\n\t\t\ttypeParams: make([]typeParamPath, 0, numTypeParams),\n\t\t\theaders:    make([]string, 0, numHeaders),\n\t\t}\n\t\tfor _, res := range results {\n\t\t\tcombined.scrub = append(combined.scrub, res.scrub...)\n\t\t\tcombined.typeParams = append(combined.typeParams, res.typeParams...)\n\t\t\tcombined.headers = append(combined.headers, res.headers...)\n\t\t}\n\t\treturn combined\n\n\tcase *schema.Type_Map:\n\t\tkey := p.typ(t.Map.Key)\n\t\tval := p.typ(t.Map.Value)\n\n\t\tcombined := declResult{\n\t\t\tscrub:      make([]scrub.Path, 0, len(key.scrub)+len(val.scrub)),\n\t\t\ttypeParams: make([]typeParamPath, 0, len(key.typeParams)+len(val.typeParams)),\n\t\t}\n\t\tfor i, res := range [...]declResult{key, val} {\n\t\t\tkind := scrub.MapKey\n\t\t\tif i == 1 {\n\t\t\t\tkind = scrub.MapValue\n\t\t\t}\n\t\t\tfor _, e := range res.scrub {\n\t\t\t\tpath := append(scrub.Path{{Kind: kind}}, e...)\n\t\t\t\tcombined.scrub = append(combined.scrub, path)\n\t\t\t}\n\t\t\tfor _, e := range res.typeParams {\n\t\t\t\tpath := append(scrub.Path{{Kind: kind}}, e.p...)\n\t\t\t\tcombined.typeParams = append(combined.typeParams, typeParamPath{p: path, paramIdx: e.paramIdx})\n\t\t\t}\n\t\t}\n\t\treturn combined\n\n\tcase *schema.Type_Struct:\n\t\tvar out declResult\n\t\tfor _, f := range t.Struct.Fields {\n\t\t\tsensitive, fieldName, caseSensitive := isSensitive(f)\n\n\t\t\t// For Auth Handlers everything is sensitive.\n\t\t\tif (p.mode & AuthHandler) != 0 {\n\t\t\t\tsensitive = true\n\t\t\t}\n\n\t\t\tif sensitive {\n\t\t\t\t// If the field is sensitive add it directly.\n\t\t\t\tout.scrub = append(out.scrub, scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}})\n\n\t\t\t\t// If this is a header field, add it to the headers to scrub.\n\t\t\t\tif headerName, ok := isHeader(f); ok {\n\t\t\t\t\tout.headers = append(out.headers, headerName)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Otherwise check the type and see if there's anything to scrub within it.\n\t\t\t\tfieldRes := p.typ(f.Typ)\n\t\t\t\tfor _, e := range fieldRes.scrub {\n\t\t\t\t\tpath := append(scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}, e...)\n\t\t\t\t\tout.scrub = append(out.scrub, path)\n\t\t\t\t}\n\t\t\t\tfor _, e := range fieldRes.typeParams {\n\t\t\t\t\tpath := append(scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}, e.p...)\n\t\t\t\t\tout.typeParams = append(out.typeParams, typeParamPath{p: path, paramIdx: e.paramIdx})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn out\n\n\tcase *schema.Type_Builtin, *schema.Type_Literal:\n\t\t// Nothing to do\n\t\treturn declResult{}\n\n\tcase *schema.Type_TypeParameter:\n\t\treturn declResult{typeParams: []typeParamPath{{\n\t\t\tparamIdx: t.TypeParameter.ParamIdx,\n\t\t}}}\n\n\tdefault:\n\t\tp.c.log.Warn().Msgf(\"got unexpected schema.Type %T in metascrub, skipping\", t)\n\t\treturn declResult{}\n\t}\n}\n\nfunc isSensitive(f *schema.Field) (sensitive bool, fieldName string, caseSensitive bool) {\n\tfieldName = f.Name\n\tif f.JsonName != \"\" {\n\t\tfieldName = f.JsonName\n\t\tcaseSensitive = true\n\t}\n\tfieldName = strconv.Quote(fieldName) // the scrub package wants exact byte matches\n\n\tfor _, t := range f.Tags {\n\t\tif t.Key == \"encore\" {\n\t\t\tsensitive = t.Name == \"sensitive\" || slices.Contains(t.Options, \"sensitive\")\n\t\t\tbreak\n\t\t}\n\t}\n\treturn sensitive, fieldName, caseSensitive\n}\n\nfunc isHeader(f *schema.Field) (headerName string, ok bool) {\n\tfor _, t := range f.Tags {\n\t\tif t.Key == \"header\" {\n\t\t\tname := t.Name\n\t\t\tif name == \"\" {\n\t\t\t\tname = f.Name\n\t\t\t}\n\t\t\treturn name, true\n\t\t}\n\t}\n\treturn \"\", false\n}\n\ntype typeParamPath struct {\n\tp        scrub.Path\n\tparamIdx uint32\n}\n"
  },
  {
    "path": "pkg/metascrub/metascrub_test.go",
    "content": "package metascrub\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"encr.dev/cli/daemon/apps\"\n\t\"encr.dev/pkg/builder\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\t\"encr.dev/v2/v2builder\"\n)\n\nfunc TestScrub(t *testing.T) {\n\tc := qt.New(t)\n\tmd := testParse(c, `\n-- svc/svc.go --\npackage svc\nimport (\n\t\"context\"\n\t\"encore.dev/types/option\"\n)\n\ntype Params struct {\n\tFoo string SCRUB\n\n\tNestedScrub struct {\n\t\tInner string SCRUB\n\t} SCRUB\n\n\tNested struct {\n\t\tInner string SCRUB\n\t}\n\n\tList []string SCRUB\n\tListInner []struct { Inner string SCRUB }\n\n\tRecurseScrub *Params SCRUB\n\t// Ideally this should yield nested scrubs but we don't support that yet.\n\tRecurse *Params\n\n\tGen      Generic[string]\n\tGenScrub Generic[string] SCRUB\n\tGenInner Generic[Bar]\n\tMulti    NestedGeneric[string, Bar]\n\n\tOption option.Option[Generic[string]]\n\tOptionScrub option.Option[Generic[string]] SCRUB\n\n\tMapOne map[Generic[Bar]]NestedGeneric[string, Bar]\n\tMapTwo GenericMap[Bar, NestedGeneric[string, Bar]]\n\n\tJsonKey string `+\"`\"+`json:\"json_key\" encore:\"sensitive\"`+\"`\"+`\n\tHeader string `+\"`\"+`header:\"X-Header\" encore:\"sensitive\"`+\"`\"+`\n}\n\ntype Generic[T any] struct {\n\tFoo T\n\tFooScrub T SCRUB\n}\n\ntype NestedGeneric[A any, B any] struct {\n\tOne NestedGenericTwo[B, string]\n\tTwo B\n}\n\ntype NestedGenericTwo[A any, B any] struct {\n\tAlpha A\n\tBeta B\n}\n\ntype GenericMap[K comparable, V any] struct {\n\tFoo map[K]V\n}\n\ntype Bar struct {\n\tScrub string SCRUB\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n`)\n\n\tcmp := New(md, zerolog.New(os.Stdout))\n\trpc := md.Svcs[0].Rpcs[0]\n\tres := cmp.Compute(rpc.RequestSchema, 0)\n\n\tf := func(name string) scrub.PathEntry {\n\t\treturn scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name)}\n\t}\n\tfCase := func(name string) scrub.PathEntry {\n\t\treturn scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name), CaseSensitive: true}\n\t}\n\tmapKey := scrub.PathEntry{Kind: scrub.MapKey}\n\tmapVal := scrub.PathEntry{Kind: scrub.MapValue}\n\n\tc.Assert(res.Payload, qt.DeepEquals, []scrub.Path{\n\t\t{f(\"Foo\")},\n\t\t{f(\"NestedScrub\")},\n\t\t{f(\"Nested\"), f(\"Inner\")},\n\t\t{f(\"List\")},\n\t\t{f(\"ListInner\"), f(\"Inner\")},\n\t\t{f(\"RecurseScrub\")},\n\t\t{f(\"Gen\"), f(\"FooScrub\")},\n\t\t{f(\"GenScrub\")},\n\t\t{f(\"GenInner\"), f(\"FooScrub\")},\n\t\t{f(\"GenInner\"), f(\"Foo\"), f(\"Scrub\")},\n\t\t{f(\"Multi\"), f(\"One\"), f(\"Alpha\"), f(\"Scrub\")},\n\t\t{f(\"Multi\"), f(\"Two\"), f(\"Scrub\")},\n\n\t\t{f(\"Option\"), f(\"FooScrub\")},\n\t\t{f(\"OptionScrub\")},\n\n\t\t{f(\"MapOne\"), mapKey, f(\"FooScrub\")},\n\t\t{f(\"MapOne\"), mapKey, f(\"Foo\"), f(\"Scrub\")},\n\t\t{f(\"MapOne\"), mapVal, f(\"One\"), f(\"Alpha\"), f(\"Scrub\")},\n\t\t{f(\"MapOne\"), mapVal, f(\"Two\"), f(\"Scrub\")},\n\t\t{f(\"MapTwo\"), f(\"Foo\"), mapKey, f(\"Scrub\")},\n\t\t{f(\"MapTwo\"), f(\"Foo\"), mapVal, f(\"One\"), f(\"Alpha\"), f(\"Scrub\")},\n\t\t{f(\"MapTwo\"), f(\"Foo\"), mapVal, f(\"Two\"), f(\"Scrub\")},\n\t\t{fCase(\"json_key\")},\n\t\t{f(\"Header\")},\n\t})\n\n\tc.Assert(res.Headers, qt.DeepEquals, []string{\"X-Header\"})\n}\n\nfunc TestScrubAuthHandler(t *testing.T) {\n\tc := qt.New(t)\n\tmd := testParse(c, `\n-- svc/svc.go --\npackage svc\nimport \"context\"\n\ntype Params struct {\n\tHeader string `+\"`\"+`header:\"Foo\"`+\"`\"+`\n\tQuery string `+\"`\"+`query:\"query\"`+\"`\"+`\n\tOther string\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n`)\n\n\tcmp := New(md, zerolog.New(os.Stdout))\n\trpc := md.Svcs[0].Rpcs[0]\n\tres := cmp.Compute(rpc.RequestSchema, AuthHandler)\n\n\tf := func(name string) scrub.PathEntry {\n\t\treturn scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name)}\n\t}\n\n\tc.Assert(res.Payload, qt.DeepEquals, []scrub.Path{\n\t\t{f(\"Header\")},\n\t\t{f(\"Query\")},\n\t\t{f(\"Other\")},\n\t})\n\n\tc.Assert(res.Headers, qt.DeepEquals, []string{\"Foo\"})\n}\n\nfunc testParse(c *qt.C, code string) *meta.Data {\n\tcode = strings.ReplaceAll(code, \"SCRUB\", \"`encore:\\\"sensitive\\\"`\")\n\tar := txtar.Parse([]byte(code))\n\tar.Files = append(ar.Files, txtar.File{Name: \"go.mod\", Data: []byte(\"module test\\n\\nrequire (\\n\\tencore.dev v1.52.1\\n)\\n\")})\n\n\troot := c.TempDir()\n\terr := txtar.Write(ar, root)\n\tc.Assert(err, qt.IsNil)\n\n\tbld := v2builder.New()\n\tctx := context.Background()\n\n\tapp := apps.NewInstance(root, \"test\", \"\")\n\tprepareResult, err := bld.Prepare(ctx, builder.PrepareParams{\n\t\tBuild:      builder.DefaultBuildInfo(),\n\t\tApp:        app,\n\t\tWorkingDir: \".\",\n\t})\n\tc.Assert(err, qt.IsNil)\n\tres, err := bld.Parse(ctx, builder.ParseParams{\n\t\tBuild:       builder.DefaultBuildInfo(),\n\t\tApp:         app,\n\t\tExperiments: nil,\n\t\tWorkingDir:  \".\",\n\t\tParseTests:  false,\n\t\tPrepare:     prepareResult,\n\t})\n\tc.Assert(err, qt.IsNil)\n\treturn res.Meta\n}\n"
  },
  {
    "path": "pkg/namealloc/namealloc.go",
    "content": "package namealloc\n\nimport (\n\t\"go/token\"\n\t\"strconv\"\n)\n\n// Allocator helps choosing names without collisions\n// and without using Go keywords. The zero value is ready\n// to be used.\ntype Allocator struct {\n\t// Reserved decides whether a given input is reserved.\n\t// If nil it defaults to token.IsKeyword.\n\tReserved func(input string) bool\n\n\tused map[string]bool\n}\n\n// Get allocates a name that is a valid, unused identifier\n// based on the input string. It ensures the same name is not\n// returned multiple times even for the same input.\nfunc (a *Allocator) Get(input string) (name string) {\n\tif a.isReserved(input) {\n\t\tinput = input + \"_\"\n\t}\n\n\tcandidate := input\n\tfor i := 2; a.used[candidate]; i++ {\n\t\tcandidate = input + strconv.Itoa(i)\n\t}\n\n\tif a.used == nil {\n\t\ta.used = make(map[string]bool)\n\t}\n\ta.used[candidate] = true\n\treturn candidate\n}\n\nfunc (a *Allocator) isReserved(input string) bool {\n\treserved := a.Reserved\n\tif reserved == nil {\n\t\treserved = token.IsKeyword\n\t}\n\treturn reserved(input)\n}\n"
  },
  {
    "path": "pkg/namealloc/namealloc_test.go",
    "content": "package namealloc\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestAlloc(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\treserved []string // reserved words\n\t\tin       []string\n\t\tout      []string\n\t}{\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tin:   []string{\"hello\", \"hello\", \"there\", \"hello\"},\n\t\t\tout:  []string{\"hello\", \"hello2\", \"there\", \"hello3\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"reserved\",\n\t\t\treserved: []string{\"hello\"},\n\t\t\tin:       []string{\"hello\", \"hello\", \"there\", \"hello\"},\n\t\t\tout:      []string{\"hello_\", \"hello_2\", \"there\", \"hello_3\"},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treserved := func(s string) bool {\n\t\t\t\tfor _, r := range tt.reserved {\n\t\t\t\t\tif r == s {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ta := &Allocator{Reserved: reserved}\n\n\t\t\tgot := make([]string, 0, len(tt.in))\n\t\t\tfor _, in := range tt.in {\n\t\t\t\tv := a.Get(in)\n\t\t\t\tgot = append(got, v)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.out) {\n\t\t\t\tt.Errorf(\"Alloc(%+v) = %+v, want %+v\", tt.in, got, tt.out)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/noopgateway/noopgateway.go",
    "content": "package noopgateway\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/julienschmidt/httprouter\"\n)\n\n// ServiceName is the name of a service.\ntype ServiceName string\n\n// Route defines a single route in the gateway.\ntype Route struct {\n\tMethods []string    // HTTP methods\n\tPath    string      // \"/path/:param/*wildcard\"\n\tDest    ServiceName // where to route the request\n\n\t// RequiresAuth specifies whether the route requires authentication.\n\tRequiresAuth bool\n}\n\n// Service describes how to route traffic to a service.\ntype Service struct {\n\t// URL is the URL to proxy traffic to.\n\tURL *url.URL\n}\n\n// AuthHandler describes the auth handler.\ntype AuthHandler struct {\n\t// Service is the service containing the auth handler.\n\tService ServiceName\n\tName    string // name of the auth handler\n\n\t// Parameters that the auth handler expects.\n\t// If any one of these are present in the request,\n\t// the auth handler is invoked.\n\tQuery  []Param\n\tHeader []Param\n\tCookie []Param\n}\n\n// Param describes a request parameter.\ntype Param struct {\n\t// WireFormat is the name of the parameter on the wire.\n\tWireFormat string\n\t// CaseSensitive specifies whether or not\n\t// the wire format is case sensitive.\n\tCaseSensitive bool\n}\n\n// Description describes a gateway.\ntype Description struct {\n\t// Routes are the routes to proxy.\n\tRoutes []*Route\n\n\t// Services defines how to proxy traffic to each service.\n\tServices map[ServiceName]Service\n\n\t// Auth describes the authentication handler, if any.\n\tAuth *AuthHandler\n}\n\n// New constructs a new gateway.\nfunc New(desc *Description) *Gateway {\n\t// TODO validate config:\n\t// route -> service mapping\n\t// requires auth -> auth handler present\n\n\tgw := &Gateway{\n\t\tRoundTripper: http.DefaultTransport,\n\t\tdesc:         desc,\n\t\trouteLookup:  newRouteLookuper(desc.Routes),\n\t}\n\n\t// Create our proxy.\n\tgw.proxy = &httputil.ReverseProxy{\n\t\tRewrite:      gw.handleRequest,\n\t\tTransport:    &errorCheckingRoundTripper{gw: gw},\n\t\tErrorHandler: gw.errorHandler,\n\t}\n\n\treturn gw\n}\n\n// Gateway implements a gateway that validates and authenticates incoming requests,\n// and forwards them to the appropriate service.\ntype Gateway struct {\n\t// Rewrite allows for rewriting the request before it is proxied.\n\t// If nil it does nothing.\n\tRewrite func(p *httputil.ProxyRequest)\n\n\t// RoundTripper is the http.RoundTripper to use. It defaults to http.DefaultTransport.\n\tRoundTripper http.RoundTripper\n\n\t// ExtraRoutes, if non-nil, specifies a router that will be consulted first.\n\t// Requests will be proxied to the backend only if the router has no matching path.\n\tExtraRoutes *mux.Router\n\n\tdesc *Description\n\n\t// routeLookup is the httprouter for resolving incoming requests\n\t// to the routes they match.\n\trouteLookup *httprouter.Router\n\n\t// proxy proxies requests to the right backend.\n\tproxy *httputil.ReverseProxy\n}\n\n// ServeHTTP implements http.Handler.\nfunc (g *Gateway) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\tif g.ExtraRoutes != nil {\n\t\tvar match mux.RouteMatch\n\t\tif g.ExtraRoutes.Match(req, &match) {\n\t\t\t// ExtraRoutes has a matching route. Use it.\n\t\t\tg.ExtraRoutes.ServeHTTP(w, req)\n\t\t\treturn\n\t\t}\n\t}\n\n\tg.proxy.ServeHTTP(w, req)\n}\n\n// handleRequest handles the request.\nfunc (g *Gateway) handleRequest(r *httputil.ProxyRequest) {\n\t// setErrResp sets an error response, by setting the\n\t// sentinel error.\n\tsetErrResp := func(err error) {\n\t\tout := &http.Request{\n\t\t\t// Assumed to be non-nil by httputil.\n\t\t\tHeader: make(http.Header),\n\t\t}\n\t\tctx := context.WithValue(context.Background(), errCtxKey, err)\n\t\tr.Out = out.WithContext(ctx)\n\t}\n\n\troute, ok := g.lookupRoute(r.In)\n\tif !ok {\n\t\tsetErrResp(errRouteNotFound)\n\t\treturn\n\t}\n\n\t// TODO perform authentication\n\n\tdest := g.desc.Services[route.Dest]\n\tr.SetURL(dest.URL)\n\n\tif g.Rewrite != nil {\n\t\tg.Rewrite(r)\n\t}\n\tr.Out.Header[\"X-Forwarded-For\"] = r.In.Header[\"X-Forwarded-For\"]\n\tr.SetXForwarded()\n}\n\n// lookupRoute looks up the route the request is for.\n// If no route is found, it reports (nil, false).\nfunc (g *Gateway) lookupRoute(req *http.Request) (route *Route, ok bool) {\n\thandle, _, tsr := g.routeLookup.Lookup(req.Method, req.URL.Path)\n\n\t// Handle trailing slash redirects.\n\tif tsr {\n\t\tpath := req.URL.Path\n\t\tif strings.HasSuffix(path, \"/\") {\n\t\t\tpath = path[:len(path)-1]\n\t\t} else {\n\t\t\tpath += \"/\"\n\t\t}\n\t\thandle, _, _ = g.routeLookup.Lookup(req.Method, path)\n\t}\n\n\tif handle == nil {\n\t\t// Route not found\n\t\treturn nil, false\n\t}\n\n\trw := &sentinelResponseWriter{}\n\thandle(rw, nil, httprouter.Params{})\n\treturn rw.route, true\n}\n\nfunc (g *Gateway) errorHandler(w http.ResponseWriter, req *http.Request, err error) {\n\t// TODO handle this properly\n\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n}\n\nvar (\n\terrRouteNotFound = errors.New(\"route not found\")\n)\n\n// sentinelResponseWriter is a sentinel value that implements\n// http.ResponseWriter as a way to get the Route registered for a given path.\ntype sentinelResponseWriter struct {\n\thttp.ResponseWriter // always nil; dummy value to implement http.ResponseWriter\n\t// route is the selected route (an output parameter).\n\troute *Route\n}\n\n// errorCheckingRoundTripper is a http.RoundTripper that\n// checks for specific error responses in the request context\n// and returns an error if they are found.\ntype errorCheckingRoundTripper struct {\n\tgw *Gateway\n}\n\nfunc (rt errorCheckingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {\n\tctx := req.Context()\n\tif err, ok := ctx.Value(errCtxKey).(error); ok && err != nil {\n\t\treturn nil, err\n\t}\n\treturn rt.gw.RoundTripper.RoundTrip(req)\n}\n\ntype ctxKey string\n\nconst errCtxKey ctxKey = \"error\"\n\nfunc newRouteLookuper(routes []*Route) *httprouter.Router {\n\tr := httprouter.New()\n\tfor _, route := range routes {\n\t\troute := route // for the closure below\n\t\tfor _, m := range route.Methods {\n\t\t\tr.Handle(m, route.Path, func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {\n\t\t\t\tw.(*sentinelResponseWriter).route = route\n\t\t\t})\n\t\t}\n\t}\n\treturn r\n}\n\n// authInfo represents extracted auth information from a request.\ntype authInfo struct {\n}\n\n// extractAuthInfo extracts the auth information from the request.\n// If there is no auth information (or the gateway has no auth handler)\n// it reports nil.\nfunc (g *Gateway) extractAuthInfo(req *http.Request) *authInfo {\n\tauth := g.desc.Auth\n\tif auth == nil {\n\t\treturn nil\n\t}\n\n\t// TODO check Query, Header, Cookie etc\n\t// TODO check legacy bearer token auth\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/noopgateway/retry_dialer.go",
    "content": "package noopgateway\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n)\n\n// RetryDialer wraps another dialer and adds exponential backoff\n// in the case of connection refused errors.\ntype RetryDialer struct {\n\tnet.Dialer\n\tMaxBackoff time.Duration\n}\n\nfunc (d *RetryDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {\n\tvar conn net.Conn\n\tb := backoff.NewExponentialBackOff()\n\n\tb.MaxElapsedTime = d.MaxBackoff\n\tif b.MaxElapsedTime == 0 {\n\t\tb.MaxElapsedTime = time.Minute // Set maximum backoff time to 1 minute\n\t}\n\n\toperation := func() error {\n\t\tvar err error\n\t\tconn, err = d.Dialer.DialContext(ctx, network, address)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"connection refused\") {\n\t\t\t\t// Retry if connection is refused\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Don't retry if connection isn't refused\n\t\t\treturn backoff.Permanent(err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr := backoff.Retry(operation, b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn conn, nil\n}\n"
  },
  {
    "path": "pkg/noopgwdesc/gateway.go",
    "content": "package noopgwdesc\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/noopgateway\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\n// Describe computes a Description based on the given metadata and service discovery configuration.\n// If serviceDiscovery is nil, the routes will be added but the service discovery setup will be empty.\nfunc Describe(md *meta.Data, serviceDiscovery map[noopgateway.ServiceName]string) *noopgateway.Description {\n\tdesc := &noopgateway.Description{\n\t\tServices: make(map[noopgateway.ServiceName]noopgateway.Service),\n\t}\n\n\tfor _, svc := range md.Svcs {\n\t\tsvcName := noopgateway.ServiceName(svc.Name)\n\n\t\tif serviceDiscovery != nil {\n\t\t\thost, ok := serviceDiscovery[svcName]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttarget := &url.URL{\n\t\t\t\tScheme: \"http\",\n\t\t\t\tHost:   host,\n\t\t\t}\n\t\t\tdesc.Services[svcName] = noopgateway.Service{\n\t\t\t\tURL: target,\n\t\t\t}\n\t\t}\n\n\t\tfor _, ep := range svc.Rpcs {\n\t\t\tmethods := ep.HttpMethods\n\t\t\tif slices.Contains(methods, \"*\") {\n\t\t\t\tmethods = []string{\"GET\", \"HEAD\", \"POST\", \"PUT\", \"DELETE\", \"CONNECT\", \"OPTIONS\", \"TRACE\", \"PATCH\"}\n\t\t\t}\n\t\t\tdesc.Routes = append(desc.Routes, &noopgateway.Route{\n\t\t\t\tMethods:      methods,\n\t\t\t\tDest:         svcName,\n\t\t\t\tRequiresAuth: ep.AccessType == meta.RPC_AUTH,\n\t\t\t\tPath:         pathToString(ep.Path),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn desc\n}\n\nfunc pathToString(path *meta.Path) string {\n\tparts := make([]string, 0, len(path.Segments))\n\tparamIdx := 0\n\tfor _, seg := range path.Segments {\n\t\tvar val string\n\t\tswitch seg.Type {\n\t\tcase meta.PathSegment_LITERAL:\n\t\t\tval = seg.Value\n\t\tcase meta.PathSegment_PARAM:\n\t\t\tval = fmt.Sprintf(\":p%d\", paramIdx)\n\t\t\tparamIdx++\n\t\tcase meta.PathSegment_WILDCARD, meta.PathSegment_FALLBACK:\n\t\t\tval = fmt.Sprintf(\"*p%d\", paramIdx)\n\t\t\tparamIdx++\n\t\t}\n\t\tparts = append(parts, val)\n\t}\n\treturn \"/\" + strings.Join(parts, \"/\")\n}\n"
  },
  {
    "path": "pkg/option/option.go",
    "content": "package option\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/google/go-cmp/cmp\"\n)\n\n// Option is a type that represents a value that may or may not be present\n//\n// It is different a normal value as it can distinguish between a zero value and a missing value\n// even on pointer types\ntype Option[T any] struct {\n\tvalue   T\n\tpresent bool\n}\n\nfunc (o *Option[T]) MarshalJSON() ([]byte, error) {\n\tif !o.present {\n\t\treturn []byte(\"null\"), nil\n\t}\n\treturn json.Marshal(o.value)\n}\n\nfunc (o *Option[T]) UnmarshalJSON(data []byte) error {\n\tif string(data) == \"null\" {\n\t\to.present = false\n\t\treturn nil\n\t}\n\n\to.present = true\n\treturn json.Unmarshal(data, &o.value)\n}\n\n// CmpOpts returns the options to use to compare options\n// by checking the unexported fields. For testing purposes.\nfunc CmpOpts() []cmp.Option {\n\treturn []cmp.Option{\n\t\tcmp.Exporter(func(rt reflect.Type) bool {\n\t\t\treturn rt.PkgPath() == \"encr.dev/pkg/option\" && strings.HasPrefix(rt.Name(), \"Option[\")\n\t\t}),\n\t}\n}\n\n// AsOptional returns an Option where a zero value T is considered None\n// and any other value is considered Some\n//\n// i.e.\n//\n//\tAsOptional(nil) == None()\n//\tAsOptional(0) == None()\n//\tAsOptional(false) == None()\n//\tAsOptional(\"\") == None()\n//\tAsOptional(&MyStruct{}) == Some(&MyStruct{})\n//\tAsOptional(1) == Some(1)\n//\tAsOptional(true) == Some(true)\nfunc AsOptional[T comparable](v T) Option[T] {\n\tvar zero T\n\tif v == zero {\n\t\treturn None[T]()\n\t}\n\treturn Some[T](v)\n}\n\n// FromPointer returns an Option where a nil pointer is considered None\n// and any other value is considered Some, with the value dereferenced.\nfunc FromPointer[T any](v *T) Option[T] {\n\tif v == nil {\n\t\treturn None[T]()\n\t}\n\treturn Some[T](*v)\n}\n\n// FromErr returns an Option[string] where a nil error is considered None\n// and any other value is considered Some, with the error message as the value.\nfunc FromErr(err error) Option[string] {\n\tif err == nil {\n\t\treturn None[string]()\n\t}\n\treturn Some(err.Error())\n}\n\n// Some returns an Option with the given value and present set to true\n//\n// This means Some(nil) is a valid present Option\n// and Some(nil) != None()\nfunc Some[T any](v T) Option[T] {\n\treturn Option[T]{value: v, present: true}\n}\n\n// None returns an Option with no value set\nfunc None[T any]() Option[T] {\n\treturn Option[T]{present: false}\n}\n\n// CommaOk is a helper function to convert a comma ok idiom into an Option.\n// If ok is true it returns Some(v), otherwise it returns None.\nfunc CommaOk[T any](v T, ok bool) Option[T] {\n\tif ok {\n\t\treturn Some[T](v)\n\t}\n\treturn None[T]()\n}\n\n// Present returns true if the Option has a value set\nfunc (o Option[T]) Present() bool {\n\treturn o.present\n}\n\n// Empty returns true if the Option has no value set\nfunc (o Option[T]) Empty() bool {\n\treturn !o.present\n}\n\n// OrElse returns an Option with the value if present, otherwise returns the alternative value\nfunc (o Option[T]) OrElse(alternative T) Option[T] {\n\tif o.present {\n\t\treturn o\n\t}\n\treturn Some(alternative)\n}\n\n// Get gets the option value and returns ok==true if present.\nfunc (o Option[T]) Get() (val T, ok bool) {\n\treturn o.value, o.present\n}\n\n// GetOrElse returns the value if present, otherwise returns the alternative value\nfunc (o Option[T]) GetOrElse(alternative T) T {\n\tif o.present {\n\t\treturn o.value\n\t}\n\treturn alternative\n}\n\n// GetOrElseF returns the value if present, otherwise returns the alternative value\nfunc (o Option[T]) GetOrElseF(alternative func() T) T {\n\tif o.present {\n\t\treturn o.value\n\t}\n\treturn alternative()\n}\n\n// MustGet returns the value if present, otherwise panics\nfunc (o Option[T]) MustGet() (rtn T) {\n\tif o.present {\n\t\treturn o.value\n\t}\n\tpanic(errors.Newf(\"Option value is not set: %T\", rtn))\n}\n\n// ForAll calls the given function with the value if present\nfunc (o Option[T]) ForAll(f func(v T)) {\n\tif o.present {\n\t\tf(o.value)\n\t}\n}\n\n// ForEach returns true if the Option is empty or the given predicate returns true on the value\nfunc (o Option[T]) ForEach(predicate func(v T) bool) bool {\n\tif o.present {\n\t\treturn predicate(o.value)\n\t}\n\treturn true\n}\n\n// Contains returns true if the Option is present and the given predicate returns true on the value\n// otherwise returns false\nfunc (o Option[T]) Contains(predicate func(v T) bool) bool {\n\tif o.present {\n\t\treturn predicate(o.value)\n\t}\n\treturn false\n}\n\nfunc (o Option[T]) String() string {\n\tif o.present {\n\t\treturn fmt.Sprintf(\"%v\", o.value)\n\t}\n\treturn \"None\"\n}\n\n// PtrOrNil returns the value as a pointer, if present, or nil otherwise.\nfunc (o Option[T]) PtrOrNil() *T {\n\tif o.present {\n\t\treturn &o.value\n\t}\n\treturn nil\n}\n\nfunc ToNullString(o Option[string]) sql.NullString {\n\treturn sql.NullString{String: o.value, Valid: o.present}\n}\n\nfunc ToNullBool(o Option[bool]) sql.NullBool {\n\treturn sql.NullBool{Bool: o.value, Valid: o.present}\n}\n\nfunc ToNullTime(o Option[time.Time]) sql.NullTime {\n\treturn sql.NullTime{Time: o.value, Valid: o.present}\n}\n"
  },
  {
    "path": "pkg/option/pkgfn.go",
    "content": "package option\n\n// Contains returns true if the option is present and matches the given value\nfunc Contains[T comparable](option Option[T], matches T) bool {\n\tif option.present {\n\t\treturn option.value == matches\n\t}\n\treturn false\n}\n\n// Map returns an Option with the value mapped by the given function if present, otherwise returns None\nfunc Map[T, R any](option Option[T], f func(T) R) Option[R] {\n\tif option.present {\n\t\treturn Some(f(option.value))\n\t}\n\treturn None[R]()\n}\n\n// FlatMap returns an Option with the value mapped by the given function if present, otherwise returns None\nfunc FlatMap[T, R any](option Option[T], f func(T) Option[R]) Option[R] {\n\tif option.present {\n\t\treturn f(option.value)\n\t}\n\treturn None[R]()\n}\n\n// Fold returns the result of f applied to the value if present, otherwise returns the defaultValue\nfunc Fold[T, R any](option Option[T], defaultValue R, f func(T) R) R {\n\tif option.present {\n\t\treturn f(option.value)\n\t}\n\treturn defaultValue\n}\n\n// FoldLeft applies the binary operator f to the value if present, otherwise returns the zero value\nfunc FoldLeft[T, R any](option Option[T], zero R, f func(accum R, value T) R) R {\n\tif option.present {\n\t\treturn f(zero, option.value)\n\t}\n\treturn zero\n}\n\n// Equal returns true if both Options are equal.\nfunc Equal[T comparable](a, b Option[T]) bool {\n\tif a.present != b.present {\n\t\treturn false\n\t}\n\tif !a.present {\n\t\treturn true\n\t}\n\treturn a.value == b.value\n}\n"
  },
  {
    "path": "pkg/paths/paths.go",
    "content": "package paths\n\nimport (\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/fns\"\n)\n\ntype FileReader func(string) ([]byte, error)\n\n// RootedFSPath returns a new FS path.\n// It should typically not be used except for at parser initialization.\n// Use FS.Join, FS.New, or FS.Resolve instead to preserve the working dir.\nfunc RootedFSPath(wd, p string) FS {\n\tif wd == \"\" {\n\t\tpanic(\"paths: empty wd\")\n\t} else if !filepath.IsAbs(wd) {\n\t\tpanic(\"paths: wd is relative\")\n\t}\n\n\tif filepath.IsAbs(p) {\n\t\treturn FS(filepath.Clean(p))\n\t} else {\n\t\treturn FS(filepath.Join(wd, p))\n\t}\n}\n\n// FS represents a filesystem path.\n//\n// It is an absolute path, and is always in the OS-specific format.\ntype FS string\n\n// ToIO returns the path for use in IO operations.\nfunc (fs FS) ToIO() string {\n\tfs.checkValid()\n\treturn string(fs)\n}\n\n// ToDisplay returns the path in a form suitable for displaying\n// to the user.\nfunc (fs FS) ToDisplay() string {\n\tfs.checkValid()\n\treturn string(fs)\n}\n\n// Resolve returns a new FS path to the given path.\n// If p is absolute it returns p directly,\n// otherwise it returns the path joined with the current path.\nfunc (fs FS) Resolve(p string) FS {\n\tfs.checkValid()\n\tif filepath.IsAbs(p) {\n\t\treturn FS(filepath.Clean(p))\n\t}\n\treturn FS(filepath.Join(string(fs), p))\n}\n\n// Join joins the path with the given elems, according to filepath.Join.\nfunc (fs FS) Join(elem ...string) FS {\n\tfs.checkValid()\n\tparts := append([]string{string(fs)}, elem...)\n\treturn FS(filepath.Join(parts...))\n}\n\nfunc (fs FS) JoinSlash(rel RelSlash) FS {\n\treturn fs.Join(filepath.FromSlash(rel.ToIO()))\n}\n\n// Base returns the filepath.Base of the path.\nfunc (fs FS) Base() string {\n\tfs.checkValid()\n\treturn filepath.Base(string(fs))\n}\n\n// Dir returns the filepath.Dir of the path.\nfunc (fs FS) Dir() FS {\n\tfs.checkValid()\n\treturn FS(filepath.Dir(string(fs)))\n}\n\n// HasPrefix reports whether fs is a descendant of other\n// or is equal to other. (i.e. it is the given path or a subdirectory of it)\nfunc (fs FS) HasPrefix(other FS) bool {\n\tfs.checkValid()\n\tother.checkValid()\n\n\t// Note: we use filepath.Rel instead of strings.HasPrefix with filepath.Abs\n\t// because that wouldn't work on case-insensitive filesystems.\n\trel, err := filepath.Rel(string(other), string(fs))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn filepath.IsLocal(rel)\n}\n\nfunc (fs FS) checkValid() {\n\tif fs == \"\" {\n\t\tpanic(\"empty FS path\")\n\t}\n}\n\n// ValidPkgPath reports whether a given module path is valid.\nfunc ValidPkgPath(p string) bool {\n\treturn p != \"\"\n}\n\n// PkgPath returns a new Pkg path for p. If p is not a valid\n// package path it reports \"\", false.\nfunc PkgPath(p string) (Pkg, bool) {\n\tif !ValidPkgPath(p) {\n\t\treturn \"\", false\n\t}\n\treturn Pkg(p), true\n}\n\nfunc MustPkgPath(p string) Pkg {\n\tif !ValidPkgPath(p) {\n\t\tpanic(\"invalid Package path\")\n\t}\n\treturn Pkg(p)\n}\n\n// Pkg represents a package path within a module.\n// It is always slash-separated.\ntype Pkg string\n\n// String returns the string representation of p.\nfunc (p Pkg) String() string {\n\treturn string(p)\n}\n\n// JoinSlash joins the path with the given elems, according to path.Join.\n// The elems are expected to be slash-separated, not filesystem-separated.\n// Use filesystem.ToSlash() to convert filesystem paths to slash-separated paths.\nfunc (p Pkg) JoinSlash(elem ...RelSlash) Pkg {\n\tp.checkValid()\n\tstrs := make([]string, len(elem)+1)\n\tstrs[0] = string(p)\n\tcopy(strs[1:], fns.Map(elem, RelSlash.String))\n\treturn Pkg(path.Join(strs...)) // Join cleans the result\n}\n\nfunc (p Pkg) checkValid() {\n\tif p == \"\" {\n\t\tpanic(\"invalid Pkg path\")\n\t}\n}\n\n// LexicallyContains reports whether the given package path contains the package path p\n// as a \"sub-package\".\nfunc (p Pkg) LexicallyContains(other Pkg) bool {\n\tp.checkValid()\n\tif other == \"\" {\n\t\treturn false\n\t}\n\treturn p == other || strings.HasPrefix(string(other), string(p)+\"/\")\n}\n\nconst stdModule = \"std\"\n\n// Mod represents a module path.\n// It is always slash-separated.\ntype Mod string\n\n// ValidModPath reports whether a given module path is valid.\nfunc ValidModPath(p string) bool {\n\treturn p != \"\"\n}\n\n// MustModPath returns a new Mod path for p.\nfunc MustModPath(p string) Mod {\n\tif !ValidModPath(p) {\n\t\tpanic(\"invalid Module path\")\n\t}\n\treturn Mod(p)\n}\n\n// StdlibMod returns the Mod path representing the standard library.\nfunc StdlibMod() Mod {\n\treturn stdModule\n}\n\n// LexicallyContains reports whether the given module path contains the package path p.\n// It only considers the lexical path and ignores whether there exists\n// a nested module that contains p.\nfunc (m Mod) LexicallyContains(p Pkg) bool {\n\tm.checkValid()\n\tif p == \"\" {\n\t\treturn false\n\t}\n\n\t// From the spec:\n\t// A module that will never be fetched as a dependency of any other module may use\n\t// any valid package path for its module path, but must take care not to collide\n\t// with paths that may be used by the module's dependencies or the Go standard\n\t// library. The Go standard library uses package paths that do not contain a dot in\n\t// the first path element, and the `go` command does not attempt to resolve such\n\t// paths from network servers. The paths `example` and `test` are reserved for\n\t// users: they will not be used in the standard library and are suitable for use in\n\t// self-contained modules, such as those defined in tutorials or example code or\n\t// created and manipulated as part of a test.\n\n\tms, ps := string(m), string(p)\n\tif m == stdModule {\n\t\t// Treat any dotless package path as being contained, as long as\n\t\t// it's not one of the reserved paths.\n\t\tif first, _, _ := strings.Cut(ps, \"/\"); strings.Contains(first, \".\") {\n\t\t\treturn false\n\t\t} else if first == \"example\" || first == \"tests\" {\n\t\t\t// Reserved; guaranteed not to be part of std\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}\n\n\t// We can treat the module path as a package path for this purpose.\n\treturn ms == ps || strings.HasPrefix(ps, ms+\"/\")\n}\n\n// RelativePathToPkg returns the relative path from the module to the package.\n// If the package is not contained within the module it reports \"\", false.\nfunc (m Mod) RelativePathToPkg(p Pkg) (relative RelSlash, ok bool) {\n\tm.checkValid()\n\tp.checkValid()\n\tif !m.LexicallyContains(p) {\n\t\treturn \"\", false\n\t}\n\n\t// The module path is a prefix of the package path.\n\t// Remove the module path and the leading slash.\n\tif m == stdModule {\n\t\treturn RelSlash(p), true\n\t}\n\n\t// Is the package path the same as the module path?\n\tif string(p) == string(m) {\n\t\treturn \".\", true\n\t}\n\n\tsuffix, ok := strings.CutPrefix(string(p), string(m)+\"/\")\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\treturn RelSlash(suffix), true\n}\n\nfunc (m Mod) Pkg(rel RelSlash) Pkg {\n\tm.checkValid()\n\tif m == stdModule {\n\t\treturn Pkg(rel)\n\t}\n\treturn Pkg(path.Join(string(m), string(rel)))\n}\n\nfunc (m Mod) checkValid() {\n\tif m == \"\" {\n\t\tpanic(\"invalid Module path\")\n\t}\n}\n\n// IsStdlib reports whether m represents the standard library.\nfunc (m Mod) IsStdlib() bool {\n\treturn m == stdModule\n}\n\n// RelSlash is a relative path that is always slash-separated.\ntype RelSlash string\n\n// ToIO converts the slash-separated path to a filesystem path\n// using filepath.FromSlash.\nfunc (p RelSlash) ToIO() string {\n\treturn filepath.FromSlash(string(p))\n}\n\n// Join joins the path with the given elems, according to path.Join.\nfunc (rel RelSlash) Join(elem ...string) RelSlash {\n\tparts := append([]string{string(rel)}, elem...)\n\treturn RelSlash(path.Join(parts...))\n}\n\nfunc (p RelSlash) String() string {\n\treturn string(p)\n}\n\n// MainModuleRelSlash is like RelSlash, but it's always relative to the application's\n// main module directory.\ntype MainModuleRelSlash string\n\n// ToIO converts the slash-separated path to a filesystem path\n// using filepath.FromSlash.\nfunc (p MainModuleRelSlash) ToIO(mainModDir FS) string {\n\treturn mainModDir.Join(filepath.FromSlash(string(p))).ToIO()\n}\n\nfunc (p MainModuleRelSlash) String() string {\n\treturn string(p)\n}\n"
  },
  {
    "path": "pkg/pgproxy/README.md",
    "content": "# pgproxy\n\npgproxy is a flexible proxy for the Postgres wire protocol that allows for customizing authentication and backend selection by breaking apart the startup message flow between frontend and backend.\n\nOnce authenticated, it falls back to being a dumb proxy\nthat simple shuffles bytes back and forth.\n"
  },
  {
    "path": "pkg/pgproxy/pgproxy.go",
    "content": "package pgproxy\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jackc/pgconn\"\n\t\"github.com/jackc/pgproto3/v2\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/fns\"\n)\n\ntype LogicalConn interface {\n\tnet.Conn\n\tCancel(*CancelData) error\n}\n\ntype HelloData interface {\n\thello()\n}\n\ntype StartupData struct {\n\tRaw      *pgproto3.StartupMessage\n\tDatabase string\n\tUsername string\n\tPassword string // may be empty if RequirePassword is false\n}\n\ntype CancelData struct {\n\tRaw *pgproto3.CancelRequest\n}\n\nfunc (*StartupData) hello() {}\nfunc (*CancelData) hello()  {}\n\ntype SingleBackendProxy struct {\n\tLog             zerolog.Logger\n\tRequirePassword bool\n\tFrontendTLS     *tls.Config\n\tDialBackend     func(context.Context, *StartupData) (LogicalConn, error)\n\n\tgotBackend chan struct{} // closed when first connection is received\n\n\tmu      sync.Mutex\n\tkeyData map[pgproto3.BackendKeyData]LogicalConn\n}\n\ntype DatabaseNotFoundError struct {\n\tDatabase string\n}\n\nfunc (e DatabaseNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"database %s not found\", e.Database)\n}\n\nfunc (p *SingleBackendProxy) Serve(ctx context.Context, ln net.Listener) error {\n\tdefer fns.CloseIgnore(ln)\n\tif p.gotBackend != nil {\n\t\tpanic(\"SingleBackendProxy: Serve called twice\")\n\t}\n\tp.gotBackend = make(chan struct{})\n\n\tgo func() {\n\t\tctx, cancel := context.WithTimeout(ctx, 10*time.Minute)\n\t\tdefer cancel()\n\n\t\tselect {\n\t\tcase <-p.gotBackend:\n\t\tcase <-ctx.Done():\n\t\t\t_ = ln.Close()\n\t\t}\n\t}()\n\n\tvar tempDelay time.Duration // how long to sleep on accept failure\n\tgotBackend := false\n\tfor {\n\t\tfrontend, err := ln.Accept()\n\t\tif err != nil {\n\t\t\tif ne, ok := err.(net.Error); ok && ne.Temporary() {\n\t\t\t\tif tempDelay == 0 {\n\t\t\t\t\ttempDelay = 5 * time.Millisecond\n\t\t\t\t} else {\n\t\t\t\t\ttempDelay *= 2\n\t\t\t\t}\n\t\t\t\tif max := 1 * time.Second; tempDelay > max {\n\t\t\t\t\ttempDelay = max\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"pgproxy: accept error: %v; retrying in %v\", err, tempDelay)\n\t\t\t\ttime.Sleep(tempDelay)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"pgproxy: could not accept: %v\", err)\n\t\t}\n\n\t\tif !gotBackend {\n\t\t\tclose(p.gotBackend)\n\t\t\tgotBackend = true\n\t\t}\n\n\t\tgo p.ProxyConn(ctx, frontend)\n\t\ttempDelay = 0\n\t}\n}\n\nfunc (p *SingleBackendProxy) ProxyConn(ctx context.Context, client net.Conn) {\n\tdefer fns.CloseIgnore(client)\n\n\tcl, err := SetupClient(client, &ClientConfig{\n\t\tTLS:          p.FrontendTLS,\n\t\tWantPassword: p.RequirePassword,\n\t})\n\tif err != nil {\n\t\tp.Log.Error().Err(err).Msg(\"unable to setup frontend\")\n\t\treturn\n\t}\n\n\tswitch data := cl.Hello.(type) {\n\tcase *StartupData:\n\t\tif err := p.doRunProxy(ctx, cl); err != nil {\n\t\t\tp.Log.Error().Err(err).Msg(\"unable to run backend proxy\")\n\t\t}\n\tcase *CancelData:\n\t\tp.cancelRequest(ctx, data)\n\tdefault:\n\t\tp.Log.Error().Msgf(\"unknown hello message type: %T\", data)\n\t}\n}\n\nfunc (p *SingleBackendProxy) doRunProxy(ctx context.Context, cl *Client) error {\n\tstartup := cl.Hello.(*StartupData)\n\tserver, err := p.DialBackend(ctx, startup)\n\tif err != nil {\n\t\t_ = cl.Backend.Send(&pgproto3.ErrorResponse{\n\t\t\tSeverity: \"FATAL\",\n\t\t\tMessage:  err.Error(),\n\t\t})\n\t\treturn err\n\t}\n\tdefer fns.CloseIgnore(server)\n\n\tfe := pgproto3.NewFrontend(pgproto3.NewChunkReader(server), server)\n\tlog.Trace().Msg(\"successfully setup server connection\")\n\n\terr = AuthenticateClient(cl.Backend)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"unable to authenticate client\")\n\t\treturn err\n\t}\n\tlog.Trace().Msg(\"successfully authenticated client\")\n\n\tkey, err := FinalizeInitialHandshake(cl.Backend, fe)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"unable to finalize handshake\")\n\t\treturn err\n\t}\n\n\tif key != nil {\n\t\tp.mu.Lock()\n\t\tif p.keyData == nil {\n\t\t\tp.keyData = make(map[pgproto3.BackendKeyData]LogicalConn)\n\t\t}\n\t\tp.keyData[*key] = server\n\t\tp.mu.Unlock()\n\t}\n\n\terr = CopySteadyState(cl.Backend, fe)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"unable to copy steady state\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype ServerConfig struct {\n\tTLS     *tls.Config // nil indicates no TLS\n\tStartup *StartupData\n}\n\n// SetupServer sets up a frontend connected to the given server.\nfunc SetupServer(server net.Conn, cfg *ServerConfig) (*pgproto3.Frontend, error) {\n\tfe, err := serverTLSNegotiate(server, cfg.TLS)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\traw := cfg.Startup.Raw\n\traw.Parameters[\"database\"] = cfg.Startup.Database\n\traw.Parameters[\"user\"] = cfg.Startup.Username\n\n\tlog.Trace().Msg(\"sending startup message to server\")\n\tif err := fe.Send(raw); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to send startup message: %v\", err)\n\t}\n\n\t// Handle authentication\n\tfor {\n\t\tmsg, err := fe.Receive()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unexpected message from server: %v\", err)\n\t\t}\n\n\t\tswitch msg := msg.(type) {\n\t\tcase *pgproto3.ErrorResponse:\n\t\t\treturn nil, pgconn.ErrorResponseToPgError(msg)\n\n\t\tcase pgproto3.AuthenticationResponseMessage:\n\t\t\tif len(cfg.Startup.Password) == 0 {\n\t\t\t\tif _, ok := msg.(*pgproto3.AuthenticationOk); !ok {\n\t\t\t\t\treturn nil, fmt.Errorf(\"backend requested authentication but no password given\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tswitch msg := msg.(type) {\n\t\t\tcase *pgproto3.AuthenticationOk:\n\t\t\t\t// We're done!\n\t\t\t\treturn fe, nil\n\n\t\t\tcase *pgproto3.AuthenticationCleartextPassword:\n\t\t\t\terr := fe.Send(&pgproto3.PasswordMessage{Password: cfg.Startup.Password})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase *pgproto3.AuthenticationMD5Password:\n\t\t\t\tpassword := computeMD5(cfg.Startup.Username, cfg.Startup.Password, msg.Salt)\n\t\t\t\terr := fe.Send(&pgproto3.PasswordMessage{Password: password})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase *pgproto3.AuthenticationSASL:\n\t\t\t\tif err := scramAuth(fe, cfg.Startup.Password, msg.AuthMechanisms); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"unsupported auth message: %T\", msg)\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unexpected message type from backend: %T\", msg)\n\t\t}\n\t}\n}\n\nfunc serverTLSNegotiate(server net.Conn, tlsConfig *tls.Config) (*pgproto3.Frontend, error) {\n\tcr := pgproto3.NewChunkReader(server)\n\tfrontend := pgproto3.NewFrontend(cr, server)\n\tif tlsConfig == nil {\n\t\treturn frontend, nil\n\t}\n\n\tlog.Trace().Msg(\"negotiating tls with server\")\n\tif err := frontend.Send(&pgproto3.SSLRequest{}); err != nil {\n\t\treturn nil, err\n\t}\n\t// Read the TLS response.\n\tresp, err := cr.Next(1)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch resp[0] {\n\tcase 'S':\n\t\tlog.Trace().Msg(\"server accepted tls request\")\n\t\ttlsConn := tls.Client(server, tlsConfig)\n\t\tif err := tlsConn.Handshake(); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"server tls handshake failed\")\n\t\t\t_ = tlsConn.Close()\n\t\t\treturn nil, fmt.Errorf(\"server tls handshake failed: %v\", err)\n\t\t}\n\t\tlog.Trace().Msg(\"completed server tls handshake\")\n\n\t\t// Return a new backend that wraps the tls conn.\n\t\treturn pgproto3.NewFrontend(pgproto3.NewChunkReader(tlsConn), tlsConn), nil\n\tcase 'N':\n\t\tlog.Trace().Msg(\"server rejected tls request\")\n\t\treturn nil, fmt.Errorf(\"server rejected tls\")\n\tcase 'E':\n\t\t// ErrorMessage: we've already parsed the first byte so read it manually.\n\t\thdr, err := cr.Next(4)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbodyLen := int(binary.BigEndian.Uint32(hdr)) - 4\n\t\tmsgBody, err := cr.Next(bodyLen)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar errMsg pgproto3.ErrorResponse\n\t\tif err := errMsg.Decode(msgBody); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Error().Msgf(\"server tls negotiation error: %+v\", errMsg)\n\t\treturn nil, fmt.Errorf(\"could not negotiate tls with server: error %s: %s\", errMsg.Code, errMsg.Message)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"got unexpected response to tls request: %v\", resp[0])\n\t}\n}\n\ntype ClientConfig struct {\n\t// TLS, if non-nil, indicates we support TLS connections.\n\tTLS *tls.Config\n\n\t// WantPassword, if true, indicates we want to capture\n\t// the password sent by the frontend.\n\tWantPassword bool\n}\n\ntype Client struct {\n\tBackend *pgproto3.Backend\n\tHello   HelloData\n}\n\n// SetupClient sets up a backend connected to the given client.\n// If tlsConfig is non-nil it negotiates TLS if requested by the client.\n//\n// On successful startup the returned message is either *pgproto3.StartupMessage or *pgproto3.CancelRequest.\n//\n// It is up to the caller to authenticate the client using AuthenticateClient.\nfunc SetupClient(client net.Conn, cfg *ClientConfig) (*Client, error) {\n\tlog.Trace().Msg(\"setting up client backend\")\n\tbe, msg, err := clientTLSNegotiate(client, cfg.TLS)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cancel, ok := msg.(*pgproto3.CancelRequest); ok {\n\t\treturn &Client{\n\t\t\tBackend: be,\n\t\t\tHello:   &CancelData{Raw: cancel},\n\t\t}, nil\n\t}\n\n\tstartup := msg.(*pgproto3.StartupMessage)\n\thello := &StartupData{\n\t\tRaw:      startup,\n\t\tDatabase: startup.Parameters[\"database\"],\n\t\tUsername: startup.Parameters[\"user\"],\n\t}\n\tif cfg.WantPassword {\n\t\terr := be.Send(&pgproto3.AuthenticationCleartextPassword{})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmsg, err := be.Receive()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpasswd, ok := msg.(*pgproto3.PasswordMessage)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"expected PasswordMessage, got %T\", msg)\n\t\t}\n\t\thello.Password = passwd.Password\n\t}\n\n\treturn &Client{\n\t\tBackend: be,\n\t\tHello:   hello,\n\t}, nil\n}\n\nfunc clientTLSNegotiate(client net.Conn, tlsConfig *tls.Config) (*pgproto3.Backend, pgproto3.FrontendMessage, error) {\n\tlog.Trace().Msg(\"negotiating TLS with client\")\n\tbackend := pgproto3.NewBackend(pgproto3.NewChunkReader(client), client)\n\thasTLS := false\n\nStartupMessageLoop:\n\tfor {\n\t\tstartup, err := backend.ReceiveStartupMessage()\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tswitch startup := startup.(type) {\n\t\tcase *pgproto3.SSLRequest:\n\t\t\tif hasTLS {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"received duplicate SSL request\")\n\t\t\t} else if tlsConfig == nil {\n\t\t\t\t// We got an SSL request but we don't want to use TLS\n\t\t\t\tif _, err := client.Write([]byte{'N'}); err != nil {\n\t\t\t\t\treturn nil, nil, err\n\t\t\t\t}\n\t\t\t\tcontinue StartupMessageLoop\n\t\t\t}\n\n\t\t\tif _, err := client.Write([]byte{'S'}); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\ttlsConn := tls.Server(client, tlsConfig)\n\t\t\tif err := tlsConn.Handshake(); err != nil {\n\t\t\t\t_ = tlsConn.Close()\n\t\t\t\treturn nil, nil, fmt.Errorf(\"client tls handshake failed: %v\", err)\n\t\t\t}\n\t\t\tlog.Trace().Msg(\"client tls handshake successful\")\n\n\t\t\t// The TLS handshake was successful.\n\t\t\t// Create a new backend that reads from the now-encrypted TLS connection.\n\t\t\thasTLS = true\n\t\t\tbackend = pgproto3.NewBackend(pgproto3.NewChunkReader(tlsConn), tlsConn)\n\t\tcase *pgproto3.CancelRequest, *pgproto3.StartupMessage:\n\t\t\t// Startup complete.\n\t\t\tlog.Debug().Msg(\"startup completed\")\n\t\t\treturn backend, startup, nil\n\t\tcase *pgproto3.GSSEncRequest:\n\t\t\t// We got a GSSAPI encryption request but we don't support it.\n\t\t\tif _, err := client.Write([]byte{'N'}); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t\tcontinue StartupMessageLoop\n\t\t}\n\t}\n}\n\ntype AuthData struct {\n\tUsername string\n\tPassword string\n}\n\n// AuthenticateClient tells the client they've successfully authenticated.\nfunc AuthenticateClient(be *pgproto3.Backend) error {\n\t_ = be.SetAuthType(pgproto3.AuthTypeOk)\n\treturn be.Send(&pgproto3.AuthenticationOk{})\n}\n\nfunc computeMD5(username, password string, salt [4]byte) string {\n\t// concat('md5', md5(concat(md5(concat(password, username)), random-salt)))\n\n\t// s1 := md5(concat(password, username))\n\t// nosemgrep\n\ts1 := md5.Sum([]byte(password + username))\n\t// s2 := md5(concat(s1, random-salt))\n\t// nosemgrep\n\ts2 := md5.Sum([]byte(hex.EncodeToString(s1[:]) + string(salt[:])))\n\treturn \"md5\" + hex.EncodeToString(s2[:])\n}\n\nfunc SendCancelRequest(conn io.ReadWriter, req *pgproto3.CancelRequest) error {\n\tbuf := make([]byte, 16)\n\tbinary.BigEndian.PutUint32(buf[0:4], 16)\n\tbinary.BigEndian.PutUint32(buf[4:8], 80877102)\n\tbinary.BigEndian.PutUint32(buf[8:12], uint32(req.ProcessID))\n\tbinary.BigEndian.PutUint32(buf[12:16], uint32(req.SecretKey))\n\t_, err := conn.Write(buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = conn.Read(buf)\n\tif err != io.EOF {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// FinalizeInitialHandshake completes the handshake between client and server,\n// snooping the BackendKeyData from the server if sent.\n// It is nil if the server did not send any backend key data.\nfunc FinalizeInitialHandshake(client *pgproto3.Backend, server *pgproto3.Frontend) (*pgproto3.BackendKeyData, error) {\n\tvar keyData *pgproto3.BackendKeyData\n\n\t// Read messages from backend until we get ReadyForQuery\n\tfor {\n\t\tmsg, err := server.Receive()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"pgproxy: cannot read from backend: %v\", err)\n\t\t} else if err := client.Send(msg); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"pgproxy: could not write to frontend: %v\", err)\n\t\t}\n\n\t\tswitch msg := msg.(type) {\n\t\tcase *pgproto3.BackendKeyData:\n\t\t\t// Make a copy; this object is only valid until the next call to Receive()\n\t\t\tcopy := *msg\n\t\t\tkeyData = &copy\n\n\t\tcase *pgproto3.ReadyForQuery:\n\t\t\t// Handshake completed\n\t\t\treturn keyData, nil\n\t\t}\n\t}\n}\n\n// CopySteadyState copies messages back and forth after the initial handshake.\nfunc CopySteadyState(client *pgproto3.Backend, server *pgproto3.Frontend) error {\n\terrChan := make(chan error, 2)\n\tmsgAck := make(chan struct{})\n\tdone := make(chan struct{})\n\tdefer close(done)\n\n\trunTask := func(task func() error) {\n\t\tgo func() {\n\t\t\terrChan <- task()\n\t\t}()\n\t}\n\tclientMsgs := make(chan pgproto3.FrontendMessage)\n\n\trunTask(func() (err error) {\n\t\tfor {\n\t\t\tmsg, err := server.Receive()\n\t\t\tif errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\tlog.Trace().Msg(\"pgproxy: connection terminated by server, closing connection\")\n\t\t\t\treturn nil\n\t\t\t} else if err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := client.Send(msg); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t})\n\n\trunTask(func() error {\n\t\tfor {\n\t\t\tmsg, err := client.Receive()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\treturn nil\n\t\t\tcase clientMsgs <- msg:\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\treturn nil\n\t\t\tcase <-msgAck:\n\t\t\t}\n\t\t}\n\t})\n\n\t// Copy from client to server.\n\tfor {\n\t\tselect {\n\t\tcase err := <-errChan:\n\t\t\treturn err\n\t\tcase msg := <-clientMsgs:\n\t\t\terr := server.Send(msg)\n\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, ok := msg.(*pgproto3.Terminate); ok {\n\t\t\t\tlog.Trace().Msg(\"received terminate from client, closing connection\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tmsgAck <- struct{}{}\n\t\t}\n\t}\n}\n\nfunc (p *SingleBackendProxy) cancelRequest(ctx context.Context, cancel *CancelData) {\n\tp.Log.Trace().Msg(\"received cancel request\")\n\tkey := pgproto3.BackendKeyData{\n\t\tProcessID: cancel.Raw.ProcessID,\n\t\tSecretKey: cancel.Raw.SecretKey,\n\t}\n\tp.mu.Lock()\n\tconn, ok := p.keyData[key]\n\tp.mu.Unlock()\n\n\tif ok {\n\t\tif err := conn.Cancel(cancel); err != nil {\n\t\t\tp.Log.Error().Err(err).Msg(\"unable to send cancel request\")\n\t\t}\n\t} else {\n\t\tp.Log.Error().Msg(\"could not find backend key data\")\n\t}\n}\n"
  },
  {
    "path": "pkg/pgproxy/scram.go",
    "content": "package pgproxy\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/jackc/pgproto3/v2\"\n\t\"golang.org/x/crypto/pbkdf2\"\n\t\"golang.org/x/text/secure/precis\"\n)\n\n// The below code is taken from github.com/jackc/pgconn (auth_scram.go).\n\n/*\nCopyright (c) 2019-2021 Jack Christensen\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\n\nconst clientNonceLen = 18\n\nfunc scramAuth(fe *pgproto3.Frontend, password string, serverAuthMechanisms []string) error {\n\tsc, err := newScramClient(serverAuthMechanisms, password)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Send client-first-message in a SASLInitialResponse\n\tsaslInitialResponse := &pgproto3.SASLInitialResponse{\n\t\tAuthMechanism: \"SCRAM-SHA-256\",\n\t\tData:          sc.clientFirstMessage(),\n\t}\n\tif err := fe.Send(saslInitialResponse); err != nil {\n\t\treturn err\n\t}\n\n\t// Receive server-first-message payload in a AuthenticationSASLContinue.\n\tmsg, err := fe.Receive()\n\tif err != nil {\n\t\treturn err\n\t}\n\tsaslContinue, ok := msg.(*pgproto3.AuthenticationSASLContinue)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected AuthenticationSASLContinue message, got %T\", msg)\n\t}\n\n\terr = sc.recvServerFirstMessage(saslContinue.Data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Send client-final-message in a SASLResponse\n\tsaslResponse := &pgproto3.SASLResponse{\n\t\tData: []byte(sc.clientFinalMessage()),\n\t}\n\tif err := fe.Send(saslResponse); err != nil {\n\t\treturn err\n\t}\n\n\t// Receive server-final-message payload in a AuthenticationSASLFinal.\n\tmsg, err = fe.Receive()\n\tif err != nil {\n\t\treturn err\n\t}\n\tsaslFinal, ok := msg.(*pgproto3.AuthenticationSASLFinal)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected AuthenticationSASLFinal message, got %T\", msg)\n\t}\n\treturn sc.recvServerFinalMessage(saslFinal.Data)\n}\n\ntype scramClient struct {\n\tserverAuthMechanisms []string\n\tpassword             []byte\n\tclientNonce          []byte\n\n\tclientFirstMessageBare []byte\n\n\tserverFirstMessage   []byte\n\tclientAndServerNonce []byte\n\tsalt                 []byte\n\titerations           int\n\n\tsaltedPassword []byte\n\tauthMessage    []byte\n}\n\nfunc newScramClient(serverAuthMechanisms []string, password string) (*scramClient, error) {\n\tsc := &scramClient{\n\t\tserverAuthMechanisms: serverAuthMechanisms,\n\t}\n\n\t// Ensure server supports SCRAM-SHA-256\n\thasScramSHA256 := false\n\tfor _, mech := range sc.serverAuthMechanisms {\n\t\tif mech == \"SCRAM-SHA-256\" {\n\t\t\thasScramSHA256 = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !hasScramSHA256 {\n\t\treturn nil, errors.New(\"server does not support SCRAM-SHA-256\")\n\t}\n\n\t// precis.OpaqueString is equivalent to SASLprep for password.\n\tvar err error\n\tsc.password, err = precis.OpaqueString.Bytes([]byte(password))\n\tif err != nil {\n\t\t// PostgreSQL allows passwords invalid according to SCRAM / SASLprep.\n\t\tsc.password = []byte(password)\n\t}\n\n\tbuf := make([]byte, clientNonceLen)\n\t_, err = rand.Read(buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsc.clientNonce = make([]byte, base64.RawStdEncoding.EncodedLen(len(buf)))\n\tbase64.RawStdEncoding.Encode(sc.clientNonce, buf)\n\n\treturn sc, nil\n}\n\nfunc (sc *scramClient) clientFirstMessage() []byte {\n\tsc.clientFirstMessageBare = []byte(fmt.Sprintf(\"n=,r=%s\", sc.clientNonce))\n\treturn []byte(fmt.Sprintf(\"n,,%s\", sc.clientFirstMessageBare))\n}\n\nfunc (sc *scramClient) recvServerFirstMessage(serverFirstMessage []byte) error {\n\tsc.serverFirstMessage = serverFirstMessage\n\tbuf := serverFirstMessage\n\tif !bytes.HasPrefix(buf, []byte(\"r=\")) {\n\t\treturn errors.New(\"invalid SCRAM server-first-message received from server: did not include r=\")\n\t}\n\tbuf = buf[2:]\n\n\tidx := bytes.IndexByte(buf, ',')\n\tif idx == -1 {\n\t\treturn errors.New(\"invalid SCRAM server-first-message received from server: did not include s=\")\n\t}\n\tsc.clientAndServerNonce = buf[:idx]\n\tbuf = buf[idx+1:]\n\n\tif !bytes.HasPrefix(buf, []byte(\"s=\")) {\n\t\treturn errors.New(\"invalid SCRAM server-first-message received from server: did not include s=\")\n\t}\n\tbuf = buf[2:]\n\n\tidx = bytes.IndexByte(buf, ',')\n\tif idx == -1 {\n\t\treturn errors.New(\"invalid SCRAM server-first-message received from server: did not include i=\")\n\t}\n\tsaltStr := buf[:idx]\n\tbuf = buf[idx+1:]\n\n\tif !bytes.HasPrefix(buf, []byte(\"i=\")) {\n\t\treturn errors.New(\"invalid SCRAM server-first-message received from server: did not include i=\")\n\t}\n\tbuf = buf[2:]\n\titerationsStr := buf\n\n\tvar err error\n\tsc.salt, err = base64.StdEncoding.DecodeString(string(saltStr))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid SCRAM salt received from server: %w\", err)\n\t}\n\n\tsc.iterations, err = strconv.Atoi(string(iterationsStr))\n\tif err != nil || sc.iterations <= 0 {\n\t\treturn fmt.Errorf(\"invalid SCRAM iteration count received from server: %w\", err)\n\t}\n\n\tif !bytes.HasPrefix(sc.clientAndServerNonce, sc.clientNonce) {\n\t\treturn errors.New(\"invalid SCRAM nonce: did not start with client nonce\")\n\t}\n\n\tif len(sc.clientAndServerNonce) <= len(sc.clientNonce) {\n\t\treturn errors.New(\"invalid SCRAM nonce: did not include server nonce\")\n\t}\n\n\treturn nil\n}\n\nfunc (sc *scramClient) clientFinalMessage() string {\n\tclientFinalMessageWithoutProof := []byte(fmt.Sprintf(\"c=biws,r=%s\", sc.clientAndServerNonce))\n\n\tsc.saltedPassword = pbkdf2.Key([]byte(sc.password), sc.salt, sc.iterations, 32, sha256.New)\n\tsc.authMessage = bytes.Join([][]byte{sc.clientFirstMessageBare, sc.serverFirstMessage, clientFinalMessageWithoutProof}, []byte(\",\"))\n\n\tclientProof := computeClientProof(sc.saltedPassword, sc.authMessage)\n\n\treturn fmt.Sprintf(\"%s,p=%s\", clientFinalMessageWithoutProof, clientProof)\n}\n\nfunc (sc *scramClient) recvServerFinalMessage(serverFinalMessage []byte) error {\n\tif !bytes.HasPrefix(serverFinalMessage, []byte(\"v=\")) {\n\t\treturn errors.New(\"invalid SCRAM server-final-message received from server\")\n\t}\n\n\tserverSignature := serverFinalMessage[2:]\n\n\tif !hmac.Equal(serverSignature, computeServerSignature(sc.saltedPassword, sc.authMessage)) {\n\t\treturn errors.New(\"invalid SCRAM ServerSignature received from server\")\n\t}\n\n\treturn nil\n}\n\nfunc computeHMAC(key, msg []byte) []byte {\n\tmac := hmac.New(sha256.New, key)\n\tmac.Write(msg)\n\treturn mac.Sum(nil)\n}\n\nfunc computeClientProof(saltedPassword, authMessage []byte) []byte {\n\tclientKey := computeHMAC(saltedPassword, []byte(\"Client Key\"))\n\tstoredKey := sha256.Sum256(clientKey)\n\tclientSignature := computeHMAC(storedKey[:], authMessage)\n\n\tclientProof := make([]byte, len(clientSignature))\n\tfor i := 0; i < len(clientSignature); i++ {\n\t\tclientProof[i] = clientKey[i] ^ clientSignature[i]\n\t}\n\n\tbuf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof)))\n\tbase64.StdEncoding.Encode(buf, clientProof)\n\treturn buf\n}\n\nfunc computeServerSignature(saltedPassword []byte, authMessage []byte) []byte {\n\tserverKey := computeHMAC(saltedPassword, []byte(\"Server Key\"))\n\tserverSignature := computeHMAC(serverKey, authMessage)\n\tbuf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature)))\n\tbase64.StdEncoding.Encode(buf, serverSignature)\n\treturn buf\n}\n"
  },
  {
    "path": "pkg/promise/prom.go",
    "content": "package promise\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\ntype Value[T any] struct {\n\tval  T\n\terr  error\n\tdone chan struct{}\n\n\tonResolve eventList[T]\n\tonReject  eventList[error]\n}\n\nfunc (v *Value[T]) Get(ctx context.Context) (T, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\tvar zero T\n\t\treturn zero, ctx.Err()\n\tcase <-v.done:\n\t\treturn v.val, v.err\n\t}\n}\n\nfunc (v *Value[T]) OnResolve(fn func(T)) {\n\tv.onResolve.Add(fn)\n}\n\nfunc (v *Value[T]) OnReject(fn func(error)) {\n\tv.onReject.Add(fn)\n}\n\nfunc New[T any](fn func() (T, error)) *Value[T] {\n\tval := &Value[T]{\n\t\tdone: make(chan struct{}),\n\t}\n\tgo func() {\n\t\tfunc() {\n\t\t\t// If we panic, we want to mark the promise as done\n\t\t\tdefer close(val.done)\n\t\t\tval.val, val.err = fn()\n\t\t}()\n\n\t\tif val.err == nil {\n\t\t\tval.onResolve.MarkDoneAndProcess(val.val)\n\t\t} else {\n\t\t\tval.onReject.MarkDoneAndProcess(val.err)\n\t\t}\n\t}()\n\treturn val\n}\n\nfunc Resolved[T any](val T) *Value[T] {\n\tdone := make(chan struct{})\n\tclose(done)\n\tv := &Value[T]{\n\t\tdone: done,\n\t\tval:  val,\n\t}\n\tv.onResolve.MarkDoneAndProcess(val)\n\treturn v\n}\n\nfunc Rejected[T any](err error) *Value[T] {\n\tdone := make(chan struct{})\n\tclose(done)\n\tv := &Value[T]{\n\t\tdone: done,\n\t\terr:  err,\n\t}\n\tv.onReject.MarkDoneAndProcess(err)\n\treturn v\n}\n\ntype eventList[V any] struct {\n\tmu    sync.Mutex\n\tdone  bool\n\tval   V\n\tfuncs []func(V)\n}\n\nfunc (g *eventList[V]) Add(fn func(V)) {\n\tg.mu.Lock()\n\tif g.done {\n\t\tg.mu.Unlock()\n\t\tfn(g.val)\n\t} else {\n\t\tg.funcs = append(g.funcs, fn)\n\t\tg.mu.Unlock()\n\t}\n}\n\nfunc (g *eventList[V]) MarkDoneAndProcess(val V) {\n\tg.mu.Lock()\n\tg.done = true\n\tg.val = val\n\tg.mu.Unlock()\n\n\tfor _, fn := range g.funcs {\n\t\tfn(val)\n\t}\n}\n\nfunc Wait2[A, B any](ctx context.Context, a *Value[A], b *Value[B]) (A, B, error) {\n\taVal, err1 := a.Get(ctx)\n\tbVal, err2 := b.Get(ctx)\n\n\terr := err1\n\tif err == nil {\n\t\terr = err2\n\t}\n\treturn aVal, bVal, err\n}\n\nfunc Wait3[A, B, C any](ctx context.Context, a *Value[A], b *Value[B], c *Value[C]) (A, B, C, error) {\n\taVal, err1 := a.Get(ctx)\n\tbVal, err2 := b.Get(ctx)\n\tcVal, err3 := c.Get(ctx)\n\n\terr := err1\n\tif err == nil {\n\t\terr = err2\n\t}\n\tif err == nil {\n\t\terr = err3\n\t}\n\treturn aVal, bVal, cVal, err\n}\n"
  },
  {
    "path": "pkg/rtconfgen/base_builder.go",
    "content": "package rtconfgen\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/golang/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n)\n\ntype ResourceID interface {\n\tcomparable\n\tfmt.Stringer\n}\n\ntype Builder struct {\n\tInfra *InfraBuilder\n\trs    *resourceSet\n\n\tenv    *runtimev1.Environment\n\tencore *runtimev1.EncorePlatform\n\tobs    *runtimev1.Observability\n\n\t// Any errors encountered during the build process.\n\terr error\n\n\t// authMethods are the service auth methods to use for authenticating to this deployment.\n\tauthMethods []*runtimev1.ServiceAuth\n\n\t// defaultGracefulShutdown is the graceful shutdown behavior to use by default,\n\t// unless a deployment overrides it.\n\tdefaultGracefulShutdown *runtimev1.GracefulShutdown\n\n\tdefaultDeployID   string\n\tdefaultDeployedAt time.Time\n\n\tdeployments map[string]*Deployment\n\tservices    map[string]*runtimev1.HostedService\n}\n\nfunc NewBuilder() *Builder {\n\trs := new(resourceSet)\n\tb := &Builder{\n\t\tInfra:       newInfraBuilder(rs),\n\t\trs:          rs,\n\t\tobs:         &runtimev1.Observability{},\n\t\tdeployments: make(map[string]*Deployment),\n\t\tservices:    make(map[string]*runtimev1.HostedService),\n\t}\n\n\treturn b\n}\n\nfunc (b *Builder) Env(env *runtimev1.Environment) *Builder {\n\tb.env = env\n\treturn b\n}\n\nfunc (b *Builder) EncorePlatform(encore *runtimev1.EncorePlatform) *Builder {\n\tb.encore = encore\n\treturn b\n}\n\nfunc (b *Builder) DefaultGracefulShutdown(s *runtimev1.GracefulShutdown) *Builder {\n\tb.defaultGracefulShutdown = s\n\treturn b\n}\n\nfunc (b *Builder) AuthMethods(m []*runtimev1.ServiceAuth) *Builder {\n\tb.authMethods = m\n\treturn b\n}\n\nfunc (b *Builder) DeployID(id string) *Builder {\n\tb.defaultDeployID = id\n\treturn b\n}\n\nfunc (b *Builder) DeployedAt(t time.Time) *Builder {\n\tb.defaultDeployedAt = t\n\treturn b\n}\n\nfunc (b *Builder) TracingProvider(p *runtimev1.TracingProvider) {\n\tb.TracingProviderFn(p.Rid, tofn(p))\n}\n\nfunc (b *Builder) TracingProviderFn(rid string, fn func() *runtimev1.TracingProvider) {\n\taddResFunc(&b.obs.Tracing, b.rs, rid, fn)\n}\n\nfunc (b *Builder) MetricsProvider(p *runtimev1.MetricsProvider) {\n\tb.MetricsProviderFn(p.Rid, tofn(p))\n}\n\nfunc (b *Builder) MetricsProviderFn(rid string, fn func() *runtimev1.MetricsProvider) {\n\taddResFunc(&b.obs.Metrics, b.rs, rid, fn)\n}\n\nfunc (b *Builder) LogsProvider(p *runtimev1.LogsProvider) {\n\tb.LogsProviderFn(p.Rid, tofn(p))\n}\n\nfunc (b *Builder) LogsProviderFn(rid string, fn func() *runtimev1.LogsProvider) {\n\taddResFunc(&b.obs.Logs, b.rs, rid, fn)\n}\n\nfunc (b *Builder) ServiceConfig(svc *runtimev1.HostedService) {\n\tb.services[svc.Name] = svc\n}\n\nfunc (b *Builder) Deployment(rid string) *Deployment {\n\tif d, ok := b.deployments[rid]; ok {\n\t\treturn d\n\t}\n\td := &Deployment{b: b, rid: rid}\n\tb.deployments[rid] = d\n\treturn d\n}\n\ntype Deployment struct {\n\tb   *Builder\n\trid string\n\n\tdeployID   option.Option[string]\n\tdeployedAt option.Option[time.Time]\n\treduceWith option.Option[*meta.Data]\n\n\tdynamicExperiments []string\n\n\t// The graceful shutdown behavior to use for this deployment.\n\t// If None, uses the default graceful shutdown behavior.\n\tgracefulShutdown option.Option[*runtimev1.GracefulShutdown]\n\n\t// The service-discovery configuration for this deployment.\n\tsd *runtimev1.ServiceDiscovery\n\n\t// The base URL for reaching this deployment from another service.\n\tsvc2svcBaseURL string\n\n\thostedGateways     []string\n\thostedServiceNames []string\n}\n\n// DeployID sets the deploy id.\nfunc (d *Deployment) DeployID(id string) *Deployment {\n\td.deployID = option.Some(id)\n\treturn d\n}\n\n// OverrideDeployedAt sets the time of deploy.\nfunc (d *Deployment) OverrideDeployedAt(t time.Time) *Deployment {\n\td.deployedAt = option.Some(t)\n\treturn d\n}\n\nfunc (d *Deployment) DynamicExperiments(experiments []string) *Deployment {\n\td.dynamicExperiments = experiments\n\treturn d\n}\n\n// HostsServices adds the given service names as being hosted by this deployment.\n// It appends and doesn't overwrite any existing hosted services.\nfunc (d *Deployment) HostsServices(names ...string) *Deployment {\n\td.hostedServiceNames = append(d.hostedServiceNames, names...)\n\treturn d\n}\n\n// HostsGateways adds the given gateway names as being hosted by this deployment.\n// It appends and doesn't overwrite any existing hosted gateways.\nfunc (d *Deployment) HostsGateways(names ...string) *Deployment {\n\td.hostedGateways = append(d.hostedGateways, names...)\n\treturn d\n}\n\n// OverrideGracefulShutdown sets the graceful shutdown behavior for this specific deployment.\n// To set a default graceful shutdown shared for all deployments, use [Builder.DefaultGracefulShutdown] instead.\nfunc (d *Deployment) OverrideGracefulShutdown(s *runtimev1.GracefulShutdown) *Deployment {\n\td.gracefulShutdown = option.Some(s)\n\treturn d\n}\n\nfunc (d *Deployment) ServiceDiscovery(sd *runtimev1.ServiceDiscovery) *Deployment {\n\td.sd = sd\n\treturn d\n}\n\nfunc (d *Deployment) ReduceWithMeta(md *meta.Data) *Deployment {\n\td.reduceWith = option.Some(md)\n\treturn d\n}\n\nfunc (d *Deployment) BuildRuntimeConfig() (*runtimev1.RuntimeConfig, error) {\n\tb := d.b\n\n\tinfra, err := b.Infra.get()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif reduced, ok := d.reduceWith.Get(); ok {\n\t\tinfra = reduceForServices(infra, reduced, d.hostedServiceNames)\n\t}\n\n\tgraceful := d.gracefulShutdown.GetOrElse(d.b.defaultGracefulShutdown)\n\n\tvar hostedServices []*runtimev1.HostedService\n\t{\n\t\tfor _, svcName := range d.hostedServiceNames {\n\t\t\t// If we have a service config defined for this service, use it.\n\t\t\tcfg := b.services[svcName]\n\t\t\tif cfg == nil {\n\t\t\t\tcfg = &runtimev1.HostedService{\n\t\t\t\t\tName: svcName,\n\t\t\t\t}\n\t\t\t}\n\t\t\thostedServices = append(hostedServices, cfg)\n\t\t}\n\t}\n\tslices.SortFunc(hostedServices, func(a, b *runtimev1.HostedService) int {\n\t\treturn cmp.Compare(a.Name, b.Name)\n\t})\n\n\t// Build metrics for this deployment\n\tvar metrics []*runtimev1.Metric\n\tif reduced, ok := d.reduceWith.Get(); ok {\n\t\t// Build a map: metric name -> list of services that use it\n\t\tmetricToServices := make(map[string][]string)\n\n\t\tfor _, svc := range reduced.Svcs {\n\t\t\t// Only include metrics from services hosted by this deployment\n\t\t\tif !slices.Contains(d.hostedServiceNames, svc.Name) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, metricName := range svc.Metrics {\n\t\t\t\tmetricToServices[metricName] = append(metricToServices[metricName], svc.Name)\n\t\t\t}\n\t\t}\n\n\t\t// Convert to Metric protobuf messages\n\t\tfor metricName, services := range metricToServices {\n\t\t\t// Sort services for consistency\n\t\t\tslices.Sort(services)\n\t\t\tmetrics = append(metrics, &runtimev1.Metric{\n\t\t\t\tEncoreName: metricName,\n\t\t\t\tServices:   services,\n\t\t\t})\n\t\t}\n\t}\n\t// Sort metrics by name for consistency\n\tslices.SortFunc(metrics, func(a, b *runtimev1.Metric) int {\n\t\treturn cmp.Compare(a.EncoreName, b.EncoreName)\n\t})\n\n\tgatewaysByName := make(map[string]*runtimev1.Gateway)\n\tfor _, gw := range infra.Resources.Gateways {\n\t\tgatewaysByName[gw.EncoreName] = gw\n\t}\n\n\tgatewayRids := fns.Map(d.hostedGateways, func(name string) string {\n\t\tgw, ok := gatewaysByName[name]\n\t\tif !ok {\n\t\t\tb.setErrf(\"gateway %q not found\", name)\n\t\t\treturn \"\"\n\t\t}\n\t\treturn gw.Rid\n\t})\n\n\tdeploy := &runtimev1.Deployment{\n\t\tHostedGateways:     gatewayRids,\n\t\tHostedServices:     hostedServices,\n\t\tServiceDiscovery:   d.sd,\n\t\tGracefulShutdown:   graceful,\n\t\tDynamicExperiments: d.dynamicExperiments,\n\t\tObservability:      b.obs,\n\t\tAuthMethods:        d.b.authMethods,\n\t\tDeployId:           d.deployID.GetOrElse(b.defaultDeployID),\n\t\tDeployedAt:         timestamppb.New(d.deployedAt.GetOrElse(b.defaultDeployedAt)),\n\t\tMetrics:            metrics,\n\t}\n\n\tcfg := &runtimev1.RuntimeConfig{\n\t\tEnvironment:    b.env,\n\t\tEncorePlatform: b.encore,\n\t\tInfra:          infra,\n\t\tDeployment:     deploy,\n\t}\n\n\t// Deep-clone the protobuf to avoid subsequent mutations from modifying this one.\n\tcfg = cloneProto(cfg)\n\n\treturn cfg, b.err\n}\n\nfunc (b *Builder) setErr(err error) {\n\tif b.err == nil {\n\t\tb.err = err\n\t}\n}\n\nfunc (b *Builder) setErrf(format string, args ...any) {\n\tb.setErr(errors.Newf(format, args...))\n}\n\nfunc cloneProto[M proto.Message](m M) M {\n\treturn proto.Clone(m).(M)\n}\n"
  },
  {
    "path": "pkg/rtconfgen/convert.go",
    "content": "package rtconfgen\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"slices\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\n\t\"go.encore.dev/platform-sdk/pkg/auth\"\n\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/pkg/fns\"\n)\n\nfunc ToLegacy(conf *runtimev1.RuntimeConfig, secretEnvs map[string][]byte) (*config.Runtime, error) {\n\tconv := &legacyConverter{in: conf, secretEnvs: secretEnvs}\n\treturn conv.Convert()\n}\n\ntype legacyConverter struct {\n\tin         *runtimev1.RuntimeConfig\n\tsecretEnvs map[string][]byte\n\terr        error\n}\n\nfunc findRID[T interface{ GetRid() string }](rid string, list []T) (T, bool) {\n\treturn fns.Find(list, func(item T) bool {\n\t\treturn item.GetRid() == rid\n\t})\n}\n\nfunc (c *legacyConverter) Convert() (*config.Runtime, error) {\n\tcfg := &config.Runtime{\n\t\tAppID:              c.in.Environment.AppId,\n\t\tAppSlug:            c.in.Environment.AppSlug,\n\t\tEnvID:              c.in.Environment.EnvId,\n\t\tEnvName:            c.in.Environment.EnvName,\n\t\tEnvType:            string(c.envType()),\n\t\tEnvCloud:           c.envCloud(),\n\t\tDeployID:           c.in.Deployment.DeployId,\n\t\tDeployedAt:         c.in.Deployment.DeployedAt.AsTime(),\n\t\tServiceDiscovery:   nil,\n\t\tServiceAuth:        nil,\n\t\tShutdownTimeout:    0,\n\t\tGracefulShutdown:   nil,\n\t\tDynamicExperiments: nil,\n\t\tGateways:           []config.Gateway{},\n\t\tPubsubTopics:       make(map[string]*config.PubsubTopic),\n\t\tBuckets:            make(map[string]*config.Bucket),\n\t\tCORS:               &config.CORS{},\n\t}\n\n\t// Deployment handling.\n\t{\n\t\tdeployment := c.in.Deployment\n\t\tcfg.HostedServices = fns.Map(deployment.HostedServices, func(s *runtimev1.HostedService) string {\n\t\t\treturn s.Name\n\t\t})\n\n\t\tcfg.ServiceAuth = fns.Map(deployment.AuthMethods, func(sa *runtimev1.ServiceAuth) config.ServiceAuth {\n\t\t\tswitch sa.AuthMethod.(type) {\n\t\t\tcase *runtimev1.ServiceAuth_EncoreAuth_:\n\t\t\t\treturn config.ServiceAuth{Method: \"encore-auth\"}\n\t\t\t}\n\t\t\treturn config.ServiceAuth{Method: \"noop\"}\n\t\t})\n\n\t\tcfg.ServiceDiscovery = make(map[string]config.Service)\n\t\tfor key, value := range deployment.ServiceDiscovery.Services {\n\t\t\tmethod := config.ServiceAuth{Method: \"noop\"}\n\t\t\tif len(value.AuthMethods) > 0 {\n\t\t\t\tif _, ok := value.AuthMethods[0].AuthMethod.(*runtimev1.ServiceAuth_EncoreAuth_); ok {\n\t\t\t\t\tmethod.Method = \"encore-auth\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tcfg.ServiceDiscovery[key] = config.Service{\n\t\t\t\tName:        key,\n\t\t\t\tURL:         value.BaseUrl,\n\t\t\t\tProtocol:    config.Http,\n\t\t\t\tServiceAuth: method,\n\t\t\t}\n\t\t}\n\n\t\tif deployment.GracefulShutdown != nil {\n\t\t\tcfg.GracefulShutdown = &config.GracefulShutdownTimings{\n\t\t\t\tTotal:         ptr(deployment.GracefulShutdown.Total.AsDuration()),\n\t\t\t\tShutdownHooks: ptr(deployment.GracefulShutdown.ShutdownHooks.AsDuration()),\n\t\t\t\tHandlers:      ptr(deployment.GracefulShutdown.Handlers.AsDuration()),\n\t\t\t}\n\t\t\tcfg.ShutdownTimeout = deployment.GracefulShutdown.Total.AsDuration()\n\t\t}\n\t\tcfg.DynamicExperiments = deployment.DynamicExperiments\n\n\t\t// Set the API Base URL if we have a gateway.\n\t\tif len(c.in.Infra.Resources.Gateways) > 0 {\n\t\t\tcfg.APIBaseURL = c.in.Infra.Resources.Gateways[0].BaseUrl\n\t\t}\n\n\t\tfor _, gwRID := range deployment.HostedGateways {\n\t\t\tidx := slices.IndexFunc(c.in.Infra.Resources.Gateways, func(gw *runtimev1.Gateway) bool {\n\t\t\t\treturn gw.Rid == gwRID\n\t\t\t})\n\t\t\tif idx >= 0 {\n\t\t\t\tgw := c.in.Infra.Resources.Gateways[idx]\n\t\t\t\tif gw.Cors != nil {\n\t\t\t\t\tvar allowOriginsWithCredentials []string\n\t\t\t\t\tswitch data := gw.Cors.AllowedOriginsWithCredentials.(type) {\n\t\t\t\t\tcase *runtimev1.Gateway_CORS_AllowedOrigins:\n\t\t\t\t\t\tallowOriginsWithCredentials = data.AllowedOrigins.AllowedOrigins\n\t\t\t\t\tcase *runtimev1.Gateway_CORS_UnsafeAllowAllOriginsWithCredentials:\n\t\t\t\t\t\tif data.UnsafeAllowAllOriginsWithCredentials {\n\t\t\t\t\t\t\tallowOriginsWithCredentials = []string{config.UnsafeAllOriginWithCredentials}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcfg.CORS = &config.CORS{\n\t\t\t\t\t\tDebug:                          gw.Cors.Debug,\n\t\t\t\t\t\tDisableCredentials:             gw.Cors.DisableCredentials,\n\t\t\t\t\t\tAllowOriginsWithCredentials:    allowOriginsWithCredentials,\n\t\t\t\t\t\tAllowOriginsWithoutCredentials: gw.Cors.AllowedOriginsWithoutCredentials.AllowedOrigins,\n\t\t\t\t\t\tExtraAllowedHeaders:            gw.Cors.ExtraAllowedHeaders,\n\t\t\t\t\t\tExtraExposedHeaders:            gw.Cors.ExtraExposedHeaders,\n\t\t\t\t\t\tAllowPrivateNetworkAccess:      gw.Cors.AllowPrivateNetworkAccess,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcfg.Gateways = append(cfg.Gateways, config.Gateway{\n\t\t\t\t\tName: gw.EncoreName,\n\t\t\t\t\tHost: gw.Hostnames[0],\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// Use the most verbose logging requested.\n\t\tcurrLevel := zerolog.PanicLevel\n\t\tfoundLevel := false\n\t\tfor _, svc := range deployment.HostedServices {\n\t\t\tif svc.LogConfig != nil {\n\t\t\t\tif level, err := zerolog.ParseLevel(*svc.LogConfig); err == nil && level < currLevel {\n\t\t\t\t\tcurrLevel = level\n\t\t\t\t\tfoundLevel = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !foundLevel {\n\t\t\tcurrLevel = zerolog.TraceLevel\n\t\t}\n\t\tcfg.LogConfig = currLevel.String()\n\t}\n\n\t// Infrastructure handling.\n\t{\n\t\tres := c.in.Infra.Resources\n\n\t\tgetClientCert := func(certKeyRID *string) (clientCertPEM, clientKeyPem string) {\n\t\t\tif certKeyRID == nil {\n\t\t\t\treturn \"\", \"\"\n\t\t\t}\n\t\t\tcert, ok := findRID(*certKeyRID, c.in.Infra.Credentials.ClientCerts)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", \"\"\n\t\t\t}\n\t\t\treturn cert.GetCert(), c.secretString(cert.GetKey())\n\t\t}\n\n\t\t// SQL Servers & Databases\n\t\t{\n\t\t\tfor _, cluster := range res.SqlClusters {\n\t\t\t\tprimary, ok := fns.Find(cluster.Servers, func(s *runtimev1.SQLServer) bool {\n\t\t\t\t\treturn s.Kind == runtimev1.ServerKind_SERVER_KIND_PRIMARY\n\t\t\t\t})\n\t\t\t\tif !ok {\n\t\t\t\t\tc.setErrf(\"unable to find primary server for SQL cluster %q\", cluster.Rid)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfor _, db := range cluster.Databases {\n\t\t\t\t\t// Find the read-write connection pool.\n\t\t\t\t\tpool, ok := fns.Find(db.ConnPools, func(pool *runtimev1.SQLConnectionPool) bool {\n\t\t\t\t\t\treturn !pool.IsReadonly\n\t\t\t\t\t})\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\t// Use the first pool if none were read-write\n\t\t\t\t\t\tpool = db.ConnPools[0]\n\t\t\t\t\t}\n\n\t\t\t\t\trole, ok := findRID(pool.RoleRid, c.in.Infra.Credentials.SqlRoles)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tc.setErrf(\"unable to find sql role %q\", pool.RoleRid)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tclientCert, clientKey := getClientCert(role.ClientCertRid)\n\t\t\t\t\tcandidateServer := &config.SQLServer{\n\t\t\t\t\t\tHost:       primary.Host,\n\t\t\t\t\t\tClientCert: clientCert,\n\t\t\t\t\t\tClientKey:  clientKey,\n\t\t\t\t\t}\n\t\t\t\t\tif primary.TlsConfig != nil {\n\t\t\t\t\t\tcandidateServer.ServerCACert = primary.TlsConfig.GetServerCaCert()\n\t\t\t\t\t}\n\n\t\t\t\t\tserverIdx := slices.IndexFunc(cfg.SQLServers, func(s *config.SQLServer) bool {\n\t\t\t\t\t\treturn s.Host == candidateServer.Host &&\n\t\t\t\t\t\t\ts.ServerCACert == candidateServer.ServerCACert &&\n\t\t\t\t\t\t\ts.ClientCert == candidateServer.ClientCert &&\n\t\t\t\t\t\t\ts.ClientKey == candidateServer.ClientKey\n\t\t\t\t\t})\n\t\t\t\t\tif serverIdx == -1 {\n\t\t\t\t\t\tserverIdx = len(cfg.SQLServers)\n\t\t\t\t\t\tcfg.SQLServers = append(cfg.SQLServers, candidateServer)\n\t\t\t\t\t}\n\n\t\t\t\t\tcfg.SQLDatabases = append(cfg.SQLDatabases, &config.SQLDatabase{\n\t\t\t\t\t\tServerID:       serverIdx,\n\t\t\t\t\t\tEncoreName:     db.EncoreName,\n\t\t\t\t\t\tDatabaseName:   db.CloudName,\n\t\t\t\t\t\tUser:           role.Username,\n\t\t\t\t\t\tPassword:       c.secretString(role.Password),\n\t\t\t\t\t\tMinConnections: int(pool.MinConnections),\n\t\t\t\t\t\tMaxConnections: int(pool.MaxConnections),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Redis Servers & Databases\n\t\t{\n\t\t\tfor _, cluster := range res.RedisClusters {\n\t\t\t\tif cluster.InMemory {\n\t\t\t\t\tidx := len(cfg.RedisServers)\n\t\t\t\t\tcfg.RedisServers = append(cfg.RedisServers, &config.RedisServer{\n\t\t\t\t\t\tInMemory: true,\n\t\t\t\t\t})\n\t\t\t\t\tfor i, db := range cluster.Databases {\n\t\t\t\t\t\tcfg.RedisDatabases = append(cfg.RedisDatabases, &config.RedisDatabase{\n\t\t\t\t\t\t\tServerID:       idx,\n\t\t\t\t\t\t\tEncoreName:     db.EncoreName,\n\t\t\t\t\t\t\tDatabase:       i,\n\t\t\t\t\t\t\tMinConnections: 0,\n\t\t\t\t\t\t\tMaxConnections: 0,\n\t\t\t\t\t\t\tKeyPrefix:      \"\",\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tprimary, ok := fns.Find(cluster.Servers, func(s *runtimev1.RedisServer) bool {\n\t\t\t\t\treturn s.Kind == runtimev1.ServerKind_SERVER_KIND_PRIMARY\n\t\t\t\t})\n\t\t\t\tif !ok {\n\t\t\t\t\tc.setErrf(\"unable to find primary server for Redis cluster %q\", cluster.Rid)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfor _, db := range cluster.Databases {\n\t\t\t\t\t// Find the read-write connection pool.\n\t\t\t\t\tpool, ok := fns.Find(db.ConnPools, func(pool *runtimev1.RedisConnectionPool) bool {\n\t\t\t\t\t\treturn !pool.IsReadonly\n\t\t\t\t\t})\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\t// Use the first pool if none were read-write\n\t\t\t\t\t\tpool = db.ConnPools[0]\n\t\t\t\t\t}\n\n\t\t\t\t\trole, ok := findRID(pool.RoleRid, c.in.Infra.Credentials.RedisRoles)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tc.setErrf(\"unable to find Redis role %q\", pool.RoleRid)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tuser, password := func() (string, string) {\n\t\t\t\t\t\tswitch s := role.Auth.(type) {\n\t\t\t\t\t\tcase nil:\n\t\t\t\t\t\t\treturn \"\", \"\" // no authentication\n\t\t\t\t\t\tcase *runtimev1.RedisRole_Acl:\n\t\t\t\t\t\t\treturn s.Acl.Username, c.secretString(s.Acl.Password)\n\t\t\t\t\t\tcase *runtimev1.RedisRole_AuthString:\n\t\t\t\t\t\t\treturn \"\", c.secretString(s.AuthString)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tc.setErrf(\"unknown redis auth type %T\", s)\n\t\t\t\t\t\t\treturn \"\", \"\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}()\n\n\t\t\t\t\tclientCert, clientKey := getClientCert(role.ClientCertRid)\n\t\t\t\t\tcandidateServer := &config.RedisServer{\n\t\t\t\t\t\tHost:       primary.Host,\n\t\t\t\t\t\tClientCert: clientCert,\n\t\t\t\t\t\tClientKey:  clientKey,\n\t\t\t\t\t\tUser:       user,\n\t\t\t\t\t\tPassword:   password,\n\t\t\t\t\t}\n\t\t\t\t\tif primary.TlsConfig != nil {\n\t\t\t\t\t\tcandidateServer.EnableTLS = true\n\t\t\t\t\t\tcandidateServer.ServerCACert = primary.TlsConfig.GetServerCaCert()\n\t\t\t\t\t}\n\n\t\t\t\t\tserverIdx := slices.IndexFunc(cfg.RedisServers, func(s *config.RedisServer) bool {\n\t\t\t\t\t\treturn reflect.DeepEqual(s, candidateServer)\n\t\t\t\t\t})\n\t\t\t\t\tif serverIdx == -1 {\n\t\t\t\t\t\tserverIdx = len(cfg.RedisServers)\n\t\t\t\t\t\tcfg.RedisServers = append(cfg.RedisServers, candidateServer)\n\t\t\t\t\t}\n\n\t\t\t\t\tcfg.RedisDatabases = append(cfg.RedisDatabases, &config.RedisDatabase{\n\t\t\t\t\t\tServerID:       serverIdx,\n\t\t\t\t\t\tEncoreName:     db.EncoreName,\n\t\t\t\t\t\tDatabase:       int(db.DatabaseIdx),\n\t\t\t\t\t\tMinConnections: int(pool.MinConnections),\n\t\t\t\t\t\tMaxConnections: int(pool.MaxConnections),\n\t\t\t\t\t\tKeyPrefix:      nilPtrToZero(db.KeyPrefix),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t// Pubsub Providers & Topics\n\t\t{\n\t\t\tfor _, cluster := range c.in.Infra.Resources.PubsubClusters {\n\t\t\t\tp := &config.PubsubProvider{}\n\t\t\t\tswitch prov := cluster.Provider.(type) {\n\t\t\t\tcase *runtimev1.PubSubCluster_Encore:\n\t\t\t\t\tp.EncoreCloud = &config.EncoreCloudPubsubProvider{}\n\t\t\t\tcase *runtimev1.PubSubCluster_Aws:\n\t\t\t\t\tp.AWS = &config.AWSPubsubProvider{}\n\t\t\t\tcase *runtimev1.PubSubCluster_Gcp:\n\t\t\t\t\tp.GCP = &config.GCPPubsubProvider{}\n\t\t\t\tcase *runtimev1.PubSubCluster_Nsq:\n\t\t\t\t\tp.NSQ = &config.NSQProvider{Host: prov.Nsq.Hosts[0]}\n\t\t\t\tcase *runtimev1.PubSubCluster_Azure:\n\t\t\t\t\tp.Azure = &config.AzureServiceBusProvider{Namespace: prov.Azure.Namespace}\n\t\t\t\tdefault:\n\t\t\t\t\tc.setErrf(\"unknown pubsub provider type %T\", prov)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tproviderID := len(cfg.PubsubProviders)\n\t\t\t\tcfg.PubsubProviders = append(cfg.PubsubProviders, p)\n\t\t\t\tfor _, top := range cluster.Topics {\n\t\t\t\t\tcfg.PubsubTopics[top.EncoreName] = &config.PubsubTopic{\n\t\t\t\t\t\tEncoreName:    top.EncoreName,\n\t\t\t\t\t\tProviderID:    providerID,\n\t\t\t\t\t\tProviderName:  top.CloudName,\n\t\t\t\t\t\tLimiter:       nil, // TODO?\n\t\t\t\t\t\tSubscriptions: make(map[string]*config.PubsubSubscription),\n\t\t\t\t\t\tGCP: func() *config.PubsubTopicGCPData {\n\t\t\t\t\t\t\tswitch pc := top.ProviderConfig.(type) {\n\t\t\t\t\t\t\tcase *runtimev1.PubSubTopic_GcpConfig:\n\t\t\t\t\t\t\t\treturn &config.PubsubTopicGCPData{\n\t\t\t\t\t\t\t\t\tProjectID: pc.GcpConfig.ProjectId,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}(),\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor _, sub := range cluster.Subscriptions {\n\t\t\t\t\ttopic := cfg.PubsubTopics[sub.TopicEncoreName]\n\t\t\t\t\tif topic == nil {\n\t\t\t\t\t\t// In the new config we could end up with a subscription where the\n\t\t\t\t\t\t// corresponding topic wasn't included, as we only include what's needed.\n\t\t\t\t\t\t// That doesn't work with the legacy metadata, so add it here in that case.\n\t\t\t\t\t\ttopic = &config.PubsubTopic{\n\t\t\t\t\t\t\tEncoreName:    sub.TopicEncoreName,\n\t\t\t\t\t\t\tProviderID:    providerID,\n\t\t\t\t\t\t\tProviderName:  sub.TopicCloudName,\n\t\t\t\t\t\t\tLimiter:       nil,\n\t\t\t\t\t\t\tSubscriptions: make(map[string]*config.PubsubSubscription),\n\t\t\t\t\t\t\tGCP: func() *config.PubsubTopicGCPData {\n\t\t\t\t\t\t\t\t// HACK: this synthesizes a topic config based on the subscription's config.\n\t\t\t\t\t\t\t\t// That's not correct in the general case, but we only get here\n\t\t\t\t\t\t\t\t// if the service doesn't have access to the topic in the first place,\n\t\t\t\t\t\t\t\t// so this should be fine and prevents the runtime from exploding since\n\t\t\t\t\t\t\t\t// it doesn't expect to get a nil topic config.\n\t\t\t\t\t\t\t\tswitch sub.ProviderConfig.(type) {\n\t\t\t\t\t\t\t\tcase *runtimev1.PubSubSubscription_GcpConfig:\n\t\t\t\t\t\t\t\t\treturn &config.PubsubTopicGCPData{\n\t\t\t\t\t\t\t\t\t\tProjectID: \"\",\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}(),\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcfg.PubsubTopics[sub.TopicEncoreName] = topic\n\t\t\t\t\t}\n\n\t\t\t\t\ttopic.Subscriptions[sub.SubscriptionEncoreName] = &config.PubsubSubscription{\n\t\t\t\t\t\tID:           sub.Rid,\n\t\t\t\t\t\tEncoreName:   sub.SubscriptionEncoreName,\n\t\t\t\t\t\tProviderName: sub.SubscriptionCloudName,\n\t\t\t\t\t\tPushOnly:     sub.PushOnly,\n\t\t\t\t\t\tGCP: func() *config.PubsubSubscriptionGCPData {\n\t\t\t\t\t\t\tswitch pc := sub.ProviderConfig.(type) {\n\t\t\t\t\t\t\tcase *runtimev1.PubSubSubscription_GcpConfig:\n\t\t\t\t\t\t\t\treturn &config.PubsubSubscriptionGCPData{\n\t\t\t\t\t\t\t\t\tProjectID:          pc.GcpConfig.ProjectId,\n\t\t\t\t\t\t\t\t\tPushServiceAccount: pc.GcpConfig.GetPushServiceAccount(),\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}(),\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Cloud Storage\n\t\t{\n\t\t\tfor _, cluster := range c.in.Infra.Resources.BucketClusters {\n\t\t\t\tp := &config.BucketProvider{}\n\t\t\t\tswitch prov := cluster.Provider.(type) {\n\t\t\t\tcase *runtimev1.BucketCluster_S3_:\n\t\t\t\t\tp.S3 = &config.S3BucketProvider{\n\t\t\t\t\t\tRegion:          prov.S3.GetRegion(),\n\t\t\t\t\t\tEndpoint:        prov.S3.Endpoint,\n\t\t\t\t\t\tAccessKeyID:     prov.S3.AccessKeyId,\n\t\t\t\t\t\tSecretAccessKey: ptrOrNil(c.secretString(prov.S3.SecretAccessKey)),\n\t\t\t\t\t}\n\t\t\t\tcase *runtimev1.BucketCluster_Gcs:\n\t\t\t\t\tp.GCS = &config.GCSBucketProvider{\n\t\t\t\t\t\tEndpoint:  prov.Gcs.GetEndpoint(),\n\t\t\t\t\t\tAnonymous: prov.Gcs.Anonymous,\n\t\t\t\t\t}\n\t\t\t\t\tif opt := prov.Gcs.LocalSign; opt != nil {\n\t\t\t\t\t\tp.GCS.LocalSign = &config.GCSLocalSignOptions{\n\t\t\t\t\t\t\tBaseURL:    opt.BaseUrl,\n\t\t\t\t\t\t\tAccessID:   opt.AccessId,\n\t\t\t\t\t\t\tPrivateKey: opt.PrivateKey,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tc.setErrf(\"unknown object storage provider type %T\", prov)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tproviderID := len(cfg.BucketProviders)\n\t\t\t\tcfg.BucketProviders = append(cfg.BucketProviders, p)\n\t\t\t\tfor _, bkt := range cluster.Buckets {\n\t\t\t\t\tcfg.Buckets[bkt.EncoreName] = &config.Bucket{\n\t\t\t\t\t\tProviderID:    providerID,\n\t\t\t\t\t\tEncoreName:    bkt.EncoreName,\n\t\t\t\t\t\tCloudName:     bkt.CloudName,\n\t\t\t\t\t\tKeyPrefix:     bkt.GetKeyPrefix(),\n\t\t\t\t\t\tPublicBaseURL: bkt.GetPublicBaseUrl(),\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\t// Observability.\n\t{\n\t\tobs := c.in.Deployment.Observability\n\t\tif len(obs.Metrics) > 0 {\n\t\t\tprov := obs.Metrics[0]\n\t\t\tm := &config.Metrics{\n\t\t\t\tCollectionInterval: prov.CollectionInterval.AsDuration(),\n\t\t\t}\n\t\t\tswitch p := prov.Provider.(type) {\n\t\t\tcase *runtimev1.MetricsProvider_Gcp:\n\t\t\t\tpp := p.Gcp\n\t\t\t\tm.CloudMonitoring = &config.GCPCloudMonitoringProvider{\n\t\t\t\t\tProjectID:               pp.ProjectId,\n\t\t\t\t\tMonitoredResourceType:   pp.MonitoredResourceType,\n\t\t\t\t\tMonitoredResourceLabels: pp.MonitoredResourceLabels,\n\t\t\t\t\tMetricNames:             pp.MetricNames,\n\t\t\t\t}\n\t\t\tcase *runtimev1.MetricsProvider_EncoreCloud:\n\t\t\t\tpp := p.EncoreCloud\n\t\t\t\tm.EncoreCloud = &config.GCPCloudMonitoringProvider{\n\t\t\t\t\tProjectID:               pp.ProjectId,\n\t\t\t\t\tMonitoredResourceType:   pp.MonitoredResourceType,\n\t\t\t\t\tMonitoredResourceLabels: pp.MonitoredResourceLabels,\n\t\t\t\t\tMetricNames:             pp.MetricNames,\n\t\t\t\t}\n\t\t\tcase *runtimev1.MetricsProvider_Aws:\n\t\t\t\tpp := p.Aws\n\t\t\t\tm.CloudWatch = &config.AWSCloudWatchMetricsProvider{Namespace: pp.Namespace}\n\t\t\tcase *runtimev1.MetricsProvider_Datadog_:\n\t\t\t\tpp := p.Datadog\n\t\t\t\tm.Datadog = &config.DatadogProvider{\n\t\t\t\t\tAPIKey: c.secretString(pp.ApiKey),\n\t\t\t\t\tSite:   pp.Site,\n\t\t\t\t}\n\n\t\t\tcase *runtimev1.MetricsProvider_PromRemoteWrite:\n\t\t\t\tpp := p.PromRemoteWrite\n\t\t\t\tm.Prometheus = &config.PrometheusRemoteWriteProvider{\n\t\t\t\t\tRemoteWriteURL: c.secretString(pp.RemoteWriteUrl),\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\tc.setErrf(\"unknown metrics provider type %T\", p)\n\t\t\t}\n\n\t\t\tcfg.Metrics = m\n\t\t}\n\n\t\t// Add the Encore Tracing endpoint, if any.\n\t\tfor _, prov := range obs.Tracing {\n\t\t\tif enc := prov.GetEncore(); enc != nil {\n\t\t\t\tcfg.TraceEndpoint = enc.TraceEndpoint\n\t\t\t\tcfg.TraceSamplingConfig = convertSamplingConfig(enc.SamplingConfig)\n\t\t\t\tcfg.TraceSamplingRate = enc.SamplingRate //nolint:staticcheck // backward compat\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif c.in.EncorePlatform != nil {\n\t\tcfg.AuthKeys = c.authKeys(c.in.EncorePlatform.PlatformSigningKeys)\n\t}\n\n\tif ec := c.in.EncorePlatform.GetEncoreCloud(); ec != nil {\n\t\tcfg.EncoreCloudAPI = &config.EncoreCloudAPI{\n\t\t\tServer: ec.ServerUrl,\n\t\t\tAuthKeys: fns.Map(c.authKeys(ec.AuthKeys), func(k config.EncoreAuthKey) auth.Key {\n\t\t\t\treturn auth.Key{KeyID: k.KeyID, Data: k.Data}\n\t\t\t}),\n\t\t}\n\t}\n\n\tif c.err != nil {\n\t\treturn nil, c.err\n\t}\n\treturn cfg, nil\n}\n\n// convertSamplingConfig converts the typed proto SamplingConfig entries\n// to a map[string]float64 with prefixed keys:\n//   - \"_\" for the global default\n//   - \"service:<name>\" for service-level\n//   - \"endpoint:<service>.<endpoint>\" for endpoint-level\n//   - \"topic:<name>\" for topic-level\n//   - \"subscription:<topic>.<subscription>\" for subscription-level\nfunc convertSamplingConfig(configs []*runtimev1.TracingProvider_SamplingConfig) map[string]float64 {\n\tif len(configs) == 0 {\n\t\treturn nil\n\t}\n\tm := make(map[string]float64, len(configs))\n\tfor _, sc := range configs {\n\t\tswitch s := sc.Scope.(type) {\n\t\tcase *runtimev1.TracingProvider_SamplingConfig_Default:\n\t\t\tm[\"_\"] = sc.Rate\n\t\tcase *runtimev1.TracingProvider_SamplingConfig_Service:\n\t\t\tm[\"service:\"+s.Service] = sc.Rate\n\t\tcase *runtimev1.TracingProvider_SamplingConfig_Endpoint_:\n\t\t\tm[\"endpoint:\"+s.Endpoint.Service+\".\"+s.Endpoint.Endpoint] = sc.Rate\n\t\tcase *runtimev1.TracingProvider_SamplingConfig_Topic:\n\t\t\tm[\"topic:\"+s.Topic] = sc.Rate\n\t\tcase *runtimev1.TracingProvider_SamplingConfig_PubsubSubscription:\n\t\t\tm[\"subscription:\"+s.PubsubSubscription.Topic+\".\"+s.PubsubSubscription.Subscription] = sc.Rate\n\t\t}\n\t}\n\treturn m\n}\n\nfunc (c *legacyConverter) envType() encore.EnvironmentType {\n\tswitch c.in.Environment.EnvType {\n\tcase runtimev1.Environment_TYPE_DEVELOPMENT:\n\t\treturn encore.EnvDevelopment\n\tcase runtimev1.Environment_TYPE_PRODUCTION:\n\t\treturn encore.EnvProduction\n\tcase runtimev1.Environment_TYPE_EPHEMERAL:\n\t\treturn encore.EnvEphemeral\n\tcase runtimev1.Environment_TYPE_TEST:\n\t\treturn encore.EnvTest\n\tdefault:\n\t\tc.setErrf(\"unknown environment type %+v\", c.in.Environment.EnvType)\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *legacyConverter) envCloud() encore.CloudProvider {\n\tswitch c.in.Environment.Cloud {\n\tcase runtimev1.Environment_CLOUD_LOCAL:\n\t\treturn encore.CloudLocal\n\tcase runtimev1.Environment_CLOUD_AWS:\n\t\treturn encore.CloudAWS\n\tcase runtimev1.Environment_CLOUD_GCP:\n\t\treturn encore.CloudGCP\n\tcase runtimev1.Environment_CLOUD_AZURE:\n\t\treturn encore.CloudAzure\n\tcase runtimev1.Environment_CLOUD_ENCORE:\n\t\treturn encore.EncoreCloud\n\tdefault:\n\t\tc.setErrf(\"unknown cloud %+v\", c.in.Environment.Cloud)\n\t\treturn \"\"\n\t}\n}\n\nfunc (c *legacyConverter) authKeys(keys []*runtimev1.EncoreAuthKey) []config.EncoreAuthKey {\n\treturn fns.Map(keys, func(k *runtimev1.EncoreAuthKey) config.EncoreAuthKey {\n\t\treturn config.EncoreAuthKey{\n\t\t\tKeyID: k.Id,\n\t\t\tData:  c.secretBytes(k.Data),\n\t\t}\n\t})\n}\n\nfunc (c *legacyConverter) limiter(lim *runtimev1.RateLimiter) *config.Limiter {\n\tif lim == nil {\n\t\treturn nil\n\t}\n\n\tswitch lim := lim.Kind.(type) {\n\tcase *runtimev1.RateLimiter_TokenBucket_:\n\t\treturn &config.Limiter{\n\t\t\tTokenBucket: &config.TokenBucketLimiter{\n\t\t\t\tPerSecondRate: lim.TokenBucket.Rate,\n\t\t\t\tBucketSize:    int(lim.TokenBucket.Burst),\n\t\t\t},\n\t\t}\n\tdefault:\n\t\tc.setErrf(\"unknown rate limiter type %T\", lim)\n\t\treturn nil\n\t}\n}\n\nfunc (c *legacyConverter) secretString(s *runtimev1.SecretData) string {\n\treturn string(c.secretBytes(s))\n}\n\nfunc (c *legacyConverter) secretBytes(s *runtimev1.SecretData) []byte {\n\tif s == nil {\n\t\treturn nil\n\t}\n\n\t// First resolve the secret data\n\tvar secretData []byte\n\tswitch data := s.Source.(type) {\n\tcase *runtimev1.SecretData_Embedded:\n\t\tsecretData = data.Embedded\n\tcase *runtimev1.SecretData_Env:\n\t\tval, ok := c.secretEnvs[data.Env]\n\t\tif !ok {\n\t\t\tc.setErrf(\"missing secret env var %q\", data.Env)\n\t\t}\n\t\tsecretData = val\n\tdefault:\n\t\tc.setErrf(\"unknown secret data type %T\", data)\n\t\treturn nil\n\t}\n\n\t// Resolve a sub-path, if any.\n\tswitch sub := s.SubPath.(type) {\n\tcase nil:\n\t\t// No sub-path defined.\n\t\treturn secretData\n\n\tcase *runtimev1.SecretData_JsonKey:\n\t\tjsonObj := map[string]any{}\n\t\tif err := json.Unmarshal(secretData, &jsonObj); err != nil {\n\t\t\tc.setErrf(\"secret data is not valid json: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t\tval, ok := jsonObj[sub.JsonKey]\n\t\tif !ok {\n\t\t\tc.setErrf(\"missing json key %q\", sub.JsonKey)\n\t\t}\n\t\tswitch val := val.(type) {\n\t\tcase string:\n\t\t\treturn []byte(val)\n\t\tcase map[string]any:\n\t\t\tbaseVal, ok := val[\"bytes\"]\n\t\t\tif !ok {\n\t\t\t\tpanic(\"missing bytes key\")\n\t\t\t}\n\t\t\tb64Str, ok := baseVal.(string)\n\t\t\tif !ok {\n\t\t\t\tpanic(\"bytes key is not a string\")\n\t\t\t}\n\t\t\tbytes, err := base64.StdEncoding.DecodeString(b64Str)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\treturn bytes\n\t\tdefault:\n\t\t\tpanic(\"unexpected value type\")\n\t\t}\n\tdefault:\n\t\tc.setErrf(\"unknown secret sub-path type %T\", s)\n\t\treturn nil\n\t}\n}\n\nfunc (c *legacyConverter) setErr(err error) {\n\tif c.err == nil {\n\t\tc.err = err\n\t}\n}\n\nfunc (c *legacyConverter) setErrf(format string, args ...any) {\n\tc.setErr(errors.Newf(format, args...))\n}\n\nfunc nilPtrToZero[T comparable](val *T) T {\n\tif val == nil {\n\t\tvar zero T\n\t\treturn zero\n\t}\n\treturn *val\n}\n\nfunc ptr[T comparable](val T) *T {\n\treturn &val\n}\n\nfunc ptrOrNil[T comparable](val T) *T {\n\tvar zero T\n\tif val == zero {\n\t\treturn nil\n\t}\n\treturn &val\n}\n\nfunc randomMapValue[K comparable, V any](m map[K]V) (V, bool) {\n\tfor _, v := range m {\n\t\treturn v, true\n\t}\n\tvar zero V\n\treturn zero, false\n}\n"
  },
  {
    "path": "pkg/rtconfgen/infra_builder.go",
    "content": "package rtconfgen\n\nimport (\n\t\"slices\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n)\n\ntype InfraBuilder struct {\n\tinfra *runtimev1.Infrastructure\n\trs    *resourceSet\n}\n\nfunc newInfraBuilder(rs *resourceSet) *InfraBuilder {\n\tinfra := &runtimev1.Infrastructure{\n\t\tCredentials: &runtimev1.Infrastructure_Credentials{},\n\t\tResources:   &runtimev1.Infrastructure_Resources{},\n\t}\n\treturn &InfraBuilder{\n\t\tinfra: infra,\n\t\trs:    rs,\n\t}\n}\n\nfunc (b *InfraBuilder) ClientCert(rid string, fn func() *runtimev1.ClientCert) *ClientCert {\n\tval := addResFunc(&b.infra.Credentials.ClientCerts, b.rs, rid, fn)\n\treturn &ClientCert{Val: val, b: b}\n}\n\ntype ClientCert struct {\n\tVal *runtimev1.ClientCert\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) SQLRole(p *runtimev1.SQLRole) *SQLRole {\n\treturn b.SQLRoleFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) SQLRoleFn(rid string, fn func() *runtimev1.SQLRole) *SQLRole {\n\tval := addResFunc(&b.infra.Credentials.SqlRoles, b.rs, rid, fn)\n\treturn &SQLRole{Val: val, b: b}\n}\n\ntype SQLRole struct {\n\tVal *runtimev1.SQLRole\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) SQLCluster(p *runtimev1.SQLCluster) *SQLCluster {\n\treturn b.SQLClusterFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) SQLClusterFn(rid string, fn func() *runtimev1.SQLCluster) *SQLCluster {\n\tval := addResFunc(&b.infra.Resources.SqlClusters, b.rs, rid, fn)\n\treturn &SQLCluster{Val: val, b: b}\n}\n\ntype SQLCluster struct {\n\tVal *runtimev1.SQLCluster\n\tb   *InfraBuilder\n}\n\nfunc (c *SQLCluster) SQLDatabase(p *runtimev1.SQLDatabase) *SQLDatabase {\n\treturn c.SQLDatabaseFn(p.Rid, tofn(p))\n}\n\nfunc (c *SQLCluster) SQLDatabaseFn(rid string, fn func() *runtimev1.SQLDatabase) *SQLDatabase {\n\tval := addResFunc(&c.Val.Databases, c.b.rs, rid, fn)\n\treturn &SQLDatabase{Val: val, b: c.b}\n}\n\ntype SQLDatabase struct {\n\tVal *runtimev1.SQLDatabase\n\tb   *InfraBuilder\n}\n\nfunc (c *SQLDatabase) AddConnectionPool(p *runtimev1.SQLConnectionPool) {\n\tc.Val.ConnPools = append(c.Val.ConnPools, p)\n}\n\nfunc (c *SQLCluster) SQLServer(p *runtimev1.SQLServer) *SQLServer {\n\treturn c.SQLServerFn(p.Rid, tofn(p))\n}\n\nfunc (c *SQLCluster) SQLServerFn(rid string, fn func() *runtimev1.SQLServer) *SQLServer {\n\tval := addResFunc(&c.Val.Servers, c.b.rs, rid, fn)\n\treturn &SQLServer{Val: val, b: c.b}\n}\n\ntype SQLServer struct {\n\tVal *runtimev1.SQLServer\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) PubSubCluster(p *runtimev1.PubSubCluster) *PubSubCluster {\n\treturn b.PubSubClusterFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) PubSubClusterFn(rid string, fn func() *runtimev1.PubSubCluster) *PubSubCluster {\n\tval := addResFunc(&b.infra.Resources.PubsubClusters, b.rs, rid, fn)\n\treturn &PubSubCluster{Val: val, b: b}\n}\n\ntype PubSubCluster struct {\n\tVal *runtimev1.PubSubCluster\n\tb   *InfraBuilder\n}\n\nfunc (c *PubSubCluster) PubSubTopic(p *runtimev1.PubSubTopic) *PubSubTopic {\n\treturn c.PubSubTopicFn(p.Rid, tofn(p))\n}\n\nfunc (c *PubSubCluster) PubSubTopicFn(rid string, fn func() *runtimev1.PubSubTopic) *PubSubTopic {\n\tval := addResFunc(&c.Val.Topics, c.b.rs, rid, fn)\n\treturn &PubSubTopic{Val: val, b: c.b}\n}\n\ntype PubSubTopic struct {\n\tVal *runtimev1.PubSubTopic\n\tb   *InfraBuilder\n}\n\nfunc (c *PubSubCluster) PubSubSubscription(p *runtimev1.PubSubSubscription) *PubSubSubscription {\n\treturn c.PubSubSubscriptionFn(p.Rid, tofn(p))\n}\n\nfunc (c *PubSubCluster) PubSubSubscriptionFn(rid string, fn func() *runtimev1.PubSubSubscription) *PubSubSubscription {\n\tval := addResFunc(&c.Val.Subscriptions, c.b.rs, rid, fn)\n\treturn &PubSubSubscription{Val: val, b: c.b}\n}\n\ntype PubSubSubscription struct {\n\tVal *runtimev1.PubSubSubscription\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) RedisRole(p *runtimev1.RedisRole) *RedisRole {\n\treturn b.RedisRoleFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) RedisRoleFn(rid string, fn func() *runtimev1.RedisRole) *RedisRole {\n\tval := addResFunc(&b.infra.Credentials.RedisRoles, b.rs, rid, fn)\n\treturn &RedisRole{Val: val, b: b}\n}\n\ntype RedisRole struct {\n\tVal *runtimev1.RedisRole\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) RedisCluster(p *runtimev1.RedisCluster) *RedisCluster {\n\treturn b.RedisClusterFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) RedisClusterFn(rid string, fn func() *runtimev1.RedisCluster) *RedisCluster {\n\tval := addResFunc(&b.infra.Resources.RedisClusters, b.rs, rid, fn)\n\treturn &RedisCluster{Val: val, b: b}\n}\n\ntype RedisCluster struct {\n\tVal *runtimev1.RedisCluster\n\tb   *InfraBuilder\n}\n\nfunc (c *RedisCluster) RedisDatabase(p *runtimev1.RedisDatabase) *RedisDatabase {\n\treturn c.RedisDatabaseFn(p.Rid, tofn(p))\n}\n\nfunc (c *RedisCluster) RedisDatabaseFn(rid string, fn func() *runtimev1.RedisDatabase) *RedisDatabase {\n\tval := addResFunc(&c.Val.Databases, c.b.rs, rid, fn)\n\treturn &RedisDatabase{Val: val, b: c.b}\n}\n\ntype RedisDatabase struct {\n\tVal *runtimev1.RedisDatabase\n\tb   *InfraBuilder\n}\n\nfunc (c *RedisDatabase) AddConnectionPool(p *runtimev1.RedisConnectionPool) {\n\tc.Val.ConnPools = append(c.Val.ConnPools, p)\n}\n\nfunc (c *RedisCluster) RedisServer(p *runtimev1.RedisServer) *RedisServer {\n\treturn c.RedisServerFn(p.Rid, tofn(p))\n}\n\nfunc (c *RedisCluster) RedisServerFn(rid string, fn func() *runtimev1.RedisServer) *RedisServer {\n\tval := addResFunc(&c.Val.Servers, c.b.rs, rid, fn)\n\treturn &RedisServer{Val: val, b: c.b}\n}\n\ntype RedisServer struct {\n\tVal *runtimev1.RedisServer\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) BucketCluster(p *runtimev1.BucketCluster) *BucketCluster {\n\treturn b.BucketClusterFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) BucketClusterFn(rid string, fn func() *runtimev1.BucketCluster) *BucketCluster {\n\tval := addResFunc(&b.infra.Resources.BucketClusters, b.rs, rid, fn)\n\treturn &BucketCluster{Val: val, b: b}\n}\n\ntype BucketCluster struct {\n\tVal *runtimev1.BucketCluster\n\tb   *InfraBuilder\n}\n\nfunc (c *BucketCluster) Bucket(p *runtimev1.Bucket) *Bucket {\n\treturn c.BucketFn(p.Rid, tofn(p))\n}\n\nfunc (c *BucketCluster) BucketFn(rid string, fn func() *runtimev1.Bucket) *Bucket {\n\tval := addResFunc(&c.Val.Buckets, c.b.rs, rid, fn)\n\treturn &Bucket{Val: val, b: c.b}\n}\n\ntype Bucket struct {\n\tVal *runtimev1.Bucket\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) Gateway(gw *runtimev1.Gateway) *Gateway {\n\treturn b.GatewayFn(gw.Rid, tofn(gw))\n}\n\nfunc (b *InfraBuilder) GatewayFn(rid string, fn func() *runtimev1.Gateway) *Gateway {\n\tval := addResFunc(&b.infra.Resources.Gateways, b.rs, rid, fn)\n\treturn &Gateway{Val: val, b: b}\n}\n\ntype Gateway struct {\n\tVal *runtimev1.Gateway\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) AppSecret(p *runtimev1.AppSecret) *AppSecret {\n\treturn b.AppSecretFn(p.Rid, tofn(p))\n}\n\nfunc (b *InfraBuilder) AppSecretFn(rid string, fn func() *runtimev1.AppSecret) *AppSecret {\n\tval := addResFunc(&b.infra.Resources.AppSecrets, b.rs, rid, fn)\n\treturn &AppSecret{Val: val, b: b}\n}\n\ntype AppSecret struct {\n\tVal *runtimev1.AppSecret\n\tb   *InfraBuilder\n}\n\nfunc (b *InfraBuilder) get() (*runtimev1.Infrastructure, error) {\n\treturn b.infra, nil\n}\n\nfunc tofn[V any](v V) func() V {\n\treturn func() V { return v }\n}\n\n// reduceForServices reduces the given infrastructure to only include resource accessible by\n// the given services, using the metadata for access control.\nfunc reduceForServices(infra *runtimev1.Infrastructure, md *meta.Data, svcs []string) *runtimev1.Infrastructure {\n\t// Clone the protobuf so the changes don't affect the original.\n\tinfra = cloneProto(infra)\n\n\tsvcNames := make(map[string]bool)\n\tfor _, svc := range svcs {\n\t\tsvcNames[svc] = true\n\t}\n\n\tdbsToKeep := make(map[string]bool)\n\tfor _, svc := range md.Svcs {\n\t\tif !svcNames[svc.Name] {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, dbName := range svc.Databases {\n\t\t\tdbsToKeep[dbName] = true\n\t\t}\n\t}\n\n\tbucketsToKeep := make(map[string]bool)\n\tfor _, svc := range md.Svcs {\n\t\tif !svcNames[svc.Name] {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, bktName := range svc.Buckets {\n\t\t\tbucketsToKeep[bktName.Bucket] = true\n\t\t}\n\t}\n\n\ttype subKey struct {\n\t\ttopicName string\n\t\tsubName   string\n\t}\n\ttopicsToKeep := make(map[string]bool)\n\tsubsToKeep := make(map[subKey]bool)\n\tfor _, topic := range md.PubsubTopics {\n\t\tfor _, publisher := range topic.Publishers {\n\t\t\tif svcNames[publisher.ServiceName] {\n\t\t\t\ttopicsToKeep[topic.Name] = true\n\t\t\t}\n\t\t}\n\n\t\tfor _, subscriber := range topic.Subscriptions {\n\t\t\tif svcNames[subscriber.ServiceName] {\n\t\t\t\tsubsToKeep[subKey{topicName: topic.Name, subName: subscriber.Name}] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tcachesToKeep := make(map[string]bool)\n\tfor _, cacheCluster := range md.CacheClusters {\n\t\tfor _, keySpace := range cacheCluster.Keyspaces {\n\t\t\tif svcNames[keySpace.Service] {\n\t\t\t\tcachesToKeep[cacheCluster.Name] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, cluster := range infra.Resources.PubsubClusters {\n\t\tcluster.Topics = slices.DeleteFunc(cluster.Topics, func(t *runtimev1.PubSubTopic) bool {\n\t\t\t_, found := topicsToKeep[t.EncoreName]\n\t\t\treturn !found\n\t\t})\n\t\tcluster.Subscriptions = slices.DeleteFunc(cluster.Subscriptions, func(t *runtimev1.PubSubSubscription) bool {\n\t\t\t_, found := subsToKeep[subKey{topicName: t.TopicEncoreName, subName: t.SubscriptionEncoreName}]\n\t\t\treturn !found\n\t\t})\n\t}\n\n\tfor _, cluster := range infra.Resources.RedisClusters {\n\t\tcluster.Databases = slices.DeleteFunc(cluster.Databases, func(t *runtimev1.RedisDatabase) bool {\n\t\t\t_, found := cachesToKeep[t.EncoreName]\n\t\t\treturn !found\n\t\t})\n\t}\n\n\tfor _, cluster := range infra.Resources.BucketClusters {\n\t\tcluster.Buckets = slices.DeleteFunc(cluster.Buckets, func(t *runtimev1.Bucket) bool {\n\t\t\t_, found := bucketsToKeep[t.EncoreName]\n\t\t\treturn !found\n\t\t})\n\t}\n\n\tsecretsToKeep := secretsUsedByServices(md, svcNames)\n\tinfra.Resources.AppSecrets = slices.DeleteFunc(infra.Resources.AppSecrets, func(t *runtimev1.AppSecret) bool {\n\t\t_, found := secretsToKeep[t.EncoreName]\n\t\treturn !found\n\t})\n\n\treturn infra\n}\n\n// secretsUsedByServices returns the set of secrets that are accessible by the given services, using the metadata for access control.\nfunc secretsUsedByServices(md *meta.Data, svcNames map[string]bool) (secretNames map[string]bool) {\n\tsecretNames = make(map[string]bool)\n\tfor _, pkg := range md.Pkgs {\n\t\tif len(pkg.Secrets) > 0 && (pkg.ServiceName == \"\" || svcNames[pkg.ServiceName]) {\n\t\t\tfor _, secret := range pkg.Secrets {\n\t\t\t\tsecretNames[secret] = true\n\t\t\t}\n\t\t}\n\t}\n\treturn secretNames\n}\n"
  },
  {
    "path": "pkg/rtconfgen/resource_map.go",
    "content": "package rtconfgen\n\nimport \"fmt\"\n\n// A resource is an object with a unique resource id (rid).\ntype resource interface {\n\tGetRid() string\n}\n\ntype resourceKey struct {\n\ttyp string\n\trid string\n}\n\n// A resourceSet tracks whether a resource has been seen before based on its id,\n// and allows efficient lookup of a resource by id.\ntype resourceSet struct {\n\tm map[resourceKey]any\n}\n\n// rsAdd adds a resource to the set. It reports whether the resource was added.\nfunc rsAdd[R any](rs *resourceSet, rid string, fn func() R) (val R, added bool) {\n\tkey := internalRSKeyByID[R](rid)\n\tif existing := rs.m[key]; existing != nil {\n\t\treturn existing.(R), false\n\t}\n\tif rs.m == nil {\n\t\trs.m = make(map[resourceKey]any)\n\t}\n\n\tval = fn()\n\trs.m[key] = val\n\treturn val, true\n}\n\nfunc internalRSKeyByID[R any](rid string) resourceKey {\n\tvar zero R\n\ttyp := fmt.Sprintf(\"%T\", zero)\n\treturn resourceKey{typ, rid}\n}\n\nfunc addResFunc[R any](dst *[]R, rs *resourceSet, rid string, fn func() R) (stored R) {\n\t*dst, stored = appendResFunc(*dst, rs, rid, fn)\n\treturn stored\n}\n\nfunc appendResFunc[R any](dst []R, rs *resourceSet, rid string, fn func() R) (result []R, stored R) {\n\tstored, updated := rsAdd(rs, rid, fn)\n\tif updated {\n\t\tdst = append(dst, stored)\n\t}\n\treturn dst, stored\n}\n"
  },
  {
    "path": "pkg/supervisor/cmd/supervisor-encore/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"encr.dev/pkg/supervisor\"\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n)\n\nfunc main() {\n\tlog.Info().Msg(\"supervisor starting\")\n\tif err := run(); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"unable to run supervisor\")\n\t}\n}\n\nfunc run() error {\n\tcfgPath := flag.String(\"c\", \"\", \"path to the config file\")\n\tflag.Parse()\n\tif *cfgPath == \"\" {\n\t\treturn errors.New(\"missing config file\")\n\t}\n\n\tcfg, err := loadSupervisorConfig(*cfgPath)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"load supervisor config\")\n\t}\n\n\t// Configure the logger.\n\trtcfg, err := loadRuntimeConfig()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"load runtime config\")\n\t}\n\tconfigureLogger(rtcfg)\n\n\tsuper, err := supervisor.New(cfg, rtcfg)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"create supervisor\")\n\t}\n\n\terr = super.Run(context.Background())\n\treturn errors.Wrap(err, \"run supervisor\")\n}\n\nfunc loadSupervisorConfig(path string) (*supervisor.Config, error) {\n\tvar cfg supervisor.Config\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"read config file\")\n\t}\n\tif err := json.Unmarshal(data, &cfg); err != nil {\n\t\treturn nil, errors.Wrap(err, \"unmarshal config file\")\n\t}\n\treturn &cfg, nil\n}\n\n// loadRuntimeConfig loads the runtime config from the ENCORE_RUNTIME_CONFIG env var.\nfunc loadRuntimeConfig() (*runtimev1.RuntimeConfig, error) {\n\tval, ok := os.LookupEnv(\"ENCORE_RUNTIME_CONFIG\")\n\tif !ok {\n\t\treturn nil, errors.New(\"ENCORE_RUNTIME_CONFIG not set\")\n\t}\n\n\tdecoded, err := base64.StdEncoding.DecodeString(val)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to decode ENCORE_RUNTIME_CONFIG\")\n\t}\n\n\tvar cfg runtimev1.RuntimeConfig\n\tif err := proto.Unmarshal(decoded, &cfg); err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to unmarshal runtime config\")\n\t}\n\n\treturn &cfg, nil\n}\n\nfunc configureLogger(cfg *runtimev1.RuntimeConfig) {\n\t// Log in GCP's log format for Encore Cloud and GCP.\n\tif cloud := cfg.Environment.Cloud; cloud == runtimev1.Environment_CLOUD_GCP || cloud == runtimev1.Environment_CLOUD_ENCORE {\n\t\tzerolog.LevelFieldName = \"severity\"\n\t\tzerolog.TimestampFieldName = \"timestamp\"\n\t\tzerolog.TimeFieldFormat = time.RFC3339Nano\n\t}\n\n\t// Create our root logger\n\tlogger := zerolog.New(os.Stderr).\n\t\tLevel(zerolog.DebugLevel).\n\t\tWith().Caller().Timestamp().Stack().Str(\"process\", \"supervisor\").\n\t\tLogger()\n\n\tlog.Logger = logger\n}\n"
  },
  {
    "path": "pkg/supervisor/supervisor.go",
    "content": "package supervisor\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\t\"go4.org/syncutil\"\n\n\t\"encr.dev/pkg/noopgateway\"\n\truntimev1 \"encr.dev/proto/encore/runtime/v1\"\n)\n\n// Config is the configuration used by the supervisor.\ntype Config struct {\n\tProcs []Proc `json:\"procs\"`\n\n\t// NoopGateways are the noop-gateways to start up,\n\t// keyed by gateway name.\n\tNoopGateways map[string]*noopgateway.Description\n}\n\n// Proc represents a supervised proc.\ntype Proc struct {\n\t// ID is a unique id representing this process.\n\tID string `json:\"id\"`\n\n\t// Command and arguments for running the proc.\n\tCommand []string `json:\"command\"`\n\n\t// Extra environment variables to pass.\n\tEnv []string `json:\"env\"`\n\n\t// supervisedProc and gateway names included in this proc.\n\tServices []string `json:\"services\"`\n\tGateways []string `json:\"gateways\"`\n}\n\n// New creates a new supervisor.\nfunc New(cfg *Config, rtCfg *runtimev1.RuntimeConfig) (*Supervisor, error) {\n\tlog := zerolog.New(os.Stderr).With().Timestamp().Logger()\n\n\tsuper := &Supervisor{\n\t\tcfg:   cfg,\n\t\trt:    rtCfg,\n\t\tprocs: make(map[string]*supervisedProc),\n\t\tlog:   log,\n\t}\n\n\treturn super, nil\n}\n\ntype Supervisor struct {\n\tcfg   *Config\n\trt    *runtimev1.RuntimeConfig\n\tprocs map[string]*supervisedProc\n\n\tbuildInfoOnce syncutil.Once\n\tbuildInfo     BuildInfo\n\n\tlog zerolog.Logger\n}\n\n// Run runs the supervisor. It returns immediately on error,\n// and otherwise blocks until ctx is canceled.\nfunc (s *Supervisor) Run(ctx context.Context) error {\n\t// Start up the procs to supervise.\n\tfor i, proc := range s.cfg.Procs {\n\t\tservices := strings.Join(proc.Services, \",\")\n\t\tgateways := strings.Join(proc.Gateways, \",\")\n\n\t\tlogger := s.log.With().\n\t\t\tStr(\"services\", services).\n\t\t\tStr(\"gateways\", gateways).\n\t\t\tStr(\"proc_id\", proc.ID).\n\t\t\tLogger()\n\n\t\tport := 12000 + i\n\t\tsp := &supervisedProc{\n\t\t\tsuper: s,\n\t\t\tproc:  proc,\n\t\t\tlog:   logger,\n\t\t\tport:  port,\n\t\t}\n\t\ts.procs[proc.ID] = sp\n\t\tgo sp.Supervise()\n\t}\n\n\t// Start up the noop-gateways.\n\tgo s.runNoopGateway(ctx, \"noop\")\n\n\t<-ctx.Done()\n\treturn nil\n}\n\n// runNoopGateway runs a noop-gateway until ctx is canceled.\nfunc (s *Supervisor) runNoopGateway(ctx context.Context, name string) {\n\tlogger := s.log.With().Str(\"gateway\", name).Logger()\n\tlogger.Info().Msg(\"starting noop-gateway\")\n\n\t// Find the default gateway\n\tvar targetGW *supervisedProc\n\tfor _, proc := range s.procs {\n\t\tif len(proc.proc.Gateways) > 0 {\n\t\t\ttargetGW = proc\n\t\t\tbreak\n\t\t}\n\t}\n\tif targetGW == nil {\n\t\tlogger.Error().Msg(\"no gateway found\")\n\t\treturn\n\t}\n\n\ttarget := &url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   fmt.Sprintf(\"localhost:%d\", targetGW.port),\n\t}\n\trp := httputil.NewSingleHostReverseProxy(target)\n\tln, err := listenGateway()\n\tif err != nil {\n\t\tlogger.Error().Err(err).Msg(\"listen\")\n\t\treturn\n\t}\n\tlogger.Info().Msgf(\"listening on %s\", ln.Addr().String())\n\tsrv := &http.Server{\n\t\tBaseContext: func(_ net.Listener) context.Context { return ctx },\n\t\tHandler:     rp,\n\t}\n\tif err := srv.Serve(ln); err != nil {\n\t\tlogger.Error().Err(err).Msg(\"serve\")\n\t\treturn\n\t}\n}\n\nfunc listenGateway() (net.Listener, error) {\n\tlistenAddr := os.Getenv(\"ENCORE_LISTEN_ADDR\")\n\tif listenAddr != \"\" {\n\t\taddrPort, err := netip.ParseAddrPort(listenAddr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn net.Listen(\"tcp\", addrPort.String())\n\t}\n\n\tport, _ := strconv.Atoi(os.Getenv(\"PORT\"))\n\tif port == 0 {\n\t\tport = 8080\n\t}\n\treturn net.Listen(\"tcp\", \":\"+strconv.Itoa(port))\n}\n\n// supervisedProc is a supervised process.\ntype supervisedProc struct {\n\tsuper   *Supervisor\n\tproc    Proc\n\tlog     zerolog.Logger\n\thealthy atomic.Bool\n\tport    int\n}\n\n// Healthy reports whether the service is currently healthy.\nfunc (s *supervisedProc) Healthy() bool {\n\treturn s.healthy.Load()\n}\n\n// Supervise supervises the service, starting it and restarting it if it exits.\nfunc (p *supervisedProc) Supervise() {\n\tconst (\n\t\tminSleep = 100 * time.Millisecond\n\t\tmaxSleep = 10 * time.Second\n\t)\n\n\tvar (\n\t\tmu         sync.Mutex\n\t\tgeneration uint64 = 1\n\t\tretrySleep        = minSleep\n\t)\n\n\terrSleep := func(msg string, err error) {\n\t\t// Mark the service as unhealthy.\n\t\tp.healthy.Store(false)\n\n\t\t// Increment retry sleep and generation.\n\t\tmu.Lock()\n\t\tgeneration++\n\t\ttoSleep := retrySleep\n\t\tretrySleep *= 2\n\t\tif retrySleep > maxSleep {\n\t\t\tretrySleep = maxSleep\n\t\t}\n\t\tmu.Unlock()\n\n\t\tp.log.Error().Err(err).Msgf(\"%s: %v, retrying in %v\", msg, err, toSleep)\n\t\ttime.Sleep(toSleep)\n\t}\n\n\t// If this proc is using the sidecar, add its environment variables.\n\tenv := os.Environ()\n\tenv = append(env, p.proc.Env...)\n\n\t// Add the port to listen on.\n\tenv = append(env, fmt.Sprintf(\"PORT=%d\", p.port))\n\n\tfor {\n\t\tmu.Lock()\n\t\tcurrGen := generation\n\t\tmu.Unlock()\n\n\t\tcmd := exec.Command(p.proc.Command[0], p.proc.Command[1:]...)\n\t\tcmd.Env = env\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\n\t\tp.log.Info().Msg(\"starting proc\")\n\t\tif err := cmd.Start(); err != nil {\n\t\t\terrSleep(\"service startup failed\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check in on the process after a few seconds and reset the retry sleep if it's still alive.\n\t\tgo func() {\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\t\t\tif generation == currGen {\n\t\t\t\t// Still alive. Reset the retry sleep.\n\t\t\t\tretrySleep = minSleep\n\t\t\t\tp.healthy.Store(true)\n\t\t\t}\n\t\t}()\n\n\t\tif err := cmd.Wait(); err != nil {\n\t\t\terrSleep(\"proc exited\", err)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\nfunc readBuildInfo() (BuildInfo, error) {\n\tvar info BuildInfo\n\tdata, err := os.ReadFile(\"/encore/build-info.json\")\n\tif err == nil {\n\t\terr = json.Unmarshal(data, &info)\n\t}\n\treturn info, errors.Wrap(err, \"read build info\")\n}\n\ntype BuildInfo struct {\n\t// The version of Encore with which the app was compiled.\n\t// This is string is for informational use only, and its format should not be relied on.\n\tEncoreCompiler string\n\n\t// AppCommit describes the commit of the app.\n\tAppCommit CommitInfo\n}\n\ntype CommitInfo struct {\n\tRevision    string\n\tUncommitted bool\n}\n\nfunc (ci CommitInfo) AsRevisionString() string {\n\tif ci.Uncommitted {\n\t\treturn fmt.Sprintf(\"%s-modified\", ci.Revision)\n\t}\n\treturn ci.Revision\n}\n"
  },
  {
    "path": "pkg/svcproxy/dialer.go",
    "content": "package svcproxy\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n)\n\ntype retryDialer struct {\n\tnet.Dialer\n}\n\nfunc (d *retryDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {\n\tvar conn net.Conn\n\tb := backoff.NewExponentialBackOff()\n\tb.MaxElapsedTime = time.Minute // Set maximum backoff time to 1 minute\n\n\toperation := func() error {\n\t\tvar err error\n\t\tconn, err = d.Dialer.DialContext(ctx, network, address)\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"connection refused\") {\n\t\t\t\t// Retry if connection is refused\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Don't retry if connection isn't refused\n\t\t\treturn backoff.Permanent(err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr := backoff.Retry(operation, b)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn conn, nil\n}\n"
  },
  {
    "path": "pkg/svcproxy/doc.go",
    "content": "// Package svcproxy provides an HTTP proxy which allows the daemon to\n// serve HTTP requests on behalf of services within the app and forward\n// then to the appropriate process.\npackage svcproxy\n"
  },
  {
    "path": "pkg/svcproxy/svcproxy.go",
    "content": "package svcproxy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/pkg/logging\"\n)\n\ntype SvcProxy struct {\n\tlistener   net.Listener\n\tlogger     zerolog.Logger\n\thttpServer *http.Server\n\n\tmu       sync.RWMutex\n\tgateways map[string]*httputil.ReverseProxy // Map of the gateway name to address and port it's listening on\n\tservices map[string]*httputil.ReverseProxy // Map of service name to address and port it's listening on\n}\n\nvar (\n\t_ http.Handler = (*SvcProxy)(nil)\n)\n\n// New creates a new service proxy and it starts listening on a random port\n//\n// You must call Close() on the returned service proxy when you are done with it.\nfunc New(ctx context.Context, logger zerolog.Logger) (*SvcProxy, error) {\n\tvar lc net.ListenConfig\n\tln, err := lc.Listen(ctx, \"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"unable to listen\")\n\t}\n\n\tproxy := &SvcProxy{\n\t\tlistener: ln,\n\t\tlogger:   logger,\n\t\tgateways: make(map[string]*httputil.ReverseProxy),\n\t\tservices: make(map[string]*httputil.ReverseProxy),\n\t}\n\n\tproxy.httpServer = &http.Server{\n\t\tAddr:        ln.Addr().String(),\n\t\tBaseContext: func(_ net.Listener) context.Context { return ctx },\n\t\tHandler:     proxy,\n\t}\n\n\tgo func() {\n\t\tif err := proxy.httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\tlogger.Err(err).Msg(\"error serving\")\n\t\t}\n\t}()\n\n\treturn proxy, nil\n}\n\n// Close stops running the service proxy\nfunc (p *SvcProxy) Close() {\n\t_ = p.httpServer.Close()\n\t_ = p.listener.Close()\n}\n\nfunc (p *SvcProxy) RegisterGateway(name string, addr netip.AddrPort) string {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.gateways[name] = p.createReverseProxy(\"gateway\", name, addr)\n\n\treturn fmt.Sprintf(\"http://%s/gateway/%s\", p.listener.Addr().String(), name)\n}\n\n// RegisterService registers a service with the proxy and returns the BaseURL to be used\n// to access the service.\nfunc (p *SvcProxy) RegisterService(name string, addr netip.AddrPort) string {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.services[name] = p.createReverseProxy(\"service\", name, addr)\n\n\treturn fmt.Sprintf(\"http://%s/service/%s\", p.listener.Addr().String(), name)\n}\n\nfunc (p *SvcProxy) createReverseProxy(what, name string, listener netip.AddrPort) *httputil.ReverseProxy {\n\treturn &httputil.ReverseProxy{\n\t\t// This transport is copied from the default transport in the http package just with the dial context\n\t\t// wrapped in our retry dialer.\n\t\tTransport: &http.Transport{\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t\tDialContext: (&retryDialer{net.Dialer{\n\t\t\t\tTimeout:   30 * time.Second,\n\t\t\t\tKeepAlive: 30 * time.Second,\n\t\t\t}}).DialContext,\n\t\t\tForceAttemptHTTP2:     true,\n\t\t\tMaxIdleConns:          100,\n\t\t\tIdleConnTimeout:       90 * time.Second,\n\t\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\t\tExpectContinueTimeout: 1 * time.Second,\n\t\t},\n\t\tRewrite: func(request *httputil.ProxyRequest) {\n\t\t\trequest.Out.URL.Scheme = \"http\"\n\t\t\trequest.Out.URL.Host = listener.String()\n\t\t\trequest.Out.URL.Path = strings.TrimPrefix(request.In.URL.Path, fmt.Sprintf(\"/%s/%s\", what, name))\n\t\t},\n\t\tErrorLog: logging.NewZeroLogAdapter(p.logger.With().Str(what, name).Logger(), zerolog.ErrorLevel),\n\t}\n}\n\n// ServeHTTP implements the http.Handler interface\nfunc (p *SvcProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {\n\t// Resolve the handler with the lock held.\n\tproxy, err := func() (http.Handler, error) {\n\t\tp.mu.RLock()\n\t\tdefer p.mu.RUnlock()\n\n\t\tparts := strings.SplitN(strings.TrimPrefix(req.URL.Path, \"/\"), \"/\", 3)\n\t\tif len(parts) >= 2 {\n\t\t\tswitch parts[0] {\n\t\t\tcase \"gateway\":\n\t\t\t\tproxy, ok := p.gateways[parts[1]]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, errors.Newf(\"unknown gateway: %s\", parts[1])\n\t\t\t\t} else {\n\t\t\t\t\treturn proxy, nil\n\t\t\t\t}\n\t\t\tcase \"service\":\n\t\t\t\tproxy, ok := p.services[parts[1]]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, errors.Newf(\"unknown service: %s\", parts[1])\n\t\t\t\t} else {\n\t\t\t\t\treturn proxy, nil\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.Newf(\"unknown path prefix: %s\", parts[0])\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil, errors.Newf(\"unknown path prefix format: %s\", req.URL.Path)\n\t\t}\n\t}()\n\n\tif err != nil {\n\t\tp.logger.Err(err).Str(\"url\", req.URL.String()).Msg(\"error proxying service request\")\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tproxy.ServeHTTP(w, req)\n}\n"
  },
  {
    "path": "pkg/tarstream/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016 Ben McClelland\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "pkg/tarstream/datavec.go",
    "content": "package tarstream\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n)\n\n// MemVec is a buffer vec type\ntype MemVec struct {\n\tData []byte\n}\n\n// PathVec is a filename vec type\ntype PathVec struct {\n\tPath string\n\tInfo os.FileInfo\n}\n\n// PadVec is a padding (0s) vec type\ntype PadVec struct {\n\tSize int64\n}\n\ntype DataReader interface {\n\tio.ReaderAt\n\tio.Closer\n}\n\nfunc nopCloser(r io.ReaderAt) DataReader {\n\treturn noopCloser{r}\n}\n\ntype noopCloser struct {\n\tio.ReaderAt\n}\n\nfunc (n noopCloser) Close() error { return nil }\n\n// Datavec is an interface for all vector types\ntype Datavec interface {\n\tClone() Datavec\n\tGetSize() int64\n\tOpen() (DataReader, error)\n}\n\n// GetSize gets the size of the memory vec\nfunc (m MemVec) GetSize() int64 {\n\treturn int64(len(m.Data))\n}\n\nfunc (m MemVec) Clone() Datavec {\n\treturn m\n}\n\n// Open opens a memory vec\nfunc (m MemVec) Open() (DataReader, error) {\n\treturn nopCloser(bytes.NewReader(m.Data)), nil\n}\n\n// GetSize gets the file size of the path vec\nfunc (p PathVec) GetSize() int64 {\n\treturn p.Info.Size()\n}\n\n// Open opens a file represented by a path vec\nfunc (p *PathVec) Open() (DataReader, error) {\n\treturn os.Open(p.Path)\n}\n\nfunc (p *PathVec) Clone() Datavec {\n\treturn p\n}\n\n// GetSize gets the size of the padding vec\nfunc (p PadVec) GetSize() int64 {\n\treturn p.Size\n}\n\n// Open opens the padding vec\nfunc (p PadVec) Open() (DataReader, error) {\n\treturn padReader{p.Size}, nil\n}\n\nfunc (p PadVec) Clone() Datavec {\n\treturn p\n}\n\ntype padReader struct {\n\tsize int64\n}\n\nfunc (r padReader) ReadAt(b []byte, off int64) (int, error) {\n\trem := int(r.size - off)\n\tif rem == 0 {\n\t\treturn 0, io.EOF\n\t}\n\n\tn := min(rem, len(b))\n\tfor i := range n {\n\t\tb[i] = 0\n\t}\n\treturn n, nil\n}\n\nfunc (r padReader) Close() error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/tarstream/datavec_test.go",
    "content": "package tarstream\n"
  },
  {
    "path": "pkg/tarstream/tarstream.go",
    "content": "package tarstream\n\nimport (\n\t\"archive/tar\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\n\t\"github.com/pkg/errors\"\n)\n\nfunc NewTarVec(vecs []Datavec) *TarVec {\n\tvar totalSize int64\n\tstarts := make([]int64, len(vecs))\n\tends := make([]int64, len(vecs))\n\tfor i, dv := range vecs {\n\t\tsize := dv.GetSize()\n\t\tstarts[i] = totalSize\n\t\tends[i] = totalSize + size\n\t\ttotalSize += size\n\t}\n\n\treturn &TarVec{\n\t\tvecs:   vecs,\n\t\tstarts: starts,\n\t\tends:   ends,\n\t\tsize:   totalSize,\n\t}\n}\n\n// TarVec is an array of datavecs representing a tarball\ntype TarVec struct {\n\tvecs   []Datavec\n\tstarts []int64 // starting offset for each vec, inclusive\n\tends   []int64 // ending offset for each vec, exclusive\n\tsize   int64\n\n\t// Current reading pos\n\tpos  int64\n\tcurr *currReader\n}\n\ntype currReader struct {\n\tidx  int // datavec index\n\tsize int64\n\tdata DataReader\n}\n\n// Size gets the size of the tarball represented by the tarvec\nfunc (tv *TarVec) Size() int64 {\n\treturn tv.size\n}\n\nfunc (tv *TarVec) Clone() *TarVec {\n\treturn &TarVec{\n\t\tvecs:   tv.vecs,\n\t\tstarts: tv.starts,\n\t\tends:   tv.ends,\n\t\tpos:    tv.pos,\n\t}\n}\n\nfunc (tv *TarVec) getReader() (*currReader, error) {\n\t// Do we have a current reader?\n\tif tv.curr != nil {\n\t\t// Is the current position within the current datavec?\n\t\tif tv.pos >= tv.starts[tv.curr.idx] && tv.pos < tv.ends[tv.curr.idx] {\n\t\t\treturn tv.curr, nil\n\t\t}\n\t\t_ = tv.curr.data.Close()\n\t\ttv.curr = nil\n\t}\n\n\t// Find the datavec that contains the current position\n\tcandidate := sort.Search(len(tv.ends), func(i int) bool {\n\t\treturn tv.ends[i] > tv.pos\n\t})\n\tif candidate == len(tv.ends) {\n\t\t// Position exceeds the end of the last data vec.\n\t\treturn nil, io.EOF\n\t}\n\n\tvec := tv.vecs[candidate]\n\tdata, err := vec.Open()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error opening data vec: %v\", err)\n\t}\n\n\ttv.curr = &currReader{\n\t\tidx:  candidate,\n\t\tsize: vec.GetSize(),\n\t\tdata: data,\n\t}\n\treturn tv.curr, nil\n}\n\n// Read the data represented by the tarvec\nfunc (tv *TarVec) Read(b []byte) (int, error) {\n\tcr, err := tv.getReader()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\toff := tv.pos - tv.starts[cr.idx]\n\n\t// Sanity checks\n\tremaining := cr.size - off\n\thasMoreVecs := cr.idx+1 < len(tv.vecs)\n\tif remaining < 0 {\n\t\tpanic(\"TarVec: negative remaining size\")\n\t} else if remaining == 0 && hasMoreVecs && cr.size > 0 {\n\t\tpanic(\"TarVec: zero remaining size but more vecs exist\")\n\t}\n\n\tn, err := cr.data.ReadAt(b, off)\n\tif err == io.EOF {\n\t\t// Ignore EOF from individual readers.\n\t\t// getReader reports EOF when running out of readers.\n\t\terr = nil\n\t}\n\n\tif n == 0 && len(b) > 0 && (err == nil || err == io.EOF) {\n\t\tpanic(\"TarVec: empty read from vec, more data remaining\")\n\t}\n\ttv.pos += int64(n)\n\n\treturn n, err\n}\n\n// Seek the virtual offset of the tarvec\nfunc (tv *TarVec) Seek(offset int64, whence int) (int64, error) {\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tif offset < 0 {\n\t\t\treturn 0, os.ErrInvalid\n\t\t}\n\t\ttv.pos = offset\n\t\treturn tv.pos, nil\n\tcase io.SeekCurrent:\n\t\tif tv.pos+offset < 0 {\n\t\t\treturn 0, os.ErrInvalid\n\t\t}\n\t\ttv.pos += offset\n\t\treturn tv.pos, nil\n\tcase io.SeekEnd:\n\t\tif tv.size+offset < 0 {\n\t\t\treturn 0, os.ErrInvalid\n\t\t}\n\t\ttv.pos = tv.size + offset\n\t\treturn tv.pos, nil\n\t}\n\treturn 0, os.ErrInvalid\n}\n\nfunc (tv *TarVec) Close() error {\n\tif tv.curr != nil {\n\t\terr := tv.curr.data.Close()\n\t\ttv.curr = nil\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Validate gets and validates the next header within the tarfile\nfunc Validate(r io.Reader) (*tar.Header, error) {\n\ttr := tar.NewReader(r)\n\t// Next will find the next header and read it in\n\t// this skips all meta-headers like long names, etc\n\t// hopefully thats what we want here\n\thdr, err := tr.Next()\n\tif err != nil {\n\t\treturn &tar.Header{}, errors.Wrap(err, fmt.Sprintf(\"read header\"))\n\t}\n\treturn hdr, nil\n}\n"
  },
  {
    "path": "pkg/tarstream/tarstream_test.go",
    "content": "package tarstream\n\nimport (\n\t\"math/rand\"\n\t\"slices\"\n\t\"testing\"\n\t\"testing/iotest\"\n\t\"testing/quick\"\n)\n\nfunc TestReader(t *testing.T) {\n\terr := quick.Check(func(data []byte) bool {\n\t\ttv := genRandomVec(data)\n\t\tif err := iotest.TestReader(tv, data); err != nil {\n\t\t\tt.Logf(\"got read err %v\", err)\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc genRandomVec(data []byte) *TarVec {\n\tvar vecs []Datavec\n\tfor len(data) > 0 {\n\t\tn := rand.Intn(len(data) + 1)\n\t\tvecData := data[:n]\n\t\tdata = data[n:]\n\t\tallZeroes := !slices.ContainsFunc(vecData, func(b byte) bool {\n\t\t\treturn b != 0\n\t\t})\n\n\t\tif allZeroes {\n\t\t\tvecs = append(vecs, PadVec{Size: int64(len(vecData))})\n\t\t} else {\n\t\t\tvecs = append(vecs, MemVec{Data: vecData})\n\t\t}\n\t}\n\treturn NewTarVec(vecs)\n}\n"
  },
  {
    "path": "pkg/traceparser/binreader.go",
    "content": "package traceparser\n\nimport (\n\t\"bufio\"\n\t\"encoding/binary\"\n\t\"io\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\nvar bin = binary.LittleEndian\n\ntype traceReader struct {\n\tbuf        *bufio.Reader\n\tversion    trace2.Version\n\tbytesRead  int\n\ttimeAnchor int64\n\terr        error // any error encountered during reading\n}\n\nfunc (tr *traceReader) setErr(err error) {\n\tif tr.err == nil {\n\t\ttr.err = err\n\t}\n}\n\n// Err reports any error encountered during reading.\nfunc (tr *traceReader) Err() error {\n\treturn tr.err\n}\n\nfunc (tr *traceReader) Bytes(b []byte) {\n\tn, err := io.ReadFull(tr.buf, b)\n\ttr.bytesRead += n\n\ttr.setErr(err)\n}\n\nfunc (tr *traceReader) Skip(n int) {\n\tdiscarded, err := tr.buf.Discard(n)\n\ttr.bytesRead += discarded\n\ttr.setErr(err)\n}\n\nfunc (tr *traceReader) Byte() byte {\n\tb, err := tr.buf.ReadByte()\n\ttr.setErr(err)\n\tif err == nil {\n\t\ttr.bytesRead++\n\t}\n\treturn b\n}\n\nfunc (tr *traceReader) Bool() bool {\n\treturn tr.Byte() != 0\n}\n\nfunc (tr *traceReader) String() string {\n\ts := string(tr.ByteString())\n\n\t// Ensure the string is valid UTF-8.\n\t// Needed because proto.Marshal requires strings to be valid utf8.\n\ts = strings.ToValidUTF8(s, string(utf8.RuneError))\n\n\treturn s\n}\n\nfunc (tr *traceReader) OptString() *string {\n\treturn ptrOrNil(tr.String())\n}\n\nfunc (tr *traceReader) OptUVarint() *uint64 {\n\treturn ptrOrNil(tr.UVarint())\n}\n\nfunc (tr *traceReader) ByteString() []byte {\n\tsize := tr.UVarint()\n\tif (size) == 0 {\n\t\treturn nil\n\t}\n\tb := make([]byte, int(size))\n\ttr.Bytes(b)\n\treturn b\n}\n\nfunc (tr *traceReader) Time() *timestamppb.Timestamp {\n\tsec := tr.Int64()\n\tnsec := tr.Int32()\n\tt := time.Unix(sec, int64(nsec)).UTC()\n\treturn timestamppb.New(t)\n}\n\nfunc (tr *traceReader) Nanotime() int64 {\n\treturn tr.Int64()\n}\n\nfunc (tr *traceReader) Int32() int32 {\n\tu := tr.Uint32()\n\tvar v int32\n\tif u&1 == 0 {\n\t\tv = int32(u >> 1)\n\t} else {\n\t\tv = ^int32(u >> 1)\n\t}\n\treturn v\n}\n\nfunc (tr *traceReader) Uint32() uint32 {\n\tvar buf [4]byte\n\ttr.Bytes(buf[:])\n\treturn bin.Uint32(buf[:])\n}\n\nfunc (tr *traceReader) Int64() int64 {\n\treturn unsignedToSigned(tr.Uint64())\n}\n\nfunc (tr *traceReader) Uint64() uint64 {\n\tvar buf [8]byte\n\ttr.Bytes(buf[:])\n\treturn bin.Uint64(buf[:])\n}\n\nfunc (tr *traceReader) Varint() int64 {\n\tu := tr.UVarint()\n\tvar v int64\n\tif u&1 == 0 {\n\t\tv = int64(u >> 1)\n\t} else {\n\t\tv = ^int64(u >> 1)\n\t}\n\treturn v\n}\n\nfunc (tr *traceReader) UVarint() uint64 {\n\ti := 0\n\tvar u uint64\n\tfor {\n\t\tb, err := tr.buf.ReadByte()\n\t\ttr.setErr(err)\n\t\tif err != nil {\n\t\t\treturn 0\n\t\t}\n\t\ttr.bytesRead++\n\t\tu |= uint64(b&^0x80) << i\n\t\tif b&0x80 == 0 {\n\t\t\treturn u\n\t\t}\n\t\ti += 7\n\t}\n}\n\nfunc (tr *traceReader) Float32() float32 {\n\tb := tr.Uint32()\n\treturn math.Float32frombits(b)\n}\n\nfunc (tr *traceReader) Float64() float64 {\n\tb := tr.Uint64()\n\treturn math.Float64frombits(b)\n}\n\nfunc (tr *traceReader) EventID() trace2.EventID {\n\treturn trace2.EventID(tr.UVarint())\n}\n\nfunc (tr *traceReader) Duration() time.Duration {\n\treturn time.Duration(tr.Varint())\n}\n\nfunc ptrOrNil[T comparable](val T) *T {\n\tvar zero T\n\tif val == zero {\n\t\treturn nil\n\t}\n\treturn &val\n}\n\nfunc unsignedToSigned(u uint64) int64 {\n\tvar v int64\n\tif u&1 == 0 {\n\t\tv = int64(u >> 1)\n\t} else {\n\t\tv = ^int64(u >> 1)\n\t}\n\treturn v\n}\n\ntype versionFilterReader struct {\n\ttraceReader *traceReader\n\tfiltered    bool\n}\n\nfunc (tr *traceReader) FromVer(version trace2.Version) versionFilterReader {\n\treturn versionFilterReader{traceReader: tr, filtered: tr.version < version}\n}\n\nfunc (tr versionFilterReader) Bool(defaultForOlderVersions bool) bool {\n\tif tr.filtered {\n\t\treturn defaultForOlderVersions\n\t}\n\treturn tr.traceReader.Bool()\n}\n"
  },
  {
    "path": "pkg/traceparser/parser.go",
    "content": "package traceparser\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"runtime/debug\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encr.dev/pkg/option\"\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n)\n\n// ParseEvent parses a single event from the buffer.\nfunc ParseEvent(buf *bufio.Reader, ta trace2.TimeAnchor, version trace2.Version) (*tracepb2.TraceEvent, error) {\n\ttp := &traceParser{traceReader: traceReader{buf: buf, version: version}, ta: ta, log: &log.Logger}\n\t// If we already have an error, return it.\n\tif err := tp.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttyp := trace2.EventType(tp.Byte())\n\tif err := tp.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\th := header{\n\t\tType:     typ,\n\t\tEventID:  trace2.EventID(tp.Uint64()),\n\t\tNanotime: tp.Nanotime(),\n\t\tTraceID:  tp.traceID(),\n\t\tSpanID:   tp.Uint64(),\n\t\tLen:      tp.Uint32(),\n\t}\n\tif err := tp.Err(); err != nil {\n\t\tlog.Error().Err(err).Any(\"header\", h).Msgf(\"failed to parse event header\")\n\t\treturn nil, err\n\t}\n\n\tbytesReadAfterHeader := tp.bytesRead\n\n\tev, err := tp.parseEvent(h)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"parse event %v: %v\", h.Type, err)\n\t}\n\n\terr = tp.Err()\n\tif err == io.EOF {\n\t\t// If we have an io.EOF and we've read exactly the right amount of bytes,\n\t\t// treat it as a non-error.\n\t\tif n := tp.bytesRead - bytesReadAfterHeader; n == int(h.Len) {\n\t\t\terr = io.EOF\n\t\t} else if n < int(h.Len) {\n\t\t\terr = io.ErrUnexpectedEOF\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"parser of event %s overflowed event buffer\", h.Type)\n\t\t}\n\t}\n\n\tif n := tp.bytesRead - bytesReadAfterHeader; n != int(h.Len) {\n\t\tlog.Info().Msgf(\"event %s: read %d bytes, expected %d\", h.Type, n, h.Len)\n\t}\n\n\treturn ev, err\n}\n\ntype spanStartEvent struct {\n\tGoid             uint32\n\tParentTraceID    option.Option[*tracepb2.TraceID]\n\tParentSpanID     option.Option[uint64]\n\tDefLoc           option.Option[uint32]\n\tCallerEventID    option.Option[trace2.EventID]\n\tExtCorrelationID option.Option[string]\n}\n\ntype spanEndEvent struct {\n\tDurationNanos uint64\n\tStatusCode    tracepb2.StatusCode\n\tErr           *tracepb2.Error\n\tPanicStack    option.Option[*tracepb2.StackTrace]\n\tParentTraceID option.Option[*tracepb2.TraceID]\n\tParentSpanID  option.Option[uint64]\n}\n\ntype traceParser struct {\n\ttraceReader\n\tta  trace2.TimeAnchor\n\tlog *zerolog.Logger\n}\n\ntype header struct {\n\tType    trace2.EventType\n\tEventID trace2.EventID\n\n\t// TS is a monotonic timestamp in nanoseconds.\n\t// It can be converted to an actual timestamp using the trace stream's epoch.\n\tNanotime int64\n\n\tTraceID *tracepb2.TraceID\n\tSpanID  uint64\n\tLen     uint32\n}\n\nvar errUnknownEvent = errors.New(\"unknown event\")\n\nfunc (tp *traceParser) parseEvent(h header) (ev *tracepb2.TraceEvent, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif b, ok := r.(bailout); ok {\n\t\t\t\terr = b.err\n\t\t\t} else {\n\t\t\t\terr = fmt.Errorf(\"panic parsing event: %v\\n%s\", r, debug.Stack())\n\t\t\t}\n\t\t}\n\t}()\n\n\tev = &tracepb2.TraceEvent{\n\t\tTraceId:   h.TraceID,\n\t\tSpanId:    h.SpanID,\n\t\tEventId:   uint64(h.EventID),\n\t\tEventTime: timestamppb.New(tp.ta.ToReal(h.Nanotime)),\n\t}\n\n\tswitch h.Type {\n\tcase trace2.RequestSpanStart:\n\t\tev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.requestSpanStart()}\n\tcase trace2.RequestSpanEnd:\n\t\tev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.requestSpanEnd()}\n\tcase trace2.AuthSpanStart:\n\t\tev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.authSpanStart()}\n\tcase trace2.AuthSpanEnd:\n\t\tev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.authSpanEnd()}\n\tcase trace2.PubsubMessageSpanStart:\n\t\tev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.pubsubMessageSpanStart()}\n\tcase trace2.PubsubMessageSpanEnd:\n\t\tev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.pubsubMessageSpanEnd()}\n\tcase trace2.TestStart:\n\t\tev.Event = &tracepb2.TraceEvent_SpanStart{SpanStart: tp.testSpanStart()}\n\tcase trace2.TestEnd:\n\t\tev.Event = &tracepb2.TraceEvent_SpanEnd{SpanEnd: tp.testSpanEnd()}\n\tdefault:\n\t\tev.Event = &tracepb2.TraceEvent_SpanEvent{SpanEvent: tp.spanEvent(h.Type)}\n\t}\n\n\treturn ev, nil\n}\n\nfunc (tp *traceParser) spanStartEvent() spanStartEvent {\n\tgoid := uint32(tp.UVarint())\n\tparentTraceID := tp.traceID()\n\tparentSpanID := tp.Uint64()\n\tdefLoc := uint32(tp.UVarint())\n\tcallerEventID := trace2.EventID(tp.UVarint())\n\textCorrelationID := tp.String()\n\n\tev := spanStartEvent{\n\t\tGoid:             goid,\n\t\tParentSpanID:     option.AsOptional(parentSpanID),\n\t\tDefLoc:           option.AsOptional(defLoc),\n\t\tCallerEventID:    option.AsOptional(callerEventID),\n\t\tExtCorrelationID: option.AsOptional(extCorrelationID),\n\t}\n\tif !parentTraceID.IsZero() {\n\t\tev.ParentTraceID = option.Some(parentTraceID)\n\t}\n\treturn ev\n}\n\nfunc (tp *traceParser) spanEndEvent() spanEndEvent {\n\tdur := tp.Duration()\n\tif dur < 0 {\n\t\tdur = 0\n\t}\n\n\tvar (\n\t\tstatus tracepb2.StatusCode\n\t\terr    *tracepb2.Error\n\t)\n\tif tp.version >= 17 {\n\t\tstatus = tp.statusCode()\n\t\terr = tp.errWithStack()\n\t} else {\n\t\terr = tp.errWithStack()\n\t\tif err != nil {\n\t\t\tstatus = tracepb2.StatusCode_STATUS_CODE_UNKNOWN\n\t\t} else {\n\t\t\tstatus = tracepb2.StatusCode_STATUS_CODE_OK\n\t\t}\n\t}\n\n\tpanicStack := tp.formattedStack()\n\tparentTraceID := tp.traceID()\n\tparentSpanID := tp.Uint64()\n\n\tev := spanEndEvent{\n\t\tDurationNanos: uint64(dur),\n\t\tStatusCode:    status,\n\t\tErr:           err,\n\t\tPanicStack:    option.AsOptional(panicStack),\n\t\tParentSpanID:  option.AsOptional(parentSpanID),\n\t}\n\tif !parentTraceID.IsZero() {\n\t\tev.ParentTraceID = option.Some(parentTraceID)\n\t}\n\treturn ev\n}\n\nfunc (tp *traceParser) spanEvent(eventType trace2.EventType) *tracepb2.SpanEvent {\n\tdefLoc := uint32(tp.UVarint())\n\tgoid := uint32(tp.UVarint())\n\tcorrelationEventID := tp.EventID()\n\n\tev := &tracepb2.SpanEvent{Goid: goid}\n\tif defLoc > 0 {\n\t\tev.DefLoc = &defLoc\n\t}\n\tif correlationEventID > 0 {\n\t\tev.CorrelationEventId = (*uint64)(&correlationEventID)\n\t}\n\n\tswitch eventType {\n\tcase trace2.RPCCallStart:\n\t\tev.Data = &tracepb2.SpanEvent_RpcCallStart{RpcCallStart: tp.rpcCallStart()}\n\tcase trace2.RPCCallEnd:\n\t\tev.Data = &tracepb2.SpanEvent_RpcCallEnd{RpcCallEnd: tp.rpcCallEnd()}\n\tcase trace2.DBQueryStart:\n\t\tev.Data = &tracepb2.SpanEvent_DbQueryStart{DbQueryStart: tp.dbQueryStart()}\n\tcase trace2.DBQueryEnd:\n\t\tev.Data = &tracepb2.SpanEvent_DbQueryEnd{DbQueryEnd: tp.dbQueryEnd()}\n\tcase trace2.DBTransactionStart:\n\t\tev.Data = &tracepb2.SpanEvent_DbTransactionStart{DbTransactionStart: tp.dbTransactionStart()}\n\tcase trace2.DBTransactionEnd:\n\t\tev.Data = &tracepb2.SpanEvent_DbTransactionEnd{DbTransactionEnd: tp.dbTransactionEnd()}\n\tcase trace2.PubsubPublishStart:\n\t\tev.Data = &tracepb2.SpanEvent_PubsubPublishStart{PubsubPublishStart: tp.pubsubPublishStart()}\n\tcase trace2.PubsubPublishEnd:\n\t\tev.Data = &tracepb2.SpanEvent_PubsubPublishEnd{PubsubPublishEnd: tp.pubsubPublishEnd()}\n\tcase trace2.HTTPCallStart:\n\t\tev.Data = &tracepb2.SpanEvent_HttpCallStart{HttpCallStart: tp.httpCallStart()}\n\tcase trace2.HTTPCallEnd:\n\t\tev.Data = &tracepb2.SpanEvent_HttpCallEnd{HttpCallEnd: tp.httpCallEnd()}\n\tcase trace2.LogMessage:\n\t\tev.Data = &tracepb2.SpanEvent_LogMessage{LogMessage: tp.logMessage()}\n\tcase trace2.ServiceInitStart:\n\t\tev.Data = &tracepb2.SpanEvent_ServiceInitStart{ServiceInitStart: tp.serviceInitStart()}\n\tcase trace2.ServiceInitEnd:\n\t\tev.Data = &tracepb2.SpanEvent_ServiceInitEnd{ServiceInitEnd: tp.serviceInitEnd()}\n\tcase trace2.CacheCallStart:\n\t\tev.Data = &tracepb2.SpanEvent_CacheCallStart{CacheCallStart: tp.cacheCallStart()}\n\tcase trace2.CacheCallEnd:\n\t\tev.Data = &tracepb2.SpanEvent_CacheCallEnd{CacheCallEnd: tp.cacheCallEnd()}\n\tcase trace2.BodyStream:\n\t\tev.Data = &tracepb2.SpanEvent_BodyStream{BodyStream: tp.bodyStream()}\n\tcase trace2.BucketObjectUploadStart:\n\t\tev.Data = &tracepb2.SpanEvent_BucketObjectUploadStart{BucketObjectUploadStart: tp.bucketObjectUploadStart()}\n\tcase trace2.BucketObjectUploadEnd:\n\t\tev.Data = &tracepb2.SpanEvent_BucketObjectUploadEnd{BucketObjectUploadEnd: tp.bucketObjectUploadEnd()}\n\tcase trace2.BucketObjectDownloadStart:\n\t\tev.Data = &tracepb2.SpanEvent_BucketObjectDownloadStart{BucketObjectDownloadStart: tp.bucketObjectDownloadStart()}\n\tcase trace2.BucketObjectDownloadEnd:\n\t\tev.Data = &tracepb2.SpanEvent_BucketObjectDownloadEnd{BucketObjectDownloadEnd: tp.bucketObjectDownloadEnd()}\n\tcase trace2.BucketObjectGetAttrsStart:\n\t\tev.Data = &tracepb2.SpanEvent_BucketObjectGetAttrsStart{BucketObjectGetAttrsStart: tp.bucketObjectGetAttrsStart()}\n\tcase trace2.BucketObjectGetAttrsEnd:\n\t\tev.Data = &tracepb2.SpanEvent_BucketObjectGetAttrsEnd{BucketObjectGetAttrsEnd: tp.bucketObjectGetAttrsEnd()}\n\tcase trace2.BucketListObjectsStart:\n\t\tev.Data = &tracepb2.SpanEvent_BucketListObjectsStart{BucketListObjectsStart: tp.bucketListObjectsStart()}\n\tcase trace2.BucketListObjectsEnd:\n\t\tev.Data = &tracepb2.SpanEvent_BucketListObjectsEnd{BucketListObjectsEnd: tp.bucketListObjectsEnd()}\n\tcase trace2.BucketDeleteObjectsStart:\n\t\tev.Data = &tracepb2.SpanEvent_BucketDeleteObjectsStart{BucketDeleteObjectsStart: tp.bucketDeleteObjectsStart()}\n\tcase trace2.BucketDeleteObjectsEnd:\n\t\tev.Data = &tracepb2.SpanEvent_BucketDeleteObjectsEnd{BucketDeleteObjectsEnd: tp.bucketDeleteObjectsEnd()}\n\n\tdefault:\n\t\ttp.bailout(fmt.Errorf(\"unknown event %v\", eventType))\n\t}\n\n\treturn ev\n}\n\nfunc (tp *traceParser) requestSpanStart() *tracepb2.SpanStart {\n\tspanStart := tp.spanStartEvent()\n\n\tstart := &tracepb2.SpanStart{\n\t\tGoid:                  spanStart.Goid,\n\t\tParentTraceId:         spanStart.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:          spanStart.ParentSpanID.PtrOrNil(),\n\t\tDefLoc:                spanStart.DefLoc.PtrOrNil(),\n\t\tCallerEventId:         (*uint64)(spanStart.CallerEventID.PtrOrNil()),\n\t\tExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(),\n\t\tData: &tracepb2.SpanStart_Request{\n\t\t\tRequest: &tracepb2.RequestSpanStart{\n\t\t\t\tServiceName:  tp.String(),\n\t\t\t\tEndpointName: tp.String(),\n\t\t\t\tHttpMethod:   tp.String(),\n\t\t\t\tPath:         tp.String(),\n\t\t\t\tPathParams: (func() []string {\n\t\t\t\t\tn := tp.UVarint()\n\t\t\t\t\tif n == 0 {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\tparams := make([]string, n)\n\t\t\t\t\tfor i := 0; i < int(n); i++ {\n\t\t\t\t\t\tparams[i] = tp.String()\n\t\t\t\t\t}\n\t\t\t\t\treturn params\n\t\t\t\t})(),\n\t\t\t\tRequestHeaders:   tp.headers(),\n\t\t\t\tRequestPayload:   tp.ByteString(),\n\t\t\t\tExtCorrelationId: ptrOrNil(tp.String()),\n\t\t\t\tUid:              ptrOrNil(tp.String()),\n\t\t\t\tMocked:           tp.FromVer(15).Bool(false),\n\t\t\t},\n\t\t},\n\t}\n\n\treturn start\n}\n\nfunc (tp *traceParser) requestSpanEnd() *tracepb2.SpanEnd {\n\tspanEnd := tp.spanEndEvent()\n\treturn &tracepb2.SpanEnd{\n\t\tDurationNanos: spanEnd.DurationNanos,\n\t\tStatusCode:    spanEnd.StatusCode,\n\t\tError:         spanEnd.Err,\n\t\tPanicStack:    spanEnd.PanicStack.GetOrElse(nil),\n\t\tParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:  spanEnd.ParentSpanID.PtrOrNil(),\n\t\tData: &tracepb2.SpanEnd_Request{\n\t\t\tRequest: &tracepb2.RequestSpanEnd{\n\t\t\t\tServiceName:     tp.String(),\n\t\t\t\tEndpointName:    tp.String(),\n\t\t\t\tHttpStatusCode:  uint32(tp.UVarint()),\n\t\t\t\tResponseHeaders: tp.headers(),\n\t\t\t\tResponsePayload: tp.ByteString(),\n\t\t\t\tCallerEventId: (func() *uint64 {\n\t\t\t\t\tif tp.version >= 16 {\n\t\t\t\t\t\tid := uint64(tp.EventID())\n\t\t\t\t\t\treturn &id\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})(),\n\t\t\t\tUid: (func() *string {\n\t\t\t\t\tif tp.version >= 17 {\n\t\t\t\t\t\treturn tp.OptString()\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) authSpanStart() *tracepb2.SpanStart {\n\tspanStart := tp.spanStartEvent()\n\n\treturn &tracepb2.SpanStart{\n\t\tGoid:                  spanStart.Goid,\n\t\tParentTraceId:         spanStart.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:          spanStart.ParentSpanID.PtrOrNil(),\n\t\tDefLoc:                spanStart.DefLoc.PtrOrNil(),\n\t\tCallerEventId:         (*uint64)(spanStart.CallerEventID.PtrOrNil()),\n\t\tExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(),\n\t\tData: &tracepb2.SpanStart_Auth{\n\t\t\tAuth: &tracepb2.AuthSpanStart{\n\t\t\t\tServiceName:  tp.String(),\n\t\t\t\tEndpointName: tp.String(),\n\t\t\t\tAuthPayload:  tp.ByteString(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) authSpanEnd() *tracepb2.SpanEnd {\n\tspanEnd := tp.spanEndEvent()\n\treturn &tracepb2.SpanEnd{\n\t\tDurationNanos: spanEnd.DurationNanos,\n\t\tStatusCode:    spanEnd.StatusCode,\n\t\tError:         spanEnd.Err,\n\t\tPanicStack:    spanEnd.PanicStack.GetOrElse(nil),\n\t\tParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:  spanEnd.ParentSpanID.PtrOrNil(),\n\t\tData: &tracepb2.SpanEnd_Auth{\n\t\t\tAuth: &tracepb2.AuthSpanEnd{\n\t\t\t\tServiceName:  tp.String(),\n\t\t\t\tEndpointName: tp.String(),\n\t\t\t\tUid:          tp.String(),\n\t\t\t\tUserData:     tp.ByteString(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) pubsubMessageSpanStart() *tracepb2.SpanStart {\n\tspanStart := tp.spanStartEvent()\n\n\treturn &tracepb2.SpanStart{\n\t\tGoid:                  spanStart.Goid,\n\t\tParentTraceId:         spanStart.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:          spanStart.ParentSpanID.PtrOrNil(),\n\t\tDefLoc:                spanStart.DefLoc.PtrOrNil(),\n\t\tCallerEventId:         (*uint64)(spanStart.CallerEventID.PtrOrNil()),\n\t\tExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(),\n\t\tData: &tracepb2.SpanStart_PubsubMessage{\n\t\t\tPubsubMessage: &tracepb2.PubsubMessageSpanStart{\n\t\t\t\tServiceName:      tp.String(),\n\t\t\t\tTopicName:        tp.String(),\n\t\t\t\tSubscriptionName: tp.String(),\n\t\t\t\tMessageId:        tp.String(),\n\t\t\t\tAttempt:          uint32(tp.UVarint()),\n\t\t\t\tPublishTime:      tp.Time(), // TODO use nanotime\n\t\t\t\tMessagePayload:   tp.ByteString(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) pubsubMessageSpanEnd() *tracepb2.SpanEnd {\n\tspanEnd := tp.spanEndEvent()\n\treturn &tracepb2.SpanEnd{\n\t\tDurationNanos: spanEnd.DurationNanos,\n\t\tStatusCode:    spanEnd.StatusCode,\n\t\tError:         spanEnd.Err,\n\t\tPanicStack:    spanEnd.PanicStack.GetOrElse(nil),\n\t\tParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:  spanEnd.ParentSpanID.PtrOrNil(),\n\t\tData: &tracepb2.SpanEnd_PubsubMessage{\n\t\t\tPubsubMessage: &tracepb2.PubsubMessageSpanEnd{\n\t\t\t\tServiceName:      tp.String(),\n\t\t\t\tTopicName:        tp.String(),\n\t\t\t\tSubscriptionName: tp.String(),\n\t\t\t\tMessageId: (func() string {\n\t\t\t\t\tif tp.version >= 17 {\n\t\t\t\t\t\treturn tp.String()\n\t\t\t\t\t}\n\t\t\t\t\treturn \"\"\n\t\t\t\t})(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) testSpanStart() *tracepb2.SpanStart {\n\tspanStart := tp.spanStartEvent()\n\n\treturn &tracepb2.SpanStart{\n\t\tGoid:                  spanStart.Goid,\n\t\tParentTraceId:         spanStart.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:          spanStart.ParentSpanID.PtrOrNil(),\n\t\tDefLoc:                spanStart.DefLoc.PtrOrNil(),\n\t\tCallerEventId:         (*uint64)(spanStart.CallerEventID.PtrOrNil()),\n\t\tExternalCorrelationId: spanStart.ExtCorrelationID.PtrOrNil(),\n\t\tData: &tracepb2.SpanStart_Test{\n\t\t\tTest: &tracepb2.TestSpanStart{\n\t\t\t\tServiceName: tp.String(),\n\t\t\t\tTestName:    tp.String(),\n\t\t\t\tUid:         tp.String(),\n\t\t\t\tTestFile:    tp.String(),\n\t\t\t\tTestLine:    tp.Uint32(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) testSpanEnd() *tracepb2.SpanEnd {\n\tspanEnd := tp.spanEndEvent()\n\treturn &tracepb2.SpanEnd{\n\t\tDurationNanos: spanEnd.DurationNanos,\n\t\tStatusCode:    spanEnd.StatusCode,\n\t\tError:         spanEnd.Err,\n\t\tPanicStack:    spanEnd.PanicStack.GetOrElse(nil),\n\t\tParentTraceId: spanEnd.ParentTraceID.GetOrElse(nil),\n\t\tParentSpanId:  spanEnd.ParentSpanID.PtrOrNil(),\n\t\tData: &tracepb2.SpanEnd_Test{\n\t\t\tTest: &tracepb2.TestSpanEnd{\n\t\t\t\tServiceName: tp.String(),\n\t\t\t\tTestName:    tp.String(),\n\t\t\t\tFailed:      tp.Bool(),\n\t\t\t\tSkipped:     tp.Bool(),\n\t\t\t\tUid: (func() *string {\n\t\t\t\t\tif tp.version >= 17 {\n\t\t\t\t\t\treturn ptrOrNil(tp.String())\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t})(),\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc (tp *traceParser) rpcCallStart() *tracepb2.RPCCallStart {\n\treturn &tracepb2.RPCCallStart{\n\t\tTargetServiceName:  tp.String(),\n\t\tTargetEndpointName: tp.String(),\n\t\tStack:              tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) rpcCallEnd() *tracepb2.RPCCallEnd {\n\treturn &tracepb2.RPCCallEnd{\n\t\tErr: tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) dbQueryStart() *tracepb2.DBQueryStart {\n\treturn &tracepb2.DBQueryStart{\n\t\tQuery: tp.String(),\n\t\tStack: tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) dbQueryEnd() *tracepb2.DBQueryEnd {\n\treturn &tracepb2.DBQueryEnd{\n\t\tErr: tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) dbTransactionStart() *tracepb2.DBTransactionStart {\n\treturn &tracepb2.DBTransactionStart{\n\t\tStack: tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) dbTransactionEnd() *tracepb2.DBTransactionEnd {\n\treturn &tracepb2.DBTransactionEnd{\n\t\tCompletion: (func() tracepb2.DBTransactionEnd_CompletionType {\n\t\t\tif commit := tp.Bool(); commit {\n\t\t\t\treturn tracepb2.DBTransactionEnd_COMMIT\n\t\t\t} else {\n\t\t\t\treturn tracepb2.DBTransactionEnd_ROLLBACK\n\t\t\t}\n\t\t})(),\n\t\tStack: tp.stack(),\n\t\tErr:   tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) pubsubPublishStart() *tracepb2.PubsubPublishStart {\n\treturn &tracepb2.PubsubPublishStart{\n\t\tTopic:   tp.String(),\n\t\tMessage: tp.ByteString(),\n\t\tStack:   tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) pubsubPublishEnd() *tracepb2.PubsubPublishEnd {\n\treturn &tracepb2.PubsubPublishEnd{\n\t\tMessageId: ptrOrNil(tp.String()),\n\t\tErr:       tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) serviceInitStart() *tracepb2.ServiceInitStart {\n\treturn &tracepb2.ServiceInitStart{\n\t\tService: tp.String(),\n\t}\n}\n\nfunc (tp *traceParser) serviceInitEnd() *tracepb2.ServiceInitEnd {\n\treturn &tracepb2.ServiceInitEnd{\n\t\tErr: tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) httpCallStart() *tracepb2.HTTPCallStart {\n\treturn &tracepb2.HTTPCallStart{\n\t\tCorrelationParentSpanId: tp.Uint64(),\n\t\tMethod:                  tp.String(),\n\t\tUrl:                     tp.String(),\n\t\tStack:                   tp.stack(),\n\t\tStartNanotime:           tp.Int64(),\n\t}\n}\n\nfunc (tp *traceParser) httpCallEnd() *tracepb2.HTTPCallEnd {\n\treturn &tracepb2.HTTPCallEnd{\n\t\tStatusCode: ptrOrNil(uint32(tp.UVarint())),\n\t\tErr:        tp.errWithStack(),\n\t\tTraceEvents: (func() []*tracepb2.HTTPTraceEvent {\n\t\t\tn := tp.UVarint()\n\t\t\tevents := make([]*tracepb2.HTTPTraceEvent, 0, n)\n\t\t\tfor i := 0; i < int(n); i++ {\n\t\t\t\tif ev := tp.httpEvent(); ev != nil {\n\t\t\t\t\tevents = append(events, ev)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn events\n\t\t})(),\n\t}\n}\n\nfunc (tp *traceParser) cacheCallStart() *tracepb2.CacheCallStart {\n\treturn &tracepb2.CacheCallStart{\n\t\tOperation: tp.String(),\n\t\tWrite:     tp.Bool(),\n\t\tStack:     tp.stack(),\n\t\tKeys: (func() []string {\n\t\t\tn := tp.UVarint()\n\t\t\tkeys := make([]string, n)\n\t\t\tfor i := 0; i < int(n); i++ {\n\t\t\t\tkeys[i] = tp.String()\n\t\t\t}\n\t\t\treturn keys\n\t\t})(),\n\t}\n}\n\nfunc (tp *traceParser) cacheCallEnd() *tracepb2.CacheCallEnd {\n\treturn &tracepb2.CacheCallEnd{\n\t\tResult: (func() tracepb2.CacheCallEnd_Result {\n\t\t\tres := tp.Byte()\n\t\t\tswitch trace2.CacheCallResult(res) {\n\t\t\tcase trace2.CacheOK:\n\t\t\t\treturn tracepb2.CacheCallEnd_OK\n\t\t\tcase trace2.CacheNoSuchKey:\n\t\t\t\treturn tracepb2.CacheCallEnd_NO_SUCH_KEY\n\t\t\tcase trace2.CacheConflict:\n\t\t\t\treturn tracepb2.CacheCallEnd_CONFLICT\n\t\t\tcase trace2.CacheErr:\n\t\t\t\treturn tracepb2.CacheCallEnd_ERR\n\t\t\tdefault:\n\t\t\t\treturn tracepb2.CacheCallEnd_UNKNOWN\n\t\t\t}\n\t\t})(),\n\t\tErr: tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectUploadStart() *tracepb2.BucketObjectUploadStart {\n\treturn &tracepb2.BucketObjectUploadStart{\n\t\tBucket: tp.String(),\n\t\tObject: tp.String(),\n\t\tAttrs:  tp.bucketObjectAttrs(),\n\t\tStack:  tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectAttrs() *tracepb2.BucketObjectAttributes {\n\treturn &tracepb2.BucketObjectAttributes{\n\t\tSize:        tp.OptUVarint(),\n\t\tVersion:     tp.OptString(),\n\t\tEtag:        tp.OptString(),\n\t\tContentType: tp.OptString(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectUploadEnd() *tracepb2.BucketObjectUploadEnd {\n\treturn &tracepb2.BucketObjectUploadEnd{\n\t\tSize:    tp.OptUVarint(),\n\t\tVersion: tp.OptString(),\n\t\tErr:     tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectDownloadStart() *tracepb2.BucketObjectDownloadStart {\n\treturn &tracepb2.BucketObjectDownloadStart{\n\t\tBucket:  tp.String(),\n\t\tObject:  tp.String(),\n\t\tVersion: tp.OptString(),\n\t\tStack:   tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectDownloadEnd() *tracepb2.BucketObjectDownloadEnd {\n\treturn &tracepb2.BucketObjectDownloadEnd{\n\t\tSize: tp.OptUVarint(),\n\t\tErr:  tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketDeleteObjectsStart() *tracepb2.BucketDeleteObjectsStart {\n\tev := &tracepb2.BucketDeleteObjectsStart{\n\t\tBucket: tp.String(),\n\t\tStack:  tp.stack(),\n\t}\n\n\tnum := tp.UVarint()\n\tfor i := 0; i < int(num); i++ {\n\t\tev.Entries = append(ev.Entries, &tracepb2.BucketDeleteObjectEntry{\n\t\t\tObject:  tp.String(),\n\t\t\tVersion: tp.OptString(),\n\t\t})\n\t}\n\n\treturn ev\n}\n\nfunc (tp *traceParser) bucketDeleteObjectsEnd() *tracepb2.BucketDeleteObjectsEnd {\n\treturn &tracepb2.BucketDeleteObjectsEnd{\n\t\tErr: tp.errWithStack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketListObjectsStart() *tracepb2.BucketListObjectsStart {\n\treturn &tracepb2.BucketListObjectsStart{\n\t\tBucket: tp.String(),\n\t\tPrefix: tp.OptString(),\n\t\tStack:  tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketListObjectsEnd() *tracepb2.BucketListObjectsEnd {\n\treturn &tracepb2.BucketListObjectsEnd{\n\t\tErr:      tp.errWithStack(),\n\t\tObserved: tp.UVarint(),\n\t\tHasMore:  tp.Bool(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectGetAttrsStart() *tracepb2.BucketObjectGetAttrsStart {\n\treturn &tracepb2.BucketObjectGetAttrsStart{\n\t\tBucket:  tp.String(),\n\t\tObject:  tp.String(),\n\t\tVersion: tp.OptString(),\n\t\tStack:   tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) bucketObjectGetAttrsEnd() *tracepb2.BucketObjectGetAttrsEnd {\n\tev := &tracepb2.BucketObjectGetAttrsEnd{\n\t\tErr: tp.errWithStack(),\n\t}\n\n\tif ev.Err == nil {\n\t\tev.Attrs = tp.bucketObjectAttrs()\n\t}\n\n\treturn ev\n}\n\nfunc (tp *traceParser) bodyStream() *tracepb2.BodyStream {\n\tflags := tp.Byte()\n\tdata := tp.ByteString()\n\treturn &tracepb2.BodyStream{\n\t\tIsResponse: flags&0b01 == 0b01,\n\t\tOverflowed: flags&0b10 == 0b10,\n\t\tData:       data,\n\t}\n}\n\nfunc (tp *traceParser) headers() map[string]string {\n\tn := tp.UVarint()\n\tif n == 0 {\n\t\treturn nil\n\t}\n\theaders := make(map[string]string, n)\n\tfor i := 0; i < int(n); i++ {\n\t\theaders[tp.String()] = tp.String()\n\t}\n\treturn headers\n}\n\nfunc (tp *traceParser) httpEvent() *tracepb2.HTTPTraceEvent {\n\tcode := trace2.HTTPEventCode(tp.Byte())\n\tev := &tracepb2.HTTPTraceEvent{\n\t\tNanotime: tp.Int64(),\n\t}\n\n\tswitch code {\n\tcase trace2.GetConn:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_GetConn{\n\t\t\tGetConn: &tracepb2.HTTPGetConn{\n\t\t\t\tHostPort: tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.GotConn:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_GotConn{\n\t\t\tGotConn: &tracepb2.HTTPGotConn{\n\t\t\t\tReused:         tp.Bool(),\n\t\t\t\tWasIdle:        tp.Bool(),\n\t\t\t\tIdleDurationNs: tp.Int64(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.GotFirstResponseByte:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_GotFirstResponseByte{\n\t\t\tGotFirstResponseByte: &tracepb2.HTTPGotFirstResponseByte{\n\t\t\t\t// No data\n\t\t\t},\n\t\t}\n\n\tcase trace2.Got1xxResponse:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_Got_1XxResponse{\n\t\t\tGot_1XxResponse: &tracepb2.HTTPGot1XxResponse{\n\t\t\t\tCode: int32(tp.Varint()),\n\t\t\t},\n\t\t}\n\n\tcase trace2.DNSStart:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_DnsStart{\n\t\t\tDnsStart: &tracepb2.HTTPDNSStart{\n\t\t\t\tHost: tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.DNSDone:\n\t\tdata := &tracepb2.HTTPDNSDone{\n\t\t\tErr: tp.ByteString(),\n\t\t}\n\t\taddrs := int(tp.UVarint())\n\t\tfor j := 0; j < addrs; j++ {\n\t\t\tdata.Addrs = append(data.Addrs, &tracepb2.DNSAddr{\n\t\t\t\tIp: tp.ByteString(),\n\t\t\t})\n\t\t}\n\t\tev.Data = &tracepb2.HTTPTraceEvent_DnsDone{DnsDone: data}\n\n\tcase trace2.ConnectStart:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_ConnectStart{\n\t\t\tConnectStart: &tracepb2.HTTPConnectStart{\n\t\t\t\tNetwork: tp.String(),\n\t\t\t\tAddr:    tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.ConnectDone:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_ConnectDone{\n\t\t\tConnectDone: &tracepb2.HTTPConnectDone{\n\t\t\t\tNetwork: tp.String(),\n\t\t\t\tAddr:    tp.String(),\n\t\t\t\tErr:     tp.ByteString(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.TLSHandshakeStart:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_TlsHandshakeStart{\n\t\t\tTlsHandshakeStart: &tracepb2.HTTPTLSHandshakeStart{\n\t\t\t\t// No data\n\t\t\t},\n\t\t}\n\n\tcase trace2.TLSHandshakeDone:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_TlsHandshakeDone{\n\t\t\tTlsHandshakeDone: &tracepb2.HTTPTLSHandshakeDone{\n\t\t\t\tErr:                tp.ByteString(),\n\t\t\t\tTlsVersion:         tp.Uint32(),\n\t\t\t\tCipherSuite:        tp.Uint32(),\n\t\t\t\tServerName:         tp.String(),\n\t\t\t\tNegotiatedProtocol: tp.String(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.WroteHeaders:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_WroteHeaders{\n\t\t\tWroteHeaders: &tracepb2.HTTPWroteHeaders{\n\t\t\t\t// No data\n\t\t\t},\n\t\t}\n\n\tcase trace2.WroteRequest:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_WroteRequest{\n\t\t\tWroteRequest: &tracepb2.HTTPWroteRequest{\n\t\t\t\tErr: tp.ByteString(),\n\t\t\t},\n\t\t}\n\n\tcase trace2.Wait100Continue:\n\t\t// no data\n\t\tev.Data = &tracepb2.HTTPTraceEvent_Wait_100Continue{\n\t\t\tWait_100Continue: &tracepb2.HTTPWait100Continue{\n\t\t\t\t// No data\n\t\t\t},\n\t\t}\n\n\tcase trace2.ClosedBody:\n\t\tev.Data = &tracepb2.HTTPTraceEvent_ClosedBody{\n\t\t\tClosedBody: &tracepb2.HTTPClosedBodyData{\n\t\t\t\tErr: tp.ByteString(),\n\t\t\t},\n\t\t}\n\n\tdefault:\n\t\t// TODO bailout\n\t\ttp.log.Error().Int32(\"code\", int32(code)).Msg(\"unknown http event code\")\n\t\treturn nil\n\t}\n\treturn ev\n}\n\nfunc (tp *traceParser) logMessage() *tracepb2.LogMessage {\n\treturn &tracepb2.LogMessage{\n\t\tLevel: (func() tracepb2.LogMessage_Level {\n\t\t\tswitch model.LogLevel(tp.Byte()) {\n\t\t\tcase model.LevelTrace:\n\t\t\t\treturn tracepb2.LogMessage_TRACE\n\t\t\tcase model.LevelDebug:\n\t\t\t\treturn tracepb2.LogMessage_DEBUG\n\t\t\tcase model.LevelInfo:\n\t\t\t\treturn tracepb2.LogMessage_INFO\n\t\t\tcase model.LevelWarn:\n\t\t\t\treturn tracepb2.LogMessage_WARN\n\t\t\tcase model.LevelError:\n\t\t\t\treturn tracepb2.LogMessage_ERROR\n\t\t\tdefault:\n\t\t\t\treturn tracepb2.LogMessage_TRACE\n\t\t\t}\n\t\t})(),\n\t\tMsg: tp.String(),\n\t\tFields: (func() []*tracepb2.LogField {\n\t\t\tn := int(tp.UVarint())\n\t\t\tif n > 64 {\n\t\t\t\t// TODO bailout\n\t\t\t}\n\t\t\tfields := make([]*tracepb2.LogField, 0, n)\n\t\t\tfor i := 0; i < n; i++ {\n\t\t\t\tfields = append(fields, tp.logField())\n\t\t\t}\n\t\t\treturn fields\n\t\t})(),\n\t\tStack: tp.stack(),\n\t}\n}\n\nfunc (tp *traceParser) logField() *tracepb2.LogField {\n\ttyp := model.LogFieldType(tp.Byte())\n\tf := &tracepb2.LogField{\n\t\tKey: tp.String(),\n\t}\n\tswitch typ {\n\tcase model.ErrField:\n\t\tf.Value = &tracepb2.LogField_Error{Error: tp.errWithStack()}\n\tcase model.StringField:\n\t\tf.Value = &tracepb2.LogField_Str{Str: tp.String()}\n\tcase model.BoolField:\n\t\tf.Value = &tracepb2.LogField_Bool{Bool: tp.Bool()}\n\tcase model.TimeField:\n\t\tf.Value = &tracepb2.LogField_Time{Time: tp.Time()}\n\tcase model.DurationField:\n\t\tf.Value = &tracepb2.LogField_Dur{Dur: tp.Int64()}\n\tcase model.UUIDField:\n\t\tb := make([]byte, 16)\n\t\ttp.Bytes(b)\n\t\tf.Value = &tracepb2.LogField_Uuid{Uuid: b}\n\tcase model.JSONField:\n\t\tval := tp.ByteString()\n\t\terr := tp.errWithStack()\n\t\tif err != nil {\n\t\t\tf.Value = &tracepb2.LogField_Error{Error: err}\n\t\t} else {\n\t\t\tf.Value = &tracepb2.LogField_Json{Json: val}\n\t\t}\n\tcase model.IntField:\n\t\tf.Value = &tracepb2.LogField_Int{Int: tp.Varint()}\n\tcase model.UintField:\n\t\tf.Value = &tracepb2.LogField_Uint{Uint: tp.UVarint()}\n\tcase model.Float32Field:\n\t\tf.Value = &tracepb2.LogField_Float32{Float32: tp.Float32()}\n\tcase model.Float64Field:\n\t\tf.Value = &tracepb2.LogField_Float64{Float64: tp.Float64()}\n\tdefault:\n\t\t// TODO bailout\n\t\ttp.log.Error().Msgf(\"unknown log field type %v\", typ)\n\t\treturn nil\n\t}\n\treturn f\n}\n\nfunc (tp *traceParser) stack() *tracepb2.StackTrace {\n\tn := int(tp.Byte())\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\ttr := &tracepb2.StackTrace{}\n\tdiffs := make([]int64, n)\n\tfor i := 0; i < n; i++ {\n\t\tdiff := tp.Varint()\n\t\tdiffs[i] = diff\n\t}\n\ttr.Pcs = diffs\n\n\tprev := int64(0)\n\n\tpcs := make([]uint64, n)\n\tfor i := 0; i < n; i++ {\n\t\tx := prev + diffs[i]\n\t\tprev = x\n\t\tpcs[i] = uint64(x)\n\t}\n\n\treturn tr\n}\n\nfunc (tp *traceParser) formattedStack() *tracepb2.StackTrace {\n\tn := int(tp.Byte())\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\ttr := &tracepb2.StackTrace{\n\t\tFrames: make([]*tracepb2.StackFrame, n),\n\t}\n\n\tfor i := 0; i < n; i++ {\n\t\ttr.Frames[i] = &tracepb2.StackFrame{\n\t\t\tFilename: tp.String(),\n\t\t\tLine:     int32(tp.UVarint()),\n\t\t\tFunc:     tp.String(),\n\t\t}\n\t}\n\n\treturn tr\n}\n\n// statusCode parses a status code.\nfunc (tp *traceParser) statusCode() tracepb2.StatusCode {\n\treturn tracepb2.StatusCode(tp.Byte())\n}\n\n// errWithStack parses an error with stack information.\nfunc (tp *traceParser) errWithStack() *tracepb2.Error {\n\tmsg := tp.String()\n\tif len(msg) == 0 {\n\t\treturn nil\n\t}\n\tstack := tp.stack()\n\treturn &tracepb2.Error{\n\t\tMsg:   msg,\n\t\tStack: stack,\n\t}\n}\n\nfunc (tp *traceParser) traceID() *tracepb2.TraceID {\n\tvar traceID [16]byte\n\ttp.Bytes(traceID[:])\n\treturn &tracepb2.TraceID{\n\t\tLow:  bin.Uint64(traceID[:8]),\n\t\tHigh: bin.Uint64(traceID[8:]),\n\t}\n}\n\nfunc (tp *traceParser) spanID() uint64 {\n\tvar spanID [8]byte\n\ttp.Bytes(spanID[:])\n\treturn bin.Uint64(spanID[:])\n}\n\ntype bailout struct {\n\terr error\n}\n\nfunc (tp *traceParser) bailout(err error) {\n\tpanic(bailout{err: err})\n}\n\n// httpStatusToStatusCode converts an HTTP status code to a tracepb2.StatusCode.\nfunc httpStatusToStatusCode(status uint32) tracepb2.StatusCode {\n\tswitch status {\n\tcase 200:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_OK\n\tcase 499:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_CANCELED\n\tcase 500:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_INTERNAL\n\tcase 400:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_INVALID_ARGUMENT\n\tcase 401:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_UNAUTHENTICATED\n\tcase 403:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_PERMISSION_DENIED\n\tcase 404:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_NOT_FOUND\n\tcase 409:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_ALREADY_EXISTS\n\tcase 429:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_RESOURCE_EXHAUSTED\n\tcase 501:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_UNIMPLEMENTED\n\tcase 503:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_UNAVAILABLE\n\tcase 504:\n\t\treturn tracepb2.StatusCode_STATUS_CODE_DEADLINE_EXCEEDED\n\tdefault:\n\t\tif status >= 200 && status < 300 {\n\t\t\treturn tracepb2.StatusCode_STATUS_CODE_OK\n\t\t}\n\t\treturn tracepb2.StatusCode_STATUS_CODE_UNKNOWN\n\t}\n}\n"
  },
  {
    "path": "pkg/traceparser/parser_test.go",
    "content": "package traceparser\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"google.golang.org/protobuf/testing/protocmp\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/types/uuid\"\n\ttracepb2 \"encr.dev/proto/encore/engine/trace2\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttraceID := model.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}\n\tspanID := model.SpanID{8, 7, 6, 5, 4, 3, 2, 1}\n\tnow := time.Now()\n\terr := errors.New(\"some-error\")\n\tgoid := uint32(123)\n\tdefLoc := uint32(456)\n\tudefLoc := uint32(defLoc) // for compat\n\tuuidVal := uuid.UUID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}\n\n\tpbNow := timestamppb.New(now)\n\tpbTraceID := &tracepb2.TraceID{High: 1157159078456920585, Low: 578437695752307201}\n\tpbSpanID := uint64(72623859790382856)\n\tpbErr := &tracepb2.Error{Msg: \"some-error\"}\n\tpbUUID := uuidVal.Bytes()\n\n\tep := trace2.EventParams{TraceID: traceID, SpanID: spanID, Goid: goid, DefLoc: defLoc}\n\n\ttests := []struct {\n\t\tName string\n\t\tEmit func(l *trace2.Log)\n\t\tWant *tracepb2.TraceEvent\n\t}{\n\t\t{\n\t\t\tName: \"RequestSpanStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.RequestSpanStart(&model.Request{\n\t\t\t\t\tType:         model.RPCCall,\n\t\t\t\t\tTraceID:      traceID,\n\t\t\t\t\tSpanID:       spanID,\n\t\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\t\tStart:        now,\n\t\t\t\t\tTraced:       true,\n\t\t\t\t\tDefLoc:       defLoc,\n\t\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\t\tService:  \"service\",\n\t\t\t\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\t\t\t\tRaw:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHTTPMethod:     \"POST\",\n\t\t\t\t\t\tPath:           \"/path/hello\",\n\t\t\t\t\t\tPathParams:     model.PathParams{{Name: \"one\", Value: \"hello\"}},\n\t\t\t\t\t\tUserID:         \"userid\",\n\t\t\t\t\t\tAuthData:       nil,\n\t\t\t\t\t\tNonRawPayload:  []byte(`{\"Body\":\"foo\"}`),\n\t\t\t\t\t\tRequestHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t\t\t\t},\n\t\t\t\t}, goid)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanStart{SpanStart: &tracepb2.SpanStart{\n\t\t\t\t\tParentTraceId:         nil,\n\t\t\t\t\tParentSpanId:          nil,\n\t\t\t\t\tExternalCorrelationId: nil,\n\t\t\t\t\tDefLoc:                &udefLoc,\n\t\t\t\t\tGoid:                  goid,\n\t\t\t\t\tData: &tracepb2.SpanStart_Request{\n\t\t\t\t\t\tRequest: &tracepb2.RequestSpanStart{\n\t\t\t\t\t\t\tServiceName:      \"service\",\n\t\t\t\t\t\t\tEndpointName:     \"endpoint\",\n\t\t\t\t\t\t\tHttpMethod:       \"POST\",\n\t\t\t\t\t\t\tPath:             \"/path/hello\",\n\t\t\t\t\t\t\tPathParams:       []string{\"hello\"},\n\t\t\t\t\t\t\tRequestHeaders:   map[string]string{\"Content-Type\": \"application/json\"},\n\t\t\t\t\t\t\tRequestPayload:   []byte(`{\"Body\":\"foo\"}`),\n\t\t\t\t\t\t\tExtCorrelationId: nil,\n\t\t\t\t\t\t\tUid:              ptr(\"userid\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"RequestSpanEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.RequestSpanEnd(trace2.RequestSpanEndParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tReq: &model.Request{\n\t\t\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\t\t\tService:  \"service\",\n\t\t\t\t\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tResp: &model.Response{\n\t\t\t\t\t\tHTTPStatus:         123,\n\t\t\t\t\t\tErr:                err,\n\t\t\t\t\t\tPayload:            []byte(\"payload\"),\n\t\t\t\t\t\tRawResponseHeaders: map[string][]string{\"Content-Type\": {\"application/json\"}},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEnd{SpanEnd: &tracepb2.SpanEnd{\n\t\t\t\t\tStatusCode: tracepb2.StatusCode_STATUS_CODE_UNKNOWN,\n\t\t\t\t\tError:      pbErr,\n\t\t\t\t\tData: &tracepb2.SpanEnd_Request{\n\t\t\t\t\t\tRequest: &tracepb2.RequestSpanEnd{\n\t\t\t\t\t\t\tServiceName:     \"service\",\n\t\t\t\t\t\t\tEndpointName:    \"endpoint\",\n\t\t\t\t\t\t\tHttpStatusCode:  123,\n\t\t\t\t\t\t\tResponseHeaders: map[string]string{\"Content-Type\": \"application/json\"},\n\t\t\t\t\t\t\tResponsePayload: []byte(\"payload\"),\n\t\t\t\t\t\t\tCallerEventId:   ptr(uint64(0)),\n\t\t\t\t\t\t\tUid:             nil, // ptrOrNil returns nil for empty strings\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"AuthSpanStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.AuthSpanStart(&model.Request{\n\t\t\t\t\tType:         model.AuthHandler,\n\t\t\t\t\tTraceID:      traceID,\n\t\t\t\t\tSpanID:       spanID,\n\t\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\t\tStart:        now,\n\t\t\t\t\tTraced:       true,\n\t\t\t\t\tDefLoc:       defLoc,\n\t\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\t\tService:  \"service\",\n\t\t\t\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\t\t\t\tRaw:      false,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tNonRawPayload: []byte(`{\"Body\":\"foo\"}`),\n\t\t\t\t\t},\n\t\t\t\t}, goid)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanStart{SpanStart: &tracepb2.SpanStart{\n\t\t\t\t\tParentTraceId:         nil,\n\t\t\t\t\tParentSpanId:          nil,\n\t\t\t\t\tExternalCorrelationId: nil,\n\t\t\t\t\tDefLoc:                &udefLoc,\n\t\t\t\t\tGoid:                  goid,\n\t\t\t\t\tData: &tracepb2.SpanStart_Auth{\n\t\t\t\t\t\tAuth: &tracepb2.AuthSpanStart{\n\t\t\t\t\t\t\tServiceName:  \"service\",\n\t\t\t\t\t\t\tEndpointName: \"endpoint\",\n\t\t\t\t\t\t\tAuthPayload:  []byte(`{\"Body\":\"foo\"}`),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"AuthSpanEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.AuthSpanEnd(trace2.AuthSpanEndParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tReq: &model.Request{\n\t\t\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\t\t\tService:  \"service\",\n\t\t\t\t\t\t\t\tEndpoint: \"endpoint\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tResp: &model.Response{\n\t\t\t\t\t\tHTTPStatus: 123,\n\t\t\t\t\t\tAuthUID:    \"userid\",\n\t\t\t\t\t\tErr:        err,\n\t\t\t\t\t\tPayload:    []byte(\"payload\"),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEnd{SpanEnd: &tracepb2.SpanEnd{\n\t\t\t\t\tStatusCode: tracepb2.StatusCode_STATUS_CODE_UNKNOWN,\n\t\t\t\t\tError:      pbErr,\n\t\t\t\t\tData: &tracepb2.SpanEnd_Auth{\n\t\t\t\t\t\tAuth: &tracepb2.AuthSpanEnd{\n\t\t\t\t\t\t\tServiceName:  \"service\",\n\t\t\t\t\t\t\tEndpointName: \"endpoint\",\n\t\t\t\t\t\t\tUid:          \"userid\",\n\t\t\t\t\t\t\tUserData:     []byte(\"payload\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"PubsubMessageSpanStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.PubsubMessageSpanStart(&model.Request{\n\t\t\t\t\tType:         model.PubSubMessage,\n\t\t\t\t\tTraceID:      traceID,\n\t\t\t\t\tSpanID:       spanID,\n\t\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\t\tStart:        now,\n\t\t\t\t\tTraced:       true,\n\t\t\t\t\tDefLoc:       defLoc,\n\t\t\t\t\tMsgData: &model.PubSubMsgData{\n\t\t\t\t\t\tDesc: &model.PubSubSubscriptionDesc{\n\t\t\t\t\t\t\tService:      \"service\",\n\t\t\t\t\t\t\tTopic:        \"topic\",\n\t\t\t\t\t\t\tSubscription: \"subscription\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMessageID: \"message-id\",\n\t\t\t\t\t\tAttempt:   3,\n\t\t\t\t\t\tPublished: now,\n\t\t\t\t\t\tPayload:   []byte(\"payload\"),\n\t\t\t\t\t},\n\t\t\t\t}, goid)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanStart{SpanStart: &tracepb2.SpanStart{\n\t\t\t\t\tParentTraceId:         nil,\n\t\t\t\t\tParentSpanId:          nil,\n\t\t\t\t\tExternalCorrelationId: nil,\n\t\t\t\t\tDefLoc:                &udefLoc,\n\t\t\t\t\tGoid:                  goid,\n\t\t\t\t\tData: &tracepb2.SpanStart_PubsubMessage{\n\t\t\t\t\t\tPubsubMessage: &tracepb2.PubsubMessageSpanStart{\n\t\t\t\t\t\t\tServiceName:      \"service\",\n\t\t\t\t\t\t\tTopicName:        \"topic\",\n\t\t\t\t\t\t\tSubscriptionName: \"subscription\",\n\t\t\t\t\t\t\tMessageId:        \"message-id\",\n\t\t\t\t\t\t\tAttempt:          3,\n\t\t\t\t\t\t\tPublishTime:      pbNow,\n\t\t\t\t\t\t\tMessagePayload:   []byte(\"payload\"),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"PubsubMessageSpanEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.PubsubMessageSpanEnd(trace2.PubsubMessageSpanEndParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tReq: &model.Request{\n\t\t\t\t\t\tMsgData: &model.PubSubMsgData{\n\t\t\t\t\t\t\tDesc: &model.PubSubSubscriptionDesc{\n\t\t\t\t\t\t\t\tService:      \"service\",\n\t\t\t\t\t\t\t\tTopic:        \"topic\",\n\t\t\t\t\t\t\t\tSubscription: \"subscription\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tResp: &model.Response{Err: err},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEnd{SpanEnd: &tracepb2.SpanEnd{\n\t\t\t\t\tStatusCode: tracepb2.StatusCode_STATUS_CODE_UNKNOWN,\n\t\t\t\t\tError:      pbErr,\n\t\t\t\t\tData: &tracepb2.SpanEnd_PubsubMessage{\n\t\t\t\t\t\tPubsubMessage: &tracepb2.PubsubMessageSpanEnd{\n\t\t\t\t\t\t\tServiceName:      \"service\",\n\t\t\t\t\t\t\tTopicName:        \"topic\",\n\t\t\t\t\t\t\tSubscriptionName: \"subscription\",\n\t\t\t\t\t\t\tMessageId:        \"\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"RPCCallStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.RPCCallStart(&model.APICall{\n\t\t\t\t\tSource:             &model.Request{TraceID: traceID, SpanID: spanID},\n\t\t\t\t\tTargetServiceName:  \"service\",\n\t\t\t\t\tTargetEndpointName: \"endpoint\",\n\t\t\t\t\tDefLoc:             defLoc,\n\t\t\t\t\tStartEventID:       0,\n\t\t\t\t}, goid)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:   goid,\n\t\t\t\t\tDefLoc: &udefLoc,\n\t\t\t\t\tData: &tracepb2.SpanEvent_RpcCallStart{\n\t\t\t\t\t\tRpcCallStart: &tracepb2.RPCCallStart{\n\t\t\t\t\t\t\tTargetServiceName:  \"service\",\n\t\t\t\t\t\t\tTargetEndpointName: \"endpoint\",\n\t\t\t\t\t\t\tStack:              nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"RPCCallEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.RPCCallEnd(&model.APICall{\n\t\t\t\t\tSource:       &model.Request{TraceID: traceID, SpanID: spanID},\n\t\t\t\t\tDefLoc:       defLoc,\n\t\t\t\t\tStartEventID: 1,\n\t\t\t\t}, goid, err)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_RpcCallEnd{\n\t\t\t\t\t\tRpcCallEnd: &tracepb2.RPCCallEnd{\n\t\t\t\t\t\t\tErr: pbErr,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"DBQueryStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tTxStartID:   1,\n\t\t\t\t\tQuery:       \"query\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tDefLoc:             &udefLoc,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_DbQueryStart{\n\t\t\t\t\t\tDbQueryStart: &tracepb2.DBQueryStart{\n\t\t\t\t\t\t\tQuery: \"query\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"DBQueryEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.DBQueryEnd(ep, 1, err)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tDefLoc:             &udefLoc,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_DbQueryEnd{\n\t\t\t\t\t\tDbQueryEnd: &tracepb2.DBQueryEnd{\n\t\t\t\t\t\t\tErr: pbErr,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"DBTransactionStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.DBTransactionStart(ep, stack.Stack{})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:   goid,\n\t\t\t\t\tDefLoc: &udefLoc,\n\t\t\t\t\tData: &tracepb2.SpanEvent_DbTransactionStart{\n\t\t\t\t\t\tDbTransactionStart: &tracepb2.DBTransactionStart{},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"DBTransactionEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.DBTransactionEnd(trace2.DBTransactionEndParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tStartID:     1,\n\t\t\t\t\tCommit:      true,\n\t\t\t\t\tErr:         err,\n\t\t\t\t\tStack:       stack.Stack{},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tDefLoc:             &udefLoc,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_DbTransactionEnd{\n\t\t\t\t\t\tDbTransactionEnd: &tracepb2.DBTransactionEnd{\n\t\t\t\t\t\t\tCompletion: tracepb2.DBTransactionEnd_COMMIT,\n\t\t\t\t\t\t\tErr:        pbErr,\n\t\t\t\t\t\t\tStack:      nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"PubsubPublishStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.PubsubPublishStart(trace2.PubsubPublishStartParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tDesc: &model.PubSubTopicDesc{\n\t\t\t\t\t\tTopic: \"topic\",\n\t\t\t\t\t},\n\t\t\t\t\tMessage: []byte(\"message\"),\n\t\t\t\t\tStack:   stack.Stack{},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:   goid,\n\t\t\t\t\tDefLoc: &udefLoc,\n\t\t\t\t\tData: &tracepb2.SpanEvent_PubsubPublishStart{\n\t\t\t\t\t\tPubsubPublishStart: &tracepb2.PubsubPublishStart{\n\t\t\t\t\t\t\tTopic:   \"topic\",\n\t\t\t\t\t\t\tMessage: []byte(\"message\"),\n\t\t\t\t\t\t\tStack:   nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"PubsubPublishEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.PubsubPublishEnd(trace2.PubsubPublishEndParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tStartID:     1,\n\t\t\t\t\tMessageID:   \"message-id\",\n\t\t\t\t\tErr:         err,\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tDefLoc:             &udefLoc,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_PubsubPublishEnd{\n\t\t\t\t\t\tPubsubPublishEnd: &tracepb2.PubsubPublishEnd{\n\t\t\t\t\t\t\tMessageId: ptr(\"message-id\"),\n\t\t\t\t\t\t\tErr:       pbErr,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"ServiceInitStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.ServiceInitStart(trace2.ServiceInitStartParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tService:     \"service\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:   goid,\n\t\t\t\t\tDefLoc: &udefLoc,\n\t\t\t\t\tData: &tracepb2.SpanEvent_ServiceInitStart{\n\t\t\t\t\t\tServiceInitStart: &tracepb2.ServiceInitStart{\n\t\t\t\t\t\t\tService: \"service\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"ServiceInitEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.ServiceInitEnd(ep, 1, err)\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tDefLoc:             &udefLoc,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_ServiceInitEnd{\n\t\t\t\t\t\tServiceInitEnd: &tracepb2.ServiceInitEnd{\n\t\t\t\t\t\t\tErr: pbErr,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"CacheCallStart\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.CacheCallStart(trace2.CacheCallStartParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tOperation:   \"operation\",\n\t\t\t\t\tIsWrite:     true,\n\t\t\t\t\tKeys:        []string{\"one\", \"two\"},\n\t\t\t\t\tStack:       stack.Stack{},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:   goid,\n\t\t\t\t\tDefLoc: &udefLoc,\n\t\t\t\t\tData: &tracepb2.SpanEvent_CacheCallStart{\n\t\t\t\t\t\tCacheCallStart: &tracepb2.CacheCallStart{\n\t\t\t\t\t\t\tOperation: \"operation\",\n\t\t\t\t\t\t\tWrite:     true,\n\t\t\t\t\t\t\tKeys:      []string{\"one\", \"two\"},\n\t\t\t\t\t\t\tStack:     nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"CacheCallEnd\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.CacheCallEnd(trace2.CacheCallEndParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tStartID:     1,\n\t\t\t\t\tRes:         trace2.CacheErr,\n\t\t\t\t\tErr:         err,\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:               goid,\n\t\t\t\t\tDefLoc:             &udefLoc,\n\t\t\t\t\tCorrelationEventId: ptr[uint64](1),\n\t\t\t\t\tData: &tracepb2.SpanEvent_CacheCallEnd{\n\t\t\t\t\t\tCacheCallEnd: &tracepb2.CacheCallEnd{\n\t\t\t\t\t\t\tResult: tracepb2.CacheCallEnd_ERR,\n\t\t\t\t\t\t\tErr:    pbErr,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tName: \"LogMessage\",\n\t\t\tEmit: func(l *trace2.Log) {\n\t\t\t\tl.LogMessage(trace2.LogMessageParams{\n\t\t\t\t\tEventParams: ep,\n\t\t\t\t\tLevel:       model.LevelWarn,\n\t\t\t\t\tMsg:         \"message\",\n\t\t\t\t\tStack:       stack.Stack{},\n\t\t\t\t\tFields: []trace2.LogField{\n\t\t\t\t\t\t{Key: \"error\", Value: err},\n\t\t\t\t\t\t{Key: \"string\", Value: \"string\"},\n\t\t\t\t\t\t{Key: \"bool\", Value: true},\n\t\t\t\t\t\t{Key: \"time\", Value: now},\n\t\t\t\t\t\t{Key: \"duration\", Value: time.Second},\n\t\t\t\t\t\t{Key: \"uuid\", Value: uuidVal},\n\t\t\t\t\t\t{Key: \"json\", Value: map[string]any{\"json\": true}},\n\t\t\t\t\t\t{Key: \"json_err\", Value: func() {}},\n\t\t\t\t\t\t{Key: \"int8\", Value: int8(-8)},\n\t\t\t\t\t\t{Key: \"int16\", Value: int16(-16)},\n\t\t\t\t\t\t{Key: \"int32\", Value: int32(-32)},\n\t\t\t\t\t\t{Key: \"int64\", Value: int64(-64)},\n\t\t\t\t\t\t{Key: \"int\", Value: int(-1)},\n\t\t\t\t\t\t{Key: \"uint8\", Value: uint8(8)},\n\t\t\t\t\t\t{Key: \"uint16\", Value: uint16(16)},\n\t\t\t\t\t\t{Key: \"uint32\", Value: uint32(32)},\n\t\t\t\t\t\t{Key: \"uint64\", Value: uint64(64)},\n\t\t\t\t\t\t{Key: \"uint\", Value: uint(1)},\n\t\t\t\t\t\t{Key: \"float32\", Value: float32(1.2)},\n\t\t\t\t\t\t{Key: \"float64\", Value: float64(3.4)},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t},\n\t\t\tWant: &tracepb2.TraceEvent{\n\t\t\t\tTraceId: pbTraceID,\n\t\t\t\tSpanId:  pbSpanID,\n\t\t\t\tEvent: &tracepb2.TraceEvent_SpanEvent{SpanEvent: &tracepb2.SpanEvent{\n\t\t\t\t\tGoid:   goid,\n\t\t\t\t\tDefLoc: &udefLoc,\n\t\t\t\t\tData: &tracepb2.SpanEvent_LogMessage{\n\t\t\t\t\t\tLogMessage: &tracepb2.LogMessage{\n\t\t\t\t\t\t\tLevel: tracepb2.LogMessage_WARN,\n\t\t\t\t\t\t\tMsg:   \"message\",\n\t\t\t\t\t\t\tStack: nil,\n\t\t\t\t\t\t\tFields: []*tracepb2.LogField{\n\t\t\t\t\t\t\t\t{Key: \"error\", Value: &tracepb2.LogField_Error{Error: pbErr}},\n\t\t\t\t\t\t\t\t{Key: \"string\", Value: &tracepb2.LogField_Str{Str: \"string\"}},\n\t\t\t\t\t\t\t\t{Key: \"bool\", Value: &tracepb2.LogField_Bool{Bool: true}},\n\t\t\t\t\t\t\t\t{Key: \"time\", Value: &tracepb2.LogField_Time{Time: pbNow}},\n\t\t\t\t\t\t\t\t{Key: \"duration\", Value: &tracepb2.LogField_Dur{Dur: int64(time.Second)}},\n\t\t\t\t\t\t\t\t{Key: \"uuid\", Value: &tracepb2.LogField_Uuid{Uuid: pbUUID}},\n\t\t\t\t\t\t\t\t{Key: \"json\", Value: &tracepb2.LogField_Json{Json: []byte(`{\"json\":true}`)}},\n\t\t\t\t\t\t\t\t{Key: \"json_err\", Value: &tracepb2.LogField_Error{Error: &tracepb2.Error{Msg: \"json: unsupported type: func()\"}}},\n\t\t\t\t\t\t\t\t{Key: \"int8\", Value: &tracepb2.LogField_Int{Int: -8}},\n\t\t\t\t\t\t\t\t{Key: \"int16\", Value: &tracepb2.LogField_Int{Int: -16}},\n\t\t\t\t\t\t\t\t{Key: \"int32\", Value: &tracepb2.LogField_Int{Int: -32}},\n\t\t\t\t\t\t\t\t{Key: \"int64\", Value: &tracepb2.LogField_Int{Int: -64}},\n\t\t\t\t\t\t\t\t{Key: \"int\", Value: &tracepb2.LogField_Int{Int: -1}},\n\t\t\t\t\t\t\t\t{Key: \"uint8\", Value: &tracepb2.LogField_Uint{Uint: 8}},\n\t\t\t\t\t\t\t\t{Key: \"uint16\", Value: &tracepb2.LogField_Uint{Uint: 16}},\n\t\t\t\t\t\t\t\t{Key: \"uint32\", Value: &tracepb2.LogField_Uint{Uint: 32}},\n\t\t\t\t\t\t\t\t{Key: \"uint64\", Value: &tracepb2.LogField_Uint{Uint: 64}},\n\t\t\t\t\t\t\t\t{Key: \"uint\", Value: &tracepb2.LogField_Uint{Uint: 1}},\n\t\t\t\t\t\t\t\t{Key: \"float32\", Value: &tracepb2.LogField_Float32{Float32: 1.2}},\n\t\t\t\t\t\t\t\t{Key: \"float64\", Value: &tracepb2.LogField_Float64{Float64: 3.4}},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name, func(t *testing.T) {\n\t\t\tlog := trace2.NewLog()\n\t\t\ttt.Emit(log)\n\t\t\tdata, _ := log.GetAndClear()\n\t\t\tta := trace2.NewTimeAnchor(0, now)\n\t\t\tgot, err := ParseEvent(bufio.NewReader(bytes.NewReader(data)), ta, 99)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\topt := []cmp.Option{\n\t\t\t\tprotocmp.Transform(),\n\t\t\t\tprotocmp.IgnoreFields(&tracepb2.TraceEvent{}, \"event_time\"),\n\t\t\t\tprotocmp.IgnoreFields(&tracepb2.TraceEvent{}, \"event_id\"),\n\t\t\t\tprotocmp.IgnoreMessages(&tracepb2.StackTrace{}),\n\t\t\t}\n\t\t\tif diff := cmp.Diff(tt.Want, got, opt...); diff != \"\" {\n\t\t\t\tt.Errorf(\"ParseEvent() mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc ptr[T any](val T) *T {\n\treturn &val\n}\n"
  },
  {
    "path": "pkg/vcs/app.go",
    "content": "package vcs\n\nimport (\n\t\"github.com/rs/zerolog/log\"\n)\n\n// GetRevision returns the version control system (VCS) revision information for the Encore application.\n//\n// If there is an error getting the revision information, no revision information is returned and the App is flagged as\n// having uncommitted files. This will happen most likely because no supported VCS system can be found.\n//\n// Supported VCS systems include;\n//  - Hg\n//  - Git\n//  - Svn\n//  - Bzr\n//  - Fossil\nfunc GetRevision(appRoot string) Status {\n\tappRoot, cmd, err := fromDir(appRoot, \"\", false)\n\tif err != nil {\n\t\tlog.Err(err).Str(\"app\", appRoot).Msg(\"unable to determine VCS system\")\n\t\treturn Status{Uncommitted: true}\n\t}\n\n\tstatus, err := cmd.Status(cmd, appRoot)\n\tif err != nil {\n\t\tlog.Err(err).Str(\"app\", appRoot).Msg(\"unable to get VCS status\")\n\t\treturn Status{Uncommitted: true}\n\t}\n\n\treturn status\n}\n"
  },
  {
    "path": "pkg/vcs/vcs.go",
    "content": "// Simplified version of the version from Go Get:\n// https://github.com/golang/go/blob/f87e28d1b9ab33491b32255f333f1f1d83eeb6fc/src/cmd/go/internal/vcs/vcs.go\n// Copyright 2012 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage vcs\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\turlpkg \"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// A cmd describes how to use a version control system\n// like Mercurial, Git, or Subversion.\ntype cmd struct {\n\tName      string\n\tCmd       string   // name of binary to invoke command\n\tRootNames []string // filename indicating the root of a checkout directory\n\n\tCreateCmd   []string // commands to download a fresh copy of a repository\n\tDownloadCmd []string // commands to download updates into an existing repository\n\n\tTagCmd         []tagCmd // commands to list tags\n\tTagLookupCmd   []tagCmd // commands to lookup tags before running tagSyncCmd\n\tTagSyncCmd     []string // commands to sync to specific tag\n\tTagSyncDefault []string // commands to sync to default tag\n\n\tScheme  []string\n\tPingCmd string\n\n\tRemoteRepo  func(v *cmd, rootDir string) (remoteRepo string, err error)\n\tResolveRepo func(v *cmd, rootDir, remoteRepo string) (realRepo string, err error)\n\tStatus      func(v *cmd, rootDir string) (Status, error)\n}\n\n// Status is the current state of a local repository.\ntype Status struct {\n\tRevision    string    // Optional.\n\tCommitTime  time.Time // Optional.\n\tUncommitted bool      // Required.\n}\n\nvar defaultSecureScheme = map[string]bool{\n\t\"https\":   true,\n\t\"git+ssh\": true,\n\t\"bzr+ssh\": true,\n\t\"svn+ssh\": true,\n\t\"ssh\":     true,\n}\n\nfunc (v *cmd) IsSecure(repo string) bool {\n\tu, err := urlpkg.Parse(repo)\n\tif err != nil {\n\t\t// If repo is not a URL, it's not secure.\n\t\treturn false\n\t}\n\treturn v.isSecureScheme(u.Scheme)\n}\n\nfunc (v *cmd) isSecureScheme(scheme string) bool {\n\tswitch v.Cmd {\n\tcase \"git\":\n\t\t// GIT_ALLOW_PROTOCOL is an environment variable defined by Git. It is a\n\t\t// colon-separated list of schemes that are allowed to be used with git\n\t\t// fetch/clone. Any scheme not mentioned will be considered insecure.\n\t\tif allow := os.Getenv(\"GIT_ALLOW_PROTOCOL\"); allow != \"\" {\n\t\t\tfor _, s := range strings.Split(allow, \":\") {\n\t\t\t\tif s == scheme {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t}\n\treturn defaultSecureScheme[scheme]\n}\n\n// A tagCmd describes a command to list available tags\n// that can be passed to tagSyncCmd.\ntype tagCmd struct {\n\tcmd     string // command to list tags\n\tpattern string // regexp to extract tags from list\n}\n\n// vcsList lists the known version control systems\nvar vcsList = []*cmd{\n\tvcsHg,\n\tvcsGit,\n\tvcsSvn,\n\tvcsBzr,\n\tvcsFossil,\n}\n\n// vcsHg describes how to use Mercurial.\nvar vcsHg = &cmd{\n\tName:      \"Mercurial\",\n\tCmd:       \"hg\",\n\tRootNames: []string{\".hg\"},\n\n\tCreateCmd:   []string{\"clone -U -- {repo} {dir}\"},\n\tDownloadCmd: []string{\"pull\"},\n\n\t// We allow both tag and branch names as 'tags'\n\t// for selecting a version. This lets people have\n\t// a go.release.r60 branch and a go1 branch\n\t// and make changes in both, without constantly\n\t// editing .hgtags.\n\tTagCmd: []tagCmd{\n\t\t{\"tags\", `^(\\S+)`},\n\t\t{\"branches\", `^(\\S+)`},\n\t},\n\tTagSyncCmd:     []string{\"update -r {tag}\"},\n\tTagSyncDefault: []string{\"update default\"},\n\n\tScheme:     []string{\"https\", \"http\", \"ssh\"},\n\tPingCmd:    \"identify -- {scheme}://{repo}\",\n\tRemoteRepo: hgRemoteRepo,\n\tStatus:     hgStatus,\n}\n\nfunc hgRemoteRepo(vcsHg *cmd, rootDir string) (remoteRepo string, err error) {\n\tout, err := vcsHg.runOutput(rootDir, \"paths default\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n\nfunc hgStatus(vcsHg *cmd, rootDir string) (Status, error) {\n\t// Output changeset ID and seconds since epoch.\n\tout, err := vcsHg.runOutputVerboseOnly(rootDir, `log -l1 -T {node}:{date|hgdate}`)\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\n\t// Successful execution without output indicates an empty repo (no commits).\n\tvar rev string\n\tvar commitTime time.Time\n\tif len(out) > 0 {\n\t\t// Strip trailing timezone offset.\n\t\tif i := bytes.IndexByte(out, ' '); i > 0 {\n\t\t\tout = out[:i]\n\t\t}\n\t\trev, commitTime, err = parseRevTime(out)\n\t\tif err != nil {\n\t\t\treturn Status{}, err\n\t\t}\n\t}\n\n\t// Also look for untracked files.\n\tout, err = vcsHg.runOutputVerboseOnly(rootDir, \"status\")\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\tuncommitted := len(out) > 0\n\n\treturn Status{\n\t\tRevision:    rev,\n\t\tCommitTime:  commitTime,\n\t\tUncommitted: uncommitted,\n\t}, nil\n}\n\n// parseRevTime parses commit details in \"revision:seconds\" format.\nfunc parseRevTime(out []byte) (string, time.Time, error) {\n\tbuf := string(bytes.TrimSpace(out))\n\n\ti := strings.IndexByte(buf, ':')\n\tif i < 1 {\n\t\treturn \"\", time.Time{}, errors.New(\"unrecognized VCS tool output\")\n\t}\n\trev := buf[:i]\n\n\tsecs, err := strconv.ParseInt(string(buf[i+1:]), 10, 64)\n\tif err != nil {\n\t\treturn \"\", time.Time{}, fmt.Errorf(\"unrecognized VCS tool output: %v\", err)\n\t}\n\n\treturn rev, time.Unix(secs, 0), nil\n}\n\n// vcsGit describes how to use Git.\nvar vcsGit = &cmd{\n\tName:      \"Git\",\n\tCmd:       \"git\",\n\tRootNames: []string{\".git\"},\n\n\tCreateCmd:   []string{\"clone -- {repo} {dir}\", \"-go-internal-cd {dir} submodule update --init --recursive\"},\n\tDownloadCmd: []string{\"pull --ff-only\", \"submodule update --init --recursive\"},\n\n\tTagCmd: []tagCmd{\n\t\t// tags/xxx matches a git tag named xxx\n\t\t// origin/xxx matches a git branch named xxx on the default remote repository\n\t\t{\"show-ref\", `(?:tags|origin)/(\\S+)$`},\n\t},\n\tTagLookupCmd: []tagCmd{\n\t\t{\"show-ref tags/{tag} origin/{tag}\", `((?:tags|origin)/\\S+)$`},\n\t},\n\tTagSyncCmd: []string{\"checkout {tag}\", \"submodule update --init --recursive\"},\n\t// both createCmd and downloadCmd update the working dir.\n\t// No need to do more here. We used to 'checkout master'\n\t// but that doesn't work if the default branch is not named master.\n\t// DO NOT add 'checkout master' here.\n\t// See golang.org/issue/9032.\n\tTagSyncDefault: []string{\"submodule update --init --recursive\"},\n\n\tScheme: []string{\"git\", \"https\", \"http\", \"git+ssh\", \"ssh\"},\n\n\t// Leave out the '--' separator in the ls-remote command: git 2.7.4 does not\n\t// support such a separator for that command, and this use should be safe\n\t// without it because the {scheme} value comes from the predefined list above.\n\t// See golang.org/issue/33836.\n\tPingCmd: \"ls-remote {scheme}://{repo}\",\n\n\tRemoteRepo: gitRemoteRepo,\n\tStatus:     gitStatus,\n}\n\n// scpSyntaxRe matches the SCP-like addresses used by Git to access\n// repositories by SSH.\nvar scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)\n\nfunc gitRemoteRepo(vcsGit *cmd, rootDir string) (remoteRepo string, err error) {\n\tcmd := \"config remote.origin.url\"\n\terrParse := errors.New(\"unable to parse output of git \" + cmd)\n\terrRemoteOriginNotFound := errors.New(\"remote origin not found\")\n\toutb, err := vcsGit.run1(rootDir, cmd, nil, false)\n\tif err != nil {\n\t\t// if it doesn't output any message, it means the config argument is correct,\n\t\t// but the config value itself doesn't exist\n\t\tif outb != nil && len(outb) == 0 {\n\t\t\treturn \"\", errRemoteOriginNotFound\n\t\t}\n\t\treturn \"\", err\n\t}\n\tout := strings.TrimSpace(string(outb))\n\n\tvar repoURL *urlpkg.URL\n\tif m := scpSyntaxRe.FindStringSubmatch(out); m != nil {\n\t\t// Match SCP-like syntax and convert it to a URL.\n\t\t// Eg, \"git@github.com:user/repo\" becomes\n\t\t// \"ssh://git@github.com/user/repo\".\n\t\trepoURL = &urlpkg.URL{\n\t\t\tScheme: \"ssh\",\n\t\t\tUser:   urlpkg.User(m[1]),\n\t\t\tHost:   m[2],\n\t\t\tPath:   m[3],\n\t\t}\n\t} else {\n\t\trepoURL, err = urlpkg.Parse(out)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// Iterate over insecure schemes too, because this function simply\n\t// reports the state of the repo. If we can't see insecure schemes then\n\t// we can't report the actual repo URL.\n\tfor _, s := range vcsGit.Scheme {\n\t\tif repoURL.Scheme == s {\n\t\t\treturn repoURL.String(), nil\n\t\t}\n\t}\n\treturn \"\", errParse\n}\n\nfunc gitStatus(vcsGit *cmd, rootDir string) (Status, error) {\n\tout, err := vcsGit.runOutputVerboseOnly(rootDir, \"status --porcelain\")\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\tuncommitted := len(out) > 0\n\n\t// \"git status\" works for empty repositories, but \"git show\" does not.\n\t// Assume there are no commits in the repo when \"git show\" fails with\n\t// uncommitted files and skip tagging revision / committime.\n\tvar rev string\n\tvar commitTime time.Time\n\tout, err = vcsGit.runOutputVerboseOnly(rootDir, \"-c log.showsignature=false show -s --format=%H:%ct\")\n\tif err != nil && !uncommitted {\n\t\treturn Status{}, err\n\t} else if err == nil {\n\t\trev, commitTime, err = parseRevTime(out)\n\t\tif err != nil {\n\t\t\treturn Status{}, err\n\t\t}\n\t}\n\n\treturn Status{\n\t\tRevision:    rev,\n\t\tCommitTime:  commitTime,\n\t\tUncommitted: uncommitted,\n\t}, nil\n}\n\n// vcsBzr describes how to use Bazaar.\nvar vcsBzr = &cmd{\n\tName:      \"Bazaar\",\n\tCmd:       \"bzr\",\n\tRootNames: []string{\".bzr\"},\n\n\tCreateCmd: []string{\"branch -- {repo} {dir}\"},\n\n\t// Without --overwrite bzr will not pull tags that changed.\n\t// Replace by --overwrite-tags after http://pad.lv/681792 goes in.\n\tDownloadCmd: []string{\"pull --overwrite\"},\n\n\tTagCmd:         []tagCmd{{\"tags\", `^(\\S+)`}},\n\tTagSyncCmd:     []string{\"update -r {tag}\"},\n\tTagSyncDefault: []string{\"update -r revno:-1\"},\n\n\tScheme:      []string{\"https\", \"http\", \"bzr\", \"bzr+ssh\"},\n\tPingCmd:     \"info -- {scheme}://{repo}\",\n\tRemoteRepo:  bzrRemoteRepo,\n\tResolveRepo: bzrResolveRepo,\n\tStatus:      bzrStatus,\n}\n\nfunc bzrRemoteRepo(vcsBzr *cmd, rootDir string) (remoteRepo string, err error) {\n\toutb, err := vcsBzr.runOutput(rootDir, \"config parent_location\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(outb)), nil\n}\n\nfunc bzrResolveRepo(vcsBzr *cmd, rootDir, remoteRepo string) (realRepo string, err error) {\n\toutb, err := vcsBzr.runOutput(rootDir, \"info \"+remoteRepo)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tout := string(outb)\n\n\t// Expect:\n\t// ...\n\t//   (branch root|repository branch): <URL>\n\t// ...\n\n\tfound := false\n\tfor _, prefix := range []string{\"\\n  branch root: \", \"\\n  repository branch: \"} {\n\t\ti := strings.Index(out, prefix)\n\t\tif i >= 0 {\n\t\t\tout = out[i+len(prefix):]\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn \"\", fmt.Errorf(\"unable to parse output of bzr info\")\n\t}\n\n\ti := strings.Index(out, \"\\n\")\n\tif i < 0 {\n\t\treturn \"\", fmt.Errorf(\"unable to parse output of bzr info\")\n\t}\n\tout = out[:i]\n\treturn strings.TrimSpace(out), nil\n}\n\nfunc bzrStatus(vcsBzr *cmd, rootDir string) (Status, error) {\n\toutb, err := vcsBzr.runOutputVerboseOnly(rootDir, \"version-info\")\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\tout := string(outb)\n\n\t// Expect (non-empty repositories only):\n\t//\n\t// revision-id: gopher@gopher.net-20211021072330-qshok76wfypw9lpm\n\t// date: 2021-09-21 12:00:00 +1000\n\t// ...\n\tvar rev string\n\tvar commitTime time.Time\n\n\tfor _, line := range strings.Split(out, \"\\n\") {\n\t\ti := strings.IndexByte(line, ':')\n\t\tif i < 0 {\n\t\t\tcontinue\n\t\t}\n\t\tkey := line[:i]\n\t\tvalue := strings.TrimSpace(line[i+1:])\n\n\t\tswitch key {\n\t\tcase \"revision-id\":\n\t\t\trev = value\n\t\tcase \"date\":\n\t\t\tvar err error\n\t\t\tcommitTime, err = time.Parse(\"2006-01-02 15:04:05 -0700\", value)\n\t\t\tif err != nil {\n\t\t\t\treturn Status{}, errors.New(\"unable to parse output of bzr version-info\")\n\t\t\t}\n\t\t}\n\t}\n\n\toutb, err = vcsBzr.runOutputVerboseOnly(rootDir, \"status\")\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\n\t// Skip warning when working directory is set to an older revision.\n\tif bytes.HasPrefix(outb, []byte(\"working tree is out of date\")) {\n\t\ti := bytes.IndexByte(outb, '\\n')\n\t\tif i < 0 {\n\t\t\ti = len(outb)\n\t\t}\n\t\toutb = outb[:i]\n\t}\n\tuncommitted := len(outb) > 0\n\n\treturn Status{\n\t\tRevision:    rev,\n\t\tCommitTime:  commitTime,\n\t\tUncommitted: uncommitted,\n\t}, nil\n}\n\n// vcsSvn describes how to use Subversion.\nvar vcsSvn = &cmd{\n\tName:      \"Subversion\",\n\tCmd:       \"svn\",\n\tRootNames: []string{\".svn\"},\n\n\tCreateCmd:   []string{\"checkout -- {repo} {dir}\"},\n\tDownloadCmd: []string{\"update\"},\n\n\t// There is no tag command in subversion.\n\t// The branch information is all in the path names.\n\n\tScheme:     []string{\"https\", \"http\", \"svn\", \"svn+ssh\"},\n\tPingCmd:    \"info -- {scheme}://{repo}\",\n\tRemoteRepo: svnRemoteRepo,\n}\n\nfunc svnRemoteRepo(vcsSvn *cmd, rootDir string) (remoteRepo string, err error) {\n\toutb, err := vcsSvn.runOutput(rootDir, \"info\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tout := string(outb)\n\n\t// Expect:\n\t//\n\t//\t ...\n\t// \tURL: <URL>\n\t// \t...\n\t//\n\t// Note that we're not using the Repository Root line,\n\t// because svn allows checking out subtrees.\n\t// The URL will be the URL of the subtree (what we used with 'svn co')\n\t// while the Repository Root may be a much higher parent.\n\ti := strings.Index(out, \"\\nURL: \")\n\tif i < 0 {\n\t\treturn \"\", fmt.Errorf(\"unable to parse output of svn info\")\n\t}\n\tout = out[i+len(\"\\nURL: \"):]\n\ti = strings.Index(out, \"\\n\")\n\tif i < 0 {\n\t\treturn \"\", fmt.Errorf(\"unable to parse output of svn info\")\n\t}\n\tout = out[:i]\n\treturn strings.TrimSpace(out), nil\n}\n\n// fossilRepoName is the name go get associates with a fossil repository. In the\n// real world the file can be named anything.\nconst fossilRepoName = \".fossil\"\n\n// vcsFossil describes how to use Fossil (fossil-scm.org)\nvar vcsFossil = &cmd{\n\tName:      \"Fossil\",\n\tCmd:       \"fossil\",\n\tRootNames: []string{\".fslckout\", \"_FOSSIL_\"},\n\n\tCreateCmd:   []string{\"-go-internal-mkdir {dir} clone -- {repo} \" + filepath.Join(\"{dir}\", fossilRepoName), \"-go-internal-cd {dir} open .fossil\"},\n\tDownloadCmd: []string{\"up\"},\n\n\tTagCmd:         []tagCmd{{\"tag ls\", `(.*)`}},\n\tTagSyncCmd:     []string{\"up tag:{tag}\"},\n\tTagSyncDefault: []string{\"up trunk\"},\n\n\tScheme:     []string{\"https\", \"http\"},\n\tRemoteRepo: fossilRemoteRepo,\n\tStatus:     fossilStatus,\n}\n\nfunc fossilRemoteRepo(vcsFossil *cmd, rootDir string) (remoteRepo string, err error) {\n\tout, err := vcsFossil.runOutput(rootDir, \"remote-url\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n\nvar errFossilInfo = errors.New(\"unable to parse output of fossil info\")\n\nfunc fossilStatus(vcsFossil *cmd, rootDir string) (Status, error) {\n\toutb, err := vcsFossil.runOutputVerboseOnly(rootDir, \"info\")\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\tout := string(outb)\n\n\t// Expect:\n\t// ...\n\t// checkout:     91ed71f22c77be0c3e250920f47bfd4e1f9024d2 2021-09-21 12:00:00 UTC\n\t// ...\n\n\t// Extract revision and commit time.\n\t// Ensure line ends with UTC (known timezone offset).\n\tconst prefix = \"\\ncheckout:\"\n\tconst suffix = \" UTC\"\n\ti := strings.Index(out, prefix)\n\tif i < 0 {\n\t\treturn Status{}, errFossilInfo\n\t}\n\tcheckout := out[i+len(prefix):]\n\ti = strings.Index(checkout, suffix)\n\tif i < 0 {\n\t\treturn Status{}, errFossilInfo\n\t}\n\tcheckout = strings.TrimSpace(checkout[:i])\n\n\ti = strings.IndexByte(checkout, ' ')\n\tif i < 0 {\n\t\treturn Status{}, errFossilInfo\n\t}\n\trev := checkout[:i]\n\n\tcommitTime, err := time.ParseInLocation(\"2006-01-02 15:04:05\", checkout[i+1:], time.UTC)\n\tif err != nil {\n\t\treturn Status{}, fmt.Errorf(\"%v: %v\", errFossilInfo, err)\n\t}\n\n\t// Also look for untracked changes.\n\toutb, err = vcsFossil.runOutputVerboseOnly(rootDir, \"changes --differ\")\n\tif err != nil {\n\t\treturn Status{}, err\n\t}\n\tuncommitted := len(outb) > 0\n\n\treturn Status{\n\t\tRevision:    rev,\n\t\tCommitTime:  commitTime,\n\t\tUncommitted: uncommitted,\n\t}, nil\n}\n\nfunc (v *cmd) String() string {\n\treturn v.Name\n}\n\n// run runs the command line cmd in the given directory.\n// keyval is a list of key, value pairs. run expands\n// instances of {key} in cmd into value, but only after\n// splitting cmd into individual arguments.\n// If an error occurs, run prints the command line and the\n// command's combined stdout+stderr to standard error.\n// Otherwise run discards the command's output.\nfunc (v *cmd) run(dir string, cmd string, keyval ...string) error {\n\t_, err := v.run1(dir, cmd, keyval, true)\n\treturn err\n}\n\n// runVerboseOnly is like run but only generates error output to standard error in verbose mode.\nfunc (v *cmd) runVerboseOnly(dir string, cmd string, keyval ...string) error {\n\t_, err := v.run1(dir, cmd, keyval, false)\n\treturn err\n}\n\n// runOutput is like run but returns the output of the command.\nfunc (v *cmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {\n\treturn v.run1(dir, cmd, keyval, true)\n}\n\n// runOutputVerboseOnly is like runOutput but only generates error output to\n// standard error in verbose mode.\nfunc (v *cmd) runOutputVerboseOnly(dir string, cmd string, keyval ...string) ([]byte, error) {\n\treturn v.run1(dir, cmd, keyval, false)\n}\n\n// run1 is the generalized implementation of run and runOutput.\nfunc (v *cmd) run1(dir string, cmdline string, keyval []string, verbose bool) ([]byte, error) {\n\tm := make(map[string]string)\n\tfor i := 0; i < len(keyval); i += 2 {\n\t\tm[keyval[i]] = keyval[i+1]\n\t}\n\targs := strings.Fields(cmdline)\n\tfor i, arg := range args {\n\t\targs[i] = expand(m, arg)\n\t}\n\n\tif len(args) >= 2 && args[0] == \"-go-internal-mkdir\" {\n\t\tvar err error\n\t\tif filepath.IsAbs(args[1]) {\n\t\t\terr = os.Mkdir(args[1], fs.ModePerm)\n\t\t} else {\n\t\t\terr = os.Mkdir(filepath.Join(dir, args[1]), fs.ModePerm)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\targs = args[2:]\n\t}\n\n\tif len(args) >= 2 && args[0] == \"-go-internal-cd\" {\n\t\tif filepath.IsAbs(args[1]) {\n\t\t\tdir = args[1]\n\t\t} else {\n\t\t\tdir = filepath.Join(dir, args[1])\n\t\t}\n\t\targs = args[2:]\n\t}\n\n\t_, err := exec.LookPath(v.Cmd)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr,\n\t\t\t\"go: missing %s command. See https://golang.org/s/gogetcmd\\n\",\n\t\t\tv.Name)\n\t\treturn nil, err\n\t}\n\n\tcmd := exec.Command(v.Cmd, args...)\n\tcmd.Dir = dir\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\tif verbose {\n\t\t\tfmt.Fprintf(os.Stderr, \"# cd %s; %s %s\\n\", dir, v.Cmd, strings.Join(args, \" \"))\n\t\t\tif ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {\n\t\t\t\tos.Stderr.Write(ee.Stderr)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintln(os.Stderr, err.Error())\n\t\t\t}\n\t\t}\n\t}\n\treturn out, err\n}\n\n// Create creates a new copy of repo in dir.\n// The parent of dir must exist; dir must not.\nfunc (v *cmd) Create(dir, repo string) error {\n\tfor _, cmd := range v.CreateCmd {\n\t\tif err := v.run(filepath.Dir(dir), cmd, \"dir\", dir, \"repo\", repo); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Download downloads any new changes for the repo in dir.\nfunc (v *cmd) Download(dir string) error {\n\tfor _, cmd := range v.DownloadCmd {\n\t\tif err := v.run(dir, cmd); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Tags returns the list of available tags for the repo in dir.\nfunc (v *cmd) Tags(dir string) ([]string, error) {\n\tvar tags []string\n\tfor _, tc := range v.TagCmd {\n\t\tout, err := v.runOutput(dir, tc.cmd)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tre := regexp.MustCompile(`(?m-s)` + tc.pattern)\n\t\tfor _, m := range re.FindAllStringSubmatch(string(out), -1) {\n\t\t\ttags = append(tags, m[1])\n\t\t}\n\t}\n\treturn tags, nil\n}\n\n// TagSync syncs the repo in dir to the named tag,\n// which either is a tag returned by tags or is v.tagDefault.\nfunc (v *cmd) TagSync(dir, tag string) error {\n\tif v.TagSyncCmd == nil {\n\t\treturn nil\n\t}\n\tif tag != \"\" {\n\t\tfor _, tc := range v.TagLookupCmd {\n\t\t\tout, err := v.runOutput(dir, tc.cmd, \"tag\", tag)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tre := regexp.MustCompile(`(?m-s)` + tc.pattern)\n\t\t\tm := re.FindStringSubmatch(string(out))\n\t\t\tif len(m) > 1 {\n\t\t\t\ttag = m[1]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif tag == \"\" && v.TagSyncDefault != nil {\n\t\tfor _, cmd := range v.TagSyncDefault {\n\t\t\tif err := v.run(dir, cmd); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor _, cmd := range v.TagSyncCmd {\n\t\tif err := v.run(dir, cmd, \"tag\", tag); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// fromDir inspects dir and its parents to determine the\n// version control system and code repository to use.\n// If no repository is found, fromDir returns an error\n// equivalent to os.ErrNotExist.\nfunc fromDir(dir, srcRoot string, allowNesting bool) (repoDir string, vcsCmd *cmd, err error) {\n\t// Clean and double-check that dir is in (a subdirectory of) srcRoot.\n\tdir = filepath.Clean(dir)\n\tif srcRoot != \"\" {\n\t\tsrcRoot = filepath.Clean(srcRoot)\n\t\tif len(dir) <= len(srcRoot) || dir[len(srcRoot)] != filepath.Separator {\n\t\t\treturn \"\", nil, fmt.Errorf(\"directory %q is outside source root %q\", dir, srcRoot)\n\t\t}\n\t}\n\n\torigDir := dir\n\tfor len(dir) > len(srcRoot) {\n\t\tfor _, vcs := range vcsList {\n\t\t\tif _, err := statAny(dir, vcs.RootNames); err == nil {\n\t\t\t\t// Record first VCS we find.\n\t\t\t\t// If allowNesting is false (as it is in GOPATH), keep looking for\n\t\t\t\t// repositories in parent directories and report an error if one is\n\t\t\t\t// found to mitigate VCS injection attacks.\n\t\t\t\tif vcsCmd == nil {\n\t\t\t\t\tvcsCmd = vcs\n\t\t\t\t\trepoDir = dir\n\t\t\t\t\tif allowNesting {\n\t\t\t\t\t\treturn repoDir, vcsCmd, nil\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Allow .git inside .git, which can arise due to submodules.\n\t\t\t\tif vcsCmd == vcs && vcs.Cmd == \"git\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Otherwise, we have one VCS inside a different VCS.\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"directory %q uses %s, but parent %q uses %s\",\n\t\t\t\t\trepoDir, vcsCmd.Cmd, dir, vcs.Cmd)\n\t\t\t}\n\t\t}\n\n\t\t// Move to parent.\n\t\tndir := filepath.Dir(dir)\n\t\tif len(ndir) >= len(dir) {\n\t\t\tbreak\n\t\t}\n\t\tdir = ndir\n\t}\n\tif vcsCmd == nil {\n\t\treturn \"\", nil, &vcsNotFoundError{dir: origDir}\n\t}\n\treturn repoDir, vcsCmd, nil\n}\n\n// statAny provides FileInfo for the first filename found in the directory.\n// Otherwise, it returns the last error seen.\nfunc statAny(dir string, filenames []string) (os.FileInfo, error) {\n\tif len(filenames) == 0 {\n\t\treturn nil, errors.New(\"invalid argument: no filenames provided\")\n\t}\n\n\tvar err error\n\tvar fi os.FileInfo\n\tfor _, name := range filenames {\n\t\tfi, err = os.Stat(filepath.Join(dir, name))\n\t\tif err == nil {\n\t\t\treturn fi, nil\n\t\t}\n\t}\n\n\treturn nil, err\n}\n\ntype vcsNotFoundError struct {\n\tdir string\n}\n\nfunc (e *vcsNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"directory %q is not using a known version control system\", e.dir)\n}\n\nfunc (e *vcsNotFoundError) Is(err error) bool {\n\treturn err == os.ErrNotExist\n}\n\n// expand rewrites s to replace {k} with match[k] for each key k in match.\nfunc expand(match map[string]string, s string) string {\n\t// We want to replace each match exactly once, and the result of expansion\n\t// must not depend on the iteration order through the map.\n\t// A strings.Replacer has exactly the properties we're looking for.\n\toldNew := make([]string, 0, 2*len(match))\n\tfor k, v := range match {\n\t\toldNew = append(oldNew, \"{\"+k+\"}\", v)\n\t}\n\treturn strings.NewReplacer(oldNew...).Replace(s)\n}\n"
  },
  {
    "path": "pkg/vfs/directory.go",
    "content": "package vfs\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"sort\"\n\t\"time\"\n)\n\ntype directoryContents struct {\n\tnode\n\tchildDirs map[string]*directoryContents\n\tfiles     map[string]*fileContents\n}\n\n// Directory is a wrapper around directoryContents which stores the state\n// needed to implement ReadDirFile\ntype Directory struct {\n\t*directoryContents\n\treadDirCount int\n\treadEntries  []fs.DirEntry\n\tclosed       bool\n}\n\nvar (\n\t_ fs.FileInfo    = (*Directory)(nil)\n\t_ fs.DirEntry    = (*Directory)(nil)\n\t_ fs.ReadDirFile = (*Directory)(nil)\n)\n\nfunc newDirectoryContents(name string) *directoryContents {\n\treturn &directoryContents{\n\t\tnode: node{\n\t\t\tname:    name,\n\t\t\tparent:  nil,\n\t\t\tmodTime: time.Now(),\n\t\t},\n\t\tchildDirs: make(map[string]*directoryContents),\n\t\tfiles:     make(map[string]*fileContents),\n\t}\n}\n\nfunc (d *Directory) createEntries() []fs.DirEntry {\n\trtn := make([]fs.DirEntry, 0, len(d.childDirs)+len(d.files))\n\tfor _, child := range d.childDirs {\n\t\trtn = append(rtn, &Directory{directoryContents: child})\n\t}\n\n\tfor _, child := range d.files {\n\t\trtn = append(rtn, &File{fileContents: child})\n\t}\n\n\tsort.SliceStable(rtn, func(i, j int) bool {\n\t\treturn rtn[i].Name() < rtn[j].Name()\n\t})\n\treturn rtn\n}\n\n// ReadDir reads the contents of the directory and returns\n// a slice of up to n DirEntry values in directory order.\n// Subsequent calls on the same file will yield further DirEntry values.\n//\n// If n > 0, ReadDir returns at most n DirEntry structures.\n// In this case, if ReadDir returns an empty slice, it will return\n// a non-nil error explaining why.\n// At the end of a directory, the error is io.EOF.\n//\n// If n <= 0, ReadDir returns all the DirEntry values from the directory\n// in a single slice. In this case, if ReadDir succeeds (reads all the way\n// to the end of the directory), it returns the slice and a nil error.\n// If it encounters an error before the end of the directory,\n// ReadDir returns the DirEntry list read until that point and a non-nil error.\nfunc (d *Directory) ReadDir(n int) ([]fs.DirEntry, error) {\n\tif d.closed {\n\t\treturn nil, fs.ErrClosed\n\t}\n\n\t// Create the directory listing if it doesn't exist already\n\tif d.readEntries == nil {\n\t\td.readEntries = d.createEntries()\n\t}\n\n\t// detect an EOF\n\tif d.readDirCount == len(d.readEntries) {\n\t\tif n <= 0 { // special case return\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, io.EOF\n\t}\n\n\t// Create the return data with the number of requested entries\n\treadNum := len(d.readEntries) - d.readDirCount\n\tif readNum > n && n > 0 {\n\t\treadNum = n\n\t}\n\trtn := make([]fs.DirEntry, readNum)\n\tfor i := 0; i < readNum; i++ {\n\t\trtn[i] = d.readEntries[d.readDirCount]\n\t\td.readDirCount++\n\t}\n\n\treturn rtn, nil\n}\n\nfunc (d *Directory) Read(_ []byte) (int, error) {\n\treturn 0, errors.New(\"cannot read directory\")\n}\n\nfunc (d *Directory) Name() string {\n\treturn d.node.name\n}\n\nfunc (d *Directory) IsDir() bool {\n\treturn true\n}\n\nfunc (d *Directory) Type() fs.FileMode {\n\treturn fs.ModeDir\n}\n\nfunc (d *Directory) Info() (fs.FileInfo, error) {\n\treturn d, nil\n}\n\nfunc (d *Directory) Size() int64 {\n\treturn 0\n}\n\nfunc (d *Directory) Mode() fs.FileMode {\n\treturn d.Type()\n}\n\nfunc (d *Directory) ModTime() time.Time {\n\treturn d.node.modTime\n}\n\nfunc (d *Directory) Stat() (fs.FileInfo, error) {\n\treturn d.Info()\n}\n\nfunc (d *Directory) Close() error {\n\td.closed = true\n\treturn nil\n}\n\nfunc (d *Directory) Sys() any {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/vfs/doc.go",
    "content": "// Package vfs represents a virtual filesystem\npackage vfs\n"
  },
  {
    "path": "pkg/vfs/file.go",
    "content": "package vfs\n\nimport (\n\t\"io\"\n\t\"io/fs\"\n\t\"time\"\n)\n\ntype fileContents struct {\n\tnode\n\tcontents []byte\n}\n\ntype File struct {\n\t*fileContents\n\tbytesRead int\n\tclosed    bool\n}\n\nvar (\n\t_ fs.File     = (*File)(nil)\n\t_ fs.FileInfo = (*File)(nil)\n\t_ fs.DirEntry = (*File)(nil)\n)\n\nfunc (f *File) Name() string {\n\treturn f.node.name\n}\n\n// Read implements io.Reader\nfunc (f *File) Read(bytes []byte) (int, error) {\n\tif f.closed {\n\t\treturn 0, fs.ErrClosed\n\t}\n\n\tif f.bytesRead >= len(f.contents) {\n\t\treturn 0, io.EOF\n\t}\n\n\tbytesRead := copy(bytes, f.contents[f.bytesRead:])\n\tf.bytesRead += bytesRead\n\tif f.bytesRead >= len(f.contents) {\n\t\treturn bytesRead, io.EOF\n\t}\n\n\treturn bytesRead, nil\n}\n\nfunc (f *File) Close() error {\n\tf.closed = true\n\treturn nil\n}\n\nfunc (f *File) Size() int64 {\n\treturn int64(len(f.contents))\n}\n\nfunc (f *File) Mode() fs.FileMode {\n\treturn fs.ModePerm & 0444 // files in this file system are read only\n}\n\nfunc (f *File) ModTime() time.Time {\n\treturn f.node.modTime\n}\n\nfunc (f *File) IsDir() bool {\n\treturn false\n}\n\nfunc (f *File) Sys() any {\n\treturn nil\n}\n\nfunc (f *File) Type() fs.FileMode {\n\treturn f.Mode() & fs.ModeType\n}\n\nfunc (f *File) Info() (fs.FileInfo, error) {\n\treturn f, nil\n}\n\nfunc (f *File) Stat() (fs.FileInfo, error) {\n\treturn f, nil\n}\n"
  },
  {
    "path": "pkg/vfs/node.go",
    "content": "package vfs\n\nimport (\n\t\"time\"\n)\n\ntype node struct {\n\tname    string // The name of this node\n\tparent  *directoryContents\n\tmodTime time.Time\n}\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/blahsvc/another.json",
    "content": "{\n  \"foo\": false\n}\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/blahsvc/test.json",
    "content": "{\n  \"foo\": \"blah\"\n}\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/foosystem/README.md",
    "content": "This is a example file\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/foosystem/anotherservice/test.txt",
    "content": "another\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/foosystem/barservice/blah.json",
    "content": "{\n  \"foo\": \"bar\"\n}\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/foosystem/barservice/test.txt",
    "content": "Hello world\n"
  },
  {
    "path": "pkg/vfs/testdata/filteredglob/nope/ignored.txt",
    "content": ""
  },
  {
    "path": "pkg/vfs/utils.go",
    "content": "package vfs\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\n\t\"encr.dev/pkg/eerror\"\n)\n\n// FromDir creates a Virtual File System (VFS) from the workingDir on the local computer\n//\n// Only files or folders which match the predicate will be added into the file system\n// A nil predicate will result in all files and folders being added into the file system\nfunc FromDir(workingDir string, predicate func(fileName string, info fs.DirEntry) bool) (*VFS, error) {\n\tdirFS := os.DirFS(workingDir)\n\trtn := New()\n\n\tif predicate == nil {\n\t\tpredicate = func(fileName string, info fs.DirEntry) bool { return true }\n\t}\n\n\terr := fs.WalkDir(dirFS, \".\", func(path string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn eerror.Wrap(err, \"vfs\", \"error walking directory\", map[string]any{\"path\": path})\n\t\t}\n\n\t\tif predicate(path, info) {\n\t\t\tif info.IsDir() {\n\t\t\t\t_ = rtn.AddDir(path)\n\t\t\t} else {\n\t\t\t\tbytes, err := fs.ReadFile(dirFS, path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn eerror.Wrap(err, \"vfs\", \"error reading file\", map[string]any{\"path\": path})\n\t\t\t\t}\n\n\t\t\t\tstat, err := fs.Stat(dirFS, path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn eerror.Wrap(err, \"vfs\", \"error stat file\", map[string]any{\"path\": path})\n\t\t\t\t}\n\n\t\t\t\tif _, err := rtn.AddFile(path, bytes, stat.ModTime()); err != nil {\n\t\t\t\t\treturn eerror.Wrap(err, \"vfs\", \"unable to add file\", map[string]any{\"path\": path})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rtn, nil\n}\n"
  },
  {
    "path": "pkg/vfs/vfs.go",
    "content": "package vfs\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype VFS struct {\n\troot *directoryContents\n}\n\nvar (\n\t_ fs.FS         = (*VFS)(nil)\n\t_ fs.ReadDirFS  = (*VFS)(nil)\n\t_ fs.ReadFileFS = (*VFS)(nil)\n\t_ fs.SubFS      = (*VFS)(nil)\n\t_ fs.StatFS     = (*VFS)(nil)\n)\n\nfunc New() *VFS {\n\treturn &VFS{root: newDirectoryContents(\"\")}\n}\n\n// Open opens the named file.\n//\n// When Open returns an error, it should be of type *PathError\n// with the Op field set to \"open\", the Path field set to name,\n// and the Err field describing the problem.\n//\n// Open should reject attempts to open names that do not satisfy\n// ValidPath(name), returning a *PathError with Err set to\n// ErrInvalid or ErrNotExist.\nfunc (v *VFS) Open(name string) (fs.File, error) {\n\tif name == \".\" {\n\t\treturn &Directory{directoryContents: v.root}, nil\n\t}\n\n\tif !fs.ValidPath(name) {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"open\",\n\t\t\tPath: name,\n\t\t\tErr:  fs.ErrInvalid,\n\t\t}\n\t}\n\n\tparts := strings.Split(name, \"/\")\n\n\tpathTravelled := make([]string, 0, len(parts))\n\tdir := v.root\n\tpathTravelled = append(pathTravelled, dir.name)\n\tif len(parts) > 1 {\n\t\tfor _, subDir := range parts[:len(parts)-1] {\n\n\t\t\tpathTravelled = append(pathTravelled, subDir)\n\t\t\tdir = dir.childDirs[subDir]\n\t\t\tif dir == nil {\n\t\t\t\treturn nil, &fs.PathError{\n\t\t\t\t\tOp:   \"open\",\n\t\t\t\t\tPath: strings.Join(pathTravelled, \"/\"),\n\t\t\t\t\tErr:  fs.ErrNotExist,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the child\n\tchildName := parts[len(parts)-1]\n\tif childFile, found := dir.files[childName]; found {\n\t\treturn &File{fileContents: childFile}, nil\n\t}\n\tif childDir, found := dir.childDirs[childName]; found {\n\t\treturn &Directory{directoryContents: childDir}, nil\n\t}\n\n\treturn nil, &fs.PathError{\n\t\tOp:   \"open\",\n\t\tPath: strings.Join(pathTravelled, \"/\"),\n\t\tErr:  fs.ErrNotExist,\n\t}\n}\n\n// ReadDir reads the named directory\n// and returns a list of directory entries sorted by filename.\nfunc (v *VFS) ReadDir(name string) ([]fs.DirEntry, error) {\n\tfile, err := v.Open(name)\n\tif err != nil {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"ReadDir\",\n\t\t\tPath: name,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\n\tswitch file := file.(type) {\n\tcase *Directory:\n\t\treturn file.createEntries(), nil\n\n\tdefault:\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"ReadDir\",\n\t\t\tPath: name,\n\t\t\tErr:  errors.New(\"not a directory\"),\n\t\t}\n\t}\n}\n\n// ReadFile reads the named file and returns its contents.\n// A successful call returns a nil error, not io.EOF.\n// (Because ReadFile reads the whole file, the expected EOF\n// from the final Read is not treated as an error to be reported.)\n//\n// The caller is permitted to modify the returned byte slice.\n// This method should return a copy of the underlying data.\nfunc (v *VFS) ReadFile(name string) ([]byte, error) {\n\tfile, err := v.Open(name)\n\tif err != nil {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"ReadFile\",\n\t\t\tPath: name,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\n\tswitch file := file.(type) {\n\tcase *File:\n\t\trtnBytes := make([]byte, len(file.contents))\n\t\tcopy(rtnBytes, file.contents)\n\t\treturn rtnBytes, nil\n\n\tdefault:\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"ReadFile\",\n\t\t\tPath: name,\n\t\t\tErr:  errors.New(\"not a file\"),\n\t\t}\n\t}\n}\n\n// Sub returns an FS corresponding to the subtree rooted at dir.\nfunc (v *VFS) Sub(dir string) (fs.FS, error) {\n\tfile, err := v.Open(dir)\n\tif err != nil {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"Sub\",\n\t\t\tPath: dir,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\n\tswitch file := file.(type) {\n\tcase *Directory:\n\t\treturn &VFS{root: file.directoryContents}, nil\n\n\tdefault:\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"Sub\",\n\t\t\tPath: dir,\n\t\t\tErr:  errors.New(\"not a directory\"),\n\t\t}\n\t}\n}\n\n// Stat returns a FileInfo describing the file.\n// If there is an error, it should be of type *PathError.\nfunc (v *VFS) Stat(name string) (fs.FileInfo, error) {\n\tfile, err := v.Open(name)\n\tif err != nil {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"Stat\",\n\t\t\tPath: name,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\n\treturn file.Stat()\n}\n\n// AddFile records a file into the VFS\nfunc (v *VFS) AddFile(path string, bytes []byte, time time.Time) (*fileContents, error) {\n\tdirPath, filename := filepath.Split(path)\n\tdir := v.AddDir(dirPath)\n\tdir.files[filename] = &fileContents{\n\t\tnode: node{\n\t\t\tname:    filename,\n\t\t\tparent:  dir,\n\t\t\tmodTime: time,\n\t\t},\n\t\tcontents: bytes,\n\t}\n\n\treturn dir.files[filename], nil\n}\n\n// AddDir records a directory into the VFS.\nfunc (v *VFS) AddDir(dirPath string) *directoryContents {\n\tdirParts := strings.Split(dirPath, \"/\")\n\tdir := v.root\n\tfor _, dirPart := range dirParts {\n\t\tif dirPart == \".\" || dirPart == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif dirPart == \"..\" {\n\t\t\tif dir.node.parent == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tdir = dir.node.parent\n\t\t\tcontinue\n\t\t}\n\n\t\tchild := dir.childDirs[dirPart]\n\t\tif child == nil {\n\t\t\tchild = newDirectoryContents(dirPart)\n\t\t\tchild.node.parent = dir\n\t\t\tdir.childDirs[dirPart] = child\n\t\t}\n\t\tdir = child\n\t}\n\n\treturn dir\n}\n"
  },
  {
    "path": "pkg/vfs/vfs_test.go",
    "content": "package vfs\n\nimport (\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"testing/fstest\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc TestFromDir(t *testing.T) {\n\tc := qt.New(t)\n\n\tdir, err := FromDir(\n\t\tfilepath.Join(\".\", \"testdata\", \"filteredglob\"),\n\t\tfunc(file string, info fs.DirEntry) bool { return filepath.Ext(file) == \".json\" },\n\t)\n\tc.Assert(err, qt.IsNil, qt.Commentf(\"error creating VFS\"))\n\n\t// Test that the VFS contains the expected number of files (no more no less)\n\tfileCount := 0\n\tdirCount := 0\n\terr = fs.WalkDir(dir, \".\", func(path string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !info.IsDir() {\n\t\t\tfileCount++\n\t\t} else {\n\t\t\tdirCount++\n\t\t}\n\t\treturn nil\n\t})\n\tc.Assert(err, qt.IsNil, qt.Commentf(\"error walking directory\"))\n\tc.Assert(fileCount, qt.Equals, 3, qt.Commentf(\"unexpected number of files\"))\n\tc.Assert(dirCount, qt.Equals, 4, qt.Commentf(\"unexpected number of directories\"))\n\n\t// Perform the standardised tests on the VFS implementation checking for the existence of the files we wanted\n\tif err := fstest.TestFS(dir, \"blahsvc/another.json\", \"blahsvc/test.json\", \"foosystem/barservice/blah.json\"); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "pkg/watcher/event.go",
    "content": "package watcher\n\nimport \"os\"\n\ntype EventType = string\n\nconst (\n\tCREATED  EventType = \"Created\"\n\tMODIFIED EventType = \"Modified\"\n\tDELETED  EventType = \"Deleted\"\n)\n\ntype Event struct {\n\tEventType EventType\n\tPath      string\n\tInfo      os.FileInfo\n}\n\ntype Events struct {\n\tlatestEvents map[string]Event // Latest event per file\n}\n\nfunc newEventBatch() *Events {\n\treturn &Events{\n\t\tlatestEvents: make(map[string]Event),\n\t}\n}\n\nfunc (e *Events) addEvent(path string, event EventType, info os.FileInfo) {\n\te.latestEvents[path] = Event{\n\t\tEventType: event,\n\t\tPath:      path,\n\t\tInfo:      info,\n\t}\n}\n\nfunc (e *Events) Events() []Event {\n\tevents := make([]Event, 0, len(e.latestEvents))\n\tfor _, event := range e.latestEvents {\n\t\tevents = append(events, event)\n\t}\n\n\treturn events\n}\n"
  },
  {
    "path": "pkg/watcher/rlimit_nix.go",
    "content": "//go:build linux || darwin\n\npackage watcher\n\nimport (\n\t\"syscall\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n// BumpRLimitSoftToHardLimit bumps the soft limit of the rlimit to the hard limit.\n//\n// To go higher the user will need to update their kernel settings which can be viewed:\n//\n//\tsysctl -a | grep kern.maxfile\nfunc BumpRLimitSoftToHardLimit() {\n\tvar rLimit syscall.Rlimit\n\terr := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to get rlimit\")\n\t}\n\trLimit.Cur = rLimit.Max\n\terr = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"failed to set rlimit\")\n\t}\n}\n"
  },
  {
    "path": "pkg/watcher/rlimit_noop.go",
    "content": "//go:build !linux && !darwin\n\npackage watcher\n\n// BumpRLimitSoftToHardLimit bumps the soft limit of the rlimit to the hard limit.\n//\n// To go higher the user will need to update their kernel settings which can be viewed:\n//\n//\tsysctl -a | grep kern.maxfile\nfunc BumpRLimitSoftToHardLimit() {\n\t// no-op\n}\n"
  },
  {
    "path": "pkg/watcher/util.go",
    "content": "package watcher\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// IgnoreFolder returns true for folders we don't want to watch certain folders\n// as they'll never impact an Encore app, and they cause an extreme amount of noise.\nfunc IgnoreFolder(folder string) bool {\n\tfolderName := filepath.Base(filepath.Clean(folder))\n\tif folderName == \"node_modules\" || folderName == \"encore.gen\" {\n\t\treturn true\n\t}\n\n\tif folderName == \"target\" {\n\t\t// Do we have a \"Cargo.toml\" file in the parent directory? If so, ignore this.\n\t\tcargoPath := filepath.Join(filepath.Dir(folder), \"Cargo.toml\")\n\t\tif _, err := os.Stat(cargoPath); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Don't watch hidden folders like `.git` or `.idea` as\n\t// they also don't impact an Encore app.\n\tif len(folderName) > 1 && folderName[0] == '.' {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "pkg/watcher/watcher.go",
    "content": "package watcher\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bep/debounce\"\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encr.dev/pkg/eerror\"\n)\n\ntype Watcher struct {\n\tmutex          sync.Mutex\n\teventCond      *sync.Cond\n\tevents         *Events\n\tsignalDebounce func(func())\n\n\tlog     *zerolog.Logger\n\tappRoot string\n\n\twatcher     *fsnotify.Watcher\n\tdirectories map[string]struct{}\n\tstop        chan struct{}\n}\n\nfunc New(appID string) (*Watcher, error) {\n\tfswatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn nil, eerror.Wrap(err, \"watcher\", \"unable to create watcher\", map[string]interface{}{\"app\": appID})\n\t}\n\n\tlogger := log.With().Str(\"component\", \"watcher\").Str(\"app\", appID).Logger()\n\tlogger.Debug().Msg(\"File system watcher created\")\n\tw := &Watcher{\n\t\twatcher:        fswatcher,\n\t\tlog:            &logger,\n\t\tdirectories:    make(map[string]struct{}),\n\t\tstop:           make(chan struct{}),\n\t\tevents:         nil,\n\t\tsignalDebounce: debounce.New(50 * time.Millisecond),\n\t}\n\n\tw.eventCond = sync.NewCond(&w.mutex)\n\n\tgo w.listenForChangeEvents()\n\n\treturn w, nil\n}\n\nfunc (w *Watcher) RecursivelyWatch(folder string) error {\n\treturn filepath.WalkDir(folder, func(path string, info os.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn eerror.Wrap(err, \"watcher\", \"unable to walk directory\", map[string]any{\"path\": path})\n\t\t}\n\n\t\tif info.IsDir() {\n\t\t\tfolder := filepath.Clean(path)\n\n\t\t\tif IgnoreFolder(folder) {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\n\t\t\t// Track the fact we're watching this directory\n\t\t\tw.mutex.Lock()\n\t\t\tif _, found := w.directories[folder]; found {\n\t\t\t\tw.mutex.Unlock()\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\t\t\tw.directories[folder] = struct{}{}\n\t\t\tw.mutex.Unlock() // unlock here to prevent reentrant locks during recursion\n\n\t\t\t// Now start watching this folder\n\t\t\tif err := w.watcher.Add(folder); err != nil {\n\t\t\t\treturn eerror.Wrap(err, \"watcher\", \"unable to add folder to watch\", map[string]any{\"folder\": folder})\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc (w *Watcher) listenForChangeEvents() {\n\tfor {\n\t\tselect {\n\t\tcase <-w.stop:\n\t\t\t_ = w.watcher.Close()\n\t\t\treturn\n\n\t\tcase event := <-w.watcher.Events:\n\t\t\tif event.Has(fsnotify.Remove) {\n\t\t\t\tw.handleDeleteEvent(event.Name)\n\t\t\t} else if event.Has(fsnotify.Create) {\n\t\t\t\tw.handleCreateEvent(event.Name)\n\t\t\t} else if event.Has(fsnotify.Write) {\n\t\t\t\tw.handleWriteEvent(event.Name)\n\t\t\t}\n\n\t\tcase err := <-w.watcher.Errors:\n\t\t\tw.log.Err(err).Msg(\"Watcher error\")\n\t\t}\n\t}\n}\n\nfunc (w *Watcher) handleCreateEvent(path string) {\n\tif info, err := os.Stat(path); err != nil {\n\t\tw.log.Err(err).Str(\"path\", path).Msg(\"unable to stat file\")\n\t} else if info.IsDir() {\n\t\tif err := w.RecursivelyWatch(path); err != nil {\n\t\t\tw.log.Err(err).Str(\"path\", path).Msg(\"unable to start watching new directory\")\n\t\t}\n\t} else {\n\t\tw.recordEventInBatch(path, CREATED, info)\n\t}\n}\n\nfunc (w *Watcher) handleDeleteEvent(path string) {\n\tpath = filepath.Clean(path)\n\n\tpathWithSep := path + string(filepath.Separator)\n\n\t// If it's a directory we're watching, stop watching it\n\tw.mutex.Lock()\n\tfor watchedFolder := range w.directories {\n\t\t// I sthis the path itself, or a subdirectory thereof?\n\t\tif strings.HasPrefix(watchedFolder, pathWithSep) || watchedFolder == path {\n\t\t\tif err := w.watcher.Remove(watchedFolder); err != nil {\n\t\t\t\tw.log.Err(err).Str(\"path\", watchedFolder).Msg(\"unable to stop watching deleted directory\")\n\t\t\t}\n\t\t\tdelete(w.directories, watchedFolder)\n\t\t}\n\t}\n\tw.mutex.Unlock()\n\n\tw.recordEventInBatch(path, DELETED, nil)\n}\n\nfunc (w *Watcher) handleWriteEvent(path string) {\n\tif info, err := os.Stat(path); err != nil {\n\t\tw.log.Err(err).Str(\"path\", path).Msg(\"unable to stat file\")\n\t} else if !info.IsDir() {\n\t\tw.recordEventInBatch(path, MODIFIED, info)\n\t}\n}\n\nfunc (w *Watcher) recordEventInBatch(path string, event EventType, info os.FileInfo) {\n\tw.mutex.Lock()\n\tdefer w.mutex.Unlock()\n\n\tif w.events == nil {\n\t\tw.events = newEventBatch()\n\t}\n\n\tw.events.addEvent(path, event, info)\n\n\t// Debounce the signal to avoid waking up on every event in case of a burst of events.\n\tw.signalDebounce(func() {\n\t\tw.eventCond.Signal()\n\t})\n}\n\nfunc (w *Watcher) WaitForEvents() (events []Event, ok bool) {\n\tw.eventCond.L.Lock()\n\tdefer w.eventCond.L.Unlock()\n\n\tfor {\n\t\tselect {\n\t\tcase <-w.stop:\n\t\t\t// We're shutting down, so return immediately.\n\t\t\treturn nil, false\n\n\t\tdefault:\n\t\t\tif w.events == nil || len(w.events.latestEvents) == 0 {\n\t\t\t\tw.eventCond.Wait()\n\t\t\t}\n\t\t\t// Post-condition: we have at least one event.\n\n\t\t\tevents := w.events.Events()\n\t\t\tw.events = newEventBatch()\n\t\t\treturn events, true\n\t\t}\n\t}\n}\n\nfunc (w *Watcher) Close() error {\n\tclose(w.stop)\n\treturn nil\n}\n\nfunc (w *Watcher) Done() <-chan struct{} {\n\treturn w.stop\n}\n"
  },
  {
    "path": "pkg/words/funcs.go",
    "content": "package words\n\nimport (\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n)\n\n// Select selects n random words from the short word list.\nfunc Select(n int) ([]string, error) {\n\tselected := make([]string, n)\n\twords := shortWords.Get()\n\tmax := big.NewInt(int64(len(words)))\n\tfor i := 0; i < n; i++ {\n\t\tj, err := rand.Int(rand.Reader, max)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"wordlist.Select %d: %v\", n, err)\n\t\t}\n\t\tselected[i] = words[j.Int64()]\n\t}\n\treturn selected, nil\n}\n"
  },
  {
    "path": "pkg/words/shortwords.txt",
    "content": "acid\nacorn\nacre\nacts\nafar\naffix\naged\nagent\nagile\naging\nagony\nahead\naide\naids\naim\najar\nalarm\nalias\nalibi\nalien\nalike\nalive\naloe\naloft\naloha\nalone\namend\namino\nample\namuse\nangel\nanger\nangle\nankle\napple\napril\napron\naqua\narea\narena\nargue\narise\narmed\narmor\narmy\naroma\narray\narson\nart\nashen\nashes\natlas\natom\nattic\naudio\navert\navoid\nawake\naward\nawoke\naxis\nbacon\nbadge\nbagel\nbaggy\nbaked\nbaker\nbalmy\nbanjo\nbarge\nbarn\nbash\nbasil\nbask\nbatch\nbath\nbaton\nbats\nblade\nblank\nblast\nblaze\nbleak\nblend\nbless\nblimp\nblink\nbloat\nblob\nblog\nblot\nblunt\nblurt\nblush\nboast\nboat\nbody\nboil\nbok\nbolt\nboned\nboney\nbonus\nbony\nbook\nbooth\nboots\nboss\nbotch\nboth\nboxer\nbreed\nbribe\nbrick\nbride\nbrim\nbring\nbrink\nbrisk\nbroad\nbroil\nbroke\nbrook\nbroom\nbrush\nbuck\nbud\nbuggy\nbulge\nbulk\nbully\nbunch\nbunny\nbunt\nbush\nbust\nbusy\nbuzz\ncable\ncache\ncadet\ncage\ncake\ncalm\ncameo\ncanal\ncandy\ncane\ncanon\ncape\ncard\ncargo\ncarol\ncarry\ncarve\ncase\ncash\ncause\ncedar\nchain\nchair\nchant\nchaos\ncharm\nchase\ncheek\ncheer\nchef\nchess\nchest\nchew\nchief\nchili\nchill\nchip\nchomp\nchop\nchow\nchuck\nchump\nchunk\nchurn\nchute\ncider\ncinch\ncity\ncivic\ncivil\nclad\nclaim\nclamp\nclap\nclash\nclasp\nclass\nclaw\nclay\nclean\nclear\ncleat\ncleft\nclerk\nclick\ncling\nclink\nclip\ncloak\nclock\nclone\ncloth\ncloud\nclump\ncoach\ncoast\ncoat\ncod\ncoil\ncoke\ncola\ncold\ncolt\ncoma\ncome\ncomic\ncomma\ncone\ncope\ncopy\ncoral\ncork\ncost\ncot\ncouch\ncough\ncover\ncozy\ncraft\ncramp\ncrane\ncrank\ncrate\ncrave\ncrawl\ncrazy\ncreme\ncrepe\ncrept\ncrib\ncried\ncrisp\ncrook\ncrop\ncross\ncrowd\ncrown\ncrumb\ncrush\ncrust\ncub\ncult\ncupid\ncure\ncurl\ncurry\ncurse\ncurve\ncurvy\ncushy\ncut\ncycle\ndab\ndad\ndaily\ndairy\ndaisy\ndance\ndandy\ndarn\ndart\ndash\ndata\ndate\ndawn\ndeaf\ndeal\ndean\ndebit\ndebt\ndebug\ndecaf\ndecal\ndecay\ndeck\ndecor\ndecoy\ndeed\ndelay\ndenim\ndense\ndent\ndepth\nderby\ndesk\ndial\ndiary\ndice\ndig\ndill\ndime\ndimly\ndiner\ndingy\ndisco\ndish\ndisk\nditch\nditzy\ndizzy\ndock\ndodge\ndoing\ndoll\ndome\ndonor\ndonut\ndose\ndot\ndove\ndown\ndowry\ndoze\ndrab\ndrama\ndrank\ndraw\ndress\ndried\ndrift\ndrill\ndrive\ndrone\ndroop\ndrove\ndrown\ndrum\ndry\nduck\nduct\ndude\ndug\nduke\nduo\ndusk\ndust\nduty\ndwarf\ndwell\neagle\nearly\nearth\neasel\neast\neaten\neats\nebay\nebony\nebook\necho\nedge\neel\neject\nelbow\nelder\nelf\nelk\nelm\nelope\nelude\nelves\nemail\nemit\nempty\nemu\nenter\nentry\nenvoy\nequal\nerase\nerror\nerupt\nessay\netch\nevade\neven\nevict\nevil\nevoke\nexact\nexit\nfable\nfaced\nfact\nfade\nfall\nfalse\nfancy\nfang\nfax\nfeast\nfeed\nfemur\nfence\nfend\nferry\nfetal\nfetch\nfever\nfiber\nfifth\nfifty\nfilm\nfilth\nfinal\nfinch\nfit\nfive\nflag\nflaky\nflame\nflap\nflask\nfled\nflick\nfling\nflint\nflip\nflirt\nfloat\nflock\nflop\nfloss\nflyer\nfoam\nfoe\nfog\nfoil\nfolic\nfolk\nfood\nfool\nfound\nfox\nfoyer\nfrail\nframe\nfray\nfresh\nfried\nfrill\nfrisk\nfrom\nfront\nfrost\nfroth\nfrown\nfroze\nfruit\ngag\ngains\ngala\ngame\ngap\ngas\ngave\ngear\ngecko\ngeek\ngem\ngenre\ngift\ngig\ngills\ngiven\ngiver\nglad\nglass\nglide\ngloss\nglove\nglow\nglue\ngoal\ngoing\ngolf\ngong\ngood\ngooey\ngoofy\ngore\ngown\ngrab\ngrain\ngrant\ngrape\ngraph\ngrasp\ngrass\ngrave\ngravy\ngray\ngreen\ngreet\ngrew\ngrid\ngrief\ngrill\ngrip\ngrit\ngroom\ngrope\ngrowl\ngrub\ngrunt\nguide\ngulf\ngulp\ngummy\nguru\ngush\ngut\nguy\nhabit\nhalf\nhalo\nhalt\nhappy\nharm\nhash\nhasty\nhatch\nhate\nhaven\nhazel\nhazy\nheap\nheat\nheave\nhedge\nhefty\nhelp\nherbs\nhers\nhub\nhug\nhula\nhull\nhuman\nhumid\nhump\nhung\nhunk\nhunt\nhurry\nhurt\nhush\nhut\nice\nicing\nicon\nicy\nigloo\nimage\nion\niron\nislam\nissue\nitem\nivory\nivy\njab\njam\njaws\njazz\njeep\njelly\njet\njiffy\njob\njog\njolly\njolt\njot\njoy\njudge\njuice\njuicy\njuly\njumbo\njump\njunky\njuror\njury\nkeep\nkeg\nkept\nkick\nkilt\nking\nkite\nkitty\nkiwi\nknee\nknelt\nkoala\nkung\nladle\nlady\nlair\nlake\nlance\nland\nlapel\nlarge\nlash\nlasso\nlast\nlatch\nlate\nlazy\nleft\nlegal\nlemon\nlend\nlens\nlent\nlevel\nlever\nlid\nlife\nlift\nlilac\nlily\nlimb\nlimes\nline\nlint\nlion\nlip\nlist\nlived\nliver\nlunar\nlunch\nlung\nlurch\nlure\nlurk\nlying\nlyric\nmace\nmaker\nmalt\nmama\nmango\nmanor\nmany\nmap\nmarch\nmardi\nmarry\nmash\nmatch\nmate\nmath\nmoan\nmocha\nmoist\nmold\nmom\nmoody\nmop\nmorse\nmost\nmotor\nmotto\nmount\nmouse\nmousy\nmouth\nmove\nmovie\nmower\nmud\nmug\nmulch\nmule\nmull\nmumbo\nmummy\nmural\nmuse\nmusic\nmusky\nmute\nnacho\nnag\nnail\nname\nnanny\nnap\nnavy\nnear\nneat\nneon\nnerd\nnest\nnet\nnext\nniece\nninth\nnutty\noak\noasis\noat\nocean\noil\nold\nolive\nomen\nonion\nonly\nooze\nopal\nopen\nopera\nopt\notter\nouch\nounce\nouter\noval\noven\nowl\nozone\npace\npagan\npager\npalm\npanda\npanic\npants\npanty\npaper\npark\nparty\npasta\npatch\npath\npatio\npayer\npecan\npenny\npep\nperch\nperky\nperm\npest\npetal\npetri\npetty\nphoto\nplank\nplant\nplaza\nplead\nplot\nplow\npluck\nplug\nplus\npoach\npod\npoem\npoet\npogo\npoint\npoise\npoker\npolar\npolio\npolka\npolo\npond\npony\npoppy\npork\nposer\npouch\npound\npout\npower\nprank\npress\nprint\nprior\nprism\nprize\nprobe\nprong\nproof\nprops\nprude\nprune\npry\npug\npull\npulp\npulse\npuma\npunch\npunk\npupil\npuppy\npurr\npurse\npush\nputt\nquack\nquake\nquery\nquiet\nquill\nquilt\nquit\nquota\nquote\nrabid\nrace\nrack\nradar\nradio\nraft\nrage\nraid\nrail\nrake\nrally\nramp\nranch\nrange\nrank\nrant\nrash\nraven\nreach\nreact\nream\nrebel\nrecap\nrelax\nrelay\nrelic\nremix\nrepay\nrepel\nreply\nrerun\nreset\nrhyme\nrice\nrich\nride\nrigid\nrigor\nrinse\nriot\nripen\nrise\nrisk\nritzy\nrival\nriver\nroast\nrobe\nrobin\nrock\nrogue\nroman\nromp\nrope\nrover\nroyal\nruby\nrug\nruin\nrule\nrunny\nrush\nrust\nrut\nsadly\nsage\nsaid\nsaint\nsalad\nsalon\nsalsa\nsalt\nsame\nsandy\nsanta\nsatin\nsauna\nsaved\nsavor\nsax\nsay\nscale\nscam\nscan\nscare\nscarf\nscary\nscoff\nscold\nscoop\nscoot\nscope\nscore\nscorn\nscout\nscowl\nscrap\nscrub\nscuba\nscuff\nsect\nsedan\nself\nsend\nsepia\nserve\nset\nseven\nshack\nshade\nshady\nshaft\nshaky\nsham\nshape\nshare\nsharp\nshed\nsheep\nsheet\nshelf\nshell\nshine\nshiny\nship\nshirt\nshock\nshop\nshore\nshout\nshove\nshown\nshowy\nshred\nshrug\nshun\nshush\nshut\nshy\nsift\nsilk\nsilly\nsilo\nsip\nsiren\nsixth\nsize\nskate\nskew\nskid\nskier\nskies\nskip\nskirt\nskit\nsky\nslab\nslack\nslain\nslam\nslang\nslash\nslate\nslaw\nsled\nsleek\nsleep\nsleet\nslept\nslice\nslick\nslimy\nsling\nslip\nslit\nslob\nslot\nslug\nslum\nslurp\nslush\nsmall\nsmash\nsmell\nsmile\nsmirk\nsmog\nsnack\nsnap\nsnare\nsnarl\nsneak\nsneer\nsniff\nsnore\nsnort\nsnout\nsnowy\nsnub\nsnuff\nspeak\nspeed\nspend\nspent\nspew\nspied\nspill\nspiny\nspoil\nspoke\nspoof\nspool\nspoon\nsport\nspot\nspout\nspray\nspree\nspur\nsquad\nsquat\nsquid\nstack\nstaff\nstage\nstain\nstall\nstamp\nstand\nstank\nstark\nstart\nstash\nstate\nstays\nsteam\nsteep\nstem\nstep\nstew\nstick\nsting\nstir\nstock\nstole\nstomp\nstony\nstood\nstool\nstoop\nstop\nstorm\nstout\nstove\nstraw\nstray\nstrut\nstuck\nstud\nstuff\nstump\nstung\nstunt\nsuds\nsugar\nsulk\nsurf\nsushi\nswab\nswan\nswarm\nsway\nswear\nsweat\nsweep\nswell\nswept\nswim\nswing\nswipe\nswirl\nswoop\nswore\nsyrup\ntacky\ntaco\ntag\ntake\ntall\ntalon\ntamer\ntank\ntaper\ntaps\ntarot\ntart\ntask\ntaste\ntasty\ntaunt\nthank\nthaw\ntheft\ntheme\nthigh\nthing\nthink\nthong\nthorn\nthose\nthrob\nthud\nthumb\nthump\nthus\ntiara\ntidal\ntidy\ntiger\ntile\ntilt\ntint\ntiny\ntrace\ntrack\ntrade\ntrain\ntrait\ntrap\ntrash\ntray\ntreat\ntree\ntrek\ntrend\ntrial\ntribe\ntrick\ntrio\ntrout\ntruce\ntruck\ntrump\ntrunk\ntry\ntug\ntulip\ntummy\nturf\ntusk\ntutor\ntutu\ntux\ntweak\ntweet\ntwice\ntwine\ntwins\ntwirl\ntwist\nuncle\nuncut\nundo\nunify\nunion\nunit\nuntie\nupon\nupper\nurban\nused\nuser\nusher\nutter\nvalue\nvapor\nvegan\nvenue\nverse\nvest\nveto\nvice\nvideo\nview\nviral\nvirus\nvisa\nvisor\nvixen\nvocal\nvoice\nvoid\nvolt\nvoter\nvowel\nwad\nwafer\nwager\nwages\nwagon\nwake\nwalk\nwand\nwasp\nwatch\nwater\nwavy\nwheat\nwhiff\nwhole\nwhoop\nwick\nwiden\nwidow\nwidth\nwife\nwifi\nwilt\nwimp\nwind\nwing\nwink\nwipe\nwired\nwiry\nwise\nwish\nwispy\nwok\nwolf\nwomb\nwool\nwoozy\nword\nwork\nworry\nwound\nwoven\nwrath\nwreck\nwrist\nxerox\nyahoo\nyam\nyard\nyear\nyeast\nyelp\nyield\nyodel\nyoga\nyoyo\nyummy\nzebra\nzero\nzesty\nzippy\nzone\nzoom\n"
  },
  {
    "path": "pkg/words/words.go",
    "content": "package words\n\nimport (\n\t_ \"embed\" // for go:embed\n\t\"strings\"\n\t\"sync\"\n)\n\ntype wordList struct {\n\traw   string\n\tonce  sync.Once\n\twords []string\n}\n\nfunc (w *wordList) Get() []string {\n\tw.once.Do(func() {\n\t\traw := strings.TrimSpace(w.raw)\n\t\tw.words = strings.Split(raw, \"\\n\")\n\t})\n\treturn w.words\n}\n\nvar (\n\t//go:embed shortwords.txt\n\tshortRaw string\n\n\tshortWords = wordList{raw: shortRaw}\n)\n"
  },
  {
    "path": "pkg/words/words_test.go",
    "content": "package words\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestWords(t *testing.T) {\n\tlist := shortWords.Get()\n\tif len(list) != 1295 {\n\t\tt.Errorf(\"expected 1295 words, got %d\", len(list))\n\t}\n\n\tseen := make(map[string]bool)\n\tfor _, w := range list {\n\t\tif seen[w] {\n\t\t\tt.Errorf(\"duplicate word %q\", w)\n\t\t} else if len(w) < 3 || len(w) > 5 {\n\t\t\tt.Errorf(\"expected word to be 3-5 5 characters, got %q\", w)\n\t\t} else if strings.TrimSpace(w) != w {\n\t\t\tt.Errorf(\"expected word to be trimmed, got %q\", w)\n\t\t}\n\t\tseen[w] = true\n\t}\n}\n"
  },
  {
    "path": "pkg/xos/xos_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\n// Package xos provides cross-platform helper functions.\npackage xos\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"syscall\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/google/renameio/v2\"\n)\n\nfunc CreateNewProcessGroup() *syscall.SysProcAttr {\n\treturn &syscall.SysProcAttr{Setpgid: true}\n}\n\nfunc SocketStat(path string) (interface{}, error) {\n\treturn os.Stat(path)\n}\n\nfunc SameSocket(a, b interface{}) bool {\n\tai := a.(os.FileInfo)\n\tbi := b.(os.FileInfo)\n\treturn os.SameFile(ai, bi)\n}\n\nfunc ArrangeExtraFiles(cmd *exec.Cmd, files ...*os.File) error {\n\tcmd.ExtraFiles = files\n\treturn nil\n}\n\nfunc IsAdminUser() (bool, error) {\n\tusr, err := user.Current()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn usr.Gid == \"0\", nil\n}\n\n// WriteFile writes the given file with the given data and permissions.\n//\n// Where possible (i.e. not on windows) it will use an atomic write process\n// which removes the possibility of a partial file being written during a crash\n// or error.\nfunc WriteFile(filename string, data []byte, perm os.FileMode) error {\n\treturn errors.WithStack(renameio.WriteFile(filename, data, perm))\n}\n\n// IsWindowsJunctionPoint reports whether a filename is a Windows junction point.\nfunc IsWindowsJunctionPoint(filename string) (ok bool, err error) {\n\treturn false, nil\n}\n"
  },
  {
    "path": "pkg/xos/xos_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage xos\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc CreateNewProcessGroup() *syscall.SysProcAttr {\n\treturn &syscall.SysProcAttr{\n\t\tCreationFlags: windows.CREATE_NEW_PROCESS_GROUP,\n\t}\n}\n\nfunc SocketStat(path string) (interface{}, error) {\n\tfd, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_READ, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OPEN_REPARSE_POINT|windows.FILE_FLAG_BACKUP_SEMANTICS, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"CreateFile %s: %w\", path, err)\n\t}\n\tdefer windows.CloseHandle(fd)\n\n\tvar d syscall.ByHandleFileInformation\n\terr = syscall.GetFileInformationByHandle(syscall.Handle(fd), &d)\n\tif err != nil {\n\t\treturn nil, &os.PathError{\"GetFileInformationByHandle\", path, err}\n\t}\n\treturn &d, nil\n}\n\nfunc SameSocket(a, b interface{}) bool {\n\tai := a.(*syscall.ByHandleFileInformation)\n\tbi := b.(*syscall.ByHandleFileInformation)\n\treturn ai.VolumeSerialNumber == bi.VolumeSerialNumber && ai.FileIndexHigh == bi.FileIndexHigh && ai.FileIndexLow == bi.FileIndexLow\n}\n\nfunc ArrangeExtraFiles(cmd *exec.Cmd, files ...*os.File) error {\n\tattr := cmd.SysProcAttr\n\tif attr == nil {\n\t\tattr = &syscall.SysProcAttr{}\n\t\tcmd.SysProcAttr = attr\n\t}\n\n\t// Flag the files to bbe inherited by the child process\n\tvar fds []string\n\tfor _, f := range files {\n\t\tfd := f.Fd()\n\t\tfds = append(fds, strconv.FormatUint(uint64(fd), 10))\n\t\terr := windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 1)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"xos.ArrangeExtraFiles: SetHandleInformation: %v\", err)\n\t\t}\n\t\tattr.AdditionalInheritedHandles = append(attr.AdditionalInheritedHandles, syscall.Handle(fd))\n\t}\n\t// If the env hasn't been set, copy over this process' env so we preserve the cmd.Env semantics.\n\tif cmd.Env == nil {\n\t\tcmd.Env = os.Environ()\n\t}\n\tcmd.Env = append(cmd.Env, \"ENCORE_EXTRA_FILES=\"+strings.Join(fds, \",\"))\n\treturn nil\n}\n\nfunc IsAdminUser() (bool, error) {\n\t// For Windows we elevate permissions on demand, so pretend we are admin\n\treturn true, nil\n}\n\n// WriteFile writes the given file with the given data and permissions.\n//\n// Where possible (i.e. not on windows) it will use an atomic write process\n// which removes the possibility of a partial file being written during a crash\n// or error.\nfunc WriteFile(filename string, data []byte, perm os.FileMode) error {\n\treturn errors.WithStack(os.WriteFile(filename, data, perm))\n}\n\n// IsWindowsJunctionPoint reports whether a filename is a Windows junction point.\nfunc IsWindowsJunctionPoint(filename string) (ok bool, err error) {\n\tptr, err := syscall.UTF16PtrFromString(filename)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tattrs, err := windows.GetFileAttributes(ptr)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn attrs&windows.FILE_ATTRIBUTE_REPARSE_POINT == windows.FILE_ATTRIBUTE_REPARSE_POINT, nil\n}\n"
  },
  {
    "path": "proto/encore/daemon/daemon.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/daemon/daemon.proto\n\npackage daemon\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype DBRole int32\n\nconst (\n\tDBRole_DB_ROLE_UNSPECIFIED DBRole = 0\n\tDBRole_DB_ROLE_SUPERUSER   DBRole = 1\n\tDBRole_DB_ROLE_ADMIN       DBRole = 2\n\tDBRole_DB_ROLE_WRITE       DBRole = 3\n\tDBRole_DB_ROLE_READ        DBRole = 4\n)\n\n// Enum value maps for DBRole.\nvar (\n\tDBRole_name = map[int32]string{\n\t\t0: \"DB_ROLE_UNSPECIFIED\",\n\t\t1: \"DB_ROLE_SUPERUSER\",\n\t\t2: \"DB_ROLE_ADMIN\",\n\t\t3: \"DB_ROLE_WRITE\",\n\t\t4: \"DB_ROLE_READ\",\n\t}\n\tDBRole_value = map[string]int32{\n\t\t\"DB_ROLE_UNSPECIFIED\": 0,\n\t\t\"DB_ROLE_SUPERUSER\":   1,\n\t\t\"DB_ROLE_ADMIN\":       2,\n\t\t\"DB_ROLE_WRITE\":       3,\n\t\t\"DB_ROLE_READ\":        4,\n\t}\n)\n\nfunc (x DBRole) Enum() *DBRole {\n\tp := new(DBRole)\n\t*p = x\n\treturn p\n}\n\nfunc (x DBRole) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (DBRole) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_daemon_daemon_proto_enumTypes[0].Descriptor()\n}\n\nfunc (DBRole) Type() protoreflect.EnumType {\n\treturn &file_encore_daemon_daemon_proto_enumTypes[0]\n}\n\nfunc (x DBRole) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use DBRole.Descriptor instead.\nfunc (DBRole) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{0}\n}\n\ntype DBClusterType int32\n\nconst (\n\tDBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED DBClusterType = 0\n\tDBClusterType_DB_CLUSTER_TYPE_RUN         DBClusterType = 1\n\tDBClusterType_DB_CLUSTER_TYPE_TEST        DBClusterType = 2\n\tDBClusterType_DB_CLUSTER_TYPE_SHADOW      DBClusterType = 3\n)\n\n// Enum value maps for DBClusterType.\nvar (\n\tDBClusterType_name = map[int32]string{\n\t\t0: \"DB_CLUSTER_TYPE_UNSPECIFIED\",\n\t\t1: \"DB_CLUSTER_TYPE_RUN\",\n\t\t2: \"DB_CLUSTER_TYPE_TEST\",\n\t\t3: \"DB_CLUSTER_TYPE_SHADOW\",\n\t}\n\tDBClusterType_value = map[string]int32{\n\t\t\"DB_CLUSTER_TYPE_UNSPECIFIED\": 0,\n\t\t\"DB_CLUSTER_TYPE_RUN\":         1,\n\t\t\"DB_CLUSTER_TYPE_TEST\":        2,\n\t\t\"DB_CLUSTER_TYPE_SHADOW\":      3,\n\t}\n)\n\nfunc (x DBClusterType) Enum() *DBClusterType {\n\tp := new(DBClusterType)\n\t*p = x\n\treturn p\n}\n\nfunc (x DBClusterType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (DBClusterType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_daemon_daemon_proto_enumTypes[1].Descriptor()\n}\n\nfunc (DBClusterType) Type() protoreflect.EnumType {\n\treturn &file_encore_daemon_daemon_proto_enumTypes[1]\n}\n\nfunc (x DBClusterType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use DBClusterType.Descriptor instead.\nfunc (DBClusterType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{1}\n}\n\ntype RunRequest_BrowserMode int32\n\nconst (\n\tRunRequest_BROWSER_AUTO   RunRequest_BrowserMode = 0\n\tRunRequest_BROWSER_NEVER  RunRequest_BrowserMode = 1\n\tRunRequest_BROWSER_ALWAYS RunRequest_BrowserMode = 2\n)\n\n// Enum value maps for RunRequest_BrowserMode.\nvar (\n\tRunRequest_BrowserMode_name = map[int32]string{\n\t\t0: \"BROWSER_AUTO\",\n\t\t1: \"BROWSER_NEVER\",\n\t\t2: \"BROWSER_ALWAYS\",\n\t}\n\tRunRequest_BrowserMode_value = map[string]int32{\n\t\t\"BROWSER_AUTO\":   0,\n\t\t\"BROWSER_NEVER\":  1,\n\t\t\"BROWSER_ALWAYS\": 2,\n\t}\n)\n\nfunc (x RunRequest_BrowserMode) Enum() *RunRequest_BrowserMode {\n\tp := new(RunRequest_BrowserMode)\n\t*p = x\n\treturn p\n}\n\nfunc (x RunRequest_BrowserMode) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (RunRequest_BrowserMode) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_daemon_daemon_proto_enumTypes[2].Descriptor()\n}\n\nfunc (RunRequest_BrowserMode) Type() protoreflect.EnumType {\n\treturn &file_encore_daemon_daemon_proto_enumTypes[2]\n}\n\nfunc (x RunRequest_BrowserMode) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use RunRequest_BrowserMode.Descriptor instead.\nfunc (RunRequest_BrowserMode) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{6, 0}\n}\n\ntype RunRequest_DebugMode int32\n\nconst (\n\tRunRequest_DEBUG_DISABLED RunRequest_DebugMode = 0\n\tRunRequest_DEBUG_ENABLED  RunRequest_DebugMode = 1\n\tRunRequest_DEBUG_BREAK    RunRequest_DebugMode = 2\n)\n\n// Enum value maps for RunRequest_DebugMode.\nvar (\n\tRunRequest_DebugMode_name = map[int32]string{\n\t\t0: \"DEBUG_DISABLED\",\n\t\t1: \"DEBUG_ENABLED\",\n\t\t2: \"DEBUG_BREAK\",\n\t}\n\tRunRequest_DebugMode_value = map[string]int32{\n\t\t\"DEBUG_DISABLED\": 0,\n\t\t\"DEBUG_ENABLED\":  1,\n\t\t\"DEBUG_BREAK\":    2,\n\t}\n)\n\nfunc (x RunRequest_DebugMode) Enum() *RunRequest_DebugMode {\n\tp := new(RunRequest_DebugMode)\n\t*p = x\n\treturn p\n}\n\nfunc (x RunRequest_DebugMode) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (RunRequest_DebugMode) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_daemon_daemon_proto_enumTypes[3].Descriptor()\n}\n\nfunc (RunRequest_DebugMode) Type() protoreflect.EnumType {\n\treturn &file_encore_daemon_daemon_proto_enumTypes[3]\n}\n\nfunc (x RunRequest_DebugMode) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use RunRequest_DebugMode.Descriptor instead.\nfunc (RunRequest_DebugMode) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{6, 1}\n}\n\ntype DumpMetaRequest_Format int32\n\nconst (\n\tDumpMetaRequest_FORMAT_UNSPECIFIED DumpMetaRequest_Format = 0\n\tDumpMetaRequest_FORMAT_JSON        DumpMetaRequest_Format = 1\n\tDumpMetaRequest_FORMAT_PROTO       DumpMetaRequest_Format = 2\n)\n\n// Enum value maps for DumpMetaRequest_Format.\nvar (\n\tDumpMetaRequest_Format_name = map[int32]string{\n\t\t0: \"FORMAT_UNSPECIFIED\",\n\t\t1: \"FORMAT_JSON\",\n\t\t2: \"FORMAT_PROTO\",\n\t}\n\tDumpMetaRequest_Format_value = map[string]int32{\n\t\t\"FORMAT_UNSPECIFIED\": 0,\n\t\t\"FORMAT_JSON\":        1,\n\t\t\"FORMAT_PROTO\":       2,\n\t}\n)\n\nfunc (x DumpMetaRequest_Format) Enum() *DumpMetaRequest_Format {\n\tp := new(DumpMetaRequest_Format)\n\t*p = x\n\treturn p\n}\n\nfunc (x DumpMetaRequest_Format) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (DumpMetaRequest_Format) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_daemon_daemon_proto_enumTypes[4].Descriptor()\n}\n\nfunc (DumpMetaRequest_Format) Type() protoreflect.EnumType {\n\treturn &file_encore_daemon_daemon_proto_enumTypes[4]\n}\n\nfunc (x DumpMetaRequest_Format) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use DumpMetaRequest_Format.Descriptor instead.\nfunc (DumpMetaRequest_Format) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{35, 0}\n}\n\ntype CommandMessage struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Msg:\n\t//\n\t//\t*CommandMessage_Output\n\t//\t*CommandMessage_Exit\n\t//\t*CommandMessage_Errors\n\tMsg           isCommandMessage_Msg `protobuf_oneof:\"msg\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CommandMessage) Reset() {\n\t*x = CommandMessage{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CommandMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CommandMessage) ProtoMessage() {}\n\nfunc (x *CommandMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CommandMessage.ProtoReflect.Descriptor instead.\nfunc (*CommandMessage) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *CommandMessage) GetMsg() isCommandMessage_Msg {\n\tif x != nil {\n\t\treturn x.Msg\n\t}\n\treturn nil\n}\n\nfunc (x *CommandMessage) GetOutput() *CommandOutput {\n\tif x != nil {\n\t\tif x, ok := x.Msg.(*CommandMessage_Output); ok {\n\t\t\treturn x.Output\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *CommandMessage) GetExit() *CommandExit {\n\tif x != nil {\n\t\tif x, ok := x.Msg.(*CommandMessage_Exit); ok {\n\t\t\treturn x.Exit\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *CommandMessage) GetErrors() *CommandDisplayErrors {\n\tif x != nil {\n\t\tif x, ok := x.Msg.(*CommandMessage_Errors); ok {\n\t\t\treturn x.Errors\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isCommandMessage_Msg interface {\n\tisCommandMessage_Msg()\n}\n\ntype CommandMessage_Output struct {\n\tOutput *CommandOutput `protobuf:\"bytes,1,opt,name=output,proto3,oneof\"`\n}\n\ntype CommandMessage_Exit struct {\n\tExit *CommandExit `protobuf:\"bytes,2,opt,name=exit,proto3,oneof\"`\n}\n\ntype CommandMessage_Errors struct {\n\tErrors *CommandDisplayErrors `protobuf:\"bytes,3,opt,name=errors,proto3,oneof\"`\n}\n\nfunc (*CommandMessage_Output) isCommandMessage_Msg() {}\n\nfunc (*CommandMessage_Exit) isCommandMessage_Msg() {}\n\nfunc (*CommandMessage_Errors) isCommandMessage_Msg() {}\n\ntype CommandOutput struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tStdout        []byte                 `protobuf:\"bytes,1,opt,name=stdout,proto3\" json:\"stdout,omitempty\"`\n\tStderr        []byte                 `protobuf:\"bytes,2,opt,name=stderr,proto3\" json:\"stderr,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CommandOutput) Reset() {\n\t*x = CommandOutput{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CommandOutput) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CommandOutput) ProtoMessage() {}\n\nfunc (x *CommandOutput) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CommandOutput.ProtoReflect.Descriptor instead.\nfunc (*CommandOutput) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *CommandOutput) GetStdout() []byte {\n\tif x != nil {\n\t\treturn x.Stdout\n\t}\n\treturn nil\n}\n\nfunc (x *CommandOutput) GetStderr() []byte {\n\tif x != nil {\n\t\treturn x.Stderr\n\t}\n\treturn nil\n}\n\ntype CommandExit struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCode          int32                  `protobuf:\"varint,1,opt,name=code,proto3\" json:\"code,omitempty\"` // exit code\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CommandExit) Reset() {\n\t*x = CommandExit{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CommandExit) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CommandExit) ProtoMessage() {}\n\nfunc (x *CommandExit) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CommandExit.ProtoReflect.Descriptor instead.\nfunc (*CommandExit) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *CommandExit) GetCode() int32 {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn 0\n}\n\ntype CommandDisplayErrors struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErrinsrc      []byte                 `protobuf:\"bytes,1,opt,name=errinsrc,proto3\" json:\"errinsrc,omitempty\"` // error messages in source code\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CommandDisplayErrors) Reset() {\n\t*x = CommandDisplayErrors{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CommandDisplayErrors) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CommandDisplayErrors) ProtoMessage() {}\n\nfunc (x *CommandDisplayErrors) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CommandDisplayErrors.ProtoReflect.Descriptor instead.\nfunc (*CommandDisplayErrors) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *CommandDisplayErrors) GetErrinsrc() []byte {\n\tif x != nil {\n\t\treturn x.Errinsrc\n\t}\n\treturn nil\n}\n\ntype CreateAppRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// app_root is the absolute filesystem path to the Encore app root.\n\tAppRoot string `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\t// template is the template used to create the app\n\tTemplate string `protobuf:\"bytes,2,opt,name=template,proto3\" json:\"template,omitempty\"`\n\t// tutorial is a flag to indicate if the app is a tutorial app\n\tTutorial      bool `protobuf:\"varint,3,opt,name=tutorial,proto3\" json:\"tutorial,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateAppRequest) Reset() {\n\t*x = CreateAppRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateAppRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateAppRequest) ProtoMessage() {}\n\nfunc (x *CreateAppRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateAppRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateAppRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *CreateAppRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateAppRequest) GetTemplate() string {\n\tif x != nil {\n\t\treturn x.Template\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateAppRequest) GetTutorial() bool {\n\tif x != nil {\n\t\treturn x.Tutorial\n\t}\n\treturn false\n}\n\ntype CreateAppResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppId         string                 `protobuf:\"bytes,1,opt,name=app_id,json=appId,proto3\" json:\"app_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateAppResponse) Reset() {\n\t*x = CreateAppResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateAppResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateAppResponse) ProtoMessage() {}\n\nfunc (x *CreateAppResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateAppResponse.ProtoReflect.Descriptor instead.\nfunc (*CreateAppResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *CreateAppResponse) GetAppId() string {\n\tif x != nil {\n\t\treturn x.AppId\n\t}\n\treturn \"\"\n}\n\ntype RunRequest struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// app_root is the absolute filesystem path to the Encore app root.\n\tAppRoot string `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\t// working_dir is the working directory relative to the app_root,\n\t// for formatting relative paths in error messages.\n\tWorkingDir string `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"`\n\t// watch, if true, enables live reloading of the app whenever the source changes.\n\tWatch bool `protobuf:\"varint,5,opt,name=watch,proto3\" json:\"watch,omitempty\"`\n\t// listen_addr is the address to listen on.\n\tListenAddr string `protobuf:\"bytes,6,opt,name=listen_addr,json=listenAddr,proto3\" json:\"listen_addr,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,7,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// trace_file, if set specifies a trace file to write trace information\n\t// about the parse and compilation process to.\n\tTraceFile *string `protobuf:\"bytes,8,opt,name=trace_file,json=traceFile,proto3,oneof\" json:\"trace_file,omitempty\"`\n\t// namespace is the infrastructure namespace to use.\n\t// If empty the active namespace is used.\n\tNamespace *string `protobuf:\"bytes,9,opt,name=namespace,proto3,oneof\" json:\"namespace,omitempty\"`\n\t// browser specifies whether and how to open the browser on startup.\n\tBrowser RunRequest_BrowserMode `protobuf:\"varint,10,opt,name=browser,proto3,enum=encore.daemon.RunRequest_BrowserMode\" json:\"browser,omitempty\"`\n\t// debug_mode specifies the debug mode to use.\n\tDebugMode RunRequest_DebugMode `protobuf:\"varint,11,opt,name=debug_mode,json=debugMode,proto3,enum=encore.daemon.RunRequest_DebugMode\" json:\"debug_mode,omitempty\"`\n\t// Log level override.\n\tLogLevel *string `protobuf:\"bytes,12,opt,name=log_level,json=logLevel,proto3,oneof\" json:\"log_level,omitempty\"`\n\t// scrub_sensitive_data, if true, scrubs sensitive data from local traces.\n\tScrubSensitiveData bool `protobuf:\"varint,13,opt,name=scrub_sensitive_data,json=scrubSensitiveData,proto3\" json:\"scrub_sensitive_data,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *RunRequest) Reset() {\n\t*x = RunRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RunRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RunRequest) ProtoMessage() {}\n\nfunc (x *RunRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RunRequest.ProtoReflect.Descriptor instead.\nfunc (*RunRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *RunRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *RunRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *RunRequest) GetWatch() bool {\n\tif x != nil {\n\t\treturn x.Watch\n\t}\n\treturn false\n}\n\nfunc (x *RunRequest) GetListenAddr() string {\n\tif x != nil {\n\t\treturn x.ListenAddr\n\t}\n\treturn \"\"\n}\n\nfunc (x *RunRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *RunRequest) GetTraceFile() string {\n\tif x != nil && x.TraceFile != nil {\n\t\treturn *x.TraceFile\n\t}\n\treturn \"\"\n}\n\nfunc (x *RunRequest) GetNamespace() string {\n\tif x != nil && x.Namespace != nil {\n\t\treturn *x.Namespace\n\t}\n\treturn \"\"\n}\n\nfunc (x *RunRequest) GetBrowser() RunRequest_BrowserMode {\n\tif x != nil {\n\t\treturn x.Browser\n\t}\n\treturn RunRequest_BROWSER_AUTO\n}\n\nfunc (x *RunRequest) GetDebugMode() RunRequest_DebugMode {\n\tif x != nil {\n\t\treturn x.DebugMode\n\t}\n\treturn RunRequest_DEBUG_DISABLED\n}\n\nfunc (x *RunRequest) GetLogLevel() string {\n\tif x != nil && x.LogLevel != nil {\n\t\treturn *x.LogLevel\n\t}\n\treturn \"\"\n}\n\nfunc (x *RunRequest) GetScrubSensitiveData() bool {\n\tif x != nil {\n\t\treturn x.ScrubSensitiveData\n\t}\n\treturn false\n}\n\ntype TestRequest struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot    string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tWorkingDir string                 `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"`\n\tArgs       []string               `protobuf:\"bytes,3,rep,name=args,proto3\" json:\"args,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,4,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// trace_file, if set specifies a trace file to write trace information\n\t// about the parse and compilation process to.\n\tTraceFile *string `protobuf:\"bytes,6,opt,name=trace_file,json=traceFile,proto3,oneof\" json:\"trace_file,omitempty\"`\n\t// codegen_debug, if true, dumps the generated code and prints where it is located.\n\tCodegenDebug bool `protobuf:\"varint,7,opt,name=codegen_debug,json=codegenDebug,proto3\" json:\"codegen_debug,omitempty\"`\n\t// temp_dir is a temp dir that will be cleaned up after tests have been executed\n\t// to write things like app meta and runtime config etc.\n\tTempDir       string `protobuf:\"bytes,8,opt,name=temp_dir,json=tempDir,proto3\" json:\"temp_dir,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TestRequest) Reset() {\n\t*x = TestRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TestRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestRequest) ProtoMessage() {}\n\nfunc (x *TestRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestRequest.ProtoReflect.Descriptor instead.\nfunc (*TestRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *TestRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestRequest) GetArgs() []string {\n\tif x != nil {\n\t\treturn x.Args\n\t}\n\treturn nil\n}\n\nfunc (x *TestRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *TestRequest) GetTraceFile() string {\n\tif x != nil && x.TraceFile != nil {\n\t\treturn *x.TraceFile\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestRequest) GetCodegenDebug() bool {\n\tif x != nil {\n\t\treturn x.CodegenDebug\n\t}\n\treturn false\n}\n\nfunc (x *TestRequest) GetTempDir() string {\n\tif x != nil {\n\t\treturn x.TempDir\n\t}\n\treturn \"\"\n}\n\ntype TestSpecRequest struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot    string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tWorkingDir string                 `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"`\n\tArgs       []string               `protobuf:\"bytes,3,rep,name=args,proto3\" json:\"args,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,4,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// temp_dir is a temp dir that will be cleaned up after tests have been executed\n\t// to write things like app meta and runtime config etc.\n\tTempDir       string `protobuf:\"bytes,5,opt,name=temp_dir,json=tempDir,proto3\" json:\"temp_dir,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TestSpecRequest) Reset() {\n\t*x = TestSpecRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TestSpecRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestSpecRequest) ProtoMessage() {}\n\nfunc (x *TestSpecRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestSpecRequest.ProtoReflect.Descriptor instead.\nfunc (*TestSpecRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *TestSpecRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpecRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpecRequest) GetArgs() []string {\n\tif x != nil {\n\t\treturn x.Args\n\t}\n\treturn nil\n}\n\nfunc (x *TestSpecRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *TestSpecRequest) GetTempDir() string {\n\tif x != nil {\n\t\treturn x.TempDir\n\t}\n\treturn \"\"\n}\n\ntype TestSpecResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCommand       string                 `protobuf:\"bytes,1,opt,name=command,proto3\" json:\"command,omitempty\"`\n\tArgs          []string               `protobuf:\"bytes,2,rep,name=args,proto3\" json:\"args,omitempty\"`\n\tEnviron       []string               `protobuf:\"bytes,3,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TestSpecResponse) Reset() {\n\t*x = TestSpecResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TestSpecResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestSpecResponse) ProtoMessage() {}\n\nfunc (x *TestSpecResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestSpecResponse.ProtoReflect.Descriptor instead.\nfunc (*TestSpecResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *TestSpecResponse) GetCommand() string {\n\tif x != nil {\n\t\treturn x.Command\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpecResponse) GetArgs() []string {\n\tif x != nil {\n\t\treturn x.Args\n\t}\n\treturn nil\n}\n\nfunc (x *TestSpecResponse) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\ntype ExecScriptRequest struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot    string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tWorkingDir string                 `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"`\n\tScriptArgs []string               `protobuf:\"bytes,4,rep,name=script_args,json=scriptArgs,proto3\" json:\"script_args,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,5,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// trace_file, if set specifies a trace file to write trace information\n\t// about the parse and compilation process to.\n\tTraceFile *string `protobuf:\"bytes,6,opt,name=trace_file,json=traceFile,proto3,oneof\" json:\"trace_file,omitempty\"`\n\t// namespace is the infrastructure namespace to use.\n\t// If empty the active namespace is used.\n\tNamespace     *string `protobuf:\"bytes,7,opt,name=namespace,proto3,oneof\" json:\"namespace,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ExecScriptRequest) Reset() {\n\t*x = ExecScriptRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ExecScriptRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExecScriptRequest) ProtoMessage() {}\n\nfunc (x *ExecScriptRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExecScriptRequest.ProtoReflect.Descriptor instead.\nfunc (*ExecScriptRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *ExecScriptRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecScriptRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecScriptRequest) GetScriptArgs() []string {\n\tif x != nil {\n\t\treturn x.ScriptArgs\n\t}\n\treturn nil\n}\n\nfunc (x *ExecScriptRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *ExecScriptRequest) GetTraceFile() string {\n\tif x != nil && x.TraceFile != nil {\n\t\treturn *x.TraceFile\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecScriptRequest) GetNamespace() string {\n\tif x != nil && x.Namespace != nil {\n\t\treturn *x.Namespace\n\t}\n\treturn \"\"\n}\n\ntype ExecSpecRequest struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot    string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tWorkingDir string                 `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"`\n\tScriptArgs []string               `protobuf:\"bytes,4,rep,name=script_args,json=scriptArgs,proto3\" json:\"script_args,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,5,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// namespace is the infrastructure namespace to use.\n\t// If empty the active namespace is used.\n\tNamespace *string `protobuf:\"bytes,7,opt,name=namespace,proto3,oneof\" json:\"namespace,omitempty\"`\n\t// temp_dir is a temp dir that will be cleaned up by the CLI after the command\n\t// has been executed, to write things like app meta and runtime config etc.\n\tTempDir       string `protobuf:\"bytes,8,opt,name=temp_dir,json=tempDir,proto3\" json:\"temp_dir,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ExecSpecRequest) Reset() {\n\t*x = ExecSpecRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ExecSpecRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExecSpecRequest) ProtoMessage() {}\n\nfunc (x *ExecSpecRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExecSpecRequest.ProtoReflect.Descriptor instead.\nfunc (*ExecSpecRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *ExecSpecRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecSpecRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecSpecRequest) GetScriptArgs() []string {\n\tif x != nil {\n\t\treturn x.ScriptArgs\n\t}\n\treturn nil\n}\n\nfunc (x *ExecSpecRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *ExecSpecRequest) GetNamespace() string {\n\tif x != nil && x.Namespace != nil {\n\t\treturn *x.Namespace\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecSpecRequest) GetTempDir() string {\n\tif x != nil {\n\t\treturn x.TempDir\n\t}\n\treturn \"\"\n}\n\ntype ExecSpecMessage struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Msg:\n\t//\n\t//\t*ExecSpecMessage_Output\n\t//\t*ExecSpecMessage_Spec\n\tMsg           isExecSpecMessage_Msg `protobuf_oneof:\"msg\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ExecSpecMessage) Reset() {\n\t*x = ExecSpecMessage{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ExecSpecMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExecSpecMessage) ProtoMessage() {}\n\nfunc (x *ExecSpecMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExecSpecMessage.ProtoReflect.Descriptor instead.\nfunc (*ExecSpecMessage) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *ExecSpecMessage) GetMsg() isExecSpecMessage_Msg {\n\tif x != nil {\n\t\treturn x.Msg\n\t}\n\treturn nil\n}\n\nfunc (x *ExecSpecMessage) GetOutput() *CommandOutput {\n\tif x != nil {\n\t\tif x, ok := x.Msg.(*ExecSpecMessage_Output); ok {\n\t\t\treturn x.Output\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ExecSpecMessage) GetSpec() *ExecSpecResponse {\n\tif x != nil {\n\t\tif x, ok := x.Msg.(*ExecSpecMessage_Spec); ok {\n\t\t\treturn x.Spec\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isExecSpecMessage_Msg interface {\n\tisExecSpecMessage_Msg()\n}\n\ntype ExecSpecMessage_Output struct {\n\tOutput *CommandOutput `protobuf:\"bytes,1,opt,name=output,proto3,oneof\"`\n}\n\ntype ExecSpecMessage_Spec struct {\n\tSpec *ExecSpecResponse `protobuf:\"bytes,2,opt,name=spec,proto3,oneof\"`\n}\n\nfunc (*ExecSpecMessage_Output) isExecSpecMessage_Msg() {}\n\nfunc (*ExecSpecMessage_Spec) isExecSpecMessage_Msg() {}\n\ntype ExecSpecResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCommand       string                 `protobuf:\"bytes,1,opt,name=command,proto3\" json:\"command,omitempty\"`\n\tArgs          []string               `protobuf:\"bytes,2,rep,name=args,proto3\" json:\"args,omitempty\"`\n\tEnviron       []string               `protobuf:\"bytes,3,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ExecSpecResponse) Reset() {\n\t*x = ExecSpecResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ExecSpecResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExecSpecResponse) ProtoMessage() {}\n\nfunc (x *ExecSpecResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExecSpecResponse.ProtoReflect.Descriptor instead.\nfunc (*ExecSpecResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *ExecSpecResponse) GetCommand() string {\n\tif x != nil {\n\t\treturn x.Command\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExecSpecResponse) GetArgs() []string {\n\tif x != nil {\n\t\treturn x.Args\n\t}\n\treturn nil\n}\n\nfunc (x *ExecSpecResponse) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\ntype CheckRequest struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot    string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tWorkingDir string                 `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"`\n\t// codegen_debug, if true, dumps the generated code and prints where it is located.\n\tCodegenDebug bool `protobuf:\"varint,3,opt,name=codegen_debug,json=codegenDebug,proto3\" json:\"codegen_debug,omitempty\"`\n\t// parse_tests, if true, exercises test parsing and codegen as well.\n\tParseTests bool `protobuf:\"varint,4,opt,name=parse_tests,json=parseTests,proto3\" json:\"parse_tests,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron       []string `protobuf:\"bytes,5,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CheckRequest) Reset() {\n\t*x = CheckRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CheckRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CheckRequest) ProtoMessage() {}\n\nfunc (x *CheckRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CheckRequest.ProtoReflect.Descriptor instead.\nfunc (*CheckRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *CheckRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *CheckRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *CheckRequest) GetCodegenDebug() bool {\n\tif x != nil {\n\t\treturn x.CodegenDebug\n\t}\n\treturn false\n}\n\nfunc (x *CheckRequest) GetParseTests() bool {\n\tif x != nil {\n\t\treturn x.ParseTests\n\t}\n\treturn false\n}\n\nfunc (x *CheckRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\ntype ExportRequest struct {\n\tstate   protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\t// goos and goarch specify the platform configuration to compile\n\t// the application for. The values must be valid GOOS/GOARCH values.\n\tGoos   string `protobuf:\"bytes,2,opt,name=goos,proto3\" json:\"goos,omitempty\"`\n\tGoarch string `protobuf:\"bytes,3,opt,name=goarch,proto3\" json:\"goarch,omitempty\"`\n\t// cgo_enabled specifies whether to build with cgo enabled.\n\t// The host must have a valid C compiler for the target platform\n\t// if true.\n\tCgoEnabled bool `protobuf:\"varint,4,opt,name=cgo_enabled,json=cgoEnabled,proto3\" json:\"cgo_enabled,omitempty\"`\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,5,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// Types that are valid to be assigned to Format:\n\t//\n\t//\t*ExportRequest_Docker\n\tFormat        isExportRequest_Format `protobuf_oneof:\"format\"`\n\tInfraConfPath string                 `protobuf:\"bytes,7,opt,name=infra_conf_path,json=infraConfPath,proto3\" json:\"infra_conf_path,omitempty\"`\n\tServices      []string               `protobuf:\"bytes,8,rep,name=services,proto3\" json:\"services,omitempty\"`\n\tGateways      []string               `protobuf:\"bytes,9,rep,name=gateways,proto3\" json:\"gateways,omitempty\"`\n\tSkipInfraConf bool                   `protobuf:\"varint,10,opt,name=skip_infra_conf,json=skipInfraConf,proto3\" json:\"skip_infra_conf,omitempty\"`\n\t// A parent path to app_root containing the .git, or the same as app_root\n\tWorkspaceRoot string `protobuf:\"bytes,11,opt,name=workspace_root,json=workspaceRoot,proto3\" json:\"workspace_root,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ExportRequest) Reset() {\n\t*x = ExportRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ExportRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ExportRequest) ProtoMessage() {}\n\nfunc (x *ExportRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ExportRequest.ProtoReflect.Descriptor instead.\nfunc (*ExportRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *ExportRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExportRequest) GetGoos() string {\n\tif x != nil {\n\t\treturn x.Goos\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExportRequest) GetGoarch() string {\n\tif x != nil {\n\t\treturn x.Goarch\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExportRequest) GetCgoEnabled() bool {\n\tif x != nil {\n\t\treturn x.CgoEnabled\n\t}\n\treturn false\n}\n\nfunc (x *ExportRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *ExportRequest) GetFormat() isExportRequest_Format {\n\tif x != nil {\n\t\treturn x.Format\n\t}\n\treturn nil\n}\n\nfunc (x *ExportRequest) GetDocker() *DockerExportParams {\n\tif x != nil {\n\t\tif x, ok := x.Format.(*ExportRequest_Docker); ok {\n\t\t\treturn x.Docker\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ExportRequest) GetInfraConfPath() string {\n\tif x != nil {\n\t\treturn x.InfraConfPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *ExportRequest) GetServices() []string {\n\tif x != nil {\n\t\treturn x.Services\n\t}\n\treturn nil\n}\n\nfunc (x *ExportRequest) GetGateways() []string {\n\tif x != nil {\n\t\treturn x.Gateways\n\t}\n\treturn nil\n}\n\nfunc (x *ExportRequest) GetSkipInfraConf() bool {\n\tif x != nil {\n\t\treturn x.SkipInfraConf\n\t}\n\treturn false\n}\n\nfunc (x *ExportRequest) GetWorkspaceRoot() string {\n\tif x != nil {\n\t\treturn x.WorkspaceRoot\n\t}\n\treturn \"\"\n}\n\ntype isExportRequest_Format interface {\n\tisExportRequest_Format()\n}\n\ntype ExportRequest_Docker struct {\n\t// docker specifies to export the app as a docker image.\n\tDocker *DockerExportParams `protobuf:\"bytes,6,opt,name=docker,proto3,oneof\"`\n}\n\nfunc (*ExportRequest_Docker) isExportRequest_Format() {}\n\ntype DockerExportParams struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// local_daemon_tag specifies what to tag the image as\n\t// in the local Docker daemon. If empty the export does not\n\t// interact with (or require) the local docker daemon at all.\n\tLocalDaemonTag string `protobuf:\"bytes,1,opt,name=local_daemon_tag,json=localDaemonTag,proto3\" json:\"local_daemon_tag,omitempty\"`\n\t// push_destination_tag specifies the remote registry tag\n\t// to push the exported image to. If empty the built image\n\t// is not pushed anywhere.\n\tPushDestinationTag string `protobuf:\"bytes,2,opt,name=push_destination_tag,json=pushDestinationTag,proto3\" json:\"push_destination_tag,omitempty\"`\n\t// base_image_tag is the base image to build the image from.\n\tBaseImageTag  string `protobuf:\"bytes,3,opt,name=base_image_tag,json=baseImageTag,proto3\" json:\"base_image_tag,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DockerExportParams) Reset() {\n\t*x = DockerExportParams{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DockerExportParams) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DockerExportParams) ProtoMessage() {}\n\nfunc (x *DockerExportParams) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DockerExportParams.ProtoReflect.Descriptor instead.\nfunc (*DockerExportParams) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *DockerExportParams) GetLocalDaemonTag() string {\n\tif x != nil {\n\t\treturn x.LocalDaemonTag\n\t}\n\treturn \"\"\n}\n\nfunc (x *DockerExportParams) GetPushDestinationTag() string {\n\tif x != nil {\n\t\treturn x.PushDestinationTag\n\t}\n\treturn \"\"\n}\n\nfunc (x *DockerExportParams) GetBaseImageTag() string {\n\tif x != nil {\n\t\treturn x.BaseImageTag\n\t}\n\treturn \"\"\n}\n\ntype DBConnectRequest struct {\n\tstate       protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot     string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tDbName      string                 `protobuf:\"bytes,2,opt,name=db_name,json=dbName,proto3\" json:\"db_name,omitempty\"`\n\tEnvName     string                 `protobuf:\"bytes,3,opt,name=env_name,json=envName,proto3\" json:\"env_name,omitempty\"` // optional\n\tClusterType DBClusterType          `protobuf:\"varint,4,opt,name=cluster_type,json=clusterType,proto3,enum=encore.daemon.DBClusterType\" json:\"cluster_type,omitempty\"`\n\t// namespace is the infrastructure namespace to use.\n\t// If empty the active namespace is used.\n\tNamespace     *string `protobuf:\"bytes,5,opt,name=namespace,proto3,oneof\" json:\"namespace,omitempty\"`\n\tRole          DBRole  `protobuf:\"varint,6,opt,name=role,proto3,enum=encore.daemon.DBRole\" json:\"role,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBConnectRequest) Reset() {\n\t*x = DBConnectRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBConnectRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBConnectRequest) ProtoMessage() {}\n\nfunc (x *DBConnectRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBConnectRequest.ProtoReflect.Descriptor instead.\nfunc (*DBConnectRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *DBConnectRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBConnectRequest) GetDbName() string {\n\tif x != nil {\n\t\treturn x.DbName\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBConnectRequest) GetEnvName() string {\n\tif x != nil {\n\t\treturn x.EnvName\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBConnectRequest) GetClusterType() DBClusterType {\n\tif x != nil {\n\t\treturn x.ClusterType\n\t}\n\treturn DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED\n}\n\nfunc (x *DBConnectRequest) GetNamespace() string {\n\tif x != nil && x.Namespace != nil {\n\t\treturn *x.Namespace\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBConnectRequest) GetRole() DBRole {\n\tif x != nil {\n\t\treturn x.Role\n\t}\n\treturn DBRole_DB_ROLE_UNSPECIFIED\n}\n\ntype DBConnectResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tDsn           string                 `protobuf:\"bytes,1,opt,name=dsn,proto3\" json:\"dsn,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBConnectResponse) Reset() {\n\t*x = DBConnectResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBConnectResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBConnectResponse) ProtoMessage() {}\n\nfunc (x *DBConnectResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBConnectResponse.ProtoReflect.Descriptor instead.\nfunc (*DBConnectResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *DBConnectResponse) GetDsn() string {\n\tif x != nil {\n\t\treturn x.Dsn\n\t}\n\treturn \"\"\n}\n\ntype DBProxyRequest struct {\n\tstate       protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot     string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tEnvName     string                 `protobuf:\"bytes,2,opt,name=env_name,json=envName,proto3\" json:\"env_name,omitempty\"` // optional\n\tPort        int32                  `protobuf:\"varint,3,opt,name=port,proto3\" json:\"port,omitempty\"`                     // optional\n\tClusterType DBClusterType          `protobuf:\"varint,4,opt,name=cluster_type,json=clusterType,proto3,enum=encore.daemon.DBClusterType\" json:\"cluster_type,omitempty\"`\n\t// namespace is the infrastructure namespace to use.\n\t// If empty the active namespace is used.\n\tNamespace     *string `protobuf:\"bytes,5,opt,name=namespace,proto3,oneof\" json:\"namespace,omitempty\"`\n\tRole          DBRole  `protobuf:\"varint,6,opt,name=role,proto3,enum=encore.daemon.DBRole\" json:\"role,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBProxyRequest) Reset() {\n\t*x = DBProxyRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBProxyRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBProxyRequest) ProtoMessage() {}\n\nfunc (x *DBProxyRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBProxyRequest.ProtoReflect.Descriptor instead.\nfunc (*DBProxyRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *DBProxyRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBProxyRequest) GetEnvName() string {\n\tif x != nil {\n\t\treturn x.EnvName\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBProxyRequest) GetPort() int32 {\n\tif x != nil {\n\t\treturn x.Port\n\t}\n\treturn 0\n}\n\nfunc (x *DBProxyRequest) GetClusterType() DBClusterType {\n\tif x != nil {\n\t\treturn x.ClusterType\n\t}\n\treturn DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED\n}\n\nfunc (x *DBProxyRequest) GetNamespace() string {\n\tif x != nil && x.Namespace != nil {\n\t\treturn *x.Namespace\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBProxyRequest) GetRole() DBRole {\n\tif x != nil {\n\t\treturn x.Role\n\t}\n\treturn DBRole_DB_ROLE_UNSPECIFIED\n}\n\ntype DBResetRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tDatabaseNames []string               `protobuf:\"bytes,2,rep,name=database_names,json=databaseNames,proto3\" json:\"database_names,omitempty\"` // database names to reset\n\tClusterType   DBClusterType          `protobuf:\"varint,3,opt,name=cluster_type,json=clusterType,proto3,enum=encore.daemon.DBClusterType\" json:\"cluster_type,omitempty\"`\n\t// namespace is the infrastructure namespace to use.\n\t// If empty the active namespace is used.\n\tNamespace     *string `protobuf:\"bytes,4,opt,name=namespace,proto3,oneof\" json:\"namespace,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBResetRequest) Reset() {\n\t*x = DBResetRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBResetRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBResetRequest) ProtoMessage() {}\n\nfunc (x *DBResetRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBResetRequest.ProtoReflect.Descriptor instead.\nfunc (*DBResetRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *DBResetRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBResetRequest) GetDatabaseNames() []string {\n\tif x != nil {\n\t\treturn x.DatabaseNames\n\t}\n\treturn nil\n}\n\nfunc (x *DBResetRequest) GetClusterType() DBClusterType {\n\tif x != nil {\n\t\treturn x.ClusterType\n\t}\n\treturn DBClusterType_DB_CLUSTER_TYPE_UNSPECIFIED\n}\n\nfunc (x *DBResetRequest) GetNamespace() string {\n\tif x != nil && x.Namespace != nil {\n\t\treturn *x.Namespace\n\t}\n\treturn \"\"\n}\n\ntype GenClientRequest struct {\n\tstate    protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppId    string                 `protobuf:\"bytes,1,opt,name=app_id,json=appId,proto3\" json:\"app_id,omitempty\"`\n\tEnvName  string                 `protobuf:\"bytes,2,opt,name=env_name,json=envName,proto3\" json:\"env_name,omitempty\"`\n\tLang     string                 `protobuf:\"bytes,3,opt,name=lang,proto3\" json:\"lang,omitempty\"`\n\tFilepath string                 `protobuf:\"bytes,4,opt,name=filepath,proto3\" json:\"filepath,omitempty\"`\n\t// Services to include in the output.\n\t// If the string \"*\" is present all services are included.\n\tServices []string `protobuf:\"bytes,5,rep,name=services,proto3\" json:\"services,omitempty\"`\n\t// Services to exclude from the output.\n\t// Takes precedence over 'services' above.\n\tExcludedServices []string `protobuf:\"bytes,6,rep,name=excluded_services,json=excludedServices,proto3\" json:\"excluded_services,omitempty\"`\n\t// Tags of endpoints to include in the output.\n\t// Only includes endpoints from services included in 'services' above.\n\tEndpointTags []string `protobuf:\"bytes,7,rep,name=endpoint_tags,json=endpointTags,proto3\" json:\"endpoint_tags,omitempty\"`\n\t// Tags of endpoints to exclude from the output.\n\t// Takes precedence over 'endpoint_tags' above.\n\tExcludedEndpointTags []string `protobuf:\"bytes,8,rep,name=excluded_endpoint_tags,json=excludedEndpointTags,proto3\" json:\"excluded_endpoint_tags,omitempty\"`\n\t// The OpenAPI spec generator by default includes private endpoints.\n\t// If this is set to `true`, private endpoints will not be included\n\t// in the generated OpenAPI spec.\n\tOpenapiExcludePrivateEndpoints *bool `protobuf:\"varint,9,opt,name=openapi_exclude_private_endpoints,json=openapiExcludePrivateEndpoints,proto3,oneof\" json:\"openapi_exclude_private_endpoints,omitempty\"`\n\t// The TS generator by default re-declares the api types in the client.\n\t// If this is set to `true`, the types will be imported and shared between\n\t// the client and the server. It assumes \"~backend\" is available in the\n\t// import path.\n\tTsSharedTypes *bool `protobuf:\"varint,10,opt,name=ts_shared_types,json=tsSharedTypes,proto3,oneof\" json:\"ts_shared_types,omitempty\"`\n\t// If set, the default export of the generate TypeScript client will be\n\t// an instantiated client with the given target. The target can be e.g.\n\t// a variable, e.g. \"import.meta.env.VITE_CLIENT_TARGET\" or a string literal.\n\tTsClientTarget *string `protobuf:\"bytes,11,opt,name=ts_client_target,json=tsClientTarget,proto3,oneof\" json:\"ts_client_target,omitempty\"`\n\t// The root directory of the app to generate a client for.\n\t// Included to be able to handle multi clone scenarios.\n\tAppRoot       string `protobuf:\"bytes,12,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GenClientRequest) Reset() {\n\t*x = GenClientRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GenClientRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GenClientRequest) ProtoMessage() {}\n\nfunc (x *GenClientRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GenClientRequest.ProtoReflect.Descriptor instead.\nfunc (*GenClientRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *GenClientRequest) GetAppId() string {\n\tif x != nil {\n\t\treturn x.AppId\n\t}\n\treturn \"\"\n}\n\nfunc (x *GenClientRequest) GetEnvName() string {\n\tif x != nil {\n\t\treturn x.EnvName\n\t}\n\treturn \"\"\n}\n\nfunc (x *GenClientRequest) GetLang() string {\n\tif x != nil {\n\t\treturn x.Lang\n\t}\n\treturn \"\"\n}\n\nfunc (x *GenClientRequest) GetFilepath() string {\n\tif x != nil {\n\t\treturn x.Filepath\n\t}\n\treturn \"\"\n}\n\nfunc (x *GenClientRequest) GetServices() []string {\n\tif x != nil {\n\t\treturn x.Services\n\t}\n\treturn nil\n}\n\nfunc (x *GenClientRequest) GetExcludedServices() []string {\n\tif x != nil {\n\t\treturn x.ExcludedServices\n\t}\n\treturn nil\n}\n\nfunc (x *GenClientRequest) GetEndpointTags() []string {\n\tif x != nil {\n\t\treturn x.EndpointTags\n\t}\n\treturn nil\n}\n\nfunc (x *GenClientRequest) GetExcludedEndpointTags() []string {\n\tif x != nil {\n\t\treturn x.ExcludedEndpointTags\n\t}\n\treturn nil\n}\n\nfunc (x *GenClientRequest) GetOpenapiExcludePrivateEndpoints() bool {\n\tif x != nil && x.OpenapiExcludePrivateEndpoints != nil {\n\t\treturn *x.OpenapiExcludePrivateEndpoints\n\t}\n\treturn false\n}\n\nfunc (x *GenClientRequest) GetTsSharedTypes() bool {\n\tif x != nil && x.TsSharedTypes != nil {\n\t\treturn *x.TsSharedTypes\n\t}\n\treturn false\n}\n\nfunc (x *GenClientRequest) GetTsClientTarget() string {\n\tif x != nil && x.TsClientTarget != nil {\n\t\treturn *x.TsClientTarget\n\t}\n\treturn \"\"\n}\n\nfunc (x *GenClientRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\ntype GenClientResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCode          []byte                 `protobuf:\"bytes,1,opt,name=code,proto3\" json:\"code,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GenClientResponse) Reset() {\n\t*x = GenClientResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GenClientResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GenClientResponse) ProtoMessage() {}\n\nfunc (x *GenClientResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GenClientResponse.ProtoReflect.Descriptor instead.\nfunc (*GenClientResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *GenClientResponse) GetCode() []byte {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn nil\n}\n\ntype GenWrappersRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GenWrappersRequest) Reset() {\n\t*x = GenWrappersRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GenWrappersRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GenWrappersRequest) ProtoMessage() {}\n\nfunc (x *GenWrappersRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GenWrappersRequest.ProtoReflect.Descriptor instead.\nfunc (*GenWrappersRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *GenWrappersRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\ntype GenWrappersResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GenWrappersResponse) Reset() {\n\t*x = GenWrappersResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GenWrappersResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GenWrappersResponse) ProtoMessage() {}\n\nfunc (x *GenWrappersResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GenWrappersResponse.ProtoReflect.Descriptor instead.\nfunc (*GenWrappersResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{24}\n}\n\ntype SecretsRefreshRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tKey           string                 `protobuf:\"bytes,2,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,3,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SecretsRefreshRequest) Reset() {\n\t*x = SecretsRefreshRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SecretsRefreshRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SecretsRefreshRequest) ProtoMessage() {}\n\nfunc (x *SecretsRefreshRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SecretsRefreshRequest.ProtoReflect.Descriptor instead.\nfunc (*SecretsRefreshRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *SecretsRefreshRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *SecretsRefreshRequest) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *SecretsRefreshRequest) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype SecretsRefreshResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SecretsRefreshResponse) Reset() {\n\t*x = SecretsRefreshResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SecretsRefreshResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SecretsRefreshResponse) ProtoMessage() {}\n\nfunc (x *SecretsRefreshResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SecretsRefreshResponse.ProtoReflect.Descriptor instead.\nfunc (*SecretsRefreshResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{26}\n}\n\ntype VersionResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tVersion       string                 `protobuf:\"bytes,1,opt,name=version,proto3\" json:\"version,omitempty\"`\n\tConfigHash    string                 `protobuf:\"bytes,2,opt,name=config_hash,json=configHash,proto3\" json:\"config_hash,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *VersionResponse) Reset() {\n\t*x = VersionResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *VersionResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*VersionResponse) ProtoMessage() {}\n\nfunc (x *VersionResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use VersionResponse.ProtoReflect.Descriptor instead.\nfunc (*VersionResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *VersionResponse) GetVersion() string {\n\tif x != nil {\n\t\treturn x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *VersionResponse) GetConfigHash() string {\n\tif x != nil {\n\t\treturn x.ConfigHash\n\t}\n\treturn \"\"\n}\n\ntype Namespace struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tActive        bool                   `protobuf:\"varint,3,opt,name=active,proto3\" json:\"active,omitempty\"`\n\tCreatedAt     string                 `protobuf:\"bytes,4,opt,name=created_at,json=createdAt,proto3\" json:\"created_at,omitempty\"`\n\tLastActiveAt  *string                `protobuf:\"bytes,5,opt,name=last_active_at,json=lastActiveAt,proto3,oneof\" json:\"last_active_at,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Namespace) Reset() {\n\t*x = Namespace{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Namespace) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Namespace) ProtoMessage() {}\n\nfunc (x *Namespace) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Namespace.ProtoReflect.Descriptor instead.\nfunc (*Namespace) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{28}\n}\n\nfunc (x *Namespace) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *Namespace) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Namespace) GetActive() bool {\n\tif x != nil {\n\t\treturn x.Active\n\t}\n\treturn false\n}\n\nfunc (x *Namespace) GetCreatedAt() string {\n\tif x != nil {\n\t\treturn x.CreatedAt\n\t}\n\treturn \"\"\n}\n\nfunc (x *Namespace) GetLastActiveAt() string {\n\tif x != nil && x.LastActiveAt != nil {\n\t\treturn *x.LastActiveAt\n\t}\n\treturn \"\"\n}\n\ntype CreateNamespaceRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateNamespaceRequest) Reset() {\n\t*x = CreateNamespaceRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateNamespaceRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateNamespaceRequest) ProtoMessage() {}\n\nfunc (x *CreateNamespaceRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateNamespaceRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateNamespaceRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{29}\n}\n\nfunc (x *CreateNamespaceRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *CreateNamespaceRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype SwitchNamespaceRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tCreate        bool                   `protobuf:\"varint,3,opt,name=create,proto3\" json:\"create,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SwitchNamespaceRequest) Reset() {\n\t*x = SwitchNamespaceRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SwitchNamespaceRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SwitchNamespaceRequest) ProtoMessage() {}\n\nfunc (x *SwitchNamespaceRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SwitchNamespaceRequest.ProtoReflect.Descriptor instead.\nfunc (*SwitchNamespaceRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{30}\n}\n\nfunc (x *SwitchNamespaceRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *SwitchNamespaceRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SwitchNamespaceRequest) GetCreate() bool {\n\tif x != nil {\n\t\treturn x.Create\n\t}\n\treturn false\n}\n\ntype ListNamespacesRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListNamespacesRequest) Reset() {\n\t*x = ListNamespacesRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[31]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListNamespacesRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListNamespacesRequest) ProtoMessage() {}\n\nfunc (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[31]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListNamespacesRequest.ProtoReflect.Descriptor instead.\nfunc (*ListNamespacesRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{31}\n}\n\nfunc (x *ListNamespacesRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\ntype DeleteNamespaceRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot       string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteNamespaceRequest) Reset() {\n\t*x = DeleteNamespaceRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[32]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteNamespaceRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteNamespaceRequest) ProtoMessage() {}\n\nfunc (x *DeleteNamespaceRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[32]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteNamespaceRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteNamespaceRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{32}\n}\n\nfunc (x *DeleteNamespaceRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *DeleteNamespaceRequest) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype ListNamespacesResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNamespaces    []*Namespace           `protobuf:\"bytes,1,rep,name=namespaces,proto3\" json:\"namespaces,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListNamespacesResponse) Reset() {\n\t*x = ListNamespacesResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[33]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListNamespacesResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListNamespacesResponse) ProtoMessage() {}\n\nfunc (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[33]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListNamespacesResponse.ProtoReflect.Descriptor instead.\nfunc (*ListNamespacesResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{33}\n}\n\nfunc (x *ListNamespacesResponse) GetNamespaces() []*Namespace {\n\tif x != nil {\n\t\treturn x.Namespaces\n\t}\n\treturn nil\n}\n\ntype TelemetryConfig struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAnonId        string                 `protobuf:\"bytes,1,opt,name=anon_id,json=anonId,proto3\" json:\"anon_id,omitempty\"`\n\tEnabled       bool                   `protobuf:\"varint,2,opt,name=enabled,proto3\" json:\"enabled,omitempty\"`\n\tDebug         bool                   `protobuf:\"varint,3,opt,name=debug,proto3\" json:\"debug,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TelemetryConfig) Reset() {\n\t*x = TelemetryConfig{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[34]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TelemetryConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TelemetryConfig) ProtoMessage() {}\n\nfunc (x *TelemetryConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[34]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TelemetryConfig.ProtoReflect.Descriptor instead.\nfunc (*TelemetryConfig) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{34}\n}\n\nfunc (x *TelemetryConfig) GetAnonId() string {\n\tif x != nil {\n\t\treturn x.AnonId\n\t}\n\treturn \"\"\n}\n\nfunc (x *TelemetryConfig) GetEnabled() bool {\n\tif x != nil {\n\t\treturn x.Enabled\n\t}\n\treturn false\n}\n\nfunc (x *TelemetryConfig) GetDebug() bool {\n\tif x != nil {\n\t\treturn x.Debug\n\t}\n\treturn false\n}\n\ntype DumpMetaRequest struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppRoot    string                 `protobuf:\"bytes,1,opt,name=app_root,json=appRoot,proto3\" json:\"app_root,omitempty\"`\n\tWorkingDir string                 `protobuf:\"bytes,2,opt,name=working_dir,json=workingDir,proto3\" json:\"working_dir,omitempty\"` // for error reporting\n\t// environ is the environment to set for the running command.\n\t// Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n\tEnviron []string `protobuf:\"bytes,3,rep,name=environ,proto3\" json:\"environ,omitempty\"`\n\t// Whether or not to parse tests.\n\tParseTests    bool                   `protobuf:\"varint,4,opt,name=parse_tests,json=parseTests,proto3\" json:\"parse_tests,omitempty\"`\n\tFormat        DumpMetaRequest_Format `protobuf:\"varint,5,opt,name=format,proto3,enum=encore.daemon.DumpMetaRequest_Format\" json:\"format,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DumpMetaRequest) Reset() {\n\t*x = DumpMetaRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[35]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DumpMetaRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DumpMetaRequest) ProtoMessage() {}\n\nfunc (x *DumpMetaRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[35]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DumpMetaRequest.ProtoReflect.Descriptor instead.\nfunc (*DumpMetaRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{35}\n}\n\nfunc (x *DumpMetaRequest) GetAppRoot() string {\n\tif x != nil {\n\t\treturn x.AppRoot\n\t}\n\treturn \"\"\n}\n\nfunc (x *DumpMetaRequest) GetWorkingDir() string {\n\tif x != nil {\n\t\treturn x.WorkingDir\n\t}\n\treturn \"\"\n}\n\nfunc (x *DumpMetaRequest) GetEnviron() []string {\n\tif x != nil {\n\t\treturn x.Environ\n\t}\n\treturn nil\n}\n\nfunc (x *DumpMetaRequest) GetParseTests() bool {\n\tif x != nil {\n\t\treturn x.ParseTests\n\t}\n\treturn false\n}\n\nfunc (x *DumpMetaRequest) GetFormat() DumpMetaRequest_Format {\n\tif x != nil {\n\t\treturn x.Format\n\t}\n\treturn DumpMetaRequest_FORMAT_UNSPECIFIED\n}\n\ntype DumpMetaResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMeta          []byte                 `protobuf:\"bytes,1,opt,name=meta,proto3\" json:\"meta,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DumpMetaResponse) Reset() {\n\t*x = DumpMetaResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[36]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DumpMetaResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DumpMetaResponse) ProtoMessage() {}\n\nfunc (x *DumpMetaResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[36]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DumpMetaResponse.ProtoReflect.Descriptor instead.\nfunc (*DumpMetaResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{36}\n}\n\nfunc (x *DumpMetaResponse) GetMeta() []byte {\n\tif x != nil {\n\t\treturn x.Meta\n\t}\n\treturn nil\n}\n\n// The following messages are used for sqlc plugin integration.\ntype SQLCPlugin struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin) Reset() {\n\t*x = SQLCPlugin{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[37]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin) ProtoMessage() {}\n\nfunc (x *SQLCPlugin) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[37]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37}\n}\n\ntype SQLCPlugin_File struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tContents      []byte                 `protobuf:\"bytes,2,opt,name=contents,proto3\" json:\"contents,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_File) Reset() {\n\t*x = SQLCPlugin_File{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[38]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_File) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_File) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_File) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[38]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_File.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_File) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 0}\n}\n\nfunc (x *SQLCPlugin_File) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_File) GetContents() []byte {\n\tif x != nil {\n\t\treturn x.Contents\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_Settings struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tVersion       string                 `protobuf:\"bytes,1,opt,name=version,proto3\" json:\"version,omitempty\"`\n\tEngine        string                 `protobuf:\"bytes,2,opt,name=engine,proto3\" json:\"engine,omitempty\"`\n\tSchema        []string               `protobuf:\"bytes,3,rep,name=schema,proto3\" json:\"schema,omitempty\"`\n\tQueries       []string               `protobuf:\"bytes,4,rep,name=queries,proto3\" json:\"queries,omitempty\"`\n\tCodegen       *SQLCPlugin_Codegen    `protobuf:\"bytes,12,opt,name=codegen,proto3\" json:\"codegen,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Settings) Reset() {\n\t*x = SQLCPlugin_Settings{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[39]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Settings) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Settings) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Settings) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[39]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Settings.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Settings) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 1}\n}\n\nfunc (x *SQLCPlugin_Settings) GetVersion() string {\n\tif x != nil {\n\t\treturn x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Settings) GetEngine() string {\n\tif x != nil {\n\t\treturn x.Engine\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Settings) GetSchema() []string {\n\tif x != nil {\n\t\treturn x.Schema\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Settings) GetQueries() []string {\n\tif x != nil {\n\t\treturn x.Queries\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Settings) GetCodegen() *SQLCPlugin_Codegen {\n\tif x != nil {\n\t\treturn x.Codegen\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_Codegen struct {\n\tstate         protoimpl.MessageState      `protogen:\"open.v1\"`\n\tOut           string                      `protobuf:\"bytes,1,opt,name=out,proto3\" json:\"out,omitempty\"`\n\tPlugin        string                      `protobuf:\"bytes,2,opt,name=plugin,proto3\" json:\"plugin,omitempty\"`\n\tOptions       []byte                      `protobuf:\"bytes,3,opt,name=options,proto3\" json:\"options,omitempty\"`\n\tEnv           []string                    `protobuf:\"bytes,4,rep,name=env,proto3\" json:\"env,omitempty\"`\n\tProcess       *SQLCPlugin_Codegen_Process `protobuf:\"bytes,5,opt,name=process,proto3\" json:\"process,omitempty\"`\n\tWasm          *SQLCPlugin_Codegen_WASM    `protobuf:\"bytes,6,opt,name=wasm,proto3\" json:\"wasm,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Codegen) Reset() {\n\t*x = SQLCPlugin_Codegen{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[40]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Codegen) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Codegen) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Codegen) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[40]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Codegen.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Codegen) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 2}\n}\n\nfunc (x *SQLCPlugin_Codegen) GetOut() string {\n\tif x != nil {\n\t\treturn x.Out\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Codegen) GetPlugin() string {\n\tif x != nil {\n\t\treturn x.Plugin\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Codegen) GetOptions() []byte {\n\tif x != nil {\n\t\treturn x.Options\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Codegen) GetEnv() []string {\n\tif x != nil {\n\t\treturn x.Env\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Codegen) GetProcess() *SQLCPlugin_Codegen_Process {\n\tif x != nil {\n\t\treturn x.Process\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Codegen) GetWasm() *SQLCPlugin_Codegen_WASM {\n\tif x != nil {\n\t\treturn x.Wasm\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_Catalog struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tComment       string                 `protobuf:\"bytes,1,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tDefaultSchema string                 `protobuf:\"bytes,2,opt,name=default_schema,json=defaultSchema,proto3\" json:\"default_schema,omitempty\"`\n\tName          string                 `protobuf:\"bytes,3,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tSchemas       []*SQLCPlugin_Schema   `protobuf:\"bytes,4,rep,name=schemas,proto3\" json:\"schemas,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Catalog) Reset() {\n\t*x = SQLCPlugin_Catalog{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[41]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Catalog) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Catalog) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Catalog) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[41]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Catalog.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Catalog) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 3}\n}\n\nfunc (x *SQLCPlugin_Catalog) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Catalog) GetDefaultSchema() string {\n\tif x != nil {\n\t\treturn x.DefaultSchema\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Catalog) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Catalog) GetSchemas() []*SQLCPlugin_Schema {\n\tif x != nil {\n\t\treturn x.Schemas\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_Schema struct {\n\tstate          protoimpl.MessageState      `protogen:\"open.v1\"`\n\tComment        string                      `protobuf:\"bytes,1,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tName           string                      `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tTables         []*SQLCPlugin_Table         `protobuf:\"bytes,3,rep,name=tables,proto3\" json:\"tables,omitempty\"`\n\tEnums          []*SQLCPlugin_Enum          `protobuf:\"bytes,4,rep,name=enums,proto3\" json:\"enums,omitempty\"`\n\tCompositeTypes []*SQLCPlugin_CompositeType `protobuf:\"bytes,5,rep,name=composite_types,json=compositeTypes,proto3\" json:\"composite_types,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Schema) Reset() {\n\t*x = SQLCPlugin_Schema{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[42]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Schema) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Schema) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Schema) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[42]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Schema.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Schema) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 4}\n}\n\nfunc (x *SQLCPlugin_Schema) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Schema) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Schema) GetTables() []*SQLCPlugin_Table {\n\tif x != nil {\n\t\treturn x.Tables\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Schema) GetEnums() []*SQLCPlugin_Enum {\n\tif x != nil {\n\t\treturn x.Enums\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Schema) GetCompositeTypes() []*SQLCPlugin_CompositeType {\n\tif x != nil {\n\t\treturn x.CompositeTypes\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_CompositeType struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,2,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_CompositeType) Reset() {\n\t*x = SQLCPlugin_CompositeType{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[43]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_CompositeType) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_CompositeType) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_CompositeType) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[43]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_CompositeType.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_CompositeType) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 5}\n}\n\nfunc (x *SQLCPlugin_CompositeType) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_CompositeType) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\ntype SQLCPlugin_Enum struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tVals          []string               `protobuf:\"bytes,2,rep,name=vals,proto3\" json:\"vals,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,3,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Enum) Reset() {\n\t*x = SQLCPlugin_Enum{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[44]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Enum) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Enum) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Enum) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[44]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Enum.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Enum) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 6}\n}\n\nfunc (x *SQLCPlugin_Enum) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Enum) GetVals() []string {\n\tif x != nil {\n\t\treturn x.Vals\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Enum) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\ntype SQLCPlugin_Table struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRel           *SQLCPlugin_Identifier `protobuf:\"bytes,1,opt,name=rel,proto3\" json:\"rel,omitempty\"`\n\tColumns       []*SQLCPlugin_Column   `protobuf:\"bytes,2,rep,name=columns,proto3\" json:\"columns,omitempty\"`\n\tComment       string                 `protobuf:\"bytes,3,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Table) Reset() {\n\t*x = SQLCPlugin_Table{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[45]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Table) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Table) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Table) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[45]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Table.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Table) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 7}\n}\n\nfunc (x *SQLCPlugin_Table) GetRel() *SQLCPlugin_Identifier {\n\tif x != nil {\n\t\treturn x.Rel\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Table) GetColumns() []*SQLCPlugin_Column {\n\tif x != nil {\n\t\treturn x.Columns\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Table) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\ntype SQLCPlugin_Identifier struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCatalog       string                 `protobuf:\"bytes,1,opt,name=catalog,proto3\" json:\"catalog,omitempty\"`\n\tSchema        string                 `protobuf:\"bytes,2,opt,name=schema,proto3\" json:\"schema,omitempty\"`\n\tName          string                 `protobuf:\"bytes,3,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Identifier) Reset() {\n\t*x = SQLCPlugin_Identifier{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[46]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Identifier) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Identifier) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Identifier) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[46]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Identifier.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Identifier) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 8}\n}\n\nfunc (x *SQLCPlugin_Identifier) GetCatalog() string {\n\tif x != nil {\n\t\treturn x.Catalog\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Identifier) GetSchema() string {\n\tif x != nil {\n\t\treturn x.Schema\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Identifier) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype SQLCPlugin_Column struct {\n\tstate        protoimpl.MessageState `protogen:\"open.v1\"`\n\tName         string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tNotNull      bool                   `protobuf:\"varint,3,opt,name=not_null,json=notNull,proto3\" json:\"not_null,omitempty\"`\n\tIsArray      bool                   `protobuf:\"varint,4,opt,name=is_array,json=isArray,proto3\" json:\"is_array,omitempty\"`\n\tComment      string                 `protobuf:\"bytes,5,opt,name=comment,proto3\" json:\"comment,omitempty\"`\n\tLength       int32                  `protobuf:\"varint,6,opt,name=length,proto3\" json:\"length,omitempty\"`\n\tIsNamedParam bool                   `protobuf:\"varint,7,opt,name=is_named_param,json=isNamedParam,proto3\" json:\"is_named_param,omitempty\"`\n\tIsFuncCall   bool                   `protobuf:\"varint,8,opt,name=is_func_call,json=isFuncCall,proto3\" json:\"is_func_call,omitempty\"`\n\t// XXX: Figure out what PostgreSQL calls `foo.id`\n\tScope         string                 `protobuf:\"bytes,9,opt,name=scope,proto3\" json:\"scope,omitempty\"`\n\tTable         *SQLCPlugin_Identifier `protobuf:\"bytes,10,opt,name=table,proto3\" json:\"table,omitempty\"`\n\tTableAlias    string                 `protobuf:\"bytes,11,opt,name=table_alias,json=tableAlias,proto3\" json:\"table_alias,omitempty\"`\n\tType          *SQLCPlugin_Identifier `protobuf:\"bytes,12,opt,name=type,proto3\" json:\"type,omitempty\"`\n\tIsSqlcSlice   bool                   `protobuf:\"varint,13,opt,name=is_sqlc_slice,json=isSqlcSlice,proto3\" json:\"is_sqlc_slice,omitempty\"`\n\tEmbedTable    *SQLCPlugin_Identifier `protobuf:\"bytes,14,opt,name=embed_table,json=embedTable,proto3\" json:\"embed_table,omitempty\"`\n\tOriginalName  string                 `protobuf:\"bytes,15,opt,name=original_name,json=originalName,proto3\" json:\"original_name,omitempty\"`\n\tUnsigned      bool                   `protobuf:\"varint,16,opt,name=unsigned,proto3\" json:\"unsigned,omitempty\"`\n\tArrayDims     int32                  `protobuf:\"varint,17,opt,name=array_dims,json=arrayDims,proto3\" json:\"array_dims,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Column) Reset() {\n\t*x = SQLCPlugin_Column{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[47]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Column) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Column) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Column) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[47]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Column.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Column) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 9}\n}\n\nfunc (x *SQLCPlugin_Column) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Column) GetNotNull() bool {\n\tif x != nil {\n\t\treturn x.NotNull\n\t}\n\treturn false\n}\n\nfunc (x *SQLCPlugin_Column) GetIsArray() bool {\n\tif x != nil {\n\t\treturn x.IsArray\n\t}\n\treturn false\n}\n\nfunc (x *SQLCPlugin_Column) GetComment() string {\n\tif x != nil {\n\t\treturn x.Comment\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Column) GetLength() int32 {\n\tif x != nil {\n\t\treturn x.Length\n\t}\n\treturn 0\n}\n\nfunc (x *SQLCPlugin_Column) GetIsNamedParam() bool {\n\tif x != nil {\n\t\treturn x.IsNamedParam\n\t}\n\treturn false\n}\n\nfunc (x *SQLCPlugin_Column) GetIsFuncCall() bool {\n\tif x != nil {\n\t\treturn x.IsFuncCall\n\t}\n\treturn false\n}\n\nfunc (x *SQLCPlugin_Column) GetScope() string {\n\tif x != nil {\n\t\treturn x.Scope\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Column) GetTable() *SQLCPlugin_Identifier {\n\tif x != nil {\n\t\treturn x.Table\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Column) GetTableAlias() string {\n\tif x != nil {\n\t\treturn x.TableAlias\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Column) GetType() *SQLCPlugin_Identifier {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Column) GetIsSqlcSlice() bool {\n\tif x != nil {\n\t\treturn x.IsSqlcSlice\n\t}\n\treturn false\n}\n\nfunc (x *SQLCPlugin_Column) GetEmbedTable() *SQLCPlugin_Identifier {\n\tif x != nil {\n\t\treturn x.EmbedTable\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Column) GetOriginalName() string {\n\tif x != nil {\n\t\treturn x.OriginalName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Column) GetUnsigned() bool {\n\tif x != nil {\n\t\treturn x.Unsigned\n\t}\n\treturn false\n}\n\nfunc (x *SQLCPlugin_Column) GetArrayDims() int32 {\n\tif x != nil {\n\t\treturn x.ArrayDims\n\t}\n\treturn 0\n}\n\ntype SQLCPlugin_Query struct {\n\tstate           protoimpl.MessageState  `protogen:\"open.v1\"`\n\tText            string                  `protobuf:\"bytes,1,opt,name=text,proto3\" json:\"text,omitempty\"`\n\tName            string                  `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tCmd             string                  `protobuf:\"bytes,3,opt,name=cmd,proto3\" json:\"cmd,omitempty\"`\n\tColumns         []*SQLCPlugin_Column    `protobuf:\"bytes,4,rep,name=columns,proto3\" json:\"columns,omitempty\"`\n\tParams          []*SQLCPlugin_Parameter `protobuf:\"bytes,5,rep,name=params,json=parameters,proto3\" json:\"params,omitempty\"`\n\tComments        []string                `protobuf:\"bytes,6,rep,name=comments,proto3\" json:\"comments,omitempty\"`\n\tFilename        string                  `protobuf:\"bytes,7,opt,name=filename,proto3\" json:\"filename,omitempty\"`\n\tInsertIntoTable *SQLCPlugin_Identifier  `protobuf:\"bytes,8,opt,name=insert_into_table,proto3\" json:\"insert_into_table,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Query) Reset() {\n\t*x = SQLCPlugin_Query{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[48]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Query) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Query) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Query) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[48]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Query.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Query) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 10}\n}\n\nfunc (x *SQLCPlugin_Query) GetText() string {\n\tif x != nil {\n\t\treturn x.Text\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Query) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Query) GetCmd() string {\n\tif x != nil {\n\t\treturn x.Cmd\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Query) GetColumns() []*SQLCPlugin_Column {\n\tif x != nil {\n\t\treturn x.Columns\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Query) GetParams() []*SQLCPlugin_Parameter {\n\tif x != nil {\n\t\treturn x.Params\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Query) GetComments() []string {\n\tif x != nil {\n\t\treturn x.Comments\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_Query) GetFilename() string {\n\tif x != nil {\n\t\treturn x.Filename\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Query) GetInsertIntoTable() *SQLCPlugin_Identifier {\n\tif x != nil {\n\t\treturn x.InsertIntoTable\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_Parameter struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNumber        int32                  `protobuf:\"varint,1,opt,name=number,proto3\" json:\"number,omitempty\"`\n\tColumn        *SQLCPlugin_Column     `protobuf:\"bytes,2,opt,name=column,proto3\" json:\"column,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Parameter) Reset() {\n\t*x = SQLCPlugin_Parameter{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[49]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Parameter) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Parameter) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Parameter) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[49]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Parameter.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Parameter) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 11}\n}\n\nfunc (x *SQLCPlugin_Parameter) GetNumber() int32 {\n\tif x != nil {\n\t\treturn x.Number\n\t}\n\treturn 0\n}\n\nfunc (x *SQLCPlugin_Parameter) GetColumn() *SQLCPlugin_Column {\n\tif x != nil {\n\t\treturn x.Column\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_GenerateRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSettings      *SQLCPlugin_Settings   `protobuf:\"bytes,1,opt,name=settings,proto3\" json:\"settings,omitempty\"`\n\tCatalog       *SQLCPlugin_Catalog    `protobuf:\"bytes,2,opt,name=catalog,proto3\" json:\"catalog,omitempty\"`\n\tQueries       []*SQLCPlugin_Query    `protobuf:\"bytes,3,rep,name=queries,proto3\" json:\"queries,omitempty\"`\n\tSqlcVersion   string                 `protobuf:\"bytes,4,opt,name=sqlc_version,proto3\" json:\"sqlc_version,omitempty\"`\n\tPluginOptions []byte                 `protobuf:\"bytes,5,opt,name=plugin_options,proto3\" json:\"plugin_options,omitempty\"`\n\tGlobalOptions []byte                 `protobuf:\"bytes,6,opt,name=global_options,proto3\" json:\"global_options,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) Reset() {\n\t*x = SQLCPlugin_GenerateRequest{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[50]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_GenerateRequest) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_GenerateRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[50]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_GenerateRequest.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_GenerateRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 12}\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) GetSettings() *SQLCPlugin_Settings {\n\tif x != nil {\n\t\treturn x.Settings\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) GetCatalog() *SQLCPlugin_Catalog {\n\tif x != nil {\n\t\treturn x.Catalog\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) GetQueries() []*SQLCPlugin_Query {\n\tif x != nil {\n\t\treturn x.Queries\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) GetSqlcVersion() string {\n\tif x != nil {\n\t\treturn x.SqlcVersion\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) GetPluginOptions() []byte {\n\tif x != nil {\n\t\treturn x.PluginOptions\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCPlugin_GenerateRequest) GetGlobalOptions() []byte {\n\tif x != nil {\n\t\treturn x.GlobalOptions\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_GenerateResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFiles         []*SQLCPlugin_File     `protobuf:\"bytes,1,rep,name=files,proto3\" json:\"files,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_GenerateResponse) Reset() {\n\t*x = SQLCPlugin_GenerateResponse{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[51]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_GenerateResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_GenerateResponse) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_GenerateResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[51]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_GenerateResponse.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_GenerateResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 13}\n}\n\nfunc (x *SQLCPlugin_GenerateResponse) GetFiles() []*SQLCPlugin_File {\n\tif x != nil {\n\t\treturn x.Files\n\t}\n\treturn nil\n}\n\ntype SQLCPlugin_Codegen_Process struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCmd           string                 `protobuf:\"bytes,1,opt,name=cmd,proto3\" json:\"cmd,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Codegen_Process) Reset() {\n\t*x = SQLCPlugin_Codegen_Process{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[52]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Codegen_Process) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Codegen_Process) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Codegen_Process) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[52]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Codegen_Process.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Codegen_Process) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 2, 0}\n}\n\nfunc (x *SQLCPlugin_Codegen_Process) GetCmd() string {\n\tif x != nil {\n\t\treturn x.Cmd\n\t}\n\treturn \"\"\n}\n\ntype SQLCPlugin_Codegen_WASM struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUrl           string                 `protobuf:\"bytes,1,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tSha256        string                 `protobuf:\"bytes,2,opt,name=sha256,proto3\" json:\"sha256,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCPlugin_Codegen_WASM) Reset() {\n\t*x = SQLCPlugin_Codegen_WASM{}\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[53]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCPlugin_Codegen_WASM) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCPlugin_Codegen_WASM) ProtoMessage() {}\n\nfunc (x *SQLCPlugin_Codegen_WASM) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_daemon_daemon_proto_msgTypes[53]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCPlugin_Codegen_WASM.ProtoReflect.Descriptor instead.\nfunc (*SQLCPlugin_Codegen_WASM) Descriptor() ([]byte, []int) {\n\treturn file_encore_daemon_daemon_proto_rawDescGZIP(), []int{37, 2, 1}\n}\n\nfunc (x *SQLCPlugin_Codegen_WASM) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCPlugin_Codegen_WASM) GetSha256() string {\n\tif x != nil {\n\t\treturn x.Sha256\n\t}\n\treturn \"\"\n}\n\nvar File_encore_daemon_daemon_proto protoreflect.FileDescriptor\n\nconst file_encore_daemon_daemon_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1aencore/daemon/daemon.proto\\x12\\rencore.daemon\\x1a\\x1bgoogle/protobuf/empty.proto\\\"\\xc0\\x01\\n\" +\n\t\"\\x0eCommandMessage\\x126\\n\" +\n\t\"\\x06output\\x18\\x01 \\x01(\\v2\\x1c.encore.daemon.CommandOutputH\\x00R\\x06output\\x120\\n\" +\n\t\"\\x04exit\\x18\\x02 \\x01(\\v2\\x1a.encore.daemon.CommandExitH\\x00R\\x04exit\\x12=\\n\" +\n\t\"\\x06errors\\x18\\x03 \\x01(\\v2#.encore.daemon.CommandDisplayErrorsH\\x00R\\x06errorsB\\x05\\n\" +\n\t\"\\x03msg\\\"?\\n\" +\n\t\"\\rCommandOutput\\x12\\x16\\n\" +\n\t\"\\x06stdout\\x18\\x01 \\x01(\\fR\\x06stdout\\x12\\x16\\n\" +\n\t\"\\x06stderr\\x18\\x02 \\x01(\\fR\\x06stderr\\\"!\\n\" +\n\t\"\\vCommandExit\\x12\\x12\\n\" +\n\t\"\\x04code\\x18\\x01 \\x01(\\x05R\\x04code\\\"2\\n\" +\n\t\"\\x14CommandDisplayErrors\\x12\\x1a\\n\" +\n\t\"\\berrinsrc\\x18\\x01 \\x01(\\fR\\berrinsrc\\\"e\\n\" +\n\t\"\\x10CreateAppRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1a\\n\" +\n\t\"\\btemplate\\x18\\x02 \\x01(\\tR\\btemplate\\x12\\x1a\\n\" +\n\t\"\\btutorial\\x18\\x03 \\x01(\\bR\\btutorial\\\"*\\n\" +\n\t\"\\x11CreateAppResponse\\x12\\x15\\n\" +\n\t\"\\x06app_id\\x18\\x01 \\x01(\\tR\\x05appId\\\"\\xf1\\x04\\n\" +\n\t\"\\n\" +\n\t\"RunRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12\\x14\\n\" +\n\t\"\\x05watch\\x18\\x05 \\x01(\\bR\\x05watch\\x12\\x1f\\n\" +\n\t\"\\vlisten_addr\\x18\\x06 \\x01(\\tR\\n\" +\n\t\"listenAddr\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\a \\x03(\\tR\\aenviron\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"trace_file\\x18\\b \\x01(\\tH\\x00R\\ttraceFile\\x88\\x01\\x01\\x12!\\n\" +\n\t\"\\tnamespace\\x18\\t \\x01(\\tH\\x01R\\tnamespace\\x88\\x01\\x01\\x12?\\n\" +\n\t\"\\abrowser\\x18\\n\" +\n\t\" \\x01(\\x0e2%.encore.daemon.RunRequest.BrowserModeR\\abrowser\\x12B\\n\" +\n\t\"\\n\" +\n\t\"debug_mode\\x18\\v \\x01(\\x0e2#.encore.daemon.RunRequest.DebugModeR\\tdebugMode\\x12 \\n\" +\n\t\"\\tlog_level\\x18\\f \\x01(\\tH\\x02R\\blogLevel\\x88\\x01\\x01\\x120\\n\" +\n\t\"\\x14scrub_sensitive_data\\x18\\r \\x01(\\bR\\x12scrubSensitiveData\\\"F\\n\" +\n\t\"\\vBrowserMode\\x12\\x10\\n\" +\n\t\"\\fBROWSER_AUTO\\x10\\x00\\x12\\x11\\n\" +\n\t\"\\rBROWSER_NEVER\\x10\\x01\\x12\\x12\\n\" +\n\t\"\\x0eBROWSER_ALWAYS\\x10\\x02\\\"C\\n\" +\n\t\"\\tDebugMode\\x12\\x12\\n\" +\n\t\"\\x0eDEBUG_DISABLED\\x10\\x00\\x12\\x11\\n\" +\n\t\"\\rDEBUG_ENABLED\\x10\\x01\\x12\\x0f\\n\" +\n\t\"\\vDEBUG_BREAK\\x10\\x02B\\r\\n\" +\n\t\"\\v_trace_fileB\\f\\n\" +\n\t\"\\n\" +\n\t\"_namespaceB\\f\\n\" +\n\t\"\\n\" +\n\t\"_log_level\\\"\\xf0\\x01\\n\" +\n\t\"\\vTestRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12\\x12\\n\" +\n\t\"\\x04args\\x18\\x03 \\x03(\\tR\\x04args\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x04 \\x03(\\tR\\aenviron\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"trace_file\\x18\\x06 \\x01(\\tH\\x00R\\ttraceFile\\x88\\x01\\x01\\x12#\\n\" +\n\t\"\\rcodegen_debug\\x18\\a \\x01(\\bR\\fcodegenDebug\\x12\\x19\\n\" +\n\t\"\\btemp_dir\\x18\\b \\x01(\\tR\\atempDirB\\r\\n\" +\n\t\"\\v_trace_fileJ\\x04\\b\\x05\\x10\\x06\\\"\\x96\\x01\\n\" +\n\t\"\\x0fTestSpecRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12\\x12\\n\" +\n\t\"\\x04args\\x18\\x03 \\x03(\\tR\\x04args\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x04 \\x03(\\tR\\aenviron\\x12\\x19\\n\" +\n\t\"\\btemp_dir\\x18\\x05 \\x01(\\tR\\atempDir\\\"Z\\n\" +\n\t\"\\x10TestSpecResponse\\x12\\x18\\n\" +\n\t\"\\acommand\\x18\\x01 \\x01(\\tR\\acommand\\x12\\x12\\n\" +\n\t\"\\x04args\\x18\\x02 \\x03(\\tR\\x04args\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x03 \\x03(\\tR\\aenviron\\\"\\xee\\x01\\n\" +\n\t\"\\x11ExecScriptRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12\\x1f\\n\" +\n\t\"\\vscript_args\\x18\\x04 \\x03(\\tR\\n\" +\n\t\"scriptArgs\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x05 \\x03(\\tR\\aenviron\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"trace_file\\x18\\x06 \\x01(\\tH\\x00R\\ttraceFile\\x88\\x01\\x01\\x12!\\n\" +\n\t\"\\tnamespace\\x18\\a \\x01(\\tH\\x01R\\tnamespace\\x88\\x01\\x01B\\r\\n\" +\n\t\"\\v_trace_fileB\\f\\n\" +\n\t\"\\n\" +\n\t\"_namespace\\\"\\xd4\\x01\\n\" +\n\t\"\\x0fExecSpecRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12\\x1f\\n\" +\n\t\"\\vscript_args\\x18\\x04 \\x03(\\tR\\n\" +\n\t\"scriptArgs\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x05 \\x03(\\tR\\aenviron\\x12!\\n\" +\n\t\"\\tnamespace\\x18\\a \\x01(\\tH\\x00R\\tnamespace\\x88\\x01\\x01\\x12\\x19\\n\" +\n\t\"\\btemp_dir\\x18\\b \\x01(\\tR\\atempDirB\\f\\n\" +\n\t\"\\n\" +\n\t\"_namespace\\\"\\x87\\x01\\n\" +\n\t\"\\x0fExecSpecMessage\\x126\\n\" +\n\t\"\\x06output\\x18\\x01 \\x01(\\v2\\x1c.encore.daemon.CommandOutputH\\x00R\\x06output\\x125\\n\" +\n\t\"\\x04spec\\x18\\x02 \\x01(\\v2\\x1f.encore.daemon.ExecSpecResponseH\\x00R\\x04specB\\x05\\n\" +\n\t\"\\x03msg\\\"Z\\n\" +\n\t\"\\x10ExecSpecResponse\\x12\\x18\\n\" +\n\t\"\\acommand\\x18\\x01 \\x01(\\tR\\acommand\\x12\\x12\\n\" +\n\t\"\\x04args\\x18\\x02 \\x03(\\tR\\x04args\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x03 \\x03(\\tR\\aenviron\\\"\\xaa\\x01\\n\" +\n\t\"\\fCheckRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12#\\n\" +\n\t\"\\rcodegen_debug\\x18\\x03 \\x01(\\bR\\fcodegenDebug\\x12\\x1f\\n\" +\n\t\"\\vparse_tests\\x18\\x04 \\x01(\\bR\\n\" +\n\t\"parseTests\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x05 \\x03(\\tR\\aenviron\\\"\\x87\\x03\\n\" +\n\t\"\\rExportRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x12\\n\" +\n\t\"\\x04goos\\x18\\x02 \\x01(\\tR\\x04goos\\x12\\x16\\n\" +\n\t\"\\x06goarch\\x18\\x03 \\x01(\\tR\\x06goarch\\x12\\x1f\\n\" +\n\t\"\\vcgo_enabled\\x18\\x04 \\x01(\\bR\\n\" +\n\t\"cgoEnabled\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x05 \\x03(\\tR\\aenviron\\x12;\\n\" +\n\t\"\\x06docker\\x18\\x06 \\x01(\\v2!.encore.daemon.DockerExportParamsH\\x00R\\x06docker\\x12&\\n\" +\n\t\"\\x0finfra_conf_path\\x18\\a \\x01(\\tR\\rinfraConfPath\\x12\\x1a\\n\" +\n\t\"\\bservices\\x18\\b \\x03(\\tR\\bservices\\x12\\x1a\\n\" +\n\t\"\\bgateways\\x18\\t \\x03(\\tR\\bgateways\\x12&\\n\" +\n\t\"\\x0fskip_infra_conf\\x18\\n\" +\n\t\" \\x01(\\bR\\rskipInfraConf\\x12%\\n\" +\n\t\"\\x0eworkspace_root\\x18\\v \\x01(\\tR\\rworkspaceRootB\\b\\n\" +\n\t\"\\x06format\\\"\\x96\\x01\\n\" +\n\t\"\\x12DockerExportParams\\x12(\\n\" +\n\t\"\\x10local_daemon_tag\\x18\\x01 \\x01(\\tR\\x0elocalDaemonTag\\x120\\n\" +\n\t\"\\x14push_destination_tag\\x18\\x02 \\x01(\\tR\\x12pushDestinationTag\\x12$\\n\" +\n\t\"\\x0ebase_image_tag\\x18\\x03 \\x01(\\tR\\fbaseImageTag\\\"\\xfe\\x01\\n\" +\n\t\"\\x10DBConnectRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x17\\n\" +\n\t\"\\adb_name\\x18\\x02 \\x01(\\tR\\x06dbName\\x12\\x19\\n\" +\n\t\"\\benv_name\\x18\\x03 \\x01(\\tR\\aenvName\\x12?\\n\" +\n\t\"\\fcluster_type\\x18\\x04 \\x01(\\x0e2\\x1c.encore.daemon.DBClusterTypeR\\vclusterType\\x12!\\n\" +\n\t\"\\tnamespace\\x18\\x05 \\x01(\\tH\\x00R\\tnamespace\\x88\\x01\\x01\\x12)\\n\" +\n\t\"\\x04role\\x18\\x06 \\x01(\\x0e2\\x15.encore.daemon.DBRoleR\\x04roleB\\f\\n\" +\n\t\"\\n\" +\n\t\"_namespace\\\"%\\n\" +\n\t\"\\x11DBConnectResponse\\x12\\x10\\n\" +\n\t\"\\x03dsn\\x18\\x01 \\x01(\\tR\\x03dsn\\\"\\xf7\\x01\\n\" +\n\t\"\\x0eDBProxyRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x19\\n\" +\n\t\"\\benv_name\\x18\\x02 \\x01(\\tR\\aenvName\\x12\\x12\\n\" +\n\t\"\\x04port\\x18\\x03 \\x01(\\x05R\\x04port\\x12?\\n\" +\n\t\"\\fcluster_type\\x18\\x04 \\x01(\\x0e2\\x1c.encore.daemon.DBClusterTypeR\\vclusterType\\x12!\\n\" +\n\t\"\\tnamespace\\x18\\x05 \\x01(\\tH\\x00R\\tnamespace\\x88\\x01\\x01\\x12)\\n\" +\n\t\"\\x04role\\x18\\x06 \\x01(\\x0e2\\x15.encore.daemon.DBRoleR\\x04roleB\\f\\n\" +\n\t\"\\n\" +\n\t\"_namespace\\\"\\xc4\\x01\\n\" +\n\t\"\\x0eDBResetRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12%\\n\" +\n\t\"\\x0edatabase_names\\x18\\x02 \\x03(\\tR\\rdatabaseNames\\x12?\\n\" +\n\t\"\\fcluster_type\\x18\\x03 \\x01(\\x0e2\\x1c.encore.daemon.DBClusterTypeR\\vclusterType\\x12!\\n\" +\n\t\"\\tnamespace\\x18\\x04 \\x01(\\tH\\x00R\\tnamespace\\x88\\x01\\x01B\\f\\n\" +\n\t\"\\n\" +\n\t\"_namespace\\\"\\xae\\x04\\n\" +\n\t\"\\x10GenClientRequest\\x12\\x15\\n\" +\n\t\"\\x06app_id\\x18\\x01 \\x01(\\tR\\x05appId\\x12\\x19\\n\" +\n\t\"\\benv_name\\x18\\x02 \\x01(\\tR\\aenvName\\x12\\x12\\n\" +\n\t\"\\x04lang\\x18\\x03 \\x01(\\tR\\x04lang\\x12\\x1a\\n\" +\n\t\"\\bfilepath\\x18\\x04 \\x01(\\tR\\bfilepath\\x12\\x1a\\n\" +\n\t\"\\bservices\\x18\\x05 \\x03(\\tR\\bservices\\x12+\\n\" +\n\t\"\\x11excluded_services\\x18\\x06 \\x03(\\tR\\x10excludedServices\\x12#\\n\" +\n\t\"\\rendpoint_tags\\x18\\a \\x03(\\tR\\fendpointTags\\x124\\n\" +\n\t\"\\x16excluded_endpoint_tags\\x18\\b \\x03(\\tR\\x14excludedEndpointTags\\x12N\\n\" +\n\t\"!openapi_exclude_private_endpoints\\x18\\t \\x01(\\bH\\x00R\\x1eopenapiExcludePrivateEndpoints\\x88\\x01\\x01\\x12+\\n\" +\n\t\"\\x0fts_shared_types\\x18\\n\" +\n\t\" \\x01(\\bH\\x01R\\rtsSharedTypes\\x88\\x01\\x01\\x12-\\n\" +\n\t\"\\x10ts_client_target\\x18\\v \\x01(\\tH\\x02R\\x0etsClientTarget\\x88\\x01\\x01\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\f \\x01(\\tR\\aappRootB$\\n\" +\n\t\"\\\"_openapi_exclude_private_endpointsB\\x12\\n\" +\n\t\"\\x10_ts_shared_typesB\\x13\\n\" +\n\t\"\\x11_ts_client_target\\\"'\\n\" +\n\t\"\\x11GenClientResponse\\x12\\x12\\n\" +\n\t\"\\x04code\\x18\\x01 \\x01(\\fR\\x04code\\\"/\\n\" +\n\t\"\\x12GenWrappersRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\\"\\x15\\n\" +\n\t\"\\x13GenWrappersResponse\\\"Z\\n\" +\n\t\"\\x15SecretsRefreshRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x02 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x03 \\x01(\\tR\\x05value\\\"\\x18\\n\" +\n\t\"\\x16SecretsRefreshResponse\\\"L\\n\" +\n\t\"\\x0fVersionResponse\\x12\\x18\\n\" +\n\t\"\\aversion\\x18\\x01 \\x01(\\tR\\aversion\\x12\\x1f\\n\" +\n\t\"\\vconfig_hash\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"configHash\\\"\\xa4\\x01\\n\" +\n\t\"\\tNamespace\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x16\\n\" +\n\t\"\\x06active\\x18\\x03 \\x01(\\bR\\x06active\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"created_at\\x18\\x04 \\x01(\\tR\\tcreatedAt\\x12)\\n\" +\n\t\"\\x0elast_active_at\\x18\\x05 \\x01(\\tH\\x00R\\flastActiveAt\\x88\\x01\\x01B\\x11\\n\" +\n\t\"\\x0f_last_active_at\\\"G\\n\" +\n\t\"\\x16CreateNamespaceRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\\"_\\n\" +\n\t\"\\x16SwitchNamespaceRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x16\\n\" +\n\t\"\\x06create\\x18\\x03 \\x01(\\bR\\x06create\\\"2\\n\" +\n\t\"\\x15ListNamespacesRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\\"G\\n\" +\n\t\"\\x16DeleteNamespaceRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\\"R\\n\" +\n\t\"\\x16ListNamespacesResponse\\x128\\n\" +\n\t\"\\n\" +\n\t\"namespaces\\x18\\x01 \\x03(\\v2\\x18.encore.daemon.NamespaceR\\n\" +\n\t\"namespaces\\\"Z\\n\" +\n\t\"\\x0fTelemetryConfig\\x12\\x17\\n\" +\n\t\"\\aanon_id\\x18\\x01 \\x01(\\tR\\x06anonId\\x12\\x18\\n\" +\n\t\"\\aenabled\\x18\\x02 \\x01(\\bR\\aenabled\\x12\\x14\\n\" +\n\t\"\\x05debug\\x18\\x03 \\x01(\\bR\\x05debug\\\"\\x8c\\x02\\n\" +\n\t\"\\x0fDumpMetaRequest\\x12\\x19\\n\" +\n\t\"\\bapp_root\\x18\\x01 \\x01(\\tR\\aappRoot\\x12\\x1f\\n\" +\n\t\"\\vworking_dir\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"workingDir\\x12\\x18\\n\" +\n\t\"\\aenviron\\x18\\x03 \\x03(\\tR\\aenviron\\x12\\x1f\\n\" +\n\t\"\\vparse_tests\\x18\\x04 \\x01(\\bR\\n\" +\n\t\"parseTests\\x12=\\n\" +\n\t\"\\x06format\\x18\\x05 \\x01(\\x0e2%.encore.daemon.DumpMetaRequest.FormatR\\x06format\\\"C\\n\" +\n\t\"\\x06Format\\x12\\x16\\n\" +\n\t\"\\x12FORMAT_UNSPECIFIED\\x10\\x00\\x12\\x0f\\n\" +\n\t\"\\vFORMAT_JSON\\x10\\x01\\x12\\x10\\n\" +\n\t\"\\fFORMAT_PROTO\\x10\\x02\\\"&\\n\" +\n\t\"\\x10DumpMetaResponse\\x12\\x12\\n\" +\n\t\"\\x04meta\\x18\\x01 \\x01(\\fR\\x04meta\\\"\\xcb\\x15\\n\" +\n\t\"\\n\" +\n\t\"SQLCPlugin\\x1a6\\n\" +\n\t\"\\x04File\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x1a\\n\" +\n\t\"\\bcontents\\x18\\x02 \\x01(\\fR\\bcontents\\x1a\\xc9\\x01\\n\" +\n\t\"\\bSettings\\x12\\x18\\n\" +\n\t\"\\aversion\\x18\\x01 \\x01(\\tR\\aversion\\x12\\x16\\n\" +\n\t\"\\x06engine\\x18\\x02 \\x01(\\tR\\x06engine\\x12\\x16\\n\" +\n\t\"\\x06schema\\x18\\x03 \\x03(\\tR\\x06schema\\x12\\x18\\n\" +\n\t\"\\aqueries\\x18\\x04 \\x03(\\tR\\aqueries\\x12;\\n\" +\n\t\"\\acodegen\\x18\\f \\x01(\\v2!.encore.daemon.SQLCPlugin.CodegenR\\acodegenJ\\x04\\b\\x05\\x10\\x06J\\x04\\b\\b\\x10\\tJ\\x04\\b\\t\\x10\\n\" +\n\t\"J\\x04\\b\\n\" +\n\t\"\\x10\\vJ\\x04\\b\\v\\x10\\f\\x1a\\xaf\\x02\\n\" +\n\t\"\\aCodegen\\x12\\x10\\n\" +\n\t\"\\x03out\\x18\\x01 \\x01(\\tR\\x03out\\x12\\x16\\n\" +\n\t\"\\x06plugin\\x18\\x02 \\x01(\\tR\\x06plugin\\x12\\x18\\n\" +\n\t\"\\aoptions\\x18\\x03 \\x01(\\fR\\aoptions\\x12\\x10\\n\" +\n\t\"\\x03env\\x18\\x04 \\x03(\\tR\\x03env\\x12C\\n\" +\n\t\"\\aprocess\\x18\\x05 \\x01(\\v2).encore.daemon.SQLCPlugin.Codegen.ProcessR\\aprocess\\x12:\\n\" +\n\t\"\\x04wasm\\x18\\x06 \\x01(\\v2&.encore.daemon.SQLCPlugin.Codegen.WASMR\\x04wasm\\x1a\\x1b\\n\" +\n\t\"\\aProcess\\x12\\x10\\n\" +\n\t\"\\x03cmd\\x18\\x01 \\x01(\\tR\\x03cmd\\x1a0\\n\" +\n\t\"\\x04WASM\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x01 \\x01(\\tR\\x03url\\x12\\x16\\n\" +\n\t\"\\x06sha256\\x18\\x02 \\x01(\\tR\\x06sha256\\x1a\\x9a\\x01\\n\" +\n\t\"\\aCatalog\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x01 \\x01(\\tR\\acomment\\x12%\\n\" +\n\t\"\\x0edefault_schema\\x18\\x02 \\x01(\\tR\\rdefaultSchema\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x03 \\x01(\\tR\\x04name\\x12:\\n\" +\n\t\"\\aschemas\\x18\\x04 \\x03(\\v2 .encore.daemon.SQLCPlugin.SchemaR\\aschemas\\x1a\\xf7\\x01\\n\" +\n\t\"\\x06Schema\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x01 \\x01(\\tR\\acomment\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x127\\n\" +\n\t\"\\x06tables\\x18\\x03 \\x03(\\v2\\x1f.encore.daemon.SQLCPlugin.TableR\\x06tables\\x124\\n\" +\n\t\"\\x05enums\\x18\\x04 \\x03(\\v2\\x1e.encore.daemon.SQLCPlugin.EnumR\\x05enums\\x12P\\n\" +\n\t\"\\x0fcomposite_types\\x18\\x05 \\x03(\\v2'.encore.daemon.SQLCPlugin.CompositeTypeR\\x0ecompositeTypes\\x1a=\\n\" +\n\t\"\\rCompositeType\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x02 \\x01(\\tR\\acomment\\x1aH\\n\" +\n\t\"\\x04Enum\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x12\\n\" +\n\t\"\\x04vals\\x18\\x02 \\x03(\\tR\\x04vals\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x03 \\x01(\\tR\\acomment\\x1a\\x95\\x01\\n\" +\n\t\"\\x05Table\\x126\\n\" +\n\t\"\\x03rel\\x18\\x01 \\x01(\\v2$.encore.daemon.SQLCPlugin.IdentifierR\\x03rel\\x12:\\n\" +\n\t\"\\acolumns\\x18\\x02 \\x03(\\v2 .encore.daemon.SQLCPlugin.ColumnR\\acolumns\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x03 \\x01(\\tR\\acomment\\x1aR\\n\" +\n\t\"\\n\" +\n\t\"Identifier\\x12\\x18\\n\" +\n\t\"\\acatalog\\x18\\x01 \\x01(\\tR\\acatalog\\x12\\x16\\n\" +\n\t\"\\x06schema\\x18\\x02 \\x01(\\tR\\x06schema\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x03 \\x01(\\tR\\x04name\\x1a\\xc4\\x04\\n\" +\n\t\"\\x06Column\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x19\\n\" +\n\t\"\\bnot_null\\x18\\x03 \\x01(\\bR\\anotNull\\x12\\x19\\n\" +\n\t\"\\bis_array\\x18\\x04 \\x01(\\bR\\aisArray\\x12\\x18\\n\" +\n\t\"\\acomment\\x18\\x05 \\x01(\\tR\\acomment\\x12\\x16\\n\" +\n\t\"\\x06length\\x18\\x06 \\x01(\\x05R\\x06length\\x12$\\n\" +\n\t\"\\x0eis_named_param\\x18\\a \\x01(\\bR\\fisNamedParam\\x12 \\n\" +\n\t\"\\fis_func_call\\x18\\b \\x01(\\bR\\n\" +\n\t\"isFuncCall\\x12\\x14\\n\" +\n\t\"\\x05scope\\x18\\t \\x01(\\tR\\x05scope\\x12:\\n\" +\n\t\"\\x05table\\x18\\n\" +\n\t\" \\x01(\\v2$.encore.daemon.SQLCPlugin.IdentifierR\\x05table\\x12\\x1f\\n\" +\n\t\"\\vtable_alias\\x18\\v \\x01(\\tR\\n\" +\n\t\"tableAlias\\x128\\n\" +\n\t\"\\x04type\\x18\\f \\x01(\\v2$.encore.daemon.SQLCPlugin.IdentifierR\\x04type\\x12\\\"\\n\" +\n\t\"\\ris_sqlc_slice\\x18\\r \\x01(\\bR\\visSqlcSlice\\x12E\\n\" +\n\t\"\\vembed_table\\x18\\x0e \\x01(\\v2$.encore.daemon.SQLCPlugin.IdentifierR\\n\" +\n\t\"embedTable\\x12#\\n\" +\n\t\"\\roriginal_name\\x18\\x0f \\x01(\\tR\\foriginalName\\x12\\x1a\\n\" +\n\t\"\\bunsigned\\x18\\x10 \\x01(\\bR\\bunsigned\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"array_dims\\x18\\x11 \\x01(\\x05R\\tarrayDims\\x1a\\xca\\x02\\n\" +\n\t\"\\x05Query\\x12\\x12\\n\" +\n\t\"\\x04text\\x18\\x01 \\x01(\\tR\\x04text\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03cmd\\x18\\x03 \\x01(\\tR\\x03cmd\\x12:\\n\" +\n\t\"\\acolumns\\x18\\x04 \\x03(\\v2 .encore.daemon.SQLCPlugin.ColumnR\\acolumns\\x12?\\n\" +\n\t\"\\x06params\\x18\\x05 \\x03(\\v2#.encore.daemon.SQLCPlugin.ParameterR\\n\" +\n\t\"parameters\\x12\\x1a\\n\" +\n\t\"\\bcomments\\x18\\x06 \\x03(\\tR\\bcomments\\x12\\x1a\\n\" +\n\t\"\\bfilename\\x18\\a \\x01(\\tR\\bfilename\\x12R\\n\" +\n\t\"\\x11insert_into_table\\x18\\b \\x01(\\v2$.encore.daemon.SQLCPlugin.IdentifierR\\x11insert_into_table\\x1a]\\n\" +\n\t\"\\tParameter\\x12\\x16\\n\" +\n\t\"\\x06number\\x18\\x01 \\x01(\\x05R\\x06number\\x128\\n\" +\n\t\"\\x06column\\x18\\x02 \\x01(\\v2 .encore.daemon.SQLCPlugin.ColumnR\\x06column\\x1a\\xbd\\x02\\n\" +\n\t\"\\x0fGenerateRequest\\x12>\\n\" +\n\t\"\\bsettings\\x18\\x01 \\x01(\\v2\\\".encore.daemon.SQLCPlugin.SettingsR\\bsettings\\x12;\\n\" +\n\t\"\\acatalog\\x18\\x02 \\x01(\\v2!.encore.daemon.SQLCPlugin.CatalogR\\acatalog\\x129\\n\" +\n\t\"\\aqueries\\x18\\x03 \\x03(\\v2\\x1f.encore.daemon.SQLCPlugin.QueryR\\aqueries\\x12\\\"\\n\" +\n\t\"\\fsqlc_version\\x18\\x04 \\x01(\\tR\\fsqlc_version\\x12&\\n\" +\n\t\"\\x0eplugin_options\\x18\\x05 \\x01(\\fR\\x0eplugin_options\\x12&\\n\" +\n\t\"\\x0eglobal_options\\x18\\x06 \\x01(\\fR\\x0eglobal_options\\x1aH\\n\" +\n\t\"\\x10GenerateResponse\\x124\\n\" +\n\t\"\\x05files\\x18\\x01 \\x03(\\v2\\x1e.encore.daemon.SQLCPlugin.FileR\\x05files*p\\n\" +\n\t\"\\x06DBRole\\x12\\x17\\n\" +\n\t\"\\x13DB_ROLE_UNSPECIFIED\\x10\\x00\\x12\\x15\\n\" +\n\t\"\\x11DB_ROLE_SUPERUSER\\x10\\x01\\x12\\x11\\n\" +\n\t\"\\rDB_ROLE_ADMIN\\x10\\x02\\x12\\x11\\n\" +\n\t\"\\rDB_ROLE_WRITE\\x10\\x03\\x12\\x10\\n\" +\n\t\"\\fDB_ROLE_READ\\x10\\x04*\\x7f\\n\" +\n\t\"\\rDBClusterType\\x12\\x1f\\n\" +\n\t\"\\x1bDB_CLUSTER_TYPE_UNSPECIFIED\\x10\\x00\\x12\\x17\\n\" +\n\t\"\\x13DB_CLUSTER_TYPE_RUN\\x10\\x01\\x12\\x18\\n\" +\n\t\"\\x14DB_CLUSTER_TYPE_TEST\\x10\\x02\\x12\\x1a\\n\" +\n\t\"\\x16DB_CLUSTER_TYPE_SHADOW\\x10\\x032\\xf5\\f\\n\" +\n\t\"\\x06Daemon\\x12A\\n\" +\n\t\"\\x03Run\\x12\\x19.encore.daemon.RunRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12C\\n\" +\n\t\"\\x04Test\\x12\\x1a.encore.daemon.TestRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12K\\n\" +\n\t\"\\bTestSpec\\x12\\x1e.encore.daemon.TestSpecRequest\\x1a\\x1f.encore.daemon.TestSpecResponse\\x12O\\n\" +\n\t\"\\n\" +\n\t\"ExecScript\\x12 .encore.daemon.ExecScriptRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12L\\n\" +\n\t\"\\bExecSpec\\x12\\x1e.encore.daemon.ExecSpecRequest\\x1a\\x1e.encore.daemon.ExecSpecMessage0\\x01\\x12E\\n\" +\n\t\"\\x05Check\\x12\\x1b.encore.daemon.CheckRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12G\\n\" +\n\t\"\\x06Export\\x12\\x1c.encore.daemon.ExportRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12N\\n\" +\n\t\"\\tDBConnect\\x12\\x1f.encore.daemon.DBConnectRequest\\x1a .encore.daemon.DBConnectResponse\\x12I\\n\" +\n\t\"\\aDBProxy\\x12\\x1d.encore.daemon.DBProxyRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12I\\n\" +\n\t\"\\aDBReset\\x12\\x1d.encore.daemon.DBResetRequest\\x1a\\x1d.encore.daemon.CommandMessage0\\x01\\x12N\\n\" +\n\t\"\\tGenClient\\x12\\x1f.encore.daemon.GenClientRequest\\x1a .encore.daemon.GenClientResponse\\x12T\\n\" +\n\t\"\\vGenWrappers\\x12!.encore.daemon.GenWrappersRequest\\x1a\\\".encore.daemon.GenWrappersResponse\\x12]\\n\" +\n\t\"\\x0eSecretsRefresh\\x12$.encore.daemon.SecretsRefreshRequest\\x1a%.encore.daemon.SecretsRefreshResponse\\x12A\\n\" +\n\t\"\\aVersion\\x12\\x16.google.protobuf.Empty\\x1a\\x1e.encore.daemon.VersionResponse\\x12R\\n\" +\n\t\"\\x0fCreateNamespace\\x12%.encore.daemon.CreateNamespaceRequest\\x1a\\x18.encore.daemon.Namespace\\x12R\\n\" +\n\t\"\\x0fSwitchNamespace\\x12%.encore.daemon.SwitchNamespaceRequest\\x1a\\x18.encore.daemon.Namespace\\x12]\\n\" +\n\t\"\\x0eListNamespaces\\x12$.encore.daemon.ListNamespacesRequest\\x1a%.encore.daemon.ListNamespacesResponse\\x12P\\n\" +\n\t\"\\x0fDeleteNamespace\\x12%.encore.daemon.DeleteNamespaceRequest\\x1a\\x16.google.protobuf.Empty\\x12K\\n\" +\n\t\"\\bDumpMeta\\x12\\x1e.encore.daemon.DumpMetaRequest\\x1a\\x1f.encore.daemon.DumpMetaResponse\\x12C\\n\" +\n\t\"\\tTelemetry\\x12\\x1e.encore.daemon.TelemetryConfig\\x1a\\x16.google.protobuf.Empty\\x12N\\n\" +\n\t\"\\tCreateApp\\x12\\x1f.encore.daemon.CreateAppRequest\\x1a .encore.daemon.CreateAppResponseB\\x1eZ\\x1cencr.dev/proto/encore/daemonb\\x06proto3\"\n\nvar (\n\tfile_encore_daemon_daemon_proto_rawDescOnce sync.Once\n\tfile_encore_daemon_daemon_proto_rawDescData []byte\n)\n\nfunc file_encore_daemon_daemon_proto_rawDescGZIP() []byte {\n\tfile_encore_daemon_daemon_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_daemon_daemon_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_daemon_daemon_proto_rawDesc), len(file_encore_daemon_daemon_proto_rawDesc)))\n\t})\n\treturn file_encore_daemon_daemon_proto_rawDescData\n}\n\nvar file_encore_daemon_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5)\nvar file_encore_daemon_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 54)\nvar file_encore_daemon_daemon_proto_goTypes = []any{\n\t(DBRole)(0),                         // 0: encore.daemon.DBRole\n\t(DBClusterType)(0),                  // 1: encore.daemon.DBClusterType\n\t(RunRequest_BrowserMode)(0),         // 2: encore.daemon.RunRequest.BrowserMode\n\t(RunRequest_DebugMode)(0),           // 3: encore.daemon.RunRequest.DebugMode\n\t(DumpMetaRequest_Format)(0),         // 4: encore.daemon.DumpMetaRequest.Format\n\t(*CommandMessage)(nil),              // 5: encore.daemon.CommandMessage\n\t(*CommandOutput)(nil),               // 6: encore.daemon.CommandOutput\n\t(*CommandExit)(nil),                 // 7: encore.daemon.CommandExit\n\t(*CommandDisplayErrors)(nil),        // 8: encore.daemon.CommandDisplayErrors\n\t(*CreateAppRequest)(nil),            // 9: encore.daemon.CreateAppRequest\n\t(*CreateAppResponse)(nil),           // 10: encore.daemon.CreateAppResponse\n\t(*RunRequest)(nil),                  // 11: encore.daemon.RunRequest\n\t(*TestRequest)(nil),                 // 12: encore.daemon.TestRequest\n\t(*TestSpecRequest)(nil),             // 13: encore.daemon.TestSpecRequest\n\t(*TestSpecResponse)(nil),            // 14: encore.daemon.TestSpecResponse\n\t(*ExecScriptRequest)(nil),           // 15: encore.daemon.ExecScriptRequest\n\t(*ExecSpecRequest)(nil),             // 16: encore.daemon.ExecSpecRequest\n\t(*ExecSpecMessage)(nil),             // 17: encore.daemon.ExecSpecMessage\n\t(*ExecSpecResponse)(nil),            // 18: encore.daemon.ExecSpecResponse\n\t(*CheckRequest)(nil),                // 19: encore.daemon.CheckRequest\n\t(*ExportRequest)(nil),               // 20: encore.daemon.ExportRequest\n\t(*DockerExportParams)(nil),          // 21: encore.daemon.DockerExportParams\n\t(*DBConnectRequest)(nil),            // 22: encore.daemon.DBConnectRequest\n\t(*DBConnectResponse)(nil),           // 23: encore.daemon.DBConnectResponse\n\t(*DBProxyRequest)(nil),              // 24: encore.daemon.DBProxyRequest\n\t(*DBResetRequest)(nil),              // 25: encore.daemon.DBResetRequest\n\t(*GenClientRequest)(nil),            // 26: encore.daemon.GenClientRequest\n\t(*GenClientResponse)(nil),           // 27: encore.daemon.GenClientResponse\n\t(*GenWrappersRequest)(nil),          // 28: encore.daemon.GenWrappersRequest\n\t(*GenWrappersResponse)(nil),         // 29: encore.daemon.GenWrappersResponse\n\t(*SecretsRefreshRequest)(nil),       // 30: encore.daemon.SecretsRefreshRequest\n\t(*SecretsRefreshResponse)(nil),      // 31: encore.daemon.SecretsRefreshResponse\n\t(*VersionResponse)(nil),             // 32: encore.daemon.VersionResponse\n\t(*Namespace)(nil),                   // 33: encore.daemon.Namespace\n\t(*CreateNamespaceRequest)(nil),      // 34: encore.daemon.CreateNamespaceRequest\n\t(*SwitchNamespaceRequest)(nil),      // 35: encore.daemon.SwitchNamespaceRequest\n\t(*ListNamespacesRequest)(nil),       // 36: encore.daemon.ListNamespacesRequest\n\t(*DeleteNamespaceRequest)(nil),      // 37: encore.daemon.DeleteNamespaceRequest\n\t(*ListNamespacesResponse)(nil),      // 38: encore.daemon.ListNamespacesResponse\n\t(*TelemetryConfig)(nil),             // 39: encore.daemon.TelemetryConfig\n\t(*DumpMetaRequest)(nil),             // 40: encore.daemon.DumpMetaRequest\n\t(*DumpMetaResponse)(nil),            // 41: encore.daemon.DumpMetaResponse\n\t(*SQLCPlugin)(nil),                  // 42: encore.daemon.SQLCPlugin\n\t(*SQLCPlugin_File)(nil),             // 43: encore.daemon.SQLCPlugin.File\n\t(*SQLCPlugin_Settings)(nil),         // 44: encore.daemon.SQLCPlugin.Settings\n\t(*SQLCPlugin_Codegen)(nil),          // 45: encore.daemon.SQLCPlugin.Codegen\n\t(*SQLCPlugin_Catalog)(nil),          // 46: encore.daemon.SQLCPlugin.Catalog\n\t(*SQLCPlugin_Schema)(nil),           // 47: encore.daemon.SQLCPlugin.Schema\n\t(*SQLCPlugin_CompositeType)(nil),    // 48: encore.daemon.SQLCPlugin.CompositeType\n\t(*SQLCPlugin_Enum)(nil),             // 49: encore.daemon.SQLCPlugin.Enum\n\t(*SQLCPlugin_Table)(nil),            // 50: encore.daemon.SQLCPlugin.Table\n\t(*SQLCPlugin_Identifier)(nil),       // 51: encore.daemon.SQLCPlugin.Identifier\n\t(*SQLCPlugin_Column)(nil),           // 52: encore.daemon.SQLCPlugin.Column\n\t(*SQLCPlugin_Query)(nil),            // 53: encore.daemon.SQLCPlugin.Query\n\t(*SQLCPlugin_Parameter)(nil),        // 54: encore.daemon.SQLCPlugin.Parameter\n\t(*SQLCPlugin_GenerateRequest)(nil),  // 55: encore.daemon.SQLCPlugin.GenerateRequest\n\t(*SQLCPlugin_GenerateResponse)(nil), // 56: encore.daemon.SQLCPlugin.GenerateResponse\n\t(*SQLCPlugin_Codegen_Process)(nil),  // 57: encore.daemon.SQLCPlugin.Codegen.Process\n\t(*SQLCPlugin_Codegen_WASM)(nil),     // 58: encore.daemon.SQLCPlugin.Codegen.WASM\n\t(*emptypb.Empty)(nil),               // 59: google.protobuf.Empty\n}\nvar file_encore_daemon_daemon_proto_depIdxs = []int32{\n\t6,  // 0: encore.daemon.CommandMessage.output:type_name -> encore.daemon.CommandOutput\n\t7,  // 1: encore.daemon.CommandMessage.exit:type_name -> encore.daemon.CommandExit\n\t8,  // 2: encore.daemon.CommandMessage.errors:type_name -> encore.daemon.CommandDisplayErrors\n\t2,  // 3: encore.daemon.RunRequest.browser:type_name -> encore.daemon.RunRequest.BrowserMode\n\t3,  // 4: encore.daemon.RunRequest.debug_mode:type_name -> encore.daemon.RunRequest.DebugMode\n\t6,  // 5: encore.daemon.ExecSpecMessage.output:type_name -> encore.daemon.CommandOutput\n\t18, // 6: encore.daemon.ExecSpecMessage.spec:type_name -> encore.daemon.ExecSpecResponse\n\t21, // 7: encore.daemon.ExportRequest.docker:type_name -> encore.daemon.DockerExportParams\n\t1,  // 8: encore.daemon.DBConnectRequest.cluster_type:type_name -> encore.daemon.DBClusterType\n\t0,  // 9: encore.daemon.DBConnectRequest.role:type_name -> encore.daemon.DBRole\n\t1,  // 10: encore.daemon.DBProxyRequest.cluster_type:type_name -> encore.daemon.DBClusterType\n\t0,  // 11: encore.daemon.DBProxyRequest.role:type_name -> encore.daemon.DBRole\n\t1,  // 12: encore.daemon.DBResetRequest.cluster_type:type_name -> encore.daemon.DBClusterType\n\t33, // 13: encore.daemon.ListNamespacesResponse.namespaces:type_name -> encore.daemon.Namespace\n\t4,  // 14: encore.daemon.DumpMetaRequest.format:type_name -> encore.daemon.DumpMetaRequest.Format\n\t45, // 15: encore.daemon.SQLCPlugin.Settings.codegen:type_name -> encore.daemon.SQLCPlugin.Codegen\n\t57, // 16: encore.daemon.SQLCPlugin.Codegen.process:type_name -> encore.daemon.SQLCPlugin.Codegen.Process\n\t58, // 17: encore.daemon.SQLCPlugin.Codegen.wasm:type_name -> encore.daemon.SQLCPlugin.Codegen.WASM\n\t47, // 18: encore.daemon.SQLCPlugin.Catalog.schemas:type_name -> encore.daemon.SQLCPlugin.Schema\n\t50, // 19: encore.daemon.SQLCPlugin.Schema.tables:type_name -> encore.daemon.SQLCPlugin.Table\n\t49, // 20: encore.daemon.SQLCPlugin.Schema.enums:type_name -> encore.daemon.SQLCPlugin.Enum\n\t48, // 21: encore.daemon.SQLCPlugin.Schema.composite_types:type_name -> encore.daemon.SQLCPlugin.CompositeType\n\t51, // 22: encore.daemon.SQLCPlugin.Table.rel:type_name -> encore.daemon.SQLCPlugin.Identifier\n\t52, // 23: encore.daemon.SQLCPlugin.Table.columns:type_name -> encore.daemon.SQLCPlugin.Column\n\t51, // 24: encore.daemon.SQLCPlugin.Column.table:type_name -> encore.daemon.SQLCPlugin.Identifier\n\t51, // 25: encore.daemon.SQLCPlugin.Column.type:type_name -> encore.daemon.SQLCPlugin.Identifier\n\t51, // 26: encore.daemon.SQLCPlugin.Column.embed_table:type_name -> encore.daemon.SQLCPlugin.Identifier\n\t52, // 27: encore.daemon.SQLCPlugin.Query.columns:type_name -> encore.daemon.SQLCPlugin.Column\n\t54, // 28: encore.daemon.SQLCPlugin.Query.params:type_name -> encore.daemon.SQLCPlugin.Parameter\n\t51, // 29: encore.daemon.SQLCPlugin.Query.insert_into_table:type_name -> encore.daemon.SQLCPlugin.Identifier\n\t52, // 30: encore.daemon.SQLCPlugin.Parameter.column:type_name -> encore.daemon.SQLCPlugin.Column\n\t44, // 31: encore.daemon.SQLCPlugin.GenerateRequest.settings:type_name -> encore.daemon.SQLCPlugin.Settings\n\t46, // 32: encore.daemon.SQLCPlugin.GenerateRequest.catalog:type_name -> encore.daemon.SQLCPlugin.Catalog\n\t53, // 33: encore.daemon.SQLCPlugin.GenerateRequest.queries:type_name -> encore.daemon.SQLCPlugin.Query\n\t43, // 34: encore.daemon.SQLCPlugin.GenerateResponse.files:type_name -> encore.daemon.SQLCPlugin.File\n\t11, // 35: encore.daemon.Daemon.Run:input_type -> encore.daemon.RunRequest\n\t12, // 36: encore.daemon.Daemon.Test:input_type -> encore.daemon.TestRequest\n\t13, // 37: encore.daemon.Daemon.TestSpec:input_type -> encore.daemon.TestSpecRequest\n\t15, // 38: encore.daemon.Daemon.ExecScript:input_type -> encore.daemon.ExecScriptRequest\n\t16, // 39: encore.daemon.Daemon.ExecSpec:input_type -> encore.daemon.ExecSpecRequest\n\t19, // 40: encore.daemon.Daemon.Check:input_type -> encore.daemon.CheckRequest\n\t20, // 41: encore.daemon.Daemon.Export:input_type -> encore.daemon.ExportRequest\n\t22, // 42: encore.daemon.Daemon.DBConnect:input_type -> encore.daemon.DBConnectRequest\n\t24, // 43: encore.daemon.Daemon.DBProxy:input_type -> encore.daemon.DBProxyRequest\n\t25, // 44: encore.daemon.Daemon.DBReset:input_type -> encore.daemon.DBResetRequest\n\t26, // 45: encore.daemon.Daemon.GenClient:input_type -> encore.daemon.GenClientRequest\n\t28, // 46: encore.daemon.Daemon.GenWrappers:input_type -> encore.daemon.GenWrappersRequest\n\t30, // 47: encore.daemon.Daemon.SecretsRefresh:input_type -> encore.daemon.SecretsRefreshRequest\n\t59, // 48: encore.daemon.Daemon.Version:input_type -> google.protobuf.Empty\n\t34, // 49: encore.daemon.Daemon.CreateNamespace:input_type -> encore.daemon.CreateNamespaceRequest\n\t35, // 50: encore.daemon.Daemon.SwitchNamespace:input_type -> encore.daemon.SwitchNamespaceRequest\n\t36, // 51: encore.daemon.Daemon.ListNamespaces:input_type -> encore.daemon.ListNamespacesRequest\n\t37, // 52: encore.daemon.Daemon.DeleteNamespace:input_type -> encore.daemon.DeleteNamespaceRequest\n\t40, // 53: encore.daemon.Daemon.DumpMeta:input_type -> encore.daemon.DumpMetaRequest\n\t39, // 54: encore.daemon.Daemon.Telemetry:input_type -> encore.daemon.TelemetryConfig\n\t9,  // 55: encore.daemon.Daemon.CreateApp:input_type -> encore.daemon.CreateAppRequest\n\t5,  // 56: encore.daemon.Daemon.Run:output_type -> encore.daemon.CommandMessage\n\t5,  // 57: encore.daemon.Daemon.Test:output_type -> encore.daemon.CommandMessage\n\t14, // 58: encore.daemon.Daemon.TestSpec:output_type -> encore.daemon.TestSpecResponse\n\t5,  // 59: encore.daemon.Daemon.ExecScript:output_type -> encore.daemon.CommandMessage\n\t17, // 60: encore.daemon.Daemon.ExecSpec:output_type -> encore.daemon.ExecSpecMessage\n\t5,  // 61: encore.daemon.Daemon.Check:output_type -> encore.daemon.CommandMessage\n\t5,  // 62: encore.daemon.Daemon.Export:output_type -> encore.daemon.CommandMessage\n\t23, // 63: encore.daemon.Daemon.DBConnect:output_type -> encore.daemon.DBConnectResponse\n\t5,  // 64: encore.daemon.Daemon.DBProxy:output_type -> encore.daemon.CommandMessage\n\t5,  // 65: encore.daemon.Daemon.DBReset:output_type -> encore.daemon.CommandMessage\n\t27, // 66: encore.daemon.Daemon.GenClient:output_type -> encore.daemon.GenClientResponse\n\t29, // 67: encore.daemon.Daemon.GenWrappers:output_type -> encore.daemon.GenWrappersResponse\n\t31, // 68: encore.daemon.Daemon.SecretsRefresh:output_type -> encore.daemon.SecretsRefreshResponse\n\t32, // 69: encore.daemon.Daemon.Version:output_type -> encore.daemon.VersionResponse\n\t33, // 70: encore.daemon.Daemon.CreateNamespace:output_type -> encore.daemon.Namespace\n\t33, // 71: encore.daemon.Daemon.SwitchNamespace:output_type -> encore.daemon.Namespace\n\t38, // 72: encore.daemon.Daemon.ListNamespaces:output_type -> encore.daemon.ListNamespacesResponse\n\t59, // 73: encore.daemon.Daemon.DeleteNamespace:output_type -> google.protobuf.Empty\n\t41, // 74: encore.daemon.Daemon.DumpMeta:output_type -> encore.daemon.DumpMetaResponse\n\t59, // 75: encore.daemon.Daemon.Telemetry:output_type -> google.protobuf.Empty\n\t10, // 76: encore.daemon.Daemon.CreateApp:output_type -> encore.daemon.CreateAppResponse\n\t56, // [56:77] is the sub-list for method output_type\n\t35, // [35:56] is the sub-list for method input_type\n\t35, // [35:35] is the sub-list for extension type_name\n\t35, // [35:35] is the sub-list for extension extendee\n\t0,  // [0:35] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_daemon_daemon_proto_init() }\nfunc file_encore_daemon_daemon_proto_init() {\n\tif File_encore_daemon_daemon_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_daemon_daemon_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*CommandMessage_Output)(nil),\n\t\t(*CommandMessage_Exit)(nil),\n\t\t(*CommandMessage_Errors)(nil),\n\t}\n\tfile_encore_daemon_daemon_proto_msgTypes[6].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[7].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[10].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[11].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[12].OneofWrappers = []any{\n\t\t(*ExecSpecMessage_Output)(nil),\n\t\t(*ExecSpecMessage_Spec)(nil),\n\t}\n\tfile_encore_daemon_daemon_proto_msgTypes[15].OneofWrappers = []any{\n\t\t(*ExportRequest_Docker)(nil),\n\t}\n\tfile_encore_daemon_daemon_proto_msgTypes[17].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[19].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[20].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[21].OneofWrappers = []any{}\n\tfile_encore_daemon_daemon_proto_msgTypes[28].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_daemon_daemon_proto_rawDesc), len(file_encore_daemon_daemon_proto_rawDesc)),\n\t\t\tNumEnums:      5,\n\t\t\tNumMessages:   54,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_encore_daemon_daemon_proto_goTypes,\n\t\tDependencyIndexes: file_encore_daemon_daemon_proto_depIdxs,\n\t\tEnumInfos:         file_encore_daemon_daemon_proto_enumTypes,\n\t\tMessageInfos:      file_encore_daemon_daemon_proto_msgTypes,\n\t}.Build()\n\tFile_encore_daemon_daemon_proto = out.File\n\tfile_encore_daemon_daemon_proto_goTypes = nil\n\tfile_encore_daemon_daemon_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/daemon/daemon.proto",
    "content": "syntax = \"proto3\";\n\npackage encore.daemon;\n\nimport \"google/protobuf/empty.proto\";\n\noption go_package = \"encr.dev/proto/encore/daemon\";\n\nservice Daemon {\n  // Run runs the application.\n  rpc Run(RunRequest) returns (stream CommandMessage);\n  // Test runs tests.\n  rpc Test(TestRequest) returns (stream CommandMessage);\n  // TestSpec returns the specification for how to run tests.\n  rpc TestSpec(TestSpecRequest) returns (TestSpecResponse);\n  // ExecScript executes a one-off script.\n  rpc ExecScript(ExecScriptRequest) returns (stream CommandMessage);\n  // ExecSpec returns the specification for how to run an exec command.\n  // It streams progress messages during setup, then sends the spec as the final message.\n  rpc ExecSpec(ExecSpecRequest) returns (stream ExecSpecMessage);\n  // Check checks the app for compilation errors.\n  rpc Check(CheckRequest) returns (stream CommandMessage);\n  // Export exports the app in various formats.\n  rpc Export(ExportRequest) returns (stream CommandMessage);\n\n  // DBConnect starts the database and returns the DSN for connecting to it.\n  rpc DBConnect(DBConnectRequest) returns (DBConnectResponse);\n  // DBProxy starts a local database proxy for connecting to remote databases\n  // on the encore.dev platform.\n  rpc DBProxy(DBProxyRequest) returns (stream CommandMessage);\n  // DBReset resets the given databases, recreating them from scratch.\n  rpc DBReset(DBResetRequest) returns (stream CommandMessage);\n\n  // GenClient generates a client based on the app's API.\n  rpc GenClient(GenClientRequest) returns (GenClientResponse);\n  // GenWrappers generates user-facing wrapper code.\n  rpc GenWrappers(GenWrappersRequest) returns (GenWrappersResponse);\n  // SecretsRefresh tells the daemon to refresh the local development secrets\n  // for the given application.\n  rpc SecretsRefresh(SecretsRefreshRequest) returns (SecretsRefreshResponse);\n  // Version reports the daemon version.\n  rpc Version(google.protobuf.Empty) returns (VersionResponse);\n\n  // CreateNamespace creates a new infra namespace.\n  rpc CreateNamespace(CreateNamespaceRequest) returns (Namespace);\n  // SwitchNamespace switches the active infra namespace.\n  rpc SwitchNamespace(SwitchNamespaceRequest) returns (Namespace);\n  // ListNamespaces lists all namespaces for the given app.\n  rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse);\n  // DeleteNamespace deletes an infra namespace.\n  rpc DeleteNamespace(DeleteNamespaceRequest) returns (google.protobuf.Empty);\n\n  rpc DumpMeta(DumpMetaRequest) returns (DumpMetaResponse);\n  // Telemetry enables or disables telemetry.\n  rpc Telemetry(TelemetryConfig) returns (google.protobuf.Empty);\n  // InitTutorial sets the tutorial flag of the app\n  rpc CreateApp(CreateAppRequest) returns (CreateAppResponse);\n}\n\nmessage CommandMessage {\n  oneof msg {\n    CommandOutput output = 1;\n    CommandExit exit = 2;\n    CommandDisplayErrors errors = 3;\n  }\n}\n\nmessage CommandOutput {\n  bytes stdout = 1;\n  bytes stderr = 2;\n}\n\nmessage CommandExit {\n  int32 code = 1; // exit code\n}\n\nmessage CommandDisplayErrors {\n  bytes errinsrc = 1; // error messages in source code\n}\n\nmessage CreateAppRequest {\n  // app_root is the absolute filesystem path to the Encore app root.\n  string app_root = 1;\n  // template is the template used to create the app\n  string template = 2;\n  // tutorial is a flag to indicate if the app is a tutorial app\n  bool tutorial = 3;\n}\n\nmessage CreateAppResponse {\n  string app_id = 1;\n}\n\nmessage RunRequest {\n  // app_root is the absolute filesystem path to the Encore app root.\n  string app_root = 1;\n  // working_dir is the working directory relative to the app_root,\n  // for formatting relative paths in error messages.\n  string working_dir = 2;\n  // watch, if true, enables live reloading of the app whenever the source changes.\n  bool watch = 5;\n  // listen_addr is the address to listen on.\n  string listen_addr = 6;\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 7;\n\n  // trace_file, if set specifies a trace file to write trace information\n  // about the parse and compilation process to.\n  optional string trace_file = 8;\n\n  // namespace is the infrastructure namespace to use.\n  // If empty the active namespace is used.\n  optional string namespace = 9;\n\n  // browser specifies whether and how to open the browser on startup.\n  BrowserMode browser = 10;\n\n  // debug_mode specifies the debug mode to use.\n  DebugMode debug_mode = 11;\n\n  // Log level override.\n  optional string log_level = 12;\n\n  // scrub_sensitive_data, if true, scrubs sensitive data from local traces.\n  bool scrub_sensitive_data = 13;\n\n  enum BrowserMode {\n    BROWSER_AUTO = 0;\n    BROWSER_NEVER = 1;\n    BROWSER_ALWAYS = 2;\n  }\n\n  enum DebugMode {\n    DEBUG_DISABLED = 0;\n    DEBUG_ENABLED = 1;\n    DEBUG_BREAK = 2;\n  }\n}\n\nmessage TestRequest {\n  string app_root = 1;\n  string working_dir = 2;\n  repeated string args = 3;\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 4;\n\n  // No longer used; debug, if true, compiles the app with flags that improve the debugging experience.\n  reserved 5;\n\n  // trace_file, if set specifies a trace file to write trace information\n  // about the parse and compilation process to.\n  optional string trace_file = 6;\n\n  // codegen_debug, if true, dumps the generated code and prints where it is located.\n  bool codegen_debug = 7;\n\n  // temp_dir is a temp dir that will be cleaned up after tests have been executed\n  // to write things like app meta and runtime config etc.\n  string temp_dir = 8;\n}\n\nmessage TestSpecRequest {\n  string app_root = 1;\n  string working_dir = 2;\n  repeated string args = 3;\n\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 4;\n\n  // temp_dir is a temp dir that will be cleaned up after tests have been executed\n  // to write things like app meta and runtime config etc.\n  string temp_dir = 5;\n}\n\nmessage TestSpecResponse {\n  string command = 1;\n  repeated string args = 2;\n  repeated string environ = 3;\n}\n\nmessage ExecScriptRequest {\n  string app_root = 1;\n  string working_dir = 2;\n\n  repeated string script_args = 4;\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 5;\n\n  // trace_file, if set specifies a trace file to write trace information\n  // about the parse and compilation process to.\n  optional string trace_file = 6;\n\n  // namespace is the infrastructure namespace to use.\n  // If empty the active namespace is used.\n  optional string namespace = 7;\n}\n\nmessage ExecSpecRequest {\n  string app_root = 1;\n  string working_dir = 2;\n\n  repeated string script_args = 4;\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 5;\n\n  // namespace is the infrastructure namespace to use.\n  // If empty the active namespace is used.\n  optional string namespace = 7;\n\n  // temp_dir is a temp dir that will be cleaned up by the CLI after the command\n  // has been executed, to write things like app meta and runtime config etc.\n  string temp_dir = 8;\n}\n\nmessage ExecSpecMessage {\n  oneof msg {\n    CommandOutput output = 1;\n    ExecSpecResponse spec = 2;\n  }\n}\n\nmessage ExecSpecResponse {\n  string command = 1;\n  repeated string args = 2;\n  repeated string environ = 3;\n}\n\nmessage CheckRequest {\n  string app_root = 1;\n  string working_dir = 2;\n  // codegen_debug, if true, dumps the generated code and prints where it is located.\n  bool codegen_debug = 3;\n  // parse_tests, if true, exercises test parsing and codegen as well.\n  bool parse_tests = 4;\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 5;\n}\n\nmessage ExportRequest {\n  string app_root = 1;\n\n  // goos and goarch specify the platform configuration to compile\n  // the application for. The values must be valid GOOS/GOARCH values.\n  string goos = 2;\n  string goarch = 3;\n\n  // cgo_enabled specifies whether to build with cgo enabled.\n  // The host must have a valid C compiler for the target platform\n  // if true.\n  bool cgo_enabled = 4;\n\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 5;\n\n  oneof format {\n    // docker specifies to export the app as a docker image.\n    DockerExportParams docker = 6;\n  }\n\n  string infra_conf_path = 7;\n  repeated string services = 8;\n  repeated string gateways = 9;\n  bool skip_infra_conf = 10;\n\n  // A parent path to app_root containing the .git, or the same as app_root\n  string workspace_root = 11;\n}\n\nmessage DockerExportParams {\n  // local_daemon_tag specifies what to tag the image as\n  // in the local Docker daemon. If empty the export does not\n  // interact with (or require) the local docker daemon at all.\n  string local_daemon_tag = 1;\n\n  // push_destination_tag specifies the remote registry tag\n  // to push the exported image to. If empty the built image\n  // is not pushed anywhere.\n  string push_destination_tag = 2;\n\n  // base_image_tag is the base image to build the image from.\n  string base_image_tag = 3;\n}\n\nmessage DBConnectRequest {\n  string app_root = 1;\n  string db_name = 2;\n  string env_name = 3; // optional\n  DBClusterType cluster_type = 4;\n\n  // namespace is the infrastructure namespace to use.\n  // If empty the active namespace is used.\n  optional string namespace = 5;\n\n  DBRole role = 6;\n}\n\nenum DBRole {\n  DB_ROLE_UNSPECIFIED = 0;\n  DB_ROLE_SUPERUSER = 1;\n  DB_ROLE_ADMIN = 2;\n  DB_ROLE_WRITE = 3;\n  DB_ROLE_READ = 4;\n}\n\nenum DBClusterType {\n  DB_CLUSTER_TYPE_UNSPECIFIED = 0;\n  DB_CLUSTER_TYPE_RUN = 1;\n  DB_CLUSTER_TYPE_TEST = 2;\n  DB_CLUSTER_TYPE_SHADOW = 3;\n}\n\nmessage DBConnectResponse {\n  string dsn = 1;\n}\n\nmessage DBProxyRequest {\n  string app_root = 1;\n  string env_name = 2; // optional\n  int32 port = 3; // optional\n  DBClusterType cluster_type = 4;\n\n  // namespace is the infrastructure namespace to use.\n  // If empty the active namespace is used.\n  optional string namespace = 5;\n  DBRole role = 6;\n}\n\nmessage DBResetRequest {\n  string app_root = 1;\n  repeated string database_names = 2; // database names to reset\n  DBClusterType cluster_type = 3;\n\n  // namespace is the infrastructure namespace to use.\n  // If empty the active namespace is used.\n  optional string namespace = 4;\n}\n\nmessage GenClientRequest {\n  string app_id = 1;\n  string env_name = 2;\n  string lang = 3;\n  string filepath = 4;\n\n  // Services to include in the output.\n  // If the string \"*\" is present all services are included.\n  repeated string services = 5;\n\n  // Services to exclude from the output.\n  // Takes precedence over 'services' above.\n  repeated string excluded_services = 6;\n\n  // Tags of endpoints to include in the output.\n  // Only includes endpoints from services included in 'services' above.\n  repeated string endpoint_tags = 7;\n\n  // Tags of endpoints to exclude from the output.\n  // Takes precedence over 'endpoint_tags' above.\n  repeated string excluded_endpoint_tags = 8;\n\n  // The OpenAPI spec generator by default includes private endpoints.\n  // If this is set to `true`, private endpoints will not be included\n  // in the generated OpenAPI spec.\n  optional bool openapi_exclude_private_endpoints = 9;\n\n  // The TS generator by default re-declares the api types in the client.\n  // If this is set to `true`, the types will be imported and shared between\n  // the client and the server. It assumes \"~backend\" is available in the\n  // import path.\n  optional bool ts_shared_types = 10;\n\n  // If set, the default export of the generate TypeScript client will be\n  // an instantiated client with the given target. The target can be e.g.\n  // a variable, e.g. \"import.meta.env.VITE_CLIENT_TARGET\" or a string literal.\n  optional string ts_client_target = 11;\n\n  // The root directory of the app to generate a client for.\n  // Included to be able to handle multi clone scenarios.\n  string app_root = 12;\n}\n\nmessage GenClientResponse {\n  bytes code = 1;\n}\n\nmessage GenWrappersRequest {\n  string app_root = 1;\n}\n\nmessage GenWrappersResponse {}\n\nmessage SecretsRefreshRequest {\n  string app_root = 1;\n  string key = 2;\n  string value = 3;\n}\n\nmessage SecretsRefreshResponse {}\n\nmessage VersionResponse {\n  string version = 1;\n  string config_hash = 2;\n}\n\n// Namespaces\n\nmessage Namespace {\n  string id = 1;\n  string name = 2;\n  bool active = 3;\n  string created_at = 4;\n  optional string last_active_at = 5;\n}\n\nmessage CreateNamespaceRequest {\n  string app_root = 1;\n  string name = 2;\n}\n\nmessage SwitchNamespaceRequest {\n  string app_root = 1;\n  string name = 2;\n  bool create = 3;\n}\n\nmessage ListNamespacesRequest {\n  string app_root = 1;\n}\n\nmessage DeleteNamespaceRequest {\n  string app_root = 1;\n  string name = 2;\n}\n\nmessage ListNamespacesResponse {\n  repeated Namespace namespaces = 1;\n}\n\nmessage TelemetryConfig {\n  string anon_id = 1;\n  bool enabled = 2;\n  bool debug = 3;\n}\n\nmessage DumpMetaRequest {\n  string app_root = 1;\n  string working_dir = 2; // for error reporting\n\n  // environ is the environment to set for the running command.\n  // Each entry is a string in the format \"KEY=VALUE\", identical to os.Environ().\n  repeated string environ = 3;\n\n  // Whether or not to parse tests.\n  bool parse_tests = 4;\n\n  Format format = 5;\n\n  enum Format {\n    FORMAT_UNSPECIFIED = 0;\n    FORMAT_JSON = 1;\n    FORMAT_PROTO = 2;\n  }\n}\n\nmessage DumpMetaResponse {\n  bytes meta = 1;\n}\n\n// The following messages are used for sqlc plugin integration.\nmessage SQLCPlugin {\n  message File {\n    string name = 1 [json_name = \"name\"];\n    bytes contents = 2 [json_name = \"contents\"];\n  }\n\n  message Settings {\n    // Rename message was field 5\n    // Overides message was field 6\n    // PythonCode message was field 8\n    // KotlinCode message was field 9\n    // GoCode message was field 10;\n    // JSONCode message was field 11;\n    reserved 5, 8, 9, 10, 11;\n\n    string version = 1 [json_name = \"version\"];\n    string engine = 2 [json_name = \"engine\"];\n    repeated string schema = 3 [json_name = \"schema\"];\n    repeated string queries = 4 [json_name = \"queries\"];\n    Codegen codegen = 12 [json_name = \"codegen\"];\n  }\n\n  message Codegen {\n    message Process {\n      string cmd = 1;\n    }\n    message WASM {\n      string url = 1;\n      string sha256 = 2;\n    }\n    string out = 1 [json_name = \"out\"];\n    string plugin = 2 [json_name = \"plugin\"];\n    bytes options = 3 [json_name = \"options\"];\n    repeated string env = 4 [json_name = \"env\"];\n    Process process = 5 [json_name = \"process\"];\n    WASM wasm = 6 [json_name = \"wasm\"];\n  }\n\n  message Catalog {\n    string comment = 1;\n    string default_schema = 2;\n    string name = 3;\n    repeated Schema schemas = 4;\n  }\n\n  message Schema {\n    string comment = 1;\n    string name = 2;\n    repeated Table tables = 3;\n    repeated Enum enums = 4;\n    repeated CompositeType composite_types = 5;\n  }\n\n  message CompositeType {\n    string name = 1;\n    string comment = 2;\n  }\n\n  message Enum {\n    string name = 1;\n    repeated string vals = 2;\n    string comment = 3;\n  }\n\n  message Table {\n    Identifier rel = 1;\n    repeated Column columns = 2;\n    string comment = 3;\n  }\n\n  message Identifier {\n    string catalog = 1;\n    string schema = 2;\n    string name = 3;\n  }\n\n  message Column {\n    string name = 1;\n    bool not_null = 3;\n    bool is_array = 4;\n    string comment = 5;\n    int32 length = 6;\n    bool is_named_param = 7;\n    bool is_func_call = 8;\n\n    // XXX: Figure out what PostgreSQL calls `foo.id`\n    string scope = 9;\n    Identifier table = 10;\n    string table_alias = 11;\n    Identifier type = 12;\n    bool is_sqlc_slice = 13;\n    Identifier embed_table = 14;\n    string original_name = 15;\n    bool unsigned = 16;\n    int32 array_dims = 17;\n  }\n\n  message Query {\n    string text = 1 [json_name = \"text\"];\n    string name = 2 [json_name = \"name\"];\n    string cmd = 3 [json_name = \"cmd\"];\n    repeated Column columns = 4 [json_name = \"columns\"];\n    repeated Parameter params = 5 [json_name = \"parameters\"];\n    repeated string comments = 6 [json_name = \"comments\"];\n    string filename = 7 [json_name = \"filename\"];\n    Identifier insert_into_table = 8 [json_name = \"insert_into_table\"];\n  }\n\n  message Parameter {\n    int32 number = 1 [json_name = \"number\"];\n    Column column = 2 [json_name = \"column\"];\n  }\n\n  message GenerateRequest {\n    Settings settings = 1 [json_name = \"settings\"];\n    Catalog catalog = 2 [json_name = \"catalog\"];\n    repeated Query queries = 3 [json_name = \"queries\"];\n    string sqlc_version = 4 [json_name = \"sqlc_version\"];\n    bytes plugin_options = 5 [json_name = \"plugin_options\"];\n    bytes global_options = 6 [json_name = \"global_options\"];\n  }\n\n  message GenerateResponse {\n    repeated File files = 1 [json_name = \"files\"];\n  }\n}\n"
  },
  {
    "path": "proto/encore/daemon/daemon_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.5.1\n// - protoc             v6.32.1\n// source: encore/daemon/daemon.proto\n\npackage daemon\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tDaemon_Run_FullMethodName             = \"/encore.daemon.Daemon/Run\"\n\tDaemon_Test_FullMethodName            = \"/encore.daemon.Daemon/Test\"\n\tDaemon_TestSpec_FullMethodName        = \"/encore.daemon.Daemon/TestSpec\"\n\tDaemon_ExecScript_FullMethodName      = \"/encore.daemon.Daemon/ExecScript\"\n\tDaemon_ExecSpec_FullMethodName        = \"/encore.daemon.Daemon/ExecSpec\"\n\tDaemon_Check_FullMethodName           = \"/encore.daemon.Daemon/Check\"\n\tDaemon_Export_FullMethodName          = \"/encore.daemon.Daemon/Export\"\n\tDaemon_DBConnect_FullMethodName       = \"/encore.daemon.Daemon/DBConnect\"\n\tDaemon_DBProxy_FullMethodName         = \"/encore.daemon.Daemon/DBProxy\"\n\tDaemon_DBReset_FullMethodName         = \"/encore.daemon.Daemon/DBReset\"\n\tDaemon_GenClient_FullMethodName       = \"/encore.daemon.Daemon/GenClient\"\n\tDaemon_GenWrappers_FullMethodName     = \"/encore.daemon.Daemon/GenWrappers\"\n\tDaemon_SecretsRefresh_FullMethodName  = \"/encore.daemon.Daemon/SecretsRefresh\"\n\tDaemon_Version_FullMethodName         = \"/encore.daemon.Daemon/Version\"\n\tDaemon_CreateNamespace_FullMethodName = \"/encore.daemon.Daemon/CreateNamespace\"\n\tDaemon_SwitchNamespace_FullMethodName = \"/encore.daemon.Daemon/SwitchNamespace\"\n\tDaemon_ListNamespaces_FullMethodName  = \"/encore.daemon.Daemon/ListNamespaces\"\n\tDaemon_DeleteNamespace_FullMethodName = \"/encore.daemon.Daemon/DeleteNamespace\"\n\tDaemon_DumpMeta_FullMethodName        = \"/encore.daemon.Daemon/DumpMeta\"\n\tDaemon_Telemetry_FullMethodName       = \"/encore.daemon.Daemon/Telemetry\"\n\tDaemon_CreateApp_FullMethodName       = \"/encore.daemon.Daemon/CreateApp\"\n)\n\n// DaemonClient is the client API for Daemon service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype DaemonClient interface {\n\t// Run runs the application.\n\tRun(ctx context.Context, in *RunRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// Test runs tests.\n\tTest(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// TestSpec returns the specification for how to run tests.\n\tTestSpec(ctx context.Context, in *TestSpecRequest, opts ...grpc.CallOption) (*TestSpecResponse, error)\n\t// ExecScript executes a one-off script.\n\tExecScript(ctx context.Context, in *ExecScriptRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// ExecSpec returns the specification for how to run an exec command.\n\t// It streams progress messages during setup, then sends the spec as the final message.\n\tExecSpec(ctx context.Context, in *ExecSpecRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSpecMessage], error)\n\t// Check checks the app for compilation errors.\n\tCheck(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// Export exports the app in various formats.\n\tExport(ctx context.Context, in *ExportRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// DBConnect starts the database and returns the DSN for connecting to it.\n\tDBConnect(ctx context.Context, in *DBConnectRequest, opts ...grpc.CallOption) (*DBConnectResponse, error)\n\t// DBProxy starts a local database proxy for connecting to remote databases\n\t// on the encore.dev platform.\n\tDBProxy(ctx context.Context, in *DBProxyRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// DBReset resets the given databases, recreating them from scratch.\n\tDBReset(ctx context.Context, in *DBResetRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error)\n\t// GenClient generates a client based on the app's API.\n\tGenClient(ctx context.Context, in *GenClientRequest, opts ...grpc.CallOption) (*GenClientResponse, error)\n\t// GenWrappers generates user-facing wrapper code.\n\tGenWrappers(ctx context.Context, in *GenWrappersRequest, opts ...grpc.CallOption) (*GenWrappersResponse, error)\n\t// SecretsRefresh tells the daemon to refresh the local development secrets\n\t// for the given application.\n\tSecretsRefresh(ctx context.Context, in *SecretsRefreshRequest, opts ...grpc.CallOption) (*SecretsRefreshResponse, error)\n\t// Version reports the daemon version.\n\tVersion(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error)\n\t// CreateNamespace creates a new infra namespace.\n\tCreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error)\n\t// SwitchNamespace switches the active infra namespace.\n\tSwitchNamespace(ctx context.Context, in *SwitchNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error)\n\t// ListNamespaces lists all namespaces for the given app.\n\tListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error)\n\t// DeleteNamespace deletes an infra namespace.\n\tDeleteNamespace(ctx context.Context, in *DeleteNamespaceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tDumpMeta(ctx context.Context, in *DumpMetaRequest, opts ...grpc.CallOption) (*DumpMetaResponse, error)\n\t// Telemetry enables or disables telemetry.\n\tTelemetry(ctx context.Context, in *TelemetryConfig, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\t// InitTutorial sets the tutorial flag of the app\n\tCreateApp(ctx context.Context, in *CreateAppRequest, opts ...grpc.CallOption) (*CreateAppResponse, error)\n}\n\ntype daemonClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewDaemonClient(cc grpc.ClientConnInterface) DaemonClient {\n\treturn &daemonClient{cc}\n}\n\nfunc (c *daemonClient) Run(ctx context.Context, in *RunRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[0], Daemon_Run_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[RunRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_RunClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) Test(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[1], Daemon_Test_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[TestRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_TestClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) TestSpec(ctx context.Context, in *TestSpecRequest, opts ...grpc.CallOption) (*TestSpecResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(TestSpecResponse)\n\terr := c.cc.Invoke(ctx, Daemon_TestSpec_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) ExecScript(ctx context.Context, in *ExecScriptRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[2], Daemon_ExecScript_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[ExecScriptRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_ExecScriptClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) ExecSpec(ctx context.Context, in *ExecSpecRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecSpecMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[3], Daemon_ExecSpec_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[ExecSpecRequest, ExecSpecMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_ExecSpecClient = grpc.ServerStreamingClient[ExecSpecMessage]\n\nfunc (c *daemonClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[4], Daemon_Check_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[CheckRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_CheckClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) Export(ctx context.Context, in *ExportRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[5], Daemon_Export_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[ExportRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_ExportClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) DBConnect(ctx context.Context, in *DBConnectRequest, opts ...grpc.CallOption) (*DBConnectResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DBConnectResponse)\n\terr := c.cc.Invoke(ctx, Daemon_DBConnect_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) DBProxy(ctx context.Context, in *DBProxyRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[6], Daemon_DBProxy_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[DBProxyRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_DBProxyClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) DBReset(ctx context.Context, in *DBResetRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[CommandMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Daemon_ServiceDesc.Streams[7], Daemon_DBReset_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[DBResetRequest, CommandMessage]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_DBResetClient = grpc.ServerStreamingClient[CommandMessage]\n\nfunc (c *daemonClient) GenClient(ctx context.Context, in *GenClientRequest, opts ...grpc.CallOption) (*GenClientResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GenClientResponse)\n\terr := c.cc.Invoke(ctx, Daemon_GenClient_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) GenWrappers(ctx context.Context, in *GenWrappersRequest, opts ...grpc.CallOption) (*GenWrappersResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GenWrappersResponse)\n\terr := c.cc.Invoke(ctx, Daemon_GenWrappers_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) SecretsRefresh(ctx context.Context, in *SecretsRefreshRequest, opts ...grpc.CallOption) (*SecretsRefreshResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(SecretsRefreshResponse)\n\terr := c.cc.Invoke(ctx, Daemon_SecretsRefresh_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) Version(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*VersionResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(VersionResponse)\n\terr := c.cc.Invoke(ctx, Daemon_Version_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) CreateNamespace(ctx context.Context, in *CreateNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Namespace)\n\terr := c.cc.Invoke(ctx, Daemon_CreateNamespace_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) SwitchNamespace(ctx context.Context, in *SwitchNamespaceRequest, opts ...grpc.CallOption) (*Namespace, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Namespace)\n\terr := c.cc.Invoke(ctx, Daemon_SwitchNamespace_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListNamespacesResponse)\n\terr := c.cc.Invoke(ctx, Daemon_ListNamespaces_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) DeleteNamespace(ctx context.Context, in *DeleteNamespaceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Daemon_DeleteNamespace_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) DumpMeta(ctx context.Context, in *DumpMetaRequest, opts ...grpc.CallOption) (*DumpMetaResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(DumpMetaResponse)\n\terr := c.cc.Invoke(ctx, Daemon_DumpMeta_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) Telemetry(ctx context.Context, in *TelemetryConfig, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Daemon_Telemetry_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *daemonClient) CreateApp(ctx context.Context, in *CreateAppRequest, opts ...grpc.CallOption) (*CreateAppResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(CreateAppResponse)\n\terr := c.cc.Invoke(ctx, Daemon_CreateApp_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// DaemonServer is the server API for Daemon service.\n// All implementations must embed UnimplementedDaemonServer\n// for forward compatibility.\ntype DaemonServer interface {\n\t// Run runs the application.\n\tRun(*RunRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// Test runs tests.\n\tTest(*TestRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// TestSpec returns the specification for how to run tests.\n\tTestSpec(context.Context, *TestSpecRequest) (*TestSpecResponse, error)\n\t// ExecScript executes a one-off script.\n\tExecScript(*ExecScriptRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// ExecSpec returns the specification for how to run an exec command.\n\t// It streams progress messages during setup, then sends the spec as the final message.\n\tExecSpec(*ExecSpecRequest, grpc.ServerStreamingServer[ExecSpecMessage]) error\n\t// Check checks the app for compilation errors.\n\tCheck(*CheckRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// Export exports the app in various formats.\n\tExport(*ExportRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// DBConnect starts the database and returns the DSN for connecting to it.\n\tDBConnect(context.Context, *DBConnectRequest) (*DBConnectResponse, error)\n\t// DBProxy starts a local database proxy for connecting to remote databases\n\t// on the encore.dev platform.\n\tDBProxy(*DBProxyRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// DBReset resets the given databases, recreating them from scratch.\n\tDBReset(*DBResetRequest, grpc.ServerStreamingServer[CommandMessage]) error\n\t// GenClient generates a client based on the app's API.\n\tGenClient(context.Context, *GenClientRequest) (*GenClientResponse, error)\n\t// GenWrappers generates user-facing wrapper code.\n\tGenWrappers(context.Context, *GenWrappersRequest) (*GenWrappersResponse, error)\n\t// SecretsRefresh tells the daemon to refresh the local development secrets\n\t// for the given application.\n\tSecretsRefresh(context.Context, *SecretsRefreshRequest) (*SecretsRefreshResponse, error)\n\t// Version reports the daemon version.\n\tVersion(context.Context, *emptypb.Empty) (*VersionResponse, error)\n\t// CreateNamespace creates a new infra namespace.\n\tCreateNamespace(context.Context, *CreateNamespaceRequest) (*Namespace, error)\n\t// SwitchNamespace switches the active infra namespace.\n\tSwitchNamespace(context.Context, *SwitchNamespaceRequest) (*Namespace, error)\n\t// ListNamespaces lists all namespaces for the given app.\n\tListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error)\n\t// DeleteNamespace deletes an infra namespace.\n\tDeleteNamespace(context.Context, *DeleteNamespaceRequest) (*emptypb.Empty, error)\n\tDumpMeta(context.Context, *DumpMetaRequest) (*DumpMetaResponse, error)\n\t// Telemetry enables or disables telemetry.\n\tTelemetry(context.Context, *TelemetryConfig) (*emptypb.Empty, error)\n\t// InitTutorial sets the tutorial flag of the app\n\tCreateApp(context.Context, *CreateAppRequest) (*CreateAppResponse, error)\n\tmustEmbedUnimplementedDaemonServer()\n}\n\n// UnimplementedDaemonServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedDaemonServer struct{}\n\nfunc (UnimplementedDaemonServer) Run(*RunRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Run not implemented\")\n}\nfunc (UnimplementedDaemonServer) Test(*TestRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Test not implemented\")\n}\nfunc (UnimplementedDaemonServer) TestSpec(context.Context, *TestSpecRequest) (*TestSpecResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method TestSpec not implemented\")\n}\nfunc (UnimplementedDaemonServer) ExecScript(*ExecScriptRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method ExecScript not implemented\")\n}\nfunc (UnimplementedDaemonServer) ExecSpec(*ExecSpecRequest, grpc.ServerStreamingServer[ExecSpecMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method ExecSpec not implemented\")\n}\nfunc (UnimplementedDaemonServer) Check(*CheckRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Check not implemented\")\n}\nfunc (UnimplementedDaemonServer) Export(*ExportRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Export not implemented\")\n}\nfunc (UnimplementedDaemonServer) DBConnect(context.Context, *DBConnectRequest) (*DBConnectResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method DBConnect not implemented\")\n}\nfunc (UnimplementedDaemonServer) DBProxy(*DBProxyRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method DBProxy not implemented\")\n}\nfunc (UnimplementedDaemonServer) DBReset(*DBResetRequest, grpc.ServerStreamingServer[CommandMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method DBReset not implemented\")\n}\nfunc (UnimplementedDaemonServer) GenClient(context.Context, *GenClientRequest) (*GenClientResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method GenClient not implemented\")\n}\nfunc (UnimplementedDaemonServer) GenWrappers(context.Context, *GenWrappersRequest) (*GenWrappersResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method GenWrappers not implemented\")\n}\nfunc (UnimplementedDaemonServer) SecretsRefresh(context.Context, *SecretsRefreshRequest) (*SecretsRefreshResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SecretsRefresh not implemented\")\n}\nfunc (UnimplementedDaemonServer) Version(context.Context, *emptypb.Empty) (*VersionResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Version not implemented\")\n}\nfunc (UnimplementedDaemonServer) CreateNamespace(context.Context, *CreateNamespaceRequest) (*Namespace, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method CreateNamespace not implemented\")\n}\nfunc (UnimplementedDaemonServer) SwitchNamespace(context.Context, *SwitchNamespaceRequest) (*Namespace, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SwitchNamespace not implemented\")\n}\nfunc (UnimplementedDaemonServer) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method ListNamespaces not implemented\")\n}\nfunc (UnimplementedDaemonServer) DeleteNamespace(context.Context, *DeleteNamespaceRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method DeleteNamespace not implemented\")\n}\nfunc (UnimplementedDaemonServer) DumpMeta(context.Context, *DumpMetaRequest) (*DumpMetaResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method DumpMeta not implemented\")\n}\nfunc (UnimplementedDaemonServer) Telemetry(context.Context, *TelemetryConfig) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Telemetry not implemented\")\n}\nfunc (UnimplementedDaemonServer) CreateApp(context.Context, *CreateAppRequest) (*CreateAppResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method CreateApp not implemented\")\n}\nfunc (UnimplementedDaemonServer) mustEmbedUnimplementedDaemonServer() {}\nfunc (UnimplementedDaemonServer) testEmbeddedByValue()                {}\n\n// UnsafeDaemonServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to DaemonServer will\n// result in compilation errors.\ntype UnsafeDaemonServer interface {\n\tmustEmbedUnimplementedDaemonServer()\n}\n\nfunc RegisterDaemonServer(s grpc.ServiceRegistrar, srv DaemonServer) {\n\t// If the following call pancis, it indicates UnimplementedDaemonServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Daemon_ServiceDesc, srv)\n}\n\nfunc _Daemon_Run_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(RunRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).Run(m, &grpc.GenericServerStream[RunRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_RunServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_Test_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(TestRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).Test(m, &grpc.GenericServerStream[TestRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_TestServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_TestSpec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(TestSpecRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).TestSpec(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_TestSpec_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).TestSpec(ctx, req.(*TestSpecRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_ExecScript_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(ExecScriptRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).ExecScript(m, &grpc.GenericServerStream[ExecScriptRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_ExecScriptServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_ExecSpec_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(ExecSpecRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).ExecSpec(m, &grpc.GenericServerStream[ExecSpecRequest, ExecSpecMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_ExecSpecServer = grpc.ServerStreamingServer[ExecSpecMessage]\n\nfunc _Daemon_Check_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(CheckRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).Check(m, &grpc.GenericServerStream[CheckRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_CheckServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_Export_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(ExportRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).Export(m, &grpc.GenericServerStream[ExportRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_ExportServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_DBConnect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DBConnectRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).DBConnect(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_DBConnect_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).DBConnect(ctx, req.(*DBConnectRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_DBProxy_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(DBProxyRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).DBProxy(m, &grpc.GenericServerStream[DBProxyRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_DBProxyServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_DBReset_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(DBResetRequest)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DaemonServer).DBReset(m, &grpc.GenericServerStream[DBResetRequest, CommandMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Daemon_DBResetServer = grpc.ServerStreamingServer[CommandMessage]\n\nfunc _Daemon_GenClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GenClientRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).GenClient(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_GenClient_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).GenClient(ctx, req.(*GenClientRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_GenWrappers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(GenWrappersRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).GenWrappers(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_GenWrappers_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).GenWrappers(ctx, req.(*GenWrappersRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_SecretsRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SecretsRefreshRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).SecretsRefresh(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_SecretsRefresh_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).SecretsRefresh(ctx, req.(*SecretsRefreshRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).Version(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_Version_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).Version(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_CreateNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateNamespaceRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).CreateNamespace(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_CreateNamespace_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).CreateNamespace(ctx, req.(*CreateNamespaceRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_SwitchNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SwitchNamespaceRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).SwitchNamespace(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_SwitchNamespace_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).SwitchNamespace(ctx, req.(*SwitchNamespaceRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_ListNamespaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ListNamespacesRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).ListNamespaces(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_ListNamespaces_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).ListNamespaces(ctx, req.(*ListNamespacesRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_DeleteNamespace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteNamespaceRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).DeleteNamespace(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_DeleteNamespace_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).DeleteNamespace(ctx, req.(*DeleteNamespaceRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_DumpMeta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DumpMetaRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).DumpMeta(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_DumpMeta_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).DumpMeta(ctx, req.(*DumpMetaRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_Telemetry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(TelemetryConfig)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).Telemetry(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_Telemetry_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).Telemetry(ctx, req.(*TelemetryConfig))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Daemon_CreateApp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateAppRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DaemonServer).CreateApp(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Daemon_CreateApp_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DaemonServer).CreateApp(ctx, req.(*CreateAppRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Daemon_ServiceDesc is the grpc.ServiceDesc for Daemon service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Daemon_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"encore.daemon.Daemon\",\n\tHandlerType: (*DaemonServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"TestSpec\",\n\t\t\tHandler:    _Daemon_TestSpec_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DBConnect\",\n\t\t\tHandler:    _Daemon_DBConnect_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GenClient\",\n\t\t\tHandler:    _Daemon_GenClient_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GenWrappers\",\n\t\t\tHandler:    _Daemon_GenWrappers_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SecretsRefresh\",\n\t\t\tHandler:    _Daemon_SecretsRefresh_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Version\",\n\t\t\tHandler:    _Daemon_Version_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateNamespace\",\n\t\t\tHandler:    _Daemon_CreateNamespace_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SwitchNamespace\",\n\t\t\tHandler:    _Daemon_SwitchNamespace_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListNamespaces\",\n\t\t\tHandler:    _Daemon_ListNamespaces_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteNamespace\",\n\t\t\tHandler:    _Daemon_DeleteNamespace_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DumpMeta\",\n\t\t\tHandler:    _Daemon_DumpMeta_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Telemetry\",\n\t\t\tHandler:    _Daemon_Telemetry_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateApp\",\n\t\t\tHandler:    _Daemon_CreateApp_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"Run\",\n\t\t\tHandler:       _Daemon_Run_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"Test\",\n\t\t\tHandler:       _Daemon_Test_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"ExecScript\",\n\t\t\tHandler:       _Daemon_ExecScript_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"ExecSpec\",\n\t\t\tHandler:       _Daemon_ExecSpec_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"Check\",\n\t\t\tHandler:       _Daemon_Check_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"Export\",\n\t\t\tHandler:       _Daemon_Export_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"DBProxy\",\n\t\t\tHandler:       _Daemon_DBProxy_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"DBReset\",\n\t\t\tHandler:       _Daemon_DBReset_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t},\n\tMetadata: \"encore/daemon/daemon.proto\",\n}\n"
  },
  {
    "path": "proto/encore/engine/trace/trace.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/engine/trace/trace.proto\n\npackage trace\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype HTTPTraceEventCode int32\n\nconst (\n\tHTTPTraceEventCode_UNKNOWN                 HTTPTraceEventCode = 0\n\tHTTPTraceEventCode_GET_CONN                HTTPTraceEventCode = 1\n\tHTTPTraceEventCode_GOT_CONN                HTTPTraceEventCode = 2\n\tHTTPTraceEventCode_GOT_FIRST_RESPONSE_BYTE HTTPTraceEventCode = 3\n\tHTTPTraceEventCode_GOT_1XX_RESPONSE        HTTPTraceEventCode = 4\n\tHTTPTraceEventCode_DNS_START               HTTPTraceEventCode = 5\n\tHTTPTraceEventCode_DNS_DONE                HTTPTraceEventCode = 6\n\tHTTPTraceEventCode_CONNECT_START           HTTPTraceEventCode = 7\n\tHTTPTraceEventCode_CONNECT_DONE            HTTPTraceEventCode = 8\n\tHTTPTraceEventCode_TLS_HANDSHAKE_START     HTTPTraceEventCode = 9\n\tHTTPTraceEventCode_TLS_HANDSHAKE_DONE      HTTPTraceEventCode = 10\n\tHTTPTraceEventCode_WROTE_HEADERS           HTTPTraceEventCode = 11\n\tHTTPTraceEventCode_WROTE_REQUEST           HTTPTraceEventCode = 12\n\tHTTPTraceEventCode_WAIT_100_CONTINUE       HTTPTraceEventCode = 13\n)\n\n// Enum value maps for HTTPTraceEventCode.\nvar (\n\tHTTPTraceEventCode_name = map[int32]string{\n\t\t0:  \"UNKNOWN\",\n\t\t1:  \"GET_CONN\",\n\t\t2:  \"GOT_CONN\",\n\t\t3:  \"GOT_FIRST_RESPONSE_BYTE\",\n\t\t4:  \"GOT_1XX_RESPONSE\",\n\t\t5:  \"DNS_START\",\n\t\t6:  \"DNS_DONE\",\n\t\t7:  \"CONNECT_START\",\n\t\t8:  \"CONNECT_DONE\",\n\t\t9:  \"TLS_HANDSHAKE_START\",\n\t\t10: \"TLS_HANDSHAKE_DONE\",\n\t\t11: \"WROTE_HEADERS\",\n\t\t12: \"WROTE_REQUEST\",\n\t\t13: \"WAIT_100_CONTINUE\",\n\t}\n\tHTTPTraceEventCode_value = map[string]int32{\n\t\t\"UNKNOWN\":                 0,\n\t\t\"GET_CONN\":                1,\n\t\t\"GOT_CONN\":                2,\n\t\t\"GOT_FIRST_RESPONSE_BYTE\": 3,\n\t\t\"GOT_1XX_RESPONSE\":        4,\n\t\t\"DNS_START\":               5,\n\t\t\"DNS_DONE\":                6,\n\t\t\"CONNECT_START\":           7,\n\t\t\"CONNECT_DONE\":            8,\n\t\t\"TLS_HANDSHAKE_START\":     9,\n\t\t\"TLS_HANDSHAKE_DONE\":      10,\n\t\t\"WROTE_HEADERS\":           11,\n\t\t\"WROTE_REQUEST\":           12,\n\t\t\"WAIT_100_CONTINUE\":       13,\n\t}\n)\n\nfunc (x HTTPTraceEventCode) Enum() *HTTPTraceEventCode {\n\tp := new(HTTPTraceEventCode)\n\t*p = x\n\treturn p\n}\n\nfunc (x HTTPTraceEventCode) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (HTTPTraceEventCode) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace_trace_proto_enumTypes[0].Descriptor()\n}\n\nfunc (HTTPTraceEventCode) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace_trace_proto_enumTypes[0]\n}\n\nfunc (x HTTPTraceEventCode) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use HTTPTraceEventCode.Descriptor instead.\nfunc (HTTPTraceEventCode) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{0}\n}\n\ntype Request_Type int32\n\nconst (\n\tRequest_RPC        Request_Type = 0\n\tRequest_AUTH       Request_Type = 1\n\tRequest_PUBSUB_MSG Request_Type = 2\n)\n\n// Enum value maps for Request_Type.\nvar (\n\tRequest_Type_name = map[int32]string{\n\t\t0: \"RPC\",\n\t\t1: \"AUTH\",\n\t\t2: \"PUBSUB_MSG\",\n\t}\n\tRequest_Type_value = map[string]int32{\n\t\t\"RPC\":        0,\n\t\t\"AUTH\":       1,\n\t\t\"PUBSUB_MSG\": 2,\n\t}\n)\n\nfunc (x Request_Type) Enum() *Request_Type {\n\tp := new(Request_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x Request_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Request_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace_trace_proto_enumTypes[1].Descriptor()\n}\n\nfunc (Request_Type) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace_trace_proto_enumTypes[1]\n}\n\nfunc (x Request_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Request_Type.Descriptor instead.\nfunc (Request_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{1, 0}\n}\n\ntype DBTransaction_CompletionType int32\n\nconst (\n\tDBTransaction_ROLLBACK DBTransaction_CompletionType = 0\n\tDBTransaction_COMMIT   DBTransaction_CompletionType = 1\n)\n\n// Enum value maps for DBTransaction_CompletionType.\nvar (\n\tDBTransaction_CompletionType_name = map[int32]string{\n\t\t0: \"ROLLBACK\",\n\t\t1: \"COMMIT\",\n\t}\n\tDBTransaction_CompletionType_value = map[string]int32{\n\t\t\"ROLLBACK\": 0,\n\t\t\"COMMIT\":   1,\n\t}\n)\n\nfunc (x DBTransaction_CompletionType) Enum() *DBTransaction_CompletionType {\n\tp := new(DBTransaction_CompletionType)\n\t*p = x\n\treturn p\n}\n\nfunc (x DBTransaction_CompletionType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (DBTransaction_CompletionType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace_trace_proto_enumTypes[2].Descriptor()\n}\n\nfunc (DBTransaction_CompletionType) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace_trace_proto_enumTypes[2]\n}\n\nfunc (x DBTransaction_CompletionType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use DBTransaction_CompletionType.Descriptor instead.\nfunc (DBTransaction_CompletionType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{5, 0}\n}\n\ntype CacheOp_Result int32\n\nconst (\n\tCacheOp_UNKNOWN     CacheOp_Result = 0\n\tCacheOp_OK          CacheOp_Result = 1\n\tCacheOp_NO_SUCH_KEY CacheOp_Result = 2\n\tCacheOp_CONFLICT    CacheOp_Result = 3\n\tCacheOp_ERR         CacheOp_Result = 4\n)\n\n// Enum value maps for CacheOp_Result.\nvar (\n\tCacheOp_Result_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"OK\",\n\t\t2: \"NO_SUCH_KEY\",\n\t\t3: \"CONFLICT\",\n\t\t4: \"ERR\",\n\t}\n\tCacheOp_Result_value = map[string]int32{\n\t\t\"UNKNOWN\":     0,\n\t\t\"OK\":          1,\n\t\t\"NO_SUCH_KEY\": 2,\n\t\t\"CONFLICT\":    3,\n\t\t\"ERR\":         4,\n\t}\n)\n\nfunc (x CacheOp_Result) Enum() *CacheOp_Result {\n\tp := new(CacheOp_Result)\n\t*p = x\n\treturn p\n}\n\nfunc (x CacheOp_Result) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (CacheOp_Result) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace_trace_proto_enumTypes[3].Descriptor()\n}\n\nfunc (CacheOp_Result) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace_trace_proto_enumTypes[3]\n}\n\nfunc (x CacheOp_Result) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use CacheOp_Result.Descriptor instead.\nfunc (CacheOp_Result) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{9, 0}\n}\n\n// Note: These values don't match the values used by the binary trace protocol,\n// as these values are stored in persisted traces and therefore must maintain\n// backwards compatibility. The binary trace protocol is versioned and doesn't\n// have the same limitations.\ntype LogMessage_Level int32\n\nconst (\n\tLogMessage_DEBUG LogMessage_Level = 0\n\tLogMessage_INFO  LogMessage_Level = 1\n\tLogMessage_ERROR LogMessage_Level = 2\n\tLogMessage_WARN  LogMessage_Level = 3\n\tLogMessage_TRACE LogMessage_Level = 4\n)\n\n// Enum value maps for LogMessage_Level.\nvar (\n\tLogMessage_Level_name = map[int32]string{\n\t\t0: \"DEBUG\",\n\t\t1: \"INFO\",\n\t\t2: \"ERROR\",\n\t\t3: \"WARN\",\n\t\t4: \"TRACE\",\n\t}\n\tLogMessage_Level_value = map[string]int32{\n\t\t\"DEBUG\": 0,\n\t\t\"INFO\":  1,\n\t\t\"ERROR\": 2,\n\t\t\"WARN\":  3,\n\t\t\"TRACE\": 4,\n\t}\n)\n\nfunc (x LogMessage_Level) Enum() *LogMessage_Level {\n\tp := new(LogMessage_Level)\n\t*p = x\n\treturn p\n}\n\nfunc (x LogMessage_Level) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (LogMessage_Level) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace_trace_proto_enumTypes[4].Descriptor()\n}\n\nfunc (LogMessage_Level) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace_trace_proto_enumTypes[4]\n}\n\nfunc (x LogMessage_Level) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use LogMessage_Level.Descriptor instead.\nfunc (LogMessage_Level) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{23, 0}\n}\n\ntype TraceID struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHigh          uint64                 `protobuf:\"varint,1,opt,name=high,proto3\" json:\"high,omitempty\"`\n\tLow           uint64                 `protobuf:\"varint,2,opt,name=low,proto3\" json:\"low,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TraceID) Reset() {\n\t*x = TraceID{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TraceID) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TraceID) ProtoMessage() {}\n\nfunc (x *TraceID) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TraceID.ProtoReflect.Descriptor instead.\nfunc (*TraceID) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *TraceID) GetHigh() uint64 {\n\tif x != nil {\n\t\treturn x.High\n\t}\n\treturn 0\n}\n\nfunc (x *TraceID) GetLow() uint64 {\n\tif x != nil {\n\t\treturn x.Low\n\t}\n\treturn 0\n}\n\ntype Request struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTraceId       *TraceID               `protobuf:\"bytes,1,opt,name=trace_id,json=traceId,proto3\" json:\"trace_id,omitempty\"`\n\tSpanId        uint64                 `protobuf:\"varint,2,opt,name=span_id,json=spanId,proto3\" json:\"span_id,omitempty\"`\n\tParentSpanId  uint64                 `protobuf:\"varint,3,opt,name=parent_span_id,json=parentSpanId,proto3\" json:\"parent_span_id,omitempty\"`\n\tParentTraceId *TraceID               `protobuf:\"bytes,32,opt,name=parent_trace_id,json=parentTraceId,proto3\" json:\"parent_trace_id,omitempty\"`\n\tGoid          uint32                 `protobuf:\"varint,4,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,5,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,6,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tDefLoc        int32                  `protobuf:\"varint,8,opt,name=def_loc,json=defLoc,proto3\" json:\"def_loc,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,11,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tEvents        []*Event               `protobuf:\"bytes,12,rep,name=events,proto3\" json:\"events,omitempty\"`\n\tType          Request_Type           `protobuf:\"varint,14,opt,name=type,proto3,enum=encore.engine.trace.Request_Type\" json:\"type,omitempty\"`\n\tErrStack      *StackTrace            `protobuf:\"bytes,15,opt,name=err_stack,json=errStack,proto3\" json:\"err_stack,omitempty\"`       // null if unavailable\n\tPanicStack    *StackTrace            `protobuf:\"bytes,34,opt,name=panic_stack,json=panicStack,proto3\" json:\"panic_stack,omitempty\"` // null if unavailable\n\t// abs_start_time is the absolute unix timestamp\n\t// (in nanosecond resolution) of when the request started.\n\tAbsStartTime uint64 `protobuf:\"varint,16,opt,name=abs_start_time,json=absStartTime,proto3\" json:\"abs_start_time,omitempty\"`\n\tServiceName  string `protobuf:\"bytes,17,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tEndpointName string `protobuf:\"bytes,18,opt,name=endpoint_name,json=endpointName,proto3\" json:\"endpoint_name,omitempty\"`\n\t// Fields set if Type == PUBSUB_MSG\n\tTopicName        string `protobuf:\"bytes,19,opt,name=topic_name,json=topicName,proto3\" json:\"topic_name,omitempty\"`\n\tSubscriptionName string `protobuf:\"bytes,20,opt,name=subscription_name,json=subscriptionName,proto3\" json:\"subscription_name,omitempty\"`\n\tMessageId        string `protobuf:\"bytes,21,opt,name=message_id,json=messageId,proto3\" json:\"message_id,omitempty\"`\n\tAttempt          uint32 `protobuf:\"varint,22,opt,name=attempt,proto3\" json:\"attempt,omitempty\"`\n\tPublishTime      uint64 `protobuf:\"varint,23,opt,name=publish_time,json=publishTime,proto3\" json:\"publish_time,omitempty\"`\n\t// Fields set if Type == RPC or AUTH\n\tInputs             [][]byte          `protobuf:\"bytes,9,rep,name=inputs,proto3\" json:\"inputs,omitempty\"`    // Deprecated: use request_payload and path_params\n\tOutputs            [][]byte          `protobuf:\"bytes,10,rep,name=outputs,proto3\" json:\"outputs,omitempty\"` // Deprecated: use response_payload and uid\n\tUid                string            `protobuf:\"bytes,13,opt,name=uid,proto3\" json:\"uid,omitempty\"`\n\tHttpMethod         string            `protobuf:\"bytes,24,opt,name=http_method,json=httpMethod,proto3\" json:\"http_method,omitempty\"`\n\tPath               string            `protobuf:\"bytes,25,opt,name=path,proto3\" json:\"path,omitempty\"`\n\tPathParams         []string          `protobuf:\"bytes,26,rep,name=path_params,json=pathParams,proto3\" json:\"path_params,omitempty\"`\n\tRequestPayload     []byte            `protobuf:\"bytes,27,opt,name=request_payload,json=requestPayload,proto3\" json:\"request_payload,omitempty\"`\n\tResponsePayload    []byte            `protobuf:\"bytes,28,opt,name=response_payload,json=responsePayload,proto3\" json:\"response_payload,omitempty\"`\n\tRawRequestHeaders  map[string]string `protobuf:\"bytes,29,rep,name=raw_request_headers,json=rawRequestHeaders,proto3\" json:\"raw_request_headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tRawResponseHeaders map[string]string `protobuf:\"bytes,30,rep,name=raw_response_headers,json=rawResponseHeaders,proto3\" json:\"raw_response_headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\t// external_request_id is the value of the X-Request-ID header.\n\tExternalRequestId     string `protobuf:\"bytes,31,opt,name=external_request_id,json=externalRequestId,proto3\" json:\"external_request_id,omitempty\"`\n\tExternalCorrelationId string `protobuf:\"bytes,33,opt,name=external_correlation_id,json=externalCorrelationId,proto3\" json:\"external_correlation_id,omitempty\"`\n\tunknownFields         protoimpl.UnknownFields\n\tsizeCache             protoimpl.SizeCache\n}\n\nfunc (x *Request) Reset() {\n\t*x = Request{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Request) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Request) ProtoMessage() {}\n\nfunc (x *Request) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Request.ProtoReflect.Descriptor instead.\nfunc (*Request) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Request) GetTraceId() *TraceID {\n\tif x != nil {\n\t\treturn x.TraceId\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.SpanId\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetParentSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.ParentSpanId\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetParentTraceId() *TraceID {\n\tif x != nil {\n\t\treturn x.ParentTraceId\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetDefLoc() int32 {\n\tif x != nil {\n\t\treturn x.DefLoc\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetEvents() []*Event {\n\tif x != nil {\n\t\treturn x.Events\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetType() Request_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn Request_RPC\n}\n\nfunc (x *Request) GetErrStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.ErrStack\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetPanicStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.PanicStack\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetAbsStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.AbsStartTime\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetEndpointName() string {\n\tif x != nil {\n\t\treturn x.EndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetTopicName() string {\n\tif x != nil {\n\t\treturn x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetSubscriptionName() string {\n\tif x != nil {\n\t\treturn x.SubscriptionName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetMessageId() string {\n\tif x != nil {\n\t\treturn x.MessageId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetAttempt() uint32 {\n\tif x != nil {\n\t\treturn x.Attempt\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetPublishTime() uint64 {\n\tif x != nil {\n\t\treturn x.PublishTime\n\t}\n\treturn 0\n}\n\nfunc (x *Request) GetInputs() [][]byte {\n\tif x != nil {\n\t\treturn x.Inputs\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetOutputs() [][]byte {\n\tif x != nil {\n\t\treturn x.Outputs\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetUid() string {\n\tif x != nil {\n\t\treturn x.Uid\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetHttpMethod() string {\n\tif x != nil {\n\t\treturn x.HttpMethod\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetPath() string {\n\tif x != nil {\n\t\treturn x.Path\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetPathParams() []string {\n\tif x != nil {\n\t\treturn x.PathParams\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetRequestPayload() []byte {\n\tif x != nil {\n\t\treturn x.RequestPayload\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetResponsePayload() []byte {\n\tif x != nil {\n\t\treturn x.ResponsePayload\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetRawRequestHeaders() map[string]string {\n\tif x != nil {\n\t\treturn x.RawRequestHeaders\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetRawResponseHeaders() map[string]string {\n\tif x != nil {\n\t\treturn x.RawResponseHeaders\n\t}\n\treturn nil\n}\n\nfunc (x *Request) GetExternalRequestId() string {\n\tif x != nil {\n\t\treturn x.ExternalRequestId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Request) GetExternalCorrelationId() string {\n\tif x != nil {\n\t\treturn x.ExternalCorrelationId\n\t}\n\treturn \"\"\n}\n\ntype Event struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Data:\n\t//\n\t//\t*Event_Rpc\n\t//\t*Event_Tx\n\t//\t*Event_Query\n\t//\t*Event_Goroutine\n\t//\t*Event_Http\n\t//\t*Event_Log\n\t//\t*Event_PublishedMsg\n\t//\t*Event_ServiceInit\n\t//\t*Event_Cache\n\t//\t*Event_BodyStream\n\tData          isEvent_Data `protobuf_oneof:\"data\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Event) Reset() {\n\t*x = Event{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Event) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Event) ProtoMessage() {}\n\nfunc (x *Event) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Event.ProtoReflect.Descriptor instead.\nfunc (*Event) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Event) GetData() isEvent_Data {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetRpc() *RPCCall {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Rpc); ok {\n\t\t\treturn x.Rpc\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetTx() *DBTransaction {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Tx); ok {\n\t\t\treturn x.Tx\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetQuery() *DBQuery {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Query); ok {\n\t\t\treturn x.Query\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetGoroutine() *Goroutine {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Goroutine); ok {\n\t\t\treturn x.Goroutine\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetHttp() *HTTPCall {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Http); ok {\n\t\t\treturn x.Http\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetLog() *LogMessage {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Log); ok {\n\t\t\treturn x.Log\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetPublishedMsg() *PubsubMsgPublished {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_PublishedMsg); ok {\n\t\t\treturn x.PublishedMsg\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetServiceInit() *ServiceInit {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_ServiceInit); ok {\n\t\t\treturn x.ServiceInit\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetCache() *CacheOp {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_Cache); ok {\n\t\t\treturn x.Cache\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetBodyStream() *BodyStream {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*Event_BodyStream); ok {\n\t\t\treturn x.BodyStream\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isEvent_Data interface {\n\tisEvent_Data()\n}\n\ntype Event_Rpc struct {\n\tRpc *RPCCall `protobuf:\"bytes,1,opt,name=rpc,proto3,oneof\"`\n}\n\ntype Event_Tx struct {\n\tTx *DBTransaction `protobuf:\"bytes,2,opt,name=tx,proto3,oneof\"`\n}\n\ntype Event_Query struct {\n\tQuery *DBQuery `protobuf:\"bytes,3,opt,name=query,proto3,oneof\"`\n}\n\ntype Event_Goroutine struct {\n\tGoroutine *Goroutine `protobuf:\"bytes,4,opt,name=goroutine,proto3,oneof\"`\n}\n\ntype Event_Http struct {\n\tHttp *HTTPCall `protobuf:\"bytes,5,opt,name=http,proto3,oneof\"`\n}\n\ntype Event_Log struct {\n\tLog *LogMessage `protobuf:\"bytes,6,opt,name=log,proto3,oneof\"`\n}\n\ntype Event_PublishedMsg struct {\n\tPublishedMsg *PubsubMsgPublished `protobuf:\"bytes,7,opt,name=publishedMsg,proto3,oneof\"`\n}\n\ntype Event_ServiceInit struct {\n\tServiceInit *ServiceInit `protobuf:\"bytes,8,opt,name=service_init,json=serviceInit,proto3,oneof\"`\n}\n\ntype Event_Cache struct {\n\tCache *CacheOp `protobuf:\"bytes,9,opt,name=cache,proto3,oneof\"`\n}\n\ntype Event_BodyStream struct {\n\tBodyStream *BodyStream `protobuf:\"bytes,10,opt,name=body_stream,json=bodyStream,proto3,oneof\"`\n}\n\nfunc (*Event_Rpc) isEvent_Data() {}\n\nfunc (*Event_Tx) isEvent_Data() {}\n\nfunc (*Event_Query) isEvent_Data() {}\n\nfunc (*Event_Goroutine) isEvent_Data() {}\n\nfunc (*Event_Http) isEvent_Data() {}\n\nfunc (*Event_Log) isEvent_Data() {}\n\nfunc (*Event_PublishedMsg) isEvent_Data() {}\n\nfunc (*Event_ServiceInit) isEvent_Data() {}\n\nfunc (*Event_Cache) isEvent_Data() {}\n\nfunc (*Event_BodyStream) isEvent_Data() {}\n\ntype RPCCall struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSpanId        uint64                 `protobuf:\"varint,1,opt,name=span_id,json=spanId,proto3\" json:\"span_id,omitempty\"`\n\tGoid          uint32                 `protobuf:\"varint,2,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tDefLoc        int32                  `protobuf:\"varint,4,opt,name=def_loc,json=defLoc,proto3\" json:\"def_loc,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,5,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,6,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,7,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,8,opt,name=stack,proto3\" json:\"stack,omitempty\"` // where it was called (null if unavailable)\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPCCall) Reset() {\n\t*x = RPCCall{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPCCall) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPCCall) ProtoMessage() {}\n\nfunc (x *RPCCall) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPCCall.ProtoReflect.Descriptor instead.\nfunc (*RPCCall) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *RPCCall) GetSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.SpanId\n\t}\n\treturn 0\n}\n\nfunc (x *RPCCall) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *RPCCall) GetDefLoc() int32 {\n\tif x != nil {\n\t\treturn x.DefLoc\n\t}\n\treturn 0\n}\n\nfunc (x *RPCCall) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *RPCCall) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *RPCCall) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *RPCCall) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype Goroutine struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid          uint32                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,3,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Goroutine) Reset() {\n\t*x = Goroutine{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Goroutine) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Goroutine) ProtoMessage() {}\n\nfunc (x *Goroutine) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Goroutine.ProtoReflect.Descriptor instead.\nfunc (*Goroutine) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *Goroutine) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *Goroutine) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *Goroutine) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\ntype DBTransaction struct {\n\tstate         protoimpl.MessageState       `protogen:\"open.v1\"`\n\tGoid          uint32                       `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tStartTime     uint64                       `protobuf:\"varint,4,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                       `protobuf:\"varint,5,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tErr           []byte                       `protobuf:\"bytes,6,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tCompletion    DBTransaction_CompletionType `protobuf:\"varint,7,opt,name=completion,proto3,enum=encore.engine.trace.DBTransaction_CompletionType\" json:\"completion,omitempty\"`\n\tQueries       []*DBQuery                   `protobuf:\"bytes,8,rep,name=queries,proto3\" json:\"queries,omitempty\"`\n\tBeginStack    *StackTrace                  `protobuf:\"bytes,9,opt,name=begin_stack,json=beginStack,proto3\" json:\"begin_stack,omitempty\"` // null if unavailable\n\tEndStack      *StackTrace                  `protobuf:\"bytes,10,opt,name=end_stack,json=endStack,proto3\" json:\"end_stack,omitempty\"`      // null if unavailable\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBTransaction) Reset() {\n\t*x = DBTransaction{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBTransaction) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBTransaction) ProtoMessage() {}\n\nfunc (x *DBTransaction) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBTransaction.ProtoReflect.Descriptor instead.\nfunc (*DBTransaction) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *DBTransaction) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *DBTransaction) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *DBTransaction) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *DBTransaction) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *DBTransaction) GetCompletion() DBTransaction_CompletionType {\n\tif x != nil {\n\t\treturn x.Completion\n\t}\n\treturn DBTransaction_ROLLBACK\n}\n\nfunc (x *DBTransaction) GetQueries() []*DBQuery {\n\tif x != nil {\n\t\treturn x.Queries\n\t}\n\treturn nil\n}\n\nfunc (x *DBTransaction) GetBeginStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.BeginStack\n\t}\n\treturn nil\n}\n\nfunc (x *DBTransaction) GetEndStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.EndStack\n\t}\n\treturn nil\n}\n\ntype DBQuery struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid          uint32                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,3,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tQuery         []byte                 `protobuf:\"bytes,5,opt,name=query,proto3\" json:\"query,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,6,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,7,opt,name=stack,proto3\" json:\"stack,omitempty\"` // null if unavailable\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBQuery) Reset() {\n\t*x = DBQuery{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBQuery) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBQuery) ProtoMessage() {}\n\nfunc (x *DBQuery) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBQuery.ProtoReflect.Descriptor instead.\nfunc (*DBQuery) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *DBQuery) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *DBQuery) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *DBQuery) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *DBQuery) GetQuery() []byte {\n\tif x != nil {\n\t\treturn x.Query\n\t}\n\treturn nil\n}\n\nfunc (x *DBQuery) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *DBQuery) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype PubsubMsgPublished struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid          uint64                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,3,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tTopic         string                 `protobuf:\"bytes,5,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tMessage       []byte                 `protobuf:\"bytes,6,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tMessageId     string                 `protobuf:\"bytes,7,opt,name=message_id,json=messageId,proto3\" json:\"message_id,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,8,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,9,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubsubMsgPublished) Reset() {\n\t*x = PubsubMsgPublished{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubsubMsgPublished) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubsubMsgPublished) ProtoMessage() {}\n\nfunc (x *PubsubMsgPublished) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubsubMsgPublished.ProtoReflect.Descriptor instead.\nfunc (*PubsubMsgPublished) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *PubsubMsgPublished) GetGoid() uint64 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *PubsubMsgPublished) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *PubsubMsgPublished) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *PubsubMsgPublished) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMsgPublished) GetMessage() []byte {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn nil\n}\n\nfunc (x *PubsubMsgPublished) GetMessageId() string {\n\tif x != nil {\n\t\treturn x.MessageId\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMsgPublished) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *PubsubMsgPublished) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype ServiceInit struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid          uint64                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tDefLoc        int32                  `protobuf:\"varint,2,opt,name=def_loc,json=defLoc,proto3\" json:\"def_loc,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,3,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tService       string                 `protobuf:\"bytes,5,opt,name=service,proto3\" json:\"service,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,6,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tErrStack      *StackTrace            `protobuf:\"bytes,7,opt,name=err_stack,json=errStack,proto3\" json:\"err_stack,omitempty\"` // null if not an error\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceInit) Reset() {\n\t*x = ServiceInit{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceInit) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceInit) ProtoMessage() {}\n\nfunc (x *ServiceInit) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceInit.ProtoReflect.Descriptor instead.\nfunc (*ServiceInit) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *ServiceInit) GetGoid() uint64 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *ServiceInit) GetDefLoc() int32 {\n\tif x != nil {\n\t\treturn x.DefLoc\n\t}\n\treturn 0\n}\n\nfunc (x *ServiceInit) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *ServiceInit) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *ServiceInit) GetService() string {\n\tif x != nil {\n\t\treturn x.Service\n\t}\n\treturn \"\"\n}\n\nfunc (x *ServiceInit) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *ServiceInit) GetErrStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.ErrStack\n\t}\n\treturn nil\n}\n\ntype CacheOp struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid          uint32                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tDefLoc        int32                  `protobuf:\"varint,2,opt,name=def_loc,json=defLoc,proto3\" json:\"def_loc,omitempty\"`\n\tStartTime     uint64                 `protobuf:\"varint,3,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime       uint64                 `protobuf:\"varint,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tOperation     string                 `protobuf:\"bytes,5,opt,name=operation,proto3\" json:\"operation,omitempty\"`\n\tKeys          []string               `protobuf:\"bytes,6,rep,name=keys,proto3\" json:\"keys,omitempty\"`\n\tInputs        [][]byte               `protobuf:\"bytes,7,rep,name=inputs,proto3\" json:\"inputs,omitempty\"`\n\tOutputs       [][]byte               `protobuf:\"bytes,8,rep,name=outputs,proto3\" json:\"outputs,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,9,opt,name=stack,proto3\" json:\"stack,omitempty\"` // null if unavailable\n\tErr           []byte                 `protobuf:\"bytes,10,opt,name=err,proto3\" json:\"err,omitempty\"`    // set iff result == ERR\n\tWrite         bool                   `protobuf:\"varint,11,opt,name=write,proto3\" json:\"write,omitempty\"`\n\tResult        CacheOp_Result         `protobuf:\"varint,12,opt,name=result,proto3,enum=encore.engine.trace.CacheOp_Result\" json:\"result,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CacheOp) Reset() {\n\t*x = CacheOp{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CacheOp) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CacheOp) ProtoMessage() {}\n\nfunc (x *CacheOp) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CacheOp.ProtoReflect.Descriptor instead.\nfunc (*CacheOp) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *CacheOp) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *CacheOp) GetDefLoc() int32 {\n\tif x != nil {\n\t\treturn x.DefLoc\n\t}\n\treturn 0\n}\n\nfunc (x *CacheOp) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *CacheOp) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *CacheOp) GetOperation() string {\n\tif x != nil {\n\t\treturn x.Operation\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheOp) GetKeys() []string {\n\tif x != nil {\n\t\treturn x.Keys\n\t}\n\treturn nil\n}\n\nfunc (x *CacheOp) GetInputs() [][]byte {\n\tif x != nil {\n\t\treturn x.Inputs\n\t}\n\treturn nil\n}\n\nfunc (x *CacheOp) GetOutputs() [][]byte {\n\tif x != nil {\n\t\treturn x.Outputs\n\t}\n\treturn nil\n}\n\nfunc (x *CacheOp) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\nfunc (x *CacheOp) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *CacheOp) GetWrite() bool {\n\tif x != nil {\n\t\treturn x.Write\n\t}\n\treturn false\n}\n\nfunc (x *CacheOp) GetResult() CacheOp_Result {\n\tif x != nil {\n\t\treturn x.Result\n\t}\n\treturn CacheOp_UNKNOWN\n}\n\ntype BodyStream struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIsResponse    bool                   `protobuf:\"varint,1,opt,name=is_response,json=isResponse,proto3\" json:\"is_response,omitempty\"`\n\tOverflowed    bool                   `protobuf:\"varint,2,opt,name=overflowed,proto3\" json:\"overflowed,omitempty\"`\n\tData          []byte                 `protobuf:\"bytes,3,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BodyStream) Reset() {\n\t*x = BodyStream{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BodyStream) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BodyStream) ProtoMessage() {}\n\nfunc (x *BodyStream) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BodyStream.ProtoReflect.Descriptor instead.\nfunc (*BodyStream) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *BodyStream) GetIsResponse() bool {\n\tif x != nil {\n\t\treturn x.IsResponse\n\t}\n\treturn false\n}\n\nfunc (x *BodyStream) GetOverflowed() bool {\n\tif x != nil {\n\t\treturn x.Overflowed\n\t}\n\treturn false\n}\n\nfunc (x *BodyStream) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype HTTPCall struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tSpanId         uint64                 `protobuf:\"varint,1,opt,name=span_id,json=spanId,proto3\" json:\"span_id,omitempty\"`\n\tGoid           uint32                 `protobuf:\"varint,2,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tStartTime      uint64                 `protobuf:\"varint,3,opt,name=start_time,json=startTime,proto3\" json:\"start_time,omitempty\"`\n\tEndTime        uint64                 `protobuf:\"varint,4,opt,name=end_time,json=endTime,proto3\" json:\"end_time,omitempty\"`\n\tMethod         string                 `protobuf:\"bytes,5,opt,name=method,proto3\" json:\"method,omitempty\"`\n\tUrl            string                 `protobuf:\"bytes,6,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tStatusCode     uint32                 `protobuf:\"varint,7,opt,name=status_code,json=statusCode,proto3\" json:\"status_code,omitempty\"`\n\tErr            []byte                 `protobuf:\"bytes,8,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tBodyClosedTime uint64                 `protobuf:\"varint,9,opt,name=body_closed_time,json=bodyClosedTime,proto3\" json:\"body_closed_time,omitempty\"`\n\tEvents         []*HTTPTraceEvent      `protobuf:\"bytes,10,rep,name=events,proto3\" json:\"events,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *HTTPCall) Reset() {\n\t*x = HTTPCall{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPCall) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPCall) ProtoMessage() {}\n\nfunc (x *HTTPCall) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPCall.ProtoReflect.Descriptor instead.\nfunc (*HTTPCall) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *HTTPCall) GetSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.SpanId\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCall) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCall) GetStartTime() uint64 {\n\tif x != nil {\n\t\treturn x.StartTime\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCall) GetEndTime() uint64 {\n\tif x != nil {\n\t\treturn x.EndTime\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCall) GetMethod() string {\n\tif x != nil {\n\t\treturn x.Method\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPCall) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPCall) GetStatusCode() uint32 {\n\tif x != nil {\n\t\treturn x.StatusCode\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCall) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPCall) GetBodyClosedTime() uint64 {\n\tif x != nil {\n\t\treturn x.BodyClosedTime\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCall) GetEvents() []*HTTPTraceEvent {\n\tif x != nil {\n\t\treturn x.Events\n\t}\n\treturn nil\n}\n\ntype HTTPTraceEvent struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tCode  HTTPTraceEventCode     `protobuf:\"varint,1,opt,name=code,proto3,enum=encore.engine.trace.HTTPTraceEventCode\" json:\"code,omitempty\"`\n\tTime  uint64                 `protobuf:\"varint,2,opt,name=time,proto3\" json:\"time,omitempty\"`\n\t// Types that are valid to be assigned to Data:\n\t//\n\t//\t*HTTPTraceEvent_GetConn\n\t//\t*HTTPTraceEvent_GotConn\n\t//\t*HTTPTraceEvent_Got_1XxResponse\n\t//\t*HTTPTraceEvent_DnsStart\n\t//\t*HTTPTraceEvent_DnsDone\n\t//\t*HTTPTraceEvent_ConnectStart\n\t//\t*HTTPTraceEvent_ConnectDone\n\t//\t*HTTPTraceEvent_TlsHandshakeDone\n\t//\t*HTTPTraceEvent_WroteRequest\n\tData          isHTTPTraceEvent_Data `protobuf_oneof:\"data\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPTraceEvent) Reset() {\n\t*x = HTTPTraceEvent{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPTraceEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPTraceEvent) ProtoMessage() {}\n\nfunc (x *HTTPTraceEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPTraceEvent.ProtoReflect.Descriptor instead.\nfunc (*HTTPTraceEvent) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *HTTPTraceEvent) GetCode() HTTPTraceEventCode {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn HTTPTraceEventCode_UNKNOWN\n}\n\nfunc (x *HTTPTraceEvent) GetTime() uint64 {\n\tif x != nil {\n\t\treturn x.Time\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPTraceEvent) GetData() isHTTPTraceEvent_Data {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGetConn() *HTTPGetConnData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_GetConn); ok {\n\t\t\treturn x.GetConn\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGotConn() *HTTPGotConnData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_GotConn); ok {\n\t\t\treturn x.GotConn\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGot_1XxResponse() *HTTPGot1XxResponseData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_Got_1XxResponse); ok {\n\t\t\treturn x.Got_1XxResponse\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetDnsStart() *HTTPDNSStartData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_DnsStart); ok {\n\t\t\treturn x.DnsStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetDnsDone() *HTTPDNSDoneData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_DnsDone); ok {\n\t\t\treturn x.DnsDone\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetConnectStart() *HTTPConnectStartData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_ConnectStart); ok {\n\t\t\treturn x.ConnectStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetConnectDone() *HTTPConnectDoneData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_ConnectDone); ok {\n\t\t\treturn x.ConnectDone\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetTlsHandshakeDone() *HTTPTLSHandshakeDoneData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_TlsHandshakeDone); ok {\n\t\t\treturn x.TlsHandshakeDone\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetWroteRequest() *HTTPWroteRequestData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_WroteRequest); ok {\n\t\t\treturn x.WroteRequest\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isHTTPTraceEvent_Data interface {\n\tisHTTPTraceEvent_Data()\n}\n\ntype HTTPTraceEvent_GetConn struct {\n\tGetConn *HTTPGetConnData `protobuf:\"bytes,3,opt,name=get_conn,json=getConn,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_GotConn struct {\n\tGotConn *HTTPGotConnData `protobuf:\"bytes,4,opt,name=got_conn,json=gotConn,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_Got_1XxResponse struct {\n\tGot_1XxResponse *HTTPGot1XxResponseData `protobuf:\"bytes,5,opt,name=got_1xx_response,json=got1xxResponse,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_DnsStart struct {\n\tDnsStart *HTTPDNSStartData `protobuf:\"bytes,6,opt,name=dns_start,json=dnsStart,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_DnsDone struct {\n\tDnsDone *HTTPDNSDoneData `protobuf:\"bytes,7,opt,name=dns_done,json=dnsDone,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_ConnectStart struct {\n\tConnectStart *HTTPConnectStartData `protobuf:\"bytes,8,opt,name=connect_start,json=connectStart,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_ConnectDone struct {\n\tConnectDone *HTTPConnectDoneData `protobuf:\"bytes,9,opt,name=connect_done,json=connectDone,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_TlsHandshakeDone struct {\n\tTlsHandshakeDone *HTTPTLSHandshakeDoneData `protobuf:\"bytes,10,opt,name=tls_handshake_done,json=tlsHandshakeDone,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_WroteRequest struct {\n\tWroteRequest *HTTPWroteRequestData `protobuf:\"bytes,11,opt,name=wrote_request,json=wroteRequest,proto3,oneof\"`\n}\n\nfunc (*HTTPTraceEvent_GetConn) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_GotConn) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_Got_1XxResponse) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_DnsStart) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_DnsDone) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_ConnectStart) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_ConnectDone) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_TlsHandshakeDone) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_WroteRequest) isHTTPTraceEvent_Data() {}\n\ntype HTTPGetConnData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHostPort      string                 `protobuf:\"bytes,1,opt,name=host_port,json=hostPort,proto3\" json:\"host_port,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPGetConnData) Reset() {\n\t*x = HTTPGetConnData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGetConnData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGetConnData) ProtoMessage() {}\n\nfunc (x *HTTPGetConnData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGetConnData.ProtoReflect.Descriptor instead.\nfunc (*HTTPGetConnData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *HTTPGetConnData) GetHostPort() string {\n\tif x != nil {\n\t\treturn x.HostPort\n\t}\n\treturn \"\"\n}\n\ntype HTTPGotConnData struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tReused         bool                   `protobuf:\"varint,1,opt,name=reused,proto3\" json:\"reused,omitempty\"`\n\tWasIdle        bool                   `protobuf:\"varint,2,opt,name=was_idle,json=wasIdle,proto3\" json:\"was_idle,omitempty\"`\n\tIdleDurationNs int64                  `protobuf:\"varint,3,opt,name=idle_duration_ns,json=idleDurationNs,proto3\" json:\"idle_duration_ns,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *HTTPGotConnData) Reset() {\n\t*x = HTTPGotConnData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGotConnData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGotConnData) ProtoMessage() {}\n\nfunc (x *HTTPGotConnData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGotConnData.ProtoReflect.Descriptor instead.\nfunc (*HTTPGotConnData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *HTTPGotConnData) GetReused() bool {\n\tif x != nil {\n\t\treturn x.Reused\n\t}\n\treturn false\n}\n\nfunc (x *HTTPGotConnData) GetWasIdle() bool {\n\tif x != nil {\n\t\treturn x.WasIdle\n\t}\n\treturn false\n}\n\nfunc (x *HTTPGotConnData) GetIdleDurationNs() int64 {\n\tif x != nil {\n\t\treturn x.IdleDurationNs\n\t}\n\treturn 0\n}\n\ntype HTTPGot1XxResponseData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCode          int32                  `protobuf:\"varint,1,opt,name=code,proto3\" json:\"code,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPGot1XxResponseData) Reset() {\n\t*x = HTTPGot1XxResponseData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGot1XxResponseData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGot1XxResponseData) ProtoMessage() {}\n\nfunc (x *HTTPGot1XxResponseData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGot1XxResponseData.ProtoReflect.Descriptor instead.\nfunc (*HTTPGot1XxResponseData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *HTTPGot1XxResponseData) GetCode() int32 {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn 0\n}\n\ntype HTTPDNSStartData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHost          string                 `protobuf:\"bytes,1,opt,name=host,proto3\" json:\"host,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPDNSStartData) Reset() {\n\t*x = HTTPDNSStartData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPDNSStartData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPDNSStartData) ProtoMessage() {}\n\nfunc (x *HTTPDNSStartData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPDNSStartData.ProtoReflect.Descriptor instead.\nfunc (*HTTPDNSStartData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *HTTPDNSStartData) GetHost() string {\n\tif x != nil {\n\t\treturn x.Host\n\t}\n\treturn \"\"\n}\n\ntype HTTPDNSDoneData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           []byte                 `protobuf:\"bytes,1,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tAddrs         []*DNSAddr             `protobuf:\"bytes,2,rep,name=addrs,proto3\" json:\"addrs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPDNSDoneData) Reset() {\n\t*x = HTTPDNSDoneData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPDNSDoneData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPDNSDoneData) ProtoMessage() {}\n\nfunc (x *HTTPDNSDoneData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPDNSDoneData.ProtoReflect.Descriptor instead.\nfunc (*HTTPDNSDoneData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *HTTPDNSDoneData) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPDNSDoneData) GetAddrs() []*DNSAddr {\n\tif x != nil {\n\t\treturn x.Addrs\n\t}\n\treturn nil\n}\n\ntype DNSAddr struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIp            []byte                 `protobuf:\"bytes,1,opt,name=ip,proto3\" json:\"ip,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DNSAddr) Reset() {\n\t*x = DNSAddr{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DNSAddr) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DNSAddr) ProtoMessage() {}\n\nfunc (x *DNSAddr) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DNSAddr.ProtoReflect.Descriptor instead.\nfunc (*DNSAddr) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *DNSAddr) GetIp() []byte {\n\tif x != nil {\n\t\treturn x.Ip\n\t}\n\treturn nil\n}\n\ntype HTTPConnectStartData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNetwork       string                 `protobuf:\"bytes,1,opt,name=network,proto3\" json:\"network,omitempty\"`\n\tAddr          string                 `protobuf:\"bytes,2,opt,name=addr,proto3\" json:\"addr,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPConnectStartData) Reset() {\n\t*x = HTTPConnectStartData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPConnectStartData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPConnectStartData) ProtoMessage() {}\n\nfunc (x *HTTPConnectStartData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPConnectStartData.ProtoReflect.Descriptor instead.\nfunc (*HTTPConnectStartData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *HTTPConnectStartData) GetNetwork() string {\n\tif x != nil {\n\t\treturn x.Network\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPConnectStartData) GetAddr() string {\n\tif x != nil {\n\t\treturn x.Addr\n\t}\n\treturn \"\"\n}\n\ntype HTTPConnectDoneData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNetwork       string                 `protobuf:\"bytes,1,opt,name=network,proto3\" json:\"network,omitempty\"`\n\tAddr          string                 `protobuf:\"bytes,2,opt,name=addr,proto3\" json:\"addr,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,3,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPConnectDoneData) Reset() {\n\t*x = HTTPConnectDoneData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPConnectDoneData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPConnectDoneData) ProtoMessage() {}\n\nfunc (x *HTTPConnectDoneData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPConnectDoneData.ProtoReflect.Descriptor instead.\nfunc (*HTTPConnectDoneData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *HTTPConnectDoneData) GetNetwork() string {\n\tif x != nil {\n\t\treturn x.Network\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPConnectDoneData) GetAddr() string {\n\tif x != nil {\n\t\treturn x.Addr\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPConnectDoneData) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype HTTPTLSHandshakeDoneData struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr                []byte                 `protobuf:\"bytes,1,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tTlsVersion         uint32                 `protobuf:\"varint,2,opt,name=tls_version,json=tlsVersion,proto3\" json:\"tls_version,omitempty\"`\n\tCipherSuite        uint32                 `protobuf:\"varint,3,opt,name=cipher_suite,json=cipherSuite,proto3\" json:\"cipher_suite,omitempty\"`\n\tServerName         string                 `protobuf:\"bytes,4,opt,name=server_name,json=serverName,proto3\" json:\"server_name,omitempty\"`\n\tNegotiatedProtocol string                 `protobuf:\"bytes,5,opt,name=negotiated_protocol,json=negotiatedProtocol,proto3\" json:\"negotiated_protocol,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) Reset() {\n\t*x = HTTPTLSHandshakeDoneData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPTLSHandshakeDoneData) ProtoMessage() {}\n\nfunc (x *HTTPTLSHandshakeDoneData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPTLSHandshakeDoneData.ProtoReflect.Descriptor instead.\nfunc (*HTTPTLSHandshakeDoneData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) GetTlsVersion() uint32 {\n\tif x != nil {\n\t\treturn x.TlsVersion\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) GetCipherSuite() uint32 {\n\tif x != nil {\n\t\treturn x.CipherSuite\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) GetServerName() string {\n\tif x != nil {\n\t\treturn x.ServerName\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPTLSHandshakeDoneData) GetNegotiatedProtocol() string {\n\tif x != nil {\n\t\treturn x.NegotiatedProtocol\n\t}\n\treturn \"\"\n}\n\ntype HTTPWroteRequestData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           []byte                 `protobuf:\"bytes,1,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPWroteRequestData) Reset() {\n\t*x = HTTPWroteRequestData{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPWroteRequestData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPWroteRequestData) ProtoMessage() {}\n\nfunc (x *HTTPWroteRequestData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPWroteRequestData.ProtoReflect.Descriptor instead.\nfunc (*HTTPWroteRequestData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *HTTPWroteRequestData) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype LogMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSpanId        uint64                 `protobuf:\"varint,1,opt,name=span_id,json=spanId,proto3\" json:\"span_id,omitempty\"`\n\tGoid          uint32                 `protobuf:\"varint,2,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tTime          uint64                 `protobuf:\"varint,3,opt,name=time,proto3\" json:\"time,omitempty\"`\n\tLevel         LogMessage_Level       `protobuf:\"varint,4,opt,name=level,proto3,enum=encore.engine.trace.LogMessage_Level\" json:\"level,omitempty\"`\n\tMsg           string                 `protobuf:\"bytes,5,opt,name=msg,proto3\" json:\"msg,omitempty\"`\n\tFields        []*LogField            `protobuf:\"bytes,6,rep,name=fields,proto3\" json:\"fields,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,7,opt,name=stack,proto3\" json:\"stack,omitempty\"` // null if unavailable\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LogMessage) Reset() {\n\t*x = LogMessage{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LogMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LogMessage) ProtoMessage() {}\n\nfunc (x *LogMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LogMessage.ProtoReflect.Descriptor instead.\nfunc (*LogMessage) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *LogMessage) GetSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.SpanId\n\t}\n\treturn 0\n}\n\nfunc (x *LogMessage) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *LogMessage) GetTime() uint64 {\n\tif x != nil {\n\t\treturn x.Time\n\t}\n\treturn 0\n}\n\nfunc (x *LogMessage) GetLevel() LogMessage_Level {\n\tif x != nil {\n\t\treturn x.Level\n\t}\n\treturn LogMessage_DEBUG\n}\n\nfunc (x *LogMessage) GetMsg() string {\n\tif x != nil {\n\t\treturn x.Msg\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogMessage) GetFields() []*LogField {\n\tif x != nil {\n\t\treturn x.Fields\n\t}\n\treturn nil\n}\n\nfunc (x *LogMessage) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype LogField struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey   string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*LogField_ErrorWithoutStack\n\t//\t*LogField_ErrorWithStack\n\t//\t*LogField_Str\n\t//\t*LogField_Bool\n\t//\t*LogField_Time\n\t//\t*LogField_Dur\n\t//\t*LogField_Uuid\n\t//\t*LogField_Json\n\t//\t*LogField_Int\n\t//\t*LogField_Uint\n\t//\t*LogField_Float32\n\t//\t*LogField_Float64\n\tValue         isLogField_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LogField) Reset() {\n\t*x = LogField{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LogField) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LogField) ProtoMessage() {}\n\nfunc (x *LogField) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LogField.ProtoReflect.Descriptor instead.\nfunc (*LogField) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{24}\n}\n\nfunc (x *LogField) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogField) GetValue() isLogField_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetErrorWithoutStack() string {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_ErrorWithoutStack); ok {\n\t\t\treturn x.ErrorWithoutStack\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogField) GetErrorWithStack() *ErrWithStack {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_ErrorWithStack); ok {\n\t\t\treturn x.ErrorWithStack\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetStr() string {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Str); ok {\n\t\t\treturn x.Str\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogField) GetBool() bool {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Bool); ok {\n\t\t\treturn x.Bool\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (x *LogField) GetTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Time); ok {\n\t\t\treturn x.Time\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetDur() int64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Dur); ok {\n\t\t\treturn x.Dur\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetUuid() []byte {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Uuid); ok {\n\t\t\treturn x.Uuid\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetJson() []byte {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Json); ok {\n\t\t\treturn x.Json\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetInt() int64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Int); ok {\n\t\t\treturn x.Int\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetUint() uint64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Uint); ok {\n\t\t\treturn x.Uint\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetFloat32() float32 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Float32); ok {\n\t\t\treturn x.Float32\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetFloat64() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Float64); ok {\n\t\t\treturn x.Float64\n\t\t}\n\t}\n\treturn 0\n}\n\ntype isLogField_Value interface {\n\tisLogField_Value()\n}\n\ntype LogField_ErrorWithoutStack struct {\n\tErrorWithoutStack string `protobuf:\"bytes,2,opt,name=error_without_stack,json=errorWithoutStack,proto3,oneof\"` // deprecated: use error_with_stack\n}\n\ntype LogField_ErrorWithStack struct {\n\tErrorWithStack *ErrWithStack `protobuf:\"bytes,13,opt,name=error_with_stack,json=errorWithStack,proto3,oneof\"`\n}\n\ntype LogField_Str struct {\n\tStr string `protobuf:\"bytes,3,opt,name=str,proto3,oneof\"`\n}\n\ntype LogField_Bool struct {\n\tBool bool `protobuf:\"varint,4,opt,name=bool,proto3,oneof\"`\n}\n\ntype LogField_Time struct {\n\tTime *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=time,proto3,oneof\"`\n}\n\ntype LogField_Dur struct {\n\tDur int64 `protobuf:\"varint,6,opt,name=dur,proto3,oneof\"`\n}\n\ntype LogField_Uuid struct {\n\tUuid []byte `protobuf:\"bytes,7,opt,name=uuid,proto3,oneof\"`\n}\n\ntype LogField_Json struct {\n\tJson []byte `protobuf:\"bytes,8,opt,name=json,proto3,oneof\"`\n}\n\ntype LogField_Int struct {\n\tInt int64 `protobuf:\"varint,9,opt,name=int,proto3,oneof\"`\n}\n\ntype LogField_Uint struct {\n\tUint uint64 `protobuf:\"varint,10,opt,name=uint,proto3,oneof\"`\n}\n\ntype LogField_Float32 struct {\n\tFloat32 float32 `protobuf:\"fixed32,11,opt,name=float32,proto3,oneof\"`\n}\n\ntype LogField_Float64 struct {\n\tFloat64 float64 `protobuf:\"fixed64,12,opt,name=float64,proto3,oneof\"`\n}\n\nfunc (*LogField_ErrorWithoutStack) isLogField_Value() {}\n\nfunc (*LogField_ErrorWithStack) isLogField_Value() {}\n\nfunc (*LogField_Str) isLogField_Value() {}\n\nfunc (*LogField_Bool) isLogField_Value() {}\n\nfunc (*LogField_Time) isLogField_Value() {}\n\nfunc (*LogField_Dur) isLogField_Value() {}\n\nfunc (*LogField_Uuid) isLogField_Value() {}\n\nfunc (*LogField_Json) isLogField_Value() {}\n\nfunc (*LogField_Int) isLogField_Value() {}\n\nfunc (*LogField_Uint) isLogField_Value() {}\n\nfunc (*LogField_Float32) isLogField_Value() {}\n\nfunc (*LogField_Float64) isLogField_Value() {}\n\ntype ErrWithStack struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tError         string                 `protobuf:\"bytes,1,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,2,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ErrWithStack) Reset() {\n\t*x = ErrWithStack{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ErrWithStack) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ErrWithStack) ProtoMessage() {}\n\nfunc (x *ErrWithStack) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ErrWithStack.ProtoReflect.Descriptor instead.\nfunc (*ErrWithStack) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *ErrWithStack) GetError() string {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn \"\"\n}\n\nfunc (x *ErrWithStack) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype StackTrace struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPcs           []int64                `protobuf:\"varint,1,rep,packed,name=pcs,proto3\" json:\"pcs,omitempty\"`\n\tFrames        []*StackFrame          `protobuf:\"bytes,2,rep,name=frames,proto3\" json:\"frames,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StackTrace) Reset() {\n\t*x = StackTrace{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StackTrace) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StackTrace) ProtoMessage() {}\n\nfunc (x *StackTrace) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StackTrace.ProtoReflect.Descriptor instead.\nfunc (*StackTrace) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *StackTrace) GetPcs() []int64 {\n\tif x != nil {\n\t\treturn x.Pcs\n\t}\n\treturn nil\n}\n\nfunc (x *StackTrace) GetFrames() []*StackFrame {\n\tif x != nil {\n\t\treturn x.Frames\n\t}\n\treturn nil\n}\n\ntype StackFrame struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFilename      string                 `protobuf:\"bytes,1,opt,name=filename,proto3\" json:\"filename,omitempty\"`\n\tFunc          string                 `protobuf:\"bytes,2,opt,name=func,proto3\" json:\"func,omitempty\"`\n\tLine          int32                  `protobuf:\"varint,3,opt,name=line,proto3\" json:\"line,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StackFrame) Reset() {\n\t*x = StackFrame{}\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StackFrame) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StackFrame) ProtoMessage() {}\n\nfunc (x *StackFrame) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace_trace_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StackFrame.ProtoReflect.Descriptor instead.\nfunc (*StackFrame) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace_trace_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *StackFrame) GetFilename() string {\n\tif x != nil {\n\t\treturn x.Filename\n\t}\n\treturn \"\"\n}\n\nfunc (x *StackFrame) GetFunc() string {\n\tif x != nil {\n\t\treturn x.Func\n\t}\n\treturn \"\"\n}\n\nfunc (x *StackFrame) GetLine() int32 {\n\tif x != nil {\n\t\treturn x.Line\n\t}\n\treturn 0\n}\n\nvar File_encore_engine_trace_trace_proto protoreflect.FileDescriptor\n\nconst file_encore_engine_trace_trace_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1fencore/engine/trace/trace.proto\\x12\\x13encore.engine.trace\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"/\\n\" +\n\t\"\\aTraceID\\x12\\x12\\n\" +\n\t\"\\x04high\\x18\\x01 \\x01(\\x04R\\x04high\\x12\\x10\\n\" +\n\t\"\\x03low\\x18\\x02 \\x01(\\x04R\\x03low\\\"\\xa2\\f\\n\" +\n\t\"\\aRequest\\x127\\n\" +\n\t\"\\btrace_id\\x18\\x01 \\x01(\\v2\\x1c.encore.engine.trace.TraceIDR\\atraceId\\x12\\x17\\n\" +\n\t\"\\aspan_id\\x18\\x02 \\x01(\\x04R\\x06spanId\\x12$\\n\" +\n\t\"\\x0eparent_span_id\\x18\\x03 \\x01(\\x04R\\fparentSpanId\\x12D\\n\" +\n\t\"\\x0fparent_trace_id\\x18  \\x01(\\v2\\x1c.encore.engine.trace.TraceIDR\\rparentTraceId\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x04 \\x01(\\rR\\x04goid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x05 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x06 \\x01(\\x04R\\aendTime\\x12\\x17\\n\" +\n\t\"\\adef_loc\\x18\\b \\x01(\\x05R\\x06defLoc\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\v \\x01(\\fR\\x03err\\x122\\n\" +\n\t\"\\x06events\\x18\\f \\x03(\\v2\\x1a.encore.engine.trace.EventR\\x06events\\x125\\n\" +\n\t\"\\x04type\\x18\\x0e \\x01(\\x0e2!.encore.engine.trace.Request.TypeR\\x04type\\x12<\\n\" +\n\t\"\\terr_stack\\x18\\x0f \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\berrStack\\x12@\\n\" +\n\t\"\\vpanic_stack\\x18\\\" \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\n\" +\n\t\"panicStack\\x12$\\n\" +\n\t\"\\x0eabs_start_time\\x18\\x10 \\x01(\\x04R\\fabsStartTime\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x11 \\x01(\\tR\\vserviceName\\x12#\\n\" +\n\t\"\\rendpoint_name\\x18\\x12 \\x01(\\tR\\fendpointName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\x13 \\x01(\\tR\\ttopicName\\x12+\\n\" +\n\t\"\\x11subscription_name\\x18\\x14 \\x01(\\tR\\x10subscriptionName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"message_id\\x18\\x15 \\x01(\\tR\\tmessageId\\x12\\x18\\n\" +\n\t\"\\aattempt\\x18\\x16 \\x01(\\rR\\aattempt\\x12!\\n\" +\n\t\"\\fpublish_time\\x18\\x17 \\x01(\\x04R\\vpublishTime\\x12\\x16\\n\" +\n\t\"\\x06inputs\\x18\\t \\x03(\\fR\\x06inputs\\x12\\x18\\n\" +\n\t\"\\aoutputs\\x18\\n\" +\n\t\" \\x03(\\fR\\aoutputs\\x12\\x10\\n\" +\n\t\"\\x03uid\\x18\\r \\x01(\\tR\\x03uid\\x12\\x1f\\n\" +\n\t\"\\vhttp_method\\x18\\x18 \\x01(\\tR\\n\" +\n\t\"httpMethod\\x12\\x12\\n\" +\n\t\"\\x04path\\x18\\x19 \\x01(\\tR\\x04path\\x12\\x1f\\n\" +\n\t\"\\vpath_params\\x18\\x1a \\x03(\\tR\\n\" +\n\t\"pathParams\\x12'\\n\" +\n\t\"\\x0frequest_payload\\x18\\x1b \\x01(\\fR\\x0erequestPayload\\x12)\\n\" +\n\t\"\\x10response_payload\\x18\\x1c \\x01(\\fR\\x0fresponsePayload\\x12c\\n\" +\n\t\"\\x13raw_request_headers\\x18\\x1d \\x03(\\v23.encore.engine.trace.Request.RawRequestHeadersEntryR\\x11rawRequestHeaders\\x12f\\n\" +\n\t\"\\x14raw_response_headers\\x18\\x1e \\x03(\\v24.encore.engine.trace.Request.RawResponseHeadersEntryR\\x12rawResponseHeaders\\x12.\\n\" +\n\t\"\\x13external_request_id\\x18\\x1f \\x01(\\tR\\x11externalRequestId\\x126\\n\" +\n\t\"\\x17external_correlation_id\\x18! \\x01(\\tR\\x15externalCorrelationId\\x1aD\\n\" +\n\t\"\\x16RawRequestHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\x1aE\\n\" +\n\t\"\\x17RawResponseHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\\")\\n\" +\n\t\"\\x04Type\\x12\\a\\n\" +\n\t\"\\x03RPC\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04AUTH\\x10\\x01\\x12\\x0e\\n\" +\n\t\"\\n\" +\n\t\"PUBSUB_MSG\\x10\\x02J\\x04\\b\\a\\x10\\b\\\"\\xe7\\x04\\n\" +\n\t\"\\x05Event\\x120\\n\" +\n\t\"\\x03rpc\\x18\\x01 \\x01(\\v2\\x1c.encore.engine.trace.RPCCallH\\x00R\\x03rpc\\x124\\n\" +\n\t\"\\x02tx\\x18\\x02 \\x01(\\v2\\\".encore.engine.trace.DBTransactionH\\x00R\\x02tx\\x124\\n\" +\n\t\"\\x05query\\x18\\x03 \\x01(\\v2\\x1c.encore.engine.trace.DBQueryH\\x00R\\x05query\\x12>\\n\" +\n\t\"\\tgoroutine\\x18\\x04 \\x01(\\v2\\x1e.encore.engine.trace.GoroutineH\\x00R\\tgoroutine\\x123\\n\" +\n\t\"\\x04http\\x18\\x05 \\x01(\\v2\\x1d.encore.engine.trace.HTTPCallH\\x00R\\x04http\\x123\\n\" +\n\t\"\\x03log\\x18\\x06 \\x01(\\v2\\x1f.encore.engine.trace.LogMessageH\\x00R\\x03log\\x12M\\n\" +\n\t\"\\fpublishedMsg\\x18\\a \\x01(\\v2'.encore.engine.trace.PubsubMsgPublishedH\\x00R\\fpublishedMsg\\x12E\\n\" +\n\t\"\\fservice_init\\x18\\b \\x01(\\v2 .encore.engine.trace.ServiceInitH\\x00R\\vserviceInit\\x124\\n\" +\n\t\"\\x05cache\\x18\\t \\x01(\\v2\\x1c.encore.engine.trace.CacheOpH\\x00R\\x05cache\\x12B\\n\" +\n\t\"\\vbody_stream\\x18\\n\" +\n\t\" \\x01(\\v2\\x1f.encore.engine.trace.BodyStreamH\\x00R\\n\" +\n\t\"bodyStreamB\\x06\\n\" +\n\t\"\\x04data\\\"\\xd8\\x01\\n\" +\n\t\"\\aRPCCall\\x12\\x17\\n\" +\n\t\"\\aspan_id\\x18\\x01 \\x01(\\x04R\\x06spanId\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x02 \\x01(\\rR\\x04goid\\x12\\x17\\n\" +\n\t\"\\adef_loc\\x18\\x04 \\x01(\\x05R\\x06defLoc\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x05 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x06 \\x01(\\x04R\\aendTime\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\a \\x01(\\fR\\x03err\\x125\\n\" +\n\t\"\\x05stack\\x18\\b \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\x05stackJ\\x04\\b\\x03\\x10\\x04\\\"_\\n\" +\n\t\"\\tGoroutine\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\rR\\x04goid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x03 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\x04R\\aendTimeJ\\x04\\b\\x02\\x10\\x03\\\"\\xb2\\x03\\n\" +\n\t\"\\rDBTransaction\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\rR\\x04goid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x04 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x05 \\x01(\\x04R\\aendTime\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x06 \\x01(\\fR\\x03err\\x12Q\\n\" +\n\t\"\\n\" +\n\t\"completion\\x18\\a \\x01(\\x0e21.encore.engine.trace.DBTransaction.CompletionTypeR\\n\" +\n\t\"completion\\x126\\n\" +\n\t\"\\aqueries\\x18\\b \\x03(\\v2\\x1c.encore.engine.trace.DBQueryR\\aqueries\\x12@\\n\" +\n\t\"\\vbegin_stack\\x18\\t \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\n\" +\n\t\"beginStack\\x12<\\n\" +\n\t\"\\tend_stack\\x18\\n\" +\n\t\" \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\bendStack\\\"*\\n\" +\n\t\"\\x0eCompletionType\\x12\\f\\n\" +\n\t\"\\bROLLBACK\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06COMMIT\\x10\\x01J\\x04\\b\\x02\\x10\\x03J\\x04\\b\\x03\\x10\\x04\\\"\\xbc\\x01\\n\" +\n\t\"\\aDBQuery\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\rR\\x04goid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x03 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\x04R\\aendTime\\x12\\x14\\n\" +\n\t\"\\x05query\\x18\\x05 \\x01(\\fR\\x05query\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x06 \\x01(\\fR\\x03err\\x125\\n\" +\n\t\"\\x05stack\\x18\\a \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\x05stackJ\\x04\\b\\x02\\x10\\x03\\\"\\xfa\\x01\\n\" +\n\t\"\\x12PubsubMsgPublished\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\x04R\\x04goid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x03 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\x04R\\aendTime\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x05 \\x01(\\tR\\x05topic\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\x06 \\x01(\\fR\\amessage\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"message_id\\x18\\a \\x01(\\tR\\tmessageId\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\b \\x01(\\fR\\x03err\\x125\\n\" +\n\t\"\\x05stack\\x18\\t \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\x05stack\\\"\\xde\\x01\\n\" +\n\t\"\\vServiceInit\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\x04R\\x04goid\\x12\\x17\\n\" +\n\t\"\\adef_loc\\x18\\x02 \\x01(\\x05R\\x06defLoc\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x03 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\x04R\\aendTime\\x12\\x18\\n\" +\n\t\"\\aservice\\x18\\x05 \\x01(\\tR\\aservice\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x06 \\x01(\\fR\\x03err\\x12<\\n\" +\n\t\"\\terr_stack\\x18\\a \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\berrStack\\\"\\xb7\\x03\\n\" +\n\t\"\\aCacheOp\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\rR\\x04goid\\x12\\x17\\n\" +\n\t\"\\adef_loc\\x18\\x02 \\x01(\\x05R\\x06defLoc\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x03 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\x04R\\aendTime\\x12\\x1c\\n\" +\n\t\"\\toperation\\x18\\x05 \\x01(\\tR\\toperation\\x12\\x12\\n\" +\n\t\"\\x04keys\\x18\\x06 \\x03(\\tR\\x04keys\\x12\\x16\\n\" +\n\t\"\\x06inputs\\x18\\a \\x03(\\fR\\x06inputs\\x12\\x18\\n\" +\n\t\"\\aoutputs\\x18\\b \\x03(\\fR\\aoutputs\\x125\\n\" +\n\t\"\\x05stack\\x18\\t \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\x05stack\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\n\" +\n\t\" \\x01(\\fR\\x03err\\x12\\x14\\n\" +\n\t\"\\x05write\\x18\\v \\x01(\\bR\\x05write\\x12;\\n\" +\n\t\"\\x06result\\x18\\f \\x01(\\x0e2#.encore.engine.trace.CacheOp.ResultR\\x06result\\\"E\\n\" +\n\t\"\\x06Result\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\x06\\n\" +\n\t\"\\x02OK\\x10\\x01\\x12\\x0f\\n\" +\n\t\"\\vNO_SUCH_KEY\\x10\\x02\\x12\\f\\n\" +\n\t\"\\bCONFLICT\\x10\\x03\\x12\\a\\n\" +\n\t\"\\x03ERR\\x10\\x04\\\"a\\n\" +\n\t\"\\n\" +\n\t\"BodyStream\\x12\\x1f\\n\" +\n\t\"\\vis_response\\x18\\x01 \\x01(\\bR\\n\" +\n\t\"isResponse\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"overflowed\\x18\\x02 \\x01(\\bR\\n\" +\n\t\"overflowed\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x03 \\x01(\\fR\\x04data\\\"\\xb5\\x02\\n\" +\n\t\"\\bHTTPCall\\x12\\x17\\n\" +\n\t\"\\aspan_id\\x18\\x01 \\x01(\\x04R\\x06spanId\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x02 \\x01(\\rR\\x04goid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"start_time\\x18\\x03 \\x01(\\x04R\\tstartTime\\x12\\x19\\n\" +\n\t\"\\bend_time\\x18\\x04 \\x01(\\x04R\\aendTime\\x12\\x16\\n\" +\n\t\"\\x06method\\x18\\x05 \\x01(\\tR\\x06method\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x06 \\x01(\\tR\\x03url\\x12\\x1f\\n\" +\n\t\"\\vstatus_code\\x18\\a \\x01(\\rR\\n\" +\n\t\"statusCode\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\b \\x01(\\fR\\x03err\\x12(\\n\" +\n\t\"\\x10body_closed_time\\x18\\t \\x01(\\x04R\\x0ebodyClosedTime\\x12;\\n\" +\n\t\"\\x06events\\x18\\n\" +\n\t\" \\x03(\\v2#.encore.engine.trace.HTTPTraceEventR\\x06events\\\"\\xa3\\x06\\n\" +\n\t\"\\x0eHTTPTraceEvent\\x12;\\n\" +\n\t\"\\x04code\\x18\\x01 \\x01(\\x0e2'.encore.engine.trace.HTTPTraceEventCodeR\\x04code\\x12\\x12\\n\" +\n\t\"\\x04time\\x18\\x02 \\x01(\\x04R\\x04time\\x12A\\n\" +\n\t\"\\bget_conn\\x18\\x03 \\x01(\\v2$.encore.engine.trace.HTTPGetConnDataH\\x00R\\agetConn\\x12A\\n\" +\n\t\"\\bgot_conn\\x18\\x04 \\x01(\\v2$.encore.engine.trace.HTTPGotConnDataH\\x00R\\agotConn\\x12W\\n\" +\n\t\"\\x10got_1xx_response\\x18\\x05 \\x01(\\v2+.encore.engine.trace.HTTPGot1xxResponseDataH\\x00R\\x0egot1xxResponse\\x12D\\n\" +\n\t\"\\tdns_start\\x18\\x06 \\x01(\\v2%.encore.engine.trace.HTTPDNSStartDataH\\x00R\\bdnsStart\\x12A\\n\" +\n\t\"\\bdns_done\\x18\\a \\x01(\\v2$.encore.engine.trace.HTTPDNSDoneDataH\\x00R\\adnsDone\\x12P\\n\" +\n\t\"\\rconnect_start\\x18\\b \\x01(\\v2).encore.engine.trace.HTTPConnectStartDataH\\x00R\\fconnectStart\\x12M\\n\" +\n\t\"\\fconnect_done\\x18\\t \\x01(\\v2(.encore.engine.trace.HTTPConnectDoneDataH\\x00R\\vconnectDone\\x12]\\n\" +\n\t\"\\x12tls_handshake_done\\x18\\n\" +\n\t\" \\x01(\\v2-.encore.engine.trace.HTTPTLSHandshakeDoneDataH\\x00R\\x10tlsHandshakeDone\\x12P\\n\" +\n\t\"\\rwrote_request\\x18\\v \\x01(\\v2).encore.engine.trace.HTTPWroteRequestDataH\\x00R\\fwroteRequestB\\x06\\n\" +\n\t\"\\x04data\\\".\\n\" +\n\t\"\\x0fHTTPGetConnData\\x12\\x1b\\n\" +\n\t\"\\thost_port\\x18\\x01 \\x01(\\tR\\bhostPort\\\"n\\n\" +\n\t\"\\x0fHTTPGotConnData\\x12\\x16\\n\" +\n\t\"\\x06reused\\x18\\x01 \\x01(\\bR\\x06reused\\x12\\x19\\n\" +\n\t\"\\bwas_idle\\x18\\x02 \\x01(\\bR\\awasIdle\\x12(\\n\" +\n\t\"\\x10idle_duration_ns\\x18\\x03 \\x01(\\x03R\\x0eidleDurationNs\\\",\\n\" +\n\t\"\\x16HTTPGot1xxResponseData\\x12\\x12\\n\" +\n\t\"\\x04code\\x18\\x01 \\x01(\\x05R\\x04code\\\"&\\n\" +\n\t\"\\x10HTTPDNSStartData\\x12\\x12\\n\" +\n\t\"\\x04host\\x18\\x01 \\x01(\\tR\\x04host\\\"W\\n\" +\n\t\"\\x0fHTTPDNSDoneData\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fR\\x03err\\x122\\n\" +\n\t\"\\x05addrs\\x18\\x02 \\x03(\\v2\\x1c.encore.engine.trace.DNSAddrR\\x05addrs\\\"\\x19\\n\" +\n\t\"\\aDNSAddr\\x12\\x0e\\n\" +\n\t\"\\x02ip\\x18\\x01 \\x01(\\fR\\x02ip\\\"D\\n\" +\n\t\"\\x14HTTPConnectStartData\\x12\\x18\\n\" +\n\t\"\\anetwork\\x18\\x01 \\x01(\\tR\\anetwork\\x12\\x12\\n\" +\n\t\"\\x04addr\\x18\\x02 \\x01(\\tR\\x04addr\\\"U\\n\" +\n\t\"\\x13HTTPConnectDoneData\\x12\\x18\\n\" +\n\t\"\\anetwork\\x18\\x01 \\x01(\\tR\\anetwork\\x12\\x12\\n\" +\n\t\"\\x04addr\\x18\\x02 \\x01(\\tR\\x04addr\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x03 \\x01(\\fR\\x03err\\\"\\xc2\\x01\\n\" +\n\t\"\\x18HTTPTLSHandshakeDoneData\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fR\\x03err\\x12\\x1f\\n\" +\n\t\"\\vtls_version\\x18\\x02 \\x01(\\rR\\n\" +\n\t\"tlsVersion\\x12!\\n\" +\n\t\"\\fcipher_suite\\x18\\x03 \\x01(\\rR\\vcipherSuite\\x12\\x1f\\n\" +\n\t\"\\vserver_name\\x18\\x04 \\x01(\\tR\\n\" +\n\t\"serverName\\x12/\\n\" +\n\t\"\\x13negotiated_protocol\\x18\\x05 \\x01(\\tR\\x12negotiatedProtocol\\\"(\\n\" +\n\t\"\\x14HTTPWroteRequestData\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fR\\x03err\\\"\\xc8\\x02\\n\" +\n\t\"\\n\" +\n\t\"LogMessage\\x12\\x17\\n\" +\n\t\"\\aspan_id\\x18\\x01 \\x01(\\x04R\\x06spanId\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x02 \\x01(\\rR\\x04goid\\x12\\x12\\n\" +\n\t\"\\x04time\\x18\\x03 \\x01(\\x04R\\x04time\\x12;\\n\" +\n\t\"\\x05level\\x18\\x04 \\x01(\\x0e2%.encore.engine.trace.LogMessage.LevelR\\x05level\\x12\\x10\\n\" +\n\t\"\\x03msg\\x18\\x05 \\x01(\\tR\\x03msg\\x125\\n\" +\n\t\"\\x06fields\\x18\\x06 \\x03(\\v2\\x1d.encore.engine.trace.LogFieldR\\x06fields\\x125\\n\" +\n\t\"\\x05stack\\x18\\a \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\x05stack\\\"<\\n\" +\n\t\"\\x05Level\\x12\\t\\n\" +\n\t\"\\x05DEBUG\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04INFO\\x10\\x01\\x12\\t\\n\" +\n\t\"\\x05ERROR\\x10\\x02\\x12\\b\\n\" +\n\t\"\\x04WARN\\x10\\x03\\x12\\t\\n\" +\n\t\"\\x05TRACE\\x10\\x04\\\"\\xa4\\x03\\n\" +\n\t\"\\bLogField\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x120\\n\" +\n\t\"\\x13error_without_stack\\x18\\x02 \\x01(\\tH\\x00R\\x11errorWithoutStack\\x12M\\n\" +\n\t\"\\x10error_with_stack\\x18\\r \\x01(\\v2!.encore.engine.trace.ErrWithStackH\\x00R\\x0eerrorWithStack\\x12\\x12\\n\" +\n\t\"\\x03str\\x18\\x03 \\x01(\\tH\\x00R\\x03str\\x12\\x14\\n\" +\n\t\"\\x04bool\\x18\\x04 \\x01(\\bH\\x00R\\x04bool\\x120\\n\" +\n\t\"\\x04time\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampH\\x00R\\x04time\\x12\\x12\\n\" +\n\t\"\\x03dur\\x18\\x06 \\x01(\\x03H\\x00R\\x03dur\\x12\\x14\\n\" +\n\t\"\\x04uuid\\x18\\a \\x01(\\fH\\x00R\\x04uuid\\x12\\x14\\n\" +\n\t\"\\x04json\\x18\\b \\x01(\\fH\\x00R\\x04json\\x12\\x12\\n\" +\n\t\"\\x03int\\x18\\t \\x01(\\x03H\\x00R\\x03int\\x12\\x14\\n\" +\n\t\"\\x04uint\\x18\\n\" +\n\t\" \\x01(\\x04H\\x00R\\x04uint\\x12\\x1a\\n\" +\n\t\"\\afloat32\\x18\\v \\x01(\\x02H\\x00R\\afloat32\\x12\\x1a\\n\" +\n\t\"\\afloat64\\x18\\f \\x01(\\x01H\\x00R\\afloat64B\\a\\n\" +\n\t\"\\x05value\\\"[\\n\" +\n\t\"\\fErrWithStack\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\x01 \\x01(\\tR\\x05error\\x125\\n\" +\n\t\"\\x05stack\\x18\\x02 \\x01(\\v2\\x1f.encore.engine.trace.StackTraceR\\x05stack\\\"W\\n\" +\n\t\"\\n\" +\n\t\"StackTrace\\x12\\x10\\n\" +\n\t\"\\x03pcs\\x18\\x01 \\x03(\\x03R\\x03pcs\\x127\\n\" +\n\t\"\\x06frames\\x18\\x02 \\x03(\\v2\\x1f.encore.engine.trace.StackFrameR\\x06frames\\\"P\\n\" +\n\t\"\\n\" +\n\t\"StackFrame\\x12\\x1a\\n\" +\n\t\"\\bfilename\\x18\\x01 \\x01(\\tR\\bfilename\\x12\\x12\\n\" +\n\t\"\\x04func\\x18\\x02 \\x01(\\tR\\x04func\\x12\\x12\\n\" +\n\t\"\\x04line\\x18\\x03 \\x01(\\x05R\\x04line*\\xa0\\x02\\n\" +\n\t\"\\x12HTTPTraceEventCode\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\f\\n\" +\n\t\"\\bGET_CONN\\x10\\x01\\x12\\f\\n\" +\n\t\"\\bGOT_CONN\\x10\\x02\\x12\\x1b\\n\" +\n\t\"\\x17GOT_FIRST_RESPONSE_BYTE\\x10\\x03\\x12\\x14\\n\" +\n\t\"\\x10GOT_1XX_RESPONSE\\x10\\x04\\x12\\r\\n\" +\n\t\"\\tDNS_START\\x10\\x05\\x12\\f\\n\" +\n\t\"\\bDNS_DONE\\x10\\x06\\x12\\x11\\n\" +\n\t\"\\rCONNECT_START\\x10\\a\\x12\\x10\\n\" +\n\t\"\\fCONNECT_DONE\\x10\\b\\x12\\x17\\n\" +\n\t\"\\x13TLS_HANDSHAKE_START\\x10\\t\\x12\\x16\\n\" +\n\t\"\\x12TLS_HANDSHAKE_DONE\\x10\\n\" +\n\t\"\\x12\\x11\\n\" +\n\t\"\\rWROTE_HEADERS\\x10\\v\\x12\\x11\\n\" +\n\t\"\\rWROTE_REQUEST\\x10\\f\\x12\\x15\\n\" +\n\t\"\\x11WAIT_100_CONTINUE\\x10\\rB$Z\\\"encr.dev/proto/encore/engine/traceb\\x06proto3\"\n\nvar (\n\tfile_encore_engine_trace_trace_proto_rawDescOnce sync.Once\n\tfile_encore_engine_trace_trace_proto_rawDescData []byte\n)\n\nfunc file_encore_engine_trace_trace_proto_rawDescGZIP() []byte {\n\tfile_encore_engine_trace_trace_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_engine_trace_trace_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_engine_trace_trace_proto_rawDesc), len(file_encore_engine_trace_trace_proto_rawDesc)))\n\t})\n\treturn file_encore_engine_trace_trace_proto_rawDescData\n}\n\nvar file_encore_engine_trace_trace_proto_enumTypes = make([]protoimpl.EnumInfo, 5)\nvar file_encore_engine_trace_trace_proto_msgTypes = make([]protoimpl.MessageInfo, 30)\nvar file_encore_engine_trace_trace_proto_goTypes = []any{\n\t(HTTPTraceEventCode)(0),           // 0: encore.engine.trace.HTTPTraceEventCode\n\t(Request_Type)(0),                 // 1: encore.engine.trace.Request.Type\n\t(DBTransaction_CompletionType)(0), // 2: encore.engine.trace.DBTransaction.CompletionType\n\t(CacheOp_Result)(0),               // 3: encore.engine.trace.CacheOp.Result\n\t(LogMessage_Level)(0),             // 4: encore.engine.trace.LogMessage.Level\n\t(*TraceID)(nil),                   // 5: encore.engine.trace.TraceID\n\t(*Request)(nil),                   // 6: encore.engine.trace.Request\n\t(*Event)(nil),                     // 7: encore.engine.trace.Event\n\t(*RPCCall)(nil),                   // 8: encore.engine.trace.RPCCall\n\t(*Goroutine)(nil),                 // 9: encore.engine.trace.Goroutine\n\t(*DBTransaction)(nil),             // 10: encore.engine.trace.DBTransaction\n\t(*DBQuery)(nil),                   // 11: encore.engine.trace.DBQuery\n\t(*PubsubMsgPublished)(nil),        // 12: encore.engine.trace.PubsubMsgPublished\n\t(*ServiceInit)(nil),               // 13: encore.engine.trace.ServiceInit\n\t(*CacheOp)(nil),                   // 14: encore.engine.trace.CacheOp\n\t(*BodyStream)(nil),                // 15: encore.engine.trace.BodyStream\n\t(*HTTPCall)(nil),                  // 16: encore.engine.trace.HTTPCall\n\t(*HTTPTraceEvent)(nil),            // 17: encore.engine.trace.HTTPTraceEvent\n\t(*HTTPGetConnData)(nil),           // 18: encore.engine.trace.HTTPGetConnData\n\t(*HTTPGotConnData)(nil),           // 19: encore.engine.trace.HTTPGotConnData\n\t(*HTTPGot1XxResponseData)(nil),    // 20: encore.engine.trace.HTTPGot1xxResponseData\n\t(*HTTPDNSStartData)(nil),          // 21: encore.engine.trace.HTTPDNSStartData\n\t(*HTTPDNSDoneData)(nil),           // 22: encore.engine.trace.HTTPDNSDoneData\n\t(*DNSAddr)(nil),                   // 23: encore.engine.trace.DNSAddr\n\t(*HTTPConnectStartData)(nil),      // 24: encore.engine.trace.HTTPConnectStartData\n\t(*HTTPConnectDoneData)(nil),       // 25: encore.engine.trace.HTTPConnectDoneData\n\t(*HTTPTLSHandshakeDoneData)(nil),  // 26: encore.engine.trace.HTTPTLSHandshakeDoneData\n\t(*HTTPWroteRequestData)(nil),      // 27: encore.engine.trace.HTTPWroteRequestData\n\t(*LogMessage)(nil),                // 28: encore.engine.trace.LogMessage\n\t(*LogField)(nil),                  // 29: encore.engine.trace.LogField\n\t(*ErrWithStack)(nil),              // 30: encore.engine.trace.ErrWithStack\n\t(*StackTrace)(nil),                // 31: encore.engine.trace.StackTrace\n\t(*StackFrame)(nil),                // 32: encore.engine.trace.StackFrame\n\tnil,                               // 33: encore.engine.trace.Request.RawRequestHeadersEntry\n\tnil,                               // 34: encore.engine.trace.Request.RawResponseHeadersEntry\n\t(*timestamppb.Timestamp)(nil),     // 35: google.protobuf.Timestamp\n}\nvar file_encore_engine_trace_trace_proto_depIdxs = []int32{\n\t5,  // 0: encore.engine.trace.Request.trace_id:type_name -> encore.engine.trace.TraceID\n\t5,  // 1: encore.engine.trace.Request.parent_trace_id:type_name -> encore.engine.trace.TraceID\n\t7,  // 2: encore.engine.trace.Request.events:type_name -> encore.engine.trace.Event\n\t1,  // 3: encore.engine.trace.Request.type:type_name -> encore.engine.trace.Request.Type\n\t31, // 4: encore.engine.trace.Request.err_stack:type_name -> encore.engine.trace.StackTrace\n\t31, // 5: encore.engine.trace.Request.panic_stack:type_name -> encore.engine.trace.StackTrace\n\t33, // 6: encore.engine.trace.Request.raw_request_headers:type_name -> encore.engine.trace.Request.RawRequestHeadersEntry\n\t34, // 7: encore.engine.trace.Request.raw_response_headers:type_name -> encore.engine.trace.Request.RawResponseHeadersEntry\n\t8,  // 8: encore.engine.trace.Event.rpc:type_name -> encore.engine.trace.RPCCall\n\t10, // 9: encore.engine.trace.Event.tx:type_name -> encore.engine.trace.DBTransaction\n\t11, // 10: encore.engine.trace.Event.query:type_name -> encore.engine.trace.DBQuery\n\t9,  // 11: encore.engine.trace.Event.goroutine:type_name -> encore.engine.trace.Goroutine\n\t16, // 12: encore.engine.trace.Event.http:type_name -> encore.engine.trace.HTTPCall\n\t28, // 13: encore.engine.trace.Event.log:type_name -> encore.engine.trace.LogMessage\n\t12, // 14: encore.engine.trace.Event.publishedMsg:type_name -> encore.engine.trace.PubsubMsgPublished\n\t13, // 15: encore.engine.trace.Event.service_init:type_name -> encore.engine.trace.ServiceInit\n\t14, // 16: encore.engine.trace.Event.cache:type_name -> encore.engine.trace.CacheOp\n\t15, // 17: encore.engine.trace.Event.body_stream:type_name -> encore.engine.trace.BodyStream\n\t31, // 18: encore.engine.trace.RPCCall.stack:type_name -> encore.engine.trace.StackTrace\n\t2,  // 19: encore.engine.trace.DBTransaction.completion:type_name -> encore.engine.trace.DBTransaction.CompletionType\n\t11, // 20: encore.engine.trace.DBTransaction.queries:type_name -> encore.engine.trace.DBQuery\n\t31, // 21: encore.engine.trace.DBTransaction.begin_stack:type_name -> encore.engine.trace.StackTrace\n\t31, // 22: encore.engine.trace.DBTransaction.end_stack:type_name -> encore.engine.trace.StackTrace\n\t31, // 23: encore.engine.trace.DBQuery.stack:type_name -> encore.engine.trace.StackTrace\n\t31, // 24: encore.engine.trace.PubsubMsgPublished.stack:type_name -> encore.engine.trace.StackTrace\n\t31, // 25: encore.engine.trace.ServiceInit.err_stack:type_name -> encore.engine.trace.StackTrace\n\t31, // 26: encore.engine.trace.CacheOp.stack:type_name -> encore.engine.trace.StackTrace\n\t3,  // 27: encore.engine.trace.CacheOp.result:type_name -> encore.engine.trace.CacheOp.Result\n\t17, // 28: encore.engine.trace.HTTPCall.events:type_name -> encore.engine.trace.HTTPTraceEvent\n\t0,  // 29: encore.engine.trace.HTTPTraceEvent.code:type_name -> encore.engine.trace.HTTPTraceEventCode\n\t18, // 30: encore.engine.trace.HTTPTraceEvent.get_conn:type_name -> encore.engine.trace.HTTPGetConnData\n\t19, // 31: encore.engine.trace.HTTPTraceEvent.got_conn:type_name -> encore.engine.trace.HTTPGotConnData\n\t20, // 32: encore.engine.trace.HTTPTraceEvent.got_1xx_response:type_name -> encore.engine.trace.HTTPGot1xxResponseData\n\t21, // 33: encore.engine.trace.HTTPTraceEvent.dns_start:type_name -> encore.engine.trace.HTTPDNSStartData\n\t22, // 34: encore.engine.trace.HTTPTraceEvent.dns_done:type_name -> encore.engine.trace.HTTPDNSDoneData\n\t24, // 35: encore.engine.trace.HTTPTraceEvent.connect_start:type_name -> encore.engine.trace.HTTPConnectStartData\n\t25, // 36: encore.engine.trace.HTTPTraceEvent.connect_done:type_name -> encore.engine.trace.HTTPConnectDoneData\n\t26, // 37: encore.engine.trace.HTTPTraceEvent.tls_handshake_done:type_name -> encore.engine.trace.HTTPTLSHandshakeDoneData\n\t27, // 38: encore.engine.trace.HTTPTraceEvent.wrote_request:type_name -> encore.engine.trace.HTTPWroteRequestData\n\t23, // 39: encore.engine.trace.HTTPDNSDoneData.addrs:type_name -> encore.engine.trace.DNSAddr\n\t4,  // 40: encore.engine.trace.LogMessage.level:type_name -> encore.engine.trace.LogMessage.Level\n\t29, // 41: encore.engine.trace.LogMessage.fields:type_name -> encore.engine.trace.LogField\n\t31, // 42: encore.engine.trace.LogMessage.stack:type_name -> encore.engine.trace.StackTrace\n\t30, // 43: encore.engine.trace.LogField.error_with_stack:type_name -> encore.engine.trace.ErrWithStack\n\t35, // 44: encore.engine.trace.LogField.time:type_name -> google.protobuf.Timestamp\n\t31, // 45: encore.engine.trace.ErrWithStack.stack:type_name -> encore.engine.trace.StackTrace\n\t32, // 46: encore.engine.trace.StackTrace.frames:type_name -> encore.engine.trace.StackFrame\n\t47, // [47:47] is the sub-list for method output_type\n\t47, // [47:47] is the sub-list for method input_type\n\t47, // [47:47] is the sub-list for extension type_name\n\t47, // [47:47] is the sub-list for extension extendee\n\t0,  // [0:47] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_engine_trace_trace_proto_init() }\nfunc file_encore_engine_trace_trace_proto_init() {\n\tif File_encore_engine_trace_trace_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_engine_trace_trace_proto_msgTypes[2].OneofWrappers = []any{\n\t\t(*Event_Rpc)(nil),\n\t\t(*Event_Tx)(nil),\n\t\t(*Event_Query)(nil),\n\t\t(*Event_Goroutine)(nil),\n\t\t(*Event_Http)(nil),\n\t\t(*Event_Log)(nil),\n\t\t(*Event_PublishedMsg)(nil),\n\t\t(*Event_ServiceInit)(nil),\n\t\t(*Event_Cache)(nil),\n\t\t(*Event_BodyStream)(nil),\n\t}\n\tfile_encore_engine_trace_trace_proto_msgTypes[12].OneofWrappers = []any{\n\t\t(*HTTPTraceEvent_GetConn)(nil),\n\t\t(*HTTPTraceEvent_GotConn)(nil),\n\t\t(*HTTPTraceEvent_Got_1XxResponse)(nil),\n\t\t(*HTTPTraceEvent_DnsStart)(nil),\n\t\t(*HTTPTraceEvent_DnsDone)(nil),\n\t\t(*HTTPTraceEvent_ConnectStart)(nil),\n\t\t(*HTTPTraceEvent_ConnectDone)(nil),\n\t\t(*HTTPTraceEvent_TlsHandshakeDone)(nil),\n\t\t(*HTTPTraceEvent_WroteRequest)(nil),\n\t}\n\tfile_encore_engine_trace_trace_proto_msgTypes[24].OneofWrappers = []any{\n\t\t(*LogField_ErrorWithoutStack)(nil),\n\t\t(*LogField_ErrorWithStack)(nil),\n\t\t(*LogField_Str)(nil),\n\t\t(*LogField_Bool)(nil),\n\t\t(*LogField_Time)(nil),\n\t\t(*LogField_Dur)(nil),\n\t\t(*LogField_Uuid)(nil),\n\t\t(*LogField_Json)(nil),\n\t\t(*LogField_Int)(nil),\n\t\t(*LogField_Uint)(nil),\n\t\t(*LogField_Float32)(nil),\n\t\t(*LogField_Float64)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_engine_trace_trace_proto_rawDesc), len(file_encore_engine_trace_trace_proto_rawDesc)),\n\t\t\tNumEnums:      5,\n\t\t\tNumMessages:   30,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_engine_trace_trace_proto_goTypes,\n\t\tDependencyIndexes: file_encore_engine_trace_trace_proto_depIdxs,\n\t\tEnumInfos:         file_encore_engine_trace_trace_proto_enumTypes,\n\t\tMessageInfos:      file_encore_engine_trace_trace_proto_msgTypes,\n\t}.Build()\n\tFile_encore_engine_trace_trace_proto = out.File\n\tfile_encore_engine_trace_trace_proto_goTypes = nil\n\tfile_encore_engine_trace_trace_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/engine/trace/trace.proto",
    "content": "syntax = \"proto3\";\n\npackage encore.engine.trace;\n\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"encr.dev/proto/encore/engine/trace\";\n\nmessage TraceID {\n  uint64 high = 1;\n  uint64 low = 2;\n}\n\nmessage Request {\n  reserved 7; // call_loc\n  TraceID trace_id = 1;\n  uint64 span_id = 2;\n  uint64 parent_span_id = 3;\n  TraceID parent_trace_id = 32;\n  uint32 goid = 4;\n  uint64 start_time = 5;\n  uint64 end_time = 6;\n  int32 def_loc = 8;\n  bytes err = 11;\n  repeated Event events = 12;\n  Type type = 14;\n  StackTrace err_stack = 15; // null if unavailable\n  StackTrace panic_stack = 34; // null if unavailable\n\n  // abs_start_time is the absolute unix timestamp\n  // (in nanosecond resolution) of when the request started.\n  uint64 abs_start_time = 16;\n\n  string service_name = 17;\n  string endpoint_name = 18;\n\n  // Fields set if Type == PUBSUB_MSG\n  string topic_name = 19;\n  string subscription_name = 20;\n  string message_id = 21;\n  uint32 attempt = 22;\n  uint64 publish_time = 23;\n\n  // Fields set if Type == RPC or AUTH\n  repeated bytes inputs = 9; // Deprecated: use request_payload and path_params\n  repeated bytes outputs = 10; // Deprecated: use response_payload and uid\n  string uid = 13;\n  string http_method = 24;\n  string path = 25;\n  repeated string path_params = 26;\n  bytes request_payload = 27;\n  bytes response_payload = 28;\n  map<string, string> raw_request_headers = 29;\n  map<string, string> raw_response_headers = 30;\n  // external_request_id is the value of the X-Request-ID header.\n  string external_request_id = 31;\n  string external_correlation_id = 33;\n\n  enum Type {\n    RPC = 0;\n    AUTH = 1;\n    PUBSUB_MSG = 2;\n  }\n}\n\nmessage Event {\n  oneof data {\n    RPCCall rpc = 1;\n    DBTransaction tx = 2;\n    DBQuery query = 3;\n    Goroutine goroutine = 4;\n    HTTPCall http = 5;\n    LogMessage log = 6;\n    PubsubMsgPublished publishedMsg = 7;\n    ServiceInit service_init = 8;\n    CacheOp cache = 9;\n    BodyStream body_stream = 10;\n  }\n}\n\nmessage RPCCall {\n  reserved 3; // call_loc\n  uint64 span_id = 1;\n  uint32 goid = 2;\n  int32 def_loc = 4;\n  uint64 start_time = 5;\n  uint64 end_time = 6;\n  bytes err = 7;\n  StackTrace stack = 8; // where it was called (null if unavailable)\n}\n\nmessage Goroutine {\n  reserved 2; // call_loc\n  uint32 goid = 1;\n  uint64 start_time = 3;\n  uint64 end_time = 4;\n}\n\nmessage DBTransaction {\n  enum CompletionType {\n    ROLLBACK = 0;\n    COMMIT = 1;\n  }\n\n  reserved 2, 3; // start_loc, end_loc\n\n  uint32 goid = 1;\n  uint64 start_time = 4;\n  uint64 end_time = 5;\n  bytes err = 6;\n  CompletionType completion = 7;\n  repeated DBQuery queries = 8;\n  StackTrace begin_stack = 9; // null if unavailable\n  StackTrace end_stack = 10; // null if unavailable\n}\n\nmessage DBQuery {\n  reserved 2; // call_loc\n  uint32 goid = 1;\n  uint64 start_time = 3;\n  uint64 end_time = 4;\n  bytes query = 5;\n  bytes err = 6;\n  StackTrace stack = 7; // null if unavailable\n}\n\nmessage PubsubMsgPublished {\n  uint64 goid = 1;\n  uint64 start_time = 3;\n  uint64 end_time = 4;\n  string topic = 5;\n  bytes message = 6;\n  string message_id = 7;\n  bytes err = 8;\n  StackTrace stack = 9;\n}\n\nmessage ServiceInit {\n  uint64 goid = 1;\n  int32 def_loc = 2;\n  uint64 start_time = 3;\n  uint64 end_time = 4;\n  string service = 5;\n  bytes err = 6;\n  StackTrace err_stack = 7; // null if not an error\n}\n\nmessage CacheOp {\n  uint32 goid = 1;\n  int32 def_loc = 2;\n  uint64 start_time = 3;\n  uint64 end_time = 4;\n  string operation = 5;\n  repeated string keys = 6;\n  repeated bytes inputs = 7;\n  repeated bytes outputs = 8;\n  StackTrace stack = 9; // null if unavailable\n  bytes err = 10; // set iff result == ERR\n  bool write = 11;\n  Result result = 12;\n\n  enum Result {\n    UNKNOWN = 0;\n    OK = 1;\n    NO_SUCH_KEY = 2;\n    CONFLICT = 3;\n    ERR = 4;\n  }\n}\n\nmessage BodyStream {\n  bool is_response = 1;\n  bool overflowed = 2;\n  bytes data = 3;\n}\n\nmessage HTTPCall {\n  uint64 span_id = 1;\n  uint32 goid = 2;\n  uint64 start_time = 3;\n  uint64 end_time = 4;\n  string method = 5;\n  string url = 6;\n  uint32 status_code = 7;\n  bytes err = 8;\n  uint64 body_closed_time = 9;\n  repeated HTTPTraceEvent events = 10;\n}\n\nenum HTTPTraceEventCode {\n  UNKNOWN = 0;\n  GET_CONN = 1;\n  GOT_CONN = 2;\n  GOT_FIRST_RESPONSE_BYTE = 3;\n  GOT_1XX_RESPONSE = 4;\n  DNS_START = 5;\n  DNS_DONE = 6;\n  CONNECT_START = 7;\n  CONNECT_DONE = 8;\n  TLS_HANDSHAKE_START = 9;\n  TLS_HANDSHAKE_DONE = 10;\n  WROTE_HEADERS = 11;\n  WROTE_REQUEST = 12;\n  WAIT_100_CONTINUE = 13;\n}\n\nmessage HTTPTraceEvent {\n  HTTPTraceEventCode code = 1;\n  uint64 time = 2;\n  oneof data {\n    HTTPGetConnData get_conn = 3;\n    HTTPGotConnData got_conn = 4;\n    HTTPGot1xxResponseData got_1xx_response = 5;\n    HTTPDNSStartData dns_start = 6;\n    HTTPDNSDoneData dns_done = 7;\n    HTTPConnectStartData connect_start = 8;\n    HTTPConnectDoneData connect_done = 9;\n    HTTPTLSHandshakeDoneData tls_handshake_done = 10;\n    HTTPWroteRequestData wrote_request = 11;\n  }\n}\n\nmessage HTTPGetConnData {\n  string host_port = 1;\n}\n\nmessage HTTPGotConnData {\n  bool reused = 1;\n  bool was_idle = 2;\n  int64 idle_duration_ns = 3;\n}\n\nmessage HTTPGot1xxResponseData {\n  int32 code = 1;\n}\n\nmessage HTTPDNSStartData {\n  string host = 1;\n}\n\nmessage HTTPDNSDoneData {\n  bytes err = 1;\n  repeated DNSAddr addrs = 2;\n}\n\nmessage DNSAddr {\n  bytes ip = 1;\n}\n\nmessage HTTPConnectStartData {\n  string network = 1;\n  string addr = 2;\n}\n\nmessage HTTPConnectDoneData {\n  string network = 1;\n  string addr = 2;\n  bytes err = 3;\n}\n\nmessage HTTPTLSHandshakeDoneData {\n  bytes err = 1;\n  uint32 tls_version = 2;\n  uint32 cipher_suite = 3;\n  string server_name = 4;\n  string negotiated_protocol = 5;\n}\n\nmessage HTTPWroteRequestData {\n  bytes err = 1;\n}\n\nmessage LogMessage {\n  // Note: These values don't match the values used by the binary trace protocol,\n  // as these values are stored in persisted traces and therefore must maintain\n  // backwards compatibility. The binary trace protocol is versioned and doesn't\n  // have the same limitations.\n  enum Level {\n    DEBUG = 0;\n    INFO = 1;\n    ERROR = 2;\n    WARN = 3;\n    TRACE = 4;\n  }\n\n  uint64 span_id = 1;\n  uint32 goid = 2;\n  uint64 time = 3;\n  Level level = 4;\n  string msg = 5;\n  repeated LogField fields = 6;\n  StackTrace stack = 7; // null if unavailable\n}\n\nmessage LogField {\n  string key = 1;\n\n  oneof value {\n    string error_without_stack = 2; // deprecated: use error_with_stack\n    ErrWithStack error_with_stack = 13;\n    string str = 3;\n    bool bool = 4;\n    google.protobuf.Timestamp time = 5;\n    int64 dur = 6;\n    bytes uuid = 7;\n    bytes json = 8;\n    int64 int = 9;\n    uint64 uint = 10;\n    float float32 = 11;\n    double float64 = 12;\n  }\n}\n\nmessage ErrWithStack {\n  string error = 1;\n  StackTrace stack = 2;\n}\n\nmessage StackTrace {\n  repeated int64 pcs = 1;\n  repeated StackFrame frames = 2;\n}\n\nmessage StackFrame {\n  string filename = 1;\n  string func = 2;\n  int32 line = 3;\n}\n"
  },
  {
    "path": "proto/encore/engine/trace/trace_util.go",
    "content": "package trace\n\nfunc (id *TraceID) IsZero() bool {\n\treturn id == nil || (id.Low == 0 && id.High == 0)\n}\n"
  },
  {
    "path": "proto/encore/engine/trace2/trace2.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/engine/trace2/trace2.proto\n\npackage trace2\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype HTTPTraceEventCode int32\n\nconst (\n\tHTTPTraceEventCode_UNKNOWN                 HTTPTraceEventCode = 0\n\tHTTPTraceEventCode_GET_CONN                HTTPTraceEventCode = 1\n\tHTTPTraceEventCode_GOT_CONN                HTTPTraceEventCode = 2\n\tHTTPTraceEventCode_GOT_FIRST_RESPONSE_BYTE HTTPTraceEventCode = 3\n\tHTTPTraceEventCode_GOT_1XX_RESPONSE        HTTPTraceEventCode = 4\n\tHTTPTraceEventCode_DNS_START               HTTPTraceEventCode = 5\n\tHTTPTraceEventCode_DNS_DONE                HTTPTraceEventCode = 6\n\tHTTPTraceEventCode_CONNECT_START           HTTPTraceEventCode = 7\n\tHTTPTraceEventCode_CONNECT_DONE            HTTPTraceEventCode = 8\n\tHTTPTraceEventCode_TLS_HANDSHAKE_START     HTTPTraceEventCode = 9\n\tHTTPTraceEventCode_TLS_HANDSHAKE_DONE      HTTPTraceEventCode = 10\n\tHTTPTraceEventCode_WROTE_HEADERS           HTTPTraceEventCode = 11\n\tHTTPTraceEventCode_WROTE_REQUEST           HTTPTraceEventCode = 12\n\tHTTPTraceEventCode_WAIT_100_CONTINUE       HTTPTraceEventCode = 13\n\tHTTPTraceEventCode_CLOSED_BODY             HTTPTraceEventCode = 14\n)\n\n// Enum value maps for HTTPTraceEventCode.\nvar (\n\tHTTPTraceEventCode_name = map[int32]string{\n\t\t0:  \"UNKNOWN\",\n\t\t1:  \"GET_CONN\",\n\t\t2:  \"GOT_CONN\",\n\t\t3:  \"GOT_FIRST_RESPONSE_BYTE\",\n\t\t4:  \"GOT_1XX_RESPONSE\",\n\t\t5:  \"DNS_START\",\n\t\t6:  \"DNS_DONE\",\n\t\t7:  \"CONNECT_START\",\n\t\t8:  \"CONNECT_DONE\",\n\t\t9:  \"TLS_HANDSHAKE_START\",\n\t\t10: \"TLS_HANDSHAKE_DONE\",\n\t\t11: \"WROTE_HEADERS\",\n\t\t12: \"WROTE_REQUEST\",\n\t\t13: \"WAIT_100_CONTINUE\",\n\t\t14: \"CLOSED_BODY\",\n\t}\n\tHTTPTraceEventCode_value = map[string]int32{\n\t\t\"UNKNOWN\":                 0,\n\t\t\"GET_CONN\":                1,\n\t\t\"GOT_CONN\":                2,\n\t\t\"GOT_FIRST_RESPONSE_BYTE\": 3,\n\t\t\"GOT_1XX_RESPONSE\":        4,\n\t\t\"DNS_START\":               5,\n\t\t\"DNS_DONE\":                6,\n\t\t\"CONNECT_START\":           7,\n\t\t\"CONNECT_DONE\":            8,\n\t\t\"TLS_HANDSHAKE_START\":     9,\n\t\t\"TLS_HANDSHAKE_DONE\":      10,\n\t\t\"WROTE_HEADERS\":           11,\n\t\t\"WROTE_REQUEST\":           12,\n\t\t\"WAIT_100_CONTINUE\":       13,\n\t\t\"CLOSED_BODY\":             14,\n\t}\n)\n\nfunc (x HTTPTraceEventCode) Enum() *HTTPTraceEventCode {\n\tp := new(HTTPTraceEventCode)\n\t*p = x\n\treturn p\n}\n\nfunc (x HTTPTraceEventCode) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (HTTPTraceEventCode) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace2_trace2_proto_enumTypes[0].Descriptor()\n}\n\nfunc (HTTPTraceEventCode) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace2_trace2_proto_enumTypes[0]\n}\n\nfunc (x HTTPTraceEventCode) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use HTTPTraceEventCode.Descriptor instead.\nfunc (HTTPTraceEventCode) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{0}\n}\n\ntype StatusCode int32\n\nconst (\n\tStatusCode_STATUS_CODE_OK                  StatusCode = 0\n\tStatusCode_STATUS_CODE_CANCELED            StatusCode = 1\n\tStatusCode_STATUS_CODE_UNKNOWN             StatusCode = 2\n\tStatusCode_STATUS_CODE_INVALID_ARGUMENT    StatusCode = 3\n\tStatusCode_STATUS_CODE_DEADLINE_EXCEEDED   StatusCode = 4\n\tStatusCode_STATUS_CODE_NOT_FOUND           StatusCode = 5\n\tStatusCode_STATUS_CODE_ALREADY_EXISTS      StatusCode = 6\n\tStatusCode_STATUS_CODE_PERMISSION_DENIED   StatusCode = 7\n\tStatusCode_STATUS_CODE_RESOURCE_EXHAUSTED  StatusCode = 8\n\tStatusCode_STATUS_CODE_FAILED_PRECONDITION StatusCode = 9\n\tStatusCode_STATUS_CODE_ABORTED             StatusCode = 10\n\tStatusCode_STATUS_CODE_OUT_OF_RANGE        StatusCode = 11\n\tStatusCode_STATUS_CODE_UNIMPLEMENTED       StatusCode = 12\n\tStatusCode_STATUS_CODE_INTERNAL            StatusCode = 13\n\tStatusCode_STATUS_CODE_UNAVAILABLE         StatusCode = 14\n\tStatusCode_STATUS_CODE_DATA_LOSS           StatusCode = 15\n\tStatusCode_STATUS_CODE_UNAUTHENTICATED     StatusCode = 16\n)\n\n// Enum value maps for StatusCode.\nvar (\n\tStatusCode_name = map[int32]string{\n\t\t0:  \"STATUS_CODE_OK\",\n\t\t1:  \"STATUS_CODE_CANCELED\",\n\t\t2:  \"STATUS_CODE_UNKNOWN\",\n\t\t3:  \"STATUS_CODE_INVALID_ARGUMENT\",\n\t\t4:  \"STATUS_CODE_DEADLINE_EXCEEDED\",\n\t\t5:  \"STATUS_CODE_NOT_FOUND\",\n\t\t6:  \"STATUS_CODE_ALREADY_EXISTS\",\n\t\t7:  \"STATUS_CODE_PERMISSION_DENIED\",\n\t\t8:  \"STATUS_CODE_RESOURCE_EXHAUSTED\",\n\t\t9:  \"STATUS_CODE_FAILED_PRECONDITION\",\n\t\t10: \"STATUS_CODE_ABORTED\",\n\t\t11: \"STATUS_CODE_OUT_OF_RANGE\",\n\t\t12: \"STATUS_CODE_UNIMPLEMENTED\",\n\t\t13: \"STATUS_CODE_INTERNAL\",\n\t\t14: \"STATUS_CODE_UNAVAILABLE\",\n\t\t15: \"STATUS_CODE_DATA_LOSS\",\n\t\t16: \"STATUS_CODE_UNAUTHENTICATED\",\n\t}\n\tStatusCode_value = map[string]int32{\n\t\t\"STATUS_CODE_OK\":                  0,\n\t\t\"STATUS_CODE_CANCELED\":            1,\n\t\t\"STATUS_CODE_UNKNOWN\":             2,\n\t\t\"STATUS_CODE_INVALID_ARGUMENT\":    3,\n\t\t\"STATUS_CODE_DEADLINE_EXCEEDED\":   4,\n\t\t\"STATUS_CODE_NOT_FOUND\":           5,\n\t\t\"STATUS_CODE_ALREADY_EXISTS\":      6,\n\t\t\"STATUS_CODE_PERMISSION_DENIED\":   7,\n\t\t\"STATUS_CODE_RESOURCE_EXHAUSTED\":  8,\n\t\t\"STATUS_CODE_FAILED_PRECONDITION\": 9,\n\t\t\"STATUS_CODE_ABORTED\":             10,\n\t\t\"STATUS_CODE_OUT_OF_RANGE\":        11,\n\t\t\"STATUS_CODE_UNIMPLEMENTED\":       12,\n\t\t\"STATUS_CODE_INTERNAL\":            13,\n\t\t\"STATUS_CODE_UNAVAILABLE\":         14,\n\t\t\"STATUS_CODE_DATA_LOSS\":           15,\n\t\t\"STATUS_CODE_UNAUTHENTICATED\":     16,\n\t}\n)\n\nfunc (x StatusCode) Enum() *StatusCode {\n\tp := new(StatusCode)\n\t*p = x\n\treturn p\n}\n\nfunc (x StatusCode) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (StatusCode) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace2_trace2_proto_enumTypes[1].Descriptor()\n}\n\nfunc (StatusCode) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace2_trace2_proto_enumTypes[1]\n}\n\nfunc (x StatusCode) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use StatusCode.Descriptor instead.\nfunc (StatusCode) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{1}\n}\n\ntype SpanSummary_SpanType int32\n\nconst (\n\tSpanSummary_UNKNOWN        SpanSummary_SpanType = 0\n\tSpanSummary_REQUEST        SpanSummary_SpanType = 1\n\tSpanSummary_AUTH           SpanSummary_SpanType = 2\n\tSpanSummary_PUBSUB_MESSAGE SpanSummary_SpanType = 3\n\tSpanSummary_TEST           SpanSummary_SpanType = 4\n)\n\n// Enum value maps for SpanSummary_SpanType.\nvar (\n\tSpanSummary_SpanType_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"REQUEST\",\n\t\t2: \"AUTH\",\n\t\t3: \"PUBSUB_MESSAGE\",\n\t\t4: \"TEST\",\n\t}\n\tSpanSummary_SpanType_value = map[string]int32{\n\t\t\"UNKNOWN\":        0,\n\t\t\"REQUEST\":        1,\n\t\t\"AUTH\":           2,\n\t\t\"PUBSUB_MESSAGE\": 3,\n\t\t\"TEST\":           4,\n\t}\n)\n\nfunc (x SpanSummary_SpanType) Enum() *SpanSummary_SpanType {\n\tp := new(SpanSummary_SpanType)\n\t*p = x\n\treturn p\n}\n\nfunc (x SpanSummary_SpanType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (SpanSummary_SpanType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace2_trace2_proto_enumTypes[2].Descriptor()\n}\n\nfunc (SpanSummary_SpanType) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace2_trace2_proto_enumTypes[2]\n}\n\nfunc (x SpanSummary_SpanType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use SpanSummary_SpanType.Descriptor instead.\nfunc (SpanSummary_SpanType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{0, 0}\n}\n\ntype DBTransactionEnd_CompletionType int32\n\nconst (\n\tDBTransactionEnd_ROLLBACK DBTransactionEnd_CompletionType = 0\n\tDBTransactionEnd_COMMIT   DBTransactionEnd_CompletionType = 1\n)\n\n// Enum value maps for DBTransactionEnd_CompletionType.\nvar (\n\tDBTransactionEnd_CompletionType_name = map[int32]string{\n\t\t0: \"ROLLBACK\",\n\t\t1: \"COMMIT\",\n\t}\n\tDBTransactionEnd_CompletionType_value = map[string]int32{\n\t\t\"ROLLBACK\": 0,\n\t\t\"COMMIT\":   1,\n\t}\n)\n\nfunc (x DBTransactionEnd_CompletionType) Enum() *DBTransactionEnd_CompletionType {\n\tp := new(DBTransactionEnd_CompletionType)\n\t*p = x\n\treturn p\n}\n\nfunc (x DBTransactionEnd_CompletionType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (DBTransactionEnd_CompletionType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace2_trace2_proto_enumTypes[3].Descriptor()\n}\n\nfunc (DBTransactionEnd_CompletionType) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace2_trace2_proto_enumTypes[3]\n}\n\nfunc (x DBTransactionEnd_CompletionType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use DBTransactionEnd_CompletionType.Descriptor instead.\nfunc (DBTransactionEnd_CompletionType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{20, 0}\n}\n\ntype CacheCallEnd_Result int32\n\nconst (\n\tCacheCallEnd_UNKNOWN     CacheCallEnd_Result = 0\n\tCacheCallEnd_OK          CacheCallEnd_Result = 1\n\tCacheCallEnd_NO_SUCH_KEY CacheCallEnd_Result = 2\n\tCacheCallEnd_CONFLICT    CacheCallEnd_Result = 3\n\tCacheCallEnd_ERR         CacheCallEnd_Result = 4\n)\n\n// Enum value maps for CacheCallEnd_Result.\nvar (\n\tCacheCallEnd_Result_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"OK\",\n\t\t2: \"NO_SUCH_KEY\",\n\t\t3: \"CONFLICT\",\n\t\t4: \"ERR\",\n\t}\n\tCacheCallEnd_Result_value = map[string]int32{\n\t\t\"UNKNOWN\":     0,\n\t\t\"OK\":          1,\n\t\t\"NO_SUCH_KEY\": 2,\n\t\t\"CONFLICT\":    3,\n\t\t\"ERR\":         4,\n\t}\n)\n\nfunc (x CacheCallEnd_Result) Enum() *CacheCallEnd_Result {\n\tp := new(CacheCallEnd_Result)\n\t*p = x\n\treturn p\n}\n\nfunc (x CacheCallEnd_Result) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (CacheCallEnd_Result) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace2_trace2_proto_enumTypes[4].Descriptor()\n}\n\nfunc (CacheCallEnd_Result) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace2_trace2_proto_enumTypes[4]\n}\n\nfunc (x CacheCallEnd_Result) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use CacheCallEnd_Result.Descriptor instead.\nfunc (CacheCallEnd_Result) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{28, 0}\n}\n\n// Note: These values don't match the values used by the binary trace protocol,\n// as these values are stored in persisted traces and therefore must maintain\n// backwards compatibility. The binary trace protocol is versioned and doesn't\n// have the same limitations.\ntype LogMessage_Level int32\n\nconst (\n\tLogMessage_DEBUG LogMessage_Level = 0\n\tLogMessage_INFO  LogMessage_Level = 1\n\tLogMessage_ERROR LogMessage_Level = 2\n\tLogMessage_WARN  LogMessage_Level = 3\n\tLogMessage_TRACE LogMessage_Level = 4\n)\n\n// Enum value maps for LogMessage_Level.\nvar (\n\tLogMessage_Level_name = map[int32]string{\n\t\t0: \"DEBUG\",\n\t\t1: \"INFO\",\n\t\t2: \"ERROR\",\n\t\t3: \"WARN\",\n\t\t4: \"TRACE\",\n\t}\n\tLogMessage_Level_value = map[string]int32{\n\t\t\"DEBUG\": 0,\n\t\t\"INFO\":  1,\n\t\t\"ERROR\": 2,\n\t\t\"WARN\":  3,\n\t\t\"TRACE\": 4,\n\t}\n)\n\nfunc (x LogMessage_Level) Enum() *LogMessage_Level {\n\tp := new(LogMessage_Level)\n\t*p = x\n\treturn p\n}\n\nfunc (x LogMessage_Level) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (LogMessage_Level) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_engine_trace2_trace2_proto_enumTypes[5].Descriptor()\n}\n\nfunc (LogMessage_Level) Type() protoreflect.EnumType {\n\treturn &file_encore_engine_trace2_trace2_proto_enumTypes[5]\n}\n\nfunc (x LogMessage_Level) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use LogMessage_Level.Descriptor instead.\nfunc (LogMessage_Level) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{60, 0}\n}\n\n// SpanSummary summarizes a span for display purposes.\ntype SpanSummary struct {\n\tstate            protoimpl.MessageState `protogen:\"open.v1\"`\n\tTraceId          string                 `protobuf:\"bytes,1,opt,name=trace_id,json=traceId,proto3\" json:\"trace_id,omitempty\"`\n\tSpanId           string                 `protobuf:\"bytes,2,opt,name=span_id,json=spanId,proto3\" json:\"span_id,omitempty\"`\n\tType             SpanSummary_SpanType   `protobuf:\"varint,3,opt,name=type,proto3,enum=encore.engine.trace2.SpanSummary_SpanType\" json:\"type,omitempty\"`\n\tIsRoot           bool                   `protobuf:\"varint,4,opt,name=is_root,json=isRoot,proto3\" json:\"is_root,omitempty\"`                        // whether it's a root request\n\tIsError          bool                   `protobuf:\"varint,5,opt,name=is_error,json=isError,proto3\" json:\"is_error,omitempty\"`                     // whether the request failed\n\tDeployedCommit   string                 `protobuf:\"bytes,6,opt,name=deployed_commit,json=deployedCommit,proto3\" json:\"deployed_commit,omitempty\"` // the commit hash of the running service\n\tStartedAt        *timestamppb.Timestamp `protobuf:\"bytes,7,opt,name=started_at,json=startedAt,proto3\" json:\"started_at,omitempty\"`\n\tDurationNanos    uint64                 `protobuf:\"varint,8,opt,name=duration_nanos,json=durationNanos,proto3\" json:\"duration_nanos,omitempty\"`\n\tServiceName      string                 `protobuf:\"bytes,9,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tEndpointName     *string                `protobuf:\"bytes,10,opt,name=endpoint_name,json=endpointName,proto3,oneof\" json:\"endpoint_name,omitempty\"`\n\tTopicName        *string                `protobuf:\"bytes,11,opt,name=topic_name,json=topicName,proto3,oneof\" json:\"topic_name,omitempty\"`\n\tSubscriptionName *string                `protobuf:\"bytes,12,opt,name=subscription_name,json=subscriptionName,proto3,oneof\" json:\"subscription_name,omitempty\"`\n\tMessageId        *string                `protobuf:\"bytes,13,opt,name=message_id,json=messageId,proto3,oneof\" json:\"message_id,omitempty\"`\n\tTestSkipped      *bool                  `protobuf:\"varint,14,opt,name=test_skipped,json=testSkipped,proto3,oneof\" json:\"test_skipped,omitempty\"`         // whether the test was skipped\n\tSrcFile          *string                `protobuf:\"bytes,15,opt,name=src_file,json=srcFile,proto3,oneof\" json:\"src_file,omitempty\"`                      // the source file where the span was started (if available)\n\tSrcLine          *uint32                `protobuf:\"varint,16,opt,name=src_line,json=srcLine,proto3,oneof\" json:\"src_line,omitempty\"`                     // the source line where the span was started (if available)\n\tParentSpanId     *string                `protobuf:\"bytes,17,opt,name=parent_span_id,json=parentSpanId,proto3,oneof\" json:\"parent_span_id,omitempty\"`     // parent of the span if it's a child, if this is not populated then it's a root\n\tCallerEventId    *uint64                `protobuf:\"varint,18,opt,name=caller_event_id,json=callerEventId,proto3,oneof\" json:\"caller_event_id,omitempty\"` // the event id of the call that started this span\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *SpanSummary) Reset() {\n\t*x = SpanSummary{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SpanSummary) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SpanSummary) ProtoMessage() {}\n\nfunc (x *SpanSummary) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SpanSummary.ProtoReflect.Descriptor instead.\nfunc (*SpanSummary) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *SpanSummary) GetTraceId() string {\n\tif x != nil {\n\t\treturn x.TraceId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetSpanId() string {\n\tif x != nil {\n\t\treturn x.SpanId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetType() SpanSummary_SpanType {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn SpanSummary_UNKNOWN\n}\n\nfunc (x *SpanSummary) GetIsRoot() bool {\n\tif x != nil {\n\t\treturn x.IsRoot\n\t}\n\treturn false\n}\n\nfunc (x *SpanSummary) GetIsError() bool {\n\tif x != nil {\n\t\treturn x.IsError\n\t}\n\treturn false\n}\n\nfunc (x *SpanSummary) GetDeployedCommit() string {\n\tif x != nil {\n\t\treturn x.DeployedCommit\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetStartedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.StartedAt\n\t}\n\treturn nil\n}\n\nfunc (x *SpanSummary) GetDurationNanos() uint64 {\n\tif x != nil {\n\t\treturn x.DurationNanos\n\t}\n\treturn 0\n}\n\nfunc (x *SpanSummary) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetEndpointName() string {\n\tif x != nil && x.EndpointName != nil {\n\t\treturn *x.EndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetTopicName() string {\n\tif x != nil && x.TopicName != nil {\n\t\treturn *x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetSubscriptionName() string {\n\tif x != nil && x.SubscriptionName != nil {\n\t\treturn *x.SubscriptionName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetMessageId() string {\n\tif x != nil && x.MessageId != nil {\n\t\treturn *x.MessageId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetTestSkipped() bool {\n\tif x != nil && x.TestSkipped != nil {\n\t\treturn *x.TestSkipped\n\t}\n\treturn false\n}\n\nfunc (x *SpanSummary) GetSrcFile() string {\n\tif x != nil && x.SrcFile != nil {\n\t\treturn *x.SrcFile\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetSrcLine() uint32 {\n\tif x != nil && x.SrcLine != nil {\n\t\treturn *x.SrcLine\n\t}\n\treturn 0\n}\n\nfunc (x *SpanSummary) GetParentSpanId() string {\n\tif x != nil && x.ParentSpanId != nil {\n\t\treturn *x.ParentSpanId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanSummary) GetCallerEventId() uint64 {\n\tif x != nil && x.CallerEventId != nil {\n\t\treturn *x.CallerEventId\n\t}\n\treturn 0\n}\n\ntype TraceID struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHigh          uint64                 `protobuf:\"varint,1,opt,name=high,proto3\" json:\"high,omitempty\"`\n\tLow           uint64                 `protobuf:\"varint,2,opt,name=low,proto3\" json:\"low,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TraceID) Reset() {\n\t*x = TraceID{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TraceID) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TraceID) ProtoMessage() {}\n\nfunc (x *TraceID) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TraceID.ProtoReflect.Descriptor instead.\nfunc (*TraceID) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *TraceID) GetHigh() uint64 {\n\tif x != nil {\n\t\treturn x.High\n\t}\n\treturn 0\n}\n\nfunc (x *TraceID) GetLow() uint64 {\n\tif x != nil {\n\t\treturn x.Low\n\t}\n\treturn 0\n}\n\ntype EventList struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tEvents        []*TraceEvent          `protobuf:\"bytes,1,rep,name=events,proto3\" json:\"events,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *EventList) Reset() {\n\t*x = EventList{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *EventList) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*EventList) ProtoMessage() {}\n\nfunc (x *EventList) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use EventList.ProtoReflect.Descriptor instead.\nfunc (*EventList) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *EventList) GetEvents() []*TraceEvent {\n\tif x != nil {\n\t\treturn x.Events\n\t}\n\treturn nil\n}\n\ntype TraceEvent struct {\n\tstate     protoimpl.MessageState `protogen:\"open.v1\"`\n\tTraceId   *TraceID               `protobuf:\"bytes,1,opt,name=trace_id,json=traceId,proto3\" json:\"trace_id,omitempty\"`\n\tSpanId    uint64                 `protobuf:\"varint,2,opt,name=span_id,json=spanId,proto3\" json:\"span_id,omitempty\"`\n\tEventId   uint64                 `protobuf:\"varint,3,opt,name=event_id,json=eventId,proto3\" json:\"event_id,omitempty\"`\n\tEventTime *timestamppb.Timestamp `protobuf:\"bytes,4,opt,name=event_time,json=eventTime,proto3\" json:\"event_time,omitempty\"`\n\t// Types that are valid to be assigned to Event:\n\t//\n\t//\t*TraceEvent_SpanStart\n\t//\t*TraceEvent_SpanEnd\n\t//\t*TraceEvent_SpanEvent\n\tEvent         isTraceEvent_Event `protobuf_oneof:\"event\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TraceEvent) Reset() {\n\t*x = TraceEvent{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TraceEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TraceEvent) ProtoMessage() {}\n\nfunc (x *TraceEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TraceEvent.ProtoReflect.Descriptor instead.\nfunc (*TraceEvent) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *TraceEvent) GetTraceId() *TraceID {\n\tif x != nil {\n\t\treturn x.TraceId\n\t}\n\treturn nil\n}\n\nfunc (x *TraceEvent) GetSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.SpanId\n\t}\n\treturn 0\n}\n\nfunc (x *TraceEvent) GetEventId() uint64 {\n\tif x != nil {\n\t\treturn x.EventId\n\t}\n\treturn 0\n}\n\nfunc (x *TraceEvent) GetEventTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.EventTime\n\t}\n\treturn nil\n}\n\nfunc (x *TraceEvent) GetEvent() isTraceEvent_Event {\n\tif x != nil {\n\t\treturn x.Event\n\t}\n\treturn nil\n}\n\nfunc (x *TraceEvent) GetSpanStart() *SpanStart {\n\tif x != nil {\n\t\tif x, ok := x.Event.(*TraceEvent_SpanStart); ok {\n\t\t\treturn x.SpanStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceEvent) GetSpanEnd() *SpanEnd {\n\tif x != nil {\n\t\tif x, ok := x.Event.(*TraceEvent_SpanEnd); ok {\n\t\t\treturn x.SpanEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceEvent) GetSpanEvent() *SpanEvent {\n\tif x != nil {\n\t\tif x, ok := x.Event.(*TraceEvent_SpanEvent); ok {\n\t\t\treturn x.SpanEvent\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isTraceEvent_Event interface {\n\tisTraceEvent_Event()\n}\n\ntype TraceEvent_SpanStart struct {\n\tSpanStart *SpanStart `protobuf:\"bytes,10,opt,name=span_start,json=spanStart,proto3,oneof\"`\n}\n\ntype TraceEvent_SpanEnd struct {\n\tSpanEnd *SpanEnd `protobuf:\"bytes,11,opt,name=span_end,json=spanEnd,proto3,oneof\"`\n}\n\ntype TraceEvent_SpanEvent struct {\n\tSpanEvent *SpanEvent `protobuf:\"bytes,12,opt,name=span_event,json=spanEvent,proto3,oneof\"`\n}\n\nfunc (*TraceEvent_SpanStart) isTraceEvent_Event() {}\n\nfunc (*TraceEvent_SpanEnd) isTraceEvent_Event() {}\n\nfunc (*TraceEvent_SpanEvent) isTraceEvent_Event() {}\n\ntype SpanStart struct {\n\tstate                 protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid                  uint32                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tParentTraceId         *TraceID               `protobuf:\"bytes,2,opt,name=parent_trace_id,json=parentTraceId,proto3,oneof\" json:\"parent_trace_id,omitempty\"`\n\tParentSpanId          *uint64                `protobuf:\"varint,3,opt,name=parent_span_id,json=parentSpanId,proto3,oneof\" json:\"parent_span_id,omitempty\"`\n\tCallerEventId         *uint64                `protobuf:\"varint,4,opt,name=caller_event_id,json=callerEventId,proto3,oneof\" json:\"caller_event_id,omitempty\"`\n\tExternalCorrelationId *string                `protobuf:\"bytes,5,opt,name=external_correlation_id,json=externalCorrelationId,proto3,oneof\" json:\"external_correlation_id,omitempty\"`\n\tDefLoc                *uint32                `protobuf:\"varint,6,opt,name=def_loc,json=defLoc,proto3,oneof\" json:\"def_loc,omitempty\"`\n\t// Types that are valid to be assigned to Data:\n\t//\n\t//\t*SpanStart_Request\n\t//\t*SpanStart_Auth\n\t//\t*SpanStart_PubsubMessage\n\t//\t*SpanStart_Test\n\tData          isSpanStart_Data `protobuf_oneof:\"data\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SpanStart) Reset() {\n\t*x = SpanStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SpanStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SpanStart) ProtoMessage() {}\n\nfunc (x *SpanStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SpanStart.ProtoReflect.Descriptor instead.\nfunc (*SpanStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *SpanStart) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *SpanStart) GetParentTraceId() *TraceID {\n\tif x != nil {\n\t\treturn x.ParentTraceId\n\t}\n\treturn nil\n}\n\nfunc (x *SpanStart) GetParentSpanId() uint64 {\n\tif x != nil && x.ParentSpanId != nil {\n\t\treturn *x.ParentSpanId\n\t}\n\treturn 0\n}\n\nfunc (x *SpanStart) GetCallerEventId() uint64 {\n\tif x != nil && x.CallerEventId != nil {\n\t\treturn *x.CallerEventId\n\t}\n\treturn 0\n}\n\nfunc (x *SpanStart) GetExternalCorrelationId() string {\n\tif x != nil && x.ExternalCorrelationId != nil {\n\t\treturn *x.ExternalCorrelationId\n\t}\n\treturn \"\"\n}\n\nfunc (x *SpanStart) GetDefLoc() uint32 {\n\tif x != nil && x.DefLoc != nil {\n\t\treturn *x.DefLoc\n\t}\n\treturn 0\n}\n\nfunc (x *SpanStart) GetData() isSpanStart_Data {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *SpanStart) GetRequest() *RequestSpanStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanStart_Request); ok {\n\t\t\treturn x.Request\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanStart) GetAuth() *AuthSpanStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanStart_Auth); ok {\n\t\t\treturn x.Auth\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanStart) GetPubsubMessage() *PubsubMessageSpanStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanStart_PubsubMessage); ok {\n\t\t\treturn x.PubsubMessage\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanStart) GetTest() *TestSpanStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanStart_Test); ok {\n\t\t\treturn x.Test\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isSpanStart_Data interface {\n\tisSpanStart_Data()\n}\n\ntype SpanStart_Request struct {\n\tRequest *RequestSpanStart `protobuf:\"bytes,10,opt,name=request,proto3,oneof\"`\n}\n\ntype SpanStart_Auth struct {\n\tAuth *AuthSpanStart `protobuf:\"bytes,11,opt,name=auth,proto3,oneof\"`\n}\n\ntype SpanStart_PubsubMessage struct {\n\tPubsubMessage *PubsubMessageSpanStart `protobuf:\"bytes,12,opt,name=pubsub_message,json=pubsubMessage,proto3,oneof\"`\n}\n\ntype SpanStart_Test struct {\n\tTest *TestSpanStart `protobuf:\"bytes,13,opt,name=test,proto3,oneof\"`\n}\n\nfunc (*SpanStart_Request) isSpanStart_Data() {}\n\nfunc (*SpanStart_Auth) isSpanStart_Data() {}\n\nfunc (*SpanStart_PubsubMessage) isSpanStart_Data() {}\n\nfunc (*SpanStart_Test) isSpanStart_Data() {}\n\ntype SpanEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tDurationNanos uint64                 `protobuf:\"varint,1,opt,name=duration_nanos,json=durationNanos,proto3\" json:\"duration_nanos,omitempty\"`\n\tError         *Error                 `protobuf:\"bytes,2,opt,name=error,proto3,oneof\" json:\"error,omitempty\"`\n\t// panic_stack is the stack trace if the span ended due to a panic\n\tPanicStack    *StackTrace `protobuf:\"bytes,3,opt,name=panic_stack,json=panicStack,proto3,oneof\" json:\"panic_stack,omitempty\"`\n\tParentTraceId *TraceID    `protobuf:\"bytes,4,opt,name=parent_trace_id,json=parentTraceId,proto3,oneof\" json:\"parent_trace_id,omitempty\"`\n\tParentSpanId  *uint64     `protobuf:\"varint,5,opt,name=parent_span_id,json=parentSpanId,proto3,oneof\" json:\"parent_span_id,omitempty\"`\n\tStatusCode    StatusCode  `protobuf:\"varint,6,opt,name=status_code,json=statusCode,proto3,enum=encore.engine.trace2.StatusCode\" json:\"status_code,omitempty\"`\n\t// Types that are valid to be assigned to Data:\n\t//\n\t//\t*SpanEnd_Request\n\t//\t*SpanEnd_Auth\n\t//\t*SpanEnd_PubsubMessage\n\t//\t*SpanEnd_Test\n\tData          isSpanEnd_Data `protobuf_oneof:\"data\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SpanEnd) Reset() {\n\t*x = SpanEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SpanEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SpanEnd) ProtoMessage() {}\n\nfunc (x *SpanEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SpanEnd.ProtoReflect.Descriptor instead.\nfunc (*SpanEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *SpanEnd) GetDurationNanos() uint64 {\n\tif x != nil {\n\t\treturn x.DurationNanos\n\t}\n\treturn 0\n}\n\nfunc (x *SpanEnd) GetError() *Error {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetPanicStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.PanicStack\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetParentTraceId() *TraceID {\n\tif x != nil {\n\t\treturn x.ParentTraceId\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetParentSpanId() uint64 {\n\tif x != nil && x.ParentSpanId != nil {\n\t\treturn *x.ParentSpanId\n\t}\n\treturn 0\n}\n\nfunc (x *SpanEnd) GetStatusCode() StatusCode {\n\tif x != nil {\n\t\treturn x.StatusCode\n\t}\n\treturn StatusCode_STATUS_CODE_OK\n}\n\nfunc (x *SpanEnd) GetData() isSpanEnd_Data {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetRequest() *RequestSpanEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEnd_Request); ok {\n\t\t\treturn x.Request\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetAuth() *AuthSpanEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEnd_Auth); ok {\n\t\t\treturn x.Auth\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetPubsubMessage() *PubsubMessageSpanEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEnd_PubsubMessage); ok {\n\t\t\treturn x.PubsubMessage\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEnd) GetTest() *TestSpanEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEnd_Test); ok {\n\t\t\treturn x.Test\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isSpanEnd_Data interface {\n\tisSpanEnd_Data()\n}\n\ntype SpanEnd_Request struct {\n\tRequest *RequestSpanEnd `protobuf:\"bytes,10,opt,name=request,proto3,oneof\"`\n}\n\ntype SpanEnd_Auth struct {\n\tAuth *AuthSpanEnd `protobuf:\"bytes,11,opt,name=auth,proto3,oneof\"`\n}\n\ntype SpanEnd_PubsubMessage struct {\n\tPubsubMessage *PubsubMessageSpanEnd `protobuf:\"bytes,12,opt,name=pubsub_message,json=pubsubMessage,proto3,oneof\"`\n}\n\ntype SpanEnd_Test struct {\n\tTest *TestSpanEnd `protobuf:\"bytes,13,opt,name=test,proto3,oneof\"`\n}\n\nfunc (*SpanEnd_Request) isSpanEnd_Data() {}\n\nfunc (*SpanEnd_Auth) isSpanEnd_Data() {}\n\nfunc (*SpanEnd_PubsubMessage) isSpanEnd_Data() {}\n\nfunc (*SpanEnd_Test) isSpanEnd_Data() {}\n\ntype RequestSpanStart struct {\n\tstate            protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName      string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tEndpointName     string                 `protobuf:\"bytes,2,opt,name=endpoint_name,json=endpointName,proto3\" json:\"endpoint_name,omitempty\"`\n\tHttpMethod       string                 `protobuf:\"bytes,3,opt,name=http_method,json=httpMethod,proto3\" json:\"http_method,omitempty\"`\n\tPath             string                 `protobuf:\"bytes,4,opt,name=path,proto3\" json:\"path,omitempty\"`\n\tPathParams       []string               `protobuf:\"bytes,5,rep,name=path_params,json=pathParams,proto3\" json:\"path_params,omitempty\"`\n\tRequestHeaders   map[string]string      `protobuf:\"bytes,6,rep,name=request_headers,json=requestHeaders,proto3\" json:\"request_headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tRequestPayload   []byte                 `protobuf:\"bytes,7,opt,name=request_payload,json=requestPayload,proto3,oneof\" json:\"request_payload,omitempty\"`\n\tExtCorrelationId *string                `protobuf:\"bytes,8,opt,name=ext_correlation_id,json=extCorrelationId,proto3,oneof\" json:\"ext_correlation_id,omitempty\"`\n\tUid              *string                `protobuf:\"bytes,9,opt,name=uid,proto3,oneof\" json:\"uid,omitempty\"`\n\t// mocked is true if the request was handled by a mock\n\tMocked        bool `protobuf:\"varint,10,opt,name=mocked,proto3\" json:\"mocked,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RequestSpanStart) Reset() {\n\t*x = RequestSpanStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RequestSpanStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RequestSpanStart) ProtoMessage() {}\n\nfunc (x *RequestSpanStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RequestSpanStart.ProtoReflect.Descriptor instead.\nfunc (*RequestSpanStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *RequestSpanStart) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanStart) GetEndpointName() string {\n\tif x != nil {\n\t\treturn x.EndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanStart) GetHttpMethod() string {\n\tif x != nil {\n\t\treturn x.HttpMethod\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanStart) GetPath() string {\n\tif x != nil {\n\t\treturn x.Path\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanStart) GetPathParams() []string {\n\tif x != nil {\n\t\treturn x.PathParams\n\t}\n\treturn nil\n}\n\nfunc (x *RequestSpanStart) GetRequestHeaders() map[string]string {\n\tif x != nil {\n\t\treturn x.RequestHeaders\n\t}\n\treturn nil\n}\n\nfunc (x *RequestSpanStart) GetRequestPayload() []byte {\n\tif x != nil {\n\t\treturn x.RequestPayload\n\t}\n\treturn nil\n}\n\nfunc (x *RequestSpanStart) GetExtCorrelationId() string {\n\tif x != nil && x.ExtCorrelationId != nil {\n\t\treturn *x.ExtCorrelationId\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanStart) GetUid() string {\n\tif x != nil && x.Uid != nil {\n\t\treturn *x.Uid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanStart) GetMocked() bool {\n\tif x != nil {\n\t\treturn x.Mocked\n\t}\n\treturn false\n}\n\ntype RequestSpanEnd struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Repeat service/endpoint name here to make it possible\n\t// to consume end events without having to look up the start.\n\tServiceName     string            `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tEndpointName    string            `protobuf:\"bytes,2,opt,name=endpoint_name,json=endpointName,proto3\" json:\"endpoint_name,omitempty\"`\n\tHttpStatusCode  uint32            `protobuf:\"varint,3,opt,name=http_status_code,json=httpStatusCode,proto3\" json:\"http_status_code,omitempty\"`\n\tResponseHeaders map[string]string `protobuf:\"bytes,4,rep,name=response_headers,json=responseHeaders,proto3\" json:\"response_headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tResponsePayload []byte            `protobuf:\"bytes,5,opt,name=response_payload,json=responsePayload,proto3,oneof\" json:\"response_payload,omitempty\"`\n\tCallerEventId   *uint64           `protobuf:\"varint,6,opt,name=caller_event_id,json=callerEventId,proto3,oneof\" json:\"caller_event_id,omitempty\"`\n\tUid             *string           `protobuf:\"bytes,7,opt,name=uid,proto3,oneof\" json:\"uid,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *RequestSpanEnd) Reset() {\n\t*x = RequestSpanEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RequestSpanEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RequestSpanEnd) ProtoMessage() {}\n\nfunc (x *RequestSpanEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RequestSpanEnd.ProtoReflect.Descriptor instead.\nfunc (*RequestSpanEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *RequestSpanEnd) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanEnd) GetEndpointName() string {\n\tif x != nil {\n\t\treturn x.EndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RequestSpanEnd) GetHttpStatusCode() uint32 {\n\tif x != nil {\n\t\treturn x.HttpStatusCode\n\t}\n\treturn 0\n}\n\nfunc (x *RequestSpanEnd) GetResponseHeaders() map[string]string {\n\tif x != nil {\n\t\treturn x.ResponseHeaders\n\t}\n\treturn nil\n}\n\nfunc (x *RequestSpanEnd) GetResponsePayload() []byte {\n\tif x != nil {\n\t\treturn x.ResponsePayload\n\t}\n\treturn nil\n}\n\nfunc (x *RequestSpanEnd) GetCallerEventId() uint64 {\n\tif x != nil && x.CallerEventId != nil {\n\t\treturn *x.CallerEventId\n\t}\n\treturn 0\n}\n\nfunc (x *RequestSpanEnd) GetUid() string {\n\tif x != nil && x.Uid != nil {\n\t\treturn *x.Uid\n\t}\n\treturn \"\"\n}\n\ntype AuthSpanStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tEndpointName  string                 `protobuf:\"bytes,2,opt,name=endpoint_name,json=endpointName,proto3\" json:\"endpoint_name,omitempty\"`\n\tAuthPayload   []byte                 `protobuf:\"bytes,3,opt,name=auth_payload,json=authPayload,proto3,oneof\" json:\"auth_payload,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AuthSpanStart) Reset() {\n\t*x = AuthSpanStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AuthSpanStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AuthSpanStart) ProtoMessage() {}\n\nfunc (x *AuthSpanStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AuthSpanStart.ProtoReflect.Descriptor instead.\nfunc (*AuthSpanStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *AuthSpanStart) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthSpanStart) GetEndpointName() string {\n\tif x != nil {\n\t\treturn x.EndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthSpanStart) GetAuthPayload() []byte {\n\tif x != nil {\n\t\treturn x.AuthPayload\n\t}\n\treturn nil\n}\n\ntype AuthSpanEnd struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Repeat service/endpoint name here to make it possible\n\t// to consume end events without having to look up the start.\n\tServiceName   string `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tEndpointName  string `protobuf:\"bytes,2,opt,name=endpoint_name,json=endpointName,proto3\" json:\"endpoint_name,omitempty\"`\n\tUid           string `protobuf:\"bytes,3,opt,name=uid,proto3\" json:\"uid,omitempty\"`\n\tUserData      []byte `protobuf:\"bytes,4,opt,name=user_data,json=userData,proto3,oneof\" json:\"user_data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AuthSpanEnd) Reset() {\n\t*x = AuthSpanEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AuthSpanEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AuthSpanEnd) ProtoMessage() {}\n\nfunc (x *AuthSpanEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AuthSpanEnd.ProtoReflect.Descriptor instead.\nfunc (*AuthSpanEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *AuthSpanEnd) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthSpanEnd) GetEndpointName() string {\n\tif x != nil {\n\t\treturn x.EndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthSpanEnd) GetUid() string {\n\tif x != nil {\n\t\treturn x.Uid\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthSpanEnd) GetUserData() []byte {\n\tif x != nil {\n\t\treturn x.UserData\n\t}\n\treturn nil\n}\n\ntype PubsubMessageSpanStart struct {\n\tstate            protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName      string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tTopicName        string                 `protobuf:\"bytes,2,opt,name=topic_name,json=topicName,proto3\" json:\"topic_name,omitempty\"`\n\tSubscriptionName string                 `protobuf:\"bytes,3,opt,name=subscription_name,json=subscriptionName,proto3\" json:\"subscription_name,omitempty\"`\n\tMessageId        string                 `protobuf:\"bytes,4,opt,name=message_id,json=messageId,proto3\" json:\"message_id,omitempty\"`\n\tAttempt          uint32                 `protobuf:\"varint,5,opt,name=attempt,proto3\" json:\"attempt,omitempty\"`\n\tPublishTime      *timestamppb.Timestamp `protobuf:\"bytes,6,opt,name=publish_time,json=publishTime,proto3\" json:\"publish_time,omitempty\"`\n\tMessagePayload   []byte                 `protobuf:\"bytes,7,opt,name=message_payload,json=messagePayload,proto3,oneof\" json:\"message_payload,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *PubsubMessageSpanStart) Reset() {\n\t*x = PubsubMessageSpanStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubsubMessageSpanStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubsubMessageSpanStart) ProtoMessage() {}\n\nfunc (x *PubsubMessageSpanStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubsubMessageSpanStart.ProtoReflect.Descriptor instead.\nfunc (*PubsubMessageSpanStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *PubsubMessageSpanStart) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanStart) GetTopicName() string {\n\tif x != nil {\n\t\treturn x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanStart) GetSubscriptionName() string {\n\tif x != nil {\n\t\treturn x.SubscriptionName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanStart) GetMessageId() string {\n\tif x != nil {\n\t\treturn x.MessageId\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanStart) GetAttempt() uint32 {\n\tif x != nil {\n\t\treturn x.Attempt\n\t}\n\treturn 0\n}\n\nfunc (x *PubsubMessageSpanStart) GetPublishTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.PublishTime\n\t}\n\treturn nil\n}\n\nfunc (x *PubsubMessageSpanStart) GetMessagePayload() []byte {\n\tif x != nil {\n\t\treturn x.MessagePayload\n\t}\n\treturn nil\n}\n\ntype PubsubMessageSpanEnd struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Repeat service/topic/subscription name here to make it possible\n\t// to consume end events without having to look up the start.\n\tServiceName      string `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tTopicName        string `protobuf:\"bytes,2,opt,name=topic_name,json=topicName,proto3\" json:\"topic_name,omitempty\"`\n\tSubscriptionName string `protobuf:\"bytes,3,opt,name=subscription_name,json=subscriptionName,proto3\" json:\"subscription_name,omitempty\"`\n\tMessageId        string `protobuf:\"bytes,4,opt,name=message_id,json=messageId,proto3\" json:\"message_id,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *PubsubMessageSpanEnd) Reset() {\n\t*x = PubsubMessageSpanEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubsubMessageSpanEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubsubMessageSpanEnd) ProtoMessage() {}\n\nfunc (x *PubsubMessageSpanEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubsubMessageSpanEnd.ProtoReflect.Descriptor instead.\nfunc (*PubsubMessageSpanEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *PubsubMessageSpanEnd) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanEnd) GetTopicName() string {\n\tif x != nil {\n\t\treturn x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanEnd) GetSubscriptionName() string {\n\tif x != nil {\n\t\treturn x.SubscriptionName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubMessageSpanEnd) GetMessageId() string {\n\tif x != nil {\n\t\treturn x.MessageId\n\t}\n\treturn \"\"\n}\n\ntype TestSpanStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tTestName      string                 `protobuf:\"bytes,2,opt,name=test_name,json=testName,proto3\" json:\"test_name,omitempty\"`\n\tUid           string                 `protobuf:\"bytes,3,opt,name=uid,proto3\" json:\"uid,omitempty\"`\n\tTestFile      string                 `protobuf:\"bytes,4,opt,name=test_file,json=testFile,proto3\" json:\"test_file,omitempty\"`\n\tTestLine      uint32                 `protobuf:\"varint,5,opt,name=test_line,json=testLine,proto3\" json:\"test_line,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TestSpanStart) Reset() {\n\t*x = TestSpanStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TestSpanStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestSpanStart) ProtoMessage() {}\n\nfunc (x *TestSpanStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestSpanStart.ProtoReflect.Descriptor instead.\nfunc (*TestSpanStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *TestSpanStart) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpanStart) GetTestName() string {\n\tif x != nil {\n\t\treturn x.TestName\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpanStart) GetUid() string {\n\tif x != nil {\n\t\treturn x.Uid\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpanStart) GetTestFile() string {\n\tif x != nil {\n\t\treturn x.TestFile\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpanStart) GetTestLine() uint32 {\n\tif x != nil {\n\t\treturn x.TestLine\n\t}\n\treturn 0\n}\n\ntype TestSpanEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tTestName      string                 `protobuf:\"bytes,2,opt,name=test_name,json=testName,proto3\" json:\"test_name,omitempty\"`\n\tFailed        bool                   `protobuf:\"varint,3,opt,name=failed,proto3\" json:\"failed,omitempty\"`\n\tSkipped       bool                   `protobuf:\"varint,4,opt,name=skipped,proto3\" json:\"skipped,omitempty\"`\n\tUid           *string                `protobuf:\"bytes,5,opt,name=uid,proto3,oneof\" json:\"uid,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TestSpanEnd) Reset() {\n\t*x = TestSpanEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TestSpanEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TestSpanEnd) ProtoMessage() {}\n\nfunc (x *TestSpanEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TestSpanEnd.ProtoReflect.Descriptor instead.\nfunc (*TestSpanEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *TestSpanEnd) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpanEnd) GetTestName() string {\n\tif x != nil {\n\t\treturn x.TestName\n\t}\n\treturn \"\"\n}\n\nfunc (x *TestSpanEnd) GetFailed() bool {\n\tif x != nil {\n\t\treturn x.Failed\n\t}\n\treturn false\n}\n\nfunc (x *TestSpanEnd) GetSkipped() bool {\n\tif x != nil {\n\t\treturn x.Skipped\n\t}\n\treturn false\n}\n\nfunc (x *TestSpanEnd) GetUid() string {\n\tif x != nil && x.Uid != nil {\n\t\treturn *x.Uid\n\t}\n\treturn \"\"\n}\n\ntype SpanEvent struct {\n\tstate  protoimpl.MessageState `protogen:\"open.v1\"`\n\tGoid   uint32                 `protobuf:\"varint,1,opt,name=goid,proto3\" json:\"goid,omitempty\"`\n\tDefLoc *uint32                `protobuf:\"varint,2,opt,name=def_loc,json=defLoc,proto3,oneof\" json:\"def_loc,omitempty\"`\n\t// correlation_event_id is the other event\n\t// this event is correlated with.\n\tCorrelationEventId *uint64 `protobuf:\"varint,3,opt,name=correlation_event_id,json=correlationEventId,proto3,oneof\" json:\"correlation_event_id,omitempty\"`\n\t// Types that are valid to be assigned to Data:\n\t//\n\t//\t*SpanEvent_LogMessage\n\t//\t*SpanEvent_BodyStream\n\t//\t*SpanEvent_RpcCallStart\n\t//\t*SpanEvent_RpcCallEnd\n\t//\t*SpanEvent_DbTransactionStart\n\t//\t*SpanEvent_DbTransactionEnd\n\t//\t*SpanEvent_DbQueryStart\n\t//\t*SpanEvent_DbQueryEnd\n\t//\t*SpanEvent_HttpCallStart\n\t//\t*SpanEvent_HttpCallEnd\n\t//\t*SpanEvent_PubsubPublishStart\n\t//\t*SpanEvent_PubsubPublishEnd\n\t//\t*SpanEvent_CacheCallStart\n\t//\t*SpanEvent_CacheCallEnd\n\t//\t*SpanEvent_ServiceInitStart\n\t//\t*SpanEvent_ServiceInitEnd\n\t//\t*SpanEvent_BucketObjectUploadStart\n\t//\t*SpanEvent_BucketObjectUploadEnd\n\t//\t*SpanEvent_BucketObjectDownloadStart\n\t//\t*SpanEvent_BucketObjectDownloadEnd\n\t//\t*SpanEvent_BucketObjectGetAttrsStart\n\t//\t*SpanEvent_BucketObjectGetAttrsEnd\n\t//\t*SpanEvent_BucketListObjectsStart\n\t//\t*SpanEvent_BucketListObjectsEnd\n\t//\t*SpanEvent_BucketDeleteObjectsStart\n\t//\t*SpanEvent_BucketDeleteObjectsEnd\n\tData          isSpanEvent_Data `protobuf_oneof:\"data\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SpanEvent) Reset() {\n\t*x = SpanEvent{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SpanEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SpanEvent) ProtoMessage() {}\n\nfunc (x *SpanEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SpanEvent.ProtoReflect.Descriptor instead.\nfunc (*SpanEvent) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *SpanEvent) GetGoid() uint32 {\n\tif x != nil {\n\t\treturn x.Goid\n\t}\n\treturn 0\n}\n\nfunc (x *SpanEvent) GetDefLoc() uint32 {\n\tif x != nil && x.DefLoc != nil {\n\t\treturn *x.DefLoc\n\t}\n\treturn 0\n}\n\nfunc (x *SpanEvent) GetCorrelationEventId() uint64 {\n\tif x != nil && x.CorrelationEventId != nil {\n\t\treturn *x.CorrelationEventId\n\t}\n\treturn 0\n}\n\nfunc (x *SpanEvent) GetData() isSpanEvent_Data {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetLogMessage() *LogMessage {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_LogMessage); ok {\n\t\t\treturn x.LogMessage\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBodyStream() *BodyStream {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BodyStream); ok {\n\t\t\treturn x.BodyStream\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetRpcCallStart() *RPCCallStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_RpcCallStart); ok {\n\t\t\treturn x.RpcCallStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetRpcCallEnd() *RPCCallEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_RpcCallEnd); ok {\n\t\t\treturn x.RpcCallEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetDbTransactionStart() *DBTransactionStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_DbTransactionStart); ok {\n\t\t\treturn x.DbTransactionStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetDbTransactionEnd() *DBTransactionEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_DbTransactionEnd); ok {\n\t\t\treturn x.DbTransactionEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetDbQueryStart() *DBQueryStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_DbQueryStart); ok {\n\t\t\treturn x.DbQueryStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetDbQueryEnd() *DBQueryEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_DbQueryEnd); ok {\n\t\t\treturn x.DbQueryEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetHttpCallStart() *HTTPCallStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_HttpCallStart); ok {\n\t\t\treturn x.HttpCallStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetHttpCallEnd() *HTTPCallEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_HttpCallEnd); ok {\n\t\t\treturn x.HttpCallEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetPubsubPublishStart() *PubsubPublishStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_PubsubPublishStart); ok {\n\t\t\treturn x.PubsubPublishStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetPubsubPublishEnd() *PubsubPublishEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_PubsubPublishEnd); ok {\n\t\t\treturn x.PubsubPublishEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetCacheCallStart() *CacheCallStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_CacheCallStart); ok {\n\t\t\treturn x.CacheCallStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetCacheCallEnd() *CacheCallEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_CacheCallEnd); ok {\n\t\t\treturn x.CacheCallEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetServiceInitStart() *ServiceInitStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_ServiceInitStart); ok {\n\t\t\treturn x.ServiceInitStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetServiceInitEnd() *ServiceInitEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_ServiceInitEnd); ok {\n\t\t\treturn x.ServiceInitEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketObjectUploadStart() *BucketObjectUploadStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketObjectUploadStart); ok {\n\t\t\treturn x.BucketObjectUploadStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketObjectUploadEnd() *BucketObjectUploadEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketObjectUploadEnd); ok {\n\t\t\treturn x.BucketObjectUploadEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketObjectDownloadStart() *BucketObjectDownloadStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketObjectDownloadStart); ok {\n\t\t\treturn x.BucketObjectDownloadStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketObjectDownloadEnd() *BucketObjectDownloadEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketObjectDownloadEnd); ok {\n\t\t\treturn x.BucketObjectDownloadEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketObjectGetAttrsStart() *BucketObjectGetAttrsStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketObjectGetAttrsStart); ok {\n\t\t\treturn x.BucketObjectGetAttrsStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketObjectGetAttrsEnd() *BucketObjectGetAttrsEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketObjectGetAttrsEnd); ok {\n\t\t\treturn x.BucketObjectGetAttrsEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketListObjectsStart() *BucketListObjectsStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketListObjectsStart); ok {\n\t\t\treturn x.BucketListObjectsStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketListObjectsEnd() *BucketListObjectsEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketListObjectsEnd); ok {\n\t\t\treturn x.BucketListObjectsEnd\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketDeleteObjectsStart() *BucketDeleteObjectsStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketDeleteObjectsStart); ok {\n\t\t\treturn x.BucketDeleteObjectsStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SpanEvent) GetBucketDeleteObjectsEnd() *BucketDeleteObjectsEnd {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*SpanEvent_BucketDeleteObjectsEnd); ok {\n\t\t\treturn x.BucketDeleteObjectsEnd\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isSpanEvent_Data interface {\n\tisSpanEvent_Data()\n}\n\ntype SpanEvent_LogMessage struct {\n\tLogMessage *LogMessage `protobuf:\"bytes,10,opt,name=log_message,json=logMessage,proto3,oneof\"`\n}\n\ntype SpanEvent_BodyStream struct {\n\tBodyStream *BodyStream `protobuf:\"bytes,11,opt,name=body_stream,json=bodyStream,proto3,oneof\"`\n}\n\ntype SpanEvent_RpcCallStart struct {\n\tRpcCallStart *RPCCallStart `protobuf:\"bytes,12,opt,name=rpc_call_start,json=rpcCallStart,proto3,oneof\"`\n}\n\ntype SpanEvent_RpcCallEnd struct {\n\tRpcCallEnd *RPCCallEnd `protobuf:\"bytes,13,opt,name=rpc_call_end,json=rpcCallEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_DbTransactionStart struct {\n\tDbTransactionStart *DBTransactionStart `protobuf:\"bytes,14,opt,name=db_transaction_start,json=dbTransactionStart,proto3,oneof\"`\n}\n\ntype SpanEvent_DbTransactionEnd struct {\n\tDbTransactionEnd *DBTransactionEnd `protobuf:\"bytes,15,opt,name=db_transaction_end,json=dbTransactionEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_DbQueryStart struct {\n\tDbQueryStart *DBQueryStart `protobuf:\"bytes,16,opt,name=db_query_start,json=dbQueryStart,proto3,oneof\"`\n}\n\ntype SpanEvent_DbQueryEnd struct {\n\tDbQueryEnd *DBQueryEnd `protobuf:\"bytes,17,opt,name=db_query_end,json=dbQueryEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_HttpCallStart struct {\n\tHttpCallStart *HTTPCallStart `protobuf:\"bytes,18,opt,name=http_call_start,json=httpCallStart,proto3,oneof\"`\n}\n\ntype SpanEvent_HttpCallEnd struct {\n\tHttpCallEnd *HTTPCallEnd `protobuf:\"bytes,19,opt,name=http_call_end,json=httpCallEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_PubsubPublishStart struct {\n\tPubsubPublishStart *PubsubPublishStart `protobuf:\"bytes,20,opt,name=pubsub_publish_start,json=pubsubPublishStart,proto3,oneof\"`\n}\n\ntype SpanEvent_PubsubPublishEnd struct {\n\tPubsubPublishEnd *PubsubPublishEnd `protobuf:\"bytes,21,opt,name=pubsub_publish_end,json=pubsubPublishEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_CacheCallStart struct {\n\tCacheCallStart *CacheCallStart `protobuf:\"bytes,22,opt,name=cache_call_start,json=cacheCallStart,proto3,oneof\"`\n}\n\ntype SpanEvent_CacheCallEnd struct {\n\tCacheCallEnd *CacheCallEnd `protobuf:\"bytes,23,opt,name=cache_call_end,json=cacheCallEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_ServiceInitStart struct {\n\tServiceInitStart *ServiceInitStart `protobuf:\"bytes,24,opt,name=service_init_start,json=serviceInitStart,proto3,oneof\"`\n}\n\ntype SpanEvent_ServiceInitEnd struct {\n\tServiceInitEnd *ServiceInitEnd `protobuf:\"bytes,25,opt,name=service_init_end,json=serviceInitEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketObjectUploadStart struct {\n\tBucketObjectUploadStart *BucketObjectUploadStart `protobuf:\"bytes,26,opt,name=bucket_object_upload_start,json=bucketObjectUploadStart,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketObjectUploadEnd struct {\n\tBucketObjectUploadEnd *BucketObjectUploadEnd `protobuf:\"bytes,27,opt,name=bucket_object_upload_end,json=bucketObjectUploadEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketObjectDownloadStart struct {\n\tBucketObjectDownloadStart *BucketObjectDownloadStart `protobuf:\"bytes,28,opt,name=bucket_object_download_start,json=bucketObjectDownloadStart,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketObjectDownloadEnd struct {\n\tBucketObjectDownloadEnd *BucketObjectDownloadEnd `protobuf:\"bytes,29,opt,name=bucket_object_download_end,json=bucketObjectDownloadEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketObjectGetAttrsStart struct {\n\tBucketObjectGetAttrsStart *BucketObjectGetAttrsStart `protobuf:\"bytes,30,opt,name=bucket_object_get_attrs_start,json=bucketObjectGetAttrsStart,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketObjectGetAttrsEnd struct {\n\tBucketObjectGetAttrsEnd *BucketObjectGetAttrsEnd `protobuf:\"bytes,31,opt,name=bucket_object_get_attrs_end,json=bucketObjectGetAttrsEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketListObjectsStart struct {\n\tBucketListObjectsStart *BucketListObjectsStart `protobuf:\"bytes,32,opt,name=bucket_list_objects_start,json=bucketListObjectsStart,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketListObjectsEnd struct {\n\tBucketListObjectsEnd *BucketListObjectsEnd `protobuf:\"bytes,33,opt,name=bucket_list_objects_end,json=bucketListObjectsEnd,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketDeleteObjectsStart struct {\n\tBucketDeleteObjectsStart *BucketDeleteObjectsStart `protobuf:\"bytes,34,opt,name=bucket_delete_objects_start,json=bucketDeleteObjectsStart,proto3,oneof\"`\n}\n\ntype SpanEvent_BucketDeleteObjectsEnd struct {\n\tBucketDeleteObjectsEnd *BucketDeleteObjectsEnd `protobuf:\"bytes,35,opt,name=bucket_delete_objects_end,json=bucketDeleteObjectsEnd,proto3,oneof\"`\n}\n\nfunc (*SpanEvent_LogMessage) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BodyStream) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_RpcCallStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_RpcCallEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_DbTransactionStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_DbTransactionEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_DbQueryStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_DbQueryEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_HttpCallStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_HttpCallEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_PubsubPublishStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_PubsubPublishEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_CacheCallStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_CacheCallEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_ServiceInitStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_ServiceInitEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketObjectUploadStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketObjectUploadEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketObjectDownloadStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketObjectDownloadEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketObjectGetAttrsStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketObjectGetAttrsEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketListObjectsStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketListObjectsEnd) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketDeleteObjectsStart) isSpanEvent_Data() {}\n\nfunc (*SpanEvent_BucketDeleteObjectsEnd) isSpanEvent_Data() {}\n\ntype RPCCallStart struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tTargetServiceName  string                 `protobuf:\"bytes,1,opt,name=target_service_name,json=targetServiceName,proto3\" json:\"target_service_name,omitempty\"`\n\tTargetEndpointName string                 `protobuf:\"bytes,2,opt,name=target_endpoint_name,json=targetEndpointName,proto3\" json:\"target_endpoint_name,omitempty\"`\n\tStack              *StackTrace            `protobuf:\"bytes,3,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *RPCCallStart) Reset() {\n\t*x = RPCCallStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPCCallStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPCCallStart) ProtoMessage() {}\n\nfunc (x *RPCCallStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPCCallStart.ProtoReflect.Descriptor instead.\nfunc (*RPCCallStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *RPCCallStart) GetTargetServiceName() string {\n\tif x != nil {\n\t\treturn x.TargetServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPCCallStart) GetTargetEndpointName() string {\n\tif x != nil {\n\t\treturn x.TargetEndpointName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPCCallStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype RPCCallEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPCCallEnd) Reset() {\n\t*x = RPCCallEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPCCallEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPCCallEnd) ProtoMessage() {}\n\nfunc (x *RPCCallEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPCCallEnd.ProtoReflect.Descriptor instead.\nfunc (*RPCCallEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *RPCCallEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype GoroutineStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GoroutineStart) Reset() {\n\t*x = GoroutineStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GoroutineStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GoroutineStart) ProtoMessage() {}\n\nfunc (x *GoroutineStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GoroutineStart.ProtoReflect.Descriptor instead.\nfunc (*GoroutineStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{17}\n}\n\ntype GoroutineEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GoroutineEnd) Reset() {\n\t*x = GoroutineEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GoroutineEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GoroutineEnd) ProtoMessage() {}\n\nfunc (x *GoroutineEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GoroutineEnd.ProtoReflect.Descriptor instead.\nfunc (*GoroutineEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{18}\n}\n\ntype DBTransactionStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tStack         *StackTrace            `protobuf:\"bytes,1,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBTransactionStart) Reset() {\n\t*x = DBTransactionStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBTransactionStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBTransactionStart) ProtoMessage() {}\n\nfunc (x *DBTransactionStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBTransactionStart.ProtoReflect.Descriptor instead.\nfunc (*DBTransactionStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *DBTransactionStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype DBTransactionEnd struct {\n\tstate         protoimpl.MessageState          `protogen:\"open.v1\"`\n\tCompletion    DBTransactionEnd_CompletionType `protobuf:\"varint,1,opt,name=completion,proto3,enum=encore.engine.trace2.DBTransactionEnd_CompletionType\" json:\"completion,omitempty\"`\n\tStack         *StackTrace                     `protobuf:\"bytes,2,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tErr           *Error                          `protobuf:\"bytes,3,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBTransactionEnd) Reset() {\n\t*x = DBTransactionEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBTransactionEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBTransactionEnd) ProtoMessage() {}\n\nfunc (x *DBTransactionEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBTransactionEnd.ProtoReflect.Descriptor instead.\nfunc (*DBTransactionEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *DBTransactionEnd) GetCompletion() DBTransactionEnd_CompletionType {\n\tif x != nil {\n\t\treturn x.Completion\n\t}\n\treturn DBTransactionEnd_ROLLBACK\n}\n\nfunc (x *DBTransactionEnd) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\nfunc (x *DBTransactionEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype DBQueryStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tQuery         string                 `protobuf:\"bytes,1,opt,name=query,proto3\" json:\"query,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,2,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBQueryStart) Reset() {\n\t*x = DBQueryStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBQueryStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBQueryStart) ProtoMessage() {}\n\nfunc (x *DBQueryStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBQueryStart.ProtoReflect.Descriptor instead.\nfunc (*DBQueryStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *DBQueryStart) GetQuery() string {\n\tif x != nil {\n\t\treturn x.Query\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBQueryStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype DBQueryEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBQueryEnd) Reset() {\n\t*x = DBQueryEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBQueryEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBQueryEnd) ProtoMessage() {}\n\nfunc (x *DBQueryEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBQueryEnd.ProtoReflect.Descriptor instead.\nfunc (*DBQueryEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *DBQueryEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype PubsubPublishStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTopic         string                 `protobuf:\"bytes,1,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tMessage       []byte                 `protobuf:\"bytes,2,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,3,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubsubPublishStart) Reset() {\n\t*x = PubsubPublishStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubsubPublishStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubsubPublishStart) ProtoMessage() {}\n\nfunc (x *PubsubPublishStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubsubPublishStart.ProtoReflect.Descriptor instead.\nfunc (*PubsubPublishStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *PubsubPublishStart) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubPublishStart) GetMessage() []byte {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn nil\n}\n\nfunc (x *PubsubPublishStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype PubsubPublishEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMessageId     *string                `protobuf:\"bytes,1,opt,name=message_id,json=messageId,proto3,oneof\" json:\"message_id,omitempty\"`\n\tErr           *Error                 `protobuf:\"bytes,2,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubsubPublishEnd) Reset() {\n\t*x = PubsubPublishEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubsubPublishEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubsubPublishEnd) ProtoMessage() {}\n\nfunc (x *PubsubPublishEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubsubPublishEnd.ProtoReflect.Descriptor instead.\nfunc (*PubsubPublishEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{24}\n}\n\nfunc (x *PubsubPublishEnd) GetMessageId() string {\n\tif x != nil && x.MessageId != nil {\n\t\treturn *x.MessageId\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubsubPublishEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype ServiceInitStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tService       string                 `protobuf:\"bytes,1,opt,name=service,proto3\" json:\"service,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceInitStart) Reset() {\n\t*x = ServiceInitStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceInitStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceInitStart) ProtoMessage() {}\n\nfunc (x *ServiceInitStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceInitStart.ProtoReflect.Descriptor instead.\nfunc (*ServiceInitStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *ServiceInitStart) GetService() string {\n\tif x != nil {\n\t\treturn x.Service\n\t}\n\treturn \"\"\n}\n\ntype ServiceInitEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceInitEnd) Reset() {\n\t*x = ServiceInitEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceInitEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceInitEnd) ProtoMessage() {}\n\nfunc (x *ServiceInitEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceInitEnd.ProtoReflect.Descriptor instead.\nfunc (*ServiceInitEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *ServiceInitEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype CacheCallStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tOperation     string                 `protobuf:\"bytes,1,opt,name=operation,proto3\" json:\"operation,omitempty\"`\n\tKeys          []string               `protobuf:\"bytes,2,rep,name=keys,proto3\" json:\"keys,omitempty\"`\n\tWrite         bool                   `protobuf:\"varint,3,opt,name=write,proto3\" json:\"write,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,4,opt,name=stack,proto3\" json:\"stack,omitempty\"` // TODO include more info (like inputs)\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CacheCallStart) Reset() {\n\t*x = CacheCallStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CacheCallStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CacheCallStart) ProtoMessage() {}\n\nfunc (x *CacheCallStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CacheCallStart.ProtoReflect.Descriptor instead.\nfunc (*CacheCallStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *CacheCallStart) GetOperation() string {\n\tif x != nil {\n\t\treturn x.Operation\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheCallStart) GetKeys() []string {\n\tif x != nil {\n\t\treturn x.Keys\n\t}\n\treturn nil\n}\n\nfunc (x *CacheCallStart) GetWrite() bool {\n\tif x != nil {\n\t\treturn x.Write\n\t}\n\treturn false\n}\n\nfunc (x *CacheCallStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype CacheCallEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tResult        CacheCallEnd_Result    `protobuf:\"varint,1,opt,name=result,proto3,enum=encore.engine.trace2.CacheCallEnd_Result\" json:\"result,omitempty\"`\n\tErr           *Error                 `protobuf:\"bytes,2,opt,name=err,proto3,oneof\" json:\"err,omitempty\"` // TODO include more info (like outputs)\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CacheCallEnd) Reset() {\n\t*x = CacheCallEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CacheCallEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CacheCallEnd) ProtoMessage() {}\n\nfunc (x *CacheCallEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CacheCallEnd.ProtoReflect.Descriptor instead.\nfunc (*CacheCallEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{28}\n}\n\nfunc (x *CacheCallEnd) GetResult() CacheCallEnd_Result {\n\tif x != nil {\n\t\treturn x.Result\n\t}\n\treturn CacheCallEnd_UNKNOWN\n}\n\nfunc (x *CacheCallEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype BucketObjectUploadStart struct {\n\tstate         protoimpl.MessageState  `protogen:\"open.v1\"`\n\tBucket        string                  `protobuf:\"bytes,1,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tObject        string                  `protobuf:\"bytes,2,opt,name=object,proto3\" json:\"object,omitempty\"`\n\tAttrs         *BucketObjectAttributes `protobuf:\"bytes,3,opt,name=attrs,proto3\" json:\"attrs,omitempty\"`\n\tStack         *StackTrace             `protobuf:\"bytes,4,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectUploadStart) Reset() {\n\t*x = BucketObjectUploadStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectUploadStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectUploadStart) ProtoMessage() {}\n\nfunc (x *BucketObjectUploadStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectUploadStart.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectUploadStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{29}\n}\n\nfunc (x *BucketObjectUploadStart) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectUploadStart) GetObject() string {\n\tif x != nil {\n\t\treturn x.Object\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectUploadStart) GetAttrs() *BucketObjectAttributes {\n\tif x != nil {\n\t\treturn x.Attrs\n\t}\n\treturn nil\n}\n\nfunc (x *BucketObjectUploadStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype BucketObjectUploadEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tSize          *uint64                `protobuf:\"varint,2,opt,name=size,proto3,oneof\" json:\"size,omitempty\"`\n\tVersion       *string                `protobuf:\"bytes,3,opt,name=version,proto3,oneof\" json:\"version,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectUploadEnd) Reset() {\n\t*x = BucketObjectUploadEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectUploadEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectUploadEnd) ProtoMessage() {}\n\nfunc (x *BucketObjectUploadEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectUploadEnd.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectUploadEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{30}\n}\n\nfunc (x *BucketObjectUploadEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *BucketObjectUploadEnd) GetSize() uint64 {\n\tif x != nil && x.Size != nil {\n\t\treturn *x.Size\n\t}\n\treturn 0\n}\n\nfunc (x *BucketObjectUploadEnd) GetVersion() string {\n\tif x != nil && x.Version != nil {\n\t\treturn *x.Version\n\t}\n\treturn \"\"\n}\n\ntype BucketObjectDownloadStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBucket        string                 `protobuf:\"bytes,1,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tObject        string                 `protobuf:\"bytes,2,opt,name=object,proto3\" json:\"object,omitempty\"`\n\tVersion       *string                `protobuf:\"bytes,3,opt,name=version,proto3,oneof\" json:\"version,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,4,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectDownloadStart) Reset() {\n\t*x = BucketObjectDownloadStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[31]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectDownloadStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectDownloadStart) ProtoMessage() {}\n\nfunc (x *BucketObjectDownloadStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[31]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectDownloadStart.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectDownloadStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{31}\n}\n\nfunc (x *BucketObjectDownloadStart) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectDownloadStart) GetObject() string {\n\tif x != nil {\n\t\treturn x.Object\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectDownloadStart) GetVersion() string {\n\tif x != nil && x.Version != nil {\n\t\treturn *x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectDownloadStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype BucketObjectDownloadEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tSize          *uint64                `protobuf:\"varint,2,opt,name=size,proto3,oneof\" json:\"size,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectDownloadEnd) Reset() {\n\t*x = BucketObjectDownloadEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[32]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectDownloadEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectDownloadEnd) ProtoMessage() {}\n\nfunc (x *BucketObjectDownloadEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[32]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectDownloadEnd.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectDownloadEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{32}\n}\n\nfunc (x *BucketObjectDownloadEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *BucketObjectDownloadEnd) GetSize() uint64 {\n\tif x != nil && x.Size != nil {\n\t\treturn *x.Size\n\t}\n\treturn 0\n}\n\ntype BucketObjectGetAttrsStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBucket        string                 `protobuf:\"bytes,1,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tObject        string                 `protobuf:\"bytes,2,opt,name=object,proto3\" json:\"object,omitempty\"`\n\tVersion       *string                `protobuf:\"bytes,3,opt,name=version,proto3,oneof\" json:\"version,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,4,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectGetAttrsStart) Reset() {\n\t*x = BucketObjectGetAttrsStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[33]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectGetAttrsStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectGetAttrsStart) ProtoMessage() {}\n\nfunc (x *BucketObjectGetAttrsStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[33]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectGetAttrsStart.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectGetAttrsStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{33}\n}\n\nfunc (x *BucketObjectGetAttrsStart) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectGetAttrsStart) GetObject() string {\n\tif x != nil {\n\t\treturn x.Object\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectGetAttrsStart) GetVersion() string {\n\tif x != nil && x.Version != nil {\n\t\treturn *x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectGetAttrsStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype BucketObjectGetAttrsEnd struct {\n\tstate         protoimpl.MessageState  `protogen:\"open.v1\"`\n\tErr           *Error                  `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tAttrs         *BucketObjectAttributes `protobuf:\"bytes,2,opt,name=attrs,proto3,oneof\" json:\"attrs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectGetAttrsEnd) Reset() {\n\t*x = BucketObjectGetAttrsEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[34]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectGetAttrsEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectGetAttrsEnd) ProtoMessage() {}\n\nfunc (x *BucketObjectGetAttrsEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[34]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectGetAttrsEnd.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectGetAttrsEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{34}\n}\n\nfunc (x *BucketObjectGetAttrsEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *BucketObjectGetAttrsEnd) GetAttrs() *BucketObjectAttributes {\n\tif x != nil {\n\t\treturn x.Attrs\n\t}\n\treturn nil\n}\n\ntype BucketListObjectsStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBucket        string                 `protobuf:\"bytes,1,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tPrefix        *string                `protobuf:\"bytes,2,opt,name=prefix,proto3,oneof\" json:\"prefix,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,3,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketListObjectsStart) Reset() {\n\t*x = BucketListObjectsStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[35]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketListObjectsStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketListObjectsStart) ProtoMessage() {}\n\nfunc (x *BucketListObjectsStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[35]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketListObjectsStart.ProtoReflect.Descriptor instead.\nfunc (*BucketListObjectsStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{35}\n}\n\nfunc (x *BucketListObjectsStart) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketListObjectsStart) GetPrefix() string {\n\tif x != nil && x.Prefix != nil {\n\t\treturn *x.Prefix\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketListObjectsStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype BucketListObjectsEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tObserved      uint64                 `protobuf:\"varint,2,opt,name=observed,proto3\" json:\"observed,omitempty\"`\n\tHasMore       bool                   `protobuf:\"varint,3,opt,name=has_more,json=hasMore,proto3\" json:\"has_more,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketListObjectsEnd) Reset() {\n\t*x = BucketListObjectsEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[36]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketListObjectsEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketListObjectsEnd) ProtoMessage() {}\n\nfunc (x *BucketListObjectsEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[36]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketListObjectsEnd.ProtoReflect.Descriptor instead.\nfunc (*BucketListObjectsEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{36}\n}\n\nfunc (x *BucketListObjectsEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *BucketListObjectsEnd) GetObserved() uint64 {\n\tif x != nil {\n\t\treturn x.Observed\n\t}\n\treturn 0\n}\n\nfunc (x *BucketListObjectsEnd) GetHasMore() bool {\n\tif x != nil {\n\t\treturn x.HasMore\n\t}\n\treturn false\n}\n\ntype BucketDeleteObjectsStart struct {\n\tstate         protoimpl.MessageState     `protogen:\"open.v1\"`\n\tBucket        string                     `protobuf:\"bytes,1,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\tStack         *StackTrace                `protobuf:\"bytes,2,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tEntries       []*BucketDeleteObjectEntry `protobuf:\"bytes,3,rep,name=entries,proto3\" json:\"entries,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketDeleteObjectsStart) Reset() {\n\t*x = BucketDeleteObjectsStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[37]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketDeleteObjectsStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketDeleteObjectsStart) ProtoMessage() {}\n\nfunc (x *BucketDeleteObjectsStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[37]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketDeleteObjectsStart.ProtoReflect.Descriptor instead.\nfunc (*BucketDeleteObjectsStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{37}\n}\n\nfunc (x *BucketDeleteObjectsStart) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketDeleteObjectsStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\nfunc (x *BucketDeleteObjectsStart) GetEntries() []*BucketDeleteObjectEntry {\n\tif x != nil {\n\t\treturn x.Entries\n\t}\n\treturn nil\n}\n\ntype BucketDeleteObjectEntry struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tObject        string                 `protobuf:\"bytes,1,opt,name=object,proto3\" json:\"object,omitempty\"`\n\tVersion       *string                `protobuf:\"bytes,2,opt,name=version,proto3,oneof\" json:\"version,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketDeleteObjectEntry) Reset() {\n\t*x = BucketDeleteObjectEntry{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[38]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketDeleteObjectEntry) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketDeleteObjectEntry) ProtoMessage() {}\n\nfunc (x *BucketDeleteObjectEntry) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[38]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketDeleteObjectEntry.ProtoReflect.Descriptor instead.\nfunc (*BucketDeleteObjectEntry) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{38}\n}\n\nfunc (x *BucketDeleteObjectEntry) GetObject() string {\n\tif x != nil {\n\t\treturn x.Object\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketDeleteObjectEntry) GetVersion() string {\n\tif x != nil && x.Version != nil {\n\t\treturn *x.Version\n\t}\n\treturn \"\"\n}\n\ntype BucketDeleteObjectsEnd struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           *Error                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketDeleteObjectsEnd) Reset() {\n\t*x = BucketDeleteObjectsEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[39]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketDeleteObjectsEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketDeleteObjectsEnd) ProtoMessage() {}\n\nfunc (x *BucketDeleteObjectsEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[39]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketDeleteObjectsEnd.ProtoReflect.Descriptor instead.\nfunc (*BucketDeleteObjectsEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{39}\n}\n\nfunc (x *BucketDeleteObjectsEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype BucketObjectAttributes struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSize          *uint64                `protobuf:\"varint,1,opt,name=size,proto3,oneof\" json:\"size,omitempty\"`\n\tVersion       *string                `protobuf:\"bytes,2,opt,name=version,proto3,oneof\" json:\"version,omitempty\"`\n\tEtag          *string                `protobuf:\"bytes,3,opt,name=etag,proto3,oneof\" json:\"etag,omitempty\"`\n\tContentType   *string                `protobuf:\"bytes,4,opt,name=content_type,json=contentType,proto3,oneof\" json:\"content_type,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketObjectAttributes) Reset() {\n\t*x = BucketObjectAttributes{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[40]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketObjectAttributes) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketObjectAttributes) ProtoMessage() {}\n\nfunc (x *BucketObjectAttributes) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[40]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketObjectAttributes.ProtoReflect.Descriptor instead.\nfunc (*BucketObjectAttributes) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{40}\n}\n\nfunc (x *BucketObjectAttributes) GetSize() uint64 {\n\tif x != nil && x.Size != nil {\n\t\treturn *x.Size\n\t}\n\treturn 0\n}\n\nfunc (x *BucketObjectAttributes) GetVersion() string {\n\tif x != nil && x.Version != nil {\n\t\treturn *x.Version\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectAttributes) GetEtag() string {\n\tif x != nil && x.Etag != nil {\n\t\treturn *x.Etag\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketObjectAttributes) GetContentType() string {\n\tif x != nil && x.ContentType != nil {\n\t\treturn *x.ContentType\n\t}\n\treturn \"\"\n}\n\ntype BodyStream struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIsResponse    bool                   `protobuf:\"varint,1,opt,name=is_response,json=isResponse,proto3\" json:\"is_response,omitempty\"`\n\tOverflowed    bool                   `protobuf:\"varint,2,opt,name=overflowed,proto3\" json:\"overflowed,omitempty\"`\n\tData          []byte                 `protobuf:\"bytes,3,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BodyStream) Reset() {\n\t*x = BodyStream{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[41]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BodyStream) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BodyStream) ProtoMessage() {}\n\nfunc (x *BodyStream) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[41]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BodyStream.ProtoReflect.Descriptor instead.\nfunc (*BodyStream) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{41}\n}\n\nfunc (x *BodyStream) GetIsResponse() bool {\n\tif x != nil {\n\t\treturn x.IsResponse\n\t}\n\treturn false\n}\n\nfunc (x *BodyStream) GetOverflowed() bool {\n\tif x != nil {\n\t\treturn x.Overflowed\n\t}\n\treturn false\n}\n\nfunc (x *BodyStream) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype HTTPCallStart struct {\n\tstate                   protoimpl.MessageState `protogen:\"open.v1\"`\n\tCorrelationParentSpanId uint64                 `protobuf:\"varint,1,opt,name=correlation_parent_span_id,json=correlationParentSpanId,proto3\" json:\"correlation_parent_span_id,omitempty\"`\n\tMethod                  string                 `protobuf:\"bytes,2,opt,name=method,proto3\" json:\"method,omitempty\"`\n\tUrl                     string                 `protobuf:\"bytes,3,opt,name=url,proto3\" json:\"url,omitempty\"`\n\tStack                   *StackTrace            `protobuf:\"bytes,4,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\t// start_nanotime is used to compute timings based on the\n\t// nanotime in the HTTP trace events.\n\tStartNanotime int64 `protobuf:\"varint,5,opt,name=start_nanotime,json=startNanotime,proto3\" json:\"start_nanotime,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPCallStart) Reset() {\n\t*x = HTTPCallStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[42]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPCallStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPCallStart) ProtoMessage() {}\n\nfunc (x *HTTPCallStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[42]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPCallStart.ProtoReflect.Descriptor instead.\nfunc (*HTTPCallStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{42}\n}\n\nfunc (x *HTTPCallStart) GetCorrelationParentSpanId() uint64 {\n\tif x != nil {\n\t\treturn x.CorrelationParentSpanId\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCallStart) GetMethod() string {\n\tif x != nil {\n\t\treturn x.Method\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPCallStart) GetUrl() string {\n\tif x != nil {\n\t\treturn x.Url\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPCallStart) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPCallStart) GetStartNanotime() int64 {\n\tif x != nil {\n\t\treturn x.StartNanotime\n\t}\n\treturn 0\n}\n\ntype HTTPCallEnd struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// status_code is set if we got a HTTP response.\n\tStatusCode *uint32 `protobuf:\"varint,1,opt,name=status_code,json=statusCode,proto3,oneof\" json:\"status_code,omitempty\"`\n\t// err is set otherwise.\n\tErr *Error `protobuf:\"bytes,2,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\t// TODO these should be moved to be asynchronous via a separate event.\n\tTraceEvents   []*HTTPTraceEvent `protobuf:\"bytes,3,rep,name=trace_events,json=traceEvents,proto3\" json:\"trace_events,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPCallEnd) Reset() {\n\t*x = HTTPCallEnd{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[43]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPCallEnd) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPCallEnd) ProtoMessage() {}\n\nfunc (x *HTTPCallEnd) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[43]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPCallEnd.ProtoReflect.Descriptor instead.\nfunc (*HTTPCallEnd) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{43}\n}\n\nfunc (x *HTTPCallEnd) GetStatusCode() uint32 {\n\tif x != nil && x.StatusCode != nil {\n\t\treturn *x.StatusCode\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPCallEnd) GetErr() *Error {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPCallEnd) GetTraceEvents() []*HTTPTraceEvent {\n\tif x != nil {\n\t\treturn x.TraceEvents\n\t}\n\treturn nil\n}\n\ntype HTTPTraceEvent struct {\n\tstate    protoimpl.MessageState `protogen:\"open.v1\"`\n\tNanotime int64                  `protobuf:\"varint,1,opt,name=nanotime,proto3\" json:\"nanotime,omitempty\"`\n\t// Types that are valid to be assigned to Data:\n\t//\n\t//\t*HTTPTraceEvent_GetConn\n\t//\t*HTTPTraceEvent_GotConn\n\t//\t*HTTPTraceEvent_GotFirstResponseByte\n\t//\t*HTTPTraceEvent_Got_1XxResponse\n\t//\t*HTTPTraceEvent_DnsStart\n\t//\t*HTTPTraceEvent_DnsDone\n\t//\t*HTTPTraceEvent_ConnectStart\n\t//\t*HTTPTraceEvent_ConnectDone\n\t//\t*HTTPTraceEvent_TlsHandshakeStart\n\t//\t*HTTPTraceEvent_TlsHandshakeDone\n\t//\t*HTTPTraceEvent_WroteHeaders\n\t//\t*HTTPTraceEvent_WroteRequest\n\t//\t*HTTPTraceEvent_Wait_100Continue\n\t//\t*HTTPTraceEvent_ClosedBody\n\tData          isHTTPTraceEvent_Data `protobuf_oneof:\"data\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPTraceEvent) Reset() {\n\t*x = HTTPTraceEvent{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[44]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPTraceEvent) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPTraceEvent) ProtoMessage() {}\n\nfunc (x *HTTPTraceEvent) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[44]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPTraceEvent.ProtoReflect.Descriptor instead.\nfunc (*HTTPTraceEvent) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{44}\n}\n\nfunc (x *HTTPTraceEvent) GetNanotime() int64 {\n\tif x != nil {\n\t\treturn x.Nanotime\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPTraceEvent) GetData() isHTTPTraceEvent_Data {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGetConn() *HTTPGetConn {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_GetConn); ok {\n\t\t\treturn x.GetConn\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGotConn() *HTTPGotConn {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_GotConn); ok {\n\t\t\treturn x.GotConn\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGotFirstResponseByte() *HTTPGotFirstResponseByte {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_GotFirstResponseByte); ok {\n\t\t\treturn x.GotFirstResponseByte\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetGot_1XxResponse() *HTTPGot1XxResponse {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_Got_1XxResponse); ok {\n\t\t\treturn x.Got_1XxResponse\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetDnsStart() *HTTPDNSStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_DnsStart); ok {\n\t\t\treturn x.DnsStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetDnsDone() *HTTPDNSDone {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_DnsDone); ok {\n\t\t\treturn x.DnsDone\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetConnectStart() *HTTPConnectStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_ConnectStart); ok {\n\t\t\treturn x.ConnectStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetConnectDone() *HTTPConnectDone {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_ConnectDone); ok {\n\t\t\treturn x.ConnectDone\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetTlsHandshakeStart() *HTTPTLSHandshakeStart {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_TlsHandshakeStart); ok {\n\t\t\treturn x.TlsHandshakeStart\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetTlsHandshakeDone() *HTTPTLSHandshakeDone {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_TlsHandshakeDone); ok {\n\t\t\treturn x.TlsHandshakeDone\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetWroteHeaders() *HTTPWroteHeaders {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_WroteHeaders); ok {\n\t\t\treturn x.WroteHeaders\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetWroteRequest() *HTTPWroteRequest {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_WroteRequest); ok {\n\t\t\treturn x.WroteRequest\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetWait_100Continue() *HTTPWait100Continue {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_Wait_100Continue); ok {\n\t\t\treturn x.Wait_100Continue\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTraceEvent) GetClosedBody() *HTTPClosedBodyData {\n\tif x != nil {\n\t\tif x, ok := x.Data.(*HTTPTraceEvent_ClosedBody); ok {\n\t\t\treturn x.ClosedBody\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isHTTPTraceEvent_Data interface {\n\tisHTTPTraceEvent_Data()\n}\n\ntype HTTPTraceEvent_GetConn struct {\n\tGetConn *HTTPGetConn `protobuf:\"bytes,2,opt,name=get_conn,json=getConn,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_GotConn struct {\n\tGotConn *HTTPGotConn `protobuf:\"bytes,3,opt,name=got_conn,json=gotConn,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_GotFirstResponseByte struct {\n\tGotFirstResponseByte *HTTPGotFirstResponseByte `protobuf:\"bytes,4,opt,name=got_first_response_byte,json=gotFirstResponseByte,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_Got_1XxResponse struct {\n\tGot_1XxResponse *HTTPGot1XxResponse `protobuf:\"bytes,5,opt,name=got_1xx_response,json=got1xxResponse,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_DnsStart struct {\n\tDnsStart *HTTPDNSStart `protobuf:\"bytes,6,opt,name=dns_start,json=dnsStart,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_DnsDone struct {\n\tDnsDone *HTTPDNSDone `protobuf:\"bytes,7,opt,name=dns_done,json=dnsDone,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_ConnectStart struct {\n\tConnectStart *HTTPConnectStart `protobuf:\"bytes,8,opt,name=connect_start,json=connectStart,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_ConnectDone struct {\n\tConnectDone *HTTPConnectDone `protobuf:\"bytes,9,opt,name=connect_done,json=connectDone,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_TlsHandshakeStart struct {\n\tTlsHandshakeStart *HTTPTLSHandshakeStart `protobuf:\"bytes,10,opt,name=tls_handshake_start,json=tlsHandshakeStart,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_TlsHandshakeDone struct {\n\tTlsHandshakeDone *HTTPTLSHandshakeDone `protobuf:\"bytes,11,opt,name=tls_handshake_done,json=tlsHandshakeDone,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_WroteHeaders struct {\n\tWroteHeaders *HTTPWroteHeaders `protobuf:\"bytes,12,opt,name=wrote_headers,json=wroteHeaders,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_WroteRequest struct {\n\tWroteRequest *HTTPWroteRequest `protobuf:\"bytes,13,opt,name=wrote_request,json=wroteRequest,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_Wait_100Continue struct {\n\tWait_100Continue *HTTPWait100Continue `protobuf:\"bytes,14,opt,name=wait_100_continue,json=wait100Continue,proto3,oneof\"`\n}\n\ntype HTTPTraceEvent_ClosedBody struct {\n\tClosedBody *HTTPClosedBodyData `protobuf:\"bytes,15,opt,name=closed_body,json=closedBody,proto3,oneof\"`\n}\n\nfunc (*HTTPTraceEvent_GetConn) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_GotConn) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_GotFirstResponseByte) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_Got_1XxResponse) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_DnsStart) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_DnsDone) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_ConnectStart) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_ConnectDone) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_TlsHandshakeStart) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_TlsHandshakeDone) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_WroteHeaders) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_WroteRequest) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_Wait_100Continue) isHTTPTraceEvent_Data() {}\n\nfunc (*HTTPTraceEvent_ClosedBody) isHTTPTraceEvent_Data() {}\n\ntype HTTPGetConn struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHostPort      string                 `protobuf:\"bytes,1,opt,name=host_port,json=hostPort,proto3\" json:\"host_port,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPGetConn) Reset() {\n\t*x = HTTPGetConn{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[45]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGetConn) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGetConn) ProtoMessage() {}\n\nfunc (x *HTTPGetConn) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[45]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGetConn.ProtoReflect.Descriptor instead.\nfunc (*HTTPGetConn) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{45}\n}\n\nfunc (x *HTTPGetConn) GetHostPort() string {\n\tif x != nil {\n\t\treturn x.HostPort\n\t}\n\treturn \"\"\n}\n\ntype HTTPGotConn struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tReused         bool                   `protobuf:\"varint,1,opt,name=reused,proto3\" json:\"reused,omitempty\"`\n\tWasIdle        bool                   `protobuf:\"varint,2,opt,name=was_idle,json=wasIdle,proto3\" json:\"was_idle,omitempty\"`\n\tIdleDurationNs int64                  `protobuf:\"varint,3,opt,name=idle_duration_ns,json=idleDurationNs,proto3\" json:\"idle_duration_ns,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *HTTPGotConn) Reset() {\n\t*x = HTTPGotConn{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[46]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGotConn) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGotConn) ProtoMessage() {}\n\nfunc (x *HTTPGotConn) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[46]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGotConn.ProtoReflect.Descriptor instead.\nfunc (*HTTPGotConn) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{46}\n}\n\nfunc (x *HTTPGotConn) GetReused() bool {\n\tif x != nil {\n\t\treturn x.Reused\n\t}\n\treturn false\n}\n\nfunc (x *HTTPGotConn) GetWasIdle() bool {\n\tif x != nil {\n\t\treturn x.WasIdle\n\t}\n\treturn false\n}\n\nfunc (x *HTTPGotConn) GetIdleDurationNs() int64 {\n\tif x != nil {\n\t\treturn x.IdleDurationNs\n\t}\n\treturn 0\n}\n\ntype HTTPGotFirstResponseByte struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPGotFirstResponseByte) Reset() {\n\t*x = HTTPGotFirstResponseByte{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[47]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGotFirstResponseByte) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGotFirstResponseByte) ProtoMessage() {}\n\nfunc (x *HTTPGotFirstResponseByte) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[47]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGotFirstResponseByte.ProtoReflect.Descriptor instead.\nfunc (*HTTPGotFirstResponseByte) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{47}\n}\n\ntype HTTPGot1XxResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tCode          int32                  `protobuf:\"varint,1,opt,name=code,proto3\" json:\"code,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPGot1XxResponse) Reset() {\n\t*x = HTTPGot1XxResponse{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[48]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPGot1XxResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPGot1XxResponse) ProtoMessage() {}\n\nfunc (x *HTTPGot1XxResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[48]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPGot1XxResponse.ProtoReflect.Descriptor instead.\nfunc (*HTTPGot1XxResponse) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{48}\n}\n\nfunc (x *HTTPGot1XxResponse) GetCode() int32 {\n\tif x != nil {\n\t\treturn x.Code\n\t}\n\treturn 0\n}\n\ntype HTTPDNSStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHost          string                 `protobuf:\"bytes,1,opt,name=host,proto3\" json:\"host,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPDNSStart) Reset() {\n\t*x = HTTPDNSStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[49]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPDNSStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPDNSStart) ProtoMessage() {}\n\nfunc (x *HTTPDNSStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[49]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPDNSStart.ProtoReflect.Descriptor instead.\nfunc (*HTTPDNSStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{49}\n}\n\nfunc (x *HTTPDNSStart) GetHost() string {\n\tif x != nil {\n\t\treturn x.Host\n\t}\n\treturn \"\"\n}\n\ntype HTTPDNSDone struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           []byte                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tAddrs         []*DNSAddr             `protobuf:\"bytes,2,rep,name=addrs,proto3\" json:\"addrs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPDNSDone) Reset() {\n\t*x = HTTPDNSDone{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[50]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPDNSDone) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPDNSDone) ProtoMessage() {}\n\nfunc (x *HTTPDNSDone) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[50]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPDNSDone.ProtoReflect.Descriptor instead.\nfunc (*HTTPDNSDone) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{50}\n}\n\nfunc (x *HTTPDNSDone) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPDNSDone) GetAddrs() []*DNSAddr {\n\tif x != nil {\n\t\treturn x.Addrs\n\t}\n\treturn nil\n}\n\ntype DNSAddr struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tIp            []byte                 `protobuf:\"bytes,1,opt,name=ip,proto3\" json:\"ip,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DNSAddr) Reset() {\n\t*x = DNSAddr{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[51]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DNSAddr) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DNSAddr) ProtoMessage() {}\n\nfunc (x *DNSAddr) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[51]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DNSAddr.ProtoReflect.Descriptor instead.\nfunc (*DNSAddr) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{51}\n}\n\nfunc (x *DNSAddr) GetIp() []byte {\n\tif x != nil {\n\t\treturn x.Ip\n\t}\n\treturn nil\n}\n\ntype HTTPConnectStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNetwork       string                 `protobuf:\"bytes,1,opt,name=network,proto3\" json:\"network,omitempty\"`\n\tAddr          string                 `protobuf:\"bytes,2,opt,name=addr,proto3\" json:\"addr,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPConnectStart) Reset() {\n\t*x = HTTPConnectStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[52]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPConnectStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPConnectStart) ProtoMessage() {}\n\nfunc (x *HTTPConnectStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[52]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPConnectStart.ProtoReflect.Descriptor instead.\nfunc (*HTTPConnectStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{52}\n}\n\nfunc (x *HTTPConnectStart) GetNetwork() string {\n\tif x != nil {\n\t\treturn x.Network\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPConnectStart) GetAddr() string {\n\tif x != nil {\n\t\treturn x.Addr\n\t}\n\treturn \"\"\n}\n\ntype HTTPConnectDone struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNetwork       string                 `protobuf:\"bytes,1,opt,name=network,proto3\" json:\"network,omitempty\"`\n\tAddr          string                 `protobuf:\"bytes,2,opt,name=addr,proto3\" json:\"addr,omitempty\"`\n\tErr           []byte                 `protobuf:\"bytes,3,opt,name=err,proto3\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPConnectDone) Reset() {\n\t*x = HTTPConnectDone{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[53]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPConnectDone) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPConnectDone) ProtoMessage() {}\n\nfunc (x *HTTPConnectDone) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[53]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPConnectDone.ProtoReflect.Descriptor instead.\nfunc (*HTTPConnectDone) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{53}\n}\n\nfunc (x *HTTPConnectDone) GetNetwork() string {\n\tif x != nil {\n\t\treturn x.Network\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPConnectDone) GetAddr() string {\n\tif x != nil {\n\t\treturn x.Addr\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPConnectDone) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype HTTPTLSHandshakeStart struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPTLSHandshakeStart) Reset() {\n\t*x = HTTPTLSHandshakeStart{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[54]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPTLSHandshakeStart) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPTLSHandshakeStart) ProtoMessage() {}\n\nfunc (x *HTTPTLSHandshakeStart) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[54]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPTLSHandshakeStart.ProtoReflect.Descriptor instead.\nfunc (*HTTPTLSHandshakeStart) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{54}\n}\n\ntype HTTPTLSHandshakeDone struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr                []byte                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tTlsVersion         uint32                 `protobuf:\"varint,2,opt,name=tls_version,json=tlsVersion,proto3\" json:\"tls_version,omitempty\"`\n\tCipherSuite        uint32                 `protobuf:\"varint,3,opt,name=cipher_suite,json=cipherSuite,proto3\" json:\"cipher_suite,omitempty\"`\n\tServerName         string                 `protobuf:\"bytes,4,opt,name=server_name,json=serverName,proto3\" json:\"server_name,omitempty\"`\n\tNegotiatedProtocol string                 `protobuf:\"bytes,5,opt,name=negotiated_protocol,json=negotiatedProtocol,proto3\" json:\"negotiated_protocol,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *HTTPTLSHandshakeDone) Reset() {\n\t*x = HTTPTLSHandshakeDone{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[55]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPTLSHandshakeDone) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPTLSHandshakeDone) ProtoMessage() {}\n\nfunc (x *HTTPTLSHandshakeDone) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[55]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPTLSHandshakeDone.ProtoReflect.Descriptor instead.\nfunc (*HTTPTLSHandshakeDone) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{55}\n}\n\nfunc (x *HTTPTLSHandshakeDone) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\nfunc (x *HTTPTLSHandshakeDone) GetTlsVersion() uint32 {\n\tif x != nil {\n\t\treturn x.TlsVersion\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPTLSHandshakeDone) GetCipherSuite() uint32 {\n\tif x != nil {\n\t\treturn x.CipherSuite\n\t}\n\treturn 0\n}\n\nfunc (x *HTTPTLSHandshakeDone) GetServerName() string {\n\tif x != nil {\n\t\treturn x.ServerName\n\t}\n\treturn \"\"\n}\n\nfunc (x *HTTPTLSHandshakeDone) GetNegotiatedProtocol() string {\n\tif x != nil {\n\t\treturn x.NegotiatedProtocol\n\t}\n\treturn \"\"\n}\n\ntype HTTPWroteHeaders struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPWroteHeaders) Reset() {\n\t*x = HTTPWroteHeaders{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[56]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPWroteHeaders) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPWroteHeaders) ProtoMessage() {}\n\nfunc (x *HTTPWroteHeaders) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[56]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPWroteHeaders.ProtoReflect.Descriptor instead.\nfunc (*HTTPWroteHeaders) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{56}\n}\n\ntype HTTPWroteRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           []byte                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPWroteRequest) Reset() {\n\t*x = HTTPWroteRequest{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[57]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPWroteRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPWroteRequest) ProtoMessage() {}\n\nfunc (x *HTTPWroteRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[57]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPWroteRequest.ProtoReflect.Descriptor instead.\nfunc (*HTTPWroteRequest) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{57}\n}\n\nfunc (x *HTTPWroteRequest) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype HTTPWait100Continue struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPWait100Continue) Reset() {\n\t*x = HTTPWait100Continue{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[58]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPWait100Continue) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPWait100Continue) ProtoMessage() {}\n\nfunc (x *HTTPWait100Continue) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[58]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPWait100Continue.ProtoReflect.Descriptor instead.\nfunc (*HTTPWait100Continue) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{58}\n}\n\ntype HTTPClosedBodyData struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tErr           []byte                 `protobuf:\"bytes,1,opt,name=err,proto3,oneof\" json:\"err,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HTTPClosedBodyData) Reset() {\n\t*x = HTTPClosedBodyData{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[59]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HTTPClosedBodyData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HTTPClosedBodyData) ProtoMessage() {}\n\nfunc (x *HTTPClosedBodyData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[59]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HTTPClosedBodyData.ProtoReflect.Descriptor instead.\nfunc (*HTTPClosedBodyData) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{59}\n}\n\nfunc (x *HTTPClosedBodyData) GetErr() []byte {\n\tif x != nil {\n\t\treturn x.Err\n\t}\n\treturn nil\n}\n\ntype LogMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tLevel         LogMessage_Level       `protobuf:\"varint,1,opt,name=level,proto3,enum=encore.engine.trace2.LogMessage_Level\" json:\"level,omitempty\"`\n\tMsg           string                 `protobuf:\"bytes,2,opt,name=msg,proto3\" json:\"msg,omitempty\"`\n\tFields        []*LogField            `protobuf:\"bytes,3,rep,name=fields,proto3\" json:\"fields,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,4,opt,name=stack,proto3\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LogMessage) Reset() {\n\t*x = LogMessage{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[60]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LogMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LogMessage) ProtoMessage() {}\n\nfunc (x *LogMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[60]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LogMessage.ProtoReflect.Descriptor instead.\nfunc (*LogMessage) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{60}\n}\n\nfunc (x *LogMessage) GetLevel() LogMessage_Level {\n\tif x != nil {\n\t\treturn x.Level\n\t}\n\treturn LogMessage_DEBUG\n}\n\nfunc (x *LogMessage) GetMsg() string {\n\tif x != nil {\n\t\treturn x.Msg\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogMessage) GetFields() []*LogField {\n\tif x != nil {\n\t\treturn x.Fields\n\t}\n\treturn nil\n}\n\nfunc (x *LogMessage) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\ntype LogField struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey   string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*LogField_Error\n\t//\t*LogField_Str\n\t//\t*LogField_Bool\n\t//\t*LogField_Time\n\t//\t*LogField_Dur\n\t//\t*LogField_Uuid\n\t//\t*LogField_Json\n\t//\t*LogField_Int\n\t//\t*LogField_Uint\n\t//\t*LogField_Float32\n\t//\t*LogField_Float64\n\tValue         isLogField_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LogField) Reset() {\n\t*x = LogField{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[61]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LogField) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LogField) ProtoMessage() {}\n\nfunc (x *LogField) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[61]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LogField.ProtoReflect.Descriptor instead.\nfunc (*LogField) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{61}\n}\n\nfunc (x *LogField) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogField) GetValue() isLogField_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetError() *Error {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Error); ok {\n\t\t\treturn x.Error\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetStr() string {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Str); ok {\n\t\t\treturn x.Str\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *LogField) GetBool() bool {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Bool); ok {\n\t\t\treturn x.Bool\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (x *LogField) GetTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Time); ok {\n\t\t\treturn x.Time\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetDur() int64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Dur); ok {\n\t\t\treturn x.Dur\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetUuid() []byte {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Uuid); ok {\n\t\t\treturn x.Uuid\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetJson() []byte {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Json); ok {\n\t\t\treturn x.Json\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *LogField) GetInt() int64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Int); ok {\n\t\t\treturn x.Int\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetUint() uint64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Uint); ok {\n\t\t\treturn x.Uint\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetFloat32() float32 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Float32); ok {\n\t\t\treturn x.Float32\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *LogField) GetFloat64() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*LogField_Float64); ok {\n\t\t\treturn x.Float64\n\t\t}\n\t}\n\treturn 0\n}\n\ntype isLogField_Value interface {\n\tisLogField_Value()\n}\n\ntype LogField_Error struct {\n\tError *Error `protobuf:\"bytes,2,opt,name=error,proto3,oneof\"`\n}\n\ntype LogField_Str struct {\n\tStr string `protobuf:\"bytes,3,opt,name=str,proto3,oneof\"`\n}\n\ntype LogField_Bool struct {\n\tBool bool `protobuf:\"varint,4,opt,name=bool,proto3,oneof\"`\n}\n\ntype LogField_Time struct {\n\tTime *timestamppb.Timestamp `protobuf:\"bytes,5,opt,name=time,proto3,oneof\"`\n}\n\ntype LogField_Dur struct {\n\tDur int64 `protobuf:\"varint,6,opt,name=dur,proto3,oneof\"`\n}\n\ntype LogField_Uuid struct {\n\tUuid []byte `protobuf:\"bytes,7,opt,name=uuid,proto3,oneof\"`\n}\n\ntype LogField_Json struct {\n\tJson []byte `protobuf:\"bytes,8,opt,name=json,proto3,oneof\"`\n}\n\ntype LogField_Int struct {\n\tInt int64 `protobuf:\"varint,9,opt,name=int,proto3,oneof\"`\n}\n\ntype LogField_Uint struct {\n\tUint uint64 `protobuf:\"varint,10,opt,name=uint,proto3,oneof\"`\n}\n\ntype LogField_Float32 struct {\n\tFloat32 float32 `protobuf:\"fixed32,11,opt,name=float32,proto3,oneof\"`\n}\n\ntype LogField_Float64 struct {\n\tFloat64 float64 `protobuf:\"fixed64,12,opt,name=float64,proto3,oneof\"`\n}\n\nfunc (*LogField_Error) isLogField_Value() {}\n\nfunc (*LogField_Str) isLogField_Value() {}\n\nfunc (*LogField_Bool) isLogField_Value() {}\n\nfunc (*LogField_Time) isLogField_Value() {}\n\nfunc (*LogField_Dur) isLogField_Value() {}\n\nfunc (*LogField_Uuid) isLogField_Value() {}\n\nfunc (*LogField_Json) isLogField_Value() {}\n\nfunc (*LogField_Int) isLogField_Value() {}\n\nfunc (*LogField_Uint) isLogField_Value() {}\n\nfunc (*LogField_Float32) isLogField_Value() {}\n\nfunc (*LogField_Float64) isLogField_Value() {}\n\ntype StackTrace struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPcs           []int64                `protobuf:\"varint,1,rep,packed,name=pcs,proto3\" json:\"pcs,omitempty\"`\n\tFrames        []*StackFrame          `protobuf:\"bytes,2,rep,name=frames,proto3\" json:\"frames,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StackTrace) Reset() {\n\t*x = StackTrace{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[62]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StackTrace) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StackTrace) ProtoMessage() {}\n\nfunc (x *StackTrace) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[62]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StackTrace.ProtoReflect.Descriptor instead.\nfunc (*StackTrace) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{62}\n}\n\nfunc (x *StackTrace) GetPcs() []int64 {\n\tif x != nil {\n\t\treturn x.Pcs\n\t}\n\treturn nil\n}\n\nfunc (x *StackTrace) GetFrames() []*StackFrame {\n\tif x != nil {\n\t\treturn x.Frames\n\t}\n\treturn nil\n}\n\ntype StackFrame struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFilename      string                 `protobuf:\"bytes,1,opt,name=filename,proto3\" json:\"filename,omitempty\"`\n\tFunc          string                 `protobuf:\"bytes,2,opt,name=func,proto3\" json:\"func,omitempty\"`\n\tLine          int32                  `protobuf:\"varint,3,opt,name=line,proto3\" json:\"line,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StackFrame) Reset() {\n\t*x = StackFrame{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[63]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StackFrame) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StackFrame) ProtoMessage() {}\n\nfunc (x *StackFrame) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[63]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StackFrame.ProtoReflect.Descriptor instead.\nfunc (*StackFrame) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{63}\n}\n\nfunc (x *StackFrame) GetFilename() string {\n\tif x != nil {\n\t\treturn x.Filename\n\t}\n\treturn \"\"\n}\n\nfunc (x *StackFrame) GetFunc() string {\n\tif x != nil {\n\t\treturn x.Func\n\t}\n\treturn \"\"\n}\n\nfunc (x *StackFrame) GetLine() int32 {\n\tif x != nil {\n\t\treturn x.Line\n\t}\n\treturn 0\n}\n\ntype Error struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMsg           string                 `protobuf:\"bytes,1,opt,name=msg,proto3\" json:\"msg,omitempty\"`\n\tStack         *StackTrace            `protobuf:\"bytes,2,opt,name=stack,proto3,oneof\" json:\"stack,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Error) Reset() {\n\t*x = Error{}\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[64]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Error) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Error) ProtoMessage() {}\n\nfunc (x *Error) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_engine_trace2_trace2_proto_msgTypes[64]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Error.ProtoReflect.Descriptor instead.\nfunc (*Error) Descriptor() ([]byte, []int) {\n\treturn file_encore_engine_trace2_trace2_proto_rawDescGZIP(), []int{64}\n}\n\nfunc (x *Error) GetMsg() string {\n\tif x != nil {\n\t\treturn x.Msg\n\t}\n\treturn \"\"\n}\n\nfunc (x *Error) GetStack() *StackTrace {\n\tif x != nil {\n\t\treturn x.Stack\n\t}\n\treturn nil\n}\n\nvar File_encore_engine_trace2_trace2_proto protoreflect.FileDescriptor\n\nconst file_encore_engine_trace2_trace2_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"!encore/engine/trace2/trace2.proto\\x12\\x14encore.engine.trace2\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xad\\a\\n\" +\n\t\"\\vSpanSummary\\x12\\x19\\n\" +\n\t\"\\btrace_id\\x18\\x01 \\x01(\\tR\\atraceId\\x12\\x17\\n\" +\n\t\"\\aspan_id\\x18\\x02 \\x01(\\tR\\x06spanId\\x12>\\n\" +\n\t\"\\x04type\\x18\\x03 \\x01(\\x0e2*.encore.engine.trace2.SpanSummary.SpanTypeR\\x04type\\x12\\x17\\n\" +\n\t\"\\ais_root\\x18\\x04 \\x01(\\bR\\x06isRoot\\x12\\x19\\n\" +\n\t\"\\bis_error\\x18\\x05 \\x01(\\bR\\aisError\\x12'\\n\" +\n\t\"\\x0fdeployed_commit\\x18\\x06 \\x01(\\tR\\x0edeployedCommit\\x129\\n\" +\n\t\"\\n\" +\n\t\"started_at\\x18\\a \\x01(\\v2\\x1a.google.protobuf.TimestampR\\tstartedAt\\x12%\\n\" +\n\t\"\\x0eduration_nanos\\x18\\b \\x01(\\x04R\\rdurationNanos\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\t \\x01(\\tR\\vserviceName\\x12(\\n\" +\n\t\"\\rendpoint_name\\x18\\n\" +\n\t\" \\x01(\\tH\\x00R\\fendpointName\\x88\\x01\\x01\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\v \\x01(\\tH\\x01R\\ttopicName\\x88\\x01\\x01\\x120\\n\" +\n\t\"\\x11subscription_name\\x18\\f \\x01(\\tH\\x02R\\x10subscriptionName\\x88\\x01\\x01\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"message_id\\x18\\r \\x01(\\tH\\x03R\\tmessageId\\x88\\x01\\x01\\x12&\\n\" +\n\t\"\\ftest_skipped\\x18\\x0e \\x01(\\bH\\x04R\\vtestSkipped\\x88\\x01\\x01\\x12\\x1e\\n\" +\n\t\"\\bsrc_file\\x18\\x0f \\x01(\\tH\\x05R\\asrcFile\\x88\\x01\\x01\\x12\\x1e\\n\" +\n\t\"\\bsrc_line\\x18\\x10 \\x01(\\rH\\x06R\\asrcLine\\x88\\x01\\x01\\x12)\\n\" +\n\t\"\\x0eparent_span_id\\x18\\x11 \\x01(\\tH\\aR\\fparentSpanId\\x88\\x01\\x01\\x12+\\n\" +\n\t\"\\x0fcaller_event_id\\x18\\x12 \\x01(\\x04H\\bR\\rcallerEventId\\x88\\x01\\x01\\\"L\\n\" +\n\t\"\\bSpanType\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aREQUEST\\x10\\x01\\x12\\b\\n\" +\n\t\"\\x04AUTH\\x10\\x02\\x12\\x12\\n\" +\n\t\"\\x0ePUBSUB_MESSAGE\\x10\\x03\\x12\\b\\n\" +\n\t\"\\x04TEST\\x10\\x04B\\x10\\n\" +\n\t\"\\x0e_endpoint_nameB\\r\\n\" +\n\t\"\\v_topic_nameB\\x14\\n\" +\n\t\"\\x12_subscription_nameB\\r\\n\" +\n\t\"\\v_message_idB\\x0f\\n\" +\n\t\"\\r_test_skippedB\\v\\n\" +\n\t\"\\t_src_fileB\\v\\n\" +\n\t\"\\t_src_lineB\\x11\\n\" +\n\t\"\\x0f_parent_span_idB\\x12\\n\" +\n\t\"\\x10_caller_event_id\\\"/\\n\" +\n\t\"\\aTraceID\\x12\\x12\\n\" +\n\t\"\\x04high\\x18\\x01 \\x01(\\x04R\\x04high\\x12\\x10\\n\" +\n\t\"\\x03low\\x18\\x02 \\x01(\\x04R\\x03low\\\"E\\n\" +\n\t\"\\tEventList\\x128\\n\" +\n\t\"\\x06events\\x18\\x01 \\x03(\\v2 .encore.engine.trace2.TraceEventR\\x06events\\\"\\xfe\\x02\\n\" +\n\t\"\\n\" +\n\t\"TraceEvent\\x128\\n\" +\n\t\"\\btrace_id\\x18\\x01 \\x01(\\v2\\x1d.encore.engine.trace2.TraceIDR\\atraceId\\x12\\x17\\n\" +\n\t\"\\aspan_id\\x18\\x02 \\x01(\\x04R\\x06spanId\\x12\\x19\\n\" +\n\t\"\\bevent_id\\x18\\x03 \\x01(\\x04R\\aeventId\\x129\\n\" +\n\t\"\\n\" +\n\t\"event_time\\x18\\x04 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\teventTime\\x12@\\n\" +\n\t\"\\n\" +\n\t\"span_start\\x18\\n\" +\n\t\" \\x01(\\v2\\x1f.encore.engine.trace2.SpanStartH\\x00R\\tspanStart\\x12:\\n\" +\n\t\"\\bspan_end\\x18\\v \\x01(\\v2\\x1d.encore.engine.trace2.SpanEndH\\x00R\\aspanEnd\\x12@\\n\" +\n\t\"\\n\" +\n\t\"span_event\\x18\\f \\x01(\\v2\\x1f.encore.engine.trace2.SpanEventH\\x00R\\tspanEventB\\a\\n\" +\n\t\"\\x05event\\\"\\x9a\\x05\\n\" +\n\t\"\\tSpanStart\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\rR\\x04goid\\x12J\\n\" +\n\t\"\\x0fparent_trace_id\\x18\\x02 \\x01(\\v2\\x1d.encore.engine.trace2.TraceIDH\\x01R\\rparentTraceId\\x88\\x01\\x01\\x12)\\n\" +\n\t\"\\x0eparent_span_id\\x18\\x03 \\x01(\\x04H\\x02R\\fparentSpanId\\x88\\x01\\x01\\x12+\\n\" +\n\t\"\\x0fcaller_event_id\\x18\\x04 \\x01(\\x04H\\x03R\\rcallerEventId\\x88\\x01\\x01\\x12;\\n\" +\n\t\"\\x17external_correlation_id\\x18\\x05 \\x01(\\tH\\x04R\\x15externalCorrelationId\\x88\\x01\\x01\\x12\\x1c\\n\" +\n\t\"\\adef_loc\\x18\\x06 \\x01(\\rH\\x05R\\x06defLoc\\x88\\x01\\x01\\x12B\\n\" +\n\t\"\\arequest\\x18\\n\" +\n\t\" \\x01(\\v2&.encore.engine.trace2.RequestSpanStartH\\x00R\\arequest\\x129\\n\" +\n\t\"\\x04auth\\x18\\v \\x01(\\v2#.encore.engine.trace2.AuthSpanStartH\\x00R\\x04auth\\x12U\\n\" +\n\t\"\\x0epubsub_message\\x18\\f \\x01(\\v2,.encore.engine.trace2.PubsubMessageSpanStartH\\x00R\\rpubsubMessage\\x129\\n\" +\n\t\"\\x04test\\x18\\r \\x01(\\v2#.encore.engine.trace2.TestSpanStartH\\x00R\\x04testB\\x06\\n\" +\n\t\"\\x04dataB\\x12\\n\" +\n\t\"\\x10_parent_trace_idB\\x11\\n\" +\n\t\"\\x0f_parent_span_idB\\x12\\n\" +\n\t\"\\x10_caller_event_idB\\x1a\\n\" +\n\t\"\\x18_external_correlation_idB\\n\" +\n\t\"\\n\" +\n\t\"\\b_def_loc\\\"\\xbc\\x05\\n\" +\n\t\"\\aSpanEnd\\x12%\\n\" +\n\t\"\\x0eduration_nanos\\x18\\x01 \\x01(\\x04R\\rdurationNanos\\x126\\n\" +\n\t\"\\x05error\\x18\\x02 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x01R\\x05error\\x88\\x01\\x01\\x12F\\n\" +\n\t\"\\vpanic_stack\\x18\\x03 \\x01(\\v2 .encore.engine.trace2.StackTraceH\\x02R\\n\" +\n\t\"panicStack\\x88\\x01\\x01\\x12J\\n\" +\n\t\"\\x0fparent_trace_id\\x18\\x04 \\x01(\\v2\\x1d.encore.engine.trace2.TraceIDH\\x03R\\rparentTraceId\\x88\\x01\\x01\\x12)\\n\" +\n\t\"\\x0eparent_span_id\\x18\\x05 \\x01(\\x04H\\x04R\\fparentSpanId\\x88\\x01\\x01\\x12A\\n\" +\n\t\"\\vstatus_code\\x18\\x06 \\x01(\\x0e2 .encore.engine.trace2.StatusCodeR\\n\" +\n\t\"statusCode\\x12@\\n\" +\n\t\"\\arequest\\x18\\n\" +\n\t\" \\x01(\\v2$.encore.engine.trace2.RequestSpanEndH\\x00R\\arequest\\x127\\n\" +\n\t\"\\x04auth\\x18\\v \\x01(\\v2!.encore.engine.trace2.AuthSpanEndH\\x00R\\x04auth\\x12S\\n\" +\n\t\"\\x0epubsub_message\\x18\\f \\x01(\\v2*.encore.engine.trace2.PubsubMessageSpanEndH\\x00R\\rpubsubMessage\\x127\\n\" +\n\t\"\\x04test\\x18\\r \\x01(\\v2!.encore.engine.trace2.TestSpanEndH\\x00R\\x04testB\\x06\\n\" +\n\t\"\\x04dataB\\b\\n\" +\n\t\"\\x06_errorB\\x0e\\n\" +\n\t\"\\f_panic_stackB\\x12\\n\" +\n\t\"\\x10_parent_trace_idB\\x11\\n\" +\n\t\"\\x0f_parent_span_id\\\"\\x9b\\x04\\n\" +\n\t\"\\x10RequestSpanStart\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12#\\n\" +\n\t\"\\rendpoint_name\\x18\\x02 \\x01(\\tR\\fendpointName\\x12\\x1f\\n\" +\n\t\"\\vhttp_method\\x18\\x03 \\x01(\\tR\\n\" +\n\t\"httpMethod\\x12\\x12\\n\" +\n\t\"\\x04path\\x18\\x04 \\x01(\\tR\\x04path\\x12\\x1f\\n\" +\n\t\"\\vpath_params\\x18\\x05 \\x03(\\tR\\n\" +\n\t\"pathParams\\x12c\\n\" +\n\t\"\\x0frequest_headers\\x18\\x06 \\x03(\\v2:.encore.engine.trace2.RequestSpanStart.RequestHeadersEntryR\\x0erequestHeaders\\x12,\\n\" +\n\t\"\\x0frequest_payload\\x18\\a \\x01(\\fH\\x00R\\x0erequestPayload\\x88\\x01\\x01\\x121\\n\" +\n\t\"\\x12ext_correlation_id\\x18\\b \\x01(\\tH\\x01R\\x10extCorrelationId\\x88\\x01\\x01\\x12\\x15\\n\" +\n\t\"\\x03uid\\x18\\t \\x01(\\tH\\x02R\\x03uid\\x88\\x01\\x01\\x12\\x16\\n\" +\n\t\"\\x06mocked\\x18\\n\" +\n\t\" \\x01(\\bR\\x06mocked\\x1aA\\n\" +\n\t\"\\x13RequestHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01B\\x12\\n\" +\n\t\"\\x10_request_payloadB\\x15\\n\" +\n\t\"\\x13_ext_correlation_idB\\x06\\n\" +\n\t\"\\x04_uid\\\"\\xd1\\x03\\n\" +\n\t\"\\x0eRequestSpanEnd\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12#\\n\" +\n\t\"\\rendpoint_name\\x18\\x02 \\x01(\\tR\\fendpointName\\x12(\\n\" +\n\t\"\\x10http_status_code\\x18\\x03 \\x01(\\rR\\x0ehttpStatusCode\\x12d\\n\" +\n\t\"\\x10response_headers\\x18\\x04 \\x03(\\v29.encore.engine.trace2.RequestSpanEnd.ResponseHeadersEntryR\\x0fresponseHeaders\\x12.\\n\" +\n\t\"\\x10response_payload\\x18\\x05 \\x01(\\fH\\x00R\\x0fresponsePayload\\x88\\x01\\x01\\x12+\\n\" +\n\t\"\\x0fcaller_event_id\\x18\\x06 \\x01(\\x04H\\x01R\\rcallerEventId\\x88\\x01\\x01\\x12\\x15\\n\" +\n\t\"\\x03uid\\x18\\a \\x01(\\tH\\x02R\\x03uid\\x88\\x01\\x01\\x1aB\\n\" +\n\t\"\\x14ResponseHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01B\\x13\\n\" +\n\t\"\\x11_response_payloadB\\x12\\n\" +\n\t\"\\x10_caller_event_idB\\x06\\n\" +\n\t\"\\x04_uid\\\"\\x90\\x01\\n\" +\n\t\"\\rAuthSpanStart\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12#\\n\" +\n\t\"\\rendpoint_name\\x18\\x02 \\x01(\\tR\\fendpointName\\x12&\\n\" +\n\t\"\\fauth_payload\\x18\\x03 \\x01(\\fH\\x00R\\vauthPayload\\x88\\x01\\x01B\\x0f\\n\" +\n\t\"\\r_auth_payload\\\"\\x97\\x01\\n\" +\n\t\"\\vAuthSpanEnd\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12#\\n\" +\n\t\"\\rendpoint_name\\x18\\x02 \\x01(\\tR\\fendpointName\\x12\\x10\\n\" +\n\t\"\\x03uid\\x18\\x03 \\x01(\\tR\\x03uid\\x12 \\n\" +\n\t\"\\tuser_data\\x18\\x04 \\x01(\\fH\\x00R\\buserData\\x88\\x01\\x01B\\f\\n\" +\n\t\"\\n\" +\n\t\"_user_data\\\"\\xc1\\x02\\n\" +\n\t\"\\x16PubsubMessageSpanStart\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\x02 \\x01(\\tR\\ttopicName\\x12+\\n\" +\n\t\"\\x11subscription_name\\x18\\x03 \\x01(\\tR\\x10subscriptionName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"message_id\\x18\\x04 \\x01(\\tR\\tmessageId\\x12\\x18\\n\" +\n\t\"\\aattempt\\x18\\x05 \\x01(\\rR\\aattempt\\x12=\\n\" +\n\t\"\\fpublish_time\\x18\\x06 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\vpublishTime\\x12,\\n\" +\n\t\"\\x0fmessage_payload\\x18\\a \\x01(\\fH\\x00R\\x0emessagePayload\\x88\\x01\\x01B\\x12\\n\" +\n\t\"\\x10_message_payload\\\"\\xa4\\x01\\n\" +\n\t\"\\x14PubsubMessageSpanEnd\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\x02 \\x01(\\tR\\ttopicName\\x12+\\n\" +\n\t\"\\x11subscription_name\\x18\\x03 \\x01(\\tR\\x10subscriptionName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"message_id\\x18\\x04 \\x01(\\tR\\tmessageId\\\"\\x9b\\x01\\n\" +\n\t\"\\rTestSpanStart\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x1b\\n\" +\n\t\"\\ttest_name\\x18\\x02 \\x01(\\tR\\btestName\\x12\\x10\\n\" +\n\t\"\\x03uid\\x18\\x03 \\x01(\\tR\\x03uid\\x12\\x1b\\n\" +\n\t\"\\ttest_file\\x18\\x04 \\x01(\\tR\\btestFile\\x12\\x1b\\n\" +\n\t\"\\ttest_line\\x18\\x05 \\x01(\\rR\\btestLine\\\"\\x9e\\x01\\n\" +\n\t\"\\vTestSpanEnd\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x1b\\n\" +\n\t\"\\ttest_name\\x18\\x02 \\x01(\\tR\\btestName\\x12\\x16\\n\" +\n\t\"\\x06failed\\x18\\x03 \\x01(\\bR\\x06failed\\x12\\x18\\n\" +\n\t\"\\askipped\\x18\\x04 \\x01(\\bR\\askipped\\x12\\x15\\n\" +\n\t\"\\x03uid\\x18\\x05 \\x01(\\tH\\x00R\\x03uid\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_uid\\\"\\xe3\\x13\\n\" +\n\t\"\\tSpanEvent\\x12\\x12\\n\" +\n\t\"\\x04goid\\x18\\x01 \\x01(\\rR\\x04goid\\x12\\x1c\\n\" +\n\t\"\\adef_loc\\x18\\x02 \\x01(\\rH\\x01R\\x06defLoc\\x88\\x01\\x01\\x125\\n\" +\n\t\"\\x14correlation_event_id\\x18\\x03 \\x01(\\x04H\\x02R\\x12correlationEventId\\x88\\x01\\x01\\x12C\\n\" +\n\t\"\\vlog_message\\x18\\n\" +\n\t\" \\x01(\\v2 .encore.engine.trace2.LogMessageH\\x00R\\n\" +\n\t\"logMessage\\x12C\\n\" +\n\t\"\\vbody_stream\\x18\\v \\x01(\\v2 .encore.engine.trace2.BodyStreamH\\x00R\\n\" +\n\t\"bodyStream\\x12J\\n\" +\n\t\"\\x0erpc_call_start\\x18\\f \\x01(\\v2\\\".encore.engine.trace2.RPCCallStartH\\x00R\\frpcCallStart\\x12D\\n\" +\n\t\"\\frpc_call_end\\x18\\r \\x01(\\v2 .encore.engine.trace2.RPCCallEndH\\x00R\\n\" +\n\t\"rpcCallEnd\\x12\\\\\\n\" +\n\t\"\\x14db_transaction_start\\x18\\x0e \\x01(\\v2(.encore.engine.trace2.DBTransactionStartH\\x00R\\x12dbTransactionStart\\x12V\\n\" +\n\t\"\\x12db_transaction_end\\x18\\x0f \\x01(\\v2&.encore.engine.trace2.DBTransactionEndH\\x00R\\x10dbTransactionEnd\\x12J\\n\" +\n\t\"\\x0edb_query_start\\x18\\x10 \\x01(\\v2\\\".encore.engine.trace2.DBQueryStartH\\x00R\\fdbQueryStart\\x12D\\n\" +\n\t\"\\fdb_query_end\\x18\\x11 \\x01(\\v2 .encore.engine.trace2.DBQueryEndH\\x00R\\n\" +\n\t\"dbQueryEnd\\x12M\\n\" +\n\t\"\\x0fhttp_call_start\\x18\\x12 \\x01(\\v2#.encore.engine.trace2.HTTPCallStartH\\x00R\\rhttpCallStart\\x12G\\n\" +\n\t\"\\rhttp_call_end\\x18\\x13 \\x01(\\v2!.encore.engine.trace2.HTTPCallEndH\\x00R\\vhttpCallEnd\\x12\\\\\\n\" +\n\t\"\\x14pubsub_publish_start\\x18\\x14 \\x01(\\v2(.encore.engine.trace2.PubsubPublishStartH\\x00R\\x12pubsubPublishStart\\x12V\\n\" +\n\t\"\\x12pubsub_publish_end\\x18\\x15 \\x01(\\v2&.encore.engine.trace2.PubsubPublishEndH\\x00R\\x10pubsubPublishEnd\\x12P\\n\" +\n\t\"\\x10cache_call_start\\x18\\x16 \\x01(\\v2$.encore.engine.trace2.CacheCallStartH\\x00R\\x0ecacheCallStart\\x12J\\n\" +\n\t\"\\x0ecache_call_end\\x18\\x17 \\x01(\\v2\\\".encore.engine.trace2.CacheCallEndH\\x00R\\fcacheCallEnd\\x12V\\n\" +\n\t\"\\x12service_init_start\\x18\\x18 \\x01(\\v2&.encore.engine.trace2.ServiceInitStartH\\x00R\\x10serviceInitStart\\x12P\\n\" +\n\t\"\\x10service_init_end\\x18\\x19 \\x01(\\v2$.encore.engine.trace2.ServiceInitEndH\\x00R\\x0eserviceInitEnd\\x12l\\n\" +\n\t\"\\x1abucket_object_upload_start\\x18\\x1a \\x01(\\v2-.encore.engine.trace2.BucketObjectUploadStartH\\x00R\\x17bucketObjectUploadStart\\x12f\\n\" +\n\t\"\\x18bucket_object_upload_end\\x18\\x1b \\x01(\\v2+.encore.engine.trace2.BucketObjectUploadEndH\\x00R\\x15bucketObjectUploadEnd\\x12r\\n\" +\n\t\"\\x1cbucket_object_download_start\\x18\\x1c \\x01(\\v2/.encore.engine.trace2.BucketObjectDownloadStartH\\x00R\\x19bucketObjectDownloadStart\\x12l\\n\" +\n\t\"\\x1abucket_object_download_end\\x18\\x1d \\x01(\\v2-.encore.engine.trace2.BucketObjectDownloadEndH\\x00R\\x17bucketObjectDownloadEnd\\x12s\\n\" +\n\t\"\\x1dbucket_object_get_attrs_start\\x18\\x1e \\x01(\\v2/.encore.engine.trace2.BucketObjectGetAttrsStartH\\x00R\\x19bucketObjectGetAttrsStart\\x12m\\n\" +\n\t\"\\x1bbucket_object_get_attrs_end\\x18\\x1f \\x01(\\v2-.encore.engine.trace2.BucketObjectGetAttrsEndH\\x00R\\x17bucketObjectGetAttrsEnd\\x12i\\n\" +\n\t\"\\x19bucket_list_objects_start\\x18  \\x01(\\v2,.encore.engine.trace2.BucketListObjectsStartH\\x00R\\x16bucketListObjectsStart\\x12c\\n\" +\n\t\"\\x17bucket_list_objects_end\\x18! \\x01(\\v2*.encore.engine.trace2.BucketListObjectsEndH\\x00R\\x14bucketListObjectsEnd\\x12o\\n\" +\n\t\"\\x1bbucket_delete_objects_start\\x18\\\" \\x01(\\v2..encore.engine.trace2.BucketDeleteObjectsStartH\\x00R\\x18bucketDeleteObjectsStart\\x12i\\n\" +\n\t\"\\x19bucket_delete_objects_end\\x18# \\x01(\\v2,.encore.engine.trace2.BucketDeleteObjectsEndH\\x00R\\x16bucketDeleteObjectsEndB\\x06\\n\" +\n\t\"\\x04dataB\\n\" +\n\t\"\\n\" +\n\t\"\\b_def_locB\\x17\\n\" +\n\t\"\\x15_correlation_event_id\\\"\\xa8\\x01\\n\" +\n\t\"\\fRPCCallStart\\x12.\\n\" +\n\t\"\\x13target_service_name\\x18\\x01 \\x01(\\tR\\x11targetServiceName\\x120\\n\" +\n\t\"\\x14target_endpoint_name\\x18\\x02 \\x01(\\tR\\x12targetEndpointName\\x126\\n\" +\n\t\"\\x05stack\\x18\\x03 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"H\\n\" +\n\t\"\\n\" +\n\t\"RPCCallEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"\\x10\\n\" +\n\t\"\\x0eGoroutineStart\\\"\\x0e\\n\" +\n\t\"\\fGoroutineEnd\\\"L\\n\" +\n\t\"\\x12DBTransactionStart\\x126\\n\" +\n\t\"\\x05stack\\x18\\x01 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"\\x89\\x02\\n\" +\n\t\"\\x10DBTransactionEnd\\x12U\\n\" +\n\t\"\\n\" +\n\t\"completion\\x18\\x01 \\x01(\\x0e25.encore.engine.trace2.DBTransactionEnd.CompletionTypeR\\n\" +\n\t\"completion\\x126\\n\" +\n\t\"\\x05stack\\x18\\x02 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\x122\\n\" +\n\t\"\\x03err\\x18\\x03 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01\\\"*\\n\" +\n\t\"\\x0eCompletionType\\x12\\f\\n\" +\n\t\"\\bROLLBACK\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06COMMIT\\x10\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"\\\\\\n\" +\n\t\"\\fDBQueryStart\\x12\\x14\\n\" +\n\t\"\\x05query\\x18\\x01 \\x01(\\tR\\x05query\\x126\\n\" +\n\t\"\\x05stack\\x18\\x02 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"H\\n\" +\n\t\"\\n\" +\n\t\"DBQueryEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"|\\n\" +\n\t\"\\x12PubsubPublishStart\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x01 \\x01(\\tR\\x05topic\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\x02 \\x01(\\fR\\amessage\\x126\\n\" +\n\t\"\\x05stack\\x18\\x03 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"\\x81\\x01\\n\" +\n\t\"\\x10PubsubPublishEnd\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"message_id\\x18\\x01 \\x01(\\tH\\x00R\\tmessageId\\x88\\x01\\x01\\x122\\n\" +\n\t\"\\x03err\\x18\\x02 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x01R\\x03err\\x88\\x01\\x01B\\r\\n\" +\n\t\"\\v_message_idB\\x06\\n\" +\n\t\"\\x04_err\\\",\\n\" +\n\t\"\\x10ServiceInitStart\\x12\\x18\\n\" +\n\t\"\\aservice\\x18\\x01 \\x01(\\tR\\aservice\\\"L\\n\" +\n\t\"\\x0eServiceInitEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"\\x90\\x01\\n\" +\n\t\"\\x0eCacheCallStart\\x12\\x1c\\n\" +\n\t\"\\toperation\\x18\\x01 \\x01(\\tR\\toperation\\x12\\x12\\n\" +\n\t\"\\x04keys\\x18\\x02 \\x03(\\tR\\x04keys\\x12\\x14\\n\" +\n\t\"\\x05write\\x18\\x03 \\x01(\\bR\\x05write\\x126\\n\" +\n\t\"\\x05stack\\x18\\x04 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"\\xd4\\x01\\n\" +\n\t\"\\fCacheCallEnd\\x12A\\n\" +\n\t\"\\x06result\\x18\\x01 \\x01(\\x0e2).encore.engine.trace2.CacheCallEnd.ResultR\\x06result\\x122\\n\" +\n\t\"\\x03err\\x18\\x02 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01\\\"E\\n\" +\n\t\"\\x06Result\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\x06\\n\" +\n\t\"\\x02OK\\x10\\x01\\x12\\x0f\\n\" +\n\t\"\\vNO_SUCH_KEY\\x10\\x02\\x12\\f\\n\" +\n\t\"\\bCONFLICT\\x10\\x03\\x12\\a\\n\" +\n\t\"\\x03ERR\\x10\\x04B\\x06\\n\" +\n\t\"\\x04_err\\\"\\xc5\\x01\\n\" +\n\t\"\\x17BucketObjectUploadStart\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x01 \\x01(\\tR\\x06bucket\\x12\\x16\\n\" +\n\t\"\\x06object\\x18\\x02 \\x01(\\tR\\x06object\\x12B\\n\" +\n\t\"\\x05attrs\\x18\\x03 \\x01(\\v2,.encore.engine.trace2.BucketObjectAttributesR\\x05attrs\\x126\\n\" +\n\t\"\\x05stack\\x18\\x04 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"\\xa0\\x01\\n\" +\n\t\"\\x15BucketObjectUploadEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01\\x12\\x17\\n\" +\n\t\"\\x04size\\x18\\x02 \\x01(\\x04H\\x01R\\x04size\\x88\\x01\\x01\\x12\\x1d\\n\" +\n\t\"\\aversion\\x18\\x03 \\x01(\\tH\\x02R\\aversion\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_errB\\a\\n\" +\n\t\"\\x05_sizeB\\n\" +\n\t\"\\n\" +\n\t\"\\b_version\\\"\\xae\\x01\\n\" +\n\t\"\\x19BucketObjectDownloadStart\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x01 \\x01(\\tR\\x06bucket\\x12\\x16\\n\" +\n\t\"\\x06object\\x18\\x02 \\x01(\\tR\\x06object\\x12\\x1d\\n\" +\n\t\"\\aversion\\x18\\x03 \\x01(\\tH\\x00R\\aversion\\x88\\x01\\x01\\x126\\n\" +\n\t\"\\x05stack\\x18\\x04 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stackB\\n\" +\n\t\"\\n\" +\n\t\"\\b_version\\\"w\\n\" +\n\t\"\\x17BucketObjectDownloadEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01\\x12\\x17\\n\" +\n\t\"\\x04size\\x18\\x02 \\x01(\\x04H\\x01R\\x04size\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_errB\\a\\n\" +\n\t\"\\x05_size\\\"\\xae\\x01\\n\" +\n\t\"\\x19BucketObjectGetAttrsStart\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x01 \\x01(\\tR\\x06bucket\\x12\\x16\\n\" +\n\t\"\\x06object\\x18\\x02 \\x01(\\tR\\x06object\\x12\\x1d\\n\" +\n\t\"\\aversion\\x18\\x03 \\x01(\\tH\\x00R\\aversion\\x88\\x01\\x01\\x126\\n\" +\n\t\"\\x05stack\\x18\\x04 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stackB\\n\" +\n\t\"\\n\" +\n\t\"\\b_version\\\"\\xa8\\x01\\n\" +\n\t\"\\x17BucketObjectGetAttrsEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01\\x12G\\n\" +\n\t\"\\x05attrs\\x18\\x02 \\x01(\\v2,.encore.engine.trace2.BucketObjectAttributesH\\x01R\\x05attrs\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_errB\\b\\n\" +\n\t\"\\x06_attrs\\\"\\x90\\x01\\n\" +\n\t\"\\x16BucketListObjectsStart\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x01 \\x01(\\tR\\x06bucket\\x12\\x1b\\n\" +\n\t\"\\x06prefix\\x18\\x02 \\x01(\\tH\\x00R\\x06prefix\\x88\\x01\\x01\\x126\\n\" +\n\t\"\\x05stack\\x18\\x03 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stackB\\t\\n\" +\n\t\"\\a_prefix\\\"\\x89\\x01\\n\" +\n\t\"\\x14BucketListObjectsEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01\\x12\\x1a\\n\" +\n\t\"\\bobserved\\x18\\x02 \\x01(\\x04R\\bobserved\\x12\\x19\\n\" +\n\t\"\\bhas_more\\x18\\x03 \\x01(\\bR\\ahasMoreB\\x06\\n\" +\n\t\"\\x04_err\\\"\\xb3\\x01\\n\" +\n\t\"\\x18BucketDeleteObjectsStart\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x01 \\x01(\\tR\\x06bucket\\x126\\n\" +\n\t\"\\x05stack\\x18\\x02 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\x12G\\n\" +\n\t\"\\aentries\\x18\\x03 \\x03(\\v2-.encore.engine.trace2.BucketDeleteObjectEntryR\\aentries\\\"\\\\\\n\" +\n\t\"\\x17BucketDeleteObjectEntry\\x12\\x16\\n\" +\n\t\"\\x06object\\x18\\x01 \\x01(\\tR\\x06object\\x12\\x1d\\n\" +\n\t\"\\aversion\\x18\\x02 \\x01(\\tH\\x00R\\aversion\\x88\\x01\\x01B\\n\" +\n\t\"\\n\" +\n\t\"\\b_version\\\"T\\n\" +\n\t\"\\x16BucketDeleteObjectsEnd\\x122\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x03err\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"\\xc0\\x01\\n\" +\n\t\"\\x16BucketObjectAttributes\\x12\\x17\\n\" +\n\t\"\\x04size\\x18\\x01 \\x01(\\x04H\\x00R\\x04size\\x88\\x01\\x01\\x12\\x1d\\n\" +\n\t\"\\aversion\\x18\\x02 \\x01(\\tH\\x01R\\aversion\\x88\\x01\\x01\\x12\\x17\\n\" +\n\t\"\\x04etag\\x18\\x03 \\x01(\\tH\\x02R\\x04etag\\x88\\x01\\x01\\x12&\\n\" +\n\t\"\\fcontent_type\\x18\\x04 \\x01(\\tH\\x03R\\vcontentType\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_sizeB\\n\" +\n\t\"\\n\" +\n\t\"\\b_versionB\\a\\n\" +\n\t\"\\x05_etagB\\x0f\\n\" +\n\t\"\\r_content_type\\\"a\\n\" +\n\t\"\\n\" +\n\t\"BodyStream\\x12\\x1f\\n\" +\n\t\"\\vis_response\\x18\\x01 \\x01(\\bR\\n\" +\n\t\"isResponse\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"overflowed\\x18\\x02 \\x01(\\bR\\n\" +\n\t\"overflowed\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x03 \\x01(\\fR\\x04data\\\"\\xd5\\x01\\n\" +\n\t\"\\rHTTPCallStart\\x12;\\n\" +\n\t\"\\x1acorrelation_parent_span_id\\x18\\x01 \\x01(\\x04R\\x17correlationParentSpanId\\x12\\x16\\n\" +\n\t\"\\x06method\\x18\\x02 \\x01(\\tR\\x06method\\x12\\x10\\n\" +\n\t\"\\x03url\\x18\\x03 \\x01(\\tR\\x03url\\x126\\n\" +\n\t\"\\x05stack\\x18\\x04 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\x12%\\n\" +\n\t\"\\x0estart_nanotime\\x18\\x05 \\x01(\\x03R\\rstartNanotime\\\"\\xc8\\x01\\n\" +\n\t\"\\vHTTPCallEnd\\x12$\\n\" +\n\t\"\\vstatus_code\\x18\\x01 \\x01(\\rH\\x00R\\n\" +\n\t\"statusCode\\x88\\x01\\x01\\x122\\n\" +\n\t\"\\x03err\\x18\\x02 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x01R\\x03err\\x88\\x01\\x01\\x12G\\n\" +\n\t\"\\ftrace_events\\x18\\x03 \\x03(\\v2$.encore.engine.trace2.HTTPTraceEventR\\vtraceEventsB\\x0e\\n\" +\n\t\"\\f_status_codeB\\x06\\n\" +\n\t\"\\x04_err\\\"\\x90\\t\\n\" +\n\t\"\\x0eHTTPTraceEvent\\x12\\x1a\\n\" +\n\t\"\\bnanotime\\x18\\x01 \\x01(\\x03R\\bnanotime\\x12>\\n\" +\n\t\"\\bget_conn\\x18\\x02 \\x01(\\v2!.encore.engine.trace2.HTTPGetConnH\\x00R\\agetConn\\x12>\\n\" +\n\t\"\\bgot_conn\\x18\\x03 \\x01(\\v2!.encore.engine.trace2.HTTPGotConnH\\x00R\\agotConn\\x12g\\n\" +\n\t\"\\x17got_first_response_byte\\x18\\x04 \\x01(\\v2..encore.engine.trace2.HTTPGotFirstResponseByteH\\x00R\\x14gotFirstResponseByte\\x12T\\n\" +\n\t\"\\x10got_1xx_response\\x18\\x05 \\x01(\\v2(.encore.engine.trace2.HTTPGot1xxResponseH\\x00R\\x0egot1xxResponse\\x12A\\n\" +\n\t\"\\tdns_start\\x18\\x06 \\x01(\\v2\\\".encore.engine.trace2.HTTPDNSStartH\\x00R\\bdnsStart\\x12>\\n\" +\n\t\"\\bdns_done\\x18\\a \\x01(\\v2!.encore.engine.trace2.HTTPDNSDoneH\\x00R\\adnsDone\\x12M\\n\" +\n\t\"\\rconnect_start\\x18\\b \\x01(\\v2&.encore.engine.trace2.HTTPConnectStartH\\x00R\\fconnectStart\\x12J\\n\" +\n\t\"\\fconnect_done\\x18\\t \\x01(\\v2%.encore.engine.trace2.HTTPConnectDoneH\\x00R\\vconnectDone\\x12]\\n\" +\n\t\"\\x13tls_handshake_start\\x18\\n\" +\n\t\" \\x01(\\v2+.encore.engine.trace2.HTTPTLSHandshakeStartH\\x00R\\x11tlsHandshakeStart\\x12Z\\n\" +\n\t\"\\x12tls_handshake_done\\x18\\v \\x01(\\v2*.encore.engine.trace2.HTTPTLSHandshakeDoneH\\x00R\\x10tlsHandshakeDone\\x12M\\n\" +\n\t\"\\rwrote_headers\\x18\\f \\x01(\\v2&.encore.engine.trace2.HTTPWroteHeadersH\\x00R\\fwroteHeaders\\x12M\\n\" +\n\t\"\\rwrote_request\\x18\\r \\x01(\\v2&.encore.engine.trace2.HTTPWroteRequestH\\x00R\\fwroteRequest\\x12W\\n\" +\n\t\"\\x11wait_100_continue\\x18\\x0e \\x01(\\v2).encore.engine.trace2.HTTPWait100ContinueH\\x00R\\x0fwait100Continue\\x12K\\n\" +\n\t\"\\vclosed_body\\x18\\x0f \\x01(\\v2(.encore.engine.trace2.HTTPClosedBodyDataH\\x00R\\n\" +\n\t\"closedBodyB\\x06\\n\" +\n\t\"\\x04data\\\"*\\n\" +\n\t\"\\vHTTPGetConn\\x12\\x1b\\n\" +\n\t\"\\thost_port\\x18\\x01 \\x01(\\tR\\bhostPort\\\"j\\n\" +\n\t\"\\vHTTPGotConn\\x12\\x16\\n\" +\n\t\"\\x06reused\\x18\\x01 \\x01(\\bR\\x06reused\\x12\\x19\\n\" +\n\t\"\\bwas_idle\\x18\\x02 \\x01(\\bR\\awasIdle\\x12(\\n\" +\n\t\"\\x10idle_duration_ns\\x18\\x03 \\x01(\\x03R\\x0eidleDurationNs\\\"\\x1a\\n\" +\n\t\"\\x18HTTPGotFirstResponseByte\\\"(\\n\" +\n\t\"\\x12HTTPGot1xxResponse\\x12\\x12\\n\" +\n\t\"\\x04code\\x18\\x01 \\x01(\\x05R\\x04code\\\"\\\"\\n\" +\n\t\"\\fHTTPDNSStart\\x12\\x12\\n\" +\n\t\"\\x04host\\x18\\x01 \\x01(\\tR\\x04host\\\"a\\n\" +\n\t\"\\vHTTPDNSDone\\x12\\x15\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fH\\x00R\\x03err\\x88\\x01\\x01\\x123\\n\" +\n\t\"\\x05addrs\\x18\\x02 \\x03(\\v2\\x1d.encore.engine.trace2.DNSAddrR\\x05addrsB\\x06\\n\" +\n\t\"\\x04_err\\\"\\x19\\n\" +\n\t\"\\aDNSAddr\\x12\\x0e\\n\" +\n\t\"\\x02ip\\x18\\x01 \\x01(\\fR\\x02ip\\\"@\\n\" +\n\t\"\\x10HTTPConnectStart\\x12\\x18\\n\" +\n\t\"\\anetwork\\x18\\x01 \\x01(\\tR\\anetwork\\x12\\x12\\n\" +\n\t\"\\x04addr\\x18\\x02 \\x01(\\tR\\x04addr\\\"Q\\n\" +\n\t\"\\x0fHTTPConnectDone\\x12\\x18\\n\" +\n\t\"\\anetwork\\x18\\x01 \\x01(\\tR\\anetwork\\x12\\x12\\n\" +\n\t\"\\x04addr\\x18\\x02 \\x01(\\tR\\x04addr\\x12\\x10\\n\" +\n\t\"\\x03err\\x18\\x03 \\x01(\\fR\\x03err\\\"\\x17\\n\" +\n\t\"\\x15HTTPTLSHandshakeStart\\\"\\xcb\\x01\\n\" +\n\t\"\\x14HTTPTLSHandshakeDone\\x12\\x15\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fH\\x00R\\x03err\\x88\\x01\\x01\\x12\\x1f\\n\" +\n\t\"\\vtls_version\\x18\\x02 \\x01(\\rR\\n\" +\n\t\"tlsVersion\\x12!\\n\" +\n\t\"\\fcipher_suite\\x18\\x03 \\x01(\\rR\\vcipherSuite\\x12\\x1f\\n\" +\n\t\"\\vserver_name\\x18\\x04 \\x01(\\tR\\n\" +\n\t\"serverName\\x12/\\n\" +\n\t\"\\x13negotiated_protocol\\x18\\x05 \\x01(\\tR\\x12negotiatedProtocolB\\x06\\n\" +\n\t\"\\x04_err\\\"\\x12\\n\" +\n\t\"\\x10HTTPWroteHeaders\\\"1\\n\" +\n\t\"\\x10HTTPWroteRequest\\x12\\x15\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fH\\x00R\\x03err\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"\\x15\\n\" +\n\t\"\\x13HTTPWait100Continue\\\"3\\n\" +\n\t\"\\x12HTTPClosedBodyData\\x12\\x15\\n\" +\n\t\"\\x03err\\x18\\x01 \\x01(\\fH\\x00R\\x03err\\x88\\x01\\x01B\\x06\\n\" +\n\t\"\\x04_err\\\"\\x8a\\x02\\n\" +\n\t\"\\n\" +\n\t\"LogMessage\\x12<\\n\" +\n\t\"\\x05level\\x18\\x01 \\x01(\\x0e2&.encore.engine.trace2.LogMessage.LevelR\\x05level\\x12\\x10\\n\" +\n\t\"\\x03msg\\x18\\x02 \\x01(\\tR\\x03msg\\x126\\n\" +\n\t\"\\x06fields\\x18\\x03 \\x03(\\v2\\x1e.encore.engine.trace2.LogFieldR\\x06fields\\x126\\n\" +\n\t\"\\x05stack\\x18\\x04 \\x01(\\v2 .encore.engine.trace2.StackTraceR\\x05stack\\\"<\\n\" +\n\t\"\\x05Level\\x12\\t\\n\" +\n\t\"\\x05DEBUG\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04INFO\\x10\\x01\\x12\\t\\n\" +\n\t\"\\x05ERROR\\x10\\x02\\x12\\b\\n\" +\n\t\"\\x04WARN\\x10\\x03\\x12\\t\\n\" +\n\t\"\\x05TRACE\\x10\\x04\\\"\\xd8\\x02\\n\" +\n\t\"\\bLogField\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x123\\n\" +\n\t\"\\x05error\\x18\\x02 \\x01(\\v2\\x1b.encore.engine.trace2.ErrorH\\x00R\\x05error\\x12\\x12\\n\" +\n\t\"\\x03str\\x18\\x03 \\x01(\\tH\\x00R\\x03str\\x12\\x14\\n\" +\n\t\"\\x04bool\\x18\\x04 \\x01(\\bH\\x00R\\x04bool\\x120\\n\" +\n\t\"\\x04time\\x18\\x05 \\x01(\\v2\\x1a.google.protobuf.TimestampH\\x00R\\x04time\\x12\\x12\\n\" +\n\t\"\\x03dur\\x18\\x06 \\x01(\\x03H\\x00R\\x03dur\\x12\\x14\\n\" +\n\t\"\\x04uuid\\x18\\a \\x01(\\fH\\x00R\\x04uuid\\x12\\x14\\n\" +\n\t\"\\x04json\\x18\\b \\x01(\\fH\\x00R\\x04json\\x12\\x12\\n\" +\n\t\"\\x03int\\x18\\t \\x01(\\x03H\\x00R\\x03int\\x12\\x14\\n\" +\n\t\"\\x04uint\\x18\\n\" +\n\t\" \\x01(\\x04H\\x00R\\x04uint\\x12\\x1a\\n\" +\n\t\"\\afloat32\\x18\\v \\x01(\\x02H\\x00R\\afloat32\\x12\\x1a\\n\" +\n\t\"\\afloat64\\x18\\f \\x01(\\x01H\\x00R\\afloat64B\\a\\n\" +\n\t\"\\x05value\\\"X\\n\" +\n\t\"\\n\" +\n\t\"StackTrace\\x12\\x10\\n\" +\n\t\"\\x03pcs\\x18\\x01 \\x03(\\x03R\\x03pcs\\x128\\n\" +\n\t\"\\x06frames\\x18\\x02 \\x03(\\v2 .encore.engine.trace2.StackFrameR\\x06frames\\\"P\\n\" +\n\t\"\\n\" +\n\t\"StackFrame\\x12\\x1a\\n\" +\n\t\"\\bfilename\\x18\\x01 \\x01(\\tR\\bfilename\\x12\\x12\\n\" +\n\t\"\\x04func\\x18\\x02 \\x01(\\tR\\x04func\\x12\\x12\\n\" +\n\t\"\\x04line\\x18\\x03 \\x01(\\x05R\\x04line\\\"`\\n\" +\n\t\"\\x05Error\\x12\\x10\\n\" +\n\t\"\\x03msg\\x18\\x01 \\x01(\\tR\\x03msg\\x12;\\n\" +\n\t\"\\x05stack\\x18\\x02 \\x01(\\v2 .encore.engine.trace2.StackTraceH\\x00R\\x05stack\\x88\\x01\\x01B\\b\\n\" +\n\t\"\\x06_stack*\\xb1\\x02\\n\" +\n\t\"\\x12HTTPTraceEventCode\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\f\\n\" +\n\t\"\\bGET_CONN\\x10\\x01\\x12\\f\\n\" +\n\t\"\\bGOT_CONN\\x10\\x02\\x12\\x1b\\n\" +\n\t\"\\x17GOT_FIRST_RESPONSE_BYTE\\x10\\x03\\x12\\x14\\n\" +\n\t\"\\x10GOT_1XX_RESPONSE\\x10\\x04\\x12\\r\\n\" +\n\t\"\\tDNS_START\\x10\\x05\\x12\\f\\n\" +\n\t\"\\bDNS_DONE\\x10\\x06\\x12\\x11\\n\" +\n\t\"\\rCONNECT_START\\x10\\a\\x12\\x10\\n\" +\n\t\"\\fCONNECT_DONE\\x10\\b\\x12\\x17\\n\" +\n\t\"\\x13TLS_HANDSHAKE_START\\x10\\t\\x12\\x16\\n\" +\n\t\"\\x12TLS_HANDSHAKE_DONE\\x10\\n\" +\n\t\"\\x12\\x11\\n\" +\n\t\"\\rWROTE_HEADERS\\x10\\v\\x12\\x11\\n\" +\n\t\"\\rWROTE_REQUEST\\x10\\f\\x12\\x15\\n\" +\n\t\"\\x11WAIT_100_CONTINUE\\x10\\r\\x12\\x0f\\n\" +\n\t\"\\vCLOSED_BODY\\x10\\x0e*\\x88\\x04\\n\" +\n\t\"\\n\" +\n\t\"StatusCode\\x12\\x12\\n\" +\n\t\"\\x0eSTATUS_CODE_OK\\x10\\x00\\x12\\x18\\n\" +\n\t\"\\x14STATUS_CODE_CANCELED\\x10\\x01\\x12\\x17\\n\" +\n\t\"\\x13STATUS_CODE_UNKNOWN\\x10\\x02\\x12 \\n\" +\n\t\"\\x1cSTATUS_CODE_INVALID_ARGUMENT\\x10\\x03\\x12!\\n\" +\n\t\"\\x1dSTATUS_CODE_DEADLINE_EXCEEDED\\x10\\x04\\x12\\x19\\n\" +\n\t\"\\x15STATUS_CODE_NOT_FOUND\\x10\\x05\\x12\\x1e\\n\" +\n\t\"\\x1aSTATUS_CODE_ALREADY_EXISTS\\x10\\x06\\x12!\\n\" +\n\t\"\\x1dSTATUS_CODE_PERMISSION_DENIED\\x10\\a\\x12\\\"\\n\" +\n\t\"\\x1eSTATUS_CODE_RESOURCE_EXHAUSTED\\x10\\b\\x12#\\n\" +\n\t\"\\x1fSTATUS_CODE_FAILED_PRECONDITION\\x10\\t\\x12\\x17\\n\" +\n\t\"\\x13STATUS_CODE_ABORTED\\x10\\n\" +\n\t\"\\x12\\x1c\\n\" +\n\t\"\\x18STATUS_CODE_OUT_OF_RANGE\\x10\\v\\x12\\x1d\\n\" +\n\t\"\\x19STATUS_CODE_UNIMPLEMENTED\\x10\\f\\x12\\x18\\n\" +\n\t\"\\x14STATUS_CODE_INTERNAL\\x10\\r\\x12\\x1b\\n\" +\n\t\"\\x17STATUS_CODE_UNAVAILABLE\\x10\\x0e\\x12\\x19\\n\" +\n\t\"\\x15STATUS_CODE_DATA_LOSS\\x10\\x0f\\x12\\x1f\\n\" +\n\t\"\\x1bSTATUS_CODE_UNAUTHENTICATED\\x10\\x10B%Z#encr.dev/proto/encore/engine/trace2b\\x06proto3\"\n\nvar (\n\tfile_encore_engine_trace2_trace2_proto_rawDescOnce sync.Once\n\tfile_encore_engine_trace2_trace2_proto_rawDescData []byte\n)\n\nfunc file_encore_engine_trace2_trace2_proto_rawDescGZIP() []byte {\n\tfile_encore_engine_trace2_trace2_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_engine_trace2_trace2_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_engine_trace2_trace2_proto_rawDesc), len(file_encore_engine_trace2_trace2_proto_rawDesc)))\n\t})\n\treturn file_encore_engine_trace2_trace2_proto_rawDescData\n}\n\nvar file_encore_engine_trace2_trace2_proto_enumTypes = make([]protoimpl.EnumInfo, 6)\nvar file_encore_engine_trace2_trace2_proto_msgTypes = make([]protoimpl.MessageInfo, 67)\nvar file_encore_engine_trace2_trace2_proto_goTypes = []any{\n\t(HTTPTraceEventCode)(0),              // 0: encore.engine.trace2.HTTPTraceEventCode\n\t(StatusCode)(0),                      // 1: encore.engine.trace2.StatusCode\n\t(SpanSummary_SpanType)(0),            // 2: encore.engine.trace2.SpanSummary.SpanType\n\t(DBTransactionEnd_CompletionType)(0), // 3: encore.engine.trace2.DBTransactionEnd.CompletionType\n\t(CacheCallEnd_Result)(0),             // 4: encore.engine.trace2.CacheCallEnd.Result\n\t(LogMessage_Level)(0),                // 5: encore.engine.trace2.LogMessage.Level\n\t(*SpanSummary)(nil),                  // 6: encore.engine.trace2.SpanSummary\n\t(*TraceID)(nil),                      // 7: encore.engine.trace2.TraceID\n\t(*EventList)(nil),                    // 8: encore.engine.trace2.EventList\n\t(*TraceEvent)(nil),                   // 9: encore.engine.trace2.TraceEvent\n\t(*SpanStart)(nil),                    // 10: encore.engine.trace2.SpanStart\n\t(*SpanEnd)(nil),                      // 11: encore.engine.trace2.SpanEnd\n\t(*RequestSpanStart)(nil),             // 12: encore.engine.trace2.RequestSpanStart\n\t(*RequestSpanEnd)(nil),               // 13: encore.engine.trace2.RequestSpanEnd\n\t(*AuthSpanStart)(nil),                // 14: encore.engine.trace2.AuthSpanStart\n\t(*AuthSpanEnd)(nil),                  // 15: encore.engine.trace2.AuthSpanEnd\n\t(*PubsubMessageSpanStart)(nil),       // 16: encore.engine.trace2.PubsubMessageSpanStart\n\t(*PubsubMessageSpanEnd)(nil),         // 17: encore.engine.trace2.PubsubMessageSpanEnd\n\t(*TestSpanStart)(nil),                // 18: encore.engine.trace2.TestSpanStart\n\t(*TestSpanEnd)(nil),                  // 19: encore.engine.trace2.TestSpanEnd\n\t(*SpanEvent)(nil),                    // 20: encore.engine.trace2.SpanEvent\n\t(*RPCCallStart)(nil),                 // 21: encore.engine.trace2.RPCCallStart\n\t(*RPCCallEnd)(nil),                   // 22: encore.engine.trace2.RPCCallEnd\n\t(*GoroutineStart)(nil),               // 23: encore.engine.trace2.GoroutineStart\n\t(*GoroutineEnd)(nil),                 // 24: encore.engine.trace2.GoroutineEnd\n\t(*DBTransactionStart)(nil),           // 25: encore.engine.trace2.DBTransactionStart\n\t(*DBTransactionEnd)(nil),             // 26: encore.engine.trace2.DBTransactionEnd\n\t(*DBQueryStart)(nil),                 // 27: encore.engine.trace2.DBQueryStart\n\t(*DBQueryEnd)(nil),                   // 28: encore.engine.trace2.DBQueryEnd\n\t(*PubsubPublishStart)(nil),           // 29: encore.engine.trace2.PubsubPublishStart\n\t(*PubsubPublishEnd)(nil),             // 30: encore.engine.trace2.PubsubPublishEnd\n\t(*ServiceInitStart)(nil),             // 31: encore.engine.trace2.ServiceInitStart\n\t(*ServiceInitEnd)(nil),               // 32: encore.engine.trace2.ServiceInitEnd\n\t(*CacheCallStart)(nil),               // 33: encore.engine.trace2.CacheCallStart\n\t(*CacheCallEnd)(nil),                 // 34: encore.engine.trace2.CacheCallEnd\n\t(*BucketObjectUploadStart)(nil),      // 35: encore.engine.trace2.BucketObjectUploadStart\n\t(*BucketObjectUploadEnd)(nil),        // 36: encore.engine.trace2.BucketObjectUploadEnd\n\t(*BucketObjectDownloadStart)(nil),    // 37: encore.engine.trace2.BucketObjectDownloadStart\n\t(*BucketObjectDownloadEnd)(nil),      // 38: encore.engine.trace2.BucketObjectDownloadEnd\n\t(*BucketObjectGetAttrsStart)(nil),    // 39: encore.engine.trace2.BucketObjectGetAttrsStart\n\t(*BucketObjectGetAttrsEnd)(nil),      // 40: encore.engine.trace2.BucketObjectGetAttrsEnd\n\t(*BucketListObjectsStart)(nil),       // 41: encore.engine.trace2.BucketListObjectsStart\n\t(*BucketListObjectsEnd)(nil),         // 42: encore.engine.trace2.BucketListObjectsEnd\n\t(*BucketDeleteObjectsStart)(nil),     // 43: encore.engine.trace2.BucketDeleteObjectsStart\n\t(*BucketDeleteObjectEntry)(nil),      // 44: encore.engine.trace2.BucketDeleteObjectEntry\n\t(*BucketDeleteObjectsEnd)(nil),       // 45: encore.engine.trace2.BucketDeleteObjectsEnd\n\t(*BucketObjectAttributes)(nil),       // 46: encore.engine.trace2.BucketObjectAttributes\n\t(*BodyStream)(nil),                   // 47: encore.engine.trace2.BodyStream\n\t(*HTTPCallStart)(nil),                // 48: encore.engine.trace2.HTTPCallStart\n\t(*HTTPCallEnd)(nil),                  // 49: encore.engine.trace2.HTTPCallEnd\n\t(*HTTPTraceEvent)(nil),               // 50: encore.engine.trace2.HTTPTraceEvent\n\t(*HTTPGetConn)(nil),                  // 51: encore.engine.trace2.HTTPGetConn\n\t(*HTTPGotConn)(nil),                  // 52: encore.engine.trace2.HTTPGotConn\n\t(*HTTPGotFirstResponseByte)(nil),     // 53: encore.engine.trace2.HTTPGotFirstResponseByte\n\t(*HTTPGot1XxResponse)(nil),           // 54: encore.engine.trace2.HTTPGot1xxResponse\n\t(*HTTPDNSStart)(nil),                 // 55: encore.engine.trace2.HTTPDNSStart\n\t(*HTTPDNSDone)(nil),                  // 56: encore.engine.trace2.HTTPDNSDone\n\t(*DNSAddr)(nil),                      // 57: encore.engine.trace2.DNSAddr\n\t(*HTTPConnectStart)(nil),             // 58: encore.engine.trace2.HTTPConnectStart\n\t(*HTTPConnectDone)(nil),              // 59: encore.engine.trace2.HTTPConnectDone\n\t(*HTTPTLSHandshakeStart)(nil),        // 60: encore.engine.trace2.HTTPTLSHandshakeStart\n\t(*HTTPTLSHandshakeDone)(nil),         // 61: encore.engine.trace2.HTTPTLSHandshakeDone\n\t(*HTTPWroteHeaders)(nil),             // 62: encore.engine.trace2.HTTPWroteHeaders\n\t(*HTTPWroteRequest)(nil),             // 63: encore.engine.trace2.HTTPWroteRequest\n\t(*HTTPWait100Continue)(nil),          // 64: encore.engine.trace2.HTTPWait100Continue\n\t(*HTTPClosedBodyData)(nil),           // 65: encore.engine.trace2.HTTPClosedBodyData\n\t(*LogMessage)(nil),                   // 66: encore.engine.trace2.LogMessage\n\t(*LogField)(nil),                     // 67: encore.engine.trace2.LogField\n\t(*StackTrace)(nil),                   // 68: encore.engine.trace2.StackTrace\n\t(*StackFrame)(nil),                   // 69: encore.engine.trace2.StackFrame\n\t(*Error)(nil),                        // 70: encore.engine.trace2.Error\n\tnil,                                  // 71: encore.engine.trace2.RequestSpanStart.RequestHeadersEntry\n\tnil,                                  // 72: encore.engine.trace2.RequestSpanEnd.ResponseHeadersEntry\n\t(*timestamppb.Timestamp)(nil),        // 73: google.protobuf.Timestamp\n}\nvar file_encore_engine_trace2_trace2_proto_depIdxs = []int32{\n\t2,   // 0: encore.engine.trace2.SpanSummary.type:type_name -> encore.engine.trace2.SpanSummary.SpanType\n\t73,  // 1: encore.engine.trace2.SpanSummary.started_at:type_name -> google.protobuf.Timestamp\n\t9,   // 2: encore.engine.trace2.EventList.events:type_name -> encore.engine.trace2.TraceEvent\n\t7,   // 3: encore.engine.trace2.TraceEvent.trace_id:type_name -> encore.engine.trace2.TraceID\n\t73,  // 4: encore.engine.trace2.TraceEvent.event_time:type_name -> google.protobuf.Timestamp\n\t10,  // 5: encore.engine.trace2.TraceEvent.span_start:type_name -> encore.engine.trace2.SpanStart\n\t11,  // 6: encore.engine.trace2.TraceEvent.span_end:type_name -> encore.engine.trace2.SpanEnd\n\t20,  // 7: encore.engine.trace2.TraceEvent.span_event:type_name -> encore.engine.trace2.SpanEvent\n\t7,   // 8: encore.engine.trace2.SpanStart.parent_trace_id:type_name -> encore.engine.trace2.TraceID\n\t12,  // 9: encore.engine.trace2.SpanStart.request:type_name -> encore.engine.trace2.RequestSpanStart\n\t14,  // 10: encore.engine.trace2.SpanStart.auth:type_name -> encore.engine.trace2.AuthSpanStart\n\t16,  // 11: encore.engine.trace2.SpanStart.pubsub_message:type_name -> encore.engine.trace2.PubsubMessageSpanStart\n\t18,  // 12: encore.engine.trace2.SpanStart.test:type_name -> encore.engine.trace2.TestSpanStart\n\t70,  // 13: encore.engine.trace2.SpanEnd.error:type_name -> encore.engine.trace2.Error\n\t68,  // 14: encore.engine.trace2.SpanEnd.panic_stack:type_name -> encore.engine.trace2.StackTrace\n\t7,   // 15: encore.engine.trace2.SpanEnd.parent_trace_id:type_name -> encore.engine.trace2.TraceID\n\t1,   // 16: encore.engine.trace2.SpanEnd.status_code:type_name -> encore.engine.trace2.StatusCode\n\t13,  // 17: encore.engine.trace2.SpanEnd.request:type_name -> encore.engine.trace2.RequestSpanEnd\n\t15,  // 18: encore.engine.trace2.SpanEnd.auth:type_name -> encore.engine.trace2.AuthSpanEnd\n\t17,  // 19: encore.engine.trace2.SpanEnd.pubsub_message:type_name -> encore.engine.trace2.PubsubMessageSpanEnd\n\t19,  // 20: encore.engine.trace2.SpanEnd.test:type_name -> encore.engine.trace2.TestSpanEnd\n\t71,  // 21: encore.engine.trace2.RequestSpanStart.request_headers:type_name -> encore.engine.trace2.RequestSpanStart.RequestHeadersEntry\n\t72,  // 22: encore.engine.trace2.RequestSpanEnd.response_headers:type_name -> encore.engine.trace2.RequestSpanEnd.ResponseHeadersEntry\n\t73,  // 23: encore.engine.trace2.PubsubMessageSpanStart.publish_time:type_name -> google.protobuf.Timestamp\n\t66,  // 24: encore.engine.trace2.SpanEvent.log_message:type_name -> encore.engine.trace2.LogMessage\n\t47,  // 25: encore.engine.trace2.SpanEvent.body_stream:type_name -> encore.engine.trace2.BodyStream\n\t21,  // 26: encore.engine.trace2.SpanEvent.rpc_call_start:type_name -> encore.engine.trace2.RPCCallStart\n\t22,  // 27: encore.engine.trace2.SpanEvent.rpc_call_end:type_name -> encore.engine.trace2.RPCCallEnd\n\t25,  // 28: encore.engine.trace2.SpanEvent.db_transaction_start:type_name -> encore.engine.trace2.DBTransactionStart\n\t26,  // 29: encore.engine.trace2.SpanEvent.db_transaction_end:type_name -> encore.engine.trace2.DBTransactionEnd\n\t27,  // 30: encore.engine.trace2.SpanEvent.db_query_start:type_name -> encore.engine.trace2.DBQueryStart\n\t28,  // 31: encore.engine.trace2.SpanEvent.db_query_end:type_name -> encore.engine.trace2.DBQueryEnd\n\t48,  // 32: encore.engine.trace2.SpanEvent.http_call_start:type_name -> encore.engine.trace2.HTTPCallStart\n\t49,  // 33: encore.engine.trace2.SpanEvent.http_call_end:type_name -> encore.engine.trace2.HTTPCallEnd\n\t29,  // 34: encore.engine.trace2.SpanEvent.pubsub_publish_start:type_name -> encore.engine.trace2.PubsubPublishStart\n\t30,  // 35: encore.engine.trace2.SpanEvent.pubsub_publish_end:type_name -> encore.engine.trace2.PubsubPublishEnd\n\t33,  // 36: encore.engine.trace2.SpanEvent.cache_call_start:type_name -> encore.engine.trace2.CacheCallStart\n\t34,  // 37: encore.engine.trace2.SpanEvent.cache_call_end:type_name -> encore.engine.trace2.CacheCallEnd\n\t31,  // 38: encore.engine.trace2.SpanEvent.service_init_start:type_name -> encore.engine.trace2.ServiceInitStart\n\t32,  // 39: encore.engine.trace2.SpanEvent.service_init_end:type_name -> encore.engine.trace2.ServiceInitEnd\n\t35,  // 40: encore.engine.trace2.SpanEvent.bucket_object_upload_start:type_name -> encore.engine.trace2.BucketObjectUploadStart\n\t36,  // 41: encore.engine.trace2.SpanEvent.bucket_object_upload_end:type_name -> encore.engine.trace2.BucketObjectUploadEnd\n\t37,  // 42: encore.engine.trace2.SpanEvent.bucket_object_download_start:type_name -> encore.engine.trace2.BucketObjectDownloadStart\n\t38,  // 43: encore.engine.trace2.SpanEvent.bucket_object_download_end:type_name -> encore.engine.trace2.BucketObjectDownloadEnd\n\t39,  // 44: encore.engine.trace2.SpanEvent.bucket_object_get_attrs_start:type_name -> encore.engine.trace2.BucketObjectGetAttrsStart\n\t40,  // 45: encore.engine.trace2.SpanEvent.bucket_object_get_attrs_end:type_name -> encore.engine.trace2.BucketObjectGetAttrsEnd\n\t41,  // 46: encore.engine.trace2.SpanEvent.bucket_list_objects_start:type_name -> encore.engine.trace2.BucketListObjectsStart\n\t42,  // 47: encore.engine.trace2.SpanEvent.bucket_list_objects_end:type_name -> encore.engine.trace2.BucketListObjectsEnd\n\t43,  // 48: encore.engine.trace2.SpanEvent.bucket_delete_objects_start:type_name -> encore.engine.trace2.BucketDeleteObjectsStart\n\t45,  // 49: encore.engine.trace2.SpanEvent.bucket_delete_objects_end:type_name -> encore.engine.trace2.BucketDeleteObjectsEnd\n\t68,  // 50: encore.engine.trace2.RPCCallStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 51: encore.engine.trace2.RPCCallEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 52: encore.engine.trace2.DBTransactionStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t3,   // 53: encore.engine.trace2.DBTransactionEnd.completion:type_name -> encore.engine.trace2.DBTransactionEnd.CompletionType\n\t68,  // 54: encore.engine.trace2.DBTransactionEnd.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 55: encore.engine.trace2.DBTransactionEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 56: encore.engine.trace2.DBQueryStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 57: encore.engine.trace2.DBQueryEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 58: encore.engine.trace2.PubsubPublishStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 59: encore.engine.trace2.PubsubPublishEnd.err:type_name -> encore.engine.trace2.Error\n\t70,  // 60: encore.engine.trace2.ServiceInitEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 61: encore.engine.trace2.CacheCallStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t4,   // 62: encore.engine.trace2.CacheCallEnd.result:type_name -> encore.engine.trace2.CacheCallEnd.Result\n\t70,  // 63: encore.engine.trace2.CacheCallEnd.err:type_name -> encore.engine.trace2.Error\n\t46,  // 64: encore.engine.trace2.BucketObjectUploadStart.attrs:type_name -> encore.engine.trace2.BucketObjectAttributes\n\t68,  // 65: encore.engine.trace2.BucketObjectUploadStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 66: encore.engine.trace2.BucketObjectUploadEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 67: encore.engine.trace2.BucketObjectDownloadStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 68: encore.engine.trace2.BucketObjectDownloadEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 69: encore.engine.trace2.BucketObjectGetAttrsStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 70: encore.engine.trace2.BucketObjectGetAttrsEnd.err:type_name -> encore.engine.trace2.Error\n\t46,  // 71: encore.engine.trace2.BucketObjectGetAttrsEnd.attrs:type_name -> encore.engine.trace2.BucketObjectAttributes\n\t68,  // 72: encore.engine.trace2.BucketListObjectsStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 73: encore.engine.trace2.BucketListObjectsEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 74: encore.engine.trace2.BucketDeleteObjectsStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t44,  // 75: encore.engine.trace2.BucketDeleteObjectsStart.entries:type_name -> encore.engine.trace2.BucketDeleteObjectEntry\n\t70,  // 76: encore.engine.trace2.BucketDeleteObjectsEnd.err:type_name -> encore.engine.trace2.Error\n\t68,  // 77: encore.engine.trace2.HTTPCallStart.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 78: encore.engine.trace2.HTTPCallEnd.err:type_name -> encore.engine.trace2.Error\n\t50,  // 79: encore.engine.trace2.HTTPCallEnd.trace_events:type_name -> encore.engine.trace2.HTTPTraceEvent\n\t51,  // 80: encore.engine.trace2.HTTPTraceEvent.get_conn:type_name -> encore.engine.trace2.HTTPGetConn\n\t52,  // 81: encore.engine.trace2.HTTPTraceEvent.got_conn:type_name -> encore.engine.trace2.HTTPGotConn\n\t53,  // 82: encore.engine.trace2.HTTPTraceEvent.got_first_response_byte:type_name -> encore.engine.trace2.HTTPGotFirstResponseByte\n\t54,  // 83: encore.engine.trace2.HTTPTraceEvent.got_1xx_response:type_name -> encore.engine.trace2.HTTPGot1xxResponse\n\t55,  // 84: encore.engine.trace2.HTTPTraceEvent.dns_start:type_name -> encore.engine.trace2.HTTPDNSStart\n\t56,  // 85: encore.engine.trace2.HTTPTraceEvent.dns_done:type_name -> encore.engine.trace2.HTTPDNSDone\n\t58,  // 86: encore.engine.trace2.HTTPTraceEvent.connect_start:type_name -> encore.engine.trace2.HTTPConnectStart\n\t59,  // 87: encore.engine.trace2.HTTPTraceEvent.connect_done:type_name -> encore.engine.trace2.HTTPConnectDone\n\t60,  // 88: encore.engine.trace2.HTTPTraceEvent.tls_handshake_start:type_name -> encore.engine.trace2.HTTPTLSHandshakeStart\n\t61,  // 89: encore.engine.trace2.HTTPTraceEvent.tls_handshake_done:type_name -> encore.engine.trace2.HTTPTLSHandshakeDone\n\t62,  // 90: encore.engine.trace2.HTTPTraceEvent.wrote_headers:type_name -> encore.engine.trace2.HTTPWroteHeaders\n\t63,  // 91: encore.engine.trace2.HTTPTraceEvent.wrote_request:type_name -> encore.engine.trace2.HTTPWroteRequest\n\t64,  // 92: encore.engine.trace2.HTTPTraceEvent.wait_100_continue:type_name -> encore.engine.trace2.HTTPWait100Continue\n\t65,  // 93: encore.engine.trace2.HTTPTraceEvent.closed_body:type_name -> encore.engine.trace2.HTTPClosedBodyData\n\t57,  // 94: encore.engine.trace2.HTTPDNSDone.addrs:type_name -> encore.engine.trace2.DNSAddr\n\t5,   // 95: encore.engine.trace2.LogMessage.level:type_name -> encore.engine.trace2.LogMessage.Level\n\t67,  // 96: encore.engine.trace2.LogMessage.fields:type_name -> encore.engine.trace2.LogField\n\t68,  // 97: encore.engine.trace2.LogMessage.stack:type_name -> encore.engine.trace2.StackTrace\n\t70,  // 98: encore.engine.trace2.LogField.error:type_name -> encore.engine.trace2.Error\n\t73,  // 99: encore.engine.trace2.LogField.time:type_name -> google.protobuf.Timestamp\n\t69,  // 100: encore.engine.trace2.StackTrace.frames:type_name -> encore.engine.trace2.StackFrame\n\t68,  // 101: encore.engine.trace2.Error.stack:type_name -> encore.engine.trace2.StackTrace\n\t102, // [102:102] is the sub-list for method output_type\n\t102, // [102:102] is the sub-list for method input_type\n\t102, // [102:102] is the sub-list for extension type_name\n\t102, // [102:102] is the sub-list for extension extendee\n\t0,   // [0:102] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_engine_trace2_trace2_proto_init() }\nfunc file_encore_engine_trace2_trace2_proto_init() {\n\tif File_encore_engine_trace2_trace2_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[0].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[3].OneofWrappers = []any{\n\t\t(*TraceEvent_SpanStart)(nil),\n\t\t(*TraceEvent_SpanEnd)(nil),\n\t\t(*TraceEvent_SpanEvent)(nil),\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[4].OneofWrappers = []any{\n\t\t(*SpanStart_Request)(nil),\n\t\t(*SpanStart_Auth)(nil),\n\t\t(*SpanStart_PubsubMessage)(nil),\n\t\t(*SpanStart_Test)(nil),\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[5].OneofWrappers = []any{\n\t\t(*SpanEnd_Request)(nil),\n\t\t(*SpanEnd_Auth)(nil),\n\t\t(*SpanEnd_PubsubMessage)(nil),\n\t\t(*SpanEnd_Test)(nil),\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[6].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[7].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[8].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[9].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[10].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[13].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[14].OneofWrappers = []any{\n\t\t(*SpanEvent_LogMessage)(nil),\n\t\t(*SpanEvent_BodyStream)(nil),\n\t\t(*SpanEvent_RpcCallStart)(nil),\n\t\t(*SpanEvent_RpcCallEnd)(nil),\n\t\t(*SpanEvent_DbTransactionStart)(nil),\n\t\t(*SpanEvent_DbTransactionEnd)(nil),\n\t\t(*SpanEvent_DbQueryStart)(nil),\n\t\t(*SpanEvent_DbQueryEnd)(nil),\n\t\t(*SpanEvent_HttpCallStart)(nil),\n\t\t(*SpanEvent_HttpCallEnd)(nil),\n\t\t(*SpanEvent_PubsubPublishStart)(nil),\n\t\t(*SpanEvent_PubsubPublishEnd)(nil),\n\t\t(*SpanEvent_CacheCallStart)(nil),\n\t\t(*SpanEvent_CacheCallEnd)(nil),\n\t\t(*SpanEvent_ServiceInitStart)(nil),\n\t\t(*SpanEvent_ServiceInitEnd)(nil),\n\t\t(*SpanEvent_BucketObjectUploadStart)(nil),\n\t\t(*SpanEvent_BucketObjectUploadEnd)(nil),\n\t\t(*SpanEvent_BucketObjectDownloadStart)(nil),\n\t\t(*SpanEvent_BucketObjectDownloadEnd)(nil),\n\t\t(*SpanEvent_BucketObjectGetAttrsStart)(nil),\n\t\t(*SpanEvent_BucketObjectGetAttrsEnd)(nil),\n\t\t(*SpanEvent_BucketListObjectsStart)(nil),\n\t\t(*SpanEvent_BucketListObjectsEnd)(nil),\n\t\t(*SpanEvent_BucketDeleteObjectsStart)(nil),\n\t\t(*SpanEvent_BucketDeleteObjectsEnd)(nil),\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[16].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[20].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[22].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[24].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[26].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[28].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[30].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[31].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[32].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[33].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[34].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[35].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[36].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[38].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[39].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[40].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[43].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[44].OneofWrappers = []any{\n\t\t(*HTTPTraceEvent_GetConn)(nil),\n\t\t(*HTTPTraceEvent_GotConn)(nil),\n\t\t(*HTTPTraceEvent_GotFirstResponseByte)(nil),\n\t\t(*HTTPTraceEvent_Got_1XxResponse)(nil),\n\t\t(*HTTPTraceEvent_DnsStart)(nil),\n\t\t(*HTTPTraceEvent_DnsDone)(nil),\n\t\t(*HTTPTraceEvent_ConnectStart)(nil),\n\t\t(*HTTPTraceEvent_ConnectDone)(nil),\n\t\t(*HTTPTraceEvent_TlsHandshakeStart)(nil),\n\t\t(*HTTPTraceEvent_TlsHandshakeDone)(nil),\n\t\t(*HTTPTraceEvent_WroteHeaders)(nil),\n\t\t(*HTTPTraceEvent_WroteRequest)(nil),\n\t\t(*HTTPTraceEvent_Wait_100Continue)(nil),\n\t\t(*HTTPTraceEvent_ClosedBody)(nil),\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[50].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[55].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[57].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[59].OneofWrappers = []any{}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[61].OneofWrappers = []any{\n\t\t(*LogField_Error)(nil),\n\t\t(*LogField_Str)(nil),\n\t\t(*LogField_Bool)(nil),\n\t\t(*LogField_Time)(nil),\n\t\t(*LogField_Dur)(nil),\n\t\t(*LogField_Uuid)(nil),\n\t\t(*LogField_Json)(nil),\n\t\t(*LogField_Int)(nil),\n\t\t(*LogField_Uint)(nil),\n\t\t(*LogField_Float32)(nil),\n\t\t(*LogField_Float64)(nil),\n\t}\n\tfile_encore_engine_trace2_trace2_proto_msgTypes[64].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_engine_trace2_trace2_proto_rawDesc), len(file_encore_engine_trace2_trace2_proto_rawDesc)),\n\t\t\tNumEnums:      6,\n\t\t\tNumMessages:   67,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_engine_trace2_trace2_proto_goTypes,\n\t\tDependencyIndexes: file_encore_engine_trace2_trace2_proto_depIdxs,\n\t\tEnumInfos:         file_encore_engine_trace2_trace2_proto_enumTypes,\n\t\tMessageInfos:      file_encore_engine_trace2_trace2_proto_msgTypes,\n\t}.Build()\n\tFile_encore_engine_trace2_trace2_proto = out.File\n\tfile_encore_engine_trace2_trace2_proto_goTypes = nil\n\tfile_encore_engine_trace2_trace2_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/engine/trace2/trace2.proto",
    "content": "syntax = \"proto3\";\n\npackage encore.engine.trace2;\n\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"encr.dev/proto/encore/engine/trace2\";\n\n// SpanSummary summarizes a span for display purposes.\nmessage SpanSummary {\n  string trace_id = 1;\n  string span_id = 2;\n  SpanType type = 3;\n  bool is_root = 4; // whether it's a root request\n  bool is_error = 5; // whether the request failed\n  string deployed_commit = 6; // the commit hash of the running service\n  google.protobuf.Timestamp started_at = 7;\n  uint64 duration_nanos = 8;\n  string service_name = 9;\n  optional string endpoint_name = 10;\n  optional string topic_name = 11;\n  optional string subscription_name = 12;\n  optional string message_id = 13;\n  optional bool test_skipped = 14; // whether the test was skipped\n  optional string src_file = 15; // the source file where the span was started (if available)\n  optional uint32 src_line = 16; // the source line where the span was started (if available)\n  optional string parent_span_id = 17; // parent of the span if it's a child, if this is not populated then it's a root\n  optional uint64 caller_event_id = 18; // the event id of the call that started this span\n\n  enum SpanType {\n    UNKNOWN = 0;\n    REQUEST = 1;\n    AUTH = 2;\n    PUBSUB_MESSAGE = 3;\n    TEST = 4;\n  }\n}\n\nmessage TraceID {\n  uint64 high = 1;\n  uint64 low = 2;\n}\n\nmessage EventList {\n  repeated TraceEvent events = 1;\n}\n\nmessage TraceEvent {\n  TraceID trace_id = 1;\n  uint64 span_id = 2;\n  uint64 event_id = 3;\n  google.protobuf.Timestamp event_time = 4;\n\n  oneof event {\n    SpanStart span_start = 10;\n    SpanEnd span_end = 11;\n    SpanEvent span_event = 12;\n  }\n}\n\nmessage SpanStart {\n  uint32 goid = 1;\n  optional TraceID parent_trace_id = 2;\n  optional uint64 parent_span_id = 3;\n  optional uint64 caller_event_id = 4;\n  optional string external_correlation_id = 5;\n  optional uint32 def_loc = 6;\n\n  oneof data {\n    RequestSpanStart request = 10;\n    AuthSpanStart auth = 11;\n    PubsubMessageSpanStart pubsub_message = 12;\n    TestSpanStart test = 13;\n  }\n}\n\nmessage SpanEnd {\n  uint64 duration_nanos = 1;\n  optional Error error = 2;\n  // panic_stack is the stack trace if the span ended due to a panic\n  optional StackTrace panic_stack = 3;\n  optional TraceID parent_trace_id = 4;\n  optional uint64 parent_span_id = 5;\n  StatusCode status_code = 6;\n\n  oneof data {\n    RequestSpanEnd request = 10;\n    AuthSpanEnd auth = 11;\n    PubsubMessageSpanEnd pubsub_message = 12;\n    TestSpanEnd test = 13;\n  }\n}\n\nmessage RequestSpanStart {\n  string service_name = 1;\n  string endpoint_name = 2;\n  string http_method = 3;\n  string path = 4;\n  repeated string path_params = 5;\n  map<string, string> request_headers = 6;\n  optional bytes request_payload = 7;\n  optional string ext_correlation_id = 8;\n  optional string uid = 9;\n  // mocked is true if the request was handled by a mock\n  bool mocked = 10;\n}\n\nmessage RequestSpanEnd {\n  // Repeat service/endpoint name here to make it possible\n  // to consume end events without having to look up the start.\n  string service_name = 1;\n  string endpoint_name = 2;\n\n  uint32 http_status_code = 3;\n  map<string, string> response_headers = 4;\n  optional bytes response_payload = 5;\n\n  optional uint64 caller_event_id = 6;\n  optional string uid = 7;\n}\n\nmessage AuthSpanStart {\n  string service_name = 1;\n  string endpoint_name = 2;\n  optional bytes auth_payload = 3;\n}\n\nmessage AuthSpanEnd {\n  // Repeat service/endpoint name here to make it possible\n  // to consume end events without having to look up the start.\n  string service_name = 1;\n  string endpoint_name = 2;\n\n  string uid = 3;\n  optional bytes user_data = 4;\n}\n\nmessage PubsubMessageSpanStart {\n  string service_name = 1;\n  string topic_name = 2;\n  string subscription_name = 3;\n  string message_id = 4;\n  uint32 attempt = 5;\n  google.protobuf.Timestamp publish_time = 6;\n  optional bytes message_payload = 7;\n}\n\nmessage PubsubMessageSpanEnd {\n  // Repeat service/topic/subscription name here to make it possible\n  // to consume end events without having to look up the start.\n  string service_name = 1;\n  string topic_name = 2;\n  string subscription_name = 3;\n  string message_id = 4;\n}\n\nmessage TestSpanStart {\n  string service_name = 1;\n  string test_name = 2;\n  string uid = 3;\n  string test_file = 4;\n  uint32 test_line = 5;\n}\n\nmessage TestSpanEnd {\n  string service_name = 1;\n  string test_name = 2;\n  bool failed = 3;\n  bool skipped = 4;\n  optional string uid = 5;\n}\n\nmessage SpanEvent {\n  uint32 goid = 1;\n  optional uint32 def_loc = 2;\n\n  // correlation_event_id is the other event\n  // this event is correlated with.\n  optional uint64 correlation_event_id = 3;\n\n  oneof data {\n    LogMessage log_message = 10;\n    BodyStream body_stream = 11;\n    RPCCallStart rpc_call_start = 12;\n    RPCCallEnd rpc_call_end = 13;\n    DBTransactionStart db_transaction_start = 14;\n    DBTransactionEnd db_transaction_end = 15;\n    DBQueryStart db_query_start = 16;\n    DBQueryEnd db_query_end = 17;\n    HTTPCallStart http_call_start = 18;\n    HTTPCallEnd http_call_end = 19;\n    PubsubPublishStart pubsub_publish_start = 20;\n    PubsubPublishEnd pubsub_publish_end = 21;\n    CacheCallStart cache_call_start = 22;\n    CacheCallEnd cache_call_end = 23;\n    ServiceInitStart service_init_start = 24;\n    ServiceInitEnd service_init_end = 25;\n\n    BucketObjectUploadStart bucket_object_upload_start = 26;\n    BucketObjectUploadEnd bucket_object_upload_end = 27;\n    BucketObjectDownloadStart bucket_object_download_start = 28;\n    BucketObjectDownloadEnd bucket_object_download_end = 29;\n    BucketObjectGetAttrsStart bucket_object_get_attrs_start = 30;\n    BucketObjectGetAttrsEnd bucket_object_get_attrs_end = 31;\n    BucketListObjectsStart bucket_list_objects_start = 32;\n    BucketListObjectsEnd bucket_list_objects_end = 33;\n    BucketDeleteObjectsStart bucket_delete_objects_start = 34;\n    BucketDeleteObjectsEnd bucket_delete_objects_end = 35;\n  }\n}\n\nmessage RPCCallStart {\n  string target_service_name = 1;\n  string target_endpoint_name = 2;\n  StackTrace stack = 3;\n}\n\nmessage RPCCallEnd {\n  optional Error err = 1;\n}\n\nmessage GoroutineStart {}\nmessage GoroutineEnd {}\n\nmessage DBTransactionStart {\n  StackTrace stack = 1;\n}\n\nmessage DBTransactionEnd {\n  enum CompletionType {\n    ROLLBACK = 0;\n    COMMIT = 1;\n  }\n\n  CompletionType completion = 1;\n  StackTrace stack = 2;\n  optional Error err = 3;\n}\n\nmessage DBQueryStart {\n  string query = 1;\n  StackTrace stack = 2;\n}\n\nmessage DBQueryEnd {\n  optional Error err = 1;\n}\n\nmessage PubsubPublishStart {\n  string topic = 1;\n  bytes message = 2;\n  StackTrace stack = 3;\n}\n\nmessage PubsubPublishEnd {\n  optional string message_id = 1;\n  optional Error err = 2;\n}\n\nmessage ServiceInitStart {\n  string service = 1;\n}\n\nmessage ServiceInitEnd {\n  optional Error err = 1;\n}\n\nmessage CacheCallStart {\n  string operation = 1;\n  repeated string keys = 2;\n  bool write = 3;\n  StackTrace stack = 4;\n  // TODO include more info (like inputs)\n}\n\nmessage CacheCallEnd {\n  Result result = 1;\n  optional Error err = 2;\n  // TODO include more info (like outputs)\n\n  enum Result {\n    UNKNOWN = 0;\n    OK = 1;\n    NO_SUCH_KEY = 2;\n    CONFLICT = 3;\n    ERR = 4;\n  }\n}\n\nmessage BucketObjectUploadStart {\n  string bucket = 1;\n  string object = 2;\n  BucketObjectAttributes attrs = 3;\n  StackTrace stack = 4;\n}\n\nmessage BucketObjectUploadEnd {\n  optional Error err = 1;\n  optional uint64 size = 2;\n  optional string version = 3;\n}\n\nmessage BucketObjectDownloadStart {\n  string bucket = 1;\n  string object = 2;\n  optional string version = 3;\n  StackTrace stack = 4;\n}\n\nmessage BucketObjectDownloadEnd {\n  optional Error err = 1;\n  optional uint64 size = 2;\n}\n\nmessage BucketObjectGetAttrsStart {\n  string bucket = 1;\n  string object = 2;\n  optional string version = 3;\n  StackTrace stack = 4;\n}\n\nmessage BucketObjectGetAttrsEnd {\n  optional Error err = 1;\n  optional BucketObjectAttributes attrs = 2;\n}\n\nmessage BucketListObjectsStart {\n  string bucket = 1;\n  optional string prefix = 2;\n  StackTrace stack = 3;\n}\n\nmessage BucketListObjectsEnd {\n  optional Error err = 1;\n  uint64 observed = 2;\n  bool has_more = 3;\n}\n\nmessage BucketDeleteObjectsStart {\n  string bucket = 1;\n  StackTrace stack = 2;\n  repeated BucketDeleteObjectEntry entries = 3;\n}\n\nmessage BucketDeleteObjectEntry {\n  string object = 1;\n  optional string version = 2;\n}\n\nmessage BucketDeleteObjectsEnd {\n  optional Error err = 1;\n}\n\nmessage BucketObjectAttributes {\n  optional uint64 size = 1;\n  optional string version = 2;\n  optional string etag = 3;\n  optional string content_type = 4;\n}\n\nmessage BodyStream {\n  bool is_response = 1;\n  bool overflowed = 2;\n  bytes data = 3;\n}\n\nmessage HTTPCallStart {\n  uint64 correlation_parent_span_id = 1;\n  string method = 2;\n  string url = 3;\n  StackTrace stack = 4;\n\n  // start_nanotime is used to compute timings based on the\n  // nanotime in the HTTP trace events.\n  int64 start_nanotime = 5;\n}\n\nmessage HTTPCallEnd {\n  // status_code is set if we got a HTTP response.\n  optional uint32 status_code = 1;\n  // err is set otherwise.\n  optional Error err = 2;\n\n  // TODO these should be moved to be asynchronous via a separate event.\n  repeated HTTPTraceEvent trace_events = 3;\n}\n\nenum HTTPTraceEventCode {\n  UNKNOWN = 0;\n  GET_CONN = 1;\n  GOT_CONN = 2;\n  GOT_FIRST_RESPONSE_BYTE = 3;\n  GOT_1XX_RESPONSE = 4;\n  DNS_START = 5;\n  DNS_DONE = 6;\n  CONNECT_START = 7;\n  CONNECT_DONE = 8;\n  TLS_HANDSHAKE_START = 9;\n  TLS_HANDSHAKE_DONE = 10;\n  WROTE_HEADERS = 11;\n  WROTE_REQUEST = 12;\n  WAIT_100_CONTINUE = 13;\n  CLOSED_BODY = 14;\n}\n\nmessage HTTPTraceEvent {\n  int64 nanotime = 1;\n  oneof data {\n    HTTPGetConn get_conn = 2;\n    HTTPGotConn got_conn = 3;\n    HTTPGotFirstResponseByte got_first_response_byte = 4;\n    HTTPGot1xxResponse got_1xx_response = 5;\n    HTTPDNSStart dns_start = 6;\n    HTTPDNSDone dns_done = 7;\n    HTTPConnectStart connect_start = 8;\n    HTTPConnectDone connect_done = 9;\n    HTTPTLSHandshakeStart tls_handshake_start = 10;\n    HTTPTLSHandshakeDone tls_handshake_done = 11;\n    HTTPWroteHeaders wrote_headers = 12;\n    HTTPWroteRequest wrote_request = 13;\n    HTTPWait100Continue wait_100_continue = 14;\n    HTTPClosedBodyData closed_body = 15;\n  }\n}\n\nmessage HTTPGetConn {\n  string host_port = 1;\n}\n\nmessage HTTPGotConn {\n  bool reused = 1;\n  bool was_idle = 2;\n  int64 idle_duration_ns = 3;\n}\n\nmessage HTTPGotFirstResponseByte {}\n\nmessage HTTPGot1xxResponse {\n  int32 code = 1;\n}\n\nmessage HTTPDNSStart {\n  string host = 1;\n}\n\nmessage HTTPDNSDone {\n  optional bytes err = 1;\n  repeated DNSAddr addrs = 2;\n}\n\nmessage DNSAddr {\n  bytes ip = 1;\n}\n\nmessage HTTPConnectStart {\n  string network = 1;\n  string addr = 2;\n}\n\nmessage HTTPConnectDone {\n  string network = 1;\n  string addr = 2;\n  bytes err = 3;\n}\n\nmessage HTTPTLSHandshakeStart {}\n\nmessage HTTPTLSHandshakeDone {\n  optional bytes err = 1;\n  uint32 tls_version = 2;\n  uint32 cipher_suite = 3;\n  string server_name = 4;\n  string negotiated_protocol = 5;\n}\n\nmessage HTTPWroteHeaders {}\n\nmessage HTTPWroteRequest {\n  optional bytes err = 1;\n}\n\nmessage HTTPWait100Continue {}\n\nmessage HTTPClosedBodyData {\n  optional bytes err = 1;\n}\n\nmessage LogMessage {\n  // Note: These values don't match the values used by the binary trace protocol,\n  // as these values are stored in persisted traces and therefore must maintain\n  // backwards compatibility. The binary trace protocol is versioned and doesn't\n  // have the same limitations.\n  enum Level {\n    DEBUG = 0;\n    INFO = 1;\n    ERROR = 2;\n    WARN = 3;\n    TRACE = 4;\n  }\n\n  Level level = 1;\n  string msg = 2;\n  repeated LogField fields = 3;\n  StackTrace stack = 4;\n}\n\nmessage LogField {\n  string key = 1;\n\n  oneof value {\n    Error error = 2;\n    string str = 3;\n    bool bool = 4;\n    google.protobuf.Timestamp time = 5;\n    int64 dur = 6;\n    bytes uuid = 7;\n    bytes json = 8;\n    int64 int = 9;\n    uint64 uint = 10;\n    float float32 = 11;\n    double float64 = 12;\n  }\n}\n\nmessage StackTrace {\n  repeated int64 pcs = 1;\n  repeated StackFrame frames = 2;\n}\n\nmessage StackFrame {\n  string filename = 1;\n  string func = 2;\n  int32 line = 3;\n}\n\nmessage Error {\n  string msg = 1;\n  optional StackTrace stack = 2;\n}\n\nenum StatusCode {\n  STATUS_CODE_OK = 0;\n  STATUS_CODE_CANCELED = 1;\n  STATUS_CODE_UNKNOWN = 2;\n  STATUS_CODE_INVALID_ARGUMENT = 3;\n  STATUS_CODE_DEADLINE_EXCEEDED = 4;\n  STATUS_CODE_NOT_FOUND = 5;\n  STATUS_CODE_ALREADY_EXISTS = 6;\n  STATUS_CODE_PERMISSION_DENIED = 7;\n  STATUS_CODE_RESOURCE_EXHAUSTED = 8;\n  STATUS_CODE_FAILED_PRECONDITION = 9;\n  STATUS_CODE_ABORTED = 10;\n  STATUS_CODE_OUT_OF_RANGE = 11;\n  STATUS_CODE_UNIMPLEMENTED = 12;\n  STATUS_CODE_INTERNAL = 13;\n  STATUS_CODE_UNAVAILABLE = 14;\n  STATUS_CODE_DATA_LOSS = 15;\n  STATUS_CODE_UNAUTHENTICATED = 16;\n}\n"
  },
  {
    "path": "proto/encore/engine/trace2/trace_util.go",
    "content": "package trace2\n\nfunc (id *TraceID) IsZero() bool {\n\treturn id == nil || (id.Low == 0 && id.High == 0)\n}\n"
  },
  {
    "path": "proto/encore/parser/meta/v1/meta.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/parser/meta/v1/meta.proto\n\npackage v1\n\nimport (\n\tv1 \"encr.dev/proto/encore/parser/schema/v1\"\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Lang describes the language an application is written in.\n// Defaults to Go if not set.\ntype Lang int32\n\nconst (\n\tLang_GO         Lang = 0\n\tLang_TYPESCRIPT Lang = 1\n)\n\n// Enum value maps for Lang.\nvar (\n\tLang_name = map[int32]string{\n\t\t0: \"GO\",\n\t\t1: \"TYPESCRIPT\",\n\t}\n\tLang_value = map[string]int32{\n\t\t\"GO\":         0,\n\t\t\"TYPESCRIPT\": 1,\n\t}\n)\n\nfunc (x Lang) Enum() *Lang {\n\tp := new(Lang)\n\t*p = x\n\treturn p\n}\n\nfunc (x Lang) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Lang) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Lang) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[0]\n}\n\nfunc (x Lang) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Lang.Descriptor instead.\nfunc (Lang) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{0}\n}\n\ntype BucketUsage_Operation int32\n\nconst (\n\tBucketUsage_UNKNOWN BucketUsage_Operation = 0\n\t// Listing objects and accessing their metadata during listing.\n\tBucketUsage_LIST_OBJECTS BucketUsage_Operation = 1\n\t// Reading the contents of an object.\n\tBucketUsage_READ_OBJECT_CONTENTS BucketUsage_Operation = 2\n\t// Creating or updating an object, with contents and metadata.\n\tBucketUsage_WRITE_OBJECT BucketUsage_Operation = 3\n\t// Updating the metadata of an object, without reading or writing its contents.\n\tBucketUsage_UPDATE_OBJECT_METADATA BucketUsage_Operation = 4\n\t// Reading the metadata of an object, or checking for its existence.\n\tBucketUsage_GET_OBJECT_METADATA BucketUsage_Operation = 5\n\t// Deleting an object.\n\tBucketUsage_DELETE_OBJECT BucketUsage_Operation = 6\n\t// Get an bucket/object's public url.\n\tBucketUsage_GET_PUBLIC_URL BucketUsage_Operation = 7\n\t// Generating a signed URL to allow an external recipient to create or\n\t// update an object.\n\tBucketUsage_SIGNED_UPLOAD_URL BucketUsage_Operation = 8\n\t// Generating a signed URL to allow an external recipient to download an object.\n\tBucketUsage_SIGNED_DOWNLOAD_URL BucketUsage_Operation = 9\n)\n\n// Enum value maps for BucketUsage_Operation.\nvar (\n\tBucketUsage_Operation_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"LIST_OBJECTS\",\n\t\t2: \"READ_OBJECT_CONTENTS\",\n\t\t3: \"WRITE_OBJECT\",\n\t\t4: \"UPDATE_OBJECT_METADATA\",\n\t\t5: \"GET_OBJECT_METADATA\",\n\t\t6: \"DELETE_OBJECT\",\n\t\t7: \"GET_PUBLIC_URL\",\n\t\t8: \"SIGNED_UPLOAD_URL\",\n\t\t9: \"SIGNED_DOWNLOAD_URL\",\n\t}\n\tBucketUsage_Operation_value = map[string]int32{\n\t\t\"UNKNOWN\":                0,\n\t\t\"LIST_OBJECTS\":           1,\n\t\t\"READ_OBJECT_CONTENTS\":   2,\n\t\t\"WRITE_OBJECT\":           3,\n\t\t\"UPDATE_OBJECT_METADATA\": 4,\n\t\t\"GET_OBJECT_METADATA\":    5,\n\t\t\"DELETE_OBJECT\":          6,\n\t\t\"GET_PUBLIC_URL\":         7,\n\t\t\"SIGNED_UPLOAD_URL\":      8,\n\t\t\"SIGNED_DOWNLOAD_URL\":    9,\n\t}\n)\n\nfunc (x BucketUsage_Operation) Enum() *BucketUsage_Operation {\n\tp := new(BucketUsage_Operation)\n\t*p = x\n\treturn p\n}\n\nfunc (x BucketUsage_Operation) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (BucketUsage_Operation) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[1].Descriptor()\n}\n\nfunc (BucketUsage_Operation) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[1]\n}\n\nfunc (x BucketUsage_Operation) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use BucketUsage_Operation.Descriptor instead.\nfunc (BucketUsage_Operation) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{4, 0}\n}\n\ntype Selector_Type int32\n\nconst (\n\tSelector_UNKNOWN Selector_Type = 0\n\tSelector_ALL     Selector_Type = 1\n\tSelector_TAG     Selector_Type = 2 // NOTE: If more types are added, update the (selector.Selector).ToProto method.\n)\n\n// Enum value maps for Selector_Type.\nvar (\n\tSelector_Type_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"ALL\",\n\t\t2: \"TAG\",\n\t}\n\tSelector_Type_value = map[string]int32{\n\t\t\"UNKNOWN\": 0,\n\t\t\"ALL\":     1,\n\t\t\"TAG\":     2,\n\t}\n)\n\nfunc (x Selector_Type) Enum() *Selector_Type {\n\tp := new(Selector_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x Selector_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Selector_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[2].Descriptor()\n}\n\nfunc (Selector_Type) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[2]\n}\n\nfunc (x Selector_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Selector_Type.Descriptor instead.\nfunc (Selector_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{5, 0}\n}\n\ntype RPC_AccessType int32\n\nconst (\n\tRPC_PRIVATE RPC_AccessType = 0\n\tRPC_PUBLIC  RPC_AccessType = 1\n\tRPC_AUTH    RPC_AccessType = 2\n)\n\n// Enum value maps for RPC_AccessType.\nvar (\n\tRPC_AccessType_name = map[int32]string{\n\t\t0: \"PRIVATE\",\n\t\t1: \"PUBLIC\",\n\t\t2: \"AUTH\",\n\t}\n\tRPC_AccessType_value = map[string]int32{\n\t\t\"PRIVATE\": 0,\n\t\t\"PUBLIC\":  1,\n\t\t\"AUTH\":    2,\n\t}\n)\n\nfunc (x RPC_AccessType) Enum() *RPC_AccessType {\n\tp := new(RPC_AccessType)\n\t*p = x\n\treturn p\n}\n\nfunc (x RPC_AccessType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (RPC_AccessType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[3].Descriptor()\n}\n\nfunc (RPC_AccessType) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[3]\n}\n\nfunc (x RPC_AccessType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use RPC_AccessType.Descriptor instead.\nfunc (RPC_AccessType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 0}\n}\n\ntype RPC_Protocol int32\n\nconst (\n\tRPC_REGULAR RPC_Protocol = 0\n\tRPC_RAW     RPC_Protocol = 1\n)\n\n// Enum value maps for RPC_Protocol.\nvar (\n\tRPC_Protocol_name = map[int32]string{\n\t\t0: \"REGULAR\",\n\t\t1: \"RAW\",\n\t}\n\tRPC_Protocol_value = map[string]int32{\n\t\t\"REGULAR\": 0,\n\t\t\"RAW\":     1,\n\t}\n)\n\nfunc (x RPC_Protocol) Enum() *RPC_Protocol {\n\tp := new(RPC_Protocol)\n\t*p = x\n\treturn p\n}\n\nfunc (x RPC_Protocol) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (RPC_Protocol) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[4].Descriptor()\n}\n\nfunc (RPC_Protocol) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[4]\n}\n\nfunc (x RPC_Protocol) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use RPC_Protocol.Descriptor instead.\nfunc (RPC_Protocol) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 1}\n}\n\ntype StaticCallNode_Package int32\n\nconst (\n\tStaticCallNode_UNKNOWN StaticCallNode_Package = 0\n\tStaticCallNode_SQLDB   StaticCallNode_Package = 1\n\tStaticCallNode_RLOG    StaticCallNode_Package = 2\n)\n\n// Enum value maps for StaticCallNode_Package.\nvar (\n\tStaticCallNode_Package_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"SQLDB\",\n\t\t2: \"RLOG\",\n\t}\n\tStaticCallNode_Package_value = map[string]int32{\n\t\t\"UNKNOWN\": 0,\n\t\t\"SQLDB\":   1,\n\t\t\"RLOG\":    2,\n\t}\n)\n\nfunc (x StaticCallNode_Package) Enum() *StaticCallNode_Package {\n\tp := new(StaticCallNode_Package)\n\t*p = x\n\treturn p\n}\n\nfunc (x StaticCallNode_Package) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (StaticCallNode_Package) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[5].Descriptor()\n}\n\nfunc (StaticCallNode_Package) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[5]\n}\n\nfunc (x StaticCallNode_Package) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use StaticCallNode_Package.Descriptor instead.\nfunc (StaticCallNode_Package) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{12, 0}\n}\n\ntype Path_Type int32\n\nconst (\n\tPath_URL            Path_Type = 0\n\tPath_CACHE_KEYSPACE Path_Type = 1\n)\n\n// Enum value maps for Path_Type.\nvar (\n\tPath_Type_name = map[int32]string{\n\t\t0: \"URL\",\n\t\t1: \"CACHE_KEYSPACE\",\n\t}\n\tPath_Type_value = map[string]int32{\n\t\t\"URL\":            0,\n\t\t\"CACHE_KEYSPACE\": 1,\n\t}\n)\n\nfunc (x Path_Type) Enum() *Path_Type {\n\tp := new(Path_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x Path_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Path_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[6].Descriptor()\n}\n\nfunc (Path_Type) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[6]\n}\n\nfunc (x Path_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Path_Type.Descriptor instead.\nfunc (Path_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{20, 0}\n}\n\ntype PathSegment_SegmentType int32\n\nconst (\n\tPathSegment_LITERAL  PathSegment_SegmentType = 0\n\tPathSegment_PARAM    PathSegment_SegmentType = 1\n\tPathSegment_WILDCARD PathSegment_SegmentType = 2\n\tPathSegment_FALLBACK PathSegment_SegmentType = 3\n)\n\n// Enum value maps for PathSegment_SegmentType.\nvar (\n\tPathSegment_SegmentType_name = map[int32]string{\n\t\t0: \"LITERAL\",\n\t\t1: \"PARAM\",\n\t\t2: \"WILDCARD\",\n\t\t3: \"FALLBACK\",\n\t}\n\tPathSegment_SegmentType_value = map[string]int32{\n\t\t\"LITERAL\":  0,\n\t\t\"PARAM\":    1,\n\t\t\"WILDCARD\": 2,\n\t\t\"FALLBACK\": 3,\n\t}\n)\n\nfunc (x PathSegment_SegmentType) Enum() *PathSegment_SegmentType {\n\tp := new(PathSegment_SegmentType)\n\t*p = x\n\treturn p\n}\n\nfunc (x PathSegment_SegmentType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (PathSegment_SegmentType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[7].Descriptor()\n}\n\nfunc (PathSegment_SegmentType) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[7]\n}\n\nfunc (x PathSegment_SegmentType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use PathSegment_SegmentType.Descriptor instead.\nfunc (PathSegment_SegmentType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{21, 0}\n}\n\ntype PathSegment_ParamType int32\n\nconst (\n\tPathSegment_STRING PathSegment_ParamType = 0\n\tPathSegment_BOOL   PathSegment_ParamType = 1\n\tPathSegment_INT8   PathSegment_ParamType = 2\n\tPathSegment_INT16  PathSegment_ParamType = 3\n\tPathSegment_INT32  PathSegment_ParamType = 4\n\tPathSegment_INT64  PathSegment_ParamType = 5\n\tPathSegment_INT    PathSegment_ParamType = 6\n\tPathSegment_UINT8  PathSegment_ParamType = 7\n\tPathSegment_UINT16 PathSegment_ParamType = 8\n\tPathSegment_UINT32 PathSegment_ParamType = 9\n\tPathSegment_UINT64 PathSegment_ParamType = 10\n\tPathSegment_UINT   PathSegment_ParamType = 11\n\tPathSegment_UUID   PathSegment_ParamType = 12\n)\n\n// Enum value maps for PathSegment_ParamType.\nvar (\n\tPathSegment_ParamType_name = map[int32]string{\n\t\t0:  \"STRING\",\n\t\t1:  \"BOOL\",\n\t\t2:  \"INT8\",\n\t\t3:  \"INT16\",\n\t\t4:  \"INT32\",\n\t\t5:  \"INT64\",\n\t\t6:  \"INT\",\n\t\t7:  \"UINT8\",\n\t\t8:  \"UINT16\",\n\t\t9:  \"UINT32\",\n\t\t10: \"UINT64\",\n\t\t11: \"UINT\",\n\t\t12: \"UUID\",\n\t}\n\tPathSegment_ParamType_value = map[string]int32{\n\t\t\"STRING\": 0,\n\t\t\"BOOL\":   1,\n\t\t\"INT8\":   2,\n\t\t\"INT16\":  3,\n\t\t\"INT32\":  4,\n\t\t\"INT64\":  5,\n\t\t\"INT\":    6,\n\t\t\"UINT8\":  7,\n\t\t\"UINT16\": 8,\n\t\t\"UINT32\": 9,\n\t\t\"UINT64\": 10,\n\t\t\"UINT\":   11,\n\t\t\"UUID\":   12,\n\t}\n)\n\nfunc (x PathSegment_ParamType) Enum() *PathSegment_ParamType {\n\tp := new(PathSegment_ParamType)\n\t*p = x\n\treturn p\n}\n\nfunc (x PathSegment_ParamType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (PathSegment_ParamType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[8].Descriptor()\n}\n\nfunc (PathSegment_ParamType) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[8]\n}\n\nfunc (x PathSegment_ParamType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use PathSegment_ParamType.Descriptor instead.\nfunc (PathSegment_ParamType) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{21, 1}\n}\n\ntype PubSubTopic_DeliveryGuarantee int32\n\nconst (\n\tPubSubTopic_AT_LEAST_ONCE PubSubTopic_DeliveryGuarantee = 0 // All messages will be delivered to each subscription at least once\n\tPubSubTopic_EXACTLY_ONCE  PubSubTopic_DeliveryGuarantee = 1 // All messages will be delivered to each subscription exactly once\n)\n\n// Enum value maps for PubSubTopic_DeliveryGuarantee.\nvar (\n\tPubSubTopic_DeliveryGuarantee_name = map[int32]string{\n\t\t0: \"AT_LEAST_ONCE\",\n\t\t1: \"EXACTLY_ONCE\",\n\t}\n\tPubSubTopic_DeliveryGuarantee_value = map[string]int32{\n\t\t\"AT_LEAST_ONCE\": 0,\n\t\t\"EXACTLY_ONCE\":  1,\n\t}\n)\n\nfunc (x PubSubTopic_DeliveryGuarantee) Enum() *PubSubTopic_DeliveryGuarantee {\n\tp := new(PubSubTopic_DeliveryGuarantee)\n\t*p = x\n\treturn p\n}\n\nfunc (x PubSubTopic_DeliveryGuarantee) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (PubSubTopic_DeliveryGuarantee) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[9].Descriptor()\n}\n\nfunc (PubSubTopic_DeliveryGuarantee) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[9]\n}\n\nfunc (x PubSubTopic_DeliveryGuarantee) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use PubSubTopic_DeliveryGuarantee.Descriptor instead.\nfunc (PubSubTopic_DeliveryGuarantee) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 0}\n}\n\ntype Metric_MetricKind int32\n\nconst (\n\tMetric_COUNTER   Metric_MetricKind = 0\n\tMetric_GAUGE     Metric_MetricKind = 1\n\tMetric_HISTOGRAM Metric_MetricKind = 2\n)\n\n// Enum value maps for Metric_MetricKind.\nvar (\n\tMetric_MetricKind_name = map[int32]string{\n\t\t0: \"COUNTER\",\n\t\t1: \"GAUGE\",\n\t\t2: \"HISTOGRAM\",\n\t}\n\tMetric_MetricKind_value = map[string]int32{\n\t\t\"COUNTER\":   0,\n\t\t\"GAUGE\":     1,\n\t\t\"HISTOGRAM\": 2,\n\t}\n)\n\nfunc (x Metric_MetricKind) Enum() *Metric_MetricKind {\n\tp := new(Metric_MetricKind)\n\t*p = x\n\treturn p\n}\n\nfunc (x Metric_MetricKind) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Metric_MetricKind) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_meta_v1_meta_proto_enumTypes[10].Descriptor()\n}\n\nfunc (Metric_MetricKind) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_meta_v1_meta_proto_enumTypes[10]\n}\n\nfunc (x Metric_MetricKind) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Metric_MetricKind.Descriptor instead.\nfunc (Metric_MetricKind) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{29, 0}\n}\n\n// Data is the metadata associated with an app version.\ntype Data struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tModulePath         string                 `protobuf:\"bytes,1,opt,name=module_path,json=modulePath,proto3\" json:\"module_path,omitempty\"`                          // app module path\n\tAppRevision        string                 `protobuf:\"bytes,2,opt,name=app_revision,json=appRevision,proto3\" json:\"app_revision,omitempty\"`                       // app revision (always the VCS revision reference)\n\tUncommittedChanges bool                   `protobuf:\"varint,8,opt,name=uncommitted_changes,json=uncommittedChanges,proto3\" json:\"uncommitted_changes,omitempty\"` // true if there where changes made on-top of the VCS revision\n\tDecls              []*v1.Decl             `protobuf:\"bytes,3,rep,name=decls,proto3\" json:\"decls,omitempty\"`\n\tPkgs               []*Package             `protobuf:\"bytes,4,rep,name=pkgs,proto3\" json:\"pkgs,omitempty\"`\n\tSvcs               []*Service             `protobuf:\"bytes,5,rep,name=svcs,proto3\" json:\"svcs,omitempty\"`\n\tAuthHandler        *AuthHandler           `protobuf:\"bytes,6,opt,name=auth_handler,json=authHandler,proto3,oneof\" json:\"auth_handler,omitempty\"` // the auth handler or nil\n\tCronJobs           []*CronJob             `protobuf:\"bytes,7,rep,name=cron_jobs,json=cronJobs,proto3\" json:\"cron_jobs,omitempty\"`\n\tPubsubTopics       []*PubSubTopic         `protobuf:\"bytes,9,rep,name=pubsub_topics,json=pubsubTopics,proto3\" json:\"pubsub_topics,omitempty\"` // All the pub sub topics declared in the application\n\tMiddleware         []*Middleware          `protobuf:\"bytes,10,rep,name=middleware,proto3\" json:\"middleware,omitempty\"`\n\tCacheClusters      []*CacheCluster        `protobuf:\"bytes,11,rep,name=cache_clusters,json=cacheClusters,proto3\" json:\"cache_clusters,omitempty\"`\n\tExperiments        []string               `protobuf:\"bytes,12,rep,name=experiments,proto3\" json:\"experiments,omitempty\"`\n\tMetrics            []*Metric              `protobuf:\"bytes,13,rep,name=metrics,proto3\" json:\"metrics,omitempty\"`\n\tSqlDatabases       []*SQLDatabase         `protobuf:\"bytes,14,rep,name=sql_databases,json=sqlDatabases,proto3\" json:\"sql_databases,omitempty\"`\n\tGateways           []*Gateway             `protobuf:\"bytes,15,rep,name=gateways,proto3\" json:\"gateways,omitempty\"`\n\tLanguage           Lang                   `protobuf:\"varint,16,opt,name=language,proto3,enum=encore.parser.meta.v1.Lang\" json:\"language,omitempty\"`\n\tBuckets            []*Bucket              `protobuf:\"bytes,17,rep,name=buckets,proto3\" json:\"buckets,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *Data) Reset() {\n\t*x = Data{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Data) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Data) ProtoMessage() {}\n\nfunc (x *Data) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Data.ProtoReflect.Descriptor instead.\nfunc (*Data) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Data) GetModulePath() string {\n\tif x != nil {\n\t\treturn x.ModulePath\n\t}\n\treturn \"\"\n}\n\nfunc (x *Data) GetAppRevision() string {\n\tif x != nil {\n\t\treturn x.AppRevision\n\t}\n\treturn \"\"\n}\n\nfunc (x *Data) GetUncommittedChanges() bool {\n\tif x != nil {\n\t\treturn x.UncommittedChanges\n\t}\n\treturn false\n}\n\nfunc (x *Data) GetDecls() []*v1.Decl {\n\tif x != nil {\n\t\treturn x.Decls\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetPkgs() []*Package {\n\tif x != nil {\n\t\treturn x.Pkgs\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetSvcs() []*Service {\n\tif x != nil {\n\t\treturn x.Svcs\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetAuthHandler() *AuthHandler {\n\tif x != nil {\n\t\treturn x.AuthHandler\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetCronJobs() []*CronJob {\n\tif x != nil {\n\t\treturn x.CronJobs\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetPubsubTopics() []*PubSubTopic {\n\tif x != nil {\n\t\treturn x.PubsubTopics\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetMiddleware() []*Middleware {\n\tif x != nil {\n\t\treturn x.Middleware\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetCacheClusters() []*CacheCluster {\n\tif x != nil {\n\t\treturn x.CacheClusters\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetExperiments() []string {\n\tif x != nil {\n\t\treturn x.Experiments\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetMetrics() []*Metric {\n\tif x != nil {\n\t\treturn x.Metrics\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetSqlDatabases() []*SQLDatabase {\n\tif x != nil {\n\t\treturn x.SqlDatabases\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetGateways() []*Gateway {\n\tif x != nil {\n\t\treturn x.Gateways\n\t}\n\treturn nil\n}\n\nfunc (x *Data) GetLanguage() Lang {\n\tif x != nil {\n\t\treturn x.Language\n\t}\n\treturn Lang_GO\n}\n\nfunc (x *Data) GetBuckets() []*Bucket {\n\tif x != nil {\n\t\treturn x.Buckets\n\t}\n\treturn nil\n}\n\n// QualifiedName is a name of an object in a specific package.\n// It is never an unqualified name, even in circumstances\n// where a package may refer to its own objects.\ntype QualifiedName struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPkg           string                 `protobuf:\"bytes,1,opt,name=pkg,proto3\" json:\"pkg,omitempty\"`   // \"rel/path/to/pkg\"\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"` // ObjectName\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *QualifiedName) Reset() {\n\t*x = QualifiedName{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *QualifiedName) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*QualifiedName) ProtoMessage() {}\n\nfunc (x *QualifiedName) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use QualifiedName.ProtoReflect.Descriptor instead.\nfunc (*QualifiedName) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *QualifiedName) GetPkg() string {\n\tif x != nil {\n\t\treturn x.Pkg\n\t}\n\treturn \"\"\n}\n\nfunc (x *QualifiedName) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\ntype Package struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tRelPath       string                 `protobuf:\"bytes,1,opt,name=rel_path,json=relPath,proto3\" json:\"rel_path,omitempty\"`             // import path relative to app root (\".\" for the app root itself)\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`                                  // package name as declared in Go files\n\tDoc           string                 `protobuf:\"bytes,3,opt,name=doc,proto3\" json:\"doc,omitempty\"`                                    // associated documentation\n\tServiceName   string                 `protobuf:\"bytes,4,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"` // service name this package is a part of, if any\n\tSecrets       []string               `protobuf:\"bytes,5,rep,name=secrets,proto3\" json:\"secrets,omitempty\"`                            // secrets required by this package\n\tRpcCalls      []*QualifiedName       `protobuf:\"bytes,6,rep,name=rpc_calls,json=rpcCalls,proto3\" json:\"rpc_calls,omitempty\"`          // RPCs called by the package\n\tTraceNodes    []*TraceNode           `protobuf:\"bytes,7,rep,name=trace_nodes,json=traceNodes,proto3\" json:\"trace_nodes,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Package) Reset() {\n\t*x = Package{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Package) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Package) ProtoMessage() {}\n\nfunc (x *Package) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Package.ProtoReflect.Descriptor instead.\nfunc (*Package) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Package) GetRelPath() string {\n\tif x != nil {\n\t\treturn x.RelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *Package) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Package) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *Package) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Package) GetSecrets() []string {\n\tif x != nil {\n\t\treturn x.Secrets\n\t}\n\treturn nil\n}\n\nfunc (x *Package) GetRpcCalls() []*QualifiedName {\n\tif x != nil {\n\t\treturn x.RpcCalls\n\t}\n\treturn nil\n}\n\nfunc (x *Package) GetTraceNodes() []*TraceNode {\n\tif x != nil {\n\t\treturn x.TraceNodes\n\t}\n\treturn nil\n}\n\ntype Service struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tRelPath       string                 `protobuf:\"bytes,2,opt,name=rel_path,json=relPath,proto3\" json:\"rel_path,omitempty\"` // import path relative to app root for the root package in the service\n\tRpcs          []*RPC                 `protobuf:\"bytes,3,rep,name=rpcs,proto3\" json:\"rpcs,omitempty\"`\n\tMigrations    []*DBMigration         `protobuf:\"bytes,4,rep,name=migrations,proto3\" json:\"migrations,omitempty\"`\n\tDatabases     []string               `protobuf:\"bytes,5,rep,name=databases,proto3\" json:\"databases,omitempty\"`                   // databases this service connects to\n\tHasConfig     bool                   `protobuf:\"varint,6,opt,name=has_config,json=hasConfig,proto3\" json:\"has_config,omitempty\"` // true if the service has uses config\n\tBuckets       []*BucketUsage         `protobuf:\"bytes,7,rep,name=buckets,proto3\" json:\"buckets,omitempty\"`                       // buckets this service uses\n\tMetrics       []string               `protobuf:\"bytes,8,rep,name=metrics,proto3\" json:\"metrics,omitempty\"`                       // metrics this service uses\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Service) Reset() {\n\t*x = Service{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Service) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Service) ProtoMessage() {}\n\nfunc (x *Service) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Service.ProtoReflect.Descriptor instead.\nfunc (*Service) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Service) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Service) GetRelPath() string {\n\tif x != nil {\n\t\treturn x.RelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *Service) GetRpcs() []*RPC {\n\tif x != nil {\n\t\treturn x.Rpcs\n\t}\n\treturn nil\n}\n\nfunc (x *Service) GetMigrations() []*DBMigration {\n\tif x != nil {\n\t\treturn x.Migrations\n\t}\n\treturn nil\n}\n\nfunc (x *Service) GetDatabases() []string {\n\tif x != nil {\n\t\treturn x.Databases\n\t}\n\treturn nil\n}\n\nfunc (x *Service) GetHasConfig() bool {\n\tif x != nil {\n\t\treturn x.HasConfig\n\t}\n\treturn false\n}\n\nfunc (x *Service) GetBuckets() []*BucketUsage {\n\tif x != nil {\n\t\treturn x.Buckets\n\t}\n\treturn nil\n}\n\nfunc (x *Service) GetMetrics() []string {\n\tif x != nil {\n\t\treturn x.Metrics\n\t}\n\treturn nil\n}\n\ntype BucketUsage struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The encore name of the bucket.\n\tBucket string `protobuf:\"bytes,1,opt,name=bucket,proto3\" json:\"bucket,omitempty\"`\n\t// Recorded operations.\n\tOperations    []BucketUsage_Operation `protobuf:\"varint,2,rep,packed,name=operations,proto3,enum=encore.parser.meta.v1.BucketUsage_Operation\" json:\"operations,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketUsage) Reset() {\n\t*x = BucketUsage{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketUsage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketUsage) ProtoMessage() {}\n\nfunc (x *BucketUsage) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketUsage.ProtoReflect.Descriptor instead.\nfunc (*BucketUsage) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *BucketUsage) GetBucket() string {\n\tif x != nil {\n\t\treturn x.Bucket\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketUsage) GetOperations() []BucketUsage_Operation {\n\tif x != nil {\n\t\treturn x.Operations\n\t}\n\treturn nil\n}\n\ntype Selector struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tType          Selector_Type          `protobuf:\"varint,1,opt,name=type,proto3,enum=encore.parser.meta.v1.Selector_Type\" json:\"type,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Selector) Reset() {\n\t*x = Selector{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Selector) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Selector) ProtoMessage() {}\n\nfunc (x *Selector) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Selector.ProtoReflect.Descriptor instead.\nfunc (*Selector) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *Selector) GetType() Selector_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn Selector_UNKNOWN\n}\n\nfunc (x *Selector) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype RPC struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tName           string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`                                                                          // name of the RPC endpoint\n\tDoc            *string                `protobuf:\"bytes,2,opt,name=doc,proto3,oneof\" json:\"doc,omitempty\"`                                                                      // associated documentation\n\tServiceName    string                 `protobuf:\"bytes,3,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`                                         // the service the RPC belongs to.\n\tAccessType     RPC_AccessType         `protobuf:\"varint,4,opt,name=access_type,json=accessType,proto3,enum=encore.parser.meta.v1.RPC_AccessType\" json:\"access_type,omitempty\"` // how can the RPC be accessed?\n\tRequestSchema  *v1.Type               `protobuf:\"bytes,5,opt,name=request_schema,json=requestSchema,proto3,oneof\" json:\"request_schema,omitempty\"`                             // request schema, or nil\n\tResponseSchema *v1.Type               `protobuf:\"bytes,6,opt,name=response_schema,json=responseSchema,proto3,oneof\" json:\"response_schema,omitempty\"`                          // response schema, or nil\n\tProto          RPC_Protocol           `protobuf:\"varint,7,opt,name=proto,proto3,enum=encore.parser.meta.v1.RPC_Protocol\" json:\"proto,omitempty\"`\n\tLoc            *v1.Loc                `protobuf:\"bytes,8,opt,name=loc,proto3\" json:\"loc,omitempty\"`\n\tPath           *Path                  `protobuf:\"bytes,9,opt,name=path,proto3\" json:\"path,omitempty\"`\n\tHttpMethods    []string               `protobuf:\"bytes,10,rep,name=http_methods,json=httpMethods,proto3\" json:\"http_methods,omitempty\"`\n\tTags           []*Selector            `protobuf:\"bytes,11,rep,name=tags,proto3\" json:\"tags,omitempty\"`\n\t// sensitive reports whether the whole payload is sensitive.\n\t// If true, none of the request/response payload will be traced.\n\tSensitive bool `protobuf:\"varint,12,opt,name=sensitive,proto3\" json:\"sensitive,omitempty\"`\n\t// Whether the endpoint can be called without auth parameters.\n\tAllowUnauthenticated bool `protobuf:\"varint,13,opt,name=allow_unauthenticated,json=allowUnauthenticated,proto3\" json:\"allow_unauthenticated,omitempty\"`\n\t// Whether the endpoint is exposed to the public, keyed by gateway.\n\tExpose map[string]*RPC_ExposeOptions `protobuf:\"bytes,14,rep,name=expose,proto3\" json:\"expose,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\t// The maximum size of the request body in bytes.\n\t// If not set, defaults to no limit.\n\tBodyLimit *uint64 `protobuf:\"varint,15,opt,name=body_limit,json=bodyLimit,proto3,oneof\" json:\"body_limit,omitempty\"`\n\t// If the endpoint is streaming\n\tStreamingRequest  bool     `protobuf:\"varint,16,opt,name=streaming_request,json=streamingRequest,proto3\" json:\"streaming_request,omitempty\"`\n\tStreamingResponse bool     `protobuf:\"varint,17,opt,name=streaming_response,json=streamingResponse,proto3\" json:\"streaming_response,omitempty\"`\n\tHandshakeSchema   *v1.Type `protobuf:\"bytes,18,opt,name=handshake_schema,json=handshakeSchema,proto3,oneof\" json:\"handshake_schema,omitempty\"` // handshake schema, or nil\n\t// If the endpoint serves static assets.\n\tStaticAssets  *RPC_StaticAssets `protobuf:\"bytes,19,opt,name=static_assets,json=staticAssets,proto3,oneof\" json:\"static_assets,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPC) Reset() {\n\t*x = RPC{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPC) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPC) ProtoMessage() {}\n\nfunc (x *RPC) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPC.ProtoReflect.Descriptor instead.\nfunc (*RPC) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *RPC) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPC) GetDoc() string {\n\tif x != nil && x.Doc != nil {\n\t\treturn *x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPC) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPC) GetAccessType() RPC_AccessType {\n\tif x != nil {\n\t\treturn x.AccessType\n\t}\n\treturn RPC_PRIVATE\n}\n\nfunc (x *RPC) GetRequestSchema() *v1.Type {\n\tif x != nil {\n\t\treturn x.RequestSchema\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetResponseSchema() *v1.Type {\n\tif x != nil {\n\t\treturn x.ResponseSchema\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetProto() RPC_Protocol {\n\tif x != nil {\n\t\treturn x.Proto\n\t}\n\treturn RPC_REGULAR\n}\n\nfunc (x *RPC) GetLoc() *v1.Loc {\n\tif x != nil {\n\t\treturn x.Loc\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetPath() *Path {\n\tif x != nil {\n\t\treturn x.Path\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetHttpMethods() []string {\n\tif x != nil {\n\t\treturn x.HttpMethods\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetTags() []*Selector {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetSensitive() bool {\n\tif x != nil {\n\t\treturn x.Sensitive\n\t}\n\treturn false\n}\n\nfunc (x *RPC) GetAllowUnauthenticated() bool {\n\tif x != nil {\n\t\treturn x.AllowUnauthenticated\n\t}\n\treturn false\n}\n\nfunc (x *RPC) GetExpose() map[string]*RPC_ExposeOptions {\n\tif x != nil {\n\t\treturn x.Expose\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetBodyLimit() uint64 {\n\tif x != nil && x.BodyLimit != nil {\n\t\treturn *x.BodyLimit\n\t}\n\treturn 0\n}\n\nfunc (x *RPC) GetStreamingRequest() bool {\n\tif x != nil {\n\t\treturn x.StreamingRequest\n\t}\n\treturn false\n}\n\nfunc (x *RPC) GetStreamingResponse() bool {\n\tif x != nil {\n\t\treturn x.StreamingResponse\n\t}\n\treturn false\n}\n\nfunc (x *RPC) GetHandshakeSchema() *v1.Type {\n\tif x != nil {\n\t\treturn x.HandshakeSchema\n\t}\n\treturn nil\n}\n\nfunc (x *RPC) GetStaticAssets() *RPC_StaticAssets {\n\tif x != nil {\n\t\treturn x.StaticAssets\n\t}\n\treturn nil\n}\n\ntype AuthHandler struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tDoc           string                 `protobuf:\"bytes,2,opt,name=doc,proto3\" json:\"doc,omitempty\"`\n\tPkgPath       string                 `protobuf:\"bytes,3,opt,name=pkg_path,json=pkgPath,proto3\" json:\"pkg_path,omitempty\"` // package (service) import path\n\tPkgName       string                 `protobuf:\"bytes,4,opt,name=pkg_name,json=pkgName,proto3\" json:\"pkg_name,omitempty\"` // package (service) name\n\tLoc           *v1.Loc                `protobuf:\"bytes,5,opt,name=loc,proto3\" json:\"loc,omitempty\"`\n\tAuthData      *v1.Type               `protobuf:\"bytes,6,opt,name=auth_data,json=authData,proto3,oneof\" json:\"auth_data,omitempty\"` // custom auth data, or nil\n\tParams        *v1.Type               `protobuf:\"bytes,7,opt,name=params,proto3,oneof\" json:\"params,omitempty\"`                     // builtin string or named type\n\tServiceName   string                 `protobuf:\"bytes,8,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AuthHandler) Reset() {\n\t*x = AuthHandler{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AuthHandler) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AuthHandler) ProtoMessage() {}\n\nfunc (x *AuthHandler) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AuthHandler.ProtoReflect.Descriptor instead.\nfunc (*AuthHandler) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *AuthHandler) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthHandler) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthHandler) GetPkgPath() string {\n\tif x != nil {\n\t\treturn x.PkgPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthHandler) GetPkgName() string {\n\tif x != nil {\n\t\treturn x.PkgName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthHandler) GetLoc() *v1.Loc {\n\tif x != nil {\n\t\treturn x.Loc\n\t}\n\treturn nil\n}\n\nfunc (x *AuthHandler) GetAuthData() *v1.Type {\n\tif x != nil {\n\t\treturn x.AuthData\n\t}\n\treturn nil\n}\n\nfunc (x *AuthHandler) GetParams() *v1.Type {\n\tif x != nil {\n\t\treturn x.Params\n\t}\n\treturn nil\n}\n\nfunc (x *AuthHandler) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\ntype Middleware struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          *QualifiedName         `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tDoc           string                 `protobuf:\"bytes,2,opt,name=doc,proto3\" json:\"doc,omitempty\"`\n\tLoc           *v1.Loc                `protobuf:\"bytes,3,opt,name=loc,proto3\" json:\"loc,omitempty\"`\n\tGlobal        bool                   `protobuf:\"varint,4,opt,name=global,proto3\" json:\"global,omitempty\"`\n\tServiceName   *string                `protobuf:\"bytes,5,opt,name=service_name,json=serviceName,proto3,oneof\" json:\"service_name,omitempty\"` // nil if global\n\tTarget        []*Selector            `protobuf:\"bytes,6,rep,name=target,proto3\" json:\"target,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Middleware) Reset() {\n\t*x = Middleware{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Middleware) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Middleware) ProtoMessage() {}\n\nfunc (x *Middleware) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Middleware.ProtoReflect.Descriptor instead.\nfunc (*Middleware) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *Middleware) GetName() *QualifiedName {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn nil\n}\n\nfunc (x *Middleware) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *Middleware) GetLoc() *v1.Loc {\n\tif x != nil {\n\t\treturn x.Loc\n\t}\n\treturn nil\n}\n\nfunc (x *Middleware) GetGlobal() bool {\n\tif x != nil {\n\t\treturn x.Global\n\t}\n\treturn false\n}\n\nfunc (x *Middleware) GetServiceName() string {\n\tif x != nil && x.ServiceName != nil {\n\t\treturn *x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Middleware) GetTarget() []*Selector {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn nil\n}\n\ntype TraceNode struct {\n\tstate        protoimpl.MessageState `protogen:\"open.v1\"`\n\tId           int32                  `protobuf:\"varint,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tFilepath     string                 `protobuf:\"bytes,2,opt,name=filepath,proto3\" json:\"filepath,omitempty\"` // slash-separated, relative to app root\n\tStartPos     int32                  `protobuf:\"varint,4,opt,name=start_pos,json=startPos,proto3\" json:\"start_pos,omitempty\"`\n\tEndPos       int32                  `protobuf:\"varint,5,opt,name=end_pos,json=endPos,proto3\" json:\"end_pos,omitempty\"`\n\tSrcLineStart int32                  `protobuf:\"varint,6,opt,name=src_line_start,json=srcLineStart,proto3\" json:\"src_line_start,omitempty\"`\n\tSrcLineEnd   int32                  `protobuf:\"varint,7,opt,name=src_line_end,json=srcLineEnd,proto3\" json:\"src_line_end,omitempty\"`\n\tSrcColStart  int32                  `protobuf:\"varint,8,opt,name=src_col_start,json=srcColStart,proto3\" json:\"src_col_start,omitempty\"`\n\tSrcColEnd    int32                  `protobuf:\"varint,9,opt,name=src_col_end,json=srcColEnd,proto3\" json:\"src_col_end,omitempty\"`\n\t// Types that are valid to be assigned to Context:\n\t//\n\t//\t*TraceNode_RpcDef\n\t//\t*TraceNode_RpcCall\n\t//\t*TraceNode_StaticCall\n\t//\t*TraceNode_AuthHandlerDef\n\t//\t*TraceNode_PubsubTopicDef\n\t//\t*TraceNode_PubsubPublish\n\t//\t*TraceNode_PubsubSubscriber\n\t//\t*TraceNode_ServiceInit\n\t//\t*TraceNode_MiddlewareDef\n\t//\t*TraceNode_CacheKeyspace\n\tContext       isTraceNode_Context `protobuf_oneof:\"context\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TraceNode) Reset() {\n\t*x = TraceNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TraceNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TraceNode) ProtoMessage() {}\n\nfunc (x *TraceNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TraceNode.ProtoReflect.Descriptor instead.\nfunc (*TraceNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *TraceNode) GetId() int32 {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetFilepath() string {\n\tif x != nil {\n\t\treturn x.Filepath\n\t}\n\treturn \"\"\n}\n\nfunc (x *TraceNode) GetStartPos() int32 {\n\tif x != nil {\n\t\treturn x.StartPos\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetEndPos() int32 {\n\tif x != nil {\n\t\treturn x.EndPos\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetSrcLineStart() int32 {\n\tif x != nil {\n\t\treturn x.SrcLineStart\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetSrcLineEnd() int32 {\n\tif x != nil {\n\t\treturn x.SrcLineEnd\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetSrcColStart() int32 {\n\tif x != nil {\n\t\treturn x.SrcColStart\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetSrcColEnd() int32 {\n\tif x != nil {\n\t\treturn x.SrcColEnd\n\t}\n\treturn 0\n}\n\nfunc (x *TraceNode) GetContext() isTraceNode_Context {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetRpcDef() *RPCDefNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_RpcDef); ok {\n\t\t\treturn x.RpcDef\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetRpcCall() *RPCCallNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_RpcCall); ok {\n\t\t\treturn x.RpcCall\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetStaticCall() *StaticCallNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_StaticCall); ok {\n\t\t\treturn x.StaticCall\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetAuthHandlerDef() *AuthHandlerDefNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_AuthHandlerDef); ok {\n\t\t\treturn x.AuthHandlerDef\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetPubsubTopicDef() *PubSubTopicDefNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_PubsubTopicDef); ok {\n\t\t\treturn x.PubsubTopicDef\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetPubsubPublish() *PubSubPublishNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_PubsubPublish); ok {\n\t\t\treturn x.PubsubPublish\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetPubsubSubscriber() *PubSubSubscriberNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_PubsubSubscriber); ok {\n\t\t\treturn x.PubsubSubscriber\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetServiceInit() *ServiceInitNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_ServiceInit); ok {\n\t\t\treturn x.ServiceInit\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetMiddlewareDef() *MiddlewareDefNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_MiddlewareDef); ok {\n\t\t\treturn x.MiddlewareDef\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TraceNode) GetCacheKeyspace() *CacheKeyspaceDefNode {\n\tif x != nil {\n\t\tif x, ok := x.Context.(*TraceNode_CacheKeyspace); ok {\n\t\t\treturn x.CacheKeyspace\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isTraceNode_Context interface {\n\tisTraceNode_Context()\n}\n\ntype TraceNode_RpcDef struct {\n\tRpcDef *RPCDefNode `protobuf:\"bytes,10,opt,name=rpc_def,json=rpcDef,proto3,oneof\"`\n}\n\ntype TraceNode_RpcCall struct {\n\tRpcCall *RPCCallNode `protobuf:\"bytes,11,opt,name=rpc_call,json=rpcCall,proto3,oneof\"`\n}\n\ntype TraceNode_StaticCall struct {\n\tStaticCall *StaticCallNode `protobuf:\"bytes,12,opt,name=static_call,json=staticCall,proto3,oneof\"`\n}\n\ntype TraceNode_AuthHandlerDef struct {\n\tAuthHandlerDef *AuthHandlerDefNode `protobuf:\"bytes,13,opt,name=auth_handler_def,json=authHandlerDef,proto3,oneof\"`\n}\n\ntype TraceNode_PubsubTopicDef struct {\n\tPubsubTopicDef *PubSubTopicDefNode `protobuf:\"bytes,14,opt,name=pubsub_topic_def,json=pubsubTopicDef,proto3,oneof\"`\n}\n\ntype TraceNode_PubsubPublish struct {\n\tPubsubPublish *PubSubPublishNode `protobuf:\"bytes,15,opt,name=pubsub_publish,json=pubsubPublish,proto3,oneof\"`\n}\n\ntype TraceNode_PubsubSubscriber struct {\n\tPubsubSubscriber *PubSubSubscriberNode `protobuf:\"bytes,16,opt,name=pubsub_subscriber,json=pubsubSubscriber,proto3,oneof\"`\n}\n\ntype TraceNode_ServiceInit struct {\n\tServiceInit *ServiceInitNode `protobuf:\"bytes,17,opt,name=service_init,json=serviceInit,proto3,oneof\"`\n}\n\ntype TraceNode_MiddlewareDef struct {\n\tMiddlewareDef *MiddlewareDefNode `protobuf:\"bytes,18,opt,name=middleware_def,json=middlewareDef,proto3,oneof\"`\n}\n\ntype TraceNode_CacheKeyspace struct {\n\tCacheKeyspace *CacheKeyspaceDefNode `protobuf:\"bytes,19,opt,name=cache_keyspace,json=cacheKeyspace,proto3,oneof\"`\n}\n\nfunc (*TraceNode_RpcDef) isTraceNode_Context() {}\n\nfunc (*TraceNode_RpcCall) isTraceNode_Context() {}\n\nfunc (*TraceNode_StaticCall) isTraceNode_Context() {}\n\nfunc (*TraceNode_AuthHandlerDef) isTraceNode_Context() {}\n\nfunc (*TraceNode_PubsubTopicDef) isTraceNode_Context() {}\n\nfunc (*TraceNode_PubsubPublish) isTraceNode_Context() {}\n\nfunc (*TraceNode_PubsubSubscriber) isTraceNode_Context() {}\n\nfunc (*TraceNode_ServiceInit) isTraceNode_Context() {}\n\nfunc (*TraceNode_MiddlewareDef) isTraceNode_Context() {}\n\nfunc (*TraceNode_CacheKeyspace) isTraceNode_Context() {}\n\ntype RPCDefNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tRpcName       string                 `protobuf:\"bytes,2,opt,name=rpc_name,json=rpcName,proto3\" json:\"rpc_name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,3,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPCDefNode) Reset() {\n\t*x = RPCDefNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPCDefNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPCDefNode) ProtoMessage() {}\n\nfunc (x *RPCDefNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPCDefNode.ProtoReflect.Descriptor instead.\nfunc (*RPCDefNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *RPCDefNode) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPCDefNode) GetRpcName() string {\n\tif x != nil {\n\t\treturn x.RpcName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPCDefNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype RPCCallNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tRpcName       string                 `protobuf:\"bytes,2,opt,name=rpc_name,json=rpcName,proto3\" json:\"rpc_name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,3,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPCCallNode) Reset() {\n\t*x = RPCCallNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPCCallNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPCCallNode) ProtoMessage() {}\n\nfunc (x *RPCCallNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPCCallNode.ProtoReflect.Descriptor instead.\nfunc (*RPCCallNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *RPCCallNode) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPCCallNode) GetRpcName() string {\n\tif x != nil {\n\t\treturn x.RpcName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPCCallNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype StaticCallNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPackage       StaticCallNode_Package `protobuf:\"varint,1,opt,name=package,proto3,enum=encore.parser.meta.v1.StaticCallNode_Package\" json:\"package,omitempty\"`\n\tFunc          string                 `protobuf:\"bytes,2,opt,name=func,proto3\" json:\"func,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,3,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StaticCallNode) Reset() {\n\t*x = StaticCallNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StaticCallNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StaticCallNode) ProtoMessage() {}\n\nfunc (x *StaticCallNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StaticCallNode.ProtoReflect.Descriptor instead.\nfunc (*StaticCallNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *StaticCallNode) GetPackage() StaticCallNode_Package {\n\tif x != nil {\n\t\treturn x.Package\n\t}\n\treturn StaticCallNode_UNKNOWN\n}\n\nfunc (x *StaticCallNode) GetFunc() string {\n\tif x != nil {\n\t\treturn x.Func\n\t}\n\treturn \"\"\n}\n\nfunc (x *StaticCallNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype AuthHandlerDefNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,3,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AuthHandlerDefNode) Reset() {\n\t*x = AuthHandlerDefNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AuthHandlerDefNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AuthHandlerDefNode) ProtoMessage() {}\n\nfunc (x *AuthHandlerDefNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AuthHandlerDefNode.ProtoReflect.Descriptor instead.\nfunc (*AuthHandlerDefNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *AuthHandlerDefNode) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthHandlerDefNode) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *AuthHandlerDefNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype PubSubTopicDefNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTopicName     string                 `protobuf:\"bytes,1,opt,name=topic_name,json=topicName,proto3\" json:\"topic_name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,2,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopicDefNode) Reset() {\n\t*x = PubSubTopicDefNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopicDefNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopicDefNode) ProtoMessage() {}\n\nfunc (x *PubSubTopicDefNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopicDefNode.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopicDefNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *PubSubTopicDefNode) GetTopicName() string {\n\tif x != nil {\n\t\treturn x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopicDefNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype PubSubPublishNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTopicName     string                 `protobuf:\"bytes,1,opt,name=topic_name,json=topicName,proto3\" json:\"topic_name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,2,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubPublishNode) Reset() {\n\t*x = PubSubPublishNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubPublishNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubPublishNode) ProtoMessage() {}\n\nfunc (x *PubSubPublishNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubPublishNode.ProtoReflect.Descriptor instead.\nfunc (*PubSubPublishNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *PubSubPublishNode) GetTopicName() string {\n\tif x != nil {\n\t\treturn x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubPublishNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype PubSubSubscriberNode struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tTopicName      string                 `protobuf:\"bytes,1,opt,name=topic_name,json=topicName,proto3\" json:\"topic_name,omitempty\"`\n\tSubscriberName string                 `protobuf:\"bytes,2,opt,name=subscriber_name,json=subscriberName,proto3\" json:\"subscriber_name,omitempty\"`\n\tServiceName    string                 `protobuf:\"bytes,3,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tContext        string                 `protobuf:\"bytes,4,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *PubSubSubscriberNode) Reset() {\n\t*x = PubSubSubscriberNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubSubscriberNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubSubscriberNode) ProtoMessage() {}\n\nfunc (x *PubSubSubscriberNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubSubscriberNode.ProtoReflect.Descriptor instead.\nfunc (*PubSubSubscriberNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *PubSubSubscriberNode) GetTopicName() string {\n\tif x != nil {\n\t\treturn x.TopicName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscriberNode) GetSubscriberName() string {\n\tif x != nil {\n\t\treturn x.SubscriberName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscriberNode) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscriberNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype ServiceInitNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tSetupFuncName string                 `protobuf:\"bytes,2,opt,name=setup_func_name,json=setupFuncName,proto3\" json:\"setup_func_name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,3,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceInitNode) Reset() {\n\t*x = ServiceInitNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceInitNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceInitNode) ProtoMessage() {}\n\nfunc (x *ServiceInitNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceInitNode.ProtoReflect.Descriptor instead.\nfunc (*ServiceInitNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *ServiceInitNode) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *ServiceInitNode) GetSetupFuncName() string {\n\tif x != nil {\n\t\treturn x.SetupFuncName\n\t}\n\treturn \"\"\n}\n\nfunc (x *ServiceInitNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype MiddlewareDefNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPkgRelPath    string                 `protobuf:\"bytes,1,opt,name=pkg_rel_path,json=pkgRelPath,proto3\" json:\"pkg_rel_path,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,3,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tTarget        []*Selector            `protobuf:\"bytes,4,rep,name=target,proto3\" json:\"target,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MiddlewareDefNode) Reset() {\n\t*x = MiddlewareDefNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MiddlewareDefNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MiddlewareDefNode) ProtoMessage() {}\n\nfunc (x *MiddlewareDefNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MiddlewareDefNode.ProtoReflect.Descriptor instead.\nfunc (*MiddlewareDefNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *MiddlewareDefNode) GetPkgRelPath() string {\n\tif x != nil {\n\t\treturn x.PkgRelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *MiddlewareDefNode) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *MiddlewareDefNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\nfunc (x *MiddlewareDefNode) GetTarget() []*Selector {\n\tif x != nil {\n\t\treturn x.Target\n\t}\n\treturn nil\n}\n\ntype CacheKeyspaceDefNode struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPkgRelPath    string                 `protobuf:\"bytes,1,opt,name=pkg_rel_path,json=pkgRelPath,proto3\" json:\"pkg_rel_path,omitempty\"`\n\tVarName       string                 `protobuf:\"bytes,2,opt,name=var_name,json=varName,proto3\" json:\"var_name,omitempty\"`\n\tClusterName   string                 `protobuf:\"bytes,3,opt,name=cluster_name,json=clusterName,proto3\" json:\"cluster_name,omitempty\"`\n\tContext       string                 `protobuf:\"bytes,4,opt,name=context,proto3\" json:\"context,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CacheKeyspaceDefNode) Reset() {\n\t*x = CacheKeyspaceDefNode{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CacheKeyspaceDefNode) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CacheKeyspaceDefNode) ProtoMessage() {}\n\nfunc (x *CacheKeyspaceDefNode) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CacheKeyspaceDefNode.ProtoReflect.Descriptor instead.\nfunc (*CacheKeyspaceDefNode) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *CacheKeyspaceDefNode) GetPkgRelPath() string {\n\tif x != nil {\n\t\treturn x.PkgRelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheKeyspaceDefNode) GetVarName() string {\n\tif x != nil {\n\t\treturn x.VarName\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheKeyspaceDefNode) GetClusterName() string {\n\tif x != nil {\n\t\treturn x.ClusterName\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheKeyspaceDefNode) GetContext() string {\n\tif x != nil {\n\t\treturn x.Context\n\t}\n\treturn \"\"\n}\n\ntype Path struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSegments      []*PathSegment         `protobuf:\"bytes,1,rep,name=segments,proto3\" json:\"segments,omitempty\"`\n\tType          Path_Type              `protobuf:\"varint,2,opt,name=type,proto3,enum=encore.parser.meta.v1.Path_Type\" json:\"type,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Path) Reset() {\n\t*x = Path{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Path) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Path) ProtoMessage() {}\n\nfunc (x *Path) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Path.ProtoReflect.Descriptor instead.\nfunc (*Path) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{20}\n}\n\nfunc (x *Path) GetSegments() []*PathSegment {\n\tif x != nil {\n\t\treturn x.Segments\n\t}\n\treturn nil\n}\n\nfunc (x *Path) GetType() Path_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn Path_URL\n}\n\ntype PathSegment struct {\n\tstate         protoimpl.MessageState  `protogen:\"open.v1\"`\n\tType          PathSegment_SegmentType `protobuf:\"varint,1,opt,name=type,proto3,enum=encore.parser.meta.v1.PathSegment_SegmentType\" json:\"type,omitempty\"`\n\tValue         string                  `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tValueType     PathSegment_ParamType   `protobuf:\"varint,3,opt,name=value_type,json=valueType,proto3,enum=encore.parser.meta.v1.PathSegment_ParamType\" json:\"value_type,omitempty\"`\n\tValidation    *v1.ValidationExpr      `protobuf:\"bytes,4,opt,name=validation,proto3,oneof\" json:\"validation,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PathSegment) Reset() {\n\t*x = PathSegment{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PathSegment) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PathSegment) ProtoMessage() {}\n\nfunc (x *PathSegment) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PathSegment.ProtoReflect.Descriptor instead.\nfunc (*PathSegment) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{21}\n}\n\nfunc (x *PathSegment) GetType() PathSegment_SegmentType {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn PathSegment_LITERAL\n}\n\nfunc (x *PathSegment) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\nfunc (x *PathSegment) GetValueType() PathSegment_ParamType {\n\tif x != nil {\n\t\treturn x.ValueType\n\t}\n\treturn PathSegment_STRING\n}\n\nfunc (x *PathSegment) GetValidation() *v1.ValidationExpr {\n\tif x != nil {\n\t\treturn x.Validation\n\t}\n\treturn nil\n}\n\ntype Gateway struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tEncoreName string                 `protobuf:\"bytes,1,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// Spec is the configuration for the gateway, if it's explicitly defined.\n\tExplicit      *Gateway_Explicit `protobuf:\"bytes,2,opt,name=explicit,proto3,oneof\" json:\"explicit,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Gateway) Reset() {\n\t*x = Gateway{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Gateway) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Gateway) ProtoMessage() {}\n\nfunc (x *Gateway) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Gateway.ProtoReflect.Descriptor instead.\nfunc (*Gateway) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{22}\n}\n\nfunc (x *Gateway) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Gateway) GetExplicit() *Gateway_Explicit {\n\tif x != nil {\n\t\treturn x.Explicit\n\t}\n\treturn nil\n}\n\ntype CronJob struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tTitle         string                 `protobuf:\"bytes,2,opt,name=title,proto3\" json:\"title,omitempty\"`\n\tDoc           *string                `protobuf:\"bytes,3,opt,name=doc,proto3,oneof\" json:\"doc,omitempty\"`\n\tSchedule      string                 `protobuf:\"bytes,4,opt,name=schedule,proto3\" json:\"schedule,omitempty\"`\n\tEndpoint      *QualifiedName         `protobuf:\"bytes,5,opt,name=endpoint,proto3\" json:\"endpoint,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CronJob) Reset() {\n\t*x = CronJob{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CronJob) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CronJob) ProtoMessage() {}\n\nfunc (x *CronJob) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CronJob.ProtoReflect.Descriptor instead.\nfunc (*CronJob) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{23}\n}\n\nfunc (x *CronJob) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *CronJob) GetTitle() string {\n\tif x != nil {\n\t\treturn x.Title\n\t}\n\treturn \"\"\n}\n\nfunc (x *CronJob) GetDoc() string {\n\tif x != nil && x.Doc != nil {\n\t\treturn *x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *CronJob) GetSchedule() string {\n\tif x != nil {\n\t\treturn x.Schedule\n\t}\n\treturn \"\"\n}\n\nfunc (x *CronJob) GetEndpoint() *QualifiedName {\n\tif x != nil {\n\t\treturn x.Endpoint\n\t}\n\treturn nil\n}\n\ntype SQLDatabase struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tName  string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tDoc   *string                `protobuf:\"bytes,2,opt,name=doc,proto3,oneof\" json:\"doc,omitempty\"`\n\t// migration_rel_path is the slash-separated path to the migrations,\n\t// relative to the main module's root directory.\n\tMigrationRelPath             *string        `protobuf:\"bytes,3,opt,name=migration_rel_path,json=migrationRelPath,proto3,oneof\" json:\"migration_rel_path,omitempty\"`\n\tMigrations                   []*DBMigration `protobuf:\"bytes,4,rep,name=migrations,proto3\" json:\"migrations,omitempty\"`\n\tAllowNonSequentialMigrations bool           `protobuf:\"varint,5,opt,name=allow_non_sequential_migrations,json=allowNonSequentialMigrations,proto3\" json:\"allow_non_sequential_migrations,omitempty\"`\n\tunknownFields                protoimpl.UnknownFields\n\tsizeCache                    protoimpl.SizeCache\n}\n\nfunc (x *SQLDatabase) Reset() {\n\t*x = SQLDatabase{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLDatabase) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLDatabase) ProtoMessage() {}\n\nfunc (x *SQLDatabase) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLDatabase.ProtoReflect.Descriptor instead.\nfunc (*SQLDatabase) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{24}\n}\n\nfunc (x *SQLDatabase) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLDatabase) GetDoc() string {\n\tif x != nil && x.Doc != nil {\n\t\treturn *x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLDatabase) GetMigrationRelPath() string {\n\tif x != nil && x.MigrationRelPath != nil {\n\t\treturn *x.MigrationRelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLDatabase) GetMigrations() []*DBMigration {\n\tif x != nil {\n\t\treturn x.Migrations\n\t}\n\treturn nil\n}\n\nfunc (x *SQLDatabase) GetAllowNonSequentialMigrations() bool {\n\tif x != nil {\n\t\treturn x.AllowNonSequentialMigrations\n\t}\n\treturn false\n}\n\ntype DBMigration struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFilename      string                 `protobuf:\"bytes,1,opt,name=filename,proto3\" json:\"filename,omitempty\"`       // filename\n\tNumber        uint64                 `protobuf:\"varint,2,opt,name=number,proto3\" json:\"number,omitempty\"`          // migration number\n\tDescription   string                 `protobuf:\"bytes,3,opt,name=description,proto3\" json:\"description,omitempty\"` // descriptive name\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DBMigration) Reset() {\n\t*x = DBMigration{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DBMigration) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DBMigration) ProtoMessage() {}\n\nfunc (x *DBMigration) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DBMigration.ProtoReflect.Descriptor instead.\nfunc (*DBMigration) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{25}\n}\n\nfunc (x *DBMigration) GetFilename() string {\n\tif x != nil {\n\t\treturn x.Filename\n\t}\n\treturn \"\"\n}\n\nfunc (x *DBMigration) GetNumber() uint64 {\n\tif x != nil {\n\t\treturn x.Number\n\t}\n\treturn 0\n}\n\nfunc (x *DBMigration) GetDescription() string {\n\tif x != nil {\n\t\treturn x.Description\n\t}\n\treturn \"\"\n}\n\ntype Bucket struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tDoc           *string                `protobuf:\"bytes,2,opt,name=doc,proto3,oneof\" json:\"doc,omitempty\"`\n\tVersioned     bool                   `protobuf:\"varint,3,opt,name=versioned,proto3\" json:\"versioned,omitempty\"`\n\tPublic        bool                   `protobuf:\"varint,4,opt,name=public,proto3\" json:\"public,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Bucket) Reset() {\n\t*x = Bucket{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Bucket) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Bucket) ProtoMessage() {}\n\nfunc (x *Bucket) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Bucket.ProtoReflect.Descriptor instead.\nfunc (*Bucket) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{26}\n}\n\nfunc (x *Bucket) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Bucket) GetDoc() string {\n\tif x != nil && x.Doc != nil {\n\t\treturn *x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *Bucket) GetVersioned() bool {\n\tif x != nil {\n\t\treturn x.Versioned\n\t}\n\treturn false\n}\n\nfunc (x *Bucket) GetPublic() bool {\n\tif x != nil {\n\t\treturn x.Public\n\t}\n\treturn false\n}\n\ntype PubSubTopic struct {\n\tstate             protoimpl.MessageState        `protogen:\"open.v1\"`\n\tName              string                        `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`                                                                                                              // The pub sub topic name (unique per application)\n\tDoc               *string                       `protobuf:\"bytes,2,opt,name=doc,proto3,oneof\" json:\"doc,omitempty\"`                                                                                                          // The documentation for the topic\n\tMessageType       *v1.Type                      `protobuf:\"bytes,3,opt,name=message_type,json=messageType,proto3\" json:\"message_type,omitempty\"`                                                                             // The type of the message\n\tDeliveryGuarantee PubSubTopic_DeliveryGuarantee `protobuf:\"varint,4,opt,name=delivery_guarantee,json=deliveryGuarantee,proto3,enum=encore.parser.meta.v1.PubSubTopic_DeliveryGuarantee\" json:\"delivery_guarantee,omitempty\"` // The delivery guarantee for the topic\n\tOrderingKey       string                        `protobuf:\"bytes,5,opt,name=ordering_key,json=orderingKey,proto3\" json:\"ordering_key,omitempty\"`                                                                             // The field used to group messages; if empty, the topic is not ordered\n\tPublishers        []*PubSubTopic_Publisher      `protobuf:\"bytes,6,rep,name=publishers,proto3\" json:\"publishers,omitempty\"`                                                                                                  // The publishers for this topic\n\tSubscriptions     []*PubSubTopic_Subscription   `protobuf:\"bytes,7,rep,name=subscriptions,proto3\" json:\"subscriptions,omitempty\"`                                                                                            // The subscriptions to the topic\n\tunknownFields     protoimpl.UnknownFields\n\tsizeCache         protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopic) Reset() {\n\t*x = PubSubTopic{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopic) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopic) ProtoMessage() {}\n\nfunc (x *PubSubTopic) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopic.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopic) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27}\n}\n\nfunc (x *PubSubTopic) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetDoc() string {\n\tif x != nil && x.Doc != nil {\n\t\treturn *x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetMessageType() *v1.Type {\n\tif x != nil {\n\t\treturn x.MessageType\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubTopic) GetDeliveryGuarantee() PubSubTopic_DeliveryGuarantee {\n\tif x != nil {\n\t\treturn x.DeliveryGuarantee\n\t}\n\treturn PubSubTopic_AT_LEAST_ONCE\n}\n\nfunc (x *PubSubTopic) GetOrderingKey() string {\n\tif x != nil {\n\t\treturn x.OrderingKey\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetPublishers() []*PubSubTopic_Publisher {\n\tif x != nil {\n\t\treturn x.Publishers\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubTopic) GetSubscriptions() []*PubSubTopic_Subscription {\n\tif x != nil {\n\t\treturn x.Subscriptions\n\t}\n\treturn nil\n}\n\ntype CacheCluster struct {\n\tstate          protoimpl.MessageState   `protogen:\"open.v1\"`\n\tName           string                   `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`                                           // The pub sub topic name (unique per application)\n\tDoc            string                   `protobuf:\"bytes,2,opt,name=doc,proto3\" json:\"doc,omitempty\"`                                             // The documentation for the topic\n\tKeyspaces      []*CacheCluster_Keyspace `protobuf:\"bytes,3,rep,name=keyspaces,proto3\" json:\"keyspaces,omitempty\"`                                 // The publishers for this topic\n\tEvictionPolicy string                   `protobuf:\"bytes,4,opt,name=eviction_policy,json=evictionPolicy,proto3\" json:\"eviction_policy,omitempty\"` // redis eviction policy\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *CacheCluster) Reset() {\n\t*x = CacheCluster{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CacheCluster) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CacheCluster) ProtoMessage() {}\n\nfunc (x *CacheCluster) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CacheCluster.ProtoReflect.Descriptor instead.\nfunc (*CacheCluster) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{28}\n}\n\nfunc (x *CacheCluster) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheCluster) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheCluster) GetKeyspaces() []*CacheCluster_Keyspace {\n\tif x != nil {\n\t\treturn x.Keyspaces\n\t}\n\treturn nil\n}\n\nfunc (x *CacheCluster) GetEvictionPolicy() string {\n\tif x != nil {\n\t\treturn x.EvictionPolicy\n\t}\n\treturn \"\"\n}\n\ntype Metric struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"` // the name of the metric\n\tValueType     v1.Builtin             `protobuf:\"varint,2,opt,name=value_type,json=valueType,proto3,enum=encore.parser.schema.v1.Builtin\" json:\"value_type,omitempty\"`\n\tDoc           string                 `protobuf:\"bytes,3,opt,name=doc,proto3\" json:\"doc,omitempty\"` // the doc string\n\tKind          Metric_MetricKind      `protobuf:\"varint,4,opt,name=kind,proto3,enum=encore.parser.meta.v1.Metric_MetricKind\" json:\"kind,omitempty\"`\n\tServiceName   *string                `protobuf:\"bytes,5,opt,name=service_name,json=serviceName,proto3,oneof\" json:\"service_name,omitempty\"` // the service the metric is exclusive to, if any.\n\tLabels        []*Metric_Label        `protobuf:\"bytes,6,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Metric) Reset() {\n\t*x = Metric{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Metric) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Metric) ProtoMessage() {}\n\nfunc (x *Metric) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Metric.ProtoReflect.Descriptor instead.\nfunc (*Metric) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{29}\n}\n\nfunc (x *Metric) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Metric) GetValueType() v1.Builtin {\n\tif x != nil {\n\t\treturn x.ValueType\n\t}\n\treturn v1.Builtin(0)\n}\n\nfunc (x *Metric) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *Metric) GetKind() Metric_MetricKind {\n\tif x != nil {\n\t\treturn x.Kind\n\t}\n\treturn Metric_COUNTER\n}\n\nfunc (x *Metric) GetServiceName() string {\n\tif x != nil && x.ServiceName != nil {\n\t\treturn *x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Metric) GetLabels() []*Metric_Label {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\ntype RPC_ExposeOptions struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPC_ExposeOptions) Reset() {\n\t*x = RPC_ExposeOptions{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[31]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPC_ExposeOptions) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPC_ExposeOptions) ProtoMessage() {}\n\nfunc (x *RPC_ExposeOptions) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[31]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPC_ExposeOptions.ProtoReflect.Descriptor instead.\nfunc (*RPC_ExposeOptions) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 1}\n}\n\ntype RPC_StaticAssets struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// dir_rel_path is the slash-separated path to the static files directory,\n\t// relative to the app root.\n\tDirRelPath string `protobuf:\"bytes,1,opt,name=dir_rel_path,json=dirRelPath,proto3\" json:\"dir_rel_path,omitempty\"`\n\t// not_found_rel_path is the relative path to the file to serve when the requested\n\t// file is not found. It is relative to the files_rel_path directory.\n\tNotFoundRelPath *string                                   `protobuf:\"bytes,2,opt,name=not_found_rel_path,json=notFoundRelPath,proto3,oneof\" json:\"not_found_rel_path,omitempty\"`\n\tNotFoundStatus  *uint32                                   `protobuf:\"varint,3,opt,name=not_found_status,json=notFoundStatus,proto3,oneof\" json:\"not_found_status,omitempty\"`\n\tHeaders         map[string]*RPC_StaticAssets_HeaderValues `protobuf:\"bytes,4,rep,name=headers,proto3\" json:\"headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *RPC_StaticAssets) Reset() {\n\t*x = RPC_StaticAssets{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[32]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPC_StaticAssets) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPC_StaticAssets) ProtoMessage() {}\n\nfunc (x *RPC_StaticAssets) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[32]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPC_StaticAssets.ProtoReflect.Descriptor instead.\nfunc (*RPC_StaticAssets) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 2}\n}\n\nfunc (x *RPC_StaticAssets) GetDirRelPath() string {\n\tif x != nil {\n\t\treturn x.DirRelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPC_StaticAssets) GetNotFoundRelPath() string {\n\tif x != nil && x.NotFoundRelPath != nil {\n\t\treturn *x.NotFoundRelPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *RPC_StaticAssets) GetNotFoundStatus() uint32 {\n\tif x != nil && x.NotFoundStatus != nil {\n\t\treturn *x.NotFoundStatus\n\t}\n\treturn 0\n}\n\nfunc (x *RPC_StaticAssets) GetHeaders() map[string]*RPC_StaticAssets_HeaderValues {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn nil\n}\n\n// Custom HTTP headers to apply to all static files served.\n// Each header can have multiple values.\ntype RPC_StaticAssets_HeaderValues struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tValues        []string               `protobuf:\"bytes,1,rep,name=values,proto3\" json:\"values,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RPC_StaticAssets_HeaderValues) Reset() {\n\t*x = RPC_StaticAssets_HeaderValues{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[33]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RPC_StaticAssets_HeaderValues) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RPC_StaticAssets_HeaderValues) ProtoMessage() {}\n\nfunc (x *RPC_StaticAssets_HeaderValues) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[33]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RPC_StaticAssets_HeaderValues.ProtoReflect.Descriptor instead.\nfunc (*RPC_StaticAssets_HeaderValues) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{6, 2, 0}\n}\n\nfunc (x *RPC_StaticAssets_HeaderValues) GetValues() []string {\n\tif x != nil {\n\t\treturn x.Values\n\t}\n\treturn nil\n}\n\ntype Gateway_Explicit struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The service name this gateway belongs to.\n\tServiceName   string       `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`\n\tAuthHandler   *AuthHandler `protobuf:\"bytes,2,opt,name=auth_handler,json=authHandler,proto3,oneof\" json:\"auth_handler,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Gateway_Explicit) Reset() {\n\t*x = Gateway_Explicit{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[35]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Gateway_Explicit) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Gateway_Explicit) ProtoMessage() {}\n\nfunc (x *Gateway_Explicit) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[35]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Gateway_Explicit.ProtoReflect.Descriptor instead.\nfunc (*Gateway_Explicit) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{22, 0}\n}\n\nfunc (x *Gateway_Explicit) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Gateway_Explicit) GetAuthHandler() *AuthHandler {\n\tif x != nil {\n\t\treturn x.AuthHandler\n\t}\n\treturn nil\n}\n\ntype PubSubTopic_Publisher struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tServiceName   string                 `protobuf:\"bytes,1,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"` // The service the publisher is in\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopic_Publisher) Reset() {\n\t*x = PubSubTopic_Publisher{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[36]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopic_Publisher) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopic_Publisher) ProtoMessage() {}\n\nfunc (x *PubSubTopic_Publisher) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[36]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopic_Publisher.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopic_Publisher) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 0}\n}\n\nfunc (x *PubSubTopic_Publisher) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\ntype PubSubTopic_Subscription struct {\n\tstate            protoimpl.MessageState   `protogen:\"open.v1\"`\n\tName             string                   `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`                                                  // The unique name of the subscription for this topic\n\tServiceName      string                   `protobuf:\"bytes,2,opt,name=service_name,json=serviceName,proto3\" json:\"service_name,omitempty\"`                 // The service that the subscriber is in\n\tAckDeadline      int64                    `protobuf:\"varint,3,opt,name=ack_deadline,json=ackDeadline,proto3\" json:\"ack_deadline,omitempty\"`                // How long has a consumer got to process and ack a message in nanoseconds\n\tMessageRetention int64                    `protobuf:\"varint,4,opt,name=message_retention,json=messageRetention,proto3\" json:\"message_retention,omitempty\"` // How long is an undelivered message kept in nanoseconds\n\tRetryPolicy      *PubSubTopic_RetryPolicy `protobuf:\"bytes,5,opt,name=retry_policy,json=retryPolicy,proto3\" json:\"retry_policy,omitempty\"`                 // The retry policy for the subscription\n\t// How many messages each instance can process concurrently.\n\t// If not set, the default is provider-specific.\n\tMaxConcurrency *int32 `protobuf:\"varint,6,opt,name=max_concurrency,json=maxConcurrency,proto3,oneof\" json:\"max_concurrency,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopic_Subscription) Reset() {\n\t*x = PubSubTopic_Subscription{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[37]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopic_Subscription) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopic_Subscription) ProtoMessage() {}\n\nfunc (x *PubSubTopic_Subscription) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[37]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopic_Subscription.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopic_Subscription) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 1}\n}\n\nfunc (x *PubSubTopic_Subscription) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic_Subscription) GetServiceName() string {\n\tif x != nil {\n\t\treturn x.ServiceName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic_Subscription) GetAckDeadline() int64 {\n\tif x != nil {\n\t\treturn x.AckDeadline\n\t}\n\treturn 0\n}\n\nfunc (x *PubSubTopic_Subscription) GetMessageRetention() int64 {\n\tif x != nil {\n\t\treturn x.MessageRetention\n\t}\n\treturn 0\n}\n\nfunc (x *PubSubTopic_Subscription) GetRetryPolicy() *PubSubTopic_RetryPolicy {\n\tif x != nil {\n\t\treturn x.RetryPolicy\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubTopic_Subscription) GetMaxConcurrency() int32 {\n\tif x != nil && x.MaxConcurrency != nil {\n\t\treturn *x.MaxConcurrency\n\t}\n\treturn 0\n}\n\ntype PubSubTopic_RetryPolicy struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMinBackoff    int64                  `protobuf:\"varint,1,opt,name=min_backoff,json=minBackoff,proto3\" json:\"min_backoff,omitempty\"` // min backoff in nanoseconds\n\tMaxBackoff    int64                  `protobuf:\"varint,2,opt,name=max_backoff,json=maxBackoff,proto3\" json:\"max_backoff,omitempty\"` // max backoff in nanoseconds\n\tMaxRetries    int64                  `protobuf:\"varint,3,opt,name=max_retries,json=maxRetries,proto3\" json:\"max_retries,omitempty\"` // max number of retries\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopic_RetryPolicy) Reset() {\n\t*x = PubSubTopic_RetryPolicy{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[38]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopic_RetryPolicy) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopic_RetryPolicy) ProtoMessage() {}\n\nfunc (x *PubSubTopic_RetryPolicy) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[38]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopic_RetryPolicy.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopic_RetryPolicy) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{27, 2}\n}\n\nfunc (x *PubSubTopic_RetryPolicy) GetMinBackoff() int64 {\n\tif x != nil {\n\t\treturn x.MinBackoff\n\t}\n\treturn 0\n}\n\nfunc (x *PubSubTopic_RetryPolicy) GetMaxBackoff() int64 {\n\tif x != nil {\n\t\treturn x.MaxBackoff\n\t}\n\treturn 0\n}\n\nfunc (x *PubSubTopic_RetryPolicy) GetMaxRetries() int64 {\n\tif x != nil {\n\t\treturn x.MaxRetries\n\t}\n\treturn 0\n}\n\ntype CacheCluster_Keyspace struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKeyType       *v1.Type               `protobuf:\"bytes,1,opt,name=key_type,json=keyType,proto3\" json:\"key_type,omitempty\"`\n\tValueType     *v1.Type               `protobuf:\"bytes,2,opt,name=value_type,json=valueType,proto3\" json:\"value_type,omitempty\"`\n\tService       string                 `protobuf:\"bytes,3,opt,name=service,proto3\" json:\"service,omitempty\"`\n\tDoc           string                 `protobuf:\"bytes,4,opt,name=doc,proto3\" json:\"doc,omitempty\"`\n\tPathPattern   *Path                  `protobuf:\"bytes,5,opt,name=path_pattern,json=pathPattern,proto3\" json:\"path_pattern,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CacheCluster_Keyspace) Reset() {\n\t*x = CacheCluster_Keyspace{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[39]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CacheCluster_Keyspace) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CacheCluster_Keyspace) ProtoMessage() {}\n\nfunc (x *CacheCluster_Keyspace) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[39]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CacheCluster_Keyspace.ProtoReflect.Descriptor instead.\nfunc (*CacheCluster_Keyspace) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{28, 0}\n}\n\nfunc (x *CacheCluster_Keyspace) GetKeyType() *v1.Type {\n\tif x != nil {\n\t\treturn x.KeyType\n\t}\n\treturn nil\n}\n\nfunc (x *CacheCluster_Keyspace) GetValueType() *v1.Type {\n\tif x != nil {\n\t\treturn x.ValueType\n\t}\n\treturn nil\n}\n\nfunc (x *CacheCluster_Keyspace) GetService() string {\n\tif x != nil {\n\t\treturn x.Service\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheCluster_Keyspace) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *CacheCluster_Keyspace) GetPathPattern() *Path {\n\tif x != nil {\n\t\treturn x.PathPattern\n\t}\n\treturn nil\n}\n\ntype Metric_Label struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tType          v1.Builtin             `protobuf:\"varint,2,opt,name=type,proto3,enum=encore.parser.schema.v1.Builtin\" json:\"type,omitempty\"`\n\tDoc           string                 `protobuf:\"bytes,3,opt,name=doc,proto3\" json:\"doc,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Metric_Label) Reset() {\n\t*x = Metric_Label{}\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[40]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Metric_Label) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Metric_Label) ProtoMessage() {}\n\nfunc (x *Metric_Label) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_meta_v1_meta_proto_msgTypes[40]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Metric_Label.ProtoReflect.Descriptor instead.\nfunc (*Metric_Label) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescGZIP(), []int{29, 0}\n}\n\nfunc (x *Metric_Label) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *Metric_Label) GetType() v1.Builtin {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn v1.Builtin(0)\n}\n\nfunc (x *Metric_Label) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nvar File_encore_parser_meta_v1_meta_proto protoreflect.FileDescriptor\n\nconst file_encore_parser_meta_v1_meta_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\" encore/parser/meta/v1/meta.proto\\x12\\x15encore.parser.meta.v1\\x1a$encore/parser/schema/v1/schema.proto\\\"\\xdc\\a\\n\" +\n\t\"\\x04Data\\x12\\x1f\\n\" +\n\t\"\\vmodule_path\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"modulePath\\x12!\\n\" +\n\t\"\\fapp_revision\\x18\\x02 \\x01(\\tR\\vappRevision\\x12/\\n\" +\n\t\"\\x13uncommitted_changes\\x18\\b \\x01(\\bR\\x12uncommittedChanges\\x123\\n\" +\n\t\"\\x05decls\\x18\\x03 \\x03(\\v2\\x1d.encore.parser.schema.v1.DeclR\\x05decls\\x122\\n\" +\n\t\"\\x04pkgs\\x18\\x04 \\x03(\\v2\\x1e.encore.parser.meta.v1.PackageR\\x04pkgs\\x122\\n\" +\n\t\"\\x04svcs\\x18\\x05 \\x03(\\v2\\x1e.encore.parser.meta.v1.ServiceR\\x04svcs\\x12J\\n\" +\n\t\"\\fauth_handler\\x18\\x06 \\x01(\\v2\\\".encore.parser.meta.v1.AuthHandlerH\\x00R\\vauthHandler\\x88\\x01\\x01\\x12;\\n\" +\n\t\"\\tcron_jobs\\x18\\a \\x03(\\v2\\x1e.encore.parser.meta.v1.CronJobR\\bcronJobs\\x12G\\n\" +\n\t\"\\rpubsub_topics\\x18\\t \\x03(\\v2\\\".encore.parser.meta.v1.PubSubTopicR\\fpubsubTopics\\x12A\\n\" +\n\t\"\\n\" +\n\t\"middleware\\x18\\n\" +\n\t\" \\x03(\\v2!.encore.parser.meta.v1.MiddlewareR\\n\" +\n\t\"middleware\\x12J\\n\" +\n\t\"\\x0ecache_clusters\\x18\\v \\x03(\\v2#.encore.parser.meta.v1.CacheClusterR\\rcacheClusters\\x12 \\n\" +\n\t\"\\vexperiments\\x18\\f \\x03(\\tR\\vexperiments\\x127\\n\" +\n\t\"\\ametrics\\x18\\r \\x03(\\v2\\x1d.encore.parser.meta.v1.MetricR\\ametrics\\x12G\\n\" +\n\t\"\\rsql_databases\\x18\\x0e \\x03(\\v2\\\".encore.parser.meta.v1.SQLDatabaseR\\fsqlDatabases\\x12:\\n\" +\n\t\"\\bgateways\\x18\\x0f \\x03(\\v2\\x1e.encore.parser.meta.v1.GatewayR\\bgateways\\x127\\n\" +\n\t\"\\blanguage\\x18\\x10 \\x01(\\x0e2\\x1b.encore.parser.meta.v1.LangR\\blanguage\\x127\\n\" +\n\t\"\\abuckets\\x18\\x11 \\x03(\\v2\\x1d.encore.parser.meta.v1.BucketR\\abucketsB\\x0f\\n\" +\n\t\"\\r_auth_handler\\\"5\\n\" +\n\t\"\\rQualifiedName\\x12\\x10\\n\" +\n\t\"\\x03pkg\\x18\\x01 \\x01(\\tR\\x03pkg\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\\"\\x8d\\x02\\n\" +\n\t\"\\aPackage\\x12\\x19\\n\" +\n\t\"\\brel_path\\x18\\x01 \\x01(\\tR\\arelPath\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x03 \\x01(\\tR\\x03doc\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x04 \\x01(\\tR\\vserviceName\\x12\\x18\\n\" +\n\t\"\\asecrets\\x18\\x05 \\x03(\\tR\\asecrets\\x12A\\n\" +\n\t\"\\trpc_calls\\x18\\x06 \\x03(\\v2$.encore.parser.meta.v1.QualifiedNameR\\brpcCalls\\x12A\\n\" +\n\t\"\\vtrace_nodes\\x18\\a \\x03(\\v2 .encore.parser.meta.v1.TraceNodeR\\n\" +\n\t\"traceNodes\\\"\\xc1\\x02\\n\" +\n\t\"\\aService\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x19\\n\" +\n\t\"\\brel_path\\x18\\x02 \\x01(\\tR\\arelPath\\x12.\\n\" +\n\t\"\\x04rpcs\\x18\\x03 \\x03(\\v2\\x1a.encore.parser.meta.v1.RPCR\\x04rpcs\\x12B\\n\" +\n\t\"\\n\" +\n\t\"migrations\\x18\\x04 \\x03(\\v2\\\".encore.parser.meta.v1.DBMigrationR\\n\" +\n\t\"migrations\\x12\\x1c\\n\" +\n\t\"\\tdatabases\\x18\\x05 \\x03(\\tR\\tdatabases\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"has_config\\x18\\x06 \\x01(\\bR\\thasConfig\\x12<\\n\" +\n\t\"\\abuckets\\x18\\a \\x03(\\v2\\\".encore.parser.meta.v1.BucketUsageR\\abuckets\\x12\\x18\\n\" +\n\t\"\\ametrics\\x18\\b \\x03(\\tR\\ametrics\\\"\\xd8\\x02\\n\" +\n\t\"\\vBucketUsage\\x12\\x16\\n\" +\n\t\"\\x06bucket\\x18\\x01 \\x01(\\tR\\x06bucket\\x12L\\n\" +\n\t\"\\n\" +\n\t\"operations\\x18\\x02 \\x03(\\x0e2,.encore.parser.meta.v1.BucketUsage.OperationR\\n\" +\n\t\"operations\\\"\\xe2\\x01\\n\" +\n\t\"\\tOperation\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\x10\\n\" +\n\t\"\\fLIST_OBJECTS\\x10\\x01\\x12\\x18\\n\" +\n\t\"\\x14READ_OBJECT_CONTENTS\\x10\\x02\\x12\\x10\\n\" +\n\t\"\\fWRITE_OBJECT\\x10\\x03\\x12\\x1a\\n\" +\n\t\"\\x16UPDATE_OBJECT_METADATA\\x10\\x04\\x12\\x17\\n\" +\n\t\"\\x13GET_OBJECT_METADATA\\x10\\x05\\x12\\x11\\n\" +\n\t\"\\rDELETE_OBJECT\\x10\\x06\\x12\\x12\\n\" +\n\t\"\\x0eGET_PUBLIC_URL\\x10\\a\\x12\\x15\\n\" +\n\t\"\\x11SIGNED_UPLOAD_URL\\x10\\b\\x12\\x17\\n\" +\n\t\"\\x13SIGNED_DOWNLOAD_URL\\x10\\t\\\"\\x81\\x01\\n\" +\n\t\"\\bSelector\\x128\\n\" +\n\t\"\\x04type\\x18\\x01 \\x01(\\x0e2$.encore.parser.meta.v1.Selector.TypeR\\x04type\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\\"%\\n\" +\n\t\"\\x04Type\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\a\\n\" +\n\t\"\\x03ALL\\x10\\x01\\x12\\a\\n\" +\n\t\"\\x03TAG\\x10\\x02\\\"\\xb4\\r\\n\" +\n\t\"\\x03RPC\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x15\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tH\\x00R\\x03doc\\x88\\x01\\x01\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x03 \\x01(\\tR\\vserviceName\\x12F\\n\" +\n\t\"\\vaccess_type\\x18\\x04 \\x01(\\x0e2%.encore.parser.meta.v1.RPC.AccessTypeR\\n\" +\n\t\"accessType\\x12I\\n\" +\n\t\"\\x0erequest_schema\\x18\\x05 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeH\\x01R\\rrequestSchema\\x88\\x01\\x01\\x12K\\n\" +\n\t\"\\x0fresponse_schema\\x18\\x06 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeH\\x02R\\x0eresponseSchema\\x88\\x01\\x01\\x129\\n\" +\n\t\"\\x05proto\\x18\\a \\x01(\\x0e2#.encore.parser.meta.v1.RPC.ProtocolR\\x05proto\\x12.\\n\" +\n\t\"\\x03loc\\x18\\b \\x01(\\v2\\x1c.encore.parser.schema.v1.LocR\\x03loc\\x12/\\n\" +\n\t\"\\x04path\\x18\\t \\x01(\\v2\\x1b.encore.parser.meta.v1.PathR\\x04path\\x12!\\n\" +\n\t\"\\fhttp_methods\\x18\\n\" +\n\t\" \\x03(\\tR\\vhttpMethods\\x123\\n\" +\n\t\"\\x04tags\\x18\\v \\x03(\\v2\\x1f.encore.parser.meta.v1.SelectorR\\x04tags\\x12\\x1c\\n\" +\n\t\"\\tsensitive\\x18\\f \\x01(\\bR\\tsensitive\\x123\\n\" +\n\t\"\\x15allow_unauthenticated\\x18\\r \\x01(\\bR\\x14allowUnauthenticated\\x12>\\n\" +\n\t\"\\x06expose\\x18\\x0e \\x03(\\v2&.encore.parser.meta.v1.RPC.ExposeEntryR\\x06expose\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"body_limit\\x18\\x0f \\x01(\\x04H\\x03R\\tbodyLimit\\x88\\x01\\x01\\x12+\\n\" +\n\t\"\\x11streaming_request\\x18\\x10 \\x01(\\bR\\x10streamingRequest\\x12-\\n\" +\n\t\"\\x12streaming_response\\x18\\x11 \\x01(\\bR\\x11streamingResponse\\x12M\\n\" +\n\t\"\\x10handshake_schema\\x18\\x12 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeH\\x04R\\x0fhandshakeSchema\\x88\\x01\\x01\\x12Q\\n\" +\n\t\"\\rstatic_assets\\x18\\x13 \\x01(\\v2'.encore.parser.meta.v1.RPC.StaticAssetsH\\x05R\\fstaticAssets\\x88\\x01\\x01\\x1ac\\n\" +\n\t\"\\vExposeEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12>\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2(.encore.parser.meta.v1.RPC.ExposeOptionsR\\x05value:\\x028\\x01\\x1a\\x0f\\n\" +\n\t\"\\rExposeOptions\\x1a\\xa7\\x03\\n\" +\n\t\"\\fStaticAssets\\x12 \\n\" +\n\t\"\\fdir_rel_path\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"dirRelPath\\x120\\n\" +\n\t\"\\x12not_found_rel_path\\x18\\x02 \\x01(\\tH\\x00R\\x0fnotFoundRelPath\\x88\\x01\\x01\\x12-\\n\" +\n\t\"\\x10not_found_status\\x18\\x03 \\x01(\\rH\\x01R\\x0enotFoundStatus\\x88\\x01\\x01\\x12N\\n\" +\n\t\"\\aheaders\\x18\\x04 \\x03(\\v24.encore.parser.meta.v1.RPC.StaticAssets.HeadersEntryR\\aheaders\\x1a&\\n\" +\n\t\"\\fHeaderValues\\x12\\x16\\n\" +\n\t\"\\x06values\\x18\\x01 \\x03(\\tR\\x06values\\x1ap\\n\" +\n\t\"\\fHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12J\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v24.encore.parser.meta.v1.RPC.StaticAssets.HeaderValuesR\\x05value:\\x028\\x01B\\x15\\n\" +\n\t\"\\x13_not_found_rel_pathB\\x13\\n\" +\n\t\"\\x11_not_found_status\\\"/\\n\" +\n\t\"\\n\" +\n\t\"AccessType\\x12\\v\\n\" +\n\t\"\\aPRIVATE\\x10\\x00\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06PUBLIC\\x10\\x01\\x12\\b\\n\" +\n\t\"\\x04AUTH\\x10\\x02\\\" \\n\" +\n\t\"\\bProtocol\\x12\\v\\n\" +\n\t\"\\aREGULAR\\x10\\x00\\x12\\a\\n\" +\n\t\"\\x03RAW\\x10\\x01B\\x06\\n\" +\n\t\"\\x04_docB\\x11\\n\" +\n\t\"\\x0f_request_schemaB\\x12\\n\" +\n\t\"\\x10_response_schemaB\\r\\n\" +\n\t\"\\v_body_limitB\\x13\\n\" +\n\t\"\\x11_handshake_schemaB\\x10\\n\" +\n\t\"\\x0e_static_assets\\\"\\xd2\\x02\\n\" +\n\t\"\\vAuthHandler\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tR\\x03doc\\x12\\x19\\n\" +\n\t\"\\bpkg_path\\x18\\x03 \\x01(\\tR\\apkgPath\\x12\\x19\\n\" +\n\t\"\\bpkg_name\\x18\\x04 \\x01(\\tR\\apkgName\\x12.\\n\" +\n\t\"\\x03loc\\x18\\x05 \\x01(\\v2\\x1c.encore.parser.schema.v1.LocR\\x03loc\\x12?\\n\" +\n\t\"\\tauth_data\\x18\\x06 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeH\\x00R\\bauthData\\x88\\x01\\x01\\x12:\\n\" +\n\t\"\\x06params\\x18\\a \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeH\\x01R\\x06params\\x88\\x01\\x01\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\b \\x01(\\tR\\vserviceNameB\\f\\n\" +\n\t\"\\n\" +\n\t\"_auth_dataB\\t\\n\" +\n\t\"\\a_params\\\"\\x92\\x02\\n\" +\n\t\"\\n\" +\n\t\"Middleware\\x128\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\v2$.encore.parser.meta.v1.QualifiedNameR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tR\\x03doc\\x12.\\n\" +\n\t\"\\x03loc\\x18\\x03 \\x01(\\v2\\x1c.encore.parser.schema.v1.LocR\\x03loc\\x12\\x16\\n\" +\n\t\"\\x06global\\x18\\x04 \\x01(\\bR\\x06global\\x12&\\n\" +\n\t\"\\fservice_name\\x18\\x05 \\x01(\\tH\\x00R\\vserviceName\\x88\\x01\\x01\\x127\\n\" +\n\t\"\\x06target\\x18\\x06 \\x03(\\v2\\x1f.encore.parser.meta.v1.SelectorR\\x06targetB\\x0f\\n\" +\n\t\"\\r_service_name\\\"\\xa0\\b\\n\" +\n\t\"\\tTraceNode\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\x05R\\x02id\\x12\\x1a\\n\" +\n\t\"\\bfilepath\\x18\\x02 \\x01(\\tR\\bfilepath\\x12\\x1b\\n\" +\n\t\"\\tstart_pos\\x18\\x04 \\x01(\\x05R\\bstartPos\\x12\\x17\\n\" +\n\t\"\\aend_pos\\x18\\x05 \\x01(\\x05R\\x06endPos\\x12$\\n\" +\n\t\"\\x0esrc_line_start\\x18\\x06 \\x01(\\x05R\\fsrcLineStart\\x12 \\n\" +\n\t\"\\fsrc_line_end\\x18\\a \\x01(\\x05R\\n\" +\n\t\"srcLineEnd\\x12\\\"\\n\" +\n\t\"\\rsrc_col_start\\x18\\b \\x01(\\x05R\\vsrcColStart\\x12\\x1e\\n\" +\n\t\"\\vsrc_col_end\\x18\\t \\x01(\\x05R\\tsrcColEnd\\x12<\\n\" +\n\t\"\\arpc_def\\x18\\n\" +\n\t\" \\x01(\\v2!.encore.parser.meta.v1.RPCDefNodeH\\x00R\\x06rpcDef\\x12?\\n\" +\n\t\"\\brpc_call\\x18\\v \\x01(\\v2\\\".encore.parser.meta.v1.RPCCallNodeH\\x00R\\arpcCall\\x12H\\n\" +\n\t\"\\vstatic_call\\x18\\f \\x01(\\v2%.encore.parser.meta.v1.StaticCallNodeH\\x00R\\n\" +\n\t\"staticCall\\x12U\\n\" +\n\t\"\\x10auth_handler_def\\x18\\r \\x01(\\v2).encore.parser.meta.v1.AuthHandlerDefNodeH\\x00R\\x0eauthHandlerDef\\x12U\\n\" +\n\t\"\\x10pubsub_topic_def\\x18\\x0e \\x01(\\v2).encore.parser.meta.v1.PubSubTopicDefNodeH\\x00R\\x0epubsubTopicDef\\x12Q\\n\" +\n\t\"\\x0epubsub_publish\\x18\\x0f \\x01(\\v2(.encore.parser.meta.v1.PubSubPublishNodeH\\x00R\\rpubsubPublish\\x12Z\\n\" +\n\t\"\\x11pubsub_subscriber\\x18\\x10 \\x01(\\v2+.encore.parser.meta.v1.PubSubSubscriberNodeH\\x00R\\x10pubsubSubscriber\\x12K\\n\" +\n\t\"\\fservice_init\\x18\\x11 \\x01(\\v2&.encore.parser.meta.v1.ServiceInitNodeH\\x00R\\vserviceInit\\x12Q\\n\" +\n\t\"\\x0emiddleware_def\\x18\\x12 \\x01(\\v2(.encore.parser.meta.v1.MiddlewareDefNodeH\\x00R\\rmiddlewareDef\\x12T\\n\" +\n\t\"\\x0ecache_keyspace\\x18\\x13 \\x01(\\v2+.encore.parser.meta.v1.CacheKeyspaceDefNodeH\\x00R\\rcacheKeyspaceB\\t\\n\" +\n\t\"\\acontext\\\"d\\n\" +\n\t\"\\n\" +\n\t\"RPCDefNode\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x19\\n\" +\n\t\"\\brpc_name\\x18\\x02 \\x01(\\tR\\arpcName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x03 \\x01(\\tR\\acontext\\\"e\\n\" +\n\t\"\\vRPCCallNode\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x19\\n\" +\n\t\"\\brpc_name\\x18\\x02 \\x01(\\tR\\arpcName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x03 \\x01(\\tR\\acontext\\\"\\xb4\\x01\\n\" +\n\t\"\\x0eStaticCallNode\\x12G\\n\" +\n\t\"\\apackage\\x18\\x01 \\x01(\\x0e2-.encore.parser.meta.v1.StaticCallNode.PackageR\\apackage\\x12\\x12\\n\" +\n\t\"\\x04func\\x18\\x02 \\x01(\\tR\\x04func\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x03 \\x01(\\tR\\acontext\\\"+\\n\" +\n\t\"\\aPackage\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05SQLDB\\x10\\x01\\x12\\b\\n\" +\n\t\"\\x04RLOG\\x10\\x02\\\"e\\n\" +\n\t\"\\x12AuthHandlerDefNode\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x03 \\x01(\\tR\\acontext\\\"M\\n\" +\n\t\"\\x12PubSubTopicDefNode\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\x01 \\x01(\\tR\\ttopicName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x02 \\x01(\\tR\\acontext\\\"L\\n\" +\n\t\"\\x11PubSubPublishNode\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\x01 \\x01(\\tR\\ttopicName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x02 \\x01(\\tR\\acontext\\\"\\x9b\\x01\\n\" +\n\t\"\\x14PubSubSubscriberNode\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"topic_name\\x18\\x01 \\x01(\\tR\\ttopicName\\x12'\\n\" +\n\t\"\\x0fsubscriber_name\\x18\\x02 \\x01(\\tR\\x0esubscriberName\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x03 \\x01(\\tR\\vserviceName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x04 \\x01(\\tR\\acontext\\\"v\\n\" +\n\t\"\\x0fServiceInitNode\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12&\\n\" +\n\t\"\\x0fsetup_func_name\\x18\\x02 \\x01(\\tR\\rsetupFuncName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x03 \\x01(\\tR\\acontext\\\"\\x9c\\x01\\n\" +\n\t\"\\x11MiddlewareDefNode\\x12 \\n\" +\n\t\"\\fpkg_rel_path\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"pkgRelPath\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x03 \\x01(\\tR\\acontext\\x127\\n\" +\n\t\"\\x06target\\x18\\x04 \\x03(\\v2\\x1f.encore.parser.meta.v1.SelectorR\\x06target\\\"\\x90\\x01\\n\" +\n\t\"\\x14CacheKeyspaceDefNode\\x12 \\n\" +\n\t\"\\fpkg_rel_path\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"pkgRelPath\\x12\\x19\\n\" +\n\t\"\\bvar_name\\x18\\x02 \\x01(\\tR\\avarName\\x12!\\n\" +\n\t\"\\fcluster_name\\x18\\x03 \\x01(\\tR\\vclusterName\\x12\\x18\\n\" +\n\t\"\\acontext\\x18\\x04 \\x01(\\tR\\acontext\\\"\\xa1\\x01\\n\" +\n\t\"\\x04Path\\x12>\\n\" +\n\t\"\\bsegments\\x18\\x01 \\x03(\\v2\\\".encore.parser.meta.v1.PathSegmentR\\bsegments\\x124\\n\" +\n\t\"\\x04type\\x18\\x02 \\x01(\\x0e2 .encore.parser.meta.v1.Path.TypeR\\x04type\\\"#\\n\" +\n\t\"\\x04Type\\x12\\a\\n\" +\n\t\"\\x03URL\\x10\\x00\\x12\\x12\\n\" +\n\t\"\\x0eCACHE_KEYSPACE\\x10\\x01\\\"\\xef\\x03\\n\" +\n\t\"\\vPathSegment\\x12B\\n\" +\n\t\"\\x04type\\x18\\x01 \\x01(\\x0e2..encore.parser.meta.v1.PathSegment.SegmentTypeR\\x04type\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\x12K\\n\" +\n\t\"\\n\" +\n\t\"value_type\\x18\\x03 \\x01(\\x0e2,.encore.parser.meta.v1.PathSegment.ParamTypeR\\tvalueType\\x12L\\n\" +\n\t\"\\n\" +\n\t\"validation\\x18\\x04 \\x01(\\v2'.encore.parser.schema.v1.ValidationExprH\\x00R\\n\" +\n\t\"validation\\x88\\x01\\x01\\\"A\\n\" +\n\t\"\\vSegmentType\\x12\\v\\n\" +\n\t\"\\aLITERAL\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05PARAM\\x10\\x01\\x12\\f\\n\" +\n\t\"\\bWILDCARD\\x10\\x02\\x12\\f\\n\" +\n\t\"\\bFALLBACK\\x10\\x03\\\"\\x98\\x01\\n\" +\n\t\"\\tParamType\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06STRING\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04BOOL\\x10\\x01\\x12\\b\\n\" +\n\t\"\\x04INT8\\x10\\x02\\x12\\t\\n\" +\n\t\"\\x05INT16\\x10\\x03\\x12\\t\\n\" +\n\t\"\\x05INT32\\x10\\x04\\x12\\t\\n\" +\n\t\"\\x05INT64\\x10\\x05\\x12\\a\\n\" +\n\t\"\\x03INT\\x10\\x06\\x12\\t\\n\" +\n\t\"\\x05UINT8\\x10\\a\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UINT16\\x10\\b\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UINT32\\x10\\t\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UINT64\\x10\\n\" +\n\t\"\\x12\\b\\n\" +\n\t\"\\x04UINT\\x10\\v\\x12\\b\\n\" +\n\t\"\\x04UUID\\x10\\fB\\r\\n\" +\n\t\"\\v_validation\\\"\\x8e\\x02\\n\" +\n\t\"\\aGateway\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12H\\n\" +\n\t\"\\bexplicit\\x18\\x02 \\x01(\\v2'.encore.parser.meta.v1.Gateway.ExplicitH\\x00R\\bexplicit\\x88\\x01\\x01\\x1a\\x8a\\x01\\n\" +\n\t\"\\bExplicit\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x12J\\n\" +\n\t\"\\fauth_handler\\x18\\x02 \\x01(\\v2\\\".encore.parser.meta.v1.AuthHandlerH\\x00R\\vauthHandler\\x88\\x01\\x01B\\x0f\\n\" +\n\t\"\\r_auth_handlerB\\v\\n\" +\n\t\"\\t_explicit\\\"\\xac\\x01\\n\" +\n\t\"\\aCronJob\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x14\\n\" +\n\t\"\\x05title\\x18\\x02 \\x01(\\tR\\x05title\\x12\\x15\\n\" +\n\t\"\\x03doc\\x18\\x03 \\x01(\\tH\\x00R\\x03doc\\x88\\x01\\x01\\x12\\x1a\\n\" +\n\t\"\\bschedule\\x18\\x04 \\x01(\\tR\\bschedule\\x12@\\n\" +\n\t\"\\bendpoint\\x18\\x05 \\x01(\\v2$.encore.parser.meta.v1.QualifiedNameR\\bendpointB\\x06\\n\" +\n\t\"\\x04_doc\\\"\\x95\\x02\\n\" +\n\t\"\\vSQLDatabase\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x15\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tH\\x00R\\x03doc\\x88\\x01\\x01\\x121\\n\" +\n\t\"\\x12migration_rel_path\\x18\\x03 \\x01(\\tH\\x01R\\x10migrationRelPath\\x88\\x01\\x01\\x12B\\n\" +\n\t\"\\n\" +\n\t\"migrations\\x18\\x04 \\x03(\\v2\\\".encore.parser.meta.v1.DBMigrationR\\n\" +\n\t\"migrations\\x12E\\n\" +\n\t\"\\x1fallow_non_sequential_migrations\\x18\\x05 \\x01(\\bR\\x1callowNonSequentialMigrationsB\\x06\\n\" +\n\t\"\\x04_docB\\x15\\n\" +\n\t\"\\x13_migration_rel_path\\\"c\\n\" +\n\t\"\\vDBMigration\\x12\\x1a\\n\" +\n\t\"\\bfilename\\x18\\x01 \\x01(\\tR\\bfilename\\x12\\x16\\n\" +\n\t\"\\x06number\\x18\\x02 \\x01(\\x04R\\x06number\\x12 \\n\" +\n\t\"\\vdescription\\x18\\x03 \\x01(\\tR\\vdescription\\\"q\\n\" +\n\t\"\\x06Bucket\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x15\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tH\\x00R\\x03doc\\x88\\x01\\x01\\x12\\x1c\\n\" +\n\t\"\\tversioned\\x18\\x03 \\x01(\\bR\\tversioned\\x12\\x16\\n\" +\n\t\"\\x06public\\x18\\x04 \\x01(\\bR\\x06publicB\\x06\\n\" +\n\t\"\\x04_doc\\\"\\xb8\\a\\n\" +\n\t\"\\vPubSubTopic\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x15\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tH\\x00R\\x03doc\\x88\\x01\\x01\\x12@\\n\" +\n\t\"\\fmessage_type\\x18\\x03 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\vmessageType\\x12c\\n\" +\n\t\"\\x12delivery_guarantee\\x18\\x04 \\x01(\\x0e24.encore.parser.meta.v1.PubSubTopic.DeliveryGuaranteeR\\x11deliveryGuarantee\\x12!\\n\" +\n\t\"\\fordering_key\\x18\\x05 \\x01(\\tR\\vorderingKey\\x12L\\n\" +\n\t\"\\n\" +\n\t\"publishers\\x18\\x06 \\x03(\\v2,.encore.parser.meta.v1.PubSubTopic.PublisherR\\n\" +\n\t\"publishers\\x12U\\n\" +\n\t\"\\rsubscriptions\\x18\\a \\x03(\\v2/.encore.parser.meta.v1.PubSubTopic.SubscriptionR\\rsubscriptions\\x1a.\\n\" +\n\t\"\\tPublisher\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x01 \\x01(\\tR\\vserviceName\\x1a\\xaa\\x02\\n\" +\n\t\"\\fSubscription\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12!\\n\" +\n\t\"\\fservice_name\\x18\\x02 \\x01(\\tR\\vserviceName\\x12!\\n\" +\n\t\"\\fack_deadline\\x18\\x03 \\x01(\\x03R\\vackDeadline\\x12+\\n\" +\n\t\"\\x11message_retention\\x18\\x04 \\x01(\\x03R\\x10messageRetention\\x12Q\\n\" +\n\t\"\\fretry_policy\\x18\\x05 \\x01(\\v2..encore.parser.meta.v1.PubSubTopic.RetryPolicyR\\vretryPolicy\\x12,\\n\" +\n\t\"\\x0fmax_concurrency\\x18\\x06 \\x01(\\x05H\\x00R\\x0emaxConcurrency\\x88\\x01\\x01B\\x12\\n\" +\n\t\"\\x10_max_concurrency\\x1ap\\n\" +\n\t\"\\vRetryPolicy\\x12\\x1f\\n\" +\n\t\"\\vmin_backoff\\x18\\x01 \\x01(\\x03R\\n\" +\n\t\"minBackoff\\x12\\x1f\\n\" +\n\t\"\\vmax_backoff\\x18\\x02 \\x01(\\x03R\\n\" +\n\t\"maxBackoff\\x12\\x1f\\n\" +\n\t\"\\vmax_retries\\x18\\x03 \\x01(\\x03R\\n\" +\n\t\"maxRetries\\\"8\\n\" +\n\t\"\\x11DeliveryGuarantee\\x12\\x11\\n\" +\n\t\"\\rAT_LEAST_ONCE\\x10\\x00\\x12\\x10\\n\" +\n\t\"\\fEXACTLY_ONCE\\x10\\x01B\\x06\\n\" +\n\t\"\\x04_doc\\\"\\x9a\\x03\\n\" +\n\t\"\\fCacheCluster\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x02 \\x01(\\tR\\x03doc\\x12J\\n\" +\n\t\"\\tkeyspaces\\x18\\x03 \\x03(\\v2,.encore.parser.meta.v1.CacheCluster.KeyspaceR\\tkeyspaces\\x12'\\n\" +\n\t\"\\x0feviction_policy\\x18\\x04 \\x01(\\tR\\x0eevictionPolicy\\x1a\\xee\\x01\\n\" +\n\t\"\\bKeyspace\\x128\\n\" +\n\t\"\\bkey_type\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\akeyType\\x12<\\n\" +\n\t\"\\n\" +\n\t\"value_type\\x18\\x02 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\tvalueType\\x12\\x18\\n\" +\n\t\"\\aservice\\x18\\x03 \\x01(\\tR\\aservice\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x04 \\x01(\\tR\\x03doc\\x12>\\n\" +\n\t\"\\fpath_pattern\\x18\\x05 \\x01(\\v2\\x1b.encore.parser.meta.v1.PathR\\vpathPattern\\\"\\xbb\\x03\\n\" +\n\t\"\\x06Metric\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12?\\n\" +\n\t\"\\n\" +\n\t\"value_type\\x18\\x02 \\x01(\\x0e2 .encore.parser.schema.v1.BuiltinR\\tvalueType\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x03 \\x01(\\tR\\x03doc\\x12<\\n\" +\n\t\"\\x04kind\\x18\\x04 \\x01(\\x0e2(.encore.parser.meta.v1.Metric.MetricKindR\\x04kind\\x12&\\n\" +\n\t\"\\fservice_name\\x18\\x05 \\x01(\\tH\\x00R\\vserviceName\\x88\\x01\\x01\\x12;\\n\" +\n\t\"\\x06labels\\x18\\x06 \\x03(\\v2#.encore.parser.meta.v1.Metric.LabelR\\x06labels\\x1aa\\n\" +\n\t\"\\x05Label\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x124\\n\" +\n\t\"\\x04type\\x18\\x02 \\x01(\\x0e2 .encore.parser.schema.v1.BuiltinR\\x04type\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x03 \\x01(\\tR\\x03doc\\\"3\\n\" +\n\t\"\\n\" +\n\t\"MetricKind\\x12\\v\\n\" +\n\t\"\\aCOUNTER\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05GAUGE\\x10\\x01\\x12\\r\\n\" +\n\t\"\\tHISTOGRAM\\x10\\x02B\\x0f\\n\" +\n\t\"\\r_service_name*\\x1e\\n\" +\n\t\"\\x04Lang\\x12\\x06\\n\" +\n\t\"\\x02GO\\x10\\x00\\x12\\x0e\\n\" +\n\t\"\\n\" +\n\t\"TYPESCRIPT\\x10\\x01B&Z$encr.dev/proto/encore/parser/meta/v1b\\x06proto3\"\n\nvar (\n\tfile_encore_parser_meta_v1_meta_proto_rawDescOnce sync.Once\n\tfile_encore_parser_meta_v1_meta_proto_rawDescData []byte\n)\n\nfunc file_encore_parser_meta_v1_meta_proto_rawDescGZIP() []byte {\n\tfile_encore_parser_meta_v1_meta_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_parser_meta_v1_meta_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_parser_meta_v1_meta_proto_rawDesc), len(file_encore_parser_meta_v1_meta_proto_rawDesc)))\n\t})\n\treturn file_encore_parser_meta_v1_meta_proto_rawDescData\n}\n\nvar file_encore_parser_meta_v1_meta_proto_enumTypes = make([]protoimpl.EnumInfo, 11)\nvar file_encore_parser_meta_v1_meta_proto_msgTypes = make([]protoimpl.MessageInfo, 41)\nvar file_encore_parser_meta_v1_meta_proto_goTypes = []any{\n\t(Lang)(0),                             // 0: encore.parser.meta.v1.Lang\n\t(BucketUsage_Operation)(0),            // 1: encore.parser.meta.v1.BucketUsage.Operation\n\t(Selector_Type)(0),                    // 2: encore.parser.meta.v1.Selector.Type\n\t(RPC_AccessType)(0),                   // 3: encore.parser.meta.v1.RPC.AccessType\n\t(RPC_Protocol)(0),                     // 4: encore.parser.meta.v1.RPC.Protocol\n\t(StaticCallNode_Package)(0),           // 5: encore.parser.meta.v1.StaticCallNode.Package\n\t(Path_Type)(0),                        // 6: encore.parser.meta.v1.Path.Type\n\t(PathSegment_SegmentType)(0),          // 7: encore.parser.meta.v1.PathSegment.SegmentType\n\t(PathSegment_ParamType)(0),            // 8: encore.parser.meta.v1.PathSegment.ParamType\n\t(PubSubTopic_DeliveryGuarantee)(0),    // 9: encore.parser.meta.v1.PubSubTopic.DeliveryGuarantee\n\t(Metric_MetricKind)(0),                // 10: encore.parser.meta.v1.Metric.MetricKind\n\t(*Data)(nil),                          // 11: encore.parser.meta.v1.Data\n\t(*QualifiedName)(nil),                 // 12: encore.parser.meta.v1.QualifiedName\n\t(*Package)(nil),                       // 13: encore.parser.meta.v1.Package\n\t(*Service)(nil),                       // 14: encore.parser.meta.v1.Service\n\t(*BucketUsage)(nil),                   // 15: encore.parser.meta.v1.BucketUsage\n\t(*Selector)(nil),                      // 16: encore.parser.meta.v1.Selector\n\t(*RPC)(nil),                           // 17: encore.parser.meta.v1.RPC\n\t(*AuthHandler)(nil),                   // 18: encore.parser.meta.v1.AuthHandler\n\t(*Middleware)(nil),                    // 19: encore.parser.meta.v1.Middleware\n\t(*TraceNode)(nil),                     // 20: encore.parser.meta.v1.TraceNode\n\t(*RPCDefNode)(nil),                    // 21: encore.parser.meta.v1.RPCDefNode\n\t(*RPCCallNode)(nil),                   // 22: encore.parser.meta.v1.RPCCallNode\n\t(*StaticCallNode)(nil),                // 23: encore.parser.meta.v1.StaticCallNode\n\t(*AuthHandlerDefNode)(nil),            // 24: encore.parser.meta.v1.AuthHandlerDefNode\n\t(*PubSubTopicDefNode)(nil),            // 25: encore.parser.meta.v1.PubSubTopicDefNode\n\t(*PubSubPublishNode)(nil),             // 26: encore.parser.meta.v1.PubSubPublishNode\n\t(*PubSubSubscriberNode)(nil),          // 27: encore.parser.meta.v1.PubSubSubscriberNode\n\t(*ServiceInitNode)(nil),               // 28: encore.parser.meta.v1.ServiceInitNode\n\t(*MiddlewareDefNode)(nil),             // 29: encore.parser.meta.v1.MiddlewareDefNode\n\t(*CacheKeyspaceDefNode)(nil),          // 30: encore.parser.meta.v1.CacheKeyspaceDefNode\n\t(*Path)(nil),                          // 31: encore.parser.meta.v1.Path\n\t(*PathSegment)(nil),                   // 32: encore.parser.meta.v1.PathSegment\n\t(*Gateway)(nil),                       // 33: encore.parser.meta.v1.Gateway\n\t(*CronJob)(nil),                       // 34: encore.parser.meta.v1.CronJob\n\t(*SQLDatabase)(nil),                   // 35: encore.parser.meta.v1.SQLDatabase\n\t(*DBMigration)(nil),                   // 36: encore.parser.meta.v1.DBMigration\n\t(*Bucket)(nil),                        // 37: encore.parser.meta.v1.Bucket\n\t(*PubSubTopic)(nil),                   // 38: encore.parser.meta.v1.PubSubTopic\n\t(*CacheCluster)(nil),                  // 39: encore.parser.meta.v1.CacheCluster\n\t(*Metric)(nil),                        // 40: encore.parser.meta.v1.Metric\n\tnil,                                   // 41: encore.parser.meta.v1.RPC.ExposeEntry\n\t(*RPC_ExposeOptions)(nil),             // 42: encore.parser.meta.v1.RPC.ExposeOptions\n\t(*RPC_StaticAssets)(nil),              // 43: encore.parser.meta.v1.RPC.StaticAssets\n\t(*RPC_StaticAssets_HeaderValues)(nil), // 44: encore.parser.meta.v1.RPC.StaticAssets.HeaderValues\n\tnil,                                   // 45: encore.parser.meta.v1.RPC.StaticAssets.HeadersEntry\n\t(*Gateway_Explicit)(nil),              // 46: encore.parser.meta.v1.Gateway.Explicit\n\t(*PubSubTopic_Publisher)(nil),         // 47: encore.parser.meta.v1.PubSubTopic.Publisher\n\t(*PubSubTopic_Subscription)(nil),      // 48: encore.parser.meta.v1.PubSubTopic.Subscription\n\t(*PubSubTopic_RetryPolicy)(nil),       // 49: encore.parser.meta.v1.PubSubTopic.RetryPolicy\n\t(*CacheCluster_Keyspace)(nil),         // 50: encore.parser.meta.v1.CacheCluster.Keyspace\n\t(*Metric_Label)(nil),                  // 51: encore.parser.meta.v1.Metric.Label\n\t(*v1.Decl)(nil),                       // 52: encore.parser.schema.v1.Decl\n\t(*v1.Type)(nil),                       // 53: encore.parser.schema.v1.Type\n\t(*v1.Loc)(nil),                        // 54: encore.parser.schema.v1.Loc\n\t(*v1.ValidationExpr)(nil),             // 55: encore.parser.schema.v1.ValidationExpr\n\t(v1.Builtin)(0),                       // 56: encore.parser.schema.v1.Builtin\n}\nvar file_encore_parser_meta_v1_meta_proto_depIdxs = []int32{\n\t52, // 0: encore.parser.meta.v1.Data.decls:type_name -> encore.parser.schema.v1.Decl\n\t13, // 1: encore.parser.meta.v1.Data.pkgs:type_name -> encore.parser.meta.v1.Package\n\t14, // 2: encore.parser.meta.v1.Data.svcs:type_name -> encore.parser.meta.v1.Service\n\t18, // 3: encore.parser.meta.v1.Data.auth_handler:type_name -> encore.parser.meta.v1.AuthHandler\n\t34, // 4: encore.parser.meta.v1.Data.cron_jobs:type_name -> encore.parser.meta.v1.CronJob\n\t38, // 5: encore.parser.meta.v1.Data.pubsub_topics:type_name -> encore.parser.meta.v1.PubSubTopic\n\t19, // 6: encore.parser.meta.v1.Data.middleware:type_name -> encore.parser.meta.v1.Middleware\n\t39, // 7: encore.parser.meta.v1.Data.cache_clusters:type_name -> encore.parser.meta.v1.CacheCluster\n\t40, // 8: encore.parser.meta.v1.Data.metrics:type_name -> encore.parser.meta.v1.Metric\n\t35, // 9: encore.parser.meta.v1.Data.sql_databases:type_name -> encore.parser.meta.v1.SQLDatabase\n\t33, // 10: encore.parser.meta.v1.Data.gateways:type_name -> encore.parser.meta.v1.Gateway\n\t0,  // 11: encore.parser.meta.v1.Data.language:type_name -> encore.parser.meta.v1.Lang\n\t37, // 12: encore.parser.meta.v1.Data.buckets:type_name -> encore.parser.meta.v1.Bucket\n\t12, // 13: encore.parser.meta.v1.Package.rpc_calls:type_name -> encore.parser.meta.v1.QualifiedName\n\t20, // 14: encore.parser.meta.v1.Package.trace_nodes:type_name -> encore.parser.meta.v1.TraceNode\n\t17, // 15: encore.parser.meta.v1.Service.rpcs:type_name -> encore.parser.meta.v1.RPC\n\t36, // 16: encore.parser.meta.v1.Service.migrations:type_name -> encore.parser.meta.v1.DBMigration\n\t15, // 17: encore.parser.meta.v1.Service.buckets:type_name -> encore.parser.meta.v1.BucketUsage\n\t1,  // 18: encore.parser.meta.v1.BucketUsage.operations:type_name -> encore.parser.meta.v1.BucketUsage.Operation\n\t2,  // 19: encore.parser.meta.v1.Selector.type:type_name -> encore.parser.meta.v1.Selector.Type\n\t3,  // 20: encore.parser.meta.v1.RPC.access_type:type_name -> encore.parser.meta.v1.RPC.AccessType\n\t53, // 21: encore.parser.meta.v1.RPC.request_schema:type_name -> encore.parser.schema.v1.Type\n\t53, // 22: encore.parser.meta.v1.RPC.response_schema:type_name -> encore.parser.schema.v1.Type\n\t4,  // 23: encore.parser.meta.v1.RPC.proto:type_name -> encore.parser.meta.v1.RPC.Protocol\n\t54, // 24: encore.parser.meta.v1.RPC.loc:type_name -> encore.parser.schema.v1.Loc\n\t31, // 25: encore.parser.meta.v1.RPC.path:type_name -> encore.parser.meta.v1.Path\n\t16, // 26: encore.parser.meta.v1.RPC.tags:type_name -> encore.parser.meta.v1.Selector\n\t41, // 27: encore.parser.meta.v1.RPC.expose:type_name -> encore.parser.meta.v1.RPC.ExposeEntry\n\t53, // 28: encore.parser.meta.v1.RPC.handshake_schema:type_name -> encore.parser.schema.v1.Type\n\t43, // 29: encore.parser.meta.v1.RPC.static_assets:type_name -> encore.parser.meta.v1.RPC.StaticAssets\n\t54, // 30: encore.parser.meta.v1.AuthHandler.loc:type_name -> encore.parser.schema.v1.Loc\n\t53, // 31: encore.parser.meta.v1.AuthHandler.auth_data:type_name -> encore.parser.schema.v1.Type\n\t53, // 32: encore.parser.meta.v1.AuthHandler.params:type_name -> encore.parser.schema.v1.Type\n\t12, // 33: encore.parser.meta.v1.Middleware.name:type_name -> encore.parser.meta.v1.QualifiedName\n\t54, // 34: encore.parser.meta.v1.Middleware.loc:type_name -> encore.parser.schema.v1.Loc\n\t16, // 35: encore.parser.meta.v1.Middleware.target:type_name -> encore.parser.meta.v1.Selector\n\t21, // 36: encore.parser.meta.v1.TraceNode.rpc_def:type_name -> encore.parser.meta.v1.RPCDefNode\n\t22, // 37: encore.parser.meta.v1.TraceNode.rpc_call:type_name -> encore.parser.meta.v1.RPCCallNode\n\t23, // 38: encore.parser.meta.v1.TraceNode.static_call:type_name -> encore.parser.meta.v1.StaticCallNode\n\t24, // 39: encore.parser.meta.v1.TraceNode.auth_handler_def:type_name -> encore.parser.meta.v1.AuthHandlerDefNode\n\t25, // 40: encore.parser.meta.v1.TraceNode.pubsub_topic_def:type_name -> encore.parser.meta.v1.PubSubTopicDefNode\n\t26, // 41: encore.parser.meta.v1.TraceNode.pubsub_publish:type_name -> encore.parser.meta.v1.PubSubPublishNode\n\t27, // 42: encore.parser.meta.v1.TraceNode.pubsub_subscriber:type_name -> encore.parser.meta.v1.PubSubSubscriberNode\n\t28, // 43: encore.parser.meta.v1.TraceNode.service_init:type_name -> encore.parser.meta.v1.ServiceInitNode\n\t29, // 44: encore.parser.meta.v1.TraceNode.middleware_def:type_name -> encore.parser.meta.v1.MiddlewareDefNode\n\t30, // 45: encore.parser.meta.v1.TraceNode.cache_keyspace:type_name -> encore.parser.meta.v1.CacheKeyspaceDefNode\n\t5,  // 46: encore.parser.meta.v1.StaticCallNode.package:type_name -> encore.parser.meta.v1.StaticCallNode.Package\n\t16, // 47: encore.parser.meta.v1.MiddlewareDefNode.target:type_name -> encore.parser.meta.v1.Selector\n\t32, // 48: encore.parser.meta.v1.Path.segments:type_name -> encore.parser.meta.v1.PathSegment\n\t6,  // 49: encore.parser.meta.v1.Path.type:type_name -> encore.parser.meta.v1.Path.Type\n\t7,  // 50: encore.parser.meta.v1.PathSegment.type:type_name -> encore.parser.meta.v1.PathSegment.SegmentType\n\t8,  // 51: encore.parser.meta.v1.PathSegment.value_type:type_name -> encore.parser.meta.v1.PathSegment.ParamType\n\t55, // 52: encore.parser.meta.v1.PathSegment.validation:type_name -> encore.parser.schema.v1.ValidationExpr\n\t46, // 53: encore.parser.meta.v1.Gateway.explicit:type_name -> encore.parser.meta.v1.Gateway.Explicit\n\t12, // 54: encore.parser.meta.v1.CronJob.endpoint:type_name -> encore.parser.meta.v1.QualifiedName\n\t36, // 55: encore.parser.meta.v1.SQLDatabase.migrations:type_name -> encore.parser.meta.v1.DBMigration\n\t53, // 56: encore.parser.meta.v1.PubSubTopic.message_type:type_name -> encore.parser.schema.v1.Type\n\t9,  // 57: encore.parser.meta.v1.PubSubTopic.delivery_guarantee:type_name -> encore.parser.meta.v1.PubSubTopic.DeliveryGuarantee\n\t47, // 58: encore.parser.meta.v1.PubSubTopic.publishers:type_name -> encore.parser.meta.v1.PubSubTopic.Publisher\n\t48, // 59: encore.parser.meta.v1.PubSubTopic.subscriptions:type_name -> encore.parser.meta.v1.PubSubTopic.Subscription\n\t50, // 60: encore.parser.meta.v1.CacheCluster.keyspaces:type_name -> encore.parser.meta.v1.CacheCluster.Keyspace\n\t56, // 61: encore.parser.meta.v1.Metric.value_type:type_name -> encore.parser.schema.v1.Builtin\n\t10, // 62: encore.parser.meta.v1.Metric.kind:type_name -> encore.parser.meta.v1.Metric.MetricKind\n\t51, // 63: encore.parser.meta.v1.Metric.labels:type_name -> encore.parser.meta.v1.Metric.Label\n\t42, // 64: encore.parser.meta.v1.RPC.ExposeEntry.value:type_name -> encore.parser.meta.v1.RPC.ExposeOptions\n\t45, // 65: encore.parser.meta.v1.RPC.StaticAssets.headers:type_name -> encore.parser.meta.v1.RPC.StaticAssets.HeadersEntry\n\t44, // 66: encore.parser.meta.v1.RPC.StaticAssets.HeadersEntry.value:type_name -> encore.parser.meta.v1.RPC.StaticAssets.HeaderValues\n\t18, // 67: encore.parser.meta.v1.Gateway.Explicit.auth_handler:type_name -> encore.parser.meta.v1.AuthHandler\n\t49, // 68: encore.parser.meta.v1.PubSubTopic.Subscription.retry_policy:type_name -> encore.parser.meta.v1.PubSubTopic.RetryPolicy\n\t53, // 69: encore.parser.meta.v1.CacheCluster.Keyspace.key_type:type_name -> encore.parser.schema.v1.Type\n\t53, // 70: encore.parser.meta.v1.CacheCluster.Keyspace.value_type:type_name -> encore.parser.schema.v1.Type\n\t31, // 71: encore.parser.meta.v1.CacheCluster.Keyspace.path_pattern:type_name -> encore.parser.meta.v1.Path\n\t56, // 72: encore.parser.meta.v1.Metric.Label.type:type_name -> encore.parser.schema.v1.Builtin\n\t73, // [73:73] is the sub-list for method output_type\n\t73, // [73:73] is the sub-list for method input_type\n\t73, // [73:73] is the sub-list for extension type_name\n\t73, // [73:73] is the sub-list for extension extendee\n\t0,  // [0:73] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_parser_meta_v1_meta_proto_init() }\nfunc file_encore_parser_meta_v1_meta_proto_init() {\n\tif File_encore_parser_meta_v1_meta_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[0].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[6].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[7].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[8].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[9].OneofWrappers = []any{\n\t\t(*TraceNode_RpcDef)(nil),\n\t\t(*TraceNode_RpcCall)(nil),\n\t\t(*TraceNode_StaticCall)(nil),\n\t\t(*TraceNode_AuthHandlerDef)(nil),\n\t\t(*TraceNode_PubsubTopicDef)(nil),\n\t\t(*TraceNode_PubsubPublish)(nil),\n\t\t(*TraceNode_PubsubSubscriber)(nil),\n\t\t(*TraceNode_ServiceInit)(nil),\n\t\t(*TraceNode_MiddlewareDef)(nil),\n\t\t(*TraceNode_CacheKeyspace)(nil),\n\t}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[21].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[22].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[23].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[24].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[26].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[27].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[29].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[32].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[35].OneofWrappers = []any{}\n\tfile_encore_parser_meta_v1_meta_proto_msgTypes[37].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_parser_meta_v1_meta_proto_rawDesc), len(file_encore_parser_meta_v1_meta_proto_rawDesc)),\n\t\t\tNumEnums:      11,\n\t\t\tNumMessages:   41,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_parser_meta_v1_meta_proto_goTypes,\n\t\tDependencyIndexes: file_encore_parser_meta_v1_meta_proto_depIdxs,\n\t\tEnumInfos:         file_encore_parser_meta_v1_meta_proto_enumTypes,\n\t\tMessageInfos:      file_encore_parser_meta_v1_meta_proto_msgTypes,\n\t}.Build()\n\tFile_encore_parser_meta_v1_meta_proto = out.File\n\tfile_encore_parser_meta_v1_meta_proto_goTypes = nil\n\tfile_encore_parser_meta_v1_meta_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/parser/meta/v1/meta.pb.ts",
    "content": "/* eslint-disable */\nimport type {\n  Loc,\n  Type,\n  Builtin,\n  Decl,\n} from \"../../../../encore/parser/schema/v1/schema.pb\";\n\nexport const protobufPackage = \"encore.parser.meta.v1\";\n\n/** Data is the metadata associated with an app version. */\nexport interface Data {\n  /** app module path */\n  module_path: string;\n  /** app revision (always the VCS revision reference) */\n  app_revision: string;\n  /** true if there where changes made on-top of the VCS revision */\n  uncommitted_changes: boolean;\n  decls: Decl[];\n  pkgs: Package[];\n  svcs: Service[];\n  /** the auth handler or nil */\n  auth_handler?: AuthHandler | undefined;\n  cron_jobs: CronJob[];\n  /** All the pub sub topics declared in the application */\n  pubsub_topics: PubSubTopic[];\n  middleware: Middleware[];\n  cache_clusters: CacheCluster[];\n  experiments: string[];\n  metrics: Metric[];\n  sql_databases: SQLDatabase[];\n}\n\n/**\n * QualifiedName is a name of an object in a specific package.\n * It is never an unqualified name, even in circumstances\n * where a package may refer to its own objects.\n */\nexport interface QualifiedName {\n  /** \"rel/path/to/pkg\" */\n  pkg: string;\n  /** ObjectName */\n  name: string;\n}\n\nexport interface Package {\n  /** import path relative to app root (\".\" for the app root itself) */\n  rel_path: string;\n  /** package name as declared in Go files */\n  name: string;\n  /** associated documentation */\n  doc: string;\n  /** service name this package is a part of, if any */\n  service_name: string;\n  /** secrets required by this package */\n  secrets: string[];\n  /** RPCs called by the package */\n  rpc_calls: QualifiedName[];\n  trace_nodes: TraceNode[];\n}\n\nexport interface Service {\n  name: string;\n  /** import path relative to app root for the root package in the service */\n  rel_path: string;\n  rpcs: RPC[];\n  migrations: DBMigration[];\n  /** databases this service connects to */\n  databases: string[];\n  /** true if the service has uses config */\n  has_config: boolean;\n}\n\nexport interface Selector {\n  type: Selector_Type;\n  value: string;\n}\n\nexport enum Selector_Type {\n  UNKNOWN = \"UNKNOWN\",\n  ALL = \"ALL\",\n  /** TAG - NOTE: If more types are added, update the (selector.Selector).ToProto method. */\n  TAG = \"TAG\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface RPC {\n  /** name of the RPC endpoint */\n  name: string;\n  /** associated documentation */\n  doc: string;\n  /** the service the RPC belongs to. */\n  service_name: string;\n  /** how can the RPC be accessed? */\n  access_type: RPC_AccessType;\n  /** request schema, or nil */\n  request_schema?: Type | undefined;\n  /** response schema, or nil */\n  response_schema?: Type | undefined;\n  proto: RPC_Protocol;\n  loc: Loc;\n  path: Path;\n  http_methods: string[];\n  tags: Selector[];\n}\n\nexport enum RPC_AccessType {\n  PRIVATE = \"PRIVATE\",\n  PUBLIC = \"PUBLIC\",\n  AUTH = \"AUTH\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport enum RPC_Protocol {\n  REGULAR = \"REGULAR\",\n  RAW = \"RAW\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface AuthHandler {\n  name: string;\n  doc: string;\n  /** package (service) import path */\n  pkg_path: string;\n  /** package (service) name */\n  pkg_name: string;\n  loc: Loc;\n  /** custom auth data, or nil */\n  auth_data?: Type | undefined;\n  /** builtin string or named type */\n  params?: Type | undefined;\n}\n\nexport interface Middleware {\n  name: QualifiedName;\n  doc: string;\n  loc: Loc;\n  global: boolean;\n  /** nil if global */\n  service_name?: string | undefined;\n  target: Selector[];\n}\n\nexport interface TraceNode {\n  id: number;\n  /** slash-separated, relative to app root */\n  filepath: string;\n  start_pos: number;\n  end_pos: number;\n  src_line_start: number;\n  src_line_end: number;\n  src_col_start: number;\n  src_col_end: number;\n  rpc_def: RPCDefNode | undefined;\n  rpc_call: RPCCallNode | undefined;\n  static_call: StaticCallNode | undefined;\n  auth_handler_def: AuthHandlerDefNode | undefined;\n  pubsub_topic_def: PubSubTopicDefNode | undefined;\n  pubsub_publish: PubSubPublishNode | undefined;\n  pubsub_subscriber: PubSubSubscriberNode | undefined;\n  service_init: ServiceInitNode | undefined;\n  middleware_def: MiddlewareDefNode | undefined;\n  cache_keyspace: CacheKeyspaceDefNode | undefined;\n}\n\nexport interface RPCDefNode {\n  service_name: string;\n  rpc_name: string;\n  context: string;\n}\n\nexport interface RPCCallNode {\n  service_name: string;\n  rpc_name: string;\n  context: string;\n}\n\nexport interface StaticCallNode {\n  package: StaticCallNode_Package;\n  func: string;\n  context: string;\n}\n\nexport enum StaticCallNode_Package {\n  UNKNOWN = \"UNKNOWN\",\n  SQLDB = \"SQLDB\",\n  RLOG = \"RLOG\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface AuthHandlerDefNode {\n  service_name: string;\n  name: string;\n  context: string;\n}\n\nexport interface PubSubTopicDefNode {\n  topic_name: string;\n  context: string;\n}\n\nexport interface PubSubPublishNode {\n  topic_name: string;\n  context: string;\n}\n\nexport interface PubSubSubscriberNode {\n  topic_name: string;\n  subscriber_name: string;\n  service_name: string;\n  context: string;\n}\n\nexport interface ServiceInitNode {\n  service_name: string;\n  setup_func_name: string;\n  context: string;\n}\n\nexport interface MiddlewareDefNode {\n  pkg_rel_path: string;\n  name: string;\n  context: string;\n  target: Selector[];\n}\n\nexport interface CacheKeyspaceDefNode {\n  pkg_rel_path: string;\n  var_name: string;\n  cluster_name: string;\n  context: string;\n}\n\nexport interface Path {\n  segments: PathSegment[];\n  type: Path_Type;\n}\n\nexport enum Path_Type {\n  URL = \"URL\",\n  CACHE_KEYSPACE = \"CACHE_KEYSPACE\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface PathSegment {\n  type: PathSegment_SegmentType;\n  value: string;\n  value_type: PathSegment_ParamType;\n}\n\nexport enum PathSegment_SegmentType {\n  LITERAL = \"LITERAL\",\n  PARAM = \"PARAM\",\n  WILDCARD = \"WILDCARD\",\n  FALLBACK = \"FALLBACK\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport enum PathSegment_ParamType {\n  STRING = \"STRING\",\n  BOOL = \"BOOL\",\n  INT8 = \"INT8\",\n  INT16 = \"INT16\",\n  INT32 = \"INT32\",\n  INT64 = \"INT64\",\n  INT = \"INT\",\n  UINT8 = \"UINT8\",\n  UINT16 = \"UINT16\",\n  UINT32 = \"UINT32\",\n  UINT64 = \"UINT64\",\n  UINT = \"UINT\",\n  UUID = \"UUID\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface CronJob {\n  id: string;\n  title: string;\n  doc: string;\n  schedule: string;\n  endpoint: QualifiedName;\n}\n\nexport interface SQLDatabase {\n  name: string;\n  doc: string;\n  /**\n   * migration_rel_path is the slash-separated path to the migrations,\n   * relative to the main module's root directory.\n   */\n  migration_rel_path: string;\n  migrations: DBMigration[];\n}\n\nexport interface DBMigration {\n  /** filename */\n  filename: string;\n  /** migration number */\n  number: number;\n  /** descriptive name */\n  description: string;\n}\n\nexport interface PubSubTopic {\n  /** The pub sub topic name (unique per application) */\n  name: string;\n  /** The documentation for the topic */\n  doc: string;\n  /** The type of the message */\n  message_type: Type;\n  /** The delivery guarantee for the topic */\n  delivery_guarantee: PubSubTopic_DeliveryGuarantee;\n  /** The field used to group messages; if empty, the topic is not ordered */\n  ordering_key: string;\n  /** The publishers for this topic */\n  publishers: PubSubTopic_Publisher[];\n  /** The subscriptions to the topic */\n  subscriptions: PubSubTopic_Subscription[];\n}\n\nexport enum PubSubTopic_DeliveryGuarantee {\n  /** AT_LEAST_ONCE - All messages will be delivered to each subscription at least once */\n  AT_LEAST_ONCE = \"AT_LEAST_ONCE\",\n  /** EXACTLY_ONCE - All messages will be delivered to each subscription exactly once */\n  EXACTLY_ONCE = \"EXACTLY_ONCE\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface PubSubTopic_Publisher {\n  /** The service the publisher is in */\n  service_name: string;\n}\n\nexport interface PubSubTopic_Subscription {\n  /** The unique name of the subscription for this topic */\n  name: string;\n  /** The service that the subscriber is in */\n  service_name: string;\n  /** How long has a consumer got to process and ack a message in nanoseconds */\n  ack_deadline: number;\n  /** How long is an undelivered message kept in nanoseconds */\n  message_retention: number;\n  /** The retry policy for the subscription */\n  retry_policy: PubSubTopic_RetryPolicy;\n}\n\nexport interface PubSubTopic_RetryPolicy {\n  /** min backoff in nanoseconds */\n  min_backoff: number;\n  /** max backoff in nanoseconds */\n  max_backoff: number;\n  /** max number of retries */\n  max_retries: number;\n}\n\nexport interface CacheCluster {\n  /** The pub sub topic name (unique per application) */\n  name: string;\n  /** The documentation for the topic */\n  doc: string;\n  /** The publishers for this topic */\n  keyspaces: CacheCluster_Keyspace[];\n  /** redis eviction policy */\n  eviction_policy: string;\n}\n\nexport interface CacheCluster_Keyspace {\n  key_type: Type;\n  value_type: Type;\n  service: string;\n  doc: string;\n  path_pattern: Path;\n}\n\nexport interface Metric {\n  /** the name of the metric */\n  name: string;\n  value_type: Builtin;\n  /** the doc string */\n  doc: string;\n  kind: Metric_MetricKind;\n  /** the service the metric is exclusive to, if any. */\n  service_name?: string | undefined;\n  labels: Metric_Label[];\n}\n\nexport enum Metric_MetricKind {\n  COUNTER = \"COUNTER\",\n  GAUGE = \"GAUGE\",\n  HISTOGRAM = \"HISTOGRAM\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\nexport interface Metric_Label {\n  key: string;\n  type: Builtin;\n  doc: string;\n}\n"
  },
  {
    "path": "proto/encore/parser/meta/v1/meta.proto",
    "content": "syntax = \"proto3\";\n\npackage encore.parser.meta.v1;\n\nimport \"encore/parser/schema/v1/schema.proto\";\n\noption go_package = \"encr.dev/proto/encore/parser/meta/v1\";\n\n// Data is the metadata associated with an app version.\nmessage Data {\n  string module_path = 1; // app module path\n  string app_revision = 2; // app revision (always the VCS revision reference)\n  bool uncommitted_changes = 8; // true if there where changes made on-top of the VCS revision\n  repeated schema.v1.Decl decls = 3;\n  repeated Package pkgs = 4;\n  repeated Service svcs = 5;\n  optional AuthHandler auth_handler = 6; // the auth handler or nil\n  repeated CronJob cron_jobs = 7;\n  repeated PubSubTopic pubsub_topics = 9; // All the pub sub topics declared in the application\n  repeated Middleware middleware = 10;\n  repeated CacheCluster cache_clusters = 11;\n  repeated string experiments = 12;\n  repeated Metric metrics = 13;\n  repeated SQLDatabase sql_databases = 14;\n  repeated Gateway gateways = 15;\n  Lang language = 16;\n  repeated Bucket buckets = 17;\n}\n\n// Lang describes the language an application is written in.\n// Defaults to Go if not set.\nenum Lang {\n  GO = 0;\n  TYPESCRIPT = 1;\n}\n\n// QualifiedName is a name of an object in a specific package.\n// It is never an unqualified name, even in circumstances\n// where a package may refer to its own objects.\nmessage QualifiedName {\n  string pkg = 1; // \"rel/path/to/pkg\"\n  string name = 2; // ObjectName\n}\n\nmessage Package {\n  string rel_path = 1; // import path relative to app root (\".\" for the app root itself)\n  string name = 2; // package name as declared in Go files\n  string doc = 3; // associated documentation\n  string service_name = 4; // service name this package is a part of, if any\n  repeated string secrets = 5; // secrets required by this package\n  repeated QualifiedName rpc_calls = 6; // RPCs called by the package\n  repeated TraceNode trace_nodes = 7;\n}\n\nmessage Service {\n  string name = 1;\n  string rel_path = 2; // import path relative to app root for the root package in the service\n  repeated RPC rpcs = 3;\n  repeated DBMigration migrations = 4;\n  repeated string databases = 5; // databases this service connects to\n  bool has_config = 6; // true if the service has uses config\n  repeated BucketUsage buckets = 7; // buckets this service uses\n  repeated string metrics = 8; // metrics this service uses\n}\n\nmessage BucketUsage {\n  // The encore name of the bucket.\n  string bucket = 1;\n\n  // Recorded operations.\n  repeated Operation operations = 2;\n\n  enum Operation {\n    UNKNOWN = 0;\n\n    // Listing objects and accessing their metadata during listing.\n    LIST_OBJECTS = 1;\n\n    // Reading the contents of an object.\n    READ_OBJECT_CONTENTS = 2;\n\n    // Creating or updating an object, with contents and metadata.\n    WRITE_OBJECT = 3;\n\n    // Updating the metadata of an object, without reading or writing its contents.\n    UPDATE_OBJECT_METADATA = 4;\n\n    // Reading the metadata of an object, or checking for its existence.\n    GET_OBJECT_METADATA = 5;\n\n    // Deleting an object.\n    DELETE_OBJECT = 6;\n\n    // Get an bucket/object's public url.\n    GET_PUBLIC_URL = 7;\n\n    // Generating a signed URL to allow an external recipient to create or\n    // update an object.\n    SIGNED_UPLOAD_URL = 8;\n\n    // Generating a signed URL to allow an external recipient to download an object.\n    SIGNED_DOWNLOAD_URL = 9;\n  }\n}\n\nmessage Selector {\n  enum Type {\n    UNKNOWN = 0;\n    ALL = 1;\n    TAG = 2;\n    // NOTE: If more types are added, update the (selector.Selector).ToProto method.\n  }\n\n  Type type = 1;\n  string value = 2;\n}\n\nmessage RPC {\n  string name = 1; // name of the RPC endpoint\n  optional string doc = 2; // associated documentation\n  string service_name = 3; // the service the RPC belongs to.\n  AccessType access_type = 4; // how can the RPC be accessed?\n  optional schema.v1.Type request_schema = 5; // request schema, or nil\n  optional schema.v1.Type response_schema = 6; // response schema, or nil\n  Protocol proto = 7;\n  schema.v1.Loc loc = 8;\n  Path path = 9;\n  repeated string http_methods = 10;\n  repeated Selector tags = 11;\n\n  // sensitive reports whether the whole payload is sensitive.\n  // If true, none of the request/response payload will be traced.\n  bool sensitive = 12;\n\n  // Whether the endpoint can be called without auth parameters.\n  bool allow_unauthenticated = 13;\n\n  // Whether the endpoint is exposed to the public, keyed by gateway.\n  map<string, ExposeOptions> expose = 14;\n\n  // The maximum size of the request body in bytes.\n  // If not set, defaults to no limit.\n  optional uint64 body_limit = 15;\n\n  // If the endpoint is streaming\n  bool streaming_request = 16;\n  bool streaming_response = 17;\n  optional schema.v1.Type handshake_schema = 18; // handshake schema, or nil\n\n  // If the endpoint serves static assets.\n  optional StaticAssets static_assets = 19;\n\n  enum AccessType {\n    PRIVATE = 0;\n    PUBLIC = 1;\n    AUTH = 2;\n  }\n\n  enum Protocol {\n    REGULAR = 0;\n    RAW = 1;\n  }\n\n  message ExposeOptions {}\n\n  message StaticAssets {\n    // dir_rel_path is the slash-separated path to the static files directory,\n    // relative to the app root.\n    string dir_rel_path = 1;\n\n    // not_found_rel_path is the relative path to the file to serve when the requested\n    // file is not found. It is relative to the files_rel_path directory.\n    optional string not_found_rel_path = 2;\n    optional uint32 not_found_status = 3;\n\n    // Custom HTTP headers to apply to all static files served.\n    // Each header can have multiple values.\n    message HeaderValues {\n      repeated string values = 1;\n    }\n    map<string, HeaderValues> headers = 4;\n  }\n}\n\nmessage AuthHandler {\n  string name = 1;\n  string doc = 2;\n  string pkg_path = 3; // package (service) import path\n  string pkg_name = 4; // package (service) name\n  schema.v1.Loc loc = 5;\n  optional schema.v1.Type auth_data = 6; // custom auth data, or nil\n  optional schema.v1.Type params = 7; // builtin string or named type\n  string service_name = 8;\n}\n\nmessage Middleware {\n  QualifiedName name = 1;\n  string doc = 2;\n  schema.v1.Loc loc = 3;\n  bool global = 4;\n  optional string service_name = 5; // nil if global\n  repeated Selector target = 6;\n}\n\nmessage TraceNode {\n  int32 id = 1;\n  string filepath = 2; // slash-separated, relative to app root\n  int32 start_pos = 4;\n  int32 end_pos = 5;\n\n  int32 src_line_start = 6;\n  int32 src_line_end = 7;\n  int32 src_col_start = 8;\n  int32 src_col_end = 9;\n\n  oneof context {\n    RPCDefNode rpc_def = 10;\n    RPCCallNode rpc_call = 11;\n    StaticCallNode static_call = 12;\n    AuthHandlerDefNode auth_handler_def = 13;\n    PubSubTopicDefNode pubsub_topic_def = 14;\n    PubSubPublishNode pubsub_publish = 15;\n    PubSubSubscriberNode pubsub_subscriber = 16;\n    ServiceInitNode service_init = 17;\n    MiddlewareDefNode middleware_def = 18;\n    CacheKeyspaceDefNode cache_keyspace = 19;\n  }\n}\n\nmessage RPCDefNode {\n  string service_name = 1;\n  string rpc_name = 2;\n  string context = 3;\n}\n\nmessage RPCCallNode {\n  string service_name = 1;\n  string rpc_name = 2;\n  string context = 3;\n}\n\nmessage StaticCallNode {\n  enum Package {\n    UNKNOWN = 0;\n    SQLDB = 1;\n    RLOG = 2;\n  }\n  Package package = 1;\n  string func = 2;\n  string context = 3;\n}\n\nmessage AuthHandlerDefNode {\n  string service_name = 1;\n  string name = 2;\n  string context = 3;\n}\n\nmessage PubSubTopicDefNode {\n  string topic_name = 1;\n  string context = 2;\n}\n\nmessage PubSubPublishNode {\n  string topic_name = 1;\n  string context = 2;\n}\n\nmessage PubSubSubscriberNode {\n  string topic_name = 1;\n  string subscriber_name = 2;\n  string service_name = 3;\n  string context = 4;\n}\n\nmessage ServiceInitNode {\n  string service_name = 1;\n  string setup_func_name = 2;\n  string context = 3;\n}\n\nmessage MiddlewareDefNode {\n  string pkg_rel_path = 1;\n  string name = 2;\n  string context = 3;\n  repeated Selector target = 4;\n}\n\nmessage CacheKeyspaceDefNode {\n  string pkg_rel_path = 1;\n  string var_name = 2;\n  string cluster_name = 3;\n  string context = 4;\n}\n\nmessage Path {\n  enum Type {\n    URL = 0;\n    CACHE_KEYSPACE = 1;\n  }\n\n  repeated PathSegment segments = 1;\n  Type type = 2;\n}\n\nmessage PathSegment {\n  enum SegmentType {\n    LITERAL = 0;\n    PARAM = 1;\n    WILDCARD = 2;\n    FALLBACK = 3;\n  }\n\n  enum ParamType {\n    STRING = 0;\n    BOOL = 1;\n    INT8 = 2;\n    INT16 = 3;\n    INT32 = 4;\n    INT64 = 5;\n    INT = 6;\n    UINT8 = 7;\n    UINT16 = 8;\n    UINT32 = 9;\n    UINT64 = 10;\n    UINT = 11;\n    UUID = 12;\n  }\n\n  SegmentType type = 1;\n  string value = 2;\n  ParamType value_type = 3;\n\n  optional schema.v1.ValidationExpr validation = 4;\n}\n\nmessage Gateway {\n  string encore_name = 1;\n\n  // Spec is the configuration for the gateway, if it's explicitly defined.\n  optional Explicit explicit = 2;\n\n  message Explicit {\n    // The service name this gateway belongs to.\n    string service_name = 1;\n    optional AuthHandler auth_handler = 2;\n  }\n}\n\nmessage CronJob {\n  string id = 1;\n  string title = 2;\n  optional string doc = 3;\n  string schedule = 4;\n  QualifiedName endpoint = 5;\n}\n\nmessage SQLDatabase {\n  string name = 1;\n  optional string doc = 2;\n  // migration_rel_path is the slash-separated path to the migrations,\n  // relative to the main module's root directory.\n  optional string migration_rel_path = 3;\n  repeated DBMigration migrations = 4;\n  bool allow_non_sequential_migrations = 5;\n}\n\nmessage DBMigration {\n  string filename = 1; // filename\n  uint64 number = 2; // migration number\n  string description = 3; // descriptive name\n}\n\nmessage Bucket {\n  string name = 1;\n  optional string doc = 2;\n  bool versioned = 3;\n  bool public = 4;\n}\n\nmessage PubSubTopic {\n  string name = 1; // The pub sub topic name (unique per application)\n  optional string doc = 2; // The documentation for the topic\n  schema.v1.Type message_type = 3; // The type of the message\n  DeliveryGuarantee delivery_guarantee = 4; // The delivery guarantee for the topic\n  string ordering_key = 5; // The field used to group messages; if empty, the topic is not ordered\n  repeated Publisher publishers = 6; // The publishers for this topic\n  repeated Subscription subscriptions = 7; // The subscriptions to the topic\n\n  message Publisher {\n    string service_name = 1; // The service the publisher is in\n  }\n\n  message Subscription {\n    string name = 1; // The unique name of the subscription for this topic\n    string service_name = 2; // The service that the subscriber is in\n    int64 ack_deadline = 3; // How long has a consumer got to process and ack a message in nanoseconds\n    int64 message_retention = 4; // How long is an undelivered message kept in nanoseconds\n    RetryPolicy retry_policy = 5; // The retry policy for the subscription\n\n    // How many messages each instance can process concurrently.\n    // If not set, the default is provider-specific.\n    optional int32 max_concurrency = 6;\n  }\n\n  message RetryPolicy {\n    int64 min_backoff = 1; // min backoff in nanoseconds\n    int64 max_backoff = 2; // max backoff in nanoseconds\n    int64 max_retries = 3; // max number of retries\n  }\n\n  enum DeliveryGuarantee {\n    AT_LEAST_ONCE = 0; // All messages will be delivered to each subscription at least once\n    EXACTLY_ONCE = 1; // All messages will be delivered to each subscription exactly once\n  }\n}\n\nmessage CacheCluster {\n  string name = 1; // The pub sub topic name (unique per application)\n  string doc = 2; // The documentation for the topic\n  repeated Keyspace keyspaces = 3; // The publishers for this topic\n  string eviction_policy = 4; // redis eviction policy\n\n  message Keyspace {\n    schema.v1.Type key_type = 1;\n    schema.v1.Type value_type = 2;\n    string service = 3;\n    string doc = 4;\n    Path path_pattern = 5;\n  }\n}\n\nmessage Metric {\n  string name = 1; // the name of the metric\n  schema.v1.Builtin value_type = 2;\n  string doc = 3; // the doc string\n  MetricKind kind = 4;\n  optional string service_name = 5; // the service the metric is exclusive to, if any.\n  repeated Label labels = 6;\n\n  enum MetricKind {\n    COUNTER = 0;\n    GAUGE = 1;\n    HISTOGRAM = 2;\n  }\n\n  message Label {\n    string key = 1;\n    schema.v1.Builtin type = 2;\n    string doc = 3;\n  }\n}\n"
  },
  {
    "path": "proto/encore/parser/schema/v1/schema.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/parser/schema/v1/schema.proto\n\npackage v1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\n// Builtin represents a type which Encore (and Go) have inbuilt support for and so can be represented by Encore's tooling\n// directly, rather than needing to understand the full implementation details of how the type is structured.\ntype Builtin int32\n\nconst (\n\t// Inbuilt Go Types\n\tBuiltin_ANY     Builtin = 0\n\tBuiltin_BOOL    Builtin = 1\n\tBuiltin_INT8    Builtin = 2\n\tBuiltin_INT16   Builtin = 3\n\tBuiltin_INT32   Builtin = 4\n\tBuiltin_INT64   Builtin = 5\n\tBuiltin_UINT8   Builtin = 6\n\tBuiltin_UINT16  Builtin = 7\n\tBuiltin_UINT32  Builtin = 8\n\tBuiltin_UINT64  Builtin = 9\n\tBuiltin_FLOAT32 Builtin = 10\n\tBuiltin_FLOAT64 Builtin = 11\n\tBuiltin_STRING  Builtin = 12\n\tBuiltin_BYTES   Builtin = 13\n\t// Additional Encore Types\n\tBuiltin_TIME    Builtin = 14\n\tBuiltin_UUID    Builtin = 15\n\tBuiltin_JSON    Builtin = 16\n\tBuiltin_USER_ID Builtin = 17\n\tBuiltin_INT     Builtin = 18\n\tBuiltin_UINT    Builtin = 19\n\tBuiltin_DECIMAL Builtin = 20\n)\n\n// Enum value maps for Builtin.\nvar (\n\tBuiltin_name = map[int32]string{\n\t\t0:  \"ANY\",\n\t\t1:  \"BOOL\",\n\t\t2:  \"INT8\",\n\t\t3:  \"INT16\",\n\t\t4:  \"INT32\",\n\t\t5:  \"INT64\",\n\t\t6:  \"UINT8\",\n\t\t7:  \"UINT16\",\n\t\t8:  \"UINT32\",\n\t\t9:  \"UINT64\",\n\t\t10: \"FLOAT32\",\n\t\t11: \"FLOAT64\",\n\t\t12: \"STRING\",\n\t\t13: \"BYTES\",\n\t\t14: \"TIME\",\n\t\t15: \"UUID\",\n\t\t16: \"JSON\",\n\t\t17: \"USER_ID\",\n\t\t18: \"INT\",\n\t\t19: \"UINT\",\n\t\t20: \"DECIMAL\",\n\t}\n\tBuiltin_value = map[string]int32{\n\t\t\"ANY\":     0,\n\t\t\"BOOL\":    1,\n\t\t\"INT8\":    2,\n\t\t\"INT16\":   3,\n\t\t\"INT32\":   4,\n\t\t\"INT64\":   5,\n\t\t\"UINT8\":   6,\n\t\t\"UINT16\":  7,\n\t\t\"UINT32\":  8,\n\t\t\"UINT64\":  9,\n\t\t\"FLOAT32\": 10,\n\t\t\"FLOAT64\": 11,\n\t\t\"STRING\":  12,\n\t\t\"BYTES\":   13,\n\t\t\"TIME\":    14,\n\t\t\"UUID\":    15,\n\t\t\"JSON\":    16,\n\t\t\"USER_ID\": 17,\n\t\t\"INT\":     18,\n\t\t\"UINT\":    19,\n\t\t\"DECIMAL\": 20,\n\t}\n)\n\nfunc (x Builtin) Enum() *Builtin {\n\tp := new(Builtin)\n\t*p = x\n\treturn p\n}\n\nfunc (x Builtin) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Builtin) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_schema_v1_schema_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Builtin) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_schema_v1_schema_proto_enumTypes[0]\n}\n\nfunc (x Builtin) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Builtin.Descriptor instead.\nfunc (Builtin) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{0}\n}\n\ntype ValidationRule_Is int32\n\nconst (\n\tValidationRule_UNKNOWN ValidationRule_Is = 0\n\tValidationRule_EMAIL   ValidationRule_Is = 1\n\tValidationRule_URL     ValidationRule_Is = 2\n)\n\n// Enum value maps for ValidationRule_Is.\nvar (\n\tValidationRule_Is_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"EMAIL\",\n\t\t2: \"URL\",\n\t}\n\tValidationRule_Is_value = map[string]int32{\n\t\t\"UNKNOWN\": 0,\n\t\t\"EMAIL\":   1,\n\t\t\"URL\":     2,\n\t}\n)\n\nfunc (x ValidationRule_Is) Enum() *ValidationRule_Is {\n\tp := new(ValidationRule_Is)\n\t*p = x\n\treturn p\n}\n\nfunc (x ValidationRule_Is) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (ValidationRule_Is) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_parser_schema_v1_schema_proto_enumTypes[1].Descriptor()\n}\n\nfunc (ValidationRule_Is) Type() protoreflect.EnumType {\n\treturn &file_encore_parser_schema_v1_schema_proto_enumTypes[1]\n}\n\nfunc (x ValidationRule_Is) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use ValidationRule_Is.Descriptor instead.\nfunc (ValidationRule_Is) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{1, 0}\n}\n\n// Type represents the base of our schema on which everything else is built on-top of. It has to be one, and only one,\n// thing from our list of meta types.\n//\n// A type may be concrete or abstract, however to determine if a type is abstract you need to recursive through the\n// structures looking for any uses of the TypeParameterPtr type\ntype Type struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Typ:\n\t//\n\t//\t*Type_Named\n\t//\t*Type_Struct\n\t//\t*Type_Map\n\t//\t*Type_List\n\t//\t*Type_Builtin\n\t//\t*Type_Pointer\n\t//\t*Type_Union\n\t//\t*Type_Literal\n\t//\t*Type_Option\n\t//\t*Type_TypeParameter\n\t//\t*Type_Config\n\tTyp           isType_Typ      `protobuf_oneof:\"typ\"`\n\tValidation    *ValidationExpr `protobuf:\"bytes,15,opt,name=validation,proto3,oneof\" json:\"validation,omitempty\"` // The validation expression for this type\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Type) Reset() {\n\t*x = Type{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Type) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Type) ProtoMessage() {}\n\nfunc (x *Type) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Type.ProtoReflect.Descriptor instead.\nfunc (*Type) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Type) GetTyp() isType_Typ {\n\tif x != nil {\n\t\treturn x.Typ\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetNamed() *Named {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Named); ok {\n\t\t\treturn x.Named\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetStruct() *Struct {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Struct); ok {\n\t\t\treturn x.Struct\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetMap() *Map {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Map); ok {\n\t\t\treturn x.Map\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetList() *List {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_List); ok {\n\t\t\treturn x.List\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetBuiltin() Builtin {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Builtin); ok {\n\t\t\treturn x.Builtin\n\t\t}\n\t}\n\treturn Builtin_ANY\n}\n\nfunc (x *Type) GetPointer() *Pointer {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Pointer); ok {\n\t\t\treturn x.Pointer\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetUnion() *Union {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Union); ok {\n\t\t\treturn x.Union\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetLiteral() *Literal {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Literal); ok {\n\t\t\treturn x.Literal\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetOption() *Option {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Option); ok {\n\t\t\treturn x.Option\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetTypeParameter() *TypeParameterRef {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_TypeParameter); ok {\n\t\t\treturn x.TypeParameter\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetConfig() *ConfigValue {\n\tif x != nil {\n\t\tif x, ok := x.Typ.(*Type_Config); ok {\n\t\t\treturn x.Config\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Type) GetValidation() *ValidationExpr {\n\tif x != nil {\n\t\treturn x.Validation\n\t}\n\treturn nil\n}\n\ntype isType_Typ interface {\n\tisType_Typ()\n}\n\ntype Type_Named struct {\n\t// Concrete / non-parameterized Types\n\tNamed *Named `protobuf:\"bytes,1,opt,name=named,proto3,oneof\"` // A \"named\" type (https://tip.golang.org/ref/spec#Types)\n}\n\ntype Type_Struct struct {\n\tStruct *Struct `protobuf:\"bytes,2,opt,name=struct,proto3,oneof\"` // The type is a struct definition\n}\n\ntype Type_Map struct {\n\tMap *Map `protobuf:\"bytes,3,opt,name=map,proto3,oneof\"` // The type is a map\n}\n\ntype Type_List struct {\n\tList *List `protobuf:\"bytes,4,opt,name=list,proto3,oneof\"` // The type is a slice\n}\n\ntype Type_Builtin struct {\n\tBuiltin Builtin `protobuf:\"varint,5,opt,name=builtin,proto3,enum=encore.parser.schema.v1.Builtin,oneof\"` // The type is one of the base built in types within Go\n}\n\ntype Type_Pointer struct {\n\tPointer *Pointer `protobuf:\"bytes,8,opt,name=pointer,proto3,oneof\"` // The type is a pointer\n}\n\ntype Type_Union struct {\n\tUnion *Union `protobuf:\"bytes,9,opt,name=union,proto3,oneof\"` // The type is a union\n}\n\ntype Type_Literal struct {\n\tLiteral *Literal `protobuf:\"bytes,10,opt,name=literal,proto3,oneof\"` // The type is a literal\n}\n\ntype Type_Option struct {\n\tOption *Option `protobuf:\"bytes,11,opt,name=option,proto3,oneof\"` // The type is an option type\n}\n\ntype Type_TypeParameter struct {\n\t// Abstract Types\n\tTypeParameter *TypeParameterRef `protobuf:\"bytes,6,opt,name=type_parameter,json=typeParameter,proto3,oneof\"` // This is placeholder for a unknown type within the declaration block\n}\n\ntype Type_Config struct {\n\t// Encore Special Types\n\tConfig *ConfigValue `protobuf:\"bytes,7,opt,name=config,proto3,oneof\"` // This value is a config value\n}\n\nfunc (*Type_Named) isType_Typ() {}\n\nfunc (*Type_Struct) isType_Typ() {}\n\nfunc (*Type_Map) isType_Typ() {}\n\nfunc (*Type_List) isType_Typ() {}\n\nfunc (*Type_Builtin) isType_Typ() {}\n\nfunc (*Type_Pointer) isType_Typ() {}\n\nfunc (*Type_Union) isType_Typ() {}\n\nfunc (*Type_Literal) isType_Typ() {}\n\nfunc (*Type_Option) isType_Typ() {}\n\nfunc (*Type_TypeParameter) isType_Typ() {}\n\nfunc (*Type_Config) isType_Typ() {}\n\ntype ValidationRule struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Rule:\n\t//\n\t//\t*ValidationRule_MinLen\n\t//\t*ValidationRule_MaxLen\n\t//\t*ValidationRule_MinVal\n\t//\t*ValidationRule_MaxVal\n\t//\t*ValidationRule_StartsWith\n\t//\t*ValidationRule_EndsWith\n\t//\t*ValidationRule_MatchesRegexp\n\t//\t*ValidationRule_Is_\n\tRule          isValidationRule_Rule `protobuf_oneof:\"rule\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ValidationRule) Reset() {\n\t*x = ValidationRule{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ValidationRule) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ValidationRule) ProtoMessage() {}\n\nfunc (x *ValidationRule) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ValidationRule.ProtoReflect.Descriptor instead.\nfunc (*ValidationRule) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *ValidationRule) GetRule() isValidationRule_Rule {\n\tif x != nil {\n\t\treturn x.Rule\n\t}\n\treturn nil\n}\n\nfunc (x *ValidationRule) GetMinLen() uint64 {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_MinLen); ok {\n\t\t\treturn x.MinLen\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *ValidationRule) GetMaxLen() uint64 {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_MaxLen); ok {\n\t\t\treturn x.MaxLen\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *ValidationRule) GetMinVal() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_MinVal); ok {\n\t\t\treturn x.MinVal\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *ValidationRule) GetMaxVal() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_MaxVal); ok {\n\t\t\treturn x.MaxVal\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *ValidationRule) GetStartsWith() string {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_StartsWith); ok {\n\t\t\treturn x.StartsWith\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *ValidationRule) GetEndsWith() string {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_EndsWith); ok {\n\t\t\treturn x.EndsWith\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *ValidationRule) GetMatchesRegexp() string {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_MatchesRegexp); ok {\n\t\t\treturn x.MatchesRegexp\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *ValidationRule) GetIs() ValidationRule_Is {\n\tif x != nil {\n\t\tif x, ok := x.Rule.(*ValidationRule_Is_); ok {\n\t\t\treturn x.Is\n\t\t}\n\t}\n\treturn ValidationRule_UNKNOWN\n}\n\ntype isValidationRule_Rule interface {\n\tisValidationRule_Rule()\n}\n\ntype ValidationRule_MinLen struct {\n\tMinLen uint64 `protobuf:\"varint,1,opt,name=min_len,json=minLen,proto3,oneof\"`\n}\n\ntype ValidationRule_MaxLen struct {\n\tMaxLen uint64 `protobuf:\"varint,2,opt,name=max_len,json=maxLen,proto3,oneof\"`\n}\n\ntype ValidationRule_MinVal struct {\n\tMinVal float64 `protobuf:\"fixed64,3,opt,name=min_val,json=minVal,proto3,oneof\"`\n}\n\ntype ValidationRule_MaxVal struct {\n\tMaxVal float64 `protobuf:\"fixed64,4,opt,name=max_val,json=maxVal,proto3,oneof\"`\n}\n\ntype ValidationRule_StartsWith struct {\n\tStartsWith string `protobuf:\"bytes,5,opt,name=starts_with,json=startsWith,proto3,oneof\"`\n}\n\ntype ValidationRule_EndsWith struct {\n\tEndsWith string `protobuf:\"bytes,6,opt,name=ends_with,json=endsWith,proto3,oneof\"`\n}\n\ntype ValidationRule_MatchesRegexp struct {\n\tMatchesRegexp string `protobuf:\"bytes,7,opt,name=matches_regexp,json=matchesRegexp,proto3,oneof\"`\n}\n\ntype ValidationRule_Is_ struct {\n\tIs ValidationRule_Is `protobuf:\"varint,8,opt,name=is,proto3,enum=encore.parser.schema.v1.ValidationRule_Is,oneof\"`\n}\n\nfunc (*ValidationRule_MinLen) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_MaxLen) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_MinVal) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_MaxVal) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_StartsWith) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_EndsWith) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_MatchesRegexp) isValidationRule_Rule() {}\n\nfunc (*ValidationRule_Is_) isValidationRule_Rule() {}\n\ntype ValidationExpr struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Expr:\n\t//\n\t//\t*ValidationExpr_Rule\n\t//\t*ValidationExpr_And_\n\t//\t*ValidationExpr_Or_\n\tExpr          isValidationExpr_Expr `protobuf_oneof:\"expr\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ValidationExpr) Reset() {\n\t*x = ValidationExpr{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ValidationExpr) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ValidationExpr) ProtoMessage() {}\n\nfunc (x *ValidationExpr) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ValidationExpr.ProtoReflect.Descriptor instead.\nfunc (*ValidationExpr) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ValidationExpr) GetExpr() isValidationExpr_Expr {\n\tif x != nil {\n\t\treturn x.Expr\n\t}\n\treturn nil\n}\n\nfunc (x *ValidationExpr) GetRule() *ValidationRule {\n\tif x != nil {\n\t\tif x, ok := x.Expr.(*ValidationExpr_Rule); ok {\n\t\t\treturn x.Rule\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ValidationExpr) GetAnd() *ValidationExpr_And {\n\tif x != nil {\n\t\tif x, ok := x.Expr.(*ValidationExpr_And_); ok {\n\t\t\treturn x.And\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ValidationExpr) GetOr() *ValidationExpr_Or {\n\tif x != nil {\n\t\tif x, ok := x.Expr.(*ValidationExpr_Or_); ok {\n\t\t\treturn x.Or\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isValidationExpr_Expr interface {\n\tisValidationExpr_Expr()\n}\n\ntype ValidationExpr_Rule struct {\n\tRule *ValidationRule `protobuf:\"bytes,1,opt,name=rule,proto3,oneof\"`\n}\n\ntype ValidationExpr_And_ struct {\n\tAnd *ValidationExpr_And `protobuf:\"bytes,2,opt,name=and,proto3,oneof\"`\n}\n\ntype ValidationExpr_Or_ struct {\n\tOr *ValidationExpr_Or `protobuf:\"bytes,3,opt,name=or,proto3,oneof\"`\n}\n\nfunc (*ValidationExpr_Rule) isValidationExpr_Expr() {}\n\nfunc (*ValidationExpr_And_) isValidationExpr_Expr() {}\n\nfunc (*ValidationExpr_Or_) isValidationExpr_Expr() {}\n\n// TypeParameterRef is a reference to a `TypeParameter` within a declaration block\ntype TypeParameterRef struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tDeclId        uint32                 `protobuf:\"varint,1,opt,name=decl_id,json=declId,proto3\" json:\"decl_id,omitempty\"`       // The ID of the declaration block\n\tParamIdx      uint32                 `protobuf:\"varint,2,opt,name=param_idx,json=paramIdx,proto3\" json:\"param_idx,omitempty\"` // The index of the type parameter within the declaration block\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TypeParameterRef) Reset() {\n\t*x = TypeParameterRef{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TypeParameterRef) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TypeParameterRef) ProtoMessage() {}\n\nfunc (x *TypeParameterRef) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TypeParameterRef.ProtoReflect.Descriptor instead.\nfunc (*TypeParameterRef) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *TypeParameterRef) GetDeclId() uint32 {\n\tif x != nil {\n\t\treturn x.DeclId\n\t}\n\treturn 0\n}\n\nfunc (x *TypeParameterRef) GetParamIdx() uint32 {\n\tif x != nil {\n\t\treturn x.ParamIdx\n\t}\n\treturn 0\n}\n\n// Decl represents the declaration of a type within the Go code which is either concrete or _parameterized_. The type is\n// concrete when there are zero type parameters assigned.\n//\n// For example the Go Code:\n// ```go\n// // Set[A] represents our set type\n// type Set[A any] = map[A]struct{}\n// ```\n//\n// Would become:\n// ```go\n//\n//\t_ = &Decl{\n//\t    id: 1,\n//\t    name: \"Set\",\n//\t    type: &Type{\n//\t        typ_map: &Map{\n//\t            key: &Type { typ_type_parameter: ... reference to \"A\" type parameter below ... },\n//\t            value: &Type { typ_struct: ... empty struct type ... },\n//\t        },\n//\t    },\n//\t    typeParameters: []*TypeParameter{ { name: \"A\" } },\n//\t    doc: \"Set[A] represents our set type\",\n//\t    loc: &Loc { ... },\n//\t}\n//\n// ```\ntype Decl struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            uint32                 `protobuf:\"varint,1,opt,name=id,proto3\" json:\"id,omitempty\"`                                  // A internal ID which we can refer to this declaration by\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`                               // The name of the type as assigned in the code\n\tType          *Type                  `protobuf:\"bytes,3,opt,name=type,proto3\" json:\"type,omitempty\"`                               // The underlying type of this declaration\n\tTypeParams    []*TypeParameter       `protobuf:\"bytes,6,rep,name=type_params,json=typeParams,proto3\" json:\"type_params,omitempty\"` // Any type parameters on this declaration (note; instantiated types used within this declaration would not be captured here)\n\tDoc           string                 `protobuf:\"bytes,4,opt,name=doc,proto3\" json:\"doc,omitempty\"`                                 // The comment block on the type\n\tLoc           *Loc                   `protobuf:\"bytes,5,opt,name=loc,proto3\" json:\"loc,omitempty\"`                                 // The location of the declaration within the project\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Decl) Reset() {\n\t*x = Decl{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Decl) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Decl) ProtoMessage() {}\n\nfunc (x *Decl) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Decl.ProtoReflect.Descriptor instead.\nfunc (*Decl) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *Decl) GetId() uint32 {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn 0\n}\n\nfunc (x *Decl) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Decl) GetType() *Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn nil\n}\n\nfunc (x *Decl) GetTypeParams() []*TypeParameter {\n\tif x != nil {\n\t\treturn x.TypeParams\n\t}\n\treturn nil\n}\n\nfunc (x *Decl) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *Decl) GetLoc() *Loc {\n\tif x != nil {\n\t\treturn x.Loc\n\t}\n\treturn nil\n}\n\n// TypeParameter acts as a place holder for an (as of yet) unknown type in the declaration; the type parameter is\n// replaced with a type argument upon instantiation of the parameterized function or type.\ntype TypeParameter struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"` // The identifier given to the type parameter\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TypeParameter) Reset() {\n\t*x = TypeParameter{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TypeParameter) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TypeParameter) ProtoMessage() {}\n\nfunc (x *TypeParameter) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TypeParameter.ProtoReflect.Descriptor instead.\nfunc (*TypeParameter) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *TypeParameter) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\n// Loc is the location of a declaration within the code base\ntype Loc struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPkgPath       string                 `protobuf:\"bytes,1,opt,name=pkg_path,json=pkgPath,proto3\" json:\"pkg_path,omitempty\"`                   // The package path within the repo (i.e. `users/signup`)\n\tPkgName       string                 `protobuf:\"bytes,2,opt,name=pkg_name,json=pkgName,proto3\" json:\"pkg_name,omitempty\"`                   // The package name (i.e. `signup`)\n\tFilename      string                 `protobuf:\"bytes,3,opt,name=filename,proto3\" json:\"filename,omitempty\"`                                // The file name (i.e. `signup.go`)\n\tStartPos      int32                  `protobuf:\"varint,4,opt,name=start_pos,json=startPos,proto3\" json:\"start_pos,omitempty\"`               // The starting index within the file for this node\n\tEndPos        int32                  `protobuf:\"varint,5,opt,name=end_pos,json=endPos,proto3\" json:\"end_pos,omitempty\"`                     // The ending index within the file for this node\n\tSrcLineStart  int32                  `protobuf:\"varint,6,opt,name=src_line_start,json=srcLineStart,proto3\" json:\"src_line_start,omitempty\"` // The starting line within the file for this node\n\tSrcLineEnd    int32                  `protobuf:\"varint,7,opt,name=src_line_end,json=srcLineEnd,proto3\" json:\"src_line_end,omitempty\"`       // The ending line within the file for this node\n\tSrcColStart   int32                  `protobuf:\"varint,8,opt,name=src_col_start,json=srcColStart,proto3\" json:\"src_col_start,omitempty\"`    // The starting column on the starting line for this node\n\tSrcColEnd     int32                  `protobuf:\"varint,9,opt,name=src_col_end,json=srcColEnd,proto3\" json:\"src_col_end,omitempty\"`          // The ending column on the ending line for this node\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Loc) Reset() {\n\t*x = Loc{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Loc) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Loc) ProtoMessage() {}\n\nfunc (x *Loc) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Loc.ProtoReflect.Descriptor instead.\nfunc (*Loc) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *Loc) GetPkgPath() string {\n\tif x != nil {\n\t\treturn x.PkgPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *Loc) GetPkgName() string {\n\tif x != nil {\n\t\treturn x.PkgName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Loc) GetFilename() string {\n\tif x != nil {\n\t\treturn x.Filename\n\t}\n\treturn \"\"\n}\n\nfunc (x *Loc) GetStartPos() int32 {\n\tif x != nil {\n\t\treturn x.StartPos\n\t}\n\treturn 0\n}\n\nfunc (x *Loc) GetEndPos() int32 {\n\tif x != nil {\n\t\treturn x.EndPos\n\t}\n\treturn 0\n}\n\nfunc (x *Loc) GetSrcLineStart() int32 {\n\tif x != nil {\n\t\treturn x.SrcLineStart\n\t}\n\treturn 0\n}\n\nfunc (x *Loc) GetSrcLineEnd() int32 {\n\tif x != nil {\n\t\treturn x.SrcLineEnd\n\t}\n\treturn 0\n}\n\nfunc (x *Loc) GetSrcColStart() int32 {\n\tif x != nil {\n\t\treturn x.SrcColStart\n\t}\n\treturn 0\n}\n\nfunc (x *Loc) GetSrcColEnd() int32 {\n\tif x != nil {\n\t\treturn x.SrcColEnd\n\t}\n\treturn 0\n}\n\n// Named references declaration block by name\ntype Named struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            uint32                 `protobuf:\"varint,1,opt,name=id,proto3\" json:\"id,omitempty\"`                                           // The `Decl.id` this name refers to\n\tTypeArguments []*Type                `protobuf:\"bytes,2,rep,name=type_arguments,json=typeArguments,proto3\" json:\"type_arguments,omitempty\"` // The type arguments used to instantiate this parameterised declaration\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Named) Reset() {\n\t*x = Named{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Named) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Named) ProtoMessage() {}\n\nfunc (x *Named) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Named.ProtoReflect.Descriptor instead.\nfunc (*Named) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *Named) GetId() uint32 {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn 0\n}\n\nfunc (x *Named) GetTypeArguments() []*Type {\n\tif x != nil {\n\t\treturn x.TypeArguments\n\t}\n\treturn nil\n}\n\n// Struct contains a list of fields which make up the struct\ntype Struct struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tFields        []*Field               `protobuf:\"bytes,1,rep,name=fields,proto3\" json:\"fields,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Struct) Reset() {\n\t*x = Struct{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Struct) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Struct) ProtoMessage() {}\n\nfunc (x *Struct) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Struct.ProtoReflect.Descriptor instead.\nfunc (*Struct) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *Struct) GetFields() []*Field {\n\tif x != nil {\n\t\treturn x.Fields\n\t}\n\treturn nil\n}\n\n// Field represents a field within a struct\ntype Field struct {\n\tstate           protoimpl.MessageState `protogen:\"open.v1\"`\n\tTyp             *Type                  `protobuf:\"bytes,1,opt,name=typ,proto3\" json:\"typ,omitempty\"`                                                  // The type of the field\n\tName            string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`                                                // The name of the field\n\tDoc             string                 `protobuf:\"bytes,3,opt,name=doc,proto3\" json:\"doc,omitempty\"`                                                  // The comment for the field\n\tJsonName        string                 `protobuf:\"bytes,4,opt,name=json_name,json=jsonName,proto3\" json:\"json_name,omitempty\"`                        // The optional json name if it's different from the field name. (The value \"-\" indicates to omit the field.)\n\tOptional        bool                   `protobuf:\"varint,5,opt,name=optional,proto3\" json:\"optional,omitempty\"`                                       // Whether the field is optional.\n\tQueryStringName string                 `protobuf:\"bytes,6,opt,name=query_string_name,json=queryStringName,proto3\" json:\"query_string_name,omitempty\"` // The query string name to use in GET/HEAD/DELETE requests. (The value \"-\" indicates to omit the field.)\n\tRawTag          string                 `protobuf:\"bytes,7,opt,name=raw_tag,json=rawTag,proto3\" json:\"raw_tag,omitempty\"`                              // The original Go struct tag; should not be parsed individually\n\tTags            []*Tag                 `protobuf:\"bytes,8,rep,name=tags,proto3\" json:\"tags,omitempty\"`                                                // Parsed go struct tags. Used for marshalling hints\n\tWire            *WireSpec              `protobuf:\"bytes,9,opt,name=wire,proto3,oneof\" json:\"wire,omitempty\"`                                          // The explicitly set wire location of the field.\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *Field) Reset() {\n\t*x = Field{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Field) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Field) ProtoMessage() {}\n\nfunc (x *Field) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Field.ProtoReflect.Descriptor instead.\nfunc (*Field) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *Field) GetTyp() *Type {\n\tif x != nil {\n\t\treturn x.Typ\n\t}\n\treturn nil\n}\n\nfunc (x *Field) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Field) GetDoc() string {\n\tif x != nil {\n\t\treturn x.Doc\n\t}\n\treturn \"\"\n}\n\nfunc (x *Field) GetJsonName() string {\n\tif x != nil {\n\t\treturn x.JsonName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Field) GetOptional() bool {\n\tif x != nil {\n\t\treturn x.Optional\n\t}\n\treturn false\n}\n\nfunc (x *Field) GetQueryStringName() string {\n\tif x != nil {\n\t\treturn x.QueryStringName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Field) GetRawTag() string {\n\tif x != nil {\n\t\treturn x.RawTag\n\t}\n\treturn \"\"\n}\n\nfunc (x *Field) GetTags() []*Tag {\n\tif x != nil {\n\t\treturn x.Tags\n\t}\n\treturn nil\n}\n\nfunc (x *Field) GetWire() *WireSpec {\n\tif x != nil {\n\t\treturn x.Wire\n\t}\n\treturn nil\n}\n\n// WireLocation provides information about how a field should be encoded on the wire.\ntype WireSpec struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Location:\n\t//\n\t//\t*WireSpec_Header_\n\t//\t*WireSpec_Query_\n\t//\t*WireSpec_Cookie_\n\t//\t*WireSpec_HttpStatus_\n\tLocation      isWireSpec_Location `protobuf_oneof:\"location\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WireSpec) Reset() {\n\t*x = WireSpec{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WireSpec) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WireSpec) ProtoMessage() {}\n\nfunc (x *WireSpec) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WireSpec.ProtoReflect.Descriptor instead.\nfunc (*WireSpec) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *WireSpec) GetLocation() isWireSpec_Location {\n\tif x != nil {\n\t\treturn x.Location\n\t}\n\treturn nil\n}\n\nfunc (x *WireSpec) GetHeader() *WireSpec_Header {\n\tif x != nil {\n\t\tif x, ok := x.Location.(*WireSpec_Header_); ok {\n\t\t\treturn x.Header\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *WireSpec) GetQuery() *WireSpec_Query {\n\tif x != nil {\n\t\tif x, ok := x.Location.(*WireSpec_Query_); ok {\n\t\t\treturn x.Query\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *WireSpec) GetCookie() *WireSpec_Cookie {\n\tif x != nil {\n\t\tif x, ok := x.Location.(*WireSpec_Cookie_); ok {\n\t\t\treturn x.Cookie\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *WireSpec) GetHttpStatus() *WireSpec_HttpStatus {\n\tif x != nil {\n\t\tif x, ok := x.Location.(*WireSpec_HttpStatus_); ok {\n\t\t\treturn x.HttpStatus\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isWireSpec_Location interface {\n\tisWireSpec_Location()\n}\n\ntype WireSpec_Header_ struct {\n\tHeader *WireSpec_Header `protobuf:\"bytes,1,opt,name=header,proto3,oneof\"`\n}\n\ntype WireSpec_Query_ struct {\n\tQuery *WireSpec_Query `protobuf:\"bytes,2,opt,name=query,proto3,oneof\"`\n}\n\ntype WireSpec_Cookie_ struct {\n\tCookie *WireSpec_Cookie `protobuf:\"bytes,3,opt,name=cookie,proto3,oneof\"`\n}\n\ntype WireSpec_HttpStatus_ struct {\n\tHttpStatus *WireSpec_HttpStatus `protobuf:\"bytes,4,opt,name=http_status,json=httpStatus,proto3,oneof\"`\n}\n\nfunc (*WireSpec_Header_) isWireSpec_Location() {}\n\nfunc (*WireSpec_Query_) isWireSpec_Location() {}\n\nfunc (*WireSpec_Cookie_) isWireSpec_Location() {}\n\nfunc (*WireSpec_HttpStatus_) isWireSpec_Location() {}\n\ntype Tag struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           string                 `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`         // The tag key (e.g. json, query, header ...)\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`       // The tag name (e.g. first_name, firstName, ...)\n\tOptions       []string               `protobuf:\"bytes,3,rep,name=options,proto3\" json:\"options,omitempty\"` // Key Options (e.g. omitempty, optional ...)\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Tag) Reset() {\n\t*x = Tag{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Tag) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Tag) ProtoMessage() {}\n\nfunc (x *Tag) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Tag.ProtoReflect.Descriptor instead.\nfunc (*Tag) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *Tag) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *Tag) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Tag) GetOptions() []string {\n\tif x != nil {\n\t\treturn x.Options\n\t}\n\treturn nil\n}\n\n// Map represents a map Type\ntype Map struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tKey           *Type                  `protobuf:\"bytes,1,opt,name=key,proto3\" json:\"key,omitempty\"`     // The type of the key for this map\n\tValue         *Type                  `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"` // The type of the value of this map\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Map) Reset() {\n\t*x = Map{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Map) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Map) ProtoMessage() {}\n\nfunc (x *Map) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Map.ProtoReflect.Descriptor instead.\nfunc (*Map) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *Map) GetKey() *Type {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn nil\n}\n\nfunc (x *Map) GetValue() *Type {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\n// List represents a list type (array or slice)\ntype List struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tElem          *Type                  `protobuf:\"bytes,1,opt,name=elem,proto3\" json:\"elem,omitempty\"` // The type of the elements in the list\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *List) Reset() {\n\t*x = List{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *List) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*List) ProtoMessage() {}\n\nfunc (x *List) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use List.ProtoReflect.Descriptor instead.\nfunc (*List) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *List) GetElem() *Type {\n\tif x != nil {\n\t\treturn x.Elem\n\t}\n\treturn nil\n}\n\n// Pointer represents a pointer to a base type\ntype Pointer struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tBase          *Type                  `protobuf:\"bytes,1,opt,name=base,proto3\" json:\"base,omitempty\"` // The type of the pointer\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Pointer) Reset() {\n\t*x = Pointer{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Pointer) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Pointer) ProtoMessage() {}\n\nfunc (x *Pointer) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Pointer.ProtoReflect.Descriptor instead.\nfunc (*Pointer) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *Pointer) GetBase() *Type {\n\tif x != nil {\n\t\treturn x.Base\n\t}\n\treturn nil\n}\n\n// Option represents an option type.\ntype Option struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tValue         *Type                  `protobuf:\"bytes,1,opt,name=value,proto3\" json:\"value,omitempty\"` // The value it may contain.\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Option) Reset() {\n\t*x = Option{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Option) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Option) ProtoMessage() {}\n\nfunc (x *Option) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Option.ProtoReflect.Descriptor instead.\nfunc (*Option) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *Option) GetValue() *Type {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\n// Union represents a union type.\ntype Union struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTypes         []*Type                `protobuf:\"bytes,1,rep,name=types,proto3\" json:\"types,omitempty\"` // The types that make up the union\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Union) Reset() {\n\t*x = Union{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Union) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Union) ProtoMessage() {}\n\nfunc (x *Union) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Union.ProtoReflect.Descriptor instead.\nfunc (*Union) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *Union) GetTypes() []*Type {\n\tif x != nil {\n\t\treturn x.Types\n\t}\n\treturn nil\n}\n\n// Literal represents a literal value.\ntype Literal struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Value:\n\t//\n\t//\t*Literal_Str\n\t//\t*Literal_Boolean\n\t//\t*Literal_Int\n\t//\t*Literal_Float\n\t//\t*Literal_Null\n\tValue         isLiteral_Value `protobuf_oneof:\"value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Literal) Reset() {\n\t*x = Literal{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Literal) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Literal) ProtoMessage() {}\n\nfunc (x *Literal) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Literal.ProtoReflect.Descriptor instead.\nfunc (*Literal) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *Literal) GetValue() isLiteral_Value {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn nil\n}\n\nfunc (x *Literal) GetStr() string {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*Literal_Str); ok {\n\t\t\treturn x.Str\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *Literal) GetBoolean() bool {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*Literal_Boolean); ok {\n\t\t\treturn x.Boolean\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (x *Literal) GetInt() int64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*Literal_Int); ok {\n\t\t\treturn x.Int\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *Literal) GetFloat() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*Literal_Float); ok {\n\t\t\treturn x.Float\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *Literal) GetNull() bool {\n\tif x != nil {\n\t\tif x, ok := x.Value.(*Literal_Null); ok {\n\t\t\treturn x.Null\n\t\t}\n\t}\n\treturn false\n}\n\ntype isLiteral_Value interface {\n\tisLiteral_Value()\n}\n\ntype Literal_Str struct {\n\tStr string `protobuf:\"bytes,1,opt,name=str,proto3,oneof\"`\n}\n\ntype Literal_Boolean struct {\n\tBoolean bool `protobuf:\"varint,2,opt,name=boolean,proto3,oneof\"`\n}\n\ntype Literal_Int struct {\n\tInt int64 `protobuf:\"varint,3,opt,name=int,proto3,oneof\"`\n}\n\ntype Literal_Float struct {\n\tFloat float64 `protobuf:\"fixed64,4,opt,name=float,proto3,oneof\"`\n}\n\ntype Literal_Null struct {\n\tNull bool `protobuf:\"varint,5,opt,name=null,proto3,oneof\"`\n}\n\nfunc (*Literal_Str) isLiteral_Value() {}\n\nfunc (*Literal_Boolean) isLiteral_Value() {}\n\nfunc (*Literal_Int) isLiteral_Value() {}\n\nfunc (*Literal_Float) isLiteral_Value() {}\n\nfunc (*Literal_Null) isLiteral_Value() {}\n\n// ConfigValue represents a config value wrapper.\ntype ConfigValue struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tElem          *Type                  `protobuf:\"bytes,1,opt,name=elem,proto3\" json:\"elem,omitempty\"`                  // The type of the config value\n\tIsValuesList  bool                   `protobuf:\"varint,2,opt,name=IsValuesList,proto3\" json:\"IsValuesList,omitempty\"` // Does this config value represent the type to `config.Values[T]`. If false it represents `config.Value[T]`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ConfigValue) Reset() {\n\t*x = ConfigValue{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ConfigValue) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ConfigValue) ProtoMessage() {}\n\nfunc (x *ConfigValue) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ConfigValue.ProtoReflect.Descriptor instead.\nfunc (*ConfigValue) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *ConfigValue) GetElem() *Type {\n\tif x != nil {\n\t\treturn x.Elem\n\t}\n\treturn nil\n}\n\nfunc (x *ConfigValue) GetIsValuesList() bool {\n\tif x != nil {\n\t\treturn x.IsValuesList\n\t}\n\treturn false\n}\n\ntype ValidationExpr_And struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tExprs         []*ValidationExpr      `protobuf:\"bytes,1,rep,name=exprs,proto3\" json:\"exprs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ValidationExpr_And) Reset() {\n\t*x = ValidationExpr_And{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ValidationExpr_And) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ValidationExpr_And) ProtoMessage() {}\n\nfunc (x *ValidationExpr_And) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ValidationExpr_And.ProtoReflect.Descriptor instead.\nfunc (*ValidationExpr_And) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{2, 0}\n}\n\nfunc (x *ValidationExpr_And) GetExprs() []*ValidationExpr {\n\tif x != nil {\n\t\treturn x.Exprs\n\t}\n\treturn nil\n}\n\ntype ValidationExpr_Or struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tExprs         []*ValidationExpr      `protobuf:\"bytes,1,rep,name=exprs,proto3\" json:\"exprs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ValidationExpr_Or) Reset() {\n\t*x = ValidationExpr_Or{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ValidationExpr_Or) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ValidationExpr_Or) ProtoMessage() {}\n\nfunc (x *ValidationExpr_Or) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ValidationExpr_Or.ProtoReflect.Descriptor instead.\nfunc (*ValidationExpr_Or) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{2, 1}\n}\n\nfunc (x *ValidationExpr_Or) GetExprs() []*ValidationExpr {\n\tif x != nil {\n\t\treturn x.Exprs\n\t}\n\treturn nil\n}\n\ntype WireSpec_Header struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The explicitly specified header name.\n\t// If empty, the name of the field is used.\n\tName          *string `protobuf:\"bytes,1,opt,name=name,proto3,oneof\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WireSpec_Header) Reset() {\n\t*x = WireSpec_Header{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WireSpec_Header) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WireSpec_Header) ProtoMessage() {}\n\nfunc (x *WireSpec_Header) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WireSpec_Header.ProtoReflect.Descriptor instead.\nfunc (*WireSpec_Header) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 0}\n}\n\nfunc (x *WireSpec_Header) GetName() string {\n\tif x != nil && x.Name != nil {\n\t\treturn *x.Name\n\t}\n\treturn \"\"\n}\n\ntype WireSpec_Query struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The explicitly specified query string name.\n\t// If empty, the name of the field is used.\n\tName          *string `protobuf:\"bytes,1,opt,name=name,proto3,oneof\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WireSpec_Query) Reset() {\n\t*x = WireSpec_Query{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WireSpec_Query) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WireSpec_Query) ProtoMessage() {}\n\nfunc (x *WireSpec_Query) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WireSpec_Query.ProtoReflect.Descriptor instead.\nfunc (*WireSpec_Query) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 1}\n}\n\nfunc (x *WireSpec_Query) GetName() string {\n\tif x != nil && x.Name != nil {\n\t\treturn *x.Name\n\t}\n\treturn \"\"\n}\n\ntype WireSpec_Cookie struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The explicitly specified cookie string name.\n\t// If empty, the name of the field is used.\n\tName          *string `protobuf:\"bytes,1,opt,name=name,proto3,oneof\" json:\"name,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WireSpec_Cookie) Reset() {\n\t*x = WireSpec_Cookie{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WireSpec_Cookie) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WireSpec_Cookie) ProtoMessage() {}\n\nfunc (x *WireSpec_Cookie) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WireSpec_Cookie.ProtoReflect.Descriptor instead.\nfunc (*WireSpec_Cookie) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 2}\n}\n\nfunc (x *WireSpec_Cookie) GetName() string {\n\tif x != nil && x.Name != nil {\n\t\treturn *x.Name\n\t}\n\treturn \"\"\n}\n\ntype WireSpec_HttpStatus struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WireSpec_HttpStatus) Reset() {\n\t*x = WireSpec_HttpStatus{}\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WireSpec_HttpStatus) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WireSpec_HttpStatus) ProtoMessage() {}\n\nfunc (x *WireSpec_HttpStatus) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_parser_schema_v1_schema_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WireSpec_HttpStatus.ProtoReflect.Descriptor instead.\nfunc (*WireSpec_HttpStatus) Descriptor() ([]byte, []int) {\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescGZIP(), []int{10, 3}\n}\n\nvar File_encore_parser_schema_v1_schema_proto protoreflect.FileDescriptor\n\nconst file_encore_parser_schema_v1_schema_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"$encore/parser/schema/v1/schema.proto\\x12\\x17encore.parser.schema.v1\\\"\\x85\\x06\\n\" +\n\t\"\\x04Type\\x126\\n\" +\n\t\"\\x05named\\x18\\x01 \\x01(\\v2\\x1e.encore.parser.schema.v1.NamedH\\x00R\\x05named\\x129\\n\" +\n\t\"\\x06struct\\x18\\x02 \\x01(\\v2\\x1f.encore.parser.schema.v1.StructH\\x00R\\x06struct\\x120\\n\" +\n\t\"\\x03map\\x18\\x03 \\x01(\\v2\\x1c.encore.parser.schema.v1.MapH\\x00R\\x03map\\x123\\n\" +\n\t\"\\x04list\\x18\\x04 \\x01(\\v2\\x1d.encore.parser.schema.v1.ListH\\x00R\\x04list\\x12<\\n\" +\n\t\"\\abuiltin\\x18\\x05 \\x01(\\x0e2 .encore.parser.schema.v1.BuiltinH\\x00R\\abuiltin\\x12<\\n\" +\n\t\"\\apointer\\x18\\b \\x01(\\v2 .encore.parser.schema.v1.PointerH\\x00R\\apointer\\x126\\n\" +\n\t\"\\x05union\\x18\\t \\x01(\\v2\\x1e.encore.parser.schema.v1.UnionH\\x00R\\x05union\\x12<\\n\" +\n\t\"\\aliteral\\x18\\n\" +\n\t\" \\x01(\\v2 .encore.parser.schema.v1.LiteralH\\x00R\\aliteral\\x129\\n\" +\n\t\"\\x06option\\x18\\v \\x01(\\v2\\x1f.encore.parser.schema.v1.OptionH\\x00R\\x06option\\x12R\\n\" +\n\t\"\\x0etype_parameter\\x18\\x06 \\x01(\\v2).encore.parser.schema.v1.TypeParameterRefH\\x00R\\rtypeParameter\\x12>\\n\" +\n\t\"\\x06config\\x18\\a \\x01(\\v2$.encore.parser.schema.v1.ConfigValueH\\x00R\\x06config\\x12L\\n\" +\n\t\"\\n\" +\n\t\"validation\\x18\\x0f \\x01(\\v2'.encore.parser.schema.v1.ValidationExprH\\x01R\\n\" +\n\t\"validation\\x88\\x01\\x01B\\x05\\n\" +\n\t\"\\x03typB\\r\\n\" +\n\t\"\\v_validation\\\"\\xd4\\x02\\n\" +\n\t\"\\x0eValidationRule\\x12\\x19\\n\" +\n\t\"\\amin_len\\x18\\x01 \\x01(\\x04H\\x00R\\x06minLen\\x12\\x19\\n\" +\n\t\"\\amax_len\\x18\\x02 \\x01(\\x04H\\x00R\\x06maxLen\\x12\\x19\\n\" +\n\t\"\\amin_val\\x18\\x03 \\x01(\\x01H\\x00R\\x06minVal\\x12\\x19\\n\" +\n\t\"\\amax_val\\x18\\x04 \\x01(\\x01H\\x00R\\x06maxVal\\x12!\\n\" +\n\t\"\\vstarts_with\\x18\\x05 \\x01(\\tH\\x00R\\n\" +\n\t\"startsWith\\x12\\x1d\\n\" +\n\t\"\\tends_with\\x18\\x06 \\x01(\\tH\\x00R\\bendsWith\\x12'\\n\" +\n\t\"\\x0ematches_regexp\\x18\\a \\x01(\\tH\\x00R\\rmatchesRegexp\\x12<\\n\" +\n\t\"\\x02is\\x18\\b \\x01(\\x0e2*.encore.parser.schema.v1.ValidationRule.IsH\\x00R\\x02is\\\"%\\n\" +\n\t\"\\x02Is\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\t\\n\" +\n\t\"\\x05EMAIL\\x10\\x01\\x12\\a\\n\" +\n\t\"\\x03URL\\x10\\x02B\\x06\\n\" +\n\t\"\\x04rule\\\"\\xe1\\x02\\n\" +\n\t\"\\x0eValidationExpr\\x12=\\n\" +\n\t\"\\x04rule\\x18\\x01 \\x01(\\v2'.encore.parser.schema.v1.ValidationRuleH\\x00R\\x04rule\\x12?\\n\" +\n\t\"\\x03and\\x18\\x02 \\x01(\\v2+.encore.parser.schema.v1.ValidationExpr.AndH\\x00R\\x03and\\x12<\\n\" +\n\t\"\\x02or\\x18\\x03 \\x01(\\v2*.encore.parser.schema.v1.ValidationExpr.OrH\\x00R\\x02or\\x1aD\\n\" +\n\t\"\\x03And\\x12=\\n\" +\n\t\"\\x05exprs\\x18\\x01 \\x03(\\v2'.encore.parser.schema.v1.ValidationExprR\\x05exprs\\x1aC\\n\" +\n\t\"\\x02Or\\x12=\\n\" +\n\t\"\\x05exprs\\x18\\x01 \\x03(\\v2'.encore.parser.schema.v1.ValidationExprR\\x05exprsB\\x06\\n\" +\n\t\"\\x04expr\\\"H\\n\" +\n\t\"\\x10TypeParameterRef\\x12\\x17\\n\" +\n\t\"\\adecl_id\\x18\\x01 \\x01(\\rR\\x06declId\\x12\\x1b\\n\" +\n\t\"\\tparam_idx\\x18\\x02 \\x01(\\rR\\bparamIdx\\\"\\xe8\\x01\\n\" +\n\t\"\\x04Decl\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\rR\\x02id\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x121\\n\" +\n\t\"\\x04type\\x18\\x03 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x04type\\x12G\\n\" +\n\t\"\\vtype_params\\x18\\x06 \\x03(\\v2&.encore.parser.schema.v1.TypeParameterR\\n\" +\n\t\"typeParams\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x04 \\x01(\\tR\\x03doc\\x12.\\n\" +\n\t\"\\x03loc\\x18\\x05 \\x01(\\v2\\x1c.encore.parser.schema.v1.LocR\\x03loc\\\"#\\n\" +\n\t\"\\rTypeParameter\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\\"\\x99\\x02\\n\" +\n\t\"\\x03Loc\\x12\\x19\\n\" +\n\t\"\\bpkg_path\\x18\\x01 \\x01(\\tR\\apkgPath\\x12\\x19\\n\" +\n\t\"\\bpkg_name\\x18\\x02 \\x01(\\tR\\apkgName\\x12\\x1a\\n\" +\n\t\"\\bfilename\\x18\\x03 \\x01(\\tR\\bfilename\\x12\\x1b\\n\" +\n\t\"\\tstart_pos\\x18\\x04 \\x01(\\x05R\\bstartPos\\x12\\x17\\n\" +\n\t\"\\aend_pos\\x18\\x05 \\x01(\\x05R\\x06endPos\\x12$\\n\" +\n\t\"\\x0esrc_line_start\\x18\\x06 \\x01(\\x05R\\fsrcLineStart\\x12 \\n\" +\n\t\"\\fsrc_line_end\\x18\\a \\x01(\\x05R\\n\" +\n\t\"srcLineEnd\\x12\\\"\\n\" +\n\t\"\\rsrc_col_start\\x18\\b \\x01(\\x05R\\vsrcColStart\\x12\\x1e\\n\" +\n\t\"\\vsrc_col_end\\x18\\t \\x01(\\x05R\\tsrcColEnd\\\"]\\n\" +\n\t\"\\x05Named\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\rR\\x02id\\x12D\\n\" +\n\t\"\\x0etype_arguments\\x18\\x02 \\x03(\\v2\\x1d.encore.parser.schema.v1.TypeR\\rtypeArguments\\\"@\\n\" +\n\t\"\\x06Struct\\x126\\n\" +\n\t\"\\x06fields\\x18\\x01 \\x03(\\v2\\x1e.encore.parser.schema.v1.FieldR\\x06fields\\\"\\xd3\\x02\\n\" +\n\t\"\\x05Field\\x12/\\n\" +\n\t\"\\x03typ\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x03typ\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x10\\n\" +\n\t\"\\x03doc\\x18\\x03 \\x01(\\tR\\x03doc\\x12\\x1b\\n\" +\n\t\"\\tjson_name\\x18\\x04 \\x01(\\tR\\bjsonName\\x12\\x1a\\n\" +\n\t\"\\boptional\\x18\\x05 \\x01(\\bR\\boptional\\x12*\\n\" +\n\t\"\\x11query_string_name\\x18\\x06 \\x01(\\tR\\x0fqueryStringName\\x12\\x17\\n\" +\n\t\"\\araw_tag\\x18\\a \\x01(\\tR\\x06rawTag\\x120\\n\" +\n\t\"\\x04tags\\x18\\b \\x03(\\v2\\x1c.encore.parser.schema.v1.TagR\\x04tags\\x12:\\n\" +\n\t\"\\x04wire\\x18\\t \\x01(\\v2!.encore.parser.schema.v1.WireSpecH\\x00R\\x04wire\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_wire\\\"\\xc1\\x03\\n\" +\n\t\"\\bWireSpec\\x12B\\n\" +\n\t\"\\x06header\\x18\\x01 \\x01(\\v2(.encore.parser.schema.v1.WireSpec.HeaderH\\x00R\\x06header\\x12?\\n\" +\n\t\"\\x05query\\x18\\x02 \\x01(\\v2'.encore.parser.schema.v1.WireSpec.QueryH\\x00R\\x05query\\x12B\\n\" +\n\t\"\\x06cookie\\x18\\x03 \\x01(\\v2(.encore.parser.schema.v1.WireSpec.CookieH\\x00R\\x06cookie\\x12O\\n\" +\n\t\"\\vhttp_status\\x18\\x04 \\x01(\\v2,.encore.parser.schema.v1.WireSpec.HttpStatusH\\x00R\\n\" +\n\t\"httpStatus\\x1a*\\n\" +\n\t\"\\x06Header\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tH\\x00R\\x04name\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_name\\x1a)\\n\" +\n\t\"\\x05Query\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tH\\x00R\\x04name\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_name\\x1a*\\n\" +\n\t\"\\x06Cookie\\x12\\x17\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tH\\x00R\\x04name\\x88\\x01\\x01B\\a\\n\" +\n\t\"\\x05_name\\x1a\\f\\n\" +\n\t\"\\n\" +\n\t\"HttpStatusB\\n\" +\n\t\"\\n\" +\n\t\"\\blocation\\\"E\\n\" +\n\t\"\\x03Tag\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x18\\n\" +\n\t\"\\aoptions\\x18\\x03 \\x03(\\tR\\aoptions\\\"k\\n\" +\n\t\"\\x03Map\\x12/\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x03key\\x123\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x05value\\\"9\\n\" +\n\t\"\\x04List\\x121\\n\" +\n\t\"\\x04elem\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x04elem\\\"<\\n\" +\n\t\"\\aPointer\\x121\\n\" +\n\t\"\\x04base\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x04base\\\"=\\n\" +\n\t\"\\x06Option\\x123\\n\" +\n\t\"\\x05value\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x05value\\\"<\\n\" +\n\t\"\\x05Union\\x123\\n\" +\n\t\"\\x05types\\x18\\x01 \\x03(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x05types\\\"\\x84\\x01\\n\" +\n\t\"\\aLiteral\\x12\\x12\\n\" +\n\t\"\\x03str\\x18\\x01 \\x01(\\tH\\x00R\\x03str\\x12\\x1a\\n\" +\n\t\"\\aboolean\\x18\\x02 \\x01(\\bH\\x00R\\aboolean\\x12\\x12\\n\" +\n\t\"\\x03int\\x18\\x03 \\x01(\\x03H\\x00R\\x03int\\x12\\x16\\n\" +\n\t\"\\x05float\\x18\\x04 \\x01(\\x01H\\x00R\\x05float\\x12\\x14\\n\" +\n\t\"\\x04null\\x18\\x05 \\x01(\\bH\\x00R\\x04nullB\\a\\n\" +\n\t\"\\x05value\\\"d\\n\" +\n\t\"\\vConfigValue\\x121\\n\" +\n\t\"\\x04elem\\x18\\x01 \\x01(\\v2\\x1d.encore.parser.schema.v1.TypeR\\x04elem\\x12\\\"\\n\" +\n\t\"\\fIsValuesList\\x18\\x02 \\x01(\\bR\\fIsValuesList*\\xf2\\x01\\n\" +\n\t\"\\aBuiltin\\x12\\a\\n\" +\n\t\"\\x03ANY\\x10\\x00\\x12\\b\\n\" +\n\t\"\\x04BOOL\\x10\\x01\\x12\\b\\n\" +\n\t\"\\x04INT8\\x10\\x02\\x12\\t\\n\" +\n\t\"\\x05INT16\\x10\\x03\\x12\\t\\n\" +\n\t\"\\x05INT32\\x10\\x04\\x12\\t\\n\" +\n\t\"\\x05INT64\\x10\\x05\\x12\\t\\n\" +\n\t\"\\x05UINT8\\x10\\x06\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UINT16\\x10\\a\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UINT32\\x10\\b\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06UINT64\\x10\\t\\x12\\v\\n\" +\n\t\"\\aFLOAT32\\x10\\n\" +\n\t\"\\x12\\v\\n\" +\n\t\"\\aFLOAT64\\x10\\v\\x12\\n\" +\n\t\"\\n\" +\n\t\"\\x06STRING\\x10\\f\\x12\\t\\n\" +\n\t\"\\x05BYTES\\x10\\r\\x12\\b\\n\" +\n\t\"\\x04TIME\\x10\\x0e\\x12\\b\\n\" +\n\t\"\\x04UUID\\x10\\x0f\\x12\\b\\n\" +\n\t\"\\x04JSON\\x10\\x10\\x12\\v\\n\" +\n\t\"\\aUSER_ID\\x10\\x11\\x12\\a\\n\" +\n\t\"\\x03INT\\x10\\x12\\x12\\b\\n\" +\n\t\"\\x04UINT\\x10\\x13\\x12\\v\\n\" +\n\t\"\\aDECIMAL\\x10\\x14B(Z&encr.dev/proto/encore/parser/schema/v1b\\x06proto3\"\n\nvar (\n\tfile_encore_parser_schema_v1_schema_proto_rawDescOnce sync.Once\n\tfile_encore_parser_schema_v1_schema_proto_rawDescData []byte\n)\n\nfunc file_encore_parser_schema_v1_schema_proto_rawDescGZIP() []byte {\n\tfile_encore_parser_schema_v1_schema_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_parser_schema_v1_schema_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_parser_schema_v1_schema_proto_rawDesc), len(file_encore_parser_schema_v1_schema_proto_rawDesc)))\n\t})\n\treturn file_encore_parser_schema_v1_schema_proto_rawDescData\n}\n\nvar file_encore_parser_schema_v1_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_encore_parser_schema_v1_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 25)\nvar file_encore_parser_schema_v1_schema_proto_goTypes = []any{\n\t(Builtin)(0),                // 0: encore.parser.schema.v1.Builtin\n\t(ValidationRule_Is)(0),      // 1: encore.parser.schema.v1.ValidationRule.Is\n\t(*Type)(nil),                // 2: encore.parser.schema.v1.Type\n\t(*ValidationRule)(nil),      // 3: encore.parser.schema.v1.ValidationRule\n\t(*ValidationExpr)(nil),      // 4: encore.parser.schema.v1.ValidationExpr\n\t(*TypeParameterRef)(nil),    // 5: encore.parser.schema.v1.TypeParameterRef\n\t(*Decl)(nil),                // 6: encore.parser.schema.v1.Decl\n\t(*TypeParameter)(nil),       // 7: encore.parser.schema.v1.TypeParameter\n\t(*Loc)(nil),                 // 8: encore.parser.schema.v1.Loc\n\t(*Named)(nil),               // 9: encore.parser.schema.v1.Named\n\t(*Struct)(nil),              // 10: encore.parser.schema.v1.Struct\n\t(*Field)(nil),               // 11: encore.parser.schema.v1.Field\n\t(*WireSpec)(nil),            // 12: encore.parser.schema.v1.WireSpec\n\t(*Tag)(nil),                 // 13: encore.parser.schema.v1.Tag\n\t(*Map)(nil),                 // 14: encore.parser.schema.v1.Map\n\t(*List)(nil),                // 15: encore.parser.schema.v1.List\n\t(*Pointer)(nil),             // 16: encore.parser.schema.v1.Pointer\n\t(*Option)(nil),              // 17: encore.parser.schema.v1.Option\n\t(*Union)(nil),               // 18: encore.parser.schema.v1.Union\n\t(*Literal)(nil),             // 19: encore.parser.schema.v1.Literal\n\t(*ConfigValue)(nil),         // 20: encore.parser.schema.v1.ConfigValue\n\t(*ValidationExpr_And)(nil),  // 21: encore.parser.schema.v1.ValidationExpr.And\n\t(*ValidationExpr_Or)(nil),   // 22: encore.parser.schema.v1.ValidationExpr.Or\n\t(*WireSpec_Header)(nil),     // 23: encore.parser.schema.v1.WireSpec.Header\n\t(*WireSpec_Query)(nil),      // 24: encore.parser.schema.v1.WireSpec.Query\n\t(*WireSpec_Cookie)(nil),     // 25: encore.parser.schema.v1.WireSpec.Cookie\n\t(*WireSpec_HttpStatus)(nil), // 26: encore.parser.schema.v1.WireSpec.HttpStatus\n}\nvar file_encore_parser_schema_v1_schema_proto_depIdxs = []int32{\n\t9,  // 0: encore.parser.schema.v1.Type.named:type_name -> encore.parser.schema.v1.Named\n\t10, // 1: encore.parser.schema.v1.Type.struct:type_name -> encore.parser.schema.v1.Struct\n\t14, // 2: encore.parser.schema.v1.Type.map:type_name -> encore.parser.schema.v1.Map\n\t15, // 3: encore.parser.schema.v1.Type.list:type_name -> encore.parser.schema.v1.List\n\t0,  // 4: encore.parser.schema.v1.Type.builtin:type_name -> encore.parser.schema.v1.Builtin\n\t16, // 5: encore.parser.schema.v1.Type.pointer:type_name -> encore.parser.schema.v1.Pointer\n\t18, // 6: encore.parser.schema.v1.Type.union:type_name -> encore.parser.schema.v1.Union\n\t19, // 7: encore.parser.schema.v1.Type.literal:type_name -> encore.parser.schema.v1.Literal\n\t17, // 8: encore.parser.schema.v1.Type.option:type_name -> encore.parser.schema.v1.Option\n\t5,  // 9: encore.parser.schema.v1.Type.type_parameter:type_name -> encore.parser.schema.v1.TypeParameterRef\n\t20, // 10: encore.parser.schema.v1.Type.config:type_name -> encore.parser.schema.v1.ConfigValue\n\t4,  // 11: encore.parser.schema.v1.Type.validation:type_name -> encore.parser.schema.v1.ValidationExpr\n\t1,  // 12: encore.parser.schema.v1.ValidationRule.is:type_name -> encore.parser.schema.v1.ValidationRule.Is\n\t3,  // 13: encore.parser.schema.v1.ValidationExpr.rule:type_name -> encore.parser.schema.v1.ValidationRule\n\t21, // 14: encore.parser.schema.v1.ValidationExpr.and:type_name -> encore.parser.schema.v1.ValidationExpr.And\n\t22, // 15: encore.parser.schema.v1.ValidationExpr.or:type_name -> encore.parser.schema.v1.ValidationExpr.Or\n\t2,  // 16: encore.parser.schema.v1.Decl.type:type_name -> encore.parser.schema.v1.Type\n\t7,  // 17: encore.parser.schema.v1.Decl.type_params:type_name -> encore.parser.schema.v1.TypeParameter\n\t8,  // 18: encore.parser.schema.v1.Decl.loc:type_name -> encore.parser.schema.v1.Loc\n\t2,  // 19: encore.parser.schema.v1.Named.type_arguments:type_name -> encore.parser.schema.v1.Type\n\t11, // 20: encore.parser.schema.v1.Struct.fields:type_name -> encore.parser.schema.v1.Field\n\t2,  // 21: encore.parser.schema.v1.Field.typ:type_name -> encore.parser.schema.v1.Type\n\t13, // 22: encore.parser.schema.v1.Field.tags:type_name -> encore.parser.schema.v1.Tag\n\t12, // 23: encore.parser.schema.v1.Field.wire:type_name -> encore.parser.schema.v1.WireSpec\n\t23, // 24: encore.parser.schema.v1.WireSpec.header:type_name -> encore.parser.schema.v1.WireSpec.Header\n\t24, // 25: encore.parser.schema.v1.WireSpec.query:type_name -> encore.parser.schema.v1.WireSpec.Query\n\t25, // 26: encore.parser.schema.v1.WireSpec.cookie:type_name -> encore.parser.schema.v1.WireSpec.Cookie\n\t26, // 27: encore.parser.schema.v1.WireSpec.http_status:type_name -> encore.parser.schema.v1.WireSpec.HttpStatus\n\t2,  // 28: encore.parser.schema.v1.Map.key:type_name -> encore.parser.schema.v1.Type\n\t2,  // 29: encore.parser.schema.v1.Map.value:type_name -> encore.parser.schema.v1.Type\n\t2,  // 30: encore.parser.schema.v1.List.elem:type_name -> encore.parser.schema.v1.Type\n\t2,  // 31: encore.parser.schema.v1.Pointer.base:type_name -> encore.parser.schema.v1.Type\n\t2,  // 32: encore.parser.schema.v1.Option.value:type_name -> encore.parser.schema.v1.Type\n\t2,  // 33: encore.parser.schema.v1.Union.types:type_name -> encore.parser.schema.v1.Type\n\t2,  // 34: encore.parser.schema.v1.ConfigValue.elem:type_name -> encore.parser.schema.v1.Type\n\t4,  // 35: encore.parser.schema.v1.ValidationExpr.And.exprs:type_name -> encore.parser.schema.v1.ValidationExpr\n\t4,  // 36: encore.parser.schema.v1.ValidationExpr.Or.exprs:type_name -> encore.parser.schema.v1.ValidationExpr\n\t37, // [37:37] is the sub-list for method output_type\n\t37, // [37:37] is the sub-list for method input_type\n\t37, // [37:37] is the sub-list for extension type_name\n\t37, // [37:37] is the sub-list for extension extendee\n\t0,  // [0:37] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_parser_schema_v1_schema_proto_init() }\nfunc file_encore_parser_schema_v1_schema_proto_init() {\n\tif File_encore_parser_schema_v1_schema_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*Type_Named)(nil),\n\t\t(*Type_Struct)(nil),\n\t\t(*Type_Map)(nil),\n\t\t(*Type_List)(nil),\n\t\t(*Type_Builtin)(nil),\n\t\t(*Type_Pointer)(nil),\n\t\t(*Type_Union)(nil),\n\t\t(*Type_Literal)(nil),\n\t\t(*Type_Option)(nil),\n\t\t(*Type_TypeParameter)(nil),\n\t\t(*Type_Config)(nil),\n\t}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[1].OneofWrappers = []any{\n\t\t(*ValidationRule_MinLen)(nil),\n\t\t(*ValidationRule_MaxLen)(nil),\n\t\t(*ValidationRule_MinVal)(nil),\n\t\t(*ValidationRule_MaxVal)(nil),\n\t\t(*ValidationRule_StartsWith)(nil),\n\t\t(*ValidationRule_EndsWith)(nil),\n\t\t(*ValidationRule_MatchesRegexp)(nil),\n\t\t(*ValidationRule_Is_)(nil),\n\t}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[2].OneofWrappers = []any{\n\t\t(*ValidationExpr_Rule)(nil),\n\t\t(*ValidationExpr_And_)(nil),\n\t\t(*ValidationExpr_Or_)(nil),\n\t}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[9].OneofWrappers = []any{}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[10].OneofWrappers = []any{\n\t\t(*WireSpec_Header_)(nil),\n\t\t(*WireSpec_Query_)(nil),\n\t\t(*WireSpec_Cookie_)(nil),\n\t\t(*WireSpec_HttpStatus_)(nil),\n\t}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[17].OneofWrappers = []any{\n\t\t(*Literal_Str)(nil),\n\t\t(*Literal_Boolean)(nil),\n\t\t(*Literal_Int)(nil),\n\t\t(*Literal_Float)(nil),\n\t\t(*Literal_Null)(nil),\n\t}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[21].OneofWrappers = []any{}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[22].OneofWrappers = []any{}\n\tfile_encore_parser_schema_v1_schema_proto_msgTypes[23].OneofWrappers = []any{}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_parser_schema_v1_schema_proto_rawDesc), len(file_encore_parser_schema_v1_schema_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   25,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_parser_schema_v1_schema_proto_goTypes,\n\t\tDependencyIndexes: file_encore_parser_schema_v1_schema_proto_depIdxs,\n\t\tEnumInfos:         file_encore_parser_schema_v1_schema_proto_enumTypes,\n\t\tMessageInfos:      file_encore_parser_schema_v1_schema_proto_msgTypes,\n\t}.Build()\n\tFile_encore_parser_schema_v1_schema_proto = out.File\n\tfile_encore_parser_schema_v1_schema_proto_goTypes = nil\n\tfile_encore_parser_schema_v1_schema_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/parser/schema/v1/schema.pb.ts",
    "content": "/* eslint-disable */\nexport const protobufPackage = \"encore.parser.schema.v1\";\n\n/**\n * Builtin represents a type which Encore (and Go) have inbuilt support for and so can be represented by Encore's tooling\n * directly, rather than needing to understand the full implementation details of how the type is structured.\n */\nexport enum Builtin {\n  /** ANY - Inbuilt Go Types */\n  ANY = \"ANY\",\n  BOOL = \"BOOL\",\n  INT8 = \"INT8\",\n  INT16 = \"INT16\",\n  INT32 = \"INT32\",\n  INT64 = \"INT64\",\n  UINT8 = \"UINT8\",\n  UINT16 = \"UINT16\",\n  UINT32 = \"UINT32\",\n  UINT64 = \"UINT64\",\n  FLOAT32 = \"FLOAT32\",\n  FLOAT64 = \"FLOAT64\",\n  STRING = \"STRING\",\n  BYTES = \"BYTES\",\n  /** TIME - Additional Encore Types */\n  TIME = \"TIME\",\n  UUID = \"UUID\",\n  JSON = \"JSON\",\n  USER_ID = \"USER_ID\",\n  INT = \"INT\",\n  UINT = \"UINT\",\n  UNRECOGNIZED = \"UNRECOGNIZED\",\n}\n\n/**\n * Type represents the base of our schema on which everything else is built on-top of. It has to be one, and only one,\n * thing from our list of meta types.\n *\n * A type may be concrete or abstract, however to determine if a type is abstract you need to recursive through the\n * structures looking for any uses of the TypeParameterPtr type\n */\nexport interface Type {\n  /** Concrete / non-parameterized Types */\n  named: Named | undefined;\n  /** The type is a struct definition */\n  struct: Struct | undefined;\n  /** The type is a map */\n  map: Map | undefined;\n  /** The type is a slice */\n  list: List | undefined;\n  /** The type is one of the base built in types within Go */\n  builtin: Builtin | undefined;\n  /** The type is a pointer */\n  pointer: Pointer | undefined;\n  /** Abstract Types */\n  type_parameter: TypeParameterRef | undefined;\n  /** Encore Special Types */\n  config: ConfigValue | undefined;\n}\n\n/** TypeParameterRef is a reference to a `TypeParameter` within a declaration block */\nexport interface TypeParameterRef {\n  /** The ID of the declaration block */\n  decl_id: number;\n  /** The index of the type parameter within the declaration block */\n  param_idx: number;\n}\n\n/**\n * Decl represents the declaration of a type within the Go code which is either concrete or _parameterized_. The type is\n * concrete when there are zero type parameters assigned.\n *\n * For example the Go Code:\n * ```\n * // Set[A] represents our set type\n * type Set[A any] = map[A]struct{}\n * ```\n *\n * Would become:\n * ```\n * _ = &Decl{\n *     id: 1,\n *     name: \"Set\",\n *     type: &Type{\n *         typ_map: &Map{\n *             key: &Type { typ_type_parameter: ... reference to \"A\" type parameter below ... },\n *             value: &Type { typ_struct: ... empty struct type ... },\n *         },\n *     },\n *     typeParameters: []*TypeParameter{ { name: \"A\" } },\n *     doc: \"Set[A] represents our set type\",\n *     loc: &Loc { ... },\n * }\n * ```\n */\nexport interface Decl {\n  /** A internal ID which we can refer to this declaration by */\n  id: number;\n  /** The name of the type as assigned in the code */\n  name: string;\n  /** The underlying type of this declaration */\n  type: Type;\n  /** Any type parameters on this declaration (note; instantiated types used within this declaration would not be captured here) */\n  type_params: TypeParameter[];\n  /** The comment block on the type */\n  doc: string;\n  /** The location of the declaration within the project */\n  loc: Loc;\n}\n\n/**\n * TypeParameter acts as a place holder for an (as of yet) unknown type in the declaration; the type parameter is\n * replaced with a type argument upon instantiation of the parameterized function or type.\n */\nexport interface TypeParameter {\n  /** The identifier given to the type parameter */\n  name: string;\n}\n\n/** Loc is the location of a declaration within the code base */\nexport interface Loc {\n  /** The package path within the repo (i.e. `users/signup`) */\n  pkg_path: string;\n  /** The package name (i.e. `signup`) */\n  pkg_name: string;\n  /** The file name (i.e. `signup.go`) */\n  filename: string;\n  /** The starting index within the file for this node */\n  start_pos: number;\n  /** The ending index within the file for this node */\n  end_pos: number;\n  /** The starting line within the file for this node */\n  src_line_start: number;\n  /** The ending line within the file for this node */\n  src_line_end: number;\n  /** The starting column on the starting line for this node */\n  src_col_start: number;\n  /** The ending column on the ending line for this node */\n  src_col_end: number;\n}\n\n/** Named references declaration block by name */\nexport interface Named {\n  /** The `Decl.id` this name refers to */\n  id: number;\n  /** The type arguments used to instantiate this parameterised declaration */\n  type_arguments: Type[];\n}\n\n/** Struct contains a list of fields which make up the struct */\nexport interface Struct {\n  fields: Field[];\n}\n\n/** Field represents a field within a struct */\nexport interface Field {\n  /** The type of the field */\n  typ: Type;\n  /** The name of the field */\n  name: string;\n  /** The comment for the field */\n  doc: string;\n  /** The optional json name if it's different from the field name. (The value \"-\" indicates to omit the field.) */\n  json_name: string;\n  /** Whether the field is optional. */\n  optional: boolean;\n  /** The query string name to use in GET/HEAD/DELETE requests. (The value \"-\" indicates to omit the field.) */\n  query_string_name: string;\n  /** The original Go struct tag; should not be parsed individually */\n  raw_tag: string;\n  /** Parsed go struct tags. Used for marshalling hints */\n  tags: Tag[];\n}\n\nexport interface Tag {\n  /** The tag key (e.g. json, query, header ...) */\n  key: string;\n  /** The tag name (e.g. first_name, firstName, ...) */\n  name: string;\n  /** Key Options (e.g. omitempty, optional ...) */\n  options: string[];\n}\n\n/** Map represents a map Type */\nexport interface Map {\n  /** The type of the key for this map */\n  key: Type;\n  /** The type of the value of this map */\n  value: Type;\n}\n\n/** List represents a list type (array or slice) */\nexport interface List {\n  /** The type of the elements in the list */\n  elem: Type;\n}\n\n/** Pointer represents a pointer to a base type */\nexport interface Pointer {\n  /** The type of the pointer */\n  base: Type;\n}\n\n/** ConfigValue represents a config value wrapper. */\nexport interface ConfigValue {\n  /** The type of the config value */\n  elem: Type;\n  /** Does this config value represent the type to `config.Values[T]`. If false it represents `config.Value[T]` */\n  IsValuesList: boolean;\n}\n"
  },
  {
    "path": "proto/encore/parser/schema/v1/schema.proto",
    "content": "syntax = \"proto3\";\n\npackage encore.parser.schema.v1;\n\noption go_package = \"encr.dev/proto/encore/parser/schema/v1\";\n\n// Type represents the base of our schema on which everything else is built on-top of. It has to be one, and only one,\n// thing from our list of meta types.\n//\n// A type may be concrete or abstract, however to determine if a type is abstract you need to recursive through the\n// structures looking for any uses of the TypeParameterPtr type\nmessage Type {\n  oneof typ {\n    /* Concrete / non-parameterized Types */\n    Named named = 1; // A \"named\" type (https://tip.golang.org/ref/spec#Types)\n    Struct struct = 2; // The type is a struct definition\n    Map map = 3; // The type is a map\n    List list = 4; // The type is a slice\n    Builtin builtin = 5; // The type is one of the base built in types within Go\n    Pointer pointer = 8; // The type is a pointer\n    Union union = 9; // The type is a union\n    Literal literal = 10; // The type is a literal\n    Option option = 11; // The type is an option type\n\n    /* Abstract Types */\n    TypeParameterRef type_parameter = 6; // This is placeholder for a unknown type within the declaration block\n\n    /* Encore Special Types */\n    ConfigValue config = 7; // This value is a config value\n  }\n\n  optional ValidationExpr validation = 15; // The validation expression for this type\n}\n\nmessage ValidationRule {\n  oneof rule {\n    uint64 min_len = 1;\n    uint64 max_len = 2;\n    double min_val = 3;\n    double max_val = 4;\n    string starts_with = 5;\n    string ends_with = 6;\n    string matches_regexp = 7;\n    Is is = 8;\n  }\n\n  enum Is {\n    UNKNOWN = 0;\n    EMAIL = 1;\n    URL = 2;\n  }\n}\n\nmessage ValidationExpr {\n  oneof expr {\n    ValidationRule rule = 1;\n    And and = 2;\n    Or or = 3;\n  }\n\n  message And {\n    repeated ValidationExpr exprs = 1;\n  }\n\n  message Or {\n    repeated ValidationExpr exprs = 1;\n  }\n}\n\n// TypeParameterRef is a reference to a `TypeParameter` within a declaration block\nmessage TypeParameterRef {\n  uint32 decl_id = 1; // The ID of the declaration block\n  uint32 param_idx = 2; // The index of the type parameter within the declaration block\n}\n\n// Decl represents the declaration of a type within the Go code which is either concrete or _parameterized_. The type is\n// concrete when there are zero type parameters assigned.\n//\n// For example the Go Code:\n// ```go\n// // Set[A] represents our set type\n// type Set[A any] = map[A]struct{}\n// ```\n//\n// Would become:\n// ```go\n// _ = &Decl{\n//     id: 1,\n//     name: \"Set\",\n//     type: &Type{\n//         typ_map: &Map{\n//             key: &Type { typ_type_parameter: ... reference to \"A\" type parameter below ... },\n//             value: &Type { typ_struct: ... empty struct type ... },\n//         },\n//     },\n//     typeParameters: []*TypeParameter{ { name: \"A\" } },\n//     doc: \"Set[A] represents our set type\",\n//     loc: &Loc { ... },\n// }\n// ```\nmessage Decl {\n  uint32 id = 1; // A internal ID which we can refer to this declaration by\n  string name = 2; // The name of the type as assigned in the code\n  Type type = 3; // The underlying type of this declaration\n  repeated TypeParameter type_params = 6; // Any type parameters on this declaration (note; instantiated types used within this declaration would not be captured here)\n  string doc = 4; // The comment block on the type\n  Loc loc = 5; // The location of the declaration within the project\n}\n\n// TypeParameter acts as a place holder for an (as of yet) unknown type in the declaration; the type parameter is\n// replaced with a type argument upon instantiation of the parameterized function or type.\nmessage TypeParameter {\n  string name = 1; // The identifier given to the type parameter\n}\n\n// Loc is the location of a declaration within the code base\nmessage Loc {\n  string pkg_path = 1; // The package path within the repo (i.e. `users/signup`)\n  string pkg_name = 2; // The package name (i.e. `signup`)\n  string filename = 3; // The file name (i.e. `signup.go`)\n  int32 start_pos = 4; // The starting index within the file for this node\n  int32 end_pos = 5; // The ending index within the file for this node\n  int32 src_line_start = 6; // The starting line within the file for this node\n  int32 src_line_end = 7; // The ending line within the file for this node\n  int32 src_col_start = 8; // The starting column on the starting line for this node\n  int32 src_col_end = 9; // The ending column on the ending line for this node\n}\n\n// Named references declaration block by name\nmessage Named {\n  uint32 id = 1; // The `Decl.id` this name refers to\n  repeated Type type_arguments = 2; // The type arguments used to instantiate this parameterised declaration\n}\n\n// Struct contains a list of fields which make up the struct\nmessage Struct {\n  repeated Field fields = 1;\n}\n\n// Field represents a field within a struct\nmessage Field {\n  Type typ = 1; // The type of the field\n  string name = 2; // The name of the field\n  string doc = 3; // The comment for the field\n  string json_name = 4; // The optional json name if it's different from the field name. (The value \"-\" indicates to omit the field.)\n  bool optional = 5; // Whether the field is optional.\n  string query_string_name = 6; // The query string name to use in GET/HEAD/DELETE requests. (The value \"-\" indicates to omit the field.)\n  string raw_tag = 7; // The original Go struct tag; should not be parsed individually\n  repeated Tag tags = 8; // Parsed go struct tags. Used for marshalling hints\n  optional WireSpec wire = 9; // The explicitly set wire location of the field.\n}\n\n// WireLocation provides information about how a field should be encoded on the wire.\nmessage WireSpec {\n  oneof location {\n    Header header = 1;\n    Query query = 2;\n    Cookie cookie = 3;\n    HttpStatus http_status = 4;\n  }\n\n  message Header {\n    // The explicitly specified header name.\n    // If empty, the name of the field is used.\n    optional string name = 1;\n  }\n\n  message Query {\n    // The explicitly specified query string name.\n    // If empty, the name of the field is used.\n    optional string name = 1;\n  }\n\n  message Cookie {\n    // The explicitly specified cookie string name.\n    // If empty, the name of the field is used.\n    optional string name = 1;\n  }\n\n  message HttpStatus {\n    // HttpStatus fields don't have a name parameter\n    // as they represent the HTTP status code itself\n  }\n}\n\nmessage Tag {\n  string key = 1; // The tag key (e.g. json, query, header ...)\n  string name = 2; // The tag name (e.g. first_name, firstName, ...)\n  repeated string options = 3; // Key Options (e.g. omitempty, optional ...)\n}\n\n// Map represents a map Type\nmessage Map {\n  Type key = 1; // The type of the key for this map\n  Type value = 2; // The type of the value of this map\n}\n\n// List represents a list type (array or slice)\nmessage List {\n  Type elem = 1; // The type of the elements in the list\n}\n\n// Pointer represents a pointer to a base type\nmessage Pointer {\n  Type base = 1; // The type of the pointer\n}\n\n// Option represents an option type.\nmessage Option {\n  Type value = 1; // The value it may contain.\n}\n\n// Union represents a union type.\nmessage Union {\n  repeated Type types = 1; // The types that make up the union\n}\n\n// Literal represents a literal value.\nmessage Literal {\n  oneof value {\n    string str = 1;\n    bool boolean = 2;\n    int64 int = 3;\n    double float = 4;\n    bool null = 5;\n  }\n}\n\n// ConfigValue represents a config value wrapper.\nmessage ConfigValue {\n  Type elem = 1; // The type of the config value\n  bool IsValuesList = 2; // Does this config value represent the type to `config.Values[T]`. If false it represents `config.Value[T]`\n}\n\n// Builtin represents a type which Encore (and Go) have inbuilt support for and so can be represented by Encore's tooling\n// directly, rather than needing to understand the full implementation details of how the type is structured.\nenum Builtin {\n  /* Inbuilt Go Types */\n  ANY = 0;\n  BOOL = 1;\n  INT8 = 2;\n  INT16 = 3;\n  INT32 = 4;\n  INT64 = 5;\n  UINT8 = 6;\n  UINT16 = 7;\n  UINT32 = 8;\n  UINT64 = 9;\n  FLOAT32 = 10;\n  FLOAT64 = 11;\n  STRING = 12;\n  BYTES = 13;\n\n  /* Additional Encore Types */\n  TIME = 14;\n  UUID = 15;\n  JSON = 16;\n  USER_ID = 17;\n\n  INT = 18;\n  UINT = 19;\n\n  DECIMAL = 20;\n}\n"
  },
  {
    "path": "proto/encore/parser/schema/v1/walk.go",
    "content": "package v1\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// Walk will perform a depth first walk of all schema nodes starting at node, calling visitor for each schema type found.\n//\n// If visitor returns false, the walk will be aborted.\nfunc Walk(decls []*Decl, node any, visitor func(node any) error) error {\n\tnamedChain := make([]uint32, 0, 10)\n\treturn walk(decls, node, visitor, namedChain)\n}\n\nfunc walk(decls []*Decl, node any, visitor func(node any) error, namedChain []uint32) error {\n\t// Check the visitor against the node type\n\tif err := visitor(node); err != nil {\n\t\treturn err\n\t}\n\n\tswitch node := node.(type) {\n\tcase *Type:\n\t\tswitch v := node.Typ.(type) {\n\t\tcase *Type_Named:\n\t\t\treturn walk(decls, v.Named, visitor, namedChain)\n\t\tcase *Type_Struct:\n\t\t\treturn walk(decls, v.Struct, visitor, namedChain)\n\t\tcase *Type_Map:\n\t\t\treturn walk(decls, v.Map, visitor, namedChain)\n\t\tcase *Type_List:\n\t\t\treturn walk(decls, v.List, visitor, namedChain)\n\t\tcase *Type_Builtin:\n\t\t\treturn walk(decls, v.Builtin, visitor, namedChain)\n\t\tcase *Type_Pointer:\n\t\t\treturn walk(decls, v.Pointer, visitor, namedChain)\n\t\tcase *Type_Option:\n\t\t\treturn walk(decls, v.Option, visitor, namedChain)\n\t\tcase *Type_TypeParameter:\n\t\t\treturn walk(decls, v.TypeParameter, visitor, namedChain)\n\t\tcase *Type_Literal:\n\t\t\treturn walk(decls, v.Literal, visitor, namedChain)\n\t\tcase *Type_Union:\n\t\t\treturn walk(decls, v.Union, visitor, namedChain)\n\t\tcase *Type_Config:\n\t\t\treturn walk(decls, v.Config, visitor, namedChain)\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unknown type encountered: %+v\", reflect.TypeOf(v)))\n\t\t}\n\n\tcase *Decl:\n\t\treturn walk(decls, decls[node.Id].Type, visitor, namedChain)\n\n\tcase *Named:\n\t\tfor _, typ := range node.TypeArguments {\n\t\t\tif err := walk(decls, typ, visitor, namedChain); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// Have we already visited this named type?\n\t\tfor i := len(namedChain) - 1; i >= 0; i-- {\n\t\t\tif namedChain[i] == node.Id {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tnamedChain = append(namedChain, node.Id)\n\n\t\treturn walk(decls, decls[node.Id].Type, visitor, namedChain)\n\tcase *Struct:\n\t\tfor _, field := range node.Fields {\n\t\t\tif err := walk(decls, field.Typ, visitor, namedChain); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase *Union:\n\t\tfor _, typ := range node.Types {\n\t\t\tif err := walk(decls, typ, visitor, namedChain); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase *Map:\n\t\tif err := walk(decls, node.Key, visitor, namedChain); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn walk(decls, node.Value, visitor, namedChain)\n\tcase *List:\n\t\treturn walk(decls, node.Elem, visitor, namedChain)\n\tcase Builtin:\n\t\treturn nil\n\tcase *Pointer:\n\t\treturn walk(decls, node.Base, visitor, namedChain)\n\tcase *Option:\n\t\treturn walk(decls, node.Value, visitor, namedChain)\n\tcase *TypeParameterRef:\n\t\treturn nil\n\tcase *Literal:\n\t\treturn nil\n\tcase *ConfigValue:\n\t\treturn walk(decls, node.Elem, visitor, namedChain)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported node type encountered during walk: %+v\", reflect.TypeOf(node)))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "proto/encore/parser/schema/v1/walk_test.go",
    "content": "package v1\n\nimport \"testing\"\n\n// TestWalk_RecursiveDataStructure tests that Walk gracefully handles\n// recursive and mutually recursive data structures.\nfunc TestWalk_RecursiveDataStructure(t *testing.T) {\n\tselfRecursive := &Decl{\n\t\tId: 0,\n\t\tType: &Type{\n\t\t\tTyp: &Type_Named{\n\t\t\t\tNamed: &Named{\n\t\t\t\t\tId: 0,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmutualRecursiveOne := &Decl{\n\t\tId: 1,\n\t\tType: &Type{\n\t\t\tTyp: &Type_Struct{\n\t\t\t\tStruct: &Struct{\n\t\t\t\t\tFields: []*Field{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTyp: &Type{Typ: &Type_Named{\n\t\t\t\t\t\t\t\tNamed: &Named{Id: 1},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tmutualRecursiveTwo := &Decl{\n\t\tId: 1,\n\t\tType: &Type{\n\t\t\tTyp: &Type_Struct{\n\t\t\t\tStruct: &Struct{\n\t\t\t\t\tFields: []*Field{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTyp: &Type{Typ: &Type_Named{\n\t\t\t\t\t\t\t\tNamed: &Named{Id: 0},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname  string\n\t\tdecls []*Decl\n\t\tnode  any\n\t}{\n\t\t{\n\t\t\tname:  \"self_recursive\",\n\t\t\tdecls: []*Decl{selfRecursive},\n\t\t\tnode:  selfRecursive.Type,\n\t\t},\n\t\t{\n\t\t\tname:  \"mutual_recursive\",\n\t\t\tdecls: []*Decl{mutualRecursiveOne, mutualRecursiveTwo},\n\t\t\tnode:  mutualRecursiveOne.Type,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvisitor := func(node any) error { return nil }\n\t\t\tif err := Walk(tt.decls, tt.node, visitor); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "proto/encore/runtime/v1/infra.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/runtime/v1/infra.proto\n\npackage runtimev1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype ServerKind int32\n\nconst (\n\tServerKind_SERVER_KIND_UNSPECIFIED ServerKind = 0\n\tServerKind_SERVER_KIND_PRIMARY     ServerKind = 1\n\t// A hot-standby (a read replica designed to take over write traffic\n\t// at a moment's notice).\n\tServerKind_SERVER_KIND_HOT_STANDBY ServerKind = 2\n\t// A read-replica.\n\tServerKind_SERVER_KIND_READ_REPLICA ServerKind = 3\n)\n\n// Enum value maps for ServerKind.\nvar (\n\tServerKind_name = map[int32]string{\n\t\t0: \"SERVER_KIND_UNSPECIFIED\",\n\t\t1: \"SERVER_KIND_PRIMARY\",\n\t\t2: \"SERVER_KIND_HOT_STANDBY\",\n\t\t3: \"SERVER_KIND_READ_REPLICA\",\n\t}\n\tServerKind_value = map[string]int32{\n\t\t\"SERVER_KIND_UNSPECIFIED\":  0,\n\t\t\"SERVER_KIND_PRIMARY\":      1,\n\t\t\"SERVER_KIND_HOT_STANDBY\":  2,\n\t\t\"SERVER_KIND_READ_REPLICA\": 3,\n\t}\n)\n\nfunc (x ServerKind) Enum() *ServerKind {\n\tp := new(ServerKind)\n\t*p = x\n\treturn p\n}\n\nfunc (x ServerKind) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (ServerKind) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_runtime_v1_infra_proto_enumTypes[0].Descriptor()\n}\n\nfunc (ServerKind) Type() protoreflect.EnumType {\n\treturn &file_encore_runtime_v1_infra_proto_enumTypes[0]\n}\n\nfunc (x ServerKind) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use ServerKind.Descriptor instead.\nfunc (ServerKind) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0}\n}\n\ntype PubSubTopic_DeliveryGuarantee int32\n\nconst (\n\tPubSubTopic_DELIVERY_GUARANTEE_UNSPECIFIED   PubSubTopic_DeliveryGuarantee = 0\n\tPubSubTopic_DELIVERY_GUARANTEE_AT_LEAST_ONCE PubSubTopic_DeliveryGuarantee = 1 // All messages will be delivered to each subscription at least once\n\tPubSubTopic_DELIVERY_GUARANTEE_EXACTLY_ONCE  PubSubTopic_DeliveryGuarantee = 2 // All messages will be delivered to each subscription exactly once\n)\n\n// Enum value maps for PubSubTopic_DeliveryGuarantee.\nvar (\n\tPubSubTopic_DeliveryGuarantee_name = map[int32]string{\n\t\t0: \"DELIVERY_GUARANTEE_UNSPECIFIED\",\n\t\t1: \"DELIVERY_GUARANTEE_AT_LEAST_ONCE\",\n\t\t2: \"DELIVERY_GUARANTEE_EXACTLY_ONCE\",\n\t}\n\tPubSubTopic_DeliveryGuarantee_value = map[string]int32{\n\t\t\"DELIVERY_GUARANTEE_UNSPECIFIED\":   0,\n\t\t\"DELIVERY_GUARANTEE_AT_LEAST_ONCE\": 1,\n\t\t\"DELIVERY_GUARANTEE_EXACTLY_ONCE\":  2,\n\t}\n)\n\nfunc (x PubSubTopic_DeliveryGuarantee) Enum() *PubSubTopic_DeliveryGuarantee {\n\tp := new(PubSubTopic_DeliveryGuarantee)\n\t*p = x\n\treturn p\n}\n\nfunc (x PubSubTopic_DeliveryGuarantee) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (PubSubTopic_DeliveryGuarantee) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_runtime_v1_infra_proto_enumTypes[1].Descriptor()\n}\n\nfunc (PubSubTopic_DeliveryGuarantee) Type() protoreflect.EnumType {\n\treturn &file_encore_runtime_v1_infra_proto_enumTypes[1]\n}\n\nfunc (x PubSubTopic_DeliveryGuarantee) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use PubSubTopic_DeliveryGuarantee.Descriptor instead.\nfunc (PubSubTopic_DeliveryGuarantee) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{15, 0}\n}\n\ntype Infrastructure struct {\n\tstate         protoimpl.MessageState      `protogen:\"open.v1\"`\n\tResources     *Infrastructure_Resources   `protobuf:\"bytes,1,opt,name=resources,proto3\" json:\"resources,omitempty\"`\n\tCredentials   *Infrastructure_Credentials `protobuf:\"bytes,2,opt,name=credentials,proto3\" json:\"credentials,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Infrastructure) Reset() {\n\t*x = Infrastructure{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Infrastructure) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Infrastructure) ProtoMessage() {}\n\nfunc (x *Infrastructure) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Infrastructure.ProtoReflect.Descriptor instead.\nfunc (*Infrastructure) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Infrastructure) GetResources() *Infrastructure_Resources {\n\tif x != nil {\n\t\treturn x.Resources\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure) GetCredentials() *Infrastructure_Credentials {\n\tif x != nil {\n\t\treturn x.Credentials\n\t}\n\treturn nil\n}\n\ntype SQLCluster struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this cluster.\n\tRid           string         `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tServers       []*SQLServer   `protobuf:\"bytes,2,rep,name=servers,proto3\" json:\"servers,omitempty\"`\n\tDatabases     []*SQLDatabase `protobuf:\"bytes,3,rep,name=databases,proto3\" json:\"databases,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLCluster) Reset() {\n\t*x = SQLCluster{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLCluster) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLCluster) ProtoMessage() {}\n\nfunc (x *SQLCluster) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLCluster.ProtoReflect.Descriptor instead.\nfunc (*SQLCluster) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *SQLCluster) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLCluster) GetServers() []*SQLServer {\n\tif x != nil {\n\t\treturn x.Servers\n\t}\n\treturn nil\n}\n\nfunc (x *SQLCluster) GetDatabases() []*SQLDatabase {\n\tif x != nil {\n\t\treturn x.Databases\n\t}\n\treturn nil\n}\n\ntype TLSConfig struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Server CA Cert PEM to use for verifying the server's certificate.\n\tServerCaCert *string `protobuf:\"bytes,1,opt,name=server_ca_cert,json=serverCaCert,proto3,oneof\" json:\"server_ca_cert,omitempty\"`\n\t// If true, skips hostname verification when connecting.\n\t// If invalid hostnames are trusted, *any* valid certificate for *any* site will be trusted for use.\n\t// This introduces significant vulnerabilities, and should only be used as a last resort.\n\tDisableTlsHostnameVerification bool `protobuf:\"varint,2,opt,name=disable_tls_hostname_verification,json=disableTlsHostnameVerification,proto3\" json:\"disable_tls_hostname_verification,omitempty\"`\n\t// If true, skips CA cert validation when connecting.\n\t// This introduces significant vulnerabilities, and should only be used as a last resort.\n\tDisableCaValidation bool `protobuf:\"varint,3,opt,name=disable_ca_validation,json=disableCaValidation,proto3\" json:\"disable_ca_validation,omitempty\"`\n\tunknownFields       protoimpl.UnknownFields\n\tsizeCache           protoimpl.SizeCache\n}\n\nfunc (x *TLSConfig) Reset() {\n\t*x = TLSConfig{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TLSConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TLSConfig) ProtoMessage() {}\n\nfunc (x *TLSConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TLSConfig.ProtoReflect.Descriptor instead.\nfunc (*TLSConfig) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *TLSConfig) GetServerCaCert() string {\n\tif x != nil && x.ServerCaCert != nil {\n\t\treturn *x.ServerCaCert\n\t}\n\treturn \"\"\n}\n\nfunc (x *TLSConfig) GetDisableTlsHostnameVerification() bool {\n\tif x != nil {\n\t\treturn x.DisableTlsHostnameVerification\n\t}\n\treturn false\n}\n\nfunc (x *TLSConfig) GetDisableCaValidation() bool {\n\tif x != nil {\n\t\treturn x.DisableCaValidation\n\t}\n\treturn false\n}\n\ntype SQLServer struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this server.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// Host is the host to connect to.\n\t// Valid formats are \"hostname\", \"hostname:port\", and \"/path/to/unix.socket\".\n\tHost string     `protobuf:\"bytes,2,opt,name=host,proto3\" json:\"host,omitempty\"`\n\tKind ServerKind `protobuf:\"varint,3,opt,name=kind,proto3,enum=encore.runtime.v1.ServerKind\" json:\"kind,omitempty\"`\n\t// TLS configuration to use when connecting.\n\tTlsConfig     *TLSConfig `protobuf:\"bytes,4,opt,name=tls_config,json=tlsConfig,proto3,oneof\" json:\"tls_config,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLServer) Reset() {\n\t*x = SQLServer{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLServer) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLServer) ProtoMessage() {}\n\nfunc (x *SQLServer) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLServer.ProtoReflect.Descriptor instead.\nfunc (*SQLServer) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *SQLServer) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLServer) GetHost() string {\n\tif x != nil {\n\t\treturn x.Host\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLServer) GetKind() ServerKind {\n\tif x != nil {\n\t\treturn x.Kind\n\t}\n\treturn ServerKind_SERVER_KIND_UNSPECIFIED\n}\n\nfunc (x *SQLServer) GetTlsConfig() *TLSConfig {\n\tif x != nil {\n\t\treturn x.TlsConfig\n\t}\n\treturn nil\n}\n\ntype ClientCert struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this certificate.\n\tRid           string      `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tCert          string      `protobuf:\"bytes,2,opt,name=cert,proto3\" json:\"cert,omitempty\"`\n\tKey           *SecretData `protobuf:\"bytes,3,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ClientCert) Reset() {\n\t*x = ClientCert{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ClientCert) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ClientCert) ProtoMessage() {}\n\nfunc (x *ClientCert) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ClientCert.ProtoReflect.Descriptor instead.\nfunc (*ClientCert) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *ClientCert) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *ClientCert) GetCert() string {\n\tif x != nil {\n\t\treturn x.Cert\n\t}\n\treturn \"\"\n}\n\nfunc (x *ClientCert) GetKey() *SecretData {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn nil\n}\n\ntype SQLRole struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this role.\n\tRid      string      `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tUsername string      `protobuf:\"bytes,2,opt,name=username,proto3\" json:\"username,omitempty\"`\n\tPassword *SecretData `protobuf:\"bytes,3,opt,name=password,proto3\" json:\"password,omitempty\"`\n\t// The client cert to use to authenticate, if any.\n\tClientCertRid *string `protobuf:\"bytes,4,opt,name=client_cert_rid,json=clientCertRid,proto3,oneof\" json:\"client_cert_rid,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLRole) Reset() {\n\t*x = SQLRole{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLRole) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLRole) ProtoMessage() {}\n\nfunc (x *SQLRole) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLRole.ProtoReflect.Descriptor instead.\nfunc (*SQLRole) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *SQLRole) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLRole) GetUsername() string {\n\tif x != nil {\n\t\treturn x.Username\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLRole) GetPassword() *SecretData {\n\tif x != nil {\n\t\treturn x.Password\n\t}\n\treturn nil\n}\n\nfunc (x *SQLRole) GetClientCertRid() string {\n\tif x != nil && x.ClientCertRid != nil {\n\t\treturn *x.ClientCertRid\n\t}\n\treturn \"\"\n}\n\ntype SQLDatabase struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this database.\n\tRid        string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tEncoreName string `protobuf:\"bytes,2,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The physical name of the database in the cluster.\n\tCloudName string `protobuf:\"bytes,3,opt,name=cloud_name,json=cloudName,proto3\" json:\"cloud_name,omitempty\"`\n\t// Connection pools to use for connecting to the database.\n\tConnPools     []*SQLConnectionPool `protobuf:\"bytes,4,rep,name=conn_pools,json=connPools,proto3\" json:\"conn_pools,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SQLDatabase) Reset() {\n\t*x = SQLDatabase{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLDatabase) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLDatabase) ProtoMessage() {}\n\nfunc (x *SQLDatabase) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLDatabase.ProtoReflect.Descriptor instead.\nfunc (*SQLDatabase) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *SQLDatabase) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLDatabase) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLDatabase) GetCloudName() string {\n\tif x != nil {\n\t\treturn x.CloudName\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLDatabase) GetConnPools() []*SQLConnectionPool {\n\tif x != nil {\n\t\treturn x.ConnPools\n\t}\n\treturn nil\n}\n\ntype SQLConnectionPool struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Whether this connection pool is for read-only servers.\n\tIsReadonly bool `protobuf:\"varint,1,opt,name=is_readonly,json=isReadonly,proto3\" json:\"is_readonly,omitempty\"`\n\t// The role to use to authenticate.\n\tRoleRid string `protobuf:\"bytes,2,opt,name=role_rid,json=roleRid,proto3\" json:\"role_rid,omitempty\"`\n\t// The minimum and maximum number of connections to use.\n\tMinConnections int32 `protobuf:\"varint,3,opt,name=min_connections,json=minConnections,proto3\" json:\"min_connections,omitempty\"`\n\tMaxConnections int32 `protobuf:\"varint,4,opt,name=max_connections,json=maxConnections,proto3\" json:\"max_connections,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *SQLConnectionPool) Reset() {\n\t*x = SQLConnectionPool{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SQLConnectionPool) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SQLConnectionPool) ProtoMessage() {}\n\nfunc (x *SQLConnectionPool) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SQLConnectionPool.ProtoReflect.Descriptor instead.\nfunc (*SQLConnectionPool) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *SQLConnectionPool) GetIsReadonly() bool {\n\tif x != nil {\n\t\treturn x.IsReadonly\n\t}\n\treturn false\n}\n\nfunc (x *SQLConnectionPool) GetRoleRid() string {\n\tif x != nil {\n\t\treturn x.RoleRid\n\t}\n\treturn \"\"\n}\n\nfunc (x *SQLConnectionPool) GetMinConnections() int32 {\n\tif x != nil {\n\t\treturn x.MinConnections\n\t}\n\treturn 0\n}\n\nfunc (x *SQLConnectionPool) GetMaxConnections() int32 {\n\tif x != nil {\n\t\treturn x.MaxConnections\n\t}\n\treturn 0\n}\n\ntype RedisCluster struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this cluster.\n\tRid       string           `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tServers   []*RedisServer   `protobuf:\"bytes,2,rep,name=servers,proto3\" json:\"servers,omitempty\"`\n\tDatabases []*RedisDatabase `protobuf:\"bytes,3,rep,name=databases,proto3\" json:\"databases,omitempty\"`\n\t// If true, the runtime will use an in-memory Redis implementation\n\t// instead of connecting to the configured servers.\n\tInMemory      bool `protobuf:\"varint,4,opt,name=in_memory,json=inMemory,proto3\" json:\"in_memory,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RedisCluster) Reset() {\n\t*x = RedisCluster{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RedisCluster) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RedisCluster) ProtoMessage() {}\n\nfunc (x *RedisCluster) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RedisCluster.ProtoReflect.Descriptor instead.\nfunc (*RedisCluster) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *RedisCluster) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisCluster) GetServers() []*RedisServer {\n\tif x != nil {\n\t\treturn x.Servers\n\t}\n\treturn nil\n}\n\nfunc (x *RedisCluster) GetDatabases() []*RedisDatabase {\n\tif x != nil {\n\t\treturn x.Databases\n\t}\n\treturn nil\n}\n\nfunc (x *RedisCluster) GetInMemory() bool {\n\tif x != nil {\n\t\treturn x.InMemory\n\t}\n\treturn false\n}\n\ntype RedisServer struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this server.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// Host is the host to connect to.\n\t// Valid formats are \"hostname\", \"hostname:port\", and \"/path/to/unix.socket\".\n\tHost string     `protobuf:\"bytes,2,opt,name=host,proto3\" json:\"host,omitempty\"`\n\tKind ServerKind `protobuf:\"varint,3,opt,name=kind,proto3,enum=encore.runtime.v1.ServerKind\" json:\"kind,omitempty\"`\n\t// TLS configuration to use when connecting.\n\t// If nil, TLS is not used.\n\tTlsConfig     *TLSConfig `protobuf:\"bytes,4,opt,name=tls_config,json=tlsConfig,proto3,oneof\" json:\"tls_config,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RedisServer) Reset() {\n\t*x = RedisServer{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RedisServer) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RedisServer) ProtoMessage() {}\n\nfunc (x *RedisServer) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RedisServer.ProtoReflect.Descriptor instead.\nfunc (*RedisServer) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *RedisServer) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisServer) GetHost() string {\n\tif x != nil {\n\t\treturn x.Host\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisServer) GetKind() ServerKind {\n\tif x != nil {\n\t\treturn x.Kind\n\t}\n\treturn ServerKind_SERVER_KIND_UNSPECIFIED\n}\n\nfunc (x *RedisServer) GetTlsConfig() *TLSConfig {\n\tif x != nil {\n\t\treturn x.TlsConfig\n\t}\n\treturn nil\n}\n\ntype RedisConnectionPool struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Whether this connection pool is for read-only servers.\n\tIsReadonly bool `protobuf:\"varint,1,opt,name=is_readonly,json=isReadonly,proto3\" json:\"is_readonly,omitempty\"`\n\t// The role to use to authenticate.\n\tRoleRid string `protobuf:\"bytes,2,opt,name=role_rid,json=roleRid,proto3\" json:\"role_rid,omitempty\"`\n\t// The minimum and maximum number of connections to use.\n\tMinConnections int32 `protobuf:\"varint,3,opt,name=min_connections,json=minConnections,proto3\" json:\"min_connections,omitempty\"`\n\tMaxConnections int32 `protobuf:\"varint,4,opt,name=max_connections,json=maxConnections,proto3\" json:\"max_connections,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *RedisConnectionPool) Reset() {\n\t*x = RedisConnectionPool{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RedisConnectionPool) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RedisConnectionPool) ProtoMessage() {}\n\nfunc (x *RedisConnectionPool) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RedisConnectionPool.ProtoReflect.Descriptor instead.\nfunc (*RedisConnectionPool) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *RedisConnectionPool) GetIsReadonly() bool {\n\tif x != nil {\n\t\treturn x.IsReadonly\n\t}\n\treturn false\n}\n\nfunc (x *RedisConnectionPool) GetRoleRid() string {\n\tif x != nil {\n\t\treturn x.RoleRid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisConnectionPool) GetMinConnections() int32 {\n\tif x != nil {\n\t\treturn x.MinConnections\n\t}\n\treturn 0\n}\n\nfunc (x *RedisConnectionPool) GetMaxConnections() int32 {\n\tif x != nil {\n\t\treturn x.MaxConnections\n\t}\n\treturn 0\n}\n\ntype RedisRole struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this role.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The client cert to use to authenticate, if any.\n\tClientCertRid *string `protobuf:\"bytes,2,opt,name=client_cert_rid,json=clientCertRid,proto3,oneof\" json:\"client_cert_rid,omitempty\"`\n\t// How to authenticate with Redis.\n\t// If unset, no authentication is used.\n\t//\n\t// Types that are valid to be assigned to Auth:\n\t//\n\t//\t*RedisRole_Acl\n\t//\t*RedisRole_AuthString\n\tAuth          isRedisRole_Auth `protobuf_oneof:\"auth\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RedisRole) Reset() {\n\t*x = RedisRole{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RedisRole) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RedisRole) ProtoMessage() {}\n\nfunc (x *RedisRole) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RedisRole.ProtoReflect.Descriptor instead.\nfunc (*RedisRole) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *RedisRole) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisRole) GetClientCertRid() string {\n\tif x != nil && x.ClientCertRid != nil {\n\t\treturn *x.ClientCertRid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisRole) GetAuth() isRedisRole_Auth {\n\tif x != nil {\n\t\treturn x.Auth\n\t}\n\treturn nil\n}\n\nfunc (x *RedisRole) GetAcl() *RedisRole_AuthACL {\n\tif x != nil {\n\t\tif x, ok := x.Auth.(*RedisRole_Acl); ok {\n\t\t\treturn x.Acl\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *RedisRole) GetAuthString() *SecretData {\n\tif x != nil {\n\t\tif x, ok := x.Auth.(*RedisRole_AuthString); ok {\n\t\t\treturn x.AuthString\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isRedisRole_Auth interface {\n\tisRedisRole_Auth()\n}\n\ntype RedisRole_Acl struct {\n\tAcl *RedisRole_AuthACL `protobuf:\"bytes,10,opt,name=acl,proto3,oneof\"` // Redis ACL\n}\n\ntype RedisRole_AuthString struct {\n\tAuthString *SecretData `protobuf:\"bytes,11,opt,name=auth_string,json=authString,proto3,oneof\"` // Redis AUTH string\n}\n\nfunc (*RedisRole_Acl) isRedisRole_Auth() {}\n\nfunc (*RedisRole_AuthString) isRedisRole_Auth() {}\n\ntype RedisDatabase struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Unique resource id for this database.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The encore name of the database.\n\tEncoreName string `protobuf:\"bytes,2,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The database index to use, [0-15].\n\tDatabaseIdx int32 `protobuf:\"varint,3,opt,name=database_idx,json=databaseIdx,proto3\" json:\"database_idx,omitempty\"`\n\t// KeyPrefix specifies a prefix to add to all cache keys\n\t// for this database. It exists to enable multiple cache clusters\n\t// to use the same physical Redis database for local development\n\t// without having to coordinate and persist database index ids.\n\tKeyPrefix *string `protobuf:\"bytes,4,opt,name=key_prefix,json=keyPrefix,proto3,oneof\" json:\"key_prefix,omitempty\"`\n\t// Connection pools to use for connecting to the database.\n\tConnPools     []*RedisConnectionPool `protobuf:\"bytes,5,rep,name=conn_pools,json=connPools,proto3\" json:\"conn_pools,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RedisDatabase) Reset() {\n\t*x = RedisDatabase{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RedisDatabase) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RedisDatabase) ProtoMessage() {}\n\nfunc (x *RedisDatabase) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RedisDatabase.ProtoReflect.Descriptor instead.\nfunc (*RedisDatabase) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *RedisDatabase) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisDatabase) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisDatabase) GetDatabaseIdx() int32 {\n\tif x != nil {\n\t\treturn x.DatabaseIdx\n\t}\n\treturn 0\n}\n\nfunc (x *RedisDatabase) GetKeyPrefix() string {\n\tif x != nil && x.KeyPrefix != nil {\n\t\treturn *x.KeyPrefix\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisDatabase) GetConnPools() []*RedisConnectionPool {\n\tif x != nil {\n\t\treturn x.ConnPools\n\t}\n\treturn nil\n}\n\ntype AppSecret struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this secret.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The encore name of the secret.\n\tEncoreName string `protobuf:\"bytes,2,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The secret data.\n\tData          *SecretData `protobuf:\"bytes,3,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AppSecret) Reset() {\n\t*x = AppSecret{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AppSecret) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AppSecret) ProtoMessage() {}\n\nfunc (x *AppSecret) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AppSecret.ProtoReflect.Descriptor instead.\nfunc (*AppSecret) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *AppSecret) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *AppSecret) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *AppSecret) GetData() *SecretData {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\ntype PubSubCluster struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this cluster.\n\tRid           string                `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tTopics        []*PubSubTopic        `protobuf:\"bytes,2,rep,name=topics,proto3\" json:\"topics,omitempty\"`\n\tSubscriptions []*PubSubSubscription `protobuf:\"bytes,3,rep,name=subscriptions,proto3\" json:\"subscriptions,omitempty\"`\n\t// Types that are valid to be assigned to Provider:\n\t//\n\t//\t*PubSubCluster_Encore\n\t//\t*PubSubCluster_Aws\n\t//\t*PubSubCluster_Gcp\n\t//\t*PubSubCluster_Azure\n\t//\t*PubSubCluster_Nsq\n\tProvider      isPubSubCluster_Provider `protobuf_oneof:\"provider\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubCluster) Reset() {\n\t*x = PubSubCluster{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubCluster) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubCluster) ProtoMessage() {}\n\nfunc (x *PubSubCluster) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubCluster.ProtoReflect.Descriptor instead.\nfunc (*PubSubCluster) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *PubSubCluster) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubCluster) GetTopics() []*PubSubTopic {\n\tif x != nil {\n\t\treturn x.Topics\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetSubscriptions() []*PubSubSubscription {\n\tif x != nil {\n\t\treturn x.Subscriptions\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetProvider() isPubSubCluster_Provider {\n\tif x != nil {\n\t\treturn x.Provider\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetEncore() *PubSubCluster_EncoreCloud {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*PubSubCluster_Encore); ok {\n\t\t\treturn x.Encore\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetAws() *PubSubCluster_AWSSqsSns {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*PubSubCluster_Aws); ok {\n\t\t\treturn x.Aws\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetGcp() *PubSubCluster_GCPPubSub {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*PubSubCluster_Gcp); ok {\n\t\t\treturn x.Gcp\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetAzure() *PubSubCluster_AzureServiceBus {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*PubSubCluster_Azure); ok {\n\t\t\treturn x.Azure\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubCluster) GetNsq() *PubSubCluster_NSQ {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*PubSubCluster_Nsq); ok {\n\t\t\treturn x.Nsq\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isPubSubCluster_Provider interface {\n\tisPubSubCluster_Provider()\n}\n\ntype PubSubCluster_Encore struct {\n\tEncore *PubSubCluster_EncoreCloud `protobuf:\"bytes,5,opt,name=encore,proto3,oneof\"`\n}\n\ntype PubSubCluster_Aws struct {\n\tAws *PubSubCluster_AWSSqsSns `protobuf:\"bytes,6,opt,name=aws,proto3,oneof\"`\n}\n\ntype PubSubCluster_Gcp struct {\n\tGcp *PubSubCluster_GCPPubSub `protobuf:\"bytes,7,opt,name=gcp,proto3,oneof\"`\n}\n\ntype PubSubCluster_Azure struct {\n\tAzure *PubSubCluster_AzureServiceBus `protobuf:\"bytes,8,opt,name=azure,proto3,oneof\"`\n}\n\ntype PubSubCluster_Nsq struct {\n\tNsq *PubSubCluster_NSQ `protobuf:\"bytes,9,opt,name=nsq,proto3,oneof\"`\n}\n\nfunc (*PubSubCluster_Encore) isPubSubCluster_Provider() {}\n\nfunc (*PubSubCluster_Aws) isPubSubCluster_Provider() {}\n\nfunc (*PubSubCluster_Gcp) isPubSubCluster_Provider() {}\n\nfunc (*PubSubCluster_Azure) isPubSubCluster_Provider() {}\n\nfunc (*PubSubCluster_Nsq) isPubSubCluster_Provider() {}\n\ntype PubSubTopic struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this topic.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The encore name of the topic.\n\tEncoreName string `protobuf:\"bytes,2,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The cloud name of the topic.\n\tCloudName string `protobuf:\"bytes,3,opt,name=cloud_name,json=cloudName,proto3\" json:\"cloud_name,omitempty\"`\n\t// The delivery guarantee.\n\tDeliveryGuarantee PubSubTopic_DeliveryGuarantee `protobuf:\"varint,4,opt,name=delivery_guarantee,json=deliveryGuarantee,proto3,enum=encore.runtime.v1.PubSubTopic_DeliveryGuarantee\" json:\"delivery_guarantee,omitempty\"`\n\t// Optional ordering attribute. Specifies the attribute name\n\t// to use for message ordering.\n\tOrderingAttr *string `protobuf:\"bytes,5,opt,name=ordering_attr,json=orderingAttr,proto3,oneof\" json:\"ordering_attr,omitempty\"`\n\t// Provider-specific configuration.\n\t// Not all providers require this, but it must always be set\n\t// for the providers that are present.\n\t//\n\t// Types that are valid to be assigned to ProviderConfig:\n\t//\n\t//\t*PubSubTopic_GcpConfig\n\tProviderConfig isPubSubTopic_ProviderConfig `protobuf_oneof:\"provider_config\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopic) Reset() {\n\t*x = PubSubTopic{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopic) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopic) ProtoMessage() {}\n\nfunc (x *PubSubTopic) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopic.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopic) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *PubSubTopic) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetCloudName() string {\n\tif x != nil {\n\t\treturn x.CloudName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetDeliveryGuarantee() PubSubTopic_DeliveryGuarantee {\n\tif x != nil {\n\t\treturn x.DeliveryGuarantee\n\t}\n\treturn PubSubTopic_DELIVERY_GUARANTEE_UNSPECIFIED\n}\n\nfunc (x *PubSubTopic) GetOrderingAttr() string {\n\tif x != nil && x.OrderingAttr != nil {\n\t\treturn *x.OrderingAttr\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubTopic) GetProviderConfig() isPubSubTopic_ProviderConfig {\n\tif x != nil {\n\t\treturn x.ProviderConfig\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubTopic) GetGcpConfig() *PubSubTopic_GCPConfig {\n\tif x != nil {\n\t\tif x, ok := x.ProviderConfig.(*PubSubTopic_GcpConfig); ok {\n\t\t\treturn x.GcpConfig\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isPubSubTopic_ProviderConfig interface {\n\tisPubSubTopic_ProviderConfig()\n}\n\ntype PubSubTopic_GcpConfig struct {\n\tGcpConfig *PubSubTopic_GCPConfig `protobuf:\"bytes,10,opt,name=gcp_config,json=gcpConfig,proto3,oneof\"` // Null: no provider-specific configuration.\n}\n\nfunc (*PubSubTopic_GcpConfig) isPubSubTopic_ProviderConfig() {}\n\ntype PubSubSubscription struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this subscription.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The encore name of the topic this subscription is for.\n\tTopicEncoreName string `protobuf:\"bytes,2,opt,name=topic_encore_name,json=topicEncoreName,proto3\" json:\"topic_encore_name,omitempty\"`\n\t// The encore name of the subscription.\n\tSubscriptionEncoreName string `protobuf:\"bytes,3,opt,name=subscription_encore_name,json=subscriptionEncoreName,proto3\" json:\"subscription_encore_name,omitempty\"`\n\t// The cloud name of the subscription.\n\tTopicCloudName string `protobuf:\"bytes,4,opt,name=topic_cloud_name,json=topicCloudName,proto3\" json:\"topic_cloud_name,omitempty\"`\n\t// The cloud name of the subscription.\n\tSubscriptionCloudName string `protobuf:\"bytes,5,opt,name=subscription_cloud_name,json=subscriptionCloudName,proto3\" json:\"subscription_cloud_name,omitempty\"`\n\t// If true the application will not actively subscribe but wait\n\t// for incoming messages to be pushed to it.\n\tPushOnly bool `protobuf:\"varint,6,opt,name=push_only,json=pushOnly,proto3\" json:\"push_only,omitempty\"`\n\t// Subscription-specific provider configuration.\n\t// Not all providers require this, but it must always be set\n\t// for the providers that are present.\n\t//\n\t// Types that are valid to be assigned to ProviderConfig:\n\t//\n\t//\t*PubSubSubscription_GcpConfig\n\tProviderConfig isPubSubSubscription_ProviderConfig `protobuf_oneof:\"provider_config\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *PubSubSubscription) Reset() {\n\t*x = PubSubSubscription{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubSubscription) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubSubscription) ProtoMessage() {}\n\nfunc (x *PubSubSubscription) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubSubscription.ProtoReflect.Descriptor instead.\nfunc (*PubSubSubscription) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{16}\n}\n\nfunc (x *PubSubSubscription) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription) GetTopicEncoreName() string {\n\tif x != nil {\n\t\treturn x.TopicEncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription) GetSubscriptionEncoreName() string {\n\tif x != nil {\n\t\treturn x.SubscriptionEncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription) GetTopicCloudName() string {\n\tif x != nil {\n\t\treturn x.TopicCloudName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription) GetSubscriptionCloudName() string {\n\tif x != nil {\n\t\treturn x.SubscriptionCloudName\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription) GetPushOnly() bool {\n\tif x != nil {\n\t\treturn x.PushOnly\n\t}\n\treturn false\n}\n\nfunc (x *PubSubSubscription) GetProviderConfig() isPubSubSubscription_ProviderConfig {\n\tif x != nil {\n\t\treturn x.ProviderConfig\n\t}\n\treturn nil\n}\n\nfunc (x *PubSubSubscription) GetGcpConfig() *PubSubSubscription_GCPConfig {\n\tif x != nil {\n\t\tif x, ok := x.ProviderConfig.(*PubSubSubscription_GcpConfig); ok {\n\t\t\treturn x.GcpConfig\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isPubSubSubscription_ProviderConfig interface {\n\tisPubSubSubscription_ProviderConfig()\n}\n\ntype PubSubSubscription_GcpConfig struct {\n\tGcpConfig *PubSubSubscription_GCPConfig `protobuf:\"bytes,10,opt,name=gcp_config,json=gcpConfig,proto3,oneof\"` // Null: no provider-specific configuration.\n}\n\nfunc (*PubSubSubscription_GcpConfig) isPubSubSubscription_ProviderConfig() {}\n\ntype BucketCluster struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this cluster.\n\tRid     string    `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tBuckets []*Bucket `protobuf:\"bytes,2,rep,name=buckets,proto3\" json:\"buckets,omitempty\"`\n\t// Types that are valid to be assigned to Provider:\n\t//\n\t//\t*BucketCluster_S3_\n\t//\t*BucketCluster_Gcs\n\tProvider      isBucketCluster_Provider `protobuf_oneof:\"provider\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketCluster) Reset() {\n\t*x = BucketCluster{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketCluster) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketCluster) ProtoMessage() {}\n\nfunc (x *BucketCluster) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketCluster.ProtoReflect.Descriptor instead.\nfunc (*BucketCluster) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17}\n}\n\nfunc (x *BucketCluster) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster) GetBuckets() []*Bucket {\n\tif x != nil {\n\t\treturn x.Buckets\n\t}\n\treturn nil\n}\n\nfunc (x *BucketCluster) GetProvider() isBucketCluster_Provider {\n\tif x != nil {\n\t\treturn x.Provider\n\t}\n\treturn nil\n}\n\nfunc (x *BucketCluster) GetS3() *BucketCluster_S3 {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*BucketCluster_S3_); ok {\n\t\t\treturn x.S3\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *BucketCluster) GetGcs() *BucketCluster_GCS {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*BucketCluster_Gcs); ok {\n\t\t\treturn x.Gcs\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isBucketCluster_Provider interface {\n\tisBucketCluster_Provider()\n}\n\ntype BucketCluster_S3_ struct {\n\tS3 *BucketCluster_S3 `protobuf:\"bytes,10,opt,name=s3,proto3,oneof\"`\n}\n\ntype BucketCluster_Gcs struct {\n\tGcs *BucketCluster_GCS `protobuf:\"bytes,11,opt,name=gcs,proto3,oneof\"`\n}\n\nfunc (*BucketCluster_S3_) isBucketCluster_Provider() {}\n\nfunc (*BucketCluster_Gcs) isBucketCluster_Provider() {}\n\ntype Bucket struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this bucket.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The encore name of the bucket.\n\tEncoreName string `protobuf:\"bytes,2,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The cloud name of the bucket.\n\tCloudName string `protobuf:\"bytes,3,opt,name=cloud_name,json=cloudName,proto3\" json:\"cloud_name,omitempty\"`\n\t// Optional key prefix to prepend to all bucket keys.\n\t//\n\t// Note: make sure it ends with a slash (\"/\") if you want\n\t// to group objects within a certain folder.\n\tKeyPrefix *string `protobuf:\"bytes,4,opt,name=key_prefix,json=keyPrefix,proto3,oneof\" json:\"key_prefix,omitempty\"`\n\t// Public base URL for accessing objects in this bucket.\n\t// Must be set for public buckets.\n\tPublicBaseUrl *string `protobuf:\"bytes,5,opt,name=public_base_url,json=publicBaseUrl,proto3,oneof\" json:\"public_base_url,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Bucket) Reset() {\n\t*x = Bucket{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Bucket) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Bucket) ProtoMessage() {}\n\nfunc (x *Bucket) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Bucket.ProtoReflect.Descriptor instead.\nfunc (*Bucket) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{18}\n}\n\nfunc (x *Bucket) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *Bucket) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Bucket) GetCloudName() string {\n\tif x != nil {\n\t\treturn x.CloudName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Bucket) GetKeyPrefix() string {\n\tif x != nil && x.KeyPrefix != nil {\n\t\treturn *x.KeyPrefix\n\t}\n\treturn \"\"\n}\n\nfunc (x *Bucket) GetPublicBaseUrl() string {\n\tif x != nil && x.PublicBaseUrl != nil {\n\t\treturn *x.PublicBaseUrl\n\t}\n\treturn \"\"\n}\n\ntype Gateway struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique id for this resource.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// The encore name of the gateway.\n\tEncoreName string `protobuf:\"bytes,2,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The base url for reaching this gateway, for returning to the application\n\t// via e.g. the metadata APIs.\n\tBaseUrl string `protobuf:\"bytes,3,opt,name=base_url,json=baseUrl,proto3\" json:\"base_url,omitempty\"`\n\t// The hostnames this gateway accepts requests for.\n\tHostnames []string `protobuf:\"bytes,4,rep,name=hostnames,proto3\" json:\"hostnames,omitempty\"`\n\t// CORS is the CORS configuration for this gateway.\n\tCors          *Gateway_CORS `protobuf:\"bytes,5,opt,name=cors,proto3\" json:\"cors,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Gateway) Reset() {\n\t*x = Gateway{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Gateway) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Gateway) ProtoMessage() {}\n\nfunc (x *Gateway) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Gateway.ProtoReflect.Descriptor instead.\nfunc (*Gateway) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{19}\n}\n\nfunc (x *Gateway) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *Gateway) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Gateway) GetBaseUrl() string {\n\tif x != nil {\n\t\treturn x.BaseUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *Gateway) GetHostnames() []string {\n\tif x != nil {\n\t\treturn x.Hostnames\n\t}\n\treturn nil\n}\n\nfunc (x *Gateway) GetCors() *Gateway_CORS {\n\tif x != nil {\n\t\treturn x.Cors\n\t}\n\treturn nil\n}\n\ntype Infrastructure_Credentials struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tClientCerts   []*ClientCert          `protobuf:\"bytes,1,rep,name=client_certs,json=clientCerts,proto3\" json:\"client_certs,omitempty\"`\n\tSqlRoles      []*SQLRole             `protobuf:\"bytes,2,rep,name=sql_roles,json=sqlRoles,proto3\" json:\"sql_roles,omitempty\"`\n\tRedisRoles    []*RedisRole           `protobuf:\"bytes,3,rep,name=redis_roles,json=redisRoles,proto3\" json:\"redis_roles,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Infrastructure_Credentials) Reset() {\n\t*x = Infrastructure_Credentials{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Infrastructure_Credentials) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Infrastructure_Credentials) ProtoMessage() {}\n\nfunc (x *Infrastructure_Credentials) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Infrastructure_Credentials.ProtoReflect.Descriptor instead.\nfunc (*Infrastructure_Credentials) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0, 0}\n}\n\nfunc (x *Infrastructure_Credentials) GetClientCerts() []*ClientCert {\n\tif x != nil {\n\t\treturn x.ClientCerts\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Credentials) GetSqlRoles() []*SQLRole {\n\tif x != nil {\n\t\treturn x.SqlRoles\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Credentials) GetRedisRoles() []*RedisRole {\n\tif x != nil {\n\t\treturn x.RedisRoles\n\t}\n\treturn nil\n}\n\ntype Infrastructure_Resources struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tGateways       []*Gateway             `protobuf:\"bytes,1,rep,name=gateways,proto3\" json:\"gateways,omitempty\"`\n\tSqlClusters    []*SQLCluster          `protobuf:\"bytes,2,rep,name=sql_clusters,json=sqlClusters,proto3\" json:\"sql_clusters,omitempty\"`\n\tPubsubClusters []*PubSubCluster       `protobuf:\"bytes,3,rep,name=pubsub_clusters,json=pubsubClusters,proto3\" json:\"pubsub_clusters,omitempty\"`\n\tRedisClusters  []*RedisCluster        `protobuf:\"bytes,4,rep,name=redis_clusters,json=redisClusters,proto3\" json:\"redis_clusters,omitempty\"`\n\tAppSecrets     []*AppSecret           `protobuf:\"bytes,5,rep,name=app_secrets,json=appSecrets,proto3\" json:\"app_secrets,omitempty\"`\n\tBucketClusters []*BucketCluster       `protobuf:\"bytes,6,rep,name=bucket_clusters,json=bucketClusters,proto3\" json:\"bucket_clusters,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *Infrastructure_Resources) Reset() {\n\t*x = Infrastructure_Resources{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Infrastructure_Resources) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Infrastructure_Resources) ProtoMessage() {}\n\nfunc (x *Infrastructure_Resources) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Infrastructure_Resources.ProtoReflect.Descriptor instead.\nfunc (*Infrastructure_Resources) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{0, 1}\n}\n\nfunc (x *Infrastructure_Resources) GetGateways() []*Gateway {\n\tif x != nil {\n\t\treturn x.Gateways\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Resources) GetSqlClusters() []*SQLCluster {\n\tif x != nil {\n\t\treturn x.SqlClusters\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Resources) GetPubsubClusters() []*PubSubCluster {\n\tif x != nil {\n\t\treturn x.PubsubClusters\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Resources) GetRedisClusters() []*RedisCluster {\n\tif x != nil {\n\t\treturn x.RedisClusters\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Resources) GetAppSecrets() []*AppSecret {\n\tif x != nil {\n\t\treturn x.AppSecrets\n\t}\n\treturn nil\n}\n\nfunc (x *Infrastructure_Resources) GetBucketClusters() []*BucketCluster {\n\tif x != nil {\n\t\treturn x.BucketClusters\n\t}\n\treturn nil\n}\n\ntype RedisRole_AuthACL struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tUsername      string                 `protobuf:\"bytes,1,opt,name=username,proto3\" json:\"username,omitempty\"`\n\tPassword      *SecretData            `protobuf:\"bytes,2,opt,name=password,proto3\" json:\"password,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RedisRole_AuthACL) Reset() {\n\t*x = RedisRole_AuthACL{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RedisRole_AuthACL) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RedisRole_AuthACL) ProtoMessage() {}\n\nfunc (x *RedisRole_AuthACL) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RedisRole_AuthACL.ProtoReflect.Descriptor instead.\nfunc (*RedisRole_AuthACL) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{11, 0}\n}\n\nfunc (x *RedisRole_AuthACL) GetUsername() string {\n\tif x != nil {\n\t\treturn x.Username\n\t}\n\treturn \"\"\n}\n\nfunc (x *RedisRole_AuthACL) GetPassword() *SecretData {\n\tif x != nil {\n\t\treturn x.Password\n\t}\n\treturn nil\n}\n\ntype PubSubCluster_EncoreCloud struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubCluster_EncoreCloud) Reset() {\n\t*x = PubSubCluster_EncoreCloud{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubCluster_EncoreCloud) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubCluster_EncoreCloud) ProtoMessage() {}\n\nfunc (x *PubSubCluster_EncoreCloud) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubCluster_EncoreCloud.ProtoReflect.Descriptor instead.\nfunc (*PubSubCluster_EncoreCloud) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 0}\n}\n\ntype PubSubCluster_AWSSqsSns struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubCluster_AWSSqsSns) Reset() {\n\t*x = PubSubCluster_AWSSqsSns{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubCluster_AWSSqsSns) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubCluster_AWSSqsSns) ProtoMessage() {}\n\nfunc (x *PubSubCluster_AWSSqsSns) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubCluster_AWSSqsSns.ProtoReflect.Descriptor instead.\nfunc (*PubSubCluster_AWSSqsSns) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 1}\n}\n\ntype PubSubCluster_GCPPubSub struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubCluster_GCPPubSub) Reset() {\n\t*x = PubSubCluster_GCPPubSub{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubCluster_GCPPubSub) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubCluster_GCPPubSub) ProtoMessage() {}\n\nfunc (x *PubSubCluster_GCPPubSub) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubCluster_GCPPubSub.ProtoReflect.Descriptor instead.\nfunc (*PubSubCluster_GCPPubSub) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 2}\n}\n\ntype PubSubCluster_NSQ struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The hosts to connect to NSQ. Must be non-empty.\n\tHosts         []string `protobuf:\"bytes,1,rep,name=hosts,proto3\" json:\"hosts,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubCluster_NSQ) Reset() {\n\t*x = PubSubCluster_NSQ{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[26]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubCluster_NSQ) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubCluster_NSQ) ProtoMessage() {}\n\nfunc (x *PubSubCluster_NSQ) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[26]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubCluster_NSQ.ProtoReflect.Descriptor instead.\nfunc (*PubSubCluster_NSQ) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 3}\n}\n\nfunc (x *PubSubCluster_NSQ) GetHosts() []string {\n\tif x != nil {\n\t\treturn x.Hosts\n\t}\n\treturn nil\n}\n\ntype PubSubCluster_AzureServiceBus struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tNamespace     string                 `protobuf:\"bytes,1,opt,name=namespace,proto3\" json:\"namespace,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubCluster_AzureServiceBus) Reset() {\n\t*x = PubSubCluster_AzureServiceBus{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[27]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubCluster_AzureServiceBus) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubCluster_AzureServiceBus) ProtoMessage() {}\n\nfunc (x *PubSubCluster_AzureServiceBus) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[27]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubCluster_AzureServiceBus.ProtoReflect.Descriptor instead.\nfunc (*PubSubCluster_AzureServiceBus) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{14, 4}\n}\n\nfunc (x *PubSubCluster_AzureServiceBus) GetNamespace() string {\n\tif x != nil {\n\t\treturn x.Namespace\n\t}\n\treturn \"\"\n}\n\ntype PubSubTopic_GCPConfig struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The GCP project id where the topic exists.\n\tProjectId     string `protobuf:\"bytes,1,opt,name=project_id,json=projectId,proto3\" json:\"project_id,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PubSubTopic_GCPConfig) Reset() {\n\t*x = PubSubTopic_GCPConfig{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[28]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubTopic_GCPConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubTopic_GCPConfig) ProtoMessage() {}\n\nfunc (x *PubSubTopic_GCPConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[28]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubTopic_GCPConfig.ProtoReflect.Descriptor instead.\nfunc (*PubSubTopic_GCPConfig) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{15, 0}\n}\n\nfunc (x *PubSubTopic_GCPConfig) GetProjectId() string {\n\tif x != nil {\n\t\treturn x.ProjectId\n\t}\n\treturn \"\"\n}\n\ntype PubSubSubscription_GCPConfig struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The GCP project id where the subscription exists.\n\tProjectId string `protobuf:\"bytes,1,opt,name=project_id,json=projectId,proto3\" json:\"project_id,omitempty\"`\n\t// The service account used to authenticate messages being delivered over push.\n\t// If unset, pushes are rejected.\n\tPushServiceAccount *string `protobuf:\"bytes,2,opt,name=push_service_account,json=pushServiceAccount,proto3,oneof\" json:\"push_service_account,omitempty\"`\n\t// The audience to use when validating JWTs delivered over push.\n\t// If set, the JWT audience claim must match. If unset, any JWT audience is allowed.\n\tPushJwtAudience *string `protobuf:\"bytes,3,opt,name=push_jwt_audience,json=pushJwtAudience,proto3,oneof\" json:\"push_jwt_audience,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *PubSubSubscription_GCPConfig) Reset() {\n\t*x = PubSubSubscription_GCPConfig{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PubSubSubscription_GCPConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PubSubSubscription_GCPConfig) ProtoMessage() {}\n\nfunc (x *PubSubSubscription_GCPConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PubSubSubscription_GCPConfig.ProtoReflect.Descriptor instead.\nfunc (*PubSubSubscription_GCPConfig) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{16, 0}\n}\n\nfunc (x *PubSubSubscription_GCPConfig) GetProjectId() string {\n\tif x != nil {\n\t\treturn x.ProjectId\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription_GCPConfig) GetPushServiceAccount() string {\n\tif x != nil && x.PushServiceAccount != nil {\n\t\treturn *x.PushServiceAccount\n\t}\n\treturn \"\"\n}\n\nfunc (x *PubSubSubscription_GCPConfig) GetPushJwtAudience() string {\n\tif x != nil && x.PushJwtAudience != nil {\n\t\treturn *x.PushJwtAudience\n\t}\n\treturn \"\"\n}\n\ntype BucketCluster_S3 struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Region to connect to.\n\tRegion string `protobuf:\"bytes,1,opt,name=region,proto3\" json:\"region,omitempty\"`\n\t// Endpoint override, if any. Must be specified if using a non-standard AWS region.\n\tEndpoint *string `protobuf:\"bytes,2,opt,name=endpoint,proto3,oneof\" json:\"endpoint,omitempty\"`\n\t// Set these to use explicit credentials for this bucket,\n\t// as opposed to resolving using AWS's default credential chain.\n\tAccessKeyId     *string     `protobuf:\"bytes,3,opt,name=access_key_id,json=accessKeyId,proto3,oneof\" json:\"access_key_id,omitempty\"`\n\tSecretAccessKey *SecretData `protobuf:\"bytes,4,opt,name=secret_access_key,json=secretAccessKey,proto3,oneof\" json:\"secret_access_key,omitempty\"`\n\tunknownFields   protoimpl.UnknownFields\n\tsizeCache       protoimpl.SizeCache\n}\n\nfunc (x *BucketCluster_S3) Reset() {\n\t*x = BucketCluster_S3{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketCluster_S3) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketCluster_S3) ProtoMessage() {}\n\nfunc (x *BucketCluster_S3) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketCluster_S3.ProtoReflect.Descriptor instead.\nfunc (*BucketCluster_S3) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17, 0}\n}\n\nfunc (x *BucketCluster_S3) GetRegion() string {\n\tif x != nil {\n\t\treturn x.Region\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster_S3) GetEndpoint() string {\n\tif x != nil && x.Endpoint != nil {\n\t\treturn *x.Endpoint\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster_S3) GetAccessKeyId() string {\n\tif x != nil && x.AccessKeyId != nil {\n\t\treturn *x.AccessKeyId\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster_S3) GetSecretAccessKey() *SecretData {\n\tif x != nil {\n\t\treturn x.SecretAccessKey\n\t}\n\treturn nil\n}\n\ntype BucketCluster_GCS struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Endpoint override, if any. Defaults to https://storage.googleapis.com if unset.\n\tEndpoint *string `protobuf:\"bytes,1,opt,name=endpoint,proto3,oneof\" json:\"endpoint,omitempty\"`\n\t// Whether to connect anonymously or if a service account should be resolved.\n\tAnonymous bool `protobuf:\"varint,2,opt,name=anonymous,proto3\" json:\"anonymous,omitempty\"`\n\t// Additional options for signed URLs when running in local dev mode.\n\t// Only use with anonymous mode.\n\tLocalSign     *BucketCluster_GCS_LocalSignOptions `protobuf:\"bytes,3,opt,name=local_sign,json=localSign,proto3,oneof\" json:\"local_sign,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketCluster_GCS) Reset() {\n\t*x = BucketCluster_GCS{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[31]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketCluster_GCS) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketCluster_GCS) ProtoMessage() {}\n\nfunc (x *BucketCluster_GCS) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[31]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketCluster_GCS.ProtoReflect.Descriptor instead.\nfunc (*BucketCluster_GCS) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17, 1}\n}\n\nfunc (x *BucketCluster_GCS) GetEndpoint() string {\n\tif x != nil && x.Endpoint != nil {\n\t\treturn *x.Endpoint\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster_GCS) GetAnonymous() bool {\n\tif x != nil {\n\t\treturn x.Anonymous\n\t}\n\treturn false\n}\n\nfunc (x *BucketCluster_GCS) GetLocalSign() *BucketCluster_GCS_LocalSignOptions {\n\tif x != nil {\n\t\treturn x.LocalSign\n\t}\n\treturn nil\n}\n\ntype BucketCluster_GCS_LocalSignOptions struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Base prefix to use for presigned URLs.\n\tBaseUrl string `protobuf:\"bytes,1,opt,name=base_url,json=baseUrl,proto3\" json:\"base_url,omitempty\"`\n\t// Use these credentials to sign local URLs. Only pass dummy credentials\n\t// here, no actual secrets.\n\tAccessId      string `protobuf:\"bytes,2,opt,name=access_id,json=accessId,proto3\" json:\"access_id,omitempty\"`\n\tPrivateKey    string `protobuf:\"bytes,3,opt,name=private_key,json=privateKey,proto3\" json:\"private_key,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketCluster_GCS_LocalSignOptions) Reset() {\n\t*x = BucketCluster_GCS_LocalSignOptions{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[32]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketCluster_GCS_LocalSignOptions) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketCluster_GCS_LocalSignOptions) ProtoMessage() {}\n\nfunc (x *BucketCluster_GCS_LocalSignOptions) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[32]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketCluster_GCS_LocalSignOptions.ProtoReflect.Descriptor instead.\nfunc (*BucketCluster_GCS_LocalSignOptions) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{17, 1, 0}\n}\n\nfunc (x *BucketCluster_GCS_LocalSignOptions) GetBaseUrl() string {\n\tif x != nil {\n\t\treturn x.BaseUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster_GCS_LocalSignOptions) GetAccessId() string {\n\tif x != nil {\n\t\treturn x.AccessId\n\t}\n\treturn \"\"\n}\n\nfunc (x *BucketCluster_GCS_LocalSignOptions) GetPrivateKey() string {\n\tif x != nil {\n\t\treturn x.PrivateKey\n\t}\n\treturn \"\"\n}\n\n// CORS describes the CORS configuration for a gateway.\ntype Gateway_CORS struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tDebug bool                   `protobuf:\"varint,1,opt,name=debug,proto3\" json:\"debug,omitempty\"`\n\t// If true, causes Encore to respond to OPTIONS requests\n\t// without setting Access-Control-Allow-Credentials: true.\n\tDisableCredentials bool `protobuf:\"varint,2,opt,name=disable_credentials,json=disableCredentials,proto3\" json:\"disable_credentials,omitempty\"`\n\t// Specifies the allowed origins for requests that include credentials.\n\t// If a request is made from an Origin in this list\n\t// Encore responds with Access-Control-Allow-Origin: <Origin>.\n\t//\n\t// If disable_credentials is true this field is not used.\n\t//\n\t// Types that are valid to be assigned to AllowedOriginsWithCredentials:\n\t//\n\t//\t*Gateway_CORS_AllowedOrigins\n\t//\t*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials\n\tAllowedOriginsWithCredentials isGateway_CORS_AllowedOriginsWithCredentials `protobuf_oneof:\"allowed_origins_with_credentials\"`\n\t// Specifies the allowed origins for requests\n\t// that don't include credentials.\n\t//\n\t// The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n\t// or \"https://*-myapp.example.com\").\n\tAllowedOriginsWithoutCredentials *Gateway_CORSAllowedOrigins `protobuf:\"bytes,5,opt,name=allowed_origins_without_credentials,json=allowedOriginsWithoutCredentials,proto3\" json:\"allowed_origins_without_credentials,omitempty\"`\n\t// Specifies extra headers to allow, beyond\n\t// the default set always recognized by Encore.\n\t// As a special case, if the list contains \"*\" all headers are allowed.\n\tExtraAllowedHeaders []string `protobuf:\"bytes,6,rep,name=extra_allowed_headers,json=extraAllowedHeaders,proto3\" json:\"extra_allowed_headers,omitempty\"`\n\t// Specifies extra headers to expose, beyond\n\t// the default set always recognized by Encore.\n\t// As a special case, if the list contains \"*\" all headers are allowed.\n\tExtraExposedHeaders []string `protobuf:\"bytes,7,rep,name=extra_exposed_headers,json=extraExposedHeaders,proto3\" json:\"extra_exposed_headers,omitempty\"`\n\t// If true, allows requests to Encore apps running\n\t// on private networks from websites.\n\t// See: https://wicg.github.io/private-network-access/\n\tAllowPrivateNetworkAccess bool `protobuf:\"varint,8,opt,name=allow_private_network_access,json=allowPrivateNetworkAccess,proto3\" json:\"allow_private_network_access,omitempty\"`\n\tunknownFields             protoimpl.UnknownFields\n\tsizeCache                 protoimpl.SizeCache\n}\n\nfunc (x *Gateway_CORS) Reset() {\n\t*x = Gateway_CORS{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[33]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Gateway_CORS) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Gateway_CORS) ProtoMessage() {}\n\nfunc (x *Gateway_CORS) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[33]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Gateway_CORS.ProtoReflect.Descriptor instead.\nfunc (*Gateway_CORS) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{19, 0}\n}\n\nfunc (x *Gateway_CORS) GetDebug() bool {\n\tif x != nil {\n\t\treturn x.Debug\n\t}\n\treturn false\n}\n\nfunc (x *Gateway_CORS) GetDisableCredentials() bool {\n\tif x != nil {\n\t\treturn x.DisableCredentials\n\t}\n\treturn false\n}\n\nfunc (x *Gateway_CORS) GetAllowedOriginsWithCredentials() isGateway_CORS_AllowedOriginsWithCredentials {\n\tif x != nil {\n\t\treturn x.AllowedOriginsWithCredentials\n\t}\n\treturn nil\n}\n\nfunc (x *Gateway_CORS) GetAllowedOrigins() *Gateway_CORSAllowedOrigins {\n\tif x != nil {\n\t\tif x, ok := x.AllowedOriginsWithCredentials.(*Gateway_CORS_AllowedOrigins); ok {\n\t\t\treturn x.AllowedOrigins\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Gateway_CORS) GetUnsafeAllowAllOriginsWithCredentials() bool {\n\tif x != nil {\n\t\tif x, ok := x.AllowedOriginsWithCredentials.(*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials); ok {\n\t\t\treturn x.UnsafeAllowAllOriginsWithCredentials\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (x *Gateway_CORS) GetAllowedOriginsWithoutCredentials() *Gateway_CORSAllowedOrigins {\n\tif x != nil {\n\t\treturn x.AllowedOriginsWithoutCredentials\n\t}\n\treturn nil\n}\n\nfunc (x *Gateway_CORS) GetExtraAllowedHeaders() []string {\n\tif x != nil {\n\t\treturn x.ExtraAllowedHeaders\n\t}\n\treturn nil\n}\n\nfunc (x *Gateway_CORS) GetExtraExposedHeaders() []string {\n\tif x != nil {\n\t\treturn x.ExtraExposedHeaders\n\t}\n\treturn nil\n}\n\nfunc (x *Gateway_CORS) GetAllowPrivateNetworkAccess() bool {\n\tif x != nil {\n\t\treturn x.AllowPrivateNetworkAccess\n\t}\n\treturn false\n}\n\ntype isGateway_CORS_AllowedOriginsWithCredentials interface {\n\tisGateway_CORS_AllowedOriginsWithCredentials()\n}\n\ntype Gateway_CORS_AllowedOrigins struct {\n\tAllowedOrigins *Gateway_CORSAllowedOrigins `protobuf:\"bytes,3,opt,name=allowed_origins,json=allowedOrigins,proto3,oneof\"`\n}\n\ntype Gateway_CORS_UnsafeAllowAllOriginsWithCredentials struct {\n\tUnsafeAllowAllOriginsWithCredentials bool `protobuf:\"varint,4,opt,name=unsafe_allow_all_origins_with_credentials,json=unsafeAllowAllOriginsWithCredentials,proto3,oneof\"`\n}\n\nfunc (*Gateway_CORS_AllowedOrigins) isGateway_CORS_AllowedOriginsWithCredentials() {}\n\nfunc (*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials) isGateway_CORS_AllowedOriginsWithCredentials() {\n}\n\ntype Gateway_CORSAllowedOrigins struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The list of allowed origins.\n\t// The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n\t// or \"https://*-myapp.example.com\").\n\t//\n\t// The string \"*\" allows all origins, except for requests with credentials;\n\t// use CORS.unsafe_allow_unsafe_all_origins_with_credentials for that.\n\tAllowedOrigins []string `protobuf:\"bytes,1,rep,name=allowed_origins,json=allowedOrigins,proto3\" json:\"allowed_origins,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *Gateway_CORSAllowedOrigins) Reset() {\n\t*x = Gateway_CORSAllowedOrigins{}\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[34]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Gateway_CORSAllowedOrigins) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Gateway_CORSAllowedOrigins) ProtoMessage() {}\n\nfunc (x *Gateway_CORSAllowedOrigins) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_infra_proto_msgTypes[34]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Gateway_CORSAllowedOrigins.ProtoReflect.Descriptor instead.\nfunc (*Gateway_CORSAllowedOrigins) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_infra_proto_rawDescGZIP(), []int{19, 1}\n}\n\nfunc (x *Gateway_CORSAllowedOrigins) GetAllowedOrigins() []string {\n\tif x != nil {\n\t\treturn x.AllowedOrigins\n\t}\n\treturn nil\n}\n\nvar File_encore_runtime_v1_infra_proto protoreflect.FileDescriptor\n\nconst file_encore_runtime_v1_infra_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1dencore/runtime/v1/infra.proto\\x12\\x11encore.runtime.v1\\x1a\\\"encore/runtime/v1/secretdata.proto\\\"\\x9b\\x06\\n\" +\n\t\"\\x0eInfrastructure\\x12I\\n\" +\n\t\"\\tresources\\x18\\x01 \\x01(\\v2+.encore.runtime.v1.Infrastructure.ResourcesR\\tresources\\x12O\\n\" +\n\t\"\\vcredentials\\x18\\x02 \\x01(\\v2-.encore.runtime.v1.Infrastructure.CredentialsR\\vcredentials\\x1a\\xc7\\x01\\n\" +\n\t\"\\vCredentials\\x12@\\n\" +\n\t\"\\fclient_certs\\x18\\x01 \\x03(\\v2\\x1d.encore.runtime.v1.ClientCertR\\vclientCerts\\x127\\n\" +\n\t\"\\tsql_roles\\x18\\x02 \\x03(\\v2\\x1a.encore.runtime.v1.SQLRoleR\\bsqlRoles\\x12=\\n\" +\n\t\"\\vredis_roles\\x18\\x03 \\x03(\\v2\\x1c.encore.runtime.v1.RedisRoleR\\n\" +\n\t\"redisRoles\\x1a\\xa2\\x03\\n\" +\n\t\"\\tResources\\x126\\n\" +\n\t\"\\bgateways\\x18\\x01 \\x03(\\v2\\x1a.encore.runtime.v1.GatewayR\\bgateways\\x12@\\n\" +\n\t\"\\fsql_clusters\\x18\\x02 \\x03(\\v2\\x1d.encore.runtime.v1.SQLClusterR\\vsqlClusters\\x12I\\n\" +\n\t\"\\x0fpubsub_clusters\\x18\\x03 \\x03(\\v2 .encore.runtime.v1.PubSubClusterR\\x0epubsubClusters\\x12F\\n\" +\n\t\"\\x0eredis_clusters\\x18\\x04 \\x03(\\v2\\x1f.encore.runtime.v1.RedisClusterR\\rredisClusters\\x12=\\n\" +\n\t\"\\vapp_secrets\\x18\\x05 \\x03(\\v2\\x1c.encore.runtime.v1.AppSecretR\\n\" +\n\t\"appSecrets\\x12I\\n\" +\n\t\"\\x0fbucket_clusters\\x18\\x06 \\x03(\\v2 .encore.runtime.v1.BucketClusterR\\x0ebucketClusters\\\"\\x94\\x01\\n\" +\n\t\"\\n\" +\n\t\"SQLCluster\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x126\\n\" +\n\t\"\\aservers\\x18\\x02 \\x03(\\v2\\x1c.encore.runtime.v1.SQLServerR\\aservers\\x12<\\n\" +\n\t\"\\tdatabases\\x18\\x03 \\x03(\\v2\\x1e.encore.runtime.v1.SQLDatabaseR\\tdatabases\\\"\\xc8\\x01\\n\" +\n\t\"\\tTLSConfig\\x12)\\n\" +\n\t\"\\x0eserver_ca_cert\\x18\\x01 \\x01(\\tH\\x00R\\fserverCaCert\\x88\\x01\\x01\\x12I\\n\" +\n\t\"!disable_tls_hostname_verification\\x18\\x02 \\x01(\\bR\\x1edisableTlsHostnameVerification\\x122\\n\" +\n\t\"\\x15disable_ca_validation\\x18\\x03 \\x01(\\bR\\x13disableCaValidationB\\x11\\n\" +\n\t\"\\x0f_server_ca_cert\\\"\\xb5\\x01\\n\" +\n\t\"\\tSQLServer\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x12\\n\" +\n\t\"\\x04host\\x18\\x02 \\x01(\\tR\\x04host\\x121\\n\" +\n\t\"\\x04kind\\x18\\x03 \\x01(\\x0e2\\x1d.encore.runtime.v1.ServerKindR\\x04kind\\x12@\\n\" +\n\t\"\\n\" +\n\t\"tls_config\\x18\\x04 \\x01(\\v2\\x1c.encore.runtime.v1.TLSConfigH\\x00R\\ttlsConfig\\x88\\x01\\x01B\\r\\n\" +\n\t\"\\v_tls_config\\\"c\\n\" +\n\t\"\\n\" +\n\t\"ClientCert\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x12\\n\" +\n\t\"\\x04cert\\x18\\x02 \\x01(\\tR\\x04cert\\x12/\\n\" +\n\t\"\\x03key\\x18\\x03 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\x03key\\\"\\xb3\\x01\\n\" +\n\t\"\\aSQLRole\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1a\\n\" +\n\t\"\\busername\\x18\\x02 \\x01(\\tR\\busername\\x129\\n\" +\n\t\"\\bpassword\\x18\\x03 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\bpassword\\x12+\\n\" +\n\t\"\\x0fclient_cert_rid\\x18\\x04 \\x01(\\tH\\x00R\\rclientCertRid\\x88\\x01\\x01B\\x12\\n\" +\n\t\"\\x10_client_cert_rid\\\"\\xa4\\x01\\n\" +\n\t\"\\vSQLDatabase\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"cloud_name\\x18\\x03 \\x01(\\tR\\tcloudName\\x12C\\n\" +\n\t\"\\n\" +\n\t\"conn_pools\\x18\\x04 \\x03(\\v2$.encore.runtime.v1.SQLConnectionPoolR\\tconnPools\\\"\\xa1\\x01\\n\" +\n\t\"\\x11SQLConnectionPool\\x12\\x1f\\n\" +\n\t\"\\vis_readonly\\x18\\x01 \\x01(\\bR\\n\" +\n\t\"isReadonly\\x12\\x19\\n\" +\n\t\"\\brole_rid\\x18\\x02 \\x01(\\tR\\aroleRid\\x12'\\n\" +\n\t\"\\x0fmin_connections\\x18\\x03 \\x01(\\x05R\\x0eminConnections\\x12'\\n\" +\n\t\"\\x0fmax_connections\\x18\\x04 \\x01(\\x05R\\x0emaxConnections\\\"\\xb7\\x01\\n\" +\n\t\"\\fRedisCluster\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x128\\n\" +\n\t\"\\aservers\\x18\\x02 \\x03(\\v2\\x1e.encore.runtime.v1.RedisServerR\\aservers\\x12>\\n\" +\n\t\"\\tdatabases\\x18\\x03 \\x03(\\v2 .encore.runtime.v1.RedisDatabaseR\\tdatabases\\x12\\x1b\\n\" +\n\t\"\\tin_memory\\x18\\x04 \\x01(\\bR\\binMemory\\\"\\xb7\\x01\\n\" +\n\t\"\\vRedisServer\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x12\\n\" +\n\t\"\\x04host\\x18\\x02 \\x01(\\tR\\x04host\\x121\\n\" +\n\t\"\\x04kind\\x18\\x03 \\x01(\\x0e2\\x1d.encore.runtime.v1.ServerKindR\\x04kind\\x12@\\n\" +\n\t\"\\n\" +\n\t\"tls_config\\x18\\x04 \\x01(\\v2\\x1c.encore.runtime.v1.TLSConfigH\\x00R\\ttlsConfig\\x88\\x01\\x01B\\r\\n\" +\n\t\"\\v_tls_config\\\"\\xa3\\x01\\n\" +\n\t\"\\x13RedisConnectionPool\\x12\\x1f\\n\" +\n\t\"\\vis_readonly\\x18\\x01 \\x01(\\bR\\n\" +\n\t\"isReadonly\\x12\\x19\\n\" +\n\t\"\\brole_rid\\x18\\x02 \\x01(\\tR\\aroleRid\\x12'\\n\" +\n\t\"\\x0fmin_connections\\x18\\x03 \\x01(\\x05R\\x0eminConnections\\x12'\\n\" +\n\t\"\\x0fmax_connections\\x18\\x04 \\x01(\\x05R\\x0emaxConnections\\\"\\xc4\\x02\\n\" +\n\t\"\\tRedisRole\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12+\\n\" +\n\t\"\\x0fclient_cert_rid\\x18\\x02 \\x01(\\tH\\x01R\\rclientCertRid\\x88\\x01\\x01\\x128\\n\" +\n\t\"\\x03acl\\x18\\n\" +\n\t\" \\x01(\\v2$.encore.runtime.v1.RedisRole.AuthACLH\\x00R\\x03acl\\x12@\\n\" +\n\t\"\\vauth_string\\x18\\v \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataH\\x00R\\n\" +\n\t\"authString\\x1a`\\n\" +\n\t\"\\aAuthACL\\x12\\x1a\\n\" +\n\t\"\\busername\\x18\\x01 \\x01(\\tR\\busername\\x129\\n\" +\n\t\"\\bpassword\\x18\\x02 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\bpasswordB\\x06\\n\" +\n\t\"\\x04authB\\x12\\n\" +\n\t\"\\x10_client_cert_rid\\\"\\xdf\\x01\\n\" +\n\t\"\\rRedisDatabase\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12!\\n\" +\n\t\"\\fdatabase_idx\\x18\\x03 \\x01(\\x05R\\vdatabaseIdx\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"key_prefix\\x18\\x04 \\x01(\\tH\\x00R\\tkeyPrefix\\x88\\x01\\x01\\x12E\\n\" +\n\t\"\\n\" +\n\t\"conn_pools\\x18\\x05 \\x03(\\v2&.encore.runtime.v1.RedisConnectionPoolR\\tconnPoolsB\\r\\n\" +\n\t\"\\v_key_prefix\\\"q\\n\" +\n\t\"\\tAppSecret\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"encoreName\\x121\\n\" +\n\t\"\\x04data\\x18\\x03 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\x04data\\\"\\xf5\\x04\\n\" +\n\t\"\\rPubSubCluster\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x126\\n\" +\n\t\"\\x06topics\\x18\\x02 \\x03(\\v2\\x1e.encore.runtime.v1.PubSubTopicR\\x06topics\\x12K\\n\" +\n\t\"\\rsubscriptions\\x18\\x03 \\x03(\\v2%.encore.runtime.v1.PubSubSubscriptionR\\rsubscriptions\\x12F\\n\" +\n\t\"\\x06encore\\x18\\x05 \\x01(\\v2,.encore.runtime.v1.PubSubCluster.EncoreCloudH\\x00R\\x06encore\\x12>\\n\" +\n\t\"\\x03aws\\x18\\x06 \\x01(\\v2*.encore.runtime.v1.PubSubCluster.AWSSqsSnsH\\x00R\\x03aws\\x12>\\n\" +\n\t\"\\x03gcp\\x18\\a \\x01(\\v2*.encore.runtime.v1.PubSubCluster.GCPPubSubH\\x00R\\x03gcp\\x12H\\n\" +\n\t\"\\x05azure\\x18\\b \\x01(\\v20.encore.runtime.v1.PubSubCluster.AzureServiceBusH\\x00R\\x05azure\\x128\\n\" +\n\t\"\\x03nsq\\x18\\t \\x01(\\v2$.encore.runtime.v1.PubSubCluster.NSQH\\x00R\\x03nsq\\x1a\\r\\n\" +\n\t\"\\vEncoreCloud\\x1a\\v\\n\" +\n\t\"\\tAWSSqsSns\\x1a\\v\\n\" +\n\t\"\\tGCPPubSub\\x1a\\x1b\\n\" +\n\t\"\\x03NSQ\\x12\\x14\\n\" +\n\t\"\\x05hosts\\x18\\x01 \\x03(\\tR\\x05hosts\\x1a/\\n\" +\n\t\"\\x0fAzureServiceBus\\x12\\x1c\\n\" +\n\t\"\\tnamespace\\x18\\x01 \\x01(\\tR\\tnamespaceB\\n\" +\n\t\"\\n\" +\n\t\"\\bprovider\\\"\\x8b\\x04\\n\" +\n\t\"\\vPubSubTopic\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"cloud_name\\x18\\x03 \\x01(\\tR\\tcloudName\\x12_\\n\" +\n\t\"\\x12delivery_guarantee\\x18\\x04 \\x01(\\x0e20.encore.runtime.v1.PubSubTopic.DeliveryGuaranteeR\\x11deliveryGuarantee\\x12(\\n\" +\n\t\"\\rordering_attr\\x18\\x05 \\x01(\\tH\\x01R\\forderingAttr\\x88\\x01\\x01\\x12I\\n\" +\n\t\"\\n\" +\n\t\"gcp_config\\x18\\n\" +\n\t\" \\x01(\\v2(.encore.runtime.v1.PubSubTopic.GCPConfigH\\x00R\\tgcpConfig\\x1a*\\n\" +\n\t\"\\tGCPConfig\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"project_id\\x18\\x01 \\x01(\\tR\\tprojectId\\\"\\x82\\x01\\n\" +\n\t\"\\x11DeliveryGuarantee\\x12\\\"\\n\" +\n\t\"\\x1eDELIVERY_GUARANTEE_UNSPECIFIED\\x10\\x00\\x12$\\n\" +\n\t\" DELIVERY_GUARANTEE_AT_LEAST_ONCE\\x10\\x01\\x12#\\n\" +\n\t\"\\x1fDELIVERY_GUARANTEE_EXACTLY_ONCE\\x10\\x02B\\x11\\n\" +\n\t\"\\x0fprovider_configB\\x10\\n\" +\n\t\"\\x0e_ordering_attr\\\"\\xb4\\x04\\n\" +\n\t\"\\x12PubSubSubscription\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12*\\n\" +\n\t\"\\x11topic_encore_name\\x18\\x02 \\x01(\\tR\\x0ftopicEncoreName\\x128\\n\" +\n\t\"\\x18subscription_encore_name\\x18\\x03 \\x01(\\tR\\x16subscriptionEncoreName\\x12(\\n\" +\n\t\"\\x10topic_cloud_name\\x18\\x04 \\x01(\\tR\\x0etopicCloudName\\x126\\n\" +\n\t\"\\x17subscription_cloud_name\\x18\\x05 \\x01(\\tR\\x15subscriptionCloudName\\x12\\x1b\\n\" +\n\t\"\\tpush_only\\x18\\x06 \\x01(\\bR\\bpushOnly\\x12P\\n\" +\n\t\"\\n\" +\n\t\"gcp_config\\x18\\n\" +\n\t\" \\x01(\\v2/.encore.runtime.v1.PubSubSubscription.GCPConfigH\\x00R\\tgcpConfig\\x1a\\xc1\\x01\\n\" +\n\t\"\\tGCPConfig\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"project_id\\x18\\x01 \\x01(\\tR\\tprojectId\\x125\\n\" +\n\t\"\\x14push_service_account\\x18\\x02 \\x01(\\tH\\x00R\\x12pushServiceAccount\\x88\\x01\\x01\\x12/\\n\" +\n\t\"\\x11push_jwt_audience\\x18\\x03 \\x01(\\tH\\x01R\\x0fpushJwtAudience\\x88\\x01\\x01B\\x17\\n\" +\n\t\"\\x15_push_service_accountB\\x14\\n\" +\n\t\"\\x12_push_jwt_audienceB\\x11\\n\" +\n\t\"\\x0fprovider_config\\\"\\xec\\x05\\n\" +\n\t\"\\rBucketCluster\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x123\\n\" +\n\t\"\\abuckets\\x18\\x02 \\x03(\\v2\\x19.encore.runtime.v1.BucketR\\abuckets\\x125\\n\" +\n\t\"\\x02s3\\x18\\n\" +\n\t\" \\x01(\\v2#.encore.runtime.v1.BucketCluster.S3H\\x00R\\x02s3\\x128\\n\" +\n\t\"\\x03gcs\\x18\\v \\x01(\\v2$.encore.runtime.v1.BucketCluster.GCSH\\x00R\\x03gcs\\x1a\\xeb\\x01\\n\" +\n\t\"\\x02S3\\x12\\x16\\n\" +\n\t\"\\x06region\\x18\\x01 \\x01(\\tR\\x06region\\x12\\x1f\\n\" +\n\t\"\\bendpoint\\x18\\x02 \\x01(\\tH\\x00R\\bendpoint\\x88\\x01\\x01\\x12'\\n\" +\n\t\"\\raccess_key_id\\x18\\x03 \\x01(\\tH\\x01R\\vaccessKeyId\\x88\\x01\\x01\\x12N\\n\" +\n\t\"\\x11secret_access_key\\x18\\x04 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataH\\x02R\\x0fsecretAccessKey\\x88\\x01\\x01B\\v\\n\" +\n\t\"\\t_endpointB\\x10\\n\" +\n\t\"\\x0e_access_key_idB\\x14\\n\" +\n\t\"\\x12_secret_access_key\\x1a\\xa8\\x02\\n\" +\n\t\"\\x03GCS\\x12\\x1f\\n\" +\n\t\"\\bendpoint\\x18\\x01 \\x01(\\tH\\x00R\\bendpoint\\x88\\x01\\x01\\x12\\x1c\\n\" +\n\t\"\\tanonymous\\x18\\x02 \\x01(\\bR\\tanonymous\\x12Y\\n\" +\n\t\"\\n\" +\n\t\"local_sign\\x18\\x03 \\x01(\\v25.encore.runtime.v1.BucketCluster.GCS.LocalSignOptionsH\\x01R\\tlocalSign\\x88\\x01\\x01\\x1ak\\n\" +\n\t\"\\x10LocalSignOptions\\x12\\x19\\n\" +\n\t\"\\bbase_url\\x18\\x01 \\x01(\\tR\\abaseUrl\\x12\\x1b\\n\" +\n\t\"\\taccess_id\\x18\\x02 \\x01(\\tR\\baccessId\\x12\\x1f\\n\" +\n\t\"\\vprivate_key\\x18\\x03 \\x01(\\tR\\n\" +\n\t\"privateKeyB\\v\\n\" +\n\t\"\\t_endpointB\\r\\n\" +\n\t\"\\v_local_signB\\n\" +\n\t\"\\n\" +\n\t\"\\bprovider\\\"\\xce\\x01\\n\" +\n\t\"\\x06Bucket\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"cloud_name\\x18\\x03 \\x01(\\tR\\tcloudName\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"key_prefix\\x18\\x04 \\x01(\\tH\\x00R\\tkeyPrefix\\x88\\x01\\x01\\x12+\\n\" +\n\t\"\\x0fpublic_base_url\\x18\\x05 \\x01(\\tH\\x01R\\rpublicBaseUrl\\x88\\x01\\x01B\\r\\n\" +\n\t\"\\v_key_prefixB\\x12\\n\" +\n\t\"\\x10_public_base_url\\\"\\xb9\\x06\\n\" +\n\t\"\\aGateway\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x02 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12\\x19\\n\" +\n\t\"\\bbase_url\\x18\\x03 \\x01(\\tR\\abaseUrl\\x12\\x1c\\n\" +\n\t\"\\thostnames\\x18\\x04 \\x03(\\tR\\thostnames\\x123\\n\" +\n\t\"\\x04cors\\x18\\x05 \\x01(\\v2\\x1f.encore.runtime.v1.Gateway.CORSR\\x04cors\\x1a\\xcd\\x04\\n\" +\n\t\"\\x04CORS\\x12\\x14\\n\" +\n\t\"\\x05debug\\x18\\x01 \\x01(\\bR\\x05debug\\x12/\\n\" +\n\t\"\\x13disable_credentials\\x18\\x02 \\x01(\\bR\\x12disableCredentials\\x12X\\n\" +\n\t\"\\x0fallowed_origins\\x18\\x03 \\x01(\\v2-.encore.runtime.v1.Gateway.CORSAllowedOriginsH\\x00R\\x0eallowedOrigins\\x12Y\\n\" +\n\t\")unsafe_allow_all_origins_with_credentials\\x18\\x04 \\x01(\\bH\\x00R$unsafeAllowAllOriginsWithCredentials\\x12|\\n\" +\n\t\"#allowed_origins_without_credentials\\x18\\x05 \\x01(\\v2-.encore.runtime.v1.Gateway.CORSAllowedOriginsR allowedOriginsWithoutCredentials\\x122\\n\" +\n\t\"\\x15extra_allowed_headers\\x18\\x06 \\x03(\\tR\\x13extraAllowedHeaders\\x122\\n\" +\n\t\"\\x15extra_exposed_headers\\x18\\a \\x03(\\tR\\x13extraExposedHeaders\\x12?\\n\" +\n\t\"\\x1callow_private_network_access\\x18\\b \\x01(\\bR\\x19allowPrivateNetworkAccessB\\\"\\n\" +\n\t\" allowed_origins_with_credentials\\x1a=\\n\" +\n\t\"\\x12CORSAllowedOrigins\\x12'\\n\" +\n\t\"\\x0fallowed_origins\\x18\\x01 \\x03(\\tR\\x0eallowedOrigins*}\\n\" +\n\t\"\\n\" +\n\t\"ServerKind\\x12\\x1b\\n\" +\n\t\"\\x17SERVER_KIND_UNSPECIFIED\\x10\\x00\\x12\\x17\\n\" +\n\t\"\\x13SERVER_KIND_PRIMARY\\x10\\x01\\x12\\x1b\\n\" +\n\t\"\\x17SERVER_KIND_HOT_STANDBY\\x10\\x02\\x12\\x1c\\n\" +\n\t\"\\x18SERVER_KIND_READ_REPLICA\\x10\\x03B,Z*encr.dev/proto/encore/runtime/v1;runtimev1b\\x06proto3\"\n\nvar (\n\tfile_encore_runtime_v1_infra_proto_rawDescOnce sync.Once\n\tfile_encore_runtime_v1_infra_proto_rawDescData []byte\n)\n\nfunc file_encore_runtime_v1_infra_proto_rawDescGZIP() []byte {\n\tfile_encore_runtime_v1_infra_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_runtime_v1_infra_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_infra_proto_rawDesc), len(file_encore_runtime_v1_infra_proto_rawDesc)))\n\t})\n\treturn file_encore_runtime_v1_infra_proto_rawDescData\n}\n\nvar file_encore_runtime_v1_infra_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_encore_runtime_v1_infra_proto_msgTypes = make([]protoimpl.MessageInfo, 35)\nvar file_encore_runtime_v1_infra_proto_goTypes = []any{\n\t(ServerKind)(0),                            // 0: encore.runtime.v1.ServerKind\n\t(PubSubTopic_DeliveryGuarantee)(0),         // 1: encore.runtime.v1.PubSubTopic.DeliveryGuarantee\n\t(*Infrastructure)(nil),                     // 2: encore.runtime.v1.Infrastructure\n\t(*SQLCluster)(nil),                         // 3: encore.runtime.v1.SQLCluster\n\t(*TLSConfig)(nil),                          // 4: encore.runtime.v1.TLSConfig\n\t(*SQLServer)(nil),                          // 5: encore.runtime.v1.SQLServer\n\t(*ClientCert)(nil),                         // 6: encore.runtime.v1.ClientCert\n\t(*SQLRole)(nil),                            // 7: encore.runtime.v1.SQLRole\n\t(*SQLDatabase)(nil),                        // 8: encore.runtime.v1.SQLDatabase\n\t(*SQLConnectionPool)(nil),                  // 9: encore.runtime.v1.SQLConnectionPool\n\t(*RedisCluster)(nil),                       // 10: encore.runtime.v1.RedisCluster\n\t(*RedisServer)(nil),                        // 11: encore.runtime.v1.RedisServer\n\t(*RedisConnectionPool)(nil),                // 12: encore.runtime.v1.RedisConnectionPool\n\t(*RedisRole)(nil),                          // 13: encore.runtime.v1.RedisRole\n\t(*RedisDatabase)(nil),                      // 14: encore.runtime.v1.RedisDatabase\n\t(*AppSecret)(nil),                          // 15: encore.runtime.v1.AppSecret\n\t(*PubSubCluster)(nil),                      // 16: encore.runtime.v1.PubSubCluster\n\t(*PubSubTopic)(nil),                        // 17: encore.runtime.v1.PubSubTopic\n\t(*PubSubSubscription)(nil),                 // 18: encore.runtime.v1.PubSubSubscription\n\t(*BucketCluster)(nil),                      // 19: encore.runtime.v1.BucketCluster\n\t(*Bucket)(nil),                             // 20: encore.runtime.v1.Bucket\n\t(*Gateway)(nil),                            // 21: encore.runtime.v1.Gateway\n\t(*Infrastructure_Credentials)(nil),         // 22: encore.runtime.v1.Infrastructure.Credentials\n\t(*Infrastructure_Resources)(nil),           // 23: encore.runtime.v1.Infrastructure.Resources\n\t(*RedisRole_AuthACL)(nil),                  // 24: encore.runtime.v1.RedisRole.AuthACL\n\t(*PubSubCluster_EncoreCloud)(nil),          // 25: encore.runtime.v1.PubSubCluster.EncoreCloud\n\t(*PubSubCluster_AWSSqsSns)(nil),            // 26: encore.runtime.v1.PubSubCluster.AWSSqsSns\n\t(*PubSubCluster_GCPPubSub)(nil),            // 27: encore.runtime.v1.PubSubCluster.GCPPubSub\n\t(*PubSubCluster_NSQ)(nil),                  // 28: encore.runtime.v1.PubSubCluster.NSQ\n\t(*PubSubCluster_AzureServiceBus)(nil),      // 29: encore.runtime.v1.PubSubCluster.AzureServiceBus\n\t(*PubSubTopic_GCPConfig)(nil),              // 30: encore.runtime.v1.PubSubTopic.GCPConfig\n\t(*PubSubSubscription_GCPConfig)(nil),       // 31: encore.runtime.v1.PubSubSubscription.GCPConfig\n\t(*BucketCluster_S3)(nil),                   // 32: encore.runtime.v1.BucketCluster.S3\n\t(*BucketCluster_GCS)(nil),                  // 33: encore.runtime.v1.BucketCluster.GCS\n\t(*BucketCluster_GCS_LocalSignOptions)(nil), // 34: encore.runtime.v1.BucketCluster.GCS.LocalSignOptions\n\t(*Gateway_CORS)(nil),                       // 35: encore.runtime.v1.Gateway.CORS\n\t(*Gateway_CORSAllowedOrigins)(nil),         // 36: encore.runtime.v1.Gateway.CORSAllowedOrigins\n\t(*SecretData)(nil),                         // 37: encore.runtime.v1.SecretData\n}\nvar file_encore_runtime_v1_infra_proto_depIdxs = []int32{\n\t23, // 0: encore.runtime.v1.Infrastructure.resources:type_name -> encore.runtime.v1.Infrastructure.Resources\n\t22, // 1: encore.runtime.v1.Infrastructure.credentials:type_name -> encore.runtime.v1.Infrastructure.Credentials\n\t5,  // 2: encore.runtime.v1.SQLCluster.servers:type_name -> encore.runtime.v1.SQLServer\n\t8,  // 3: encore.runtime.v1.SQLCluster.databases:type_name -> encore.runtime.v1.SQLDatabase\n\t0,  // 4: encore.runtime.v1.SQLServer.kind:type_name -> encore.runtime.v1.ServerKind\n\t4,  // 5: encore.runtime.v1.SQLServer.tls_config:type_name -> encore.runtime.v1.TLSConfig\n\t37, // 6: encore.runtime.v1.ClientCert.key:type_name -> encore.runtime.v1.SecretData\n\t37, // 7: encore.runtime.v1.SQLRole.password:type_name -> encore.runtime.v1.SecretData\n\t9,  // 8: encore.runtime.v1.SQLDatabase.conn_pools:type_name -> encore.runtime.v1.SQLConnectionPool\n\t11, // 9: encore.runtime.v1.RedisCluster.servers:type_name -> encore.runtime.v1.RedisServer\n\t14, // 10: encore.runtime.v1.RedisCluster.databases:type_name -> encore.runtime.v1.RedisDatabase\n\t0,  // 11: encore.runtime.v1.RedisServer.kind:type_name -> encore.runtime.v1.ServerKind\n\t4,  // 12: encore.runtime.v1.RedisServer.tls_config:type_name -> encore.runtime.v1.TLSConfig\n\t24, // 13: encore.runtime.v1.RedisRole.acl:type_name -> encore.runtime.v1.RedisRole.AuthACL\n\t37, // 14: encore.runtime.v1.RedisRole.auth_string:type_name -> encore.runtime.v1.SecretData\n\t12, // 15: encore.runtime.v1.RedisDatabase.conn_pools:type_name -> encore.runtime.v1.RedisConnectionPool\n\t37, // 16: encore.runtime.v1.AppSecret.data:type_name -> encore.runtime.v1.SecretData\n\t17, // 17: encore.runtime.v1.PubSubCluster.topics:type_name -> encore.runtime.v1.PubSubTopic\n\t18, // 18: encore.runtime.v1.PubSubCluster.subscriptions:type_name -> encore.runtime.v1.PubSubSubscription\n\t25, // 19: encore.runtime.v1.PubSubCluster.encore:type_name -> encore.runtime.v1.PubSubCluster.EncoreCloud\n\t26, // 20: encore.runtime.v1.PubSubCluster.aws:type_name -> encore.runtime.v1.PubSubCluster.AWSSqsSns\n\t27, // 21: encore.runtime.v1.PubSubCluster.gcp:type_name -> encore.runtime.v1.PubSubCluster.GCPPubSub\n\t29, // 22: encore.runtime.v1.PubSubCluster.azure:type_name -> encore.runtime.v1.PubSubCluster.AzureServiceBus\n\t28, // 23: encore.runtime.v1.PubSubCluster.nsq:type_name -> encore.runtime.v1.PubSubCluster.NSQ\n\t1,  // 24: encore.runtime.v1.PubSubTopic.delivery_guarantee:type_name -> encore.runtime.v1.PubSubTopic.DeliveryGuarantee\n\t30, // 25: encore.runtime.v1.PubSubTopic.gcp_config:type_name -> encore.runtime.v1.PubSubTopic.GCPConfig\n\t31, // 26: encore.runtime.v1.PubSubSubscription.gcp_config:type_name -> encore.runtime.v1.PubSubSubscription.GCPConfig\n\t20, // 27: encore.runtime.v1.BucketCluster.buckets:type_name -> encore.runtime.v1.Bucket\n\t32, // 28: encore.runtime.v1.BucketCluster.s3:type_name -> encore.runtime.v1.BucketCluster.S3\n\t33, // 29: encore.runtime.v1.BucketCluster.gcs:type_name -> encore.runtime.v1.BucketCluster.GCS\n\t35, // 30: encore.runtime.v1.Gateway.cors:type_name -> encore.runtime.v1.Gateway.CORS\n\t6,  // 31: encore.runtime.v1.Infrastructure.Credentials.client_certs:type_name -> encore.runtime.v1.ClientCert\n\t7,  // 32: encore.runtime.v1.Infrastructure.Credentials.sql_roles:type_name -> encore.runtime.v1.SQLRole\n\t13, // 33: encore.runtime.v1.Infrastructure.Credentials.redis_roles:type_name -> encore.runtime.v1.RedisRole\n\t21, // 34: encore.runtime.v1.Infrastructure.Resources.gateways:type_name -> encore.runtime.v1.Gateway\n\t3,  // 35: encore.runtime.v1.Infrastructure.Resources.sql_clusters:type_name -> encore.runtime.v1.SQLCluster\n\t16, // 36: encore.runtime.v1.Infrastructure.Resources.pubsub_clusters:type_name -> encore.runtime.v1.PubSubCluster\n\t10, // 37: encore.runtime.v1.Infrastructure.Resources.redis_clusters:type_name -> encore.runtime.v1.RedisCluster\n\t15, // 38: encore.runtime.v1.Infrastructure.Resources.app_secrets:type_name -> encore.runtime.v1.AppSecret\n\t19, // 39: encore.runtime.v1.Infrastructure.Resources.bucket_clusters:type_name -> encore.runtime.v1.BucketCluster\n\t37, // 40: encore.runtime.v1.RedisRole.AuthACL.password:type_name -> encore.runtime.v1.SecretData\n\t37, // 41: encore.runtime.v1.BucketCluster.S3.secret_access_key:type_name -> encore.runtime.v1.SecretData\n\t34, // 42: encore.runtime.v1.BucketCluster.GCS.local_sign:type_name -> encore.runtime.v1.BucketCluster.GCS.LocalSignOptions\n\t36, // 43: encore.runtime.v1.Gateway.CORS.allowed_origins:type_name -> encore.runtime.v1.Gateway.CORSAllowedOrigins\n\t36, // 44: encore.runtime.v1.Gateway.CORS.allowed_origins_without_credentials:type_name -> encore.runtime.v1.Gateway.CORSAllowedOrigins\n\t45, // [45:45] is the sub-list for method output_type\n\t45, // [45:45] is the sub-list for method input_type\n\t45, // [45:45] is the sub-list for extension type_name\n\t45, // [45:45] is the sub-list for extension extendee\n\t0,  // [0:45] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_runtime_v1_infra_proto_init() }\nfunc file_encore_runtime_v1_infra_proto_init() {\n\tif File_encore_runtime_v1_infra_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_runtime_v1_secretdata_proto_init()\n\tfile_encore_runtime_v1_infra_proto_msgTypes[2].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[3].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[5].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[9].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[11].OneofWrappers = []any{\n\t\t(*RedisRole_Acl)(nil),\n\t\t(*RedisRole_AuthString)(nil),\n\t}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[12].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[14].OneofWrappers = []any{\n\t\t(*PubSubCluster_Encore)(nil),\n\t\t(*PubSubCluster_Aws)(nil),\n\t\t(*PubSubCluster_Gcp)(nil),\n\t\t(*PubSubCluster_Azure)(nil),\n\t\t(*PubSubCluster_Nsq)(nil),\n\t}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[15].OneofWrappers = []any{\n\t\t(*PubSubTopic_GcpConfig)(nil),\n\t}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[16].OneofWrappers = []any{\n\t\t(*PubSubSubscription_GcpConfig)(nil),\n\t}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[17].OneofWrappers = []any{\n\t\t(*BucketCluster_S3_)(nil),\n\t\t(*BucketCluster_Gcs)(nil),\n\t}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[18].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[29].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[30].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[31].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_infra_proto_msgTypes[33].OneofWrappers = []any{\n\t\t(*Gateway_CORS_AllowedOrigins)(nil),\n\t\t(*Gateway_CORS_UnsafeAllowAllOriginsWithCredentials)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_infra_proto_rawDesc), len(file_encore_runtime_v1_infra_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   35,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_runtime_v1_infra_proto_goTypes,\n\t\tDependencyIndexes: file_encore_runtime_v1_infra_proto_depIdxs,\n\t\tEnumInfos:         file_encore_runtime_v1_infra_proto_enumTypes,\n\t\tMessageInfos:      file_encore_runtime_v1_infra_proto_msgTypes,\n\t}.Build()\n\tFile_encore_runtime_v1_infra_proto = out.File\n\tfile_encore_runtime_v1_infra_proto_goTypes = nil\n\tfile_encore_runtime_v1_infra_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/runtime/v1/infra.proto",
    "content": "syntax = \"proto3\";\npackage encore.runtime.v1;\n\nimport \"encore/runtime/v1/secretdata.proto\";\n\noption go_package = \"encr.dev/proto/encore/runtime/v1;runtimev1\";\n\nmessage Infrastructure {\n  Resources resources = 1;\n  Credentials credentials = 2;\n\n  message Credentials {\n    repeated ClientCert client_certs = 1;\n    repeated SQLRole sql_roles = 2;\n    repeated RedisRole redis_roles = 3;\n  }\n\n  message Resources {\n    repeated Gateway gateways = 1;\n    repeated SQLCluster sql_clusters = 2;\n    repeated PubSubCluster pubsub_clusters = 3;\n    repeated RedisCluster redis_clusters = 4;\n    repeated AppSecret app_secrets = 5;\n    repeated BucketCluster bucket_clusters = 6;\n  }\n}\n\nmessage SQLCluster {\n  // The unique resource id for this cluster.\n  string rid = 1;\n\n  repeated SQLServer servers = 2;\n  repeated SQLDatabase databases = 3;\n}\n\nenum ServerKind {\n  SERVER_KIND_UNSPECIFIED = 0;\n  SERVER_KIND_PRIMARY = 1;\n\n  // A hot-standby (a read replica designed to take over write traffic\n  // at a moment's notice).\n  SERVER_KIND_HOT_STANDBY = 2;\n\n  // A read-replica.\n  SERVER_KIND_READ_REPLICA = 3;\n}\n\nmessage TLSConfig {\n  // Server CA Cert PEM to use for verifying the server's certificate.\n  optional string server_ca_cert = 1;\n\n  // If true, skips hostname verification when connecting.\n  // If invalid hostnames are trusted, *any* valid certificate for *any* site will be trusted for use.\n  // This introduces significant vulnerabilities, and should only be used as a last resort.\n  bool disable_tls_hostname_verification = 2;\n\n  // If true, skips CA cert validation when connecting.\n  // This introduces significant vulnerabilities, and should only be used as a last resort.\n  bool disable_ca_validation = 3;\n}\n\nmessage SQLServer {\n  // The unique resource id for this server.\n  string rid = 1;\n\n  // Host is the host to connect to.\n  // Valid formats are \"hostname\", \"hostname:port\", and \"/path/to/unix.socket\".\n  string host = 2;\n  ServerKind kind = 3;\n\n  // TLS configuration to use when connecting.\n  optional TLSConfig tls_config = 4;\n}\n\nmessage ClientCert {\n  // The unique resource id for this certificate.\n  string rid = 1;\n\n  string cert = 2;\n  SecretData key = 3;\n}\n\nmessage SQLRole {\n  // The unique resource id for this role.\n  string rid = 1;\n\n  string username = 2;\n  SecretData password = 3;\n\n  // The client cert to use to authenticate, if any.\n  optional string client_cert_rid = 4;\n}\n\nmessage SQLDatabase {\n  // The unique resource id for this database.\n  string rid = 1;\n\n  string encore_name = 2;\n\n  // The physical name of the database in the cluster.\n  string cloud_name = 3;\n\n  // Connection pools to use for connecting to the database.\n  repeated SQLConnectionPool conn_pools = 4;\n}\n\nmessage SQLConnectionPool {\n  // Whether this connection pool is for read-only servers.\n  bool is_readonly = 1;\n\n  // The role to use to authenticate.\n  string role_rid = 2;\n\n  // The minimum and maximum number of connections to use.\n  int32 min_connections = 3;\n  int32 max_connections = 4;\n}\n\nmessage RedisCluster {\n  // The unique resource id for this cluster.\n  string rid = 1;\n\n  repeated RedisServer servers = 2;\n\n  repeated RedisDatabase databases = 3;\n\n  // If true, the runtime will use an in-memory Redis implementation\n  // instead of connecting to the configured servers.\n  bool in_memory = 4;\n}\n\nmessage RedisServer {\n  // The unique resource id for this server.\n  string rid = 1;\n\n  // Host is the host to connect to.\n  // Valid formats are \"hostname\", \"hostname:port\", and \"/path/to/unix.socket\".\n  string host = 2;\n  ServerKind kind = 3;\n\n  // TLS configuration to use when connecting.\n  // If nil, TLS is not used.\n  optional TLSConfig tls_config = 4;\n}\n\nmessage RedisConnectionPool {\n  // Whether this connection pool is for read-only servers.\n  bool is_readonly = 1;\n\n  // The role to use to authenticate.\n  string role_rid = 2;\n\n  // The minimum and maximum number of connections to use.\n  int32 min_connections = 3;\n  int32 max_connections = 4;\n}\n\nmessage RedisRole {\n  // The unique resource id for this role.\n  string rid = 1;\n\n  // The client cert to use to authenticate, if any.\n  optional string client_cert_rid = 2;\n\n  // How to authenticate with Redis.\n  // If unset, no authentication is used.\n  oneof auth {\n    AuthACL acl = 10; // Redis ACL\n    SecretData auth_string = 11; // Redis AUTH string\n  }\n\n  message AuthACL {\n    string username = 1;\n    SecretData password = 2;\n  }\n}\n\nmessage RedisDatabase {\n  // Unique resource id for this database.\n  string rid = 1;\n\n  // The encore name of the database.\n  string encore_name = 2;\n\n  // The database index to use, [0-15].\n  int32 database_idx = 3;\n\n  // KeyPrefix specifies a prefix to add to all cache keys\n  // for this database. It exists to enable multiple cache clusters\n  // to use the same physical Redis database for local development\n  // without having to coordinate and persist database index ids.\n  optional string key_prefix = 4;\n\n  // Connection pools to use for connecting to the database.\n  repeated RedisConnectionPool conn_pools = 5;\n}\n\nmessage AppSecret {\n  // The unique resource id for this secret.\n  string rid = 1;\n\n  // The encore name of the secret.\n  string encore_name = 2;\n\n  // The secret data.\n  SecretData data = 3;\n}\n\nmessage PubSubCluster {\n  // The unique resource id for this cluster.\n  string rid = 1;\n\n  repeated PubSubTopic topics = 2;\n  repeated PubSubSubscription subscriptions = 3;\n\n  oneof provider {\n    EncoreCloud encore = 5;\n    AWSSqsSns aws = 6;\n    GCPPubSub gcp = 7;\n    AzureServiceBus azure = 8;\n    NSQ nsq = 9;\n  }\n\n  message EncoreCloud {}\n  message AWSSqsSns {}\n  message GCPPubSub {}\n\n  message NSQ {\n    // The hosts to connect to NSQ. Must be non-empty.\n    repeated string hosts = 1;\n  }\n\n  message AzureServiceBus {\n    string namespace = 1;\n  }\n}\n\nmessage PubSubTopic {\n  // The unique resource id for this topic.\n  string rid = 1;\n\n  // The encore name of the topic.\n  string encore_name = 2;\n\n  // The cloud name of the topic.\n  string cloud_name = 3;\n\n  // The delivery guarantee.\n  DeliveryGuarantee delivery_guarantee = 4;\n\n  // Optional ordering attribute. Specifies the attribute name\n  // to use for message ordering.\n  optional string ordering_attr = 5;\n\n  // Provider-specific configuration.\n  // Not all providers require this, but it must always be set\n  // for the providers that are present.\n  oneof provider_config {\n    GCPConfig gcp_config = 10;\n    // Null: no provider-specific configuration.\n  }\n\n  message GCPConfig {\n    // The GCP project id where the topic exists.\n    string project_id = 1;\n  }\n\n  enum DeliveryGuarantee {\n    DELIVERY_GUARANTEE_UNSPECIFIED = 0;\n    DELIVERY_GUARANTEE_AT_LEAST_ONCE = 1; // All messages will be delivered to each subscription at least once\n    DELIVERY_GUARANTEE_EXACTLY_ONCE = 2; // All messages will be delivered to each subscription exactly once\n  }\n}\n\nmessage PubSubSubscription {\n  // The unique resource id for this subscription.\n  string rid = 1;\n\n  // The encore name of the topic this subscription is for.\n  string topic_encore_name = 2;\n\n  // The encore name of the subscription.\n  string subscription_encore_name = 3;\n\n  // The cloud name of the subscription.\n  string topic_cloud_name = 4;\n\n  // The cloud name of the subscription.\n  string subscription_cloud_name = 5;\n\n  // If true the application will not actively subscribe but wait\n  // for incoming messages to be pushed to it.\n  bool push_only = 6;\n\n  // Subscription-specific provider configuration.\n  // Not all providers require this, but it must always be set\n  // for the providers that are present.\n  oneof provider_config {\n    GCPConfig gcp_config = 10;\n    // Null: no provider-specific configuration.\n  }\n\n  message GCPConfig {\n    // The GCP project id where the subscription exists.\n    string project_id = 1;\n\n    // The service account used to authenticate messages being delivered over push.\n    // If unset, pushes are rejected.\n    optional string push_service_account = 2;\n\n    // The audience to use when validating JWTs delivered over push.\n    // If set, the JWT audience claim must match. If unset, any JWT audience is allowed.\n    optional string push_jwt_audience = 3;\n  }\n}\n\nmessage BucketCluster {\n  // The unique resource id for this cluster.\n  string rid = 1;\n\n  repeated Bucket buckets = 2;\n\n  oneof provider {\n    S3 s3 = 10;\n    GCS gcs = 11;\n  }\n\n  message S3 {\n    // Region to connect to.\n    string region = 1;\n\n    // Endpoint override, if any. Must be specified if using a non-standard AWS region.\n    optional string endpoint = 2;\n\n    // Set these to use explicit credentials for this bucket,\n    // as opposed to resolving using AWS's default credential chain.\n    optional string access_key_id = 3;\n    optional SecretData secret_access_key = 4;\n  }\n\n  message GCS {\n    // Endpoint override, if any. Defaults to https://storage.googleapis.com if unset.\n    optional string endpoint = 1;\n\n    // Whether to connect anonymously or if a service account should be resolved.\n    bool anonymous = 2;\n\n    // Additional options for signed URLs when running in local dev mode.\n    // Only use with anonymous mode.\n    optional LocalSignOptions local_sign = 3;\n\n    message LocalSignOptions {\n      // Base prefix to use for presigned URLs.\n      string base_url = 1;\n\n      // Use these credentials to sign local URLs. Only pass dummy credentials\n      // here, no actual secrets.\n      string access_id = 2;\n      string private_key = 3;\n    }\n  }\n}\n\nmessage Bucket {\n  // The unique resource id for this bucket.\n  string rid = 1;\n\n  // The encore name of the bucket.\n  string encore_name = 2;\n\n  // The cloud name of the bucket.\n  string cloud_name = 3;\n\n  // Optional key prefix to prepend to all bucket keys.\n  //\n  // Note: make sure it ends with a slash (\"/\") if you want\n  // to group objects within a certain folder.\n  optional string key_prefix = 4;\n\n  // Public base URL for accessing objects in this bucket.\n  // Must be set for public buckets.\n  optional string public_base_url = 5;\n}\n\nmessage Gateway {\n  // The unique id for this resource.\n  string rid = 1;\n\n  //  The encore name of the gateway.\n  string encore_name = 2;\n\n  // The base url for reaching this gateway, for returning to the application\n  // via e.g. the metadata APIs.\n  string base_url = 3;\n\n  // The hostnames this gateway accepts requests for.\n  repeated string hostnames = 4;\n\n  // CORS is the CORS configuration for this gateway.\n  CORS cors = 5;\n\n  // CORS describes the CORS configuration for a gateway.\n  message CORS {\n    bool debug = 1;\n\n    // If true, causes Encore to respond to OPTIONS requests\n    // without setting Access-Control-Allow-Credentials: true.\n    bool disable_credentials = 2;\n\n    // Specifies the allowed origins for requests that include credentials.\n    // If a request is made from an Origin in this list\n    // Encore responds with Access-Control-Allow-Origin: <Origin>.\n    //\n    // If disable_credentials is true this field is not used.\n    oneof allowed_origins_with_credentials {\n      CORSAllowedOrigins allowed_origins = 3;\n      bool unsafe_allow_all_origins_with_credentials = 4;\n    }\n\n    // Specifies the allowed origins for requests\n    // that don't include credentials.\n    //\n    // The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n    // or \"https://*-myapp.example.com\").\n    CORSAllowedOrigins allowed_origins_without_credentials = 5;\n\n    // Specifies extra headers to allow, beyond\n    // the default set always recognized by Encore.\n    // As a special case, if the list contains \"*\" all headers are allowed.\n    repeated string extra_allowed_headers = 6;\n\n    // Specifies extra headers to expose, beyond\n    // the default set always recognized by Encore.\n    // As a special case, if the list contains \"*\" all headers are allowed.\n    repeated string extra_exposed_headers = 7;\n\n    // If true, allows requests to Encore apps running\n    // on private networks from websites.\n    // See: https://wicg.github.io/private-network-access/\n    bool allow_private_network_access = 8;\n  }\n\n  message CORSAllowedOrigins {\n    // The list of allowed origins.\n    // The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n    // or \"https://*-myapp.example.com\").\n    //\n    // The string \"*\" allows all origins, except for requests with credentials;\n    // use CORS.unsafe_allow_unsafe_all_origins_with_credentials for that.\n    repeated string allowed_origins = 1;\n  }\n}\n"
  },
  {
    "path": "proto/encore/runtime/v1/runtime.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/runtime/v1/runtime.proto\n\npackage runtimev1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\tdurationpb \"google.golang.org/protobuf/types/known/durationpb\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Environment_Type int32\n\nconst (\n\tEnvironment_TYPE_UNSPECIFIED Environment_Type = 0\n\tEnvironment_TYPE_DEVELOPMENT Environment_Type = 1\n\tEnvironment_TYPE_PRODUCTION  Environment_Type = 2\n\tEnvironment_TYPE_EPHEMERAL   Environment_Type = 3\n\tEnvironment_TYPE_TEST        Environment_Type = 4\n)\n\n// Enum value maps for Environment_Type.\nvar (\n\tEnvironment_Type_name = map[int32]string{\n\t\t0: \"TYPE_UNSPECIFIED\",\n\t\t1: \"TYPE_DEVELOPMENT\",\n\t\t2: \"TYPE_PRODUCTION\",\n\t\t3: \"TYPE_EPHEMERAL\",\n\t\t4: \"TYPE_TEST\",\n\t}\n\tEnvironment_Type_value = map[string]int32{\n\t\t\"TYPE_UNSPECIFIED\": 0,\n\t\t\"TYPE_DEVELOPMENT\": 1,\n\t\t\"TYPE_PRODUCTION\":  2,\n\t\t\"TYPE_EPHEMERAL\":   3,\n\t\t\"TYPE_TEST\":        4,\n\t}\n)\n\nfunc (x Environment_Type) Enum() *Environment_Type {\n\tp := new(Environment_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x Environment_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Environment_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_runtime_v1_runtime_proto_enumTypes[0].Descriptor()\n}\n\nfunc (Environment_Type) Type() protoreflect.EnumType {\n\treturn &file_encore_runtime_v1_runtime_proto_enumTypes[0]\n}\n\nfunc (x Environment_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Environment_Type.Descriptor instead.\nfunc (Environment_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{1, 0}\n}\n\ntype Environment_Cloud int32\n\nconst (\n\tEnvironment_CLOUD_UNSPECIFIED Environment_Cloud = 0\n\tEnvironment_CLOUD_LOCAL       Environment_Cloud = 1\n\tEnvironment_CLOUD_ENCORE      Environment_Cloud = 2\n\tEnvironment_CLOUD_AWS         Environment_Cloud = 3\n\tEnvironment_CLOUD_GCP         Environment_Cloud = 4\n\tEnvironment_CLOUD_AZURE       Environment_Cloud = 5\n)\n\n// Enum value maps for Environment_Cloud.\nvar (\n\tEnvironment_Cloud_name = map[int32]string{\n\t\t0: \"CLOUD_UNSPECIFIED\",\n\t\t1: \"CLOUD_LOCAL\",\n\t\t2: \"CLOUD_ENCORE\",\n\t\t3: \"CLOUD_AWS\",\n\t\t4: \"CLOUD_GCP\",\n\t\t5: \"CLOUD_AZURE\",\n\t}\n\tEnvironment_Cloud_value = map[string]int32{\n\t\t\"CLOUD_UNSPECIFIED\": 0,\n\t\t\"CLOUD_LOCAL\":       1,\n\t\t\"CLOUD_ENCORE\":      2,\n\t\t\"CLOUD_AWS\":         3,\n\t\t\"CLOUD_GCP\":         4,\n\t\t\"CLOUD_AZURE\":       5,\n\t}\n)\n\nfunc (x Environment_Cloud) Enum() *Environment_Cloud {\n\tp := new(Environment_Cloud)\n\t*p = x\n\treturn p\n}\n\nfunc (x Environment_Cloud) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Environment_Cloud) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_runtime_v1_runtime_proto_enumTypes[1].Descriptor()\n}\n\nfunc (Environment_Cloud) Type() protoreflect.EnumType {\n\treturn &file_encore_runtime_v1_runtime_proto_enumTypes[1]\n}\n\nfunc (x Environment_Cloud) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Environment_Cloud.Descriptor instead.\nfunc (Environment_Cloud) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{1, 1}\n}\n\ntype RuntimeConfig struct {\n\tstate          protoimpl.MessageState `protogen:\"open.v1\"`\n\tEnvironment    *Environment           `protobuf:\"bytes,1,opt,name=environment,proto3\" json:\"environment,omitempty\"`\n\tInfra          *Infrastructure        `protobuf:\"bytes,2,opt,name=infra,proto3\" json:\"infra,omitempty\"`\n\tDeployment     *Deployment            `protobuf:\"bytes,3,opt,name=deployment,proto3\" json:\"deployment,omitempty\"`\n\tEncorePlatform *EncorePlatform        `protobuf:\"bytes,5,opt,name=encore_platform,json=encorePlatform,proto3,oneof\" json:\"encore_platform,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *RuntimeConfig) Reset() {\n\t*x = RuntimeConfig{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RuntimeConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RuntimeConfig) ProtoMessage() {}\n\nfunc (x *RuntimeConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RuntimeConfig.ProtoReflect.Descriptor instead.\nfunc (*RuntimeConfig) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *RuntimeConfig) GetEnvironment() *Environment {\n\tif x != nil {\n\t\treturn x.Environment\n\t}\n\treturn nil\n}\n\nfunc (x *RuntimeConfig) GetInfra() *Infrastructure {\n\tif x != nil {\n\t\treturn x.Infra\n\t}\n\treturn nil\n}\n\nfunc (x *RuntimeConfig) GetDeployment() *Deployment {\n\tif x != nil {\n\t\treturn x.Deployment\n\t}\n\treturn nil\n}\n\nfunc (x *RuntimeConfig) GetEncorePlatform() *EncorePlatform {\n\tif x != nil {\n\t\treturn x.EncorePlatform\n\t}\n\treturn nil\n}\n\ntype Environment struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAppId         string                 `protobuf:\"bytes,1,opt,name=app_id,json=appId,proto3\" json:\"app_id,omitempty\"`\n\tAppSlug       string                 `protobuf:\"bytes,2,opt,name=app_slug,json=appSlug,proto3\" json:\"app_slug,omitempty\"`\n\tEnvId         string                 `protobuf:\"bytes,3,opt,name=env_id,json=envId,proto3\" json:\"env_id,omitempty\"`\n\tEnvName       string                 `protobuf:\"bytes,4,opt,name=env_name,json=envName,proto3\" json:\"env_name,omitempty\"`\n\tEnvType       Environment_Type       `protobuf:\"varint,5,opt,name=env_type,json=envType,proto3,enum=encore.runtime.v1.Environment_Type\" json:\"env_type,omitempty\"`\n\tCloud         Environment_Cloud      `protobuf:\"varint,6,opt,name=cloud,proto3,enum=encore.runtime.v1.Environment_Cloud\" json:\"cloud,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Environment) Reset() {\n\t*x = Environment{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Environment) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Environment) ProtoMessage() {}\n\nfunc (x *Environment) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Environment.ProtoReflect.Descriptor instead.\nfunc (*Environment) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Environment) GetAppId() string {\n\tif x != nil {\n\t\treturn x.AppId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Environment) GetAppSlug() string {\n\tif x != nil {\n\t\treturn x.AppSlug\n\t}\n\treturn \"\"\n}\n\nfunc (x *Environment) GetEnvId() string {\n\tif x != nil {\n\t\treturn x.EnvId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Environment) GetEnvName() string {\n\tif x != nil {\n\t\treturn x.EnvName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Environment) GetEnvType() Environment_Type {\n\tif x != nil {\n\t\treturn x.EnvType\n\t}\n\treturn Environment_TYPE_UNSPECIFIED\n}\n\nfunc (x *Environment) GetCloud() Environment_Cloud {\n\tif x != nil {\n\t\treturn x.Cloud\n\t}\n\treturn Environment_CLOUD_UNSPECIFIED\n}\n\n// Describes the configuration related to a specific deployment,\n// meaning a group of services deployed together (think a single k8s Deployment).\ntype Deployment struct {\n\tstate      protoimpl.MessageState `protogen:\"open.v1\"`\n\tDeployId   string                 `protobuf:\"bytes,1,opt,name=deploy_id,json=deployId,proto3\" json:\"deploy_id,omitempty\"`\n\tDeployedAt *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=deployed_at,json=deployedAt,proto3\" json:\"deployed_at,omitempty\"`\n\t// A list of experiments to enable at runtime.\n\tDynamicExperiments []string `protobuf:\"bytes,3,rep,name=dynamic_experiments,json=dynamicExperiments,proto3\" json:\"dynamic_experiments,omitempty\"`\n\t// The gateways hosted by this deployment, by rid.\n\tHostedGateways []string `protobuf:\"bytes,4,rep,name=hosted_gateways,json=hostedGateways,proto3\" json:\"hosted_gateways,omitempty\"`\n\t// The services hosted by this deployment.\n\tHostedServices []*HostedService `protobuf:\"bytes,5,rep,name=hosted_services,json=hostedServices,proto3\" json:\"hosted_services,omitempty\"`\n\t// The authentication method(s) that can be used when talking\n\t// to this deployment for internal service-to-service calls.\n\t//\n\t// An empty list means no service-to-service calls can be made to this deployment.\n\tAuthMethods []*ServiceAuth `protobuf:\"bytes,6,rep,name=auth_methods,json=authMethods,proto3\" json:\"auth_methods,omitempty\"`\n\t// Observability-related configuration.\n\tObservability *Observability `protobuf:\"bytes,7,opt,name=observability,proto3\" json:\"observability,omitempty\"`\n\t// Service discovery configuration.\n\tServiceDiscovery *ServiceDiscovery `protobuf:\"bytes,8,opt,name=service_discovery,json=serviceDiscovery,proto3\" json:\"service_discovery,omitempty\"`\n\t// Graceful shutdown behavior.\n\tGracefulShutdown *GracefulShutdown `protobuf:\"bytes,9,opt,name=graceful_shutdown,json=gracefulShutdown,proto3\" json:\"graceful_shutdown,omitempty\"`\n\t// The metrics used by this deployment.\n\tMetrics       []*Metric `protobuf:\"bytes,10,rep,name=metrics,proto3\" json:\"metrics,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Deployment) Reset() {\n\t*x = Deployment{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Deployment) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Deployment) ProtoMessage() {}\n\nfunc (x *Deployment) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Deployment.ProtoReflect.Descriptor instead.\nfunc (*Deployment) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Deployment) GetDeployId() string {\n\tif x != nil {\n\t\treturn x.DeployId\n\t}\n\treturn \"\"\n}\n\nfunc (x *Deployment) GetDeployedAt() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.DeployedAt\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetDynamicExperiments() []string {\n\tif x != nil {\n\t\treturn x.DynamicExperiments\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetHostedGateways() []string {\n\tif x != nil {\n\t\treturn x.HostedGateways\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetHostedServices() []*HostedService {\n\tif x != nil {\n\t\treturn x.HostedServices\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetAuthMethods() []*ServiceAuth {\n\tif x != nil {\n\t\treturn x.AuthMethods\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetObservability() *Observability {\n\tif x != nil {\n\t\treturn x.Observability\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetServiceDiscovery() *ServiceDiscovery {\n\tif x != nil {\n\t\treturn x.ServiceDiscovery\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetGracefulShutdown() *GracefulShutdown {\n\tif x != nil {\n\t\treturn x.GracefulShutdown\n\t}\n\treturn nil\n}\n\nfunc (x *Deployment) GetMetrics() []*Metric {\n\tif x != nil {\n\t\treturn x.Metrics\n\t}\n\treturn nil\n}\n\ntype Observability struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The observability providers to use.\n\tTracing       []*TracingProvider `protobuf:\"bytes,1,rep,name=tracing,proto3\" json:\"tracing,omitempty\"`\n\tMetrics       []*MetricsProvider `protobuf:\"bytes,2,rep,name=metrics,proto3\" json:\"metrics,omitempty\"`\n\tLogs          []*LogsProvider    `protobuf:\"bytes,3,rep,name=logs,proto3\" json:\"logs,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Observability) Reset() {\n\t*x = Observability{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Observability) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Observability) ProtoMessage() {}\n\nfunc (x *Observability) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Observability.ProtoReflect.Descriptor instead.\nfunc (*Observability) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Observability) GetTracing() []*TracingProvider {\n\tif x != nil {\n\t\treturn x.Tracing\n\t}\n\treturn nil\n}\n\nfunc (x *Observability) GetMetrics() []*MetricsProvider {\n\tif x != nil {\n\t\treturn x.Metrics\n\t}\n\treturn nil\n}\n\nfunc (x *Observability) GetLogs() []*LogsProvider {\n\tif x != nil {\n\t\treturn x.Logs\n\t}\n\treturn nil\n}\n\ntype HostedService struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The name of the service.\n\tName string `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\t// Number of worker threads to use.\n\t// If unset it defaults to 1. If set to 0 the runtime\n\t// automatically determines the number of threads to use\n\t// based on the number of CPUs available.\n\tWorkerThreads *int32 `protobuf:\"varint,2,opt,name=worker_threads,json=workerThreads,proto3,oneof\" json:\"worker_threads,omitempty\"`\n\t// The log configuration to use for this service.\n\t// If unset it defaults to \"trace\".\n\tLogConfig     *string `protobuf:\"bytes,3,opt,name=log_config,json=logConfig,proto3,oneof\" json:\"log_config,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HostedService) Reset() {\n\t*x = HostedService{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HostedService) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HostedService) ProtoMessage() {}\n\nfunc (x *HostedService) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HostedService.ProtoReflect.Descriptor instead.\nfunc (*HostedService) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *HostedService) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *HostedService) GetWorkerThreads() int32 {\n\tif x != nil && x.WorkerThreads != nil {\n\t\treturn *x.WorkerThreads\n\t}\n\treturn 0\n}\n\nfunc (x *HostedService) GetLogConfig() string {\n\tif x != nil && x.LogConfig != nil {\n\t\treturn *x.LogConfig\n\t}\n\treturn \"\"\n}\n\ntype ServiceAuth struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The auth method to use.\n\t//\n\t// Types that are valid to be assigned to AuthMethod:\n\t//\n\t//\t*ServiceAuth_Noop\n\t//\t*ServiceAuth_EncoreAuth_\n\tAuthMethod    isServiceAuth_AuthMethod `protobuf_oneof:\"auth_method\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceAuth) Reset() {\n\t*x = ServiceAuth{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceAuth) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceAuth) ProtoMessage() {}\n\nfunc (x *ServiceAuth) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceAuth.ProtoReflect.Descriptor instead.\nfunc (*ServiceAuth) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *ServiceAuth) GetAuthMethod() isServiceAuth_AuthMethod {\n\tif x != nil {\n\t\treturn x.AuthMethod\n\t}\n\treturn nil\n}\n\nfunc (x *ServiceAuth) GetNoop() *ServiceAuth_NoopAuth {\n\tif x != nil {\n\t\tif x, ok := x.AuthMethod.(*ServiceAuth_Noop); ok {\n\t\t\treturn x.Noop\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *ServiceAuth) GetEncoreAuth() *ServiceAuth_EncoreAuth {\n\tif x != nil {\n\t\tif x, ok := x.AuthMethod.(*ServiceAuth_EncoreAuth_); ok {\n\t\t\treturn x.EncoreAuth\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isServiceAuth_AuthMethod interface {\n\tisServiceAuth_AuthMethod()\n}\n\ntype ServiceAuth_Noop struct {\n\t// Messages start at 10 to allow for other fields on ServiceAuth.\n\tNoop *ServiceAuth_NoopAuth `protobuf:\"bytes,10,opt,name=noop,proto3,oneof\"`\n}\n\ntype ServiceAuth_EncoreAuth_ struct {\n\tEncoreAuth *ServiceAuth_EncoreAuth `protobuf:\"bytes,11,opt,name=encore_auth,json=encoreAuth,proto3,oneof\"`\n}\n\nfunc (*ServiceAuth_Noop) isServiceAuth_AuthMethod() {}\n\nfunc (*ServiceAuth_EncoreAuth_) isServiceAuth_AuthMethod() {}\n\ntype TracingProvider struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this provider.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// Types that are valid to be assigned to Provider:\n\t//\n\t//\t*TracingProvider_Encore\n\tProvider      isTracingProvider_Provider `protobuf_oneof:\"provider\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TracingProvider) Reset() {\n\t*x = TracingProvider{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TracingProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TracingProvider) ProtoMessage() {}\n\nfunc (x *TracingProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TracingProvider.ProtoReflect.Descriptor instead.\nfunc (*TracingProvider) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *TracingProvider) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *TracingProvider) GetProvider() isTracingProvider_Provider {\n\tif x != nil {\n\t\treturn x.Provider\n\t}\n\treturn nil\n}\n\nfunc (x *TracingProvider) GetEncore() *TracingProvider_EncoreTracingProvider {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*TracingProvider_Encore); ok {\n\t\t\treturn x.Encore\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isTracingProvider_Provider interface {\n\tisTracingProvider_Provider()\n}\n\ntype TracingProvider_Encore struct {\n\tEncore *TracingProvider_EncoreTracingProvider `protobuf:\"bytes,10,opt,name=encore,proto3,oneof\"`\n}\n\nfunc (*TracingProvider_Encore) isTracingProvider_Provider() {}\n\ntype MetricsProvider struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this provider.\n\tRid                string               `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tCollectionInterval *durationpb.Duration `protobuf:\"bytes,2,opt,name=collection_interval,json=collectionInterval,proto3\" json:\"collection_interval,omitempty\"`\n\t// Types that are valid to be assigned to Provider:\n\t//\n\t//\t*MetricsProvider_EncoreCloud\n\t//\t*MetricsProvider_Gcp\n\t//\t*MetricsProvider_Aws\n\t//\t*MetricsProvider_PromRemoteWrite\n\t//\t*MetricsProvider_Datadog_\n\tProvider      isMetricsProvider_Provider `protobuf_oneof:\"provider\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MetricsProvider) Reset() {\n\t*x = MetricsProvider{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MetricsProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MetricsProvider) ProtoMessage() {}\n\nfunc (x *MetricsProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MetricsProvider.ProtoReflect.Descriptor instead.\nfunc (*MetricsProvider) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *MetricsProvider) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *MetricsProvider) GetCollectionInterval() *durationpb.Duration {\n\tif x != nil {\n\t\treturn x.CollectionInterval\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider) GetProvider() isMetricsProvider_Provider {\n\tif x != nil {\n\t\treturn x.Provider\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider) GetEncoreCloud() *MetricsProvider_GCPCloudMonitoring {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*MetricsProvider_EncoreCloud); ok {\n\t\t\treturn x.EncoreCloud\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider) GetGcp() *MetricsProvider_GCPCloudMonitoring {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*MetricsProvider_Gcp); ok {\n\t\t\treturn x.Gcp\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider) GetAws() *MetricsProvider_AWSCloudWatch {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*MetricsProvider_Aws); ok {\n\t\t\treturn x.Aws\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider) GetPromRemoteWrite() *MetricsProvider_PrometheusRemoteWrite {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*MetricsProvider_PromRemoteWrite); ok {\n\t\t\treturn x.PromRemoteWrite\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider) GetDatadog() *MetricsProvider_Datadog {\n\tif x != nil {\n\t\tif x, ok := x.Provider.(*MetricsProvider_Datadog_); ok {\n\t\t\treturn x.Datadog\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isMetricsProvider_Provider interface {\n\tisMetricsProvider_Provider()\n}\n\ntype MetricsProvider_EncoreCloud struct {\n\tEncoreCloud *MetricsProvider_GCPCloudMonitoring `protobuf:\"bytes,10,opt,name=encore_cloud,json=encoreCloud,proto3,oneof\"`\n}\n\ntype MetricsProvider_Gcp struct {\n\tGcp *MetricsProvider_GCPCloudMonitoring `protobuf:\"bytes,11,opt,name=gcp,proto3,oneof\"`\n}\n\ntype MetricsProvider_Aws struct {\n\tAws *MetricsProvider_AWSCloudWatch `protobuf:\"bytes,12,opt,name=aws,proto3,oneof\"`\n}\n\ntype MetricsProvider_PromRemoteWrite struct {\n\tPromRemoteWrite *MetricsProvider_PrometheusRemoteWrite `protobuf:\"bytes,13,opt,name=prom_remote_write,json=promRemoteWrite,proto3,oneof\"`\n}\n\ntype MetricsProvider_Datadog_ struct {\n\tDatadog *MetricsProvider_Datadog `protobuf:\"bytes,14,opt,name=datadog,proto3,oneof\"`\n}\n\nfunc (*MetricsProvider_EncoreCloud) isMetricsProvider_Provider() {}\n\nfunc (*MetricsProvider_Gcp) isMetricsProvider_Provider() {}\n\nfunc (*MetricsProvider_Aws) isMetricsProvider_Provider() {}\n\nfunc (*MetricsProvider_PromRemoteWrite) isMetricsProvider_Provider() {}\n\nfunc (*MetricsProvider_Datadog_) isMetricsProvider_Provider() {}\n\ntype LogsProvider struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this provider.\n\tRid           string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LogsProvider) Reset() {\n\t*x = LogsProvider{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LogsProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LogsProvider) ProtoMessage() {}\n\nfunc (x *LogsProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LogsProvider.ProtoReflect.Descriptor instead.\nfunc (*LogsProvider) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *LogsProvider) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\ntype EncoreAuthKey struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            uint32                 `protobuf:\"varint,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tData          *SecretData            `protobuf:\"bytes,2,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *EncoreAuthKey) Reset() {\n\t*x = EncoreAuthKey{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *EncoreAuthKey) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*EncoreAuthKey) ProtoMessage() {}\n\nfunc (x *EncoreAuthKey) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use EncoreAuthKey.ProtoReflect.Descriptor instead.\nfunc (*EncoreAuthKey) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *EncoreAuthKey) GetId() uint32 {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn 0\n}\n\nfunc (x *EncoreAuthKey) GetData() *SecretData {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\n// Describes service discovery configuration.\ntype ServiceDiscovery struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Where services are located, keyed by the service name.\n\tServices      map[string]*ServiceDiscovery_Location `protobuf:\"bytes,1,rep,name=services,proto3\" json:\"services,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceDiscovery) Reset() {\n\t*x = ServiceDiscovery{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceDiscovery) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceDiscovery) ProtoMessage() {}\n\nfunc (x *ServiceDiscovery) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceDiscovery.ProtoReflect.Descriptor instead.\nfunc (*ServiceDiscovery) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *ServiceDiscovery) GetServices() map[string]*ServiceDiscovery_Location {\n\tif x != nil {\n\t\treturn x.Services\n\t}\n\treturn nil\n}\n\n// GracefulShutdown defines the graceful shutdown timings.\ntype GracefulShutdown struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Total is how long we allow the total shutdown to take\n\t// before the process forcibly exits.\n\tTotal *durationpb.Duration `protobuf:\"bytes,1,opt,name=total,proto3\" json:\"total,omitempty\"`\n\t// ShutdownHooks is how long before [total] runs out that we cancel\n\t// the context that is passed to the shutdown hooks.\n\t//\n\t// It is expected that ShutdownHooks is a larger value than Handlers.\n\tShutdownHooks *durationpb.Duration `protobuf:\"bytes,2,opt,name=shutdown_hooks,json=shutdownHooks,proto3\" json:\"shutdown_hooks,omitempty\"`\n\t// Handlers is how long before [total] runs out that we cancel\n\t// the context that is passed to API and PubSub Subscription handlers.\n\t//\n\t// For example, if [total] is 10 seconds and [handlers] is 2 seconds,\n\t// then we will cancel the context passed to handlers 8 seconds after\n\t// a graceful shutdown is initiated.\n\tHandlers      *durationpb.Duration `protobuf:\"bytes,3,opt,name=handlers,proto3\" json:\"handlers,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GracefulShutdown) Reset() {\n\t*x = GracefulShutdown{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GracefulShutdown) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GracefulShutdown) ProtoMessage() {}\n\nfunc (x *GracefulShutdown) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GracefulShutdown.ProtoReflect.Descriptor instead.\nfunc (*GracefulShutdown) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *GracefulShutdown) GetTotal() *durationpb.Duration {\n\tif x != nil {\n\t\treturn x.Total\n\t}\n\treturn nil\n}\n\nfunc (x *GracefulShutdown) GetShutdownHooks() *durationpb.Duration {\n\tif x != nil {\n\t\treturn x.ShutdownHooks\n\t}\n\treturn nil\n}\n\nfunc (x *GracefulShutdown) GetHandlers() *durationpb.Duration {\n\tif x != nil {\n\t\treturn x.Handlers\n\t}\n\treturn nil\n}\n\ntype EncorePlatform struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Auth keys for validating signed requests from the Encore Platform.\n\tPlatformSigningKeys []*EncoreAuthKey `protobuf:\"bytes,1,rep,name=platform_signing_keys,json=platformSigningKeys,proto3\" json:\"platform_signing_keys,omitempty\"`\n\t// The Encore Cloud configuration to use, if running in Encore Cloud.\n\tEncoreCloud   *EncoreCloudProvider `protobuf:\"bytes,2,opt,name=encore_cloud,json=encoreCloud,proto3,oneof\" json:\"encore_cloud,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *EncorePlatform) Reset() {\n\t*x = EncorePlatform{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[12]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *EncorePlatform) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*EncorePlatform) ProtoMessage() {}\n\nfunc (x *EncorePlatform) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[12]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use EncorePlatform.ProtoReflect.Descriptor instead.\nfunc (*EncorePlatform) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{12}\n}\n\nfunc (x *EncorePlatform) GetPlatformSigningKeys() []*EncoreAuthKey {\n\tif x != nil {\n\t\treturn x.PlatformSigningKeys\n\t}\n\treturn nil\n}\n\nfunc (x *EncorePlatform) GetEncoreCloud() *EncoreCloudProvider {\n\tif x != nil {\n\t\treturn x.EncoreCloud\n\t}\n\treturn nil\n}\n\ntype RateLimiter struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Kind:\n\t//\n\t//\t*RateLimiter_TokenBucket_\n\tKind          isRateLimiter_Kind `protobuf_oneof:\"kind\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RateLimiter) Reset() {\n\t*x = RateLimiter{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[13]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RateLimiter) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RateLimiter) ProtoMessage() {}\n\nfunc (x *RateLimiter) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[13]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RateLimiter.ProtoReflect.Descriptor instead.\nfunc (*RateLimiter) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{13}\n}\n\nfunc (x *RateLimiter) GetKind() isRateLimiter_Kind {\n\tif x != nil {\n\t\treturn x.Kind\n\t}\n\treturn nil\n}\n\nfunc (x *RateLimiter) GetTokenBucket() *RateLimiter_TokenBucket {\n\tif x != nil {\n\t\tif x, ok := x.Kind.(*RateLimiter_TokenBucket_); ok {\n\t\t\treturn x.TokenBucket\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isRateLimiter_Kind interface {\n\tisRateLimiter_Kind()\n}\n\ntype RateLimiter_TokenBucket_ struct {\n\tTokenBucket *RateLimiter_TokenBucket `protobuf:\"bytes,1,opt,name=token_bucket,json=tokenBucket,proto3,oneof\"`\n}\n\nfunc (*RateLimiter_TokenBucket_) isRateLimiter_Kind() {}\n\ntype EncoreCloudProvider struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The unique resource id for this provider.\n\tRid string `protobuf:\"bytes,1,opt,name=rid,proto3\" json:\"rid,omitempty\"`\n\t// URL to use to authenticate with the server.\n\tServerUrl     string           `protobuf:\"bytes,2,opt,name=server_url,json=serverUrl,proto3\" json:\"server_url,omitempty\"`\n\tAuthKeys      []*EncoreAuthKey `protobuf:\"bytes,3,rep,name=auth_keys,json=authKeys,proto3\" json:\"auth_keys,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *EncoreCloudProvider) Reset() {\n\t*x = EncoreCloudProvider{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[14]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *EncoreCloudProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*EncoreCloudProvider) ProtoMessage() {}\n\nfunc (x *EncoreCloudProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[14]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use EncoreCloudProvider.ProtoReflect.Descriptor instead.\nfunc (*EncoreCloudProvider) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{14}\n}\n\nfunc (x *EncoreCloudProvider) GetRid() string {\n\tif x != nil {\n\t\treturn x.Rid\n\t}\n\treturn \"\"\n}\n\nfunc (x *EncoreCloudProvider) GetServerUrl() string {\n\tif x != nil {\n\t\treturn x.ServerUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *EncoreCloudProvider) GetAuthKeys() []*EncoreAuthKey {\n\tif x != nil {\n\t\treturn x.AuthKeys\n\t}\n\treturn nil\n}\n\ntype Metric struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The encore name of the metric.\n\tEncoreName string `protobuf:\"bytes,1,opt,name=encore_name,json=encoreName,proto3\" json:\"encore_name,omitempty\"`\n\t// The services that have access to this metric.\n\tServices      []string `protobuf:\"bytes,2,rep,name=services,proto3\" json:\"services,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Metric) Reset() {\n\t*x = Metric{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[15]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Metric) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Metric) ProtoMessage() {}\n\nfunc (x *Metric) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[15]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Metric.ProtoReflect.Descriptor instead.\nfunc (*Metric) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{15}\n}\n\nfunc (x *Metric) GetEncoreName() string {\n\tif x != nil {\n\t\treturn x.EncoreName\n\t}\n\treturn \"\"\n}\n\nfunc (x *Metric) GetServices() []string {\n\tif x != nil {\n\t\treturn x.Services\n\t}\n\treturn nil\n}\n\ntype ServiceAuth_NoopAuth struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceAuth_NoopAuth) Reset() {\n\t*x = ServiceAuth_NoopAuth{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[16]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceAuth_NoopAuth) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceAuth_NoopAuth) ProtoMessage() {}\n\nfunc (x *ServiceAuth_NoopAuth) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[16]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceAuth_NoopAuth.ProtoReflect.Descriptor instead.\nfunc (*ServiceAuth_NoopAuth) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{5, 0}\n}\n\ntype ServiceAuth_EncoreAuth struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAuthKeys      []*EncoreAuthKey       `protobuf:\"bytes,1,rep,name=auth_keys,json=authKeys,proto3\" json:\"auth_keys,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceAuth_EncoreAuth) Reset() {\n\t*x = ServiceAuth_EncoreAuth{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[17]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceAuth_EncoreAuth) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceAuth_EncoreAuth) ProtoMessage() {}\n\nfunc (x *ServiceAuth_EncoreAuth) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[17]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceAuth_EncoreAuth.ProtoReflect.Descriptor instead.\nfunc (*ServiceAuth_EncoreAuth) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{5, 1}\n}\n\nfunc (x *ServiceAuth_EncoreAuth) GetAuthKeys() []*EncoreAuthKey {\n\tif x != nil {\n\t\treturn x.AuthKeys\n\t}\n\treturn nil\n}\n\ntype TracingProvider_EncoreTracingProvider struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTraceEndpoint string                 `protobuf:\"bytes,1,opt,name=trace_endpoint,json=traceEndpoint,proto3\" json:\"trace_endpoint,omitempty\"`\n\t// Deprecated: Use sampling_config instead.\n\t//\n\t// Deprecated: Marked as deprecated in encore/runtime/v1/runtime.proto.\n\tSamplingRate *float64 `protobuf:\"fixed64,2,opt,name=sampling_rate,json=samplingRate,proto3,oneof\" json:\"sampling_rate,omitempty\"`\n\t// Sampling rates for different scopes.\n\t//\n\t// When deciding whether to sample a trace, the most specific matching\n\t// scope wins.\n\t//\n\t// If no scope matches, all traces are sampled.\n\tSamplingConfig []*TracingProvider_SamplingConfig `protobuf:\"bytes,3,rep,name=sampling_config,json=samplingConfig,proto3\" json:\"sampling_config,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *TracingProvider_EncoreTracingProvider) Reset() {\n\t*x = TracingProvider_EncoreTracingProvider{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[18]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TracingProvider_EncoreTracingProvider) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TracingProvider_EncoreTracingProvider) ProtoMessage() {}\n\nfunc (x *TracingProvider_EncoreTracingProvider) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[18]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TracingProvider_EncoreTracingProvider.ProtoReflect.Descriptor instead.\nfunc (*TracingProvider_EncoreTracingProvider) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 0}\n}\n\nfunc (x *TracingProvider_EncoreTracingProvider) GetTraceEndpoint() string {\n\tif x != nil {\n\t\treturn x.TraceEndpoint\n\t}\n\treturn \"\"\n}\n\n// Deprecated: Marked as deprecated in encore/runtime/v1/runtime.proto.\nfunc (x *TracingProvider_EncoreTracingProvider) GetSamplingRate() float64 {\n\tif x != nil && x.SamplingRate != nil {\n\t\treturn *x.SamplingRate\n\t}\n\treturn 0\n}\n\nfunc (x *TracingProvider_EncoreTracingProvider) GetSamplingConfig() []*TracingProvider_SamplingConfig {\n\tif x != nil {\n\t\treturn x.SamplingConfig\n\t}\n\treturn nil\n}\n\ntype TracingProvider_SamplingConfig struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The sampling rate, between [0, 1].\n\t// 0 means never sample, 1 means always sample.\n\tRate float64 `protobuf:\"fixed64,1,opt,name=rate,proto3\" json:\"rate,omitempty\"`\n\t// The scope this rate applies to.\n\t//\n\t// Types that are valid to be assigned to Scope:\n\t//\n\t//\t*TracingProvider_SamplingConfig_Default\n\t//\t*TracingProvider_SamplingConfig_Service\n\t//\t*TracingProvider_SamplingConfig_Endpoint_\n\t//\t*TracingProvider_SamplingConfig_Topic\n\t//\t*TracingProvider_SamplingConfig_PubsubSubscription\n\tScope         isTracingProvider_SamplingConfig_Scope `protobuf_oneof:\"scope\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TracingProvider_SamplingConfig) Reset() {\n\t*x = TracingProvider_SamplingConfig{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[19]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TracingProvider_SamplingConfig) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TracingProvider_SamplingConfig) ProtoMessage() {}\n\nfunc (x *TracingProvider_SamplingConfig) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[19]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TracingProvider_SamplingConfig.ProtoReflect.Descriptor instead.\nfunc (*TracingProvider_SamplingConfig) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 1}\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetRate() float64 {\n\tif x != nil {\n\t\treturn x.Rate\n\t}\n\treturn 0\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetScope() isTracingProvider_SamplingConfig_Scope {\n\tif x != nil {\n\t\treturn x.Scope\n\t}\n\treturn nil\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetDefault() *emptypb.Empty {\n\tif x != nil {\n\t\tif x, ok := x.Scope.(*TracingProvider_SamplingConfig_Default); ok {\n\t\t\treturn x.Default\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetService() string {\n\tif x != nil {\n\t\tif x, ok := x.Scope.(*TracingProvider_SamplingConfig_Service); ok {\n\t\t\treturn x.Service\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetEndpoint() *TracingProvider_SamplingConfig_Endpoint {\n\tif x != nil {\n\t\tif x, ok := x.Scope.(*TracingProvider_SamplingConfig_Endpoint_); ok {\n\t\t\treturn x.Endpoint\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetTopic() string {\n\tif x != nil {\n\t\tif x, ok := x.Scope.(*TracingProvider_SamplingConfig_Topic); ok {\n\t\t\treturn x.Topic\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *TracingProvider_SamplingConfig) GetPubsubSubscription() *TracingProvider_SamplingConfig_PubSubSubscription {\n\tif x != nil {\n\t\tif x, ok := x.Scope.(*TracingProvider_SamplingConfig_PubsubSubscription); ok {\n\t\t\treturn x.PubsubSubscription\n\t\t}\n\t}\n\treturn nil\n}\n\ntype isTracingProvider_SamplingConfig_Scope interface {\n\tisTracingProvider_SamplingConfig_Scope()\n}\n\ntype TracingProvider_SamplingConfig_Default struct {\n\t// Applies to all traces that don't match a more specific scope.\n\tDefault *emptypb.Empty `protobuf:\"bytes,2,opt,name=default,proto3,oneof\"`\n}\n\ntype TracingProvider_SamplingConfig_Service struct {\n\t// Applies to all endpoints within the named service.\n\tService string `protobuf:\"bytes,3,opt,name=service,proto3,oneof\"`\n}\n\ntype TracingProvider_SamplingConfig_Endpoint_ struct {\n\t// Applies to a specific API endpoint.\n\tEndpoint *TracingProvider_SamplingConfig_Endpoint `protobuf:\"bytes,4,opt,name=endpoint,proto3,oneof\"`\n}\n\ntype TracingProvider_SamplingConfig_Topic struct {\n\t// Applies to all subscriptions within the named topic.\n\tTopic string `protobuf:\"bytes,5,opt,name=topic,proto3,oneof\"`\n}\n\ntype TracingProvider_SamplingConfig_PubsubSubscription struct {\n\t// Applies to a specific pubsub subscription.\n\tPubsubSubscription *TracingProvider_SamplingConfig_PubSubSubscription `protobuf:\"bytes,6,opt,name=pubsub_subscription,json=pubsubSubscription,proto3,oneof\"`\n}\n\nfunc (*TracingProvider_SamplingConfig_Default) isTracingProvider_SamplingConfig_Scope() {}\n\nfunc (*TracingProvider_SamplingConfig_Service) isTracingProvider_SamplingConfig_Scope() {}\n\nfunc (*TracingProvider_SamplingConfig_Endpoint_) isTracingProvider_SamplingConfig_Scope() {}\n\nfunc (*TracingProvider_SamplingConfig_Topic) isTracingProvider_SamplingConfig_Scope() {}\n\nfunc (*TracingProvider_SamplingConfig_PubsubSubscription) isTracingProvider_SamplingConfig_Scope() {}\n\ntype TracingProvider_SamplingConfig_Endpoint struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tService       string                 `protobuf:\"bytes,1,opt,name=service,proto3\" json:\"service,omitempty\"`\n\tEndpoint      string                 `protobuf:\"bytes,2,opt,name=endpoint,proto3\" json:\"endpoint,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TracingProvider_SamplingConfig_Endpoint) Reset() {\n\t*x = TracingProvider_SamplingConfig_Endpoint{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[20]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TracingProvider_SamplingConfig_Endpoint) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TracingProvider_SamplingConfig_Endpoint) ProtoMessage() {}\n\nfunc (x *TracingProvider_SamplingConfig_Endpoint) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[20]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TracingProvider_SamplingConfig_Endpoint.ProtoReflect.Descriptor instead.\nfunc (*TracingProvider_SamplingConfig_Endpoint) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 1, 0}\n}\n\nfunc (x *TracingProvider_SamplingConfig_Endpoint) GetService() string {\n\tif x != nil {\n\t\treturn x.Service\n\t}\n\treturn \"\"\n}\n\nfunc (x *TracingProvider_SamplingConfig_Endpoint) GetEndpoint() string {\n\tif x != nil {\n\t\treturn x.Endpoint\n\t}\n\treturn \"\"\n}\n\ntype TracingProvider_SamplingConfig_PubSubSubscription struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTopic         string                 `protobuf:\"bytes,1,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tSubscription  string                 `protobuf:\"bytes,2,opt,name=subscription,proto3\" json:\"subscription,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TracingProvider_SamplingConfig_PubSubSubscription) Reset() {\n\t*x = TracingProvider_SamplingConfig_PubSubSubscription{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[21]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TracingProvider_SamplingConfig_PubSubSubscription) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TracingProvider_SamplingConfig_PubSubSubscription) ProtoMessage() {}\n\nfunc (x *TracingProvider_SamplingConfig_PubSubSubscription) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[21]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TracingProvider_SamplingConfig_PubSubSubscription.ProtoReflect.Descriptor instead.\nfunc (*TracingProvider_SamplingConfig_PubSubSubscription) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{6, 1, 1}\n}\n\nfunc (x *TracingProvider_SamplingConfig_PubSubSubscription) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *TracingProvider_SamplingConfig_PubSubSubscription) GetSubscription() string {\n\tif x != nil {\n\t\treturn x.Subscription\n\t}\n\treturn \"\"\n}\n\ntype MetricsProvider_GCPCloudMonitoring struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The GCP project id to send metrics to.\n\tProjectId string `protobuf:\"bytes,1,opt,name=project_id,json=projectId,proto3\" json:\"project_id,omitempty\"`\n\t// The enum value for the monitored resource this application is monitoring.\n\t// See https://cloud.google.com/monitoring/api/resources for valid values.\n\tMonitoredResourceType string `protobuf:\"bytes,2,opt,name=monitored_resource_type,json=monitoredResourceType,proto3\" json:\"monitored_resource_type,omitempty\"`\n\t// The labels to specify for the monitored resource.\n\t// Each monitored resource type has a pre-defined set of labels that must be set.\n\t// See https://cloud.google.com/monitoring/api/resources for expected labels.\n\tMonitoredResourceLabels map[string]string `protobuf:\"bytes,3,rep,name=monitored_resource_labels,json=monitoredResourceLabels,proto3\" json:\"monitored_resource_labels,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\t// The mapping between metric names in Encore and metric names in GCP.\n\tMetricNames   map[string]string `protobuf:\"bytes,4,rep,name=metric_names,json=metricNames,proto3\" json:\"metric_names,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) Reset() {\n\t*x = MetricsProvider_GCPCloudMonitoring{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[22]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MetricsProvider_GCPCloudMonitoring) ProtoMessage() {}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[22]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MetricsProvider_GCPCloudMonitoring.ProtoReflect.Descriptor instead.\nfunc (*MetricsProvider_GCPCloudMonitoring) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 0}\n}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) GetProjectId() string {\n\tif x != nil {\n\t\treturn x.ProjectId\n\t}\n\treturn \"\"\n}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) GetMonitoredResourceType() string {\n\tif x != nil {\n\t\treturn x.MonitoredResourceType\n\t}\n\treturn \"\"\n}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) GetMonitoredResourceLabels() map[string]string {\n\tif x != nil {\n\t\treturn x.MonitoredResourceLabels\n\t}\n\treturn nil\n}\n\nfunc (x *MetricsProvider_GCPCloudMonitoring) GetMetricNames() map[string]string {\n\tif x != nil {\n\t\treturn x.MetricNames\n\t}\n\treturn nil\n}\n\ntype MetricsProvider_AWSCloudWatch struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The namespace to use for metrics.\n\tNamespace     string `protobuf:\"bytes,1,opt,name=namespace,proto3\" json:\"namespace,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MetricsProvider_AWSCloudWatch) Reset() {\n\t*x = MetricsProvider_AWSCloudWatch{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[23]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MetricsProvider_AWSCloudWatch) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MetricsProvider_AWSCloudWatch) ProtoMessage() {}\n\nfunc (x *MetricsProvider_AWSCloudWatch) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[23]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MetricsProvider_AWSCloudWatch.ProtoReflect.Descriptor instead.\nfunc (*MetricsProvider_AWSCloudWatch) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 1}\n}\n\nfunc (x *MetricsProvider_AWSCloudWatch) GetNamespace() string {\n\tif x != nil {\n\t\treturn x.Namespace\n\t}\n\treturn \"\"\n}\n\ntype MetricsProvider_PrometheusRemoteWrite struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The URL to send metrics to.\n\tRemoteWriteUrl *SecretData `protobuf:\"bytes,1,opt,name=remote_write_url,json=remoteWriteUrl,proto3\" json:\"remote_write_url,omitempty\"`\n\tunknownFields  protoimpl.UnknownFields\n\tsizeCache      protoimpl.SizeCache\n}\n\nfunc (x *MetricsProvider_PrometheusRemoteWrite) Reset() {\n\t*x = MetricsProvider_PrometheusRemoteWrite{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[24]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MetricsProvider_PrometheusRemoteWrite) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MetricsProvider_PrometheusRemoteWrite) ProtoMessage() {}\n\nfunc (x *MetricsProvider_PrometheusRemoteWrite) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[24]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MetricsProvider_PrometheusRemoteWrite.ProtoReflect.Descriptor instead.\nfunc (*MetricsProvider_PrometheusRemoteWrite) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 2}\n}\n\nfunc (x *MetricsProvider_PrometheusRemoteWrite) GetRemoteWriteUrl() *SecretData {\n\tif x != nil {\n\t\treturn x.RemoteWriteUrl\n\t}\n\treturn nil\n}\n\ntype MetricsProvider_Datadog struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSite          string                 `protobuf:\"bytes,1,opt,name=site,proto3\" json:\"site,omitempty\"`\n\tApiKey        *SecretData            `protobuf:\"bytes,2,opt,name=api_key,json=apiKey,proto3\" json:\"api_key,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *MetricsProvider_Datadog) Reset() {\n\t*x = MetricsProvider_Datadog{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[25]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MetricsProvider_Datadog) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MetricsProvider_Datadog) ProtoMessage() {}\n\nfunc (x *MetricsProvider_Datadog) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[25]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MetricsProvider_Datadog.ProtoReflect.Descriptor instead.\nfunc (*MetricsProvider_Datadog) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{7, 3}\n}\n\nfunc (x *MetricsProvider_Datadog) GetSite() string {\n\tif x != nil {\n\t\treturn x.Site\n\t}\n\treturn \"\"\n}\n\nfunc (x *MetricsProvider_Datadog) GetApiKey() *SecretData {\n\tif x != nil {\n\t\treturn x.ApiKey\n\t}\n\treturn nil\n}\n\ntype ServiceDiscovery_Location struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The base URL of the service (including scheme and port).\n\tBaseUrl string `protobuf:\"bytes,1,opt,name=base_url,json=baseUrl,proto3\" json:\"base_url,omitempty\"`\n\t// The auth methods to use when talking to this service.\n\tAuthMethods   []*ServiceAuth `protobuf:\"bytes,2,rep,name=auth_methods,json=authMethods,proto3\" json:\"auth_methods,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ServiceDiscovery_Location) Reset() {\n\t*x = ServiceDiscovery_Location{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[29]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ServiceDiscovery_Location) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ServiceDiscovery_Location) ProtoMessage() {}\n\nfunc (x *ServiceDiscovery_Location) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[29]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ServiceDiscovery_Location.ProtoReflect.Descriptor instead.\nfunc (*ServiceDiscovery_Location) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{10, 1}\n}\n\nfunc (x *ServiceDiscovery_Location) GetBaseUrl() string {\n\tif x != nil {\n\t\treturn x.BaseUrl\n\t}\n\treturn \"\"\n}\n\nfunc (x *ServiceDiscovery_Location) GetAuthMethods() []*ServiceAuth {\n\tif x != nil {\n\t\treturn x.AuthMethods\n\t}\n\treturn nil\n}\n\ntype RateLimiter_TokenBucket struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// The rate (in events per per second) to allow.\n\tRate float64 `protobuf:\"fixed64,1,opt,name=rate,proto3\" json:\"rate,omitempty\"`\n\t// The burst size to allow.\n\tBurst         uint32 `protobuf:\"varint,2,opt,name=burst,proto3\" json:\"burst,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *RateLimiter_TokenBucket) Reset() {\n\t*x = RateLimiter_TokenBucket{}\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[30]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *RateLimiter_TokenBucket) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*RateLimiter_TokenBucket) ProtoMessage() {}\n\nfunc (x *RateLimiter_TokenBucket) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_runtime_proto_msgTypes[30]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use RateLimiter_TokenBucket.ProtoReflect.Descriptor instead.\nfunc (*RateLimiter_TokenBucket) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_runtime_proto_rawDescGZIP(), []int{13, 0}\n}\n\nfunc (x *RateLimiter_TokenBucket) GetRate() float64 {\n\tif x != nil {\n\t\treturn x.Rate\n\t}\n\treturn 0\n}\n\nfunc (x *RateLimiter_TokenBucket) GetBurst() uint32 {\n\tif x != nil {\n\t\treturn x.Burst\n\t}\n\treturn 0\n}\n\nvar File_encore_runtime_v1_runtime_proto protoreflect.FileDescriptor\n\nconst file_encore_runtime_v1_runtime_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x1fencore/runtime/v1/runtime.proto\\x12\\x11encore.runtime.v1\\x1a\\x1dencore/runtime/v1/infra.proto\\x1a\\\"encore/runtime/v1/secretdata.proto\\x1a\\x1egoogle/protobuf/duration.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"\\xae\\x02\\n\" +\n\t\"\\rRuntimeConfig\\x12@\\n\" +\n\t\"\\venvironment\\x18\\x01 \\x01(\\v2\\x1e.encore.runtime.v1.EnvironmentR\\venvironment\\x127\\n\" +\n\t\"\\x05infra\\x18\\x02 \\x01(\\v2!.encore.runtime.v1.InfrastructureR\\x05infra\\x12=\\n\" +\n\t\"\\n\" +\n\t\"deployment\\x18\\x03 \\x01(\\v2\\x1d.encore.runtime.v1.DeploymentR\\n\" +\n\t\"deployment\\x12O\\n\" +\n\t\"\\x0fencore_platform\\x18\\x05 \\x01(\\v2!.encore.runtime.v1.EncorePlatformH\\x00R\\x0eencorePlatform\\x88\\x01\\x01B\\x12\\n\" +\n\t\"\\x10_encore_platform\\\"\\xcb\\x03\\n\" +\n\t\"\\vEnvironment\\x12\\x15\\n\" +\n\t\"\\x06app_id\\x18\\x01 \\x01(\\tR\\x05appId\\x12\\x19\\n\" +\n\t\"\\bapp_slug\\x18\\x02 \\x01(\\tR\\aappSlug\\x12\\x15\\n\" +\n\t\"\\x06env_id\\x18\\x03 \\x01(\\tR\\x05envId\\x12\\x19\\n\" +\n\t\"\\benv_name\\x18\\x04 \\x01(\\tR\\aenvName\\x12>\\n\" +\n\t\"\\benv_type\\x18\\x05 \\x01(\\x0e2#.encore.runtime.v1.Environment.TypeR\\aenvType\\x12:\\n\" +\n\t\"\\x05cloud\\x18\\x06 \\x01(\\x0e2$.encore.runtime.v1.Environment.CloudR\\x05cloud\\\"j\\n\" +\n\t\"\\x04Type\\x12\\x14\\n\" +\n\t\"\\x10TYPE_UNSPECIFIED\\x10\\x00\\x12\\x14\\n\" +\n\t\"\\x10TYPE_DEVELOPMENT\\x10\\x01\\x12\\x13\\n\" +\n\t\"\\x0fTYPE_PRODUCTION\\x10\\x02\\x12\\x12\\n\" +\n\t\"\\x0eTYPE_EPHEMERAL\\x10\\x03\\x12\\r\\n\" +\n\t\"\\tTYPE_TEST\\x10\\x04\\\"p\\n\" +\n\t\"\\x05Cloud\\x12\\x15\\n\" +\n\t\"\\x11CLOUD_UNSPECIFIED\\x10\\x00\\x12\\x0f\\n\" +\n\t\"\\vCLOUD_LOCAL\\x10\\x01\\x12\\x10\\n\" +\n\t\"\\fCLOUD_ENCORE\\x10\\x02\\x12\\r\\n\" +\n\t\"\\tCLOUD_AWS\\x10\\x03\\x12\\r\\n\" +\n\t\"\\tCLOUD_GCP\\x10\\x04\\x12\\x0f\\n\" +\n\t\"\\vCLOUD_AZURE\\x10\\x05\\\"\\xef\\x04\\n\" +\n\t\"\\n\" +\n\t\"Deployment\\x12\\x1b\\n\" +\n\t\"\\tdeploy_id\\x18\\x01 \\x01(\\tR\\bdeployId\\x12;\\n\" +\n\t\"\\vdeployed_at\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\n\" +\n\t\"deployedAt\\x12/\\n\" +\n\t\"\\x13dynamic_experiments\\x18\\x03 \\x03(\\tR\\x12dynamicExperiments\\x12'\\n\" +\n\t\"\\x0fhosted_gateways\\x18\\x04 \\x03(\\tR\\x0ehostedGateways\\x12I\\n\" +\n\t\"\\x0fhosted_services\\x18\\x05 \\x03(\\v2 .encore.runtime.v1.HostedServiceR\\x0ehostedServices\\x12A\\n\" +\n\t\"\\fauth_methods\\x18\\x06 \\x03(\\v2\\x1e.encore.runtime.v1.ServiceAuthR\\vauthMethods\\x12F\\n\" +\n\t\"\\robservability\\x18\\a \\x01(\\v2 .encore.runtime.v1.ObservabilityR\\robservability\\x12P\\n\" +\n\t\"\\x11service_discovery\\x18\\b \\x01(\\v2#.encore.runtime.v1.ServiceDiscoveryR\\x10serviceDiscovery\\x12P\\n\" +\n\t\"\\x11graceful_shutdown\\x18\\t \\x01(\\v2#.encore.runtime.v1.GracefulShutdownR\\x10gracefulShutdown\\x123\\n\" +\n\t\"\\ametrics\\x18\\n\" +\n\t\" \\x03(\\v2\\x19.encore.runtime.v1.MetricR\\ametrics\\\"\\xc0\\x01\\n\" +\n\t\"\\rObservability\\x12<\\n\" +\n\t\"\\atracing\\x18\\x01 \\x03(\\v2\\\".encore.runtime.v1.TracingProviderR\\atracing\\x12<\\n\" +\n\t\"\\ametrics\\x18\\x02 \\x03(\\v2\\\".encore.runtime.v1.MetricsProviderR\\ametrics\\x123\\n\" +\n\t\"\\x04logs\\x18\\x03 \\x03(\\v2\\x1f.encore.runtime.v1.LogsProviderR\\x04logs\\\"\\x95\\x01\\n\" +\n\t\"\\rHostedService\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12*\\n\" +\n\t\"\\x0eworker_threads\\x18\\x02 \\x01(\\x05H\\x00R\\rworkerThreads\\x88\\x01\\x01\\x12\\\"\\n\" +\n\t\"\\n\" +\n\t\"log_config\\x18\\x03 \\x01(\\tH\\x01R\\tlogConfig\\x88\\x01\\x01B\\x11\\n\" +\n\t\"\\x0f_worker_threadsB\\r\\n\" +\n\t\"\\v_log_config\\\"\\x82\\x02\\n\" +\n\t\"\\vServiceAuth\\x12=\\n\" +\n\t\"\\x04noop\\x18\\n\" +\n\t\" \\x01(\\v2'.encore.runtime.v1.ServiceAuth.NoopAuthH\\x00R\\x04noop\\x12L\\n\" +\n\t\"\\vencore_auth\\x18\\v \\x01(\\v2).encore.runtime.v1.ServiceAuth.EncoreAuthH\\x00R\\n\" +\n\t\"encoreAuth\\x1a\\n\" +\n\t\"\\n\" +\n\t\"\\bNoopAuth\\x1aK\\n\" +\n\t\"\\n\" +\n\t\"EncoreAuth\\x12=\\n\" +\n\t\"\\tauth_keys\\x18\\x01 \\x03(\\v2 .encore.runtime.v1.EncoreAuthKeyR\\bauthKeysB\\r\\n\" +\n\t\"\\vauth_method\\\"\\xdd\\x06\\n\" +\n\t\"\\x0fTracingProvider\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12R\\n\" +\n\t\"\\x06encore\\x18\\n\" +\n\t\" \\x01(\\v28.encore.runtime.v1.TracingProvider.EncoreTracingProviderH\\x00R\\x06encore\\x1a\\xda\\x01\\n\" +\n\t\"\\x15EncoreTracingProvider\\x12%\\n\" +\n\t\"\\x0etrace_endpoint\\x18\\x01 \\x01(\\tR\\rtraceEndpoint\\x12,\\n\" +\n\t\"\\rsampling_rate\\x18\\x02 \\x01(\\x01B\\x02\\x18\\x01H\\x00R\\fsamplingRate\\x88\\x01\\x01\\x12Z\\n\" +\n\t\"\\x0fsampling_config\\x18\\x03 \\x03(\\v21.encore.runtime.v1.TracingProvider.SamplingConfigR\\x0esamplingConfigB\\x10\\n\" +\n\t\"\\x0e_sampling_rate\\x1a\\xfa\\x03\\n\" +\n\t\"\\x0eSamplingConfig\\x12\\x12\\n\" +\n\t\"\\x04rate\\x18\\x01 \\x01(\\x01R\\x04rate\\x122\\n\" +\n\t\"\\adefault\\x18\\x02 \\x01(\\v2\\x16.google.protobuf.EmptyH\\x00R\\adefault\\x12\\x1a\\n\" +\n\t\"\\aservice\\x18\\x03 \\x01(\\tH\\x00R\\aservice\\x12X\\n\" +\n\t\"\\bendpoint\\x18\\x04 \\x01(\\v2:.encore.runtime.v1.TracingProvider.SamplingConfig.EndpointH\\x00R\\bendpoint\\x12\\x16\\n\" +\n\t\"\\x05topic\\x18\\x05 \\x01(\\tH\\x00R\\x05topic\\x12w\\n\" +\n\t\"\\x13pubsub_subscription\\x18\\x06 \\x01(\\v2D.encore.runtime.v1.TracingProvider.SamplingConfig.PubSubSubscriptionH\\x00R\\x12pubsubSubscription\\x1a@\\n\" +\n\t\"\\bEndpoint\\x12\\x18\\n\" +\n\t\"\\aservice\\x18\\x01 \\x01(\\tR\\aservice\\x12\\x1a\\n\" +\n\t\"\\bendpoint\\x18\\x02 \\x01(\\tR\\bendpoint\\x1aN\\n\" +\n\t\"\\x12PubSubSubscription\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x01 \\x01(\\tR\\x05topic\\x12\\\"\\n\" +\n\t\"\\fsubscription\\x18\\x02 \\x01(\\tR\\fsubscriptionB\\a\\n\" +\n\t\"\\x05scopeB\\n\" +\n\t\"\\n\" +\n\t\"\\bprovider\\\"\\xf6\\t\\n\" +\n\t\"\\x0fMetricsProvider\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12J\\n\" +\n\t\"\\x13collection_interval\\x18\\x02 \\x01(\\v2\\x19.google.protobuf.DurationR\\x12collectionInterval\\x12Z\\n\" +\n\t\"\\fencore_cloud\\x18\\n\" +\n\t\" \\x01(\\v25.encore.runtime.v1.MetricsProvider.GCPCloudMonitoringH\\x00R\\vencoreCloud\\x12I\\n\" +\n\t\"\\x03gcp\\x18\\v \\x01(\\v25.encore.runtime.v1.MetricsProvider.GCPCloudMonitoringH\\x00R\\x03gcp\\x12D\\n\" +\n\t\"\\x03aws\\x18\\f \\x01(\\v20.encore.runtime.v1.MetricsProvider.AWSCloudWatchH\\x00R\\x03aws\\x12f\\n\" +\n\t\"\\x11prom_remote_write\\x18\\r \\x01(\\v28.encore.runtime.v1.MetricsProvider.PrometheusRemoteWriteH\\x00R\\x0fpromRemoteWrite\\x12F\\n\" +\n\t\"\\adatadog\\x18\\x0e \\x01(\\v2*.encore.runtime.v1.MetricsProvider.DatadogH\\x00R\\adatadog\\x1a\\xf3\\x03\\n\" +\n\t\"\\x12GCPCloudMonitoring\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"project_id\\x18\\x01 \\x01(\\tR\\tprojectId\\x126\\n\" +\n\t\"\\x17monitored_resource_type\\x18\\x02 \\x01(\\tR\\x15monitoredResourceType\\x12\\x8e\\x01\\n\" +\n\t\"\\x19monitored_resource_labels\\x18\\x03 \\x03(\\v2R.encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MonitoredResourceLabelsEntryR\\x17monitoredResourceLabels\\x12i\\n\" +\n\t\"\\fmetric_names\\x18\\x04 \\x03(\\v2F.encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MetricNamesEntryR\\vmetricNames\\x1aJ\\n\" +\n\t\"\\x1cMonitoredResourceLabelsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\x1a>\\n\" +\n\t\"\\x10MetricNamesEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\x1a-\\n\" +\n\t\"\\rAWSCloudWatch\\x12\\x1c\\n\" +\n\t\"\\tnamespace\\x18\\x01 \\x01(\\tR\\tnamespace\\x1a`\\n\" +\n\t\"\\x15PrometheusRemoteWrite\\x12G\\n\" +\n\t\"\\x10remote_write_url\\x18\\x01 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\x0eremoteWriteUrl\\x1aU\\n\" +\n\t\"\\aDatadog\\x12\\x12\\n\" +\n\t\"\\x04site\\x18\\x01 \\x01(\\tR\\x04site\\x126\\n\" +\n\t\"\\aapi_key\\x18\\x02 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\x06apiKeyB\\n\" +\n\t\"\\n\" +\n\t\"\\bprovider\\\" \\n\" +\n\t\"\\fLogsProvider\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\\"R\\n\" +\n\t\"\\rEncoreAuthKey\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\rR\\x02id\\x121\\n\" +\n\t\"\\x04data\\x18\\x02 \\x01(\\v2\\x1d.encore.runtime.v1.SecretDataR\\x04data\\\"\\xb6\\x02\\n\" +\n\t\"\\x10ServiceDiscovery\\x12M\\n\" +\n\t\"\\bservices\\x18\\x01 \\x03(\\v21.encore.runtime.v1.ServiceDiscovery.ServicesEntryR\\bservices\\x1ai\\n\" +\n\t\"\\rServicesEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12B\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\v2,.encore.runtime.v1.ServiceDiscovery.LocationR\\x05value:\\x028\\x01\\x1ah\\n\" +\n\t\"\\bLocation\\x12\\x19\\n\" +\n\t\"\\bbase_url\\x18\\x01 \\x01(\\tR\\abaseUrl\\x12A\\n\" +\n\t\"\\fauth_methods\\x18\\x02 \\x03(\\v2\\x1e.encore.runtime.v1.ServiceAuthR\\vauthMethods\\\"\\xbc\\x01\\n\" +\n\t\"\\x10GracefulShutdown\\x12/\\n\" +\n\t\"\\x05total\\x18\\x01 \\x01(\\v2\\x19.google.protobuf.DurationR\\x05total\\x12@\\n\" +\n\t\"\\x0eshutdown_hooks\\x18\\x02 \\x01(\\v2\\x19.google.protobuf.DurationR\\rshutdownHooks\\x125\\n\" +\n\t\"\\bhandlers\\x18\\x03 \\x01(\\v2\\x19.google.protobuf.DurationR\\bhandlers\\\"\\xc7\\x01\\n\" +\n\t\"\\x0eEncorePlatform\\x12T\\n\" +\n\t\"\\x15platform_signing_keys\\x18\\x01 \\x03(\\v2 .encore.runtime.v1.EncoreAuthKeyR\\x13platformSigningKeys\\x12N\\n\" +\n\t\"\\fencore_cloud\\x18\\x02 \\x01(\\v2&.encore.runtime.v1.EncoreCloudProviderH\\x00R\\vencoreCloud\\x88\\x01\\x01B\\x0f\\n\" +\n\t\"\\r_encore_cloud\\\"\\x9f\\x01\\n\" +\n\t\"\\vRateLimiter\\x12O\\n\" +\n\t\"\\ftoken_bucket\\x18\\x01 \\x01(\\v2*.encore.runtime.v1.RateLimiter.TokenBucketH\\x00R\\vtokenBucket\\x1a7\\n\" +\n\t\"\\vTokenBucket\\x12\\x12\\n\" +\n\t\"\\x04rate\\x18\\x01 \\x01(\\x01R\\x04rate\\x12\\x14\\n\" +\n\t\"\\x05burst\\x18\\x02 \\x01(\\rR\\x05burstB\\x06\\n\" +\n\t\"\\x04kind\\\"\\x85\\x01\\n\" +\n\t\"\\x13EncoreCloudProvider\\x12\\x10\\n\" +\n\t\"\\x03rid\\x18\\x01 \\x01(\\tR\\x03rid\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"server_url\\x18\\x02 \\x01(\\tR\\tserverUrl\\x12=\\n\" +\n\t\"\\tauth_keys\\x18\\x03 \\x03(\\v2 .encore.runtime.v1.EncoreAuthKeyR\\bauthKeys\\\"E\\n\" +\n\t\"\\x06Metric\\x12\\x1f\\n\" +\n\t\"\\vencore_name\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"encoreName\\x12\\x1a\\n\" +\n\t\"\\bservices\\x18\\x02 \\x03(\\tR\\bservicesB,Z*encr.dev/proto/encore/runtime/v1;runtimev1b\\x06proto3\"\n\nvar (\n\tfile_encore_runtime_v1_runtime_proto_rawDescOnce sync.Once\n\tfile_encore_runtime_v1_runtime_proto_rawDescData []byte\n)\n\nfunc file_encore_runtime_v1_runtime_proto_rawDescGZIP() []byte {\n\tfile_encore_runtime_v1_runtime_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_runtime_v1_runtime_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_runtime_proto_rawDesc), len(file_encore_runtime_v1_runtime_proto_rawDesc)))\n\t})\n\treturn file_encore_runtime_v1_runtime_proto_rawDescData\n}\n\nvar file_encore_runtime_v1_runtime_proto_enumTypes = make([]protoimpl.EnumInfo, 2)\nvar file_encore_runtime_v1_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 31)\nvar file_encore_runtime_v1_runtime_proto_goTypes = []any{\n\t(Environment_Type)(0),                                     // 0: encore.runtime.v1.Environment.Type\n\t(Environment_Cloud)(0),                                    // 1: encore.runtime.v1.Environment.Cloud\n\t(*RuntimeConfig)(nil),                                     // 2: encore.runtime.v1.RuntimeConfig\n\t(*Environment)(nil),                                       // 3: encore.runtime.v1.Environment\n\t(*Deployment)(nil),                                        // 4: encore.runtime.v1.Deployment\n\t(*Observability)(nil),                                     // 5: encore.runtime.v1.Observability\n\t(*HostedService)(nil),                                     // 6: encore.runtime.v1.HostedService\n\t(*ServiceAuth)(nil),                                       // 7: encore.runtime.v1.ServiceAuth\n\t(*TracingProvider)(nil),                                   // 8: encore.runtime.v1.TracingProvider\n\t(*MetricsProvider)(nil),                                   // 9: encore.runtime.v1.MetricsProvider\n\t(*LogsProvider)(nil),                                      // 10: encore.runtime.v1.LogsProvider\n\t(*EncoreAuthKey)(nil),                                     // 11: encore.runtime.v1.EncoreAuthKey\n\t(*ServiceDiscovery)(nil),                                  // 12: encore.runtime.v1.ServiceDiscovery\n\t(*GracefulShutdown)(nil),                                  // 13: encore.runtime.v1.GracefulShutdown\n\t(*EncorePlatform)(nil),                                    // 14: encore.runtime.v1.EncorePlatform\n\t(*RateLimiter)(nil),                                       // 15: encore.runtime.v1.RateLimiter\n\t(*EncoreCloudProvider)(nil),                               // 16: encore.runtime.v1.EncoreCloudProvider\n\t(*Metric)(nil),                                            // 17: encore.runtime.v1.Metric\n\t(*ServiceAuth_NoopAuth)(nil),                              // 18: encore.runtime.v1.ServiceAuth.NoopAuth\n\t(*ServiceAuth_EncoreAuth)(nil),                            // 19: encore.runtime.v1.ServiceAuth.EncoreAuth\n\t(*TracingProvider_EncoreTracingProvider)(nil),             // 20: encore.runtime.v1.TracingProvider.EncoreTracingProvider\n\t(*TracingProvider_SamplingConfig)(nil),                    // 21: encore.runtime.v1.TracingProvider.SamplingConfig\n\t(*TracingProvider_SamplingConfig_Endpoint)(nil),           // 22: encore.runtime.v1.TracingProvider.SamplingConfig.Endpoint\n\t(*TracingProvider_SamplingConfig_PubSubSubscription)(nil), // 23: encore.runtime.v1.TracingProvider.SamplingConfig.PubSubSubscription\n\t(*MetricsProvider_GCPCloudMonitoring)(nil),                // 24: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring\n\t(*MetricsProvider_AWSCloudWatch)(nil),                     // 25: encore.runtime.v1.MetricsProvider.AWSCloudWatch\n\t(*MetricsProvider_PrometheusRemoteWrite)(nil),             // 26: encore.runtime.v1.MetricsProvider.PrometheusRemoteWrite\n\t(*MetricsProvider_Datadog)(nil),                           // 27: encore.runtime.v1.MetricsProvider.Datadog\n\tnil,                                                       // 28: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MonitoredResourceLabelsEntry\n\tnil,                                                       // 29: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MetricNamesEntry\n\tnil,                                                       // 30: encore.runtime.v1.ServiceDiscovery.ServicesEntry\n\t(*ServiceDiscovery_Location)(nil),                         // 31: encore.runtime.v1.ServiceDiscovery.Location\n\t(*RateLimiter_TokenBucket)(nil),                           // 32: encore.runtime.v1.RateLimiter.TokenBucket\n\t(*Infrastructure)(nil),                                    // 33: encore.runtime.v1.Infrastructure\n\t(*timestamppb.Timestamp)(nil),                             // 34: google.protobuf.Timestamp\n\t(*durationpb.Duration)(nil),                               // 35: google.protobuf.Duration\n\t(*SecretData)(nil),                                        // 36: encore.runtime.v1.SecretData\n\t(*emptypb.Empty)(nil),                                     // 37: google.protobuf.Empty\n}\nvar file_encore_runtime_v1_runtime_proto_depIdxs = []int32{\n\t3,  // 0: encore.runtime.v1.RuntimeConfig.environment:type_name -> encore.runtime.v1.Environment\n\t33, // 1: encore.runtime.v1.RuntimeConfig.infra:type_name -> encore.runtime.v1.Infrastructure\n\t4,  // 2: encore.runtime.v1.RuntimeConfig.deployment:type_name -> encore.runtime.v1.Deployment\n\t14, // 3: encore.runtime.v1.RuntimeConfig.encore_platform:type_name -> encore.runtime.v1.EncorePlatform\n\t0,  // 4: encore.runtime.v1.Environment.env_type:type_name -> encore.runtime.v1.Environment.Type\n\t1,  // 5: encore.runtime.v1.Environment.cloud:type_name -> encore.runtime.v1.Environment.Cloud\n\t34, // 6: encore.runtime.v1.Deployment.deployed_at:type_name -> google.protobuf.Timestamp\n\t6,  // 7: encore.runtime.v1.Deployment.hosted_services:type_name -> encore.runtime.v1.HostedService\n\t7,  // 8: encore.runtime.v1.Deployment.auth_methods:type_name -> encore.runtime.v1.ServiceAuth\n\t5,  // 9: encore.runtime.v1.Deployment.observability:type_name -> encore.runtime.v1.Observability\n\t12, // 10: encore.runtime.v1.Deployment.service_discovery:type_name -> encore.runtime.v1.ServiceDiscovery\n\t13, // 11: encore.runtime.v1.Deployment.graceful_shutdown:type_name -> encore.runtime.v1.GracefulShutdown\n\t17, // 12: encore.runtime.v1.Deployment.metrics:type_name -> encore.runtime.v1.Metric\n\t8,  // 13: encore.runtime.v1.Observability.tracing:type_name -> encore.runtime.v1.TracingProvider\n\t9,  // 14: encore.runtime.v1.Observability.metrics:type_name -> encore.runtime.v1.MetricsProvider\n\t10, // 15: encore.runtime.v1.Observability.logs:type_name -> encore.runtime.v1.LogsProvider\n\t18, // 16: encore.runtime.v1.ServiceAuth.noop:type_name -> encore.runtime.v1.ServiceAuth.NoopAuth\n\t19, // 17: encore.runtime.v1.ServiceAuth.encore_auth:type_name -> encore.runtime.v1.ServiceAuth.EncoreAuth\n\t20, // 18: encore.runtime.v1.TracingProvider.encore:type_name -> encore.runtime.v1.TracingProvider.EncoreTracingProvider\n\t35, // 19: encore.runtime.v1.MetricsProvider.collection_interval:type_name -> google.protobuf.Duration\n\t24, // 20: encore.runtime.v1.MetricsProvider.encore_cloud:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring\n\t24, // 21: encore.runtime.v1.MetricsProvider.gcp:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring\n\t25, // 22: encore.runtime.v1.MetricsProvider.aws:type_name -> encore.runtime.v1.MetricsProvider.AWSCloudWatch\n\t26, // 23: encore.runtime.v1.MetricsProvider.prom_remote_write:type_name -> encore.runtime.v1.MetricsProvider.PrometheusRemoteWrite\n\t27, // 24: encore.runtime.v1.MetricsProvider.datadog:type_name -> encore.runtime.v1.MetricsProvider.Datadog\n\t36, // 25: encore.runtime.v1.EncoreAuthKey.data:type_name -> encore.runtime.v1.SecretData\n\t30, // 26: encore.runtime.v1.ServiceDiscovery.services:type_name -> encore.runtime.v1.ServiceDiscovery.ServicesEntry\n\t35, // 27: encore.runtime.v1.GracefulShutdown.total:type_name -> google.protobuf.Duration\n\t35, // 28: encore.runtime.v1.GracefulShutdown.shutdown_hooks:type_name -> google.protobuf.Duration\n\t35, // 29: encore.runtime.v1.GracefulShutdown.handlers:type_name -> google.protobuf.Duration\n\t11, // 30: encore.runtime.v1.EncorePlatform.platform_signing_keys:type_name -> encore.runtime.v1.EncoreAuthKey\n\t16, // 31: encore.runtime.v1.EncorePlatform.encore_cloud:type_name -> encore.runtime.v1.EncoreCloudProvider\n\t32, // 32: encore.runtime.v1.RateLimiter.token_bucket:type_name -> encore.runtime.v1.RateLimiter.TokenBucket\n\t11, // 33: encore.runtime.v1.EncoreCloudProvider.auth_keys:type_name -> encore.runtime.v1.EncoreAuthKey\n\t11, // 34: encore.runtime.v1.ServiceAuth.EncoreAuth.auth_keys:type_name -> encore.runtime.v1.EncoreAuthKey\n\t21, // 35: encore.runtime.v1.TracingProvider.EncoreTracingProvider.sampling_config:type_name -> encore.runtime.v1.TracingProvider.SamplingConfig\n\t37, // 36: encore.runtime.v1.TracingProvider.SamplingConfig.default:type_name -> google.protobuf.Empty\n\t22, // 37: encore.runtime.v1.TracingProvider.SamplingConfig.endpoint:type_name -> encore.runtime.v1.TracingProvider.SamplingConfig.Endpoint\n\t23, // 38: encore.runtime.v1.TracingProvider.SamplingConfig.pubsub_subscription:type_name -> encore.runtime.v1.TracingProvider.SamplingConfig.PubSubSubscription\n\t28, // 39: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.monitored_resource_labels:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MonitoredResourceLabelsEntry\n\t29, // 40: encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.metric_names:type_name -> encore.runtime.v1.MetricsProvider.GCPCloudMonitoring.MetricNamesEntry\n\t36, // 41: encore.runtime.v1.MetricsProvider.PrometheusRemoteWrite.remote_write_url:type_name -> encore.runtime.v1.SecretData\n\t36, // 42: encore.runtime.v1.MetricsProvider.Datadog.api_key:type_name -> encore.runtime.v1.SecretData\n\t31, // 43: encore.runtime.v1.ServiceDiscovery.ServicesEntry.value:type_name -> encore.runtime.v1.ServiceDiscovery.Location\n\t7,  // 44: encore.runtime.v1.ServiceDiscovery.Location.auth_methods:type_name -> encore.runtime.v1.ServiceAuth\n\t45, // [45:45] is the sub-list for method output_type\n\t45, // [45:45] is the sub-list for method input_type\n\t45, // [45:45] is the sub-list for extension type_name\n\t45, // [45:45] is the sub-list for extension extendee\n\t0,  // [0:45] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_runtime_v1_runtime_proto_init() }\nfunc file_encore_runtime_v1_runtime_proto_init() {\n\tif File_encore_runtime_v1_runtime_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_runtime_v1_infra_proto_init()\n\tfile_encore_runtime_v1_secretdata_proto_init()\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[0].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[4].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[5].OneofWrappers = []any{\n\t\t(*ServiceAuth_Noop)(nil),\n\t\t(*ServiceAuth_EncoreAuth_)(nil),\n\t}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[6].OneofWrappers = []any{\n\t\t(*TracingProvider_Encore)(nil),\n\t}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[7].OneofWrappers = []any{\n\t\t(*MetricsProvider_EncoreCloud)(nil),\n\t\t(*MetricsProvider_Gcp)(nil),\n\t\t(*MetricsProvider_Aws)(nil),\n\t\t(*MetricsProvider_PromRemoteWrite)(nil),\n\t\t(*MetricsProvider_Datadog_)(nil),\n\t}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[12].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[13].OneofWrappers = []any{\n\t\t(*RateLimiter_TokenBucket_)(nil),\n\t}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[18].OneofWrappers = []any{}\n\tfile_encore_runtime_v1_runtime_proto_msgTypes[19].OneofWrappers = []any{\n\t\t(*TracingProvider_SamplingConfig_Default)(nil),\n\t\t(*TracingProvider_SamplingConfig_Service)(nil),\n\t\t(*TracingProvider_SamplingConfig_Endpoint_)(nil),\n\t\t(*TracingProvider_SamplingConfig_Topic)(nil),\n\t\t(*TracingProvider_SamplingConfig_PubsubSubscription)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_runtime_proto_rawDesc), len(file_encore_runtime_v1_runtime_proto_rawDesc)),\n\t\t\tNumEnums:      2,\n\t\t\tNumMessages:   31,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_runtime_v1_runtime_proto_goTypes,\n\t\tDependencyIndexes: file_encore_runtime_v1_runtime_proto_depIdxs,\n\t\tEnumInfos:         file_encore_runtime_v1_runtime_proto_enumTypes,\n\t\tMessageInfos:      file_encore_runtime_v1_runtime_proto_msgTypes,\n\t}.Build()\n\tFile_encore_runtime_v1_runtime_proto = out.File\n\tfile_encore_runtime_v1_runtime_proto_goTypes = nil\n\tfile_encore_runtime_v1_runtime_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/runtime/v1/runtime.proto",
    "content": "syntax = \"proto3\";\npackage encore.runtime.v1;\n\nimport \"encore/runtime/v1/infra.proto\";\nimport \"encore/runtime/v1/secretdata.proto\";\nimport \"google/protobuf/duration.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"encr.dev/proto/encore/runtime/v1;runtimev1\";\n\nmessage RuntimeConfig {\n  Environment environment = 1;\n  Infrastructure infra = 2;\n  Deployment deployment = 3;\n  optional EncorePlatform encore_platform = 5;\n}\n\nmessage Environment {\n  string app_id = 1;\n  string app_slug = 2;\n  string env_id = 3;\n  string env_name = 4;\n  Type env_type = 5;\n  Cloud cloud = 6;\n\n  enum Type {\n    TYPE_UNSPECIFIED = 0;\n    TYPE_DEVELOPMENT = 1;\n    TYPE_PRODUCTION = 2;\n    TYPE_EPHEMERAL = 3;\n    TYPE_TEST = 4;\n  }\n\n  enum Cloud {\n    CLOUD_UNSPECIFIED = 0;\n    CLOUD_LOCAL = 1;\n    CLOUD_ENCORE = 2;\n    CLOUD_AWS = 3;\n    CLOUD_GCP = 4;\n    CLOUD_AZURE = 5;\n  }\n}\n\n// Describes the configuration related to a specific deployment,\n// meaning a group of services deployed together (think a single k8s Deployment).\nmessage Deployment {\n  string deploy_id = 1;\n  google.protobuf.Timestamp deployed_at = 2;\n\n  // A list of experiments to enable at runtime.\n  repeated string dynamic_experiments = 3;\n\n  // The gateways hosted by this deployment, by rid.\n  repeated string hosted_gateways = 4;\n\n  // The services hosted by this deployment.\n  repeated HostedService hosted_services = 5;\n\n  // The authentication method(s) that can be used when talking\n  // to this deployment for internal service-to-service calls.\n  //\n  // An empty list means no service-to-service calls can be made to this deployment.\n  repeated ServiceAuth auth_methods = 6;\n\n  // Observability-related configuration.\n  Observability observability = 7;\n\n  // Service discovery configuration.\n  ServiceDiscovery service_discovery = 8;\n\n  // Graceful shutdown behavior.\n  GracefulShutdown graceful_shutdown = 9;\n\n  // The metrics used by this deployment.\n  repeated Metric metrics = 10;\n}\n\nmessage Observability {\n  // The observability providers to use.\n  repeated TracingProvider tracing = 1;\n  repeated MetricsProvider metrics = 2;\n  repeated LogsProvider logs = 3;\n}\n\nmessage HostedService {\n  // The name of the service.\n  string name = 1;\n\n  // Number of worker threads to use.\n  // If unset it defaults to 1. If set to 0 the runtime\n  // automatically determines the number of threads to use\n  // based on the number of CPUs available.\n  optional int32 worker_threads = 2;\n\n  // The log configuration to use for this service.\n  // If unset it defaults to \"trace\".\n  optional string log_config = 3;\n}\n\nmessage ServiceAuth {\n  // The auth method to use.\n  oneof auth_method {\n    // Messages start at 10 to allow for other fields on ServiceAuth.\n    NoopAuth noop = 10;\n    EncoreAuth encore_auth = 11;\n  }\n\n  message NoopAuth {}\n  message EncoreAuth {\n    repeated EncoreAuthKey auth_keys = 1;\n  }\n}\n\nmessage TracingProvider {\n  // The unique resource id for this provider.\n  string rid = 1;\n\n  oneof provider {\n    EncoreTracingProvider encore = 10;\n  }\n\n  message EncoreTracingProvider {\n    string trace_endpoint = 1;\n\n    // Deprecated: Use sampling_config instead.\n    optional double sampling_rate = 2 [deprecated = true];\n\n    // Sampling rates for different scopes.\n    //\n    // When deciding whether to sample a trace, the most specific matching\n    // scope wins.\n    //\n    // If no scope matches, all traces are sampled.\n    repeated SamplingConfig sampling_config = 3;\n  }\n\n  message SamplingConfig {\n    // The sampling rate, between [0, 1].\n    // 0 means never sample, 1 means always sample.\n    double rate = 1;\n\n    // The scope this rate applies to.\n    oneof scope {\n      // Applies to all traces that don't match a more specific scope.\n      google.protobuf.Empty default = 2;\n      // Applies to all endpoints within the named service.\n      string service = 3;\n      // Applies to a specific API endpoint.\n      Endpoint endpoint = 4;\n      // Applies to all subscriptions within the named topic.\n      string topic = 5;\n      // Applies to a specific pubsub subscription.\n      PubSubSubscription pubsub_subscription = 6;\n    }\n\n    message Endpoint {\n      string service = 1;\n      string endpoint = 2;\n    }\n\n    message PubSubSubscription {\n      string topic = 1;\n      string subscription = 2;\n    }\n  }\n}\n\nmessage MetricsProvider {\n  // The unique resource id for this provider.\n  string rid = 1;\n\n  google.protobuf.Duration collection_interval = 2;\n\n  oneof provider {\n    GCPCloudMonitoring encore_cloud = 10;\n    GCPCloudMonitoring gcp = 11;\n    AWSCloudWatch aws = 12;\n    PrometheusRemoteWrite prom_remote_write = 13;\n    Datadog datadog = 14;\n  }\n\n  message GCPCloudMonitoring {\n    // The GCP project id to send metrics to.\n    string project_id = 1;\n\n    // The enum value for the monitored resource this application is monitoring.\n    // See https://cloud.google.com/monitoring/api/resources for valid values.\n    string monitored_resource_type = 2;\n\n    // The labels to specify for the monitored resource.\n    // Each monitored resource type has a pre-defined set of labels that must be set.\n    // See https://cloud.google.com/monitoring/api/resources for expected labels.\n    map<string, string> monitored_resource_labels = 3;\n\n    // The mapping between metric names in Encore and metric names in GCP.\n    map<string, string> metric_names = 4;\n  }\n\n  message AWSCloudWatch {\n    // The namespace to use for metrics.\n    string namespace = 1;\n  }\n\n  message PrometheusRemoteWrite {\n    // The URL to send metrics to.\n    SecretData remote_write_url = 1;\n  }\n\n  message Datadog {\n    string site = 1;\n    SecretData api_key = 2;\n  }\n}\n\nmessage LogsProvider {\n  // The unique resource id for this provider.\n  string rid = 1;\n\n  // Not yet implemented.\n}\n\nmessage EncoreAuthKey {\n  uint32 id = 1;\n  SecretData data = 2;\n}\n\n// Describes service discovery configuration.\nmessage ServiceDiscovery {\n  // Where services are located, keyed by the service name.\n  map<string, Location> services = 1;\n\n  message Location {\n    // The base URL of the service (including scheme and port).\n    string base_url = 1;\n\n    // The auth methods to use when talking to this service.\n    repeated ServiceAuth auth_methods = 2;\n  }\n}\n\n// GracefulShutdown defines the graceful shutdown timings.\nmessage GracefulShutdown {\n  // Total is how long we allow the total shutdown to take\n  // before the process forcibly exits.\n  google.protobuf.Duration total = 1;\n\n  // ShutdownHooks is how long before [total] runs out that we cancel\n  // the context that is passed to the shutdown hooks.\n  //\n  // It is expected that ShutdownHooks is a larger value than Handlers.\n  google.protobuf.Duration shutdown_hooks = 2;\n\n  // Handlers is how long before [total] runs out that we cancel\n  // the context that is passed to API and PubSub Subscription handlers.\n  //\n  // For example, if [total] is 10 seconds and [handlers] is 2 seconds,\n  // then we will cancel the context passed to handlers 8 seconds after\n  // a graceful shutdown is initiated.\n  google.protobuf.Duration handlers = 3;\n}\n\nmessage EncorePlatform {\n  // Auth keys for validating signed requests from the Encore Platform.\n  repeated EncoreAuthKey platform_signing_keys = 1;\n\n  // The Encore Cloud configuration to use, if running in Encore Cloud.\n  optional EncoreCloudProvider encore_cloud = 2;\n}\n\nmessage RateLimiter {\n  oneof kind {\n    TokenBucket token_bucket = 1;\n  }\n\n  message TokenBucket {\n    // The rate (in events per per second) to allow.\n    double rate = 1;\n    // The burst size to allow.\n    uint32 burst = 2;\n  }\n}\n\nmessage EncoreCloudProvider {\n  // The unique resource id for this provider.\n  string rid = 1;\n\n  // URL to use to authenticate with the server.\n  string server_url = 2;\n\n  repeated EncoreAuthKey auth_keys = 3;\n}\n\nmessage Metric {\n  // The encore name of the metric.\n  string encore_name = 1;\n\n  // The services that have access to this metric.\n  repeated string services = 2;\n}\n"
  },
  {
    "path": "proto/encore/runtime/v1/secretdata.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: encore/runtime/v1/secretdata.proto\n\npackage runtimev1\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype SecretData_Encoding int32\n\nconst (\n\t// Indicates the value is used as-is.\n\tSecretData_ENCODING_NONE SecretData_Encoding = 0\n\t// Indicates the value is base64-encoded.\n\tSecretData_ENCODING_BASE64 SecretData_Encoding = 1\n\t// Indicates the value is gzip-compressed and then base64-encoded.\n\tSecretData_ENCODING_GZIP SecretData_Encoding = 2\n)\n\n// Enum value maps for SecretData_Encoding.\nvar (\n\tSecretData_Encoding_name = map[int32]string{\n\t\t0: \"ENCODING_NONE\",\n\t\t1: \"ENCODING_BASE64\",\n\t\t2: \"ENCODING_GZIP\",\n\t}\n\tSecretData_Encoding_value = map[string]int32{\n\t\t\"ENCODING_NONE\":   0,\n\t\t\"ENCODING_BASE64\": 1,\n\t\t\"ENCODING_GZIP\":   2,\n\t}\n)\n\nfunc (x SecretData_Encoding) Enum() *SecretData_Encoding {\n\tp := new(SecretData_Encoding)\n\t*p = x\n\treturn p\n}\n\nfunc (x SecretData_Encoding) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (SecretData_Encoding) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_encore_runtime_v1_secretdata_proto_enumTypes[0].Descriptor()\n}\n\nfunc (SecretData_Encoding) Type() protoreflect.EnumType {\n\treturn &file_encore_runtime_v1_secretdata_proto_enumTypes[0]\n}\n\nfunc (x SecretData_Encoding) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use SecretData_Encoding.Descriptor instead.\nfunc (SecretData_Encoding) EnumDescriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_secretdata_proto_rawDescGZIP(), []int{0, 0}\n}\n\n// Defines how to resolve a secret value.\ntype SecretData struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// How to resolve the initial secret value.\n\t// The output of this step is always a byte slice.\n\t//\n\t// Types that are valid to be assigned to Source:\n\t//\n\t//\t*SecretData_Embedded\n\t//\t*SecretData_Env\n\tSource isSecretData_Source `protobuf_oneof:\"source\"`\n\t// How the value is encoded.\n\tEncoding SecretData_Encoding `protobuf:\"varint,20,opt,name=encoding,proto3,enum=encore.runtime.v1.SecretData_Encoding\" json:\"encoding,omitempty\"`\n\t// sub_path is an optional path to a sub-value within the secret data.\n\t//\n\t// Types that are valid to be assigned to SubPath:\n\t//\n\t//\t*SecretData_JsonKey\n\tSubPath       isSecretData_SubPath `protobuf_oneof:\"sub_path\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SecretData) Reset() {\n\t*x = SecretData{}\n\tmi := &file_encore_runtime_v1_secretdata_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SecretData) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SecretData) ProtoMessage() {}\n\nfunc (x *SecretData) ProtoReflect() protoreflect.Message {\n\tmi := &file_encore_runtime_v1_secretdata_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SecretData.ProtoReflect.Descriptor instead.\nfunc (*SecretData) Descriptor() ([]byte, []int) {\n\treturn file_encore_runtime_v1_secretdata_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *SecretData) GetSource() isSecretData_Source {\n\tif x != nil {\n\t\treturn x.Source\n\t}\n\treturn nil\n}\n\nfunc (x *SecretData) GetEmbedded() []byte {\n\tif x != nil {\n\t\tif x, ok := x.Source.(*SecretData_Embedded); ok {\n\t\t\treturn x.Embedded\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *SecretData) GetEnv() string {\n\tif x != nil {\n\t\tif x, ok := x.Source.(*SecretData_Env); ok {\n\t\t\treturn x.Env\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (x *SecretData) GetEncoding() SecretData_Encoding {\n\tif x != nil {\n\t\treturn x.Encoding\n\t}\n\treturn SecretData_ENCODING_NONE\n}\n\nfunc (x *SecretData) GetSubPath() isSecretData_SubPath {\n\tif x != nil {\n\t\treturn x.SubPath\n\t}\n\treturn nil\n}\n\nfunc (x *SecretData) GetJsonKey() string {\n\tif x != nil {\n\t\tif x, ok := x.SubPath.(*SecretData_JsonKey); ok {\n\t\t\treturn x.JsonKey\n\t\t}\n\t}\n\treturn \"\"\n}\n\ntype isSecretData_Source interface {\n\tisSecretData_Source()\n}\n\ntype SecretData_Embedded struct {\n\t// The secret data is embedded directly in the configuration.\n\t// This is insecure unless `encrypted` is true, and should only\n\t// be used for local development.\n\tEmbedded []byte `protobuf:\"bytes,1,opt,name=embedded,proto3,oneof\"`\n}\n\ntype SecretData_Env struct {\n\t// Look up the secret data in an env variable with the given name.\n\t// Assumes the\n\tEnv string `protobuf:\"bytes,2,opt,name=env,proto3,oneof\"`\n}\n\nfunc (*SecretData_Embedded) isSecretData_Source() {}\n\nfunc (*SecretData_Env) isSecretData_Source() {}\n\ntype isSecretData_SubPath interface {\n\tisSecretData_SubPath()\n}\n\ntype SecretData_JsonKey struct {\n\t// json_key indicates the secret data is a JSON map,\n\t// and the resolved secret value is a key in that map.\n\t//\n\t// The value is encoded differently based on its type.\n\t// Supported types are utf-8 strings and raw bytes:\n\t// - For strings, the value is the string itself, e.g. \"foo\".\n\t// - For raw bytes, the value is a JSON object with a single key \"bytes\" and the value is the base64-encoded bytes.\n\t//\n\t// For example: '{\"foo\": \"string-value\", \"bar\": {\"bytes\": \"aGVsbG8=\"}}'.\n\tJsonKey string `protobuf:\"bytes,10,opt,name=json_key,json=jsonKey,proto3,oneof\"`\n}\n\nfunc (*SecretData_JsonKey) isSecretData_SubPath() {}\n\nvar File_encore_runtime_v1_secretdata_proto protoreflect.FileDescriptor\n\nconst file_encore_runtime_v1_secretdata_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\\"encore/runtime/v1/secretdata.proto\\x12\\x11encore.runtime.v1\\\"\\x88\\x02\\n\" +\n\t\"\\n\" +\n\t\"SecretData\\x12\\x1c\\n\" +\n\t\"\\bembedded\\x18\\x01 \\x01(\\fH\\x00R\\bembedded\\x12\\x12\\n\" +\n\t\"\\x03env\\x18\\x02 \\x01(\\tH\\x00R\\x03env\\x12B\\n\" +\n\t\"\\bencoding\\x18\\x14 \\x01(\\x0e2&.encore.runtime.v1.SecretData.EncodingR\\bencoding\\x12\\x1b\\n\" +\n\t\"\\bjson_key\\x18\\n\" +\n\t\" \\x01(\\tH\\x01R\\ajsonKey\\\"E\\n\" +\n\t\"\\bEncoding\\x12\\x11\\n\" +\n\t\"\\rENCODING_NONE\\x10\\x00\\x12\\x13\\n\" +\n\t\"\\x0fENCODING_BASE64\\x10\\x01\\x12\\x11\\n\" +\n\t\"\\rENCODING_GZIP\\x10\\x02B\\b\\n\" +\n\t\"\\x06sourceB\\n\" +\n\t\"\\n\" +\n\t\"\\bsub_pathJ\\x04\\b\\x03\\x10\\n\" +\n\t\"J\\x04\\b\\f\\x10\\x14B,Z*encr.dev/proto/encore/runtime/v1;runtimev1b\\x06proto3\"\n\nvar (\n\tfile_encore_runtime_v1_secretdata_proto_rawDescOnce sync.Once\n\tfile_encore_runtime_v1_secretdata_proto_rawDescData []byte\n)\n\nfunc file_encore_runtime_v1_secretdata_proto_rawDescGZIP() []byte {\n\tfile_encore_runtime_v1_secretdata_proto_rawDescOnce.Do(func() {\n\t\tfile_encore_runtime_v1_secretdata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_secretdata_proto_rawDesc), len(file_encore_runtime_v1_secretdata_proto_rawDesc)))\n\t})\n\treturn file_encore_runtime_v1_secretdata_proto_rawDescData\n}\n\nvar file_encore_runtime_v1_secretdata_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_encore_runtime_v1_secretdata_proto_msgTypes = make([]protoimpl.MessageInfo, 1)\nvar file_encore_runtime_v1_secretdata_proto_goTypes = []any{\n\t(SecretData_Encoding)(0), // 0: encore.runtime.v1.SecretData.Encoding\n\t(*SecretData)(nil),       // 1: encore.runtime.v1.SecretData\n}\nvar file_encore_runtime_v1_secretdata_proto_depIdxs = []int32{\n\t0, // 0: encore.runtime.v1.SecretData.encoding:type_name -> encore.runtime.v1.SecretData.Encoding\n\t1, // [1:1] is the sub-list for method output_type\n\t1, // [1:1] is the sub-list for method input_type\n\t1, // [1:1] is the sub-list for extension type_name\n\t1, // [1:1] is the sub-list for extension extendee\n\t0, // [0:1] is the sub-list for field type_name\n}\n\nfunc init() { file_encore_runtime_v1_secretdata_proto_init() }\nfunc file_encore_runtime_v1_secretdata_proto_init() {\n\tif File_encore_runtime_v1_secretdata_proto != nil {\n\t\treturn\n\t}\n\tfile_encore_runtime_v1_secretdata_proto_msgTypes[0].OneofWrappers = []any{\n\t\t(*SecretData_Embedded)(nil),\n\t\t(*SecretData_Env)(nil),\n\t\t(*SecretData_JsonKey)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_encore_runtime_v1_secretdata_proto_rawDesc), len(file_encore_runtime_v1_secretdata_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   1,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_encore_runtime_v1_secretdata_proto_goTypes,\n\t\tDependencyIndexes: file_encore_runtime_v1_secretdata_proto_depIdxs,\n\t\tEnumInfos:         file_encore_runtime_v1_secretdata_proto_enumTypes,\n\t\tMessageInfos:      file_encore_runtime_v1_secretdata_proto_msgTypes,\n\t}.Build()\n\tFile_encore_runtime_v1_secretdata_proto = out.File\n\tfile_encore_runtime_v1_secretdata_proto_goTypes = nil\n\tfile_encore_runtime_v1_secretdata_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "proto/encore/runtime/v1/secretdata.proto",
    "content": "syntax = \"proto3\";\npackage encore.runtime.v1;\n\noption go_package = \"encr.dev/proto/encore/runtime/v1;runtimev1\";\n\n// Defines how to resolve a secret value.\nmessage SecretData {\n  // How to resolve the initial secret value.\n  // The output of this step is always a byte slice.\n  oneof source {\n    // The secret data is embedded directly in the configuration.\n    // This is insecure unless `encrypted` is true, and should only\n    // be used for local development.\n    bytes embedded = 1;\n\n    // Look up the secret data in an env variable with the given name.\n    // Assumes the\n    string env = 2;\n  }\n  reserved 3 to 9; // for future sources\n\n  // How the value is encoded.\n  Encoding encoding = 20;\n\n  // sub_path is an optional path to a sub-value within the secret data.\n  oneof sub_path {\n    // json_key indicates the secret data is a JSON map,\n    // and the resolved secret value is a key in that map.\n    //\n    // The value is encoded differently based on its type.\n    // Supported types are utf-8 strings and raw bytes:\n    // - For strings, the value is the string itself, e.g. \"foo\".\n    // - For raw bytes, the value is a JSON object with a single key \"bytes\" and the value is the base64-encoded bytes.\n    //\n    // For example: '{\"foo\": \"string-value\", \"bar\": {\"bytes\": \"aGVsbG8=\"}}'.\n    string json_key = 10;\n\n    // null: the raw secret data is the resolved value.\n  }\n  reserved 12 to 19; // for future sub_paths\n\n  enum Encoding {\n    // Indicates the value is used as-is.\n    ENCODING_NONE = 0;\n    // Indicates the value is base64-encoded.\n    ENCODING_BASE64 = 1;\n    // Indicates the value is gzip-compressed and then base64-encoded.\n    ENCODING_GZIP = 2;\n  }\n}\n"
  },
  {
    "path": "proto/gen.go",
    "content": "package pb\n\n//go:generate ./gen.sh\n"
  },
  {
    "path": "proto/gen.sh",
    "content": "#!/usr/bin/env bash\n\nset -e -x\n\nGO_OPT=paths=source_relative\nGRPC_OPT=paths=source_relative\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n  ./encore/parser/meta/v1/meta.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n  ./encore/parser/schema/v1/schema.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n  ./encore/engine/trace/trace.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n  ./encore/engine/trace2/trace2.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT --go-grpc_out=. --go-grpc_opt=$GRPC_OPT \\\n  ./encore/daemon/daemon.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n./encore/runtime/v1/infra.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n./encore/runtime/v1/runtime.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n./encore/runtime/v1/secretdata.proto\n\nprotoc -I . --go_out=. --go_opt=$GO_OPT \\\n./encore/runtime/v1/secretdata.proto\n\n# Prometheus protos for metrics exporter\nprotoc -I . --go_out=../runtimes/go/appruntime/infrasdk/metrics/prometheus --go_opt=$GO_OPT \\\n./prompb/types.proto\nprotoc -I . --go_out=../runtimes/go/appruntime/infrasdk/metrics/prometheus --go_opt=$GO_OPT \\\n./prompb/remote.proto\n"
  },
  {
    "path": "proto/prompb/remote.proto",
    "content": "// Copyright 2016 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\npackage prometheus;\n\noption go_package = \"encore.dev/appruntime/metrics/prometheus/prompb\";\n\nimport \"prompb/types.proto\";\n\nmessage WriteRequest {\n  repeated prometheus.TimeSeries timeseries = 1;\n  // Cortex uses this field to determine the source of the write request.\n  // We reserve it to avoid any compatibility issues.\n  reserved 2;\n  repeated prometheus.MetricMetadata metadata = 3;\n}\n\n// ReadRequest represents a remote read request.\nmessage ReadRequest {\n  repeated Query queries = 1;\n\n  enum ResponseType {\n    // Server will return a single ReadResponse message with matched series that\n    // includes list of raw samples. It's recommended to use streamed response\n    // types instead.\n    //\n    // Response headers:\n    // Content-Type: \"application/x-protobuf\"\n    // Content-Encoding: \"snappy\"\n    SAMPLES = 0;\n    // Server will stream a delimited ChunkedReadResponse message that\n    // contains XOR or HISTOGRAM(!) encoded chunks for a single series.\n    // Each message is following varint size and fixed size bigendian\n    // uint32 for CRC32 Castagnoli checksum.\n    //\n    // Response headers:\n    // Content-Type: \"application/x-streamed-protobuf;\n    // proto=prometheus.ChunkedReadResponse\" Content-Encoding: \"\"\n    STREAMED_XOR_CHUNKS = 1;\n  }\n\n  // accepted_response_types allows negotiating the content type of the\n  // response.\n  //\n  // Response types are taken from the list in the FIFO order. If no response\n  // type in `accepted_response_types` is implemented by server, error is\n  // returned. For request that do not contain `accepted_response_types` field\n  // the SAMPLES response type will be used.\n  repeated ResponseType accepted_response_types = 2;\n}\n\n// ReadResponse is a response when response_type equals SAMPLES.\nmessage ReadResponse {\n  // In same order as the request's queries.\n  repeated QueryResult results = 1;\n}\n\nmessage Query {\n  int64 start_timestamp_ms = 1;\n  int64 end_timestamp_ms = 2;\n  repeated prometheus.LabelMatcher matchers = 3;\n  prometheus.ReadHints hints = 4;\n}\n\nmessage QueryResult {\n  // Samples within a time series must be ordered by time.\n  repeated prometheus.TimeSeries timeseries = 1;\n}\n\n// ChunkedReadResponse is a response when response_type equals\n// STREAMED_XOR_CHUNKS. We strictly stream full series after series, optionally\n// split by time. This means that a single frame can contain partition of the\n// single series, but once a new series is started to be streamed it means that\n// no more chunks will be sent for previous one. Series are returned sorted in\n// the same way TSDB block are internally.\nmessage ChunkedReadResponse {\n  repeated prometheus.ChunkedSeries chunked_series = 1;\n\n  // query_index represents an index of the query from ReadRequest.queries these\n  // chunks relates to.\n  int64 query_index = 2;\n}\n"
  },
  {
    "path": "proto/prompb/types.proto",
    "content": "// Copyright 2017 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\npackage prometheus;\n\noption go_package = \"encore.dev/appruntime/metrics/prometheus/prompb\";\n\nmessage MetricMetadata {\n  enum MetricType {\n    UNKNOWN        = 0;\n    COUNTER        = 1;\n    GAUGE          = 2;\n    HISTOGRAM      = 3;\n    GAUGEHISTOGRAM = 4;\n    SUMMARY        = 5;\n    INFO           = 6;\n    STATESET       = 7;\n  }\n\n  // Represents the metric type, these match the set from Prometheus.\n  // Refer to model/textparse/interface.go for details.\n  MetricType type = 1;\n  string metric_family_name = 2;\n  string help = 4;\n  string unit = 5;\n}\n\nmessage Sample {\n  double value    = 1;\n  // timestamp is in ms format, see model/timestamp/timestamp.go for\n  // conversion from time.Time to Prometheus timestamp.\n  int64 timestamp = 2;\n}\n\nmessage Exemplar {\n  // Optional, can be empty.\n  repeated Label labels = 1;\n  double value = 2;\n  // timestamp is in ms format, see model/timestamp/timestamp.go for\n  // conversion from time.Time to Prometheus timestamp.\n  int64 timestamp = 3;\n}\n\n// A native histogram, also known as a sparse histogram.\n// Original design doc:\n// https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit\n// The appendix of this design doc also explains the concept of float\n// histograms. This Histogram message can represent both, the usual\n// integer histogram as well as a float histogram.\nmessage Histogram {\n  enum ResetHint {\n    UNKNOWN = 0; // Need to test for a counter reset explicitly.\n    YES     = 1; // This is the 1st histogram after a counter reset.\n    NO      = 2; // There was no counter reset between this and the previous Histogram.\n    GAUGE   = 3; // This is a gauge histogram where counter resets don't happen.\n  }\n\n  oneof count { // Count of observations in the histogram.\n    uint64 count_int   = 1;\n    double count_float = 2;\n  }\n  double sum = 3; // Sum of observations in the histogram.\n  // The schema defines the bucket schema. Currently, valid numbers\n  // are -4 <= n <= 8. They are all for base-2 bucket schemas, where 1\n  // is a bucket boundary in each case, and then each power of two is\n  // divided into 2^n logarithmic buckets. Or in other words, each\n  // bucket boundary is the previous boundary times 2^(2^-n). In the\n  // future, more bucket schemas may be added using numbers < -4 or >\n  // 8.\n  sint32 schema             = 4;\n  double zero_threshold     = 5; // Breadth of the zero bucket.\n  oneof zero_count { // Count in zero bucket.\n    uint64 zero_count_int     = 6;\n    double zero_count_float   = 7;\n  }\n  \n  // Negative Buckets.\n  repeated BucketSpan negative_spans =  8;\n  // Use either \"negative_deltas\" or \"negative_counts\", the former for\n  // regular histograms with integer counts, the latter for float\n  // histograms.\n  repeated sint64 negative_deltas    =  9; // Count delta of each bucket compared to previous one (or to zero for 1st bucket).\n  repeated double negative_counts    = 10; // Absolute count of each bucket.\n\n  // Positive Buckets.\n  repeated BucketSpan positive_spans = 11;\n  // Use either \"positive_deltas\" or \"positive_counts\", the former for\n  // regular histograms with integer counts, the latter for float\n  // histograms.\n  repeated sint64 positive_deltas    = 12; // Count delta of each bucket compared to previous one (or to zero for 1st bucket).\n  repeated double positive_counts    = 13; // Absolute count of each bucket.\n\n  ResetHint reset_hint               = 14;\n  // timestamp is in ms format, see model/timestamp/timestamp.go for\n  // conversion from time.Time to Prometheus timestamp.\n  int64 timestamp = 15;\n} \n\n// A BucketSpan defines a number of consecutive buckets with their\n// offset. Logically, it would be more straightforward to include the\n// bucket counts in the Span. However, the protobuf representation is\n// more compact in the way the data is structured here (with all the\n// buckets in a single array separate from the Spans).\nmessage BucketSpan {\n  sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative).\n  uint32 length = 2; // Length of consecutive buckets.\n}\n\n// TimeSeries represents samples and labels for a single time series.\nmessage TimeSeries {\n  // For a timeseries to be valid, and for the samples and exemplars\n  // to be ingested by the remote system properly, the labels field is required.\n  repeated Label labels         = 1;\n  repeated Sample samples       = 2;\n  repeated Exemplar exemplars   = 3;\n  repeated Histogram histograms = 4;\n}\n\nmessage Label {\n  string name  = 1;\n  string value = 2;\n}\n\nmessage Labels {\n  repeated Label labels = 1;\n}\n\n// Matcher specifies a rule, which can match or set of labels or not.\nmessage LabelMatcher {\n  enum Type {\n    EQ  = 0;\n    NEQ = 1;\n    RE  = 2;\n    NRE = 3;\n  }\n  Type type    = 1;\n  string name  = 2;\n  string value = 3;\n}\n\nmessage ReadHints {\n  int64 step_ms = 1;  // Query step size in milliseconds.\n  string func = 2;    // String representation of surrounding function or aggregation.\n  int64 start_ms = 3; // Start time in milliseconds.\n  int64 end_ms = 4;   // End time in milliseconds.\n  repeated string grouping = 5; // List of label names used in aggregation.\n  bool by = 6; // Indicate whether it is without or by.\n  int64 range_ms = 7; // Range vector selector range in milliseconds.\n}\n\n// Chunk represents a TSDB chunk.\n// Time range [min, max] is inclusive.\nmessage Chunk {\n  int64 min_time_ms = 1;\n  int64 max_time_ms = 2;\n\n  // We require this to match chunkenc.Encoding.\n  enum Encoding {\n    UNKNOWN   = 0;\n    XOR       = 1;\n    HISTOGRAM = 2;\n  }\n  Encoding type  = 3;\n  bytes data     = 4;\n}\n\n// ChunkedSeries represents single, encoded time series.\nmessage ChunkedSeries {\n  // Labels should be sorted.\n  repeated Label labels = 1;\n  // Chunks will be in start time order and may overlap.\n  repeated Chunk chunks = 2;\n}\n"
  },
  {
    "path": "runtimes/core/Cargo.toml",
    "content": "[package]\nname = \"encore-runtime-core\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[features]\n# Tracing of the Encore runtime itself.\nrttrace = []\n\n[dependencies]\npingora = { version = \"0.8\", features = [\"lb\", \"openssl\"] }\nanyhow = \"1.0.76\"\nasync-trait = \"0.1\"\nbase64 = \"0.21.5\"\ngjson = \"0.8.1\"\nprost = \"0.12.3\"\nprost-types = \"0.12.3\"\nserde = \"1.0.193\"\nserde_json = { version = \"1.0.108\", features = [\"raw_value\"] }\ntokio = { version = \"1.35.1\", features = [\"sync\"] }\ntokio-stream = \"0.1.17\"\ntokio-nsq = \"0.14.0\"\nxid = \"1.0.3\"\nlog = { version = \"0.4.20\", features = [\"kv_unstable\", \"kv_unstable_serde\"] }\nbytes = { version = \"1.5.0\", features = [] }\npostgres-protocol = \"0.6.8\"\npgvector = { version = \"0.4\", features = [\"postgres\", \"serde\"] }\ntokio-postgres = { version = \"0.7.13\", features = [\n    \"array-impls\",\n    \"with-serde_json-1\",\n    \"with-geo-types-0_7\",\n    \"with-uuid-1\",\n    \"with-chrono-0_4\",\n    \"with-cidr-0_3\",\n] }\ncidr = \"0.3.1\"\ntokio-util = \"0.7.10\"\ntokio-tungstenite = { version = \"0.21.0\", features = [\n    \"rustls-tls-native-roots\",\n] }\nfutures-util = \"0.3.31\"\nrand = \"0.8.5\"\nenv_logger = \"0.10.1\"\ngoogle-cloud-pubsub = \"0.22.1\"\ngoogle-cloud-googleapis = \"0.12.0\"\nhyper = { version = \"1.1.0\", features = [\"server\", \"http1\", \"http2\", \"client\"] }\nhttp-body-util = \"0.1.0\"\nhttp = \"1.0.0\"\nmatchit = \"0.7.3\"\naxum = { version = \"0.7.5\", features = [\"ws\"] }\nchrono = { version = \"0.4.31\", features = [\"serde\"] }\nonce_cell = \"1.19.0\"\ncolored = \"2.1.0\"\nbacktrace = \"0.3.69\"\nserde_with = \"3.4.0\"\nmime = \"0.3.17\"\nfutures = \"0.3.30\"\nnative-tls = \"0.2.11\"\npostgres-native-tls = \"0.5.0\"\nreqwest = { version = \"0.12.4\", features = [\"stream\", \"json\"] }\nurl = \"2.5.0\"\nfutures-core = { version = \"0.3.30\", features = [] }\nserde_urlencoded = \"0.7.1\"\nform_urlencoded = \"1.2.1\"\nhttpdate = \"1.0.3\"\nhmac = \"0.12.1\"\nsha2 = \"0.10.8\"\nsha3 = \"0.10.8\"\nhex = \"0.4.3\"\nsubtle = \"2.5.0\"\nradix_fmt = \"1.0.0\"\nindexmap = { version = \"2.2.1\", features = [\"serde\"] }\ntower-service = \"0.3.2\"\nduct = \"0.13.7\"\nbase32 = \"0.4.0\"\n\n# We need to vendor openssl to allow cross-compilation on our build systems\nopenssl = { version = \"0.10.57\", features = [\"vendored\"] }\nbb8 = \"0.9\"\nbb8-postgres = \"0.9\"\nbb8-redis = \"0.26\"\nredis = { version = \"1.0\", features = [\n    \"tokio-rustls-comp\",\n    \"tls-rustls-insecure\",\n    \"connection-manager\",\n] }\nuuid = \"1.7.0\"\nopenssl-probe = \"0.1.5\"\njsonwebtoken = \"9.2.0\"\ngoogle-cloud-gax = \"0.17.0\"\naws-sdk-sns = \"1.20.0\"\naws-config = \"1.1.10\"\naws-sdk-sqs = \"1.19.0\"\ntokio-retry = \"0.3.0\"\nrsa = { version = \"0.9.6\", features = [\"pem\"] }\nflate2 = \"1.0.30\"\nurlencoding = \"2.1.3\"\ntower-http = { version = \"0.5.2\", features = [\"fs\"] }\ngoogle-cloud-storage = \"0.22.1\"\nserde_path_to_error = \"0.1.16\"\ntracing = \"0.1.40\"\ntracing-subscriber = { version = \"0.3.18\", features = [\n    \"alloc\",\n    \"ansi\",\n    \"env-filter\",\n    \"fmt\",\n    \"matchers\",\n    \"nu-ansi-term\",\n    \"once_cell\",\n    \"regex\",\n    \"registry\",\n    \"sharded-slab\",\n    \"smallvec\",\n    \"std\",\n    \"thread_local\",\n    \"tracing\",\n], default-features = false }\nthiserror = \"1.0.64\"\nasync-stream = \"0.3.6\"\nmd5 = \"0.7.0\"\naws-sdk-s3 = \"1.58.0\"\naws-smithy-types = { version = \"1.2.8\", features = [\n    \"byte-stream-poll-next\",\n    \"rt-tokio\",\n] }\npercent-encoding = \"2.3.1\"\naws-credential-types = \"1.2.1\"\nregex = \"1.11.1\"\nemail_address = \"0.2.9\"\ncookie = \"0.18.1\"\nmalachite = \"0.6.1\"\nbyteorder = \"1.5.0\"\nmetrics = \"0.24.2\"\ndashmap = \"6.1.0\"\ngoogle-cloud-monitoring-v3 = \"1.0.0\"\ngoogle-cloud-api = \"1.0.0\"\ngoogle-cloud-wkt = \"1.0.0\"\nsysinfo = \"0.37.2\"\naws-sdk-cloudwatch = { version = \"1.94.0\", default-features = false, features = [\n    \"behavior-version-latest\",\n    \"rt-tokio\",\n    \"rustls\",\n] }\ndatadog-api-client = \"0.20.0\"\nsnap = \"1.1.1\"\nminiredis-rs = { path = \"../../miniredis\" }\n\n[build-dependencies]\nprost-build = \"0.12.3\"\n\n[dev-dependencies]\nassert_matches = \"1.5.0\"\ninsta = { version = \"1.38.0\", features = [\"yaml\"] }\nquickcheck = \"1.0.3\"\nproptest = \"1.7.0\"\n"
  },
  {
    "path": "runtimes/core/build.rs",
    "content": "use std::path::{Path, PathBuf};\n\nfn main() -> std::io::Result<()> {\n    prost_build::compile_protos(\n        &[\n            \"../../proto/encore/runtime/v1/runtime.proto\",\n            \"../../proto/encore/parser/meta/v1/meta.proto\",\n            \"../../proto/prompb/remote.proto\",\n        ],\n        &[\"../../proto/\"],\n    )?;\n\n    // We add an extra compile time environment variable which allows our error module\n    // to know where the root of the workspace that we are compiling is - thus in stack traces\n    // we can show the relative path to the file that caused the error.\n    println!(\n        \"cargo:rustc-env=ENCORE_BINARY_SRC_PATH={}\",\n        workspace_dir().to_string_lossy()\n    );\n\n    println!(\"cargo:rustc-env=ENCORE_BINARY_GIT_HASH={}\", get_git_hash());\n\n    Ok(())\n}\n\nfn workspace_dir() -> PathBuf {\n    let output = std::process::Command::new(env!(\"CARGO\"))\n        .arg(\"locate-project\")\n        .arg(\"--workspace\")\n        .arg(\"--message-format=plain\")\n        .output()\n        .unwrap()\n        .stdout;\n\n    let cargo_toml_file = Path::new(std::str::from_utf8(&output).unwrap().trim());\n    cargo_toml_file.parent().unwrap().to_path_buf()\n}\n\nuse std::env;\n\nfn get_git_hash() -> String {\n    use std::process::Command;\n\n    let commit = Command::new(\"git\")\n        .arg(\"rev-parse\")\n        .arg(\"--verify\")\n        .arg(\"HEAD\")\n        .output();\n    if let Ok(commit_output) = commit {\n        let commit_string = String::from_utf8_lossy(&commit_output.stdout);\n\n        commit_string\n            .lines()\n            .next()\n            .unwrap_or(\"unknown\")\n            .to_string()\n    } else {\n        \"unknown\".to_string()\n    }\n}\n"
  },
  {
    "path": "runtimes/core/resources/test/infra.config.json",
    "content": "{\n  \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n  \"metadata\": {\n    \"app_id\": \"my-app\",\n    \"env_name\": \"my-env\",\n    \"env_type\": \"production\",\n    \"cloud\": \"gcp\",\n    \"base_url\": \"https://my-app.com\"\n  },\n  \"sql_servers\": [\n    {\n      \"host\": \"my-db-host:5432\",\n      \"tls_config\": {\n        \"ca\": \"test\",\n        \"client_cert\": {\n          \"cert\": \"test\",\n          \"key\": \"test\"\n        },\n        \"disable_tls_hostname_verification\": false,\n        \"disabled\": false\n      },\n      \"databases\": {\n        \"mydb\": {\n          \"client_cert\": {\n            \"cert\": \"test\",\n            \"key\": \"test\"\n          },\n          \"max_connections\": 10,\n          \"min_connections\": 10,\n          \"username\": \"my-db-owner\",\n          \"password\": {\"$env\": \"DB_PASSWORD\"}\n\n        }\n      }\n    }\n  ],\n  \"service_discovery\": {\n    \"myservice\": {\n      \"base_url\": \"https://my-service:8044\"\n    },\n    \"myservice2\": {\n      \"base_url\": \"https://my-service2:8044\",\n      \"auth\": [{\n        \"type\": \"key\",\n        \"key\": {\n          \"$env\": \"SVC_TO_SVC_KEY\"\n        },\n        \"id\": 1\n      }]\n    }\n  },\n  \"redis\": {\n    \"encoreredis\": {\n      \"database_index\": 5,\n      \"max_connections\": 10,\n      \"min_connections\": 10,\n      \"key_prefix\": \"my-app:my-env:\",\n      \"tls_config\": {\n        \"disable_tls_hostname_verification\": false,\n        \"disabled\": false,\n        \"ca\": \"test\",\n        \"client_cert\": {\n          \"cert\": \"test\",\n          \"key\": \"test\"\n        }\n      },\n      \"auth\": {\n        \"type\": \"acl\",\n        \"username\": \"encoreredis\",\n        \"password\": {\"$env\": \"REDIS_PASSWORD\"}\n      },\n      \"host\": \"my-redis-host\"\n    }\n  },\n  \"metrics\": {\n    \"type\": \"prometheus\",\n    \"remote_write_url\": \"https://my-remote-write-url\"\n  },\n  \"graceful_shutdown\": {\n    \"total\": 30,\n    \"handlers\": 20,\n    \"shutdown_hooks\": 10\n  },\n  \"auth\": [\n    {\n      \"type\": \"key\",\n      \"id\": 1,\n      \"key\": {\"$env\": \"SVC_TO_SVC_KEY\"}\n    }\n  ],\n  \"secrets\": {\n    \"AppSecret\": {\"$env\": \"APP_SECRET\"}\n  },\n  \"pubsub\": [\n    {\n      \"type\": \"gcp_pubsub\",\n      \"project_id\": \"my-project\",\n      \"topics\": {\n        \"encore-topic\": {\n          \"name\": \"gcp-topic-name\",\n          \"subscriptions\": {\n            \"encore-subscription\": {\n              \"name\": \"gcp-subscription-name\",\n              \"project_id\": \"test\",\n              \"push_config\": {\n                \"id\": \"test\",\n                \"jwt_audience\": \"test\",\n                \"service_account\": \"test\"\n              }\n            }\n          }\n        }\n      }\n    }\n  ],\n  \"cors\": {\n    \"debug\": true,\n    \"allow_headers\": [\"Authorization\", \"Content-Type\"],\n    \"expose_headers\": [\"*\"],\n    \"allow_origins_with_credentials\": [\"https://test.com\"],\n    \"allow_origins_without_credentials\": [\"https://test.com\"]\n  },\n  \"hosted_gateways\": [\"api-gateway\"],\n  \"hosted_services\": [\"my-service\", \"my-service2\"]\n}"
  },
  {
    "path": "runtimes/core/src/api/auth/local.rs",
    "content": "use crate::metrics::counter;\n\nuse crate::api::auth::{AuthHandler, AuthPayload, AuthRequest, AuthResponse};\nuse crate::api::schema::encoding::Schema;\nuse crate::api::{APIResult, HandlerResponse, HandlerResponseInner, PValues};\nuse crate::log::LogFromRust;\nuse crate::model::{AuthRequestData, RequestData};\nuse crate::trace::Tracer;\nuse crate::{api, model, EndpointName};\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::{Arc, RwLock};\n\npub struct LocalAuthHandler {\n    pub name: EndpointName,\n    pub schema: Schema,\n    pub handler: RwLock<Option<Arc<dyn api::TypedHandler>>>,\n    pub tracer: Tracer,\n    pub requests_total: counter::Schema<u64>,\n}\n\nimpl LocalAuthHandler {\n    pub fn set_handler(&self, handler: Option<Arc<dyn api::TypedHandler>>) {\n        let mut guard = self.handler.write().unwrap();\n        *guard = handler;\n    }\n}\n\nimpl AuthHandler for LocalAuthHandler {\n    fn name(&self) -> &EndpointName {\n        &self.name\n    }\n\n    fn handle_auth(\n        self: Arc<Self>,\n        req: AuthRequest,\n    ) -> Pin<Box<dyn Future<Output = APIResult<AuthResponse>> + Send + 'static>> {\n        let this = self.clone();\n        Box::pin(async move {\n            let handler = {\n                let guard = this.handler.read().unwrap();\n                // If we don't have a handler set, return an error.\n                let Some(handler) = guard.as_ref() else {\n                    return Err(api::Error::internal(anyhow::anyhow!(\n                        \"auth handler implementation not registered for {}\",\n                        this.name\n                    )));\n                };\n                handler.clone()\n            };\n\n            let query = match &self.schema.query {\n                None => None,\n                Some(qry) => qry.parse(req.query.as_deref())?,\n            };\n            let header = match &self.schema.header {\n                None => None,\n                Some(hdr) => hdr.parse(&req.headers)?,\n            };\n            let cookie = match &self.schema.cookie {\n                None => None,\n                Some(c) => c.parse_req(&req.headers)?,\n            };\n\n            let meta = req.call_meta;\n            let span_id = meta.this_span_id.unwrap_or_else(model::SpanId::generate);\n            let span = model::SpanKey(meta.trace_id, span_id);\n            let parent_span = meta.parent_span_id.map(|sp| meta.trace_id.with_span(sp));\n\n            let traced = meta\n                .trace_sampled\n                .unwrap_or_else(|| self.tracer.should_sample(&self.name));\n\n            let req = Arc::new(model::Request {\n                span,\n                parent_trace: None,\n                parent_span,\n                caller_event_id: meta.parent_event_id,\n                ext_correlation_id: meta.ext_correlation_id,\n                is_platform_request: false, // TODO\n                internal_caller: None,      // TODO\n                start: tokio::time::Instant::now(),\n                start_time: std::time::SystemTime::now(),\n                data: RequestData::Auth(AuthRequestData {\n                    auth_handler: this.name().clone(),\n                    parsed_payload: AuthPayload {\n                        query,\n                        header,\n                        cookie,\n                    },\n                }),\n                traced,\n            });\n\n            let logger = crate::log::root();\n            logger.info(Some(&req), \"running auth handler\", None);\n\n            self.tracer.request_span_start(&req, false);\n            let auth_response: HandlerResponse = handler.call(req.clone()).await;\n            let duration = tokio::time::Instant::now().duration_since(req.start);\n\n            if let Err(e) = &auth_response {\n                logger.error(Some(&req), \"auth handler failed\", Some(e), None);\n            }\n            logger.info(Some(&req), \"auth handler completed\", {\n                let mut fields = crate::log::Fields::new();\n                let dur_ms = (duration.as_secs() as f64 * 1000f64)\n                    + (duration.subsec_nanos() as f64 / 1_000_000f64);\n                fields.insert(\n                    \"duration\".into(),\n                    serde_json::Value::Number(serde_json::Number::from_f64(dur_ms).unwrap_or_else(\n                        || {\n                            // Fall back to integer if the f64 conversion fails\n                            serde_json::Number::from(duration.as_millis() as u64)\n                        },\n                    )),\n                );\n                Some(fields)\n            });\n\n            let result: APIResult<(PValues, String)> = match auth_response {\n                Ok(HandlerResponseInner {\n                    payload: Some(payload),\n                    ..\n                }) => {\n                    let auth_uid = payload\n                        .get(\"userID\")\n                        .and_then(|v| v.as_str())\n                        .map(String::from);\n                    match auth_uid {\n                        Some(uid) => Ok((payload, uid.to_string())),\n                        None => Err(api::Error {\n                            code: api::ErrCode::Unauthenticated,\n                            message: \"unauthenticated\".to_string(),\n                            internal_message: Some(\n                                \"auth handler did not return a userID field\".to_string(),\n                            ),\n                            stack: None,\n                            details: None,\n                        }),\n                    }\n                }\n                Ok(HandlerResponseInner { payload: None, .. }) => Err(api::Error {\n                    code: api::ErrCode::Unauthenticated,\n                    message: \"unauthenticated\".to_string(),\n                    internal_message: Some(\"auth handler returned null\".to_string()),\n                    stack: None,\n                    details: None,\n                }),\n                Err(e) => Err(e),\n            };\n\n            match result {\n                Ok((auth_data, auth_uid)) => {\n                    let model_resp = model::Response {\n                        request: req.clone(),\n                        duration,\n                        data: model::ResponseData::Auth(Ok(model::AuthSuccessResponse {\n                            user_data: auth_data.clone(),\n                            user_id: auth_uid.clone(),\n                        })),\n                    };\n\n                    self.tracer.request_span_end(&model_resp, false);\n                    self.requests_total.with([(\"code\", \"ok\")]).increment();\n                    Ok(AuthResponse::Authenticated {\n                        auth_uid,\n                        auth_data,\n                    })\n                }\n                Err(e) => {\n                    let model_resp = model::Response {\n                        request: req.clone(),\n                        duration,\n                        data: model::ResponseData::Auth(Err(e.clone())),\n                    };\n                    self.tracer.request_span_end(&model_resp, false);\n                    self.requests_total\n                        .with([(\"code\", e.code.to_string())])\n                        .increment();\n                    Err(e)\n                }\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/auth/mod.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse http::header::COOKIE;\nuse serde::Serialize;\n\nuse crate::api::reqauth::CallMeta;\nuse crate::api::APIResult;\nuse crate::{api, EndpointName};\n\nuse crate::api::schema::encoding::Schema;\npub use local::LocalAuthHandler;\npub use remote::RemoteAuthHandler;\n\nuse super::jsonschema::JSONSchema;\nuse super::PValues;\n\nmod local;\nmod remote;\n\npub type AxumRequest = axum::http::Request<axum::body::Body>;\n\n#[derive(Debug)]\npub struct AuthRequest {\n    pub headers: axum::http::HeaderMap,\n    pub query: Option<String>,\n    pub call_meta: CallMeta,\n}\n\n#[derive(Debug)]\npub enum AuthResponse {\n    Authenticated {\n        auth_uid: String,\n        auth_data: PValues,\n    },\n    Unauthenticated {\n        error: api::Error,\n    },\n}\n\n/// A trait for handlers that accept auth parameters and return an auth result.\npub trait AuthHandler: Sync + Send + 'static {\n    fn name(&self) -> &EndpointName;\n\n    fn handle_auth(\n        self: Arc<Self>,\n        req: AuthRequest,\n    ) -> Pin<Box<dyn Future<Output = APIResult<AuthResponse>> + Send + 'static>>;\n}\n\npub struct Authenticator {\n    schema: Schema,\n    auth_data: JSONSchema,\n    auth_handler: AuthHandlerType,\n}\n\n#[derive(Clone)]\npub enum AuthHandlerType {\n    Local(Arc<LocalAuthHandler>),\n    Remote(Arc<RemoteAuthHandler>),\n}\n\nimpl AuthHandlerType {\n    fn set_local_handler(&self, handler: Option<Arc<dyn api::TypedHandler>>) {\n        if let Self::Local(local) = self {\n            local.set_handler(handler);\n        }\n    }\n}\n\nimpl Authenticator {\n    pub fn new(\n        schema: Schema,\n        auth_data: JSONSchema,\n        auth_handler: AuthHandlerType,\n    ) -> anyhow::Result<Self> {\n        Ok(Self {\n            schema,\n            auth_data,\n            auth_handler,\n        })\n    }\n\n    pub fn local(\n        schema: Schema,\n        auth_data: JSONSchema,\n        local: LocalAuthHandler,\n    ) -> anyhow::Result<Self> {\n        Self::new(schema, auth_data, AuthHandlerType::Local(Arc::new(local)))\n    }\n\n    pub fn remote(\n        schema: Schema,\n        auth_data: JSONSchema,\n        remote: RemoteAuthHandler,\n    ) -> anyhow::Result<Self> {\n        Self::new(schema, auth_data, AuthHandlerType::Remote(Arc::new(remote)))\n    }\n\n    pub fn schema(&self) -> &Schema {\n        &self.schema\n    }\n\n    pub fn auth_data(&self) -> &JSONSchema {\n        &self.auth_data\n    }\n\n    pub async fn authenticate<R: InboundRequest>(\n        &self,\n        req: &R,\n        meta: CallMeta,\n    ) -> APIResult<AuthResponse> {\n        if !self.contains_auth_params(req) {\n            return Ok(AuthResponse::Unauthenticated {\n                error: api::Error::unauthenticated(),\n            });\n        }\n\n        let auth_req = self.build_auth_request(req, meta);\n        let resp = match &self.auth_handler {\n            AuthHandlerType::Local(local) => local.clone().handle_auth(auth_req).await,\n            AuthHandlerType::Remote(remote) => remote.clone().handle_auth(auth_req).await,\n        };\n        match resp {\n            Ok(resp) => Ok(resp),\n            Err(error) if error.code == api::ErrCode::Unauthenticated => {\n                Ok(AuthResponse::Unauthenticated { error })\n            }\n            Err(err) => Err(err),\n        }\n    }\n\n    pub fn set_local_handler_impl(&self, handler: Option<Arc<dyn api::TypedHandler>>) {\n        self.auth_handler.set_local_handler(handler);\n    }\n\n    fn build_auth_request<R: InboundRequest>(\n        &self,\n        inbound: &R,\n        mut call_meta: CallMeta,\n    ) -> AuthRequest {\n        // Ignore the parent span id as gateways don't currently record a span.\n        call_meta.parent_span_id = None;\n\n        // Headers.\n        let mut headers = match &self.schema.header {\n            None => axum::http::header::HeaderMap::new(),\n            Some(schema) => {\n                let mut dest = axum::http::header::HeaderMap::with_capacity(schema.len());\n                let inbound_headers = inbound.headers();\n                for (json_key, field) in schema.fields() {\n                    let header_name = field.name_override.as_deref().unwrap_or(json_key.as_ref());\n                    let Ok(header_name) =\n                        axum::http::HeaderName::from_bytes(header_name.as_bytes())\n                    else {\n                        continue;\n                    };\n                    for value in inbound_headers.get_all(&header_name) {\n                        dest.append(header_name.clone(), value.to_owned());\n                    }\n                }\n                dest\n            }\n        };\n\n        // Cookies.\n        if let Some(schema) = &self.schema.cookie {\n            let mut inbound_cookies = cookie::CookieJar::new();\n            inbound\n                .headers()\n                .get_all(COOKIE)\n                .iter()\n                .filter_map(|raw| raw.to_str().ok())\n                .flat_map(cookie::Cookie::split_parse)\n                .flatten()\n                .for_each(|c| inbound_cookies.add_original(c.into_owned()));\n\n            for (key, field) in schema.fields() {\n                let cookie_name = field.name_override.as_deref().unwrap_or(key.as_ref());\n                let Some(c) = inbound_cookies.get(cookie_name) else {\n                    continue;\n                };\n                let Ok(val) = axum::http::HeaderValue::from_bytes(c.to_string().as_bytes()) else {\n                    continue;\n                };\n\n                headers.append(http::header::COOKIE, val);\n            }\n        };\n\n        // Move query params.\n        let query = match &self.schema.query {\n            None => None,\n            Some(schema) => {\n                let query_data = inbound.query().unwrap_or_default().as_bytes();\n                let parsed = form_urlencoded::parse(query_data);\n\n                let mut dest = form_urlencoded::Serializer::new(String::new());\n                for (key, value) in parsed {\n                    if schema.contains_name(key.as_ref()) {\n                        dest.append_pair(key.as_ref(), value.as_ref());\n                    }\n                }\n\n                Some(dest.finish())\n            }\n        };\n\n        AuthRequest {\n            headers,\n            query,\n            call_meta,\n        }\n    }\n\n    fn contains_auth_params<R: InboundRequest>(&self, req: &R) -> bool {\n        if let Some(query) = &self.schema.query {\n            if query.contains_any(req.query().unwrap_or_default().as_bytes()) {\n                return true;\n            }\n        }\n\n        if let Some(header) = &self.schema.header {\n            if header.contains_any(&req.headers()) {\n                return true;\n            }\n        }\n\n        if let Some(cookie) = &self.schema.cookie {\n            if cookie.contains_any(&req.headers()) {\n                return true;\n            }\n        }\n\n        false\n    }\n}\n\n#[derive(Debug, Serialize, Clone)]\npub struct AuthPayload {\n    #[serde(flatten)]\n    pub query: Option<PValues>,\n\n    #[serde(flatten)]\n    pub header: Option<PValues>,\n\n    #[serde(flatten)]\n    pub cookie: Option<PValues>,\n}\n\npub trait InboundRequest {\n    fn headers(&self) -> &axum::http::HeaderMap;\n    fn query(&self) -> Option<&str>;\n}\n\nimpl InboundRequest for axum::http::Request<axum::body::Body> {\n    fn headers(&self) -> &axum::http::HeaderMap {\n        self.headers()\n    }\n\n    fn query(&self) -> Option<&str> {\n        self.uri().query()\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/auth/remote.rs",
    "content": "use crate::api::auth::{AuthHandler, AuthRequest, AuthResponse};\nuse crate::api::call::{CallDesc, ServiceRegistry};\nuse crate::api::httputil::{convert_headers, join_url_path, merge_query};\nuse crate::api::jsonschema::{DecodeConfig, JSONSchema};\nuse crate::api::reqauth::caller::Caller;\nuse crate::api::reqauth::meta::{MetaKey, MetaMap};\nuse crate::api::reqauth::svcauth;\nuse crate::api::{APIResult, PValues};\nuse crate::{api, trace, EndpointName};\nuse anyhow::Context;\nuse std::borrow::Cow;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\npub struct RemoteAuthHandler {\n    name: EndpointName,\n    svc_auth_method: Arc<dyn svcauth::ServiceAuthMethod>,\n    auth_handler_url: reqwest::Url,\n    http_client: reqwest::Client,\n    auth_data_schema: JSONSchema,\n    tracer: trace::Tracer,\n}\n\nimpl RemoteAuthHandler {\n    pub fn new(\n        name: EndpointName,\n        reg: &ServiceRegistry,\n        http_client: reqwest::Client,\n        auth_data_schema: JSONSchema,\n        tracer: trace::Tracer,\n    ) -> anyhow::Result<Self> {\n        let svc_auth_method = reg\n            .service_auth_method(name.service())\n            .context(\"no service auth method found for auth handler\")?;\n\n        let auth_handler_url = {\n            let mut base_url: reqwest::Url = reg\n                .service_base_url(name.service())\n                .context(\"no base url found for auth handler\")?\n                .parse()\n                .context(\"invalid service base url\")?;\n\n            let auth_path = format!(\"/__encore/authhandler/{}\", name.endpoint());\n            let combined_path =\n                join_url_path(base_url.path(), &auth_path).context(\"invalid auth handler path\")?;\n            base_url.set_path(&combined_path);\n            base_url\n        };\n\n        Ok(Self {\n            name,\n            svc_auth_method,\n            auth_handler_url,\n            http_client,\n            auth_data_schema,\n            tracer,\n        })\n    }\n\n    fn build_req(&self, auth_req: &AuthRequest) -> APIResult<reqwest::Request> {\n        let dest = self.auth_handler_url.clone();\n        let mut req = self\n            .http_client\n            .post(dest)\n            .headers(convert_headers(&auth_req.headers))\n            .build()\n            .map_err(api::Error::internal)?;\n\n        if let Some(query) = merge_query(req.url().query(), auth_req.query.as_deref()) {\n            let query = query.as_ref();\n            req.url_mut().set_query(Some(query));\n        }\n\n        Ok(req)\n    }\n\n    async fn handle_auth(self: Arc<Self>, req: AuthRequest) -> APIResult<AuthResponse> {\n        // TODO this is copied from the Go version but should be better designed.\n        // We should have a way of identifying the gateway as the caller.\n        // There is Caller::Gateway but it means something else.\n        let caller = Caller::APIEndpoint(EndpointName::new(\"gateway\", \"__encore/authhandler\"));\n\n        let meta = &req.call_meta;\n        let desc: CallDesc<()> = CallDesc {\n            caller: &caller,\n            parent_span: meta.parent_span_id.map(|sp| meta.trace_id.with_span(sp)),\n            parent_event_id: None,\n            ext_correlation_id: meta\n                .ext_correlation_id\n                .as_ref()\n                .map(|s| Cow::Borrowed(s.as_str())),\n            traced: meta\n                .trace_sampled\n                .unwrap_or_else(|| self.tracer.should_sample(&self.name)),\n            auth_user_id: None,\n            auth_data: None,\n            svc_auth_method: self.svc_auth_method.as_ref(),\n        };\n\n        let mut req = self.build_req(&req)?;\n        desc.add_meta(req.headers_mut())\n            .map_err(api::Error::internal)?;\n\n        let resp = self\n            .http_client\n            .execute(req)\n            .await\n            .map_err(api::Error::internal)?;\n\n        // Resolve the user id, if present, since parse_api_response consumes resp.\n        let user_id = resp\n            .headers()\n            .get_meta(MetaKey::UserId)\n            .map(|s| s.to_string());\n\n        match parse_auth_response(resp, &self.auth_data_schema).await {\n            Ok(data) => {\n                if let Some(user_id) = user_id {\n                    Ok(AuthResponse::Authenticated {\n                        auth_uid: user_id,\n                        auth_data: data,\n                    })\n                } else {\n                    Ok(AuthResponse::Unauthenticated {\n                        error: api::Error::unauthenticated(),\n                    })\n                }\n            }\n\n            // Map the unauthenticated error code to the unauthenticated result.\n            Err(error) if error.code == api::ErrCode::Unauthenticated => {\n                Ok(AuthResponse::Unauthenticated { error })\n            }\n\n            Err(err) => Err(err),\n        }\n    }\n}\n\nimpl AuthHandler for RemoteAuthHandler {\n    fn name(&self) -> &EndpointName {\n        &self.name\n    }\n\n    fn handle_auth(\n        self: Arc<Self>,\n        req: AuthRequest,\n    ) -> Pin<Box<dyn Future<Output = APIResult<AuthResponse>> + Send + 'static>> {\n        Box::pin(self.handle_auth(req))\n    }\n}\n\nasync fn parse_auth_response(resp: reqwest::Response, schema: &JSONSchema) -> APIResult<PValues> {\n    let status = resp.status();\n    if status.is_success() {\n        // Do we have a JSON response?\n        match resp.headers().get(reqwest::header::CONTENT_TYPE) {\n            Some(content_type) if content_type == \"application/json\" => {\n                let bytes = resp.bytes().await.map_err(api::Error::internal)?;\n                let mut jsonde = serde_json::Deserializer::from_slice(&bytes);\n                let cfg = DecodeConfig {\n                    coerce_strings: false,\n                    arrays_as_repeated_fields: false,\n                };\n                let value = schema.deserialize(&mut jsonde, cfg).map_err(|e| {\n                    api::Error::invalid_argument(\"unable to decode response body\", e)\n                })?;\n                Ok(value)\n            }\n            _ => Err(api::Error::internal(anyhow::anyhow!(\n                \"missing auth data from auth handler\"\n            ))),\n        }\n    } else {\n        match resp.headers().get(reqwest::header::CONTENT_TYPE) {\n            Some(content_type) if content_type == \"application/json\" => {\n                match resp.json::<api::Error>().await {\n                    Ok(data) => Err(data),\n                    Err(e) => Err(api::Error::internal(e)),\n                }\n            }\n            _ => {\n                // We have some non-JSON error response.\n                let body = resp.text().await.unwrap_or_else(|_| \"\".into());\n                Err(api::Error {\n                    code: api::ErrCode::Internal,\n                    message: body,\n                    internal_message: None,\n                    stack: None,\n                    details: None,\n                })\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/call.rs",
    "content": "use std::borrow::{Borrow, Cow};\nuse std::collections::HashMap;\nuse std::future::Future;\nuse std::sync::Arc;\nuse std::time::SystemTime;\n\nuse anyhow::Context;\nuse tokio_tungstenite::tungstenite::client::IntoClientRequest;\nuse url::Url;\n\nuse encore::runtime::v1 as pb;\n\nuse crate::api::reqauth::caller::Caller;\nuse crate::api::reqauth::meta::MetaKey;\nuse crate::api::reqauth::{service_auth_method, svcauth};\nuse crate::api::schema::{JSONPayload, ToOutgoingRequest};\nuse crate::api::{schema, APIResult, Endpoint, EndpointMap};\nuse crate::model::{SpanKey, TraceEventId};\nuse crate::names::EndpointName;\nuse crate::trace::Tracer;\nuse crate::{api, encore, model, secrets, EncoreName, Hosted};\n\nuse super::reqauth::meta::MetaMapMut;\nuse super::websocket_client::WebSocketClient;\nuse super::HandshakeSchema;\nuse super::ResponsePayload;\n\n/// Tracks where services are located and how to call them.\npub struct ServiceRegistry {\n    endpoints: Arc<EndpointMap>,\n    base_urls: HashMap<EncoreName, String>,\n    http_client: reqwest::Client,\n    tracer: Tracer,\n    service_auth: HashMap<EncoreName, Arc<dyn svcauth::ServiceAuthMethod>>,\n    deploy_id: String,\n}\n\nimpl ServiceRegistry {\n    #[allow(clippy::too_many_arguments)]\n    pub fn new(\n        secrets: &secrets::Manager,\n        endpoints: Arc<EndpointMap>,\n        env: &pb::Environment,\n        sd: pb::ServiceDiscovery,\n        own_address: Option<&str>,\n        own_auth_methods: &[Arc<dyn svcauth::ServiceAuthMethod>],\n        hosted_services: &Hosted,\n        deploy_id: String,\n        http_client: reqwest::Client,\n        tracer: Tracer,\n    ) -> anyhow::Result<Self> {\n        let mut base_urls = HashMap::with_capacity(sd.services.len());\n        let mut service_auth = HashMap::with_capacity(sd.services.len());\n        for (svc, mut loc) in sd.services {\n            let svc = EncoreName::from(svc);\n            base_urls.insert(svc.clone(), loc.base_url);\n\n            let auth_method = if loc.auth_methods.is_empty() {\n                Arc::new(svcauth::Noop)\n            } else {\n                service_auth_method(secrets, env, loc.auth_methods.swap_remove(0))\n                    .context(\"compute service auth methods\")?\n            };\n            service_auth.insert(svc, auth_method);\n        }\n\n        if let Some(own_address) = own_address {\n            let own_address = format!(\"http://{own_address}\");\n            for svc_name in hosted_services.iter() {\n                if !base_urls.contains_key(svc_name) {\n                    let svc = EncoreName::from(svc_name);\n                    base_urls.insert(svc.clone(), own_address.clone());\n\n                    let auth_method = if own_auth_methods.is_empty() {\n                        Arc::new(svcauth::Noop)\n                    } else {\n                        own_auth_methods[0].clone()\n                    };\n                    service_auth.insert(svc, auth_method);\n                }\n            }\n        } else if !hosted_services.is_empty() {\n            // This shouldn't happen if things are configured correctly.\n            ::log::error!(\n                \"internal encore error: cannot host services without provided own address\"\n            );\n        }\n\n        Ok(Self {\n            endpoints,\n            base_urls,\n            http_client,\n            tracer,\n            service_auth,\n            deploy_id,\n        })\n    }\n\n    pub fn endpoints(&self) -> &EndpointMap {\n        self.endpoints.as_ref()\n    }\n\n    pub fn service_base_url<Q>(&self, service_name: &Q) -> Option<&String>\n    where\n        EncoreName: Borrow<Q>,\n        Q: Eq + std::hash::Hash + ?Sized,\n    {\n        self.base_urls.get(service_name)\n    }\n\n    pub fn service_auth_method<Q>(\n        &self,\n        service_name: &Q,\n    ) -> Option<Arc<dyn svcauth::ServiceAuthMethod>>\n    where\n        EncoreName: Borrow<Q>,\n        Q: Eq + std::hash::Hash + ?Sized,\n    {\n        self.service_auth.get(service_name).cloned()\n    }\n\n    pub fn api_call(\n        &self,\n        target: EndpointName,\n        data: JSONPayload,\n        source: Option<Arc<model::Request>>,\n        opts: Option<api::CallOpts>,\n    ) -> impl Future<Output = APIResult<ResponsePayload>> + 'static {\n        let tracer = self.tracer.clone();\n        let call = model::APICall { source, target };\n        let start_event_id = tracer.rpc_call_start(&call);\n\n        let fut = self.do_api_call(\n            &call.target,\n            data,\n            call.source.as_deref(),\n            start_event_id,\n            opts.as_ref(),\n        );\n        async move {\n            let result = fut.await;\n            tracer.rpc_call_end(crate::trace::protocol::RPCCallEndData {\n                start_id: start_event_id,\n                call: &call,\n                err: result.as_ref().err(),\n            });\n            result\n        }\n    }\n\n    pub fn connect_stream(\n        &self,\n        target: EndpointName,\n        data: JSONPayload,\n        source: Option<Arc<model::Request>>,\n        opts: Option<api::CallOpts>,\n    ) -> impl Future<Output = APIResult<WebSocketClient>> + 'static {\n        let tracer = self.tracer.clone();\n        let call = model::APICall { source, target };\n        let start_event_id = tracer.rpc_call_start(&call);\n\n        let fut = self.do_connect_stream(\n            &call.target,\n            data,\n            call.source.as_deref(),\n            start_event_id,\n            opts.as_ref(),\n        );\n\n        async move {\n            let result = fut.await;\n            tracer.rpc_call_end(crate::trace::protocol::RPCCallEndData {\n                call: &call,\n                start_id: start_event_id,\n                err: result.as_ref().err(),\n            });\n            result\n        }\n    }\n\n    fn do_api_call(\n        &self,\n        target: &EndpointName,\n        data: JSONPayload,\n        source: Option<&model::Request>,\n        start_event_id: Option<TraceEventId>,\n        opts: Option<&api::CallOpts>,\n    ) -> impl Future<Output = APIResult<ResponsePayload>> + 'static {\n        let http_client = self.http_client.clone();\n        let req = self.prepare_api_call_request(target, data, source, start_event_id, opts);\n        async move {\n            match req {\n                Ok((req, resp_schema)) => {\n                    let fut = http_client.execute(req);\n                    match fut.await {\n                        Ok(resp) => {\n                            if !resp.status().is_success() {\n                                return Err(extract_error(resp).await);\n                            }\n                            resp_schema.extract(resp).await\n                        }\n                        Err(e) => Err(api::Error::internal(e)),\n                    }\n                }\n                Err(e) => Err(e),\n            }\n        }\n    }\n\n    fn prepare_api_call_request(\n        &self,\n        target: &EndpointName,\n        mut data: JSONPayload,\n        source: Option<&model::Request>,\n        start_event_id: Option<TraceEventId>,\n        opts: Option<&api::CallOpts>,\n    ) -> APIResult<(reqwest::Request, Arc<schema::Response>)> {\n        let base_url = self\n            .base_urls\n            .get(target.service())\n            .ok_or_else(|| api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"service not found\".into(),\n                internal_message: Some(format!(\n                    \"no service discovery configuration found for service {}\",\n                    target.service()\n                )),\n                stack: None,\n                details: None,\n            })?;\n\n        let Some(endpoint) = self.endpoints.get(target).cloned() else {\n            return Err(api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"endpoint not found\".into(),\n                internal_message: Some(format!(\n                    \"endpoint {target} not found in application metadata\"\n                )),\n                stack: None,\n                details: None,\n            });\n        };\n\n        let req_schema = &endpoint.request[0];\n        let method = req_schema.methods[0];\n        let req_path = req_schema.path.to_request_path(&mut data)?;\n        let req_url = format!(\"{base_url}{req_path}\");\n        let req_url = Url::parse(&req_url).map_err(|_| api::Error {\n            code: api::ErrCode::Internal,\n            message: \"failed to build endpoint url\".into(),\n            internal_message: Some(format!(\n                \"failed to build endpoint url for endpoint {target}\"\n            )),\n            stack: None,\n            details: None,\n        })?;\n\n        let mut req = self\n            .http_client\n            .request(method.into(), req_url)\n            .build()\n            .map_err(api::Error::internal)?;\n\n        if let Some(qry) = &req_schema.query {\n            qry.to_outgoing_request(&mut data, &mut req)?;\n        }\n        if let Some(hdr) = &req_schema.header {\n            hdr.to_outgoing_request(&mut data, &mut req)?;\n        }\n        if let Some(c) = &req_schema.cookie {\n            c.to_outgoing_request(&mut data, &mut req)?;\n        }\n\n        match &req_schema.body {\n            schema::RequestBody::Typed(Some(body)) => {\n                body.to_outgoing_request(&mut data, &mut req)?\n            }\n            schema::RequestBody::Typed(None) => {}\n\n            schema::RequestBody::Raw => {\n                return Err(api::Error {\n                    code: api::ErrCode::Internal,\n                    message: \"internal error\".into(),\n                    internal_message: Some(\"cannot make api calls to raw endpoints\".to_string()),\n                    stack: None,\n                    details: None,\n                });\n            }\n        }\n\n        // Add call metadata.\n        let headers = req.headers_mut();\n        self.propagate_call_meta(headers, &endpoint, source, start_event_id, opts)\n            .map_err(api::Error::internal)?;\n\n        let resp_schema = endpoint.response.clone();\n\n        Ok((req, resp_schema))\n    }\n\n    fn do_connect_stream(\n        &self,\n        target: &EndpointName,\n        data: JSONPayload,\n        source: Option<&model::Request>,\n        start_event_id: Option<TraceEventId>,\n        opts: Option<&api::CallOpts>,\n    ) -> impl Future<Output = APIResult<WebSocketClient>> + 'static {\n        let req = self.prepare_stream_request(target, data, source, start_event_id, opts);\n        async move {\n            match req {\n                Ok((req, outgoing, incoming)) => {\n                    let schema = schema::Stream::new(incoming, outgoing);\n                    WebSocketClient::connect(req, schema).await\n                }\n                Err(e) => Err(e),\n            }\n        }\n    }\n\n    fn prepare_stream_request(\n        &self,\n        target: &EndpointName,\n        mut data: JSONPayload,\n        source: Option<&model::Request>,\n        start_event_id: Option<TraceEventId>,\n        opts: Option<&api::CallOpts>,\n    ) -> APIResult<(\n        http::Request<()>,\n        Arc<schema::Request>,\n        Arc<schema::Response>,\n    )> {\n        let base_url = self\n            .base_urls\n            .get(target.service())\n            .ok_or_else(|| api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"service not found\".into(),\n                internal_message: Some(format!(\n                    \"no service discovery configuration found for service {}\",\n                    target.service()\n                )),\n                stack: None,\n                details: None,\n            })?;\n\n        let Some(endpoint) = self.endpoints.get(target) else {\n            return Err(api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"endpoint not found\".into(),\n                internal_message: Some(format!(\n                    \"endpoint {target} not found in application metadata\"\n                )),\n                stack: None,\n                details: None,\n            });\n        };\n\n        let Some(handshake) = &endpoint.handshake else {\n            return Err(api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"no handshake schema found\".into(),\n                internal_message: Some(format!(\n                    \"endpoint {target} doesn't have a handshake schema specified\"\n                )),\n                stack: None,\n                details: None,\n            });\n        };\n\n        let req_path = handshake.path().to_request_path(&mut data)?;\n\n        let base_url = base_url\n            .replace(\"http://\", \"ws://\")\n            .replace(\"https://\", \"wss://\");\n\n        let req_url = Url::parse(&format!(\"{base_url}{req_path}\")).map_err(|_| api::Error {\n            code: api::ErrCode::Internal,\n            message: \"failed to build endpoint url\".into(),\n            internal_message: Some(format!(\n                \"failed to build endpoint url for endpoint {target}\"\n            )),\n            stack: None,\n            details: None,\n        })?;\n\n        let mut req = req_url\n            .into_client_request()\n            .map_err(|e| api::Error::invalid_argument(\"unable to create request\", e))?;\n\n        if let HandshakeSchema::Request(req_schema) = handshake.as_ref() {\n            if let Some(qry) = &req_schema.query {\n                qry.to_outgoing_request(&mut data, &mut req)?;\n            }\n\n            if let Some(hdr) = &req_schema.header {\n                hdr.to_outgoing_request(&mut data, &mut req)?;\n            }\n\n            if let Some(c) = &req_schema.cookie {\n                c.to_outgoing_request(&mut data, &mut req)?;\n            }\n        }\n\n        self.propagate_call_meta(req.headers_mut(), endpoint, source, start_event_id, opts)\n            .map_err(api::Error::internal)?;\n\n        let outgoing = endpoint.request[0].clone();\n        let incoming = endpoint.response.clone();\n        Ok((req, outgoing, incoming))\n    }\n\n    fn propagate_call_meta(\n        &self,\n        headers: &mut reqwest::header::HeaderMap,\n        endpoint: &Endpoint,\n        source: Option<&model::Request>,\n        parent_event_id: Option<TraceEventId>,\n        opts: Option<&api::CallOpts>,\n    ) -> anyhow::Result<()> {\n        let svc_auth_method = self\n            .service_auth_method(endpoint.name.service())\n            .ok_or_else(|| api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"not found\".into(),\n                internal_message: Some(format!(\n                    \"no service auth method found for service {}\",\n                    endpoint.name.service()\n                )),\n                stack: None,\n                details: None,\n            })?;\n\n        let caller = match source {\n            Some(source) => match source.data {\n                model::RequestData::RPC(ref data) => {\n                    Caller::APIEndpoint(data.endpoint.name.clone())\n                }\n                model::RequestData::Auth(ref data) => {\n                    Caller::APIEndpoint(data.auth_handler.clone())\n                }\n                model::RequestData::PubSub(ref data) => Caller::PubSubMessage {\n                    topic: data.topic.clone(),\n                    subscription: data.subscription.clone(),\n                    message_id: data.message_id.clone(),\n                },\n                model::RequestData::Stream(ref data) => {\n                    Caller::APIEndpoint(data.endpoint.name.clone())\n                }\n            },\n            None => Caller::App {\n                deploy_id: self.deploy_id.clone(),\n            },\n        };\n\n        let auth_opts = opts.as_ref().and_then(|o| o.auth.as_ref());\n\n        let auth_data = auth_opts.map(|o| &o.data).or_else(|| {\n            source.and_then(|r| match &r.data {\n                model::RequestData::RPC(data) => data.auth_data.as_ref(),\n                model::RequestData::Stream(data) => data.auth_data.as_ref(),\n                model::RequestData::Auth(_) => None,\n                model::RequestData::PubSub(_) => None,\n            })\n        });\n\n        let auth_user_id = auth_opts\n            .map(|o| &o.user_id)\n            .or_else(|| {\n                source.and_then(|r| match &r.data {\n                    model::RequestData::RPC(data) => data.auth_user_id.as_ref(),\n                    model::RequestData::Stream(data) => data.auth_user_id.as_ref(),\n                    model::RequestData::Auth(_) => None,\n                    model::RequestData::PubSub(_) => None,\n                })\n            })\n            .map(|id| Cow::Borrowed(id.as_str()));\n\n        let desc = CallDesc {\n            caller: &caller,\n            svc_auth_method: svc_auth_method.as_ref(),\n            parent_span: source.map(|r| r.span),\n            parent_event_id,\n            ext_correlation_id: source.and_then(|r| {\n                r.ext_correlation_id\n                    .as_ref()\n                    .map(|id| Cow::Borrowed(id.as_str()))\n            }),\n            traced: source.map(|r| r.traced).unwrap_or(false),\n            auth_user_id,\n            auth_data,\n        };\n\n        desc.add_meta(headers)?;\n\n        Ok(())\n    }\n}\n\npub struct CallDesc<'a, AuthData> {\n    pub caller: &'a Caller,\n\n    pub parent_span: Option<SpanKey>,\n    pub parent_event_id: Option<TraceEventId>,\n    pub ext_correlation_id: Option<Cow<'a, str>>,\n\n    /// Whether the source request is being traced.\n    pub traced: bool,\n\n    pub auth_user_id: Option<Cow<'a, str>>,\n    pub auth_data: Option<AuthData>,\n\n    pub svc_auth_method: &'a dyn svcauth::ServiceAuthMethod,\n}\n\nimpl<'a, AuthData> CallDesc<'a, AuthData>\nwhere\n    AuthData: serde::ser::Serialize + 'a,\n{\n    pub fn add_meta<R: MetaMapMut>(self, headers: &mut R) -> anyhow::Result<()> {\n        headers.set(MetaKey::Version, \"1\".to_string())?;\n\n        if let Some(span) = self.parent_span {\n            headers.set(\n                MetaKey::TraceParent,\n                format!(\n                    \"00-{}-{}-{}\",\n                    span.0.serialize_std(),\n                    span.1.serialize_std(),\n                    if self.traced { \"01\" } else { \"00\" },\n                ),\n            )?;\n\n            let mut trace_state = format!(\"encore/span-id={}\", span.1.serialize_std());\n\n            if let Some(event_id) = self.parent_event_id.map(|id| id.serialize()) {\n                trace_state.push_str(\",encore/event-id=\");\n                trace_state.push_str(event_id.to_string().as_str());\n            }\n            headers.set(MetaKey::TraceState, trace_state)?;\n        }\n\n        // TODO handle GCP span propagation with tracestate key.\n        // headers.set(MetaKey::TraceState, \"\")?;\n\n        if let Some(corr_id) = self.ext_correlation_id {\n            headers.set(MetaKey::XCorrelationId, corr_id.into_owned())?;\n        }\n\n        // Add auth data.\n        if let Some(auth_uid) = self.auth_user_id {\n            headers.set(MetaKey::UserId, auth_uid.into_owned())?;\n            if let Some(auth_data) = self.auth_data {\n                if let Ok(auth_data) = serde_json::to_string(&auth_data) {\n                    headers.set(MetaKey::UserData, auth_data)?;\n                }\n            }\n        }\n\n        // Caller.\n        headers.set(MetaKey::Caller, self.caller.serialize())?;\n\n        let now = SystemTime::now();\n\n        self.svc_auth_method\n            .sign(headers, now)\n            .map_err(api::Error::internal)?;\n\n        headers.set(\n            MetaKey::SvcAuthMethod,\n            self.svc_auth_method.name().to_string(),\n        )?;\n\n        Ok(())\n    }\n}\n\nasync fn extract_error(resp: reqwest::Response) -> api::Error {\n    match resp.bytes().await {\n        Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_else(|err| {\n            api::Error::invalid_argument(\"unable to parse error response\", err)\n        }),\n        Err(err) => api::Error::invalid_argument(\"unable to read response body\", err),\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/LICENSE",
    "content": "Copyright (c) 2019-2021 Tower Contributors\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/allow_credentials.rs",
    "content": "use std::{fmt, sync::Arc};\n\nuse http::{\n    header::{self, HeaderName, HeaderValue},\n    request::Parts as RequestParts,\n};\n\n/// Holds configuration for how to set the [`Access-Control-Allow-Credentials`][mdn] header.\n///\n/// See [`CorsLayer::allow_credentials`] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials\n/// [`CorsLayer::allow_credentials`]: super::CorsLayer::allow_credentials\n#[derive(Clone, Default)]\n#[must_use]\npub struct AllowCredentials(AllowCredentialsInner);\n\nimpl AllowCredentials {\n    /// Allow credentials for all requests\n    ///\n    /// See [`CorsLayer::allow_credentials`] for more details.\n    ///\n    /// [`CorsLayer::allow_credentials`]: super::CorsLayer::allow_credentials\n    pub fn yes() -> Self {\n        Self(AllowCredentialsInner::Yes)\n    }\n\n    /// Allow credentials for some requests, based on a given predicate\n    ///\n    /// The first argument to the predicate is the request origin.\n    ///\n    /// See [`CorsLayer::allow_credentials`] for more details.\n    ///\n    /// [`CorsLayer::allow_credentials`]: super::CorsLayer::allow_credentials\n    pub fn predicate<F>(f: F) -> Self\n    where\n        F: Fn(&HeaderValue, &RequestParts) -> bool + Send + Sync + 'static,\n    {\n        Self(AllowCredentialsInner::Predicate(Arc::new(f)))\n    }\n\n    pub(super) fn is_true(&self) -> bool {\n        matches!(&self.0, AllowCredentialsInner::Yes)\n    }\n\n    pub(super) fn to_header(\n        &self,\n        origin: Option<&HeaderValue>,\n        parts: &RequestParts,\n    ) -> Option<(HeaderName, HeaderValue)> {\n        #[allow(clippy::declare_interior_mutable_const)]\n        const TRUE: HeaderValue = HeaderValue::from_static(\"true\");\n\n        let allow_creds = match &self.0 {\n            AllowCredentialsInner::Yes => true,\n            AllowCredentialsInner::No => false,\n            AllowCredentialsInner::Predicate(c) => c(origin?, parts),\n        };\n\n        allow_creds.then_some((header::ACCESS_CONTROL_ALLOW_CREDENTIALS, TRUE))\n    }\n}\n\nimpl From<bool> for AllowCredentials {\n    fn from(v: bool) -> Self {\n        match v {\n            true => Self(AllowCredentialsInner::Yes),\n            false => Self(AllowCredentialsInner::No),\n        }\n    }\n}\n\nimpl fmt::Debug for AllowCredentials {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self.0 {\n            AllowCredentialsInner::Yes => f.debug_tuple(\"Yes\").finish(),\n            AllowCredentialsInner::No => f.debug_tuple(\"No\").finish(),\n            AllowCredentialsInner::Predicate(_) => f.debug_tuple(\"Predicate\").finish(),\n        }\n    }\n}\n\ntype PredicateFn =\n    Arc<dyn for<'a> Fn(&'a HeaderValue, &'a RequestParts) -> bool + Send + Sync + 'static>;\n\n#[derive(Clone, Default)]\nenum AllowCredentialsInner {\n    Yes,\n    #[default]\n    No,\n    Predicate(PredicateFn),\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/allow_headers.rs",
    "content": "use std::fmt;\n\nuse http::{\n    header::{self, HeaderName, HeaderValue},\n    request::Parts as RequestParts,\n};\n\nuse super::{separated_by_commas, Any, WILDCARD};\n\n/// Holds configuration for how to set the [`Access-Control-Allow-Headers`][mdn] header.\n///\n/// See [`CorsLayer::allow_headers`] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers\n/// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers\n#[derive(Clone, Default)]\n#[must_use]\npub struct AllowHeaders(AllowHeadersInner);\n\nimpl AllowHeaders {\n    /// Allow any headers by sending a wildcard (`*`)\n    ///\n    /// See [`CorsLayer::allow_headers`] for more details.\n    ///\n    /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers\n    pub fn any() -> Self {\n        Self(AllowHeadersInner::Const(Some(WILDCARD)))\n    }\n\n    /// Set multiple allowed headers\n    ///\n    /// See [`CorsLayer::allow_headers`] for more details.\n    ///\n    /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers\n    pub fn list<I>(headers: I) -> Self\n    where\n        I: IntoIterator<Item = HeaderName>,\n    {\n        Self(AllowHeadersInner::Const(separated_by_commas(\n            headers.into_iter().map(Into::into),\n        )))\n    }\n\n    /// Allow any headers, by mirroring the preflight [`Access-Control-Request-Headers`][mdn]\n    /// header.\n    ///\n    /// See [`CorsLayer::allow_headers`] for more details.\n    ///\n    /// [`CorsLayer::allow_headers`]: super::CorsLayer::allow_headers\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers\n    pub fn mirror_request() -> Self {\n        Self(AllowHeadersInner::MirrorRequest)\n    }\n\n    #[allow(clippy::borrow_interior_mutable_const)]\n    pub(super) fn is_wildcard(&self) -> bool {\n        matches!(&self.0, AllowHeadersInner::Const(Some(v)) if v == WILDCARD)\n    }\n\n    pub(super) fn to_header(&self, parts: &RequestParts) -> Option<(HeaderName, HeaderValue)> {\n        let allow_headers = match &self.0 {\n            AllowHeadersInner::Const(v) => v.clone()?,\n            AllowHeadersInner::MirrorRequest => parts\n                .headers\n                .get(header::ACCESS_CONTROL_REQUEST_HEADERS)?\n                .clone(),\n        };\n\n        Some((header::ACCESS_CONTROL_ALLOW_HEADERS, allow_headers))\n    }\n}\n\nimpl fmt::Debug for AllowHeaders {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match &self.0 {\n            AllowHeadersInner::Const(inner) => f.debug_tuple(\"Const\").field(inner).finish(),\n            AllowHeadersInner::MirrorRequest => f.debug_tuple(\"MirrorRequest\").finish(),\n        }\n    }\n}\n\nimpl From<Any> for AllowHeaders {\n    fn from(_: Any) -> Self {\n        Self::any()\n    }\n}\n\nimpl<const N: usize> From<[HeaderName; N]> for AllowHeaders {\n    fn from(arr: [HeaderName; N]) -> Self {\n        Self::list(arr)\n    }\n}\n\nimpl From<Vec<HeaderName>> for AllowHeaders {\n    fn from(vec: Vec<HeaderName>) -> Self {\n        Self::list(vec)\n    }\n}\n\n#[derive(Clone)]\nenum AllowHeadersInner {\n    Const(Option<HeaderValue>),\n    MirrorRequest,\n}\n\nimpl Default for AllowHeadersInner {\n    fn default() -> Self {\n        Self::Const(None)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/allow_methods.rs",
    "content": "use std::fmt;\n\nuse http::{\n    header::{self, HeaderName, HeaderValue},\n    request::Parts as RequestParts,\n    Method,\n};\n\nuse super::{separated_by_commas, Any, WILDCARD};\n\n/// Holds configuration for how to set the [`Access-Control-Allow-Methods`][mdn] header.\n///\n/// See [`CorsLayer::allow_methods`] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods\n/// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods\n#[derive(Clone, Default)]\n#[must_use]\npub struct AllowMethods(AllowMethodsInner);\n\nimpl AllowMethods {\n    /// Allow any method by sending a wildcard (`*`)\n    ///\n    /// See [`CorsLayer::allow_methods`] for more details.\n    ///\n    /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods\n    pub fn any() -> Self {\n        Self(AllowMethodsInner::Const(Some(WILDCARD)))\n    }\n\n    /// Set a single allowed method\n    ///\n    /// See [`CorsLayer::allow_methods`] for more details.\n    ///\n    /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods\n    pub fn exact(method: Method) -> Self {\n        Self(AllowMethodsInner::Const(Some(\n            HeaderValue::from_str(method.as_str()).unwrap(),\n        )))\n    }\n\n    /// Set multiple allowed methods\n    ///\n    /// See [`CorsLayer::allow_methods`] for more details.\n    ///\n    /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods\n    pub fn list<I>(methods: I) -> Self\n    where\n        I: IntoIterator<Item = Method>,\n    {\n        Self(AllowMethodsInner::Const(separated_by_commas(\n            methods\n                .into_iter()\n                .map(|m| HeaderValue::from_str(m.as_str()).unwrap()),\n        )))\n    }\n\n    /// Allow any method, by mirroring the preflight [`Access-Control-Request-Method`][mdn]\n    /// header.\n    ///\n    /// See [`CorsLayer::allow_methods`] for more details.\n    ///\n    /// [`CorsLayer::allow_methods`]: super::CorsLayer::allow_methods\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method\n    pub fn mirror_request() -> Self {\n        Self(AllowMethodsInner::MirrorRequest)\n    }\n\n    #[allow(clippy::borrow_interior_mutable_const)]\n    pub(super) fn is_wildcard(&self) -> bool {\n        matches!(&self.0, AllowMethodsInner::Const(Some(v)) if v == WILDCARD)\n    }\n\n    pub(super) fn to_header(&self, parts: &RequestParts) -> Option<(HeaderName, HeaderValue)> {\n        let allow_methods = match &self.0 {\n            AllowMethodsInner::Const(v) => v.clone()?,\n            AllowMethodsInner::MirrorRequest => parts\n                .headers\n                .get(header::ACCESS_CONTROL_REQUEST_METHOD)?\n                .clone(),\n        };\n\n        Some((header::ACCESS_CONTROL_ALLOW_METHODS, allow_methods))\n    }\n}\n\nimpl fmt::Debug for AllowMethods {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match &self.0 {\n            AllowMethodsInner::Const(inner) => f.debug_tuple(\"Const\").field(inner).finish(),\n            AllowMethodsInner::MirrorRequest => f.debug_tuple(\"MirrorRequest\").finish(),\n        }\n    }\n}\n\nimpl From<Any> for AllowMethods {\n    fn from(_: Any) -> Self {\n        Self::any()\n    }\n}\n\nimpl From<Method> for AllowMethods {\n    fn from(method: Method) -> Self {\n        Self::exact(method)\n    }\n}\n\nimpl<const N: usize> From<[Method; N]> for AllowMethods {\n    fn from(arr: [Method; N]) -> Self {\n        Self::list(arr)\n    }\n}\n\nimpl From<Vec<Method>> for AllowMethods {\n    fn from(vec: Vec<Method>) -> Self {\n        Self::list(vec)\n    }\n}\n\n#[derive(Clone)]\nenum AllowMethodsInner {\n    Const(Option<HeaderValue>),\n    MirrorRequest,\n}\n\nimpl Default for AllowMethodsInner {\n    fn default() -> Self {\n        Self::Const(None)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/allow_origin.rs",
    "content": "use http::{\n    header::{self, HeaderValue},\n    request::Parts as RequestParts,\n    HeaderName,\n};\nuse std::{fmt, sync::Arc};\n\nuse super::{Any, WILDCARD};\n\n/// Holds configuration for how to set the [`Access-Control-Allow-Origin`][mdn] header.\n///\n/// See [`CorsLayer::allow_origin`] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin\n/// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin\n#[derive(Clone, Default)]\n#[must_use]\npub struct AllowOrigin(OriginInner);\n\nimpl AllowOrigin {\n    /// Allow any origin by sending a wildcard (`*`)\n    ///\n    /// See [`CorsLayer::allow_origin`] for more details.\n    ///\n    /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin\n    pub fn any() -> Self {\n        Self(OriginInner::Const(WILDCARD))\n    }\n\n    /// Set a single allowed origin\n    ///\n    /// See [`CorsLayer::allow_origin`] for more details.\n    ///\n    /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin\n    pub fn exact(origin: HeaderValue) -> Self {\n        Self(OriginInner::Const(origin))\n    }\n\n    /// Set multiple allowed origins\n    ///\n    /// See [`CorsLayer::allow_origin`] for more details.\n    ///\n    /// # Panics\n    ///\n    /// If the iterator contains a wildcard (`*`).\n    ///\n    /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin\n    #[allow(clippy::borrow_interior_mutable_const)]\n    pub fn list<I>(origins: I) -> Self\n    where\n        I: IntoIterator<Item = HeaderValue>,\n    {\n        let origins = origins.into_iter().collect::<Vec<_>>();\n        if origins.contains(&WILDCARD) {\n            panic!(\n                \"Wildcard origin (`*`) cannot be passed to `AllowOrigin::list`. \\\n                 Use `AllowOrigin::any()` instead\"\n            );\n        }\n\n        Self(OriginInner::List(origins))\n    }\n\n    /// Set the allowed origins from a predicate\n    ///\n    /// See [`CorsLayer::allow_origin`] for more details.\n    ///\n    /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin\n    pub fn predicate<F>(f: F) -> Self\n    where\n        F: Fn(&HeaderValue, &RequestParts) -> bool + Send + Sync + 'static,\n    {\n        Self(OriginInner::Predicate(Arc::new(f)))\n    }\n\n    /// Allow any origin, by mirroring the request origin\n    ///\n    /// This is equivalent to\n    /// [`AllowOrigin::predicate(|_, _| true)`][Self::predicate].\n    ///\n    /// See [`CorsLayer::allow_origin`] for more details.\n    ///\n    /// [`CorsLayer::allow_origin`]: super::CorsLayer::allow_origin\n    pub fn mirror_request() -> Self {\n        Self::predicate(|_, _| true)\n    }\n\n    #[allow(clippy::borrow_interior_mutable_const)]\n    pub(super) fn is_wildcard(&self) -> bool {\n        matches!(&self.0, OriginInner::Const(v) if v == WILDCARD)\n    }\n\n    pub(super) fn to_header(\n        &self,\n        origin: Option<&HeaderValue>,\n        parts: &RequestParts,\n    ) -> Option<(HeaderName, HeaderValue)> {\n        let name = header::ACCESS_CONTROL_ALLOW_ORIGIN;\n\n        match &self.0 {\n            OriginInner::Const(v) => Some((name, v.clone())),\n            OriginInner::List(l) => origin.filter(|o| l.contains(o)).map(|o| (name, o.clone())),\n            OriginInner::Predicate(c) => origin.filter(|o| c(o, parts)).map(|o| (name, o.clone())),\n        }\n    }\n}\n\nimpl fmt::Debug for AllowOrigin {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match &self.0 {\n            OriginInner::Const(inner) => f.debug_tuple(\"Const\").field(inner).finish(),\n            OriginInner::List(inner) => f.debug_tuple(\"List\").field(inner).finish(),\n            OriginInner::Predicate(_) => f.debug_tuple(\"Predicate\").finish(),\n        }\n    }\n}\n\nimpl From<Any> for AllowOrigin {\n    fn from(_: Any) -> Self {\n        Self::any()\n    }\n}\n\nimpl From<HeaderValue> for AllowOrigin {\n    fn from(val: HeaderValue) -> Self {\n        Self::exact(val)\n    }\n}\n\nimpl<const N: usize> From<[HeaderValue; N]> for AllowOrigin {\n    fn from(arr: [HeaderValue; N]) -> Self {\n        Self::list(arr)\n    }\n}\n\nimpl From<Vec<HeaderValue>> for AllowOrigin {\n    fn from(vec: Vec<HeaderValue>) -> Self {\n        Self::list(vec)\n    }\n}\n\ntype PredicateFn =\n    Arc<dyn for<'a> Fn(&'a HeaderValue, &'a RequestParts) -> bool + Send + Sync + 'static>;\n\n#[derive(Clone)]\nenum OriginInner {\n    Const(HeaderValue),\n    List(Vec<HeaderValue>),\n    Predicate(PredicateFn),\n}\n\nimpl Default for OriginInner {\n    fn default() -> Self {\n        Self::List(Vec::new())\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/allow_private_network.rs",
    "content": "use std::{fmt, sync::Arc};\n\nuse http::{\n    header::{HeaderName, HeaderValue},\n    request::Parts as RequestParts,\n};\n\n/// Holds configuration for how to set the [`Access-Control-Allow-Private-Network`][wicg] header.\n///\n/// See [`CorsLayer::allow_private_network`] for more details.\n///\n/// [wicg]: https://wicg.github.io/private-network-access/\n/// [`CorsLayer::allow_private_network`]: super::CorsLayer::allow_private_network\n#[derive(Clone, Default)]\n#[must_use]\npub struct AllowPrivateNetwork(AllowPrivateNetworkInner);\n\nimpl AllowPrivateNetwork {\n    /// Allow requests via a more private network than the one used to access the origin\n    ///\n    /// See [`CorsLayer::allow_private_network`] for more details.\n    ///\n    /// [`CorsLayer::allow_private_network`]: super::CorsLayer::allow_private_network\n    pub fn yes() -> Self {\n        Self(AllowPrivateNetworkInner::Yes)\n    }\n\n    /// Allow requests via private network for some requests, based on a given predicate\n    ///\n    /// The first argument to the predicate is the request origin.\n    ///\n    /// See [`CorsLayer::allow_private_network`] for more details.\n    ///\n    /// [`CorsLayer::allow_private_network`]: super::CorsLayer::allow_private_network\n    pub fn predicate<F>(f: F) -> Self\n    where\n        F: Fn(&HeaderValue, &RequestParts) -> bool + Send + Sync + 'static,\n    {\n        Self(AllowPrivateNetworkInner::Predicate(Arc::new(f)))\n    }\n\n    #[allow(\n        clippy::declare_interior_mutable_const,\n        clippy::borrow_interior_mutable_const\n    )]\n    pub(super) fn to_header(\n        &self,\n        origin: Option<&HeaderValue>,\n        parts: &RequestParts,\n    ) -> Option<(HeaderName, HeaderValue)> {\n        #[allow(clippy::declare_interior_mutable_const)]\n        const REQUEST_PRIVATE_NETWORK: HeaderName =\n            HeaderName::from_static(\"access-control-request-private-network\");\n\n        #[allow(clippy::declare_interior_mutable_const)]\n        const ALLOW_PRIVATE_NETWORK: HeaderName =\n            HeaderName::from_static(\"access-control-allow-private-network\");\n\n        const TRUE: HeaderValue = HeaderValue::from_static(\"true\");\n\n        // Cheapest fallback: allow_private_network hasn't been set\n        if let AllowPrivateNetworkInner::No = &self.0 {\n            return None;\n        }\n\n        // Access-Control-Allow-Private-Network is only relevant if the request\n        // has the Access-Control-Request-Private-Network header set, else skip\n        if parts.headers.get(REQUEST_PRIVATE_NETWORK) != Some(&TRUE) {\n            return None;\n        }\n\n        let allow_private_network = match &self.0 {\n            AllowPrivateNetworkInner::Yes => true,\n            AllowPrivateNetworkInner::No => false, // unreachable, but not harmful\n            AllowPrivateNetworkInner::Predicate(c) => c(origin?, parts),\n        };\n\n        allow_private_network.then_some((ALLOW_PRIVATE_NETWORK, TRUE))\n    }\n}\n\nimpl From<bool> for AllowPrivateNetwork {\n    fn from(v: bool) -> Self {\n        match v {\n            true => Self(AllowPrivateNetworkInner::Yes),\n            false => Self(AllowPrivateNetworkInner::No),\n        }\n    }\n}\n\nimpl fmt::Debug for AllowPrivateNetwork {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self.0 {\n            AllowPrivateNetworkInner::Yes => f.debug_tuple(\"Yes\").finish(),\n            AllowPrivateNetworkInner::No => f.debug_tuple(\"No\").finish(),\n            AllowPrivateNetworkInner::Predicate(_) => f.debug_tuple(\"Predicate\").finish(),\n        }\n    }\n}\ntype PredicateFn =\n    Arc<dyn for<'a> Fn(&'a HeaderValue, &'a RequestParts) -> bool + Send + Sync + 'static>;\n\n#[derive(Clone, Default)]\nenum AllowPrivateNetworkInner {\n    Yes,\n    #[default]\n    No,\n    Predicate(PredicateFn),\n}\n\n#[cfg(test)]\nmod tests {\n    #![allow(\n        clippy::declare_interior_mutable_const,\n        clippy::borrow_interior_mutable_const\n    )]\n\n    use super::AllowPrivateNetwork;\n    use crate::api::cors::cors_headers_config::CorsHeadersConfig;\n\n    use http::{header::ORIGIN, request::Parts, HeaderName, HeaderValue};\n    use pingora::http::{RequestHeader, ResponseHeader};\n\n    const REQUEST_PRIVATE_NETWORK: HeaderName =\n        HeaderName::from_static(\"access-control-request-private-network\");\n\n    const ALLOW_PRIVATE_NETWORK: HeaderName =\n        HeaderName::from_static(\"access-control-allow-private-network\");\n\n    const TRUE: HeaderValue = HeaderValue::from_static(\"true\");\n\n    #[tokio::test]\n    async fn cors_private_network_header_is_added_correctly() {\n        let conf = CorsHeadersConfig::new().allow_private_network(true);\n\n        let mut req = RequestHeader::build(http::Method::POST, b\"/some/path\", None).unwrap();\n        req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap();\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n\n        conf.apply(&req, &mut resp).unwrap();\n        assert_eq!(resp.headers.get(ALLOW_PRIVATE_NETWORK).unwrap(), TRUE);\n\n        let req = RequestHeader::build(http::Method::POST, b\"/some/path\", None).unwrap();\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n\n        conf.apply(&req, &mut resp).unwrap();\n        assert!(resp.headers.get(ALLOW_PRIVATE_NETWORK).is_none());\n    }\n\n    #[tokio::test]\n    async fn cors_private_network_header_is_added_correctly_with_predicate() {\n        let allow_private_network =\n            AllowPrivateNetwork::predicate(|origin: &HeaderValue, parts: &Parts| {\n                parts.uri.path() == \"/allow-private\" && origin == \"localhost\"\n            });\n\n        let conf = CorsHeadersConfig::new().allow_private_network(allow_private_network);\n\n        let mut req = RequestHeader::build(http::Method::POST, b\"/allow-private\", None).unwrap();\n        req.insert_header(ORIGIN, \"localhost\").unwrap();\n        req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap();\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n\n        conf.apply(&req, &mut resp).unwrap();\n        assert_eq!(resp.headers.get(ALLOW_PRIVATE_NETWORK).unwrap(), TRUE);\n\n        let mut req = RequestHeader::build(http::Method::POST, b\"/other\", None).unwrap();\n        req.insert_header(ORIGIN, \"localhost\").unwrap();\n        req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap();\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n\n        conf.apply(&req, &mut resp).unwrap();\n        assert!(resp.headers.get(ALLOW_PRIVATE_NETWORK).is_none());\n\n        let mut req = RequestHeader::build(http::Method::POST, b\"/allow-private\", None).unwrap();\n        req.insert_header(ORIGIN, \"not-localhost\").unwrap();\n        req.insert_header(REQUEST_PRIVATE_NETWORK, TRUE).unwrap();\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n\n        conf.apply(&req, &mut resp).unwrap();\n        assert!(resp.headers.get(ALLOW_PRIVATE_NETWORK).is_none());\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/expose_headers.rs",
    "content": "use std::fmt;\n\nuse http::header::{self, HeaderName, HeaderValue};\n\nuse super::{separated_by_commas, Any, WILDCARD};\n\n/// Holds configuration for how to set the [`Access-Control-Expose-Headers`][mdn] header.\n///\n/// See [`CorsLayer::expose_headers`] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers\n/// [`CorsLayer::expose_headers`]: super::CorsLayer::expose_headers\n#[derive(Clone, Default)]\n#[must_use]\npub struct ExposeHeaders(ExposeHeadersInner);\n\nimpl ExposeHeaders {\n    /// Expose any / all headers by sending a wildcard (`*`)\n    ///\n    /// See [`CorsLayer::expose_headers`] for more details.\n    ///\n    /// [`CorsLayer::expose_headers`]: super::CorsLayer::expose_headers\n    pub fn any() -> Self {\n        Self(ExposeHeadersInner::Const(Some(WILDCARD)))\n    }\n\n    /// Set multiple exposed header names\n    ///\n    /// See [`CorsLayer::expose_headers`] for more details.\n    ///\n    /// [`CorsLayer::expose_headers`]: super::CorsLayer::expose_headers\n    pub fn list<I>(headers: I) -> Self\n    where\n        I: IntoIterator<Item = HeaderName>,\n    {\n        Self(ExposeHeadersInner::Const(separated_by_commas(\n            headers.into_iter().map(Into::into),\n        )))\n    }\n\n    #[allow(clippy::borrow_interior_mutable_const)]\n    pub(super) fn is_wildcard(&self) -> bool {\n        matches!(&self.0, ExposeHeadersInner::Const(Some(v)) if v == WILDCARD)\n    }\n\n    pub(super) fn to_header(&self) -> Option<(HeaderName, HeaderValue)> {\n        let expose_headers = match &self.0 {\n            ExposeHeadersInner::Const(v) => v.clone()?,\n        };\n\n        Some((header::ACCESS_CONTROL_EXPOSE_HEADERS, expose_headers))\n    }\n}\n\nimpl fmt::Debug for ExposeHeaders {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match &self.0 {\n            ExposeHeadersInner::Const(inner) => f.debug_tuple(\"Const\").field(inner).finish(),\n        }\n    }\n}\n\nimpl From<Any> for ExposeHeaders {\n    fn from(_: Any) -> Self {\n        Self::any()\n    }\n}\n\nimpl<const N: usize> From<[HeaderName; N]> for ExposeHeaders {\n    fn from(arr: [HeaderName; N]) -> Self {\n        Self::list(arr)\n    }\n}\n\nimpl From<Vec<HeaderName>> for ExposeHeaders {\n    fn from(vec: Vec<HeaderName>) -> Self {\n        Self::list(vec)\n    }\n}\n\n#[derive(Clone)]\nenum ExposeHeadersInner {\n    Const(Option<HeaderValue>),\n}\n\nimpl Default for ExposeHeadersInner {\n    fn default() -> Self {\n        ExposeHeadersInner::Const(None)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/max_age.rs",
    "content": "use std::{fmt, sync::Arc, time::Duration};\n\nuse http::{\n    header::{self, HeaderName, HeaderValue},\n    request::Parts as RequestParts,\n};\n\n/// Holds configuration for how to set the [`Access-Control-Max-Age`][mdn] header.\n///\n/// See [`CorsLayer::max_age`][super::CorsLayer::max_age] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age\n#[derive(Clone, Default)]\n#[must_use]\npub struct MaxAge(MaxAgeInner);\n\nimpl MaxAge {\n    /// Set a static max-age value\n    ///\n    /// See [`CorsLayer::max_age`][super::CorsLayer::max_age] for more details.\n    pub fn exact(max_age: Duration) -> Self {\n        Self(MaxAgeInner::Exact(Some(max_age.as_secs().into())))\n    }\n\n    /// Set the max-age based on the preflight request parts\n    ///\n    /// See [`CorsLayer::max_age`][super::CorsLayer::max_age] for more details.\n    pub fn dynamic<F>(f: F) -> Self\n    where\n        F: Fn(&HeaderValue, &RequestParts) -> Duration + Send + Sync + 'static,\n    {\n        Self(MaxAgeInner::Fn(Arc::new(f)))\n    }\n\n    pub(super) fn to_header(\n        &self,\n        origin: Option<&HeaderValue>,\n        parts: &RequestParts,\n    ) -> Option<(HeaderName, HeaderValue)> {\n        let max_age = match &self.0 {\n            MaxAgeInner::Exact(v) => v.clone()?,\n            MaxAgeInner::Fn(c) => c(origin?, parts).as_secs().into(),\n        };\n\n        Some((header::ACCESS_CONTROL_MAX_AGE, max_age))\n    }\n}\n\nimpl fmt::Debug for MaxAge {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match &self.0 {\n            MaxAgeInner::Exact(inner) => f.debug_tuple(\"Exact\").field(inner).finish(),\n            MaxAgeInner::Fn(_) => f.debug_tuple(\"Fn\").finish(),\n        }\n    }\n}\n\nimpl From<Duration> for MaxAge {\n    fn from(max_age: Duration) -> Self {\n        Self::exact(max_age)\n    }\n}\n\ntype MaxAgeFn =\n    Arc<dyn for<'a> Fn(&'a HeaderValue, &'a RequestParts) -> Duration + Send + Sync + 'static>;\n\n#[derive(Clone)]\nenum MaxAgeInner {\n    Exact(Option<HeaderValue>),\n    Fn(MaxAgeFn),\n}\n\nimpl Default for MaxAgeInner {\n    fn default() -> Self {\n        Self::Exact(None)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/mod.rs",
    "content": "#![allow(clippy::enum_variant_names)]\n\nuse bytes::{BufMut, BytesMut};\nuse http::{\n    header::{self, HeaderName},\n    HeaderValue, Method,\n};\nuse pingora::http::{RequestHeader, ResponseHeader};\n\nmod allow_credentials;\nmod allow_headers;\nmod allow_methods;\nmod allow_origin;\nmod allow_private_network;\nmod expose_headers;\nmod max_age;\nmod vary;\n\npub use self::{\n    allow_credentials::AllowCredentials, allow_headers::AllowHeaders, allow_methods::AllowMethods,\n    allow_origin::AllowOrigin, allow_private_network::AllowPrivateNetwork,\n    expose_headers::ExposeHeaders, max_age::MaxAge, vary::Vary,\n};\n\n/// Configuration for how cors headers should be appied\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS\n#[derive(Debug, Clone)]\n#[must_use]\npub struct CorsHeadersConfig {\n    allow_credentials: AllowCredentials,\n    allow_headers: AllowHeaders,\n    allow_methods: AllowMethods,\n    allow_origin: AllowOrigin,\n    allow_private_network: AllowPrivateNetwork,\n    expose_headers: ExposeHeaders,\n    max_age: MaxAge,\n    vary: Vary,\n}\n\n#[allow(clippy::declare_interior_mutable_const)]\nconst WILDCARD: HeaderValue = HeaderValue::from_static(\"*\");\n\nimpl CorsHeadersConfig {\n    /// Create a new `CorsHeadersConfig`.\n    ///\n    /// No headers are sent by default. Use the builder methods to customize\n    /// the behavior.\n    ///\n    /// You need to set at least an allowed origin for browsers to make\n    /// successful cross-origin requests to your service.\n    pub fn new() -> Self {\n        Self {\n            allow_credentials: Default::default(),\n            allow_headers: Default::default(),\n            allow_methods: Default::default(),\n            allow_origin: Default::default(),\n            allow_private_network: Default::default(),\n            expose_headers: Default::default(),\n            max_age: Default::default(),\n            vary: Default::default(),\n        }\n    }\n\n    /// A permissive configuration:\n    ///\n    /// - All request headers allowed.\n    /// - All methods allowed.\n    /// - All origins allowed.\n    /// - All headers exposed.\n    pub fn permissive() -> Self {\n        Self::new()\n            .allow_headers(Any)\n            .allow_methods(Any)\n            .allow_origin(Any)\n            .expose_headers(Any)\n    }\n\n    /// A very permissive configuration:\n    ///\n    /// - **Credentials allowed.**\n    /// - The method received in `Access-Control-Request-Method` is sent back\n    ///   as an allowed method.\n    /// - The origin of the preflight request is sent back as an allowed origin.\n    /// - The header names received in `Access-Control-Request-Headers` are sent\n    ///   back as allowed headers.\n    /// - No headers are currently exposed, but this may change in the future.\n    pub fn very_permissive() -> Self {\n        Self::new()\n            .allow_credentials(true)\n            .allow_headers(AllowHeaders::mirror_request())\n            .allow_methods(AllowMethods::mirror_request())\n            .allow_origin(AllowOrigin::mirror_request())\n    }\n\n    /// Set the [`Access-Control-Allow-Credentials`][mdn] header.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials\n    pub fn allow_credentials<T>(mut self, allow_credentials: T) -> Self\n    where\n        T: Into<AllowCredentials>,\n    {\n        self.allow_credentials = allow_credentials.into();\n        self\n    }\n\n    /// Set the value of the [`Access-Control-Allow-Headers`][mdn] header.\n    ///\n    /// Note that multiple calls to this method will override any previous\n    /// calls.\n    ///\n    /// Also note that `Access-Control-Allow-Headers` is required for requests that have\n    /// `Access-Control-Request-Headers`.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers\n    pub fn allow_headers<T>(mut self, headers: T) -> Self\n    where\n        T: Into<AllowHeaders>,\n    {\n        self.allow_headers = headers.into();\n        self\n    }\n\n    /// Set the value of the [`Access-Control-Max-Age`][mdn] header.\n    ///\n    /// By default the header will not be set which disables caching and will\n    /// require a preflight call for all requests.\n    ///\n    /// Note that each browser has a maximum internal value that takes\n    /// precedence when the Access-Control-Max-Age is greater. For more details\n    /// see [mdn].\n    ///\n    /// If you need more flexibility, you can use supply a function which can\n    /// dynamically decide the max-age based on the origin and other parts of\n    /// each preflight request, using `MaxAge::dynamic`.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age\n    pub fn max_age<T>(mut self, max_age: T) -> Self\n    where\n        T: Into<MaxAge>,\n    {\n        self.max_age = max_age.into();\n        self\n    }\n\n    /// Set the value of the [`Access-Control-Allow-Methods`][mdn] header.\n    ///\n    /// Note that multiple calls to this method will override any previous\n    /// calls.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods\n    pub fn allow_methods<T>(mut self, methods: T) -> Self\n    where\n        T: Into<AllowMethods>,\n    {\n        self.allow_methods = methods.into();\n        self\n    }\n\n    /// Set the value of the [`Access-Control-Allow-Origin`][mdn] header.\n    ///\n    /// You can also use a closure with `AllowOrigin::predicate`\n    ///\n    /// Note that multiple calls to this method will override any previous\n    /// calls.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin\n    pub fn allow_origin<T>(mut self, origin: T) -> Self\n    where\n        T: Into<AllowOrigin>,\n    {\n        self.allow_origin = origin.into();\n        self\n    }\n\n    /// Set the value of the [`Access-Control-Expose-Headers`][mdn] header.\n    ///\n    /// Note that multiple calls to this method will override any previous\n    /// calls.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers\n    pub fn expose_headers<T>(mut self, headers: T) -> Self\n    where\n        T: Into<ExposeHeaders>,\n    {\n        self.expose_headers = headers.into();\n        self\n    }\n\n    /// Set the value of the [`Access-Control-Allow-Private-Network`][wicg] header.\n    ///\n    /// [wicg]: https://wicg.github.io/private-network-access/\n    pub fn allow_private_network<T>(mut self, allow_private_network: T) -> Self\n    where\n        T: Into<AllowPrivateNetwork>,\n    {\n        self.allow_private_network = allow_private_network.into();\n        self\n    }\n\n    /// Set the value(s) of the [`Vary`][mdn] header.\n    ///\n    /// In contrast to the other headers, this one has a non-empty default of\n    /// [`preflight_request_headers()`].\n    ///\n    /// You only need to set this is you want to remove some of these defaults,\n    /// or if you use a closure for one of the other headers and want to add a\n    /// vary header accordingly.\n    ///\n    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary\n    pub fn vary<T>(mut self, headers: T) -> Self\n    where\n        T: Into<Vary>,\n    {\n        self.vary = headers.into();\n        self\n    }\n}\n\n/// Represents a wildcard value (`*`) used with some CORS headers such as\n/// [`CorsHeadersConfig::allow_methods`].\n#[derive(Debug, Clone, Copy)]\n#[must_use]\npub struct Any;\n\nfn separated_by_commas<I>(mut iter: I) -> Option<HeaderValue>\nwhere\n    I: Iterator<Item = HeaderValue>,\n{\n    match iter.next() {\n        Some(fst) => {\n            let mut result = BytesMut::from(fst.as_bytes());\n            for val in iter {\n                result.reserve(val.len() + 1);\n                result.put_u8(b',');\n                result.extend_from_slice(val.as_bytes());\n            }\n\n            Some(HeaderValue::from_maybe_shared(result.freeze()).unwrap())\n        }\n        None => None,\n    }\n}\n\nimpl Default for CorsHeadersConfig {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl CorsHeadersConfig {\n    pub fn apply(&self, req: &RequestHeader, resp: &mut ResponseHeader) -> pingora::Result<()> {\n        let origin = req.headers.get(&header::ORIGIN);\n\n        // These headers are applied to both preflight and subsequent regular CORS requests:\n        // https://fetch.spec.whatwg.org/#http-responses\n\n        append_response_header(resp, self.allow_credentials.to_header(origin, req))?;\n        append_response_header(resp, self.allow_private_network.to_header(origin, req))?;\n        append_response_header(resp, self.vary.to_header())?;\n        append_response_header(resp, self.allow_origin.to_header(origin, req))?;\n\n        // Return results immediately upon preflight request\n        if req.method == Method::OPTIONS {\n            // These headers are applied only to preflight requests\n            append_response_header(resp, self.allow_methods.to_header(req))?;\n            append_response_header(resp, self.allow_headers.to_header(req))?;\n            append_response_header(resp, self.max_age.to_header(origin, req))?;\n        } else {\n            // This header is applied only to non-preflight requests\n            append_response_header(resp, self.expose_headers.to_header())?;\n        }\n\n        Ok(())\n    }\n}\n\nfn append_response_header(\n    resp: &mut ResponseHeader,\n    header: Option<(HeaderName, HeaderValue)>,\n) -> pingora::Result<()> {\n    if let Some((key, value)) = header {\n        resp.append_header(key, value)?;\n    }\n\n    Ok(())\n}\n\npub fn ensure_usable_cors_rules(config: &CorsHeadersConfig) {\n    if config.allow_credentials.is_true() {\n        assert!(\n            !config.allow_headers.is_wildcard(),\n            \"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \\\n             with `Access-Control-Allow-Headers: *`\"\n        );\n\n        assert!(\n            !config.allow_methods.is_wildcard(),\n            \"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \\\n             with `Access-Control-Allow-Methods: *`\"\n        );\n\n        assert!(\n            !config.allow_origin.is_wildcard(),\n            \"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \\\n             with `Access-Control-Allow-Origin: *`\"\n        );\n\n        assert!(\n            !config.expose_headers.is_wildcard(),\n            \"Invalid CORS configuration: Cannot combine `Access-Control-Allow-Credentials: true` \\\n             with `Access-Control-Expose-Headers: *`\"\n        );\n    }\n}\n\n/// Returns an iterator over the three request headers that may be involved in a CORS preflight request.\n///\n/// This is the default set of header names returned in the `vary` header\npub fn preflight_request_headers() -> impl Iterator<Item = HeaderName> {\n    [\n        header::ORIGIN,\n        header::ACCESS_CONTROL_REQUEST_METHOD,\n        header::ACCESS_CONTROL_REQUEST_HEADERS,\n    ]\n    .into_iter()\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/cors_headers_config/vary.rs",
    "content": "use http::header::{self, HeaderName, HeaderValue};\n\nuse super::preflight_request_headers;\n\n/// Holds configuration for how to set the [`Vary`][mdn] header.\n///\n/// See [`CorsLayer::vary`] for more details.\n///\n/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary\n/// [`CorsLayer::vary`]: super::CorsLayer::vary\n#[derive(Clone, Debug)]\npub struct Vary(Vec<HeaderValue>);\n\nimpl Vary {\n    /// Set the list of header names to return as vary header values\n    ///\n    /// See [`CorsLayer::vary`] for more details.\n    ///\n    /// [`CorsLayer::vary`]: super::CorsLayer::vary\n    pub fn list<I>(headers: I) -> Self\n    where\n        I: IntoIterator<Item = HeaderName>,\n    {\n        Self(headers.into_iter().map(Into::into).collect())\n    }\n\n    pub(super) fn to_header(&self) -> Option<(HeaderName, HeaderValue)> {\n        let values = &self.0;\n        let mut res = values.first()?.as_bytes().to_owned();\n        for val in &values[1..] {\n            res.extend_from_slice(b\", \");\n            res.extend_from_slice(val.as_bytes());\n        }\n\n        let header_val = HeaderValue::from_bytes(&res)\n            .expect(\"comma-separated list of HeaderValues is always a valid HeaderValue\");\n        Some((header::VARY, header_val))\n    }\n}\n\nimpl Default for Vary {\n    fn default() -> Self {\n        Self::list(preflight_request_headers())\n    }\n}\n\nimpl<const N: usize> From<[HeaderName; N]> for Vary {\n    fn from(arr: [HeaderName; N]) -> Self {\n        Self::list(arr)\n    }\n}\n\nimpl From<Vec<HeaderName>> for Vary {\n    fn from(vec: Vec<HeaderName>) -> Self {\n        Self::list(vec)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/mod.rs",
    "content": "use crate::api::{auth, EndpointMap};\nuse crate::encore::runtime::v1 as pb;\nuse crate::encore::runtime::v1::gateway::CorsAllowedOrigins;\nuse anyhow::Context;\nuse axum::http::{HeaderName, HeaderValue};\nuse http::header::{ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION, COOKIE};\nuse std::collections::HashSet;\nuse std::str::FromStr;\n\nuse self::cors_headers_config::{ensure_usable_cors_rules, CorsHeadersConfig};\n\npub mod cors_headers_config;\n\n#[cfg(test)]\nmod tests;\n\n/// The default set of allowed headers.\n#[allow(clippy::declare_interior_mutable_const)]\nconst ALWAYS_ALLOWED_HEADERS: [HeaderName; 8] = [\n    HeaderName::from_static(\"accept\"),\n    HeaderName::from_static(\"authorization\"),\n    HeaderName::from_static(\"content-type\"),\n    HeaderName::from_static(\"origin\"),\n    HeaderName::from_static(\"user-agent\"),\n    HeaderName::from_static(\"x-correlation-id\"),\n    HeaderName::from_static(\"x-request-id\"),\n    HeaderName::from_static(\"x-requested-with\"),\n];\n\n#[allow(clippy::declare_interior_mutable_const)]\npub const ALWAYS_EXPOSED_HEADERS: [HeaderName; 3] = [\n    HeaderName::from_static(\"x-request-id\"),\n    HeaderName::from_static(\"x-correlation-id\"),\n    HeaderName::from_static(\"x-encore-trace-id\"),\n];\n\npub fn config(cfg: &pb::gateway::Cors, meta: MetaHeaders) -> anyhow::Result<CorsHeadersConfig> {\n    let allow_any_headers = cfg.extra_allowed_headers.iter().any(|val| val == \"*\");\n\n    let allow_headers = if allow_any_headers {\n        cors_headers_config::AllowHeaders::mirror_request()\n    } else {\n        let mut allowed_headers = cfg\n            .extra_allowed_headers\n            .iter()\n            .map(|s| HeaderName::from_str(s))\n            .collect::<Result<Vec<_>, _>>()\n            .context(\"failed to parse extra allowed headers\")?;\n        #[allow(clippy::borrow_interior_mutable_const)]\n        allowed_headers.extend_from_slice(&ALWAYS_ALLOWED_HEADERS);\n        allowed_headers.extend(meta.allow_headers);\n\n        cors_headers_config::AllowHeaders::list(allowed_headers)\n    };\n\n    let mut exposed_headers = cfg\n        .extra_exposed_headers\n        .iter()\n        .map(|s| HeaderName::from_str(s))\n        .collect::<Result<Vec<_>, _>>()\n        .context(\"failed to parse extra exposed headers\")?;\n    #[allow(clippy::borrow_interior_mutable_const)]\n    exposed_headers.extend_from_slice(&ALWAYS_EXPOSED_HEADERS);\n    exposed_headers.extend(meta.expose_headers);\n\n    // Compute the allowed origins.\n    let allow_origin = {\n        use pb::gateway::cors::AllowedOriginsWithCredentials;\n        let with_creds = match &cfg.allowed_origins_with_credentials {\n            Some(AllowedOriginsWithCredentials::UnsafeAllowAllOriginsWithCredentials(true)) => {\n                OriginSet::All\n            }\n            Some(AllowedOriginsWithCredentials::AllowedOrigins(list)) => {\n                OriginSet::new(list.allowed_origins.clone())\n            }\n            _ => OriginSet::Some(vec![]),\n        };\n        let without_creds = {\n            if let Some(CorsAllowedOrigins { allowed_origins }) =\n                &cfg.allowed_origins_without_credentials\n            {\n                OriginSet::new(allowed_origins.to_vec())\n            } else {\n                OriginSet::All\n            }\n        };\n\n        let request_has_creds = |req: &axum::http::request::Parts| -> bool {\n            if req.headers.contains_key(AUTHORIZATION) || req.headers.contains_key(COOKIE) {\n                return true;\n            }\n\n            if req.method == http::method::Method::OPTIONS {\n                return req\n                    .headers\n                    .get_all(ACCESS_CONTROL_REQUEST_HEADERS)\n                    .iter()\n                    .any(|val| {\n                        val.to_str()\n                            .map(|val| {\n                                val.split(\",\")\n                                    .map(|val| val.trim())\n                                    .any(|val| val == \"authorization\" || val == \"cookie\")\n                            })\n                            .unwrap_or(false)\n                    });\n            }\n\n            false\n        };\n\n        let pred = move |origin: &HeaderValue, req: &axum::http::request::Parts| {\n            let Ok(origin) = origin.to_str() else {\n                return false;\n            };\n            if request_has_creds(req) {\n                with_creds.allows(origin)\n            } else {\n                without_creds.allows(origin)\n            }\n        };\n        pred\n    };\n\n    let config = CorsHeadersConfig::new()\n        .allow_private_network(cfg.allow_private_network_access)\n        .allow_headers(allow_headers)\n        .expose_headers(cors_headers_config::ExposeHeaders::list(exposed_headers))\n        .allow_credentials(!cfg.disable_credentials)\n        .allow_methods(cors_headers_config::AllowMethods::mirror_request())\n        .allow_origin(cors_headers_config::AllowOrigin::predicate(allow_origin));\n\n    ensure_usable_cors_rules(&config);\n    Ok(config)\n}\n\nenum OriginSet {\n    All,\n    Some(Vec<Origin>),\n}\n\nimpl OriginSet {\n    fn new(origins: Vec<String>) -> Self {\n        let mut set = Vec::with_capacity(origins.len());\n        for o in origins {\n            if o == \"*\" {\n                return Self::All;\n            }\n            set.push(crate::api::cors::Origin::new(o));\n        }\n        Self::Some(set)\n    }\n\n    fn allows(&self, origin: &str) -> bool {\n        let origin = origin.to_lowercase();\n        match self {\n            Self::All => true,\n            Self::Some(origins) => origins.iter().any(|o| o.matches(&origin)),\n        }\n    }\n}\n\nenum Origin {\n    Exact(String),\n    Wildcard { prefix: String, suffix: String },\n}\n\nimpl Origin {\n    fn new(origin: String) -> Self {\n        match origin.split_once('*') {\n            Some((prefix, suffix)) => Self::Wildcard {\n                prefix: prefix.to_string(),\n                suffix: suffix.to_string(),\n            },\n            None => Self::Exact(origin),\n        }\n    }\n\n    fn matches(&self, origin: &str) -> bool {\n        match self {\n            Self::Exact(exact) => origin == exact,\n            Self::Wildcard { prefix, suffix } => {\n                // Length must be greater than the prefix and suffix combined,\n                // to ensure the wildcard matches at least one character.\n                origin.len() > (prefix.len() + suffix.len())\n                    && origin.starts_with(prefix)\n                    && origin.ends_with(suffix)\n            }\n        }\n    }\n}\n\n/// Additional CORS configuration based on the app metadata.\npub struct MetaHeaders {\n    pub allow_headers: HashSet<HeaderName>,\n    pub expose_headers: HashSet<HeaderName>,\n}\n\nimpl MetaHeaders {\n    pub fn from_schema(endpoints: &EndpointMap, auth: Option<&auth::Authenticator>) -> Self {\n        let mut allow_headers = HashSet::new();\n        let mut expose_headers = HashSet::new();\n\n        for ep in endpoints.values() {\n            if !ep.exposed {\n                continue;\n            }\n            for h in ep.request.iter().flat_map(|req| req.header.iter()) {\n                allow_headers.extend(h.header_names());\n            }\n            expose_headers.extend(ep.response.header.iter().flat_map(|h| h.header_names()));\n        }\n\n        // If we have an auth handler, add the auth headers to the allow list.\n        if let Some(auth) = auth {\n            allow_headers.extend(auth.schema().header.iter().flat_map(|h| h.header_names()));\n        }\n\n        Self {\n            allow_headers,\n            expose_headers,\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/cors/tests.rs",
    "content": "use http::header::{\n    ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS,\n    ACCESS_CONTROL_REQUEST_METHOD, AUTHORIZATION, CONTENT_TYPE, ORIGIN,\n};\nuse pingora::http::{RequestHeader, ResponseHeader};\n\nuse super::*;\n\nfn check_origins(cors: &CorsHeadersConfig, creds: bool, good: bool, origins: &[HeaderValue]) {\n    for origin in origins {\n        let mut req = RequestHeader::build(\"OPTIONS\", b\"/\", None).expect(\"construct request\");\n        req.insert_header(ORIGIN, origin)\n            .expect(\"insert origin header\");\n\n        if creds {\n            req.insert_header(ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION)\n                .expect(\"insert access-control-request-headers\");\n        }\n\n        let mut resp = ResponseHeader::build(200, None).expect(\"construct response\");\n\n        cors.apply(&req, &mut resp).expect(\"apply cors config\");\n\n        let allow_origin = resp.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN);\n        let allowed = allow_origin.map(|val| val == origin).unwrap_or(false);\n\n        if allowed != good {\n            panic!(\"origin={origin:?} creds={creds}: got allowed={allowed}, want {good}\");\n        } else {\n            println!(\"origin={origin:?} creds={creds}: ok allowed={allowed}\");\n        }\n    }\n}\n\nfn check_headers(cors: &CorsHeadersConfig, allowed_headers: &[HeaderName], want_ok: bool) {\n    let mut req = RequestHeader::build(\"OPTIONS\", b\"/\", None).expect(\"construct request\");\n\n    req.insert_header(ORIGIN, HeaderValue::from_static(\"https://example.org\"))\n        .expect(\"insert origin header\");\n\n    req.insert_header(\n        ACCESS_CONTROL_REQUEST_METHOD,\n        HeaderValue::from_static(\"GET\"),\n    )\n    .expect(\"insert access-control-request-method\");\n\n    let allowed_headers_val = HeaderValue::from_bytes(allowed_headers.join(\", \").as_bytes())\n        .expect(\"create access-control-request-headers value\");\n    req.insert_header(ACCESS_CONTROL_REQUEST_HEADERS, allowed_headers_val)\n        .expect(\"insert access-control-request-headers\");\n\n    let mut resp = ResponseHeader::build(200, None).expect(\"construct response\");\n\n    cors.apply(&req, &mut resp).expect(\"apply cors headers\");\n\n    if !allowed_headers.is_empty() && want_ok {\n        // check that the CORS response is valid (e.g if its a request with credentials)\n        assert!(\n            resp.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).is_some(),\n            \"expected cors request to be valid, but access-control-allow-origin is not set\"\n        )\n    }\n\n    // check allowed headers.\n    let allow_headers_val = resp\n        .headers\n        .get(ACCESS_CONTROL_ALLOW_HEADERS)\n        .expect(\"access-control-allow-headers to be present\")\n        .to_str()\n        .expect(\"convert to str\");\n    let allow_headers: HashSet<HeaderName> = allow_headers_val\n        .split(\",\")\n        .map(|val| HeaderName::from_bytes(val.trim().as_bytes()).expect(\"construct header name\"))\n        .collect();\n\n    if want_ok {\n        for val in allowed_headers {\n            assert!(\n                allow_headers.contains(val),\n                \"want header {val:?} to be allowed, got false; resp header={allow_headers_val}\"\n            )\n        }\n    } else {\n        assert_ne!(\n            allow_headers_val, \"\",\n            \"want headers not to be allowed, got {allow_headers_val}\"\n        );\n    }\n}\n\nstruct TestCase<'a> {\n    cors_cfg: pb::gateway::Cors,\n    creds_good_origins: &'a [HeaderValue],\n    creds_bad_origins: &'a [HeaderValue],\n    nocreds_good_origins: &'a [HeaderValue],\n    nocreds_bad_origins: &'a [HeaderValue],\n    good_headers: &'a [HeaderName],\n    bad_headers: &'a [HeaderName],\n}\n\nfn run_test_case(test_case: TestCase) {\n    let meta = MetaHeaders {\n        allow_headers: [HeaderName::from_static(\"x-static-test\")].into(),\n        expose_headers: [HeaderName::from_static(\"x-exposed-test\")].into(),\n    };\n\n    let cors = config(&test_case.cors_cfg, meta).expect(\"run cors config\");\n\n    check_origins(&cors, true, true, test_case.creds_good_origins);\n    check_origins(&cors, true, false, test_case.creds_bad_origins);\n\n    check_origins(&cors, false, true, test_case.nocreds_good_origins);\n    check_origins(&cors, false, false, test_case.nocreds_bad_origins);\n\n    check_headers(&cors, test_case.good_headers, true);\n\n    for header in test_case.bad_headers {\n        let mut test_headers = test_case.good_headers.to_vec();\n        test_headers.push(header.clone());\n\n        check_headers(&cors, &test_headers, false);\n    }\n}\n\n#[test]\nfn test_empty() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: None,\n            allowed_origins_without_credentials: None,\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"evil.com\"),\n            HeaderValue::from_static(\"localhost\"),\n        ],\n        nocreds_good_origins: &[\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"localhost\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"icanhazcheezburger.com\"),\n        ],\n        nocreds_bad_origins: &[],\n        good_headers: &[CONTENT_TYPE, ORIGIN],\n        bad_headers: &[\n            HeaderName::from_static(\"x-requested-with\"),\n            HeaderName::from_static(\"x-forwarded-for\"),\n        ],\n    });\n}\n\n#[test]\nfn test_allowed_creds() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: Some(\n                pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins(\n                    pb::gateway::CorsAllowedOrigins {\n                        allowed_origins: vec![String::from(\"localhost\"), String::from(\"ok.org\")],\n                    },\n                ),\n            ),\n            allowed_origins_without_credentials: None,\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[\n            HeaderValue::from_static(\"localhost\"),\n            HeaderValue::from_static(\"ok.org\"),\n        ],\n        creds_bad_origins: &[\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"evil.com\"),\n        ],\n        nocreds_good_origins: &[\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"localhost\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"icanhazcheezburger.com\"),\n            HeaderValue::from_static(\"ok.org\"),\n        ],\n        nocreds_bad_origins: &[],\n        good_headers: &[],\n        bad_headers: &[],\n    });\n}\n\n#[test]\nfn test_allowed_glob_creds() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: Some(\n                pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins(\n                    pb::gateway::CorsAllowedOrigins {\n                        allowed_origins: vec![\n                            String::from(\"https://*.example.com\"),\n                            String::from(\"wss://ok1-*.example.com\"),\n                            String::from(\"https://*-ok2.example.com\"),\n                        ],\n                    },\n                ),\n            ),\n            allowed_origins_without_credentials: None,\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[\n            HeaderValue::from_static(\"https://foo.example.com\"),\n            HeaderValue::from_static(\"wss://ok1-foo.example.com\"),\n            HeaderValue::from_static(\"https://foo-ok2.example.com\"),\n        ],\n        creds_bad_origins: &[\n            HeaderValue::from_static(\"http://foo.example.com\"), // Wrong scheme\n            HeaderValue::from_static(\"htts://.example.com\"),    // No subdomain\n            HeaderValue::from_static(\"ws://ok1-foo.example.com\"), // Wrong scheme\n            HeaderValue::from_static(\".example.com\"),           // No scheme\n            HeaderValue::from_static(\"https://evil.com\"),       // bad domain\n        ],\n        nocreds_good_origins: &[],\n        nocreds_bad_origins: &[],\n        good_headers: &[],\n        bad_headers: &[],\n    });\n}\n\n#[test]\nfn test_allowed_nocreds() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: None,\n            allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins {\n                allowed_origins: vec![String::from(\"localhost\"), String::from(\"ok.org\")],\n            }),\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[\n            HeaderValue::from_static(\"localhost\"),\n            HeaderValue::from_static(\"ok.org\"),\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"evil.com\"),\n        ],\n        nocreds_good_origins: &[\n            HeaderValue::from_static(\"localhost\"),\n            HeaderValue::from_static(\"ok.org\"),\n        ],\n        nocreds_bad_origins: &[\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"icanhazcheezburger.com\"),\n        ],\n        good_headers: &[],\n        bad_headers: &[],\n    });\n}\n\n#[test]\nfn test_allowed_disjoint_sets() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: Some(\n                pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins(\n                    pb::gateway::CorsAllowedOrigins {\n                        allowed_origins: vec![String::from(\"foo.com\")],\n                    },\n                ),\n            ),\n            allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins {\n                allowed_origins: vec![String::from(\"bar.org\")],\n            }),\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[HeaderValue::from_static(\"foo.com\")],\n        creds_bad_origins: &[\n            HeaderValue::from_static(\"bar.org\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"localhost\"),\n        ],\n        nocreds_good_origins: &[HeaderValue::from_static(\"bar.org\")],\n        nocreds_bad_origins: &[\n            HeaderValue::from_static(\"foo.com\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"localhost\"),\n        ],\n        good_headers: &[],\n        bad_headers: &[],\n    });\n}\n\n#[test]\nfn test_allowed_wildcard_without_creds() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: None,\n            allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins {\n                allowed_origins: vec![String::from(\"*\")],\n            }),\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[\n            HeaderValue::from_static(\"bar.org\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"localhost\"),\n        ],\n        nocreds_good_origins: &[\n            HeaderValue::from_static(\"bar.org\"),\n            HeaderValue::from_static(\"bar.com\"),\n            HeaderValue::from_static(\"\"),\n            HeaderValue::from_static(\"localhost\"),\n        ],\n        nocreds_bad_origins: &[],\n        good_headers: &[],\n        bad_headers: &[],\n    });\n}\n\n#[test]\nfn test_allowed_unsafe_wildcard_with_creds() {\n    run_test_case(TestCase {\n            cors_cfg: pb::gateway::Cors {\n                debug: false,\n                disable_credentials: false,\n                allowed_origins_with_credentials: Some(\n                    pb::gateway::cors::AllowedOriginsWithCredentials::UnsafeAllowAllOriginsWithCredentials(true),\n                ),\n                allowed_origins_without_credentials: None,\n                extra_allowed_headers: vec![],\n                extra_exposed_headers: vec![],\n                allow_private_network_access: false,\n            },\n            creds_good_origins: &[\n                HeaderValue::from_static(\"bar.org\"),\n                HeaderValue::from_static(\"bar.com\"),\n                HeaderValue::from_static(\"\"),\n                HeaderValue::from_static(\"localhost\"),\n                HeaderValue::from_static(\"unsafe.evil.com\"),\n            ],\n            creds_bad_origins: &[],\n            nocreds_good_origins: &[],\n            nocreds_bad_origins: &[],\n            good_headers: &[],\n            bad_headers: &[],\n        });\n}\n\n#[test]\nfn test_extra_headers() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: None,\n            allowed_origins_without_credentials: None,\n            extra_allowed_headers: vec![\n                \"Not-Authorization\".to_string(),\n                \"X-Forwarded-For\".to_string(),\n                \"X-Real-Ip\".to_string(),\n            ],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[],\n        nocreds_good_origins: &[],\n        nocreds_bad_origins: &[],\n        good_headers: &[\n            CONTENT_TYPE,\n            ORIGIN,\n            HeaderName::from_static(\"x-requested-with\"),\n            HeaderName::from_static(\"x-real-ip\"),\n            HeaderName::from_static(\"not-authorization\"),\n        ],\n        bad_headers: &[\n            HeaderName::from_static(\"x-forwarded-for\"),\n            HeaderName::from_static(\"x-evil-header\"),\n            HeaderName::from_static(\"authorization\"),\n        ],\n    });\n}\n\n#[test]\nfn test_extra_headers_wildcard() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: None,\n            allowed_origins_without_credentials: None,\n            extra_allowed_headers: vec![\n                \"X-Forwarded-For\".to_string(),\n                \"*\".to_string(),\n                \"X-Real-Ip\".to_string(),\n            ],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[],\n        nocreds_good_origins: &[],\n        nocreds_bad_origins: &[],\n        good_headers: &[\n            CONTENT_TYPE,\n            ORIGIN,\n            HeaderName::from_static(\"x-requested-with\"),\n            HeaderName::from_static(\"x-real-ip\"),\n            HeaderName::from_static(\"x-forwarded-for\"),\n            HeaderName::from_static(\"x-evil-header\"),\n            HeaderName::from_static(\"not-authorization\"),\n        ],\n        bad_headers: &[HeaderName::from_static(\"authorization\")],\n    });\n}\n\n#[test]\nfn test_static_headers() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: None,\n            allowed_origins_without_credentials: None,\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[],\n        nocreds_good_origins: &[],\n        nocreds_bad_origins: &[],\n        good_headers: &[\n            CONTENT_TYPE,\n            ORIGIN,\n            HeaderName::from_static(\"x-static-test\"),\n        ],\n        bad_headers: &[],\n    });\n}\n\n#[test]\nfn test_wildcard_without_creds() {\n    run_test_case(TestCase {\n        cors_cfg: pb::gateway::Cors {\n            debug: false,\n            disable_credentials: false,\n            allowed_origins_with_credentials: Some(\n                pb::gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins(\n                    pb::gateway::CorsAllowedOrigins {\n                        allowed_origins: vec![String::from(\"https://vercel.app\")],\n                    },\n                ),\n            ),\n            allowed_origins_without_credentials: Some(pb::gateway::CorsAllowedOrigins {\n                allowed_origins: vec![String::from(\"https://*-foo.vercel.app\")],\n            }),\n            extra_allowed_headers: vec![],\n            extra_exposed_headers: vec![],\n            allow_private_network_access: false,\n        },\n        creds_good_origins: &[],\n        creds_bad_origins: &[HeaderValue::from_static(\"https://blah-foo.vercel.app\")],\n        nocreds_good_origins: &[HeaderValue::from_static(\"https://blah-foo.vercel.app\")],\n        nocreds_bad_origins: &[],\n        good_headers: &[],\n        bad_headers: &[],\n    });\n}\n"
  },
  {
    "path": "runtimes/core/src/api/encore_routes/healthz.rs",
    "content": "use axum::extract::Request;\nuse axum::response::{IntoResponse, Json};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone)]\npub struct Handler {\n    pub app_revision: String,\n    pub deploy_id: String,\n}\n\nimpl Handler {\n    pub fn health_check(self) -> Response {\n        log::trace!(code = \"ok\"; \"handling incoming health check request\");\n        Response {\n            code: \"ok\".into(),\n            message: \"Your Encore app is up and running!\".into(),\n            details: Details {\n                app_revision: self.app_revision,\n                encore_compiler: \"\".into(),\n                deploy_id: self.deploy_id,\n                checks: vec![],\n                enabled_experiments: vec![],\n            },\n        }\n    }\n}\n\nimpl axum::handler::Handler<(), ()> for Handler {\n    type Future = std::pin::Pin<\n        Box<dyn std::future::Future<Output = axum::response::Response<axum::body::Body>> + Send>,\n    >;\n\n    fn call(self, _req: Request, _state: ()) -> Self::Future {\n        let resp = self.health_check();\n        Box::pin(async move { Json(resp).into_response() })\n    }\n}\n\n#[derive(Serialize, Deserialize)]\npub struct Response {\n    pub code: String,\n    pub message: String,\n    pub details: Details,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct Details {\n    pub app_revision: String,\n    pub encore_compiler: String,\n    pub deploy_id: String,\n    pub checks: Vec<CheckResult>,\n    pub enabled_experiments: Vec<String>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct CheckResult {\n    pub name: String,\n    pub passed: bool,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub error: Option<String>,\n}\n"
  },
  {
    "path": "runtimes/core/src/api/encore_routes/mod.rs",
    "content": "use axum::routing;\n\nuse crate::pubsub;\n\npub mod healthz;\n\npub struct Desc {\n    pub healthz: healthz::Handler,\n    pub push_registry: pubsub::PushHandlerRegistry,\n}\n\nimpl Desc {\n    pub fn router(self) -> axum::Router<()> {\n        axum::Router::new()\n            .route(\"/__encore/healthz\", routing::any(self.healthz))\n            .route(\n                \"/__encore/pubsub/push/:subscription_id\",\n                routing::any(self.push_registry),\n            )\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/endpoint.rs",
    "content": "use std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::{Arc, Mutex};\n\nuse anyhow::Context;\nuse axum::extract::{FromRequestParts, WebSocketUpgrade};\nuse axum::http::HeaderValue;\nuse axum::response::IntoResponse;\nuse bytes::{BufMut, BytesMut};\nuse http::HeaderMap;\nuse indexmap::IndexMap;\nuse percent_encoding::percent_decode_str;\nuse serde::Serialize;\n\nuse crate::api::reqauth::{platform, svcauth, CallMeta};\nuse crate::api::schema::encoding::{\n    handshake_encoding, request_encoding, response_encoding, HandshakeSchemaUnderConstruction,\n    ReqSchemaUnderConstruction, SchemaUnderConstruction,\n};\nuse crate::api::schema::{JSONPayload, Method};\nuse crate::api::{jsonschema, schema, ErrCode, Error};\nuse crate::encore::parser::meta::v1::rpc;\nuse crate::encore::parser::meta::v1::{self as meta, selector};\nuse crate::log::LogFromRust;\nuse crate::metrics::counter;\nuse crate::model::StreamDirection;\nuse crate::names::EndpointName;\nuse crate::trace;\nuse crate::{model, Hosted};\n\nuse super::pvalue::{PValue, PValues};\nuse super::reqauth::caller::Caller;\n\n/// Cached environment variable for whether to include error stacks in error response logs.\nstatic ENCORE_LOG_INCLUDE_ERROR_STACK: once_cell::sync::Lazy<bool> =\n    once_cell::sync::Lazy::new(|| {\n        std::env::var(\"ENCORE_LOG_INCLUDE_ERROR_STACK\")\n            .map(|v| !v.is_empty() && v != \"0\")\n            .unwrap_or(false)\n    });\n\n#[derive(Debug)]\npub struct SuccessResponse {\n    pub status: axum::http::StatusCode,\n    pub headers: axum::http::HeaderMap,\n    pub body: Option<PValue>,\n}\n\n/// Represents the result of calling an API endpoint.\npub type Response = APIResult<SuccessResponse>;\n\npub type APIResult<T> = Result<T, Error>;\n\nimpl IntoResponse for SuccessResponse {\n    fn into_response(self) -> axum::http::Response<axum::body::Body> {\n        // Serialize the response body.\n        // Use a small initial capacity of 128 bytes like serde_json::to_vec\n        // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189\n        let bld = {\n            let mut bld = axum::http::Response::builder();\n            *(bld.headers_mut().unwrap()) = self.headers;\n            bld\n        }\n        .status(self.status);\n\n        match self.body {\n            Some(body) => {\n                let bld = bld.header(\n                    axum::http::header::CONTENT_TYPE,\n                    HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),\n                );\n                let mut buf = BytesMut::with_capacity(128).writer();\n                match serde_json::to_writer(&mut buf, &body) {\n                    Ok(()) => bld\n                        .body(axum::body::Body::from(buf.into_inner().freeze()))\n                        .unwrap(),\n                    Err(err) => Error::internal(err).to_response(None),\n                }\n            }\n            None => bld.body(axum::body::Body::empty()).unwrap(),\n        }\n    }\n}\n\npub trait ToResponse {\n    fn to_response(&self, caller: Option<Caller>) -> axum::response::Response;\n}\n\nimpl ToResponse for Error {\n    fn to_response(&self, caller: Option<Caller>) -> axum::http::Response<axum::body::Body> {\n        // considure response to be external if caller is gateway, or if the caller is\n        // unknown\n        let internal_call = caller.map(|caller| !caller.is_gateway()).unwrap_or(false);\n\n        let mut buf = BytesMut::with_capacity(128).writer();\n\n        if internal_call {\n            serde_json::to_writer(&mut buf, &self).unwrap();\n        } else {\n            serde_json::to_writer(&mut buf, &self.as_external()).unwrap();\n        }\n\n        axum::http::Response::builder()\n            .status::<axum::http::status::StatusCode>(self.code.into())\n            .header(\n                axum::http::header::CONTENT_TYPE,\n                HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),\n            )\n            .body(axum::body::Body::from(buf.into_inner().freeze()))\n            .unwrap()\n    }\n}\n\npub type HandlerRequest = Arc<model::Request>;\npub type HandlerResponse = APIResult<HandlerResponseInner>;\n\npub struct HandlerResponseInner {\n    pub payload: JSONPayload,\n    pub extra_headers: Option<HeaderMap>,\n    pub status: Option<u16>,\n}\n\n/// A trait for handlers that accept a request and return a response.\npub trait TypedHandler: Send + Sync + 'static {\n    fn call(\n        self: Arc<Self>,\n        req: HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = HandlerResponse> + Send + 'static>>;\n}\n\n/// A trait for handlers that accept a request and return a response.\npub trait BoxedHandler: Send + Sync + 'static {\n    fn call(\n        self: Arc<Self>,\n        req: HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = ResponseData> + Send + 'static>>;\n}\n\npub enum ResponseData {\n    Typed(HandlerResponse),\n    Raw(axum::http::Response<axum::body::Body>),\n}\n\n/// Schema variations for stream handshake\n#[derive(Debug)]\npub enum HandshakeSchema {\n    // Handshake with only a path, no parseable data\n    Path(schema::Path),\n    // Handshake with a request schema\n    Request(schema::Request),\n}\n\nimpl HandshakeSchema {\n    pub fn path(&self) -> &schema::Path {\n        match self {\n            HandshakeSchema::Path(path) => path,\n            HandshakeSchema::Request(schema::Request { path, .. }) => path,\n        }\n    }\n}\n\n/// Represents a single API Endpoint.\n#[derive(Debug)]\npub struct Endpoint {\n    pub name: EndpointName,\n    pub path: meta::Path,\n    pub handshake: Option<Arc<HandshakeSchema>>,\n    pub request: Vec<Arc<schema::Request>>,\n    pub response: Arc<schema::Response>,\n\n    /// Whether this is a raw endpoint.\n    pub raw: bool,\n\n    /// Whether the service is exposed publicly.\n    pub exposed: bool,\n\n    /// Whether the service requires authentication data.\n    pub requires_auth: bool,\n\n    /// The maximum size of the request body.\n    /// If None, no limits are applied.\n    pub body_limit: Option<u64>,\n\n    /// The static assets to serve from this endpoint.\n    /// Set only for static asset endpoints.\n    pub static_assets: Option<meta::rpc::StaticAssets>,\n\n    /// The tags for this endpoint.\n    pub tags: Vec<String>,\n\n    /// Treat endpoint as sensitive and redact details from traces.\n    pub sensitive: bool,\n}\n\nimpl Endpoint {\n    pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {\n        self.request\n            .iter()\n            .flat_map(|schema| schema.methods.iter().copied())\n    }\n}\n\n#[derive(Debug, Serialize, Clone)]\npub struct RequestPayload {\n    #[serde(flatten)]\n    pub path: Option<IndexMap<String, PValue>>,\n\n    #[serde(flatten)]\n    pub query: Option<PValues>,\n\n    #[serde(flatten)]\n    pub header: Option<PValues>,\n\n    #[serde(flatten)]\n    pub cookie: Option<PValues>,\n\n    #[serde(flatten, skip_serializing_if = \"Body::is_raw\")]\n    pub body: Body,\n}\n\n#[derive(Debug, Serialize, Clone)]\n#[serde(untagged)]\npub enum Body {\n    Typed(Option<PValues>),\n    #[serde(skip)]\n    Raw(Arc<std::sync::Mutex<Option<axum::body::Body>>>),\n}\n\nimpl Body {\n    pub fn is_raw(&self) -> bool {\n        matches!(self, Body::Raw(_))\n    }\n}\n\n#[derive(Debug, Serialize, Clone)]\npub struct ResponsePayload {\n    #[serde(flatten)]\n    pub header: Option<PValues>,\n\n    #[serde(flatten)]\n    pub cookie: Option<PValues>,\n\n    #[serde(flatten, skip_serializing_if = \"Body::is_raw\")]\n    pub body: Body,\n}\n\npub type EndpointMap = HashMap<EndpointName, Arc<Endpoint>>;\n\n/// Compute a set of endpoint descriptions based on metadata\n/// and a list of which endpoints are hosted by this runtime.\npub fn endpoints_from_meta(\n    md: &meta::Data,\n    hosted_services: &Hosted,\n) -> anyhow::Result<(Arc<EndpointMap>, Vec<EndpointName>)> {\n    let mut registry_builder = jsonschema::Builder::new(md);\n\n    struct EndpointUnderConstruction<'a> {\n        svc: &'a meta::Service,\n        ep: &'a meta::Rpc,\n        handshake_schema: Option<HandshakeSchemaUnderConstruction>,\n        request_schemas: Vec<ReqSchemaUnderConstruction>,\n        response_schema: SchemaUnderConstruction,\n    }\n\n    // Compute the schemas for each endpoint.\n    let mut endpoints = Vec::new();\n    let mut hosted_endpoints = Vec::new();\n    for svc in &md.svcs {\n        let is_hosted = hosted_services.contains(&svc.name);\n        for ep in &svc.rpcs {\n            // If this endpoint is hosted, mark it as such.\n            if is_hosted {\n                hosted_endpoints.push(EndpointName::new(&svc.name, &ep.name));\n            }\n\n            let handshake_schema = handshake_encoding(&mut registry_builder, md, ep)?;\n            let request_schemas = request_encoding(&mut registry_builder, md, ep)?;\n            let response_schema = response_encoding(&mut registry_builder, md, ep)?;\n\n            endpoints.push(EndpointUnderConstruction {\n                svc,\n                ep,\n                handshake_schema,\n                request_schemas,\n                response_schema,\n            });\n        }\n    }\n\n    let registry = registry_builder.build();\n\n    let mut endpoint_map = EndpointMap::with_capacity(endpoints.len());\n\n    for ep in endpoints {\n        let mut request_schemas = Vec::with_capacity(ep.request_schemas.len());\n        let raw = rpc::Protocol::try_from(ep.ep.proto).is_ok_and(|p| p == rpc::Protocol::Raw);\n\n        let handshake_schema = ep\n            .handshake_schema\n            .map(|schema| schema.build(&registry))\n            .transpose()?;\n\n        let handshake = handshake_schema\n            .map(|handshake_schema| -> anyhow::Result<Arc<HandshakeSchema>> {\n                let path = handshake_schema\n                    .schema\n                    .path\n                    .context(\"endpoint must have a path defined\")?;\n\n                let handshake_schema = if handshake_schema.parse_data {\n                    let handshake_schema = schema::Request {\n                        methods: vec![],\n                        path,\n                        header: handshake_schema.schema.header,\n                        query: handshake_schema.schema.query,\n                        body: schema::RequestBody::Typed(None),\n                        cookie: handshake_schema.schema.cookie,\n                        stream: false,\n                    };\n\n                    HandshakeSchema::Request(handshake_schema)\n                } else {\n                    HandshakeSchema::Path(path)\n                };\n\n                Ok(Arc::new(handshake_schema))\n            })\n            .transpose()?;\n\n        for req_schema in ep.request_schemas {\n            let req_schema = req_schema.build(&registry)?;\n            let path = req_schema\n                .schema\n                .path\n                .or_else(|| handshake.as_ref().map(|hs| hs.path().clone()))\n                .context(\"endpoint must have path defined\")?;\n\n            request_schemas.push(Arc::new(schema::Request {\n                methods: req_schema.methods,\n                path,\n                header: req_schema.schema.header,\n                query: req_schema.schema.query,\n                cookie: req_schema.schema.cookie,\n                body: if raw {\n                    schema::RequestBody::Raw\n                } else {\n                    schema::RequestBody::Typed(req_schema.schema.body)\n                },\n                stream: ep.ep.streaming_request,\n            }));\n        }\n        let resp_schema = ep.response_schema.build(&registry)?;\n\n        // We only support a single gateway right now.\n        let exposed = ep.ep.expose.contains_key(\"api-gateway\");\n        let raw =\n            rpc::Protocol::try_from(ep.ep.proto).is_ok_and(|proto| proto == rpc::Protocol::Raw);\n\n        let tags = ep\n            .ep\n            .tags\n            .iter()\n            .filter(|item| item.r#type() == selector::Type::Tag)\n            .map(|item| item.value.clone())\n            .collect();\n\n        let endpoint = Endpoint {\n            name: EndpointName::new(ep.svc.name.clone(), ep.ep.name.clone()),\n            path: ep.ep.path.clone().unwrap_or_else(|| meta::Path {\n                r#type: meta::path::Type::Url as i32,\n                segments: vec![meta::PathSegment {\n                    r#type: meta::path_segment::SegmentType::Literal as i32,\n                    value_type: meta::path_segment::ParamType::String as i32,\n                    value: format!(\"/{}.{}\", ep.ep.service_name, ep.ep.name),\n                    validation: None,\n                }],\n            }),\n            handshake,\n            request: request_schemas,\n            response: Arc::new(schema::Response {\n                header: resp_schema.header,\n                cookie: resp_schema.cookie,\n                body: resp_schema.body,\n                http_status: resp_schema.http_status,\n                stream: ep.ep.streaming_response,\n            }),\n            raw,\n            exposed,\n            requires_auth: !ep.ep.allow_unauthenticated,\n            body_limit: ep.ep.body_limit,\n            static_assets: ep.ep.static_assets.clone(),\n            tags,\n            sensitive: ep.ep.sensitive,\n        };\n\n        endpoint_map.insert(\n            EndpointName::new(&ep.svc.name, &ep.ep.name),\n            Arc::new(endpoint),\n        );\n    }\n\n    Ok((Arc::new(endpoint_map), hosted_endpoints))\n}\n\npub(super) struct EndpointHandler {\n    pub endpoint: Arc<Endpoint>,\n    pub handler: Arc<dyn BoxedHandler>,\n    pub shared: Arc<SharedEndpointData>,\n    pub requests_total: counter::Schema<u64>,\n}\n\n#[derive(Debug)]\npub(super) struct SharedEndpointData {\n    pub tracer: trace::Tracer,\n    pub platform_auth: Arc<platform::RequestValidator>,\n    pub inbound_svc_auth: Vec<Arc<dyn svcauth::ServiceAuthMethod>>,\n\n    /// The schema to use when parsing auth data, if any.\n    /// NOTE: This assumes there's at most a single API Gateway.\n    /// When we support multiple this needs to be made into a map, and the\n    /// correct schema looked up based on the gateway being used.\n    pub auth_data_schemas: HashMap<String, Option<jsonschema::JSONSchema>>,\n}\n\nimpl Clone for EndpointHandler {\n    fn clone(&self) -> Self {\n        Self {\n            endpoint: self.endpoint.clone(),\n            handler: self.handler.clone(),\n            shared: self.shared.clone(),\n            requests_total: self.requests_total.clone(),\n        }\n    }\n}\n\nimpl EndpointHandler {\n    async fn parse_request(\n        &self,\n        axum_req: axum::extract::Request,\n    ) -> APIResult<Arc<model::Request>> {\n        let method = axum_req.method();\n        // Method conversion should never fail since we only register valid methods.\n        let api_method = Method::try_from(method.clone()).expect(\"invalid method\");\n\n        let req_schema = self\n            .endpoint\n            .request\n            .iter()\n            .find(|schema| schema.methods.contains(&api_method))\n            .expect(\"request schema must exist for all endpoints\");\n\n        let streaming_request = req_schema.stream;\n        let streaming_response = self.endpoint.response.stream;\n\n        let stream_direction = match (streaming_request, streaming_response) {\n            (true, true) => Some(StreamDirection::InOut),\n            (true, false) => Some(StreamDirection::In),\n            (false, true) => Some(StreamDirection::Out),\n            (false, false) => None,\n        };\n\n        let (mut parts, body) = axum_req\n            .map(|b| match self.endpoint.body_limit {\n                None => b,\n                Some(limit) => {\n                    axum::body::Body::new(http_body_util::Limited::new(b, limit as usize))\n                }\n            })\n            .into_parts();\n\n        // Authenticate the request from the platform, if applicable.\n        #[allow(clippy::manual_unwrap_or_default)]\n        let platform_seal_of_approval = match self.authenticate_platform(&parts) {\n            Ok(seal) => seal,\n            Err(_err) => None,\n        };\n\n        let meta = CallMeta::parse_with_caller(\n            &self.shared.inbound_svc_auth,\n            &parts.headers,\n            &self.shared.auth_data_schemas,\n        )?;\n\n        let parsed_payload = if let Some(handshake_schema) = &self.endpoint.handshake {\n            match handshake_schema.as_ref() {\n                HandshakeSchema::Request(req_schema) => {\n                    req_schema.extract(&mut parts, body).await?\n                }\n                HandshakeSchema::Path(_) => None,\n            }\n        } else {\n            req_schema.extract(&mut parts, body).await?\n        };\n\n        // Extract caller information.\n        let (internal_caller, auth_user_id, auth_data) = match meta.internal {\n            Some(internal) => (Some(internal.caller), internal.auth_uid, internal.auth_data),\n            None => (None, None, None),\n        };\n\n        let trace_id = meta.trace_id;\n        let span_id = meta.this_span_id.unwrap_or_else(model::SpanId::generate);\n        let span = trace_id.with_span(span_id);\n        let parent_span = meta.parent_span_id.map(|sp| trace_id.with_span(sp));\n\n        let traced = if platform_seal_of_approval.is_some() {\n            true\n        } else {\n            meta.trace_sampled\n                .unwrap_or_else(|| self.shared.tracer.should_sample(&self.endpoint.name))\n        };\n\n        let data = if let Some(direction) = stream_direction {\n            let websocket_upgrade = Mutex::new(Some(\n                WebSocketUpgrade::from_request_parts(&mut parts, &()).await?,\n            ));\n\n            model::RequestData::Stream(model::StreamRequestData {\n                endpoint: self.endpoint.clone(),\n                path: parts.uri.path().to_string(),\n                path_and_query: parts\n                    .uri\n                    .path_and_query()\n                    .map(|q| q.to_string())\n                    .unwrap_or_default(),\n                path_params: parsed_payload.as_ref().and_then(|p| p.path.clone()),\n                req_headers: parts.headers,\n                auth_user_id,\n                auth_data,\n                parsed_payload,\n                websocket_upgrade,\n                direction,\n            })\n        } else {\n            model::RequestData::RPC(model::RPCRequestData {\n                endpoint: self.endpoint.clone(),\n                method: api_method,\n                path: parts.uri.path().to_string(),\n                path_and_query: parts\n                    .uri\n                    .path_and_query()\n                    .map(|q| q.to_string())\n                    .unwrap_or_default(),\n                path_params: parsed_payload.as_ref().and_then(|p| p.path.clone()),\n                req_headers: parts.headers,\n                auth_user_id,\n                auth_data,\n                parsed_payload,\n            })\n        };\n\n        let request = Arc::new(model::Request {\n            span,\n            parent_trace: None,\n            parent_span,\n            caller_event_id: meta.parent_event_id,\n            ext_correlation_id: meta.ext_correlation_id,\n            start: tokio::time::Instant::now(),\n            start_time: std::time::SystemTime::now(),\n            is_platform_request: platform_seal_of_approval.is_some(),\n            internal_caller,\n            data,\n            traced,\n        });\n\n        Ok(request)\n    }\n\n    fn handle(\n        self,\n        axum_req: axum::extract::Request,\n    ) -> Pin<Box<dyn Future<Output = axum::http::Response<axum::body::Body>> + Send + 'static>>\n    {\n        Box::pin(async move {\n            let request = match self.parse_request(axum_req).await {\n                Ok(req) => req,\n                Err(err) => return err.to_response(None),\n            };\n\n            let internal_caller = request.internal_caller.clone();\n            let sensitive = self.endpoint.sensitive;\n\n            // If the endpoint isn't exposed, return a 404.\n            if !self.endpoint.exposed && !request.allows_private_endpoint_call() {\n                return Error {\n                    code: ErrCode::NotFound,\n                    message: \"endpoint not found\".into(),\n                    internal_message: Some(\"the endpoint was found, but is not exposed\".into()),\n                    stack: None,\n                    details: None,\n                }\n                .to_response(internal_caller);\n            } else if self.endpoint.requires_auth && !request.has_authenticated_user() {\n                return Error {\n                    code: ErrCode::Unauthenticated,\n                    message: \"endpoint requires auth but none provided\".into(),\n                    internal_message: None,\n                    stack: None,\n                    details: None,\n                }\n                .to_response(internal_caller);\n            }\n\n            let logger = crate::log::root();\n            logger.info(Some(&request), \"starting request\", None);\n\n            self.shared.tracer.request_span_start(&request, sensitive);\n\n            let resp: ResponseData = self.handler.call(request.clone()).await;\n\n            let duration = tokio::time::Instant::now().duration_since(request.start);\n\n            // If we had a request failure, log that separately.\n            if let ResponseData::Typed(Err(err)) = &resp {\n                logger.error(Some(&request), \"request failed\", Some(err), {\n                    let mut fields = crate::log::Fields::new();\n                    fields.insert(\n                        \"code\".into(),\n                        serde_json::Value::String(err.code.to_string()),\n                    );\n\n                    if let Some(internal_message) = &err.internal_message {\n                        fields.insert(\n                            \"internal_message\".into(),\n                            serde_json::Value::String(internal_message.clone()),\n                        );\n                    }\n\n                    if *ENCORE_LOG_INCLUDE_ERROR_STACK {\n                        if let Some(stack) = &err.stack {\n                            if let Ok(value) = serde_json::to_value(stack) {\n                                fields.insert(\"stack\".into(), value);\n                            }\n                        }\n                    }\n\n                    Some(fields)\n                });\n            }\n\n            let code = match &resp {\n                ResponseData::Typed(Ok(_)) => \"ok\".to_string(),\n                ResponseData::Typed(Err(err)) => err.code.to_string(),\n                ResponseData::Raw(resp) => ErrCode::from(resp.status()).to_string(),\n            };\n\n            logger.info(Some(&request), \"request completed\", {\n                let mut fields = crate::log::Fields::new();\n                let dur_ms = (duration.as_secs() as f64 * 1000f64)\n                    + (duration.subsec_nanos() as f64 / 1_000_000f64);\n\n                fields.insert(\n                    \"duration\".into(),\n                    serde_json::Value::Number(serde_json::Number::from_f64(dur_ms).unwrap_or_else(\n                        || {\n                            // Fall back to integer if the f64 conversion fails\n                            serde_json::Number::from(duration.as_millis() as u64)\n                        },\n                    )),\n                );\n\n                fields.insert(\"code\".into(), serde_json::Value::String(code.clone()));\n                Some(fields)\n            });\n\n            let (mut encoded_resp, resp_payload, extra_headers, error) = match resp {\n                ResponseData::Raw(resp) => (resp, None, None, None),\n                ResponseData::Typed(Ok(response)) => (\n                    self.endpoint\n                        .response\n                        .encode(&response.payload, response.status.unwrap_or(200))\n                        .unwrap_or_else(|err| err.to_response(internal_caller)),\n                    Some(response.payload),\n                    response.extra_headers,\n                    None,\n                ),\n                ResponseData::Typed(Err(err)) => (\n                    err.as_ref().to_response(internal_caller),\n                    None,\n                    None,\n                    Some(err),\n                ),\n            };\n\n            {\n                let model_resp = model::Response {\n                    request: request.clone(),\n                    duration,\n                    data: model::ResponseData::RPC(model::RPCResponseData {\n                        status_code: encoded_resp.status().as_u16(),\n                        resp_payload,\n                        error,\n                        resp_headers: encoded_resp.headers().clone(),\n                    }),\n                };\n                self.shared.tracer.request_span_end(&model_resp, sensitive);\n                self.requests_total.with([(\"code\", code)]).increment();\n            }\n\n            if let Ok(val) = HeaderValue::from_str(request.span.0.serialize_encore().as_str()) {\n                encoded_resp.headers_mut().insert(\"x-encore-trace-id\", val);\n            }\n\n            if let Some(extra_headers) = extra_headers {\n                encoded_resp.headers_mut().extend(extra_headers)\n            }\n\n            encoded_resp\n        })\n    }\n\n    fn authenticate_platform(\n        &self,\n        req: &axum::http::request::Parts,\n    ) -> Result<Option<platform::SealOfApproval>, platform::ValidationError> {\n        let Some(x_encore_auth_header) = req.headers.get(\"x-encore-auth\") else {\n            return Ok(None);\n        };\n        let x_encore_auth_header = x_encore_auth_header\n            .to_str()\n            .map_err(|_| platform::ValidationError::InvalidMac)?;\n\n        let Some(date_header) = req.headers.get(\"Date\") else {\n            return Err(platform::ValidationError::InvalidDateHeader);\n        };\n        let date_header = date_header\n            .to_str()\n            .map_err(|_| platform::ValidationError::InvalidDateHeader)?;\n\n        let request_path = percent_decode_str(req.uri.path()).decode_utf8_lossy();\n        let req = platform::ValidationData {\n            request_path: &request_path,\n            date_header,\n            x_encore_auth_header,\n        };\n\n        self.shared\n            .platform_auth\n            .validate_platform_request(&req)\n            .map(Some)\n    }\n}\n\nimpl axum::handler::Handler<(), ()> for EndpointHandler {\n    type Future =\n        Pin<Box<dyn Future<Output = axum::http::Response<axum::body::Body>> + Send + 'static>>;\n\n    fn call(self, axum_req: axum::extract::Request, _state: ()) -> Self::Future {\n        self.handle(axum_req)\n    }\n}\n\npub fn path_supports_tsr(path: &str) -> bool {\n    path != \"/\" && !path.ends_with('/') && !path.contains(\"/*\")\n}\n"
  },
  {
    "path": "runtimes/core/src/api/error.rs",
    "content": "use std::fmt::{Debug, Display};\nuse std::str::FromStr;\n\nuse crate::api::jsonschema;\nuse crate::error::{AppError, StackTrace};\nuse axum::extract::ws::rejection::WebSocketUpgradeRejection;\nuse serde::ser::SerializeStruct;\nuse serde::{Deserialize, Deserializer, Serialize};\nuse serde_with::{DeserializeFromStr, SerializeDisplay};\n\nuse super::jsonschema::JSONSchema;\nuse super::PValues;\n\n/// Represents an API Error.\n#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct Error {\n    pub code: ErrCode,\n    pub message: String,\n    pub internal_message: Option<String>,\n    #[serde(deserialize_with = \"deserialize_details_as_any\")]\n    pub details: ErrDetails,\n\n    #[serde(skip_serializing)]\n    pub stack: Option<StackTrace>,\n}\n\npub type ErrDetails = Option<Box<PValues>>;\n\n// We don't have a schema for error details, so we do best effort to deserialize them.\n// Special types like Date will be lost, as we can't know if its a Date or a string.\nfn deserialize_details_as_any<'de, D>(deserializer: D) -> Result<ErrDetails, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    Ok(JSONSchema::any()\n        .deserialize(deserializer, jsonschema::DecodeConfig::default())\n        .map(|pvalues| Some(Box::new(pvalues)))\n        .unwrap_or(None))\n}\n\n/// ErrorExternal hides internal information on `Error` when it serializes\n#[derive(Debug)]\npub struct ExternalError<'a>(&'a Error);\n\n/// Cached environment variable for whether to include internal message in all errors.\nstatic ENCORE_API_INCLUDE_INTERNAL_MESSAGE: once_cell::sync::Lazy<bool> =\n    once_cell::sync::Lazy::new(|| {\n        std::env::var(\"ENCORE_API_INCLUDE_INTERNAL_MESSAGE\")\n            .map(|v| !v.is_empty() && v != \"0\")\n            .unwrap_or(false)\n    });\n\n/// Cached environment variable for whether to include the stack in error responses.\nstatic ENCORE_API_INCLUDE_ERROR_STACK: once_cell::sync::Lazy<bool> =\n    once_cell::sync::Lazy::new(|| {\n        std::env::var(\"ENCORE_API_INCLUDE_ERROR_STACK\")\n            .map(|v| !v.is_empty() && v != \"0\")\n            .unwrap_or(false)\n    });\n\nimpl Serialize for ExternalError<'_> {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: serde::Serializer,\n    {\n        let mut error = serializer.serialize_struct(\"Error\", 3)?;\n        error.serialize_field(\"code\", &self.0.code)?;\n        error.serialize_field(\"message\", &self.0.message)?;\n        error.serialize_field(\"details\", &self.0.details)?;\n\n        // Include internal message even in external errors iff the environment variable is set.\n        if *ENCORE_API_INCLUDE_INTERNAL_MESSAGE {\n            error.serialize_field(\"internal_message\", &self.0.internal_message)?;\n        }\n\n        // Do the same for stack traces.\n        if *ENCORE_API_INCLUDE_ERROR_STACK {\n            error.serialize_field(\"stack\", &self.0.stack)?;\n        }\n\n        error.end()\n    }\n}\n\nimpl Error {\n    pub fn as_external(&self) -> ExternalError<'_> {\n        ExternalError(self)\n    }\n\n    pub fn internal<E>(cause: E) -> Self\n    where\n        E: Into<anyhow::Error>,\n    {\n        Self {\n            code: ErrCode::Internal,\n            message: ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(format!(\"{:#?}\", cause.into())),\n            stack: None,\n            details: None,\n        }\n    }\n\n    pub fn invalid_argument<S, E>(public_msg: S, cause: E) -> Self\n    where\n        S: Into<String>,\n        E: Into<anyhow::Error>,\n    {\n        Self {\n            code: ErrCode::InvalidArgument,\n            message: format!(\"{}: {:?}\", public_msg.into(), cause.into()),\n            internal_message: None,\n            stack: None,\n            details: None,\n        }\n    }\n\n    pub fn not_found<S>(public_msg: S) -> Self\n    where\n        S: Into<String>,\n    {\n        Self {\n            code: ErrCode::NotFound,\n            message: public_msg.into(),\n            internal_message: None,\n            stack: None,\n            details: None,\n        }\n    }\n\n    pub fn unauthenticated() -> Self {\n        Self {\n            code: ErrCode::Unauthenticated,\n            message: ErrCode::Unauthenticated.default_public_message().into(),\n            internal_message: None,\n            stack: None,\n            details: None,\n        }\n    }\n}\n\nimpl From<WebSocketUpgradeRejection> for Error {\n    fn from(value: WebSocketUpgradeRejection) -> Self {\n        Error {\n            code: value.status().into(),\n            message: value.body_text(),\n            internal_message: Some(value.body_text()),\n            stack: None,\n            details: None,\n        }\n    }\n}\n\nimpl From<Error> for AppError {\n    fn from(val: Error) -> Self {\n        let message = match val.internal_message {\n            Some(ref internal_msg) => format!(\"{}: {}\", val.message, internal_msg),\n            None => val.message,\n        };\n        AppError::new(message)\n    }\n}\n\nimpl From<&Error> for AppError {\n    fn from(val: &Error) -> Self {\n        let message = match val.internal_message {\n            Some(ref internal_msg) => format!(\"{}: {}\", val.message, internal_msg),\n            None => val.message.clone(),\n        };\n\n        // TODO: capture the JS stack trace for this error\n        AppError {\n            message,\n            stack: vec![],\n            cause: None,\n        }\n    }\n}\n\nimpl From<Error> for Box<pingora::Error> {\n    fn from(err: Error) -> Self {\n        pingora::Error::because(\n            pingora::ErrorType::HTTPStatus(err.code.status_code().into()),\n            err.code.to_string(),\n            err,\n        )\n    }\n}\n\nimpl std::error::Error for Error {}\n\nimpl Display for Error {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match &self.internal_message {\n            Some(msg) => write!(f, \"{msg}\"),\n            None => write!(f, \"{}\", self.message),\n        }\n    }\n}\n\nimpl AsRef<Error> for Error {\n    fn as_ref(&self) -> &Self {\n        self\n    }\n}\n\n/// Represents an API Error.\n#[derive(SerializeDisplay, DeserializeFromStr, Debug, Copy, Clone, PartialEq, Eq)]\npub enum ErrCode {\n    /// Canceled indicates the operation was canceled (typically by the caller).\n    ///\n    /// Encore will generate this error code when cancellation is requested.\n    Canceled,\n\n    /// Unknown error. An example of where this error may be returned is\n    /// if a Status value received from another address space belongs to\n    /// an error-space that is not known in this address space. Also\n    /// errors raised by APIs that do not return enough error information\n    /// may be converted to this error.\n    ///\n    /// Encore will generate this error code in the above two mentioned cases.\n    Unknown,\n\n    /// InvalidArgument indicates client specified an invalid argument.\n    /// Note that this differs from FailedPrecondition. It indicates arguments\n    /// that are problematic regardless of the state of the system\n    /// (e.g., a malformed file name).\n    ///\n    /// Encore will generate this error code if the request data cannot be parsed.\n    InvalidArgument,\n\n    /// DeadlineExceeded means operation expired before completion.\n    /// For operations that change the state of the system, this error may be\n    /// returned even if the operation has completed successfully. For\n    /// example, a successful response from a server could have been delayed\n    /// long enough for the deadline to expire.\n    ///\n    /// Encore will generate this error code when the deadline is exceeded.\n    DeadlineExceeded,\n\n    /// NotFound means some requested entity (e.g., file or directory) was\n    /// not found.\n    ///\n    /// Encore will not generate this error code.\n    NotFound,\n\n    /// AlreadyExists means an attempt to create an entity failed because one\n    /// already exists.\n    ///\n    /// Encore will not generate this error code.\n    AlreadyExists,\n\n    /// PermissionDenied indicates the caller does not have permission to\n    /// execute the specified operation. It must not be used for rejections\n    /// caused by exhausting some resource (use ResourceExhausted\n    /// instead for those errors). It must not be\n    /// used if the caller cannot be identified (use Unauthenticated\n    /// instead for those errors).\n    ///\n    /// Encore will not generate this error code.\n    PermissionDenied,\n\n    /// ResourceExhausted indicates some resource has been exhausted, perhaps\n    /// a per-user quota, or perhaps the entire file system is out of space.\n    ///\n    /// Encore will generate this error code in out-of-memory and server overload\n    /// situations, or when a message is larger than the configured maximum size.\n    ResourceExhausted,\n\n    /// FailedPrecondition indicates operation was rejected because the\n    /// system is not in a state required for the operation's execution.\n    /// For example, directory to be deleted may be non-empty, an rmdir\n    /// operation is applied to a non-directory, etc.\n    ///\n    /// A litmus test that may help a service implementor in deciding\n    /// between FailedPrecondition, Aborted, and Unavailable:\n    ///  (a) Use Unavailable if the client can retry just the failing call.\n    ///  (b) Use Aborted if the client should retry at a higher-level\n    ///      (e.g., restarting a read-modify-write sequence).\n    ///  (c) Use FailedPrecondition if the client should not retry until\n    ///      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n    ///      fails because the directory is non-empty, FailedPrecondition\n    ///      should be returned since the client should not retry unless\n    ///      they have first fixed up the directory by deleting files from it.\n    ///  (d) Use FailedPrecondition if the client performs conditional\n    ///      Get/Update/Delete on a resource and the resource on the\n    ///      server does not match the condition. E.g., conflicting\n    ///      read-modify-write on the same resource.\n    ///\n    /// Encore will not generate this error code.\n    FailedPrecondition,\n\n    /// Aborted indicates the operation was aborted, typically due to a\n    /// concurrency issue like sequencer check failures, transaction aborts,\n    /// etc.\n    ///\n    /// See litmus test above for deciding between FailedPrecondition,\n    /// Aborted, and Unavailable.\n    Aborted,\n\n    /// OutOfRange means operation was attempted past the valid range.\n    /// E.g., seeking or reading past end of file.\n    ///\n    /// Unlike InvalidArgument, this error indicates a problem that may\n    /// be fixed if the system state changes. For example, a 32-bit file\n    /// system will generate InvalidArgument if asked to read at an\n    /// offset that is not in the range [0,2^32-1], but it will generate\n    /// OutOfRange if asked to read from an offset past the current\n    /// file size.\n    ///\n    /// There is a fair bit of overlap between FailedPrecondition and\n    /// OutOfRange. We recommend using OutOfRange (the more specific\n    /// error) when it applies so that callers who are iterating through\n    /// a space can easily look for an OutOfRange error to detect when\n    /// they are done.\n    ///\n    /// Encore will not generate this error code.\n    OutOfRange,\n\n    /// Unimplemented indicates operation is not implemented or not\n    /// supported/enabled in this service.\n    ///\n    /// Encore will generate this error code when an endpoint does not exist.\n    Unimplemented,\n\n    /// Internal errors. Means some invariants expected by underlying\n    /// system has been broken. If you see one of these errors,\n    /// something is very broken.\n    ///\n    /// Encore will generate this error code in several internal error conditions.\n    Internal,\n\n    /// Unavailable indicates the service is currently unavailable.\n    /// This is a most likely a transient condition and may be corrected\n    /// by retrying with a backoff. Note that it is not always safe to retry\n    /// non-idempotent operations.\n    ///\n    /// See litmus test above for deciding between FailedPrecondition,\n    /// Aborted, and Unavailable.\n    ///\n    /// Encore will generate this error code in aubrupt shutdown of a server process\n    /// or network connection.\n    Unavailable,\n\n    /// DataLoss indicates unrecoverable data loss or corruption.\n    ///\n    /// Encore will not generate this error code.\n    DataLoss,\n\n    /// Unauthenticated indicates the request does not have valid\n    /// authentication credentials for the operation.\n    ///\n    /// Encore will generate this error code when the authentication metadata\n    /// is invalid or missing, and expects auth handlers to return errors with\n    /// this code when the auth token is not valid.\n    Unauthenticated,\n}\n\nimpl ErrCode {\n    pub fn default_public_message(&self) -> &'static str {\n        match self {\n            ErrCode::Canceled => \"the operation was canceled\",\n            ErrCode::Unknown => \"an unknown error occurred\",\n            ErrCode::InvalidArgument => \"the request is invalid\",\n            ErrCode::DeadlineExceeded => \"the operation timed out\",\n            ErrCode::NotFound => \"the requested resource was not found\",\n            ErrCode::AlreadyExists => \"the resource already exists\",\n            ErrCode::PermissionDenied => \"the caller does not have permission to execute the specified operation\",\n            ErrCode::ResourceExhausted => \"the resource has been exhausted\",\n            ErrCode::FailedPrecondition => \"the operation was rejected because the system is not in a state required for the operation's execution\",\n            ErrCode::Aborted => \"the operation was aborted\",\n            ErrCode::OutOfRange => \"the operation was attempted past the valid range\",\n            ErrCode::Unimplemented => \"the operation is not implemented or not supported/enabled in this service\",\n            ErrCode::Internal => \"an internal error occurred\",\n            ErrCode::Unavailable => \"the service is currently unavailable\",\n            ErrCode::DataLoss => \"unrecoverable data loss or corruption occurred\",\n            ErrCode::Unauthenticated => \"the request does not have valid authentication credentials for the operation\",\n        }\n    }\n\n    /// Converts the error code to the trace protocol byte value.\n    pub fn to_trace_code(&self) -> u8 {\n        match self {\n            ErrCode::Canceled => 1,\n            ErrCode::Unknown => 2,\n            ErrCode::InvalidArgument => 3,\n            ErrCode::DeadlineExceeded => 4,\n            ErrCode::NotFound => 5,\n            ErrCode::AlreadyExists => 6,\n            ErrCode::PermissionDenied => 7,\n            ErrCode::ResourceExhausted => 8,\n            ErrCode::FailedPrecondition => 9,\n            ErrCode::Aborted => 10,\n            ErrCode::OutOfRange => 11,\n            ErrCode::Unimplemented => 12,\n            ErrCode::Internal => 13,\n            ErrCode::Unavailable => 14,\n            ErrCode::DataLoss => 15,\n            ErrCode::Unauthenticated => 16,\n        }\n    }\n\n    pub fn status_code(&self) -> axum::http::StatusCode {\n        match self {\n            ErrCode::Canceled => axum::http::StatusCode::from_u16(499).unwrap(),\n            ErrCode::Unknown => axum::http::StatusCode::INTERNAL_SERVER_ERROR,\n            ErrCode::InvalidArgument => axum::http::StatusCode::BAD_REQUEST,\n            ErrCode::DeadlineExceeded => axum::http::StatusCode::GATEWAY_TIMEOUT,\n            ErrCode::NotFound => axum::http::StatusCode::NOT_FOUND,\n            ErrCode::AlreadyExists => axum::http::StatusCode::CONFLICT,\n            ErrCode::PermissionDenied => axum::http::StatusCode::FORBIDDEN,\n            ErrCode::ResourceExhausted => axum::http::StatusCode::TOO_MANY_REQUESTS,\n            ErrCode::FailedPrecondition => axum::http::StatusCode::BAD_REQUEST,\n            ErrCode::Aborted => axum::http::StatusCode::CONFLICT,\n            ErrCode::OutOfRange => axum::http::StatusCode::BAD_REQUEST,\n            ErrCode::Unimplemented => axum::http::StatusCode::NOT_IMPLEMENTED,\n            ErrCode::Internal => axum::http::StatusCode::INTERNAL_SERVER_ERROR,\n            ErrCode::Unavailable => axum::http::StatusCode::SERVICE_UNAVAILABLE,\n            ErrCode::DataLoss => axum::http::StatusCode::INTERNAL_SERVER_ERROR,\n            ErrCode::Unauthenticated => axum::http::StatusCode::UNAUTHORIZED,\n        }\n    }\n}\n\nimpl Display for ErrCode {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ErrCode::Canceled => write!(f, \"canceled\"),\n            ErrCode::Unknown => write!(f, \"unknown\"),\n            ErrCode::InvalidArgument => write!(f, \"invalid_argument\"),\n            ErrCode::DeadlineExceeded => write!(f, \"deadline_exceeded\"),\n            ErrCode::NotFound => write!(f, \"not_found\"),\n            ErrCode::AlreadyExists => write!(f, \"already_exists\"),\n            ErrCode::PermissionDenied => write!(f, \"permission_denied\"),\n            ErrCode::ResourceExhausted => write!(f, \"resource_exhausted\"),\n            ErrCode::FailedPrecondition => write!(f, \"failed_precondition\"),\n            ErrCode::Aborted => write!(f, \"aborted\"),\n            ErrCode::OutOfRange => write!(f, \"out_of_range\"),\n            ErrCode::Unimplemented => write!(f, \"unimplemented\"),\n            ErrCode::Internal => write!(f, \"internal\"),\n            ErrCode::Unavailable => write!(f, \"unavailable\"),\n            ErrCode::DataLoss => write!(f, \"data_loss\"),\n            ErrCode::Unauthenticated => write!(f, \"unauthenticated\"),\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct UnknownErrCode {\n    pub code: String,\n}\n\nimpl Display for UnknownErrCode {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{} (unknown)\", self.code)\n    }\n}\n\nimpl std::error::Error for UnknownErrCode {}\n\nimpl FromStr for ErrCode {\n    type Err = UnknownErrCode;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s {\n            \"canceled\" => Ok(ErrCode::Canceled),\n            \"unknown\" => Ok(ErrCode::Unknown),\n            \"invalid_argument\" => Ok(ErrCode::InvalidArgument),\n            \"deadline_exceeded\" => Ok(ErrCode::DeadlineExceeded),\n            \"not_found\" => Ok(ErrCode::NotFound),\n            \"already_exists\" => Ok(ErrCode::AlreadyExists),\n            \"permission_denied\" => Ok(ErrCode::PermissionDenied),\n            \"resource_exhausted\" => Ok(ErrCode::ResourceExhausted),\n            \"failed_precondition\" => Ok(ErrCode::FailedPrecondition),\n            \"aborted\" => Ok(ErrCode::Aborted),\n            \"out_of_range\" => Ok(ErrCode::OutOfRange),\n            \"unimplemented\" => Ok(ErrCode::Unimplemented),\n            \"internal\" => Ok(ErrCode::Internal),\n            \"unavailable\" => Ok(ErrCode::Unavailable),\n            \"data_loss\" => Ok(ErrCode::DataLoss),\n            \"unauthenticated\" => Ok(ErrCode::Unauthenticated),\n            other => Err(UnknownErrCode {\n                code: other.to_owned(),\n            }),\n        }\n    }\n}\n\nimpl From<ErrCode> for axum::http::status::StatusCode {\n    fn from(val: ErrCode) -> Self {\n        val.status_code()\n    }\n}\n\nimpl From<axum::http::status::StatusCode> for ErrCode {\n    fn from(status: axum::http::status::StatusCode) -> Self {\n        match status.as_u16() {\n            400 => ErrCode::InvalidArgument,\n            401 => ErrCode::Unauthenticated,\n            403 => ErrCode::PermissionDenied,\n            404 => ErrCode::NotFound,\n            409 => ErrCode::AlreadyExists,\n            429 => ErrCode::ResourceExhausted,\n            499 => ErrCode::Canceled,\n            500 => ErrCode::Internal,\n            501 => ErrCode::Unimplemented,\n            503 => ErrCode::Unavailable,\n            504 => ErrCode::DeadlineExceeded,\n            _ => ErrCode::Unknown,\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/gateway/mod.rs",
    "content": "mod router;\nmod websocket;\n\nuse std::borrow::Cow;\nuse std::collections::HashMap;\nuse std::net::SocketAddr;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse axum::async_trait;\nuse bytes::{BufMut, Bytes, BytesMut};\nuse http::uri::Scheme;\nuse http::HeaderValue;\nuse hyper::header;\nuse pingora::http::{RequestHeader, ResponseHeader};\nuse pingora::protocols::http::error_resp;\nuse pingora::proxy::{http_proxy_service, FailToProxy, ProxyHttp, Session};\nuse pingora::server::configuration::{Opt, ServerConf};\nuse pingora::services::Service;\nuse pingora::upstreams::peer::HttpPeer;\nuse pingora::{Error, ErrorSource, ErrorType, OkOrErr, OrErr};\nuse router::Target;\nuse tokio::sync::watch;\nuse url::Url;\n\nuse crate::api::auth;\nuse crate::api::call::{CallDesc, ServiceRegistry};\nuse crate::api::paths::PathSet;\nuse crate::api::reqauth::caller::Caller;\nuse crate::api::reqauth::meta::{MetaKey, MetaMap};\nuse crate::api::reqauth::{svcauth, CallMeta};\nuse crate::{api, model, trace, EncoreName};\n\nuse super::cors::cors_headers_config::CorsHeadersConfig;\nuse super::encore_routes::healthz;\n\n#[derive(Clone)]\npub struct Gateway {\n    inner: Arc<Inner>,\n}\n\nstruct Inner {\n    shared: Arc<SharedGatewayData>,\n    service_registry: Arc<ServiceRegistry>,\n    router: router::Router,\n    cors_config: CorsHeadersConfig,\n    healthz: healthz::Handler,\n    own_api_address: Option<SocketAddr>,\n    proxied_push_subs: HashMap<String, ProxiedPushSub>,\n}\n\n/// A push subscription that the gateway proxies to another service.\n#[derive(Clone, Debug)]\npub struct ProxiedPushSub {\n    pub service_name: EncoreName,\n    pub topic: EncoreName,\n    pub subscription: EncoreName,\n}\n\npub struct GatewayCtx {\n    upstream_service_name: EncoreName,\n    upstream_sampling_target: router::SamplingTarget,\n    upstream_base_path: String,\n    upstream_host: Option<String>,\n    upstream_require_auth: bool,\n    trace_id: Option<model::TraceId>,\n}\n\nimpl GatewayCtx {\n    fn prepend_base_path(&self, uri: &http::Uri) -> anyhow::Result<http::Uri> {\n        let mut builder = http::Uri::builder();\n        if let Some(scheme) = uri.scheme() {\n            builder = builder.scheme(scheme.clone());\n        }\n        if let Some(authority) = uri.authority() {\n            builder = builder.authority(authority.clone());\n        }\n\n        let base_path = self.upstream_base_path.trim_end_matches('/');\n        builder = builder.path_and_query(format!(\n            \"{}{}\",\n            base_path,\n            uri.path_and_query().map_or(\"\", |pq| pq.as_str())\n        ));\n\n        builder.build().context(\"failed to build uri\")\n    }\n}\n\nimpl Gateway {\n    #[allow(clippy::too_many_arguments)]\n    pub fn new(\n        name: EncoreName,\n        service_registry: Arc<ServiceRegistry>,\n        service_routes: PathSet<EncoreName, Arc<api::Endpoint>>,\n        auth_handler: Option<auth::Authenticator>,\n        cors_config: CorsHeadersConfig,\n        healthz: healthz::Handler,\n        own_api_address: Option<SocketAddr>,\n        proxied_push_subs: HashMap<String, ProxiedPushSub>,\n        tracer: trace::Tracer,\n        inbound_svc_auth: Vec<Arc<dyn svcauth::ServiceAuthMethod>>,\n    ) -> anyhow::Result<Self> {\n        // Filter out noop auth methods since they provide no actual\n        // authentication guarantees for verifying internal callers.\n        let authenticated_inbound_svc_auth = inbound_svc_auth\n            .into_iter()\n            .filter(|a| a.name() != \"noop\")\n            .collect();\n\n        let shared = Arc::new(SharedGatewayData {\n            name,\n            auth: auth_handler,\n            tracer,\n            authenticated_inbound_svc_auth,\n        });\n\n        let mut router = router::Router::new();\n        router.add_routes(&service_routes)?;\n\n        Ok(Gateway {\n            inner: Arc::new(Inner {\n                shared,\n                service_registry,\n                router,\n                cors_config,\n                healthz,\n                own_api_address,\n                proxied_push_subs,\n            }),\n        })\n    }\n\n    pub fn auth_handler(&self) -> Option<&auth::Authenticator> {\n        self.inner.shared.auth.as_ref()\n    }\n\n    pub async fn serve(self, listen_addr: &str) -> anyhow::Result<()> {\n        let conf = Arc::new(\n            ServerConf::new_with_opt_override(&Opt {\n                upgrade: false,\n                daemon: false,\n                nocapture: false,\n                test: false,\n                conf: None,\n            })\n            .unwrap(),\n        );\n        let mut proxy = http_proxy_service(&conf, self);\n\n        proxy.add_tcp(listen_addr);\n\n        let (_tx, rx) = watch::channel(false);\n        proxy\n            .start_service(\n                #[cfg(unix)]\n                None,\n                rx,\n                1, // listeners_per_fd\n            )\n            .await;\n\n        Ok(())\n    }\n}\n\n#[async_trait]\nimpl ProxyHttp for Gateway {\n    type CTX = Option<GatewayCtx>;\n\n    fn new_ctx(&self) -> Self::CTX {\n        None\n    }\n\n    // see https://github.com/cloudflare/pingora/blob/main/docs/user_guide/internals.md for\n    // details on when different filters are called.\n\n    async fn request_filter(\n        &self,\n        session: &mut Session,\n        _ctx: &mut Self::CTX,\n    ) -> pingora::Result<bool>\n    where\n        Self::CTX: Send + Sync,\n    {\n        if session.req_header().uri.path() == \"/__encore/healthz\" {\n            let healthz_resp = self.inner.healthz.clone().health_check();\n            let healthz_bytes: Vec<u8> = serde_json::to_vec(&healthz_resp)\n                .or_err(ErrorType::HTTPStatus(500), \"could not encode response\")?;\n\n            let mut header = ResponseHeader::build(200, None)?;\n            header.insert_header(header::CONTENT_LENGTH, healthz_bytes.len())?;\n            header.insert_header(header::CONTENT_TYPE, \"application/json\")?;\n            session\n                .write_response_header(Box::new(header), false)\n                .await?;\n            session\n                .write_response_body(Some(Bytes::from(healthz_bytes)), true)\n                .await?;\n\n            return Ok(true);\n        }\n\n        // preflight request, return early with cors headers\n        if axum::http::Method::OPTIONS == session.req_header().method {\n            let mut resp = ResponseHeader::build(200, None)?;\n            self.inner\n                .cors_config\n                .apply(session.req_header(), &mut resp)?;\n            resp.insert_header(header::CONTENT_LENGTH, 0)?;\n            session.write_response_header(Box::new(resp), true).await?;\n\n            return Ok(true);\n        }\n\n        Ok(false)\n    }\n\n    async fn upstream_peer(\n        &self,\n        session: &mut Session,\n        ctx: &mut Self::CTX,\n    ) -> pingora::Result<Box<HttpPeer>> {\n        let path = session.req_header().uri.path();\n\n        // Check if this is a pubsub push request and if we need to proxy it to another service\n        let push_proxy_svc = path\n            .strip_prefix(\"/__encore/pubsub/push/\")\n            .and_then(|sub_id| self.inner.proxied_push_subs.get(sub_id))\n            .map(|sub| Target {\n                service_name: sub.service_name.clone(),\n                sampling_target: router::SamplingTarget::PubSub,\n                requires_auth: false,\n            });\n\n        if let Some(own_api_addr) = &self.inner.own_api_address {\n            if push_proxy_svc.is_none() && path.starts_with(\"/__encore/\") {\n                return Ok(Box::new(HttpPeer::new(own_api_addr, false, \"\".to_string())));\n            }\n        }\n        let target = push_proxy_svc.map_or_else(\n            || {\n                // Find which service handles the path route\n                session\n                    .req_header()\n                    .method\n                    .as_ref()\n                    .try_into()\n                    .map_err(|e: anyhow::Error| api::Error {\n                        code: api::ErrCode::InvalidArgument,\n                        message: \"invalid method\".to_string(),\n                        internal_message: Some(e.to_string()),\n                        stack: None,\n                        details: None,\n                    })\n                    .and_then(|method| self.inner.router.route_to_service(method, path))\n                    .cloned()\n            },\n            Ok,\n        )?;\n\n        let upstream = self\n            .inner\n            .service_registry\n            .service_base_url(&target.service_name)\n            .or_err(ErrorType::InternalError, \"couldn't find upstream\")?;\n\n        let upstream_url: Url = upstream\n            .parse()\n            .or_err(ErrorType::InternalError, \"upstream not a valid url\")?;\n\n        let upstream_addrs = upstream_url\n            .socket_addrs(|| match upstream_url.scheme() {\n                \"https\" => Some(443),\n                \"http\" => Some(80),\n                _ => None,\n            })\n            .or_err(\n                ErrorType::InternalError,\n                \"couldn't lookup upstream ip address\",\n            )?;\n\n        let upstream_addr = upstream_addrs.first().or_err(\n            ErrorType::InternalError,\n            \"didn't find any upstream ip addresses\",\n        )?;\n\n        let tls = upstream_url.scheme() == \"https\";\n        let host = upstream_url.host().map(|h| h.to_string());\n        let peer = HttpPeer::new(upstream_addr, tls, host.clone().unwrap_or_default());\n\n        ctx.replace(GatewayCtx {\n            upstream_base_path: upstream_url.path().to_string(),\n            upstream_host: host,\n            upstream_service_name: target.service_name.clone(),\n            upstream_sampling_target: target.sampling_target.clone(),\n            upstream_require_auth: target.requires_auth,\n            trace_id: None,\n        });\n\n        Ok(Box::new(peer))\n    }\n\n    async fn response_filter(\n        &self,\n        session: &mut Session,\n        upstream_response: &mut ResponseHeader,\n        ctx: &mut Self::CTX,\n    ) -> pingora::Result<()>\n    where\n        Self::CTX: Send + Sync,\n    {\n        if let Some(gateway_ctx) = ctx.as_ref() {\n            self.inner\n                .cors_config\n                .apply(session.req_header(), upstream_response)?;\n\n            if let Some(trace_id) = gateway_ctx.trace_id {\n                maybe_add_trace_id_header(upstream_response, trace_id);\n            }\n        }\n\n        Ok(())\n    }\n\n    async fn upstream_request_filter(\n        &self,\n        session: &mut Session,\n        upstream_request: &mut RequestHeader,\n        ctx: &mut Self::CTX,\n    ) -> pingora::Result<()>\n    where\n        Self::CTX: Send + Sync,\n    {\n        if let Some(gateway_ctx) = ctx.as_mut() {\n            let new_uri = gateway_ctx\n                .prepend_base_path(&upstream_request.uri)\n                .or_err(\n                    ErrorType::InternalError,\n                    \"failed to prepend upstream base path\",\n                )?;\n\n            upstream_request.set_uri(new_uri);\n\n            if let Some(ref host) = gateway_ctx.upstream_host {\n                upstream_request.insert_header(header::HOST, host)?;\n            }\n\n            if session.is_upgrade_req() {\n                websocket::update_headers_from_websocket_protocol(upstream_request).or_err(\n                    ErrorType::HTTPStatus(400),\n                    \"invalid auth data passed in websocket protocol header\",\n                )?;\n            }\n\n            // Set X-Forwarded-* headers, based on https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/net/http/httputil/reverseproxy.go;l=78\n            if let Some(client_addr) = session.client_addr().and_then(|addr| addr.as_inet()) {\n                let client_ip = client_addr.ip().to_string();\n\n                let prior_headers = upstream_request\n                    .headers\n                    .get_all(\"x-forwarded-for\")\n                    .iter()\n                    .filter_map(|v| std::str::from_utf8(v.as_bytes()).ok())\n                    .fold(String::new(), |mut acc, header| {\n                        if !acc.is_empty() {\n                            acc.push_str(\", \");\n                        }\n                        acc.push_str(header);\n                        acc\n                    });\n\n                if !prior_headers.is_empty() {\n                    let combined = format!(\"{prior_headers}, {client_ip}\");\n                    upstream_request.insert_header(\"x-forwarded-for\", combined)?;\n                } else {\n                    upstream_request.insert_header(\"x-forwarded-for\", client_ip)?;\n                }\n            } else {\n                upstream_request.remove_header(\"x-forwarded-for\");\n            }\n\n            if let Some(host) = session.req_header().headers.get(header::HOST) {\n                upstream_request.insert_header(\"x-forwarded-host\", host)?;\n            }\n\n            upstream_request.insert_header(\n                \"x-forwarded-proto\",\n                match session.req_header().uri.scheme() == Some(&Scheme::HTTPS) {\n                    true => \"https\",\n                    false => \"http\",\n                },\n            )?;\n\n            let svc_auth_method = self\n                .inner\n                .service_registry\n                .service_auth_method(&gateway_ctx.upstream_service_name)\n                .unwrap_or_else(|| Arc::new(svcauth::Noop));\n\n            let headers = &upstream_request.headers;\n\n            // If the request has a caller header, try to authenticate it.\n            // If authenticated, use the internal caller directly so that\n            // private endpoint access is granted through the gateway.\n            // Note: we only use non-noop auth methods here, since noop auth\n            // provides no actual authentication guarantees.\n            let has_internal_caller = headers.get_meta(MetaKey::Caller).is_some()\n                && !self.inner.shared.authenticated_inbound_svc_auth.is_empty();\n            let (mut call_meta, authenticated_internal_caller) = if has_internal_caller {\n                match CallMeta::parse_with_caller(\n                    &self.inner.shared.authenticated_inbound_svc_auth,\n                    headers,\n                    &HashMap::new(),\n                ) {\n                    Ok(meta) => {\n                        let caller = meta.internal.as_ref().map(|i| i.caller.clone());\n                        (meta, caller)\n                    }\n                    Err(_) => {\n                        // Caller verification failed (e.g. invalid signature).\n                        // Treat as an external request.\n                        let meta = CallMeta::parse_without_caller(headers).or_err(\n                            ErrorType::InternalError,\n                            \"couldn't parse CallMeta from request\",\n                        )?;\n                        (meta, None)\n                    }\n                }\n            } else {\n                let meta = CallMeta::parse_without_caller(headers).or_err(\n                    ErrorType::InternalError,\n                    \"couldn't parse CallMeta from request\",\n                )?;\n                (meta, None)\n            };\n            gateway_ctx.trace_id = Some(call_meta.trace_id);\n            if call_meta.parent_span_id.is_none() {\n                call_meta.parent_span_id = Some(model::SpanId::generate());\n            }\n\n            // If the request comes from an authenticated internal service,\n            // use that as the caller directly so that private endpoint access\n            // is granted. Otherwise, use the gateway as the caller.\n            let caller = authenticated_internal_caller.unwrap_or(Caller::Gateway {\n                gateway: self.inner.shared.name.clone(),\n            });\n            let mut desc = CallDesc {\n                caller: &caller,\n                parent_span: call_meta\n                    .parent_span_id\n                    .map(|sp| call_meta.trace_id.with_span(sp)),\n                parent_event_id: None,\n                ext_correlation_id: call_meta\n                    .ext_correlation_id\n                    .as_ref()\n                    .map(|s| Cow::Borrowed(s.as_str())),\n                traced: call_meta.trace_sampled.unwrap_or_else(|| {\n                    match &gateway_ctx.upstream_sampling_target {\n                        router::SamplingTarget::Api(name) => {\n                            self.inner.shared.tracer.should_sample(name)\n                        }\n                        router::SamplingTarget::PubSub => {\n                            self.inner.shared.tracer.should_sample_default()\n                        }\n                    }\n                }),\n                auth_user_id: None,\n                auth_data: None,\n                svc_auth_method: svc_auth_method.as_ref(),\n            };\n\n            if let Some(auth_handler) = &self.inner.shared.auth {\n                // Use the same trace sampling decision for the auth handler\n                // as for the rest of the request.\n                let auth_response = auth_handler\n                    .authenticate(\n                        upstream_request,\n                        CallMeta {\n                            trace_sampled: Some(desc.traced),\n                            ..call_meta.clone()\n                        },\n                    )\n                    .await\n                    .or_err(ErrorType::InternalError, \"couldn't authenticate request\")?;\n\n                match auth_response {\n                    auth::AuthResponse::Authenticated {\n                        auth_uid,\n                        auth_data,\n                    } => {\n                        desc.auth_user_id = Some(Cow::Owned(auth_uid));\n                        desc.auth_data = Some(auth_data);\n                    }\n                    auth::AuthResponse::Unauthenticated { error } => {\n                        if gateway_ctx.upstream_require_auth {\n                            return Err(error.into());\n                        }\n                    }\n                };\n            }\n\n            desc.add_meta(upstream_request)\n                .or_err(ErrorType::InternalError, \"couldn't set request meta\")?;\n        }\n\n        Ok(())\n    }\n\n    async fn fail_to_proxy(\n        &self,\n        session: &mut Session,\n        e: &Error,\n        ctx: &mut Self::CTX,\n    ) -> FailToProxy\n    where\n        Self::CTX: Send + Sync,\n    {\n        // modified version of `Session::respond_error` that adds cors headers,\n        // and handles specific errors\n\n        let code = match e.etype() {\n            ErrorType::HTTPStatus(code) => *code,\n            _ => {\n                match e.esource() {\n                    ErrorSource::Upstream => 502,\n                    ErrorSource::Downstream => {\n                        match e.etype() {\n                            ErrorType::WriteError\n                            | ErrorType::ReadError\n                            | ErrorType::ConnectionClosed => {\n                                /* conn already dead */\n                                return FailToProxy {\n                                    error_code: 0,\n                                    can_reuse_downstream: false,\n                                };\n                            }\n                            _ => 400,\n                        }\n                    }\n                    ErrorSource::Internal | ErrorSource::Unset => 500,\n                }\n            }\n        };\n\n        let (mut resp, body) = if let Some(api_error) = as_api_error(e) {\n            let (resp, body) = api_error_response(api_error);\n            (resp, Some(body))\n        } else {\n            (\n                match code {\n                    /* common error responses are pre-generated */\n                    502 => error_resp::HTTP_502_RESPONSE.clone(),\n                    400 => error_resp::HTTP_400_RESPONSE.clone(),\n                    _ => error_resp::gen_error_response(code),\n                },\n                None,\n            )\n        };\n\n        if let Err(e) = self\n            .inner\n            .cors_config\n            .apply(session.req_header(), &mut resp)\n        {\n            log::error!(\"failed setting cors header in error response: {e}\");\n        }\n        if let Some(gateway_ctx) = ctx.as_ref() {\n            if let Some(trace_id) = gateway_ctx.trace_id {\n                maybe_add_trace_id_header(&mut resp, trace_id);\n            }\n        }\n        session.set_keepalive(None);\n        session\n            .write_response_header(Box::new(resp), false)\n            .await\n            .unwrap_or_else(|e| {\n                log::error!(\"failed to send error response to downstream: {e}\");\n            });\n\n        session\n            .write_response_body(body, true)\n            .await\n            .unwrap_or_else(|e| log::error!(\"failed to write body: {e}\"));\n\n        FailToProxy {\n            error_code: code,\n            can_reuse_downstream: false,\n        }\n    }\n}\n\nfn as_api_error(err: &pingora::Error) -> Option<&api::Error> {\n    err.root_cause().downcast_ref::<api::Error>()\n}\n\nfn maybe_add_trace_id_header(resp: &mut ResponseHeader, trace_id: model::TraceId) {\n    if resp.headers.contains_key(\"x-encore-trace-id\") {\n        return;\n    }\n\n    let value = trace_id.serialize_encore();\n    if let Ok(val) = HeaderValue::from_str(value.as_str()) {\n        let _ = resp.insert_header(\"x-encore-trace-id\", val);\n    }\n}\n\nfn api_error_response(err: &api::Error) -> (ResponseHeader, bytes::Bytes) {\n    let mut buf = BytesMut::with_capacity(128).writer();\n    serde_json::to_writer(&mut buf, &err.as_external()).unwrap();\n\n    let mut resp = ResponseHeader::build(err.code.status_code(), Some(5)).unwrap();\n    resp.insert_header(header::SERVER, &pingora::protocols::http::SERVER_NAME[..])\n        .unwrap();\n    resp.insert_header(header::DATE, \"Sun, 06 Nov 1994 08:49:37 GMT\")\n        .unwrap(); // placeholder\n    resp.insert_header(header::CONTENT_LENGTH, buf.get_ref().len())\n        .unwrap();\n    resp.insert_header(header::CACHE_CONTROL, \"private, no-store\")\n        .unwrap();\n    resp.insert_header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())\n        .unwrap();\n\n    (resp, buf.into_inner().into())\n}\n\nimpl crate::api::auth::InboundRequest for RequestHeader {\n    fn headers(&self) -> &axum::http::HeaderMap {\n        &self.headers\n    }\n\n    fn query(&self) -> Option<&str> {\n        self.uri.query()\n    }\n}\n\nstruct SharedGatewayData {\n    name: EncoreName,\n    auth: Option<auth::Authenticator>,\n    tracer: trace::Tracer,\n    /// Non-noop service auth methods for verifying incoming internal callers.\n    /// Noop auth is excluded since it provides no authentication guarantees.\n    authenticated_inbound_svc_auth: Vec<Arc<dyn svcauth::ServiceAuthMethod>>,\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn maybe_add_trace_id_header_adds_when_missing() {\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n        let trace_id = model::TraceId([1; 16]);\n\n        maybe_add_trace_id_header(&mut resp, trace_id);\n\n        let val = resp\n            .headers\n            .get(\"x-encore-trace-id\")\n            .expect(\"header missing\");\n        assert_eq!(val.to_str().unwrap(), trace_id.serialize_encore());\n    }\n\n    #[test]\n    fn maybe_add_trace_id_header_keeps_existing() {\n        let mut resp = ResponseHeader::build(200, None).unwrap();\n        resp.insert_header(\"x-encore-trace-id\", HeaderValue::from_static(\"existing\"))\n            .unwrap();\n        let trace_id = model::TraceId([2; 16]);\n\n        maybe_add_trace_id_header(&mut resp, trace_id);\n\n        let val = resp\n            .headers\n            .get(\"x-encore-trace-id\")\n            .expect(\"header missing\");\n        assert_eq!(val.to_str().unwrap(), \"existing\");\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/gateway/router.rs",
    "content": "use std::sync::Arc;\n\nuse crate::{\n    api::{self, paths::PathSet, schema::Method},\n    EncoreName, EndpointName,\n};\n\n#[derive(Clone)]\npub struct Router {\n    main: matchit::Router<MethodRoute>,\n    fallback: matchit::Router<MethodRoute>,\n}\n\nimpl Router {\n    pub fn new() -> Self {\n        let main = matchit::Router::new();\n        let fallback = matchit::Router::new();\n        Router { main, fallback }\n    }\n\n    pub fn add_routes(\n        &mut self,\n        routes: &PathSet<EncoreName, Arc<api::Endpoint>>,\n    ) -> anyhow::Result<()> {\n        for (router, routes) in [\n            (&mut self.main, &routes.main),\n            (&mut self.fallback, &routes.fallback),\n        ] {\n            fn register_methods(\n                mr: &mut MethodRoute,\n                path: &str,\n                service: &EncoreName,\n                endpoint: &api::Endpoint,\n            ) {\n                for method in endpoint.methods() {\n                    let dst = match method {\n                        Method::GET => &mut mr.get,\n                        Method::HEAD => &mut mr.head,\n                        Method::POST => &mut mr.post,\n                        Method::PUT => &mut mr.put,\n                        Method::DELETE => &mut mr.delete,\n                        Method::OPTIONS => &mut mr.option,\n                        Method::TRACE => &mut mr.trace,\n                        Method::PATCH => &mut mr.patch,\n                    };\n                    log::trace!(path = path, method = method.as_str(); \"registering route\");\n                    if dst.is_some() {\n                        ::log::error!(method = method.as_str(), path = path; \"tried to register same route twice, skipping\");\n                        continue;\n                    }\n                    dst.replace(Target {\n                        service_name: service.clone(),\n                        sampling_target: SamplingTarget::Api(endpoint.name.clone()),\n                        requires_auth: endpoint.requires_auth,\n                    });\n                }\n            }\n\n            for (service, routes) in routes {\n                for (endpoint, paths) in routes {\n                    for path in paths {\n                        // Create a method route where we register each method the endpoint supports.\n                        let mut mr = MethodRoute::default();\n                        register_methods(&mut mr, path, service, endpoint);\n                        match router.insert(path, mr) {\n                            Ok(()) => {}\n                            Err(matchit::InsertError::Conflict { .. }) => {\n                                // If we have a conflict, we need to merge the method routes.\n                                let mr = router.at_mut(path).unwrap().value;\n                                register_methods(mr, path, service, endpoint);\n                            }\n                            Err(e) => return Err(e.into()),\n                        }\n                    }\n                }\n            }\n        }\n\n        Ok(())\n    }\n\n    pub fn route_to_service(\n        &self,\n        method: api::schema::Method,\n        path: &str,\n    ) -> Result<&Target, api::Error> {\n        let mut found_path_match = false;\n        for router in [&self.main, &self.fallback] {\n            if let Ok(service) = router.at(path) {\n                found_path_match = true;\n                let service = service.value.for_method(method);\n                if let Some(service) = service {\n                    return Ok(service);\n                }\n            }\n        }\n\n        // We couldn't find a matching route.\n        Err(if found_path_match {\n            api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"no route for method\".to_string(),\n                internal_message: Some(format!(\"no route for method {method:?}: {path}\")),\n                stack: None,\n                details: None,\n            }\n        } else {\n            api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"endpoint not found\".to_string(),\n                internal_message: Some(format!(\"no such endpoint exists: {path}\")),\n                stack: None,\n                details: None,\n            }\n        })\n    }\n}\n\n#[derive(Clone, Debug)]\npub enum SamplingTarget {\n    Api(EndpointName),\n    PubSub,\n}\n\n#[derive(Clone, Debug)]\npub struct Target {\n    pub service_name: EncoreName,\n    pub sampling_target: SamplingTarget,\n    pub requires_auth: bool,\n}\n\n#[derive(Clone, Default)]\npub struct MethodRoute {\n    get: Option<Target>,\n    head: Option<Target>,\n    post: Option<Target>,\n    put: Option<Target>,\n    delete: Option<Target>,\n    option: Option<Target>,\n    trace: Option<Target>,\n    patch: Option<Target>,\n}\n\nimpl MethodRoute {\n    fn for_method(&self, method: api::schema::Method) -> Option<&Target> {\n        match method {\n            Method::GET => self.get.as_ref(),\n            Method::HEAD => self.head.as_ref(),\n            Method::POST => self.post.as_ref(),\n            Method::PUT => self.put.as_ref(),\n            Method::DELETE => self.delete.as_ref(),\n            Method::OPTIONS => self.option.as_ref(),\n            Method::TRACE => self.trace.as_ref(),\n            Method::PATCH => self.patch.as_ref(),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/gateway/websocket.rs",
    "content": "use std::{collections::HashMap, str::FromStr};\n\nuse anyhow::anyhow;\nuse base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};\nuse http::{header::SEC_WEBSOCKET_PROTOCOL, HeaderName, HeaderValue};\nuse pingora::http::RequestHeader;\nuse serde::{\n    de::{self, Visitor},\n    Deserialize,\n};\n\nconst ENCORE_DEV_HEADERS: &str = \"encore.dev.headers.\";\n\n// hack to be able to have browsers send request headers when setting up a websocket\n// inspired by https://github.com/kubernetes/kubernetes/commit/714f97d7baf4975ad3aa47735a868a81a984d1f0\npub fn update_headers_from_websocket_protocol(\n    upstream_request: &mut RequestHeader,\n) -> anyhow::Result<()> {\n    let headers = upstream_request\n        .headers\n        .get_all(SEC_WEBSOCKET_PROTOCOL)\n        .into_iter()\n        .cloned()\n        .collect::<Vec<_>>();\n\n    if upstream_request\n        .remove_header(&SEC_WEBSOCKET_PROTOCOL)\n        .is_none()\n    {\n        return Ok(());\n    }\n\n    for header_value in headers {\n        let mut filterd_protocols = Vec::new();\n\n        for protocol in header_value.to_str()?.split(',') {\n            let protocol = protocol.trim();\n            if protocol.starts_with(ENCORE_DEV_HEADERS) {\n                let data = protocol.strip_prefix(ENCORE_DEV_HEADERS).unwrap();\n                let decoded = URL_SAFE_NO_PAD.decode(data)?;\n                let auth_data: AuthHeaderMap = serde_json::from_slice(&decoded)?;\n\n                for (name, value) in auth_data.0 {\n                    if is_forbidden_request_header(&name) {\n                        return Err(anyhow!(\"header {name} not allowed to be set\"));\n                    }\n                    upstream_request.append_header(name, value)?;\n                }\n            } else {\n                filterd_protocols.push(protocol);\n            }\n        }\n\n        if !filterd_protocols.is_empty() {\n            upstream_request.append_header(SEC_WEBSOCKET_PROTOCOL, filterd_protocols.join(\", \"))?;\n        }\n    }\n\n    Ok(())\n}\n\nstruct AuthHeaderMap(HashMap<HeaderName, HeaderValue>);\n\nimpl<'de> Deserialize<'de> for AuthHeaderMap {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        deserializer.deserialize_map(AuthHeaderMapVisitor)\n    }\n}\n\nstruct AuthHeaderMapVisitor;\n\nimpl<'de> Visitor<'de> for AuthHeaderMapVisitor {\n    type Value = AuthHeaderMap;\n\n    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {\n        formatter.write_str(\"AuthHeaderMap\")\n    }\n\n    fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>\n    where\n        A: serde::de::MapAccess<'de>,\n    {\n        let mut map = HashMap::with_capacity(access.size_hint().unwrap_or_default());\n\n        while let Some((key, value)) = access.next_entry::<&str, &str>()? {\n            let name = HeaderName::from_str(key).map_err(de::Error::custom)?;\n            let value = HeaderValue::from_str(value).map_err(de::Error::custom)?;\n\n            map.insert(name, value);\n        }\n\n        Ok(AuthHeaderMap(map))\n    }\n}\n\n// see https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name\nfn is_forbidden_request_header(name: &HeaderName) -> bool {\n    use http::header::*;\n    match *name {\n        ACCEPT_CHARSET\n        | ACCEPT_ENCODING\n        | ACCESS_CONTROL_REQUEST_HEADERS\n        | ACCESS_CONTROL_REQUEST_METHOD\n        | CONNECTION\n        | CONTENT_LENGTH\n        | COOKIE\n        | DATE\n        | DNT\n        | EXPECT\n        | HOST\n        | ORIGIN\n        | REFERER\n        | TE\n        | TRAILER\n        | TRANSFER_ENCODING\n        | UPGRADE\n        | VIA => true,\n        ref n if n == HeaderName::from_static(\"keep-alive\") => true,\n        ref n if n == HeaderName::from_static(\"permissions-policy\") => true,\n        ref n if n.as_str().starts_with(\"sec-\") => true,\n        ref n if n.as_str().starts_with(\"proxy-\") => true,\n        _ => false,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use http::{header::AUTHORIZATION, HeaderMap, HeaderName, HeaderValue};\n    use serde_json::json;\n\n    use super::*;\n\n    #[test]\n    fn test_headers_from_websocket_protocol_preserve_order() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"protocol-a\"),\n        )\n        .unwrap();\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"protocol-b1, protocol-b2\"),\n        )\n        .unwrap();\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"protocol-c\"),\n        )\n        .unwrap();\n\n        let expected = req.headers.clone();\n\n        update_headers_from_websocket_protocol(&mut req)\n            .expect(\"update headers from websocket protocol\");\n\n        assert_eq!(expected, req.headers);\n    }\n\n    #[test]\n    fn test_filter_encore_headers() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"main-protocol\"),\n        )\n        .unwrap();\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"protocol-1, encore.dev.headers.e30K, protocol-2\"),\n        )\n        .unwrap();\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"encore.dev.headers.e30K\"),\n        )\n        .unwrap();\n\n        update_headers_from_websocket_protocol(&mut req)\n            .expect(\"update headers from websocket protocol\");\n\n        let expected = HeaderMap::from_iter(vec![\n            (\n                SEC_WEBSOCKET_PROTOCOL,\n                HeaderValue::from_static(\"main-protocol\"),\n            ),\n            (\n                SEC_WEBSOCKET_PROTOCOL,\n                HeaderValue::from_static(\"protocol-1, protocol-2\"),\n            ),\n        ]);\n        assert_eq!(expected, req.headers);\n    }\n\n    #[test]\n    fn test_adds_auth_data_headers() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        let data = json!({\n            \"authorization\": \"token\",\n            \"x-other-header\": \"value\"\n        });\n\n        let bytes = serde_json::to_vec(&data).unwrap();\n        let encoded = URL_SAFE_NO_PAD.encode(bytes);\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_str(&format!(\"main-protocol, encore.dev.headers.{encoded}\")).unwrap(),\n        )\n        .unwrap();\n\n        update_headers_from_websocket_protocol(&mut req)\n            .expect(\"update headers from websocket protocol\");\n\n        let expected = HeaderMap::from_iter(vec![\n            (\n                SEC_WEBSOCKET_PROTOCOL,\n                HeaderValue::from_static(\"main-protocol\"),\n            ),\n            (AUTHORIZATION, HeaderValue::from_static(\"token\")),\n            (\n                HeaderName::from_static(\"x-other-header\"),\n                HeaderValue::from_static(\"value\"),\n            ),\n        ]);\n        assert_eq!(expected, req.headers);\n    }\n\n    #[test]\n    fn test_appends_auth_data_headers() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        let data = json!({\n            \"x-other-header\": \"new-value\"\n        });\n\n        let bytes = serde_json::to_vec(&data).unwrap();\n        let encoded = URL_SAFE_NO_PAD.encode(bytes);\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_str(&format!(\"main-protocol, encore.dev.headers.{encoded}\")).unwrap(),\n        )\n        .unwrap();\n        req.append_header(\n            HeaderName::from_static(\"x-other-header\"),\n            HeaderValue::from_static(\"prev-value\"),\n        )\n        .unwrap();\n\n        update_headers_from_websocket_protocol(&mut req)\n            .expect(\"update headers from websocket protocol\");\n\n        let expected = HeaderMap::from_iter(vec![\n            (\n                SEC_WEBSOCKET_PROTOCOL,\n                HeaderValue::from_static(\"main-protocol\"),\n            ),\n            (\n                HeaderName::from_static(\"x-other-header\"),\n                HeaderValue::from_static(\"prev-value\"),\n            ),\n            (\n                HeaderName::from_static(\"x-other-header\"),\n                HeaderValue::from_static(\"new-value\"),\n            ),\n        ]);\n        assert_eq!(expected, req.headers);\n    }\n\n    #[test]\n    fn test_invalid_auth_data_header() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_static(\"main-protocol, encore.dev.headers.invalid\"),\n        )\n        .unwrap();\n\n        assert!(update_headers_from_websocket_protocol(&mut req).is_err());\n    }\n\n    #[test]\n    fn test_forbidden_request_headers() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        let data = json!({\n            \"cOOkie\": \"xyz\",\n        });\n\n        let bytes = serde_json::to_vec(&data).unwrap();\n        let encoded = URL_SAFE_NO_PAD.encode(bytes);\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_str(&format!(\"main-protocol, encore.dev.headers.{encoded}\")).unwrap(),\n        )\n        .unwrap();\n\n        assert!(update_headers_from_websocket_protocol(&mut req).is_err());\n    }\n    #[test]\n    fn test_forbidden_prefix_request_headers() {\n        let mut req = RequestHeader::build(http::Method::GET, b\"/some/path\", None).unwrap();\n\n        let data = json!({\n            \"SEC-anything\": \"xyz\",\n        });\n\n        let bytes = serde_json::to_vec(&data).unwrap();\n        let encoded = URL_SAFE_NO_PAD.encode(bytes);\n\n        req.append_header(\n            SEC_WEBSOCKET_PROTOCOL,\n            HeaderValue::from_str(&format!(\"main-protocol, encore.dev.headers.{encoded}\")).unwrap(),\n        )\n        .unwrap();\n\n        assert!(update_headers_from_websocket_protocol(&mut req).is_err());\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/http.rs",
    "content": "#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]\npub enum Method {\n    GET,\n    HEAD,\n    POST,\n    PUT,\n    DELETE,\n    // CONNECT,\n    OPTIONS,\n    TRACE,\n    PATCH,\n}\n\nimpl TryFrom<&str> for Method {\n    type Error = anyhow::Error;\n\n    fn try_from(s: &str) -> Result<Self, Self::Error> {\n        match s {\n            \"GET\" => Ok(Method::GET),\n            \"HEAD\" => Ok(Method::HEAD),\n            \"POST\" => Ok(Method::POST),\n            \"PUT\" => Ok(Method::PUT),\n            \"DELETE\" => Ok(Method::DELETE),\n            // \"CONNECT\" => Ok(Method::CONNECT),\n            \"OPTIONS\" => Ok(Method::OPTIONS),\n            \"TRACE\" => Ok(Method::TRACE),\n            \"PATCH\" => Ok(Method::PATCH),\n            _ => Err(anyhow::anyhow!(\"invalid method: {}\", s)),\n        }\n    }\n}\n\nimpl Into<axum::http::Method> for Method {\n    fn into(self) -> axum::http::Method {\n        match self {\n            Method::GET => axum::http::Method::GET,\n            Method::HEAD => axum::http::Method::HEAD,\n            Method::POST => axum::http::Method::POST,\n            Method::PUT => axum::http::Method::PUT,\n            Method::DELETE => axum::http::Method::DELETE,\n            // Method::CONNECT => axum::http::Method::CONNECT,\n            Method::OPTIONS => axum::http::Method::OPTIONS,\n            Method::TRACE => axum::http::Method::TRACE,\n            Method::PATCH => axum::http::Method::PATCH,\n        }\n    }\n}\n\npub(super) fn method_filter<M>(methods: M) -> Option<axum::routing::MethodFilter>\nwhere\n    M: Iterator<Item = Method>,\n{\n    use axum::routing::MethodFilter;\n    let mut filter = None;\n    for method in methods {\n        let method_filter = match method {\n            Method::GET => MethodFilter::GET,\n            Method::HEAD => MethodFilter::HEAD,\n            Method::POST => MethodFilter::POST,\n            Method::PUT => MethodFilter::PUT,\n            Method::DELETE => MethodFilter::DELETE,\n            // Method::CONNECT => MethodFilter::CONNECT,\n            Method::OPTIONS => MethodFilter::OPTIONS,\n            Method::TRACE => MethodFilter::TRACE,\n            Method::PATCH => MethodFilter::PATCH,\n        };\n        filter = Some(filter.unwrap_or(method_filter).or(method_filter));\n    }\n    filter\n}\n"
  },
  {
    "path": "runtimes/core/src/api/http_server.rs",
    "content": "use std::convert::Infallible;\nuse std::task::{Context, Poll};\n\nuse axum::body::HttpBody;\nuse axum::http::Request;\nuse axum::response::Response;\nuse axum::routing::future::RouteFuture;\nuse axum::serve::IncomingStream;\nuse axum::Router;\nuse tower_service::Service;\n\n#[derive(Clone)]\npub struct HttpServer {\n    encore_routes: Router,\n    api: Option<Router>,\n    fallback: Router,\n}\n\nimpl HttpServer {\n    pub fn new(encore_routes: Router, api: Option<Router>, fallback: Router) -> Self {\n        Self {\n            encore_routes,\n            api,\n            fallback,\n        }\n    }\n}\n\nimpl Service<IncomingStream<'_>> for HttpServer {\n    type Response = Self;\n    type Error = Infallible;\n    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;\n\n    #[inline]\n    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        Poll::Ready(Ok(()))\n    }\n\n    #[inline]\n    fn call(&mut self, _req: IncomingStream<'_>) -> Self::Future {\n        std::future::ready(Ok(self.clone()))\n    }\n}\n\nimpl<B> Service<Request<B>> for HttpServer\nwhere\n    B: HttpBody<Data = bytes::Bytes> + Send + 'static,\n    B::Error: Into<axum::BoxError>,\n{\n    type Response = Response;\n    type Error = Infallible;\n    type Future = RouteFuture<Infallible>;\n\n    #[inline]\n    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {\n        Poll::Ready(Ok(()))\n    }\n\n    #[inline]\n    fn call(&mut self, req: Request<B>) -> Self::Future {\n        if req.uri().path().starts_with(\"/__encore/\") {\n            return self.encore_routes.call(req);\n        }\n\n        let router = match self.api.as_mut() {\n            Some(api) => api,\n            None => &mut self.fallback,\n        };\n        router.call(req)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/httputil.rs",
    "content": "use std::borrow::Cow;\n\nuse anyhow::Context;\n\npub fn convert_headers(headers: &axum::http::HeaderMap) -> reqwest::header::HeaderMap {\n    let mut out = reqwest::header::HeaderMap::with_capacity(headers.len());\n    for (k, v) in headers {\n        let Ok((k, v)) = convert_header(k, v) else {\n            continue;\n        };\n        out.insert(k, v);\n    }\n    out\n}\n\npub fn convert_header(\n    key: &axum::http::HeaderName,\n    value: &axum::http::HeaderValue,\n) -> anyhow::Result<(reqwest::header::HeaderName, reqwest::header::HeaderValue)> {\n    let k = reqwest::header::HeaderName::from_bytes(key.as_str().as_bytes())\n        .context(\"invalid header name\")?;\n    let v = reqwest::header::HeaderValue::from_bytes(value.as_bytes())\n        .context(\"invalid header value\")?;\n    Ok((k, v))\n}\n\npub fn merge_query<'b>(target: Option<&str>, inbound: Option<&'b str>) -> Option<Cow<'b, str>> {\n    match (target, inbound) {\n        (Some(a), Some(b)) => {\n            let mut s = String::with_capacity(a.len() + b.len() + 1);\n            s.push_str(a);\n            s.push('&');\n            s.push_str(b);\n            Some(Cow::Owned(s))\n        }\n        (None, Some(b)) => Some(Cow::Borrowed(b)),\n        (_, None) => None,\n    }\n}\n\npub fn join_url_path<'b>(target: &str, inbound: &'b str) -> Option<Cow<'b, str>> {\n    if inbound.is_empty() {\n        return None;\n    } else if target.is_empty() {\n        return Some(Cow::Borrowed(inbound));\n    }\n\n    let a_slash = target.ends_with('/');\n    let b_slash = inbound.starts_with('/');\n    Some(match (a_slash, b_slash) {\n        (true, true) => {\n            let mut s = String::with_capacity(target.len() + inbound.len() - 1);\n            s.push_str(target);\n            s.push_str(&inbound[1..]);\n            Cow::Owned(s)\n        }\n        (false, false) => {\n            let mut s = String::with_capacity(target.len() + inbound.len() + 1);\n            s.push_str(target);\n            s.push('/');\n            s.push_str(inbound);\n            Cow::Owned(s)\n        }\n        _ => {\n            let mut s = String::with_capacity(target.len() + inbound.len());\n            s.push_str(target);\n            s.push_str(inbound);\n            Cow::Owned(s)\n        }\n    })\n}\n"
  },
  {
    "path": "runtimes/core/src/api/jsonschema/de.rs",
    "content": "use std::borrow::Cow;\nuse std::collections::{BTreeMap, HashMap, HashSet};\nuse std::fmt;\nuse std::fmt::Display;\nuse std::marker::PhantomData;\nuse std::str::FromStr;\n\nuse serde::de::{DeserializeSeed, MapAccess, SeqAccess, Unexpected, Visitor};\nuse serde::Deserializer;\n\nuse crate::api::jsonschema::{validation::Validation, Registry};\nuse crate::api::{self, PValue, PValues};\n\nuse serde_json::Number as JSONNumber;\nuse serde_json::Value as JVal;\n\n#[derive(Debug, Clone)]\npub enum Value {\n    // A JSON primitive (e.g. string, number, boolean, null).\n    Basic(Basic),\n\n    // A literal value.\n    Literal(Literal),\n\n    /// Consume a value if present.\n    Option(BasicOrValue),\n\n    /// Consume a map of key-value pairs (the keys are always strings in JSON).\n    Map(BasicOrValue),\n\n    /// A struct with a set of known fields.\n    Struct(Struct),\n\n    /// Consume an array of values.\n    Array(BasicOrValue),\n\n    /// Consume a single value, one of a union of possible types.\n    Union(Vec<BasicOrValue>),\n\n    /// Reference to another value.\n    Ref(usize),\n\n    /// A value with additional value-based validation.\n    Validation(Validation),\n}\n\n#[derive(Debug, Clone, Default)]\npub struct Struct {\n    pub fields: HashMap<String, Field>,\n}\n\nimpl Struct {\n    pub fn is_empty(&self) -> bool {\n        self.fields.is_empty()\n    }\n\n    pub fn contains_name(&self, name: &str) -> bool {\n        self.fields\n            .iter()\n            .any(|(field_name, field)| field.name_override.as_deref().unwrap_or(field_name) == name)\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Field {\n    pub value: BasicOrValue,\n    pub optional: bool,\n    pub name_override: Option<String>,\n}\n\nimpl Value {\n    pub fn is_basic(&self) -> bool {\n        matches!(self, Value::Basic(_))\n    }\n\n    pub fn is_option(&self) -> bool {\n        matches!(self, Value::Option(_))\n    }\n\n    pub fn is_map(&self) -> bool {\n        matches!(self, Value::Map(_))\n    }\n\n    pub fn is_struct(&self) -> bool {\n        matches!(self, Value::Struct { .. })\n    }\n\n    pub fn is_array(&self) -> bool {\n        matches!(self, Value::Array(_))\n    }\n\n    pub fn is_ref(&self) -> bool {\n        matches!(self, Value::Ref(_))\n    }\n\n    pub fn expecting<'a>(&'a self, reg: &'a Registry) -> Cow<'a, str> {\n        match self {\n            Value::Array(_) => Cow::Borrowed(\"a JSON array\"),\n            Value::Basic(basic) => Cow::Borrowed(basic.expecting()),\n            Value::Map(_) => Cow::Borrowed(\"a JSON map\"),\n            Value::Literal(lit) => Cow::Owned(lit.expecting()),\n            Value::Option(bov) => bov.expecting(reg),\n            Value::Ref(idx) => reg.get(*idx).expecting(reg),\n            Value::Struct { .. } => Cow::Borrowed(\"a JSON object\"),\n            Value::Validation(v) => v.bov.expecting(reg),\n            Value::Union(types) => {\n                let mut s = String::new();\n                let num = types.len();\n                for (i, typ) in types.iter().enumerate() {\n                    if i > 0 {\n                        if i == (num - 1) && num > 2 {\n                            s.push_str(\", or \");\n                        } else if i == (num - 1) {\n                            s.push_str(\" or \");\n                        } else {\n                            s.push_str(\", \");\n                        }\n                    }\n                    s.push_str(&typ.expecting(reg));\n                }\n                Cow::Owned(s)\n            }\n        }\n    }\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum Basic {\n    Any, // Any valid JSON value.\n    Null,\n    Bool,\n    Number,\n    String,\n    DateTime,\n    Decimal,\n}\n\nimpl Basic {\n    pub fn expecting(&self) -> &'static str {\n        match self {\n            Basic::Any => \"any valid JSON value\",\n            Basic::Null => \"null\",\n            Basic::Bool => \"a boolean\",\n            Basic::Number => \"a number\",\n            Basic::String => \"a string\",\n            Basic::DateTime => \"a datetime string\",\n            Basic::Decimal => \"a decimal\",\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub enum Literal {\n    Str(String), // A literal string\n    Bool(bool),\n    Int(i64),\n    Float(f64),\n}\n\nimpl Literal {\n    pub fn expecting(&self) -> String {\n        match self {\n            Literal::Str(lit) => format!(\"{lit:#?}\"),\n            Literal::Bool(lit) => format!(\"{lit:#?}\"),\n            Literal::Int(lit) => format!(\"{lit:#?}\"),\n            Literal::Float(lit) => format!(\"{lit:#?}\"),\n        }\n    }\n\n    pub fn expecting_type(&self) -> &'static str {\n        match self {\n            Literal::Str(_) => \"string\",\n            Literal::Bool(_) => \"boolean\",\n            Literal::Int(_) => \"integer\",\n            Literal::Float(_) => \"number\",\n        }\n    }\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum BasicOrValue {\n    Basic(Basic),\n    Value(usize),\n}\n\nimpl BasicOrValue {\n    pub fn expecting<'a>(&'a self, reg: &'a Registry) -> Cow<'a, str> {\n        match self {\n            BasicOrValue::Basic(basic) => Cow::Borrowed(basic.expecting()),\n            BasicOrValue::Value(idx) => reg.get(*idx).expecting(reg),\n        }\n    }\n}\n\n#[derive(Debug, Default, Clone)]\npub struct DecodeConfig {\n    // If true, attempts to parse strings as other primitive types\n    // when there's a type mismatch.\n    pub coerce_strings: bool,\n\n    // Set to true if arrays are serialized into repeated fields\n    // (e.g query string parameters)\n    pub arrays_as_repeated_fields: bool,\n}\n\n#[derive(Copy, Clone, Debug)]\npub(super) struct DecodeValue<'a> {\n    pub(super) cfg: &'a DecodeConfig,\n    pub(super) reg: &'a Registry,\n    pub(super) value: &'a Value,\n}\n\nimpl<'a> DecodeValue<'a> {\n    fn resolve(&'a self, idx: usize) -> DecodeValue<'a> {\n        DecodeValue {\n            cfg: self.cfg,\n            reg: self.reg,\n            value: &self.reg.values[idx],\n        }\n    }\n}\n\nimpl<'de: 'a, 'a> DeserializeSeed<'de> for DecodeValue<'a> {\n    type Value = PValue;\n\n    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        deserializer.deserialize_any(self)\n    }\n}\n\nmacro_rules! recurse_ref {\n    ($self:ident, $idx:expr, $method:ident, $value:expr) => {{\n        let visitor = DecodeValue {\n            cfg: $self.cfg,\n            reg: $self.reg,\n            value: &$self.reg.values[*$idx],\n        };\n        visitor.$method($value)\n    }};\n}\n\nmacro_rules! recurse_ref0 {\n    ($self:ident, $idx:expr, $method:ident) => {{\n        let visitor = DecodeValue {\n            cfg: $self.cfg,\n            reg: $self.reg,\n            value: &$self.reg.values[*$idx],\n        };\n        visitor.$method()\n    }};\n}\n\nmacro_rules! recurse {\n    ($self:ident, $bov:expr, $method:ident, $value:expr) => {{\n        match $bov {\n            BasicOrValue::Basic(basic) => {\n                let basic_val = Value::Basic(*basic);\n                let visitor = DecodeValue {\n                    cfg: $self.cfg,\n                    reg: $self.reg,\n                    value: &basic_val,\n                };\n                visitor.$method($value)\n            }\n            BasicOrValue::Value(idx) => {\n                let visitor = DecodeValue {\n                    cfg: $self.cfg,\n                    reg: $self.reg,\n                    value: &$self.reg.values[*idx],\n                };\n                visitor.$method($value)\n            }\n        }\n    }};\n}\n\nmacro_rules! validate_pval {\n    ($self:ident, $v:ident, $method:ident, $value:expr) => {{\n        let inner = recurse!($self, &$v.bov, $method, $value)?;\n        match $v.validate_pval(&inner) {\n            Ok(()) => Ok(inner),\n            Err(err) => Err(serde::de::Error::custom(err)),\n        }\n    }};\n}\n\nmacro_rules! validate_jval {\n    ($self:ident, $v:ident, $method:ident, $value:expr) => {{\n        recurse!($self, &$v.bov, $method, $value)?;\n        match $v.validate_jval($value) {\n            Ok(()) => Ok(()),\n            Err(err) => Err(serde::de::Error::custom(err)),\n        }\n    }};\n}\n\nmacro_rules! recurse0 {\n    ($self:ident, $bov:expr, $method:ident) => {{\n        match $bov {\n            BasicOrValue::Basic(basic) => {\n                let basic_val = Value::Basic(*basic);\n                let visitor = DecodeValue {\n                    cfg: $self.cfg,\n                    reg: $self.reg,\n                    value: &basic_val,\n                };\n                visitor.$method()\n            }\n            BasicOrValue::Value(idx) => {\n                let visitor = DecodeValue {\n                    cfg: $self.cfg,\n                    reg: &$self.reg,\n                    value: &$self.reg.values[*idx],\n                };\n                visitor.$method()\n            }\n        }\n    }};\n}\n\nimpl<'de> Visitor<'de> for DecodeValue<'_> {\n    type Value = PValue;\n\n    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {\n        match self.value {\n            Value::Literal(lit) => {\n                let s = lit.expecting();\n                formatter.write_str(&s)\n            }\n            Value::Basic(b) => formatter.write_str(match b {\n                Basic::Any => \"any valid JSON value\",\n                Basic::Null => \"null\",\n                Basic::Bool => \"a boolean\",\n                Basic::Number => \"a number\",\n                Basic::String => \"a string\",\n                Basic::DateTime => \"a datetime string\",\n                Basic::Decimal => \"a decimal\",\n            }),\n            Value::Map(_) => formatter.write_str(\"a JSON object\"),\n            Value::Array(_) => formatter.write_str(\"a JSON array\"),\n            Value::Union(union) => {\n                let num = union.len();\n                let mut s = String::new();\n                for (i, typ) in union.iter().enumerate() {\n                    if i > 0 {\n                        if num > 2 && i == (num - 1) {\n                            s.push_str(\", or \");\n                        } else if i == (num - 1) {\n                            s.push_str(\" or \");\n                        } else {\n                            s.push_str(\", \");\n                        }\n                    }\n                    let expecting = typ.expecting(self.reg);\n                    s.push_str(&expecting);\n                }\n                formatter.write_str(&s)\n            }\n            Value::Option(_) => formatter.write_str(\"any valid JSON value or null\"),\n            Value::Struct { .. } => formatter.write_str(\"a JSON object\"),\n            Value::Ref(_) => formatter.write_str(\"a JSON value\"),\n            Value::Validation(v) => formatter.write_str(v.bov.expecting(self.reg).as_ref()),\n        }\n    }\n\n    #[inline]\n    fn visit_bool<E>(self, value: bool) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        match &self.value {\n            Value::Basic(Basic::Any | Basic::Bool) => Ok(PValue::Bool(value)),\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_bool, value),\n            Value::Option(val) => {\n                recurse!(self, val, visit_bool, value)\n            }\n            Value::Literal(Literal::Bool(bool)) if *bool == value => Ok(PValue::Bool(value)),\n            Value::Validation(v) => validate_pval!(self, v, visit_bool, value),\n            Value::Union(types) => {\n                for typ in types {\n                    let res: Result<_, E> = recurse!(self, typ, visit_bool, value);\n                    if let Ok(val) = res {\n                        return Ok(val);\n                    }\n                }\n                Err(serde::de::Error::invalid_type(\n                    Unexpected::Bool(value),\n                    &self,\n                ))\n            }\n            _ => Err(serde::de::Error::invalid_type(\n                Unexpected::Bool(value),\n                &self,\n            )),\n        }\n    }\n\n    #[inline]\n    fn visit_i64<E>(self, value: i64) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        match self.value {\n            Value::Basic(Basic::Any | Basic::Number) => Ok(PValue::Number(value.into())),\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_i64, value),\n            Value::Option(val) => {\n                recurse!(self, val, visit_i64, value)\n            }\n            Value::Literal(Literal::Int(val)) if *val == value => Ok(PValue::Number(value.into())),\n            Value::Validation(v) => validate_pval!(self, v, visit_i64, value),\n            Value::Union(types) => {\n                for typ in types {\n                    let res: Result<_, E> = recurse!(self, typ, visit_i64, value);\n                    if let Ok(val) = res {\n                        return Ok(val);\n                    }\n                }\n                Err(serde::de::Error::invalid_type(\n                    Unexpected::Signed(value),\n                    &self,\n                ))\n            }\n            _ => Err(serde::de::Error::invalid_type(\n                Unexpected::Signed(value),\n                &self,\n            )),\n        }\n    }\n\n    #[inline]\n    fn visit_u64<E>(self, value: u64) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        match self.value {\n            Value::Basic(Basic::Any | Basic::Number) => Ok(PValue::Number(value.into())),\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_u64, value),\n            Value::Option(val) => {\n                recurse!(self, val, visit_u64, value)\n            }\n            Value::Literal(Literal::Int(val)) if *val == value as i64 => {\n                Ok(PValue::Number(value.into()))\n            }\n            Value::Validation(v) => validate_pval!(self, v, visit_u64, value),\n            Value::Union(types) => {\n                for typ in types {\n                    let res: Result<_, E> = recurse!(self, typ, visit_u64, value);\n                    if let Ok(val) = res {\n                        return Ok(val);\n                    }\n                }\n                Err(serde::de::Error::invalid_type(\n                    Unexpected::Unsigned(value),\n                    &self,\n                ))\n            }\n            _ => Err(serde::de::Error::invalid_type(\n                Unexpected::Unsigned(value),\n                &self,\n            )),\n        }\n    }\n\n    #[inline]\n    fn visit_f64<E>(self, value: f64) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        match self.value {\n            Value::Basic(Basic::Any | Basic::Number) => {\n                Ok(JSONNumber::from_f64(value).map_or(PValue::Null, PValue::Number))\n            }\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_f64, value),\n            Value::Option(bov) => {\n                recurse!(self, bov, visit_f64, value)\n            }\n            Value::Literal(Literal::Float(val)) if *val == value => {\n                if let Some(num) = JSONNumber::from_f64(value) {\n                    Ok(PValue::Number(num))\n                } else {\n                    Err(serde::de::Error::custom(format_args!(\n                        \"expected {val}, got {value}\"\n                    )))\n                }\n            }\n            Value::Validation(v) => validate_pval!(self, v, visit_f64, value),\n            Value::Union(types) => {\n                for typ in types {\n                    let res: Result<_, E> = recurse!(self, typ, visit_f64, value);\n                    if let Ok(val) = res {\n                        return Ok(val);\n                    }\n                }\n                Err(serde::de::Error::invalid_type(\n                    Unexpected::Float(value),\n                    &self,\n                ))\n            }\n            _ => Err(serde::de::Error::invalid_type(\n                Unexpected::Float(value),\n                &self,\n            )),\n        }\n    }\n\n    #[inline]\n    fn visit_str<E>(self, value: &str) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        self.visit_string(String::from(value))\n    }\n\n    #[inline]\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self), ret, level = \"trace\")\n    )]\n    fn visit_string<E>(self, value: String) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        match self.value {\n            Value::Array(bov) if self.cfg.arrays_as_repeated_fields => match bov {\n                BasicOrValue::Basic(basic) => {\n                    let basic_val = Value::Basic(*basic);\n                    let visitor = DecodeValue {\n                        cfg: self.cfg,\n                        reg: self.reg,\n                        value: &basic_val,\n                    };\n                    Ok(PValue::Array(vec![visitor.visit_string(value)?]))\n                }\n                BasicOrValue::Value(idx) => {\n                    let visitor = DecodeValue {\n                        cfg: self.cfg,\n                        reg: self.reg,\n                        value: &self.reg.values[*idx],\n                    };\n                    Ok(PValue::Array(vec![visitor.visit_string(value)?]))\n                }\n            },\n\n            Value::Basic(b) => match b {\n                Basic::Any | Basic::String => Ok(PValue::String(value)),\n                Basic::DateTime => api::DateTime::parse_from_rfc3339(&value)\n                    .map(PValue::DateTime)\n                    .map_err(|e| serde::de::Error::custom(format_args!(\"invalid datetime: {e}\",))),\n                Basic::Decimal => api::Decimal::from_str(&value)\n                    .map(PValue::Decimal)\n                    .map_err(|e| serde::de::Error::custom(format_args!(\"invalid decimal: {e}\",))),\n                Basic::Bool if self.cfg.coerce_strings => {\n                    if value == \"true\" {\n                        Ok(PValue::Bool(true))\n                    } else if value == \"false\" {\n                        Ok(PValue::Bool(false))\n                    } else {\n                        Err(serde::de::Error::custom(format_args!(\n                            \"expected a boolean, got {value}\"\n                        )))\n                    }\n                }\n                Basic::Number if self.cfg.coerce_strings => {\n                    if let Ok(num) = value.parse::<i64>() {\n                        Ok(PValue::Number(num.into()))\n                    } else if let Ok(num) = value.parse::<f64>() {\n                        Ok(JSONNumber::from_f64(num).map_or(PValue::Null, PValue::Number))\n                    } else {\n                        Err(serde::de::Error::custom(format_args!(\n                            \"expected a number, got {value}\"\n                        )))\n                    }\n                }\n                Basic::Null if self.cfg.coerce_strings => {\n                    if value == \"null\" {\n                        Ok(PValue::Null)\n                    } else {\n                        Err(serde::de::Error::custom(format_args!(\n                            \"expected null, got {value}\"\n                        )))\n                    }\n                }\n                _ => Err(serde::de::Error::invalid_type(\n                    Unexpected::Str(&value),\n                    &self,\n                )),\n            },\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_string, value),\n            Value::Option(bov) => {\n                recurse!(self, bov, visit_string, value)\n            }\n\n            Value::Literal(Literal::Str(val)) if val.as_str() == value => Ok(PValue::String(value)),\n            Value::Literal(lit) => match lit {\n                Literal::Str(val) if val.as_str() == value => Ok(PValue::String(value)),\n                Literal::Bool(_) if self.cfg.coerce_strings => {\n                    if let Ok(got) = value.parse::<bool>() {\n                        self.visit_bool(got)\n                    } else {\n                        Err(serde::de::Error::custom(format_args!(\n                            \"expected {}, got {}\",\n                            lit.expecting(),\n                            value,\n                        )))\n                    }\n                }\n                Literal::Int(_) if self.cfg.coerce_strings => {\n                    if let Ok(got) = value.parse::<i64>() {\n                        self.visit_i64(got)\n                    } else {\n                        Err(serde::de::Error::custom(format_args!(\n                            \"expected {}, got {}\",\n                            lit.expecting(),\n                            value,\n                        )))\n                    }\n                }\n                Literal::Float(_) if self.cfg.coerce_strings => {\n                    if let Ok(got) = value.parse::<f64>() {\n                        self.visit_f64(got)\n                    } else {\n                        Err(serde::de::Error::custom(format_args!(\n                            \"expected {}, got {}\",\n                            lit.expecting(),\n                            value,\n                        )))\n                    }\n                }\n                _ => Err(serde::de::Error::custom(format_args!(\n                    \"expected {}, got {}\",\n                    lit.expecting(),\n                    value,\n                ))),\n            },\n            Value::Validation(v) => validate_pval!(self, v, visit_string, value),\n\n            Value::Union(types) => {\n                for typ in types {\n                    let res: Result<_, E> = recurse!(self, typ, visit_string, value.clone());\n                    if let Ok(val) = res {\n                        return Ok(val);\n                    }\n                }\n                Err(serde::de::Error::invalid_type(\n                    Unexpected::Str(&value),\n                    &self,\n                ))\n            }\n            _ => Err(serde::de::Error::invalid_type(\n                Unexpected::Str(&value),\n                &self,\n            )),\n        }\n    }\n\n    #[inline]\n    fn visit_none<E>(self) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        match self.value {\n            Value::Basic(Basic::Any | Basic::Null) | Value::Option(_) => Ok(PValue::Null),\n            Value::Ref(idx) => recurse_ref0!(self, idx, visit_none),\n            Value::Union(types) => {\n                for typ in types {\n                    let res: Result<_, E> = recurse0!(self, typ, visit_none);\n                    if let Ok(val) = res {\n                        return Ok(val);\n                    }\n                }\n                Err(serde::de::Error::invalid_type(Unexpected::Option, &self))\n            }\n            _ => Err(serde::de::Error::invalid_type(Unexpected::Option, &self)),\n        }\n    }\n\n    #[inline]\n    fn visit_some<D>(self, deserializer: D) -> Result<PValue, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        DeserializeSeed::deserialize(self, deserializer)\n    }\n\n    #[inline]\n    fn visit_unit<E>(self) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        self.visit_none()\n    }\n\n    #[inline]\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self, seq), ret, level = \"trace\")\n    )]\n    fn visit_seq<A>(self, mut seq: A) -> Result<PValue, A::Error>\n    where\n        A: SeqAccess<'de>,\n    {\n        match &self.value {\n            Value::Basic(Basic::Any) => visit_seq(self, seq),\n            Value::Array(bov) => match bov {\n                BasicOrValue::Basic(basic) => {\n                    let basic_val = Value::Basic(*basic);\n                    let visitor = DecodeValue {\n                        cfg: self.cfg,\n                        reg: self.reg,\n                        value: &basic_val,\n                    };\n                    visit_seq(visitor, seq)\n                }\n                BasicOrValue::Value(idx) => {\n                    let visitor = DecodeValue {\n                        cfg: self.cfg,\n                        reg: self.reg,\n                        value: &self.reg.values[*idx],\n                    };\n                    visit_seq(visitor, seq)\n                }\n            },\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_seq, seq),\n            Value::Option(bov) => recurse!(self, bov, visit_seq, seq),\n            Value::Validation(v) => validate_pval!(self, v, visit_seq, seq),\n            Value::Union(candidates) => {\n                let mut vec: Vec<serde_json::Value> = Vec::new();\n                while let Some(val) = seq.next_element()? {\n                    vec.push(val);\n                }\n                let arr = JVal::Array(vec);\n\n                for c in candidates {\n                    match c {\n                        BasicOrValue::Basic(basic) => {\n                            let basic_val = Value::Basic(*basic);\n                            let visitor = DecodeValue {\n                                cfg: self.cfg,\n                                reg: self.reg,\n                                value: &basic_val,\n                            };\n                            if visitor.validate::<A::Error>(&arr).is_ok() {\n                                return visitor.transform(arr);\n                            }\n                        }\n                        BasicOrValue::Value(idx) => {\n                            let visitor = DecodeValue {\n                                cfg: self.cfg,\n                                reg: self.reg,\n                                value: &self.reg.values[*idx],\n                            };\n                            if visitor.validate::<A::Error>(&arr).is_ok() {\n                                return visitor.transform(arr);\n                            }\n                        }\n                    }\n                }\n\n                Err(serde::de::Error::invalid_type(Unexpected::Seq, &self))\n            }\n\n            _ => Err(serde::de::Error::invalid_type(Unexpected::Seq, &self)),\n        }\n    }\n\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self, map), ret, level = \"trace\")\n    )]\n    fn visit_map<A>(self, mut map: A) -> Result<PValue, A::Error>\n    where\n        A: MapAccess<'de>,\n    {\n        match &self.value {\n            Value::Basic(Basic::Any) => visit_map(self, map),\n            Value::Map(bov) => match bov {\n                BasicOrValue::Basic(basic) => {\n                    let basic_val = Value::Basic(*basic);\n                    let visitor = DecodeValue {\n                        cfg: self.cfg,\n                        reg: self.reg,\n                        value: &basic_val,\n                    };\n                    visit_map(visitor, map)\n                }\n                BasicOrValue::Value(idx) => {\n                    let visitor = DecodeValue {\n                        cfg: self.cfg,\n                        reg: self.reg,\n                        value: &self.reg.values[*idx],\n                    };\n                    visit_map(visitor, map)\n                }\n            },\n\n            Value::Struct(Struct { fields }) => {\n                let mut values = PValues::new();\n                let mut seen = HashSet::new();\n                while let Some(key) = map.next_key::<String>()? {\n                    // Get the corresponding value from the schema.\n                    match fields.get(&key) {\n                        Some(entry) => {\n                            // Resolve the field value.\n                            let value = match &entry.value {\n                                BasicOrValue::Value(field_idx) => {\n                                    let field = self.resolve(*field_idx);\n                                    map.next_value_seed(field)?\n                                }\n                                BasicOrValue::Basic(basic) => {\n                                    let field = DecodeValue {\n                                        cfg: self.cfg,\n                                        reg: self.reg,\n                                        value: &Value::Basic(*basic),\n                                    };\n                                    map.next_value_seed(field)?\n                                }\n                            };\n\n                            let allow_duplicate_fields =\n                                self.cfg.arrays_as_repeated_fields && value.is_array();\n                            let duplicate = !seen.insert(key.clone());\n\n                            // Check for duplicate keys.\n                            if duplicate && !allow_duplicate_fields {\n                                return Err(serde::de::Error::custom(format_args!(\n                                    \"duplicate field {key}\"\n                                )));\n                            }\n\n                            // Insert it into our map.\n                            if self.cfg.arrays_as_repeated_fields && value.is_array() {\n                                if let PValue::Array(vec) = value {\n                                    values\n                                        .entry(key)\n                                        .and_modify(|prev| {\n                                            if let PValue::Array(prev) = prev {\n                                                prev.extend(vec.clone());\n                                            }\n                                        })\n                                        .or_insert_with(|| PValue::Array(vec));\n                                }\n                            } else {\n                                values.insert(key, value);\n                            }\n                        }\n                        None => {\n                            // Unknown field; ignore it.\n                            map.next_value::<serde::de::IgnoredAny>()?;\n                        }\n                    }\n                }\n\n                // Report any missing fields.\n                if seen.len() != fields.len() {\n                    let missing = fields\n                        .iter()\n                        .filter_map(|(key, field)| {\n                            if seen.contains(key) {\n                                return None;\n                            }\n\n                            // If the field is optional, don't consider it missing.\n                            if field.optional {\n                                return None;\n                            } else if let BasicOrValue::Value(idx) = &field.value {\n                                if matches!(self.resolve(*idx).value, Value::Option(_)) {\n                                    return None;\n                                }\n                            }\n\n                            Some(key.as_str())\n                        })\n                        .collect::<Vec<_>>();\n\n                    match missing.len() {\n                        0 => {} // do nothing\n                        1 => {\n                            return Err(serde::de::Error::custom(format_args!(\n                                \"missing field {}\",\n                                missing[0]\n                            )))\n                        }\n                        _ => {\n                            return Err(serde::de::Error::custom(format_args!(\n                                \"missing fields {}\",\n                                FieldList { names: &missing }\n                            )))\n                        }\n                    }\n                }\n\n                Ok(PValue::Object(values))\n            }\n\n            Value::Ref(idx) => recurse_ref!(self, idx, visit_map, map),\n            Value::Option(bov) => recurse!(self, bov, visit_map, map),\n            Value::Validation(v) => validate_pval!(self, v, visit_map, map),\n            Value::Union(candidates) => {\n                let mut values = serde_json::Map::new();\n                while let Some((key, value)) = map.next_entry()? {\n                    values.insert(key, value);\n                }\n                let map = JVal::Object(values);\n                for c in candidates {\n                    match c {\n                        BasicOrValue::Basic(basic) => {\n                            let basic_val = Value::Basic(*basic);\n                            let visitor = DecodeValue {\n                                cfg: self.cfg,\n                                reg: self.reg,\n                                value: &basic_val,\n                            };\n                            if visitor.validate::<A::Error>(&map).is_ok() {\n                                return visitor.transform(map);\n                            }\n                        }\n                        BasicOrValue::Value(idx) => {\n                            let visitor = DecodeValue {\n                                cfg: self.cfg,\n                                reg: self.reg,\n                                value: &self.reg.values[*idx],\n                            };\n                            if visitor.validate::<A::Error>(&map).is_ok() {\n                                return visitor.transform(map);\n                            }\n                        }\n                    }\n                }\n                Err(serde::de::Error::invalid_type(Unexpected::Map, &self))\n            }\n            _ => Err(serde::de::Error::invalid_type(Unexpected::Map, &self)),\n        }\n    }\n}\n\nfn visit_seq<'de, A>(elem: DecodeValue, mut seq: A) -> Result<PValue, A::Error>\nwhere\n    A: SeqAccess<'de>,\n{\n    let mut vec = Vec::new();\n    // TODO optimize to stop using JSONValueVisitor and use serde_json's visitor directly?\n    while let Some(elem) = seq.next_element_seed(elem)? {\n        vec.push(elem);\n    }\n    Ok(PValue::Array(vec))\n}\n\nfn visit_map<'de, A>(elem: DecodeValue, mut map: A) -> Result<PValue, A::Error>\nwhere\n    A: MapAccess<'de>,\n{\n    let mut values = PValues::new();\n    while let Some((key, value)) = map.next_entry_seed(PhantomData, elem)? {\n        values.insert(key, value);\n    }\n    Ok(PValue::Object(values))\n}\n\nstruct FieldList<'a> {\n    names: &'a [&'a str],\n}\n\nimpl DecodeValue<'_> {\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self), ret, level = \"trace\")\n    )]\n    pub fn validate<E>(&self, value: &JVal) -> Result<(), E>\n    where\n        E: serde::de::Error,\n    {\n        match value {\n            JVal::Null => match self.value {\n                Value::Basic(Basic::Any | Basic::Null) => Ok(()),\n                Value::Option(_) => Ok(()),\n                Value::Ref(idx) => recurse_ref!(self, idx, validate, value),\n                Value::Validation(v) => validate_jval!(self, v, validate, value),\n                Value::Union(types) => {\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, validate, value);\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    Err(serde::de::Error::invalid_type(Unexpected::Option, self))\n                }\n                _ => Err(serde::de::Error::invalid_type(Unexpected::Option, self)),\n            },\n            JVal::Bool(bool) => match self.value {\n                Value::Basic(Basic::Any | Basic::Bool) => Ok(()),\n                Value::Ref(idx) => recurse_ref!(self, idx, validate, value),\n                Value::Option(val) => {\n                    recurse!(self, val, validate, value)\n                }\n                Value::Literal(lit) => match lit {\n                    Literal::Bool(val) if *bool == *val => Ok(()),\n                    _ => Err(serde::de::Error::custom(format_args!(\n                        \"expected {}, got {}\",\n                        lit.expecting(),\n                        bool,\n                    ))),\n                },\n                Value::Validation(v) => validate_jval!(self, v, validate, value),\n                Value::Union(types) => {\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, validate, value);\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    Err(serde::de::Error::invalid_type(\n                        Unexpected::Bool(*bool),\n                        self,\n                    ))\n                }\n                _ => Err(serde::de::Error::invalid_type(\n                    Unexpected::Bool(*bool),\n                    self,\n                )),\n            },\n            JVal::Number(num) => match self.value {\n                Value::Basic(Basic::Any | Basic::Number) => Ok(()),\n                Value::Ref(idx) => recurse_ref!(self, idx, validate, value),\n                Value::Option(val) => {\n                    recurse!(self, val, validate, value)\n                }\n                Value::Literal(lit) => match lit {\n                    Literal::Int(val) if num.as_i64() == Some(*val) => Ok(()),\n                    Literal::Float(val) if num.as_f64() == Some(*val) => Ok(()),\n                    _ => Err(serde::de::Error::custom(format_args!(\n                        \"expected {}, got {}\",\n                        lit.expecting(),\n                        num,\n                    ))),\n                },\n                Value::Validation(v) => validate_jval!(self, v, validate, value),\n                Value::Union(types) => {\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, validate, value);\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    Err(serde::de::Error::invalid_type(\n                        Unexpected::Other(\"number\"),\n                        self,\n                    ))\n                }\n                _ => Err(serde::de::Error::invalid_type(\n                    Unexpected::Other(\"number\"),\n                    self,\n                )),\n            },\n\n            JVal::String(string) => match self.value {\n                Value::Basic(Basic::Any | Basic::String) => Ok(()),\n                Value::Basic(Basic::DateTime) => api::DateTime::parse_from_rfc3339(string)\n                    .map(|_| ())\n                    .map_err(|e| serde::de::Error::custom(format_args!(\"invalid datetime: {e}\",))),\n                Value::Basic(Basic::Decimal) => api::Decimal::from_str(string)\n                    .map(|_| ())\n                    .map_err(|e| serde::de::Error::custom(format_args!(\"invalid decimal: {e}\",))),\n                Value::Ref(idx) => recurse_ref!(self, idx, validate, value),\n                Value::Option(val) => {\n                    recurse!(self, val, validate, value)\n                }\n                Value::Literal(lit) => match lit {\n                    Literal::Str(val) if string.as_str() == *val => Ok(()),\n                    _ => Err(serde::de::Error::custom(format_args!(\n                        \"expected {}, got {}\",\n                        lit.expecting(),\n                        string,\n                    ))),\n                },\n                Value::Validation(v) => validate_jval!(self, v, validate, value),\n                Value::Union(types) => {\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, validate, value);\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    Err(serde::de::Error::invalid_type(\n                        Unexpected::Str(string),\n                        self,\n                    ))\n                }\n                _ => Err(serde::de::Error::invalid_type(\n                    Unexpected::Str(string),\n                    self,\n                )),\n            },\n\n            JVal::Array(array) => match self.value {\n                Value::Basic(Basic::Any) => Ok(()),\n                Value::Array(bov) => match bov {\n                    BasicOrValue::Basic(basic) => {\n                        let basic_val = Value::Basic(*basic);\n                        let visitor = DecodeValue {\n                            cfg: self.cfg,\n                            reg: self.reg,\n                            value: &basic_val,\n                        };\n                        for elem in array {\n                            visitor.validate(elem)?;\n                        }\n                        Ok(())\n                    }\n                    BasicOrValue::Value(idx) => {\n                        let visitor = DecodeValue {\n                            cfg: self.cfg,\n                            reg: self.reg,\n                            value: &self.reg.values[*idx],\n                        };\n                        for elem in array {\n                            visitor.validate(elem)?;\n                        }\n                        Ok(())\n                    }\n                },\n                Value::Ref(idx) => recurse_ref!(self, idx, validate, value),\n                Value::Option(bov) => {\n                    for elem in array {\n                        recurse!(self, bov, validate, elem)?;\n                    }\n                    Ok(())\n                }\n                Value::Validation(v) => validate_jval!(self, v, validate, value),\n                Value::Union(types) => {\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, validate, value);\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    Err(serde::de::Error::invalid_type(Unexpected::Seq, self))\n                }\n                _ => Err(serde::de::Error::invalid_type(Unexpected::Seq, self)),\n            },\n\n            JVal::Object(map) => match self.value {\n                Value::Ref(idx) => recurse_ref!(self, idx, validate, value),\n                Value::Option(bov) => recurse!(self, bov, validate, value),\n                Value::Basic(Basic::Any) => Ok(()),\n\n                Value::Union(types) => {\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, validate, value);\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    Err(serde::de::Error::invalid_type(Unexpected::Map, self))\n                }\n                Value::Map(bov) => match bov {\n                    BasicOrValue::Basic(basic) => {\n                        let basic_val = Value::Basic(*basic);\n                        let visitor = DecodeValue {\n                            cfg: self.cfg,\n                            reg: self.reg,\n                            value: &basic_val,\n                        };\n                        for (_key, value) in map {\n                            visitor.validate(value)?;\n                        }\n                        Ok(())\n                    }\n                    BasicOrValue::Value(idx) => {\n                        let visitor = DecodeValue {\n                            cfg: self.cfg,\n                            reg: self.reg,\n                            value: &self.reg.values[*idx],\n                        };\n                        for (_key, value) in map {\n                            visitor.validate(value)?;\n                        }\n                        Ok(())\n                    }\n                },\n                Value::Struct(Struct { fields }) => {\n                    let mut seen = HashSet::new();\n                    for (key, value) in map {\n                        match fields.get(key.as_str()) {\n                            Some(entry) => {\n                                seen.insert(key.clone());\n\n                                match &entry.value {\n                                    BasicOrValue::Value(field_idx) => {\n                                        let field = self.resolve(*field_idx);\n                                        field.validate(value)?\n                                    }\n                                    BasicOrValue::Basic(basic) => {\n                                        let field = DecodeValue {\n                                            cfg: self.cfg,\n                                            reg: self.reg,\n                                            value: &Value::Basic(*basic),\n                                        };\n                                        field.validate(value)?\n                                    }\n                                }\n                            }\n                            None => {\n                                // Unknown field; ignore it.\n                            }\n                        }\n                    }\n\n                    // Report any missing fields.\n                    if seen.len() != fields.len() {\n                        let missing = fields\n                            .iter()\n                            .filter_map(|(key, field)| {\n                                if seen.contains(key) {\n                                    return None;\n                                }\n\n                                if field.optional {\n                                    return None;\n                                } else if let BasicOrValue::Value(idx) = &field.value {\n                                    if matches!(self.resolve(*idx).value, Value::Option(_)) {\n                                        return None;\n                                    }\n                                }\n\n                                Some(key.as_str())\n                            })\n                            .collect::<Vec<_>>();\n\n                        match missing.len() {\n                            0 => {} // do nothing\n                            1 => {\n                                return Err(serde::de::Error::custom(format_args!(\n                                    \"missing field {}\",\n                                    missing[0]\n                                )))\n                            }\n                            _ => {\n                                return Err(serde::de::Error::custom(format_args!(\n                                    \"missing fields {}\",\n                                    FieldList { names: &missing }\n                                )))\n                            }\n                        }\n                    }\n\n                    Ok(())\n                }\n                Value::Validation(v) => validate_jval!(self, v, validate, value),\n\n                _ => Err(serde::de::Error::invalid_type(Unexpected::Map, self)),\n            },\n        }\n    }\n\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self), ret, level = \"trace\")\n    )]\n    fn transform<E>(&self, value: JVal) -> Result<PValue, E>\n    where\n        E: serde::de::Error,\n    {\n        Ok(match value {\n            JVal::Null => PValue::Null,\n            JVal::Bool(val) => PValue::Bool(val),\n            JVal::Number(num) => PValue::Number(num),\n            JVal::Array(vals) => match self.value {\n                Value::Ref(idx) => return recurse_ref!(self, idx, transform, JVal::Array(vals)),\n                Value::Option(bov) => return recurse!(self, bov, transform, JVal::Array(vals)),\n                Value::Validation(v) => {\n                    return recurse!(self, &v.bov, transform, JVal::Array(vals))\n                }\n                Value::Basic(Basic::Any) => {\n                    let mut new_vals = Vec::with_capacity(vals.len());\n                    for val in vals {\n                        new_vals.push(self.transform(val)?);\n                    }\n                    PValue::Array(new_vals)\n                }\n                Value::Array(bov) => {\n                    let mut new_vals = Vec::with_capacity(vals.len());\n                    for val in vals {\n                        let val = recurse!(self, bov, transform, val)?;\n                        new_vals.push(val);\n                    }\n                    PValue::Array(new_vals)\n                }\n                Value::Union(candidates) => {\n                    let val = JVal::Array(vals);\n                    for c in candidates {\n                        match c {\n                            BasicOrValue::Basic(basic) => {\n                                let basic_val = Value::Basic(*basic);\n                                let visitor = DecodeValue {\n                                    cfg: self.cfg,\n                                    reg: self.reg,\n                                    value: &basic_val,\n                                };\n                                if visitor.validate::<E>(&val).is_ok() {\n                                    return visitor.transform(val);\n                                }\n                            }\n                            BasicOrValue::Value(idx) => {\n                                let visitor = DecodeValue {\n                                    cfg: self.cfg,\n                                    reg: self.reg,\n                                    value: &self.reg.values[*idx],\n                                };\n                                if visitor.validate::<E>(&val).is_ok() {\n                                    return visitor.transform(val);\n                                }\n                            }\n                        }\n                    }\n                    return Err(serde::de::Error::invalid_type(Unexpected::Seq, self));\n                }\n                Value::Basic(basic) => {\n                    return Err(serde::de::Error::invalid_type(\n                        Unexpected::Other(basic.expecting()),\n                        self,\n                    ))\n                }\n                Value::Literal(lit) => {\n                    return Err(serde::de::Error::invalid_type(\n                        Unexpected::Other(lit.expecting_type()),\n                        self,\n                    ))\n                }\n                Value::Map(_) | Value::Struct(_) => {\n                    return Err(serde::de::Error::invalid_type(Unexpected::Map, self))\n                }\n            },\n            JVal::Object(obj) => match self.value {\n                Value::Ref(idx) => return recurse_ref!(self, idx, transform, JVal::Object(obj)),\n                Value::Option(bov) => return recurse!(self, bov, transform, JVal::Object(obj)),\n                Value::Validation(v) => {\n                    return recurse!(self, &v.bov, transform, JVal::Object(obj))\n                }\n                Value::Basic(Basic::Any) => {\n                    let mut new_obj = BTreeMap::new();\n                    for (key, val) in obj {\n                        new_obj.insert(key, self.transform(val)?);\n                    }\n                    PValue::Object(new_obj)\n                }\n                Value::Map(bov) => {\n                    let mut new_obj = BTreeMap::new();\n                    for (key, val) in obj {\n                        let val = recurse!(self, bov, transform, val)?;\n                        new_obj.insert(key, val);\n                    }\n                    PValue::Object(new_obj)\n                }\n                Value::Struct(Struct { fields }) => {\n                    let mut new_obj = BTreeMap::new();\n                    for (key, value) in obj {\n                        match fields.get(key.as_str()) {\n                            Some(entry) => {\n                                let val = recurse!(self, &entry.value, transform, value)?;\n                                new_obj.insert(key, val);\n                            }\n                            None => {\n                                // Unknown field; ignore it.\n                            }\n                        }\n                    }\n                    PValue::Object(new_obj)\n                }\n                Value::Union(candidates) => {\n                    let val = JVal::Object(obj);\n                    for c in candidates {\n                        match c {\n                            BasicOrValue::Basic(basic) => {\n                                let basic_val = Value::Basic(*basic);\n                                let visitor = DecodeValue {\n                                    cfg: self.cfg,\n                                    reg: self.reg,\n                                    value: &basic_val,\n                                };\n                                if visitor.validate::<E>(&val).is_ok() {\n                                    return visitor.transform(val);\n                                }\n                            }\n                            BasicOrValue::Value(idx) => {\n                                let visitor = DecodeValue {\n                                    cfg: self.cfg,\n                                    reg: self.reg,\n                                    value: &self.reg.values[*idx],\n                                };\n                                if visitor.validate::<E>(&val).is_ok() {\n                                    return visitor.transform(val);\n                                }\n                            }\n                        }\n                    }\n\n                    return Err(serde::de::Error::invalid_type(Unexpected::Map, self));\n                }\n                Value::Basic(basic) => {\n                    return Err(serde::de::Error::invalid_type(\n                        Unexpected::Other(basic.expecting()),\n                        self,\n                    ))\n                }\n                Value::Literal(lit) => {\n                    return Err(serde::de::Error::invalid_type(\n                        Unexpected::Other(lit.expecting_type()),\n                        self,\n                    ))\n                }\n                Value::Array(_) => {\n                    return Err(serde::de::Error::invalid_type(Unexpected::Seq, self))\n                }\n            },\n            JVal::String(str) => match self.value {\n                Value::Ref(idx) => return recurse_ref!(self, idx, transform, JVal::String(str)),\n                Value::Option(bov) => return recurse!(self, bov, transform, JVal::String(str)),\n                Value::Validation(v) => {\n                    return recurse!(self, &v.bov, transform, JVal::String(str))\n                }\n                Value::Basic(Basic::DateTime) => api::DateTime::parse_from_rfc3339(&str)\n                    .map(PValue::DateTime)\n                    .map_err(|e| {\n                        serde::de::Error::custom(format_args!(\"invalid datetime: {e}\",))\n                    })?,\n\n                Value::Basic(Basic::Decimal) => api::Decimal::from_str(&str)\n                    .map(PValue::Decimal)\n                    .map_err(|e| {\n                    serde::de::Error::custom(format_args!(\"invalid decimal: {e}\",))\n                })?,\n\n                // Any non-datetime, non-decimal basic value gets transformed into a string.\n                Value::Basic(_) => PValue::String(str),\n\n                Value::Literal(_) => PValue::String(str),\n                Value::Union(types) => {\n                    let val = JVal::String(str);\n                    for typ in types {\n                        let res: Result<_, E> = recurse!(self, typ, transform, val.clone());\n                        if res.is_ok() {\n                            return res;\n                        }\n                    }\n                    return Err(serde::de::Error::invalid_type(\n                        Unexpected::Other(\"string\"),\n                        self,\n                    ));\n                }\n                Value::Map(_) | Value::Struct(_) | Value::Array(_) => {\n                    return Err(serde::de::Error::invalid_type(Unexpected::Str(&str), self))\n                }\n            },\n        })\n    }\n}\n\nimpl Display for FieldList<'_> {\n    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n        match self.names.len() {\n            0 => panic!(), // special case elsewhere\n            1 => write!(formatter, \"`{}`\", self.names[0]),\n            2 => write!(formatter, \"`{}` and `{}`\", self.names[0], self.names[1]),\n            _ => {\n                for (i, alt) in self.names.iter().enumerate() {\n                    if i > 0 {\n                        write!(formatter, \", \")?;\n                    }\n                    if i == self.names.len() - 1 {\n                        write!(formatter, \"and \")?;\n                    }\n                    write!(formatter, \"`{alt}`\")?;\n                }\n                Ok(())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/jsonschema/meta.rs",
    "content": "use std::collections::HashMap;\nuse std::ops::Deref;\nuse std::sync::Arc;\n\nuse anyhow::{Context, Result};\n\nuse crate::api::jsonschema::de::{Basic, BasicOrValue, Field, Literal, Struct};\nuse crate::api::jsonschema::{JSONSchema, Registry, Value};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::parser::schema::v1 as schema;\nuse crate::encore::parser::schema::v1::r#type::Typ;\n\nuse super::validation;\n\nimpl Registry {\n    pub fn schema(self: &Arc<Self>, id: usize) -> JSONSchema {\n        JSONSchema {\n            registry: self.clone(),\n            root: id,\n        }\n    }\n}\n\n/// Builder builds a JSONSchema registry.\npub struct Builder<'a> {\n    md: &'a meta::Data,\n\n    /// Values that have been computed so far.\n    /// None indicates a declaration that's being computed;\n    /// it's stored as None until it's computed to be able to handle\n    /// recursive references.\n    values: Vec<Option<Value>>,\n\n    /// Map of declaration ids to value indices.\n    decls: HashMap<u32, usize>,\n}\n\nstruct BuilderCtx<'a, 'b: 'a> {\n    builder: &'a mut Builder<'b>,\n\n    /// The ids of computed type arguments, for the current declaration being processed.\n    type_args: &'a [usize],\n}\n\nimpl<'a> Builder<'a> {\n    pub fn new(md: &'a meta::Data) -> Self {\n        Self {\n            md,\n            values: Vec::new(),\n            decls: HashMap::new(),\n        }\n    }\n\n    pub fn build(self) -> Arc<Registry> {\n        // Ensure all values have been computed.\n        let mut values = Vec::with_capacity(self.values.len());\n        for v in self.values {\n            values.push(v.expect(\"missing value\"));\n        }\n\n        Arc::new(Registry { values })\n    }\n\n    #[inline]\n    pub fn get(&self, idx: usize) -> Option<&Value> {\n        self.values.get(idx).and_then(|v| v.as_ref())\n    }\n\n    #[inline]\n    pub fn register_value(&mut self, val: Value) -> usize {\n        match val {\n            // If it's already a ref, return it unmodified.\n            Value::Ref(idx) => idx,\n            val => {\n                let mut ctx = BuilderCtx {\n                    builder: self,\n                    type_args: &[],\n                };\n                ctx.reg(val)\n            }\n        }\n    }\n\n    #[inline]\n    pub fn register_type(&mut self, typ: &schema::Type) -> Result<usize> {\n        let mut ctx = BuilderCtx {\n            builder: self,\n            type_args: &[],\n        };\n        let val = ctx.typ(typ)?;\n\n        Ok(match val {\n            // If it's a ref, return its index directly.\n            Value::Ref(idx) => idx,\n            val => ctx.reg(val),\n        })\n    }\n\n    pub fn struct_field<'b>(&mut self, f: &'b schema::Field) -> Result<(&'b String, Field)> {\n        // This should be safe to do because it's only called for schema types,\n        // and schema types don't include any type arguments, so we shouldn't need to worry\n        // about missing type arguments.\n        let ctx = &mut BuilderCtx {\n            builder: self,\n            type_args: &[],\n        };\n        ctx.struct_field(f)\n    }\n}\n\nimpl BuilderCtx<'_, '_> {\n    /// Computes the JSONSchema value for the given type.\n    #[inline]\n    fn typ<T: ToType>(&mut self, typ: T) -> Result<Value> {\n        let tt = typ.tt()?;\n\n        let val = match tt {\n            Typ::Named(named) => self.named(named),\n            Typ::Builtin(builtin) => {\n                let builtin = schema::Builtin::try_from(*builtin).context(\"invalid builtin\")?;\n                Ok(self.builtin(builtin))\n            }\n\n            Typ::Pointer(ptr) => self.ptr(ptr),\n            Typ::Option(opt) => self.option(opt),\n            Typ::Struct(st) => Ok(Value::Struct(self.struct_val(st)?)),\n            Typ::Map(map) => self.map(map),\n            Typ::List(list) => self.list(list),\n            Typ::Union(union) => self.union(union),\n            Typ::Literal(lit) => self.literal(lit),\n            Typ::Config(_) => anyhow::bail!(\"config not yet supported\"),\n            Typ::TypeParameter(param) => {\n                let idx = self\n                    .type_args\n                    .get(param.param_idx as usize)\n                    .ok_or_else(|| anyhow::anyhow!(\"missing type argument\"))?;\n                Ok(Value::Ref(*idx))\n            }\n        }?;\n\n        if let Some(expr) = typ.validation() {\n            let bov = self.bov(val);\n            Ok(Value::Validation(validation::Validation {\n                expr: expr.try_into()?,\n                bov,\n            }))\n        } else {\n            Ok(val)\n        }\n    }\n\n    #[inline]\n    fn named(&mut self, named: &schema::Named) -> Result<Value> {\n        let decl = self\n            .builder\n            .md\n            .decls\n            .get(named.id as usize)\n            .context(\"missing decl\")?;\n\n        // Compute indices for the type arguments.\n        let type_args: Result<Vec<usize>> = named\n            .type_arguments\n            .iter()\n            .map(|t| self.typ(t).map(|v| self.reg(v)))\n            .collect();\n        let type_args = type_args?;\n\n        // Create a nested context that includes the type arguments.\n        let mut nested = BuilderCtx {\n            builder: self.builder,\n            type_args: &type_args,\n        };\n\n        let idx = nested.decl(decl)?;\n        Ok(Value::Ref(idx))\n    }\n\n    #[inline]\n    fn decl(&mut self, decl: &schema::Decl) -> Result<usize> {\n        // Do we have a value for this decl already?\n        if let Some(idx) = self.builder.decls.get(&decl.id) {\n            return Ok(*idx);\n        }\n\n        // Allocate an index first to handle recursive references.\n        let idx = self.builder.values.len();\n        self.builder.values.push(None);\n        self.builder.decls.insert(decl.id, idx);\n\n        // Then compute the type and update the stored value.\n        let typ = self.typ(&decl.r#type)?;\n        self.builder.values[idx] = Some(typ);\n        Ok(idx)\n    }\n\n    #[inline]\n    fn ptr(&mut self, ptr: &schema::Pointer) -> Result<Value> {\n        self.typ(&ptr.base)\n    }\n\n    #[inline]\n    fn option(&mut self, opt: &schema::Option) -> Result<Value> {\n        let value = self.typ(&opt.value)?;\n        Ok(Value::Union(vec![\n            self.bov(value),\n            BasicOrValue::Basic(Basic::Null),\n        ]))\n    }\n\n    #[inline]\n    fn builtin(&mut self, b: schema::Builtin) -> Value {\n        use schema::Builtin;\n        Value::Basic(match b {\n            Builtin::Any | Builtin::Json => Basic::Any,\n            Builtin::Bool => Basic::Bool,\n            Builtin::String | Builtin::Bytes | Builtin::Uuid | Builtin::UserId => Basic::String,\n            Builtin::Time => Basic::DateTime,\n            Builtin::Decimal => Basic::Decimal,\n\n            Builtin::Int\n            | Builtin::Uint\n            | Builtin::Int8\n            | Builtin::Int16\n            | Builtin::Int32\n            | Builtin::Int64\n            | Builtin::Uint8\n            | Builtin::Uint16\n            | Builtin::Uint32\n            | Builtin::Uint64\n            | Builtin::Float32\n            | Builtin::Float64 => Basic::Number,\n        })\n    }\n\n    #[inline]\n    pub fn struct_val(&mut self, st: &schema::Struct) -> Result<Struct> {\n        Ok(Struct {\n            fields: {\n                let mut map = HashMap::with_capacity(st.fields.len());\n                for f in &st.fields {\n                    let (k, v) = self.struct_field(f)?;\n                    map.insert(k.to_owned(), v);\n                }\n                map\n            },\n        })\n    }\n\n    #[inline]\n    fn struct_field<'c>(&mut self, f: &'c schema::Field) -> Result<(&'c String, Field)> {\n        let typ = self.typ(&f.typ)?;\n        let value = match typ {\n            Value::Basic(basic) => BasicOrValue::Basic(basic),\n            val => self.bov(val),\n        };\n\n        Ok((\n            &f.name,\n            Field {\n                value,\n                optional: f.optional,\n                name_override: None,\n            },\n        ))\n    }\n\n    #[inline]\n    fn map(&mut self, map: &schema::Map) -> Result<Value> {\n        // Note: JSON doesn't support anything but string keys,\n        // so we don't actually track the key type for the purpose\n        // of JSON schemas. Ignore it here.\n        let value = self.typ(&map.value)?;\n        Ok(Value::Map(self.bov(value)))\n    }\n\n    #[inline]\n    fn list(&mut self, list: &schema::List) -> Result<Value> {\n        let value = self.typ(&list.elem)?;\n        Ok(Value::Array(self.bov(value)))\n    }\n\n    #[inline]\n    fn union(&mut self, union: &schema::Union) -> Result<Value> {\n        let values: Result<Vec<BasicOrValue>> = union\n            .types\n            .iter()\n            .map(|t| self.typ(t).map(|v| self.bov(v)))\n            .collect();\n        Ok(Value::Union(values?))\n    }\n\n    #[inline]\n    fn literal(&mut self, literal: &schema::Literal) -> Result<Value> {\n        Ok(match literal.value.clone() {\n            Some(schema::literal::Value::Str(val)) => Value::Literal(Literal::Str(val)),\n            Some(schema::literal::Value::Boolean(val)) => Value::Literal(Literal::Bool(val)),\n            Some(schema::literal::Value::Int(val)) => Value::Literal(Literal::Int(val)),\n            Some(schema::literal::Value::Float(val)) => Value::Literal(Literal::Float(val)),\n            Some(schema::literal::Value::Null(_)) => Value::Basic(Basic::Null),\n            None => anyhow::bail!(\"missing literal value\"),\n        })\n    }\n\n    #[inline]\n    fn bov(&mut self, value: Value) -> BasicOrValue {\n        match value {\n            Value::Basic(basic) => BasicOrValue::Basic(basic),\n            val => BasicOrValue::Value(self.reg(val)),\n        }\n    }\n\n    #[inline]\n    fn reg(&mut self, value: Value) -> usize {\n        let idx = self.builder.values.len();\n        self.builder.values.push(Some(value));\n        idx\n    }\n}\n\ntrait ToType: std::fmt::Debug {\n    fn tt(&self) -> Result<&Typ>;\n    fn validation(&self) -> Option<&schema::ValidationExpr>;\n}\n\nimpl<T> ToType for Option<T>\nwhere\n    T: ToType,\n{\n    fn tt(&self) -> Result<&Typ> {\n        self.as_ref().context(\"missing type\")?.tt()\n    }\n\n    fn validation(&self) -> Option<&schema::ValidationExpr> {\n        self.as_ref().and_then(|t| t.validation())\n    }\n}\n\nimpl<T> ToType for Box<T>\nwhere\n    T: ToType,\n{\n    fn tt(&self) -> Result<&Typ> {\n        self.deref().tt()\n    }\n\n    fn validation(&self) -> Option<&schema::ValidationExpr> {\n        self.deref().validation()\n    }\n}\n\nimpl ToType for schema::Type {\n    fn tt(&self) -> Result<&Typ> {\n        self.typ.as_ref().context(\"missing type\")\n    }\n\n    fn validation(&self) -> Option<&schema::ValidationExpr> {\n        self.validation.as_ref()\n    }\n}\n\nimpl<T: ToType> ToType for &T {\n    fn tt(&self) -> Result<&Typ> {\n        (*self).tt()\n    }\n\n    fn validation(&self) -> Option<&schema::ValidationExpr> {\n        (*self).validation()\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/jsonschema/mod.rs",
    "content": "use std::fmt;\nuse std::sync::Arc;\n\nuse serde::de::{DeserializeSeed, Deserializer};\n\npub use de::{Basic, BasicOrValue, Field, Struct, Value};\n\npub use crate::api::jsonschema::de::DecodeConfig;\nuse crate::api::jsonschema::de::DecodeValue;\n\nmod de;\nmod meta;\nmod parse;\nmod ser;\npub mod validation;\n\nuse crate::api::jsonschema::parse::ParseWithSchema;\nuse crate::api::APIResult;\npub use meta::Builder;\n\nuse super::{PValue, PValues};\n\n#[derive(Clone)]\npub struct JSONSchema {\n    registry: Arc<Registry>,\n    root: usize,\n}\n\npub struct Registry {\n    /// Vector of allocated values.\n    values: Vec<Value>,\n}\n\nimpl Registry {\n    pub fn get(&self, mut idx: usize) -> &Value {\n        loop {\n            match &self.values[idx] {\n                Value::Ref(i) => idx = *i,\n                other => return other,\n            }\n        }\n    }\n}\n\nimpl fmt::Debug for Registry {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        // Don't render the list of values since it's too large.\n        f.debug_struct(\"Registry\").finish()\n    }\n}\n\nimpl JSONSchema {\n    pub fn root_value(&self) -> &Value {\n        &self.registry.values[self.root]\n    }\n\n    #[inline]\n    pub fn root(&self) -> &Struct {\n        let Value::Struct(str) = &self.registry.values[self.root] else {\n            panic!(\"root is not a struct\");\n        };\n        str\n    }\n\n    pub fn parse<P, O>(&self, payload: P) -> APIResult<O>\n    where\n        P: ParseWithSchema<O>,\n        O: Sized,\n    {\n        payload.parse_with_schema(self)\n    }\n\n    pub fn seed_deserializer<'de: 'a, 'a>(\n        &'a self,\n        cfg: DecodeConfig,\n    ) -> impl DeserializeSeed<'de> + 'a {\n        SchemaDeserializer { cfg, schema: self }\n    }\n\n    pub fn deserialize<'de, T>(\n        &self,\n        de: T,\n        cfg: DecodeConfig,\n    ) -> Result<PValues, serde_path_to_error::Error<T::Error>>\n    where\n        T: Deserializer<'de>,\n    {\n        let seed = SchemaDeserializer { cfg, schema: self };\n        let mut track = serde_path_to_error::Track::new();\n        let de = serde_path_to_error::Deserializer::new(de, &mut track);\n        match seed.deserialize(de) {\n            Ok(t) => Ok(t),\n            Err(err) => Err(serde_path_to_error::Error::new(track.path(), err)),\n        }\n    }\n\n    pub fn null() -> Self {\n        JSONSchema {\n            registry: Arc::new(Registry {\n                values: vec![Value::Basic(Basic::Null)],\n            }),\n            root: 0,\n        }\n    }\n\n    pub fn any() -> Self {\n        JSONSchema {\n            registry: Arc::new(Registry {\n                values: vec![Value::Basic(Basic::Any)],\n            }),\n            root: 0,\n        }\n    }\n}\n\nimpl fmt::Debug for JSONSchema {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self.registry.values.get(self.root) {\n            Some(v) => v.write_debug(&self.registry, f),\n            None => write!(f, \"Ref({})\", self.root),\n        }\n    }\n}\n\nimpl Value {\n    fn write_debug(&self, reg: &Registry, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Value::Basic(b) => write!(f, \"{b:?}\"),\n            Value::Struct(Struct { fields }) => {\n                f.debug_struct(\"Struct\").field(\"fields\", &fields).finish()\n            }\n            Value::Option(v) => f.debug_struct(\"Option\").field(\"value\", &v).finish(),\n            Value::Array(v) => f.debug_struct(\"Array\").field(\"value\", &v).finish(),\n            Value::Map(v) => f.debug_struct(\"Map\").field(\"value\", &v).finish(),\n            Value::Union(v) => f.debug_struct(\"Union\").field(\"types\", &v).finish(),\n            Value::Literal(v) => f.debug_struct(\"Literal\").field(\"value\", &v).finish(),\n            Value::Ref(idx) => match reg.values.get(*idx) {\n                Some(v) => v.write_debug(reg, f),\n                None => write!(f, \"Ref({idx})\"),\n            },\n            Value::Validation(v) => f\n                .debug_struct(\"Validation\")\n                .field(\"bov\", &v.bov)\n                .field(\"expr\", &v.expr)\n                .finish(),\n        }\n    }\n}\n\nimpl<'de: 'a, 'a> DeserializeSeed<'de> for SchemaDeserializer<'a> {\n    type Value = PValues;\n\n    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        let visitor = DecodeValue {\n            cfg: &self.cfg,\n            reg: &self.schema.registry,\n            value: &self.schema.registry.values[self.schema.root],\n        };\n        let value = deserializer.deserialize_any(visitor)?;\n        match value {\n            PValue::Object(map) => Ok(map),\n            _ => Err(serde::de::Error::custom(\"expected object\")),\n        }\n    }\n}\n\npub struct SchemaDeserializer<'a> {\n    cfg: DecodeConfig,\n    schema: &'a JSONSchema,\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::api::jsonschema::de::*;\n    use std::collections::HashMap;\n\n    #[test]\n    fn test() {\n        let reg = Arc::new(Registry {\n            values: vec![\n                Value::Struct(Struct {\n                    fields: {\n                        let mut fields = HashMap::new();\n                        fields.insert(\n                            \"bar\".to_string(),\n                            Field {\n                                value: BasicOrValue::Value(1),\n                                optional: false,\n                                name_override: None,\n                            },\n                        );\n                        fields\n                    },\n                }),\n                Value::Option(BasicOrValue::Value(2)),\n                Value::Basic(Basic::Any),\n            ],\n        });\n\n        let schema = JSONSchema {\n            registry: reg.clone(),\n            root: 0,\n        };\n\n        let str = r#\"{\"foo\": \"bar\", \"blah\": \"baz\"}\"#;\n        let mut jsonde = serde_json::Deserializer::from_str(str);\n        let res = schema.deserialize(&mut jsonde, DecodeConfig::default());\n        println!(\"{res:?}\");\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/jsonschema/parse.rs",
    "content": "use crate::api::jsonschema::{Basic, BasicOrValue, JSONSchema, Registry, Struct, Value};\nuse crate::api::{self, Cookie, PValue, PValues, SameSite};\nuse crate::api::{schema, APIResult};\nuse schema::ToHeaderStr;\n\nuse std::str::FromStr;\n\nuse crate::api::jsonschema::de::Literal;\n\npub trait ParseWithSchema<Output> {\n    fn parse_with_schema(self, schema: &JSONSchema) -> APIResult<Output>;\n}\n\nmacro_rules! header_to_str {\n    ($header_value:expr) => {\n        $header_value.to_str().map_err(|err| api::Error {\n            code: api::ErrCode::InvalidArgument,\n            message: \"invalid header value\".to_string(),\n            internal_message: Some(format!(\"invalid header value: {}\", err)),\n            stack: None,\n            details: None,\n        })\n    };\n}\n\nimpl<H> ParseWithSchema<PValues> for H\nwhere\n    H: schema::HTTPHeaders,\n{\n    fn parse_with_schema(self, schema: &JSONSchema) -> APIResult<PValues> {\n        let mut result = PValues::new();\n        let reg = schema.registry.as_ref();\n\n        for (field_key, field) in schema.root().fields.iter() {\n            let header_name = field.name_override.as_deref().unwrap_or(field_key.as_str());\n            let mut values = self.get_all(header_name);\n            let Some(header_value) = values.next() else {\n                if field.optional {\n                    continue;\n                } else {\n                    return Err(api::Error {\n                        code: api::ErrCode::InvalidArgument,\n                        message: format!(\"missing required header: {header_name}\"),\n                        internal_message: None,\n                        stack: None,\n                        details: None,\n                    });\n                }\n            };\n\n            result.insert(\n                field_key.clone(),\n                match &field.value {\n                    BasicOrValue::Basic(basic) => {\n                        let basic = Value::Basic(*basic);\n\n                        parse_header_value(header_to_str!(header_value)?, reg, &basic)?\n                    }\n                    BasicOrValue::Value(idx) => {\n                        // Determine the type of the value(s).\n                        let basic_val: Value; // for borrowing below\n                        let (value_type, is_array) = match reg.get(*idx) {\n                            Value::Array(bov) => (\n                                match bov {\n                                    BasicOrValue::Value(idx) => reg.get(*idx),\n                                    BasicOrValue::Basic(basic) => {\n                                        basic_val = Value::Basic(*basic);\n                                        &basic_val\n                                    }\n                                },\n                                true,\n                            ),\n                            val => (val, false),\n                        };\n\n                        if is_array {\n                            let values = std::iter::once(header_value).chain(values);\n                            let mut arr = Vec::new();\n                            for header_value in values {\n                                let value = parse_header_value(\n                                    header_to_str!(header_value)?,\n                                    reg,\n                                    value_type,\n                                )?;\n                                arr.push(value);\n                            }\n                            PValue::Array(arr)\n                        } else {\n                            parse_header_value(header_to_str!(header_value)?, reg, value_type)?\n                        }\n                    }\n                },\n            );\n        }\n\n        Ok(result)\n    }\n}\n\n#[derive(Clone, Copy)]\nenum ValueType {\n    Header,\n    Cookie,\n}\n\nimpl ValueType {\n    fn error_message(&self) -> &'static str {\n        match self {\n            ValueType::Header => \"invalid header value\",\n            ValueType::Cookie => \"invalid cookie value\",\n        }\n    }\n}\n\nfn parse_str_value(\n    value_str: &str,\n    reg: &Registry,\n    schema: &Value,\n    value_type: ValueType,\n) -> APIResult<PValue> {\n    match schema {\n        // Recurse\n        Value::Ref(idx) => parse_str_value(value_str, reg, &reg.values[*idx], value_type),\n\n        Value::Validation(v) => {\n            let inner = match &v.bov {\n                BasicOrValue::Basic(basic) => parse_basic_str(basic, value_str),\n                BasicOrValue::Value(idx) => {\n                    parse_str_value(value_str, reg, &reg.values[*idx], value_type)\n                }\n            }?;\n            match v.validate_pval(&inner) {\n                Ok(()) => Ok(inner),\n                Err(err) => Err(api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: value_type.error_message().to_string(),\n                    internal_message: Some(format!(\"{}: {}\", value_type.error_message(), err)),\n                    stack: None,\n                    details: None,\n                }),\n            }\n        }\n\n        // If we have an empty value for an option, that's fine.\n        Value::Option(_) if value_str.is_empty() => Ok(PValue::Null),\n\n        // Otherwise recurse.\n        Value::Option(opt) => match opt {\n            BasicOrValue::Basic(basic) => parse_basic_str(basic, value_str),\n            BasicOrValue::Value(idx) => {\n                parse_str_value(value_str, reg, &reg.values[*idx], value_type)\n            }\n        },\n\n        Value::Basic(basic) => parse_basic_str(basic, value_str),\n\n        Value::Struct { .. } | Value::Map(_) | Value::Array(_) => unsupported(reg, schema),\n\n        Value::Literal(lit) => match lit {\n            Literal::Str(want) if value_str == want => Ok(PValue::String(want.to_string())),\n            Literal::Bool(true) if value_str == \"true\" => Ok(PValue::Bool(true)),\n            Literal::Bool(false) if value_str == \"false\" => Ok(PValue::Bool(false)),\n            Literal::Int(want) if value_str.parse() == Ok(*want) => {\n                Ok(PValue::Number(serde_json::Number::from(*want)))\n            }\n            Literal::Float(want) if value_str.parse() == Ok(*want) => {\n                if let Some(num) = serde_json::Number::from_f64(*want) {\n                    Ok(PValue::Number(num))\n                } else {\n                    Err(api::Error {\n                        code: api::ErrCode::InvalidArgument,\n                        message: value_type.error_message().to_string(),\n                        internal_message: Some(format!(\"invalid float value: {value_str}\")),\n                        stack: None,\n                        details: None,\n                    })\n                }\n            }\n\n            want => Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: value_type.error_message().to_string(),\n                internal_message: Some(format!(\"expected {}, got {}\", want.expecting(), value_str)),\n                stack: None,\n                details: None,\n            }),\n        },\n\n        Value::Union(union) => {\n            // Find the first value that matches.\n            for value in union {\n                let result = match value {\n                    BasicOrValue::Basic(basic) => parse_basic_str(basic, value_str),\n                    BasicOrValue::Value(idx) => {\n                        let value = reg.get(*idx);\n                        parse_str_value(value_str, reg, value, value_type)\n                    }\n                };\n                match result {\n                    Ok(value) => return Ok(value),\n                    Err(_) => continue,\n                }\n            }\n            Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: value_type.error_message().to_string(),\n                internal_message: Some(format!(\"no union value matched: {value_str}\")),\n                stack: None,\n                details: None,\n            })\n        }\n    }\n}\n\nfn parse_header_value(header: &str, reg: &Registry, schema: &Value) -> APIResult<PValue> {\n    parse_str_value(header, reg, schema, ValueType::Header)\n}\n\nfn parse_cookie_value(cookie_value: &str, reg: &Registry, schema: &Value) -> APIResult<PValue> {\n    parse_str_value(cookie_value, reg, schema, ValueType::Cookie)\n}\n\nimpl ParseWithSchema<PValue> for PValue {\n    fn parse_with_schema(self, schema: &JSONSchema) -> APIResult<PValue> {\n        let reg = schema.registry.as_ref();\n        let fields = &schema.root().fields;\n        match self {\n            PValue::Object(obj) => {\n                let mut result = PValues::new();\n                for (key, value) in obj {\n                    let value = match fields.get(&key) {\n                        // Not known to schema; pass it unmodified.\n                        None => value,\n\n                        Some(field) => match &field.value {\n                            BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, value)?,\n\n                            BasicOrValue::Value(idx) => {\n                                parse_json_value(value, reg, &reg.values[*idx])?\n                            }\n                        },\n                    };\n                    result.insert(key, value);\n                }\n                Ok(PValue::Object(result))\n            }\n\n            _ => unexpected_json(reg, schema.root_value(), &self),\n        }\n    }\n}\n\nimpl ParseWithSchema<PValues> for cookie::CookieJar {\n    fn parse_with_schema(self, schema: &JSONSchema) -> APIResult<PValues> {\n        let mut result = PValues::new();\n        let reg = schema.registry.as_ref();\n\n        for (field_key, field) in schema.root().fields.iter() {\n            let name = field.name_override.as_deref().unwrap_or(field_key.as_str());\n            let Some(cookie) = self.get(name) else {\n                if field.optional {\n                    continue;\n                } else {\n                    return Err(api::Error {\n                        code: api::ErrCode::InvalidArgument,\n                        message: format!(\"missing required cookie: {name}\"),\n                        internal_message: None,\n                        stack: None,\n                        details: None,\n                    });\n                }\n            };\n\n            let cookie_value = match field.value {\n                BasicOrValue::Basic(basic) => parse_basic_str(&basic, cookie.value())?,\n                BasicOrValue::Value(idx) => parse_cookie_value(cookie.value(), reg, reg.get(idx))?,\n            };\n\n            let cookie = Cookie {\n                name: name.to_string(),\n                value: Box::new(cookie_value),\n                path: cookie.path().map(|s| s.to_string()),\n                domain: cookie.domain().map(|s| s.to_string()),\n                secure: cookie.secure(),\n                http_only: cookie.http_only(),\n                expires: cookie.expires_datetime().map(|dt| {\n                    let system_time: std::time::SystemTime = dt.into();\n                    let utc: chrono::DateTime<chrono::Utc> = chrono::DateTime::from(system_time);\n                    utc.into()\n                }),\n                max_age: cookie\n                    .max_age()\n                    .map(|duration| duration.whole_seconds() as u64),\n                same_site: cookie.same_site().map(|ss| match ss {\n                    cookie::SameSite::Strict => SameSite::Strict,\n                    cookie::SameSite::Lax => SameSite::Lax,\n                    cookie::SameSite::None => SameSite::None,\n                }),\n                partitioned: cookie.partitioned(),\n            };\n\n            result.insert(field_key.clone(), PValue::Cookie(cookie));\n        }\n\n        Ok(result)\n    }\n}\n\n#[cfg_attr(\n    feature = \"rttrace\",\n    tracing::instrument(skip(reg), ret, level = \"trace\")\n)]\nfn parse_json_value(this: PValue, reg: &Registry, schema: &Value) -> APIResult<PValue> {\n    match schema {\n        // Recurse\n        Value::Ref(idx) => parse_json_value(this, reg, &reg.values[*idx]),\n\n        Value::Validation(v) => {\n            let inner = match &v.bov {\n                BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, this),\n                BasicOrValue::Value(idx) => parse_json_value(this, reg, &reg.values[*idx]),\n            }?;\n            match v.validate_pval(&inner) {\n                Ok(()) => Ok(inner),\n                Err(err) => Err(api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: err.to_string(),\n                    internal_message: None,\n                    stack: None,\n                    details: None,\n                }),\n            }\n        }\n\n        // If we have a null value for an option, that's fine.\n        Value::Option(_) if this.is_null() => Ok(PValue::Null),\n\n        // Otherwise recurse.\n        Value::Option(opt) => match opt {\n            BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, this),\n            BasicOrValue::Value(idx) => parse_json_value(this, reg, &reg.values[*idx]),\n        },\n\n        Value::Basic(basic) => parse_basic_json(reg, basic, this),\n\n        Value::Literal(lit) => {\n            let invalid = |got| {\n                Err(api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: \"invalid value\".to_string(),\n                    internal_message: Some(format!(\"expected {}, got {:#?}\", lit.expecting(), got)),\n                    stack: None,\n                    details: None,\n                })\n            };\n\n            match (this, lit) {\n                (PValue::String(got), Literal::Str(want)) if &got == want => {\n                    Ok(PValue::String(got))\n                }\n                (PValue::Bool(got), Literal::Bool(want)) if &got == want => Ok(PValue::Bool(got)),\n                (PValue::Number(got), Literal::Int(want)) => {\n                    if got.as_i64() == Some(*want) {\n                        Ok(PValue::Number(got))\n                    } else {\n                        invalid(PValue::Number(got))\n                    }\n                }\n                (PValue::Number(got), Literal::Float(want)) => {\n                    if got.as_f64() == Some(*want) {\n                        Ok(PValue::Number(got))\n                    } else {\n                        invalid(PValue::Number(got))\n                    }\n                }\n                (got, _) => invalid(got),\n            }\n        }\n\n        Value::Struct(Struct { fields }) => match this {\n            PValue::Object(obj) => {\n                let mut result = PValues::new();\n                for (key, value) in obj {\n                    let value = match fields.get(&key) {\n                        // Not known to schema; pass it unmodified.\n                        None => value,\n\n                        Some(field) => match &field.value {\n                            BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, value)?,\n\n                            BasicOrValue::Value(idx) => {\n                                parse_json_value(value, reg, &reg.values[*idx])?\n                            }\n                        },\n                    };\n                    result.insert(key, value);\n                }\n                Ok(PValue::Object(result))\n            }\n\n            _ => unexpected_json(reg, schema, &this),\n        },\n\n        Value::Map(value_type) => match this {\n            PValue::Object(obj) => {\n                let mut result = PValues::new();\n                for (key, value) in obj {\n                    let value = match value_type {\n                        BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, value)?,\n                        BasicOrValue::Value(idx) => {\n                            parse_json_value(value, reg, &reg.values[*idx])?\n                        }\n                    };\n                    result.insert(key, value);\n                }\n                Ok(PValue::Object(result))\n            }\n\n            _ => unexpected_json(reg, schema, &this),\n        },\n\n        Value::Array(value_type) => match this {\n            PValue::Array(arr) => {\n                let mut result = Vec::with_capacity(arr.len());\n                for val in arr {\n                    let value = match value_type {\n                        BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, val)?,\n                        BasicOrValue::Value(idx) => parse_json_value(val, reg, &reg.values[*idx])?,\n                    };\n                    result.push(value);\n                }\n                Ok(PValue::Array(result))\n            }\n\n            _ => unexpected_json(reg, schema, &this),\n        },\n\n        Value::Union(types) => {\n            // Find the first type that matches.\n            for candidate in types {\n                let result = match candidate {\n                    BasicOrValue::Basic(basic) => parse_basic_json(reg, basic, this.clone()),\n                    BasicOrValue::Value(idx) => {\n                        parse_json_value(this.clone(), reg, &reg.values[*idx])\n                    }\n                };\n                if let Ok(value) = result {\n                    return Ok(value);\n                }\n            }\n\n            // Couldn't find a match.\n            Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"invalid value\".to_string(),\n                internal_message: Some(format!(\"no union type matched: {}\", describe_json(&this),)),\n                stack: None,\n                details: None,\n            })\n        }\n    }\n}\n\nfn unexpected_json(reg: &Registry, schema: &Value, value: &PValue) -> APIResult<PValue> {\n    Err(api::Error {\n        code: api::ErrCode::InvalidArgument,\n        message: \"invalid value\".to_string(),\n        internal_message: Some(format!(\n            \"expected {}, got {}\",\n            schema.expecting(reg),\n            describe_json(value),\n        )),\n        stack: None,\n        details: None,\n    })\n}\n\nfn unsupported<T>(reg: &Registry, schema: &Value) -> APIResult<T> {\n    Err(api::Error {\n        code: api::ErrCode::InvalidArgument,\n        message: \"unsupported schema type\".to_string(),\n        internal_message: Some(format!(\n            \"got an unsupported schema type: {}\",\n            schema.expecting(reg),\n        )),\n        stack: None,\n        details: None,\n    })\n}\n\nfn describe_json(value: &PValue) -> &'static str {\n    match value {\n        PValue::Null => \"null\",\n        PValue::Bool(_) => \"a boolean\",\n        PValue::Number(_) => \"a number\",\n        PValue::Decimal(_) => \"a decimal\",\n        PValue::String(_) => \"a string\",\n        PValue::DateTime(_) => \"a datetime\",\n        PValue::Array(_) => \"an array\",\n        PValue::Object(_) => \"an object\",\n        PValue::Cookie(_) => \"a cookie\",\n    }\n}\n\nfn parse_basic_json(reg: &Registry, basic: &Basic, value: PValue) -> APIResult<PValue> {\n    match (basic, &value) {\n        (Basic::Any, _) => Ok(value),\n\n        (Basic::Null, PValue::Null) => Ok(value),\n        (Basic::Bool, PValue::Bool(_)) => Ok(value),\n        (Basic::Number, PValue::Number(_)) => Ok(value),\n        (Basic::String, PValue::String(_)) => Ok(value),\n\n        (Basic::String, PValue::Number(num)) => Ok(PValue::String(num.to_string())),\n        (Basic::String, PValue::Bool(bool)) => Ok(PValue::String(bool.to_string())),\n\n        (_, PValue::String(str)) => match basic {\n            Basic::Bool => match str.as_str() {\n                \"true\" => Ok(PValue::Bool(true)),\n                \"false\" => Ok(PValue::Bool(false)),\n                _ => Err(api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: format!(\"invalid boolean value: {str}\"),\n                    internal_message: None,\n                    stack: None,\n                    details: None,\n                }),\n            },\n            Basic::Number => serde_json::Number::from_str(str)\n                .map(PValue::Number)\n                .map_err(|_err| api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: format!(\"invalid number value: {str}\"),\n                    internal_message: None,\n                    stack: None,\n                    details: None,\n                }),\n            Basic::Null if str == \"null\" => Ok(PValue::Null),\n\n            _ => unexpected_json(reg, &Value::Basic(*basic), &value),\n        },\n\n        _ => unexpected_json(reg, &Value::Basic(*basic), &value),\n    }\n}\n\nfn parse_basic_str(basic: &Basic, str: &str) -> APIResult<PValue> {\n    match basic {\n        Basic::Any | Basic::String => Ok(PValue::String(str.to_string())),\n\n        Basic::Null if str.is_empty() || str == \"null\" => Ok(PValue::Null),\n        Basic::Null => Err(api::Error {\n            code: api::ErrCode::InvalidArgument,\n            message: \"invalid value\".to_string(),\n            internal_message: Some(format!(\"expected {}, got {:#?}\", basic.expecting(), str)),\n            stack: None,\n            details: None,\n        }),\n\n        Basic::Bool => match str {\n            \"true\" => Ok(PValue::Bool(true)),\n            \"false\" => Ok(PValue::Bool(false)),\n            _ => Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: format!(\"invalid boolean value: {str}\"),\n                internal_message: None,\n                stack: None,\n                details: None,\n            }),\n        },\n\n        Basic::Number => serde_json::Number::from_str(str)\n            .map(PValue::Number)\n            .map_err(|_err| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: format!(\"invalid number value: {str}\"),\n                internal_message: None,\n                stack: None,\n                details: None,\n            }),\n\n        Basic::DateTime => api::DateTime::parse_from_rfc3339(str)\n            .map(PValue::DateTime)\n            .map_err(|_err| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"invalid datetime\".to_string(),\n                internal_message: Some(format!(\"invalid datetime string {str:?}\")),\n                stack: None,\n                details: None,\n            }),\n\n        Basic::Decimal => api::Decimal::from_str(str)\n            .map(PValue::Decimal)\n            .map_err(|_err| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: format!(\"invalid decimal value: {str}\"),\n                internal_message: None,\n                stack: None,\n                details: None,\n            }),\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/jsonschema/ser.rs",
    "content": "use crate::api::jsonschema::{JSONSchema, Struct};\nuse crate::api::schema::JSONPayload;\nuse serde::{ser::SerializeMap, Serialize, Serializer};\n\nstruct SchemaSerializeWrapper<'a> {\n    schema: &'a Struct,\n    payload: &'a JSONPayload,\n}\n\nimpl Serialize for SchemaSerializeWrapper<'_> {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        let mut map = serializer.serialize_map(None)?;\n        if let Some(payload) = self.payload {\n            for (key, _value) in self.schema.fields.iter() {\n                if let Some(value) = payload.get(key) {\n                    map.serialize_entry(key, value)?;\n                }\n            }\n        }\n        map.end()\n    }\n}\n\nimpl JSONSchema {\n    pub fn to_json(&self, payload: &JSONPayload) -> serde_json::Result<String> {\n        serde_json::to_string(&self.serialize(payload))\n    }\n\n    pub fn to_json_pretty(&self, payload: &JSONPayload) -> serde_json::Result<String> {\n        serde_json::to_string_pretty(&self.serialize(payload))\n    }\n\n    pub fn to_vec(&self, payload: &JSONPayload) -> serde_json::Result<Vec<u8>> {\n        serde_json::to_vec(&self.serialize(payload))\n    }\n\n    pub fn to_vec_pretty(&self, payload: &JSONPayload) -> serde_json::Result<Vec<u8>> {\n        serde_json::to_vec_pretty(&self.serialize(payload))\n    }\n\n    pub fn serialize<'a>(&'a self, payload: &'a JSONPayload) -> impl Serialize + 'a {\n        SchemaSerializeWrapper {\n            schema: self.root(),\n            payload,\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/jsonschema/validation.rs",
    "content": "use std::fmt::Display;\n\nuse crate::{\n    api::{Decimal, PValue},\n    encore::parser::schema::v1 as schema,\n};\nuse thiserror::Error;\n\nuse super::BasicOrValue;\n\n#[derive(Debug, Clone)]\npub struct Validation {\n    pub bov: BasicOrValue,\n    pub expr: Expr,\n}\n\nimpl Validation {\n    pub fn validate_pval<'a>(&'a self, val: &'a PValue) -> Result<(), Error<'a>> {\n        self.expr.validate_pval(val)\n    }\n\n    pub fn validate_jval<'a>(&'a self, val: &'a serde_json::Value) -> Result<(), Error<'a>> {\n        self.expr.validate_jval(val)\n    }\n}\n\n#[derive(Debug, Clone)]\npub enum Expr {\n    Rule(Rule),\n    And(Vec<Expr>),\n    Or(Vec<Expr>),\n}\n\nmacro_rules! impl_validate {\n    ($method:ident, $typ:ty) => {\n        pub fn $method<'a>(&'a self, val: &'a $typ) -> Result<(), Error<'a>> {\n            match self {\n                Expr::Rule(rule) => rule.$method(val),\n                Expr::And(exprs) => {\n                    for expr in exprs {\n                        expr.$method(val)?;\n                    }\n                    Ok(())\n                }\n                Expr::Or(exprs) => {\n                    let mut first_err = None;\n                    for expr in exprs {\n                        match expr.$method(val) {\n                            Ok(()) => return Ok(()),\n                            Err(err) => {\n                                if first_err.is_none() {\n                                    first_err = Some(err);\n                                }\n                            }\n                        }\n                    }\n                    match first_err {\n                        Some(err) => Err(err),\n                        None => Ok(()),\n                    }\n                }\n            }\n        }\n    };\n}\n\nimpl Expr {\n    impl_validate!(validate_pval, PValue);\n    impl_validate!(validate_jval, serde_json::Value);\n}\n\n#[derive(Debug, Clone)]\npub enum Rule {\n    MinLen(u64),\n    MaxLen(u64),\n    MinVal(f64),\n    MaxVal(f64),\n    StartsWith(String),\n    EndsWith(String),\n    MatchesRegexp(regex::Regex),\n    Is(Is),\n}\n\n#[derive(Debug, Clone)]\npub enum Is {\n    Email,\n    Url,\n}\n\n#[derive(Debug)]\npub enum Num<'a> {\n    Number(&'a serde_json::Number),\n    Decimal(&'a Decimal),\n}\n\nimpl<'a> Display for Num<'a> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Num::Number(number) => write!(f, \"{number}\"),\n            Num::Decimal(decimal) => write!(f, \"{decimal}\"),\n        }\n    }\n}\n\n#[derive(Error, Debug)]\npub enum Error<'a> {\n    #[error(\"length too short (got {got}, expected at least {min})\")]\n    MinLen { got: usize, min: usize },\n    #[error(\"length too long (got {got}, expected at most {max})\")]\n    MaxLen { got: usize, max: usize },\n\n    #[error(\"value must be at least {min} (got {got})\")]\n    MinVal { got: Num<'a>, min: f64 },\n    #[error(\"value must be at most {max} (got {got})\")]\n    MaxVal { got: Num<'a>, max: f64 },\n\n    #[error(\"value does not match the regexp {regexp:#?}\")]\n    MatchesRegexp { regexp: &'a str },\n\n    #[error(\"value does not start with {prefix:#?}\")]\n    StartsWith { prefix: &'a str },\n\n    #[error(\"value does not end with {suffix:#?}\")]\n    EndsWith { suffix: &'a str },\n\n    #[error(\"value is not {expected}\")]\n    Is { expected: &'a str },\n\n    #[error(\"unexpected type (expected {want})\")]\n    UnexpectedType { want: &'a str },\n}\n\nimpl Rule {\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self), ret, level = \"trace\")\n    )]\n    pub fn validate_pval<'a>(&'a self, val: &'a PValue) -> Result<(), Error<'a>> {\n        match self {\n            Rule::MinLen(min_len) => match val {\n                PValue::Array(arr) => {\n                    if arr.len() < *min_len as usize {\n                        Err(Error::MinLen {\n                            got: arr.len(),\n                            min: *min_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n                PValue::String(str) => {\n                    if str.len() < *min_len as usize {\n                        Err(Error::MinLen {\n                            got: str.len(),\n                            min: *min_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType {\n                    want: \"string or array\",\n                }),\n            },\n\n            Rule::MaxLen(max_len) => match val {\n                PValue::Array(arr) => {\n                    if arr.len() > *max_len as usize {\n                        Err(Error::MaxLen {\n                            got: arr.len(),\n                            max: *max_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n                PValue::String(str) => {\n                    if str.len() > *max_len as usize {\n                        Err(Error::MaxLen {\n                            got: str.len(),\n                            max: *max_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType {\n                    want: \"string or array\",\n                }),\n            },\n\n            Rule::MinVal(min_val) => match val {\n                PValue::Number(num) => {\n                    let bad = if num.is_i64() {\n                        num.as_i64().unwrap() < *min_val as i64\n                    } else if num.is_u64() {\n                        num.as_u64().unwrap() < *min_val as u64\n                    } else if num.is_f64() {\n                        num.as_f64().unwrap() < *min_val\n                    } else {\n                        return Err(Error::UnexpectedType { want: \"number\" });\n                    };\n                    if bad {\n                        Err(Error::MinVal {\n                            got: Num::Number(num),\n                            min: *min_val,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n                PValue::Decimal(d) => {\n                    let bad = d < *min_val;\n                    if bad {\n                        Err(Error::MinVal {\n                            got: Num::Decimal(d),\n                            min: *min_val,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"number\" }),\n            },\n\n            Rule::MaxVal(max_val) => match val {\n                PValue::Number(num) => {\n                    let bad = if num.is_i64() {\n                        num.as_i64().unwrap() > *max_val as i64\n                    } else if num.is_u64() {\n                        num.as_u64().unwrap() > *max_val as u64\n                    } else if num.is_f64() {\n                        num.as_f64().unwrap() > *max_val\n                    } else {\n                        return Err(Error::UnexpectedType { want: \"number\" });\n                    };\n                    if bad {\n                        Err(Error::MaxVal {\n                            got: Num::Number(num),\n                            max: *max_val,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n                PValue::Decimal(d) => {\n                    let bad = d > *max_val;\n                    if bad {\n                        Err(Error::MaxVal {\n                            got: Num::Decimal(d),\n                            max: *max_val,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"number\" }),\n            },\n\n            Rule::StartsWith(prefix) => match val {\n                PValue::String(str) => {\n                    if str.starts_with(prefix) {\n                        Ok(())\n                    } else {\n                        Err(Error::StartsWith { prefix })\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::EndsWith(suffix) => match val {\n                PValue::String(str) => {\n                    if str.ends_with(suffix) {\n                        Ok(())\n                    } else {\n                        Err(Error::EndsWith { suffix })\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::MatchesRegexp(re) => match val {\n                PValue::String(str) => {\n                    if re.is_match(str) {\n                        Ok(())\n                    } else {\n                        Err(Error::MatchesRegexp {\n                            regexp: re.as_str(),\n                        })\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::Is(Is::Email) => match val {\n                PValue::String(str) => {\n                    let email = email_address::EmailAddress::parse_with_options(\n                        str,\n                        email_address::Options::default().without_display_text(),\n                    );\n                    match email {\n                        Ok(_) => Ok(()),\n                        Err(_) => Err(Error::Is {\n                            expected: \"an email\",\n                        }),\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::Is(Is::Url) => match val {\n                PValue::String(str) => {\n                    let u = url::Url::parse(str);\n                    match u {\n                        Ok(_) => Ok(()),\n                        Err(_) => Err(Error::Is { expected: \"a url\" }),\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n        }\n    }\n\n    #[cfg_attr(\n        feature = \"rttrace\",\n        tracing::instrument(skip(self), ret, level = \"trace\")\n    )]\n    pub fn validate_jval<'a>(&'a self, val: &'a serde_json::Value) -> Result<(), Error<'a>> {\n        use serde_json::Value as JVal;\n\n        match self {\n            Rule::MinLen(min_len) => match val {\n                JVal::Array(arr) => {\n                    if arr.len() < *min_len as usize {\n                        Err(Error::MinLen {\n                            got: arr.len(),\n                            min: *min_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n                JVal::String(str) => {\n                    if str.len() < *min_len as usize {\n                        Err(Error::MinLen {\n                            got: str.len(),\n                            min: *min_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType {\n                    want: \"string or array\",\n                }),\n            },\n\n            Rule::MaxLen(max_len) => match val {\n                JVal::Array(arr) => {\n                    if arr.len() > *max_len as usize {\n                        Err(Error::MaxLen {\n                            got: arr.len(),\n                            max: *max_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n                JVal::String(str) => {\n                    if str.len() > *max_len as usize {\n                        Err(Error::MaxLen {\n                            got: str.len(),\n                            max: *max_len as usize,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType {\n                    want: \"string or array\",\n                }),\n            },\n\n            Rule::MinVal(min_val) => match val {\n                JVal::Number(num) => {\n                    let bad = if num.is_i64() {\n                        num.as_i64().unwrap() < *min_val as i64\n                    } else if num.is_u64() {\n                        num.as_u64().unwrap() < *min_val as u64\n                    } else if num.is_f64() {\n                        num.as_f64().unwrap() < *min_val\n                    } else {\n                        return Err(Error::UnexpectedType { want: \"number\" });\n                    };\n                    if bad {\n                        Err(Error::MinVal {\n                            got: Num::Number(num),\n                            min: *min_val,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"number\" }),\n            },\n\n            Rule::MaxVal(max_val) => match val {\n                JVal::Number(num) => {\n                    let bad = if num.is_i64() {\n                        num.as_i64().unwrap() > *max_val as i64\n                    } else if num.is_u64() {\n                        num.as_u64().unwrap() > *max_val as u64\n                    } else if num.is_f64() {\n                        num.as_f64().unwrap() > *max_val\n                    } else {\n                        return Err(Error::UnexpectedType { want: \"number\" });\n                    };\n                    if bad {\n                        Err(Error::MaxVal {\n                            got: Num::Number(num),\n                            max: *max_val,\n                        })\n                    } else {\n                        Ok(())\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"number\" }),\n            },\n\n            Rule::StartsWith(want) => match val {\n                JVal::String(got) => {\n                    if got.starts_with(got) {\n                        Ok(())\n                    } else {\n                        Err(Error::StartsWith { prefix: want })\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::EndsWith(want) => match val {\n                JVal::String(got) => {\n                    if got.ends_with(got) {\n                        Ok(())\n                    } else {\n                        Err(Error::EndsWith { suffix: want })\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::MatchesRegexp(re) => match val {\n                JVal::String(str) => {\n                    if re.is_match(str) {\n                        Ok(())\n                    } else {\n                        Err(Error::MatchesRegexp {\n                            regexp: re.as_str(),\n                        })\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::Is(Is::Email) => match val {\n                JVal::String(str) => {\n                    let email = email_address::EmailAddress::parse_with_options(\n                        str,\n                        email_address::Options::default().without_display_text(),\n                    );\n                    match email {\n                        Ok(_) => Ok(()),\n                        Err(_) => Err(Error::Is { expected: \"email\" }),\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n\n            Rule::Is(Is::Url) => match val {\n                JVal::String(str) => {\n                    let u = url::Url::parse(str);\n                    match u {\n                        Ok(_) => Ok(()),\n                        Err(_) => Err(Error::Is { expected: \"url\" }),\n                    }\n                }\n\n                _ => Err(Error::UnexpectedType { want: \"string\" }),\n            },\n        }\n    }\n}\n\nimpl TryFrom<&schema::ValidationExpr> for Expr {\n    type Error = anyhow::Error;\n\n    fn try_from(expr: &schema::ValidationExpr) -> Result<Self, Self::Error> {\n        let Some(expr) = &expr.expr else {\n            return Err(anyhow::anyhow!(\"missing expr\"));\n        };\n\n        use schema::validation_expr::Expr as PbExpr;\n\n        match expr {\n            PbExpr::Rule(rule) => Ok(Expr::Rule(rule.try_into()?)),\n            PbExpr::And(expr) => {\n                let mut and = Vec::new();\n                for expr in &expr.exprs {\n                    and.push(expr.try_into()?);\n                }\n                Ok(Expr::And(and))\n            }\n            PbExpr::Or(expr) => {\n                let mut or = Vec::new();\n                for expr in &expr.exprs {\n                    or.push(expr.try_into()?);\n                }\n                Ok(Expr::Or(or))\n            }\n        }\n    }\n}\n\nimpl TryFrom<&schema::ValidationRule> for Rule {\n    type Error = anyhow::Error;\n\n    fn try_from(rule: &schema::ValidationRule) -> Result<Self, Self::Error> {\n        let Some(rule) = &rule.rule else {\n            return Err(anyhow::anyhow!(\"missing validation rule\"));\n        };\n\n        use schema::validation_rule::Is as PbIs;\n        use schema::validation_rule::Rule as PbRule;\n        match rule {\n            PbRule::MinLen(val) => Ok(Rule::MinLen(*val)),\n            PbRule::MaxLen(val) => Ok(Rule::MaxLen(*val)),\n            PbRule::MinVal(val) => Ok(Rule::MinVal(*val)),\n            PbRule::MaxVal(val) => Ok(Rule::MaxVal(*val)),\n            PbRule::StartsWith(val) => Ok(Rule::StartsWith(val.clone())),\n            PbRule::EndsWith(val) => Ok(Rule::EndsWith(val.clone())),\n            PbRule::MatchesRegexp(val) => {\n                let re = regex::Regex::new(val)?;\n                Ok(Rule::MatchesRegexp(re))\n            }\n            PbRule::Is(is) => Ok(Rule::Is(match PbIs::try_from(*is)? {\n                PbIs::Unknown => anyhow::bail!(\"unknown 'is' rule\"),\n                PbIs::Email => Is::Email,\n                PbIs::Url => Is::Url,\n            })),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/manager.rs",
    "content": "use std::collections::HashMap;\nuse std::future::{Future, IntoFuture};\nuse std::sync::{Arc, Mutex};\n\nuse anyhow::Context;\n\nuse crate::api::auth::{LocalAuthHandler, RemoteAuthHandler};\nuse crate::api::call::ServiceRegistry;\nuse crate::api::gateway::Gateway;\nuse crate::api::http_server::HttpServer;\nuse crate::api::paths::Pather;\nuse crate::api::reqauth::platform;\nuse crate::api::schema::encoding::EncodingConfig;\nuse crate::api::schema::JSONPayload;\nuse crate::api::{\n    auth, cors, encore_routes, endpoints_from_meta, jsonschema, paths, reqauth, server, APIResult,\n    Endpoint, ToResponse,\n};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as runtime;\nuse crate::trace::Tracer;\nuse crate::{api, metrics, model, pubsub, secrets, EncoreName, EndpointName, Hosted};\n\nuse super::encore_routes::healthz;\nuse super::websocket_client::WebSocketClient;\nuse super::{PValues, ResponsePayload};\n\npub struct ManagerConfig<'a> {\n    pub meta: &'a meta::Data,\n    pub environment: &'a runtime::Environment,\n    pub gateways: Vec<runtime::Gateway>,\n    pub hosted_services: Vec<runtime::HostedService>,\n    pub hosted_gateway_rids: Vec<String>,\n    pub svc_auth_methods: Vec<runtime::ServiceAuth>,\n    pub deploy_id: String,\n    pub platform: &'a runtime::EncorePlatform,\n    pub secrets: &'a secrets::Manager,\n    pub service_discovery: runtime::ServiceDiscovery,\n    pub http_client: reqwest::Client,\n    pub tracer: Tracer,\n    pub platform_validator: Arc<platform::RequestValidator>,\n    pub pubsub_push_registry: pubsub::PushHandlerRegistry,\n    pub runtime: tokio::runtime::Handle,\n    pub testing: bool,\n    pub proxied_push_subs: HashMap<String, super::gateway::ProxiedPushSub>,\n    pub metrics: &'a metrics::Manager,\n}\n\npub struct Manager {\n    gateway_listen_addr: Option<String>,\n    api_listener: Mutex<Option<std::net::TcpListener>>,\n    service_registry: Arc<ServiceRegistry>,\n    healthz: healthz::Handler,\n    pubsub_push_registry: pubsub::PushHandlerRegistry,\n\n    api_server: Option<server::Server>,\n    runtime: tokio::runtime::Handle,\n\n    gateways: HashMap<EncoreName, Gateway>,\n    testing: bool,\n    metrics: metrics::Manager,\n}\n\nimpl ManagerConfig<'_> {\n    pub fn build(mut self) -> anyhow::Result<Manager> {\n        let gateway_listen_addr = if !self.hosted_gateway_rids.is_empty() {\n            // We have a gateway. Have the gateway listen on the provided listen_addr.\n            Some(listen_addr())\n        } else {\n            None\n        };\n\n        let api_listener = if !self.hosted_services.is_empty() {\n            // If we already have a gateway, it's listening on the externally provided listen addr.\n            // Use a random local port in that case.\n            let addr = if gateway_listen_addr.is_some() {\n                \"127.0.0.1:0\".to_string()\n            } else {\n                listen_addr()\n            };\n            let ln = std::net::TcpListener::bind(addr).context(\"unable to bind to port\")?;\n            Some(ln)\n        } else {\n            None\n        };\n\n        // Get the local address for use by the service registry\n        // for calling services hosted by this instance.\n        let own_api_address = match api_listener {\n            None => None,\n            Some(ref ln) => {\n                let addr = ln\n                    .local_addr()\n                    .context(\"unable to determine listen address\")?;\n                Some(addr)\n            }\n        };\n\n        let healthz_handler = encore_routes::healthz::Handler {\n            app_revision: self.meta.app_revision.clone(),\n            // Remove the \"roll_\" prefix from the deploy_id.\n            deploy_id: self\n                .deploy_id\n                .strip_prefix(\"roll_\")\n                .unwrap_or(&self.deploy_id)\n                .to_string(),\n        };\n\n        let hosted_services = Hosted::from_iter(self.hosted_services.into_iter().map(|s| s.name));\n        let (endpoints, hosted_endpoints) = endpoints_from_meta(self.meta, &hosted_services)\n            .context(\"unable to compute endpoints descriptions\")?;\n\n        let inbound_svc_auth = {\n            let mut entries = Vec::with_capacity(self.svc_auth_methods.len());\n            for auth in self.svc_auth_methods.drain(..) {\n                let auth_method =\n                    reqauth::service_auth_method(self.secrets, self.environment, auth)\n                        .context(\"unable to initialize service auth method\")?;\n                entries.push(auth_method);\n            }\n            entries\n        };\n\n        let service_registry = ServiceRegistry::new(\n            self.secrets,\n            endpoints.clone(),\n            self.environment,\n            self.service_discovery,\n            own_api_address\n                .as_ref()\n                .map(|addr| addr.to_string())\n                .as_deref(),\n            &inbound_svc_auth,\n            &hosted_services,\n            self.deploy_id.clone(),\n            self.http_client.clone(),\n            self.tracer.clone(),\n        )\n        .context(\"unable to create service registry\")?;\n        let service_registry = Arc::new(service_registry);\n\n        let gateways_by_rid: HashMap<String, runtime::Gateway> =\n            HashMap::from_iter(self.gateways.drain(..).map(|gw| (gw.rid.clone(), gw)));\n\n        let hosted_gateways: HashMap<&str, &runtime::Gateway> =\n            HashMap::from_iter(self.hosted_gateway_rids.iter().filter_map(|rid| {\n                gateways_by_rid\n                    .get(rid)\n                    .map(|gw| (gw.encore_name.as_str(), gw))\n            }));\n        let mut gateways = HashMap::new();\n        let routes = paths::compute(endpoints.values().map(|ep| RoutePerService(ep.to_owned())));\n\n        let mut auth_data_schemas = HashMap::new();\n        for gw in &self.meta.gateways {\n            let Some(gw_cfg) = hosted_gateways.get(gw.encore_name.as_str()) else {\n                continue;\n            };\n            let Some(cors_cfg) = &gw_cfg.cors else {\n                anyhow::bail!(\"missing CORS configuration for gateway {}\", gw.encore_name);\n            };\n\n            let auth_handler = build_auth_handler(\n                self.meta,\n                gw,\n                &service_registry,\n                self.http_client.clone(),\n                self.tracer.clone(),\n                self.metrics.registry(),\n            )\n            .context(\"unable to build authenticator\")?;\n\n            let meta_headers = cors::MetaHeaders::from_schema(&endpoints, auth_handler.as_ref());\n            let cors_config = cors::config(cors_cfg, meta_headers)\n                .context(\"failed to parse CORS configuration\")?;\n\n            auth_data_schemas.insert(\n                gw.encore_name.clone(),\n                auth_handler.as_ref().map(|ah| ah.auth_data().clone()),\n            );\n\n            gateways.insert(\n                gw.encore_name.clone().into(),\n                Gateway::new(\n                    gw.encore_name.clone().into(),\n                    service_registry.clone(),\n                    routes.clone(),\n                    auth_handler,\n                    cors_config,\n                    healthz_handler.clone(),\n                    own_api_address,\n                    self.proxied_push_subs.clone(),\n                    self.tracer.clone(),\n                    inbound_svc_auth.clone(),\n                )\n                .context(\"couldn't create gateway\")?,\n            );\n        }\n\n        let api_server = if !hosted_services.is_empty() {\n            let server = server::Server::new(\n                endpoints.clone(),\n                hosted_endpoints,\n                self.platform_validator,\n                inbound_svc_auth,\n                self.tracer.clone(),\n                auth_data_schemas,\n                Arc::clone(self.metrics.registry()),\n            )\n            .context(\"unable to create API server\")?;\n            Some(server)\n        } else {\n            None\n        };\n\n        Ok(Manager {\n            gateway_listen_addr,\n            api_listener: Mutex::new(api_listener),\n            service_registry,\n            api_server,\n            gateways,\n            pubsub_push_registry: self.pubsub_push_registry,\n            runtime: self.runtime,\n            healthz: healthz_handler,\n            testing: self.testing,\n            metrics: self.metrics.clone(),\n        })\n    }\n}\n\n#[derive(Debug)]\nstruct RoutePerService(Arc<Endpoint>);\n\nimpl Pather for RoutePerService {\n    type Key = EncoreName;\n    type Value = Arc<Endpoint>;\n\n    fn key(&self) -> Self::Key {\n        self.0.name.service().into()\n    }\n    fn value(&self) -> Self::Value {\n        self.0.clone()\n    }\n    fn path(&self) -> &meta::Path {\n        &self.0.path\n    }\n}\n\nfn build_auth_handler(\n    meta: &meta::Data,\n    gw: &meta::Gateway,\n    service_registry: &ServiceRegistry,\n    http_client: reqwest::Client,\n    tracer: Tracer,\n    metrics_registry: &Arc<metrics::Registry>,\n) -> anyhow::Result<Option<auth::Authenticator>> {\n    let Some(explicit) = &gw.explicit else {\n        return Ok(None);\n    };\n    let Some(auth) = &explicit.auth_handler else {\n        return Ok(None);\n    };\n\n    let auth_params = auth.params.as_ref().context(\"missing auth params\")?;\n    let mut builder = jsonschema::Builder::new(meta);\n    let schema = {\n        let mut cfg = EncodingConfig {\n            meta,\n            registry_builder: &mut builder,\n            default_loc: None,\n            rpc_path: None,\n            supports_body: false,\n            supports_query: true,\n            supports_header: true,\n            supports_path: false,\n            supports_http_status: false,\n        };\n\n        cfg.compute(auth_params)\n            .context(\"unable to compute auth handler schema\")?\n    };\n\n    let auth_data_schema_idx =\n        builder.register_type(auth.auth_data.as_ref().context(\"missing auth data\")?)?;\n\n    let registry = builder.build();\n    let schema = schema\n        .build(&registry)\n        .context(\"unable to build auth handler schema\")?;\n\n    // let is_local = hosted_services.contains(&explicit.service_name);\n    let is_local = true;\n    let name = EndpointName::new(explicit.service_name.clone(), auth.name.clone());\n    let requests_total =\n        metrics::requests_total_counter(metrics_registry, &explicit.service_name, &auth.name);\n\n    let auth_data = registry.schema(auth_data_schema_idx);\n    let auth_handler = if is_local {\n        auth::Authenticator::local(\n            schema.clone(),\n            auth_data,\n            LocalAuthHandler {\n                name,\n                schema,\n                handler: Default::default(),\n                tracer,\n                requests_total,\n            },\n        )?\n    } else {\n        auth::Authenticator::remote(\n            schema,\n            auth_data.clone(),\n            RemoteAuthHandler::new(name, service_registry, http_client, auth_data, tracer)?,\n        )?\n    };\n\n    Ok(Some(auth_handler))\n}\n\nimpl Manager {\n    pub fn gateway(&self, name: &EncoreName) -> Option<&Gateway> {\n        self.gateways.get(name)\n    }\n\n    pub fn server(&self) -> Option<&server::Server> {\n        self.api_server.as_ref()\n    }\n\n    pub fn call(\n        &self,\n        target: EndpointName,\n        data: JSONPayload,\n        source: Option<Arc<model::Request>>,\n        opts: Option<CallOpts>,\n    ) -> impl Future<Output = APIResult<ResponsePayload>> + 'static {\n        self.service_registry.api_call(target, data, source, opts)\n    }\n\n    pub fn endpoints(&self) -> &api::EndpointMap {\n        self.service_registry.endpoints()\n    }\n\n    pub fn metrics_registry(&self) -> &Arc<metrics::Registry> {\n        self.metrics.registry()\n    }\n\n    pub fn stream(\n        &self,\n        endpoint_name: EndpointName,\n        data: JSONPayload,\n        source: Option<Arc<model::Request>>,\n        opts: Option<api::CallOpts>,\n    ) -> impl Future<Output = APIResult<WebSocketClient>> + 'static {\n        self.service_registry\n            .connect_stream(endpoint_name, data, source, opts)\n    }\n\n    /// Starts serving the API.\n    pub fn start_serving(&self) -> tokio::task::JoinHandle<anyhow::Result<()>> {\n        let api = self.api_server.as_ref().map(|srv| srv.router());\n\n        async fn fallback(\n            req: axum::http::Request<axum::body::Body>,\n        ) -> axum::response::Response<axum::body::Body> {\n            api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"endpoint not found\".to_string(),\n                internal_message: Some(format!(\"no such endpoint exists: {}\", req.uri().path())),\n                stack: None,\n                details: None,\n            }\n            .to_response(None)\n        }\n\n        let encore_routes = encore_routes::Desc {\n            healthz: self.healthz.clone(),\n            push_registry: self.pubsub_push_registry.clone(),\n        }\n        .router();\n\n        let fallback = axum::Router::new().fallback(fallback);\n        let server = HttpServer::new(encore_routes, api, fallback);\n\n        let api_listener = self.api_listener.lock().unwrap().take();\n        let gateway_listener = self.gateway_listen_addr.clone();\n\n        // TODO handle multiple gateways\n        let gateway = self.gateways.values().next().cloned();\n        let testing = self.testing;\n\n        self.runtime.spawn(async move {\n            let gateway_parts = (gateway, gateway_listener);\n            let gateway_fut = match gateway_parts {\n                (Some(gw), Some(ref ln)) => {\n                    if !testing {\n                        log::debug!(addr=ln; \"gateway listening for incoming requests\");\n                        Some(gw.serve(ln))\n                    } else {\n                        // No need running the gateway in tests\n                        None\n                    }\n                },\n                (Some(_), None) => {\n                    ::log::error!(\"internal encore error: misconfigured api gateway (missing listener), skipping\");\n                    None\n                }\n                (None, Some(_)) => {\n                    ::log::error!(\"internal encore error: misconfigured api gateway (missing gateway config), skipping\");\n                    None\n                }\n                (None, None) => None,\n            };\n\n            let api_fut = match api_listener {\n                Some(ln) => {\n                    let addr = ln.local_addr().map(|addr| addr.to_string()).unwrap_or_default();\n                    log::debug!(addr = addr; \"api server listening for incoming requests\");\n\n                    ln\n                        .set_nonblocking(true)\n                        .context(\"unable to set nonblocking\")?;\n                    let axum_listener = tokio::net::TcpListener::from_std(ln)\n                        .context(\"unable to convert listener to tokio\")?;\n                    let fut = axum::serve(axum_listener, server).into_future();\n                    Some(fut)\n                }\n                None => None,\n            };\n\n            tokio::select! {\n                res = async { gateway_fut.unwrap().await }, if gateway_fut.is_some() => {\n                    res.context(\"serve gateway\").inspect_err(|err| log::error!(\"api gateway failed: {:?}\", err))?;\n                },\n                res = async { api_fut.unwrap().await }, if api_fut.is_some() => {\n                    res.context(\"serve api\").inspect_err(|err| log::error!(\"api server failed: {:?}\", err))?;\n                },\n                else => {\n                    // Nothing to serve.\n                    ::log::debug!(\"no api server or gateway to serve\");\n                }\n            };\n            Ok(())\n        })\n    }\n}\n\nfn listen_addr() -> String {\n    if let Ok(addr) = std::env::var(\"ENCORE_LISTEN_ADDR\") {\n        return addr;\n    }\n    if let Ok(port) = std::env::var(\"PORT\") {\n        return format!(\"0.0.0.0:{port}\");\n    }\n    \"0.0.0.0:8080\".to_string()\n}\n\n#[derive(Debug)]\npub struct CallOpts {\n    pub auth: Option<AuthOpts>,\n}\n\n#[derive(Debug)]\npub struct AuthOpts {\n    pub data: PValues,\n    pub user_id: String,\n}\n"
  },
  {
    "path": "runtimes/core/src/api/mod.rs",
    "content": "pub mod auth;\npub mod call;\nmod cors;\nmod encore_routes;\nmod endpoint;\nmod error;\npub mod gateway;\nmod http_server;\nmod httputil;\npub mod jsonschema;\nmod manager;\nmod paths;\nmod pvalue;\npub mod reqauth;\npub mod schema;\nmod server;\nmod static_assets;\npub mod websocket;\npub mod websocket_client;\n\npub use endpoint::*;\npub use error::*;\npub use manager::*;\npub use pvalue::*;\n"
  },
  {
    "path": "runtimes/core/src/api/paths.rs",
    "content": "use crate::encore::parser::meta::v1 as meta;\nuse serde::Serialize;\nuse std::collections::{HashMap, HashSet};\n\npub trait Pather {\n    type Key;\n    type Value;\n\n    fn key(&self) -> Self::Key;\n    fn value(&self) -> Self::Value;\n    fn path(&self) -> &meta::Path;\n}\n\n#[derive(Debug, Serialize, Clone)]\npub struct PathSet<K, V> {\n    pub main: HashMap<K, Vec<(V, Vec<String>)>>,\n    pub fallback: HashMap<K, Vec<(V, Vec<String>)>>,\n}\n\n/// Computes paths to register, grouped by the given key for easier correlation.\npub fn compute<P, K, V>(endpoints: impl Iterator<Item = P>) -> PathSet<K, V>\nwhere\n    P: Pather<Key = K, Value = V>,\n    K: Eq + Clone + std::hash::Hash,\n{\n    use crate::encore::parser::meta::v1::path_segment::SegmentType;\n\n    let mut main: HashMap<K, Vec<(V, Vec<String>)>> = HashMap::new();\n    let mut fallback: HashMap<K, Vec<(V, Vec<String>)>> = HashMap::new();\n    for ep in endpoints {\n        let path = ep.path();\n        let mut entries = Vec::with_capacity(2);\n\n        // Compute the axum path.\n        let mut result = String::new();\n        for seg in &path.segments {\n            let typ = SegmentType::try_from(seg.r#type).unwrap_or(SegmentType::Literal);\n            match typ {\n                SegmentType::Literal => {\n                    result.push('/');\n                    result.push_str(&seg.value)\n                }\n                SegmentType::Param => result.push_str(\"/:_\"),\n                SegmentType::Wildcard => {\n                    // The wildcard is the last segment.\n                    // Axum doesn't match e.g. \"/\" for \"/*wildcard\", so we need to register both.\n                    result.push('/');\n                    entries.push(result.clone());\n\n                    result.push_str(\"*_\");\n                }\n                SegmentType::Fallback => {\n                    // Axum doesn't match e.g. \"/\" for \"/*wildcard\", so we need to register both.\n                    result.push('/');\n                    entries.push(result.clone());\n\n                    result.push_str(\"*_\");\n                }\n            }\n        }\n        entries.push(result);\n\n        let is_fallback = path\n            .segments\n            .last()\n            .is_some_and(|seg| seg.r#type == SegmentType::Fallback as i32);\n\n        let key = ep.key();\n        let routes = (ep.value(), entries);\n        if is_fallback {\n            fallback.entry(key).or_default().push(routes);\n        } else {\n            main.entry(key).or_default().push(routes);\n        }\n    }\n\n    // Add paths for TSR redirects.\n    {\n        for paths in [&mut main, &mut fallback] {\n            let path_set: HashSet<String> = HashSet::from_iter(\n                paths\n                    .values()\n                    .flatten()\n                    .flat_map(|(_, routes)| routes.iter())\n                    .cloned(),\n            );\n            for entries in paths.values_mut() {\n                for (_, endpoint_routes) in entries {\n                    let mut tsr_routes = Vec::new();\n                    for route in endpoint_routes.iter() {\n                        // Is this entry incompatible with TSR?\n                        if route == \"/\" || route.contains(\"/*\") || route.ends_with('/') {\n                            continue;\n                        }\n\n                        let tsr = format!(\"{route}/\");\n                        if !path_set.contains(&tsr) {\n                            tsr_routes.push(tsr);\n                        }\n                    }\n                    endpoint_routes.extend(tsr_routes);\n                }\n            }\n        }\n    }\n\n    PathSet { main, fallback }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::encore::parser::meta::v1::path_segment::SegmentType;\n    use serde::ser::SerializeStruct;\n\n    #[test]\n    fn test_basic() {\n        let endpoints = vec![\n            ep(\"one\", \"a\", &[lit(\"foo\")]),\n            ep(\"two\", \"a\", &[lit(\"foo\"), lit(\"bar\")]),\n        ];\n\n        let paths = compute(endpoints.into_iter());\n        insta::with_settings!({sort_maps => true}, {\n            insta::assert_yaml_snapshot!(paths);\n        });\n    }\n\n    #[test]\n    fn test_tsr_conflict() {\n        let endpoints = vec![\n            ep(\"one\", \"a\", &[lit(\"foo\"), lit(\"\")]),\n            ep(\"two\", \"b\", &[lit(\"foo\")]),\n        ];\n\n        let paths = compute(endpoints.into_iter());\n        insta::with_settings!({sort_maps => true}, {\n            insta::assert_yaml_snapshot!(paths);\n        });\n    }\n\n    #[test]\n    fn test_wildcard() {\n        let endpoints = vec![ep(\"one\", \"a\", &[wildcard(\"foo\")])];\n\n        let paths = compute(endpoints.into_iter());\n        insta::with_settings!({sort_maps => true}, {\n            insta::assert_yaml_snapshot!(paths);\n        });\n    }\n\n    #[test]\n    fn test_fallback() {\n        let endpoints = vec![ep(\"one\", \"a\", &[fallback(\"foo\")])];\n\n        let paths = compute(endpoints.into_iter());\n        insta::with_settings!({sort_maps => true}, {\n            insta::assert_yaml_snapshot!(paths);\n        });\n    }\n\n    fn path(segs: &[meta::PathSegment]) -> meta::Path {\n        meta::Path {\n            segments: segs.to_vec(),\n            r#type: meta::path::Type::Url as i32,\n        }\n    }\n\n    fn seg(typ: SegmentType, value: &str) -> meta::PathSegment {\n        meta::PathSegment {\n            r#type: typ as i32,\n            value: value.to_string(),\n            value_type: meta::path_segment::ParamType::String as i32,\n            validation: None,\n        }\n    }\n\n    fn lit(str: &str) -> meta::PathSegment {\n        seg(SegmentType::Literal, str)\n    }\n\n    fn wildcard(str: &str) -> meta::PathSegment {\n        seg(SegmentType::Wildcard, str)\n    }\n\n    fn fallback(str: &str) -> meta::PathSegment {\n        seg(SegmentType::Fallback, str)\n    }\n\n    #[derive(Clone, Debug)]\n    struct TestEndpoint {\n        name: &'static str,\n        key: &'static str,\n        path: meta::Path,\n    }\n\n    impl Serialize for TestEndpoint {\n        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n        where\n            S: serde::Serializer,\n        {\n            let mut state = serializer.serialize_struct(\"TestEndpoint\", 2)?;\n            let path = path_to_str(&self.path);\n            state.serialize_field(\"key\", &self.key)?;\n            state.serialize_field(\"path\", &path)?;\n            state.end()\n        }\n    }\n\n    fn ep(name: &'static str, key: &'static str, segs: &[meta::PathSegment]) -> TestEndpoint {\n        TestEndpoint {\n            name,\n            key,\n            path: path(segs),\n        }\n    }\n\n    impl Pather for TestEndpoint {\n        type Key = String;\n        type Value = String;\n\n        fn key(&self) -> Self::Key {\n            self.key.to_string()\n        }\n        fn value(&self) -> Self::Value {\n            self.name.to_string()\n        }\n\n        fn path(&self) -> &meta::Path {\n            &self.path\n        }\n    }\n\n    fn path_to_str(path: &meta::Path) -> String {\n        let mut result = String::new();\n        for seg in &path.segments {\n            result.push('/');\n\n            use meta::path_segment::SegmentType;\n            match SegmentType::try_from(seg.r#type).unwrap() {\n                SegmentType::Literal => result.push_str(&seg.value),\n                SegmentType::Param => {\n                    result.push(':');\n                    result.push_str(&seg.value)\n                }\n                SegmentType::Wildcard | SegmentType::Fallback => {\n                    result.push('*');\n                    result.push_str(&seg.value)\n                }\n            }\n        }\n\n        result\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/pvalue.rs",
    "content": "use std::{\n    collections::BTreeMap,\n    fmt::{Debug, Display},\n    ops::{Add, Div, Mul, Sub},\n    str::FromStr,\n};\n\nuse bytes::BytesMut;\nuse malachite::rational::{conversion::primitive_int_from_rational, Rational};\nuse malachite::{\n    base::num::conversion::{\n        string::options::ToSciOptions,\n        traits::{FromSciString, ToSci},\n    },\n    rational::conversion::primitive_float_from_rational,\n};\nuse serde::{Serialize, Serializer};\n\nuse crate::sqldb;\n\n/// Represents any valid value in a request/response payload.\n///\n/// It is a more type-safe version of JSON, where we support additional\n/// semantic types like timestamps.\n#[derive(Clone, Eq, PartialEq, Debug)]\npub enum PValue {\n    /// Represents a JSON null value.\n    Null,\n\n    /// Represents a JSON boolean.\n    Bool(bool),\n\n    /// Represents a JSON number, whether integer or floating point.\n    Number(serde_json::Number),\n\n    /// Represents a Decimal type with arbitrary precision.\n    Decimal(Decimal),\n\n    /// Represents a JSON string.\n    String(String),\n\n    /// Represents a JSON array.\n    Array(Vec<PValue>),\n\n    /// Represents a JSON object.\n    Object(PValues),\n\n    // Represents a datetime value.\n    DateTime(DateTime),\n\n    // Represents a cookie.\n    Cookie(Cookie),\n}\n\nimpl PValue {\n    pub fn is_null(&self) -> bool {\n        matches!(self, PValue::Null)\n    }\n\n    pub fn is_array(&self) -> bool {\n        matches!(self, PValue::Array(..))\n    }\n\n    /// If the `PValue` is a String, returns the associated str.\n    /// Returns None otherwise.\n    pub fn as_str(&self) -> Option<&str> {\n        match self {\n            PValue::String(s) => Some(s),\n            _ => None,\n        }\n    }\n\n    pub fn type_name(&self) -> &'static str {\n        match self {\n            PValue::Null => \"null\",\n            PValue::Bool(_) => \"boolean\",\n            PValue::Number(_) => \"number\",\n            PValue::String(_) => \"string\",\n            PValue::Array(_) => \"array\",\n            PValue::Object(_) => \"object\",\n            PValue::DateTime(_) => \"datetime\",\n            PValue::Cookie(_) => \"cookie\",\n            PValue::Decimal(_) => \"decimal\",\n        }\n    }\n}\n\npub type PValues = BTreeMap<String, PValue>;\n\npub type DateTime = chrono::DateTime<chrono::FixedOffset>;\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Cookie {\n    pub name: String,\n    pub value: Box<PValue>,\n    pub path: Option<String>,\n    pub domain: Option<String>,\n    pub secure: Option<bool>,\n    pub http_only: Option<bool>,\n    pub expires: Option<DateTime>,\n    pub max_age: Option<u64>,\n    pub same_site: Option<SameSite>,\n    pub partitioned: Option<bool>,\n}\n\nimpl<'a> From<&'a Cookie> for cookie::Cookie<'a> {\n    fn from(value: &'a Cookie) -> Self {\n        let mut builder = cookie::CookieBuilder::new(&value.name, value.value.to_string());\n        if let Some(path) = &value.path {\n            builder = builder.path(path);\n        }\n        if let Some(domain) = &value.domain {\n            builder = builder.domain(domain);\n        }\n        if let Some(secure) = &value.secure {\n            builder = builder.secure(*secure);\n        }\n        if let Some(http_only) = &value.http_only {\n            builder = builder.http_only(*http_only);\n        }\n        if let Some(expires) = &value.expires {\n            let system_time: std::time::SystemTime = (*expires).into();\n            let expire = cookie::time::OffsetDateTime::from(system_time);\n            builder = builder.expires(expire);\n        }\n        if let Some(max_age) = &value.max_age {\n            builder = builder.max_age(cookie::time::Duration::seconds(*max_age as i64));\n        }\n        if let Some(same_site) = &value.same_site {\n            let same_site = match same_site {\n                SameSite::Strict => cookie::SameSite::Strict,\n                SameSite::Lax => cookie::SameSite::Lax,\n                SameSite::None => cookie::SameSite::None,\n            };\n            builder = builder.same_site(same_site);\n        }\n        if let Some(partitioned) = &value.partitioned {\n            builder = builder.partitioned(*partitioned);\n        }\n\n        builder.build()\n    }\n}\n\nimpl Display for Cookie {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let c: cookie::Cookie<'_> = self.into();\n        write!(f, \"{c}\")\n    }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum SameSite {\n    Strict,\n    Lax,\n    None,\n}\n\nimpl Display for SameSite {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            SameSite::Strict => write!(f, \"Strict\"),\n            SameSite::Lax => write!(f, \"Lax\"),\n            SameSite::None => write!(f, \"None\"),\n        }\n    }\n}\n\n#[derive(Clone, Hash, Eq, PartialEq, Debug)]\npub struct Decimal(Rational);\n\nimpl Add for &Decimal {\n    type Output = Decimal;\n\n    fn add(self, rhs: Self) -> Self::Output {\n        Decimal((&self.0).add(&rhs.0))\n    }\n}\nimpl Sub for &Decimal {\n    type Output = Decimal;\n\n    fn sub(self, rhs: Self) -> Self::Output {\n        Decimal((&self.0).sub(&rhs.0))\n    }\n}\nimpl Mul for &Decimal {\n    type Output = Decimal;\n\n    fn mul(self, rhs: Self) -> Self::Output {\n        Decimal((&self.0).mul(&rhs.0))\n    }\n}\nimpl Div for &Decimal {\n    type Output = Decimal;\n\n    fn div(self, rhs: Self) -> Self::Output {\n        Decimal((&self.0).div(&rhs.0))\n    }\n}\n\nimpl TryFrom<&Decimal> for i64 {\n    type Error = primitive_int_from_rational::SignedFromRationalError;\n\n    fn try_from(value: &Decimal) -> Result<Self, Self::Error> {\n        i64::try_from(&value.0)\n    }\n}\n\nimpl TryFrom<&Decimal> for i32 {\n    type Error = primitive_int_from_rational::SignedFromRationalError;\n\n    fn try_from(value: &Decimal) -> Result<Self, Self::Error> {\n        i32::try_from(&value.0)\n    }\n}\n\nimpl TryFrom<&Decimal> for i16 {\n    type Error = primitive_int_from_rational::SignedFromRationalError;\n\n    fn try_from(value: &Decimal) -> Result<Self, Self::Error> {\n        i16::try_from(&value.0)\n    }\n}\n\nimpl TryFrom<&Decimal> for f64 {\n    type Error = primitive_float_from_rational::FloatConversionError;\n\n    fn try_from(value: &Decimal) -> Result<Self, Self::Error> {\n        f64::try_from(&value.0)\n    }\n}\n\nimpl TryFrom<&Decimal> for f32 {\n    type Error = primitive_float_from_rational::FloatConversionError;\n\n    fn try_from(value: &Decimal) -> Result<Self, Self::Error> {\n        f32::try_from(&value.0)\n    }\n}\n\nimpl PartialEq<f64> for &Decimal {\n    fn eq(&self, other: &f64) -> bool {\n        self.0 == *other\n    }\n}\n\nimpl PartialOrd<f64> for &Decimal {\n    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {\n        self.0.partial_cmp(other)\n    }\n}\n\nimpl FromStr for Decimal {\n    type Err = anyhow::Error;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let r = Rational::from_sci_string(s)\n            .ok_or_else(|| anyhow::anyhow!(\"Failed to parse decimal from string: {s}\"))?;\n        Ok(Decimal(r))\n    }\n}\n\nimpl Display for Decimal {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let mut opts = ToSciOptions::default();\n        opts.set_size_complete();\n        opts.set_include_trailing_zeros(true);\n        if !self.0.fmt_sci_valid(opts) {\n            // e.g the number is 1/3\n            opts.set_scale(10);\n        }\n        self.0.fmt_sci(f, opts)\n    }\n}\n\nimpl From<Rational> for Decimal {\n    fn from(r: Rational) -> Self {\n        Decimal(r)\n    }\n}\n\nimpl tokio_postgres::types::ToSql for Decimal {\n    fn to_sql(\n        &self,\n        _ty: &tokio_postgres::types::Type,\n        out: &mut BytesMut,\n    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {\n        let n = sqldb::numeric::Numeric::from_str(&self.to_string())?;\n        sqldb::numeric::numeric_to_sql(n, out);\n        Ok(tokio_postgres::types::IsNull::No)\n    }\n\n    tokio_postgres::types::accepts!(NUMERIC);\n    tokio_postgres::types::to_sql_checked!();\n}\n\nimpl<'a> tokio_postgres::types::FromSql<'a> for Decimal {\n    fn from_sql(\n        _ty: &tokio_postgres::types::Type,\n        raw: &[u8],\n    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {\n        let n = sqldb::numeric::numeric_from_sql(raw)?;\n        let d = Decimal::from_str(&n.to_string())?;\n        Ok(d)\n    }\n\n    tokio_postgres::types::accepts!(NUMERIC);\n}\n\nimpl Serialize for PValue {\n    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>\n    where\n        S: Serializer,\n    {\n        match self {\n            PValue::Null => serializer.serialize_unit(),\n            PValue::Bool(b) => serializer.serialize_bool(*b),\n            PValue::Number(n) => n.serialize(serializer),\n            PValue::String(s) => serializer.serialize_str(s),\n            PValue::Array(a) => a.serialize(serializer),\n            PValue::Object(o) => o.serialize(serializer),\n            PValue::DateTime(dt) => dt.serialize(serializer),\n            PValue::Cookie(c) => serializer.serialize_str(&c.to_string()),\n            PValue::Decimal(d) => serializer.serialize_str(&d.to_string()),\n        }\n    }\n}\n\nimpl Display for PValue {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            PValue::Null => write!(f, \"null\"),\n            PValue::Bool(b) => write!(f, \"{b}\"),\n            PValue::Number(n) => write!(f, \"{n}\"),\n            PValue::String(s) => write!(f, \"{s}\"),\n            PValue::DateTime(dt) => write!(f, \"{}\", dt.to_rfc3339()),\n            PValue::Array(a) => {\n                write!(f, \"[\")?;\n                for (i, v) in a.iter().enumerate() {\n                    if i > 0 {\n                        write!(f, \", \")?;\n                    }\n                    write!(f, \"{v}\")?;\n                }\n                write!(f, \"]\")\n            }\n            PValue::Object(o) => {\n                write!(f, \"{{\")?;\n                for (i, (k, v)) in o.iter().enumerate() {\n                    if i > 0 {\n                        write!(f, \", \")?;\n                    }\n                    write!(f, \"{k}: {v}\")?;\n                }\n                write!(f, \"}}\")\n            }\n            PValue::Cookie(c) => write!(f, \"{c}\"),\n            PValue::Decimal(d) => write!(f, \"{d}\",),\n        }\n    }\n}\n\nimpl From<serde_json::Value> for PValue {\n    fn from(value: serde_json::Value) -> Self {\n        match value {\n            serde_json::Value::Null => PValue::Null,\n            serde_json::Value::Bool(b) => PValue::Bool(b),\n            serde_json::Value::Number(n) => PValue::Number(n),\n            serde_json::Value::String(s) => PValue::String(s),\n            serde_json::Value::Array(a) => PValue::Array(a.into_iter().map(PValue::from).collect()),\n            serde_json::Value::Object(o) => {\n                PValue::Object(o.into_iter().map(|(k, v)| (k, PValue::from(v))).collect())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/caller.rs",
    "content": "use crate::{EncoreName, EndpointName};\nuse std::str::FromStr;\n\n#[derive(Debug, Clone)]\npub enum Caller {\n    APIEndpoint(EndpointName),\n    PubSubMessage {\n        topic: EncoreName,\n        subscription: EncoreName,\n        message_id: String,\n    },\n    App {\n        deploy_id: String,\n    },\n    Gateway {\n        /// The name of the gateway.\n        gateway: EncoreName,\n    },\n    EncorePrincipal(String),\n}\n\nimpl Caller {\n    pub fn serialize(&self) -> String {\n        match self {\n            Caller::APIEndpoint(name) => format!(\"api:{}.{}\", name.service(), name.endpoint()),\n            Caller::PubSubMessage {\n                topic,\n                subscription,\n                message_id,\n            } => format!(\"pubsub:{topic}:{subscription}:{message_id}\"),\n            Caller::Gateway { gateway } => {\n                format!(\"gateway:{gateway}\")\n            }\n            Caller::App { deploy_id } => format!(\"app:{deploy_id}\"),\n            Caller::EncorePrincipal(name) => format!(\"encore:{name}\"),\n        }\n    }\n\n    /// Whether private APIs can be called\n    pub fn private_api_access(&self) -> bool {\n        use Caller::*;\n        match self {\n            APIEndpoint(_) | PubSubMessage { .. } | App { .. } | EncorePrincipal(_) => true,\n\n            Gateway { .. } => false,\n        }\n    }\n\n    pub fn is_gateway(&self) -> bool {\n        matches!(&self, Caller::Gateway { .. })\n    }\n}\n\nimpl FromStr for Caller {\n    type Err = anyhow::Error;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        fn parse(s: &str) -> Option<Caller> {\n            let (kind, rest) = s.split_once(':')?;\n\n            Some(match kind {\n                \"api\" => {\n                    let (service, endpoint) = rest.split_once('.')?;\n\n                    Caller::APIEndpoint(EndpointName::new(service, endpoint))\n                }\n                \"pubsub\" => {\n                    let mut parts = rest.splitn(3, ':');\n                    let topic = parts.next()?;\n                    let subscription = parts.next()?;\n                    let message_id = parts.next()?;\n                    Caller::PubSubMessage {\n                        topic: EncoreName::from(topic),\n                        subscription: EncoreName::from(subscription),\n                        message_id: message_id.to_string(),\n                    }\n                }\n                \"app\" => Caller::App {\n                    deploy_id: rest.to_string(),\n                },\n                \"gateway\" => {\n                    let mut parts = rest.splitn(2, '.');\n                    let gateway = parts.next()?;\n                    Caller::Gateway {\n                        gateway: EncoreName::from(gateway),\n                    }\n                }\n                \"encore\" => Caller::EncorePrincipal(rest.to_string()),\n                _ => return None,\n            })\n        }\n\n        parse(s).ok_or_else(|| anyhow::anyhow!(\"invalid caller string\"))\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/encoreauth/mod.rs",
    "content": "mod ophash;\nmod sign;\n\npub use ophash::OperationHash;\npub use sign::{sign, sign_for_verification, InvalidSignature, SignatureComponents};\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/encoreauth/ophash.rs",
    "content": "use anyhow::Context;\nuse sha3::{Digest, Sha3_256};\nuse std::str::FromStr;\n\npub struct OperationHash {\n    output: sha3::digest::Output<Sha3_256>,\n    hex: String,\n}\n\nimpl OperationHash {\n    pub fn new<'a>(\n        obj: &[u8],\n        action: &[u8],\n        payload: Option<&[u8]>,\n        additional_context: impl Iterator<Item = &'a [u8]>,\n    ) -> Self {\n        let mut hasher = <Sha3_256 as Digest>::new();\n        hasher.update(obj);\n        hasher.update(action);\n\n        if let Some(payload) = payload {\n            hasher.update(b\"\\0\");\n            hasher.update((payload.len() as u32).to_le_bytes());\n            hasher.update(payload);\n        }\n\n        for c in additional_context {\n            hasher.update(b\"\\0\");\n            hasher.update((c.len() as u32).to_le_bytes());\n            hasher.update(c);\n        }\n\n        let output = hasher.finalize();\n        let hex = hex::encode(output.as_slice());\n        Self { output, hex }\n    }\n\n    pub fn as_hex(&self) -> &str {\n        &self.hex\n    }\n\n    pub fn ct_eq(&self, other: &Self) -> bool {\n        use subtle::ConstantTimeEq;\n        self.output.ct_eq(&other.output).into()\n    }\n}\n\nimpl FromStr for OperationHash {\n    type Err = anyhow::Error;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let raw = hex::decode(s).context(\"invalid hex\")?;\n        let output = <sha3::digest::Output<Sha3_256>>::from_exact_iter(raw.into_iter())\n            .context(\"invalid hash length\")?;\n        Ok(Self {\n            output,\n            hex: s.to_string(),\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/encoreauth/sign.rs",
    "content": "use std::fmt::{Display, Formatter};\nuse std::str::FromStr;\nuse std::time::SystemTime;\n\nuse bytes::{BufMut, BytesMut};\nuse chrono::{DateTime, SecondsFormat, Utc};\nuse hmac::{Hmac, Mac};\n\nuse crate::api::reqauth::encoreauth::ophash::OperationHash;\n\nconst SIGNATURE_VERSION: &str = \"ENCORE1\";\nconst _HASH_IMPL: &str = \"HMAC-SHA3-256\";\n\n// This must match the values of the constants above.\nconst AUTH_SCHEME: &str = \"ENCORE1-HMAC-SHA3-256\";\n\n/// Sign creates the authorization headers for a new request.\n///\n/// The signature algorithm is based on the AWS Signature Version 4 signing process and is valid for 2 minutes\n/// from the time the request is signed.\npub fn sign(\n    key: (u32, &[u8]),\n    app_slug: &str,\n    env_name: &str,\n    timestamp: SystemTime,\n    operation: &OperationHash,\n) -> String {\n    sign_for_verification(key, app_slug, env_name, timestamp, operation)\n}\n\npub fn sign_for_verification(\n    key: (u32, &[u8]),\n    app_slug: &str,\n    env_name: &str,\n    timestamp: SystemTime,\n    operation: &OperationHash,\n) -> String {\n    let credentials = create_credential_string(timestamp, app_slug, env_name, key.0);\n    let request_digest = build_request_digest(timestamp, &credentials, operation);\n    let signing_key = derive_signing_key(key.1, timestamp, app_slug, env_name).into_bytes();\n\n    let signature = hash_hmac(&signing_key, request_digest.as_bytes()).into_bytes();\n    let signature = hex::encode(signature);\n\n    format!(\n        \"{} cred=\\\"{}\\\", op={}, sig={}\",\n        AUTH_SCHEME,\n        credentials,\n        operation.as_hex(),\n        signature\n    )\n}\n\npub struct SignatureComponents {\n    pub key_id: u32,\n    pub app_slug: String,\n    pub env_name: String,\n    pub timestamp: SystemTime,\n    pub operation_hash: OperationHash,\n}\n\n#[derive(Debug)]\npub enum InvalidSignature {\n    InvalidAuthorizationHeader,\n    InvalidDateHeader,\n\n    InvalidAuthScheme,\n    InvalidCredentialString,\n    InvalidOperationHash,\n    UnknownParameter(String),\n}\n\nimpl Display for InvalidSignature {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        use InvalidSignature::*;\n        match self {\n            InvalidAuthorizationHeader => write!(f, \"invalid authorization header\"),\n            InvalidDateHeader => write!(f, \"invalid date header\"),\n            InvalidAuthScheme => write!(f, \"invalid auth scheme\"),\n            InvalidCredentialString => write!(f, \"invalid credential string\"),\n            InvalidOperationHash => write!(f, \"invalid operation hash\"),\n            UnknownParameter(name) => write!(f, \"unknown parameter: {name}\"),\n        }\n    }\n}\n\nimpl std::error::Error for InvalidSignature {}\n\nimpl SignatureComponents {\n    pub fn parse(authorization: &str, date: &str) -> Result<Self, InvalidSignature> {\n        let http_date =\n            httpdate::parse_http_date(date).map_err(|_| InvalidSignature::InvalidDateHeader)?;\n        let date_str = <DateTime<Utc>>::from(http_date)\n            .format(\"%Y%m%d\")\n            .to_string();\n\n        let mut auth_components = authorization.splitn(2, ' ');\n        let scheme = auth_components\n            .next()\n            .ok_or(InvalidSignature::InvalidAuthorizationHeader)?;\n        if scheme != AUTH_SCHEME {\n            return Err(InvalidSignature::InvalidAuthScheme);\n        }\n\n        let parameters = auth_components\n            .next()\n            .ok_or(InvalidSignature::InvalidAuthorizationHeader)?;\n\n        let mut op_hash = None;\n        let mut creds = None;\n        for param in parameters.split(\", \") {\n            let (name, value) = param\n                .split_once('=')\n                .ok_or(InvalidSignature::InvalidAuthorizationHeader)?;\n            match name {\n                \"cred\" => {\n                    if creds.is_some() {\n                        return Err(InvalidSignature::InvalidAuthorizationHeader);\n                    }\n\n                    // Unquote the value.\n                    let value = value\n                        .strip_prefix('\"')\n                        .and_then(|v| v.strip_suffix('\"'))\n                        .ok_or(InvalidSignature::InvalidCredentialString)?;\n\n                    let parsed = parse_credential_string(value)?;\n                    if parsed.date != date_str {\n                        return Err(InvalidSignature::InvalidDateHeader);\n                    }\n                    creds = Some(parsed);\n                }\n                \"op\" => {\n                    if op_hash.is_some() {\n                        return Err(InvalidSignature::InvalidAuthorizationHeader);\n                    }\n                    op_hash = Some(\n                        OperationHash::from_str(value)\n                            .map_err(|_| InvalidSignature::InvalidOperationHash)?,\n                    );\n                }\n                \"sig\" => {\n                    // No need to do anything with the signature\n                }\n                _ => {\n                    return Err(InvalidSignature::UnknownParameter(name.to_string()));\n                }\n            }\n        }\n\n        let Some(creds) = creds else {\n            return Err(InvalidSignature::InvalidAuthorizationHeader);\n        };\n\n        Ok(Self {\n            key_id: creds.key_id,\n            app_slug: creds.app_slug,\n            env_name: creds.env_name,\n            timestamp: http_date,\n            operation_hash: op_hash.ok_or(InvalidSignature::InvalidAuthorizationHeader)?,\n        })\n    }\n}\n\nfn create_credential_string(\n    timestamp: SystemTime,\n    app_slug: &str,\n    env_name: &str,\n    key_id: u32,\n) -> String {\n    let dt: DateTime<Utc> = timestamp.into();\n    let date = dt.format(\"%Y%m%d\");\n    format!(\"{date}/{app_slug}/{env_name}/{key_id}\")\n}\n\nstruct CredentialComponents {\n    key_id: u32,\n    app_slug: String,\n    env_name: String,\n    date: String,\n}\n\nfn parse_credential_string(s: &str) -> Result<CredentialComponents, InvalidSignature> {\n    let mut parts = s.split('/');\n    let date = parts\n        .next()\n        .ok_or(InvalidSignature::InvalidCredentialString)?\n        .to_string();\n    let app_slug = parts\n        .next()\n        .ok_or(InvalidSignature::InvalidCredentialString)?\n        .to_string();\n    let env_name = parts\n        .next()\n        .ok_or(InvalidSignature::InvalidCredentialString)?\n        .to_string();\n    let key_id = parts\n        .next()\n        .ok_or(InvalidSignature::InvalidCredentialString)?\n        .parse::<u32>()\n        .map_err(|_| InvalidSignature::InvalidCredentialString)?;\n\n    if parts.next().is_some() {\n        return Err(InvalidSignature::InvalidCredentialString);\n    }\n\n    Ok(CredentialComponents {\n        key_id,\n        app_slug,\n        env_name,\n        date,\n    })\n}\n\n/// The request digest represents the request that we want to make\n/// and is the data we will sign.\n///\n/// It is a newline separated string of the following:\n///\n/// - The auth scheme being used.\n/// - Timestamp in RFC3339 format.\n/// - App slug and environment name.\n/// - The operation hash.\nfn build_request_digest(\n    timestamp: SystemTime,\n    credentials: &str,\n    operation: &OperationHash,\n) -> String {\n    let dt: DateTime<Utc> = timestamp.into();\n    let timestamp = dt.to_rfc3339_opts(SecondsFormat::Secs, true);\n    format!(\n        \"{}\\n{}\\n{}\\n{}\",\n        AUTH_SCHEME,\n        timestamp,\n        credentials,\n        operation.as_hex(),\n    )\n}\n\n/// The signing key is a HMAC-SHA3-256 hash of the following, where each component is hashed in order,\n/// and the result of each hash is used as the key for the next hash:\n/// - Signature version.\n/// - The shared secret between the app and Encore.\n/// - The date in YYYYMMDD format.\n/// - The application slug.\n/// - The environment name.\n/// - The string \"encore_request\".\nfn derive_signing_key(\n    key_data: &[u8],\n    timestamp: SystemTime,\n    app_slug: &str,\n    env_name: &str,\n) -> hmac::digest::CtOutput<HmacSha3_256> {\n    let base_key = {\n        let mut bytes = BytesMut::with_capacity(SIGNATURE_VERSION.len() + key_data.len());\n        bytes.put_slice(SIGNATURE_VERSION.as_bytes());\n        bytes.put_slice(key_data);\n        bytes.to_vec()\n    };\n\n    let date_key = {\n        let dt: DateTime<Utc> = timestamp.into();\n        let timestamp = dt.format(\"%Y%m%d\").to_string();\n        hash_hmac(&base_key, timestamp.as_bytes()).into_bytes()\n    };\n\n    let app_key = hash_hmac(&date_key, app_slug.as_bytes()).into_bytes();\n    let env_key = hash_hmac(&app_key, env_name.as_bytes()).into_bytes();\n\n    hash_hmac(&env_key, b\"encore_request\")\n}\n\ntype HmacSha3_256 = Hmac<sha3::Sha3_256>;\n\nfn hash_hmac(key: &[u8], data: &[u8]) -> hmac::digest::CtOutput<HmacSha3_256> {\n    HmacSha3_256::new_from_slice(key)\n        .expect(\"hmac can accept keys of any size\")\n        .chain_update(data)\n        .finalize()\n}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/meta.rs",
    "content": "use std::str::{self, FromStr};\n\nuse http::HeaderValue;\n\npub trait HeaderValueExt {\n    fn to_utf8_str(&self) -> Result<&str, std::str::Utf8Error>;\n}\n\nimpl HeaderValueExt for HeaderValue {\n    // Some header values may contain utf8 characters (e.g authdata) so we use `str::from_utf8`\n    // rather than using `HeaderValues::to_str` which errors on non-visible ASCII characters.\n    fn to_utf8_str(&self) -> Result<&str, std::str::Utf8Error> {\n        std::str::from_utf8(self.as_bytes())\n    }\n}\n\n#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]\npub enum MetaKey {\n    TraceParent,\n    TraceState,\n    XCorrelationId,\n    Version,\n    UserId,\n    UserData,\n    Caller,\n    Callee,\n    SvcAuthMethod,\n    SvcAuthEncoreAuthHash,\n    SvcAuthEncoreAuthDate,\n}\n\nimpl MetaKey {\n    pub fn header_key(&self) -> &'static str {\n        use MetaKey::*;\n        match self {\n            TraceParent => \"traceparent\",\n            TraceState => \"tracestate\",\n            XCorrelationId => \"x-correlation-id\",\n            Version => \"x-encore-meta-version\",\n            UserId => \"x-encore-meta-userid\",\n            UserData => \"x-encore-meta-authdata\",\n            Caller => \"x-encore-meta-caller\",\n            Callee => \"x-encore-meta-callee\",\n            SvcAuthMethod => \"x-encore-meta-svc-auth-method\",\n            SvcAuthEncoreAuthHash => \"x-encore-meta-svc-auth\",\n            SvcAuthEncoreAuthDate => \"x-encore-meta-date\",\n        }\n    }\n}\n\npub struct NotMetaKey;\n\nimpl FromStr for MetaKey {\n    type Err = NotMetaKey;\n\n    fn from_str(value: &str) -> Result<Self, Self::Err> {\n        use MetaKey::*;\n        Ok(match value {\n            \"traceparent\" => TraceParent,\n            \"tracestate\" => TraceState,\n            \"x-correlation-id\" => XCorrelationId,\n            \"x-encore-meta-version\" => Version,\n            \"x-encore-meta-userid\" => UserId,\n            \"x-encore-meta-authdata\" => UserData,\n            \"x-encore-meta-caller\" => Caller,\n            \"x-encore-meta-callee\" => Callee,\n            \"x-encore-meta-svc-auth-method\" => SvcAuthMethod,\n            \"x-encore-meta-svc-auth\" => SvcAuthEncoreAuthHash,\n            \"x-encore-meta-date\" => SvcAuthEncoreAuthDate,\n            _ => return Err(NotMetaKey),\n        })\n    }\n}\npub trait MetaMapMut: MetaMap {\n    fn set(&mut self, key: MetaKey, value: String) -> anyhow::Result<()>;\n}\n\npub trait MetaMap {\n    fn get_meta(&self, key: MetaKey) -> Option<&str>;\n    fn meta_values<'a>(&'a self, key: MetaKey) -> Box<dyn Iterator<Item = &'a str> + 'a>;\n\n    /// Returns all meta keys, sorted alphabetically based on MetaKey::header_key.\n    fn sorted_meta_keys(&self) -> Vec<MetaKey>;\n}\n\nimpl MetaMapMut for reqwest::header::HeaderMap {\n    fn set(&mut self, key: MetaKey, value: String) -> anyhow::Result<()> {\n        self.insert(key.header_key(), value.parse()?);\n        Ok(())\n    }\n}\n\nimpl MetaMap for axum::http::HeaderMap {\n    fn get_meta(&self, key: MetaKey) -> Option<&str> {\n        self.get(key.header_key())\n            .and_then(|val| val.to_utf8_str().ok())\n    }\n\n    fn meta_values<'a>(&'a self, key: MetaKey) -> Box<dyn Iterator<Item = &'a str> + 'a> {\n        Box::new(\n            self.get_all(key.header_key())\n                .iter()\n                .filter_map(|v| v.to_utf8_str().ok()),\n        )\n    }\n\n    fn sorted_meta_keys(&self) -> Vec<MetaKey> {\n        let mut keys: Vec<_> = self\n            .keys()\n            .filter_map(|k| MetaKey::from_str(k.as_str()).ok())\n            .collect();\n        keys.sort_by_key(|k| k.header_key());\n        keys\n    }\n}\n\nimpl MetaMapMut for pingora::http::RequestHeader {\n    fn set(&mut self, key: MetaKey, value: String) -> anyhow::Result<()> {\n        self.insert_header(key.header_key(), value)?;\n        Ok(())\n    }\n}\n\nimpl MetaMap for pingora::http::RequestHeader {\n    fn get_meta(&self, key: MetaKey) -> Option<&str> {\n        self.headers\n            .get(key.header_key())\n            .and_then(|v| v.to_utf8_str().ok())\n    }\n\n    fn meta_values<'a>(&'a self, key: MetaKey) -> Box<dyn Iterator<Item = &'a str> + 'a> {\n        Box::new(\n            self.headers\n                .get_all(key.header_key())\n                .iter()\n                .filter_map(|v| v.to_utf8_str().ok()),\n        )\n    }\n\n    fn sorted_meta_keys(&self) -> Vec<MetaKey> {\n        let mut keys: Vec<_> = self\n            .headers\n            .keys()\n            .filter_map(|k| MetaKey::from_str(k.as_str()).ok())\n            .collect();\n        keys.sort_by_key(|k| k.header_key());\n        keys\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/mod.rs",
    "content": "use crate::api::jsonschema::DecodeConfig;\nuse crate::api::reqauth::caller::Caller;\nuse crate::api::reqauth::meta::MetaMap;\nuse crate::api::APIResult;\nuse crate::encore::runtime::v1 as pb;\nuse crate::{api, model, secrets};\nuse anyhow::Context;\nuse std::collections::HashMap;\nuse std::sync::Arc;\nuse std::time::SystemTime;\n\nuse super::{jsonschema, PValues};\n\npub mod caller;\nmod encoreauth;\npub mod meta;\npub mod platform;\npub mod svcauth;\n\n/// Computes the service auth method to use for communicating with a given service.\npub fn service_auth_method(\n    secrets: &secrets::Manager,\n    env: &pb::Environment,\n    auth_method: pb::ServiceAuth,\n) -> anyhow::Result<Arc<dyn svcauth::ServiceAuthMethod>> {\n    let obj: Arc<dyn svcauth::ServiceAuthMethod> = match auth_method.auth_method {\n        None | Some(pb::service_auth::AuthMethod::Noop(_)) => Arc::new(svcauth::Noop),\n        Some(pb::service_auth::AuthMethod::EncoreAuth(ea)) => {\n            let auth_keys = ea\n                .auth_keys\n                .into_iter()\n                .filter_map(|k| {\n                    let data = k.data?;\n                    Some(svcauth::EncoreAuthKey {\n                        key_id: k.id,\n                        data: secrets.load(data),\n                    })\n                })\n                .collect::<Vec<_>>();\n\n            if auth_keys.is_empty() {\n                anyhow::bail!(\"no auth keys provided for encore-auth method\");\n            }\n\n            Arc::new(svcauth::EncoreAuth::new(\n                env.app_slug.clone(),\n                env.env_name.clone(),\n                auth_keys,\n            ))\n        }\n    };\n    Ok(obj)\n}\n\n#[derive(Debug, Clone)]\npub struct CallMeta {\n    /// The trace id to use. Equal to caller_trace_id if set, and generated otherwise.\n    pub trace_id: model::TraceId,\n\n    /// The trace id of the caller; None if not traced.\n    pub caller_trace_id: Option<model::TraceId>,\n\n    /// The span id of the caller (None if there's no parent).\n    pub parent_span_id: Option<model::SpanId>,\n\n    /// The span id of THIS request, if predefined by the caller (None in most cases).\n    pub this_span_id: Option<model::SpanId>,\n\n    /// The event id which started the API call (None if there's no parent).\n    pub parent_event_id: Option<model::TraceEventId>,\n\n    /// Correlation id to use.\n    pub ext_correlation_id: Option<String>,\n\n    /// Whether the caller sampled trace info\n    /// None if there's no parent span or if the sampled flag couldn't be parsed.\n    pub trace_sampled: Option<bool>,\n\n    /// Information about an internal call, if any.\n    /// If set it can be trusted as it has been authenticated.\n    pub internal: Option<InternalCallMeta>,\n}\n\n#[derive(Debug, Clone)]\npub struct InternalCallMeta {\n    /// The source of the call.\n    pub caller: Caller,\n\n    /// The authenticated user id, if any.\n    pub auth_uid: Option<String>,\n\n    /// The user data, if any.\n    pub auth_data: Option<PValues>,\n}\n\nimpl CallMeta {\n    pub fn parse_with_caller(\n        auth: &[Arc<dyn svcauth::ServiceAuthMethod>],\n        headers: &axum::http::HeaderMap,\n        auth_data_schemas: &HashMap<String, Option<jsonschema::JSONSchema>>,\n    ) -> APIResult<Self> {\n        Self::parse(headers, auth, true, Some(auth_data_schemas))\n    }\n\n    pub fn parse_without_caller(headers: &axum::http::HeaderMap) -> APIResult<Self> {\n        Self::parse(headers, &[], false, None)\n    }\n\n    fn parse(\n        headers: &axum::http::HeaderMap,\n        auth: &[Arc<dyn svcauth::ServiceAuthMethod>],\n        parse_caller: bool,\n        auth_data_schemas: Option<&HashMap<String, Option<jsonschema::JSONSchema>>>,\n    ) -> APIResult<Self> {\n        let do_parse = move || -> anyhow::Result<CallMeta> {\n            use meta::MetaKey;\n\n            if let Some(version) = headers.get_meta(MetaKey::Version) {\n                if version != \"1\" {\n                    anyhow::bail!(\"unknown encore meta version\");\n                }\n            }\n\n            let mut meta = CallMeta {\n                trace_id: model::TraceId::generate(),\n                caller_trace_id: None,\n                parent_span_id: None,\n                this_span_id: None,\n                parent_event_id: None,\n                ext_correlation_id: None,\n                trace_sampled: None,\n                internal: None,\n            };\n\n            // If it was an internal call, parse it.\n            if parse_caller {\n                if let Some(caller) = headers.get_meta(MetaKey::Caller) {\n                    // Find the auth method.\n                    let auth_method = headers.get_meta(MetaKey::SvcAuthMethod);\n                    let Some(auth) = auth.iter().find(|a| auth_method == Some(a.name())) else {\n                        anyhow::bail!(\"unknown service auth method\");\n                    };\n\n                    // Verify the caller's signature.\n                    auth.verify(headers, SystemTime::now())\n                        .context(\"invalid service authentication data\")?;\n\n                    let caller = caller.parse().context(\"invalid meta caller\")?;\n\n                    // TODO: Currently we assume a single auth data schema.\n                    // When we support multiple gateways with distinct schemas we'll need to change this.\n                    let auth_data_schema = auth_data_schemas\n                        .and_then(|s| s.values().filter_map(|s| s.as_ref()).next());\n\n                    // Parse the auth data, if provided.\n                    let auth_data = match (headers.get_meta(MetaKey::UserData), &auth_data_schema) {\n                        (None, _) => None,\n                        (Some(data), None) => {\n                            // Hack: temporarily work around the absence of a schema in certain situations.\n                            let any_schema = {\n                                use crate::encore::parser::meta::v1 as meta;\n                                let md = meta::Data::default();\n                                let mut builder = jsonschema::Builder::new(&md);\n                                let idx = builder.register_value(jsonschema::Value::Basic(\n                                    jsonschema::Basic::Any,\n                                ));\n                                let registry = builder.build();\n                                registry.schema(idx)\n                            };\n                            let mut jsonde = serde_json::Deserializer::from_str(data);\n                            let data = any_schema\n                                .deserialize(&mut jsonde, DecodeConfig::default())\n                                .context(\"invalid auth data\")?;\n                            Some(data)\n                        }\n                        (Some(data), Some(schema)) => {\n                            let mut jsonde = serde_json::Deserializer::from_str(data);\n                            let data = schema\n                                .deserialize(&mut jsonde, DecodeConfig::default())\n                                .context(\"invalid auth data\")?;\n                            Some(data)\n                        }\n                    };\n\n                    meta.internal = Some(InternalCallMeta {\n                        caller,\n                        auth_uid: headers.get_meta(MetaKey::UserId).map(|s| s.to_string()),\n                        auth_data,\n                    });\n                };\n            }\n\n            // For now we only read the traceparent for internal-to-internal calls, this is because CloudRun\n            // is adding a traceparent header to all requests, which is causing our trace system to get confused\n            // and think that the initial request is a child of another already traced request\n            //\n            // In the future we should be able to remove this check and read the traceparent header for all requests\n            // to interopt with other tracing systems.\n            if meta.internal.is_some() {\n                if let Some(traceparent) = headers.get_meta(MetaKey::TraceParent) {\n                    // Parse the traceparent.\n                    if let Ok((trace_id, parent_span_id, sampled)) = parse_traceparent(traceparent)\n                    {\n                        meta.trace_id = trace_id;\n                        meta.caller_trace_id = Some(trace_id);\n                        meta.parent_span_id = Some(parent_span_id);\n                        meta.trace_sampled = Some(sampled);\n                    };\n\n                    // If the caller is a gateway, ignore the parent span id as gateways don't currently record a span.\n                    // If we include it the root request won't be tagged as such.\n                    if let Some(internal) = &meta.internal {\n                        if matches!(internal.caller, Caller::Gateway { .. }) {\n                            meta.parent_span_id = None;\n                        }\n                    }\n\n                    // Parse the trace state.\n                    if let (Some(event_id), parent_span) =\n                        parse_tracestate(headers.meta_values(MetaKey::TraceState))\n                    {\n                        meta.parent_event_id = Some(event_id);\n                        // If we where given a parent span ID, use that instead of the one from the traceparent header\n                        // This is because GCP Cloud Run will add it's own spans in before the application code is run\n                        // and thus we lose the parent span ID from the traceparent header\n                        if let Some(parent_span) = parent_span {\n                            meta.parent_span_id = Some(parent_span);\n                        }\n                    }\n                }\n            }\n\n            meta.ext_correlation_id = headers.get_meta(MetaKey::XCorrelationId).map(|s| {\n                // Limit the maximum length the correlation id can have.\n                s[..s.len().min(64)].to_string()\n            });\n\n            Ok(meta)\n        };\n\n        do_parse().map_err(|e| api::Error::invalid_argument(\"unable to parse request\", e))\n    }\n}\n\nfn parse_traceparent(s: &str) -> anyhow::Result<(model::TraceId, model::SpanId, bool)> {\n    let version = \"00\";\n    let trace_id_len = 32;\n    let span_id_len = 16;\n    let trace_flags_len = 2;\n\n    let ver_start = 0;\n    let ver_end = ver_start + version.len();\n    let ver_sep = ver_end;\n\n    let trace_id_start = ver_sep + 1;\n    let trace_id_end = trace_id_start + trace_id_len;\n    let trace_id_sep = trace_id_end;\n\n    let span_id_start = trace_id_sep + 1;\n    let span_id_end = span_id_start + span_id_len;\n    let span_id_sep = span_id_end;\n\n    let trace_flags_start = span_id_sep + 1;\n    let trace_flags_end = trace_flags_start + trace_flags_len;\n    let total_len = trace_flags_end;\n\n    if s.len() != total_len {\n        anyhow::bail!(\"invalid traceparent length\");\n    } else if &s[ver_start..ver_end] != version {\n        anyhow::bail!(\"invalid traceparent version\");\n    } else if &s[ver_sep..ver_sep + 1] != \"-\" {\n        anyhow::bail!(\"invalid traceparent version separator\");\n    } else if &s[trace_id_sep..trace_id_sep + 1] != \"-\" {\n        anyhow::bail!(\"invalid traceparent trace id separator\");\n    } else if &s[span_id_sep..span_id_sep + 1] != \"-\" {\n        anyhow::bail!(\"invalid traceparent span id separator\");\n    }\n\n    let trace_id = &s[trace_id_start..trace_id_end];\n    let trace_id = model::TraceId::parse_std(trace_id).context(\"invalid trace id\")?;\n\n    let span_id = &s[span_id_start..span_id_end];\n    let span_id = model::SpanId::parse_std(span_id).context(\"invalid span id\")?;\n\n    // Parse trace flags - bit 0 (0x01) indicates \"sampled\"\n    let trace_flags = &s[trace_flags_start..trace_flags_end];\n    let trace_flags = u8::from_str_radix(trace_flags, 16).context(\"invalid trace flags\")?;\n    let sampled = trace_flags & 0x01 != 0;\n\n    Ok((trace_id, span_id, sampled))\n}\n\nfn parse_tracestate<'a>(\n    vals: impl Iterator<Item = &'a str>,\n) -> (Option<model::TraceEventId>, Option<model::SpanId>) {\n    enum Data {\n        EventId(model::TraceEventId),\n        SpanId(model::SpanId),\n    }\n\n    let parse_entry = |val: &str| -> Option<Data> {\n        let (key, val) = val.split_once('=')?;\n\n        match key {\n            \"encore/event-id\" => Some(Data::EventId(val.parse().ok()?)),\n            \"encore/span-id\" => Some(Data::SpanId(model::SpanId::parse_std(val).ok()?)),\n            _ => None,\n        }\n    };\n\n    let mut event_id = None;\n    let mut span_id = None;\n\n    for val in vals {\n        for field in val.split(',') {\n            match parse_entry(field) {\n                Some(Data::EventId(id)) => event_id = Some(id),\n                Some(Data::SpanId(id)) => span_id = Some(id),\n                None => (),\n            }\n        }\n    }\n\n    (event_id, span_id)\n}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/platform.rs",
    "content": "use crate::{encore, secrets};\nuse anyhow::Context;\nuse base64::engine::general_purpose;\nuse base64::Engine;\nuse encore::runtime::v1 as pb;\nuse hmac::Mac;\nuse percent_encoding::percent_decode_str;\nuse std::fmt::Display;\nuse std::time::SystemTime;\n\npub struct RequestValidator {\n    keys: Box<[SigningKey]>,\n}\n\nimpl std::fmt::Debug for RequestValidator {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"RequestValidator\").finish()\n    }\n}\n\npub struct ValidationData<'a> {\n    pub request_path: &'a str,\n    pub date_header: &'a str,\n    pub x_encore_auth_header: &'a str,\n}\n\n/// A seal of approval is a record that the request originated from the Encore Platform.\n#[derive(Debug)]\npub struct SealOfApproval;\n\nimpl RequestValidator {\n    pub fn new(secrets: &secrets::Manager, keys: Vec<pb::EncoreAuthKey>) -> Self {\n        let keys = keys\n            .into_iter()\n            .filter_map(|k| match k.data {\n                Some(data) => Some(SigningKey {\n                    id: k.id,\n                    data: secrets.load(data),\n                }),\n                None => None,\n            })\n            .collect();\n        Self { keys }\n    }\n\n    #[cfg(test)]\n    pub fn new_mock() -> Self {\n        use crate::secrets::Secret;\n\n        Self {\n            keys: [SigningKey {\n                id: 123,\n                data: Secret::new_for_test(\"secret data\"),\n            }]\n            .into(),\n        }\n    }\n\n    pub fn validate_platform_request(\n        &self,\n        req: &ValidationData,\n    ) -> Result<SealOfApproval, ValidationError> {\n        let decoded_auth_header = BASE64\n            .decode(req.x_encore_auth_header.as_bytes())\n            .map_err(|_| ValidationError::InvalidMac)?;\n\n        // Pull out key ID from hmac prefix\n        const KEY_ID_LEN: usize = 4;\n        if decoded_auth_header.len() < KEY_ID_LEN {\n            return Err(ValidationError::InvalidMac);\n        }\n\n        let key_id = u32::from_be_bytes(decoded_auth_header[..KEY_ID_LEN].try_into().unwrap());\n        let received_mac = &decoded_auth_header[KEY_ID_LEN..];\n        for k in self.keys.iter() {\n            if k.id == key_id {\n                let secret_data = k.data.get().map_err(ValidationError::SecretResolve)?;\n                return check_auth_key(secret_data, req, received_mac);\n            }\n        }\n\n        Err(ValidationError::UnknownMacKey)\n    }\n\n    pub fn sign_outgoing_request(&self, req: &mut reqwest::Request) -> anyhow::Result<()> {\n        let path = percent_decode_str(req.url().path())\n            .decode_utf8_lossy()\n            .to_string();\n\n        let date_str = req\n            .headers_mut()\n            .entry(reqwest::header::DATE)\n            .or_insert_with(|| {\n                let date_str = httpdate::fmt_http_date(SystemTime::now());\n                date_str.parse().unwrap()\n            });\n\n        let key = &self.keys[0];\n        let key_data = key.data.get().context(\"unable to resolve signing key\")?;\n        let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(key_data).unwrap();\n        mac.update(date_str.as_bytes());\n        mac.update(b\"\\x00\");\n        mac.update(path.as_bytes());\n\n        let mac_bytes = mac.finalize().into_bytes();\n        let combined = [key.id.to_be_bytes().as_slice(), mac_bytes.as_slice()].concat();\n        let auth_header = BASE64.encode(combined);\n        req.headers_mut().insert(\n            reqwest::header::HeaderName::from_static(\"x-encore-auth\"),\n            reqwest::header::HeaderValue::from_str(&auth_header).context(\"invalid auth header\")?,\n        );\n        Ok(())\n    }\n}\n\nstruct SigningKey {\n    id: u32,\n    data: secrets::Secret,\n}\n\nconst BASE64: general_purpose::GeneralPurpose = general_purpose::STANDARD_NO_PAD;\n\nfn check_auth_key(\n    decryption_key: &[u8],\n    req: &ValidationData,\n    received_mac: &[u8],\n) -> Result<SealOfApproval, ValidationError> {\n    let request_date = httpdate::parse_http_date(req.date_header)\n        .map_err(|_| ValidationError::InvalidDateHeader)?;\n\n    let now = SystemTime::now();\n    let diff = now\n        .duration_since(request_date)\n        .unwrap_or_else(|e| e.duration());\n\n    const THRESHOLD: u64 = 15 * 60;\n    if diff.as_secs() > THRESHOLD {\n        return Err(ValidationError::TimeSkew);\n    }\n\n    // Compute the MAC.\n    type HmacSha256 = hmac::Hmac<sha2::Sha256>;\n    let mut computed_mac =\n        HmacSha256::new_from_slice(decryption_key).map_err(|_| ValidationError::InvalidMacKey)?;\n    computed_mac.update(req.date_header.as_bytes());\n    computed_mac.update(b\"\\x00\");\n    computed_mac.update(req.request_path.as_bytes());\n\n    computed_mac\n        .verify_slice(received_mac)\n        .map_err(|_| ValidationError::InvalidMac)?;\n\n    Ok(SealOfApproval)\n}\n\n#[derive(Debug)]\npub enum ValidationError {\n    InvalidMac,\n    UnknownMacKey,\n    InvalidMacKey,\n    InvalidDateHeader,\n    TimeSkew,\n    SecretResolve(secrets::ResolveError),\n}\n\nimpl Display for ValidationError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ValidationError::InvalidMac => write!(f, \"invalid mac\"),\n            ValidationError::UnknownMacKey => write!(f, \"unknown mac key\"),\n            ValidationError::InvalidMacKey => write!(f, \"invalid mac key\"),\n            ValidationError::InvalidDateHeader => write!(f, \"invalid or missing date header\"),\n            ValidationError::TimeSkew => write!(f, \"time skew\"),\n            ValidationError::SecretResolve(e) => {\n                write!(f, \"resolve secret: {e}\")\n            }\n        }\n    }\n}\n\nimpl std::error::Error for ValidationError {}\n"
  },
  {
    "path": "runtimes/core/src/api/reqauth/svcauth.rs",
    "content": "use std::fmt::{Debug, Display};\nuse std::time::SystemTime;\n\nuse anyhow::Context;\nuse sha3::digest::Digest;\nuse subtle::ConstantTimeEq;\n\nuse crate::api::reqauth::encoreauth;\nuse crate::api::reqauth::encoreauth::{OperationHash, SignatureComponents};\nuse crate::api::reqauth::meta::{MetaKey, MetaMapMut};\nuse crate::secrets;\nuse crate::secrets::Secret;\n\nuse super::meta::MetaMap;\n\npub trait ServiceAuthMethod: Debug + Send + Sync + 'static {\n    fn name(&self) -> &'static str;\n    fn sign(&self, headers: &mut dyn MetaMapMut, now: SystemTime) -> anyhow::Result<()>;\n    fn verify(&self, headers: &dyn MetaMap, now: SystemTime) -> Result<(), VerifyError>;\n}\n\n#[derive(Debug)]\npub struct Noop;\n\nimpl ServiceAuthMethod for Noop {\n    fn name(&self) -> &'static str {\n        \"noop\"\n    }\n\n    fn sign(&self, _headers: &mut dyn MetaMapMut, _now: SystemTime) -> anyhow::Result<()> {\n        Ok(())\n    }\n\n    fn verify(&self, _headers: &dyn MetaMap, _now: SystemTime) -> Result<(), VerifyError> {\n        Ok(())\n    }\n}\n\npub struct EncoreAuthKey {\n    pub key_id: u32,\n    pub data: Secret,\n}\n\npub struct EncoreAuth {\n    app_slug: String,\n    env_name: String,\n    keys: Vec<EncoreAuthKey>,\n    latest_idx: usize, // index into keys\n}\n\nimpl Debug for EncoreAuth {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"EncoreAuth\")\n            .field(\"app_slug\", &self.app_slug)\n            .field(\"env_name\", &self.env_name)\n            .finish()\n    }\n}\n\nimpl EncoreAuth {\n    pub fn new(app_slug: String, env_name: String, keys: Vec<EncoreAuthKey>) -> Self {\n        if keys.is_empty() {\n            panic!(\"auth keys must not be empty\");\n        }\n\n        let latest_idx = {\n            let mut max_id = keys[0].key_id;\n            let mut max_idx = 0;\n            for (idx, k) in keys.iter().enumerate() {\n                if k.key_id > max_id {\n                    max_idx = idx;\n                    max_id = k.key_id;\n                }\n            }\n            max_idx\n        };\n\n        Self {\n            app_slug,\n            env_name,\n            keys,\n            latest_idx,\n        }\n    }\n}\n\n#[derive(Debug)]\npub enum VerifyError {\n    NoAuthorizationHeader,\n    NoDateHeader,\n    InvalidHeader(encoreauth::InvalidSignature),\n    SignatureMismatch,\n    DateSkew,\n    UnknownKey,\n    ResolveKeyData(secrets::ResolveError),\n}\n\nimpl Display for VerifyError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        use VerifyError::*;\n        match self {\n            NoAuthorizationHeader => write!(f, \"no authorization header\"),\n            NoDateHeader => write!(f, \"no date header\"),\n            InvalidHeader(e) => write!(f, \"invalid header: {e}\"),\n            SignatureMismatch => write!(f, \"signature mismatch\"),\n            DateSkew => write!(f, \"date skew\"),\n            UnknownKey => write!(f, \"unknown key\"),\n            ResolveKeyData(e) => write!(f, \"unable to resolve secret key data: {e}\"),\n        }\n    }\n}\n\nimpl std::error::Error for VerifyError {}\n\nimpl ServiceAuthMethod for EncoreAuth {\n    fn name(&self) -> &'static str {\n        \"encore-auth\"\n    }\n\n    fn sign(&self, headers: &mut dyn MetaMapMut, now: SystemTime) -> anyhow::Result<()> {\n        let op_hash = self.build_op_hash(headers);\n\n        let key = &self.keys[self.latest_idx];\n        let key_data = key.data.get().context(\"unable to resolve auth key data\")?;\n\n        let hash = encoreauth::sign(\n            (key.key_id, key_data),\n            &self.app_slug,\n            &self.env_name,\n            now,\n            &op_hash,\n        );\n\n        headers\n            .set(MetaKey::SvcAuthEncoreAuthHash, hash)\n            .context(\"set auth hash header\")?;\n        headers\n            .set(MetaKey::SvcAuthEncoreAuthDate, httpdate::fmt_http_date(now))\n            .context(\"set auth date header\")?;\n\n        Ok(())\n    }\n\n    fn verify(&self, headers: &dyn MetaMap, now: SystemTime) -> Result<(), VerifyError> {\n        let auth_header = headers\n            .get_meta(MetaKey::SvcAuthEncoreAuthHash)\n            .ok_or(VerifyError::NoAuthorizationHeader)?;\n        let date_header = headers\n            .get_meta(MetaKey::SvcAuthEncoreAuthDate)\n            .ok_or(VerifyError::NoDateHeader)?;\n\n        let components = SignatureComponents::parse(auth_header, date_header)\n            .map_err(VerifyError::InvalidHeader)?;\n\n        let diff = now\n            .duration_since(components.timestamp)\n            .unwrap_or_else(|e| e.duration());\n        if diff.as_secs() > 120 {\n            return Err(VerifyError::DateSkew);\n        }\n\n        let key = self\n            .keys\n            .iter()\n            .find(|k| k.key_id == components.key_id)\n            .ok_or(VerifyError::UnknownKey)?;\n\n        let key_data = key.data.get().map_err(VerifyError::ResolveKeyData)?;\n        let expected_signature = encoreauth::sign_for_verification(\n            (key.key_id, key_data),\n            &components.app_slug,\n            &components.env_name,\n            components.timestamp,\n            &components.operation_hash,\n        );\n\n        let signature_match: bool = expected_signature\n            .as_bytes()\n            .ct_eq(auth_header.as_bytes())\n            .into();\n        if !signature_match {\n            return Err(VerifyError::SignatureMismatch);\n        }\n\n        let expected_op_hash = self.build_op_hash(headers);\n        if !expected_op_hash.ct_eq(&components.operation_hash) {\n            return Err(VerifyError::SignatureMismatch);\n        }\n\n        Ok(())\n    }\n}\n\nimpl EncoreAuth {\n    fn build_op_hash<R: MetaMap + ?Sized>(&self, req: &R) -> OperationHash {\n        // Build a deterministic hash of the meta keys and values.\n        let mut hash = <sha3::Sha3_256 as Digest>::new();\n        for key in req.sorted_meta_keys() {\n            use MetaKey::*;\n            match key {\n                SvcAuthMethod | SvcAuthEncoreAuthHash | SvcAuthEncoreAuthDate => {\n                    // Skip these headers, as they are part of the auth mechanism itself.\n                }\n\n                TraceParent | TraceState => {\n                    // Skip these headers, as they are part of the tracing mechanism and could be changed\n                    // by things like load balancers.\n                }\n\n                XCorrelationId | Version | UserId | UserData | Caller | Callee => {\n                    // Read all values for this key, and sort them.\n                    let mut values = req.meta_values(key).collect::<Vec<_>>();\n                    values.sort();\n\n                    for value in values {\n                        hash.update(key.header_key());\n                        hash.update(b\"=\");\n                        hash.update(value.as_bytes());\n                        hash.update(b\"\\n\");\n                    }\n                }\n            }\n        }\n\n        let payload = hash.finalize();\n        OperationHash::new(\n            \"internal-api\".as_bytes(),\n            \"call\".as_bytes(),\n            Some(payload.as_slice()),\n            std::iter::empty(),\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::api::reqauth::meta::MetaMap;\n\n    use super::*;\n\n    fn metas<R: MetaMap>(req: &R) -> Vec<(MetaKey, Vec<String>)> {\n        let keys = req.sorted_meta_keys();\n        keys.into_iter()\n            .map(|k| {\n                (\n                    k,\n                    req.meta_values(k)\n                        .map(|s| s.to_string())\n                        .collect::<Vec<_>>(),\n                )\n            })\n            .collect()\n    }\n\n    fn convert_header_map(src: reqwest::header::HeaderMap) -> axum::http::HeaderMap {\n        let mut dst = axum::http::HeaderMap::new();\n        for entry in src.iter() {\n            let key: axum::http::HeaderName = entry.0.as_str().parse().unwrap();\n            let value = entry.1.to_str().unwrap().parse().unwrap();\n            dst.insert(key, value);\n        }\n        dst\n    }\n\n    #[test]\n    fn test_encore_auth() -> anyhow::Result<()> {\n        let mut headers = reqwest::header::HeaderMap::new();\n\n        let auth = EncoreAuth {\n            app_slug: \"app\".into(),\n            env_name: \"env\".into(),\n            keys: vec![EncoreAuthKey {\n                key_id: 123,\n                data: Secret::new_for_test(\"secret data\"),\n            }],\n            latest_idx: 0,\n        };\n\n        let now = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1234567890);\n        auth.sign(&mut headers, now)\n            .context(\"unable to sign request\")?;\n\n        let out_headers = convert_header_map(headers);\n        auth.verify(&out_headers, now)\n            .context(\"unable to verify request\")?;\n\n        assert_eq!(\n            metas(&out_headers),\n            vec![\n                (\n                    MetaKey::SvcAuthEncoreAuthDate,\n                    vec![\"Fri, 13 Feb 2009 23:31:30 GMT\".to_string()]\n                ),\n                (MetaKey::SvcAuthEncoreAuthHash, vec![r#\"ENCORE1-HMAC-SHA3-256 cred=\"20090213/app/env/123\", op=f3c70a419394ce9d56efafad2208154b92c8596d7396b3a2b4ea7fd925d28dc2, sig=fc0c88b47c13d999353ecc8681d91d9c03209a1f05583b92d84e429fedfe387a\"#.to_string()]),\n            ]\n        );\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/body.rs",
    "content": "use axum::http::HeaderValue;\nuse bytes::Bytes;\n\nuse crate::api::jsonschema::DecodeConfig;\nuse crate::api::schema::{JSONPayload, ToOutgoingRequest};\nuse crate::api::{self, PValues};\nuse crate::api::{jsonschema, APIResult};\nuse http_body_util::BodyExt;\nuse reqwest::header::CONTENT_TYPE;\n\n#[derive(Debug, Clone)]\npub struct Body {\n    schema: jsonschema::JSONSchema,\n}\n\nimpl Body {\n    pub fn new(schema: jsonschema::JSONSchema) -> Self {\n        Self { schema }\n    }\n\n    pub async fn parse_incoming_request_body(\n        &self,\n        body: axum::body::Body,\n    ) -> APIResult<Option<PValues>> {\n        // Collect the bytes of the request body.\n        // Serde doesn't support async streaming reads (at least not yet).\n        let bytes = body\n            .collect()\n            .await\n            .map_err(|e| api::Error::invalid_argument(\"unable to read request body\", e))?\n            .to_bytes();\n\n        let mut jsonde = serde_json::Deserializer::from_slice(&bytes);\n        let cfg = DecodeConfig {\n            coerce_strings: false,\n            arrays_as_repeated_fields: false,\n        };\n        let value = self\n            .schema\n            .deserialize(&mut jsonde, cfg)\n            .map_err(|e| api::Error::invalid_argument(\"unable to decode request body\", e))?;\n        Ok(Some(value))\n    }\n\n    pub async fn parse_response_body(&self, body: Bytes) -> APIResult<Option<PValues>> {\n        let mut jsonde = serde_json::Deserializer::from_slice(&body);\n        let cfg = DecodeConfig {\n            coerce_strings: false,\n            arrays_as_repeated_fields: false,\n        };\n        let value = self\n            .schema\n            .deserialize(&mut jsonde, cfg)\n            .map_err(|e| api::Error::invalid_argument(\"unable to decode response body\", e))?;\n        Ok(Some(value))\n    }\n}\n\nimpl ToOutgoingRequest<reqwest::Request> for Body {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut reqwest::Request,\n    ) -> APIResult<()> {\n        if payload.is_none() {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing body payload\".to_string(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            });\n        };\n\n        let body = self.schema.to_vec(payload).map_err(api::Error::internal)?;\n        if !req.headers().contains_key(CONTENT_TYPE) {\n            req.headers_mut().insert(\n                CONTENT_TYPE,\n                reqwest::header::HeaderValue::from_static(\"application/json\"),\n            );\n        }\n        *req.body_mut() = Some(body.into());\n        Ok(())\n    }\n}\n\nimpl Body {\n    pub fn to_outgoing_payload(&self, payload: &JSONPayload) -> APIResult<Vec<u8>> {\n        if payload.is_none() {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing body payload\".to_string(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            });\n        };\n\n        let body = self.schema.to_vec(payload).map_err(api::Error::internal)?;\n        Ok(body)\n    }\n    pub fn to_response(\n        &self,\n        payload: &JSONPayload,\n        resp: axum::http::response::Builder,\n    ) -> APIResult<axum::http::Response<axum::body::Body>> {\n        let buf = self.schema.to_vec(payload).map_err(api::Error::internal)?;\n        let resp = resp\n            .header(\n                axum::http::header::CONTENT_TYPE,\n                HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),\n            )\n            .body(axum::body::Body::from(buf))\n            .map_err(api::Error::internal)?;\n\n        Ok(resp)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/cookie.rs",
    "content": "use http::{\n    header::{COOKIE, SET_COOKIE},\n    HeaderValue,\n};\n\nuse crate::api::{self, jsonschema, schema::ToResponse, APIResult, PValue, PValues};\n\nuse super::{HTTPHeaders, JSONPayload, ToHeaderStr, ToOutgoingRequest};\n\n#[derive(Debug, Clone)]\npub struct Cookie {\n    schema: jsonschema::JSONSchema,\n}\n\nimpl Cookie {\n    pub fn new(schema: jsonschema::JSONSchema) -> Self {\n        Self { schema }\n    }\n\n    pub fn contains_any(&self, headers: &impl HTTPHeaders) -> bool {\n        let mut jar = cookie::CookieJar::new();\n        for raw in headers.get_all(axum::http::header::COOKIE.as_str()) {\n            if let Ok(raw) = raw.to_str() {\n                for c in cookie::Cookie::split_parse(raw).flatten() {\n                    jar.add_original(c.into_owned());\n                }\n            }\n        }\n\n        for (name, field) in self.schema.root().fields.iter() {\n            let cookie_name = field.name_override.as_deref().unwrap_or(name.as_str());\n\n            if let Some(c) = jar.get(cookie_name) {\n                // Only consider non-empty values to be present.\n                if !c.value().is_empty() {\n                    return true;\n                }\n            }\n        }\n        false\n    }\n\n    pub fn fields(&self) -> impl Iterator<Item = (&String, &jsonschema::Field)> {\n        self.schema.root().fields.iter()\n    }\n\n    pub fn parse_incoming_request_parts(\n        &self,\n        req: &axum::http::request::Parts,\n    ) -> APIResult<Option<PValues>> {\n        self.parse_req(&req.headers)\n    }\n\n    pub fn parse_req(&self, headers: &axum::http::HeaderMap) -> APIResult<Option<PValues>> {\n        if self.schema.root().is_empty() {\n            return Ok(None);\n        }\n\n        let mut jar = cookie::CookieJar::new();\n        headers\n            .get_all(COOKIE)\n            .iter()\n            .filter_map(|raw| raw.to_str().ok())\n            .flat_map(cookie::Cookie::split_parse)\n            .flatten()\n            .for_each(|c| jar.add_original(c.into_owned()));\n\n        match self.schema.parse(jar) {\n            Ok(decoded) => Ok(Some(decoded)),\n            Err(err) => Err(err),\n        }\n    }\n\n    pub fn parse_resp(&self, headers: &axum::http::HeaderMap) -> APIResult<Option<PValues>> {\n        if self.schema.root().is_empty() {\n            return Ok(None);\n        }\n\n        let mut jar = cookie::CookieJar::new();\n        headers\n            .get_all(SET_COOKIE)\n            .iter()\n            .filter_map(|raw| raw.to_str().ok())\n            .flat_map(cookie::Cookie::parse)\n            .for_each(|c| jar.add_original(c.into_owned()));\n\n        match self.schema.parse(jar) {\n            Ok(decoded) => Ok(Some(decoded)),\n            Err(err) => Err(err),\n        }\n    }\n}\n\nimpl ToOutgoingRequest<http::HeaderMap> for Cookie {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        headers: &mut http::HeaderMap,\n    ) -> APIResult<()> {\n        if self.schema.root().is_empty() {\n            return Ok(());\n        }\n\n        let Some(payload) = payload else {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing cookie parameters\".to_string(),\n                internal_message: Some(\"missing cookie parameters\".to_string()),\n                stack: None,\n                details: None,\n            });\n        };\n\n        for (key, field) in self.schema.root().fields.iter() {\n            let Some(PValue::Cookie(c)) = payload.get(key) else {\n                if field.optional {\n                    continue;\n                }\n                return Err(api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: format!(\"missing cookie parameter: {key}\"),\n                    internal_message: Some(format!(\"missing cookie parameter: {key}\")),\n                    stack: None,\n                    details: None,\n                });\n            };\n\n            let header_value =\n                HeaderValue::from_str(&c.to_string()).map_err(api::Error::internal)?;\n\n            headers.append(http::header::COOKIE, header_value);\n        }\n\n        Ok(())\n    }\n}\n\nimpl ToOutgoingRequest<reqwest::Request> for Cookie {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut reqwest::Request,\n    ) -> APIResult<()> {\n        let headers = req.headers_mut();\n        self.to_outgoing_request(payload, headers)\n    }\n}\n\nimpl<B> ToOutgoingRequest<http::Request<B>> for Cookie {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut http::Request<B>,\n    ) -> APIResult<()> {\n        let headers = req.headers_mut();\n        self.to_outgoing_request(payload, headers)\n    }\n}\n\nimpl ToResponse for Cookie {\n    fn to_response(\n        &self,\n        payload: &super::JSONPayload,\n        mut resp: axum::http::response::Builder,\n    ) -> APIResult<axum::http::response::Builder> {\n        if self.schema.root().is_empty() {\n            return Ok(resp);\n        }\n\n        let Some(payload) = payload else {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing cookie parameters\".to_string(),\n                internal_message: Some(\"missing cookie parameters\".to_string()),\n                stack: None,\n                details: None,\n            });\n        };\n\n        let schema = self.schema.root();\n        for (key, value) in payload.iter() {\n            let key = key.as_str();\n            if !schema.fields.contains_key(key) {\n                continue;\n            };\n            let PValue::Cookie(c) = value else {\n                continue;\n            };\n            resp = resp.header(SET_COOKIE, c.to_string());\n        }\n        Ok(resp)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/encoding.rs",
    "content": "use crate::api::jsonschema;\nuse crate::api::schema::{Body, Cookie, Header, HttpStatus, Method, Path, Query};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::parser::meta::v1::path_segment::SegmentType;\nuse crate::encore::parser::schema::v1 as schema;\nuse crate::encore::parser::schema::v1::r#type::Typ;\nuse anyhow::Context;\nuse std::borrow::Cow;\nuse std::collections::{HashMap, HashSet};\nuse std::sync::Arc;\n\n#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]\npub enum DefaultLoc {\n    Body,\n    Query,\n}\n\nimpl DefaultLoc {\n    pub fn into_wire_loc(self) -> WireLoc {\n        match self {\n            DefaultLoc::Body => WireLoc::Body,\n            DefaultLoc::Query => WireLoc::Query,\n        }\n    }\n}\n\n#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]\npub enum WireLoc {\n    Body,\n    Query,\n    Header(String),\n    Path,\n    Cookie(String),\n}\n\npub struct EncodingConfig<'a, 'b> {\n    pub meta: &'a meta::Data,\n    pub registry_builder: &'a mut jsonschema::Builder<'b>,\n    pub default_loc: Option<DefaultLoc>,\n    pub rpc_path: Option<&'a meta::Path>,\n    pub supports_body: bool,\n    pub supports_query: bool,\n    pub supports_header: bool,\n    pub supports_path: bool,\n    pub supports_http_status: bool,\n}\n\n#[derive(Debug)]\npub struct SchemaUnderConstruction {\n    combined: Option<usize>,\n    body: Option<usize>,\n    query: Option<usize>,\n    header: Option<usize>,\n    cookie: Option<usize>,\n    http_status: Option<HttpStatus>,\n    rpc_path: Option<meta::Path>,\n}\n\nimpl SchemaUnderConstruction {\n    pub fn build(self, reg: &Arc<jsonschema::Registry>) -> anyhow::Result<Schema> {\n        Ok(Schema {\n            combined: self.combined.map(|v| reg.schema(v)),\n            body: self.body.map(|v| Body::new(reg.schema(v))),\n            query: self.query.map(|v| Query::new(reg.schema(v))),\n            header: self.header.map(|v| Header::new(reg.schema(v))),\n            cookie: self.cookie.map(|v| Cookie::new(reg.schema(v))),\n            http_status: self.http_status,\n            path: self.rpc_path.as_ref().map(Path::from_meta).transpose()?,\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Schema {\n    pub combined: Option<jsonschema::JSONSchema>,\n    pub query: Option<Query>,\n    pub header: Option<Header>,\n    pub body: Option<Body>,\n    pub path: Option<Path>,\n    pub cookie: Option<Cookie>,\n    pub http_status: Option<HttpStatus>,\n}\n\nimpl EncodingConfig<'_, '_> {\n    pub fn compute(&mut self, typ: &schema::Type) -> anyhow::Result<SchemaUnderConstruction> {\n        let typ = typ.typ.as_ref().context(\"type without type\")?;\n        let typ = resolve_type(self.meta, typ)?;\n\n        let Typ::Struct(st) = typ.as_ref() else {\n            return Ok(SchemaUnderConstruction {\n                combined: None,\n                body: None,\n                query: None,\n                header: None,\n                cookie: None,\n                http_status: None,\n                rpc_path: self.rpc_path.cloned(),\n            });\n        };\n\n        // Determine which fields belong to the path, if any.\n        let path_fields = {\n            let mut path_fields = HashSet::new();\n            if let Some(rpc_path) = self.rpc_path {\n                for seg in &rpc_path.segments {\n                    let typ = SegmentType::try_from(seg.r#type).context(\"invalid segment type\")?;\n                    match typ {\n                        SegmentType::Literal => {}\n                        SegmentType::Param | SegmentType::Wildcard | SegmentType::Fallback => {\n                            path_fields.insert(seg.value.as_str());\n                        }\n                    }\n                }\n            }\n            path_fields\n        };\n\n        let mut combined = jsonschema::Struct::default();\n        let mut body: Option<jsonschema::Struct> = None;\n        let mut query: Option<jsonschema::Struct> = None;\n        let mut header: Option<jsonschema::Struct> = None;\n        let mut cookie: Option<jsonschema::Struct> = None;\n        let mut http_status: Option<HttpStatus> = None;\n\n        for f in &st.fields {\n            // If it's a path field, skip it. We handle it separately in Path::from_meta.\n            if path_fields.contains(f.name.as_str()) {\n                continue;\n            }\n\n            let (name, mut field) = self.registry_builder.struct_field(f)?;\n            combined.fields.insert(name.to_owned(), field.clone());\n\n            let default_loc = || -> anyhow::Result<DefaultLoc> {\n                self.default_loc\n                    .with_context(|| format!(\"no location defined for field {}\", f.name))\n            };\n\n            // Resolve which location the field should be in.\n            let loc = f.wire.as_ref().and_then(|w| w.location.as_ref());\n            let wire_loc = match loc {\n                None => default_loc()?.into_wire_loc(),\n                Some(schema::wire_spec::Location::Header(hdr)) => {\n                    WireLoc::Header(hdr.name.as_ref().unwrap_or(&f.name).clone())\n                }\n                Some(schema::wire_spec::Location::Query(_)) => WireLoc::Query,\n                Some(schema::wire_spec::Location::Cookie(c)) => {\n                    WireLoc::Cookie(c.name.as_ref().unwrap_or(&f.name).clone())\n                }\n                Some(schema::wire_spec::Location::HttpStatus(_)) => {\n                    if self.supports_http_status {\n                        http_status = Some(HttpStatus::new(f.name.clone()));\n                        continue;\n                    } else {\n                        default_loc()?.into_wire_loc()\n                    }\n                }\n            };\n\n            // Add the field to the appropriate struct.\n            let (dst, name_override) = match wire_loc {\n                WireLoc::Body => (&mut body, None),\n                WireLoc::Query => (&mut query, None),\n                WireLoc::Header(s) => (&mut header, Some(s)),\n                WireLoc::Path => unreachable!(),\n                WireLoc::Cookie(s) => (&mut cookie, Some(s)),\n            };\n            field.name_override = name_override;\n\n            match dst {\n                Some(dst) => {\n                    dst.fields.insert(name.to_owned(), field);\n                }\n                None => {\n                    *dst = Some(jsonschema::Struct {\n                        fields: {\n                            let mut fields = HashMap::new();\n                            fields.insert(name.to_owned(), field);\n                            fields\n                        },\n                    });\n                }\n            }\n        }\n\n        let mut build = |s| {\n            self.registry_builder\n                .register_value(jsonschema::Value::Struct(s))\n        };\n\n        Ok(SchemaUnderConstruction {\n            combined: Some(build(combined)),\n            body: body.map(&mut build),\n            query: query.map(&mut build),\n            header: header.map(&mut build),\n            cookie: cookie.map(&mut build),\n            http_status,\n            rpc_path: self.rpc_path.cloned(),\n        })\n    }\n\n    #[allow(dead_code)]\n    fn resolve_struct<'b>(\n        &'b self,\n        typ: &'b schema::Type,\n    ) -> anyhow::Result<Cow<'b, schema::Struct>> {\n        let typ = typ.typ.as_ref().context(\"type without type\")?;\n        match typ {\n            Typ::Struct(s) => Ok(Cow::Borrowed(s)),\n            Typ::Pointer(ptr) => {\n                let base = ptr.base.as_ref().context(\"pointer without base\")?;\n                self.resolve_struct(base)\n            }\n            Typ::Named(named) => {\n                let decl = &self.meta.decls[named.id as usize];\n                let typ = decl.r#type.as_ref().context(\"decl without type\")?;\n                self.resolve_struct(typ)\n            }\n            _ => anyhow::bail!(\"expected struct, got {:?}\", typ),\n        }\n    }\n}\n\nfn resolve_type<'a>(meta: &'a meta::Data, typ: &'a Typ) -> anyhow::Result<Cow<'a, Typ>> {\n    let resolver = TypeArgResolver {\n        meta,\n        resolved_args: vec![],\n        decls: vec![],\n    };\n    resolver.resolve(typ)\n}\n\nstruct TypeArgResolver<'a> {\n    meta: &'a meta::Data,\n    resolved_args: Vec<Cow<'a, Typ>>,\n\n    /// List of declarations being processed.\n    /// Used to detect cycles.\n    decls: Vec<u32>,\n}\n\nimpl<'a> TypeArgResolver<'a> {\n    fn resolve(&self, typ: &'a Typ) -> anyhow::Result<Cow<'a, Typ>> {\n        match typ {\n            Typ::Named(named) => {\n                let decl = &self.meta.decls[named.id as usize];\n                if self.decls.contains(&decl.id) {\n                    // Return it unmodified.\n                    return Ok(Cow::Borrowed(typ));\n                }\n\n                let args = self.resolve_types(&named.type_arguments)?;\n                let nested = TypeArgResolver {\n                    meta: self.meta,\n                    resolved_args: args,\n                    decls: {\n                        let mut decls = self.decls.clone();\n                        decls.push(decl.id);\n                        decls\n                    },\n                };\n                let typ = decl.r#type.as_ref().context(\"decl without type\")?;\n                let typ = typ.typ.as_ref().context(\"type without type\")?;\n                nested.resolve(typ)\n            }\n\n            Typ::Struct(strukt) => {\n                let mut cows = Vec::with_capacity(strukt.fields.len());\n                for field in &strukt.fields {\n                    let t = field.typ.as_ref().context(\"field without type\")?;\n                    let typ = t.typ.as_ref().context(\"type without type\")?;\n                    let resolved = self.resolve(typ)?;\n                    cows.push((resolved, t.validation.as_ref()));\n                }\n\n                let mut fields = Vec::with_capacity(strukt.fields.len());\n                for (field, (typ, v)) in strukt.fields.iter().zip(cows) {\n                    fields.push(schema::Field {\n                        typ: Some(schema::Type {\n                            typ: Some(typ.into_owned()),\n                            validation: v.cloned(),\n                        }),\n                        ..field.clone()\n                    });\n                }\n                Ok(Cow::Owned(Typ::Struct(schema::Struct { fields })))\n            }\n\n            Typ::Map(map) => {\n                let key = map.key.as_ref().context(\"map without key\")?;\n                let key_typ = key.typ.as_ref().context(\"key without type\")?;\n                let value = map.value.as_ref().context(\"map without value\")?;\n                let val_typ = value.typ.as_ref().context(\"value without type\")?;\n                let key_typ = self.resolve(key_typ)?;\n                let val_typ = self.resolve(val_typ)?;\n\n                if matches!((&key_typ, &val_typ), (Cow::Borrowed(_), Cow::Borrowed(_))) {\n                    Ok(Cow::Borrowed(typ))\n                } else {\n                    Ok(Cow::Owned(Typ::Map(Box::new(schema::Map {\n                        key: Some(Box::new(schema::Type {\n                            typ: Some(key_typ.into_owned()),\n                            validation: key.validation.clone(),\n                        })),\n                        value: Some(Box::new(schema::Type {\n                            typ: Some(val_typ.into_owned()),\n                            validation: value.validation.clone(),\n                        })),\n                    }))))\n                }\n            }\n\n            Typ::List(list) => {\n                let elem = list.elem.as_ref().context(\"list without elem\")?;\n                let elem_typ = elem.typ.as_ref().context(\"elem without type\")?;\n                let elem_typ = self.resolve(elem_typ)?;\n                if matches!(elem_typ, Cow::Borrowed(_)) {\n                    Ok(Cow::Borrowed(typ))\n                } else {\n                    Ok(Cow::Owned(Typ::List(Box::new(schema::List {\n                        elem: Some(Box::new(schema::Type {\n                            typ: Some(elem_typ.into_owned()),\n                            validation: elem.validation.clone(),\n                        })),\n                    }))))\n                }\n            }\n\n            Typ::Union(union) => {\n                let types = self.resolve_types(&union.types)?;\n                let types = types\n                    .into_iter()\n                    .zip(&union.types)\n                    .map(|(typ, t)| schema::Type {\n                        typ: Some(typ.into_owned()),\n                        validation: t.validation.clone(),\n                    })\n                    .collect::<Vec<_>>();\n\n                Ok(Cow::Owned(Typ::Union(schema::Union { types })))\n            }\n\n            Typ::Builtin(_) => Ok(Cow::Borrowed(typ)),\n\n            Typ::Literal(_) => Ok(Cow::Borrowed(typ)),\n\n            Typ::Pointer(ptr) => {\n                let base = ptr.base.as_ref().context(\"pointer without base\")?;\n                let typ = base.typ.as_ref().context(\"base without type\")?;\n                self.resolve(typ)\n            }\n\n            Typ::Option(opt) => {\n                let value = opt.value.as_ref().context(\"option without value\")?;\n                let inner = value.typ.as_ref().context(\"value without type\")?;\n                match self.resolve(inner)? {\n                    Cow::Borrowed(_) => Ok(Cow::Borrowed(typ)),\n                    Cow::Owned(inner) => Ok(Cow::Owned(Typ::Option(Box::new(schema::Option {\n                        value: Some(Box::new(schema::Type {\n                            typ: Some(inner),\n                            validation: value.validation.clone(),\n                        })),\n                    })))),\n                }\n            }\n\n            Typ::TypeParameter(param) => {\n                let idx = param.param_idx as usize;\n                let typ = &self.resolved_args[idx];\n                Ok(typ.clone())\n            }\n\n            Typ::Config(_cfg) => {\n                anyhow::bail!(\"config types are not supported\")\n            }\n        }\n    }\n\n    fn resolve_types(&self, types: &'a [schema::Type]) -> anyhow::Result<Vec<Cow<'a, Typ>>> {\n        types\n            .iter()\n            .map(|typ| {\n                let typ = typ.typ.as_ref().context(\"type without type\")?;\n                self.resolve(typ)\n            })\n            .collect()\n    }\n}\n\npub struct ReqSchemaUnderConstruction {\n    pub methods: Vec<Method>,\n    pub schema: SchemaUnderConstruction,\n}\n\nimpl ReqSchemaUnderConstruction {\n    pub fn build(self, reg: &Arc<jsonschema::Registry>) -> anyhow::Result<ReqSchema> {\n        Ok(ReqSchema {\n            methods: self.methods,\n            schema: self.schema.build(reg)?,\n        })\n    }\n}\n\npub struct ReqSchema {\n    pub methods: Vec<Method>,\n    pub schema: Schema,\n}\n\npub struct HandshakeSchemaUnderConstruction {\n    pub parse_data: bool,\n    pub schema: SchemaUnderConstruction,\n}\n\nimpl HandshakeSchemaUnderConstruction {\n    pub fn build(self, reg: &Arc<jsonschema::Registry>) -> anyhow::Result<HandshakeSchema> {\n        Ok(HandshakeSchema {\n            parse_data: self.parse_data,\n            schema: self.schema.build(reg)?,\n        })\n    }\n}\npub struct HandshakeSchema {\n    pub parse_data: bool,\n    pub schema: Schema,\n}\n\n/// Computes the handshake encoding for the given rpc.\npub fn handshake_encoding(\n    registry_builder: &mut jsonschema::Builder,\n    meta: &meta::Data,\n    rpc: &meta::Rpc,\n) -> anyhow::Result<Option<HandshakeSchemaUnderConstruction>> {\n    // Only streaming endpoints have a handshake schema\n    if !rpc.streaming_request && !rpc.streaming_response {\n        return Ok(None);\n    }\n\n    let default_path = meta::Path {\n        segments: vec![meta::PathSegment {\n            value: format!(\"{}.{}\", rpc.service_name, rpc.name),\n            r#type: SegmentType::Literal as i32,\n            value_type: meta::path_segment::ParamType::String as i32,\n            validation: None,\n        }],\n        r#type: meta::path::Type::Url as i32,\n    };\n    let rpc_path = rpc.path.as_ref().unwrap_or(&default_path);\n\n    let Some(handshake_schema) = &rpc.handshake_schema else {\n        let parse_data = rpc.path.as_ref().is_some_and(|path| {\n            path.segments\n                .iter()\n                .any(|segment| segment.r#type() != SegmentType::Literal)\n        });\n\n        return Ok(Some(HandshakeSchemaUnderConstruction {\n            parse_data,\n            schema: SchemaUnderConstruction {\n                combined: None,\n                http_status: None,\n                body: None,\n                query: None,\n                header: None,\n                cookie: None,\n                rpc_path: Some(rpc_path.clone()),\n            },\n        }));\n    };\n\n    let mut config = EncodingConfig {\n        meta,\n        registry_builder,\n        default_loc: Some(DefaultLoc::Query),\n        rpc_path: Some(rpc_path),\n        supports_body: false,\n        supports_query: true,\n        supports_header: true,\n        supports_path: true,\n        supports_http_status: false,\n    };\n\n    let schema = config.compute(handshake_schema)?;\n\n    Ok(Some(HandshakeSchemaUnderConstruction {\n        parse_data: true,\n        schema,\n    }))\n}\n\n/// Computes the request encoding for the given rpc.\npub fn request_encoding(\n    registry_builder: &mut jsonschema::Builder,\n    meta: &meta::Data,\n    rpc: &meta::Rpc,\n) -> anyhow::Result<Vec<ReqSchemaUnderConstruction>> {\n    // Streaming request can only have a body schema\n    if rpc.streaming_request || rpc.streaming_response {\n        let Some(request_schema) = &rpc.request_schema else {\n            return Ok(vec![ReqSchemaUnderConstruction {\n                methods: vec![Method::GET],\n                schema: SchemaUnderConstruction {\n                    combined: None,\n                    http_status: None,\n                    body: None,\n                    query: None,\n                    header: None,\n                    cookie: None,\n                    rpc_path: None,\n                },\n            }]);\n        };\n\n        let mut config = EncodingConfig {\n            meta,\n            registry_builder,\n            default_loc: Some(DefaultLoc::Body),\n            rpc_path: None,\n            supports_body: true,\n            supports_query: false,\n            supports_header: false,\n            supports_path: false,\n            supports_http_status: false,\n        };\n\n        let schema = config.compute(request_schema)?;\n        return Ok(vec![ReqSchemaUnderConstruction {\n            methods: vec![Method::GET],\n            schema,\n        }]);\n    }\n\n    // Compute the set of methods.\n    let methods = {\n        let methods: anyhow::Result<Vec<Method>> = rpc\n            .http_methods\n            .iter()\n            .map(|m| Method::try_from(m.as_str()))\n            .collect();\n        methods.context(\"unable to parse http methods\")?\n    };\n\n    let default_path = meta::Path {\n        segments: vec![meta::PathSegment {\n            value: format!(\"{}.{}\", rpc.service_name, rpc.name),\n            r#type: SegmentType::Literal as i32,\n            value_type: meta::path_segment::ParamType::String as i32,\n            validation: None,\n        }],\n        r#type: meta::path::Type::Url as i32,\n    };\n    let rpc_path = rpc.path.as_ref().unwrap_or(&default_path);\n\n    // If there is no request schema, use the same encoding for all methods.\n    let Some(request_schema) = &rpc.request_schema else {\n        return Ok(vec![ReqSchemaUnderConstruction {\n            methods,\n            schema: SchemaUnderConstruction {\n                combined: None,\n                http_status: None,\n                body: None,\n                query: None,\n                header: None,\n                cookie: None,\n                rpc_path: Some(rpc_path.clone()),\n            },\n        }]);\n    };\n\n    let mut schemas = Vec::new();\n\n    for default_loc in split_by_loc(&methods) {\n        let mut config = EncodingConfig {\n            meta,\n            registry_builder,\n            default_loc: Some(default_loc.0),\n            rpc_path: Some(rpc_path),\n            supports_body: true,\n            supports_query: true,\n            supports_header: true,\n            supports_path: true,\n            supports_http_status: false,\n        };\n        let schema = config.compute(request_schema)?;\n        schemas.push(ReqSchemaUnderConstruction {\n            methods: default_loc.1.clone(),\n            schema,\n        });\n    }\n\n    Ok(schemas)\n}\n\n/// Computes the request encoding for the given rpc.\npub fn response_encoding(\n    registry_builder: &mut jsonschema::Builder,\n    meta: &meta::Data,\n    rpc: &meta::Rpc,\n) -> anyhow::Result<SchemaUnderConstruction> {\n    let Some(response_schema) = &rpc.response_schema else {\n        return Ok(SchemaUnderConstruction {\n            combined: None,\n            body: None,\n            query: None,\n            header: None,\n            cookie: None,\n            http_status: None,\n            rpc_path: None,\n        });\n    };\n\n    let mut config = EncodingConfig {\n        meta,\n        registry_builder,\n        default_loc: Some(DefaultLoc::Body),\n        rpc_path: None,\n        supports_body: true,\n        supports_query: false,\n        supports_header: true,\n        supports_path: false,\n        supports_http_status: true,\n    };\n    config.compute(response_schema)\n}\n\nfn split_by_loc(methods: &[Method]) -> impl Iterator<Item = (DefaultLoc, Vec<Method>)> {\n    let mut locs = HashMap::new();\n    for m in methods {\n        let loc = if m.supports_body() {\n            DefaultLoc::Body\n        } else {\n            DefaultLoc::Query\n        };\n        locs.entry(loc).or_insert(Vec::new()).push(*m);\n    }\n\n    locs.into_iter()\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/header.rs",
    "content": "use crate::api::reqauth::meta::HeaderValueExt;\nuse crate::api::schema::{JSONPayload, ParseResponse, ToOutgoingRequest, ToResponse};\nuse crate::api::{self, PValue, PValues};\nuse crate::api::{jsonschema, APIResult};\nuse std::str::FromStr;\n\n#[derive(Debug, Clone)]\npub struct Header {\n    schema: jsonschema::JSONSchema,\n}\n\nimpl Header {\n    pub fn new(schema: jsonschema::JSONSchema) -> Self {\n        Self { schema }\n    }\n\n    pub fn contains_any(&self, headers: &impl HTTPHeaders) -> bool {\n        for (name, field) in self.schema.root().fields.iter() {\n            let header_name = field.name_override.as_deref().unwrap_or(name.as_str());\n\n            if let Some(val) = headers.get(header_name) {\n                // Only consider non-empty values to be present.\n                if !val.is_empty() {\n                    return true;\n                }\n            }\n        }\n        false\n    }\n\n    pub fn contains_key(&self, key: &str) -> bool {\n        self.schema.root().fields.contains_key(key)\n    }\n\n    pub fn len(&self) -> usize {\n        self.schema.root().fields.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    pub fn fields(&self) -> impl Iterator<Item = (&String, &jsonschema::Field)> {\n        self.schema.root().fields.iter()\n    }\n\n    /// Returns an iterator that yields the header names that are expected by the schema.\n    pub fn header_names(&self) -> impl Iterator<Item = axum::http::HeaderName> + '_ {\n        self.schema\n            .root()\n            .fields\n            .iter()\n            .filter_map(|(name, field)| {\n                let header_name = field.name_override.as_deref().unwrap_or(name.as_str());\n                axum::http::HeaderName::from_str(header_name).ok()\n            })\n    }\n}\n\npub trait AsStr {\n    fn as_str(&self) -> &str;\n}\n\npub trait ToHeaderStr {\n    type Error: std::error::Error;\n\n    fn to_str(&self) -> Result<&str, Self::Error>;\n    fn is_empty(&self) -> bool;\n}\n\nimpl AsStr for &axum::http::header::HeaderName {\n    fn as_str(&self) -> &str {\n        <axum::http::header::HeaderName>::as_str(self)\n    }\n}\n\nimpl ToHeaderStr for &axum::http::header::HeaderValue {\n    type Error = std::str::Utf8Error;\n\n    fn to_str(&self) -> Result<&str, Self::Error> {\n        <axum::http::header::HeaderValue>::to_utf8_str(self)\n    }\n    fn is_empty(&self) -> bool {\n        <axum::http::header::HeaderValue>::is_empty(self)\n    }\n}\n\npub trait HTTPHeaders {\n    type Name: AsStr;\n    type Value: ToHeaderStr;\n    type Iter: Iterator<Item = (Self::Name, Self::Value)>;\n    type GetAll: Iterator<Item = Self::Value>;\n\n    fn headers(&self) -> Self::Iter;\n    fn get(&self, key: &str) -> Option<Self::Value>;\n    fn get_all(&self, key: &str) -> Self::GetAll;\n    fn contains_key(&self, key: &str) -> bool;\n}\n\nimpl<'a> HTTPHeaders for &'a axum::http::HeaderMap {\n    type Name = &'a axum::http::header::HeaderName;\n    type Value = &'a axum::http::header::HeaderValue;\n    type Iter = axum::http::header::Iter<'a, axum::http::header::HeaderValue>;\n    type GetAll = axum::http::header::ValueIter<'a, axum::http::header::HeaderValue>;\n\n    fn headers(&self) -> Self::Iter {\n        self.iter()\n    }\n\n    fn get(&self, key: &str) -> Option<Self::Value> {\n        <axum::http::HeaderMap>::get(self, key)\n    }\n\n    fn get_all(&self, key: &str) -> Self::GetAll {\n        <axum::http::HeaderMap>::get_all(self, key).iter()\n    }\n\n    fn contains_key(&self, key: &str) -> bool {\n        <axum::http::HeaderMap>::contains_key(self, key)\n    }\n}\n\nimpl Header {\n    pub fn parse_incoming_request_parts(\n        &self,\n        req: &axum::http::request::Parts,\n    ) -> APIResult<Option<PValues>> {\n        self.parse(&req.headers)\n    }\n\n    pub fn parse(&self, headers: &axum::http::HeaderMap) -> APIResult<Option<PValues>> {\n        if self.schema.root().is_empty() {\n            return Ok(None);\n        }\n\n        match self.schema.parse(headers) {\n            Ok(decoded) => Ok(Some(decoded)),\n            Err(err) => Err(err),\n        }\n    }\n}\n\nimpl ParseResponse for Header {\n    type Output = Option<PValues>;\n\n    fn parse_response(&self, resp: &mut reqwest::Response) -> APIResult<Self::Output> {\n        if self.schema.root().is_empty() {\n            return Ok(None);\n        }\n\n        match self.schema.parse(resp.headers()) {\n            Ok(decoded) => Ok(Some(decoded)),\n            Err(err) => Err(err),\n        }\n    }\n}\n\nimpl ToOutgoingRequest<http::HeaderMap> for Header {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        headers: &mut http::HeaderMap,\n    ) -> APIResult<()> {\n        if self.schema.root().is_empty() {\n            return Ok(());\n        }\n\n        let Some(payload) = payload else {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing header parameters\".to_string(),\n                internal_message: Some(\"missing header parameters\".to_string()),\n                stack: None,\n                details: None,\n            });\n        };\n\n        for (key, field) in self.schema.root().fields.iter() {\n            let Some(value) = payload.get(key) else {\n                if field.optional {\n                    continue;\n                }\n                return Err(api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: format!(\"missing header parameter: {key}\"),\n                    internal_message: Some(format!(\"missing header parameter: {key}\")),\n                    stack: None,\n                    details: None,\n                });\n            };\n\n            let header_name = field.name_override.as_deref().unwrap_or(key);\n            let header_name =\n                reqwest::header::HeaderName::from_str(header_name).map_err(api::Error::internal)?;\n            match to_reqwest_header_value(value)? {\n                ReqwestHeaders::Single(value) => {\n                    headers.append(header_name, value);\n                }\n                ReqwestHeaders::Multi(values) => {\n                    for value in values {\n                        headers.append(header_name.clone(), value);\n                    }\n                }\n            }\n        }\n\n        Ok(())\n    }\n}\n\nimpl ToOutgoingRequest<reqwest::Request> for Header {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut reqwest::Request,\n    ) -> APIResult<()> {\n        let headers = req.headers_mut();\n        self.to_outgoing_request(payload, headers)\n    }\n}\n\nimpl<B> ToOutgoingRequest<http::Request<B>> for Header {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut http::Request<B>,\n    ) -> APIResult<()> {\n        let headers = req.headers_mut();\n        self.to_outgoing_request(payload, headers)\n    }\n}\n\nimpl ToResponse for Header {\n    fn to_response(\n        &self,\n        payload: &JSONPayload,\n        mut resp: axum::http::response::Builder,\n    ) -> APIResult<axum::http::response::Builder> {\n        if self.schema.root().is_empty() {\n            return Ok(resp);\n        }\n\n        let Some(payload) = payload else {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing header parameters\".to_string(),\n                internal_message: Some(\"missing header parameters\".to_string()),\n                stack: None,\n                details: None,\n            });\n        };\n\n        let schema = self.schema.root();\n        for (key, value) in payload.iter() {\n            let key = key.as_str();\n            let Some(field) = schema.fields.get(key) else {\n                continue; // Not a header.\n            };\n            let header_name = field.name_override.as_deref().unwrap_or(key);\n            let header_name = axum::http::header::HeaderName::from_str(header_name)\n                .map_err(api::Error::internal)?;\n\n            match to_axum_header_value(value)? {\n                AxumHeaders::Single(value) => resp = resp.header(header_name, value),\n                AxumHeaders::Multi(values) => {\n                    for value in values {\n                        resp = resp.header(header_name.clone(), value);\n                    }\n                }\n            }\n        }\n\n        Ok(resp)\n    }\n}\n\nenum ReqwestHeaders {\n    Single(reqwest::header::HeaderValue),\n    Multi(Vec<reqwest::header::HeaderValue>),\n}\n\nfn to_reqwest_header_value(value: &PValue) -> APIResult<ReqwestHeaders> {\n    use PValue::*;\n    use ReqwestHeaders::*;\n\n    Ok(Single(match value {\n        Null => reqwest::header::HeaderValue::from_static(\"null\"),\n\n        Bool(bool) => {\n            reqwest::header::HeaderValue::from_static(if *bool { \"true\" } else { \"false\" })\n        }\n\n        String(str) => reqwest::header::HeaderValue::from_str(str).map_err(|e| api::Error {\n            code: api::ErrCode::InvalidArgument,\n            message: \"unable to convert string to header value\".to_string(),\n            internal_message: Some(format!(\"unable to convert string to header value: {e}\")),\n            stack: None,\n            details: None,\n        })?,\n\n        DateTime(dt) => {\n            let s = dt.to_rfc3339();\n            reqwest::header::HeaderValue::from_str(&s).map_err(|e| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"unable to convert datetime to header value\".to_string(),\n                internal_message: Some(format!(\"unable to convert datetime to header value: {e}\")),\n                stack: None,\n                details: None,\n            })?\n        }\n\n        Number(num) => {\n            let str = num.to_string();\n            reqwest::header::HeaderValue::from_str(&str).map_err(|e| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"unable to convert number to header value\".to_string(),\n                internal_message: Some(format!(\"unable to convert number to header value: {e}\")),\n                stack: None,\n                details: None,\n            })?\n        }\n\n        Decimal(d) => {\n            reqwest::header::HeaderValue::from_str(&d.to_string()).map_err(|e| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"unable to convert decimal to header value\".to_string(),\n                internal_message: Some(format!(\"unable to convert decimal to header value: {e}\")),\n                stack: None,\n                details: None,\n            })?\n        }\n\n        Array(arr) => {\n            let mut values = Vec::with_capacity(arr.len());\n            for value in arr.iter() {\n                match to_reqwest_header_value(value)? {\n                    Single(value) => values.push(value),\n                    Multi(_) => {\n                        return Err(api::Error {\n                            code: api::ErrCode::InvalidArgument,\n                            message: \"nested array type unsupported as header value\".into(),\n                            internal_message: None,\n                            stack: None,\n                            details: None,\n                        })\n                    }\n                }\n            }\n            return Ok(Multi(values));\n        }\n\n        Object(_) => {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"map type unsupported as header value\".into(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            });\n        }\n        Cookie(_) => {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"cookie type unsupported as header value\".into(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            })\n        }\n    }))\n}\n\nenum AxumHeaders {\n    Single(axum::http::HeaderValue),\n    Multi(Vec<axum::http::HeaderValue>),\n}\n\nfn to_axum_header_value(value: &PValue) -> APIResult<AxumHeaders> {\n    use PValue::*;\n\n    Ok(AxumHeaders::Single(match value {\n        Null => axum::http::HeaderValue::from_static(\"null\"),\n\n        Bool(bool) => axum::http::HeaderValue::from_static(if *bool { \"true\" } else { \"false\" }),\n\n        String(str) => axum::http::HeaderValue::from_str(str).map_err(|e| api::Error {\n            code: api::ErrCode::InvalidArgument,\n            message: \"unable to convert string to header value\".to_string(),\n            internal_message: Some(format!(\"unable to convert string to header value: {e}\")),\n            stack: None,\n            details: None,\n        })?,\n\n        DateTime(dt) => {\n            let s = dt.to_rfc3339();\n            axum::http::HeaderValue::from_str(&s).map_err(|e| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"unable to convert datetime to header value\".to_string(),\n                internal_message: Some(format!(\"unable to convert datetime to header value: {e}\")),\n                stack: None,\n                details: None,\n            })?\n        }\n\n        Number(num) => {\n            let str = num.to_string();\n            axum::http::HeaderValue::from_str(&str).map_err(|e| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"unable to convert number to header value\".to_string(),\n                internal_message: Some(format!(\"unable to convert number to header value: {e}\")),\n                stack: None,\n                details: None,\n            })?\n        }\n\n        Decimal(d) => {\n            axum::http::HeaderValue::from_str(&d.to_string()).map_err(|e| api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"unable to convert decimal to header value\".to_string(),\n                internal_message: Some(format!(\"unable to convert decimal to header value: {e}\")),\n                stack: None,\n                details: None,\n            })?\n        }\n\n        Array(arr) => {\n            let mut values = Vec::with_capacity(arr.len());\n            for value in arr.iter() {\n                match to_axum_header_value(value)? {\n                    AxumHeaders::Single(value) => values.push(value),\n                    AxumHeaders::Multi(_) => {\n                        return Err(api::Error {\n                            code: api::ErrCode::InvalidArgument,\n                            message: \"nested array type unsupported as header value\".into(),\n                            internal_message: None,\n                            stack: None,\n                            details: None,\n                        })\n                    }\n                }\n            }\n            return Ok(AxumHeaders::Multi(values));\n        }\n\n        Object(_) => {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"map type unsupported as header value\".into(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            });\n        }\n\n        Cookie(_) => {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"cookie type unsupported as header value\".into(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            })\n        }\n    }))\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/httpstatus.rs",
    "content": "use crate::api::{APIResult, PValues};\nuse axum::http::response::Builder as ResponseBuilder;\n\n/// HTTP status code parameter specification.\n#[derive(Debug, Clone)]\npub struct HttpStatus {\n    pub name: String,\n}\n\nimpl HttpStatus {\n    pub fn new(name: String) -> Self {\n        Self { name }\n    }\n\n    /// Extract HTTP status code from response payload and apply it to the response builder.\n    pub fn to_response(\n        &self,\n        payload: &Option<PValues>,\n        mut resp_builder: ResponseBuilder,\n    ) -> APIResult<ResponseBuilder> {\n        if let Some(payload) = payload {\n            if let Some(status_value) = payload.get(&self.name) {\n                let status_code = self.extract_status_code(status_value)?;\n                resp_builder = resp_builder.status(status_code);\n            }\n        }\n        Ok(resp_builder)\n    }\n\n    /// Extract and validate HTTP status code from a PValue.\n    fn extract_status_code(&self, status_value: &crate::api::PValue) -> APIResult<u16> {\n        let status_code = match status_value {\n            crate::api::PValue::Number(n) => n.as_u64().ok_or_else(|| {\n                crate::api::Error::invalid_argument(\n                    \"invalid http status code\",\n                    anyhow::anyhow!(\n                        \"HTTP status field '{}' must be a positive integer\",\n                        self.name\n                    ),\n                )\n            })?,\n            _ => {\n                return Err(crate::api::Error::invalid_argument(\n                    \"invalid http status code\",\n                    anyhow::anyhow!(\n                        \"HTTP status field '{}' must be a number, got: {}\",\n                        self.name,\n                        status_value.type_name()\n                    ),\n                ));\n            }\n        };\n\n        if !(100..=599).contains(&status_code) {\n            return Err(crate::api::Error::invalid_argument(\n                \"invalid http status code\",\n                anyhow::anyhow!(\n                    \"HTTP status field '{}' must be a between 100 and 599, got: {status_code}\",\n                    self.name,\n                ),\n            ));\n        }\n\n        Ok(status_code as u16)\n    }\n}\n\nimpl super::ToResponse for HttpStatus {\n    fn to_response(\n        &self,\n        payload: &super::JSONPayload,\n        resp: axum::http::response::Builder,\n    ) -> APIResult<axum::http::response::Builder> {\n        self.to_response(payload, resp)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/method.rs",
    "content": "#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]\npub enum Method {\n    GET,\n    HEAD,\n    POST,\n    PUT,\n    DELETE,\n    // CONNECT,\n    OPTIONS,\n    TRACE,\n    PATCH,\n}\n\nimpl Method {\n    pub fn as_str(self) -> &'static str {\n        match self {\n            Method::GET => \"GET\",\n            Method::HEAD => \"HEAD\",\n            Method::POST => \"POST\",\n            Method::PUT => \"PUT\",\n            Method::DELETE => \"DELETE\",\n            // Method::CONNECT => \"CONNECT\",\n            Method::OPTIONS => \"OPTIONS\",\n            Method::TRACE => \"TRACE\",\n            Method::PATCH => \"PATCH\",\n        }\n    }\n\n    /// Whether the method supports a request body.\n    pub fn supports_body(&self) -> bool {\n        match self {\n            Self::POST | Self::PUT | Self::PATCH /* | Self::CONNECT */ => true,\n            Self::GET | Self::DELETE | Self::HEAD | Self::OPTIONS | Self::TRACE => false,\n        }\n    }\n}\n\nimpl TryFrom<&str> for Method {\n    type Error = anyhow::Error;\n\n    fn try_from(s: &str) -> Result<Self, Self::Error> {\n        match s {\n            \"GET\" => Ok(Method::GET),\n            \"HEAD\" => Ok(Method::HEAD),\n            \"POST\" => Ok(Method::POST),\n            \"PUT\" => Ok(Method::PUT),\n            \"DELETE\" => Ok(Method::DELETE),\n            // \"CONNECT\" => Ok(Method::CONNECT),\n            \"OPTIONS\" => Ok(Method::OPTIONS),\n            \"TRACE\" => Ok(Method::TRACE),\n            \"PATCH\" => Ok(Method::PATCH),\n            _ => Err(anyhow::anyhow!(\"invalid method: {}\", s)),\n        }\n    }\n}\n\nimpl From<Method> for axum::http::Method {\n    fn from(val: Method) -> Self {\n        match val {\n            Method::GET => axum::http::Method::GET,\n            Method::HEAD => axum::http::Method::HEAD,\n            Method::POST => axum::http::Method::POST,\n            Method::PUT => axum::http::Method::PUT,\n            Method::DELETE => axum::http::Method::DELETE,\n            // Method::CONNECT => http::Method::CONNECT,\n            Method::OPTIONS => axum::http::Method::OPTIONS,\n            Method::TRACE => axum::http::Method::TRACE,\n            Method::PATCH => axum::http::Method::PATCH,\n        }\n    }\n}\n\nimpl TryFrom<axum::http::Method> for Method {\n    type Error = anyhow::Error;\n    fn try_from(m: axum::http::Method) -> anyhow::Result<Self> {\n        Ok(match m.as_str() {\n            \"GET\" => Method::GET,\n            \"HEAD\" => Method::HEAD,\n            \"POST\" => Method::POST,\n            \"PUT\" => Method::PUT,\n            \"DELETE\" => Method::DELETE,\n            // \"CONNECT\" => Method::CONNECT,\n            \"OPTIONS\" => Method::OPTIONS,\n            \"TRACE\" => Method::TRACE,\n            \"PATCH\" => Method::PATCH,\n            x => anyhow::bail!(\"invalid method: {}\", x),\n        })\n    }\n}\n\npub fn method_filter<M>(methods: M) -> Option<axum::routing::MethodFilter>\nwhere\n    M: Iterator<Item = Method>,\n{\n    use axum::routing::MethodFilter;\n    let mut filter = None;\n    for method in methods {\n        let method_filter = match method {\n            Method::GET => MethodFilter::GET,\n            Method::HEAD => MethodFilter::HEAD,\n            Method::POST => MethodFilter::POST,\n            Method::PUT => MethodFilter::PUT,\n            Method::DELETE => MethodFilter::DELETE,\n            // Method::CONNECT => MethodFilter::CONNECT,\n            Method::OPTIONS => MethodFilter::OPTIONS,\n            Method::TRACE => MethodFilter::TRACE,\n            Method::PATCH => MethodFilter::PATCH,\n        };\n        filter = Some(filter.unwrap_or(method_filter).or(method_filter));\n    }\n    filter\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/mod.rs",
    "content": "use crate::api;\npub use body::*;\npub use cookie::*;\npub use header::*;\npub use httpstatus::*;\npub use method::*;\npub use path::*;\npub use query::*;\nuse std::sync::Arc;\n\nuse crate::api::{endpoint, APIResult, PValues, RequestPayload};\n\nuse super::ResponsePayload;\n\nmod body;\nmod cookie;\npub mod encoding;\nmod header;\nmod httpstatus;\nmod method;\nmod path;\nmod query;\n\npub type JSONPayload = Option<PValues>;\n\npub trait ToOutgoingRequest<Request> {\n    fn to_outgoing_request(&self, payload: &mut JSONPayload, req: &mut Request) -> APIResult<()>;\n}\n\npub trait ToResponse {\n    fn to_response(\n        &self,\n        payload: &JSONPayload,\n        resp: axum::http::response::Builder,\n    ) -> APIResult<axum::http::response::Builder>;\n}\n\npub trait ParseResponse {\n    type Output: Sized;\n    fn parse_response(&self, resp: &mut reqwest::Response) -> APIResult<Self::Output>;\n}\n\n/// The request schema for a set of methods.\n#[derive(Debug)]\npub struct Request {\n    /// The methods the schema is applicable for.\n    pub methods: Vec<Method>,\n\n    /// Path to reach the endpoint.\n    pub path: Path,\n\n    /// Header names used by the endpoint.\n    pub header: Option<Header>,\n\n    /// Cookie names used by the endpoint.\n    pub cookie: Option<Cookie>,\n\n    /// Query string names used by the endpoint.\n    pub query: Option<Query>,\n\n    /// Request body.\n    pub body: RequestBody,\n\n    /// If this is a streamed request\n    pub stream: bool,\n}\n\n#[derive(Debug)]\npub enum RequestBody {\n    Typed(Option<Body>),\n    Raw,\n}\n\nimpl Request {\n    pub async fn extract(\n        &self,\n        parts: &mut axum::http::request::Parts,\n        body: axum::body::Body,\n    ) -> APIResult<Option<RequestPayload>> {\n        let path = self.path.parse_incoming_request_parts(parts)?;\n        let query = match &self.query {\n            None => None,\n            Some(q) => q.parse_incoming_request_parts(parts)?,\n        };\n        let header = match &self.header {\n            None => None,\n            Some(h) => h.parse_incoming_request_parts(parts)?,\n        };\n        let cookie = match &self.cookie {\n            None => None,\n            Some(c) => c.parse_incoming_request_parts(parts)?,\n        };\n\n        let body = match &self.body {\n            RequestBody::Raw => endpoint::Body::Raw(Arc::new(std::sync::Mutex::new(Some(body)))),\n            RequestBody::Typed(None) => endpoint::Body::Typed(None),\n            RequestBody::Typed(Some(b)) => {\n                endpoint::Body::Typed(b.parse_incoming_request_body(body).await?)\n            }\n        };\n\n        Ok(Some(RequestPayload {\n            path,\n            query,\n            header,\n            cookie,\n            body,\n        }))\n    }\n}\n\n/// The response schema for an endpoint.\n#[derive(Debug)]\npub struct Response {\n    /// Response header names returned by the endpoint.\n    pub header: Option<Header>,\n\n    /// Response cookie names returned by the endpoint.\n    pub cookie: Option<Cookie>,\n\n    /// Response body, if any.\n    pub body: Option<Body>,\n\n    /// HTTP status code field, if any.\n    pub http_status: Option<HttpStatus>,\n\n    /// If this is a streamed response\n    pub stream: bool,\n}\n\nimpl Response {\n    pub fn encode(\n        &self,\n        payload: &JSONPayload,\n        status: u16,\n    ) -> APIResult<axum::http::Response<axum::body::Body>> {\n        let mut bld = axum::http::Response::builder().status(status);\n\n        if let Some(hdr) = &self.header {\n            bld = hdr.to_response(payload, bld)?\n        };\n        if let Some(c) = &self.cookie {\n            bld = c.to_response(payload, bld)?;\n        }\n        if let Some(hs) = &self.http_status {\n            bld = hs.to_response(payload, bld)?;\n        }\n        match &self.body {\n            Some(body) => body.to_response(payload, bld),\n            None => bld\n                .body(axum::body::Body::empty())\n                .map_err(api::Error::internal),\n        }\n    }\n\n    pub async fn extract(&self, resp: reqwest::Response) -> APIResult<ResponsePayload> {\n        let header = match &self.header {\n            None => None,\n            Some(h) => h.parse(resp.headers())?,\n        };\n\n        let cookie = match &self.cookie {\n            None => None,\n            Some(c) => c.parse_resp(resp.headers())?,\n        };\n\n        // Do we have a body schema?\n        let body = endpoint::Body::Typed(match &self.body {\n            None => None,\n            Some(body_schema) => {\n                // If so we expect a JSON body.\n                match resp.headers().get(axum::http::header::CONTENT_TYPE) {\n                    Some(content_type) if content_type == \"application/json\" => {\n                        // Collect the bytes of the request body.\n                        // Serde doesn't support async streaming reads (at least not yet).\n                        let bytes = resp.bytes().await.map_err(|e| {\n                            api::Error::invalid_argument(\"unable to read response body\", e)\n                        })?;\n\n                        body_schema.parse_response_body(bytes).await?\n                    }\n                    _ => {\n                        // We didn't get a JSON body, so return an error.\n                        return Err(api::Error::internal(anyhow::anyhow!(\"expected json body\")));\n                    }\n                }\n            }\n        });\n\n        Ok(ResponsePayload {\n            header,\n            body,\n            cookie,\n        })\n    }\n}\n\npub struct Stream {\n    incoming: Box<dyn StreamMessage>,\n    outgoing: Box<dyn StreamMessage>,\n}\n\nimpl Stream {\n    pub fn new<I, O>(incoming: I, outgoing: O) -> Self\n    where\n        I: StreamMessage,\n        O: StreamMessage,\n    {\n        Stream {\n            incoming: Box::new(incoming),\n            outgoing: Box::new(outgoing),\n        }\n    }\n\n    pub fn to_outgoing_message(&self, msg: PValues) -> APIResult<Vec<u8>> {\n        let body_schema = self.outgoing.body().ok_or_else(|| {\n            super::Error::internal(anyhow::anyhow!(\"outgoing message body can't be empty\"))\n        })?;\n\n        body_schema.to_outgoing_payload(&Some(msg))\n    }\n\n    pub async fn parse_incoming_message(&self, bytes: &[u8]) -> APIResult<PValues> {\n        let Some(body) = self.incoming.body() else {\n            return Err(super::Error {\n                code: super::ErrCode::InvalidArgument,\n                message: \"invalid streaming body type in schema\".to_string(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            });\n        };\n\n        let value = body\n            .parse_incoming_request_body(bytes.to_vec().into())\n            .await?\n            .ok_or_else(|| super::Error {\n                code: super::ErrCode::InvalidArgument,\n                message: \"missing payload\".to_string(),\n                internal_message: None,\n                stack: None,\n                details: None,\n            })?;\n\n        Ok(value)\n    }\n}\n\npub trait StreamMessage: Send + Sync + 'static {\n    fn body(&self) -> Option<&Body>;\n}\n\nimpl StreamMessage for Arc<Response> {\n    fn body(&self) -> Option<&Body> {\n        self.body.as_ref()\n    }\n}\n\nimpl StreamMessage for Arc<Request> {\n    fn body(&self) -> Option<&Body> {\n        if let RequestBody::Typed(body) = &self.body {\n            body.as_ref()\n        } else {\n            None\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/path.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::ptr;\nuse std::str::FromStr;\nuse std::task::{Poll, RawWaker, RawWakerVTable, Waker};\n\nuse anyhow::Context;\nuse axum::extract::rejection::PathRejection;\nuse axum::extract::FromRequestParts;\nuse axum::http::request::Parts;\nuse indexmap::IndexMap;\n\nuse crate::api::jsonschema::Basic;\nuse crate::api::schema::JSONPayload;\nuse crate::api::{self, pvalue::PValue};\nuse crate::api::{jsonschema, APIResult};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::parser::meta::v1::path_segment::ParamType;\n\n/// The URL path to an endpoint, e.g. (\"/foo/bar/:id\").\n#[derive(Debug, Clone)]\npub struct Path {\n    /// The path segments.\n    segments: Vec<Segment>,\n    dynamic_segments: Vec<DynamicSegment>,\n\n    /// The capacity to use for generating requests.\n    capacity: usize,\n}\n\n#[derive(Debug, Clone)]\nstruct DynamicSegment {\n    name: Box<str>,\n    typ: Basic,\n    validation: Option<jsonschema::validation::Expr>,\n}\n\nimpl Path {\n    pub fn from_meta(path: &meta::Path) -> anyhow::Result<Self> {\n        let mut segments = Vec::with_capacity(path.segments.len());\n        for seg in &path.segments {\n            use meta::path_segment::SegmentType;\n            let validation = seg\n                .validation\n                .as_ref()\n                .map(|v| {\n                    jsonschema::validation::Expr::try_from(v)\n                        .context(\"invalid path segment validation\")\n                })\n                .transpose()?;\n\n            match SegmentType::try_from(seg.r#type).context(\"invalid path segment type\")? {\n                SegmentType::Literal => {\n                    segments.push(Segment::Literal(seg.value.clone().into_boxed_str()))\n                }\n                SegmentType::Param => {\n                    let name = &seg.value;\n                    let typ = match ParamType::try_from(seg.value_type)\n                        .context(\"invalid path parameter type\")?\n                    {\n                        ParamType::String => Basic::String,\n                        ParamType::Bool => Basic::Bool,\n                        ParamType::Uuid => Basic::String,\n                        ParamType::Int\n                        | ParamType::Int8\n                        | ParamType::Int16\n                        | ParamType::Int32\n                        | ParamType::Int64\n                        | ParamType::Uint\n                        | ParamType::Uint8\n                        | ParamType::Uint16\n                        | ParamType::Uint32\n                        | ParamType::Uint64 => Basic::Number,\n                    };\n\n                    segments.push(Segment::Param {\n                        name: name.clone().into_boxed_str(),\n                        typ,\n                        validation,\n                    });\n                }\n\n                SegmentType::Wildcard => segments.push(Segment::Wildcard {\n                    name: seg.clone().value.into_boxed_str(),\n                    validation,\n                }),\n                SegmentType::Fallback => segments.push(Segment::Fallback {\n                    name: seg.clone().value.into_boxed_str(),\n                    validation,\n                }),\n            }\n        }\n\n        Ok(Self::from_segments(segments))\n    }\n\n    pub fn from_segments(segments: Vec<Segment>) -> Self {\n        let mut capacity = 0;\n        let mut dynamic_segments = Vec::new();\n        for seg in segments.iter() {\n            use Segment::*;\n            capacity += 1; // slash\n            match seg {\n                Literal(lit) => capacity += lit.len(),\n                Param {\n                    name,\n                    typ,\n                    validation,\n                } => {\n                    capacity += 10; // assume path parameters on average are 10 characters long\n                    dynamic_segments.push(DynamicSegment {\n                        name: name.clone(),\n                        typ: *typ,\n                        validation: validation.clone(),\n                    });\n                }\n                Wildcard { name, validation } | Fallback { name, validation } => {\n                    // Assume path parameters on average are 10 characters long.\n                    capacity += 10;\n                    dynamic_segments.push(DynamicSegment {\n                        name: name.clone(),\n                        typ: jsonschema::Basic::String,\n                        validation: validation.clone(),\n                    });\n                }\n            }\n        }\n\n        Self {\n            segments,\n            dynamic_segments,\n            capacity,\n        }\n    }\n}\n\n/// Represents a path segment.\n#[derive(Debug, Clone)]\npub enum Segment {\n    Literal(Box<str>),\n    Param {\n        name: Box<str>,\n        /// The type of the path parameter.\n        typ: jsonschema::Basic,\n        validation: Option<jsonschema::validation::Expr>,\n    },\n    Wildcard {\n        name: Box<str>,\n        validation: Option<jsonschema::validation::Expr>,\n    },\n    Fallback {\n        name: Box<str>,\n        validation: Option<jsonschema::validation::Expr>,\n    },\n}\n\nimpl Path {\n    /// Computes the request path to use for making an API call to this path with the given payload.\n    pub fn to_request_path(&self, payload: &mut JSONPayload) -> Result<String, api::Error> {\n        let mut path = String::with_capacity(self.capacity);\n        for seg in self.segments.iter() {\n            path.push('/');\n\n            use Segment::*;\n            match seg {\n                Literal(lit) => path.push_str(lit),\n                Param { name, .. } | Wildcard { name, .. } | Fallback { name, .. } => {\n                    let Some(payload) = payload else {\n                        return Err(api::Error {\n                            code: api::ErrCode::InvalidArgument,\n                            message: \"missing field in request payload\".into(),\n                            internal_message: Some(format!(\n                                \"missing field in request payload: {}\",\n                                &name\n                            )),\n                            stack: None,\n                            details: None,\n                        });\n                    };\n\n                    // Find the data in the payload.\n                    let Some(value) = payload.get(name.as_ref()) else {\n                        return Err(api::Error {\n                            code: api::ErrCode::InvalidArgument,\n                            message: \"missing field in request payload\".into(),\n                            internal_message: Some(format!(\n                                \"missing field in request payload: {}\",\n                                &name\n                            )),\n                            stack: None,\n                            details: None,\n                        });\n                    };\n\n                    match value {\n                        PValue::String(str) => {\n                            // URL-encode the string, so it doesn't get reinterpreted\n                            // as multiple path segments.\n                            let encoded = urlencoding::encode(str);\n                            path.push_str(&encoded);\n                        }\n                        PValue::Null => path.push_str(\"null\"),\n                        PValue::Bool(bool) => path.push_str(if *bool { \"true\" } else { \"false\" }),\n                        PValue::Number(num) => {\n                            let str = num.to_string();\n                            path.push_str(&str);\n                        }\n                        PValue::Decimal(d) => {\n                            let str = d.to_string();\n                            path.push_str(&str);\n                        }\n                        PValue::DateTime(dt) => {\n                            let encoded = dt.to_rfc3339();\n                            path.push_str(&encoded);\n                        }\n                        PValue::Array(_) | PValue::Object(_) | PValue::Cookie(_) => {\n                            return Err(api::Error {\n                                code: api::ErrCode::InvalidArgument,\n                                message: \"unsupported type in request payload\".into(),\n                                internal_message: Some(format!(\n                                    \"unsupported type in request payload for field {}\",\n                                    &name\n                                )),\n                                stack: None,\n                                details: None,\n                            })\n                        }\n                    }\n                }\n            }\n        }\n\n        Ok(path)\n    }\n}\n\nimpl Path {\n    pub fn parse_incoming_request_parts(\n        &self,\n        req: &mut Parts,\n    ) -> APIResult<Option<IndexMap<String, PValue>>> {\n        if self.dynamic_segments.is_empty() {\n            return Ok(None);\n        }\n\n        let fut = axum::extract::Path::<Vec<(String, String)>>::from_request_parts(req, &());\n\n        let result = resolve_immediate_future(fut).map_err(|_| api::Error {\n            code: api::ErrCode::InvalidArgument,\n            message: \"unable to parse path params\".into(),\n            internal_message: Some(\"polling path params returned pending\".into()),\n            stack: None,\n            details: None,\n        })?;\n\n        match result {\n            Ok(axum::extract::Path(params)) => {\n                let mut map = IndexMap::with_capacity(params.len());\n\n                // For each param, find the corresponding segment and deserialize it.\n                for (idx, (_axum_name, val)) in params.into_iter().enumerate() {\n                    if let Some(DynamicSegment {\n                        name,\n                        typ,\n                        validation,\n                    }) = self.dynamic_segments.get(idx)\n                    {\n                        // Decode it into the correct type based on the type.\n                        let val = match &typ {\n                            // For strings and any, use the value directly.\n                            Basic::String | Basic::Any => PValue::String(val),\n\n                            // For numbers and booleans, use the JSON parser.\n                            Basic::Number => {\n                                let val = serde_json::from_str::<serde_json::Number>(&val)\n                                    .map_err(|err| api::Error {\n                                        code: api::ErrCode::InvalidArgument,\n                                        message: \"path parameter is not a valid number\".into(),\n                                        internal_message: Some(err.to_string()),\n                                        stack: None,\n                                        details: None,\n                                    })?;\n                                PValue::Number(val)\n                            }\n                            Basic::Bool => {\n                                let val = serde_json::from_str::<bool>(&val).map_err(|err| {\n                                    api::Error {\n                                        code: api::ErrCode::InvalidArgument,\n                                        message: \"path parameter is not a valid boolean\".into(),\n                                        internal_message: Some(err.to_string()),\n                                        stack: None,\n                                        details: None,\n                                    }\n                                })?;\n                                PValue::Bool(val)\n                            }\n\n                            Basic::DateTime => {\n                                let val =\n                                    api::DateTime::parse_from_rfc3339(&val).map_err(|err| {\n                                        api::Error {\n                                            code: api::ErrCode::InvalidArgument,\n                                            message: \"path parameter is not a valid datetime\"\n                                                .into(),\n                                            internal_message: Some(err.to_string()),\n                                            stack: None,\n                                            details: None,\n                                        }\n                                    })?;\n                                PValue::DateTime(val)\n                            }\n\n                            Basic::Decimal => {\n                                let val =\n                                    api::Decimal::from_str(&val).map_err(|err| api::Error {\n                                        code: api::ErrCode::InvalidArgument,\n                                        message: \"path parameter is not a valid decimal\".into(),\n                                        internal_message: Some(err.to_string()),\n                                        stack: None,\n                                        details: None,\n                                    })?;\n                                PValue::Decimal(val)\n                            }\n\n                            // We shouldn't have null here, but handle it just in case.\n                            Basic::Null => PValue::Null,\n                        };\n\n                        // Validate the value, if we have a validation expression.\n                        if let Some(validation) = validation.as_ref() {\n                            if let Err(err) = validation.validate_pval(&val) {\n                                return Err(api::Error {\n                                    code: api::ErrCode::InvalidArgument,\n                                    message: format!(\"invalid path parameter {name}: {err}\"),\n                                    internal_message: None,\n                                    stack: None,\n                                    details: None,\n                                });\n                            }\n                        }\n\n                        map.insert(name.to_string(), val);\n                    }\n                }\n\n                Ok(Some(map))\n            }\n            Err(err) => Err(match err {\n                PathRejection::FailedToDeserializePathParams(err) => api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: \"unable to parse path params\".into(),\n                    internal_message: Some(err.to_string()),\n                    stack: None,\n                    details: None,\n                },\n                PathRejection::MissingPathParams(err) => api::Error {\n                    code: api::ErrCode::InvalidArgument,\n                    message: \"missing path params\".into(),\n                    internal_message: Some(err.to_string()),\n                    stack: None,\n                    details: None,\n                },\n                err => api::Error {\n                    code: api::ErrCode::Internal,\n                    message: \"unable to parse path params\".into(),\n                    internal_message: Some(err.to_string()),\n                    stack: None,\n                    details: None,\n                },\n            }),\n        }\n    }\n}\n\nstruct FuturePendingError;\n\nfn resolve_immediate_future<F, T>(mut fut: F) -> Result<T, FuturePendingError>\nwhere\n    F: Future<Output = T> + Unpin,\n{\n    let waker = noop_waker();\n    let mut cx = std::task::Context::from_waker(&waker);\n\n    match Pin::new(&mut fut).poll(&mut cx) {\n        Poll::Ready(value) => Ok(value),\n        Poll::Pending => Err(FuturePendingError),\n    }\n}\n\nfn noop_waker() -> Waker {\n    const VTABLE: RawWakerVTable = RawWakerVTable::new(\n        // Cloning just returns a new no-op raw waker\n        |_| RAW,\n        // `wake` does nothing\n        |_| {},\n        // `wake_by_ref` does nothing\n        |_| {},\n        // Dropping does nothing as we don't allocate anything\n        |_| {},\n    );\n    const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE);\n\n    // SAFETY: This is copied from https://github.com/rust-lang/rust/pull/96875.\n    unsafe { Waker::from_raw(RAW) }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/schema/query.rs",
    "content": "use std::str::FromStr;\n\nuse crate::api::jsonschema::DecodeConfig;\nuse crate::api::schema::{JSONPayload, ToOutgoingRequest};\nuse crate::api::{self, PValues};\nuse crate::api::{jsonschema, APIResult};\nuse serde::Serialize;\nuse url::Url;\n\n#[derive(Debug, Clone)]\npub struct Query {\n    schema: jsonschema::JSONSchema,\n}\n\nimpl Query {\n    pub fn new(schema: jsonschema::JSONSchema) -> Self {\n        Self { schema }\n    }\n\n    pub fn parse_incoming_request_parts(\n        &self,\n        req: &axum::http::request::Parts,\n    ) -> APIResult<Option<PValues>> {\n        self.parse(req.uri.query())\n    }\n\n    pub fn parse(&self, query: Option<&str>) -> APIResult<Option<PValues>> {\n        let parsed = form_urlencoded::parse(query.unwrap_or_default().as_bytes());\n        let de = serde_urlencoded::Deserializer::new(parsed);\n        let cfg = DecodeConfig {\n            coerce_strings: true,\n            arrays_as_repeated_fields: true,\n        };\n\n        let decoded = self\n            .schema\n            .deserialize(de, cfg)\n            .map_err(|e| api::Error::invalid_argument(\"unable to decode query string\", e))?;\n\n        Ok(Some(decoded))\n    }\n\n    pub fn contains_name(&self, name: &str) -> bool {\n        self.schema.root().contains_name(name)\n    }\n\n    pub fn contains_any(&self, query: &[u8]) -> bool {\n        let schema = &self.schema.root();\n        if schema.is_empty() {\n            return false;\n        }\n\n        let parsed = form_urlencoded::parse(query);\n        for (key, val) in parsed {\n            // Only consider non-empty values to be present.\n            if !val.is_empty() && schema.contains_name(key.as_ref()) {\n                return true;\n            }\n        }\n\n        false\n    }\n\n    pub fn len(&self) -> usize {\n        self.schema.root().fields.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.len() == 0\n    }\n\n    pub fn fields(&self) -> impl Iterator<Item = (&String, &jsonschema::Field)> {\n        self.schema.root().fields.iter()\n    }\n}\n\nimpl ToOutgoingRequest<http::Request<()>> for Query {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut http::Request<()>,\n    ) -> APIResult<()> {\n        let Some(payload) = payload else {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing query parameters\".to_string(),\n                internal_message: Some(\"missing query parameters\".to_string()),\n                stack: None,\n                details: None,\n            });\n        };\n\n        // Serialize the payload.\n        let mut url = Url::parse(&req.uri().to_string()).map_err(api::Error::internal)?;\n\n        {\n            let mut pairs = url.query_pairs_mut();\n            let serializer = serde_urlencoded::Serializer::new(&mut pairs);\n\n            flatten_payload(payload)\n                .serialize(serializer)\n                .map_err(api::Error::internal)?;\n        }\n\n        *req.uri_mut() = http::Uri::from_str(url.as_str()).map_err(api::Error::internal)?;\n\n        Ok(())\n    }\n}\n\nimpl ToOutgoingRequest<reqwest::Request> for Query {\n    fn to_outgoing_request(\n        &self,\n        payload: &mut JSONPayload,\n        req: &mut reqwest::Request,\n    ) -> APIResult<()> {\n        if self.schema.root().is_empty() {\n            return Ok(());\n        }\n\n        let Some(payload) = payload else {\n            return Err(api::Error {\n                code: api::ErrCode::InvalidArgument,\n                message: \"missing query parameters\".to_string(),\n                internal_message: Some(\"missing query parameters\".to_string()),\n                stack: None,\n                details: None,\n            });\n        };\n\n        // Serialize the payload.\n        {\n            let url = req.url_mut();\n            let mut pairs = url.query_pairs_mut();\n            let serializer = serde_urlencoded::Serializer::new(&mut pairs);\n\n            flatten_payload(payload)\n                .serialize(serializer)\n                .map_err(api::Error::internal)?;\n        }\n\n        // If the query string is now empty, set it to None.\n        if let Some(\"\") = req.url().query() {\n            req.url_mut().set_query(None);\n        }\n\n        Ok(())\n    }\n}\n\n// Flatten arrays into multi-fields for serde_urlencoded\nfn flatten_payload(payload: &PValues) -> Vec<(&String, &api::PValue)> {\n    payload\n        .iter()\n        .flat_map(|(k, v)| {\n            if let api::PValue::Array(vec) = v {\n                vec.iter().map(|val| (k, val)).collect()\n            } else {\n                vec![(k, v)]\n            }\n        })\n        .collect()\n}\n"
  },
  {
    "path": "runtimes/core/src/api/server.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt::Debug;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::atomic::AtomicUsize;\nuse std::sync::{Arc, Mutex, RwLock};\n\nuse crate::api::endpoint::{EndpointHandler, SharedEndpointData};\nuse crate::api::paths::Pather;\nuse crate::api::reqauth::svcauth;\nuse crate::api::static_assets::StaticAssetsHandler;\nuse crate::api::{self, ToResponse};\nuse crate::api::{paths, reqauth, schema, BoxedHandler, EndpointMap};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::names::EndpointName;\nuse crate::trace;\n\nuse super::jsonschema::JSONSchema;\n\n/// An alias for the concrete type of a server handler.\ntype ServerHandler = ReplaceableHandler<EndpointHandler>;\n\n/// Server is an API server. It serves the registered API endpoints.\n///\n/// When running tests there's not a single entrypoint, so the server\n/// is designed to support incrementally adding endpoints.\n///\n/// We handle this by registering all handlers with axum up-front, and add\n/// the handler once it has been registered.\n#[derive(Debug)]\npub struct Server {\n    endpoints: Arc<EndpointMap>,\n\n    hosted_endpoints: Mutex<HashMap<EndpointName, ServerHandler>>,\n\n    router: Mutex<Option<axum::Router>>,\n\n    /// Data shared between all endpoints.\n    shared: Arc<SharedEndpointData>,\n\n    /// Metrics registry for creating metrics\n    metrics_registry: Arc<crate::metrics::Registry>,\n}\n\nimpl Server {\n    pub fn new(\n        endpoints: Arc<EndpointMap>,\n        hosted_endpoints: Vec<EndpointName>,\n        platform_auth: Arc<reqauth::platform::RequestValidator>,\n        inbound_svc_auth: Vec<Arc<dyn svcauth::ServiceAuthMethod>>,\n        tracer: trace::Tracer,\n        auth_data_schemas: HashMap<String, Option<JSONSchema>>,\n        metrics_registry: Arc<crate::metrics::Registry>,\n    ) -> anyhow::Result<Self> {\n        // Register the routes, and track the handlers in a map so we can easily\n        // set the request handler when registered.\n        let mut router = axum::Router::new();\n\n        async fn not_found_handler(\n            req: axum::http::Request<axum::body::Body>,\n        ) -> axum::response::Response<axum::body::Body> {\n            api::Error {\n                code: api::ErrCode::NotFound,\n                message: \"endpoint not found\".to_string(),\n                internal_message: Some(format!(\"no such endpoint exists: {}\", req.uri().path())),\n                stack: None,\n                details: None,\n            }\n            .to_response(None)\n        }\n\n        let mut fallback_router = axum::Router::new();\n        fallback_router = fallback_router.fallback(not_found_handler);\n\n        let mut handler_map = HashMap::with_capacity(hosted_endpoints.len());\n        let path_set = paths::compute(hosted_endpoints.iter().map(|ep| EndpointPathResolver {\n            ep: endpoints.get(ep).unwrap().to_owned(),\n        }));\n\n        let shared = Arc::new(SharedEndpointData {\n            tracer,\n            platform_auth,\n            inbound_svc_auth,\n            auth_data_schemas,\n        });\n\n        let mut register = |paths: &[(Arc<api::Endpoint>, Vec<String>)],\n                            mut router: axum::Router|\n         -> axum::Router {\n            for (ep, paths) in paths {\n                match schema::method_filter(ep.methods()) {\n                    Some(filter) => {\n                        let server_handler = ServerHandler::default();\n\n                        if let Some(assets) = &ep.static_assets {\n                            // For static asset routes, configure the static asset handler directly.\n                            // There's no need to defer it for dynamic runtime registration.\n                            let static_handler = StaticAssetsHandler::new(assets);\n                            let requests_total = crate::metrics::requests_total_counter(\n                                &metrics_registry,\n                                ep.name.service(),\n                                ep.name.endpoint(),\n                            );\n\n                            let handler = EndpointHandler {\n                                endpoint: ep.clone(),\n                                handler: Arc::new(static_handler),\n                                shared: shared.clone(),\n                                requests_total,\n                            };\n                            server_handler.set(handler);\n                        }\n\n                        let handler = axum::routing::on(filter, server_handler.clone());\n                        for path in paths {\n                            router = router.route(path, handler.clone());\n                        }\n                        handler_map.insert(ep.name.clone(), server_handler);\n                    }\n                    None => {\n                        log::warn!(\"no methods for endpoint {}, skipping\", ep.name);\n                    }\n                }\n            }\n            router\n        };\n\n        if let Some(main_paths) = path_set.main.get(&()) {\n            router = register(main_paths, router);\n        }\n        if let Some(fallback_paths) = path_set.fallback.get(&()) {\n            fallback_router = register(fallback_paths, fallback_router);\n        }\n\n        // Register our fallback route.\n        router = router.fallback_service(fallback_router);\n\n        Ok(Self {\n            endpoints,\n            hosted_endpoints: Mutex::new(handler_map),\n            router: Mutex::new(Some(router)),\n            shared,\n            metrics_registry,\n        })\n    }\n\n    pub fn router(&self) -> axum::Router {\n        self.router.lock().unwrap().as_ref().unwrap().clone()\n    }\n\n    /// Registers a handler for the given endpoint.\n    /// Reports an error if the handler was not found.\n    pub fn register_handler(\n        &self,\n        endpoint_name: EndpointName,\n        handler: Arc<dyn BoxedHandler>,\n    ) -> anyhow::Result<()> {\n        match self\n            .hosted_endpoints\n            .lock()\n            .unwrap()\n            .get_mut(&endpoint_name)\n        {\n            None => Ok(()), // anyhow::bail!(\"no handler found for endpoint: {}\", endpoint_name),\n            Some(h) => {\n                let endpoint = self.endpoints.get(&endpoint_name).unwrap().to_owned();\n                let requests_total = crate::metrics::requests_total_counter(\n                    &self.metrics_registry,\n                    endpoint.name.service(),\n                    endpoint.name.endpoint(),\n                );\n\n                let handler = EndpointHandler {\n                    endpoint,\n                    handler,\n                    shared: self.shared.clone(),\n                    requests_total,\n                };\n\n                h.add(handler);\n                Ok(())\n            }\n        }\n    }\n}\n\nstruct EndpointPathResolver {\n    ep: Arc<api::Endpoint>,\n}\n\nimpl Pather for EndpointPathResolver {\n    type Key = ();\n    type Value = Arc<api::Endpoint>;\n\n    fn key(&self) {}\n\n    fn value(&self) -> Self::Value {\n        self.ep.clone()\n    }\n\n    fn path(&self) -> &meta::Path {\n        &self.ep.path\n    }\n}\n\n#[derive(Debug)]\nstruct LoadBalancingHandler<H> {\n    handlers: Vec<H>,\n    counter: AtomicUsize,\n}\n\nimpl<H> Default for LoadBalancingHandler<H> {\n    fn default() -> Self {\n        Self {\n            handlers: Vec::new(),\n            counter: AtomicUsize::new(0),\n        }\n    }\n}\n\nimpl<H> LoadBalancingHandler<H> {\n    pub fn single(handler: H) -> Self {\n        Self {\n            handlers: vec![handler],\n            counter: AtomicUsize::new(1),\n        }\n    }\n\n    pub fn add(&mut self, handler: H) {\n        self.handlers.push(handler);\n    }\n\n    pub fn len(&self) -> usize {\n        self.handlers.len()\n    }\n\n    pub fn next(&self) -> &H {\n        let n = self.handlers.len();\n        // If we have a single handler, skip the increment and modulo steps.\n        if n == 1 {\n            return &self.handlers[0];\n        }\n\n        // Atomically increment the counter, and then get the next handler.\n        let idx = self\n            .counter\n            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n        &self.handlers[idx % n]\n    }\n}\n\n/// A replaceable handler is a handler that can be replaced at runtime.\n/// It is used to support incremental registration of endpoints.\n#[derive(Clone)]\nstruct ReplaceableHandler<H> {\n    /// Underlying handler. The RwLock is used to be able to inject the underlying handler.\n    handler: Arc<RwLock<LoadBalancingHandler<H>>>,\n}\n\nimpl<H> Debug for ReplaceableHandler<H> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"ReplaceableHandler\").finish()\n    }\n}\n\nimpl<H> Default for ReplaceableHandler<H> {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl<H> ReplaceableHandler<H> {\n    pub fn new() -> Self {\n        Self {\n            handler: Arc::new(RwLock::default()),\n        }\n    }\n\n    /// Set sets the handler.\n    pub fn set(&self, handler: H) {\n        *self.handler.write().unwrap() = LoadBalancingHandler::single(handler);\n    }\n\n    /// Set sets the handler.\n    pub fn add(&self, handler: H) {\n        self.handler.write().unwrap().add(handler);\n    }\n}\n\nimpl<H> axum::handler::Handler<(), ()> for ReplaceableHandler<H>\nwhere\n    H: axum::handler::Handler<(), ()> + Sync,\n{\n    type Future = MaybeHandlerFuture<H::Future>;\n\n    fn call(self, req: axum::extract::Request, state: ()) -> Self::Future {\n        let handlers = self.handler.read().unwrap();\n        match handlers.len() {\n            0 => MaybeHandlerFuture { fut: None },\n            _ => {\n                let handler = handlers.next().clone();\n                MaybeHandlerFuture {\n                    fut: Some(Box::pin(handler.call(req, state))),\n                }\n            }\n        }\n    }\n}\n\n/// A MaybeHandlerFuture is a future that may or may not have a future.\n/// If there is no future, it returns a 404 response.\nstruct MaybeHandlerFuture<F> {\n    fut: Option<Pin<Box<F>>>,\n}\n\nimpl<F> Future for MaybeHandlerFuture<F>\nwhere\n    F: Future<Output = axum::response::Response> + Send + 'static,\n{\n    type Output = axum::response::Response;\n\n    fn poll(\n        mut self: Pin<&mut Self>,\n        cx: &mut std::task::Context<'_>,\n    ) -> std::task::Poll<axum::response::Response> {\n        match self.fut.as_mut() {\n            // If we have a future, poll it.\n            Some(fut) => fut.as_mut().poll(cx),\n\n            // Otherwise we return a 404 response.\n            None => {\n                let resp = api::Error {\n                    code: api::ErrCode::NotFound,\n                    message: \"endpoint not found\".to_string(),\n                    internal_message: Some(\"no handler registered for endpoint\".to_string()),\n                    stack: None,\n                    details: None,\n                }\n                .to_response(None);\n                std::task::Poll::Ready(resp)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__basic.snap",
    "content": "---\nsource: runtimes/core/src/api/paths.rs\nexpression: paths\n---\nmain:\n  a:\n    - - one\n      - - /foo\n        - /foo/\n    - - two\n      - - /foo/bar\n        - /foo/bar/\nfallback: {}\n"
  },
  {
    "path": "runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__fallback.snap",
    "content": "---\nsource: runtimes/core/src/api/paths.rs\nexpression: paths\n---\nmain: {}\nfallback:\n  a:\n    - - one\n      - - /\n        - /*_\n"
  },
  {
    "path": "runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__paths_to_register.snap",
    "content": "---\nsource: runtimes/core/src/api/paths.rs\nexpression: paths\n---\nmain:\n  a:\n    - endpoint:\n        key: a\n        path: /a\n      routes:\n        - /a\n        - /a/\nfallback: {}\n"
  },
  {
    "path": "runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__tsr_conflict.snap",
    "content": "---\nsource: runtimes/core/src/api/paths.rs\nexpression: paths\n---\nmain:\n  a:\n    - - one\n      - - /foo/\n  b:\n    - - two\n      - - /foo\nfallback: {}\n"
  },
  {
    "path": "runtimes/core/src/api/snapshots/encore_runtime_core__api__paths__tests__wildcard.snap",
    "content": "---\nsource: runtimes/core/src/api/paths.rs\nexpression: paths\n---\nmain:\n  a:\n    - - one\n      - - /\n        - /*_\nfallback: {}\n"
  },
  {
    "path": "runtimes/core/src/api/static_assets.rs",
    "content": "use std::{convert::Infallible, future::Future, path::PathBuf, pin::Pin, sync::Arc};\n\nuse http::{HeaderName, HeaderValue, StatusCode};\nuse http_body_util::Empty;\nuse std::fmt::Debug;\nuse std::io;\nuse tower_http::services::{fs::ServeDir, ServeFile};\nuse tower_service::Service;\n\nuse crate::{encore::parser::meta::v1 as meta, model::RequestData};\n\nuse super::{BoxedHandler, Error, HandlerRequest, ResponseData};\n\n#[derive(Clone, Debug)]\npub struct StaticAssetsHandler {\n    service: Arc<dyn FileServer>,\n    not_found_handler: bool,\n    not_found_status: StatusCode,\n    headers: Vec<(HeaderName, HeaderValue)>,\n}\n\nimpl StaticAssetsHandler {\n    pub fn new(cfg: &meta::rpc::StaticAssets) -> Self {\n        let service = ServeDir::new(PathBuf::from(&cfg.dir_rel_path));\n\n        let not_found_status = cfg\n            .not_found_status\n            .and_then(|c| StatusCode::from_u16(c as u16).ok())\n            .unwrap_or(StatusCode::NOT_FOUND);\n\n        let not_found = cfg\n            .not_found_rel_path\n            .as_ref()\n            .map(|p| ServeFile::new(PathBuf::from(p)));\n        let not_found_handler = not_found.is_some();\n        let service: Arc<dyn FileServer> = match not_found {\n            Some(not_found) => Arc::new(service.not_found_service(not_found)),\n            None => Arc::new(service),\n        };\n\n        let headers: Vec<(HeaderName, HeaderValue)> = cfg\n            .headers\n            .iter()\n            .flat_map(|(key, header_values)| {\n                HeaderName::from_bytes(key.as_bytes())\n                    .inspect_err(|e| {\n                        log::error!(\"skipping header: '{}' - {}\", key, e);\n                    })\n                    .ok()\n                    .map(|header_name| {\n                        header_values.values.iter().filter_map(move |value| {\n                            HeaderValue::from_bytes(value.as_bytes())\n                                .inspect_err(|e| {\n                                    log::error!(\"skipping header '{}': '{}' - {}\", key, value, e);\n                                })\n                                .ok()\n                                .map(|header_value| (header_name.clone(), header_value))\n                        })\n                    })\n                    .into_iter()\n                    .flatten()\n            })\n            .collect();\n\n        StaticAssetsHandler {\n            service,\n            not_found_handler,\n            not_found_status,\n            headers,\n        }\n    }\n}\n\nimpl BoxedHandler for StaticAssetsHandler {\n    fn call(\n        self: Arc<Self>,\n        req: HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = ResponseData> + Send + 'static>> {\n        Box::pin(async move {\n            let RequestData::RPC(data) = &req.data else {\n                return ResponseData::Typed(Err(Error::internal(anyhow::anyhow!(\n                    \"invalid request data type\"\n                ))));\n            };\n\n            // Find the file path from the request.\n            let file_path = match &data.path_params {\n                Some(params) => params\n                    .values()\n                    .next()\n                    .and_then(|v| v.as_str())\n                    .map(|s| format!(\"/{s}\"))\n                    .unwrap_or(\"/\".to_string()),\n                None => \"/\".to_string(),\n            };\n\n            let httpreq = {\n                let mut b = axum::http::request::Request::builder();\n                {\n                    // Copy headers into request.\n                    let headers = b.headers_mut().unwrap();\n                    for (k, v) in &data.req_headers {\n                        headers.append(k.clone(), v.clone());\n                    }\n                }\n                match b\n                    .method(data.method)\n                    .uri(file_path)\n                    .body(Empty::<bytes::Bytes>::new())\n                {\n                    Ok(req) => req,\n                    Err(e) => {\n                        return ResponseData::Typed(Err(Error::invalid_argument(\n                            \"invalid file path\",\n                            e,\n                        )));\n                    }\n                }\n            };\n\n            match self.service.serve(httpreq).await {\n                Ok(mut resp) => {\n                    let resp_headers = resp.headers_mut();\n                    for (name, value) in &self.headers {\n                        resp_headers.append(name.clone(), value.clone());\n                    }\n\n                    match resp.status() {\n                        // 1xx, 2xx, 3xx are all considered successful.\n                        code if code.is_informational()\n                            || code.is_success()\n                            || code.is_redirection() =>\n                        {\n                            ResponseData::Raw(resp.map(axum::body::Body::new))\n                        }\n                        axum::http::StatusCode::NOT_FOUND => {\n                            // If we have a not found handler, use that directly.\n                            if self.not_found_handler {\n                                *resp.status_mut() = self.not_found_status;\n                                ResponseData::Raw(resp.map(axum::body::Body::new))\n                            } else {\n                                // Otherwise return our standard not found error.\n                                ResponseData::Typed(Err(Error::not_found(\"file not found\")))\n                            }\n                        }\n                        axum::http::StatusCode::METHOD_NOT_ALLOWED => {\n                            ResponseData::Typed(Err(Error {\n                                code: super::ErrCode::InvalidArgument,\n                                internal_message: None,\n                                message: \"method not allowed\".to_string(),\n                                stack: None,\n                                details: None,\n                            }))\n                        }\n                        axum::http::StatusCode::INTERNAL_SERVER_ERROR => {\n                            ResponseData::Typed(Err(Error {\n                                code: super::ErrCode::Internal,\n                                internal_message: None,\n                                message: \"failed to serve static asset\".to_string(),\n                                stack: None,\n                                details: None,\n                            }))\n                        }\n                        code => ResponseData::Typed(Err(Error::internal(anyhow::anyhow!(\n                            \"failed to serve static asset: {}\",\n                            code,\n                        )))),\n                    }\n                }\n                Err(e) => ResponseData::Typed(Err(Error::internal(e))),\n            }\n        })\n    }\n}\n\ntrait FileServer: Sync + Send + Debug {\n    fn serve(\n        &self,\n        req: axum::http::Request<Empty<bytes::Bytes>>,\n    ) -> Pin<Box<dyn Future<Output = Result<FileRes, io::Error>> + Send + 'static>>;\n}\n\ntype FileReq = axum::http::Request<Empty<bytes::Bytes>>;\ntype FileRes = axum::http::Response<tower_http::services::fs::ServeFileSystemResponseBody>;\n\nimpl<F> FileServer for ServeDir<F>\nwhere\n    F: Service<FileReq, Response = FileRes, Error = Infallible>\n        + Debug\n        + Clone\n        + Sync\n        + Send\n        + 'static,\n    F::Future: Send + 'static,\n{\n    fn serve(\n        &self,\n        req: axum::http::Request<Empty<bytes::Bytes>>,\n    ) -> Pin<Box<dyn Future<Output = Result<FileRes, io::Error>> + Send + 'static>> {\n        let mut this = self.clone();\n        Box::pin(async move { this.try_call(req).await })\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/websocket.rs",
    "content": "use std::sync::Arc;\n\nuse anyhow::{anyhow, Context};\nuse axum::extract::ws::{Message, WebSocket};\nuse futures::Future;\nuse tokio::sync::{\n    mpsc::{self, UnboundedReceiver, UnboundedSender},\n    watch,\n};\n\nuse crate::model::{self, Request, RequestData};\n\nuse super::{schema, APIResult, HandlerResponse, HandlerResponseInner, PValues};\n\npub enum StreamMessagePayload {\n    InOut(Socket),\n    Out(Sink),\n    In(Stream),\n}\n\npub fn upgrade_request<C, Fut>(\n    req: Arc<Request>,\n    callback: C,\n) -> APIResult<axum::response::Response>\nwhere\n    C: FnOnce(Arc<Request>, StreamMessagePayload, UnboundedSender<HandlerResponse>) -> Fut\n        + Send\n        + 'static,\n    Fut: Future<Output = ()> + Send + 'static,\n{\n    let RequestData::Stream(ref data) = req.data else {\n        return Err(super::Error::internal(anyhow!(\n            \"wrong request data type for stream\"\n        )));\n    };\n\n    let req_schema = data\n        .endpoint\n        .request\n        .first()\n        .context(\"no request schema\")\n        .map_err(super::Error::internal)?\n        .clone();\n\n    let resp_schema = data.endpoint.response.clone();\n\n    let upgrade = {\n        if let Some(upgrade) = data\n            .websocket_upgrade\n            .lock()\n            .expect(\"mutex poisoned\")\n            .take()\n        {\n            upgrade\n        } else {\n            return Err(super::Error::internal(anyhow!(\n                \"websocket already upgraded\"\n            )));\n        }\n    };\n\n    let (tx, mut rx) = mpsc::unbounded_channel::<HandlerResponse>();\n\n    let direction = data.direction;\n    Ok(upgrade\n        .protocols([\"encore-ws\"])\n        .on_failed_upgrade(|err| log::debug!(\"websocket upgrade failed: {err}\"))\n        .on_upgrade(move |ws| async move {\n            let socket = Socket::new(ws, schema::Stream::new(req_schema, resp_schema).into());\n\n            let payload = match direction {\n                model::StreamDirection::InOut => StreamMessagePayload::InOut(socket),\n                model::StreamDirection::In => {\n                    let (sink, stream) = socket.split();\n\n                    tokio::spawn(async move {\n                        match rx.recv().await {\n                            Some(resp) => match resp {\n                                Ok(HandlerResponseInner {\n                                    payload: Some(resp),\n                                    ..\n                                }) => {\n                                    if sink.send(resp).is_err() {\n                                        log::debug!(\"sink channel closed\");\n                                    }\n                                }\n                                Ok(HandlerResponseInner { payload: None, .. }) => {\n                                    log::warn!(\"responded with empty response\")\n                                }\n                                Err(err) => log::warn!(\"responded with error: {err:?}\"),\n                            },\n                            None => log::debug!(\"response channel closed\"),\n                        };\n                    });\n\n                    StreamMessagePayload::In(stream)\n                }\n                model::StreamDirection::Out => {\n                    let (sink, _stream) = socket.split();\n                    StreamMessagePayload::Out(sink)\n                }\n            };\n\n            (callback)(req, payload, tx).await\n        }))\n}\n\npub struct Socket {\n    outgoing_message_tx: UnboundedSender<PValues>,\n    incoming_message_rx: tokio::sync::Mutex<UnboundedReceiver<PValues>>,\n    shutdown: watch::Sender<bool>,\n}\n\nimpl Socket {\n    fn new(mut websocket: WebSocket, schema: Arc<schema::Stream>) -> Self {\n        let (shutdown, mut shutdown_watch) = watch::channel(false);\n\n        let (outgoing_message_tx, mut outgoing_messages_rx) = mpsc::unbounded_channel();\n        let (incoming_messages_tx, incoming_message_rx) = mpsc::unbounded_channel();\n\n        tokio::spawn({\n            async move {\n                loop {\n                    tokio::select! {\n                        msg = websocket.recv() => match msg {\n                            None => {\n                                log::trace!(\"websocket closed\");\n                                break\n                            },\n                            Some(Ok(msg)) => {\n                                if let Err(e) = Socket::handle_incoming_message(\n                                    &schema,\n                                    &incoming_messages_tx,\n                                    msg,\n                                )\n                                .await\n                                {\n                                    log::warn!(\"failed handling incoming message: {e}\");\n                                    break;\n                                }\n                            },\n                            Some(Err(e)) => {\n                                log::debug!(\"websocket receive failed: {e}\");\n                                break;\n                            }\n                        },\n                        msg = outgoing_messages_rx.recv() => {\n                            match msg {\n                                None => {\n                                    _ = websocket.close().await;\n                                    log::trace!(\"websocket closed\");\n                                    break;\n                                }\n                                Some(msg) => Socket::handle_outgoing_message(&schema, &mut websocket, msg).await\n                            }\n                        },\n                        _ = shutdown_watch.changed() => {\n                            // gracefully shutdown, wait for all messages to be read on out channel\n                            // before closing the websocket\n                            outgoing_messages_rx.close();\n                        }\n                    }\n                }\n\n                log::trace!(\"socket closed\");\n            }\n        });\n\n        Socket {\n            outgoing_message_tx,\n            incoming_message_rx: tokio::sync::Mutex::new(incoming_message_rx),\n            shutdown,\n        }\n    }\n\n    pub fn send(&self, msg: PValues) -> anyhow::Result<()> {\n        self.outgoing_message_tx.send(msg)?;\n        Ok(())\n    }\n\n    pub async fn recv(&self) -> Option<PValues> {\n        self.incoming_message_rx.lock().await.recv().await\n    }\n\n    pub fn close(&self) {\n        _ = self.shutdown.send(true);\n    }\n\n    pub fn split(self) -> (Sink, Stream) {\n        let Self {\n            outgoing_message_tx: tx,\n            incoming_message_rx: rx,\n            shutdown,\n        } = self;\n\n        let sink = Sink { tx, shutdown };\n        let stream = Stream { rx };\n\n        (sink, stream)\n    }\n\n    async fn handle_outgoing_message(\n        schema: &schema::Stream,\n        websocket: &mut WebSocket,\n        msg: PValues,\n    ) {\n        let msg = schema\n            .to_outgoing_message(msg)\n            .and_then(|msg| String::from_utf8(msg).map_err(super::Error::internal));\n\n        match msg {\n            Ok(msg) => {\n                if let Err(e) = websocket.send(Message::Text(msg)).await {\n                    log::debug!(\"failed to send message to socket: {e}\")\n                }\n            }\n            Err(e) => log::warn!(\"failed to send message to socket: {e}\"),\n        }\n    }\n\n    async fn handle_incoming_message<M>(\n        schema: &schema::Stream,\n        incoming: &UnboundedSender<PValues>,\n        msg: M,\n    ) -> anyhow::Result<()>\n    where\n        M: MessagePayload,\n    {\n        if let Some(data) = msg.payload() {\n            match schema.parse_incoming_message(data).await {\n                Ok(msg) => {\n                    if let Err(e) = incoming.send(msg) {\n                        return Err(anyhow!(\"tried to send on closed channel: {e}\"));\n                    }\n                }\n                Err(e) => log::warn!(\"failed to parse incoming message: {e}\"),\n            };\n        }\n\n        Ok(())\n    }\n}\n\ntrait MessagePayload {\n    fn payload(&self) -> Option<&[u8]>;\n}\n\nimpl MessagePayload for axum::extract::ws::Message {\n    fn payload(&self) -> Option<&[u8]> {\n        match self {\n            Message::Text(text) => Some(text.as_bytes()),\n            Message::Binary(data) => Some(data),\n            // these message types are handled by axum\n            Message::Ping(_) | Message::Pong(_) | Message::Close(_) => None,\n        }\n    }\n}\npub struct Sink {\n    tx: UnboundedSender<PValues>,\n    shutdown: watch::Sender<bool>,\n}\n\nimpl Sink {\n    pub fn send(&self, msg: PValues) -> anyhow::Result<()> {\n        self.tx.send(msg)?;\n        Ok(())\n    }\n\n    pub fn close(&self) {\n        _ = self.shutdown.send(true);\n    }\n}\n\npub struct Stream {\n    rx: tokio::sync::Mutex<UnboundedReceiver<PValues>>,\n}\nimpl Stream {\n    pub async fn recv(&self) -> Option<PValues> {\n        self.rx.lock().await.recv().await\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/api/websocket_client.rs",
    "content": "use std::sync::Arc;\n\nuse futures::sink::SinkExt;\nuse futures::stream::SplitSink;\nuse futures::stream::SplitStream;\nuse futures::stream::StreamExt;\nuse tokio::net::TcpStream;\nuse tokio::sync::mpsc::UnboundedReceiver;\nuse tokio::sync::mpsc::UnboundedSender;\nuse tokio::sync::watch;\nuse tokio::sync::Mutex;\nuse tokio_tungstenite::MaybeTlsStream;\nuse tokio_tungstenite::{tungstenite::Message, WebSocketStream};\n\nuse super::schema;\nuse super::APIResult;\nuse super::PValues;\n\npub struct WebSocketClient {\n    send_channel: UnboundedSender<Message>,\n    receive_channel: Mutex<UnboundedReceiver<Message>>,\n    shutdown: watch::Sender<bool>,\n    schema: Arc<schema::Stream>,\n}\n\nimpl WebSocketClient {\n    pub async fn connect(\n        request: http::Request<()>,\n        schema: schema::Stream,\n    ) -> APIResult<WebSocketClient> {\n        let (connection, _resp) = tokio_tungstenite::connect_async(request)\n            .await\n            .map_err(|e| super::Error {\n                code: super::ErrCode::Unknown,\n                message: \"failed connecting to websocket endpoint\".to_string(),\n                internal_message: Some(e.to_string()),\n                stack: None,\n                details: None,\n            })?;\n\n        let (ws_write, ws_read) = connection.split();\n\n        let (send_channel_tx, send_channel_rx) = tokio::sync::mpsc::unbounded_channel();\n        let (receive_channel_tx, receive_channel_rx) = tokio::sync::mpsc::unbounded_channel();\n\n        let (shutdown, shutdown_watch) = watch::channel(false);\n\n        tokio::spawn(send_to_ws(send_channel_rx, ws_write, shutdown_watch));\n        tokio::spawn(ws_to_receive(ws_read, receive_channel_tx));\n\n        Ok(WebSocketClient {\n            send_channel: send_channel_tx,\n            receive_channel: Mutex::new(receive_channel_rx),\n            shutdown,\n            schema: schema.into(),\n        })\n    }\n\n    pub fn send(&self, msg: PValues) -> APIResult<()> {\n        let msg = self.schema.to_outgoing_message(msg)?;\n        let msg = String::from_utf8(msg).map_err(super::Error::internal)?;\n\n        self.send_channel\n            .send(Message::Text(msg))\n            .map_err(super::Error::internal)?;\n\n        Ok(())\n    }\n\n    pub async fn recv(&self) -> Option<APIResult<PValues>> {\n        loop {\n            let msg = self.receive_channel.lock().await.recv().await;\n\n            let bytes: bytes::Bytes = match msg {\n                Some(Message::Text(msg)) => msg.into(),\n                Some(Message::Binary(vec)) => vec.into(),\n                Some(_msg) => continue,\n                None => return None,\n            };\n\n            return Some(self.schema.parse_incoming_message(&bytes).await);\n        }\n    }\n\n    pub fn close(&self) {\n        if let Err(err) = self.shutdown.send(true) {\n            log::trace!(\"error sending shutdown signal: {err}\");\n        }\n    }\n}\n\nasync fn send_to_ws(\n    mut rx: UnboundedReceiver<Message>,\n    mut ws: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,\n    mut shutdown: watch::Receiver<bool>,\n) {\n    loop {\n        tokio::select! {\n            _ = shutdown.changed() => {\n                rx.close()\n            },\n            msg = rx.recv() => match msg {\n                Some(msg) => {\n                    if let Err(err) = ws.send(msg).await {\n                        log::debug!(\"failed sending over websocket: {err}\");\n                    }\n                },\n                None => {\n                    log::trace!(\"receive channel closed, shutting down\");\n\n                    if let Err(err) = ws.close().await {\n                        log::trace!(\"closing websocket failed: {err}\");\n                    }\n\n                    break;\n                },\n\n            },\n        }\n    }\n}\n\nasync fn ws_to_receive(\n    mut ws: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,\n    tx: UnboundedSender<Message>,\n) {\n    loop {\n        let msg = ws.next().await;\n\n        match msg {\n            Some(Ok(msg)) => {\n                if let Err(err) = tx.send(msg) {\n                    log::warn!(\"failed sending to receive channel: {err}\");\n                    break;\n                }\n            }\n            Some(Err(err)) => {\n                log::debug!(\"received an error from websocket: {err}\");\n                break;\n            }\n            None => {\n                log::trace!(\"websocket closed, shutting down\");\n                break;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/base32.rs",
    "content": "use std::cmp::min;\n\n#[derive(Copy, Clone)]\npub enum Alphabet {\n    #[allow(dead_code)]\n    RFC4648 {\n        padding: bool,\n    },\n    #[allow(dead_code)]\n    Crockford,\n    Encore,\n}\n\nconst RFC4648_ALPHABET: &[u8] = b\"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567\";\nconst CROCKFORD_ALPHABET: &[u8] = b\"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\nconst ENCORE_ALPHABET: &[u8] = b\"0123456789abcdefghijklmnopqrstuv\";\n\npub fn encode(alphabet: Alphabet, data: &[u8]) -> String {\n    let (alphabet, padding) = match alphabet {\n        Alphabet::RFC4648 { padding } => (RFC4648_ALPHABET, padding),\n        Alphabet::Crockford => (CROCKFORD_ALPHABET, false),\n        Alphabet::Encore => (ENCORE_ALPHABET, false),\n    };\n    let mut ret = Vec::with_capacity(data.len().div_ceil(4) * 5);\n\n    for chunk in data.chunks(5) {\n        let buf = {\n            let mut buf = [0u8; 5];\n            for (i, &b) in chunk.iter().enumerate() {\n                buf[i] = b;\n            }\n            buf\n        };\n        ret.push(alphabet[((buf[0] & 0xF8) >> 3) as usize]);\n        ret.push(alphabet[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize]);\n        ret.push(alphabet[((buf[1] & 0x3E) >> 1) as usize]);\n        ret.push(alphabet[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize]);\n        ret.push(alphabet[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize]);\n        ret.push(alphabet[((buf[3] & 0x7C) >> 2) as usize]);\n        ret.push(alphabet[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize]);\n        ret.push(alphabet[(buf[4] & 0x1F) as usize]);\n    }\n\n    if !data.len().is_multiple_of(5) {\n        let len = ret.len();\n        let num_extra = 8 - (data.len() % 5 * 8).div_ceil(5);\n        if padding {\n            for i in 1..num_extra + 1 {\n                ret[len - i] = b'=';\n            }\n        } else {\n            ret.truncate(len - num_extra);\n        }\n    }\n\n    String::from_utf8(ret).unwrap()\n}\n\nconst RFC4648_INV_ALPHABET: [i8; 43] = [\n    -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8,\n    9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,\n];\nconst CROCKFORD_INV_ALPHABET: [i8; 43] = [\n    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 1,\n    18, 19, 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, 29, 30, 31,\n];\nconst ENCORE_INV_ALPHABET: [i8; 43] = [\n    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18,\n    19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1,\n];\n\npub fn decode(alphabet: Alphabet, data: &str) -> Option<Vec<u8>> {\n    if !data.is_ascii() {\n        return None;\n    }\n    let data = data.as_bytes();\n    let alphabet = match alphabet {\n        Alphabet::RFC4648 { .. } => RFC4648_INV_ALPHABET,\n        Alphabet::Crockford => CROCKFORD_INV_ALPHABET,\n        Alphabet::Encore => ENCORE_INV_ALPHABET,\n    };\n    let mut unpadded_data_length = data.len();\n    for i in 1..min(6, data.len()) + 1 {\n        if data[data.len() - i] != b'=' {\n            break;\n        }\n        unpadded_data_length -= 1;\n    }\n    let output_length = unpadded_data_length * 5 / 8;\n    let mut ret = Vec::with_capacity(output_length.div_ceil(5) * 5);\n    for chunk in data.chunks(8) {\n        let buf = {\n            let mut buf = [0u8; 8];\n            for (i, &c) in chunk.iter().enumerate() {\n                match alphabet.get(c.to_ascii_uppercase().wrapping_sub(b'0') as usize) {\n                    Some(&-1) | None => return None,\n                    Some(&value) => buf[i] = value as u8,\n                };\n            }\n            buf\n        };\n        ret.push((buf[0] << 3) | (buf[1] >> 2));\n        ret.push((buf[1] << 6) | (buf[2] << 1) | (buf[3] >> 4));\n        ret.push((buf[3] << 4) | (buf[4] >> 1));\n        ret.push((buf[4] << 7) | (buf[5] << 2) | (buf[6] >> 3));\n        ret.push((buf[6] << 5) | buf[7]);\n    }\n    ret.truncate(output_length);\n    Some(ret)\n}\n\n#[cfg(test)]\n#[allow(dead_code, unused_attributes)]\nmod test {\n    use super::Alphabet::{Crockford, Encore, RFC4648};\n    use super::{decode, encode};\n\n    #[derive(Clone)]\n    struct B32 {\n        c: u8,\n    }\n\n    impl quickcheck::Arbitrary for B32 {\n        fn arbitrary(g: &mut quickcheck::Gen) -> B32 {\n            let alphabet = b\"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n            B32 {\n                c: *g.choose(alphabet).unwrap(),\n            }\n        }\n    }\n\n    impl std::fmt::Debug for B32 {\n        fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {\n            (self.c as char).fmt(f)\n        }\n    }\n\n    #[test]\n    fn masks_crockford() {\n        assert_eq!(\n            encode(Crockford, &[0xF8, 0x3E, 0x0F, 0x83, 0xE0]),\n            \"Z0Z0Z0Z0\"\n        );\n        assert_eq!(\n            encode(Crockford, &[0x07, 0xC1, 0xF0, 0x7C, 0x1F]),\n            \"0Z0Z0Z0Z\"\n        );\n        assert_eq!(\n            decode(Crockford, \"Z0Z0Z0Z0\").unwrap(),\n            [0xF8, 0x3E, 0x0F, 0x83, 0xE0]\n        );\n        assert_eq!(\n            decode(Crockford, \"0Z0Z0Z0Z\").unwrap(),\n            [0x07, 0xC1, 0xF0, 0x7C, 0x1F]\n        );\n    }\n\n    #[test]\n    fn masks_rfc4648() {\n        assert_eq!(\n            encode(RFC4648 { padding: true }, &[0xF8, 0x3E, 0x7F, 0x83, 0xE7]),\n            \"7A7H7A7H\"\n        );\n        assert_eq!(\n            encode(RFC4648 { padding: true }, &[0x77, 0xC1, 0xF7, 0x7C, 0x1F]),\n            \"O7A7O7A7\"\n        );\n        assert_eq!(\n            decode(RFC4648 { padding: true }, \"7A7H7A7H\").unwrap(),\n            [0xF8, 0x3E, 0x7F, 0x83, 0xE7]\n        );\n        assert_eq!(\n            decode(RFC4648 { padding: true }, \"O7A7O7A7\").unwrap(),\n            [0x77, 0xC1, 0xF7, 0x7C, 0x1F]\n        );\n        assert_eq!(\n            encode(RFC4648 { padding: true }, &[0xF8, 0x3E, 0x7F, 0x83]),\n            \"7A7H7AY=\"\n        );\n    }\n\n    #[test]\n    fn masks_unpadded_rfc4648() {\n        assert_eq!(\n            encode(RFC4648 { padding: false }, &[0xF8, 0x3E, 0x7F, 0x83, 0xE7]),\n            \"7A7H7A7H\"\n        );\n        assert_eq!(\n            encode(RFC4648 { padding: false }, &[0x77, 0xC1, 0xF7, 0x7C, 0x1F]),\n            \"O7A7O7A7\"\n        );\n        assert_eq!(\n            decode(RFC4648 { padding: false }, \"7A7H7A7H\").unwrap(),\n            [0xF8, 0x3E, 0x7F, 0x83, 0xE7]\n        );\n        assert_eq!(\n            decode(RFC4648 { padding: false }, \"O7A7O7A7\").unwrap(),\n            [0x77, 0xC1, 0xF7, 0x7C, 0x1F]\n        );\n        assert_eq!(\n            encode(RFC4648 { padding: false }, &[0xF8, 0x3E, 0x7F, 0x83]),\n            \"7A7H7AY\"\n        );\n    }\n\n    #[test]\n    fn padding() {\n        let num_padding = [0, 6, 4, 3, 1];\n        for i in 1..6 {\n            let encoded = encode(\n                RFC4648 { padding: true },\n                (0..(i as u8)).collect::<Vec<u8>>().as_ref(),\n            );\n            assert_eq!(encoded.len(), 8);\n            for j in 0..(num_padding[i % 5]) {\n                assert_eq!(encoded.as_bytes()[encoded.len() - j - 1], b'=');\n            }\n            for j in 0..(8 - num_padding[i % 5]) {\n                assert!(encoded.as_bytes()[j] != b'=');\n            }\n        }\n    }\n\n    #[test]\n    fn invertible_encore() {\n        fn test(data: Vec<u8>) -> bool {\n            decode(Encore, encode(Encore, data.as_ref()).as_ref()).unwrap() == data\n        }\n        quickcheck::quickcheck(test as fn(Vec<u8>) -> bool)\n    }\n\n    #[test]\n    fn invertible_crockford() {\n        fn test(data: Vec<u8>) -> bool {\n            decode(Crockford, encode(Crockford, data.as_ref()).as_ref()).unwrap() == data\n        }\n        quickcheck::quickcheck(test as fn(Vec<u8>) -> bool)\n    }\n\n    #[test]\n    fn invertible_rfc4648() {\n        fn test(data: Vec<u8>) -> bool {\n            decode(\n                RFC4648 { padding: true },\n                encode(RFC4648 { padding: true }, data.as_ref()).as_ref(),\n            )\n            .unwrap()\n                == data\n        }\n        quickcheck::quickcheck(test as fn(Vec<u8>) -> bool)\n    }\n\n    #[test]\n    fn invertible_unpadded_rfc4648() {\n        fn test(data: Vec<u8>) -> bool {\n            decode(\n                RFC4648 { padding: false },\n                encode(RFC4648 { padding: false }, data.as_ref()).as_ref(),\n            )\n            .unwrap()\n                == data\n        }\n        quickcheck::quickcheck(test as fn(Vec<u8>) -> bool)\n    }\n\n    #[test]\n    fn lower_case() {\n        fn test(data: Vec<B32>) -> bool {\n            let data: String = data.iter().map(|e| e.c as char).collect();\n            decode(Crockford, data.as_ref())\n                == decode(Crockford, data.to_ascii_lowercase().as_ref())\n        }\n        quickcheck::quickcheck(test as fn(Vec<B32>) -> bool)\n    }\n\n    #[test]\n    #[allow(non_snake_case)]\n    fn iIlL1_oO0() {\n        assert_eq!(decode(Crockford, \"IiLlOo\"), decode(Crockford, \"111100\"));\n    }\n\n    #[test]\n    fn invalid_chars_crockford() {\n        assert_eq!(decode(Crockford, \",\"), None)\n    }\n\n    #[test]\n    fn invalid_chars_rfc4648() {\n        assert_eq!(decode(RFC4648 { padding: true }, \",\"), None)\n    }\n\n    #[test]\n    fn invalid_chars_unpadded_rfc4648() {\n        assert_eq!(decode(RFC4648 { padding: false }, \",\"), None)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/cache/client.rs",
    "content": "use std::time::{SystemTime, UNIX_EPOCH};\n\nuse bb8::{ErrorSink, Pool as Bb8Pool, RunError};\nuse bb8_redis::redis::{self as redis, RedisResult};\nuse bb8_redis::RedisConnectionManager;\nuse redis::{FromRedisValue, SetExpiry, ToSingleRedisArg};\n\nuse crate::cache::error::{Error, OpResult, Result};\nuse crate::cache::tracer::CacheTracer;\nuse crate::model::Request;\nuse crate::trace::Tracer;\n\n/// TTL operation for cache write commands.\n#[derive(Debug, Clone, Copy)]\npub enum TtlOp {\n    /// Preserve the existing TTL (KEEPTTL for SET; no-op for others).\n    Keep,\n    /// Set TTL in milliseconds (PX for SET; atomic PEXPIREAT for others).\n    SetMs(u64),\n    /// Remove TTL / never expire (no TTL flags for SET; atomic PERSIST for others).\n    Persist,\n}\n\n/// Direction for list operations.\n#[derive(Debug, Clone, Copy)]\npub enum ListDirection {\n    Left,\n    Right,\n}\n\nimpl From<ListDirection> for redis::Direction {\n    fn from(value: ListDirection) -> Self {\n        match value {\n            ListDirection::Left => Self::Left,\n            ListDirection::Right => Self::Right,\n        }\n    }\n}\n\nenum LRemOp {\n    All,\n    First(u64),\n    Last(u64),\n}\n\nimpl LRemOp {\n    fn name(&self) -> &'static str {\n        match self {\n            LRemOp::All => \"remove all\",\n            LRemOp::First(_) => \"remove first\",\n            LRemOp::Last(_) => \"remove last\",\n        }\n    }\n}\n\n/// Converts a relative TTL in milliseconds to an absolute PEXPIREAT timestamp.\nfn expire_at_ms(relative_ms: u64) -> u64 {\n    SystemTime::now()\n        .duration_since(UNIX_EPOCH)\n        .unwrap()\n        .as_millis() as u64\n        + relative_ms\n}\n\n/// Builds an expiration command for a key based on the TTL operation.\nfn exp_cmd(key: &str, ttl: Option<TtlOp>) -> Option<redis::Cmd> {\n    match ttl? {\n        TtlOp::Keep => None,\n        TtlOp::SetMs(ms) => Some(\n            redis::cmd(\"PEXPIREAT\")\n                .arg(key)\n                .arg(expire_at_ms(ms))\n                .take(),\n        ),\n        TtlOp::Persist => Some(redis::cmd(\"PERSIST\").arg(key).take()),\n    }\n}\n\n#[derive(Debug, Clone)]\nstruct RedisErrorSink {\n    cluster_name: String,\n}\n\nimpl ErrorSink<redis::RedisError> for RedisErrorSink {\n    fn sink(&self, err: redis::RedisError) {\n        log::error!(\n            \"cache cluster {}: connection pool error: {:?}\",\n            self.cluster_name,\n            err\n        );\n    }\n\n    fn boxed_clone(&self) -> Box<dyn ErrorSink<redis::RedisError>> {\n        Box::new(self.clone())\n    }\n}\n\nstruct RedisBackend {\n    pool: Bb8Pool<RedisConnectionManager>,\n}\n\nimpl RedisBackend {\n    fn new(\n        client: redis::Client,\n        cluster_name: String,\n        min_conns: u32,\n        max_conns: u32,\n    ) -> anyhow::Result<Self> {\n        let conn_info = client.get_connection_info().clone();\n        let mgr = RedisConnectionManager::new(conn_info)?;\n\n        let mut pool = Bb8Pool::builder()\n            .error_sink(Box::new(RedisErrorSink { cluster_name }))\n            .max_size(if max_conns > 0 {\n                max_conns\n            } else {\n                (std::thread::available_parallelism()\n                    .map(|n| n.get())\n                    .unwrap_or(4)\n                    * 10) as u32\n            })\n            .connection_timeout(std::time::Duration::from_secs(10));\n\n        if min_conns > 0 {\n            pool = pool.min_idle(Some(min_conns));\n        }\n\n        let pool = pool.build_unchecked(mgr);\n        Ok(Self { pool })\n    }\n\n    async fn conn(&self) -> Result<bb8::PooledConnection<'_, RedisConnectionManager>> {\n        self.pool.get().await.map_err(|e| match e {\n            RunError::User(err) => Error::Redis(err),\n            RunError::TimedOut => Error::PoolTimeout,\n        })\n    }\n\n    /// Execute a single Redis command, mapping nil to Error::Miss.\n    async fn query<T, F>(&self, f: F) -> Result<T>\n    where\n        F: FnOnce(&mut redis::Pipeline) -> &mut redis::Pipeline,\n        T: FromRedisValue,\n    {\n        let mut pipe = redis::pipe();\n        let mut conn = self.conn().await?;\n        let res: RedisResult<(Option<T>,)> = f(&mut pipe).query_async(&mut *conn).await;\n        match res?.0 {\n            None => Err(Error::Miss),\n            Some(v) => Ok(v),\n        }\n    }\n\n    /// Execute a Redis command atomically with TTL management for one key.\n    async fn query_with_ttl<T, F>(&self, key: &str, ttl: Option<TtlOp>, f: F) -> Result<T>\n    where\n        F: FnOnce(&mut redis::Pipeline) -> &mut redis::Pipeline,\n        T: FromRedisValue,\n    {\n        let exp = exp_cmd(key, ttl);\n        if exp.is_none() {\n            return self.query(f).await;\n        }\n\n        let mut pipe = redis::pipe();\n        let mut conn = self.conn().await?;\n        pipe.atomic();\n        f(&mut pipe);\n        if let Some(exp) = exp {\n            pipe.add_command(exp).ignore();\n        }\n        let res: RedisResult<(Option<T>,)> = pipe.query_async(&mut *conn).await;\n        match res?.0 {\n            None => Err(Error::Miss),\n            Some(v) => Ok(v),\n        }\n    }\n\n    /// Execute a Redis command atomically with TTL management for two keys.\n    async fn query_with_ttl2<T, F>(&self, keys: (&str, &str), ttl: Option<TtlOp>, f: F) -> Result<T>\n    where\n        F: FnOnce(&mut redis::Pipeline) -> &mut redis::Pipeline,\n        T: FromRedisValue,\n    {\n        let exp_a = exp_cmd(keys.0, ttl);\n        let exp_b = exp_cmd(keys.1, ttl);\n        if exp_a.is_none() && exp_b.is_none() {\n            return self.query(f).await;\n        }\n\n        let mut pipe = redis::pipe();\n        let mut conn = self.conn().await?;\n        pipe.atomic();\n        f(&mut pipe);\n        if let Some(exp) = exp_a {\n            pipe.add_command(exp).ignore();\n        }\n        if let Some(exp) = exp_b {\n            pipe.add_command(exp).ignore();\n        }\n        let res: RedisResult<(Option<T>,)> = pipe.query_async(&mut *conn).await;\n        match res?.0 {\n            None => Err(Error::Miss),\n            Some(v) => Ok(v),\n        }\n    }\n\n    async fn _set<T, V>(\n        &self,\n        key: &str,\n        ttl: Option<TtlOp>,\n        set_cond: Option<redis::ExistenceCheck>,\n        get: bool,\n        value: V,\n    ) -> Result<T>\n    where\n        V: ToSingleRedisArg + Sync + Send,\n        T: FromRedisValue,\n    {\n        let mut opts = redis::SetOptions::default().get(get);\n\n        if let Some(set_cond) = set_cond {\n            opts = opts.conditional_set(set_cond);\n        }\n\n        if let Some(set_exp) = ttl.and_then(|t| match t {\n            TtlOp::Keep => Some(SetExpiry::KEEPTTL),\n            TtlOp::SetMs(ms) => Some(SetExpiry::PX(ms)),\n            TtlOp::Persist => None,\n        }) {\n            opts = opts.with_expiration(set_exp);\n        }\n\n        match self.query(|pipe| pipe.set_options(key, value, opts)).await {\n            Err(Error::Miss) if matches!(set_cond, Some(redis::ExistenceCheck::NX)) => {\n                Err(Error::KeyExist)\n            }\n            other => other,\n        }\n    }\n\n    async fn get(&self, key: &str) -> Result<Vec<u8>> {\n        self.query(|pipe| pipe.get(key)).await\n    }\n\n    async fn set(&self, key: &str, value: &[u8], ttl: Option<TtlOp>) -> Result<()> {\n        self._set(key, ttl, None, false, value).await\n    }\n\n    async fn set_if_not_exists(&self, key: &str, value: &[u8], ttl: Option<TtlOp>) -> Result<()> {\n        self._set(key, ttl, Some(redis::ExistenceCheck::NX), false, value)\n            .await\n    }\n\n    async fn replace(&self, key: &str, value: &[u8], ttl: Option<TtlOp>) -> Result<()> {\n        self._set(key, ttl, Some(redis::ExistenceCheck::XX), false, value)\n            .await\n    }\n\n    async fn get_and_set(&self, key: &str, value: &[u8], ttl: Option<TtlOp>) -> Result<Vec<u8>> {\n        self._set(key, ttl, None, true, value).await\n    }\n\n    async fn get_and_delete(&self, key: &str) -> Result<Vec<u8>> {\n        self.query(|pipe| pipe.get_del(key)).await\n    }\n\n    async fn delete(&self, keys: &[&str]) -> Result<u64> {\n        self.query(|pipe| pipe.del(keys)).await\n    }\n\n    async fn mget(&self, keys: &[&str]) -> Result<Vec<Option<Vec<u8>>>> {\n        self.query(|pipe| pipe.mget(keys)).await\n    }\n\n    async fn append(&self, key: &str, value: &[u8], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.append(key, value))\n            .await\n    }\n\n    async fn get_range(&self, key: &str, start: i64, end: i64) -> Result<Vec<u8>> {\n        self.query(|pipe| pipe.getrange(key, start as isize, end as isize))\n            .await\n    }\n\n    async fn set_range(\n        &self,\n        key: &str,\n        offset: i64,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n    ) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.setrange(key, offset as isize, value))\n            .await\n    }\n\n    async fn strlen(&self, key: &str) -> Result<i64> {\n        self.query(|pipe| pipe.strlen(key)).await\n    }\n\n    async fn incr_by(&self, key: &str, delta: i64, ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.incr(key, delta))\n            .await\n    }\n\n    async fn incr_by_float(&self, key: &str, delta: f64, ttl: Option<TtlOp>) -> Result<f64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.incr(key, delta))\n            .await\n    }\n\n    async fn lpush(&self, key: &str, values: &[&[u8]], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.lpush(key, values))\n            .await\n    }\n\n    async fn rpush(&self, key: &str, values: &[&[u8]], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.rpush(key, values))\n            .await\n    }\n\n    async fn lpop(&self, key: &str, ttl: Option<TtlOp>) -> Result<Vec<u8>> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.lpop(key, None))\n            .await\n    }\n\n    async fn rpop(&self, key: &str, ttl: Option<TtlOp>) -> Result<Vec<u8>> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.rpop(key, None))\n            .await\n    }\n\n    async fn lindex(&self, key: &str, index: i64) -> Result<Vec<u8>> {\n        self.query(|pipe| pipe.lindex(key, index as isize)).await\n    }\n\n    async fn lset(&self, key: &str, index: i64, value: &[u8], _ttl: Option<TtlOp>) -> Result<()> {\n        self.query(|pipe| pipe.lset(key, index as isize, value))\n            .await\n    }\n\n    async fn lrange(&self, key: &str, start: i64, stop: i64) -> Result<Vec<Vec<u8>>> {\n        self.query(|pipe| pipe.lrange(key, start as isize, stop as isize))\n            .await\n    }\n\n    async fn ltrim(&self, key: &str, start: i64, stop: i64, _ttl: Option<TtlOp>) -> Result<()> {\n        self.query(|pipe| pipe.ltrim(key, start as isize, stop as isize))\n            .await\n    }\n\n    async fn linsert_before(\n        &self,\n        key: &str,\n        pivot: &[u8],\n        value: &[u8],\n        ttl: Option<TtlOp>,\n    ) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.linsert_before(key, pivot, value))\n            .await\n    }\n\n    async fn linsert_after(\n        &self,\n        key: &str,\n        pivot: &[u8],\n        value: &[u8],\n        ttl: Option<TtlOp>,\n    ) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.linsert_after(key, pivot, value))\n            .await\n    }\n\n    async fn lrem(&self, key: &str, count: i64, value: &[u8], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.lrem(key, count as isize, value))\n            .await\n    }\n\n    async fn lmove(\n        &self,\n        src: &str,\n        dst: &str,\n        src_dir: ListDirection,\n        dst_dir: ListDirection,\n        ttl: Option<TtlOp>,\n    ) -> Result<Vec<u8>> {\n        self.query_with_ttl2((src, dst), ttl, |pipe| {\n            pipe.lmove(src, dst, src_dir.into(), dst_dir.into())\n        })\n        .await\n    }\n\n    async fn llen(&self, key: &str) -> Result<i64> {\n        self.query(|pipe| pipe.llen(key)).await\n    }\n\n    async fn sadd(&self, key: &str, members: &[&[u8]], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.sadd(key, members))\n            .await\n    }\n\n    async fn srem(&self, key: &str, members: &[&[u8]], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.srem(key, members))\n            .await\n    }\n\n    async fn sismember(&self, key: &str, member: &[u8]) -> Result<bool> {\n        self.query(|pipe| pipe.sismember(key, member)).await\n    }\n\n    async fn spop(&self, key: &str, ttl: Option<TtlOp>) -> Result<Vec<u8>> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.spop(key)).await\n    }\n\n    async fn spop_n(&self, key: &str, count: usize, ttl: Option<TtlOp>) -> Result<Vec<Vec<u8>>> {\n        self.query_with_ttl(key, ttl, |pipe| pipe.spop(key).arg(count))\n            .await\n    }\n\n    async fn srandmember(&self, key: &str) -> Result<Vec<u8>> {\n        self.query(|pipe| pipe.srandmember(key)).await\n    }\n\n    async fn srandmember_n(&self, key: &str, count: i64) -> Result<Vec<Vec<u8>>> {\n        self.query(|pipe| pipe.srandmember_multiple(key, count as isize))\n            .await\n    }\n\n    async fn smembers(&self, key: &str) -> Result<Vec<Vec<u8>>> {\n        self.query(|pipe| pipe.smembers(key)).await\n    }\n\n    async fn scard(&self, key: &str) -> Result<i64> {\n        self.query(|pipe| pipe.scard(key)).await\n    }\n\n    async fn sdiff(&self, keys: &[&str]) -> Result<Vec<Vec<u8>>> {\n        self.query(|pipe| pipe.sdiff(keys)).await\n    }\n\n    async fn sdiffstore(&self, dest: &str, keys: &[&str], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(dest, ttl, |pipe| pipe.sdiffstore(dest, keys))\n            .await\n    }\n\n    async fn sinter(&self, keys: &[&str]) -> Result<Vec<Vec<u8>>> {\n        self.query(|pipe| pipe.sinter(keys)).await\n    }\n\n    async fn sinterstore(&self, dest: &str, keys: &[&str], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(dest, ttl, |pipe| pipe.sinterstore(dest, keys))\n            .await\n    }\n\n    async fn sunion(&self, keys: &[&str]) -> Result<Vec<Vec<u8>>> {\n        self.query(|pipe| pipe.sunion(keys)).await\n    }\n\n    async fn sunionstore(&self, dest: &str, keys: &[&str], ttl: Option<TtlOp>) -> Result<i64> {\n        self.query_with_ttl(dest, ttl, |pipe| pipe.sunionstore(dest, keys))\n            .await\n    }\n\n    async fn smove(&self, src: &str, dst: &str, member: &[u8], ttl: Option<TtlOp>) -> Result<bool> {\n        self.query_with_ttl2((src, dst), ttl, |pipe| pipe.smove(src, dst, member))\n            .await\n    }\n}\n\n/// A cache client for a Redis-compatible cluster.\n/// Handles key prefixing, tracing, and dispatching to the Redis backend.\npub struct Client {\n    backend: RedisBackend,\n    tracer: CacheTracer,\n    key_prefix: Option<String>,\n}\n\nimpl Client {\n    pub(crate) fn new(\n        client: redis::Client,\n        key_prefix: Option<String>,\n        tracer: Tracer,\n        min_conns: u32,\n        max_conns: u32,\n    ) -> anyhow::Result<Self> {\n        let cluster_name = key_prefix.clone().unwrap_or_else(|| \"default\".to_string());\n        let backend = RedisBackend::new(client, cluster_name, min_conns, max_conns)?;\n        Ok(Self {\n            backend,\n            tracer: CacheTracer::new(tracer),\n            key_prefix,\n        })\n    }\n\n    fn prefixed_key(&self, key: &str) -> String {\n        match &self.key_prefix {\n            Some(prefix) => format!(\"{}{}\", prefix, key),\n            None => key.to_string(),\n        }\n    }\n\n    /// Get a value by key.\n    pub async fn get(&self, key: &str, source: Option<&Request>) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"get\", false, &[&key], async || {\n                self.backend.get(&key).await\n            })\n            .await\n    }\n\n    /// Set a value by key with optional TTL operation.\n    pub async fn set(\n        &self,\n        key: &str,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<()> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set\", true, &[&key], async || {\n                self.backend.set(&key, value, ttl).await\n            })\n            .await\n    }\n\n    /// Set a value only if the key doesn't exist (SET NX).\n    pub async fn set_if_not_exists(\n        &self,\n        key: &str,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<()> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set if not exists\", true, &[&key], async || {\n                self.backend.set_if_not_exists(&key, value, ttl).await\n            })\n            .await\n    }\n\n    /// Replace a value only if the key exists (SET XX).\n    pub async fn replace(\n        &self,\n        key: &str,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<()> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"replace\", true, &[&key], async || {\n                self.backend.replace(&key, value, ttl).await\n            })\n            .await\n    }\n\n    /// Get old value and set new value atomically (SET GET).\n    pub async fn get_and_set(\n        &self,\n        key: &str,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"get and set\", true, &[&key], async || {\n                self.backend.get_and_set(&key, value, ttl).await\n            })\n            .await\n    }\n\n    /// Get value and delete key atomically (GETDEL).\n    pub async fn get_and_delete(&self, key: &str, source: Option<&Request>) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"get and delete\", true, &[&key], async || {\n                self.backend.get_and_delete(&key).await\n            })\n            .await\n    }\n\n    /// Delete one or more keys.\n    pub async fn delete(&self, keys: &[&str], source: Option<&Request>) -> OpResult<u64> {\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        self.tracer\n            .trace(source, \"delete\", true, &key_refs, async || {\n                self.backend.delete(&key_refs).await\n            })\n            .await\n    }\n\n    /// Get multiple values (MGET).\n    pub async fn mget(\n        &self,\n        keys: &[&str],\n        source: Option<&Request>,\n    ) -> OpResult<Vec<Option<Vec<u8>>>> {\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        self.tracer\n            .trace(source, \"multi get\", false, &key_refs, async || {\n                self.backend.mget(&key_refs).await\n            })\n            .await\n    }\n\n    /// Append to a string value.\n    pub async fn append(\n        &self,\n        key: &str,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"append\", true, &[&key], async || {\n                self.backend.append(&key, value, ttl).await\n            })\n            .await\n    }\n\n    /// Get a substring of a string value.\n    pub async fn get_range(\n        &self,\n        key: &str,\n        start: i64,\n        end: i64,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"get range\", false, &[&key], async || {\n                self.backend.get_range(&key, start, end).await\n            })\n            .await\n    }\n\n    /// Set a substring at a specific offset.\n    pub async fn set_range(\n        &self,\n        key: &str,\n        offset: i64,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set range\", true, &[&key], async || {\n                self.backend.set_range(&key, offset, value, ttl).await\n            })\n            .await\n    }\n\n    /// Get string length.\n    pub async fn strlen(&self, key: &str, source: Option<&Request>) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"len\", false, &[&key], async || {\n                self.backend.strlen(&key).await\n            })\n            .await\n    }\n\n    /// Increment an integer value.\n    pub async fn incr_by(\n        &self,\n        key: &str,\n        delta: i64,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"increment\", true, &[&key], async || {\n                self.backend.incr_by(&key, delta, ttl).await\n            })\n            .await\n    }\n\n    /// Decrement an integer value.\n    pub async fn decr_by(\n        &self,\n        key: &str,\n        delta: i64,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"decrement\", true, &[&key], async || {\n                self.backend.incr_by(&key, -delta, ttl).await\n            })\n            .await\n    }\n\n    /// Increment a float value.\n    pub async fn incr_by_float(\n        &self,\n        key: &str,\n        delta: f64,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<f64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"increment\", true, &[&key], async || {\n                self.backend.incr_by_float(&key, delta, ttl).await\n            })\n            .await\n    }\n\n    /// Push values to the left (head) of a list.\n    pub async fn lpush(\n        &self,\n        key: &str,\n        values: &[&[u8]],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"push left\", true, &[&key], async || {\n                self.backend.lpush(&key, values, ttl).await\n            })\n            .await\n    }\n\n    /// Push values to the right (tail) of a list.\n    pub async fn rpush(\n        &self,\n        key: &str,\n        values: &[&[u8]],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"push right\", true, &[&key], async || {\n                self.backend.rpush(&key, values, ttl).await\n            })\n            .await\n    }\n\n    /// Pop value from the left (head) of a list.\n    pub async fn lpop(\n        &self,\n        key: &str,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"pop left\", true, &[&key], async || {\n                self.backend.lpop(&key, ttl).await\n            })\n            .await\n    }\n\n    /// Pop value from the right (tail) of a list.\n    pub async fn rpop(\n        &self,\n        key: &str,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"pop right\", true, &[&key], async || {\n                self.backend.rpop(&key, ttl).await\n            })\n            .await\n    }\n\n    /// Get element at index from a list.\n    pub async fn lindex(\n        &self,\n        key: &str,\n        index: i64,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"list get\", false, &[&key], async || {\n                self.backend.lindex(&key, index).await\n            })\n            .await\n    }\n\n    /// Set element at index in a list.\n    pub async fn lset(\n        &self,\n        key: &str,\n        index: i64,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<()> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"list set\", true, &[&key], async || {\n                self.backend.lset(&key, index, value, ttl).await\n            })\n            .await\n    }\n\n    /// Get a range of elements from a list.\n    pub async fn lrange(\n        &self,\n        key: &str,\n        start: i64,\n        stop: i64,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<Vec<u8>>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"get range\", false, &[&key], async || {\n                self.backend.lrange(&key, start, stop).await\n            })\n            .await\n    }\n\n    /// Get all elements of a list. Equivalent to LRANGE 0 -1 but traced as \"items\".\n    pub async fn lrange_all(&self, key: &str, source: Option<&Request>) -> OpResult<Vec<Vec<u8>>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"items\", false, &[&key], async || {\n                self.backend.lrange(&key, 0, -1).await\n            })\n            .await\n    }\n\n    /// Trim list to specified range.\n    pub async fn ltrim(\n        &self,\n        key: &str,\n        start: i64,\n        stop: i64,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<()> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"list trim\", true, &[&key], async || {\n                self.backend.ltrim(&key, start, stop, ttl).await\n            })\n            .await\n    }\n\n    /// Insert element before pivot in list.\n    pub async fn linsert_before(\n        &self,\n        key: &str,\n        pivot: &[u8],\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"insert before\", true, &[&key], async || {\n                let result = self.backend.linsert_before(&key, pivot, value, ttl).await;\n                match result {\n                    Ok(n) if n < 0 => Err(Error::Miss),\n                    other => other,\n                }\n            })\n            .await\n    }\n\n    /// Insert element after pivot in list.\n    pub async fn linsert_after(\n        &self,\n        key: &str,\n        pivot: &[u8],\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"insert after\", true, &[&key], async || {\n                let result = self.backend.linsert_after(&key, pivot, value, ttl).await;\n                match result {\n                    Ok(n) if n < 0 => Err(Error::Miss),\n                    other => other,\n                }\n            })\n            .await\n    }\n\n    /// Remove elements from list.\n    pub async fn lrem_all(\n        &self,\n        key: &str,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        self._lrem(key, LRemOp::All, value, ttl, source).await\n    }\n    /// Remove elements from list.\n    pub async fn lrem_first(\n        &self,\n        key: &str,\n        count: u64,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        self._lrem(key, LRemOp::First(count), value, ttl, source)\n            .await\n    }\n    /// Remove elements from list.\n    pub async fn lrem_last(\n        &self,\n        key: &str,\n        count: u64,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        self._lrem(key, LRemOp::Last(count), value, ttl, source)\n            .await\n    }\n\n    async fn _lrem(\n        &self,\n        key: &str,\n        op: LRemOp,\n        value: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, op.name(), true, &[&key], async || {\n                let count: i64 = {\n                    use LRemOp::*;\n                    match &op {\n                        First(0) | Last(0) => return Ok(0),\n                        All => 0,\n                        First(count) => *count as i64,\n                        Last(count) => -(*count as i64),\n                    }\n                };\n                self.backend.lrem(&key, count, value, ttl).await\n            })\n            .await\n    }\n\n    /// Move element between lists.\n    pub async fn lmove(\n        &self,\n        src: &str,\n        dst: &str,\n        src_dir: ListDirection,\n        dst_dir: ListDirection,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let src_key = self.prefixed_key(src);\n        let dst_key = self.prefixed_key(dst);\n        self.tracer\n            .trace(\n                source,\n                \"list move\",\n                true,\n                &[&src_key, &dst_key],\n                async || {\n                    self.backend\n                        .lmove(&src_key, &dst_key, src_dir, dst_dir, ttl)\n                        .await\n                },\n            )\n            .await\n    }\n\n    /// Get list length.\n    pub async fn llen(&self, key: &str, source: Option<&Request>) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"list len\", false, &[&key], async || {\n                self.backend.llen(&key).await\n            })\n            .await\n    }\n\n    /// Add members to a set.\n    pub async fn sadd(\n        &self,\n        key: &str,\n        members: &[&[u8]],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set add\", true, &[&key], async || {\n                self.backend.sadd(&key, members, ttl).await\n            })\n            .await\n    }\n\n    /// Remove members from a set.\n    pub async fn srem(\n        &self,\n        key: &str,\n        members: &[&[u8]],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set remove\", true, &[&key], async || {\n                self.backend.srem(&key, members, ttl).await\n            })\n            .await\n    }\n\n    /// Check if member exists in set.\n    pub async fn sismember(\n        &self,\n        key: &str,\n        member: &[u8],\n        source: Option<&Request>,\n    ) -> OpResult<bool> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set contains\", false, &[&key], async || {\n                self.backend.sismember(&key, member).await\n            })\n            .await\n    }\n\n    /// Pop a single random member from a set (SPOP without count).\n    pub async fn spop(\n        &self,\n        key: &str,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set pop one\", true, &[&key], async || {\n                self.backend.spop(&key, ttl).await\n            })\n            .await\n    }\n\n    /// Pop random members from a set (SPOP with count).\n    pub async fn spop_n(\n        &self,\n        key: &str,\n        count: usize,\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<Vec<u8>>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set pop\", true, &[&key], async || {\n                self.backend.spop_n(&key, count, ttl).await\n            })\n            .await\n    }\n\n    /// Get a single random member from a set without removing (SRANDMEMBER).\n    pub async fn srandmember(&self, key: &str, source: Option<&Request>) -> OpResult<Vec<u8>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set sample one\", false, &[&key], async || {\n                self.backend.srandmember(&key).await\n            })\n            .await\n    }\n\n    /// Get random members from a set without removing (SRANDMEMBER).\n    pub async fn srandmember_n(\n        &self,\n        key: &str,\n        count: i64,\n        source: Option<&Request>,\n    ) -> OpResult<Vec<Vec<u8>>> {\n        let key = self.prefixed_key(key);\n        let op = if count < 0 {\n            \"set sample with replacement\"\n        } else {\n            \"set sample\"\n        };\n        self.tracer\n            .trace(source, op, false, &[&key], async || {\n                self.backend.srandmember_n(&key, count).await\n            })\n            .await\n    }\n\n    /// Get all members of a set.\n    pub async fn smembers(&self, key: &str, source: Option<&Request>) -> OpResult<Vec<Vec<u8>>> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set items\", false, &[&key], async || {\n                self.backend.smembers(&key).await\n            })\n            .await\n    }\n\n    /// Get set cardinality.\n    pub async fn scard(&self, key: &str, source: Option<&Request>) -> OpResult<i64> {\n        let key = self.prefixed_key(key);\n        self.tracer\n            .trace(source, \"set len\", false, &[&key], async || {\n                self.backend.scard(&key).await\n            })\n            .await\n    }\n\n    /// Set difference.\n    pub async fn sdiff(&self, keys: &[&str], source: Option<&Request>) -> OpResult<Vec<Vec<u8>>> {\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        self.tracer\n            .trace(source, \"set diff\", false, &key_refs, async || {\n                self.backend.sdiff(&key_refs).await\n            })\n            .await\n    }\n\n    /// Store set difference.\n    pub async fn sdiffstore(\n        &self,\n        dest: &str,\n        keys: &[&str],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let dest_key = self.prefixed_key(dest);\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let mut all_keys: Vec<&str> = vec![dest_key.as_str()];\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        all_keys.extend(key_refs.iter().copied());\n        self.tracer\n            .trace(source, \"store set diff\", true, &all_keys, async || {\n                self.backend.sdiffstore(&dest_key, &key_refs, ttl).await\n            })\n            .await\n    }\n\n    /// Set intersection.\n    pub async fn sinter(&self, keys: &[&str], source: Option<&Request>) -> OpResult<Vec<Vec<u8>>> {\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        self.tracer\n            .trace(source, \"intersect\", false, &key_refs, async || {\n                self.backend.sinter(&key_refs).await\n            })\n            .await\n    }\n\n    /// Store set intersection.\n    pub async fn sinterstore(\n        &self,\n        dest: &str,\n        keys: &[&str],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let dest_key = self.prefixed_key(dest);\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let mut all_keys: Vec<&str> = vec![dest_key.as_str()];\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        all_keys.extend(key_refs.iter().copied());\n        self.tracer\n            .trace(source, \"store set intersect\", true, &all_keys, async || {\n                self.backend.sinterstore(&dest_key, &key_refs, ttl).await\n            })\n            .await\n    }\n\n    /// Set union.\n    pub async fn sunion(&self, keys: &[&str], source: Option<&Request>) -> OpResult<Vec<Vec<u8>>> {\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        self.tracer\n            .trace(source, \"union\", false, &key_refs, async || {\n                self.backend.sunion(&key_refs).await\n            })\n            .await\n    }\n\n    /// Store set union.\n    pub async fn sunionstore(\n        &self,\n        dest: &str,\n        keys: &[&str],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<i64> {\n        let dest_key = self.prefixed_key(dest);\n        let prefixed: Vec<String> = keys.iter().map(|k| self.prefixed_key(k)).collect();\n        let mut all_keys: Vec<&str> = vec![dest_key.as_str()];\n        let key_refs: Vec<&str> = prefixed.iter().map(|s| s.as_str()).collect();\n        all_keys.extend(key_refs.iter().copied());\n        self.tracer\n            .trace(source, \"store set union\", true, &all_keys, async || {\n                self.backend.sunionstore(&dest_key, &key_refs, ttl).await\n            })\n            .await\n    }\n\n    /// Move member between sets.\n    pub async fn smove(\n        &self,\n        src: &str,\n        dst: &str,\n        member: &[u8],\n        ttl: Option<TtlOp>,\n        source: Option<&Request>,\n    ) -> OpResult<bool> {\n        let src_key = self.prefixed_key(src);\n        let dst_key = self.prefixed_key(dst);\n        self.tracer\n            .trace(source, \"move\", true, &[&src_key, &dst_key], async || {\n                self.backend.smove(&src_key, &dst_key, member, ttl).await\n            })\n            .await\n    }\n}\n\n#[cfg(test)]\n#[path = \"client_tests.rs\"]\nmod client_tests;\n"
  },
  {
    "path": "runtimes/core/src/cache/client_tests.rs",
    "content": "use std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::OnceLock;\n\nuse crate::cache::client::{Client, ListDirection, TtlOp};\nuse crate::cache::error::Error;\nuse crate::cache::miniredis::MiniredisServer;\nuse crate::trace::Tracer;\n\nstatic TEST_MINIREDIS: OnceLock<MiniredisServer> = OnceLock::new();\nstatic TEST_COUNTER: AtomicU64 = AtomicU64::new(0);\n\nfn new_test_pool() -> Client {\n    let server = TEST_MINIREDIS.get_or_init(|| {\n        // Spawn a dedicated thread with its own runtime for miniredis,\n        // since we can't create a Runtime from within a #[tokio::test].\n        let (tx, rx) = std::sync::mpsc::channel();\n        std::thread::spawn(move || {\n            let rt = tokio::runtime::Runtime::new().expect(\"failed to create runtime\");\n            let server = rt\n                .block_on(MiniredisServer::start())\n                .expect(\"failed to start miniredis for tests\");\n            tx.send(server).expect(\"failed to send server\");\n            // Park forever to keep the runtime (and its server task) alive.\n            loop {\n                std::thread::park();\n            }\n        });\n        rx.recv().expect(\"failed to receive miniredis server\")\n    });\n    let url = format!(\"redis://{}\", server.addr());\n    let client = bb8_redis::redis::Client::open(url).expect(\"failed to create redis client\");\n    // Use a unique key prefix per test to avoid interference between parallel tests.\n    let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);\n    let prefix = format!(\"test{}:\", id);\n    Client::new(client, Some(prefix), Tracer::noop(), 0, 10).expect(\"failed to create cache client\")\n}\n\nfn is_miss(err: &crate::cache::OpError) -> bool {\n    matches!(err.source, Error::Miss)\n}\n\nfn is_key_exist(err: &crate::cache::OpError) -> bool {\n    matches!(err.source, Error::KeyExist)\n}\n\n#[tokio::test]\nasync fn test_set_get_delete() {\n    let p = new_test_pool();\n\n    p.set(\"k\", b\"hello\", None, None).await.unwrap();\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"hello\".to_vec());\n\n    let deleted = p.delete(&[\"k\"], None).await.unwrap();\n    assert_eq!(deleted, 1);\n\n    let err = p.get(\"k\", None).await.unwrap_err();\n    assert!(is_miss(&err));\n}\n\n#[tokio::test]\nasync fn test_set_overwrites() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", None, None).await.unwrap();\n    p.set(\"k\", b\"v2\", None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_get_missing_key() {\n    let p = new_test_pool();\n    let err = p.get(\"missing\", None).await.unwrap_err();\n    assert!(is_miss(&err));\n}\n\n#[tokio::test]\nasync fn test_set_empty_value() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"\", None, None).await.unwrap();\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_binary_value() {\n    let p = new_test_pool();\n    let binary = vec![0u8, 1, 2, 255, 254, 0, 128];\n    p.set(\"k\", &binary, None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), binary);\n}\n\n#[tokio::test]\nasync fn test_set_large_value() {\n    let p = new_test_pool();\n    let large = vec![b'x'; 100_000];\n    p.set(\"k\", &large, None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), large);\n}\n\n#[tokio::test]\nasync fn test_delete_multiple() {\n    let p = new_test_pool();\n    p.set(\"a\", b\"1\", None, None).await.unwrap();\n    p.set(\"b\", b\"2\", None, None).await.unwrap();\n    p.set(\"c\", b\"3\", None, None).await.unwrap();\n\n    let deleted = p.delete(&[\"a\", \"c\", \"missing\"], None).await.unwrap();\n    assert_eq!(deleted, 2);\n    assert!(is_miss(&p.get(\"a\", None).await.unwrap_err()));\n    assert_eq!(p.get(\"b\", None).await.unwrap(), b\"2\".to_vec());\n    assert!(is_miss(&p.get(\"c\", None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_delete_all_missing() {\n    let p = new_test_pool();\n    let deleted = p.delete(&[\"x\", \"y\", \"z\"], None).await.unwrap();\n    assert_eq!(deleted, 0);\n}\n\n#[tokio::test]\nasync fn test_delete_single() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v\", None, None).await.unwrap();\n    assert_eq!(p.delete(&[\"k\"], None).await.unwrap(), 1);\n    assert!(is_miss(&p.get(\"k\", None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_delete_idempotent() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v\", None, None).await.unwrap();\n    assert_eq!(p.delete(&[\"k\"], None).await.unwrap(), 1);\n    assert_eq!(p.delete(&[\"k\"], None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_set_if_not_exists() {\n    let p = new_test_pool();\n\n    p.set_if_not_exists(\"k\", b\"v1\", None, None).await.unwrap();\n\n    let err = p\n        .set_if_not_exists(\"k\", b\"v2\", None, None)\n        .await\n        .unwrap_err();\n    assert!(is_key_exist(&err));\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"v1\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_if_not_exists_with_ttl() {\n    let p = new_test_pool();\n    p.set_if_not_exists(\"k\", b\"v\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_if_not_exists_after_delete() {\n    let p = new_test_pool();\n    p.set_if_not_exists(\"k\", b\"v1\", None, None).await.unwrap();\n    p.delete(&[\"k\"], None).await.unwrap();\n    p.set_if_not_exists(\"k\", b\"v2\", None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_replace() {\n    let p = new_test_pool();\n\n    let err = p.replace(\"k\", b\"v1\", None, None).await.unwrap_err();\n    assert!(is_miss(&err));\n\n    p.set(\"k\", b\"v1\", None, None).await.unwrap();\n    p.replace(\"k\", b\"v2\", None, None).await.unwrap();\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_replace_with_ttl() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", None, None).await.unwrap();\n    p.replace(\"k\", b\"v2\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_replace_multiple_times() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", None, None).await.unwrap();\n    p.replace(\"k\", b\"v2\", None, None).await.unwrap();\n    p.replace(\"k\", b\"v3\", None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v3\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_get_and_set() {\n    let p = new_test_pool();\n\n    // get_and_set on missing key returns Miss but still sets the value.\n    let err = p.get_and_set(\"k\", b\"v1\", None, None).await.unwrap_err();\n    assert!(is_miss(&err));\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"v1\".to_vec());\n\n    let old = p.get_and_set(\"k\", b\"v2\", None, None).await.unwrap();\n    assert_eq!(old, b\"v1\".to_vec());\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_get_and_set_with_ttl() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", None, None).await.unwrap();\n    let old = p\n        .get_and_set(\"k\", b\"v2\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(old, b\"v1\".to_vec());\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_get_and_set_chain() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", None, None).await.unwrap();\n    let old1 = p.get_and_set(\"k\", b\"v2\", None, None).await.unwrap();\n    let old2 = p.get_and_set(\"k\", b\"v3\", None, None).await.unwrap();\n    assert_eq!(old1, b\"v1\".to_vec());\n    assert_eq!(old2, b\"v2\".to_vec());\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v3\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_get_and_delete() {\n    let p = new_test_pool();\n\n    p.set(\"k\", b\"val\", None, None).await.unwrap();\n\n    let old = p.get_and_delete(\"k\", None).await.unwrap();\n    assert_eq!(old, b\"val\".to_vec());\n\n    let err = p.get(\"k\", None).await.unwrap_err();\n    assert!(is_miss(&err));\n\n    // Double delete returns Miss.\n    let err = p.get_and_delete(\"k\", None).await.unwrap_err();\n    assert!(is_miss(&err));\n}\n\n#[tokio::test]\nasync fn test_get_and_delete_missing_key() {\n    let p = new_test_pool();\n    let err = p.get_and_delete(\"missing\", None).await.unwrap_err();\n    assert!(is_miss(&err));\n}\n\n#[tokio::test]\nasync fn test_mget() {\n    let p = new_test_pool();\n\n    p.set(\"a\", b\"1\", None, None).await.unwrap();\n    p.set(\"b\", b\"2\", None, None).await.unwrap();\n\n    let vals = p.mget(&[\"a\", \"b\", \"c\"], None).await.unwrap();\n    assert_eq!(vals.len(), 3);\n    assert_eq!(vals[0], Some(b\"1\".to_vec()));\n    assert_eq!(vals[1], Some(b\"2\".to_vec()));\n    assert_eq!(vals[2], None);\n}\n\n#[tokio::test]\nasync fn test_mget_all_missing() {\n    let p = new_test_pool();\n    let vals = p.mget(&[\"x\", \"y\"], None).await.unwrap();\n    assert_eq!(vals, vec![None, None]);\n}\n\n#[tokio::test]\nasync fn test_mget_single_key() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v\", None, None).await.unwrap();\n    let vals = p.mget(&[\"k\"], None).await.unwrap();\n    assert_eq!(vals, vec![Some(b\"v\".to_vec())]);\n}\n\n#[tokio::test]\nasync fn test_mget_duplicate_keys() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v\", None, None).await.unwrap();\n    let vals = p.mget(&[\"k\", \"k\"], None).await.unwrap();\n    assert_eq!(vals, vec![Some(b\"v\".to_vec()), Some(b\"v\".to_vec())]);\n}\n\n#[tokio::test]\nasync fn test_append() {\n    let p = new_test_pool();\n\n    // Append to non-existent key creates it.\n    let len = p.append(\"k\", b\"hello\", None, None).await.unwrap();\n    assert_eq!(len, 5);\n\n    let len = p.append(\"k\", b\" world\", None, None).await.unwrap();\n    assert_eq!(len, 11);\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"hello world\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_append_empty() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"hi\", None, None).await.unwrap();\n    let len = p.append(\"k\", b\"\", None, None).await.unwrap();\n    assert_eq!(len, 2);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"hi\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_append_with_ttl() {\n    let p = new_test_pool();\n    let len = p\n        .append(\"k\", b\"hello\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(len, 5);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"hello\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_append_binary() {\n    let p = new_test_pool();\n    p.append(\"k\", &[0u8, 1, 2], None, None).await.unwrap();\n    p.append(\"k\", &[3u8, 4], None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), vec![0, 1, 2, 3, 4]);\n}\n\n#[tokio::test]\nasync fn test_get_range() {\n    let p = new_test_pool();\n\n    p.set(\"k\", b\"hello world\", None, None).await.unwrap();\n\n    assert_eq!(\n        p.get_range(\"k\", 0, 4, None).await.unwrap(),\n        b\"hello\".to_vec()\n    );\n    assert_eq!(\n        p.get_range(\"k\", 6, 10, None).await.unwrap(),\n        b\"world\".to_vec()\n    );\n\n    // Negative indices.\n    assert_eq!(\n        p.get_range(\"k\", -5, -1, None).await.unwrap(),\n        b\"world\".to_vec()\n    );\n\n    // Missing key returns empty.\n    assert_eq!(\n        p.get_range(\"missing\", 0, 10, None).await.unwrap(),\n        b\"\".to_vec()\n    );\n}\n\n#[tokio::test]\nasync fn test_get_range_full_string() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"hello\", None, None).await.unwrap();\n    assert_eq!(\n        p.get_range(\"k\", 0, -1, None).await.unwrap(),\n        b\"hello\".to_vec()\n    );\n}\n\n#[tokio::test]\nasync fn test_get_range_single_char() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"abc\", None, None).await.unwrap();\n    assert_eq!(p.get_range(\"k\", 1, 1, None).await.unwrap(), b\"b\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_get_range_beyond_end() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"hi\", None, None).await.unwrap();\n    // Range past end returns what's available.\n    assert_eq!(\n        p.get_range(\"k\", 0, 100, None).await.unwrap(),\n        b\"hi\".to_vec()\n    );\n}\n\n#[tokio::test]\nasync fn test_set_range() {\n    let p = new_test_pool();\n\n    p.set(\"k\", b\"hello world\", None, None).await.unwrap();\n\n    let new_len = p.set_range(\"k\", 6, b\"rust!\", None, None).await.unwrap();\n    assert_eq!(new_len, 11);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"hello rust!\".to_vec());\n\n    // Set range past end extends the string.\n    let new_len = p.set_range(\"k\", 11, b\"!!\", None, None).await.unwrap();\n    assert_eq!(new_len, 13);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"hello rust!!!\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_range_with_gap() {\n    let p = new_test_pool();\n\n    // Setting at offset past current length pads with zeros.\n    let len = p.set_range(\"k\", 5, b\"hi\", None, None).await.unwrap();\n    assert_eq!(len, 7);\n    let val = p.get(\"k\", None).await.unwrap();\n    assert_eq!(&val[..5], &[0, 0, 0, 0, 0]);\n    assert_eq!(&val[5..], b\"hi\");\n}\n\n#[tokio::test]\nasync fn test_set_range_on_missing_key() {\n    let p = new_test_pool();\n    // SETRANGE on missing key at offset 0 creates the key.\n    let len = p.set_range(\"k\", 0, b\"abc\", None, None).await.unwrap();\n    assert_eq!(len, 3);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"abc\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_range_empty_value() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"hello\", None, None).await.unwrap();\n    let len = p.set_range(\"k\", 2, b\"\", None, None).await.unwrap();\n    assert_eq!(len, 5);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"hello\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_range_with_ttl() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"hello\", None, None).await.unwrap();\n    let len = p\n        .set_range(\"k\", 0, b\"HE\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(len, 5);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"HEllo\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_strlen() {\n    let p = new_test_pool();\n\n    assert_eq!(p.strlen(\"missing\", None).await.unwrap(), 0);\n\n    p.set(\"k\", b\"hello\", None, None).await.unwrap();\n    assert_eq!(p.strlen(\"k\", None).await.unwrap(), 5);\n}\n\n#[tokio::test]\nasync fn test_strlen_empty_value() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"\", None, None).await.unwrap();\n    assert_eq!(p.strlen(\"k\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_strlen_after_append() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"hi\", None, None).await.unwrap();\n    p.append(\"k\", b\"!!!\", None, None).await.unwrap();\n    assert_eq!(p.strlen(\"k\", None).await.unwrap(), 5);\n}\n\n#[tokio::test]\nasync fn test_incr_by() {\n    let p = new_test_pool();\n\n    p.set(\"k\", b\"10\", None, None).await.unwrap();\n\n    let v = p.incr_by(\"k\", 5, None, None).await.unwrap();\n    assert_eq!(v, 15);\n\n    let v = p.decr_by(\"k\", 3, None, None).await.unwrap();\n    assert_eq!(v, 12);\n\n    // Negative delta acts as decrement.\n    let v = p.incr_by(\"k\", -2, None, None).await.unwrap();\n    assert_eq!(v, 10);\n}\n\n#[tokio::test]\nasync fn test_incr_creates_key() {\n    let p = new_test_pool();\n\n    let v = p.incr_by(\"k\", 7, None, None).await.unwrap();\n    assert_eq!(v, 7);\n\n    let v = p.get(\"k\", None).await.unwrap();\n    assert_eq!(v, b\"7\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_incr_by_invalid_value() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"not-a-number\", None, None).await.unwrap();\n    assert!(p.incr_by(\"k\", 1, None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_decr_creates_key() {\n    let p = new_test_pool();\n    let v = p.decr_by(\"k\", 3, None, None).await.unwrap();\n    assert_eq!(v, -3);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"-3\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_incr_by_zero() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"5\", None, None).await.unwrap();\n    let v = p.incr_by(\"k\", 0, None, None).await.unwrap();\n    assert_eq!(v, 5);\n}\n\n#[tokio::test]\nasync fn test_incr_negative_value() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"-10\", None, None).await.unwrap();\n    let v = p.incr_by(\"k\", 3, None, None).await.unwrap();\n    assert_eq!(v, -7);\n}\n\n#[tokio::test]\nasync fn test_incr_with_ttl() {\n    let p = new_test_pool();\n    let v = p\n        .incr_by(\"k\", 1, Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(v, 1);\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"1\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_decr_with_ttl() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"100\", None, None).await.unwrap();\n    let v = p\n        .decr_by(\"k\", 25, Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(v, 75);\n}\n\n#[tokio::test]\nasync fn test_incr_by_float() {\n    let p = new_test_pool();\n\n    let v = p.incr_by_float(\"k\", 1.5, None, None).await.unwrap();\n    assert!((v - 1.5).abs() < f64::EPSILON);\n\n    let v = p.incr_by_float(\"k\", 2.5, None, None).await.unwrap();\n    assert!((v - 4.0).abs() < f64::EPSILON);\n\n    let v = p.incr_by_float(\"k\", -1.0, None, None).await.unwrap();\n    assert!((v - 3.0).abs() < f64::EPSILON);\n}\n\n#[tokio::test]\nasync fn test_incr_by_float_on_invalid_value() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"not-a-number\", None, None).await.unwrap();\n    assert!(p.incr_by_float(\"k\", 1.0, None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_incr_by_float_on_integer_string() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"10\", None, None).await.unwrap();\n    let v = p.incr_by_float(\"k\", 0.5, None, None).await.unwrap();\n    assert!((v - 10.5).abs() < f64::EPSILON);\n}\n\n#[tokio::test]\nasync fn test_incr_by_float_with_ttl() {\n    let p = new_test_pool();\n    let v = p\n        .incr_by_float(\"k\", 3.125, Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert!((v - 3.125).abs() < 0.001);\n}\n\n#[tokio::test]\nasync fn test_list_push_pop() {\n    let p = new_test_pool();\n\n    let len = p.lpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(len, 2);\n\n    let len = p.rpush(\"l\", &[b\"c\", b\"d\"], None, None).await.unwrap();\n    assert_eq!(len, 4);\n\n    // LPUSH pushes left-to-right, so [a, b] becomes [b, a] at the head.\n    let items = p.lrange_all(\"l\", None).await.unwrap();\n    assert_eq!(items, vec![b\"b\", b\"a\", b\"c\", b\"d\"]);\n\n    let val = p.lpop(\"l\", None, None).await.unwrap();\n    assert_eq!(val, b\"b\".to_vec());\n\n    let val = p.rpop(\"l\", None, None).await.unwrap();\n    assert_eq!(val, b\"d\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_lpush_single() {\n    let p = new_test_pool();\n    let len = p.lpush(\"l\", &[b\"a\"], None, None).await.unwrap();\n    assert_eq!(len, 1);\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_rpush_single() {\n    let p = new_test_pool();\n    let len = p.rpush(\"l\", &[b\"a\"], None, None).await.unwrap();\n    assert_eq!(len, 1);\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_lpush_extends_existing() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let len = p.lpush(\"l\", &[b\"z\"], None, None).await.unwrap();\n    assert_eq!(len, 3);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"z\".to_vec(), b\"a\".to_vec(), b\"b\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_rpush_extends_existing() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let len = p.rpush(\"l\", &[b\"c\"], None, None).await.unwrap();\n    assert_eq!(len, 3);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lpush_with_ttl() {\n    let p = new_test_pool();\n    let len = p\n        .lpush(\"l\", &[b\"a\"], Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(len, 1);\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_rpush_with_ttl() {\n    let p = new_test_pool();\n    let len = p\n        .rpush(\"l\", &[b\"a\"], Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(len, 1);\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_lpush_multiple_ordering() {\n    let p = new_test_pool();\n    // LPUSH [a, b, c] results in [c, b, a] because each is pushed to head.\n    p.lpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"c\".to_vec(), b\"b\".to_vec(), b\"a\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lpop_rpop_empty() {\n    let p = new_test_pool();\n    assert!(is_miss(&p.lpop(\"missing\", None, None).await.unwrap_err()));\n    assert!(is_miss(&p.rpop(\"missing\", None, None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_lpop_until_empty() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(p.lpop(\"l\", None, None).await.unwrap(), b\"a\".to_vec());\n    assert_eq!(p.lpop(\"l\", None, None).await.unwrap(), b\"b\".to_vec());\n    assert!(is_miss(&p.lpop(\"l\", None, None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_rpop_until_empty() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(p.rpop(\"l\", None, None).await.unwrap(), b\"b\".to_vec());\n    assert_eq!(p.rpop(\"l\", None, None).await.unwrap(), b\"a\".to_vec());\n    assert!(is_miss(&p.rpop(\"l\", None, None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_lpop_with_ttl() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let v = p\n        .lpop(\"l\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(v, b\"a\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_rpop_with_ttl() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let v = p\n        .rpop(\"l\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(v, b\"b\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_lindex() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n\n    assert_eq!(p.lindex(\"l\", 0, None).await.unwrap(), b\"a\".to_vec());\n    assert_eq!(p.lindex(\"l\", 2, None).await.unwrap(), b\"c\".to_vec());\n    assert_eq!(p.lindex(\"l\", -1, None).await.unwrap(), b\"c\".to_vec());\n    assert_eq!(p.lindex(\"l\", -3, None).await.unwrap(), b\"a\".to_vec());\n\n    // Out of range.\n    assert!(is_miss(&p.lindex(\"l\", 10, None).await.unwrap_err()));\n    assert!(is_miss(&p.lindex(\"l\", -10, None).await.unwrap_err()));\n\n    // Missing key.\n    assert!(is_miss(&p.lindex(\"missing\", 0, None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_lindex_single_element() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"only\"], None, None).await.unwrap();\n    assert_eq!(p.lindex(\"l\", 0, None).await.unwrap(), b\"only\".to_vec());\n    assert_eq!(p.lindex(\"l\", -1, None).await.unwrap(), b\"only\".to_vec());\n    assert!(is_miss(&p.lindex(\"l\", 1, None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_lset() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n\n    p.lset(\"l\", 1, b\"B\", None, None).await.unwrap();\n    assert_eq!(p.lindex(\"l\", 1, None).await.unwrap(), b\"B\".to_vec());\n\n    // Negative index.\n    p.lset(\"l\", -1, b\"C\", None, None).await.unwrap();\n    assert_eq!(p.lindex(\"l\", 2, None).await.unwrap(), b\"C\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_lset_out_of_range() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\"], None, None).await.unwrap();\n    assert!(p.lset(\"l\", 5, b\"x\", None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_lset_missing_key() {\n    let p = new_test_pool();\n    assert!(p.lset(\"missing\", 0, b\"x\", None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_lset_negative_out_of_range() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert!(p.lset(\"l\", -5, b\"x\", None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_lset_first_and_last() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.lset(\"l\", 0, b\"A\", None, None).await.unwrap();\n    p.lset(\"l\", -1, b\"C\", None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"A\".to_vec(), b\"b\".to_vec(), b\"C\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lrange() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\", b\"d\", b\"e\"], None, None)\n        .await\n        .unwrap();\n\n    assert_eq!(\n        p.lrange(\"l\", 0, 2, None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n    assert_eq!(\n        p.lrange(\"l\", -2, -1, None).await.unwrap(),\n        vec![b\"d\".to_vec(), b\"e\".to_vec()]\n    );\n\n    // Empty range.\n    assert_eq!(\n        p.lrange(\"l\", 5, 10, None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n\n    // Missing key.\n    assert_eq!(\n        p.lrange(\"missing\", 0, -1, None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n}\n\n#[tokio::test]\nasync fn test_lrange_inverted() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    // start > stop returns empty list.\n    assert_eq!(\n        p.lrange(\"l\", 2, 0, None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n}\n\n#[tokio::test]\nasync fn test_list_range_items() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\", b\"d\"], None, None)\n        .await\n        .unwrap();\n\n    let sub = p.lrange(\"l\", 1, 2, None).await.unwrap();\n    assert_eq!(sub, vec![b\"b\".to_vec(), b\"c\".to_vec()]);\n\n    let all = p.lrange_all(\"l\", None).await.unwrap();\n    assert_eq!(all.len(), 4);\n}\n\n#[tokio::test]\nasync fn test_lrange_all_empty() {\n    let p = new_test_pool();\n    assert_eq!(\n        p.lrange_all(\"missing\", None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n}\n\n#[tokio::test]\nasync fn test_ltrim() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\", b\"d\", b\"e\"], None, None)\n        .await\n        .unwrap();\n\n    p.ltrim(\"l\", 1, 3, None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"b\".to_vec(), b\"c\".to_vec(), b\"d\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_ltrim_clears_when_out_of_range() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.ltrim(\"l\", 5, 10, None, None).await.unwrap();\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_ltrim_negative_indices() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\", b\"d\"], None, None)\n        .await\n        .unwrap();\n    p.ltrim(\"l\", -3, -2, None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_ltrim_keep_all() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.ltrim(\"l\", 0, -1, None, None).await.unwrap();\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 3);\n}\n\n#[tokio::test]\nasync fn test_ltrim_to_single() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.ltrim(\"l\", 1, 1, None, None).await.unwrap();\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_ltrim_missing_key() {\n    let p = new_test_pool();\n    // LTRIM on missing key is a no-op (no error).\n    p.ltrim(\"missing\", 0, 1, None, None).await.unwrap();\n}\n\n#[tokio::test]\nasync fn test_linsert_before() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"c\"], None, None).await.unwrap();\n\n    let len = p.linsert_before(\"l\", b\"c\", b\"b\", None, None).await.unwrap();\n    assert_eq!(len, 3);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n\n    // Pivot not found returns Miss.\n    assert!(is_miss(\n        &p.linsert_before(\"l\", b\"z\", b\"x\", None, None)\n            .await\n            .unwrap_err()\n    ));\n}\n\n#[tokio::test]\nasync fn test_linsert_after() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"c\"], None, None).await.unwrap();\n\n    let len = p.linsert_after(\"l\", b\"a\", b\"b\", None, None).await.unwrap();\n    assert_eq!(len, 3);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n\n    // Pivot not found returns Miss.\n    assert!(is_miss(\n        &p.linsert_after(\"l\", b\"z\", b\"x\", None, None)\n            .await\n            .unwrap_err()\n    ));\n}\n\n#[tokio::test]\nasync fn test_linsert_missing_key() {\n    let p = new_test_pool();\n    // Redis LINSERT on a non-existent key returns 0 (not an error).\n    let result = p\n        .linsert_before(\"missing\", b\"a\", b\"b\", None, None)\n        .await\n        .unwrap();\n    assert_eq!(result, 0);\n    let result = p\n        .linsert_after(\"missing\", b\"a\", b\"b\", None, None)\n        .await\n        .unwrap();\n    assert_eq!(result, 0);\n}\n\n#[tokio::test]\nasync fn test_linsert_before_first() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"b\", b\"c\"], None, None).await.unwrap();\n    p.linsert_before(\"l\", b\"b\", b\"a\", None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_linsert_after_last() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.linsert_after(\"l\", b\"b\", b\"c\", None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_linsert_with_duplicate_pivots() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"a\", b\"b\"], None, None).await.unwrap();\n    // LINSERT finds the first occurrence of the pivot.\n    p.linsert_before(\"l\", b\"a\", b\"x\", None, None).await.unwrap();\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"x\".to_vec(), b\"a\".to_vec(), b\"a\".to_vec(), b\"b\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_linsert_with_ttl() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"c\"], None, None).await.unwrap();\n    p.linsert_before(\"l\", b\"c\", b\"b\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 3);\n}\n\n#[tokio::test]\nasync fn test_lrem_first() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"a\", b\"c\", b\"a\"], None, None)\n        .await\n        .unwrap();\n\n    let removed = p.lrem_first(\"l\", 2, b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 2);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"b\".to_vec(), b\"c\".to_vec(), b\"a\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lrem_last() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"a\", b\"c\", b\"a\"], None, None)\n        .await\n        .unwrap();\n\n    let removed = p.lrem_last(\"l\", 2, b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 2);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lrem_all() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"a\", b\"c\", b\"a\"], None, None)\n        .await\n        .unwrap();\n\n    let removed = p.lrem_all(\"l\", b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 3);\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lrem_missing_key() {\n    let p = new_test_pool();\n    assert_eq!(p.lrem_all(\"missing\", b\"a\", None, None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_lrem_value_not_in_list() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    let removed = p.lrem_all(\"l\", b\"z\", None, None).await.unwrap();\n    assert_eq!(removed, 0);\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 3);\n}\n\n#[tokio::test]\nasync fn test_lrem_first_more_than_exist() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"a\"], None, None).await.unwrap();\n    // count=10 but only 2 occurrences of \"a\".\n    let removed = p.lrem_first(\"l\", 10, b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 2);\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_lrem_last_more_than_exist() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"a\"], None, None).await.unwrap();\n    let removed = p.lrem_last(\"l\", 10, b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 2);\n    assert_eq!(p.lrange_all(\"l\", None).await.unwrap(), vec![b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_lrem_first_zero_count() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"a\"], None, None).await.unwrap();\n    // count=0 in lrem_first should return 0 immediately (no-op).\n    let removed = p.lrem_first(\"l\", 0, b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 0);\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_lrem_last_zero_count() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"a\"], None, None).await.unwrap();\n    let removed = p.lrem_last(\"l\", 0, b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 0);\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_lrem_all_empties_list() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"a\", b\"a\"], None, None).await.unwrap();\n    let removed = p.lrem_all(\"l\", b\"a\", None, None).await.unwrap();\n    assert_eq!(removed, 3);\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_lmove() {\n    let p = new_test_pool();\n    p.rpush(\"src\", &[b\"a\", b\"b\", b\"c\"], None, None)\n        .await\n        .unwrap();\n\n    // Move left of src to right of dst.\n    let val = p\n        .lmove(\n            \"src\",\n            \"dst\",\n            ListDirection::Left,\n            ListDirection::Right,\n            None,\n            None,\n        )\n        .await\n        .unwrap();\n    assert_eq!(val, b\"a\".to_vec());\n\n    assert_eq!(\n        p.lrange_all(\"src\", None).await.unwrap(),\n        vec![b\"b\".to_vec(), b\"c\".to_vec()]\n    );\n    assert_eq!(\n        p.lrange_all(\"dst\", None).await.unwrap(),\n        vec![b\"a\".to_vec()]\n    );\n\n    // Move right of src to left of dst.\n    let val = p\n        .lmove(\n            \"src\",\n            \"dst\",\n            ListDirection::Right,\n            ListDirection::Left,\n            None,\n            None,\n        )\n        .await\n        .unwrap();\n    assert_eq!(val, b\"c\".to_vec());\n\n    assert_eq!(\n        p.lrange_all(\"src\", None).await.unwrap(),\n        vec![b\"b\".to_vec()]\n    );\n    assert_eq!(\n        p.lrange_all(\"dst\", None).await.unwrap(),\n        vec![b\"c\".to_vec(), b\"a\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lmove_empty_source() {\n    let p = new_test_pool();\n    assert!(is_miss(\n        &p.lmove(\n            \"missing\",\n            \"dst\",\n            ListDirection::Left,\n            ListDirection::Right,\n            None,\n            None\n        )\n        .await\n        .unwrap_err()\n    ));\n}\n\n#[tokio::test]\nasync fn test_lmove_same_list_rotate() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n\n    // Rotate: move right to left (rpoplpush on same key).\n    let val = p\n        .lmove(\n            \"l\",\n            \"l\",\n            ListDirection::Right,\n            ListDirection::Left,\n            None,\n            None,\n        )\n        .await\n        .unwrap();\n    assert_eq!(val, b\"c\".to_vec());\n    assert_eq!(\n        p.lrange_all(\"l\", None).await.unwrap(),\n        vec![b\"c\".to_vec(), b\"a\".to_vec(), b\"b\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lmove_left_to_left() {\n    let p = new_test_pool();\n    p.rpush(\"src\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.rpush(\"dst\", &[b\"x\"], None, None).await.unwrap();\n\n    let val = p\n        .lmove(\n            \"src\",\n            \"dst\",\n            ListDirection::Left,\n            ListDirection::Left,\n            None,\n            None,\n        )\n        .await\n        .unwrap();\n    assert_eq!(val, b\"a\".to_vec());\n    assert_eq!(\n        p.lrange_all(\"dst\", None).await.unwrap(),\n        vec![b\"a\".to_vec(), b\"x\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lmove_right_to_right() {\n    let p = new_test_pool();\n    p.rpush(\"src\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.rpush(\"dst\", &[b\"x\"], None, None).await.unwrap();\n\n    let val = p\n        .lmove(\n            \"src\",\n            \"dst\",\n            ListDirection::Right,\n            ListDirection::Right,\n            None,\n            None,\n        )\n        .await\n        .unwrap();\n    assert_eq!(val, b\"b\".to_vec());\n    assert_eq!(\n        p.lrange_all(\"dst\", None).await.unwrap(),\n        vec![b\"x\".to_vec(), b\"b\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lmove_creates_destination() {\n    let p = new_test_pool();\n    p.rpush(\"src\", &[b\"a\"], None, None).await.unwrap();\n\n    p.lmove(\n        \"src\",\n        \"dst\",\n        ListDirection::Left,\n        ListDirection::Right,\n        None,\n        None,\n    )\n    .await\n    .unwrap();\n\n    assert_eq!(p.llen(\"src\", None).await.unwrap(), 0);\n    assert_eq!(\n        p.lrange_all(\"dst\", None).await.unwrap(),\n        vec![b\"a\".to_vec()]\n    );\n}\n\n#[tokio::test]\nasync fn test_lmove_with_ttl() {\n    let p = new_test_pool();\n    p.rpush(\"src\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let val = p\n        .lmove(\n            \"src\",\n            \"dst\",\n            ListDirection::Left,\n            ListDirection::Right,\n            Some(TtlOp::SetMs(100_000)),\n            None,\n        )\n        .await\n        .unwrap();\n    assert_eq!(val, b\"a\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_llen() {\n    let p = new_test_pool();\n    assert_eq!(p.llen(\"missing\", None).await.unwrap(), 0);\n\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 3);\n}\n\n#[tokio::test]\nasync fn test_llen_after_pop() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.lpop(\"l\", None, None).await.unwrap();\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_set_add_remove() {\n    let p = new_test_pool();\n\n    let added = p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    assert_eq!(added, 3);\n\n    // Adding duplicates.\n    let added = p.sadd(\"s\", &[b\"b\", b\"c\", b\"d\"], None, None).await.unwrap();\n    assert_eq!(added, 1);\n\n    let removed = p.srem(\"s\", &[b\"a\", b\"missing\"], None, None).await.unwrap();\n    assert_eq!(removed, 1);\n\n    let len = p.scard(\"s\", None).await.unwrap();\n    assert_eq!(len, 3);\n}\n\n#[tokio::test]\nasync fn test_sadd_single() {\n    let p = new_test_pool();\n    let added = p.sadd(\"s\", &[b\"x\"], None, None).await.unwrap();\n    assert_eq!(added, 1);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 1);\n}\n\n#[tokio::test]\nasync fn test_sadd_all_duplicates() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let added = p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(added, 0);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_sadd_with_ttl() {\n    let p = new_test_pool();\n    let added = p\n        .sadd(\"s\", &[b\"a\"], Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(added, 1);\n}\n\n#[tokio::test]\nasync fn test_srem_from_missing_key() {\n    let p = new_test_pool();\n    let removed = p.srem(\"missing\", &[b\"a\"], None, None).await.unwrap();\n    assert_eq!(removed, 0);\n}\n\n#[tokio::test]\nasync fn test_srem_all_members() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let removed = p.srem(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(removed, 2);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_srem_with_ttl() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    let removed = p\n        .srem(\"s\", &[b\"a\"], Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert_eq!(removed, 1);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_set_contains() {\n    let p = new_test_pool();\n\n    p.sadd(\"s\", &[b\"x\"], None, None).await.unwrap();\n\n    assert!(p.sismember(\"s\", b\"x\", None).await.unwrap());\n    assert!(!p.sismember(\"s\", b\"y\", None).await.unwrap());\n    assert!(!p.sismember(\"missing\", b\"x\", None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_sismember_after_remove() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.srem(\"s\", &[b\"a\"], None, None).await.unwrap();\n    assert!(!p.sismember(\"s\", b\"a\", None).await.unwrap());\n    assert!(p.sismember(\"s\", b\"b\", None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_set_members_len() {\n    let p = new_test_pool();\n\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n\n    let mut members = p.smembers(\"s\", None).await.unwrap();\n    members.sort();\n    assert_eq!(members, vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]);\n\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 3);\n    assert_eq!(p.scard(\"missing\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_smembers_empty() {\n    let p = new_test_pool();\n    assert_eq!(\n        p.smembers(\"missing\", None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n}\n\n#[tokio::test]\nasync fn test_smembers_single() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"only\"], None, None).await.unwrap();\n    assert_eq!(p.smembers(\"s\", None).await.unwrap(), vec![b\"only\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_spop() {\n    let p = new_test_pool();\n\n    p.sadd(\"s\", &[b\"only\"], None, None).await.unwrap();\n\n    // spop removes and returns the member.\n    let popped = p.spop(\"s\", None, None).await.unwrap();\n    assert_eq!(popped, b\"only\".to_vec());\n\n    // Empty set returns Miss.\n    let err = p.spop(\"s\", None, None).await.unwrap_err();\n    assert!(is_miss(&err));\n}\n\n#[tokio::test]\nasync fn test_spop_missing_key() {\n    let p = new_test_pool();\n    assert!(is_miss(&p.spop(\"missing\", None, None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_spop_reduces_cardinality() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    let popped = p.spop(\"s\", None, None).await.unwrap();\n    assert!(!popped.is_empty());\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 2);\n    assert!(!p.sismember(\"s\", &popped, None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_spop_n() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n\n    let popped = p.spop_n(\"s\", 2, None, None).await.unwrap();\n    assert_eq!(popped.len(), 2);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 1);\n\n    // Pop from missing key.\n    assert_eq!(\n        p.spop_n(\"missing\", 1, None, None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n}\n\n#[tokio::test]\nasync fn test_spop_n_more_than_set_size() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let popped = p.spop_n(\"s\", 10, None, None).await.unwrap();\n    assert_eq!(popped.len(), 2);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_spop_n_zero() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\"], None, None).await.unwrap();\n    let popped = p.spop_n(\"s\", 0, None, None).await.unwrap();\n    assert!(popped.is_empty());\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 1);\n}\n\n#[tokio::test]\nasync fn test_srandmember() {\n    let p = new_test_pool();\n\n    // Empty set returns Miss.\n    let err = p.srandmember(\"s\", None).await.unwrap_err();\n    assert!(is_miss(&err));\n\n    p.sadd(\"s\", &[b\"m\"], None, None).await.unwrap();\n    let sampled = p.srandmember(\"s\", None).await.unwrap();\n    assert_eq!(sampled, b\"m\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_srandmember_doesnt_remove() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\"], None, None).await.unwrap();\n    p.srandmember(\"s\", None).await.unwrap();\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 1);\n}\n\n#[tokio::test]\nasync fn test_srandmember_from_larger_set() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\", b\"d\", b\"e\"], None, None)\n        .await\n        .unwrap();\n    let sampled = p.srandmember(\"s\", None).await.unwrap();\n    assert!(p.sismember(\"s\", &sampled, None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_srandmember_n() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n\n    // Positive count — distinct members.\n    let sample = p.srandmember_n(\"s\", 2, None).await.unwrap();\n    assert_eq!(sample.len(), 2);\n    for m in &sample {\n        assert!(p.sismember(\"s\", m, None).await.unwrap());\n    }\n\n    // Positive count > set size returns at most set size.\n    let sample = p.srandmember_n(\"s\", 10, None).await.unwrap();\n    assert_eq!(sample.len(), 3);\n\n    // Empty/missing set.\n    let result = p.srandmember_n(\"missing\", 2, None).await;\n    match result {\n        Ok(v) => assert!(v.is_empty()),\n        Err(e) => assert!(is_miss(&e)),\n    }\n}\n\n#[tokio::test]\nasync fn test_srandmember_negative_count() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\"], None, None).await.unwrap();\n\n    // Negative count — allows duplicates, returns exactly abs(count).\n    let sample = p.srandmember_n(\"s\", -5, None).await.unwrap();\n    assert_eq!(sample.len(), 5);\n    for m in &sample {\n        assert_eq!(m, &b\"a\".to_vec());\n    }\n}\n\n#[tokio::test]\nasync fn test_srandmember_n_distinct() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\", b\"c\", b\"d\", b\"e\"], None, None)\n        .await\n        .unwrap();\n    let sample = p.srandmember_n(\"s\", 3, None).await.unwrap();\n    assert_eq!(sample.len(), 3);\n    // All distinct.\n    let mut sorted = sample.clone();\n    sorted.sort();\n    sorted.dedup();\n    assert_eq!(sorted.len(), 3);\n}\n\n#[tokio::test]\nasync fn test_srandmember_n_negative_from_multiple() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let sample = p.srandmember_n(\"s\", -10, None).await.unwrap();\n    assert_eq!(sample.len(), 10);\n    for m in &sample {\n        assert!(m == b\"a\" || m == b\"b\");\n    }\n}\n\n#[tokio::test]\nasync fn test_set_diff() {\n    let p = new_test_pool();\n\n    p.sadd(\"s1\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"b\", b\"c\", b\"d\"], None, None).await.unwrap();\n\n    let mut diff = p.sdiff(&[\"s1\", \"s2\"], None).await.unwrap();\n    diff.sort();\n    assert_eq!(diff, vec![b\"a\".to_vec()]);\n\n    // Diff with missing set — returns all of first set.\n    let mut diff = p.sdiff(&[\"s1\", \"missing\"], None).await.unwrap();\n    diff.sort();\n    assert_eq!(diff, vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]);\n\n    let count = p\n        .sdiffstore(\"dest\", &[\"s1\", \"s2\"], None, None)\n        .await\n        .unwrap();\n    assert_eq!(count, 1);\n\n    let mut stored = p.smembers(\"dest\", None).await.unwrap();\n    stored.sort();\n    assert_eq!(stored, vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sdiff_single_set() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let mut diff = p.sdiff(&[\"s\"], None).await.unwrap();\n    diff.sort();\n    assert_eq!(diff, vec![b\"a\".to_vec(), b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sdiff_identical_sets() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let diff = p.sdiff(&[\"s1\", \"s2\"], None).await.unwrap();\n    assert!(diff.is_empty());\n}\n\n#[tokio::test]\nasync fn test_sdiff_three_sets() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\", b\"b\", b\"c\", b\"d\"], None, None)\n        .await\n        .unwrap();\n    p.sadd(\"s2\", &[b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s3\", &[b\"c\"], None, None).await.unwrap();\n    let mut diff = p.sdiff(&[\"s1\", \"s2\", \"s3\"], None).await.unwrap();\n    diff.sort();\n    assert_eq!(diff, vec![b\"a\".to_vec(), b\"d\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sdiffstore_overwrites_destination() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"b\"], None, None).await.unwrap();\n    p.sadd(\"dest\", &[b\"old1\", b\"old2\", b\"old3\"], None, None)\n        .await\n        .unwrap();\n    let count = p\n        .sdiffstore(\"dest\", &[\"s1\", \"s2\"], None, None)\n        .await\n        .unwrap();\n    assert_eq!(count, 1);\n    assert_eq!(p.smembers(\"dest\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_set_intersect() {\n    let p = new_test_pool();\n\n    p.sadd(\"s1\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"b\", b\"c\", b\"d\"], None, None).await.unwrap();\n\n    let mut inter = p.sinter(&[\"s1\", \"s2\"], None).await.unwrap();\n    inter.sort();\n    assert_eq!(inter, vec![b\"b\".to_vec(), b\"c\".to_vec()]);\n\n    // Intersection with missing set — empty.\n    assert_eq!(\n        p.sinter(&[\"s1\", \"missing\"], None).await.unwrap(),\n        Vec::<Vec<u8>>::new()\n    );\n\n    let count = p\n        .sinterstore(\"dest\", &[\"s1\", \"s2\"], None, None)\n        .await\n        .unwrap();\n    assert_eq!(count, 2);\n\n    let mut stored = p.smembers(\"dest\", None).await.unwrap();\n    stored.sort();\n    assert_eq!(stored, vec![b\"b\".to_vec(), b\"c\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sinter_single_set() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let mut inter = p.sinter(&[\"s\"], None).await.unwrap();\n    inter.sort();\n    assert_eq!(inter, vec![b\"a\".to_vec(), b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sinter_disjoint() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"c\", b\"d\"], None, None).await.unwrap();\n    let inter = p.sinter(&[\"s1\", \"s2\"], None).await.unwrap();\n    assert!(inter.is_empty());\n}\n\n#[tokio::test]\nasync fn test_sinter_three_sets() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\", b\"b\", b\"c\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"b\", b\"c\", b\"d\"], None, None).await.unwrap();\n    p.sadd(\"s3\", &[b\"c\", b\"d\", b\"e\"], None, None).await.unwrap();\n    let inter = p.sinter(&[\"s1\", \"s2\", \"s3\"], None).await.unwrap();\n    assert_eq!(inter, vec![b\"c\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sinterstore_overwrites_destination() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"dest\", &[b\"old\"], None, None).await.unwrap();\n    let count = p\n        .sinterstore(\"dest\", &[\"s1\", \"s2\"], None, None)\n        .await\n        .unwrap();\n    assert_eq!(count, 2);\n    let mut stored = p.smembers(\"dest\", None).await.unwrap();\n    stored.sort();\n    assert_eq!(stored, vec![b\"a\".to_vec(), b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_set_union() {\n    let p = new_test_pool();\n\n    p.sadd(\"s1\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"b\", b\"c\"], None, None).await.unwrap();\n\n    let mut un = p.sunion(&[\"s1\", \"s2\"], None).await.unwrap();\n    un.sort();\n    assert_eq!(un, vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]);\n\n    let count = p\n        .sunionstore(\"dest\", &[\"s1\", \"s2\"], None, None)\n        .await\n        .unwrap();\n    assert_eq!(count, 3);\n\n    let mut stored = p.smembers(\"dest\", None).await.unwrap();\n    stored.sort();\n    assert_eq!(stored, vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sunion_single_set() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    let mut un = p.sunion(&[\"s\"], None).await.unwrap();\n    un.sort();\n    assert_eq!(un, vec![b\"a\".to_vec(), b\"b\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sunion_with_missing() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\"], None, None).await.unwrap();\n    let un = p.sunion(&[\"s1\", \"missing\"], None).await.unwrap();\n    assert_eq!(un, vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sunion_three_sets() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\"], None, None).await.unwrap();\n    p.sadd(\"s2\", &[b\"b\"], None, None).await.unwrap();\n    p.sadd(\"s3\", &[b\"c\"], None, None).await.unwrap();\n    let mut un = p.sunion(&[\"s1\", \"s2\", \"s3\"], None).await.unwrap();\n    un.sort();\n    assert_eq!(un, vec![b\"a\".to_vec(), b\"b\".to_vec(), b\"c\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_sunionstore_overwrites_destination() {\n    let p = new_test_pool();\n    p.sadd(\"s1\", &[b\"a\"], None, None).await.unwrap();\n    p.sadd(\"dest\", &[b\"old1\", b\"old2\"], None, None)\n        .await\n        .unwrap();\n    let count = p.sunionstore(\"dest\", &[\"s1\"], None, None).await.unwrap();\n    assert_eq!(count, 1);\n    assert_eq!(p.smembers(\"dest\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_set_move() {\n    let p = new_test_pool();\n\n    p.sadd(\"src\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"dst\", &[b\"c\"], None, None).await.unwrap();\n\n    let moved = p.smove(\"src\", \"dst\", b\"a\", None, None).await.unwrap();\n    assert!(moved);\n\n    assert!(!p.sismember(\"src\", b\"a\", None).await.unwrap());\n    assert!(p.sismember(\"dst\", b\"a\", None).await.unwrap());\n\n    // Moving non-existent member returns false.\n    let moved = p.smove(\"src\", \"dst\", b\"z\", None, None).await.unwrap();\n    assert!(!moved);\n}\n\n#[tokio::test]\nasync fn test_smove_missing_source() {\n    let p = new_test_pool();\n    assert!(!p.smove(\"missing\", \"dst\", b\"a\", None, None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_smove_creates_destination() {\n    let p = new_test_pool();\n    p.sadd(\"src\", &[b\"a\"], None, None).await.unwrap();\n\n    assert!(p.smove(\"src\", \"dst\", b\"a\", None, None).await.unwrap());\n    assert_eq!(p.scard(\"src\", None).await.unwrap(), 0);\n    assert_eq!(p.scard(\"dst\", None).await.unwrap(), 1);\n    assert!(p.sismember(\"dst\", b\"a\", None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_smove_member_already_in_destination() {\n    let p = new_test_pool();\n    p.sadd(\"src\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"dst\", &[b\"a\", b\"c\"], None, None).await.unwrap();\n\n    // \"a\" is in both src and dst. Move should still succeed.\n    let moved = p.smove(\"src\", \"dst\", b\"a\", None, None).await.unwrap();\n    assert!(moved);\n    assert!(!p.sismember(\"src\", b\"a\", None).await.unwrap());\n    assert!(p.sismember(\"dst\", b\"a\", None).await.unwrap());\n    // dst should still have 2 members (a was already there, not duplicated).\n    assert_eq!(p.scard(\"dst\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_smove_with_ttl() {\n    let p = new_test_pool();\n    p.sadd(\"src\", &[b\"a\"], None, None).await.unwrap();\n    let moved = p\n        .smove(\"src\", \"dst\", b\"a\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    assert!(moved);\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_string_on_list() {\n    let p = new_test_pool();\n    p.rpush(\"k\", &[b\"a\"], None, None).await.unwrap();\n    assert!(p.get(\"k\", None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_list_on_string() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"val\", None, None).await.unwrap();\n    assert!(p.lpush(\"k\", &[b\"a\"], None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_set_on_string() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"val\", None, None).await.unwrap();\n    assert!(p.sadd(\"k\", &[b\"a\"], None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_string_on_set() {\n    let p = new_test_pool();\n    p.sadd(\"k\", &[b\"a\"], None, None).await.unwrap();\n    assert!(p.get(\"k\", None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_list_on_set() {\n    let p = new_test_pool();\n    p.sadd(\"k\", &[b\"a\"], None, None).await.unwrap();\n    assert!(p.lpush(\"k\", &[b\"x\"], None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_set_on_list() {\n    let p = new_test_pool();\n    p.rpush(\"k\", &[b\"a\"], None, None).await.unwrap();\n    assert!(p.sadd(\"k\", &[b\"x\"], None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_incr_on_list() {\n    let p = new_test_pool();\n    p.rpush(\"k\", &[b\"1\"], None, None).await.unwrap();\n    assert!(p.incr_by(\"k\", 1, None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_type_mismatch_append_on_list() {\n    let p = new_test_pool();\n    p.rpush(\"k\", &[b\"a\"], None, None).await.unwrap();\n    assert!(p.append(\"k\", b\"x\", None, None).await.is_err());\n}\n\n#[tokio::test]\nasync fn test_set_with_persist_ttl() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    p.set(\"k\", b\"v2\", Some(TtlOp::Persist), None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_ttl_keep_preserves_expiry() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    p.set(\"k\", b\"v2\", Some(TtlOp::Keep), None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_replace_with_keep_ttl() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    p.replace(\"k\", b\"v2\", Some(TtlOp::Keep), None)\n        .await\n        .unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_expired_key_not_returned() {\n    let p = new_test_pool();\n    // Use a short TTL; miniredis FastForward advances 1s per real second.\n    p.set(\"k\", b\"val\", Some(TtlOp::SetMs(1)), None)\n        .await\n        .unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;\n    assert!(is_miss(&p.get(\"k\", None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_set_if_not_exists_on_expired_key() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"old\", Some(TtlOp::SetMs(1)), None)\n        .await\n        .unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;\n\n    p.set_if_not_exists(\"k\", b\"new\", None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"new\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_replace_on_expired_key_fails() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"old\", Some(TtlOp::SetMs(1)), None)\n        .await\n        .unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;\n    assert!(is_miss(\n        &p.replace(\"k\", b\"new\", None, None).await.unwrap_err()\n    ));\n}\n\n#[tokio::test]\nasync fn test_set_with_ttl_then_overwrite_without() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v1\", Some(TtlOp::SetMs(100_000)), None)\n        .await\n        .unwrap();\n    // Overwriting without TTL should remove TTL (Persist semantics).\n    p.set(\"k\", b\"v2\", Some(TtlOp::Persist), None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v2\".to_vec());\n}\n\n#[tokio::test]\nasync fn test_set_if_not_exists_with_ttl_expires() {\n    let p = new_test_pool();\n    p.set_if_not_exists(\"k\", b\"v\", Some(TtlOp::SetMs(1)), None)\n        .await\n        .unwrap();\n    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;\n    assert!(is_miss(&p.get(\"k\", None).await.unwrap_err()));\n}\n\n#[tokio::test]\nasync fn test_independent_keys_different_types() {\n    let p = new_test_pool();\n\n    p.set(\"str\", b\"hello\", None, None).await.unwrap();\n    p.rpush(\"list\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    p.sadd(\"set\", &[b\"x\", b\"y\"], None, None).await.unwrap();\n\n    // All independent operations work.\n    assert_eq!(p.get(\"str\", None).await.unwrap(), b\"hello\".to_vec());\n    assert_eq!(p.llen(\"list\", None).await.unwrap(), 2);\n    assert_eq!(p.scard(\"set\", None).await.unwrap(), 2);\n\n    // Deleting one doesn't affect others.\n    p.delete(&[\"str\"], None).await.unwrap();\n    assert_eq!(p.llen(\"list\", None).await.unwrap(), 2);\n    assert_eq!(p.scard(\"set\", None).await.unwrap(), 2);\n}\n\n#[tokio::test]\nasync fn test_delete_list_key() {\n    let p = new_test_pool();\n    p.rpush(\"l\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(p.delete(&[\"l\"], None).await.unwrap(), 1);\n    assert_eq!(p.llen(\"l\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_delete_set_key() {\n    let p = new_test_pool();\n    p.sadd(\"s\", &[b\"a\", b\"b\"], None, None).await.unwrap();\n    assert_eq!(p.delete(&[\"s\"], None).await.unwrap(), 1);\n    assert_eq!(p.scard(\"s\", None).await.unwrap(), 0);\n}\n\n#[tokio::test]\nasync fn test_delete_mixed_types() {\n    let p = new_test_pool();\n    p.set(\"str\", b\"v\", None, None).await.unwrap();\n    p.rpush(\"list\", &[b\"a\"], None, None).await.unwrap();\n    p.sadd(\"set\", &[b\"x\"], None, None).await.unwrap();\n    let deleted = p\n        .delete(&[\"str\", \"list\", \"set\", \"missing\"], None)\n        .await\n        .unwrap();\n    assert_eq!(deleted, 3);\n}\n\n#[tokio::test]\nasync fn test_reuse_key_string_to_list() {\n    let p = new_test_pool();\n    p.set(\"k\", b\"v\", None, None).await.unwrap();\n    p.delete(&[\"k\"], None).await.unwrap();\n    p.rpush(\"k\", &[b\"a\"], None, None).await.unwrap();\n    assert_eq!(p.lrange_all(\"k\", None).await.unwrap(), vec![b\"a\".to_vec()]);\n}\n\n#[tokio::test]\nasync fn test_reuse_key_list_to_set() {\n    let p = new_test_pool();\n    p.rpush(\"k\", &[b\"a\"], None, None).await.unwrap();\n    p.delete(&[\"k\"], None).await.unwrap();\n    p.sadd(\"k\", &[b\"x\"], None, None).await.unwrap();\n    assert!(p.sismember(\"k\", b\"x\", None).await.unwrap());\n}\n\n#[tokio::test]\nasync fn test_reuse_key_set_to_string() {\n    let p = new_test_pool();\n    p.sadd(\"k\", &[b\"x\"], None, None).await.unwrap();\n    p.delete(&[\"k\"], None).await.unwrap();\n    p.set(\"k\", b\"v\", None, None).await.unwrap();\n    assert_eq!(p.get(\"k\", None).await.unwrap(), b\"v\".to_vec());\n}\n"
  },
  {
    "path": "runtimes/core/src/cache/error.rs",
    "content": "use bb8_redis::redis;\nuse thiserror::Error;\n\n/// Result type for internal cache operations (without operation context).\npub type Result<T> = std::result::Result<T, Error>;\n\n/// Result type for pool operations with operation context.\npub type OpResult<T> = std::result::Result<T, OpError>;\n\n/// Error wrapper with operation context (operation name + key).\n/// Analogous to Go's `cache.OpError`.\n#[derive(Error, Debug)]\n#[error(\"cache {operation} \\\"{key}\\\": {source}\")]\npub struct OpError {\n    pub operation: &'static str,\n    pub key: String,\n    #[source]\n    pub source: Error,\n}\n\nimpl OpError {\n    pub fn new(operation: &'static str, key: &str, source: Error) -> Self {\n        Self {\n            operation,\n            key: key.to_string(),\n            source,\n        }\n    }\n}\n\n/// Error type for cache operations.\n#[derive(Error, Debug, Clone)]\npub enum Error {\n    /// Miss is the error value reported when a key is missing from the cache.\n    #[error(\"cache miss\")]\n    Miss,\n\n    /// KeyExists is the error reported when a key already exists\n    /// and the requested operation is specified to only apply to\n    /// keys that do not already exist.\n    #[error(\"key already exist\")]\n    KeyExist,\n\n    /// Redis error.\n    #[error(\"redis error: {0}\")]\n    Redis(#[from] redis::RedisError),\n\n    /// Connection pool error.\n    #[error(\"connection pool timeout\")]\n    PoolTimeout,\n}\n"
  },
  {
    "path": "runtimes/core/src/cache/manager.rs",
    "content": "use std::collections::HashMap;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse bb8_redis::redis;\nuse redis::{ConnectionAddr, IntoConnectionInfo, RedisConnectionInfo, TlsCertificates};\n\nuse crate::cache::client::Client;\nuse crate::cache::miniredis::MiniredisServer;\nuse crate::cache::noop::NoopCluster;\nuse crate::encore::runtime::v1 as pb;\nuse crate::names::EncoreName;\nuse crate::secrets;\nuse crate::trace::Tracer;\n\n/// Manager manages cache cluster connections.\npub struct Manager {\n    clusters: Arc<HashMap<EncoreName, Arc<ClusterImpl>>>,\n    /// Miniredis-backed cluster for testing and in-memory fallback.\n    miniredis_cluster: Option<Arc<ClusterImpl>>,\n    /// Keeps the in-process miniredis server alive for the lifetime of the Manager.\n    _miniredis: Option<MiniredisServer>,\n}\n\n/// Configuration for creating a Manager.\npub struct ManagerConfig<'a> {\n    pub clusters: Vec<pb::RedisCluster>,\n    pub creds: &'a pb::infrastructure::Credentials,\n    pub secrets: &'a secrets::Manager,\n    pub tracer: Tracer,\n    pub testing: bool,\n    pub runtime: tokio::runtime::Handle,\n}\n\nimpl ManagerConfig<'_> {\n    pub fn build(self) -> anyhow::Result<Manager> {\n        // Use miniredis for testing or when any cluster has in_memory set.\n        let needs_miniredis = self.testing || self.clusters.iter().any(|c| c.in_memory);\n\n        let clusters =\n            clusters_from_cfg(self.clusters, self.creds, self.secrets, self.tracer.clone())\n                .context(\"failed to parse Redis clusters\")?;\n\n        let (miniredis_cluster, miniredis) = if needs_miniredis {\n            log::debug!(\"cache: starting in-process miniredis server\");\n            let server = self\n                .runtime\n                .block_on(MiniredisServer::start())\n                .context(\"failed to start miniredis server\")?;\n            let url = format!(\"redis://{}\", server.addr());\n            let client = redis::Client::open(url).context(\"failed to create miniredis client\")?;\n            let cluster = Arc::new(ClusterImpl::new(\n                EncoreName::from(\"miniredis\".to_string()),\n                client,\n                None, // no key prefix — matches Go runtime behavior\n                self.tracer.clone(),\n                0,  // min_conns\n                10, // max_conns\n            ));\n\n            (Some(cluster), Some(server))\n        } else {\n            (None, None)\n        };\n\n        Ok(Manager {\n            clusters: Arc::new(clusters),\n            miniredis_cluster,\n            _miniredis: miniredis,\n        })\n    }\n}\n\nimpl Manager {\n    /// Returns a cluster by name.\n    /// If the cluster is not configured and miniredis is available (testing or in-memory mode),\n    /// returns the miniredis-backed cluster. Otherwise, returns a NoopCluster\n    /// that errors on all operations.\n    pub fn cluster(&self, name: &EncoreName) -> Arc<dyn Cluster> {\n        match self.clusters.get(name) {\n            Some(cluster) => cluster.clone(),\n            None => {\n                // If we have a miniredis cluster (testing or in-memory),\n                // use it as a fallback for unconfigured clusters.\n                if let Some(cluster) = &self.miniredis_cluster {\n                    log::debug!(\n                        \"cache: using miniredis fallback for unconfigured cluster {}\",\n                        name\n                    );\n                    cluster.clone()\n                } else {\n                    Arc::new(NoopCluster::new(name.clone()))\n                }\n            }\n        }\n    }\n}\n\n/// Trait representing a cache cluster.\npub trait Cluster: Send + Sync {\n    /// Returns the name of the cluster.\n    fn name(&self) -> &EncoreName;\n\n    /// Creates a new cache client for this cluster.\n    fn client(&self) -> anyhow::Result<Client>;\n}\n\n/// Implementation of a configured cache cluster.\npub struct ClusterImpl {\n    name: EncoreName,\n    client: redis::Client,\n    key_prefix: Option<String>,\n    tracer: Tracer,\n    min_conns: u32,\n    max_conns: u32,\n}\n\nimpl ClusterImpl {\n    fn new(\n        name: EncoreName,\n        client: redis::Client,\n        key_prefix: Option<String>,\n        tracer: Tracer,\n        min_conns: u32,\n        max_conns: u32,\n    ) -> Self {\n        Self {\n            name,\n            client,\n            key_prefix,\n            tracer,\n            min_conns,\n            max_conns,\n        }\n    }\n}\n\nimpl Cluster for ClusterImpl {\n    fn name(&self) -> &EncoreName {\n        &self.name\n    }\n\n    fn client(&self) -> anyhow::Result<Client> {\n        Client::new(\n            self.client.clone(),\n            self.key_prefix.clone(),\n            self.tracer.clone(),\n            self.min_conns,\n            self.max_conns,\n        )\n    }\n}\n\n/// Builds cluster configurations from proto config.\nfn clusters_from_cfg(\n    clusters: Vec<pb::RedisCluster>,\n    creds: &pb::infrastructure::Credentials,\n    secrets: &secrets::Manager,\n    tracer: Tracer,\n) -> anyhow::Result<HashMap<EncoreName, Arc<ClusterImpl>>> {\n    let mut result = HashMap::new();\n\n    // Build role lookup\n    let roles: HashMap<&str, &pb::RedisRole> = creds\n        .redis_roles\n        .iter()\n        .map(|r| (r.rid.as_str(), r))\n        .collect();\n\n    for cluster in clusters {\n        // Skip in-memory clusters; they'll use the miniredis fallback.\n        if cluster.in_memory {\n            continue;\n        }\n\n        // Get the primary server\n        let server = cluster\n            .servers\n            .iter()\n            .find(|s| s.kind() == pb::ServerKind::Primary);\n\n        let Some(server) = server else {\n            log::warn!(\n                \"no primary server found for Redis cluster {}, skipping\",\n                cluster.rid\n            );\n            continue;\n        };\n\n        // Process each database in the cluster\n        for db in &cluster.databases {\n            // Get the read-write pool for this db\n            let Some(pool) = db.conn_pools.iter().find(|p| !p.is_readonly) else {\n                log::warn!(\n                    \"no read-write pool found for Redis database {}, skipping\",\n                    db.encore_name\n                );\n                continue;\n            };\n\n            // Get the role to authenticate with\n            let role = roles.get(pool.role_rid.as_str()).with_context(|| {\n                format!(\n                    \"no role found with rid {} for Redis database {}\",\n                    pool.role_rid, db.encore_name\n                )\n            })?;\n\n            // Build connection info and client\n            let client = build_redis_client(server, db, role, secrets)?;\n\n            let name: EncoreName = db.encore_name.clone().into();\n            result.insert(\n                name.clone(),\n                Arc::new(ClusterImpl::new(\n                    name,\n                    client,\n                    db.key_prefix.clone(),\n                    tracer.clone(),\n                    pool.min_connections as u32,\n                    pool.max_connections as u32,\n                )),\n            );\n        }\n    }\n\n    Ok(result)\n}\n\n/// Builds a Redis client with proper TLS configuration.\nfn build_redis_client(\n    server: &pb::RedisServer,\n    db: &pb::RedisDatabase,\n    role: &pb::RedisRole,\n    secrets: &secrets::Manager,\n) -> anyhow::Result<redis::Client> {\n    use pb::redis_role::Auth;\n\n    // Parse host and port\n    let (host, port) = if server.host.starts_with('/') {\n        // Unix socket - use URL-based connection\n        let url = build_unix_socket_url(&server.host, db.database_idx, role, secrets)?;\n        return redis::Client::open(url).context(\"failed to create Redis client\");\n    } else if let Some((h, p)) = server.host.split_once(':') {\n        (h.to_string(), p.parse::<u16>().context(\"invalid port\")?)\n    } else {\n        (server.host.clone(), 6379)\n    };\n\n    let (username, password) = match &role.auth {\n        Some(Auth::AuthString(secret_data)) => {\n            let password = secrets.load(secret_data.clone());\n            let password = password\n                .get()\n                .context(\"failed to resolve Redis auth string\")?;\n            let password_str = std::str::from_utf8(password).context(\"invalid auth string\")?;\n            // Trim whitespace/newlines that might be in the secret\n            let password_str = password_str.trim().to_string();\n            (None, Some(password_str))\n        }\n        Some(Auth::Acl(acl)) => {\n            let password = acl\n                .password\n                .as_ref()\n                .context(\"ACL auth requires password\")?;\n            let password = secrets.load(password.clone());\n            let password = password.get().context(\"failed to resolve Redis password\")?;\n            let password_str = std::str::from_utf8(password).context(\"invalid password\")?;\n            let password_str = password_str.trim().to_string();\n            // If username is empty, treat it as password-only auth (like AuthString)\n            let username = if acl.username.is_empty() {\n                None\n            } else {\n                Some(acl.username.clone())\n            };\n            (username, Some(password_str))\n        }\n        None => (None, None),\n    };\n\n    // Build connection address based on TLS config\n    let mut addr = if let Some(tls_config) = &server.tls_config {\n        // TLS enabled - check for insecure mode\n        let insecure = tls_config.disable_ca_validation;\n        ConnectionAddr::TcpTls {\n            host,\n            port,\n            insecure,\n            tls_params: None, // TLS params will be set via build_with_tls\n        }\n    } else {\n        // No TLS\n        ConnectionAddr::Tcp(host, port)\n    };\n\n    // Handle hostname verification separately from CA validation\n    if let Some(tls_config) = &server.tls_config {\n        if tls_config.disable_tls_hostname_verification {\n            addr.set_danger_accept_invalid_hostnames(true);\n        }\n    }\n\n    let mut redis_info = RedisConnectionInfo::default().set_db(db.database_idx as i64);\n    if let Some(user) = username {\n        redis_info = redis_info.set_username(user);\n    }\n    if let Some(pass) = password {\n        redis_info = redis_info.set_password(pass);\n    }\n\n    // Build connection info using builder pattern\n    let conn_info = addr\n        .into_connection_info()\n        .context(\"failed to create connection info\")?\n        .set_redis_settings(redis_info);\n\n    // Create client with or without TLS certificates\n    if let Some(tls_config) = &server.tls_config {\n        // Build TLS certificates config\n        let root_cert = tls_config\n            .server_ca_cert\n            .as_ref()\n            .map(|cert| cert.as_bytes().to_vec());\n\n        let tls_certs = TlsCertificates {\n            client_tls: None, // No client cert support yet\n            root_cert,\n        };\n\n        redis::Client::build_with_tls(conn_info, tls_certs)\n            .context(\"failed to create Redis client with TLS\")\n    } else {\n        redis::Client::open(conn_info).context(\"failed to create Redis client\")\n    }\n}\n\n/// Builds a Unix socket connection URL.\nfn build_unix_socket_url(\n    socket_path: &str,\n    db_idx: i32,\n    role: &pb::RedisRole,\n    secrets: &secrets::Manager,\n) -> anyhow::Result<String> {\n    use pb::redis_role::Auth;\n\n    // Build auth portion for query string\n    let auth_params = match &role.auth {\n        Some(Auth::AuthString(secret_data)) => {\n            let password = secrets.load(secret_data.clone());\n            let password = password\n                .get()\n                .context(\"failed to resolve Redis auth string\")?;\n            let password_str = std::str::from_utf8(password).context(\"invalid auth string\")?;\n            format!(\"&password={}\", urlencoding::encode(password_str))\n        }\n        Some(Auth::Acl(acl)) => {\n            let password = acl\n                .password\n                .as_ref()\n                .context(\"ACL auth requires password\")?;\n            let password = secrets.load(password.clone());\n            let password = password.get().context(\"failed to resolve Redis password\")?;\n            let password_str = std::str::from_utf8(password).context(\"invalid password\")?;\n            format!(\n                \"&username={}&password={}\",\n                urlencoding::encode(&acl.username),\n                urlencoding::encode(password_str)\n            )\n        }\n        None => String::new(),\n    };\n\n    Ok(format!(\n        \"redis+unix://{}?db={db_idx}{auth_params}\",\n        socket_path\n    ))\n}\n"
  },
  {
    "path": "runtimes/core/src/cache/miniredis.rs",
    "content": "use std::net::SocketAddr;\nuse std::time::Duration;\n\nuse miniredis_rs::Miniredis;\n\n/// An in-process miniredis server with a background cleanup task\n/// that fast-forwards time and prunes excess keys (matching the\n/// behavior of the old Go miniredis-encore binary).\n///\n/// The server and cleanup task run as tokio tasks and will shut\n/// down when the tokio runtime is dropped.\npub struct MiniredisServer {\n    server: Miniredis,\n    addr: SocketAddr,\n}\n\nimpl MiniredisServer {\n    /// Start a new in-process miniredis server on a random port.\n    ///\n    /// Also spawns a background cleanup task that fast-forwards\n    /// time by 1s every second and prunes keys above 100 every 15s.\n    pub async fn start() -> anyhow::Result<Self> {\n        let server = Miniredis::run()\n            .await\n            .map_err(|e| anyhow::anyhow!(\"failed to start miniredis: {}\", e))?;\n        let addr = server.addr();\n\n        // Spawn the cleanup task on the current runtime.\n        tokio::spawn(cleanup_task(server.clone()));\n\n        Ok(Self { server, addr })\n    }\n\n    /// Returns the bound address of the miniredis server.\n    pub fn addr(&self) -> SocketAddr {\n        self.addr\n    }\n\n    /// Returns a reference to the underlying miniredis server.\n    pub fn server(&self) -> &Miniredis {\n        &self.server\n    }\n}\n\n/// Background task that fast-forwards miniredis time and periodically prunes\n/// excess keys, matching the Go binary's doCleanup behavior.\nasync fn cleanup_task(server: Miniredis) {\n    let mut interval = tokio::time::interval(Duration::from_secs(1));\n    let mut acc = Duration::ZERO;\n    loop {\n        interval.tick().await;\n        server.fast_forward(Duration::from_secs(1));\n        acc += Duration::from_secs(1);\n        if acc >= Duration::from_secs(15) {\n            acc = Duration::ZERO;\n            // Prune to 100 keys.\n            let keys = server.keys();\n            if keys.len() > 100 {\n                let to_delete = keys.len() - 100;\n                for key in keys.iter().take(to_delete) {\n                    server.del(key);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/cache/mod.rs",
    "content": "mod client;\nmod error;\nmod manager;\npub mod miniredis;\nmod noop;\nmod tracer;\n\npub use client::{Client, ListDirection, TtlOp};\npub use error::{Error, OpError, OpResult, Result};\npub use manager::{Cluster, ClusterImpl, Manager, ManagerConfig};\n"
  },
  {
    "path": "runtimes/core/src/cache/noop.rs",
    "content": "use crate::cache::client::Client;\nuse crate::cache::manager::Cluster;\nuse crate::names::EncoreName;\n\n/// NoopCluster is returned when a cache cluster is not configured.\n/// All operations on it will return an error immediately.\npub struct NoopCluster {\n    name: EncoreName,\n}\n\nimpl NoopCluster {\n    pub fn new(name: EncoreName) -> Self {\n        Self { name }\n    }\n}\n\nimpl Cluster for NoopCluster {\n    fn name(&self) -> &EncoreName {\n        &self.name\n    }\n\n    fn client(&self) -> anyhow::Result<Client> {\n        anyhow::bail!(\"cache: this service is not configured to use this cache cluster\")\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/cache/tracer.rs",
    "content": "use std::future::Future;\n\nuse crate::{\n    cache::{error::Error, OpError, OpResult},\n    model::Request,\n    trace::{\n        protocol::{self, CacheCallStartData, CacheOpResult},\n        Tracer,\n    },\n};\n\npub(crate) struct CacheTracer(Tracer);\n\nimpl CacheTracer {\n    pub(crate) fn new(inner: Tracer) -> Self {\n        Self(inner)\n    }\n\n    pub(crate) async fn trace<'a, T, F, Fut>(\n        &self,\n        source: Option<&'a Request>,\n        operation: &'static str,\n        is_write: bool,\n        keys: &'a [&'a str],\n        f: F,\n    ) -> OpResult<T>\n    where\n        F: FnOnce() -> Fut,\n        Fut: Future<Output = crate::cache::Result<T>>,\n    {\n        let traced = if let Some(source) = source {\n            let start_id = self.0.cache_call_start(CacheCallStartData {\n                source,\n                operation,\n                is_write,\n                keys,\n            });\n            Some((start_id, source))\n        } else {\n            None\n        };\n\n        let result = match f().await {\n            Ok(value) => Ok(value),\n            Err(err) => Err(OpError::new(\n                operation,\n                keys.first().copied().unwrap_or(\"\"),\n                err.clone(),\n            )),\n        };\n\n        if let Some((start_id, source)) = traced {\n            let (cache_op_result, error) = match result.as_ref() {\n                Ok(_) => (CacheOpResult::Ok, None),\n                Err(err) => match &err.source {\n                    Error::Miss => (CacheOpResult::NoSuchKey, None),\n                    Error::KeyExist => (CacheOpResult::Conflict, None),\n                    _ => (CacheOpResult::Err, Some(err)),\n                },\n            };\n\n            self.0.cache_call_end(protocol::CacheCallEndData {\n                start_id,\n                source,\n                result: cache_op_result,\n                error,\n            });\n        }\n\n        result\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/error/conversions.rs",
    "content": "use crate::error::AppError;\n\nimpl From<&str> for AppError {\n    fn from(message: &str) -> Self {\n        AppError::new(message)\n    }\n}\nimpl From<String> for AppError {\n    fn from(message: String) -> Self {\n        AppError::new(message)\n    }\n}\nimpl From<anyhow::Error> for AppError {\n    fn from(error: anyhow::Error) -> Self {\n        let message = error.to_string();\n\n        // Create a chain of causes\n        let mut cause: Option<Box<AppError>> = None;\n        while let Some(err) = error.chain().next_back() {\n            cause = Some(Box::new(Self {\n                message: err.to_string(),\n                stack: vec![],\n                cause,\n            }));\n        }\n\n        Self {\n            message,\n            stack: vec![],\n            cause,\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/error/mod.rs",
    "content": "mod conversions;\n\nuse backtrace::SymbolName;\nuse colored::Colorize;\nuse serde::{Deserialize, Serialize};\nuse std::fmt::Display;\n\n/// AppError represents an error that occurred in the application langauge\n/// (i.e. in TypeScript code, not the rust runtime).\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct AppError {\n    /// The error message\n    pub message: String,\n\n    /// The stack trace\n    pub stack: StackTrace,\n\n    /// The cause of the error (if any)\n    pub cause: Option<Box<AppError>>,\n}\n\nimpl AppError {\n    /// Create a new AppError with the given message.\n    ///\n    /// Note: The stack trace will be set to the rust stack trace at the time this function is called.\n    /// If you want to set the stack trace to something else, use `AppError::with_stack`.\n    #[track_caller]\n    pub fn new<S: Into<String>>(message: S) -> Self {\n        Self {\n            message: message.into(),\n            stack: capture_stack_trace(),\n            cause: None,\n        }\n    }\n\n    /// Wrap an existing error with a new message.\n    ///\n    /// Note: The stack trace will be set to the rust stack trace at the time this function is called.\n    /// If you want to set the stack trace to something else, use `AppError::with_stack`.\n    #[track_caller]\n    pub fn wrap<Err: Into<AppError>, S: Into<String>>(error: Err, message: S) -> Self {\n        Self {\n            message: message.into(),\n            stack: capture_stack_trace(),\n            cause: Some(Box::new(error.into())),\n        }\n    }\n\n    /// Updates the stack trace of the error to the given stack trace\n    pub fn with_stack(self, stack: StackTrace) -> Self {\n        Self { stack, ..self }\n    }\n\n    /// Updates the cause of the error to the given error\n    pub fn with_cause<Err: Into<AppError>>(self, cause: Err) -> Self {\n        Self {\n            cause: Some(Box::new(cause.into())),\n            ..self\n        }\n    }\n\n    /// Trims the stack trace to remove any frames from before\n    /// the given file and line number.\n    ///\n    /// This is useful for removing frames caused by a conversion into an AppError.\n    /// If the given file and line number are not found in the stack trace, then the original\n    /// stack trace is returned.\n    pub fn trim_stack(self, file: &str, line: u32, drop_extra: usize) -> Self {\n        let idx = self\n            .stack\n            .iter()\n            .position(|frame| frame.file == file && frame.line == line)\n            .map(|idx| idx + 1 + drop_extra); // + 1 for the frame we called this on\n\n        match idx {\n            Some(idx) => Self {\n                stack: self.stack.into_iter().skip(idx).collect(),\n                ..self\n            },\n            None => self,\n        }\n    }\n}\n\nconst MAX_FRAMES_TO_DISPLAY: usize = 6;\nconst STACK_TAB_SIZE: &str = \"  \";\n\n/// Write the stack trace to the given formatter.\npub fn write_stack_trace<W: std::fmt::Write>(stack: &StackTrace, f: &mut W) -> std::fmt::Result {\n    // If we have a stack trace add it\n    if !stack.is_empty() {\n        write!(f, \"\\n{}{}\", STACK_TAB_SIZE, \"Stack:\".magenta())?;\n\n        // What's the longest function name including module name (for modules not named \"main\")\n        let mut longest_func = 0;\n        for frame in stack.iter().take(MAX_FRAMES_TO_DISPLAY + 1) {\n            if let Some(func) = &frame.function {\n                longest_func = longest_func.max(\n                    func.len() + // Function name length\n                        frame.module.as_ref().map(|s| s.len() + 1).unwrap_or(0), // plus \"[module].\" if module is present\n                );\n            }\n        }\n\n        for (index, frame) in stack.iter().enumerate() {\n            let (module_and_function, readable_len) = match (&frame.module, &frame.function) {\n                (Some(module), Some(function)) => (\n                    format!(\"{}.{}\", module.bright_black(), function.magenta()),\n                    module.len() + function.len() + 1,\n                ),\n                (None, Some(function)) => (format!(\"{}\", function.magenta()), function.len()),\n                (Some(_), None) => (\"\".to_string(), 0),\n                (None, None) => (\"\".to_string(), 0),\n            };\n\n            let spacing_after_function = if longest_func >= readable_len {\n                \" \".repeat(longest_func - readable_len)\n            } else {\n                \"\".to_string()\n            };\n\n            let line_and_column = frame\n                .column\n                .map(|col| format!(\"{}:{}\", frame.line, col))\n                .unwrap_or(format!(\"{}\", frame.line));\n\n            write!(\n                f,\n                \"\\n{}{}at {}{} {}:{}\",\n                STACK_TAB_SIZE,\n                STACK_TAB_SIZE,\n                module_and_function,\n                spacing_after_function,\n                frame.file,\n                line_and_column\n            )?;\n\n            // Only print the first 6 frames\n            if index >= MAX_FRAMES_TO_DISPLAY {\n                write!(\n                    f,\n                    \"\\n{}{}... remaining {} frames omitted...\",\n                    STACK_TAB_SIZE,\n                    STACK_TAB_SIZE,\n                    stack.len() - index\n                )?;\n                break;\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Display the error in a human readable format.\nimpl Display for AppError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"{} {}\",\n            \"Error:\".red(),\n            self.message.replace('\\n', \"\\n\\t\")\n        )?;\n        write_stack_trace(&self.stack, f)?;\n\n        // While there are more causes, print them\n        let mut cause = &self.cause;\n        while cause.is_some() {\n            let error = cause.as_ref().expect(\"cause should not be None\");\n\n            write!(f, \"\\n\\n\\t{} {}\", \"Caused by:\".red(), error.message)?;\n            write_stack_trace(&error.stack, f)?;\n            cause = &error.cause;\n        }\n\n        Ok(())\n    }\n}\n\n/// A stack trace from an error.\npub type StackTrace = Vec<StackFrame>;\n\n/// A stack frame in a backtrace from an error.\n///\n/// Note: The serde field names, match those used in our Go `errinsrc` package.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct StackFrame {\n    /// The name of the file on the file system\n    #[serde(rename = \"file\")]\n    pub file: String,\n\n    /// The line number in that file\n    #[serde(rename = \"line\")]\n    pub line: u32,\n\n    /// The column number in that file (if available)\n    #[serde(skip_serializing)]\n    pub column: Option<u32>,\n\n    /// The name of the module containing the line (if available)\n    #[serde(skip_serializing)]\n    pub module: Option<String>,\n\n    /// The name of the function or method containing the line (if available)\n    #[serde(rename = \"func\")]\n    pub function: Option<String>,\n}\n\n/// capture_stack_trace captures a stack trace from the current location in the rust code base\n#[track_caller]\nfn capture_stack_trace() -> StackTrace {\n    let caller = std::panic::Location::caller();\n\n    let backtrace = backtrace::Backtrace::new();\n\n    let stacktrace: StackTrace = convert_backtrace_to_stack_trace(&backtrace)\n        .into_iter()\n        .skip_while(|frame| {\n            // Skip all the frames before the caller\n            !(frame.file.ends_with(caller.file()) && frame.line == caller.line())\n        })\n        .collect();\n\n    // If the backtrace is empty, then we just return the caller which thanks to the\n    // track_caller macro means we will always have at least one frame to report\n    if stacktrace.is_empty() {\n        vec![StackFrame {\n            file: caller.file().to_string(),\n            line: caller.line(),\n            column: None,\n            module: None,\n            function: None,\n        }]\n    } else {\n        // otherwise we can report the full trace\n        stacktrace\n    }\n}\n\n/// convert_backtrace_to_stack_trace converts a rust backtrace to a stack trace\nfn convert_backtrace_to_stack_trace(backtrace: &backtrace::Backtrace) -> StackTrace {\n    let mut stack_trace = Vec::new();\n\n    for frame in backtrace.frames() {\n        if let Some(symbol) = frame.symbols().first() {\n            match (symbol.filename(), symbol.lineno()) {\n                (Some(filename), Some(line)) => {\n                    let (module, function) = split_symbol_into_module_function(symbol.name());\n\n                    let frame = StackFrame {\n                        file: trim_file_path(filename.to_string_lossy().to_string()),\n                        line,\n                        column: symbol.colno(),\n                        module,\n                        function,\n                    };\n\n                    if !is_common_rust_frame(&frame) {\n                        stack_trace.push(frame);\n                    }\n                }\n                _ => continue,\n            }\n        }\n    }\n\n    stack_trace\n}\n\nfn split_symbol_into_module_function(\n    symbol: Option<SymbolName>,\n) -> (Option<String>, Option<String>) {\n    match symbol {\n        Some(symbol) => {\n            let symbol_str = symbol.to_string();\n            let parts: Vec<&str> = symbol_str.split(\"::\").collect();\n            if parts.len() < 3 {\n                return (None, Some(symbol.to_string()));\n            }\n\n            let function_idx = parts\n                .iter()\n                .rposition(|s| s.starts_with(|c: char| c.is_uppercase()))\n                .unwrap_or_else(|| {\n                    parts\n                        .iter()\n                        .rposition(|s| (*s).eq(\"{{closure}}\"))\n                        .map(|idx| idx - 1)\n                        .unwrap_or_else(|| parts.len() - 2)\n                });\n\n            let module = parts[..function_idx].join(\"::\");\n            let function = parts[function_idx..parts.len() - 1].join(\"::\");\n\n            (Some(module), Some(function))\n        }\n        None => (None, None),\n    }\n}\n\n/// trim_file_path trims the file path to be relative to the root of the binary being compiled\nfn trim_file_path(full_path: String) -> String {\n    let compile_target = env!(\"ENCORE_BINARY_SRC_PATH\");\n\n    full_path\n        .strip_prefix(compile_target)\n        .map(|str| str[1..].to_string())\n        .unwrap_or(full_path)\n        .to_string()\n}\n\nfn is_common_rust_frame(frame: &StackFrame) -> bool {\n    frame.file.starts_with(\"/rustc/\")\n}\n"
  },
  {
    "path": "runtimes/core/src/infracfg.rs",
    "content": "use crate::encore::runtime::v1::infrastructure::{Credentials, Resources};\nuse crate::encore::runtime::v1::{\n    self as pbruntime, environment, gateway, metrics_provider, pub_sub_cluster,\n    pub_sub_subscription, pub_sub_topic, redis_role, secret_data, service_auth, service_discovery,\n    AppSecret, Deployment, Environment, Infrastructure, MetricsProvider, Observability,\n    PubSubCluster, PubSubSubscription, PubSubTopic, RedisCluster, RedisConnectionPool,\n    RedisDatabase, RedisRole, RedisServer, RuntimeConfig, SqlCluster, SqlConnectionPool,\n    SqlDatabase, SqlRole, SqlServer, TlsConfig,\n};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct InfraConfig {\n    pub metadata: Option<Metadata>,\n    pub graceful_shutdown: Option<GracefulShutdown>,\n    pub auth: Option<Vec<Auth>>,\n    pub service_discovery: Option<HashMap<String, ServiceDiscovery>>,\n    pub metrics: Option<Metrics>,\n    pub used_metrics: Option<Vec<Metric>>,\n    pub sql_servers: Option<Vec<SQLServer>>,\n    pub redis: Option<HashMap<String, Redis>>,\n    pub pubsub: Option<Vec<PubSub>>,\n    pub secrets: Option<Secrets>,\n    pub hosted_services: Option<Vec<String>>,\n    pub hosted_gateways: Option<Vec<String>>,\n    pub cors: Option<CORS>,\n    pub object_storage: Option<Vec<ObjectStorage>>,\n    pub worker_threads: Option<i32>,\n    pub log_config: Option<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(tag = \"type\")]\npub enum ObjectStorage {\n    #[serde(rename = \"gcs\")]\n    GCS(GCS),\n    #[serde(rename = \"s3\")]\n    S3(S3),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct GCS {\n    pub endpoint: Option<String>,\n    pub buckets: HashMap<String, Bucket>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct S3 {\n    pub region: String,\n    pub endpoint: Option<String>,\n    pub access_key_id: Option<String>,\n    pub secret_access_key: Option<EnvString>,\n    pub buckets: HashMap<String, Bucket>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Bucket {\n    pub name: String,\n    pub key_prefix: Option<String>,\n    pub public_base_url: Option<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize, Default)]\npub struct Metadata {\n    pub app_id: Option<String>,\n    pub env_name: Option<String>,\n    pub env_type: Option<String>,\n    pub cloud: Option<String>,\n    pub base_url: Option<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct CORS {\n    pub debug: Option<bool>,\n    pub allow_headers: Option<Vec<String>>,\n    pub expose_headers: Option<Vec<String>>,\n    pub allow_origins_without_credentials: Option<Vec<String>>,\n    pub allow_origins_with_credentials: Option<Vec<String>>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct GracefulShutdown {\n    pub total: Option<i32>,\n\n    pub shutdown_hooks: Option<i32>,\n\n    pub handlers: Option<i32>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(tag = \"type\")]\npub enum Auth {\n    #[serde(rename = \"key\")]\n    Key(KeyAuth),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct KeyAuth {\n    pub id: i32,\n    pub key: EnvString,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct ServiceDiscovery {\n    pub base_url: String,\n\n    pub auth: Option<Vec<Auth>>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(tag = \"type\")]\npub enum Metrics {\n    #[serde(rename = \"prometheus\")]\n    Prometheus(PrometheusMetrics),\n    #[serde(rename = \"datadog\")]\n    Datadog(DatadogMetrics),\n    #[serde(rename = \"gcp_cloud_monitoring\")]\n    GCPCloudMonitoring(GCPCloudMonitoringMetrics),\n    #[serde(rename = \"aws_cloudwatch\")]\n    AWSCloudWatch(AWSCloudWatchMetrics),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct PrometheusMetrics {\n    pub collection_interval: Option<i32>,\n    pub remote_write_url: EnvString,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct DatadogMetrics {\n    pub collection_interval: Option<i32>,\n    pub site: String,\n    pub api_key: EnvString,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct GCPCloudMonitoringMetrics {\n    pub collection_interval: Option<i32>,\n    pub project_id: String,\n    pub monitored_resource_type: String,\n    pub monitored_resource_labels: Option<HashMap<String, String>>,\n    pub metric_names: Option<HashMap<String, String>>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct AWSCloudWatchMetrics {\n    pub collection_interval: Option<i32>,\n    pub namespace: String,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Metric {\n    name: String,\n    services: Vec<String>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum Secrets {\n    Map(HashMap<String, EnvString>),\n    EnvRef(EnvRef),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct EnvRef {\n    #[serde(rename = \"$env\")]\n    pub env: String,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum EnvString {\n    String(String),\n    EnvRef(EnvRef),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct SQLServer {\n    pub host: String,\n    pub tls_config: Option<TLSConfig>,\n    pub databases: HashMap<String, SQLDatabase>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct TLSConfig {\n    #[serde(default)]\n    pub disabled: bool,\n    pub ca: Option<String>,\n    pub client_cert: Option<ClientCert>,\n    #[serde(default)]\n    pub disable_tls_hostname_verification: bool,\n    #[serde(default)]\n    pub disable_ca_validation: bool,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct SQLDatabase {\n    pub name: Option<String>,\n    pub max_connections: Option<i32>,\n    pub min_connections: Option<i32>,\n    pub username: String,\n    pub password: EnvString,\n    pub client_cert: Option<ClientCert>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Redis {\n    pub host: String,\n    pub database_index: i32,\n\n    pub auth: Option<RedisAuth>,\n\n    pub key_prefix: Option<String>,\n\n    pub tls_config: Option<TLSConfig>,\n\n    pub max_connections: Option<i32>,\n\n    pub min_connections: Option<i32>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct RedisAuth {\n    pub r#type: String,\n\n    pub username: Option<String>,\n\n    pub password: Option<EnvString>,\n\n    pub auth_string: Option<EnvString>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct ClientCert {\n    pub cert: String,\n    pub key: EnvString,\n}\n\n// PubSub-related structures\n\n#[derive(Debug, Serialize, Deserialize)]\n#[serde(tag = \"type\")]\npub enum PubSub {\n    #[serde(rename = \"gcp_pubsub\")]\n    GCPPubsub(GCPPubsub),\n    #[serde(rename = \"aws_sns_sqs\")]\n    AWSSnsSqs(AWSSnsSqs),\n    #[serde(rename = \"nsq\")]\n    NSQ(NSQPubsub),\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct GCPPubsub {\n    pub project_id: String,\n    pub topics: HashMap<String, GCPTopic>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct GCPTopic {\n    pub name: String,\n    pub project_id: Option<String>,\n    #[serde(skip_serializing_if = \"HashMap::is_empty\", default)]\n    pub subscriptions: HashMap<String, GCPSub>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct GCPSub {\n    pub name: String,\n\n    pub project_id: Option<String>,\n\n    pub push_config: Option<PushConfig>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct PushConfig {\n    pub service_account: String,\n    pub jwt_audience: String,\n    pub id: String,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct AWSSnsSqs {\n    pub topics: HashMap<String, AWSTopic>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct AWSTopic {\n    pub arn: String,\n    #[serde(skip_serializing_if = \"HashMap::is_empty\", default)]\n    pub subscriptions: HashMap<String, AWSSub>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct AWSSub {\n    pub url: String,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct NSQPubsub {\n    pub hosts: String,\n    pub topics: HashMap<String, NSQTopic>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct NSQTopic {\n    pub name: String,\n    #[serde(skip_serializing_if = \"HashMap::is_empty\", default)]\n    pub subscriptions: HashMap<String, NSQSub>,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct NSQSub {\n    pub name: String,\n}\n\npub fn map_infra_to_runtime(infra: InfraConfig) -> RuntimeConfig {\n    let mut next_rid = 0;\n    let mut get_next_rid = || {\n        let rid = next_rid;\n        next_rid += 1;\n        rid.to_string()\n    };\n\n    let metadata = infra.metadata.unwrap_or_default();\n    // Map the Environment\n    let environment = Some(Environment {\n        app_id: \"\".to_string(),\n        app_slug: metadata.app_id.unwrap_or_default(),\n        env_id: \"\".to_string(),\n        env_name: metadata.env_name.unwrap_or_default(),\n        env_type: metadata\n            .env_type\n            .as_ref()\n            .map(|t| match t.as_str() {\n                \"development\" => environment::Type::Development as i32,\n                \"production\" => environment::Type::Production as i32,\n                \"ephemeral\" => environment::Type::Ephemeral as i32,\n                \"test\" => environment::Type::Test as i32,\n                _ => environment::Type::Unspecified as i32,\n            })\n            .unwrap_or(environment::Type::Unspecified as i32),\n        cloud: metadata\n            .cloud\n            .as_ref()\n            .map(|c| match c.as_str() {\n                \"local\" => environment::Cloud::Local as i32,\n                \"encore\" => environment::Cloud::Encore as i32,\n                \"aws\" => environment::Cloud::Aws as i32,\n                \"gcp\" => environment::Cloud::Gcp as i32,\n                \"azure\" => environment::Cloud::Azure as i32,\n                _ => environment::Cloud::Unspecified as i32,\n            })\n            .unwrap_or(environment::Cloud::Unspecified as i32),\n    });\n\n    // Map GracefulShutdown\n    let graceful_shutdown =\n        infra\n            .graceful_shutdown\n            .as_ref()\n            .map(|gs| pbruntime::GracefulShutdown {\n                total: gs.total.map(|t| prost_types::Duration {\n                    seconds: t as i64,\n                    nanos: 0,\n                }),\n                shutdown_hooks: gs.shutdown_hooks.map(|t| prost_types::Duration {\n                    seconds: t as i64,\n                    nanos: 0,\n                }),\n                handlers: gs.handlers.map(|t| prost_types::Duration {\n                    seconds: t as i64,\n                    nanos: 0,\n                }),\n            });\n\n    // Map Auth methods\n    let auth_methods = infra\n        .auth\n        .as_ref()\n        .map(|auths| {\n            auths\n                .iter()\n                .map(|auth| {\n                    let auth_method = match auth {\n                        Auth::Key(k) => {\n                            service_auth::AuthMethod::EncoreAuth(service_auth::EncoreAuth {\n                                auth_keys: vec![pbruntime::EncoreAuthKey {\n                                    id: k.id as u32,\n                                    data: Some(map_env_string_to_secret_data(&k.key)),\n                                }],\n                            })\n                        }\n                    };\n                    pbruntime::ServiceAuth {\n                        auth_method: Some(auth_method),\n                    }\n                })\n                .collect()\n        })\n        .unwrap_or_else(|| {\n            vec![pbruntime::ServiceAuth {\n                auth_method: Some(service_auth::AuthMethod::Noop(service_auth::NoopAuth {})),\n            }]\n        });\n\n    // Map ServiceDiscovery\n    let service_discovery = infra.service_discovery.map(|services| {\n        let services_mapped = services\n            .into_iter()\n            .map(|(name, sd)| {\n                let svc_auth_methods = sd\n                    .auth\n                    .map(|auths| {\n                        auths\n                            .into_iter()\n                            .map(|auth| match auth {\n                                Auth::Key(k) => pbruntime::ServiceAuth {\n                                    auth_method: Some(service_auth::AuthMethod::EncoreAuth(\n                                        service_auth::EncoreAuth {\n                                            auth_keys: vec![pbruntime::EncoreAuthKey {\n                                                id: k.id as u32,\n                                                data: Some(map_env_string_to_secret_data(&k.key)),\n                                            }],\n                                        },\n                                    )),\n                                },\n                            })\n                            .collect()\n                    })\n                    .unwrap_or(auth_methods.clone());\n                (\n                    name,\n                    service_discovery::Location {\n                        base_url: sd.base_url,\n                        auth_methods: svc_auth_methods,\n                    },\n                )\n            })\n            .collect();\n\n        pbruntime::ServiceDiscovery {\n            services: services_mapped,\n        }\n    });\n\n    // Map Buckets\n    let buckets = infra.object_storage.map(|object_storages| {\n        object_storages\n            .into_iter()\n            .map(|os| match os {\n                ObjectStorage::GCS(gcs) => pbruntime::BucketCluster {\n                    rid: get_next_rid(),\n                    provider: Some(pbruntime::bucket_cluster::Provider::Gcs(\n                        pbruntime::bucket_cluster::Gcs {\n                            endpoint: gcs.endpoint,\n                            anonymous: false,\n                            local_sign: None,\n                        },\n                    )),\n                    buckets: gcs\n                        .buckets\n                        .into_iter()\n                        .map(|(name, bucket)| pbruntime::Bucket {\n                            encore_name: name,\n                            cloud_name: bucket.name,\n                            key_prefix: bucket.key_prefix,\n                            public_base_url: bucket.public_base_url,\n                            rid: get_next_rid(),\n                        })\n                        .collect(),\n                },\n                ObjectStorage::S3(s3) => pbruntime::BucketCluster {\n                    rid: get_next_rid(),\n                    provider: Some(pbruntime::bucket_cluster::Provider::S3(\n                        pbruntime::bucket_cluster::S3 {\n                            region: s3.region,\n                            endpoint: s3.endpoint,\n                            access_key_id: s3.access_key_id,\n                            secret_access_key: s3\n                                .secret_access_key\n                                .as_ref()\n                                .map(map_env_string_to_secret_data),\n                        },\n                    )),\n                    buckets: s3\n                        .buckets\n                        .into_iter()\n                        .map(|(name, bucket)| pbruntime::Bucket {\n                            encore_name: name,\n                            cloud_name: bucket.name,\n                            key_prefix: bucket.key_prefix,\n                            public_base_url: bucket.public_base_url,\n                            rid: get_next_rid(),\n                        })\n                        .collect(),\n                },\n            })\n            .collect()\n    });\n\n    // Map Metrics\n    let metrics = infra.metrics.map(|metrics| {\n        let (provider, interval) = match metrics {\n            Metrics::Prometheus(pm) => (\n                metrics_provider::Provider::PromRemoteWrite(\n                    metrics_provider::PrometheusRemoteWrite {\n                        remote_write_url: Some(map_env_string_to_secret_data(&pm.remote_write_url)),\n                    },\n                ),\n                pm.collection_interval,\n            ),\n            Metrics::Datadog(dd) => (\n                metrics_provider::Provider::Datadog(metrics_provider::Datadog {\n                    site: dd.site,\n                    api_key: Some(map_env_string_to_secret_data(&dd.api_key)),\n                }),\n                dd.collection_interval,\n            ),\n            Metrics::GCPCloudMonitoring(gcp) => (\n                metrics_provider::Provider::Gcp(metrics_provider::GcpCloudMonitoring {\n                    project_id: gcp.project_id,\n                    monitored_resource_type: gcp.monitored_resource_type,\n                    monitored_resource_labels: gcp.monitored_resource_labels.unwrap_or_default(),\n                    metric_names: gcp.metric_names.unwrap_or_default(),\n                }),\n                gcp.collection_interval,\n            ),\n            Metrics::AWSCloudWatch(aws) => (\n                metrics_provider::Provider::Aws(metrics_provider::AwsCloudWatch {\n                    namespace: aws.namespace,\n                }),\n                aws.collection_interval,\n            ),\n        };\n\n        vec![MetricsProvider {\n            rid: get_next_rid(),\n            collection_interval: interval.map(|i| prost_types::Duration {\n                seconds: i as i64,\n                nanos: 0,\n            }),\n            provider: Some(provider),\n        }]\n    });\n\n    // Map Observability\n    let observability = Some(Observability {\n        metrics: metrics.unwrap_or_default(),\n        tracing: Vec::new(),\n        logs: Vec::new(),\n    });\n\n    let cors = infra.cors.map(|cors| gateway::Cors {\n        debug: cors.debug.unwrap_or(false),\n        disable_credentials: false,\n        allowed_origins_without_credentials: cors\n            .allow_origins_without_credentials\n            .map(|f| gateway::CorsAllowedOrigins { allowed_origins: f }),\n        allowed_origins_with_credentials: cors.allow_origins_with_credentials.map(|f| {\n            gateway::cors::AllowedOriginsWithCredentials::AllowedOrigins(\n                gateway::CorsAllowedOrigins { allowed_origins: f },\n            )\n        }),\n        extra_allowed_headers: cors.allow_headers.unwrap_or_default(),\n        extra_exposed_headers: cors.expose_headers.unwrap_or_default(),\n        allow_private_network_access: true,\n    });\n\n    let gateways = infra\n        .hosted_gateways\n        .map(|gateways| {\n            gateways\n                .into_iter()\n                .map(|gateway| pbruntime::Gateway {\n                    rid: get_next_rid(),\n                    encore_name: gateway,\n                    base_url: metadata.base_url.clone().unwrap_or_default(),\n                    hostnames: vec![],\n                    cors: cors.clone(),\n                })\n                .collect::<Vec<_>>()\n        })\n        .unwrap_or_default();\n\n    // Map Deployment\n    let deployment = Some(Deployment {\n        deploy_id: String::new(),\n        deployed_at: None,\n        dynamic_experiments: Vec::new(),\n        hosted_gateways: gateways.iter().map(|g| g.rid.clone()).collect(),\n        hosted_services: infra\n            .hosted_services\n            .map(|services| {\n                services\n                    .iter()\n                    .map(|service| pbruntime::HostedService {\n                        name: service.clone(),\n                        worker_threads: infra.worker_threads,\n                        log_config: infra.log_config.clone(),\n                    })\n                    .collect()\n            })\n            .unwrap_or_default(),\n        auth_methods,\n        observability,\n        service_discovery,\n        graceful_shutdown,\n        metrics: infra\n            .used_metrics\n            .unwrap_or_default()\n            .into_iter()\n            .map(|m| pbruntime::Metric {\n                encore_name: m.name,\n                services: m.services,\n            })\n            .collect(),\n    });\n\n    let mut credentials = Credentials {\n        client_certs: Vec::new(),\n        sql_roles: Vec::new(),\n        redis_roles: Vec::new(),\n    };\n\n    // Map SQL Servers\n    let sql_clusters = infra.sql_servers.map(|servers| {\n        servers\n            .into_iter()\n            .map(|server| {\n                let default_client_cert = server\n                    .tls_config\n                    .as_ref()\n                    .and_then(|tls| tls.client_cert.as_ref())\n                    .map(|f| {\n                        let rid = get_next_rid();\n                        let client_cert = pbruntime::ClientCert {\n                            rid: rid.clone(),\n                            cert: f.cert.clone(),\n                            key: Some(map_env_string_to_secret_data(&f.key)),\n                        };\n                        credentials.client_certs.push(client_cert);\n                        rid\n                    });\n\n                let databases = server\n                    .databases\n                    .into_iter()\n                    .map(|(name, db)| {\n                        let client_cert = db\n                            .client_cert\n                            .map(|f| {\n                                let rid = get_next_rid();\n                                let client_cert = pbruntime::ClientCert {\n                                    rid: rid.clone(),\n                                    cert: f.cert,\n                                    key: Some(map_env_string_to_secret_data(&f.key)),\n                                };\n                                credentials.client_certs.push(client_cert);\n                                rid\n                            })\n                            .or_else(|| default_client_cert.clone());\n                        let role_rid = get_next_rid();\n                        let role = SqlRole {\n                            rid: role_rid.clone(),\n                            client_cert_rid: client_cert,\n                            username: db.username,\n                            password: Some(map_env_string_to_secret_data(&db.password)),\n                        };\n                        credentials.sql_roles.push(role);\n                        SqlDatabase {\n                            rid: get_next_rid(),\n                            encore_name: name.clone(),\n                            cloud_name: db.name.unwrap_or(name),\n                            conn_pools: vec![SqlConnectionPool {\n                                is_readonly: false,\n                                role_rid,\n                                min_connections: db.min_connections.unwrap_or(0),\n                                max_connections: db.max_connections.unwrap_or(100),\n                            }],\n                        }\n                    })\n                    .collect();\n\n                SqlCluster {\n                    rid: get_next_rid(),\n                    servers: vec![SqlServer {\n                        rid: get_next_rid(),\n                        host: server.host,\n                        kind: pbruntime::ServerKind::Primary as i32,\n                        tls_config: server.tls_config.map_or_else(\n                            || Some(TlsConfig::default()),\n                            |tls| match tls.disabled {\n                                true => None,\n                                false => Some(TlsConfig {\n                                    server_ca_cert: tls.ca,\n                                    disable_tls_hostname_verification: tls\n                                        .disable_tls_hostname_verification,\n                                    disable_ca_validation: tls.disable_ca_validation,\n                                }),\n                            },\n                        ),\n                    }],\n                    databases,\n                }\n            })\n            .collect()\n    });\n\n    // Map Redis\n    let redis_clusters = infra.redis.map(|redis_map| {\n        redis_map\n            .into_iter()\n            .map(|(name, redis)| {\n                let client_cert = redis\n                    .tls_config\n                    .as_ref()\n                    .and_then(|tls| tls.client_cert.as_ref())\n                    .map(|f| {\n                        let rid = get_next_rid();\n                        let client_cert = pbruntime::ClientCert {\n                            rid: rid.clone(),\n                            cert: f.cert.clone(),\n                            key: Some(map_env_string_to_secret_data(&f.key)),\n                        };\n                        credentials.client_certs.push(client_cert);\n                        rid\n                    });\n                let auth = redis.auth.map(|ra| match ra.r#type.as_str() {\n                    \"auth_string\" => redis_role::Auth::AuthString(map_env_string_to_secret_data(\n                        ra.auth_string.as_ref().unwrap(),\n                    )),\n                    \"acl\" => redis_role::Auth::Acl(redis_role::AuthAcl {\n                        username: ra.username.unwrap(),\n                        password: Some(map_env_string_to_secret_data(\n                            ra.password.as_ref().unwrap(),\n                        )),\n                    }),\n                    _ => redis_role::Auth::AuthString(map_env_string_to_secret_data(\n                        ra.auth_string.as_ref().unwrap(),\n                    )),\n                });\n\n                let role_rid = get_next_rid();\n                let role = RedisRole {\n                    rid: role_rid.clone(),\n                    client_cert_rid: client_cert,\n                    auth,\n                };\n                credentials.redis_roles.push(role);\n                let database = RedisDatabase {\n                    rid: get_next_rid(),\n                    encore_name: name, // Use the key as the name\n                    database_idx: redis.database_index,\n                    key_prefix: redis.key_prefix,\n                    conn_pools: vec![RedisConnectionPool {\n                        is_readonly: false,\n                        role_rid,\n                        min_connections: redis.min_connections.unwrap_or(0),\n                        max_connections: redis.max_connections.unwrap_or(100),\n                    }],\n                };\n\n                RedisCluster {\n                    rid: String::new(), // Assign a unique RID\n                    servers: vec![RedisServer {\n                        rid: String::new(), // Assign a unique RID\n                        host: redis.host,\n                        kind: pbruntime::ServerKind::Primary as i32,\n                        tls_config: redis.tls_config.map_or_else(\n                            || Some(TlsConfig::default()),\n                            |tls| match tls.disabled {\n                                true => None,\n                                false => Some(TlsConfig {\n                                    server_ca_cert: tls.ca,\n                                    disable_tls_hostname_verification: tls\n                                        .disable_tls_hostname_verification,\n                                    disable_ca_validation: tls.disable_ca_validation,\n                                }),\n                            },\n                        ),\n                    }],\n                    databases: vec![database],\n                    in_memory: false,\n                }\n            })\n            .collect()\n    });\n\n    // Map PubSub\n    let pubsub_clusters = infra.pubsub.map(|pubsubs| {\n        pubsubs\n            .into_iter()\n            .map(|pubsub| {\n                // Handle different PubSub types\n                let (provider, topics, subscriptions) = match pubsub {\n                    PubSub::GCPPubsub(gcp) => {\n                        let topics = gcp\n                            .topics\n                            .iter()\n                            .map(|(name, topic)| PubSubTopic {\n                                rid: String::new(),\n                                encore_name: name.clone(),\n                                cloud_name: topic.name.clone(),\n                                delivery_guarantee: pub_sub_topic::DeliveryGuarantee::AtLeastOnce\n                                    as i32,\n                                ordering_attr: None,\n                                provider_config: Some(pub_sub_topic::ProviderConfig::GcpConfig(\n                                    pub_sub_topic::GcpConfig {\n                                        project_id: topic\n                                            .project_id\n                                            .clone()\n                                            .unwrap_or_else(|| gcp.project_id.clone()),\n                                    },\n                                )),\n                            })\n                            .collect();\n                        let subscriptions = gcp\n                            .topics\n                            .iter()\n                            .flat_map(|(topic_name, topic)| {\n                                topic.subscriptions.iter().map(|(sub_name, sub)| {\n                                    PubSubSubscription {\n                                        rid: String::new(),\n                                        topic_encore_name: topic_name.clone(),\n                                        subscription_encore_name: sub_name.clone(),\n                                        topic_cloud_name: topic.name.clone(),\n                                        subscription_cloud_name: sub.name.clone(),\n                                        push_only: sub.push_config.is_some(),\n                                        provider_config: Some(\n                                            pub_sub_subscription::ProviderConfig::GcpConfig(\n                                                pub_sub_subscription::GcpConfig {\n                                                    project_id: sub\n                                                        .project_id\n                                                        .clone()\n                                                        .unwrap_or_else(|| gcp.project_id.clone()),\n                                                    push_service_account: sub\n                                                        .push_config\n                                                        .as_ref()\n                                                        .map(|pc| pc.service_account.clone()),\n                                                    push_jwt_audience: sub\n                                                        .push_config\n                                                        .as_ref()\n                                                        .map(|pc| pc.jwt_audience.clone()),\n                                                },\n                                            ),\n                                        ),\n                                    }\n                                })\n                            })\n                            .collect();\n\n                        let provider =\n                            pub_sub_cluster::Provider::Gcp(pub_sub_cluster::GcpPubSub {});\n                        (Some(provider), topics, subscriptions)\n                    }\n                    PubSub::AWSSnsSqs(aws) => {\n                        let topics = aws\n                            .topics\n                            .iter()\n                            .map(|(name, topic)| PubSubTopic {\n                                rid: String::new(),\n                                encore_name: name.clone(),\n                                cloud_name: topic.arn.clone(),\n                                delivery_guarantee: pub_sub_topic::DeliveryGuarantee::AtLeastOnce\n                                    as i32, // AWS typically provides at-least-once delivery\n                                ordering_attr: None, // Add ordering if necessary\n                                provider_config: None, // AWS doesn't need additional provider config here\n                            })\n                            .collect();\n\n                        let subscriptions = aws\n                            .topics\n                            .iter()\n                            .flat_map(|(topic_name, topic)| {\n                                topic.subscriptions.iter().map(|(sub_name, sub)| {\n                                    PubSubSubscription {\n                                        rid: String::new(),\n                                        topic_encore_name: topic_name.clone(),\n                                        subscription_encore_name: sub_name.clone(),\n                                        topic_cloud_name: topic.arn.clone(),\n                                        subscription_cloud_name: sub.url.clone(),\n                                        push_only: false, // AWS SQS doesn't typically use push config\n                                        provider_config: None, // AWS doesn't need additional provider config\n                                    }\n                                })\n                            })\n                            .collect();\n\n                        let provider =\n                            pub_sub_cluster::Provider::Aws(pub_sub_cluster::AwsSqsSns {});\n\n                        (Some(provider), topics, subscriptions)\n                    }\n                    PubSub::NSQ(nsq) => {\n                        let topics = nsq\n                            .topics\n                            .iter()\n                            .map(|(name, topic)| PubSubTopic {\n                                rid: String::new(),\n                                encore_name: name.clone(),\n                                cloud_name: topic.name.clone(), // NSQ doesn't have cloud-specific names, using the topic name\n                                delivery_guarantee: pub_sub_topic::DeliveryGuarantee::AtLeastOnce\n                                    as i32, // NSQ typically guarantees at-least-once delivery\n                                ordering_attr: None, // NSQ doesn't handle message ordering natively\n                                provider_config: None, // No additional provider config for NSQ\n                            })\n                            .collect();\n\n                        let subscriptions = nsq\n                            .topics\n                            .iter()\n                            .flat_map(|(topic_name, topic)| {\n                                topic.subscriptions.iter().map(|(sub_name, sub)| {\n                                    PubSubSubscription {\n                                        rid: String::new(),\n                                        topic_encore_name: topic_name.clone(),\n                                        subscription_encore_name: sub_name.clone(),\n                                        topic_cloud_name: topic.name.clone(), // Using topic name for simplicity\n                                        subscription_cloud_name: sub.name.clone(),\n                                        push_only: false, // NSQ is pull-based, no push config\n                                        provider_config: None, // No additional provider config for NSQ\n                                    }\n                                })\n                            })\n                            .collect();\n\n                        let provider = pub_sub_cluster::Provider::Nsq(pub_sub_cluster::Nsq {\n                            hosts: vec![nsq.hosts.clone()], // Mapping NSQ hosts\n                        });\n\n                        (Some(provider), topics, subscriptions)\n                    }\n                };\n\n                PubSubCluster {\n                    rid: get_next_rid(),\n                    topics,\n                    subscriptions,\n                    provider,\n                }\n            })\n            .collect()\n    });\n\n    // Map Secrets\n    let app_secrets: Vec<AppSecret> = match infra.secrets {\n        Some(Secrets::Map(secrets_map)) => secrets_map\n            .into_iter()\n            .map(|(name, value)| AppSecret {\n                rid: get_next_rid(),\n                encore_name: name.clone(),\n                data: Some(map_env_string_to_secret_data(&value)),\n            })\n            .collect(),\n        Some(Secrets::EnvRef(env_ref)) => {\n            // Fetch the environment variable\n            match std::env::var(env_ref.env) {\n                Ok(secrets_json) => {\n                    // Parse the JSON string into a HashMap\n                    match serde_json::from_str::<HashMap<String, String>>(&secrets_json) {\n                        Ok(secrets_map) => secrets_map\n                            .into_iter()\n                            .map(|(name, value)| AppSecret {\n                                rid: get_next_rid(),\n                                encore_name: name,\n                                data: Some(pbruntime::SecretData {\n                                    encoding: secret_data::Encoding::None as i32,\n                                    source: Some(secret_data::Source::Embedded(value.into_bytes())),\n                                    sub_path: None,\n                                }),\n                            })\n                            .collect(),\n                        Err(_) => {\n                            ::log::error!(\n                                \"Failed to parse secrets JSON from secret environment variable\"\n                            );\n                            Vec::new()\n                        }\n                    }\n                }\n                Err(_) => {\n                    ::log::error!(\"Failed to read secrets from environment variable\");\n                    Vec::new()\n                }\n            }\n        }\n        None => Vec::new(),\n    };\n\n    // Map Infrastructure Resources\n    let resources = Some(Resources {\n        gateways,\n        sql_clusters: sql_clusters.unwrap_or_default(),\n        pubsub_clusters: pubsub_clusters.unwrap_or_default(),\n        redis_clusters: redis_clusters.unwrap_or_default(),\n        app_secrets,\n        bucket_clusters: buckets.unwrap_or_default(),\n    });\n\n    let infra_struct = Some(Infrastructure {\n        resources,\n        credentials: Some(credentials),\n    });\n\n    // Construct the final RuntimeConfig\n    RuntimeConfig {\n        environment,\n        infra: infra_struct,\n        deployment,\n        encore_platform: None,\n    }\n}\n\n// Helper function to map EnvString to SecretData\nfn map_env_string_to_secret_data(env_string: &EnvString) -> pbruntime::SecretData {\n    match env_string {\n        EnvString::String(s) => pbruntime::SecretData {\n            encoding: secret_data::Encoding::None as i32,\n            source: Some(secret_data::Source::Embedded(s.clone().into_bytes())),\n            sub_path: None,\n        },\n        EnvString::EnvRef(env_ref) => pbruntime::SecretData {\n            encoding: secret_data::Encoding::None as i32,\n            source: Some(secret_data::Source::Env(env_ref.env.clone())),\n            sub_path: None,\n        },\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use prost::Message;\n    use serde_json;\n    use std::fs;\n\n    #[test]\n    fn test_map_infra_to_runtime() {\n        // Load and parse the infra.config.json fixture\n        let infra_json = fs::read_to_string(format!(\n            \"{}/resources/test/infra.config.json\",\n            env!(\"CARGO_MANIFEST_DIR\")\n        ))\n        .expect(\"Failed to read infra.config.json\");\n        let infra_config: InfraConfig =\n            serde_json::from_str(&infra_json).expect(\"Failed to parse infra.config.json\");\n\n        // Convert InfraConfig to Runtime\n        let runtime: RuntimeConfig = map_infra_to_runtime(infra_config);\n\n        // Load and parse the runtime.json fixture\n        let runtime_data = fs::read(format!(\n            \"{}/resources/test/runtime.pb\",\n            env!(\"CARGO_MANIFEST_DIR\")\n        ))\n        .expect(\"Failed to read runtime.json\");\n        let expected_runtime =\n            RuntimeConfig::decode(runtime_data.as_slice()).expect(\"Failed to parse runtime.json\");\n\n        // Compare the converted runtime with the expected runtime\n        assert_eq!(\n            runtime, expected_runtime,\n            \"Converted runtime does not match expected runtime\"\n        );\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/lib.rs",
    "content": "use std::borrow::Borrow;\nuse std::collections::HashSet;\nuse std::fmt::Display;\nuse std::hash::Hash;\nuse std::io::Read;\n\nuse std::path::Path;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse base64::Engine;\nuse duct::cmd;\nuse prost::Message;\n\nuse crate::api::reqauth::platform;\npub use names::{CloudName, EncoreName, EndpointName};\n\nuse crate::encore::parser::meta::v1 as metapb;\nuse crate::encore::runtime::v1 as runtimepb;\n\npub mod api;\nmod base32;\npub mod cache;\npub mod error;\npub mod infracfg;\npub mod log;\npub mod meta;\npub mod metadata;\npub mod metrics;\npub mod model;\nmod names;\npub mod objects;\npub mod proccfg;\npub mod pubsub;\npub mod runtime_config;\npub mod secrets;\npub mod sqldb;\nmod trace;\n\npub mod encore {\n    pub mod runtime {\n        pub mod v1 {\n            include!(concat!(env!(\"OUT_DIR\"), \"/encore.runtime.v1.rs\"));\n        }\n    }\n\n    pub mod parser {\n        pub mod meta {\n            pub mod v1 {\n                include!(concat!(env!(\"OUT_DIR\"), \"/encore.parser.meta.v1.rs\"));\n            }\n        }\n\n        pub mod schema {\n            pub mod v1 {\n                include!(concat!(env!(\"OUT_DIR\"), \"/encore.parser.schema.v1.rs\"));\n            }\n        }\n    }\n}\n\npub struct RuntimeBuilder {\n    cfg: Option<runtimepb::RuntimeConfig>,\n    proc_cfg: Option<proccfg::ProcessConfig>,\n    md: Option<metapb::Data>,\n    err: Option<anyhow::Error>,\n    test_mode: bool,\n    is_worker: bool,\n}\n\nimpl Default for RuntimeBuilder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl RuntimeBuilder {\n    pub fn new() -> Self {\n        Self {\n            cfg: None,\n            proc_cfg: None,\n            md: None,\n            err: None,\n            test_mode: false,\n            is_worker: false,\n        }\n    }\n\n    pub fn with_test_mode(mut self, enabled: bool) -> Self {\n        self.test_mode = enabled;\n        if enabled {\n            enable_test_mode().unwrap();\n        }\n        self\n    }\n\n    pub fn with_worker(mut self, enabled: bool) -> Self {\n        self.is_worker = enabled;\n        self\n    }\n\n    pub fn with_runtime_config(mut self, cfg: runtimepb::RuntimeConfig) -> Self {\n        self.cfg = Some(cfg);\n        self\n    }\n\n    pub fn with_proc_config(mut self, proc_cfg: proccfg::ProcessConfig) -> Self {\n        self.proc_cfg = Some(proc_cfg);\n        self\n    }\n\n    pub fn with_runtime_config_from_env(mut self) -> Self {\n        if self.err.is_none() {\n            match infra_config_from_env() {\n                Ok(opt_cfg) => match opt_cfg {\n                    Some(cfg) => self.cfg = Some(cfg),\n                    None => match runtime_config_from_env() {\n                        Ok(cfg) => self.cfg = Some(cfg),\n                        Err(e) => {\n                            self.err = Some(\n                                anyhow::Error::new(e).context(\"unable to parse runtime config\"),\n                            )\n                        }\n                    },\n                },\n                Err(e) => {\n                    self.err = Some(anyhow::Error::new(e).context(\"unable to parse infra config\"))\n                }\n            }\n            match proc_config_from_env() {\n                Ok(cfg) => self.proc_cfg = cfg,\n                Err(e) => {\n                    self.err = Some(anyhow::Error::new(e).context(\"unable to parse process config\"))\n                }\n            }\n        }\n        self\n    }\n\n    pub fn with_meta(mut self, md: metapb::Data) -> Self {\n        self.md = Some(md);\n        self\n    }\n\n    pub fn with_meta_autodetect(mut self) -> Self {\n        fn auto_detect() -> Result<metapb::Data, anyhow::Error> {\n            match meta_from_env() {\n                Ok(md) => Ok(md),\n                Err(ParseError::EnvNotPresent) => {\n                    let path = Path::new(\"/encore/meta\");\n                    parse_meta(path).context(\"unable to parse app metadata file\")\n                }\n                Err(e) => {\n                    Err(anyhow::Error::new(e)\n                        .context(\"unable to parse app metadata from environment\"))\n                }\n            }\n        }\n\n        if self.err.is_none() {\n            match auto_detect() {\n                Ok(md) => self.md = Some(md),\n                Err(e) => self.err = Some(e),\n            }\n        }\n        self\n    }\n\n    pub fn with_meta_from_env(mut self) -> Self {\n        if self.err.is_none() {\n            match meta_from_env() {\n                Ok(md) => self.md = Some(md),\n                Err(e) => {\n                    self.err = Some(anyhow::Error::new(e).context(\"unable to parse app metadata\"))\n                }\n            }\n        }\n        self\n    }\n\n    pub fn with_meta_from_path(mut self, meta_path: &Path) -> Self {\n        if self.err.is_none() {\n            match parse_meta(meta_path) {\n                Ok(md) => self.md = Some(md),\n                Err(e) => {\n                    self.err = Some(anyhow::Error::new(e).context(\"unable to parse app metadata\"))\n                }\n            }\n        }\n        self\n    }\n\n    pub fn build(self) -> anyhow::Result<Runtime> {\n        if let Some(err) = self.err {\n            return Err(err);\n        }\n        let mut cfg = self.cfg.context(\"runtime config not provided\")?;\n        let md = self.md.context(\"metadata not provided\")?;\n        if let Some(proc_config) = self.proc_cfg {\n            proc_config.apply(&mut cfg)?;\n        }\n        Runtime::new(cfg, md, self.test_mode)\n    }\n}\n\npub struct Runtime {\n    md: metapb::Data,\n    pubsub: pubsub::Manager,\n    secrets: secrets::Manager,\n    sqldb: sqldb::Manager,\n    cache: cache::Manager,\n    objects: objects::Manager,\n    api: api::Manager,\n    app_meta: meta::AppMeta,\n    compute: ComputeConfig,\n    runtime: tokio::runtime::Runtime,\n    metrics: metrics::Manager,\n    runtime_config: runtime_config::RuntimeConfig,\n}\n\nimpl Runtime {\n    pub fn builder() -> RuntimeBuilder {\n        RuntimeBuilder::new()\n    }\n\n    pub fn new(\n        mut cfg: runtimepb::RuntimeConfig,\n        md: metapb::Data,\n        testing: bool,\n    ) -> anyhow::Result<Self> {\n        // Initialize OpenSSL system root certificates, so that libraries can find them.\n        unsafe {\n            openssl_probe::init_openssl_env_vars();\n        }\n\n        let tokio_rt = tokio::runtime::Builder::new_multi_thread()\n            .enable_all()\n            .build()\n            .context(\"failed to build tokio runtime\")?;\n\n        let app_meta = meta::AppMeta::new(&cfg, &md);\n        let runtime_config = runtime_config::RuntimeConfig::new(&cfg);\n        let environment = cfg.environment.take().unwrap_or_default();\n        let mut infra = cfg.infra.take().unwrap_or_default();\n        let resources = infra.resources.take().unwrap_or_default();\n        let creds = infra.credentials.take().unwrap_or_default();\n        let encore_platform = cfg.encore_platform.take().unwrap_or_default();\n\n        let mut deployment = cfg.deployment.take().unwrap_or_default();\n        let service_discovery = deployment.service_discovery.take().unwrap_or_default();\n        let observability = deployment.observability.take().unwrap_or_default();\n\n        let http_client = reqwest::Client::builder()\n            .build()\n            .context(\"failed to build http client\")?;\n\n        let secrets = secrets::Manager::new(resources.app_secrets);\n        let platform_validator = platform::RequestValidator::new(\n            &secrets,\n            encore_platform.platform_signing_keys.clone(),\n        );\n        let platform_validator = Arc::new(platform_validator);\n\n        // Initialize metrics manager from runtime config\n        let metrics_manager = metrics::Manager::from_runtime_config(\n            &observability,\n            &environment,\n            &secrets,\n            &http_client,\n            tokio_rt.handle().clone(),\n        );\n\n        // Set up observability.\n        let disable_tracing =\n            testing || std::env::var(\"ENCORE_NOTRACE\").is_ok_and(|v| !v.is_empty());\n        let tracer = if !disable_tracing {\n            let trace_cfg = observability\n                .tracing\n                .into_iter()\n                .find_map(|p| match p.provider {\n                    Some(runtimepb::tracing_provider::Provider::Encore(encore)) => {\n                        #[allow(deprecated)]\n                        let sampling_rate = encore.sampling_rate;\n                        Some((encore.sampling_config, sampling_rate, encore.trace_endpoint))\n                    }\n                    _ => None,\n                })\n                .and_then(|(cfg, sr, ep)| match reqwest::Url::parse(&ep) {\n                    Ok(ep) => Some((cfg, sr, ep)),\n                    Err(err) => {\n                        ::log::warn!(\"disabling tracing: invalid trace endpoint {}: {}\", ep, err);\n                        None\n                    }\n                });\n\n            match trace_cfg {\n                Some((trace_sampling_config, trace_sampling_rate, trace_endpoint)) => {\n                    let config = trace::ReporterConfig {\n                        app_id: environment.app_id.clone(),\n                        env_id: environment.env_id.clone(),\n                        deploy_id: deployment.deploy_id.clone(),\n                        app_commit: md.app_revision.clone(),\n                        trace_endpoint,\n                        trace_sampling_config: trace::TraceSamplingConfig::new(\n                            trace_sampling_config,\n                            trace_sampling_rate,\n                        ),\n                        platform_validator: platform_validator.clone(),\n                    };\n\n                    let (tracer, reporter) = trace::streaming_tracer(http_client.clone(), config);\n                    tokio_rt.spawn(reporter.start_reporting());\n                    tracer\n                }\n                None => trace::Tracer::noop(),\n            }\n        } else {\n            trace::Tracer::noop()\n        };\n\n        log::set_tracer(tracer.clone());\n\n        // Find push subscriptions which should be proxied to the subscribing service by the gateway\n        let proxied_push_subs = resources\n            .pubsub_clusters\n            .iter()\n            .flat_map(|c| c.subscriptions.iter())\n            .filter(|s| s.push_only)\n            .filter_map(|s| {\n                let topic = md\n                    .pubsub_topics\n                    .iter()\n                    .find(|t| t.name == s.topic_encore_name)?;\n                let sub = topic\n                    .subscriptions\n                    .iter()\n                    .find(|ms| ms.name == s.subscription_encore_name)?;\n\n                match deployment\n                    .hosted_services\n                    .iter()\n                    .any(|s| s.name == sub.service_name)\n                {\n                    true => None,\n                    false => Some((\n                        s.rid.clone(),\n                        api::gateway::ProxiedPushSub {\n                            service_name: EncoreName::from(sub.service_name.clone()),\n                            topic: EncoreName::from(topic.name.clone()),\n                            subscription: EncoreName::from(sub.name.clone()),\n                        },\n                    )),\n                }\n            })\n            .collect();\n\n        let pubsub = pubsub::Manager::new(tracer.clone(), resources.pubsub_clusters, &md)?;\n        let objects =\n            objects::Manager::new(&secrets, tracer.clone(), resources.bucket_clusters, &md);\n        let sqldb = sqldb::ManagerConfig {\n            clusters: resources.sql_clusters,\n            creds: &creds,\n            secrets: &secrets,\n            tracer: tracer.clone(),\n            runtime: tokio_rt.handle().clone(),\n        }\n        .build()\n        .context(\"unable to initialize sqldb proxy\")?;\n\n        let cache = cache::ManagerConfig {\n            clusters: resources.redis_clusters,\n            creds: &creds,\n            secrets: &secrets,\n            tracer: tracer.clone(),\n            testing,\n            runtime: tokio_rt.handle().clone(),\n        }\n        .build()\n        .context(\"unable to initialize cache manager\")?;\n\n        // Determine the compute configuration.\n        let compute = {\n            let mut cfg = ComputeConfig::default();\n            for svc in deployment.hosted_services.iter() {\n                if let Some(log_config) = &svc.log_config {\n                    cfg.log_level = Some(log_config.clone());\n                }\n                if let Some(worker_threads) = svc.worker_threads {\n                    // If we have worker threads already configured on the compute config,\n                    // determine the new value.\n                    cfg.worker_threads = Some(match (cfg.worker_threads, worker_threads) {\n                        // If either explicitly wants 1 worker threads (disabling it), set it to 1.\n                        (Some(1), _) | (_, 1) => 1,\n\n                        // If we have worker threads enabled on both, set it to the minimum.\n                        (Some(a), b) if a > 1 && b > 1 => a.min(b),\n\n                        // Otherwise use the existing value, if any.\n                        (Some(a), _) => a,\n                        // If we don't have an existing value, use the new value.\n                        (None, b) => b,\n                    });\n                }\n            }\n            cfg\n        };\n\n        let api = api::ManagerConfig {\n            meta: &md,\n            environment: &environment,\n            gateways: resources.gateways,\n            hosted_services: deployment.hosted_services,\n            hosted_gateway_rids: deployment.hosted_gateways,\n            svc_auth_methods: deployment.auth_methods,\n            deploy_id: deployment.deploy_id,\n            platform: &encore_platform,\n            secrets: &secrets,\n            service_discovery,\n            http_client: http_client.clone(),\n            tracer,\n            platform_validator,\n            pubsub_push_registry: pubsub.push_registry(),\n            runtime: tokio_rt.handle().clone(),\n            testing,\n            proxied_push_subs,\n            metrics: &metrics_manager,\n        }\n        .build()\n        .context(\"unable to initialize api manager\")?;\n\n        let sqldb_handle = sqldb.start_serving();\n        tokio_rt.spawn(async move {\n            if let Err(err) = sqldb_handle.await {\n                ::log::error!(\"sqldb proxy failed: {err}\");\n            }\n        });\n\n        ::log::debug!(\"encore runtime successfully initialized\");\n\n        Ok(Self {\n            md,\n            pubsub,\n            secrets,\n            sqldb,\n            cache,\n            objects,\n            api,\n            app_meta,\n            compute,\n            runtime: tokio_rt,\n            metrics: metrics_manager,\n            runtime_config,\n        })\n    }\n\n    #[inline]\n    pub fn pubsub(&self) -> &pubsub::Manager {\n        &self.pubsub\n    }\n\n    #[inline]\n    pub fn secrets(&self) -> &secrets::Manager {\n        &self.secrets\n    }\n\n    #[inline]\n    pub fn sqldb(&self) -> &sqldb::Manager {\n        &self.sqldb\n    }\n\n    #[inline]\n    pub fn cache(&self) -> &cache::Manager {\n        &self.cache\n    }\n\n    #[inline]\n    pub fn objects(&self) -> &objects::Manager {\n        &self.objects\n    }\n\n    #[inline]\n    pub fn metadata(&self) -> &metapb::Data {\n        &self.md\n    }\n\n    #[inline]\n    pub fn api(&self) -> &api::Manager {\n        &self.api\n    }\n\n    #[inline]\n    pub fn metrics(&self) -> &metrics::Manager {\n        &self.metrics\n    }\n\n    #[inline]\n    pub fn endpoints(&self) -> &api::EndpointMap {\n        self.api.endpoints()\n    }\n\n    #[inline]\n    pub fn tokio_handle(&self) -> &tokio::runtime::Handle {\n        self.runtime.handle()\n    }\n\n    #[inline]\n    pub fn run_blocking(&self) {\n        self.runtime.block_on(async move {\n            let api_handle = self.api().start_serving();\n\n            if let Err(err) = api_handle.await {\n                ::log::error!(\"failed to start serving: {:?}\", err);\n            }\n        });\n    }\n\n    #[inline]\n    pub fn app_meta(&self) -> &meta::AppMeta {\n        &self.app_meta\n    }\n\n    #[inline]\n    pub fn runtime_config(&self) -> &runtime_config::RuntimeConfig {\n        &self.runtime_config\n    }\n\n    #[inline]\n    pub fn compute(&self) -> &ComputeConfig {\n        &self.compute\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct ComputeConfig {\n    pub log_level: Option<String>,\n    pub worker_threads: Option<i32>,\n}\n\n#[derive(Debug)]\nenum ParseError {\n    EnvNotPresent,\n    EnvVar(std::env::VarError),\n    Base64(base64::DecodeError),\n    Proto(prost::DecodeError),\n    IO(std::io::Error),\n}\n\nimpl Display for ParseError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ParseError::EnvNotPresent => write!(f, \"environment variable not present\"),\n            ParseError::EnvVar(e) => write!(f, \"failed to read environment variable: {e}\"),\n            ParseError::Base64(e) => write!(f, \"failed to decode environment variable: {e}\"),\n            ParseError::Proto(e) => write!(f, \"failed to parse environment variable: {e}\"),\n            ParseError::IO(e) => write!(f, \"failed to read file: {e}\"),\n        }\n    }\n}\n\nimpl std::error::Error for ParseError {}\n\nfn infra_config_from_env() -> Result<Option<runtimepb::RuntimeConfig>, ParseError> {\n    let cfg_path = match std::env::var(\"ENCORE_INFRA_CONFIG_PATH\") {\n        Ok(cfg) => cfg,\n        Err(std::env::VarError::NotPresent) => return Ok(None),\n        Err(e) => return Err(ParseError::EnvVar(e)),\n    };\n    let file_content = std::fs::read_to_string(cfg_path).map_err(ParseError::IO)?;\n    let infra_config: infracfg::InfraConfig = serde_json::from_str(&file_content)\n        .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;\n    let runtime_config = infracfg::map_infra_to_runtime(infra_config);\n    Ok(Some(runtime_config))\n}\n\nfn runtime_config_from_env() -> Result<runtimepb::RuntimeConfig, ParseError> {\n    let cfg = match std::env::var(\"ENCORE_RUNTIME_CONFIG\") {\n        Ok(cfg) => cfg,\n        Err(std::env::VarError::NotPresent) => {\n            // Not present. Check the ENCORE_RUNTIME_CONFIG_PATH environment variable.\n            match std::env::var(\"ENCORE_RUNTIME_CONFIG_PATH\") {\n                Ok(path) => {\n                    let path = Path::new(&path);\n                    return parse_runtime_config(path);\n                }\n                Err(std::env::VarError::NotPresent) => return Err(ParseError::EnvNotPresent),\n                Err(e) => return Err(ParseError::EnvVar(e)),\n            }\n        }\n        Err(e) => return Err(ParseError::EnvVar(e)),\n    };\n\n    if cfg.starts_with(\"gzip:\") {\n        // Parse the remainder as base64-encoded gzip data.\n        let cfg = cfg.as_bytes();\n        let cfg = &cfg[\"gzip:\".len()..];\n        let gzip_data = base64::engine::general_purpose::STANDARD\n            .decode(cfg)\n            .map_err(ParseError::Base64)?;\n\n        let mut decoder = flate2::read::GzDecoder::new(&gzip_data[..]);\n        let mut raw_data = Vec::new();\n        decoder.read_to_end(&mut raw_data).map_err(ParseError::IO)?;\n        runtimepb::RuntimeConfig::decode(&raw_data[..]).map_err(ParseError::Proto)\n    } else {\n        let decoded = base64::engine::general_purpose::STANDARD\n            .decode(cfg.as_bytes())\n            .map_err(ParseError::Base64)?;\n        runtimepb::RuntimeConfig::decode(&decoded[..]).map_err(ParseError::Proto)\n    }\n}\n\nfn parse_runtime_config(path: &Path) -> Result<runtimepb::RuntimeConfig, ParseError> {\n    let data = std::fs::read(path).map_err(ParseError::IO)?;\n    runtimepb::RuntimeConfig::decode(&data[..]).map_err(ParseError::Proto)\n}\n\nfn proc_config_from_env() -> Result<Option<proccfg::ProcessConfig>, ParseError> {\n    let encoded_config = match std::env::var(\"ENCORE_PROCESS_CONFIG\") {\n        Ok(config) => config,\n        Err(std::env::VarError::NotPresent) => return Ok(None),\n        Err(e) => return Err(ParseError::EnvVar(e)),\n    };\n\n    let decoded = base64::engine::general_purpose::STANDARD\n        .decode(encoded_config)\n        .map_err(ParseError::Base64)?;\n\n    let json_str = String::from_utf8(decoded)\n        .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;\n\n    let config = serde_json::from_str(&json_str)\n        .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;\n\n    Ok(Some(config))\n}\n\nfn meta_from_env() -> Result<metapb::Data, ParseError> {\n    let cfg = match std::env::var(\"ENCORE_APP_META\") {\n        Ok(cfg) => cfg,\n        Err(std::env::VarError::NotPresent) => {\n            // Not present. Check the ENCORE_APP_META_PATH environment variable.\n            match std::env::var(\"ENCORE_APP_META_PATH\") {\n                Ok(path) => {\n                    let path = Path::new(&path);\n                    return parse_meta(path);\n                }\n                Err(std::env::VarError::NotPresent) => return Err(ParseError::EnvNotPresent),\n                Err(e) => return Err(ParseError::EnvVar(e)),\n            }\n        }\n        Err(e) => return Err(ParseError::EnvVar(e)),\n    };\n\n    if cfg.starts_with(\"gzip:\") {\n        // Parse the remainder as base64-encoded gzip data.\n        let cfg = cfg.as_bytes();\n        let cfg = &cfg[\"gzip:\".len()..];\n        let gzip_data = base64::engine::general_purpose::STANDARD\n            .decode(cfg)\n            .map_err(ParseError::Base64)?;\n\n        let mut decoder = flate2::read::GzDecoder::new(&gzip_data[..]);\n        let mut raw_data = Vec::new();\n        decoder.read_to_end(&mut raw_data).map_err(ParseError::IO)?;\n        metapb::Data::decode(&raw_data[..]).map_err(ParseError::Proto)\n    } else {\n        let decoded = base64::engine::general_purpose::STANDARD\n            .decode(cfg.as_bytes())\n            .map_err(ParseError::Base64)?;\n        metapb::Data::decode(&decoded[..]).map_err(ParseError::Proto)\n    }\n}\n\nfn parse_meta(path: &Path) -> Result<metapb::Data, ParseError> {\n    let data = std::fs::read(path).map_err(ParseError::IO)?;\n    metapb::Data::decode(&data[..]).map_err(ParseError::Proto)\n}\n\nfn enable_test_mode() -> Result<(), ParseError> {\n    if std::env::var(\"ENCORE_APP_META\").is_ok() || std::env::var(\"ENCORE_APP_META_PATH\").is_ok() {\n        return Ok(());\n    }\n\n    let out = cmd!(\"encore\", \"test\", \"--prepare\")\n        .stdout_capture()\n        .stderr_capture()\n        .run()\n        .map_err(ParseError::IO)?;\n    if !out.status.success() {\n        return Err(ParseError::IO(std::io::Error::other(\n            String::from_utf8(out.stderr).unwrap(),\n        )));\n    }\n\n    let data =\n        String::from_utf8(out.stdout).map_err(|e| ParseError::IO(std::io::Error::other(e)))?;\n\n    for line in data.split('\\n') {\n        let Some((name, value)) = line.split_once('=') else {\n            continue;\n        };\n        match name {\n            \"ENCORE_APP_META\" => std::env::set_var(\"ENCORE_APP_META\", value),\n            \"ENCORE_RUNTIME_CONFIG\" => std::env::set_var(\"ENCORE_RUNTIME_CONFIG\", value),\n            _ => (),\n        }\n    }\n\n    Ok(())\n}\n\n/// Describes which services or gateways are hosted by this server.\n#[derive(Debug, Clone)]\npub struct Hosted(pub HashSet<String>);\n\nimpl Hosted {\n    pub fn is_empty(&self) -> bool {\n        self.0.is_empty()\n    }\n\n    pub fn iter(&self) -> impl Iterator<Item = &String> {\n        self.0.iter()\n    }\n\n    /// Reports whether the given service/entity is hosted by this runtime.\n    pub fn contains<Q>(&self, name: &Q) -> bool\n    where\n        String: Borrow<Q>,\n        Q: Eq + Hash + ?Sized,\n    {\n        self.0.contains(name)\n    }\n}\n\nimpl FromIterator<String> for Hosted {\n    fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {\n        Self(iter.into_iter().collect())\n    }\n}\n\n/// Returns the version of the Encore runtime.\npub fn version() -> &'static str {\n    option_env!(\"ENCORE_VERSION\").unwrap_or(env!(\"CARGO_PKG_VERSION\"))\n}\n\n/// Returns the git commit used to build the Encore runtime.\npub fn build_commit() -> &'static str {\n    env!(\"ENCORE_BINARY_GIT_HASH\")\n}\n"
  },
  {
    "path": "runtimes/core/src/log/consolewriter.rs",
    "content": "use crate::error::{write_stack_trace, StackFrame};\nuse crate::log::fields::FieldConfig;\nuse crate::log::writers::Writer;\nuse anyhow::Context;\nuse chrono::Timelike;\nuse colored::Colorize;\nuse serde_json::Value;\nuse std::cell::RefCell;\nuse std::collections::BTreeMap;\nuse std::io::Write;\nuse std::sync::Mutex;\n\npub struct ConsoleWriter<W: Write + Sync + Send + 'static> {\n    field_config: &'static FieldConfig,\n    mu: Mutex<RefCell<Box<W>>>,\n}\n\nimpl<W: Write + Sync + Send + 'static> ConsoleWriter<W> {\n    pub fn new(field_config: &'static FieldConfig, w: W) -> Self {\n        Self {\n            field_config,\n            mu: Mutex::new(RefCell::new(Box::new(w))),\n        }\n    }\n\n    fn write_fields(\n        &self,\n        buf: &mut Vec<u8>,\n        values: &BTreeMap<String, Value>,\n    ) -> anyhow::Result<()> {\n        // error field is always first\n        if let Some(err) = values.get(self.field_config.error_field_name) {\n            if !buf.is_empty() {\n                buf.push(b' ');\n            }\n            write!(\n                buf,\n                \"{}{}\",\n                format!(\"{}=\", self.field_config.error_field_name).cyan(),\n                format!(\"{err}\").red()\n            )\n            .context(\"unable to write error field value\")?;\n        }\n\n        for key in values.keys() {\n            if key == self.field_config.timestamp_field_name\n                || key == self.field_config.level_field_name\n                || key == self.field_config.caller_field_name\n                || key == self.field_config.message_field_name\n                || key == self.field_config.error_field_name\n                || key == self.field_config.stack_trace_field_name\n            {\n                continue;\n            }\n\n            if !buf.is_empty() {\n                buf.push(b' ');\n            }\n            write!(buf, \"{}\", format!(\"{key}=\").cyan())\n                .context(format!(\"unable to write field key {key}\"))?;\n\n            let value = values.get(key).expect(\"key not found\");\n\n            let value_to_print = match value {\n                // Strings have special handling, as if the string does not contain any special characters or whitespace\n                // we just print the string, otherwise we print the string as a JSON string (i.e. wrapped in quotes and escaped)\n                Value::String(s) => {\n                    if s.contains(' ')\n                        || s.contains('\\t')\n                        || s.contains('\\n')\n                        || s.contains('\\r')\n                        || s.contains('\\\\')\n                        || s.contains('\"')\n                    {\n                        format!(\"{value}\")\n                    } else {\n                        s.to_string()\n                    }\n                }\n                _ => format!(\"{value}\"),\n            };\n\n            write!(buf, \"{value_to_print}\")\n                .context(format!(\"unable to write field value {key}\"))?;\n        }\n\n        // Finally, write the stack trace to the log\n        if let Some(stack) = values.get(self.field_config.stack_trace_field_name) {\n            let stack: Result<Vec<StackFrame>, serde_json::Error> =\n                serde_json::from_value(stack.clone());\n            if let Ok(stack) = stack {\n                let mut writer = VecWriter { buf };\n                write_stack_trace(&stack, &mut writer).context(\"unable to write stack trace\")?;\n            }\n        }\n\n        Ok(())\n    }\n}\n\nstruct VecWriter<'a> {\n    buf: &'a mut Vec<u8>,\n}\nimpl std::fmt::Write for VecWriter<'_> {\n    fn write_str(&mut self, s: &str) -> std::fmt::Result {\n        self.buf.extend_from_slice(s.as_bytes());\n        Ok(())\n    }\n}\n\nimpl<W: Write + Sync + Send + 'static> Writer for ConsoleWriter<W> {\n    fn write(&self, level: log::Level, values: &BTreeMap<String, Value>) -> anyhow::Result<()> {\n        let mut buf = Vec::with_capacity(256);\n\n        write_part(\n            &mut buf,\n            self.field_config.timestamp_field_name,\n            values,\n            format_timestamp,\n        )?;\n        write_level(&mut buf, level)?;\n        write_part(\n            &mut buf,\n            self.field_config.caller_field_name,\n            values,\n            format_caller,\n        )?;\n        write_part(\n            &mut buf,\n            self.field_config.message_field_name,\n            values,\n            format_message,\n        )?;\n\n        self.write_fields(&mut buf, values)?;\n\n        buf.write_all(b\"\\n\").context(\"new line\")?;\n\n        match self.mu.lock() {\n            Ok(guard) => {\n                let mut w = guard\n                    .try_borrow_mut()\n                    .context(\"unable to borrow console output\")?;\n                w.write_all(&buf).context(\"write\")?;\n                Ok(())\n            }\n            Err(poisoned) => Err(anyhow::anyhow!(\"poisoned mutex: {:?}\", poisoned)),\n        }\n    }\n}\n\nfn write_part(\n    buf: &mut Vec<u8>,\n    field: &'static str,\n    values: &BTreeMap<String, Value>,\n    mapper: fn(&str) -> anyhow::Result<String>,\n) -> anyhow::Result<()> {\n    if let Some(value) = values.get(field) {\n        if let Some(value) = value.as_str() {\n            let value = mapper(value).context(format!(\"unable to map part {field}\"))?;\n            if !buf.is_empty() {\n                buf.push(b' ');\n            }\n            write!(buf, \"{value}\").context(format!(\"unable to write part {field}\"))?;\n        }\n    }\n    Ok(())\n}\n\nfn format_timestamp(timestamp: &str) -> anyhow::Result<String> {\n    let timestamp = chrono::DateTime::parse_from_rfc3339(timestamp)\n        .context(format!(\"unable to parse timestamp: {timestamp}\"))?;\n    let datetime: chrono::DateTime<chrono::Local> = timestamp.into();\n\n    let (is_pm, hour) = datetime.hour12();\n    let minute = datetime.minute();\n\n    let mut timestamp = String::with_capacity(32);\n    timestamp.push_str(&format!(\"{hour:02}:{minute:02}\"));\n\n    if is_pm {\n        timestamp.push_str(\"PM\");\n    } else {\n        timestamp.push_str(\"AM\");\n    }\n\n    Ok(format!(\"{}\", timestamp.bright_black()))\n}\n\nfn write_level(buf: &mut Vec<u8>, level: log::Level) -> anyhow::Result<()> {\n    let level_str = match level {\n        log::Level::Trace => \"TRC\".magenta(),\n        log::Level::Debug => \"DBG\".yellow(),\n        log::Level::Info => \"INF\".green(),\n        log::Level::Warn => \"WRN\".red(),\n        log::Level::Error => \"ERR\".red().bold(),\n    };\n\n    if !buf.is_empty() {\n        buf.push(b' ');\n    }\n    write!(buf, \"{level_str}\").context(\"unable to write log level\")?;\n\n    Ok(())\n}\n\nfn format_caller(caller: &str) -> anyhow::Result<String> {\n    Ok(format!(\"{} {}\", caller.bold(), \">\".cyan()))\n}\n\nfn format_message(message: &str) -> anyhow::Result<String> {\n    Ok(message.to_string())\n}\n"
  },
  {
    "path": "runtimes/core/src/log/fields.rs",
    "content": "/// Fields control the names of the fields that are used in the log output.\n#[derive(Debug)]\npub struct FieldConfig {\n    pub timestamp_field_name: &'static str,\n\n    pub level_field_name: &'static str,\n    pub level_trace_value: &'static str,\n    pub level_debug_value: &'static str,\n    pub level_info_value: &'static str,\n    pub level_warn_value: &'static str,\n    pub level_error_value: &'static str,\n    pub level_fatal_value: &'static str,\n\n    pub message_field_name: &'static str,\n\n    pub error_field_name: &'static str,\n\n    pub caller_field_name: &'static str,\n\n    pub stack_trace_field_name: &'static str,\n}\n\npub static DEFAULT_FIELDS: FieldConfig = FieldConfig {\n    timestamp_field_name: \"time\",\n\n    level_field_name: \"level\",\n    level_trace_value: \"trace\",\n    level_debug_value: \"debug\",\n    level_info_value: \"info\",\n    level_warn_value: \"warn\",\n    level_error_value: \"error\",\n    level_fatal_value: \"fatal\",\n\n    message_field_name: \"message\",\n\n    error_field_name: \"error\",\n\n    caller_field_name: \"caller\",\n\n    stack_trace_field_name: \"stack\",\n};\n\npub static GCP_FIELDS: FieldConfig = FieldConfig {\n    timestamp_field_name: \"timestamp\",\n\n    level_field_name: \"severity\",\n    level_trace_value: \"DEBUG\",\n    level_debug_value: \"DEBUG\",\n    level_info_value: \"INFO\",\n    level_warn_value: \"WARNING\",\n    level_error_value: \"ERROR\",\n    level_fatal_value: \"CRITICAL\",\n\n    message_field_name: \"message\",\n\n    error_field_name: \"error\",\n\n    caller_field_name: \"caller\",\n\n    stack_trace_field_name: \"stacktrace\",\n};\n\nimpl FieldConfig {\n    pub fn default() -> &'static FieldConfig {\n        // If we're running in GCP, then we'll use the GCP fields.\n        for var in &[\n            \"GCP_PROJECT\",\n            \"GOOGLE_CLOUD_PROJECT\",\n            \"GCP_METADATA_PROJECT\",\n        ] {\n            if std::env::var(var).is_ok() {\n                return &GCP_FIELDS;\n            }\n        }\n        &DEFAULT_FIELDS\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/log/logger.rs",
    "content": "use crate::error::AppError;\nuse crate::log::fields::FieldConfig;\nuse crate::log::writers::{default_writer, Writer};\nuse crate::model::{self, LogField};\nuse crate::trace::protocol::LogMessageData;\nuse crate::trace::Tracer;\nuse anyhow::Context;\nuse env_logger::filter::Filter;\nuse log::{Log, Metadata, Record};\nuse std::collections::BTreeMap;\nuse std::sync::{Arc, RwLock};\nuse std::time::SystemTime;\n\npub type Fields = BTreeMap<String, serde_json::Value>;\n\n/// Logger is a structured JSON logger that can be used to emit structured logs to stderr\n#[derive(Debug, Clone)]\npub struct Logger {\n    filter: Arc<Filter>,\n    app_level: log::LevelFilter,\n    field_config: &'static FieldConfig,\n    writer: Arc<dyn Writer>,\n    extra_fields: Fields,\n    tracer: Arc<RwLock<Tracer>>,\n}\n\nimpl Logger {\n    /// New returns a new logger with the given field config.\n    pub fn new(\n        app_level: log::LevelFilter,\n        filter: Filter,\n        field_config: &'static FieldConfig,\n    ) -> Self {\n        Self {\n            filter: Arc::new(filter),\n            app_level,\n            field_config,\n            writer: default_writer(field_config),\n            extra_fields: Fields::new(),\n            tracer: Arc::new(RwLock::new(Tracer::noop())),\n        }\n    }\n\n    /// Sets the loggers tracer\n    pub fn set_tracer(&self, tracer: Tracer) {\n        let mut t = self.tracer.write().expect(\"tracer lock poisoned\");\n        *t = tracer;\n    }\n\n    /// Returns a new logger with the given log level.\n    pub fn with_level(&self, level: log::LevelFilter) -> Self {\n        Self {\n            app_level: level,\n            ..self.clone()\n        }\n    }\n\n    /// Returns a new logger with the given writer.\n    pub fn with_writer(&self, writer: Arc<dyn Writer>) -> Self {\n        Self {\n            writer,\n            ..self.clone()\n        }\n    }\n\n    /// Returns a new logger with the given fields added to the context\n    /// that the logger will use when emitting logs as extra fields\n    pub fn with(&self, fields: Fields) -> Self {\n        let mut replacement = self.clone();\n\n        for (key, value) in fields.iter() {\n            replacement\n                .extra_fields\n                .insert(key.to_string(), value.clone());\n        }\n\n        replacement\n    }\n\n    /// Returns the current log level as expected by the `log` crate.\n    fn level_to_value(&self, level: ::log::Level) -> serde_json::Value {\n        serde_json::Value::from(match level {\n            ::log::Level::Trace => self.field_config.level_trace_value,\n            ::log::Level::Debug => self.field_config.level_debug_value,\n            ::log::Level::Info => self.field_config.level_info_value,\n            ::log::Level::Warn => self.field_config.level_warn_value,\n            ::log::Level::Error => self.field_config.level_error_value,\n        })\n    }\n\n    /// Takes the given message and attempts to log it to the configured writer.\n    fn try_log(\n        &self,\n        request: Option<&model::Request>,\n        level: log::Level,\n        msg: String,\n        error: Option<AppError>,\n        caller: Option<String>,\n        fields: Option<Fields>,\n    ) -> anyhow::Result<()> {\n        self.write_to_trace(request, level, &msg, &fields);\n\n        let mut values = Fields::new();\n\n        // Copy the extra fields into the values map.\n        for (key, value) in self.extra_fields.iter() {\n            values.insert(key.to_string(), value.clone());\n        }\n\n        // Copy the fields from the logger into the values map.\n        if let Some(fields) = fields {\n            values.extend(fields);\n        }\n\n        // If we have a caller field, add it to the values map.\n        if let Some(caller) = caller {\n            values.insert(\n                self.field_config.caller_field_name.to_string(),\n                serde_json::Value::from(caller),\n            );\n        }\n\n        // If we have an error field, then let's add it\n        if let Some(error) = error {\n            values.insert(\n                self.field_config.error_field_name.to_string(),\n                serde_json::Value::from(error.message),\n            );\n\n            if !error.stack.is_empty() {\n                values.insert(\n                    self.field_config.stack_trace_field_name.to_string(),\n                    serde_json::to_value(error.stack)?,\n                );\n            }\n        }\n\n        // Now add the standard fields.\n        values.insert(\n            self.field_config.level_field_name.to_string(),\n            self.level_to_value(level),\n        );\n        values.insert(\n            self.field_config.timestamp_field_name.to_string(),\n            iso8601_now(),\n        );\n        values.insert(\n            self.field_config.message_field_name.to_string(),\n            serde_json::Value::from(msg),\n        );\n\n        if let Some(req) = request {\n            match &req.data {\n                model::RequestData::RPC(rpc) => {\n                    let ep = &rpc.endpoint.name;\n                    values.insert(\n                        \"service\".into(),\n                        serde_json::Value::String(ep.service().to_string()),\n                    );\n                    values.insert(\n                        \"endpoint\".into(),\n                        serde_json::Value::String(ep.endpoint().to_string()),\n                    );\n                    if let Some(uid) = &rpc.auth_user_id {\n                        values.insert(\"uid\".into(), serde_json::Value::String(uid.clone()));\n                    }\n                }\n                model::RequestData::Auth(auth) => {\n                    let ep = &auth.auth_handler;\n                    values.insert(\n                        \"service\".into(),\n                        serde_json::Value::String(ep.service().to_string()),\n                    );\n                    values.insert(\n                        \"endpoint\".into(),\n                        serde_json::Value::String(ep.endpoint().to_string()),\n                    );\n                }\n                model::RequestData::PubSub(msg) => {\n                    values.insert(\n                        \"service\".into(),\n                        serde_json::Value::String(msg.service.to_string()),\n                    );\n                    values.insert(\n                        \"topic\".into(),\n                        serde_json::Value::String(msg.topic.to_string()),\n                    );\n                    values.insert(\n                        \"subscription\".into(),\n                        serde_json::Value::String(msg.subscription.to_string()),\n                    );\n                }\n                model::RequestData::Stream(data) => {\n                    let ep = &data.endpoint.name;\n                    values.insert(\n                        \"service\".into(),\n                        serde_json::Value::String(ep.service().to_string()),\n                    );\n                    values.insert(\n                        \"endpoint\".into(),\n                        serde_json::Value::String(ep.endpoint().to_string()),\n                    );\n                }\n            };\n\n            values.insert(\n                \"trace_id\".into(),\n                serde_json::Value::String(req.span.0.serialize_encore()),\n            );\n            values.insert(\n                \"span_id\".into(),\n                serde_json::Value::String(req.span.1.serialize_encore()),\n            );\n\n            if let Some(corr_id) = &req.ext_correlation_id {\n                values.insert(\n                    \"x_correlation_id\".into(),\n                    serde_json::Value::String(corr_id.clone()),\n                );\n            } else if let Some(parent_trace) = &req.parent_trace {\n                values.insert(\n                    \"x_correlation_id\".into(),\n                    serde_json::Value::String(parent_trace.serialize_encore()),\n                );\n            }\n        }\n\n        // Now write the log to the configured writer.\n        self.writer\n            .write(level, &values)\n            .context(\"unable to write\")?;\n\n        Ok(())\n    }\n\n    /// Takes a `log::Record` and attempts to log it to the configured writer.\n    fn try_log_record(&self, record: &Record) -> anyhow::Result<()> {\n        let kvs = record.key_values();\n        let mut visitor = KeyValueVisitor(BTreeMap::new());\n        let _ = kvs.visit(&mut visitor);\n\n        let msg = match record.args().as_str() {\n            Some(msg) => msg.to_string(),\n            None => record.args().to_string(),\n        };\n\n        let caller = match (record.module_path(), record.file(), record.line()) {\n            (Some(module), _, _) => Some(module.to_string()),\n            (_, Some(file), Some(line)) => Some(format!(\"{file}:{line}\")),\n            _ => None,\n        };\n\n        self.try_log(None, record.level(), msg, None, caller, Some(visitor.0))\n    }\n\n    /// Writes the log to trace\n    fn write_to_trace(\n        &self,\n        request: Option<&model::Request>,\n        level: log::Level,\n        msg: &str,\n        fields: &Option<Fields>,\n    ) {\n        let fields = if !self.extra_fields.is_empty() || fields.is_some() {\n            let field_vec: Vec<_> = self\n                .extra_fields\n                .iter()\n                .chain(fields.iter().flat_map(|f| f.iter()))\n                .map(|(key, val)| match val {\n                    serde_json::Value::Number(num) => {\n                        if num.is_i64() {\n                            LogField {\n                                key,\n                                value: model::LogFieldValue::I64(num.as_i64().unwrap()),\n                            }\n                        } else if num.is_u64() {\n                            LogField {\n                                key,\n                                value: model::LogFieldValue::U64(num.as_u64().unwrap()),\n                            }\n                        } else if num.is_f64() {\n                            LogField {\n                                key,\n                                value: model::LogFieldValue::F64(num.as_f64().unwrap()),\n                            }\n                        } else {\n                            // this can't happen as we have handle all the cases above,\n                            // but we need to handle this case for the iterator to function\n                            LogField {\n                                key,\n                                value: model::LogFieldValue::Json(&serde_json::Value::Null),\n                            }\n                        }\n                    }\n                    serde_json::Value::Bool(b) => LogField {\n                        key,\n                        value: model::LogFieldValue::Bool(*b),\n                    },\n                    serde_json::Value::String(ref str) => LogField {\n                        key,\n                        value: model::LogFieldValue::String(str),\n                    },\n                    serde_json::Value::Array(_)\n                    | serde_json::Value::Object(_)\n                    | serde_json::Value::Null => LogField {\n                        key,\n                        value: model::LogFieldValue::Json(val),\n                    },\n                })\n                .collect();\n            Some(field_vec.into_iter())\n        } else {\n            None\n        };\n\n        self.tracer\n            .read()\n            .expect(\"tracer lock poisoned\")\n            .log_message(LogMessageData {\n                source: request,\n                msg,\n                level,\n                fields,\n            });\n    }\n}\n\n/// This trait defines the logging functions that are available on the `Logger` type.\n///\n/// It is used to allow Rust code to emit structured logs via our `Logger` implementation\n/// as it will automatically capture the caller location.\npub trait LogFromRust<T: std::fmt::Display> {\n    fn trace(&self, req: Option<&model::Request>, msg: T, fields: Option<Fields>);\n    fn debug(&self, req: Option<&model::Request>, msg: T, fields: Option<Fields>);\n\n    fn info(&self, req: Option<&model::Request>, msg: T, fields: Option<Fields>);\n\n    fn warn<Err: Into<AppError>>(\n        &self,\n        req: Option<&model::Request>,\n        msg: T,\n        error: Option<Err>,\n        fields: Option<Fields>,\n    );\n\n    fn error<Err: Into<AppError>>(\n        &self,\n        req: Option<&model::Request>,\n        msg: T,\n        error: Option<Err>,\n        fields: Option<Fields>,\n    );\n}\n\n/// This trait defines the logging functions that are available on the `Logger` type.\n///\n/// It is used to allow other languages to emit structured logs via our `Logger` implementation\n/// with them passing in their own caller location.\npub trait LogFromExternalRuntime<T: std::fmt::Display> {\n    fn log<Err: Into<AppError>>(\n        &self,\n        request: Option<&model::Request>,\n        level: log::Level,\n        msg: T,\n        error: Option<Err>,\n        caller: Option<String>,\n        fields: Option<Fields>,\n    ) -> anyhow::Result<()>;\n}\n\nimpl<T> LogFromExternalRuntime<T> for Logger\nwhere\n    T: std::fmt::Display,\n{\n    /// Logs the given message at the trace level\n    fn log<Err: Into<AppError>>(\n        &self,\n        request: Option<&model::Request>,\n        level: log::Level,\n        msg: T,\n        error: Option<Err>,\n        caller: Option<String>,\n        fields: Option<Fields>,\n    ) -> anyhow::Result<()> {\n        if level > self.app_level {\n            return Ok(());\n        }\n\n        self.try_log(\n            request,\n            level,\n            msg.to_string(),\n            error.map(|e| e.into().trim_stack(file!(), line!(), 1)),\n            caller,\n            fields,\n        )\n    }\n}\n\nimpl<T> LogFromRust<T> for Logger\nwhere\n    T: std::fmt::Display,\n{\n    #[track_caller]\n    fn trace(&self, req: Option<&model::Request>, msg: T, fields: Option<Fields>) {\n        if log::Level::Trace > self.app_level {\n            return;\n        }\n\n        self.try_log(req, log::Level::Trace, msg.to_string(), None, None, fields)\n            .expect(\"failed to log\");\n    }\n\n    #[track_caller]\n    fn debug(&self, req: Option<&model::Request>, msg: T, fields: Option<Fields>) {\n        if log::Level::Debug > self.app_level {\n            return;\n        }\n\n        self.try_log(req, log::Level::Debug, msg.to_string(), None, None, fields)\n            .expect(\"failed to log\");\n    }\n\n    #[track_caller]\n    fn info(&self, req: Option<&model::Request>, msg: T, fields: Option<Fields>) {\n        if log::Level::Info > self.app_level {\n            return;\n        }\n\n        self.try_log(req, log::Level::Info, msg.to_string(), None, None, fields)\n            .expect(\"failed to log\");\n    }\n\n    #[track_caller]\n    fn warn<Err: Into<AppError>>(\n        &self,\n        req: Option<&model::Request>,\n        msg: T,\n        error: Option<Err>,\n        fields: Option<Fields>,\n    ) {\n        if log::Level::Warn > self.app_level {\n            return;\n        }\n\n        self.try_log(\n            req,\n            log::Level::Warn,\n            msg.to_string(),\n            error.map(|e| e.into().trim_stack(file!(), line!(), 1)),\n            None,\n            fields,\n        )\n        .expect(\"failed to log\");\n    }\n\n    #[track_caller]\n    fn error<Err: Into<AppError>>(\n        &self,\n        req: Option<&model::Request>,\n        msg: T,\n        error: Option<Err>,\n        fields: Option<Fields>,\n    ) {\n        if log::Level::Error > self.app_level {\n            return;\n        }\n\n        self.try_log(\n            req,\n            log::Level::Error,\n            msg.to_string(),\n            error.map(|e| e.into().trim_stack(file!(), line!(), 1)),\n            None,\n            fields,\n        )\n        .expect(\"failed to log\");\n    }\n}\n\n/// Returns the current unix timestamp in milliseconds.\n#[inline]\npub fn iso8601_now() -> serde_json::Value {\n    let now = SystemTime::now();\n    let date = chrono::DateTime::<chrono::Utc>::from(now);\n\n    serde_json::Value::from(date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))\n}\n\n/// Implement the `Log` trait for `Logger` which allows other creates which use the `log` facade\n/// crate to emit structured logs via our `Logger` implementation.\nimpl Log for Logger {\n    fn enabled(&self, metadata: &Metadata) -> bool {\n        self.filter.enabled(metadata)\n    }\n\n    fn log(&self, record: &Record) {\n        if self.enabled(record.metadata()) {\n            self.try_log_record(record).unwrap_or_else(|e| {\n                eprintln!(\"failed to log: {e}\");\n            });\n        }\n    }\n\n    fn flush(&self) {}\n}\n\n/// A visitor that can be used to visit key-value pairs and insert them into a `BTreeMap`.\n/// after converting them from the `log::kv::Value` type to `serde_json::Value`.\nstruct KeyValueVisitor(BTreeMap<String, serde_json::Value>);\n\nimpl log::kv::Visitor<'_> for KeyValueVisitor {\n    #[inline]\n    fn visit_pair(\n        &mut self,\n        key: log::kv::Key,\n        value: log::kv::Value,\n    ) -> Result<(), log::kv::Error> {\n        match serde_json::to_value(value) {\n            Ok(value) => {\n                self.0.insert(key.to_string(), value);\n                Ok(())\n            }\n            Err(e) => Err(log::kv::Error::boxed(e)),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/log/mod.rs",
    "content": "use once_cell::sync::OnceCell;\n\nmod consolewriter;\nmod fields;\nmod logger;\nmod writers;\n\nuse crate::log::fields::FieldConfig;\npub use logger::{Fields, LogFromExternalRuntime, LogFromRust, Logger};\n\nuse crate::trace::Tracer;\n\n/// The global root logger instance that is used by both the `log` crate\n/// and all other code in the Encore runtime.\nstatic ROOT: OnceCell<&Logger> = OnceCell::new();\n\n/// Initialize the global logger with the `root()` instance\n///\n/// This function is idempotent and will not re-initialize the logger\n/// if it has already been initialized.\npub fn init() {\n    // Initialize the logger first.\n    _ = root();\n\n    // Set a custom panic hook to ensure panics are logged at error level.\n    // We write directly to stderr in JSON format to ensure the message\n    // is properly captured by log aggregators like Cloud Run.\n    std::panic::set_hook(Box::new(|info| {\n        use std::io::Write;\n\n        let msg = info.to_string();\n        let location = info\n            .location()\n            .map(|l| format!(\"{}:{}:{}\", l.file(), l.line(), l.column()));\n\n        // Write JSON directly to stderr to ensure proper log level detection.\n        let json = serde_json::json!({\n            \"level\": \"error\",\n            \"severity\": \"ERROR\",\n            \"message\": msg,\n            \"caller\": location,\n            \"time\": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),\n        });\n        let _ = writeln!(std::io::stderr(), \"{}\", json);\n    }));\n}\n\n/// Set the tracer on the global logger\npub fn set_tracer(tracer: Tracer) {\n    root().set_tracer(tracer);\n}\n\n/// Returns a reference to the global root logger instance.\npub fn root() -> &'static Logger {\n    ROOT.get_or_init(|| {\n        let logger = {\n            let fields = FieldConfig::default();\n\n            // Construct our rust log filter.\n            let filter = {\n                // If RUST_LOG is set, use that.\n                let level = std::env::var(\"RUST_LOG\").unwrap_or_else(|_| {\n                    // Otherwise use ENCORE_RUNTIME_LOG to set the Encore runtime log level,\n                    // which defaults\n                    let level = std::env::var(\"ENCORE_RUNTIME_LOG\").unwrap_or(\"debug\".to_string());\n                    format!(\"encore_={level},pingora_core::listeners=warn,pingora_core::services::listening=warn,tokio_postgres::proxy={level},tokio_postgres::connect_proxy={level}\")\n                });\n                env_logger::filter::Builder::new().parse(&level).build()\n            };\n\n            // Construct our app log level.\n            let app_level: log::LevelFilter = std::env::var(\"ENCORE_LOG\")\n                .ok()\n                .and_then(|v| v.parse().ok())\n                .unwrap_or(log::LevelFilter::Trace);\n\n            Logger::new(app_level, filter, fields)\n        };\n\n        // Leak the logger to ensure it has a static lifetime.\n        // We only do this once.\n        let logger = Box::leak(Box::new(logger));\n\n        let disable_logging = std::env::var(\"ENCORE_NOLOG\").is_ok_and(|v| !v.is_empty());\n        let filter = if disable_logging {\n            log::LevelFilter::Off\n        } else {\n            log::LevelFilter::Trace\n        };\n\n        #[cfg(feature = \"rttrace\")]\n        {\n            let filter = tracing_subscriber::EnvFilter::from_env(\"ENCORE_RUNTIME_TRACE\");\n            tracing_subscriber::fmt()\n                .with_span_events(tracing_subscriber::fmt::format::FmtSpan::ENTER)\n                .with_env_filter(filter)\n                .with_writer(std::io::stderr)\n                .init();\n        }\n\n        log::set_max_level(filter);\n        log::set_logger(logger).expect(\"unable to set global logger instance\");\n        logger\n    })\n}\n"
  },
  {
    "path": "runtimes/core/src/log/writers.rs",
    "content": "use crate::log::consolewriter::ConsoleWriter;\nuse crate::log::fields::FieldConfig;\nuse anyhow::Context;\nuse serde_json::Value;\nuse std::collections::BTreeMap;\nuse std::env;\nuse std::fmt::Debug;\nuse std::io::{IoSlice, Write};\nuse std::sync::mpsc::{self, Receiver, RecvError, SyncSender, TryRecvError};\nuse std::sync::Arc;\nuse std::time::Duration;\n\n/// A log writer.\npub trait Writer: Send + Sync + 'static {\n    /// Write the given key-value pairs to the log.\n    fn write(&self, level: log::Level, values: &BTreeMap<String, Value>) -> anyhow::Result<()>;\n}\n\nimpl Debug for dyn Writer {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"Writer\").finish()\n    }\n}\n\n/// default_writer returns the default writer based on the environment.\n///\n/// If the `ENCORE_LOG_FORMAT` environment variable is set to `console` then\n/// the pretty console writer will be used to write logs to stderr, otherwise\n/// JSONL logs will be written to stderr.\n///\n/// For JSONL logs, if a tokio runtime is detected then the async writer\n/// will be used, otherwise a blocking writer will be used, resulting\n/// in blocking writes to stderr.\npub fn default_writer(fields: &'static FieldConfig) -> Arc<dyn Writer> {\n    // Check if the user has set the `ENCORE_LOG_FORMAT` environment variable to `console`.\n    // if so we'll use the pretty console writer.\n    for var in &[\"ENCORE_LOG_FORMAT\"] {\n        if let Ok(format) = env::var(var) {\n            if format == \"console\" {\n                return Arc::new(ConsoleWriter::new(fields, std::io::stderr()));\n            }\n        }\n    }\n\n    Arc::new(ActorWriter::default())\n}\n\n// ActorWriter creates a bounded channel that sends log data to a separate thread that handles the writing.\npub struct ActorWriter {\n    sender: SyncSender<Vec<u8>>,\n}\nimpl ActorWriter {\n    pub fn new<W: Write + Sync + Send + 'static>(mut writer: W) -> Self {\n        let (sender, recv) = mpsc::sync_channel::<Vec<u8>>(10_000);\n        std::thread::spawn(move || {\n            while let Ok(bytes) = Self::recv_batch(&recv) {\n                Self::write_batch_with_retry(&mut writer, &bytes);\n            }\n        });\n        Self { sender }\n    }\n\n    fn recv_batch(recv: &Receiver<Vec<u8>>) -> Result<Vec<Vec<u8>>, RecvError> {\n        const MAX_BATCH_SIZE: usize = 256;\n\n        // wait for a log message\n        let mut bufs = vec![recv.recv()?];\n\n        // receive logs until channel is empty or max batch size is reached\n        loop {\n            match recv.try_recv() {\n                Ok(log) => {\n                    bufs.push(log);\n\n                    if bufs.len() >= MAX_BATCH_SIZE {\n                        break;\n                    }\n                }\n                // on error, break the loop and return the bufs that we have already collected.\n                Err(TryRecvError::Disconnected) => break,\n                Err(TryRecvError::Empty) => break,\n            }\n        }\n        Ok(bufs)\n    }\n\n    fn write_batch_with_retry<W: Write>(writer: &mut W, bufs: &[Vec<u8>]) {\n        const INITIAL_DELAY_MS: u64 = 1;\n        const MAX_DELAY_MS: u64 = 1000;\n\n        let mut io_slices = bufs\n            .iter()\n            .map(|buf| IoSlice::new(buf))\n            .collect::<Vec<IoSlice>>();\n        let mut bufs = &mut io_slices[..];\n\n        // Guarantee that bufs is empty if it contains no data,\n        // to avoid calling write_vectored if there is no data to be written.\n        IoSlice::advance_slices(&mut bufs, 0);\n        let mut delay_ms = INITIAL_DELAY_MS;\n        while !bufs.is_empty() {\n            match writer.write_vectored(bufs) {\n                Ok(0) | Err(_) => {\n                    std::thread::sleep(Duration::from_millis(delay_ms));\n                    delay_ms = u64::min(delay_ms * 2, MAX_DELAY_MS);\n                }\n                Ok(n) => {\n                    delay_ms = INITIAL_DELAY_MS;\n                    IoSlice::advance_slices(&mut bufs, n)\n                }\n            }\n        }\n    }\n}\nimpl Writer for ActorWriter {\n    fn write(&self, _: log::Level, values: &BTreeMap<String, Value>) -> anyhow::Result<()> {\n        let mut buf = Vec::with_capacity(256);\n        serde_json::to_writer(&mut buf, values)\n            .map_err(std::io::Error::from)\n            .context(\"serde_writer\")?;\n        buf.extend_from_slice(b\"\\n\");\n\n        self.sender.send(buf)?;\n        Ok(())\n    }\n}\n\nimpl Default for ActorWriter {\n    fn default() -> Self {\n        Self::new(std::io::stderr())\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/meta/mod.rs",
    "content": "use serde::Serialize;\n\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as rt;\n\n#[derive(Debug, Clone, Default, Serialize)]\npub struct AppMeta {\n    /// The Encore application ID. If the application is not linked to the Encore platform this will be an empty string.\n    /// To link to the Encore platform run `encore app link` from your terminal in the root directory of the Encore app.\n    pub app_id: String,\n\n    /// The base URL which can be used to call the API of this running application.\n    ///\n    /// For local development it is \"http://localhost:<port>\", typically \"http://localhost:4000\".\n    ///\n    /// If a custom domain is used for this environment it is returned here, but note that\n    /// changes only take effect at the time of deployment while custom domains can be updated at any time.\n    pub api_base_url: String,\n\n    /// Information about the environment the app is running in.\n    pub environment: EnvironmentMeta,\n\n    /// Information about the build.\n    pub build: BuildMeta,\n\n    /// Information about the deployment.\n    pub deploy: DeployMeta,\n}\n\nimpl AppMeta {\n    pub fn new(rt: &rt::RuntimeConfig, md: &meta::Data) -> AppMeta {\n        let env = rt.environment.as_ref();\n\n        let app_id = env.map(|e| e.app_id.clone()).unwrap_or_default();\n\n        let api_base_url = rt\n            .infra\n            .as_ref()\n            .and_then(|infra| infra.resources.as_ref())\n            .and_then(|res| res.gateways.first())\n            .map(|gw| gw.base_url.clone())\n            .unwrap_or_default();\n\n        AppMeta {\n            app_id,\n            api_base_url,\n            environment: env.map_or_else(EnvironmentMeta::default, EnvironmentMeta::from),\n            build: BuildMeta::from(md),\n            deploy: rt\n                .deployment\n                .as_ref()\n                .map_or_else(DeployMeta::default, DeployMeta::from),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, Serialize)]\npub struct EnvironmentMeta {\n    /// The name of environment that this application.\n    /// For local development it is \"local\".\n    pub name: String,\n\n    /// The type of environment is this application running in.\n    /// For local development it is \"development\".\n    pub r#type: EnvironmentType,\n\n    /// The cloud this is running in.\n    /// For local development it is \"local\".\n    pub cloud: CloudProvider,\n}\n\nimpl From<&rt::Environment> for EnvironmentMeta {\n    fn from(env: &rt::Environment) -> Self {\n        let env_type = rt::environment::Type::try_from(env.env_type)\n            .unwrap_or(rt::environment::Type::Unspecified);\n        let cloud = rt::environment::Cloud::try_from(env.cloud)\n            .unwrap_or(rt::environment::Cloud::Unspecified);\n        EnvironmentMeta {\n            name: env.env_name.clone(),\n            r#type: EnvironmentType::from(&env_type),\n            cloud: CloudProvider::from(&cloud),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"lowercase\")]\n#[derive(Default)]\npub enum EnvironmentType {\n    // A production environment.\n    Production,\n    // A long-lived cloud-hosted, non-production environment, such as test environments, or local development.\n    #[default]\n    Development,\n    // A short-lived cloud-hosted, non-production environments, such as preview environments\n    Ephemeral,\n    // When running automated tests.\n    Test,\n}\n\nimpl From<&rt::environment::Type> for EnvironmentType {\n    fn from(env: &rt::environment::Type) -> Self {\n        use rt::environment::Type::*;\n        match env {\n            Production => Self::Production,\n            Ephemeral => Self::Ephemeral,\n            Test => Self::Test,\n            Development | Unspecified => Self::Development,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Serialize)]\n#[serde(rename_all = \"lowercase\")]\n#[derive(Default)]\npub enum CloudProvider {\n    AWS,\n    GCP,\n    Azure,\n    Encore,\n    #[default]\n    Local,\n}\n\nimpl From<&rt::environment::Cloud> for CloudProvider {\n    fn from(cloud: &rt::environment::Cloud) -> Self {\n        use rt::environment::Cloud::*;\n        match cloud {\n            Aws => Self::AWS,\n            Gcp => Self::GCP,\n            Azure => Self::Azure,\n            Encore => Self::Encore,\n            Local | Unspecified => Self::Local,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Default, Serialize)]\npub struct BuildMeta {\n    // The git commit that formed the base of this build.\n    pub revision: String,\n    // Whether there were uncommitted changes on top of the commit.\n    pub uncommitted_changes: bool,\n}\n\nimpl From<&meta::Data> for BuildMeta {\n    fn from(md: &meta::Data) -> Self {\n        BuildMeta {\n            revision: md.app_revision.clone(),\n            uncommitted_changes: md.uncommitted_changes,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct HostedService {\n    // The name of the service\n    pub name: String,\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct DeployMeta {\n    // The unique id of the deployment. Generated by the Encore Platform.\n    pub id: String,\n    // The time the deployment was made.\n    pub deploy_time: chrono::DateTime<chrono::Utc>,\n    // The services hosted by this deployment.\n    pub hosted_services: Vec<HostedService>,\n}\n\nimpl From<&rt::Deployment> for DeployMeta {\n    fn from(rt: &rt::Deployment) -> Self {\n        DeployMeta {\n            id: rt.deploy_id.clone(),\n            deploy_time: rt\n                .deployed_at\n                .as_ref()\n                .and_then(|d| chrono::DateTime::from_timestamp(d.seconds, d.nanos as u32))\n                .unwrap_or_else(chrono::Utc::now),\n            hosted_services: rt\n                .hosted_services\n                .iter()\n                .map(|s| HostedService {\n                    name: s.name.clone(),\n                })\n                .collect(),\n        }\n    }\n}\n\nimpl Default for DeployMeta {\n    fn default() -> Self {\n        DeployMeta {\n            id: \"\".into(),\n            deploy_time: chrono::Utc::now(),\n            hosted_services: vec![],\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metadata/aws.rs",
    "content": "use std::time::Duration;\n\nuse anyhow::Context;\n\n#[derive(serde::Deserialize)]\npub struct AwsTaskMeta {\n    #[serde(rename = \"ServiceName\")]\n    pub service_name: String,\n    #[serde(rename = \"Revision\")]\n    pub revision: String,\n    #[serde(rename = \"TaskARN\")]\n    pub task_arn: String,\n}\n\n#[derive(Debug)]\npub struct AwsMetadataClient {\n    http_client: reqwest::Client,\n    metadata_uri: String,\n}\n\nimpl AwsMetadataClient {\n    pub fn new(http_client: reqwest::Client, metadata_uri: String) -> Self {\n        AwsMetadataClient {\n            http_client,\n            metadata_uri,\n        }\n    }\n\n    pub async fn fetch_task_meta(&self) -> anyhow::Result<AwsTaskMeta> {\n        let req = self\n            .http_client\n            .get(format!(\"{}/task\", self.metadata_uri))\n            .timeout(Duration::from_secs(30))\n            .build()\n            .context(\"create metadata request\")?;\n\n        let resp = self\n            .http_client\n            .execute(req)\n            .await\n            .context(\"send metadata request\")?;\n\n        let task_meta = resp\n            .json::<AwsTaskMeta>()\n            .await\n            .context(\"deserialize task metadata\")?;\n\n        Ok(task_meta)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metadata/gce.rs",
    "content": "//! GCE metadata client implementation based on golangs \"cloud.google.com/go/compute/metadata\"\n\nuse std::time::Duration;\nuse tokio::sync::OnceCell;\n\n#[derive(Debug, thiserror::Error)]\npub enum GceMetadataError {\n    #[error(\"HTTP request failed: {0}\")]\n    HttpRequest(#[from] reqwest::Error),\n    #[error(\"GCE metadata not defined (404)\")]\n    NotDefined,\n    #[error(\"GCE metadata server temporarily unavailable (503)\")]\n    ServiceUnavailable,\n    #[error(\"GCE metadata server returned error status: {status}\")]\n    HttpStatus { status: reqwest::StatusCode },\n    #[error(\"Failed to read response body: {0}\")]\n    ResponseBody(reqwest::Error),\n}\n\n// Default metadata server IP as documented by Google\nconst METADATA_IP: &str = \"169.254.169.254\";\n// Env to override metadata server host, if not set the METADATA_IP will be used\nconst METADATA_HOST_ENV: &str = \"GCE_METADATA_HOST\";\nconst METADATA_FLAVOR_HEADER: &str = \"Metadata-Flavor\";\nconst GOOGLE_HEADER_VALUE: &str = \"Google\";\nconst USER_AGENT: &str = \"encore-runtime/0.1.0\";\n// Timeout and retry configuration\nconst REQUEST_TIMEOUT: Duration = Duration::from_secs(5);\nconst MAX_RETRIES: usize = 3;\n\n// Global cache for instance ID to ensure it's shared across all calls\nstatic INSTANCE_ID_CACHE: OnceCell<String> = OnceCell::const_new();\n\n#[derive(Debug)]\npub struct GceMetadataClient {\n    http_client: reqwest::Client,\n}\n\nimpl GceMetadataClient {\n    pub fn new(http_client: reqwest::Client) -> Self {\n        Self { http_client }\n    }\n\n    /// Build metadata URL, checking GCE_METADATA_HOST environment variable first\n    fn build_metadata_url(path: &str) -> String {\n        let host = std::env::var(METADATA_HOST_ENV).unwrap_or_else(|_| METADATA_IP.to_string());\n        let path = path.trim_start_matches('/'); // Remove leading slashes like Go does\n        format!(\"http://{}/computeMetadata/v1/{}\", host, path)\n    }\n\n    /// Get the instance ID from GCE metadata server, with global caching\n    /// This ensures only one HTTP request is made even with concurrent calls\n    pub async fn instance_id(&self) -> Result<String, GceMetadataError> {\n        let instance_id = INSTANCE_ID_CACHE\n            .get_or_try_init(|| async { self.fetch_metadata(\"instance/id\").await })\n            .await?;\n\n        Ok(instance_id.clone())\n    }\n\n    /// Fetch metadata from the GCE metadata server\n    pub async fn fetch_metadata(&self, path: &str) -> Result<String, GceMetadataError> {\n        let url = Self::build_metadata_url(path);\n\n        for attempt in 1..=MAX_RETRIES {\n            let result = self.try_fetch(&url).await;\n\n            match &result {\n                Ok(_) => {\n                    return result;\n                }\n                Err(e) if attempt == MAX_RETRIES => {\n                    log::error!(\n                        \"Failed to fetch GCE metadata after {} attempts: {}\",\n                        MAX_RETRIES,\n                        e\n                    );\n                    return result;\n                }\n                Err(e) => {\n                    log::warn!(\"Attempt {} failed: {}, retrying...\", attempt, e);\n                    tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;\n                }\n            }\n        }\n\n        unreachable!(\"unexpected failure fetching metadata\")\n    }\n\n    async fn try_fetch(&self, url: &str) -> Result<String, GceMetadataError> {\n        let response = self\n            .http_client\n            .get(url)\n            .header(METADATA_FLAVOR_HEADER, GOOGLE_HEADER_VALUE)\n            .header(http::header::USER_AGENT, USER_AGENT)\n            .timeout(REQUEST_TIMEOUT)\n            .send()\n            .await?;\n\n        let status = response.status();\n        if status.is_success() {\n            let text = response\n                .text()\n                .await\n                .map_err(GceMetadataError::ResponseBody)?;\n            Ok(text.trim().to_string())\n        } else if status == reqwest::StatusCode::NOT_FOUND {\n            Err(GceMetadataError::NotDefined)\n        } else if status == reqwest::StatusCode::SERVICE_UNAVAILABLE {\n            Err(GceMetadataError::ServiceUnavailable)\n        } else {\n            Err(GceMetadataError::HttpStatus { status })\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metadata/mod.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::{\n    encore::runtime::v1::{environment::Cloud, Environment},\n    metadata::aws::AwsMetadataClient,\n};\nuse anyhow::Context;\nuse tokio::sync::OnceCell;\n\nmod aws;\nmod gce;\n\n#[derive(Debug)]\npub struct ContainerMetaClient {\n    cell: OnceCell<ContainerMetadata>,\n    env: Environment,\n    http_client: reqwest::Client,\n    fallback: ContainerMetadata,\n}\n\nimpl ContainerMetaClient {\n    pub fn new(env: Environment, http_client: reqwest::Client) -> Self {\n        Self {\n            cell: OnceCell::new(),\n            fallback: ContainerMetadata {\n                env_name: env.env_name.clone(),\n                ..Default::default()\n            },\n            env,\n            http_client,\n        }\n    }\n\n    pub async fn collect(&self) -> anyhow::Result<&ContainerMetadata> {\n        self.cell\n            .get_or_try_init(|| async {\n                ContainerMetadata::collect(&self.env, &self.http_client).await\n            })\n            .await\n    }\n\n    pub fn fallback(&self) -> &ContainerMetadata {\n        &self.fallback\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct ContainerMetadata {\n    pub service_id: String,\n    pub revision_id: String,\n    pub instance_id: String,\n    pub env_name: String,\n}\n\nimpl ContainerMetadata {\n    pub fn labels(&self) -> Vec<(String, String)> {\n        vec![\n            (\"service_id\".to_string(), self.service_id.clone()),\n            (\"revision_id\".to_string(), self.revision_id.clone()),\n            (\"instance_id\".to_string(), self.instance_id.clone()),\n            (\"env_name\".to_string(), self.env_name.clone()),\n        ]\n    }\n\n    pub async fn collect(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result<Self> {\n        match env.cloud() {\n            Cloud::Gcp | Cloud::Encore => Self::collect_gcp(env, http_client).await,\n            Cloud::Aws => Self::collect_aws(env, http_client).await,\n            Cloud::Azure | Cloud::Unspecified | Cloud::Local => anyhow::bail!(\n                \"can't collect container meta in {}\",\n                env.cloud().as_str_name()\n            ),\n        }\n    }\n\n    async fn collect_aws(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result<Self> {\n        // Encore supports running on both ECS Fargate and EKS.\n        // For Fargate, we can get the metadata from the ECS metadata service.\n        // For EKS there doesn't appear to be a standard way to get the metadata, so skip it in that case.\n        let metadata_uri = std::env::var(\"ECS_CONTAINER_METADATA_URI_V4\")\n            .map_err(|_| anyhow::anyhow!(\"unable to get ecs container metadata uri\"))?;\n\n        let client = AwsMetadataClient::new(http_client.clone(), metadata_uri);\n        let task_meta = client.fetch_task_meta().await?;\n\n        let instance_id = task_meta\n            .task_arn\n            .get(task_meta.task_arn.len().saturating_sub(8)..)\n            .unwrap_or(&task_meta.task_arn)\n            .to_string();\n\n        Ok(Self {\n            service_id: task_meta.service_name,\n            revision_id: task_meta.revision,\n            instance_id,\n            env_name: env.env_name.clone(),\n        })\n    }\n\n    async fn collect_gcp(env: &Environment, http_client: &reqwest::Client) -> anyhow::Result<Self> {\n        let service = std::env::var(\"K_SERVICE\").map_err(|_| {\n            anyhow::anyhow!(\"unable to get service ID: env variable 'K_SERVICE' unset\")\n        })?;\n\n        let revision = std::env::var(\"K_REVISION\").map_err(|_| {\n            anyhow::anyhow!(\"unable to get revision ID: env variable 'K_REVISION' unset\")\n        })?;\n\n        let revision = revision\n            .strip_prefix(&format!(\"{}-\", service))\n            .unwrap_or(&revision)\n            .to_string();\n\n        let instance_id = match std::env::var(\"K_POD\") {\n            Ok(pod_id) => {\n                // If we have a K8s POD name, take the last part of it which is the random pod ID\n                // On GKE, the InstanceID appears to be the Node, so if the multiple replicas are running\n                // on the same InstanceID then we'd have a collision. This is unlikely, but possible -\n                // hence why we use the pod ID instead.\n                pod_id\n                    .rsplit('-')\n                    .next()\n                    .ok_or_else(|| anyhow::anyhow!(\"invalid instance ID '{}'\", pod_id))?\n                    .to_string()\n            }\n            Err(_) => {\n                // If we don't have a K8s POD name, we're running on Cloud Run and can get the instance ID from the metadata server\n                let metadata_client = gce::GceMetadataClient::new(http_client.clone());\n\n                metadata_client\n                    .instance_id()\n                    .await\n                    .context(\"failed to get instance ID from GCE metadata server\")?\n            }\n        };\n\n        Ok(Self {\n            service_id: service,\n            revision_id: revision,\n            instance_id,\n            env_name: env.env_name.clone(),\n        })\n    }\n}\n\n/// Process environment variable substitution in labels\n/// Replaces $ENV:VARIABLE_NAME with the actual environment variable value\npub fn process_env_substitution(labels: &mut HashMap<String, String>) {\n    for (_, value) in labels.iter_mut() {\n        if value.starts_with(\"$ENV:\") {\n            let env_var = &value[5..];\n            if let Ok(env_value) = std::env::var(env_var) {\n                *value = env_value;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/atomic.rs",
    "content": "use std::sync::{\n    atomic::{AtomicU64, Ordering},\n    Arc,\n};\n\nuse crate::metrics::{CounterOps, GaugeOps};\n\nimpl CounterOps<u64> for AtomicU64 {\n    fn increment(&self, value: u64) {\n        self.fetch_add(value, Ordering::Release);\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        crate::metrics::MetricValue::CounterU64(self.load(Ordering::Acquire))\n    }\n}\n\nimpl CounterOps<i64> for AtomicU64 {\n    fn increment(&self, value: i64) {\n        self.fetch_add(value as u64, Ordering::Release);\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        crate::metrics::MetricValue::CounterI64(self.load(Ordering::Acquire) as i64)\n    }\n}\n\nimpl<T> CounterOps<T> for Arc<AtomicU64>\nwhere\n    AtomicU64: CounterOps<T>,\n{\n    fn increment(&self, value: T) {\n        CounterOps::<T>::increment(&(**self), value)\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        CounterOps::<T>::get(&(**self))\n    }\n}\n\nimpl GaugeOps<u64> for AtomicU64 {\n    fn set(&self, value: u64) {\n        self.swap(value, Ordering::AcqRel);\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        crate::metrics::MetricValue::GaugeU64(self.load(Ordering::Acquire))\n    }\n}\n\nimpl GaugeOps<i64> for AtomicU64 {\n    fn set(&self, value: i64) {\n        self.swap(value as u64, Ordering::AcqRel);\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        crate::metrics::MetricValue::GaugeI64(self.load(Ordering::Acquire) as i64)\n    }\n}\n\nimpl GaugeOps<f64> for AtomicU64 {\n    fn set(&self, value: f64) {\n        self.swap(value.to_bits(), Ordering::AcqRel);\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        crate::metrics::MetricValue::GaugeF64(f64::from_bits(self.load(Ordering::Acquire)))\n    }\n}\n\nimpl<T> GaugeOps<T> for Arc<AtomicU64>\nwhere\n    AtomicU64: GaugeOps<T>,\n{\n    fn set(&self, value: T) {\n        GaugeOps::<T>::set(&(**self), value)\n    }\n\n    fn get(&self) -> crate::metrics::MetricValue {\n        GaugeOps::<T>::get(&(**self))\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/counter.rs",
    "content": "use malachite::base::num::basic::traits::One;\n\nuse crate::metrics;\nuse std::collections::{HashMap, HashSet};\nuse std::marker::PhantomData;\nuse std::sync::atomic::AtomicU64;\nuse std::sync::Arc;\n\npub trait CounterOps<T> {\n    fn increment(&self, value: T);\n    fn get(&self) -> crate::metrics::MetricValue;\n}\n\n/// A typed counter that can be incremented\n/// T must be compatible with CounterOps for type-safe operations\npub struct Counter<T> {\n    atomic: Arc<AtomicU64>,\n    _phantom: PhantomData<T>,\n}\n\nimpl<T> Counter<T>\nwhere\n    Arc<AtomicU64>: CounterOps<T>,\n    T: One,\n{\n    /// Create a new counter with the given atomic storage\n    /// This is typically called by Registry, not directly by users\n    pub(crate) fn new(atomic: Arc<AtomicU64>) -> Self {\n        Self {\n            atomic,\n            _phantom: PhantomData,\n        }\n    }\n\n    /// Increment the counter by 1\n    pub fn increment(&self) {\n        CounterOps::increment(&self.atomic, T::ONE);\n    }\n\n    /// Get the current value of the counter\n    pub fn get(&self) -> metrics::MetricValue {\n        CounterOps::get(&self.atomic)\n    }\n}\n\n/// A counter schema that defines static labels and required dynamic label keys\n/// Validates dynamic labels at increment time and creates separate time series\n/// for each unique combination of static + dynamic labels\n#[derive(Clone, Debug)]\npub struct Schema<T> {\n    name: String,\n    static_labels: Vec<(String, String)>,\n    required_dynamic_keys: HashSet<String>,\n    registry: Arc<metrics::Registry>,\n    _phantom: PhantomData<T>,\n}\n\nimpl<T> Schema<T>\nwhere\n    Arc<AtomicU64>: CounterOps<T>,\n    T: One + Send + Sync + 'static,\n{\n    /// Create a new counter schema\n    pub(crate) fn new(\n        name: String,\n        static_labels: Vec<(String, String)>,\n        required_dynamic_keys: HashSet<String>,\n        registry: Arc<metrics::Registry>,\n    ) -> Self {\n        Self {\n            name,\n            static_labels,\n            required_dynamic_keys,\n            registry,\n            _phantom: PhantomData,\n        }\n    }\n\n    pub fn with<L, K, V>(&self, dynamic_labels: L) -> Counter<T>\n    where\n        L: IntoIterator<Item = (K, V)>,\n        K: Into<String>,\n        V: Into<String>,\n    {\n        // Convert dynamic_labels to HashMap first\n        let dynamic_labels_map: HashMap<String, String> = dynamic_labels\n            .into_iter()\n            .map(|(k, v)| (k.into(), v.into()))\n            .collect();\n\n        // Validate required keys are present\n        let missing: Vec<String> = self\n            .required_dynamic_keys\n            .iter()\n            .filter(|key| !dynamic_labels_map.contains_key(*key))\n            .cloned()\n            .collect();\n\n        if !missing.is_empty() {\n            log::warn!(\n                \"missing required dynamic labels for metric '{}': {:?}, required keys: {:?}\",\n                self.name,\n                missing,\n                self.required_dynamic_keys\n            );\n        }\n\n        self.get_or_create_counter(dynamic_labels_map)\n    }\n\n    /// Increment the counter with the given dynamic labels\n    pub fn increment(&self)\n    where\n        T: One,\n    {\n        if !self.required_dynamic_keys.is_empty() {\n            log::warn!(\n                \"incrementing counter '{}' without required dynamic labels, required keys: {:?}\",\n                self.name,\n                self.required_dynamic_keys\n            );\n        }\n\n        self.get_or_create_counter(HashMap::new()).increment();\n    }\n\n    /// Get or create a counter for the given dynamic labels\n    fn get_or_create_counter(&self, dynamic_labels: HashMap<String, String>) -> Counter<T> {\n        // Create merged labels (static + dynamic)\n        let mut merged_labels = self.static_labels.clone();\n        for (key, value) in dynamic_labels {\n            merged_labels.push((key, value));\n        }\n\n        self.registry.get_or_create_counter(\n            &self.name,\n            merged_labels.iter().map(|(k, v)| (k.as_str(), v.as_str())),\n        )\n    }\n}\n\n/// Builder for creating counter schemas with static labels and required dynamic keys\npub struct CounterSchemaBuilder<T> {\n    name: String,\n    static_labels: Vec<(String, String)>,\n    required_dynamic_keys: HashSet<String>,\n    registry: Arc<metrics::Registry>,\n    _phantom: std::marker::PhantomData<T>,\n}\n\nimpl<T> CounterSchemaBuilder<T>\nwhere\n    Arc<AtomicU64>: CounterOps<T>,\n    T: One + Send + Sync + 'static,\n{\n    pub(crate) fn new(name: String, registry: Arc<metrics::Registry>) -> Self {\n        Self {\n            name,\n            static_labels: Vec::new(),\n            required_dynamic_keys: HashSet::new(),\n            registry,\n            _phantom: std::marker::PhantomData,\n        }\n    }\n\n    /// Add static labels that are set once when the schema is created\n    pub fn static_labels<I, K, V>(mut self, labels: I) -> Self\n    where\n        I: IntoIterator<Item = (K, V)>,\n        K: AsRef<str>,\n        V: AsRef<str>,\n    {\n        for (key, value) in labels {\n            self.static_labels\n                .push((key.as_ref().to_string(), value.as_ref().to_string()));\n        }\n        self\n    }\n\n    /// Add a single static label\n    pub fn static_label(mut self, key: &str, value: &str) -> Self {\n        self.static_labels\n            .push((key.to_string(), value.to_string()));\n        self\n    }\n\n    /// Specify required dynamic label keys that must be provided at increment time\n    pub fn require_dynamic_keys<I, K>(mut self, keys: I) -> Self\n    where\n        I: IntoIterator<Item = K>,\n        K: AsRef<str>,\n    {\n        for key in keys {\n            self.required_dynamic_keys.insert(key.as_ref().to_string());\n        }\n        self\n    }\n\n    /// Add a single required dynamic key\n    pub fn require_dynamic_key(mut self, key: &str) -> Self {\n        self.required_dynamic_keys.insert(key.to_string());\n        self\n    }\n\n    /// Build the counter schema\n    pub fn build(self) -> Schema<T> {\n        Schema::new(\n            self.name,\n            self.static_labels,\n            self.required_dynamic_keys,\n            self.registry,\n        )\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/exporter/aws.rs",
    "content": "use crate::metadata::ContainerMetaClient;\nuse crate::metrics::exporter::Exporter;\nuse crate::metrics::{CollectedMetric, MetricValue};\nuse anyhow::Context;\nuse aws_sdk_cloudwatch as cloudwatch;\nuse aws_sdk_cloudwatch::types::{Dimension, MetricDatum};\nuse std::sync::Arc;\nuse std::time::SystemTime;\n\n#[derive(Debug)]\npub struct Aws {\n    client: Arc<LazyCloudWatchClient>,\n    namespace: String,\n    container_meta_client: ContainerMetaClient,\n    container_dims: tokio::sync::OnceCell<Arc<Vec<Dimension>>>,\n}\n\n#[derive(Debug)]\nstruct LazyCloudWatchClient {\n    cell: tokio::sync::OnceCell<anyhow::Result<cloudwatch::Client>>,\n}\n\nimpl LazyCloudWatchClient {\n    fn new() -> Self {\n        Self {\n            cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn get(&self) -> &anyhow::Result<cloudwatch::Client> {\n        self.cell\n            .get_or_init(|| async {\n                let config = aws_config::defaults(aws_config::BehaviorVersion::v2025_08_07())\n                    .load()\n                    .await;\n\n                Ok(cloudwatch::Client::new(&config))\n            })\n            .await\n    }\n}\n\nimpl Aws {\n    pub fn new(namespace: String, container_meta_client: ContainerMetaClient) -> Self {\n        Self {\n            client: Arc::new(LazyCloudWatchClient::new()),\n            namespace,\n            container_meta_client,\n            container_dims: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn export_metrics(&self, metrics: Vec<CollectedMetric>) -> anyhow::Result<()> {\n        if metrics.is_empty() {\n            return Ok(());\n        }\n\n        let client = match self.client.get().await {\n            Ok(client) => client,\n            Err(e) => {\n                log::error!(\"failed to get CloudWatch client: {}\", e);\n                return Err(anyhow::anyhow!(\"failed to get CloudWatch client: {}\", e));\n            }\n        };\n\n        log::trace!(\n            \"Exporting {} metrics to AWS CloudWatch namespace {}\",\n            metrics.len(),\n            self.namespace\n        );\n\n        let metric_data = self.get_metric_data(metrics).await;\n\n        // Send metrics in batches (CloudWatch allows up to 1000 metrics per request)\n        for batch in metric_data.chunks(1000) {\n            if let Err(e) = self.send_metric_batch(client, batch.to_vec()).await {\n                log::error!(\"failed to export metrics batch: {}\", e);\n            }\n        }\n\n        Ok(())\n    }\n\n    async fn container_dimensions(&self) -> Arc<Vec<Dimension>> {\n        self.container_dims\n            .get_or_try_init(|| async {\n                let container_metadata = self.container_meta_client.collect().await?;\n                anyhow::Ok(Arc::new(\n                    container_metadata\n                        .labels()\n                        .into_iter()\n                        .map(|(key, value)| Dimension::builder().name(key).value(value).build())\n                        .collect(),\n                ))\n            })\n            .await\n            .map(Arc::clone)\n            .unwrap_or_else(|e| {\n                log::warn!(\"failed fetching container metadata: {e}, using fallback\");\n                Arc::new(\n                    self.container_meta_client\n                        .fallback()\n                        .labels()\n                        .into_iter()\n                        .map(|(key, value)| Dimension::builder().name(key).value(value).build())\n                        .collect(),\n                )\n            })\n    }\n\n    async fn get_metric_data(&self, collected: Vec<CollectedMetric>) -> Vec<MetricDatum> {\n        let now = SystemTime::now();\n        let mut data: Vec<MetricDatum> = Vec::with_capacity(collected.len());\n\n        let container_dims = self.container_dimensions().await;\n        let container_dims_len = container_dims.len();\n\n        for metric in collected {\n            let metric_name = metric.key.name().to_string();\n\n            let mut dimensions: Vec<Dimension> =\n                Vec::with_capacity(container_dims_len + metric.key.labels().count());\n\n            // Add container metadata dimensions\n            dimensions.extend(container_dims.iter().cloned());\n\n            for label in metric.key.labels() {\n                let value = label.value();\n                if value.is_empty() {\n                    log::warn!(\n                        \"Skipping empty label '{}' for metric '{}' - CloudWatch does not support empty dimension values\",\n                        label.key(),\n                        metric_name\n                    );\n                    continue;\n                }\n\n                dimensions.push(Dimension::builder().name(label.key()).value(value).build());\n            }\n\n            let value = match metric.value {\n                MetricValue::CounterU64(val) => val as f64,\n                MetricValue::CounterI64(val) => val as f64,\n                MetricValue::GaugeF64(val) => val,\n                MetricValue::GaugeU64(val) => val as f64,\n                MetricValue::GaugeI64(val) => val as f64,\n            };\n\n            let mut datum_builder = MetricDatum::builder()\n                .metric_name(metric_name)\n                .timestamp(aws_smithy_types::DateTime::from(now))\n                .value(value)\n                .set_dimensions(Some(dimensions));\n\n            // For cumulative counters, include the start time\n            if matches!(\n                metric.value,\n                MetricValue::CounterU64(_) | MetricValue::CounterI64(_)\n            ) {\n                // CloudWatch uses storage resolution to determine how data is aggregated\n                // For counters, we use high resolution (1 second) to better track cumulative values\n                datum_builder = datum_builder.storage_resolution(1);\n            }\n\n            data.push(datum_builder.build())\n        }\n\n        data\n    }\n\n    async fn send_metric_batch(\n        &self,\n        client: &cloudwatch::Client,\n        metric_data: Vec<MetricDatum>,\n    ) -> Result<(), anyhow::Error> {\n        client\n            .put_metric_data()\n            .namespace(&self.namespace)\n            .set_metric_data(Some(metric_data))\n            .send()\n            .await\n            .context(\"send metrics to CloudWatch\")?;\n\n        Ok(())\n    }\n}\n\n#[async_trait::async_trait]\nimpl Exporter for Aws {\n    async fn export(&self, metrics: Vec<CollectedMetric>) {\n        if let Err(err) = self.export_metrics(metrics).await {\n            log::error!(\"Failed to export metrics to AWS CloudWatch: {}\", err);\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/exporter/datadog.rs",
    "content": "use crate::{\n    encore::runtime::v1 as pb,\n    metadata::ContainerMetaClient,\n    metrics::{exporter::Exporter, CollectedMetric, MetricValue},\n    secrets,\n};\nuse anyhow::Context;\nuse dashmap::DashMap;\nuse datadog_api_client::datadog;\nuse datadog_api_client::datadogV2::api_metrics::{MetricsAPI, SubmitMetricsOptionalParams};\nuse datadog_api_client::datadogV2::model::{\n    MetricIntakeType, MetricPayload, MetricPoint, MetricSeries,\n};\nuse std::sync::{Arc, Mutex};\nuse std::time::SystemTime;\n\npub struct Datadog {\n    client: Arc<LazyDatadogClient>,\n    container_meta_client: ContainerMetaClient,\n    last_export: Arc<Mutex<i64>>,\n    last_value: Arc<DashMap<u64, f64>>,\n    container_tags: tokio::sync::OnceCell<Arc<Vec<String>>>,\n}\n\n#[derive(Clone)]\nstruct DatadogClient {\n    config: datadog::Configuration,\n}\n\nimpl DatadogClient {\n    async fn send_metrics(&self, metric_series: Vec<MetricSeries>) -> Result<(), anyhow::Error> {\n        let api = MetricsAPI::with_config(self.config.clone());\n        let payload = MetricPayload::new(metric_series);\n\n        api.submit_metrics(payload, SubmitMetricsOptionalParams::default())\n            .await\n            .context(\"submit metrics to Datadog\")?;\n\n        Ok(())\n    }\n}\n\nstruct LazyDatadogClient {\n    cell: tokio::sync::OnceCell<DatadogClient>,\n    site: String,\n    api_key: String,\n}\n\nimpl LazyDatadogClient {\n    fn new(site: String, api_key: String) -> Self {\n        Self {\n            cell: tokio::sync::OnceCell::new(),\n            site,\n            api_key,\n        }\n    }\n\n    async fn get(&self) -> &DatadogClient {\n        self.cell\n            .get_or_init(|| async {\n                // Create Datadog API client configuration\n                let mut configuration = datadog::Configuration::new();\n                configuration\n                    .server_variables\n                    .insert(\"site\".to_string(), self.site.clone());\n                configuration.set_auth_key(\n                    \"apiKeyAuth\",\n                    datadog::APIKey {\n                        key: self.api_key.clone(),\n                        prefix: String::new(),\n                    },\n                );\n\n                DatadogClient {\n                    config: configuration,\n                }\n            })\n            .await\n    }\n}\n\nimpl Datadog {\n    pub fn new(\n        provider_cfg: &pb::metrics_provider::Datadog,\n        secrets: &secrets::Manager,\n        container_meta_client: ContainerMetaClient,\n    ) -> anyhow::Result<Self> {\n        let api_key = match &provider_cfg.api_key {\n            Some(data) => {\n                let secret = secrets.load(data.clone());\n                let api_key_bytes = secret.get().context(\"failed to resolve Datadog API key\")?;\n                std::str::from_utf8(api_key_bytes)\n                    .context(\"Datadog API key is not valid UTF-8\")?\n                    .to_string()\n            }\n            None => {\n                return Err(anyhow::anyhow!(\"Datadog API key not provided\"));\n            }\n        };\n\n        Ok(Self {\n            client: Arc::new(LazyDatadogClient::new(provider_cfg.site.clone(), api_key)),\n            container_meta_client,\n            last_export: Arc::new(Mutex::new(\n                SystemTime::now()\n                    .duration_since(SystemTime::UNIX_EPOCH)\n                    .expect(\"system time before Unix epoch\")\n                    .as_secs() as i64,\n            )),\n            last_value: Arc::new(DashMap::new()),\n            container_tags: tokio::sync::OnceCell::new(),\n        })\n    }\n\n    async fn export_metrics(&self, metrics: Vec<CollectedMetric>) -> anyhow::Result<()> {\n        if metrics.is_empty() {\n            return Ok(());\n        }\n\n        let client = self.client.get().await;\n\n        log::trace!(\n            \"Exporting {} metrics to Datadog site {}\",\n            metrics.len(),\n            self.client.site\n        );\n\n        let now = SystemTime::now()\n            .duration_since(SystemTime::UNIX_EPOCH)\n            .expect(\"system time before Unix epoch\")\n            .as_secs() as i64;\n\n        let metric_series = self.get_metric_data(metrics, now).await;\n\n        if !metric_series.is_empty() {\n            client.send_metrics(metric_series).await?;\n        }\n\n        // Update last export time\n        if let Ok(mut last_export) = self.last_export.lock() {\n            *last_export = now;\n        }\n\n        Ok(())\n    }\n\n    async fn container_tags_vec(&self) -> Arc<Vec<String>> {\n        self.container_tags\n            .get_or_try_init(|| async {\n                let container_metadata = self.container_meta_client.collect().await?;\n                anyhow::Ok(Arc::new(\n                    container_metadata\n                        .labels()\n                        .into_iter()\n                        .map(|(key, value)| format!(\"{}:{}\", key, value))\n                        .collect(),\n                ))\n            })\n            .await\n            .map(Arc::clone)\n            .unwrap_or_else(|e| {\n                log::warn!(\"failed fetching container metadata: {e}, using fallback\");\n                Arc::new(\n                    self.container_meta_client\n                        .fallback()\n                        .labels()\n                        .into_iter()\n                        .map(|(key, value)| format!(\"{}:{}\", key, value))\n                        .collect(),\n                )\n            })\n    }\n\n    async fn get_metric_data(\n        &self,\n        collected: Vec<CollectedMetric>,\n        now: i64,\n    ) -> Vec<MetricSeries> {\n        let mut data: Vec<MetricSeries> = Vec::with_capacity(collected.len());\n\n        let container_tags = self.container_tags_vec().await;\n        let container_tags_len = container_tags.len();\n\n        let last_export = self.last_export.lock().ok().map(|t| *t).unwrap_or(now);\n        let interval = now - last_export;\n\n        for metric in collected {\n            let metric_name = metric.key.name().to_string();\n\n            // Build tags: container metadata + metric labels\n            let mut tags: Vec<String> =\n                Vec::with_capacity(container_tags_len + metric.key.labels().count());\n            // Add container metadata tags\n            tags.extend(container_tags.iter().cloned());\n\n            for label in metric.key.labels() {\n                tags.push(format!(\"{}:{}\", label.key(), label.value()));\n            }\n\n            let (metric_type, value) = match metric.value {\n                MetricValue::CounterU64(val) => {\n                    let value = val as f64;\n                    let key = metric.key.get_hash();\n                    let last_val = self.last_value.get(&key).map(|v| *v).unwrap_or(0.0);\n                    self.last_value.insert(key, value);\n                    let delta = value - last_val;\n                    (MetricIntakeType::COUNT, delta)\n                }\n                MetricValue::CounterI64(val) => {\n                    let value = val as f64;\n                    let key = metric.key.get_hash();\n                    let last_val = self.last_value.get(&key).map(|v| *v).unwrap_or(0.0);\n                    self.last_value.insert(key, value);\n                    let delta = value - last_val;\n                    (MetricIntakeType::COUNT, delta)\n                }\n                MetricValue::GaugeF64(val) => (MetricIntakeType::GAUGE, val),\n                MetricValue::GaugeU64(val) => (MetricIntakeType::GAUGE, val as f64),\n                MetricValue::GaugeI64(val) => (MetricIntakeType::GAUGE, val as f64),\n            };\n\n            let point = MetricPoint::new().timestamp(now).value(value);\n\n            let series = MetricSeries::new(metric_name, vec![point])\n                .type_(metric_type)\n                .interval(interval)\n                .tags(tags);\n\n            data.push(series);\n        }\n\n        data\n    }\n}\n\n#[async_trait::async_trait]\nimpl Exporter for Datadog {\n    async fn export(&self, metrics: Vec<CollectedMetric>) {\n        if let Err(err) = self.export_metrics(metrics).await {\n            log::error!(\"Failed to export metrics to Datadog: {}\", err);\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/exporter/gcp.rs",
    "content": "use crate::metadata::ContainerMetaClient;\nuse crate::metrics::exporter::Exporter;\nuse crate::metrics::{CollectedMetric, MetricValue};\nuse anyhow::Context;\nuse google_cloud_api::model::metric_descriptor::{MetricKind, ValueType};\nuse google_cloud_api::model::{Metric, MonitoredResource};\nuse google_cloud_monitoring_v3::client::MetricService;\nuse google_cloud_monitoring_v3::model::{Point, TimeInterval, TimeSeries, TypedValue};\nuse std::collections::HashMap;\nuse std::sync::Arc;\nuse std::time::SystemTime;\nuse tokio::sync::OnceCell;\n\ntype LabelPairs = Vec<(String, String)>;\n\n#[derive(Debug)]\npub struct Gcp {\n    client: Arc<LazyMonitoringClient>,\n    project_id: String,\n    monitored_resource_type: String,\n    monitored_resource_labels: HashMap<String, String>,\n    metric_names: HashMap<String, String>,\n    container_meta_client: Arc<ContainerMetaClient>,\n    container_labels: Arc<OnceCell<Arc<LabelPairs>>>,\n}\n\n#[derive(Debug)]\nstruct LazyMonitoringClient {\n    cell: OnceCell<anyhow::Result<MetricService>>,\n}\n\nimpl LazyMonitoringClient {\n    fn new() -> Self {\n        Self {\n            cell: OnceCell::new(),\n        }\n    }\n\n    async fn get(&self) -> &anyhow::Result<MetricService> {\n        self.cell\n            .get_or_init(|| async {\n                MetricService::builder()\n                    .build()\n                    .await\n                    .inspect_err(|e| log::error!(\"Failed to create GCP monitoring client: {e:?}\"))\n                    .context(\"create monitoring client\")\n            })\n            .await\n    }\n}\n\nimpl Gcp {\n    pub fn new(\n        project_id: String,\n        monitored_resource_type: String,\n        monitored_resource_labels: HashMap<String, String>,\n        metric_names: HashMap<String, String>,\n        container_meta_client: ContainerMetaClient,\n    ) -> Self {\n        Self {\n            client: Arc::new(LazyMonitoringClient::new()),\n            project_id,\n            monitored_resource_type,\n            monitored_resource_labels,\n            metric_names,\n            container_meta_client: Arc::new(container_meta_client),\n            container_labels: Arc::new(OnceCell::new()),\n        }\n    }\n\n    async fn export_metrics(&self, metrics: Vec<CollectedMetric>) -> Result<(), anyhow::Error> {\n        if metrics.is_empty() {\n            return Ok(());\n        }\n\n        let client = match self.client.get().await {\n            Ok(client) => client,\n            Err(e) => {\n                log::error!(\"Failed to get monitoring client: {}\", e);\n                return Err(anyhow::anyhow!(\"Failed to get monitoring client: {}\", e));\n            }\n        };\n\n        log::trace!(\n            \"Exporting {} metrics to GCP project {}\",\n            metrics.len(),\n            self.project_id\n        );\n\n        let time_series = self.get_metric_data(metrics).await;\n\n        // Send metrics in batches (Google Cloud allows up to 200 time series per request)\n        for batch in time_series.chunks(200) {\n            if let Err(e) = self.send_time_series_batch(client, batch.to_vec()).await {\n                log::error!(\"Failed to export metrics batch: {}\", e);\n            }\n        }\n\n        Ok(())\n    }\n\n    async fn container_labels(&self) -> Arc<Vec<(String, String)>> {\n        self.container_labels\n            .get_or_try_init(|| async {\n                let container_metadata = self.container_meta_client.collect().await?;\n                anyhow::Ok(Arc::new(container_metadata.labels()))\n            })\n            .await\n            .map(Arc::clone)\n            .unwrap_or_else(|e| {\n                log::warn!(\"failed fetching container metadata: {e}, using fallback\");\n                Arc::new(self.container_meta_client.fallback().labels())\n            })\n    }\n\n    async fn get_metric_data(&self, collected: Vec<CollectedMetric>) -> Vec<TimeSeries> {\n        let end_time = SystemTime::now();\n        let ts_end_time: google_cloud_wkt::Timestamp = end_time.try_into().unwrap_or_default();\n\n        let mut data: Vec<TimeSeries> = Vec::with_capacity(collected.len());\n\n        let container_labels = self.container_labels().await;\n        let container_labels_len = container_labels.len();\n\n        let instance_id = &self\n            .container_meta_client\n            .collect()\n            .await\n            .unwrap_or(self.container_meta_client.fallback())\n            .instance_id;\n\n        for metric in collected {\n            let cloud_metric_name = match self.metric_names.get(metric.key.name()) {\n                Some(name) => name,\n                None => {\n                    log::warn!(\n                        \"Skipping metric '{}' - no cloud metric name configured\",\n                        metric.key.name()\n                    );\n                    continue;\n                }\n            };\n\n            let mut labels =\n                HashMap::with_capacity(container_labels_len + metric.key.labels().len());\n            labels.extend(container_labels.iter().cloned());\n            labels.extend(\n                metric\n                    .key\n                    .labels()\n                    .map(|label| (label.key().to_string(), label.value().to_string())),\n            );\n\n            let (kind, value_type, typed_value, interval) = match metric.value {\n                MetricValue::CounterU64(val) => {\n                    let start_time: google_cloud_wkt::Timestamp =\n                        metric.registered_at.try_into().unwrap_or_default();\n\n                    (\n                        MetricKind::Cumulative,\n                        ValueType::Int64,\n                        TypedValue::new().set_int64_value(val as i64),\n                        TimeInterval::new()\n                            .set_start_time(start_time)\n                            .set_end_time(ts_end_time),\n                    )\n                }\n                MetricValue::CounterI64(val) => {\n                    let start_time: google_cloud_wkt::Timestamp =\n                        metric.registered_at.try_into().unwrap_or_default();\n\n                    (\n                        MetricKind::Cumulative,\n                        ValueType::Int64,\n                        TypedValue::new().set_int64_value(val),\n                        TimeInterval::new()\n                            .set_start_time(start_time)\n                            .set_end_time(ts_end_time),\n                    )\n                }\n                MetricValue::GaugeF64(val) => (\n                    MetricKind::Gauge,\n                    ValueType::Double,\n                    TypedValue::new().set_double_value(val),\n                    TimeInterval::new().set_end_time(ts_end_time),\n                ),\n                MetricValue::GaugeU64(val) => (\n                    MetricKind::Gauge,\n                    ValueType::Int64,\n                    TypedValue::new().set_int64_value(val as i64),\n                    TimeInterval::new().set_end_time(ts_end_time),\n                ),\n                MetricValue::GaugeI64(val) => (\n                    MetricKind::Gauge,\n                    ValueType::Int64,\n                    TypedValue::new().set_int64_value(val),\n                    TimeInterval::new().set_end_time(ts_end_time),\n                ),\n            };\n\n            // Add container instance ID to node_id if present\n            let mut monitored_resource_labels = self.monitored_resource_labels.clone();\n            if let Some(node_id) = monitored_resource_labels.get(\"node_id\") {\n                monitored_resource_labels.insert(\n                    \"node_id\".to_string(),\n                    format!(\"{}-{}\", node_id, instance_id),\n                );\n            }\n\n            let mut mr = MonitoredResource::new().set_type(&self.monitored_resource_type);\n            mr.labels = monitored_resource_labels;\n\n            data.push(\n                TimeSeries::new()\n                    .set_metric_kind(kind)\n                    .set_metric(\n                        Metric::new()\n                            .set_type(format!(\"custom.googleapis.com/{}\", cloud_metric_name))\n                            .set_labels(labels),\n                    )\n                    .set_resource(mr)\n                    .set_value_type(value_type)\n                    .set_points(vec![Point::new()\n                        .set_interval(interval)\n                        .set_value(typed_value)]),\n            );\n        }\n\n        data\n    }\n\n    async fn send_time_series_batch(\n        &self,\n        client: &MetricService,\n        time_series: Vec<google_cloud_monitoring_v3::model::TimeSeries>,\n    ) -> Result<(), anyhow::Error> {\n        client\n            .create_time_series()\n            .set_name(format!(\"projects/{}\", self.project_id))\n            .set_time_series(time_series)\n            .send()\n            .await\n            .map_err(anyhow::Error::new)?;\n\n        Ok(())\n    }\n}\n\n#[async_trait::async_trait]\nimpl Exporter for Gcp {\n    async fn export(&self, metrics: Vec<CollectedMetric>) {\n        if let Err(err) = self.export_metrics(metrics).await {\n            log::error!(\"Failed to export metrics to GCP: {}\", err);\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/exporter/mod.rs",
    "content": "mod aws;\nmod datadog;\nmod gcp;\nmod prometheus;\npub use aws::Aws;\npub use datadog::Datadog;\npub use gcp::Gcp;\npub use prometheus::Prometheus;\n\n#[async_trait::async_trait]\npub trait Exporter: Send + Sync {\n    async fn export(&self, metrics: Vec<crate::metrics::CollectedMetric>);\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/exporter/prometheus.rs",
    "content": "use crate::encore::runtime::v1 as pb;\nuse crate::metadata::ContainerMetaClient;\nuse crate::metrics::exporter::Exporter;\nuse crate::metrics::{CollectedMetric, MetricValue};\nuse crate::secrets;\nuse anyhow::Context;\nuse prost::Message;\nuse std::sync::Arc;\nuse std::time::SystemTime;\nuse tokio::sync::OnceCell;\nuse url::Url;\n\n#[derive(Debug)]\npub struct Prometheus {\n    client: reqwest::Client,\n    remote_write_url: Url,\n    container_meta_client: ContainerMetaClient,\n    container_labels: OnceCell<Arc<Vec<prompb::Label>>>,\n}\n\nimpl Prometheus {\n    pub fn new(\n        provider_cfg: &pb::metrics_provider::PrometheusRemoteWrite,\n        secrets: &secrets::Manager,\n        container_meta_client: ContainerMetaClient,\n    ) -> anyhow::Result<Self> {\n        let remote_write_url = match &provider_cfg.remote_write_url {\n            Some(data) => {\n                let secret = secrets.load(data.clone());\n                let url_bytes = secret\n                    .get()\n                    .context(\"failed to resolve Prometheus Remote Write Url\")?;\n                let s = std::str::from_utf8(url_bytes)\n                    .context(\"Prometheus Remote Write Url is not valid UTF-8\")?;\n                Url::parse(s).context(\"Prometheus Remote Write Url not valid\")?\n            }\n            None => {\n                return Err(anyhow::anyhow!(\"Prometheus Remote Write Url not provided\"));\n            }\n        };\n\n        Ok(Self {\n            client: reqwest::Client::new(),\n            remote_write_url,\n            container_meta_client,\n            container_labels: OnceCell::new(),\n        })\n    }\n\n    async fn export_metrics(&self, metrics: Vec<CollectedMetric>) -> anyhow::Result<()> {\n        if metrics.is_empty() {\n            return Ok(());\n        }\n\n        log::trace!(\n            \"Exporting {} metrics to Prometheus remote write host {}\",\n            metrics.len(),\n            self.remote_write_url.host_str().unwrap_or_default()\n        );\n\n        let time_series = self.get_metric_data(metrics).await;\n\n        // Create WriteRequest with the time series\n        let write_request = prompb::WriteRequest {\n            timeseries: time_series,\n            metadata: vec![],\n        };\n\n        // Serialize to protobuf\n        let mut proto_buf = Vec::new();\n        write_request\n            .encode(&mut proto_buf)\n            .context(\"marshal metrics into Protobuf\")?;\n\n        // Compress with Snappy\n        let mut encoder = snap::raw::Encoder::new();\n        let encoded = encoder\n            .compress_vec(&proto_buf)\n            .context(\"compress metrics with Snappy\")?;\n\n        // Send HTTP request\n        let response = self\n            .client\n            .post(self.remote_write_url.clone())\n            .header(\"Content-Type\", \"application/x-protobuf\")\n            .header(\"Content-Encoding\", \"snappy\")\n            .header(\"User-Agent\", \"encore\")\n            .header(\"X-Prometheus-Remote-Write-Version\", \"0.1.0\")\n            .body(encoded)\n            .send()\n            .await\n            .context(\"send metrics to Prometheus remote write destination\")?;\n\n        if !response.status().is_success() {\n            let status = response.status();\n            let body = response\n                .text()\n                .await\n                .unwrap_or_else(|_| \"<unable to read response body>\".to_string());\n            anyhow::bail!(\n                \"Prometheus remote write returned non-success status {}: {}\",\n                status,\n                body\n            );\n        }\n\n        Ok(())\n    }\n\n    async fn container_labels(&self) -> Arc<Vec<prompb::Label>> {\n        self.container_labels\n            .get_or_try_init(|| async {\n                let container_metadata = self.container_meta_client.collect().await?;\n                anyhow::Ok(Arc::new(\n                    container_metadata\n                        .labels()\n                        .into_iter()\n                        .map(|(name, value)| prompb::Label { name, value })\n                        .collect(),\n                ))\n            })\n            .await\n            .map(Arc::clone)\n            .unwrap_or_else(|e| {\n                log::warn!(\"failed fetching container metadata: {e}, using fallback\");\n                Arc::new(\n                    self.container_meta_client\n                        .fallback()\n                        .labels()\n                        .into_iter()\n                        .map(|(name, value)| prompb::Label { name, value })\n                        .collect(),\n                )\n            })\n    }\n\n    async fn get_metric_data(&self, collected: Vec<CollectedMetric>) -> Vec<prompb::TimeSeries> {\n        let now = SystemTime::now();\n        let timestamp = from_time(now);\n        let mut data: Vec<prompb::TimeSeries> = Vec::with_capacity(collected.len());\n\n        let container_labels = self.container_labels().await;\n        let container_labels_len = container_labels.len();\n\n        for metric in collected {\n            let metric_name = metric.key.name().to_string();\n\n            // Build labels: container metadata + metric labels + __name__\n            let mut labels: Vec<prompb::Label> =\n                Vec::with_capacity(container_labels_len + metric.key.labels().len() + 1);\n\n            // Add container metadata labels\n            labels.extend(container_labels.iter().cloned());\n\n            // Add metric-specific labels\n            for label in metric.key.labels() {\n                labels.push(prompb::Label {\n                    name: label.key().to_string(),\n                    value: label.value().to_string(),\n                });\n            }\n\n            // Add __name__ label for the metric name\n            labels.push(prompb::Label {\n                name: \"__name__\".to_string(),\n                value: metric_name,\n            });\n\n            // Sort labels lexicographically by name, as required by some Prometheus implementations.\n            labels.sort_unstable_by(|a, b| a.name.cmp(&b.name));\n\n            // Convert metric value to float64\n            let value = match metric.value {\n                MetricValue::CounterU64(val) => val as f64,\n                MetricValue::CounterI64(val) => val as f64,\n                MetricValue::GaugeF64(val) => val,\n                MetricValue::GaugeU64(val) => val as f64,\n                MetricValue::GaugeI64(val) => val as f64,\n            };\n\n            data.push(prompb::TimeSeries {\n                labels,\n                samples: vec![prompb::Sample { value, timestamp }],\n                exemplars: vec![],\n                histograms: vec![],\n            });\n        }\n\n        data\n    }\n}\n\n#[async_trait::async_trait]\nimpl Exporter for Prometheus {\n    async fn export(&self, metrics: Vec<CollectedMetric>) {\n        if let Err(err) = self.export_metrics(metrics).await {\n            log::error!(\"Failed to export metrics to Prometheus: {:#}\", err);\n        }\n    }\n}\n\n/// Convert SystemTime to Prometheus timestamp (milliseconds since Unix epoch)\nfn from_time(t: SystemTime) -> i64 {\n    match t.duration_since(SystemTime::UNIX_EPOCH) {\n        Ok(duration) => {\n            let secs = duration.as_secs() as i64;\n            let nanos = duration.subsec_nanos() as i64;\n            secs * 1000 + nanos / 1_000_000\n        }\n        Err(_) => 0, // If time is before Unix epoch, return 0\n    }\n}\n\n#[allow(dead_code)]\nmod prompb {\n    include!(concat!(env!(\"OUT_DIR\"), \"/prometheus.rs\"));\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::encore::runtime::v1 as pb;\n    use std::time::SystemTime;\n\n    /// Test that labels are sorted lexicographically by name.\n    /// Some Prometheus implementations require this or they will reject the request\n    /// with \"out of order labels\" error.\n    #[tokio::test]\n    async fn test_labels_are_sorted() {\n        let env = pb::Environment::default();\n        let container_meta_client = ContainerMetaClient::new(env, reqwest::Client::new());\n\n        // Create a metric with labels in non-sorted order: \"zebra\", \"apple\", \"middle\"\n        let key = metrics::Key::from_parts(\n            \"test_metric\",\n            vec![\n                metrics::Label::new(\"zebra\", \"last\"),\n                metrics::Label::new(\"apple\", \"first\"),\n                metrics::Label::new(\"middle\", \"mid\"),\n            ],\n        );\n\n        let collected = vec![CollectedMetric {\n            key,\n            value: MetricValue::CounterU64(42),\n            registered_at: SystemTime::now(),\n        }];\n\n        let prometheus = Prometheus {\n            client: reqwest::Client::new(),\n            remote_write_url: Url::parse(\"http://localhost:9090/api/v1/write\").unwrap(),\n            container_meta_client,\n            container_labels: OnceCell::new(),\n        };\n\n        let time_series = prometheus.get_metric_data(collected).await;\n        assert_eq!(time_series.len(), 1);\n\n        let label_names: Vec<&str> = time_series[0]\n            .labels\n            .iter()\n            .map(|l| l.name.as_str())\n            .collect();\n\n        // Labels should be sorted lexicographically\n        assert_eq!(\n            label_names,\n            vec![\n                \"__name__\",\n                \"apple\",\n                \"env_name\",\n                \"instance_id\",\n                \"middle\",\n                \"revision_id\",\n                \"service_id\",\n                \"zebra\"\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/gauge.rs",
    "content": "use crate::metrics;\nuse std::collections::{HashMap, HashSet};\nuse std::marker::PhantomData;\nuse std::sync::atomic::AtomicU64;\nuse std::sync::Arc;\n\npub trait GaugeOps<T> {\n    fn set(&self, value: T);\n    fn get(&self) -> crate::metrics::MetricValue;\n}\n\n/// A typed gauge that can be set, incremented, or decremented\n/// T must be compatible with GaugeOps for type-safe operations\npub struct Gauge<T> {\n    atomic: Arc<AtomicU64>,\n    _phantom: PhantomData<T>,\n}\n\nimpl<T> Gauge<T>\nwhere\n    Arc<AtomicU64>: GaugeOps<T>,\n{\n    /// Create a new gauge with the given atomic storage\n    /// This is typically called by Registry, not directly by users\n    pub(crate) fn new(atomic: Arc<AtomicU64>) -> Self {\n        Self {\n            atomic,\n            _phantom: PhantomData,\n        }\n    }\n\n    /// Set the gauge to the specified value\n    pub fn set(&self, value: T) {\n        GaugeOps::set(&self.atomic, value);\n    }\n\n    /// Get the current value of the gauge\n    pub fn get(&self) -> metrics::MetricValue {\n        GaugeOps::get(&self.atomic)\n    }\n}\n\n/// A gauge schema that defines static labels and required dynamic label keys\n/// Validates dynamic labels at set/add/sub time and creates separate time series\n/// for each unique combination of static + dynamic labels\n#[derive(Clone, Debug)]\npub struct Schema<T> {\n    name: String,\n    static_labels: Vec<(String, String)>,\n    required_dynamic_keys: HashSet<String>,\n    registry: Arc<metrics::Registry>,\n    _phantom: PhantomData<T>,\n}\n\nimpl<T> Schema<T>\nwhere\n    Arc<AtomicU64>: GaugeOps<T>,\n    T: Send + Sync + 'static,\n{\n    /// Create a new gauge schema\n    pub(crate) fn new(\n        name: String,\n        static_labels: Vec<(String, String)>,\n        required_dynamic_keys: HashSet<String>,\n        registry: Arc<metrics::Registry>,\n    ) -> Self {\n        Self {\n            name,\n            static_labels,\n            required_dynamic_keys,\n            registry,\n            _phantom: PhantomData,\n        }\n    }\n\n    /// Set the gauge value directly without dynamic labels\n    pub fn set(&self, value: T) {\n        if !self.required_dynamic_keys.is_empty() {\n            log::warn!(\n                \"setting gauge '{}' without required dynamic labels, required keys: {:?}\",\n                self.name,\n                self.required_dynamic_keys\n            );\n        }\n\n        self.get_or_create_gauge(HashMap::new()).set(value);\n    }\n\n    // Set the dynamic label values and return a completed Gauge\n    pub fn with<L, K, V>(&self, dynamic_labels: L) -> Gauge<T>\n    where\n        L: IntoIterator<Item = (K, V)>,\n        K: Into<String>,\n        V: Into<String>,\n    {\n        // Convert dynamic_labels to HashMap first\n        let dynamic_labels_map: HashMap<String, String> = dynamic_labels\n            .into_iter()\n            .map(|(k, v)| (k.into(), v.into()))\n            .collect();\n\n        // Validate required keys are present\n        let missing: Vec<String> = self\n            .required_dynamic_keys\n            .iter()\n            .filter(|key| !dynamic_labels_map.contains_key(*key))\n            .cloned()\n            .collect();\n\n        if !missing.is_empty() {\n            log::warn!(\n                \"missing required dynamic labels for metric '{}': {:?}, required keys: {:?}\",\n                self.name,\n                missing,\n                self.required_dynamic_keys\n            );\n        }\n\n        self.get_or_create_gauge(dynamic_labels_map)\n    }\n\n    /// Get or create a gauge for the given dynamic labels\n    fn get_or_create_gauge(&self, dynamic_labels: HashMap<String, String>) -> Gauge<T> {\n        // Create merged labels (static + dynamic)\n        let mut merged_labels = self.static_labels.clone();\n        for (key, value) in dynamic_labels {\n            merged_labels.push((key, value));\n        }\n\n        self.registry.get_or_create_gauge(\n            &self.name,\n            merged_labels.iter().map(|(k, v)| (k.as_str(), v.as_str())),\n        )\n    }\n}\n\n/// Builder for creating gauge schemas with static labels and required dynamic keys\npub struct GaugeSchemaBuilder<T> {\n    name: String,\n    static_labels: Vec<(String, String)>,\n    required_dynamic_keys: HashSet<String>,\n    registry: Arc<metrics::Registry>,\n    _phantom: std::marker::PhantomData<T>,\n}\n\nimpl<T> GaugeSchemaBuilder<T>\nwhere\n    Arc<AtomicU64>: GaugeOps<T>,\n    T: Send + Sync + 'static,\n{\n    pub(crate) fn new(name: String, registry: Arc<metrics::Registry>) -> Self {\n        Self {\n            name,\n            static_labels: Vec::new(),\n            required_dynamic_keys: HashSet::new(),\n            registry,\n            _phantom: std::marker::PhantomData,\n        }\n    }\n\n    /// Add static labels that are set once when the schema is created\n    pub fn static_labels<I, K, V>(mut self, labels: I) -> Self\n    where\n        I: IntoIterator<Item = (K, V)>,\n        K: AsRef<str>,\n        V: AsRef<str>,\n    {\n        for (key, value) in labels {\n            self.static_labels\n                .push((key.as_ref().to_string(), value.as_ref().to_string()));\n        }\n        self\n    }\n\n    /// Add a single static label\n    pub fn static_label(mut self, key: &str, value: &str) -> Self {\n        self.static_labels\n            .push((key.to_string(), value.to_string()));\n        self\n    }\n\n    /// Specify required dynamic label keys that must be provided at set/add/sub time\n    pub fn require_dynamic_keys<I, K>(mut self, keys: I) -> Self\n    where\n        I: IntoIterator<Item = K>,\n        K: AsRef<str>,\n    {\n        for key in keys {\n            self.required_dynamic_keys.insert(key.as_ref().to_string());\n        }\n        self\n    }\n\n    /// Add a single required dynamic key\n    pub fn require_dynamic_key(mut self, key: &str) -> Self {\n        self.required_dynamic_keys.insert(key.to_string());\n        self\n    }\n\n    /// Build the gauge schema\n    pub fn build(self) -> Schema<T> {\n        Schema::new(\n            self.name,\n            self.static_labels,\n            self.required_dynamic_keys,\n            self.registry,\n        )\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/manager.rs",
    "content": "use crate::{\n    encore::runtime::v1::{self as pb, Environment},\n    metadata::{process_env_substitution, ContainerMetaClient},\n    metrics::{\n        exporter::{self, Exporter},\n        registry::Registry,\n    },\n    secrets,\n};\nuse std::sync::Arc;\nuse std::time::Duration;\n\n#[derive(Debug, Clone)]\nenum ProviderType {\n    Gcp(pb::metrics_provider::GcpCloudMonitoring),\n    EncoreCloud(pb::metrics_provider::GcpCloudMonitoring),\n    Aws(pb::metrics_provider::AwsCloudWatch),\n    Datadog(pb::metrics_provider::Datadog),\n    Prometheus(pb::metrics_provider::PrometheusRemoteWrite),\n}\n\nimpl ProviderType {\n    fn from_config(provider: &pb::MetricsProvider) -> Option<Self> {\n        match &provider.provider {\n            Some(pb::metrics_provider::Provider::Gcp(config)) => Some(Self::Gcp(config.clone())),\n            Some(pb::metrics_provider::Provider::EncoreCloud(config)) => {\n                Some(Self::EncoreCloud(config.clone()))\n            }\n            Some(pb::metrics_provider::Provider::Aws(config)) => Some(Self::Aws(config.clone())),\n            Some(pb::metrics_provider::Provider::Datadog(config)) => {\n                Some(Self::Datadog(config.clone()))\n            }\n            Some(pb::metrics_provider::Provider::PromRemoteWrite(config)) => {\n                Some(Self::Prometheus(config.clone()))\n            }\n            None => {\n                log::warn!(\"no metrics provider configured\");\n                None\n            }\n        }\n    }\n\n    fn create_exporter(\n        &self,\n        env: &Environment,\n        secrets: &secrets::Manager,\n        http_client: &reqwest::Client,\n    ) -> anyhow::Result<Arc<dyn Exporter + Send + Sync>> {\n        match self {\n            Self::Gcp(config) | Self::EncoreCloud(config) => {\n                Ok(Self::create_gcp_exporter(config, env, http_client))\n            }\n            Self::Aws(config) => Ok(Self::create_aws_exporter(config, env, http_client)),\n            Self::Datadog(config) => {\n                Self::create_datadog_exporter(config, secrets, env, http_client)\n            }\n            Self::Prometheus(config) => {\n                Self::create_prometheus_exporter(config, secrets, env, http_client)\n            }\n        }\n    }\n\n    fn create_prometheus_exporter(\n        provider_cfg: &pb::metrics_provider::PrometheusRemoteWrite,\n        secrets: &secrets::Manager,\n        env: &Environment,\n        http_client: &reqwest::Client,\n    ) -> anyhow::Result<Arc<dyn Exporter + Send + Sync>> {\n        let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone());\n        Ok(Arc::new(exporter::Prometheus::new(\n            provider_cfg,\n            secrets,\n            container_meta_client,\n        )?))\n    }\n\n    fn create_datadog_exporter(\n        provider_cfg: &pb::metrics_provider::Datadog,\n        secrets: &secrets::Manager,\n        env: &Environment,\n        http_client: &reqwest::Client,\n    ) -> anyhow::Result<Arc<dyn Exporter + Send + Sync>> {\n        let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone());\n        Ok(Arc::new(exporter::Datadog::new(\n            provider_cfg,\n            secrets,\n            container_meta_client,\n        )?))\n    }\n\n    fn create_aws_exporter(\n        provider_cfg: &pb::metrics_provider::AwsCloudWatch,\n        env: &Environment,\n        http_client: &reqwest::Client,\n    ) -> Arc<dyn Exporter + Send + Sync> {\n        let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone());\n        Arc::new(exporter::Aws::new(\n            provider_cfg.namespace.clone(),\n            container_meta_client,\n        ))\n    }\n\n    fn create_gcp_exporter(\n        provider_cfg: &pb::metrics_provider::GcpCloudMonitoring,\n        env: &Environment,\n        http_client: &reqwest::Client,\n    ) -> Arc<dyn Exporter + Send + Sync> {\n        let container_meta_client = ContainerMetaClient::new(env.clone(), http_client.clone());\n\n        let mut labels = provider_cfg.monitored_resource_labels.clone();\n        process_env_substitution(&mut labels);\n\n        Arc::new(exporter::Gcp::new(\n            provider_cfg.project_id.clone(),\n            provider_cfg.monitored_resource_type.clone(),\n            labels,\n            provider_cfg.metric_names.clone(),\n            container_meta_client,\n        ))\n    }\n}\n\n#[derive(Clone)]\npub struct Manager {\n    exporter: Option<Arc<dyn Exporter>>,\n    registry: Arc<Registry>,\n}\n\nimpl Manager {\n    pub fn new() -> Self {\n        let registry = Arc::new(Registry::new());\n\n        Self {\n            exporter: None,\n            registry,\n        }\n    }\n\n    pub fn registry(&self) -> &Arc<Registry> {\n        &self.registry\n    }\n\n    pub fn from_runtime_config(\n        observability: &pb::Observability,\n        environment: &pb::Environment,\n        secrets: &secrets::Manager,\n        http_client: &reqwest::Client,\n        runtime_handle: tokio::runtime::Handle,\n    ) -> Self {\n        let mut manager = Self::new();\n\n        for metrics_provider in &observability.metrics {\n            if let Some(provider_type) = ProviderType::from_config(metrics_provider) {\n                match provider_type.create_exporter(environment, secrets, http_client) {\n                    Ok(exporter) => {\n                        manager.exporter = Some(exporter);\n                        break; // Take the first valid provider\n                    }\n                    Err(err) => {\n                        log::error!(\"Failed to create metrics exporter: {}\", err);\n                    }\n                }\n            }\n        }\n\n        // Start collection if we have an exporter\n        if manager.exporter.is_some() {\n            let collection_interval = observability\n                .metrics\n                .first()\n                .and_then(|p| p.collection_interval.as_ref())\n                .and_then(|d| Duration::try_from(d.clone()).ok())\n                .filter(|d| !d.is_zero())\n                .unwrap_or(Duration::from_secs(60)); // Default to 1 minute\n            manager.start_collection_loop(runtime_handle, collection_interval);\n        }\n\n        manager\n    }\n\n    pub fn with_exporter(mut self, exporter: std::sync::Arc<dyn Exporter + Send + Sync>) -> Self {\n        self.exporter = Some(exporter);\n        self\n    }\n\n    pub async fn collect_and_export(&self) {\n        let metrics = self.registry.collect();\n        if let Some(ref exporter) = self.exporter {\n            exporter.export(metrics).await;\n        }\n    }\n\n    pub fn collect_metrics(&self) -> Vec<crate::metrics::CollectedMetric> {\n        self.registry.collect()\n    }\n\n    pub fn start_collection_loop(\n        &self,\n        runtime_handle: tokio::runtime::Handle,\n        interval: std::time::Duration,\n    ) {\n        let manager = self.clone();\n        runtime_handle.spawn(async move {\n            let mut interval_timer = tokio::time::interval(interval);\n            loop {\n                interval_timer.tick().await;\n                manager.collect_and_export().await;\n            }\n        });\n    }\n}\n\nimpl Default for Manager {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/mod.rs",
    "content": "mod atomic;\nmod exporter;\nmod manager;\nmod registry;\nmod system;\n\npub mod counter;\npub mod gauge;\n\n#[cfg(test)]\nmod test;\n\nuse std::sync::Arc;\n\npub use counter::{Counter, CounterOps};\npub use gauge::{Gauge, GaugeOps};\npub use manager::Manager;\npub use registry::{CollectedMetric, MetricValue, MetricsCollector, Registry};\npub use system::SystemMetricsCollector;\n\n/// Create a requests counter schema\npub fn requests_total_counter(\n    registry: &Arc<Registry>,\n    service: &str,\n    endpoint: &str,\n) -> counter::Schema<u64> {\n    registry\n        .counter_schema::<u64>(\"e_requests_total\")\n        .static_labels([(\"service\", service), (\"endpoint\", endpoint)])\n        .require_dynamic_key(\"code\")\n        .build()\n}\n\n/// Create a memory usage gauge schema\npub fn memory_usage_gauge_schema(registry: &Arc<Registry>) -> gauge::Schema<u64> {\n    registry\n        .gauge_schema::<u64>(\"e_sys_memory_used_bytes\")\n        .build()\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/registry.rs",
    "content": "use crate::metrics::counter::{CounterOps, CounterSchemaBuilder};\nuse crate::metrics::gauge::{GaugeOps, GaugeSchemaBuilder};\n\nuse super::system::SystemMetricsCollector;\nuse super::{Counter, Gauge};\nuse dashmap::DashMap;\nuse malachite::base::num::basic::traits::One;\nuse metrics::{Key, Label};\nuse std::sync::atomic::AtomicU64;\nuse std::sync::{Arc, RwLock};\nuse std::time::SystemTime;\n\n/// Trait for external metrics collectors (e.g., JS runtime, other language runtimes)\npub trait MetricsCollector: Send + Sync {\n    /// Collect all metrics from this collector\n    fn collect(&self) -> Vec<CollectedMetric>;\n}\n\nstruct MetricStorage {\n    atomic: Arc<AtomicU64>,\n    getter: Box<dyn Fn() -> MetricValue + Send + Sync>,\n    registered_at: SystemTime,\n}\n\nimpl std::fmt::Debug for MetricStorage {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"MetricStorage\")\n            .field(\"atomic\", &self.atomic)\n            .field(\"registered_at\", &self.registered_at)\n            .finish()\n    }\n}\n\n#[derive(Debug, Clone, PartialEq)]\npub enum MetricValue {\n    // Counter variants\n    CounterU64(u64),\n    CounterI64(i64),\n\n    // Gauge variants\n    GaugeU64(u64),\n    GaugeI64(i64),\n    GaugeF64(f64),\n}\n\n#[derive(Debug, Clone)]\npub struct CollectedMetric {\n    pub key: Key,\n    pub value: MetricValue,\n    pub registered_at: SystemTime,\n}\n\npub struct Registry {\n    counters: DashMap<Key, MetricStorage>,\n    gauges: DashMap<Key, MetricStorage>,\n    system_metrics: SystemMetricsCollector,\n    external_collectors: RwLock<Vec<Arc<dyn MetricsCollector>>>,\n}\n\nimpl std::fmt::Debug for Registry {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"Registry\")\n            .field(\"counters\", &self.counters)\n            .field(\"gauges\", &self.gauges)\n            .field(\"system_metrics\", &self.system_metrics)\n            .finish()\n    }\n}\n\nimpl Registry {\n    pub fn new() -> Self {\n        Self {\n            counters: DashMap::new(),\n            gauges: DashMap::new(),\n            system_metrics: SystemMetricsCollector::new(),\n            external_collectors: RwLock::new(Vec::new()),\n        }\n    }\n\n    /// Register an external metrics collector (e.g., from JS runtime)\n    pub fn register_collector(&self, collector: Arc<dyn MetricsCollector>) {\n        self.external_collectors\n            .write()\n            .expect(\"mutex poisoned\")\n            .push(collector);\n    }\n\n    /// Create a counter with the given name and labels\n    pub fn get_or_create_counter<'a, T>(\n        &self,\n        name: &str,\n        labels: impl IntoIterator<Item = (&'a str, &'a str)>,\n    ) -> Counter<T>\n    where\n        Arc<AtomicU64>: CounterOps<T>,\n        T: One + Send + Sync + 'static,\n    {\n        let labels_vec: Vec<Label> = labels\n            .into_iter()\n            .map(|(k, v)| Label::new(k.to_string(), v.to_string()))\n            .collect();\n        let key = Key::from_parts(name.to_string(), labels_vec);\n\n        let entry = self.counters.entry(key).or_insert_with(|| {\n            let atomic = Arc::new(AtomicU64::new(0));\n            let counter = Counter::new(Arc::clone(&atomic));\n            let getter = Box::new(move || counter.get());\n            MetricStorage {\n                atomic,\n                getter,\n                registered_at: SystemTime::now(),\n            }\n        });\n\n        Counter::new(Arc::clone(&entry.atomic))\n    }\n\n    /// Create a gauge with the given name and labels\n    pub fn get_or_create_gauge<'a, T>(\n        &self,\n        name: &str,\n        labels: impl IntoIterator<Item = (&'a str, &'a str)>,\n    ) -> Gauge<T>\n    where\n        Arc<AtomicU64>: GaugeOps<T>,\n        T: Send + Sync + 'static,\n    {\n        let labels_vec: Vec<Label> = labels\n            .into_iter()\n            .map(|(k, v)| Label::new(k.to_string(), v.to_string()))\n            .collect();\n        let key = Key::from_parts(name.to_string(), labels_vec);\n\n        let entry = self.gauges.entry(key).or_insert_with(|| {\n            let atomic = Arc::new(AtomicU64::new(0));\n            let gauge = Gauge::new(Arc::clone(&atomic));\n            let getter = Box::new(move || gauge.get());\n            MetricStorage {\n                atomic,\n                getter,\n                registered_at: SystemTime::now(),\n            }\n        });\n\n        Gauge::new(Arc::clone(&entry.atomic))\n    }\n\n    /// Create a counter schema builder for defining static and dynamic labels\n    pub fn counter_schema<T>(self: &Arc<Self>, name: &str) -> CounterSchemaBuilder<T>\n    where\n        Arc<AtomicU64>: CounterOps<T>,\n        T: One + Send + Sync + 'static,\n    {\n        CounterSchemaBuilder::new(name.to_string(), Arc::clone(self))\n    }\n\n    /// Create a gauge schema builder for defining static and dynamic labels\n    pub fn gauge_schema<T>(self: &Arc<Self>, name: &str) -> GaugeSchemaBuilder<T>\n    where\n        Arc<AtomicU64>: GaugeOps<T>,\n        T: Send + Sync + 'static,\n    {\n        GaugeSchemaBuilder::new(name.to_string(), Arc::clone(self))\n    }\n\n    pub fn collect(self: &Arc<Self>) -> Vec<CollectedMetric> {\n        let mut collected_metrics = Vec::new();\n\n        self.system_metrics.update(self);\n\n        // Collect counters\n        for entry in self.counters.iter() {\n            let key = entry.key();\n            let store = entry.value();\n\n            let value = (store.getter)();\n\n            collected_metrics.push(CollectedMetric {\n                value,\n                key: key.clone(),\n                registered_at: store.registered_at,\n            });\n        }\n\n        // Collect gauges\n        for entry in self.gauges.iter() {\n            let key = entry.key();\n            let store = entry.value();\n\n            let value = (store.getter)();\n\n            collected_metrics.push(CollectedMetric {\n                value,\n                key: key.clone(),\n                registered_at: store.registered_at,\n            });\n        }\n\n        // Collect from external collectors (e.g., JS runtime)\n        let collectors = self.external_collectors.read().expect(\"mutex poisoned\");\n        for collector in collectors.iter() {\n            collected_metrics.extend(collector.collect());\n        }\n\n        collected_metrics\n    }\n}\n\nimpl Default for Registry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/system.rs",
    "content": "use crate::metrics::gauge;\n\nuse super::{memory_usage_gauge_schema, Registry};\nuse std::sync::Mutex;\nuse sysinfo::System;\n\n/// Collector for system metrics\n#[derive(Debug)]\npub struct SystemMetricsCollector {\n    system: Mutex<System>,\n    memory_schema: std::sync::OnceLock<gauge::Schema<u64>>,\n}\n\nimpl SystemMetricsCollector {\n    pub fn new() -> Self {\n        let mut system = System::new();\n        Self::refresh(&mut system);\n\n        Self {\n            system: Mutex::new(system),\n            memory_schema: std::sync::OnceLock::new(),\n        }\n    }\n\n    fn refresh(system: &mut System) {\n        system.refresh_memory();\n    }\n\n    /// Collect and record all system metrics directly to the metrics registry\n    pub fn update(&self, registry: &std::sync::Arc<Registry>) {\n        let mut system = match self.system.try_lock() {\n            Ok(guard) => guard,\n            Err(_) => {\n                // Already an update in progress\n                return;\n            }\n        };\n\n        Self::refresh(&mut system);\n\n        // Initialize schemas if not already done (lazy initialization)\n        let memory_schema = self\n            .memory_schema\n            .get_or_init(|| memory_usage_gauge_schema(registry));\n\n        // Record memory metrics using schema\n        memory_schema.set(system.used_memory());\n    }\n}\n\nimpl Default for SystemMetricsCollector {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_system_metrics_collector_creation() {\n        use crate::metrics::Manager;\n\n        let manager = Manager::new();\n        let collector = SystemMetricsCollector::new();\n\n        // Update system metrics using the registry directly\n        collector.update(manager.registry());\n\n        let collected = manager.collect_metrics();\n\n        // Check if system metrics were recorded\n        let memory_metrics: Vec<_> = collected\n            .iter()\n            .filter(|m| m.key.name() == \"e_sys_memory_used_bytes\")\n            .collect();\n\n        // Should have memory metrics\n        assert!(\n            !memory_metrics.is_empty(),\n            \"Memory metrics should be present\"\n        );\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/metrics/test.rs",
    "content": "use super::*;\nuse std::sync::Arc;\n\n#[cfg(test)]\nmod counter_tests {\n    use super::*;\n\n    #[test]\n    fn test_counter_with_labels() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        registry\n            .get_or_create_counter::<u64>(\n                \"test_counter_with_labels\",\n                [(\"service\", \"test_service\"), (\"method\", \"GET\")],\n            )\n            .increment();\n\n        let collected = manager.collect_metrics();\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_counter_with_labels\");\n        assert!(metric.is_some());\n\n        let m = metric.unwrap();\n        assert_eq!(m.key.labels().len(), 2);\n\n        let labels: Vec<_> = m.key.labels().map(|l| (l.key(), l.value())).collect();\n\n        assert!(labels.contains(&(\"service\", \"test_service\")));\n        assert!(labels.contains(&(\"method\", \"GET\")));\n\n        // Verify it's the correct counter type\n        if let MetricValue::CounterU64(value) = m.value {\n            assert_eq!(value, 1);\n        } else {\n            panic!(\"Expected CounterU64, got {:?}\", m.value);\n        }\n    }\n\n    #[test]\n    fn test_counter_increment() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        registry\n            .get_or_create_counter::<u64>(\"test_increment\", [])\n            .increment();\n\n        let collected = manager.collect_metrics();\n        let metric = collected.iter().find(|m| m.key.name() == \"test_increment\");\n        assert!(metric.is_some());\n\n        let m = metric.unwrap();\n        if let MetricValue::CounterU64(value) = m.value {\n            assert_eq!(value, 1);\n        } else {\n            panic!(\"Expected CounterU64, got {:?}\", m.value);\n        }\n    }\n}\n\n#[cfg(test)]\nmod registry_tests {\n    use super::*;\n\n    #[test]\n    fn test_registry_collect_counters() {\n        let registry = Arc::new(Registry::new());\n\n        let counter = registry.get_or_create_counter::<u64>(\"test_counter\", [(\"label\", \"value\")]);\n        counter.increment();\n        counter.increment();\n        counter.increment();\n        counter.increment();\n        counter.increment();\n\n        let collected = registry.collect();\n\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_counter\")\n            .expect(\"test_counter not found\");\n        assert_eq!(metric.key.name(), \"test_counter\");\n        assert_eq!(metric.key.labels().len(), 1);\n        match metric.value {\n            MetricValue::CounterU64(value) => assert_eq!(value, 5),\n            _ => panic!(\"Expected CounterU64 value, got {:?}\", metric.value),\n        }\n    }\n\n    #[test]\n    fn test_registry_collect_gauges() {\n        let registry = Arc::new(Registry::new());\n\n        registry\n            .get_or_create_gauge::<f64>(\"test_gauge\", [])\n            .set(42.5);\n\n        let collected = registry.collect();\n\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_gauge\")\n            .expect(\"test_gauge not found\");\n        assert_eq!(metric.key.name(), \"test_gauge\");\n        match metric.value {\n            MetricValue::GaugeF64(value) => assert!((value - 42.5).abs() < f64::EPSILON),\n            _ => panic!(\"Expected GaugeF64 value, got {:?}\", metric.value),\n        }\n    }\n\n    #[test]\n    fn test_registry_registered_at() {\n        let registry = Arc::new(Registry::new());\n\n        let before = std::time::SystemTime::now();\n        registry.get_or_create_counter::<u64>(\"test_metric\", []);\n        let after = std::time::SystemTime::now();\n\n        // Find the key that was created\n        let collected = registry.collect();\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_metric\")\n            .unwrap();\n\n        let time = metric.registered_at;\n        assert!(time >= before && time <= after);\n\n        // Creating the counter again should not update first seen\n        registry.get_or_create_counter::<u64>(\"test_metric\", []);\n        // Find the key that was created\n        let collected = registry.collect();\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_metric\")\n            .unwrap();\n        let time2 = metric.registered_at;\n\n        assert_eq!(time, time2)\n    }\n\n    #[test]\n    fn test_registry_always_collects_system_metrics() {\n        let registry = Arc::new(Registry::new());\n\n        let collected = registry.collect();\n\n        // Check that system metrics are present\n        let has_memory_metrics = collected\n            .iter()\n            .any(|m| m.key.name() == \"e_sys_memory_used_bytes\");\n\n        assert!(has_memory_metrics, \"Memory metrics should be present\");\n    }\n}\n\n#[cfg(test)]\nmod gauge_tests {\n    use super::*;\n\n    #[test]\n    fn test_gauge_with_labels() {\n        let manager = Manager::new();\n\n        let registry = manager.registry();\n        // Create gauge with labels using new API\n        registry\n            .get_or_create_gauge::<f64>(\n                \"test_gauge_with_labels\",\n                [(\"service\", \"test_service\"), (\"region\", \"us-west\")],\n            )\n            .set(42.5);\n\n        let collected = manager.collect_metrics();\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_gauge_with_labels\");\n        assert!(metric.is_some());\n\n        let m = metric.unwrap();\n        assert_eq!(m.key.labels().len(), 2);\n        assert!(m\n            .key\n            .labels()\n            .any(|label| label.key() == \"service\" && label.value() == \"test_service\"));\n        assert!(m\n            .key\n            .labels()\n            .any(|label| label.key() == \"region\" && label.value() == \"us-west\"));\n\n        match m.value {\n            MetricValue::GaugeF64(value) => assert!((value - 42.5).abs() < f64::EPSILON),\n            _ => panic!(\"Expected gauge value\"),\n        }\n    }\n}\n\n#[cfg(test)]\nmod manager_tests {\n    use super::*;\n\n    #[test]\n    fn test_manager_collect_metrics() {\n        let manager = Manager::new();\n\n        manager\n            .registry()\n            .get_or_create_counter::<u64>(\"test_manager_counter\", [])\n            .increment();\n\n        let collected = manager.collect_metrics();\n        assert!(!collected.is_empty());\n\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"test_manager_counter\");\n        assert!(metric.is_some());\n    }\n\n    #[derive(Debug)]\n    struct MockExporter {\n        exported_metrics: Arc<std::sync::Mutex<Vec<Vec<CollectedMetric>>>>,\n    }\n\n    impl MockExporter {\n        fn new() -> Self {\n            Self {\n                exported_metrics: Arc::new(std::sync::Mutex::new(Vec::new())),\n            }\n        }\n\n        fn get_exported_metrics(&self) -> Vec<Vec<CollectedMetric>> {\n            self.exported_metrics.lock().unwrap().clone()\n        }\n    }\n\n    #[async_trait::async_trait]\n    impl super::super::exporter::Exporter for MockExporter {\n        async fn export(&self, metrics: Vec<CollectedMetric>) {\n            self.exported_metrics.lock().unwrap().push(metrics);\n        }\n    }\n\n    #[tokio::test]\n    async fn test_manager_with_exporter() {\n        let mock_exporter = Arc::new(MockExporter::new());\n        let manager = Manager::new().with_exporter(mock_exporter.clone());\n\n        let registry = manager.registry();\n        registry\n            .get_or_create_counter::<u64>(\"test_export_counter\", [])\n            .increment();\n\n        // Collect and export\n        manager.collect_and_export().await;\n\n        let exported = mock_exporter.get_exported_metrics();\n        assert_eq!(exported.len(), 1);\n        assert!(!exported[0].is_empty());\n\n        let metric = exported[0]\n            .iter()\n            .find(|m| m.key.name() == \"test_export_counter\");\n        assert!(metric.is_some());\n    }\n}\n\n#[cfg(test)]\nmod collected_metric_tests {\n    use super::*;\n\n    #[test]\n    fn test_gauge_types_integer_and_float() {\n        let manager = Manager::new();\n\n        let registry = manager.registry();\n\n        // Test float gauge\n        let float_gauge: Gauge<f64> = registry.get_or_create_gauge(\"test_float_metric\", []);\n        float_gauge.set(42.5);\n\n        let float_gauge_temp: Gauge<f64> =\n            registry.get_or_create_gauge(\"test_float_metric\", [(\"type\", \"temperature\")]);\n        float_gauge_temp.set(99.9);\n\n        // Test integer gauge\n        let int_gauge: Gauge<i64> = registry.get_or_create_gauge(\"test_int_metric\", []);\n        int_gauge.set(12345);\n\n        let int_gauge_count: Gauge<i64> =\n            registry.get_or_create_gauge(\"test_int_metric\", [(\"type\", \"count\")]);\n        int_gauge_count.set(67890);\n\n        // Test large integer to verify precision\n        let large_int = i64::MAX;\n        let int_gauge_large: Gauge<i64> =\n            registry.get_or_create_gauge(\"test_int_metric\", [(\"type\", \"large\")]);\n        int_gauge_large.set(large_int);\n\n        // Test small negative integer to verify precision\n        let small_int = i64::MIN;\n        let int_gauge_small: Gauge<i64> =\n            registry.get_or_create_gauge(\"test_int_metric\", [(\"type\", \"small\")]);\n        int_gauge_small.set(small_int);\n\n        let metrics = manager.collect_metrics();\n\n        let float_metrics: Vec<_> = metrics\n            .iter()\n            .filter(|m| m.key.name() == \"test_float_metric\")\n            .collect();\n        let int_metrics: Vec<_> = metrics\n            .iter()\n            .filter(|m| m.key.name() == \"test_int_metric\")\n            .collect();\n\n        // Should have 2 float metrics and 4 int metrics\n        assert_eq!(float_metrics.len(), 2);\n        assert_eq!(int_metrics.len(), 4);\n\n        // Check float metrics with their labels\n        for m in &float_metrics {\n            if let MetricValue::GaugeF64(val) = m.value {\n                if m.key.labels().count() == 0 {\n                    // No labels - should be 42.5\n                    assert_eq!(val, 42.5);\n                } else {\n                    // Should have type=temperature label\n                    let has_temp_label = m\n                        .key\n                        .labels()\n                        .any(|l| l.key() == \"type\" && l.value() == \"temperature\");\n                    assert!(has_temp_label);\n                    assert_eq!(val, 99.9);\n                }\n            } else {\n                panic!(\"Float metric has wrong type: {:?}\", m.value);\n            }\n        }\n\n        // Check int metrics with their labels\n        for m in &int_metrics {\n            if let MetricValue::GaugeI64(val) = m.value {\n                if m.key.labels().count() == 0 {\n                    // No labels - should be 12345\n                    assert_eq!(val, 12345);\n                } else {\n                    // Check which label it has\n                    let label_value = m\n                        .key\n                        .labels()\n                        .find(|l| l.key() == \"type\")\n                        .map(|l| l.value())\n                        .expect(\"Should have 'type' label\");\n\n                    match label_value {\n                        \"count\" => assert_eq!(val, 67890),\n                        \"large\" => assert_eq!(val, i64::MAX),\n                        \"small\" => assert_eq!(val, i64::MIN),\n                        _ => panic!(\"Unexpected label value: {}\", label_value),\n                    }\n                }\n            } else {\n                panic!(\"Integer metric has wrong type: {:?}\", m.value);\n            }\n        }\n    }\n\n    #[test]\n    fn test_counter_types_integer() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        // Test u64 counter\n        let u64_counter: Counter<u64> = registry.get_or_create_counter(\"test_u64_counter\", []);\n        u64_counter.increment();\n        u64_counter.increment();\n        u64_counter.increment();\n\n        let u64_counter_labeled: Counter<u64> =\n            registry.get_or_create_counter(\"test_u64_counter\", [(\"type\", \"requests\")]);\n        u64_counter_labeled.increment();\n        u64_counter_labeled.increment();\n\n        // Test i64 counter\n        let i64_counter = registry.get_or_create_counter::<i64>(\"test_i64_counter\", []);\n        i64_counter.increment();\n        i64_counter.increment();\n        i64_counter.increment();\n        i64_counter.increment();\n\n        let i64_counter_labeled: Counter<i64> =\n            registry.get_or_create_counter(\"test_i64_counter\", [(\"type\", \"errors\")]);\n        i64_counter_labeled.increment();\n\n        // Collect metrics\n        let metrics = manager.collect_metrics();\n\n        let u64_metrics: Vec<_> = metrics\n            .iter()\n            .filter(|m| m.key.name() == \"test_u64_counter\")\n            .collect();\n        let i64_metrics: Vec<_> = metrics\n            .iter()\n            .filter(|m| m.key.name() == \"test_i64_counter\")\n            .collect();\n\n        // Should have 2 u64 metrics and 2 i64 metrics\n        assert_eq!(u64_metrics.len(), 2);\n        assert_eq!(i64_metrics.len(), 2);\n\n        // Check u64 counter metrics\n        for m in &u64_metrics {\n            if let MetricValue::CounterU64(val) = m.value {\n                if m.key.labels().count() == 0 {\n                    // No labels - should be 3\n                    assert_eq!(val, 3);\n                } else {\n                    // Should have type=requests label\n                    let has_requests_label = m\n                        .key\n                        .labels()\n                        .any(|l| l.key() == \"type\" && l.value() == \"requests\");\n                    assert!(has_requests_label);\n                    assert_eq!(val, 2);\n                }\n            } else {\n                panic!(\"u64 counter metric has wrong type: {:?}\", m.value);\n            }\n        }\n\n        // Check i64 counter metrics\n        for m in &i64_metrics {\n            if let MetricValue::CounterI64(val) = m.value {\n                if m.key.labels().count() == 0 {\n                    // No labels - should be 4\n                    assert_eq!(val, 4);\n                } else {\n                    // Should have type=errors label\n                    let has_errors_label = m\n                        .key\n                        .labels()\n                        .any(|l| l.key() == \"type\" && l.value() == \"errors\");\n                    assert!(has_errors_label);\n                    assert_eq!(val, 1);\n                }\n            } else {\n                panic!(\"i64 counter metric has wrong type: {:?}\", m.value);\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod concurrency_tests {\n    use super::*;\n    use std::sync::{Arc, Barrier};\n    use std::thread;\n\n    #[test]\n    fn test_counter_concurrent_increments() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        let num_threads = 10;\n        let increments_per_thread = 1000;\n        let barrier = Arc::new(Barrier::new(num_threads));\n\n        let handles: Vec<_> = (0..num_threads)\n            .map(|_| {\n                let counter = registry.get_or_create_counter::<u64>(\"concurrent_counter\", []);\n                let barrier = barrier.clone();\n                thread::spawn(move || {\n                    barrier.wait();\n                    for _ in 0..increments_per_thread {\n                        counter.increment();\n                    }\n                })\n            })\n            .collect();\n\n        for handle in handles {\n            handle.join().unwrap();\n        }\n\n        let collected = manager.collect_metrics();\n        let metric = collected\n            .iter()\n            .find(|m| m.key.name() == \"concurrent_counter\")\n            .unwrap();\n\n        if let MetricValue::CounterU64(value) = metric.value {\n            assert_eq!(value, (num_threads * increments_per_thread) as u64);\n        } else {\n            panic!(\"Expected CounterU64\");\n        }\n    }\n\n    #[test]\n    fn test_gauge_concurrent_operations() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n        let num_threads = 8;\n        let operations_per_thread = 100;\n\n        let handles: Vec<_> = (0..num_threads)\n            .map(|thread_id| {\n                let registry = Arc::clone(registry);\n                thread::spawn(move || {\n                    for i in 0..operations_per_thread {\n                        let thread_id_str = thread_id.to_string();\n                        let gauge = registry.get_or_create_gauge::<f64>(\n                            \"concurrent_gauge\",\n                            [(\"thread\", thread_id_str.as_str())],\n                        );\n                        gauge.set((thread_id * operations_per_thread + i) as f64);\n                    }\n                })\n            })\n            .collect();\n\n        for handle in handles {\n            handle.join().unwrap();\n        }\n\n        let collected = manager.collect_metrics();\n        let gauge_metrics: Vec<_> = collected\n            .iter()\n            .filter(|m| m.key.name() == \"concurrent_gauge\")\n            .collect();\n\n        assert_eq!(gauge_metrics.len(), num_threads);\n\n        for metric in gauge_metrics {\n            if let MetricValue::GaugeF64(value) = metric.value {\n                let thread_id: usize = metric\n                    .key\n                    .labels()\n                    .find(|l| l.key() == \"thread\")\n                    .expect(\"no thread label set on metric\")\n                    .value()\n                    .parse()\n                    .expect(\"couldn't parse thread id\");\n\n                assert_eq!(\n                    value,\n                    (thread_id * operations_per_thread + operations_per_thread - 1) as f64 // last value written in the loop for each thread\n                );\n            } else {\n                panic!(\"Expected GaugeF64\");\n            }\n        }\n    }\n\n    #[test]\n    fn test_registry_concurrent_metric_creation() {\n        let registry = Arc::new(Registry::new());\n        let num_threads = 16;\n        let metrics_per_thread = 50;\n\n        let handles: Vec<_> = (0..num_threads)\n            .map(|thread_id| {\n                let registry = Arc::clone(&registry);\n                thread::spawn(move || {\n                    for i in 0..metrics_per_thread {\n                        let metric_name = format!(\"metric_{}_{}\", thread_id, i);\n                        registry\n                            .get_or_create_counter::<u64>(&metric_name, [])\n                            .increment();\n                        registry\n                            .get_or_create_gauge::<f64>(&metric_name, [(\"type\", \"gauge\")])\n                            .set(i as f64);\n                    }\n                })\n            })\n            .collect();\n\n        for handle in handles {\n            handle.join().unwrap();\n        }\n\n        let collected = registry.collect();\n\n        let counters = collected\n            .iter()\n            .filter(|m| matches!(m.value, MetricValue::CounterU64(_)))\n            .count();\n        let gauges = collected\n            .iter()\n            .filter(|m| matches!(m.value, MetricValue::GaugeF64(_)))\n            .count();\n\n        // Account for system metrics that are always collected\n        assert!(counters >= num_threads * metrics_per_thread);\n        assert!(gauges >= num_threads * metrics_per_thread);\n    }\n\n    #[test]\n    fn test_concurrent_collection_and_updates() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        let registry_clone = Arc::clone(registry);\n        let manager_clone = manager.clone();\n\n        let updater_handle = thread::spawn(move || {\n            let counter = registry_clone.get_or_create_counter::<u64>(\"collection_test\", []);\n            let gauge = registry_clone.get_or_create_gauge::<f64>(\"collection_test_gauge\", []);\n            for i in 0..1000 {\n                counter.increment();\n                gauge.set(i as f64);\n                thread::sleep(std::time::Duration::from_micros(1));\n            }\n        });\n\n        let collector_handle = thread::spawn(move || {\n            for _ in 0..100 {\n                let _metrics = manager_clone.collect_metrics();\n                thread::sleep(std::time::Duration::from_micros(10));\n            }\n        });\n\n        updater_handle.join().unwrap();\n        collector_handle.join().unwrap();\n\n        let final_metrics = manager.collect_metrics();\n        assert!(!final_metrics.is_empty());\n    }\n}\n\n#[cfg(test)]\nmod memory_performance_tests {\n    use super::*;\n\n    #[test]\n    fn test_high_cardinality_metrics() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        let num_services = 20;\n        let num_endpoints = 50;\n        let num_status_codes = 10;\n\n        for service_id in 0..num_services {\n            for endpoint_id in 0..num_endpoints {\n                for status_code in 200..(200 + num_status_codes) {\n                    let service_str = format!(\"service_{}\", service_id);\n                    let endpoint_str = format!(\"endpoint_{}\", endpoint_id);\n                    let status_str = status_code.to_string();\n                    let counter = registry.get_or_create_counter::<u64>(\n                        \"high_cardinality_requests\",\n                        [\n                            (\"service\", service_str.as_str()),\n                            (\"endpoint\", endpoint_str.as_str()),\n                            (\"status\", status_str.as_str()),\n                        ],\n                    );\n                    counter.increment();\n                }\n            }\n        }\n\n        let collected = manager.collect_metrics();\n        let request_metrics: Vec<_> = collected\n            .iter()\n            .filter(|m| m.key.name() == \"high_cardinality_requests\")\n            .collect();\n\n        let expected_count = num_services * num_endpoints * num_status_codes;\n        assert_eq!(request_metrics.len(), expected_count);\n\n        for metric in request_metrics {\n            assert_eq!(metric.key.labels().count(), 3);\n            if let MetricValue::CounterU64(value) = metric.value {\n                assert_eq!(value, 1);\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod manager_export_tests {\n    use super::*;\n    use std::sync::Arc;\n    use std::time::Duration;\n\n    #[tokio::test]\n    async fn test_manager_without_exporter() {\n        let manager = Manager::new();\n        let registry = manager.registry();\n\n        registry\n            .get_or_create_counter::<u64>(\"no_exporter_test\", [])\n            .increment();\n\n        manager.collect_and_export().await;\n\n        let metrics = manager.collect_metrics();\n        assert!(!metrics.is_empty());\n\n        let found = metrics.iter().any(|m| m.key.name() == \"no_exporter_test\");\n        assert!(found);\n    }\n\n    #[tokio::test]\n    async fn test_manager_concurrent_collect_and_export() {\n        let manager = Arc::new(Manager::new());\n        let registry = manager.registry();\n\n        let manager1 = manager.clone();\n        let manager2 = manager.clone();\n        let registry_clone = Arc::clone(registry);\n\n        let updater = tokio::spawn(async move {\n            let counter = registry_clone.get_or_create_counter::<u64>(\"concurrent_export_test\", []);\n            for _ in 0..100 {\n                counter.increment();\n                tokio::time::sleep(Duration::from_millis(1)).await;\n            }\n        });\n\n        let collector1 = tokio::spawn(async move {\n            for _ in 0..10 {\n                manager1.collect_and_export().await;\n                tokio::time::sleep(Duration::from_millis(5)).await;\n            }\n        });\n\n        let collector2 = tokio::spawn(async move {\n            for _ in 0..10 {\n                let _metrics = manager2.collect_metrics();\n                tokio::time::sleep(Duration::from_millis(3)).await;\n            }\n        });\n\n        let _ = tokio::try_join!(updater, collector1, collector2);\n\n        let final_metrics = manager.collect_metrics();\n        let test_metric = final_metrics\n            .iter()\n            .find(|m| m.key.name() == \"concurrent_export_test\")\n            .unwrap();\n\n        if let MetricValue::CounterU64(value) = test_metric.value {\n            assert_eq!(value, 100);\n        }\n    }\n}\n\n#[cfg(test)]\nmod type_system_atomic_tests {\n    use malachite::base::num::basic::floats::PrimitiveFloat;\n\n    use super::*;\n\n    #[test]\n    fn test_float_precision_in_atomic_operations() {\n        let registry = Arc::new(Registry::new());\n\n        let test_values = [\n            0.0,\n            -0.0,\n            1.0,\n            -1.0,\n            f64::MIN,\n            f64::MAX,\n            f64::EPSILON,\n            std::f64::consts::PI,\n            std::f64::consts::E,\n            1.23456789012345,\n            -9.87654321098765,\n        ];\n\n        for (i, &value) in test_values.iter().enumerate() {\n            let gauge = registry.get_or_create_gauge::<f64>(&format!(\"precision_test_{}\", i), []);\n            gauge.set(value);\n\n            let collected = registry.collect();\n            let metric = collected\n                .iter()\n                .find(|m| m.key.name() == format!(\"precision_test_{}\", i))\n                .unwrap();\n\n            if let MetricValue::GaugeF64(stored_value) = metric.value {\n                assert_eq!(stored_value, value, \"Precision lost for value: {}\", value);\n            } else {\n                panic!(\"Expected GaugeF64, got {:?}\", metric.value);\n            }\n        }\n    }\n\n    #[test]\n    fn test_special_float_values() {\n        let registry = Arc::new(Registry::new());\n\n        let special_values = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 0.0, -0.0];\n\n        for (i, &value) in special_values.iter().enumerate() {\n            let gauge = registry.get_or_create_gauge::<f64>(&format!(\"special_float_{}\", i), []);\n            gauge.set(value);\n\n            let collected = registry.collect();\n            let metric = collected\n                .iter()\n                .find(|m| m.key.name() == format!(\"special_float_{}\", i))\n                .unwrap();\n\n            if let MetricValue::GaugeF64(stored_value) = metric.value {\n                if value.is_nan() {\n                    assert!(stored_value.is_nan());\n                } else if value.is_infinite() {\n                    assert!(stored_value.is_infinite());\n                    assert_eq!(stored_value.is_sign_positive(), value.is_sign_positive());\n                } else if value.is_negative_zero() {\n                    assert!(stored_value.is_negative_zero())\n                } else {\n                    assert_eq!(stored_value.to_bits(), value.to_bits());\n                }\n            }\n        }\n    }\n\n    #[test]\n    fn test_counter_ops_trait_consistency() {\n        use crate::metrics::CounterOps;\n        use std::sync::atomic::{AtomicU64, Ordering};\n\n        let atomic = Arc::new(AtomicU64::new(0));\n\n        atomic.increment(5u64);\n        assert_eq!(atomic.load(Ordering::Acquire), 5);\n\n        atomic.increment(10i64);\n        assert_eq!(atomic.load(Ordering::Acquire), 15);\n\n        if let MetricValue::CounterU64(value) = CounterOps::<u64>::get(&atomic) {\n            assert_eq!(value, 15);\n        }\n\n        if let MetricValue::CounterI64(value) = CounterOps::<i64>::get(&atomic) {\n            assert_eq!(value, 15);\n        }\n    }\n\n    #[test]\n    fn test_gauge_ops_trait_consistency() {\n        use crate::metrics::GaugeOps;\n        use std::sync::atomic::AtomicU64;\n\n        let atomic = Arc::new(AtomicU64::new(0));\n\n        atomic.set(42u64);\n        if let MetricValue::GaugeU64(value) = GaugeOps::<u64>::get(&atomic) {\n            assert_eq!(value, 42);\n        }\n\n        atomic.set(-42i64);\n        if let MetricValue::GaugeI64(value) = GaugeOps::<i64>::get(&atomic) {\n            assert_eq!(value, -42);\n        }\n\n        atomic.set(3.21f64);\n        if let MetricValue::GaugeF64(value) = GaugeOps::<f64>::get(&atomic) {\n            assert!((value - 3.21).abs() < f64::EPSILON);\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/model/mod.rs",
    "content": "use axum::extract::WebSocketUpgrade;\nuse chrono::Utc;\nuse indexmap::IndexMap;\nuse std::fmt::Debug;\nuse std::str::FromStr;\nuse std::sync::{Arc, Mutex};\nuse std::time::SystemTime;\n\nuse rand::RngCore;\nuse tokio::time::Instant;\n\nuse crate::api::reqauth::caller::Caller;\nuse crate::api::schema::JSONPayload;\nuse crate::api::{auth, Endpoint, PValue, PValues};\nuse crate::{api, EncoreName, EndpointName};\n\n#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]\npub struct TraceId(pub [u8; 16]);\n\n#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]\npub struct SpanId(pub [u8; 8]);\n\n/// Uniquely identifies a span.\n#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]\npub struct SpanKey(pub TraceId, pub SpanId);\n\n/// Uniquely identifies an event within a trace.\n#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]\n#[must_use]\npub struct TraceEventId(pub u64);\n\nimpl FromStr for TraceEventId {\n    type Err = std::num::ParseIntError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let id = u64::from_str_radix(s, 32)?;\n        Ok(TraceEventId(id))\n    }\n}\n\nimpl TraceEventId {\n    pub fn serialize(&self) -> String {\n        radix_fmt::radix(self.0, 32).to_string()\n    }\n}\n\nimpl TraceId {\n    pub fn generate() -> Self {\n        let mut trace_id = [0u8; 16];\n        rand::thread_rng().fill_bytes(&mut trace_id);\n        TraceId(trace_id)\n    }\n\n    pub fn serialize_encore(&self) -> String {\n        crate::base32::encode(crate::base32::Alphabet::Encore, &self.0)\n    }\n\n    pub fn serialize_std(&self) -> String {\n        hex::encode(self.0)\n    }\n\n    pub fn parse_encore(s: &str) -> Result<Self, InvalidBase32> {\n        let Some(bytes) = crate::base32::decode(crate::base32::Alphabet::Encore, s) else {\n            return Err(InvalidBase32);\n        };\n        let trace_id: [u8; 16] = bytes.try_into().map_err(|_| InvalidBase32)?;\n        Ok(TraceId(trace_id))\n    }\n\n    pub fn parse_std(s: &str) -> Result<Self, hex::FromHexError> {\n        let bytes = hex::decode(s)?;\n        let trace_id: [u8; 16] = bytes\n            .try_into()\n            .map_err(|_| hex::FromHexError::InvalidStringLength)?;\n        Ok(TraceId(trace_id))\n    }\n\n    pub fn with_span(&self, span_id: SpanId) -> SpanKey {\n        SpanKey(*self, span_id)\n    }\n}\n\npub struct InvalidBase32;\n\nimpl SpanId {\n    pub fn generate() -> Self {\n        let mut span_id = [0u8; 8];\n        rand::thread_rng().fill_bytes(&mut span_id);\n        SpanId(span_id)\n    }\n\n    pub fn serialize_encore(&self) -> String {\n        crate::base32::encode(crate::base32::Alphabet::Encore, &self.0)\n    }\n\n    pub fn serialize_std(&self) -> String {\n        hex::encode(self.0)\n    }\n\n    pub fn parse_encore(s: &str) -> Result<Self, InvalidBase32> {\n        let Some(bytes) = crate::base32::decode(crate::base32::Alphabet::Encore, s) else {\n            return Err(InvalidBase32);\n        };\n        let span_id: [u8; 8] = bytes.try_into().map_err(|_| InvalidBase32)?;\n        Ok(SpanId(span_id))\n    }\n\n    pub fn parse_std(s: &str) -> Result<Self, hex::FromHexError> {\n        let bytes = hex::decode(s)?;\n        let span_id: [u8; 8] = bytes\n            .try_into()\n            .map_err(|_| hex::FromHexError::InvalidStringLength)?;\n        Ok(SpanId(span_id))\n    }\n}\n\npub struct APICall {\n    pub source: Option<Arc<Request>>,\n    pub target: EndpointName,\n}\n\n#[derive(Debug)]\npub struct Request {\n    /// The span for this request.\n    /// Always set even if the request is not traced, as it's used for request tracking.\n    pub span: SpanKey,\n\n    /// The trace that generated this trace.\n    pub parent_trace: Option<TraceId>,\n\n    /// The parent span for this request.\n    pub parent_span: Option<SpanKey>,\n\n    /// The event ID of the caller, if any.\n    pub caller_event_id: Option<TraceEventId>,\n\n    /// The externally-provided correlation ID, if any.\n    pub ext_correlation_id: Option<String>,\n\n    /// True if the request originated from the Encore Platform.\n    pub is_platform_request: bool,\n\n    /// Who's making the request, if any.\n    pub internal_caller: Option<Caller>,\n\n    /// When the request started.\n    pub start: Instant,\n    pub start_time: SystemTime,\n\n    /// Type-specific data.\n    pub data: RequestData,\n\n    /// Whether this request should be traced.\n    /// Determined by sampling rate or inherited from parent.\n    pub traced: bool,\n}\n\nimpl Request {\n    pub fn allows_private_endpoint_call(&self) -> bool {\n        if self.is_platform_request {\n            true\n        } else if let Some(caller) = &self.internal_caller {\n            caller.private_api_access()\n        } else {\n            false\n        }\n    }\n\n    pub fn has_authenticated_user(&self) -> bool {\n        match &self.data {\n            RequestData::RPC(data) => data.auth_user_id.is_some(),\n            RequestData::Stream(data) => data.auth_user_id.is_some(),\n            RequestData::Auth(_) => false,\n            RequestData::PubSub(_) => false,\n        }\n    }\n\n    pub fn take_raw_body(&self) -> Option<axum::body::Body> {\n        if let RequestData::RPC(data) = &self.data {\n            if let Some(data) = data.parsed_payload.as_ref() {\n                if let api::Body::Raw(body) = &data.body {\n                    return body.lock().unwrap().take();\n                }\n            }\n        }\n        None\n    }\n}\n\n#[derive(Debug)]\n#[allow(clippy::large_enum_variant)]\npub enum RequestData {\n    RPC(RPCRequestData),\n    Stream(StreamRequestData),\n    Auth(AuthRequestData),\n    PubSub(PubSubRequestData),\n}\n\n#[derive(Debug, Copy, Clone)]\npub enum StreamDirection {\n    InOut,\n    In,\n    Out,\n}\n\n#[derive(Debug)]\npub struct StreamRequestData {\n    /// The description of the endpoint.\n    pub endpoint: Arc<Endpoint>,\n\n    /// WebSocket Upgrade\n    pub websocket_upgrade: Mutex<Option<WebSocketUpgrade>>,\n\n    /// The request path.\n    pub path: String,\n    pub path_and_query: String,\n\n    /// The request path params, if any.\n    pub path_params: Option<IndexMap<String, PValue>>,\n\n    /// The request headers\n    pub req_headers: axum::http::HeaderMap,\n\n    /// The authenticated user id, if any.\n    pub auth_user_id: Option<String>,\n\n    /// The user data for the authenticated user, if any.\n    pub auth_data: Option<PValues>,\n\n    /// The parsed application payload.\n    pub parsed_payload: Option<api::RequestPayload>,\n\n    /// Stream direction\n    pub direction: StreamDirection,\n}\n\n#[derive(Debug)]\npub struct RPCRequestData {\n    /// The description of the endpoint.\n    pub endpoint: Arc<Endpoint>,\n\n    /// The request method.\n    pub method: api::schema::Method,\n\n    /// The request path.\n    pub path: String,\n    pub path_and_query: String,\n\n    /// The request path params, if any.\n    pub path_params: Option<IndexMap<String, PValue>>,\n\n    /// The request headers\n    pub req_headers: axum::http::HeaderMap,\n\n    /// The authenticated user id, if any.\n    pub auth_user_id: Option<String>,\n\n    /// The user data for the authenticated user, if any.\n    pub auth_data: Option<PValues>,\n\n    /// The parsed application payload.\n    pub parsed_payload: Option<api::RequestPayload>,\n}\n\npub struct AuthRequestData {\n    /// The name of the auth handler.\n    pub auth_handler: EndpointName,\n\n    /// The parsed authentication payload.\n    pub parsed_payload: auth::AuthPayload,\n}\n\nimpl Debug for AuthRequestData {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"AuthRequestData\")\n            .field(\"auth_handler\", &self.auth_handler)\n            .field(\"parsed_payload\", &self.parsed_payload)\n            .finish()\n    }\n}\n\n#[derive(Debug)]\npub struct PubSubRequestData {\n    /// The service processing the message.\n    pub service: EncoreName,\n    pub topic: EncoreName,\n    pub subscription: EncoreName,\n    pub message_id: String,\n    pub published: chrono::DateTime<Utc>,\n    pub attempt: u32,\n    pub payload: Vec<u8>,\n    pub parsed_payload: Option<PValues>,\n}\n\n#[derive(Debug)]\npub struct Response {\n    /// The request this response is for.\n    pub request: Arc<Request>,\n\n    /// How long the request took.\n    pub duration: std::time::Duration,\n\n    /// The result of the response.\n    pub data: ResponseData,\n}\n\n#[derive(Debug)]\npub enum ResponseData {\n    RPC(RPCResponseData),\n    Auth(Result<AuthSuccessResponse, api::Error>),\n    PubSub(Result<(), api::Error>),\n}\n\n#[derive(Debug)]\npub struct RPCResponseData {\n    /// The response status code.\n    pub status_code: u16,\n\n    /// The response payload.\n    pub resp_payload: Option<JSONPayload>,\n\n    /// The response headers.\n    pub resp_headers: axum::http::HeaderMap,\n\n    /// Any error that occurred.\n    pub error: Option<api::Error>,\n}\n\n#[derive(Debug)]\npub struct AuthSuccessResponse {\n    /// The resolved user id.\n    pub user_id: String,\n\n    /// The user data.\n    pub user_data: PValues,\n}\n\npub enum LogFieldValue<'a> {\n    String(&'a str),\n    U64(u64),\n    I64(i64),\n    F64(f64),\n    Bool(bool),\n    Json(&'a serde_json::Value),\n}\n\npub struct LogField<'a> {\n    pub key: &'a str,\n    pub value: LogFieldValue<'a>,\n}\n\nimpl LogField<'_> {\n    pub fn type_byte(&self) -> u8 {\n        match self.value {\n            LogFieldValue::String(_) => 2,\n            LogFieldValue::Bool(_) => 3,\n            LogFieldValue::I64(_) => 8,\n            LogFieldValue::Json(_) => 7,\n            LogFieldValue::U64(_) => 9,\n            LogFieldValue::F64(_) => 11,\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/names.rs",
    "content": "use anyhow::Context;\nuse std::borrow::Borrow;\nuse std::fmt::Display;\nuse std::hash::Hash;\nuse std::ops::Deref;\n\n#[derive(Debug, Clone, Eq, Hash, PartialEq)]\npub struct EncoreName(String);\n\nimpl Deref for EncoreName {\n    type Target = str;\n    fn deref(&self) -> &str {\n        &self.0\n    }\n}\n\nimpl AsRef<str> for EncoreName {\n    fn as_ref(&self) -> &str {\n        &self.0\n    }\n}\n\nimpl From<String> for EncoreName {\n    fn from(value: String) -> Self {\n        Self(value)\n    }\n}\n\nimpl From<&str> for EncoreName {\n    fn from(value: &str) -> Self {\n        Self(value.to_string())\n    }\n}\n\nimpl From<&String> for EncoreName {\n    fn from(value: &String) -> Self {\n        Self(value.clone())\n    }\n}\n\nimpl Borrow<str> for EncoreName {\n    fn borrow(&self) -> &str {\n        &self.0\n    }\n}\nimpl Borrow<str> for &EncoreName {\n    fn borrow(&self) -> &str {\n        &self.0\n    }\n}\n\nimpl Borrow<String> for EncoreName {\n    fn borrow(&self) -> &String {\n        &self.0\n    }\n}\n\nimpl Borrow<String> for &EncoreName {\n    fn borrow(&self) -> &String {\n        &self.0\n    }\n}\n\nimpl Display for EncoreName {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.0)\n    }\n}\n\nimpl From<EncoreName> for String {\n    fn from(value: EncoreName) -> Self {\n        value.0\n    }\n}\n\nimpl From<&EncoreName> for String {\n    fn from(value: &EncoreName) -> Self {\n        value.0.clone()\n    }\n}\n\n#[derive(Debug, Clone, Eq, Hash, PartialEq)]\npub struct CloudName(String);\n\nimpl Borrow<String> for &CloudName {\n    fn borrow(&self) -> &String {\n        &self.0\n    }\n}\n\nimpl Borrow<String> for CloudName {\n    fn borrow(&self) -> &String {\n        &self.0\n    }\n}\n\nimpl AsRef<str> for CloudName {\n    fn as_ref(&self) -> &str {\n        &self.0\n    }\n}\n\nimpl Deref for CloudName {\n    type Target = str;\n    fn deref(&self) -> &str {\n        &self.0\n    }\n}\n\nimpl Display for CloudName {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.0)\n    }\n}\n\nimpl From<String> for CloudName {\n    fn from(value: String) -> Self {\n        Self(value)\n    }\n}\n\nimpl From<CloudName> for String {\n    fn from(val: CloudName) -> Self {\n        val.0\n    }\n}\n\nimpl From<&CloudName> for String {\n    fn from(value: &CloudName) -> Self {\n        value.0.clone()\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct EndpointName {\n    /// The full name (\"service.endpoint\")\n    name: String,\n\n    /// Cached length of the service name.\n    service_len: usize,\n}\n\nimpl Hash for EndpointName {\n    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n        self.name.hash(state)\n    }\n}\n\nimpl PartialEq for EndpointName {\n    fn eq(&self, other: &Self) -> bool {\n        self.name == other.name\n    }\n}\n\nimpl Eq for EndpointName {}\n\nimpl Deref for EndpointName {\n    type Target = str;\n\n    fn deref(&self) -> &str {\n        &self.name\n    }\n}\n\nimpl Display for EndpointName {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.name)\n    }\n}\n\nimpl EndpointName {\n    pub fn new<S: Into<String>>(service: S, endpoint: S) -> Self {\n        let mut name = service.into();\n        let service_len = name.len();\n        name.push('.');\n        name.push_str(&endpoint.into());\n\n        Self { name, service_len }\n    }\n\n    pub fn service(&self) -> &str {\n        &self.name[..self.service_len]\n    }\n\n    pub fn endpoint(&self) -> &str {\n        &self.name[self.service_len + 1..]\n    }\n}\n\nimpl TryFrom<String> for EndpointName {\n    type Error = anyhow::Error;\n\n    fn try_from(value: String) -> Result<Self, Self::Error> {\n        // Find the '.'.\n        let idx = value.find('.').context(\"missing '.'\")?;\n        if idx == 0 {\n            anyhow::bail!(\"missing service name\");\n        } else if idx == value.len() - 1 {\n            anyhow::bail!(\"missing endpoint name\");\n        }\n\n        Ok(Self {\n            name: value,\n            service_len: idx,\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/gcs/bucket.rs",
    "content": "use async_stream::try_stream;\nuse futures::TryStreamExt;\nuse google_cloud_storage::http::objects::download::Range;\nuse google_cloud_storage::http::objects::get::GetObjectRequest;\nuse google_cloud_storage::http::objects::upload::{Media, UploadObjectRequest, UploadType};\nuse google_cloud_storage::sign::SignBy;\nuse google_cloud_storage::sign::SignedURLOptions;\nuse std::borrow::Cow;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\nuse std::time::SystemTime;\nuse tokio::io::AsyncRead;\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::objects::{\n    AttrsOptions, DeleteOptions, DownloadOptions, DownloadStream, DownloadUrlOptions, Error,\n    ExistsOptions, ListEntry, ListOptions, ObjectAttrs, PublicUrlError, UploadOptions,\n    UploadUrlOptions,\n};\nuse crate::{objects, CloudName, EncoreName};\nuse google_cloud_storage as gcs;\n\nuse super::LazyGCSClient;\n\n#[derive(Debug)]\npub struct Bucket {\n    client: Arc<LazyGCSClient>,\n    encore_name: EncoreName,\n    cloud_name: CloudName,\n    public_base_url: Option<String>,\n    key_prefix: Option<String>,\n    local_sign: Option<LocalSignOptions>,\n}\n\n#[derive(Debug)]\npub struct LocalSignOptions {\n    base_url: String,\n    access_id: String,\n    private_key: String,\n}\n\nfn local_sign_config_from_client(client: &LazyGCSClient) -> Option<LocalSignOptions> {\n    client.cfg.local_sign.as_ref().map(|cfg| LocalSignOptions {\n        base_url: cfg.base_url.clone(),\n        access_id: cfg.access_id.clone(),\n        private_key: cfg.private_key.clone(),\n    })\n}\n\nimpl Bucket {\n    pub(super) fn new(client: Arc<LazyGCSClient>, cfg: &pb::Bucket) -> Self {\n        let local_sign = local_sign_config_from_client(&client);\n        Self {\n            client,\n            encore_name: cfg.encore_name.clone().into(),\n            cloud_name: cfg.cloud_name.clone().into(),\n            public_base_url: cfg.public_base_url.clone(),\n            key_prefix: cfg.key_prefix.clone(),\n            local_sign,\n        }\n    }\n\n    /// Computes the object name, including the key prefix if present.\n    fn obj_name<'a>(&'_ self, name: Cow<'a, str>) -> Cow<'a, str> {\n        match &self.key_prefix {\n            Some(prefix) => {\n                let mut key = prefix.to_owned();\n                key.push_str(&name);\n                Cow::Owned(key)\n            }\n            None => name,\n        }\n    }\n\n    /// Returns the name with the key prefix stripped, if present.\n    fn strip_prefix<'a>(&'_ self, name: Cow<'a, str>) -> Cow<'a, str> {\n        match &self.key_prefix {\n            Some(prefix) => name\n                .as_ref()\n                .strip_prefix(prefix)\n                .map(|s| Cow::Owned(s.to_string()))\n                .unwrap_or(name),\n            None => name,\n        }\n    }\n}\n\nimpl objects::BucketImpl for Bucket {\n    fn name(&self) -> &EncoreName {\n        &self.encore_name\n    }\n\n    fn object(self: Arc<Self>, name: String) -> Arc<dyn objects::ObjectImpl> {\n        Arc::new(Object {\n            bkt: self,\n            key: name,\n        })\n    }\n\n    fn list(\n        self: Arc<Self>,\n        options: ListOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::ListStream, objects::Error>> + Send + 'static>>\n    {\n        Box::pin(async move {\n            match self.client.get().await {\n                Ok(client) => {\n                    let client = client.clone();\n\n                    let mut total_seen = 0;\n                    const DEFAULT_MAX_RESULTS: u64 = 1000;\n                    let s: objects::ListStream = Box::new(try_stream! {\n                        let max_results = if let Some(limit) = options.limit {\n                            limit.min(DEFAULT_MAX_RESULTS) as i32\n                        } else {\n                            DEFAULT_MAX_RESULTS as i32\n                        };\n\n                        let mut req = gcs::http::objects::list::ListObjectsRequest {\n                            bucket: self.cloud_name.to_string(),\n                            max_results: Some(max_results),\n                            ..Default::default()\n                        };\n\n\n                        // Filter by key prefix, if provided.\n                        if let Some(key_prefix) = &self.key_prefix {\n                            req.prefix = Some(key_prefix.clone());\n                        }\n\n                        if let Some(key_prefix) = &options.prefix {\n                            req.prefix = Some(req.prefix.unwrap_or_default().clone() + key_prefix);\n                        }\n\n                        'PageLoop:\n                        loop {\n                            let resp = client.list_objects(&req).await.map_err(|e| Error::Other(e.into()))?;\n                            if let Some(items) = resp.items {\n                                for obj in items {\n                                    total_seen += 1;\n                                    if let Some(limit) = options.limit {\n                                        if total_seen > limit {\n                                            break 'PageLoop;\n                                        }\n                                    }\n\n                                    let entry = ListEntry {\n                                        name: self.strip_prefix(Cow::Owned(obj.name)).into_owned(),\n                                        size: obj.size as u64,\n                                        etag: obj.etag,\n                                    };\n                                    yield entry;\n                                }\n                            }\n\n                            req.page_token = resp.next_page_token;\n\n                            // Are we close to being done? If so, adjust the max_results\n                            // to avoid over-fetching.\n                            if let Some(limit) = options.limit {\n                                if total_seen >= limit {\n                                    break 'PageLoop;\n                                }\n                                let remaining = limit - total_seen;\n                                req.max_results = Some(remaining.min(DEFAULT_MAX_RESULTS) as i32);\n                            }\n\n                            if req.page_token.is_none() {\n                                break;\n                            }\n                        }\n                    });\n\n                    Ok(s)\n                }\n\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n}\n\n#[derive(Debug)]\nstruct Object {\n    bkt: Arc<Bucket>,\n    key: String,\n}\n\nimpl objects::ObjectImpl for Object {\n    fn bucket_name(&self) -> &EncoreName {\n        &self.bkt.encore_name\n    }\n\n    fn key(&self) -> &str {\n        &self.key\n    }\n\n    fn attrs(\n        self: Arc<Self>,\n        options: AttrsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ObjectAttrs, Error>> + Send>> {\n        Box::pin(async move {\n            match self.bkt.client.get().await {\n                Ok(client) => {\n                    let mut req = gcs::http::objects::get::GetObjectRequest {\n                        bucket: self.bkt.cloud_name.to_string(),\n                        object: self.bkt.obj_name(Cow::Borrowed(&self.key)).into_owned(),\n                        ..Default::default()\n                    };\n\n                    if let Some(version) = options.version {\n                        req.generation = Some(parse_version(version)?);\n                    }\n\n                    let obj = client.get_object(&req).await.map_err(map_err)?;\n                    Ok(ObjectAttrs {\n                        name: obj.name,\n                        version: Some(obj.generation.to_string()),\n                        size: obj.size as u64,\n                        content_type: obj.content_type,\n                        etag: obj.etag,\n                    })\n                }\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n\n    fn signed_upload_url(\n        self: Arc<Self>,\n        options: UploadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>> {\n        let gcs_opts = SignedURLOptions {\n            method: gcs::sign::SignedURLMethod::PUT,\n            expires: options.ttl,\n            start_time: Some(SystemTime::now()),\n            ..Default::default()\n        };\n        self.signed_url(gcs_opts)\n    }\n\n    fn signed_download_url(\n        self: Arc<Self>,\n        options: DownloadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>> {\n        let gcs_opts = SignedURLOptions {\n            method: gcs::sign::SignedURLMethod::GET,\n            expires: options.ttl,\n            start_time: Some(SystemTime::now()),\n            ..Default::default()\n        };\n        self.signed_url(gcs_opts)\n    }\n\n    fn exists(\n        self: Arc<Self>,\n        options: ExistsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send>> {\n        Box::pin(async move {\n            match self.bkt.client.get().await {\n                Ok(client) => {\n                    let mut req = gcs::http::objects::get::GetObjectRequest {\n                        bucket: self.bkt.cloud_name.to_string(),\n                        object: self.bkt.obj_name(Cow::Borrowed(&self.key)).into_owned(),\n                        ..Default::default()\n                    };\n\n                    if let Some(version) = options.version {\n                        req.generation = Some(parse_version(version)?);\n                    }\n\n                    match client.get_object(&req).await.map_err(map_err) {\n                        Ok(_obj) => Ok(true),\n                        Err(Error::NotFound) => Ok(false),\n                        Err(err) => Err(err),\n                    }\n                }\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n\n    fn upload(\n        self: Arc<Self>,\n        data: Box<dyn AsyncRead + Unpin + Send + Sync + 'static>,\n        opts: objects::UploadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ObjectAttrs, Error>> + Send>> {\n        Box::pin(async move {\n            match self.bkt.client.get().await {\n                Ok(client) => {\n                    let mut req = UploadObjectRequest {\n                        bucket: self.bkt.cloud_name.to_string(),\n                        ..Default::default()\n                    };\n\n                    let cloud_name = self.bkt.obj_name(Cow::Borrowed(&self.key));\n                    let mut media = Media::new(cloud_name.into_owned());\n\n                    apply_upload_opts(opts, &mut req, &mut media);\n\n                    let upload_type = UploadType::Simple(media);\n                    let stream = tokio_util::io::ReaderStream::new(data);\n\n                    match client\n                        .upload_streamed_object(&req, stream, &upload_type)\n                        .await\n                    {\n                        Ok(obj) => Ok(ObjectAttrs {\n                            name: obj.name,\n                            version: Some(obj.generation.to_string()),\n                            size: obj.size as u64,\n                            content_type: obj.content_type,\n                            etag: obj.etag,\n                        }),\n                        Err(err) => Err(map_err(err)),\n                    }\n                }\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n\n    fn download(\n        self: Arc<Self>,\n        options: DownloadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<DownloadStream, Error>> + Send>> {\n        fn convert_err(err: gcs::http::Error) -> Error {\n            use gcs::http::error::ErrorResponse;\n            match err {\n                gcs::http::Error::Response(ErrorResponse { code: 404, .. }) => Error::NotFound,\n                gcs::http::Error::HttpClient(err)\n                    if err.status().map(|s| s.as_u16()) == Some(404) =>\n                {\n                    Error::NotFound\n                }\n                err => Error::Other(err.into()),\n            }\n        }\n\n        Box::pin(async move {\n            match self.bkt.client.get().await {\n                Ok(client) => {\n                    let mut req = GetObjectRequest {\n                        bucket: self.bkt.cloud_name.to_string(),\n                        object: self.bkt.obj_name(Cow::Borrowed(&self.key)).into_owned(),\n                        ..Default::default()\n                    };\n\n                    if let Some(version) = options.version {\n                        req.generation = Some(parse_version(version)?);\n                    }\n\n                    let resp = client\n                        .download_streamed_object(&req, &Range::default())\n                        .await;\n\n                    let stream = resp.map_err(convert_err)?;\n                    let stream: DownloadStream = Box::pin(stream.map_err(convert_err));\n                    Ok(stream)\n                }\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n\n    fn delete(\n        self: Arc<Self>,\n        options: DeleteOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> {\n        Box::pin(async move {\n            match self.bkt.client.get().await {\n                Ok(client) => {\n                    let mut req = gcs::http::objects::delete::DeleteObjectRequest {\n                        bucket: self.bkt.cloud_name.to_string(),\n                        object: self.bkt.obj_name(Cow::Borrowed(&self.key)).into_owned(),\n                        ..Default::default()\n                    };\n\n                    if let Some(version) = options.version {\n                        req.generation = Some(parse_version(version)?);\n                    }\n\n                    match client.delete_object(&req).await.map_err(map_err) {\n                        Ok(_) => Ok(()),\n                        Err(Error::NotFound) => Err(Error::NotFound),\n                        Err(err) => Err(err),\n                    }\n                }\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n\n    fn public_url(&self) -> Result<String, PublicUrlError> {\n        let Some(base_url) = self.bkt.public_base_url.clone() else {\n            return Err(PublicUrlError::PrivateBucket);\n        };\n\n        let url = objects::public_url(base_url, &self.key);\n        Ok(url)\n    }\n}\n\nimpl Object {\n    fn signed_url(\n        self: Arc<Self>,\n        gcs_opts: SignedURLOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>> {\n        Box::pin(async move {\n            match self.bkt.client.get().await {\n                Ok(client) => {\n                    // We use a fake GCS service for local development. Ideally, the runtime\n                    // code would be oblivious to this once the GCS client is set up. But that\n                    // turns out to be difficult for URL signing, so we add a special case\n                    // here.\n                    let local_sign = &self.bkt.local_sign;\n                    let (access_id, sign_by) = match local_sign {\n                        Some(opt) => (\n                            Some(opt.access_id.clone()),\n                            Some(SignBy::PrivateKey(opt.private_key.as_bytes().to_vec())),\n                        ),\n                        None => (None, None),\n                    };\n\n                    let name = self.bkt.obj_name(Cow::Borrowed(&self.key)).into_owned();\n                    let mut url = client\n                        .signed_url(&self.bkt.cloud_name, &name, access_id, sign_by, gcs_opts)\n                        .await\n                        .map_err(|e| Error::Internal(e.into()))?;\n\n                    // More special handling for the local dev case.\n                    if let Some(cfg) = local_sign {\n                        url = replace_url_prefix(&url, &cfg.base_url)\n                            .into_owned()\n                            .to_string();\n                    }\n\n                    Ok(url)\n                }\n                Err(err) => Err(Error::Internal(anyhow::anyhow!(\n                    \"unable to resolve client: {}\",\n                    err\n                ))),\n            }\n        })\n    }\n}\n\nfn replace_url_prefix<'a>(orig_url: &'a str, base: &str) -> Cow<'a, str> {\n    match url::Url::parse(orig_url) {\n        Ok(url) => {\n            let mut out = match url.path().is_empty() {\n                true => base.to_string(),\n                false => {\n                    format!(\n                        \"{}/{}\",\n                        base.trim_end_matches('/'),\n                        url.path().trim_start_matches(\"/\")\n                    )\n                }\n            };\n            if let Some(query) = url.query() {\n                out.push('?');\n                out.push_str(query);\n            }\n            Cow::Owned(out)\n        }\n        Err(_) => {\n            // If the input URL fails parsing, just don't do the replace\n            Cow::Borrowed(orig_url)\n        }\n    }\n}\n\nfn apply_upload_opts(opts: UploadOptions, req: &mut UploadObjectRequest, media: &mut Media) {\n    if let Some(content_type) = opts.content_type {\n        media.content_type = Cow::Owned(content_type);\n    }\n    if let Some(pre) = opts.preconditions {\n        if pre.not_exists == Some(true) {\n            req.if_generation_match = Some(0);\n        }\n    }\n}\n\nfn parse_version(version: String) -> Result<i64, Error> {\n    version.parse().map_err(|err| {\n        Error::Other(anyhow::anyhow!(\n            \"invalid version number {}: {}\",\n            version,\n            err\n        ))\n    })\n}\n\nfn map_err(err: gcs::http::Error) -> Error {\n    use gcs::http::error::ErrorResponse;\n    match err {\n        gcs::http::Error::Response(ErrorResponse { code, .. }) => match code {\n            404 => Error::NotFound,\n            412 => Error::PreconditionFailed,\n            _ => Error::Other(err.into()),\n        },\n        gcs::http::Error::HttpClient(err) => {\n            let status = err.status().map(|s| s.as_u16());\n            match status {\n                Some(404) => Error::NotFound,\n                Some(412) => Error::PreconditionFailed,\n                _ => Error::Other(err.into()),\n            }\n        }\n        err => Error::Other(err.into()),\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/gcs/mod.rs",
    "content": "use std::fmt::Debug;\nuse std::sync::Arc;\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::objects;\nuse crate::objects::gcs::bucket::Bucket;\nuse anyhow::Context;\nuse google_cloud_storage as gcs;\n\nmod bucket;\n\n#[derive(Debug)]\npub struct Cluster {\n    client: Arc<LazyGCSClient>,\n}\n\nimpl Cluster {\n    pub fn new(cfg: pb::bucket_cluster::Gcs) -> Self {\n        let client = Arc::new(LazyGCSClient::new(cfg));\n\n        // Begin initializing the client in the background.\n        tokio::spawn(client.clone().begin_initialize());\n\n        Self { client }\n    }\n}\n\nimpl objects::ClusterImpl for Cluster {\n    fn bucket(self: Arc<Self>, cfg: &pb::Bucket) -> Arc<dyn objects::BucketImpl + 'static> {\n        Arc::new(Bucket::new(self.client.clone(), cfg))\n    }\n}\n\nstruct LazyGCSClient {\n    cfg: pb::bucket_cluster::Gcs,\n    cell: tokio::sync::OnceCell<anyhow::Result<Arc<gcs::client::Client>>>,\n}\n\nimpl Debug for LazyGCSClient {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"LazyGCPClient\").finish()\n    }\n}\n\nimpl LazyGCSClient {\n    fn new(cfg: pb::bucket_cluster::Gcs) -> Self {\n        Self {\n            cfg,\n            cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn get(&self) -> &anyhow::Result<Arc<gcs::client::Client>> {\n        self.cell.get_or_init(|| initialize(&self.cfg)).await\n    }\n\n    async fn begin_initialize(self: Arc<Self>) {\n        self.get().await;\n    }\n}\n\nasync fn initialize(cfg: &pb::bucket_cluster::Gcs) -> anyhow::Result<Arc<gcs::client::Client>> {\n    let mut config = gcs::client::ClientConfig::default();\n    if let Some(endpoint) = &cfg.endpoint {\n        config.storage_endpoint.clone_from(endpoint);\n    }\n\n    if cfg.anonymous {\n        config = config.anonymous();\n    } else {\n        config = config\n            .with_auth()\n            .await\n            .inspect_err(|e| log::error!(\"unable to resolve client config: {e:?}\"))\n            .context(\"unable to resolve client config\")?;\n    }\n\n    Ok(Arc::new(gcs::client::Client::new(config)))\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/manager.rs",
    "content": "use std::collections::HashMap;\nuse std::sync::{Arc, RwLock};\n\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::names::EncoreName;\nuse crate::objects::{gcs, noop, s3, BucketImpl, ClusterImpl};\nuse crate::secrets;\nuse crate::trace::Tracer;\n\nuse super::Bucket;\n\npub struct Manager {\n    tracer: Tracer,\n    bucket_cfg: HashMap<EncoreName, (Arc<dyn ClusterImpl>, pb::Bucket)>,\n\n    buckets: Arc<RwLock<HashMap<EncoreName, Arc<dyn BucketImpl>>>>,\n}\n\nimpl Manager {\n    pub fn new(\n        secrets: &secrets::Manager,\n        tracer: Tracer,\n        clusters: Vec<pb::BucketCluster>,\n        md: &meta::Data,\n    ) -> Self {\n        let bucket_cfg = make_cfg_maps(secrets, clusters, md);\n\n        Self {\n            tracer,\n            bucket_cfg,\n            buckets: Arc::default(),\n        }\n    }\n\n    pub fn bucket(&self, name: EncoreName) -> Option<Bucket> {\n        let imp = self.bucket_impl(name)?;\n        Some(Bucket {\n            imp,\n            tracer: self.tracer.clone(),\n        })\n    }\n\n    fn bucket_impl(&self, name: EncoreName) -> Option<Arc<dyn BucketImpl>> {\n        if let Some(bkt) = self.buckets.read().unwrap().get(&name) {\n            return Some(bkt.clone());\n        }\n\n        let bkt = {\n            if let Some((cluster, bucket_cfg)) = self.bucket_cfg.get(&name) {\n                cluster.clone().bucket(bucket_cfg)\n            } else {\n                Arc::new(noop::Bucket::new(name.clone()))\n            }\n        };\n\n        self.buckets.write().unwrap().insert(name, bkt.clone());\n        Some(bkt)\n    }\n}\n\nfn make_cfg_maps(\n    secrets: &secrets::Manager,\n    clusters: Vec<pb::BucketCluster>,\n    _md: &meta::Data,\n) -> HashMap<EncoreName, (Arc<dyn ClusterImpl>, pb::Bucket)> {\n    let mut bucket_map = HashMap::new();\n\n    for cluster_cfg in clusters {\n        let cluster = match cluster_cfg.provider {\n            Some(provider) => new_cluster(secrets, provider),\n            None => {\n                log::error!(\"missing bucket cluster provider: {}\", cluster_cfg.rid);\n                Arc::new(noop::Cluster)\n            }\n        };\n\n        for bucket_cfg in cluster_cfg.buckets {\n            bucket_map.insert(\n                bucket_cfg.encore_name.clone().into(),\n                (cluster.clone(), bucket_cfg),\n            );\n        }\n    }\n\n    bucket_map\n}\n\nfn new_cluster(\n    secrets: &secrets::Manager,\n    provider: pb::bucket_cluster::Provider,\n) -> Arc<dyn ClusterImpl> {\n    match provider {\n        pb::bucket_cluster::Provider::S3(s3cfg) => {\n            let secret_access_key = s3cfg\n                .secret_access_key\n                .as_ref()\n                .map(|k| secrets.load(k.clone()));\n            Arc::new(s3::Cluster::new(s3cfg, secret_access_key))\n        }\n        pb::bucket_cluster::Provider::Gcs(gcscfg) => Arc::new(gcs::Cluster::new(gcscfg.clone())),\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/mod.rs",
    "content": "use bytes::Bytes;\nuse futures::{Stream, StreamExt};\nuse std::borrow::Cow;\nuse std::future::Future;\nuse std::sync::Arc;\nuse std::time::Duration;\nuse std::{fmt::Debug, pin::Pin};\nuse thiserror::Error;\nuse tokio::io::AsyncRead;\n\npub use manager::Manager;\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::trace::{protocol, Tracer};\nuse crate::{model, EncoreName};\n\nmod gcs;\nmod manager;\nmod noop;\nmod s3;\n\ntrait ClusterImpl: Debug + Send + Sync {\n    fn bucket(self: Arc<Self>, cfg: &pb::Bucket) -> Arc<dyn BucketImpl + 'static>;\n}\n\ntrait BucketImpl: Debug + Send + Sync {\n    #[allow(dead_code)]\n    fn name(&self) -> &EncoreName;\n\n    fn object(self: Arc<Self>, name: String) -> Arc<dyn ObjectImpl + 'static>;\n\n    fn list(\n        self: Arc<Self>,\n        options: ListOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ListStream, Error>> + Send + 'static>>;\n}\n\ntype ListStream = Box<dyn Stream<Item = Result<ListEntry, Error>> + Send>;\n\ntrait ObjectImpl: Debug + Send + Sync {\n    fn bucket_name(&self) -> &EncoreName;\n    fn key(&self) -> &str;\n\n    fn exists(\n        self: Arc<Self>,\n        options: ExistsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send>>;\n\n    fn upload(\n        self: Arc<Self>,\n        data: Box<dyn AsyncRead + Unpin + Send + Sync + 'static>,\n        options: UploadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ObjectAttrs, Error>> + Send>>;\n\n    fn signed_upload_url(\n        self: Arc<Self>,\n        options: UploadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>>;\n\n    fn signed_download_url(\n        self: Arc<Self>,\n        options: DownloadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>>;\n\n    fn download(\n        self: Arc<Self>,\n        options: DownloadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<DownloadStream, Error>> + Send>>;\n\n    fn attrs(\n        self: Arc<Self>,\n        options: AttrsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ObjectAttrs, Error>> + Send>>;\n\n    fn delete(\n        self: Arc<Self>,\n        options: DeleteOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>;\n\n    fn public_url(&self) -> Result<String, PublicUrlError>;\n}\n\n#[derive(Debug)]\npub struct Bucket {\n    tracer: Tracer,\n    imp: Arc<dyn BucketImpl>,\n}\n\nimpl Bucket {\n    pub fn object(&self, name: String) -> Object {\n        Object {\n            imp: self.imp.clone().object(name),\n            tracer: self.tracer.clone(),\n        }\n    }\n\n    pub async fn list(\n        &self,\n        options: ListOptions,\n        source: Option<Arc<model::Request>>,\n    ) -> Result<ListIterator, Error> {\n        let start_id = source.as_deref().and_then(|source| {\n            self.tracer\n                .bucket_list_objects_start(protocol::BucketListObjectsStart {\n                    source,\n                    bucket: self.imp.name(),\n                    prefix: options.prefix.as_deref(),\n                })\n        });\n\n        let stream = match self.imp.clone().list(options).await {\n            Ok(stream) => stream,\n            Err(err) => {\n                if let Some(source) = source.as_deref() {\n                    self.tracer\n                        .bucket_list_objects_end(protocol::BucketListObjectsEnd {\n                            source,\n                            start_id,\n                            result: protocol::BucketListObjectsEndResult::Err(&err),\n                        });\n                }\n                return Err(err);\n            }\n        };\n\n        Ok(ListIterator {\n            stream: stream.into(),\n            source,\n            start_id,\n            tracer: self.tracer.clone(),\n\n            yielded_entries: 0,\n            seen_end: false,\n            err: None,\n        })\n    }\n}\n\n#[derive(Debug)]\npub struct Object {\n    tracer: Tracer,\n    imp: Arc<dyn ObjectImpl>,\n}\n\n#[derive(Debug, Error)]\npub enum PublicUrlError {\n    #[error(\"bucket is not public\")]\n    PrivateBucket,\n    #[error(\"invalid object name\")]\n    InvalidObjectName,\n    #[error(\"public url not supported in noop bucket\")]\n    NoopBucket,\n}\n\nimpl Object {\n    pub async fn exists(\n        &self,\n        options: ExistsOptions,\n        source: Option<Arc<model::Request>>,\n    ) -> Result<bool, Error> {\n        let start_id = source.as_deref().and_then(|source| {\n            self.tracer\n                .bucket_object_get_attrs_start(protocol::BucketObjectGetAttrsStart {\n                    source,\n                    bucket: self.imp.bucket_name(),\n                    object: self.imp.key(),\n                    version: options.version.as_deref(),\n                })\n        });\n\n        let res = self.imp.clone().exists(options).await;\n\n        if let Some(source) = source.as_deref() {\n            self.tracer\n                .bucket_object_get_attrs_end(protocol::BucketObjectGetAttrsEnd {\n                    start_id,\n                    source,\n                    result: match &res {\n                        Ok(true) => {\n                            protocol::BucketObjectGetAttrsEndResult::Success(Default::default())\n                        }\n                        Ok(false) => protocol::BucketObjectGetAttrsEndResult::Err(&Error::NotFound),\n                        Err(err) => protocol::BucketObjectGetAttrsEndResult::Err(err),\n                    },\n                });\n        }\n        res\n    }\n\n    pub fn upload(\n        &self,\n        data: Box<dyn AsyncRead + Unpin + Send + Sync + 'static>,\n        options: UploadOptions,\n        source: Option<Arc<model::Request>>,\n    ) -> impl Future<Output = Result<ObjectAttrs, Error>> + Send + 'static {\n        let tracer = self.tracer.clone();\n        let imp = self.imp.clone();\n\n        async move {\n            let start_id = source.as_deref().and_then(|source| {\n                tracer.bucket_object_upload_start(protocol::BucketObjectUploadStart {\n                    source,\n                    bucket: imp.bucket_name(),\n                    object: imp.key(),\n                    attrs: protocol::BucketObjectAttributes {\n                        content_type: options.content_type.as_deref(),\n                        ..Default::default()\n                    },\n                })\n            });\n\n            let res = imp.upload(data, options).await;\n\n            if let Some(source) = source.as_deref() {\n                tracer.bucket_object_upload_end(protocol::BucketObjectUploadEnd {\n                    start_id,\n                    source,\n                    result: match &res {\n                        Ok(attrs) => protocol::BucketObjectUploadEndResult::Success {\n                            size: attrs.size,\n                            version: attrs.version.as_deref(),\n                        },\n                        Err(err) => protocol::BucketObjectUploadEndResult::Err(err),\n                    },\n                });\n            }\n\n            res\n        }\n    }\n\n    pub async fn signed_upload_url(\n        &self,\n        options: UploadUrlOptions,\n        _source: Option<Arc<model::Request>>,\n    ) -> Result<String, Error> {\n        const SEVEN_DAYS: Duration = Duration::new(7 * 86400, 0);\n        if options.ttl > SEVEN_DAYS {\n            return Err(Error::InvalidArgument);\n        }\n        self.imp.clone().signed_upload_url(options).await\n    }\n\n    pub async fn signed_download_url(\n        &self,\n        options: DownloadUrlOptions,\n        _source: Option<Arc<model::Request>>,\n    ) -> Result<String, Error> {\n        const SEVEN_DAYS: Duration = Duration::new(7 * 86400, 0);\n        if options.ttl > SEVEN_DAYS {\n            return Err(Error::InvalidArgument);\n        }\n        self.imp.clone().signed_download_url(options).await\n    }\n\n    pub fn download_stream(\n        &self,\n        options: DownloadOptions,\n        _source: Option<Arc<model::Request>>,\n    ) -> impl Future<Output = Result<DownloadStream, Error>> + Send + 'static {\n        self.imp.clone().download(options)\n    }\n\n    pub fn download_all(\n        &self,\n        options: DownloadOptions,\n        source: Option<Arc<model::Request>>,\n    ) -> impl Future<Output = Result<Vec<u8>, Error>> + Send + 'static {\n        let tracer = self.tracer.clone();\n        let imp = self.imp.clone();\n        let start_id = source.as_deref().and_then(|source| {\n            tracer.bucket_object_download_start(protocol::BucketObjectDownloadStart {\n                source,\n                bucket: imp.bucket_name(),\n                object: imp.key(),\n                version: options.version.as_deref(),\n            })\n        });\n\n        let fut = self.do_download_all(options);\n        async move {\n            let res = fut.await;\n\n            if let Some(source) = source.as_deref() {\n                tracer.bucket_object_download_end(protocol::BucketObjectDownloadEnd {\n                    start_id,\n                    source,\n                    result: match &res {\n                        Ok(bytes) => protocol::BucketObjectDownloadEndResult::Success {\n                            size: bytes.len() as u64,\n                        },\n                        Err(err) => protocol::BucketObjectDownloadEndResult::Err(err),\n                    },\n                });\n            }\n\n            res\n        }\n    }\n\n    fn do_download_all(\n        &self,\n        options: DownloadOptions,\n    ) -> impl Future<Output = Result<Vec<u8>, Error>> + Send + 'static {\n        let stream = self.imp.clone().download(options);\n        async move {\n            let mut bytes = Vec::new();\n            let mut stream = stream.await?;\n\n            while let Some(chunk) = stream.next().await {\n                bytes.extend_from_slice(&chunk?);\n            }\n            Ok(bytes)\n        }\n    }\n\n    pub async fn attrs(\n        &self,\n        options: AttrsOptions,\n        source: Option<Arc<model::Request>>,\n    ) -> Result<ObjectAttrs, Error> {\n        let start_id = source.as_deref().and_then(|source| {\n            self.tracer\n                .bucket_object_get_attrs_start(protocol::BucketObjectGetAttrsStart {\n                    source,\n                    bucket: self.imp.bucket_name(),\n                    object: self.imp.key(),\n                    version: options.version.as_deref(),\n                })\n        });\n\n        let res = self.imp.clone().attrs(options).await;\n\n        if let Some(source) = source.as_deref() {\n            self.tracer\n                .bucket_object_get_attrs_end(protocol::BucketObjectGetAttrsEnd {\n                    start_id,\n                    source,\n                    result: match &res {\n                        Ok(attrs) => protocol::BucketObjectGetAttrsEndResult::Success(attrs.into()),\n                        Err(err) => protocol::BucketObjectGetAttrsEndResult::Err(err),\n                    },\n                });\n        }\n        res\n    }\n\n    pub async fn delete(\n        &self,\n        options: DeleteOptions,\n        source: Option<Arc<model::Request>>,\n    ) -> Result<(), Error> {\n        let start_id = source.as_deref().and_then(|source| {\n            self.tracer\n                .bucket_delete_objects_start(protocol::BucketDeleteObjectsStart {\n                    source,\n                    bucket: self.imp.bucket_name(),\n                    objects: [protocol::BucketDeleteObjectEntry {\n                        object: self.imp.key(),\n                        version: options.version.as_deref(),\n                    }]\n                    .into_iter(),\n                })\n        });\n\n        let res = self.imp.clone().delete(options).await;\n\n        if let Some(source) = source.as_deref() {\n            self.tracer\n                .bucket_delete_objects_end(protocol::BucketDeleteObjectsEnd {\n                    start_id,\n                    source,\n                    result: match &res {\n                        Ok(()) => protocol::BucketDeleteObjectsEndResult::Success,\n                        Err(err) => protocol::BucketDeleteObjectsEndResult::Err(err),\n                    },\n                });\n        }\n\n        res\n    }\n\n    /// Returns the public URL of the object, if available.\n    /// If the bucket is not public, it reports None.\n    pub fn public_url(&self) -> Result<String, PublicUrlError> {\n        self.imp.public_url()\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[error(\"object not found\")]\n    NotFound,\n\n    #[error(\"precondition failed\")]\n    PreconditionFailed,\n\n    #[error(\"invalid argument\")]\n    InvalidArgument,\n\n    #[error(\"internal error: {0:?}\")]\n    Internal(anyhow::Error),\n\n    #[error(\"{0:?}\")]\n    Other(anyhow::Error),\n}\n\npub type DownloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, Error>> + Send>>;\n\npub struct ObjectAttrs {\n    pub name: String,\n    pub version: Option<String>,\n    pub size: u64,\n    pub content_type: Option<String>,\n    pub etag: String,\n}\n\npub struct ListEntry {\n    pub name: String,\n    pub size: u64,\n    pub etag: String,\n}\n\n#[derive(Debug, Default)]\npub struct ExistsOptions {\n    pub version: Option<String>,\n}\n\n#[derive(Debug, Default)]\npub struct UploadOptions {\n    pub content_type: Option<String>,\n    pub preconditions: Option<UploadPreconditions>,\n}\n\n#[derive(Debug, Default)]\npub struct UploadPreconditions {\n    pub not_exists: Option<bool>,\n}\n\n#[derive(Debug, Default)]\npub struct DownloadOptions {\n    pub version: Option<String>,\n}\n\n#[derive(Debug, Default)]\npub struct AttrsOptions {\n    pub version: Option<String>,\n}\n\n#[derive(Debug, Default)]\npub struct UploadUrlOptions {\n    pub ttl: Duration,\n}\n\n#[derive(Debug, Default)]\npub struct DownloadUrlOptions {\n    pub ttl: Duration,\n}\n\n#[derive(Debug, Default)]\npub struct DeleteOptions {\n    pub version: Option<String>,\n}\n\n#[derive(Debug, Default)]\npub struct ListOptions {\n    pub prefix: Option<String>,\n    pub limit: Option<u64>,\n}\n\npub struct ListIterator {\n    stream: Pin<Box<dyn Stream<Item = Result<ListEntry, Error>> + Send>>,\n    tracer: Tracer,\n    start_id: Option<model::TraceEventId>,\n    source: Option<Arc<model::Request>>,\n    err: Option<String>,\n\n    yielded_entries: u64,\n    seen_end: bool,\n}\n\nimpl ListIterator {\n    pub async fn next(&mut self) -> Option<Result<ListEntry, Error>> {\n        let res = self.stream.next().await;\n\n        match &res {\n            None => {\n                self.seen_end = true;\n            }\n            Some(Ok(_)) => {\n                self.yielded_entries += 1;\n            }\n            Some(Err(err)) => {\n                if self.err.is_none() {\n                    self.err = Some(err.to_string());\n                }\n            }\n        }\n\n        res\n    }\n}\n\nimpl Drop for ListIterator {\n    fn drop(&mut self) {\n        if let Some(source) = self.source.as_deref() {\n            self.tracer\n                .bucket_list_objects_end(protocol::BucketListObjectsEnd {\n                    start_id: self.start_id,\n                    source,\n                    result: match self.err {\n                        Some(ref err) => protocol::BucketListObjectsEndResult::Err(err),\n                        None => protocol::BucketListObjectsEndResult::Success {\n                            observed: self.yielded_entries,\n                            has_more: !self.seen_end,\n                        },\n                    },\n                });\n        }\n    }\n}\n\nuse percent_encoding::{AsciiSet, CONTROLS};\n\n// From https://url.spec.whatwg.org/#c0-control-percent-encode-set\n\nconst QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'\"').add(b'#').add(b'<').add(b'>');\nconst PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}');\n\nfn escape_path(s: &str) -> Cow<'_, str> {\n    percent_encoding::percent_encode(s.as_bytes(), PATH).into()\n}\n\n/// Computes the public url given a base url and object name.\nfn public_url(base_url: String, name: &str) -> String {\n    let mut url = base_url;\n\n    if !url.ends_with('/') {\n        url.push('/');\n    }\n    url.push_str(&escape_path(name));\n    url\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/noop/mod.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse futures::future;\nuse tokio::io::AsyncRead;\n\nuse crate::objects;\nuse crate::{encore::runtime::v1 as pb, EncoreName};\n\nuse super::{\n    AttrsOptions, DeleteOptions, DownloadOptions, DownloadUrlOptions, ExistsOptions, ListOptions,\n    PublicUrlError, UploadUrlOptions,\n};\n\n#[derive(Debug)]\npub struct Cluster;\n\n#[derive(Debug)]\npub struct Bucket {\n    name: EncoreName,\n}\n\nimpl Bucket {\n    pub fn new(name: EncoreName) -> Self {\n        Self { name }\n    }\n}\n\n#[derive(Debug)]\npub struct Object {\n    bkt: Arc<Bucket>,\n    name: String,\n}\n\nimpl objects::ClusterImpl for Cluster {\n    fn bucket(self: Arc<Self>, cfg: &pb::Bucket) -> Arc<dyn objects::BucketImpl> {\n        Arc::new(Bucket {\n            name: cfg.encore_name.clone().into(),\n        })\n    }\n}\n\nimpl objects::BucketImpl for Bucket {\n    fn name(&self) -> &EncoreName {\n        &self.name\n    }\n\n    fn object(self: Arc<Self>, name: String) -> Arc<dyn objects::ObjectImpl> {\n        Arc::new(Object {\n            name,\n            bkt: self.clone(),\n        })\n    }\n\n    fn list(\n        self: Arc<Self>,\n        _options: ListOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::ListStream, objects::Error>> + Send + 'static>>\n    {\n        Box::pin(async move {\n            Err(objects::Error::Internal(anyhow::anyhow!(\n                \"noop bucket does not support list\"\n            )))\n        })\n    }\n}\n\nimpl objects::ObjectImpl for Object {\n    fn bucket_name(&self) -> &EncoreName {\n        &self.bkt.name\n    }\n\n    fn key(&self) -> &str {\n        &self.name\n    }\n\n    fn attrs(\n        self: Arc<Self>,\n        _options: AttrsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::ObjectAttrs, objects::Error>> + Send>> {\n        Box::pin(future::ready(Err(objects::Error::Internal(\n            anyhow::anyhow!(\"noop bucket does not support attrs\"),\n        ))))\n    }\n\n    fn signed_upload_url(\n        self: Arc<Self>,\n        _options: UploadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, objects::Error>> + Send>> {\n        Box::pin(future::ready(Err(objects::Error::Internal(\n            anyhow::anyhow!(\"noop bucket does not support getting upload URL\"),\n        ))))\n    }\n\n    fn signed_download_url(\n        self: Arc<Self>,\n        _options: DownloadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, objects::Error>> + Send>> {\n        Box::pin(future::ready(Err(objects::Error::Internal(\n            anyhow::anyhow!(\"noop bucket does not support getting download URL\"),\n        ))))\n    }\n\n    fn exists(\n        self: Arc<Self>,\n        _options: ExistsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<bool, objects::Error>> + Send>> {\n        Box::pin(future::ready(Err(objects::Error::Internal(\n            anyhow::anyhow!(\"noop bucket does not support exists\"),\n        ))))\n    }\n\n    fn upload(\n        self: Arc<Self>,\n        _data: Box<dyn AsyncRead + Unpin + Send + Sync + 'static>,\n        _options: objects::UploadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::ObjectAttrs, objects::Error>> + Send>> {\n        Box::pin(future::ready(Err(objects::Error::Other(anyhow::anyhow!(\n            \"noop bucket does not support upload\"\n        )))))\n    }\n\n    fn download(\n        self: Arc<Self>,\n        _options: DownloadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::DownloadStream, objects::Error>> + Send>> {\n        Box::pin(async move {\n            Err(objects::Error::Internal(anyhow::anyhow!(\n                \"noop bucket does not support download\"\n            )))\n        })\n    }\n\n    fn delete(\n        self: Arc<Self>,\n        _options: DeleteOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<(), objects::Error>> + Send>> {\n        Box::pin(future::ready(Err(objects::Error::Internal(\n            anyhow::anyhow!(\"noop bucket does not support delete\"),\n        ))))\n    }\n\n    fn public_url(&self) -> Result<String, PublicUrlError> {\n        Err(PublicUrlError::NoopBucket)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/s3/bucket.rs",
    "content": "use async_stream::{stream, try_stream};\nuse aws_sdk_s3 as s3;\nuse aws_sdk_s3::error::SdkError;\nuse aws_sdk_s3::presigning::PresigningConfig;\nuse aws_smithy_types::byte_stream::ByteStream;\nuse base64::Engine;\nuse bytes::{Bytes, BytesMut};\nuse std::borrow::Cow;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\nuse tokio::io::{AsyncRead, AsyncReadExt};\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::objects::{\n    self, AttrsOptions, DeleteOptions, DownloadOptions, DownloadUrlOptions, Error, ExistsOptions,\n    ListEntry, ListOptions, ObjectAttrs, PublicUrlError, UploadUrlOptions,\n};\nuse crate::{CloudName, EncoreName};\n\nuse super::LazyS3Client;\n\nconst CHUNK_SIZE: usize = 8_388_608; // 8 Mebibytes, min is 5 (5_242_880);\n\n#[derive(Debug)]\npub struct Bucket {\n    client: Arc<LazyS3Client>,\n    encore_name: EncoreName,\n    cloud_name: CloudName,\n    public_base_url: Option<String>,\n    key_prefix: Option<String>,\n}\n\nimpl Bucket {\n    pub(super) fn new(client: Arc<LazyS3Client>, cfg: &pb::Bucket) -> Self {\n        Self {\n            client,\n            encore_name: cfg.encore_name.clone().into(),\n            cloud_name: cfg.cloud_name.clone().into(),\n            public_base_url: cfg.public_base_url.clone(),\n            key_prefix: cfg.key_prefix.clone(),\n        }\n    }\n\n    /// Computes the object name, including the key prefix if present.\n    fn obj_name<'a>(&'_ self, name: Cow<'a, str>) -> Cow<'a, str> {\n        match &self.key_prefix {\n            Some(prefix) => {\n                let mut key = prefix.to_owned();\n                key.push_str(&name);\n                Cow::Owned(key)\n            }\n            None => name,\n        }\n    }\n\n    /// Returns the name with the key prefix stripped, if present.\n    fn strip_prefix<'a>(&'_ self, name: Cow<'a, str>) -> Cow<'a, str> {\n        match &self.key_prefix {\n            Some(prefix) => name\n                .as_ref()\n                .strip_prefix(prefix)\n                .map(|s| Cow::Owned(s.to_string()))\n                .unwrap_or(name),\n            None => name,\n        }\n    }\n}\n\nimpl objects::BucketImpl for Bucket {\n    fn name(&self) -> &EncoreName {\n        &self.encore_name\n    }\n\n    fn object(self: Arc<Self>, name: String) -> Arc<dyn objects::ObjectImpl> {\n        Arc::new(Object {\n            bkt: self.clone(),\n            name,\n        })\n    }\n\n    fn list(\n        self: Arc<Self>,\n        options: ListOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::ListStream, objects::Error>> + Send + 'static>>\n    {\n        Box::pin(async move {\n            let mut total_seen = 0;\n            let client = self.client.get().await.clone();\n            let s: objects::ListStream = Box::new(try_stream! {\n                let mut req = client.list_objects_v2()\n                    .bucket(&self.cloud_name);\n\n                if let Some(key_prefix) = self.key_prefix.clone() {\n                    req = req.prefix(key_prefix);\n                }\n\n                if let Some(key_prefix) = &options.prefix {\n                    let current_prefix = req.get_prefix().as_deref().unwrap_or_default();\n                    let new_prefix = format!(\"{current_prefix}{key_prefix}\");\n                    req = req.prefix(new_prefix);\n                }\n\n                let page_size = if let Some(limit) = options.limit {\n                    limit.min(1000) as i32\n                } else {\n                    1000\n                };\n\n                let mut stream = req.into_paginator()\n                    .page_size(page_size)\n                    .send();\n\n                'PageLoop:\n                while let Some(resp) = stream.try_next().await.map_err(|e| Error::Other(e.into()))? {\n                    for obj in resp.contents.unwrap_or_default() {\n                        total_seen += 1;\n                        if let Some(limit) = options.limit {\n                            if total_seen > limit {\n                                // We've reached the limit, stop the stream.\n                                break 'PageLoop;\n                            }\n                        }\n\n                        let entry = ListEntry {\n                            name: self.strip_prefix(Cow::Owned(obj.key.unwrap_or_default())).into_owned(),\n                            size: obj.size.unwrap_or_default() as u64,\n                            etag: parse_etag(obj.e_tag),\n                        };\n                        yield entry;\n                    }\n                }\n            });\n\n            Ok(s)\n        })\n    }\n}\n\n#[derive(Debug)]\nstruct Object {\n    bkt: Arc<Bucket>,\n    name: String,\n}\n\nimpl objects::ObjectImpl for Object {\n    fn bucket_name(&self) -> &EncoreName {\n        &self.bkt.encore_name\n    }\n\n    fn key(&self) -> &str {\n        &self.name\n    }\n\n    fn attrs(\n        self: Arc<Self>,\n        options: AttrsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ObjectAttrs, Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let cloud_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n            let res = client\n                .head_object()\n                .bucket(&self.bkt.cloud_name)\n                .key(cloud_name)\n                .set_version_id(options.version)\n                .send()\n                .await;\n\n            match res {\n                Ok(obj) => Ok(ObjectAttrs {\n                    name: self.name.clone(),\n                    version: obj.version_id,\n                    size: obj.content_length.unwrap_or_default() as u64,\n                    content_type: obj.content_type,\n                    etag: parse_etag(obj.e_tag),\n                }),\n                Err(SdkError::ServiceError(err)) if err.err().is_not_found() => {\n                    Err(Error::NotFound)\n                }\n                Err(err) => Err(Error::Other(err.into())),\n            }\n        })\n    }\n\n    fn signed_upload_url(\n        self: Arc<Self>,\n        options: UploadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let obj_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n\n            let res = client\n                .put_object()\n                .bucket(&self.bkt.cloud_name)\n                .key(obj_name)\n                .presigned(\n                    PresigningConfig::expires_in(options.ttl)\n                        .map_err(|e| Error::Other(e.into()))?,\n                )\n                .await;\n            match res {\n                Ok(req) => Ok(String::from(req.uri())),\n                Err(err) => Err(Error::Other(err.into())),\n            }\n        })\n    }\n\n    fn signed_download_url(\n        self: Arc<Self>,\n        options: DownloadUrlOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<String, Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let obj_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n\n            let res = client\n                .get_object()\n                .bucket(&self.bkt.cloud_name)\n                .key(obj_name)\n                .presigned(\n                    PresigningConfig::expires_in(options.ttl)\n                        .map_err(|e| Error::Other(e.into()))?,\n                )\n                .await;\n            match res {\n                Ok(req) => Ok(String::from(req.uri())),\n                Err(err) => Err(Error::Other(err.into())),\n            }\n        })\n    }\n\n    fn exists(\n        self: Arc<Self>,\n        options: ExistsOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let cloud_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n            let res = client\n                .head_object()\n                .bucket(&self.bkt.cloud_name)\n                .key(cloud_name)\n                .set_version_id(options.version)\n                .send()\n                .await;\n            match res {\n                Ok(_) => Ok(true),\n                Err(SdkError::ServiceError(err)) if err.err().is_not_found() => Ok(false),\n                Err(err) => Err(Error::Other(err.into())),\n            }\n        })\n    }\n\n    fn upload(\n        self: Arc<Self>,\n        mut data: Box<dyn AsyncRead + Unpin + Send + Sync + 'static>,\n        options: objects::UploadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<ObjectAttrs, Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let cloud_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n            let first_chunk = read_chunk_async(&mut data).await.map_err(|e| {\n                Error::Other(anyhow::anyhow!(\"uable to read from data source: {}\", e))\n            })?;\n\n            match first_chunk {\n                Chunk::Complete(chunk) => {\n                    // The file is small; do a regular upload.\n                    let chunk = chunk.freeze();\n                    let total_size = chunk.len();\n                    let content_md5 = base64::engine::general_purpose::STANDARD\n                        .encode(md5::compute(&chunk).as_ref());\n\n                    let mut req = client\n                        .put_object()\n                        .bucket(&self.bkt.cloud_name)\n                        .key(cloud_name)\n                        .content_length(total_size as i64)\n                        .content_md5(content_md5)\n                        .set_content_type(options.content_type.clone())\n                        .body(ByteStream::from(chunk));\n\n                    if let Some(precond) = options.preconditions {\n                        if precond.not_exists == Some(true) {\n                            req = req.if_none_match(\"*\");\n                        }\n                    }\n\n                    let resp = req.send().await.map_err(map_upload_err)?;\n                    Ok(ObjectAttrs {\n                        name: self.name.clone(),\n                        version: resp.version_id,\n                        size: total_size as u64,\n                        content_type: options.content_type,\n                        etag: resp.e_tag.unwrap_or_default(),\n                    })\n                }\n\n                Chunk::Part(chunk) => {\n                    // Large file; do a multipart upload.\n                    let upload = client\n                        .create_multipart_upload()\n                        .bucket(&self.bkt.cloud_name)\n                        .key(cloud_name.to_string())\n                        .set_content_type(options.content_type.clone())\n                        .send()\n                        .await\n                        .map_err(|err| {\n                            Error::Other(anyhow::anyhow!(\n                                \"unable to begin streaming upload: {}\",\n                                err\n                            ))\n                        })?;\n\n                    let Some(upload_id) = upload.upload_id else {\n                        return Err(Error::Other(anyhow::anyhow!(\n                            \"missing upload_id in streaming_upload\"\n                        )));\n                    };\n\n                    let res = upload_multipart_chunks(\n                        &client,\n                        &mut data,\n                        chunk.freeze(),\n                        &upload_id,\n                        &self.bkt.cloud_name,\n                        &cloud_name,\n                        &options,\n                    )\n                    .await;\n\n                    if let UploadMultipartResult::CompleteSuccess { total_size, output } = res {\n                        return Ok(ObjectAttrs {\n                            name: self.name.clone(),\n                            version: output.version_id,\n                            size: total_size,\n                            content_type: options.content_type,\n                            etag: parse_etag(output.e_tag),\n                        });\n                    }\n\n                    // Abort the upload.\n                    let fut = client\n                        .abort_multipart_upload()\n                        .bucket(&self.bkt.cloud_name)\n                        .key(cloud_name)\n                        .upload_id(upload_id)\n                        .send();\n                    tokio::spawn(async move {\n                        let _ = fut.await;\n                    });\n\n                    Err(match res {\n                        UploadMultipartResult::CompleteSuccess { .. } => unreachable!(),\n                        UploadMultipartResult::UploadError(err) => Error::Other(err.into()),\n                        UploadMultipartResult::CompleteError(err) => map_upload_err(err),\n                        UploadMultipartResult::ReadContents(err) => Error::Other(anyhow::anyhow!(\n                            \"unable to read from data source: {}\",\n                            err\n                        )),\n                    })\n                }\n            }\n        })\n    }\n\n    fn download(\n        self: Arc<Self>,\n        options: DownloadOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<objects::DownloadStream, objects::Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let cloud_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n            let res = client\n                .get_object()\n                .bucket(&self.bkt.cloud_name)\n                .key(cloud_name.into_owned())\n                .set_version_id(options.version)\n                .send()\n                .await;\n\n            match res {\n                Ok(mut resp) => {\n                    let result = stream! {\n                        while let Some(chunk) = resp.body.next().await {\n                            yield chunk.map_err(|e| objects::Error::Other(e.into()));\n                        }\n                    };\n                    let result: objects::DownloadStream = Box::pin(result);\n                    Ok(result)\n                }\n                Err(SdkError::ServiceError(err)) if err.err().is_no_such_key() => {\n                    Err(objects::Error::NotFound)\n                }\n                Err(err) => Err(objects::Error::Other(err.into())),\n            }\n        })\n    }\n\n    fn delete(\n        self: Arc<Self>,\n        options: DeleteOptions,\n    ) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> {\n        Box::pin(async move {\n            let client = self.bkt.client.get().await.clone();\n            let cloud_name = self.bkt.obj_name(Cow::Borrowed(&self.name));\n            let res = client\n                .delete_object()\n                .bucket(&self.bkt.cloud_name)\n                .key(cloud_name.into_owned())\n                .set_version_id(options.version)\n                .send()\n                .await;\n            match res {\n                Ok(_) => Ok(()),\n                Err(SdkError::ServiceError(err)) if err.raw().status().as_u16() == 404 => {\n                    Err(Error::NotFound)\n                }\n                Err(err) => Err(Error::Other(err.into())),\n            }\n        })\n    }\n\n    fn public_url(&self) -> Result<String, PublicUrlError> {\n        let Some(base_url) = self.bkt.public_base_url.clone() else {\n            return Err(PublicUrlError::PrivateBucket);\n        };\n\n        let url = objects::public_url(base_url, &self.name);\n        Ok(url)\n    }\n}\n\nenum Chunk {\n    Part(BytesMut),\n    Complete(BytesMut),\n}\n\nimpl Chunk {\n    fn into_bytes(self) -> BytesMut {\n        match self {\n            Chunk::Part(buf) => buf,\n            Chunk::Complete(buf) => buf,\n        }\n    }\n}\n\nasync fn read_chunk_async<R: AsyncRead + Unpin + ?Sized>(reader: &mut R) -> std::io::Result<Chunk> {\n    // Use an initial capacity of 10KiB.\n    let mut buf = BytesMut::with_capacity(10 * 1024);\n    while buf.len() < CHUNK_SIZE {\n        // If the buf has no available capacity, we need to allocate more.\n        if buf.len() == buf.capacity() {\n            buf.reserve(buf.capacity());\n        }\n\n        let n = reader.read_buf(&mut buf).await?;\n        if n == 0 {\n            // We've reached the end of the stream.\n            // This is guaranteed to be the case since in the case\n            // where the buffer was full we reserved\n            // additional capacity in the buffer before reading.\n            return Ok(Chunk::Complete(buf));\n        }\n    }\n\n    Ok(Chunk::Part(buf))\n}\n\n#[allow(clippy::large_enum_variant)]\nenum UploadMultipartResult {\n    CompleteSuccess {\n        total_size: u64,\n        output: s3::operation::complete_multipart_upload::CompleteMultipartUploadOutput,\n    },\n    CompleteError(\n        s3::error::SdkError<s3::operation::complete_multipart_upload::CompleteMultipartUploadError>,\n    ),\n    UploadError(s3::error::SdkError<s3::operation::upload_part::UploadPartError>),\n    ReadContents(std::io::Error),\n}\n\nasync fn upload_multipart_chunks<R: AsyncRead + Unpin + ?Sized>(\n    client: &s3::Client,\n    reader: &mut R,\n    first_chunk: Bytes,\n    upload_id: &str,\n    bucket: &CloudName,\n    key: &str,\n    options: &objects::UploadOptions,\n) -> UploadMultipartResult {\n    let mut handles = Vec::new();\n    let mut part_numbers = Vec::new();\n    let mut part_number = 0;\n    let mut total_size = 0;\n    let mut upload_part = |chunk: Bytes| {\n        part_number += 1;\n        let content_md5 =\n            base64::engine::general_purpose::STANDARD.encode(md5::compute(&chunk).as_ref());\n        total_size += chunk.len() as u64;\n        let handle = client\n            .upload_part()\n            .bucket(bucket)\n            .key(key)\n            .upload_id(upload_id)\n            .part_number(part_number)\n            .content_length(chunk.len() as i64)\n            .content_md5(content_md5)\n            .body(ByteStream::from(chunk))\n            .send();\n        handles.push(handle);\n        part_numbers.push(part_number);\n    };\n\n    upload_part(first_chunk);\n    loop {\n        let bytes = match read_chunk_async(reader).await {\n            Ok(chunk) => chunk.into_bytes(),\n            Err(err) => return UploadMultipartResult::ReadContents(err),\n        };\n        if bytes.is_empty() {\n            break;\n        }\n        upload_part(bytes.freeze());\n    }\n\n    // Wait for all the parts to finish uploading.\n    let responses = futures::future::join_all(handles).await;\n\n    // Check for errors and collect the completed parts.\n    let mut completed_parts = Vec::new();\n    for (i, res) in responses.into_iter().enumerate() {\n        match res {\n            Ok(output) => {\n                let part = s3::types::CompletedPart::builder()\n                    .part_number(part_numbers[i])\n                    .set_e_tag(output.e_tag)\n                    .build();\n                completed_parts.push(part);\n            }\n            Err(err) => return UploadMultipartResult::UploadError(err),\n        }\n    }\n\n    let multipart_upload = s3::types::CompletedMultipartUpload::builder()\n        .set_parts(Some(completed_parts))\n        .build();\n\n    let mut req = client\n        .complete_multipart_upload()\n        .bucket(bucket)\n        .key(key)\n        .upload_id(upload_id)\n        .multipart_upload(multipart_upload);\n\n    if let Some(precond) = &options.preconditions {\n        if precond.not_exists == Some(true) {\n            req = req.if_none_match(\"*\");\n        }\n    }\n\n    let resp = req.send().await;\n    match resp {\n        Ok(output) => UploadMultipartResult::CompleteSuccess { total_size, output },\n        Err(err) => UploadMultipartResult::CompleteError(err),\n    }\n}\n\nfn parse_etag(s: Option<String>) -> String {\n    match s {\n        Some(s) => {\n            if s.starts_with('\"') && s.ends_with('\"') {\n                s[1..s.len() - 1].to_string()\n            } else {\n                s\n            }\n        }\n        None => \"\".to_string(),\n    }\n}\n\nfn map_upload_err<E>(err: s3::error::SdkError<E>) -> objects::Error\nwhere\n    E: std::fmt::Debug,\n{\n    if err\n        .raw_response()\n        .is_some_and(|r| r.status().as_u16() == 412)\n    {\n        Error::PreconditionFailed\n    } else {\n        Error::Other(anyhow::anyhow!(\"failed to upload: {:?}\", err))\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/objects/s3/mod.rs",
    "content": "use std::sync::Arc;\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::objects;\nuse crate::objects::s3::bucket::Bucket;\nuse crate::secrets::Secret;\nuse aws_sdk_s3 as s3;\n\nmod bucket;\n\n#[derive(Debug)]\npub struct Cluster {\n    client: Arc<LazyS3Client>,\n}\n\nimpl Cluster {\n    pub fn new(cfg: pb::bucket_cluster::S3, secret_access_key: Option<Secret>) -> Self {\n        let client = Arc::new(LazyS3Client::new(cfg, secret_access_key));\n        Self { client }\n    }\n}\n\nimpl objects::ClusterImpl for Cluster {\n    fn bucket(self: Arc<Self>, cfg: &pb::Bucket) -> Arc<dyn objects::BucketImpl + 'static> {\n        Arc::new(Bucket::new(self.client.clone(), cfg))\n    }\n}\n\nstruct LazyS3Client {\n    cfg: pb::bucket_cluster::S3,\n    secret_access_key: Option<Secret>,\n    cell: tokio::sync::OnceCell<Arc<s3::Client>>,\n}\n\nimpl std::fmt::Debug for LazyS3Client {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"LazyS3Client\").finish()\n    }\n}\n\nimpl LazyS3Client {\n    fn new(cfg: pb::bucket_cluster::S3, secret_access_key: Option<Secret>) -> Self {\n        Self {\n            cfg,\n            secret_access_key,\n            cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn get(&self) -> &Arc<s3::Client> {\n        self.cell\n            .get_or_init(|| async {\n                let region = aws_config::Region::new(self.cfg.region.clone());\n                let mut builder =\n                    aws_config::defaults(aws_config::BehaviorVersion::v2025_08_07()).region(region);\n                if let Some(endpoint) = self.cfg.endpoint.as_ref() {\n                    builder = builder.endpoint_url(endpoint.clone());\n                }\n\n                if let (Some(access_key_id), Some(secret_access_key)) = (\n                    self.cfg.access_key_id.as_ref(),\n                    self.secret_access_key.as_ref(),\n                ) {\n                    use aws_credential_types::Credentials;\n                    let secret_access_key = secret_access_key\n                        .get()\n                        .expect(\"unable to resolve s3 secret access key\");\n                    let secret_access_key = std::str::from_utf8(secret_access_key)\n                        .expect(\"unable to parse s3 secret access key as utf-8\");\n\n                    builder = builder.credentials_provider(Credentials::new(\n                        access_key_id,\n                        secret_access_key,\n                        None,\n                        None,\n                        \"encore-runtime\",\n                    ));\n                }\n\n                let cfg = builder.load().await;\n                Arc::new(s3::Client::new(&cfg))\n            })\n            .await\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/proccfg.rs",
    "content": "use std::collections::HashMap;\n\nuse anyhow::{Context, Result};\nuse serde::{Deserialize, Serialize};\n\nuse crate::encore::runtime::v1 as runtimepb;\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct ProcessConfig {\n    hosted_services: Vec<String>,\n    hosted_gateways: Vec<String>,\n    local_service_ports: HashMap<String, u16>,\n}\n\nimpl ProcessConfig {\n    pub fn apply(&self, cfg: &mut runtimepb::RuntimeConfig) -> Result<()> {\n        let deployment = cfg.deployment.get_or_insert_with(Default::default);\n\n        // Construct a map of existing hosted services, keyed by name.\n        let hosted_services = deployment\n            .hosted_services\n            .iter()\n            .map(|s| (s.name.clone(), s.clone()))\n            .collect::<HashMap<_, _>>();\n\n        deployment.hosted_services = self\n            .hosted_services\n            .iter()\n            .map(|s| {\n                hosted_services\n                    .get(s)\n                    .cloned()\n                    .unwrap_or_else(|| runtimepb::HostedService {\n                        name: s.clone(),\n                        log_config: None,\n                        worker_threads: None,\n                    })\n            })\n            .collect();\n        deployment.hosted_gateways = self\n            .hosted_gateways\n            .iter()\n            .map(|s| {\n                cfg.infra\n                    .as_ref()\n                    .context(\"gateway not found in infra resources\")\n                    .and_then(|r| r.resources.as_ref().context(\"resources not found in infra\"))\n                    .and_then(|r| {\n                        r.gateways\n                            .iter()\n                            .find(|g| g.encore_name == *s)\n                            .context(\"gateway not found in infra resources\")\n                            .map(|r| r.rid.clone())\n                    })\n            })\n            .collect::<Result<Vec<_>>>()?;\n\n        let svc_discovery = deployment\n            .service_discovery\n            .get_or_insert_with(Default::default);\n        // Iterate through service_ports and add service_discovery entries\n        for (service_name, port) in &self.local_service_ports {\n            let base_url = format!(\"http://127.0.0.1:{port}\");\n            svc_discovery.services.insert(\n                service_name.clone(),\n                runtimepb::service_discovery::Location {\n                    base_url: base_url.clone(),\n                    auth_methods: deployment.auth_methods.clone(),\n                },\n            );\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/gcp/jwk.rs",
    "content": "use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};\n\nuse anyhow::Context;\nuse jsonwebtoken::jwk::{Jwk, JwkSet, KeyAlgorithm};\nuse serde::Deserialize;\n\n/// Implements a client that fetches and caches JWK sets from a URL.\n#[derive(Debug)]\npub struct CachingClient {\n    /// The cached JWK set, keyed by the URL.\n    cached: tokio::sync::RwLock<HashMap<&'static str, CachedJwkSet>>,\n}\n\nimpl CachingClient {\n    pub fn new() -> Self {\n        Self {\n            cached: tokio::sync::RwLock::default(),\n        }\n    }\n\n    /// Returns the cached JWK set, fetching it if necessary.\n    pub async fn get(&self, url: &'static str) -> anyhow::Result<Arc<JwkSet>> {\n        // Do we already have a cached JWK set that's still valid?\n        if let Some(cached) = self.get_if_cached(url).await {\n            return Ok(cached);\n        }\n\n        // Fetch the JWK set from the URL.\n        let response = fetch(url).await?;\n        let set = response.set.clone();\n\n        // Update the cache.\n        {\n            let mut write_guard = self.cached.write().await;\n            write_guard.insert(url, response);\n        }\n\n        Ok(set)\n    }\n\n    /// Reports the cached JWK set if it's still valid.\n    async fn get_if_cached(&self, url: &str) -> Option<Arc<JwkSet>> {\n        let read_guard = self.cached.read().await;\n        if let Some(cached) = read_guard.get(url) {\n            if cached.is_valid() {\n                return Some(cached.set.clone());\n            }\n        }\n        None\n    }\n}\n\n#[derive(Debug)]\nstruct CachedJwkSet {\n    /// The JWK set.\n    set: Arc<JwkSet>,\n\n    /// The time the JWK set cache expires.\n    /// None if the response should not be cached.\n    exp: Option<std::time::Instant>,\n}\n\nimpl CachedJwkSet {\n    pub fn is_valid(&self) -> bool {\n        self.exp\n            .map(|exp| exp > std::time::Instant::now())\n            .unwrap_or(false)\n    }\n}\n\n/// Fetches a JWK set from a URL and returns it.\nasync fn fetch(url: &str) -> anyhow::Result<CachedJwkSet> {\n    // Fetch the JWK set from the URL.\n    let response = reqwest::get(url).await?;\n\n    // If the status is not 200, return an error.\n    if !response.status().is_success() {\n        return Err(anyhow::anyhow!(\n            \"failed to fetch JWK set: {}\",\n            response.status()\n        ));\n    }\n\n    // Determine the expiration time from the cache headers.\n    let exp = response_cache_exp_time(response.headers());\n    let exp = exp.map(|dur| std::time::Instant::now() + dur);\n\n    #[derive(Deserialize)]\n    struct RawKeyList {\n        keys: Vec<serde_json::Value>,\n    }\n    let key_list: RawKeyList = response.json().await.context(\"unable to parse key list\")?;\n    let keys = key_list\n        .keys\n        .into_iter()\n        .filter_map(parse_key)\n        .collect::<Vec<Jwk>>();\n\n    Ok(CachedJwkSet {\n        set: Arc::new(JwkSet { keys }),\n        exp,\n    })\n}\n\nfn parse_key(val: serde_json::Value) -> Option<Jwk> {\n    if let serde_json::Value::Object(map) = &val {\n        if let Some(serde_json::Value::String(alg)) = map.get(\"alg\") {\n            if KeyAlgorithm::from_str(alg).is_err() {\n                return None;\n            }\n            // We have an algorithm that can be parsed, so parse the whole JWT.\n            return serde_json::from_value(val).ok();\n        }\n    }\n\n    None\n}\n\nfn response_cache_exp_time(resp: &reqwest::header::HeaderMap) -> Option<Duration> {\n    let cache_control = resp.get(\"cache-control\").and_then(|v| v.to_str().ok());\n    let age = resp.get(\"age\").and_then(|v| v.to_str().ok());\n\n    cache_exp_time(cache_control, age)\n}\n\nfn cache_exp_time(\n    cache_control_header: Option<&str>,\n    age_header: Option<&str>,\n) -> Option<Duration> {\n    let mut max_age = None;\n    if let Some(cache_control) = cache_control_header {\n        let parts = cache_control.split(',');\n        for part in parts {\n            let directive = part.trim();\n            if directive.starts_with(\"max-age=\") {\n                if let Some(eq_idx) = directive.find('=') {\n                    let age_value = directive[eq_idx + 1..].trim();\n                    if let Ok(seconds) = age_value.parse::<u64>() {\n                        max_age = Some(Duration::from_secs(seconds));\n                    }\n                }\n            }\n        }\n    }\n\n    let mut age = Duration::from_secs(0);\n    if let Some(age_header) = age_header {\n        if let Ok(age_secs) = age_header.parse::<u64>() {\n            age = Duration::from_secs(age_secs);\n        }\n    }\n\n    max_age.and_then(|ma| if ma >= age { Some(ma - age) } else { None })\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_cache_exp_time() {\n        assert_eq!(cache_exp_time(None, None), None);\n        assert_eq!(\n            cache_exp_time(Some(\"max-age=60\"), None),\n            Some(Duration::from_secs(60))\n        );\n        assert_eq!(\n            cache_exp_time(Some(\"max-age=60\"), Some(\"30\")),\n            Some(Duration::from_secs(30))\n        );\n        assert_eq!(\n            cache_exp_time(Some(\"max-age=60, stale-while-revalidate=30\"), Some(\"30\")),\n            Some(Duration::from_secs(30))\n        );\n\n        // Test when max-age is below age.\n        assert_eq!(cache_exp_time(Some(\"max-age=30\"), Some(\"60\")), None);\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/gcp/mod.rs",
    "content": "use std::sync::Arc;\n\nuse anyhow::Context;\nuse google_cloud_pubsub as gcp;\n\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub;\nuse crate::pubsub::gcp::sub::Subscription;\nuse crate::pubsub::gcp::topic::Topic;\n\nmod jwk;\nmod push_sub;\nmod sub;\nmod topic;\n#[derive(Debug)]\npub struct Cluster {\n    client: Arc<LazyGCPClient>,\n}\n\nimpl Cluster {\n    pub fn new() -> Self {\n        let client = Arc::new(LazyGCPClient::new());\n        Self { client }\n    }\n}\n\nimpl pubsub::Cluster for Cluster {\n    fn topic(\n        &self,\n        cfg: &pb::PubSubTopic,\n        _publisher_id: xid::Id,\n    ) -> Arc<dyn pubsub::Topic + 'static> {\n        Arc::new(Topic::new(self.client.clone(), cfg))\n    }\n\n    fn subscription(\n        &self,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Arc<dyn pubsub::Subscription + 'static> {\n        // If this is a push-based subscription, return that implementation.\n        if let Some(pb::pub_sub_subscription::ProviderConfig::GcpConfig(gcp_cfg)) =\n            cfg.provider_config.as_ref()\n        {\n            if gcp_cfg.push_service_account.is_some() {\n                return Arc::new(push_sub::PushSubscription::new(cfg));\n            }\n        }\n\n        Arc::new(Subscription::new(self.client.clone(), cfg, meta))\n    }\n}\n\n#[derive(Debug)]\nstruct LazyGCPClient {\n    cell: tokio::sync::OnceCell<anyhow::Result<gcp::client::Client>>,\n}\n\nimpl LazyGCPClient {\n    fn new() -> Self {\n        Self {\n            cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn get(&self) -> &anyhow::Result<gcp::client::Client> {\n        self.cell\n            .get_or_init(|| async {\n                let config = gcp::client::ClientConfig::default()\n                    .with_auth()\n                    .await\n                    .inspect_err(|e| log::error!(\"failed to get client config: {e:?}\"))\n                    .context(\"get client config\")?;\n                gcp::client::Client::new(config)\n                    .await\n                    .inspect_err(|e| log::error!(\"failed to get client: {e:?}\"))\n                    .context(\"get client\")\n            })\n            .await\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/gcp/push_sub.rs",
    "content": "use std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::{Arc, RwLock};\n\nuse anyhow::Context;\nuse axum::extract::Request;\nuse axum::RequestExt;\nuse chrono::{DateTime, Utc};\nuse http_body_util::BodyExt;\nuse serde::Deserialize;\n\nuse crate::api::{self, APIResult, ToResponse};\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub::manager::SubHandler;\nuse crate::pubsub::{self, MessageId};\n\nuse super::jwk::{self, CachingClient};\n\n#[derive(Debug, Clone)]\npub struct PushSubscription {\n    inner: Arc<Inner>,\n}\n\n#[derive(Debug)]\nstruct Inner {\n    subscription_id: String,\n    handler: RwLock<Option<Arc<SubHandler>>>,\n    validator: GoogleJWTValidator,\n}\n\nimpl PushSubscription {\n    pub(super) fn new(cfg: &pb::PubSubSubscription) -> Self {\n        let Some(pb::pub_sub_subscription::ProviderConfig::GcpConfig(gcp_cfg)) =\n            cfg.provider_config.as_ref()\n        else {\n            panic!(\"missing gcp config for subscription\")\n        };\n\n        let Some(service_account) = &gcp_cfg.push_service_account else {\n            panic!(\"missing push_service_account for subscription\")\n        };\n\n        let google_validator = GoogleJWTValidator {\n            client: CachingClient::new(),\n            push_service_account: service_account.clone(),\n            audience: gcp_cfg.push_jwt_audience.clone(),\n        };\n\n        Self {\n            inner: Arc::new(Inner {\n                subscription_id: cfg.rid.clone(),\n                handler: RwLock::new(None),\n                validator: google_validator,\n            }),\n        }\n    }\n}\n\nimpl pubsub::Subscription for PushSubscription {\n    fn subscribe(\n        &self,\n        handler: Arc<SubHandler>,\n    ) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>> {\n        self.inner.handler.write().unwrap().replace(handler);\n\n        // Block forever; the handler is called from the HTTP handler.\n        Box::pin(futures::future::pending())\n    }\n\n    fn push_handler(&self) -> Option<(String, Arc<dyn pubsub::PushRequestHandler>)> {\n        Some((\n            self.inner.subscription_id.clone(),\n            Arc::new(PushHandler {\n                inner: self.inner.clone(),\n            }),\n        ))\n    }\n}\n\n#[derive(Debug, Clone)]\nstruct PushHandler {\n    inner: Arc<Inner>,\n}\n\nimpl pubsub::PushRequestHandler for PushHandler {\n    fn handle_push(\n        &self,\n        req: Request,\n    ) -> Pin<Box<dyn Future<Output = axum::response::Response<axum::body::Body>> + Send + 'static>>\n    {\n        let inner = self.inner.clone();\n        Box::pin(async move {\n            match inner.handle_req(req).await {\n                Ok(()) => axum::response::Response::new(axum::body::Body::empty()),\n                Err(e) => {\n                    log::error!(\"push handler returned error: {:?}\", e);\n                    e.to_response(None)\n                }\n            }\n        })\n    }\n}\n\n/// Payload of a push message from GCP Pub/Sub.\n/// This is documented in https://cloud.google.com/pubsub/docs/push\n#[derive(Debug, Deserialize)]\nstruct PushPayload {\n    message: PushMessage,\n    #[allow(dead_code)]\n    subscription: String,\n    #[serde(rename = \"deliveryAttempt\")]\n    delivery_attempt: Option<u32>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct PushMessage {\n    #[serde(default)]\n    attributes: HashMap<String, String>,\n    #[serde(with = \"base64\")]\n    data: Vec<u8>,\n    #[serde(rename = \"messageId\")]\n    message_id: String,\n    #[serde(rename = \"publishTime\")]\n    publish_time: DateTime<Utc>,\n}\n\nimpl Inner {\n    async fn handle_req(&self, req: Request) -> APIResult<()> {\n        // Do we have a handler registered yet? If not, there's no point in proceeding.\n        let handler = {\n            let read_guard = self.handler.read().unwrap();\n            let Some(handler) = (*read_guard).clone() else {\n                return Err(api::Error {\n                    code: api::ErrCode::Internal,\n                    message: \"no handler registered for subscription\".to_string(),\n                    internal_message: None,\n                    stack: None,\n                    details: None,\n                });\n            };\n            handler\n        };\n\n        // Validate the JWT token.\n        self.validator\n            .validate_google_jwt(req.headers())\n            .await\n            .map_err(api::Error::internal)?;\n\n        // Parse the request payload.\n        let bytes = req\n            .into_limited_body()\n            .collect()\n            .await\n            .map_err(api::Error::internal)?\n            .to_bytes();\n        let msg: PushPayload = serde_json::from_slice(&bytes).map_err(api::Error::internal)?;\n\n        let msg = pubsub::Message {\n            id: msg.message.message_id as MessageId,\n            publish_time: Some(msg.message.publish_time),\n            attempt: msg.delivery_attempt.unwrap_or(1),\n            data: pubsub::MessageData {\n                attrs: msg.message.attributes,\n                raw_body: msg.message.data,\n            },\n        };\n\n        match handler.handle_message(msg).await {\n            Ok(()) => Ok(()),\n            Err(err) => {\n                log::info!(\"message handler failed, nacking message: {:?}\", err);\n                Err(err)\n            }\n        }\n    }\n}\n\n#[derive(Debug)]\nstruct GoogleJWTValidator {\n    client: jwk::CachingClient,\n    audience: Option<String>,\n    push_service_account: String,\n}\n\n/// The certs URL for RSA keys.\nconst GOOGLE_SA_CERTS_URL: &str = \"https://www.googleapis.com/oauth2/v3/certs\";\n\n/// The certs URL for other keys.\nconst GOOGLE_IAP_CERTS_URL: &str = \"https://www.gstatic.com/iap/verify/public_key-jwk\";\n\nimpl GoogleJWTValidator {\n    pub async fn validate_google_jwt(&self, req: &axum::http::HeaderMap) -> anyhow::Result<()> {\n        // Extract the JWT from the header\n        let auth_header = req\n            .get(\"Authorization\")\n            .ok_or_else(|| anyhow::anyhow!(\"missing auth header\"))?;\n        let token = auth_header\n            .to_str()\n            .map_err(|_| anyhow::anyhow!(\"invalid auth header\"))?;\n        let token = token\n            .strip_prefix(\"Bearer \")\n            .ok_or_else(|| anyhow::anyhow!(\"invalid auth header\"))?;\n\n        let token_header = jsonwebtoken::decode_header(token)?;\n        let Some(token_key_id) = token_header.kid.as_ref() else {\n            return Err(anyhow::anyhow!(\"missing kid in token header\"));\n        };\n\n        let url = match token_header.alg {\n            jsonwebtoken::Algorithm::RS256\n            | jsonwebtoken::Algorithm::RS384\n            | jsonwebtoken::Algorithm::RS512 => GOOGLE_SA_CERTS_URL,\n            _ => GOOGLE_IAP_CERTS_URL,\n        };\n\n        // Get the JWK set to validate the token against.\n        let jwks = self\n            .client\n            .get(url)\n            .await\n            .context(\"unable to fetch JWK keys\")?;\n\n        // Find the key that matches the token.\n        let jwk_key = jwks.find(token_key_id).ok_or_else(|| {\n            anyhow::anyhow!(\"unable to find JWK key for token: {:?}\", token_key_id)\n        })?;\n\n        // Decode all the claims.\n        #[derive(Deserialize)]\n        struct Claims {\n            // Custom claims from GCP\n            email: String,\n            email_verified: bool,\n        }\n\n        let decoding_key = jsonwebtoken::DecodingKey::from_jwk(jwk_key)\n            .context(\"unable to create JWT decoding key\")?;\n\n        // Per the Go GCP library, the only supported algorithms are RS256 and ES256.\n        let alg = match token_header.alg {\n            jsonwebtoken::Algorithm::RS256 | jsonwebtoken::Algorithm::ES256 => token_header.alg,\n            _ => {\n                return Err(anyhow::anyhow!(\n                    \"unexpected algorithm: {:?}\",\n                    token_header.alg\n                ));\n            }\n        };\n\n        let mut validation = jsonwebtoken::Validation::new(alg);\n        if let Some(aud) = &self.audience {\n            validation.set_audience(&[aud]);\n        }\n        validation.set_issuer(&[\"accounts.google.com\", \"https://accounts.google.com\"]);\n        validation.set_required_spec_claims(&[\"exp\", \"iss\", \"aud\"]);\n\n        let jwt = jsonwebtoken::decode::<Claims>(token, &decoding_key, &validation)\n            .context(\"unable to decode JWT claims\")?;\n        if jwt.claims.email != self.push_service_account {\n            return Err(anyhow::anyhow!(\"invalid email\"));\n        }\n        if !jwt.claims.email_verified {\n            return Err(anyhow::anyhow!(\"email not verified\"));\n        }\n\n        Ok(())\n    }\n}\n\nmod base64 {\n    use base64::engine::{general_purpose::STANDARD, Engine};\n    use serde::{Deserialize, Serialize};\n    use serde::{Deserializer, Serializer};\n\n    #[allow(dead_code)]\n    pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {\n        let base64 = STANDARD.encode(v);\n        String::serialize(&base64, s)\n    }\n\n    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {\n        let base64 = String::deserialize(d)?;\n        STANDARD\n            .decode(base64.as_bytes())\n            .map_err(serde::de::Error::custom)\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/gcp/sub.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse anyhow::Result;\nuse google_cloud_pubsub as gcp;\nuse google_cloud_pubsub::apiv1::default_retry_setting;\nuse tokio_util::sync::CancellationToken;\n\nuse crate::api::{self, APIResult};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub::gcp::LazyGCPClient;\nuse crate::pubsub::manager::SubHandler;\nuse crate::pubsub::{self, MessageId};\n\n#[derive(Debug)]\npub struct Subscription {\n    inner: Arc<InnerSubscription>,\n}\n\nimpl Subscription {\n    pub(super) fn new(\n        client: Arc<LazyGCPClient>,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Self {\n        let inner = InnerSubscription::new(client, cfg, meta);\n        Self {\n            inner: Arc::new(inner),\n        }\n    }\n}\n\nimpl pubsub::Subscription for Subscription {\n    fn subscribe(\n        &self,\n        handler: Arc<SubHandler>,\n    ) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>> {\n        let inner = self.inner.clone();\n        Box::pin(async move {\n            let sub = inner.get_sub().await.map_err(api::Error::internal)?;\n            let cancel = CancellationToken::new();\n            sub.receive(\n                move |message, cancel| {\n                    let handler = handler.clone();\n                    handle_message(handler, message, cancel)\n                },\n                cancel,\n                Some(inner.receive_cfg.clone()),\n            )\n            .await\n            .map_err(api::Error::internal)?;\n            Ok(())\n        })\n    }\n}\n\n#[derive(Debug)]\nstruct InnerSubscription {\n    client: Arc<LazyGCPClient>,\n    project_id: String,\n    sub_name: String,\n    receive_cfg: gcp::subscription::ReceiveConfig,\n    cell: tokio::sync::OnceCell<Result<gcp::subscription::Subscription>>,\n}\n\nimpl InnerSubscription {\n    pub(super) fn new(\n        client: Arc<LazyGCPClient>,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Self {\n        let Some(pb::pub_sub_subscription::ProviderConfig::GcpConfig(gcp_cfg)) =\n            cfg.provider_config.as_ref()\n        else {\n            panic!(\"missing gcp config for subscription\")\n        };\n\n        let receive_cfg = gcp::subscription::ReceiveConfig {\n            subscriber_config: gcp::subscriber::SubscriberConfig {\n                max_outstanding_messages: meta.max_concurrency.map_or(100, |v| v as i64),\n                retry_setting: Some(google_cloud_gax::retry::RetrySetting {\n                    from_millis: meta.retry_policy.as_ref().map_or(10, |retry| {\n                        let min_backoff = retry.min_backoff.max(0) as u64;\n                        min_backoff / 1_000_000 // nanos to millis\n                    }),\n                    max_delay: meta.retry_policy.as_ref().map(|retry| {\n                        let max_backoff = retry.max_backoff.max(0) as u64;\n                        std::time::Duration::from_nanos(max_backoff)\n                    }),\n                    ..default_retry_setting()\n                }),\n                ..Default::default()\n            },\n            ..Default::default()\n        };\n\n        Self {\n            client,\n            project_id: gcp_cfg.project_id.clone(),\n            sub_name: cfg.subscription_cloud_name.clone(),\n            receive_cfg,\n            cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn get_sub(&self) -> Result<&gcp::subscription::Subscription> {\n        let res = self\n            .cell\n            .get_or_init(|| async {\n                match self.client.get().await {\n                    Ok(client) => {\n                        let fqdn = format!(\n                            \"projects/{}/subscriptions/{}\",\n                            self.project_id, self.sub_name\n                        );\n                        Ok(client.subscription(&fqdn))\n                    }\n                    Err(e) => anyhow::bail!(\"failed to get gcp client: {}\", e),\n                }\n            })\n            .await;\n        match res {\n            Ok(sub) => Ok(sub),\n            Err(e) => anyhow::bail!(\"failed to get topic: {}\", e),\n        }\n    }\n}\n\nasync fn handle_message(\n    handler: Arc<SubHandler>,\n    mut message: gcp::subscriber::ReceivedMessage,\n    _cancel: CancellationToken,\n) {\n    let attempt = message.delivery_attempt().unwrap_or(1) as u32;\n    let publish_time = message\n        .message\n        .publish_time\n        .as_ref()\n        .and_then(|ts| chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32));\n\n    let raw_body = message.message.data.drain(..).collect();\n\n    let msg = pubsub::Message {\n        id: message.message.message_id.clone() as MessageId,\n        publish_time,\n        attempt,\n        data: pubsub::MessageData {\n            attrs: message.message.attributes.clone().into_iter().collect(),\n            raw_body,\n        },\n    };\n\n    // Process the message asynchronously.\n    match handler.handle_message(msg).await {\n        Ok(()) => {\n            // Acknowledge the message.\n            if let Err(err) = message.ack().await {\n                log::error!(\"failed to ack message: {:?}\", err);\n            }\n        }\n        Err(err) => {\n            log::info!(\"message handler failed, nacking message: {:?}\", err);\n            if let Err(err) = message.nack().await {\n                log::error!(\"failed to nack message: {:?}\", err);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/gcp/topic.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse anyhow::Result;\nuse google_cloud_googleapis::pubsub::v1::PubsubMessage;\nuse google_cloud_pubsub as gcp;\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::names::CloudName;\nuse crate::pubsub::gcp::LazyGCPClient;\nuse crate::pubsub::{self, MessageData, MessageId};\n\n#[derive(Debug)]\npub struct Topic {\n    client: Arc<LazyGCPClient>,\n    project_id: String,\n    cloud_name: CloudName,\n    cell: tokio::sync::OnceCell<Result<(gcp::topic::Topic, gcp::publisher::Publisher)>>,\n}\n\nimpl Topic {\n    pub(super) fn new(client: Arc<LazyGCPClient>, cfg: &pb::PubSubTopic) -> Self {\n        let Some(pb::pub_sub_topic::ProviderConfig::GcpConfig(gcp_cfg)) =\n            cfg.provider_config.as_ref()\n        else {\n            panic!(\"missing gcp config for topic\")\n        };\n\n        Self {\n            client,\n            project_id: gcp_cfg.project_id.clone(),\n            cloud_name: cfg.cloud_name.clone().into(),\n            cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn get_topic(&self) -> Result<(&gcp::topic::Topic, &gcp::publisher::Publisher)> {\n        let res = self\n            .cell\n            .get_or_init(|| async {\n                match self.client.get().await {\n                    Ok(client) => {\n                        let fqtn =\n                            format!(\"projects/{}/topics/{}\", self.project_id, self.cloud_name);\n                        let topic = client.topic(&fqtn);\n                        let publisher = topic.new_publisher(None);\n                        Ok((topic, publisher))\n                    }\n                    Err(e) => anyhow::bail!(\"failed to get gcp client: {}\", e),\n                }\n            })\n            .await;\n        match res {\n            Ok((topic, publisher)) => Ok((topic, publisher)),\n            Err(e) => anyhow::bail!(\"failed to get topic: {}\", e),\n        }\n    }\n}\n\nimpl pubsub::Topic for Topic {\n    fn publish(\n        &self,\n        msg: MessageData,\n        ordering_key: Option<String>,\n    ) -> Pin<Box<dyn Future<Output = Result<MessageId>> + Send + '_>> {\n        Box::pin(async move {\n            let (_, publisher) = self.get_topic().await?;\n            let awaiter = publisher\n                .publish(PubsubMessage {\n                    data: msg.raw_body,\n                    attributes: msg.attrs.into_iter().collect(),\n                    ordering_key: ordering_key.unwrap_or_default(),\n                    ..Default::default()\n                })\n                .await;\n            match awaiter.get().await {\n                Ok(id) => Ok(id as MessageId),\n                Err(e) => Err(e.into()),\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/manager.rs",
    "content": "use std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::atomic::AtomicUsize;\nuse std::sync::{Arc, OnceLock, RwLock};\n\nuse anyhow::Context;\nuse chrono::Utc;\nuse futures::future::Shared;\nuse futures::FutureExt;\n\nuse crate::api::jsonschema::{self, JSONSchema};\nuse crate::api::{APIResult, PValues};\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::parser::schema::v1 as schema;\nuse crate::encore::runtime::v1 as pb;\nuse crate::log::LogFromRust;\nuse crate::model::{PubSubRequestData, RequestData, ResponseData, SpanId, SpanKey, TraceId};\nuse crate::names::EncoreName;\nuse crate::pubsub::noop::NoopCluster;\nuse crate::pubsub::{\n    gcp, noop, nsq, sqs_sns, Cluster, Message, MessageData, MessageId, SubName, Subscription,\n    SubscriptionHandler, Topic,\n};\nuse crate::trace::{protocol, Tracer};\nuse crate::{api, model};\n\nuse super::push_registry::PushHandlerRegistry;\n\npub struct Manager {\n    tracer: Tracer,\n    topic_cfg: HashMap<EncoreName, TopicConfig>,\n    sub_cfg: HashMap<SubName, SubConfig>,\n    publisher_id: xid::Id,\n\n    topics: Arc<RwLock<HashMap<EncoreName, Arc<TopicInner>>>>,\n    subs: Arc<RwLock<HashMap<SubName, Arc<SubscriptionObj>>>>,\n    push_registry: PushHandlerRegistry,\n}\n\n#[derive(Debug)]\npub struct TopicObj {\n    inner: Arc<TopicInner>,\n}\n\n#[derive(Debug)]\nstruct TopicInner {\n    name: EncoreName,\n    tracer: Tracer,\n    imp: Arc<dyn Topic>,\n    attr_fields: Arc<Vec<String>>,\n    ordering_attr: Option<String>,\n}\n\nimpl TopicObj {\n    pub fn publish(\n        &self,\n        payload: PValues,\n        source: Option<Arc<model::Request>>,\n    ) -> impl Future<Output = anyhow::Result<MessageId>> + 'static {\n        self.inner.publish(payload, source)\n    }\n}\n\nimpl TopicInner {\n    pub fn publish(\n        &self,\n        payload: PValues,\n        source: Option<Arc<model::Request>>,\n    ) -> impl Future<Output = anyhow::Result<MessageId>> + 'static {\n        let tracer = self.tracer.clone();\n        let inner = self.imp.clone();\n        let name = self.name.clone();\n        let attr_fields = self.attr_fields.clone();\n        let ordering_attr = self.ordering_attr.clone();\n        async move {\n            let raw_body = serde_json::to_vec_pretty(&payload)\n                .context(\"unable to serialize message payload\")?;\n            let mut msg = MessageData {\n                attrs: HashMap::new(),\n                raw_body,\n            };\n\n            for name in attr_fields.iter() {\n                if let Some(val) = payload.get(name) {\n                    msg.attrs.insert(name.clone(), val.to_string());\n                }\n            }\n\n            let ordering_key: Option<String> = if let Some(attr) = ordering_attr {\n                Some(\n                    msg.attrs\n                        .get(&attr)\n                        .with_context(|| format!(\"ordering attribute {attr} not found\"))?\n                        .clone(),\n                )\n            } else {\n                None\n            };\n\n            if let Some(source) = source.as_deref() {\n                msg.attrs.insert(\n                    ATTR_PARENT_TRACE_ID.to_string(),\n                    source.span.0.serialize_encore(),\n                );\n                if let Some(ext_correlation_id) = &source.ext_correlation_id {\n                    msg.attrs.insert(\n                        ATTR_EXT_CORRELATION_ID.to_string(),\n                        ext_correlation_id.clone(),\n                    );\n                }\n                // If this is a platform request, propagate the sampled flag so that\n                // subscribers always trace platform-initiated messages.\n                if source.is_platform_request {\n                    msg.attrs\n                        .insert(ATTR_FORCE_TRACE.to_string(), \"true\".to_string());\n                }\n                let start_id = tracer.pubsub_publish_start(protocol::PublishStartData {\n                    source,\n                    topic: &name,\n                    payload: &msg.raw_body,\n                });\n                let result = inner.publish(msg, ordering_key).await;\n                tracer.pubsub_publish_end(protocol::PublishEndData {\n                    start_id,\n                    source,\n                    result: &result,\n                });\n                result\n            } else {\n                inner.publish(msg, ordering_key).await\n            }\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct SubscriptionObj {\n    inner: Arc<dyn Subscription>,\n    tracer: Tracer,\n    service: EncoreName,\n    topic: EncoreName,\n    subscription: EncoreName,\n    schema: JSONSchema,\n\n    handler: OnceLock<Arc<SubHandler>>,\n    subscribe_fut: OnceLock<Shared<SubscribeFut>>,\n}\n\ntype SubscribeFut = Pin<Box<dyn Future<Output = APIResult<()>> + Send>>;\n\nimpl SubscriptionObj {\n    pub async fn subscribe(\n        self: &Arc<Self>,\n        handler: Arc<dyn SubscriptionHandler>,\n    ) -> APIResult<()> {\n        let h = self.handler.get_or_init(|| {\n            Arc::new(SubHandler {\n                obj: self.clone(),\n                handlers: RwLock::new(Vec::new()),\n                counter: AtomicUsize::new(0),\n            })\n        });\n        h.add_handler(handler);\n\n        self.subscribe_fut\n            .get_or_init(|| self.inner.subscribe(h.clone()).shared())\n            .clone()\n            .await\n    }\n}\n\n#[derive(Debug)]\npub struct SubHandler {\n    obj: Arc<SubscriptionObj>,\n    handlers: RwLock<Vec<Arc<dyn SubscriptionHandler>>>,\n    counter: AtomicUsize,\n}\n\nconst ATTR_PARENT_TRACE_ID: &str = \"encore_parent_trace_id\";\nconst ATTR_EXT_CORRELATION_ID: &str = \"encore_ext_correlation_id\";\nconst ATTR_FORCE_TRACE: &str = \"encore_force_trace\";\n\nimpl SubHandler {\n    fn add_handler(&self, h: Arc<dyn SubscriptionHandler>) {\n        self.handlers.write().unwrap().push(h);\n    }\n\n    pub(super) fn handle_message(\n        &self,\n        msg: Message,\n    ) -> Pin<Box<dyn Future<Output = Result<(), api::Error>> + Send + '_>> {\n        Box::pin(async move {\n            let span = SpanKey(TraceId::generate(), SpanId::generate());\n\n            let parent_trace_id: Option<TraceId> = msg\n                .data\n                .attrs\n                .get(ATTR_PARENT_TRACE_ID)\n                .and_then(|s| TraceId::parse_encore(s).ok());\n            let ext_correlation_id = msg.data.attrs.get(ATTR_EXT_CORRELATION_ID);\n\n            // If force trace is set, always trace. Otherwise, make an independent sampling decision.\n            let traced = msg\n                .data\n                .attrs\n                .get(ATTR_FORCE_TRACE)\n                .is_some_and(|s| s == \"true\")\n                || self.obj.tracer.should_sample_pubsub(\n                    &self.obj.service,\n                    &self.obj.topic,\n                    &self.obj.subscription,\n                );\n\n            let mut de = serde_json::Deserializer::from_slice(&msg.data.raw_body);\n            let parsed_payload = self.obj.schema.deserialize(\n                &mut de,\n                jsonschema::DecodeConfig {\n                    coerce_strings: false,\n                    arrays_as_repeated_fields: false,\n                },\n            );\n            let (parsed_payload, parse_error) = match parsed_payload {\n                Ok(parsed_payload) => (Some(parsed_payload), None),\n                Err(e) => (\n                    None,\n                    Some(api::Error::invalid_argument(\n                        \"unable to parse message payload\",\n                        e,\n                    )),\n                ),\n            };\n\n            let start = tokio::time::Instant::now();\n            let start_time = std::time::SystemTime::now();\n            let req = Arc::new(model::Request {\n                span,\n                parent_trace: parent_trace_id,\n                parent_span: None,\n                caller_event_id: None,\n                ext_correlation_id: ext_correlation_id.cloned(),\n                is_platform_request: false,\n                internal_caller: None,\n                start,\n                start_time,\n                data: RequestData::PubSub(PubSubRequestData {\n                    service: self.obj.service.clone(),\n                    topic: self.obj.topic.clone(),\n                    subscription: self.obj.subscription.clone(),\n                    message_id: msg.id.to_string(),\n                    published: msg.publish_time.unwrap_or_else(Utc::now),\n                    attempt: msg.attempt,\n                    payload: msg.data.raw_body.clone(),\n                    parsed_payload,\n                }),\n                traced,\n            });\n\n            let logger = crate::log::root();\n            logger.info(Some(&req), \"starting request\", None);\n\n            self.obj.tracer.request_span_start(&req, false);\n\n            let result = {\n                // If we have a parse error, use that as the result immediately.\n                if let Some(parse_error) = parse_error {\n                    Err(parse_error)\n                } else {\n                    let handler = self.next_handler();\n                    handler.handle_message(req.clone()).await\n                }\n            };\n\n            logger.info(Some(&req), \"request completed\", None);\n\n            let resp = model::Response {\n                request: req,\n                duration: tokio::time::Instant::now().duration_since(start),\n                data: ResponseData::PubSub(result.clone()),\n            };\n            self.obj.tracer.request_span_end(&resp, false);\n            result\n        })\n    }\n\n    fn next_handler(&self) -> Arc<dyn SubscriptionHandler> {\n        let handlers = self.handlers.read().unwrap();\n        let n = handlers.len();\n        // If we have a single handler, skip the increment and modulo steps.\n        if n == 1 {\n            return handlers[0].clone();\n        }\n\n        // Atomically increment the counter, and then get the next handler.\n        let idx = self\n            .counter\n            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);\n        handlers[idx % n].clone()\n    }\n}\n\nimpl Manager {\n    pub fn new(\n        tracer: Tracer,\n        clusters: Vec<pb::PubSubCluster>,\n        md: &meta::Data,\n    ) -> anyhow::Result<Self> {\n        let (topic_cfg, sub_cfg) = make_cfg_maps(clusters, md)?;\n\n        Ok(Self {\n            publisher_id: xid::new(),\n            tracer,\n            topic_cfg,\n            sub_cfg,\n            topics: Arc::default(),\n            subs: Arc::default(),\n            push_registry: PushHandlerRegistry::new(),\n        })\n    }\n\n    pub fn topic(&self, name: EncoreName) -> Option<TopicObj> {\n        let inner = self.topic_impl(name)?;\n        Some(TopicObj { inner })\n    }\n\n    fn topic_impl(&self, name: EncoreName) -> Option<Arc<TopicInner>> {\n        if let Some(topic) = self.topics.read().unwrap().get(&name) {\n            return Some(topic.clone());\n        }\n\n        let topic = Arc::new({\n            if let Some(cfg) = self.topic_cfg.get(&name) {\n                let imp = cfg.cluster.topic(&cfg.cfg, self.publisher_id);\n                TopicInner {\n                    name: name.clone(),\n                    imp,\n                    tracer: self.tracer.clone(),\n                    attr_fields: cfg.attr_fields.clone(),\n                    ordering_attr: cfg.cfg.ordering_attr.clone(),\n                }\n            } else {\n                TopicInner {\n                    name: name.clone(),\n                    imp: Arc::new(noop::NoopTopic),\n                    tracer: self.tracer.clone(),\n                    attr_fields: Arc::new(vec![]),\n                    ordering_attr: None,\n                }\n            }\n        });\n\n        self.topics.write().unwrap().insert(name, topic.clone());\n        Some(topic)\n    }\n\n    pub fn subscription(&self, name: SubName) -> Option<Arc<SubscriptionObj>> {\n        if let Some(sub) = self.subs.read().unwrap().get(&name) {\n            return Some(sub.clone());\n        }\n\n        let sub = {\n            if let Some(cfg) = self.sub_cfg.get(&name) {\n                let inner = cfg.cluster.subscription(&cfg.cfg, &cfg.meta);\n\n                // If we have a push handler, register it.\n                if let Some((sub_id, push_handler)) = inner.push_handler() {\n                    self.push_registry.register(sub_id, push_handler);\n                }\n\n                Arc::new(SubscriptionObj {\n                    inner,\n                    tracer: self.tracer.clone(),\n                    service: cfg.meta.service_name.clone().into(),\n                    topic: name.topic.clone(),\n                    subscription: name.subscription.clone(),\n                    schema: cfg.schema.clone(),\n                    handler: OnceLock::new(),\n                    subscribe_fut: Default::default(),\n                })\n            } else {\n                let inner = Arc::new(noop::NoopSubscription);\n                Arc::new(SubscriptionObj {\n                    inner,\n                    tracer: self.tracer.clone(),\n                    topic: name.topic.clone(),\n                    subscription: name.subscription.clone(),\n\n                    // We don't have a service if there is no sub config, but that's fine\n                    // since it's only used by tracing, and a no-op subscription won't\n                    // generate any traces.\n                    service: \"\".into(),\n\n                    // We don't have a schema since it's an unknown subscription.\n                    // Use a null schema.\n                    schema: JSONSchema::null(),\n\n                    handler: OnceLock::new(),\n                    subscribe_fut: Default::default(),\n                })\n            }\n        };\n\n        self.subs.write().unwrap().insert(name, sub.clone());\n\n        Some(sub)\n    }\n\n    pub fn push_registry(&self) -> PushHandlerRegistry {\n        self.push_registry.clone()\n    }\n}\n\n#[derive(Debug)]\nstruct TopicConfig {\n    cluster: Arc<dyn Cluster>,\n    cfg: pb::PubSubTopic,\n\n    /// Names of fields in the payload that should be copied into\n    /// the PubSub message attributes.\n    attr_fields: Arc<Vec<String>>,\n}\n\n#[derive(Debug)]\nstruct SubConfig {\n    cluster: Arc<dyn Cluster>,\n    cfg: pb::PubSubSubscription,\n    meta: meta::pub_sub_topic::Subscription,\n    schema: JSONSchema,\n}\n\nfn make_cfg_maps(\n    clusters: Vec<pb::PubSubCluster>,\n    md: &meta::Data,\n) -> anyhow::Result<(\n    HashMap<EncoreName, TopicConfig>,\n    HashMap<SubName, SubConfig>,\n)> {\n    let mut topic_map = HashMap::new();\n    let mut sub_map = HashMap::new();\n\n    let mut schema_builder = jsonschema::Builder::new(md);\n    let (meta_topics, meta_subs) = {\n        let mut topic_map = HashMap::new();\n        let mut sub_map = HashMap::new();\n\n        for topic in &md.pubsub_topics {\n            let Some(schema_type) = &topic.message_type else {\n                anyhow::bail!(\"topic {} has no message type\", topic.name);\n            };\n            let attr_fields = message_attr_fields(&md.decls, schema_type, 0)?;\n            let schema_idx = schema_builder\n                .register_type(schema_type)\n                .with_context(|| format!(\"invalid schema for topic {}\", topic.name))?;\n\n            topic_map.insert(topic.name.clone(), Arc::new(attr_fields));\n            for sub in &topic.subscriptions {\n                let name = SubName {\n                    topic: topic.name.clone().into(),\n                    subscription: sub.name.clone().into(),\n                };\n                sub_map.insert(name, (sub, schema_idx));\n            }\n        }\n        (topic_map, sub_map)\n    };\n\n    let schemas = schema_builder.build();\n    for cluster_cfg in clusters {\n        let cluster = new_cluster(&cluster_cfg);\n\n        for topic_cfg in cluster_cfg.topics {\n            let Some(attr_fields) = meta_topics.get(&topic_cfg.encore_name) else {\n                anyhow::bail!(\"topic {} not found in metadata\", topic_cfg.encore_name);\n            };\n            topic_map.insert(\n                topic_cfg.encore_name.clone().into(),\n                TopicConfig {\n                    cluster: cluster.clone(),\n                    cfg: topic_cfg,\n                    attr_fields: attr_fields.clone(),\n                },\n            );\n        }\n\n        for sub_cfg in cluster_cfg.subscriptions {\n            let topic_name = sub_cfg.topic_encore_name.clone().into();\n            let sub_name = sub_cfg.subscription_encore_name.clone().into();\n            let name = SubName {\n                topic: topic_name,\n                subscription: sub_name,\n            };\n            let Some(&(meta_sub, idx)) = meta_subs.get(&name) else {\n                continue;\n            };\n\n            let schema = schemas.schema(idx);\n            sub_map.insert(\n                name,\n                SubConfig {\n                    cluster: cluster.clone(),\n                    cfg: sub_cfg,\n                    meta: meta_sub.to_owned(),\n                    schema,\n                },\n            );\n        }\n    }\n\n    Ok((topic_map, sub_map))\n}\n\nfn new_cluster(cluster: &pb::PubSubCluster) -> Arc<dyn Cluster> {\n    let Some(provider) = &cluster.provider else {\n        log::error!(\"missing PubSub cluster provider: {}\", cluster.rid);\n        return Arc::new(NoopCluster);\n    };\n\n    match provider {\n        pb::pub_sub_cluster::Provider::Gcp(_) => return Arc::new(gcp::Cluster::new()),\n        pb::pub_sub_cluster::Provider::Nsq(cfg) => {\n            return Arc::new(nsq::Cluster::new(cfg.hosts[0].clone()));\n        }\n        pb::pub_sub_cluster::Provider::Aws(_) => return Arc::new(sqs_sns::Cluster::new()),\n        pb::pub_sub_cluster::Provider::Encore(_) => {\n            log::error!(\"Encore Cloud Pub/Sub not yet supported: {}\", cluster.rid);\n        }\n        pb::pub_sub_cluster::Provider::Azure(_) => {\n            log::error!(\"Azure Pub/Sub not yet supported: {}\", cluster.rid);\n        }\n    }\n\n    Arc::new(NoopCluster)\n}\n\nfn message_attr_fields(\n    decls: &[schema::Decl],\n    typ: &schema::Type,\n    depth: u16,\n) -> anyhow::Result<Vec<String>> {\n    if depth > 100 {\n        anyhow::bail!(\"pubsub message attribute computation: recursion depth exceeded\");\n    }\n    let Some(typ) = &typ.typ else {\n        return Ok(vec![]);\n    };\n\n    use schema::r#type::Typ;\n    match typ {\n        Typ::Named(named) => {\n            let Some(decl) = decls.get(named.id as usize) else {\n                anyhow::bail!(\"missing decl for named type\");\n            };\n            let Some(decl_typ) = decl.r#type.as_ref() else {\n                anyhow::bail!(\"missing type for named decl\");\n            };\n            message_attr_fields(decls, decl_typ, depth + 1)\n        }\n        Typ::Struct(st) => {\n            let names: Vec<String> = st\n                .fields\n                .iter()\n                .filter_map(|f| {\n                    f.tags.iter().find_map(|tag| {\n                        if tag.key == \"pubsub-attr\" {\n                            Some(tag.name.clone())\n                        } else {\n                            None\n                        }\n                    })\n                })\n                .collect();\n            Ok(names)\n        }\n\n        Typ::Map(_)\n        | Typ::List(_)\n        | Typ::Builtin(_)\n        | Typ::Pointer(_)\n        | Typ::Option(_)\n        | Typ::Union(_)\n        | Typ::Literal(_)\n        | Typ::TypeParameter(_)\n        | Typ::Config(_) => Ok(vec![]),\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/mod.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt::Debug;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\npub use manager::{Manager, SubscriptionObj, TopicObj};\npub use push_registry::PushHandlerRegistry;\n\nuse crate::api::APIResult;\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::names::EncoreName;\nuse crate::pubsub::manager::SubHandler;\nuse crate::{api, model};\n\nmod gcp;\nmod manager;\nmod noop;\nmod nsq;\nmod push_registry;\nmod sqs_sns;\n\npub type MessageId = String;\n\npub struct MessageData {\n    pub attrs: HashMap<String, String>,\n    pub raw_body: Vec<u8>,\n}\n\npub struct Message {\n    pub id: MessageId,\n    pub publish_time: Option<chrono::DateTime<chrono::Utc>>,\n    /// starts at 1\n    pub attempt: u32,\n    pub data: MessageData,\n}\n\ntrait Cluster: Debug + Send + Sync {\n    fn topic(&self, cfg: &pb::PubSubTopic, publisher_id: xid::Id) -> Arc<dyn Topic + 'static>;\n    fn subscription(\n        &self,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Arc<dyn Subscription + 'static>;\n}\n\ntrait Topic: Debug + Send + Sync {\n    fn publish(\n        &self,\n        msg: MessageData,\n        ordering_key: Option<String>,\n    ) -> Pin<Box<dyn Future<Output = anyhow::Result<MessageId>> + Send + '_>>;\n}\n\ntrait Subscription: Debug + Send + Sync {\n    fn subscribe(\n        &self,\n        handler: Arc<SubHandler>,\n    ) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>>;\n\n    fn push_handler(&self) -> Option<(String, Arc<dyn PushRequestHandler>)> {\n        None\n    }\n}\n\ntrait PushRequestHandler: Debug + Sync + Send + 'static {\n    fn handle_push(\n        &self,\n        req: axum::extract::Request,\n    ) -> Pin<Box<dyn Future<Output = axum::response::Response<axum::body::Body>> + Send + 'static>>;\n}\n\npub trait SubscriptionHandler: Debug + Send + Sync {\n    fn handle_message(\n        &self,\n        msg: Arc<model::Request>,\n    ) -> Pin<Box<dyn Future<Output = Result<(), api::Error>> + Send + '_>>;\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Hash)]\npub struct SubName {\n    pub topic: EncoreName,\n    pub subscription: EncoreName,\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/noop/mod.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse anyhow::Result;\n\nuse crate::api::APIResult;\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub;\nuse crate::pubsub::manager::SubHandler;\n\n#[derive(Debug)]\npub struct NoopCluster;\n\n#[derive(Debug)]\npub struct NoopTopic;\n#[derive(Debug)]\npub struct NoopSubscription;\n\nimpl pubsub::Cluster for NoopCluster {\n    fn topic(&self, _cfg: &pb::PubSubTopic, _publisher_id: xid::Id) -> Arc<dyn pubsub::Topic> {\n        Arc::new(NoopTopic)\n    }\n\n    fn subscription(\n        &self,\n        _cfg: &pb::PubSubSubscription,\n        _meta: &meta::pub_sub_topic::Subscription,\n    ) -> Arc<dyn pubsub::Subscription> {\n        Arc::new(NoopSubscription)\n    }\n}\n\nimpl pubsub::Topic for NoopTopic {\n    fn publish(\n        &self,\n        _: pubsub::MessageData,\n        _: Option<String>,\n    ) -> Pin<Box<dyn Future<Output = Result<pubsub::MessageId>> + Send + '_>> {\n        Box::pin(async {\n            anyhow::bail!(\"topic not configured\");\n        })\n    }\n}\n\nimpl pubsub::Subscription for NoopSubscription {\n    fn subscribe(\n        &self,\n        _: Arc<SubHandler>,\n    ) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>> {\n        Box::pin(futures::future::pending())\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/nsq/mod.rs",
    "content": "use std::sync::Arc;\n\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub;\nuse crate::pubsub::nsq::sub::NsqSubscription;\nuse crate::pubsub::nsq::topic::NsqTopic;\n\nmod sub;\nmod topic;\n\n#[derive(Debug)]\npub struct Cluster {\n    /// Address to the NSQ server.\n    address: String,\n}\n\nimpl Cluster {\n    pub fn new(address: String) -> Self {\n        Self { address }\n    }\n}\n\nimpl pubsub::Cluster for Cluster {\n    fn topic(\n        &self,\n        cfg: &pb::PubSubTopic,\n        _publisher_id: xid::Id,\n    ) -> Arc<dyn pubsub::Topic + 'static> {\n        Arc::new(NsqTopic::new(self.address.clone(), cfg))\n    }\n\n    fn subscription(\n        &self,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Arc<dyn pubsub::Subscription + 'static> {\n        Arc::new(NsqSubscription::new(self.address.clone(), cfg, meta))\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/nsq/sub.rs",
    "content": "use std::fmt::Debug;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse anyhow::{Context, Result};\nuse tokio_nsq::{\n    NSQChannel, NSQConsumerConfig, NSQConsumerConfigSources, NSQMessage, NSQRequeueDelay, NSQTopic,\n};\n\nuse crate::api::APIResult;\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub;\nuse crate::pubsub::manager::SubHandler;\nuse crate::pubsub::nsq::topic::EncodedMessage;\nuse crate::pubsub::Subscription;\n\npub struct NsqSubscription {\n    addr: String,\n    config: NSQConsumerConfig,\n    max_retries: i64,\n}\n\nimpl Debug for NsqSubscription {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"NsqSubscription\")\n            .field(\"addr\", &self.addr)\n            .finish()\n    }\n}\n\nimpl NsqSubscription {\n    pub(super) fn new(\n        addr: String,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Self {\n        let topic = NSQTopic::new(&cfg.topic_cloud_name)\n            .expect(\"topic_cloud_name should be valid NSQ topic name\");\n\n        let channel = NSQChannel::new(&cfg.subscription_cloud_name)\n            .expect(\"subscription_cloud_name should be valid NSQ channel name\");\n\n        let mut config = NSQConsumerConfig::new(topic, channel)\n            .set_sources(NSQConsumerConfigSources::Daemons(vec![addr.clone()]))\n            .set_max_in_flight(meta.max_concurrency.map_or(100, |v| v as u32));\n\n        // For local development, default to 2 retries if we don't have a retry policy.\n        // We don't want to retry forever but zero retries might cause surprises when suddenly\n        // things start retrying in other environments.\n        let mut max_retries = 2;\n\n        if let Some(retry) = &meta.retry_policy {\n            let min_backoff = Duration::from_nanos(retry.min_backoff.max(0) as u64);\n            config = config.set_base_requeue_interval(clamp(\n                min_backoff,\n                Duration::from_secs(0),\n                Duration::from_secs(60 * 60),\n            ));\n\n            let max_backoff = Duration::from_nanos(retry.max_backoff.max(0) as u64);\n            config = config.set_max_requeue_interval(clamp(\n                max_backoff,\n                Duration::from_secs(0),\n                Duration::from_secs(60 * 60),\n            ));\n            max_retries = retry.max_retries;\n        }\n\n        NsqSubscription {\n            addr,\n            config,\n            max_retries,\n        }\n    }\n}\n\nimpl Subscription for NsqSubscription {\n    fn subscribe(\n        &self,\n        handler: Arc<SubHandler>,\n    ) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>> {\n        let mut consumer = self.config.clone().build();\n        let max_retries = self.max_retries;\n\n        Box::pin(async move {\n            loop {\n                let Some(msg) = consumer.consume_filtered().await else {\n                    continue;\n                };\n\n                // If the attempt exceeds the max retries, drop it.\n                // Attempt starts at 1 for the first delivery, which means\n                // the retry count is (attempt-1).\n                let retry = msg.attempt as i64 - 1;\n                if retry > max_retries {\n                    msg.finish().await;\n                    continue;\n                }\n\n                // Process the message asynchronously.\n                let h = handler.clone();\n                tokio::spawn(async move { process_message(msg, h).await });\n            }\n        })\n    }\n}\n\nasync fn process_message(mut msg: NSQMessage, handler: Arc<SubHandler>) {\n    let body: Vec<u8> = msg.body.drain(..).collect();\n    let timestamp = msg.timestamp;\n    let attempt = msg.attempt;\n\n    // Create a channel to signal when to stop sending touch messages\n    let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel::<()>();\n\n    // Spawn a background task to send touch messages every 30 seconds\n    let touch_handle = tokio::spawn(async move {\n        let mut interval = tokio::time::interval(Duration::from_secs(30));\n        // Skip the first tick (immediate)\n        interval.tick().await;\n\n        loop {\n            tokio::select! {\n                _ = interval.tick() => {\n                    msg.touch().await;\n                }\n                _ = &mut stop_rx => {\n                    // Stop signal received, exit the loop\n                    break;\n                }\n            }\n        }\n\n        // Return the message so we can finish or requeue it\n        msg\n    });\n\n    let result = handle_message(body, timestamp, attempt, handler).await;\n\n    // Signal the touch task to stop and return the message\n    let _ = stop_tx.send(());\n    let msg = match touch_handle.await {\n        Ok(msg) => msg,\n        Err(err) => {\n            log::error!(\n                \"touch task failed, unable to finish or requeue message: {:?}\",\n                err\n            );\n            // If the touch task failed, we can't access the message to finish or requeue it.\n            // NSQ will automatically requeue the message after the timeout expires,\n            // so we return early and let NSQ handle the requeue.\n            return;\n        }\n    };\n\n    match result {\n        Ok(()) => msg.finish().await,\n        Err(err) => {\n            log::info!(\"message handler failed, requeueing message: {:?}\", err);\n            msg.requeue(NSQRequeueDelay::DefaultDelay).await;\n        }\n    }\n}\n\nasync fn handle_message(\n    body: Vec<u8>,\n    timestamp: u64,\n    attempt: u16,\n    handler: Arc<SubHandler>,\n) -> Result<()> {\n    let encoded =\n        serde_json::from_slice::<EncodedMessage>(&body).context(\"failed to decode message\")?;\n\n    let publish_time = nano_timestamp(timestamp);\n    let raw_body = serde_json::to_vec_pretty(&encoded.body).unwrap_or_default();\n    let pubsub_msg = pubsub::Message {\n        id: encoded.id,\n        publish_time,\n        attempt: attempt as u32,\n        data: pubsub::MessageData {\n            attrs: encoded.attrs,\n            raw_body,\n        },\n    };\n\n    handler\n        .handle_message(pubsub_msg)\n        .await\n        .context(\"message handler failed\")\n}\n\nfn nano_timestamp(mut nsec: u64) -> Option<chrono::DateTime<chrono::Utc>> {\n    // From Go's time.Unix.\n    let mut sec: i64 = 0;\n    if nsec >= 1_000_000_000 {\n        let n = nsec / 1_000_000_000;\n        sec += n as i64;\n        nsec -= n * 1_000_000_000;\n    }\n    chrono::DateTime::from_timestamp(sec, nsec as u32)\n}\n\nfn clamp<T: PartialOrd>(val: T, min: T, max: T) -> T {\n    if val < min {\n        min\n    } else if val > max {\n        max\n    } else {\n        val\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/nsq/topic.rs",
    "content": "use std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\n\nuse anyhow::{Context, Result};\nuse serde::{Deserialize, Serialize};\nuse tokio::sync::{mpsc, oneshot};\nuse tokio_nsq::{NSQEvent, NSQProducerConfig, NSQTopic};\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub::{MessageData, MessageId, Topic};\n\nstruct PublishRequest {\n    msg: MessageData,\n    resp: oneshot::Sender<Result<MessageId>>,\n}\n\n#[derive(Debug)]\npub struct NsqTopic {\n    tx: mpsc::Sender<PublishRequest>,\n}\n\nimpl NsqTopic {\n    pub(super) fn new(addr: String, cfg: &pb::PubSubTopic) -> Self {\n        let cloud_name = cfg.cloud_name.clone();\n        let (tx, mut rx) = mpsc::channel::<PublishRequest>(32);\n        tokio::spawn(async move {\n            let topic =\n                NSQTopic::new(&cloud_name).expect(\"cloud_name should be valid NSQ topic name\");\n            let mut producer = NSQProducerConfig::new(addr).build();\n\n            // Wait for the producer to send a Ready event.\n            loop {\n                if let Some(NSQEvent::Healthy()) = producer.consume().await {\n                    break;\n                }\n            }\n\n            loop {\n                // Wait for either a publish request or an acknowledgement from the producer.\n                tokio::select! {\n                    req = rx.recv() => {\n                        let Some(req) = req else {\n                            break;\n                        };\n\n                        // Serialize the message body.\n                        let encoded = EncodedMessage::new_for_data(req.msg);\n                        let bytes = serde_json::to_vec(&encoded).expect(\"unable to serialize request\");\n\n                        // TODO handle error\n                        producer\n                            .publish(&topic, bytes)\n                            .await\n                            .expect(\"failed to publish message\");\n\n                        // Ignore error.\n                        _ = req.resp.send(Ok(encoded.id));\n                    }\n                    _ = producer.consume() => {}\n                }\n            }\n        });\n\n        NsqTopic { tx }\n    }\n}\n\nimpl Topic for NsqTopic {\n    fn publish(\n        &self,\n        msg: MessageData,\n        _ordering_key: Option<String>,\n    ) -> Pin<Box<dyn Future<Output = Result<MessageId>> + Send + '_>> {\n        let tx = self.tx.clone();\n        Box::pin(async move {\n            let (resp_tx, resp_rx) = oneshot::channel::<Result<MessageId>>();\n            let req = PublishRequest { msg, resp: resp_tx };\n            tx.send(req).await.context(\"failed to send message\")?;\n\n            resp_rx.await.context(\"failed to receive response\")?\n        })\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub(super) struct EncodedMessage {\n    pub id: MessageId,\n    pub attrs: HashMap<String, String>,\n    pub body: Option<serde_json::Value>,\n}\n\nimpl EncodedMessage {\n    pub fn new_for_data(msg: MessageData) -> Self {\n        let body = serde_json::from_slice(&msg.raw_body).unwrap_or_default();\n        Self {\n            id: xid::new().to_string(),\n            attrs: msg.attrs,\n            body,\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/push_registry.rs",
    "content": "use std::{collections::HashMap, pin::Pin, sync::Arc};\n\nuse axum::{extract::Path, RequestExt};\nuse futures::Future;\nuse std::sync::RwLock;\n\nuse crate::api::{self, ToResponse};\n\nuse super::PushRequestHandler;\n\n#[derive(Debug, Clone)]\npub struct PushHandlerRegistry {\n    inner: Arc<Inner>,\n}\n\nimpl Default for PushHandlerRegistry {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\nimpl PushHandlerRegistry {\n    pub fn new() -> Self {\n        Self {\n            inner: Arc::new(Inner {\n                handlers: Arc::new(RwLock::new(HashMap::new())),\n            }),\n        }\n    }\n}\n\n#[derive(Debug)]\nstruct Inner {\n    handlers: Arc<RwLock<HashMap<String, Arc<dyn PushRequestHandler>>>>,\n}\n\nimpl PushHandlerRegistry {\n    pub(super) fn register(&self, subscription_id: String, handler: Arc<dyn PushRequestHandler>) {\n        self.inner\n            .handlers\n            .write()\n            .unwrap()\n            .insert(subscription_id, handler);\n    }\n\n    async fn handle(\n        self,\n        mut req: axum::extract::Request,\n    ) -> axum::response::Response<axum::body::Body> {\n        let id: String = match req.extract_parts().await {\n            Ok(Path(id)) => id,\n            Err(e) => {\n                ::log::error!(\n                    \"encore pub/sub push handler: unable to extract push id from path: {:?}\",\n                    e\n                );\n                return api::Error::internal(e).to_response(None);\n            }\n        };\n\n        ::log::trace!(\"encore pub/sub push handler: handling push request\");\n\n        let handler = self.inner.handlers.read().unwrap().get(&id).cloned();\n\n        match handler {\n            Some(handler) => handler.handle_push(req).await,\n            None => api::Error::not_found(\"no handler registered for push subscription\")\n                .to_response(None),\n        }\n    }\n}\n\nimpl axum::handler::Handler<(), ()> for PushHandlerRegistry {\n    type Future = Pin<Box<dyn Future<Output = axum::response::Response<axum::body::Body>> + Send>>;\n\n    fn call(self, req: axum::extract::Request, _state: ()) -> Self::Future {\n        Box::pin(self.handle(req))\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/sqs_sns/fetcher.rs",
    "content": "use std::fmt::Debug;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\npub trait Fetcher: Clone + Sync + Send {\n    type Item;\n    type Error: Debug;\n\n    #[allow(clippy::type_complexity)]\n    fn fetch(\n        self,\n        max_items: usize,\n    ) -> Pin<Box<dyn Future<Output = Result<Vec<Self::Item>, Self::Error>> + Send + 'static>>;\n    fn process(self, item: Self::Item) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>>;\n}\n\n#[derive(Debug, Clone)]\npub struct Config {\n    /// The maximum number of items to process at once.\n    pub max_concurrency: usize,\n\n    /// The maximum number of items to fetch at once.\n    pub max_batch_size: usize,\n}\n\npub async fn process_concurrently<F: Fetcher>(cfg: Config, fetcher: F) {\n    // Semaphore representing work being processed.\n    let sem = Arc::new(tokio::sync::Semaphore::new(cfg.max_concurrency));\n\n    // The effective max batch size is the minimum of the maximum concurrency\n    // and the maximum batch size.\n    let max_batch = cfg.max_concurrency.min(cfg.max_batch_size);\n\n    // Retry policy configuration.\n    let (base_sleep, max_sleep, mut err_sleep) = {\n        use tokio::time::Duration;\n        let base = Duration::from_millis(150);\n        let max = Duration::from_secs(5);\n        (base, max, base)\n    };\n\n    // Fetch work.\n    loop {\n        // How many items shall we fetch?\n        let (to_fetch, permit) = {\n            // Wait for at least one permit.\n            let mut permit = sem.acquire().await.expect(\"semaphore is closed\");\n\n            // Do we have any additional available permits and the max batch size allows for it?\n            let extra = sem.available_permits().min(max_batch - 1);\n            if extra > 0 {\n                // Acquire additional permits. Guaranteed to succeed since we've checked\n                // the available permits, and there's no race since this task is the only one\n                // acquiring permits.\n                let extra_permit = sem\n                    .acquire_many(extra as u32)\n                    .await\n                    .expect(\"semaphore is closed\");\n                permit.merge(extra_permit);\n            }\n            (1 + extra, permit)\n        };\n\n        let fetch_result = fetcher.clone().fetch(to_fetch).await;\n        match fetch_result {\n            Ok(work) => {\n                err_sleep = base_sleep;\n\n                // Process the work. We forget the permit here,\n                // and release individual items back to the semaphore as we process them.\n                let unused_permits = permit.num_permits() - work.len();\n                permit.forget();\n\n                // Add back any permits that were not used, in case we fetched fewer items than the max.\n                if unused_permits > 0 {\n                    sem.add_permits(unused_permits);\n                }\n\n                for item in work {\n                    let fut = fetcher.clone().process(item);\n                    let sem = sem.clone();\n                    tokio::spawn(async move {\n                        fut.await;\n                        sem.add_permits(1);\n                    });\n                }\n            }\n            Err(err) => {\n                log::error!(\"encore: pub/sub fetch error, retrying: {err:#?}\");\n                tokio::time::sleep(err_sleep).await;\n                err_sleep = err_sleep.mul_f32(1.5).min(max_sleep);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/sqs_sns/mod.rs",
    "content": "use std::sync::Arc;\n\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::pubsub;\nuse crate::pubsub::sqs_sns::sub::Subscription;\nuse crate::pubsub::sqs_sns::topic::Topic;\n\nmod fetcher;\nmod sub;\nmod topic;\n\n#[derive(Debug)]\npub struct Cluster {\n    /// publisher_id is a unique ID for this Encore app instance, used as the Message Group ID\n    /// for topics which don't specify a grouping field. This is based on [AWS's recommendation]\n    /// that each producer should have a unique message group ID to send all it's messages.\n    ///\n    /// [AWS's recommendation]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-understanding-logic.html\n    _publisher_id: xid::Id,\n\n    client: Arc<LazyClient>,\n}\n\nimpl Cluster {\n    pub fn new() -> Self {\n        let publisher_id = xid::new();\n        let client = Arc::new(LazyClient::new());\n        Self {\n            _publisher_id: publisher_id,\n            client,\n        }\n    }\n}\n\nimpl pubsub::Cluster for Cluster {\n    fn topic(\n        &self,\n        cfg: &pb::PubSubTopic,\n        publisher_id: xid::Id,\n    ) -> Arc<dyn pubsub::Topic + 'static> {\n        Arc::new(Topic::new(self.client.clone(), cfg, publisher_id))\n    }\n\n    fn subscription(\n        &self,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Arc<dyn pubsub::Subscription + 'static> {\n        Arc::new(Subscription::new(self.client.clone(), cfg, meta))\n    }\n}\n\n#[derive(Debug)]\nstruct LazyClient {\n    sns_cell: tokio::sync::OnceCell<aws_sdk_sns::Client>,\n    sqs_cell: tokio::sync::OnceCell<aws_sdk_sqs::Client>,\n}\n\nimpl LazyClient {\n    fn new() -> Self {\n        Self {\n            sns_cell: tokio::sync::OnceCell::new(),\n            sqs_cell: tokio::sync::OnceCell::new(),\n        }\n    }\n\n    async fn config(&self) -> aws_config::SdkConfig {\n        let provider = aws_config::meta::region::RegionProviderChain::default_provider();\n        aws_config::defaults(aws_config::BehaviorVersion::latest())\n            .region(provider)\n            .load()\n            .await\n    }\n\n    async fn get_sns(&self) -> &aws_sdk_sns::Client {\n        self.sns_cell\n            .get_or_init(|| async {\n                let cfg = self.config().await;\n                aws_sdk_sns::Client::new(&cfg)\n            })\n            .await\n    }\n\n    async fn get_sqs(&self) -> &aws_sdk_sqs::Client {\n        self.sqs_cell\n            .get_or_init(|| async {\n                let cfg = self.config().await;\n                aws_sdk_sqs::Client::new(&cfg)\n            })\n            .await\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/sqs_sns/sub.rs",
    "content": "use std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse anyhow::{Context, Result};\nuse aws_sdk_sqs::types::MessageSystemAttributeName;\nuse serde::Deserialize;\nuse tokio_retry::strategy::ExponentialBackoff;\nuse tokio_retry::{Action, Retry};\n\nuse crate::api::APIResult;\nuse crate::encore::parser::meta::v1 as meta;\nuse crate::encore::runtime::v1 as pb;\nuse crate::names::CloudName;\nuse crate::pubsub::manager::SubHandler;\nuse crate::pubsub::sqs_sns::{fetcher, LazyClient};\nuse crate::pubsub::{self};\n\n#[derive(Debug)]\npub struct Subscription {\n    client: Arc<LazyClient>,\n    cloud_name: CloudName,\n    ack_deadline: Duration,\n    fetcher_cfg: fetcher::Config,\n    requeue_policy: ExponentialBackoff,\n}\n\nimpl Subscription {\n    pub(super) fn new(\n        client: Arc<LazyClient>,\n        cfg: &pb::PubSubSubscription,\n        meta: &meta::pub_sub_topic::Subscription,\n    ) -> Self {\n        let mut requeue_policy = ExponentialBackoff::from_millis(\n            meta.retry_policy\n                .as_ref()\n                .map(|retry| retry.min_backoff / 1_000_000)\n                .unwrap_or(1000) as u64,\n        )\n        .factor(2);\n\n        if let Some(retry) = &meta.retry_policy {\n            requeue_policy =\n                requeue_policy.max_delay(Duration::from_nanos(retry.max_backoff as u64));\n        }\n\n        let fetcher_cfg = fetcher::Config {\n            max_concurrency: meta.max_concurrency.unwrap_or(100) as usize,\n            max_batch_size: 10, // AWS SQS max batch size\n        };\n\n        // Clamp the ack deadline to between [1s, 12h].\n        let ack_deadline = Duration::from_nanos(\n            meta.ack_deadline\n                .clamp(1_000_000_000, 12 * 3600 * 1_000_000_000) as u64,\n        );\n\n        Self {\n            client,\n            cloud_name: cfg.subscription_cloud_name.clone().into(),\n            ack_deadline,\n            fetcher_cfg,\n            requeue_policy,\n        }\n    }\n}\n\nimpl pubsub::Subscription for Subscription {\n    fn subscribe(\n        &self,\n        handler: Arc<SubHandler>,\n    ) -> Pin<Box<dyn Future<Output = APIResult<()>> + Send + 'static>> {\n        let client = self.client.clone();\n        let cloud_name = self.cloud_name.clone();\n        let ack_deadline = self.ack_deadline;\n        let requeue_policy = self.requeue_policy.clone();\n        let fetcher_cfg = self.fetcher_cfg.clone();\n\n        Box::pin(async move {\n            let client = client.get_sqs().await.clone();\n\n            let sqs_fetcher = Arc::new(SqsFetcher {\n                handler,\n                client,\n                queue_url: cloud_name.into(),\n                ack_deadline,\n                requeue_policy,\n            });\n            fetcher::process_concurrently(fetcher_cfg.clone(), sqs_fetcher).await;\n\n            Ok(())\n        })\n    }\n}\n\nstruct SqsFetcher {\n    client: aws_sdk_sqs::Client,\n    queue_url: String,\n    ack_deadline: Duration,\n    requeue_policy: ExponentialBackoff,\n    handler: Arc<SubHandler>,\n}\n\nimpl fetcher::Fetcher for Arc<SqsFetcher> {\n    type Item = aws_sdk_sqs::types::Message;\n    type Error = anyhow::Error;\n\n    fn fetch(\n        self,\n        max_items: usize,\n    ) -> Pin<\n        Box<\n            dyn Future<Output = std::result::Result<Vec<Self::Item>, Self::Error>> + Send + 'static,\n        >,\n    > {\n        let client = self.client.clone();\n        let queue_url = self.queue_url.clone();\n        let ack_deadline = self.ack_deadline;\n\n        Box::pin(async move {\n            let result = client\n                .receive_message()\n                .queue_url(queue_url)\n                .message_system_attribute_names(MessageSystemAttributeName::ApproximateReceiveCount)\n                .visibility_timeout(ack_deadline.as_secs() as i32)\n                .wait_time_seconds(20) // maximum allowed time\n                .max_number_of_messages(max_items as i32)\n                .send()\n                .await;\n            result\n                .map(|res| res.messages.unwrap_or_default())\n                .context(\"unable to receive messages\")\n        })\n    }\n\n    fn process(self, item: Self::Item) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {\n        Box::pin(async move {\n            let receipt_handle = item.receipt_handle.clone().expect(\"missing receipt handle\");\n            let attempt = parse_attempt(&item);\n\n            let result = match parse_message(item, attempt) {\n                Ok(msg) => self\n                    .handler\n                    .handle_message(msg)\n                    .await\n                    .map_err(|err| err.into()),\n                Err(err) => {\n                    log::error!(\n                        \"encore: internal error: failed to parse message from SQS: {:#?}\",\n                        err\n                    );\n                    Err(err)\n                }\n            };\n\n            match result {\n                Ok(()) => {\n                    let delete_action = DeleteMessageAction {\n                        fetcher: self.clone(),\n                        receipt_handle,\n                    };\n\n                    // Retry deleting a few times.\n                    // If we can't delete the message, it'll be redelivered. Not much we can do.\n                    let retry = ExponentialBackoff::from_millis(100).factor(2).take(5);\n                    if let Err(err) = Retry::spawn(retry, delete_action).await {\n                        log::error!(\n                            \"encore: internal error: failed to delete aws pub/sub message: {}\",\n                            err\n                        );\n                    }\n                }\n                Err(_) => {\n                    // Determine the requeue delay.\n                    let requeue_delay = self\n                        .requeue_policy\n                        .clone()\n                        .nth((attempt.max(1) - 1) as usize)\n                        .unwrap_or(Duration::from_secs(1));\n\n                    let requeue_action = RequeueMessageAction {\n                        fetcher: self.clone(),\n                        receipt_handle,\n                        visibility_timeout: requeue_delay,\n                    };\n\n                    // Retry requeuing a few times.\n                    let retry = ExponentialBackoff::from_millis(100).factor(2).take(5);\n                    if let Err(err) = Retry::spawn(retry, requeue_action).await {\n                        log::error!(\n                            \"encore: internal error: failed to requeue aws pub/sub message: {}\",\n                            err\n                        );\n                    }\n                }\n            }\n        })\n    }\n}\n\nstruct RequeueMessageAction {\n    fetcher: Arc<SqsFetcher>,\n    receipt_handle: String,\n    visibility_timeout: Duration,\n}\n\nimpl Action for RequeueMessageAction {\n    type Future = Pin<Box<dyn Future<Output = Result<()>> + Send>>;\n    type Item = ();\n    type Error = anyhow::Error;\n\n    fn run(&mut self) -> Self::Future {\n        let fetcher = self.fetcher.clone();\n        let receipt_handle = self.receipt_handle.clone();\n        let visibility_timeout = self.visibility_timeout;\n\n        Box::pin(async move {\n            fetcher\n                .client\n                .change_message_visibility()\n                .queue_url(fetcher.queue_url.clone())\n                .receipt_handle(receipt_handle)\n                .visibility_timeout(visibility_timeout.as_secs() as i32)\n                .send()\n                .await\n                .map(|_| ())\n                .context(\"unable to requeue message\")\n        })\n    }\n}\n\nstruct DeleteMessageAction {\n    fetcher: Arc<SqsFetcher>,\n    receipt_handle: String,\n}\n\nimpl Action for DeleteMessageAction {\n    type Future = Pin<Box<dyn Future<Output = Result<()>> + Send>>;\n    type Item = ();\n    type Error = anyhow::Error;\n\n    fn run(&mut self) -> Self::Future {\n        let fetcher = self.fetcher.clone();\n        let receipt_handle = self.receipt_handle.clone();\n\n        Box::pin(async move {\n            fetcher\n                .client\n                .delete_message()\n                .queue_url(fetcher.queue_url.clone())\n                .receipt_handle(receipt_handle)\n                .send()\n                .await\n                .map(|_| ())\n                .context(\"unable to delete message\")\n        })\n    }\n}\n\nfn parse_attempt(message: &aws_sdk_sqs::types::Message) -> u32 {\n    message\n        .attributes\n        .as_ref()\n        .and_then(|attrs| attrs.get(&MessageSystemAttributeName::ApproximateReceiveCount))\n        .and_then(|s| s.parse::<u32>().ok())\n        .unwrap_or(1)\n}\n\nfn parse_message(message: aws_sdk_sqs::types::Message, attempt: u32) -> Result<pubsub::Message> {\n    // We currently have to clone the message data because we can't move it out of the\n    // ReceivedMessage as we need to call ack/nack afterwards.\n    let sns_message: SNSMessageWrapper =\n        serde_json::from_str(message.body.as_deref().unwrap_or_default())\n            .context(\"failed to decode SNS message body\")?;\n\n    let publish_time = chrono::DateTime::parse_from_rfc3339(&sns_message.timestamp)\n        .context(\"failed to parse publish timestamp\")?\n        .with_timezone(&chrono::Utc);\n\n    let attrs = sns_message\n        .message_attributes\n        .into_iter()\n        .filter_map(|(k, v)| {\n            if v.r#type == \"String\" {\n                Some((k, v.value))\n            } else {\n                None\n            }\n        })\n        .collect::<HashMap<_, _>>();\n\n    let raw_body = sns_message.message.as_bytes().to_vec();\n    Ok(pubsub::Message {\n        id: sns_message.message_id,\n        publish_time: Some(publish_time),\n        attempt,\n        data: pubsub::MessageData { attrs, raw_body },\n    })\n}\n\n/// SNSMessageWrapper matches the JSON that is sent to SQS from an SNS subscription\n#[derive(Debug, Deserialize)]\n#[allow(dead_code)]\nstruct SNSMessageWrapper {\n    #[serde(rename = \"Type\")]\n    r#type: String,\n    #[serde(rename = \"MessageId\")]\n    message_id: String,\n    #[serde(rename = \"TopicArn\")]\n    topic_arn: String,\n    #[serde(rename = \"Message\")]\n    message: String,\n    #[serde(rename = \"Timestamp\")]\n    timestamp: String,\n    #[serde(rename = \"SignatureVersion\", default)]\n    signature_version: String,\n    #[serde(rename = \"SigningCertURL\", default)]\n    signing_cert_url: String,\n    #[serde(rename = \"UnsubscribeURL\")]\n    unsubscribe_url: String,\n    #[serde(rename = \"MessageAttributes\", default)]\n    message_attributes: HashMap<String, MessageAttribute>,\n}\n\n#[derive(Debug, Deserialize)]\nstruct MessageAttribute {\n    #[serde(rename = \"Type\")]\n    r#type: String,\n    #[serde(rename = \"Value\")]\n    value: String,\n}\n"
  },
  {
    "path": "runtimes/core/src/pubsub/sqs_sns/topic.rs",
    "content": "use std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse anyhow::{Context, Result};\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::encore::runtime::v1::pub_sub_topic::DeliveryGuarantee;\nuse crate::names::CloudName;\nuse crate::pubsub::sqs_sns::LazyClient;\nuse crate::pubsub::{self, MessageData, MessageId};\n\n#[derive(Debug)]\npub struct Topic {\n    client: Arc<LazyClient>,\n    cloud_name: CloudName,\n    delivery_guarantee: DeliveryGuarantee,\n    publisher_id: xid::Id,\n}\n\nimpl Topic {\n    pub(super) fn new(\n        client: Arc<LazyClient>,\n        cfg: &pb::PubSubTopic,\n        publisher_id: xid::Id,\n    ) -> Self {\n        Self {\n            client,\n            cloud_name: cfg.cloud_name.clone().into(),\n            delivery_guarantee: cfg.delivery_guarantee(),\n            publisher_id,\n        }\n    }\n}\n\nimpl pubsub::Topic for Topic {\n    fn publish(\n        &self,\n        msg: MessageData,\n        ordering_key: Option<String>,\n    ) -> Pin<Box<dyn Future<Output = Result<MessageId>> + Send + '_>> {\n        Box::pin(async move {\n            // The raw body is JSON, so it's valid UTF8.\n            let data =\n                String::from_utf8(msg.raw_body).context(\"failed to serialize message body\")?;\n\n            let attrs: Result<HashMap<String, aws_sdk_sns::types::MessageAttributeValue>, _> = msg\n                .attrs\n                .into_iter()\n                .map(|(k, v)| {\n                    aws_sdk_sns::types::MessageAttributeValue::builder()\n                        .data_type(\"String\".to_string())\n                        .string_value(v)\n                        .build()\n                        .map(|val| (k, val))\n                })\n                .collect();\n            let attrs = attrs.context(\"failed to build message attributes\")?;\n\n            let client = self.client.get_sns().await;\n            let mut params = client\n                .publish()\n                .topic_arn(self.cloud_name.to_string())\n                .message(data);\n\n            if let Some(ordering_key) = ordering_key {\n                params = params.message_group_id(ordering_key);\n                params = params.message_deduplication_id(format!(\"msg_{}\", xid::new()));\n            } else if self.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {\n                params = params.message_group_id(format!(\"inst_{}\", self.publisher_id));\n                params = params.message_deduplication_id(format!(\"msg_{}\", xid::new()));\n            }\n\n            let result = params.set_message_attributes(Some(attrs)).send().await;\n\n            match result {\n                Ok(id) => Ok(id.message_id.unwrap_or(\"\".to_string()) as MessageId),\n                Err(e) => Err(e.into()),\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/runtime_config/mod.rs",
    "content": "use std::collections::HashMap;\n\nuse serde::Serialize;\n\nuse crate::encore::runtime::v1 as rt;\n\n#[derive(Debug, Clone, Serialize)]\npub struct Metric {\n    pub name: String,\n    pub services: Vec<String>,\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct RuntimeConfig {\n    pub metrics: HashMap<String, Metric>,\n}\n\nimpl RuntimeConfig {\n    pub fn new(rt: &rt::RuntimeConfig) -> Self {\n        let metrics = rt\n            .deployment\n            .as_ref()\n            .map(|d| {\n                d.metrics\n                    .iter()\n                    .map(|m| {\n                        (\n                            m.encore_name.clone(),\n                            Metric {\n                                name: m.encore_name.clone(),\n                                services: m.services.clone(),\n                            },\n                        )\n                    })\n                    .collect()\n            })\n            .unwrap_or_default();\n        Self { metrics }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/secrets/mod.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt::Display;\nuse std::sync::{Arc, OnceLock};\n\nuse std::io::Read as _;\n\nuse base64::{engine::general_purpose, Engine as _};\nuse flate2::read::GzDecoder;\n\nuse crate::encore::runtime::v1 as pb;\nuse encore::runtime::v1::secret_data::{Source, SubPath};\nuse encore::runtime::v1::SecretData;\n\nuse crate::encore;\nuse crate::encore::runtime::v1::secret_data::Encoding;\nuse crate::names::EncoreName;\n\npub struct Manager {\n    app_secrets: HashMap<EncoreName, Arc<Secret>>,\n}\n\nimpl Manager {\n    pub fn new(app_secrets: Vec<pb::AppSecret>) -> Self {\n        let app_secrets = app_secrets\n            .into_iter()\n            .filter_map(|s| match s.data {\n                Some(data) => Some((s.encore_name.into(), Arc::new(Secret::new(data)))),\n                None => None,\n            })\n            .collect();\n        Self { app_secrets }\n    }\n\n    pub fn load(&self, data: SecretData) -> Secret {\n        Secret::new(data)\n    }\n\n    /// Retrieve the secret for the given encore name.\n    /// If the secret is not found, returns None.\n    pub fn app_secret(&self, name: EncoreName) -> Option<Arc<Secret>> {\n        self.app_secrets.get(&name).cloned()\n    }\n}\n\npub struct Secret {\n    data: SecretData,\n    resolved: OnceLock<ResolveResult<Vec<u8>>>,\n}\n\nimpl Secret {\n    fn new(data: SecretData) -> Self {\n        Self {\n            data,\n            resolved: OnceLock::new(),\n        }\n    }\n\n    pub fn new_for_test(plaintext: &'static str) -> Self {\n        Self::new(SecretData {\n            source: Some(Source::Embedded(plaintext.as_bytes().to_vec())),\n            sub_path: None,\n            encoding: Encoding::None as i32,\n        })\n    }\n\n    pub fn get(&self) -> Result<&[u8], ResolveError> {\n        let result = self.resolved.get_or_init(|| resolve(&self.data)).as_deref();\n        match result {\n            Ok(bytes) => Ok(bytes),\n            Err(err) => Err(*err),\n        }\n    }\n}\n\nconst BASE64: general_purpose::GeneralPurpose = general_purpose::STANDARD;\n\n#[derive(Debug, Copy, Clone)]\npub enum ResolveError {\n    EnvVarNotFound,\n    JsonKeyNotFound,\n    JsonValueNotString,\n    InvalidBase64,\n    InvalidJSON,\n    InvalidJSONValue,\n    InvalidGzip,\n    InvalidSecretSource,\n    UnknownEncoding,\n}\n\nimpl std::error::Error for ResolveError {}\n\nimpl Display for ResolveError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ResolveError::EnvVarNotFound => write!(f, \"environment variable not found\"),\n            ResolveError::JsonKeyNotFound => write!(f, \"JSON key not found\"),\n            ResolveError::JsonValueNotString => write!(f, \"JSON value is not a string\"),\n            ResolveError::InvalidBase64 => write!(f, \"invalid base64\"),\n            ResolveError::InvalidGzip => write!(f, \"invalid gzip data\"),\n            ResolveError::InvalidJSON => write!(f, \"invalid JSON\"),\n            ResolveError::InvalidJSONValue => write!(f, \"invalid JSON value encoding\"),\n            ResolveError::InvalidSecretSource => write!(f, \"invalid secret source\"),\n            ResolveError::UnknownEncoding => write!(f, \"unknown encoding\"),\n        }\n    }\n}\n\ntype ResolveResult<T> = Result<T, ResolveError>;\n\nfn resolve(data: &SecretData) -> ResolveResult<Vec<u8>> {\n    let value = match &data.source {\n        Some(Source::Embedded(data)) => data.clone(),\n        Some(Source::Env(name)) => {\n            let value = std::env::var(name).map_err(|_| ResolveError::EnvVarNotFound)?;\n            value.into_bytes()\n        }\n        None => Err(ResolveError::InvalidSecretSource)?,\n    };\n\n    // Shall we decode this?\n    let encoding = Encoding::try_from(data.encoding).map_err(|_| ResolveError::UnknownEncoding)?;\n    let value = match encoding {\n        Encoding::None => value,\n        Encoding::Gzip => {\n            let compressed = BASE64\n                .decode(value)\n                .map_err(|_| ResolveError::InvalidBase64)?;\n            let mut decoder = GzDecoder::new(&compressed[..]);\n            let mut decompressed = Vec::new();\n            decoder\n                .read_to_end(&mut decompressed)\n                .map_err(|_| ResolveError::InvalidGzip)?;\n            decompressed\n        }\n        Encoding::Base64 => BASE64\n            .decode(&value)\n            .map_err(|_| ResolveError::InvalidBase64)?,\n    };\n\n    // Is there a subpath?\n    match &data.sub_path {\n        None => Ok(value),\n\n        Some(SubPath::JsonKey(json_key)) => {\n            // Escape the JSON key since we use gjson.\n            let json_key = escape_gjson_key(json_key);\n\n            let str_value = std::str::from_utf8(&value).map_err(|_| ResolveError::InvalidJSON)?;\n            let value = gjson::get(str_value, &json_key);\n            match value.kind() {\n                gjson::Kind::String => {\n                    // Use the string as-is.\n                    Ok(value.str().as_bytes().to_vec())\n                }\n\n                gjson::Kind::Object => {\n                    // Iterate over the keys to find the first \"bytes\" or \"string\" key.\n                    let mut result: Option<ResolveResult<Vec<u8>>> = None;\n                    let iter = |key: gjson::Value, value: gjson::Value| {\n                        match key.str() {\n                            \"bytes\" => {\n                                // Decode the bytes from base64.\n                                let res = BASE64\n                                    .decode(value.str())\n                                    .map_err(|_| ResolveError::InvalidBase64);\n                                result = Some(res);\n                            }\n                            \"string\" => {\n                                // Use the string as-is.\n                                result = Some(Ok(value.str().as_bytes().to_vec()));\n                            }\n                            _ => {}\n                        }\n                        result.is_some()\n                    };\n                    value.each(iter);\n                    result.unwrap_or(Err(ResolveError::InvalidJSONValue))\n                }\n\n                gjson::Kind::Null => Err(ResolveError::JsonKeyNotFound),\n                _ => Err(ResolveError::JsonValueNotString),\n            }\n        }\n    }\n}\n\nfn escape_gjson_key(key: &str) -> String {\n    fn is_safe_path_key_char(c: char) -> bool {\n        c.is_ascii_lowercase()\n            || c.is_ascii_uppercase()\n            || c.is_ascii_digit()\n            || c <= ' '\n            || c > '~'\n            || c == '_'\n            || c == '-'\n            || c == ':'\n    }\n\n    let mut escaped = String::with_capacity(key.len());\n    for c in key.chars() {\n        if is_safe_path_key_char(c) {\n            escaped.push(c);\n        } else {\n            escaped.push('\\\\');\n            escaped.push(c);\n        }\n    }\n    escaped\n}\n\n#[cfg(test)]\nmod tests {\n    use assert_matches::assert_matches;\n\n    #[test]\n    fn test_resolve() {\n        use super::*;\n        use encore::runtime::v1::{secret_data::Source, SecretData};\n\n        let secret = Secret::new(SecretData {\n            source: Some(Source::Embedded(b\"hello\".to_vec())),\n            sub_path: None,\n            encoding: Encoding::None as i32,\n        });\n        assert_eq!(secret.get().unwrap(), b\"hello\");\n\n        let secret = Secret::new(SecretData {\n            source: Some(Source::Embedded(b\"aGVsbG8=\".to_vec())),\n            sub_path: None,\n            encoding: Encoding::Base64 as i32,\n        });\n        assert_eq!(secret.get().unwrap(), b\"hello\");\n\n        let secret = Secret::new(SecretData {\n            source: Some(Source::Embedded(b\"aGVsbG8=\".to_vec())),\n            sub_path: None,\n            encoding: Encoding::None as i32,\n        });\n        assert_eq!(secret.get().unwrap(), b\"aGVsbG8=\");\n\n        {\n            let data = SecretData {\n                source: Some(Source::Env(\"TEST_SECRET\".to_string())),\n                sub_path: None,\n                encoding: Encoding::None as i32,\n            };\n            let secret = Secret::new(data.clone());\n            assert_matches!(secret.get(), Err(ResolveError::EnvVarNotFound));\n\n            std::env::set_var(\"TEST_SECRET\", \"hello\");\n            let secret = Secret::new(data);\n            assert_eq!(secret.get().unwrap(), b\"hello\");\n        }\n\n        // Test json_key.\n        {\n            let secret = Secret::new(SecretData {\n                source: Some(Source::Embedded(br#\"{\"foo\": \"hello\"}\"#.to_vec())),\n                sub_path: Some(SubPath::JsonKey(\"foo\".to_string())),\n                encoding: Encoding::None as i32,\n            });\n            assert_eq!(secret.get().unwrap(), b\"hello\");\n\n            let secret = Secret::new(SecretData {\n                source: Some(Source::Embedded(\n                    br#\"{\"foo\": {\"bytes\": \"aGVsbG8=\"}}\"#.to_vec(),\n                )),\n                sub_path: Some(SubPath::JsonKey(\"bar\".to_string())),\n                encoding: Encoding::None as i32,\n            });\n            assert_matches!(secret.get(), Err(ResolveError::JsonKeyNotFound));\n\n            let secret = Secret::new(SecretData {\n                source: Some(Source::Embedded(\n                    br#\"{\"foo\": {\"bytes\": \"aGVsbG8=\"}}\"#.to_vec(),\n                )),\n                sub_path: Some(SubPath::JsonKey(\"foo\".to_string())),\n                encoding: Encoding::None as i32,\n            });\n            assert_matches!(secret.get().unwrap(), b\"hello\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/sqldb/client.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt::Write;\nuse std::future::Future;\nuse std::pin::Pin;\n\nuse bb8::{ErrorSink, PooledConnection, RunError};\nuse bb8_postgres::PostgresConnectionManager;\nuse futures_util::StreamExt;\n\nuse tokio_postgres::types::BorrowToSql;\n\nuse crate::sqldb::val::RowValue;\nuse crate::trace::{protocol, Tracer};\nuse crate::{model, sqldb};\n\nuse super::transaction::Transaction;\n\ntype Mgr = PostgresConnectionManager<postgres_native_tls::MakeTlsConnector>;\n\npub struct Pool {\n    pool: bb8::Pool<Mgr>,\n    tracer: QueryTracer,\n}\n\nimpl Pool {\n    pub fn new<DB: sqldb::Database>(db: &DB, tracer: Tracer) -> anyhow::Result<Self> {\n        let tls = db.tls()?.clone();\n        let mgr = Mgr::new(db.config()?.clone(), tls);\n\n        let pool_cfg = db.pool_config()?;\n        let mut pool = bb8::Pool::builder()\n            .error_sink(Box::new(RustLoggerSink {\n                db_name: db.name().to_string(),\n            }))\n            .max_size(if pool_cfg.max_conns > 0 {\n                pool_cfg.max_conns\n            } else {\n                30\n            });\n\n        if pool_cfg.min_conns > 0 {\n            pool = pool.min_idle(Some(pool_cfg.min_conns));\n        }\n\n        let pool = pool.build_unchecked(mgr);\n        Ok(Self {\n            pool,\n            tracer: QueryTracer(tracer),\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\nstruct RustLoggerSink {\n    db_name: String,\n}\n\nimpl ErrorSink<tokio_postgres::Error> for RustLoggerSink {\n    fn sink(&self, err: tokio_postgres::Error) {\n        let mut msg = format!(\n            \"database {}: connection pool error: {:?}\",\n            self.db_name, err\n        );\n        let mut source = std::error::Error::source(&err);\n        while let Some(cause) = source {\n            let _ = write!(msg, \"\\n  caused by: {cause}\");\n            source = std::error::Error::source(cause);\n        }\n        log::error!(\"{msg}\");\n    }\n\n    fn boxed_clone(&self) -> Box<dyn ErrorSink<tokio_postgres::Error>> {\n        Box::new(self.clone())\n    }\n}\n\nimpl Pool {\n    pub async fn query_raw<P, I>(\n        &self,\n        query: &str,\n        params: I,\n        source: Option<&model::Request>,\n    ) -> Result<Cursor, Error>\n    where\n        P: BorrowToSql,\n        I: IntoIterator<Item = P>,\n        I::IntoIter: ExactSizeIterator,\n    {\n        self.tracer\n            .trace(source, query, || async {\n                let conn = self.pool.get().await.map_err(|e| match e {\n                    RunError::User(err) => Error::DB(err),\n                    RunError::TimedOut => Error::ConnectTimeout,\n                })?;\n                conn.query_raw(query, params).await.map_err(Error::from)\n            })\n            .await\n    }\n\n    pub async fn acquire(&self) -> Result<Connection, tokio_postgres::Error> {\n        let conn = self.pool.get_owned().await.map_err(|e| match e {\n            RunError::User(err) => err,\n            RunError::TimedOut => tokio_postgres::Error::__private_api_timeout(),\n        })?;\n        Ok(Connection {\n            conn: tokio::sync::RwLock::new(Some(conn)),\n            tracer: self.tracer.clone(),\n        })\n    }\n\n    pub async fn begin(&self, source: Option<&model::Request>) -> Result<Transaction, Error> {\n        let conn = self.pool.get_owned().await.map_err(|e| match e {\n            RunError::User(err) => err,\n            RunError::TimedOut => tokio_postgres::Error::__private_api_timeout(),\n        })?;\n        Transaction::begin(conn, self.tracer.clone(), source).await\n    }\n}\n\npub struct Cursor {\n    stream: Pin<Box<tokio_postgres::RowStream>>,\n}\n\nimpl Cursor {\n    pub async fn next(&mut self) -> Option<Result<Row, tokio_postgres::Error>> {\n        match self.stream.next().await {\n            Some(Ok(row)) => Some(Ok(Row { row })),\n            Some(Err(err)) => Some(Err(err)),\n            None => None,\n        }\n    }\n}\n\npub struct Row {\n    row: tokio_postgres::Row,\n}\n\nimpl Row {\n    pub fn values(&self) -> anyhow::Result<HashMap<String, RowValue>> {\n        let cols = self.row.columns();\n        let mut map = HashMap::with_capacity(cols.len());\n        for (i, col) in cols.iter().enumerate() {\n            let name = col.name().to_string();\n            let value: RowValue = self\n                .row\n                .try_get(i)\n                .map_err(|e| anyhow::anyhow!(\"unable to parse column {}: {:#?}\", name, e))?;\n            map.insert(name, value);\n        }\n        Ok(map)\n    }\n}\n\npub(crate) type PooledConn =\n    PooledConnection<'static, PostgresConnectionManager<postgres_native_tls::MakeTlsConnector>>;\n\npub struct Connection {\n    conn: tokio::sync::RwLock<Option<PooledConn>>,\n    tracer: QueryTracer,\n}\n\nimpl Connection {\n    pub async fn close(&self) {\n        let mut guard = self.conn.write().await;\n        if let Some(conn) = guard.take() {\n            drop(conn);\n        }\n    }\n\n    pub async fn query_raw<P, I>(\n        &self,\n        query: &str,\n        params: I,\n        source: Option<&model::Request>,\n    ) -> Result<Cursor, Error>\n    where\n        P: BorrowToSql,\n        I: IntoIterator<Item = P>,\n        I::IntoIter: ExactSizeIterator,\n    {\n        self.tracer\n            .trace(source, query, || async {\n                let guard = self.conn.read().await;\n                let Some(conn) = guard.as_ref() else {\n                    return Err(Error::Closed);\n                };\n                conn.query_raw(query, params).await.map_err(Error::from)\n            })\n            .await\n    }\n}\n\n#[derive(Debug)]\npub enum Error {\n    DB(tokio_postgres::Error),\n    Closed,\n    ConnectTimeout,\n}\n\nimpl std::fmt::Display for Error {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Error::DB(err) => <tokio_postgres::Error as std::fmt::Display>::fmt(err, f),\n            Error::Closed => f.write_str(\"connection_closed\"),\n            Error::ConnectTimeout => f.write_str(\"timeout establishing connection\"),\n        }\n    }\n}\n\nimpl From<tokio_postgres::Error> for Error {\n    fn from(err: tokio_postgres::Error) -> Self {\n        Error::DB(err)\n    }\n}\n\n#[derive(Debug, Clone)]\npub(crate) struct QueryTracer(Tracer);\n\nimpl QueryTracer {\n    pub(crate) async fn trace<F, Fut>(\n        &self,\n        source: Option<&model::Request>,\n        query: &str,\n        exec: F,\n    ) -> Result<Cursor, Error>\n    where\n        F: FnOnce() -> Fut,\n        Fut: Future<Output = Result<tokio_postgres::RowStream, Error>>,\n    {\n        let start_id = source.and_then(|source| {\n            self.0\n                .db_query_start(protocol::DBQueryStartData { source, query })\n        });\n\n        let result = exec().await;\n\n        if let Some(source) = source {\n            self.0.db_query_end(protocol::DBQueryEndData {\n                start_id,\n                source,\n                error: result.as_ref().err(),\n            });\n        }\n\n        let stream = result?;\n        Ok(Cursor {\n            stream: Box::pin(stream),\n        })\n    }\n\n    pub(crate) async fn trace_batch_execute<F, Fut>(\n        &self,\n        source: Option<&model::Request>,\n        query: &str,\n        exec: F,\n    ) -> Result<(), Error>\n    where\n        F: FnOnce() -> Fut,\n        Fut: Future<Output = Result<(), Error>>,\n    {\n        let start_id = source.and_then(|source| {\n            self.0\n                .db_query_start(protocol::DBQueryStartData { source, query })\n        });\n\n        let result = exec().await;\n\n        if let Some(source) = source {\n            self.0.db_query_end(protocol::DBQueryEndData {\n                start_id,\n                source,\n                error: result.as_ref().err(),\n            });\n        }\n\n        result\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/sqldb/manager.rs",
    "content": "use anyhow::Context;\nuse std::borrow::Cow;\nuse std::collections::HashMap;\nuse std::sync::{Arc, Mutex};\nuse tokio_postgres::proxy;\n\nuse tokio_postgres::proxy::{AcceptConn, AuthMethod, ClientBouncer, RejectConn};\n\nuse crate::encore::runtime::v1 as pb;\nuse crate::names::EncoreName;\nuse crate::secrets;\nuse crate::sqldb::Pool;\nuse crate::trace::Tracer;\n\npub struct Manager {\n    databases: Arc<HashMap<EncoreName, Arc<DatabaseImpl>>>,\n    proxy_port: u16,\n    listener: Mutex<Option<std::net::TcpListener>>,\n    runtime: tokio::runtime::Handle,\n}\n\npub struct ManagerConfig<'a> {\n    pub clusters: Vec<pb::SqlCluster>,\n    pub creds: &'a pb::infrastructure::Credentials,\n    pub secrets: &'a secrets::Manager,\n    pub tracer: Tracer,\n    pub runtime: tokio::runtime::Handle,\n}\n\nimpl ManagerConfig<'_> {\n    pub fn build(self) -> anyhow::Result<Manager> {\n        // Start listening so we can tell which port it is when we generate configuration.\n        let listener =\n            std::net::TcpListener::bind(\"127.0.0.1:0\").context(\"unable to bind to sqldb port\")?;\n        let proxy_port = listener\n            .local_addr()\n            .context(\"unable to get local address\")?\n            .port();\n\n        let databases = databases_from_cfg(\n            self.clusters,\n            self.creds,\n            self.secrets,\n            proxy_port,\n            self.tracer,\n        )\n        .context(\"failed to parse SQL clusters\")?;\n        let databases = Arc::new(databases);\n\n        Ok(Manager {\n            databases,\n            proxy_port,\n            runtime: self.runtime,\n            listener: Mutex::new(Some(listener)),\n        })\n    }\n}\n\nimpl Manager {\n    pub fn database(&self, name: &EncoreName) -> Arc<dyn Database> {\n        match self.databases.get(name) {\n            Some(db) => db.clone(),\n            None => {\n                let proxy_conn_string = proxy_conn_string(name, self.proxy_port);\n                Arc::new(NoopDatabase {\n                    name: name.clone(),\n                    proxy_conn_string,\n                })\n            }\n        }\n    }\n\n    pub fn start_serving(&self) -> tokio::task::JoinHandle<anyhow::Result<()>> {\n        let manager = proxy::ProxyManager::new(Bouncer {\n            databases: self.databases.clone(),\n        });\n\n        let listener = self.listener.lock().unwrap().take();\n        let runtime = self.runtime.clone();\n        self.runtime.spawn(async move {\n            let listener = listener.context(\"sqldb server already started\")?;\n            listener\n                .set_nonblocking(true)\n                .context(\"unable to set nonblocking\")?;\n            let listener = tokio::net::TcpListener::from_std(listener)\n                .context(\"unable to convert listener to tokio\")?;\n\n            let addr = listener\n                .local_addr()\n                .map(|a| a.to_string())\n                .unwrap_or(\"unknown\".to_string());\n\n            log::debug!(addr=addr; \"encore runtime database proxy listening for incoming requests\");\n\n            loop {\n                let (stream, _) = listener.accept().await?;\n                let mgr = manager.clone();\n                runtime.spawn(mgr.handle_conn(stream));\n            }\n        })\n    }\n}\n\npub trait Database: Send + Sync {\n    // The name of the database.\n    fn name(&self) -> &EncoreName;\n\n    fn pool_config(&self) -> anyhow::Result<PoolConfig>;\n    fn config(&self) -> anyhow::Result<&tokio_postgres::Config>;\n    fn tls(&self) -> anyhow::Result<&postgres_native_tls::MakeTlsConnector>;\n    fn new_pool(&self) -> anyhow::Result<Pool>;\n\n    /// Returns the connection string for connecting to this database via the proxy.\n    fn proxy_conn_string(&self) -> &str;\n}\n\n/// Represents a SQL Database available to the runtime.\npub struct DatabaseImpl {\n    name: EncoreName,\n    config: Arc<tokio_postgres::Config>,\n    tls: postgres_native_tls::MakeTlsConnector,\n    proxy_conn_string: String,\n    tracer: Tracer,\n\n    min_conns: u32,\n    max_conns: u32,\n}\n\n#[derive(Debug, Clone)]\npub struct PoolConfig {\n    pub min_conns: u32,\n    pub max_conns: u32,\n}\n\nimpl Database for DatabaseImpl {\n    fn name(&self) -> &EncoreName {\n        &self.name\n    }\n\n    fn pool_config(&self) -> anyhow::Result<PoolConfig> {\n        Ok(PoolConfig {\n            min_conns: self.min_conns,\n            max_conns: self.max_conns,\n        })\n    }\n\n    fn config(&self) -> anyhow::Result<&tokio_postgres::Config> {\n        Ok(&self.config)\n    }\n\n    fn tls(&self) -> anyhow::Result<&postgres_native_tls::MakeTlsConnector> {\n        Ok(&self.tls)\n    }\n\n    fn new_pool(&self) -> anyhow::Result<Pool> {\n        Pool::new(self, self.tracer.clone())\n    }\n\n    fn proxy_conn_string(&self) -> &str {\n        &self.proxy_conn_string\n    }\n}\n\nstruct NoopDatabase {\n    name: EncoreName,\n    proxy_conn_string: String,\n}\n\nimpl Database for NoopDatabase {\n    fn name(&self) -> &EncoreName {\n        &self.name\n    }\n\n    fn pool_config(&self) -> anyhow::Result<PoolConfig> {\n        anyhow::bail!(\"this database is not configured for use by this process\")\n    }\n\n    fn config(&self) -> anyhow::Result<&tokio_postgres::Config> {\n        anyhow::bail!(\"this database is not configured for use by this process\")\n    }\n\n    fn tls(&self) -> anyhow::Result<&postgres_native_tls::MakeTlsConnector> {\n        anyhow::bail!(\"this database is not configured for use by this process\")\n    }\n\n    fn new_pool(&self) -> anyhow::Result<Pool> {\n        anyhow::bail!(\"this database is not configured for use by this process\")\n    }\n\n    fn proxy_conn_string(&self) -> &str {\n        // We need to return a valid connection string here,\n        // as this is typically called during initialization.\n        // The proxy will reject any connections to the database.\n        &self.proxy_conn_string\n    }\n}\n\n#[derive(Clone)]\nstruct Bouncer {\n    databases: Arc<HashMap<EncoreName, Arc<DatabaseImpl>>>,\n}\n\nimpl ClientBouncer for Bouncer {\n    // TODO support TLS\n    type Tls = postgres_native_tls::MakeTlsConnector;\n    type Future = futures::future::Ready<Result<AcceptConn<Self::Tls>, RejectConn>>;\n\n    fn handle_startup(\n        &self,\n        info: &postgres_protocol::message::startup::StartupData,\n    ) -> Self::Future {\n        let resolve = move || {\n            let db_name = info\n                .parameters\n                .get(\"database\")\n                .ok_or(RejectConn::UnknownDatabase)?;\n            let db_name =\n                String::from_utf8(db_name.to_vec()).map_err(|_| RejectConn::UnknownDatabase)?;\n            let db = self\n                .databases\n                .get(&db_name)\n                .ok_or(RejectConn::UnknownDatabase)?;\n\n            Ok(AcceptConn {\n                auth_method: AuthMethod::Trust,\n                tls: db.tls.clone(),\n                backend_config: db.config.clone(),\n            })\n        };\n        futures::future::ready(resolve())\n    }\n}\n\n/// Returns the connection string for connecting to the database via the proxy.\nfn proxy_conn_string(db_encore_name: &str, proxy_port: u16) -> String {\n    format!(\"postgresql://encore:password@127.0.0.1:{proxy_port}/{db_encore_name}?sslmode=disable\",)\n}\n\n/// Computes the database configuration for the given clusters.\nfn databases_from_cfg(\n    clusters: Vec<pb::SqlCluster>,\n    creds: &pb::infrastructure::Credentials,\n    secrets: &secrets::Manager,\n    proxy_port: u16,\n    tracer: Tracer,\n) -> anyhow::Result<HashMap<EncoreName, Arc<DatabaseImpl>>> {\n    let mut databases = HashMap::new();\n    for c in clusters {\n        // Get the primary server.\n        let server = c\n            .servers\n            .into_iter()\n            .find(|s| s.kind() == pb::ServerKind::Primary);\n        let Some(server) = server else {\n            log::warn!(\"no primary server found for cluster {}, skipping\", c.rid);\n            continue;\n        };\n\n        for db in c.databases {\n            // Get the read-write pool for this db.\n            let pool = db.conn_pools.into_iter().find(|p| !p.is_readonly);\n            let Some(pool) = pool else {\n                log::warn!(\n                    \"no read-write pool found for database {}, skipping\",\n                    db.encore_name\n                );\n                continue;\n            };\n\n            // Get the role to authenticate with.\n            let role = creds\n                .sql_roles\n                .iter()\n                .find(|r| r.rid == pool.role_rid)\n                .with_context(|| {\n                    format!(\n                        \"no role found with rid {} for database {}\",\n                        pool.role_rid, db.encore_name\n                    )\n                })?;\n\n            let mut config = tokio_postgres::Config::new();\n\n            // Add host/port configuration\n            if server.host.starts_with('/') {\n                // Unix socket\n                config.host(&server.host);\n            } else if let Some((host, port)) = server.host.split_once(':') {\n                config.host(host);\n                config.port(port.parse::<u16>().context(\"invalid port\")?);\n            } else {\n                config.host(&server.host);\n                config.port(5432);\n            }\n\n            config.user(&role.username);\n            if let Some(password) = &role.password {\n                let sec = secrets.load(password.clone());\n                let password = sec.get().context(\"failed to resolve password\")?;\n                config.password(password);\n            }\n\n            config.dbname(&db.cloud_name);\n            config.application_name(\"encore\");\n\n            let mut tls_builder = native_tls::TlsConnector::builder();\n            if let Some(tls_config) = &server.tls_config {\n                if let Some(server_ca_cert) = &tls_config.server_ca_cert {\n                    let cert = native_tls::Certificate::from_pem(server_ca_cert.as_bytes())\n                        .context(\"unable to parse server ca certificate\")?;\n                    tls_builder.add_root_certificate(cert);\n                    config.ssl_mode(tokio_postgres::config::SslMode::Require);\n                } else {\n                    config.ssl_mode(tokio_postgres::config::SslMode::Prefer);\n                }\n\n                if tls_config.disable_tls_hostname_verification {\n                    tls_builder.danger_accept_invalid_hostnames(true);\n                }\n                if tls_config.disable_ca_validation {\n                    tls_builder.danger_accept_invalid_certs(true);\n                }\n            } else {\n                config.ssl_mode(tokio_postgres::config::SslMode::Disable);\n            }\n\n            if let Some(client_cert_rid) = &role.client_cert_rid {\n                // Add a client certificate.\n                let client_cert = creds\n                    .client_certs\n                    .iter()\n                    .find(|c| c.rid == *client_cert_rid)\n                    .with_context(|| {\n                        format!(\n                            \"no client certificate found with rid {} for database {}\",\n                            client_cert_rid, db.encore_name\n                        )\n                    })?;\n\n                // Parse the client key secret.\n                let client_key = client_cert\n                    .key\n                    .as_ref()\n                    .context(\"client certificate has no key\")?;\n                let client_key = secrets.load(client_key.clone());\n                let client_key = client_key.get().context(\"failed to resolve client key\")?;\n\n                let client_key = convert_client_key_if_necessary(client_key)\n                    .context(\"failed to convert client key to PKCS#8\")?;\n                let identity = native_tls::Identity::from_pkcs8(\n                    client_cert.cert.as_bytes(),\n                    client_key.as_ref(),\n                )\n                .context(\"failed to parse client certificate\")?;\n                tls_builder.identity(identity);\n            }\n\n            let tls = tls_builder\n                .build()\n                .context(\"failed to build TLS connector\")?;\n            let tls = postgres_native_tls::MakeTlsConnector::new(tls);\n\n            let proxy_conn_string = proxy_conn_string(&db.encore_name, proxy_port);\n\n            let name: EncoreName = db.encore_name.into();\n            databases.insert(\n                name.clone(),\n                Arc::new(DatabaseImpl {\n                    name,\n                    config: Arc::new(config),\n                    tls,\n                    proxy_conn_string,\n                    tracer: tracer.clone(),\n\n                    min_conns: pool.min_connections as u32,\n                    max_conns: pool.max_connections as u32,\n                }),\n            );\n        }\n    }\n\n    Ok(databases)\n}\n\n/// Converts the client key from PKCS#1 to PKCS#8 if necessary.\nfn convert_client_key_if_necessary(pem: &[u8]) -> anyhow::Result<Cow<'_, [u8]>> {\n    let Ok(pem_str) = std::str::from_utf8(pem) else {\n        // Assume the key is already in PKCS#8 format.\n        return Ok(Cow::Borrowed(pem));\n    };\n    if !pem_str.starts_with(\"-----BEGIN RSA PRIVATE KEY-----\") {\n        // Key is not in PKCS#1 format, assume it's already in PKCS#8 format.\n        return Ok(Cow::Borrowed(pem));\n    }\n\n    use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::EncodePrivateKey};\n\n    let pkey = rsa::RsaPrivateKey::from_pkcs1_pem(pem_str)\n        .context(\"failed to parse PKCS#1 private key\")?;\n    let pkcs8 = pkey\n        .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)\n        .context(\"failed to convert PKCS#1 private key to PKCS#8\")?;\n    Ok(Cow::Owned(pkcs8.as_bytes().to_owned()))\n}\n"
  },
  {
    "path": "runtimes/core/src/sqldb/mod.rs",
    "content": "mod client;\nmod manager;\npub mod numeric;\nmod transaction;\nmod val;\n\npub use client::{Connection, Cursor, Pool, Row};\npub use manager::{Database, DatabaseImpl, Manager, ManagerConfig};\npub use transaction::Transaction;\npub use val::RowValue;\n"
  },
  {
    "path": "runtimes/core/src/sqldb/numeric.rs",
    "content": "//! Conversions to and from Postgres's binary format for the numeric type.\nuse byteorder::{BigEndian, ReadBytesExt};\nuse bytes::{BufMut, BytesMut};\nuse std::boxed::Box as StdBox;\nuse std::error::Error;\nuse std::str::{self, FromStr};\n\n/// Serializes a `NUMERIC` value.\n#[inline]\npub fn numeric_to_sql(v: Numeric, buf: &mut BytesMut) {\n    let num_digits = v.digits.len() as u16;\n    buf.put_u16(num_digits);\n    buf.put_i16(v.weight);\n    buf.put_u16(v.sign.into_u16());\n    buf.put_u16(v.scale);\n\n    for digit in v.digits {\n        buf.put_i16(digit);\n    }\n}\n\n/// Deserializes a `NUMERIC` value.\n#[inline]\npub fn numeric_from_sql(mut buf: &[u8]) -> Result<Numeric, StdBox<dyn Error + Sync + Send>> {\n    let num_digits = buf.read_u16::<BigEndian>()?;\n    let mut digits = Vec::with_capacity(num_digits.into());\n\n    let weight = buf.read_i16::<BigEndian>()?;\n    let sign = NumericSign::try_from_u16(buf.read_u16::<BigEndian>()?)?;\n\n    let scale = buf.read_u16::<BigEndian>()?;\n\n    for _ in 0..num_digits {\n        digits.push(buf.read_i16::<BigEndian>()?);\n    }\n\n    Ok(Numeric {\n        sign,\n        scale,\n        weight,\n        digits,\n    })\n}\n\n/// Numeric sign\n#[derive(Debug, Copy, Clone, PartialEq, Eq)]\npub enum NumericSign {\n    /// Positive number\n    Positive,\n    /// Negative number\n    Negative,\n    /// Not a number\n    NaN,\n    /// Positive infinity\n    PositiveInfinity,\n    /// Negative infinity\n    NegativeInfinity,\n}\n\nimpl NumericSign {\n    #[inline]\n    fn try_from_u16(sign: u16) -> Result<NumericSign, StdBox<dyn Error + Sync + Send>> {\n        match sign {\n            0x0000 => Ok(NumericSign::Positive),\n            0x4000 => Ok(NumericSign::Negative),\n            0xC000 => Ok(NumericSign::NaN),\n            0xD000 => Ok(NumericSign::PositiveInfinity),\n            0xF000 => Ok(NumericSign::NegativeInfinity),\n            _ => Err(\"invalid sign in numeric value\".into()),\n        }\n    }\n\n    #[inline]\n    fn into_u16(self) -> u16 {\n        match self {\n            NumericSign::Positive => 0x0000,\n            NumericSign::Negative => 0x4000,\n            NumericSign::NaN => 0xC000,\n            NumericSign::PositiveInfinity => 0xD000,\n            NumericSign::NegativeInfinity => 0xF000,\n        }\n    }\n}\n\n/// A Posgres numeric\n#[derive(Debug, PartialEq, Eq)]\npub struct Numeric {\n    sign: NumericSign,\n    scale: u16,\n    weight: i16,\n    digits: Vec<i16>,\n}\n\nimpl Numeric {\n    /// Returns the weight of the numeric value.\n    #[inline]\n    pub fn weight(&self) -> i16 {\n        self.weight\n    }\n\n    /// Returns the scale of the numeric value.\n    #[inline]\n    pub fn scale(&self) -> u16 {\n        self.scale\n    }\n\n    fn nan() -> Self {\n        Self {\n            sign: NumericSign::NaN,\n            scale: 0,\n            weight: 0,\n            digits: vec![],\n        }\n    }\n\n    fn infinity() -> Self {\n        Self {\n            sign: NumericSign::PositiveInfinity,\n            scale: 0,\n            weight: 0,\n            digits: vec![],\n        }\n    }\n\n    fn negative_infinity() -> Self {\n        Self {\n            sign: NumericSign::NegativeInfinity,\n            scale: 0,\n            weight: 0,\n            digits: vec![],\n        }\n    }\n}\n\nimpl std::fmt::Display for Numeric {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self.sign {\n            NumericSign::NaN => write!(f, \"NaN\"),\n            NumericSign::PositiveInfinity => write!(f, \"Infinity\"),\n            NumericSign::NegativeInfinity => write!(f, \"-Infinity\"),\n            NumericSign::Negative => {\n                write!(f, \"-\")?;\n                format_positive_number(self, f)\n            }\n            NumericSign::Positive => format_positive_number(self, f),\n        }\n    }\n}\n\nimpl FromStr for Numeric {\n    type Err = ParseError;\n\n    fn from_str(value: &str) -> Result<Self, Self::Err> {\n        if let Some(special) = parse_special_values(value)? {\n            return Ok(special);\n        }\n\n        let components = parse_number_components(value)?;\n\n        if components.exponent == 0 {\n            parse_simple_number(&components)\n        } else {\n            parse_scientific_number(&components)\n        }\n    }\n}\n\n/// Parse a simple number without scientific notation (fast path)\nfn parse_simple_number(components: &ParsedComponents<'_>) -> Result<Numeric, ParseError> {\n    let mut digits = Vec::new();\n    let mut weight;\n    let scale = components.decimal_part.len() as u16;\n\n    let integer_digits = build_integer_digits_from_bytes(components.integer_part)?;\n    let has_integer = !integer_digits.is_empty();\n\n    if has_integer {\n        // Calculate weight from non-zero integer digits\n        let non_zero_start = components\n            .integer_part\n            .iter()\n            .position(|&b| b != b'0')\n            .unwrap_or(0);\n        let significant_len = components.integer_part.len() - non_zero_start;\n        weight = ((significant_len - 1) / 4) as i16;\n        digits.extend(integer_digits);\n    } else {\n        weight = if !components.decimal_part.is_empty() {\n            -1\n        } else {\n            0\n        };\n    }\n\n    if !components.decimal_part.is_empty() {\n        if has_integer {\n            let decimal_digits = build_decimal_digits_from_bytes(components.decimal_part)?;\n            digits.extend(decimal_digits);\n        } else {\n            process_decimal_digits(components.decimal_part, &mut digits, &mut weight)?;\n        }\n    }\n\n    // Normalize zeros\n    normalize_leading_zeros(&mut digits, &mut weight);\n    normalize_trailing_zeros(&mut digits);\n\n    Ok(Numeric {\n        sign: components.sign,\n        scale,\n        weight,\n        digits,\n    })\n}\n\n/// Parse a number with scientific notation\nfn parse_scientific_number(components: &ParsedComponents<'_>) -> Result<Numeric, ParseError> {\n    let layout = ScientificLayout::new(\n        components.integer_part,\n        components.decimal_part,\n        components.exponent,\n    );\n\n    // Get digit vectors from the scientific layout\n    let effective_integer = layout.integer_digits();\n    let effective_decimal = layout.decimal_digits();\n\n    let mut digits = Vec::new();\n    let mut weight;\n\n    // Calculate scale based on decimal digits from the layout\n    let effective_scale = effective_decimal.len() as u16;\n\n    let scale = components.calculate_scale(effective_scale);\n\n    // Process integer and decimal parts\n    let integer_digits = build_integer_digits_from_bytes(&effective_integer)?;\n    let has_integer = !integer_digits.is_empty();\n\n    if has_integer {\n        // Calculate weight from non-zero integer digits\n        let non_zero_start = effective_integer\n            .iter()\n            .position(|&b| b != b'0')\n            .unwrap_or(0);\n        let significant_len = effective_integer.len() - non_zero_start;\n        weight = ((significant_len - 1) / 4) as i16;\n        digits.extend(integer_digits);\n    } else {\n        weight = if !effective_decimal.is_empty() { -1 } else { 0 };\n    }\n\n    // Process decimal part\n    if !effective_decimal.is_empty() {\n        if has_integer {\n            // Mixed number - just add decimal digits\n            let decimal_digits = build_decimal_digits_from_bytes(&effective_decimal)?;\n            digits.extend(decimal_digits);\n        } else {\n            process_decimal_digits(&effective_decimal, &mut digits, &mut weight)?;\n        }\n    }\n\n    // Normalize zeros\n    normalize_leading_zeros(&mut digits, &mut weight);\n    normalize_trailing_zeros(&mut digits);\n\n    Ok(Numeric {\n        sign: components.sign,\n        scale,\n        weight,\n        digits,\n    })\n}\n\nfn format_positive_number(n: &Numeric, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    format_integer_part(n, f)?;\n    format_decimal_part(n, f)?;\n    Ok(())\n}\n\nfn format_integer_part(n: &Numeric, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    if n.weight() < 0 {\n        write!(f, \"0\")?;\n        return Ok(());\n    }\n\n    for i in 0..=n.weight() {\n        let d = n.digits.get(i as usize).unwrap_or(&0);\n        if i == 0 {\n            write!(f, \"{d}\")?;\n        } else {\n            write!(f, \"{d:04}\")?;\n        }\n    }\n\n    Ok(())\n}\n\nfn format_decimal_part(n: &Numeric, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    if n.scale() == 0 {\n        return Ok(());\n    }\n\n    write!(f, \".\")?;\n\n    let mut remaining_decimals = n.scale();\n\n    let mut curr_weight = -1;\n    while curr_weight > n.weight() {\n        write!(f, \"0000\")?;\n        curr_weight -= 1;\n        remaining_decimals = remaining_decimals.saturating_sub(4);\n    }\n\n    if remaining_decimals == 0 {\n        return Ok(());\n    }\n\n    let decimal_idx = if n.weight() < 0 { 0 } else { n.weight() + 1 } as usize;\n    for i in decimal_idx..n.digits.len() {\n        let d = n.digits.get(i).unwrap_or(&0);\n        if remaining_decimals >= 4 {\n            write!(f, \"{d:04}\")?;\n            remaining_decimals -= 4;\n        } else {\n            let truncated = d / 10_i16.pow(4 - remaining_decimals as u32);\n            write!(\n                f,\n                \"{:0width$}\",\n                truncated,\n                width = remaining_decimals as usize\n            )?;\n\n            remaining_decimals = 0;\n            break;\n        }\n    }\n\n    if remaining_decimals > 0 {\n        write!(f, \"{:0width$}\", 0, width = remaining_decimals as usize)?;\n    }\n\n    Ok(())\n}\n\ntype ParseError = StdBox<dyn Error + Sync + Send>;\n\n/// Components of a parsed numeric string\nstruct ParsedComponents<'a> {\n    sign: NumericSign,\n    integer_part: &'a [u8],\n    decimal_part: &'a [u8],\n    exponent: i32,\n}\n\nimpl<'a> ParsedComponents<'a> {\n    /// Calculate the scale for a numeric value, handling scientific notation\n    fn calculate_scale(&self, effective_scale: u16) -> u16 {\n        // Check if the original number is zero\n        let is_zero = self.integer_part.iter().all(|&b| b == b'0')\n            && (self.decimal_part.is_empty() || self.decimal_part.iter().all(|&b| b == b'0'));\n        if is_zero {\n            if self.exponent < 0 {\n                // For \"0e-10\", preserve the scale implied by the exponent\n                (-self.exponent as u16).max(self.decimal_part.len() as u16)\n            } else {\n                // For \"0.0e5\", preserve the original decimal scale\n                self.decimal_part.len() as u16\n            }\n        } else if self.exponent < 0 {\n            // For negative exponents, calculate implied decimal places\n            let implied = self.decimal_part.len() as i32 + (-self.exponent);\n            implied.max(0).min(u16::MAX as i32) as u16\n        } else {\n            // For positive exponents, use the effective scale\n            effective_scale\n        }\n    }\n}\n\nfn parse_special_values(s: &str) -> Result<Option<Numeric>, ParseError> {\n    if s.eq_ignore_ascii_case(\"NaN\") {\n        return Ok(Some(Numeric::nan()));\n    }\n    if s.eq_ignore_ascii_case(\"Infinity\") || s.eq_ignore_ascii_case(\"Inf\") {\n        return Ok(Some(Numeric::infinity()));\n    }\n    if s.eq_ignore_ascii_case(\"-Infinity\") || s.eq_ignore_ascii_case(\"-Inf\") {\n        return Ok(Some(Numeric::negative_infinity()));\n    }\n    Ok(None)\n}\n\nfn parse_scientific_component(s: &[u8]) -> Result<(&[u8], Option<i32>), ParseError> {\n    let (s, e) = split_e(s);\n\n    let exp = if let Some(mut e) = e {\n        if e.is_empty() {\n            return Err(\"empty scientific notation string\".into());\n        }\n\n        let mut positive = true;\n        let mut exp: i32 = 0;\n\n        if let Some(&b'-') = e.first() {\n            positive = false;\n            e = &e[1..];\n        } else if let Some(&b'+') = e.first() {\n            e = &e[1..];\n        }\n\n        if e.is_empty() {\n            return Err(\"empty scientific notation exponent\".into());\n        }\n\n        for &b in e {\n            if !b.is_ascii_digit() {\n                return Err(\"scientific notation string contain non-digit character\".into());\n            }\n            // Prevent integer overflow in exponent\n            exp = exp.saturating_mul(10).saturating_add((b - b'0') as i32);\n            if exp > 1000000 {\n                // Reasonable limit for exponents\n                return Err(\"scientific notation exponent too large\".into());\n            }\n        }\n\n        Some(if positive { exp } else { -exp })\n    } else {\n        None\n    };\n\n    Ok((s, exp))\n}\n\nfn parse_number_components(s: &str) -> Result<ParsedComponents<'_>, ParseError> {\n    let mut sign = NumericSign::Positive;\n\n    let mut s = s.as_bytes();\n    if let Some(&b'-') = s.first() {\n        sign = NumericSign::Negative;\n        s = &s[1..];\n    };\n\n    // Check for completely empty input after removing sign\n    if s.is_empty() {\n        return Err(\"empty numeric string\".into());\n    }\n\n    let (s, exp) = parse_scientific_component(s)?;\n    let (integer_part, decimal_part) = split_decimal(s);\n    let decimal_part = decimal_part.unwrap_or_default();\n\n    if integer_part.is_empty() && decimal_part.is_empty() {\n        return Err(\"invalid numeric string\".into());\n    }\n\n    Ok(ParsedComponents {\n        sign,\n        integer_part,\n        decimal_part,\n        exponent: exp.unwrap_or(0),\n    })\n}\n\n/// Represents the layout of digits after applying scientific notation transformation,\n/// calculating positions and zero-padding.\n#[derive(Debug)]\nstruct ScientificLayout<'a> {\n    integer_part: &'a [u8],\n    decimal_part: &'a [u8],\n    decimal_point_pos: i32,\n    leading_zeros: usize,\n    trailing_zeros: usize,\n}\n\nimpl<'a> ScientificLayout<'a> {\n    fn new(integer: &'a [u8], decimal: &'a [u8], exponent: i32) -> Self {\n        let decimal_point_pos = integer.len() as i32 + exponent;\n\n        let total_original_digits = integer.len() + decimal.len();\n\n        let (leading_zeros, trailing_zeros) = if exponent > 0 {\n            let total_needed = decimal_point_pos as usize;\n            let trailing_zeros = total_needed.saturating_sub(total_original_digits);\n            (0, trailing_zeros)\n        } else if decimal_point_pos <= 0 {\n            let leading_zeros = (-decimal_point_pos) as usize;\n            (leading_zeros, 0)\n        } else {\n            (0, 0)\n        };\n\n        Self {\n            integer_part: integer,\n            decimal_part: decimal,\n            decimal_point_pos,\n            leading_zeros,\n            trailing_zeros,\n        }\n    }\n\n    /// Returns the effective integer digits\n    fn integer_digits(&self) -> Vec<u8> {\n        if self.decimal_point_pos <= 0 {\n            return vec![];\n        }\n\n        let split_pos = (self.decimal_point_pos as usize)\n            .min(self.integer_part.len() + self.decimal_part.len());\n\n        let total_len = split_pos + self.trailing_zeros;\n        let mut result = Vec::with_capacity(total_len);\n\n        result.extend_from_slice(self.integer_part);\n\n        // Add from decimal part if needed\n        if split_pos > self.integer_part.len() {\n            let decimal_count = split_pos - self.integer_part.len();\n            result.extend(self.decimal_part.iter().take(decimal_count));\n        }\n\n        // Add trailing zeros\n        result.resize(total_len, b'0');\n\n        result\n    }\n\n    /// Returns the effective decimal digits\n    fn decimal_digits(&self) -> Vec<u8> {\n        if self.decimal_point_pos <= 0 {\n            // All digits become decimal with leading zeros\n            let total_len = self.leading_zeros + self.integer_part.len() + self.decimal_part.len();\n            let mut result = Vec::with_capacity(total_len);\n            result.resize(self.leading_zeros, b'0');\n            result.extend_from_slice(self.integer_part);\n            result.extend_from_slice(self.decimal_part);\n            result\n        } else {\n            let split_pos = (self.decimal_point_pos as usize)\n                .min(self.integer_part.len() + self.decimal_part.len());\n\n            if split_pos >= self.integer_part.len() + self.decimal_part.len() {\n                // No decimal digits\n                Vec::new()\n            } else if split_pos >= self.integer_part.len() {\n                // Split within decimal part\n                let skip_decimal = split_pos - self.integer_part.len();\n                self.decimal_part[skip_decimal..].to_vec()\n            } else {\n                // Split within integer part\n                let mut result = Vec::new();\n                result.extend_from_slice(&self.integer_part[split_pos..]);\n                result.extend_from_slice(self.decimal_part);\n                result\n            }\n        }\n    }\n}\n\n/// Process decimal-only numbers, setting weight based on leading zeros\nfn process_decimal_digits(\n    decimal_bytes: &[u8],\n    digits: &mut Vec<i16>,\n    weight: &mut i16,\n) -> Result<(), ParseError> {\n    let leading_zeros = decimal_bytes.iter().take_while(|&&b| b == b'0').count();\n\n    if leading_zeros < decimal_bytes.len() {\n        // Calculate weight based on position of first significant digit group\n        *weight = -(((leading_zeros / 4) + 1) as i16);\n\n        // Process from the start of the significant digit group\n        let group_start = (leading_zeros / 4) * 4;\n        let decimal_digits = build_decimal_digits_from_bytes(&decimal_bytes[group_start..])?;\n        digits.extend(decimal_digits);\n    } else {\n        // All zeros\n        *weight = 0;\n    }\n    Ok(())\n}\n\n/// Builds base-10000 digits from integer digit bytes.\n/// Returns digits in most-significant-first order.\nfn build_integer_digits_from_bytes(digits: &[u8]) -> Result<Vec<i16>, ParseError> {\n    // Skip leading zeros\n    let trimmed_start = digits\n        .iter()\n        .position(|&b| b != b'0')\n        .unwrap_or(digits.len());\n\n    if trimmed_start == digits.len() {\n        return Ok(Vec::new());\n    }\n\n    let trimmed = &digits[trimmed_start..];\n\n    // Process in chunks of 4 from the right, converting directly to base-10000\n    let mut result = Vec::with_capacity(trimmed.len().div_ceil(4));\n\n    for chunk in trimmed.rchunks(4) {\n        let mut digit = 0i16;\n        for &b in chunk {\n            if !b.is_ascii_digit() {\n                return Err(\"invalid digit character\".into());\n            }\n            digit = digit * 10 + (b - b'0') as i16;\n        }\n        result.push(digit);\n    }\n\n    result.reverse(); // Reverse to get most-significant-first order\n    Ok(result)\n}\n\n/// Builds base-10000 digits from decimal part bytes.\nfn build_decimal_digits_from_bytes(digits: &[u8]) -> Result<Vec<i16>, ParseError> {\n    if digits.is_empty() {\n        return Ok(Vec::new());\n    }\n\n    // Process in 4-digit chunks, padding the last chunk\n    let mut result = Vec::with_capacity(digits.len().div_ceil(4));\n\n    for chunk in digits.chunks(4) {\n        let mut digit = 0i16;\n        for &b in chunk {\n            if !b.is_ascii_digit() {\n                return Err(\"invalid digit character\".into());\n            }\n            digit = digit * 10 + (b - b'0') as i16;\n        }\n\n        // Pad to 4 digits for proper base-10000 representation\n        if chunk.len() < 4 {\n            digit *= 10i16.pow((4 - chunk.len()) as u32);\n        }\n\n        result.push(digit);\n    }\n\n    Ok(result)\n}\n\n/// Normalize leading zeros\nfn normalize_leading_zeros(digits: &mut Vec<i16>, weight: &mut i16) {\n    let leading_zero_count = digits.iter().take_while(|&&d| d == 0).count();\n\n    if leading_zero_count > 0 {\n        if leading_zero_count == digits.len() {\n            // All digits are zero\n            *weight = 0;\n            digits.clear();\n        } else {\n            // Remove leading zeros\n            digits.drain(0..leading_zero_count);\n            *weight -= leading_zero_count as i16;\n        }\n    }\n}\n\n/// Remove trailing zeros\nfn normalize_trailing_zeros(digits: &mut Vec<i16>) {\n    // Find last non-zero position\n    if let Some(last_nonzero) = digits.iter().rposition(|&d| d != 0) {\n        digits.truncate(last_nonzero + 1);\n    } else {\n        digits.clear();\n    }\n}\n\nfn split_e(s: &[u8]) -> (&[u8], Option<&[u8]>) {\n    let mut s = s.splitn(2, |&b| b == b'e' || b == b'E');\n    let first = s.next().unwrap();\n    let second = s.next();\n    (first, second)\n}\n\nfn split_decimal(s: &[u8]) -> (&[u8], Option<&[u8]>) {\n    let mut s = s.splitn(2, |&b| b == b'.');\n    let first = s.next().unwrap();\n    let second = s.next();\n    (first, second)\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_string_deserialization_and_serialization() {\n        let cases = &[\n            (\n                \"0\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![],\n                },\n            ),\n            (\n                \"1\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![1],\n                },\n            ),\n            (\n                \"-1\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![1],\n                },\n            ),\n            (\n                \"10\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![10],\n                },\n            ),\n            (\n                \"-10\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![10],\n                },\n            ),\n            (\n                \"20000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"-20000\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"20001\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2, 1],\n                },\n            ),\n            (\n                \"-20001\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2, 1],\n                },\n            ),\n            (\n                \"200000000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 2,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"2.0\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 1,\n                    weight: 0,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"2.1\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 1,\n                    weight: 0,\n                    digits: vec![2, 1000],\n                },\n            ),\n            (\n                \"2.10\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 2,\n                    weight: 0,\n                    digits: vec![2, 1000],\n                },\n            ),\n            (\n                \"200000000.0001\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 4,\n                    weight: 2,\n                    digits: vec![2, 0, 0, 1],\n                },\n            ),\n            (\n                \"-200000000.0001\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 4,\n                    weight: 2,\n                    digits: vec![2, 0, 0, 1],\n                },\n            ),\n            (\n                \"0.1\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 1,\n                    weight: -1,\n                    digits: vec![1000],\n                },\n            ),\n            (\n                \"-0.1\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 1,\n                    weight: -1,\n                    digits: vec![1000],\n                },\n            ),\n            (\n                \"123.456\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 3,\n                    weight: 0,\n                    digits: vec![123, 4560],\n                },\n            ),\n            (\n                \"-123.456\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 3,\n                    weight: 0,\n                    digits: vec![123, 4560],\n                },\n            ),\n            (\n                \"-123.0456\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 4,\n                    weight: 0,\n                    digits: vec![123, 456],\n                },\n            ),\n            (\n                \"0.1000000000000000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 16,\n                    weight: -1,\n                    digits: vec![1000],\n                },\n            ),\n            (\n                \"-0.1000000000000000\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 16,\n                    weight: -1,\n                    digits: vec![1000],\n                },\n            ),\n            (\n                \"0.003159370000000000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 18,\n                    weight: -1,\n                    digits: vec![31, 5937],\n                },\n            ),\n            (\n                \"-0.003159370000000000\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 18,\n                    weight: -1,\n                    digits: vec![31, 5937],\n                },\n            ),\n            (\n                \"0.0000000000000002\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 16,\n                    weight: -4,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"-0.0000000000000002\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 16,\n                    weight: -4,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 35,\n                    digits: vec![\n                        1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n                        0, 0, 0, 0, 0, 0, 0, 1,\n                    ],\n                },\n            ),\n            (\n                \"-100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 35,\n                    digits: vec![\n                        1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n                        0, 0, 0, 0, 0, 0, 0, 1,\n                    ],\n                },\n            ),\n        ];\n\n        for (str, n) in cases {\n            assert_eq!(*str, n.to_string(), \"numeric to string\");\n            let num = str.parse::<Numeric>().expect(\"parse numeric\");\n            assert_eq!(num, *n, \"numeric from string\");\n        }\n    }\n\n    #[test]\n    fn test_from_scientific_notation() {\n        let cases = &[\n            (\n                \"2e4\",\n                \"20000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"2e+4\",\n                \"20000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"-2e4\",\n                \"-20000\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"-2e-4\",\n                \"-0.0002\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 4,\n                    weight: -1,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"1.234e4\",\n                \"12340\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![1, 2340],\n                },\n            ),\n            (\n                \"-1.234e4\",\n                \"-12340\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![1, 2340],\n                },\n            ),\n            (\n                \"1.234e5\",\n                \"123400\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![12, 3400],\n                },\n            ),\n            (\n                \"-1.234e5\",\n                \"-123400\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![12, 3400],\n                },\n            ),\n            (\n                \"1.234e8\",\n                \"123400000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 2,\n                    digits: vec![1, 2340],\n                },\n            ),\n            (\n                \"-1.234e8\",\n                \"-123400000\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 2,\n                    digits: vec![1, 2340],\n                },\n            ),\n            (\n                \"0.0001e4\",\n                \"1\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![1],\n                },\n            ),\n            (\n                \"-0.0001e4\",\n                \"-1\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![1],\n                },\n            ),\n            (\n                \"0.0001e5\",\n                \"10\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![10],\n                },\n            ),\n            (\n                \"-0.0001e5\",\n                \"-10\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 0,\n                    digits: vec![10],\n                },\n            ),\n            (\n                \"2e16\",\n                \"20000000000000000\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 4,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"-2e16\",\n                \"-20000000000000000\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 0,\n                    weight: 4,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"2e-16\",\n                \"0.0000000000000002\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 16,\n                    weight: -4,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"-2e-16\",\n                \"-0.0000000000000002\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 16,\n                    weight: -4,\n                    digits: vec![2],\n                },\n            ),\n            (\n                \"2e-17\",\n                \"0.00000000000000002\",\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 17,\n                    weight: -5,\n                    digits: vec![2000],\n                },\n            ),\n            (\n                \"-2e-17\",\n                \"-0.00000000000000002\",\n                Numeric {\n                    sign: NumericSign::Negative,\n                    scale: 17,\n                    weight: -5,\n                    digits: vec![2000],\n                },\n            ),\n        ];\n\n        for (e, str, n) in cases {\n            let num = e.parse::<Numeric>().expect(\"parse numeric\");\n            assert_eq!(num, *n, \"{e} to numeric\");\n            assert_eq!(num.to_string(), *str, \"{e} back to string\");\n        }\n    }\n\n    #[test]\n    fn test_error_conditions() {\n        // Test inputs that should definitely be errors\n        let definitely_invalid = &[\n            \"\",        // Empty string\n            \".\",       // Just a decimal sign\n            \"-\",       // Just minus sign\n            \"+\",       // Just plus sign\n            \"abc\",     // Non-numeric characters\n            \"1.2.3\",   // Multiple decimal points\n            \"1.2.3e4\", // Multiple decimals with scientific\n            \"1e2e3\",   // Multiple scientific notations\n            \"∞\",       // Unicode infinity (not ASCII)\n            \"1ee2\",    // Double e\n            \"1e\",      // Scientific notation without exponent\n            \"1e+\",     // Scientific notation with just +\n            \"1e-\",     // Scientific notation with just -\n        ];\n\n        for &invalid in definitely_invalid {\n            assert!(\n                invalid.parse::<Numeric>().is_err(),\n                \"Should fail to parse: '{}'\",\n                invalid\n            );\n        }\n\n        // Test that some edge cases are actually valid\n        let valid_edge_cases = &[\n            (\"-.1\", \"-0.1\"), // Negative with no integer part\n            (\"1.\", \"1\"),     // Trailing decimal point\n            (\"0.\", \"0\"),     // Zero with trailing decimal\n            (\".1\", \"0.1\"),   // Decimal point prefix\n            (\".0\", \"0.0\"),   // Zero with decimal point prefix\n        ];\n\n        for (input, expected) in valid_edge_cases {\n            let num = input\n                .parse::<Numeric>()\n                .unwrap_or_else(|_| panic!(\"Should parse: '{}'\", input));\n            assert_eq!(\n                num.to_string(),\n                *expected,\n                \"Edge case: {} -> {}\",\n                input,\n                expected\n            );\n        }\n    }\n\n    #[test]\n    fn test_scientific_notation_edge_cases() {\n        let cases = &[\n            // Zero with various scientific notations - scale is preserved\n            (\"0e0\", \"0\"),\n            (\"0e10\", \"0\"),\n            (\"0e-10\", \"0.0000000000\"), // Scale preserved from scientific notation\n            (\"0.0e5\", \"0.0\"),\n            (\"0.000e100\", \"0.000\"),\n            // Very large positive exponents\n            (\n                \"1e50\",\n                \"100000000000000000000000000000000000000000000000000\",\n            ),\n            (\"2e20\", \"200000000000000000000\"),\n            // Very large negative exponents\n            (\n                \"1e-50\",\n                \"0.00000000000000000000000000000000000000000000000001\",\n            ),\n            (\"5e-25\", \"0.0000000000000000000000005\"),\n            // Leading zeros in mantissa with scientific notation\n            (\"000123e2\", \"12300\"),\n            (\"0.000123e3\", \"0.123\"),\n            (\"0.000123e6\", \"123\"),\n            // Trailing zeros\n            (\"1230e-2\", \"12.30\"),\n            (\"1000e3\", \"1000000\"),\n        ];\n\n        for (input, expected) in cases {\n            let num = input\n                .parse::<Numeric>()\n                .unwrap_or_else(|_| panic!(\"Failed to parse: {}\", input));\n            let result = num.to_string();\n            assert_eq!(\n                result, *expected,\n                \"Scientific notation: {} -> {}\",\n                input, expected\n            );\n        }\n    }\n\n    #[test]\n    fn test_normalization_edge_cases() {\n        let cases = &[\n            // Various patterns of leading zeros\n            (\"000\", \"0\"),\n            (\"000.000\", \"0.000\"),\n            (\"0001\", \"1\"),\n            (\"0001.0010\", \"1.0010\"), // Trailing zeros in scale are preserved\n            // Trailing zeros in different positions\n            (\"1.000\", \"1.000\"),\n            (\"1.100\", \"1.100\"),\n            (\"1.010\", \"1.010\"),\n            (\"10.000\", \"10.000\"),\n            // All zeros in digit groups\n            (\"10000\", \"10000\"),         // This becomes weight=1, digits=[1]\n            (\"100000000\", \"100000000\"), // This becomes weight=2, digits=[1]\n        ];\n\n        for (input, expected) in cases {\n            let num = input\n                .parse::<Numeric>()\n                .unwrap_or_else(|_| panic!(\"Failed to parse: {}\", input));\n            let result = num.to_string();\n            assert_eq!(\n                result, *expected,\n                \"Normalization: {} -> {}\",\n                input, expected\n            );\n        }\n    }\n\n    #[test]\n    fn test_weight_boundary_conditions() {\n        let cases = &[\n            // Test numbers that fall on base-10000 digit boundaries\n            (\"1\", 0),         // Single digit -> weight 0\n            (\"9999\", 0),      // Max single group -> weight 0\n            (\"10000\", 1),     // Min double group -> weight 1\n            (\"99999999\", 1),  // Max double group -> weight 1\n            (\"100000000\", 2), // Min triple group -> weight 2\n            (\"0.1\", -1),\n            (\"0.0001\", -1),\n            (\"0.00001\", -2),\n            (\"0.000000001\", -3),\n        ];\n\n        for (input, expected_weight) in cases {\n            let num = input\n                .parse::<Numeric>()\n                .unwrap_or_else(|_| panic!(\"Failed to parse: {}\", input));\n            assert_eq!(\n                num.weight(),\n                *expected_weight,\n                \"Weight for {}: expected {}, got {}\",\n                input,\n                expected_weight,\n                num.weight()\n            );\n        }\n    }\n\n    #[test]\n    fn test_precision_limits() {\n        // Test very high precision numbers\n        let high_precision_cases = &[\n            \"0.123456789012345678901234567890123456789012345678901234567890\",\n            \"123456789012345678901234567890.123456789012345678901234567890\",\n            \"0.000000000000000000000000000000000000000000000000000000000123456789\",\n        ];\n\n        for input in high_precision_cases {\n            let num = input\n                .parse::<Numeric>()\n                .unwrap_or_else(|_| panic!(\"Failed to parse: {}\", input));\n            let result = num.to_string();\n            // Should be able to round-trip, though might lose trailing zeros\n            let reparsed = result\n                .parse::<Numeric>()\n                .unwrap_or_else(|_| panic!(\"Failed to reparse: {}\", result));\n            assert_eq!(num, reparsed, \"Round-trip failed for: {}\", input);\n        }\n    }\n\n    #[test]\n    fn test_display_formatting_edge_cases() {\n        let cases = &[\n            // Numbers that test decimal formatting boundaries\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 4,\n                    weight: -1,\n                    digits: vec![1],\n                },\n                \"0.0001\",\n            ),\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 4,\n                    weight: -2,\n                    digits: vec![1],\n                },\n                \"0.0000\",\n            ), // Only 4 decimal places due to scale\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 8,\n                    weight: -2,\n                    digits: vec![1],\n                },\n                \"0.00000001\",\n            ), // 8 decimal places\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 1,\n                    weight: -1,\n                    digits: vec![5000],\n                },\n                \"0.5\",\n            ),\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 8,\n                    weight: -2,\n                    digits: vec![1234],\n                },\n                \"0.00001234\",\n            ),\n            // Numbers that test weight group boundaries\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![1],\n                },\n                \"10000\",\n            ),\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 2,\n                    digits: vec![1],\n                },\n                \"100000000\",\n            ),\n            (\n                Numeric {\n                    sign: NumericSign::Positive,\n                    scale: 0,\n                    weight: 1,\n                    digits: vec![1, 2345],\n                },\n                \"12345\",\n            ),\n        ];\n\n        for (numeric, expected) in cases {\n            let result = numeric.to_string();\n            assert_eq!(\n                result, *expected,\n                \"Display formatting failed for {:?}\",\n                numeric\n            );\n        }\n    }\n\n    #[test]\n    fn test_various_corner_cases() {\n        // Scientific notation bounds - exponent limits\n        let exponent_bounds = &[\n            \"1e1000000\",  // At exponent limit (should work)\n            \"1e1000001\",  // Above exponent limit (should error)\n            \"1e-1000000\", // At negative limit (should work)\n            \"1e-1000001\", // Above negative limit (should error)\n        ];\n\n        // First and third should work, second and fourth should error\n        assert!(\n            exponent_bounds[0].parse::<Numeric>().is_ok(),\n            \"Should parse: {}\",\n            exponent_bounds[0]\n        );\n        assert!(\n            exponent_bounds[1].parse::<Numeric>().is_err(),\n            \"Should error: {}\",\n            exponent_bounds[1]\n        );\n        assert!(\n            exponent_bounds[2].parse::<Numeric>().is_ok(),\n            \"Should parse: {}\",\n            exponent_bounds[2]\n        );\n        assert!(\n            exponent_bounds[3].parse::<Numeric>().is_err(),\n            \"Should error: {}\",\n            exponent_bounds[3]\n        );\n\n        // Scale calculation u16 boundary\n        let scale_boundary = &[\n            (\"1e-65535\", 65535_u16),     // Maximum u16 scale\n            (\"1.234e-65533\", 65535_u16), // Should cap at u16::MAX\n        ];\n        for (input, expected_scale) in scale_boundary {\n            let parsed = input.parse::<Numeric>().unwrap();\n            assert_eq!(parsed.scale(), *expected_scale, \"Scale boundary: {}\", input);\n        }\n\n        // Weight boundary conditions - very deep decimals\n        let weight_boundaries = &[\n            (\"0.0001\", -1_i16),      // Basic negative weight\n            (\"0.00001\", -2_i16),     // Weight -2\n            (\"0.000000001\", -3_i16), // Weight -3 (9 zeros, 3rd group)\n        ];\n        for (input, expected_weight) in weight_boundaries {\n            let parsed = input.parse::<Numeric>().unwrap();\n            assert_eq!(\n                parsed.weight(),\n                *expected_weight,\n                \"Weight boundary: {}\",\n                input\n            );\n        }\n\n        // Digit processing - 4-digit boundary cases\n        let digit_boundaries = &[\n            (\"0.0000123\", -2_i16),      // 4 leading zeros -> 2nd group\n            (\"0.00000001\", -2_i16),     // 7 leading zeros -> 2nd group\n            (\"0.000000000001\", -3_i16), // 11 leading zeros -> 3rd group\n        ];\n        for (input, expected_weight) in digit_boundaries {\n            let parsed = input.parse::<Numeric>().unwrap();\n            assert_eq!(\n                parsed.weight(),\n                *expected_weight,\n                \"Digit boundary: {}\",\n                input\n            );\n        }\n\n        // ScientificLayout split logic edge cases\n        let split_cases = &[\n            (\"12.34e-2\", \"0.1234\"),     // Split within decimal\n            (\"123.45e-5\", \"0.0012345\"), // Split creates leading zeros\n            (\"123e-1\", \"12.3\"),         // Split exactly at boundary\n            (\"1e0\", \"1\"),               // No-op scientific notation\n        ];\n        for (input, expected) in split_cases {\n            let parsed = input.parse::<Numeric>().unwrap();\n            assert_eq!(format!(\"{}\", parsed), *expected, \"Split case: {}\", input);\n        }\n\n        // Error conditions with detailed messages\n        let detailed_errors = &[\n            \"1e1000001\", // scientific notation exponent too large\n            \"1e+\",       // empty scientific notation exponent\n            \"1e-\",       // empty scientific notation exponent\n            \"1e\",        // empty scientific notation string\n            \"\",          // empty numeric string\n        ];\n        for input in detailed_errors {\n            assert!(input.parse::<Numeric>().is_err(), \"Should error: {}\", input);\n        }\n\n        // Zero with various scales to test calculate_scale zero handling\n        let zero_scale_cases = &[\n            (\"0e-10\", 10_u16),  // Zero with implied scale\n            (\"0.0e5\", 1_u16),   // Zero preserving original scale\n            (\"0.000e3\", 3_u16), // Zero with decimal scale\n        ];\n        for (input, expected_scale) in zero_scale_cases {\n            let parsed = input.parse::<Numeric>().unwrap();\n            assert_eq!(\n                parsed.scale(),\n                *expected_scale,\n                \"Zero scale case: {}\",\n                input\n            );\n        }\n    }\n\n    use proptest::prelude::*;\n    proptest! {\n        #[test]\n        fn test_arbitrary_f64_from_string_and_back(value in any::<f64>()) {\n            let prop_val = value.to_string();\n            let numeric = Numeric::from_str(&prop_val).expect(\"parse numeric\");\n            let str_val = numeric.to_string();\n            assert_eq!(prop_val, str_val, \"proprty test value {value}\");\n        }\n        #[test]\n        fn test_arbitrary_i64_from_string_and_back(value in any::<i64>()) {\n            let prop_val = value.to_string();\n            let numeric = Numeric::from_str(&prop_val).expect(\"parse numeric\");\n            let str_val = numeric.to_string();\n            assert_eq!(prop_val, str_val, \"proprty test value {value}\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/sqldb/transaction.rs",
    "content": "use tokio_postgres::types::BorrowToSql;\n\nuse crate::model;\n\nuse super::{\n    client::{Error, PooledConn, QueryTracer},\n    Cursor,\n};\n\n// Heavily inspired by rust-postgres, but where the transaction doesnt have a lifetime, so it can\n// be shared via napi-rs.\n//\n// https://github.com/sfackler/rust-postgres/blob/720ffe83216714bf9716a03122c547a2e8e9bfd9/tokio-postgres/src/transaction.rs\n\npub struct Transaction {\n    conn: PooledConn,\n    tracer: QueryTracer,\n    done: bool,\n}\n\nimpl Transaction {\n    pub(crate) async fn begin(\n        conn: PooledConn,\n        tracer: QueryTracer,\n        source: Option<&model::Request>,\n    ) -> Result<Self, Error> {\n        struct RollbackIfNotDone<'me> {\n            client: &'me tokio_postgres::Client,\n            done: bool,\n        }\n\n        impl Drop for RollbackIfNotDone<'_> {\n            fn drop(&mut self) {\n                if self.done {\n                    return;\n                }\n\n                self.client.__private_api_rollback(None);\n            }\n        }\n\n        // This is done, as `Future` created by this method can be dropped after\n        // `RequestMessages` is synchronously send to the `Connection` by\n        // `batch_execute()`, but before `Responses` is asynchronously polled to\n        // completion. In that case `Transaction` won't be created and thus\n        // won't be rolled back.\n        {\n            let mut cleaner = RollbackIfNotDone {\n                client: &conn,\n                done: false,\n            };\n\n            tracer\n                .trace_batch_execute(source, \"BEGIN\", || async {\n                    conn.batch_execute(\"BEGIN\").await.map_err(Error::from)\n                })\n                .await?;\n\n            cleaner.done = true;\n        }\n\n        Ok(Transaction {\n            conn,\n            tracer,\n            done: false,\n        })\n    }\n\n    pub async fn commit(mut self, source: Option<&model::Request>) -> Result<(), Error> {\n        self.done = true;\n        self.batch_execute(\"COMMIT\", source).await\n    }\n\n    pub async fn rollback(mut self, source: Option<&model::Request>) -> Result<(), Error> {\n        self.done = true;\n        self.batch_execute(\"ROLLBACK\", source).await\n    }\n\n    async fn batch_execute(\n        &self,\n        query: &str,\n        source: Option<&model::Request>,\n    ) -> Result<(), Error> {\n        self.tracer\n            .trace_batch_execute(source, query, || async {\n                self.conn.batch_execute(query).await.map_err(Error::from)\n            })\n            .await\n    }\n\n    pub async fn query_raw<P, I>(\n        &self,\n        query: &str,\n        params: I,\n        source: Option<&model::Request>,\n    ) -> Result<Cursor, Error>\n    where\n        P: BorrowToSql,\n        I: IntoIterator<Item = P>,\n        I::IntoIter: ExactSizeIterator,\n    {\n        self.tracer\n            .trace(source, query, || async {\n                self.conn\n                    .query_raw(query, params)\n                    .await\n                    .map_err(Error::from)\n            })\n            .await\n    }\n}\n\nimpl Drop for Transaction {\n    fn drop(&mut self) {\n        if self.done {\n            return;\n        }\n\n        log::warn!(\"transaction not completed, forcing rollback\");\n        self.conn.__private_api_rollback(None);\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/sqldb/val.rs",
    "content": "use anyhow::Context;\nuse bytes::{BufMut, BytesMut};\nuse serde::Serialize;\nuse std::{error::Error, str::FromStr};\nuse tokio_postgres::types::{to_sql_checked, FromSql, IsNull, Kind, ToSql, Type};\nuse uuid::Uuid;\n\nuse crate::api::{DateTime, Decimal, PValue};\n\n#[derive(Debug)]\npub enum RowValue {\n    PVal(PValue),\n    Bytes(Vec<u8>),\n    Uuid(uuid::Uuid),\n    Inet(cidr::IpInet),\n    Cidr(cidr::IpCidr),\n}\n\nfn is_pgvector(ty: &Type) -> bool {\n    ty.name() == \"vector\"\n}\n\nimpl ToSql for RowValue {\n    fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>>\n    where\n        Self: Sized,\n    {\n        match self {\n            Self::Bytes(val) => val.to_sql(ty, out),\n            Self::Uuid(val) => match *ty {\n                Type::UUID => val.to_sql(ty, out),\n                Type::TEXT | Type::VARCHAR => val.to_string().to_sql(ty, out),\n                _ => Err(format!(\"uuid not supported for column of type {ty}\").into()),\n            },\n            Self::Cidr(val) => match *ty {\n                Type::CIDR => val.to_sql(ty, out),\n                Type::TEXT | Type::VARCHAR => val.to_string().to_sql(ty, out),\n                _ => Err(format!(\"cidr not supported for column of type {ty}\").into()),\n            },\n            Self::Inet(val) => match *ty {\n                Type::INET => val.to_sql(ty, out),\n                Type::TEXT | Type::VARCHAR => val.to_string().to_sql(ty, out),\n                _ => Err(format!(\"inet not supported for column of type {ty}\").into()),\n            },\n\n            Self::PVal(val) => val.to_sql(ty, out),\n        }\n    }\n\n    fn accepts(ty: &Type) -> bool {\n        matches!(\n            *ty,\n            Type::BYTEA | Type::UUID | Type::TEXT | Type::VARCHAR | Type::CIDR | Type::INET\n        ) || matches!(ty.kind(), Kind::Array(ty) if <RowValue as ToSql>::accepts(ty))\n            || <PValue as ToSql>::accepts(ty)\n    }\n\n    to_sql_checked!();\n}\n\nimpl ToSql for PValue {\n    fn to_sql(\n        &self,\n        ty: &Type,\n        out: &mut BytesMut,\n    ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {\n        match *ty {\n            Type::JSON | Type::JSONB => {\n                if *ty == Type::JSONB {\n                    out.put_u8(1);\n                }\n\n                let mut ser = serde_json::ser::Serializer::new(out.writer());\n                self.serialize(&mut ser)?;\n                Ok(IsNull::No)\n            }\n\n            _ => match self {\n                PValue::Null => Ok(IsNull::Yes),\n                PValue::Bool(bool) => match *ty {\n                    Type::BOOL => bool.to_sql(ty, out),\n                    Type::TEXT | Type::VARCHAR => bool.to_string().to_sql(ty, out),\n                    _ => Err(format!(\"bool not supported for column of type {ty}\").into()),\n                },\n\n                PValue::String(str) => match *ty {\n                    Type::TEXT | Type::VARCHAR | Type::NAME => str.to_sql(ty, out),\n                    Type::BYTEA => {\n                        let val = str.as_bytes();\n                        val.to_sql(ty, out)\n                    }\n                    Type::TIMESTAMP => {\n                        let val = str.parse::<chrono::NaiveDateTime>().map_err(Box::new)?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::TIMESTAMPTZ => {\n                        let val = chrono::DateTime::parse_from_rfc3339(str).map_err(Box::new)?;\n                        val.with_timezone(&chrono::Utc).to_sql(ty, out)\n                    }\n                    Type::DATE => {\n                        let val =\n                            chrono::NaiveDate::parse_from_str(str, \"%Y-%m-%d\").map_err(Box::new)?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::TIME => {\n                        let val =\n                            chrono::NaiveTime::parse_from_str(str, \"%H:%M:%S\").map_err(Box::new)?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::UUID => {\n                        let val = Uuid::parse_str(str).context(\"unable to parse uuid\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::CIDR => {\n                        let val = cidr::IpCidr::from_str(str).context(\"unable to parse cidr\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::INET => {\n                        let val = cidr::IpInet::from_str(str).context(\"unable to parse inet\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::NUMERIC => {\n                        let d = Decimal::from_str(str)?;\n                        d.to_sql(ty, out)\n                    }\n                    _ => {\n                        if let Kind::Enum(_) = ty.kind() {\n                            str.to_sql(ty, out)\n                        } else if is_pgvector(ty) {\n                            let val: pgvector::Vector =\n                                serde_json::from_str(str).context(\"unable to parse vector\")?;\n                            val.to_sql(ty, out)\n                        } else {\n                            Err(format!(\"string not supported for column of type {ty}\").into())\n                        }\n                    }\n                },\n\n                PValue::Number(num) => match *ty {\n                    Type::INT2 => {\n                        let val: Result<i16, _> = if num.is_i64() {\n                            num.as_i64().unwrap().try_into()\n                        } else if num.is_u64() {\n                            num.as_u64().unwrap().try_into()\n                        } else if num.is_f64() {\n                            let float = num.as_f64().unwrap();\n                            let res = float as i16;\n                            if res as f64 == float {\n                                Ok(res)\n                            } else {\n                                return Err(format!(\"number {float} is not an i16\").into());\n                            }\n                        } else {\n                            return Err(format!(\"unsupported number: {num:?}\").into());\n                        };\n                        val.map_err(Box::new)?.to_sql(ty, out)\n                    }\n                    Type::INT4 => {\n                        let val: Result<i32, _> = if num.is_i64() {\n                            num.as_i64().unwrap().try_into()\n                        } else if num.is_u64() {\n                            num.as_u64().unwrap().try_into()\n                        } else if num.is_f64() {\n                            let float = num.as_f64().unwrap();\n                            let res = float as i32;\n                            if res as f64 == float {\n                                Ok(res)\n                            } else {\n                                return Err(format!(\"number {float} is not an i32\").into());\n                            }\n                        } else {\n                            return Err(format!(\"unsupported number: {num:?}\").into());\n                        };\n                        val.map_err(Box::new)?.to_sql(ty, out)\n                    }\n                    Type::INT8 => {\n                        let val: Result<i64, _> = if num.is_i64() {\n                            Ok(num.as_i64().unwrap())\n                        } else if num.is_u64() {\n                            num.as_u64().unwrap().try_into()\n                        } else if num.is_f64() {\n                            let float = num.as_f64().unwrap();\n                            let res = float as i64;\n                            if res as f64 == float {\n                                Ok(res)\n                            } else {\n                                return Err(format!(\"number {float} is not an i64\").into());\n                            }\n                        } else {\n                            return Err(format!(\"unsupported number: {num:?}\").into());\n                        };\n                        val.map_err(Box::new)?.to_sql(ty, out)\n                    }\n                    Type::FLOAT4 => {\n                        let val: f32 = if num.is_i64() {\n                            num.as_i64().unwrap() as f32\n                        } else if num.is_u64() {\n                            num.as_u64().unwrap() as f32\n                        } else if num.is_f64() {\n                            num.as_f64().unwrap() as f32\n                        } else {\n                            return Err(format!(\"unsupported number: {num:?}\").into());\n                        };\n                        val.to_sql(ty, out)\n                    }\n                    Type::FLOAT8 => {\n                        let val: f64 = if num.is_i64() {\n                            num.as_i64().unwrap() as f64\n                        } else if num.is_u64() {\n                            num.as_u64().unwrap() as f64\n                        } else if num.is_f64() {\n                            num.as_f64().unwrap()\n                        } else {\n                            return Err(format!(\"unsupported number: {num:?}\").into());\n                        };\n                        val.to_sql(ty, out)\n                    }\n                    Type::OID => {\n                        let val: Result<u32, _> = if num.is_i64() {\n                            num.as_i64().unwrap().try_into()\n                        } else if num.is_u64() {\n                            num.as_u64().unwrap().try_into()\n                        } else if num.is_f64() {\n                            let float = num.as_f64().unwrap();\n                            let res = float as u32;\n                            if res as f64 == float {\n                                Ok(res)\n                            } else {\n                                return Err(format!(\"number {float} is not an u32\").into());\n                            }\n                        } else {\n                            return Err(format!(\"unsupported number: {num:?}\").into());\n                        };\n                        val.map_err(Box::new)?.to_sql(ty, out)\n                    }\n                    Type::NUMERIC => {\n                        let num_str = num.to_string();\n                        let d = Decimal::from_str(&num_str)?;\n                        d.to_sql(ty, out)\n                    }\n                    Type::TEXT | Type::VARCHAR => self.to_string().to_sql(ty, out),\n                    _ => {\n                        if num.is_i64() {\n                            num.as_i64().unwrap().to_sql(ty, out)\n                        } else if num.is_u64() {\n                            (num.as_u64().unwrap() as i64).to_sql(ty, out)\n                        } else if num.is_f64() {\n                            num.as_f64().unwrap().to_sql(ty, out)\n                        } else {\n                            Err(format!(\"unsupported number: {num:?}\").into())\n                        }\n                    }\n                },\n                PValue::Decimal(d) => match *ty {\n                    Type::NUMERIC => d.to_sql(ty, out),\n                    Type::FLOAT4 => {\n                        let val =\n                            f32::try_from(d).map_err(|_| \"Cannot convert decimal to FLOAT4\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::FLOAT8 => {\n                        let val =\n                            f64::try_from(d).map_err(|_| \"Cannot convert decimal to FLOAT8\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::INT2 => {\n                        let val = i16::try_from(d).map_err(|_| \"Cannot convert decimal to INT2\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::INT4 => {\n                        let val = i32::try_from(d).map_err(|_| \"Cannot convert decimal to INT4\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::INT8 => {\n                        let val = i64::try_from(d).map_err(|_| \"Cannot convert decimal to INT8\")?;\n                        val.to_sql(ty, out)\n                    }\n                    Type::TEXT | Type::VARCHAR => {\n                        let decimal_str = d.to_string();\n                        decimal_str.to_sql(ty, out)\n                    }\n\n                    _ => Err(format!(\"unsupported type for Decimal: {ty}\").into()),\n                },\n                PValue::DateTime(dt) => match *ty {\n                    Type::DATE => dt.naive_utc().date().to_sql(ty, out),\n                    Type::TIMESTAMP => dt.naive_utc().to_sql(ty, out),\n                    Type::TIMESTAMPTZ => dt.to_sql(ty, out),\n                    Type::TEXT | Type::VARCHAR => dt.to_rfc3339().to_sql(ty, out),\n                    _ => Err(format!(\"unsupported type for DateTime: {ty}\").into()),\n                },\n                PValue::Array(arr) => {\n                    if is_pgvector(ty) {\n                        let floats = arr\n                            .iter()\n                            .map(|v| match v {\n                                PValue::Number(n) => n\n                                    .as_f64()\n                                    .or_else(|| n.as_i64().map(|i| i as f64))\n                                    .or_else(|| n.as_u64().map(|u| u as f64))\n                                    .map(|f| f as f32)\n                                    .ok_or_else(|| \"vector element must be a number\".into()),\n                                _ => Err(\"vector element must be a number\".into()),\n                            })\n                            .collect::<Result<Vec<f32>, Box<dyn Error + Sync + Send>>>()?;\n                        pgvector::Vector::from(floats).to_sql(ty, out)\n                    } else {\n                        arr.to_sql(ty, out)\n                    }\n                }\n                PValue::Object(_) => {\n                    Err(format!(\"object not supported for column of type {ty}\").into())\n                }\n                PValue::Cookie(_) => {\n                    Err(format!(\"cookie not supported for column of type {ty}\").into())\n                }\n            },\n        }\n    }\n\n    fn accepts(ty: &Type) -> bool {\n        matches!(\n            *ty,\n            Type::BOOL\n                | Type::BYTEA\n                | Type::TEXT\n                | Type::VARCHAR\n                | Type::INT2\n                | Type::INT4\n                | Type::INT8\n                | Type::OID\n                | Type::JSONB\n                | Type::JSON\n                | Type::UUID\n                | Type::FLOAT4\n                | Type::FLOAT8\n                | Type::TIMESTAMP\n                | Type::TIMESTAMPTZ\n                | Type::DATE\n                | Type::TIME\n                | Type::INET\n                | Type::CIDR\n                | Type::NAME\n                | Type::NUMERIC\n        ) || matches!(ty.kind(), Kind::Enum(_))\n            || matches!(ty.kind(), Kind::Array(ty) if <PValue as ToSql>::accepts(ty))\n            || is_pgvector(ty)\n    }\n    to_sql_checked!();\n}\n\nimpl<'a> FromSql<'a> for RowValue {\n    fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {\n        Ok(match *ty {\n            Type::BYTEA => {\n                let val: Vec<u8> = FromSql::from_sql(ty, raw)?;\n                Self::Bytes(val)\n            }\n            Type::UUID => {\n                let val: uuid::Uuid = FromSql::from_sql(ty, raw)?;\n                Self::Uuid(val)\n            }\n            Type::CIDR => {\n                let val: cidr::IpCidr = FromSql::from_sql(ty, raw)?;\n                Self::Cidr(val)\n            }\n            Type::INET => {\n                let val: cidr::IpInet = FromSql::from_sql(ty, raw)?;\n                Self::Inet(val)\n            }\n            _ => {\n                if <PValue as FromSql>::accepts(ty) {\n                    Self::PVal(FromSql::from_sql(ty, raw)?)\n                } else {\n                    return Err(format!(\"unsupported type: {ty:?}\").into());\n                }\n            }\n        })\n    }\n\n    fn from_sql_null(_: &Type) -> Result<Self, Box<dyn Error + Sync + Send>> {\n        Ok(Self::PVal(PValue::Null))\n    }\n\n    fn accepts(ty: &Type) -> bool {\n        matches!(*ty, Type::BYTEA | Type::UUID | Type::CIDR | Type::INET)\n            || matches!(ty.kind(), Kind::Array(ty) if <RowValue as FromSql>::accepts(ty))\n            || <PValue as FromSql>::accepts(ty)\n    }\n}\n\nimpl<'a> FromSql<'a> for PValue {\n    fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {\n        Ok(match *ty {\n            Type::BOOL => {\n                let val: bool = FromSql::from_sql(ty, raw)?;\n                PValue::Bool(val)\n            }\n            Type::TEXT | Type::VARCHAR | Type::NAME => {\n                let val: String = FromSql::from_sql(ty, raw)?;\n                PValue::String(val)\n            }\n            Type::INT2 => {\n                let val: i16 = FromSql::from_sql(ty, raw)?;\n                PValue::Number(serde_json::Number::from(val))\n            }\n            Type::INT4 => {\n                let val: i32 = FromSql::from_sql(ty, raw)?;\n                PValue::Number(serde_json::Number::from(val))\n            }\n            Type::INT8 => {\n                let val: i64 = FromSql::from_sql(ty, raw)?;\n                PValue::Number(serde_json::Number::from(val))\n            }\n            Type::OID => {\n                let val: u32 = FromSql::from_sql(ty, raw)?;\n                PValue::Number(serde_json::Number::from(val))\n            }\n            Type::JSON | Type::JSONB => {\n                let val: serde_json::Value = FromSql::from_sql(ty, raw)?;\n                val.into()\n            }\n            Type::FLOAT4 => {\n                let val: f32 = FromSql::from_sql(ty, raw)?;\n                match serde_json::Number::from_f64(val as f64) {\n                    Some(num) => PValue::Number(num),\n                    None => PValue::Null,\n                }\n            }\n            Type::FLOAT8 => {\n                let val: f64 = FromSql::from_sql(ty, raw)?;\n                match serde_json::Number::from_f64(val) {\n                    Some(num) => PValue::Number(num),\n                    None => PValue::Null,\n                }\n            }\n            Type::NUMERIC => {\n                let d = Decimal::from_sql(ty, raw)?;\n                PValue::Decimal(d)\n            }\n            Type::TIMESTAMP => {\n                let val: DateTime = FromSql::from_sql(ty, raw)?;\n                PValue::DateTime(val)\n            }\n            Type::TIMESTAMPTZ => {\n                let val: DateTime = FromSql::from_sql(ty, raw)?;\n                PValue::DateTime(val)\n            }\n            Type::DATE => {\n                let val: chrono::NaiveDate = FromSql::from_sql(ty, raw)?;\n                PValue::String(val.to_string())\n            }\n            Type::TIME => {\n                let val: chrono::NaiveTime = FromSql::from_sql(ty, raw)?;\n                PValue::String(val.to_string())\n            }\n            Type::CIDR => {\n                let val: cidr::IpCidr = FromSql::from_sql(ty, raw)?;\n                PValue::String(val.to_string())\n            }\n            Type::INET => {\n                let val: cidr::IpInet = FromSql::from_sql(ty, raw)?;\n                PValue::String(val.to_string())\n            }\n\n            _ => {\n                if let Kind::Array(_) = ty.kind() {\n                    let val: Vec<_> = FromSql::from_sql(ty, raw)?;\n                    PValue::Array(val)\n                } else if let Kind::Enum(_) = ty.kind() {\n                    let val = std::str::from_utf8(raw)?;\n                    PValue::String(val.to_string())\n                } else if is_pgvector(ty) {\n                    let val: pgvector::Vector = FromSql::from_sql(ty, raw)?;\n                    let arr = val\n                        .as_slice()\n                        .iter()\n                        .map(|n| match serde_json::Number::from_f64(*n as f64) {\n                            Some(num) => PValue::Number(num),\n                            None => PValue::Null,\n                        })\n                        .collect();\n                    PValue::Array(arr)\n                } else {\n                    return Err(format!(\"unsupported type: {ty:?}\").into());\n                }\n            }\n        })\n    }\n\n    fn from_sql_null(_: &Type) -> Result<Self, Box<dyn Error + Sync + Send>> {\n        Ok(PValue::Null)\n    }\n\n    fn accepts(ty: &Type) -> bool {\n        matches!(\n            *ty,\n            Type::BOOL\n                | Type::TEXT\n                | Type::VARCHAR\n                | Type::INT2\n                | Type::INT4\n                | Type::INT8\n                | Type::OID\n                | Type::JSONB\n                | Type::JSON\n                | Type::FLOAT4\n                | Type::FLOAT8\n                | Type::TIMESTAMP\n                | Type::TIMESTAMPTZ\n                | Type::DATE\n                | Type::TIME\n                | Type::CIDR\n                | Type::INET\n                | Type::NAME\n                | Type::NUMERIC\n        ) || matches!(ty.kind(), Kind::Enum(_))\n            || matches!(ty.kind(), Kind::Array(ty) if <PValue as FromSql>::accepts(ty))\n            || is_pgvector(ty)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use bytes::BytesMut;\n    use serde_json::json;\n    use tokio_postgres::types::Type;\n\n    #[test]\n    fn test_rowvalue_to_sql_bytes() {\n        let value = RowValue::Bytes(vec![1, 2, 3]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::BYTEA, &mut buf);\n        assert!(result.is_ok());\n        assert_eq!(buf, vec![1, 2, 3]);\n    }\n\n    #[test]\n    fn test_rowvalue_to_sql_uuid() {\n        let uuid = Uuid::nil();\n        let value = RowValue::Uuid(uuid);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::UUID, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_rowvalue_to_sql_pval() {\n        let value = RowValue::PVal(PValue::String(\"test\".to_string()));\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::TEXT, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_pvalue_to_sql_json() {\n        let value: PValue = json!({\"key\": \"value\"}).into();\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::JSONB, &mut buf);\n        assert!(result.is_ok());\n        assert_eq!(buf[0], 1); // JSONB prefix\n    }\n\n    #[test]\n    fn test_pvalue_to_sql_number() {\n        let value = PValue::Number(serde_json::Number::from(42));\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::INT4, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_rowvalue_from_sql_bytes() {\n        let raw = &[1, 2, 3];\n        let result = RowValue::from_sql(&Type::BYTEA, raw);\n        assert!(result.is_ok());\n        if let RowValue::Bytes(val) = result.unwrap() {\n            assert_eq!(val, vec![1, 2, 3]);\n        } else {\n            panic!(\"Expected RowValue::Bytes\");\n        }\n    }\n\n    #[test]\n    fn test_rowvalue_from_sql_uuid() {\n        let uuid = Uuid::nil();\n        let raw = uuid.as_bytes();\n        let result = RowValue::from_sql(&Type::UUID, raw);\n        assert!(result.is_ok());\n        if let RowValue::Uuid(val) = result.unwrap() {\n            assert_eq!(val, uuid);\n        } else {\n            panic!(\"Expected RowValue::Uuid\");\n        }\n    }\n\n    #[test]\n    fn test_rowvalue_to_sql_cidr() {\n        let cidr = cidr::IpCidr::new(std::net::Ipv4Addr::new(192, 168, 0, 0).into(), 16).unwrap();\n        let value = RowValue::Cidr(cidr);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::CIDR, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_rowvalue_from_sql_cidr() {\n        let cidr = cidr::IpCidr::new(std::net::Ipv4Addr::new(192, 168, 0, 0).into(), 16).unwrap();\n        let mut buf = BytesMut::new();\n        cidr.to_sql(&Type::CIDR, &mut buf).unwrap();\n        let result = RowValue::from_sql(&Type::CIDR, &buf);\n        assert!(result.is_ok());\n        if let RowValue::Cidr(val) = result.unwrap() {\n            assert_eq!(val, cidr);\n        } else {\n            panic!(\"Expected RowValue::Cidr\");\n        }\n    }\n\n    #[test]\n    fn test_rowvalue_to_sql_inet() {\n        let inet = cidr::IpInet::new_host(std::net::Ipv4Addr::new(192, 168, 1, 1).into());\n        let value = RowValue::Inet(inet);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::INET, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_rowvalue_from_sql_inet() {\n        let inet = cidr::IpInet::new_host(std::net::Ipv4Addr::new(192, 168, 1, 1).into());\n        let mut buf = BytesMut::new();\n        inet.to_sql(&Type::INET, &mut buf).unwrap();\n        let result = RowValue::from_sql(&Type::INET, &buf);\n        assert!(result.is_ok());\n        if let RowValue::Inet(val) = result.unwrap() {\n            assert_eq!(val, inet);\n        } else {\n            panic!(\"Expected RowValue::Inet\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_bool() {\n        let raw = &[1]; // true\n        let result = PValue::from_sql(&Type::BOOL, raw);\n        assert!(result.is_ok());\n        if let PValue::Bool(val) = result.unwrap() {\n            assert!(val);\n        } else {\n            panic!(\"Expected PValue::Bool\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_string() {\n        let raw = b\"test\";\n        let result = PValue::from_sql(&Type::TEXT, raw);\n        assert!(result.is_ok());\n        if let PValue::String(val) = result.unwrap() {\n            assert_eq!(val, \"test\");\n        } else {\n            panic!(\"Expected PValue::String\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_name() {\n        let raw = b\"test\";\n        let result = PValue::from_sql(&Type::NAME, raw);\n        assert!(result.is_ok());\n        if let PValue::String(val) = result.unwrap() {\n            assert_eq!(val, \"test\");\n        } else {\n            panic!(\"Expected PValue::String\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_number() {\n        let raw = &[0, 0, 0, 42]; // INT4 representation of 42\n        let result = PValue::from_sql(&Type::INT4, raw);\n        assert!(result.is_ok());\n        if let PValue::Number(val) = result.unwrap() {\n            assert_eq!(val.as_i64().unwrap(), 42);\n        } else {\n            panic!(\"Expected PValue::Number\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_to_sql_invalid_type() {\n        let value = PValue::String(\"test\".to_string());\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::BOOL, &mut buf); // Invalid type\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_rowvalue_from_sql_invalid_type() {\n        let raw = &[1, 2, 3];\n        let result = RowValue::from_sql(&Type::BOOL, raw); // Invalid type\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_null() {\n        let result = PValue::from_sql_null(&Type::TEXT);\n        assert!(result.is_ok());\n        if let PValue::Null = result.unwrap() {\n            // Expected null value\n        } else {\n            panic!(\"Expected PValue::Null\");\n        }\n    }\n\n    #[test]\n    fn test_rowvalue_from_sql_null() {\n        let result = RowValue::from_sql_null(&Type::TEXT);\n        assert!(result.is_ok());\n        if let RowValue::PVal(PValue::Null) = result.unwrap() {\n            // Expected null value\n        } else {\n            panic!(\"Expected RowValue::PVal(PValue::Null)\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_to_sql_array() {\n        let value = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from(1)),\n            PValue::Number(serde_json::Number::from(2)),\n        ]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&Type::INT4_ARRAY, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_array() {\n        // raw representation of INT4_ARRAY with values 1,2,3\n        let raw: &[u8] = &[\n            0, 0, 0, 1, // dimentions\n            0, 0, 0, 0, // has nulls\n            0, 0, 0, 23, // element type\n            0, 0, 0, 3, // array length\n            0, 0, 0, 1, // lower bound\n            0, 0, 0, 4, // value length\n            0, 0, 0, 1, // value\n            0, 0, 0, 4, // value length\n            0, 0, 0, 2, // value\n            0, 0, 0, 4, // value length\n            0, 0, 0, 3, // value\n        ];\n\n        let result = PValue::from_sql(&Type::INT4_ARRAY, raw);\n        assert!(result.is_ok());\n        if let PValue::Array(val) = result.unwrap() {\n            assert_eq!(val.len(), 3);\n            assert_eq!(\n                val,\n                vec![\n                    PValue::Number(1.into()),\n                    PValue::Number(2.into()),\n                    PValue::Number(3.into())\n                ]\n            );\n        } else {\n            panic!(\"Expected PValue::Array\");\n        }\n    }\n\n    #[test]\n    fn test_pvalue_to_sql_pgvector_from_string() {\n        // Create a mock vector type\n        let vector_type = Type::new(\"vector\".to_string(), 0, Kind::Simple, \"\".to_string());\n\n        // Test valid vector string\n        let value = PValue::String(\"[1.0, 2.0, 3.0]\".to_string());\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_ok());\n\n        // Test invalid vector string\n        let value = PValue::String(\"invalid\".to_string());\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_pvalue_to_sql_pgvector_from_array() {\n        // Create a mock vector type\n        let vector_type = Type::new(\"vector\".to_string(), 0, Kind::Simple, \"\".to_string());\n\n        // Test valid array of numbers\n        let value = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from_f64(1.0).unwrap()),\n            PValue::Number(serde_json::Number::from_f64(2.5).unwrap()),\n            PValue::Number(serde_json::Number::from_f64(3.75).unwrap()),\n        ]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_ok());\n\n        // Test array with integer numbers (should convert to f32)\n        let value = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from(1)),\n            PValue::Number(serde_json::Number::from(2)),\n            PValue::Number(serde_json::Number::from(3)),\n        ]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_ok());\n\n        // Test array with non-number elements\n        let value = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from(1)),\n            PValue::String(\"not a number\".to_string()),\n        ]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_err());\n\n        // Test array with null\n        let value = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from(1)),\n            PValue::Null,\n        ]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn test_pvalue_from_sql_pgvector() {\n        // Create a mock vector type\n        let vector_type = Type::new(\"vector\".to_string(), 0, Kind::Simple, \"\".to_string());\n\n        // Create a pgvector and serialize it\n        let vector = pgvector::Vector::from(vec![1.0f32, 2.5f32, 3.75f32]);\n        let mut buf = BytesMut::new();\n        vector.to_sql(&vector_type, &mut buf).unwrap();\n\n        // Test deserialization\n        let result = PValue::from_sql(&vector_type, &buf);\n        assert!(result.is_ok());\n\n        if let PValue::Array(arr) = result.unwrap() {\n            assert_eq!(arr.len(), 3);\n\n            // Check values (allowing for float precision)\n            if let PValue::Number(n) = &arr[0] {\n                assert_eq!(n.as_f64().unwrap(), 1.0);\n            } else {\n                panic!(\"Expected PValue::Number\");\n            }\n\n            if let PValue::Number(n) = &arr[1] {\n                assert_eq!(n.as_f64().unwrap(), 2.5);\n            } else {\n                panic!(\"Expected PValue::Number\");\n            }\n\n            if let PValue::Number(n) = &arr[2] {\n                assert_eq!(n.as_f64().unwrap(), 3.75);\n            } else {\n                panic!(\"Expected PValue::Number\");\n            }\n        } else {\n            panic!(\"Expected PValue::Array\");\n        }\n    }\n\n    #[test]\n    fn test_pgvector_roundtrip() {\n        // Create a mock vector type\n        let vector_type = Type::new(\"vector\".to_string(), 0, Kind::Simple, \"\".to_string());\n\n        // Test roundtrip with array input\n        let original = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from_f64(1.1).unwrap()),\n            PValue::Number(serde_json::Number::from_f64(2.2).unwrap()),\n            PValue::Number(serde_json::Number::from_f64(3.3).unwrap()),\n        ]);\n\n        // Serialize\n        let mut buf = BytesMut::new();\n        original.to_sql(&vector_type, &mut buf).unwrap();\n\n        // Deserialize\n        let deserialized = PValue::from_sql(&vector_type, &buf).unwrap();\n\n        // Verify\n        if let PValue::Array(arr) = deserialized {\n            assert_eq!(arr.len(), 3);\n            if let (PValue::Number(n1), PValue::Number(n2), PValue::Number(n3)) =\n                (&arr[0], &arr[1], &arr[2])\n            {\n                assert!((n1.as_f64().unwrap() - 1.1).abs() < 0.01);\n                assert!((n2.as_f64().unwrap() - 2.2).abs() < 0.01);\n                assert!((n3.as_f64().unwrap() - 3.3).abs() < 0.01);\n            } else {\n                panic!(\"Expected all elements to be numbers\");\n            }\n        } else {\n            panic!(\"Expected PValue::Array\");\n        }\n    }\n\n    #[test]\n    fn test_pgvector_edge_cases() {\n        // Create a mock vector type\n        let vector_type = Type::new(\"vector\".to_string(), 0, Kind::Simple, \"\".to_string());\n\n        // Test empty vector\n        let value = PValue::Array(vec![]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_ok());\n\n        // Test single element vector\n        let value = PValue::Array(vec![PValue::Number(\n            serde_json::Number::from_f64(42.0).unwrap(),\n        )]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_ok());\n\n        // Test with special float values\n        let value = PValue::Array(vec![\n            PValue::Number(serde_json::Number::from_f64(0.0).unwrap()),\n            PValue::Number(serde_json::Number::from_f64(-1.0).unwrap()),\n            PValue::Number(serde_json::Number::from_f64(f64::MAX).unwrap()),\n        ]);\n        let mut buf = BytesMut::new();\n        let result = value.to_sql(&vector_type, &mut buf);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_pgvector_nan_infinity_handling() {\n        // Create a mock vector type\n        let vector_type = Type::new(\"vector\".to_string(), 0, Kind::Simple, \"\".to_string());\n\n        // Test that NaN values result in PValue::Null when deserializing\n        let vector = pgvector::Vector::from(vec![1.0f32, f32::NAN, 3.0f32]);\n        let mut buf = BytesMut::new();\n        vector.to_sql(&vector_type, &mut buf).unwrap();\n\n        let result = PValue::from_sql(&vector_type, &buf).unwrap();\n        if let PValue::Array(arr) = result {\n            assert_eq!(arr.len(), 3);\n            assert!(matches!(&arr[0], PValue::Number(_)));\n            assert!(matches!(&arr[1], PValue::Null)); // NaN becomes Null\n            assert!(matches!(&arr[2], PValue::Number(_)));\n        } else {\n            panic!(\"Expected PValue::Array\");\n        }\n\n        // Test positive infinity\n        let vector = pgvector::Vector::from(vec![f32::INFINITY]);\n        let mut buf = BytesMut::new();\n        vector.to_sql(&vector_type, &mut buf).unwrap();\n\n        let result = PValue::from_sql(&vector_type, &buf).unwrap();\n        if let PValue::Array(arr) = result {\n            assert_eq!(arr.len(), 1);\n            assert!(matches!(&arr[0], PValue::Null)); // Infinity becomes Null\n        } else {\n            panic!(\"Expected PValue::Array\");\n        }\n\n        // Test negative infinity\n        let vector = pgvector::Vector::from(vec![f32::NEG_INFINITY]);\n        let mut buf = BytesMut::new();\n        vector.to_sql(&vector_type, &mut buf).unwrap();\n\n        let result = PValue::from_sql(&vector_type, &buf).unwrap();\n        if let PValue::Array(arr) = result {\n            assert_eq!(arr.len(), 1);\n            assert!(matches!(&arr[0], PValue::Null)); // Negative infinity becomes Null\n        } else {\n            panic!(\"Expected PValue::Array\");\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/trace/eventbuf.rs",
    "content": "#![allow(dead_code)]\n\nuse crate::{api, error::StackTrace};\n\nuse bytes::{BufMut, Bytes, BytesMut};\n\n/// A buffer for encoding trace events.\npub struct EventBuffer {\n    scratch: [u8; 10],\n    buf: BytesMut,\n}\n\nimpl AsRef<[u8]> for EventBuffer {\n    fn as_ref(&self) -> &[u8] {\n        &self.buf\n    }\n}\n\nimpl EventBuffer {\n    pub fn with_capacity(size: usize) -> Self {\n        EventBuffer {\n            scratch: [0; 10],\n            buf: BytesMut::with_capacity(size),\n        }\n    }\n\n    pub(super) fn freeze(self) -> Bytes {\n        self.buf.freeze()\n    }\n\n    /// Writes a single byte.\n    #[inline]\n    pub fn byte(&mut self, byte: u8) {\n        self.buf.reserve(1);\n        self.buf.put_u8(byte);\n    }\n\n    /// Writes a known number of bytes.\n    #[inline]\n    pub fn bytes<const N: usize>(&mut self, bytes: &[u8; N]) {\n        self.buf.reserve(N);\n        self.buf.put_slice(bytes);\n    }\n\n    /// Ensures the buffer has enough capacity for `additional` bytes.\n    /// Used to avoid additional allocations.\n    #[inline]\n    pub fn reserve(&mut self, additional: usize) {\n        self.buf.reserve(additional);\n    }\n\n    /// Writes a variable-length string.\n    #[inline]\n    pub fn str<S: AsRef<str>>(&mut self, str: S) {\n        self.byte_string(str.as_ref().as_bytes());\n    }\n\n    /// Writes a variable-length byte string.\n    #[inline]\n    pub fn byte_string(&mut self, bytes: &[u8]) {\n        // 10 bytes is the maximum length of a uvarint.\n        self.buf.reserve(10 + bytes.len());\n\n        self.uvarint(bytes.len() as u64);\n        self.buf.extend_from_slice(bytes);\n    }\n\n    /// Writes a variable-length truncated byte string.\n    /// If truncation is necessary, the `truncation_suffix` is appended to the end of the string,\n    /// leading to the final length being `max_len + truncation_suffix.len()`.\n    #[inline]\n    pub fn truncated_byte_string(\n        &mut self,\n        bytes: &[u8],\n        max_len: usize,\n        truncation_suffix: &[u8],\n    ) {\n        if bytes.len() <= max_len {\n            self.byte_string(bytes);\n        } else {\n            let combined_len = max_len + truncation_suffix.len();\n            self.uvarint(combined_len as u64);\n            self.buf.reserve(combined_len);\n            self.buf.put_slice(&bytes[..max_len]);\n            self.buf.put_slice(truncation_suffix);\n        }\n    }\n\n    /// Writes a single boolean bit.\n    #[inline]\n    pub fn bool(&mut self, b: bool) {\n        self.byte(if b { 1 } else { 0 });\n    }\n\n    /// Writes the current system time.\n    #[inline]\n    pub fn system_time_now(&mut self) {\n        self.system_time(std::time::SystemTime::now());\n    }\n\n    /// Writes a system time.\n    #[inline]\n    pub fn system_time(&mut self, time: std::time::SystemTime) {\n        let duration = time\n            .duration_since(std::time::UNIX_EPOCH)\n            .expect(\"time is before UNIX_EPOCH\");\n        self.buf.reserve(8 + 4);\n        self.i64(duration.as_secs() as i64);\n        self.i32(duration.subsec_nanos() as i32);\n    }\n\n    /// Writes an UTC timestamp.\n    #[inline]\n    pub fn time(&mut self, time: &chrono::DateTime<chrono::Utc>) {\n        self.buf.reserve(8 + 4);\n        self.i64(time.timestamp());\n        self.i32(time.timestamp_subsec_nanos() as i32);\n    }\n\n    /// Writes a variable-length signed integer.\n    #[inline]\n    pub fn ivarint<I: Into<i64>>(&mut self, i: I) {\n        self.uvarint(signed_to_unsigned_i64(i.into()));\n    }\n\n    /// Writes a variable-length unsigned integer.\n    #[inline]\n    pub fn uvarint<U: Into<u64>>(&mut self, u: U) {\n        let mut u: u64 = u.into();\n        let mut i = 0;\n        while u >= 0x80 {\n            self.scratch[i] = (u as u8) | 0x80;\n            u >>= 7;\n            i += 1;\n        }\n        self.scratch[i] = u as u8;\n        i += 1;\n        self.buf.extend_from_slice(&self.scratch[..i]);\n    }\n\n    /// Writes a float, always as 8 bytes.\n    #[inline]\n    pub fn f64(&mut self, f: f64) {\n        let data: [u8; 8] = f.to_le_bytes();\n        self.buf.extend_from_slice(&data);\n    }\n\n    /// Writes a signed integer, always as 8 bytes.\n    #[inline]\n    pub fn i64(&mut self, i: i64) {\n        self.u64(signed_to_unsigned_i64(i));\n    }\n\n    /// Writes an unsigned integer, always as 8 bytes.\n    #[inline]\n    pub fn u64(&mut self, u: u64) {\n        let data: [u8; 8] = u.to_le_bytes();\n        self.buf.extend_from_slice(&data);\n    }\n\n    /// Writes a signed integer, always as 4 bytes.\n    #[inline]\n    pub fn i32(&mut self, i: i32) {\n        self.u32(signed_to_unsigned_i32(i));\n    }\n\n    /// Writes an unsigned integer, always as 4 bytes.\n    #[inline]\n    pub fn u32(&mut self, u: u32) {\n        let data: [u8; 4] = u.to_le_bytes();\n        self.buf.extend_from_slice(&data);\n    }\n\n    /// Writes a duration.\n    #[inline]\n    pub fn duration(&mut self, duration: std::time::Duration) {\n        // The current trace protocol only supports durations that fit in an i64.\n        // If the duration exceeds that, truncate it to the maximum value.\n        //\n        // Note: Rust's duration type is for positive durations only, so we only consider\n        // the positive range of i64 here.\n        let nanos = duration.as_nanos();\n        let nanos: i64 = if nanos > i64::MAX as u128 {\n            i64::MAX\n        } else {\n            nanos as i64\n        };\n        self.ivarint(nanos);\n    }\n\n    #[inline]\n    pub fn api_err_with_legacy_stack(&mut self, err: Option<&api::Error>) {\n        match err {\n            Some(err) => {\n                let err = serde_json::to_string_pretty(err).unwrap_or(\"unknown error\".to_string());\n                self.str(err);\n                self.nyi_stack_pcs()\n            }\n            None => self.str(\"\"),\n        }\n    }\n\n    #[inline]\n    pub fn err_with_legacy_stack<E: std::fmt::Display>(&mut self, err: Option<&E>) {\n        match err {\n            Some(err) => {\n                let err = err.to_string();\n                self.str(&err);\n                self.nyi_stack_pcs()\n            }\n            None => self.str(\"\"),\n        }\n    }\n\n    /// Adds the stack pcs to the buffer.\n    /// It's not supported for the new runtime yet, so it just writes 0 frames.\n    #[inline]\n    pub fn nyi_stack_pcs(&mut self) {\n        self.byte(0);\n    }\n\n    #[inline]\n    pub fn formatted_stack(&mut self, stack: Option<&StackTrace>) {\n        match stack {\n            Some(frames) => {\n                self.byte(frames.len().min(256) as u8);\n                for frame in frames.iter().take(256) {\n                    self.str(&frame.file);\n                    self.uvarint(frame.line);\n                    self.str(frame.function.as_deref().unwrap_or(\"unknown\"));\n                }\n            }\n            None => self.byte(0),\n        }\n    }\n}\n\n#[inline]\npub(super) fn signed_to_unsigned_i64(i: i64) -> u64 {\n    if i < 0 {\n        ((!(i as u64)) << 1) | 1 // complement i, bit 0 is 1\n    } else {\n        (i as u64) << 1 // do not complement i, bit 0 is 0\n    }\n}\n\n#[inline]\npub(super) fn signed_to_unsigned_i32(i: i32) -> u32 {\n    if i < 0 {\n        ((!(i as u32)) << 1) | 1 // complement i, bit 0 is 1\n    } else {\n        (i as u32) << 1 // do not complement i, bit 0 is 0\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/trace/log.rs",
    "content": "use std::collections::HashMap;\nuse std::sync::Arc;\n\nuse crate::api::reqauth::platform;\nuse crate::encore::runtime::v1 as runtimepb;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse tokio::sync::mpsc::UnboundedReceiver;\nuse tokio_stream::wrappers::UnboundedReceiverStream;\n\nuse crate::model;\nuse crate::trace::eventbuf::signed_to_unsigned_i64;\nuse crate::trace::protocol::{EventType, TRACE_VERSION};\nuse crate::trace::time_anchor::TimeAnchor;\nuse crate::trace::Tracer;\n\npub struct ReporterConfig {\n    pub app_id: String,\n    pub env_id: String,\n    pub deploy_id: String,\n    pub app_commit: String,\n    pub trace_endpoint: reqwest::Url,\n    pub trace_sampling_config: TraceSamplingConfig,\n    pub platform_validator: Arc<platform::RequestValidator>,\n}\n\n#[derive(Debug, Clone)]\npub struct TraceSamplingConfig {\n    inner: Option<Arc<SamplingRates>>,\n}\n\n#[derive(Debug)]\nstruct SamplingRates {\n    /// Rates keyed by \"service.endpoint\" for API endpoints.\n    endpoint: HashMap<String, f64>,\n    /// Rates keyed by service name for API services.\n    service: HashMap<String, f64>,\n    /// Rates keyed by \"topic.subscription\" for pubsub subscriptions.\n    subscription: HashMap<String, f64>,\n    /// Rates keyed by topic name for pubsub topics.\n    topic: HashMap<String, f64>,\n    /// Default rate.\n    default: Option<f64>,\n}\n\nuse runtimepb::tracing_provider::sampling_config::Scope;\n\nimpl TraceSamplingConfig {\n    pub fn new(\n        config: Vec<runtimepb::tracing_provider::SamplingConfig>,\n        legacy_rate: Option<f64>,\n    ) -> Self {\n        if config.is_empty() && legacy_rate.is_none() {\n            return Self { inner: None };\n        }\n\n        let mut default_rate = None;\n        let mut endpoint = HashMap::new();\n        let mut service = HashMap::new();\n        let mut subscription = HashMap::new();\n        let mut topic = HashMap::new();\n\n        for entry in &config {\n            let rate = entry.rate;\n            match &entry.scope {\n                Some(Scope::Default(_)) => default_rate = Some(rate),\n                Some(Scope::Service(s)) => {\n                    service.insert(s.clone(), rate);\n                }\n                Some(Scope::Endpoint(ep)) => {\n                    let key = format!(\"{}.{}\", ep.service, ep.endpoint);\n                    endpoint.insert(key, rate);\n                }\n                Some(Scope::Topic(t)) => {\n                    topic.insert(t.clone(), rate);\n                }\n                Some(Scope::PubsubSubscription(sub)) => {\n                    let key = format!(\"{}.{}\", sub.topic, sub.subscription);\n                    subscription.insert(key, rate);\n                }\n                None => {}\n            }\n        }\n\n        // Backward compat: use deprecated sampling_rate as default.\n        if config.is_empty() {\n            if let Some(rate) = legacy_rate {\n                default_rate = Some(rate);\n            }\n        }\n\n        if default_rate.is_none()\n            && endpoint.is_empty()\n            && service.is_empty()\n            && subscription.is_empty()\n            && topic.is_empty()\n        {\n            return Self { inner: None };\n        }\n\n        Self {\n            inner: Some(Arc::new(SamplingRates {\n                endpoint,\n                service,\n                subscription,\n                topic,\n                default: default_rate,\n            })),\n        }\n    }\n\n    /// Look up the sampling rate for an API endpoint.\n    /// Falls back: endpoint → service → default.\n    /// Returns None if no config matches (meaning: always sample).\n    pub fn lookup_api(&self, name: &crate::EndpointName) -> Option<f64> {\n        let rates = self.inner.as_ref()?;\n        rates\n            .endpoint\n            .get(&**name)\n            .or_else(|| rates.service.get(name.service()))\n            .or_else(|| rates.default.as_ref())\n            .copied()\n    }\n\n    /// Look up the sampling rate for a pubsub subscription.\n    /// Falls back: subscription → topic → service → default.\n    /// Returns None if no config matches (meaning: always sample).\n    pub fn lookup_pubsub(&self, service: &str, topic: &str, subscription: &str) -> Option<f64> {\n        let rates = self.inner.as_ref()?;\n        let key = format!(\"{}.{}\", topic, subscription);\n        rates\n            .subscription\n            .get(&key)\n            .or_else(|| rates.topic.get(topic))\n            .or_else(|| rates.service.get(service))\n            .or_else(|| rates.default.as_ref())\n            .copied()\n    }\n\n    /// Look up the default sampling rate.\n    /// Returns None if no default is configured (meaning: always sample).\n    pub fn lookup_default(&self) -> Option<f64> {\n        let rates = self.inner.as_ref()?;\n        rates.default\n    }\n}\n\n/// Sends traces to the trace server.\n#[must_use]\npub struct Reporter {\n    rx: tokio::sync::mpsc::UnboundedReceiver<TraceEvent>,\n    anchor: TimeAnchor,\n    http_client: reqwest::Client,\n    config: ReporterConfig,\n}\n\npub fn streaming_tracer(\n    http_client: reqwest::Client,\n    config: ReporterConfig,\n) -> (Tracer, Reporter) {\n    let (tx, rx) = tokio::sync::mpsc::unbounded_channel();\n    let tracer = Tracer::new(tx, config.trace_sampling_config.clone());\n\n    let anchor = TimeAnchor::new();\n    let reporter = Reporter {\n        rx,\n        anchor,\n        http_client,\n        config,\n    };\n    (tracer, reporter)\n}\n\n#[derive(Debug)]\npub(super) struct TraceEvent {\n    pub typ: EventType,\n    pub id: model::TraceEventId,\n    pub data: Bytes,\n    pub span: model::SpanKey,\n    pub ts: tokio::time::Instant,\n}\n\nimpl Reporter {\n    /// Starts reporting trace events to the trace server.\n    ///\n    /// This method runs in an infinite loop until all senders are dropped,\n    /// continuously collecting trace events and sending them to the trace server.\n    pub async fn start_reporting(mut self) {\n        let trace_headers = match self.create_trace_headers(self.anchor.trace_header().as_str()) {\n            Ok(trace_headers) => trace_headers,\n            Err(err) => {\n                log::error!(\"couldn't setup headers for tracing requests, exiting. Error: {err}\");\n                return;\n            }\n        };\n\n        loop {\n            let mut body_sender = None;\n\n            let timeout_duration = std::time::Duration::from_millis(1000);\n            let mut no_data_timeout = Box::pin(tokio::time::sleep(timeout_duration));\n\n            loop {\n                tokio::select! {\n                    event = self.rx.recv() => {\n                        match event {\n                            Some(event) => {\n                                // Wait for at least one entry on rx before we open a HTTP request.\n                                if body_sender.is_none() {\n                                    match self.setup_trace_request(&trace_headers) {\n                                        Ok(sender) => body_sender = Some(sender),\n                                        Err(err) => {\n                                            log::error!(\"failed to create request: {err}\");\n                                            break;\n                                        }\n                                    }\n                                }\n\n                                // Add the event to the stream\n                                if let Some(sender) = &body_sender {\n                                    if let Err(err) = sender.send(event) {\n                                        log::error!(\"failed to stream event: {err}\");\n                                        break;\n                                    }\n                                }\n\n                                // Reset the timeout\n                                no_data_timeout = Box::pin(tokio::time::sleep(timeout_duration));\n                            }\n                            None => {\n                                // The stream is closed. This only happens if all senders have been dropped,\n                                // which should never happen in regular use.\n                                return;\n                            }\n                        }\n                    }\n                    _ = &mut no_data_timeout => {\n                        // Timeout reached with no new events\n                        if let Some(sender) = body_sender {\n                            // Close the stream and wait for a new event\n                            drop(sender);\n                        }\n                        break;\n                    }\n                }\n            }\n        }\n    }\n\n    fn create_body_stream(\n        &self,\n        rx: UnboundedReceiver<TraceEvent>,\n    ) -> impl futures::Stream<Item = Result<Bytes, std::io::Error>> {\n        let anchor = self.anchor.clone();\n        UnboundedReceiverStream::new(rx).flat_map(move |event| {\n            let streaming_event = StreamingTraceEvent { event };\n            futures::stream::iter(vec![\n                Ok::<_, std::io::Error>(streaming_event.header(&anchor)),\n                Ok::<_, std::io::Error>(streaming_event.event.data),\n            ])\n        })\n    }\n\n    fn setup_trace_request(\n        &self,\n        trace_headers: &reqwest::header::HeaderMap,\n    ) -> Result<tokio::sync::mpsc::UnboundedSender<TraceEvent>, String> {\n        {\n            let http_client: &reqwest::Client = &self.http_client;\n            let endpoint: &reqwest::Url = &self.config.trace_endpoint;\n            let validator: &Arc<platform::RequestValidator> = &self.config.platform_validator;\n            // Create a channel for the streaming body\n            let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<TraceEvent>();\n\n            let mut req = match http_client\n                .post(endpoint.clone())\n                .headers(trace_headers.clone())\n                .body(reqwest::Body::wrap_stream(self.create_body_stream(rx)))\n                .build()\n            {\n                Ok(req) => req,\n                Err(err) => {\n                    return Err(format!(\"failed to build trace request: {err:?}\"));\n                }\n            };\n\n            if let Err(err) = validator.sign_outgoing_request(&mut req) {\n                return Err(format!(\"failed to sign trace request: {err:?}\"));\n            }\n\n            // Start the request\n            let request = http_client.execute(req);\n            tokio::spawn(async move {\n                match request.await {\n                    Ok(resp) if !resp.status().is_success() => {\n                        let status = resp.status();\n                        let body = resp.text().await.unwrap_or_else(|_| String::new());\n                        log::error!(\"failed to send trace: HTTP {}: {}\", status, body);\n                    }\n                    Err(err) => {\n                        log::error!(\"failed to send trace: {}\", err);\n                    }\n                    _ => {}\n                }\n            });\n\n            Ok(tx)\n        }\n    }\n\n    fn create_trace_headers(\n        &self,\n        trace_time_anchor: &str,\n    ) -> Result<reqwest::header::HeaderMap, reqwest::header::InvalidHeaderValue> {\n        use reqwest::header::*;\n        let mut headers = HeaderMap::new();\n\n        headers.insert(\n            \"X-Encore-App-Id\",\n            HeaderValue::from_str(&self.config.app_id)?,\n        );\n        headers.insert(\n            \"X-Encore-Env-Id\",\n            HeaderValue::from_str(&self.config.env_id)?,\n        );\n        headers.insert(\n            \"X-Encore-Deploy-Id\",\n            HeaderValue::from_str(&self.config.deploy_id)?,\n        );\n        headers.insert(\n            \"X-Encore-App-Commit\",\n            HeaderValue::from_str(&self.config.app_commit)?,\n        );\n        headers.insert(\"X-Encore-Trace-Version\", HeaderValue::from(TRACE_VERSION));\n        headers.insert(\n            \"X-Encore-Trace-TimeAnchor\",\n            HeaderValue::from_str(trace_time_anchor)?,\n        );\n\n        Ok(headers)\n    }\n}\n\n/// Represents a trace event that is being streamed.\n#[derive(Debug)]\nstruct StreamingTraceEvent {\n    /// The event itself.\n    event: TraceEvent,\n}\n\nimpl StreamingTraceEvent {\n    fn header(&self, anchor: &TimeAnchor) -> Bytes {\n        let event_type = self.event.typ;\n        let event_id = self.event.id.0;\n        let trace_id = &self.event.span.0 .0;\n        let span_id = &self.event.span.1 .0;\n        let ln = self.event.data.len();\n\n        // Compute the timestamp, relative to the anchor's timestamp.\n        let ts = self\n            .event\n            .ts\n            .saturating_duration_since(anchor.instant)\n            .as_nanos() as i64;\n        let ts = signed_to_unsigned_i64(ts);\n\n        Bytes::from(vec![\n            // Event type, 1 byte\n            event_type as u8,\n            // Event ID, 8 bytes\n            event_id as u8,\n            (event_id >> 8) as u8,\n            (event_id >> 16) as u8,\n            (event_id >> 24) as u8,\n            (event_id >> 32) as u8,\n            (event_id >> 40) as u8,\n            (event_id >> 48) as u8,\n            (event_id >> 56) as u8,\n            // Timestamp, 8 bytes\n            ts as u8,\n            (ts >> 8) as u8,\n            (ts >> 16) as u8,\n            (ts >> 24) as u8,\n            (ts >> 32) as u8,\n            (ts >> 40) as u8,\n            (ts >> 48) as u8,\n            (ts >> 56) as u8,\n            // Trace ID, 16 bytes\n            trace_id[0],\n            trace_id[1],\n            trace_id[2],\n            trace_id[3],\n            trace_id[4],\n            trace_id[5],\n            trace_id[6],\n            trace_id[7],\n            trace_id[8],\n            trace_id[9],\n            trace_id[10],\n            trace_id[11],\n            trace_id[12],\n            trace_id[13],\n            trace_id[14],\n            trace_id[15],\n            // Span ID, 8 bytes\n            span_id[0],\n            span_id[1],\n            span_id[2],\n            span_id[3],\n            span_id[4],\n            span_id[5],\n            span_id[6],\n            span_id[7],\n            // Event data length, 4 bytes\n            ln as u8,\n            (ln >> 8) as u8,\n            (ln >> 16) as u8,\n            (ln >> 24) as u8,\n        ])\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::api::reqauth::platform::RequestValidator;\n\n    use tokio_stream::StreamExt;\n    use url::Url;\n\n    fn setup_reporter() -> Reporter {\n        let (_tx, rx) = tokio::sync::mpsc::unbounded_channel();\n\n        Reporter {\n            rx,\n            anchor: TimeAnchor::new(),\n            http_client: reqwest::Client::new(),\n            config: ReporterConfig {\n                app_id: \"test-app\".to_string(),\n                env_id: \"test-env\".to_string(),\n                deploy_id: \"test-deploy\".to_string(),\n                app_commit: \"test-commit\".to_string(),\n                trace_endpoint: Url::parse(\"http://localhost:8080\").unwrap(),\n                trace_sampling_config: TraceSamplingConfig::new(vec![], None),\n                platform_validator: RequestValidator::new_mock().into(),\n            },\n        }\n    }\n\n    #[tokio::test]\n    async fn test_event_to_stream_empty_payload() {\n        let reporter = setup_reporter();\n        let (body_tx, body_rx) = tokio::sync::mpsc::unbounded_channel();\n        let mut body_stream = Box::pin(reporter.create_body_stream(body_rx));\n\n        // Create an event with empty data payload\n        let event = TraceEvent {\n            typ: EventType::LogMessage,\n            id: model::TraceEventId(100),\n            data: Bytes::from(vec![]), // Empty payload\n            span: model::SpanKey(model::TraceId([5; 16]), model::SpanId([6; 8])),\n            ts: tokio::time::Instant::now(),\n        };\n\n        // Send the event with empty payload\n        body_tx\n            .send(event)\n            .expect(\"Failed to send event with empty payload\");\n\n        // Verify header was received\n        let header = body_stream\n            .next()\n            .await\n            .expect(\"Failed to receive header for empty payload\")\n            .expect(\"Header should be Ok\");\n        assert_eq!(header[0], EventType::LogMessage as u8);\n\n        // Verify data length in header is zero\n        let data_length = (header[41] as u32)\n            | ((header[42] as u32) << 8)\n            | ((header[43] as u32) << 16)\n            | ((header[44] as u32) << 24);\n        assert_eq!(data_length, 0, \"Data length in header should be zero\");\n\n        // Verify empty data was received\n        let data = body_stream\n            .next()\n            .await\n            .expect(\"Failed to receive empty data payload\")\n            .expect(\"Data should be Ok\");\n        assert_eq!(data.len(), 0, \"Data payload should be empty\");\n\n        drop(body_tx);\n\n        // Verify stream is now empty\n        assert!(\n            body_stream.next().await.is_none(),\n            \"Stream should be empty after receiving header and data\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_event_to_stream() {\n        let reporter = setup_reporter();\n        let (body_tx, body_rx) = tokio::sync::mpsc::unbounded_channel();\n        let mut body_stream = Box::pin(reporter.create_body_stream(body_rx));\n\n        // Use specific test values that we can verify in the header\n        let event_id = 42u64;\n        let trace_id = [\n            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,\n            0xFF, 0x00,\n        ];\n        let span_id = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22];\n        let test_data = \"hello world\";\n        let data_bytes = Bytes::from(test_data);\n\n        let event = TraceEvent {\n            typ: EventType::HTTPCallStart,\n            id: model::TraceEventId(event_id),\n            data: data_bytes.clone(),\n            span: model::SpanKey(model::TraceId(trace_id), model::SpanId(span_id)),\n            ts: tokio::time::Instant::now(),\n        };\n\n        body_tx.send(event).expect(\"Failed to send event to stream\");\n\n        let header = body_stream\n            .next()\n            .await\n            .expect(\"Failed to receive header from stream\")\n            .expect(\"Header should be Ok\");\n        let data = body_stream\n            .next()\n            .await\n            .expect(\"Failed to receive data from stream\")\n            .expect(\"Data should be Ok\");\n\n        // Verify that header has correct format and size\n        assert_eq!(header.len(), 45, \"Header should be exactly 45 bytes\");\n\n        // Verify event type\n        assert_eq!(\n            header[0],\n            EventType::HTTPCallStart as u8,\n            \"Event type should match\"\n        );\n\n        // Verify event ID (bytes 1-8)\n        let header_event_id = (header[1] as u64)\n            | ((header[2] as u64) << 8)\n            | ((header[3] as u64) << 16)\n            | ((header[4] as u64) << 24)\n            | ((header[5] as u64) << 32)\n            | ((header[6] as u64) << 40)\n            | ((header[7] as u64) << 48)\n            | ((header[8] as u64) << 56);\n        assert_eq!(header_event_id, event_id, \"Event ID in header should match\");\n\n        // Verify trace ID (bytes 17-32)\n        for i in 0..16 {\n            assert_eq!(\n                header[17 + i],\n                trace_id[i],\n                \"Trace ID byte {i} should match\"\n            );\n        }\n\n        // Verify span ID (bytes 33-40)\n        for i in 0..8 {\n            assert_eq!(header[33 + i], span_id[i], \"Span ID byte {i} should match\");\n        }\n\n        // Verify data length (bytes 41-44)\n        let data_length = (header[41] as u32)\n            | ((header[42] as u32) << 8)\n            | ((header[43] as u32) << 16)\n            | ((header[44] as u32) << 24);\n        assert_eq!(\n            data_length,\n            test_data.len() as u32,\n            \"Data length in header should match\"\n        );\n\n        // Verify data content\n        assert_eq!(data, data_bytes, \"Data payload should match original\");\n        assert_eq!(data.len(), test_data.len(), \"Data length should match\");\n\n        drop(body_tx);\n        // Verify channel is now empty\n        assert!(\n            body_stream.next().await.is_none(),\n            \"Channel should be empty after receiving header and data\"\n        );\n    }\n\n    #[tokio::test]\n    async fn test_event_to_stream_large_payload() {\n        let reporter = setup_reporter();\n        let (body_tx, body_rx) = tokio::sync::mpsc::unbounded_channel();\n        let mut body_stream = Box::pin(reporter.create_body_stream(body_rx));\n\n        // Create a large payload (1MB)\n        let large_data_size = 1024 * 1024;\n        let large_data = vec![0xAA; large_data_size];\n\n        let event = TraceEvent {\n            typ: EventType::DBQueryStart,\n            id: model::TraceEventId(42),\n            data: Bytes::from(large_data.clone()),\n            span: model::SpanKey(model::TraceId([3; 16]), model::SpanId([4; 8])),\n            ts: tokio::time::Instant::now(),\n        };\n\n        // Send the event with large payload\n        body_tx.send(event).expect(\"Failed to send event to stream\");\n\n        // Verify header was received\n        let header = body_stream\n            .next()\n            .await\n            .expect(\"Failed to receive header for large payload\")\n            .expect(\"Header should be Ok\");\n        assert_eq!(header[0], EventType::DBQueryStart as u8);\n\n        // Extract the data length from the header (last 4 bytes)\n        let data_length = (header[41] as u32)\n            | ((header[42] as u32) << 8)\n            | ((header[43] as u32) << 16)\n            | ((header[44] as u32) << 24);\n\n        // Verify the header correctly indicates the large size\n        assert_eq!(data_length, large_data_size as u32);\n\n        // Verify data was received and matches the original\n        let data = body_stream\n            .next()\n            .await\n            .expect(\"Failed to receive large payload data\")\n            .expect(\"Data should be Ok\");\n        assert_eq!(data.len(), large_data_size);\n        assert_eq!(data[0], 0xAA);\n        assert_eq!(data[large_data_size - 1], 0xAA);\n\n        drop(body_tx);\n        // Verify channel is now empty\n        assert!(body_stream.next().await.is_none());\n    }\n\n    #[test]\n    fn test_streaming_trace_event_header_format() {\n        // Create a time anchor\n        let anchor = TimeAnchor::new();\n\n        // Create a trace event\n        let event_type = EventType::LogMessage;\n        let event_id = 42u64;\n        let trace_id = model::TraceId([5; 16]);\n        let span_id = model::SpanId([6; 8]);\n        let data = Bytes::from(vec![10, 20, 30]);\n\n        let event = TraceEvent {\n            typ: event_type,\n            id: model::TraceEventId(event_id),\n            data: data.clone(),\n            span: model::SpanKey(trace_id, span_id),\n            ts: anchor.instant + std::time::Duration::from_nanos(123456789),\n        };\n\n        // Create a StreamingTraceEvent and get the header\n        let streaming_event = StreamingTraceEvent { event };\n        let header = streaming_event.header(&anchor);\n\n        // Header should be exactly 45 bytes\n        // 1 (type) + 8 (event id) + 8 (timestamp) + 16 (trace id) + 8 (span id) + 4 (data length)\n        assert_eq!(header.len(), 45);\n\n        // Check event type\n        assert_eq!(header[0], event_type as u8);\n\n        // Check event ID (little endian)\n        assert_eq!(header[1], (event_id & 0xFF) as u8);\n        assert_eq!(header[2], ((event_id >> 8) & 0xFF) as u8);\n        assert_eq!(header[3], ((event_id >> 16) & 0xFF) as u8);\n        assert_eq!(header[4], ((event_id >> 24) & 0xFF) as u8);\n        assert_eq!(header[5], ((event_id >> 32) & 0xFF) as u8);\n        assert_eq!(header[6], ((event_id >> 40) & 0xFF) as u8);\n        assert_eq!(header[7], ((event_id >> 48) & 0xFF) as u8);\n        assert_eq!(header[8], ((event_id >> 56) & 0xFF) as u8);\n\n        // Check trace ID\n        for i in 0..16 {\n            assert_eq!(header[17 + i], 5);\n        }\n\n        // Check span ID\n        for i in 0..8 {\n            assert_eq!(header[33 + i], 6);\n        }\n\n        // Check data length (3 bytes, little endian)\n        assert_eq!(header[41], 3);\n        assert_eq!(header[42], 0);\n        assert_eq!(header[43], 0);\n        assert_eq!(header[44], 0);\n    }\n\n    #[test]\n    fn test_streaming_trace_event_timestamp_encoding() {\n        // Create a time anchor\n        let anchor = TimeAnchor::new();\n\n        // Create events with different timestamps\n        let time_offsets = [\n            0,             // Same as anchor\n            1000,          // 1 microsecond\n            1_000_000,     // 1 millisecond\n            1_000_000_000, // 1 second\n        ];\n\n        for &offset_nanos in &time_offsets {\n            let ts = anchor.instant + std::time::Duration::from_nanos(offset_nanos as u64);\n\n            let event = TraceEvent {\n                typ: EventType::LogMessage,\n                id: model::TraceEventId(1),\n                data: Bytes::from(vec![]),\n                span: model::SpanKey(model::TraceId([0; 16]), model::SpanId([0; 8])),\n                ts,\n            };\n\n            // Create a StreamingTraceEvent and get the header\n            let streaming_event = StreamingTraceEvent { event };\n            let header = streaming_event.header(&anchor);\n\n            // Extract the timestamp from the header (bytes 9-16)\n            let encoded_ts = (header[9] as u64)\n                | ((header[10] as u64) << 8)\n                | ((header[11] as u64) << 16)\n                | ((header[12] as u64) << 24)\n                | ((header[13] as u64) << 32)\n                | ((header[14] as u64) << 40)\n                | ((header[15] as u64) << 48)\n                | ((header[16] as u64) << 56);\n\n            // For non-negative timestamps, the encoded value should be the offset * 2\n            assert_eq!(encoded_ts, (offset_nanos as u64) << 1);\n        }\n    }\n\n    #[test]\n    fn test_streaming_trace_event_different_event_types() {\n        // Create a time anchor\n        let anchor = TimeAnchor::new();\n\n        // Test different event types\n        let event_types = [\n            EventType::RequestSpanStart,\n            EventType::DBQueryStart,\n            EventType::RPCCallEnd,\n            EventType::LogMessage,\n            EventType::TestEnd,\n        ];\n\n        for &event_type in &event_types {\n            let event = TraceEvent {\n                typ: event_type,\n                id: model::TraceEventId(1),\n                data: Bytes::from(vec![]),\n                span: model::SpanKey(model::TraceId([0; 16]), model::SpanId([0; 8])),\n                ts: anchor.instant,\n            };\n\n            // Create a StreamingTraceEvent and get the header\n            let streaming_event = StreamingTraceEvent { event };\n            let header = streaming_event.header(&anchor);\n\n            // First byte should be the event type\n            assert_eq!(header[0], event_type as u8);\n        }\n    }\n\n    #[test]\n    fn test_streaming_trace_event_data_length() {\n        // Create a time anchor\n        let anchor = TimeAnchor::new();\n\n        // Test different data lengths\n        let data_lengths = [0, 1, 10, 255, 256, 65535, 16777215];\n\n        for &length in &data_lengths {\n            // Create data of the specified length\n            let data = Bytes::from(vec![0; length]);\n\n            let event = TraceEvent {\n                typ: EventType::LogMessage,\n                id: model::TraceEventId(1),\n                data,\n                span: model::SpanKey(model::TraceId([0; 16]), model::SpanId([0; 8])),\n                ts: anchor.instant,\n            };\n\n            // Create a StreamingTraceEvent and get the header\n            let streaming_event = StreamingTraceEvent { event };\n            let header = streaming_event.header(&anchor);\n\n            // Last 4 bytes should contain the length in little-endian format\n            let encoded_length = (header[41] as u32)\n                | ((header[42] as u32) << 8)\n                | ((header[43] as u32) << 16)\n                | ((header[44] as u32) << 24);\n\n            assert_eq!(encoded_length, length as u32);\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/trace/mod.rs",
    "content": "mod eventbuf;\nmod log;\npub mod protocol;\nmod time_anchor;\n\npub use log::{streaming_tracer, ReporterConfig, TraceSamplingConfig};\npub use protocol::Tracer;\n"
  },
  {
    "path": "runtimes/core/src/trace/protocol.rs",
    "content": "#![allow(dead_code)]\n//! Implements the trace protocol.\n\nuse std::sync::atomic::{AtomicU64, Ordering};\n\nuse crate::api::reqauth::meta::HeaderValueExt;\nuse crate::api::{self, PValue};\nuse crate::model::{APICall, LogField, LogFieldValue, Request, TraceEventId};\nuse crate::trace::eventbuf::EventBuffer;\nuse crate::trace::log::TraceEvent;\nuse crate::{model, objects, EncoreName, EndpointName};\n\n/// Represents a type of trace event.\n#[derive(Debug, Clone, Copy)]\n#[repr(u8)]\npub enum EventType {\n    RequestSpanStart = 0x01,\n    RequestSpanEnd = 0x02,\n    AuthSpanStart = 0x03,\n    AuthSpanEnd = 0x04,\n    PubsubMessageSpanStart = 0x05,\n    PubsubMessageSpanEnd = 0x06,\n    DBTransactionStart = 0x07,\n    DBTransactionEnd = 0x08,\n    DBQueryStart = 0x09,\n    DBQueryEnd = 0x0A,\n    RPCCallStart = 0x0B,\n    RPCCallEnd = 0x0C,\n    HTTPCallStart = 0x0D,\n    HTTPCallEnd = 0x0E,\n    LogMessage = 0x0F,\n    PubsubPublishStart = 0x10,\n    PubsubPublishEnd = 0x11,\n    ServiceInitStart = 0x12,\n    ServiceInitEnd = 0x13,\n    CacheCallStart = 0x14,\n    CacheCallEnd = 0x15,\n    BodyStream = 0x16,\n    // NOTE: We don't have an easy way of implementing test tracing for rust (i.e. typescript)\n    // so that's why we haven't implemented emitting TestStart/TestEnd etc.\n    TestStart = 0x17,\n    TestEnd = 0x18,\n    BucketObjectUploadStart = 0x19,\n    BucketObjectUploadEnd = 0x1A,\n    BucketObjectDownloadStart = 0x1B,\n    BucketObjectDownloadEnd = 0x1C,\n    BucketObjectGetAttrsStart = 0x1D,\n    BucketObjectGetAttrsEnd = 0x1E,\n    BucketListObjectsStart = 0x1F,\n    BucketListObjectsEnd = 0x20,\n    BucketDeleteObjectsStart = 0x21,\n    BucketDeleteObjectsEnd = 0x22,\n}\n\n// A global event id counter.\nstatic EVENT_ID: AtomicU64 = AtomicU64::new(1);\n\n#[derive(Debug, Clone)]\npub struct Tracer {\n    tx: Option<tokio::sync::mpsc::UnboundedSender<TraceEvent>>,\n    sampling_rate_config: super::TraceSamplingConfig,\n}\n\npub static TRACE_VERSION: u16 = 17;\n\nimpl Tracer {\n    pub(super) fn new(\n        tx: tokio::sync::mpsc::UnboundedSender<TraceEvent>,\n        sampling_rate_config: super::TraceSamplingConfig,\n    ) -> Self {\n        Self {\n            tx: Some(tx),\n            sampling_rate_config,\n        }\n    }\n\n    pub fn noop() -> Self {\n        Self {\n            tx: None,\n            sampling_rate_config: super::TraceSamplingConfig::new(vec![], None),\n        }\n    }\n\n    /// Determines whether a new API request should be traced based on sampling rate.\n    /// Returns false if this is a noop tracer (no sender).\n    ///\n    /// Looks up the sampling rate: endpoint → service → default.\n    /// If no match is found, always sample.\n    pub fn should_sample(&self, endpoint: &EndpointName) -> bool {\n        if self.tx.is_none() {\n            return false;\n        }\n        match self.sampling_rate_config.lookup_api(endpoint) {\n            None => true,\n            Some(rate) => rand::random::<f64>() < rate,\n        }\n    }\n\n    /// Determines whether a new pubsub subscription trace should be sampled.\n    /// Returns false if this is a noop tracer (no sender).\n    ///\n    /// Looks up the sampling rate: subscription → topic → service → default.\n    /// If no match is found, always sample.\n    pub fn should_sample_pubsub(&self, service: &str, topic: &str, subscription: &str) -> bool {\n        if self.tx.is_none() {\n            return false;\n        }\n        match self\n            .sampling_rate_config\n            .lookup_pubsub(service, topic, subscription)\n        {\n            None => true,\n            Some(rate) => rand::random::<f64>() < rate,\n        }\n    }\n\n    /// Determines whether a new trace should be sampled based on the default sampling rate.\n    /// Returns false if this is a noop tracer (no sender).\n    ///\n    /// If no default rate is configured, always samples.\n    pub fn should_sample_default(&self) -> bool {\n        if self.tx.is_none() {\n            return false;\n        }\n        match self.sampling_rate_config.lookup_default() {\n            None => true,\n            Some(rate) => rand::random::<f64>() < rate,\n        }\n    }\n}\n\npub struct LogMessageData<'a, I> {\n    pub source: Option<&'a Request>,\n    pub msg: &'a str,\n    pub level: log::Level,\n    pub fields: Option<I>,\n}\n\nimpl<I> LogMessageData<'_, I> {\n    fn level_byte(&self) -> u8 {\n        match self.level {\n            log::Level::Error => 4,\n            log::Level::Warn => 3,\n            log::Level::Info => 2,\n            log::Level::Debug => 1,\n            log::Level::Trace => 0,\n        }\n    }\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn log_message<'a, I>(&self, data: LogMessageData<I>)\n    where\n        I: ExactSizeIterator<Item = LogField<'a>>,\n    {\n        let Some(source) = data.source else {\n            return;\n        };\n\n        let fields_count = data.fields.as_ref().map(|fields| fields.len()).unwrap_or(0);\n\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + data.msg.len() + 1 + 64 * fields_count,\n        }\n        .into_eb();\n\n        eb.byte(data.level_byte());\n        eb.str(data.msg);\n        eb.uvarint(fields_count as u64);\n\n        if let Some(fields) = data.fields {\n            for field in fields {\n                eb.byte(field.type_byte());\n                eb.str(field.key);\n\n                match field.value {\n                    LogFieldValue::String(str) => eb.str(str),\n                    LogFieldValue::U64(n) => eb.uvarint(n),\n                    LogFieldValue::I64(n) => eb.ivarint(n),\n                    LogFieldValue::F64(n) => eb.f64(n),\n                    LogFieldValue::Bool(b) => eb.bool(b),\n                    LogFieldValue::Json(json) => match serde_json::to_vec(json) {\n                        Ok(bytes) => {\n                            eb.byte_string(&bytes);\n                            eb.nyi_stack_pcs()\n                        }\n                        Err(err) => {\n                            eb.byte_string(&[]);\n                            eb.err_with_legacy_stack(Some(&err))\n                        }\n                    },\n                }\n            }\n        }\n\n        eb.nyi_stack_pcs();\n\n        _ = self.send(EventType::LogMessage, source.span, eb);\n    }\n}\n\nimpl Tracer {\n    // Note: We don't have an easy way of implementing test tracing for rust (i.e. typescript)\n    // so that's why we haven't implemented emitting TestStart/TestEnd etc.\n    #[inline]\n    pub fn request_span_start(&self, req: &model::Request, redact_details: bool) {\n        if !req.traced {\n            return;\n        }\n        let mut eb = SpanStartEventData {\n            parent: Parent::from(req),\n            caller_event_id: req.caller_event_id,\n            ext_correlation_id: req.ext_correlation_id.as_deref(),\n            extra_space: 100,\n        }\n        .into_eb();\n\n        let event_type = match &req.data {\n            model::RequestData::RPC(rpc) => {\n                eb.str(rpc.endpoint.name.service());\n                eb.str(rpc.endpoint.name.endpoint());\n                eb.str(rpc.method.as_str());\n                eb.str(&rpc.path);\n\n                // Encode path params. We only encode the values since the keys are known in metadata.\n                let path_params = if !redact_details {\n                    rpc.parsed_payload.as_ref().and_then(|p| p.path.as_ref())\n                } else {\n                    None\n                };\n\n                if let Some(path_params) = path_params {\n                    eb.uvarint(path_params.len() as u64);\n                    for (_, v) in path_params {\n                        match &v {\n                            PValue::String(s) => eb.str(s.as_str()),\n                            other => eb.str(other.to_string().as_str()),\n                        }\n                    }\n                } else {\n                    eb.uvarint(0u64);\n                }\n\n                // Encode request headers. If a header has multiple values it is encoded multiple times.\n                if !redact_details {\n                    eb.headers(&rpc.req_headers);\n                } else {\n                    eb.uvarint(0u64);\n                }\n\n                if !redact_details {\n                    let payload = rpc\n                        .parsed_payload\n                        .as_ref()\n                        .and_then(|p| serde_json::to_vec_pretty(p).ok());\n                    eb.opt_byte_string(payload.as_deref());\n                } else {\n                    eb.byte_string(b\"<redacted>\");\n                };\n\n                eb.opt_str(req.ext_correlation_id.as_deref()); // yes, this is repeated for some reason\n                eb.opt_str(rpc.auth_user_id.as_deref());\n                eb.bool(false); // NOTE: mocked field not used\n\n                EventType::RequestSpanStart\n            }\n\n            model::RequestData::Auth(auth) => {\n                let name = &auth.auth_handler;\n                eb.str(name.service());\n                eb.str(name.endpoint());\n\n                // TODO: non-raw payload.\n                eb.byte_string(&[]);\n\n                EventType::AuthSpanStart\n            }\n            model::RequestData::PubSub(msg_data) => {\n                eb.str(&msg_data.service);\n                eb.str(&msg_data.topic);\n                eb.str(&msg_data.subscription);\n                eb.str(&msg_data.message_id);\n                eb.uvarint(msg_data.attempt as u64);\n                eb.time(&msg_data.published);\n                eb.byte_string(&msg_data.payload);\n\n                EventType::PubsubMessageSpanStart\n            }\n            model::RequestData::Stream(data) => {\n                eb.str(data.endpoint.name.service());\n                eb.str(data.endpoint.name.endpoint());\n                eb.str(\"GET\");\n                eb.str(&data.path);\n\n                // Encode path params. We only encode the values since the keys are known in metadata.\n                let path_params = if !redact_details {\n                    data.parsed_payload.as_ref().and_then(|p| p.path.as_ref())\n                } else {\n                    None\n                };\n\n                if let Some(path_params) = path_params {\n                    eb.uvarint(path_params.len() as u64);\n                    for (_, v) in path_params {\n                        match &v {\n                            PValue::String(s) => eb.str(s.as_str()),\n                            other => eb.str(other.to_string().as_str()),\n                        }\n                    }\n                } else {\n                    eb.uvarint(0u64);\n                }\n\n                // Encode request headers. If a header has multiple values it is encoded multiple times.\n                if !redact_details {\n                    eb.headers(&data.req_headers);\n                } else {\n                    eb.uvarint(0u64);\n                }\n\n                if !redact_details {\n                    let payload = data\n                        .parsed_payload\n                        .as_ref()\n                        .and_then(|p| serde_json::to_vec_pretty(p).ok());\n                    eb.opt_byte_string(payload.as_deref());\n                } else {\n                    eb.byte_string(b\"<redacted>\");\n                };\n\n                eb.opt_str(req.ext_correlation_id.as_deref()); // yes, this is repeated for some reason\n                eb.opt_str(data.auth_user_id.as_deref());\n                eb.bool(false); // NOTE: mocked field not used\n\n                EventType::RequestSpanStart\n            }\n        };\n\n        _ = self.send(event_type, req.span, eb);\n    }\n\n    #[inline]\n    pub fn request_span_end(&self, resp: &model::Response, redact_details: bool) {\n        let req = resp.request.as_ref();\n        if !req.traced {\n            return;\n        }\n\n        let mut eb = SpanEndEventData {\n            parent: Parent::from(req),\n            duration: resp.duration,\n            err: match &resp.data {\n                model::ResponseData::RPC(rpc) => rpc.error.as_ref(),\n                model::ResponseData::Auth(res) => res.as_ref().err(),\n                model::ResponseData::PubSub(res) => res.as_ref().err(),\n            },\n            extra_space: 100,\n        }\n        .into_eb();\n\n        match &req.data {\n            model::RequestData::RPC(req_data) => {\n                eb.str(req_data.endpoint.name.service());\n                eb.str(req_data.endpoint.name.endpoint());\n            }\n            model::RequestData::Auth(auth_data) => {\n                let name = &auth_data.auth_handler;\n                eb.str(name.service());\n                eb.str(name.endpoint());\n            }\n            model::RequestData::PubSub(msg_data) => {\n                eb.str(&msg_data.service);\n                eb.str(&msg_data.topic);\n                eb.str(&msg_data.subscription);\n                eb.str(&msg_data.message_id);\n            }\n            model::RequestData::Stream(data) => {\n                eb.str(data.endpoint.name.service());\n                eb.str(data.endpoint.name.endpoint());\n            }\n        }\n\n        let event_type = match &resp.data {\n            model::ResponseData::RPC(resp_data) => {\n                eb.uvarint(resp_data.status_code);\n                if !redact_details {\n                    eb.headers(&resp_data.resp_headers);\n                } else {\n                    eb.uvarint(0u64);\n                }\n\n                if !redact_details {\n                    let payload = resp_data\n                        .resp_payload\n                        .as_ref()\n                        .and_then(|p| serde_json::to_vec_pretty(p).ok());\n                    eb.opt_byte_string(payload.as_deref());\n                } else {\n                    eb.byte_string(b\"<redacted>\");\n                };\n\n                eb.event_id(req.caller_event_id);\n\n                // uid\n                let uid = match &req.data {\n                    model::RequestData::RPC(rpc) => rpc.auth_user_id.as_deref(),\n                    model::RequestData::Stream(data) => data.auth_user_id.as_deref(),\n                    _ => None,\n                };\n                eb.str(uid.unwrap_or(\"\"));\n\n                EventType::RequestSpanEnd\n            }\n            model::ResponseData::Auth(auth_result) => {\n                match auth_result {\n                    Ok(auth_success) => {\n                        eb.str(auth_success.user_id.as_str());\n                        let user_data = serde_json::to_string(&auth_success.user_data)\n                            .unwrap_or_else(|_| String::new());\n                        eb.str(&user_data);\n                    }\n                    Err(_) => {\n                        eb.str(\"\"); // auth uid\n                        eb.str(\"\"); // response payload\n                    }\n                }\n\n                EventType::AuthSpanEnd\n            }\n\n            model::ResponseData::PubSub(_) => EventType::PubsubMessageSpanEnd,\n        };\n\n        _ = self.send(event_type, req.span, eb);\n    }\n}\n\npub struct RPCCallEndData<'a> {\n    pub call: &'a APICall,\n    pub start_id: Option<TraceEventId>,\n    pub err: Option<&'a api::Error>,\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn rpc_call_start(&self, call: &APICall) -> Option<TraceEventId> {\n        let source = call.source.as_ref()?;\n        if !source.traced {\n            return None;\n        }\n        let (service, endpoint) = (call.target.service(), call.target.endpoint());\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + service.len() + endpoint.len(),\n        }\n        .into_eb();\n\n        eb.str(service);\n        eb.str(endpoint);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::RPCCallStart, source.span, eb))\n    }\n\n    #[inline]\n    pub fn rpc_call_end(&self, data: RPCCallEndData) {\n        let Some(source) = data.call.source.as_ref() else {\n            return;\n        };\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let (service, endpoint) = (data.call.target.service(), data.call.target.endpoint());\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + service.len() + endpoint.len(),\n        }\n        .into_eb();\n\n        eb.api_err_with_legacy_stack(data.err);\n\n        _ = self.send(EventType::RPCCallEnd, source.span, eb);\n    }\n}\n\npub struct PublishStartData<'a> {\n    pub source: &'a Request,\n    pub topic: &'a EncoreName,\n    pub payload: &'a [u8],\n}\n\npub struct PublishEndData<'a> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: &'a anyhow::Result<String>,\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn pubsub_publish_start(&self, data: PublishStartData) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + 8 + data.topic.len() + data.payload.len(),\n        }\n        .into_eb();\n\n        eb.str(data.topic);\n        eb.byte_string(data.payload);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::PubsubPublishStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn pubsub_publish_end(&self, data: PublishEndData) {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        eb.str(data.result.as_deref().unwrap_or(\"\"));\n        eb.err_with_legacy_stack(data.result.as_ref().err());\n\n        _ = self.send(EventType::PubsubPublishEnd, data.source.span, eb);\n    }\n}\n\npub struct DBQueryStartData<'a> {\n    pub source: &'a Request,\n    pub query: &'a str,\n}\n\npub struct DBQueryEndData<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub error: Option<&'a E>,\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn db_query_start(&self, data: DBQueryStartData) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + data.query.len() + 32,\n        }\n        .into_eb();\n\n        eb.str(data.query);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::DBQueryStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn db_query_end<E>(&self, data: DBQueryEndData<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        eb.err_with_legacy_stack(data.error);\n\n        _ = self.send(EventType::DBQueryEnd, data.source.span, eb);\n    }\n}\n\npub struct BucketObjectUploadStart<'a> {\n    pub source: &'a Request,\n    pub bucket: &'a EncoreName,\n    pub object: &'a str,\n    pub attrs: BucketObjectAttributes<'a>,\n}\n\npub struct BucketObjectUploadEnd<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: BucketObjectUploadEndResult<'a, E>,\n}\n\npub enum BucketObjectUploadEndResult<'a, E> {\n    Success { size: u64, version: Option<&'a str> },\n    Err(&'a E),\n}\n\n#[derive(Default, Debug)]\npub struct BucketObjectAttributes<'a> {\n    pub size: Option<u64>,\n    pub version: Option<&'a str>,\n    pub etag: Option<&'a str>,\n    pub content_type: Option<&'a str>,\n}\n\nimpl<'a> From<&'a objects::ObjectAttrs> for BucketObjectAttributes<'a> {\n    fn from(attrs: &'a objects::ObjectAttrs) -> Self {\n        Self {\n            size: Some(attrs.size),\n            version: attrs.version.as_deref(),\n            etag: Some(&attrs.etag),\n            content_type: attrs.content_type.as_deref(),\n        }\n    }\n}\n\nimpl EventBuffer {\n    fn bucket_object_attrs(&mut self, attrs: &BucketObjectAttributes) {\n        self.uvarint(attrs.size.unwrap_or(0));\n        self.opt_str(attrs.version);\n        self.opt_str(attrs.etag);\n        self.opt_str(attrs.content_type);\n    }\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn bucket_object_upload_start(\n        &self,\n        data: BucketObjectUploadStart,\n    ) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + 8 + data.bucket.len() + data.object.len(),\n        }\n        .into_eb();\n\n        eb.str(data.bucket);\n        eb.str(data.object);\n        eb.bucket_object_attrs(&data.attrs);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::BucketObjectUploadStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn bucket_object_upload_end<E>(&self, data: BucketObjectUploadEnd<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        match data.result {\n            BucketObjectUploadEndResult::Success { size, version } => {\n                eb.uvarint(size);\n                eb.opt_str(version);\n                eb.err_with_legacy_stack::<E>(None);\n            }\n            BucketObjectUploadEndResult::Err(err) => {\n                eb.uvarint(0u64);\n                eb.err_with_legacy_stack(Some(err));\n            }\n        }\n\n        _ = self.send(EventType::BucketObjectUploadEnd, data.source.span, eb);\n    }\n}\n\npub struct BucketObjectDownloadStart<'a> {\n    pub source: &'a Request,\n    pub bucket: &'a EncoreName,\n    pub object: &'a str,\n    pub version: Option<&'a str>,\n}\n\npub struct BucketObjectDownloadEnd<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: BucketObjectDownloadEndResult<'a, E>,\n}\n\npub enum BucketObjectDownloadEndResult<'a, E> {\n    Success { size: u64 },\n    Err(&'a E),\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn bucket_object_download_start(\n        &self,\n        data: BucketObjectDownloadStart,\n    ) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + 8 + data.bucket.len() + data.object.len(),\n        }\n        .into_eb();\n\n        eb.str(data.bucket);\n        eb.str(data.object);\n        eb.opt_str(data.version);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::BucketObjectDownloadStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn bucket_object_download_end<E>(&self, data: BucketObjectDownloadEnd<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        match data.result {\n            BucketObjectDownloadEndResult::Success { size } => {\n                eb.uvarint(size);\n                eb.err_with_legacy_stack::<E>(None);\n            }\n            BucketObjectDownloadEndResult::Err(err) => {\n                eb.uvarint(0u64);\n                eb.err_with_legacy_stack(Some(err));\n            }\n        }\n\n        _ = self.send(EventType::BucketObjectDownloadEnd, data.source.span, eb);\n    }\n}\n\npub struct BucketDeleteObjectsStart<'a, O> {\n    pub source: &'a Request,\n    pub bucket: &'a EncoreName,\n    pub objects: O,\n}\n\npub struct BucketDeleteObjectEntry<'a> {\n    pub object: &'a str,\n    pub version: Option<&'a str>,\n}\n\npub struct BucketDeleteObjectsEnd<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: BucketDeleteObjectsEndResult<'a, E>,\n}\n\npub enum BucketDeleteObjectsEndResult<'a, E> {\n    Success,\n    Err(&'a E),\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn bucket_delete_objects_start<'a, O>(\n        &self,\n        data: BucketDeleteObjectsStart<'a, O>,\n    ) -> Option<TraceEventId>\n    where\n        O: ExactSizeIterator<Item = BucketDeleteObjectEntry<'a>>,\n    {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + 8 + data.bucket.len() + data.objects.len() * 8,\n        }\n        .into_eb();\n\n        eb.str(data.bucket);\n        eb.nyi_stack_pcs();\n        eb.uvarint(data.objects.len() as u64);\n        for obj in data.objects {\n            eb.str(obj.object);\n            eb.opt_str(obj.version);\n        }\n\n        Some(self.send(EventType::BucketDeleteObjectsStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn bucket_delete_objects_end<E>(&self, data: BucketDeleteObjectsEnd<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        match data.result {\n            BucketDeleteObjectsEndResult::Success => {\n                eb.err_with_legacy_stack::<E>(None);\n            }\n            BucketDeleteObjectsEndResult::Err(err) => {\n                eb.err_with_legacy_stack(Some(err));\n            }\n        }\n\n        _ = self.send(EventType::BucketDeleteObjectsEnd, data.source.span, eb);\n    }\n}\n\npub struct BucketListObjectsStart<'a> {\n    pub source: &'a Request,\n    pub bucket: &'a EncoreName,\n    pub prefix: Option<&'a str>,\n}\n\npub struct BucketListObjectsEnd<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: BucketListObjectsEndResult<'a, E>,\n}\n\npub enum BucketListObjectsEndResult<'a, E> {\n    Success { observed: u64, has_more: bool },\n    Err(&'a E),\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn bucket_list_objects_start(&self, data: BucketListObjectsStart) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4\n                + 4\n                + 8\n                + data.bucket.len()\n                + data.prefix.map(|p| p.len()).unwrap_or_default(),\n        }\n        .into_eb();\n\n        eb.str(data.bucket);\n        eb.opt_str(data.prefix);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::BucketListObjectsStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn bucket_list_objects_end<E>(&self, data: BucketListObjectsEnd<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        match data.result {\n            BucketListObjectsEndResult::Success { observed, has_more } => {\n                eb.err_with_legacy_stack::<E>(None);\n                eb.uvarint(observed);\n                eb.bool(has_more);\n            }\n            BucketListObjectsEndResult::Err(err) => {\n                eb.err_with_legacy_stack(Some(err));\n                eb.uvarint(0u64);\n                eb.bool(false);\n            }\n        }\n\n        _ = self.send(EventType::BucketListObjectsEnd, data.source.span, eb);\n    }\n}\n\npub struct BucketObjectGetAttrsStart<'a> {\n    pub source: &'a Request,\n    pub bucket: &'a EncoreName,\n    pub object: &'a str,\n    pub version: Option<&'a str>,\n}\n\npub struct BucketObjectGetAttrsEnd<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: BucketObjectGetAttrsEndResult<'a, E>,\n}\n\npub enum BucketObjectGetAttrsEndResult<'a, E> {\n    Success(BucketObjectAttributes<'a>),\n    Err(&'a E),\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn bucket_object_get_attrs_start(\n        &self,\n        data: BucketObjectGetAttrsStart,\n    ) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 4 + 4 + 8 + data.bucket.len() + data.object.len(),\n        }\n        .into_eb();\n\n        eb.str(data.bucket);\n        eb.str(data.object);\n        eb.opt_str(data.version);\n        eb.nyi_stack_pcs();\n\n        Some(self.send(EventType::BucketObjectGetAttrsStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn bucket_object_get_attrs_end<E>(&self, data: BucketObjectGetAttrsEnd<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 4 + 4 + 8,\n        }\n        .into_eb();\n\n        match data.result {\n            BucketObjectGetAttrsEndResult::Success(attrs) => {\n                eb.err_with_legacy_stack::<E>(None);\n                eb.bucket_object_attrs(&attrs);\n            }\n            BucketObjectGetAttrsEndResult::Err(err) => {\n                eb.err_with_legacy_stack(Some(err));\n            }\n        }\n\n        _ = self.send(EventType::BucketObjectGetAttrsEnd, data.source.span, eb);\n    }\n}\n\n/// Cache operation result for tracing.\n/// Values must match the Go trace parser's CacheOp_Result enum.\n#[derive(Debug, Clone, Copy)]\n#[repr(u8)]\npub enum CacheOpResult {\n    Unknown = 0,\n    Ok = 1,\n    NoSuchKey = 2,\n    Conflict = 3,\n    Err = 4,\n}\n\npub struct CacheCallStartData<'a> {\n    pub source: &'a Request,\n    pub operation: &'static str,\n    pub is_write: bool,\n    pub keys: &'a [&'a str],\n}\n\npub struct CacheCallEndData<'a, E> {\n    pub start_id: Option<TraceEventId>,\n    pub source: &'a Request,\n    pub result: CacheOpResult,\n    pub error: Option<&'a E>,\n}\n\nimpl Tracer {\n    #[inline]\n    pub fn cache_call_start(&self, data: CacheCallStartData) -> Option<TraceEventId> {\n        if !data.source.traced {\n            return None;\n        }\n        let mut eb = BasicEventData {\n            correlation_event_id: None,\n            extra_space: 64 + data.operation.len() + data.keys.len() * 32,\n        }\n        .into_eb();\n\n        eb.str(data.operation);\n        eb.bool(data.is_write);\n        eb.nyi_stack_pcs();\n        eb.uvarint(data.keys.len() as u64);\n        for key in data.keys {\n            eb.str(key);\n        }\n\n        Some(self.send(EventType::CacheCallStart, data.source.span, eb))\n    }\n\n    #[inline]\n    pub fn cache_call_end<E>(&self, data: CacheCallEndData<E>)\n    where\n        E: std::fmt::Display,\n    {\n        let Some(start_id) = data.start_id else {\n            return;\n        };\n        let mut eb = BasicEventData {\n            correlation_event_id: Some(start_id),\n            extra_space: 32,\n        }\n        .into_eb();\n\n        eb.byte(data.result as u8);\n        eb.err_with_legacy_stack(data.error);\n\n        _ = self.send(EventType::CacheCallEnd, data.source.span, eb);\n    }\n}\n\nimpl Tracer {\n    #[inline]\n    fn send(&self, typ: EventType, span: model::SpanKey, eb: EventBuffer) -> model::TraceEventId {\n        // Make sure the event id is never 0, as it's used to indicate \"no event\" in the protocol.\n        let mut id = EVENT_ID.fetch_add(1, Ordering::SeqCst);\n        if id == 0 {\n            id = EVENT_ID.fetch_add(1, Ordering::SeqCst);\n        }\n        let id = model::TraceEventId(id);\n\n        // If we have a sender, send the event. Otherwise this is a no-op tracer.\n        if let Some(tx) = &self.tx {\n            _ = tx.send(TraceEvent {\n                typ,\n                span,\n                id,\n                data: eb.freeze(),\n                ts: tokio::time::Instant::now(),\n            });\n        }\n\n        id\n    }\n}\n\nimpl EventBuffer {\n    fn parent(&mut self, parent: Option<&Parent>) {\n        self.reserve(16 + 8);\n\n        if let Some(parent) = parent {\n            match parent {\n                Parent::Trace(trace) => {\n                    self.bytes(&trace.0);\n                    self.bytes(&[0; 8]);\n                }\n                Parent::Span(span) => {\n                    self.bytes(&span.0 .0);\n                    self.bytes(&span.1 .0);\n                }\n            }\n        } else {\n            self.bytes(&[0; 16]);\n            self.bytes(&[0; 8]);\n        }\n    }\n\n    /// Writes a span key to the buffer.\n    /// Writes 0 bytes if key is None.\n    fn span_key(&mut self, key: Option<model::SpanKey>) {\n        self.reserve(16 + 8);\n        match key {\n            Some(key) => {\n                self.bytes(&key.0 .0);\n                self.bytes(&key.1 .0);\n            }\n            None => {\n                self.bytes(&[0; 16]);\n                self.bytes(&[0; 8]);\n            }\n        }\n    }\n\n    fn event_id(&mut self, event_id: Option<model::TraceEventId>) {\n        self.uvarint(match event_id {\n            Some(event_id) => event_id.0,\n            None => 0,\n        });\n    }\n\n    fn opt_str(&mut self, s: Option<&str>) {\n        let str = s.unwrap_or(\"\");\n        self.str(str);\n    }\n\n    fn opt_byte_string(&mut self, s: Option<&[u8]>) {\n        let bytes = s.unwrap_or(&[]);\n        self.byte_string(bytes);\n    }\n\n    fn headers(&mut self, headers: &axum::http::HeaderMap) {\n        self.uvarint(headers.len() as u64);\n        for (k, v) in headers.iter() {\n            self.str(k.as_str());\n            self.str(v.to_utf8_str().unwrap_or(\"\"));\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\nenum Parent {\n    Trace(model::TraceId),\n    Span(model::SpanKey),\n}\n\nimpl Parent {\n    fn from(req: &model::Request) -> Option<Self> {\n        if let Some(span) = req.parent_span {\n            Some(Parent::Span(span))\n        } else {\n            req.parent_trace.map(Parent::Trace)\n        }\n    }\n}\n\nstruct SpanStartEventData<'a> {\n    parent: Option<Parent>,\n    caller_event_id: Option<model::TraceEventId>,\n    ext_correlation_id: Option<&'a str>,\n\n    /// Additional extra space to allocate in the buffer.\n    extra_space: usize,\n}\n\nimpl SpanStartEventData<'_> {\n    pub fn into_eb(self) -> EventBuffer {\n        let correlation_len = self.ext_correlation_id.map(|s| s.len()).unwrap_or(0);\n        let mut eb =\n            EventBuffer::with_capacity(4 + 16 + 8 + 4 + correlation_len + 2 + self.extra_space);\n\n        eb.uvarint(0u64); // TODO: GOID\n        eb.parent(self.parent.as_ref());\n        eb.uvarint(0u64); // TODO: def loc\n        eb.event_id(self.caller_event_id);\n        eb.opt_str(self.ext_correlation_id);\n\n        eb\n    }\n}\n\nstruct SpanEndEventData<'a> {\n    parent: Option<Parent>,\n    duration: std::time::Duration,\n    err: Option<&'a api::Error>,\n\n    /// Additional extra space to allocate in the buffer.\n    extra_space: usize,\n}\n\nimpl SpanEndEventData<'_> {\n    pub fn into_eb(self) -> EventBuffer {\n        let mut eb = EventBuffer::with_capacity(8 + 12 + 8 + self.extra_space);\n\n        eb.duration(self.duration);\n\n        let status_code: u8 = self\n            .err\n            .as_ref()\n            .map(|e| e.code.to_trace_code())\n            .unwrap_or(0);\n        eb.byte(status_code);\n\n        eb.api_err_with_legacy_stack(self.err);\n        eb.formatted_stack(self.err.as_ref().and_then(|err| err.stack.as_ref()));\n        eb.parent(self.parent.as_ref());\n\n        eb\n    }\n}\n\nstruct BasicEventData {\n    correlation_event_id: Option<model::TraceEventId>,\n\n    /// Additional extra space to allocate in the buffer.\n    extra_space: usize,\n}\n\nimpl BasicEventData {\n    pub fn into_eb(self) -> EventBuffer {\n        let mut eb = EventBuffer::with_capacity(4 + 4 + self.extra_space);\n\n        eb.uvarint(0u64); // TODO: def loc\n        eb.uvarint(0u64); // TODO: GOID\n        eb.event_id(self.correlation_event_id);\n\n        eb\n    }\n}\n"
  },
  {
    "path": "runtimes/core/src/trace/time_anchor.rs",
    "content": "/// Correlates a system time with a time instant.\n#[derive(Debug, Clone)]\npub struct TimeAnchor {\n    /// The system time.\n    pub system_time: chrono::DateTime<chrono::Utc>,\n\n    /// The time instant.\n    pub instant: tokio::time::Instant,\n}\n\nimpl TimeAnchor {\n    pub fn new() -> Self {\n        TimeAnchor {\n            system_time: chrono::Utc::now(),\n            instant: tokio::time::Instant::now(),\n        }\n    }\n\n    pub fn trace_header(&self) -> String {\n        // Format the system as with RFC3339Nano.\n        let dt = self\n            .system_time\n            .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true);\n\n        format!(\"0 {dt}\")\n    }\n}\n"
  },
  {
    "path": "runtimes/go/README.md",
    "content": "# Encore Runtime\n\nThis is the Encore runtime module that provides APIs and internal runtime components\nfor running Encore applications.\n\nUsing [the Public API generator tool](../tools/publicapigen) the [encore.dev Public API](https://github.com/encoredev/encore.dev)\nis generated from this package. However, the following are not included in the public API:\n- The [runtime](./runtime) package, as that is considered to be an internal implementation detail of Encore and it's API is considered unstable.\n- Any internal packages, such as [pubsub/internal](./pubsub/internal), as those can't be accessed outside the Encore runtime.\n- Any files with the suffix of `_internal.go` as those are implementation details and not expected to be called from outside an Encore application\n- Any files with the suffix of `_test.go` as those simply form the testsuite for the runtime's implementation\n- Any functions, types or variables which are not exported from the package (unless the comment `//publicapigen:keep` is present)\n- The body of any functions, each body is replaced with a `panic()` call.\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/auth.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/shared/cloudtrace\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/internal/platformauth\"\n)\n\ntype AuthHandlerDesc[Params any] struct {\n\t// Service and Endpoint name the auth handler this description is for.\n\tService     string\n\tSvcNum      uint16\n\tEndpoint    string\n\tDefLoc      uint32\n\tHasAuthData bool // whether the handler returns custom auth data\n\n\tDecodeAuth  func(*http.Request) (Params, error)\n\tAuthHandler func(context.Context, Params) (model.AuthInfo, error)\n\n\tScrubRequestPaths []scrub.Path\n\n\trpcDescOnce   sync.Once\n\tcachedRPCDesc *model.RPCDesc\n}\n\n// AuthHandler is an interface that is either implemented by [AuthHandlerDesc[Params]] or [remoteAuthHandler]\n// depending on whether the auth handler is hosted in this container or not.\ntype AuthHandler interface {\n\t// Authenticate authenticates the request.\n\t// If err != nil it should be written back as the response.\n\tAuthenticate(IncomingContext) (model.AuthInfo, error)\n\n\t// HostedByService returns the name of the service that hosts the auth handler.\n\tHostedByService() string\n\n\t// ParseAuthData parses the auth data from the request and returns nil if successful.\n\t// and data is present, otherwise it returns an error.\n\tParseAuthData(c IncomingContext) error\n}\n\nfunc (d *AuthHandlerDesc[Params]) Authenticate(c IncomingContext) (model.AuthInfo, error) {\n\tparam, err := d.DecodeAuth(c.req)\n\tvar info model.AuthInfo\n\tif err != nil {\n\t\treturn model.AuthInfo{}, err\n\t}\n\n\tdone := make(chan struct{})\n\tcall, err := c.server.beginAuth(d.DefLoc)\n\tif err != nil {\n\t\treturn model.AuthInfo{}, err\n\t}\n\n\tvar authErr error\n\tgo func() {\n\t\tdefer close(done)\n\t\t_, authErr = c.server.beginRequest(c.req.Context(), &beginRequestParams{\n\t\t\tTraceID:       c.callMeta.TraceID,\n\t\t\tParentSpanID:  c.callMeta.ParentSpanID,\n\t\t\tParentSampled: c.callMeta.TraceSampled,\n\t\t\tSpanID:        call.SpanID,\n\t\t\tDefLoc:        d.DefLoc,\n\t\t\tType:          model.AuthHandler,\n\t\t\tData: &model.RPCData{\n\t\t\t\tDesc:               d.rpcDesc(),\n\t\t\t\tNonRawPayload:      d.marshalParams(c.server.json, param),\n\t\t\t\tTypedPayload:       param,\n\t\t\t\tRequestHeaders:     c.req.Header,\n\t\t\t\tFromEncorePlatform: platformauth.IsEncorePlatformRequest(c.req.Context()),\n\t\t\t},\n\t\t\tExtCorrelationID:    clampTo64Chars(c.req.Header.Get(\"X-Correlation-ID\")),\n\t\t\tAdditionalLogFields: cloudtrace.StructuredLogFields(c.req),\n\t\t})\n\t\tif authErr != nil {\n\t\t\treturn\n\t\t}\n\t\tdefer func() {\n\t\t\tif err2 := recover(); err2 != nil {\n\t\t\t\tpanicStack := stack.Build(0)\n\t\t\t\tauthErr = errs.B().Code(errs.Internal).Meta(\"panic_stack\", panicStack).Msgf(\n\t\t\t\t\t\"auth handler panicked: %v\", err2).Err()\n\t\t\t\tc.server.finishRequest(newErrResp(authErr, 0))\n\t\t\t}\n\t\t}()\n\n\t\tif err := runValidate(param); err != nil {\n\t\t\tauthErr = err\n\t\t\tc.server.finishRequest(newErrResp(authErr, 0))\n\t\t\treturn\n\t\t}\n\n\t\tinfo, authErr = d.AuthHandler(c.req.Context(), param)\n\n\t\tif authErr != nil {\n\t\t\tauthErr = errs.RoundTrip(authErr)\n\t\t\tc.server.finishRequest(newErrResp(authErr, 0))\n\t\t} else {\n\t\t\tresp := d.newAuthResp(info, authErr, c.server.json)\n\t\t\tc.server.finishRequest(resp)\n\t\t}\n\t}()\n\t<-done\n\n\treturn info, authErr\n}\n\nfunc (d *AuthHandlerDesc[Params]) HostedByService() string {\n\treturn d.Service\n}\n\nfunc (d *AuthHandlerDesc[Params]) ParseAuthData(c IncomingContext) error {\n\t_, err := d.DecodeAuth(c.req)\n\treturn err\n}\n\n// runAuthHandler runs the auth handler, if provided.\n// It reports whether to proceed with calling the handler.\nfunc (s *Server) runAuthHandler(h Handler, c IncomingContext) (info model.AuthInfo, proceed bool) {\n\trequiresAuth := h.AccessType() == RequiresAuth\n\tif s.authHandler == nil {\n\t\tif requiresAuth {\n\t\t\tpanic(fmt.Sprintf(\"internal error: API %s.%s requires auth but no auth handler set\",\n\t\t\t\th.ServiceName(), h.EndpointName()))\n\t\t}\n\t\treturn model.AuthInfo{}, true\n\t}\n\n\t// If this is a service to service call, we use the existing auth info.\n\tif c.callMeta.IsServiceToService() {\n\t\tif c.auth.UID == \"\" && requiresAuth {\n\t\t\t// Unless there isn't some and we need it, in which case we error.\n\t\t\terr := errs.B().Code(errs.Unauthenticated).Msg(\"no auth info provided\").Err()\n\t\t\treturnError(c, err, 0, nil)\n\t\t\treturn model.AuthInfo{}, false\n\t\t}\n\n\t\treturn c.auth, true\n\t}\n\n\tvar err error\n\tinfo, err = s.authHandler.Authenticate(c)\n\tif err != nil {\n\t\t// If the auth handler returned Unauthenticated and the endpoint doesn't actually require auth,\n\t\t// continue as if no auth information was provided.\n\t\tif errs.Code(err) == errs.Unauthenticated && !requiresAuth {\n\t\t\treturn model.AuthInfo{}, true\n\t\t} else {\n\t\t\treturnError(c, err, 0, nil)\n\t\t\treturn model.AuthInfo{}, false\n\t\t}\n\t}\n\n\treturn info, true\n}\n\n// rpcDesc returns the RPC description for this endpoint,\n// computing and caching the first time it's called.\nfunc (d *AuthHandlerDesc[Params]) rpcDesc() *model.RPCDesc {\n\td.rpcDescOnce.Do(func() {\n\t\tdesc := &model.RPCDesc{\n\t\t\tService:     d.Service,\n\t\t\tSvcNum:      d.SvcNum,\n\t\t\tEndpoint:    d.Endpoint,\n\t\t\tAuthHandler: true,\n\t\t\tRaw:         false,\n\n\t\t\tScrubRequestPaths: d.ScrubRequestPaths,\n\n\t\t\t// TODO would be nice to support these for auth handlers in the future.\n\t\t\tRequestType:  nil,\n\t\t\tResponseType: nil,\n\t\t\tTags:         nil,\n\n\t\t\tExposed:      true,\n\t\t\tAuthRequired: false,\n\t\t}\n\t\td.cachedRPCDesc = desc\n\t})\n\treturn d.cachedRPCDesc\n}\n\nfunc (d *AuthHandlerDesc[Params]) marshalParams(json jsoniter.API, p Params) []byte {\n\tdata, _ := json.Marshal(p)\n\treturn data\n}\n\n// newAuthResp returns an *model.Response for an auth response.\nfunc (d *AuthHandlerDesc[Params]) newAuthResp(info model.AuthInfo, authErr error, json jsoniter.API) *model.Response {\n\tvar payload []byte\n\tif d.HasAuthData {\n\t\tpayload = marshalParams(json, info.UserData)\n\t}\n\n\treturn &model.Response{\n\t\tHTTPStatus:   errs.HTTPStatus(authErr),\n\t\tErr:          authErr,\n\t\tAuthUID:      info.UID,\n\t\tTypedPayload: info.UserData,\n\t\tPayload:      payload,\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/auth_remote.go",
    "content": "package api\n\nimport (\n\t\"maps\"\n\t\"net/http\"\n\n\t\"github.com/julienschmidt/httprouter\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/beta/errs\"\n)\n\n// remoteAuthHandler is an AuthHandler that calls a remotely hosted auth handler.\n//\n// This is used when the auth handler is not being hosted in this container, but in\n// another container somewhere else in the cluster.\ntype remoteAuthHandler struct {\n\tserver         *Server        // The server we're running in\n\thostingService config.Service // The service name of the remote auth handler\n\tauthURL        string         // The URL of the remote auth handler\n\toriginal       AuthHandler\n\tlogger         zerolog.Logger\n\ttraceLogs      bool\n}\n\nvar _ AuthHandler = (*remoteAuthHandler)(nil)\n\nfunc (r *remoteAuthHandler) Authenticate(c IncomingContext) (model.AuthInfo, error) {\n\t// Quickly check if the auth data is parsable here, if it isn't we can return early\n\t// without making a remote call.\n\tif err := r.original.ParseAuthData(c); err != nil {\n\t\treturn model.AuthInfo{}, err\n\t}\n\n\tif r.traceLogs {\n\t\tr.logger.Trace().Msg(\"calling auth handler\")\n\t}\n\n\t// Create the auth request\n\tauthReq, err := http.NewRequestWithContext(c.ctx, http.MethodPost, r.authURL, nil)\n\tif err != nil {\n\t\tr.logger.Err(err).Msg(\"unable to create auth request\")\n\t\treturn model.AuthInfo{}, errs.Wrap(err, \"unable to create auth request\")\n\t}\n\n\t// Copy over any data which might be needed by the auth handler (query string, headers and cookies)\n\tauthReq.URL.RawQuery = c.req.URL.RawQuery\n\tauthReq.Header = maps.Clone(c.req.Header)\n\tdelete(authReq.Header, \"X-Encore-Auth\") // Don't copy the platform auth key across\n\n\t// Add call meta data to the request, so the receiving service can use to allow access to the auth handler\n\t// This also passes a shared TraceID to the auth handler, so it and the service called after the gateway\n\t// can both be traced as part of the same trace.\n\tmeta := CallMetaFromContext(c.ctx)\n\tmeta.Internal = &InternalCallMeta{\n\t\t// Note we're acting as a ApiCaller here so we can access the __encore/auth_handler endpoint and\n\t\t// also receive full marshalled errors back from the auth handler (as ApiCaller's are allowed PrivateAPIAccess)\n\t\tCaller: ApiCaller{ServiceName: \"gateway\", Endpoint: \"__encore/authhandler\"},\n\t}\n\tif err := meta.AddToRequest(r.server, r.hostingService, transport.HTTPRequest(authReq)); err != nil {\n\t\tr.logger.Err(err).Msg(\"unable to add call metadata to auth request\")\n\t\treturn model.AuthInfo{}, errs.Wrap(err, \"unable to add call metadata to auth request\")\n\t}\n\n\t// Call the auth handler\n\tresp, err := r.server.httpClient.Do(authReq)\n\tif err != nil {\n\t\treturn model.AuthInfo{}, errs.Wrap(err, \"unable to make auth request\")\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// Handle the response\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\t\t// user was authenticated, let's decode it\n\t\tt := transport.HTTPResponse(resp)\n\t\tuid, found := t.ReadMeta(\"UserID\")\n\t\tif !found {\n\t\t\treturn model.AuthInfo{}, errs.B().Code(errs.Unauthenticated).Msg(\"no uid\").Err()\n\t\t}\n\n\t\tauthData := newAuthDataObj()\n\t\tif err := authJSON.NewDecoder(resp.Body).Decode(authData); err != nil {\n\t\t\treturn model.AuthInfo{}, errs.Wrap(err, \"unable to decode auth data\")\n\t\t}\n\n\t\treturn model.AuthInfo{\n\t\t\tUID:      model.UID(uid),\n\t\t\tUserData: authData,\n\t\t}, nil\n\n\tdefault:\n\t\t// the user was not authenticated, let's return the error\n\t\terr := unmarshalErrorResponse(resp)\n\t\tif errs.Code(err) != errs.Unauthenticated && r.traceLogs {\n\t\t\tr.logger.Trace().Err(err).Msg(\"auth handler returned error\")\n\t\t}\n\n\t\treturn model.AuthInfo{}, err\n\t}\n}\n\n// handleRemoteAuthCall is the server side of remoteAuthHandler.Authenticate\nfunc (s *Server) handleRemoteAuthCall(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {\n\t// Parse the incoming call metadata\n\tmeta := CallMetaFromContext(req.Context())\n\n\t// Check if the caller was the gateway, if not return an error\n\tif meta.Internal == nil || meta.Internal.Caller == nil {\n\t\terrs.HTTPErrorWithCode(w, errs.B().Code(errs.PermissionDenied).Msg(\"permission denied\").Err(), 0)\n\t\treturn\n\t}\n\tcaller, ok := meta.Internal.Caller.(ApiCaller)\n\tif !ok || caller.ServiceName != \"gateway\" || caller.Endpoint != \"__encore/authhandler\" {\n\t\terrs.HTTPErrorWithCode(w, errs.B().Code(errs.PermissionDenied).Msg(\"permission denied\").Err(), 0)\n\t\treturn\n\t}\n\n\t// originalC captures the meta _before_ we removed the internal call metadata\n\t// this is used for returnError to marshal the full error\n\toriginalC := s.NewIncomingContext(w, req, nil, meta)\n\n\t// Remove the internal call metadata, so it doesn't get passed to the auth handler\n\tmeta.Internal = nil\n\n\t// Create a new IncomingContext\n\tc := s.NewIncomingContext(w, req, nil, meta)\n\n\t// Call the original auth handler\n\tauthInfo, err := s.authHandler.Authenticate(c)\n\tif err != nil {\n\t\treturnError(originalC, err, 0, nil)\n\t\treturn\n\t}\n\n\t// Add the user ID to the response metadata\n\tt := transport.HTTPResponseWriter(w)\n\tt.SetMeta(\"UserID\", string(authInfo.UID))\n\n\t// Write the auth info to the response\n\tif err := authJSON.NewEncoder(w).Encode(authInfo.UserData); err != nil {\n\t\treturnError(originalC, err, 0, nil)\n\t\treturn\n\t}\n}\n\nfunc (r *remoteAuthHandler) HostedByService() string {\n\treturn r.hostingService.Name\n}\n\nfunc (r *remoteAuthHandler) ParseAuthData(c IncomingContext) error {\n\treturn r.original.ParseAuthData(c)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/call_context.go",
    "content": "//go:build encore_app\n\npackage api\n\nimport (\n\t\"context\"\n)\n\nfunc NewCallContext(ctx context.Context) CallContext {\n\treturn Singleton.NewCallContext(ctx)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/call_meta.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/apisdk/api/svcauth\"\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/shared/cloud\"\n\t\"encore.dev/beta/errs\"\n)\n\nvar (\n\t// authJSON can not be pretty printed, as new lines are not allowed in HTTP headers\n\tauthJSON = jsoniter.Config{\n\t\tIndentionStep:          0,\n\t\tValidateJsonRawMessage: true,\n\t\tTagKey:                 \"auth-json\",\n\t}.Froze()\n)\n\nconst (\n\tcallerMetaName = \"Caller\"\n\tcalleeMetaName = \"Callee\"\n)\n\ntype metaContextKey string\n\nvar (\n\tmetaContextKeyAuthInfo metaContextKey = \"requestMeta\"\n)\n\n// CallMeta is metadata for an RPC call\ntype CallMeta struct {\n\t// Untrusted fields\n\t// (i.e. we allow these fields to be passed in from anywhere, including external requests)\n\tTraceID       model.TraceID      // The trace ID of the calling request (zero if not tracing)\n\tSpanID        model.SpanID       // The span ID of _this_ request if predefined by the caller (zero in most cases)\n\tParentSpanID  model.SpanID       // The span ID of the calling request (zero if there's no parent)\n\tParentEventID model.TraceEventID // The event ID which started the RPC call (zero if there's no parent)\n\tCorrelationID string             // The correlation ID of the calling request\n\tTraceSampled  bool               // Whether the caller sampled trace info\n\n\t// Internal meta data which gets populated by Encore on service to service calls\n\t//\n\t// If set, the values can be trusted as they would have been authenticated to be correct\n\tInternal *InternalCallMeta\n}\n\n// InternalCallMeta is metadata for an RPC call which is being made\n// between two Encore services within the same application.\ntype InternalCallMeta struct {\n\tCaller   Caller // The name of the service which is making the call\n\tAuthUID  string // The UID of the authenticated user\n\tAuthData any    // The data of the authenticated user\n}\n\n// addInternalCallMeta adds internal metadata to the external request\n// we're about to make.\n//\n// It does this in a transport agnostic way, allowing us to add metadata\n// to any transport request supported by Encore.\nfunc (s *Server) metaFromAPICall(call *model.APICall) (meta CallMeta, err error) {\n\t// Check the auth data before we marshal\n\t// this is because auth.WithContext can set the auth data to any type\n\t// and we want to ensure it's the expected type before we marshal it\n\tif err := CheckAuthData(call.UserID, call.AuthData); err != nil {\n\t\treturn meta, err\n\t}\n\n\t// Default the caller to the current app deployment\n\tvar caller Caller\n\tcaller = AppCaller{s.runtime.DeployID}\n\n\t// Trace the source data if we're tracing\n\tif call != nil && call.Source != nil {\n\t\tmeta.TraceID = call.Source.TraceID\n\t\tmeta.ParentSpanID = call.Source.SpanID\n\t\tmeta.ParentEventID = call.StartEventID\n\t\tmeta.CorrelationID = call.Source.ExtCorrelationID\n\t\tmeta.TraceSampled = call.Source.Traced\n\n\t\tif call.Source.RPCData != nil && call.Source.RPCData.Desc != nil {\n\t\t\t// If we're processing an API call, let's update the caller\n\t\t\tcaller = ApiCaller{\n\t\t\t\tServiceName: call.Source.RPCData.Desc.Service,\n\t\t\t\tEndpoint:    call.Source.RPCData.Desc.Endpoint,\n\t\t\t}\n\t\t} else if call.Source.MsgData != nil && call.Source.MsgData.MessageID != \"\" {\n\t\t\t// If we're processing a PubSub message, let's update the caller\n\t\t\tcaller = PubSubCaller{\n\t\t\t\tTopic:        call.Source.MsgData.Desc.Topic,\n\t\t\t\tSubscription: call.Source.MsgData.Desc.Subscription,\n\t\t\t\tMessageID:    call.Source.MsgData.MessageID,\n\t\t\t}\n\t\t}\n\t}\n\n\tmeta.Internal = &InternalCallMeta{\n\t\tCaller:   caller,\n\t\tAuthUID:  string(call.UserID),\n\t\tAuthData: call.AuthData,\n\t}\n\n\treturn meta, nil\n}\n\n// AddToRequest adds the metadata to the given request\nfunc (meta CallMeta) AddToRequest(server *Server, targetService config.Service, req transport.Transport) error {\n\t// Future proofing: if we ever create a breaking change to the transport meta\n\t// we can use this version number to indicate which version of the meta we're using\n\treq.SetMeta(\"Version\", \"1\")\n\n\t// If we're tracing, pass the trace ID, span ID and event ID to the downstream service\n\tif !meta.TraceID.IsZero() {\n\t\t// Encode Encore's trace ID and span ID as the traceparent header\n\t\tsampled := \"00\"\n\t\tif meta.TraceSampled {\n\t\t\tsampled = \"01\"\n\t\t}\n\t\treq.SetMeta(transport.TraceParentKey, fmt.Sprintf(\"00-%x-%x-%s\", meta.TraceID[:], meta.ParentSpanID[:], sampled))\n\n\t\tif !meta.ParentSpanID.IsZero() {\n\t\t\t// Because Encore does not count an RPC call as a span, but rather a set of events within a span\n\t\t\t// we also need to pass the event ID which started the RPC call in the tracestate header\n\t\t\teventID := strconv.FormatUint(uint64(meta.ParentEventID), 36)\n\t\t\tif server.runtime.EnvCloud == cloud.GCP || server.runtime.EnvCloud == cloud.Encore {\n\t\t\t\t// In GCP they add their own span's into the \"trace\", which breaks our parent span link\n\t\t\t\t// so we need to add the parent span ID to the tracestate header so we can track our own parent span\n\t\t\t\treq.SetMeta(transport.TraceStateKey, fmt.Sprintf(\"%s=%x,%s=%s\", eventTraceStateSpanIDKey, meta.ParentSpanID[:], eventTraceStateEventIDKey, eventID))\n\t\t\t} else {\n\t\t\t\t// Otherwise all we need to know is our event ID\n\t\t\t\treq.SetMeta(transport.TraceStateKey, fmt.Sprintf(\"%s=%s\", eventTraceStateEventIDKey, eventID))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Pass the correlation ID to the downstream service.\n\t// However, we do _not_ pass the X-Request-ID down, as it is not meant to be propagated through request chains\n\tif meta.CorrelationID != \"\" {\n\t\treq.SetMeta(transport.CorrelationIDKey, meta.CorrelationID)\n\t}\n\n\t// If we're making an internal call, add the internal metadata to the request\n\tif meta.Internal != nil {\n\t\t// Add a marker to the request to indicate that this is an internal call\n\t\treq.SetMeta(callerMetaName, meta.Internal.Caller.CallerString())\n\n\t\t// Add the auth data\n\t\tif meta.Internal.AuthUID != \"\" {\n\t\t\treq.SetMeta(\"UserID\", meta.Internal.AuthUID)\n\n\t\t\tif meta.Internal.AuthData != nil {\n\t\t\t\tauthData, err := authJSON.Marshal(meta.Internal.AuthData)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errs.B().Cause(err).Msg(\"failed to marshal auth data\").Err()\n\t\t\t\t}\n\t\t\t\treq.SetMeta(\"AuthData\", string(authData))\n\t\t\t}\n\t\t}\n\n\t\t// If we're making an internal call, sign the request\n\t\ttargetAuth := server.outboundSvcAuth[targetService.ServiceAuth.Method]\n\t\tif targetAuth == nil {\n\t\t\treturn errs.B().Msg(\"no internal auth method configured to talk with target service\").Err()\n\t\t}\n\t\tif err := svcauth.Sign(targetAuth, req); err != nil {\n\t\t\treturn errs.B().Cause(err).Msg(\"failed to sign internal call\").Err()\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (meta CallMeta) IsServiceToService() bool {\n\treturn meta.Internal != nil && meta.Internal.Caller != nil\n}\n\nfunc (meta CallMeta) PrivateAPIAccess() bool {\n\treturn meta.Internal != nil && meta.Internal.Caller != nil && meta.Internal.Caller.PrivateAPIAccess()\n}\n\n// MetaFromRequest reads the metadata from the given request and returns it\nfunc (s *Server) MetaFromRequest(req transport.Transport) (meta CallMeta, err error) {\n\t// Read the meta version if set and check it's only version 1\n\t// as that's the only version we support\n\tif metaVersion, found := req.ReadMeta(\"Version\"); found && metaVersion != \"1\" {\n\t\treturn CallMeta{}, errors.New(\"unknown encore meta version\")\n\t}\n\n\t// If it was an internal call, read the internal metadata\n\tif callerStr, found := req.ReadMeta(callerMetaName); found {\n\t\tisInternalCall, err := svcauth.Verify(req, s.inboundSvcAuth)\n\t\tif err != nil {\n\t\t\treturn CallMeta{}, fmt.Errorf(\"failed to verify internal call: %w\", err)\n\t\t}\n\t\tif !isInternalCall {\n\t\t\treturn CallMeta{}, errors.New(\"no internal call auth found\")\n\t\t}\n\n\t\tcaller, err := ParseCallerString(callerStr)\n\t\tif err != nil {\n\t\t\treturn CallMeta{}, fmt.Errorf(\"failed to parse caller string: %w\", err)\n\t\t}\n\n\t\tmeta.Internal = &InternalCallMeta{\n\t\t\tCaller: caller,\n\t\t}\n\n\t\t// Pull the auth data out of the request\n\t\tif uid, found := req.ReadMeta(\"UserID\"); found && uid != \"\" {\n\t\t\tmeta.Internal.AuthUID = uid\n\n\t\t\tif data, found := req.ReadMeta(\"AuthData\"); found && data != \"\" {\n\t\t\t\tmeta.Internal.AuthData = newAuthDataObj()\n\n\t\t\t\tif err := authJSON.Unmarshal([]byte(data), meta.Internal.AuthData); err != nil {\n\t\t\t\t\treturn CallMeta{}, errs.B().Cause(err).Msg(\"failed to unmarshal auth data\").Err()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// If we where tracing read the trace ID, span ID\n\tif traceParent, found := req.ReadMeta(transport.TraceParentKey); found &&\n\t\t// For now we only read the traceparent for internal-to-internal calls, this is because CloudRun\n\t\t// is adding a traceparent header to all requests, which is causing our trace system to get confused\n\t\t// and think that the initial request is a child of another already traced request\n\t\t//\n\t\t// In the future we should be able to remove this check and read the traceparent header for all requests\n\t\t// to interopt with other tracing systems.\n\t\tmeta.Internal != nil {\n\n\t\tmeta.TraceID, meta.ParentSpanID, meta.TraceSampled, _ = parseTraceParent(traceParent)\n\n\t\t// If the caller is a gateway, ignore the parent span id as gateways don't currently record a span.\n\t\t// If we include it the root request won't be tagged as such.\n\t\tif _, isGateway := meta.Internal.Caller.(GatewayCaller); isGateway {\n\t\t\tmeta.ParentSpanID = model.SpanID{}\n\t\t}\n\n\t\tif traceState, found := req.ReadMetaValues(transport.TraceStateKey); found {\n\t\t\tparentEventID, parentSpanID, ok := parseTraceState(traceState)\n\t\t\tif ok {\n\t\t\t\tmeta.ParentEventID = parentEventID\n\n\t\t\t\t// If we where given a parent span ID, use that instead of the one from the traceparent header\n\t\t\t\t// This is because GCP Cloud Run will add it's own spans in before the application code is run\n\t\t\t\t// and thus we lose the parent span ID from the traceparent header\n\t\t\t\tif !parentSpanID.IsZero() {\n\t\t\t\t\tmeta.ParentSpanID = parentSpanID\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif correlationID, found := req.ReadMeta(transport.CorrelationIDKey); found {\n\t\t// Don't allow arbitrary correlation IDs to be passed through\n\t\tif len(meta.CorrelationID) > 64 {\n\t\t\tmeta.CorrelationID = correlationID[:64]\n\t\t} else {\n\t\t\tmeta.CorrelationID = correlationID\n\t\t}\n\t}\n\n\treturn meta, nil\n}\n\n// parseTraceParent parses the trace and span ids from s, which is assumed\n// to be in the format of the traceparent header (see https://www.w3.org/TR/trace-context/).\n// If it's not a valid traceparent header it returns zero ids and ok == false.\nfunc parseTraceParent(s string) (traceID model.TraceID, spanID model.SpanID, sampled, ok bool) {\n\tconst (\n\t\tversion       = \"00\"\n\t\ttraceIDLen    = 32\n\t\tspanIDLen     = 16\n\t\ttraceFlagsLen = 2\n\n\t\tverStart     = 0\n\t\tverEnd       = verStart + len(version)\n\t\tverSep       = verEnd\n\t\ttraceIDStart = verSep + 1\n\t\ttraceIDEnd   = traceIDStart + traceIDLen\n\t\ttraceIDSep   = traceIDEnd\n\t\tspanIDStart  = traceIDSep + 1\n\t\tspanIDEnd    = spanIDStart + spanIDLen\n\t\tspanIDSep    = spanIDEnd\n\t\tflagsStart   = spanIDSep + 1\n\t\tflagsEnd     = flagsStart + traceFlagsLen\n\t\ttotalLen     = flagsEnd\n\t)\n\n\tif len(s) != totalLen || s[verStart:verEnd] != version || s[verSep] != '-' || s[traceIDSep] != '-' || s[spanIDSep] != '-' {\n\t\treturn model.TraceID{}, model.SpanID{}, false, false\n\t}\n\n\t_, err := hex.Decode(traceID[:], []byte(s[traceIDStart:traceIDEnd]))\n\tif err != nil {\n\t\treturn model.TraceID{}, model.SpanID{}, false, false\n\t}\n\n\t_, err = hex.Decode(spanID[:], []byte(s[spanIDStart:spanIDEnd]))\n\tif err != nil {\n\t\treturn model.TraceID{}, model.SpanID{}, false, false\n\t}\n\n\tvar flags [1]byte\n\t_, err = hex.Decode(flags[:], []byte(s[flagsStart:flagsEnd]))\n\tif err != nil {\n\t\treturn model.TraceID{}, model.SpanID{}, false, false\n\t}\n\n\tsampled = flags[0]&1 == 1\n\n\treturn traceID, spanID, sampled, true\n}\n\n// parseTraceState parses the trace event id from the tracestate header (see https://www.w3.org/TR/trace-context/).\n// If no valid Encore event ID can be parsed it returns zero and ok == false.\n//\n// Note the spec allows for multiple `tracestate` headers to be sent, so we need to check all of them.\nfunc parseTraceState(headerValues []string) (eventID model.TraceEventID, parentSpanID model.SpanID, ok bool) {\n\tfor _, thisHeader := range headerValues {\n\t\ttheseFields := strings.Split(thisHeader, \",\")\n\t\tfor _, field := range theseFields {\n\t\t\tparts := strings.Split(field, \"=\")\n\t\t\tif len(parts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tswitch parts[0] {\n\t\t\tcase eventTraceStateSpanIDKey:\n\t\t\t\tspanIDBytes, err := hex.DecodeString(parts[1])\n\t\t\t\tif err != nil || len(spanIDBytes) != 8 {\n\t\t\t\t\treturn 0, model.SpanID{}, false\n\t\t\t\t}\n\t\t\t\tcopy(parentSpanID[:], spanIDBytes)\n\n\t\t\tcase eventTraceStateEventIDKey:\n\t\t\t\teventIDUint, err := strconv.ParseUint(parts[1], 36, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn 0, model.SpanID{}, false\n\t\t\t\t}\n\n\t\t\t\teventID = model.TraceEventID(eventIDUint)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn eventID, parentSpanID, eventID != 0\n}\n\nfunc SetCallMetaInContext(ctx context.Context, meta CallMeta) context.Context {\n\treturn context.WithValue(ctx, metaContextKeyAuthInfo, meta)\n}\n\nfunc CallMetaFromContext(ctx context.Context) CallMeta {\n\treturn ctx.Value(metaContextKeyAuthInfo).(CallMeta)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/call_meta_test.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/frankban/quicktest\"\n\n\t\"encore.dev/appruntime/exported/model\"\n)\n\nfunc TestParseTraceParent(t *testing.T) {\n\tq := quicktest.New(t)\n\n\texpTraceID, _ := model.GenTraceID()\n\texpSpanID, _ := model.GenSpanID()\n\texpSampled := \"01\"\n\n\ttraceID, spanID, sampled, ok := parseTraceParent(fmt.Sprintf(\"00-%x-%x-%s\", expTraceID[:], expSpanID[:], expSampled))\n\tq.Assert(ok, quicktest.IsTrue)\n\tq.Assert(traceID, quicktest.DeepEquals, expTraceID)\n\tq.Assert(spanID, quicktest.DeepEquals, expSpanID)\n\tq.Assert(sampled, quicktest.Equals, true)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/callers.go",
    "content": "package api\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Caller is an interface which can be used to identify the caller of an RPC call.\n// if the caller is an authenticated source.\ntype Caller interface {\n\tCallerString() string   // What is the caller wireformat?\n\tPrivateAPIAccess() bool // Is this caller allowed to access private APIs?\n}\n\n// ApiCaller is a caller which represents an RPC which made a service-to-service API call\ntype ApiCaller struct {\n\tServiceName string\n\tEndpoint    string\n}\n\nfunc (s ApiCaller) CallerString() string {\n\treturn fmt.Sprintf(\"api:%s.%s\", s.ServiceName, s.Endpoint)\n}\n\nfunc (s ApiCaller) PrivateAPIAccess() bool {\n\treturn true\n}\n\n// PubSubCaller is a caller which represents a PubSub subscription which made a service-to-service API call\ntype PubSubCaller struct {\n\tTopic        string\n\tSubscription string\n\tMessageID    string\n}\n\nfunc (p PubSubCaller) CallerString() string {\n\treturn fmt.Sprintf(\"pubsub:%s:%s:%s\", p.Topic, p.Subscription, p.MessageID)\n}\n\nfunc (p PubSubCaller) PrivateAPIAccess() bool {\n\treturn true\n}\n\n// AppCaller is a caller which represents the app itself made a service-to-service API call, but outside any traced process\n// - This most likely means the call was made from a background process or init function\ntype AppCaller struct {\n\tDeployID string\n}\n\nfunc (a AppCaller) CallerString() string {\n\treturn fmt.Sprintf(\"app:%s\", a.DeployID)\n}\n\nfunc (a AppCaller) PrivateAPIAccess() bool {\n\treturn true\n}\n\n// GatewayCaller is a caller which represents an API gateway within the application made a service-to-service API call\n// This is used to identify the fact that the call was made from a gateway, and not from a background process or init function\n// and should not be allowed access to the private routes\ntype GatewayCaller struct {\n\tGatewayName string // The encore name of the gateway\n}\n\nfunc (g GatewayCaller) CallerString() string {\n\t// Return the string with \".none\" for backwards compatibility.\n\t// This can be removed later when all services support the new format.\n\treturn fmt.Sprintf(\"gateway:%s.none\", g.GatewayName)\n}\n\nfunc (g GatewayCaller) PrivateAPIAccess() bool {\n\treturn false\n}\n\n// EncoreCaller represents an RPC call made from Encore's central systems (such as the Cloud dashboard)\ntype EncoreCaller struct {\n\tPrincipal string // The principal which made the call, could be an end user or a service\n}\n\nfunc (e EncoreCaller) CallerString() string {\n\treturn fmt.Sprintf(\"encore:%s\", e.Principal)\n}\n\nfunc (e EncoreCaller) PrivateAPIAccess() bool {\n\treturn true\n}\n\n// ParseCallerString parses a caller string into a Caller object\nfunc ParseCallerString(callerStr string) (Caller, error) {\n\tswitch {\n\tcase strings.HasPrefix(callerStr, \"api:\"):\n\t\tservice, endpoint, found := strings.Cut(callerStr[len(\"api:\"):], \".\")\n\t\tif !found {\n\t\t\treturn nil, errors.New(\"invalid api caller\")\n\t\t}\n\t\treturn ApiCaller{service, endpoint}, nil\n\tcase strings.HasPrefix(callerStr, \"pubsub:\"):\n\t\ttopic, subscriptionAndMsgId, found := strings.Cut(callerStr[len(\"pubsub:\"):], \":\")\n\t\tif !found {\n\t\t\treturn nil, errors.New(\"invalid pubsub caller\")\n\t\t}\n\t\tsubscription, msgId, found := strings.Cut(subscriptionAndMsgId, \":\")\n\t\tif !found {\n\t\t\treturn nil, errors.New(\"invalid pubsub caller\")\n\t\t}\n\t\treturn PubSubCaller{topic, subscription, msgId}, nil\n\tcase strings.HasPrefix(callerStr, \"app:\"):\n\t\treturn AppCaller{callerStr[len(\"app:\"):]}, nil\n\tcase strings.HasPrefix(callerStr, \"gateway:\"):\n\t\t// We used to have \"service.endpoint\" but now we only have the gateway name.\n\t\t// Still parse the old format for backwards compatibility.\n\t\tgateway, _, _ := strings.Cut(callerStr[len(\"gateway:\"):], \".\")\n\t\treturn GatewayCaller{gateway}, nil\n\tcase strings.HasPrefix(callerStr, \"encore:\"):\n\t\treturn EncoreCaller{callerStr[len(\"encore:\"):]}, nil\n\tdefault:\n\t\treturn nil, errors.New(\"invalid caller\")\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/capture.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/felixge/httpsnoop\"\n)\n\n// rawRequestBodyCapturer is an io.ReadCloser that can be substituted for\n// (*http.Request).Body to capture its payload for tracing raw requests.\n//\n// It only parses requests matching certain mime types and only\n// a limited amount of data suitable for displaying in traces.\ntype rawRequestBodyCapturer struct {\n\tstate      captureState\n\tunderlying io.ReadCloser\n\n\tbufMu      sync.Mutex\n\tbuf        *bytes.Buffer\n\toverflowed bool // whether the buffer overflowed\n}\n\nfunc newRawRequestBodyCapturer(req *http.Request) *rawRequestBodyCapturer {\n\tbuf := captureBufferPool.Get().(*bytes.Buffer)\n\tbuf.Reset()\n\n\tvar state captureState\n\t// Don't capture WebSockets etc\n\tif isUpgradeRequest(req) {\n\t\tstate = notCapturing\n\t} else {\n\t\tstate = shouldCaptureContentType(req.Header.Get(\"Content-Type\"), false)\n\t}\n\n\treturn &rawRequestBodyCapturer{\n\t\tstate:      state,\n\t\tunderlying: req.Body,\n\t\tbuf:        buf,\n\t}\n}\n\n// captureState defines whether the capturer is actively capturing.\ntype captureState uint8\n\nconst (\n\tnotCapturing captureState = 0\n\tcapturing    captureState = 1\n\n\t// peeking means the capturer is evaluating whether or not to capture\n\t// the full request, based on peeking at the first few bytes of\n\t// the request. It's used when the Content-Type is missing or vague.\n\tpeeking captureState = 2\n)\n\nconst (\n\t// MaxRawRequestCaptureLen is the maximum buffer size to keep for\n\t// captured request payloads.\n\tMaxRawRequestCaptureLen  = 10 << 10  // 10 KiB\n\tMaxRawResponseCaptureLen = 100 << 10 // 100 KiB\n)\n\n// Read implements io.Reader.\nfunc (c *rawRequestBodyCapturer) Read(b []byte) (int, error) {\n\tn, err := c.underlying.Read(b)\n\n\tif n > 0 && c.state != notCapturing && !c.overflowed {\n\t\tc.bufMu.Lock()\n\t\tdefer c.bufMu.Unlock()\n\n\t\t// Guard against the buffer having been disposed of.\n\t\tif c.buf != nil {\n\t\t\tremaining := MaxRawRequestCaptureLen - c.buf.Len()\n\t\t\ttoWrite := n\n\t\t\t// If we can write less than we read, mark the buffer as having overflowed.\n\t\t\tif remaining < n {\n\t\t\t\ttoWrite = remaining\n\t\t\t\tc.overflowed = true\n\t\t\t}\n\t\t\tc.buf.Write(b[:toWrite])\n\n\t\t\tif c.state == peeking && c.buf.Len() >= 512 {\n\t\t\t\tcontentType := http.DetectContentType(c.buf.Bytes())\n\t\t\t\tc.state = shouldCaptureContentType(contentType, true)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn n, err\n}\n\n// Close implements io.Closer.\nfunc (c *rawRequestBodyCapturer) Close() error {\n\treturn c.underlying.Close()\n}\n\n// FinishCapturing finishes the capturing, returning the captured bytes thus far.\n// The returned buf may be used until Dispose is called but not after that.\n//\n// If c is nil it reports nil, false.\nfunc (c *rawRequestBodyCapturer) FinishCapturing() (data []byte, overflowed bool) {\n\tif c == nil {\n\t\treturn nil, false\n\t}\n\n\tc.bufMu.Lock()\n\tdefer c.bufMu.Unlock()\n\n\t// If we're definitely not capturing, return no data.\n\tif c.state == notCapturing {\n\t\treturn nil, false\n\t}\n\n\t// If we're peeking, make a decision.\n\tdata = c.buf.Bytes()\n\tif c.state == peeking {\n\t\tcontentType := http.DetectContentType(data)\n\t\tc.state = shouldCaptureContentType(contentType, true)\n\t\tif c.state != capturing {\n\t\t\t// Not capturing after all; return no data.\n\t\t\treturn nil, false\n\t\t}\n\t}\n\n\t// Stop capturing.\n\tc.state = notCapturing\n\treturn data, c.overflowed\n}\n\n// Dispose disposes of the capturer, returning the\n// resources to the pool for use by future requests.\n//\n// The capturer may not be used after calling Dispose.\nfunc (c *rawRequestBodyCapturer) Dispose() {\n\tc.bufMu.Lock()\n\tdefer c.bufMu.Unlock()\n\tif c.buf != nil {\n\t\tcaptureBufferPool.Put(c.buf)\n\t\tc.buf = nil\n\t}\n}\n\nvar captureBufferPool = sync.Pool{\n\tNew: func() any {\n\t\treturn new(bytes.Buffer)\n\t},\n}\n\nfunc shouldCaptureContentType(contentType string, didPeek bool) captureState {\n\tif idx := strings.IndexByte(contentType, ';'); idx >= 0 {\n\t\tcontentType = contentType[:idx]\n\t}\n\n\tswitch contentType {\n\tcase \"application/json\", \"text/plain\", \"application/x-www-form-urlencoded\", \"text/csv\",\n\t\t\"text/javascript\", \"application/ld+json\", \"application/xml\", \"text/xml\", \"application/atom+xml\":\n\t\treturn capturing\n\n\t// Unknown content type; peek at the data to decide what to do.\n\tcase \"\", \"application/octet-stream\":\n\t\t// If we already peeked, don't capture these.\n\t\tif didPeek {\n\t\t\treturn notCapturing\n\t\t}\n\t\treturn peeking\n\n\tdefault:\n\t\treturn notCapturing\n\t}\n}\n\nfunc isUpgradeRequest(req *http.Request) bool {\n\treturn req.Header.Get(\"Upgrade\") != \"\"\n}\n\ntype rawResponseCapturer struct {\n\tstate captureState\n\tw     http.ResponseWriter\n\thooks httpsnoop.Hooks\n\n\tCode          int\n\tHeaderWritten bool\n\tBytesWritten  int64\n\n\t// Header is a snapshot of the HTTP headers when they get written.\n\tHeader http.Header\n\n\tbufMu      sync.Mutex\n\tbuf        *bytes.Buffer\n\toverflowed bool // whether the buffer overflowed\n}\n\nfunc newRawResponseCapturer(w http.ResponseWriter, req *http.Request) *rawResponseCapturer {\n\tbuf := captureBufferPool.Get().(*bytes.Buffer)\n\tbuf.Reset()\n\n\tvar state captureState\n\t// Don't capture WebSockets etc\n\tif isUpgradeRequest(req) {\n\t\tstate = notCapturing\n\t} else {\n\t\tstate = peeking\n\t}\n\n\tc := &rawResponseCapturer{\n\t\tstate: state,\n\t\tw:     w,\n\t\tbuf:   buf,\n\t}\n\tc.hooks = httpsnoop.Hooks{\n\t\tWriteHeader: func(next httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {\n\t\t\treturn func(code int) {\n\t\t\t\tc.onWriteHeader(code, next)\n\t\t\t}\n\t\t},\n\n\t\tWrite: func(next httpsnoop.WriteFunc) httpsnoop.WriteFunc {\n\t\t\treturn func(p []byte) (int, error) {\n\t\t\t\treturn c.onWrite(p, next)\n\t\t\t}\n\t\t},\n\n\t\tReadFrom: func(next httpsnoop.ReadFromFunc) httpsnoop.ReadFromFunc {\n\t\t\treturn func(src io.Reader) (int64, error) {\n\t\t\t\treturn c.onReadFrom(src, next)\n\t\t\t}\n\t\t},\n\t}\n\treturn c\n}\n\n// InvokeHandler invokes the given HTTP handler, using the response capturer to\n// capture information about the response.\nfunc (c *rawResponseCapturer) InvokeHandler(h http.Handler, req *http.Request) {\n\tc.Code = 200 // by default\n\tw := httpsnoop.Wrap(c.w, c.hooks)\n\th.ServeHTTP(w, req)\n}\n\nfunc (c *rawResponseCapturer) onWriteHeader(code int, next httpsnoop.WriteHeaderFunc) {\n\tnext(code)\n\tc.markHeadersWritten(code)\n}\n\nfunc (c *rawResponseCapturer) onWrite(p []byte, next httpsnoop.WriteFunc) (int, error) {\n\tn, err := next(p)\n\tc.markHeadersWritten(200)\n\tc.BytesWritten += int64(n)\n\tc.writeToBuf(p[:n])\n\treturn n, err\n}\n\nfunc (c *rawResponseCapturer) onReadFrom(src io.Reader, next httpsnoop.ReadFromFunc) (int64, error) {\n\tif c.state != notCapturing {\n\t\t// If we're capturing data,\n\t\tsrc = io.TeeReader(src, rawResponseCapturerTeeWriter{c})\n\t}\n\tn, err := next(src)\n\tc.markHeadersWritten(200)\n\tc.BytesWritten += n\n\treturn n, err\n}\n\n// writeToBuf writes data to the capture buffer, if we're capturing data.\nfunc (c *rawResponseCapturer) writeToBuf(b []byte) {\n\tn := len(b)\n\n\tif n > 0 && c.state != notCapturing && !c.overflowed {\n\t\tc.bufMu.Lock()\n\t\tdefer c.bufMu.Unlock()\n\n\t\t// Guard against the buffer having been disposed of.\n\t\tif c.buf != nil {\n\t\t\tremaining := MaxRawResponseCaptureLen - c.buf.Len()\n\t\t\ttoWrite := n\n\t\t\t// If we can write less than we read, mark the buffer as having overflowed.\n\t\t\tif remaining < n {\n\t\t\t\ttoWrite = remaining\n\t\t\t\tc.overflowed = true\n\t\t\t}\n\t\t\tc.buf.Write(b[:toWrite])\n\n\t\t\tif c.state == peeking && c.buf.Len() >= 512 {\n\t\t\t\tcontentType := http.DetectContentType(c.buf.Bytes())\n\t\t\t\tc.state = shouldCaptureContentType(contentType, true)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// markHeadersWritten marks the headers as written, using the given status code\n// and snapshotting the HTTP headers written.\n// If the headers have already been written, this is a no-op.\nfunc (c *rawResponseCapturer) markHeadersWritten(code int) {\n\tif c.HeaderWritten {\n\t\treturn\n\t}\n\tc.Code = code\n\tc.HeaderWritten = true\n\n\t// Snapshot the headers\n\tsrc := c.w.Header()\n\tc.Header = make(http.Header, len(src))\n\tfor k, v := range src {\n\t\tc.Header[k] = v\n\t}\n}\n\n// FinishCapturing finishes the capturing, returning the captured bytes thus far.\n// The returned buf may be used until Dispose is called but not after that.\n//\n// If c is nil it reports nil, false.\nfunc (c *rawResponseCapturer) FinishCapturing() (data []byte, overflowed bool) {\n\tif c == nil {\n\t\treturn nil, false\n\t}\n\n\tc.bufMu.Lock()\n\tdefer c.bufMu.Unlock()\n\n\t// If we're definitely not capturing, return no data.\n\tif c.state == notCapturing {\n\t\treturn nil, false\n\t}\n\n\t// If we're peeking, make a decision.\n\tdata = c.buf.Bytes()\n\tif c.state == peeking {\n\t\tcontentType := http.DetectContentType(data)\n\t\tc.state = shouldCaptureContentType(contentType, true)\n\t\tif c.state != capturing {\n\t\t\t// Not capturing after all; return no data.\n\t\t\treturn nil, false\n\t\t}\n\t}\n\n\t// Stop capturing.\n\tc.state = notCapturing\n\treturn data, c.overflowed\n}\n\n// Dispose disposes of the capturer, returning the\n// resources to the pool for use by future requests.\n//\n// The capturer may not be used after calling Dispose.\nfunc (c *rawResponseCapturer) Dispose() {\n\tc.bufMu.Lock()\n\tdefer c.bufMu.Unlock()\n\tif c.buf != nil {\n\t\tcaptureBufferPool.Put(c.buf)\n\t\tc.buf = nil\n\t}\n}\n\ntype rawResponseCapturerTeeWriter struct {\n\tc *rawResponseCapturer\n}\n\n// Write implements io.Writer, for the use with onReadFrom.\nfunc (w rawResponseCapturerTeeWriter) Write(p []byte) (int, error) {\n\t// onReadFrom is handling the updating of status code, headers, and bytes written,\n\t// so we only write to our buffer here.\n\tw.c.writeToBuf(p)\n\treturn len(p), nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/encore_routes.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/julienschmidt/httprouter\"\n\n\t\"encore.dev/appruntime/shared/jsonapi\"\n\t\"encore.dev/beta/errs\"\n)\n\nfunc (s *Server) registerEncoreRoutes() {\n\ts.encore.HandlerFunc(wildcardMethod, \"/healthz\", s.handleHealthz)\n\ts.encore.Handle(\"POST\", \"/pubsub/push/:subscription_id\", s.handlePubsubPush)\n\ts.encore.Handle(\"POST\", \"/authhandler\", s.handleRemoteAuthCall)\n}\n\n// handleHealthz returns the current health and deployment details of the running Encore application\nfunc (s *Server) handleHealthz(w http.ResponseWriter, req *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\n\tstatusStr := \"ok\"\n\tstatusCode := http.StatusOK\n\n\t// Run all health checks\n\ttype checkResult struct {\n\t\tName   string `json:\"name\"`\n\t\tPassed bool   `json:\"passed\"`\n\t\tError  string `json:\"error,omitempty\"`\n\t}\n\tvar checkResults []checkResult\n\tfor _, result := range s.healthMgr.RunAll(req.Context()) {\n\t\terrStr := \"\"\n\t\tif result.Err != nil {\n\t\t\tstatusStr = \"unhealthy\"\n\t\t\tstatusCode = http.StatusInternalServerError\n\t\t\terrStr = result.Err.Error()\n\t\t}\n\n\t\tcheckResults = append(checkResults, checkResult{\n\t\t\tName:   result.Name,\n\t\t\tPassed: result.Err == nil,\n\t\t\tError:  errStr,\n\t\t})\n\t}\n\n\tw.WriteHeader(statusCode)\n\tbytes, _ := jsonapi.Default.Marshal(struct {\n\t\tCode    string `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t\tDetails any    `json:\"details\"`\n\t}{\n\t\tCode:    statusStr,\n\t\tMessage: \"Your Encore app is up and running!\",\n\t\tDetails: struct {\n\t\t\tAppRevision        string        `json:\"app_revision\"`\n\t\t\tEncoreCompiler     string        `json:\"encore_compiler\"`\n\t\t\tDeployId           string        `json:\"deploy_id\"`\n\t\t\tChecks             []checkResult `json:\"checks\"`\n\t\t\tEnabledExperiments []string      `json:\"enabled_experiments\"`\n\t\t}{\n\t\t\tAppRevision:        s.static.AppCommit.AsRevisionString(),\n\t\t\tEncoreCompiler:     s.static.EncoreCompiler,\n\t\t\tDeployId:           s.runtime.DeployID,\n\t\t\tChecks:             checkResults,\n\t\t\tEnabledExperiments: s.experiments.StringList(),\n\t\t},\n\t})\n\t// nosemgrep\n\t_, _ = w.Write(bytes)\n}\n\n// handlePubsubPush acts like an internal router from the Encore push route, to a registered handler for the given\n// subscription\nfunc (s *Server) handlePubsubPush(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {\n\tsubscriptionID := ps.ByName(\"subscription_id\")\n\tif subscriptionID == \"\" {\n\t\terr := errs.B().Code(errs.InvalidArgument).Msg(\"missing subscription ID\").Err()\n\t\ts.rt.Logger().Err(err).Str(\"subscription_id\", subscriptionID).Msg(\"invalid PubSub push request\")\n\t\terrs.HTTPError(w, err)\n\t\treturn\n\t}\n\t// Is this a gateway and the pubsub subscription isn't hosted here?\n\t// If so forward the request to the target service instead.\n\tif remoteSubHandler, ok := s.remotePubSubPush[subscriptionID]; ok {\n\t\tremoteSubHandler.ServeHTTP(w, req)\n\t\treturn\n\t}\n\n\ts.pubsubMgr.HandlePubSubPush(w, req, subscriptionID)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/errmarshalling/fallback.go",
    "content": "package errmarshalling\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/modern-go/reflect2\"\n)\n\nfunc init() {\n\tmarshaller := &jsonMarshaller{\n\t\tunmarshal: fallbackUnmarshal,\n\t\tdecoder: func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {\n\t\t\terr := fallbackUnmarshal(iter)\n\t\t\tfmt.Println(\"here with \", err)\n\t\t},\n\t}\n\tserdeByName[\"error\"] = marshaller\n}\n\nfunc fallbackUnmarshal(itr *jsoniter.Iterator) error {\n\tvar errMsg string\n\tvar wrapped error\n\tvar wrappedList []error\n\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase MessageKey:\n\t\t\terrMsg = itr.ReadString()\n\t\tcase WrappedKey:\n\t\t\tswitch itr.WhatIsNext() {\n\t\t\tcase jsoniter.ArrayValue:\n\t\t\t\titr.ReadArrayCB(func(itr *jsoniter.Iterator) bool {\n\t\t\t\t\twrappedList = append(wrappedList, UnmarshalError(itr))\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\tcase jsoniter.ObjectValue:\n\t\t\t\twrapped = UnmarshalError(itr)\n\t\t\tdefault:\n\t\t\t\titr.ReportError(\"unmarshal\", \"expected array or object\")\n\t\t\t\titr.Skip()\n\t\t\t}\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\n\tswitch {\n\tcase len(wrappedList) > 0:\n\t\treturn &fallbackMultiWrapErr{\n\t\t\tmsg:     errMsg,\n\t\t\twrapped: wrappedList,\n\t\t}\n\tcase wrapped != nil:\n\t\treturn &fallbackSingleWrapErr{\n\t\t\tmsg:     errMsg,\n\t\t\twrapped: wrapped,\n\t\t}\n\tdefault:\n\t\treturn errors.New(errMsg)\n\t}\n}\n\nfunc createFallbackEncoder(typ reflect2.Type) jsoniter.ValEncoder {\n\treturn &jsonMarshaller{\n\t\tencoder: func(ptr unsafe.Pointer, stream *jsoniter.Stream) {\n\t\t\tpointerToVal := typ.PackEFace(ptr)                        // *[error type]\n\t\t\tiface := reflect.ValueOf(pointerToVal).Elem().Interface() // [error type]\n\t\t\terr := iface.(error)                                      // error\n\n\t\t\t// check if we have a custom marshaller for this type\n\t\t\t// if we do then use it.\n\t\t\t//\n\t\t\t// this path can occur if the field is strongly typed as `error`\n\t\t\t_, found := serdeByType[reflect2.TypeOf(iface)]\n\t\t\tif found {\n\t\t\t\t// this will go via the custom marshaller now\n\t\t\t\tstream.WriteVal(iface)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tstream.WriteObjectStart()\n\n\t\t\tstream.WriteObjectField(TypeKey)\n\t\t\tstream.WriteString(\"error\")\n\t\t\tstream.WriteMore()\n\n\t\t\tstream.WriteObjectField(MessageKey)\n\t\t\tstream.WriteString(err.Error())\n\n\t\t\tswitch err := err.(type) {\n\t\t\tcase interface{ Unwrap() error }:\n\t\t\t\twrapped := err.Unwrap()\n\t\t\t\tif wrapped != nil {\n\t\t\t\t\tstream.WriteMore()\n\t\t\t\t\tstream.WriteObjectField(WrappedKey)\n\t\t\t\t\tstream.WriteVal(wrapped)\n\t\t\t\t}\n\n\t\t\tcase interface{ Unwrap() []error }:\n\t\t\t\twrapped := err.Unwrap()\n\t\t\t\tif len(wrapped) > 0 {\n\t\t\t\t\tstream.WriteMore()\n\t\t\t\t\tstream.WriteObjectField(WrappedKey)\n\t\t\t\t\tstream.WriteVal(wrapped)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstream.WriteObjectEnd()\n\t\t},\n\t}\n}\n\n// fallbackSingleWrapErr is a fallback error type that wraps a single error.\ntype fallbackSingleWrapErr struct {\n\tmsg     string\n\twrapped error\n}\n\nvar _ error = (*fallbackSingleWrapErr)(nil)\n\nfunc (f *fallbackSingleWrapErr) Error() string {\n\treturn f.msg\n}\n\nfunc (f *fallbackSingleWrapErr) Unwrap() error {\n\treturn f.wrapped\n}\n\n// fallbackMultiWrapErr is a fallback error type that wraps multiple errors.\ntype fallbackMultiWrapErr struct {\n\tmsg     string\n\twrapped []error\n}\n\nvar _ error = (*fallbackMultiWrapErr)(nil)\n\nfunc (f *fallbackMultiWrapErr) Error() string {\n\treturn f.msg\n}\n\nfunc (f *fallbackMultiWrapErr) Unwrap() []error {\n\treturn f.wrapped\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/errmarshalling/jsonextension.go",
    "content": "package errmarshalling\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/modern-go/reflect2\"\n)\n\nvar (\n\t// jsonMarshaller by reflect2 type (we use this for speed in the extension)\n\tserdeByType = map[reflect2.Type]*jsonMarshaller{}\n\n\t// jsonMarshaller by name (we use this for use in Unmarshal)\n\tserdeByName = map[string]*jsonMarshaller{}\n\n\t// errType is the reflect2 type of the error interface\n\terrType = reflect2.Type2(reflect.TypeOf((*error)(nil)).Elem())\n)\n\nconst (\n\t// TypeKey is the key used to identify the type of an error\n\t// used to unmarshal the correct error type\n\tTypeKey = \"@type\"\n\n\t// MessageKey is the key used to identify the message of an error\n\t// we use a common name, which allows the fallback unmarshaler to work\n\tMessageKey = \"msg\"\n\n\t// WrappedKey is the key used to identify the wrapped error(s) of an error\n\t// it can be either a single object or an array of objects\n\t// again we use a common name, which allows the fallback unmarshaler to work\n\tWrappedKey = \"wraps\"\n)\n\n// RegisterErrorMarshaller registers a custom marshaller for a specific error type\n//\n// The error type has to be used as a pointer, and must be registered before any\n// calls to [Marshal] or [Unmarshal] are made.\n//\n// The encoder and decoder functions are responsible for encoding and decoding\n// the error object.\n//\n// Encoder will be passed a *jsoniter.Stream which is already in object mode.\n// so fields can be written directly to it without needing to call WriteObjectStart or WriteObjectEnd.\n//\n// Decoder will be passed a *jsoniter.Iterator before the object has been read.\n// allowing calls to ReadObjectCB to read the fields of the object.\nfunc RegisterErrorMarshaller[T error](encoder func(T, *jsoniter.Stream), decoder func(T, *jsoniter.Iterator)) {\n\tvar zero T\n\ttyp := reflect2.TypeOf(zero)\n\tif !typ.LikePtr() {\n\t\tpanic(fmt.Errorf(\"error type %s must be a pointer\", typ.String()))\n\t}\n\tbaseType := reflect2.TypeOfPtr(zero).Elem()\n\ttypeName := baseType.Type1().PkgPath() + \".\" + baseType.Type1().Name()\n\n\tmarshaller := &jsonMarshaller{\n\t\tunmarshal: func(itr *jsoniter.Iterator) error {\n\t\t\tzero := baseType.New()\n\t\t\titr.ReadVal(&zero)\n\t\t\treturn zero.(error)\n\t\t},\n\t\tencoder: func(ptr unsafe.Pointer, stream *jsoniter.Stream) {\n\t\t\tstream.WriteObjectStart()\n\n\t\t\t// Write the type name\n\t\t\tstream.WriteObjectField(TypeKey)\n\t\t\tstream.WriteString(typeName)\n\t\t\tstream.WriteMore()\n\n\t\t\t// Hand over to the encoder provided\n\t\t\tencoder(*(*T)(ptr), stream)\n\n\t\t\tstream.WriteObjectEnd()\n\t\t},\n\t\tdecoder: func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {\n\t\t\t// Create a new instance of the error object\n\t\t\tobj := baseType.UnsafeNew()\n\n\t\t\t// let the passed in decoder do it's job\n\t\t\tdecoder(baseType.PackEFace(obj).(T), iter)\n\n\t\t\t// set the pointer to the new object that we just unmarhsalled\n\t\t\tbaseType.UnsafeSet(ptr, obj)\n\t\t},\n\t}\n\n\tserdeByName[typeName] = marshaller\n\tserdeByType[typ] = marshaller\n}\n\n// jsonExtension is a jsoniter extension that allows us to register custom\n// error encoder and decoders using [RegisterErrorMarshaller]\ntype jsonExtension struct {\n\tjsoniter.DummyExtension\n}\n\nfunc (extension *jsonExtension) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder {\n\t// If somebody is trying to unmarshal an error directly\n\t// we know how to do that\n\tif typ == errType {\n\t\treturn &jsonMarshaller{decoder: func(ptr unsafe.Pointer, iter *jsoniter.Iterator) {\n\t\t\terr := UnmarshalError(iter)\n\t\t\terrType.UnsafeSet(ptr, unsafe.Pointer(&err))\n\t\t}}\n\t}\n\n\t// jsoniter is asking if we know how to decode this value\n\t// we index by the base type, but we get a pointer type here - hence the \"PtrTo\" call.\n\tif f, ok := serdeByType[reflect2.PtrTo(typ)]; ok {\n\t\treturn f\n\t}\n\treturn nil\n}\n\nfunc (extension *jsonExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder {\n\t// jsoniter is asking if we know how to encode this value\n\t// we'll be passed a pointer type here, so we can index directly\n\tif f, ok := serdeByType[typ]; ok {\n\t\treturn f\n\t}\n\n\t// However if we don't have a marshaller for this type, we can check if it implements the error interface\n\t// and if it does, we can create a fallback marshaller for it.\n\tif typ.Implements(errType) {\n\t\treturn createFallbackEncoder(typ)\n\t}\n\n\treturn nil\n}\n\n// jsonMarshaller is a simple struct which implements jsoniter's ValEncoder and ValDecoder interfaces\n// allowing us to use it as a single object without having to create multiple objects\ntype jsonMarshaller struct {\n\t// unmarshal takes an iterator, creates a new version of the error object, and returns it\n\tunmarshal func(itr *jsoniter.Iterator) error\n\tencoder   jsoniter.EncoderFunc\n\tdecoder   jsoniter.DecoderFunc\n}\n\nvar (\n\t_ jsoniter.ValEncoder = (*jsonMarshaller)(nil)\n\t_ jsoniter.ValDecoder = (*jsonMarshaller)(nil)\n)\n\nfunc (j *jsonMarshaller) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {\n\tj.encoder(ptr, stream)\n}\n\nfunc (j *jsonMarshaller) IsEmpty(ptr unsafe.Pointer) bool {\n\treturn false\n}\n\nfunc (j *jsonMarshaller) Decode(value unsafe.Pointer, iter *jsoniter.Iterator) {\n\tj.decoder(value, iter)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/errmarshalling/marshal.go",
    "content": "package errmarshalling\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar json = JsonAPI()\n\n// JsonAPI returns a jsoniter.API instance that can be used to marshal\n// errors\n//\n// It is only exported for use in tests!\nfunc JsonAPI() jsoniter.API {\n\tapi := jsoniter.Config{\n\t\tEscapeHTML:             false,\n\t\tIndentionStep:          2,\n\t\tSortMapKeys:            true,\n\t\tValidateJsonRawMessage: true,\n\t\tTagKey:                 \"err_json\", // ignore `json` tags (as we use them to hide stuff sometimes)\n\t}.Froze()\n\n\t// Register the JSON extension to allow us to register custom marshallers\n\t// For our error types\n\tapi.RegisterExtension(&jsonExtension{})\n\n\treturn api\n}\n\n// Marshal will marshal the given error into a JSON byte slice\n//\n// If the error is nil, the return value will be the JSON null value.\n//\n// If an error or panic occurs while marshalling the error,\n// a generic error will be marshalled instead\nfunc Marshal(err error) (rtn []byte) {\n\t// Recover from any panics that may occur while marshalling and\n\t// return a generic error message around the panic\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\t// because we don't know the cause of the panic, let's write\n\t\t\t// the bytes manually to avoid any further panics if it's being caused by\n\t\t\t// jsoniter\n\t\t\trtn = []byte(\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"{ \\\"%s\\\": \\\"error\\\", \\\"%s\\\": \\\"%s\\\"}\",\n\t\t\t\t\tTypeKey, MessageKey,\n\t\t\t\t\tstrconv.Quote(fmt.Sprintf(\"panic occured while marshalling error: %v\", r)),\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t}()\n\n\t// No error, nothing to marshal\n\tif err == nil {\n\t\treturn []byte{'n', 'u', 'l', 'l'}\n\t}\n\n\t// Try to marshal the error using the registered marshallers\n\twf, err := json.Marshal(err)\n\tif err == nil {\n\t\treturn wf\n\t}\n\n\t// Otherwise fall back and marshal the error as a super generic error\n\tvar buf bytes.Buffer\n\tstream := json.BorrowStream(&buf)\n\tdefer json.ReturnStream(stream)\n\n\tstream.WriteObjectStart()\n\tstream.WriteObjectField(TypeKey)\n\tstream.WriteString(\"error\")\n\tstream.WriteMore()\n\tstream.WriteObjectField(MessageKey)\n\tstream.WriteString(err.Error())\n\tstream.WriteObjectEnd()\n\t_ = stream.Flush()\n\n\treturn buf.Bytes()\n}\n\n// Unmarshal attempts to unmarshal an error from the given bytes,\n//\n// On success, the unmarshalled error will be returned, and the error will be nil\n// On failure, unmarshalled will be nil and the error will be non-nil\nfunc Unmarshal(bytes []byte) (unmarshalled error, err error) {\n\t// Read the type name lazily (we will only read enough bytes to get the type name)\n\ttypeName := json.Get(bytes, TypeKey).ToString()\n\n\tmarshallers, found := serdeByName[typeName]\n\tif !found {\n\t\treturn nil, fmt.Errorf(\"error type %s not registered\", typeName)\n\t}\n\n\titr := json.BorrowIterator(bytes)\n\tdefer json.ReturnIterator(itr)\n\n\tunmarshalledError := marshallers.unmarshal(itr)\n\n\treturn unmarshalledError, itr.Error\n}\n\n// UnmarshalError attempts to unmarshal an error from the given iterator,\n// returning the unmarshalled error if successful, or nil if not.\nfunc UnmarshalError(itr *jsoniter.Iterator) (unmarshalledError error) {\n\tunmarshalled, err := Unmarshal(itr.SkipAndReturnBytes())\n\tif err != nil {\n\t\titr.ReportError(\"unmarshal\", err.Error())\n\t\treturn nil\n\t} else {\n\t\treturn unmarshalled\n\t}\n}\n\n// TryWriteValue attempts to marshal the given value into JSON, returning the\n// marshalled bytes if successful, or false if not.\nfunc TryWriteValue(stream *jsoniter.Stream, fieldName string, value any) error {\n\tif value == nil {\n\t\tstream.WriteNil()\n\t\treturn nil\n\t}\n\n\t// Create a sub-stream to write the value into\n\t// which we can then check for errors on without affecting the main stream\n\tvar buf bytes.Buffer\n\tsubStream := stream.Pool().BorrowStream(&buf)\n\tdefer stream.Pool().ReturnStream(subStream)\n\n\t// Update the substream's indent level to match the main stream\n\t// we need to use reflection to do this as the indent level is not exported\n\t// by the jsoniter.Stream type\n\t//\n\t// (We could have simply used the subtream to check for errors, and then rewritten\n\t// the same object into the main stream, but this would be less efficient)\n\tcurrentPtr := unsafe.Pointer(reflect.Indirect(reflect.ValueOf(stream)).FieldByName(\"indention\").UnsafeAddr())\n\tsubStreamPtr := unsafe.Pointer(reflect.Indirect(reflect.ValueOf(subStream)).FieldByName(\"indention\").UnsafeAddr())\n\tindent := *(*int)(currentPtr)\n\t*((*int)(subStreamPtr)) = indent\n\n\t// Write the value into the substream & flush it\n\tsubStream.WriteVal(value)\n\tif err := subStream.Flush(); err != nil {\n\t\treturn err\n\t}\n\n\t// Write the substream's bytes into the main stream\n\tstream.WriteMore()\n\tstream.WriteObjectField(fieldName)\n\t_, err := stream.Write(buf.Bytes())\n\n\treturn err\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/errmarshalling/marshal_test.go",
    "content": "package errmarshalling_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/modern-go/reflect2\"\n\n\t\"encore.dev/appruntime/apisdk/api/errmarshalling\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/storage/cache\"\n\t\"encore.dev/storage/sqldb\"\n\t\"encore.dev/storage/sqldb/sqlerr\"\n)\n\ntype SomeRandomType struct {\n\tFoo string\n\tBar bool\n\tErr error\n}\n\nfunc TestMarshal(t *testing.T) {\n\tparams := []struct {\n\t\tname string\n\t\terr  error\n\t}{\n\t\t{\"basic\", errors.New(\"blah\")},\n\t\t{\"joined errors\", errors.Join(errors.New(\"error a\"), errors.New(\"error b\"))},\n\t\t{\"single wrapped error\", fmt.Errorf(\"outer error: %w\", errors.New(\"inner issue\"))},\n\t\t{\"3 deep single wrapped error\", fmt.Errorf(\"outer error: %w\", fmt.Errorf(\"inner issue: %w\", errors.New(\"inner inner issue\")))},\n\t\t{\"multiple wrapped errors\", fmt.Errorf(\"outer error: %w\", errors.Join(errors.New(\"inner issue\"), errors.New(\"inner issue 2\")))},\n\t\t{\"encore errs\", errs.B().Code(errs.OutOfRange).Meta(\"foo\", \"bar\", \"x\", 123, \"y\", false).Msg(\"out of range in foo\").Err()},\n\t\t{\"encore err wrapped in Go error\", fmt.Errorf(\"outer: %w\", errs.B().Code(errs.OutOfRange).Msg(\"out of range in foo\").Err())},\n\t\t{\"sqldb error\", &sqldb.Error{\n\t\t\tCode:           sqlerr.NotNullViolation,\n\t\t\tSeverity:       sqlerr.SeverityError,\n\t\t\tDatabaseCode:   \"PG2341\",\n\t\t\tMessage:        \"Not null happened matey\",\n\t\t\tSchemaName:     \"your_database\",\n\t\t\tTableName:      \"in_this_table\",\n\t\t\tColumnName:     \"on_this_column\",\n\t\t\tDataTypeName:   \"string\",\n\t\t\tConstraintName: \"not_null\",\n\t\t}},\n\t\t{\"cache operror\", &cache.OpError{\n\t\t\tOperation: \"get\",\n\t\t\tRawKey:    \"foo\",\n\t\t\tErr:       errors.New(\"some error\"),\n\t\t}},\n\t}\n\n\tfor _, p := range params {\n\t\tp := p\n\t\tt.Run(p.name, func(t *testing.T) {\n\t\t\t_ = roundTrip(t, p.err)\n\t\t})\n\t}\n\n\tt.Run(\"encore err with underlying\", func(t *testing.T) {\n\t\tinner := errs.B().Code(errs.OutOfRange).Msg(\"out of range in foo\").Err()\n\t\terrIn := errs.B().Code(errs.Internal).\n\t\t\tCause(inner).\n\t\t\tMeta(\"hello\", 1, \"goodbye\", 2).\n\t\t\tMsg(\"blah10\").\n\t\t\tErr()\n\t\terrOut := roundTrip(t, errIn)\n\n\t\tfmt.Println(\"Error In: \", errIn)\n\t\tfmt.Println(\"Error Out:\", errOut)\n\t})\n}\n\nfunc roundTrip(t *testing.T, err error) error {\n\tbytes := errmarshalling.Marshal(err)\n\tfmt.Println(string(bytes))\n\n\tunmarshalled, unmarshallingErr := errmarshalling.Unmarshal(bytes)\n\tif unmarshallingErr != nil {\n\t\tt.Fatalf(\n\t\t\t\"Failed to unmarshal error\\n\\nGot: %v\\n\\nErr: %v\", err, unmarshallingErr)\n\t\treturn nil\n\t}\n\n\tif unmarshalled == nil {\n\t\tt.Fatalf(\"Expected error %v, got nil\", err)\n\t}\n\n\tif unmarshalled.Error() != err.Error() {\n\t\tt.Errorf(\"Expected error %v, got %v\", err, unmarshalled)\n\t}\n\n\toriginalType := reflect2.TypeOf(err)\n\tunmarshalledType := reflect2.TypeOf(unmarshalled)\n\n\tif originalType.String() != unmarshalledType.String() &&\n\t\toriginalType.String() != \"*errors.joinError\" &&\n\t\toriginalType.String() != \"*fmt.wrapError\" {\n\t\tfmt.Println(\"Original type:\", originalType.Type1().PkgPath())\n\t\tt.Errorf(\"Expected error type %v, got %v\", originalType, unmarshalledType)\n\t}\n\n\treturn unmarshalled\n}\n\nfunc TestMarshalWithCustomData(t *testing.T) {\n\tdata := SomeRandomType{\n\t\tFoo: \"hello\",\n\t\tBar: true,\n\t\tErr: errs.B().Code(errs.OutOfRange).Meta(\"a\", \"b\").Msg(\"out of range in foo\").Err(),\n\t}\n\n\tjson := errmarshalling.JsonAPI()\n\n\tbytes, err := json.Marshal(data)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdata2 := SomeRandomType{}\n\tif err := json.Unmarshal(bytes, &data2); err != nil {\n\t\tt.Fatalf(\"unable to unmarshal: %v\", err)\n\t}\n\n\tunmarshalledMeta := errs.Meta(data2.Err)\n\tif len(unmarshalledMeta) == 0 {\n\t\tt.Fatalf(\"expected metadata to be unmarshalled\")\n\t}\n\tif unmarshalledMeta[\"a\"] != \"b\" {\n\t\tt.Fatalf(\"unmarshalled metadata is incorrect: %v\", unmarshalledMeta)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/gateway.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\n\t\"github.com/julienschmidt/httprouter\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/beta/errs\"\n)\n\n// IsGateway returns true if this instance of the container is acting as an API\n// gateway\nfunc (s *Server) IsGateway() bool {\n\treturn len(s.runtime.Gateways) > 0\n}\n\n// createGatewayHandlerAdapter creates a httprouter.Handle that proxies requests\n// on top of the given handler to the service that is hosting the handler.\nfunc (s *Server) createGatewayHandlerAdapter(h Handler) httprouter.Handle {\n\tservice, found := s.runtime.ServiceDiscovery[h.ServiceName()]\n\tif !found {\n\t\tpanic(fmt.Sprintf(\"service %q not found in service discovery when hosted in gateway\", h.ServiceName()))\n\t}\n\n\tserviceBaseURL, err := url.Parse(service.URL)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to parse service URL %q: %v\", service.URL, err))\n\t}\n\n\t// On cloud environments, we want to log the proxying of requests to services\n\t// but locally we don't want the overhead of logging every request.\n\tlogger := s.rootLogger.With().Str(\"service\", service.Name).Str(\"endpoint\", h.EndpointName()).Str(\"base_url\", serviceBaseURL.String()).Logger()\n\n\tproxy := s.createProxyToService(service, h.EndpointName(), serviceBaseURL, logger)\n\treturn func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {\n\t\ts.beginOperation()\n\t\tdefer s.finishOperation()\n\n\t\tmeta := CallMetaFromContext(req.Context())\n\n\t\tinfo, proceed := s.runAuthHandler(h, s.NewIncomingContext(w, req, toUnnamedParams(ps), meta))\n\t\tif proceed {\n\t\t\tmeta.Internal = &InternalCallMeta{\n\t\t\t\tCaller: GatewayCaller{\n\t\t\t\t\tGatewayName: \"api-gateway\",\n\t\t\t\t},\n\t\t\t\tAuthUID:  string(info.UID),\n\t\t\t\tAuthData: info.UserData,\n\t\t\t}\n\t\t\treq = req.WithContext(SetCallMetaInContext(req.Context(), meta))\n\n\t\t\tif s.runtime.EnvCloud != \"local\" {\n\t\t\t\tlogger.Trace().Msg(\"proxying request to service\")\n\t\t\t}\n\t\t\tproxy.ServeHTTP(w, req)\n\t\t}\n\t}\n}\n\n// createProxyToService creates a httputil.ReverseProxy that proxies requests onto the target service.\nfunc (s *Server) createProxyToService(service config.Service, endpointName string, serviceBaseURL *url.URL, logger zerolog.Logger) *httputil.ReverseProxy {\n\tcallee := fmt.Sprintf(\"%s.%s\", service.Name, endpointName)\n\n\tproxy := &httputil.ReverseProxy{\n\t\t// Rewrite the inbound request\n\t\tRewrite: func(req *httputil.ProxyRequest) {\n\t\t\treq.SetURL(serviceBaseURL)\n\n\t\t\tt := transport.HTTPRequest(req.Out)\n\t\t\tt.SetMeta(calleeMetaName, callee) // required by the Handler which verifies we wanted to call this endpoint\n\n\t\t\tmeta := CallMetaFromContext(req.In.Context())\n\t\t\tif err := meta.AddToRequest(s, service, t); err != nil {\n\t\t\t\tlogger.Err(err).Msg(\"failed to add call metadata to request\")\n\t\t\t}\n\t\t\treq.Out.Header[\"X-Forwarded-For\"] = req.In.Header[\"X-Forwarded-For\"]\n\t\t\treq.SetXForwarded()\n\t\t},\n\t\t// Have the reverse proxy log errors to our logger.\n\t\tErrorLog: newZeroLogAdapter(logger, zerolog.ErrorLevel),\n\t\t// Handle proxy errors using our error handler output\n\t\tErrorHandler: func(w http.ResponseWriter, req *http.Request, err error) {\n\t\t\tlogger.Err(err).Msg(\"error proxying request to service\")\n\t\t\terrs.HTTPError(w, errs.B().Cause(err).Code(errs.Unavailable).Err())\n\t\t},\n\t}\n\n\t// If the service is served without TLS, we need to configure the proxy to allow forwarding\n\t// HTTP2 in clear text to make sure grpc requests are forwarded correctly.\n\tif serviceBaseURL.Scheme == \"http\" {\n\t\tproxy.Transport = transport.NewH2CTransport(http.DefaultTransport)\n\t}\n\treturn proxy\n}\n\ntype zeroLogWriter struct {\n\tlogger zerolog.Logger\n\tlevel  zerolog.Level\n}\n\nfunc (z *zeroLogWriter) Write(p []byte) (n int, err error) {\n\tz.logger.WithLevel(z.level).CallerSkipFrame(3).Msg(string(p))\n\treturn len(p), nil\n}\n\n// NewZeroLogAdapter returns a new log.Logger that writes to the given zerolog.Logger at the given level.\nfunc newZeroLogAdapter(logger zerolog.Logger, level zerolog.Level) *log.Logger {\n\tzlw := &zeroLogWriter{logger, level}\n\treturn log.New(zlw, \"\", 0)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/handler.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\tjsoniter \"github.com/json-iterator/go\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/appruntime/apisdk/api/errmarshalling\"\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/shared/cfgutil\"\n\t\"encore.dev/appruntime/shared/cloudtrace\"\n\t\"encore.dev/appruntime/shared/jsonapi\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/internal/platformauth\"\n\t\"encore.dev/middleware\"\n)\n\n// NamedParams are named path parameters.\ntype NamedParams = model.PathParams\n\n// UnnamedParams are unnamed parameters from an incoming request.\ntype UnnamedParams []string\n\ntype Void struct{}\n\n// SerializeVoid serializes the Void type. It's called by generated code.\nfunc SerializeVoid(json jsoniter.API, _ Void) ([][]byte, error) {\n\treturn [][]byte{[]byte(\"{}\")}, nil\n}\n\n// CloneVoid clones the Void type. It's called by generated code.\nfunc CloneVoid(Void) (Void, error) {\n\treturn Void{}, nil\n}\n\n// isVoid reports whether a generic type is Void.\nfunc isVoid[T any]() bool {\n\tvar zero T\n\t_, ok := any(zero).(Void)\n\treturn ok\n}\n\n// Desc is a description of an API handler.\ntype Desc[Req, Resp any] struct {\n\t// SvcNum is the 1-based index into the list of services.\n\tSvcNum uint16\n\n\t// Service and Endpoint name the API this description is for.\n\tService  string\n\tEndpoint string\n\n\tMethods []string\n\tPath    string\n\tRawPath string\n\tDefLoc  uint32\n\n\t// PathParamNames are the names of the path params, in order.\n\tPathParamNames []string\n\n\t// Tags are the tags for this API, excluding the \"tag:\" prefix.\n\tTags []string\n\n\t// Access describes the access type for this API.\n\tAccess Access\n\n\t// If raw is true, RawHandler is set and AppHandler and EncodeResp are nil.\n\tRaw bool\n\n\t// If Fallback is true, the handler is a fallback handler\n\t// for when other routes don't match.\n\tFallback bool\n\n\tDecodeReq      func(*http.Request, UnnamedParams, jsoniter.API) (Req, UnnamedParams, error)\n\tCloneReq       func(Req) (Req, error)\n\tReqPath        func(Req) (path string, params UnnamedParams, err error)\n\tReqUserPayload func(Req) any\n\n\tAppHandler func(context.Context, Req) (Resp, error)\n\tRawHandler func(http.ResponseWriter, *http.Request)\n\n\tEncodeResp func(http.ResponseWriter, jsoniter.API, Resp, int) error\n\tCloneResp  func(Resp) (Resp, error)\n\n\t// EncodeExternalReq encodes a request, writing the payload to the stream\n\t// and headers and query strings to the returned maps.\n\tEncodeExternalReq func(Req, *jsoniter.Stream) (http.Header, url.Values, error)\n\n\t// DecodeExternalResp decodes the response, reading the payload into the response object.\n\tDecodeExternalResp func(*http.Response, jsoniter.API) (Resp, error)\n\n\t// GlobalMiddlewareIDs is the ordered list of global middleware IDs\n\t// to invoke before calling the API handler.\n\tGlobalMiddlewareIDs []string\n\n\t// ServiceMiddleware is the ordered list of middleware to invoke before\n\t// calling the API handler.\n\tServiceMiddleware []*Middleware\n\n\tScrubRequestPaths    []scrub.Path\n\tScrubRequestHeaders  map[string]bool\n\tScrubResponsePaths   []scrub.Path\n\tScrubResponseHeaders map[string]bool\n\n\trpcDescOnce   sync.Once\n\tcachedRPCDesc *model.RPCDesc\n\n\tmockCacheMu   sync.RWMutex\n\tmockObjCache  map[any]reflectedAPIMethod[Req, Resp]    // map of object to reflected method\n\tmockFuncCache map[uint64]reflectedAPIMethod[Req, Resp] // map of model.ApiMock.ID to reflected method\n}\n\nfunc (d *Desc[Req, Resp]) AccessType() Access     { return d.Access }\nfunc (d *Desc[Req, Resp]) ServiceName() string    { return d.Service }\nfunc (d *Desc[Req, Resp]) EndpointName() string   { return d.Endpoint }\nfunc (d *Desc[Req, Resp]) HTTPMethods() []string  { return d.Methods }\nfunc (d *Desc[Req, Resp]) SemanticPath() string   { return d.Path }\nfunc (d *Desc[Req, Resp]) HTTPRouterPath() string { return d.RawPath }\nfunc (d *Desc[Req, Resp]) IsFallback() bool       { return d.Fallback }\n\nfunc (d *Desc[Req, Resp]) Handle(c IncomingContext) {\n\tif d.Raw {\n\t\tc.capturer = newRawRequestBodyCapturer(c.req)\n\t\tc.req.Body = c.capturer\n\t\tdefer c.capturer.Dispose()\n\t}\n\n\t// If this is an internal encore-to-encore call, we need to verify the caller is allowed to make this call.\n\tif c.callMeta.IsServiceToService() {\n\t\tt := transport.HTTPRequest(c.req)\n\t\ttargetAPI, _ := t.ReadMeta(calleeMetaName)\n\n\t\tif targetAPI != fmt.Sprintf(\"%s.%s\", d.Service, d.Endpoint) {\n\t\t\treturnError(c, errs.B().Code(errs.PermissionDenied).Msg(\"internal call auth did not align with API\").Err(), 0, nil)\n\t\t\treturn\n\t\t}\n\t}\n\n\treqData, beginErr := d.begin(c)\n\tif beginErr != nil {\n\t\treturnError(c, beginErr, 0, nil)\n\t\treturn\n\t}\n\n\tresp, respData := d.handleIncoming(c, reqData)\n\tif resp.Err != nil {\n\t\tc.server.finishRequest(resp)\n\n\t\t// If the endpoint is raw it has already written its response;\n\t\t// don't write another.\n\t\tif !d.Raw {\n\t\t\treturnError(c, resp.Err, resp.HTTPStatus, resp.Headers)\n\t\t}\n\t\treturn\n\t}\n\n\tif !d.Raw {\n\t\t// Apply any custom headers from middleware\n\t\tfor key, values := range resp.Headers {\n\t\t\tfor _, value := range values {\n\t\t\t\tc.w.Header().Add(key, value)\n\t\t\t}\n\t\t}\n\n\t\tc.w.Header().Set(\"Content-Type\", \"application/json\")\n\t\tc.w.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\t\tresp.Err = d.EncodeResp(c.w, c.server.json, respData, resp.HTTPStatus)\n\t}\n\tc.server.finishRequest(resp)\n}\n\n// returnError is a helper function which will return an error to the client when we handle\n// an incoming request.\n//\n// If the requests is an internal service to service request, we will return an error\n// in a full unabridged format, allowing the calling code to unmarshal the error without\n// loss of information.\n//\n// If the request is an external request, we will return a more user friendly error\n// message, which will be more suitable for display to the end user and not include\n// any internal details.\n//\n// If statusCodeToUse is 0, we will use the default status code for the error using\n// the [errs] package.\n//\n// headers will be applied to the response if provided.\nfunc returnError(c IncomingContext, err error, statusCodeToUse int, headers http.Header) {\n\t// Apply any custom headers from middleware\n\tfor key, values := range headers {\n\t\tfor _, value := range values {\n\t\t\tc.w.Header().Add(key, value)\n\t\t}\n\t}\n\n\tif c.callMeta.PrivateAPIAccess() {\n\t\t// If this is an internal service to service call, we want to return the full error\n\t\t// we'll add a header to the response to indicate that the error is a full error\n\t\t// and the calling code can use this to determine how to unmarshal the response object.\n\t\tc.w.Header().Set(\"X-Encore-Full-Error\", \"1\")\n\n\t\tc.w.Header().Set(\"Content-Type\", \"application/json\")\n\t\tc.w.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\n\t\t// Write the status code out\n\t\tif statusCodeToUse == 0 {\n\t\t\tstatusCodeToUse = errs.HTTPStatus(err)\n\t\t}\n\t\tc.w.WriteHeader(statusCodeToUse)\n\n\t\t// Use our internal error marshalling package to marshal the error\n\t\terrBytes := errmarshalling.Marshal(err)\n\t\t_, _ = c.w.Write(errBytes)\n\n\t} else {\n\t\terrs.HTTPErrorWithCode(c.w, err, statusCodeToUse)\n\t}\n}\n\nfunc (d *Desc[Req, Resp]) begin(c IncomingContext) (reqData Req, beginErr error) {\n\treqData, params, decodeErr := d.DecodeReq(c.req, c.ps, c.server.json)\n\n\tif d.Access == RequiresAuth && c.auth.UID == \"\" {\n\t\tbeginErr = errs.B().\n\t\t\tCode(errs.Unauthenticated).\n\t\t\tMeta(\"service\", d.Service, \"endpoint\", d.Endpoint).\n\t\t\tMsg(\"endpoint requires auth but none provided\").\n\t\t\tErr()\n\t\treturn\n\t}\n\n\t// Only compute inputs and payload if we have valid reqData.\n\tvar payload any\n\tvar nonRawPayload []byte\n\tif decodeErr == nil {\n\t\tpayload = d.ReqUserPayload(reqData)\n\t\tif !d.Raw {\n\t\t\tnonRawPayload = marshalParams(c.server.json, payload)\n\t\t}\n\t}\n\n\t_, err := c.server.beginRequest(c.ctx, &beginRequestParams{\n\t\tType:          model.RPCCall,\n\t\tDefLoc:        d.DefLoc,\n\t\tTraceID:       c.callMeta.TraceID,\n\t\tSpanID:        c.callMeta.SpanID,\n\t\tParentSpanID:  c.callMeta.ParentSpanID,\n\t\tCallerEventID: c.callMeta.ParentEventID,\n\t\tParentSampled: c.callMeta.TraceSampled,\n\n\t\tData: &model.RPCData{\n\t\t\tDesc:                 d.rpcDesc(),\n\t\t\tHTTPMethod:           c.req.Method,\n\t\t\tPath:                 c.req.URL.Path,\n\t\t\tPathParams:           d.toNamedParams(params),\n\t\t\tTypedPayload:         payload,\n\t\t\tNonRawPayload:        nonRawPayload,\n\t\t\tUserID:               c.auth.UID,\n\t\t\tAuthData:             c.auth.UserData,\n\t\t\tRequestHeaders:       headersWithHost(c.req),\n\t\t\tFromEncorePlatform:   platformauth.IsEncorePlatformRequest(c.req.Context()),\n\t\t\tServiceToServiceCall: c.callMeta.IsServiceToService(),\n\t\t},\n\n\t\tExtRequestID:        clampTo64Chars(c.req.Header.Get(\"X-Request-ID\")),\n\t\tExtCorrelationID:    clampTo64Chars(c.req.Header.Get(\"X-Correlation-ID\")),\n\t\tAdditionalLogFields: cloudtrace.StructuredLogFields(c.req),\n\t})\n\tif err != nil {\n\t\tbeginErr = errs.B().Code(errs.Internal).Msg(\"internal error\").Err()\n\t\treturn\n\t}\n\n\t// If we fail after having begun the request, mark it as completed.\n\tdefer func() {\n\t\tif beginErr != nil {\n\t\t\tc.server.finishRequest(newErrResp(beginErr, 0))\n\t\t}\n\t}()\n\n\tif decodeErr != nil {\n\t\tbeginErr = errs.WrapCode(decodeErr, errs.InvalidArgument, \"decode request\")\n\t\treturn\n\t}\n\n\treturn reqData, nil\n}\n\n// handleIncoming executes the given handler, running middleware in the process.\nfunc (d *Desc[Req, Resp]) handleIncoming(c IncomingContext, reqData Req) (resp *model.Response, respData Resp) {\n\tif err := d.validate(reqData); err != nil {\n\t\treturn newErrResp(err, 0), respData\n\t}\n\n\tvar respCapturer *rawResponseCapturer\n\n\tinvokeHandler := func(mwReq middleware.Request) (mwResp middleware.Response) {\n\t\tif d.Raw {\n\t\t\trespCapturer = newRawResponseCapturer(c.w, c.req)\n\t\t\treturn d.invokeHandlerRaw(mwReq, c, respCapturer)\n\t\t} else {\n\t\t\treturn d.invokeHandlerNonRaw(mwReq, reqData, d.AppHandler)\n\t\t}\n\t}\n\n\trespData, httpStatus, headers, err := d.executeEndpoint(c.execContext, invokeHandler)\n\n\tresp = newRespWithHeaders(respData, httpStatus, err, headers, d.Raw, c.capturer, respCapturer, c.server.json)\n\treturn resp, respData\n}\n\n// executeEndpoint executes the given handler, running middleware in the process.\nfunc (d *Desc[Req, Resp]) executeEndpoint(c execContext, invokeHandler func(middleware.Request) middleware.Response) (resp Resp, httpStatus int, headers http.Header, respErr error) {\n\tvar counter int\n\tvar nextFn middleware.Next\n\n\tvar (\n\t\tglobalMiddleware = c.server.getGlobalMiddleware(d.GlobalMiddlewareIDs)\n\t\tsvcMiddleware    = d.ServiceMiddleware\n\n\t\tnumGlobalMiddleware  = len(globalMiddleware)\n\t\tnumServiceMiddleware = len(svcMiddleware)\n\t\tnumTotalMiddleware   = numGlobalMiddleware + numServiceMiddleware\n\t)\n\n\tnextFn = func(req middleware.Request) (resp middleware.Response) {\n\t\t// Ensure the HTTP status code is correctly set in the response\n\t\tdefer func() {\n\t\t\t// If no explicit HTTP status has been set, then we use the default for the type of error\n\t\t\t// or if Err is nil, we'll set 200\n\t\t\tif resp.HTTPStatus == 0 {\n\t\t\t\tresp.HTTPStatus = errs.HTTPStatus(resp.Err)\n\t\t\t}\n\t\t}()\n\n\t\tidx := counter\n\t\tcounter++\n\n\t\tswitch {\n\t\tcase idx < numTotalMiddleware:\n\t\t\tvar mw *Middleware\n\t\t\tif idx < numGlobalMiddleware {\n\t\t\t\tmw = globalMiddleware[idx]\n\t\t\t} else {\n\t\t\t\tmw = svcMiddleware[idx-numGlobalMiddleware]\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\t// Catch middleware panic\n\t\t\t\tif e := recover(); e != nil {\n\t\t\t\t\tpanicStack := stack.BuildWithoutGoRuntime(2)\n\t\t\t\t\tresp.Err = errs.B().Code(errs.Internal).Stack(panicStack).Meta(\"panic_stack\", panicStack).Msgf(\"panic executing middleware %s.%s: %v\",\n\t\t\t\t\t\tmw.PkgName, mw.Name, e).Err()\n\t\t\t\t\tresp.HTTPStatus = 500\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn mw.Invoke(req, nextFn)\n\n\t\tcase idx == numTotalMiddleware:\n\t\t\tdefer func() {\n\t\t\t\t// Catch handler panic\n\t\t\t\tif e := recover(); e != nil {\n\t\t\t\t\tpanicStack := stack.BuildWithoutGoRuntime(2)\n\t\t\t\t\tresp.Err = errs.B().Code(errs.Internal).Stack(panicStack).Meta(\"panic_stack\", panicStack).Msgf(\n\t\t\t\t\t\t\"panic handling request: %v\", e).Err()\n\t\t\t\t\tresp.HTTPStatus = 500\n\t\t\t\t}\n\t\t\t}()\n\t\t\treturn invokeHandler(req)\n\n\t\tdefault:\n\t\t\treturn middleware.Response{\n\t\t\t\tErr:        errs.B().Code(errs.Internal).Msg(\"middleware called next() too many times\").Err(),\n\t\t\t\tHTTPStatus: 500,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Only create the middleware.Request object if we actually have middleware.\n\tmwReq := middleware.NewLazyRequest(c.ctx, func() *encore.Request {\n\t\treturn c.server.encoreMgr.CurrentRequest()\n\t})\n\tmwResp := nextFn(mwReq)\n\n\tif mwResp.Err != nil {\n\t\treturn resp, mwResp.HTTPStatus, mwResp.GetHeaders(), mwResp.Err\n\t} else {\n\t\tif resp, ok := mwResp.Payload.(Resp); ok || isVoid[Resp]() {\n\t\t\treturn resp, mwResp.HTTPStatus, mwResp.GetHeaders(), mwResp.Err\n\t\t}\n\t}\n\n\treturn resp, 500, mwResp.GetHeaders(), errs.B().Code(errs.Internal).Msgf(\n\t\t\"invalid middleware: cannot return payload of type %T for endpoint %s.%s (expected type %T)\",\n\t\tmwResp.Payload, d.Service, d.Endpoint, resp,\n\t).Err()\n}\n\n// invokeHandlerNonRaw invokes the handler for a regular (non-raw) endpoint. If the endpoint is raw, it panics.\nfunc (d *Desc[Req, Resp]) invokeHandlerNonRaw(mwReq middleware.Request, reqData Req, handler func(context.Context, Req) (Resp, error)) (mwResp middleware.Response) {\n\tif d.Raw {\n\t\tpanic(\"invokeHandlerNonRaw called on Raw endpoint\")\n\t}\n\n\thandlerResp, handlerErr := handler(mwReq.Context(), reqData)\n\tif handlerErr != nil {\n\t\tmwResp.Err = errs.Convert(handlerErr)\n\t\tmwResp.HTTPStatus = errs.HTTPStatus(mwResp.Err)\n\t} else {\n\t\t// Only assign the payload if we're not dealing with *Void,\n\t\t// otherwise we would end up making Payload a \"typed nil\".\n\t\tif !isVoid[Resp]() {\n\t\t\tmwResp.Payload = handlerResp\n\t\t}\n\t\tmwResp.HTTPStatus = 200\n\t}\n\treturn mwResp\n}\n\n// invokeHandlerRaw invokes the handler for a raw endpoint. If the endpoint is not raw, it panics.\nfunc (d *Desc[Req, Resp]) invokeHandlerRaw(mwReq middleware.Request, c IncomingContext, capturer *rawResponseCapturer) (mwResp middleware.Response) {\n\tif !d.Raw {\n\t\tpanic(\"invokeHandlerRaw called on non-Raw endpoint\")\n\t}\n\n\t// Middleware can override the context, so check if the context is different\n\t// and if so change the request context.\n\thttpReq := c.req\n\tif ctx := mwReq.Context(); ctx != c.req.Context() {\n\t\thttpReq = httpReq.WithContext(ctx)\n\t}\n\n\tcapturer.InvokeHandler(http.HandlerFunc(d.RawHandler), httpReq)\n\n\tif capturer.Code >= 400 {\n\t\tstatusText := http.StatusText(capturer.Code)\n\t\tif statusText == \"\" {\n\t\t\tstatusText = \"HTTP \" + strconv.Itoa(capturer.Code)\n\t\t}\n\t\tmwResp.Err = errs.B().Code(errs.HTTPStatusToCode(capturer.Code)).Msg(statusText).Err()\n\t}\n\n\tmwResp.HTTPStatus = capturer.Code\n\treturn mwResp\n}\n\nfunc (d *Desc[Req, Resp]) toNamedParams(ps UnnamedParams) NamedParams {\n\tnamed := make(NamedParams, len(ps))\n\tfor i, p := range ps {\n\t\tnamed[i].Name = d.PathParamNames[i]\n\t\tnamed[i].Value = p\n\t}\n\treturn named\n}\n\ntype CallContext struct {\n\tctx    context.Context\n\tserver *Server\n}\n\nfunc (d *Desc[Req, Resp]) Call(c CallContext, req Req) (respData Resp, respErr error) {\n\t// If we're inside a test, we need to check if the target service has been mocked\n\t// and if it has, we need to route the call to the mock, otherwise\n\t// we'll make an internal call to the API\n\tif c.server.static.Testing {\n\t\tif mockedAPI, found := c.server.testingMgr.GetAPIMock(d.Service, d.Endpoint); found && mockedAPI.Function != nil {\n\t\t\tfunction, err := d.getMockFunction(mockedAPI)\n\t\t\tif err != nil {\n\t\t\t\treturn respData, errs.Wrap(err, \"unable to call mocked API due to an issue with the mock\")\n\t\t\t}\n\t\t\treturn d.mockedCall(c, function, req, mockedAPI.RunMiddleware)\n\t\t} else if mockedService, found := c.server.testingMgr.GetServiceMock(d.Service); found && mockedService.Service != nil {\n\t\t\tmethod, err := d.getMockMethod(mockedService)\n\t\t\tif err != nil {\n\t\t\t\treturn respData, errs.Wrap(err, \"unable to call mocked API due to an issue with the mock\")\n\t\t\t}\n\t\t\treturn d.mockedCall(c, method, req, mockedService.RunMiddleware)\n\t\t} else {\n\t\t\treturn d.internalCall(c, req)\n\t\t}\n\t}\n\n\tif cfgutil.IsHostedService(c.server.runtime, d.Service) {\n\t\t// If we're calling a hosted service, we can route via the\n\t\t// internal process\n\t\treturn d.internalCall(c, req)\n\t}\n\n\t// Otherwise we need to route via the service discovery mechanism\n\tservice, found := c.server.runtime.ServiceDiscovery[d.Service]\n\tif !found {\n\t\t// Any service we need to talk to should be in the service discovery map, if it is not\n\t\t// that implies the code is doing something unexpected and we should fail fast.\n\t\treturn respData, errs.B().Code(errs.Internal).Meta(\"service\", d.Service).Msg(\"no route to service found\").Err()\n\t} else {\n\t\treturn d.externalCall(c, service, req)\n\t}\n}\n\n// getMockMethod returns a reflected method for the given object, caching the result.\n// so subsequent calls will be faster.\nfunc (d *Desc[Req, Resp]) getMockMethod(svcMock model.ServiceMock) (reflectedAPIMethod[Req, Resp], error) {\n\td.mockCacheMu.RLock()\n\tmethod, found := d.mockObjCache[svcMock.Service]\n\td.mockCacheMu.RUnlock()\n\n\tif !found {\n\t\td.mockCacheMu.Lock()\n\t\tdefer d.mockCacheMu.Unlock()\n\n\t\t// Get a reflected value of the object\n\t\tval := reflect.ValueOf(svcMock.Service)\n\t\tif !val.IsValid() {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"object %T is not valid\", svcMock).Err()\n\t\t}\n\n\t\t// Get the method\n\t\t// nosemgrep\n\t\tmethodVal := val.MethodByName(d.Endpoint)\n\t\tif !methodVal.IsValid() {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"method %s not found on object %T\", d.Endpoint, svcMock.Service).Err()\n\t\t}\n\n\t\tm, err := createReflectionCaller[Req, Resp](methodVal)\n\t\tif err != nil {\n\t\t\treturn nil, errs.Wrap(err, \"unable to create mock caller\")\n\t\t}\n\n\t\t// Cache the method\n\t\tif len(d.mockObjCache) == 0 {\n\t\t\td.mockObjCache = make(map[any]reflectedAPIMethod[Req, Resp])\n\t\t}\n\t\td.mockObjCache[svcMock.Service] = m\n\t\treturn m, nil\n\t}\n\n\treturn method, nil\n}\n\n// getMockFunction returns a reflected method for the given function, caching the result.\n// so subsequent calls will be faster.\nfunc (d *Desc[Req, Resp]) getMockFunction(apiMock model.ApiMock) (reflectedAPIMethod[Req, Resp], error) {\n\td.mockCacheMu.RLock()\n\tmethod, found := d.mockFuncCache[apiMock.ID]\n\td.mockCacheMu.RUnlock()\n\n\tif !found {\n\t\td.mockCacheMu.Lock()\n\t\tdefer d.mockCacheMu.Unlock()\n\n\t\t// Get a reflected value of the object\n\t\tval := reflect.ValueOf(apiMock.Function)\n\t\tif !val.IsValid() {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"function %T is not valid\", apiMock.Function).Err()\n\t\t}\n\n\t\tm, err := createReflectionCaller[Req, Resp](val)\n\t\tif err != nil {\n\t\t\treturn nil, errs.Wrap(err, \"unable to create mock caller\")\n\t\t}\n\n\t\t// Cache the method\n\t\tif len(d.mockFuncCache) == 0 {\n\t\t\td.mockFuncCache = make(map[uint64]reflectedAPIMethod[Req, Resp])\n\t\t}\n\t\td.mockFuncCache[apiMock.ID] = m\n\t\treturn m, nil\n\t}\n\n\treturn method, nil\n}\n\nfunc (d *Desc[Req, Resp]) mockedCall(c CallContext, mock reflectedAPIMethod[Req, Resp], req Req, runMiddleware bool) (respData Resp, respErr error) {\n\treturn d.runCall(c, req, true, func(ec execContext, req Req) (Resp, int, error) {\n\t\t// If we want to run middleware, use the same code path as internalCall but switch out the handler\n\t\t// to our mock.\n\t\tif runMiddleware {\n\t\t\tresp, status, _, err := d.executeEndpoint(ec, func(mwReq middleware.Request) (mwResp middleware.Response) {\n\t\t\t\treturn d.invokeHandlerNonRaw(mwReq, req, mock)\n\t\t\t})\n\t\t\treturn resp, status, err\n\t\t}\n\n\t\trespData, err := mock(ec.ctx, req)\n\t\tif err != nil {\n\t\t\treturn respData, errs.HTTPStatus(err), err\n\t\t}\n\t\treturn respData, 200, nil\n\t})\n}\n\nfunc (d *Desc[Req, Resp]) internalCall(c CallContext, req Req) (respData Resp, respErr error) {\n\treturn d.runCall(c, req, false, func(ec execContext, req Req) (Resp, int, error) {\n\t\tresp, status, _, err := d.executeEndpoint(ec, func(mwReq middleware.Request) middleware.Response {\n\t\t\treturn d.invokeHandlerNonRaw(mwReq, req, d.AppHandler)\n\t\t})\n\t\treturn resp, status, err\n\t})\n}\n\nfunc (d *Desc[Req, Resp]) runCall(c CallContext, req Req, mocked bool, executor func(ec execContext, req Req) (Resp, int, error)) (respData Resp, respErr error) {\n\t// TODO: we don't currently support service-to-service calls of raw endpoints.\n\t// To fix this we need to improve our request serialization and DI support to\n\t// separate the signature for outgoing calls versus handlers.\n\tif d.Raw {\n\t\trespErr = errs.B().Code(errs.Internal).Msg(\"internal encore error: cannot call raw endpoints in service-to-service calls\").Err()\n\t\treturn\n\t}\n\n\treq, err := d.CloneReq(req)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to clone request\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\tpath, params, err := d.ReqPath(req)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to compute request path\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\tcall, meta, err := c.server.beginCall(c.ctx, d.Service, d.Endpoint, d.DefLoc)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to begin call\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\t// Run the request in a different goroutine\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tdefer close(done)\n\n\t\t// Default to GET if there are no methods available (or it's a wildcard)\n\t\thttpMethod := \"GET\"\n\t\tif len(d.Methods) > 0 && d.Methods[0] != \"*\" {\n\t\t\thttpMethod = d.Methods[0]\n\t\t}\n\n\t\tuserPayload := d.ReqUserPayload(req)\n\t\tvar nonRawPayload []byte\n\t\tif !d.Raw {\n\t\t\tnonRawPayload = marshalParams(c.server.json, userPayload)\n\t\t}\n\n\t\treqModel, beginErr := c.server.beginRequest(c.ctx, &beginRequestParams{\n\t\t\tType:          model.RPCCall,\n\t\t\tDefLoc:        d.DefLoc,\n\t\t\tCallerEventID: call.StartEventID,\n\n\t\t\tData: &model.RPCData{\n\t\t\t\tHTTPMethod:    httpMethod,\n\t\t\t\tPath:          path,\n\t\t\t\tPathParams:    d.toNamedParams(params),\n\t\t\t\tTypedPayload:  userPayload,\n\t\t\t\tDesc:          d.rpcDesc(),\n\t\t\t\tNonRawPayload: nonRawPayload,\n\n\t\t\t\tFromEncorePlatform:   false,\n\t\t\t\tRequestHeaders:       nil, // not set right now for internal requests\n\t\t\t\tServiceToServiceCall: true,\n\t\t\t\tMocked:               mocked,\n\t\t\t},\n\t\t})\n\n\t\t// Now round-trip any auth data that was set on the request\n\t\t// to emulate what happens in the HTTP case.\n\t\tif experiments.AuthDataRoundTrip.Enabled(c.server.experiments) && reqModel.RPCData.AuthData != nil {\n\t\t\tjsonBytes, err := jsonapi.Default.Marshal(reqModel.RPCData.AuthData)\n\t\t\tif err != nil {\n\t\t\t\tc.server.rootLogger.Err(err).Msg(\"unable to marshal auth data\")\n\t\t\t\trespErr = errs.B().Cause(err).Code(errs.Internal).Msg(\"internal error\").Err()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\treqModel.RPCData.AuthData = newAuthDataObj()\n\t\t\tif err := jsonapi.Default.Unmarshal(jsonBytes, reqModel.RPCData.AuthData); err != nil {\n\t\t\t\tc.server.rootLogger.Err(err).Msg(\"unable to unmarshal auth data\")\n\t\t\t\trespErr = errs.B().Cause(err).Code(errs.Internal).Msg(\"internal error\").Err()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif beginErr != nil {\n\t\t\trespErr = errs.B().Cause(beginErr).Code(errs.Internal).Msg(\"internal error\").Err()\n\t\t\treturn\n\t\t}\n\n\t\tif err := d.validate(req); err != nil {\n\t\t\trespErr = err\n\t\t\treturn\n\t\t}\n\n\t\tec := c.server.newExecContext(c.ctx, params, meta)\n\t\tr, httpStatus, rpcErr := executor(ec, req)\n\n\t\tif rpcErr == nil {\n\t\t\tr, rpcErr = d.CloneResp(r)\n\t\t\tif rpcErr != nil {\n\t\t\t\t// only override the http status if an error occurred trying to clone the response\n\t\t\t\thttpStatus = errs.HTTPStatus(errs.Convert(rpcErr))\n\t\t\t}\n\t\t}\n\t\tif rpcErr != nil {\n\t\t\trespErr = errs.RoundTrip(rpcErr)\n\t\t\tc.server.finishRequest(newErrResp(respErr, httpStatus))\n\t\t} else {\n\t\t\trespData, respErr = r, nil\n\t\t\t// Always nil for now since we don't support raw endpoints in service-to-service calls.\n\t\t\tvar (\n\t\t\t\treqCapture  *rawRequestBodyCapturer\n\t\t\t\trespCapture *rawResponseCapturer\n\t\t\t)\n\t\t\tmodelResp := newResp(respData, httpStatus, respErr, d.Raw, reqCapture, respCapture, c.server.json)\n\t\t\tc.server.finishRequest(modelResp)\n\t\t}\n\t}()\n\t<-done\n\n\tif respErr != nil {\n\t\tc.server.rootLogger.Err(respErr).Msg(\"call failed\")\n\t}\n\tc.server.finishCall(call, respErr)\n\treturn\n}\n\nfunc (d *Desc[Req, Resp]) externalCall(c CallContext, service config.Service, req Req) (respData Resp, respErr error) {\n\t// TODO: we don't currently support service-to-service calls of raw endpoints.\n\t// To fix this we need to improve our request serialization and DI support to\n\t// separate the signature for outgoing calls versus handlers.\n\tif d.Raw {\n\t\trespErr = errs.B().Code(errs.Internal).Msg(\"internal encore error: cannot call raw endpoints in service-to-service calls\").Err()\n\t\treturn\n\t}\n\n\t// Lookup the service\n\tif service.Protocol != config.Http {\n\t\t// For now we only support HTTP services\n\t\trespErr = errs.B().Code(errs.Internal).Msg(\"internal encore error: unsupported service protocol\").Err()\n\t\treturn\n\t}\n\n\tpath, _, err := d.ReqPath(req)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to compute request path\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\t// Default to POST if there are no methods available (or it's a wildcard)\n\thttpMethod := \"POST\"\n\tif len(d.Methods) > 0 && d.Methods[0] != \"*\" {\n\t\thttpMethod = d.Methods[0]\n\t}\n\n\t// Encode the request payload\n\tvar buf bytes.Buffer\n\tstream := c.server.json.BorrowStream(&buf)\n\theader, queryString, err := d.EncodeExternalReq(req, stream)\n\tif err2 := stream.Flush(); err == nil {\n\t\terr = err2\n\t}\n\tc.server.json.ReturnStream(stream)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to marshal request\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\treqURL := service.URL + path\n\tif len(queryString) > 0 {\n\t\treqURL += \"?\" + queryString.Encode()\n\t}\n\thttpReq, err := http.NewRequestWithContext(c.ctx, httpMethod, reqURL, &buf)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to create HTTP request\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\tfor key, val := range header {\n\t\thttpReq.Header[key] = val\n\t}\n\treqTransport := transport.HTTPRequest(httpReq)\n\n\t// Set the name of the API we want to call\n\treqTransport.SetMeta(calleeMetaName, fmt.Sprintf(\"%s.%s\", d.Service, d.Endpoint))\n\n\tcall, meta, err := c.server.beginCall(c.ctx, d.Service, d.Endpoint, d.DefLoc)\n\tif err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to begin call\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\tif err := meta.AddToRequest(c.server, service, reqTransport); err != nil {\n\t\tc.server.rootLogger.Err(err).Msg(\"unable to add metadata to request\")\n\t\trespErr = errs.Convert(err)\n\t\treturn\n\t}\n\n\trespData, respErr = (func() (resp Resp, err error) {\n\t\thttpResp, err := c.server.httpClient.Do(httpReq)\n\t\tif err != nil {\n\t\t\treturn resp, err\n\t\t}\n\t\tdefer func() { _ = httpResp.Body.Close() }()\n\n\t\tif httpResp.StatusCode >= 400 {\n\t\t\treturn resp, unmarshalErrorResponse(httpResp)\n\t\t}\n\n\t\treturn d.DecodeExternalResp(httpResp, c.server.json)\n\t})()\n\n\tif respErr != nil {\n\t\tc.server.rootLogger.Err(respErr).Str(\"target\", fmt.Sprintf(\"%s.%s\", d.Service, d.Endpoint)).Str(\"url\", reqURL).Msg(\"call failed\")\n\t}\n\n\tc.server.finishCall(call, respErr)\n\treturn\n}\n\n// unmarshalErrorResponse unmarshals an error response from an HTTP response.\n//\n// If the error sent back from the server was an Encore encoded error, we unmashal that to restore the original\n// error types. Otherwise, we return a internal server generic error.\nfunc unmarshalErrorResponse(httpResp *http.Response) error {\n\tif httpResp.Header.Get(\"X-Encore-Full-Error\") == \"1\" {\n\t\terrBytes, err := io.ReadAll(httpResp.Body)\n\t\tif err != nil {\n\t\t\treturn errs.B().Code(errs.Internal).Msg(\"request failed: unable to read response\").Err()\n\t\t}\n\n\t\trespErr, marshallingErr := errmarshalling.Unmarshal(errBytes)\n\t\tif marshallingErr != nil {\n\t\t\treturn errs.B().Code(errs.Internal).Cause(err).Msg(\"request failed: unable to unmarshal response\").Err()\n\t\t}\n\n\t\treturn respErr\n\t} else {\n\t\tbodyBytes, err := io.ReadAll(httpResp.Body)\n\t\tif err != nil {\n\t\t\treturn errs.B().Code(errs.Internal).Msg(\"request failed: unable to read response\").Err()\n\t\t}\n\n\t\tif len(bodyBytes) == 0 {\n\t\t\treturn errs.B().Code(errs.Internal).Msgf(\"request failed: status %s\", httpResp.Status).Err()\n\t\t} else {\n\t\t\treturn errs.B().Code(errs.Internal).Msgf(\"request failed: status %s: %s\", httpResp.Status, string(bodyBytes)).Err()\n\t\t}\n\t}\n}\n\n// headersWithHost clones the request headers and adds the Host header from req.Host.\n// This is needed because Go stores the Host header in req.Host, not in req.Header.\nfunc headersWithHost(req *http.Request) http.Header {\n\tvar headers http.Header\n\tif req.Header != nil {\n\t\theaders = maps.Clone(req.Header)\n\t} else {\n\t\theaders = make(http.Header)\n\t}\n\tif req.Host != \"\" {\n\t\theaders.Set(\"Host\", req.Host)\n\t}\n\treturn headers\n}\n\n// validate validates the request, and returns a validation error on failure.\n// If the user payload does not implement Validator, it returns nil.\nfunc (d *Desc[Req, Resp]) validate(req Req) error {\n\treturn runValidate(d.ReqUserPayload(req))\n}\n\n// runValidate validates the request, and returns a validation error on failure.\n// If the user payload does not implement Validator, it returns nil.\nfunc runValidate(userPayload any) error {\n\tif v, ok := userPayload.(Validator); ok {\n\t\tif err := v.Validate(); err != nil {\n\t\t\t// If we already have an *errs.Error, return it directly.\n\t\t\tif _, ok := err.(*errs.Error); ok {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn errs.WrapCode(err, errs.InvalidArgument, \"validation failed\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// rpcDesc returns the RPC description for this endpoint,\n// computing and caching the first time it's called.\nfunc (d *Desc[Req, Resp]) rpcDesc() *model.RPCDesc {\n\td.rpcDescOnce.Do(func() {\n\t\tvar reqTyp Req\n\t\tdesc := &model.RPCDesc{\n\t\t\tService:      d.Service,\n\t\t\tSvcNum:       d.SvcNum,\n\t\t\tEndpoint:     d.Endpoint,\n\t\t\tRaw:          d.Raw,\n\t\t\tRequestType:  reflect.TypeOf(reqTyp),\n\t\t\tTags:         d.Tags,\n\t\t\tExposed:      d.Access == Public || d.Access == RequiresAuth,\n\t\t\tAuthRequired: d.Access == RequiresAuth,\n\n\t\t\tScrubRequestPaths:    d.ScrubRequestPaths,\n\t\t\tScrubResponsePaths:   d.ScrubResponsePaths,\n\t\t\tScrubRequestHeaders:  d.ScrubRequestHeaders,\n\t\t\tScrubResponseHeaders: d.ScrubResponseHeaders,\n\t\t}\n\n\t\tif !isVoid[Resp]() {\n\t\t\tvar typ Resp\n\t\t\tdesc.ResponseType = reflect.TypeOf(typ)\n\t\t}\n\t\td.cachedRPCDesc = desc\n\t})\n\treturn d.cachedRPCDesc\n}\n\nfunc marshalParams[Resp any](json jsoniter.API, resp Resp) []byte {\n\tif isVoid[Resp]() {\n\t\treturn nil\n\t}\n\tdata, _ := json.Marshal(resp)\n\treturn data\n}\n\n// newResp returns an *model.Response for a response.\nfunc newResp[Resp any](respData Resp, httpStatus int, err error, isRaw bool,\n\treqCapture *rawRequestBodyCapturer, respCapture *rawResponseCapturer, json jsoniter.API,\n) *model.Response {\n\treturn newRespWithHeaders(respData, httpStatus, err, nil, isRaw, reqCapture, respCapture, json)\n}\n\n// newRespWithHeaders returns an *model.Response for a response with custom headers.\nfunc newRespWithHeaders[Resp any](respData Resp, httpStatus int, err error, headers http.Header, isRaw bool,\n\treqCapture *rawRequestBodyCapturer, respCapture *rawResponseCapturer, json jsoniter.API,\n) *model.Response {\n\tresp := &model.Response{\n\t\tHTTPStatus: httpStatus,\n\t\tErr:        err,\n\t\tHeaders:    headers,\n\t}\n\n\tif isRaw {\n\t\tif reqCapture != nil {\n\t\t\tresp.RawRequestPayload, resp.RawRequestPayloadOverflowed = reqCapture.FinishCapturing()\n\t\t}\n\t\tif respCapture != nil {\n\t\t\tresp.RawResponseHeaders = respCapture.Header\n\t\t\tresp.RawResponsePayload, resp.RawResponsePayloadOverflowed = respCapture.FinishCapturing()\n\t\t}\n\t} else {\n\t\tresp.TypedPayload = respData\n\t\tresp.Payload = marshalParams(json, respData)\n\t}\n\treturn resp\n}\n\n// newErrResp returns an *model.Response for an error.\n// If httpStatus is 0 it's inferred from the error.\nfunc newErrResp(err error, httpStatus int) *model.Response {\n\tif httpStatus == 0 {\n\t\thttpStatus = errs.HTTPStatus(err)\n\t}\n\treturn &model.Response{\n\t\tHTTPStatus: httpStatus,\n\t\tErr:        err,\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/handler_test.go",
    "content": "package api_test\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/benbjohnson/clock\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/rs/zerolog\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/health\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/appruntime/shared/traceprovider\"\n\t\"encore.dev/appruntime/shared/traceprovider/mock_trace\"\n\t\"encore.dev/beta/errs\"\n\tusermetrics \"encore.dev/metrics\"\n\t\"encore.dev/middleware\"\n\t\"encore.dev/pubsub\"\n)\n\ntype mockReq struct {\n\tBody string\n}\n\ntype mockResp struct {\n\tMessage string\n}\n\nfunc TestDesc_EndToEnd(t *testing.T) {\n\tserver, _, metricsRegistry := testServer(t, clock.New(), false)\n\n\ttests := []struct {\n\t\tname        string\n\t\taccess      api.Access\n\t\treqBody     string\n\t\trespBody    string\n\t\tstatus      int\n\t\trespHeaders http.Header\n\t}{\n\t\t{\n\t\t\tname:        \"echo\",\n\t\t\taccess:      api.Public,\n\t\t\treqBody:     `{\"Body\": \"foo\"}`,\n\t\t\trespBody:    `{\"Message\":\"foo\"}`,\n\t\t\tstatus:      200,\n\t\t\trespHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid\",\n\t\t\taccess:      api.Public,\n\t\t\treqBody:     `invalid json`,\n\t\t\trespBody:    ``,\n\t\t\tstatus:      400,\n\t\t\trespHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t\t{\n\t\t\tname:        \"unauthenticated\",\n\t\t\taccess:      api.RequiresAuth,\n\t\t\treqBody:     `{}`,\n\t\t\trespBody:    ``,\n\t\t\tstatus:      401,\n\t\t\trespHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/\", strings.NewReader(test.reqBody))\n\t\t\tps := api.UnnamedParams{\"value\"}\n\t\t\tdesc := newMockAPIDesc(test.access)\n\t\t\tdesc.Handle(server.NewIncomingContext(w, req, ps, api.CallMeta{}))\n\t\t\tif w.Code != test.status {\n\t\t\t\tt.Errorf(\"got code %d, want %d\", w.Code, test.status)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif test.respBody != \"\" {\n\t\t\t\tif got := w.Body.String(); got != test.respBody {\n\t\t\t\t\tt.Errorf(\"got body %q, want %q\", got, test.respBody)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif test.respHeaders != nil {\n\t\t\t\tfor key, val := range test.respHeaders {\n\t\t\t\t\tif diff := cmp.Diff(val, w.Header()[key]); diff != \"\" {\n\t\t\t\t\t\tt.Errorf(\"header %s: unexpected response header value (-want +got):\\n%s\", key, diff)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t})\n\t}\n\n\tcollected := metricsRegistry.Collect()\n\tif len(collected) != 2 {\n\t\tt.Fatalf(\"got %d metrics, want 2\", len(collected))\n\t}\n\n\tokLabels := []usermetrics.KeyValue{\n\t\t{\n\t\t\tKey:   \"endpoint\",\n\t\t\tValue: \"endpoint\",\n\t\t},\n\t\t{\n\t\t\tKey:   \"code\",\n\t\t\tValue: \"ok\",\n\t\t},\n\t}\n\trequestsTotalOk := findMetric(collected, \"e_requests_total\", okLabels)\n\tif requestsTotalOk == nil {\n\t\tt.Log(`e_requests_total{endpoint=\"endpoint\",code=\"ok\"} metric not found`)\n\t\tt.FailNow()\n\t}\n\n\tif _, ok := requestsTotalOk.Val.([]uint64); !ok {\n\t\tt.Log(`expected e_requests_total{endpoint=\"endpoint\",code=\"ok\"} value to be []uint64`)\n\t\tt.FailNow()\n\t}\n\n\tinvalidArgLabels := []usermetrics.KeyValue{\n\t\t{\n\t\t\tKey:   \"endpoint\",\n\t\t\tValue: \"endpoint\",\n\t\t},\n\t\t{\n\t\t\tKey:   \"code\",\n\t\t\tValue: errs.InvalidArgument.String(),\n\t\t},\n\t}\n\trequestsTotalInvalidArg := findMetric(collected, \"e_requests_total\", invalidArgLabels)\n\tif requestsTotalInvalidArg == nil {\n\t\tt.Log(`e_requests_total{endpoint=\"endpoint\",code=\"invalid_argument\"} metric not found`)\n\t\tt.FailNow()\n\t}\n\n\tif _, ok := requestsTotalInvalidArg.Val.([]uint64); !ok {\n\t\tt.Log(`expected e_requests_total{endpoint=\"endpoint\",code=\"invalid_argument\"} value to be []uint64`)\n\t\tt.FailNow()\n\t}\n}\n\nfunc findMetric(collected []usermetrics.CollectedMetric, name string, labels []usermetrics.KeyValue) *usermetrics.CollectedMetric {\n\tfor _, metric := range collected {\n\t\tif metric.Info.Name() == name &&\n\t\t\treflect.DeepEqual(metric.Labels, labels) {\n\t\t\treturn &metric\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc TestDescGeneratesTrace(t *testing.T) {\n\tmodel.EnableTestMode(t)\n\tklock := clock.NewMock()\n\tklock.Set(time.Now())\n\n\ttests := []struct {\n\t\tname       string\n\t\taccess     api.Access\n\t\traw        bool\n\t\treqBody    string\n\t\treqHeaders http.Header\n\t\twant       *model.Request\n\t}{\n\t\t{\n\t\t\tname:       \"echo\",\n\t\t\taccess:     api.Public,\n\t\t\treqBody:    `{\"Body\": \"foo\"}`,\n\t\t\treqHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t\twant: &model.Request{\n\t\t\t\tType:         model.RPCCall,\n\t\t\t\tTraceID:      model.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tSpanID:       model.SpanID{0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\tStart:        klock.Now(),\n\t\t\t\tTraced:       true,\n\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\tService:      \"service\",\n\t\t\t\t\t\tEndpoint:     \"endpoint\",\n\t\t\t\t\t\tRaw:          false,\n\t\t\t\t\t\tRequestType:  reflect.TypeOf(&mockReq{}),\n\t\t\t\t\t\tResponseType: reflect.TypeOf(&mockResp{}),\n\t\t\t\t\t\tExposed:      true,\n\t\t\t\t\t\tAuthRequired: false,\n\t\t\t\t\t},\n\t\t\t\t\tHTTPMethod:     \"POST\",\n\t\t\t\t\tPath:           \"/path/hello\",\n\t\t\t\t\tPathParams:     model.PathParams{{Name: \"one\", Value: \"hello\"}},\n\t\t\t\t\tUserID:         \"\",\n\t\t\t\t\tAuthData:       nil,\n\t\t\t\t\tTypedPayload:   &mockReq{Body: \"foo\"},\n\t\t\t\t\tNonRawPayload:  []byte(`{\"Body\":\"foo\"}`),\n\t\t\t\t\tRequestHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}, \"Host\": []string{\"example.com\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid\",\n\t\t\taccess:  api.Public,\n\t\t\treqBody: `invalid json`,\n\t\t\twant: &model.Request{\n\t\t\t\tType:         model.RPCCall,\n\t\t\t\tTraceID:      model.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tSpanID:       model.SpanID{0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\tStart:        klock.Now(),\n\t\t\t\tTraced:       true,\n\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\tService:      \"service\",\n\t\t\t\t\t\tEndpoint:     \"endpoint\",\n\t\t\t\t\t\tRaw:          false,\n\t\t\t\t\t\tRequestType:  reflect.TypeOf(&mockReq{}),\n\t\t\t\t\t\tResponseType: reflect.TypeOf(&mockResp{}),\n\t\t\t\t\t\tExposed:      true,\n\t\t\t\t\t\tAuthRequired: false,\n\t\t\t\t\t},\n\t\t\t\t\tHTTPMethod:     \"POST\",\n\t\t\t\t\tPath:           \"/path/hello\",\n\t\t\t\t\tPathParams:     model.PathParams{{Name: \"one\", Value: \"hello\"}},\n\t\t\t\t\tUserID:         \"\",\n\t\t\t\t\tAuthData:       nil,\n\t\t\t\t\tTypedPayload:   nil,\n\t\t\t\t\tRequestHeaders: http.Header{\"Host\": []string{\"example.com\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"unauthenticated\",\n\t\t\taccess:  api.RequiresAuth,\n\t\t\treqBody: `{}`,\n\t\t\twant:    nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"raw\",\n\t\t\taccess:     api.Public,\n\t\t\traw:        true,\n\t\t\treqBody:    `{}`,\n\t\t\treqHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}},\n\t\t\twant: &model.Request{\n\t\t\t\tType:         model.RPCCall,\n\t\t\t\tTraceID:      model.TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tSpanID:       model.SpanID{0, 0, 0, 0, 0, 0, 0, 1},\n\t\t\t\tParentSpanID: model.SpanID{},\n\t\t\t\tStart:        klock.Now(),\n\t\t\t\tTraced:       true,\n\t\t\t\tRPCData: &model.RPCData{\n\t\t\t\t\tDesc: &model.RPCDesc{\n\t\t\t\t\t\tService:      \"service\",\n\t\t\t\t\t\tEndpoint:     \"raw\",\n\t\t\t\t\t\tRaw:          true,\n\t\t\t\t\t\tRequestType:  reflect.TypeOf(&rawMockReq{}),\n\t\t\t\t\t\tResponseType: nil,\n\t\t\t\t\t\tExposed:      true,\n\t\t\t\t\t\tAuthRequired: false,\n\t\t\t\t\t},\n\t\t\t\t\tHTTPMethod:     \"POST\",\n\t\t\t\t\tPath:           \"/path/hello\",\n\t\t\t\t\tPathParams:     model.PathParams{{Name: \"one\", Value: \"hello\"}},\n\t\t\t\t\tUserID:         \"\",\n\t\t\t\t\tAuthData:       nil,\n\t\t\t\t\tTypedPayload:   nil,\n\t\t\t\t\tRequestHeaders: http.Header{\"Content-Type\": []string{\"application/json\"}, \"Host\": []string{\"example.com\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\topts := []cmp.Option{\n\t\tcmpopts.IgnoreFields(model.Request{}, \"Logger\"),\n\t\tcmp.Comparer(func(a, b reflect.Type) bool { return a == b }),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tserver, traceMock, _ := testServer(t, klock, true)\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/path/hello\", strings.NewReader(test.reqBody))\n\t\t\treq.Header = test.reqHeaders\n\t\t\tps := api.UnnamedParams{\"hello\"}\n\n\t\t\tvar handler api.Handler\n\t\t\tif test.raw {\n\t\t\t\thandler = newRawMockAPIDesc(test.access, nil)\n\t\t\t} else {\n\t\t\t\thandler = newMockAPIDesc(test.access)\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\tbeginReq *model.Request\n\t\t\t)\n\n\t\t\ttraceMock.\n\t\t\t\tEXPECT().RequestSpanStart(gomock.Any(), gomock.Any()).Do(\n\t\t\t\tfunc(req *model.Request, _ uint32) {\n\t\t\t\t\tbeginReq = req\n\t\t\t\t}).MaxTimes(1)\n\n\t\t\ttraceMock.\n\t\t\t\tEXPECT().\n\t\t\t\tRequestSpanEnd(gomock.Any()).MaxTimes(1)\n\n\t\t\ttraceMock.EXPECT().WaitAndClear().AnyTimes()\n\t\t\ttraceMock.EXPECT().WaitUntilDone().AnyTimes()\n\t\t\ttraceMock.EXPECT().MarkDone().MaxTimes(1)\n\n\t\t\thandler.Handle(server.NewIncomingContext(w, req, ps, api.CallMeta{}))\n\n\t\t\tif diff := cmp.Diff(test.want, beginReq, opts...); diff != \"\" {\n\t\t\t\tt.Errorf(\"beginReq mismatch (-want +got):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestRawEndpointOverflow tests that raw endpoint capturing\n// is limited to the max capture size.\nfunc TestRawEndpointOverflow(t *testing.T) {\n\tmodel.EnableTestMode(t)\n\tklock := clock.NewMock()\n\tklock.Set(time.Now())\n\n\tserver, traceMock, _ := testServer(t, klock, true)\n\n\tvar (\n\t\treqBody           = strings.Repeat(\"a\", 2*api.MaxRawRequestCaptureLen)\n\t\trespBody          = strings.Repeat(\"b\", 2*api.MaxRawResponseCaptureLen)\n\t\twantTraceReqData  = reqBody[:api.MaxRawRequestCaptureLen]\n\t\twantTraceRespData = respBody[:api.MaxRawResponseCaptureLen]\n\t)\n\n\tw := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/path/hello\", strings.NewReader(reqBody))\n\tps := api.UnnamedParams{\"hello\"}\n\n\thandler := newRawMockAPIDesc(api.Public, func(w http.ResponseWriter, r *http.Request) {\n\t\t_, _ = io.ReadAll(r.Body) // consume the body\n\t\t_, _ = w.Write([]byte(respBody))\n\t})\n\n\tvar params []trace2.BodyStreamParams\n\n\ttraceMock.\n\t\tEXPECT().\n\t\tRequestSpanStart(gomock.Any(), gomock.Any()).\n\t\tMaxTimes(1)\n\n\ttraceMock.\n\t\tEXPECT().\n\t\tRequestSpanEnd(gomock.Any()).\n\t\tMaxTimes(1)\n\n\ttraceMock.\n\t\tEXPECT().\n\t\tBodyStream(gomock.Any()).Do(\n\t\tfunc(p trace2.BodyStreamParams) {\n\t\t\tparams = append(params, p)\n\t\t}).AnyTimes()\n\n\ttraceMock.EXPECT().WaitAndClear().MinTimes(1)\n\ttraceMock.EXPECT().MarkDone().Times(1)\n\n\thandler.Handle(server.NewIncomingContext(w, req, ps, api.CallMeta{}))\n\n\tif len(params) != 2 {\n\t\tt.Fatalf(\"got %d BodyStream events, want %d\", len(params), 2)\n\t}\n\twant := []trace2.BodyStreamParams{\n\t\t{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: params[0].TraceID,\n\t\t\t\tSpanID:  params[0].SpanID,\n\t\t\t},\n\t\t\tIsResponse: false,\n\t\t\tOverflowed: true,\n\t\t\tData:       []byte(wantTraceReqData),\n\t\t},\n\t\t{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: params[0].TraceID,\n\t\t\t\tSpanID:  params[0].SpanID,\n\t\t\t},\n\t\t\tIsResponse: true,\n\t\t\tOverflowed: true,\n\t\t\tData:       []byte(wantTraceRespData),\n\t\t},\n\t}\n\tif diff := cmp.Diff(want, params); diff != \"\" {\n\t\tt.Errorf(\"BodyStream params mismatch (-want +got):\\n%s\", diff)\n\t}\n}\n\nfunc testServer(t *testing.T, klock clock.Clock, mockTraces bool) (*api.Server, *mock_trace.MockLogger, *usermetrics.Registry) {\n\tctrl := gomock.NewController(t)\n\n\tvar tf traceprovider.Factory\n\ttraceMock := mock_trace.NewMockLogger(ctrl)\n\tif mockTraces {\n\t\ttf = mock_trace.NewMockFactory(traceMock)\n\t} else {\n\t\ttf = &traceprovider.DefaultFactory{}\n\t}\n\n\tstatic := &config.Static{}\n\truntime := &config.Runtime{}\n\n\tlogger := zerolog.New(os.Stdout)\n\trt := reqtrack.New(logger, nil, tf)\n\tmetricsRegistry := usermetrics.NewRegistry(rt, len(static.BundledServices))\n\tjson := jsoniter.ConfigCompatibleWithStandardLibrary\n\tencoreMgr := encore.NewManager(static, runtime, rt)\n\ttsMgr := testsupport.NewManager(static, rt, logger)\n\tpubsubMgr := pubsub.NewManager(static, runtime, rt, tsMgr, logger, json)\n\thealthMgr := health.NewCheckRegistry()\n\ttestingMgr := testsupport.NewManager(static, rt, logger)\n\tserver := api.NewServer(static, runtime, rt, nil, encoreMgr, pubsubMgr, logger, metricsRegistry, healthMgr, testingMgr, json, klock)\n\treturn server, traceMock, metricsRegistry\n}\n\nfunc newMockAPIDesc(access api.Access) *api.Desc[*mockReq, *mockResp] {\n\treturn &api.Desc[*mockReq, *mockResp]{\n\t\tService:        \"service\",\n\t\tEndpoint:       \"endpoint\",\n\t\tPath:           \"/path/:one\",\n\t\tAccess:         access,\n\t\tPathParamNames: []string{\"one\"},\n\t\tRaw:            false,\n\n\t\tDecodeReq: func(req *http.Request, ps api.UnnamedParams, json jsoniter.API) (*mockReq, api.UnnamedParams, error) {\n\t\t\tvar reqData mockReq\n\t\t\tif err := json.NewDecoder(req.Body).Decode(&reqData); err != nil {\n\t\t\t\treturn nil, ps, err\n\t\t\t}\n\t\t\treturn &reqData, ps, nil\n\t\t},\n\t\tCloneReq: func(req *mockReq) (*mockReq, error) {\n\t\t\tif req == nil {\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\tclone := *req\n\t\t\treturn &clone, nil\n\t\t},\n\t\tReqPath: func(req *mockReq) (string, api.UnnamedParams, error) {\n\t\t\treturn \"/path/TODO\", nil, nil\n\t\t},\n\t\tReqUserPayload: func(req *mockReq) any {\n\t\t\treturn req\n\t\t},\n\t\tAppHandler: func(ctx context.Context, req *mockReq) (*mockResp, error) {\n\t\t\treturn &mockResp{Message: req.Body}, nil\n\t\t},\n\t\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp *mockResp, status int) error {\n\t\t\tdata, err := json.Marshal(resp)\n\t\t\t_, _ = w.Write(data)\n\t\t\treturn err\n\t\t},\n\t\tCloneResp: func(resp *mockResp) (*mockResp, error) {\n\t\t\tif resp == nil {\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\tclone := *resp\n\t\t\treturn &clone, nil\n\t\t},\n\t}\n}\n\ntype rawMockReq struct{}\n\nfunc newRawMockAPIDesc(access api.Access, handler http.HandlerFunc) *api.Desc[*rawMockReq, api.Void] {\n\treturn &api.Desc[*rawMockReq, api.Void]{\n\t\tService:        \"service\",\n\t\tEndpoint:       \"raw\",\n\t\tPath:           \"/path/:one\",\n\t\tAccess:         access,\n\t\tPathParamNames: []string{\"one\"},\n\t\tRaw:            true,\n\n\t\tDecodeReq: func(req *http.Request, ps api.UnnamedParams, json jsoniter.API) (*rawMockReq, api.UnnamedParams, error) {\n\t\t\treturn &rawMockReq{}, ps, nil\n\t\t},\n\t\tCloneReq: func(req *rawMockReq) (*rawMockReq, error) {\n\t\t\tif req == nil {\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\tclone := *req\n\t\t\treturn &clone, nil\n\t\t},\n\t\tReqPath: func(req *rawMockReq) (string, api.UnnamedParams, error) {\n\t\t\treturn \"/foo\", nil, nil\n\t\t},\n\t\tReqUserPayload: func(req *rawMockReq) any {\n\t\t\treturn nil\n\t\t},\n\t\tRawHandler: func(w http.ResponseWriter, req *http.Request) {\n\t\t\tif handler != nil {\n\t\t\t\thandler.ServeHTTP(w, req)\n\t\t\t}\n\t\t},\n\t\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp api.Void, status int) error {\n\t\t\treturn nil\n\t\t},\n\t\tCloneResp: func(resp api.Void) (api.Void, error) {\n\t\t\treturn resp, nil\n\t\t},\n\t}\n}\n\n// TestMiddlewareHeaders tests that middleware can set headers and they are properly\n// written to the HTTP response.\nfunc TestMiddlewareHeaders(t *testing.T) {\n\tmodel.EnableTestMode(t)\n\n\tserver, _, _ := testServer(t, clock.New(), false)\n\n\t// Create a middleware that sets headers\n\theaderMiddleware := &api.Middleware{\n\t\tID:      \"test-middleware\",\n\t\tPkgName: \"test\",\n\t\tName:    \"HeaderMiddleware\",\n\t\tGlobal:  false,\n\t\tInvoke: func(req middleware.Request, next middleware.Next) middleware.Response {\n\t\t\tresp := next(req)\n\n\t\t\t// Set various types of headers\n\t\t\tresp.Header().Set(\"X-Custom-Header\", \"custom-value\")\n\t\t\tresp.Header().Add(\"X-Multi-Header\", \"value1\")\n\t\t\tresp.Header().Add(\"X-Multi-Header\", \"value2\")\n\t\t\tresp.Header().Set(\"X-Middleware-Applied\", \"true\")\n\n\t\t\treturn resp\n\t\t},\n\t}\n\n\t// Create API desc with the middleware\n\tdesc := newMockAPIDesc(api.Public)\n\tdesc.ServiceMiddleware = []*api.Middleware{headerMiddleware}\n\n\ttests := []struct {\n\t\tname            string\n\t\texpectSuccess   bool\n\t\texpectedBody    string\n\t\texpectedHeaders map[string][]string\n\t}{\n\t\t{\n\t\t\tname:          \"success_with_headers\",\n\t\t\texpectSuccess: true,\n\t\t\texpectedBody:  `{\"Message\":\"test\"}`,\n\t\t\texpectedHeaders: map[string][]string{\n\t\t\t\t\"X-Custom-Header\":        {\"custom-value\"},\n\t\t\t\t\"X-Multi-Header\":         {\"value1\", \"value2\"},\n\t\t\t\t\"X-Middleware-Applied\":   {\"true\"},\n\t\t\t\t\"Content-Type\":           {\"application/json\"},\n\t\t\t\t\"X-Content-Type-Options\": {\"nosniff\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(\"POST\", \"/path/hello\", strings.NewReader(`{\"Body\":\"test\"}`))\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\tps := api.UnnamedParams{\"hello\"}\n\n\t\t\tdesc.Handle(server.NewIncomingContext(w, req, ps, api.CallMeta{}))\n\n\t\t\t// Check status code\n\t\t\tif test.expectSuccess && w.Code != 200 {\n\t\t\t\tt.Errorf(\"expected success (200), got %d\", w.Code)\n\t\t\t}\n\n\t\t\t// Check response body\n\t\t\tif test.expectedBody != \"\" {\n\t\t\t\tif got := w.Body.String(); got != test.expectedBody {\n\t\t\t\t\tt.Errorf(\"got body %q, want %q\", got, test.expectedBody)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check headers\n\t\t\tfor expectedHeader, expectedValues := range test.expectedHeaders {\n\t\t\t\tgotValues := w.Header().Values(expectedHeader)\n\t\t\t\tif len(gotValues) != len(expectedValues) {\n\t\t\t\t\tt.Errorf(\"header %s: got %d values %v, want %d values %v\",\n\t\t\t\t\t\texpectedHeader, len(gotValues), gotValues, len(expectedValues), expectedValues)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor i, expectedValue := range expectedValues {\n\t\t\t\t\tif gotValues[i] != expectedValue {\n\t\t\t\t\t\tt.Errorf(\"header %s[%d]: got %q, want %q\",\n\t\t\t\t\t\t\texpectedHeader, i, gotValues[i], expectedValue)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestMiddlewareHeadersOnError tests that middleware headers are applied even when an error occurs.\nfunc TestMiddlewareHeadersOnError(t *testing.T) {\n\tmodel.EnableTestMode(t)\n\n\tserver, _, _ := testServer(t, clock.New(), false)\n\n\t// Create a middleware that sets headers\n\theaderMiddleware := &api.Middleware{\n\t\tID:      \"test-middleware\",\n\t\tPkgName: \"test\",\n\t\tName:    \"HeaderMiddleware\",\n\t\tGlobal:  false,\n\t\tInvoke: func(req middleware.Request, next middleware.Next) middleware.Response {\n\t\t\tresp := next(req)\n\n\t\t\t// Set headers regardless of success/error\n\t\t\tresp.Header().Set(\"X-Error-Header\", \"error-value\")\n\t\t\tresp.Header().Set(\"X-Always-Present\", \"always\")\n\n\t\t\treturn resp\n\t\t},\n\t}\n\n\t// Create API desc with the middleware that returns an error\n\tdesc := newMockAPIDesc(api.Public)\n\tdesc.ServiceMiddleware = []*api.Middleware{headerMiddleware}\n\tdesc.AppHandler = func(ctx context.Context, req *mockReq) (*mockResp, error) {\n\t\treturn nil, errs.B().Code(errs.Internal).Msg(\"test error\").Err()\n\t}\n\n\tw := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/path/hello\", strings.NewReader(`{\"Body\":\"test\"}`))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tps := api.UnnamedParams{\"hello\"}\n\n\tdesc.Handle(server.NewIncomingContext(w, req, ps, api.CallMeta{}))\n\n\t// Check that error status is returned\n\tif w.Code != 500 {\n\t\tt.Errorf(\"expected error status 500, got %d\", w.Code)\n\t}\n\n\t// Check that middleware headers are still applied\n\texpectedHeaders := map[string]string{\n\t\t\"X-Error-Header\":   \"error-value\",\n\t\t\"X-Always-Present\": \"always\",\n\t}\n\n\tfor expectedHeader, expectedValue := range expectedHeaders {\n\t\tgotValue := w.Header().Get(expectedHeader)\n\t\tif gotValue != expectedValue {\n\t\t\tt.Errorf(\"header %s: got %q, want %q\", expectedHeader, gotValue, expectedValue)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/middleware.go",
    "content": "package api\n\nimport \"encore.dev/middleware\"\n\ntype Middleware struct {\n\tID      string\n\tPkgName string\n\tName    string\n\tGlobal  bool\n\tDefLoc  uint32\n\tInvoke  middleware.Signature\n}\n\n// Validator is the interface implemented by types\n// that can validate incoming requests.\ntype Validator interface {\n\tValidate() error\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/pubsub_push_proxy.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/beta/errs\"\n)\n\nfunc (s *Server) createPubsubPushProxy(target config.Service) (*httputil.ReverseProxy, error) {\n\ttargetUrl, err := url.Parse(target.URL)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlogger := s.rootLogger.With().Str(\"remote_push_url\", target.URL).Logger()\n\n\treturn &httputil.ReverseProxy{\n\t\t// Rewrite the inbound request\n\t\tRewrite: func(req *httputil.ProxyRequest) {\n\t\t\treq.SetURL(targetUrl)\n\t\t\tt := transport.HTTPRequest(req.Out)\n\t\t\tmeta := CallMetaFromContext(req.In.Context())\n\t\t\tif err := meta.AddToRequest(s, target, t); err != nil {\n\t\t\t\tlogger.Err(err).Msg(\"failed to add call metadata to request\")\n\t\t\t}\n\t\t},\n\t\t// Have the reverse proxy log errors to our logger.\n\t\tErrorLog: newZeroLogAdapter(logger, zerolog.ErrorLevel),\n\t\t// Handle proxy errors using our error handler output\n\t\tErrorHandler: func(w http.ResponseWriter, req *http.Request, err error) {\n\t\t\tlogger.Err(err).Msg(\"error proxying request to service\")\n\t\t\terrs.HTTPError(w, errs.B().Cause(err).Code(errs.Unavailable).Err())\n\t\t},\n\t}, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/reflection.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"encore.dev/beta/errs\"\n)\n\nvar (\n\tvoidType    = reflect.TypeOf((*Void)(nil)).Elem()\n\tcontextType = reflect.TypeOf((*context.Context)(nil)).Elem()\n\terrorType   = reflect.TypeOf((*error)(nil)).Elem()\n)\n\ntype reflectedAPIMethod[Req any, Resp any] func(ctx context.Context, req Req) (Resp, error)\n\n// createReflectionCaller takes a reflected function which should match the signature of an API method which is\n// described by the Req and Resp types, and returns a function which can be used to call that function using\n// the provided request and response types.\n//\n// It will return an error if the function does not match the expected signature.\nfunc createReflectionCaller[Req any, Resp any](function reflect.Value) (reflectedAPIMethod[Req, Resp], error) {\n\t// Sanity check that the function is a function\n\tif function.Kind() != reflect.Func {\n\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected a function, got %s\", function.Kind()).Err()\n\t}\n\ttyp := function.Type()\n\n\t// Get the request type\n\trequestType := reflect.TypeOf((*Req)(nil)).Elem()\n\tif requestType.Kind() != reflect.Pointer {\n\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected request type to be a pointer, got %s\", requestType.Kind()).Err()\n\t}\n\trequestType = requestType.Elem()\n\tif requestType.Kind() != reflect.Struct {\n\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected request type to be a struct, got %s\", requestType.Kind()).Err()\n\t}\n\n\texpectedNumInParams := 1 + requestType.NumField()\n\n\t// Build up the list of parameters we expect in the order they should be passed to the function\n\texpectedParamTypes := make([]reflect.Type, 1, expectedNumInParams)\n\tparamFieldIndexes := make([]int, 1, expectedNumInParams)\n\n\t// All API's must always have a context as the first parameter\n\texpectedParamTypes[0] = contextType\n\tparamFieldIndexes[0] = -1 // -1 means it's not a field on the request struct\n\n\t// Now dynamically add the rest of the parameters both the payload and any path parameters\n\tvar payloadType reflect.Type\n\tvar payloadFieldIndex int\n\tfor i := 0; i < requestType.NumField(); i++ {\n\t\tif requestType.Field(i).Name == \"Payload\" {\n\t\t\tpayloadType = requestType.Field(i).Type\n\t\t\tpayloadFieldIndex = i\n\t\t} else {\n\t\t\texpectedParamTypes = append(expectedParamTypes, requestType.Field(i).Type)\n\t\t\tparamFieldIndexes = append(paramFieldIndexes, i)\n\t\t}\n\t}\n\tif payloadType != nil {\n\t\texpectedParamTypes = append(expectedParamTypes, payloadType)\n\t\tparamFieldIndexes = append(paramFieldIndexes, payloadFieldIndex)\n\t}\n\n\t// Check the number of parameters is correct, if not return an error\n\tnumInParams := typ.NumIn()\n\tif numInParams != expectedNumInParams {\n\t\texpectedParams := make([]string, expectedNumInParams)\n\t\texpectedParams[0] = \"context.Context\"\n\t\tfor i := 0; i < requestType.NumField(); i++ {\n\t\t\texpectedParams[i+1] = requestType.Field(i).Type.String()\n\t\t}\n\n\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected %d parameters (%s), got %d parameters\", expectedNumInParams, strings.Join(expectedParams, \", \"), numInParams).Err()\n\t}\n\n\t// Check all the parameters are of the correct type, if not return an error\n\tfor i, expected := range expectedParamTypes {\n\t\tactual := typ.In(i)\n\t\tif actual != expected {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected parameter %d to be %s, got %s\", i+1, expected, actual).Err()\n\t\t}\n\t}\n\n\t// If Resp is of type Void, then we expect 1 return value, otherwise 2\n\tresponseType := reflect.TypeOf((*Resp)(nil)).Elem()\n\tnumReturnValues := typ.NumOut()\n\tisVoidResponse := responseType == voidType\n\tvar errResponseIdx int\n\n\tif isVoidResponse {\n\t\terrResponseIdx = 0\n\n\t\tif numReturnValues != 1 {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected one return value (error), got %d return values\", numReturnValues).Err()\n\t\t}\n\n\t\tif typ.Out(0) != errorType {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected the return value to be an error, got %s\", typ.Out(0)).Err()\n\t\t}\n\t} else {\n\t\terrResponseIdx = 1\n\n\t\tif numReturnValues != 2 {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected two return values (%s, error), got %d return values\", responseType, numReturnValues).Err()\n\t\t}\n\n\t\tif typ.Out(0) != responseType {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected first return value to be %s, got %s\", responseType, typ.Out(0)).Err()\n\t\t}\n\n\t\tif typ.Out(1) != errorType {\n\t\t\treturn nil, errs.B().Code(errs.Internal).Msgf(\"expected second return value to be an error, got %s\", typ.Out(1)).Err()\n\t\t}\n\t}\n\n\treturn func(ctx context.Context, req Req) (respObj Resp, respErr error) {\n\t\tinParams := make([]reflect.Value, 1, expectedNumInParams)\n\t\tinParams[0] = reflect.ValueOf(ctx)\n\n\t\t// If we have parameters on the request, then we need to pass them to the function\n\t\treqValue := reflect.ValueOf(req)\n\t\tif !reqValue.IsNil() {\n\t\t\treqValue = reqValue.Elem() // deference the pointer that Encore always sets here\n\n\t\t\tfor i := 1; i < expectedNumInParams; i++ {\n\t\t\t\tinParams = append(inParams, reqValue.Field(paramFieldIndexes[i]))\n\t\t\t}\n\t\t} else {\n\t\t\t// If we don't have an `EncoreInteral_FoobarRequest` object, then we need to pass in the zero value for each\n\t\t\t// parameter - this shouldn't happen as all Encore generated API calls will always pass in a request object\n\t\t\tfor i := 1; i < expectedNumInParams; i++ {\n\t\t\t\tinParams = append(inParams, reflect.Zero(expectedParamTypes[i]))\n\t\t\t}\n\t\t}\n\n\t\toutParams := function.Call(inParams)\n\n\t\t// These two casts are safe because we've already checked the types above\n\t\trespParam := outParams[0]\n\t\tif !isVoidResponse && respParam.IsValid() && !respParam.IsZero() {\n\t\t\trespObj = respParam.Interface().(Resp)\n\t\t}\n\n\t\toutErr := outParams[errResponseIdx]\n\t\tif outErr.IsValid() && !outErr.IsZero() {\n\t\t\trespErr = outErr.Interface().(error)\n\t\t}\n\t\treturn\n\t}, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/reflection_test.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc Test_createReflectionCaller(t *testing.T) {\n\tc := qt.New(t)\n\tc.Parallel()\n\n\tc.Run(\"test parameter types are type checked\", func(c *qt.C) {\n\t\tc.Run(\"missing context param\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, Void](reflect.ValueOf(func() {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected 1 parameters \\\\(context.Context\\\\), got 0 parameters\")\n\t\t})\n\n\t\tc.Run(\"context param is wrong type\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, Void](reflect.ValueOf(func(int) {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected parameter 1 to be context.Context, got int\")\n\t\t})\n\n\t\tc.Run(\"missing parameter type\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreExampleParamsRequest, Void](reflect.ValueOf(func(context.Context) {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected 2 parameters \\\\(context.Context, \\\\*api.ExampleParams\\\\), got 1 parameters\")\n\t\t})\n\n\t\tc.Run(\"parameter type is wrong\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreExampleParamsRequest, Void](reflect.ValueOf(func(context.Context, int) {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected parameter 2 to be \\\\*api.ExampleParams, got int\")\n\t\t})\n\n\t\tc.Run(\"path params are type checks\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreExampleWithPathParams, Void](reflect.ValueOf(func(context.Context, *ExampleParams, string, int, []string) {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected parameter 2 to be string, got \\\\*api.ExampleParams\")\n\t\t})\n\t})\n\n\tc.Run(\"test response types are type checked\", func(c *qt.C) {\n\t\tc.Run(\"void api doesn't return error\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, Void](reflect.ValueOf(func(context.Context) {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected one return value \\\\(error\\\\), got 0 return values\")\n\t\t})\n\n\t\tc.Run(\"void api returns too many values\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, Void](reflect.ValueOf(func(context.Context) (error, Void) { return nil, Void{} }))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected one return value \\\\(error\\\\), got 2 return values\")\n\t\t})\n\n\t\tc.Run(\"api returns nothing\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, *ExampleResponse](reflect.ValueOf(func(context.Context) {}))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected two return values \\\\(\\\\*api.ExampleResponse, error\\\\), got 0 return values\")\n\t\t})\n\n\t\tc.Run(\"api response type is wrong\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, *ExampleResponse](reflect.ValueOf(func(context.Context) (int, error) { return 0, nil }))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected first return value to be \\\\*api.ExampleResponse, got int\")\n\t\t})\n\n\t\tc.Run(\"api error type is wrong\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, *ExampleResponse](reflect.ValueOf(func(context.Context) (*ExampleResponse, int) { return nil, 0 }))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected second return value to be an error, got int\")\n\t\t})\n\n\t\tc.Run(\"api returns too many values\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\t_, err := createReflectionCaller[*EncoreEmptyReq, *ExampleResponse](reflect.ValueOf(func(context.Context) (*ExampleResponse, error, int) { return nil, nil, 0 }))\n\t\t\tc.Assert(err, qt.ErrorMatches, \".*expected two return values \\\\(\\\\*api.ExampleResponse, error\\\\), got 3 return values\")\n\t\t})\n\t})\n\n\tc.Run(\"test calling the returned functions\", func(c *qt.C) {\n\t\tc.Run(\"basic api only returning an error\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\tcalled := false\n\t\t\tshouldError := false\n\n\t\t\tmethod, err := createReflectionCaller[*EncoreEmptyReq, Void](reflect.ValueOf(func(ctx context.Context) error {\n\t\t\t\tif shouldError {\n\t\t\t\t\treturn errors.New(\"test error\")\n\t\t\t\t}\n\t\t\t\tcalled = true\n\t\t\t\treturn nil\n\t\t\t}))\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"unable to create the reflection caller\"))\n\n\t\t\t// Test the method works and actually gets called\n\t\t\tresp, err := method(context.Background(), nil)\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"api should return no error\"))\n\t\t\tc.Assert(resp, qt.Equals, Void{}, qt.Commentf(\"api should return Void{}\"))\n\t\t\tc.Assert(called, qt.Equals, true, qt.Commentf(\"api should have been called\"))\n\n\t\t\t// Test the error is returned if the API errors\n\t\t\tshouldError = true\n\t\t\tresp, err = method(context.Background(), nil)\n\t\t\tc.Assert(err, qt.ErrorMatches, \"test error\", qt.Commentf(\"api should return an error now\"))\n\t\t})\n\n\t\tc.Run(\"api that takes parameters but returns nothing\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\t\t\tcalledWith := \"<not called>\"\n\n\t\t\tmethod, err := createReflectionCaller[*EncoreExampleParamsRequest, Void](reflect.ValueOf(func(ctx context.Context, params *ExampleParams) error {\n\t\t\t\tif params.Param2 == \"error\" {\n\t\t\t\t\treturn errors.New(params.Param1)\n\t\t\t\t}\n\n\t\t\t\tcalledWith = params.Param1\n\t\t\t\treturn nil\n\t\t\t}))\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"unable to create the reflection caller\"))\n\n\t\t\t// Test the method works and actually gets called\n\t\t\tresp, err := method(context.Background(), &EncoreExampleParamsRequest{\n\t\t\t\tPayload: &ExampleParams{\n\t\t\t\t\tParam1: \"this value\",\n\t\t\t\t\tParam2: \"ignored\",\n\t\t\t\t},\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.Equals, Void{}, qt.Commentf(\"api should return Void{}\"))\n\t\t\tc.Assert(calledWith, qt.Equals, \"this value\", qt.Commentf(\"api should have been called with the correct value\"))\n\n\t\t\t// Test the error is returned if the API errors\n\t\t\tresp, err = method(context.Background(), &EncoreExampleParamsRequest{\n\t\t\t\tPayload: &ExampleParams{\n\t\t\t\t\tParam1: \"my amazing error message\",\n\t\t\t\t\tParam2: \"error\",\n\t\t\t\t},\n\t\t\t})\n\t\t\tc.Assert(err, qt.ErrorMatches, \"my amazing error message\", qt.Commentf(\"api should return an error now\"))\n\t\t})\n\n\t\tc.Run(\"api that takes path parameters\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\n\t\t\tcalledWith := \"<not called>\"\n\t\t\tmethod, err := createReflectionCaller[*EncoreExampleWithPathParams, Void](reflect.ValueOf(func(ctx context.Context, name string, age int, tags []string, params *ExampleParams) error {\n\t\t\t\tcalledWith = fmt.Sprintf(\"%s - %s=%d %v\", params.Param1, name, age, tags)\n\t\t\t\treturn nil\n\t\t\t}))\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"unable to create the reflection caller\"))\n\n\t\t\t// Test the method works and actually gets called\n\t\t\tresp, err := method(context.Background(), &EncoreExampleWithPathParams{\n\t\t\t\tPayload: &ExampleParams{\n\t\t\t\t\tParam1: \"this value\",\n\t\t\t\t\tParam2: \"ignored\",\n\t\t\t\t},\n\t\t\t\tP0: \"first param\",\n\t\t\t\tP1: 1337,\n\t\t\t\tP2: []string{\"a\", \"b\", \"c\"},\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.Equals, Void{}, qt.Commentf(\"api should return Void{}\"))\n\t\t\tc.Assert(calledWith, qt.Equals, \"this value - first param=1337 [a b c]\", qt.Commentf(\"api should have been called with the correct value\"))\n\t\t})\n\n\t\tc.Run(\"api that returns a response\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\n\t\t\treturnNil := false\n\t\t\treturnErr := false\n\t\t\tmethod, err := createReflectionCaller[*EncoreEmptyReq, *ExampleResponse](reflect.ValueOf(func(ctx context.Context) (*ExampleResponse, error) {\n\t\t\t\tif returnErr {\n\t\t\t\t\treturn nil, errors.New(\"test error\")\n\t\t\t\t}\n\t\t\t\tif returnNil {\n\t\t\t\t\treturn nil, nil\n\t\t\t\t}\n\t\t\t\treturn &ExampleResponse{Value: \"hello\"}, nil\n\t\t\t}))\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"unable to create the reflection caller\"))\n\n\t\t\t// Test the method works and actually gets called\n\t\t\tresp, err := method(context.Background(), nil)\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.DeepEquals, &ExampleResponse{Value: \"hello\"}, qt.Commentf(\"api should return the correct response\"))\n\n\t\t\t// Test nil, nil is handled correctly\n\t\t\treturnNil = true\n\t\t\tresp, err = method(context.Background(), nil)\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.IsNil, qt.Commentf(\"api should return nil\"))\n\n\t\t\t// Test error is returned correctly\n\t\t\treturnErr = true\n\t\t\tresp, err = method(context.Background(), nil)\n\t\t\tc.Assert(err, qt.ErrorMatches, \"test error\", qt.Commentf(\"api should return an error\"))\n\t\t\tc.Assert(resp, qt.IsNil, qt.Commentf(\"api should return nil\"))\n\t\t})\n\n\t\tc.Run(\"test receiver methods work too\", func(c *qt.C) {\n\t\t\tc.Parallel()\n\n\t\t\tobj := &ExampleMockService{value: \"hello\", calledWith: \"<not called>\"}\n\t\t\tmethod, err := createReflectionCaller[*EncoreExampleParamsRequest, *ExampleResponse](reflect.ValueOf(obj.ExampleMethod))\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"unable to create the reflection caller\"))\n\n\t\t\t// Test the method works and actually gets called, and can reference the struct instance\n\t\t\tresp, err := method(context.Background(), &EncoreExampleParamsRequest{\n\t\t\t\tPayload: &ExampleParams{\n\t\t\t\t\tParam1: \"this value\",\n\t\t\t\t\tParam2: \"ignored\",\n\t\t\t\t},\n\t\t\t})\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.DeepEquals, &ExampleResponse{Value: \"hello\"}, qt.Commentf(\"api should return the correct response\"))\n\t\t\tc.Assert(obj.calledWith, qt.Equals, \"this value - ignored\", qt.Commentf(\"api should have been called with the correct value\"))\n\n\t\t\t// Test nil, nil is handled correctly\n\t\t\tresp, err = method(context.Background(), &EncoreExampleParamsRequest{Payload: &ExampleParams{\n\t\t\t\tParam1: \"foobar\",\n\t\t\t\tParam2: \"nil\",\n\t\t\t}})\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.IsNil, qt.Commentf(\"api should return nil\"))\n\t\t\tc.Assert(obj.calledWith, qt.Equals, \"foobar - nil\", qt.Commentf(\"api should have been called with the correct value\"))\n\n\t\t\t// Test error is returned correctly\n\t\t\tresp, err = method(context.Background(), &EncoreExampleParamsRequest{Payload: &ExampleParams{\n\t\t\t\tParam1: \"error\",\n\t\t\t\tParam2: \"test error value\",\n\t\t\t}})\n\t\t\tc.Assert(err, qt.ErrorMatches, \"test error value\", qt.Commentf(\"api should return an error\"))\n\t\t\tc.Assert(resp, qt.IsNil, qt.Commentf(\"api should return nil\"))\n\t\t\tc.Assert(obj.calledWith, qt.Equals, \"error - test error value\", qt.Commentf(\"api should have been called with the correct value\"))\n\n\t\t\t// Test nil request is handled correctly\n\t\t\tresp, err = method(context.Background(), &EncoreExampleParamsRequest{Payload: nil})\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.DeepEquals, &ExampleResponse{Value: \"you gave me nil!\"}, qt.Commentf(\"api should return the correct response\"))\n\t\t\tc.Assert(obj.calledWith, qt.Equals, \"got nil\", qt.Commentf(\"api should have been called with the correct value\"))\n\n\t\t\t// And finally test a nil Encore internal param is handled correctly\n\t\t\tobj.calledWith = \"<not called>\"\n\t\t\tresp, err = method(context.Background(), nil)\n\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"didn't expect an error form the api\"))\n\t\t\tc.Assert(resp, qt.DeepEquals, &ExampleResponse{Value: \"you gave me nil!\"}, qt.Commentf(\"api should return the correct response\"))\n\t\t\tc.Assert(obj.calledWith, qt.Equals, \"got nil\", qt.Commentf(\"api should have been called with the correct value\"))\n\t\t})\n\t})\n}\n\ntype ExampleMockService struct {\n\tvalue      string\n\tcalledWith string\n}\n\nfunc (e *ExampleMockService) ExampleMethod(_ context.Context, p *ExampleParams) (*ExampleResponse, error) {\n\tif p == nil {\n\t\te.calledWith = \"got nil\"\n\t\treturn &ExampleResponse{Value: \"you gave me nil!\"}, nil\n\t}\n\n\te.calledWith = fmt.Sprintf(\"%s - %s\", p.Param1, p.Param2)\n\n\tif p.Param1 == \"error\" {\n\t\treturn nil, errors.New(p.Param2)\n\t} else if p.Param2 == \"nil\" {\n\t\treturn nil, nil\n\t}\n\treturn &ExampleResponse{Value: e.value}, nil\n}\n\ntype EncoreEmptyReq struct{}\n\ntype EncoreExampleParamsRequest struct {\n\tPayload *ExampleParams\n}\n\n// Note: this is how Encore generates the request struct for an API\n//\n// The parameter type is always called `Payload` and placed first\n// then each path parameter is added in order after that\ntype EncoreExampleWithPathParams struct {\n\tPayload *ExampleParams\n\tP0      string\n\tP1      int\n\tP2      []string\n}\n\ntype ExampleParams struct {\n\tParam1 string\n\tParam2 string\n}\n\ntype ExampleResponse struct {\n\tValue string\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/registry.go",
    "content": "//go:build encore_app\n\npackage api\n\nimport \"reflect\"\n\nfunc RegisterEndpoint(handler Handler, function any) {\n\tSingleton.registerEndpoint(handler, function)\n}\n\nfunc RegisterAuthHandler(handler AuthHandler) {\n\tSingleton.setAuthHandler(handler)\n}\n\n// RegisterAuthDataType registers the type of the auth data that will be\n// returned by the auth handler. This is used to verify that the auth data\n// returned by the auth handler is of the correct type.\n//\n// Note type T is required to be a pointer type.\nfunc RegisterAuthDataType[T any]() {\n\tvar zero T\n\tRegisteredAuthDataType = reflect.TypeOf(zero)\n}\n\nfunc RegisterGlobalMiddleware(mw *Middleware) {\n\tSingleton.registerGlobalMiddleware(mw)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/reqtrack.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/beta/errs\"\n)\n\nfunc (s *Server) beginOperation() {\n\ts.rt.BeginOperation()\n}\n\nfunc (s *Server) finishOperation() {\n\ts.rt.FinishOperation()\n}\n\ntype beginRequestParams struct {\n\tType   model.RequestType\n\tDefLoc uint32\n\tData   *model.RPCData\n\n\t// TraceID is the trace ID to use.\n\t// If it is the zero value it will be copied from the parent request.\n\tTraceID model.TraceID\n\n\t// SpanID is the span ID to use.\n\t// If it is the zero value a new span id is generated.\n\tSpanID model.SpanID\n\n\t// ParentTraceID is the parent trace ID to use for correlation.\n\t// It is copied from the parent request if it is empty.\n\tParentTraceID model.TraceID\n\n\t// ParentSpanID is the parent's span ID to use for correlation.\n\t// It is copied from the parent request if it is empty.\n\tParentSpanID model.SpanID\n\n\t// ParentSampled indicates whether the parent span sampled trace information.\n\tParentSampled bool\n\n\t// CallerEventID is the event ID in the parent span that triggered this request.\n\t// It's used to correlate the request with the originating call.\n\tCallerEventID model.TraceEventID\n\n\t// ExtRequestID specifies the externally-provided request id, if any.\n\t// If not empty, it will be recorded as part of the \"starting request\" log message\n\t// to facilitate request correlation.\n\tExtRequestID string\n\n\t// ExtCorrelationID is the externally-provided correlation ID, if any.\n\t// If not empty, it will be recorded on each log message with \"correlation_id\" key.\n\t// to facilitate request correlation.\n\tExtCorrelationID string\n\n\t// AdditionalLogFields is a map of additional fields to be added to all the log message.\n\t// This is mainly used to add the trace identifiers to the log messages\n\t// so the clouds logging can correlate the logs with the trace.\n\tAdditionalLogFields map[string]string\n}\n\nfunc (s *Server) beginRequest(ctx context.Context, p *beginRequestParams) (*model.Request, error) {\n\ttraceID := p.TraceID\n\tif traceID.IsZero() {\n\t\tid, err := model.GenTraceID()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttraceID = id\n\t}\n\n\tspanID := p.SpanID\n\tif spanID.IsZero() {\n\t\tid, err := model.GenSpanID()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tspanID = id\n\t}\n\n\tvar traced bool\n\tif p.Data.FromEncorePlatform {\n\t\ttraced = true\n\t} else if p.ParentSpanID.IsZero() {\n\t\ttraced = s.rt.SampleTrace(p.Data.Desc.Service, p.Data.Desc.Endpoint)\n\t} else {\n\t\ttraced = p.ParentSampled\n\t}\n\n\treq := &model.Request{\n\t\tType:             p.Type,\n\t\tTraceID:          traceID,\n\t\tSpanID:           spanID,\n\t\tParentSpanID:     p.ParentSpanID,\n\t\tParentTraceID:    p.ParentTraceID,\n\t\tCallerEventID:    p.CallerEventID,\n\t\tExtCorrelationID: p.ExtCorrelationID,\n\t\tDefLoc:           p.DefLoc,\n\t\tSvcNum:           p.Data.Desc.SvcNum,\n\t\tStart:            s.clock.Now(),\n\t\tTraced:           traced,\n\t\tRPCData:          p.Data,\n\t}\n\n\tdata := req.RPCData\n\n\t// Update request data based on call options, if any\n\tif opts, _ := ctx.Value(callOptionsKey).(*CallOptions); opts != nil {\n\t\tif a := opts.Auth; a != nil {\n\t\t\tif err := CheckAuthData(a.UID, a.UserData); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid API call options: %v\", err)\n\t\t\t}\n\t\t\tdata.UserID = a.UID\n\t\t\tdata.AuthData = a.UserData\n\t\t}\n\t}\n\n\t// Begin the request, copying data over from the previous request.\n\ts.rt.BeginRequest(req)\n\tif curr := s.rt.Current(); curr.Trace != nil {\n\t\tswitch req.Type {\n\t\tcase model.RPCCall:\n\t\t\tcurr.Trace.RequestSpanStart(req, curr.Goctr)\n\t\tcase model.AuthHandler:\n\t\t\tcurr.Trace.AuthSpanStart(req, curr.Goctr)\n\t\tcase model.PubSubMessage:\n\t\t\tcurr.Trace.PubsubMessageSpanStart(req, curr.Goctr)\n\t\t}\n\t}\n\n\t// Now that we have up-to-date information in req (possibly copied from\n\t// the parent request), construct our logger.\n\tdesc := req.RPCData.Desc\n\tlogCtx := s.rootLogger.With().Str(\"service\", desc.Service).Str(\"endpoint\", desc.Endpoint)\n\tif data.UserID != \"\" {\n\t\tlogCtx = logCtx.Str(\"uid\", string(data.UserID))\n\t}\n\n\tif req.Test != nil {\n\t\tlogCtx = logCtx.Str(\"test\", req.Test.Current.Name())\n\t}\n\n\tif req.TraceID != (model.TraceID{}) {\n\t\tlogCtx = logCtx.Str(\"trace_id\", req.TraceID.String())\n\t}\n\n\tif req.ExtCorrelationID != \"\" {\n\t\tlogCtx = logCtx.Str(\"x_correlation_id\", req.ExtCorrelationID)\n\t} else if req.ParentTraceID != (model.TraceID{}) {\n\t\tlogCtx = logCtx.Str(\"x_correlation_id\", req.ParentTraceID.String())\n\t}\n\n\t// Add additional log fields, if any\n\tfor k, v := range p.AdditionalLogFields {\n\t\tlogCtx = logCtx.Str(k, v)\n\t}\n\n\treqLogger := logCtx.Logger()\n\treq.Logger = &reqLogger\n\n\tswitch req.Type {\n\tcase model.AuthHandler:\n\t\treq.Logger.Trace().Msg(\"running auth handler\")\n\tdefault:\n\t\tev := req.Logger.Trace()\n\t\tif p.ExtRequestID != \"\" {\n\t\t\tev = ev.Str(\"ext_request_id\", p.ExtRequestID)\n\t\t}\n\t\tev.Msg(\"starting request\")\n\t}\n\n\treturn req, nil\n}\n\nfunc (s *Server) finishRequest(resp *model.Response) {\n\tcurr := s.rt.Current()\n\treq := curr.Req\n\tif req == nil {\n\t\tpanic(\"encore: no current request running\")\n\t}\n\n\tif resp.Err != nil {\n\t\tswitch req.Type {\n\t\tcase model.AuthHandler:\n\t\t\treq.Logger.Error().Err(resp.Err).Msg(\"auth handler failed\")\n\t\tdefault:\n\t\t\te := errs.Convert(resp.Err).(*errs.Error)\n\t\t\tev := req.Logger.Error()\n\n\t\t\tvar panicStack *stack.Stack\n\t\t\tfor k, v := range e.Meta {\n\t\t\t\tif k == \"panic_stack\" {\n\t\t\t\t\tif st, ok := v.(stack.Stack); ok {\n\t\t\t\t\t\tpanicStack = &st\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tev = ev.Interface(k, v)\n\t\t\t}\n\n\t\t\tif panicStack != nil {\n\t\t\t\tev = ev.Interface(\"stack\", stack.Format(*panicStack))\n\t\t\t}\n\n\t\t\tev.Str(\"error\", e.ErrorMessage()).Str(\"code\", e.Code.String()).Msg(\"request failed\")\n\t\t}\n\t}\n\n\tresp.Duration = time.Since(req.Start)\n\tswitch req.Type {\n\tcase model.AuthHandler:\n\t\treq.Logger.Trace().Dur(\"duration\", resp.Duration).Msg(\"auth handler completed\")\n\tdefault:\n\t\tif resp.HTTPStatus != errs.HTTPStatus(resp.Err) {\n\t\t\tcode := errs.HTTPStatusToCode(resp.HTTPStatus).String()\n\t\t\treq.Logger.Trace().Dur(\"duration\", resp.Duration).Str(\"code\", code).Int(\"http_code\", resp.HTTPStatus).Msg(\"request completed\")\n\t\t} else {\n\t\t\tcode := errs.Code(resp.Err).String()\n\t\t\treq.Logger.Trace().Dur(\"duration\", resp.Duration).Str(\"code\", code).Msg(\"request completed\")\n\t\t}\n\t}\n\n\tif curr.Trace != nil {\n\t\t// Capture the recorded bytes from the request and response body, if any.\n\t\tif len(resp.RawRequestPayload) > 0 {\n\t\t\tcurr.Trace.BodyStream(trace2.BodyStreamParams{\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: req.TraceID,\n\t\t\t\t\tSpanID:  req.SpanID,\n\t\t\t\t},\n\t\t\t\tIsResponse: false,\n\t\t\t\tOverflowed: resp.RawRequestPayloadOverflowed,\n\t\t\t\tData:       resp.RawRequestPayload,\n\t\t\t})\n\t\t}\n\n\t\tif len(resp.RawResponsePayload) > 0 {\n\t\t\tcurr.Trace.BodyStream(trace2.BodyStreamParams{\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: req.TraceID,\n\t\t\t\t\tSpanID:  req.SpanID,\n\t\t\t\t},\n\t\t\t\tIsResponse: true,\n\t\t\t\tOverflowed: resp.RawResponsePayloadOverflowed,\n\t\t\t\tData:       resp.RawResponsePayload,\n\t\t\t})\n\t\t}\n\n\t\tep := trace2.EventParams{TraceID: req.TraceID, SpanID: req.SpanID}\n\t\tswitch req.Type {\n\t\tcase model.RPCCall:\n\t\t\tcurr.Trace.RequestSpanEnd(trace2.RequestSpanEndParams{\n\t\t\t\tEventParams:   ep,\n\t\t\t\tReq:           req,\n\t\t\t\tResp:          resp,\n\t\t\t\tCallerEventID: req.CallerEventID,\n\t\t\t})\n\t\tcase model.AuthHandler:\n\t\t\tcurr.Trace.AuthSpanEnd(trace2.AuthSpanEndParams{\n\t\t\t\tEventParams: ep,\n\t\t\t\tReq:         req,\n\t\t\t\tResp:        resp,\n\t\t\t})\n\t\tcase model.PubSubMessage:\n\t\t\tcurr.Trace.PubsubMessageSpanEnd(trace2.PubsubMessageSpanEndParams{\n\t\t\t\tEventParams: ep,\n\t\t\t\tReq:         req,\n\t\t\t\tResp:        resp,\n\t\t\t})\n\t\t}\n\t}\n\n\ts.requestsTotal.With(requestsTotalLabels{\n\t\tendpoint: req.RPCData.Desc.Endpoint,\n\t\tcode:     Code(resp.Err, resp.HTTPStatus),\n\t}).Increment()\n\ts.rt.FinishRequest(false)\n}\n\ntype CallOptions struct {\n\tAuth *model.AuthInfo\n}\n\ntype ctxKey string\n\nconst callOptionsKey ctxKey = \"call\"\n\nfunc WithCallOptions(ctx context.Context, opts *CallOptions) context.Context {\n\treturn context.WithValue(ctx, callOptionsKey, opts)\n}\n\nfunc GetCallOptions(ctx context.Context) *CallOptions {\n\tif opts, _ := ctx.Value(callOptionsKey).(*CallOptions); opts != nil {\n\t\treturn opts\n\t}\n\treturn &CallOptions{}\n}\n\n// RegisteredAuthDataType is the reflect type of the auth handler's data type.\n//\n// If no auth handler is configured, this is nil.\n// If an auth handler is configured, this is a pointer to the auth handler's data type.\nvar RegisteredAuthDataType reflect.Type\n\n// newAuthDataObj returns a new instance of the configured auth handler's data type.\n// If no auth handler is configured, nil is returned.\nfunc newAuthDataObj() any {\n\tif RegisteredAuthDataType == nil {\n\t\treturn nil\n\t}\n\treturn reflect.New(RegisteredAuthDataType.Elem()).Interface()\n}\n\n// CheckAuthData checks whether the given auth information is valid\n// based on the configured auth handler's data type.\nfunc CheckAuthData(uid model.UID, userData any) error {\n\tif uid == \"\" && userData != nil {\n\t\treturn fmt.Errorf(\"empty uid and non-empty auth data\")\n\t}\n\n\tif RegisteredAuthDataType != nil {\n\t\ttt := reflect.TypeOf(userData)\n\t\tif uid != \"\" && userData == nil {\n\t\t\treturn fmt.Errorf(\"missing auth data (auth handler specifies auth data of type %s)\", tt)\n\t\t} else if userData != nil {\n\t\t\tif tt != RegisteredAuthDataType {\n\t\t\t\treturn fmt.Errorf(\"wrong type for auth data (got %s, expected %s)\",\n\t\t\t\t\ttt, RegisteredAuthDataType)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif userData != nil {\n\t\t\treturn fmt.Errorf(\"unexpected auth data provided (auth handler specifies no auth data)\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Server) beginCall(ctx context.Context, serviceName, endpointName string, defLoc uint32) (*model.APICall, CallMeta, error) {\n\tcall := &model.APICall{\n\t\tTargetServiceName:  serviceName,\n\t\tTargetEndpointName: endpointName,\n\t\tDefLoc:             defLoc,\n\t}\n\n\tcurr := s.rt.Current()\n\tcall.Source = curr.Req\n\n\t// Add  auth data to the call, if any\n\tif curr.Req != nil && curr.Req.RPCData != nil {\n\t\tcall.UserID = curr.Req.RPCData.UserID\n\t\tcall.AuthData = curr.Req.RPCData.AuthData\n\t}\n\n\t// Update request data based on call options, if any\n\tif opts, _ := ctx.Value(callOptionsKey).(*CallOptions); opts != nil {\n\t\tif a := opts.Auth; a != nil {\n\t\t\tcall.UserID = a.UID\n\t\t\tcall.AuthData = a.UserData\n\t\t}\n\t}\n\n\tif curr.Trace != nil {\n\t\tcall.StartEventID = curr.Trace.RPCCallStart(call, curr.Goctr)\n\t}\n\n\tmeta, err := s.metaFromAPICall(call)\n\tif err != nil {\n\t\treturn nil, CallMeta{}, err\n\t}\n\n\treturn call, meta, nil\n}\n\nfunc (s *Server) finishCall(call *model.APICall, err error) {\n\tif curr := s.rt.Current(); curr.Trace != nil && call.StartEventID != 0 {\n\t\tcurr.Trace.RPCCallEnd(call, curr.Goctr, err)\n\t}\n}\n\nfunc (s *Server) beginAuth(defLoc uint32) (*model.AuthCall, error) {\n\tspanID, err := model.GenSpanID()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not generate request id: %v\", err)\n\t}\n\tcallID := atomic.AddUint64(&s.callCtr, 1)\n\n\tcall := &model.AuthCall{\n\t\tID:     callID,\n\t\tSpanID: spanID,\n\t\tDefLoc: defLoc,\n\t}\n\n\treturn call, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/server.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/benbjohnson/clock\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/julienschmidt/httprouter\"\n\t\"github.com/rs/zerolog\"\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/appruntime/apisdk/api/svcauth\"\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/apisdk/cors\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/shared/cfgutil\"\n\t\"encore.dev/appruntime/shared/cloudtrace\"\n\t\"encore.dev/appruntime/shared/health\"\n\t\"encore.dev/appruntime/shared/platform\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/internal/platformauth\"\n\t\"encore.dev/metrics\"\n\t\"encore.dev/pubsub\"\n)\n\ntype Access string\n\nconst (\n\tPublic       Access = \"public\"\n\tRequiresAuth Access = \"auth\"\n\tPrivate      Access = \"private\"\n)\n\nconst (\n\t// eventTraceStateEventIDKey is the key used to store the event ID in the trace state passed between instances\n\t// It is encoded as a base36 string of the underlying uint64.\n\teventTraceStateEventIDKey = \"encore/event-id\"\n\n\t// eventTraceStateSpanIDKey is the key used to store the span ID in the trace state passed between instances\n\t// We require this on GCP because the traceparent gets a new span ID inserted by GCP's own Trace implementation\n\t// (I suspect for the load balancers) and so the span ID in the traceparent is not the same as the span ID\n\t// need to create a child span.\n\teventTraceStateSpanIDKey = \"encore/span-id\"\n)\n\n// execContext contains the data needed for executing a request.\ntype execContext struct {\n\tserver *Server\n\tctx    context.Context\n\tps     UnnamedParams\n\tauth   model.AuthInfo\n\n\tcallMeta CallMeta\n}\n\ntype IncomingContext struct {\n\texecContext\n\tw   http.ResponseWriter\n\treq *http.Request\n\n\t// capturer is set in handleIncoming for raw requests\n\t// to capture the request body\n\tcapturer *rawRequestBodyCapturer\n}\n\ntype Handler interface {\n\tServiceName() string\n\tEndpointName() string\n\tAccessType() Access\n\tSemanticPath() string\n\tHTTPRouterPath() string\n\tHTTPMethods() []string\n\tIsFallback() bool\n\tHandle(c IncomingContext)\n}\n\ntype requestsTotalLabels struct {\n\tendpoint string // Endpoint name.\n\tcode     string // Human-readable HTTP status code.\n}\n\ntype Server struct {\n\tstatic         *config.Static\n\truntime        *config.Runtime\n\trt             *reqtrack.RequestTracker\n\tpc             *platform.Client // if nil, requests are not authenticated against platform\n\tencoreMgr      *encore.Manager\n\tpubsubMgr      *pubsub.Manager\n\trequestsTotal  *metrics.CounterGroup[requestsTotalLabels, uint64]\n\thttpClient     *http.Client\n\tclock          clock.Clock\n\trootLogger     zerolog.Logger\n\tjson           jsoniter.API\n\ttracingEnabled bool\n\texperiments    *experiments.Set // The set of experiments enabled for this runtime\n\n\tauthHandler AuthHandler\n\n\tglobalMiddleware    map[string]*Middleware\n\tregisteredHandlers  []Handler\n\tfunctionsToHandlers map[uintptr]Handler\n\n\tpublic           *httprouter.Router\n\tpublicFallback   *httprouter.Router\n\tprivate          *httprouter.Router\n\tprivateFallback  *httprouter.Router\n\tencore           *httprouter.Router\n\tinboundSvcAuth   map[string]svcauth.ServiceAuth // auth methods used to accept inbound service-to-service calls\n\toutboundSvcAuth  map[string]svcauth.ServiceAuth // auth methods used to make outbound service-to-service calls\n\thttpsrv          *http.Server\n\thttpCtx          context.Context\n\thttpCtxCancel    context.CancelFunc\n\trunningHandlers  sync.WaitGroup\n\tremotePubSubPush map[string]*httputil.ReverseProxy\n\n\tcallCtr uint64\n\n\tpubsubSubscriptions map[string]func(r *http.Request) error\n\thealthMgr           *health.CheckRegistry\n\ttestingMgr          *testsupport.Manager\n}\n\nfunc NewServer(static *config.Static, runtime *config.Runtime, rt *reqtrack.RequestTracker, pc *platform.Client, encoreMgr *encore.Manager, pubsubMgr *pubsub.Manager, rootLogger zerolog.Logger, reg *metrics.Registry, healthMgr *health.CheckRegistry, testingMgr *testsupport.Manager, json jsoniter.API, clock clock.Clock) *Server {\n\trequestsTotal := metrics.NewCounterGroupInternal[requestsTotalLabels, uint64](reg, \"e_requests_total\", metrics.CounterConfig{\n\t\tEncoreInternal_LabelMapper: func(labels requestsTotalLabels) []metrics.KeyValue {\n\t\t\treturn []metrics.KeyValue{\n\t\t\t\t{Key: \"endpoint\", Value: labels.endpoint},\n\t\t\t\t{Key: \"code\", Value: labels.code},\n\t\t\t}\n\t\t},\n\t})\n\n\tnewRouter := func() *httprouter.Router {\n\t\trouter := httprouter.New()\n\t\trouter.HandleOPTIONS = false\n\t\trouter.RedirectFixedPath = false\n\t\trouter.RedirectTrailingSlash = false\n\t\treturn router\n\t}\n\n\tinboundSvcAuth, outboundSvcAuth, err := svcauth.LoadMethods(clock, runtime)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"error loading service auth methods: %w\", err))\n\t}\n\n\ts := &Server{\n\t\tstatic:              static,\n\t\truntime:             runtime,\n\t\tpc:                  pc,\n\t\trt:                  rt,\n\t\tencoreMgr:           encoreMgr,\n\t\tpubsubMgr:           pubsubMgr,\n\t\thealthMgr:           healthMgr,\n\t\ttestingMgr:          testingMgr,\n\t\trequestsTotal:       requestsTotal,\n\t\thttpClient:          &http.Client{},\n\t\tclock:               clock,\n\t\trootLogger:          rootLogger,\n\t\tjson:                json,\n\t\ttracingEnabled:      rt.TracingEnabled(),\n\t\texperiments:         experiments.FromConfig(static, runtime),\n\t\tfunctionsToHandlers: make(map[uintptr]Handler),\n\n\t\tpublic:           newRouter(),\n\t\tpublicFallback:   newRouter(),\n\t\tprivate:          newRouter(),\n\t\tprivateFallback:  newRouter(),\n\t\tencore:           newRouter(),\n\t\tinboundSvcAuth:   inboundSvcAuth,\n\t\toutboundSvcAuth:  outboundSvcAuth,\n\t\tremotePubSubPush: make(map[string]*httputil.ReverseProxy),\n\t}\n\n\t// Create our HTTP server handler chain\n\n\t// Start with the underlying router\n\tvar baseHandler http.Handler = http.HandlerFunc(s.handler)\n\n\t// If we're acting as an API Gateway, then we need to add CORS support\n\tif s.IsGateway() {\n\t\tcorsCfg := &config.CORS{}\n\t\tif runtime.CORS != nil {\n\t\t\tcorsCfg = runtime.CORS\n\t\t}\n\t\tbaseHandler = cors.Wrap(\n\t\t\tcorsCfg,\n\t\t\tstatic.CORSAllowHeaders,\n\t\t\tstatic.CORSExposeHeaders,\n\t\t\tbaseHandler,\n\t\t\trootLogger,\n\t\t)\n\t}\n\n\t// Finally, this handler is used to track the number of running handlers\n\t// on the server so we can wait for them to finish before shutting down\n\t//\n\t// It must be the first handler in the chain to ensure the runningHandlers\n\t// count is always correct\n\tactiveHandlersWrapper := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif s.httpCtx.Err() != nil {\n\t\t\t// We are shutting down, return 503 with a retry-after header to tell clients to back off\n\t\t\t// we shouldn't ever see this as `httpCtx` is only cancelled after the server has already\n\t\t\t// started shutting down, but this is here as a safety net just in case\n\t\t\tw.Header().Set(\"Retry-After\", \"2\")\n\t\t\terrs.HTTPErrorWithCode(\n\t\t\t\tw,\n\t\t\t\terrs.B().Code(errs.Unavailable).Msg(\"server is shutting down\").Err(),\n\t\t\t\thttp.StatusServiceUnavailable,\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\n\t\t// Now we can call the next handler in the chain\n\t\ts.runningHandlers.Add(1)\n\t\tdefer s.runningHandlers.Done()\n\n\t\tbaseHandler.ServeHTTP(w, r)\n\t})\n\n\t// Now we have the handler chain setup, create the HTTP server object\n\ts.httpCtx, s.httpCtxCancel = context.WithCancel(context.Background())\n\ts.httpsrv = &http.Server{\n\t\tHandler: h2c.NewHandler(activeHandlersWrapper, &http2.Server{}),\n\t\tBaseContext: func(_ net.Listener) context.Context {\n\t\t\t// We set the base context which allows us to cancel it when the server is shutting down\n\t\t\treturn s.httpCtx\n\t\t},\n\t}\n\n\ts.configureRemotePubsubPush()\n\ts.registerEncoreRoutes()\n\n\treturn s\n}\n\n// configureRemotePubsubPush adds pubsub push handlers for push subscriptions that are not hosted by this service.\n// This is only done for gateway services.\nfunc (s *Server) configureRemotePubsubPush() {\n\tif !s.IsGateway() {\n\t\treturn\n\t}\n\tfor _, topic := range s.runtime.PubsubTopics {\n\t\tstatTop, ok := s.static.PubsubTopics[topic.EncoreName]\n\t\tif !ok {\n\t\t\tpanic(fmt.Errorf(\"runtime topic %s not found in static config\", topic.EncoreName))\n\t\t}\n\t\tfor _, sub := range topic.Subscriptions {\n\t\t\tstatSub, ok := statTop.Subscriptions[sub.EncoreName]\n\t\t\tif !ok {\n\t\t\t\tpanic(fmt.Errorf(\"runtime sub %s/%s not found in static config\", topic.EncoreName, sub.EncoreName))\n\t\t\t}\n\t\t\tif slices.Contains(s.runtime.HostedServices, statSub.Service) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tservice, found := s.runtime.ServiceDiscovery[statSub.Service]\n\t\t\tif !found {\n\t\t\t\tpanic(fmt.Errorf(\"service %q not found in service discovery, but needed for the remote push handler\", statSub.Service))\n\t\t\t}\n\t\t\tvar err error\n\t\t\ts.remotePubSubPush[sub.ID], err = s.createPubsubPushProxy(service)\n\t\t\tif err != nil {\n\t\t\t\tpanic(fmt.Errorf(\"error creating remote pubsub push proxy: %w\", err))\n\t\t\t}\n\t\t}\n\t}\n}\n\n// setAuthHandler sets the auth handler to use.\n// If h is nil it means no auth handler is used.\nfunc (s *Server) setAuthHandler(h AuthHandler) {\n\tauthService := h.HostedByService()\n\n\tif !cfgutil.IsHostedService(s.runtime, authService) {\n\t\tservice, found := s.runtime.ServiceDiscovery[authService]\n\t\tif !found {\n\t\t\tpanic(fmt.Errorf(\"service %q not found in service discovery, but needed for the auth handler\", authService))\n\t\t}\n\n\t\tauthURL := fmt.Sprintf(\"%s/__encore/authhandler\", service.URL)\n\n\t\ts.authHandler = &remoteAuthHandler{\n\t\t\tserver:         s,\n\t\t\thostingService: service,\n\t\t\tauthURL:        authURL,\n\t\t\toriginal:       h,\n\t\t\tlogger:         s.rootLogger.With().Str(\"auth_url\", authURL).Logger(),\n\t\t\ttraceLogs:      s.runtime.EnvCloud != \"local\", // log auth calls in prod containers only\n\t\t}\n\t} else {\n\t\ts.authHandler = h\n\t}\n}\n\nfunc (s *Server) RegisteredHandlers() []Handler {\n\treturn s.registeredHandlers\n}\n\n// wildcardMethod is an internal method name we register wildcard methods under.\nconst wildcardMethod = \"__ENCORE_WILDCARD__\"\n\nfunc (s *Server) registerEndpoint(h Handler, function any) {\n\trouterPath := h.HTTPRouterPath()\n\n\t// Decide which routers to use.\n\tprivate, public := s.private, s.public\n\tif h.IsFallback() {\n\t\tprivate, public = s.privateFallback, s.publicFallback\n\t}\n\n\tvar adapter httprouter.Handle\n\n\tswitch {\n\tcase cfgutil.IsHostedService(s.runtime, h.ServiceName()):\n\t\tadapter = s.createServiceHandlerAdapter(h)\n\n\tcase s.IsGateway():\n\t\tadapter = s.createGatewayHandlerAdapter(h)\n\n\tdefault:\n\t\t// not hosted do nothing\n\t\treturn\n\t}\n\n\ts.registeredHandlers = append(s.registeredHandlers, h)\n\n\t// Register the adapter\n\tfor _, m := range h.HTTPMethods() {\n\t\tif m == \"*\" {\n\t\t\tm = wildcardMethod\n\t\t}\n\n\t\tprivate.Handle(m, routerPath, adapter)\n\t\tif access := h.AccessType(); access == Public || access == RequiresAuth {\n\t\t\tpublic.Handle(m, routerPath, adapter)\n\t\t}\n\t}\n\n\t// Register the function mapped to the handler - this allows `et.MockEndpoint` to lookup the Handler\n\t// for a given function\n\tif s.static.Testing {\n\t\tif reflect.TypeOf(function).Kind() == reflect.Func {\n\t\t\ts.functionsToHandlers[reflect.ValueOf(function).Pointer()] = h\n\t\t} else {\n\t\t\ts.rootLogger.Warn().Str(\"service\", h.ServiceName()).Str(\"endpoint\", h.EndpointName()).Msgf(\"not registering function as lookup for API handler as it is not a function: %T\", function)\n\t\t}\n\t}\n}\n\n// HandlerForFunc returns the Handler for the given function or nil if it does not exist.\nfunc (s *Server) HandlerForFunc(function any) Handler {\n\treturn s.functionsToHandlers[reflect.ValueOf(function).Pointer()]\n}\n\n// ServiceExists returns true if the given service exists and has at least one endpoint.\nfunc (s *Server) ServiceExists(serviceName string) bool {\n\tfor _, h := range s.registeredHandlers {\n\t\tif strings.EqualFold(h.ServiceName(), serviceName) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (s *Server) registerGlobalMiddleware(mw *Middleware) {\n\tif s.globalMiddleware == nil {\n\t\ts.globalMiddleware = make(map[string]*Middleware)\n\t}\n\ts.globalMiddleware[mw.ID] = mw\n}\n\nfunc (s *Server) getGlobalMiddleware(ids []string) []*Middleware {\n\t// Don't add global middleware when tests are executing,\n\t// as it's not possible to guarantee all global middleware\n\t// have actually been imported when the tests run.\n\tif s.static.Testing {\n\t\treturn nil\n\t}\n\n\tresult := make([]*Middleware, 0, len(ids))\n\tfor _, id := range ids {\n\t\tmw, ok := s.globalMiddleware[id]\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"middleware %q not registered\", id))\n\t\t}\n\t\tresult = append(result, mw)\n\t}\n\treturn result\n}\n\nfunc (s *Server) Serve(ln net.Listener) error {\n\tif s.runtime.EnvCloud != \"local\" || s.IsGateway() {\n\t\ts.rootLogger.Trace().Msg(\"listening for incoming HTTP requests\")\n\t}\n\treturn s.httpsrv.Serve(ln)\n}\n\n// Shutdown gracefully shuts down the server.\nfunc (s *Server) Shutdown(p *shutdown.Process) error {\n\t// Once it's time to force-close tasks, cancel the base context.\n\tgo func() {\n\t\t<-p.ForceCloseTasks.Done()\n\t\ts.httpCtxCancel()\n\t}()\n\n\t// Begin shutting down the server\n\tshutdownErr := make(chan error, 1)\n\tgo func() { shutdownErr <- s.httpsrv.Shutdown(p.ForceShutdown) }()\n\n\t// Wait for the running handlers to finish.\n\ts.runningHandlers.Wait()\n\tp.MarkOutstandingRequestsCompleted()\n\n\treturn <-shutdownErr\n}\n\nfunc (s *Server) handler(w http.ResponseWriter, req *http.Request) {\n\t// Select a router based on access\n\trouter, fallbackRouter := s.public, s.publicFallback\n\n\t// The Encore platform is authorised to call private APIs directly, thus if we have this header set,\n\t// and authenticate it, then we can switch over to the private router which contains all APIs not just\n\t// the publicly accessible ones.\n\tif sig := req.Header.Get(\"X-Encore-Auth\"); sig != \"\" && s.pc != nil {\n\t\tif ok, err := s.pc.ValidatePlatformRequest(req, sig); err == nil && ok {\n\t\t\t// Successfully authenticated\n\t\t\treq = req.WithContext(platformauth.WithEncorePlatformSealOfApproval(req.Context()))\n\t\t\trouter, fallbackRouter = s.private, s.privateFallback\n\t\t} else if err != nil {\n\t\t\thttp.Error(w, \"could not authenticate request\", http.StatusBadGateway)\n\t\t\treturn\n\t\t} else {\n\t\t\thttp.Error(w, \"invalid request signature\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Extract the call meta from the request\n\treq, internalCaller, ok := s.extractCallMeta(w, req)\n\tif !ok {\n\t\t// extractCallMeta has already written the response\n\t\treturn\n\t}\n\tif internalCaller != nil && internalCaller.PrivateAPIAccess() {\n\t\t// If this request is from another service running in this app, allow it access to the private API routes\n\t\trouter, fallbackRouter = s.private, s.privateFallback\n\t}\n\n\tpath := determineRequestPath(req.URL)\n\n\t// Switch to the Encore internal router if we are on the Encore internal path\n\tconst internalPrefix = \"/__encore\"\n\tif strings.HasPrefix(path, internalPrefix+\"/\") {\n\t\trouter, fallbackRouter = s.encore, nil\n\t\tpath = path[len(internalPrefix):] // keep leading slash\n\t}\n\n\tfindRoute := func(r *httprouter.Router) (h httprouter.Handle, p httprouter.Params, handledTSR bool) {\n\t\th, p, _ = r.Lookup(req.Method, path)\n\t\tif h == nil {\n\t\t\th, p, _ = r.Lookup(wildcardMethod, path)\n\t\t}\n\n\t\tif h == nil {\n\t\t\t// If we still couldn't find a handler, check if there is one\n\t\t\t// with a trailing slash redirect.\n\t\t\tif handleTrailingSlashRedirect(r, w, req, path) {\n\t\t\t\treturn nil, nil, true\n\t\t\t}\n\t\t}\n\n\t\treturn h, p, false\n\t}\n\n\t// Find the route. Try first with the chosen router and otherwise check the fallback router.\n\th, p, handled := findRoute(router)\n\tif !handled && h == nil && fallbackRouter != nil {\n\t\th, p, handled = findRoute(fallbackRouter)\n\t}\n\n\t// If the router already handled the request via a trailing-slash redirect, we're done.\n\tif handled {\n\t\treturn\n\t}\n\n\tif h != nil {\n\t\t// Found an endpoint.\n\t\th(w, req, p)\n\t\treturn\n\t}\n\n\t// Endpoint not found\n\ts.rootLogger.Trace().Str(\"path\", path).Bool(\"gateway\", s.IsGateway()).Strs(\"hosting\", s.runtime.HostedServices).Msg(\"endpoint not found\")\n\terrs.HTTPError(w, errs.B().Code(errs.NotFound).Msg(\"endpoint not found\").Err())\n}\n\nfunc (s *Server) extractCallMeta(w http.ResponseWriter, req *http.Request) (updatedReq *http.Request, internalCaller Caller, ok bool) {\n\t// Extract the metadata from the request so we can allow access to the private router.\n\t// If the metadata is not present, then we assume this is a public request.\n\tmeta, err := s.MetaFromRequest(transport.HTTPRequest(req))\n\tif err != nil {\n\t\ts.rootLogger.Error().Err(err).Msg(\"failed to extract metadata from request\")\n\t\thttp.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)\n\t\treturn nil, nil, false\n\t}\n\n\t// Extract any cloud generated Trace identifiers from the request.\n\t// and use them if we don't have any trace information in the metadata already\n\tcloudGeneratedTraceIDs := cloudtrace.ExtractCloudTraceIDs(s.rootLogger, req)\n\tif meta.TraceID.IsZero() {\n\t\tmeta.TraceID = cloudGeneratedTraceIDs.TraceID\n\t}\n\n\t// SpanID will be zero already, so if our Cloud generated one for us, we should\n\t// use it as the SpanID for this request\n\tmeta.SpanID = cloudGeneratedTraceIDs.SpanID\n\n\t// If we still don't have a trace id, generate one.\n\tif meta.TraceID.IsZero() {\n\t\tmeta.TraceID, _ = model.GenTraceID()\n\t\tmeta.ParentSpanID = model.SpanID{} // no parent span if we have no trace id\n\t}\n\n\tvar caller Caller\n\tif meta.Internal != nil {\n\t\tcaller = meta.Internal.Caller\n\t}\n\n\treturn req.WithContext(SetCallMetaInContext(req.Context(), meta)), caller, true\n}\n\nfunc (s *Server) newExecContext(ctx context.Context, ps UnnamedParams, callMeta CallMeta) execContext {\n\tvar auth model.AuthInfo\n\tif callMeta.Internal != nil {\n\t\tauth = model.AuthInfo{\n\t\t\tUID:      model.UID(callMeta.Internal.AuthUID),\n\t\t\tUserData: callMeta.Internal.AuthData,\n\t\t}\n\t}\n\treturn execContext{s, ctx, ps, auth, callMeta}\n}\n\nfunc (s *Server) NewIncomingContext(w http.ResponseWriter, req *http.Request, ps UnnamedParams, callMeta CallMeta) IncomingContext {\n\tec := s.newExecContext(req.Context(), ps, callMeta)\n\treturn IncomingContext{ec, w, req, nil}\n}\n\nfunc (s *Server) NewCallContext(ctx context.Context) CallContext {\n\treturn CallContext{ctx, s}\n}\n\nfunc toUnnamedParams(ps httprouter.Params) UnnamedParams {\n\tparams := make(UnnamedParams, len(ps))\n\tfor i, p := range ps {\n\t\tparams[i] = p.Value\n\t}\n\treturn params\n}\n\n// handleTrailingSlashRedirect checks if there's a matching handler\n// with (without) a trailing slash and redirects to it if there is.\n//\n// This is a modified version of the built-in support in httprouter.\n// We can't use the built-in one due to how we handle multiple methods\n// for the same route using Lookup instead of Handle.\nfunc handleTrailingSlashRedirect(r *httprouter.Router, w http.ResponseWriter, req *http.Request, path string) (handled bool) {\n\t// CONNECT does not support redirects.\n\tif req.Method == http.MethodConnect {\n\t\treturn false\n\t}\n\n\t// Modify the path to include (exclude) the trailing slash.\n\tif len(path) > 1 && path[len(path)-1] == '/' {\n\t\tpath = path[:len(path)-1]\n\t} else {\n\t\tpath += \"/\"\n\t}\n\n\t// Check if there is a handler for the modified path.\n\th, _, _ := r.Lookup(req.Method, path)\n\tif h == nil {\n\t\th, _, _ = r.Lookup(wildcardMethod, path)\n\t}\n\n\tif h == nil {\n\t\t// Couldn't find a handler.\n\t\treturn false\n\t}\n\n\t// Moved Permanently, request with GET method\n\tcode := http.StatusMovedPermanently\n\tif req.Method != http.MethodGet {\n\t\t// Permanent Redirect, request with same method\n\t\tcode = http.StatusPermanentRedirect\n\t}\n\n\thttp.Redirect(w, req, path, code)\n\treturn true\n}\n\n// determineRequestPath determines the path to use for routing\n// based on the incoming request URL u.\nfunc determineRequestPath(u *url.URL) string {\n\t// To support use cases like routing \"/foo%2Fbar/baz\" to \"/:a/*b\" as a = \"foo/bar\", b = \"baz\"\n\t// we need to be careful about the escaping.\n\t//\n\t// The way the net/url package works is a bit subtle, but URL.RawPath is non-empty if and only if\n\t// the default encoding of Path differs from the incoming request.\n\t// However, we don't want to always use RawPath (or EscapedPath(), in practice) because\n\t// it over-escapes: it turns '{foo}' into '%7Bfoo%7D' which we don't want.\n\t//\n\t// So, use req.URL.Path when possible, and only use EscapedPath() when necessary.\n\tpath := u.Path\n\tif u.RawPath != \"\" {\n\t\tpath = u.EscapedPath()\n\t}\n\treturn path\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/server_test.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/julienschmidt/httprouter\"\n)\n\nfunc Test_handleTrailingSlashRedirect(t *testing.T) {\n\tr := httprouter.New()\n\tdummy := func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {}\n\tr.GET(\"/foo\", dummy)\n\tr.GET(\"/bar/\", dummy)\n\tr.POST(\"/post\", dummy)\n\n\ttests := []struct {\n\t\t// inputs\n\t\tmethod, path string\n\t\t// outputs\n\t\thandled bool\n\t\tcode    int\n\t\tdest    string\n\t}{\n\t\t// Matches existing routes\n\t\t{\"GET\", \"/foo\", false, 0, \"\"},\n\t\t{\"GET\", \"/bar/\", false, 0, \"\"},\n\n\t\t// Redirect to with (without) trailing slash\n\t\t{\"GET\", \"/foo/\", true, http.StatusMovedPermanently, \"/foo\"},\n\t\t{\"GET\", \"/bar\", true, http.StatusMovedPermanently, \"/bar/\"},\n\t\t{\"POST\", \"/post/\", true, http.StatusPermanentRedirect, \"/post\"},\n\n\t\t// Unknown routes\n\t\t{\"GET\", \"/baz\", false, 0, \"\"},\n\t\t{\"GET\", \"/baz/\", false, 0, \"\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tw := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(test.method, test.path, nil)\n\t\thandled := handleTrailingSlashRedirect(r, w, req, test.path)\n\t\tif !handled && !test.handled {\n\t\t\tcontinue\n\t\t} else if handled != test.handled {\n\t\t\tt.Errorf(\"%s %s: got handled=%v, want %v\", test.method, test.path, handled, test.handled)\n\t\t\tcontinue\n\t\t}\n\n\t\tif w.Code != test.code {\n\t\t\tt.Errorf(\"%s %s: got code=%d, want %d\", test.method, test.path, w.Code, test.code)\n\t\t} else if w.Header().Get(\"Location\") != test.dest {\n\t\t\tt.Errorf(\"%s %s: got dest=%s, want %s\", test.method, test.path, w.Header().Get(\"Location\"), test.dest)\n\t\t}\n\t}\n}\n\nfunc Test_determineRequestPath(t *testing.T) {\n\ttests := []struct {\n\t\tpath    string\n\t\trawPath string\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tpath:    \"/{foo}\",\n\t\t\trawPath: \"\",\n\t\t\twant:    \"/{foo}\",\n\t\t},\n\t\t{\n\t\t\tpath:    \"/foo/bar/baz\",\n\t\t\trawPath: \"/foo/bar%2Fbaz\",\n\t\t\twant:    \"/foo/bar%2Fbaz\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tu := &url.URL{Path: tt.path, RawPath: tt.rawPath}\n\t\tif got := determineRequestPath(u); got != tt.want {\n\t\t\tt.Errorf(\"determineRequestPath(&url.URL{Path: %q, RawPath: %q}) = %q, want %q\",\n\t\t\t\ttt.path, tt.rawPath, got, tt.want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/services.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/julienschmidt/httprouter\"\n)\n\nfunc (s *Server) createServiceHandlerAdapter(h Handler) httprouter.Handle {\n\treturn func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {\n\t\t// Delete the header so it can't be accessed.\n\t\treq.Header.Del(\"X-Encore-Auth\")\n\n\t\tparams := toUnnamedParams(ps)\n\n\t\t// Extract metadata from the request.\n\t\tmeta := CallMetaFromContext(req.Context())\n\n\t\t// Always send the trace id back.\n\t\ttraceIDStr := meta.TraceID.String()\n\t\tw.Header().Set(\"X-Encore-Trace-ID\", traceIDStr)\n\n\t\t// Echo the X-Request-ID back to the caller if present,\n\t\t// otherwise send back the trace id.\n\t\treqID := req.Header.Get(\"X-Request-ID\")\n\t\tif reqID == \"\" {\n\t\t\treqID = traceIDStr\n\t\t} else if len(reqID) > 64 {\n\t\t\t// Don't allow arbitrarily long request IDs.\n\t\t\ts.rootLogger.Warn().Int(\"length\", len(reqID)).Msg(\"X-Request-ID was too long and is being truncated to 64 characters\")\n\t\t\treqID = reqID[:64]\n\t\t}\n\t\tw.Header().Set(\"X-Request-ID\", reqID)\n\n\t\t// Read the correlation ID from the request.\n\t\tif meta.CorrelationID != \"\" {\n\t\t\tw.Header().Set(\"X-Correlation-ID\", meta.CorrelationID)\n\t\t}\n\n\t\ts.processRequest(h, s.NewIncomingContext(w, req, params, meta))\n\t}\n}\n\nfunc (s *Server) processRequest(h Handler, c IncomingContext) {\n\tc.server.beginOperation()\n\tdefer c.server.finishOperation()\n\n\tinfo, proceed := s.runAuthHandler(h, c)\n\tif proceed {\n\t\tc.auth = info\n\t\th.Handle(c)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/singleton.go",
    "content": "//go:build encore_app\n\npackage api\n\nimport (\n\t\"github.com/benbjohnson/clock\"\n\n\tencore \"encore.dev\"\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/health\"\n\t\"encore.dev/appruntime/shared/jsonapi\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/platform\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/metrics\"\n\t\"encore.dev/pubsub\"\n)\n\nvar Singleton = NewServer(\n\tappconf.Static, appconf.Runtime, reqtrack.Singleton, platform.Singleton,\n\tencore.Singleton, pubsub.Singleton, logging.RootLogger, metrics.Singleton,\n\thealth.Singleton, testsupport.Singleton,\n\tjsonapi.Default, clock.New(),\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/svcauth/doc.go",
    "content": "// Package svcauth provides various authentication mechanisms for Encore services\n// to use verify the identity of incoming requests from other Encore services within\n// the same application.\npackage svcauth\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/svcauth/encoreauth.go",
    "content": "package svcauth\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/benbjohnson/clock\"\n\t\"go.encore.dev/platform-sdk/pkg/auth\"\n\t\"golang.org/x/crypto/sha3\"\n\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/beta/errs\"\n)\n\nconst ecAuthHashHeader = \"Svc-Auth\"\nconst ecDateHeader = \"Date\"\n\n// encoreAuth is a ServiceAuth implementation that uses the Encore auth package to sign requests.\ntype encoreAuth struct {\n\tappSlug   string\n\tenvName   string\n\tkeys      []auth.Key\n\tlatestKey auth.Key\n\tclock     clock.Clock\n}\n\nfunc newEncoreAuth(clock clock.Clock, appSlug string, envName string, keys []config.EncoreAuthKey) ServiceAuth {\n\tvar keySet []auth.Key\n\tvar latestKey auth.Key\n\tfor _, key := range keys {\n\t\tkeySet = append(keySet, auth.Key{\n\t\t\tKeyID: key.KeyID,\n\t\t\tData:  key.Data,\n\t\t})\n\t\tif latestKey.KeyID < key.KeyID {\n\t\t\tlatestKey = auth.Key{\n\t\t\t\tKeyID: key.KeyID,\n\t\t\t\tData:  key.Data,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &encoreAuth{\n\t\tappSlug:   appSlug,\n\t\tenvName:   envName,\n\t\tkeys:      keySet,\n\t\tlatestKey: latestKey,\n\t\tclock:     clock,\n\t}\n}\n\nfunc (ea *encoreAuth) method() string {\n\treturn \"encore-auth\"\n}\n\nfunc (ea *encoreAuth) verify(req transport.Transport) error {\n\theaders := &auth.Headers{}\n\tif authStr, found := req.ReadMeta(ecAuthHashHeader); !found {\n\t\treturn auth.ErrNoAuthorizationHeader\n\t} else {\n\t\theaders.Authorization = authStr\n\t}\n\tif dateStr, found := req.ReadMeta(ecDateHeader); !found {\n\t\treturn auth.ErrNoDateHeader\n\t} else {\n\t\theaders.Date = dateStr\n\t}\n\n\tkeyID, appSlug, envName, timestamp, opHash, err := headers.SigningComponents()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// First the timestamp, and don't do any work if it's too old or too new\n\tconst allowedClockSkew = 2 * time.Minute\n\tif diff := ea.clock.Since(timestamp); diff > allowedClockSkew || diff < -allowedClockSkew {\n\t\treturn auth.ErrAuthenticationExpired\n\t}\n\n\t// Find the key\n\tvar key auth.Key\n\tfor _, k := range ea.keys {\n\t\tif k.KeyID == keyID {\n\t\t\tkey = k\n\t\t\tbreak\n\t\t}\n\t}\n\tif key.KeyID == 0 {\n\t\treturn auth.ErrAuthenticationFailed\n\t}\n\n\t// Rebuild the signature\n\texpectedHeaders := auth.SignForVerification(&key, appSlug, envName, timestamp, opHash)\n\n\t// Verify the signature\n\tif !expectedHeaders.Equal(headers) {\n\t\treturn auth.ErrAuthenticationFailed\n\t}\n\n\t// Now we're verified the signature - now let's compare the OpHash received\n\t// against the OpHash we would have generated for this request.\n\t// We do this here to minimize the risk of timing attacks.\n\texpectedOpHash, err := ea.buildOpHash(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif expectedOpHash != opHash {\n\t\treturn auth.ErrAuthenticationFailed\n\t}\n\n\treturn nil\n}\n\nfunc (ea *encoreAuth) sign(req transport.Transport) error {\n\topHash, err := ea.buildOpHash(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\theaders := auth.Sign(&ea.latestKey, ea.appSlug, ea.envName, ea.clock, opHash)\n\n\treq.SetMeta(ecAuthHashHeader, headers.Authorization)\n\treq.SetMeta(ecDateHeader, headers.Date)\n\treturn nil\n}\n\n// buildOpHash builds the operation hash for the request.\nfunc (ea *encoreAuth) buildOpHash(req transport.Transport) (auth.OperationHash, error) {\n\t// Build a deterministic hash of the meta keys and values\n\thash := sha3.New256()\n\tfor _, key := range req.ListMetaKeys() {\n\t\tswitch key {\n\t\tcase AuthMethodMetaKey, ecAuthHashHeader, ecDateHeader:\n\t\t\t// Skip these headers, as they are part of the auth mechanism itself\n\n\t\tcase transport.TraceParentKey, transport.TraceStateKey:\n\t\t\t// Skip these headers, as they are part of the tracing mechanism and could be changed\n\t\t\t// by things like load balancers\n\n\t\tdefault:\n\t\t\t// Read all values for this key, and sort them\n\t\t\tvalues, found := req.ReadMetaValues(key)\n\t\t\tif !found {\n\t\t\t\treturn \"\", errs.B().Code(errs.Internal).Msg(\"failed to read metadata value\").Err()\n\t\t\t}\n\t\t\tsort.Strings(values)\n\n\t\t\tfor _, value := range values {\n\t\t\t\tif _, err := fmt.Fprintf(hash, \"%s=%s\\n\", key, value); err != nil {\n\t\t\t\t\treturn \"\", errs.B().Code(errs.Internal).Cause(err).Msg(\"failed to write to hash\").Err()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate the operation hash\n\topHash, err := auth.NewOperationHash(\"internal-api\", \"call\", auth.BytesPayload(hash.Sum(nil)))\n\tif err != nil {\n\t\treturn \"\", errs.B().Code(errs.Internal).Cause(err).Msg(\"failed to create operation hash for internal API call\").Err()\n\t}\n\treturn opHash, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/svcauth/noop.go",
    "content": "package svcauth\n\nimport (\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n)\n\n// noop is a ServiceAuth implementation that does not perform any authentication.\n//\n// It is intended to be used for local development or where services are running within their own\n// private network and there is no threat model resulting in the need to authenticate requests.\ntype noop struct{}\n\nvar Noop noop\n\nvar _ ServiceAuth = noop{}\n\nfunc (n noop) method() string {\n\treturn \"noop\"\n}\n\nfunc (n noop) verify(transport.Transport) error {\n\treturn nil\n}\n\nfunc (n noop) sign(transport.Transport) error {\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/svcauth/pkgfn.go",
    "content": "package svcauth\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/benbjohnson/clock\"\n\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n\t\"encore.dev/appruntime/exported/config\"\n)\n\nconst (\n\tAuthMethodMetaKey = \"Svc-Auth-Method\"\n)\n\n// Sign signs the request using the given authentication method.\nfunc Sign(method ServiceAuth, req transport.Transport) error {\n\tif err := method.sign(req); err != nil {\n\t\treturn fmt.Errorf(\"failed to sign request: %w\", err)\n\t}\n\treq.SetMeta(AuthMethodMetaKey, method.method())\n\n\treturn nil\n}\n\n// Verify verifies the authenticity of the request using the given authentication methods.\nfunc Verify(req transport.Transport, loadedAuthMethods map[string]ServiceAuth) (internalCall bool, err error) {\n\tmethod, found := req.ReadMeta(AuthMethodMetaKey)\n\tif !found {\n\t\t// If this is not set, it means that the request is not an internal service to service call.\n\t\treturn false, nil\n\t}\n\n\tfor _, authMethod := range loadedAuthMethods {\n\t\tif authMethod.method() == method {\n\t\t\tif err := authMethod.verify(req); err != nil {\n\t\t\t\treturn false, fmt.Errorf(\"failed to verify request: %w\", err)\n\t\t\t}\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, fmt.Errorf(\"unknown service to service authentication method: %s\", method)\n}\n\n// LoadMethods loads the service to service authentication methods from the given config.\nfunc LoadMethods(clock clock.Clock, cfg *config.Runtime) (inbound, outbound map[string]ServiceAuth, err error) {\n\tinbound = make(map[string]ServiceAuth)\n\toutbound = make(map[string]ServiceAuth)\n\n\tload := func(authCfg config.ServiceAuth) (ServiceAuth, error) {\n\t\tswitch authCfg.Method {\n\t\tcase \"noop\":\n\t\t\treturn &noop{}, nil\n\t\tcase \"encore-auth\":\n\t\t\treturn newEncoreAuth(clock, cfg.AppSlug, cfg.EnvName, cfg.AuthKeys), nil\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unknown service to service authentication method: %s\", authCfg.Method)\n\t\t}\n\t}\n\n\t// Load all the inbound auth methods.\n\tfor _, authCfg := range cfg.ServiceAuth {\n\t\tinbound[authCfg.Method], err = load(authCfg)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\t// Load all the outbound auth methods.\n\tfor _, svc := range cfg.ServiceDiscovery {\n\t\tif _, found := outbound[svc.ServiceAuth.Method]; !found {\n\t\t\toutbound[svc.ServiceAuth.Method], err = load(svc.ServiceAuth)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn inbound, outbound, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/svcauth/svcauth.go",
    "content": "package svcauth\n\nimport (\n\t\"encore.dev/appruntime/apisdk/api/transport\"\n)\n\n// ServiceAuth is an interface that provides authentication for internal service to service\n// calls within the same Encore application.\ntype ServiceAuth interface {\n\t// Method returns the name of the authentication method.\n\tmethod() string\n\n\t// Verify verifies the authenticity of the request.\n\t// If the request is not authentic, an error is returned.\n\tverify(req transport.Transport) error\n\n\t// Sign signs the request.\n\t// If the request cannot be signed, an error is returned.\n\tsign(req transport.Transport) error\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/transport/doc.go",
    "content": "// Package transport provides an abstraction over the transport layer\n// which allows us to add and read metadata from the transport without\n// having to know the underlying transport implementation.\npackage transport\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/transport/eh2c.go",
    "content": "package transport\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"golang.org/x/net/http2\"\n)\n\nfunc NewH2CTransport(defaultTransport http.RoundTripper) http.RoundTripper {\n\treturn &H2CTransport{\n\t\th2c: &http2.Transport{\n\t\t\tAllowHTTP: true,\n\t\t\tDialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {\n\t\t\t\tvar d net.Dialer\n\t\t\t\treturn d.DialContext(ctx, network, addr)\n\t\t\t},\n\t\t},\n\t\tdef: defaultTransport,\n\t}\n}\n\ntype H2CTransport struct {\n\th2c http.RoundTripper\n\tdef http.RoundTripper\n}\n\nfunc (h *H2CTransport) RoundTrip(request *http.Request) (*http.Response, error) {\n\tif request.URL.Scheme == \"http\" && request.ProtoMajor == 2 {\n\t\treturn h.h2c.RoundTrip(request)\n\t}\n\treturn h.def.RoundTrip(request)\n}\n\nvar _ http.RoundTripper = (*H2CTransport)(nil)\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/transport/http.go",
    "content": "package transport\n\nimport (\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// HTTPRequest returns a Transport implementation for the given HTTP request.\nfunc HTTPRequest(req *http.Request) Transport {\n\treturn &httpHeaders{headers: req.Header}\n}\n\n// HTTPResponse returns a Transport implementation for the given HTTP response.\nfunc HTTPResponse(resp *http.Response) Transport {\n\treturn &httpHeaders{headers: resp.Header}\n}\n\n// HTTPResponseWriter returns a Transport implementation for the given HTTP response.\nfunc HTTPResponseWriter(w http.ResponseWriter) Transport {\n\treturn &httpHeaders{headers: w.Header()}\n}\n\n// httpHeaders is a Transport implementation for HTTP requests and responses.\n// which gives us a uniform way to add and read the headers from the either\n// a [http.Request] or a [http.ResponseWriter].\ntype httpHeaders struct {\n\theaders http.Header\n}\n\nvar _ Transport = (*httpHeaders)(nil)\n\nfunc metaKeyToHTTPHeader(key string) string {\n\tswitch key {\n\tcase TraceParentKey:\n\t\treturn \"traceparent\"\n\tcase TraceStateKey:\n\t\treturn \"tracestate\"\n\tcase CorrelationIDKey:\n\t\treturn \"X-Correlation-ID\"\n\tdefault:\n\t\treturn \"X-Encore-Meta-\" + key\n\t}\n}\n\nfunc (h *httpHeaders) SetMeta(key string, value string) {\n\th.headers.Set(metaKeyToHTTPHeader(key), value)\n}\n\nfunc (h *httpHeaders) ReadMeta(key string) (value string, found bool) {\n\tvalue = h.headers.Get(metaKeyToHTTPHeader(key))\n\treturn value, value != \"\"\n}\n\nfunc (h *httpHeaders) ReadMetaValues(key string) (values []string, found bool) {\n\tvalues = h.headers.Values(metaKeyToHTTPHeader(key))\n\treturn values, len(values) > 0\n}\n\nfunc (h *httpHeaders) ListMetaKeys() []string {\n\trtn := make([]string, 0, len(h.headers))\n\n\t// List all keys\n\tfor key := range h.headers {\n\t\tkey := http.CanonicalHeaderKey(key)\n\n\t\tswitch {\n\t\tcase key == \"Traceparent\":\n\t\t\trtn = append(rtn, TraceParentKey)\n\t\tcase key == \"Tracestate\":\n\t\t\trtn = append(rtn, TraceStateKey)\n\t\tcase key == \"X-Correlation-Id\":\n\t\t\trtn = append(rtn, CorrelationIDKey)\n\t\tcase strings.HasPrefix(key, \"X-Encore-Meta-\"):\n\t\t\trtn = append(rtn, key[14:])\n\t\t}\n\t}\n\n\tsort.Strings(rtn)\n\n\treturn rtn\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/transport/meta.go",
    "content": "package transport\n\n// These are predefined keys for metadata that we use in Encore.\n//\n// which allow each transport method know about them and handle them\n// in a transport specific way if needed (including renaming them)\nconst (\n\tTraceParentKey   = \"Traceparent\"\n\tTraceStateKey    = \"Tracestate\"\n\tCorrelationIDKey = \"Correlation-ID\"\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/transport/transport.go",
    "content": "package transport\n\n// Transport is the interface for the transport layer which allows us to add\n// and read metadata from the transport without having to know the underlying\n// transport implementation.\ntype Transport interface {\n\t// SetMeta sets a key-value pair to the metadata of the transport.\n\tSetMeta(key string, value string)\n\n\t// ReadMeta reads a metadata key off the transport.\n\t// If there are multiple values for the key, the first value is returned.\n\tReadMeta(key string) (value string, found bool)\n\n\t// ReadMetaValues reads all values for a metadata key off the transport.\n\tReadMetaValues(key string) (values []string, found bool)\n\n\t// ListMetaKeys lists all metadata keys on the transport.\n\t//\n\t// The keys returned will be ordered alphabetically and will\n\t// not contain duplicates (i.e. if a key is set multiple times).\n\t//\n\t// Keys will be normalized, such that they look the same on\n\t// both send and receive sides.\n\tListMetaKeys() []string\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/api/util.go",
    "content": "package api\n\nimport (\n\t\"strconv\"\n\n\t\"encore.dev/beta/errs\"\n)\n\nfunc clampTo64Chars(str string) string {\n\tif len(str) > 64 {\n\t\treturn str[:64]\n\t}\n\treturn str\n}\n\nfunc Code(err error, httpStatus int) string {\n\tif err != nil {\n\t\te := errs.Convert(err).(*errs.Error)\n\t\treturn e.Code.String()\n\t}\n\n\tif httpStatus == 0 {\n\t\treturn errs.OK.String()\n\t}\n\n\tif code := errs.HTTPStatusToCode(httpStatus); code != errs.Unknown {\n\t\treturn code.String()\n\t}\n\treturn \"http_\" + strconv.Itoa(httpStatus)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/app/app.go",
    "content": "package app\n\nimport (\n\t\"github.com/rs/zerolog\"\n\t\"go.uber.org/automaxprocs/maxprocs\"\n\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/appruntime/apisdk/service\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\n\t// Initialize the metric subsystem\n\t_ \"encore.dev/appruntime/infrasdk/metrics\"\n)\n\ntype App struct {\n\truntime  *config.Runtime\n\tservice  *service.Manager\n\tapi      *api.Server\n\tshutdown *shutdown.Tracker\n\tlogger   zerolog.Logger\n}\n\nfunc New(runtime *config.Runtime, service *service.Manager, api *api.Server, shutdown *shutdown.Tracker, logger zerolog.Logger) *App {\n\tapp := &App{\n\t\truntime:  runtime,\n\t\tservice:  service,\n\t\tapi:      api,\n\t\tshutdown: shutdown,\n\t\tlogger:   logger,\n\t}\n\n\treturn app\n}\n\nfunc (app *App) Run() error {\n\tif app.runtime.EnvCloud != \"local\" {\n\t\t// Set the maximum number of processes to use based on the enviroment we're running inside\n\t\t// and what we can detect. Note this is required because the default value of GOMAXPROCS is\n\t\t// the number of logical CPUs on the machine, which inside a kubernetes environment is not\n\t\t// the same as the number of CPUs allocated to the container. This can lead to CPU throttling\n\t\t// by the kernel and high tail latencies.\n\t\t//\n\t\t// We only do this when the app starts up, rather than using the automaxprocs magic import\n\t\t// so it does not impact anything else which imports the Encore runtime (such as the CLI tooling).\n\t\tundoMaxProcs, err := maxprocs.Set(maxprocs.Logger(func(s string, args ...interface{}) {\n\t\t\tapp.logger.Debug().Msgf(s, args...)\n\t\t}))\n\t\tif err != nil {\n\t\t\tapp.logger.Err(err).Msg(\"failed to set GOMAXPROCS\")\n\t\t} else {\n\t\t\tdefer undoMaxProcs()\n\t\t}\n\t}\n\n\tln, err := Listen()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = ln.Close() }()\n\n\tapp.Start()\n\n\t// Begin serving requests.\n\tserveCh := make(chan error, 1)\n\tgo func() {\n\t\tserveCh <- app.api.Serve(ln)\n\t}()\n\n\tif err := app.service.InitializeServices(); err != nil {\n\t\tapp.shutdown.Shutdown(nil, err)\n\t\treturn err\n\t}\n\n\t// Wait for the Serve to return before triggering shutdown.\n\tserveErr := <-serveCh\n\n\tisGraceful := app.shutdown.ShutdownInitiated()\n\tapp.shutdown.Shutdown(nil, serveErr)\n\n\t// If Serve returned due to graceful shutdown, ignore the error from serve.\n\tif isGraceful {\n\t\tserveErr = nil\n\t}\n\treturn serveErr\n}\n\nfunc (app *App) Start() {\n\tapp.logStartupInfo()\n\tapp.shutdown.RegisterShutdownHandler(app.api.Shutdown)\n}\n\nfunc (app *App) logStartupInfo() {\n\tswitch {\n\tcase app.runtime.EnvType == \"test\":\n\t\t// Don't log during tests.\n\tcase app.runtime.EnvCloud == \"local\" && len(app.runtime.Gateways) == 0:\n\t\t// The gateway will log this for us\n\tdefault:\n\t\t// If we have a lot of handlers, don't log each one being registered.\n\t\thandlers := app.api.RegisteredHandlers()\n\t\tlogEachRegistration := len(handlers) < 8 // chosen by a fair dice roll\n\n\t\tif logEachRegistration {\n\t\t\tfor _, h := range handlers {\n\t\t\t\tapp.logger.Trace().\n\t\t\t\t\tStr(\"service\", h.ServiceName()).\n\t\t\t\t\tStr(\"endpoint\", h.EndpointName()).\n\t\t\t\t\tStr(\"path\", h.SemanticPath()).\n\t\t\t\t\tMsg(\"registered API endpoint\")\n\t\t\t}\n\t\t} else {\n\t\t\tapp.logger.Trace().Msgf(\"registered %d API endpoints\", len(handlers))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/app/appinit/appinit.go",
    "content": "//go:build encore_app\n\npackage appinit\n\nimport (\n\t\"io\"\n\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/appruntime/apisdk/app\"\n\t\"encore.dev/appruntime/apisdk/service\"\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n)\n\n// AppMain is the entrypoint to the Encore Application.\nfunc AppMain() {\n\tinst := app.New(appconf.Runtime, service.Singleton, api.Singleton, shutdown.Singleton, logging.RootLogger)\n\tif err := inst.Run(); err != nil && err != io.EOF {\n\t\tlogging.RootLogger.Fatal().Err(err).Msg(\"could not run\")\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/app/setup.go",
    "content": "package app\n\nimport (\n\t\"net\"\n\t\"net/netip\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"encore.dev/appruntime/shared/encoreenv\"\n)\n\nfunc Listen() (net.Listener, error) {\n\tlistenAddr := encoreenv.Get(\"ENCORE_LISTEN_ADDR\")\n\tif listenAddr != \"\" {\n\t\taddrPort, err := netip.ParseAddrPort(listenAddr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn net.Listen(\"tcp\", addrPort.String())\n\t}\n\n\tport, _ := strconv.Atoi(os.Getenv(\"PORT\"))\n\tif port == 0 {\n\t\tport = 8080\n\t}\n\treturn net.Listen(\"tcp\", \":\"+strconv.Itoa(port))\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/cors/cors.go",
    "content": "package cors\n\nimport (\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/rs/cors\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\nfunc Wrap(cfg *config.CORS, staticAllowedHeaders, staticExposedHeaders []string, handler http.Handler, logger zerolog.Logger) http.Handler {\n\tc := cors.New(Options(cfg, staticAllowedHeaders, staticExposedHeaders))\n\tif cfg.Debug {\n\t\tlogger := logger.With().Str(\"subsystem\", \"cors\").Logger()\n\t\tlogger.Debug().Msg(\"CORS system running in debug mode. All requests will be logged.\")\n\t\tc.Log = &logger\n\t}\n\treturn c.Handler(handler)\n}\n\nfunc Options(cfg *config.CORS, staticAllowedHeaders, staticExposedHeaders []string) cors.Options {\n\t// Sort origins to allow for binary search\n\toriginsCreds := sortedSliceCopy(cfg.AllowOriginsWithCredentials)\n\toriginsWithoutCreds := sortedSliceCopy(cfg.AllowOriginsWithoutCredentials)\n\tglobCreds := getGlobOrigins(cfg.AllowOriginsWithCredentials)\n\tglobWithoutCreds := getGlobOrigins(cfg.AllowOriginsWithoutCredentials)\n\n\t// Determine if we have a wildcard origins\n\thasWildcardOriginWithoutCreds := cfg.AllowOriginsWithoutCredentials == nil || sortedSliceContains(originsWithoutCreds, \"*\")\n\thasUnsafeWildcardOriginWithCreds := sortedSliceContains(originsCreds, config.UnsafeAllOriginWithCredentials)\n\n\t// allowedHeaders are the headers allowed through CORS.\n\tallowedHeaders := []string{\n\t\t\"Authorization\",\n\t\t\"Content-Type\",\n\t\t\"User-Agent\",\n\t\t\"X-Request-ID\",\n\t\t\"X-Correlation-ID\",\n\t}\n\tallowedHeaders = append(allowedHeaders, cfg.ExtraAllowedHeaders...)\n\tallowedHeaders = append(allowedHeaders, staticAllowedHeaders...)\n\n\texposedHeaders := []string{\n\t\t\"X-Request-ID\",\n\t\t\"X-Correlation-ID\",\n\t\t\"X-Encore-Trace-ID\",\n\t}\n\texposedHeaders = append(exposedHeaders, cfg.ExtraExposedHeaders...)\n\texposedHeaders = append(exposedHeaders, staticExposedHeaders...)\n\n\t// Sort the slices so the output looks nicer.\n\tsort.Strings(allowedHeaders)\n\tsort.Strings(exposedHeaders)\n\n\treturn cors.Options{\n\t\tDebug:               cfg.Debug,\n\t\tAllowCredentials:    !cfg.DisableCredentials,\n\t\tAllowedMethods:      []string{\"GET\", \"POST\", \"PUT\", \"PATCH\", \"HEAD\", \"DELETE\", \"OPTIONS\", \"TRACE\", \"CONNECT\"},\n\t\tAllowedHeaders:      allowedHeaders,\n\t\tExposedHeaders:      exposedHeaders,\n\t\tAllowPrivateNetwork: cfg.AllowPrivateNetworkAccess,\n\t\tAllowOriginRequestFunc: func(r *http.Request, origin string) bool {\n\t\t\t// If the request has credentials, look up origins in AllowOriginsWithCredentials.\n\t\t\t// Credentials are cookies, authorization headers, or TLS client certificates.\n\t\t\thasCreds := func(r *http.Request) bool {\n\t\t\t\tif len(r.Cookies()) > 0 || len(r.Header.Values(\"Authorization\")) > 0 || (r.TLS != nil && len(r.TLS.PeerCertificates) > 0) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\n\t\t\t\tif r.Method == http.MethodOptions {\n\t\t\t\t\treturn slices.ContainsFunc(\n\t\t\t\t\t\tr.Header.Values(\"Access-Control-Request-Headers\"),\n\t\t\t\t\t\tfunc(val string) bool {\n\t\t\t\t\t\t\treturn slices.ContainsFunc(\n\t\t\t\t\t\t\t\tstrings.Split(val, \",\"),\n\t\t\t\t\t\t\t\tfunc(val string) bool {\n\t\t\t\t\t\t\t\t\tval = strings.TrimSpace(val)\n\t\t\t\t\t\t\t\t\treturn val == \"authorization\" || val == \"cookie\"\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif hasCreds(r) {\n\t\t\t\tok := hasUnsafeWildcardOriginWithCreds || sortedSliceContains(originsCreds, origin)\n\t\t\t\tif !ok {\n\t\t\t\t\t// Not an exact match. Check any glob origins.\n\t\t\t\t\tok = globCreds.Matches(origin)\n\t\t\t\t}\n\t\t\t\treturn ok\n\t\t\t}\n\t\t\t// Post-condition: request is without credentials\n\n\t\t\tok := hasWildcardOriginWithoutCreds || sortedSliceContains(originsWithoutCreds, origin)\n\t\t\tif !ok {\n\t\t\t\t// Not an exact match. Check any glob origins.\n\t\t\t\tok = globWithoutCreds.Matches(origin)\n\t\t\t}\n\t\t\treturn ok\n\t\t},\n\t}\n}\n\nfunc sortedSliceContains(haystack []string, needle string) bool {\n\tidx := sort.SearchStrings(haystack, needle)\n\treturn idx < len(haystack) && haystack[idx] == needle\n}\n\nfunc sortedSliceCopy(src []string) []string {\n\tif src == nil {\n\t\treturn nil\n\t}\n\n\tdst := make([]string, len(src))\n\tcopy(dst, src)\n\tsort.Strings(dst)\n\treturn dst\n}\n\n// globOrigin represents a parsed origin pattern with glob support.\ntype globOrigin struct {\n\tscheme, hostname, port string\n}\n\ntype globOriginSet []globOrigin\n\nfunc (s globOriginSet) Matches(origin string) bool {\n\to, ok := parseOrigin(origin)\n\tif !ok {\n\t\treturn false\n\t}\n\tfor _, pattern := range s {\n\t\tif pattern.matches(o) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (pattern globOrigin) matches(origin globOrigin) bool {\n\tif pattern.scheme != origin.scheme {\n\t\treturn false\n\t}\n\tif matched, _ := filepath.Match(pattern.port, origin.port); !matched {\n\t\treturn false\n\t}\n\tmatched, _ := filepath.Match(pattern.hostname, origin.hostname)\n\treturn matched\n}\n\n// parseOrigin splits an origin string into scheme, hostname, and port.\n// The port is normalized to the default port for the scheme if not specified.\n// See https://developer.mozilla.org/en-US/docs/Glossary/Origin.\nfunc parseOrigin(origin string) (globOrigin, bool) {\n\tscheme, rest, ok := strings.Cut(origin, \"://\")\n\tif !ok {\n\t\treturn globOrigin{}, false\n\t}\n\t// Strip any path component.\n\tif idx := strings.Index(rest, \"/\"); idx != -1 {\n\t\trest = rest[:idx]\n\t}\n\thostname, port, hasPort := strings.Cut(rest, \":\")\n\tif !hasPort {\n\t\tswitch scheme {\n\t\tcase \"http\":\n\t\t\tport = \"80\"\n\t\tcase \"https\":\n\t\t\tport = \"443\"\n\t\t}\n\t}\n\treturn globOrigin{scheme, hostname, port}, true\n}\n\nfunc getGlobOrigins(origins []string) globOriginSet {\n\tvar globs []globOrigin\n\tfor _, o := range origins {\n\t\tif o == \"*\" || !strings.Contains(o, \"*\") {\n\t\t\tcontinue\n\t\t}\n\t\tif parsed, ok := parseOrigin(o); ok {\n\t\t\tglobs = append(globs, parsed)\n\t\t}\n\t}\n\treturn globs\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/cors/cors_test.go",
    "content": "package cors\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t_ \"unsafe\"\n\n\t\"github.com/rs/cors\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\nfunc TestOptions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname               string\n\t\tcfg                config.CORS\n\t\tcredsGoodOrigins   []string\n\t\tcredsBadOrigins    []string\n\t\tnocredsGoodOrigins []string\n\t\tnocredsBadOrigins  []string\n\t\tgoodHeaders        []string\n\t\tbadHeaders         []string\n\t}{\n\t\t{\n\t\t\tname:               \"empty\",\n\t\t\tcfg:                config.CORS{},\n\t\t\tcredsGoodOrigins:   []string{},\n\t\t\tcredsBadOrigins:    []string{\"foo.com\", \"evil.com\", \"localhost\"},\n\t\t\tnocredsGoodOrigins: []string{\"foo.com\", \"localhost\", \"\", \"icanhazcheezburger.com\"},\n\t\t\tnocredsBadOrigins:  []string{},\n\t\t\tgoodHeaders:        []string{\"Content-Type\", \"Origin\"},\n\t\t\tbadHeaders:         []string{\"X-Requested-With\", \"X-Forwarded-For\"},\n\t\t},\n\t\t{\n\t\t\tname: \"allowed_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithCredentials: []string{\"localhost\", \"ok.org\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins:   []string{\"localhost\", \"ok.org\"},\n\t\t\tcredsBadOrigins:    []string{\"foo.com\", \"evil.com\"},\n\t\t\tnocredsGoodOrigins: []string{\"foo.com\", \"localhost\", \"\", \"icanhazcheezburger.com\", \"ok.org\"},\n\t\t\tnocredsBadOrigins:  []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"allowed_glob_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithCredentials: []string{\"https://*.example.com\", \"wss://ok1-*.example.com\", \"https://*-ok2.example.com\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins: []string{\"https://foo.example.com\", \"wss://ok1-foo.example.com\", \"https://foo-ok2.example.com\"},\n\t\t\tcredsBadOrigins: []string{\n\t\t\t\t\"http://foo.example.com\",   // Wrong scheme\n\t\t\t\t\"htts://.example.com\",      // No subdomain\n\t\t\t\t\"ws://ok1-foo.example.com\", // Wrong scheme\n\t\t\t\t\".example.com\",             // No scheme\n\t\t\t\t\"https://evil.com\",         // bad domain\n\t\t\t},\n\t\t\tnocredsGoodOrigins: []string{},\n\t\t\tnocredsBadOrigins:  []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"allowed_nocreds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithoutCredentials: []string{\"localhost\", \"ok.org\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins:   []string{},\n\t\t\tcredsBadOrigins:    []string{\"localhost\", \"ok.org\", \"foo.com\", \"evil.com\"},\n\t\t\tnocredsGoodOrigins: []string{\"localhost\", \"ok.org\"},\n\t\t\tnocredsBadOrigins:  []string{\"foo.com\", \"\", \"icanhazcheezburger.com\"},\n\t\t},\n\t\t{\n\t\t\tname: \"allowed_disjoint_sets\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithCredentials:    []string{\"foo.com\"},\n\t\t\t\tAllowOriginsWithoutCredentials: []string{\"bar.org\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins:   []string{\"foo.com\"},\n\t\t\tcredsBadOrigins:    []string{\"bar.org\", \"\", \"localhost\"},\n\t\t\tnocredsGoodOrigins: []string{\"bar.org\"},\n\t\t\tnocredsBadOrigins:  []string{\"foo.com\", \"\", \"localhost\"},\n\t\t},\n\t\t{\n\t\t\tname: \"allowed_wildcard_without_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithoutCredentials: []string{\"*\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins:   []string{},\n\t\t\tcredsBadOrigins:    []string{\"bar.org\", \"\", \"localhost\"},\n\t\t\tnocredsGoodOrigins: []string{\"bar.org\", \"bar.com\", \"\", \"localhost\"},\n\t\t},\n\t\t{\n\t\t\tname: \"allowed_unsafe_wildcard_with_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithCredentials: []string{config.UnsafeAllOriginWithCredentials},\n\t\t\t},\n\t\t\tcredsGoodOrigins: []string{\"bar.org\", \"bar.com\", \"\", \"localhost\", \"unsafe.evil.com\"},\n\t\t},\n\t\t{\n\t\t\tname: \"extra_headers\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tExtraAllowedHeaders: []string{\"Not-Authorization\", \"X-Forwarded-For\", \"X-Real-Ip\"},\n\t\t\t},\n\t\t\tgoodHeaders: []string{\"Not-Authorization\", \"Content-Type\", \"Origin\", \"X-Forwarded-For\", \"X-Real-Ip\"},\n\t\t\tbadHeaders:  []string{\"X-Requested-With\", \"X-Evil-Header\", \"Authorization\"},\n\t\t},\n\t\t{\n\t\t\tname: \"extra_headers_wildcard\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tExtraAllowedHeaders: []string{\"X-Forwarded-For\", \"*\", \"X-Real-Ip\"},\n\t\t\t},\n\t\t\tgoodHeaders: []string{\"Not-Authorization\", \"Content-Type\", \"Origin\", \"X-Forwarded-For\", \"X-Real-Ip\", \"X-Requested-With\", \"X-Evil-Header\"},\n\t\t\tbadHeaders:  []string{\"Authorization\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"static_headers\",\n\t\t\tcfg:         config.CORS{},\n\t\t\tgoodHeaders: []string{\"Content-Type\", \"Origin\", \"X-Static-Test\"},\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard_port_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithCredentials: []string{\"http://localhost:*\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins:   []string{\"http://localhost:3000\", \"http://localhost:8080\", \"http://localhost:80\", \"http://localhost\"},\n\t\t\tcredsBadOrigins:    []string{\"https://localhost:3000\", \"http://evil.com:3000\"},\n\t\t\tnocredsGoodOrigins: []string{},\n\t\t\tnocredsBadOrigins:  []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard_port_without_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithoutCredentials: []string{\"http://localhost:*\"},\n\t\t\t},\n\t\t\tcredsGoodOrigins:   []string{},\n\t\t\tcredsBadOrigins:    []string{\"http://localhost:3000\"},\n\t\t\tnocredsGoodOrigins: []string{\"http://localhost:3000\", \"http://localhost:8080\", \"http://localhost\"},\n\t\t\tnocredsBadOrigins:  []string{\"https://localhost:3000\"},\n\t\t},\n\t\t{\n\t\t\tname: \"wildcard_without_creds\",\n\t\t\tcfg: config.CORS{\n\t\t\t\tAllowOriginsWithCredentials:    []string{\"https://vercel.app\"},\n\t\t\t\tAllowOriginsWithoutCredentials: []string{\"https://*-foo.vercel.app\"},\n\t\t\t},\n\t\t\tcredsBadOrigins:    []string{\"https://blah-foo.vercel.app\"},\n\t\t\tnocredsGoodOrigins: []string{\"https://blah-foo.vercel.app\"},\n\t\t},\n\t}\n\n\tcheckOrigins := func(t *testing.T, c *cors.Cors, creds, good bool, origins []string) {\n\t\tfor _, o := range origins {\n\t\t\th := make(http.Header)\n\t\t\th.Set(\"Origin\", o)\n\t\t\tif creds {\n\t\t\t\th.Set(\"Access-Control-Request-Headers\", \"authorization\")\n\t\t\t}\n\t\t\tallowed := c.OriginAllowed(&http.Request{Header: h, Method: \"OPTIONS\"})\n\t\t\tif allowed != good {\n\t\t\t\tt.Fatalf(\"origin=%s creds=%v: got allowed=%v, want %v\", o, creds, allowed, good)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"origin=%s creds=%v: ok allowed=%v\", o, creds, allowed)\n\t\t\t}\n\t\t}\n\t}\n\n\tcheckHeaders := func(t *testing.T, c *cors.Cors, allowedHeaders []string, wantOK bool) {\n\t\treq := httptest.NewRequest(\"OPTIONS\", \"/\", nil)\n\t\treq.Header.Set(\"Origin\", \"https://example.org\")\n\t\treq.Header.Set(\"Access-Control-Request-Method\", \"GET\")\n\t\treq.Header.Set(\"Access-Control-Request-Headers\", strings.ToLower(strings.Join(allowedHeaders, \",\")))\n\t\tw := httptest.NewRecorder()\n\t\tc.ServeHTTP(w, req, nil)\n\n\t\tif w.Code != http.StatusNoContent {\n\t\t\tt.Fatalf(\"got OPTIONS response code %d, want 204\", w.Code)\n\t\t}\n\n\t\t// Check allowed headers.\n\t\t{\n\t\t\trawAllowedHeaders := w.Header().Get(\"Access-Control-Allow-Headers\")\n\t\t\tallowHeaders := strings.Split(rawAllowedHeaders, \", \")\n\t\t\tallowed := make(map[string]bool)\n\t\t\tfor _, val := range allowHeaders {\n\t\t\t\tallowed[strings.TrimSpace(val)] = true\n\t\t\t}\n\n\t\t\tif wantOK {\n\t\t\t\tfor _, val := range allowedHeaders {\n\t\t\t\t\tif !allowed[val] {\n\t\t\t\t\t\tt.Fatalf(\"want header %q to be allowed, got false; resp header=%q\", val, rawAllowedHeaders)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif rawAllowedHeaders != \"\" {\n\t\t\t\t\tt.Fatalf(\"want headers not to be allowed, got %q\", rawAllowedHeaders)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot := Options(&tt.cfg, []string{\"X-Static-Test\"}, []string{\"X-Exposed-Test\"})\n\t\t\tgot.Debug = true\n\t\t\tc := cors.New(got)\n\t\t\tc.Log = log.New(os.Stdout, \"cors: \", 0)\n\n\t\t\tcheckOrigins(t, c, true, true, tt.credsGoodOrigins)\n\t\t\tcheckOrigins(t, c, true, false, tt.credsBadOrigins)\n\t\t\tcheckOrigins(t, c, false, true, tt.nocredsGoodOrigins)\n\t\t\tcheckOrigins(t, c, false, false, tt.nocredsBadOrigins)\n\n\t\t\t// Only good headers should always be ok\n\t\t\tcheckHeaders(t, c, tt.goodHeaders, true)\n\n\t\t\t// Make sure all the bad headers are invalid, one by one\n\t\t\tfor _, vad := range tt.badHeaders {\n\t\t\t\theaders := append(tt.goodHeaders, vad)\n\t\t\t\tcheckHeaders(t, c, headers, false)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/service/service.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/cfgutil\"\n\t\"encore.dev/appruntime/shared/health\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/syncutil\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/beta/errs\"\n\teshutdown \"encore.dev/shutdown\"\n)\n\n// Initializer is a service initializer.\ntype Initializer interface {\n\t// ServiceName reports the name of the service.\n\tServiceName() string\n\n\t// InitService initializes the service.\n\tInitService() error\n\n\t// GetDecl returns the service declaration,\n\t// initializing it if necessary.\n\tGetDecl() (any, error)\n}\n\ntype Decl[T any] struct {\n\tService string\n\tName    string\n\n\t// Setup sets up the service instance.\n\t// If nil, the service is initialized with new(T).\n\tSetup func() (*T, error)\n\n\t// SetupDefLoc is the location of the Setup function.\n\t// It is 0 if Setup is nil.\n\tSetupDefLoc uint32\n\n\tholder InstanceHolder[T]\n}\n\ntype InstanceHolder[T any] struct {\n\tsetupOnce syncutil.Once\n\tinstance  *T\n}\n\nfunc (g *Decl[T]) ServiceName() string {\n\treturn g.Service\n}\n\nfunc doSetupService[T any](mgr *Manager, decl *Decl[T], holder *InstanceHolder[T]) (err error) {\n\tcurr := mgr.rt.Current()\n\tif curr.Trace != nil && curr.Req != nil && decl.SetupDefLoc != 0 {\n\t\teventParams := trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  decl.SetupDefLoc,\n\t\t}\n\t\tstartID := curr.Trace.ServiceInitStart(trace2.ServiceInitStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tService:     decl.Service,\n\t\t})\n\t\tdefer curr.Trace.ServiceInitEnd(eventParams, startID, err)\n\t}\n\n\tsetupFn := decl.Setup\n\tif setupFn == nil {\n\t\tsetupFn = func() (*T, error) { return new(T), nil }\n\t}\n\n\tinstance, err := setupFn()\n\tif err != nil {\n\t\tmgr.rt.Logger().Error().Err(err).Str(\"service\", decl.Service).Msg(\"service initialization failed\")\n\t\treturn errs.B().Code(errs.Internal).Msgf(\"service %s: initialization failed\", decl.Service).Err()\n\t}\n\tholder.instance = instance\n\n\t// If the API Decl supports graceful shutdown, register that with the server.\n\tif gs, ok := any(instance).(shutdowner); ok {\n\t\tmgr.registerShutdownHandler(serviceShutdown{decl.Service, gs})\n\t} else if legacy, ok := any(instance).(legacyShutdowner); ok {\n\t\tadapter := legacyShutdownAdapter{legacy}\n\t\tmgr.registerShutdownHandler(serviceShutdown{decl.Service, adapter})\n\t}\n\treturn nil\n}\n\n// shutdowner is the interface for service structs that\n// support graceful shutdown.\ntype shutdowner interface {\n\tShutdown(p eshutdown.Progress) error\n}\n\n// legacyShutdowner is the old-style interface for service structs that\n// support graceful shutdown.\ntype legacyShutdowner interface {\n\tShutdown(force context.Context)\n}\n\ntype legacyShutdownAdapter struct {\n\ts legacyShutdowner\n}\n\nfunc (a legacyShutdownAdapter) Shutdown(p eshutdown.Progress) error {\n\ta.s.Shutdown(p.ForceShutdown)\n\treturn nil\n}\n\ntype serviceShutdown struct {\n\tname     string\n\tinstance shutdowner\n}\n\nfunc NewManager(static *config.Static, runtime *config.Runtime, rt *reqtrack.RequestTracker, healthChecks *health.CheckRegistry, rootLogger zerolog.Logger, testMgr *testsupport.Manager) *Manager {\n\tmgr := &Manager{static: static, rt: rt, runtime: runtime, rootLogger: rootLogger, testMgr: testMgr, svcMap: make(map[string]Initializer), initialisedServices: make(map[string]struct{})}\n\n\t// Register with the health check service.\n\thealthChecks.Register(mgr)\n\n\treturn mgr\n}\n\ntype Manager struct {\n\tstatic     *config.Static\n\truntime    *config.Runtime\n\trt         *reqtrack.RequestTracker\n\trootLogger zerolog.Logger\n\ttestMgr    *testsupport.Manager\n\tsvcInit    []Initializer\n\tsvcMap     map[string]Initializer\n\n\tinitialisedMu       sync.RWMutex\n\tinitialisedServices map[string]struct{}\n\n\tshutdownMu       sync.Mutex\n\tshutdownHandlers []serviceShutdown\n}\n\nfunc (mgr *Manager) RegisterService(i Initializer) {\n\tname := i.ServiceName()\n\tif !cfgutil.IsHostedService(mgr.runtime, name) {\n\t\treturn\n\t}\n\n\tif _, ok := mgr.svcMap[name]; ok {\n\t\tpanic(fmt.Sprintf(\"service %s: already registered\", name))\n\t}\n\tmgr.svcMap[name] = i\n\tmgr.svcInit = append(mgr.svcInit, i)\n}\n\nfunc (mgr *Manager) InitializeServices() error {\n\tnum := len(mgr.svcInit)\n\tresults := make(chan error, num)\n\n\tfor _, svc := range mgr.svcInit {\n\t\tsvc := svc\n\t\tgo func() {\n\t\t\terr := svc.InitService()\n\t\t\tif err == nil {\n\t\t\t\tmgr.initialisedMu.Lock()\n\t\t\t\tdefer mgr.initialisedMu.Unlock()\n\t\t\t\tmgr.initialisedServices[svc.ServiceName()] = struct{}{}\n\t\t\t}\n\t\t\tresults <- err\n\t\t}()\n\t}\n\n\tfor i := 0; i < num; i++ {\n\t\tif err := <-results; err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// HealthCheck returns a failure if any services have not yet been initialized.\n//\n// This allows the health check service to report that the server is not yet\n// ready to serve requests.\nfunc (mgr *Manager) HealthCheck(ctx context.Context) []health.CheckResult {\n\tmgr.initialisedMu.RLock()\n\tdefer mgr.initialisedMu.RUnlock()\n\n\t// If all services have been initialized, return a single check result.\n\tif len(mgr.initialisedServices) == len(mgr.svcMap) {\n\t\treturn []health.CheckResult{{Name: \"services.initialized\"}}\n\t}\n\n\t// Build a list of services that have not been initialized.\n\tuninitializedServices := make([]string, 0, len(mgr.svcMap)-len(mgr.initialisedServices))\n\tfor svc := range mgr.svcMap {\n\t\tif _, ok := mgr.initialisedServices[svc]; !ok {\n\t\t\tuninitializedServices = append(uninitializedServices, svc)\n\t\t}\n\t}\n\tsort.Strings(uninitializedServices)\n\n\t// Return an error listing the names of each service not yet initialized.\n\treturn []health.CheckResult{{\n\t\tName: \"services.initialized\",\n\t\tErr:  fmt.Errorf(\"the following services have not returned from their initService functions: %s\", strings.Join(uninitializedServices, \", \")),\n\t}}\n}\n\nfunc (mgr *Manager) GetService(name string) (i Initializer, ok bool) {\n\ti, ok = mgr.svcMap[name]\n\treturn i, ok\n}\n\nfunc (mgr *Manager) Shutdown(p *shutdown.Process) (err error) {\n\tdefer p.MarkServicesShutdownCompleted(err)\n\tdoLog := true\n\n\tmgr.shutdownMu.Lock()\n\thandlers := mgr.shutdownHandlers\n\tmgr.shutdownMu.Unlock()\n\n\tprogress := p.Progress()\n\n\tvar wg sync.WaitGroup\n\twg.Add(len(handlers))\n\tfor _, h := range handlers {\n\t\th := h\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tif doLog {\n\t\t\t\tmgr.rootLogger.Trace().Str(\"service\", h.name).Msg(\"shutting down service...\")\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tmgr.rootLogger.Error().Str(\"service\", h.name).Interface(\"panic\", r).Msg(\"service shutdown panicked\")\n\t\t\t\t\t} else if mgr.runtime.EnvCloud != \"local\" {\n\t\t\t\t\t\tmgr.rootLogger.Trace().Str(\"service\", h.name).Msg(\"service shutdown complete\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tif err := h.instance.Shutdown(progress); err != nil {\n\t\t\t\tmgr.rootLogger.Error().Err(err).Str(\"service\", h.name).Msg(\"service reported unclean shutdown\")\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\treturn nil\n}\n\nfunc (mgr *Manager) registerShutdownHandler(h serviceShutdown) {\n\tmgr.shutdownMu.Lock()\n\tdefer mgr.shutdownMu.Unlock()\n\tmgr.shutdownHandlers = append(mgr.shutdownHandlers, h)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/apisdk/service/singleton.go",
    "content": "//go:build encore_app\n\npackage service\n\nimport (\n\t\"fmt\"\n\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/health\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\nvar Singleton *Manager\n\nfunc init() {\n\tSingleton = NewManager(appconf.Static, appconf.Runtime, reqtrack.Singleton, health.Singleton, logging.RootLogger, testsupport.Singleton)\n\tshutdown.Singleton.RegisterShutdownHandler(Singleton.Shutdown)\n}\n\nfunc Register(i Initializer) {\n\tSingleton.RegisterService(i)\n}\n\n// Get returns the API Decl, initializing it if necessary.\nfunc (g *Decl[T]) Get() (*T, error) {\n\tif Singleton.static.Testing {\n\t\t// Do we have a mock registered?\n\t\tif mock, ok := Singleton.testMgr.GetServiceMock(g.Service); ok {\n\t\t\treturn mock.Service.(*T), nil\n\t\t}\n\n\t\tif Singleton.testMgr.GetIsolatedServices() {\n\t\t\ttestData := Singleton.rt.Current().Req.Test\n\n\t\t\t// Get the instance holder while under lock, but then\n\t\t\t// unlock the mutex before calling doSetupService - as if the service\n\t\t\t// init calls another service which we need to initialize, we would\n\t\t\t// otherwise deadlock.\n\t\t\ttestData.ServiceInstancesMu.Lock()\n\t\t\tholderAny, ok := testData.ServiceInstances[g.Service]\n\t\t\tif !ok {\n\t\t\t\tholderAny = &InstanceHolder[T]{}\n\t\t\t\ttestData.ServiceInstances[g.Service] = holderAny\n\t\t\t}\n\t\t\ttestData.ServiceInstancesMu.Unlock()\n\n\t\t\t// This is a bit of a hack, but we need to cast the holder to the\n\t\t\t// correct type as the TestData struct is in our model package, which\n\t\t\t// we don't want to introduce complex types to\n\t\t\tholder, ok := holderAny.(*InstanceHolder[T])\n\t\t\tif !ok {\n\t\t\t\tvar zero *InstanceHolder[T]\n\t\t\t\treturn nil, fmt.Errorf(\"failed to cast service instance holder to correct type for service %s. Found %T expected %T\", g.Name, holderAny, zero)\n\t\t\t}\n\t\t\terr := holder.setupOnce.Do(func() error {\n\t\t\t\treturn doSetupService(Singleton, g, holder)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn holder.instance, nil\n\t\t}\n\t}\n\n\terr := g.InitService()\n\treturn g.holder.instance, err\n}\n\n// GetDecl returns the API Decl, initializing it if necessary.\nfunc (g *Decl[T]) GetDecl() (any, error) {\n\treturn g.Get()\n}\n\nfunc (g *Decl[T]) InitService() error {\n\treturn g.holder.setupOnce.Do(func() error {\n\t\treturn doSetupService(Singleton, g, &g.holder)\n\t})\n}\n\n// Get returns the service initializer with the given name.\n// The declaration is cast to the given type T.\nfunc Get[T any](name string) (T, error) {\n\tsvc, ok := Singleton.GetService(name)\n\tif !ok {\n\t\tvar zero T\n\t\treturn zero, fmt.Errorf(\"service.Get(%q): unknown service\", name)\n\t}\n\n\tdecl, err := svc.GetDecl()\n\tif err != nil {\n\t\tvar zero T\n\t\treturn zero, err\n\t}\n\n\ts, ok := decl.(T)\n\tif !ok {\n\t\tvar zero T\n\t\treturn zero, fmt.Errorf(\"service.Get(%q): service is of type %T, not %T\", name, decl, zero)\n\t}\n\n\treturn s, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/doc.go",
    "content": "// Package appruntime provides internal functionality for use by\n// Encore to run Encore applications. Packages within this directory\n// are intended exclusively for use by the Encore compiler and should\n// not be called directly from Encore applications. The APIs can change\n// at any time.\npackage appruntime\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/config.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"go.encore.dev/platform-sdk/pkg/auth\"\n)\n\ntype Static struct {\n\t// The version of Encore which the app was compiled with.\n\t// This is string is for informational use only, and it's format should not be relied on.\n\tEncoreCompiler string\n\tAppCommit      CommitInfo // The commit which this service was built from\n\n\tCORSAllowHeaders  []string // Headers to be allowed by cors\n\tCORSExposeHeaders []string // Headers to be exposed by cors\n\tPubsubTopics      map[string]*StaticPubsubTopic\n\n\tTesting         bool\n\tTestServiceMap  map[string]string // map of service names to their filesystem root\n\tTestAppRootPath string            // the root path of the app when running tests\n\n\t// PrettyPrintLogs indicates whether logs should be pretty-printed.\n\t// It's set when building a separate test binary, for example.\n\tPrettyPrintLogs bool\n\n\t// BundledServices are the services bundled in this binary.\n\tBundledServices []string\n\n\t// EnabledExperiments is a list of experiments that are enabled for this app\n\t// which where enabled at compile time.\n\tEnabledExperiments []string `json:\"experiments,omitempty\"`\n\n\t// EmbeddedEnvs is a set of embedded environment variables.\n\tEmbeddedEnvs map[string]string\n}\n\ntype Runtime struct {\n\tAppID               string             `json:\"app_id\"`\n\tAppSlug             string             `json:\"app_slug\"`\n\tAPIBaseURL          string             `json:\"api_base_url\"`\n\tEnvID               string             `json:\"env_id\"`\n\tEnvName             string             `json:\"env_name\"`\n\tEnvType             string             `json:\"env_type\"`\n\tEnvCloud            string             `json:\"env_cloud\"`\n\tDeployID            string             `json:\"deploy_id\"` // Overridden by ENCORE_DEPLOY_ID env var if set\n\tDeployedAt          time.Time          `json:\"deploy_time\"`\n\tTraceEndpoint       string             `json:\"trace_endpoint,omitempty\"`\n\tTraceSamplingRate   *float64           `json:\"trace_sampling_rate,omitempty\"` // Deprecated: Use TraceSamplingConfig instead.\n\tTraceSamplingConfig map[string]float64 `json:\"trace_sampling_config,omitempty\"`\n\tAuthKeys            []EncoreAuthKey    `json:\"auth_keys,omitempty\"`\n\tCORS                *CORS              `json:\"cors,omitempty\"`\n\tEncoreCloudAPI      *EncoreCloudAPI    `json:\"ec_api,omitempty\"` // If nil, the app is not running in Encore Cloud\n\n\tSQLDatabases     []*SQLDatabase          `json:\"sql_databases,omitempty\"`\n\tSQLServers       []*SQLServer            `json:\"sql_servers,omitempty\"`\n\tPubsubProviders  []*PubsubProvider       `json:\"pubsub_providers,omitempty\"`\n\tPubsubTopics     map[string]*PubsubTopic `json:\"pubsub_topics,omitempty\"`\n\tRedisServers     []*RedisServer          `json:\"redis_servers,omitempty\"`\n\tRedisDatabases   []*RedisDatabase        `json:\"redis_databases,omitempty\"`\n\tBucketProviders  []*BucketProvider       `json:\"bucket_providers,omitempty\"`\n\tBuckets          map[string]*Bucket      `json:\"buckets,omitempty\"`\n\tMetrics          *Metrics                `json:\"metrics,omitempty\"`\n\tGateways         []Gateway               `json:\"gateways,omitempty\"`          // Gateways defines the gateways which should be served by the container\n\tHostedServices   []string                `json:\"hosted_services,omitempty\"`   // List of services to be hosted within this container (zero length means all services, unless there's a gateway running)\n\tServiceDiscovery map[string]Service      `json:\"service_discovery,omitempty\"` // ServiceDiscovery lists where all the services are being hosted if not in this container\n\n\t// ServiceAuth defines which authentication method can be used\n\t// when talking to this runtime for internal service-to-service\n\t// calls.\n\t//\n\t// An empty slice means that no service-to-service calls can be made\n\tServiceAuth []ServiceAuth `json:\"service_auth,omitempty\"`\n\n\t// ShutdownTimeout is the duration before non-graceful shutdown is initiated,\n\t// meaning connections are closed even if outstanding requests are still in flight.\n\t// If zero, it shuts down immediately.\n\t//\n\t// Deprecated: Use GracefulShutdown.Total instead.\n\tShutdownTimeout time.Duration `json:\"shutdown_timeout\"`\n\n\t// GracefulShutdown defines the timings for the graceful shutdown process.\n\tGracefulShutdown *GracefulShutdownTimings `json:\"graceful_shutdown,omitempty\"`\n\n\t// DynamicExperiments is a list of experiments that are enabled for this app\n\t// which impact runtime behaviour, but which were not enabled at compile time.\n\t//\n\t// Experiments which impact compilation should be handled by the compiler\n\t// and added to the static config.\n\tDynamicExperiments []string `json:\"dynamic_experiments,omitempty\"`\n\n\t// Log configuration to set for the application.\n\t// If empty it defaults to \"trace\".\n\tLogConfig string `json:\"log_config\"`\n}\n\n// GracefulShutdownTimings defines the timings for the graceful shutdown process.\ntype GracefulShutdownTimings struct {\n\t// Total is how long we allow the total shutdown to take\n\t// before we simply kill the process using [os.Exit]\n\t//\n\t// If not set, it will default to [Runtime.ShutdownTimeout] during\n\t// the migration period to this config.\n\t//\n\t// If [Runtime.ShutdownTimeout] is also not set, it will default to\n\t// 500ms.\n\tTotal *time.Duration `json:\"total,omitempty\"`\n\n\t// ShutdownHooks is how long before [Total] runs out that we cancel\n\t// the context that is passed to the shutdown hooks.\n\t//\n\t// If not set, it will default to 1 second.\n\t//\n\t// It is expected that ShutdownHooks is a larger value than Handlers.\n\tShutdownHooks *time.Duration `json:\"shutdown_hooks,omitempty\"`\n\n\t// Handlers is how long before [Total] runs out that we cancel\n\t// the context that is passed to API and PubSub Subscription handlers.\n\t//\n\t// If not set, it will default to 1 second.\n\t//\n\t// For example, if [Total] is 10 seconds and [Handlers] is 2 seconds,\n\t// then we will cancel the context passed to handlers 8 seconds after\n\t// a graceful shutdown is initiated.\n\tHandlers *time.Duration `json:\"handlers,omitempty\"`\n}\n\n// Gateway defines the configuration of a gateway which should be served\n// by the container\ntype Gateway struct {\n\t// Name is the name of the gateway\n\tName string `json:\"name\"`\n\t// Host is the hostname of the gateway\n\tHost string `json:\"host\"`\n}\n\n// Service defines the service discovery configuration for a service\ntype Service struct {\n\t// Name is the name of the service\n\tName string `json:\"name\"`\n\t// URL is the base URL of the service (including protocol and port)\n\tURL string `json:\"url\"`\n\t// Protocol is the protocol that the service talks\n\tProtocol SvcProtocol `json:\"protocol\"`\n\n\t// ServiceAuth is the authentication configuration required for\n\t// internal service to service calls being made to this service.\n\tServiceAuth ServiceAuth `json:\"service_auth\"`\n}\n\ntype SvcProtocol string\n\nconst (\n\tHttp SvcProtocol = \"http\"\n)\n\ntype ServiceAuth struct {\n\t// Method is the name of the authentication method.\n\tMethod string `json:\"method\"`\n}\n\n// UnsafeAllOriginWithCredentials can be used to specify that all origins are\n// allowed to call this API with credentials. It is unsafe and misuse can lead\n// to security issues. Only use if you know what you're doing.\nconst UnsafeAllOriginWithCredentials = \"UNSAFE_ALL_ORIGINS_WITH_CREDENTIALS\"\n\ntype CORS struct {\n\t// Debug enables debug logging of all requests passing through the CORS system\n\tDebug bool `json:\"debug\"`\n\n\t// DisableCredentials, if true, causes Encore to respond to OPTIONS requests\n\t// without setting Access-Control-Allow-Credentials: true.\n\tDisableCredentials bool `json:\"disable_credentials,omitempty\"`\n\n\t// AllowOriginsWithCredentials specifies the allowed origins for requests\n\t// that include credentials. If a request is made from an Origin in this list\n\t// Encore responds with Access-Control-Allow-Origin: <Origin>.\n\t// If DisableCredentials is true this field is not used.\n\t//\n\t// The URLs in this list may include wildcards (e.g. \"https://*.example.com\"\n\t// or \"https://*-myapp.example.com\").\n\tAllowOriginsWithCredentials []string `json:\"allow_origins_with_credentials,omitempty\"`\n\n\t// AllowOriginsWithoutCredentials specifies the allowed origins for requests\n\t// that don't include credentials. If nil it defaults to allowing all domains\n\t// (equivalent to []string{\"*\"}).\n\tAllowOriginsWithoutCredentials []string `json:\"allow_origins_without_credentials,omitempty\"`\n\n\t// ExtraAllowedHeaders specifies extra headers to allow, beyond\n\t// the default set always recognized by Encore.\n\t// As a special case, if the list contains \"*\" all headers are allowed.\n\tExtraAllowedHeaders []string `json:\"raw_allowed_headers,omitempty\"`\n\n\t// ExtraExposedHeaders specifies extra headers to expose, beyond\n\t// the default set always recognized by Encore.\n\t// As a special case, if the list contains \"*\" all headers are allowed.\n\tExtraExposedHeaders []string `json:\"raw_exposed_headers,omitempty\"`\n\n\t// AllowAccessWhenOnPrivateNetwork, if true, allows requests to Encore apps running\n\t// on private networks from websites.\n\t//\n\t// See: https://wicg.github.io/private-network-access/\n\tAllowPrivateNetworkAccess bool `json:\"allow_private_network_access,omitempty\"`\n}\n\ntype CommitInfo struct {\n\tRevision    string `json:\"revision\"`\n\tUncommitted bool   `json:\"uncommitted\"`\n}\n\nfunc (ci CommitInfo) AsRevisionString() string {\n\tif ci.Uncommitted {\n\t\treturn fmt.Sprintf(\"%s-modified\", ci.Revision)\n\t}\n\treturn ci.Revision\n}\n\nfunc (r *Runtime) Copy() *Runtime {\n\tcfg := *r\n\tcfg.AuthKeys = make([]EncoreAuthKey, len(r.AuthKeys))\n\tfor i, authKey := range r.AuthKeys {\n\t\tcfg.AuthKeys[i] = authKey.Copy()\n\t}\n\tcopy(cfg.SQLDatabases, r.SQLDatabases)\n\n\treturn &cfg\n}\n\n// EncoreCloudAPI is the configuration for the Encore Cloud API\n// which the runtime uses to communicate with infrastructure\n// services when running on Encore Cloud.\ntype EncoreCloudAPI struct {\n\tServer string `json:\"server\"` // The Encore Cloud server we're using\n\n\t// The auth keys to use when authenticating with the Encore Cloud API server, either for signing requests or\n\t// authenticating requests from the Encore Cloud API server.\n\t//\n\t// Note: these are not the same as the auth keys used to authenticate requests from Encore's central platform.\n\tAuthKeys []auth.Key `json:\"auth_keys\"`\n}\n\ntype EncoreAuthKey struct {\n\tKeyID uint32 `json:\"kid\"`\n\tData  []byte `json:\"data\"`\n}\n\nfunc (eak EncoreAuthKey) Copy() EncoreAuthKey {\n\tc := eak\n\tcopy(c.Data, eak.Data)\n\treturn c\n}\n\ntype PubsubProvider struct {\n\tNSQ         *NSQProvider               `json:\"nsq,omitempty\"`          // set if the provider is NSQ\n\tGCP         *GCPPubsubProvider         `json:\"gcp,omitempty\"`          // set if the provider is GCP\n\tAWS         *AWSPubsubProvider         `json:\"aws,omitempty\"`          // set if the provider is AWS\n\tAzure       *AzureServiceBusProvider   `json:\"azure,omitempty\"`        // set if the provider is Azure\n\tEncoreCloud *EncoreCloudPubsubProvider `json:\"encore_cloud,omitempty\"` // set if the provider is Encore Cloud\n}\n\ntype AzureServiceBusProvider struct {\n\tNamespace string `json:\"namespace\"`\n}\ntype NSQProvider struct {\n\tHost string `json:\"host\"`\n}\n\ntype EncoreCloudPubsubProvider struct{}\n\n// GCPPubsubProvider currently has no specific configuration.\ntype GCPPubsubProvider struct {\n}\n\n// AWSPubsubProvider currently has no specific configuration.\ntype AWSPubsubProvider struct {\n}\n\ntype PubsubTopic struct {\n\tEncoreName   string   `json:\"encore_name\"`       // the Encore name for the pubsub topic\n\tProviderID   int      `json:\"provider_id\"`       // The index into (*Runtime).PubsubProviders.\n\tProviderName string   `json:\"provider_name\"`     // the name for the pubsub topic as defined by the provider\n\tLimiter      *Limiter `json:\"limiter,omitempty\"` // the rate limiter for the topic\n\n\t// Subscriptions contains the subscriptions to this topic,\n\t// keyed by the Encore name.\n\tSubscriptions map[string]*PubsubSubscription `json:\"subscriptions\"`\n\n\t// GCP contains GCP-specific configuration.\n\t// It is set if the provider is GCP.\n\tGCP *PubsubTopicGCPData `json:\"gcp,omitempty\"`\n}\n\ntype PubsubSubscription struct {\n\tID           string `json:\"id\"`            // the subscription ID\n\tEncoreName   string `json:\"encore_name\"`   // the Encore name for the subscription\n\tProviderName string `json:\"provider_name\"` // the name for the pubsub subscription as defined by the provider\n\tPushOnly     bool   `json:\"push_only\"`     // if true the application will not actively subscribe to the pub, but instead will rely on HTTP push messages\n\n\t// GCP contains GCP-specific configuration.\n\t// It is set if the subscription exists in GCP.\n\tGCP *PubsubSubscriptionGCPData `json:\"gcp,omitempty\"`\n}\n\ntype PubsubTopicGCPData struct {\n\t// ProjectID is the GCP project id where the topic exists.\n\tProjectID string `json:\"project_id\"`\n}\n\ntype PubsubSubscriptionGCPData struct {\n\t// ProjectID is the GCP project id where the subscription exists.\n\tProjectID string `json:\"project_id\"`\n\n\t// PushServiceAccount is the service account used to authenticate\n\t// messages being delivered over push.\n\t// If empty pushes are not accepted.\n\tPushServiceAccount string `json:\"push_service_account\"`\n}\n\ntype StaticPubsubTopic struct {\n\tSubscriptions map[string]*StaticPubsubSubscription\n\tScrubPaths    []scrub.Path\n}\n\ntype StaticPubsubSubscription struct {\n\tService    string // the service that subscription is in\n\tSvcNum     uint16 // the service number the subscription is in\n\tTraceIdx   uint32 // The trace Idx of the subscription\n\tScrubPaths []scrub.Path\n}\n\ntype SQLServer struct {\n\t// Host is the host to connect to.\n\t// Valid formats are \"hostname\", \"hostname:port\", and \"/path/to/unix.socket\".\n\tHost string `json:\"host\"`\n\n\t// ServerCACert is the PEM-encoded server CA cert, or \"\" if not required.\n\tServerCACert string `json:\"server_ca_cert,omitempty\"`\n\t// ClientCert is the PEM-encoded client cert, or \"\" if not required.\n\tClientCert string `json:\"client_cert,omitempty\"`\n\t// ClientKey is the PEM-encoded client key, or \"\" if not required.\n\tClientKey string `json:\"client_key,omitempty\"`\n}\n\ntype SQLDatabase struct {\n\tServerID     int    `json:\"server_id\"`     // the index into (*Runtime).SQLServers\n\tEncoreName   string `json:\"encore_name\"`   // the Encore name for the database\n\tDatabaseName string `json:\"database_name\"` // the actual database name as known by the SQL server.\n\tUser         string `json:\"user\"`\n\tPassword     string `json:\"password\"`\n\n\t// MinConnections is the minimum number of open connections to use\n\t// for this database. If zero it defaults to 2.\n\tMinConnections int `json:\"min_connections\"`\n\n\t// MaxConnections is the maximum number of open connections to use\n\t// for this database. If zero it defaults to 30.\n\tMaxConnections int `json:\"max_connections\"`\n}\n\ntype RedisServer struct {\n\t// Host is the host to connect to.\n\t// Valid formats are \"hostname\", \"hostname:port\", and \"/path/to/unix.socket\".\n\tHost string `json:\"host\"`\n\n\t// User and password specify the authentication behavior to redis.\n\t// If both are provided, it uses Redis v6's ACL support.\n\t// If a password but no username is provided, it uses Redis's AUTH string support.\n\t// If neither is supplied it uses no authentication.\n\tUser     string `json:\"user,omitempty\"`\n\tPassword string `json:\"password,omitempty\"`\n\n\t// EnableTLS specifies whether or not to use TLS to connect.\n\t// If ServerCACert, ClientCert, or ClientKey are provided it is\n\t// automatically enabled regardless of the value.\n\tEnableTLS bool `json:\"enable_tls\"`\n\t// ServerCACert is the PEM-encoded server CA cert, or \"\" if not required.\n\tServerCACert string `json:\"server_ca_cert,omitempty\"`\n\t// ClientCert is the PEM-encoded client cert, or \"\" if not required.\n\tClientCert string `json:\"client_cert,omitempty\"`\n\t// ClientKey is the PEM-encoded client key, or \"\" if not required.\n\tClientKey string `json:\"client_key,omitempty\"`\n\n\t// InMemory tells the runtime to use an in-memory store\n\t// instead of connecting to this server.\n\tInMemory bool `json:\"in_memory\"`\n}\n\ntype RedisDatabase struct {\n\tServerID   int    `json:\"server_id\"`   // the index into (*Runtime).RedisServers\n\tEncoreName string `json:\"encore_name\"` // the Encore name for the database\n\n\t// Database is the database index to use, from 0-15.\n\tDatabase int `json:\"database\"`\n\n\t// MinConnections is the minimum number of open connections to use\n\t// for this database. It defaults to 1.\n\tMinConnections int `json:\"min_connections\"`\n\n\t// MaxConnections is the maximum number of open connections to use\n\t// for this database. If zero it defaults to 10*GOMAXPROCS.\n\tMaxConnections int `json:\"max_connections\"`\n\n\t// KeyPrefix specifies a prefix to add to all cache keys\n\t// for this database. It exists to enable multiple cache clusters\n\t// to use the same physical Redis database for local development\n\t// without having to coordinate and persist database index ids.\n\tKeyPrefix string `json:\"key_prefix\"`\n}\n\ntype BucketProvider struct {\n\tS3  *S3BucketProvider  `json:\"s3,omitempty\"`  // set if the provider is S3\n\tGCS *GCSBucketProvider `json:\"gcs,omitempty\"` // set if the provider is GCS\n}\n\ntype S3BucketProvider struct {\n\tRegion string `json:\"region\"`\n\t// The endpoint to use. If nil, the default endpoint for the region is used.\n\t// Must be set for non-AWS endpoints.\n\tEndpoint *string `json:\"endpoint\"`\n\n\t// The access key to use. If either is nil, the default credentials are used.\n\tAccessKeyID     *string `json:\"access_key_id\"`\n\tSecretAccessKey *string `json:\"secret_access_key\"`\n}\n\ntype GCSBucketProvider struct {\n\tEndpoint  string `json:\"endpoint\"`\n\tAnonymous bool   `json:\"anonymous\"`\n\n\t// Additional options for signed URLs when running in local dev mode.\n\t// Only use with anonymous mode.\n\tLocalSign *GCSLocalSignOptions `json:\"local_sign,omitempty\"`\n}\n\ntype GCSLocalSignOptions struct {\n\tBaseURL    string `json:\"base_url\"`\n\tAccessID   string `json:\"access_id\"`\n\tPrivateKey string `json:\"private_key\"`\n}\n\ntype Bucket struct {\n\tProviderID int    `json:\"cluster_id\"`  // the index into (*Runtime).BucketProviders\n\tEncoreName string `json:\"encore_name\"` // the Encore name for the bucket\n\tCloudName  string `json:\"cloud_name\"`  // the cloud name for the bucket\n\tKeyPrefix  string `json:\"key_prefix\"`  // the prefix to use for all keys in the bucket\n\n\t// The public base url for the bucket.\n\t// Only set if the bucket is public.\n\tPublicBaseURL string `json:\"public_base_url\"`\n}\n\ntype Metrics struct {\n\tCollectionInterval time.Duration                  `json:\"collection_interval,omitempty\"`\n\tEncoreCloud        *GCPCloudMonitoringProvider    `json:\"encore_cloud,omitempty\"`\n\tCloudMonitoring    *GCPCloudMonitoringProvider    `json:\"gcp_cloud_monitoring,omitempty\"`\n\tCloudWatch         *AWSCloudWatchMetricsProvider  `json:\"aws_cloud_watch,omitempty\"`\n\tLogsBased          *LogsBasedMetricsProvider      `json:\"logs_based,omitempty\"`\n\tPrometheus         *PrometheusRemoteWriteProvider `json:\"prometheus,omitempty\"`\n\tDatadog            *DatadogProvider               `json:\"datadog,omitempty\"`\n}\n\ntype GCPCloudMonitoringProvider struct {\n\t// ProjectID is the GCP project id to send metrics to.\n\tProjectID string\n\n\t// MonitoredResourceType is the enum value for the monitored resource this application is monitoring.\n\t// See https://cloud.google.com/monitoring/api/resources for valid values.\n\tMonitoredResourceType string\n\t// MonitoredResourceLabels are the labels to specify for the monitored resource.\n\t// Each monitored resource type has a pre-defined set of labels that must be set.\n\t// See https://cloud.google.com/monitoring/api/resources for expected labels.\n\tMonitoredResourceLabels map[string]string\n\n\t// MetricNames contains the mapping between metric names in Encore and metric\n\t// names in GCP.\n\tMetricNames map[string]string\n}\n\ntype AWSCloudWatchMetricsProvider struct {\n\t// Namespace is the namespace to use for metrics.\n\tNamespace string\n}\n\ntype PrometheusRemoteWriteProvider struct {\n\t// The URL of the endpoint to send samples to.\n\tRemoteWriteURL string\n}\n\ntype DatadogProvider struct {\n\tSite   string\n\tAPIKey string\n}\n\ntype LogsBasedMetricsProvider struct{}\n\n// Limiter represents a rate limiter that can be used for certain types of operations\n//\n// The fields are mutually exclusive, which ever is not nil is the limiter that will be used,\n// if all are nil, then no limit is enforced.\ntype Limiter struct {\n\tTokenBucket *TokenBucketLimiter `json:\"token_bucket\"` // A token bucket limiter\n}\n\ntype TokenBucketLimiter struct {\n\tPerSecondRate float64 `json:\"rate\"` // The rate at which to allow requests to pass through.\n\tBucketSize    int     `json:\"size\"` // The size of the token bucket (starts full)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/infra/config.go",
    "content": "package infra\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n)\n\ntype InfraConfig struct {\n\tMetadata         Metadata                     `json:\"metadata,omitempty\"`\n\tGracefulShutdown *GracefulShutdown            `json:\"graceful_shutdown,omitempty\"`\n\tAuth             []*Auth                      `json:\"auth,omitempty\"`\n\tServiceDiscovery map[string]*ServiceDiscovery `json:\"service_discovery,omitempty\"`\n\tMetrics          *Metrics                     `json:\"metrics,omitempty\"`\n\tSQLServers       []*SQLServer                 `json:\"sql_servers,omitempty\"`\n\tRedis            map[string]*Redis            `json:\"redis,omitempty\"`\n\tPubSub           []*PubSub                    `json:\"pubsub,omitempty\"`\n\tSecrets          Secrets                      `json:\"secrets,omitempty\"`\n\tObjectStorage    []*ObjectStorage             `json:\"object_storage,omitempty\"`\n\n\t// Log configuration for the application.\n\t// If empty it defaults to \"trace\".\n\tLogConfig string `json:\"log_config,omitemty\"`\n\n\t// Number of worker threads to use for the application.\n\t// If unset it defaults to a single worker thread.\n\t// If set to 0 it defaults to the number of CPUs.\n\tWorkerThreads *int `json:\"worker_threads,omitempty\"`\n\n\t// These fields are not defined in the json schema and should not be\n\t// set by the user. They're computed during the build/eject process.\n\tHostedServices []string `json:\"hosted_services,omitempty\"`\n\tHostedGateways []string `json:\"hosted_gateways,omitempty\"`\n\tCORS           *CORS    `json:\"cors,omitempty\"`\n}\n\ntype ObjectStorage struct {\n\tType string `json:\"type\"`\n\tGCS  *GCS   `json:\"gcs,omitempty\"`\n\tS3   *S3    `json:\"s3,omitempty\"`\n}\n\nfunc (o *ObjectStorage) GetBuckets() map[string]*Bucket {\n\tswitch o.Type {\n\tcase \"gcs\":\n\t\treturn o.GCS.Buckets\n\tcase \"s3\":\n\t\treturn o.S3.Buckets\n\tdefault:\n\t\tpanic(\"unsupported object storage type\")\n\t}\n}\n\nfunc (o *ObjectStorage) DeleteBucket(name string) {\n\tswitch o.Type {\n\tcase \"gcs\":\n\t\tdelete(o.GCS.Buckets, name)\n\tcase \"s3\":\n\t\tdelete(o.S3.Buckets, name)\n\tdefault:\n\t\tpanic(\"unsupported object storage type\")\n\t}\n\n}\n\nfunc (a *ObjectStorage) Validate(v *validator) {\n\tv.ValidateField(\"Type\", OneOf(a.Type, \"gcs\", \"s3\"))\n\tswitch a.Type {\n\tcase \"gcs\":\n\t\ta.GCS.Validate(v)\n\tcase \"s3\":\n\t\ta.S3.Validate(v)\n\tdefault:\n\t\tv.ValidateField(\"type\", Err(\"unsupported object storage type\"))\n\t}\n}\n\nfunc (p *ObjectStorage) MarshalJSON() ([]byte, error) {\n\t// Create a map to hold the JSON structure\n\tm := make(map[string]interface{})\n\n\t// Always add the type\n\tm[\"type\"] = p.Type\n\n\t// Add the specific object storage configuration based on the type\n\tswitch p.Type {\n\tcase \"gcs\":\n\t\tif p.GCS != nil {\n\t\t\tfor k, v := range structToMap(p.GCS) {\n\t\t\t\tm[k] = v\n\t\t\t}\n\t\t}\n\tcase \"s3\":\n\t\tif p.S3 != nil {\n\t\t\tfor k, v := range structToMap(p.S3) {\n\t\t\t\tm[k] = v\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported object storage type\")\n\t}\n\n\treturn json.Marshal(m)\n}\n\n// UnmarshalJSON custom unmarshaller for PubSub.\nfunc (p *ObjectStorage) UnmarshalJSON(data []byte) error {\n\t// Anonymous struct to capture the \"type\" field first.\n\tvar aux struct {\n\t\tType string `json:\"type,omitempty\"`\n\t}\n\tif err := json.Unmarshal(data, &aux); err != nil {\n\t\treturn err\n\t}\n\n\t// Set the Type field.\n\tp.Type = aux.Type\n\n\t// Unmarshal based on the \"type\" field.\n\tswitch aux.Type {\n\tcase \"gcs\":\n\t\tvar g GCS\n\t\tif err := json.Unmarshal(data, &g); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.GCS = &g\n\tcase \"s3\":\n\t\tvar a S3\n\t\tif err := json.Unmarshal(data, &a); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.S3 = &a\n\tdefault:\n\t\treturn errors.New(\"unsupported object storage type\")\n\t}\n\n\treturn nil\n}\n\ntype S3 struct {\n\tRegion   string `json:\"region\"`\n\tEndpoint string `json:\"endpoint,omitempty\"`\n\n\tAccessKeyID     string    `json:\"access_key_id,omitempty\"`\n\tSecretAccessKey EnvString `json:\"secret_access_key,omitempty\"`\n\n\tBuckets map[string]*Bucket `json:\"buckets,omitempty\"`\n}\n\nfunc (a *S3) Validate(v *validator) {\n\tv.ValidateField(\"region\", NotZero(a.Region))\n\tif a.AccessKeyID != \"\" {\n\t\tv.ValidatePtrEnvRef(\"secret_access_key\", &a.SecretAccessKey, \"S3 Secret Access Key\", NotZero[string])\n\t}\n\tValidateChildMap(v, \"buckets\", a.Buckets)\n}\n\ntype GCS struct {\n\tEndpoint string             `json:\"endpoint,omitempty\"`\n\tBuckets  map[string]*Bucket `json:\"buckets,omitempty\"`\n}\n\nfunc (a *GCS) Validate(v *validator) {\n\tValidateChildMap(v, \"buckets\", a.Buckets)\n}\n\ntype Bucket struct {\n\tName          string `json:\"name,omitempty\"`\n\tKeyPrefix     string `json:\"key_prefix,omitempty\"`\n\tPublicBaseURL string `json:\"public_base_url,omitempty\"`\n}\n\nfunc (a *Bucket) Validate(v *validator) {\n\tv.ValidateField(\"name\", NotZero(a.Name))\n\n\tv.ValidateField(\"public_base_url\", func() error {\n\t\tif a.PublicBaseURL != \"\" {\n\t\t\tif _, err := url.Parse(a.PublicBaseURL); err != nil {\n\t\t\t\treturn fmt.Errorf(\"Not a valid URL: %v\", err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\ntype Metadata struct {\n\tAppID   string `json:\"app_id,omitempty\"`\n\tEnvName string `json:\"env_name,omitempty\"`\n\tEnvType string `json:\"env_type,omitempty\"`\n\tCloud   string `json:\"cloud,omitempty\"`\n\tBaseURL string `json:\"base_url,omitempty\"`\n}\n\n// Copy of the CORS struct from the appfile\ntype CORS struct {\n\tDebug                          bool     `json:\"debug,omitempty\"`\n\tAllowHeaders                   []string `json:\"allow_headers,omitempty\"`\n\tExposeHeaders                  []string `json:\"expose_headers,omitempty\"`\n\tAllowOriginsWithoutCredentials []string `json:\"allow_origins_without_credentials,omitempty\"`\n\tAllowOriginsWithCredentials    []string `json:\"allow_origins_with_credentials,omitempty\"`\n}\n\nfunc (i *InfraConfig) Validate(v *validator) {\n\tv.ValidateChild(\"graceful_shutdown\", i.GracefulShutdown)\n\tValidateChildList(v, \"auth\", i.Auth)\n\tValidateChildMap(v, \"service_discovery\", i.ServiceDiscovery)\n\tValidateChildList(v, \"object_storage\", i.ObjectStorage)\n\tv.ValidateChild(\"metrics\", i.Metrics)\n\tValidateChildList(v, \"sql_servers\", i.SQLServers)\n\tValidateChildMap(v, \"redis\", i.Redis)\n\tValidateChildList(v, \"pubsub\", i.PubSub)\n\tv.ValidateChild(\"secrets\", i.Secrets)\n}\n\ntype Secrets struct {\n\tSecretsMap map[string]EnvString\n\tEnvRef     *EnvRef\n}\n\nfunc (s Secrets) Validate(v *validator) {\n\tif s.EnvRef != nil {\n\t\tv.ValidateEnvRef(\"env_ref\", *s.EnvRef, \"An environment variable containing a JSON object of secrets\")\n\t\treturn\n\t}\n\tfor name, value := range s.SecretsMap {\n\t\tv.ValidateEnvString(name, value, \"Secret\", nil)\n\t}\n}\n\nfunc (s *Secrets) GetSecrets() map[string]string {\n\tif s.EnvRef != nil {\n\t\trefs := make(map[string]string)\n\t\tenvValue := os.Getenv(s.EnvRef.Env)\n\t\tif err := json.Unmarshal([]byte(envValue), &refs); err != nil {\n\t\t\tlog.Fatalf(\"Error unmarshalling secrets\")\n\t\t}\n\t\treturn refs\n\t}\n\treturn MapValues(s.SecretsMap, func(k string, v EnvString) string {\n\t\treturn v.Value()\n\t})\n}\n\n// UnmarshalJSON is a custom JSON unmarshaller for the Secrets type.\nfunc (s *Secrets) UnmarshalJSON(data []byte) error {\n\t// Try unmarshalling as an EnvRef.\n\tvar ref EnvRef\n\tif err := json.Unmarshal(data, &ref); err == nil && ref.Env != \"\" {\n\t\ts.EnvRef = &ref\n\t\treturn nil\n\t}\n\n\t// Try unmarshalling as a map of strings to EnvString.\n\tvar m map[string]EnvString\n\tif err := json.Unmarshal(data, &m); err == nil {\n\t\ts.SecretsMap = m\n\t\treturn nil\n\t}\n\treturn errors.New(\"invalid Secrets structure\")\n}\n\n// MarshalJSON is a custom JSON marshaller for the Secrets type.\nfunc (s Secrets) MarshalJSON() ([]byte, error) {\n\tif s.EnvRef == nil {\n\t\treturn json.Marshal(s.SecretsMap)\n\t}\n\treturn json.Marshal(s.EnvRef)\n}\n\ntype GracefulShutdown struct {\n\tTotal         *int `json:\"total,omitempty\"`\n\tShutdownHooks *int `json:\"shutdown_hooks,omitempty\"`\n\tHandlers      *int `json:\"handlers,omitempty\"`\n}\n\nfunc (g *GracefulShutdown) Validate(v *validator) {\n\tv.ValidateField(\"total\", NilOr(g.Total, GreaterOrEqual(0)))\n\tv.ValidateField(\"shutdown_hooks\", NilOr(g.ShutdownHooks, GreaterOrEqual(0)))\n\tv.ValidateField(\"handlers\", NilOr(g.Handlers, GreaterOrEqual(0)))\n}\n\ntype Auth struct {\n\tType string    `json:\"type,omitempty\"`\n\tID   int       `json:\"id,omitempty\"`\n\tKey  EnvString `json:\"key,omitempty\"`\n}\n\nfunc (a *Auth) Validate(v *validator) {\n\tv.ValidateField(\"type\", OneOf(a.Type, \"key\"))\n\tv.ValidateEnvString(\"key\", a.Key, \"Service Authorization Key\", NotZero[string])\n}\n\ntype ServiceDiscovery struct {\n\tBaseURL string  `json:\"base_url,omitempty\"`\n\tAuth    []*Auth `json:\"auth,omitempty\"`\n}\n\nfunc (s *ServiceDiscovery) Validate(v *validator) {\n\tv.ValidateField(\"base_url\", NotZero(s.BaseURL))\n\tValidateChildList(v, \"auth\", s.Auth)\n}\n\n// Main Metrics struct which embeds the different metric types.\ntype Metrics struct {\n\tType               string `json:\"type,omitempty\"`\n\tCollectionInterval int    `json:\"collection_interval,omitempty\"`\n\tPrometheus         *Prometheus\n\tDatadog            *Datadog\n\tGCPCloudMonitoring *GCPCloudMonitoring\n\tAWSCloudWatch      *AWSCloudWatch\n}\n\n// MarshalJSON custom marshaller to handle dynamic types in Metrics.\nfunc (m *Metrics) MarshalJSON() ([]byte, error) {\n\t// Create a map to hold the JSON structure\n\tdata := make(map[string]interface{})\n\n\tdata[\"type\"] = m.Type\n\tdata[\"collection_interval\"] = m.CollectionInterval\n\n\tswitch m.Type {\n\tcase \"prometheus\":\n\t\tif m.Prometheus != nil {\n\t\t\tfor k, v := range structToMap(m.Prometheus) {\n\t\t\t\tdata[k] = v\n\t\t\t}\n\t\t}\n\tcase \"datadog\":\n\t\tif m.Datadog != nil {\n\t\t\tfor k, v := range structToMap(m.Datadog) {\n\t\t\t\tdata[k] = v\n\t\t\t}\n\t\t}\n\tcase \"gcp_cloud_monitoring\":\n\t\tif m.GCPCloudMonitoring != nil {\n\t\t\tfor k, v := range structToMap(m.GCPCloudMonitoring) {\n\t\t\t\tdata[k] = v\n\t\t\t}\n\t\t}\n\tcase \"aws_cloudwatch\":\n\t\tif m.AWSCloudWatch != nil {\n\t\t\tfor k, v := range structToMap(m.AWSCloudWatch) {\n\t\t\t\tdata[k] = v\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported metrics type\")\n\t}\n\n\treturn json.Marshal(data)\n}\n\n// UnmarshalJSON custom unmarshaller to handle dynamic types in Metrics.\nfunc (m *Metrics) UnmarshalJSON(data []byte) error {\n\t// Anonymous struct to capture the base fields first\n\tvar aux struct {\n\t\tType               string `json:\"type,omitempty\"`\n\t\tCollectionInterval int    `json:\"collection_interval,omitempty\"`\n\t}\n\tif err := json.Unmarshal(data, &aux); err != nil {\n\t\treturn err\n\t}\n\n\t// Set the base fields\n\tm.Type = aux.Type\n\tm.CollectionInterval = aux.CollectionInterval\n\n\t// Unmarshal based on the \"type\" field\n\tswitch aux.Type {\n\tcase \"prometheus\":\n\t\tvar p Prometheus\n\t\tif err := json.Unmarshal(data, &p); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.Prometheus = &p\n\tcase \"datadog\":\n\t\tvar d Datadog\n\t\tif err := json.Unmarshal(data, &d); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.Datadog = &d\n\tcase \"gcp_cloud_monitoring\":\n\t\tvar g GCPCloudMonitoring\n\t\tif err := json.Unmarshal(data, &g); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.GCPCloudMonitoring = &g\n\tcase \"aws_cloudwatch\":\n\t\tvar a AWSCloudWatch\n\t\tif err := json.Unmarshal(data, &a); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.AWSCloudWatch = &a\n\tdefault:\n\t\treturn errors.New(\"unsupported metrics type\")\n\t}\n\n\treturn nil\n}\n\nfunc (m *Metrics) Validate(v *validator) {\n\tswitch m.Type {\n\tcase \"prometheus\":\n\t\tm.Prometheus.Validate(v)\n\tcase \"datadog\":\n\t\tm.Datadog.Validate(v)\n\tcase \"gcp_cloud_monitoring\":\n\t\tm.GCPCloudMonitoring.Validate(v)\n\tcase \"aws_cloudwatch\":\n\t\tm.AWSCloudWatch.Validate(v)\n\tdefault:\n\t\tv.ValidateField(\"type\", Err(\"unsupported metrics type\"))\n\t}\n}\n\n// Prometheus-specific metric configuration.\ntype Prometheus struct {\n\tRemoteWriteURL EnvString `json:\"remote_write_url,omitempty\"`\n}\n\nfunc (p *Prometheus) Validate(v *validator) {\n\tv.ValidateEnvString(\"remote_write_url\", p.RemoteWriteURL, \"Prometheus Remote Write URL\", NotZero[string])\n}\n\n// Datadog-specific metric configuration.\ntype Datadog struct {\n\tSite   string    `json:\"site,omitempty\"`\n\tAPIKey EnvString `json:\"api_key,omitempty\"`\n}\n\nfunc (d *Datadog) Validate(v *validator) {\n\tv.ValidateField(\"site\", NotZero(d.Site))\n\tv.ValidateEnvString(\"api_key\", d.APIKey, \"Datadog API Key\", NotZero[string])\n}\n\n// GCP Cloud Monitoring-specific metric configuration.\ntype GCPCloudMonitoring struct {\n\tProjectID               string            `json:\"project_id,omitempty\"`\n\tMonitoredResourceType   string            `json:\"monitored_resource_type,omitempty\"`\n\tMonitoredResourceLabels map[string]string `json:\"monitored_resource_labels,omitempty\"`\n\tMetricNames             map[string]string `json:\"metric_names,omitempty\"`\n}\n\nfunc (g *GCPCloudMonitoring) Validate(v *validator) {\n\tv.ValidateField(\"project_id\", NotZero(g.ProjectID))\n\tv.ValidateField(\"monitored_resource_type\", NotZero(g.MonitoredResourceType))\n}\n\n// AWS CloudWatch-specific metric configuration.\ntype AWSCloudWatch struct {\n\tNamespace string `json:\"namespace,omitempty\"`\n}\n\nfunc (a *AWSCloudWatch) Validate(v *validator) {\n\tv.ValidateField(\"namespace\", NotZero(a.Namespace))\n}\n\ntype SQLServer struct {\n\tHost      string                  `json:\"host,omitempty\"`\n\tTLSConfig *TLSConfig              `json:\"tls_config,omitempty\"`\n\tDatabases map[string]*SQLDatabase `json:\"databases,omitempty\"`\n}\n\nfunc (s *SQLServer) Validate(v *validator) {\n\tv.ValidateField(\"host\", NotZero(s.Host))\n\tv.ValidateChild(\"tls_config\", s.TLSConfig)\n\tValidateChildMap(v, \"databases\", s.Databases)\n}\n\ntype TLSConfig struct {\n\tCA                             string      `json:\"ca,omitempty\"`\n\tClientCert                     *ClientCert `json:\"client_cert,omitempty\"`\n\tDisableTLSHostnameVerification bool        `json:\"disable_tls_hostname_verification,omitempty\"`\n\tDisableCAValidation            bool        `json:\"disable_ca_validation,omitempty\"`\n}\n\nfunc (t *TLSConfig) Validate(v *validator) {\n\tv.ValidateChild(\"client_cert\", t.ClientCert)\n}\n\ntype SQLDatabase struct {\n\tName           string      `json:\"name,omitempty\"`\n\tMaxConnections int         `json:\"max_connections,omitempty\"`\n\tMinConnections int         `json:\"min_connections,omitempty\"`\n\tUsername       EnvString   `json:\"username,omitempty\"`\n\tPassword       EnvString   `json:\"password,omitempty\"`\n\tClientCert     *ClientCert `json:\"client_cert,omitempty\"`\n}\n\nfunc (s *SQLDatabase) Validate(v *validator) {\n\tv.ValidateField(\"max_connections\", GreaterOrEqual(s.MinConnections)(s.MaxConnections))\n\tv.ValidateField(\"min_connections\", GreaterOrEqual(0)(s.MinConnections))\n\tv.ValidateEnvString(\"username\", s.Username, \"Database Username\", NotZero[string])\n\tv.ValidateEnvString(\"password\", s.Password, \"Database Password\", NotZero[string])\n\tv.ValidateChild(\"client_cert\", s.ClientCert)\n}\n\ntype Redis struct {\n\tHost           string     `json:\"host,omitempty\"`\n\tDatabaseIndex  int        `json:\"database_index,omitempty\"`\n\tAuth           *RedisAuth `json:\"auth,omitempty\"`\n\tKeyPrefix      *string    `json:\"key_prefix,omitempty\"`\n\tTLSConfig      *TLSConfig `json:\"tls_config,omitempty\"`\n\tMaxConnections *int       `json:\"max_connections,omitempty\"`\n\tMinConnections *int       `json:\"min_connections,omitempty\"`\n\tInMemory       bool       `json:\"in_memory,omitempty\"`\n}\n\nfunc (r *Redis) Validate(v *validator) {\n\tv.ValidateField(\"host\", NotZero(r.Host))\n\tv.ValidateField(\"database_index\", Between(0, 15)(r.DatabaseIndex))\n\tv.ValidateChild(\"auth\", r.Auth)\n\tv.ValidateChild(\"tls_config\", r.TLSConfig)\n\tv.ValidateField(\"max_connections\", NilOr(r.MaxConnections, GreaterOrEqual(0)))\n\tv.ValidateField(\"min_connections\", NilOr(r.MinConnections, GreaterOrEqual(0)))\n}\n\ntype RedisAuth struct {\n\tType       string     `json:\"type,omitempty\"`\n\tUsername   *EnvString `json:\"username,omitempty\"`\n\tPassword   *EnvString `json:\"password,omitempty\"`\n\tAuthString *EnvString `json:\"auth_string,omitempty\"`\n}\n\nfunc (r *RedisAuth) Validate(v *validator) {\n\tv.ValidateField(\"type\", NotZero(r.Type))\n\tswitch r.Type {\n\tcase \"acl\":\n\t\tv.ValidatePtrEnvRef(\"username\", r.Username, \"Redis Username\", NotZero[string])\n\t\tv.ValidatePtrEnvRef(\"password\", r.Password, \"Redis Password\", NotZero[string])\n\tcase \"auth_string\":\n\t\tv.ValidatePtrEnvRef(\"auth_string\", r.AuthString, \"Redis Auth String\", NotZero[string])\n\tdefault:\n\t\tv.ValidateField(\"type\", Err(\"unsupported Redis auth type\"))\n\t}\n}\n\ntype ClientCert struct {\n\tCert string    `json:\"cert,omitempty\"`\n\tKey  EnvString `json:\"key,omitempty\"`\n}\n\nfunc (c *ClientCert) Validate(v *validator) {\n\tv.ValidateField(\"cert\", NotZero(c.Cert))\n\tv.ValidateEnvString(\"key\", c.Key, \"Client Certificate Key\", NotZero[string])\n}\n\n// Main PubSub struct which embeds different PubSub types.\ntype PubSub struct {\n\tType string `json:\"type,omitempty\"`\n\tGCP  *GCPPubsub\n\tAWS  *AWSSNS_SQS\n\tNSQ  *NSQPubsub\n}\n\nfunc (p *PubSub) Validate(v *validator) {\n\tswitch p.Type {\n\tcase \"gcp_pubsub\":\n\t\tp.GCP.Validate(v)\n\tcase \"aws_sns_sqs\":\n\t\tp.AWS.Validate(v)\n\tcase \"nsq\":\n\t\tp.NSQ.Validate(v)\n\tdefault:\n\t\tv.ValidateField(\"type\", Err(\"unsupported pubsub type\"))\n\t}\n}\n\nfunc (p *PubSub) DeleteTopic(name string) {\n\tswitch p.Type {\n\tcase \"gcp_pubsub\":\n\t\tp.GCP.DeleteTopic(name)\n\tcase \"aws_sns_sqs\":\n\t\tp.AWS.DeleteTopic(name)\n\tcase \"nsq\":\n\t\tp.NSQ.DeleteTopic(name)\n\t}\n}\n\nfunc (p *PubSub) GetTopics() map[string]PubsubTopic {\n\tswitch p.Type {\n\tcase \"gcp_pubsub\":\n\t\treturn p.GCP.GetTopics()\n\tcase \"aws_sns_sqs\":\n\t\treturn p.AWS.GetTopics()\n\tcase \"nsq\":\n\t\treturn p.NSQ.GetTopics()\n\tdefault:\n\t\tpanic(\"unsupported pubsub type\")\n\t}\n}\n\ntype PubsubTopic interface {\n\tGetSubscriptions() map[string]PubsubSubscription\n\tDeleteSubscription(name string)\n}\n\ntype PubsubSubscription interface{}\n\ntype PubsubProvider interface {\n\tGetTopics() map[string]PubsubTopic\n\tDeleteTopic(name string)\n}\n\n// GCPPubsub specific configuration.\ntype GCPPubsub struct {\n\tProjectID string               `json:\"project_id,omitempty\"`\n\tTopics    map[string]*GCPTopic `json:\"topics,omitempty\"`\n}\n\nfunc (g *GCPPubsub) Validate(v *validator) {\n\tValidateChildMap(v, \"topics\", g.Topics)\n}\n\nfunc (g *GCPPubsub) GetTopics() map[string]PubsubTopic {\n\treturn MapValues(g.Topics, func(k string, v *GCPTopic) PubsubTopic {\n\t\treturn v\n\t})\n}\n\nfunc (g *GCPPubsub) DeleteTopic(name string) {\n\tdelete(g.Topics, name)\n}\n\ntype GCPTopic struct {\n\tName          string             `json:\"name,omitempty\"`\n\tProjectID     string             `json:\"project_id,omitempty\"`\n\tSubscriptions map[string]*GCPSub `json:\"subscriptions,omitempty\"`\n}\n\nfunc (g *GCPTopic) Validate(v *validator) {\n\tv.ValidateField(\"name\", NotZero(g.Name))\n\tpubsub := Ancestor[*PubSub](v)\n\tv.ValidateField(\"project_id\", AnyNonZero(g.ProjectID, pubsub.GCP.ProjectID))\n\tValidateChildMap(v, \"subscriptions\", g.Subscriptions)\n}\n\nfunc (g *GCPTopic) GetSubscriptions() map[string]PubsubSubscription {\n\treturn MapValues(g.Subscriptions, func(k string, v *GCPSub) PubsubSubscription {\n\t\treturn v\n\t})\n}\n\nfunc (g *GCPTopic) DeleteSubscription(name string) {\n\tdelete(g.Subscriptions, name)\n}\n\ntype GCPSub struct {\n\tName       string      `json:\"name,omitempty\"`\n\tProjectID  string      `json:\"project_id,omitempty\"`\n\tPushConfig *PushConfig `json:\"push_config,omitempty\"`\n}\n\nfunc (g *GCPSub) Validate(v *validator) {\n\tv.ValidateField(\"name\", NotZero(g.Name))\n\tpubsub := Ancestor[*PubSub](v)\n\tv.ValidateField(\"project_id\", AnyNonZero(g.ProjectID, pubsub.GCP.ProjectID))\n\tv.ValidateChild(\"push_config\", g.PushConfig)\n}\n\ntype PushConfig struct {\n\tServiceAccount string `json:\"service_account,omitempty\"`\n\tJWTAudience    string `json:\"jwt_audience,omitempty\"`\n\tID             string `json:\"id,omitempty\"`\n}\n\nfunc (p *PushConfig) Validate(v *validator) {\n\tv.ValidateField(\"service_account\", NotZero(p.ServiceAccount))\n\tv.ValidateField(\"jwt_audience\", NotZero(p.JWTAudience))\n\tv.ValidateField(\"id\", NotZero(p.ID))\n}\n\n// AWSSNS_SQS specific configuration.\ntype AWSSNS_SQS struct {\n\tTopics map[string]*AWSTopic `json:\"topics,omitempty\"`\n}\n\nfunc (a *AWSSNS_SQS) Validate(v *validator) {\n\tValidateChildMap(v, \"topics\", a.Topics)\n}\n\nfunc (a *AWSSNS_SQS) GetTopics() map[string]PubsubTopic {\n\treturn MapValues(a.Topics, func(k string, v *AWSTopic) PubsubTopic {\n\t\treturn v\n\t})\n}\n\nfunc (a *AWSSNS_SQS) DeleteTopic(name string) {\n\tdelete(a.Topics, name)\n}\n\ntype AWSTopic struct {\n\tARN           string             `json:\"arn,omitempty\"`\n\tSubscriptions map[string]*AWSSub `json:\"subscriptions,omitempty\"`\n}\n\nfunc (a *AWSTopic) Validate(v *validator) {\n\tv.ValidateField(\"arn\", NotZero(a.ARN))\n\tValidateChildMap(v, \"subscriptions\", a.Subscriptions)\n}\n\nfunc (a *AWSTopic) GetSubscriptions() map[string]PubsubSubscription {\n\treturn MapValues(a.Subscriptions, func(k string, v *AWSSub) PubsubSubscription {\n\t\treturn v\n\t})\n}\n\nfunc (a *AWSTopic) DeleteSubscription(name string) {\n\tdelete(a.Subscriptions, name)\n}\n\ntype AWSSub struct {\n\tURL string `json:\"url,omitempty\"`\n}\n\nfunc (a *AWSSub) Validate(v *validator) {\n\tv.ValidateField(\"url\", NotZero(a.URL))\n}\n\n// NSQPubsub specific configuration.\ntype NSQPubsub struct {\n\tHosts  string               `json:\"hosts,omitempty\"`\n\tTopics map[string]*NSQTopic `json:\"topics,omitempty\"`\n}\n\nfunc (n *NSQPubsub) Validate(v *validator) {\n\tv.ValidateField(\"hosts\", NotZero(n.Hosts))\n\tValidateChildMap(v, \"topics\", n.Topics)\n}\n\nfunc (n *NSQPubsub) GetTopics() map[string]PubsubTopic {\n\treturn MapValues(n.Topics, func(k string, v *NSQTopic) PubsubTopic {\n\t\treturn v\n\t})\n}\n\nfunc (n *NSQPubsub) DeleteTopic(name string) {\n\tdelete(n.Topics, name)\n}\n\ntype NSQTopic struct {\n\tName          string             `json:\"name,omitempty\"`\n\tSubscriptions map[string]*NSQSub `json:\"subscriptions,omitempty\"`\n}\n\nfunc (n *NSQTopic) Validate(v *validator) {\n\tv.ValidateField(\"name\", NotZero(n.Name))\n\tValidateChildMap(v, \"subscriptions\", n.Subscriptions)\n}\n\nfunc (n *NSQTopic) GetSubscriptions() map[string]PubsubSubscription {\n\treturn MapValues(n.Subscriptions, func(k string, v *NSQSub) PubsubSubscription {\n\t\treturn v\n\t})\n}\n\nfunc (n *NSQTopic) DeleteSubscription(name string) {\n\tdelete(n.Subscriptions, name)\n}\n\ntype NSQSub struct {\n\tName string `json:\"name,omitempty\"`\n}\n\nfunc (n *NSQSub) Validate(v *validator) {\n\tv.ValidateField(\"name\", NotZero(n.Name))\n}\n\n// MarshalJSON custom marshaller for PubSub.\nfunc (p *PubSub) MarshalJSON() ([]byte, error) {\n\t// Create a map to hold the JSON structure\n\tm := make(map[string]interface{})\n\n\t// Always add the type\n\tm[\"type\"] = p.Type\n\n\t// Add the specific pubsub configuration based on the type\n\tswitch p.Type {\n\tcase \"gcp_pubsub\":\n\t\tif p.GCP != nil {\n\t\t\tfor k, v := range structToMap(p.GCP) {\n\t\t\t\tm[k] = v\n\t\t\t}\n\t\t}\n\tcase \"aws_sns_sqs\":\n\t\tif p.AWS != nil {\n\t\t\tfor k, v := range structToMap(p.AWS) {\n\t\t\t\tm[k] = v\n\t\t\t}\n\t\t}\n\tcase \"nsq\":\n\t\tif p.NSQ != nil {\n\t\t\tfor k, v := range structToMap(p.NSQ) {\n\t\t\t\tm[k] = v\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported pubsub type\")\n\t}\n\n\treturn json.Marshal(m)\n}\n\n// structToMap converts a struct to a map[string]interface{}\n// It uses json.Marshal and json.Unmarshal to avoid reflection\nfunc structToMap(v interface{}) map[string]interface{} {\n\tdata, _ := json.Marshal(v)\n\tvar m map[string]interface{}\n\t_ = json.Unmarshal(data, &m)\n\treturn m\n}\n\n// UnmarshalJSON custom unmarshaller for PubSub.\nfunc (p *PubSub) UnmarshalJSON(data []byte) error {\n\t// Anonymous struct to capture the \"type\" field first.\n\tvar aux struct {\n\t\tType string `json:\"type,omitempty\"`\n\t}\n\tif err := json.Unmarshal(data, &aux); err != nil {\n\t\treturn err\n\t}\n\n\t// Set the Type field.\n\tp.Type = aux.Type\n\n\t// Unmarshal based on the \"type\" field.\n\tswitch aux.Type {\n\tcase \"gcp_pubsub\":\n\t\tvar g GCPPubsub\n\t\tif err := json.Unmarshal(data, &g); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.GCP = &g\n\tcase \"aws_sns_sqs\":\n\t\tvar a AWSSNS_SQS\n\t\tif err := json.Unmarshal(data, &a); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.AWS = &a\n\tcase \"nsq\":\n\t\tvar n NSQPubsub\n\t\tif err := json.Unmarshal(data, &n); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.NSQ = &n\n\tdefault:\n\t\treturn errors.New(\"unsupported pubsub type\")\n\t}\n\n\treturn nil\n}\n\n// Definitions of env_string and env_ref\ntype EnvString struct {\n\tStr string  `json:\"string,omitempty\"`\n\tEnv *EnvRef `json:\"env,omitempty\"`\n}\n\nfunc (e *EnvString) IsEnvRef() bool {\n\treturn e.Env != nil\n}\n\n// Value returns the resolved value of the EnvString.\n// If Env is set, it returns the value of the environment variable.\n// Otherwise, it returns the Str value.\nfunc (e EnvString) Value() string {\n\tif e.Env != nil {\n\t\treturn os.Getenv(e.Env.Env)\n\t}\n\treturn e.Str\n}\n\ntype EnvRef struct {\n\tEnv string `json:\"$env,omitempty\"`\n}\n\nfunc (e *EnvRef) Describe(desc string) EnvDesc {\n\treturn EnvDesc{\n\t\tName:        e.Env,\n\t\tDescription: desc,\n\t}\n}\n\n// UnmarshalJSON is the custom unmarshalling function for the EnvString type.\nfunc (e *EnvString) UnmarshalJSON(data []byte) error {\n\t// Try unmarshalling into a simple string first.\n\tvar simpleString string\n\tif err := json.Unmarshal(data, &simpleString); err == nil {\n\t\te.Str = simpleString\n\t\te.Env = nil\n\t\treturn nil\n\t}\n\n\t// If it isn't a string, try unmarshalling into an EnvRef object.\n\tvar envRef EnvRef\n\tif err := json.Unmarshal(data, &envRef); err == nil {\n\t\te.Str = \"\"\n\t\te.Env = &envRef\n\t\treturn nil\n\t}\n\n\t// If neither works, return an error.\n\treturn errors.New(\"invalid EnvString format\")\n}\n\n// MarshalJSON is the custom marshaller function for the EnvString type (optional if needed).\nfunc (e EnvString) MarshalJSON() ([]byte, error) {\n\t// If EnvRef is set, marshal as an object.\n\tif e.Env != nil {\n\t\treturn json.Marshal(e.Env)\n\t}\n\t// Otherwise, marshal as a simple string.\n\treturn json.Marshal(e.Str)\n}\n\nfunc MapValues[T comparable, V any, V2 any](m map[T]V, fn func(T, V) V2) map[T]V2 {\n\tres := make(map[T]V2)\n\tfor k, v := range m {\n\t\tres[k] = fn(k, v)\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/infra/config_test.go",
    "content": "package infra\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc TestInfraConfigMarshalUnmarshal(t *testing.T) {\n\tc := qt.New(t)\n\t// Read the test data file\n\tdata, err := os.ReadFile(\"testdata/infra.config.json\")\n\tc.Assert(err, qt.IsNil)\n\n\t// Unmarshal the JSON data into InfraConfig\n\tvar config InfraConfig\n\terr = json.Unmarshal(data, &config)\n\tc.Assert(err, qt.IsNil)\n\n\t// Marshal the InfraConfig back to JSON\n\tmarshaledData, err := json.Marshal(config)\n\tc.Assert(err, qt.IsNil)\n\n\t// Unmarshal the marshaled JSON data back to InfraConfig and compare the two objects\n\tvar newConfig InfraConfig\n\terr = json.Unmarshal(marshaledData, &newConfig)\n\tc.Assert(err, qt.IsNil)\n\n\tc.Assert(newConfig, qt.DeepEquals, config)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/infra/testdata/infra.config.json",
    "content": "{\n  \"$schema\": \"https://encore.dev/schemas/infra.schema.json\",\n  \"metadata\": {\n    \"app_id\": \"my-app\",\n    \"env_name\": \"my-env\",\n    \"env_type\": \"production\",\n    \"cloud\": \"gcp\",\n    \"base_url\": \"https://my-app.com\"\n  },\n  \"sql_servers\": [\n    {\n      \"host\": \"my-db-host:5432\",\n      \"tls_config\": {\n        \"ca\": \"test\",\n        \"client_cert\": {\n          \"cert\": \"test\",\n          \"key\": \"test\"\n        },\n        \"disable_tls_hostname_verification\": false,\n        \"disabled\": false\n      },\n      \"databases\": {\n        \"mydb\": {\n          \"client_cert\": {\n            \"cert\": \"test\",\n            \"key\": \"test\"\n          },\n          \"max_connections\": 10,\n          \"min_connections\": 10,\n          \"username\": \"my-db-owner\",\n          \"password\": {\"$env\": \"DB_PASSWORD\"}\n\n        }\n      }\n    }\n  ],\n  \"service_discovery\": {\n    \"myservice\": {\n      \"base_url\": \"https://my-service:8044\"\n    },\n    \"myservice2\": {\n      \"base_url\": \"https://my-service2:8044\",\n      \"auth\": [{\n        \"type\": \"key\",\n        \"key\": {\n          \"$env\": \"SVC_TO_SVC_KEY\"\n        },\n        \"id\": 1\n      }]\n    }\n  },\n  \"redis\": {\n    \"encoreredis\": {\n      \"database_index\": 5,\n      \"max_connections\": 10,\n      \"min_connections\": 10,\n      \"key_prefix\": \"my-app:my-env:\",\n      \"tls_config\": {\n        \"disable_tls_hostname_verification\": false,\n        \"disabled\": false,\n        \"ca\": \"test\",\n        \"client_cert\": {\n          \"cert\": \"test\",\n          \"key\": \"test\"\n        }\n      },\n      \"auth\": {\n        \"type\": \"acl\",\n        \"username\": \"encoreredis\",\n        \"password\": {\"$env\": \"REDIS_PASSWORD\"}\n      },\n      \"host\": \"my-redis-host\"\n    }\n  },\n  \"metrics\": {\n    \"type\": \"prometheus\",\n    \"remote_write_url\": \"https://my-remote-write-url\"\n  },\n  \"graceful_shutdown\": {\n    \"total\": 30,\n    \"handlers\": 20,\n    \"shutdown_hooks\": 10\n  },\n  \"auth\": [\n    {\n      \"type\": \"key\",\n      \"id\": 1,\n      \"key\": {\"$env\": \"SVC_TO_SVC_KEY\"}\n    }\n  ],\n  \"secrets\": {\n    \"AppSecret\": {\"$env\": \"APP_SECRET\"}\n  },\n  \"pubsub\": [\n    {\n      \"type\": \"gcp_pubsub\",\n      \"project_id\": \"my-project\",\n      \"topics\": {\n        \"encore-topic\": {\n          \"name\": \"gcp-topic-name\",\n          \"subscriptions\": {\n            \"encore-subscription\": {\n              \"name\": \"gcp-subscription-name\",\n              \"project_id\": \"test\",\n              \"push_config\": {\n                \"id\": \"test\",\n                \"jwt_audience\": \"test\",\n                \"service_account\": \"test\"\n              }\n            }\n          }\n        }\n      }\n    }\n  ],\n  \"cors\": {\n    \"debug\": true,\n    \"allow_headers\": [\"Authorization\", \"Content-Type\"],\n    \"expose_headers\": [\"*\"],\n    \"allow_origins_with_credentials\": [\"https://test.com\"],\n    \"allow_origins_without_credentials\": [\"https://test.com\"]\n  },\n  \"hosted_gateways\": [\"api-gateway\"],\n  \"hosted_services\": [\"my-service\", \"my-service2\"]\n}"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/infra/testdata/runtime.json",
    "content": "{\n  \"app_id\": \"\",\n  \"app_slug\": \"my-app\",\n  \"api_base_url\": \"https://my-app.com\",\n  \"env_id\": \"\",\n  \"env_name\": \"my-env\",\n  \"env_type\": \"production\",\n  \"env_cloud\": \"gcp\",\n  \"deploy_id\": \"\",\n  \"deploy_time\": \"0001-01-01T00:00:00Z\",\n  \"auth_keys\": [\n    {\n      \"kid\": 1,\n      \"data\": \"\"\n    }\n  ],\n  \"cors\": {\n    \"debug\": true,\n    \"allow_origins_with_credentials\": [\n      \"https://test.com\"\n    ],\n    \"allow_origins_without_credentials\": [\n      \"https://test.com\"\n    ],\n    \"raw_allowed_headers\": [\n      \"Authorization\",\n      \"Content-Type\"\n    ],\n    \"raw_exposed_headers\": [\n      \"*\"\n    ],\n    \"allow_private_network_access\": true\n  },\n  \"sql_databases\": [\n    {\n      \"server_id\": 0,\n      \"encore_name\": \"mydb\",\n      \"database_name\": \"mydb\",\n      \"user\": \"my-db-owner\",\n      \"password\": \"\",\n      \"min_connections\": 10,\n      \"max_connections\": 10\n    }\n  ],\n  \"sql_servers\": [\n    {\n      \"host\": \"my-db-host:5432\",\n      \"server_ca_cert\": \"test\",\n      \"client_cert\": \"test\",\n      \"client_key\": \"test\"\n    }\n  ],\n  \"pubsub_providers\": [\n    {\n      \"gcp\": {}\n    }\n  ],\n  \"pubsub_topics\": {\n    \"encore-topic\": {\n      \"encore_name\": \"encore-topic\",\n      \"provider_id\": 0,\n      \"provider_name\": \"gcp-topic-name\",\n      \"subscriptions\": {\n        \"encore-subscription\": {\n          \"id\": \"test\",\n          \"encore_name\": \"encore-subscription\",\n          \"provider_name\": \"gcp-subscription-name\",\n          \"push_only\": true,\n          \"gcp\": {\n            \"project_id\": \"test\",\n            \"push_service_account\": \"test\"\n          }\n        }\n      },\n      \"gcp\": {\n        \"project_id\": \"my-project\"\n      }\n    }\n  },\n  \"bucket_providers\": [],\n  \"redis_servers\": [\n    {\n      \"host\": \"my-redis-host\",\n      \"user\": \"encoreredis\",\n      \"enable_tls\": true,\n      \"server_ca_cert\": \"test\",\n      \"client_cert\": \"test\",\n      \"client_key\": \"test\"\n    }\n  ],\n  \"redis_databases\": [\n    {\n      \"server_id\": 0,\n      \"encore_name\": \"encoreredis\",\n      \"database\": 5,\n      \"min_connections\": 10,\n      \"max_connections\": 10,\n      \"key_prefix\": \"my-app:my-env:\"\n    }\n  ],\n  \"metrics\": {\n    \"prometheus\": {\n      \"RemoteWriteURL\": \"https://my-remote-write-url\"\n    }\n  },\n  \"gateways\": [\n    {\n      \"name\": \"api-gateway\",\n      \"host\": \"\"\n    }\n  ],\n  \"hosted_services\": [\n    \"my-service\",\n    \"my-service2\"\n  ],\n  \"service_discovery\": {\n    \"myservice\": {\n      \"name\": \"myservice\",\n      \"url\": \"https://my-service:8044\",\n      \"protocol\": \"http\",\n      \"service_auth\": {\n        \"method\": \"encore-auth\"\n      }\n    },\n    \"myservice2\": {\n      \"name\": \"myservice2\",\n      \"url\": \"https://my-service2:8044\",\n      \"protocol\": \"http\",\n      \"service_auth\": {\n        \"method\": \"encore-auth\"\n      }\n    }\n  },\n  \"service_auth\": [\n    {\n      \"method\": \"encore-auth\"\n    }\n  ],\n  \"shutdown_timeout\": 0,\n  \"graceful_shutdown\": {\n    \"total\": 30000000000,\n    \"shutdown_hooks\": 10000000000,\n    \"handlers\": 20000000000\n  }\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/infra/validation.go",
    "content": "package infra\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/modern-go/reflect2\"\n)\n\ntype JSONPath string\n\nfunc (j JSONPath) String() string {\n\treturn string(j)\n}\n\nfunc (j JSONPath) Append(path JSONPath) JSONPath {\n\treturn JSONPath(fmt.Sprintf(\"%s.%s\", j, path))\n}\n\nfunc Ancestor[T any](a *validator) T {\n\tfor a != nil {\n\t\tif v, ok := a.instance.(T); ok {\n\t\t\treturn v\n\t\t}\n\t\ta = a.parent\n\t}\n\tvar t T\n\treturn t\n}\n\ntype Predicate func() error\n\nfunc Err(err string) Predicate {\n\treturn func() error {\n\t\treturn errors.New(err)\n\t}\n}\n\nfunc AnyNonZero[T comparable](v ...T) Predicate {\n\treturn func() error {\n\t\tvar zero T\n\t\tfor _, val := range v {\n\t\t\tif zero != val {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn errors.New(\"Must not be empty\")\n\t}\n}\n\nfunc OneOf[T comparable](v T, vals ...T) Predicate {\n\treturn func() error {\n\t\tif !slices.Contains(vals, v) {\n\t\t\treturn errors.New(\"Must be one of: \" + fmt.Sprint(vals))\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc NotZero[T comparable](v T) Predicate {\n\treturn func() error {\n\t\tvar zero T\n\t\tif zero == v {\n\t\t\treturn errors.New(\"Must not be empty\")\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc NilOr[T any](v *T, predFns ...func(T) Predicate) Predicate {\n\treturn func() error {\n\t\tif v == nil {\n\t\t\treturn nil\n\t\t}\n\t\tfor _, predFn := range predFns {\n\t\t\tif err := predFn(*v)(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc Between[T cmp.Ordered](from, to T) func(T) Predicate {\n\treturn func(t T) Predicate {\n\t\treturn func() error {\n\t\t\tif t < from || t > to {\n\t\t\t\treturn fmt.Errorf(\"Must be between %v and %v (inclusive)\", from, to)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc GreaterOrEqual[T cmp.Ordered](than T) func(T) Predicate {\n\treturn func(t T) Predicate {\n\t\treturn func() error {\n\t\t\tif t < than {\n\t\t\t\treturn fmt.Errorf(\"Must be greater than or equal to %v\", than)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\ntype EnvDesc struct {\n\tName        string `json:\"name,omitempty\"`\n\tDescription string `json:\"description,omitempty\"`\n}\n\nfunc Validate(v Validatable) (map[JSONPath]EnvDesc, map[JSONPath]error) {\n\troot := &validator{instance: v, envDescs: make(map[string]EnvDesc), errors: make(map[string]error)}\n\tv.Validate(root)\n\treturn root.Collect()\n}\n\nfunc ValidateChildList[T Validatable](v *validator, name string, items []T) {\n\tfor i, item := range items {\n\t\tv.ValidateChild(fmt.Sprintf(\"%s[%d]\", name, i), item)\n\t}\n}\n\nfunc ValidateChildMap[T Validatable](v *validator, name string, items map[string]T) {\n\tfor k, item := range items {\n\t\tv.ValidateChild(fmt.Sprintf(\"%s.%s\", name, k), item)\n\t}\n}\n\ntype validator struct {\n\tparent   *validator\n\tname     string\n\tinstance Validatable\n\tenvDescs map[string]EnvDesc\n\terrors   map[string]error\n\tchildren []*validator\n}\n\nfunc (val *validator) Collect() (map[JSONPath]EnvDesc, map[JSONPath]error) {\n\tenvDescs := map[JSONPath]EnvDesc{}\n\terrors := map[JSONPath]error{}\n\tfor k, v := range val.envDescs {\n\t\tenvDescs[JSONPath(val.name).Append(JSONPath(k))] = v\n\t}\n\tfor k, v := range val.errors {\n\t\terrors[JSONPath(val.name).Append(JSONPath(k))] = v\n\t}\n\tfor _, child := range val.children {\n\t\tchildEnvDescs, childErrors := child.Collect()\n\t\tfor k, v := range childEnvDescs {\n\t\t\tenvDescs[JSONPath(val.name).Append(k)] = v\n\t\t}\n\t\tfor k, v := range childErrors {\n\t\t\terrors[JSONPath(val.name).Append(k)] = v\n\t\t}\n\t}\n\treturn envDescs, errors\n}\n\nfunc (v *validator) ValidatePtrEnvRef(name string, ref *EnvString, envDesc string, predFn func(string) Predicate) {\n\tif ref == nil {\n\t\treturn\n\t}\n\tv.ValidateEnvString(name, *ref, envDesc, predFn)\n}\n\nfunc (v *validator) ValidateEnvRef(name string, ref EnvRef, envDesc string) {\n\tv.envDescs[name] = ref.Describe(envDesc)\n}\n\nfunc (v *validator) ValidateEnvString(name string, envStr EnvString, envDesc string, predFn func(string) Predicate) {\n\tif envStr.IsEnvRef() {\n\t\tv.ValidateEnvRef(name, *envStr.Env, envDesc)\n\t} else if predFn != nil {\n\t\tv.ValidateField(name, predFn(envStr.Str))\n\t}\n}\n\nfunc (v *validator) ValidateField(name string, pred Predicate) {\n\terr := pred()\n\tif err == nil {\n\t\treturn\n\t}\n\tv.errors[name] = err\n}\n\nfunc (v *validator) ValidateChild(name string, instance Validatable) {\n\tif reflect2.IsNil(instance) {\n\t\treturn\n\t}\n\tchild := &validator{name: name, instance: instance, parent: v, envDescs: make(map[string]EnvDesc), errors: make(map[string]error)}\n\tinstance.Validate(child)\n\tv.children = append(v.children, child)\n}\n\ntype Validatable interface {\n\tValidate(validator *validator)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/parse.go",
    "content": "package config\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/config/infra\"\n)\n\nfunc gunzip(data []byte) ([]byte, error) {\n\tgz, err := gzip.NewReader(bytes.NewReader(data))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn io.ReadAll(gz)\n}\n\ntype ProcessConfig struct {\n\tHostedServices    []string       `json:\"hosted_services\"`\n\tHostedGateways    []string       `json:\"hosted_gateways\"`\n\tLocalServicePorts map[string]int `json:\"local_service_ports\"`\n}\n\nfunc parseRuntimeConfigEnv(config string) *Runtime {\n\tvar cfg Runtime\n\t// We used to support RawURLEncoding, but now we use StdEncoding.\n\t// Try both if StdEncoding fails.\n\tvar (\n\t\tbytes []byte\n\t\terr   error\n\t)\n\tconfig, isGzipped := strings.CutPrefix(config, \"gzip:\")\n\t// nosemgrep\n\tif bytes, err = base64.StdEncoding.DecodeString(config); err != nil {\n\t\tbytes, err = base64.RawURLEncoding.DecodeString(config)\n\t}\n\tif err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not decode encore runtime config:\", err)\n\t}\n\tif isGzipped {\n\t\tif bytes, err = gunzip(bytes); err != nil {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: could not gunzip encore runtime config:\", err)\n\t\t}\n\t}\n\tif err := json.Unmarshal(bytes, &cfg); err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not parse encore runtime config:\", err)\n\t}\n\n\tif _, err := url.Parse(cfg.APIBaseURL); err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not parse api base url from encore runtime config:\", err)\n\t}\n\treturn &cfg\n}\n\nfunc parseProcessConfigEnv(processCfg string, cfg *Runtime) {\n\tif processCfg == \"\" {\n\t\treturn\n\t}\n\tbytes, err := base64.StdEncoding.DecodeString(processCfg)\n\tif err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not decode encore process config:\", err)\n\t}\n\tvar procCfg ProcessConfig\n\tif err := json.Unmarshal(bytes, &procCfg); err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not parse encore process config:\", err)\n\t}\n\tcfg.HostedServices = procCfg.HostedServices\n\tvar hostedGateways []Gateway\n\tfor _, name := range procCfg.HostedGateways {\n\t\ti := slices.IndexFunc(cfg.Gateways, func(gw Gateway) bool { return gw.Name == name })\n\t\tif i == -1 {\n\t\t\tlog.Fatalf(\"encore runtime: fatal error: gateway %q not found in runtime config\", name)\n\t\t}\n\t\thostedGateways = append(hostedGateways, cfg.Gateways[i])\n\t}\n\tcfg.Gateways = hostedGateways\n\n\t// Use noop service auth method if not specified\n\tsvcAuth := ServiceAuth{\"noop\"}\n\tif len(cfg.ServiceAuth) > 0 {\n\t\t// Use the first service auth method from the runtime config\n\t\tsvcAuth = cfg.ServiceAuth[0]\n\t}\n\n\tfor name, port := range procCfg.LocalServicePorts {\n\t\tif cfg.ServiceDiscovery == nil {\n\t\t\tcfg.ServiceDiscovery = make(map[string]Service)\n\t\t}\n\t\tcfg.ServiceDiscovery[name] = Service{\n\t\t\tName:        name,\n\t\t\tURL:         fmt.Sprintf(\"http://localhost:%d\", port),\n\t\t\tProtocol:    Http,\n\t\t\tServiceAuth: svcAuth,\n\t\t}\n\t}\n}\n\nfunc toPtr[T any](t T) *T {\n\treturn &t\n}\n\nfunc LoadInfraConfig(infraCfgPath string) (*infra.InfraConfig, error) {\n\tvar envCfg infra.InfraConfig\n\tfile, err := os.Open(infraCfgPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not open infra config: %w\", err)\n\t}\n\tdefer func() { _ = file.Close() }()\n\n\tdecoder := json.NewDecoder(file)\n\terr = decoder.Decode(&envCfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not decode infra config: %w\", err)\n\t}\n\treturn &envCfg, nil\n}\n\nfunc LoadRuntimeConfig(runtimeCfgPath string) (*Runtime, error) {\n\tvar cfg Runtime\n\tfile, err := os.Open(runtimeCfgPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not open runtime config: %w\", err)\n\t}\n\tdefer func() { _ = file.Close() }()\n\n\tdecoder := json.NewDecoder(file)\n\terr = decoder.Decode(&cfg)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not decode runtime config: %w\", err)\n\t}\n\treturn &cfg, nil\n}\n\nfunc parseInfraConfigEnv(infraCfgPath string) *Runtime {\n\tvar cfg Runtime\n\tinfraCfg, err := LoadInfraConfig(infraCfgPath)\n\tif err != nil {\n\t\tlog.Fatalf(\"encore runtime: fatal error: %v\", err)\n\t}\n\n\tcfg.AppSlug = infraCfg.Metadata.AppID\n\tcfg.EnvName = infraCfg.Metadata.EnvName\n\tcfg.EnvType = infraCfg.Metadata.EnvType\n\tcfg.EnvCloud = infraCfg.Metadata.Cloud\n\tcfg.APIBaseURL = infraCfg.Metadata.BaseURL\n\tcfg.LogConfig = infraCfg.LogConfig\n\n\t// Map graceful shutdown configuration\n\tif infraCfg.GracefulShutdown != nil {\n\t\tcfg.GracefulShutdown = &GracefulShutdownTimings{}\n\t\tif infraCfg.GracefulShutdown.Total != nil {\n\t\t\tcfg.GracefulShutdown.Total = toPtr(time.Duration(*infraCfg.GracefulShutdown.Total) * time.Second)\n\t\t}\n\t\tif infraCfg.GracefulShutdown.ShutdownHooks != nil {\n\t\t\tcfg.GracefulShutdown.ShutdownHooks = toPtr(time.Duration(*infraCfg.GracefulShutdown.ShutdownHooks) * time.Second)\n\t\t}\n\t\tif infraCfg.GracefulShutdown.Handlers != nil {\n\t\t\tcfg.GracefulShutdown.Handlers = toPtr(time.Duration(*infraCfg.GracefulShutdown.Handlers) * time.Second)\n\t\t}\n\t}\n\n\t// Map authentication configuration\n\tcfg.ServiceAuth = make([]ServiceAuth, len(infraCfg.Auth))\n\tif len(infraCfg.Auth) == 0 {\n\t\tcfg.ServiceAuth = []ServiceAuth{{Method: \"noop\"}}\n\t}\n\tfor i, auth := range infraCfg.Auth {\n\t\tswitch auth.Type {\n\t\tcase \"key\":\n\t\t\tcfg.ServiceAuth[i] = ServiceAuth{\n\t\t\t\tMethod: \"encore-auth\",\n\t\t\t}\n\t\t\tcfg.AuthKeys = append(cfg.AuthKeys, EncoreAuthKey{\n\t\t\t\tKeyID: uint32(auth.ID),\n\t\t\t\tData:  []byte(auth.Key.Value()),\n\t\t\t})\n\t\tdefault:\n\t\t\tlog.Fatalf(\"encore runtime: fatal error: unsupported auth type %q\", auth.Type)\n\t\t}\n\t}\n\n\t// Map metrics configuration\n\tif infraCfg.Metrics != nil {\n\t\tcfg.Metrics = &Metrics{\n\t\t\tCollectionInterval: time.Duration(infraCfg.Metrics.CollectionInterval) * time.Second,\n\t\t}\n\t\tswitch infraCfg.Metrics.Type {\n\t\tcase \"prometheus\":\n\t\t\tif infraCfg.Metrics.Prometheus != nil {\n\t\t\t\tcfg.Metrics.Prometheus = &PrometheusRemoteWriteProvider{\n\t\t\t\t\tinfraCfg.Metrics.Prometheus.RemoteWriteURL.Value(),\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"datadog\":\n\t\t\tif infraCfg.Metrics.Datadog != nil {\n\t\t\t\tcfg.Metrics.Datadog = &DatadogProvider{\n\t\t\t\t\tinfraCfg.Metrics.Datadog.Site,\n\t\t\t\t\tinfraCfg.Metrics.Datadog.APIKey.Value(),\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"gcp_cloud_monitoring\":\n\t\t\tif infraCfg.Metrics.GCPCloudMonitoring != nil {\n\t\t\t\tcfg.Metrics.CloudMonitoring = &GCPCloudMonitoringProvider{\n\t\t\t\t\tinfraCfg.Metrics.GCPCloudMonitoring.ProjectID,\n\t\t\t\t\tinfraCfg.Metrics.GCPCloudMonitoring.MonitoredResourceType,\n\t\t\t\t\tinfraCfg.Metrics.GCPCloudMonitoring.MonitoredResourceLabels,\n\t\t\t\t\tinfraCfg.Metrics.GCPCloudMonitoring.MetricNames,\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"aws_cloudwatch\":\n\t\t\tif infraCfg.Metrics.AWSCloudWatch != nil {\n\t\t\t\tcfg.Metrics.CloudWatch = &AWSCloudWatchMetricsProvider{\n\t\t\t\t\tinfraCfg.Metrics.AWSCloudWatch.Namespace,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Map SQL servers configuration\n\tcfg.SQLServers = make([]*SQLServer, len(infraCfg.SQLServers))\n\tfor i, sqlServer := range infraCfg.SQLServers {\n\t\tcfg.SQLServers[i] = &SQLServer{\n\t\t\tHost: sqlServer.Host,\n\t\t}\n\t\tif sqlServer.TLSConfig != nil {\n\t\t\tcfg.SQLServers[i].ServerCACert = sqlServer.TLSConfig.CA\n\t\t\tif sqlServer.TLSConfig.ClientCert != nil {\n\t\t\t\tcfg.SQLServers[i].ClientCert = sqlServer.TLSConfig.ClientCert.Cert\n\t\t\t\tcfg.SQLServers[i].ClientKey = sqlServer.TLSConfig.ClientCert.Key.Value()\n\t\t\t}\n\t\t}\n\n\t\tfor dbName, db := range sqlServer.Databases {\n\t\t\tcfg.SQLDatabases = append(cfg.SQLDatabases, &SQLDatabase{\n\t\t\t\tServerID:       i,\n\t\t\t\tEncoreName:     orDefault(db.Name, dbName),\n\t\t\t\tDatabaseName:   dbName,\n\t\t\t\tUser:           db.Username.Value(),\n\t\t\t\tPassword:       db.Password.Value(),\n\t\t\t\tMinConnections: db.MinConnections,\n\t\t\t\tMaxConnections: db.MaxConnections,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Map Redis configuration\n\tcfg.RedisServers = make([]*RedisServer, len(infraCfg.Redis))\n\tvar i int\n\tfor name, redis := range infraCfg.Redis {\n\t\tcfg.RedisServers[i] = &RedisServer{\n\t\t\tHost:     redis.Host,\n\t\t\tInMemory: redis.InMemory,\n\t\t}\n\t\tif redis.TLSConfig != nil {\n\t\t\tcfg.RedisServers[i].EnableTLS = true\n\t\t\tcfg.RedisServers[i].ServerCACert = redis.TLSConfig.CA\n\t\t\tif redis.TLSConfig.ClientCert != nil {\n\t\t\t\tcfg.RedisServers[i].ClientCert = redis.TLSConfig.ClientCert.Cert\n\t\t\t\tcfg.RedisServers[i].ClientKey = redis.TLSConfig.ClientCert.Key.Value()\n\t\t\t}\n\t\t}\n\t\tif redis.Auth != nil {\n\t\t\tswitch redis.Auth.Type {\n\t\t\tcase \"acl\":\n\t\t\t\tcfg.RedisServers[i].User = redis.Auth.Username.Value()\n\t\t\t\tcfg.RedisServers[i].Password = redis.Auth.Password.Value()\n\t\t\tcase \"auth_string\":\n\t\t\t\tcfg.RedisServers[i].Password = redis.Auth.AuthString.Value()\n\t\t\tdefault:\n\t\t\t\tlog.Fatalf(\"encore runtime: fatal error: unsupported redis auth type %q\", redis.Auth.Type)\n\t\t\t}\n\t\t}\n\t\tcfg.RedisDatabases = append(cfg.RedisDatabases, &RedisDatabase{\n\t\t\tServerID:       i,\n\t\t\tEncoreName:     name,\n\t\t\tDatabase:       redis.DatabaseIndex,\n\t\t\tMinConnections: orDefaultPtr(redis.MinConnections, 0),\n\t\t\tMaxConnections: orDefaultPtr(redis.MaxConnections, 0),\n\t\t\tKeyPrefix:      orDefaultPtr(redis.KeyPrefix, \"\"),\n\t\t})\n\t\ti++\n\t}\n\n\t// Map PubSub configuration\n\tcfg.PubsubProviders = make([]*PubsubProvider, len(infraCfg.PubSub))\n\tfor i, pubsub := range infraCfg.PubSub {\n\t\tswitch pubsub.Type {\n\t\tcase \"gcp_pubsub\":\n\t\t\tcfg.PubsubProviders[i] = &PubsubProvider{\n\t\t\t\tGCP: &GCPPubsubProvider{},\n\t\t\t}\n\t\tcase \"aws_sns_sqs\":\n\t\t\tcfg.PubsubProviders[i] = &PubsubProvider{\n\t\t\t\tAWS: &AWSPubsubProvider{},\n\t\t\t}\n\t\tcase \"nsq\":\n\t\t\tcfg.PubsubProviders[i] = &PubsubProvider{\n\t\t\t\tNSQ: &NSQProvider{\n\t\t\t\t\tHost: pubsub.NSQ.Hosts,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tcfg.PubsubTopics = map[string]*PubsubTopic{}\n\t\tfor topicName, topic := range pubsub.GetTopics() {\n\t\t\tswitch topic := topic.(type) {\n\t\t\tcase *infra.GCPTopic:\n\t\t\t\tcfg.PubsubTopics[topicName] = &PubsubTopic{\n\t\t\t\t\tEncoreName:    topicName,\n\t\t\t\t\tProviderID:    i,\n\t\t\t\t\tProviderName:  topic.Name,\n\t\t\t\t\tSubscriptions: map[string]*PubsubSubscription{},\n\t\t\t\t\tGCP: &PubsubTopicGCPData{\n\t\t\t\t\t\tProjectID: orDefault(topic.ProjectID, pubsub.GCP.ProjectID),\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\tcase *infra.AWSTopic:\n\t\t\t\tcfg.PubsubTopics[topicName] = &PubsubTopic{\n\t\t\t\t\tEncoreName:    topicName,\n\t\t\t\t\tProviderID:    i,\n\t\t\t\t\tProviderName:  topic.ARN,\n\t\t\t\t\tSubscriptions: map[string]*PubsubSubscription{},\n\t\t\t\t}\n\t\t\tcase *infra.NSQTopic:\n\t\t\t\tcfg.PubsubTopics[topicName] = &PubsubTopic{\n\t\t\t\t\tEncoreName:    topicName,\n\t\t\t\t\tProviderID:    i,\n\t\t\t\t\tProviderName:  topic.Name,\n\t\t\t\t\tSubscriptions: map[string]*PubsubSubscription{},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor subName, subscription := range topic.GetSubscriptions() {\n\t\t\t\tswitch subscription := subscription.(type) {\n\t\t\t\tcase *infra.GCPSub:\n\t\t\t\t\tsub := &PubsubSubscription{\n\t\t\t\t\t\tEncoreName:   subName,\n\t\t\t\t\t\tProviderName: subscription.Name,\n\t\t\t\t\t\tPushOnly:     subscription.PushConfig != nil,\n\t\t\t\t\t\tGCP:          &PubsubSubscriptionGCPData{ProjectID: orDefault(subscription.ProjectID, pubsub.GCP.ProjectID)},\n\t\t\t\t\t}\n\t\t\t\t\tif subscription.PushConfig != nil {\n\t\t\t\t\t\tsub.ID = subscription.PushConfig.ID\n\t\t\t\t\t\tsub.GCP.PushServiceAccount = subscription.PushConfig.ServiceAccount\n\t\t\t\t\t}\n\t\t\t\t\tcfg.PubsubTopics[topicName].Subscriptions[subName] = sub\n\t\t\t\tcase *infra.AWSSub:\n\t\t\t\t\tcfg.PubsubTopics[topicName].Subscriptions[subName] = &PubsubSubscription{\n\t\t\t\t\t\tEncoreName:   subName,\n\t\t\t\t\t\tProviderName: subscription.URL,\n\t\t\t\t\t\tPushOnly:     false,\n\t\t\t\t\t}\n\t\t\t\tcase *infra.NSQSub:\n\t\t\t\t\tcfg.PubsubTopics[topicName].Subscriptions[subName] = &PubsubSubscription{\n\t\t\t\t\t\tEncoreName:   subName,\n\t\t\t\t\t\tProviderName: subscription.Name,\n\t\t\t\t\t\tPushOnly:     false,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Map Service Discovery configuration\n\tcfg.ServiceDiscovery = make(map[string]Service)\n\tfor name, service := range infraCfg.ServiceDiscovery {\n\t\tcfg.ServiceDiscovery[name] = Service{\n\t\t\tName:        name,\n\t\t\tURL:         service.BaseURL,\n\t\t\tProtocol:    Http,\n\t\t\tServiceAuth: cfg.ServiceAuth[0],\n\t\t}\n\t}\n\n\t// Map Buckets\n\tcfg.BucketProviders = make([]*BucketProvider, len(infraCfg.ObjectStorage))\n\tfor i, storage := range infraCfg.ObjectStorage {\n\t\tswitch storage.Type {\n\t\tcase \"gcs\":\n\t\t\tcfg.BucketProviders[i] = &BucketProvider{\n\t\t\t\tGCS: &GCSBucketProvider{\n\t\t\t\t\tEndpoint: storage.GCS.Endpoint,\n\t\t\t\t},\n\t\t\t}\n\t\tcase \"s3\":\n\t\t\tcfg.BucketProviders[i] = &BucketProvider{\n\t\t\t\tS3: &S3BucketProvider{\n\t\t\t\t\tRegion:          storage.S3.Region,\n\t\t\t\t\tEndpoint:        nilOr(storage.S3.Endpoint),\n\t\t\t\t\tAccessKeyID:     nilOr(storage.S3.AccessKeyID),\n\t\t\t\t\tSecretAccessKey: nilOr(storage.S3.SecretAccessKey.Value()),\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tcfg.Buckets = map[string]*Bucket{}\n\t\tfor bucketName, bucket := range storage.GetBuckets() {\n\t\t\tcfg.Buckets[bucketName] = &Bucket{\n\t\t\t\tProviderID:    i,\n\t\t\t\tEncoreName:    bucketName,\n\t\t\t\tCloudName:     bucket.Name,\n\t\t\t\tKeyPrefix:     bucket.KeyPrefix,\n\t\t\t\tPublicBaseURL: bucket.PublicBaseURL,\n\t\t\t}\n\t\t}\n\t}\n\n\tif infraCfg.CORS != nil {\n\t\tcfg.CORS = &CORS{\n\t\t\tDebug:                          infraCfg.CORS.Debug,\n\t\t\tDisableCredentials:             false,\n\t\t\tAllowOriginsWithCredentials:    infraCfg.CORS.AllowOriginsWithCredentials,\n\t\t\tAllowOriginsWithoutCredentials: infraCfg.CORS.AllowOriginsWithoutCredentials,\n\t\t\tExtraAllowedHeaders:            infraCfg.CORS.AllowHeaders,\n\t\t\tExtraExposedHeaders:            infraCfg.CORS.ExposeHeaders,\n\t\t\tAllowPrivateNetworkAccess:      true,\n\t\t}\n\t}\n\t// Map hosted services\n\tcfg.HostedServices = infraCfg.HostedServices\n\tcfg.Gateways = make([]Gateway, len(infraCfg.HostedGateways))\n\tfor i, gw := range infraCfg.HostedGateways {\n\t\tcfg.Gateways[i] = Gateway{\n\t\t\tName: gw,\n\t\t}\n\t}\n\treturn &cfg\n}\n\nfunc nilOr[T comparable](val T) *T {\n\tvar zero T\n\tif val == zero {\n\t\treturn nil\n\t}\n\treturn &val\n\n}\n\nfunc orDefaultPtr[T any](val *T, def T) T {\n\tif val == nil {\n\t\treturn def\n\t}\n\treturn *val\n}\n\nfunc orDefault[T comparable](val T, def T) T {\n\tvar zero T\n\tif val == zero {\n\t\treturn def\n\t}\n\treturn val\n}\n\n// ParseRuntime parses the Encore runtime config.\nfunc ParseRuntime(runtimeConfig, runtimeConfigPath, processCfg, infraCfgPath, deployID string) *Runtime {\n\tvar cfg *Runtime\n\tif infraCfgPath != \"\" {\n\t\tcfg = parseInfraConfigEnv(infraCfgPath)\n\t} else if runtimeConfig != \"\" {\n\t\tcfg = parseRuntimeConfigEnv(runtimeConfig)\n\t} else if runtimeConfigPath != \"\" {\n\t\tvar err error\n\t\tcfg, err = LoadRuntimeConfig(runtimeConfigPath)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: could not load encore runtime config:\", err)\n\t\t}\n\t} else {\n\t\tlog.Fatalln(\"encore runtime: fatal error: no encore runtime or infra config provided\")\n\t}\n\tparseProcessConfigEnv(processCfg, cfg)\n\n\t// If the environment deploy ID is set, use that instead of the one\n\t// embedded in the runtime config\n\tif deployID != \"\" {\n\t\tcfg.DeployID = deployID\n\t}\n\n\treturn cfg\n}\n\n// ParseStatic parses the Encore static config.\nfunc ParseStatic(config string) *Static {\n\tif config == \"\" {\n\t\tlog.Fatalln(\"encore runtime: fatal error: no encore static config provided\")\n\t}\n\tbytes, err := base64.StdEncoding.DecodeString(config)\n\tif err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not decode encore static config:\", err)\n\t}\n\tvar cfg Static\n\tif err := json.Unmarshal(bytes, &cfg); err != nil {\n\t\tlog.Fatalln(\"encore runtime: fatal error: could not parse encore static config:\", err)\n\t}\n\treturn &cfg\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/config/parse_test.go",
    "content": "package config\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc gzipData(data []byte) ([]byte, error) {\n\tvar b bytes.Buffer\n\tgz := gzip.NewWriter(&b)\n\tif _, err := gz.Write(data); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := gz.Close(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn b.Bytes(), nil\n}\n\nfunc TestGZippedContent(t *testing.T) {\n\tvar tests = map[string]struct {\n\t\tGZip          bool\n\t\tConfig        *Runtime\n\t\tProcessConfig *ProcessConfig\n\t\tMergedConfig  *Runtime\n\t}{\n\t\t\"zipped\": {\n\t\t\tGZip: true,\n\t\t\tConfig: &Runtime{\n\t\t\t\tAppSlug: \"no-env-ref\",\n\t\t\t\tPubsubTopics: map[string]*PubsubTopic{\n\t\t\t\t\t\"one\": {\n\t\t\t\t\t\tEncoreName: \"testTopic1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"unzipped\": {\n\t\t\tGZip: false,\n\t\t\tConfig: &Runtime{\n\t\t\t\tAppSlug: \"test\",\n\t\t\t\tPubsubTopics: map[string]*PubsubTopic{\n\t\t\t\t\t\"one\": {\n\t\t\t\t\t\tEncoreName: \"testTopic1\",\n\t\t\t\t\t},\n\t\t\t\t\t\"two\": {\n\t\t\t\t\t\tEncoreName: \"testTopic2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"process-config-wo-gw\": {\n\t\t\tGZip: false,\n\t\t\tConfig: &Runtime{\n\t\t\t\tAppSlug:        \"test\",\n\t\t\t\tHostedServices: []string{\"one\", \"two\", \"three\"},\n\t\t\t\tGateways: []Gateway{{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\tProcessConfig: &ProcessConfig{\n\t\t\t\tHostedServices: []string{\"one\"},\n\t\t\t},\n\t\t\tMergedConfig: &Runtime{\n\t\t\t\tAppSlug:        \"test\",\n\t\t\t\tHostedServices: []string{\"one\"},\n\t\t\t},\n\t\t},\n\t\t\"process-config-w-gw\": {\n\t\t\tGZip: false,\n\t\t\tConfig: &Runtime{\n\t\t\t\tAppSlug:        \"test\",\n\t\t\t\tHostedServices: []string{\"one\", \"two\", \"three\"},\n\t\t\t\tGateways: []Gateway{{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tHost: \"test\",\n\t\t\t\t}},\n\t\t\t},\n\t\t\tProcessConfig: &ProcessConfig{\n\t\t\t\tHostedGateways: []string{\"test\"},\n\t\t\t},\n\t\t\tMergedConfig: &Runtime{\n\t\t\t\tAppSlug: \"test\",\n\t\t\t\tGateways: []Gateway{{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tHost: \"test\",\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\trawData, err := json.Marshal(test.Config)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"could not marshal test data: %v\", err)\n\t\t\t}\n\t\t\tvar cfgString string\n\t\t\tif test.GZip {\n\t\t\t\tdata, err := gzipData(rawData)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"could not gzip data: %v\", err)\n\t\t\t\t}\n\t\t\t\tcfgString = \"gzip:\" + base64.StdEncoding.EncodeToString(data)\n\t\t\t} else {\n\t\t\t\tcfgString = base64.StdEncoding.EncodeToString(rawData)\n\t\t\t}\n\n\t\t\texpected := test.Config\n\t\t\tprocCfg := \"\"\n\t\t\tif test.ProcessConfig != nil {\n\t\t\t\texpected = test.MergedConfig\n\t\t\t\trawData, err := json.Marshal(test.ProcessConfig)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"could not marshal process config: %v\", err)\n\t\t\t\t}\n\t\t\t\tprocCfg = base64.StdEncoding.EncodeToString(rawData)\n\t\t\t}\n\t\t\tresp := ParseRuntime(cfgString, \"\", procCfg, \"\", \"\")\n\t\t\tif !reflect.DeepEqual(resp, expected) {\n\t\t\t\tt.Fatalf(\"expected %+v, got %+v\", test.Config, resp)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseInfraConfigEnv(t *testing.T) {\n\tc := qt.New(t)\n\n\t// Parse the infra config using parseInfraConfigEnv\n\tparsedRuntime := parseInfraConfigEnv(\"infra/testdata/infra.config.json\")\n\n\t// Read the runtime test data file\n\truntimeData, err := os.ReadFile(\"infra/testdata/runtime.json\")\n\tc.Assert(err, qt.IsNil)\n\n\t// Unmarshal the runtime JSON data into Runtime\n\tvar expectedRuntime Runtime\n\terr = json.Unmarshal(runtimeData, &expectedRuntime)\n\tc.Assert(err, qt.IsNil)\n\n\t// Compare the parsed runtime with the expected runtime\n\tc.Assert(parsedRuntime, qt.DeepEquals, &expectedRuntime)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/experiments/cli.go",
    "content": "//go:build !encore_app\n\n// Note this file is only included by the CLI and not by the app runtime.\n\npackage experiments\n\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\n// FromAppFileAndEnviron creates an experiment set which represents the enabled experiments\n// within a particular run of Encore.\n//\n// All errors reported by FromAppFileAndEnviron are due to unknown experiment names.\n// The error type is of type *UnknownExperimentError.\nfunc FromAppFileAndEnviron(fromAppFile []Name, environ []string) (*Set, error) {\n\tconst envName = \"ENCORE_EXPERIMENT\"\n\n\tset := &Set{make(map[Name]struct{})}\n\n\t// Add experiments enabled in the app file\n\tif err := set.add(fromAppFile...); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Grab experiments from the environmental variables of this process.\n\tif val := os.Getenv(envName); val != \"\" {\n\t\tif err := set.add(parseEnvVal(val)...); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Grab experiments from the environmental variables of the caller\n\tconst prefix = envName + \"=\"\n\tfor _, env := range environ {\n\t\tif strings.HasPrefix(env, prefix) {\n\t\t\tval := env[len(prefix):]\n\t\t\tif err := set.add(parseEnvVal(val)...); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn set, nil\n}\n\nfunc (s *Set) add(keys ...Name) error {\n\tfor _, key := range keys {\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !key.Valid() {\n\t\t\treturn &UnknownExperimentError{key}\n\t\t}\n\t\ts.enabled[key] = struct{}{}\n\t}\n\treturn nil\n}\n\nfunc parseEnvVal(val string) []Name {\n\tif val == \"\" {\n\t\treturn nil\n\t}\n\n\tval = strings.Trim(val, `\"'`)\n\tstrs := strings.Split(val, \",\")\n\tnames := make([]Name, len(strs))\n\tfor i, s := range strs {\n\t\tnames[i] = Name(s)\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/experiments/errors.go",
    "content": "package experiments\n\n// UnknownExperimentError is an error returned when an app tries to use\n// an experiment that is not known to the current version of Encore.\ntype UnknownExperimentError struct {\n\tName Name\n}\n\nfunc (e *UnknownExperimentError) Error() string {\n\treturn \"unknown experiment: \" + string(e.Name)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/experiments/names.go",
    "content": "package experiments\n\n// Name is the name of an experiment\ntype Name string\n\nconst (\n\t/* Current experiments are listed here */\n\n\t// LocalSecretsOverride is an experiment to allow for secrets\n\t// to be overridden with values from a \".secrets.local\" file.\n\tLocalSecretsOverride Name = \"local-secrets-override\"\n\n\t// Metrics is an experiment to enable metrics.\n\tMetrics Name = \"metrics\"\n\n\t// V2 enables the new parser and compiler.\n\tV2 Name = \"v2\"\n\n\t// BetaRuntime enables the beta runtime.\n\tBetaRuntime Name = \"beta-runtime\"\n\n\t// LocalMultiProcess forces each Encore service to run as it's own independent process locally\n\t// without being able to share memory which emulates the behaviour that will be seen in production\n\t// in a multi process setup.\n\tLocalMultiProcess Name = \"local-multi-process\"\n\n\t// AuthDataRoundTrip forces auth data to be round-tripped through the wireformat\n\t// when internal API calls are made.\n\tAuthDataRoundTrip Name = \"auth-data-round-trip\"\n\n\t// TypeScript enables building the app with TypeScript support.\n\tTypeScript Name = \"typescript\"\n\n\t// StreamTraces enables streaming traces to the Encore platform as they're happening,\n\t// as opposed to waiting for the request to finish before starting the upload.\n\tStreamTraces Name = \"stream-traces\"\n\n\t// AdaptiveGCPPubSubGoroutines enables adaptive configuration of the number of\n\t// goroutines to use on GCP. Useful for applications with a large number of subscriptions.\n\tAdaptiveGCPPubSubGoroutines Name = \"adaptive-gcp-pubsub-goroutines\"\n\n\t// TSWorkerThreads enables multiple worker threads for Encore.ts.\n\tTSWorkerThreads Name = \"ts-worker-threads\"\n\n\t// BunRuntime enables bun as the nodejs runtime\n\tBunRuntime Name = \"bun-runtime\"\n)\n\n// Valid reports whether the given name is a known experiment.\nfunc (x Name) Valid() bool {\n\tswitch x {\n\tcase LocalSecretsOverride,\n\t\tMetrics,\n\t\tV2,\n\t\tBetaRuntime,\n\t\tLocalMultiProcess,\n\t\tAuthDataRoundTrip,\n\t\tTypeScript,\n\t\tStreamTraces,\n\t\tAdaptiveGCPPubSubGoroutines,\n\t\tTSWorkerThreads,\n\t\tBunRuntime:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Enabled returns true if this experiment enabled in the given set\nfunc (x Name) Enabled(set *Set) bool {\n\tif set == nil {\n\t\t// If there's no set, then it's not enabled\n\t\treturn false\n\t}\n\n\t// Does the release set contain this?\n\t_, ok := set.enabled[x]\n\treturn ok\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/experiments/set.go",
    "content": "package experiments\n\nimport (\n\t\"slices\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// Set is a set of experiments enabled within this app\ntype Set struct {\n\tenabled map[Name]struct{}\n}\n\n// FromConfig constructs a new Experiments object from both the static and runtime configs.\n//\n// It is used by the Encore runtime to construct the Experiments set in a running application.\n//\n// Unknown experiments are ignored.\nfunc FromConfig(static *config.Static, runtime *config.Runtime) *Set {\n\te := &Set{make(map[Name]struct{})}\n\n\t// Note we don't check for valid experiments here, because the static and runtime configs\n\t// are already validated by the compiler, and from the platform side\n\t// we might have enabled experiments that are not yet known to the compiler which this\n\t// binary was compiled with.\n\tif static != nil {\n\t\tfor _, exp := range static.EnabledExperiments {\n\t\t\te.enabled[Name(exp)] = struct{}{}\n\t\t}\n\t}\n\n\tif runtime != nil {\n\t\tfor _, exp := range runtime.DynamicExperiments {\n\t\t\te.enabled[Name(exp)] = struct{}{}\n\t\t}\n\t}\n\n\treturn e\n}\n\n// List returns a list of all experiments enabled in this set.\nfunc (s *Set) List() []Name {\n\tif s == nil {\n\t\treturn nil\n\t}\n\tnames := make([]Name, 0, len(s.enabled))\n\tfor key := range s.enabled {\n\t\tnames = append(names, key)\n\t}\n\tslices.Sort(names)\n\treturn names\n}\n\n// StringList returns a list of all experiments enabled in this set.\nfunc (s *Set) StringList() []string {\n\tnames := s.List()\n\trtn := make([]string, len(names))\n\tfor i, n := range names {\n\t\trtn[i] = string(n)\n\t}\n\treturn rtn\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/model/request.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"github.com/rs/zerolog\"\n)\n\ntype TraceEventID uint64\n\ntype RequestType byte\n\nconst (\n\tRPCCall       RequestType = 0x01\n\tAuthHandler   RequestType = 0x02\n\tPubSubMessage RequestType = 0x03\n\tTest          RequestType = 0x04\n)\n\ntype RPCDesc struct {\n\tService      string\n\tSvcNum       uint16\n\tEndpoint     string\n\tAuthHandler  bool // true if this is an auth handler\n\tRaw          bool\n\tRequestType  reflect.Type // nil if no payload\n\tResponseType reflect.Type // nil if no payload\n\tTags         []string\n\n\tExposed      bool // True if the endpoint is exposed (access level \"public\" or \"auth\")\n\tAuthRequired bool // True if the endpoint requires authentication (\"auth\")\n\n\tScrubRequestPaths    []scrub.Path\n\tScrubRequestHeaders  map[string]bool\n\tScrubResponsePaths   []scrub.Path\n\tScrubResponseHeaders map[string]bool\n}\n\ntype PathParams []PathParam\n\n// PathParam represents a parsed path parameter.\ntype PathParam struct {\n\tName  string // the name of the path parameter, without leading ':' or '*'.\n\tValue string // the parsed path parameter value.\n}\n\ntype Request struct {\n\tType             RequestType\n\tTraceID          TraceID\n\tSpanID           SpanID\n\tParentSpanID     SpanID\n\tParentTraceID    TraceID\n\tCallerEventID    TraceEventID // the event that triggered this request\n\tExtCorrelationID string       // The externally-provided correlation ID, if any.\n\n\tStart  time.Time\n\tLogger *zerolog.Logger\n\tTraced bool\n\tDefLoc uint32\n\n\t// SvcNum is the 1-based index of the service into the service list.\n\t// It's here instead of within RPCData/MsgData/Test for performance.\n\tSvcNum uint16\n\n\t// Set if Type == RPCCall\n\tRPCData *RPCData\n\n\t// Set if Type == PubSubMessage\n\tMsgData *PubSubMsgData\n\n\t// If we're running a test, this contains the test information.\n\tTest *TestData\n}\n\n// Service reports the current service, if any.\n// It checks the appropriate sub-structure based on the request type.\nfunc (req *Request) Service() string {\n\tswitch req.Type {\n\tcase RPCCall, AuthHandler:\n\t\treturn req.RPCData.Desc.Service\n\tcase PubSubMessage:\n\t\treturn req.MsgData.Desc.Service\n\tdefault:\n\t\tif req.Test != nil {\n\t\t\treturn req.Test.Service\n\t\t}\n\t\treturn \"\"\n\t}\n}\n\ntype RPCData struct {\n\tDesc *RPCDesc\n\n\t// HTTPMethod is the HTTP method used to call the endpoint.\n\t// It is not set for auth handlers.\n\tHTTPMethod string\n\n\tPath       string\n\tPathParams PathParams\n\n\t// UserID and AuthData are the authentication information\n\t// provided for this endpoint. It is not set for auth handlers\n\t// as the information is not available yet.\n\tUserID   UID\n\tAuthData any\n\n\t// Decoded request payload, for non-raw requests\n\tTypedPayload any\n\n\t// NonRawPayload is the JSON-marshalled request payload, if any, or nil.\n\t// This is never set for raw requests, as the body hasn't been read yet.\n\tNonRawPayload []byte\n\n\t// RequestHeaders contains the HTTP headers from the incoming request.\n\tRequestHeaders http.Header\n\n\t// FromEncorePlatform specifies whether the request was an\n\t// authenticated request from the Encore Platform.\n\tFromEncorePlatform bool\n\n\t// ServiceToServiceCall is true if the request was a service-to-service call.\n\t// otherwise it is false if the request originates from outside the Encore application.\n\tServiceToServiceCall bool\n\n\t// Mocked is true if the request was handled by a mock.\n\tMocked bool\n}\n\ntype PubSubTopicDesc struct {\n\tTopic      string\n\tScrubPaths []scrub.Path\n}\n\ntype PubSubSubscriptionDesc struct {\n\tService      string\n\tTopic        string\n\tSubscription string\n\tScrubPaths   []scrub.Path\n}\n\ntype PubSubMsgData struct {\n\tDesc *PubSubSubscriptionDesc\n\n\tMessageID      string\n\tPublished      time.Time\n\tAttempt        int\n\tDecodedPayload any\n\t// Payload is the JSON-encoded payload.\n\tPayload []byte\n}\n\ntype TestData struct {\n\tCtx     context.Context    // The context we're running for this test\n\tCancel  context.CancelFunc // The function to cancel this tests context\n\tCurrent *testing.T         // The current test running\n\tParent  *Request           // The parent request (if we're looking at sub-tests)\n\tService string             // the service being tested, if any\n\tConfig  *TestConfig        // The test config (should always be set) and managed by the testsupport Manager\n\n\tTestFile string // The file the test is in\n\tTestLine uint32 // The line the test is on\n\n\t// UserID and AuthData are the test-level auth information,\n\t// if overridden.\n\tUserID   UID\n\tAuthData any\n\n\tServiceInstancesMu sync.Mutex\n\tServiceInstances   map[string]any // The service instances isolated to this test\n\n\tWait sync.WaitGroup // If we're spun up async go routines, this wait allows to the test to wait for them to end\n}\n\n// TestConfig contains configuration for testing,\n//\n// It can either be the global test config, or a per-test config.\ntype TestConfig struct {\n\t// The parent test config, if any.\n\t//\n\t// If this is not set, then this test config exists at the global level.\n\tParent *TestConfig\n\n\t// Lock for the below fields\n\tMu sync.RWMutex\n\n\tServiceMocks     map[string]ServiceMock\n\tAPIMocks         map[string]map[string]ApiMock\n\tIsolatedServices *bool                // Whether to isolate services for this test\n\tEndCallbacks     []func(t *testing.T) // Callbacks to run when the test ends\n}\n\ntype ServiceMock struct {\n\tService       any\n\tRunMiddleware bool\n}\n\ntype ApiMock struct {\n\tID            uint64\n\tFunction      any\n\tRunMiddleware bool\n}\n\ntype Response struct {\n\t// HTTPStatus is the HTTP status to respond with.\n\tHTTPStatus int\n\n\t// Duration is how long the request took.\n\tDuration time.Duration\n\n\t// Err is the error returned from the handler or middleware, or nil.\n\tErr error\n\n\t// Typed response payload, for non-raw requests.\n\tTypedPayload any\n\n\t// Payload is the response payload, if any, or nil.\n\t// It is used for non-raw endpoints as well as auth handlers.\n\tPayload []byte\n\n\t// AuthUID is the resolved user id if this is an auth handler.\n\tAuthUID UID\n\n\t// Headers are HTTP headers to add to the response, set by middleware.\n\t// For non-raw endpoints, these will be merged with the standard headers.\n\tHeaders http.Header\n\n\t// RawRequestPayload contains the captured request payload, for raw requests.\n\t// It is nil if nothing was captured.\n\tRawRequestPayload           []byte\n\tRawRequestPayloadOverflowed bool // whether the payload overflowed\n\n\t// RawResponsePayload contains the captured response payload, for raw requests.\n\t// It is nil if nothing was captured.\n\tRawResponsePayload           []byte\n\tRawResponsePayloadOverflowed bool // whether the payload overflowed\n\n\t// RawResponseHeaders specifies the HTTP headers for the outgoing response,\n\t// for raw endpoints only.\n\tRawResponseHeaders http.Header\n}\n\ntype APICall struct {\n\tID     uint64 // call id\n\tSource *Request\n\tSpanID SpanID // deprecated: this is not used\n\tDefLoc uint32\n\n\t// Service/endpoint being called\n\tTargetServiceName  string\n\tTargetEndpointName string\n\n\t// Auth info for the target endpoint\n\tUserID   UID\n\tAuthData any\n\n\tStartEventID TraceEventID\n}\n\ntype AuthCall struct {\n\tID     uint64 // call id\n\tSpanID SpanID\n\tDefLoc uint32\n}\n\ntype UID string\n\ntype AuthInfo struct {\n\tUID      UID\n\tUserData any\n}\n\ntype LogLevel byte\n\nconst (\n\tLevelTrace LogLevel = 0 // unused; reserve for future use\n\tLevelDebug LogLevel = 1\n\tLevelInfo  LogLevel = 2\n\tLevelWarn  LogLevel = 3\n\tLevelError LogLevel = 4\n)\n\ntype LogFieldType byte\n\nconst (\n\tErrField      LogFieldType = 1\n\tStringField   LogFieldType = 2\n\tBoolField     LogFieldType = 3\n\tTimeField     LogFieldType = 4\n\tDurationField LogFieldType = 5\n\tUUIDField     LogFieldType = 6\n\tJSONField     LogFieldType = 7\n\tIntField      LogFieldType = 8\n\tUintField     LogFieldType = 9\n\tFloat32Field  LogFieldType = 10\n\tFloat64Field  LogFieldType = 11\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/model/trace.go",
    "content": "package model\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base32\"\n\t\"testing\"\n\t_ \"unsafe\"\n)\n\ntype (\n\tTraceID [16]byte\n\tSpanID  [8]byte\n)\n\nfunc (id TraceID) String() string {\n\tif id.IsZero() {\n\t\treturn \"\"\n\t}\n\treturn b32.EncodeToString(id[:])\n}\n\nfunc (id TraceID) IsZero() bool {\n\treturn id == TraceID{}\n}\n\nfunc (id SpanID) String() string {\n\tif id.IsZero() {\n\t\treturn \"\"\n\t}\n\treturn b32.EncodeToString(id[:])\n}\n\nfunc (id SpanID) IsZero() bool {\n\treturn id == SpanID{}\n}\n\nconst encodeHex = \"0123456789abcdefghijklmnopqrstuv\"\n\nvar b32 = base32.NewEncoding(encodeHex).WithPadding(base32.NoPadding)\n\n// GenerateConstantValsForTests if true causes GenTraceID and GenSpanID\n// to always generate the constant {0, 0, 0, ..., 1} byte sequence for testing.\nvar GenerateConstantValsForTests = false\n\n// GenTraceID generates a new trace id.\nfunc GenTraceID() (TraceID, error) {\n\tif GenerateConstantValsForTests {\n\t\treturn TraceID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, nil\n\t}\n\n\tvar traceID TraceID\n\t_, err := rand.Read(traceID[:])\n\treturn traceID, err\n}\n\n// ParseTraceID takes the hex encoded string form of a trace id and returns the bytes\nfunc ParseTraceID(str string) (TraceID, error) {\n\tvar traceID TraceID\n\t_, err := b32.Decode(traceID[:], []byte(str))\n\treturn traceID, err\n}\n\n// GenSpanID generates a span id.\nfunc GenSpanID() (SpanID, error) {\n\tif GenerateConstantValsForTests {\n\t\treturn SpanID{0, 0, 0, 0, 0, 0, 0, 1}, nil\n\t}\n\n\tvar span SpanID\n\t_, err := rand.Read(span[:])\n\treturn span, err\n}\n\n// EnableTestMode enables generation of sequential trace/span ids for the duration of the test.\nfunc EnableTestMode(t *testing.T) {\n\tGenerateConstantValsForTests = true\n\tt.Cleanup(func() { GenerateConstantValsForTests = false })\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/scrub/benchmark_test.go",
    "content": "package scrub\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n)\n\nvar benchmarkTotal int64 // prevent compiler from optimizing away the benchmark\n\nfunc BenchmarkJSONIndices(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tbounds := JSONIndices(benchJSON, benchPaths)\n\t\tbenchmarkTotal += int64(len(bounds))\n\t}\n\tb.ReportMetric(float64(len(benchJSON)*b.N)/float64(b.Elapsed().Seconds())/(1024.0*1024.0), \"MiB/s\")\n}\n\nfunc BenchmarkStdlibUnmarshal(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tdata := new(benchData)\n\t\tif err := json.Unmarshal(benchJSON, &data); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\t\tbenchmarkTotal += int64(len(data.Data))\n\t}\n\tb.ReportMetric(float64(len(benchJSON)*b.N)/float64(b.Elapsed().Seconds())/(1024.0*1024.0), \"MiB/s\")\n}\n\nvar benchPaths = []Path{\n\t// data[*].friends[*].name\n\t{\n\t\t{Kind: ObjectField, FieldName: \"data\", CaseSensitive: true},\n\t\t{Kind: ObjectField, FieldName: \"friends\", CaseSensitive: true},\n\t\t{Kind: ObjectField, FieldName: \"name\", CaseSensitive: true},\n\t},\n\t// data[*].name\n\t{\n\t\t{Kind: ObjectField, FieldName: \"data\", CaseSensitive: true},\n\t\t{Kind: ObjectField, FieldName: \"name\", CaseSensitive: true},\n\t},\n\t// data[*].address\n\t{\n\t\t{Kind: ObjectField, FieldName: \"data\", CaseSensitive: true},\n\t\t{Kind: ObjectField, FieldName: \"address\", CaseSensitive: true},\n\t},\n\t// data[*].email\n\t{\n\t\t{Kind: ObjectField, FieldName: \"data\", CaseSensitive: true},\n\t\t{Kind: ObjectField, FieldName: \"email\", CaseSensitive: true},\n\t},\n}\n\n// benchJSON is randomly generated sample data.\nvar benchJSON = []byte(`\n{\"data\": [\n  {\n    \"_id\": \"6481d7ace923338d11cdecaf\",\n    \"index\": 0,\n    \"guid\": \"04d97d05-b475-4465-b15d-de7159794dac\",\n    \"isActive\": true,\n    \"balance\": \"$2,218.53\",\n    \"picture\": \"http://placehold.it/32x32\",\n    \"age\": 31,\n    \"eyeColor\": \"green\",\n    \"name\": \"Jo Pugh\",\n    \"gender\": \"female\",\n    \"company\": \"QUANTASIS\",\n    \"email\": \"jopugh@quantasis.com\",\n    \"phone\": \"+1 (934) 590-3966\",\n    \"address\": \"955 Granite Street, Nutrioso, West Virginia, 3957\",\n    \"about\": \"Culpa ut et in sit officia voluptate. Anim dolor esse qui pariatur nisi exercitation nostrud tempor labore sint. Est duis ut est nulla in tempor id velit minim deserunt ullamco laborum. Qui dolor nostrud anim deserunt nostrud anim adipisicing nostrud aliqua. Adipisicing occaecat adipisicing duis et labore sunt id ea et magna adipisicing labore. Consectetur tempor mollit mollit duis nostrud officia in elit. Dolor dolore deserunt fugiat labore aliqua fugiat irure sunt ipsum veniam.\\r\\n\",\n    \"registered\": \"2022-07-01T04:27:58 -02:00\",\n    \"latitude\": -57.797481,\n    \"longitude\": 84.309916,\n    \"tags\": [\n      \"ad\",\n      \"eiusmod\",\n      \"ex\",\n      \"dolor\",\n      \"sunt\",\n      \"excepteur\",\n      \"duis\"\n    ],\n    \"friends\": [\n      {\n        \"id\": 0,\n        \"name\": \"Bray Hogan\"\n      },\n      {\n        \"id\": 1,\n        \"name\": \"Hope Conley\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Rollins Combs\"\n      }\n    ],\n    \"greeting\": \"Hello, Jo Pugh! You have 7 unread messages.\",\n    \"favoriteFruit\": \"strawberry\"\n  },\n  {\n    \"_id\": \"6481d7ac6b1179b1585fca2f\",\n    \"index\": 1,\n    \"guid\": \"56da4017-435e-4d4a-a9b3-6ec8ada947f6\",\n    \"isActive\": true,\n    \"balance\": \"$1,492.03\",\n    \"picture\": \"http://placehold.it/32x32\",\n    \"age\": 21,\n    \"eyeColor\": \"blue\",\n    \"name\": \"Mills Nicholson\",\n    \"gender\": \"male\",\n    \"company\": \"BOILICON\",\n    \"email\": \"millsnicholson@boilicon.com\",\n    \"phone\": \"+1 (846) 499-2163\",\n    \"address\": \"846 Albemarle Road, Celeryville, Florida, 3173\",\n    \"about\": \"Et fugiat adipisicing sit eu. Irure enim occaecat sunt duis dolor. Qui mollit velit aute excepteur aliqua qui incididunt adipisicing tempor enim.\\r\\n\",\n    \"registered\": \"2019-11-17T02:23:12 -01:00\",\n    \"latitude\": 39.918662,\n    \"longitude\": 101.881282,\n    \"tags\": [\n      \"consequat\",\n      \"laboris\",\n      \"minim\",\n      \"id\",\n      \"laborum\",\n      \"labore\",\n      \"cillum\"\n    ],\n    \"friends\": [\n      {\n        \"id\": 0,\n        \"name\": \"Cleo Juarez\"\n      },\n      {\n        \"id\": 1,\n        \"name\": \"Aline Rowe\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Ester Oconnor\"\n      }\n    ],\n    \"greeting\": \"Hello, Mills Nicholson! You have 5 unread messages.\",\n    \"favoriteFruit\": \"strawberry\"\n  },\n  {\n    \"_id\": \"6481d7ac88ee534c9f5bfff0\",\n    \"index\": 2,\n    \"guid\": \"b46ff220-b872-4d1a-b095-2a16d30b4fbd\",\n    \"isActive\": false,\n    \"balance\": \"$1,673.74\",\n    \"picture\": \"http://placehold.it/32x32\",\n    \"age\": 34,\n    \"eyeColor\": \"blue\",\n    \"name\": \"Eunice Gould\",\n    \"gender\": \"female\",\n    \"company\": \"AVIT\",\n    \"email\": \"eunicegould@avit.com\",\n    \"phone\": \"+1 (962) 427-2923\",\n    \"address\": \"477 Hinsdale Street, Edneyville, Oregon, 9253\",\n    \"about\": \"Et Lorem qui reprehenderit incididunt labore est occaecat cupidatat ullamco mollit. Amet cillum consectetur ex sunt sint anim adipisicing duis veniam do qui enim anim cillum. Irure eu elit minim et est. Fugiat officia incididunt elit ut fugiat in do mollit ullamco sint in ut consequat. Occaecat deserunt dolor ipsum veniam aliqua mollit sit culpa elit ex.\\r\\n\",\n    \"registered\": \"2018-06-17T11:52:20 -02:00\",\n    \"latitude\": 50.048343,\n    \"longitude\": -78.508861,\n    \"tags\": [\n      \"amet\",\n      \"ullamco\",\n      \"voluptate\",\n      \"voluptate\",\n      \"dolor\",\n      \"elit\",\n      \"id\"\n    ],\n    \"friends\": [\n      {\n        \"id\": 0,\n        \"name\": \"Ericka Pennington\"\n      },\n      {\n        \"id\": 1,\n        \"name\": \"Cain Pearson\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Pacheco Walters\"\n      }\n    ],\n    \"greeting\": \"Hello, Eunice Gould! You have 10 unread messages.\",\n    \"favoriteFruit\": \"banana\"\n  },\n  {\n    \"_id\": \"6481d7acce8494cef525f2bc\",\n    \"index\": 3,\n    \"guid\": \"1498919d-fae5-4ade-aa4a-7eb332d899e6\",\n    \"isActive\": false,\n    \"balance\": \"$1,592.80\",\n    \"picture\": \"http://placehold.it/32x32\",\n    \"age\": 33,\n    \"eyeColor\": \"brown\",\n    \"name\": \"Lindsay Howe\",\n    \"gender\": \"male\",\n    \"company\": \"EXTRAGEN\",\n    \"email\": \"lindsayhowe@extragen.com\",\n    \"phone\": \"+1 (940) 473-3478\",\n    \"address\": \"759 Montieth Street, Dellview, Wyoming, 3283\",\n    \"about\": \"Commodo minim sint non id aute minim exercitation. In qui nulla ut non reprehenderit adipisicing duis elit. Dolore non aliqua est veniam nisi id eiusmod aliqua non fugiat labore. Ullamco dolore culpa aliquip id tempor laboris. Officia adipisicing pariatur ullamco sunt veniam pariatur ullamco ullamco nostrud nisi qui mollit. Nisi velit ut commodo magna qui fugiat eu ut amet qui consequat excepteur.\\r\\n\",\n    \"registered\": \"2014-11-27T02:20:32 -01:00\",\n    \"latitude\": -53.18764,\n    \"longitude\": -164.847231,\n    \"tags\": [\n      \"qui\",\n      \"nulla\",\n      \"quis\",\n      \"exercitation\",\n      \"non\",\n      \"occaecat\",\n      \"est\"\n    ],\n    \"friends\": [\n      {\n        \"id\": 0,\n        \"name\": \"Stone Gallagher\"\n      },\n      {\n        \"id\": 1,\n        \"name\": \"Chavez Pope\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Farmer Mueller\"\n      }\n    ],\n    \"greeting\": \"Hello, Lindsay Howe! You have 4 unread messages.\",\n    \"favoriteFruit\": \"apple\"\n  },\n  {\n    \"_id\": \"6481d7ac14c51c8748cbab88\",\n    \"index\": 4,\n    \"guid\": \"f72fa31e-bd75-46ce-a9bf-53675f50f01c\",\n    \"isActive\": true,\n    \"balance\": \"$1,160.87\",\n    \"picture\": \"http://placehold.it/32x32\",\n    \"age\": 37,\n    \"eyeColor\": \"brown\",\n    \"name\": \"Minerva Sims\",\n    \"gender\": \"female\",\n    \"company\": \"NETPLAX\",\n    \"email\": \"minervasims@netplax.com\",\n    \"phone\": \"+1 (887) 477-2537\",\n    \"address\": \"864 Ryder Avenue, Tedrow, Virginia, 4180\",\n    \"about\": \"Magna id sit adipisicing eiusmod fugiat nulla voluptate sint culpa sit consectetur est sint. Culpa velit eu velit non Lorem irure adipisicing fugiat culpa aliquip Lorem eiusmod. Occaecat ad est commodo est irure veniam magna deserunt laboris commodo quis cupidatat occaecat. Consequat exercitation officia commodo id. Incididunt magna sunt mollit labore. Proident exercitation est enim mollit reprehenderit Lorem anim. Fugiat consectetur elit anim reprehenderit.\\r\\n\",\n    \"registered\": \"2014-02-05T11:36:57 -01:00\",\n    \"latitude\": 5.818084,\n    \"longitude\": 156.041539,\n    \"tags\": [\n      \"do\",\n      \"amet\",\n      \"ex\",\n      \"dolor\",\n      \"cupidatat\",\n      \"dolor\",\n      \"tempor\"\n    ],\n    \"friends\": [\n      {\n        \"id\": 0,\n        \"name\": \"Karen Dickerson\"\n      },\n      {\n        \"id\": 1,\n        \"name\": \"Frye Jones\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Rosie Travis\"\n      }\n    ],\n    \"greeting\": \"Hello, Minerva Sims! You have 5 unread messages.\",\n    \"favoriteFruit\": \"apple\"\n  },\n  {\n    \"_id\": \"6481d7ac80e906f930d8deea\",\n    \"index\": 5,\n    \"guid\": \"17c377e0-0ba6-46ea-8faa-6c0301ffb3d4\",\n    \"isActive\": true,\n    \"balance\": \"$3,838.92\",\n    \"picture\": \"http://placehold.it/32x32\",\n    \"age\": 37,\n    \"eyeColor\": \"green\",\n    \"name\": \"Fischer Avery\",\n    \"gender\": \"male\",\n    \"company\": \"POLARIUM\",\n    \"email\": \"fischeravery@polarium.com\",\n    \"phone\": \"+1 (876) 442-3098\",\n    \"address\": \"339 Wyona Street, Wadsworth, Arizona, 5446\",\n    \"about\": \"Laborum sint consectetur et ex officia. Aliqua veniam aliquip aliqua pariatur deserunt quis esse nisi non amet culpa. Sint nostrud magna sit amet mollit fugiat non adipisicing.\\r\\n\",\n    \"registered\": \"2022-01-30T12:04:00 -01:00\",\n    \"latitude\": 33.64373,\n    \"longitude\": 114.28595,\n    \"tags\": [\n      \"ex\",\n      \"ipsum\",\n      \"adipisicing\",\n      \"sint\",\n      \"elit\",\n      \"et\",\n      \"ad\"\n    ],\n    \"friends\": [\n      {\n        \"id\": 0,\n        \"name\": \"Gayle Walker\"\n      },\n      {\n        \"id\": 1,\n        \"name\": \"Sherman Tyler\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Gladys Shaffer\"\n      }\n    ],\n    \"greeting\": \"Hello, Fischer Avery! You have 2 unread messages.\",\n    \"favoriteFruit\": \"banana\"\n  }\n]}\n`)\n\ntype benchData struct {\n\tData []struct {\n\t\tId         string   `json:\"_id\"`\n\t\tIndex      int      `json:\"index\"`\n\t\tGuid       string   `json:\"guid\"`\n\t\tIsActive   bool     `json:\"isActive\"`\n\t\tBalance    string   `json:\"balance\"`\n\t\tPicture    string   `json:\"picture\"`\n\t\tAge        int      `json:\"age\"`\n\t\tEyeColor   string   `json:\"eyeColor\"`\n\t\tName       string   `json:\"name\"`\n\t\tGender     string   `json:\"gender\"`\n\t\tCompany    string   `json:\"company\"`\n\t\tEmail      string   `json:\"email\"`\n\t\tPhone      string   `json:\"phone\"`\n\t\tAddress    string   `json:\"address\"`\n\t\tAbout      string   `json:\"about\"`\n\t\tRegistered string   `json:\"registered\"`\n\t\tLatitude   float64  `json:\"latitude\"`\n\t\tLongitude  float64  `json:\"longitude\"`\n\t\tTags       []string `json:\"tags\"`\n\t\tFriends    []struct {\n\t\t\tId   int    `json:\"id\"`\n\t\t\tName string `json:\"name\"`\n\t\t} `json:\"friends\"`\n\t\tGreeting      string `json:\"greeting\"`\n\t\tFavoriteFruit string `json:\"favoriteFruit\"`\n\t} `json:\"data\"`\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/scrub/scanner.go",
    "content": "package scrub\n\nimport \"io\"\n\nfunc newScanner(r io.ByteReader) *scanner {\n\treturn &scanner{r: r}\n}\n\ntype scanner struct {\n\tr          io.ByteReader\n\tit         scanItem\n\terr        error // first error encountered; EOF on successful completion\n\tpos        int64\n\tlastRead   byte\n\tunreadByte byte // unread byte in storage, or 0 if none.\n\n\t// stack keeps track of the stack of objects/arrays we've started\n\t// but not yet completed parsing. It's used to determine whether\n\t// a value encountered in parsing an object is a key or a value.\n\tstack []scanState\n}\n\n// Next advances the scanner to the next token.\n// It reports false when the whole input stream has been consumed,\n// or when reading it encountered an error.\nfunc (s *scanner) Next() bool {\n\ts.it = s.scan()\n\treturn s.err == nil || s.it.tok != 0\n}\n\nfunc (s *scanner) Item() scanItem {\n\treturn s.it\n}\n\n// Err reports the first error encountered during scanning,\n// or nil if no errors were encountered.\n//\n// When the underlying reader reports io.EOF this is not reported as an\n// error but as the successful completion of scanning, so Err reports nil.\nfunc (s *scanner) Err() error {\n\tif s.err == io.EOF {\n\t\treturn nil\n\t}\n\treturn s.err\n}\n\n// scan consumes a single scan item.\nfunc (s *scanner) scan() scanItem {\n\tit, preContext, postContext := s.scanOne()\n\n\t// If we're inside an object, update the key detection\n\tif n := len(s.stack); n > 0 {\n\t\tif st := &s.stack[n-1]; st.tok == objectBegin {\n\t\t\tswitch it.tok {\n\t\t\tcase objectBegin, arrayBegin, literal:\n\t\t\t\tst.isKey = !st.isKey\n\t\t\t}\n\t\t\tif postContext == ':' {\n\t\t\t\tst.isKey = true\n\t\t\t} else if preContext == ':' {\n\t\t\t\tst.isKey = false\n\t\t\t}\n\n\t\t\tit.isMapKey = st.isKey\n\t\t}\n\t}\n\n\tswitch it.tok {\n\tcase objectBegin, arrayBegin:\n\t\ts.stack = append(s.stack, scanState{tok: it.tok})\n\tcase objectEnd, arrayEnd:\n\t\t// Pop the last entry off the stack, if we have one.\n\t\t// Assume begin/end are balanced; it's not valid JSON if they're not\n\t\t// and this is just a best-effort approach anyway.\n\t\tif n := len(s.stack); n > 0 {\n\t\t\ts.stack = s.stack[:n-1]\n\t\t}\n\t}\n\n\treturn it\n}\n\n// scanOne scans a single item.\n// The extraContext reports whether the scanning encountered a\n// newline, comma, or colon in the process.\nfunc (s *scanner) scanOne() (it scanItem, preContext, postContext byte) {\n\tfor {\n\t\tc := s.readToken()\n\t\tswitch c {\n\t\tcase '\"':\n\t\t\tit = s.scanString()\n\t\tcase '{':\n\t\t\tit = scanItem{from: s.pos - 1, to: s.pos, tok: objectBegin}\n\t\tcase '}':\n\t\t\tit = scanItem{from: s.pos - 1, to: s.pos, tok: objectEnd}\n\t\tcase '[':\n\t\t\tit = scanItem{from: s.pos - 1, to: s.pos, tok: arrayBegin}\n\t\tcase ']':\n\t\t\tit = scanItem{from: s.pos - 1, to: s.pos, tok: arrayEnd}\n\t\tcase ':', ',':\n\t\t\tpreContext = c\n\t\t\tcontinue\n\t\tcase 0:\n\t\t\treturn\n\t\tdefault:\n\t\t\tit = s.scanLiteral()\n\t\t}\n\t\tpostContext = s.peekToken()\n\t\treturn\n\t}\n}\n\nfunc (s *scanner) scanString() scanItem {\n\t// Use s.pos-1 since scan already consumed the first byte.\n\tit := scanItem{from: s.pos - 1, tok: literal}\n\n\tfor {\n\t\tb := s.readByte()\n\n\t\tif b == '\\\\' {\n\t\t\tb = s.readByte()\n\t\t\t// Escaped symbols never signal the end of the string,\n\t\t\t// so it's safe to just continue here regardless of value.\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch b {\n\t\tcase '\"':\n\t\t\tit.to = s.pos\n\t\t\treturn it\n\t\tcase '\\n', '\\r':\n\t\t\t// newline or line feed, treat this as the end of the string.\n\t\t\ts.unread()\n\t\t\tit.to = s.pos\n\t\t\treturn it\n\t\tcase 0:\n\t\t\t// end of input\n\t\t\tit.to = s.pos\n\t\t\treturn it\n\t\t}\n\t}\n}\n\nfunc (s *scanner) scanLiteral() scanItem {\n\t// Use s.pos-1 since scan already consumed the first byte.\n\tit := scanItem{from: s.pos - 1, tok: literal}\n\tfor {\n\t\tb := s.readByte()\n\t\tswitch b {\n\t\tcase '\"', ' ', '\\r', '\\n', '\\t', ',', ':', '{', '[', ']', '}':\n\t\t\t// new token to follow\n\t\t\ts.unread()\n\t\t\tit.to = s.pos\n\t\t\treturn it\n\t\tcase 0:\n\t\t\t// end of input\n\t\t\tit.to = s.pos\n\t\t\treturn it\n\t\t}\n\t}\n}\n\n// readToken returns the next token in the input.\n// If there is no more data it reports 0.\nfunc (s *scanner) readToken() byte {\n\tfor {\n\t\tb := s.readByte()\n\t\t// Ignore whitespace.\n\t\tswitch b {\n\t\tcase ' ', '\\t', '\\n', '\\r':\n\t\t\tcontinue\n\t\t}\n\t\treturn b\n\t}\n}\n\n// peekToken peeks at the next token.\n// It is equivalent to read() followed by unread().\nfunc (s *scanner) peekToken() byte {\n\tb := s.readToken()\n\tif b != 0 {\n\t\ts.unread()\n\t}\n\treturn b\n}\n\n// readByte reads a single byte and returns it.\n// If there is no more data, it reports 0.\nfunc (s *scanner) readByte() byte {\n\t// Do we have an unread byte?\n\tvar b byte\n\tif b = s.unreadByte; b != 0 {\n\t\ts.lastRead = b\n\t\ts.unreadByte = 0\n\t\ts.pos++\n\t\treturn b\n\t}\n\n\tif s.err != nil {\n\t\treturn 0\n\t}\n\n\tb, s.err = s.r.ReadByte()\n\tif s.err == nil {\n\t\ts.lastRead = b\n\t\ts.pos++\n\t}\n\treturn b\n}\n\nfunc (s *scanner) unread() {\n\tif s.unreadByte != 0 {\n\t\tpanic(\"cannot unread multiple bytes in a row\")\n\t}\n\ts.unreadByte = s.lastRead\n\ts.pos--\n}\n\ntype scanItem struct {\n\tfrom, to int64\n\ttok      token\n\tisMapKey bool\n}\n\nfunc (it scanItem) Bounds() Bounds {\n\treturn Bounds{\n\t\tFrom: int(it.from),\n\t\tTo:   int(it.to),\n\t}\n}\n\n// token represents a single\ntype token uint8\n\n//go:generate stringer -type=token\nconst (\n\tunknown token = iota\n\n\tobjectBegin // {\n\tobjectEnd   // }\n\tarrayBegin  // [\n\tarrayEnd    // ]\n\tliteral\n)\n\ntype scanState struct {\n\ttok   token\n\tisKey bool\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/scrub/scanner_test.go",
    "content": "package scrub\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n)\n\nvar tests = []scannerTest{\n\t{\n\t\tname: \"simple\",\n\t\tsrc:  `{\"foo\": \"bar\"}`,\n\t\twant: []tokDesc{\n\t\t\tobjStart, k(str(\"foo\")), str(\"bar\"), objEnd,\n\t\t},\n\t},\n\t{\n\t\tname: \"missing_quotes\",\n\t\tsrc:  `{\"foo\": \"bar}`,\n\t\twant: []tokDesc{\n\t\t\tobjStart, k(str(\"foo\")), raw(`\"bar}`),\n\t\t},\n\t},\n\t{\n\t\tname: \"escaped_string\",\n\t\tsrc:  `\"foo\\\"bar\"`,\n\t\twant: []tokDesc{\n\t\t\tstr(\"foo\\\"bar\"),\n\t\t},\n\t},\n\t{\n\t\tname: \"escaped_string_at_end\",\n\t\tsrc:  `\"foo\\\"\"`,\n\t\twant: []tokDesc{\n\t\t\tstr(\"foo\\\"\"),\n\t\t},\n\t},\n\t{\n\t\tname: \"escaped_quoted_string\",\n\t\tsrc:  `[\"\\\"one\\\"\"]`,\n\t\twant: []tokDesc{\n\t\t\tarrStart, str(\"\\\"one\\\"\"), arrEnd,\n\t\t},\n\t},\n\t{\n\t\tname: \"newline_reset_key\",\n\t\tsrc:  \"{0: true, 1:\\n2: true}\",\n\t\twant: []tokDesc{\n\t\t\tobjStart,\n\t\t\tk(num(0)), raw(\"true\"),\n\t\t\tk(num(1)),\n\t\t\tk(num(2)), raw(\"true\"),\n\t\t\tobjEnd,\n\t\t},\n\t},\n\t{\n\t\tname: \"newline_missing_quotes_reset_key\",\n\t\tsrc: `{\"a\n\"b\": \"c\"\n}`,\n\t\twant: []tokDesc{\n\t\t\tobjStart,\n\t\t\tk(raw(`\"a`)),\n\t\t\tk(str(\"b\")), str(\"c\"),\n\t\t\tobjEnd,\n\t\t},\n\t},\n\t{\n\t\tname: \"invalid_array\",\n\t\tsrc:  `[{\"foo\", [\"test\"]]`,\n\t\twant: []tokDesc{\n\t\t\tarrStart, objStart, k(str(\"foo\")),\n\t\t\tarrStart, str(\"test\"), arrEnd,\n\t\t\tarrEnd,\n\t\t},\n\t},\n\t{\n\t\tname: \"array_as_obj_key\",\n\t\tsrc:  `[{[\"test\"]]`,\n\t\twant: []tokDesc{\n\t\t\tarrStart, objStart,\n\t\t\tarrStart, str(\"test\"), arrEnd,\n\t\t\tarrEnd,\n\t\t},\n\t},\n\t{\n\t\tname: \"multiple_top_level_objs\",\n\t\tsrc:  `{\"foo\", \"bar\"}[\"test\"]null`,\n\t\twant: []tokDesc{\n\t\t\tobjStart, k(str(\"foo\")), str(\"bar\"), objEnd,\n\t\t\tarrStart, str(\"test\"), arrEnd,\n\t\t\traw(\"null\"),\n\t\t},\n\t},\n\t{\n\t\tname: \"whitespace_positions\",\n\t\tsrc:  \"{\\n  \\\"a\\\": {\\n    \\\"b\\\": 1\\n  }\\n}\",\n\t\twant: []tokDesc{\n\t\t\tobjStart, k(str(\"a\")),\n\t\t\tobjStart, k(str(\"b\")), raw(\"1\"), objEnd,\n\t\t\tobjEnd,\n\t\t},\n\t},\n}\n\nfunc FuzzScanner(f *testing.F) {\n\tfor _, test := range tests {\n\t\tf.Add(test.src)\n\t}\n\tf.Fuzz(func(t *testing.T, src string) {\n\t\ts := newScanner(strings.NewReader(src))\n\t\tfor s.Next() {\n\t\t\t_ = s.Item()\n\t\t}\n\t\tif err := s.Err(); err != nil {\n\t\t\tt.Errorf(\"%v\", err)\n\t\t}\n\t})\n}\n\nfunc TestScanner(t *testing.T) {\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tlit := func(it scanItem) string {\n\t\t\t\tif it.from < 0 || it.from > int64(len(test.src)) {\n\t\t\t\t\treturn fmt.Sprintf(\"<INVALID FROM: %d>\", it.from)\n\t\t\t\t} else if it.to < 0 || it.to > int64(len(test.src)) {\n\t\t\t\t\treturn fmt.Sprintf(\"<INVALID TO: %d>\", it.to)\n\t\t\t\t}\n\t\t\t\treturn test.src[it.from:it.to]\n\t\t\t}\n\n\t\t\ts := newScanner(strings.NewReader(test.src))\n\t\t\tfor i, want := range test.want {\n\t\t\t\tif !s.Next() {\n\t\t\t\t\tt.Fatalf(\"tok[%d]: got Next() = false, want true\", i)\n\t\t\t\t}\n\t\t\t\tit := s.Item()\n\t\t\t\tif it.tok != want.tok {\n\t\t\t\t\tt.Fatalf(\"tok[%d]: got tok kind %s, want %s\", i, it.tok, want.tok)\n\t\t\t\t} else if got := lit(it); got != want.val {\n\t\t\t\t\tt.Fatalf(\"tok[%d]: got lit %s, want %s\", i, got, want.val)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif s.Next() {\n\t\t\t\tit := s.Item()\n\t\t\t\tt.Fatalf(\"got extra token beyond expected tokens: %s (val: %s)\", it.tok, lit(it))\n\t\t\t}\n\t\t\tif err := s.Err(); err != nil {\n\t\t\t\tt.Fatalf(\"got err %v, want nil\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype scannerTest struct {\n\tname string\n\tsrc  string\n\twant []tokDesc\n}\n\ntype tokDesc struct {\n\ttok      token\n\tval      string\n\tisMapKey bool\n}\n\nfunc str(s string) tokDesc {\n\treturn raw(strconv.Quote(s))\n}\n\nfunc num(n int) tokDesc {\n\treturn tokDesc{tok: literal, val: strconv.Itoa(n)}\n}\n\nfunc raw(s string) tokDesc {\n\treturn tokDesc{tok: literal, val: s}\n}\n\nfunc k(t tokDesc) tokDesc {\n\tt.isMapKey = true\n\treturn t\n}\n\nvar (\n\tobjStart = tokDesc{tok: objectBegin, val: \"{\"}\n\tobjEnd   = tokDesc{tok: objectEnd, val: \"}\"}\n\tarrStart = tokDesc{tok: arrayBegin, val: \"[\"}\n\tarrEnd   = tokDesc{tok: arrayEnd, val: \"]\"}\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/scrub/scrub.go",
    "content": "package scrub\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/fmstephe/unsafeutil\"\n)\n\ntype EntryKind int\n\nconst (\n\tObjectField EntryKind = iota\n\tMapKey\n\tMapValue\n)\n\n// scrubNode is an internal EntryKind for scrubbing the node\nconst scrubNode EntryKind = -1\n\ntype Path []PathEntry\n\ntype PathEntry struct {\n\tKind          EntryKind\n\tFieldName     string\n\tCaseSensitive bool\n}\n\n// JSON scrubs the input JSON data, substituting values at the given paths\n// with replaceWith.\n//\n// It returns the scrubbed data. If no substitutions were made scrubbed\n// is the same slice as input.\nfunc JSON(input []byte, paths []Path, replaceWith []byte) (scrubbed []byte) {\n\tindices := JSONIndices(input, paths)\n\treturn scrub(input, indices, replaceWith)\n}\n\nfunc scrub(input []byte, indices []Bounds, replaceWith []byte) (scrubbed []byte) {\n\tif len(indices) == 0 {\n\t\treturn input\n\t}\n\n\t// Compute the new length\n\tnewLen := len(input)\n\treplaceLen := len(replaceWith)\n\tfor _, idx := range indices {\n\t\tsegLen := idx.To - idx.From\n\t\tnewLen = newLen - segLen + replaceLen\n\t}\n\n\tscrubbed = make([]byte, newLen)\n\tprevEnd := 0\n\tdstIdx := 0\n\tfor _, idx := range indices {\n\t\tdstIdx += copy(scrubbed[dstIdx:], input[prevEnd:idx.From])\n\t\tdstIdx += copy(scrubbed[dstIdx:], replaceWith)\n\t\tprevEnd = idx.To\n\t}\n\n\tcopy(scrubbed[dstIdx:], input[prevEnd:])\n\n\treturn scrubbed\n}\n\ntype Bounds struct {\n\tFrom, To int\n}\n\n// JSONIndices computes the indices to replace in order to scrub the input JSON data.\nfunc JSONIndices(input []byte, paths []Path) []Bounds {\n\tnodes := groupNodes(paths)\n\tstr := newStream(input, nodes)\n\treturn str.Process()\n}\n\nfunc newStream(input []byte, rootNodes []node) *stream {\n\treturn &stream{\n\t\ts:         newScanner(bytes.NewReader(input)),\n\t\tinput:     input,\n\t\trootNodes: rootNodes,\n\t}\n}\n\ntype stream struct {\n\ts         *scanner\n\tinput     []byte\n\trootNodes []node\n\n\tdebugLit string\n\ttok      token\n\tpos      Bounds\n\tmapKey   bool\n\n\tunreadItem scanItem\n\n\ttoScrub []Bounds\n}\n\nfunc (s *stream) Process() []Bounds {\n\ts.next() // initialize\n\tfor s.tok != unknown {\n\t\ts.processValue(s.rootNodes)\n\t}\n\treturn s.toScrub\n}\n\n// processValue processes a single complete value (such as an entire object\n// or array recursively) and returns its bounds.\nfunc (s *stream) processValue(nodes []node) {\n\tif s.scrubNow(nodes) {\n\t\tbounds := s.skipValue()\n\t\ts.toScrub = append(s.toScrub, bounds)\n\t\treturn\n\t} else if len(nodes) == 0 {\n\t\t// no more nodes; just skip the value entirely.\n\t\ts.skipValue()\n\t\treturn\n\t}\n\n\tswitch s.tok {\n\tcase objectBegin:\n\t\ts.processObject(nodes)\n\tcase arrayBegin:\n\t\ts.processArray(nodes)\n\tdefault:\n\t\t// nothing to do\n\t\ts.next()\n\t}\n}\n\nfunc (s *stream) processArray(nodes []node) {\n\ts.next() // Move to first array value\n\tfor {\n\t\tif s.tok == unknown {\n\t\t\treturn\n\t\t} else if s.tok == arrayEnd {\n\t\t\ts.next()\n\t\t\treturn\n\t\t}\n\n\t\ts.processValue(nodes)\n\t}\n}\n\nfunc (s *stream) processObject(nodes []node) {\n\ts.next() // Move to the first key\n\tfor {\n\t\tif s.tok == unknown {\n\t\t\treturn\n\t\t} else if s.tok == objectEnd {\n\t\t\ts.next()\n\t\t\treturn\n\t\t}\n\n\t\t// Determine which nodes to continue with\n\t\tcurrNodes, valueNodes := s.matchingMapNodes(nodes)\n\t\ts.processValue(currNodes)\n\n\t\t// If we have a map value next, process it.\n\t\tif s.isMapValue() {\n\t\t\ts.processValue(valueNodes)\n\t\t}\n\t}\n}\n\nfunc (s *stream) skipValue() Bounds {\n\tstart := s.pos.From\n\tend := s.pos.To\n\tvar depth int\n\tfor {\n\t\tswitch s.tok {\n\t\tcase objectBegin, arrayBegin:\n\t\t\tdepth++\n\t\tcase objectEnd, arrayEnd:\n\t\t\tdepth--\n\t\t}\n\t\tend = s.pos.To\n\t\ts.next()\n\t\tif depth <= 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn Bounds{From: start, To: end}\n}\n\nfunc (s *stream) matchingMapNodes(nodes []node) (currNodes, valueNodes []node) {\n\tfor _, n := range nodes {\n\t\tif (n.Kind == MapKey && s.mapKey) || (n.Kind == MapValue && !s.mapKey) {\n\t\t\tcurrNodes = append(currNodes, n.Children...)\n\t\t} else if n.Kind == ObjectField && s.mapKey {\n\t\t\tfieldName := s.input[s.pos.From:s.pos.To]\n\t\t\tcs := n.CaseSensitive\n\n\t\t\t// Have we found a matching field?\n\t\t\tvar fieldMatch bool\n\t\t\tif cs {\n\t\t\t\tfieldMatch = bytes.Equal(fieldName, unsafeutil.StringToBytes(n.FieldName))\n\t\t\t} else {\n\t\t\t\tfieldMatch = bytes.EqualFold(fieldName, unsafeutil.StringToBytes(n.FieldName))\n\t\t\t}\n\n\t\t\tif fieldMatch {\n\t\t\t\tvalueNodes = append(valueNodes, n.Children...)\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// isMapValue reports whether the current token is a map value.\nfunc (s *stream) isMapValue() bool {\n\tif s.mapKey {\n\t\treturn false\n\t}\n\tswitch s.tok {\n\tcase objectBegin, arrayBegin, literal:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (s *stream) scrubNow(nodes []node) bool {\n\tfor _, n := range nodes {\n\t\tif n.Kind == scrubNode {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (s *stream) next() {\n\t// Handle unread\n\tif s.unreadItem.tok != 0 {\n\t\tit := s.unreadItem\n\t\ts.unreadItem = scanItem{}\n\t\ts.tok, s.pos, s.mapKey = it.tok, it.Bounds(), it.isMapKey\n\t\ts.debugLit = string(s.input[s.pos.From:s.pos.To])\n\t\treturn\n\t}\n\n\tif !s.s.Next() {\n\t\ts.tok = unknown\n\t\ts.debugLit = \"\"\n\t} else {\n\t\tit := s.s.Item()\n\t\ts.tok, s.pos, s.mapKey = it.tok, it.Bounds(), it.isMapKey\n\t\ts.debugLit = string(s.input[s.pos.From:s.pos.To])\n\t}\n}\n\nfunc (s *stream) unread() {\n\tif s.unreadItem.tok != 0 {\n\t\tpanic(fmt.Sprintf(\"double unread: %s followed by %s\", s.unreadItem.tok, s.tok))\n\t}\n\n\ts.unreadItem = scanItem{\n\t\ttok:      s.tok,\n\t\tfrom:     int64(s.pos.From),\n\t\tto:       int64(s.pos.To),\n\t\tisMapKey: s.mapKey,\n\t}\n}\n\n// groupNodes transforms the paths into a node tree for efficient matching.\nfunc groupNodes(paths []Path) []node {\n\tfindMatch := func(parent *node, e PathEntry) int {\n\t\tfor idx, c := range parent.Children {\n\t\t\tif c.Kind != e.Kind {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif e.Kind != ObjectField {\n\t\t\t\t// The other attributes only matter for ObjectField\n\t\t\t\treturn idx\n\t\t\t}\n\n\t\t\tif e.CaseSensitive != c.CaseSensitive {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar equal bool\n\t\t\tif e.CaseSensitive {\n\t\t\t\tequal = strings.EqualFold(e.FieldName, c.FieldName)\n\t\t\t} else {\n\t\t\t\tequal = e.FieldName == c.FieldName\n\t\t\t}\n\t\t\tif equal {\n\t\t\t\treturn idx\n\t\t\t}\n\t\t}\n\t\treturn -1\n\t}\n\n\thasScrub := func(parent *node) bool {\n\t\treturn len(parent.Children) == 1 && parent.Children[0].Kind == scrubNode\n\t}\n\n\troot := node{}\n\tfor _, path := range paths {\n\t\tparent := &root\n\t\tfor _, e := range path {\n\t\t\t// If we're already scrubbing this path, we're done.\n\t\t\tif hasScrub(parent) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tidx := findMatch(parent, e)\n\t\t\tif idx == -1 {\n\t\t\t\tparent.Children = append(parent.Children, node{\n\t\t\t\t\tKind:          e.Kind,\n\t\t\t\t\tFieldName:     e.FieldName,\n\t\t\t\t\tCaseSensitive: e.CaseSensitive,\n\t\t\t\t})\n\t\t\t\tparent = &parent.Children[len(parent.Children)-1]\n\t\t\t} else {\n\t\t\t\tparent = &parent.Children[idx]\n\t\t\t}\n\t\t}\n\n\t\t// At the end of each path we want to scrub the current value.\n\t\t// Add a synthetic node to do so.\n\t\tparent.Children = []node{{Kind: scrubNode}}\n\t}\n\n\treturn root.Children\n}\n\ntype node struct {\n\tKind          EntryKind\n\tFieldName     string\n\tCaseSensitive bool\n\tChildren      []node\n}\n\nfunc unsafeStrToBytes(s string) []byte {\n\treturn *(*[]byte)(unsafe.Pointer(&s))\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/scrub/scrub_test.go",
    "content": "package scrub\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nfunc Test_groupNodes(t *testing.T) {\n\tf := func(name string) PathEntry {\n\t\treturn PathEntry{Kind: ObjectField, FieldName: name, CaseSensitive: false}\n\t}\n\tcf := func(name string) PathEntry {\n\t\treturn PathEntry{Kind: ObjectField, FieldName: name, CaseSensitive: true}\n\t}\n\tnf := func(name string, ch ...node) node {\n\t\treturn node{Kind: ObjectField, FieldName: name, CaseSensitive: false, Children: ch}\n\t}\n\tncf := func(name string, ch ...node) node {\n\t\treturn node{Kind: ObjectField, FieldName: name, CaseSensitive: true, Children: ch}\n\t}\n\tscr := node{Kind: scrubNode}\n\n\ttests := []struct {\n\t\tname  string\n\t\tpaths []Path\n\t\twant  []node\n\t}{\n\t\t{\n\t\t\tname: \"single\",\n\t\t\tpaths: []Path{\n\t\t\t\t{f(\"a\")},\n\t\t\t},\n\t\t\twant: []node{\n\t\t\t\tnf(\"a\", scr),\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname: \"two_separate\",\n\t\t\tpaths: []Path{\n\t\t\t\t{f(\"a\")},\n\t\t\t\t{f(\"b\")},\n\t\t\t},\n\t\t\twant: []node{nf(\"a\", scr), nf(\"b\", scr)},\n\t\t},\n\n\t\t{\n\t\t\tname: \"case_sensitive_match\",\n\t\t\tpaths: []Path{\n\t\t\t\t{cf(\"a\")},\n\t\t\t\t{cf(\"a\"), f(\"b\")},\n\t\t\t},\n\t\t\twant: []node{ncf(\"a\", scr)},\n\t\t},\n\n\t\t{\n\t\t\tname: \"case_sensitive_mismatch\",\n\t\t\tpaths: []Path{\n\t\t\t\t{cf(\"a\")},\n\t\t\t\t{f(\"a\"), cf(\"a\")},\n\t\t\t},\n\t\t\twant: []node{\n\t\t\t\tncf(\"a\", scr),\n\t\t\t\tnf(\"a\", ncf(\"a\", scr)),\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname: \"complex\",\n\t\t\tpaths: []Path{\n\t\t\t\t{f(\"a\"), f(\"b\"), f(\"c\")},\n\t\t\t\t{f(\"a\"), f(\"d\"), cf(\"e\")},\n\t\t\t\t{f(\"a\"), cf(\"b\"), cf(\"e\")},\n\t\t\t},\n\t\t\twant: []node{\n\t\t\t\tnf(\"a\",\n\t\t\t\t\tnf(\"b\", nf(\"c\", scr)),\n\t\t\t\t\tnf(\"d\", ncf(\"e\", scr)),\n\t\t\t\t\tncf(\"b\", ncf(\"e\", scr)),\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := groupNodes(tt.paths)\n\t\t\tif diff := cmp.Diff(got, tt.want); diff != \"\" {\n\t\t\t\tt.Error(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_scrub(t *testing.T) {\n\ttests := []struct {\n\t\tinput   string\n\t\tbounds  []Bounds\n\t\treplace string\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tinput:   \"aaa bbb ccc\",\n\t\t\tbounds:  []Bounds{{0, 3}},\n\t\t\treplace: \"X\",\n\t\t\twant:    \"X bbb ccc\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"aaa bbb ccc\",\n\t\t\tbounds:  []Bounds{{0, 1}, {1, 2}, {2, 3}},\n\t\t\treplace: \"X\",\n\t\t\twant:    \"XXX bbb ccc\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"aaa bbb ccc\",\n\t\t\tbounds:  []Bounds{{0, 1}, {1, 2}, {2, 3}},\n\t\t\treplace: \"XX\",\n\t\t\twant:    \"XXXXXX bbb ccc\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"aaa\",\n\t\t\tbounds:  []Bounds{{0, 3}},\n\t\t\treplace: \"XX\",\n\t\t\twant:    \"XX\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"a\",\n\t\t\tbounds:  []Bounds{{0, 0}},\n\t\t\treplace: \"XXX\",\n\t\t\twant:    \"XXXa\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"a\",\n\t\t\tbounds:  []Bounds{{1, 1}},\n\t\t\treplace: \"XXX\",\n\t\t\twant:    \"aXXX\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"\",\n\t\t\tbounds:  []Bounds{{0, 0}},\n\t\t\treplace: \"XXX\",\n\t\t\twant:    \"XXX\",\n\t\t},\n\t\t{\n\t\t\tinput:   \"a\",\n\t\t\tbounds:  []Bounds{{0, 1}},\n\t\t\treplace: \"XXX\",\n\t\t\twant:    \"XXX\",\n\t\t},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tif got := scrub([]byte(tt.input), tt.bounds, []byte(tt.replace)); !bytes.Equal(got, []byte(tt.want)) {\n\t\t\t\tt.Errorf(\"scrub() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\tpaths      []Path\n\t\twantOutput string\n\t}{\n\t\t{\n\t\t\tname:  \"simple_object\",\n\t\t\tinput: `{\"a\": \"1234\"}`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{{Kind: ObjectField, FieldName: `\"a\"`, CaseSensitive: true}},\n\t\t\t},\n\t\t\twantOutput: `{\"a\": \"[sensitive]\"}`,\n\t\t},\n\t\t{\n\t\t\tname:  \"array_nesting\",\n\t\t\tinput: `[[{\"a\": \"1234\"}], [{\"b\": \"1234\"}], [[[{\"a\":\"1234\"}, {\"a\": \"1234\"}]]]`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{{Kind: ObjectField, FieldName: `\"a\"`, CaseSensitive: true}},\n\t\t\t},\n\t\t\twantOutput: `[[{\"a\": \"[sensitive]\"}], [{\"b\": \"1234\"}], [[[{\"a\":\"[sensitive]\"}, {\"a\": \"[sensitive]\"}]]]`,\n\t\t},\n\t\t{\n\t\t\tname:  \"case_sensitivity\",\n\t\t\tinput: `[[{\"a\": \"1234\"}], [{\"A\": \"1234\"}], [[[{\"aa\":\"1234\"}, [\"aA\": \"1234\"]]]]`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{{Kind: ObjectField, FieldName: `\"a\"`, CaseSensitive: true}},\n\t\t\t},\n\t\t\twantOutput: `[[{\"a\": \"[sensitive]\"}], [{\"A\": \"1234\"}], [[[{\"aa\":\"1234\"}, [\"aA\": \"1234\"]]]]`,\n\t\t},\n\t\t{\n\t\t\tname:  \"object_nesting_1\",\n\t\t\tinput: `{\"a\": \"1234\", \"b\": {\"a\": \"1234\"}`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{{Kind: ObjectField, FieldName: `\"a\"`, CaseSensitive: true}},\n\t\t\t},\n\t\t\twantOutput: `{\"a\": \"[sensitive]\", \"b\": {\"a\": \"1234\"}`,\n\t\t},\n\t\t{\n\t\t\tname:  \"object_nesting_2\",\n\t\t\tinput: `{\"a\": \"1234\", \"b\": {\"a\": \"1234\"}`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"b\"`, CaseSensitive: true},\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"a\"`, CaseSensitive: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantOutput: `{\"a\": \"1234\", \"b\": {\"a\": \"[sensitive]\"}`,\n\t\t},\n\t\t{\n\t\t\tname:  \"missing_map_values\",\n\t\t\tinput: \"{0:\\n 1: 123}\",\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{\n\t\t\t\t\t{Kind: ObjectField, FieldName: \"1\", CaseSensitive: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantOutput: \"{0:\\n 1: \\\"[sensitive]\\\"}\",\n\t\t},\n\t\t{\n\t\t\tname:  \"map_values_multiline\",\n\t\t\tinput: \"{0:\\n 1}\",\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{\n\t\t\t\t\t{Kind: ObjectField, FieldName: \"0\", CaseSensitive: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantOutput: \"{0:\\n \\\"[sensitive]\\\"}\",\n\t\t},\n\t\t{\n\t\t\tname:  \"test\",\n\t\t\tinput: `{\"One\":[\"\\\"one\\\"\"]}}`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"x\"`, CaseSensitive: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantOutput: `{\"One\":[\"\\\"one\\\"\"]}}`,\n\t\t},\n\t\t{\n\t\t\tname:  \"nested_partial_scrub\",\n\t\t\tinput: `{\"paypay\": \"bar\", \"Nested\": {\"Foo\": \"baz\", \"Bar\": 1}, \"NS\": {\"Sensitive\": \"hi\", \"Bar\": 2}, \"NotSensitive\": false}`,\n\t\t\tpaths: []Path{\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"Header\"`, CaseSensitive: false}},\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"Payload\"`, CaseSensitive: false}},\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"Nested\"`, CaseSensitive: false}},\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"NS\"`, CaseSensitive: false}, {Kind: ObjectField, FieldName: `\"Sensitive\"`, CaseSensitive: false}},\n\t\t\t},\n\t\t\twantOutput: `{\"paypay\": \"bar\", \"Nested\": \"[sensitive]\", \"NS\": {\"Sensitive\": \"[sensitive]\", \"Bar\": 2}, \"NotSensitive\": false}`,\n\t\t},\n\t\t{\n\t\t\tname:  \"debug\",\n\t\t\tinput: \"{\\n  \\\"Header\\\": \\\"foo\\\",\\n  \\\"Payload\\\": \\\"bar\\\",\\n  \\\"Nested\\\": {\\n    \\\"Foo\\\": \\\"baz\\\",\\n    \\\"Bar\\\": 1\\n  },\\n  \\\"NS\\\": {\\n    \\\"Sensitive\\\": \\\"hi\\\",\\n    \\\"Bar\\\": 2\\n  },\\n  \\\"NotSensitive\\\": false\\n}\",\n\t\t\tpaths: []Path{\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"Header\"`, CaseSensitive: false}},\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"Payload\"`, CaseSensitive: false}},\n\t\t\t\t{{Kind: ObjectField, FieldName: `\"Nested\"`, CaseSensitive: false}},\n\t\t\t\t{\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"NS\"`, CaseSensitive: false},\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"Sensitive\"`, CaseSensitive: false},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantOutput: \"{\\n  \\\"Header\\\": \\\"[sensitive]\\\",\\n  \\\"Payload\\\": \\\"[sensitive]\\\",\\n  \\\"Nested\\\": \\\"[sensitive]\\\",\\n  \\\"NS\\\": {\\n    \\\"Sensitive\\\": \\\"[sensitive]\\\",\\n    \\\"Bar\\\": 2\\n  },\\n  \\\"NotSensitive\\\": false\\n}\",\n\t\t},\n\t\t{\n\t\t\tname:  \"multiple_array_nested_obj\",\n\t\t\tinput: `{\"one\": [{\"two\": {\"three\": \"ABC\"}}, {\"two\": {\"three\": \"DEF\"}}]}`,\n\t\t\tpaths: []Path{\n\t\t\t\t[]PathEntry{\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"one\"`, CaseSensitive: true},\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"two\"`, CaseSensitive: true},\n\t\t\t\t\t{Kind: ObjectField, FieldName: `\"three\"`, CaseSensitive: true},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantOutput: `{\"one\": [{\"two\": {\"three\": \"[sensitive]\"}}, {\"two\": {\"three\": \"[sensitive]\"}}]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tinput := []byte(tt.input)\n\t\t\tif out := JSON(input, tt.paths, []byte(`\"[sensitive]\"`)); !bytes.Equal(out, []byte(tt.wantOutput)) {\n\t\t\t\tt.Errorf(\"scrub() = %s, want %s\", out, tt.wantOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/scrub/token_string.go",
    "content": "// Code generated by \"stringer -type=token\"; DO NOT EDIT.\n\npackage scrub\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[unknown-0]\n\t_ = x[objectBegin-1]\n\t_ = x[objectEnd-2]\n\t_ = x[arrayBegin-3]\n\t_ = x[arrayEnd-4]\n\t_ = x[literal-5]\n}\n\nconst _token_name = \"unknownobjectBeginobjectEndarrayBeginarrayEndliteral\"\n\nvar _token_index = [...]uint8{0, 7, 18, 27, 37, 45, 52}\n\nfunc (i token) String() string {\n\tif i >= token(len(_token_index)-1) {\n\t\treturn \"token(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _token_name[_token_index[i]:_token_index[i+1]]\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/stack/stack.go",
    "content": "// Package stack collects stack traces.\npackage stack\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n)\n\ntype Stack struct {\n\tFrames []uintptr\n\tOff    uintptr\n}\n\nfunc Build(skip int) Stack {\n\treturn BuildWithFilters(skip+1, nil)\n}\n\nfunc BuildWithoutGoRuntime(skip int) Stack {\n\treturn BuildWithFilters(skip+1, func(pc uintptr) bool {\n\t\tfn := runtime.FuncForPC(pc)\n\t\tif fn == nil {\n\t\t\treturn true\n\t\t}\n\t\tname := fn.Name()\n\t\treturn !strings.HasPrefix(name, \"runtime.\") && !strings.HasPrefix(name, \"testing.\")\n\t})\n}\n\nfunc BuildWithFilters(skip int, filter func(pcs uintptr) bool) Stack {\n\tpcs := make([]uintptr, 101)\n\tidx, off := encoreCallers(skip+1, pcs)\n\tpcs = pcs[:idx]\n\tif idx == 0 {\n\t\treturn Stack{}\n\t}\n\n\t// Apply filters\n\tif filter != nil {\n\t\tnewPcs := make([]uintptr, 0, len(pcs))\n\t\tfor _, pc := range pcs {\n\t\t\tif filter(pc) {\n\t\t\t\tnewPcs = append(newPcs, pc)\n\t\t\t}\n\t\t}\n\n\t\tpcs = newPcs\n\t}\n\n\t// Go through our PCs and see if we reached a stop PC.\n\tstopMu.RLock()\n\tfor i, pc := range pcs {\n\t\tif stopPCs[pc] {\n\t\t\tstopMu.RUnlock()\n\t\t\treturn Stack{Frames: pcs[:i], Off: off}\n\t\t}\n\t}\n\tstopMu.RUnlock()\n\n\tfor i, p := range pcs {\n\t\tfn := runtime.FuncForPC(p)\n\t\t// Is this a new stop point?\n\t\tif fn != nil && strings.Contains(fn.Name(), \"__encore_\") {\n\t\t\tstopMu.Lock()\n\t\t\tstopPCs[p] = true\n\t\t\tstopMu.Unlock()\n\t\t\treturn Stack{Frames: pcs[:i], Off: off}\n\t\t}\n\t}\n\treturn Stack{Frames: pcs, Off: off}\n}\n\nfunc Print(s Stack) {\n\tvar b bytes.Buffer\n\tcf := runtime.CallersFrames(s.Frames)\n\ti := 0\n\tfor {\n\t\tf, more := cf.Next()\n\t\tpc := s.Frames[i] - s.Off\n\t\tfmt.Fprintf(&b, \"%d: %s:%d: %s\\n\", pc, f.File, f.Line, f.Function)\n\t\tif !more {\n\t\t\tbreak\n\t\t}\n\t\ti++\n\t}\n\tif s := b.Bytes(); len(s) > 0 {\n\t\t_, _ = os.Stdout.Write(s)\n\t}\n}\n\ntype FormattedFrame struct {\n\tFile string\n\tLine int\n\tFunc string\n}\n\nfunc Format(s Stack) []FormattedFrame {\n\tif len(s.Frames) == 0 {\n\t\treturn nil\n\t}\n\n\tvar frames []FormattedFrame\n\tcf := runtime.CallersFrames(s.Frames)\n\ti := 0\n\tfor {\n\t\tf, more := cf.Next()\n\t\tframes = append(frames, FormattedFrame{\n\t\t\tFile: f.File,\n\t\t\tLine: f.Line,\n\t\t\tFunc: f.Function,\n\t\t})\n\t\tif !more {\n\t\t\tbreak\n\t\t}\n\t\ti++\n\t}\n\treturn frames\n}\n\nvar (\n\tstopMu  sync.RWMutex\n\tstopPCs = make(map[uintptr]bool)\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/stack/stack_app.go",
    "content": "//go:build encore_app\n\npackage stack\n\nimport _ \"unsafe\" // for go:linkname\n\n//go:linkname encoreCallers runtime.encoreCallers\nfunc encoreCallers(skip int, pc []uintptr) (n int, off uintptr)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/stack/stack_noapp.go",
    "content": "//go:build !encore_app\n\npackage stack\n\nimport \"runtime\"\n\nfunc encoreCallers(skip int, pc []uintptr) (n int, off uintptr) {\n\tn = runtime.Callers(skip, pc)\n\n\t// Outside of an Encore app we can't easily determine the offset.\n\t// This is only relevant to be able to construct ASLR-independent\n\t// program counters for doing PC lookups from other processes,\n\t// such as during trace rendering. This is not something we need\n\t// to worry about when not running inside an Encore app.\n\toff = 0\n\n\treturn\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/stack/stack_test.go",
    "content": "package stack\n\nimport (\n\t\"testing\"\n)\n\nfunc BenchmarkBuild(b *testing.B) {\n\tsum := 0\n\tfor i := 0; i < b.N; i++ {\n\t\tn := __encore_foo()\n\t\tsum += n\n\t}\n\tb.Log(sum)\n}\n\nfunc __encore_foo() int {\n\treturn userCode()\n}\n\nfunc userCode() int {\n\ts := Build(2)\n\treturn len(s.Frames)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/events.go",
    "content": "package trace\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/beta/errs\"\n)\n\ntype EventType byte\n\nconst (\n\tRequestStart       EventType = 0x01\n\tRequestEnd         EventType = 0x02\n\tGoStart            EventType = 0x03\n\tGoEnd              EventType = 0x04\n\tGoClear            EventType = 0x05\n\tTxStart            EventType = 0x06\n\tTxEnd              EventType = 0x07\n\tQueryStart         EventType = 0x08\n\tQueryEnd           EventType = 0x09\n\tCallStart          EventType = 0x0A\n\tCallEnd            EventType = 0x0B\n\tAuthStart          EventType = 0x0C\n\tAuthEnd            EventType = 0x0D\n\tHTTPCallStart      EventType = 0x0E\n\tHTTPCallEnd        EventType = 0x0F\n\tHTTPCallBodyClosed EventType = 0x10\n\tLogMessage         EventType = 0x11\n\tPublishStart       EventType = 0x12\n\tPublishEnd         EventType = 0x13\n\tServiceInitStart   EventType = 0x14\n\tServiceInitEnd     EventType = 0x15\n\tCacheOpStart       EventType = 0x16\n\tCacheOpEnd         EventType = 0x17\n\tBodyStream         EventType = 0x18\n)\n\nfunc (te EventType) String() string {\n\tswitch te {\n\tcase RequestStart:\n\t\treturn \"RequestStart\"\n\tcase RequestEnd:\n\t\treturn \"RequestEnd\"\n\tcase GoStart:\n\t\treturn \"GoStart\"\n\tcase GoEnd:\n\t\treturn \"GoEnd\"\n\tcase GoClear:\n\t\treturn \"GoClear\"\n\tcase TxStart:\n\t\treturn \"TxStart\"\n\tcase TxEnd:\n\t\treturn \"TxEnd\"\n\tcase QueryStart:\n\t\treturn \"QueryStart\"\n\tcase QueryEnd:\n\t\treturn \"QueryEnd\"\n\tcase CallStart:\n\t\treturn \"CallStart\"\n\tcase CallEnd:\n\t\treturn \"CallEnd\"\n\tcase AuthStart:\n\t\treturn \"AuthStart\"\n\tcase AuthEnd:\n\t\treturn \"AuthEnd\"\n\tcase HTTPCallStart:\n\t\treturn \"HTTPCallStart\"\n\tcase HTTPCallEnd:\n\t\treturn \"HTTPCallEnd\"\n\tcase HTTPCallBodyClosed:\n\t\treturn \"HTTPCallBodyClosed\"\n\tcase LogMessage:\n\t\treturn \"LogMessage\"\n\tcase PublishStart:\n\t\treturn \"PublishStart\"\n\tcase PublishEnd:\n\t\treturn \"PublishEnd\"\n\tcase ServiceInitStart:\n\t\treturn \"ServiceInitStart\"\n\tcase ServiceInitEnd:\n\t\treturn \"ServiceInitEnd\"\n\tcase CacheOpStart:\n\t\treturn \"CacheOpStart\"\n\tcase CacheOpEnd:\n\t\treturn \"CacheOpEnd\"\n\tcase BodyStream:\n\t\treturn \"BodyStream\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"Unknown(%x)\", byte(te))\n\t}\n}\n\nfunc (l *Log) BeginRequest(req *model.Request, goid uint32) {\n\ttb := NewBuffer(1 + 8 + 8 + 8 + 8 + 8 + 8 + 64)\n\ttb.Byte(byte(req.Type))\n\ttb.Now()\n\ttb.Bytes(req.TraceID[:])\n\ttb.Bytes(req.ParentTraceID[:])\n\ttb.Bytes(req.SpanID[:])\n\ttb.Bytes(req.ParentSpanID[:])\n\ttb.UVarint(uint64(goid))\n\ttb.UVarint(uint64(req.DefLoc)) // endpoint expr idx\n\n\tswitch req.Type {\n\tcase model.RPCCall:\n\t\tdata := req.RPCData\n\t\tdesc := data.Desc\n\t\ttb.Bool(desc.Raw)\n\t\ttb.String(desc.Service)\n\t\ttb.String(desc.Endpoint)\n\t\ttb.String(data.HTTPMethod)\n\n\t\ttb.String(data.Path)\n\t\ttb.UVarint(uint64(len(data.PathParams)))\n\t\tfor _, pp := range data.PathParams {\n\t\t\ttb.String(pp.Value)\n\t\t}\n\t\ttb.String(string(data.UserID))\n\t\ttb.String(data.RequestHeaders.Get(\"X-Request-ID\"))\n\t\ttb.String(req.ExtCorrelationID)\n\n\t\tif desc.Raw {\n\t\t\tl.logHeaders(&tb, data.RequestHeaders, desc.ScrubRequestHeaders)\n\t\t} else {\n\t\t\t// Scrub the payload according to the RPC description.\n\t\t\tscrubbed := scrub.JSON(data.NonRawPayload, desc.ScrubRequestPaths, []byte(`\"[REDACTED]\"`))\n\t\t\ttb.ByteString(scrubbed)\n\t\t}\n\n\tcase model.AuthHandler:\n\t\tdata := req.RPCData\n\t\tdesc := data.Desc\n\t\ttb.String(desc.Service)\n\t\ttb.String(desc.Endpoint)\n\n\t\tscrubbed := scrub.JSON(data.NonRawPayload, desc.ScrubRequestPaths, []byte(`\"[REDACTED]\"`))\n\t\ttb.ByteString(scrubbed)\n\n\tcase model.PubSubMessage:\n\t\tdata := req.MsgData\n\t\tdesc := data.Desc\n\t\ttb.String(desc.Service)\n\t\ttb.String(desc.Topic)\n\t\ttb.String(desc.Subscription)\n\t\ttb.String(data.MessageID)\n\t\ttb.Uint32(uint32(data.Attempt))\n\t\ttb.Time(data.Published)\n\n\t\tscrubbed := scrub.JSON(data.Payload, desc.ScrubPaths, []byte(`\"[REDACTED]\"`))\n\t\ttb.ByteString(scrubbed)\n\t}\n\n\tl.Add(RequestStart, tb.Buf())\n}\n\nfunc (l *Log) FinishRequest(req *model.Request, resp *model.Response) {\n\ttb := NewBuffer(64)\n\ttb.Byte(byte(req.Type))\n\ttb.Bytes(req.SpanID[:])\n\n\ttb.Err(resp.Err)\n\tif resp.Err != nil {\n\t\ttb.Stack(errs.Stack(resp.Err))\n\n\t\tif panicStack, ok := errs.Meta(resp.Err)[\"panic_stack\"].(stack.Stack); ok {\n\t\t\ttb.FormattedStack(panicStack)\n\t\t} else {\n\t\t\ttb.FormattedStack(stack.Stack{})\n\t\t}\n\t}\n\n\tswitch req.Type {\n\tcase model.RPCCall:\n\t\tisRaw := req.RPCData.Desc.Raw\n\t\ttb.Bool(isRaw)\n\t\tif isRaw {\n\t\t\tl.logHeaders(&tb, resp.RawResponseHeaders, req.RPCData.Desc.ScrubResponseHeaders)\n\t\t} else {\n\t\t\tscrubbed := scrub.JSON(resp.Payload, req.RPCData.Desc.ScrubResponsePaths, []byte(`\"[REDACTED]\"`))\n\t\t\ttb.ByteString(scrubbed)\n\t\t}\n\tcase model.AuthHandler:\n\t\ttb.String(string(resp.AuthUID))\n\n\t\tscrubbed := scrub.JSON(resp.Payload, req.RPCData.Desc.ScrubResponsePaths, []byte(`\"[REDACTED]\"`))\n\t\ttb.ByteString(scrubbed)\n\tcase model.PubSubMessage:\n\t\tscrubbed := scrub.JSON(resp.Payload, req.MsgData.Desc.ScrubPaths, []byte(`\"[REDACTED]\"`))\n\t\ttb.ByteString(scrubbed)\n\t}\n\n\tl.Add(RequestEnd, tb.Buf())\n}\n\nfunc (l *Log) BeginCall(call *model.APICall, goid uint32) {\n\ttb := NewBuffer(8 + 4 + 4 + 4)\n\ttb.UVarint(call.ID)\n\ttb.Bytes(call.Source.SpanID[:])\n\ttb.Bytes(call.SpanID[:])\n\ttb.UVarint(uint64(goid))\n\ttb.UVarint(0)                   // call expr idx; no longer used\n\ttb.UVarint(uint64(call.DefLoc)) // endpoint expr idx\n\ttb.Stack(stack.Build(3))\n\tl.Add(CallStart, tb.Buf())\n}\n\nfunc (l *Log) FinishCall(call *model.APICall, err error) {\n\ttb := NewBuffer(8 + 4 + 4 + 4)\n\ttb.UVarint(call.ID)\n\tif err != nil {\n\t\tmsg := err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t\ttb.String(msg)\n\t} else {\n\t\ttb.String(\"\")\n\t}\n\tl.Add(CallEnd, tb.Buf())\n}\n\nfunc (l *Log) BeginAuth(call *model.AuthCall, goid uint32) {\n\ttb := NewBuffer(8 + 4 + 4 + 4)\n\ttb.UVarint(call.ID)\n\ttb.Bytes(call.SpanID[:])\n\ttb.UVarint(uint64(goid))\n\ttb.UVarint(uint64(call.DefLoc)) // auth handler expr idx\n\tl.Add(AuthStart, tb.Buf())\n}\n\nfunc (l *Log) FinishAuth(call *model.AuthCall, uid model.UID, err error) {\n\ttb := NewBuffer(64)\n\ttb.UVarint(call.ID)\n\ttb.String(string(uid))\n\tif err != nil {\n\t\tmsg := err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t\ttb.String(msg)\n\t\ttb.Stack(errs.Stack(err))\n\t} else {\n\t\ttb.String(\"\")\n\t\ttb.Stack(stack.Stack{}) // no stack\n\t}\n\tl.Add(AuthEnd, tb.Buf())\n}\n\ntype DBQueryStartParams struct {\n\tQuery   string\n\tSpanID  model.SpanID\n\tGoid    uint32\n\tQueryID uint64\n\tTxID    uint64\n\tStack   stack.Stack\n}\n\nfunc (l *Log) DBQueryStart(p DBQueryStartParams) {\n\tvar tb Buffer\n\ttb.UVarint(p.QueryID)\n\ttb.Bytes(p.SpanID[:])\n\ttb.UVarint(p.TxID)\n\ttb.UVarint(uint64(p.Goid))\n\ttb.String(p.Query)\n\ttb.Stack(p.Stack)\n\tl.Add(QueryStart, tb.Buf())\n}\n\nfunc (l *Log) DBQueryEnd(queryID uint64, err error) {\n\tvar tb Buffer\n\ttb.UVarint(queryID)\n\tif err != nil {\n\t\ttb.String(err.Error())\n\t} else {\n\t\ttb.String(\"\")\n\t}\n\tl.Add(QueryEnd, tb.Buf())\n}\n\ntype DBTxStartParams struct {\n\tSpanID model.SpanID\n\tGoid   uint32\n\tTxID   uint64\n\tStack  stack.Stack\n}\n\nfunc (l *Log) DBTxStart(p DBTxStartParams) {\n\tvar tb Buffer\n\ttb.UVarint(p.TxID)\n\ttb.Bytes(p.SpanID[:])\n\ttb.UVarint(uint64(p.Goid))\n\ttb.Stack(p.Stack)\n\tl.Add(TxStart, tb.Buf())\n}\n\ntype DBTxEndParams struct {\n\tSpanID model.SpanID\n\tGoid   uint32\n\tTxID   uint64\n\tCommit bool\n\tErr    error\n\tStack  stack.Stack\n}\n\nfunc (l *Log) DBTxEnd(p DBTxEndParams) {\n\tvar tb Buffer\n\ttb.UVarint(p.TxID)\n\ttb.Bytes(p.SpanID[:])\n\ttb.UVarint(uint64(p.Goid))\n\tif p.Commit {\n\t\ttb.Byte(1)\n\t} else {\n\t\ttb.Byte(0)\n\t}\n\tif p.Err != nil {\n\t\ttb.String(p.Err.Error())\n\t} else {\n\t\ttb.String(\"\")\n\t}\n\ttb.Stack(p.Stack)\n\tl.Add(TxEnd, tb.Buf())\n}\n\nfunc (l *Log) PublishStart(topic string, msg []byte, spanID model.SpanID, goid uint32, publishID uint64, skipFrames int) {\n\tvar tb Buffer\n\ttb.UVarint(publishID)\n\ttb.Bytes(spanID[:])\n\ttb.UVarint(uint64(goid))\n\ttb.String(topic)\n\ttb.ByteString(msg)\n\ttb.Stack(stack.Build(skipFrames))\n\tl.Add(PublishStart, tb.Buf())\n}\n\nfunc (l *Log) PublishEnd(publishID uint64, messageID string, err error) {\n\tvar tb Buffer\n\ttb.UVarint(publishID)\n\ttb.String(messageID)\n\ttb.Err(err)\n\tl.Add(PublishEnd, tb.Buf())\n}\n\nfunc (l *Log) GoStart(spanID model.SpanID, goctr uint32) {\n\tl.Add(GoStart, []byte{\n\t\tspanID[0],\n\t\tspanID[1],\n\t\tspanID[2],\n\t\tspanID[3],\n\t\tspanID[4],\n\t\tspanID[5],\n\t\tspanID[6],\n\t\tspanID[7],\n\t\tbyte(goctr),\n\t\tbyte(goctr >> 8),\n\t\tbyte(goctr >> 16),\n\t\tbyte(goctr >> 24),\n\t})\n}\n\nfunc (l *Log) GoClear(spanID model.SpanID, goctr uint32) {\n\tl.Add(GoClear, []byte{\n\t\tspanID[0],\n\t\tspanID[1],\n\t\tspanID[2],\n\t\tspanID[3],\n\t\tspanID[4],\n\t\tspanID[5],\n\t\tspanID[6],\n\t\tspanID[7],\n\t\tbyte(goctr),\n\t\tbyte(goctr >> 8),\n\t\tbyte(goctr >> 16),\n\t\tbyte(goctr >> 24),\n\t})\n}\n\nfunc (l *Log) GoEnd(spanID model.SpanID, goctr uint32) {\n\tl.Add(GoEnd, []byte{\n\t\tspanID[0],\n\t\tspanID[1],\n\t\tspanID[2],\n\t\tspanID[3],\n\t\tspanID[4],\n\t\tspanID[5],\n\t\tspanID[6],\n\t\tspanID[7],\n\t\tbyte(goctr),\n\t\tbyte(goctr >> 8),\n\t\tbyte(goctr >> 16),\n\t\tbyte(goctr >> 24),\n\t})\n}\n\ntype ServiceInitStartParams struct {\n\tInitCtr uint64\n\tSpanID  model.SpanID\n\tGoctr   uint32\n\tDefLoc  int32\n\tService string\n}\n\nfunc (l *Log) ServiceInitStart(p ServiceInitStartParams) {\n\tvar tb Buffer\n\ttb.Bytes(p.SpanID[:])\n\ttb.UVarint(p.InitCtr)\n\ttb.UVarint(uint64(p.Goctr))\n\ttb.UVarint(uint64(p.DefLoc))\n\ttb.String(p.Service)\n\tl.Add(ServiceInitStart, tb.Buf())\n}\n\nfunc (l *Log) ServiceInitEnd(initCtr uint64, err error) {\n\tvar tb Buffer\n\ttb.UVarint(initCtr)\n\ttb.Err(err)\n\tif err != nil {\n\t\ttb.Stack(errs.Stack(err))\n\t}\n\tl.Add(ServiceInitEnd, tb.Buf())\n}\n\ntype CacheOpStartParams struct {\n\tDefLoc    int32\n\tOperation string\n\tIsWrite   bool\n\tKeys      []string\n\tInputs    [][]byte\n\tSpanID    model.SpanID\n\tGoid      uint32\n\tOpID      uint64\n\tStack     stack.Stack\n}\n\nfunc (l *Log) CacheOpStart(p CacheOpStartParams) {\n\tvar tb Buffer\n\ttb.UVarint(p.OpID)\n\ttb.Bytes(p.SpanID[:])\n\ttb.UVarint(uint64(p.Goid))\n\ttb.UVarint(uint64(p.DefLoc))\n\ttb.String(p.Operation)\n\ttb.Bool(p.IsWrite)\n\ttb.Stack(p.Stack)\n\n\ttb.UVarint(uint64(len(p.Keys)))\n\tfor _, k := range p.Keys {\n\t\ttb.String(k)\n\t}\n\n\ttb.UVarint(uint64(len(p.Inputs)))\n\tsuffix := []byte(\"...\")\n\tfor _, val := range p.Inputs {\n\t\tconst maxLen = 4 * 1024 // 4KiB\n\t\ttb.TruncatedByteString(val, maxLen, suffix)\n\t}\n\n\tl.Add(CacheOpStart, tb.Buf())\n}\n\ntype CacheOpEndParams struct {\n\tOpID    uint64\n\tRes     CacheOpResult\n\tErr     error // must be set iff Res == CacheErr\n\tOutputs [][]byte\n}\n\nfunc (l *Log) CacheOpEnd(p CacheOpEndParams) {\n\tvar tb Buffer\n\ttb.UVarint(p.OpID)\n\ttb.Byte(byte(p.Res))\n\tif p.Res == CacheErr {\n\t\terr := p.Err\n\t\tif err == nil {\n\t\t\terr = errors.New(\"unknown error\")\n\t\t}\n\t\ttb.Err(err)\n\t}\n\n\ttb.UVarint(uint64(len(p.Outputs)))\n\tsuffix := []byte(\"...\")\n\tfor _, val := range p.Outputs {\n\t\tconst maxLen = 4 * 1024 // 4KiB\n\t\ttb.TruncatedByteString(val, maxLen, suffix)\n\t}\n\tl.Add(CacheOpEnd, tb.Buf())\n}\n\ntype CacheOpResult uint8\n\nconst (\n\tCacheOK        CacheOpResult = 1\n\tCacheNoSuchKey CacheOpResult = 2\n\tCacheConflict  CacheOpResult = 3\n\tCacheErr       CacheOpResult = 4\n)\n\ntype BodyStreamParams struct {\n\tSpanID model.SpanID\n\n\t// IsResponse specifies whether the stream was a response body\n\t// or a request body.\n\tIsResponse bool\n\n\t// Overflowed specifies whether the capturing overflowed.\n\tOverflowed bool\n\n\t// Data is the data read.\n\tData []byte\n}\n\nfunc (l *Log) BodyStream(p BodyStreamParams) {\n\tvar tb Buffer\n\ttb.Bytes(p.SpanID[:])\n\n\tvar flags byte = 0\n\tif p.IsResponse {\n\t\tflags |= 1 << 0\n\t}\n\tif p.Overflowed {\n\t\tflags |= 1 << 1\n\t}\n\ttb.Byte(flags)\n\n\ttb.ByteString(p.Data)\n\tl.Add(BodyStream, tb.Buf())\n}\n\nfunc (l *Log) logHeaders(tb *Buffer, headers http.Header, scrubHeaders map[string]bool) {\n\ttb.UVarint(uint64(len(headers)))\n\tfor k, v := range headers {\n\t\ttb.String(k)\n\n\t\tfirstVal := \"\"\n\t\tif scrubHeaders[k] {\n\t\t\tfirstVal = \"[REDACTED]\"\n\t\t} else if len(v) > 0 {\n\t\t\tfirstVal = v[0]\n\t\t}\n\n\t\ttb.String(firstVal)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/http.go",
    "content": "package trace\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"net/textproto\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t_ \"unsafe\" // for go:linkname\n\n\tmodel2 \"encore.dev/appruntime/exported/model\"\n)\n\nfunc (l *Log) HTTPBeginRoundTrip(httpReq *http.Request, req *model2.Request, goid uint32) (context.Context, error) {\n\tif l == nil {\n\t\treturn httpReq.Context(), nil\n\t}\n\n\tspanID, err := model2.GenSpanID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treqID := atomic.AddUint64(&httpReqIDCtr, 1)\n\n\ttb := NewBuffer(8 + 4 + 4 + 4 + len(httpReq.Method) + 128)\n\ttb.UVarint(reqID)\n\ttb.Bytes(req.SpanID[:])\n\ttb.Bytes(spanID[:])\n\ttb.UVarint(uint64(goid))\n\ttb.String(httpReq.Method)\n\ttb.String(httpReq.URL.String())\n\n\tl.Add(HTTPCallStart, tb.Buf())\n\n\trt := &httpRoundTrip{\n\t\tReqID:  reqID,\n\t\tSpanID: spanID,\n\t\tlog:    l,\n\t}\n\tctx := context.WithValue(httpReq.Context(), rtKey, rt)\n\ttr := &httptrace.ClientTrace{\n\t\tGetConn:              rt.getConn,\n\t\tGotConn:              rt.gotConn,\n\t\tGotFirstResponseByte: rt.gotFirstResponseByte,\n\t\tGot1xxResponse:       rt.got1xxResponse,\n\t\tDNSStart:             rt.dnsStart,\n\t\tDNSDone:              rt.dnsDone,\n\t\tConnectStart:         rt.connectStart,\n\t\tConnectDone:          rt.connectDone,\n\t\tTLSHandshakeStart:    rt.tlsHandshakeStart,\n\t\tTLSHandshakeDone:     rt.tlsHandshakeDone,\n\t\tWroteHeaders:         rt.wroteHeaders,\n\t\tWait100Continue:      rt.wait100Continue,\n\t\tWroteRequest:         rt.wroteRequest,\n\t}\n\treturn httptrace.WithClientTrace(ctx, tr), nil\n}\n\nfunc (l *Log) HTTPCompleteRoundTrip(req *http.Request, resp *http.Response, err error) {\n\trt, ok := req.Context().Value(rtKey).(*httpRoundTrip)\n\tif !ok {\n\t\treturn\n\t}\n\n\ttb := NewBuffer(8 + 4 + 4 + 4)\n\ttb.UVarint(rt.ReqID)\n\tif err != nil {\n\t\tmsg := err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t\ttb.String(msg)\n\t\ttb.UVarint(0)\n\t} else {\n\t\ttb.String(\"\")\n\t\ttb.UVarint(uint64(resp.StatusCode))\n\t}\n\trt.encodeEvents(&tb)\n\trt.log.Add(HTTPCallEnd, tb.Buf())\n\n\tif req.Method != \"HEAD\" && resp != nil {\n\t\tresp.Body = wrapRespBody(resp.Body, rt)\n\t}\n}\n\nvar httpReqIDCtr uint64\n\ntype httpRoundTrip struct {\n\tReqID  uint64\n\tSpanID model2.SpanID\n\n\tlog Logger\n\n\tmu     sync.Mutex\n\tevents []httpEvent\n}\n\nfunc (rt *httpRoundTrip) getConn(hostPort string) {\n\trt.addEvent(getConn, &getConnEvent{hostPort: hostPort})\n}\n\nfunc (rt *httpRoundTrip) gotConn(info httptrace.GotConnInfo) {\n\trt.addEvent(gotConn, &gotConnEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) gotFirstResponseByte() {\n\trt.addEvent(gotFirstResponseByte, nil)\n}\n\nfunc (rt *httpRoundTrip) got1xxResponse(code int, header textproto.MIMEHeader) error {\n\trt.addEvent(got1xxResponse, &got1xxResponseEvent{code: code, header: header})\n\treturn nil\n}\n\nfunc (rt *httpRoundTrip) dnsStart(info httptrace.DNSStartInfo) {\n\trt.addEvent(dnsStart, &dnsStartEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) dnsDone(info httptrace.DNSDoneInfo) {\n\trt.addEvent(dnsDone, &dnsDoneEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) connectStart(network, addr string) {\n\trt.addEvent(connectStart, &connectStartEvent{network: network, addr: addr})\n}\n\nfunc (rt *httpRoundTrip) connectDone(network, addr string, err error) {\n\trt.addEvent(connectDone, &connectDoneEvent{network: network, addr: addr, err: err})\n}\n\nfunc (rt *httpRoundTrip) tlsHandshakeStart() {\n\trt.addEvent(tlsHandshakeStart, nil)\n}\n\nfunc (rt *httpRoundTrip) tlsHandshakeDone(state tls.ConnectionState, err error) {\n\trt.addEvent(tlsHandshakeDone, &tlsHandshakeDoneEvent{info: state, err: err})\n}\n\nfunc (rt *httpRoundTrip) wroteHeaders() {\n\trt.addEvent(wroteHeaders, nil)\n}\n\nfunc (rt *httpRoundTrip) wroteRequest(info httptrace.WroteRequestInfo) {\n\trt.addEvent(wroteRequest, &wroteRequestEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) wait100Continue() {\n\trt.addEvent(wait100Continue, nil)\n}\n\nfunc (rt *httpRoundTrip) addEvent(code httpEventCode, data httpEventData) {\n\tts := nanotime()\n\trt.mu.Lock()\n\tdefer rt.mu.Unlock()\n\n\trt.events = append(rt.events, httpEvent{\n\t\tcode: code,\n\t\tts:   ts,\n\t\tdata: data,\n\t})\n}\n\nfunc (rt *httpRoundTrip) encodeEvents(tb *Buffer) {\n\trt.mu.Lock()\n\tn := len(rt.events)\n\tevs := rt.events[:]\n\trt.mu.Unlock()\n\n\ttb.UVarint(uint64(n))\n\tfor _, e := range evs {\n\t\ttb.Bytes([]byte{byte(e.code)})\n\t\ttb.Int64(e.ts)\n\t\tif e.data != nil {\n\t\t\te.data.Encode(tb)\n\t\t}\n\t}\n}\n\nfunc (rt *httpRoundTrip) ClosedBody(err error) {\n\ttb := NewBuffer(8 + 4)\n\ttb.UVarint(rt.ReqID)\n\tif err != nil {\n\t\tmsg := err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t\ttb.String(msg)\n\t} else {\n\t\ttb.String(\"\")\n\t}\n\n\trt.log.Add(HTTPCallBodyClosed, tb.Buf())\n}\n\nfunc wrapRespBody(body io.ReadCloser, rt *httpRoundTrip) io.ReadCloser {\n\tif readWriteCloser, ok := body.(io.ReadWriteCloser); ok {\n\t\treturn writerCloseTracker{readWriteCloser, rt}\n\t}\n\treturn closeTracker{body, rt}\n}\n\ntype closeTracker struct {\n\tio.ReadCloser\n\trt *httpRoundTrip\n}\n\nfunc (c closeTracker) Close() error {\n\terr := c.ReadCloser.Close()\n\tc.rt.ClosedBody(err)\n\treturn err\n}\n\ntype writerCloseTracker struct {\n\tio.ReadWriteCloser\n\trt *httpRoundTrip\n}\n\nfunc (c writerCloseTracker) Close() error {\n\terr := c.ReadWriteCloser.Close()\n\tc.rt.ClosedBody(err)\n\treturn err\n}\n\ntype httpEvent struct {\n\tcode httpEventCode\n\tts   int64\n\tdata httpEventData // or nil\n}\n\ntype httpEventData interface {\n\tEncode(tb *Buffer)\n}\n\ntype httpEventCode byte\n\nconst (\n\tgetConn              = 0x01\n\tgotConn              = 0x02\n\tgotFirstResponseByte = 0x03\n\tgot1xxResponse       = 0x04\n\tdnsStart             = 0x05\n\tdnsDone              = 0x06\n\tconnectStart         = 0x07\n\tconnectDone          = 0x08\n\ttlsHandshakeStart    = 0x09\n\ttlsHandshakeDone     = 0x0A\n\twroteHeaders         = 0x0B\n\twroteRequest         = 0x0C\n\twait100Continue      = 0x0D\n)\n\ntype getConnEvent struct {\n\thostPort string\n}\n\nfunc (e *getConnEvent) Encode(tb *Buffer) {\n\ttb.String(e.hostPort)\n}\n\ntype gotConnEvent struct {\n\tinfo httptrace.GotConnInfo\n}\n\nfunc (e *gotConnEvent) Encode(tb *Buffer) {\n\ttb.Bool(e.info.Reused)\n\ttb.Bool(e.info.WasIdle)\n\ttb.Int64(int64(e.info.IdleTime))\n}\n\ntype got1xxResponseEvent struct {\n\tcode   int\n\theader textproto.MIMEHeader\n}\n\nfunc (e *got1xxResponseEvent) Encode(tb *Buffer) {\n\ttb.Varint(int64(e.code))\n\t// TODO: write header as well?\n}\n\ntype dnsStartEvent struct {\n\tinfo httptrace.DNSStartInfo\n}\n\nfunc (e *dnsStartEvent) Encode(tb *Buffer) {\n\ttb.String(e.info.Host)\n}\n\ntype dnsDoneEvent struct {\n\tinfo httptrace.DNSDoneInfo\n}\n\nfunc (e *dnsDoneEvent) Encode(tb *Buffer) {\n\tif err := e.info.Err; err != nil {\n\t\tmsg := err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t\ttb.String(msg)\n\t} else {\n\t\ttb.String(\"\")\n\t}\n\ttb.UVarint(uint64(len(e.info.Addrs)))\n\tfor _, a := range e.info.Addrs {\n\t\ttb.ByteString(a.IP)\n\t}\n}\n\ntype connectStartEvent struct {\n\tnetwork string\n\taddr    string\n}\n\nfunc (e *connectStartEvent) Encode(tb *Buffer) {\n\ttb.String(e.network)\n\ttb.String(e.addr)\n}\n\ntype connectDoneEvent struct {\n\tnetwork string\n\taddr    string\n\terr     error\n}\n\nfunc (e *connectDoneEvent) Encode(tb *Buffer) {\n\ttb.String(e.network)\n\ttb.String(e.addr)\n\ttb.Err(e.err)\n}\n\ntype tlsHandshakeDoneEvent struct {\n\tinfo tls.ConnectionState\n\terr  error\n}\n\nfunc (e *tlsHandshakeDoneEvent) Encode(tb *Buffer) {\n\ttb.Err(e.err)\n\ttb.Uint32(uint32(e.info.Version))\n\ttb.Uint32(uint32(e.info.CipherSuite))\n\ttb.String(e.info.ServerName)\n\ttb.String(e.info.NegotiatedProtocol)\n}\n\ntype wroteRequestEvent struct {\n\tinfo httptrace.WroteRequestInfo\n}\n\nfunc (e *wroteRequestEvent) Encode(tb *Buffer) {\n\ttb.Err(e.info.Err)\n}\n\ntype contextKey int\n\nconst (\n\trtKey contextKey = iota\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/log.go",
    "content": "package trace\n\nimport (\n\t\"math\"\n\t\"time\"\n\t_ \"unsafe\" // for go:linkname\n\n\t\"encore.dev/appruntime/exported/stack\"\n)\n\ntype Log struct {\n\t// mu must be the runtime mutex and not a regular sync.Mutex,\n\t// as certain events (Go{Start,Clear,End}) are sometimes executed by system goroutines,\n\t// which do not support sync.Mutex.\n\tmu mutex\n\n\tdata []byte\n}\n\n// Ensure Log implements Logger.\nvar _ Logger = (*Log)(nil)\n\n// Add adds a new event in the trace log.\n// If l is nil, it does nothing.\nfunc (l *Log) Add(event EventType, data []byte) {\n\tif l == nil {\n\t\treturn\n\t}\n\tln := len(data)\n\tif ln > (1<<32 - 1) {\n\t\tprintln(\"encore.traceEvent: event too large, dropping\")\n\t\treturn\n\t}\n\n\tmutexLock(&l.mu)\n\tdefer mutexUnlock(&l.mu)\n\n\t// Do this in the critical section to ensure we don't get\n\t// out-of-order timestamps.\n\tt := nanotime()\n\tvar b [13]byte\n\tb[0] = byte(event)\n\tb[1] = byte(t)\n\tb[2] = byte(t >> 8)\n\tb[3] = byte(t >> 16)\n\tb[4] = byte(t >> 24)\n\tb[5] = byte(t >> 32)\n\tb[6] = byte(t >> 40)\n\tb[7] = byte(t >> 48)\n\tb[8] = byte(t >> 56)\n\tb[9] = byte(ln)\n\tb[10] = byte(ln >> 8)\n\tb[11] = byte(ln >> 16)\n\tb[12] = byte(ln >> 24)\n\tl.data = append(l.data, append(b[:], data...)...)\n}\n\n// GetAndClear gets the data and clears the buffer.\nfunc (l *Log) GetAndClear() []byte {\n\tmutexLock(&l.mu)\n\tdata := l.data\n\tl.data = l.data[len(l.data):]\n\tmutexUnlock(&l.mu)\n\treturn data\n}\n\n// Buffer is a performant, low-overhead, growable buffer\n// for buffering trace data in a compact way.\n//\n// The zero value is ready to be used, but NewBuffer\n// can be used to provide an initial size hint.\ntype Buffer struct {\n\tscratch [10]byte\n\tbuf     []byte\n}\n\nfunc NewBuffer(size int) Buffer {\n\treturn Buffer{buf: make([]byte, 0, size)}\n}\n\nfunc (tb *Buffer) Buf() []byte {\n\treturn tb.buf\n}\n\nfunc (tb *Buffer) Byte(b byte) {\n\ttb.buf = append(tb.buf, b)\n}\n\nfunc (tb *Buffer) Bytes(b []byte) {\n\ttb.buf = append(tb.buf, b...)\n}\n\nfunc (tb *Buffer) String(s string) {\n\ttb.UVarint(uint64(len(s)))\n\ttb.Bytes([]byte(s))\n}\n\nfunc (tb *Buffer) ByteString(b []byte) {\n\ttb.UVarint(uint64(len(b)))\n\ttb.Bytes(b)\n}\n\n// TruncatedByteString is like ByteString except it truncates b to maximum of maxLen.\n// If truncationSuffix is provided, it is appended after truncating, leading to\n// the final length being maxLen+len(truncationSuffix).\nfunc (tb *Buffer) TruncatedByteString(b []byte, maxLen int, truncationSuffix []byte) {\n\tif size := len(b); size > maxLen {\n\t\ttb.UVarint(uint64(maxLen + len(truncationSuffix)))\n\t\ttb.Bytes(b[:maxLen])\n\t\ttb.Bytes(truncationSuffix)\n\t} else {\n\t\ttb.ByteString(b)\n\t}\n}\n\nfunc (tb *Buffer) Now() {\n\tnow := time.Now()\n\ttb.Time(now)\n}\n\nfunc (tb *Buffer) Bool(b bool) {\n\tif b {\n\t\ttb.Bytes([]byte{1})\n\t} else {\n\t\ttb.Bytes([]byte{0})\n\t}\n}\n\nfunc (tb *Buffer) Err(err error) {\n\tmsg := \"\"\n\tif err != nil {\n\t\tmsg = err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t}\n\ttb.String(msg)\n}\n\nfunc (tb *Buffer) Time(t time.Time) {\n\ttb.Int64(t.Unix())\n\ttb.Int32(int32(t.Nanosecond()))\n}\n\nfunc (tb *Buffer) Int32(x int32) {\n\tvar u uint32\n\tif x < 0 {\n\t\tu = (^uint32(x) << 1) | 1 // complement i, bit 0 is 1\n\t} else {\n\t\tu = (uint32(x) << 1) // do not complement i, bit 0 is 0\n\t}\n\ttb.Uint32(u)\n}\n\nfunc (tb *Buffer) Uint32(x uint32) {\n\ttb.buf = append(tb.buf,\n\t\tbyte(x),\n\t\tbyte(x>>8),\n\t\tbyte(x>>16),\n\t\tbyte(x>>24),\n\t)\n}\n\nfunc (tb *Buffer) Int64(x int64) {\n\tvar u uint64\n\tif x < 0 {\n\t\tu = (^uint64(x) << 1) | 1 // complement i, bit 0 is 1\n\t} else {\n\t\tu = (uint64(x) << 1) // do not complement i, bit 0 is 0\n\t}\n\ttb.Uint64(u)\n}\n\nfunc (tb *Buffer) Uint64(x uint64) {\n\ttb.buf = append(tb.buf,\n\t\tbyte(x),\n\t\tbyte(x>>8),\n\t\tbyte(x>>16),\n\t\tbyte(x>>24),\n\t\tbyte(x>>32),\n\t\tbyte(x>>40),\n\t\tbyte(x>>48),\n\t\tbyte(x>>56),\n\t)\n}\n\nfunc (tb *Buffer) Varint(x int64) {\n\tvar u uint64\n\tif x < 0 {\n\t\tu = (^uint64(x) << 1) | 1 // complement i, bit 0 is 1\n\t} else {\n\t\tu = (uint64(x) << 1) // do not complement i, bit 0 is 0\n\t}\n\ttb.UVarint(u)\n}\n\nfunc (tb *Buffer) UVarint(u uint64) {\n\ti := 0\n\tfor u >= 0x80 {\n\t\ttb.scratch[i] = byte(u) | 0x80\n\t\tu >>= 7\n\t\ti++\n\t}\n\ttb.scratch[i] = byte(u)\n\ti++\n\ttb.Bytes(tb.scratch[:i])\n}\n\nfunc (tb *Buffer) Float32(f float32) {\n\ttb.Uint32(math.Float32bits(f))\n}\n\nfunc (tb *Buffer) Float64(f float64) {\n\ttb.Uint64(math.Float64bits(f))\n}\n\nfunc (tb *Buffer) Stack(s stack.Stack) {\n\tn := len(s.Frames)\n\tif n > 0xFF {\n\t\t// Should never happen (the runtime caps it at 100),\n\t\t// but be defensive about it.\n\t\tn = 0xFF\n\t}\n\ttb.Byte(byte(n))\n\tif n == 0 {\n\t\treturn\n\t}\n\n\tvar prev int64 = 0\n\tfor _, pc := range s.Frames[:n] {\n\t\tp := int64(pc - s.Off)\n\t\tdiff := p - prev\n\t\ttb.Varint(diff)\n\t\tprev = p\n\t}\n}\n\nfunc (tb *Buffer) FormattedStack(s stack.Stack) {\n\tframes := stack.Format(s)\n\tn := len(frames)\n\tif n > 0xFF {\n\t\t// Should never happen (the runtime caps it at 100),\n\t\t// but be defensive about it.\n\t\tn = 0xFF\n\t}\n\n\ttb.Byte(byte(n))\n\tif n == 0 {\n\t\treturn\n\t}\n\n\tfor _, f := range frames[:n] {\n\t\ttb.String(f.File)\n\t\ttb.UVarint(uint64(f.Line))\n\t\ttb.String(f.Func)\n\t}\n}\n\n//go:linkname nanotime runtime.nanotime\nfunc nanotime() int64\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/logger.go",
    "content": "package trace\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tmodel2 \"encore.dev/appruntime/exported/model\"\n)\n\n//go:generate mockgen -source=./logger.go -destination ./mock_trace/mock_trace.go Logger\n\ntype Logger interface {\n\tAdd(event EventType, data []byte)\n\tGetAndClear() []byte\n\tBeginRequest(req *model2.Request, goid uint32)\n\tFinishRequest(req *model2.Request, resp *model2.Response)\n\tBeginCall(call *model2.APICall, goid uint32)\n\tFinishCall(call *model2.APICall, err error)\n\tBeginAuth(call *model2.AuthCall, goid uint32)\n\tFinishAuth(call *model2.AuthCall, uid model2.UID, err error)\n\tDBQueryStart(p DBQueryStartParams)\n\tDBQueryEnd(queryID uint64, err error)\n\tDBTxStart(p DBTxStartParams)\n\tDBTxEnd(p DBTxEndParams)\n\tPublishStart(topic string, msg []byte, spanID model2.SpanID, goid uint32, publishID uint64, skipFrames int)\n\tPublishEnd(publishID uint64, messageID string, err error)\n\tGoStart(spanID model2.SpanID, goctr uint32)\n\tGoClear(spanID model2.SpanID, goctr uint32)\n\tGoEnd(spanID model2.SpanID, goctr uint32)\n\tServiceInitStart(p ServiceInitStartParams)\n\tServiceInitEnd(initCtr uint64, err error)\n\tCacheOpStart(p CacheOpStartParams)\n\tCacheOpEnd(p CacheOpEndParams)\n\tBodyStream(p BodyStreamParams)\n\tHTTPBeginRoundTrip(httpReq *http.Request, req *model2.Request, goid uint32) (context.Context, error)\n\tHTTPCompleteRoundTrip(req *http.Request, resp *http.Response, err error)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/mock_trace/mock_trace.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: ./logger.go\n\n// Package mock_trace is a generated GoMock package.\npackage mock_trace\n\nimport (\n\tcontext \"context\"\n\thttp \"net/http\"\n\treflect \"reflect\"\n\n\tmodel \"encore.dev/appruntime/exported/model\"\n\ttrace \"encore.dev/appruntime/exported/trace\"\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockLogger is a mock of Logger interface.\ntype MockLogger struct {\n\tctrl     *gomock.Controller\n\trecorder *MockLoggerMockRecorder\n}\n\n// MockLoggerMockRecorder is the mock recorder for MockLogger.\ntype MockLoggerMockRecorder struct {\n\tmock *MockLogger\n}\n\n// NewMockLogger creates a new mock instance.\nfunc NewMockLogger(ctrl *gomock.Controller) *MockLogger {\n\tmock := &MockLogger{ctrl: ctrl}\n\tmock.recorder = &MockLoggerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockLogger) EXPECT() *MockLoggerMockRecorder {\n\treturn m.recorder\n}\n\n// Add mocks base method.\nfunc (m *MockLogger) Add(event trace.EventType, data []byte) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"Add\", event, data)\n}\n\n// Add indicates an expected call of Add.\nfunc (mr *MockLoggerMockRecorder) Add(event, data interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Add\", reflect.TypeOf((*MockLogger)(nil).Add), event, data)\n}\n\n// BeginAuth mocks base method.\nfunc (m *MockLogger) BeginAuth(call *model.AuthCall, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BeginAuth\", call, goid)\n}\n\n// BeginAuth indicates an expected call of BeginAuth.\nfunc (mr *MockLoggerMockRecorder) BeginAuth(call, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BeginAuth\", reflect.TypeOf((*MockLogger)(nil).BeginAuth), call, goid)\n}\n\n// BeginCall mocks base method.\nfunc (m *MockLogger) BeginCall(call *model.APICall, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BeginCall\", call, goid)\n}\n\n// BeginCall indicates an expected call of BeginCall.\nfunc (mr *MockLoggerMockRecorder) BeginCall(call, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BeginCall\", reflect.TypeOf((*MockLogger)(nil).BeginCall), call, goid)\n}\n\n// BeginRequest mocks base method.\nfunc (m *MockLogger) BeginRequest(req *model.Request, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BeginRequest\", req, goid)\n}\n\n// BeginRequest indicates an expected call of BeginRequest.\nfunc (mr *MockLoggerMockRecorder) BeginRequest(req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BeginRequest\", reflect.TypeOf((*MockLogger)(nil).BeginRequest), req, goid)\n}\n\n// BodyStream mocks base method.\nfunc (m *MockLogger) BodyStream(p trace.BodyStreamParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BodyStream\", p)\n}\n\n// BodyStream indicates an expected call of BodyStream.\nfunc (mr *MockLoggerMockRecorder) BodyStream(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BodyStream\", reflect.TypeOf((*MockLogger)(nil).BodyStream), p)\n}\n\n// CacheOpEnd mocks base method.\nfunc (m *MockLogger) CacheOpEnd(p trace.CacheOpEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"CacheOpEnd\", p)\n}\n\n// CacheOpEnd indicates an expected call of CacheOpEnd.\nfunc (mr *MockLoggerMockRecorder) CacheOpEnd(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CacheOpEnd\", reflect.TypeOf((*MockLogger)(nil).CacheOpEnd), p)\n}\n\n// CacheOpStart mocks base method.\nfunc (m *MockLogger) CacheOpStart(p trace.CacheOpStartParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"CacheOpStart\", p)\n}\n\n// CacheOpStart indicates an expected call of CacheOpStart.\nfunc (mr *MockLoggerMockRecorder) CacheOpStart(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CacheOpStart\", reflect.TypeOf((*MockLogger)(nil).CacheOpStart), p)\n}\n\n// DBQueryEnd mocks base method.\nfunc (m *MockLogger) DBQueryEnd(queryID uint64, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"DBQueryEnd\", queryID, err)\n}\n\n// DBQueryEnd indicates an expected call of DBQueryEnd.\nfunc (mr *MockLoggerMockRecorder) DBQueryEnd(queryID, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBQueryEnd\", reflect.TypeOf((*MockLogger)(nil).DBQueryEnd), queryID, err)\n}\n\n// DBQueryStart mocks base method.\nfunc (m *MockLogger) DBQueryStart(p trace.DBQueryStartParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"DBQueryStart\", p)\n}\n\n// DBQueryStart indicates an expected call of DBQueryStart.\nfunc (mr *MockLoggerMockRecorder) DBQueryStart(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBQueryStart\", reflect.TypeOf((*MockLogger)(nil).DBQueryStart), p)\n}\n\n// DBTxEnd mocks base method.\nfunc (m *MockLogger) DBTxEnd(p trace.DBTxEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"DBTxEnd\", p)\n}\n\n// DBTxEnd indicates an expected call of DBTxEnd.\nfunc (mr *MockLoggerMockRecorder) DBTxEnd(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBTxEnd\", reflect.TypeOf((*MockLogger)(nil).DBTxEnd), p)\n}\n\n// DBTxStart mocks base method.\nfunc (m *MockLogger) DBTxStart(p trace.DBTxStartParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"DBTxStart\", p)\n}\n\n// DBTxStart indicates an expected call of DBTxStart.\nfunc (mr *MockLoggerMockRecorder) DBTxStart(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBTxStart\", reflect.TypeOf((*MockLogger)(nil).DBTxStart), p)\n}\n\n// FinishAuth mocks base method.\nfunc (m *MockLogger) FinishAuth(call *model.AuthCall, uid model.UID, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"FinishAuth\", call, uid, err)\n}\n\n// FinishAuth indicates an expected call of FinishAuth.\nfunc (mr *MockLoggerMockRecorder) FinishAuth(call, uid, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"FinishAuth\", reflect.TypeOf((*MockLogger)(nil).FinishAuth), call, uid, err)\n}\n\n// FinishCall mocks base method.\nfunc (m *MockLogger) FinishCall(call *model.APICall, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"FinishCall\", call, err)\n}\n\n// FinishCall indicates an expected call of FinishCall.\nfunc (mr *MockLoggerMockRecorder) FinishCall(call, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"FinishCall\", reflect.TypeOf((*MockLogger)(nil).FinishCall), call, err)\n}\n\n// FinishRequest mocks base method.\nfunc (m *MockLogger) FinishRequest(req *model.Request, resp *model.Response) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"FinishRequest\", req, resp)\n}\n\n// FinishRequest indicates an expected call of FinishRequest.\nfunc (mr *MockLoggerMockRecorder) FinishRequest(req, resp interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"FinishRequest\", reflect.TypeOf((*MockLogger)(nil).FinishRequest), req, resp)\n}\n\n// GetAndClear mocks base method.\nfunc (m *MockLogger) GetAndClear() []byte {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetAndClear\")\n\tret0, _ := ret[0].([]byte)\n\treturn ret0\n}\n\n// GetAndClear indicates an expected call of GetAndClear.\nfunc (mr *MockLoggerMockRecorder) GetAndClear() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetAndClear\", reflect.TypeOf((*MockLogger)(nil).GetAndClear))\n}\n\n// GoClear mocks base method.\nfunc (m *MockLogger) GoClear(spanID model.SpanID, goctr uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"GoClear\", spanID, goctr)\n}\n\n// GoClear indicates an expected call of GoClear.\nfunc (mr *MockLoggerMockRecorder) GoClear(spanID, goctr interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GoClear\", reflect.TypeOf((*MockLogger)(nil).GoClear), spanID, goctr)\n}\n\n// GoEnd mocks base method.\nfunc (m *MockLogger) GoEnd(spanID model.SpanID, goctr uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"GoEnd\", spanID, goctr)\n}\n\n// GoEnd indicates an expected call of GoEnd.\nfunc (mr *MockLoggerMockRecorder) GoEnd(spanID, goctr interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GoEnd\", reflect.TypeOf((*MockLogger)(nil).GoEnd), spanID, goctr)\n}\n\n// GoStart mocks base method.\nfunc (m *MockLogger) GoStart(spanID model.SpanID, goctr uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"GoStart\", spanID, goctr)\n}\n\n// GoStart indicates an expected call of GoStart.\nfunc (mr *MockLoggerMockRecorder) GoStart(spanID, goctr interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GoStart\", reflect.TypeOf((*MockLogger)(nil).GoStart), spanID, goctr)\n}\n\n// HTTPBeginRoundTrip mocks base method.\nfunc (m *MockLogger) HTTPBeginRoundTrip(httpReq *http.Request, req *model.Request, goid uint32) (context.Context, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"HTTPBeginRoundTrip\", httpReq, req, goid)\n\tret0, _ := ret[0].(context.Context)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// HTTPBeginRoundTrip indicates an expected call of HTTPBeginRoundTrip.\nfunc (mr *MockLoggerMockRecorder) HTTPBeginRoundTrip(httpReq, req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"HTTPBeginRoundTrip\", reflect.TypeOf((*MockLogger)(nil).HTTPBeginRoundTrip), httpReq, req, goid)\n}\n\n// HTTPCompleteRoundTrip mocks base method.\nfunc (m *MockLogger) HTTPCompleteRoundTrip(req *http.Request, resp *http.Response, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"HTTPCompleteRoundTrip\", req, resp, err)\n}\n\n// HTTPCompleteRoundTrip indicates an expected call of HTTPCompleteRoundTrip.\nfunc (mr *MockLoggerMockRecorder) HTTPCompleteRoundTrip(req, resp, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"HTTPCompleteRoundTrip\", reflect.TypeOf((*MockLogger)(nil).HTTPCompleteRoundTrip), req, resp, err)\n}\n\n// PublishEnd mocks base method.\nfunc (m *MockLogger) PublishEnd(publishID uint64, messageID string, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"PublishEnd\", publishID, messageID, err)\n}\n\n// PublishEnd indicates an expected call of PublishEnd.\nfunc (mr *MockLoggerMockRecorder) PublishEnd(publishID, messageID, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PublishEnd\", reflect.TypeOf((*MockLogger)(nil).PublishEnd), publishID, messageID, err)\n}\n\n// PublishStart mocks base method.\nfunc (m *MockLogger) PublishStart(topic string, msg []byte, spanID model.SpanID, goid uint32, publishID uint64, skipFrames int) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"PublishStart\", topic, msg, spanID, goid, publishID, skipFrames)\n}\n\n// PublishStart indicates an expected call of PublishStart.\nfunc (mr *MockLoggerMockRecorder) PublishStart(topic, msg, spanID, goid, publishID, skipFrames interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PublishStart\", reflect.TypeOf((*MockLogger)(nil).PublishStart), topic, msg, spanID, goid, publishID, skipFrames)\n}\n\n// ServiceInitEnd mocks base method.\nfunc (m *MockLogger) ServiceInitEnd(initCtr uint64, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"ServiceInitEnd\", initCtr, err)\n}\n\n// ServiceInitEnd indicates an expected call of ServiceInitEnd.\nfunc (mr *MockLoggerMockRecorder) ServiceInitEnd(initCtr, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceInitEnd\", reflect.TypeOf((*MockLogger)(nil).ServiceInitEnd), initCtr, err)\n}\n\n// ServiceInitStart mocks base method.\nfunc (m *MockLogger) ServiceInitStart(p trace.ServiceInitStartParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"ServiceInitStart\", p)\n}\n\n// ServiceInitStart indicates an expected call of ServiceInitStart.\nfunc (mr *MockLoggerMockRecorder) ServiceInitStart(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceInitStart\", reflect.TypeOf((*MockLogger)(nil).ServiceInitStart), p)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/mutex_app.go",
    "content": "//go:build encore_app\n\npackage trace\n\nimport _ \"unsafe\" // for go:linkname\n\n// mutex must exactly match implementation in the runtime.\ntype mutex struct {\n\tkey uintptr\n}\n\n//go:linkname mutexLock runtime.lock\nfunc mutexLock(mut *mutex)\n\n//go:linkname mutexUnlock runtime.unlock\nfunc mutexUnlock(mut *mutex)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/mutex_noapp.go",
    "content": "//go:build !encore_app\n\npackage trace\n\nimport \"sync\"\n\ntype mutex struct {\n\tmut sync.Mutex\n}\n\nfunc mutexLock(m *mutex) { m.mut.Lock() }\n\nfunc mutexUnlock(m *mutex) { m.mut.Unlock() }\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace/version.go",
    "content": "package trace\n\ntype Version int\n\n// CurrentVersion is the trace protocol version this package produces traces in.\nconst CurrentVersion Version = 13\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/events.go",
    "content": "package trace2\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype EventType byte\n\nconst (\n\tRequestSpanStart          EventType = 0x01\n\tRequestSpanEnd            EventType = 0x02\n\tAuthSpanStart             EventType = 0x03\n\tAuthSpanEnd               EventType = 0x04\n\tPubsubMessageSpanStart    EventType = 0x05\n\tPubsubMessageSpanEnd      EventType = 0x06\n\tDBTransactionStart        EventType = 0x07\n\tDBTransactionEnd          EventType = 0x08\n\tDBQueryStart              EventType = 0x09\n\tDBQueryEnd                EventType = 0x0A\n\tRPCCallStart              EventType = 0x0B\n\tRPCCallEnd                EventType = 0x0C\n\tHTTPCallStart             EventType = 0x0D\n\tHTTPCallEnd               EventType = 0x0E\n\tLogMessage                EventType = 0x0F\n\tPubsubPublishStart        EventType = 0x10\n\tPubsubPublishEnd          EventType = 0x11\n\tServiceInitStart          EventType = 0x12\n\tServiceInitEnd            EventType = 0x13\n\tCacheCallStart            EventType = 0x14\n\tCacheCallEnd              EventType = 0x15\n\tBodyStream                EventType = 0x16\n\tTestStart                 EventType = 0x17\n\tTestEnd                   EventType = 0x18\n\tBucketObjectUploadStart   EventType = 0x19\n\tBucketObjectUploadEnd     EventType = 0x1A\n\tBucketObjectDownloadStart EventType = 0x1B\n\tBucketObjectDownloadEnd   EventType = 0x1C\n\tBucketObjectGetAttrsStart EventType = 0x1D\n\tBucketObjectGetAttrsEnd   EventType = 0x1E\n\tBucketListObjectsStart    EventType = 0x1F\n\tBucketListObjectsEnd      EventType = 0x20\n\tBucketDeleteObjectsStart  EventType = 0x21\n\tBucketDeleteObjectsEnd    EventType = 0x22\n)\n\nfunc (te EventType) String() string {\n\tswitch te {\n\tcase RequestSpanStart:\n\t\treturn \"RequestSpanStart\"\n\tcase RequestSpanEnd:\n\t\treturn \"RequestSpanEnd\"\n\tcase AuthSpanStart:\n\t\treturn \"AuthSpanStart\"\n\tcase AuthSpanEnd:\n\t\treturn \"AuthSpanEnd\"\n\tcase PubsubMessageSpanStart:\n\t\treturn \"PubsubMessageSpanStart\"\n\tcase PubsubMessageSpanEnd:\n\t\treturn \"PubsubMessageSpanEnd\"\n\tcase DBTransactionStart:\n\t\treturn \"DBTransactionStart\"\n\tcase DBTransactionEnd:\n\t\treturn \"DBTransactionEnd\"\n\tcase DBQueryStart:\n\t\treturn \"QueryStart\"\n\tcase DBQueryEnd:\n\t\treturn \"QueryEnd\"\n\tcase RPCCallStart:\n\t\treturn \"RPCCallStart\"\n\tcase RPCCallEnd:\n\t\treturn \"RPCCallEnd\"\n\tcase HTTPCallStart:\n\t\treturn \"HTTPCallStart\"\n\tcase HTTPCallEnd:\n\t\treturn \"HTTPCallEnd\"\n\tcase LogMessage:\n\t\treturn \"LogMessage\"\n\tcase PubsubPublishStart:\n\t\treturn \"PubsubPublishStart\"\n\tcase PubsubPublishEnd:\n\t\treturn \"PubsubPublishEnd\"\n\tcase ServiceInitStart:\n\t\treturn \"ServiceInitStart\"\n\tcase ServiceInitEnd:\n\t\treturn \"ServiceInitEnd\"\n\tcase CacheCallStart:\n\t\treturn \"CacheCallStart\"\n\tcase CacheCallEnd:\n\t\treturn \"CacheCallEnd\"\n\tcase BodyStream:\n\t\treturn \"BodyStream\"\n\tcase TestStart:\n\t\treturn \"TestStart\"\n\tcase TestEnd:\n\t\treturn \"TestEnd\"\n\tcase BucketObjectUploadStart:\n\t\treturn \"BucketObjectUploadStart\"\n\tcase BucketObjectUploadEnd:\n\t\treturn \"BucketObjectUploadEnd\"\n\tcase BucketObjectDownloadStart:\n\t\treturn \"BucketObjectDownloadStart\"\n\tcase BucketObjectDownloadEnd:\n\t\treturn \"BucketObjectDownloadEnd\"\n\tcase BucketObjectGetAttrsStart:\n\t\treturn \"BucketObjectGetAttrsStart\"\n\tcase BucketObjectGetAttrsEnd:\n\t\treturn \"BucketObjectGetAttrsEnd\"\n\tcase BucketListObjectsStart:\n\t\treturn \"BucketListObjectsStart\"\n\tcase BucketListObjectsEnd:\n\t\treturn \"BucketListObjectsEnd\"\n\tcase BucketDeleteObjectsStart:\n\t\treturn \"BucketDeleteObjectsStart\"\n\tcase BucketDeleteObjectsEnd:\n\t\treturn \"BucketDeleteObjectsEnd\"\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"Unknown(%x)\", byte(te))\n\t}\n}\n\ntype EventParams struct {\n\tTraceID model.TraceID\n\tSpanID  model.SpanID\n\tGoid    uint32\n\tDefLoc  uint32\n}\n\ntype spanStartEventData struct {\n\tGoid             uint32\n\tParentTraceID    model.TraceID\n\tParentSpanID     model.SpanID\n\tDefLoc           uint32\n\tCallerEventID    model.TraceEventID\n\tExtCorrelationID string\n\n\tExtraSpace int\n}\n\nfunc (l *Log) newSpanStartEvent(data spanStartEventData) EventBuffer {\n\ttb := NewEventBuffer(4 + 16 + 8 + 4 + len(data.ExtCorrelationID) + 2 + data.ExtraSpace)\n\ttb.UVarint(uint64(data.Goid))\n\ttb.Bytes(data.ParentTraceID[:])\n\ttb.Bytes(data.ParentSpanID[:])\n\ttb.UVarint(uint64(data.DefLoc))\n\ttb.UVarint(uint64(data.CallerEventID))\n\ttb.String(data.ExtCorrelationID)\n\treturn tb\n}\n\ntype spanEndEventData struct {\n\tDuration      time.Duration\n\tErr           error\n\tParentTraceID model.TraceID\n\tParentSpanID  model.SpanID\n\tExtraSpace    int\n}\n\nfunc (l *Log) newSpanEndEvent(data spanEndEventData) EventBuffer {\n\ttb := NewEventBuffer(8 + 1 + 12 + 8 + data.ExtraSpace)\n\ttb.Duration(data.Duration)\n\ttb.StatusCode(errs.Code(data.Err))\n\ttb.ErrWithStack(data.Err)\n\tif panicStack, ok := errs.Meta(data.Err)[\"panic_stack\"].(stack.Stack); ok {\n\t\ttb.FormattedStack(panicStack)\n\t} else {\n\t\ttb.FormattedStack(stack.Stack{})\n\t}\n\n\ttb.Bytes(data.ParentTraceID[:])\n\ttb.Bytes(data.ParentSpanID[:])\n\treturn tb\n}\n\ntype eventData struct {\n\tCommon             EventParams\n\tCorrelationEventID EventID\n\tExtraSpace         int\n}\n\nfunc (l *Log) newEvent(data eventData) EventBuffer {\n\ttb := NewEventBuffer(4 + 4 + data.ExtraSpace)\n\ttb.UVarint(uint64(data.Common.DefLoc))\n\ttb.UVarint(uint64(data.Common.Goid))\n\ttb.EventID(data.CorrelationEventID)\n\treturn tb\n}\n\nfunc (l *Log) RequestSpanStart(req *model.Request, goid uint32) {\n\tdata := req.RPCData\n\tdesc := data.Desc\n\ttb := l.newSpanStartEvent(spanStartEventData{\n\t\tParentTraceID:    req.ParentTraceID,\n\t\tParentSpanID:     req.ParentSpanID,\n\t\tDefLoc:           req.DefLoc,\n\t\tGoid:             goid,\n\t\tCallerEventID:    req.CallerEventID,\n\t\tExtCorrelationID: req.ExtCorrelationID,\n\t\tExtraSpace:       100,\n\t})\n\n\ttb.String(desc.Service)\n\ttb.String(desc.Endpoint)\n\ttb.String(data.HTTPMethod)\n\n\ttb.String(data.Path)\n\ttb.UVarint(uint64(len(data.PathParams)))\n\tfor _, pp := range data.PathParams {\n\t\ttb.String(pp.Value)\n\t}\n\n\tl.logHeaders(&tb, data.RequestHeaders, desc.ScrubRequestHeaders)\n\n\tscrubbedRequest := scrub.JSON(data.NonRawPayload, desc.ScrubRequestPaths, []byte(`\"[REDACTED]\"`))\n\ttb.ByteString(scrubbedRequest)\n\n\ttb.String(req.ExtCorrelationID)\n\ttb.String(string(data.UserID))\n\ttb.Bool(data.Mocked)\n\n\tl.Add(Event{\n\t\tType:    RequestSpanStart,\n\t\tTraceID: req.TraceID,\n\t\tSpanID:  req.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype RequestSpanEndParams struct {\n\tEventParams\n\tReq           *model.Request\n\tResp          *model.Response\n\tCallerEventID model.TraceEventID\n}\n\nfunc (l *Log) RequestSpanEnd(p RequestSpanEndParams) {\n\tdesc := p.Req.RPCData.Desc\n\tuid := string(p.Req.RPCData.UserID)\n\tscrubbedResponse := scrub.JSON(p.Resp.Payload, desc.ScrubResponsePaths, []byte(`\"[REDACTED]\"`))\n\n\ttb := l.newSpanEndEvent(spanEndEventData{\n\t\tDuration:      p.Resp.Duration,\n\t\tErr:           p.Resp.Err,\n\t\tParentTraceID: p.Req.ParentTraceID,\n\t\tParentSpanID:  p.Req.ParentSpanID,\n\t\tExtraSpace:    len(desc.Service) + len(desc.Endpoint) + 64 + len(scrubbedResponse) + len(uid) + 2,\n\t})\n\n\ttb.String(desc.Service)\n\ttb.String(desc.Endpoint)\n\n\ttb.UVarint(uint64(p.Resp.HTTPStatus))\n\tl.logHeaders(&tb, p.Resp.RawResponseHeaders, desc.ScrubResponseHeaders)\n\ttb.ByteString(scrubbedResponse)\n\ttb.UVarint(uint64(p.CallerEventID))\n\ttb.String(uid)\n\n\tl.Add(Event{\n\t\tType:    RequestSpanEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) AuthSpanStart(req *model.Request, goid uint32) {\n\tdata := req.RPCData\n\tdesc := data.Desc\n\ttb := l.newSpanStartEvent(spanStartEventData{\n\t\tParentTraceID:    req.ParentTraceID,\n\t\tParentSpanID:     req.ParentSpanID,\n\t\tDefLoc:           req.DefLoc,\n\t\tGoid:             goid,\n\t\tCallerEventID:    req.CallerEventID,\n\t\tExtCorrelationID: req.ExtCorrelationID,\n\t\tExtraSpace:       len(desc.Service) + len(desc.Endpoint) + len(data.NonRawPayload) + 5,\n\t})\n\n\ttb.String(desc.Service)\n\ttb.String(desc.Endpoint)\n\n\tscrubbed := scrub.JSON(data.NonRawPayload, desc.ScrubRequestPaths, []byte(`\"[REDACTED]\"`))\n\ttb.ByteString(scrubbed)\n\n\tl.Add(Event{\n\t\tType:    AuthSpanStart,\n\t\tTraceID: req.TraceID,\n\t\tSpanID:  req.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype AuthSpanEndParams struct {\n\tEventParams\n\tReq  *model.Request\n\tResp *model.Response\n}\n\nfunc (l *Log) AuthSpanEnd(p AuthSpanEndParams) {\n\tdesc := p.Req.RPCData.Desc\n\ttb := l.newSpanEndEvent(spanEndEventData{\n\t\tDuration:      p.Resp.Duration,\n\t\tErr:           p.Resp.Err,\n\t\tParentTraceID: p.Req.ParentTraceID,\n\t\tParentSpanID:  p.Req.ParentSpanID,\n\t\tExtraSpace:    len(desc.Service) + len(desc.Endpoint) + 64 + len(p.Resp.Payload),\n\t})\n\n\ttb.String(desc.Service)\n\ttb.String(desc.Endpoint)\n\ttb.String(string(p.Resp.AuthUID))\n\ttb.ByteString(p.Resp.Payload)\n\n\tl.Add(Event{\n\t\tType:    AuthSpanEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) PubsubMessageSpanStart(req *model.Request, goid uint32) {\n\tdata := req.MsgData\n\ttb := l.newSpanStartEvent(spanStartEventData{\n\t\tParentTraceID:    req.ParentTraceID,\n\t\tParentSpanID:     req.ParentSpanID,\n\t\tDefLoc:           req.DefLoc,\n\t\tGoid:             goid,\n\t\tCallerEventID:    req.CallerEventID,\n\t\tExtCorrelationID: req.ExtCorrelationID,\n\t\tExtraSpace:       len(data.Desc.Service) + len(data.Desc.Topic) + len(data.Desc.Subscription) + len(data.Payload) + 20,\n\t})\n\n\ttb.String(data.Desc.Service)\n\ttb.String(data.Desc.Topic)\n\ttb.String(data.Desc.Subscription)\n\ttb.String(data.MessageID)\n\ttb.UVarint(uint64(data.Attempt))\n\ttb.Time(data.Published)\n\ttb.ByteString(scrub.JSON(data.Payload, req.MsgData.Desc.ScrubPaths, []byte(`\"[REDACTED]\"`)))\n\n\tl.Add(Event{\n\t\tType:    PubsubMessageSpanStart,\n\t\tTraceID: req.TraceID,\n\t\tSpanID:  req.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype PubsubMessageSpanEndParams struct {\n\tEventParams\n\tReq  *model.Request\n\tResp *model.Response\n}\n\nfunc (l *Log) PubsubMessageSpanEnd(p PubsubMessageSpanEndParams) {\n\tmsg := p.Req.MsgData\n\ttb := l.newSpanEndEvent(spanEndEventData{\n\t\tDuration:      p.Resp.Duration,\n\t\tErr:           p.Resp.Err,\n\t\tParentTraceID: p.Req.ParentTraceID,\n\t\tParentSpanID:  p.Req.ParentSpanID,\n\t\tExtraSpace:    len(msg.Desc.Service) + len(msg.Desc.Topic) + len(msg.Desc.Subscription) + len(msg.MessageID) + 6,\n\t})\n\n\ttb.String(msg.Desc.Service)\n\ttb.String(msg.Desc.Topic)\n\ttb.String(msg.Desc.Subscription)\n\ttb.String(msg.MessageID)\n\n\tl.Add(Event{\n\t\tType:    PubsubMessageSpanEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) TestSpanStart(req *model.Request, goid uint32) {\n\tdata := req.Test\n\ttb := l.newSpanStartEvent(spanStartEventData{\n\t\tParentTraceID:    req.ParentTraceID,\n\t\tParentSpanID:     req.ParentSpanID,\n\t\tDefLoc:           req.DefLoc,\n\t\tGoid:             goid,\n\t\tCallerEventID:    req.CallerEventID,\n\t\tExtCorrelationID: req.ExtCorrelationID,\n\t\tExtraSpace:       len(data.Service) + len(data.Current.Name()) + len(data.UserID) + len(data.TestFile) + 30,\n\t})\n\n\ttb.String(data.Service)\n\ttb.String(data.Current.Name())\n\ttb.String(string(data.UserID))\n\ttb.String(data.TestFile)\n\ttb.Uint32(data.TestLine)\n\n\tl.Add(Event{\n\t\tType:    TestStart,\n\t\tTraceID: req.TraceID,\n\t\tSpanID:  req.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype TestSpanEndParams struct {\n\tEventParams\n\tReq     *model.Request\n\tFailed  bool\n\tSkipped bool\n}\n\nfunc (l *Log) TestSpanEnd(p TestSpanEndParams) {\n\tdesc := p.Req.Test\n\tvar err error\n\tif desc.Current.Failed() {\n\t\terr = errors.New(\"test failed\")\n\t}\n\tuid := string(desc.UserID)\n\ttb := l.newSpanEndEvent(spanEndEventData{\n\t\tDuration:      time.Since(p.Req.Start),\n\t\tErr:           err,\n\t\tParentTraceID: p.Req.ParentTraceID,\n\t\tParentSpanID:  p.Req.ParentSpanID,\n\t\tExtraSpace:    len(desc.Service) + len(desc.Current.Name()) + len(uid) + 20,\n\t})\n\n\ttb.String(desc.Service)\n\ttb.String(desc.Current.Name())\n\ttb.Bool(p.Failed)\n\ttb.Bool(p.Skipped)\n\ttb.String(uid)\n\n\tl.Add(Event{\n\t\tType:    TestEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) RPCCallStart(call *model.APICall, goid uint32) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon: EventParams{\n\t\t\tGoid:   goid,\n\t\t\tDefLoc: call.DefLoc,\n\t\t},\n\t\tExtraSpace: len(call.TargetServiceName) + len(call.TargetServiceName) + 4 + 64,\n\t})\n\ttb.String(call.TargetServiceName)\n\ttb.String(call.TargetEndpointName)\n\ttb.Stack(stack.Build(3))\n\treturn l.Add(Event{\n\t\tType:    RPCCallStart,\n\t\tTraceID: call.Source.TraceID,\n\t\tSpanID:  call.Source.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) RPCCallEnd(call *model.APICall, goid uint32, err error) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             EventParams{Goid: goid},\n\t\tExtraSpace:         64,\n\t\tCorrelationEventID: call.StartEventID,\n\t})\n\n\ttb.ErrWithStack(err)\n\n\tl.Add(Event{\n\t\tType:    RPCCallEnd,\n\t\tTraceID: call.Source.TraceID,\n\t\tSpanID:  call.Source.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype DBQueryStartParams struct {\n\tEventParams\n\tTxStartID EventID // zero if not in a transaction\n\tStack     stack.Stack\n\tQuery     string\n}\n\nfunc (l *Log) DBQueryStart(p DBQueryStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.TxStartID,\n\t\tExtraSpace:         64,\n\t})\n\n\ttb.String(p.Query)\n\ttb.Stack(p.Stack)\n\n\treturn l.Add(Event{\n\t\tType:    DBQueryStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) DBQueryEnd(p EventParams, startID EventID, err error) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p,\n\t\tExtraSpace:         64,\n\t\tCorrelationEventID: startID,\n\t})\n\ttb.ErrWithStack(err)\n\tl.Add(Event{\n\t\tType:    DBQueryEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) DBTransactionStart(p EventParams, stack stack.Stack) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.Stack(stack)\n\n\treturn l.Add(Event{\n\t\tType:    DBTransactionStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype DBTransactionEndParams struct {\n\tEventParams\n\tStartID EventID\n\tCommit  bool\n\tErr     error\n\tStack   stack.Stack\n}\n\nfunc (l *Log) DBTransactionEnd(p DBTransactionEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         64,\n\t})\n\n\ttb.Bool(p.Commit)\n\ttb.Stack(p.Stack)\n\ttb.ErrWithStack(p.Err)\n\n\tl.Add(Event{\n\t\tType:    DBTransactionEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype PubsubPublishStartParams struct {\n\tEventParams\n\tDesc    *model.PubSubTopicDesc\n\tMessage []byte\n\tStack   stack.Stack\n}\n\nfunc (l *Log) PubsubPublishStart(p PubsubPublishStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Desc.Topic)\n\ttb.ByteString(scrub.JSON(p.Message, p.Desc.ScrubPaths, []byte(`\"[REDACTED]\"`)))\n\ttb.Stack(p.Stack)\n\n\treturn l.Add(Event{\n\t\tType:    PubsubPublishStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype PubsubPublishEndParams struct {\n\tEventParams\n\tStartID   EventID\n\tMessageID string\n\tErr       error\n}\n\nfunc (l *Log) PubsubPublishEnd(p PubsubPublishEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         64,\n\t})\n\n\ttb.String(p.MessageID)\n\ttb.ErrWithStack(p.Err)\n\n\tl.Add(Event{\n\t\tType:    PubsubPublishEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype ServiceInitStartParams struct {\n\tEventParams\n\tService string\n}\n\nfunc (l *Log) ServiceInitStart(p ServiceInitStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\ttb.String(p.Service)\n\n\treturn l.Add(Event{\n\t\tType:    ServiceInitStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) ServiceInitEnd(p EventParams, start EventID, err error) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p,\n\t\tExtraSpace:         64,\n\t\tCorrelationEventID: start,\n\t})\n\n\ttb.ErrWithStack(err)\n\n\tl.Add(Event{\n\t\tType:    ServiceInitEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype CacheCallStartParams struct {\n\tEventParams\n\tOperation string\n\tIsWrite   bool\n\tKeys      []string\n\tStack     stack.Stack\n}\n\nfunc (l *Log) CacheCallStart(p CacheCallStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Operation)\n\ttb.Bool(p.IsWrite)\n\ttb.Stack(p.Stack)\n\n\ttb.UVarint(uint64(len(p.Keys)))\n\tfor _, k := range p.Keys {\n\t\ttb.String(k)\n\t}\n\n\treturn l.Add(Event{\n\t\tType:    CacheCallStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype CacheCallEndParams struct {\n\tEventParams\n\tStartID EventID\n\tRes     CacheCallResult\n\tErr     error\n}\n\nfunc (l *Log) CacheCallEnd(p CacheCallEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tExtraSpace:         64,\n\t\tCorrelationEventID: p.StartID,\n\t})\n\n\ttb.Byte(byte(p.Res))\n\ttb.ErrWithStack(p.Err)\n\n\tl.Add(Event{\n\t\tType:    CacheCallEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype CacheCallResult uint8\n\nconst (\n\tCacheOK        CacheCallResult = 1\n\tCacheNoSuchKey CacheCallResult = 2\n\tCacheConflict  CacheCallResult = 3\n\tCacheErr       CacheCallResult = 4\n)\n\ntype BodyStreamParams struct {\n\tEventParams\n\n\t// IsResponse specifies whether the stream was a response body\n\t// or a request body.\n\tIsResponse bool\n\n\t// Overflowed specifies whether the capturing overflowed.\n\tOverflowed bool\n\n\t// Data is the data read.\n\tData []byte\n}\n\nfunc (l *Log) BodyStream(p BodyStreamParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\tvar flags byte = 0\n\tif p.IsResponse {\n\t\tflags |= 1 << 0\n\t}\n\tif p.Overflowed {\n\t\tflags |= 1 << 1\n\t}\n\ttb.Byte(flags)\n\ttb.ByteString(p.Data)\n\n\tl.Add(Event{\n\t\tType:    BodyStream,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketObjectUploadStartParams struct {\n\tEventParams\n\tBucket string\n\tObject string\n\tAttrs  BucketObjectAttributes\n\tStack  stack.Stack\n}\n\ntype BucketObjectAttributes struct {\n\tSize        *uint64\n\tVersion     *string\n\tETag        *string\n\tContentType *string\n}\n\nfunc (l *Log) BucketObjectUploadStart(p BucketObjectUploadStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Bucket)\n\ttb.String(p.Object)\n\ttb.bucketObjectAttrs(&p.Attrs)\n\ttb.Stack(p.Stack)\n\n\treturn l.Add(Event{\n\t\tType:    BucketObjectUploadStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (tb *EventBuffer) bucketObjectAttrs(attrs *BucketObjectAttributes) {\n\ttb.OptUVarint(attrs.Size)\n\ttb.OptString(attrs.Version)\n\ttb.OptString(attrs.ETag)\n\ttb.OptString(attrs.ContentType)\n}\n\ntype BucketObjectUploadEndParams struct {\n\tEventParams\n\tStartID EventID\n\n\tErr error\n\t// Set iff err == nil\n\tSize    uint64\n\tVersion *string\n}\n\nfunc (l *Log) BucketObjectUploadEnd(p BucketObjectUploadEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         64,\n\t})\n\n\ttb.UVarint(p.Size)\n\ttb.OptString(p.Version)\n\ttb.ErrWithStack(p.Err)\n\n\tl.Add(Event{\n\t\tType:    BucketObjectUploadEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketObjectDownloadStartParams struct {\n\tEventParams\n\tBucket  string\n\tObject  string\n\tVersion *string\n\tStack   stack.Stack\n}\n\nfunc (l *Log) BucketObjectDownloadStart(p BucketObjectDownloadStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Bucket)\n\ttb.String(p.Object)\n\ttb.OptString(p.Version)\n\ttb.Stack(p.Stack)\n\n\treturn l.Add(Event{\n\t\tType:    BucketObjectDownloadStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketObjectDownloadEndParams struct {\n\tEventParams\n\tStartID EventID\n\n\tErr error\n\t// Set iff err == nil\n\tSize uint64\n}\n\nfunc (l *Log) BucketObjectDownloadEnd(p BucketObjectDownloadEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         4 + 4 + 8,\n\t})\n\n\ttb.UVarint(p.Size)\n\ttb.ErrWithStack(p.Err)\n\n\tl.Add(Event{\n\t\tType:    BucketObjectDownloadEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketObjectGetAttrsStartParams struct {\n\tEventParams\n\tBucket  string\n\tObject  string\n\tVersion *string\n\tStack   stack.Stack\n}\n\nfunc (l *Log) BucketObjectGetAttrsStart(p BucketObjectGetAttrsStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Bucket)\n\ttb.String(p.Object)\n\ttb.OptString(p.Version)\n\ttb.Stack(p.Stack)\n\n\treturn l.Add(Event{\n\t\tType:    BucketObjectGetAttrsStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketObjectGetAttrsEndParams struct {\n\tEventParams\n\tStartID EventID\n\n\tErr error\n\t// Set iff err == nil\n\tAttrs *BucketObjectAttributes\n}\n\nfunc (l *Log) BucketObjectGetAttrsEnd(p BucketObjectGetAttrsEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         4 + 4 + 8,\n\t})\n\n\ttb.ErrWithStack(p.Err)\n\tif p.Err == nil {\n\t\ttb.bucketObjectAttrs(p.Attrs)\n\t}\n\n\tl.Add(Event{\n\t\tType:    BucketObjectGetAttrsEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketListObjectsStartParams struct {\n\tEventParams\n\tBucket string\n\tPrefix *string\n\tStack  stack.Stack\n}\n\nfunc (l *Log) BucketListObjectsStart(p BucketListObjectsStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Bucket)\n\ttb.OptString(p.Prefix)\n\ttb.Stack(p.Stack)\n\n\treturn l.Add(Event{\n\t\tType:    BucketListObjectsStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketListObjectsEndParams struct {\n\tEventParams\n\tStartID EventID\n\n\tErr error\n\t// Set iff err == nil\n\tObserved uint64\n\tHasMore  bool\n}\n\nfunc (l *Log) BucketListObjectsEnd(p BucketListObjectsEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         4 + 4 + 8,\n\t})\n\n\ttb.ErrWithStack(p.Err)\n\ttb.UVarint(p.Observed)\n\ttb.Bool(p.HasMore)\n\n\tl.Add(Event{\n\t\tType:    BucketListObjectsEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketDeleteObjectsStartParams struct {\n\tEventParams\n\tBucket  string\n\tObjects []BucketDeleteObjectsEntry\n\tStack   stack.Stack\n}\n\ntype BucketDeleteObjectsEntry struct {\n\tObject  string\n\tVersion *string\n}\n\nfunc (l *Log) BucketDeleteObjectsStart(p BucketDeleteObjectsStartParams) EventID {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: 64,\n\t})\n\n\ttb.String(p.Bucket)\n\ttb.Stack(p.Stack)\n\ttb.UVarint(uint64(len(p.Objects)))\n\tfor _, e := range p.Objects {\n\t\ttb.String(e.Object)\n\t\ttb.OptString(e.Version)\n\t}\n\n\treturn l.Add(Event{\n\t\tType:    BucketDeleteObjectsStart,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\ntype BucketDeleteObjectsEndParams struct {\n\tEventParams\n\tStartID EventID\n\n\tErr error\n}\n\nfunc (l *Log) BucketDeleteObjectsEnd(p BucketDeleteObjectsEndParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:             p.EventParams,\n\t\tCorrelationEventID: p.StartID,\n\t\tExtraSpace:         4 + 4 + 8,\n\t})\n\n\ttb.ErrWithStack(p.Err)\n\n\tl.Add(Event{\n\t\tType:    BucketDeleteObjectsEnd,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc (l *Log) logHeaders(tb *EventBuffer, headers http.Header, scrubHeaders map[string]bool) {\n\ttb.UVarint(uint64(len(headers)))\n\tfor k, v := range headers {\n\t\tfirstVal := \"\"\n\t\tif scrubHeaders[k] {\n\t\t\tfirstVal = \"[REDACTED]\"\n\t\t} else if len(v) > 0 {\n\t\t\tfirstVal = v[0]\n\t\t}\n\t\ttb.String(k)\n\t\ttb.String(firstVal)\n\t}\n}\n\ntype LogMessageParams struct {\n\tEventParams\n\tLevel  model.LogLevel\n\tMsg    string\n\tStack  stack.Stack\n\tFields []LogField\n}\n\ntype LogField struct {\n\tKey   string\n\tValue any\n}\n\nfunc (l *Log) LogMessage(p LogMessageParams) {\n\ttb := l.newEvent(eventData{\n\t\tCommon:     p.EventParams,\n\t\tExtraSpace: len(p.Msg) + 1 + 64*len(p.Fields),\n\t})\n\n\ttb.Byte(byte(p.Level))\n\ttb.String(p.Msg)\n\n\ttb.UVarint(uint64(len(p.Fields)))\n\tfor _, f := range p.Fields {\n\t\taddLogField(&tb, f.Key, f.Value)\n\t}\n\ttb.Stack(p.Stack)\n\n\tl.Add(Event{\n\t\tType:    LogMessage,\n\t\tTraceID: p.TraceID,\n\t\tSpanID:  p.SpanID,\n\t\tData:    tb,\n\t})\n}\n\nfunc addLogField(tb *EventBuffer, key string, val any) {\n\tswitch val := val.(type) {\n\tcase error:\n\t\ttb.Byte(byte(model.ErrField))\n\t\ttb.String(key)\n\t\ttb.ErrWithStack(val)\n\tcase string:\n\t\ttb.Byte(byte(model.StringField))\n\t\ttb.String(key)\n\t\ttb.String(val)\n\tcase bool:\n\t\ttb.Byte(byte(model.BoolField))\n\t\ttb.String(key)\n\t\ttb.Bool(val)\n\tcase time.Time:\n\t\ttb.Byte(byte(model.TimeField))\n\t\ttb.String(key)\n\t\ttb.Time(val)\n\tcase time.Duration:\n\t\ttb.Byte(byte(model.DurationField))\n\t\ttb.String(key)\n\t\ttb.Int64(int64(val))\n\tcase uuid.UUID:\n\t\ttb.Byte(byte(model.UUIDField))\n\t\ttb.String(key)\n\t\ttb.Bytes(val[:])\n\n\tdefault:\n\t\ttb.Byte(byte(model.JSONField))\n\t\ttb.String(key)\n\t\tdata, err := json.Marshal(val)\n\t\tif err != nil {\n\t\t\ttb.ByteString(nil)\n\t\t\ttb.ErrWithStack(err)\n\t\t} else {\n\t\t\ttb.ByteString(data)\n\t\t\ttb.ErrWithStack(nil)\n\t\t}\n\n\tcase int8:\n\t\ttb.Byte(byte(model.IntField))\n\t\ttb.String(key)\n\t\ttb.Varint(int64(val))\n\tcase int16:\n\t\ttb.Byte(byte(model.IntField))\n\t\ttb.String(key)\n\t\ttb.Varint(int64(val))\n\tcase int32:\n\t\ttb.Byte(byte(model.IntField))\n\t\ttb.String(key)\n\t\ttb.Varint(int64(val))\n\tcase int64:\n\t\ttb.Byte(byte(model.IntField))\n\t\ttb.String(key)\n\t\ttb.Varint(int64(val))\n\tcase int:\n\t\ttb.Byte(byte(model.IntField))\n\t\ttb.String(key)\n\t\ttb.Varint(int64(val))\n\n\tcase uint8:\n\t\ttb.Byte(byte(model.UintField))\n\t\ttb.String(key)\n\t\ttb.UVarint(uint64(val))\n\tcase uint16:\n\t\ttb.Byte(byte(model.UintField))\n\t\ttb.String(key)\n\t\ttb.UVarint(uint64(val))\n\tcase uint32:\n\t\ttb.Byte(byte(model.UintField))\n\t\ttb.String(key)\n\t\ttb.UVarint(uint64(val))\n\tcase uint64:\n\t\ttb.Byte(byte(model.UintField))\n\t\ttb.String(key)\n\t\ttb.UVarint(uint64(val))\n\tcase uint:\n\t\ttb.Byte(byte(model.UintField))\n\t\ttb.String(key)\n\t\ttb.UVarint(uint64(val))\n\n\tcase float32:\n\t\ttb.Byte(byte(model.Float32Field))\n\t\ttb.String(key)\n\t\ttb.Float32(val)\n\tcase float64:\n\t\ttb.Byte(byte(model.Float64Field))\n\t\ttb.String(key)\n\t\ttb.Float64(val)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/http.go",
    "content": "package trace2\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptrace\"\n\t\"net/textproto\"\n\t\"sync\"\n\t_ \"unsafe\" // for go:linkname\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n)\n\nfunc (l *Log) HTTPBeginRoundTrip(httpReq *http.Request, req *model.Request, goid uint32) (context.Context, error) {\n\tif l == nil {\n\t\treturn httpReq.Context(), nil\n\t}\n\n\tcallCorrelationParentSpanID, err := model.GenSpanID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequestURL := httpReq.URL.String()\n\n\ttb := l.newEvent(eventData{\n\t\tCommon:     EventParams{Goid: goid},\n\t\tExtraSpace: 8 + len(httpReq.Method) + len(requestURL) + 4,\n\t})\n\n\ttb.Bytes(callCorrelationParentSpanID[:])\n\ttb.String(httpReq.Method)\n\ttb.String(requestURL)\n\ttb.Stack(stack.Build(4))\n\ttb.Int64(nanotime())\n\n\teventID := l.Add(Event{\n\t\tType:    HTTPCallStart,\n\t\tTraceID: req.TraceID,\n\t\tSpanID:  req.SpanID,\n\t\tData:    tb,\n\t})\n\n\trt := &httpRoundTrip{\n\t\tTraceID:                 req.TraceID,\n\t\tSpanID:                  req.SpanID,\n\t\tStartID:                 eventID,\n\t\tCorrelationParentSpanID: callCorrelationParentSpanID,\n\t\tlog:                     l,\n\t}\n\n\tctx := context.WithValue(httpReq.Context(), rtKey, rt)\n\ttr := &httptrace.ClientTrace{\n\t\tGetConn:              rt.getConn,\n\t\tGotConn:              rt.gotConn,\n\t\tGotFirstResponseByte: rt.gotFirstResponseByte,\n\t\tGot1xxResponse:       rt.got1xxResponse,\n\t\tDNSStart:             rt.dnsStart,\n\t\tDNSDone:              rt.dnsDone,\n\t\tConnectStart:         rt.connectStart,\n\t\tConnectDone:          rt.connectDone,\n\t\tTLSHandshakeStart:    rt.tlsHandshakeStart,\n\t\tTLSHandshakeDone:     rt.tlsHandshakeDone,\n\t\tWroteHeaders:         rt.wroteHeaders,\n\t\tWait100Continue:      rt.wait100Continue,\n\t\tWroteRequest:         rt.wroteRequest,\n\t}\n\treturn httptrace.WithClientTrace(ctx, tr), nil\n}\n\nfunc (l *Log) HTTPCompleteRoundTrip(req *http.Request, resp *http.Response, goid uint32, err error) {\n\trt, ok := req.Context().Value(rtKey).(*httpRoundTrip)\n\tif !ok {\n\t\treturn\n\t}\n\n\ttb := l.newEvent(eventData{\n\t\tCommon:             EventParams{Goid: goid},\n\t\tCorrelationEventID: rt.StartID,\n\t\tExtraSpace:         64,\n\t})\n\n\tif resp != nil {\n\t\ttb.UVarint(uint64(resp.StatusCode))\n\t} else {\n\t\ttb.UVarint(0)\n\t}\n\ttb.ErrWithStack(err)\n\n\trt.encodeEvents(&tb)\n\trt.log.Add(Event{\n\t\tType:    HTTPCallEnd,\n\t\tTraceID: rt.TraceID,\n\t\tSpanID:  rt.SpanID,\n\t\tData:    tb,\n\t})\n\n\tif req.Method != \"HEAD\" && resp != nil {\n\t\tresp.Body = wrapRespBody(resp.Body, rt)\n\t}\n}\n\ntype httpRoundTrip struct {\n\tTraceID                 model.TraceID\n\tSpanID                  model.SpanID\n\tStartID                 EventID\n\tCorrelationParentSpanID model.SpanID\n\n\tlog Logger\n\n\tmu     sync.Mutex\n\tevents []httpEvent\n}\n\nfunc (rt *httpRoundTrip) getConn(hostPort string) {\n\trt.addEvent(GetConn, &getConnEvent{hostPort: hostPort})\n}\n\nfunc (rt *httpRoundTrip) gotConn(info httptrace.GotConnInfo) {\n\trt.addEvent(GotConn, &gotConnEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) gotFirstResponseByte() {\n\trt.addEvent(GotFirstResponseByte, nil)\n}\n\nfunc (rt *httpRoundTrip) got1xxResponse(code int, header textproto.MIMEHeader) error {\n\trt.addEvent(Got1xxResponse, &got1xxResponseEvent{code: code, header: header})\n\treturn nil\n}\n\nfunc (rt *httpRoundTrip) dnsStart(info httptrace.DNSStartInfo) {\n\trt.addEvent(DNSStart, &dnsStartEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) dnsDone(info httptrace.DNSDoneInfo) {\n\trt.addEvent(DNSDone, &dnsDoneEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) connectStart(network, addr string) {\n\trt.addEvent(ConnectStart, &connectStartEvent{network: network, addr: addr})\n}\n\nfunc (rt *httpRoundTrip) connectDone(network, addr string, err error) {\n\trt.addEvent(ConnectDone, &connectDoneEvent{network: network, addr: addr, err: err})\n}\n\nfunc (rt *httpRoundTrip) tlsHandshakeStart() {\n\trt.addEvent(TLSHandshakeStart, nil)\n}\n\nfunc (rt *httpRoundTrip) tlsHandshakeDone(state tls.ConnectionState, err error) {\n\trt.addEvent(TLSHandshakeDone, &tlsHandshakeDoneEvent{info: state, err: err})\n}\n\nfunc (rt *httpRoundTrip) wroteHeaders() {\n\trt.addEvent(WroteHeaders, nil)\n}\n\nfunc (rt *httpRoundTrip) wroteRequest(info httptrace.WroteRequestInfo) {\n\trt.addEvent(WroteRequest, &wroteRequestEvent{info: info})\n}\n\nfunc (rt *httpRoundTrip) wait100Continue() {\n\trt.addEvent(Wait100Continue, nil)\n}\n\nfunc (rt *httpRoundTrip) addEvent(code HTTPEventCode, data httpEventData) {\n\tts := nanotime()\n\trt.mu.Lock()\n\tdefer rt.mu.Unlock()\n\n\trt.events = append(rt.events, httpEvent{\n\t\tcode: code,\n\t\tts:   ts,\n\t\tdata: data,\n\t})\n}\n\nfunc (rt *httpRoundTrip) encodeEvents(tb *EventBuffer) {\n\trt.mu.Lock()\n\tn := len(rt.events)\n\tevs := rt.events[:]\n\trt.mu.Unlock()\n\n\ttb.UVarint(uint64(n))\n\tfor _, e := range evs {\n\t\ttb.Bytes([]byte{byte(e.code)})\n\t\ttb.Int64(e.ts)\n\t\tif e.data != nil {\n\t\t\te.data.Encode(tb)\n\t\t}\n\t}\n}\n\nfunc (rt *httpRoundTrip) closedBody(err error) {\n\trt.addEvent(ClosedBody, &closedBodyEvent{err: err})\n}\n\nfunc wrapRespBody(body io.ReadCloser, rt *httpRoundTrip) io.ReadCloser {\n\tif readWriteCloser, ok := body.(io.ReadWriteCloser); ok {\n\t\treturn writerCloseTracker{readWriteCloser, rt}\n\t}\n\treturn closeTracker{body, rt}\n}\n\ntype closeTracker struct {\n\tio.ReadCloser\n\trt *httpRoundTrip\n}\n\nfunc (c closeTracker) Close() error {\n\terr := c.ReadCloser.Close()\n\tc.rt.closedBody(err)\n\treturn err\n}\n\ntype writerCloseTracker struct {\n\tio.ReadWriteCloser\n\trt *httpRoundTrip\n}\n\nfunc (c writerCloseTracker) Close() error {\n\terr := c.ReadWriteCloser.Close()\n\tc.rt.closedBody(err)\n\treturn err\n}\n\ntype httpEvent struct {\n\tcode HTTPEventCode\n\tts   int64\n\tdata httpEventData // or nil\n}\n\ntype httpEventData interface {\n\tEncode(tb *EventBuffer)\n}\n\ntype HTTPEventCode byte\n\nconst (\n\tGetConn              = 1\n\tGotConn              = 2\n\tGotFirstResponseByte = 3\n\tGot1xxResponse       = 4\n\tDNSStart             = 5\n\tDNSDone              = 6\n\tConnectStart         = 7\n\tConnectDone          = 8\n\tTLSHandshakeStart    = 9\n\tTLSHandshakeDone     = 10\n\tWroteHeaders         = 11\n\tWroteRequest         = 12\n\tWait100Continue      = 13\n\tClosedBody           = 14\n)\n\ntype getConnEvent struct {\n\thostPort string\n}\n\nfunc (e *getConnEvent) Encode(tb *EventBuffer) {\n\ttb.String(e.hostPort)\n}\n\ntype gotConnEvent struct {\n\tinfo httptrace.GotConnInfo\n}\n\nfunc (e *gotConnEvent) Encode(tb *EventBuffer) {\n\ttb.Bool(e.info.Reused)\n\ttb.Bool(e.info.WasIdle)\n\ttb.Int64(int64(e.info.IdleTime))\n}\n\ntype got1xxResponseEvent struct {\n\tcode   int\n\theader textproto.MIMEHeader\n}\n\nfunc (e *got1xxResponseEvent) Encode(tb *EventBuffer) {\n\ttb.Varint(int64(e.code))\n\t// TODO: write header as well?\n}\n\ntype dnsStartEvent struct {\n\tinfo httptrace.DNSStartInfo\n}\n\nfunc (e *dnsStartEvent) Encode(tb *EventBuffer) {\n\ttb.String(e.info.Host)\n}\n\ntype dnsDoneEvent struct {\n\tinfo httptrace.DNSDoneInfo\n}\n\nfunc (e *dnsDoneEvent) Encode(tb *EventBuffer) {\n\tif err := e.info.Err; err != nil {\n\t\tmsg := err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t\ttb.String(msg)\n\t} else {\n\t\ttb.String(\"\")\n\t}\n\ttb.UVarint(uint64(len(e.info.Addrs)))\n\tfor _, a := range e.info.Addrs {\n\t\ttb.ByteString(a.IP)\n\t}\n}\n\ntype connectStartEvent struct {\n\tnetwork string\n\taddr    string\n}\n\nfunc (e *connectStartEvent) Encode(tb *EventBuffer) {\n\ttb.String(e.network)\n\ttb.String(e.addr)\n}\n\ntype connectDoneEvent struct {\n\tnetwork string\n\taddr    string\n\terr     error\n}\n\nfunc (e *connectDoneEvent) Encode(tb *EventBuffer) {\n\ttb.String(e.network)\n\ttb.String(e.addr)\n\ttb.Err(e.err)\n}\n\ntype tlsHandshakeDoneEvent struct {\n\tinfo tls.ConnectionState\n\terr  error\n}\n\nfunc (e *tlsHandshakeDoneEvent) Encode(tb *EventBuffer) {\n\ttb.Err(e.err)\n\ttb.Uint32(uint32(e.info.Version))\n\ttb.Uint32(uint32(e.info.CipherSuite))\n\ttb.String(e.info.ServerName)\n\ttb.String(e.info.NegotiatedProtocol)\n}\n\ntype wroteRequestEvent struct {\n\tinfo httptrace.WroteRequestInfo\n}\n\nfunc (e *wroteRequestEvent) Encode(tb *EventBuffer) {\n\ttb.Err(e.info.Err)\n}\n\ntype closedBodyEvent struct {\n\terr error\n}\n\nfunc (e *closedBodyEvent) Encode(tb *EventBuffer) {\n\ttb.Err(e.err)\n}\n\ntype contextKey int\n\nconst (\n\trtKey contextKey = iota\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/log.go",
    "content": "package trace2\n\nimport (\n\t\"math\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\t_ \"unsafe\" // for go:linkname\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/beta/errs\"\n)\n\ntype EventID = model.TraceEventID\n\n// nextEventID is an atomic counter for event IDs.\nvar nextEventID atomic.Uint64\n\nfunc NewLog() *Log {\n\tl := &Log{}\n\tl.cond = sync.NewCond(&l.mu)\n\treturn l\n}\n\ntype Log struct {\n\tmu   sync.Mutex\n\tdata []byte\n\tdone bool\n\tcond *sync.Cond\n}\n\n// Ensure Log implements Logger.\nvar _ Logger = (*Log)(nil)\n\ntype Event struct {\n\tType    EventType\n\tTraceID model.TraceID\n\tSpanID  model.SpanID\n\tData    EventBuffer\n}\n\n// Add adds a new event in the trace log.\n// If l is nil, it does nothing.\nfunc (l *Log) Add(e Event) EventID {\n\tif l == nil {\n\t\treturn 0\n\t}\n\n\teventData := e.Data.Buf()\n\tln := len(eventData)\n\tif ln > (1<<32 - 1) {\n\t\tprintln(\"encore.traceEvent: event too large, dropping\")\n\t\treturn 0\n\t}\n\n\teventID := nextEventID.Add(1)\n\tif eventID == 0 {\n\t\t// We use 0 to indicate \"no event\" in several places,\n\t\t// so don't use that value.\n\t\teventID = nextEventID.Add(1)\n\t}\n\n\tts := signedToUnsigned(nanotime())\n\theader := [...]byte{\n\t\t// Event type, 1 byte\n\t\tbyte(e.Type),\n\n\t\t// Event ID, 8 bytes\n\t\tbyte(eventID),\n\t\tbyte(eventID >> 8),\n\t\tbyte(eventID >> 16),\n\t\tbyte(eventID >> 24),\n\t\tbyte(eventID >> 32),\n\t\tbyte(eventID >> 40),\n\t\tbyte(eventID >> 48),\n\t\tbyte(eventID >> 56),\n\n\t\t// Timestamp, 8 bytes\n\t\tbyte(ts),\n\t\tbyte(ts >> 8),\n\t\tbyte(ts >> 16),\n\t\tbyte(ts >> 24),\n\t\tbyte(ts >> 32),\n\t\tbyte(ts >> 40),\n\t\tbyte(ts >> 48),\n\t\tbyte(ts >> 56),\n\n\t\t// Trace ID, 16 bytes\n\t\te.TraceID[0], e.TraceID[1], e.TraceID[2], e.TraceID[3],\n\t\te.TraceID[4], e.TraceID[5], e.TraceID[6], e.TraceID[7],\n\t\te.TraceID[8], e.TraceID[9], e.TraceID[10], e.TraceID[11],\n\t\te.TraceID[12], e.TraceID[13], e.TraceID[14], e.TraceID[15],\n\n\t\t// Span ID, 8 bytes\n\t\te.SpanID[0], e.SpanID[1], e.SpanID[2], e.SpanID[3],\n\t\te.SpanID[4], e.SpanID[5], e.SpanID[6], e.SpanID[7],\n\n\t\t// Event data length, 4 bytes\n\t\tbyte(ln),\n\t\tbyte(ln >> 8),\n\t\tbyte(ln >> 16),\n\t\tbyte(ln >> 24),\n\t}\n\n\tl.mu.Lock()\n\tl.data = append(l.data, append(header[:], eventData...)...)\n\tl.mu.Unlock()\n\tl.cond.Broadcast()\n\n\treturn EventID(eventID)\n}\n\nfunc (l *Log) WaitUntilDone() {\n\tl.mu.Lock()\n\tfor !l.done {\n\t\tl.cond.Wait()\n\t}\n\tl.mu.Unlock()\n}\n\n// WaitAtLeast waits for at least dur to pass or for the log to be done.\n// If no trace data is being written it can block for longer than dur.\n// It reports whether the trace is done at the time of returning.\nfunc (l *Log) WaitAtLeast(dur time.Duration) (done bool) {\n\tnow := time.Now()\n\tl.mu.Lock()\n\tfor !l.done && time.Since(now) < dur {\n\t\tl.cond.Wait()\n\t}\n\tdone = l.done\n\tl.mu.Unlock()\n\treturn done\n}\n\n// WaitAndClear blocks for data to arrive and then returns the data\n// and whether the log has been completed. It also clears the log from\n// any data it returns.\nfunc (l *Log) WaitAndClear() (data []byte, done bool) {\n\tl.mu.Lock()\n\tfor len(l.data) == 0 && !l.done {\n\t\tl.cond.Wait()\n\t}\n\tdone = l.done\n\tdata = l.data\n\tl.clearDataBuf()\n\tl.mu.Unlock()\n\treturn data, done\n}\n\n// MarkDone marks the log as done.\nfunc (l *Log) MarkDone() {\n\tl.mu.Lock()\n\tl.done = true\n\tl.mu.Unlock()\n\tl.cond.Broadcast()\n}\n\nconst (\n\tmaxBufferSize     = 100 * (10 << 20) // 100 MiB\n\tinitialBufferSize = 10 * (10 << 20)  // 10 MiB\n)\n\n// GetAndClear gets the data and clears the buffer.\nfunc (l *Log) GetAndClear() (data []byte, done bool) {\n\tl.mu.Lock()\n\tdata, done = l.data, l.done\n\tl.clearDataBuf()\n\tl.mu.Unlock()\n\treturn data, done\n}\n\n// clearDataBuf clears the data buf, either allocating a new buffer\n// or by setting its length to 0 (keeping its capacity).\nfunc (l *Log) clearDataBuf() {\n\t// Determine if we should keep growing the buffer or if it's time to\n\t// create a new one to allow the old one to be GC'd.\n\tif cap(l.data) > maxBufferSize {\n\t\tl.data = make([]byte, 0, initialBufferSize)\n\t} else {\n\t\tl.data = l.data[len(l.data):]\n\t}\n}\n\n// EventBuffer is a performant, low-overhead, growable buffer\n// for buffering trace data in a compact way.\n//\n// The zero value is ready to be used, but NewEventBuffer\n// can be used to provide an initial size hint.\ntype EventBuffer struct {\n\tscratch [10]byte\n\tbuf     []byte\n}\n\nfunc NewEventBuffer(size int) EventBuffer {\n\treturn EventBuffer{buf: make([]byte, 0, size)}\n}\n\nfunc (tb *EventBuffer) Buf() []byte {\n\treturn tb.buf\n}\n\nfunc (tb *EventBuffer) Byte(b byte) {\n\ttb.buf = append(tb.buf, b)\n}\n\nfunc (tb *EventBuffer) Bytes(b []byte) {\n\ttb.buf = append(tb.buf, b...)\n}\n\nfunc (tb *EventBuffer) String(s string) {\n\ttb.UVarint(uint64(len(s)))\n\ttb.Bytes([]byte(s))\n}\n\nfunc (tb *EventBuffer) ByteString(b []byte) {\n\ttb.UVarint(uint64(len(b)))\n\ttb.Bytes(b)\n}\n\nfunc (tb *EventBuffer) StatusCode(code errs.ErrCode) {\n\ttb.Byte(byte(code))\n}\n\n// TruncatedByteString is like ByteString except it truncates b to maximum of maxLen.\n// If truncationSuffix is provided, it is appended after truncating, leading to\n// the final length being maxLen+len(truncationSuffix).\nfunc (tb *EventBuffer) TruncatedByteString(b []byte, maxLen int, truncationSuffix []byte) {\n\tif size := len(b); size > maxLen {\n\t\ttb.UVarint(uint64(maxLen + len(truncationSuffix)))\n\t\ttb.Bytes(b[:maxLen])\n\t\ttb.Bytes(truncationSuffix)\n\t} else {\n\t\ttb.ByteString(b)\n\t}\n}\n\nfunc (tb *EventBuffer) Now() {\n\tnow := time.Now()\n\ttb.Time(now)\n}\n\nfunc (tb *EventBuffer) Bool(b bool) {\n\tif b {\n\t\ttb.Bytes([]byte{1})\n\t} else {\n\t\ttb.Bytes([]byte{0})\n\t}\n}\n\nfunc (tb *EventBuffer) Err(err error) {\n\tmsg := \"\"\n\tif err != nil {\n\t\tmsg = err.Error()\n\t\tif msg == \"\" {\n\t\t\tmsg = \"unknown error\"\n\t\t}\n\t}\n\ttb.String(msg)\n}\n\nfunc (tb *EventBuffer) ErrWithStack(err error) {\n\tif err == nil {\n\t\ttb.String(\"\")\n\t\treturn\n\t}\n\n\tmsg := err.Error()\n\tif msg == \"\" {\n\t\tmsg = \"unknown error\"\n\t}\n\ttb.String(msg)\n\ttb.Stack(errs.Stack(err))\n}\n\nfunc (tb *EventBuffer) Time(t time.Time) {\n\ttb.Int64(t.Unix())\n\ttb.Int32(int32(t.Nanosecond()))\n}\n\nfunc (tb *EventBuffer) Int32(x int32) {\n\tvar u uint32\n\tif x < 0 {\n\t\tu = (^uint32(x) << 1) | 1 // complement i, bit 0 is 1\n\t} else {\n\t\tu = (uint32(x) << 1) // do not complement i, bit 0 is 0\n\t}\n\ttb.Uint32(u)\n}\n\nfunc (tb *EventBuffer) Uint32(x uint32) {\n\ttb.buf = append(tb.buf,\n\t\tbyte(x),\n\t\tbyte(x>>8),\n\t\tbyte(x>>16),\n\t\tbyte(x>>24),\n\t)\n}\n\nfunc (tb *EventBuffer) Int64(i int64) {\n\ttb.Uint64(signedToUnsigned(i))\n}\n\nfunc (tb *EventBuffer) EventID(id EventID) {\n\ttb.UVarint(uint64(id))\n}\n\nfunc (tb *EventBuffer) OptString(s *string) {\n\tif s != nil {\n\t\ttb.String(*s)\n\t} else {\n\t\ttb.String(\"\")\n\t}\n}\n\nfunc (tb *EventBuffer) OptUVarint(i *uint64) {\n\tif i != nil {\n\t\ttb.UVarint(*i)\n\t} else {\n\t\ttb.UVarint(0)\n\t}\n}\n\nfunc (tb *EventBuffer) Uint64(x uint64) {\n\ttb.buf = append(tb.buf,\n\t\tbyte(x),\n\t\tbyte(x>>8),\n\t\tbyte(x>>16),\n\t\tbyte(x>>24),\n\t\tbyte(x>>32),\n\t\tbyte(x>>40),\n\t\tbyte(x>>48),\n\t\tbyte(x>>56),\n\t)\n}\n\nfunc (tb *EventBuffer) Varint(i int64) {\n\ttb.UVarint(signedToUnsigned(i))\n}\n\nfunc (tb *EventBuffer) UVarint(u uint64) {\n\ti := 0\n\tfor u >= 0x80 {\n\t\ttb.scratch[i] = byte(u) | 0x80\n\t\tu >>= 7\n\t\ti++\n\t}\n\ttb.scratch[i] = byte(u)\n\ti++\n\ttb.Bytes(tb.scratch[:i])\n}\n\nfunc (tb *EventBuffer) Float32(f float32) {\n\ttb.Uint32(math.Float32bits(f))\n}\n\nfunc (tb *EventBuffer) Float64(f float64) {\n\ttb.Uint64(math.Float64bits(f))\n}\n\nfunc (tb *EventBuffer) Duration(dur time.Duration) {\n\ttb.Varint(int64(dur))\n}\n\nfunc (tb *EventBuffer) Stack(s stack.Stack) {\n\tn := len(s.Frames)\n\tif n > 0xFF {\n\t\t// Should never happen (the runtime caps it at 100),\n\t\t// but be defensive about it.\n\t\tn = 0xFF\n\t}\n\ttb.Byte(byte(n))\n\tif n == 0 {\n\t\treturn\n\t}\n\n\tvar prev int64 = 0\n\tfor _, pc := range s.Frames {\n\t\tp := int64(pc - s.Off)\n\t\tdiff := p - prev\n\t\ttb.Varint(diff)\n\t\tprev = p\n\t}\n}\n\n// FormattedStack is like Stack but includes the formatted frames.\nfunc (tb *EventBuffer) FormattedStack(s stack.Stack) {\n\tframes := stack.Format(s)\n\tn := len(frames)\n\tif n > 0xFF {\n\t\t// Should never happen (the runtime caps it at 100),\n\t\t// but be defensive about it.\n\t\tn = 0xFF\n\t}\n\n\ttb.Byte(byte(n))\n\tif n == 0 {\n\t\treturn\n\t}\n\n\tfor _, f := range frames[:n] {\n\t\ttb.String(f.File)\n\t\ttb.UVarint(uint64(f.Line))\n\t\ttb.String(f.Func)\n\t}\n}\n\nfunc signedToUnsigned(i int64) uint64 {\n\tif i < 0 {\n\t\treturn (^uint64(i) << 1) | 1 // complement i, bit 0 is 1\n\t} else {\n\t\treturn (uint64(i) << 1) // do not complement i, bit 0 is 0\n\t}\n}\n\n//go:linkname nanotime runtime.nanotime\nfunc nanotime() int64\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/logger.go",
    "content": "package trace2\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n)\n\n//go:generate mockgen -source=./logger.go -package=mock_trace -destination ../../shared/traceprovider/mock_trace/mock_trace.go Logger\n\ntype Logger interface {\n\tMarkDone()\n\tAdd(Event) EventID\n\n\tWaitUntilDone()\n\tWaitAtLeast(time.Duration) bool\n\tGetAndClear() (data []byte, done bool)\n\tWaitAndClear() (data []byte, done bool)\n\n\tRequestSpanStart(req *model.Request, goid uint32)\n\tRequestSpanEnd(params RequestSpanEndParams)\n\tAuthSpanStart(req *model.Request, goid uint32)\n\tAuthSpanEnd(params AuthSpanEndParams)\n\tPubsubMessageSpanStart(req *model.Request, goid uint32)\n\tPubsubMessageSpanEnd(params PubsubMessageSpanEndParams)\n\tTestSpanStart(req *model.Request, goid uint32)\n\tTestSpanEnd(params TestSpanEndParams)\n\tRPCCallStart(call *model.APICall, goid uint32) EventID\n\tRPCCallEnd(call *model.APICall, goid uint32, err error)\n\tDBQueryStart(p DBQueryStartParams) EventID\n\tDBQueryEnd(EventParams, EventID, error)\n\tDBTransactionStart(EventParams, stack.Stack) EventID\n\tDBTransactionEnd(DBTransactionEndParams)\n\tPubsubPublishStart(PubsubPublishStartParams) EventID\n\tPubsubPublishEnd(PubsubPublishEndParams)\n\tServiceInitStart(ServiceInitStartParams) EventID\n\tServiceInitEnd(EventParams, EventID, error)\n\tCacheCallStart(CacheCallStartParams) EventID\n\tCacheCallEnd(CacheCallEndParams)\n\tBodyStream(BodyStreamParams)\n\tLogMessage(LogMessageParams)\n\tHTTPBeginRoundTrip(httpReq *http.Request, req *model.Request, goid uint32) (context.Context, error)\n\tHTTPCompleteRoundTrip(req *http.Request, resp *http.Response, goid uint32, err error)\n\n\tBucketObjectUploadStart(BucketObjectUploadStartParams) EventID\n\tBucketObjectUploadEnd(BucketObjectUploadEndParams)\n\tBucketObjectDownloadStart(BucketObjectDownloadStartParams) EventID\n\tBucketObjectDownloadEnd(BucketObjectDownloadEndParams)\n\tBucketObjectGetAttrsStart(BucketObjectGetAttrsStartParams) EventID\n\tBucketObjectGetAttrsEnd(BucketObjectGetAttrsEndParams)\n\tBucketListObjectsStart(BucketListObjectsStartParams) EventID\n\tBucketListObjectsEnd(BucketListObjectsEndParams)\n\tBucketDeleteObjectsStart(BucketDeleteObjectsStartParams) EventID\n\tBucketDeleteObjectsEnd(BucketDeleteObjectsEndParams)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/mutex_app.go",
    "content": "//go:build encore_app\n\npackage trace2\n\nimport _ \"unsafe\" // for go:linkname\n\n// mutex must exactly match implementation in the runtime.\ntype mutex struct {\n\tkey uintptr\n}\n\n//go:linkname mutexLock runtime.lock\nfunc mutexLock(mut *mutex)\n\n//go:linkname mutexUnlock runtime.unlock\nfunc mutexUnlock(mut *mutex)\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/mutex_noapp.go",
    "content": "//go:build !encore_app\n\npackage trace2\n\nimport \"sync\"\n\ntype mutex struct {\n\tmut sync.Mutex\n}\n\nfunc mutexLock(m *mutex) { m.mut.Lock() }\n\nfunc mutexUnlock(m *mutex) { m.mut.Unlock() }\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/timeanchor.go",
    "content": "package trace2\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// NewTimeAnchor constructs a new TimeAnchor.\nfunc NewTimeAnchor(nano int64, real time.Time) TimeAnchor {\n\treturn TimeAnchor{nano: nano, real: real}\n}\n\n// NewTimeAnchorNow constructs a new TimeAnchor based on the current time.\nfunc NewTimeAnchorNow() TimeAnchor {\n\tnow := time.Now()\n\tnano := nanotime()\n\treturn NewTimeAnchor(nano, now)\n}\n\n// TimeAnchor represents a mapping between nanotime() timestamps\n// and real-world time.Time instants.\ntype TimeAnchor struct {\n\tnano int64\n\treal time.Time\n}\n\n// ToReal converts a nanotime() timestamp to a real-world time.Time instant.\nfunc (ta TimeAnchor) ToReal(nano int64) time.Time {\n\treturn ta.real.Add(time.Duration(nano - ta.nano))\n}\n\n// MarshalText marshals the anchor as text. It never fails.\nfunc (ta TimeAnchor) MarshalText() ([]byte, error) {\n\tnano := strconv.FormatInt(ta.nano, 10)\n\treturn []byte(nano + \" \" + ta.real.Format(time.RFC3339Nano)), nil\n}\n\n// UnmarshalText unmarshals the anchor from text.\nfunc (ta *TimeAnchor) UnmarshalText(text []byte) error {\n\ta, b, ok := strings.Cut(string(text), \" \")\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid time anchor format: %q\", text)\n\t}\n\tnano, err := strconv.ParseInt(a, 10, 64)\n\tif err != nil {\n\t\treturn err\n\t}\n\treal, err := time.Parse(time.RFC3339Nano, b)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tta.nano = nano\n\tta.real = real\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/exported/trace2/version.go",
    "content": "package trace2\n\ntype Version int\n\n// CurrentVersion is the trace protocol version this package produces traces in.\nconst CurrentVersion Version = 17\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metadata/aws_collector.go",
    "content": "//go:build !encore_no_aws\n\npackage metadata\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\tencore \"encore.dev\"\n)\n\nfunc init() {\n\tregisterCollector(collectorDesc{\n\t\tname: \"aws\",\n\t\tmatches: func(envCloud string) bool {\n\t\t\treturn envCloud == encore.CloudAWS\n\t\t},\n\t\tcollect: func() (*ContainerMetadata, error) {\n\t\t\t// Encore supports running on both ECS Fargate and EKS.\n\t\t\t// For Fargate, we can get the metadata from the ECS metadata service.\n\t\t\t// For EKS there doesn't appear to be a standard way to get the metadata, so skip it in that case.\n\t\t\tmetadataURI, ok := os.LookupEnv(\"ECS_CONTAINER_METADATA_URI_V4\")\n\t\t\tif !ok {\n\t\t\t\treturn &ContainerMetadata{\n\t\t\t\t\tServiceID:  \"\",\n\t\t\t\t\tRevisionID: \"\",\n\t\t\t\t\tInstanceID: \"\",\n\t\t\t\t}, nil\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\t\t\tdefer cancel()\n\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURI+\"/task\", nil)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to create metadata request: %w\", err)\n\t\t\t}\n\n\t\t\tresp, err := http.DefaultClient.Do(req)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error sending metadata request: %w\", err)\n\t\t\t}\n\t\t\tdefer func() { _ = resp.Body.Close() }()\n\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error reading metadata response: %w\", err)\n\t\t\t}\n\n\t\t\ttaskMetadata := &struct {\n\t\t\t\tServiceName string `json:\"ServiceName\"`\n\t\t\t\tRevision    string `json:\"Revision\"`\n\t\t\t\tTaskARN     string `json:\"TaskARN\"`\n\t\t\t}{}\n\t\t\terr = json.Unmarshal(body, &taskMetadata)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error unmarshaling metadata response body: %w\", err)\n\t\t\t}\n\n\t\t\treturn &ContainerMetadata{\n\t\t\t\tServiceID:  taskMetadata.ServiceName,\n\t\t\t\tRevisionID: taskMetadata.Revision,\n\t\t\t\tInstanceID: taskMetadata.TaskARN[len(taskMetadata.TaskARN)-8:],\n\t\t\t}, nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metadata/cloud_run_collector.go",
    "content": "//go:build !encore_no_gcp\n\npackage metadata\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tgcemetadata \"cloud.google.com/go/compute/metadata\"\n\n\tencore \"encore.dev\"\n)\n\nfunc init() {\n\tregisterCollector(collectorDesc{\n\t\tname: \"cloud_run\",\n\t\tmatches: func(envCloud string) bool {\n\t\t\treturn envCloud == encore.EncoreCloud || envCloud == encore.CloudGCP\n\t\t},\n\t\tcollect: func() (*ContainerMetadata, error) {\n\t\t\tservice, ok := os.LookupEnv(\"K_SERVICE\")\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to get service ID: env variable '%s' unset\", \"K_SERVICE\")\n\t\t\t}\n\n\t\t\trevision, ok := os.LookupEnv(\"K_REVISION\")\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to get revision ID: env variable '%s' unset\", \"K_REVISION\")\n\t\t\t}\n\t\t\trevision = strings.TrimPrefix(revision, service+\"-\")\n\n\t\t\tinstanceID, ok := os.LookupEnv(\"K_POD\")\n\t\t\tif !ok {\n\t\t\t\t// If we don't have a K8s POD name, we're running on Cloud Run and can get the instance ID from the metadata server\n\t\t\t\tvar err error\n\t\t\t\tinstanceID, err = gcemetadata.InstanceID()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unable to get instance ID: %w\", err)\n\t\t\t\t}\n\t\t\t\tinstanceID = instanceID[len(instanceID)-8:]\n\t\t\t} else {\n\t\t\t\t// If we have a K8s POD name, take the last part of it which is the random pod ID\n\t\t\t\t// On GKE, the InstanceID appears to be the Node, so if the multiple replicas are running\n\t\t\t\t// on the same InstanceID then we'd have a collision. This is unlikely, but possible -\n\t\t\t\t// hence why we use the pod ID instead.\n\t\t\t\tidx := strings.LastIndex(instanceID, \"-\")\n\t\t\t\tif idx == -1 {\n\t\t\t\t\treturn nil, fmt.Errorf(\"invalid instance ID '%s'\", instanceID)\n\t\t\t\t}\n\t\t\t\tinstanceID = instanceID[idx+1:]\n\t\t\t}\n\n\t\t\treturn &ContainerMetadata{\n\t\t\t\tServiceID:  service,\n\t\t\t\tRevisionID: revision,\n\t\t\t\tInstanceID: instanceID,\n\t\t\t}, nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metadata/metadata.go",
    "content": "package metadata\n\nimport (\n\t\"fmt\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\ntype collectorDesc struct {\n\tname    string\n\tmatches func(envCloud string) bool\n\tcollect func() (*ContainerMetadata, error)\n}\n\nvar collectorRegistry []collectorDesc\n\nfunc registerCollector(desc collectorDesc) {\n\tcollectorRegistry = append(collectorRegistry, desc)\n}\n\ntype Label struct {\n\tkey, value string\n}\n\ntype Labels []Label\n\nfunc (l Labels) AsMap() map[string]string {\n\tm := make(map[string]string)\n\tfor _, kv := range l {\n\t\tm[kv.key] = kv.value\n\t}\n\treturn m\n}\n\nfunc (l *Labels) AddNonEmpty(key, value string) *Labels {\n\tif value != \"\" {\n\t\t*l = append(*l, Label{key, value})\n\t}\n\treturn l\n}\n\ntype ContainerMetadata struct {\n\tServiceID  string\n\tRevisionID string\n\tInstanceID string\n\tEnvName    string\n}\n\nfunc (md *ContainerMetadata) Labels() Labels {\n\tvar labels Labels\n\tlabels.AddNonEmpty(\"service_id\", md.ServiceID)\n\tlabels.AddNonEmpty(\"revision_id\", md.RevisionID)\n\tlabels.AddNonEmpty(\"instance_id\", md.InstanceID)\n\tlabels.AddNonEmpty(\"env_name\", md.EnvName)\n\treturn labels\n}\n\nfunc MapMetadataLabels[T any](md *ContainerMetadata, fn func(k, v string) T) []T {\n\tvar labels []T\n\tfor _, v := range md.Labels() {\n\t\tlabels = append(labels, fn(v.key, v.value))\n\t}\n\treturn labels\n}\n\nfunc GetContainerMetadata(cfg *config.Runtime) (*ContainerMetadata, error) {\n\tfor _, collector := range collectorRegistry {\n\t\tif collector.matches(cfg.EnvCloud) {\n\t\t\tmd, err := collector.collect()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tmd.EnvName = cfg.EnvName\n\t\t\treturn md, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"no metadata collector found for environment cloud '%s'\", cfg.EnvCloud)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/README.md",
    "content": "# `metrics`\n\nThis package allows an Encore app to export custom metrics to metric providers.\n\nWe have two types of custom metrics: a predefined set of custom metrics which are enabled for all Encore apps and\nuser-defined custom metrics.\n\n## Predefined Encore metrics\n\nThese are the metrics all Encore apps export to metric providers:\n\n- `e_requests_total` measures the number of requests and has three labels `service`, `endpoint` and `code`. `code` is a\n  human-readable HTTP status code (e.g. `ok`, `not_found`).\n- `e_sys_memory_heap_objects_bytes` measures the memory occupied by live objects and dead objects that have not yet been\n  marked free by the garbage collector.\n- `e_sys_sched_goroutines` measures the number of live goroutines."
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/aws/cloudwatch.go",
    "content": "//go:build !encore_no_aws\n\npackage aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsconfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/cloudwatch\"\n\t\"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/system\"\n\t\"encore.dev/appruntime/shared/nativehist\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/metrics\"\n)\n\nfunc New(svcs []string, cfg *config.AWSCloudWatchMetricsProvider, meta *metadata.ContainerMetadata, rootLogger zerolog.Logger) *Exporter {\n\t// Precompute container metadata dimensions.\n\texporter := &Exporter{\n\t\tsvcs:       svcs,\n\t\tcfg:        cfg,\n\t\trootLogger: rootLogger,\n\t\tcontainerMetadataDims: metadata.MapMetadataLabels(meta, func(key, value string) types.Dimension {\n\t\t\treturn types.Dimension{\n\t\t\t\tName:  aws.String(key),\n\t\t\t\tValue: aws.String(value),\n\t\t\t}\n\t\t}),\n\t}\n\n\treturn exporter\n}\n\ntype Exporter struct {\n\tsvcs                  []string\n\tcfg                   *config.AWSCloudWatchMetricsProvider\n\tcontainerMetadataDims []types.Dimension\n\trootLogger            zerolog.Logger\n\n\tclientMu sync.Mutex\n\tclient   *cloudwatch.Client\n}\n\nfunc (x *Exporter) Shutdown(p *shutdown.Process) error {\n\treturn nil\n}\n\nfunc (x *Exporter) Export(ctx context.Context, collected []metrics.CollectedMetric) error {\n\tnow := time.Now()\n\tdata := x.getMetricData(now, collected)\n\tdata = append(data, x.getSysMetrics(now)...)\n\n\t// CloudWatch has a maximum of 1000 metrics per PutMetricData request\n\tconst maxMetricsPerRequest = 1000\n\tclient := x.getClient()\n\n\tfor i := 0; i < len(data); i += maxMetricsPerRequest {\n\t\tend := min(i+maxMetricsPerRequest, len(data))\n\n\t\tbatch := data[i:end]\n\t\t_, err := client.PutMetricData(ctx, &cloudwatch.PutMetricDataInput{\n\t\t\tMetricData: batch,\n\t\t\tNamespace:  aws.String(x.cfg.Namespace),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to send metrics to AWS CloudWatch: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (x *Exporter) getMetricData(now time.Time, collected []metrics.CollectedMetric) []types.MetricDatum {\n\tdata := make([]types.MetricDatum, 0, len(collected))\n\n\tdoAdd := func(val float64, metricName string, baseDims []types.Dimension, svcIdx uint16) {\n\t\tdims := make([]types.Dimension, len(baseDims)+1)\n\t\tcopy(dims, baseDims)\n\t\tdims[len(baseDims)] = types.Dimension{\n\t\t\tName:  aws.String(\"service\"),\n\t\t\tValue: aws.String(x.svcs[svcIdx]),\n\t\t}\n\t\tdata = append(data, types.MetricDatum{\n\t\t\tMetricName: aws.String(metricName),\n\t\t\tTimestamp:  aws.Time(now),\n\t\t\tValue:      aws.Float64(val),\n\t\t\tDimensions: dims,\n\t\t})\n\t}\n\n\tfor _, m := range collected {\n\t\tdims := make([]types.Dimension, len(x.containerMetadataDims), len(x.containerMetadataDims)+len(m.Labels))\n\t\tcopy(dims, x.containerMetadataDims)\n\t\tfor _, label := range m.Labels {\n\t\t\tif label.Value == \"\" {\n\t\t\t\tx.rootLogger.Warn().Str(\"label\", label.Key).Msg(\"metrics: aws cloudwatch does not support empty label values, skipping\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tdims = append(dims, types.Dimension{\n\t\t\t\tName:  aws.String(label.Key),\n\t\t\t\tValue: aws.String(label.Value),\n\t\t\t})\n\t\t}\n\n\t\tsvcNum := m.Info.SvcNum()\n\t\tswitch vals := m.Val.(type) {\n\t\tcase []float64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(vals[0], m.Info.Name(), dims, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(val, m.Info.Name(), dims, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []int64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]), m.Info.Name(), dims, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val), m.Info.Name(), dims, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []uint64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]), m.Info.Name(), dims, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val), m.Info.Name(), dims, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []time.Duration:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]/time.Second), m.Info.Name(), dims, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val/time.Second), m.Info.Name(), dims, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []*nativehist.Histogram:\n\t\t\t// TODO implement support\n\t\tdefault:\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: unknown value type %T for metric %s\",\n\t\t\t\tm.Val, m.Info.Name())\n\t\t}\n\t}\n\n\treturn data\n}\n\nfunc (x *Exporter) getSysMetrics(now time.Time) []types.MetricDatum {\n\tsysMetrics := system.ReadSysMetrics(x.rootLogger)\n\treturn []types.MetricDatum{\n\t\t{\n\t\t\tMetricName: aws.String(system.MetricNameHeapObjectsBytes),\n\t\t\tTimestamp:  aws.Time(now),\n\t\t\tValue:      aws.Float64(float64(sysMetrics[system.MetricNameHeapObjectsBytes])),\n\t\t\tDimensions: x.containerMetadataDims,\n\t\t},\n\t\t{\n\t\t\tMetricName: aws.String(system.MetricNameGoroutines),\n\t\t\tTimestamp:  aws.Time(now),\n\t\t\tValue:      aws.Float64(float64(sysMetrics[system.MetricNameGoroutines])),\n\t\t\tDimensions: x.containerMetadataDims,\n\t\t},\n\t}\n}\n\nfunc (x *Exporter) getClient() *cloudwatch.Client {\n\tx.clientMu.Lock()\n\tdefer x.clientMu.Unlock()\n\tif x.client == nil {\n\t\tcfg, err := awsconfig.LoadDefaultConfig(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"unable to load AWS config: %v\", err))\n\t\t}\n\t\tcl := cloudwatch.NewFromConfig(cfg)\n\t\tx.client = cl\n\t}\n\treturn x.client\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/aws/cloudwatch_test.go",
    "content": "//go:build !encore_no_aws\n\npackage aws\n\nimport (\n\t\"io\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/cloudwatch/types\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rs/zerolog\"\n\t\"google.golang.org/protobuf/testing/protocmp\"\n\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/metrics\"\n)\n\ntype metricInfo struct {\n\tname   string\n\ttyp    metrics.MetricType\n\tsvcNum uint16\n}\n\nfunc (m metricInfo) Name() string             { return m.name }\nfunc (m metricInfo) Type() metrics.MetricType { return m.typ }\nfunc (m metricInfo) SvcNum() uint16           { return m.svcNum }\n\nfunc TestGetMetricData(t *testing.T) {\n\tnow := time.Now()\n\tsvcs := []string{\"foo\", \"bar\"}\n\tmeta := &metadata.ContainerMetadata{\n\t\tServiceID:  \"a-fargate-task\",\n\t\tRevisionID: \"43\",\n\t\tInstanceID: \"a-fargate-task-instance\",\n\t}\n\n\tbaseDimensions := []types.Dimension{\n\t\t{Name: aws.String(\"service_id\"), Value: aws.String(\"a-fargate-task\")},\n\t\t{Name: aws.String(\"revision_id\"), Value: aws.String(\"43\")},\n\t\t{Name: aws.String(\"instance_id\"), Value: aws.String(\"a-fargate-task-instance\")},\n\t}\n\n\tmakeDimensions := func(dims ...types.Dimension) []types.Dimension {\n\t\trtn := make([]types.Dimension, len(baseDimensions)+len(dims))\n\t\ti := 0\n\t\tfor _, dim := range baseDimensions {\n\t\t\trtn[i] = dim\n\t\t\ti++\n\t\t}\n\t\tfor _, dim := range dims {\n\t\t\trtn[i] = dim\n\t\t\ti++\n\t\t}\n\t\treturn rtn\n\t}\n\n\ttests := []struct {\n\t\tname   string\n\t\tmetric metrics.CollectedMetric\n\t\tdata   []types.MetricDatum\n\t}{\n\t\t{\n\t\t\tname: \"counter\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_counter\", metrics.CounterType, 1},\n\t\t\t\tVal:  []int64{10},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []types.MetricDatum{{\n\t\t\t\tMetricName: aws.String(\"test_counter\"),\n\t\t\t\tDimensions: makeDimensions(\n\t\t\t\t\ttypes.Dimension{Name: aws.String(\"service\"), Value: aws.String(\"foo\")},\n\t\t\t\t),\n\t\t\t\tTimestamp: aws.Time(now),\n\t\t\t\tValue:     aws.Float64(10),\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"gauge\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_gauge\", metrics.GaugeType, 2},\n\t\t\t\tVal:  []float64{0.5},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []types.MetricDatum{{\n\t\t\t\tMetricName: aws.String(\"test_gauge\"),\n\t\t\t\tDimensions: makeDimensions(\n\t\t\t\t\ttypes.Dimension{Name: aws.String(\"service\"), Value: aws.String(\"bar\")},\n\t\t\t\t),\n\t\t\t\tTimestamp: aws.Time(now),\n\t\t\t\tValue:     aws.Float64(0.5),\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"labels\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo:   metricInfo{\"test_labels\", metrics.GaugeType, 1},\n\t\t\t\tLabels: []metrics.KeyValue{{\"key\", \"value\"}},\n\t\t\t\tVal:    []float64{-1.5},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []types.MetricDatum{{\n\t\t\t\tMetricName: aws.String(\"test_labels\"),\n\t\t\t\tDimensions: makeDimensions(\n\t\t\t\t\ttypes.Dimension{Name: aws.String(\"key\"), Value: aws.String(\"value\")},\n\t\t\t\t\ttypes.Dimension{Name: aws.String(\"service\"), Value: aws.String(\"foo\")},\n\t\t\t\t),\n\t\t\t\tTimestamp: aws.Time(now),\n\t\t\t\tValue:     aws.Float64(-1.5),\n\t\t\t}},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tx := New(svcs, nil, meta, zerolog.New(io.Discard))\n\n\t\t\tgot := x.getMetricData(now, []metrics.CollectedMetric{test.metric})\n\t\t\tif diff := cmp.Diff(\n\t\t\t\tgot, test.data,\n\t\t\t\tprotocmp.Transform(),\n\t\t\t\tcmpopts.IgnoreUnexported(types.Dimension{}, types.MetricDatum{}),\n\t\t\t); diff != \"\" {\n\t\t\t\tt.Errorf(\"getMetricData() mismatch (-got +want):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/aws_cloudwatch_exporter.go",
    "content": "//go:build !encore_no_aws\n\npackage metrics\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/aws\"\n)\n\nfunc init() {\n\tregisterProvider(providerDesc{\n\t\tname: \"aws_cloudwatch\",\n\t\tmatches: func(cfg *config.Metrics) bool {\n\t\t\treturn cfg.CloudWatch != nil\n\t\t},\n\t\tnewExporter: func(m *Manager) exporter {\n\t\t\tcontainerMetadata, err := metadata.GetContainerMetadata(m.runtime)\n\t\t\tif err != nil {\n\t\t\t\tm.rootLogger.Err(err).Msg(\"unable to initialize metrics exporter: error getting container metadata\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn aws.New(m.static.BundledServices, m.runtime.Metrics.CloudWatch, containerMetadata, m.rootLogger)\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/datadog/datadog.go",
    "content": "//go:build !encore_no_datadog\n\npackage datadog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/DataDog/datadog-api-client-go/v2/api/datadog\"\n\t\"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/system\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/metrics\"\n)\n\nfunc New(svcs []string, cfg *config.DatadogProvider, meta *metadata.ContainerMetadata, rootLogger zerolog.Logger) *Exporter {\n\tconfiguration := datadog.NewConfiguration()\n\tapiClient := datadog.NewAPIClient(configuration)\n\tapi := datadogV2.NewMetricsApi(apiClient)\n\n\t// Precompute container metadata labels.\n\treturn &Exporter{\n\t\tclient: api,\n\t\tsvcs:   svcs,\n\t\tcfg:    cfg,\n\t\tcontainerMetadataLabels: metadata.MapMetadataLabels(meta, func(k, v string) string {\n\t\t\treturn fmt.Sprintf(\"%s:%s\", k, v)\n\t\t}),\n\t\trootLogger: rootLogger,\n\t\tlastExport: time.Now().Unix(),\n\t\tlastValue:  map[tsSvcKey]float64{},\n\t}\n}\n\ntype tsSvcKey struct {\n\ttsID uint64\n\tsvc  uint16\n}\n\ntype Exporter struct {\n\tclient                  *datadogV2.MetricsApi\n\tsvcs                    []string\n\tcfg                     *config.DatadogProvider\n\tcontainerMetadataLabels []string\n\trootLogger              zerolog.Logger\n\tlastExport              int64\n\tlastValue               map[tsSvcKey]float64\n}\n\nfunc (x *Exporter) Shutdown(p *shutdown.Process) error {\n\treturn nil\n}\n\nfunc (x *Exporter) Export(ctx context.Context, collected []metrics.CollectedMetric) error {\n\tnow := time.Now()\n\tdata := x.getMetricData(now, collected)\n\tdata = append(data, x.getSysMetrics(now)...)\n\tbody := datadogV2.MetricPayload{Series: data}\n\n\tctx = x.newContext(ctx)\n\t_, _, err := x.client.SubmitMetrics(ctx, body, *datadogV2.NewSubmitMetricsOptionalParameters())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to send metrics to Datadog: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc (x *Exporter) getMetricData(now time.Time, collected []metrics.CollectedMetric) []datadogV2.MetricSeries {\n\tdata := make([]datadogV2.MetricSeries, 0, len(collected))\n\tfor _, m := range collected {\n\t\tvar metricType *datadogV2.MetricIntakeType\n\t\tswitch m.Info.Type() {\n\t\tcase metrics.CounterType:\n\t\t\tmetricType = datadogV2.METRICINTAKETYPE_COUNT.Ptr()\n\t\tcase metrics.GaugeType:\n\t\t\tmetricType = datadogV2.METRICINTAKETYPE_GAUGE.Ptr()\n\t\tdefault:\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: unknown metric type %v for metric %s\", m.Info.Type(), m.Info.Name())\n\t\t\tcontinue\n\t\t}\n\n\t\tlabels := make([]string, len(x.containerMetadataLabels))\n\t\tcopy(labels, x.containerMetadataLabels)\n\t\tfor _, label := range m.Labels {\n\t\t\tlabels = append(labels, label.Key+\":\"+label.Value)\n\t\t}\n\n\t\tdoAdd := func(val float64, metricName string, baseLabels []string, svcIdx uint16) {\n\t\t\tlabels := make([]string, len(baseLabels)+1)\n\t\t\tcopy(labels, baseLabels)\n\t\t\tlabels[len(baseLabels)] = \"service:\" + x.svcs[svcIdx]\n\t\t\tif m.Info.Type() == metrics.CounterType {\n\t\t\t\tkey := tsSvcKey{tsID: m.TimeSeriesID, svc: svcIdx}\n\t\t\t\tlastVal := x.lastValue[key]\n\t\t\t\tx.lastValue[key] = val\n\t\t\t\tval = val - lastVal\n\t\t\t}\n\t\t\tdata = append(data, datadogV2.MetricSeries{\n\t\t\t\tInterval: datadog.PtrInt64(now.Unix() - x.lastExport),\n\t\t\t\tMetric:   metricName,\n\t\t\t\tPoints: []datadogV2.MetricPoint{{\n\t\t\t\t\tTimestamp: datadog.PtrInt64(now.Unix()),\n\t\t\t\t\tValue:     datadog.PtrFloat64(val),\n\t\t\t\t}},\n\t\t\t\tTags: labels,\n\t\t\t\tType: metricType,\n\t\t\t})\n\t\t}\n\n\t\tsvcNum := m.Info.SvcNum()\n\t\tswitch vals := m.Val.(type) {\n\t\tcase []float64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(vals[0], m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(val, m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []int64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]), m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val), m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []uint64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]), m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val), m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []time.Duration:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]/time.Second), m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val/time.Second), m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: unknown value type %T for metric %s\", m.Val, m.Info.Name())\n\t\t}\n\t}\n\n\tx.lastExport = now.Unix()\n\treturn data\n}\n\nfunc (x *Exporter) getSysMetrics(now time.Time) []datadogV2.MetricSeries {\n\tsysMetrics := system.ReadSysMetrics(x.rootLogger)\n\treturn []datadogV2.MetricSeries{\n\t\t{\n\t\t\tMetric: system.MetricNameHeapObjectsBytes,\n\t\t\tPoints: []datadogV2.MetricPoint{{\n\t\t\t\tTimestamp: datadog.PtrInt64(now.Unix()),\n\t\t\t\tValue:     datadog.PtrFloat64(float64(sysMetrics[system.MetricNameHeapObjectsBytes])),\n\t\t\t}},\n\t\t\tTags: x.containerMetadataLabels,\n\t\t\tType: datadogV2.METRICINTAKETYPE_GAUGE.Ptr(),\n\t\t},\n\t\t{\n\t\t\tMetric: system.MetricNameGoroutines,\n\t\t\tPoints: []datadogV2.MetricPoint{{\n\t\t\t\tTimestamp: datadog.PtrInt64(now.Unix()),\n\t\t\t\tValue:     datadog.PtrFloat64(float64(sysMetrics[system.MetricNameGoroutines])),\n\t\t\t}},\n\t\t\tTags: x.containerMetadataLabels,\n\t\t\tType: datadogV2.METRICINTAKETYPE_GAUGE.Ptr(),\n\t\t},\n\t}\n}\n\nfunc (x *Exporter) newContext(parent context.Context) context.Context {\n\treturn context.WithValue(\n\t\tcontext.WithValue(\n\t\t\tparent,\n\t\t\tdatadog.ContextServerVariables,\n\t\t\tmap[string]string{\"site\": x.cfg.Site},\n\t\t),\n\t\tdatadog.ContextAPIKeys,\n\t\tmap[string]datadog.APIKey{\n\t\t\t\"apiKeyAuth\": {Key: x.cfg.APIKey},\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/datadog_exporter.go",
    "content": "//go:build !encore_no_datadog\n\npackage metrics\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/datadog\"\n)\n\nfunc init() {\n\tregisterProvider(providerDesc{\n\t\tname: \"datadog\",\n\t\tmatches: func(cfg *config.Metrics) bool {\n\t\t\treturn cfg.Datadog != nil\n\t\t},\n\t\tnewExporter: func(m *Manager) exporter {\n\t\t\tcontainerMetadata, err := metadata.GetContainerMetadata(m.runtime)\n\t\t\tif err != nil {\n\t\t\t\tm.rootLogger.Err(err).Msg(\"unable to initialize metrics exporter: error getting container metadata\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn datadog.New(m.static.BundledServices, m.runtime.Metrics.Datadog, containerMetadata, m.rootLogger)\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/encore_cloud_exporter.go",
    "content": "//go:build !encore_no_gcp\n\npackage metrics\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/gcp\"\n)\n\nfunc init() {\n\tregisterProvider(providerDesc{\n\t\tname: \"encore_cloud\",\n\t\tmatches: func(cfg *config.Metrics) bool {\n\t\t\treturn cfg.EncoreCloud != nil\n\t\t},\n\t\tnewExporter: func(mgr *Manager) exporter {\n\t\t\tcontainerMetadata, err := metadata.GetContainerMetadata(mgr.runtime)\n\t\t\tif err != nil {\n\t\t\t\tmgr.rootLogger.Err(err).Msg(\"unable to initialize metrics exporter: error getting container metadata\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tmetricsCfg := mgr.runtime.Metrics\n\t\t\tnodeID, ok := metricsCfg.EncoreCloud.MonitoredResourceLabels[\"node_id\"]\n\t\t\tif !ok {\n\t\t\t\tmgr.rootLogger.Err(err).Msg(\"unable to initialize metrics exporter: missing node_id\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tmetricsCfg.EncoreCloud.MonitoredResourceLabels[\"node_id\"] = nodeID + \"-\" + containerMetadata.InstanceID\n\t\t\treturn gcp.New(mgr.static.BundledServices, metricsCfg.EncoreCloud, containerMetadata, mgr.rootLogger)\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/gcp/cloud_monitoring.go",
    "content": "//go:build !encore_no_gcp\n\npackage gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"time\"\n\n\tmonitoring \"cloud.google.com/go/monitoring/apiv3/v2\"\n\t\"cloud.google.com/go/monitoring/apiv3/v2/monitoringpb\"\n\t\"github.com/rs/zerolog\"\n\tmetricpb \"google.golang.org/genproto/googleapis/api/metric\"\n\tmonitoredrespb \"google.golang.org/genproto/googleapis/api/monitoredres\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/system\"\n\t\"encore.dev/appruntime/shared/nativehist\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/metrics\"\n)\n\nfunc New(svcs []string, cfg *config.GCPCloudMonitoringProvider, meta *metadata.ContainerMetadata, rootLogger zerolog.Logger) *Exporter {\n\t// Precompute container metadata labels.\n\treturn &Exporter{\n\t\tsvcs:                    svcs,\n\t\tcfg:                     cfg,\n\t\tcontainerMetadataLabels: meta.Labels().AsMap(),\n\t\trootLogger:              rootLogger,\n\n\t\tfirstSeenCounter: make(map[uint64]*timestamppb.Timestamp),\n\n\t\tmetricNames: cfg.MetricNames,\n\t}\n}\n\ntype Exporter struct {\n\tsvcs                    []string\n\tcfg                     *config.GCPCloudMonitoringProvider\n\tcontainerMetadataLabels map[string]string\n\trootLogger              zerolog.Logger\n\n\tclientMu sync.Mutex\n\tclient   *monitoring.MetricClient\n\n\tfirstSeenCounter map[uint64]*timestamppb.Timestamp\n\n\tdummyStart, dummyEnd time.Time\n\n\tmetricNames map[string]string\n}\n\nfunc (x *Exporter) Shutdown(p *shutdown.Process) error {\n\tx.clientMu.Lock()\n\tdefer x.clientMu.Unlock()\n\tif x.client != nil {\n\t\t_ = x.client.Close()\n\t}\n\treturn nil\n}\n\nfunc (x *Exporter) Export(ctx context.Context, collected []metrics.CollectedMetric) error {\n\t// Call time.Now twice so we don't get identical timestamps,\n\t// which is not allowed for cumulative metrics.\n\tnewCounterStart := time.Now().Add(-time.Microsecond)\n\tendTime := time.Now()\n\n\tdata := x.getMetricData(newCounterStart, endTime, collected)\n\tdata = append(data, x.getSysMetrics(endTime)...)\n\tif len(data) == 0 {\n\t\treturn nil\n\t}\n\n\t// Batch the time series into chunks of 200 (GCP's max per API call)\n\tconst maxTimeSeriesPerRequest = 200\n\tclient := x.getClient()\n\n\tfor i := 0; i < len(data); i += maxTimeSeriesPerRequest {\n\t\tend := min(i+maxTimeSeriesPerRequest, len(data))\n\t\tbatch := data[i:end]\n\n\t\terr := client.CreateTimeSeries(ctx, &monitoringpb.CreateTimeSeriesRequest{\n\t\t\tName:       \"projects/\" + x.cfg.ProjectID,\n\t\t\tTimeSeries: batch,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"write metrics to GCP Cloud Monitoring: %v\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (x *Exporter) getMetricData(newCounterStart, endTime time.Time, collected []metrics.CollectedMetric) []*monitoringpb.TimeSeries {\n\tpbNewCounterStart := timestamppb.New(newCounterStart)\n\tpbEndTime := timestamppb.New(endTime)\n\n\tmonitoredResource := &monitoredrespb.MonitoredResource{\n\t\tType:   x.cfg.MonitoredResourceType,\n\t\tLabels: x.cfg.MonitoredResourceLabels,\n\t}\n\n\tdata := make([]*monitoringpb.TimeSeries, 0, len(collected))\n\tfor _, m := range collected {\n\t\tbaseLabels := make(map[string]string, len(x.containerMetadataLabels)+len(m.Labels))\n\t\tfor k, v := range x.containerMetadataLabels {\n\t\t\tbaseLabels[k] = v\n\t\t}\n\t\tfor _, v := range m.Labels {\n\t\t\tbaseLabels[v.Key] = v.Value\n\t\t}\n\n\t\tvar kind metricpb.MetricDescriptor_MetricKind\n\t\tinterval := &monitoringpb.TimeInterval{EndTime: pbEndTime}\n\t\tswitch m.Info.Type() {\n\t\tcase metrics.CounterType:\n\t\t\t// Determine when we first saw this time series.\n\t\t\tstartTime := x.firstSeenCounter[m.TimeSeriesID]\n\t\t\tif startTime == nil {\n\t\t\t\tstartTime = pbNewCounterStart\n\t\t\t\tx.firstSeenCounter[m.TimeSeriesID] = startTime\n\t\t\t}\n\t\t\tinterval.StartTime = startTime\n\n\t\t\tkind = metricpb.MetricDescriptor_CUMULATIVE\n\t\tcase metrics.GaugeType:\n\t\t\tkind = metricpb.MetricDescriptor_GAUGE\n\t\tdefault:\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: unknown metric type %v for metric %s\", m.Info.Type(), m.Info.Name())\n\t\t\tcontinue\n\t\t}\n\n\t\tsvcNum := m.Info.SvcNum()\n\t\tmetricType := \"custom.googleapis.com/\" + m.Info.Name()\n\t\tcloudMetricName, ok := x.metricNames[m.Info.Name()]\n\t\tif !ok {\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: metric %s not found in config\", m.Info.Name())\n\t\t\tcontinue\n\t\t}\n\t\tmetricType = \"custom.googleapis.com/\" + cloudMetricName\n\n\t\tdoAdd := func(val *monitoringpb.TypedValue, svcIdx uint16) {\n\t\t\tlabels := make(map[string]string, len(baseLabels)+1)\n\t\t\tfor k, v := range baseLabels {\n\t\t\t\tlabels[k] = v\n\t\t\t}\n\t\t\tlabels[\"service\"] = x.svcs[svcIdx]\n\n\t\t\tdata = append(data, &monitoringpb.TimeSeries{\n\t\t\t\tMetricKind: kind,\n\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\tType:   metricType,\n\t\t\t\t\tLabels: labels,\n\t\t\t\t},\n\t\t\t\tResource: monitoredResource,\n\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\tInterval: interval,\n\t\t\t\t\tValue:    val,\n\t\t\t\t}},\n\t\t\t})\n\t\t}\n\n\t\tswitch vals := m.Val.(type) {\n\t\tcase []float64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(floatVal(vals[0]), svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(floatVal(val), uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase []int64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(int64Val(vals[0]), svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(int64Val(val), uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase []uint64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(uint64Val(vals[0]), svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(uint64Val(val), uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase []time.Duration:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(floatVal(float64(vals[0]/time.Second)), svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(floatVal(float64(val/time.Second)), uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase []*nativehist.Histogram:\n\t\t\t// TODO implement support\n\n\t\tdefault:\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: unknown value type %T for metric %s\",\n\t\t\t\tm.Val, m.Info.Name())\n\t\t}\n\n\t}\n\n\treturn data\n}\n\nfunc (x *Exporter) getPoint(newCounterStart, endTime *timestamppb.Timestamp, m *metrics.CollectedMetric) (point *monitoringpb.Point, kind metricpb.MetricDescriptor_MetricKind) {\n\tvalue := &monitoringpb.TypedValue{}\n\tswitch v := m.Val.(type) {\n\tcase float64:\n\t\tvalue.Value = &monitoringpb.TypedValue_DoubleValue{DoubleValue: v}\n\tcase int64:\n\t\tvalue.Value = &monitoringpb.TypedValue_Int64Value{Int64Value: v}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled value type %T\", v))\n\t}\n\n\tswitch m.Info.Type() {\n\tcase metrics.CounterType:\n\t\t// Determine when we first saw this time series.\n\t\tstartTime := x.firstSeenCounter[m.TimeSeriesID]\n\t\tif startTime == nil {\n\t\t\tstartTime = newCounterStart\n\t\t\tx.firstSeenCounter[m.TimeSeriesID] = startTime\n\t\t}\n\n\t\tkind = metricpb.MetricDescriptor_CUMULATIVE\n\t\tpoint = &monitoringpb.Point{\n\t\t\tInterval: &monitoringpb.TimeInterval{\n\t\t\t\tStartTime: startTime,\n\t\t\t\tEndTime:   endTime,\n\t\t\t},\n\t\t\tValue: value,\n\t\t}\n\n\tcase metrics.GaugeType:\n\t\tkind = metricpb.MetricDescriptor_GAUGE\n\t\tpoint = &monitoringpb.Point{\n\t\t\tInterval: &monitoringpb.TimeInterval{\n\t\t\t\tEndTime: endTime,\n\t\t\t},\n\t\t\tValue: value,\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled metric type %v\", m.Info.Type()))\n\t}\n\treturn point, kind\n}\n\nfunc floatVal(val float64) *monitoringpb.TypedValue {\n\treturn &monitoringpb.TypedValue{\n\t\tValue: &monitoringpb.TypedValue_DoubleValue{\n\t\t\tDoubleValue: val,\n\t\t},\n\t}\n}\n\nfunc int64Val(val int64) *monitoringpb.TypedValue {\n\treturn &monitoringpb.TypedValue{\n\t\tValue: &monitoringpb.TypedValue_Int64Value{\n\t\t\tInt64Value: val,\n\t\t},\n\t}\n}\nfunc uint64Val(val uint64) *monitoringpb.TypedValue {\n\t// Return a float if this value exceeds the range of int64.\n\tif val > math.MaxInt64 {\n\t\treturn &monitoringpb.TypedValue{\n\t\t\tValue: &monitoringpb.TypedValue_DoubleValue{\n\t\t\t\tDoubleValue: float64(val),\n\t\t\t},\n\t\t}\n\t}\n\treturn &monitoringpb.TypedValue{\n\t\tValue: &monitoringpb.TypedValue_Int64Value{\n\t\t\tInt64Value: int64(val),\n\t\t},\n\t}\n}\n\nfunc (x *Exporter) getSysMetrics(now time.Time) []*monitoringpb.TimeSeries {\n\tvar output []*monitoringpb.TimeSeries\n\tmonitoredResource := &monitoredrespb.MonitoredResource{\n\t\tType:   x.cfg.MonitoredResourceType,\n\t\tLabels: x.cfg.MonitoredResourceLabels,\n\t}\n\tsysMetrics := system.ReadSysMetrics(x.rootLogger)\n\n\tif cloudMetricName, ok := x.metricNames[system.MetricNameHeapObjectsBytes]; !ok {\n\t\tx.rootLogger.Error().Msgf(\"encore: internal error: metric %s not found in config\", system.MetricNameHeapObjectsBytes)\n\t} else {\n\t\toutput = append(output, &monitoringpb.TimeSeries{\n\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\tMetric: &metricpb.Metric{\n\t\t\t\tType:   \"custom.googleapis.com/\" + cloudMetricName,\n\t\t\t\tLabels: x.containerMetadataLabels,\n\t\t\t},\n\t\t\tResource: monitoredResource,\n\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: timestamppb.New(now)},\n\t\t\t\tValue:    uint64Val(sysMetrics[system.MetricNameHeapObjectsBytes]),\n\t\t\t}},\n\t\t})\n\t}\n\n\tif cloudMetricName, ok := x.metricNames[system.MetricNameGoroutines]; !ok {\n\t\tx.rootLogger.Error().Msgf(\"encore: internal error: metric %s not found in config\", system.MetricNameGoroutines)\n\t} else {\n\t\toutput = append(output, &monitoringpb.TimeSeries{\n\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\tMetric: &metricpb.Metric{\n\t\t\t\tType:   \"custom.googleapis.com/\" + cloudMetricName,\n\t\t\t\tLabels: x.containerMetadataLabels,\n\t\t\t},\n\t\t\tResource: monitoredResource,\n\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: timestamppb.New(now)},\n\t\t\t\tValue:    uint64Val(sysMetrics[system.MetricNameGoroutines]),\n\t\t\t}},\n\t\t})\n\t}\n\n\treturn output\n}\n\nfunc (x *Exporter) getClient() *monitoring.MetricClient {\n\tx.clientMu.Lock()\n\tdefer x.clientMu.Unlock()\n\tif x.client == nil {\n\t\tcl, err := monitoring.NewMetricClient(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"failed to create metrics client: %s\", err))\n\t\t}\n\t\tx.client = cl\n\t}\n\treturn x.client\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/gcp/cloud_monitoring_test.go",
    "content": "//go:build !encore_no_gcp\n\npackage gcp\n\nimport (\n\t\"io\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"cloud.google.com/go/monitoring/apiv3/v2/monitoringpb\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/rs/zerolog\"\n\tmetricpb \"google.golang.org/genproto/googleapis/api/metric\"\n\tmonitoredres \"google.golang.org/genproto/googleapis/api/monitoredres\"\n\t\"google.golang.org/protobuf/testing/protocmp\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/metrics\"\n)\n\ntype metricInfo struct {\n\tname   string\n\ttyp    metrics.MetricType\n\tsvcNum uint16\n}\n\nfunc (m metricInfo) Name() string             { return m.name }\nfunc (m metricInfo) Type() metrics.MetricType { return m.typ }\nfunc (m metricInfo) SvcNum() uint16           { return m.svcNum }\n\nfunc TestGetMetricData(t *testing.T) {\n\tnewCounterStart := time.Now()\n\tnow := time.Now()\n\tcfg := &config.GCPCloudMonitoringProvider{\n\t\tProjectID:               \"test-project\",\n\t\tMonitoredResourceType:   \"resource-type\",\n\t\tMonitoredResourceLabels: map[string]string{\"key\": \"value\"},\n\t}\n\n\tmeta := &metadata.ContainerMetadata{\n\t\tServiceID:  \"a_cloudrun_instance\",\n\t\tRevisionID: \"32\",\n\t\tInstanceID: \"cloudrun_asd24dsc2\",\n\t}\n\n\taddContainerLabels := func(labels map[string]string) map[string]string {\n\t\tlabels[\"service_id\"] = meta.ServiceID\n\t\tlabels[\"revision_id\"] = meta.RevisionID\n\t\tlabels[\"instance_id\"] = meta.InstanceID\n\n\t\treturn labels\n\t}\n\n\tmonitoredRes := &monitoredres.MonitoredResource{\n\t\tType:   \"resource-type\",\n\t\tLabels: map[string]string{\"key\": \"value\"},\n\t}\n\tpbStart := timestamppb.New(newCounterStart)\n\tpbEnd := timestamppb.New(now)\n\n\tsvcs := []string{\"foo\", \"bar\"}\n\ttests := []struct {\n\t\tname   string\n\t\tmetric metrics.CollectedMetric\n\t\tdata   []*monitoringpb.TimeSeries\n\t}{\n\t\t{\n\t\t\tname: \"counter\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_counter\", metrics.CounterType, 1},\n\t\t\t\tVal:  []int64{10},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*monitoringpb.TimeSeries{{\n\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\tType:   \"custom.googleapis.com/test_counter\",\n\t\t\t\t\tLabels: addContainerLabels(map[string]string{\"service\": \"foo\"}),\n\t\t\t\t},\n\t\t\t\tResource:   monitoredRes,\n\t\t\t\tMetricKind: metricpb.MetricDescriptor_CUMULATIVE,\n\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\tInterval: &monitoringpb.TimeInterval{StartTime: pbStart, EndTime: pbEnd},\n\t\t\t\t\tValue:    int64Val(10),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"gauge\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_gauge\", metrics.GaugeType, 2},\n\t\t\t\tVal:  []float64{0.5},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*monitoringpb.TimeSeries{{\n\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\tType:   \"custom.googleapis.com/test_gauge\",\n\t\t\t\t\tLabels: addContainerLabels(map[string]string{\"service\": \"bar\"}),\n\t\t\t\t},\n\t\t\t\tResource:   monitoredRes,\n\t\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: pbEnd},\n\t\t\t\t\tValue:    floatVal(0.5),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"labels\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo:   metricInfo{\"test_labels\", metrics.GaugeType, 1},\n\t\t\t\tLabels: []metrics.KeyValue{{\"key\", \"value\"}},\n\t\t\t\tVal:    []uint64{2},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*monitoringpb.TimeSeries{{\n\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\tType:   \"custom.googleapis.com/test_labels\",\n\t\t\t\t\tLabels: addContainerLabels(map[string]string{\"service\": \"foo\", \"key\": \"value\"}),\n\t\t\t\t},\n\t\t\t\tResource:   monitoredRes,\n\t\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: pbEnd},\n\t\t\t\t\tValue:    int64Val(2),\n\t\t\t\t}},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"labels_multi_svcs\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo:   metricInfo{\"test_labels\", metrics.GaugeType, 0},\n\t\t\t\tLabels: []metrics.KeyValue{{\"key\", \"value\"}},\n\t\t\t\tVal:    []time.Duration{2 * time.Second, 4 * time.Second},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 2)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\tvalid[1].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*monitoringpb.TimeSeries{\n\t\t\t\t{\n\t\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\t\tType:   \"custom.googleapis.com/test_labels\",\n\t\t\t\t\t\tLabels: addContainerLabels(map[string]string{\"service\": \"foo\", \"key\": \"value\"}),\n\t\t\t\t\t},\n\t\t\t\t\tResource:   monitoredRes,\n\t\t\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: pbEnd},\n\t\t\t\t\t\tValue:    floatVal(2),\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\t\tType:   \"custom.googleapis.com/test_labels\",\n\t\t\t\t\t\tLabels: addContainerLabels(map[string]string{\"service\": \"bar\", \"key\": \"value\"}),\n\t\t\t\t\t},\n\t\t\t\t\tResource:   monitoredRes,\n\t\t\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: pbEnd},\n\t\t\t\t\t\tValue:    floatVal(4),\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid_counter\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo:  metricInfo{\"test_counter\", metrics.CounterType, 1},\n\t\t\t\tVal:   make([]int64, 1),\n\t\t\t\tValid: make([]atomic.Bool, 1),\n\t\t\t},\n\t\t\tdata: []*monitoringpb.TimeSeries{},\n\t\t},\n\t\t{\n\t\t\tname: \"unset_gauges\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo:   metricInfo{\"test_gauges\", metrics.GaugeType, 0},\n\t\t\t\tLabels: []metrics.KeyValue{{\"key\", \"value\"}},\n\t\t\t\tVal:    []uint64{1, 0},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 2)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\tvalid[1].Store(false)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*monitoringpb.TimeSeries{\n\t\t\t\t{\n\t\t\t\t\tMetric: &metricpb.Metric{\n\t\t\t\t\t\tType:   \"custom.googleapis.com/test_gauges\",\n\t\t\t\t\t\tLabels: addContainerLabels(map[string]string{\"service\": \"foo\", \"key\": \"value\"}),\n\t\t\t\t\t},\n\t\t\t\t\tResource:   monitoredRes,\n\t\t\t\t\tMetricKind: metricpb.MetricDescriptor_GAUGE,\n\t\t\t\t\tPoints: []*monitoringpb.Point{{\n\t\t\t\t\t\tInterval: &monitoringpb.TimeInterval{EndTime: pbEnd},\n\t\t\t\t\t\tValue:    uint64Val(1),\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tcfg.MetricNames = map[string]string{\n\t\t\t\ttest.metric.Info.Name(): test.metric.Info.Name(),\n\t\t\t}\n\t\t\tx := New(svcs, cfg, meta, zerolog.New(io.Discard))\n\t\t\tgot := x.getMetricData(newCounterStart, now, []metrics.CollectedMetric{test.metric})\n\t\t\tif diff := cmp.Diff(got, test.data, protocmp.Transform()); diff != \"\" {\n\t\t\t\tt.Errorf(\"getMetricData() mismatch (-got +want):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/gcp_cloud_monitoring_exporter.go",
    "content": "//go:build !encore_no_gcp\n\npackage metrics\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/gcp\"\n)\n\nfunc init() {\n\tregisterProvider(providerDesc{\n\t\tname: \"gcp_cloud_monitoring\",\n\t\tmatches: func(cfg *config.Metrics) bool {\n\t\t\treturn cfg.CloudMonitoring != nil\n\t\t},\n\t\tnewExporter: func(mgr *Manager) exporter {\n\t\t\tcontainerMetadata, err := metadata.GetContainerMetadata(mgr.runtime)\n\t\t\tif err != nil {\n\t\t\t\tmgr.rootLogger.Err(err).Msg(\"unable to initialize metrics exporter: error getting container metadata\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tmetricsCfg := mgr.runtime.Metrics\n\n\t\t\tnodeID, ok := metricsCfg.CloudMonitoring.MonitoredResourceLabels[\"node_id\"]\n\t\t\tif ok {\n\t\t\t\tmetricsCfg.CloudMonitoring.MonitoredResourceLabels[\"node_id\"] = nodeID + \"-\" + containerMetadata.InstanceID\n\t\t\t}\n\n\t\t\t// Replace $ENV: prefix with the actual environment variable value\n\t\t\tfor k, v := range metricsCfg.CloudMonitoring.MonitoredResourceLabels {\n\t\t\t\tif strings.HasPrefix(v, \"$ENV:\") {\n\t\t\t\t\tmetricsCfg.CloudMonitoring.MonitoredResourceLabels[k] = os.Getenv(v[5:])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn gcp.New(mgr.static.BundledServices, metricsCfg.CloudMonitoring, containerMetadata, mgr.rootLogger)\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/logs_based_exporter.go",
    "content": "package metrics\n\nimport (\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/rlog\"\n)\n\n// AWS CloudWatch logs-based metrics support up to three dimensions:\n//\n// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#logs-metric-filters-dimensions\n//\n// For this reason, we check that the number of tags passed in by the caller\n// isn't greater than three. This doesn't cover the case where the same metric is\n// published with different sets of tags over multiple calls to the functions\n// defined in this file.\n//\n// GCP Cloud Monitoring logs-based metrics support up to ten dimensions (referred\n// to as 'labels'):\n//\n// https://cloud.google.com/logging/docs/logs-based-metrics/labels#limitations\n//\n// However, since logs-based metrics with more than three dimensions aren't\n// supported by AWS CloudWatch, we support only up to three dimensions per metric\n// in GCP too.\n\nconst encoreMetricKey = rlog.InternalKeyPrefix + \"metric_name\"\n\ntype logsBasedEmitter struct {\n\tlogger zerolog.Logger\n}\n\nfunc newLogsBasedEmitter(logger zerolog.Logger) *logsBasedEmitter {\n\treturn &logsBasedEmitter{logger: logger}\n}\n\nfunc (e *logsBasedEmitter) IncCounter(name string, tags ...string) {\n\t// See comment above.\n\tif len(tags) > 6 {\n\t\tpanic(\"emitting metric with more than 3 dimensions is not supported\")\n\t}\n\n\te.logCounter(name, tags...)\n}\n\nfunc (e *logsBasedEmitter) Observe(name string, key string, value float64, tags ...string) {\n\t// See comment above.\n\tif len(tags) > 6 {\n\t\tpanic(\"emitting metric with more than 3 dimensions is not supported\")\n\t}\n\n\te.logValue(name, key, value, tags...)\n}\n\nfunc (e *logsBasedEmitter) logCounter(name string, tags ...string) {\n\tev := e.logger.Trace().Str(encoreMetricKey, name)\n\tev = addTags(ev, tags...)\n\tev.Send()\n}\n\nfunc (e *logsBasedEmitter) logValue(name string, observationKey string, observationValue float64, tags ...string) {\n\tev := e.logger.Trace().Str(encoreMetricKey, name).Float64(observationKey, observationValue)\n\tev = addTags(ev, tags...)\n\tev.Send()\n}\n\nfunc addTags(ev *zerolog.Event, tags ...string) *zerolog.Event {\n\tfor i := 0; i < len(tags); i += 2 {\n\t\tif i+1 < len(tags) {\n\t\t\tev = ev.Str(tags[i], tags[i+1])\n\t\t}\n\t}\n\treturn ev\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/logs_based_exporter_test.go",
    "content": "package metrics\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/rs/zerolog\"\n)\n\nfunc TestLogCounter(t *testing.T) {\n\ttestCases := []struct {\n\t\tName    string\n\t\tCounter string\n\t\tTags    []string\n\t\tWant    string\n\t}{\n\t\t{\n\t\t\tName:    \"Increase counter with one dimension\",\n\t\t\tCounter: \"test_counter\",\n\t\t\tTags:    []string{\"tag\", \"value\"},\n\t\t\tWant: `{\"level\":\"trace\",\"encore_metric_name\":\"test_counter\",\"tag\":\"value\"}\n`,\n\t\t},\n\t\t{\n\t\t\tName:    \"Increase counter with two dimensions\",\n\t\t\tCounter: \"test_counter\",\n\t\t\tTags:    []string{\"tag_1\", \"value_1\", \"tag_2\", \"value_2\"},\n\t\t\tWant: `{\"level\":\"trace\",\"encore_metric_name\":\"test_counter\",\"tag_1\":\"value_1\",\"tag_2\":\"value_2\"}\n`,\n\t\t},\n\t\t{\n\t\t\tName:    \"Increase counter with three dimensions\",\n\t\t\tCounter: \"test_counter\",\n\t\t\tTags:    []string{\"tag_1\", \"value_1\", \"tag_2\", \"value_2\", \"tag_3\", \"value_3\"},\n\t\t\tWant: `{\"level\":\"trace\",\"encore_metric_name\":\"test_counter\",\"tag_1\":\"value_1\",\"tag_2\":\"value_2\",\"tag_3\":\"value_3\"}\n`,\n\t\t},\n\t\t{\n\t\t\tName:    \"Drop tag without value\",\n\t\t\tCounter: \"test_counter\",\n\t\t\tTags:    []string{\"tag_1\", \"value_1\", \"tag_2\"},\n\t\t\tWant: `{\"level\":\"trace\",\"encore_metric_name\":\"test_counter\",\"tag_1\":\"value_1\"}\n`,\n\t\t},\n\t}\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.Name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := zerolog.New(&buf)\n\t\t\tnewLogsBasedEmitter(logger).logCounter(testCase.Counter, testCase.Tags...)\n\t\t\tactual := buf.String()\n\t\t\tif actual != testCase.Want {\n\t\t\t\tt.Fatalf(\"\\nwant:\\n\\t%q\\ngot:\\n\\t%q\\n\", testCase.Want, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/metrics\"\n)\n\ntype Manager struct {\n\tctx    context.Context\n\tcancel func()\n\n\tstatic     *config.Static\n\truntime    *config.Runtime\n\treg        *metrics.Registry\n\trootLogger zerolog.Logger\n\texp        exporter\n\n\tlogsEmitter *logsBasedEmitter\n}\n\nfunc NewManager(reg *metrics.Registry, static *config.Static, rtConf *config.Runtime, rootLogger zerolog.Logger) *Manager {\n\tctx, cancel := context.WithCancel(context.Background())\n\tmgr := &Manager{\n\t\tctx:        ctx,\n\t\tcancel:     cancel,\n\t\treg:        reg,\n\t\tstatic:     static,\n\t\truntime:    rtConf,\n\t\trootLogger: rootLogger,\n\t}\n\n\t// Metrics aren't configured, return.\n\tif rtConf.Metrics == nil {\n\t\treturn mgr\n\t}\n\n\tfor _, desc := range providerRegistry {\n\t\tif desc.matches(rtConf.Metrics) {\n\t\t\tmgr.exp = desc.newExporter(mgr)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif rtConf.Metrics.LogsBased != nil {\n\t\tmgr.logsEmitter = newLogsBasedEmitter(rootLogger)\n\t}\n\n\treturn mgr\n}\n\nfunc (mgr *Manager) Shutdown(p *shutdown.Process) error {\n\t// Wait for all services and all tasks to shut down before we shut down metrics.\n\t<-p.ServicesShutdownCompleted.Done()\n\t<-p.OutstandingTasks.Done()\n\n\tmgr.cancel()\n\n\tif mgr.exp != nil {\n\t\treturn mgr.exp.Shutdown(p)\n\t}\n\treturn nil\n}\n\nfunc (mgr *Manager) BeginCollection() {\n\tif mgr.exp == nil {\n\t\treturn\n\t} else if mgr.runtime.EnvType == \"test\" {\n\t\t// Don't collect metrics when running tests.\n\t\treturn\n\t}\n\n\tinterval := mgr.runtime.Metrics.CollectionInterval\n\tif interval <= 0 {\n\t\tinterval = time.Minute\n\t}\n\ttimeoutDur := interval / 2\n\n\tticker := time.NewTicker(interval)\n\tfor {\n\t\tselect {\n\t\tcase <-mgr.ctx.Done():\n\t\t\tticker.Stop()\n\t\tcase <-ticker.C:\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), timeoutDur)\n\t\t\tmgr.collectNow(ctx)\n\t\t\tcancel()\n\t\t}\n\t}\n}\n\nfunc (mgr *Manager) collectNow(ctx context.Context) {\n\tif mgr.exp == nil {\n\t\treturn\n\t}\n\n\tm := mgr.reg.Collect()\n\tif err := mgr.exp.Export(ctx, m); err != nil {\n\t\tmgr.rootLogger.Error().Err(err).Msg(\"unable to emit metrics\")\n\t} else {\n\t\tmgr.rootLogger.Trace().Int(\"num_metrics\", len(m)).Msg(\"successfully emitted metrics\")\n\t}\n}\n\ntype exporter interface {\n\tExport(context.Context, []metrics.CollectedMetric) error\n\tShutdown(p *shutdown.Process) error\n}\n\ntype providerDesc struct {\n\tname        string\n\tmatches     func(cfg *config.Metrics) bool\n\tnewExporter func(m *Manager) exporter\n}\n\nvar providerRegistry []providerDesc\n\nfunc registerProvider(desc providerDesc) {\n\tproviderRegistry = append(providerRegistry, desc)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/metrics_test.go",
    "content": "package metrics\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/beta/errs\"\n)\n\nfunc TestCode(t *testing.T) {\n\ttestCases := []struct {\n\t\terr        error\n\t\thttpStatus int\n\t\twant       string\n\t}{\n\t\t{\n\t\t\terr:        nil,\n\t\t\thttpStatus: 0,\n\t\t\twant:       \"ok\",\n\t\t},\n\t\t{\n\t\t\terr:  &errs.Error{Code: errs.OK},\n\t\t\twant: errs.OK.String(),\n\t\t},\n\t\t{\n\t\t\terr:  &errs.Error{Code: errs.Internal},\n\t\t\twant: errs.Internal.String(),\n\t\t},\n\t\t{\n\t\t\terr:  errors.New(\"unknown error\"),\n\t\t\twant: errs.Unknown.String(),\n\t\t},\n\t\t{\n\t\t\thttpStatus: http.StatusOK,\n\t\t\twant:       \"ok\",\n\t\t},\n\t\t{\n\t\t\thttpStatus: http.StatusCreated,\n\t\t\twant:       \"http_201\",\n\t\t},\n\t\t{\n\t\t\thttpStatus: http.StatusTeapot,\n\t\t\twant:       \"http_418\",\n\t\t},\n\t}\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.want, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcode := api.Code(testCase.err, testCase.httpStatus)\n\t\t\tif code != testCase.want {\n\t\t\t\tt.Errorf(\"got '%s', want '%s'\", code, testCase.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/metricstest/test_exporter.go",
    "content": "package metricstest\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/rs/zerolog\"\n)\n\n// TestMetricsExporter is meant to be used in tests. It mimics the behavior of\n// other metrics exporters in production in that it panics when the caller passes\n// in a metric with more than three dimensions.\n//\n// Also, it keeps track of which metrics have been exported. This is useful in\n// tests when you want to assert that the code under test has emitted metrics.\ntype TestMetricsExporter struct {\n\tlogger          zerolog.Logger\n\tExportedMetrics []*ExportedMetric\n}\n\ntype MetricType string\n\nconst (\n\tMetricTypeCounter     MetricType = \"counter\"\n\tMetricTypeObservation MetricType = \"observation\"\n)\n\ntype ExportedMetric struct {\n\tmetricType MetricType\n\tname       string\n\tkey        string // Only populated for 'observation' metric types\n\tvalue      float64\n\ttags       map[string]string\n}\n\nfunc NewTestMetricsExporter(logger zerolog.Logger) *TestMetricsExporter {\n\treturn &TestMetricsExporter{logger: logger}\n}\n\nfunc (e *TestMetricsExporter) IncCounter(name string, tags ...string) {\n\t// See comment above.\n\tif len(tags) > 6 {\n\t\tpanic(\"emitting metric with more than 3 dimensions is not supported\")\n\t}\n\n\ttagMap := tagMap(tags...)\n\tmetric := e.find(MetricTypeCounter, name, tagMap)\n\tif metric != nil {\n\t\tmetric.value += 1\n\t\treturn\n\t}\n\n\te.ExportedMetrics = append(e.ExportedMetrics, &ExportedMetric{\n\t\tmetricType: MetricTypeCounter,\n\t\tname:       name,\n\t\tvalue:      1,\n\t\ttags:       tagMap,\n\t})\n}\n\nfunc (e *TestMetricsExporter) Observe(name string, key string, value float64, tags ...string) {\n\t// See comment above.\n\tif len(tags) > 6 {\n\t\tpanic(\"emitting metric with more than 3 dimensions is not supported\")\n\t}\n\n\ttagMap := tagMap(tags...)\n\te.ExportedMetrics = append(e.ExportedMetrics, &ExportedMetric{\n\t\tmetricType: MetricTypeObservation,\n\t\tname:       name,\n\t\tkey:        key,\n\t\tvalue:      value,\n\t\ttags:       tagMap,\n\t})\n}\n\nfunc tagMap(tags ...string) map[string]string {\n\ttagMap := make(map[string]string, len(tags))\n\tfor i := 0; i < len(tags); i += 2 {\n\t\tif i+1 < len(tags) {\n\t\t\ttagMap[tags[i]] = tags[i+1]\n\t\t}\n\t}\n\treturn tagMap\n}\n\nfunc (e *TestMetricsExporter) AssertCounter(t *testing.T, name string, value int, tags map[string]string) {\n\tt.Helper()\n\n\tactual := e.find(MetricTypeCounter, name, tags)\n\tif actual == nil {\n\t\tt.Errorf(\"counter assertion failed: counter metric '%s' with tags %s not emitted\", name, tags)\n\t\treturn\n\t}\n\n\tif int(actual.value) != value {\n\t\tt.Errorf(\"counter assertion failed: expected counter value %d, got %d\", value, int(actual.value))\n\t}\n\treturn\n}\n\nfunc (e *TestMetricsExporter) AssertObservation(\n\tt *testing.T,\n\tname string,\n\tkey string,\n\tassert func(value float64) bool,\n\ttags map[string]string,\n) {\n\tt.Helper()\n\n\tactual := e.find(MetricTypeObservation, name, tags)\n\tif actual == nil {\n\t\tt.Errorf(\"observation assertion failed: observation metric '%s' with tags %s not emitted\", name, tags)\n\t\treturn\n\t}\n\n\tif actual.key != key {\n\t\tt.Errorf(\"observation assertion failed: expected %s, got %s\", key, actual.key)\n\t}\n\n\tif !assert(actual.value) {\n\t\tt.Errorf(\"observation assertion failed: unexpected metric value %f\", actual.value)\n\t}\n\treturn\n}\n\nfunc (e *TestMetricsExporter) find(metricType MetricType, metricName string, tags map[string]string) *ExportedMetric {\n\tfor _, metric := range e.ExportedMetrics {\n\t\tif metric.metricType == metricType &&\n\t\t\tmetric.name == metricName &&\n\t\t\treflect.DeepEqual(metric.tags, tags) {\n\t\t\treturn metric\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/null_exporter.go",
    "content": "package metrics\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/metrics\"\n)\n\ntype NullMetricsExporter struct{}\n\nfunc NewNullMetricsExporter() *NullMetricsExporter {\n\treturn &NullMetricsExporter{}\n}\n\nfunc (e *NullMetricsExporter) Export(ctx context.Context, metrics []metrics.CollectedMetric) error {\n\treturn nil\n}\n\nfunc (e *NullMetricsExporter) Shutdown(p *shutdown.Process) error { return nil }\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/prometheus/prometheus.go",
    "content": "//go:build !encore_no_prometheus\n\npackage prometheus\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/golang/protobuf/proto\"\n\t\"github.com/golang/snappy\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/prometheus/prompb\"\n\t\"encore.dev/appruntime/infrasdk/metrics/system\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/metrics\"\n)\n\nfunc New(svcs []string, cfg *config.PrometheusRemoteWriteProvider, meta *metadata.ContainerMetadata, rootLogger zerolog.Logger) *Exporter {\n\t// Precompute container metadata labels.\n\treturn &Exporter{\n\t\tsvcs: svcs,\n\t\tcfg:  cfg,\n\t\tcontainerMetadataLabels: metadata.MapMetadataLabels(meta, func(k, v string) *prompb.Label {\n\t\t\treturn &prompb.Label{Name: k, Value: v}\n\t\t}),\n\t\trootLogger: rootLogger,\n\t}\n}\n\ntype Exporter struct {\n\tsvcs                    []string\n\tcfg                     *config.PrometheusRemoteWriteProvider\n\tcontainerMetadataLabels []*prompb.Label\n\trootLogger              zerolog.Logger\n}\n\nfunc (x *Exporter) Shutdown(p *shutdown.Process) error { return nil }\n\nfunc (x *Exporter) Export(ctx context.Context, collected []metrics.CollectedMetric) error {\n\tnow := time.Now()\n\tdata := x.getMetricData(now, collected)\n\tdata = append(data, x.getSysMetrics(now)...)\n\tproto, err := proto.Marshal(&prompb.WriteRequest{Timeseries: data})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to marshal metrics into Protobuf: %v\", err)\n\t}\n\n\tencoded := snappy.Encode(nil, proto)\n\tbody := bytes.NewReader(encoded)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, x.cfg.RemoteWriteURL, body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to create HTTP request: %v\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-protobuf\")\n\treq.Header.Set(\"Content-Encoding\", \"snappy\")\n\treq.Header.Set(\"User-Agent\", \"encore\")\n\treq.Header.Set(\"X-Prometheus-Remote-Write-Version\", \"0.1.0\")\n\t_, err = http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to send metrics to Prometheus remote write destination: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (x *Exporter) getMetricData(now time.Time, collected []metrics.CollectedMetric) []*prompb.TimeSeries {\n\tdata := make([]*prompb.TimeSeries, 0, len(collected))\n\n\tdoAdd := func(val float64, metricName string, baseLabels []*prompb.Label, svcIdx uint16) {\n\t\tlabels := make([]*prompb.Label, len(baseLabels)+2)\n\t\tcopy(labels, baseLabels)\n\t\tlabels[len(baseLabels)] = &prompb.Label{Name: \"__name__\", Value: metricName}\n\t\tlabels[len(baseLabels)+1] = &prompb.Label{Name: \"service\", Value: x.svcs[svcIdx]}\n\t\t// Sort labels lexicographically by name, as required by some Prometheus implementations.\n\t\tslices.SortFunc(labels, func(a, b *prompb.Label) int {\n\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t})\n\t\tdata = append(data, &prompb.TimeSeries{\n\t\t\tLabels: labels,\n\t\t\tSamples: []*prompb.Sample{\n\t\t\t\t{\n\t\t\t\t\tValue:     val,\n\t\t\t\t\tTimestamp: FromTime(now),\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\tfor _, m := range collected {\n\t\tlabels := make([]*prompb.Label, len(x.containerMetadataLabels))\n\t\tcopy(labels, x.containerMetadataLabels)\n\t\tfor _, label := range m.Labels {\n\t\t\tlabels = append(labels, &prompb.Label{\n\t\t\t\tName:  label.Key,\n\t\t\t\tValue: label.Value,\n\t\t\t})\n\t\t}\n\n\t\tsvcNum := m.Info.SvcNum()\n\t\tswitch vals := m.Val.(type) {\n\t\tcase []float64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(vals[0], m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(val, m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []int64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]), m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val), m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []uint64:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]), m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val), m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase []time.Duration:\n\t\t\tif svcNum > 0 {\n\t\t\t\tif m.Valid[0].Load() {\n\t\t\t\t\tdoAdd(float64(vals[0]/time.Second), m.Info.Name(), labels, svcNum-1)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, val := range vals {\n\t\t\t\t\tif m.Valid[i].Load() {\n\t\t\t\t\t\tdoAdd(float64(val/time.Second), m.Info.Name(), labels, uint16(i))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\tx.rootLogger.Error().Msgf(\"encore: internal error: unknown value type %T for metric %s\",\n\t\t\t\tm.Val, m.Info.Name())\n\t\t}\n\t}\n\n\treturn data\n}\n\nfunc (x *Exporter) getSysMetrics(now time.Time) []*prompb.TimeSeries {\n\taddMetricNameLabel := func(metricName string) []*prompb.Label {\n\t\tlabels := make([]*prompb.Label, len(x.containerMetadataLabels)+1)\n\t\tcopy(labels, x.containerMetadataLabels)\n\t\tlabels[len(x.containerMetadataLabels)] = &prompb.Label{\n\t\t\tName:  \"__name__\",\n\t\t\tValue: metricName,\n\t\t}\n\t\t// Sort labels lexicographically by name, as required by some Prometheus implementations.\n\t\tslices.SortFunc(labels, func(a, b *prompb.Label) int {\n\t\t\treturn strings.Compare(a.Name, b.Name)\n\t\t})\n\t\treturn labels\n\t}\n\n\tsysMetrics := system.ReadSysMetrics(x.rootLogger)\n\treturn []*prompb.TimeSeries{\n\t\t{\n\t\t\tLabels: addMetricNameLabel(system.MetricNameHeapObjectsBytes),\n\t\t\tSamples: []*prompb.Sample{{\n\t\t\t\tValue:     float64(sysMetrics[system.MetricNameHeapObjectsBytes]),\n\t\t\t\tTimestamp: FromTime(now),\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tLabels: addMetricNameLabel(system.MetricNameGoroutines),\n\t\t\tSamples: []*prompb.Sample{{\n\t\t\t\tValue:     float64(sysMetrics[system.MetricNameGoroutines]),\n\t\t\t\tTimestamp: FromTime(now),\n\t\t\t}},\n\t\t},\n\t}\n}\n\n// FromTime returns a new millisecond timestamp from a time.\nfunc FromTime(t time.Time) int64 {\n\treturn t.Unix()*1000 + int64(t.Nanosecond())/int64(time.Millisecond)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/prometheus/prometheus_test.go",
    "content": "package prometheus\n\nimport (\n\t\"reflect\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/infrasdk/metrics/prometheus/prompb\"\n\t\"encore.dev/metrics\"\n)\n\ntype metricInfo struct {\n\tname   string\n\ttyp    metrics.MetricType\n\tsvcNum uint16\n}\n\nfunc (m metricInfo) Name() string             { return m.name }\nfunc (m metricInfo) Type() metrics.MetricType { return m.typ }\nfunc (m metricInfo) SvcNum() uint16           { return m.svcNum }\n\nfunc TestGetMetricData(t *testing.T) {\n\tnow := time.Now()\n\tsvcs := []string{\"foo\", \"bar\"}\n\ttests := []struct {\n\t\tname   string\n\t\tmetric metrics.CollectedMetric\n\t\tdata   []*prompb.TimeSeries\n\t}{\n\t\t{\n\t\t\tname: \"counter\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_counter\", metrics.CounterType, 1},\n\t\t\t\tVal:  []int64{10},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*prompb.TimeSeries{\n\t\t\t\t{\n\t\t\t\t\tLabels: []*prompb.Label{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"__name__\",\n\t\t\t\t\t\t\tValue: \"test_counter\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"service\",\n\t\t\t\t\t\t\tValue: \"foo\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSamples: []*prompb.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tValue:     10,\n\t\t\t\t\t\t\tTimestamp: FromTime(now),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"gauge\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_gauge\", metrics.GaugeType, 2},\n\t\t\t\tVal:  []float64{0.5},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*prompb.TimeSeries{\n\t\t\t\t{\n\t\t\t\t\tLabels: []*prompb.Label{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"__name__\",\n\t\t\t\t\t\t\tValue: \"test_gauge\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"service\",\n\t\t\t\t\t\t\tValue: \"bar\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSamples: []*prompb.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tValue:     0.5,\n\t\t\t\t\t\t\tTimestamp: FromTime(now),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"labels\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo:   metricInfo{\"test_labels\", metrics.GaugeType, 1},\n\t\t\t\tLabels: []metrics.KeyValue{{\"key\", \"value\"}},\n\t\t\t\tVal:    []float64{-1.5},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 1)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*prompb.TimeSeries{\n\t\t\t\t{\n\t\t\t\t\tLabels: []*prompb.Label{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"__name__\",\n\t\t\t\t\t\t\tValue: \"test_labels\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"key\",\n\t\t\t\t\t\t\tValue: \"value\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"service\",\n\t\t\t\t\t\t\tValue: \"foo\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSamples: []*prompb.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tValue:     -1.5,\n\t\t\t\t\t\t\tTimestamp: FromTime(now),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple services\",\n\t\t\tmetric: metrics.CollectedMetric{\n\t\t\t\tInfo: metricInfo{\"test_counter\", metrics.CounterType, 0},\n\t\t\t\tVal:  []int64{1, 1},\n\t\t\t\tValid: func() []atomic.Bool {\n\t\t\t\t\tvalid := make([]atomic.Bool, 2)\n\t\t\t\t\tvalid[0].Store(true)\n\t\t\t\t\tvalid[1].Store(true)\n\t\t\t\t\treturn valid\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tdata: []*prompb.TimeSeries{\n\t\t\t\t{\n\t\t\t\t\tLabels: []*prompb.Label{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"__name__\",\n\t\t\t\t\t\t\tValue: \"test_counter\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"service\",\n\t\t\t\t\t\t\tValue: \"foo\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSamples: []*prompb.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tValue:     1,\n\t\t\t\t\t\t\tTimestamp: FromTime(now),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tLabels: []*prompb.Label{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"__name__\",\n\t\t\t\t\t\t\tValue: \"test_counter\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"service\",\n\t\t\t\t\t\t\tValue: \"bar\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSamples: []*prompb.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tValue:     1,\n\t\t\t\t\t\t\tTimestamp: FromTime(now),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tx := &Exporter{svcs: svcs}\n\t\t\tgot := x.getMetricData(now, []metrics.CollectedMetric{test.metric})\n\t\t\tif len(got) != len(test.data) {\n\t\t\t\tt.Errorf(\"got %d items, want %d\", len(got), len(test.data))\n\t\t\t} else if !reflect.DeepEqual(got, test.data) {\n\t\t\t\tt.Errorf(\"got %+v, want %+v\", got, test.data)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/remote.pb.go",
    "content": "// Copyright 2016 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: prompb/remote.proto\n\npackage prompb\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype ReadRequest_ResponseType int32\n\nconst (\n\t// Server will return a single ReadResponse message with matched series that\n\t// includes list of raw samples. It's recommended to use streamed response\n\t// types instead.\n\t//\n\t// Response headers:\n\t// Content-Type: \"application/x-protobuf\"\n\t// Content-Encoding: \"snappy\"\n\tReadRequest_SAMPLES ReadRequest_ResponseType = 0\n\t// Server will stream a delimited ChunkedReadResponse message that\n\t// contains XOR or HISTOGRAM(!) encoded chunks for a single series.\n\t// Each message is following varint size and fixed size bigendian\n\t// uint32 for CRC32 Castagnoli checksum.\n\t//\n\t// Response headers:\n\t// Content-Type: \"application/x-streamed-protobuf;\n\t// proto=prometheus.ChunkedReadResponse\" Content-Encoding: \"\"\n\tReadRequest_STREAMED_XOR_CHUNKS ReadRequest_ResponseType = 1\n)\n\n// Enum value maps for ReadRequest_ResponseType.\nvar (\n\tReadRequest_ResponseType_name = map[int32]string{\n\t\t0: \"SAMPLES\",\n\t\t1: \"STREAMED_XOR_CHUNKS\",\n\t}\n\tReadRequest_ResponseType_value = map[string]int32{\n\t\t\"SAMPLES\":             0,\n\t\t\"STREAMED_XOR_CHUNKS\": 1,\n\t}\n)\n\nfunc (x ReadRequest_ResponseType) Enum() *ReadRequest_ResponseType {\n\tp := new(ReadRequest_ResponseType)\n\t*p = x\n\treturn p\n}\n\nfunc (x ReadRequest_ResponseType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (ReadRequest_ResponseType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_prompb_remote_proto_enumTypes[0].Descriptor()\n}\n\nfunc (ReadRequest_ResponseType) Type() protoreflect.EnumType {\n\treturn &file_prompb_remote_proto_enumTypes[0]\n}\n\nfunc (x ReadRequest_ResponseType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use ReadRequest_ResponseType.Descriptor instead.\nfunc (ReadRequest_ResponseType) EnumDescriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{1, 0}\n}\n\ntype WriteRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTimeseries    []*TimeSeries          `protobuf:\"bytes,1,rep,name=timeseries,proto3\" json:\"timeseries,omitempty\"`\n\tMetadata      []*MetricMetadata      `protobuf:\"bytes,3,rep,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *WriteRequest) Reset() {\n\t*x = WriteRequest{}\n\tmi := &file_prompb_remote_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *WriteRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*WriteRequest) ProtoMessage() {}\n\nfunc (x *WriteRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_remote_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead.\nfunc (*WriteRequest) Descriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *WriteRequest) GetTimeseries() []*TimeSeries {\n\tif x != nil {\n\t\treturn x.Timeseries\n\t}\n\treturn nil\n}\n\nfunc (x *WriteRequest) GetMetadata() []*MetricMetadata {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn nil\n}\n\n// ReadRequest represents a remote read request.\ntype ReadRequest struct {\n\tstate   protoimpl.MessageState `protogen:\"open.v1\"`\n\tQueries []*Query               `protobuf:\"bytes,1,rep,name=queries,proto3\" json:\"queries,omitempty\"`\n\t// accepted_response_types allows negotiating the content type of the\n\t// response.\n\t//\n\t// Response types are taken from the list in the FIFO order. If no response\n\t// type in `accepted_response_types` is implemented by server, error is\n\t// returned. For request that do not contain `accepted_response_types` field\n\t// the SAMPLES response type will be used.\n\tAcceptedResponseTypes []ReadRequest_ResponseType `protobuf:\"varint,2,rep,packed,name=accepted_response_types,json=acceptedResponseTypes,proto3,enum=prometheus.ReadRequest_ResponseType\" json:\"accepted_response_types,omitempty\"`\n\tunknownFields         protoimpl.UnknownFields\n\tsizeCache             protoimpl.SizeCache\n}\n\nfunc (x *ReadRequest) Reset() {\n\t*x = ReadRequest{}\n\tmi := &file_prompb_remote_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ReadRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ReadRequest) ProtoMessage() {}\n\nfunc (x *ReadRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_remote_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead.\nfunc (*ReadRequest) Descriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *ReadRequest) GetQueries() []*Query {\n\tif x != nil {\n\t\treturn x.Queries\n\t}\n\treturn nil\n}\n\nfunc (x *ReadRequest) GetAcceptedResponseTypes() []ReadRequest_ResponseType {\n\tif x != nil {\n\t\treturn x.AcceptedResponseTypes\n\t}\n\treturn nil\n}\n\n// ReadResponse is a response when response_type equals SAMPLES.\ntype ReadResponse struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// In same order as the request's queries.\n\tResults       []*QueryResult `protobuf:\"bytes,1,rep,name=results,proto3\" json:\"results,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ReadResponse) Reset() {\n\t*x = ReadResponse{}\n\tmi := &file_prompb_remote_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ReadResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ReadResponse) ProtoMessage() {}\n\nfunc (x *ReadResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_remote_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead.\nfunc (*ReadResponse) Descriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *ReadResponse) GetResults() []*QueryResult {\n\tif x != nil {\n\t\treturn x.Results\n\t}\n\treturn nil\n}\n\ntype Query struct {\n\tstate            protoimpl.MessageState `protogen:\"open.v1\"`\n\tStartTimestampMs int64                  `protobuf:\"varint,1,opt,name=start_timestamp_ms,json=startTimestampMs,proto3\" json:\"start_timestamp_ms,omitempty\"`\n\tEndTimestampMs   int64                  `protobuf:\"varint,2,opt,name=end_timestamp_ms,json=endTimestampMs,proto3\" json:\"end_timestamp_ms,omitempty\"`\n\tMatchers         []*LabelMatcher        `protobuf:\"bytes,3,rep,name=matchers,proto3\" json:\"matchers,omitempty\"`\n\tHints            *ReadHints             `protobuf:\"bytes,4,opt,name=hints,proto3\" json:\"hints,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *Query) Reset() {\n\t*x = Query{}\n\tmi := &file_prompb_remote_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Query) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Query) ProtoMessage() {}\n\nfunc (x *Query) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_remote_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Query.ProtoReflect.Descriptor instead.\nfunc (*Query) Descriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Query) GetStartTimestampMs() int64 {\n\tif x != nil {\n\t\treturn x.StartTimestampMs\n\t}\n\treturn 0\n}\n\nfunc (x *Query) GetEndTimestampMs() int64 {\n\tif x != nil {\n\t\treturn x.EndTimestampMs\n\t}\n\treturn 0\n}\n\nfunc (x *Query) GetMatchers() []*LabelMatcher {\n\tif x != nil {\n\t\treturn x.Matchers\n\t}\n\treturn nil\n}\n\nfunc (x *Query) GetHints() *ReadHints {\n\tif x != nil {\n\t\treturn x.Hints\n\t}\n\treturn nil\n}\n\ntype QueryResult struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Samples within a time series must be ordered by time.\n\tTimeseries    []*TimeSeries `protobuf:\"bytes,1,rep,name=timeseries,proto3\" json:\"timeseries,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *QueryResult) Reset() {\n\t*x = QueryResult{}\n\tmi := &file_prompb_remote_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *QueryResult) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*QueryResult) ProtoMessage() {}\n\nfunc (x *QueryResult) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_remote_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use QueryResult.ProtoReflect.Descriptor instead.\nfunc (*QueryResult) Descriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *QueryResult) GetTimeseries() []*TimeSeries {\n\tif x != nil {\n\t\treturn x.Timeseries\n\t}\n\treturn nil\n}\n\n// ChunkedReadResponse is a response when response_type equals\n// STREAMED_XOR_CHUNKS. We strictly stream full series after series, optionally\n// split by time. This means that a single frame can contain partition of the\n// single series, but once a new series is started to be streamed it means that\n// no more chunks will be sent for previous one. Series are returned sorted in\n// the same way TSDB block are internally.\ntype ChunkedReadResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tChunkedSeries []*ChunkedSeries       `protobuf:\"bytes,1,rep,name=chunked_series,json=chunkedSeries,proto3\" json:\"chunked_series,omitempty\"`\n\t// query_index represents an index of the query from ReadRequest.queries these\n\t// chunks relates to.\n\tQueryIndex    int64 `protobuf:\"varint,2,opt,name=query_index,json=queryIndex,proto3\" json:\"query_index,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ChunkedReadResponse) Reset() {\n\t*x = ChunkedReadResponse{}\n\tmi := &file_prompb_remote_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ChunkedReadResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ChunkedReadResponse) ProtoMessage() {}\n\nfunc (x *ChunkedReadResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_remote_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ChunkedReadResponse.ProtoReflect.Descriptor instead.\nfunc (*ChunkedReadResponse) Descriptor() ([]byte, []int) {\n\treturn file_prompb_remote_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *ChunkedReadResponse) GetChunkedSeries() []*ChunkedSeries {\n\tif x != nil {\n\t\treturn x.ChunkedSeries\n\t}\n\treturn nil\n}\n\nfunc (x *ChunkedReadResponse) GetQueryIndex() int64 {\n\tif x != nil {\n\t\treturn x.QueryIndex\n\t}\n\treturn 0\n}\n\nvar File_prompb_remote_proto protoreflect.FileDescriptor\n\nconst file_prompb_remote_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x13prompb/remote.proto\\x12\\n\" +\n\t\"prometheus\\x1a\\x12prompb/types.proto\\\"\\x84\\x01\\n\" +\n\t\"\\fWriteRequest\\x126\\n\" +\n\t\"\\n\" +\n\t\"timeseries\\x18\\x01 \\x03(\\v2\\x16.prometheus.TimeSeriesR\\n\" +\n\t\"timeseries\\x126\\n\" +\n\t\"\\bmetadata\\x18\\x03 \\x03(\\v2\\x1a.prometheus.MetricMetadataR\\bmetadataJ\\x04\\b\\x02\\x10\\x03\\\"\\xce\\x01\\n\" +\n\t\"\\vReadRequest\\x12+\\n\" +\n\t\"\\aqueries\\x18\\x01 \\x03(\\v2\\x11.prometheus.QueryR\\aqueries\\x12\\\\\\n\" +\n\t\"\\x17accepted_response_types\\x18\\x02 \\x03(\\x0e2$.prometheus.ReadRequest.ResponseTypeR\\x15acceptedResponseTypes\\\"4\\n\" +\n\t\"\\fResponseType\\x12\\v\\n\" +\n\t\"\\aSAMPLES\\x10\\x00\\x12\\x17\\n\" +\n\t\"\\x13STREAMED_XOR_CHUNKS\\x10\\x01\\\"A\\n\" +\n\t\"\\fReadResponse\\x121\\n\" +\n\t\"\\aresults\\x18\\x01 \\x03(\\v2\\x17.prometheus.QueryResultR\\aresults\\\"\\xc2\\x01\\n\" +\n\t\"\\x05Query\\x12,\\n\" +\n\t\"\\x12start_timestamp_ms\\x18\\x01 \\x01(\\x03R\\x10startTimestampMs\\x12(\\n\" +\n\t\"\\x10end_timestamp_ms\\x18\\x02 \\x01(\\x03R\\x0eendTimestampMs\\x124\\n\" +\n\t\"\\bmatchers\\x18\\x03 \\x03(\\v2\\x18.prometheus.LabelMatcherR\\bmatchers\\x12+\\n\" +\n\t\"\\x05hints\\x18\\x04 \\x01(\\v2\\x15.prometheus.ReadHintsR\\x05hints\\\"E\\n\" +\n\t\"\\vQueryResult\\x126\\n\" +\n\t\"\\n\" +\n\t\"timeseries\\x18\\x01 \\x03(\\v2\\x16.prometheus.TimeSeriesR\\n\" +\n\t\"timeseries\\\"x\\n\" +\n\t\"\\x13ChunkedReadResponse\\x12@\\n\" +\n\t\"\\x0echunked_series\\x18\\x01 \\x03(\\v2\\x19.prometheus.ChunkedSeriesR\\rchunkedSeries\\x12\\x1f\\n\" +\n\t\"\\vquery_index\\x18\\x02 \\x01(\\x03R\\n\" +\n\t\"queryIndexB1Z/encore.dev/appruntime/metrics/prometheus/prompbb\\x06proto3\"\n\nvar (\n\tfile_prompb_remote_proto_rawDescOnce sync.Once\n\tfile_prompb_remote_proto_rawDescData []byte\n)\n\nfunc file_prompb_remote_proto_rawDescGZIP() []byte {\n\tfile_prompb_remote_proto_rawDescOnce.Do(func() {\n\t\tfile_prompb_remote_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_prompb_remote_proto_rawDesc), len(file_prompb_remote_proto_rawDesc)))\n\t})\n\treturn file_prompb_remote_proto_rawDescData\n}\n\nvar file_prompb_remote_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_prompb_remote_proto_msgTypes = make([]protoimpl.MessageInfo, 6)\nvar file_prompb_remote_proto_goTypes = []any{\n\t(ReadRequest_ResponseType)(0), // 0: prometheus.ReadRequest.ResponseType\n\t(*WriteRequest)(nil),          // 1: prometheus.WriteRequest\n\t(*ReadRequest)(nil),           // 2: prometheus.ReadRequest\n\t(*ReadResponse)(nil),          // 3: prometheus.ReadResponse\n\t(*Query)(nil),                 // 4: prometheus.Query\n\t(*QueryResult)(nil),           // 5: prometheus.QueryResult\n\t(*ChunkedReadResponse)(nil),   // 6: prometheus.ChunkedReadResponse\n\t(*TimeSeries)(nil),            // 7: prometheus.TimeSeries\n\t(*MetricMetadata)(nil),        // 8: prometheus.MetricMetadata\n\t(*LabelMatcher)(nil),          // 9: prometheus.LabelMatcher\n\t(*ReadHints)(nil),             // 10: prometheus.ReadHints\n\t(*ChunkedSeries)(nil),         // 11: prometheus.ChunkedSeries\n}\nvar file_prompb_remote_proto_depIdxs = []int32{\n\t7,  // 0: prometheus.WriteRequest.timeseries:type_name -> prometheus.TimeSeries\n\t8,  // 1: prometheus.WriteRequest.metadata:type_name -> prometheus.MetricMetadata\n\t4,  // 2: prometheus.ReadRequest.queries:type_name -> prometheus.Query\n\t0,  // 3: prometheus.ReadRequest.accepted_response_types:type_name -> prometheus.ReadRequest.ResponseType\n\t5,  // 4: prometheus.ReadResponse.results:type_name -> prometheus.QueryResult\n\t9,  // 5: prometheus.Query.matchers:type_name -> prometheus.LabelMatcher\n\t10, // 6: prometheus.Query.hints:type_name -> prometheus.ReadHints\n\t7,  // 7: prometheus.QueryResult.timeseries:type_name -> prometheus.TimeSeries\n\t11, // 8: prometheus.ChunkedReadResponse.chunked_series:type_name -> prometheus.ChunkedSeries\n\t9,  // [9:9] is the sub-list for method output_type\n\t9,  // [9:9] is the sub-list for method input_type\n\t9,  // [9:9] is the sub-list for extension type_name\n\t9,  // [9:9] is the sub-list for extension extendee\n\t0,  // [0:9] is the sub-list for field type_name\n}\n\nfunc init() { file_prompb_remote_proto_init() }\nfunc file_prompb_remote_proto_init() {\n\tif File_prompb_remote_proto != nil {\n\t\treturn\n\t}\n\tfile_prompb_types_proto_init()\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_prompb_remote_proto_rawDesc), len(file_prompb_remote_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   6,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_prompb_remote_proto_goTypes,\n\t\tDependencyIndexes: file_prompb_remote_proto_depIdxs,\n\t\tEnumInfos:         file_prompb_remote_proto_enumTypes,\n\t\tMessageInfos:      file_prompb_remote_proto_msgTypes,\n\t}.Build()\n\tFile_prompb_remote_proto = out.File\n\tfile_prompb_remote_proto_goTypes = nil\n\tfile_prompb_remote_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/prometheus/prompb/types.pb.go",
    "content": "// Copyright 2017 Prometheus Team\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.10\n// \tprotoc        v6.32.1\n// source: prompb/types.proto\n\npackage prompb\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype MetricMetadata_MetricType int32\n\nconst (\n\tMetricMetadata_UNKNOWN        MetricMetadata_MetricType = 0\n\tMetricMetadata_COUNTER        MetricMetadata_MetricType = 1\n\tMetricMetadata_GAUGE          MetricMetadata_MetricType = 2\n\tMetricMetadata_HISTOGRAM      MetricMetadata_MetricType = 3\n\tMetricMetadata_GAUGEHISTOGRAM MetricMetadata_MetricType = 4\n\tMetricMetadata_SUMMARY        MetricMetadata_MetricType = 5\n\tMetricMetadata_INFO           MetricMetadata_MetricType = 6\n\tMetricMetadata_STATESET       MetricMetadata_MetricType = 7\n)\n\n// Enum value maps for MetricMetadata_MetricType.\nvar (\n\tMetricMetadata_MetricType_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"COUNTER\",\n\t\t2: \"GAUGE\",\n\t\t3: \"HISTOGRAM\",\n\t\t4: \"GAUGEHISTOGRAM\",\n\t\t5: \"SUMMARY\",\n\t\t6: \"INFO\",\n\t\t7: \"STATESET\",\n\t}\n\tMetricMetadata_MetricType_value = map[string]int32{\n\t\t\"UNKNOWN\":        0,\n\t\t\"COUNTER\":        1,\n\t\t\"GAUGE\":          2,\n\t\t\"HISTOGRAM\":      3,\n\t\t\"GAUGEHISTOGRAM\": 4,\n\t\t\"SUMMARY\":        5,\n\t\t\"INFO\":           6,\n\t\t\"STATESET\":       7,\n\t}\n)\n\nfunc (x MetricMetadata_MetricType) Enum() *MetricMetadata_MetricType {\n\tp := new(MetricMetadata_MetricType)\n\t*p = x\n\treturn p\n}\n\nfunc (x MetricMetadata_MetricType) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (MetricMetadata_MetricType) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_prompb_types_proto_enumTypes[0].Descriptor()\n}\n\nfunc (MetricMetadata_MetricType) Type() protoreflect.EnumType {\n\treturn &file_prompb_types_proto_enumTypes[0]\n}\n\nfunc (x MetricMetadata_MetricType) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use MetricMetadata_MetricType.Descriptor instead.\nfunc (MetricMetadata_MetricType) EnumDescriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{0, 0}\n}\n\ntype Histogram_ResetHint int32\n\nconst (\n\tHistogram_UNKNOWN Histogram_ResetHint = 0 // Need to test for a counter reset explicitly.\n\tHistogram_YES     Histogram_ResetHint = 1 // This is the 1st histogram after a counter reset.\n\tHistogram_NO      Histogram_ResetHint = 2 // There was no counter reset between this and the previous Histogram.\n\tHistogram_GAUGE   Histogram_ResetHint = 3 // This is a gauge histogram where counter resets don't happen.\n)\n\n// Enum value maps for Histogram_ResetHint.\nvar (\n\tHistogram_ResetHint_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"YES\",\n\t\t2: \"NO\",\n\t\t3: \"GAUGE\",\n\t}\n\tHistogram_ResetHint_value = map[string]int32{\n\t\t\"UNKNOWN\": 0,\n\t\t\"YES\":     1,\n\t\t\"NO\":      2,\n\t\t\"GAUGE\":   3,\n\t}\n)\n\nfunc (x Histogram_ResetHint) Enum() *Histogram_ResetHint {\n\tp := new(Histogram_ResetHint)\n\t*p = x\n\treturn p\n}\n\nfunc (x Histogram_ResetHint) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Histogram_ResetHint) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_prompb_types_proto_enumTypes[1].Descriptor()\n}\n\nfunc (Histogram_ResetHint) Type() protoreflect.EnumType {\n\treturn &file_prompb_types_proto_enumTypes[1]\n}\n\nfunc (x Histogram_ResetHint) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Histogram_ResetHint.Descriptor instead.\nfunc (Histogram_ResetHint) EnumDescriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{3, 0}\n}\n\ntype LabelMatcher_Type int32\n\nconst (\n\tLabelMatcher_EQ  LabelMatcher_Type = 0\n\tLabelMatcher_NEQ LabelMatcher_Type = 1\n\tLabelMatcher_RE  LabelMatcher_Type = 2\n\tLabelMatcher_NRE LabelMatcher_Type = 3\n)\n\n// Enum value maps for LabelMatcher_Type.\nvar (\n\tLabelMatcher_Type_name = map[int32]string{\n\t\t0: \"EQ\",\n\t\t1: \"NEQ\",\n\t\t2: \"RE\",\n\t\t3: \"NRE\",\n\t}\n\tLabelMatcher_Type_value = map[string]int32{\n\t\t\"EQ\":  0,\n\t\t\"NEQ\": 1,\n\t\t\"RE\":  2,\n\t\t\"NRE\": 3,\n\t}\n)\n\nfunc (x LabelMatcher_Type) Enum() *LabelMatcher_Type {\n\tp := new(LabelMatcher_Type)\n\t*p = x\n\treturn p\n}\n\nfunc (x LabelMatcher_Type) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (LabelMatcher_Type) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_prompb_types_proto_enumTypes[2].Descriptor()\n}\n\nfunc (LabelMatcher_Type) Type() protoreflect.EnumType {\n\treturn &file_prompb_types_proto_enumTypes[2]\n}\n\nfunc (x LabelMatcher_Type) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use LabelMatcher_Type.Descriptor instead.\nfunc (LabelMatcher_Type) EnumDescriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{8, 0}\n}\n\n// We require this to match chunkenc.Encoding.\ntype Chunk_Encoding int32\n\nconst (\n\tChunk_UNKNOWN   Chunk_Encoding = 0\n\tChunk_XOR       Chunk_Encoding = 1\n\tChunk_HISTOGRAM Chunk_Encoding = 2\n)\n\n// Enum value maps for Chunk_Encoding.\nvar (\n\tChunk_Encoding_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"XOR\",\n\t\t2: \"HISTOGRAM\",\n\t}\n\tChunk_Encoding_value = map[string]int32{\n\t\t\"UNKNOWN\":   0,\n\t\t\"XOR\":       1,\n\t\t\"HISTOGRAM\": 2,\n\t}\n)\n\nfunc (x Chunk_Encoding) Enum() *Chunk_Encoding {\n\tp := new(Chunk_Encoding)\n\t*p = x\n\treturn p\n}\n\nfunc (x Chunk_Encoding) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (Chunk_Encoding) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_prompb_types_proto_enumTypes[3].Descriptor()\n}\n\nfunc (Chunk_Encoding) Type() protoreflect.EnumType {\n\treturn &file_prompb_types_proto_enumTypes[3]\n}\n\nfunc (x Chunk_Encoding) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use Chunk_Encoding.Descriptor instead.\nfunc (Chunk_Encoding) EnumDescriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{10, 0}\n}\n\ntype MetricMetadata struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Represents the metric type, these match the set from Prometheus.\n\t// Refer to model/textparse/interface.go for details.\n\tType             MetricMetadata_MetricType `protobuf:\"varint,1,opt,name=type,proto3,enum=prometheus.MetricMetadata_MetricType\" json:\"type,omitempty\"`\n\tMetricFamilyName string                    `protobuf:\"bytes,2,opt,name=metric_family_name,json=metricFamilyName,proto3\" json:\"metric_family_name,omitempty\"`\n\tHelp             string                    `protobuf:\"bytes,4,opt,name=help,proto3\" json:\"help,omitempty\"`\n\tUnit             string                    `protobuf:\"bytes,5,opt,name=unit,proto3\" json:\"unit,omitempty\"`\n\tunknownFields    protoimpl.UnknownFields\n\tsizeCache        protoimpl.SizeCache\n}\n\nfunc (x *MetricMetadata) Reset() {\n\t*x = MetricMetadata{}\n\tmi := &file_prompb_types_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *MetricMetadata) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*MetricMetadata) ProtoMessage() {}\n\nfunc (x *MetricMetadata) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use MetricMetadata.ProtoReflect.Descriptor instead.\nfunc (*MetricMetadata) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *MetricMetadata) GetType() MetricMetadata_MetricType {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn MetricMetadata_UNKNOWN\n}\n\nfunc (x *MetricMetadata) GetMetricFamilyName() string {\n\tif x != nil {\n\t\treturn x.MetricFamilyName\n\t}\n\treturn \"\"\n}\n\nfunc (x *MetricMetadata) GetHelp() string {\n\tif x != nil {\n\t\treturn x.Help\n\t}\n\treturn \"\"\n}\n\nfunc (x *MetricMetadata) GetUnit() string {\n\tif x != nil {\n\t\treturn x.Unit\n\t}\n\treturn \"\"\n}\n\ntype Sample struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\tValue float64                `protobuf:\"fixed64,1,opt,name=value,proto3\" json:\"value,omitempty\"`\n\t// timestamp is in ms format, see model/timestamp/timestamp.go for\n\t// conversion from time.Time to Prometheus timestamp.\n\tTimestamp     int64 `protobuf:\"varint,2,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Sample) Reset() {\n\t*x = Sample{}\n\tmi := &file_prompb_types_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Sample) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Sample) ProtoMessage() {}\n\nfunc (x *Sample) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Sample.ProtoReflect.Descriptor instead.\nfunc (*Sample) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Sample) GetValue() float64 {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn 0\n}\n\nfunc (x *Sample) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\ntype Exemplar struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Optional, can be empty.\n\tLabels []*Label `protobuf:\"bytes,1,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\tValue  float64  `protobuf:\"fixed64,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\t// timestamp is in ms format, see model/timestamp/timestamp.go for\n\t// conversion from time.Time to Prometheus timestamp.\n\tTimestamp     int64 `protobuf:\"varint,3,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Exemplar) Reset() {\n\t*x = Exemplar{}\n\tmi := &file_prompb_types_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Exemplar) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Exemplar) ProtoMessage() {}\n\nfunc (x *Exemplar) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Exemplar.ProtoReflect.Descriptor instead.\nfunc (*Exemplar) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *Exemplar) GetLabels() []*Label {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *Exemplar) GetValue() float64 {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn 0\n}\n\nfunc (x *Exemplar) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\n// A native histogram, also known as a sparse histogram.\n// Original design doc:\n// https://docs.google.com/document/d/1cLNv3aufPZb3fNfaJgdaRBZsInZKKIHo9E6HinJVbpM/edit\n// The appendix of this design doc also explains the concept of float\n// histograms. This Histogram message can represent both, the usual\n// integer histogram as well as a float histogram.\ntype Histogram struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Types that are valid to be assigned to Count:\n\t//\n\t//\t*Histogram_CountInt\n\t//\t*Histogram_CountFloat\n\tCount isHistogram_Count `protobuf_oneof:\"count\"`\n\tSum   float64           `protobuf:\"fixed64,3,opt,name=sum,proto3\" json:\"sum,omitempty\"` // Sum of observations in the histogram.\n\t// The schema defines the bucket schema. Currently, valid numbers\n\t// are -4 <= n <= 8. They are all for base-2 bucket schemas, where 1\n\t// is a bucket boundary in each case, and then each power of two is\n\t// divided into 2^n logarithmic buckets. Or in other words, each\n\t// bucket boundary is the previous boundary times 2^(2^-n). In the\n\t// future, more bucket schemas may be added using numbers < -4 or >\n\t// 8.\n\tSchema        int32   `protobuf:\"zigzag32,4,opt,name=schema,proto3\" json:\"schema,omitempty\"`\n\tZeroThreshold float64 `protobuf:\"fixed64,5,opt,name=zero_threshold,json=zeroThreshold,proto3\" json:\"zero_threshold,omitempty\"` // Breadth of the zero bucket.\n\t// Types that are valid to be assigned to ZeroCount:\n\t//\n\t//\t*Histogram_ZeroCountInt\n\t//\t*Histogram_ZeroCountFloat\n\tZeroCount isHistogram_ZeroCount `protobuf_oneof:\"zero_count\"`\n\t// Negative Buckets.\n\tNegativeSpans []*BucketSpan `protobuf:\"bytes,8,rep,name=negative_spans,json=negativeSpans,proto3\" json:\"negative_spans,omitempty\"`\n\t// Use either \"negative_deltas\" or \"negative_counts\", the former for\n\t// regular histograms with integer counts, the latter for float\n\t// histograms.\n\tNegativeDeltas []int64   `protobuf:\"zigzag64,9,rep,packed,name=negative_deltas,json=negativeDeltas,proto3\" json:\"negative_deltas,omitempty\"` // Count delta of each bucket compared to previous one (or to zero for 1st bucket).\n\tNegativeCounts []float64 `protobuf:\"fixed64,10,rep,packed,name=negative_counts,json=negativeCounts,proto3\" json:\"negative_counts,omitempty\"` // Absolute count of each bucket.\n\t// Positive Buckets.\n\tPositiveSpans []*BucketSpan `protobuf:\"bytes,11,rep,name=positive_spans,json=positiveSpans,proto3\" json:\"positive_spans,omitempty\"`\n\t// Use either \"positive_deltas\" or \"positive_counts\", the former for\n\t// regular histograms with integer counts, the latter for float\n\t// histograms.\n\tPositiveDeltas []int64             `protobuf:\"zigzag64,12,rep,packed,name=positive_deltas,json=positiveDeltas,proto3\" json:\"positive_deltas,omitempty\"` // Count delta of each bucket compared to previous one (or to zero for 1st bucket).\n\tPositiveCounts []float64           `protobuf:\"fixed64,13,rep,packed,name=positive_counts,json=positiveCounts,proto3\" json:\"positive_counts,omitempty\"`  // Absolute count of each bucket.\n\tResetHint      Histogram_ResetHint `protobuf:\"varint,14,opt,name=reset_hint,json=resetHint,proto3,enum=prometheus.Histogram_ResetHint\" json:\"reset_hint,omitempty\"`\n\t// timestamp is in ms format, see model/timestamp/timestamp.go for\n\t// conversion from time.Time to Prometheus timestamp.\n\tTimestamp     int64 `protobuf:\"varint,15,opt,name=timestamp,proto3\" json:\"timestamp,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Histogram) Reset() {\n\t*x = Histogram{}\n\tmi := &file_prompb_types_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Histogram) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Histogram) ProtoMessage() {}\n\nfunc (x *Histogram) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Histogram.ProtoReflect.Descriptor instead.\nfunc (*Histogram) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Histogram) GetCount() isHistogram_Count {\n\tif x != nil {\n\t\treturn x.Count\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetCountInt() uint64 {\n\tif x != nil {\n\t\tif x, ok := x.Count.(*Histogram_CountInt); ok {\n\t\t\treturn x.CountInt\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetCountFloat() float64 {\n\tif x != nil {\n\t\tif x, ok := x.Count.(*Histogram_CountFloat); ok {\n\t\t\treturn x.CountFloat\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetSum() float64 {\n\tif x != nil {\n\t\treturn x.Sum\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetSchema() int32 {\n\tif x != nil {\n\t\treturn x.Schema\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetZeroThreshold() float64 {\n\tif x != nil {\n\t\treturn x.ZeroThreshold\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetZeroCount() isHistogram_ZeroCount {\n\tif x != nil {\n\t\treturn x.ZeroCount\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetZeroCountInt() uint64 {\n\tif x != nil {\n\t\tif x, ok := x.ZeroCount.(*Histogram_ZeroCountInt); ok {\n\t\t\treturn x.ZeroCountInt\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetZeroCountFloat() float64 {\n\tif x != nil {\n\t\tif x, ok := x.ZeroCount.(*Histogram_ZeroCountFloat); ok {\n\t\t\treturn x.ZeroCountFloat\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (x *Histogram) GetNegativeSpans() []*BucketSpan {\n\tif x != nil {\n\t\treturn x.NegativeSpans\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetNegativeDeltas() []int64 {\n\tif x != nil {\n\t\treturn x.NegativeDeltas\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetNegativeCounts() []float64 {\n\tif x != nil {\n\t\treturn x.NegativeCounts\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetPositiveSpans() []*BucketSpan {\n\tif x != nil {\n\t\treturn x.PositiveSpans\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetPositiveDeltas() []int64 {\n\tif x != nil {\n\t\treturn x.PositiveDeltas\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetPositiveCounts() []float64 {\n\tif x != nil {\n\t\treturn x.PositiveCounts\n\t}\n\treturn nil\n}\n\nfunc (x *Histogram) GetResetHint() Histogram_ResetHint {\n\tif x != nil {\n\t\treturn x.ResetHint\n\t}\n\treturn Histogram_UNKNOWN\n}\n\nfunc (x *Histogram) GetTimestamp() int64 {\n\tif x != nil {\n\t\treturn x.Timestamp\n\t}\n\treturn 0\n}\n\ntype isHistogram_Count interface {\n\tisHistogram_Count()\n}\n\ntype Histogram_CountInt struct {\n\tCountInt uint64 `protobuf:\"varint,1,opt,name=count_int,json=countInt,proto3,oneof\"`\n}\n\ntype Histogram_CountFloat struct {\n\tCountFloat float64 `protobuf:\"fixed64,2,opt,name=count_float,json=countFloat,proto3,oneof\"`\n}\n\nfunc (*Histogram_CountInt) isHistogram_Count() {}\n\nfunc (*Histogram_CountFloat) isHistogram_Count() {}\n\ntype isHistogram_ZeroCount interface {\n\tisHistogram_ZeroCount()\n}\n\ntype Histogram_ZeroCountInt struct {\n\tZeroCountInt uint64 `protobuf:\"varint,6,opt,name=zero_count_int,json=zeroCountInt,proto3,oneof\"`\n}\n\ntype Histogram_ZeroCountFloat struct {\n\tZeroCountFloat float64 `protobuf:\"fixed64,7,opt,name=zero_count_float,json=zeroCountFloat,proto3,oneof\"`\n}\n\nfunc (*Histogram_ZeroCountInt) isHistogram_ZeroCount() {}\n\nfunc (*Histogram_ZeroCountFloat) isHistogram_ZeroCount() {}\n\n// A BucketSpan defines a number of consecutive buckets with their\n// offset. Logically, it would be more straightforward to include the\n// bucket counts in the Span. However, the protobuf representation is\n// more compact in the way the data is structured here (with all the\n// buckets in a single array separate from the Spans).\ntype BucketSpan struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tOffset        int32                  `protobuf:\"zigzag32,1,opt,name=offset,proto3\" json:\"offset,omitempty\"` // Gap to previous span, or starting point for 1st span (which can be negative).\n\tLength        uint32                 `protobuf:\"varint,2,opt,name=length,proto3\" json:\"length,omitempty\"`   // Length of consecutive buckets.\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BucketSpan) Reset() {\n\t*x = BucketSpan{}\n\tmi := &file_prompb_types_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BucketSpan) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BucketSpan) ProtoMessage() {}\n\nfunc (x *BucketSpan) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BucketSpan.ProtoReflect.Descriptor instead.\nfunc (*BucketSpan) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *BucketSpan) GetOffset() int32 {\n\tif x != nil {\n\t\treturn x.Offset\n\t}\n\treturn 0\n}\n\nfunc (x *BucketSpan) GetLength() uint32 {\n\tif x != nil {\n\t\treturn x.Length\n\t}\n\treturn 0\n}\n\n// TimeSeries represents samples and labels for a single time series.\ntype TimeSeries struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// For a timeseries to be valid, and for the samples and exemplars\n\t// to be ingested by the remote system properly, the labels field is required.\n\tLabels        []*Label     `protobuf:\"bytes,1,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\tSamples       []*Sample    `protobuf:\"bytes,2,rep,name=samples,proto3\" json:\"samples,omitempty\"`\n\tExemplars     []*Exemplar  `protobuf:\"bytes,3,rep,name=exemplars,proto3\" json:\"exemplars,omitempty\"`\n\tHistograms    []*Histogram `protobuf:\"bytes,4,rep,name=histograms,proto3\" json:\"histograms,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TimeSeries) Reset() {\n\t*x = TimeSeries{}\n\tmi := &file_prompb_types_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TimeSeries) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TimeSeries) ProtoMessage() {}\n\nfunc (x *TimeSeries) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TimeSeries.ProtoReflect.Descriptor instead.\nfunc (*TimeSeries) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *TimeSeries) GetLabels() []*Label {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *TimeSeries) GetSamples() []*Sample {\n\tif x != nil {\n\t\treturn x.Samples\n\t}\n\treturn nil\n}\n\nfunc (x *TimeSeries) GetExemplars() []*Exemplar {\n\tif x != nil {\n\t\treturn x.Exemplars\n\t}\n\treturn nil\n}\n\nfunc (x *TimeSeries) GetHistograms() []*Histogram {\n\tif x != nil {\n\t\treturn x.Histograms\n\t}\n\treturn nil\n}\n\ntype Label struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tName          string                 `protobuf:\"bytes,1,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,2,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Label) Reset() {\n\t*x = Label{}\n\tmi := &file_prompb_types_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Label) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Label) ProtoMessage() {}\n\nfunc (x *Label) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Label.ProtoReflect.Descriptor instead.\nfunc (*Label) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *Label) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *Label) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype Labels struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tLabels        []*Label               `protobuf:\"bytes,1,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Labels) Reset() {\n\t*x = Labels{}\n\tmi := &file_prompb_types_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Labels) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Labels) ProtoMessage() {}\n\nfunc (x *Labels) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Labels.ProtoReflect.Descriptor instead.\nfunc (*Labels) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *Labels) GetLabels() []*Label {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\n// Matcher specifies a rule, which can match or set of labels or not.\ntype LabelMatcher struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tType          LabelMatcher_Type      `protobuf:\"varint,1,opt,name=type,proto3,enum=prometheus.LabelMatcher_Type\" json:\"type,omitempty\"`\n\tName          string                 `protobuf:\"bytes,2,opt,name=name,proto3\" json:\"name,omitempty\"`\n\tValue         string                 `protobuf:\"bytes,3,opt,name=value,proto3\" json:\"value,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *LabelMatcher) Reset() {\n\t*x = LabelMatcher{}\n\tmi := &file_prompb_types_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *LabelMatcher) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*LabelMatcher) ProtoMessage() {}\n\nfunc (x *LabelMatcher) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use LabelMatcher.ProtoReflect.Descriptor instead.\nfunc (*LabelMatcher) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *LabelMatcher) GetType() LabelMatcher_Type {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn LabelMatcher_EQ\n}\n\nfunc (x *LabelMatcher) GetName() string {\n\tif x != nil {\n\t\treturn x.Name\n\t}\n\treturn \"\"\n}\n\nfunc (x *LabelMatcher) GetValue() string {\n\tif x != nil {\n\t\treturn x.Value\n\t}\n\treturn \"\"\n}\n\ntype ReadHints struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tStepMs        int64                  `protobuf:\"varint,1,opt,name=step_ms,json=stepMs,proto3\" json:\"step_ms,omitempty\"`    // Query step size in milliseconds.\n\tFunc          string                 `protobuf:\"bytes,2,opt,name=func,proto3\" json:\"func,omitempty\"`                       // String representation of surrounding function or aggregation.\n\tStartMs       int64                  `protobuf:\"varint,3,opt,name=start_ms,json=startMs,proto3\" json:\"start_ms,omitempty\"` // Start time in milliseconds.\n\tEndMs         int64                  `protobuf:\"varint,4,opt,name=end_ms,json=endMs,proto3\" json:\"end_ms,omitempty\"`       // End time in milliseconds.\n\tGrouping      []string               `protobuf:\"bytes,5,rep,name=grouping,proto3\" json:\"grouping,omitempty\"`               // List of label names used in aggregation.\n\tBy            bool                   `protobuf:\"varint,6,opt,name=by,proto3\" json:\"by,omitempty\"`                          // Indicate whether it is without or by.\n\tRangeMs       int64                  `protobuf:\"varint,7,opt,name=range_ms,json=rangeMs,proto3\" json:\"range_ms,omitempty\"` // Range vector selector range in milliseconds.\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ReadHints) Reset() {\n\t*x = ReadHints{}\n\tmi := &file_prompb_types_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ReadHints) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ReadHints) ProtoMessage() {}\n\nfunc (x *ReadHints) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ReadHints.ProtoReflect.Descriptor instead.\nfunc (*ReadHints) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *ReadHints) GetStepMs() int64 {\n\tif x != nil {\n\t\treturn x.StepMs\n\t}\n\treturn 0\n}\n\nfunc (x *ReadHints) GetFunc() string {\n\tif x != nil {\n\t\treturn x.Func\n\t}\n\treturn \"\"\n}\n\nfunc (x *ReadHints) GetStartMs() int64 {\n\tif x != nil {\n\t\treturn x.StartMs\n\t}\n\treturn 0\n}\n\nfunc (x *ReadHints) GetEndMs() int64 {\n\tif x != nil {\n\t\treturn x.EndMs\n\t}\n\treturn 0\n}\n\nfunc (x *ReadHints) GetGrouping() []string {\n\tif x != nil {\n\t\treturn x.Grouping\n\t}\n\treturn nil\n}\n\nfunc (x *ReadHints) GetBy() bool {\n\tif x != nil {\n\t\treturn x.By\n\t}\n\treturn false\n}\n\nfunc (x *ReadHints) GetRangeMs() int64 {\n\tif x != nil {\n\t\treturn x.RangeMs\n\t}\n\treturn 0\n}\n\n// Chunk represents a TSDB chunk.\n// Time range [min, max] is inclusive.\ntype Chunk struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMinTimeMs     int64                  `protobuf:\"varint,1,opt,name=min_time_ms,json=minTimeMs,proto3\" json:\"min_time_ms,omitempty\"`\n\tMaxTimeMs     int64                  `protobuf:\"varint,2,opt,name=max_time_ms,json=maxTimeMs,proto3\" json:\"max_time_ms,omitempty\"`\n\tType          Chunk_Encoding         `protobuf:\"varint,3,opt,name=type,proto3,enum=prometheus.Chunk_Encoding\" json:\"type,omitempty\"`\n\tData          []byte                 `protobuf:\"bytes,4,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Chunk) Reset() {\n\t*x = Chunk{}\n\tmi := &file_prompb_types_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Chunk) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Chunk) ProtoMessage() {}\n\nfunc (x *Chunk) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Chunk.ProtoReflect.Descriptor instead.\nfunc (*Chunk) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *Chunk) GetMinTimeMs() int64 {\n\tif x != nil {\n\t\treturn x.MinTimeMs\n\t}\n\treturn 0\n}\n\nfunc (x *Chunk) GetMaxTimeMs() int64 {\n\tif x != nil {\n\t\treturn x.MaxTimeMs\n\t}\n\treturn 0\n}\n\nfunc (x *Chunk) GetType() Chunk_Encoding {\n\tif x != nil {\n\t\treturn x.Type\n\t}\n\treturn Chunk_UNKNOWN\n}\n\nfunc (x *Chunk) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\n// ChunkedSeries represents single, encoded time series.\ntype ChunkedSeries struct {\n\tstate protoimpl.MessageState `protogen:\"open.v1\"`\n\t// Labels should be sorted.\n\tLabels []*Label `protobuf:\"bytes,1,rep,name=labels,proto3\" json:\"labels,omitempty\"`\n\t// Chunks will be in start time order and may overlap.\n\tChunks        []*Chunk `protobuf:\"bytes,2,rep,name=chunks,proto3\" json:\"chunks,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ChunkedSeries) Reset() {\n\t*x = ChunkedSeries{}\n\tmi := &file_prompb_types_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ChunkedSeries) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ChunkedSeries) ProtoMessage() {}\n\nfunc (x *ChunkedSeries) ProtoReflect() protoreflect.Message {\n\tmi := &file_prompb_types_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ChunkedSeries.ProtoReflect.Descriptor instead.\nfunc (*ChunkedSeries) Descriptor() ([]byte, []int) {\n\treturn file_prompb_types_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *ChunkedSeries) GetLabels() []*Label {\n\tif x != nil {\n\t\treturn x.Labels\n\t}\n\treturn nil\n}\n\nfunc (x *ChunkedSeries) GetChunks() []*Chunk {\n\tif x != nil {\n\t\treturn x.Chunks\n\t}\n\treturn nil\n}\n\nvar File_prompb_types_proto protoreflect.FileDescriptor\n\nconst file_prompb_types_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x12prompb/types.proto\\x12\\n\" +\n\t\"prometheus\\\"\\x9c\\x02\\n\" +\n\t\"\\x0eMetricMetadata\\x129\\n\" +\n\t\"\\x04type\\x18\\x01 \\x01(\\x0e2%.prometheus.MetricMetadata.MetricTypeR\\x04type\\x12,\\n\" +\n\t\"\\x12metric_family_name\\x18\\x02 \\x01(\\tR\\x10metricFamilyName\\x12\\x12\\n\" +\n\t\"\\x04help\\x18\\x04 \\x01(\\tR\\x04help\\x12\\x12\\n\" +\n\t\"\\x04unit\\x18\\x05 \\x01(\\tR\\x04unit\\\"y\\n\" +\n\t\"\\n\" +\n\t\"MetricType\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aCOUNTER\\x10\\x01\\x12\\t\\n\" +\n\t\"\\x05GAUGE\\x10\\x02\\x12\\r\\n\" +\n\t\"\\tHISTOGRAM\\x10\\x03\\x12\\x12\\n\" +\n\t\"\\x0eGAUGEHISTOGRAM\\x10\\x04\\x12\\v\\n\" +\n\t\"\\aSUMMARY\\x10\\x05\\x12\\b\\n\" +\n\t\"\\x04INFO\\x10\\x06\\x12\\f\\n\" +\n\t\"\\bSTATESET\\x10\\a\\\"<\\n\" +\n\t\"\\x06Sample\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x01 \\x01(\\x01R\\x05value\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x02 \\x01(\\x03R\\ttimestamp\\\"i\\n\" +\n\t\"\\bExemplar\\x12)\\n\" +\n\t\"\\x06labels\\x18\\x01 \\x03(\\v2\\x11.prometheus.LabelR\\x06labels\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\x01R\\x05value\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x03 \\x01(\\x03R\\ttimestamp\\\"\\xbf\\x05\\n\" +\n\t\"\\tHistogram\\x12\\x1d\\n\" +\n\t\"\\tcount_int\\x18\\x01 \\x01(\\x04H\\x00R\\bcountInt\\x12!\\n\" +\n\t\"\\vcount_float\\x18\\x02 \\x01(\\x01H\\x00R\\n\" +\n\t\"countFloat\\x12\\x10\\n\" +\n\t\"\\x03sum\\x18\\x03 \\x01(\\x01R\\x03sum\\x12\\x16\\n\" +\n\t\"\\x06schema\\x18\\x04 \\x01(\\x11R\\x06schema\\x12%\\n\" +\n\t\"\\x0ezero_threshold\\x18\\x05 \\x01(\\x01R\\rzeroThreshold\\x12&\\n\" +\n\t\"\\x0ezero_count_int\\x18\\x06 \\x01(\\x04H\\x01R\\fzeroCountInt\\x12*\\n\" +\n\t\"\\x10zero_count_float\\x18\\a \\x01(\\x01H\\x01R\\x0ezeroCountFloat\\x12=\\n\" +\n\t\"\\x0enegative_spans\\x18\\b \\x03(\\v2\\x16.prometheus.BucketSpanR\\rnegativeSpans\\x12'\\n\" +\n\t\"\\x0fnegative_deltas\\x18\\t \\x03(\\x12R\\x0enegativeDeltas\\x12'\\n\" +\n\t\"\\x0fnegative_counts\\x18\\n\" +\n\t\" \\x03(\\x01R\\x0enegativeCounts\\x12=\\n\" +\n\t\"\\x0epositive_spans\\x18\\v \\x03(\\v2\\x16.prometheus.BucketSpanR\\rpositiveSpans\\x12'\\n\" +\n\t\"\\x0fpositive_deltas\\x18\\f \\x03(\\x12R\\x0epositiveDeltas\\x12'\\n\" +\n\t\"\\x0fpositive_counts\\x18\\r \\x03(\\x01R\\x0epositiveCounts\\x12>\\n\" +\n\t\"\\n\" +\n\t\"reset_hint\\x18\\x0e \\x01(\\x0e2\\x1f.prometheus.Histogram.ResetHintR\\tresetHint\\x12\\x1c\\n\" +\n\t\"\\ttimestamp\\x18\\x0f \\x01(\\x03R\\ttimestamp\\\"4\\n\" +\n\t\"\\tResetHint\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\a\\n\" +\n\t\"\\x03YES\\x10\\x01\\x12\\x06\\n\" +\n\t\"\\x02NO\\x10\\x02\\x12\\t\\n\" +\n\t\"\\x05GAUGE\\x10\\x03B\\a\\n\" +\n\t\"\\x05countB\\f\\n\" +\n\t\"\\n\" +\n\t\"zero_count\\\"<\\n\" +\n\t\"\\n\" +\n\t\"BucketSpan\\x12\\x16\\n\" +\n\t\"\\x06offset\\x18\\x01 \\x01(\\x11R\\x06offset\\x12\\x16\\n\" +\n\t\"\\x06length\\x18\\x02 \\x01(\\rR\\x06length\\\"\\xd0\\x01\\n\" +\n\t\"\\n\" +\n\t\"TimeSeries\\x12)\\n\" +\n\t\"\\x06labels\\x18\\x01 \\x03(\\v2\\x11.prometheus.LabelR\\x06labels\\x12,\\n\" +\n\t\"\\asamples\\x18\\x02 \\x03(\\v2\\x12.prometheus.SampleR\\asamples\\x122\\n\" +\n\t\"\\texemplars\\x18\\x03 \\x03(\\v2\\x14.prometheus.ExemplarR\\texemplars\\x125\\n\" +\n\t\"\\n\" +\n\t\"histograms\\x18\\x04 \\x03(\\v2\\x15.prometheus.HistogramR\\n\" +\n\t\"histograms\\\"1\\n\" +\n\t\"\\x05Label\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x01 \\x01(\\tR\\x04name\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value\\\"3\\n\" +\n\t\"\\x06Labels\\x12)\\n\" +\n\t\"\\x06labels\\x18\\x01 \\x03(\\v2\\x11.prometheus.LabelR\\x06labels\\\"\\x95\\x01\\n\" +\n\t\"\\fLabelMatcher\\x121\\n\" +\n\t\"\\x04type\\x18\\x01 \\x01(\\x0e2\\x1d.prometheus.LabelMatcher.TypeR\\x04type\\x12\\x12\\n\" +\n\t\"\\x04name\\x18\\x02 \\x01(\\tR\\x04name\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x03 \\x01(\\tR\\x05value\\\"(\\n\" +\n\t\"\\x04Type\\x12\\x06\\n\" +\n\t\"\\x02EQ\\x10\\x00\\x12\\a\\n\" +\n\t\"\\x03NEQ\\x10\\x01\\x12\\x06\\n\" +\n\t\"\\x02RE\\x10\\x02\\x12\\a\\n\" +\n\t\"\\x03NRE\\x10\\x03\\\"\\xb1\\x01\\n\" +\n\t\"\\tReadHints\\x12\\x17\\n\" +\n\t\"\\astep_ms\\x18\\x01 \\x01(\\x03R\\x06stepMs\\x12\\x12\\n\" +\n\t\"\\x04func\\x18\\x02 \\x01(\\tR\\x04func\\x12\\x19\\n\" +\n\t\"\\bstart_ms\\x18\\x03 \\x01(\\x03R\\astartMs\\x12\\x15\\n\" +\n\t\"\\x06end_ms\\x18\\x04 \\x01(\\x03R\\x05endMs\\x12\\x1a\\n\" +\n\t\"\\bgrouping\\x18\\x05 \\x03(\\tR\\bgrouping\\x12\\x0e\\n\" +\n\t\"\\x02by\\x18\\x06 \\x01(\\bR\\x02by\\x12\\x19\\n\" +\n\t\"\\brange_ms\\x18\\a \\x01(\\x03R\\arangeMs\\\"\\xbc\\x01\\n\" +\n\t\"\\x05Chunk\\x12\\x1e\\n\" +\n\t\"\\vmin_time_ms\\x18\\x01 \\x01(\\x03R\\tminTimeMs\\x12\\x1e\\n\" +\n\t\"\\vmax_time_ms\\x18\\x02 \\x01(\\x03R\\tmaxTimeMs\\x12.\\n\" +\n\t\"\\x04type\\x18\\x03 \\x01(\\x0e2\\x1a.prometheus.Chunk.EncodingR\\x04type\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x04 \\x01(\\fR\\x04data\\\"/\\n\" +\n\t\"\\bEncoding\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\a\\n\" +\n\t\"\\x03XOR\\x10\\x01\\x12\\r\\n\" +\n\t\"\\tHISTOGRAM\\x10\\x02\\\"e\\n\" +\n\t\"\\rChunkedSeries\\x12)\\n\" +\n\t\"\\x06labels\\x18\\x01 \\x03(\\v2\\x11.prometheus.LabelR\\x06labels\\x12)\\n\" +\n\t\"\\x06chunks\\x18\\x02 \\x03(\\v2\\x11.prometheus.ChunkR\\x06chunksB1Z/encore.dev/appruntime/metrics/prometheus/prompbb\\x06proto3\"\n\nvar (\n\tfile_prompb_types_proto_rawDescOnce sync.Once\n\tfile_prompb_types_proto_rawDescData []byte\n)\n\nfunc file_prompb_types_proto_rawDescGZIP() []byte {\n\tfile_prompb_types_proto_rawDescOnce.Do(func() {\n\t\tfile_prompb_types_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_prompb_types_proto_rawDesc), len(file_prompb_types_proto_rawDesc)))\n\t})\n\treturn file_prompb_types_proto_rawDescData\n}\n\nvar file_prompb_types_proto_enumTypes = make([]protoimpl.EnumInfo, 4)\nvar file_prompb_types_proto_msgTypes = make([]protoimpl.MessageInfo, 12)\nvar file_prompb_types_proto_goTypes = []any{\n\t(MetricMetadata_MetricType)(0), // 0: prometheus.MetricMetadata.MetricType\n\t(Histogram_ResetHint)(0),       // 1: prometheus.Histogram.ResetHint\n\t(LabelMatcher_Type)(0),         // 2: prometheus.LabelMatcher.Type\n\t(Chunk_Encoding)(0),            // 3: prometheus.Chunk.Encoding\n\t(*MetricMetadata)(nil),         // 4: prometheus.MetricMetadata\n\t(*Sample)(nil),                 // 5: prometheus.Sample\n\t(*Exemplar)(nil),               // 6: prometheus.Exemplar\n\t(*Histogram)(nil),              // 7: prometheus.Histogram\n\t(*BucketSpan)(nil),             // 8: prometheus.BucketSpan\n\t(*TimeSeries)(nil),             // 9: prometheus.TimeSeries\n\t(*Label)(nil),                  // 10: prometheus.Label\n\t(*Labels)(nil),                 // 11: prometheus.Labels\n\t(*LabelMatcher)(nil),           // 12: prometheus.LabelMatcher\n\t(*ReadHints)(nil),              // 13: prometheus.ReadHints\n\t(*Chunk)(nil),                  // 14: prometheus.Chunk\n\t(*ChunkedSeries)(nil),          // 15: prometheus.ChunkedSeries\n}\nvar file_prompb_types_proto_depIdxs = []int32{\n\t0,  // 0: prometheus.MetricMetadata.type:type_name -> prometheus.MetricMetadata.MetricType\n\t10, // 1: prometheus.Exemplar.labels:type_name -> prometheus.Label\n\t8,  // 2: prometheus.Histogram.negative_spans:type_name -> prometheus.BucketSpan\n\t8,  // 3: prometheus.Histogram.positive_spans:type_name -> prometheus.BucketSpan\n\t1,  // 4: prometheus.Histogram.reset_hint:type_name -> prometheus.Histogram.ResetHint\n\t10, // 5: prometheus.TimeSeries.labels:type_name -> prometheus.Label\n\t5,  // 6: prometheus.TimeSeries.samples:type_name -> prometheus.Sample\n\t6,  // 7: prometheus.TimeSeries.exemplars:type_name -> prometheus.Exemplar\n\t7,  // 8: prometheus.TimeSeries.histograms:type_name -> prometheus.Histogram\n\t10, // 9: prometheus.Labels.labels:type_name -> prometheus.Label\n\t2,  // 10: prometheus.LabelMatcher.type:type_name -> prometheus.LabelMatcher.Type\n\t3,  // 11: prometheus.Chunk.type:type_name -> prometheus.Chunk.Encoding\n\t10, // 12: prometheus.ChunkedSeries.labels:type_name -> prometheus.Label\n\t14, // 13: prometheus.ChunkedSeries.chunks:type_name -> prometheus.Chunk\n\t14, // [14:14] is the sub-list for method output_type\n\t14, // [14:14] is the sub-list for method input_type\n\t14, // [14:14] is the sub-list for extension type_name\n\t14, // [14:14] is the sub-list for extension extendee\n\t0,  // [0:14] is the sub-list for field type_name\n}\n\nfunc init() { file_prompb_types_proto_init() }\nfunc file_prompb_types_proto_init() {\n\tif File_prompb_types_proto != nil {\n\t\treturn\n\t}\n\tfile_prompb_types_proto_msgTypes[3].OneofWrappers = []any{\n\t\t(*Histogram_CountInt)(nil),\n\t\t(*Histogram_CountFloat)(nil),\n\t\t(*Histogram_ZeroCountInt)(nil),\n\t\t(*Histogram_ZeroCountFloat)(nil),\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_prompb_types_proto_rawDesc), len(file_prompb_types_proto_rawDesc)),\n\t\t\tNumEnums:      4,\n\t\t\tNumMessages:   12,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   0,\n\t\t},\n\t\tGoTypes:           file_prompb_types_proto_goTypes,\n\t\tDependencyIndexes: file_prompb_types_proto_depIdxs,\n\t\tEnumInfos:         file_prompb_types_proto_enumTypes,\n\t\tMessageInfos:      file_prompb_types_proto_msgTypes,\n\t}.Build()\n\tFile_prompb_types_proto = out.File\n\tfile_prompb_types_proto_goTypes = nil\n\tfile_prompb_types_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/prometheus_exporter.go",
    "content": "//go:build !encore_no_prometheus\n\npackage metrics\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/infrasdk/metadata\"\n\t\"encore.dev/appruntime/infrasdk/metrics/prometheus\"\n)\n\nfunc init() {\n\tregisterProvider(providerDesc{\n\t\tname: \"prometheus\",\n\t\tmatches: func(cfg *config.Metrics) bool {\n\t\t\treturn cfg.Prometheus != nil\n\t\t},\n\t\tnewExporter: func(m *Manager) exporter {\n\t\t\tcontainerMetadata, err := metadata.GetContainerMetadata(m.runtime)\n\t\t\tif err != nil {\n\t\t\t\tm.rootLogger.Err(err).Msg(\"unable to initialize metrics exporter: error getting container metadata\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn prometheus.New(m.static.BundledServices, m.runtime.Metrics.Prometheus, containerMetadata, m.rootLogger)\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/system/system.go",
    "content": "package system\n\nimport (\n\t\"runtime/metrics\"\n\n\t\"github.com/rs/zerolog\"\n)\n\n// These are the metrics exposed by Go we currently track. Once we update our\n// fork to Go 1.20, we should add:\n//\n// - /cpu/classes/gc/pause:cpu-seconds\n// - /cpu/classes/idle:cpu-seconds\n// - /cpu/classes/total:cpu-seconds\n// - /sync/mutex/wait/total:seconds\nconst (\n\tMetricNameHeapObjectsBytes = \"e_sys_memory_heap_objects_bytes\"\n\tMetricNameGoroutines       = \"e_sys_sched_goroutines\"\n\n\tgoMetricHeapObjectsBytes = \"/memory/classes/heap/objects:bytes\"\n\tgoMetricGoroutines       = \"/sched/goroutines:goroutines\"\n)\n\nvar encoreMetricNames = map[string]string{\n\tgoMetricHeapObjectsBytes: MetricNameHeapObjectsBytes,\n\tgoMetricGoroutines:       MetricNameGoroutines,\n}\n\nfunc ReadSysMetrics(logger zerolog.Logger) map[string]uint64 {\n\tsamples := []metrics.Sample{\n\t\t{Name: goMetricHeapObjectsBytes},\n\t\t{Name: goMetricGoroutines},\n\t}\n\tmetrics.Read(samples)\n\n\toutput := make(map[string]uint64, len(samples))\n\tfor _, sample := range samples {\n\t\tswitch sample.Value.Kind() {\n\t\tcase metrics.KindUint64:\n\t\t\toutput[encoreMetricNames[sample.Name]] = sample.Value.Uint64()\n\t\tcase metrics.KindBad:\n\t\t\t// This means the metric is unsupported. It's expected to happen very rarely\n\t\t\t// possibly due to a large change in a particular Go implementation.\n\t\t\tlogger.Warn().Str(\"metric\", sample.Name).Msg(\"metric no longer supported\")\n\t\tdefault:\n\t\t\tlogger.Warn().Str(\"metric\", sample.Name).Msg(\"unexpected metric kind\")\n\t\t}\n\t}\n\treturn output\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/metrics/zzz_singleton_internal.go",
    "content": "//go:build encore_app\n\npackage metrics\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\tusermetrics \"encore.dev/metrics\"\n)\n\n// This file is named \"zzz_singleton_internal.go\" so that it is the last file\n// in the package, to ensure all other init functions are run before\n// we instantiate the manager.\n\n// publicapigen:drop\nvar Singleton *Manager\n\nfunc init() {\n\tSingleton = NewManager(usermetrics.Singleton, appconf.Static, appconf.Runtime, logging.RootLogger)\n\tshutdown.Singleton.RegisterShutdownHandler(Singleton.Shutdown)\n\tgo Singleton.BeginCollection()\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/secrets/manager_internal.go",
    "content": "package secrets\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"maps\"\n\t\"os\"\n\t\"strings\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/cfgutil\"\n)\n\ntype Manager struct {\n\tcfg     *config.Runtime\n\tsecrets map[string]string\n}\n\nfunc NewManager(cfg *config.Runtime, infraCfgEnv, appSecretsEnv string) *Manager {\n\tsecrets := parse(appSecretsEnv)\n\tif infraCfgEnv != \"\" {\n\t\tcfg, err := config.LoadInfraConfig(infraCfgEnv)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"encore: could not read infra config\", err)\n\t\t}\n\t\tmaps.Copy(secrets, cfg.Secrets.GetSecrets())\n\t}\n\treturn &Manager{cfg: cfg, secrets: secrets}\n}\n\n// Load loads a secret.\nfunc (mgr *Manager) Load(key string, inService string) string {\n\tif val, ok := mgr.secrets[key]; ok {\n\t\treturn val\n\t}\n\n\t// For anything but local development or a gateway, a missing secret is a fatal error.\n\tif mgr.cfg.EnvCloud != \"local\" && cfgutil.IsHostedService(mgr.cfg, inService) {\n\t\tfmt.Fprintln(os.Stderr, \"encore: could not find secret\", key)\n\t\tos.Exit(2)\n\t}\n\n\treturn \"\"\n}\n\n// parse parses secrets in \"key1=base64(val1),key2=base64(val2)\" format into a map.\nfunc parse(s string) map[string]string {\n\ts, isGzipped := strings.CutPrefix(s, \"gzip:\")\n\tif isGzipped {\n\t\tvar b []byte\n\t\tvar err error\n\n\t\tif b, err = base64.StdEncoding.DecodeString(s); err != nil {\n\t\t\tb, err = base64.RawURLEncoding.DecodeString(s)\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: could not decode app secrets:\", err)\n\t\t}\n\t\tgz, err := gzip.NewReader(bytes.NewReader(b))\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: could not unzip app secrets:\", err)\n\t\t}\n\t\tb, err = io.ReadAll(gz)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: could not read app secrets:\", err)\n\t\t}\n\n\t\ts = string(b)\n\t}\n\tm := make(map[string]string)\n\tif s == \"\" {\n\t\treturn m\n\t}\n\tfor _, part := range strings.Split(s, \",\") {\n\t\tkv := strings.SplitN(part, \"=\", 2)\n\t\tif len(kv) != 2 {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: invalid secret value\")\n\t\t}\n\t\tval, err := base64.RawURLEncoding.DecodeString(kv[1])\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"encore runtime: fatal error: invalid secret value\")\n\t\t}\n\t\tm[kv[0]] = string(val)\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/infrasdk/secrets/secrets.go",
    "content": "//go:build encore_app\n\npackage secrets\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/encoreenv\"\n)\n\nvar singleton = NewManager(\n\tappconf.Runtime,\n\tencoreenv.Get(\"ENCORE_INFRA_CONFIG_PATH\"),\n\tencoreenv.Get(\"ENCORE_APP_SECRETS\"),\n)\n\nfunc Load(key string, inService string) string {\n\treturn singleton.Load(key, inService)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/appconf/appconf.go",
    "content": "//go:build encore_app\n\npackage appconf\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/encoreenv\"\n)\n\n// static is set using linker flags.\nvar static string\n\nvar (\n\t// Static is embedded at compile time using overlays.\n\tStatic *config.Static\n\n\t// Runtime is injected at runtime using environment variables.\n\tRuntime *config.Runtime\n)\n\nfunc init() {\n\tStatic = config.ParseStatic(static)\n\n\t// Set the embedded environment variables.\n\tfor k, v := range Static.EmbeddedEnvs {\n\t\tencoreenv.Set(k, v)\n\t}\n\n\tRuntime = config.ParseRuntime(\n\t\tencoreenv.Get(\"ENCORE_RUNTIME_CONFIG\"),\n\t\tencoreenv.Get(\"ENCORE_RUNTIME_CONFIG_PATH\"),\n\t\tencoreenv.Get(\"ENCORE_PROCESS_CONFIG\"),\n\t\tencoreenv.Get(\"ENCORE_INFRA_CONFIG_PATH\"),\n\t\tencoreenv.Get(\"ENCORE_DEPLOY_ID\"),\n\t)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/cfgutil/svc.go",
    "content": "package cfgutil\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// IsHostedService returns true if the given service is hosted in this container\n// or false otherwise.\n//\n// If no service name is passed in, then this function returns true if any service\n// code is running in this container\nfunc IsHostedService(runtime *config.Runtime, serviceName string) bool {\n\t// No runtime configured services or gateways means all services are running here\n\tif len(runtime.HostedServices) == 0 && len(runtime.Gateways) == 0 {\n\t\treturn true\n\t}\n\n\t// If we're not hosting a gateway and no service name is passed in\n\t// then we're checking if we're running any service code\n\tif serviceName == \"\" && len(runtime.HostedServices) > 0 {\n\t\treturn true\n\t}\n\n\t// Otherwise check if we're hosting this\n\tfor _, service := range runtime.HostedServices {\n\t\tif service == serviceName {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/cloud/clouds.go",
    "content": "// Package cloud contains some helpers for referring to different cloud implementations\npackage cloud\n\n// CloudProvider represents the cloud provider this application is running in.\n//\n// Additional cloud providers may be added in the future.\ntype CloudProvider = string\n\nconst (\n\tAWS   CloudProvider = \"aws\"\n\tGCP   CloudProvider = \"gcp\"\n\tAzure CloudProvider = \"azure\"\n\n\t// Encore is Encore's own cloud offering, and the default provider for new Environments.\n\tEncore CloudProvider = \"encore\"\n\n\t// Local is used when an application is running from the Encore CLI by using either 'encore run' or 'encore test'\n\tLocal CloudProvider = \"local\"\n)\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/cloudtrace/extractors.go",
    "content": "package cloudtrace\n\nimport (\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/model\"\n)\n\ntype TraceContext struct {\n\tTraceID model.TraceID // The trace ID we are in (zero if not set yet)\n\tSpanID  model.SpanID  // The span ID that represents this request (zero if not set by the cloud provider)\n}\n\n// parseB3Headers parses the B3 headers from the request and returns the trace context.\nfunc parseB3Headers(logger zerolog.Logger, r *http.Request) *TraceContext {\n\ttraceIDHex := r.Header.Get(\"X-B3-TraceId\")\n\tspanIDHex := r.Header.Get(\"X-B3-SpanId\") // Note: ParentSpanId is used to denote the parent span ID in the B3 spec\n\n\tif traceIDHex == \"\" || spanIDHex == \"\" {\n\t\treturn nil\n\t}\n\n\t// Extract\n\ttraceID, err := hex.DecodeString(traceIDHex)\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Str(\"header\", traceIDHex).Msg(\"Failed to decode X-B3-TraceId header\")\n\t\treturn nil\n\t}\n\tspanID, err := hex.DecodeString(spanIDHex)\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Str(\"header\", spanIDHex).Msg(\"Failed to decode X-B3-SpanId header\")\n\t\treturn nil\n\t}\n\n\t// Validate\n\tif len(traceID) != 16 {\n\t\tlogger.Warn().Str(\"header\", traceIDHex).Msg(\"X-B3-TraceId header was not 16 bytes\")\n\t\treturn nil\n\t}\n\tif len(spanID) != 8 {\n\t\tlogger.Warn().Str(\"header\", spanIDHex).Msg(\"X-B3-SpanId header was not 8 bytes\")\n\t\treturn nil\n\t}\n\n\t// Return\n\tvar traceContext TraceContext\n\tcopy(traceContext.TraceID[:], traceID)\n\tcopy(traceContext.SpanID[:], spanID)\n\treturn &traceContext\n}\n\n// parseB3SingleHeader parses the B3 single header from the request and returns the trace context.\nfunc parseB3SingleHeader(logger zerolog.Logger, r *http.Request) *TraceContext {\n\tb3Header := r.Header.Get(\"b3\")\n\tparts := strings.Split(b3Header, \"-\")\n\tif len(parts) < 2 {\n\t\treturn nil\n\t}\n\n\t// Extract\n\ttraceID, err := hex.DecodeString(parts[0])\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Str(\"header\", b3Header).Msg(\"Failed to decode b3 header, trace ID was not hex encoded\")\n\t\treturn nil\n\t}\n\tspanID, err := hex.DecodeString(parts[1])\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Str(\"header\", b3Header).Msg(\"Failed to decode b3 header, span ID was not hex encoded\")\n\t\treturn nil\n\t}\n\n\t// Validate\n\tif len(traceID) != 16 {\n\t\tlogger.Warn().Str(\"header\", b3Header).Msg(\"Failed to decode b3 header, trace ID was not 16 bytes\")\n\t\treturn nil\n\t}\n\tif len(spanID) != 8 {\n\t\tlogger.Warn().Str(\"header\", b3Header).Msg(\"Failed to decode b3 header, span ID was not 8 bytes\")\n\t\treturn nil\n\t}\n\n\t// Return\n\tvar traceContext TraceContext\n\tcopy(traceContext.TraceID[:], traceID)\n\tcopy(traceContext.SpanID[:], spanID)\n\treturn &traceContext\n}\n\n// parseGCloudTraceContext parses the Google Cloud Trace header from the request and returns the trace context.\nfunc parseGCloudTraceContext(logger zerolog.Logger, r *http.Request) *TraceContext {\n\ttraceHeader := r.Header.Get(\"X-Cloud-Trace-Context\")\n\tparts := strings.SplitN(traceHeader, \"/\", 2)\n\tif len(parts) < 2 {\n\t\treturn nil\n\t}\n\n\t// Extract\n\ttraceIDHex := parts[0]\n\tspanIDParts := strings.SplitN(parts[1], \";\", 2)\n\tspanIDDecStr := spanIDParts[0] // Split on semicolon to ignore optional fields (like ;o=1)\n\n\ttraceID, err := hex.DecodeString(traceIDHex)\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Str(\"header\", traceHeader).Msg(\"Failed to decode X-Cloud-Trace-Context header, trace ID was not hex encoded\")\n\t\treturn nil\n\t}\n\tspanIDDec, err := strconv.ParseUint(spanIDDecStr, 10, 64)\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Str(\"header\", traceHeader).Msg(\"Failed to decode X-Cloud-Trace-Context header, span ID was not a decimal number\")\n\t\treturn nil\n\t}\n\n\t// Validate\n\tif len(traceID) != 16 {\n\t\tlogger.Warn().Str(\"header\", traceHeader).Msg(\"Failed to decode X-Cloud-Trace-Context header, trace ID was not 16 bytes\")\n\t\treturn nil\n\t}\n\n\t// Return\n\tvar traceContext TraceContext\n\tcopy(traceContext.TraceID[:], traceID)\n\tbinary.BigEndian.PutUint64(traceContext.SpanID[:], spanIDDec)\n\treturn &traceContext\n}\n\n// parseAWSXRayTraceContext returns the Trace ID\nfunc parseAWSXRayTraceContext(logger zerolog.Logger, r *http.Request) *TraceContext {\n\ttraceHeader := r.Header.Get(\"X-Amzn-Trace-Id\")\n\tparts := strings.Split(traceHeader, \";\")\n\ttraceIDHex := \"\"\n\n\t// Extract\n\tfor _, part := range parts {\n\t\tkeyValue := strings.Split(part, \"=\")\n\t\tif len(keyValue) != 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch keyValue[0] {\n\t\tcase \"Root\":\n\t\t\ttraceParts := strings.Split(keyValue[1], \"-\")\n\t\t\tif len(traceParts) != 3 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttraceIDHex = traceParts[2]\n\t\t}\n\t}\n\n\tif traceIDHex == \"\" {\n\t\treturn nil\n\t}\n\n\ttraceID, err := hex.DecodeString(traceIDHex)\n\tif err != nil {\n\t\tlogger.Warn().Str(\"header\", traceHeader).Msg(\"Failed to decode X-Amzn-Trace-Id header, trace ID was not hex\")\n\t\treturn nil\n\t}\n\n\t// Validate\n\tif len(traceID) != 16 {\n\t\tlogger.Warn().Str(\"header\", traceHeader).Msg(\"Failed to decode X-Amzn-Trace-Id header, trace ID was not 16 bytes\")\n\t\treturn nil\n\t}\n\n\t// Return\n\tvar traceContext TraceContext\n\tcopy(traceContext.TraceID[:], traceID)\n\t// AWS does not generate a SpanID for us\n\treturn nil\n}\n\n// parseTraceParent returns the standardised trace parent header, which doesn't give us an ID for this span\n// but does given us a trace ID\nfunc parseTraceParent(logger zerolog.Logger, r *http.Request) *TraceContext {\n\ttraceHeader := r.Header.Get(\"traceparent\")\n\n\t// Extract\n\tparts := strings.Split(traceHeader, \"-\")\n\tif len(parts) < 3 {\n\t\treturn nil\n\t}\n\ttraceID, err := hex.DecodeString(parts[1])\n\tif err != nil {\n\t\tlogger.Warn().Str(\"header\", traceHeader).Msg(\"Failed to decode traceparent header, trace ID was not hex\")\n\t\treturn nil\n\t}\n\n\t// Validate\n\tif len(traceID) != 16 {\n\t\tlogger.Warn().Str(\"header\", traceHeader).Msg(\"Failed to decode traceparent header, trace ID was not 16 bytes\")\n\t\treturn nil\n\t}\n\n\t// Return\n\tvar traceContext TraceContext\n\tcopy(traceContext.TraceID[:], traceID)\n\t// traceparent doesn't tell give us a span ID for this request\n\treturn &traceContext\n}\n\n// ExtractCloudTraceIDs extracts the clouds expected Trace ID and Span ID for this request.\n//\n// This function will never return nil\nfunc ExtractCloudTraceIDs(logger zerolog.Logger, req *http.Request) *TraceContext {\n\textractors := []func(zerolog.Logger, *http.Request) *TraceContext{\n\t\t// parseGCloudTraceContext, // GCP specific and gives us _our_ span ID\n\t\t// parseAWSXRayTraceContext, // AWS specific and does not given us a span ID\n\t\tparseB3Headers,      // Older standard which does give us a span ID, but is cloud agnostic\n\t\tparseB3SingleHeader, // Different variant of the B3 standard, which gives us a cloud ID\n\t\tparseTraceParent,    // We do this last, as ideally we want a span ID which the cloud already knows about for this request\n\t}\n\n\tfor _, extractor := range extractors {\n\t\tif traceContext := extractor(logger, req); traceContext != nil {\n\t\t\treturn traceContext\n\t\t}\n\t}\n\n\treturn &TraceContext{}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/cloudtrace/gcp.go",
    "content": "package cloudtrace\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n)\n\n// gcpProjectID is the GCP Project ID for this process.\nvar (\n\tgcpProjectID   = \"\"\n\tgcpProjectLoad sync.Once\n)\n\n// GcpProjectID returns the GCP Project ID for this process.\n//\n// In order of precdecence:\n// 1. Look for GCP_PROJECT environment variable\n// 2. Look for GCLOUD_PROJECT environment variable\n// 3. Look for GOOGLE_CLOUD_PROJECT environment variable\n// 4. Check for GOOGLE_APPLICATION_CREDENTIALS JSON file\n// 5. GCE project ID from metadata server\n//\n// This code is derived from: https://github.com/googleapis/google-auth-library-nodejs/blob/070ec96b78dc26791bacb452ebef13d0a5ae6b18/src/auth/googleauth.ts#L245\n//\n// If it fails or panics, it will return an empty string and will not attempt to load again.\nfunc GcpProjectID() string {\n\tgcpProjectLoad.Do(func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tgcpProjectID = \"\"\n\t\t\t}\n\t\t}()\n\n\t\tgcpProjectID = gcpProjectIDFromEnv()\n\n\t\tif gcpProjectID == \"\" {\n\t\t\tgcpProjectID = gcpProjectIDFromCredsFile()\n\t\t}\n\n\t\tif gcpProjectID == \"\" {\n\t\t\tgcpProjectID = gcpProjectIDFromMetadata()\n\t\t}\n\t})\n\n\treturn gcpProjectID\n}\n\nfunc gcpProjectIDFromEnv() string {\n\tswitch {\n\tcase os.Getenv(\"GCP_PROJECT\") != \"\":\n\t\treturn os.Getenv(\"GCP_PROJECT\")\n\tcase os.Getenv(\"GCLOUD_PROJECT\") != \"\":\n\t\treturn os.Getenv(\"GCLOUD_PROJECT\")\n\tcase os.Getenv(\"GOOGLE_CLOUD_PROJECT\") != \"\":\n\t\treturn os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tcase os.Getenv(\"gcp_project\") != \"\":\n\t\treturn os.Getenv(\"gcp_project\")\n\tcase os.Getenv(\"gcloud_project\") != \"\":\n\t\treturn os.Getenv(\"gcloud_project\")\n\tcase os.Getenv(\"google_cloud_project\") != \"\":\n\t\treturn os.Getenv(\"google_cloud_project\")\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc gcpProjectIDFromCredsFile() string {\n\tcreds := os.Getenv(\"GOOGLE_APPLICATION_CREDENTIALS\")\n\tif creds == \"\" {\n\t\tcreds = os.Getenv(\"google_application_credentials\")\n\t}\n\tif creds == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// Read the file\n\tcontents, err := os.ReadFile(creds)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\t// Unmarshal the JSON\n\ttype Creds struct {\n\t\tProjectID      string `json:\"project_id,omitempty\"`\n\t\tQuotaProjectID string `json:\"quota_project_id,omitempty\"`\n\t}\n\tvar c Creds\n\tif err := json.Unmarshal(contents, &c); err != nil {\n\t\treturn \"\"\n\t}\n\n\t// Return the ProjectID if set\n\tswitch {\n\tcase c.ProjectID != \"\":\n\t\treturn c.ProjectID\n\tcase c.QuotaProjectID != \"\":\n\t\treturn c.QuotaProjectID\n\t}\n\n\treturn \"\"\n}\n\nfunc gcpProjectIDFromMetadata() string {\n\tconst url = \"http://metadata.google.internal/computeMetadata/v1/project/project-id\"\n\t// nosemgrep\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treq.Header.Add(\"Metadata-Flavor\", \"Google\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\"\n\t}\n\n\tprojectIDBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn string(projectIDBytes)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/cloudtrace/logfields.go",
    "content": "package cloudtrace\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n// StructuredLogFields returns a map of structured log fields that should be\n// added to the log entry for the given request such that the cloud's native\n// tracing system can pick up the log entry and associate it with the trace.\nfunc StructuredLogFields(req *http.Request) map[string]string {\n\t// No request, no fields\n\tif req == nil {\n\t\treturn nil\n\t}\n\n\tadditionalLogFields := make(map[string]string)\n\n\t// On GCP if we add their trace context to a specific log field\n\t// then it will automatically be picked up by Stackdriver and\n\t// associated with the trace.\n\tif gcpTraceContext := req.Header.Get(\"X-Cloud-Trace-Context\"); gcpTraceContext != \"\" {\n\t\tgcpProjectID := GcpProjectID()\n\t\tif gcpProjectID != \"\" {\n\t\t\tctx := parseGCloudTraceContext(log.Logger, req)\n\n\t\t\t// Add the trace and span fields to the log entry\n\t\t\tif !ctx.TraceID.IsZero() {\n\t\t\t\tadditionalLogFields[\"logging.googleapis.com/trace\"] = fmt.Sprintf(\"projects/%s/traces/%x\", gcpProjectID, ctx.TraceID[:])\n\n\t\t\t\tif !ctx.SpanID.IsZero() {\n\t\t\t\t\t// Google specifies we should use hex encoding for the span ID on these logs so the Trace Explorer can pick them up.\n\t\t\t\t\t// (even though the span ID is a uint64 when passed in the header & Google's own access logging using the integer version\n\t\t\t\t\t// the UI expects a hex string)\n\t\t\t\t\tadditionalLogFields[\"logging.googleapis.com/spanId\"] = fmt.Sprintf(\"%x\", ctx.SpanID[:])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn additionalLogFields\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/encoreenv/app.go",
    "content": "//go:build encore_app\n\npackage encoreenv\n\n// isApp is true if the current build is an app build.\nconst isApp = true\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/encoreenv/encoreenv.go",
    "content": "package encoreenv\n\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\nfunc Get(env string) string {\n\treturn envs[env]\n}\n\nfunc Set(env, val string) {\n\tenvs[env] = val\n}\n\nvar envs map[string]string\n\nfunc init() {\n\tenvs = make(map[string]string)\n\tfor _, e := range os.Environ() {\n\t\tif strings.HasPrefix(e, \"ENCORE_\") {\n\t\t\tkey, val, _ := strings.Cut(e, \"=\")\n\t\t\tenvs[key] = val\n\n\t\t\t// Unset ENCORE_ environment variables if we're running\n\t\t\t// inside the Encore application.\n\t\t\tif isApp {\n\t\t\t\t_ = os.Unsetenv(key)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/encoreenv/noapp.go",
    "content": "//go:build !encore_app\n\npackage encoreenv\n\n// isApp is true if the current build is an app build.\nconst isApp = false\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/etype/marshal.go",
    "content": "package etype\n\nimport (\n\t\"encoding/base64\"\n\tstdjson \"encoding/json\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/types/option\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype ElemMarshaller[T any] func(T) (val string, present bool)\n\nfunc MarshalOne[T any](fn ElemMarshaller[T], data T) string {\n\tif val, present := fn(data); present {\n\t\treturn val\n\t} else {\n\t\treturn \"\"\n\t}\n}\n\nfunc MarshalOneAsList[T any](fn ElemMarshaller[T], data T) []string {\n\tif val, present := fn(data); present {\n\t\treturn []string{val}\n\t} else {\n\t\treturn []string{}\n\t}\n}\n\nfunc MarshalList[T any](fn ElemMarshaller[T], data []T) []string {\n\tresult := make([]string, 0, len(data))\n\tfor _, x := range data {\n\t\tif val, present := fn(x); present {\n\t\t\tresult = append(result, val)\n\t\t}\n\t}\n\treturn result\n}\n\n// OptionMarshaller wraps an ElemMarshaller to produce an ElemMarshaller for option.Option types.\nfunc OptionMarshaller[T any](inner ElemMarshaller[T]) ElemMarshaller[option.Option[T]] {\n\treturn func(s option.Option[T]) (val string, present bool) {\n\t\tif val, ok := s.Get(); ok {\n\t\t\treturn inner(val)\n\t\t}\n\t\treturn \"\", false\n\t}\n}\n\nfunc MarshalInt16(s int16) (v string, present bool) {\n\treturn strconv.FormatInt(int64(s), 10), true\n}\n\nfunc MarshalUint16(s uint16) (v string, present bool) {\n\treturn strconv.FormatUint(uint64(s), 10), true\n}\n\nfunc MarshalFloat64(s float64) (v string, present bool) {\n\treturn strconv.FormatFloat(s, uint8(0x66), -1, 64), true\n}\n\nfunc MarshalBytes(s []byte) (v string, present bool) {\n\treturn base64.URLEncoding.EncodeToString(s), true\n}\n\nfunc MarshalUserID(s auth.UID) (v string, present bool) {\n\treturn string(s), true\n}\n\nfunc MarshalUint32(s uint32) (v string, present bool) {\n\treturn strconv.FormatUint(uint64(s), 10), true\n}\n\nfunc MarshalString(s string) (v string, present bool) {\n\treturn s, true\n}\n\nfunc MarshalUUID(s uuid.UUID) (v string, present bool) {\n\treturn s.String(), true\n}\n\nfunc MarshalJSON(s stdjson.RawMessage) (v string, present bool) {\n\treturn string(s), true\n}\n\nfunc MarshalInt(s int) (v string, present bool) {\n\treturn strconv.FormatInt(int64(s), 10), true\n}\n\nfunc MarshalUint(s uint) (v string, present bool) {\n\treturn strconv.FormatUint(uint64(s), 10), true\n}\n\nfunc MarshalBool(s bool) (v string, present bool) {\n\treturn strconv.FormatBool(s), true\n}\n\nfunc MarshalUint8(s uint8) (v string, present bool) {\n\treturn strconv.FormatUint(uint64(s), 10), true\n}\n\nfunc MarshalUint64(s uint64) (v string, present bool) {\n\treturn strconv.FormatUint(s, 10), true\n}\n\nfunc MarshalFloat32(s float32) (v string, present bool) {\n\treturn strconv.FormatFloat(float64(s), uint8(0x66), -1, 32), true\n}\n\nfunc MarshalTime(s time.Time) (v string, present bool) {\n\treturn s.Format(time.RFC3339), true\n}\n\nfunc MarshalInt8(s int8) (v string, present bool) {\n\treturn strconv.FormatInt(int64(s), 10), true\n}\n\nfunc MarshalInt32(s int32) (v string, present bool) {\n\treturn strconv.FormatInt(int64(s), 10), true\n}\n\nfunc MarshalInt64(s int64) (v string, present bool) {\n\treturn strconv.FormatInt(s, 10), true\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/etype/unmarshal.go",
    "content": "package etype\n\nimport (\n\t\"encoding/base64\"\n\tstdjson \"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"time\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/types/option\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype ElemUnmarshaller[T any] func(s string) (T, error)\n\nfunc UnmarshalOne[T any](u *Unmarshaller, fn ElemUnmarshaller[T], field, data string, required bool) (val T) {\n\tif !required && data == \"\" {\n\t\treturn val\n\t}\n\n\tu.NonEmptyValues++\n\tval, err := fn(data)\n\tif err != nil {\n\t\tu.setErr(\"invalid parameter\", field, err)\n\t}\n\treturn val\n}\n\nfunc UnmarshalList[T any](u *Unmarshaller, fn ElemUnmarshaller[T], field string, data []string, required bool) []T {\n\tif !required && len(data) == 0 {\n\t\treturn nil\n\t}\n\tu.NonEmptyValues++\n\tvar list []T\n\tfor _, x := range data {\n\t\tval, err := fn(x)\n\t\tif err != nil {\n\t\t\tu.setErr(\"invalid parameter\", field, err)\n\t\t\tbreak\n\t\t}\n\t\tlist = append(list, val)\n\t}\n\treturn list\n}\n\n// OptionUnmarshaller wraps an ElemUnmarshaller to produce an ElemUnmarshaller for option.Option types.\nfunc OptionUnmarshaller[T any](inner ElemUnmarshaller[T]) ElemUnmarshaller[option.Option[T]] {\n\treturn func(s string) (option.Option[T], error) {\n\t\tv, err := inner(s)\n\t\tif err != nil {\n\t\t\treturn option.None[T](), err\n\t\t}\n\t\treturn option.Some(v), nil\n\t}\n}\n\n// IncNonEmpty increments the number of non-empty values this decoder has decoded.\n// It's useful when decoding something that doesn't go through the unmarshaller\n// but still was present, so that code that checks NonEmptyValues is correct.\nfunc (u *Unmarshaller) IncNonEmpty() {\n\tu.NonEmptyValues++\n}\n\n// Unmarshaller is used to serialize request data into strings and deserialize response data from strings\ntype Unmarshaller struct {\n\tError          error // The last error that occurred\n\tNonEmptyValues int   // The number of values this decoder has decoded\n}\n\nfunc UnmarshalInt16(s string) (int16, error) {\n\tx, err := strconv.ParseInt(s, 10, 16)\n\treturn int16(x), err\n}\n\nfunc UnmarshalUint16(s string) (uint16, error) {\n\tx, err := strconv.ParseUint(s, 10, 16)\n\treturn uint16(x), err\n}\n\nfunc UnmarshalFloat64(s string) (v float64, err error) {\n\tx, err := strconv.ParseFloat(s, 64)\n\treturn x, err\n}\n\nfunc UnmarshalBytes(s string) ([]byte, error) {\n\tv, err := base64.URLEncoding.DecodeString(s)\n\treturn v, err\n}\n\nfunc UnmarshalUserID(s string) (auth.UID, error) {\n\treturn auth.UID(s), nil\n}\n\nfunc UnmarshalUint32(s string) (uint32, error) {\n\tx, err := strconv.ParseUint(s, 10, 32)\n\treturn uint32(x), err\n}\n\nfunc UnmarshalString(s string) (string, error) {\n\treturn s, nil\n}\n\nfunc UnmarshalUUID(s string) (uuid.UUID, error) {\n\tv, err := uuid.FromString(s)\n\treturn v, err\n}\n\nfunc UnmarshalJSON(s string) (stdjson.RawMessage, error) {\n\treturn stdjson.RawMessage(s), nil\n}\n\nfunc UnmarshalInt(s string) (int, error) {\n\tx, err := strconv.ParseInt(s, 10, 64)\n\treturn int(x), err\n}\n\nfunc UnmarshalUint(s string) (uint, error) {\n\tx, err := strconv.ParseUint(s, 10, 64)\n\treturn uint(x), err\n}\n\nfunc UnmarshalBool(s string) (bool, error) {\n\tv, err := strconv.ParseBool(s)\n\treturn v, err\n}\n\nfunc UnmarshalUint8(s string) (uint8, error) {\n\tx, err := strconv.ParseUint(s, 10, 8)\n\treturn uint8(x), err\n}\n\nfunc UnmarshalUint64(s string) (uint64, error) {\n\tx, err := strconv.ParseUint(s, 10, 64)\n\treturn x, err\n}\n\nfunc UnmarshalFloat32(s string) (float32, error) {\n\tx, err := strconv.ParseFloat(s, 32)\n\treturn float32(x), err\n}\n\nfunc UnmarshalTime(s string) (time.Time, error) {\n\tv, err := time.Parse(time.RFC3339, s)\n\treturn v, err\n}\n\nfunc UnmarshalInt8(s string) (int8, error) {\n\tx, err := strconv.ParseInt(s, 10, 8)\n\treturn int8(x), err\n}\n\nfunc UnmarshalInt32(s string) (int32, error) {\n\tx, err := strconv.ParseInt(s, 10, 32)\n\treturn int32(x), err\n}\n\nfunc UnmarshalInt64(s string) (int64, error) {\n\tx, err := strconv.ParseInt(s, 10, 64)\n\treturn x, err\n}\n\n// setErr sets the error within the object if one is not already set\nfunc (u *Unmarshaller) setErr(msg, field string, err error) {\n\tif err != nil && u.Error == nil {\n\t\tu.Error = fmt.Errorf(\"%s: %s: %w\", field, msg, err)\n\t}\n}\n\nfunc (u *Unmarshaller) ReadBody(body io.Reader) (payload []byte) {\n\tpayload, err := io.ReadAll(body)\n\tif err == nil && len(payload) == 0 {\n\t\tu.setErr(\"missing request body\", \"request_body\", fmt.Errorf(\"missing request body\"))\n\t} else if err != nil {\n\t\tu.setErr(\"could not parse request body\", \"request_body\", err)\n\t}\n\treturn payload\n}\n\nfunc (u *Unmarshaller) ParseJSON(field string, iter *jsoniter.Iterator, dst any) {\n\titer.ReadVal(dst)\n\tu.setErr(\"invalid json parameter\", field, iter.Error)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/health/check.go",
    "content": "package health\n\nimport (\n\t\"context\"\n)\n\n// Check is an interface that can be implemented by any type that wants to be\n// registered as a health check.\ntype Check interface {\n\t// HealthCheck returns a slice of CheckResult structs, one for each check\n\t// that was performed.\n\tHealthCheck(ctx context.Context) []CheckResult\n}\n\n// CheckResult is a struct that contains the result of a health check.\ntype CheckResult struct {\n\tName string // Name is the name of the check.\n\tErr  error  // Err is the error returned by the check (nil for healthy)\n}\n\n// checkFunc is a type that implements the Check interface.\ntype checkFunc struct {\n\tname  string\n\tcheck func(ctx context.Context) error\n}\n\nfunc (c *checkFunc) HealthCheck(ctx context.Context) []CheckResult {\n\treturn []CheckResult{{Name: c.name, Err: c.check(ctx)}}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/health/health.go",
    "content": "package health\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog/log\"\n)\n\n// CheckRegistry is a registry of health checks from the API and Infra SDKs\n// and other parts of the runtime.\ntype CheckRegistry struct {\n\tm      sync.Mutex\n\tchecks []Check\n}\n\n// NewCheckRegistry creates a new CheckRegistry.\n//\n// If running in an app there is a [Singleton]\nfunc NewCheckRegistry() *CheckRegistry {\n\treturn &CheckRegistry{}\n}\n\n// Register registers a new health check.\n//\n// Checks must complete within 5 seconds, otherwise\n// they will be terminated and considered failed.\n//\n// Checks can be called at any time and could have\n// multiple goroutines calling them concurrently.\nfunc (c *CheckRegistry) Register(check Check) {\n\tc.m.Lock()\n\tdefer c.m.Unlock()\n\tc.checks = append(c.checks, check)\n}\n\n// RegisterFunc registers a new health check from a function with a given name\n//\n// This is a convince wrapper over [CheckRegistry.Register], see that function\n// for more details and expected behavior.\nfunc (c *CheckRegistry) RegisterFunc(name string, check func(ctx context.Context) error) {\n\tc.m.Lock()\n\tdefer c.m.Unlock()\n\tc.Register(&checkFunc{name, check})\n}\n\n// GetChecks returns all registered health checks.\nfunc (c *CheckRegistry) GetChecks() []Check {\n\tc.m.Lock()\n\tdefer c.m.Unlock()\n\treturn c.checks\n}\n\n// RunAll runs all health checks and returns the results.\nfunc (c *CheckRegistry) RunAll(ctx context.Context) []CheckResult {\n\tctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\tdefer cancel()\n\n\tchecks := c.GetChecks()\n\n\t// Run all checks in parallel.\n\tresults := make(chan []CheckResult, len(checks))\n\tvar wg sync.WaitGroup\n\twg.Add(len(checks))\n\tfor _, check := range checks {\n\t\tcheck := check\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Error().Any(\"panic\", r).Msg(\"health check resulted in a panic\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tresults <- check.HealthCheck(ctx)\n\t\t}()\n\t}\n\n\t// Wait for all checks to complete for the context to be cancelled.\n\tvar allResults []CheckResult\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(done)\n\t}()\n\n\t// Wait for all checks to complete or the context to be cancelled.\n\tselect {\n\tcase <-done:\n\tcase <-ctx.Done():\n\t\tallResults = append(allResults, CheckResult{\n\t\t\tName: \"health-checks.run\",\n\t\t\tErr:  ctx.Err(),\n\t\t})\n\t}\n\tclose(results) // then close the results channel\n\n\t// Collect results.\n\tfor results := range results {\n\t\tallResults = append(allResults, results...)\n\t}\n\n\t// Sort results by name.\n\tslices.SortFunc(allResults, func(a, b CheckResult) int {\n\t\treturn cmp.Compare(a.Name, b.Name)\n\t})\n\n\treturn allResults\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/health/singleton.go",
    "content": "//go:build encore_app\n\npackage health\n\n// Singleton is the singleton instance of the health check registry\n// for a running Encore application.\nvar Singleton = NewCheckRegistry()\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/jsonapi/jsonapi.go",
    "content": "//go:build encore_app\n\npackage jsonapi\n\nimport (\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/appconf\"\n)\n\nvar Default = jsonAPI(appconf.Runtime)\n\nfunc jsonAPI(rt *config.Runtime) jsoniter.API {\n\tindentStep := 2\n\tif rt.EnvType == \"production\" {\n\t\tindentStep = 0\n\t}\n\treturn jsoniter.Config{\n\t\tEscapeHTML:             false,\n\t\tIndentionStep:          indentStep,\n\t\tSortMapKeys:            true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/jsonapi/jsonapi_nonapp.go",
    "content": "//go:build !encore_app\n\npackage jsonapi\n\n// Note: This version of the file exists so we can run `go test` on the runtime module.\n// This is because during those test we skip anything flagged as `encore_app` as we are\n// not running inside an Encore application and so don't have access to things like\n// configuration or compiled overlays.\n\nimport (\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar Default = jsonAPI()\n\nfunc jsonAPI() jsoniter.API {\n\treturn jsoniter.Config{\n\t\tEscapeHTML:             false,\n\t\tIndentionStep:          2,\n\t\tSortMapKeys:            true,\n\t\tValidateJsonRawMessage: true,\n\t}.Froze()\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/logging/logging.go",
    "content": "//go:build encore_app\n\npackage logging\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/cloud\"\n)\n\nvar RootLogger = configure(appconf.Static, appconf.Runtime)\n\nfunc configure(static *config.Static, runtime *config.Runtime) zerolog.Logger {\n\tvar logOutput io.Writer = os.Stderr\n\tif static.PrettyPrintLogs {\n\t\tlogOutput = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {\n\t\t\tw.Out = logOutput\n\t\t})\n\t}\n\n\tlevel := zerolog.TraceLevel\n\tif runtime.LogConfig != \"\" {\n\t\tif l, err := zerolog.ParseLevel(runtime.LogConfig); err == nil {\n\t\t\tlevel = l\n\t\t}\n\t}\n\n\treconfigureZerologFormat(runtime)\n\treturn zerolog.New(logOutput).Level(level).With().Timestamp().Logger()\n}\n\nfunc reconfigureZerologFormat(runtime *config.Runtime) {\n\t// Note: if updating this function, also update\n\t// mapCloudFieldNamesToExpected in cli/cmd/encore/logs.go\n\t// as that reverses this for log streaming\n\tswitch runtime.EnvCloud {\n\tcase cloud.GCP, cloud.Encore:\n\t\tzerolog.LevelFieldName = \"severity\"\n\t\tzerolog.TimestampFieldName = \"timestamp\"\n\t\tzerolog.TimeFieldFormat = time.RFC3339Nano\n\tdefault:\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/nativehist/PROMETHEUS_LICENSE.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/nativehist/nativehist.go",
    "content": "// This file is originally from Prometheus, which is licensed under the Apache License 2.0.\n// See PROMETHEUS_LICENSE.txt for its license.\n\npackage nativehist\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\nfunc New(bucketFactor float64) *Histogram {\n\treturn &Histogram{\n\t\tSchema: pickSchema(bucketFactor),\n\t}\n}\n\ntype Histogram struct {\n\t// Order in this struct matters for the alignment required by atomic\n\t// operations, see http://golang.org/pkg/sync/atomic/#pkg-note-BUG\n\tCount uint64\n\n\t// NumZeroValues counts the number of observations in the zero bucket.\n\tNumZeroValues uint64\n\n\t// Schema is the Histogram bucket Schema. It's decided on creation.\n\tSchema int32\n\n\t// PostiveVals and NegativeVals are the buckets for non-zero observations.\n\tPositiveVals, NegativeVals sync.Map\n}\n\n// Observe records an observation in the histogram.\nfunc (h *Histogram) Observe(v float64) {\n\tvar (\n\t\tkey    int\n\t\tschema = atomic.LoadInt32(&h.Schema)\n\t\tisInf  bool\n\t)\n\tif math.IsInf(v, 0) {\n\t\t// Pretend v is MaxFloat64 but later increment key by one.\n\t\tif math.IsInf(v, +1) {\n\t\t\tv = math.MaxFloat64\n\t\t} else {\n\t\t\tv = -math.MaxFloat64\n\t\t}\n\t\tisInf = true\n\t}\n\tfrac, exp := math.Frexp(math.Abs(v))\n\tif schema > 0 {\n\t\tbounds := nativeHistogramBounds[schema]\n\t\tkey = sort.SearchFloat64s(bounds, frac) + (exp-1)*len(bounds)\n\t} else {\n\t\tkey = exp\n\t\tif frac == 0.5 {\n\t\t\tkey--\n\t\t}\n\t\tdiv := 1 << -schema\n\t\tkey = (key + div - 1) / div\n\t}\n\tif isInf {\n\t\tkey++\n\t}\n\tswitch {\n\tcase v > histogramZeroThreshold:\n\t\taddToBucket(&h.PositiveVals, key, 1)\n\tcase v < -histogramZeroThreshold:\n\t\taddToBucket(&h.NegativeVals, key, 1)\n\tdefault:\n\t\tatomic.AddUint64(&h.NumZeroValues, 1)\n\t}\n}\n\nfunc (h *Histogram) reset() {\n\tatomic.StoreUint64(&h.Count, 0)\n\tatomic.StoreUint64(&h.NumZeroValues, 0)\n\tclearSyncMap(&h.PositiveVals)\n\tclearSyncMap(&h.NegativeVals)\n}\n\n// addToBucket increments the sparse bucket at key by the provided amount. It\n// returns true if a new sparse bucket had to be created for that.\nfunc addToBucket(buckets *sync.Map, key int, increment int64) bool {\n\tif existingBucket, ok := buckets.Load(key); ok {\n\t\t// Fast path without allocation.\n\t\tatomic.AddInt64(existingBucket.(*int64), increment)\n\t\treturn false\n\t}\n\t// Bucket doesn't exist yet. Slow path allocating new counter.\n\tnewBucket := increment // TODO(beorn7): Check if this is sufficient to not let increment escape.\n\tif actualBucket, loaded := buckets.LoadOrStore(key, &newBucket); loaded {\n\t\t// The bucket was created concurrently in another goroutine.\n\t\t// Have to increment after all.\n\t\tatomic.AddInt64(actualBucket.(*int64), increment)\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc clearSyncMap(m *sync.Map) {\n\tm.Range(func(k, v interface{}) bool {\n\t\tm.Delete(k)\n\t\treturn true\n\t})\n}\n\n// nativeHistogramBounds for the frac of observed values. Only relevant for\n// schema > 0. The position in the slice is the schema. (0 is never used, just\n// here for convenience of using the schema directly as the index.)\n//\n// TODO(beorn7): Currently, we do a binary search into these slices. There are\n// ways to turn it into a small number of simple array lookups. It probably only\n// matters for schema 5 and beyond, but should be investigated. See this comment\n// as a starting point:\n// https://github.com/open-telemetry/opentelemetry-specification/issues/1776#issuecomment-870164310\nvar nativeHistogramBounds = [][]float64{\n\t// Schema \"0\":\n\t{0.5},\n\t// Schema 1:\n\t{0.5, 0.7071067811865475},\n\t// Schema 2:\n\t{0.5, 0.5946035575013605, 0.7071067811865475, 0.8408964152537144},\n\t// Schema 3:\n\t{\n\t\t0.5, 0.5452538663326288, 0.5946035575013605, 0.6484197773255048,\n\t\t0.7071067811865475, 0.7711054127039704, 0.8408964152537144, 0.9170040432046711,\n\t},\n\t// Schema 4:\n\t{\n\t\t0.5, 0.5221368912137069, 0.5452538663326288, 0.5693943173783458,\n\t\t0.5946035575013605, 0.620928906036742, 0.6484197773255048, 0.6771277734684463,\n\t\t0.7071067811865475, 0.7384130729697496, 0.7711054127039704, 0.805245165974627,\n\t\t0.8408964152537144, 0.8781260801866495, 0.9170040432046711, 0.9576032806985735,\n\t},\n\t// Schema 5:\n\t{\n\t\t0.5, 0.5109485743270583, 0.5221368912137069, 0.5335702003384117,\n\t\t0.5452538663326288, 0.5571933712979462, 0.5693943173783458, 0.5818624293887887,\n\t\t0.5946035575013605, 0.6076236799902344, 0.620928906036742, 0.6345254785958666,\n\t\t0.6484197773255048, 0.6626183215798706, 0.6771277734684463, 0.6919549409819159,\n\t\t0.7071067811865475, 0.7225904034885232, 0.7384130729697496, 0.7545822137967112,\n\t\t0.7711054127039704, 0.7879904225539431, 0.805245165974627, 0.8228777390769823,\n\t\t0.8408964152537144, 0.8593096490612387, 0.8781260801866495, 0.8973545375015533,\n\t\t0.9170040432046711, 0.9370838170551498, 0.9576032806985735, 0.9785720620876999,\n\t},\n\t// Schema 6:\n\t{\n\t\t0.5, 0.5054446430258502, 0.5109485743270583, 0.5165124395106142,\n\t\t0.5221368912137069, 0.5278225891802786, 0.5335702003384117, 0.5393803988785598,\n\t\t0.5452538663326288, 0.5511912916539204, 0.5571933712979462, 0.5632608093041209,\n\t\t0.5693943173783458, 0.5755946149764913, 0.5818624293887887, 0.5881984958251406,\n\t\t0.5946035575013605, 0.6010783657263515, 0.6076236799902344, 0.6142402680534349,\n\t\t0.620928906036742, 0.6276903785123455, 0.6345254785958666, 0.6414350080393891,\n\t\t0.6484197773255048, 0.6554806057623822, 0.6626183215798706, 0.6698337620266515,\n\t\t0.6771277734684463, 0.6845012114872953, 0.6919549409819159, 0.6994898362691555,\n\t\t0.7071067811865475, 0.7148066691959849, 0.7225904034885232, 0.7304588970903234,\n\t\t0.7384130729697496, 0.7464538641456323, 0.7545822137967112, 0.762799075372269,\n\t\t0.7711054127039704, 0.7795022001189185, 0.7879904225539431, 0.7965710756711334,\n\t\t0.805245165974627, 0.8140137109286738, 0.8228777390769823, 0.8318382901633681,\n\t\t0.8408964152537144, 0.8500531768592616, 0.8593096490612387, 0.8686669176368529,\n\t\t0.8781260801866495, 0.8876882462632604, 0.8973545375015533, 0.9071260877501991,\n\t\t0.9170040432046711, 0.9269895625416926, 0.9370838170551498, 0.9472879907934827,\n\t\t0.9576032806985735, 0.9680308967461471, 0.9785720620876999, 0.9892280131939752,\n\t},\n\t// Schema 7:\n\t{\n\t\t0.5, 0.5027149505564014, 0.5054446430258502, 0.5081891574554764,\n\t\t0.5109485743270583, 0.5137229745593818, 0.5165124395106142, 0.5193170509806894,\n\t\t0.5221368912137069, 0.5249720429003435, 0.5278225891802786, 0.5306886136446309,\n\t\t0.5335702003384117, 0.5364674337629877, 0.5393803988785598, 0.5423091811066545,\n\t\t0.5452538663326288, 0.5482145409081883, 0.5511912916539204, 0.5541842058618393,\n\t\t0.5571933712979462, 0.5602188762048033, 0.5632608093041209, 0.5663192597993595,\n\t\t0.5693943173783458, 0.572486072215902, 0.5755946149764913, 0.5787200368168754,\n\t\t0.5818624293887887, 0.585021884841625, 0.5881984958251406, 0.5913923554921704,\n\t\t0.5946035575013605, 0.5978321960199137, 0.6010783657263515, 0.6043421618132907,\n\t\t0.6076236799902344, 0.6109230164863786, 0.6142402680534349, 0.6175755319684665,\n\t\t0.620928906036742, 0.6243004885946023, 0.6276903785123455, 0.6310986751971253,\n\t\t0.6345254785958666, 0.637970889198196, 0.6414350080393891, 0.6449179367033329,\n\t\t0.6484197773255048, 0.6519406325959679, 0.6554806057623822, 0.659039800633032,\n\t\t0.6626183215798706, 0.6662162735415805, 0.6698337620266515, 0.6734708931164728,\n\t\t0.6771277734684463, 0.6808045103191123, 0.6845012114872953, 0.688217985377265,\n\t\t0.6919549409819159, 0.6957121878859629, 0.6994898362691555, 0.7032879969095076,\n\t\t0.7071067811865475, 0.7109463010845827, 0.7148066691959849, 0.718687998724491,\n\t\t0.7225904034885232, 0.7265139979245261, 0.7304588970903234, 0.7344252166684908,\n\t\t0.7384130729697496, 0.7424225829363761, 0.7464538641456323, 0.7505070348132126,\n\t\t0.7545822137967112, 0.7586795205991071, 0.762799075372269, 0.7669409989204777,\n\t\t0.7711054127039704, 0.7752924388424999, 0.7795022001189185, 0.7837348199827764,\n\t\t0.7879904225539431, 0.7922691326262467, 0.7965710756711334, 0.8008963778413465,\n\t\t0.805245165974627, 0.8096175675974316, 0.8140137109286738, 0.8184337248834821,\n\t\t0.8228777390769823, 0.8273458838280969, 0.8318382901633681, 0.8363550898207981,\n\t\t0.8408964152537144, 0.8454623996346523, 0.8500531768592616, 0.8546688815502312,\n\t\t0.8593096490612387, 0.8639756154809185, 0.8686669176368529, 0.8733836930995842,\n\t\t0.8781260801866495, 0.8828942179666361, 0.8876882462632604, 0.8925083056594671,\n\t\t0.8973545375015533, 0.9022270839033115, 0.9071260877501991, 0.9120516927035263,\n\t\t0.9170040432046711, 0.9219832844793128, 0.9269895625416926, 0.9320230241988943,\n\t\t0.9370838170551498, 0.9421720895161669, 0.9472879907934827, 0.9524316709088368,\n\t\t0.9576032806985735, 0.9628029718180622, 0.9680308967461471, 0.9732872087896164,\n\t\t0.9785720620876999, 0.9838856116165875, 0.9892280131939752, 0.9945994234836328,\n\t},\n\t// Schema 8:\n\t{\n\t\t0.5, 0.5013556375251013, 0.5027149505564014, 0.5040779490592088,\n\t\t0.5054446430258502, 0.5068150424757447, 0.5081891574554764, 0.509566998038869,\n\t\t0.5109485743270583, 0.5123338964485679, 0.5137229745593818, 0.5151158188430205,\n\t\t0.5165124395106142, 0.5179128468009786, 0.5193170509806894, 0.520725062344158,\n\t\t0.5221368912137069, 0.5235525479396449, 0.5249720429003435, 0.526395386502313,\n\t\t0.5278225891802786, 0.5292536613972564, 0.5306886136446309, 0.5321274564422321,\n\t\t0.5335702003384117, 0.5350168559101208, 0.5364674337629877, 0.5379219445313954,\n\t\t0.5393803988785598, 0.5408428074966075, 0.5423091811066545, 0.5437795304588847,\n\t\t0.5452538663326288, 0.5467321995364429, 0.5482145409081883, 0.549700901315111,\n\t\t0.5511912916539204, 0.5526857228508706, 0.5541842058618393, 0.5556867516724088,\n\t\t0.5571933712979462, 0.5587040757836845, 0.5602188762048033, 0.5617377836665098,\n\t\t0.5632608093041209, 0.564787964283144, 0.5663192597993595, 0.5678547070789026,\n\t\t0.5693943173783458, 0.5709381019847808, 0.572486072215902, 0.5740382394200894,\n\t\t0.5755946149764913, 0.5771552102951081, 0.5787200368168754, 0.5802891060137493,\n\t\t0.5818624293887887, 0.5834400184762408, 0.585021884841625, 0.5866080400818185,\n\t\t0.5881984958251406, 0.5897932637314379, 0.5913923554921704, 0.5929957828304968,\n\t\t0.5946035575013605, 0.5962156912915756, 0.5978321960199137, 0.5994530835371903,\n\t\t0.6010783657263515, 0.6027080545025619, 0.6043421618132907, 0.6059806996384005,\n\t\t0.6076236799902344, 0.6092711149137041, 0.6109230164863786, 0.6125793968185725,\n\t\t0.6142402680534349, 0.6159056423670379, 0.6175755319684665, 0.6192499490999082,\n\t\t0.620928906036742, 0.622612415087629, 0.6243004885946023, 0.6259931389331581,\n\t\t0.6276903785123455, 0.6293922197748583, 0.6310986751971253, 0.6328097572894031,\n\t\t0.6345254785958666, 0.6362458516947014, 0.637970889198196, 0.6397006037528346,\n\t\t0.6414350080393891, 0.6431741147730128, 0.6449179367033329, 0.6466664866145447,\n\t\t0.6484197773255048, 0.6501778216898253, 0.6519406325959679, 0.6537082229673385,\n\t\t0.6554806057623822, 0.6572577939746774, 0.659039800633032, 0.6608266388015788,\n\t\t0.6626183215798706, 0.6644148621029772, 0.6662162735415805, 0.6680225691020727,\n\t\t0.6698337620266515, 0.6716498655934177, 0.6734708931164728, 0.6752968579460171,\n\t\t0.6771277734684463, 0.6789636531064505, 0.6808045103191123, 0.6826503586020058,\n\t\t0.6845012114872953, 0.6863570825438342, 0.688217985377265, 0.690083933630119,\n\t\t0.6919549409819159, 0.6938310211492645, 0.6957121878859629, 0.6975984549830999,\n\t\t0.6994898362691555, 0.7013863456101023, 0.7032879969095076, 0.7051948041086352,\n\t\t0.7071067811865475, 0.7090239421602076, 0.7109463010845827, 0.7128738720527471,\n\t\t0.7148066691959849, 0.7167447066838943, 0.718687998724491, 0.7206365595643126,\n\t\t0.7225904034885232, 0.7245495448210174, 0.7265139979245261, 0.7284837772007218,\n\t\t0.7304588970903234, 0.7324393720732029, 0.7344252166684908, 0.7364164454346837,\n\t\t0.7384130729697496, 0.7404151139112358, 0.7424225829363761, 0.7444354947621984,\n\t\t0.7464538641456323, 0.7484777058836176, 0.7505070348132126, 0.7525418658117031,\n\t\t0.7545822137967112, 0.7566280937263048, 0.7586795205991071, 0.7607365094544071,\n\t\t0.762799075372269, 0.7648672334736434, 0.7669409989204777, 0.7690203869158282,\n\t\t0.7711054127039704, 0.7731960915705107, 0.7752924388424999, 0.7773944698885442,\n\t\t0.7795022001189185, 0.7816156449856788, 0.7837348199827764, 0.7858597406461707,\n\t\t0.7879904225539431, 0.7901268813264122, 0.7922691326262467, 0.7944171921585818,\n\t\t0.7965710756711334, 0.7987307989543135, 0.8008963778413465, 0.8030678282083853,\n\t\t0.805245165974627, 0.8074284071024302, 0.8096175675974316, 0.8118126635086642,\n\t\t0.8140137109286738, 0.8162207259936375, 0.8184337248834821, 0.820652723822003,\n\t\t0.8228777390769823, 0.8251087869603088, 0.8273458838280969, 0.8295890460808079,\n\t\t0.8318382901633681, 0.8340936325652911, 0.8363550898207981, 0.8386226785089391,\n\t\t0.8408964152537144, 0.8431763167241966, 0.8454623996346523, 0.8477546807446661,\n\t\t0.8500531768592616, 0.8523579048290255, 0.8546688815502312, 0.8569861239649629,\n\t\t0.8593096490612387, 0.8616394738731368, 0.8639756154809185, 0.8663180910111553,\n\t\t0.8686669176368529, 0.871022112577578, 0.8733836930995842, 0.8757516765159389,\n\t\t0.8781260801866495, 0.8805069215187917, 0.8828942179666361, 0.8852879870317771,\n\t\t0.8876882462632604, 0.890095013257712, 0.8925083056594671, 0.8949281411607002,\n\t\t0.8973545375015533, 0.8997875124702672, 0.9022270839033115, 0.9046732696855155,\n\t\t0.9071260877501991, 0.909585556079304, 0.9120516927035263, 0.9145245157024483,\n\t\t0.9170040432046711, 0.9194902933879467, 0.9219832844793128, 0.9244830347552253,\n\t\t0.9269895625416926, 0.92950288621441, 0.9320230241988943, 0.9345499949706191,\n\t\t0.9370838170551498, 0.93962450902828, 0.9421720895161669, 0.9447265771954693,\n\t\t0.9472879907934827, 0.9498563490882775, 0.9524316709088368, 0.9550139751351947,\n\t\t0.9576032806985735, 0.9601996065815236, 0.9628029718180622, 0.9654133954938133,\n\t\t0.9680308967461471, 0.9706554947643201, 0.9732872087896164, 0.9759260581154889,\n\t\t0.9785720620876999, 0.9812252401044634, 0.9838856116165875, 0.9865531961276168,\n\t\t0.9892280131939752, 0.9919100824251095, 0.9945994234836328, 0.9972960560854698,\n\t},\n}\n\n// The nativeHistogramBounds above can be generated with the code below.\n//\n// TODO(beorn7): It's tempting to actually use `go generate` to generate the\n// code above. However, this could lead to slightly different numbers on\n// different architectures. We still need to come to terms if we are fine with\n// that, or if we might prefer to specify precise numbers in the standard.\n//\n// var nativeHistogramBounds [][]float64 = make([][]float64, 9)\n//\n// func init() {\n// \t// Populate nativeHistogramBounds.\n// \tnumBuckets := 1\n// \tfor i := range nativeHistogramBounds {\n// \t\tbounds := []float64{0.5}\n// \t\tfactor := math.Exp2(math.Exp2(float64(-i)))\n// \t\tfor j := 0; j < numBuckets-1; j++ {\n// \t\t\tvar bound float64\n// \t\t\tif (j+1)%2 == 0 {\n// \t\t\t\t// Use previously calculated value for increased precision.\n// \t\t\t\tbound = nativeHistogramBounds[i-1][j/2+1]\n// \t\t\t} else {\n// \t\t\t\tbound = bounds[j] * factor\n// \t\t\t}\n// \t\t\tbounds = append(bounds, bound)\n// \t\t}\n// \t\tnumBuckets *= 2\n// \t\tnativeHistogramBounds[i] = bounds\n// \t}\n// }\n\n// histogramZeroThreshold is the absolute value threshold at which values are considered zero.\n//\n// The value is 2^-128 (or 0.5*2^-127 in the actual IEEE 754 representation),\n// which is a bucket boundary at all possible resolutions.\nconst histogramZeroThreshold = 2.938735877055719e-39\n\n// pickSchema returns the largest number n between -4 and 8 such that\n// 2^(2^-n) is less or equal the provided bucketFactor.\n//\n// Special cases:\n//   - bucketFactor <= 1: panics.\n//   - bucketFactor < 2^(2^-8) (but > 1): still returns 8.\nfunc pickSchema(bucketFactor float64) int32 {\n\tif bucketFactor <= 1 {\n\t\tpanic(fmt.Errorf(\"bucketFactor %f is <=1\", bucketFactor))\n\t}\n\tfloor := math.Floor(math.Log2(math.Log2(bucketFactor)))\n\tswitch {\n\tcase floor <= -8:\n\t\treturn 8\n\tcase floor >= 4:\n\t\treturn -4\n\tdefault:\n\t\treturn -int32(floor)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/platform/platform.go",
    "content": "// Package platform handles communication with the Encore Platform.\npackage platform\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n)\n\nfunc NewClient(static *config.Static, rt *config.Runtime) *Client {\n\texp := experiments.FromConfig(static, rt)\n\treturn &Client{static, rt, exp}\n}\n\ntype Client struct {\n\tstatic  *config.Static\n\truntime *config.Runtime\n\texp     *experiments.Set\n}\n\nfunc (c *Client) addAuthKey(req *http.Request) {\n\tk := c.runtime.AuthKeys[0]\n\tdate := time.Now().UTC().Format(http.TimeFormat)\n\treq.Header.Set(\"Date\", date)\n\n\tmac := hmac.New(sha256.New, k.Data)\n\t_, _ = fmt.Fprintf(mac, \"%s\\x00%s\", date, req.URL.Path)\n\n\tbytes := make([]byte, 4, 4+sha256.Size)\n\tbinary.BigEndian.PutUint32(bytes[0:4], k.KeyID)\n\tbytes = mac.Sum(bytes)\n\tauth := base64.RawStdEncoding.EncodeToString(bytes)\n\treq.Header.Set(\"X-Encore-Auth\", auth)\n}\n\n// ValidatePlatformRequest validates whether a request originated from the platform.\nfunc (c *Client) ValidatePlatformRequest(req *http.Request, macSig string) (bool, error) {\n\tmacBytes, err := base64.RawStdEncoding.DecodeString(macSig)\n\tif err != nil {\n\t\treturn false, nil\n\t}\n\n\t// Pull out key ID from hmac prefix\n\tconst keyIDLen = 4\n\tif len(macBytes) < keyIDLen {\n\t\treturn false, nil\n\t}\n\n\tkeyID := binary.BigEndian.Uint32(macBytes[:keyIDLen])\n\tmac := macBytes[keyIDLen:]\n\tfor _, k := range c.runtime.AuthKeys {\n\t\tif k.KeyID == keyID {\n\t\t\treturn checkAuthKey(k, req, mac), nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc checkAuthKey(key config.EncoreAuthKey, req *http.Request, gotMac []byte) bool {\n\tdateStr := req.Header.Get(\"Date\")\n\tif dateStr == \"\" {\n\t\treturn false\n\t}\n\tdate, err := http.ParseTime(dateStr)\n\tif err != nil {\n\t\treturn false\n\t}\n\tconst threshold = 15 * time.Minute\n\tif diff := time.Since(date); diff > threshold || diff < -threshold {\n\t\treturn false\n\t}\n\n\tmac := hmac.New(sha256.New, key.Data)\n\t_, _ = fmt.Fprintf(mac, \"%s\\x00%s\", dateStr, req.URL.Path)\n\texpected := mac.Sum(nil)\n\treturn hmac.Equal(expected, gotMac)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/platform/singleton.go",
    "content": "//go:build encore_app\n\npackage platform\n\nimport \"encore.dev/appruntime/shared/appconf\"\n\nvar Singleton = NewClient(appconf.Static, appconf.Runtime)\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/platform/streaming_trace.go",
    "content": "package platform\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\nfunc (c *Client) StreamTrace(log trace2.Logger) error {\n\tif c.static.Testing {\n\t\t// In testing we want to block the test until the trace is done.\n\t\treturn c.blockingTrace(log)\n\t} else {\n\t\treturn c.streamingTrace(log)\n\t}\n}\n\n// streamingTrace streams a trace to the platform.\nfunc (c *Client) streamingTrace(log trace2.Logger) error {\n\t// Wait a bit for the trace to start, so we can avoid the overhead\n\t// of small chunk streaming if the trace is short.\n\tdone := log.WaitAtLeast(1 * time.Second)\n\tvar body io.Reader\n\tif done {\n\t\tdata, _ := log.GetAndClear()\n\t\tif len(data) == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Use a bytes.Reader so net/http knows the Content-Length.\n\t\tbody = bytes.NewReader(data)\n\t} else {\n\t\tr := &traceLogReader{log: log}\n\t\tif r.IsDoneAndEmpty() {\n\t\t\t// We didn't get any trace data; don't bother streaming.\n\t\t\treturn nil\n\t\t}\n\t\tbody = r\n\t}\n\n\t// Use a background context since the trace is streaming,\n\t// and we don't know how long it will take to complete.\n\tctx := context.Background()\n\treturn c.sendTraceRequest(ctx, body)\n}\n\n// blockingTrace waits for the trace to complete before sending it.\nfunc (c *Client) blockingTrace(log trace2.Logger) error {\n\t// Wait for the trace to complete\n\tlog.WaitUntilDone()\n\tdata, _ := log.GetAndClear()\n\tif len(data) == 0 {\n\t\treturn nil // optimization\n\t}\n\tbody := bytes.NewReader(data)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\treturn c.sendTraceRequest(ctx, body)\n}\n\nfunc (c *Client) sendTraceRequest(ctx context.Context, body io.Reader) error {\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.runtime.TraceEndpoint, body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tta, err := trace2.NewTimeAnchorNow().MarshalText()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"X-Encore-App-ID\", c.runtime.AppID)\n\treq.Header.Set(\"X-Encore-Env-ID\", c.runtime.EnvID)\n\treq.Header.Set(\"X-Encore-Deploy-ID\", c.runtime.DeployID)\n\treq.Header.Set(\"X-Encore-App-Commit\", c.static.AppCommit.AsRevisionString())\n\treq.Header.Set(\"X-Encore-Trace-Version\", strconv.Itoa(int(trace2.CurrentVersion)))\n\treq.Header.Set(\"X-Encore-Trace-TimeAnchor\", string(ta))\n\tc.addAuthKey(req)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"http %s: %s\", resp.Status, body)\n\t}\n\treturn nil\n}\n\n// traceLogReader implements io.Reader by reading from a trace log.\ntype traceLogReader struct {\n\tlog  trace2.Logger\n\tdata []byte // current buffer\n\tdone bool\n}\n\nfunc (r *traceLogReader) Read(b []byte) (int, error) {\n\tr.readMoreIfNeeded()\n\t// Post-condition: we have data, or we're done (or both).\n\n\tif len(r.data) > 0 {\n\t\tn := copy(b, r.data)\n\t\tr.data = r.data[n:]\n\t\treturn n, nil\n\t} else {\n\t\treturn 0, io.EOF\n\t}\n}\n\n// IsDoneAndEmpty blocks until we have some trace data, and then\n// reports whether the trace is done and empty.\nfunc (r *traceLogReader) IsDoneAndEmpty() bool {\n\tr.readMoreIfNeeded()\n\treturn len(r.data) == 0 && r.done\n}\n\nfunc (r *traceLogReader) readMoreIfNeeded() {\n\tfor len(r.data) == 0 && !r.done {\n\t\tr.data, r.done = r.log.WaitAndClear()\n\t}\n\t// Post-condition: we have data, or we're done (or both).\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/reqtrack/impl.go",
    "content": "package reqtrack\n\nimport (\n\t\"sync/atomic\"\n\t_ \"unsafe\" // for go:linkname\n\n\tmodel2 \"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\ntype reqTrackImpl interface {\n\tget() *encoreG\n\tset(e *encoreG)\n}\n\n// encoreG tracks per-goroutine Encore-specific data.\n// This must match the definition in the encore-go runtime.\ntype encoreG struct {\n\t// op is the current operation the goroutine is a part of.\n\top *encoreOp\n\n\t// req is request-specific data defined in the Encore runtime.\n\treq *encoreReq\n\n\t// goctr is the per-op goroutine counter.\n\tgoctr uint32\n}\n\n// encoreOp represents an Encore operation.\n// This must match the definition in the encore-go runtime.\ntype encoreOp struct {\n\t// t is the RequestTracker this is part of.\n\tt *RequestTracker\n\n\t// start is the start time of the operation\n\tstart int64 // start time of trace from nanotime()\n\n\t// trace is the trace log; it is nil if the op is not traced.\n\ttrace atomic.Pointer[lazyTraceInit]\n\n\t// refs is the op refcount. It is 1 + number of requests\n\t// that reference this op (see doc comment above).\n\t// It is accessed atomically.\n\trefs int32\n\n\t// goidCtr is a per-operation goroutine counter, for telling\n\t// apart goroutines participating in the operation.\n\tgoidCtr uint32\n}\n\nfunc (op *encoreOp) beginTracing() {\n\top.trace.CompareAndSwap(nil, newLazyTrace(op.t))\n}\n\n// encoreReq represents an Encore API request.\ntype encoreReq struct {\n\t// spanID is the request span id.\n\tspanID model2.SpanID\n\t// data is request-specific data defined in the Encore runtime.\n\tdata *model2.Request\n}\n\n// beginOp begins a new Encore operation.\n// The trace parameter determines if the op is traced.\n//\n// It tags the current goroutine with the op.\n// It panics if the goroutine is already part of an op.\nfunc (t *RequestTracker) beginOp(trace bool) *encoreOp {\n\top := t.newOp(trace)\n\tt.tagG(op, nil)\n\treturn op\n}\n\n// newOp creates a new encoreOp.\nfunc (t *RequestTracker) newOp(trace bool) *encoreOp {\n\top := &encoreOp{\n\t\tt:     t,\n\t\tstart: nanotime(),\n\t\trefs:  1,\n\t}\n\tif trace && t.trace != nil {\n\t\top.beginTracing()\n\t}\n\treturn op\n}\n\n// encoreTagG tags the g as participating in op, and with req\n// as its request data.\n// It does not increment the ref count, which means req\n// must already be an active request.\n// g must not already be part of an op.\nfunc (t *RequestTracker) tagG(op *encoreOp, req *encoreReq) (goctr uint32) {\n\tif t.impl.get() != nil {\n\t\tpanic(\"encore.tagG: goroutine already part of another operation\")\n\t}\n\tgoctr = atomic.AddUint32(&op.goidCtr, 1)\n\tt.impl.set(&encoreG{\n\t\top:    op,\n\t\treq:   req,\n\t\tgoctr: goctr,\n\t})\n\treturn goctr\n}\n\n// finishOp marks an operation as finished\n// and unsets the operation tag on the g.\n// It must be part of an operation.\nfunc (t *RequestTracker) finishOp() {\n\te := t.impl.get()\n\tif e == nil {\n\t\tpanic(\"encore.finishOp: goroutine not in an operation\")\n\t}\n\te.op.decRef(false)\n\tt.impl.set(nil)\n}\n\n// incRef increases the op's refcount by one.\nfunc (op *encoreOp) incRef() int32 {\n\treturn atomic.AddInt32(&op.refs, 1)\n}\n\n// decRef decreases the op's refcount by one.\n// If it reaches zero and the op is traced, it sends off the trace.\nfunc (op *encoreOp) decRef(blockOnTraceSend bool) int32 {\n\tn := atomic.AddInt32(&op.refs, -1)\n\tif n == 0 {\n\t\tif trace := op.trace.Load(); trace != nil {\n\t\t\ttrace.MarkDone()\n\n\t\t\tif blockOnTraceSend {\n\t\t\t\ttrace.WaitForStreamSent()\n\t\t\t}\n\t\t}\n\t}\n\treturn n\n}\n\n// beginReq sets the request data for the current g,\n// and increases the ref count on the operation.\n// If the g is not part of an op, it creates a new op\n// that is bound to the request lifetime.\nfunc (t *RequestTracker) beginReq(data *model2.Request, trace bool) {\n\te := t.impl.get()\n\treq := &encoreReq{spanID: data.SpanID, data: data}\n\tif e == nil {\n\t\top := t.newOp(trace)\n\t\tt.tagG(op, req)\n\t\t// Don't increment the op refcount since it starts at one,\n\t\t// and this is not a standalone op.\n\t} else {\n\t\tif e.req != nil {\n\t\t\tpanic(\"encore.beginReq: request already running\")\n\t\t}\n\t\te.op.incRef()\n\t\te.req = req\n\n\t\t// Begin tracing if we haven't, already.\n\t\tif trace && e.op.trace.Load() == nil {\n\t\t\te.op.beginTracing()\n\t\t}\n\t}\n}\n\n// finishReq completes the request and decreases the\n// ref count on the operation.\n// The g must be processing a request.\nfunc (t *RequestTracker) finishReq(blockOnTraceSend bool) {\n\te := t.impl.get()\n\tif e == nil {\n\t\tpanic(\"encore.finishReq: goroutine not in an operation\")\n\t} else if e.req == nil {\n\t\tpanic(\"encore.finishReq: no current request\")\n\t}\n\te.op.decRef(blockOnTraceSend)\n\te.req = nil\n}\n\nfunc (t *RequestTracker) currentReq() (req *model2.Request, tr trace2.Logger, goctr uint32, svcNum uint16) {\n\tif g := t.impl.get(); g != nil {\n\t\tvar tr trace2.Logger\n\t\tif g.op != nil {\n\t\t\tif trace := g.op.trace.Load(); trace != nil {\n\t\t\t\ttr = trace.Logger()\n\t\t\t}\n\t\t}\n\t\tif g.req != nil {\n\t\t\treq = g.req.data\n\t\t\tsvcNum = req.SvcNum\n\t\t}\n\t\treturn req, tr, g.goctr, svcNum\n\t}\n\treturn nil, nil, 0, 0\n}\n\n// encoreClearReq clears request data from the running g\n// without decrementing the ref count.\n// The g must be processing a request.\nfunc (t *RequestTracker) clearReq() {\n\te := t.impl.get()\n\tif e == nil {\n\t\tpanic(\"encore.replaceReq: goroutine not in an operation\")\n\t} else if e.req == nil {\n\t\tpanic(\"encore.replaceReq: no current request\")\n\t}\n\te.req = nil\n}\n\n//go:linkname nanotime runtime.nanotime\nfunc nanotime() int64\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/reqtrack/impl_app.go",
    "content": "//go:build encore_app\n\npackage reqtrack\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync/atomic\"\n\t_ \"unsafe\" // for go:linkname\n)\n\nfunc newImpl() reqTrackImpl {\n\treturn appImpl{}\n}\n\ntype appImpl struct{}\n\nvar _ reqTrackImpl = appImpl{}\n\nfunc (appImpl) get() *encoreG {\n\treturn getEncoreG()\n}\n\nfunc (appImpl) set(val *encoreG) {\n\tsetEncoreG(val)\n}\n\n// getEncoreG gets the encore data for the current g, or nil.\n//\n//go:linkname getEncoreG runtime.getEncoreG\nfunc getEncoreG() *encoreG\n\n// setEncoreG sets the encore data for the current g to val.\n//\n//go:linkname setEncoreG runtime.setEncoreG\nfunc setEncoreG(val *encoreG)\n\n//go:linkname startEncoreG runtime.startEncoreG\nfunc startEncoreG(src *encoreG) *encoreG {\n\tif src == nil {\n\t\treturn nil\n\t}\n\n\tgoctr := atomic.AddUint32(&src.op.goidCtr, 1)\n\tdst := &encoreG{\n\t\top:    src.op,\n\t\treq:   src.req,\n\t\tgoctr: goctr,\n\t}\n\n\treturn dst\n}\n\n//go:linkname exitEncoreG runtime.exitEncoreG\nfunc exitEncoreG(e *encoreG) {}\n\n//go:linkname beginHTTPRoundTrip net/http.encoreBeginRoundTrip\nfunc beginHTTPRoundTrip(req *http.Request) (context.Context, error) {\n\tg := getEncoreG()\n\tif g == nil || g.req == nil || !g.req.data.Traced {\n\t\treturn req.Context(), nil\n\t} else if req.URL == nil {\n\t\treturn nil, fmt.Errorf(\"http: nil Request.URL\")\n\t}\n\n\tif trace := g.op.trace.Load(); trace != nil {\n\t\treturn trace.Logger().HTTPBeginRoundTrip(req, g.req.data, g.goctr)\n\t}\n\n\treturn req.Context(), nil\n}\n\n//go:linkname finishHTTPRoundTrip net/http.encoreFinishRoundTrip\nfunc finishHTTPRoundTrip(req *http.Request, resp *http.Response, err error) {\n\tif g := getEncoreG(); g != nil && g.req != nil {\n\t\tif trace := g.op.trace.Load(); trace != nil {\n\t\t\ttrace.Logger().HTTPCompleteRoundTrip(req, resp, g.goctr, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/reqtrack/impl_noapp.go",
    "content": "//go:build !encore_app\n\npackage reqtrack\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"sync\"\n)\n\nfunc newImpl() reqTrackImpl {\n\treturn &noappImpl{\n\t\tgmap: make(map[int64]*encoreG),\n\t}\n}\n\ntype noappImpl struct {\n\tmu   sync.Mutex\n\tgmap map[int64]*encoreG\n}\n\nvar _ reqTrackImpl = (*noappImpl)(nil)\n\nfunc (i *noappImpl) get() *encoreG {\n\tid := goroutineID()\n\n\ti.mu.Lock()\n\tdefer i.mu.Unlock()\n\treturn i.gmap[id]\n}\n\nfunc (i *noappImpl) set(val *encoreG) {\n\tid := goroutineID()\n\n\ti.mu.Lock()\n\tdefer i.mu.Unlock()\n\ti.gmap[id] = val\n}\n\n// The below code snippet is copied from go4.org/syncutil/syncdebug.\n//\n// Copyright 2013 The Perkeep Authors\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//      http://www.apache.org/licenses/LICENSE-2.0\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nconst stackBufSize = 16 << 20\n\nvar stackBuf = make(chan []byte, 8)\n\nfunc getBuf() []byte {\n\tselect {\n\tcase b := <-stackBuf:\n\t\treturn b[:stackBufSize]\n\tdefault:\n\t\treturn make([]byte, stackBufSize)\n\t}\n}\n\nfunc putBuf(b []byte) {\n\tselect {\n\tcase stackBuf <- b:\n\tdefault:\n\t}\n}\n\nvar goroutineSpace = []byte(\"goroutine \")\n\nfunc goroutineID() int64 {\n\tb := getBuf()\n\tdefer putBuf(b)\n\tb = b[:runtime.Stack(b, false)]\n\t// Parse the 4707 out of \"goroutine 4707 [\"\n\tb = bytes.TrimPrefix(b, goroutineSpace)\n\ti := bytes.IndexByte(b, ' ')\n\tif i < 0 {\n\t\tpanic(fmt.Sprintf(\"No space found in %q\", b))\n\t}\n\tb = b[:i]\n\t// nosemgrep\n\tn, err := strconv.ParseUint(string(b), 10, 64)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Failed to parse goroutine ID out of %q: %v\", b, err))\n\t}\n\treturn int64(n)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/reqtrack/reqtrack.go",
    "content": "package reqtrack\n\nimport (\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/traceprovider\"\n)\n\n// New creates a new RequestTracker.\n//\n// If traceProvider is nil no traces are generated.\n// If streamer is nil no traces are streamed to the platform.\nfunc New(rootLogger zerolog.Logger, streamer TraceStreamer, traceProvider traceprovider.Factory) *RequestTracker {\n\tif streamer == nil {\n\t\tstreamer = &noopTraceStreamer{}\n\t}\n\n\treturn &RequestTracker{\n\t\tstreamer:   streamer,\n\t\timpl:       newImpl(),\n\t\ttrace:      traceProvider,\n\t\trootLogger: rootLogger,\n\t}\n}\n\ntype RequestTracker struct {\n\tstreamer   TraceStreamer\n\timpl       reqTrackImpl\n\ttrace      traceprovider.Factory // nil if tracing is not enabled\n\trootLogger zerolog.Logger\n}\n\nfunc (t *RequestTracker) BeginOperation() {\n\tt.beginOp(false)\n}\n\nfunc (t *RequestTracker) FinishOperation() {\n\tt.finishOp()\n}\n\nfunc (t *RequestTracker) BeginRequest(req *model.Request) {\n\tif prev, _, _, _ := t.currentReq(); prev != nil {\n\t\tcopyReqInfoFromParent(req, prev)\n\t\tt.clearReq()\n\t}\n\tt.beginReq(req, req.Traced)\n}\n\n// copyReqInfoFromParent copies over relevant request from the parent request.\n// If the relevant fields are already set on next they are not copied over.\nfunc copyReqInfoFromParent(next, prev *model.Request) {\n\tif prevData, nextData := prev.RPCData, next.RPCData; prevData != nil && nextData != nil {\n\t\tif nextData.UserID == \"\" {\n\t\t\tnextData.UserID = prevData.UserID\n\t\t}\n\t\tif nextData.AuthData == nil {\n\t\t\tnextData.AuthData = prevData.AuthData\n\t\t}\n\t} else if nextData != nil && prev.Test != nil {\n\t\tif nextData.UserID == \"\" {\n\t\t\tnextData.UserID = prev.Test.UserID\n\t\t}\n\t\tif nextData.AuthData == nil {\n\t\t\tnextData.AuthData = prev.Test.AuthData\n\t\t}\n\t}\n\n\tif !prev.TraceID.IsZero() {\n\t\tnext.TraceID = prev.TraceID\n\t}\n\tif next.ParentSpanID.IsZero() {\n\t\tnext.ParentSpanID = prev.SpanID\n\t}\n\tif next.ParentTraceID.IsZero() {\n\t\tnext.ParentTraceID = prev.ParentTraceID\n\t}\n\tif next.ExtCorrelationID == \"\" {\n\t\tnext.ExtCorrelationID = prev.ExtCorrelationID\n\t}\n\tif next.Test == nil {\n\t\tnext.Test = prev.Test\n\t}\n\tnext.Traced = prev.Traced\n}\n\nfunc (t *RequestTracker) FinishRequest(blockOnTraceSend bool) {\n\tt.finishReq(blockOnTraceSend)\n}\n\ntype Current struct {\n\tReq    *model.Request // can be nil\n\tTrace  trace2.Logger  // can be nil\n\tGoctr  uint32         // zero if Req == nil && Trace == nil\n\tSvcNum uint16         // 0 if not in a service\n}\n\nfunc (t *RequestTracker) Current() Current {\n\treq, tr, goid, svc := t.currentReq()\n\treturn Current{req, tr, goid, svc}\n}\n\nfunc (t *RequestTracker) Logger() *zerolog.Logger {\n\tif curr := t.Current(); curr.Req != nil && curr.Req.Logger != nil {\n\t\treturn curr.Req.Logger\n\t}\n\treturn &t.rootLogger\n}\n\nfunc (t *RequestTracker) TracingEnabled() bool {\n\treturn t.trace != nil\n}\n\nfunc (t *RequestTracker) SampleTrace(service, endpoint string) bool {\n\treturn t.trace != nil && t.trace.SampleTrace(service, endpoint)\n}\n\nfunc (t *RequestTracker) SamplePubSub(service, topic, subscription string) bool {\n\treturn t.trace != nil && t.trace.SamplePubSub(service, topic, subscription)\n}\n\nfunc (t *RequestTracker) SampleDefault() bool {\n\treturn t.trace != nil && t.trace.SampleDefault()\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/reqtrack/singleton.go",
    "content": "//go:build encore_app\n\npackage reqtrack\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/platform\"\n\t\"encore.dev/appruntime/shared/traceprovider\"\n)\n\nvar Singleton *RequestTracker\n\nfunc init() {\n\tvar traceFactory traceprovider.Factory\n\ttracingEnabled := appconf.Runtime.TraceEndpoint != \"\" && len(appconf.Runtime.AuthKeys) > 0\n\tif tracingEnabled {\n\t\t// Use the new sampling config if set, otherwise fall back to the deprecated scalar rate.\n\t\tsamplingConfig := appconf.Runtime.TraceSamplingConfig\n\t\tif len(samplingConfig) == 0 && appconf.Runtime.TraceSamplingRate != nil {\n\t\t\tsamplingConfig = map[string]float64{\"_\": *appconf.Runtime.TraceSamplingRate}\n\t\t}\n\t\ttraceFactory = traceprovider.NewDefaultFactory(samplingConfig)\n\t}\n\n\tSingleton = New(logging.RootLogger, platform.Singleton, traceFactory)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/reqtrack/trace_stream.go",
    "content": "package reqtrack\n\nimport (\n\t\"sync\"\n\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\ntype TraceStreamer interface {\n\tStreamTrace(trace2.Logger) error\n}\n\ntype noopTraceStreamer struct{}\n\n// StreamTrace implements TraceStreamer by consuming the trace log\n// but not doing anything with it.\nfunc (noopTraceStreamer) StreamTrace(log trace2.Logger) error {\n\tfor {\n\t\t_, done := log.WaitAndClear()\n\t\tif done {\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc newLazyTrace(rt *RequestTracker) *lazyTraceInit {\n\tlog := rt.trace.NewLogger()\n\treturn &lazyTraceInit{rt: rt, log: log}\n}\n\n// lazyTraceInit is a lazily initialized trace logger.\n// It is used to defer trace streaming until the trace is actually used.\ntype lazyTraceInit struct {\n\trt       *RequestTracker\n\tlog      trace2.Logger\n\tinitOnce sync.Once\n\tsent     sync.WaitGroup\n}\n\nfunc (l *lazyTraceInit) MarkDone() {\n\tl.log.MarkDone()\n\tl.initStream()\n}\n\nfunc (l *lazyTraceInit) Logger() trace2.Logger {\n\tl.initStream()\n\treturn l.log\n}\n\nfunc (l *lazyTraceInit) WaitForStreamSent() {\n\tl.sent.Wait()\n}\n\nfunc (l *lazyTraceInit) initStream() {\n\tl.initOnce.Do(func() {\n\t\tl.sent.Add(1)\n\n\t\tgo func() {\n\t\t\tdefer l.sent.Done()\n\n\t\t\tif err := l.rt.streamer.StreamTrace(l.log); err != nil {\n\t\t\t\tl.rt.rootLogger.Error().Err(err).Msg(\"failed to stream trace\")\n\t\t\t}\n\t\t}()\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/serde/utils.go",
    "content": "package serde\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\ntype JSONSerializer struct {\n\tbuffer        *bytes.Buffer\n\tstream        *jsoniter.Stream\n\twrittenFields int\n}\n\n// SerializeJSONFunc is used by generated code to serialize JSON.\nfunc SerializeJSONFunc(cfg jsoniter.API, fn func(serializer *JSONSerializer)) ([]byte, error) {\n\ts := &JSONSerializer{}\n\ts.buffer = new(bytes.Buffer)\n\ts.stream = jsoniter.NewStream(cfg, s.buffer, 1024)\n\ts.stream.WriteObjectStart()\n\tfn(s)\n\ts.stream.WriteObjectEnd()\n\terr := s.stream.Flush()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.buffer.Bytes(), s.stream.Error\n}\n\nfunc (s *JSONSerializer) WriteField(name string, val any, omitEmpty bool) {\n\tif omitEmpty && reflect.ValueOf(val).IsZero() {\n\t\treturn\n\t}\n\tif s.writtenFields > 0 {\n\t\ts.stream.WriteMore()\n\t}\n\ts.stream.WriteObjectField(name)\n\ts.stream.WriteVal(val)\n\ts.writtenFields++\n}\n\nfunc SerializeInputs(json jsoniter.API, inputs ...any) ([][]byte, error) {\n\toutputs := make([][]byte, len(inputs))\n\tfor i, input := range inputs {\n\t\tout, err := json.Marshal(input)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\toutputs[i] = out\n\t}\n\treturn outputs, nil\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/shutdown/shutdown.go",
    "content": "package shutdown\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/encoreenv\"\n\t\"encore.dev/appruntime/shared/health\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/shutdown\"\n)\n\ntype Handler func(p *Process) error\n\ntype Tracker struct {\n\tlogger zerolog.Logger\n\n\twatchSignals bool\n\n\ttimings processTimings\n\n\tinitiated chan struct{} // closed when graceful shutdown is initiated\n\tonce      sync.Once     // to trigger shutdown logic only once\n\n\tmu       sync.Mutex\n\thandlers []Handler\n}\n\nfunc NewTracker(runtime *config.Runtime, logger zerolog.Logger) *Tracker {\n\tt := &Tracker{\n\t\twatchSignals: runtime.EnvType != \"test\",\n\t\tinitiated:    make(chan struct{}),\n\t\ttimings:      timingsFromConfig(runtime),\n\t}\n\n\t// Determine the appropriate log level.\n\tswitch {\n\tcase encoreenv.Get(\"ENCORE_SHUTDOWN_TRACE\") != \"\":\n\t\tt.logger = logger.Level(zerolog.TraceLevel)\n\tcase runtime.EnvCloud == \"local\":\n\t\t// For local development only show errors\n\t\tt.logger = logger.Level(zerolog.ErrorLevel)\n\tdefault:\n\t\tt.logger = logger.Level(zerolog.InfoLevel)\n\t}\n\n\treturn t\n}\n\ntype processTimings struct {\n\t// keepAcceptingFor is the duration from the moment we receive a SIGTERM\n\t// after which we stop accepting new requests. However we will will\n\t// report being unhealthy to the load balancer immediately.\n\t//\n\t// This is needed as in a Kubernetes environment, the pod sent a SIGTERM\n\t// once it's replacement is ready, however it will take some time for that\n\t// to propagate to the load balancer. If we stop accepting requests immediately\n\t// we will have a period of time where the load balancer will still send\n\t// requests to the pod, which will be rejected. This will cause the load\n\t// balancer to report 502 errors.\n\t//\n\t// See: https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing#traffic_does_not_reach_endpoints\n\tkeepAcceptingFor time.Duration\n\n\t// cancelRunningTasksAfter is the duration (measured from shutdown initiation)\n\t// after which running tasks (outstanding API calls & PubSub messages) have\n\t// their contexts canceled.\n\tcancelRunningTasksAfter time.Duration\n\n\t// forceCloseTasksGrace is the duration (measured from when canceling running tasks)\n\t// after which the tasks are considered done, even if they're still running.\n\tforceCloseTasksGrace time.Duration\n\n\t// forceShutdownAfter is the duration (measured from shutdown initiation)\n\t// after which the shutdown process enters the \"force shutdown\" phase,\n\t// tearing down infrastructure resources.\n\tforceShutdownAfter time.Duration\n\n\t// forceShutdownGrace is the grace period after beginning the force shutdown\n\t// before the shutdown is marked as completed, causing the process to exit.\n\tforceShutdownGrace time.Duration\n}\n\nfunc timingsFromConfig(runtime *config.Runtime) processTimings {\n\t// Setup the config\n\tvar cfg config.GracefulShutdownTimings\n\tif runtime.GracefulShutdown != nil {\n\t\tcfg = *runtime.GracefulShutdown\n\t}\n\n\tt := processTimings{\n\t\tkeepAcceptingFor:     0,\n\t\tforceCloseTasksGrace: 1 * time.Second,\n\t\tforceShutdownGrace:   1 * time.Second,\n\t}\n\n\t// Handle the migration from ShutdownTimout to GracefulShutdown configuration\n\tvar totalTime time.Duration\n\tif cfg.Total == nil {\n\t\tt.forceShutdownAfter = runtime.ShutdownTimeout\n\t\tif t.forceShutdownAfter <= 0 {\n\t\t\tt.forceShutdownAfter = 5 * time.Second\n\t\t}\n\t\ttotalTime = runtime.ShutdownTimeout\n\t} else {\n\t\tt.forceShutdownAfter = *cfg.Total - t.forceShutdownGrace\n\t\tif t.forceShutdownAfter <= 0 {\n\t\t\tt.forceShutdownAfter = 500 * time.Millisecond\n\t\t}\n\t\ttotalTime = *cfg.Total\n\t}\n\n\t// Get the handler timeout\n\tif cfg.Handlers == nil {\n\t\tt.cancelRunningTasksAfter = t.forceShutdownAfter - t.forceCloseTasksGrace\n\t} else {\n\t\tt.cancelRunningTasksAfter = *cfg.Handlers\n\t}\n\tif t.cancelRunningTasksAfter < 0 {\n\t\tt.cancelRunningTasksAfter = 0\n\t}\n\n\tk8sGraceTimeSecs := encoreenv.Get(\"ENCORE_K8S_GRACE_TERMINATION_SECONDS\")\n\tif k8sGraceTimeSecs != \"\" {\n\t\tif graceSecs, err := strconv.Atoi(k8sGraceTimeSecs); err != nil {\n\t\t\tpanic(fmt.Sprintf(\"invalid value for ENCORE_K8S_GRACE_TERMINATION_SECONDS (sepected an interger): %s\", err))\n\t\t} else {\n\t\t\t// If we know what the grace termination is for the kubernetes pods, we want to keep accepting new traffic\n\t\t\t// for almost all of that duration - minus what the Encore runtime needs to perform a graceful shutdown.\n\t\t\t//\n\t\t\t// We'll immediately report a health failure when SIGTERM is received, however we'll still accept new\n\t\t\t// traffic as we wait for routers and load balancers to update have propergated that we're trying\n\t\t\t// to cleanly shutdown.\n\t\t\tt.keepAcceptingFor = (time.Duration(graceSecs) * time.Second) - totalTime\n\t\t\tif t.keepAcceptingFor < 0 {\n\t\t\t\tt.keepAcceptingFor = 0\n\t\t\t}\n\t\t}\n\t}\n\n\treturn t\n}\n\n// WatchForShutdownSignals watches for shutdown signals (SIGTERM, SIGINT)\n// and triggers the graceful shutdown when such a signal is received.\nfunc (t *Tracker) WatchForShutdownSignals() {\n\tif !t.watchSignals {\n\t\treturn\n\t}\n\n\tgracefulSignal := make(chan os.Signal, 1)\n\tsignal.Notify(gracefulSignal, syscall.SIGTERM, syscall.SIGINT)\n\n\tgo func() {\n\t\tsignalReceived := <-gracefulSignal\n\t\tt.Shutdown(signalReceived, nil)\n\t}()\n}\n\n// RegisterShutdownHandler registers a shutdown handler that will be called when the app\n// is gracefully shutting down.\n//\n// The given context is closed when the graceful shutdown window is closed and it's\n// time to forcefully shut down. force.Deadline() can be inspected to learn when this\n// will happen in advance.\n//\n// The shutdown is cooperative: the process will not exit until all shutdown hooks\n// have returned, unless the process is forcefully killed by a signal (which may happen\n// in certain cloud environments if the graceful shutdown takes longer than its timeout).\n//\n// If t is nil this function is a no-op.\nfunc (t *Tracker) RegisterShutdownHandler(fn Handler) {\n\tif t == nil {\n\t\treturn\n\t}\n\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\tt.handlers = append(t.handlers, fn)\n}\n\n// ShutdownInitiated reports whether graceful shutdown has been initiated.\nfunc (t *Tracker) ShutdownInitiated() bool {\n\tselect {\n\tcase <-t.initiated:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc functionName(fn any) (rtn string) {\n\tdefer func() {\n\t\tif r := recover(); r != nil && rtn == \"\" {\n\t\t\trtn = fmt.Sprintf(\"<panic getting function name: %v>\", r)\n\t\t}\n\t}()\n\n\treturn strings.TrimSuffix(runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name(), \"-fm\")\n}\n\n// HealthCheck returns an health check failure once a SIGTERM has been received.\n//\n// This is to allow load balancers to detect this instance is shutting down\n// and should not be routed to for new traffic.\nfunc (t *Tracker) HealthCheck(_ context.Context) []health.CheckResult {\n\tvar reportError error\n\tif t.ShutdownInitiated() {\n\t\treportError = errors.New(\"SIGTERM has been received, graceful shutdown started\")\n\t}\n\n\treturn []health.CheckResult{{\n\t\tName: \"shutdown-signal-monitoring\",\n\t\tErr:  reportError,\n\t}}\n}\n\n// Shutdown triggers the shutdown logic.\n// If it has already been triggered, it does nothing and returns immediately.\nfunc (t *Tracker) Shutdown(reasonSignal os.Signal, reasonError error) {\n\tt.once.Do(func() {\n\t\tclose(t.initiated)\n\n\t\tif reasonError != nil {\n\t\t\tt.logger.Err(reasonError).Msg(\"a fatal error occurred, initiating graceful shutdown\")\n\t\t} else if reasonSignal != nil {\n\t\t\tt.logger.Info().Str(\"signal\", reasonSignal.String()).Msg(\"got shutdown signal, initiating graceful shutdown\")\n\t\t} else {\n\t\t\tt.logger.Trace().Msg(\"initiating graceful shutdown\")\n\t\t}\n\n\t\t// If we received a SIGTERM and have a configured keepAcceptingFor duration\n\t\t// then log the fact we're going to continue accepting new requests and then\n\t\t// sleep for that time before begining to graceful shutdown.\n\t\tif reasonSignal == syscall.SIGTERM && t.timings.keepAcceptingFor > 0 {\n\t\t\tt.logger.Info().Str(\"duration\", t.timings.keepAcceptingFor.String()).Msg(\"continuing to accept requests for a short period of time to allow the load balancer to update\")\n\t\t\ttime.Sleep(t.timings.keepAcceptingFor)\n\t\t\tt.logger.Info().Msg(\"stopping to accept new requests and continuing graceful shutdown\")\n\t\t}\n\n\t\tp := t.beginShutdownProcess()\n\t\tgo t.runShutdownHandlers(p)\n\t\tt.exitOnCompletion(p)\n\t})\n}\n\n// Process mirrors [encore.dev/shutdown.Progress] but has internal fields\n// for controlling the behavior.\ntype Process struct {\n\tLog                       *zerolog.Logger\n\tOutstandingRequests       context.Context\n\tcancelOutstandingRequests context.CancelFunc\n\n\tOutstandingPubSubMessages       context.Context\n\tcancelOutstandingPubSubMessages context.CancelFunc\n\n\tOutstandingTasks       context.Context\n\tcancelOutstandingTasks context.CancelFunc\n\n\tForceCloseTasks       context.Context\n\tcancelForceCloseTasks context.CancelFunc\n\n\tServicesShutdownCompleted     context.Context\n\tmarkServicesShutdownCompleted context.CancelCauseFunc\n\n\tForceShutdown       context.Context\n\tcancelForceShutdown context.CancelFunc\n\n\thandlersCompleted     context.Context\n\tmarkHandlersCompleted context.CancelCauseFunc\n\n\t// ShutdownCompleted is closed when all shutdown hooks have returned.\n\tShutdownCompleted     context.Context\n\tmarkShutdownCompleted context.CancelCauseFunc\n}\n\n// cleanShutdown is a sentinel error used by the shutdown logic to indicate\n// a clean shutdown, via context.Cause.\nvar cleanShutdown = errors.New(\"clean shutdown\")\n\nfunc (t *Tracker) beginShutdownProcess() *Process {\n\tstart := time.Now()\n\n\ttt := t.timings\n\toutstandingTasks, cancelOutstandingTasks := context.WithDeadline(context.Background(), start.Add(tt.cancelRunningTasksAfter+tt.forceCloseTasksGrace))\n\toutstandingRequests, cancelOutstandingRequests := context.WithCancel(outstandingTasks)\n\toutstandingPubSubMessages, cancelOutstandingPubSubMessages := context.WithCancel(outstandingTasks)\n\n\tforceCloseTasks, cancelForceCloseTasks := context.WithDeadline(outstandingTasks, start.Add(tt.cancelRunningTasksAfter))\n\n\tforceShutdown, cancelForceShutdown := context.WithDeadline(context.Background(), start.Add(tt.forceShutdownAfter))\n\n\tserviceShutdownCompleted, cancelServiceShutdownCompleted := context.WithCancelCause(context.Background())\n\thandlersCompleted, cancelHandlersCompleted := context.WithCancelCause(context.Background())\n\n\tshutdownCompleted, cancelShutdownCompleted := context.WithCancelCause(context.Background())\n\n\t// Close the runningHandlers context when both\n\t// outstandingRequests and outstandingPubSubMessages are done.\n\tgo func() {\n\t\t<-outstandingRequests.Done()\n\t\t<-outstandingPubSubMessages.Done()\n\t\tcancelOutstandingTasks()\n\n\t\t// This is redundant (the context is derived from runningTasks),\n\t\t// but it makes the linter happy.\n\t\tcancelForceCloseTasks()\n\t}()\n\n\t// Cancel forceShutdown early if running tasks and handlers complete.\n\tgo func() {\n\t\t<-outstandingTasks.Done()\n\t\t<-handlersCompleted.Done()\n\t\tcancelForceShutdown()\n\t}()\n\n\t// Mark the shutdown completed.\n\tgo func() {\n\t\t<-forceShutdown.Done()\n\t\t// When forceShutdown is done, see if it was due to reaching the deadline (unclean shutdown)\n\t\t// or if we canceled the context early (clean shutdown).\n\t\tif errors.Is(forceShutdown.Err(), context.Canceled) {\n\t\t\tcancelShutdownCompleted(cleanShutdown)\n\t\t\treturn\n\t\t} else {\n\t\t\t// We reached the deadline. The ForceShutdown context was canceled just now,\n\t\t\t// so give it another second to let the shutdown handlers finish.\n\t\t\ttimeout, cancel := context.WithTimeout(handlersCompleted, tt.forceShutdownGrace)\n\t\t\tdefer cancel()\n\t\t\t<-timeout.Done()\n\n\t\t\tif errors.Is(timeout.Err(), context.Canceled) {\n\t\t\t\t// The handlers did eventually complete, so this is a clean shutdown.\n\t\t\t\tcancelShutdownCompleted(cleanShutdown)\n\t\t\t} else {\n\t\t\t\tcancelShutdownCompleted(timeout.Err())\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn &Process{\n\t\tLog:                       &t.logger,\n\t\tOutstandingRequests:       outstandingRequests,\n\t\tcancelOutstandingRequests: cancelOutstandingRequests,\n\n\t\tOutstandingPubSubMessages:       outstandingPubSubMessages,\n\t\tcancelOutstandingPubSubMessages: cancelOutstandingPubSubMessages,\n\n\t\tOutstandingTasks:       outstandingTasks,\n\t\tcancelOutstandingTasks: cancelOutstandingTasks,\n\n\t\tForceCloseTasks:       forceCloseTasks,\n\t\tcancelForceCloseTasks: cancelForceCloseTasks,\n\n\t\tForceShutdown:       forceShutdown,\n\t\tcancelForceShutdown: cancelForceShutdown,\n\n\t\tServicesShutdownCompleted:     serviceShutdownCompleted,\n\t\tmarkServicesShutdownCompleted: cancelServiceShutdownCompleted,\n\n\t\thandlersCompleted:     handlersCompleted,\n\t\tmarkHandlersCompleted: cancelHandlersCompleted,\n\n\t\tShutdownCompleted:     shutdownCompleted,\n\t\tmarkShutdownCompleted: cancelShutdownCompleted,\n\t}\n}\n\n// Progress converts p into the public shutdown.Progress type.\nfunc (p *Process) Progress() shutdown.Progress {\n\treturn shutdown.Progress{\n\t\tOutstandingRequests:       p.OutstandingRequests,\n\t\tOutstandingPubSubMessages: p.OutstandingPubSubMessages,\n\t\tOutstandingTasks:          p.OutstandingTasks,\n\t\tForceCloseTasks:           p.ForceCloseTasks,\n\t\tForceShutdown:             p.ForceShutdown,\n\t}\n}\n\nfunc (p *Process) MarkOutstandingRequestsCompleted() {\n\tp.cancelOutstandingRequests()\n}\n\nfunc (p *Process) MarkOutstandingPubSubMessagesCompleted() {\n\tp.cancelOutstandingPubSubMessages()\n}\n\nfunc (p *Process) MarkServicesShutdownCompleted(err error) {\n\t// TODO change error type to capture where the service came from\n\tif err != nil {\n\t\tp.markServicesShutdownCompleted(err)\n\t} else {\n\t\tp.markServicesShutdownCompleted(cleanShutdown)\n\t}\n}\n\n// WasCleanShutdown reports whether the shutdown was clean.\n// Its return value is undefined before p.shutdownCompleted is closed.\nfunc (p *Process) WasCleanShutdown() bool {\n\treturn errors.Is(context.Cause(p.ShutdownCompleted), cleanShutdown)\n}\n\ntype shutdownError struct {\n\thandlerName string\n\terr         error\n}\n\nfunc (e shutdownError) Error() string {\n\treturn fmt.Sprintf(\"shutdown handler %q: %v\", e.handlerName, e.err)\n}\n\nfunc (e shutdownError) Unwrap() error {\n\treturn e.err\n}\n\ntype shutdownErrors struct {\n\terrors []error\n}\n\nfunc (e shutdownErrors) Unwrap() []error {\n\treturn e.errors\n}\n\nfunc (e shutdownErrors) Error() string {\n\tswitch len(e.errors) {\n\tcase 0:\n\t\treturn \"no shutdown errors\"\n\tcase 1:\n\t\treturn e.errors[0].Error()\n\tdefault:\n\t\tvar buf strings.Builder\n\t\tbuf.WriteString(\"multiple shutdown errors: \")\n\t\tfor i, err := range e.errors {\n\t\t\tif i > 0 {\n\t\t\t\tbuf.WriteString(\"; \")\n\t\t\t}\n\t\t\tbuf.WriteString(err.Error())\n\t\t}\n\t\treturn buf.String()\n\t}\n}\n\n// runShutdownHandlers runs the registered shutdown handlers.\nfunc (t *Tracker) runShutdownHandlers(p *Process) {\n\tvar (\n\t\tshutdownErrorMu sync.Mutex\n\t\tshutdownErrs    []error\n\t)\n\taddShutdownErr := func(err shutdownError) {\n\t\tshutdownErrorMu.Lock()\n\t\tdefer shutdownErrorMu.Unlock()\n\t\tshutdownErrs = append(shutdownErrs, err)\n\t}\n\n\t// Mark the handlers as completed when we're done.\n\tdefer func() {\n\t\tshutdownErrorMu.Lock()\n\t\terrList := shutdownErrs\n\t\tshutdownErrorMu.Unlock()\n\n\t\t// Determine the error to use.\n\t\tvar shutdownErr error\n\t\tif len(errList) > 0 {\n\t\t\tshutdownErr = shutdownErrors{errors: errList}\n\t\t}\n\n\t\tt.logger.Trace().Err(shutdownErr).Msg(\"all shutdown hooks completed\")\n\n\t\tif shutdownErr != nil {\n\t\t\tp.markHandlersCompleted(shutdownErr)\n\t\t} else {\n\t\t\tp.markHandlersCompleted(cleanShutdown)\n\t\t}\n\t}()\n\n\tt.mu.Lock()\n\thandlers := t.handlers\n\tt.mu.Unlock()\n\n\tvar wg sync.WaitGroup\n\twg.Add(len(handlers))\n\n\tfor _, fn := range handlers {\n\t\tfn := fn\n\t\tname := functionName(fn)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\terr := shutdownError{\n\t\t\t\t\t\thandlerName: name,\n\t\t\t\t\t\terr:         errs.B().Msgf(\"panic: %s\", r).Err(),\n\t\t\t\t\t}\n\t\t\t\t\taddShutdownErr(err)\n\t\t\t\t\tt.logger.Err(err).Interface(\"panic\", r).Msg(\"panic encountered during shutdown hook\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tdefer t.logger.Trace().Str(\"hook\", name).Msg(\"shutdown hook completed\")\n\t\t\tt.logger.Trace().Str(\"hook\", name).Msg(\"running shutdown hook...\")\n\t\t\tif err := fn(p); err != nil {\n\t\t\t\tshutdownErr := shutdownError{handlerName: name, err: err}\n\t\t\t\tt.logger.Error().Err(shutdownErr).Str(\"hook\", name).Msg(\"shutdown handler returned an error\")\n\t\t\t\taddShutdownErr(shutdownErr)\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\n// exitOnCompletion exits the process when the shutdown is completed.\nfunc (t *Tracker) exitOnCompletion(p *Process) {\n\t<-p.ShutdownCompleted.Done()\n\n\tif p.WasCleanShutdown() {\n\t\tt.logger.Trace().Msg(\"graceful shutdown completed\")\n\t\tos.Exit(0)\n\t} else {\n\t\tt.logger.Trace().Msg(\"graceful shutdown window closed, forcing shutdown\")\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/shutdown/singleton.go",
    "content": "//go:build encore_app\n\npackage shutdown\n\nimport (\n\t\"encore.dev/appruntime/shared/health\"\n\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n)\n\nvar Singleton *Tracker\n\nfunc init() {\n\tSingleton = NewTracker(appconf.Runtime, logging.RootLogger)\n\thealth.Singleton.Register(Singleton)\n\tSingleton.WatchForShutdownSignals()\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/syncutil/once.go",
    "content": "/*\nCopyright 2014 The Perkeep Authors\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage syncutil\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\n// A Once will perform a successful action exactly once.\n//\n// Unlike a sync.Once, this Once's func returns an error\n// and is re-armed on failure.\ntype Once struct {\n\tm    sync.Mutex\n\tdone uint32\n}\n\n// Do calls the function f if and only if Do has not been invoked\n// without error for this instance of Once.  In other words, given\n//\n//\tvar once Once\n//\n// if once.Do(f) is called multiple times, only the first call will\n// invoke f, even if f has a different value in each invocation unless\n// f returns an error.  A new instance of Once is required for each\n// function to execute.\n//\n// Do is intended for initialization that must be run exactly once.  Since f\n// is niladic, it may be necessary to use a function literal to capture the\n// arguments to a function to be invoked by Do:\n//\n//\terr := config.once.Do(func() error { return config.init(filename) })\nfunc (o *Once) Do(f func() error) error {\n\tif atomic.LoadUint32(&o.done) == 1 {\n\t\treturn nil\n\t}\n\t// Slow-path.\n\to.m.Lock()\n\tdefer o.m.Unlock()\n\tvar err error\n\tif o.done == 0 {\n\t\terr = f()\n\t\tif err == nil {\n\t\t\tatomic.StoreUint32(&o.done, 1)\n\t\t}\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/syncutil/once_test.go",
    "content": "package syncutil\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestOnce(t *testing.T) {\n\ttimesRan := 0\n\tf := func() error {\n\t\ttimesRan++\n\t\treturn nil\n\t}\n\n\tonce := Once{}\n\n\tconst numCalls = 10\n\tresults := make(chan error, numCalls)\n\n\tfor i := 0; i < numCalls; i++ {\n\t\tgo func() {\n\t\t\tresults <- once.Do(f)\n\t\t}()\n\t}\n\n\tfor i := 0; i < numCalls; i++ {\n\t\tif err := <-results; err != nil {\n\t\t\tt.Errorf(\"Expected no errors, got %v\", err)\n\t\t}\n\t}\n\n\tif timesRan != 1 {\n\t\tt.Errorf(\"Expected to run one time, ran %d\", timesRan)\n\t}\n}\n\n// TestOnceErroring verifies we retry on every error, but stop after\n// the first success.\nfunc TestOnceErroring(t *testing.T) {\n\ttimesRan := 0\n\tf := func() error {\n\t\ttimesRan++\n\t\tif timesRan < 3 {\n\t\t\treturn errors.New(\"retry\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tonce := Once{}\n\tconst numCalls = 10\n\tresults := make(chan error, numCalls)\n\n\tfor i := 0; i < numCalls; i++ {\n\t\tgo func() {\n\t\t\tresults <- once.Do(f)\n\t\t}()\n\t}\n\n\tnumErrs := 0\n\tfor i := 0; i < numCalls; i++ {\n\t\tif err := <-results; err != nil {\n\t\t\tnumErrs++\n\t\t}\n\t}\n\n\tif numErrs != 2 {\n\t\tt.Errorf(\"Expected two errors, got %d\", numErrs)\n\t}\n\n\tif timesRan != 3 {\n\t\tt.Errorf(\"Expected to run two times, ran %d\", timesRan)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/syncutil/syncutil.go",
    "content": "// Package syncutil is a vendored version of go4.org/syncutil.Once.\n\n/*\nCopyright 2014 The Perkeep Authors\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n     http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package syncutil provides various synchronization utilities.\npackage syncutil\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/testsupport/runtimehooks_app.go",
    "content": "//go:build encore_app\n\npackage testsupport\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t_ \"unsafe\" // for go:linkname\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\nvar Singleton = NewManager(appconf.Static, reqtrack.Singleton, logging.RootLogger)\n\nfunc isGeneratedWrapperTest(t *testing.T) bool {\n\t// A test with an empty name is the generated wrapper test that Go adds around all the users tests.\n\t// we don't want to treat this as a real test, so we ignore it.\n\treturn t.Name() == \"\"\n}\n\n//go:linkname encoreStartTest testing.encoreStartTest\nfunc encoreStartTest(t *testing.T, fn func(t *testing.T)) {\n\tif isGeneratedWrapperTest(t) {\n\t\treturn\n\t}\n\tSingleton.StartTest(t, fn)\n}\n\n//go:linkname encorePauseTest testing.encorePauseTest\nfunc encorePauseTest(t *testing.T) {\n\tif isGeneratedWrapperTest(t) {\n\t\treturn\n\t}\n\tSingleton.PauseTest(t)\n}\n\n//go:linkname encoreResumeTest testing.encoreResumeTest\nfunc encoreResumeTest(t *testing.T) {\n\tif isGeneratedWrapperTest(t) {\n\t\treturn\n\t}\n\tSingleton.ResumeTest(t)\n}\n\n//go:linkname encoreEndTest testing.encoreEndTest\nfunc encoreEndTest(t *testing.T) {\n\tif isGeneratedWrapperTest(t) {\n\t\treturn\n\t}\n\tSingleton.EndTest(t)\n}\n\n//go:linkname encoreTestLog testing.encoreTestLog\nfunc encoreTestLog(line string, frameSkip int) {\n\tcurr := Singleton.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.LogMessage(trace2.LogMessageParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tLevel: model.LevelTrace,\n\n\t\t\t// Note all trace logs have 4 spaces added to every line, so we don't want \"trimspace\"\n\t\t\tMsg:    strings.TrimRight(line, \" \\n\\r\\t\"),\n\t\t\tStack:  stack.Build(frameSkip + 1),\n\t\t\tFields: nil,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/testsupport/testconfig.go",
    "content": "package testsupport\n\nimport (\n\t\"sync/atomic\"\n\n\t\"encore.dev/appruntime/exported/model\"\n)\n\ntype TestConfig = model.TestConfig\n\nvar (\n\tnextApiMockID atomic.Uint64\n)\n\nfunc newTestConfig(parent *model.TestConfig) *model.TestConfig {\n\treturn &model.TestConfig{\n\t\tParent:       parent,\n\t\tServiceMocks: make(map[string]model.ServiceMock),\n\t\tAPIMocks:     make(map[string]map[string]model.ApiMock),\n\t}\n}\n\n// walkConfig walks the test config hierarchy, starting from the given config, and calls the given function on each\n// until the function returns true.\n//\n// walkConfig takes care to lock and unlock the read mutex on the config hierarchy as it walks it.\nfunc walkConfig[T any](config *model.TestConfig, f func(*model.TestConfig) (value T, found bool)) (value T, found bool) {\n\texec := func(config *model.TestConfig) (value T, found bool) {\n\t\tconfig.Mu.RLock()\n\t\tdefer config.Mu.RUnlock()\n\t\treturn f(config)\n\t}\n\n\tfor config != nil {\n\t\tvalue, found := exec(config)\n\t\tif found {\n\t\t\treturn value, found\n\t\t}\n\n\t\tconfig = config.Parent\n\t}\n\n\treturn value, false\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/testsupport/testsupport.go",
    "content": "package testsupport\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\t_ \"unsafe\" // for go:linkname\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\ntype Manager struct {\n\tstatic         *config.Static\n\trt             *reqtrack.RequestTracker\n\trootLogger     zerolog.Logger\n\trootTestConfig *TestConfig\n\n\twd              string\n\ttestServiceOnce sync.Once\n\ttestService     string\n\ttestServiceNum  uint16\n}\n\nfunc NewManager(static *config.Static, rt *reqtrack.RequestTracker, rootLogger zerolog.Logger) *Manager {\n\twd, _ := os.Getwd()\n\treturn &Manager{static: static, rt: rt, rootLogger: rootLogger, wd: wd, rootTestConfig: newTestConfig(nil)}\n}\n\n// StartTest is called when a test starts running. This allows Encore's testing framework to\n// isolate behavior between different tests on global state.\nfunc (mgr *Manager) StartTest(t *testing.T, fn func(*testing.T)) {\n\tvar parent *model.Request\n\tparentConfig := mgr.rootTestConfig\n\n\t// Convert the fn pointer to a file/line number if possible\n\t// This is useful for debugging tests that are hanging\n\tvar testFile string\n\tvar testLine int\n\tfnPtr := reflect.ValueOf(fn).Pointer()\n\tif f := runtime.FuncForPC(fnPtr); f != nil {\n\t\ttestFile, testLine = f.FileLine(fnPtr)\n\n\t\tif mgr.static.TestAppRootPath != \"\" {\n\t\t\ttestFile = strings.TrimPrefix(testFile, mgr.static.TestAppRootPath+string(filepath.Separator))\n\t\t}\n\t}\n\n\tvar traceID model.TraceID\n\tvar parentSpanID model.SpanID\n\tif curr := mgr.rt.Current(); curr.Req != nil {\n\t\tparent = curr.Req\n\t\ttraceID = curr.Req.TraceID\n\t\tparentSpanID = curr.Req.ParentSpanID\n\t\tparentConfig = curr.Req.Test.Config\n\t}\n\n\tspanID, err := model.GenSpanID()\n\tif err != nil {\n\t\tt.Fatalf(\"encoreStartTest: failed to generate span ID: %v\", err)\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tlogger := mgr.rootLogger.With().Str(\"test\", t.Name()).Logger()\n\n\ttestService, svcNum := mgr.TestService()\n\n\tif traceID.IsZero() {\n\t\tid, err := model.GenTraceID()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"encoreStartTest: failed to generate trace ID: %v\", err)\n\t\t}\n\t\ttraceID = id\n\t}\n\n\treq := &model.Request{\n\t\tType:         model.Test,\n\t\tTraceID:      traceID,\n\t\tSpanID:       spanID,\n\t\tParentSpanID: parentSpanID,\n\t\tStart:        time.Now(),\n\t\tTraced:       mgr.rt.TracingEnabled(),\n\t\tTest: &model.TestData{\n\t\t\tCtx:              ctx,\n\t\t\tCancel:           cancel,\n\t\t\tCurrent:          t,\n\t\t\tParent:           parent,\n\t\t\tService:          testService,\n\t\t\tTestFile:         testFile,\n\t\t\tTestLine:         uint32(testLine),\n\t\t\tConfig:           newTestConfig(parentConfig),\n\t\t\tServiceInstances: make(map[string]any),\n\t\t},\n\t\tLogger: &logger,\n\t\tSvcNum: svcNum,\n\t}\n\tmgr.rt.BeginRequest(req)\n\tif curr := mgr.rt.Current(); curr.Trace != nil {\n\t\tcurr.Trace.TestSpanStart(req, curr.Goctr)\n\t}\n}\n\n// PauseTest is called when a test is paused. This allows Encore's testing framework to\n// isolate behavior between different tests on global state.\nfunc (mgr *Manager) PauseTest(t *testing.T) {\n}\n\n// ResumeTest is called when a test is resumed after being paused. This allows Encore's testing framework to clear down any state from the test\n// and to perform any assertions on that state that it needs to.\nfunc (mgr *Manager) ResumeTest(t *testing.T) {\n\treq := mgr.rt.Current().Req\n\tif req == nil || req.Test == nil {\n\t\tpanic(\"encoreResumeTest: no active test\")\n\t}\n\tif req.Test.Current != t {\n\t\tpanic(\"encoreResumeTest: active test is not this test\")\n\t}\n\n\t// Tests get paused when they call `t.Parallel()` and are held there until the parent test\n\t// completes, at which case all parallel child tests are resumed.\n\t// As such, we assume that the test actually \"starts\" from now\n\treq.Start = time.Now()\n}\n\n// EndTest is called when a test ends. This allows Encore's testing framework to clear down any state from the test\n// and to perform any assertions on that state that it needs to.\nfunc (mgr *Manager) EndTest(t *testing.T) {\n\tcurr := mgr.rt.Current()\n\treq := curr.Req\n\tif req == nil || req.Test == nil {\n\t\tpanic(\"encoreEndTest: no active test\")\n\t}\n\tif req.Test.Current != t {\n\t\tpanic(\"encoreEndTest: active test is not this test\")\n\t}\n\ttestData := req.Test\n\n\t// Wait for any async code to finish up-to 30 seconds\n\t// if any async code is still running after 30 seconds, we'll fail the test\n\tdone := make(chan struct{})\n\tgo func() {\n\t\ttestData.Wait.Wait()\n\t\tdone <- struct{}{}\n\t}()\n\tselect {\n\tcase <-time.After(30 * time.Second):\n\t\tt.Errorf(\"test timed out waiting for async code to finish after 30 seconds\")\n\t\tt.Fail()\n\n\t\t// Now cancel to context to try and force those go-routines to exit\n\t\ttestData.Cancel()\n\tcase <-done:\n\t}\n\n\t// Run end callbacks.\n\t(func() {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tt.Errorf(\"encore: internal error: panic occured running test end callback: %v\\n\\n%s\", err, debug.Stack())\n\t\t\t}\n\t\t}()\n\t\tfor _, cb := range testData.Config.EndCallbacks {\n\t\t\tcb(t)\n\t\t}\n\t})()\n\n\tif curr.Trace != nil {\n\t\tcurr.Trace.TestSpanEnd(trace2.TestSpanEndParams{\n\t\t\tEventParams: trace2.EventParams{TraceID: req.TraceID, SpanID: req.SpanID},\n\t\t\tReq:         req,\n\t\t\tFailed:      t.Failed(),\n\t\t\tSkipped:     t.Skipped(),\n\t\t})\n\t}\n\n\tmgr.rt.FinishRequest(true)\n}\n\n// CurrentTest returns the currently running test.\n// If no test is running, it panics.\nfunc (mgr *Manager) CurrentTest() *testing.T {\n\ttd := mgr.current()\n\treturn td.Current\n}\n\n// current returns the currently running test data.\n// If no test is running, it panics.\nfunc (mgr *Manager) current() *model.TestData {\n\treq := mgr.rt.Current().Req\n\tif req == nil || req.Test == nil {\n\t\tpanic(\"CurrentTest: no active test\")\n\t}\n\treturn req.Test\n}\n\nfunc (mgr *Manager) TestService() (svcName string, svcNum uint16) {\n\tmgr.testServiceOnce.Do(func() {\n\t\tfor svc, path := range mgr.static.TestServiceMap {\n\t\t\tif mgr.wd == path || strings.HasPrefix(mgr.wd, path+string(filepath.Separator)) {\n\t\t\t\tmgr.testService = svc\n\t\t\t\tmgr.testServiceNum = uint16(slices.Index(mgr.static.BundledServices, svc) + 1)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t})\n\n\treturn mgr.testService, mgr.testServiceNum\n}\n\n// RunAsyncCodeInTest allows us to trigger code to run asynchronously in a test\n// to emulate real world async race scenarios.\n//\n// This works by running `f` in a new Go routine which can process the \"request\"\n// however, the test will not be able to finish until the go runtime exits\nfunc (mgr *Manager) RunAsyncCodeInTest(t *testing.T, f func(ctx context.Context)) {\n\ttd := mgr.current()\n\tif td.Current != t {\n\t\tpanic(\"RunAsyncCodeInTest: active test is not this test\")\n\t}\n\ttd.Wait.Add(1)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tt.Errorf(\"panic occured: %v\\n\\n%s\", err, debug.Stack())\n\t\t\t\tt.Fail()\n\t\t\t}\n\n\t\t\ttd.Wait.Done()\n\t\t}()\n\n\t\tf(td.Ctx)\n\t}()\n}\n\n// currentConfig returns the current test config object\nfunc (mgr *Manager) currentConfig() *TestConfig {\n\treq := mgr.rt.Current().Req\n\tif req == nil || req.Test == nil {\n\t\treturn mgr.rootTestConfig\n\t}\n\n\tif req.Test.Config == nil {\n\t\tpanic(\"currentConfig: no active test config even though in test\")\n\t}\n\n\treturn req.Test.Config\n}\n\n// SetIsolatedServices sets whether isolated services should be enabled for the current test\nfunc (mgr *Manager) SetIsolatedServices(enabled bool) {\n\tcfg := mgr.currentConfig()\n\tcfg.Mu.Lock()\n\tdefer cfg.Mu.Unlock()\n\tcfg.IsolatedServices = &enabled\n}\n\n// GetIsolatedServices returns whether isolated services are enabled for the current test\nfunc (mgr *Manager) GetIsolatedServices() bool {\n\tresult, _ := walkConfig(mgr.currentConfig(), func(cfg *TestConfig) (value *bool, found bool) {\n\t\tvalue, found = cfg.IsolatedServices, cfg.IsolatedServices != nil\n\t\treturn\n\t})\n\n\tif result == nil {\n\t\treturn false\n\t}\n\treturn *result\n}\n\n// SetServiceMock allows us to set a mock for a service for the current test\nfunc (mgr *Manager) SetServiceMock(service string, mock any, runMiddleware bool) {\n\tservice = strings.TrimSpace(strings.ToLower(service))\n\n\tcfg := mgr.currentConfig()\n\tcfg.Mu.Lock()\n\tdefer cfg.Mu.Unlock()\n\tcfg.ServiceMocks[service] = model.ServiceMock{\n\t\tService:       mock,\n\t\tRunMiddleware: runMiddleware,\n\t}\n}\n\n// GetServiceMock allows us to get a mock for a service for the current test\n// or any parent tests - returning the lowest level mock available.\nfunc (mgr *Manager) GetServiceMock(service string) (model.ServiceMock, bool) {\n\tservice = strings.TrimSpace(strings.ToLower(service))\n\n\treturn walkConfig(mgr.currentConfig(), func(cfg *TestConfig) (value model.ServiceMock, found bool) {\n\t\tvalue, found = cfg.ServiceMocks[service]\n\t\treturn\n\t})\n}\n\n// SetAPIMock allows us to set a mock for an API for the current test\nfunc (mgr *Manager) SetAPIMock(service string, api string, mock any, runMiddleware bool) {\n\tservice = strings.TrimSpace(strings.ToLower(service))\n\tapi = strings.TrimSpace(strings.ToLower(api))\n\n\tcfg := mgr.currentConfig()\n\tcfg.Mu.Lock()\n\tdefer cfg.Mu.Unlock()\n\n\tif cfg.APIMocks[service] == nil {\n\t\tcfg.APIMocks[service] = make(map[string]model.ApiMock)\n\t}\n\tcfg.APIMocks[service][api] = model.ApiMock{\n\t\tID:            nextApiMockID.Add(1),\n\t\tFunction:      mock,\n\t\tRunMiddleware: runMiddleware,\n\t}\n}\n\n// GetAPIMock allows us to get a mock for an API for the current test\n// or any parent tests - returning the lowest level mock available.\nfunc (mgr *Manager) GetAPIMock(service string, api string) (model.ApiMock, bool) {\n\tservice = strings.TrimSpace(strings.ToLower(service))\n\tapi = strings.TrimSpace(strings.ToLower(api))\n\n\treturn walkConfig(mgr.currentConfig(), func(cfg *TestConfig) (value model.ApiMock, found bool) {\n\t\tif cfg.APIMocks[service] == nil {\n\t\t\treturn\n\t\t}\n\t\tvalue, found = cfg.APIMocks[service][api]\n\t\treturn\n\t})\n}\n\nfunc (mgr *Manager) AddEndCallback(fn func(t *testing.T)) {\n\tcfg := mgr.currentConfig()\n\tcfg.Mu.Lock()\n\tdefer cfg.Mu.Unlock()\n\tcfg.EndCallbacks = append(cfg.EndCallbacks, fn)\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/traceprovider/mock_trace/factory.go",
    "content": "package mock_trace\n\nimport (\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/traceprovider\"\n)\n\nfunc NewMockFactory(log *MockLogger) traceprovider.Factory {\n\treturn &mockFactory{log}\n}\n\ntype mockFactory struct {\n\tlog *MockLogger\n}\n\nfunc (f *mockFactory) NewLogger() trace2.Logger {\n\treturn f.log\n}\n\nfunc (f *mockFactory) SampleTrace(service, endpoint string) bool {\n\treturn true\n}\n\nfunc (f *mockFactory) SamplePubSub(service, topic, subscription string) bool {\n\treturn true\n}\n\nfunc (f *mockFactory) SampleDefault() bool {\n\treturn true\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/traceprovider/mock_trace/mock_trace.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: ./logger.go\n\n// Package mock_trace is a generated GoMock package.\npackage mock_trace\n\nimport (\n\tcontext \"context\"\n\thttp \"net/http\"\n\treflect \"reflect\"\n\ttime \"time\"\n\n\tmodel \"encore.dev/appruntime/exported/model\"\n\tstack \"encore.dev/appruntime/exported/stack\"\n\ttrace2 \"encore.dev/appruntime/exported/trace2\"\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockLogger is a mock of Logger interface.\ntype MockLogger struct {\n\tctrl     *gomock.Controller\n\trecorder *MockLoggerMockRecorder\n}\n\n// MockLoggerMockRecorder is the mock recorder for MockLogger.\ntype MockLoggerMockRecorder struct {\n\tmock *MockLogger\n}\n\n// NewMockLogger creates a new mock instance.\nfunc NewMockLogger(ctrl *gomock.Controller) *MockLogger {\n\tmock := &MockLogger{ctrl: ctrl}\n\tmock.recorder = &MockLoggerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockLogger) EXPECT() *MockLoggerMockRecorder {\n\treturn m.recorder\n}\n\n// Add mocks base method.\nfunc (m *MockLogger) Add(arg0 trace2.Event) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Add\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// Add indicates an expected call of Add.\nfunc (mr *MockLoggerMockRecorder) Add(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Add\", reflect.TypeOf((*MockLogger)(nil).Add), arg0)\n}\n\n// AuthSpanEnd mocks base method.\nfunc (m *MockLogger) AuthSpanEnd(params trace2.AuthSpanEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"AuthSpanEnd\", params)\n}\n\n// AuthSpanEnd indicates an expected call of AuthSpanEnd.\nfunc (mr *MockLoggerMockRecorder) AuthSpanEnd(params interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"AuthSpanEnd\", reflect.TypeOf((*MockLogger)(nil).AuthSpanEnd), params)\n}\n\n// AuthSpanStart mocks base method.\nfunc (m *MockLogger) AuthSpanStart(req *model.Request, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"AuthSpanStart\", req, goid)\n}\n\n// AuthSpanStart indicates an expected call of AuthSpanStart.\nfunc (mr *MockLoggerMockRecorder) AuthSpanStart(req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"AuthSpanStart\", reflect.TypeOf((*MockLogger)(nil).AuthSpanStart), req, goid)\n}\n\n// BodyStream mocks base method.\nfunc (m *MockLogger) BodyStream(arg0 trace2.BodyStreamParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BodyStream\", arg0)\n}\n\n// BodyStream indicates an expected call of BodyStream.\nfunc (mr *MockLoggerMockRecorder) BodyStream(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BodyStream\", reflect.TypeOf((*MockLogger)(nil).BodyStream), arg0)\n}\n\n// BucketDeleteObjectsEnd mocks base method.\nfunc (m *MockLogger) BucketDeleteObjectsEnd(arg0 trace2.BucketDeleteObjectsEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BucketDeleteObjectsEnd\", arg0)\n}\n\n// BucketDeleteObjectsEnd indicates an expected call of BucketDeleteObjectsEnd.\nfunc (mr *MockLoggerMockRecorder) BucketDeleteObjectsEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketDeleteObjectsEnd\", reflect.TypeOf((*MockLogger)(nil).BucketDeleteObjectsEnd), arg0)\n}\n\n// BucketDeleteObjectsStart mocks base method.\nfunc (m *MockLogger) BucketDeleteObjectsStart(arg0 trace2.BucketDeleteObjectsStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BucketDeleteObjectsStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// BucketDeleteObjectsStart indicates an expected call of BucketDeleteObjectsStart.\nfunc (mr *MockLoggerMockRecorder) BucketDeleteObjectsStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketDeleteObjectsStart\", reflect.TypeOf((*MockLogger)(nil).BucketDeleteObjectsStart), arg0)\n}\n\n// BucketListObjectsEnd mocks base method.\nfunc (m *MockLogger) BucketListObjectsEnd(arg0 trace2.BucketListObjectsEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BucketListObjectsEnd\", arg0)\n}\n\n// BucketListObjectsEnd indicates an expected call of BucketListObjectsEnd.\nfunc (mr *MockLoggerMockRecorder) BucketListObjectsEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketListObjectsEnd\", reflect.TypeOf((*MockLogger)(nil).BucketListObjectsEnd), arg0)\n}\n\n// BucketListObjectsStart mocks base method.\nfunc (m *MockLogger) BucketListObjectsStart(arg0 trace2.BucketListObjectsStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BucketListObjectsStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// BucketListObjectsStart indicates an expected call of BucketListObjectsStart.\nfunc (mr *MockLoggerMockRecorder) BucketListObjectsStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketListObjectsStart\", reflect.TypeOf((*MockLogger)(nil).BucketListObjectsStart), arg0)\n}\n\n// BucketObjectDownloadEnd mocks base method.\nfunc (m *MockLogger) BucketObjectDownloadEnd(arg0 trace2.BucketObjectDownloadEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BucketObjectDownloadEnd\", arg0)\n}\n\n// BucketObjectDownloadEnd indicates an expected call of BucketObjectDownloadEnd.\nfunc (mr *MockLoggerMockRecorder) BucketObjectDownloadEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketObjectDownloadEnd\", reflect.TypeOf((*MockLogger)(nil).BucketObjectDownloadEnd), arg0)\n}\n\n// BucketObjectDownloadStart mocks base method.\nfunc (m *MockLogger) BucketObjectDownloadStart(arg0 trace2.BucketObjectDownloadStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BucketObjectDownloadStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// BucketObjectDownloadStart indicates an expected call of BucketObjectDownloadStart.\nfunc (mr *MockLoggerMockRecorder) BucketObjectDownloadStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketObjectDownloadStart\", reflect.TypeOf((*MockLogger)(nil).BucketObjectDownloadStart), arg0)\n}\n\n// BucketObjectGetAttrsEnd mocks base method.\nfunc (m *MockLogger) BucketObjectGetAttrsEnd(arg0 trace2.BucketObjectGetAttrsEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BucketObjectGetAttrsEnd\", arg0)\n}\n\n// BucketObjectGetAttrsEnd indicates an expected call of BucketObjectGetAttrsEnd.\nfunc (mr *MockLoggerMockRecorder) BucketObjectGetAttrsEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketObjectGetAttrsEnd\", reflect.TypeOf((*MockLogger)(nil).BucketObjectGetAttrsEnd), arg0)\n}\n\n// BucketObjectGetAttrsStart mocks base method.\nfunc (m *MockLogger) BucketObjectGetAttrsStart(arg0 trace2.BucketObjectGetAttrsStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BucketObjectGetAttrsStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// BucketObjectGetAttrsStart indicates an expected call of BucketObjectGetAttrsStart.\nfunc (mr *MockLoggerMockRecorder) BucketObjectGetAttrsStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketObjectGetAttrsStart\", reflect.TypeOf((*MockLogger)(nil).BucketObjectGetAttrsStart), arg0)\n}\n\n// BucketObjectUploadEnd mocks base method.\nfunc (m *MockLogger) BucketObjectUploadEnd(arg0 trace2.BucketObjectUploadEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"BucketObjectUploadEnd\", arg0)\n}\n\n// BucketObjectUploadEnd indicates an expected call of BucketObjectUploadEnd.\nfunc (mr *MockLoggerMockRecorder) BucketObjectUploadEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketObjectUploadEnd\", reflect.TypeOf((*MockLogger)(nil).BucketObjectUploadEnd), arg0)\n}\n\n// BucketObjectUploadStart mocks base method.\nfunc (m *MockLogger) BucketObjectUploadStart(arg0 trace2.BucketObjectUploadStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BucketObjectUploadStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// BucketObjectUploadStart indicates an expected call of BucketObjectUploadStart.\nfunc (mr *MockLoggerMockRecorder) BucketObjectUploadStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BucketObjectUploadStart\", reflect.TypeOf((*MockLogger)(nil).BucketObjectUploadStart), arg0)\n}\n\n// CacheCallEnd mocks base method.\nfunc (m *MockLogger) CacheCallEnd(arg0 trace2.CacheCallEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"CacheCallEnd\", arg0)\n}\n\n// CacheCallEnd indicates an expected call of CacheCallEnd.\nfunc (mr *MockLoggerMockRecorder) CacheCallEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CacheCallEnd\", reflect.TypeOf((*MockLogger)(nil).CacheCallEnd), arg0)\n}\n\n// CacheCallStart mocks base method.\nfunc (m *MockLogger) CacheCallStart(arg0 trace2.CacheCallStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CacheCallStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// CacheCallStart indicates an expected call of CacheCallStart.\nfunc (mr *MockLoggerMockRecorder) CacheCallStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CacheCallStart\", reflect.TypeOf((*MockLogger)(nil).CacheCallStart), arg0)\n}\n\n// DBQueryEnd mocks base method.\nfunc (m *MockLogger) DBQueryEnd(arg0 trace2.EventParams, arg1 trace2.EventID, arg2 error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"DBQueryEnd\", arg0, arg1, arg2)\n}\n\n// DBQueryEnd indicates an expected call of DBQueryEnd.\nfunc (mr *MockLoggerMockRecorder) DBQueryEnd(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBQueryEnd\", reflect.TypeOf((*MockLogger)(nil).DBQueryEnd), arg0, arg1, arg2)\n}\n\n// DBQueryStart mocks base method.\nfunc (m *MockLogger) DBQueryStart(p trace2.DBQueryStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DBQueryStart\", p)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// DBQueryStart indicates an expected call of DBQueryStart.\nfunc (mr *MockLoggerMockRecorder) DBQueryStart(p interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBQueryStart\", reflect.TypeOf((*MockLogger)(nil).DBQueryStart), p)\n}\n\n// DBTransactionEnd mocks base method.\nfunc (m *MockLogger) DBTransactionEnd(arg0 trace2.DBTransactionEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"DBTransactionEnd\", arg0)\n}\n\n// DBTransactionEnd indicates an expected call of DBTransactionEnd.\nfunc (mr *MockLoggerMockRecorder) DBTransactionEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBTransactionEnd\", reflect.TypeOf((*MockLogger)(nil).DBTransactionEnd), arg0)\n}\n\n// DBTransactionStart mocks base method.\nfunc (m *MockLogger) DBTransactionStart(arg0 trace2.EventParams, arg1 stack.Stack) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DBTransactionStart\", arg0, arg1)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// DBTransactionStart indicates an expected call of DBTransactionStart.\nfunc (mr *MockLoggerMockRecorder) DBTransactionStart(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DBTransactionStart\", reflect.TypeOf((*MockLogger)(nil).DBTransactionStart), arg0, arg1)\n}\n\n// GetAndClear mocks base method.\nfunc (m *MockLogger) GetAndClear() ([]byte, bool) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetAndClear\")\n\tret0, _ := ret[0].([]byte)\n\tret1, _ := ret[1].(bool)\n\treturn ret0, ret1\n}\n\n// GetAndClear indicates an expected call of GetAndClear.\nfunc (mr *MockLoggerMockRecorder) GetAndClear() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetAndClear\", reflect.TypeOf((*MockLogger)(nil).GetAndClear))\n}\n\n// HTTPBeginRoundTrip mocks base method.\nfunc (m *MockLogger) HTTPBeginRoundTrip(httpReq *http.Request, req *model.Request, goid uint32) (context.Context, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"HTTPBeginRoundTrip\", httpReq, req, goid)\n\tret0, _ := ret[0].(context.Context)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// HTTPBeginRoundTrip indicates an expected call of HTTPBeginRoundTrip.\nfunc (mr *MockLoggerMockRecorder) HTTPBeginRoundTrip(httpReq, req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"HTTPBeginRoundTrip\", reflect.TypeOf((*MockLogger)(nil).HTTPBeginRoundTrip), httpReq, req, goid)\n}\n\n// HTTPCompleteRoundTrip mocks base method.\nfunc (m *MockLogger) HTTPCompleteRoundTrip(req *http.Request, resp *http.Response, goid uint32, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"HTTPCompleteRoundTrip\", req, resp, goid, err)\n}\n\n// HTTPCompleteRoundTrip indicates an expected call of HTTPCompleteRoundTrip.\nfunc (mr *MockLoggerMockRecorder) HTTPCompleteRoundTrip(req, resp, goid, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"HTTPCompleteRoundTrip\", reflect.TypeOf((*MockLogger)(nil).HTTPCompleteRoundTrip), req, resp, goid, err)\n}\n\n// LogMessage mocks base method.\nfunc (m *MockLogger) LogMessage(arg0 trace2.LogMessageParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"LogMessage\", arg0)\n}\n\n// LogMessage indicates an expected call of LogMessage.\nfunc (mr *MockLoggerMockRecorder) LogMessage(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"LogMessage\", reflect.TypeOf((*MockLogger)(nil).LogMessage), arg0)\n}\n\n// MarkDone mocks base method.\nfunc (m *MockLogger) MarkDone() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"MarkDone\")\n}\n\n// MarkDone indicates an expected call of MarkDone.\nfunc (mr *MockLoggerMockRecorder) MarkDone() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"MarkDone\", reflect.TypeOf((*MockLogger)(nil).MarkDone))\n}\n\n// PubsubMessageSpanEnd mocks base method.\nfunc (m *MockLogger) PubsubMessageSpanEnd(params trace2.PubsubMessageSpanEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"PubsubMessageSpanEnd\", params)\n}\n\n// PubsubMessageSpanEnd indicates an expected call of PubsubMessageSpanEnd.\nfunc (mr *MockLoggerMockRecorder) PubsubMessageSpanEnd(params interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PubsubMessageSpanEnd\", reflect.TypeOf((*MockLogger)(nil).PubsubMessageSpanEnd), params)\n}\n\n// PubsubMessageSpanStart mocks base method.\nfunc (m *MockLogger) PubsubMessageSpanStart(req *model.Request, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"PubsubMessageSpanStart\", req, goid)\n}\n\n// PubsubMessageSpanStart indicates an expected call of PubsubMessageSpanStart.\nfunc (mr *MockLoggerMockRecorder) PubsubMessageSpanStart(req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PubsubMessageSpanStart\", reflect.TypeOf((*MockLogger)(nil).PubsubMessageSpanStart), req, goid)\n}\n\n// PubsubPublishEnd mocks base method.\nfunc (m *MockLogger) PubsubPublishEnd(arg0 trace2.PubsubPublishEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"PubsubPublishEnd\", arg0)\n}\n\n// PubsubPublishEnd indicates an expected call of PubsubPublishEnd.\nfunc (mr *MockLoggerMockRecorder) PubsubPublishEnd(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PubsubPublishEnd\", reflect.TypeOf((*MockLogger)(nil).PubsubPublishEnd), arg0)\n}\n\n// PubsubPublishStart mocks base method.\nfunc (m *MockLogger) PubsubPublishStart(arg0 trace2.PubsubPublishStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PubsubPublishStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// PubsubPublishStart indicates an expected call of PubsubPublishStart.\nfunc (mr *MockLoggerMockRecorder) PubsubPublishStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PubsubPublishStart\", reflect.TypeOf((*MockLogger)(nil).PubsubPublishStart), arg0)\n}\n\n// RPCCallEnd mocks base method.\nfunc (m *MockLogger) RPCCallEnd(call *model.APICall, goid uint32, err error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"RPCCallEnd\", call, goid, err)\n}\n\n// RPCCallEnd indicates an expected call of RPCCallEnd.\nfunc (mr *MockLoggerMockRecorder) RPCCallEnd(call, goid, err interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RPCCallEnd\", reflect.TypeOf((*MockLogger)(nil).RPCCallEnd), call, goid, err)\n}\n\n// RPCCallStart mocks base method.\nfunc (m *MockLogger) RPCCallStart(call *model.APICall, goid uint32) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RPCCallStart\", call, goid)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// RPCCallStart indicates an expected call of RPCCallStart.\nfunc (mr *MockLoggerMockRecorder) RPCCallStart(call, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RPCCallStart\", reflect.TypeOf((*MockLogger)(nil).RPCCallStart), call, goid)\n}\n\n// RequestSpanEnd mocks base method.\nfunc (m *MockLogger) RequestSpanEnd(params trace2.RequestSpanEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"RequestSpanEnd\", params)\n}\n\n// RequestSpanEnd indicates an expected call of RequestSpanEnd.\nfunc (mr *MockLoggerMockRecorder) RequestSpanEnd(params interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RequestSpanEnd\", reflect.TypeOf((*MockLogger)(nil).RequestSpanEnd), params)\n}\n\n// RequestSpanStart mocks base method.\nfunc (m *MockLogger) RequestSpanStart(req *model.Request, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"RequestSpanStart\", req, goid)\n}\n\n// RequestSpanStart indicates an expected call of RequestSpanStart.\nfunc (mr *MockLoggerMockRecorder) RequestSpanStart(req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RequestSpanStart\", reflect.TypeOf((*MockLogger)(nil).RequestSpanStart), req, goid)\n}\n\n// ServiceInitEnd mocks base method.\nfunc (m *MockLogger) ServiceInitEnd(arg0 trace2.EventParams, arg1 trace2.EventID, arg2 error) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"ServiceInitEnd\", arg0, arg1, arg2)\n}\n\n// ServiceInitEnd indicates an expected call of ServiceInitEnd.\nfunc (mr *MockLoggerMockRecorder) ServiceInitEnd(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceInitEnd\", reflect.TypeOf((*MockLogger)(nil).ServiceInitEnd), arg0, arg1, arg2)\n}\n\n// ServiceInitStart mocks base method.\nfunc (m *MockLogger) ServiceInitStart(arg0 trace2.ServiceInitStartParams) trace2.EventID {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceInitStart\", arg0)\n\tret0, _ := ret[0].(trace2.EventID)\n\treturn ret0\n}\n\n// ServiceInitStart indicates an expected call of ServiceInitStart.\nfunc (mr *MockLoggerMockRecorder) ServiceInitStart(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceInitStart\", reflect.TypeOf((*MockLogger)(nil).ServiceInitStart), arg0)\n}\n\n// TestSpanEnd mocks base method.\nfunc (m *MockLogger) TestSpanEnd(params trace2.TestSpanEndParams) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"TestSpanEnd\", params)\n}\n\n// TestSpanEnd indicates an expected call of TestSpanEnd.\nfunc (mr *MockLoggerMockRecorder) TestSpanEnd(params interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TestSpanEnd\", reflect.TypeOf((*MockLogger)(nil).TestSpanEnd), params)\n}\n\n// TestSpanStart mocks base method.\nfunc (m *MockLogger) TestSpanStart(req *model.Request, goid uint32) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"TestSpanStart\", req, goid)\n}\n\n// TestSpanStart indicates an expected call of TestSpanStart.\nfunc (mr *MockLoggerMockRecorder) TestSpanStart(req, goid interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TestSpanStart\", reflect.TypeOf((*MockLogger)(nil).TestSpanStart), req, goid)\n}\n\n// WaitAndClear mocks base method.\nfunc (m *MockLogger) WaitAndClear() ([]byte, bool) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"WaitAndClear\")\n\tret0, _ := ret[0].([]byte)\n\tret1, _ := ret[1].(bool)\n\treturn ret0, ret1\n}\n\n// WaitAndClear indicates an expected call of WaitAndClear.\nfunc (mr *MockLoggerMockRecorder) WaitAndClear() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"WaitAndClear\", reflect.TypeOf((*MockLogger)(nil).WaitAndClear))\n}\n\n// WaitAtLeast mocks base method.\nfunc (m *MockLogger) WaitAtLeast(arg0 time.Duration) bool {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"WaitAtLeast\", arg0)\n\tret0, _ := ret[0].(bool)\n\treturn ret0\n}\n\n// WaitAtLeast indicates an expected call of WaitAtLeast.\nfunc (mr *MockLoggerMockRecorder) WaitAtLeast(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"WaitAtLeast\", reflect.TypeOf((*MockLogger)(nil).WaitAtLeast), arg0)\n}\n\n// WaitUntilDone mocks base method.\nfunc (m *MockLogger) WaitUntilDone() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"WaitUntilDone\")\n}\n\n// WaitUntilDone indicates an expected call of WaitUntilDone.\nfunc (mr *MockLoggerMockRecorder) WaitUntilDone() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"WaitUntilDone\", reflect.TypeOf((*MockLogger)(nil).WaitUntilDone))\n}\n"
  },
  {
    "path": "runtimes/go/appruntime/shared/traceprovider/traceprovider.go",
    "content": "package traceprovider\n\nimport (\n\t\"math/rand/v2\"\n\t\"strings\"\n\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\ntype Factory interface {\n\tNewLogger() trace2.Logger\n\tSampleTrace(service, endpoint string) bool\n\tSamplePubSub(service, topic, subscription string) bool\n\tSampleDefault() bool\n}\n\n// samplingRates holds pre-split sampling rates for fast lookup.\ntype samplingRates struct {\n\t// endpoint holds rates keyed by \"service.endpoint\" for API endpoints.\n\tendpoint map[string]float64\n\t// service holds rates keyed by service name.\n\tservice map[string]float64\n\t// subscription holds rates keyed by \"topic.subscription\" for pubsub subscriptions.\n\tsubscription map[string]float64\n\t// topic holds rates keyed by topic name.\n\ttopic map[string]float64\n\t// defaultRate is the default rate, or -1 if unset.\n\tdefaultRate float64\n}\n\ntype DefaultFactory struct {\n\trates *samplingRates // nil means always sample\n}\n\nfunc NewDefaultFactory(cfg map[string]float64) *DefaultFactory {\n\tif len(cfg) == 0 {\n\t\treturn &DefaultFactory{}\n\t}\n\n\tr := &samplingRates{defaultRate: -1}\n\tfor key, rate := range cfg {\n\t\tif key == \"_\" {\n\t\t\tr.defaultRate = rate\n\t\t} else if name, ok := strings.CutPrefix(key, \"service:\"); ok {\n\t\t\tif r.service == nil {\n\t\t\t\tr.service = make(map[string]float64)\n\t\t\t}\n\t\t\tr.service[name] = rate\n\t\t} else if name, ok := strings.CutPrefix(key, \"endpoint:\"); ok {\n\t\t\tif r.endpoint == nil {\n\t\t\t\tr.endpoint = make(map[string]float64)\n\t\t\t}\n\t\t\tr.endpoint[name] = rate\n\t\t} else if name, ok := strings.CutPrefix(key, \"topic:\"); ok {\n\t\t\tif r.topic == nil {\n\t\t\t\tr.topic = make(map[string]float64)\n\t\t\t}\n\t\t\tr.topic[name] = rate\n\t\t} else if name, ok := strings.CutPrefix(key, \"subscription:\"); ok {\n\t\t\tif r.subscription == nil {\n\t\t\t\tr.subscription = make(map[string]float64)\n\t\t\t}\n\t\t\tr.subscription[name] = rate\n\t\t}\n\t}\n\treturn &DefaultFactory{rates: r}\n}\n\nfunc (f *DefaultFactory) NewLogger() trace2.Logger {\n\treturn trace2.NewLog()\n}\n\n// SampleTrace determines whether to sample an API endpoint trace.\n// Lookup order: endpoint → service → default.\nfunc (f *DefaultFactory) SampleTrace(service, endpoint string) bool {\n\tr := f.rates\n\tif r == nil {\n\t\treturn true\n\t}\n\tif len(r.endpoint) > 0 {\n\t\tif rate, ok := r.endpoint[service+\".\"+endpoint]; ok {\n\t\t\treturn rand.Float64() < rate\n\t\t}\n\t}\n\tif rate, ok := r.service[service]; ok {\n\t\treturn rand.Float64() < rate\n\t}\n\tif r.defaultRate >= 0 {\n\t\treturn rand.Float64() < r.defaultRate\n\t}\n\treturn true\n}\n\n// SamplePubSub determines whether to sample a pubsub subscription trace.\n// Lookup order: subscription → topic → service → default.\nfunc (f *DefaultFactory) SamplePubSub(service, topic, subscription string) bool {\n\tr := f.rates\n\tif r == nil {\n\t\treturn true\n\t}\n\tif len(r.subscription) > 0 {\n\t\tif rate, ok := r.subscription[topic+\".\"+subscription]; ok {\n\t\t\treturn rand.Float64() < rate\n\t\t}\n\t}\n\tif rate, ok := r.topic[topic]; ok {\n\t\treturn rand.Float64() < rate\n\t}\n\tif rate, ok := r.service[service]; ok {\n\t\treturn rand.Float64() < rate\n\t}\n\tif r.defaultRate >= 0 {\n\t\treturn rand.Float64() < r.defaultRate\n\t}\n\treturn true\n}\n\n// SampleDefault determines whether to sample based on the default sampling rate.\n// If no default rate is configured, always samples.\nfunc (f *DefaultFactory) SampleDefault() bool {\n\tr := f.rates\n\tif r == nil {\n\t\treturn true\n\t}\n\tif r.defaultRate >= 0 {\n\t\treturn rand.Float64() < r.defaultRate\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "runtimes/go/beta/auth/auth.go",
    "content": "// Package auth provides the APIs to get information about the authenticated users.\n//\n// For more information about how authentication works with Encore applications see https://encore.dev/docs/develop/auth.\npackage auth\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\n// UID is a unique identifier representing a user (a user id).\ntype UID = model.UID\n\n//publicapigen:drop\ntype Manager struct {\n\trt *reqtrack.RequestTracker\n}\n\n//publicapigen:drop\nfunc NewManager(rt *reqtrack.RequestTracker) *Manager {\n\treturn &Manager{rt}\n}\n\nfunc (mgr *Manager) UserID() (UID, bool) {\n\tif curr := mgr.rt.Current(); curr.Req != nil {\n\t\tif curr.Req.RPCData != nil {\n\t\t\tuid := curr.Req.RPCData.UserID\n\t\t\treturn uid, uid != \"\"\n\t\t} else if curr.Req.Test != nil {\n\t\t\tuid := curr.Req.Test.UserID\n\t\t\treturn uid, uid != \"\"\n\t\t}\n\t}\n\treturn \"\", false\n}\n\nfunc (mgr *Manager) Data() interface{} {\n\tif curr := mgr.rt.Current(); curr.Req != nil {\n\t\tif curr.Req.RPCData != nil {\n\t\t\treturn curr.Req.RPCData.AuthData\n\t\t} else if curr.Req.Test != nil {\n\t\t\treturn curr.Req.Test.AuthData\n\t\t}\n\t}\n\treturn nil\n}\n\n// WithContext returns a new context that sets the auth information for outgoing API calls.\n// It does not affect the auth information for the current request.\n//\n// Passing in an empty string as the uid results in unsetting the auth information,\n// causing future API calls to behave as if there was no authenticated user.\n//\n// If the application's auth handler returns custom auth data, two additional\n// requirements exist. First, the data parameter passed to WithContext must be of\n// the same type as the auth handler returns. Second, if the uid argument is not\n// the empty string then data may not be nil. If these requirements are not met,\n// API calls made with these options will not be made and will immediately return\n// a client-side error.\nfunc WithContext(ctx context.Context, uid UID, data interface{}) context.Context {\n\topts := *api.GetCallOptions(ctx) // make a copy\n\topts.Auth = &model.AuthInfo{UID: uid, UserData: data}\n\treturn api.WithCallOptions(ctx, &opts)\n}\n"
  },
  {
    "path": "runtimes/go/beta/auth/pkgfn.go",
    "content": "//go:build encore_app\n\npackage auth\n\nimport \"encore.dev/appruntime/shared/reqtrack\"\n\n//publicapigen:drop\nvar Singleton = NewManager(reqtrack.Singleton)\n\n// UserID reports the uid of the user making the request.\n// The second result is true if there is a user and false\n// if the request was made without authentication details.\nfunc UserID() (UID, bool) {\n\treturn Singleton.UserID()\n}\n\n// Data returns the structured auth data for the request.\n// It returns nil if the request was made without authentication details,\n// and the API endpoint does not require them.\n//\n// Expected usage is to immediately cast it to the registered auth data type:\n//\n//\tusr, ok := auth.Data().(*user.Data)\n//\tif !ok { /* ... */ }\nfunc Data() any {\n\treturn Singleton.Data()\n}\n"
  },
  {
    "path": "runtimes/go/beta/errs/builder.go",
    "content": "package errs\n\nimport (\n\t\"fmt\"\n\n\t\"encore.dev/appruntime/exported/stack\"\n)\n\n// A Builder allows for gradual construction of an error.\n// The zero value is ready for use.\n//\n// Use Err() to construct the error.\ntype Builder struct {\n\tcode     ErrCode\n\tcodeSet  bool\n\tdet      ErrDetails\n\tdetSet   bool\n\tstack    stack.Stack\n\tstackSet bool\n\n\tmsg  string\n\tmeta []interface{}\n\terr  error\n}\n\n// B is a shorthand for creating a new Builder.\nfunc B() *Builder { return &Builder{} }\n\n// Code sets the error code.\nfunc (b *Builder) Code(c ErrCode) *Builder {\n\tb.code = c\n\tb.codeSet = true\n\treturn b\n}\n\n// Msg sets the error message.\nfunc (b *Builder) Msg(msg string) *Builder {\n\tb.msg = msg\n\treturn b\n}\n\n// Msgf is like Msg but uses fmt.Sprintf to construct the message.\nfunc (b *Builder) Msgf(format string, args ...interface{}) *Builder {\n\tb.msg = fmt.Sprintf(format, args...)\n\treturn b\n}\n\n// Meta appends metadata key-value pairs.\nfunc (b *Builder) Meta(metaPairs ...interface{}) *Builder {\n\tb.meta = append(b.meta, metaPairs...)\n\treturn b\n}\n\n// Details sets the details.\nfunc (b *Builder) Details(det ErrDetails) *Builder {\n\tb.det = det\n\tb.detSet = true\n\treturn b\n}\n\n// Cause sets the underlying error cause.\nfunc (b *Builder) Cause(err error) *Builder {\n\tb.err = err\n\tif e, ok := err.(*Error); ok {\n\t\tif !b.codeSet {\n\t\t\tb.code = e.Code\n\t\t}\n\t\tif !b.detSet {\n\t\t\tb.det = e.Details\n\t\t}\n\t\tif !b.stackSet {\n\t\t\tb.stack = e.stack\n\t\t\tb.stackSet = true\n\t\t}\n\t}\n\treturn b\n}\n\n// Err returns the constructed error.\n// It never returns nil.\n//\n// If Code has not been set or has been set to OK,\n// the Code is set to Unknown.\n//\n// If Msg has not been set and Cause is nil,\n// the Msg is set to \"unknown error\".\nfunc (b *Builder) Err() error {\n\tcode := b.code\n\tif code == OK {\n\t\tcode = Unknown\n\t}\n\n\tmsg := b.msg\n\tif msg == \"\" && b.err == nil {\n\t\tmsg = \"unknown error\"\n\t}\n\n\tvar errMeta Metadata\n\tvar s stack.Stack\n\tif b.stackSet {\n\t\ts = b.stack\n\t} else if e, ok := b.err.(*Error); ok {\n\t\terrMeta = e.Meta\n\t\ts = e.stack\n\t} else {\n\t\ts = stack.Build(2)\n\t}\n\n\treturn &Error{\n\t\tCode:       code,\n\t\tMessage:    msg,\n\t\tMeta:       mergeMeta(errMeta, b.meta),\n\t\tDetails:    b.det,\n\t\tunderlying: b.err,\n\t\tstack:      s,\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/beta/errs/codes.go",
    "content": "package errs\n\n// ErrCode is an RPC error code.\ntype ErrCode int\n\nconst (\n\t// OK indicates the operation was successful.\n\tOK ErrCode = 0\n\n\t// Canceled indicates the operation was canceled (typically by the caller).\n\t//\n\t// Encore will generate this error code when cancellation is requested.\n\tCanceled ErrCode = 1\n\n\t// Unknown error. An example of where this error may be returned is\n\t// if a Status value received from another address space belongs to\n\t// an error-space that is not known in this address space. Also\n\t// errors raised by APIs that do not return enough error information\n\t// may be converted to this error.\n\t//\n\t// Encore will generate this error code in the above two mentioned cases.\n\tUnknown ErrCode = 2\n\n\t// InvalidArgument indicates client specified an invalid argument.\n\t// Note that this differs from FailedPrecondition. It indicates arguments\n\t// that are problematic regardless of the state of the system\n\t// (e.g., a malformed file name).\n\t//\n\t// Encore will generate this error code if the request data cannot be parsed.\n\tInvalidArgument ErrCode = 3\n\n\t// DeadlineExceeded means operation expired before completion.\n\t// For operations that change the state of the system, this error may be\n\t// returned even if the operation has completed successfully. For\n\t// example, a successful response from a server could have been delayed\n\t// long enough for the deadline to expire.\n\t//\n\t// Encore will generate this error code when the deadline is exceeded.\n\tDeadlineExceeded ErrCode = 4\n\n\t// NotFound means some requested entity (e.g., file or directory) was\n\t// not found.\n\t//\n\t// Encore will not generate this error code.\n\tNotFound ErrCode = 5\n\n\t// AlreadyExists means an attempt to create an entity failed because one\n\t// already exists.\n\t//\n\t// Encore will not generate this error code.\n\tAlreadyExists ErrCode = 6\n\n\t// PermissionDenied indicates the caller does not have permission to\n\t// execute the specified operation. It must not be used for rejections\n\t// caused by exhausting some resource (use ResourceExhausted\n\t// instead for those errors). It must not be\n\t// used if the caller cannot be identified (use Unauthenticated\n\t// instead for those errors).\n\t//\n\t// Encore will not generate this error code.\n\tPermissionDenied ErrCode = 7\n\n\t// ResourceExhausted indicates some resource has been exhausted, perhaps\n\t// a per-user quota, or perhaps the entire file system is out of space.\n\t//\n\t// Encore will generate this error code in out-of-memory and server overload\n\t// situations, or when a message is larger than the configured maximum size.\n\tResourceExhausted ErrCode = 8\n\n\t// FailedPrecondition indicates operation was rejected because the\n\t// system is not in a state required for the operation's execution.\n\t// For example, directory to be deleted may be non-empty, an rmdir\n\t// operation is applied to a non-directory, etc.\n\t//\n\t// A litmus test that may help a service implementor in deciding\n\t// between FailedPrecondition, Aborted, and Unavailable:\n\t//  (a) Use Unavailable if the client can retry just the failing call.\n\t//  (b) Use Aborted if the client should retry at a higher-level\n\t//      (e.g., restarting a read-modify-write sequence).\n\t//  (c) Use FailedPrecondition if the client should not retry until\n\t//      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n\t//      fails because the directory is non-empty, FailedPrecondition\n\t//      should be returned since the client should not retry unless\n\t//      they have first fixed up the directory by deleting files from it.\n\t//  (d) Use FailedPrecondition if the client performs conditional\n\t//      Get/Update/Delete on a resource and the resource on the\n\t//      server does not match the condition. E.g., conflicting\n\t//      read-modify-write on the same resource.\n\t//\n\t// Encore will not generate this error code.\n\tFailedPrecondition ErrCode = 9\n\n\t// Aborted indicates the operation was aborted, typically due to a\n\t// concurrency issue like sequencer check failures, transaction aborts,\n\t// etc.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\tAborted ErrCode = 10\n\n\t// OutOfRange means operation was attempted past the valid range.\n\t// E.g., seeking or reading past end of file.\n\t//\n\t// Unlike InvalidArgument, this error indicates a problem that may\n\t// be fixed if the system state changes. For example, a 32-bit file\n\t// system will generate InvalidArgument if asked to read at an\n\t// offset that is not in the range [0,2^32-1], but it will generate\n\t// OutOfRange if asked to read from an offset past the current\n\t// file size.\n\t//\n\t// There is a fair bit of overlap between FailedPrecondition and\n\t// OutOfRange. We recommend using OutOfRange (the more specific\n\t// error) when it applies so that callers who are iterating through\n\t// a space can easily look for an OutOfRange error to detect when\n\t// they are done.\n\t//\n\t// Encore will not generate this error code.\n\tOutOfRange ErrCode = 11\n\n\t// Unimplemented indicates operation is not implemented or not\n\t// supported/enabled in this service.\n\t//\n\t// Encore will generate this error code when an endpoint does not exist.\n\tUnimplemented ErrCode = 12\n\n\t// Internal errors. Means some invariants expected by underlying\n\t// system has been broken. If you see one of these errors,\n\t// something is very broken.\n\t//\n\t// Encore will generate this error code in several internal error conditions.\n\tInternal ErrCode = 13\n\n\t// Unavailable indicates the service is currently unavailable.\n\t// This is a most likely a transient condition and may be corrected\n\t// by retrying with a backoff. Note that it is not always safe to retry\n\t// non-idempotent operations.\n\t//\n\t// See litmus test above for deciding between FailedPrecondition,\n\t// Aborted, and Unavailable.\n\t//\n\t// Encore will generate this error code in aubrupt shutdown of a server process\n\t// or network connection.\n\tUnavailable ErrCode = 14\n\n\t// DataLoss indicates unrecoverable data loss or corruption.\n\t//\n\t// Encore will not generate this error code.\n\tDataLoss ErrCode = 15\n\n\t// Unauthenticated indicates the request does not have valid\n\t// authentication credentials for the operation.\n\t//\n\t// Encore will generate this error code when the authentication metadata\n\t// is invalid or missing, and expects auth handlers to return errors with\n\t// this code when the auth token is not valid.\n\tUnauthenticated ErrCode = 16\n)\n\n// String returns the string representation of c.\n//\n//publicapigen:keep\nfunc (c ErrCode) String() string {\n\treturn codeNames[c]\n}\n\n// HTTPStatus reports a suitable HTTP status code for an error, based on its code.\n// If err is nil it reports 200. If it's not an *Error it reports 500.\n//\n//publicapigen:keep\nfunc (c ErrCode) HTTPStatus() int {\n\treturn codeStatus[c]\n}\n\n//publicapigen:keep\nfunc (c ErrCode) MarshalJSON() ([]byte, error) {\n\ts := c.String()\n\treturn []byte(\"\\\"\" + s + \"\\\"\"), nil\n}\n\n//publicapigen:keep\nvar codeNames = [...]string{\n\tOK:                 \"ok\",\n\tCanceled:           \"canceled\",\n\tUnknown:            \"unknown\",\n\tInvalidArgument:    \"invalid_argument\",\n\tDeadlineExceeded:   \"deadline_exceeded\",\n\tNotFound:           \"not_found\",\n\tAlreadyExists:      \"already_exists\",\n\tPermissionDenied:   \"permission_denied\",\n\tResourceExhausted:  \"resource_exhausted\",\n\tFailedPrecondition: \"failed_precondition\",\n\tAborted:            \"aborted\",\n\tOutOfRange:         \"out_of_range\",\n\tUnimplemented:      \"unimplemented\",\n\tInternal:           \"internal\",\n\tUnavailable:        \"unavailable\",\n\tDataLoss:           \"data_loss\",\n\tUnauthenticated:    \"unauthenticated\",\n}\n\n//publicapigen:keep\nvar codeStatus = [...]int{\n\tOK:                 200,\n\tCanceled:           499,\n\tUnknown:            500,\n\tInvalidArgument:    400,\n\tDeadlineExceeded:   504,\n\tNotFound:           404,\n\tAlreadyExists:      409,\n\tPermissionDenied:   403,\n\tResourceExhausted:  429,\n\tFailedPrecondition: 400,\n\tAborted:            409,\n\tOutOfRange:         400,\n\tUnimplemented:      501,\n\tInternal:           500,\n\tUnavailable:        503,\n\tDataLoss:           500,\n\tUnauthenticated:    401,\n}\n"
  },
  {
    "path": "runtimes/go/beta/errs/details.go",
    "content": "package errs\n\n// ErrDetails is a marker interface for telling Encore\n// the type is used for reporting error details.\n//\n// We require a marker method (as opposed to using interface{})\n// to facilitate static analysis and to ensure the type\n// can be properly serialized across the network.\ntype ErrDetails interface {\n\tErrDetails() // marker method; it need not do anything\n}\n"
  },
  {
    "path": "runtimes/go/beta/errs/error.go",
    "content": "// Package errs provides structured error handling for Encore applications.\n//\n// See https://encore.dev/docs/develop/errors for more information about how errors work within Encore applications.\npackage errs\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"unsafe\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/exported/stack\"\n)\n\nvar json = jsoniter.Config{\n\tEscapeHTML:             false,\n\tSortMapKeys:            false,\n\tValidateJsonRawMessage: true,\n}.Froze()\n\n// An Error is an error that provides structured information\n// about the error. It includes an error code, a message,\n// optionally additional structured details about the error\n// and arbitrary key-value metadata.\n//\n// The Details field is returned to external clients.\n// The Meta field is only exposed to internal calls within Encore.\n//\n// Internally it captures an underlying error for printing\n// and for use with errors.Is/As and call stack information.\n//\n// To provide accurate stack information, users are expected\n// to convert non-Error errors into *Error as close to the\n// root cause as possible. This is made simple with Wrap.\ntype Error struct {\n\t// Code is the error code to return.\n\tCode ErrCode `json:\"code\"`\n\t// Message is a descriptive message of the error.\n\tMessage string `json:\"message\"`\n\t// Details are user-defined additional details.\n\tDetails ErrDetails `json:\"details\"`\n\t// Meta are arbitrary key-value pairs for use within\n\t// the Encore application. They are not exposed to external clients.\n\tMeta Metadata `json:\"-\"`\n\n\t// underlying is the underlying error,\n\t// for use with errors.Is and errors.As.\n\t// It is not propagated across RPC boundaries.\n\tunderlying error\n\n\tstack stack.Stack\n}\n\n// Metadata represents structured key-value pairs, for attaching arbitrary\n// metadata to errors. The metadata is propagated between internal services\n// but is not exposed to external clients.\ntype Metadata map[string]interface{}\n\n// Wrap wraps the err, adding additional error information.\n// If err is nil it returns nil.\n//\n// If err is already an *Error its code, message, and details\n// are copied over to the new error.\nfunc Wrap(err error, msg string, metaPairs ...interface{}) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\te := &Error{Code: Unknown, Message: msg, underlying: err}\n\tvar ee *Error\n\tif errors.As(err, &ee) {\n\t\te.Details = ee.Details\n\t\te.Code = ee.Code\n\t\te.Meta = mergeMeta(ee.Meta, metaPairs)\n\t\te.stack = ee.stack\n\t} else {\n\t\te.Meta = mergeMeta(nil, metaPairs)\n\t\te.stack = stack.Build(2)\n\t}\n\treturn e\n}\n\n// WrapCode is like Wrap but also sets the error code.\n// If code is OK it reports nil.\nfunc WrapCode(err error, code ErrCode, msg string, metaPairs ...interface{}) error {\n\tif err == nil || code == OK {\n\t\treturn nil\n\t}\n\n\te := &Error{Code: code, Message: msg, underlying: err}\n\tvar ee *Error\n\tif errors.As(err, &ee) {\n\t\te.Details = ee.Details\n\t\te.Meta = mergeMeta(ee.Meta, metaPairs)\n\t\te.stack = ee.stack\n\t} else {\n\t\te.Meta = mergeMeta(nil, metaPairs)\n\t\te.stack = stack.Build(2)\n\t}\n\treturn e\n}\n\n// Convert converts an error to an *Error.\n// If the error is already an *Error it returns it unmodified.\n// If err is nil it returns nil.\nfunc Convert(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tif e, ok := err.(*Error); ok {\n\t\t// If the parent is already an *Error we can return it directly.\n\t\treturn e\n\t}\n\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\t// The error itself isn't an *Error, but it wraps one somewhere in the chain. Create a new *Error that preserves\n\t\t// the outer error but takes properties from inner *Error\n\t\treturn &Error{\n\t\t\tCode:       e.Code,\n\t\t\tMessage:    err.Error(),\n\t\t\tDetails:    e.Details,\n\t\t\tMeta:       e.Meta,\n\t\t\tunderlying: err,\n\t\t\tstack:      e.stack,\n\t\t}\n\t}\n\n\treturn &Error{\n\t\tCode:       Unknown,\n\t\tunderlying: err,\n\t\tstack:      stack.Build(2),\n\t}\n}\n\n// Code reports the error code from an error.\n// If err is nil it reports OK.\n// Otherwise if err is not an *Error it reports Unknown.\nfunc Code(err error) ErrCode {\n\tif err == nil {\n\t\treturn OK\n\t}\n\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\treturn e.Code\n\t}\n\treturn Unknown\n}\n\n// Meta reports the metadata included in the error.\n// If err is nil or the error lacks metadata it reports nil.\nfunc Meta(err error) Metadata {\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\treturn e.Meta\n\t}\n\treturn nil\n}\n\n// Details reports the error details included in the error.\n// If err is nil or the error lacks details it reports nil.\nfunc Details(err error) ErrDetails {\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\treturn e.Details\n\t}\n\treturn nil\n}\n\n// Error reports the error code and message.\nfunc (e *Error) Error() string {\n\tif e.Code == Unknown {\n\t\treturn \"unknown code: \" + e.ErrorMessage()\n\t}\n\treturn e.Code.String() + \": \" + e.ErrorMessage()\n}\n\n// ErrorMessage reports the error message, joining this\n// error's message with the messages from any underlying errors.\nfunc (e *Error) ErrorMessage() string {\n\tif e.underlying == nil {\n\t\treturn e.Message\n\t}\n\n\tvar b strings.Builder\n\tb.WriteString(e.Message)\n\n\tnext := e.underlying\n\tfor next != nil {\n\t\tvar msg string\n\t\tif e, ok := next.(*Error); ok {\n\t\t\tmsg = e.Message\n\t\t\tnext = e.underlying\n\t\t} else {\n\t\t\tmsg = next.Error()\n\t\t\tnext = nil\n\t\t}\n\t\tif b.Len() > 0 && msg != \"\" {\n\t\t\tb.WriteString(\": \")\n\t\t}\n\t\tb.WriteString(msg)\n\t}\n\treturn b.String()\n}\n\n// Unwrap returns the underlying error, if any.\nfunc (e *Error) Unwrap() error {\n\treturn e.underlying\n}\n\n// HTTPError writes structured error information to w using JSON encoding.\n// The status code is computed with HTTPStatus.\n//\n// If err is nil it writes:\n//\n//\t{\"code\": \"ok\", \"message\": \"\", \"details\": null}\nfunc HTTPError(w http.ResponseWriter, err error) {\n\tHTTPErrorWithCode(w, err, 0)\n}\n\nfunc mergeMeta(md Metadata, pairs []interface{}) Metadata {\n\tn := len(pairs)\n\tif n%2 != 0 {\n\t\tpanic(fmt.Sprintf(\"got uneven number (%d) of metadata key-values\", n))\n\t}\n\tif md == nil && n > 0 {\n\t\tmd = make(Metadata, n/2)\n\t}\n\tfor i := 0; i < n; i += 2 {\n\t\tkey, ok := pairs[i].(string)\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"metadata key-value pair #%d key is not a string (is %T)\", i/2, pairs[i]))\n\t\t}\n\t\tmd[key] = pairs[i+1]\n\t}\n\treturn md\n}\n\nfunc init() {\n\tjsoniter.RegisterTypeEncoderFunc(\"errs.Error\", func(ptr unsafe.Pointer, stream *jsoniter.Stream) {\n\t\te := (*Error)(ptr)\n\t\tstream.WriteObjectStart()\n\t\tstream.WriteObjectField(\"code\")\n\t\tstream.WriteString(e.Code.String())\n\t\tstream.WriteMore()\n\t\tstream.WriteObjectField(\"message\")\n\t\tstream.WriteString(e.ErrorMessage())\n\t\tstream.WriteMore()\n\t\tstream.WriteObjectField(\"details\")\n\t\tstream.WriteVal(e.Details)\n\t\tstream.WriteObjectEnd()\n\t}, nil)\n}\n"
  },
  {
    "path": "runtimes/go/beta/errs/errs_internal.go",
    "content": "package errs\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/apisdk/api/errmarshalling\"\n\t\"encore.dev/appruntime/exported/stack\"\n)\n\nvar statusToCode = map[int]ErrCode{\n\t200: OK,\n\t499: Canceled,\n\t500: Internal,\n\t400: InvalidArgument,\n\t401: Unauthenticated,\n\t403: PermissionDenied,\n\t404: NotFound,\n\t409: AlreadyExists,\n\t429: ResourceExhausted,\n\t501: Unimplemented,\n\t503: Unavailable,\n\t504: DeadlineExceeded,\n}\n\nfunc HTTPStatusToCode(status int) ErrCode {\n\tif c, ok := statusToCode[status]; ok {\n\t\treturn c\n\t}\n\treturn Unknown\n}\n\nfunc Stack(err error) stack.Stack {\n\tif e, ok := err.(*Error); ok {\n\t\treturn e.stack\n\t}\n\treturn stack.Stack{}\n}\n\nfunc DropStackFrame(err error) error {\n\tif e, ok := err.(*Error); ok && len(e.stack.Frames) > 0 {\n\t\te.stack.Frames = e.stack.Frames[1:]\n\t}\n\treturn err\n}\n\n// Stack sets the stack of the new error.\nfunc (b *Builder) Stack(s stack.Stack) *Builder {\n\tb.stack = s\n\tb.stackSet = true\n\treturn b\n}\n\n// RoundTrip copies an error, returning an equivalent error\n// for replicating across RPC boundaries.\nfunc RoundTrip(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t} else if e, ok := err.(*Error); ok {\n\t\te2 := &Error{\n\t\t\tCode:    e.Code,\n\t\t\tMessage: e.Message,\n\t\t\tstack:   stack.Build(3), // skip caller of RoundTrip as well\n\t\t}\n\n\t\t// Register the stack type, even though we don't use it here as\n\t\t// we pass \"panic_stack\" via the meta data and gob will error\n\t\t// if the type hasn't been registered.\n\t\tgob.Register(e2.stack)\n\n\t\tif e.underlying != nil {\n\t\t\te2.underlying = errors.New(func() (rtn string) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\trtn = fmt.Sprintf(\"panic in calling underlying.Error(): %+v\", r)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\treturn e.underlying.Error()\n\t\t\t}())\n\t\t}\n\n\t\t// Copy details\n\t\tif e.Details != nil {\n\t\t\tvar buf bytes.Buffer\n\t\t\tgob.Register(e.Details)\n\t\t\tenc := gob.NewEncoder(&buf)\n\t\t\tif err := enc.Encode(struct{ Details ErrDetails }{Details: e.Details}); err != nil {\n\t\t\t\tlog.Printf(\"failed to encode error details: %v\", err)\n\t\t\t} else {\n\t\t\t\tdec := gob.NewDecoder(&buf)\n\t\t\t\tvar dst struct{ Details ErrDetails }\n\t\t\t\tif err := dec.Decode(&dst); err != nil {\n\t\t\t\t\tlog.Printf(\"failed to decode error details: %v\", err)\n\t\t\t\t} else {\n\t\t\t\t\te2.Details = dst.Details\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Copy meta\n\t\tif e.Meta != nil {\n\t\t\tvar buf bytes.Buffer\n\t\t\tenc := gob.NewEncoder(&buf)\n\t\t\tif err := enc.Encode(e.Meta); err != nil {\n\t\t\t\tlog.Printf(\"failed to encode error metadata: %v\", err)\n\t\t\t} else {\n\t\t\t\tdec := gob.NewDecoder(&buf)\n\t\t\t\tif err := dec.Decode(&e2.Meta); err != nil {\n\t\t\t\t\tlog.Printf(\"failed to decode error metadata: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn e2\n\t} else {\n\t\treturn &Error{\n\t\t\tCode:    Unknown,\n\t\t\tMessage: err.Error(),\n\t\t\tstack:   stack.Build(3), // skip caller of RoundTrip as well\n\t\t}\n\t}\n}\n\nfunc HTTPStatus(err error) int {\n\tcode := Code(err)\n\tswitch code {\n\tcase OK:\n\t\treturn 200\n\tcase Canceled:\n\t\treturn 499\n\tcase Unknown:\n\t\treturn 500\n\tcase InvalidArgument:\n\t\treturn 400\n\tcase DeadlineExceeded:\n\t\treturn 504\n\tcase NotFound:\n\t\treturn 404\n\tcase AlreadyExists:\n\t\treturn 409\n\tcase PermissionDenied:\n\t\treturn 403\n\tcase ResourceExhausted:\n\t\treturn 429\n\tcase FailedPrecondition:\n\t\treturn 400\n\tcase Aborted:\n\t\treturn 409\n\tcase OutOfRange:\n\t\treturn 400\n\tcase Unimplemented:\n\t\treturn 501\n\tcase Internal:\n\t\treturn 500\n\tcase Unavailable:\n\t\treturn 503\n\tcase DataLoss:\n\t\treturn 500\n\tcase Unauthenticated:\n\t\treturn 401\n\tdefault:\n\t\treturn 500\n\t}\n}\n\n// HTTPErrorWithCode writes structured error information to w using JSON encoding.\n// The given status code is used if it is non-zero, and otherwise\n// it is computed with HTTPStatus.\n//\n// If err is nil it writes:\n//\n//\t{\"code\": \"ok\", \"message\": \"\", \"details\": null}\nfunc HTTPErrorWithCode(w http.ResponseWriter, err error, code int) {\n\tif code == 0 {\n\t\tcode = HTTPStatus(err)\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\n\tif err == nil {\n\t\tw.WriteHeader(code)\n\t\t_, _ = w.Write([]byte(`{\n  \"code\": \"ok\",\n  \"message\": \"\",\n  \"details\": null\n}\n`))\n\t\treturn\n\t}\n\n\te := Convert(err).(*Error)\n\tdata, err2 := json.MarshalIndent(e, \"\", \"  \")\n\tif err2 != nil {\n\t\t// Must be the details; drop them\n\t\te2 := &Error{Code: e.Code, Message: e.Message}\n\t\tdata, _ = json.MarshalIndent(e2, \"\", \"  \")\n\t}\n\tw.WriteHeader(code)\n\t// nosemgrep\n\t_, _ = w.Write(data)\n}\n\n// writeErrorFieldsToInternalStream writes the error fields to the given stream\n// for passing between running Encore services.\n//\n// Note we do not marshal the Details object as we would need to allow\n// for reflection loading of the type by name, which is not safe and could\n// lead to arbitrary code execution.\nfunc writeErrorFieldsToInternalStream(e *Error, stream *jsoniter.Stream) {\n\tstream.WriteObjectField(\"code\")\n\tstream.WriteInt(int(e.Code))\n\n\tstream.WriteMore()\n\tstream.WriteObjectField(errmarshalling.MessageKey)\n\tstream.WriteString(e.Message)\n\n\tif len(e.Meta) > 0 {\n\t\tif err := errmarshalling.TryWriteValue(stream, \"meta\", e.Meta); err != nil {\n\t\t\t// Only report the error in the JSON stream\n\t\t\t// don't error out the whole marshal as it's critical\n\t\t\t// that we marshal the error.\n\t\t\tstream.WriteMore()\n\t\t\tstream.WriteObjectField(\"meta_marshal_error\")\n\t\t\tstream.WriteString(err.Error())\n\t\t}\n\t}\n\n\tif e.underlying != nil {\n\t\tstream.WriteMore()\n\t\tstream.WriteObjectField(errmarshalling.WrappedKey)\n\t\tstream.WriteVal(e.underlying)\n\t}\n}\n\nfunc unmarshalFromInternalIterator(e *Error, itr *jsoniter.Iterator) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"code\":\n\t\t\te.Code = ErrCode(itr.ReadInt())\n\t\tcase errmarshalling.MessageKey:\n\t\t\te.Message = itr.ReadString()\n\t\tcase \"meta\":\n\t\t\titr.ReadVal(&e.Meta)\n\t\tcase errmarshalling.WrappedKey:\n\t\t\te.underlying = errmarshalling.UnmarshalError(itr)\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\n\t\treturn true\n\t})\n}\n\nfunc init() {\n\terrmarshalling.RegisterErrorMarshaller(writeErrorFieldsToInternalStream, unmarshalFromInternalIterator)\n}\n"
  },
  {
    "path": "runtimes/go/beta/package.go",
    "content": "// Package beta contains packages which can be used in Encore applications, however their APIs are not stable\n// and may change in future releases.\npackage beta\n"
  },
  {
    "path": "runtimes/go/config/helpers_internal.go",
    "content": "//go:build encore_app\n\npackage config\n\nimport (\n\t\"fmt\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\ntype Unmarshaler[T any] func(itr *jsoniter.Iterator, path []string) T\n\n// CreateValue creates a new Value on the given path with the given value\nfunc CreateValue[T any](value T, pathToValue ValuePath) Value[T] {\n\tvalueID := Singleton.nextID()\n\treturn func() T {\n\t\tSingleton.valueMeta(valueID, pathToValue)\n\t\treturn testOverrideOrValue(valueID, value)\n\t}\n}\n\n// CreateValueList creates a new Value Slice on the given path with the given values\nfunc CreateValueList[T any](value []T, pathToValue ValuePath) Values[T] {\n\tvalueID := Singleton.nextID()\n\treturn func() []T {\n\t\tSingleton.valueMeta(valueID, pathToValue)\n\t\treturn testOverrideOrValue(valueID, value)\n\t}\n}\n\n// GetMetaForValue returns the ValueID and ValuePath for the given Value\nfunc GetMetaForValue[T any](value func() T) (ValueID, ValuePath) {\n\t// Get the current request\n\treq := Singleton.rt.Current()\n\n\t// Lock so we're the only running extraction\n\tSingleton.extraction.mutex.Lock()\n\tdefer Singleton.extraction.mutex.Unlock()\n\n\t// Scope the extraction to the current goroutine\n\tSingleton.extraction.scopeMutex.Lock()\n\tSingleton.extraction.forSpan = req.Req.SpanID\n\tSingleton.extraction.forGoRoutine = req.Goctr\n\tSingleton.extraction.scopeMutex.Unlock()\n\n\t// Now flag that we're running extract and need to go via the slow path in Manager.valueMeta\n\tSingleton.extraction.running.Store(true)\n\tdefer func() {\n\t\tSingleton.extraction.count = 0\n\n\t\t// Reset the scoping\n\t\tSingleton.extraction.scopeMutex.Lock()\n\t\tSingleton.extraction.forSpan = [8]byte{0, 0, 0, 0, 0, 0, 0, 0}\n\t\tSingleton.extraction.forGoRoutine = 0\n\t\tSingleton.extraction.scopeMutex.Unlock()\n\n\t\t// Release the locks\n\t\tSingleton.extraction.running.Store(false)\n\t}()\n\n\t// Call the value, which will trigger the extraction inside Manager.valueMeta\n\tvalue()\n\n\t// Expect exactly 1 extraction\n\tif Singleton.extraction.count != 1 {\n\t\tpanic(fmt.Sprintf(\"config.Value metadata extraction failed, got %d extractions, expected 1\", Singleton.extraction.count))\n\t}\n\n\t// Return the extracted values\n\treturn Singleton.extraction.ExtractedID, Singleton.extraction.ExtractedPath\n}\n\n// ReadArray is a helper function that generated code can use to read an array from the JSON iterator\nfunc ReadArray[T any](itr *jsoniter.Iterator, cb func(itr *jsoniter.Iterator, idx int) T) []T {\n\trtn := make([]T, 0)\n\n\titr.ReadArrayCB(func(iterator *jsoniter.Iterator) bool {\n\t\trtn = append(rtn, cb(iterator, len(rtn)))\n\t\treturn true\n\t})\n\n\treturn rtn\n}\n\n// ReadMap is a helper function that generated code can use to read an map from the JSON iterator\nfunc ReadMap[K comparable, V any](itr *jsoniter.Iterator, cb func(itr *jsoniter.Iterator, key string) (K, V)) map[K]V {\n\trtn := make(map[K]V)\n\n\titr.ReadObjectCB(func(iterator *jsoniter.Iterator, key string) bool {\n\t\tk, v := cb(iterator, key)\n\t\trtn[k] = v\n\t\treturn true\n\t})\n\n\treturn rtn\n}\n"
  },
  {
    "path": "runtimes/go/config/manager_internal.go",
    "content": "package config\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/shared/encoreenv\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\ntype ValueID uint64\ntype ValuePath []string\n\ntype Manager struct {\n\t// Runtime components we need for config\n\trt   *reqtrack.RequestTracker\n\tjson jsoniter.API\n\n\t// config tracking systems\n\tnextValueID atomic.Uint64\n\textraction  struct {\n\t\tmutex         sync.Mutex   // Used only by GetMetaForValue\n\t\trunning       atomic.Bool  // Only store when under mutex\n\t\tscopeMutex    sync.RWMutex // Mutex for the forSpan / forGoRoutine\n\t\tforSpan       model.SpanID // Which span are we extracting for?\n\t\tforGoRoutine  uint32       // Which go routine within the span are we extracting for\n\t\tcount         int          // How many extractions have we done?\n\t\tExtractedID   ValueID      // What's the ValueID we extracted?\n\t\tExtractedPath ValuePath    // What's the path we extracted?\n\t}\n\n\t// Test support\n\ttestMutex     sync.RWMutex\n\ttestOverrides map[*testing.T]map[ValueID]any\n}\n\nfunc NewManager(rt *reqtrack.RequestTracker, json jsoniter.API) *Manager {\n\treturn &Manager{\n\t\trt:            rt,\n\t\tjson:          json,\n\t\ttestOverrides: make(map[*testing.T]map[ValueID]any),\n\t}\n}\n\nfunc (m *Manager) getComputedCUE(serviceName string) (jsonBytes []byte, found bool, err error) {\n\tif m == nil {\n\t\treturn nil, true, fmt.Errorf(\"config subsystem has not been initialized\")\n\t}\n\n\t// Fetch the raw JSON config for this service\n\tenvVar := encoreenv.Get(envName(serviceName))\n\tif envVar == \"\" {\n\t\treturn nil, false, fmt.Errorf(\"configuration for service `%s` not found, expected it in environmental variable %s\", serviceName, envName(serviceName))\n\t}\n\tcfgBytes, err := base64.RawURLEncoding.DecodeString(envVar)\n\tif err != nil {\n\t\treturn nil, true, fmt.Errorf(\"failed to decode configuration for service `%s`: %v\", serviceName, err)\n\t}\n\treturn cfgBytes, true, nil\n}\n\n// nextID returns the next unique ID for a config value to use to be tracked\nfunc (m *Manager) nextID() ValueID {\n\tif m == nil {\n\t\tpanic(\"config subsystem has not been initialized\")\n\t}\n\treturn ValueID(m.nextValueID.Add(1))\n}\n\n// valueMeta is used by Values to provide their ID and path to GetMetaForValue\n//\n// It is called by the Value function itself, and so is called in the context of the\n// of a goroutine that is running the GetMetaForValue function. If we are not in\n// that goroutine this method has no effect and the value is returned as normal.\nfunc (m *Manager) valueMeta(id ValueID, path ValuePath) {\n\t// Fast pass if we're not extracting\n\tif !m.extraction.running.Load() {\n\t\treturn\n\t}\n\n\t// Check if we're the right Goroutine that we want to extract from\n\treq := m.rt.Current()\n\tm.extraction.scopeMutex.RLock()\n\tdefer m.extraction.scopeMutex.RUnlock()\n\tif req.Req.SpanID != m.extraction.forSpan || req.Goctr != m.extraction.forGoRoutine {\n\t\treturn\n\t}\n\n\t// We're the right goroutine, so we can store the value\n\tm.extraction.ExtractedID = id\n\tm.extraction.ExtractedPath = path\n\tm.extraction.count++\n}\n\n// envName takes a service name and converts it to an environment variable name in which\n// the service's configuration JSON is stored at runtime\nfunc envName(serviceName string) string {\n\t// normalise the name\n\tserviceName = strings.ToUpper(serviceName)\n\n\treturn fmt.Sprintf(\"ENCORE_CFG_%s\", serviceName)\n}\n"
  },
  {
    "path": "runtimes/go/config/pkgfn.go",
    "content": "//go:build encore_app\n\n// Package config provides a simple way to access configuration values for a\n// service using the Load function.\n//\n// # By default configuration is pulled at build time from CUE files in each service directory\n//\n// For more information about configuration see https://encore.dev/docs/develop/config.\npackage config\n\nimport (\n\t\"fmt\"\n\n\t\"encore.dev/appruntime/shared/jsonapi\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\n//publicapigen:drop\nvar Singleton = NewManager(reqtrack.Singleton, jsonapi.Default)\n\n// Load returns the fully loaded configuration for this service.\n//\n// The configuration is loaded from the CUE files in the service directory and\n// will be validated by Encore at compile time, which ensures this function will\n// return a valid configuration at runtime.\n//\n// Encore will generate a `encore.gen.cue` file in the service directory which\n// will contain generated CUE matching the configuration type T.\n//\n// Note: This function can only be called from within services and cannot be\n// referenced from other services.\nfunc Load[T any](__serviceName string, __unmarshaler Unmarshaler[T]) T {\n\t// Get the computed cfg\n\tcfgBytes, found, err := Singleton.getComputedCUE(__serviceName)\n\tif err != nil {\n\t\t// If the config is not found, return a zero value\n\t\tif !found {\n\t\t\tvar zero T\n\t\t\treturn zero\n\t\t}\n\n\t\tpanic(err.Error())\n\t}\n\n\t// Create an iterator for the JSON config\n\titr := Singleton.json.BorrowIterator(cfgBytes)\n\tdefer Singleton.json.ReturnIterator(itr)\n\tif itr.Error != nil {\n\t\tpanic(fmt.Sprintf(\"failed to unmarshal config for service %s: %v\", __serviceName, itr.Error))\n\t}\n\n\t// Now unmarshal the root object\n\treturn __unmarshaler(itr, nil)\n}\n"
  },
  {
    "path": "runtimes/go/config/test_internal.go",
    "content": "//go:build encore_app\n\npackage config\n\n// SetValueForTest changes the value of cfg to newValue within the current test and any subtests.\nfunc SetValueForTest[T any](value Value[T], newValue T) {\n\t// Check we're running in a test\n\treq := Singleton.rt.Current().Req\n\tif req == nil || req.Test == nil {\n\t\tpanic(\"et.SetCfg called outside of a unit test\")\n\t}\n\n\t// Get the value ID\n\tvalueID, _ := GetMetaForValue(value)\n\n\tSingleton.testMutex.Lock()\n\tdefer Singleton.testMutex.Unlock()\n\n\t// Get the overrides map for this test\n\toverrides, found := Singleton.testOverrides[req.Test.Current]\n\tif !found {\n\t\toverrides = make(map[ValueID]any)\n\t\tSingleton.testOverrides[req.Test.Current] = overrides\n\t}\n\n\toverrides[valueID] = newValue\n}\n\n// testOverrideOrValue returns an overridden value if one exists for this test or it's parents\n// otherwise it returns the originalValue.\n//\n// If we're not in a unit test, it returns the originalValue.\nfunc testOverrideOrValue[T any](valueID ValueID, originalValue T) T {\n\treq := Singleton.rt.Current().Req\n\tif req == nil || req.Test == nil {\n\t\t// Not in a unit test\n\t\treturn originalValue\n\t}\n\ttestData := req.Test\n\n\tSingleton.testMutex.RLock()\n\tdefer Singleton.testMutex.RUnlock()\n\n\t// Get the overrides map for this test or any of the parent tests\n\tfor testData != nil && testData.Current != nil {\n\t\t// Check if this test has overrides\n\t\toverrides, found := Singleton.testOverrides[testData.Current]\n\t\tif found {\n\t\t\tif overriddenValue, found := overrides[valueID]; found {\n\t\t\t\treturn overriddenValue.(T)\n\t\t\t}\n\t\t}\n\n\t\t// Iterate up the test parents\n\t\tif testData.Parent == nil {\n\t\t\tbreak\n\t\t}\n\t\ttestData = testData.Parent.Test\n\t}\n\n\treturn originalValue\n}\n"
  },
  {
    "path": "runtimes/go/config/types.go",
    "content": "package config\n\nimport (\n\t\"time\"\n\n\t\"encore.dev/types/uuid\"\n)\n\n// Value represents a value in the configuration for this application\n// which can be any value represented in the configuration files.\n//\n// It is a function because the underlying value could change while\n// the application is still running due to unit tests providing\n// overrides to test different behaviours. To change the value within\n// a single unit test, use the et.SetCfg function.\ntype Value[T any] func() T\n\n// Values represents a list of values in the configuration for this\n// application which can be any value represented in the configuration files.\n//\n// It is a function because the underlying value could change while\n// the application is still running due to unit tests providing\n// overrides to test different behaviours.\ntype Values[T any] func() []T\n\n/*\nThe following types represent syntax sugar and are all just shorthand for\nValue[T]\n*/\n\ntype Bool = Value[bool]\ntype Int8 = Value[int8]\ntype Int16 = Value[int16]\ntype Int32 = Value[int32]\ntype Int64 = Value[int64]\ntype Uint8 = Value[uint8]\ntype Uint16 = Value[uint16]\ntype Uint32 = Value[uint32]\ntype Uint64 = Value[uint64]\ntype Float32 = Value[float32]\ntype Float64 = Value[float64]\ntype String = Value[string]\ntype Bytes = Value[[]byte]\ntype Time = Value[time.Time]\ntype UUID = Value[uuid.UUID]\ntype Int = Value[int]\ntype Uint = Value[uint]\n"
  },
  {
    "path": "runtimes/go/cron/cron.go",
    "content": "// Package cron provides support for cron jobs: recurring tasks that run on a schedule.\n//\n// For more information about Encore's cron job support, see https://encore.dev/docs/develop/cron-jobs.\npackage cron\n\n// NewJob defines a new cron job. It is specially recognized by the Encore Parser\n// and results in the Encore Platform provisioning the cron job on next deploy.\n// Note that cron jobs do not automatically execute when running the application locally.\n// To test the cron job implementation, test the target endpoint directly.\n//\n// The id argument is a unique identifier you give to each cron job. If you later\n// refactor the code and move the cron job definition to another package, Encore uses\n// this ID to keep track that it's the same cron job and not a different one.\n//\n// The ID must be defined in kebab-case, be no longer than 63 characters, start with\n// a letter and end with either a letter or number.\n//\n// The fields provided in the JobConfig must be constant literals, as they are parsed\n// directly by the Encore Platform and are not actually executed at runtime.\n//\n// To define a new cron job, call NewJob and assign it a package-level variable:\n//\n//\timport \"encore.dev/cron\"\n//\n//\t// Send a welcome email to everyone who signed up in the last two hours.\n//\tvar _ = cron.NewJob(\"welcome-email\", cron.JobConfig{\n//\t\tTitle:    \"Send welcome emails\",\n//\t\tEvery:    2 * cron.Hour,\n//\t\tEndpoint: SendWelcomeEmail,\n//\t})\n//\n//\t// SendWelcomeEmail emails everyone who signed up recently.\n//\t// It's idempotent: it only sends a welcome email to each person once.\n//\t//encore:api private\n//\tfunc SendWelcomeEmail(ctx context.Context) error {\n//\t\t// ...\n//\t\treturn nil\n//\t}\nfunc NewJob(id string, jobConfig JobConfig) *Job {\n\treturn &Job{\n\t\tID:       id,\n\t\tTitle:    jobConfig.Title,\n\t\tEvery:    jobConfig.Every,\n\t\tSchedule: jobConfig.Schedule,\n\t\tEndpoint: jobConfig.Endpoint,\n\t}\n}\n\n// JobConfig represents the configuration of a single cron job.\n//\n// The fields provided in the JobConfig must be constant literals, as they are parsed\n// directly by the Encore Platform and are not actually executed at runtime.\ntype JobConfig struct {\n\t// Title is the descriptive title of the cron job, typically a short sentence like \"Send welcome emails\".\n\tTitle string\n\n\t// Endpoint is the Encore API endpoint that should be called when the cron job executes.\n\t// It must not take any parameters other than context.Context; that is, its signature must be\n\t// either \"func(context.Context) error\" or \"func(context.Context) (T, error)\" for any type T.\n\tEndpoint any\n\n\t// Every defines how often the cron job should execute.\n\t// You must either specify either Every or Schedule (but not both).\n\t//\n\t// In order to ensure a consistent delay between each run, the interval used must divide 24 hours evenly.\n\t// For example, 10 * cron.Minute and 6 * cron.Hour are both allowed (since 24 hours is evenly divisible by both),\n\t// whereas 7 * cron.Hour is not (since 24 is not evenly divisible by 7).\n\t//\n\t// The Encore compiler will catch this and give you a helpful error at compile-time if you try to use an invalid interval.\n\tEvery Duration\n\n\t// Schedule defines when the cron job should execute, using a Cron Expression.\n\t// You must either specify either Every or Schedule (but not both).\n\t//\n\t// For more information on cron expressions, see https://en.wikipedia.org/wiki/Cron.\n\tSchedule string\n}\n\n// Job represents a created cron job. It can be inspected at runtime to determine information\n// about the cron job.\ntype Job struct {\n\tID       string\n\tTitle    string\n\tEvery    Duration\n\tSchedule string\n\tEndpoint interface{}\n}\n\n// Duration represents the duration between cron execution intervals, expressed in seconds.\n// Specific durations can easily be achieved using constant expressions, such as:\n//\n//\tcron.Hour + 30*cron.Minute // 90 minutes\ntype Duration int64\n\nconst (\n\tMinute Duration = 60\n\tHour   Duration = 60 * Minute\n)\n"
  },
  {
    "path": "runtimes/go/et/auth.go",
    "content": "package et\n\nimport (\n\t\"fmt\"\n\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/beta/auth\"\n)\n\nfunc (mgr *Manager) OverrideAuthInfo(uid auth.UID, authData any) {\n\tif curr := mgr.rt.Current(); curr.Req != nil {\n\t\tif err := api.CheckAuthData(uid, authData); err != nil {\n\t\t\tpanic(fmt.Errorf(\"override auth info: %v\", err))\n\t\t}\n\t\tif rpcData := curr.Req.RPCData; rpcData != nil {\n\t\t\trpcData.UserID = uid\n\t\t\trpcData.AuthData = authData\n\t\t} else if testData := curr.Req.Test; testData != nil {\n\t\t\ttestData.UserID = uid\n\t\t\ttestData.AuthData = authData\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/et/config.go",
    "content": "//go:build encore_app\n\npackage et\n\nimport (\n\t\"reflect\"\n\n\t\"encore.dev/config\"\n)\n\n// SetCfg changes the value of cfg to newValue within the current test and any subtests.\n// Other tests running will not be affected.\n//\n// It does not support setting slices and panics if given a config value that is a slice.\nfunc SetCfg[T any](cfg config.Value[T], newValue T) {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot set config in non-test environment\")\n\t}\n\n\trt := reflect.TypeOf(newValue)\n\tswitch rt.Kind() {\n\tcase reflect.Slice:\n\t\tpanic(\"et.SetCfg does not support setting slices\")\n\tcase reflect.Array:\n\t\tpanic(\"et.SetCfg does not support setting arrays\")\n\t}\n\n\tconfig.SetValueForTest[T](cfg, newValue)\n}\n"
  },
  {
    "path": "runtimes/go/et/manager_internal.go",
    "content": "package et\n\nimport (\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/storage/sqldb\"\n)\n\n//publicapigen:drop\ntype Manager struct {\n\tstatic  *config.Static\n\truntime *config.Runtime\n\trt      *reqtrack.RequestTracker\n\ttestMgr *testsupport.Manager\n\tserver  *api.Server\n\tdb      *sqldb.Manager\n}\n\n//publicapigen:drop\nfunc NewManager(static *config.Static, runtime *config.Runtime, rt *reqtrack.RequestTracker, testMgr *testsupport.Manager, server *api.Server, db *sqldb.Manager) *Manager {\n\tif runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot create manager in non-test environment\")\n\t}\n\n\treturn &Manager{static, runtime, rt, testMgr, server, db}\n}\n"
  },
  {
    "path": "runtimes/go/et/mocking.go",
    "content": "//go:build encore_app\n\npackage et\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// MockOption is a function that can be passed to MockEndpoint or MockService to configure the mocking behavior.\ntype MockOption func(*mockOptions)\n\n//publicapigen:keep\ntype mockOptions struct {\n\trunMiddleware bool\n}\n\n// RunMiddleware is a MockOption that sets whether to run the middleware chain\n// prior to invoking the mock.\nfunc RunMiddleware(enabled bool) MockOption {\n\treturn func(options *mockOptions) {\n\t\toptions.runMiddleware = enabled\n\t}\n}\n\n// MockEndpoint allows you to mock out an endpoint in your tests; Any calls made to the endpoint\n// during this test or any of its sub-tests will be routed to the mock you provide.\n//\n// Your mocked function must match the signature of the endpoint you are mocking.\n//\n// For example, if you have an endpoint defined as:\n//\n//\t//encore:api public\n//\tfunc MyAPI(ctx context.Context, req *MyAPIRequest) (*MyAPIResponse, error) {\n//\t\t...\n//\t}\n//\n// You can mock it out in your test as:\n//\n//\tet.MockEndpoint(mysvc.MyAPI, func(ctx context.Context, req *MyAPIRequest) (*MyAPIResponse, error) {\n//\t\t...\n//\t})\n//\n// If you want to mock out a single endpoint method on a service, you can use the generated helper\n// package function as the `originalEndpoint` argument to this function, however if you want to\n// mock out more than one API method on a service, consider using [MockService].\n//\n// Note: if you use [MockService] to mock a service and then use this function to mock\n// an endpoint on that service, the endpoint mock will take precedence over the service mock.\n//\n// Setting the mock to nil will remove the endpoint mock.\nfunc MockEndpoint[T any](originalEndpoint T, mock T, opts ...MockOption) {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot mock endpoint in non-test environment\")\n\t}\n\toptions := &mockOptions{}\n\tfor _, opt := range opts {\n\t\topt(options)\n\t}\n\n\thandler := Singleton.server.HandlerForFunc(originalEndpoint)\n\tif handler == nil {\n\t\tpanic(fmt.Sprintf(\"the function %T does not appear to be labelled as an Encore API.\", originalEndpoint))\n\t}\n\n\t// This code ensures if the user set a `nil` mock, we get an untyped nil\n\t// instead of a typed nil - which we can't check without reflecting and given\n\t// that is checked in the hot path pf the Desc.Call method, it's better to do it here\n\tvar mockFunctionAsAny any\n\tif reflect.ValueOf(mock).IsValid() && !reflect.ValueOf(mock).IsNil() {\n\t\tmockFunctionAsAny = mock\n\t}\n\n\tSingleton.testMgr.SetAPIMock(handler.ServiceName(), handler.EndpointName(), mockFunctionAsAny, options.runMiddleware)\n}\n\n// MockService allows you to mock out a service in your tests; Any calls made to the service\n// during this test or any of its sub-tests will be routed to the mock you provide.\n//\n// Your mock must implement the all the API methods of the service which are used during the\n// test(s). If you do not implement a method, it will panic when that method is called.\n//\n// If you want to ensure compile time safety, you can use the Interface type generated for\n// the service, which will ensure that you implement all the methods. For example:\n//\n//\tpackage svca\n//\n//\timport (\n//\t\t\"testing\"\n//\t\t\"encore.dev/et\"\n//\n//\t\t\"encore.app/svcb\"\n//\t)\n//\n//\tfunc TestServiceA(t *testing.T) {\n//\t\tet.MockService[svcb.Interface](\"svcb\", &myMockType{})\n//\t\tSomeFuncInThisPackageWhichUltimatelyCallsServiceB()\n//\t}\n//\n// Setting the mock to nil will remove the service mock.\nfunc MockService[T any](serviceName string, mock T, opts ...MockOption) {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot mock service in non-test environment\")\n\t}\n\toptions := &mockOptions{}\n\tfor _, opt := range opts {\n\t\topt(options)\n\t}\n\n\tif !Singleton.server.ServiceExists(serviceName) {\n\t\tpanic(fmt.Sprintf(\"cannot mock service %s: service does not exist\", serviceName))\n\t}\n\n\tSingleton.testMgr.SetServiceMock(serviceName, mock, options.runMiddleware)\n}\n"
  },
  {
    "path": "runtimes/go/et/package.go",
    "content": "// Package et stands for Encore Tests and provides a number of functions and tools for writing fully integrated\n// test suites for Encore applications.\npackage et\n"
  },
  {
    "path": "runtimes/go/et/pkgfn.go",
    "content": "//go:build encore_app\n\npackage et\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"encore.dev/beta/auth\"\n\t\"encore.dev/storage/sqldb\"\n)\n\n// OverrideAuthInfo overrides the auth information for the current request.\n// Subsequent calls to auth.UserID and auth.Data() within the same request\n// will return the given uid and data, and API calls made from the request\n// will propagate the newly set user info.\n//\n// Passing in an empty string as the uid results in unsetting the auth information,\n// causing future API calls to behave as if there was no authenticated user.\n//\n// If the application's auth handler returns custom auth data, two additional\n// requirements exist. First, the data parameter passed to WithContext must be of\n// the same type as the auth handler returns. Second, if the uid argument is not\n// the empty string then data may not be nil. If these requirements are not met,\n// API calls made with these options will not be made and will immediately return\n// a client-side error.\n//\n// OverrideAuthInfo is not safe for concurrent use with code that invokes\n// auth.UserID or auth.Data() within the same request.\nfunc OverrideAuthInfo(uid auth.UID, data any) {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot override auth info in non-test environment\")\n\t}\n\tSingleton.OverrideAuthInfo(uid, data)\n}\n\n// EnableServiceInstanceIsolation will causes all Service singletons to be isolated to each test\n// from this test and on any of its sub-tests. (Calling this in a TestMain has the impact\n// of isolating all tests in the package.)\n//\n// By default, Service singletons are shared across all tests and initialized on\n// the first call to that service by any test, which results in faster tests as you\n// are not re-initializing the service for each test, however if your service structs\n// contain state that is not reset between tests, this can cause issues. In that case,\n// you can call this function to isolate the services for the impacted tests.\nfunc EnableServiceInstanceIsolation() {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot enable service instance isolation in non-test environment\")\n\t}\n\tSingleton.testMgr.SetIsolatedServices(true)\n}\n\n//publicapigen:keep\ntype stringLiteral string\n\n// NewTestDatabase creates a new, fresh database for the database with the given name.\n// The database name must be a database known to Encore (via `sqldb.NewDatabase`),\n// otherwise it reports an error.\n//\n// The new database is cloned from a template database that has had all migrations applied to it,\n// but excludes any of the changes applied to the given db.\n//\n// The returned database is isolated to the current test and any sub-tests,\n// and is automatically dropped at the end of the test, and any\n// open connections are automatically closed.\n//\n// The provided name must be a constant string literal (like \"mydb\").\nfunc NewTestDatabase(ctx context.Context, name stringLiteral) (*sqldb.Database, error) {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\treturn nil, fmt.Errorf(\"et: cannot create test database in non-test environment\")\n\t}\n\treturn Singleton.db.NewTestDatabase(ctx, string(name))\n}\n"
  },
  {
    "path": "runtimes/go/et/pubsub.go",
    "content": "//go:build encore_app\n\npackage et\n\nimport (\n\t\"encore.dev/pubsub\"\n)\n\n// Topic returns a TopicHelper for the given topic.\nfunc Topic[T any](topic *pubsub.Topic[T]) TopicHelpers[T] {\n\tif Singleton.runtime.EnvType != \"test\" {\n\t\tpanic(\"et: cannot mock topic in non-test environment\")\n\t}\n\treturn pubsub.GetTestTopicInstance(topic).(TopicHelpers[T])\n}\n\n// TopicHelpers provides functions for interacting with the backing topic implementation\n// during unit tests. It is designed to help test code that uses the pubsub.Topic\n//\n// Note all functions on this TopicHelpers are scoped to the current test\n// and will only impact and observe state from the current test\ntype TopicHelpers[T any] interface {\n\t// PublishedMessages returns a slice of all messages published during this test on this topic.\n\tPublishedMessages() []T\n}\n"
  },
  {
    "path": "runtimes/go/et/singleton_internal.go",
    "content": "//go:build encore_app\n\npackage et\n\nimport (\n\t\"encore.dev/appruntime/apisdk/api\"\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/storage/sqldb\"\n)\n\n//publicapigen:drop\nvar Singleton = NewManager(appconf.Static, appconf.Runtime, reqtrack.Singleton, testsupport.Singleton, api.Singleton, sqldb.Singleton)\n"
  },
  {
    "path": "runtimes/go/et/sqldb.go",
    "content": "package et\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/storage/sqldb\"\n)\n\nfunc (mgr *Manager) NewTestDatabase(ctx context.Context, name string) (*sqldb.Database, error) {\n\treturn mgr.db.NewTestDatabase(ctx, name)\n}\n"
  },
  {
    "path": "runtimes/go/example_test.go",
    "content": "//go:build encore_app\n\npackage encore_test\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"encore.dev\"\n)\n\ntype Client interface{}\n\nvar client Client\n\nfunc NewRedshiftClient() Client         { return nil }\nfunc NewBigQueryClient() Client         { return nil }\nfunc LocalFileWriter(dir string) Client { return nil }\n\n// Change the implementation of some code based on which cloud provider is being used.\nfunc ExampleMeta() {\n\tswitch encore.Meta().Environment.Cloud {\n\tcase encore.CloudAWS:\n\t\tclient = NewRedshiftClient()\n\tcase encore.CloudGCP:\n\t\tclient = NewBigQueryClient()\n\tcase encore.CloudLocal:\n\t\tclient = LocalFileWriter(\"/tmp/app-writes.txt\")\n\tdefault:\n\t\tpanic(\"unsupported cloud provider\")\n\t}\n}\n\n// Check if the application is running in production\nfunc ExampleMeta_2() {\n\tif encore.Meta().Environment.Type != encore.EnvProduction {\n\t\tfmt.Println(\"running in development\")\n\t} else {\n\t\tfmt.Println(\"running in production\")\n\t}\n\t// Output: running in development\n}\n\nfunc ExampleCurrentRequest() {\n\treq := encore.CurrentRequest()\n\telapsed := time.Since(req.Started)\n\n\tif req.Type == encore.APICall {\n\t\tfmt.Printf(\"%s.%s has been running for %.3f seconds\", req.Service, req.Endpoint, elapsed.Seconds())\n\t}\n\t// Output: myservice.api has been running for 0.543 seconds\n}\n"
  },
  {
    "path": "runtimes/go/go.mod",
    "content": "module encore.dev\n\ngo 1.24.0\n\nrequire (\n\tcloud.google.com/go/compute/metadata v0.5.0\n\tcloud.google.com/go/monitoring v1.20.4\n\tcloud.google.com/go/pubsub v1.41.0\n\tcloud.google.com/go/storage v1.41.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.3\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.0\n\tgithub.com/DataDog/datadog-api-client-go/v2 v2.9.0\n\tgithub.com/alicebob/miniredis/v2 v2.23.0\n\tgithub.com/aws/aws-sdk-go-v2 v1.32.4\n\tgithub.com/aws/aws-sdk-go-v2/config v1.26.6\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.16.16\n\tgithub.com/aws/aws-sdk-go-v2/service/cloudwatch v1.32.2\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.66.3\n\tgithub.com/aws/aws-sdk-go-v2/service/sns v1.26.7\n\tgithub.com/aws/aws-sdk-go-v2/service/sqs v1.29.7\n\tgithub.com/aws/smithy-go v1.22.0\n\tgithub.com/benbjohnson/clock v1.3.3\n\tgithub.com/felixge/httpsnoop v1.0.4\n\tgithub.com/fmstephe/unsafeutil v1.0.0\n\tgithub.com/frankban/quicktest v1.14.5\n\tgithub.com/go-redis/redis/v8 v8.11.5\n\tgithub.com/golang/mock v1.6.0\n\tgithub.com/golang/protobuf v1.5.4\n\tgithub.com/golang/snappy v0.0.4\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/jackc/pgx/v5 v5.4.3\n\tgithub.com/json-iterator/go v1.1.12\n\tgithub.com/julienschmidt/httprouter v1.3.0\n\tgithub.com/modern-go/reflect2 v1.0.2\n\tgithub.com/nsqio/go-nsq v1.1.0\n\tgithub.com/rs/cors v1.8.3-0.20221003140808-fcebdb403f4d\n\tgithub.com/rs/xid v1.5.0\n\tgithub.com/rs/zerolog v1.31.0\n\tgo.encore.dev/platform-sdk v1.1.0\n\tgo.uber.org/automaxprocs v1.5.3\n\tgolang.org/x/crypto v0.25.0\n\tgolang.org/x/net v0.27.0\n\tgolang.org/x/sync v0.8.0\n\tgolang.org/x/time v0.6.0\n\tgoogle.golang.org/api v0.191.0\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f\n\tgoogle.golang.org/grpc v1.64.1\n\tgoogle.golang.org/protobuf v1.34.2\n)\n\nrequire (\n\tcloud.google.com/go v0.115.0 // indirect\n\tcloud.google.com/go/auth v0.8.0 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect\n\tcloud.google.com/go/iam v1.1.12 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect\n\tgithub.com/DataDog/zstd v1.5.0 // indirect\n\tgithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/dnaeon/go-vcr v1.2.0 // indirect\n\tgithub.com/fsnotify/fsnotify v1.6.0 // indirect\n\tgithub.com/go-logr/logr v1.4.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.5.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/google/s2a-go v0.1.8 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.13.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.1 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/klauspost/compress v1.17.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.19 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/onsi/gomega v1.30.0 // indirect\n\tgithub.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect\n\tgithub.com/rogpeppe/go-internal v1.11.0 // indirect\n\tgithub.com/stretchr/testify v1.9.0 // indirect\n\tgithub.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect\n\tgo.opentelemetry.io/otel v1.24.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.24.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.24.0 // indirect\n\tgolang.org/x/oauth2 v0.22.0 // indirect\n\tgolang.org/x/sys v0.22.0 // indirect\n\tgolang.org/x/text v0.16.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect\n\tnhooyr.io/websocket v1.8.7 // indirect\n)\n"
  },
  {
    "path": "runtimes/go/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=\ncloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=\ncloud.google.com/go/auth v0.8.0 h1:y8jUJLl/Fg+qNBWxP/Hox2ezJvjkrPb952PC1p0G6A4=\ncloud.google.com/go/auth v0.8.0/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=\ncloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=\ncloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=\ncloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=\ncloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=\ncloud.google.com/go/iam v1.1.12 h1:JixGLimRrNGcxvJEQ8+clfLxPlbeZA6MuRJ+qJNQ5Xw=\ncloud.google.com/go/iam v1.1.12/go.mod h1:9LDX8J7dN5YRyzVHxwQzrQs9opFFqn0Mxs9nAeB+Hhg=\ncloud.google.com/go/kms v1.18.4 h1:dYN3OCsQ6wJLLtOnI8DGUwQ5shMusXsWCCC+s09ATsk=\ncloud.google.com/go/kms v1.18.4/go.mod h1:SG1bgQ3UWW6/KdPo9uuJnzELXY5YTTMJtDYvajiQ22g=\ncloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=\ncloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=\ncloud.google.com/go/monitoring v1.20.4 h1:zwcViK7mT9SV0kzKqLOI3spRadvsmvw/R9z1MHNeC0E=\ncloud.google.com/go/monitoring v1.20.4/go.mod h1:v7F/UcLRw15EX7xq565N7Ae5tnYEE28+Cl717aTXG4c=\ncloud.google.com/go/pubsub v1.41.0 h1:ZPaM/CvTO6T+1tQOs/jJ4OEMpjtel0PTLV7j1JK+ZrI=\ncloud.google.com/go/pubsub v1.41.0/go.mod h1:g+YzC6w/3N91tzG66e2BZtp7WrpBBMXVa3Y9zVoOGpk=\ncloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=\ncloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.3 h1:8LoU8N2lIUzkmstvwXvVfniMZlFbesfT2AmA1aqvRr8=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.3/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=\ngithub.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.0 h1:ebO2jmZyctLSMBTvjsxZv/Ml3rGsvnJHUImVWotBl7I=\ngithub.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.0/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 h1:VgSJlZH5u0k2qxSpqyghcFQKmvYckj46uymKK5XzkBM=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v0.7.0/go.mod h1:BDJ5qMFKx9DugEg3+uQSDCdbYPr5s9vBTrL9P8TpqOU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/DataDog/datadog-api-client-go/v2 v2.9.0 h1:1Cz3mqj95iqnQPykEovq2p52rrU26XvLC2Fz6hPE+TU=\ngithub.com/DataDog/datadog-api-client-go/v2 v2.9.0/go.mod h1:sHt3EuVMN8PSYJu065qwp3pZxCwR3RZP4sJnYwj/ZQY=\ngithub.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo=\ngithub.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=\ngithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=\ngithub.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=\ngithub.com/alicebob/miniredis/v2 v2.23.0 h1:+lwAJYjvvdIVg6doFHuotFjueJ/7KY10xo/vm3X3Scw=\ngithub.com/alicebob/miniredis/v2 v2.23.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=\ngithub.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE=\ngithub.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=\ngithub.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=\ngithub.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 h1:1SZBDiRzzs3sNhOMVApyWPduWYGAX0imGy06XiBnCAM=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23/go.mod h1:i9TkxgbZmHVh2S0La6CAXtnyFhlCX/pJ0JsOvBAS6Mk=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatch v1.32.2 h1:vQfCIHSDouEvbE4EuDrlCGKcrtABEqF3cMt61nGEV4g=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatch v1.32.2/go.mod h1:3ToKMEhVj+Q+HzZ8Hqin6LdAKtsi3zVXVNUPpQMd+Xk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 h1:aaPpoG15S2qHkWm4KlEyF01zovK1nW4BBbyXuHNSE90=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4/go.mod h1:eD9gS2EARTKgGr/W5xwgY/ik9z/zqpW+m/xOQbVxrMk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 h1:E5ZAVOmI2apR8ADb72Q63KqwwwdW1XcMeXIlrZ1Psjg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4/go.mod h1:wezzqVUOVVdk+2Z/JzQT4NxAU0NbhRe5W8pIE72jsWI=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.66.3 h1:neNOYJl72bHrz9ikAEED4VqWyND/Po0DnEx64RW6YM4=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.66.3/go.mod h1:TMhLIyRIyoGVlaEMAt+ITMbwskSTpcGsCPDq91/ihY0=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.26.7 h1:DylmW2c1Z7qGxN3Y02k+voPbtM1mh7Rp+gV+7maG5io=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.26.7/go.mod h1:mLFiISZfiZAqZEfPWUsZBK8gD4dYCKuKAfapV+KrIVQ=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.29.7 h1:tRNrFDGRm81e6nTX5Q4CFblea99eAfm0dxXazGpLceU=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.29.7/go.mod h1:8GWUDux5Z2h6z2efAtr54RdHXtLm8sq7Rg85ZNY/CZM=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=\ngithub.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=\ngithub.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=\ngithub.com/benbjohnson/clock v1.3.3 h1:g+rSsSaAzhHJYcIQE78hJ3AhyjjtQvleKDjlhdBnIhc=\ngithub.com/benbjohnson/clock v1.3.3/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=\ngithub.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fmstephe/unsafeutil v1.0.0 h1:hWKjyW7jOL7rfCiBgX61tGy742pZ3C3VpHcGwTAgB2w=\ngithub.com/fmstephe/unsafeutil v1.0.0/go.mod h1:00y9QPGpX2A5iB0UmPDtnSpO4c2XsRQu3dQYuGL8+RA=\ngithub.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=\ngithub.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=\ngithub.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=\ngithub.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=\ngithub.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=\ngithub.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=\ngithub.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=\ngithub.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=\ngithub.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=\ngithub.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=\ngithub.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=\ngithub.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=\ngithub.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=\ngithub.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=\ngithub.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=\ngithub.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=\ngithub.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=\ngithub.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=\ngithub.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=\ngithub.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=\ngithub.com/rs/cors v1.8.3-0.20221003140808-fcebdb403f4d h1:gNEXs+4IbftZmT6WnAJbBWgbPrjDjqaMfuNeKODqBhc=\ngithub.com/rs/cors v1.8.3-0.20221003140808-fcebdb403f4d/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=\ngithub.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=\ngithub.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=\ngithub.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=\ngithub.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=\ngithub.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=\ngo.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI=\ngo.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI=\ngo.encore.dev/platform-sdk v1.1.0 h1:0BYLt7ZAoPje3KMLee6/gA2FECHwzi1sKgp3SqC+QRo=\ngo.encore.dev/platform-sdk v1.1.0/go.mod h1:ImcJU8p0V3bSXb+d++Ni/8hFDwVZaFpUAAySTY2x6FY=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=\ngo.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=\ngo.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=\ngo.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=\ngo.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=\ngo.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=\ngo.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=\ngo.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=\ngo.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=\ngo.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=\ngo.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=\ngolang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=\ngolang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=\ngolang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=\ngolang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=\ngolang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=\ngolang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=\ngolang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=\ngolang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngoogle.golang.org/api v0.191.0 h1:cJcF09Z+4HAB2t5qTQM1ZtfL/PemsLFkcFG67qq2afk=\ngoogle.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf h1:OqdXDEakZCVtDiZTjcxfwbHPCT11ycCEsTKesBVKvyY=\ngoogle.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=\ngoogle.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=\ngoogle.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nnhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=\nnhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\n"
  },
  {
    "path": "runtimes/go/internal/limiter/limiter.go",
    "content": "// Package limiter provides a simple interface for rate limiting requests.\npackage limiter\n\nimport (\n\t\"context\"\n\n\t\"golang.org/x/time/rate\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// Limiter is an interface for rate limiting requests.\n//\n// We're using an interface here instead of [golang.org/x/time/rate.Limiter] so that\n// we can easily swap out the implementation in the future for something like doorman.\ntype Limiter interface {\n\t// Wait blocks until the limiter is ready to accept a new request, or the context is canceled.\n\t//\n\t// If an error is returned, the request being limited should be aborted.\n\tWait(ctx context.Context) error\n}\n\n// New creates a new [Limiter] based on the given configuration.\n//\n// A nil configuration will result in a no-op limiter which allows all requests.\nfunc New(cfg *config.Limiter) Limiter {\n\tif cfg == nil {\n\t\treturn noopLimiter{}\n\t}\n\n\tswitch {\n\tcase cfg.TokenBucket != nil:\n\t\treturn rate.NewLimiter(rate.Limit(cfg.TokenBucket.PerSecondRate), cfg.TokenBucket.BucketSize)\n\tdefault:\n\t\treturn noopLimiter{}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/internal/limiter/noop.go",
    "content": "package limiter\n\nimport (\n\t\"context\"\n)\n\n// noopLimiter is a Limiter that never limits the request\ntype noopLimiter struct{}\n\nvar _ Limiter = noopLimiter{}\n\nfunc (n noopLimiter) Wait(ctx context.Context) error {\n\t// We return the context error here, so if the context was cancelled, we'll\n\t// behave the same as a rate limiter would.\n\treturn ctx.Err()\n}\n"
  },
  {
    "path": "runtimes/go/internal/platformauth/platformauth.go",
    "content": "// Package platformauth contains contexts for marking a request\n// as being authenticated to come from the Encore platform.\n//\n// It's an internal package to ensure only the Encore runtime can\n// mark requests as such.\npackage platformauth\n\nimport \"context\"\n\ntype ctxKey string\n\nconst platformAuthCtxKey ctxKey = \"platformAuthCtxKey\"\n\nfunc WithEncorePlatformSealOfApproval(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, platformAuthCtxKey, true)\n}\n\n// IsEncorePlatformRequest returns true if the given context originated from\n// a request from the Encore Platform.\nfunc IsEncorePlatformRequest(ctx context.Context) bool {\n\tvalue := ctx.Value(platformAuthCtxKey)\n\tif value == nil {\n\t\treturn false\n\t}\n\n\tv, ok := value.(bool)\n\treturn ok && v\n}\n"
  },
  {
    "path": "runtimes/go/meta.go",
    "content": "package encore\n\nimport (\n\t\"net/url\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/cloud\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\n//publicapigen:drop\ntype Manager struct {\n\tstatic     *config.Static\n\truntime    *config.Runtime\n\trt         *reqtrack.RequestTracker\n\tapiBaseURL *url.URL\n}\n\n//publicapigen:drop\nfunc NewManager(static *config.Static, rtConf *config.Runtime, rt *reqtrack.RequestTracker) *Manager {\n\tbaseURL, err := url.Parse(rtConf.APIBaseURL)\n\tif err != nil {\n\t\tpanic(\"invalid APIBaseURL: \" + err.Error())\n\t}\n\treturn &Manager{static, rtConf, rt, baseURL}\n}\n\n// Meta returns metadata about the running application.\n//\n// Meta will never return nil.\nfunc (mgr *Manager) Meta() *AppMetadata {\n\treturn &AppMetadata{\n\t\tAppID:      mgr.runtime.AppSlug,\n\t\tAPIBaseURL: *mgr.apiBaseURL,\n\t\tEnvironment: EnvironmentMeta{\n\t\t\tName:  mgr.runtime.EnvName,\n\t\t\tType:  EnvironmentType(mgr.runtime.EnvType),\n\t\t\tCloud: CloudProvider(mgr.runtime.EnvCloud),\n\t\t},\n\t\tBuild: BuildMeta{\n\t\t\tRevision:           mgr.static.AppCommit.Revision,\n\t\t\tUncommittedChanges: mgr.static.AppCommit.Uncommitted,\n\t\t},\n\t\tDeploy: DeployMeta{\n\t\t\tID:   mgr.runtime.DeployID,\n\t\t\tTime: mgr.runtime.DeployedAt,\n\t\t},\n\t}\n}\n\n// AppMetadata contains metadata about the running Encore application.\ntype AppMetadata struct {\n\t// The application ID, if the application is not linked to the Encore platform this will be an empty string.\n\t//\n\t// To link to the Encore platform run `encore app link` from your terminal in the root directory of the Encore app.\n\tAppID string\n\n\t// The base URL which can be used to call the API of this running application.\n\t//\n\t// For local development it is \"http://localhost:<port>\", typically \"http://localhost:4000\".\n\t//\n\t// If a custom domain is used for this environment it is returned here, but note that\n\t// changes only take effect at the time of deployment while custom domains can be updated at any time.\n\tAPIBaseURL url.URL\n\n\t// Information about the environment the app is running in.\n\tEnvironment EnvironmentMeta\n\n\t// Information about the running binary itself.\n\tBuild BuildMeta\n\n\t// Information about this deployment of the binary\n\tDeploy DeployMeta\n}\n\ntype EnvironmentMeta struct {\n\t// The name of environment that this application.\n\t// For local development it is \"local\".\n\tName string\n\n\t// The type of environment is this application running in\n\t// For local development this will be EnvLocal\n\tType EnvironmentType\n\n\t// The cloud that this environment is running on\n\t// For local development this is CloudLocal\n\tCloud CloudProvider\n}\n\ntype BuildMeta struct {\n\t// The git commit that formed the base of this build.\n\tRevision string\n\n\t// true if there were uncommitted changes on top of the Commit.\n\tUncommittedChanges bool\n}\n\ntype DeployMeta struct {\n\t// The deployment ID created by the Encore Platform.\n\tID string\n\n\t// The time the Encore Platform deployed this build to the environment.\n\tTime time.Time\n}\n\n// EnvironmentType represents the type of environment.\n//\n// For more information on environment types see https://encore.dev/docs/deploy/environments#environment-types\n//\n// Additional environment types may be added in the future.\ntype EnvironmentType string\n\nconst (\n\t// EnvProduction represents a production environment.\n\tEnvProduction EnvironmentType = \"production\"\n\n\t// EnvDevelopment represents a long-lived cloud-hosted, non-production environment, such as test environments.\n\tEnvDevelopment EnvironmentType = \"development\"\n\n\t// EnvEphemeral represents short-lived cloud-hosted, non-production environments, such as preview environments\n\t// that only exist while a particular pull request is open.\n\tEnvEphemeral EnvironmentType = \"ephemeral\"\n\n\t// EnvLocal represents the local development environment when using 'encore run' or `encore test`.\n\t//\n\t// Deprecated: EnvLocal is deprecated and Encore will no longer return this value. A locally running environment\n\t// can be identified by the combination of EnvDevelopment && CloudLocal. This constant will be removed in a future\n\t// version of Encore.\n\tEnvLocal EnvironmentType = \"local\"\n\n\t// EnvTest represents a running unit test\n\tEnvTest EnvironmentType = \"test\"\n)\n\n// CloudProvider represents the cloud provider this application is running in.\n//\n// For more information about how Cloud Providers work with Encore, see https://encore.dev/docs/deploy/own-cloud\n//\n// Additional cloud providers may be added in the future.\ntype CloudProvider = cloud.CloudProvider\n\nconst (\n\tCloudAWS   CloudProvider = cloud.AWS\n\tCloudGCP   CloudProvider = cloud.GCP\n\tCloudAzure CloudProvider = cloud.Azure\n\n\t// EncoreCloud is Encore's own cloud offering, and the default provider for new Environments.\n\tEncoreCloud CloudProvider = cloud.Encore\n\n\t// CloudLocal is used when an application is running from the Encore CLI by using either\n\t// 'encore run' or 'encore test'\n\tCloudLocal CloudProvider = cloud.Local\n)\n"
  },
  {
    "path": "runtimes/go/metrics/bits_internal.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"unsafe\"\n)\n\nfunc getAtomicAdder[V Value]() func(addr *V, delta V) {\n\tvar typ V\n\tswitch any(typ).(type) {\n\tcase int64:\n\t\treturn func(addr *V, delta V) {\n\t\t\ta := (*int64)(unsafe.Pointer(addr))\n\t\t\tatomic.AddInt64(a, int64(delta))\n\t\t}\n\tcase uint64:\n\t\treturn func(addr *V, delta V) {\n\t\t\ta := (*uint64)(unsafe.Pointer(addr))\n\t\t\tatomic.AddUint64(a, uint64(delta))\n\t\t}\n\tcase float64:\n\t\treturn func(addr *V, delta V) {\n\t\t\ta := (*float64)(unsafe.Pointer(addr))\n\t\t\tatomicAddFloat64(a, float64(delta))\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled value type %T\", typ))\n\t}\n}\n\nfunc getAtomicSetter[V Value]() func(addr *V, new V) {\n\tvar typ V\n\tswitch any(typ).(type) {\n\tcase int64:\n\t\treturn func(addr *V, new V) {\n\t\t\ta := (*int64)(unsafe.Pointer(addr))\n\t\t\tatomic.StoreInt64(a, int64(new))\n\t\t}\n\tcase uint64:\n\t\treturn func(addr *V, new V) {\n\t\t\ta := (*uint64)(unsafe.Pointer(addr))\n\t\t\tatomic.StoreUint64(a, uint64(new))\n\t\t}\n\tcase float64:\n\t\treturn func(addr *V, new V) {\n\t\t\ta := (*float64)(unsafe.Pointer(addr))\n\t\t\tatomicStoreFloat64(a, float64(new))\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled value type %T\", typ))\n\t}\n}\n\nfunc getAtomicIncrementer[V Value](adder func(addr *V, delta V)) func(addr *V) {\n\treturn func(addr *V) {\n\t\tadder(addr, 1)\n\t}\n}\n\nfunc atomicAddFloat64(addr *float64, delta float64) float64 {\n\tfor {\n\t\toldBits := atomic.LoadUint64((*uint64)(unsafe.Pointer(addr)))\n\t\tnewFloat := math.Float64frombits(oldBits) + delta\n\t\tnewBits := math.Float64bits(newFloat)\n\t\tif atomic.CompareAndSwapUint64((*uint64)(unsafe.Pointer(addr)), oldBits, newBits) {\n\t\t\treturn newFloat\n\t\t}\n\t}\n}\n\nfunc atomicStoreFloat64(addr *float64, value float64) {\n\tatomic.StoreUint64((*uint64)(unsafe.Pointer(addr)), math.Float64bits(value))\n}\n\nfunc atomicLoadFloat64(addr *float64) float64 {\n\treturn math.Float64frombits(atomic.LoadUint64((*uint64)(unsafe.Pointer(addr))))\n}\n\ntype initGate struct {\n\tstate uint32\n\tmu    sync.Mutex\n}\n\nfunc (g *initGate) Start() {\n\tg.mu.Lock()\n\tif !atomic.CompareAndSwapUint32(&g.state, 0, 1) {\n\t\tg.mu.Unlock() // don't leave the mutex in a locked state if we panic\n\t\tpanic(\"initGate: already started\")\n\t}\n}\n\nfunc (g *initGate) Done() {\n\tif !atomic.CompareAndSwapUint32(&g.state, 1, 2) {\n\t\tpanic(\"initGate: not in state initing\")\n\t}\n\tg.mu.Unlock()\n}\n\nfunc (g *initGate) Wait() {\n\tfor {\n\t\tswitch atomic.LoadUint32(&g.state) {\n\t\tcase 0:\n\t\t\tcontinue // not started yet\n\t\tcase 1:\n\t\t\t// It's running, block on the mutex before returning.\n\t\t\tg.mu.Lock()\n\t\t\tg.mu.Unlock()\n\t\t\treturn\n\t\tcase 2:\n\t\t\treturn // done\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/metrics/histogram_internal.go",
    "content": "//go:build encore_app\n\npackage metrics\n\nimport (\n\t\"math\"\n\n\t\"encore.dev/appruntime/shared/nativehist\"\n)\n\n// HistogramConfig configures a histogram.\n// It's currently a placeholder as there is not yet any additional configuration.\ntype HistogramConfig struct {\n\t//publicapigen:drop\n\tEncoreInternal_LabelMapper any // func(L) []KeyValue\n\n\t//publicapigen:drop\n\tEncoreInternal_SvcNum uint16\n}\n\n// NewHistogram creates a new histogram metric, without any labels.\n// Use NewHistogramGroup for histograms with labels.\nfunc NewHistogram[V Value](name string, cfg HistogramConfig) *Histogram[V] {\n\treturn newHistogramInternal[V](newMetricInfo[V](Singleton, name, HistogramType, cfg.EncoreInternal_SvcNum))\n}\n\nfunc newHistogramInternal[V Value](m *metricInfo[V]) *Histogram[V] {\n\tts, setup := getTS[*nativehist.Histogram](m.reg, m.name, nil, m)\n\n\t// Initialize the values if they haven't yet been set up.\n\tif !setup {\n\t\tn := m.reg.numSvcs\n\t\tif m.svcNum > 0 {\n\t\t\tn = 1\n\t\t}\n\t\tts.value = make([]*nativehist.Histogram, n)\n\t\tfor i := range ts.value {\n\t\t\tts.value[i] = nativehist.New(bucketFactor)\n\t\t}\n\t}\n\n\treturn &Histogram[V]{\n\t\tmetricInfo: m,\n\t\tts:         ts,\n\t\ttoFloat:    makeToFloat[V](),\n\t}\n}\n\ntype Histogram[V Value] struct {\n\t*metricInfo[V]\n\tts      *timeseries[*nativehist.Histogram]\n\ttoFloat func(V) float64\n}\n\nfunc (h *Histogram[V]) Observe(val V) {\n\tf := h.toFloat(val)\n\tif math.IsNaN(f) {\n\t\treturn\n\t}\n\tif idx, ok := h.svcIdx(); ok {\n\t\th.ts.value[idx].Observe(f)\n\t}\n}\n\n// NewHistogramGroup creates a new histogram group with a set of labels,\n// where each unique combination of labels becomes its own histogram.\n//\n// The Labels type must be a named struct, where each field corresponds to\n// a single label. Each field must be of type string.\nfunc NewHistogramGroup[L Labels, V Value](name string, cfg HistogramConfig) *HistogramGroup[L, V] {\n\treturn newHistogramGroup[L, V](Singleton, name, cfg)\n}\n\nfunc newHistogramGroup[L Labels, V Value](mgr *Registry, name string, cfg HistogramConfig) *HistogramGroup[L, V] {\n\tlabelMapper := cfg.EncoreInternal_LabelMapper.(func(L) []KeyValue)\n\tm := newMetricInfo[V](mgr, name, HistogramType, cfg.EncoreInternal_SvcNum)\n\treturn &HistogramGroup[L, V]{\n\t\tmetricInfo:  m,\n\t\tlabelMapper: labelMapper,\n\t\ttoFloat:     makeToFloat[V](),\n\t}\n}\n\ntype HistogramGroup[L Labels, V Value] struct {\n\t*metricInfo[V]\n\tlabelMapper func(L) []KeyValue\n\ttoFloat     func(V) float64\n}\n\nfunc (c *HistogramGroup[L, V]) With(labels L) *Histogram[V] {\n\tts := c.get(labels)\n\treturn &Histogram[V]{\n\t\tmetricInfo: c.metricInfo,\n\t\tts:         ts,\n\t\ttoFloat:    c.toFloat,\n\t}\n}\n\nfunc (c *HistogramGroup[L, V]) get(labels L) *timeseries[*nativehist.Histogram] {\n\tts, setup := getTS[*nativehist.Histogram](c.reg, c.name, labels, c.metricInfo)\n\n\tif !setup {\n\t\t// Initialize this histogram timeseries on first use.\n\t\tts.init.Start()\n\t\tdefer ts.init.Done()\n\n\t\tn := c.reg.numSvcs\n\t\tif c.svcNum > 0 {\n\t\t\tn = 1\n\t\t}\n\t\tts.value = make([]*nativehist.Histogram, n)\n\t\tfor i := range ts.value {\n\t\t\tts.value[i] = nativehist.New(bucketFactor)\n\t\t}\n\t} else {\n\t\t// Wait for the timeseries to be initialized before we continue.\n\t\tts.init.Wait()\n\t}\n\n\treturn ts\n}\n\nfunc makeToFloat[V Value]() func(V) float64 {\n\tvar zero V\n\tswitch any(zero).(type) {\n\tcase int64:\n\t\treturn func(val V) float64 { return float64(val) }\n\tcase float64:\n\t\treturn func(val V) float64 { return float64(val) }\n\tdefault:\n\t\tpanic(\"invalid unit\")\n\t}\n}\n\nconst bucketFactor = 1.1\n"
  },
  {
    "path": "runtimes/go/metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"sync/atomic\"\n)\n\ntype Labels interface {\n\tcomparable\n}\n\n// CounterConfig configures a counter.\n// It's currently a placeholder as there is not yet any additional configuration.\ntype CounterConfig struct {\n\t//publicapigen:drop\n\tEncoreInternal_LabelMapper any // func(L) []KeyValue\n\n\t//publicapigen:drop\n\tEncoreInternal_SvcNum uint16\n}\n\nfunc newCounterInternal[V Value](m *metricInfo[V]) *Counter[V] {\n\tts, setup := m.getTS(nil)\n\tif !setup {\n\t\tts.setup(nil)\n\t}\n\treturn &Counter[V]{metricInfo: m, ts: ts}\n}\n\ntype Counter[V Value] struct {\n\t*metricInfo[V]\n\tts *timeseries[V]\n}\n\n// Increment increments the counter by 1.\nfunc (c *Counter[V]) Increment() {\n\tif idx, ok := c.svcIdx(); ok {\n\t\tc.inc(&c.ts.value[idx])\n\t\tc.ts.valid[idx].Store(true)\n\t}\n}\n\n// Add adds an arbitrary, non-negative value to the counter.\n// It panics if the delta is < 0.\nfunc (c *Counter[V]) Add(delta V) {\n\tif delta < 0 {\n\t\tpanic(fmt.Sprintf(\"metrics: cannot add negative value %v to counter\", delta))\n\t}\n\tif idx, ok := c.svcIdx(); ok {\n\t\tc.add(&c.ts.value[idx], delta)\n\t\tc.ts.valid[idx].Store(true)\n\t}\n}\n\n//publicapigen:drop\nfunc NewCounterGroupInternal[L Labels, V Value](reg *Registry, name string, cfg CounterConfig) *CounterGroup[L, V] {\n\treturn newCounterGroup[L, V](reg, name, cfg)\n}\n\nfunc newCounterGroup[L Labels, V Value](mgr *Registry, name string, cfg CounterConfig) *CounterGroup[L, V] {\n\tlabelMapper := cfg.EncoreInternal_LabelMapper.(func(L) []KeyValue)\n\tm := newMetricInfo[V](mgr, name, CounterType, cfg.EncoreInternal_SvcNum)\n\treturn &CounterGroup[L, V]{metricInfo: m, labelMapper: labelMapper}\n}\n\ntype CounterGroup[L Labels, V Value] struct {\n\t*metricInfo[V]\n\tlabelMapper func(L) []KeyValue\n}\n\nfunc (c *CounterGroup[L, V]) With(labels L) *Counter[V] {\n\tts := c.get(labels)\n\treturn &Counter[V]{metricInfo: c.metricInfo, ts: ts}\n}\n\nfunc (c *CounterGroup[L, V]) get(labels L) *timeseries[V] {\n\tts, setup := c.metricInfo.getTS(labels)\n\tif !setup {\n\t\tts.setup(c.labelMapper(labels))\n\t} else {\n\t\t// Wait for the timeseries to be initialized before we continue.\n\t\tts.init.Wait()\n\t}\n\treturn ts\n}\n\nfunc newGauge[V Value](m *metricInfo[V]) *Gauge[V] {\n\tts, setup := m.getTS(nil)\n\tif !setup {\n\t\tts.setup(nil)\n\t}\n\n\treturn &Gauge[V]{metricInfo: m, ts: ts}\n}\n\n// GaugeConfig configures a gauge.\n// It's currently a placeholder as there is not yet any additional configuration.\ntype GaugeConfig struct {\n\t//publicapigen:drop\n\tEncoreInternal_LabelMapper any // func(L) any) []KeyValue\n\n\t//publicapigen:drop\n\tEncoreInternal_SvcNum uint16\n}\n\ntype Gauge[V Value] struct {\n\t*metricInfo[V]\n\tts *timeseries[V]\n}\n\nfunc (g *Gauge[V]) Set(val V) {\n\tif idx, ok := g.svcIdx(); ok {\n\t\tg.set(&g.ts.value[idx], val)\n\t\tg.ts.valid[idx].Store(true)\n\t}\n}\n\nfunc (g *Gauge[V]) Add(val V) {\n\tif idx, ok := g.svcIdx(); ok {\n\t\tg.add(&g.ts.value[idx], val)\n\t\tg.ts.valid[idx].Store(true)\n\t}\n}\n\nfunc newGaugeGroup[L Labels, V Value](mgr *Registry, name string, cfg GaugeConfig) *GaugeGroup[L, V] {\n\tlabelMapper := cfg.EncoreInternal_LabelMapper.(func(L) []KeyValue)\n\tm := newMetricInfo[V](mgr, name, GaugeType, cfg.EncoreInternal_SvcNum)\n\treturn &GaugeGroup[L, V]{metricInfo: m, labelMapper: labelMapper}\n}\n\ntype GaugeGroup[L Labels, V Value] struct {\n\t*metricInfo[V]\n\tlabelMapper func(L) []KeyValue\n}\n\nfunc (g *GaugeGroup[L, V]) With(labels L) *Gauge[V] {\n\tts := g.get(labels)\n\treturn &Gauge[V]{metricInfo: g.metricInfo, ts: ts}\n}\n\nfunc (g *GaugeGroup[L, V]) get(labels L) *timeseries[V] {\n\tts, setup := g.metricInfo.getTS(labels)\n\tif !setup {\n\t\tts.setup(g.labelMapper(labels))\n\t} else {\n\t\t// Wait for the timeseries to be initialized before we continue.\n\t\tts.init.Wait()\n\t}\n\treturn ts\n}\n\nfunc newMetricInfo[V Value](mgr *Registry, name string, typ MetricType, svcNum uint16) *metricInfo[V] {\n\tadd := getAtomicAdder[V]()\n\tset := getAtomicSetter[V]()\n\tinc := getAtomicIncrementer[V](add)\n\n\treturn &metricInfo[V]{\n\t\treg:    mgr,\n\t\tname:   name,\n\t\ttyp:    typ,\n\t\tsvcNum: svcNum,\n\n\t\tadd: add,\n\t\tset: set,\n\t\tinc: inc,\n\t}\n}\n\ntype metricInfo[V Value] struct {\n\treg    *Registry\n\tname   string\n\ttyp    MetricType\n\tsvcNum uint16\n\n\tadd func(addr *V, val V)\n\tset func(addr *V, val V)\n\tinc func(addr *V)\n}\n\nfunc (m *metricInfo[V]) svcIdx() (idx uint16, ok bool) {\n\tif m.svcNum > 0 {\n\t\treturn 0, true\n\t} else if curr := m.reg.rt.Current(); curr.SvcNum > 0 {\n\t\treturn curr.SvcNum - 1, true\n\t}\n\treturn 0, false\n}\n\nfunc (m *metricInfo[V]) getTS(labels any) (ts *timeseries[V], setup bool) {\n\tts, setup = getTS[V](m.reg, m.name, labels, m)\n\n\t// Initialize the values if they haven't yet been set up.\n\tif !setup {\n\t\tif m.svcNum > 0 {\n\t\t\tts.value = make([]V, 1)\n\t\t\tts.valid = make([]atomic.Bool, 1)\n\t\t} else {\n\t\t\tn := m.reg.numSvcs\n\t\t\tts.value = make([]V, n)\n\t\t\tts.valid = make([]atomic.Bool, n)\n\t\t}\n\t}\n\n\treturn ts, setup\n}\n\nfunc (m *metricInfo[V]) Name() string     { return m.name }\nfunc (m *metricInfo[V]) Type() MetricType { return m.typ }\nfunc (m *metricInfo[V]) SvcNum() uint16   { return m.svcNum }\n"
  },
  {
    "path": "runtimes/go/metrics/metrics_test.go",
    "content": "package metrics\n\nimport (\n\t\"reflect\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\nfunc TestCounter(t *testing.T) {\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tm := newMetricInfo[int64](mgr, \"foo\", CounterType, 1)\n\tc := newCounterInternal(m)\n\n\tts, loaded := m.getTS(nil)\n\teq(t, loaded, true)\n\teq(t, ts.init.state, 2)\n\teq(t, ts.info.Name(), \"foo\")\n\tif ts.labels != nil {\n\t\tt.Fatalf(\"got labels %+v, want nil\", ts.labels)\n\t}\n\n\teq(t, ts.value[0], 0)\n\tc.Increment()\n\tc.Add(2)\n\teq(t, ts.value[0], 3)\n\n\tc2 := newCounterInternal(m)\n\tts2, loaded2 := c2.getTS(nil)\n\teq(t, loaded2, true)\n\teq(t, ts2, ts)\n\n\tc2.Increment()\n\teq(t, ts.value[0], 4)\n\teq(t, countryRegistry(&mgr.registry), 1)\n}\n\nfunc TestCounter_MultipleServices(t *testing.T) {\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 2)\n\tm := newMetricInfo[int64](mgr, \"foo\", CounterType, 0)\n\tc := newCounterInternal(m)\n\n\tts, loaded := m.getTS(nil)\n\teq(t, loaded, true)\n\teq(t, ts.init.state, 2)\n\teq(t, ts.info.Name(), \"foo\")\n\tif ts.labels != nil {\n\t\tt.Fatalf(\"got labels %+v, want nil\", ts.labels)\n\t}\n\n\teq(t, len(ts.value), 2)\n\teq(t, ts.value[0], 0)\n\teq(t, ts.value[1], 0)\n\n\t// Without a service running these should be no-ops.\n\tc.Increment()\n\tc.Add(2)\n\teq(t, ts.value[0], 0)\n\teq(t, ts.value[1], 0)\n\n\t// Inside a request they should work.\n\t{\n\t\trt.BeginRequest(&model.Request{SvcNum: 1})\n\t\tc.Increment()\n\t\tc.Add(2)\n\t\teq(t, ts.value[0], 3)\n\t\teq(t, ts.value[1], 0)\n\t\trt.FinishRequest(false)\n\t}\n\n\t// Without a service running these should be no-ops again.\n\tc.Increment()\n\tc.Add(2)\n\teq(t, ts.value[0], 3)\n\teq(t, ts.value[1], 0)\n\n\t// Inside a request they should work.\n\t{\n\t\trt.BeginRequest(&model.Request{SvcNum: 2})\n\t\tc.Increment()\n\t\teq(t, ts.value[0], 3)\n\t\teq(t, ts.value[1], 1)\n\t\trt.FinishRequest(false)\n\t}\n\n\t// Without a service running these should be no-ops again.\n\tc.Increment()\n\tc.Add(2)\n\teq(t, ts.value[0], 3)\n\teq(t, ts.value[1], 1)\n}\n\nfunc TestGauge(t *testing.T) {\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tm := newMetricInfo[float64](mgr, \"foo\", GaugeType, 1)\n\tc := newGauge(m)\n\n\tts, loaded := m.getTS(nil)\n\teq(t, loaded, true)\n\teq(t, ts.init.state, 2)\n\teq(t, ts.info.Name(), \"foo\")\n\tif ts.labels != nil {\n\t\tt.Fatalf(\"got labels %+v, want nil\", ts.labels)\n\t}\n\n\teq(t, ts.value[0], 0)\n\tc.Set(1.5)\n\teq(t, ts.value[0], 1.5)\n\n\tc2 := newGauge(m)\n\tts2, loaded2 := c2.getTS(nil)\n\teq(t, loaded2, true)\n\teq(t, ts2, ts)\n\n\tc2.Set(2)\n\teq(t, ts.value[0], 2)\n\n\teq(t, countryRegistry(&mgr.registry), 1)\n}\n\nfunc TestCounterGroup(t *testing.T) {\n\ttype myLabels struct {\n\t\tkey string\n\t}\n\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tc := newCounterGroup[myLabels, int64](mgr, \"foo\", CounterConfig{\n\t\tEncoreInternal_SvcNum: 1,\n\t\tEncoreInternal_LabelMapper: func(labels myLabels) []KeyValue {\n\t\t\treturn []KeyValue{{Key: \"Key\", Value: labels.key}}\n\t\t},\n\t})\n\n\t// GaugeGroup loads time series on-demand.\n\teq(t, countryRegistry(&mgr.registry), 0)\n\tc.With(myLabels{key: \"foo\"}).Increment()\n\teq(t, countryRegistry(&mgr.registry), 1)\n\tc.With(myLabels{key: \"foo\"}).Add(2)\n\teq(t, countryRegistry(&mgr.registry), 1)\n\tc.With(myLabels{key: \"bar\"}).Add(5)\n\teq(t, countryRegistry(&mgr.registry), 2)\n\n\tts := c.get(myLabels{key: \"foo\"})\n\teq(t, ts.init.state, 2)\n\teq(t, ts.info.Name(), \"foo\")\n\tif !reflect.DeepEqual(ts.labels, []KeyValue{{Key: \"Key\", Value: \"foo\"}}) {\n\t\tt.Fatalf(\"got labels %+v, want [{Key foo}]\", ts.labels)\n\t}\n\n\teq(t, ts.value[0], 3)\n\n\tts2 := c.get(myLabels{key: \"foo\"})\n\teq(t, ts2, ts)\n\teq(t, countryRegistry(&mgr.registry), 2)\n\n}\n\nfunc TestGaugeGroup(t *testing.T) {\n\ttype myLabels struct {\n\t\tkey string\n\t}\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tc := newGaugeGroup[myLabels, float64](mgr, \"foo\", GaugeConfig{\n\t\tEncoreInternal_SvcNum: 1,\n\t\tEncoreInternal_LabelMapper: func(labels myLabels) []KeyValue {\n\t\t\treturn []KeyValue{{Key: \"Key\", Value: labels.key}}\n\t\t},\n\t})\n\n\t// GaugeGroup loads time series on-demand.\n\teq(t, countryRegistry(&mgr.registry), 0)\n\tc.With(myLabels{key: \"foo\"}).Set(1.5)\n\teq(t, countryRegistry(&mgr.registry), 1)\n\tc.With(myLabels{key: \"foo\"}).Set(2.5)\n\teq(t, countryRegistry(&mgr.registry), 1)\n\tc.With(myLabels{key: \"bar\"}).Set(3.5)\n\teq(t, countryRegistry(&mgr.registry), 2)\n\n\tts := c.get(myLabels{key: \"foo\"})\n\teq(t, ts.init.state, 2)\n\teq(t, ts.info.Name(), \"foo\")\n\tif !reflect.DeepEqual(ts.labels, []KeyValue{{Key: \"Key\", Value: \"foo\"}}) {\n\t\tt.Fatalf(\"got labels %+v, want [{Key foo}]\", ts.labels)\n\t}\n\n\teq(t, ts.value[0], 2.5)\n\n\tts2 := c.get(myLabels{key: \"foo\"})\n\teq(t, ts2, ts)\n\teq(t, countryRegistry(&mgr.registry), 2)\n}\n\nfunc BenchmarkCounter_Inc(b *testing.B) {\n\tb.ReportAllocs()\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tm := newMetricInfo[int64](mgr, \"foo\", CounterType, 1)\n\tc := newCounterInternal(m)\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tc.Increment()\n\t}\n\teq(b, c.ts.value[0], int64(b.N))\n}\n\nfunc BenchmarkCounter_NewLabel(b *testing.B) {\n\ttype myLabels struct {\n\t\tkey string\n\t}\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tc := newCounterGroup[myLabels, int64](mgr, \"foo\", CounterConfig{\n\t\tEncoreInternal_SvcNum: 1,\n\t\tEncoreInternal_LabelMapper: func(labels myLabels) []KeyValue {\n\t\t\treturn []KeyValue{{Key: \"Key\", Value: labels.key}}\n\t\t},\n\t})\n\n\tkeys := make([]string, b.N)\n\tfor i := 0; i < b.N; i++ {\n\t\tkeys[i] = strconv.Itoa(i)\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tc.With(myLabels{key: keys[i]}).Increment()\n\t}\n}\n\nfunc BenchmarkCounter_NewLabelSometimes(b *testing.B) {\n\ttype myLabels struct {\n\t\tkey string\n\t}\n\trt := reqtrack.New(zerolog.Logger{}, nil, nil)\n\tmgr := NewRegistry(rt, 1)\n\tc := newCounterGroup[myLabels, int64](mgr, \"foo\", CounterConfig{\n\t\tEncoreInternal_SvcNum: 1,\n\t\tEncoreInternal_LabelMapper: func(labels myLabels) []KeyValue {\n\t\t\treturn []KeyValue{{Key: \"Key\", Value: labels.key}}\n\t\t},\n\t})\n\n\tdenom := 10\n\tnumLabels := b.N / denom\n\n\tkeys := make([]string, numLabels)\n\tfor i := 0; i < numLabels; i++ {\n\t\tkeys[i] = strconv.Itoa(i)\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor i := 0; i < numLabels; i++ {\n\t\tfor j := 0; j < denom; j++ {\n\t\t\tc.With(myLabels{key: keys[i]}).Increment()\n\t\t}\n\t}\n}\n\nfunc eq[Val comparable](t testing.TB, got, want Val) {\n\tt.Helper()\n\tif got != want {\n\t\tt.Fatalf(\"got %v, want %v\", got, want)\n\t}\n}\n\nfunc countryRegistry(reg *sync.Map) int {\n\tvar count int\n\treg.Range(func(key, value interface{}) bool {\n\t\tcount++\n\t\treturn true\n\t})\n\treturn count\n}\n"
  },
  {
    "path": "runtimes/go/metrics/pkgfn.go",
    "content": "//go:build encore_app\n\npackage metrics\n\n// NewCounter creates a new counter metric, without any labels.\n// Use NewCounterGroup for metrics with labels.\nfunc NewCounter[V Value](name string, cfg CounterConfig) *Counter[V] {\n\treturn newCounterInternal[V](newMetricInfo[V](Singleton, name, CounterType, cfg.EncoreInternal_SvcNum))\n}\n\n// NewCounterGroup creates a new counter group with a set of labels,\n// where each unique combination of labels becomes its own counter.\n//\n// The Labels type must be a named struct, where each field corresponds to\n// a single label. Each field must be of type string.\nfunc NewCounterGroup[L Labels, V Value](name string, cfg CounterConfig) *CounterGroup[L, V] {\n\treturn newCounterGroup[L, V](Singleton, name, cfg)\n}\n\n// NewGauge creates a new counter metric, without any labels.\n// Use NewGaugeGroup for metrics with labels.\nfunc NewGauge[V Value](name string, cfg GaugeConfig) *Gauge[V] {\n\treturn newGauge[V](newMetricInfo[V](Singleton, name, GaugeType, cfg.EncoreInternal_SvcNum))\n}\n\n// NewGaugeGroup creates a new gauge group with a set of labels,\n// where each unique combination of labels becomes its own gauge.\n//\n// The Labels type must be a named struct, where each field corresponds to\n// a single label. Each field must be of type string.\nfunc NewGaugeGroup[L Labels, V Value](name string, cfg GaugeConfig) *GaugeGroup[L, V] {\n\treturn newGaugeGroup[L, V](Singleton, name, cfg)\n}\n"
  },
  {
    "path": "runtimes/go/metrics/registry_internal.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"encore.dev/appruntime/shared/nativehist\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\ntype Registry struct {\n\trt       *reqtrack.RequestTracker\n\tnumSvcs  uint16\n\ttsid     uint64\n\tregistry sync.Map // map[registryKey]*timeseries\n}\n\nfunc NewRegistry(rt *reqtrack.RequestTracker, numServicesInBinary int) *Registry {\n\treturn &Registry{rt: rt, numSvcs: uint16(numServicesInBinary)}\n}\n\nfunc (r *Registry) Collect() []CollectedMetric {\n\tmetrics := make([]CollectedMetric, 0, 128)\n\tr.registry.Range(func(key, value any) bool {\n\t\tswitch val := value.(type) {\n\t\tcase *timeseries[int64]:\n\t\t\tmetrics = append(metrics, CollectedMetric{\n\t\t\t\tInfo:         val.info,\n\t\t\t\tTimeSeriesID: val.id,\n\t\t\t\tLabels:       val.labels,\n\t\t\t\tVal:          val.value,\n\t\t\t\tValid:        val.valid,\n\t\t\t})\n\t\tcase *timeseries[uint64]:\n\t\t\tmetrics = append(metrics, CollectedMetric{\n\t\t\t\tInfo:         val.info,\n\t\t\t\tTimeSeriesID: val.id,\n\t\t\t\tLabels:       val.labels,\n\t\t\t\tVal:          val.value,\n\t\t\t\tValid:        val.valid,\n\t\t\t})\n\t\tcase *timeseries[float64]:\n\t\t\tmetrics = append(metrics, CollectedMetric{\n\t\t\t\tInfo:         val.info,\n\t\t\t\tTimeSeriesID: val.id,\n\t\t\t\tLabels:       val.labels,\n\t\t\t\tVal:          val.value,\n\t\t\t\tValid:        val.valid,\n\t\t\t})\n\t\tcase *timeseries[*nativehist.Histogram]:\n\t\t\tmetrics = append(metrics, CollectedMetric{\n\t\t\t\tInfo:         val.info,\n\t\t\t\tTimeSeriesID: val.id,\n\t\t\t\tLabels:       val.labels,\n\t\t\t\tVal:          val.value,\n\t\t\t\tValid:        val.valid,\n\t\t\t})\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unhandled timeseries type %T\", val))\n\t\t}\n\t\treturn true\n\t})\n\treturn metrics\n}\n\ntype MetricType int\n\nconst (\n\tCounterType MetricType = iota\n\tGaugeType\n\tHistogramType\n)\n\ntype MetricInfo interface {\n\tName() string\n\tType() MetricType\n\tSvcNum() uint16\n}\n\ntype CollectedMetric struct {\n\tInfo         MetricInfo\n\tTimeSeriesID uint64\n\tLabels       []KeyValue\n\tVal          any // []T where T is any of Value\n\tValid        []atomic.Bool\n}\n\ntype registryKey struct {\n\tmetricName string\n\tlabels     any // guaranteed to be comparable\n}\n\ntype timeseries[T any] struct {\n\tinfo   MetricInfo\n\tid     uint64\n\tinit   initGate\n\tlabels []KeyValue\n\tvalue  []T\n\tvalid  []atomic.Bool\n}\n\nfunc (ts *timeseries[V]) setup(labels []KeyValue) {\n\tts.init.Start()\n\tdefer ts.init.Done()\n\tts.labels = labels\n}\n\ntype KeyValue struct {\n\tKey   string\n\tValue string\n}\n\nfunc getTS[T any](r *Registry, name string, labels any, info MetricInfo) (ts *timeseries[T], loaded bool) {\n\tkey := registryKey{metricName: name, labels: labels}\n\tif val, ok := r.registry.Load(key); ok {\n\t\treturn val.(*timeseries[T]), true\n\t}\n\tval, loaded := r.registry.LoadOrStore(key, &timeseries[T]{\n\t\tinfo: info,\n\t\tid:   atomic.AddUint64(&r.tsid, 1),\n\t})\n\treturn val.(*timeseries[T]), loaded\n}\n"
  },
  {
    "path": "runtimes/go/metrics/singleton_internal.go",
    "content": "//go:build encore_app\n\npackage metrics\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\nvar Singleton = NewRegistry(reqtrack.Singleton, len(appconf.Static.BundledServices))\n"
  },
  {
    "path": "runtimes/go/metrics/units.go",
    "content": "package metrics\n\n// TODO Document\n\ntype Value interface {\n\tuint64 | int64 | float64\n}\n"
  },
  {
    "path": "runtimes/go/middleware/middleware.go",
    "content": "// Package middleware provides middleware functionality for defining\n// generic processing across multiple API endpoints, typically for\n// cross-cutting concerns like validation, caching, or error monitoring.\n//\n// For documentation on how to use middleware in Encore see https://encore.dev/docs/develop/middleware.\npackage middleware\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\tencore \"encore.dev\"\n)\n\n// Signature is the signature a middleware should have.\n// The implementation must return a Response which then\n// becomes the API's response.\n//\n// Middleware processing forms a chain, where the first\n// defined middleware is called first. The provided next\n// function is used to invoke the next middleware in the\n// chain, or the actual API handler if it's the last middleware.\n//\n// The implementation should call next at most once.\n// If the middleware encounters an error it is permissible to\n// not call next and directly return a Response with Err set\n// to a non-nil value.\n//\n// The type exists solely for documentation purposes;\n// it is otherwise not used.\ntype Signature func(req Request, next Next) Response\n\n// Next is the function to invoke the next middleware in the chain,\n// or the actual API handler if the middleware is the last one.\ntype Next func(Request) Response\n\n// A Request provides information about the incoming request that\n// the middleware is processing.\ntype Request struct {\n\tctx   context.Context\n\tcache *reqCache\n}\n\n// WithContext returns a new Request with the context set to ctx.\nfunc (r *Request) WithContext(ctx context.Context) Request {\n\tr2 := *r\n\tr2.ctx = ctx\n\treturn r2\n}\n\n// Context reports the request's context.\nfunc (r *Request) Context() context.Context {\n\treturn r.ctx\n}\n\n// Data returns information about the request.\nfunc (r *Request) Data() *encore.Request {\n\treturn r.cache.Get()\n}\n\n// Response represents the API handler's response.\n// It can be modified by each middleware to customize\n// the API response.\n//\n// The Payload's type must not be changed: it is not permissible\n// to return a payload with a different type than the API returns.\n//\n// If Err is non-nil it becomes serialized as the response.\n//\n// For Raw endpoints neither Payload nor Err are used.\ntype Response struct {\n\t// Payload is the API's response payload.\n\t// It is nil if the API handler returned an error or if\n\t// the API is a raw endpoint.\n\t//\n\t// The payload can be mutated by middleware but the type must not be changed;\n\t// it must always be of the same type as the API handler's return type.\n\t// Otherwise, an error is returned.\n\tPayload any\n\n\t// Err is the error returned from the API handler or another middleware.\n\t// It is written as output for non-raw endpoints.\n\tErr error\n\n\t// HTTPStatus is the HTTP status code the response is written with.\n\t//\n\t// If zero is returned from middleware, Encore will choose an appropriate status code based\n\t// status code depending on the type of error being returned (or 200 for success).\n\t//\n\t// If a non-zero value is returned from a middleware, Encore will use that status code\n\t// regardless of what Err is set to.\n\t//\n\t// For raw handlers middleware cannot modify this as it has already\n\t// been written to the network.\n\tHTTPStatus int\n\n\t// headers are HTTP headers to add to the response, lazily initialized.\n\t// Use Header() method to access.\n\theaders http.Header\n}\n\n// Header returns the header map that will be sent by the response.\n// The returned map is lazily initialized on first call.\n// Changing the header map after a call to WriteHeader has no effect.\n//\n// For raw handlers middleware cannot modify headers as the response has already\n// been written to the network.\nfunc (r *Response) Header() http.Header {\n\tif r.headers == nil {\n\t\tr.headers = make(http.Header)\n\t}\n\treturn r.headers\n}\n\n// GetHeaders returns the headers map, or nil if no headers have been set.\n// This method is used internally by the Encore runtime.\n//\n//publicapigen:drop\nfunc (r *Response) GetHeaders() http.Header {\n\treturn r.headers\n}\n\n// NewRequest constructs a new Request that returns the given context and request data.\n// It is primarily used for testing middleware.\nfunc NewRequest(ctx context.Context, data *encore.Request) Request {\n\treturn Request{\n\t\tctx:   ctx,\n\t\tcache: newReqCache(func() *encore.Request { return data }),\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/middleware/middleware_internal.go",
    "content": "package middleware\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\tencore \"encore.dev\"\n)\n\nfunc newReqCache(fn func() *encore.Request) *reqCache {\n\treturn &reqCache{load: fn}\n}\n\ntype reqCache struct {\n\tloadOnce sync.Once\n\tload     func() *encore.Request\n\treq      *encore.Request\n}\n\nfunc (r *reqCache) Get() *encore.Request {\n\tr.loadOnce.Do(func() { r.req = r.load() })\n\treturn r.req\n}\n\nfunc NewLazyRequest(ctx context.Context, fn func() *encore.Request) Request {\n\treturn Request{ctx: ctx, cache: newReqCache(fn)}\n}\n"
  },
  {
    "path": "runtimes/go/package.go",
    "content": "// Package encore provides the runtime API contract, which Encore applications are build against.\n//\n// # Encore – The Backend Development Engine\n//\n// https://encore.dev\n//\n// Encore makes it incredibly simple to create distributed systems, backend services and APIs. While still deploying to\n// your own cloud account, Encore helps you escape the maze of cloud complexity:\n//\n// - No endless repetition of boilerplate.\n//\n// - No infrastructure to worry about.\n//\n// - No reinventing the wheel.\n//\n// Start building with a fantastic flow state experience that unlocks your creative potential.\n// All of this is freely available, based on the Open Source Encore Go Framework.\n//\n// For more information visit the website https://encore.dev or the documentation at https://encore.dev/docs.\n//\n// # Package encore\n//\n// This package provides the APIs for getting AppMetadata about the current application and the CurrentRequest.\n// For more information see https://encore.dev/docs/develop/metadata.\npackage encore\n"
  },
  {
    "path": "runtimes/go/pkgfn.go",
    "content": "//go:build encore_app\n\npackage encore\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\n//publicapigen:drop\nvar Singleton = NewManager(appconf.Static, appconf.Runtime, reqtrack.Singleton)\n\n// Meta returns metadata about the running application.\n//\n// Meta will never return nil.\nfunc Meta() *AppMetadata {\n\treturn Singleton.Meta()\n}\n\n// CurrentRequest returns the Request that is currently being handled by the calling goroutine\n//\n// It is safe for concurrent use and will return a new Request on each evocation, so can be mutated by the\n// calling code without impacting future calls.\n//\n// CurrentRequest never returns nil.\nfunc CurrentRequest() *Request {\n\treturn Singleton.CurrentRequest()\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/aws/manager.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsConfig \"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sns\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\t\"github.com/rs/xid\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\ntype Manager struct {\n\tctxs *utils.Contexts\n\n\t// publisherID is a unique ID for this Encore app instance, used as the Message Group ID\n\t// for topics which don't specify a grouping field. This is based on [AWS's recommendation]\n\t// that each producer should have a unique message group ID to send all it's messages.\n\t//\n\t// We use an XID here as we've already got the library included within the runtime and it has an excellent\n\t// way of generating unique IDs in a distributed system without needing to talk to a central service.\n\t//\n\t// [AWS's recommendation]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-understanding-logic.html\n\tpublisherID xid.ID\n\n\tcfgOnce   sync.Once\n\tawsCfg    aws.Config\n\tsnsClient *sns.Client\n\tsqsClient *sqs.Client\n}\n\nfunc NewManager(ctxs *utils.Contexts) *Manager {\n\treturn &Manager{ctxs: ctxs, publisherID: xid.New()}\n}\n\nfunc (mgr *Manager) ProviderName() string { return \"aws\" }\n\nfunc (mgr *Manager) Matches(cfg *config.PubsubProvider) bool {\n\treturn cfg.AWS != nil\n}\n\n// getConfig loads the required AWS config to connect to AWS\nfunc (mgr *Manager) getConfig(ctx context.Context) aws.Config {\n\tmgr.cfgOnce.Do(func() {\n\t\tcfg, err := awsConfig.LoadDefaultConfig(ctx)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"unable to load AWS config: %v\", err))\n\t\t}\n\t\tmgr.awsCfg = cfg\n\n\t})\n\n\treturn mgr.awsCfg\n}\n\nfunc (mgr *Manager) getSNSClient(ctx context.Context) *sns.Client {\n\tif mgr.snsClient == nil {\n\t\tmgr.snsClient = sns.NewFromConfig(mgr.getConfig(ctx))\n\t}\n\treturn mgr.snsClient\n}\n\nfunc (mgr *Manager) getSQSClient(ctx context.Context) *sqs.Client {\n\tif mgr.sqsClient == nil {\n\t\tmgr.sqsClient = sqs.NewFromConfig(mgr.getConfig(ctx))\n\t}\n\treturn mgr.sqsClient\n}\n\nfunc (mgr *Manager) NewTopic(_ *config.PubsubProvider, staticCfg types.TopicConfig, runtimeCfg *config.PubsubTopic) types.TopicImplementation {\n\tsnsClient := mgr.getSNSClient(mgr.ctxs.Connection)\n\tsqsClient := mgr.getSQSClient(mgr.ctxs.Connection)\n\n\t// Check we have permissions to interact with the given topic\n\t// otherwise the first time we will find out is when we try and publish to it\n\t_, err := snsClient.GetTopicAttributes(mgr.ctxs.Connection, &sns.GetTopicAttributesInput{\n\t\tTopicArn: aws.String(runtimeCfg.ProviderName),\n\t})\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"unable to verify SNS topic attributes (may be missing IAM role allowing access): %v\", err))\n\t}\n\n\treturn &topic{mgr.ctxs, mgr.publisherID, snsClient, sqsClient, staticCfg, runtimeCfg}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/aws/topic.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sns\"\n\tsnsTypes \"github.com/aws/aws-sdk-go-v2/service/sns/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\tsqsTypes \"github.com/aws/aws-sdk-go-v2/service/sqs/types\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\ntype topic struct {\n\tctxs        *utils.Contexts\n\tpublisherID xid.ID\n\tsnsClient   *sns.Client\n\tsqsClient   *sqs.Client\n\tstaticCfg   types.TopicConfig\n\truntimeCfg  *config.PubsubTopic\n}\n\nvar _ types.TopicImplementation = (*topic)(nil)\n\nfunc (t *topic) PublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\tattributes := make(map[string]snsTypes.MessageAttributeValue)\n\tfor key, value := range attrs {\n\t\tattributes[key] = snsTypes.MessageAttributeValue{\n\t\t\tDataType:    aws.String(\"String\"),\n\t\t\tStringValue: aws.String(value),\n\t\t}\n\t}\n\n\tparams := &sns.PublishInput{\n\t\tMessage:           aws.String(string(data)),\n\t\tMessageAttributes: attributes,\n\t\tTopicArn:          aws.String(t.runtimeCfg.ProviderName),\n\t}\n\n\t// For exactly-once delivery on AWS we need to:\n\t//\n\t// 1. Set a message group ID (as this is a requirement for FIFO queues)\n\t// 2. Set a message deduplication ID as this is required to enable exactly-once delivery\n\tif t.staticCfg.DeliveryGuarantee == types.ExactlyOnce {\n\t\tparams.MessageGroupId = aws.String(fmt.Sprintf(\"inst_%s\", t.publisherID.String()))\n\t\tparams.MessageDeduplicationId = aws.String(fmt.Sprintf(\"msg_%s\", xid.New().String()))\n\t}\n\n\t// If we have an explicit ordering key, use that as the message group ID and mark the topic as FIFO\n\tif orderingKey != \"\" {\n\t\tparams.MessageGroupId = aws.String(orderingKey)\n\t\tparams.MessageDeduplicationId = aws.String(fmt.Sprintf(\"msg_%s\", xid.New().String()))\n\t}\n\n\tresult, err := t.snsClient.Publish(ctx, params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn aws.ToString(result.MessageId), nil\n}\n\nfunc (t *topic) Subscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy, implCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\tackDeadline = utils.Clamp(ackDeadline, time.Second, 12*time.Hour)\n\n\tif maxConcurrency == 0 {\n\t\tmaxConcurrency = 1 // FIXME(domblack): This retains the old behaviour, but allows user customisation - in a future release we should remove this\n\t}\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlogger.Error().Interface(\"panic\", r).Msg(\"panic in subscriber, no longer processing messages\")\n\t\t\t} else {\n\t\t\t\tlogger.Info().Msg(\"subscriber stopped due to context cancellation\")\n\t\t\t}\n\t\t}()\n\n\t\tfor t.ctxs.Fetch.Err() == nil {\n\t\t\terr := utils.WorkConcurrently(\n\t\t\t\tt.ctxs,\n\t\t\t\tmaxConcurrency, 10,\n\t\t\t\tfunc(ctx context.Context, maxToFetch int) ([]sqsTypes.Message, error) {\n\t\t\t\t\t// We should only long poll for 20 seconds, so if this takes more than\n\t\t\t\t\t// 30 seconds we should cancel the context and try again\n\t\t\t\t\t//\n\t\t\t\t\t// We do this in case the ReceiveMessage call gets stuck on the server\n\t\t\t\t\t// and doesn't return\n\t\t\t\t\tctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\t\tdefer cancel()\n\n\t\t\t\t\tresp, err := t.sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{\n\t\t\t\t\t\tQueueUrl:              aws.String(implCfg.ProviderName),\n\t\t\t\t\t\tAttributeNames:        []sqsTypes.QueueAttributeName{\"ApproximateReceiveCount\"},\n\t\t\t\t\t\tMaxNumberOfMessages:   int32(maxToFetch),\n\t\t\t\t\t\tMessageAttributeNames: []string{\"All\"},\n\t\t\t\t\t\tVisibilityTimeout:     int32(ackDeadline.Seconds()),\n\t\t\t\t\t\tWaitTimeSeconds:       20, // Maximum allowed time\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\treturn resp.Messages, nil\n\t\t\t\t},\n\t\t\t\tfunc(ctx context.Context, msg sqsTypes.Message) error {\n\t\t\t\t\t// Parse the message body\n\t\t\t\t\tmsgWrapper := &SNSMessageWrapper{}\n\t\t\t\t\tif err := json.Unmarshal([]byte(aws.ToString(msg.Body)), msgWrapper); err != nil {\n\t\t\t\t\t\tlogger.Err(err).Str(\"sqs_msg_id\", aws.ToString(msg.MessageId)).Msg(\"unable to parse message\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get the delivery attempt number\n\t\t\t\t\tdeliveryAttempt, err := parseInt(msg.Attributes, \"ApproximateReceiveCount\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Warn().Err(err).Str(\"msg_id\", msgWrapper.MessageId).Msg(\"unable to parse receive count\")\n\t\t\t\t\t}\n\n\t\t\t\t\t// Extract the attributes\n\t\t\t\t\tattributes := make(map[string]string)\n\t\t\t\t\tfor key, value := range msgWrapper.MessageAttributes {\n\t\t\t\t\t\tswitch value.Type {\n\t\t\t\t\t\tcase \"String\":\n\t\t\t\t\t\t\tattributes[key] = value.Value\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tlogger.Warn().Err(err).Str(\"msg_id\", msgWrapper.MessageId).Str(\"attr_name\", key).Str(\"attr_type\", value.Type).Msg(\"unsupported attribute data type\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Call the callback, and if there was no error, then we can delete the message\n\t\t\t\t\tmsgCtx, cancel := context.WithTimeout(ctx, ackDeadline)\n\t\t\t\t\tdefer cancel()\n\t\t\t\t\terr = f(msgCtx, msgWrapper.MessageId, msgWrapper.Timestamp, int(deliveryAttempt), attributes, []byte(msgWrapper.Message))\n\t\t\t\t\tcancel()\n\n\t\t\t\t\t// Check if the context has been cancelled, and if so, return the error\n\t\t\t\t\tif ctx.Err() != nil {\n\t\t\t\t\t\treturn ctx.Err()\n\t\t\t\t\t}\n\n\t\t\t\t\t// We want to wait a maximum of 30 seconds for the callback to complete\n\t\t\t\t\t// otherwise we assume it has failed and we should retry\n\t\t\t\t\t//\n\t\t\t\t\t// We do this in case the callback gets stuck and doesn't return\n\t\t\t\t\tctx, responseCancel := context.WithTimeout(ctx, 30*time.Second)\n\t\t\t\t\tdefer responseCancel()\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Err(err).Str(\"msg_id\", msgWrapper.MessageId).Msg(\"unable to process message\")\n\n\t\t\t\t\t\t// If there was an error processing the message, apply the backoff policy\n\t\t\t\t\t\t_, delay := utils.GetDelay(retryPolicy.MaxRetries, retryPolicy.MinBackoff, retryPolicy.MaxBackoff, uint16(deliveryAttempt))\n\t\t\t\t\t\t_, visibilityChangeErr := t.sqsClient.ChangeMessageVisibility(t.ctxs.Connection, &sqs.ChangeMessageVisibilityInput{\n\t\t\t\t\t\t\tQueueUrl:          aws.String(implCfg.ProviderName),\n\t\t\t\t\t\t\tReceiptHandle:     msg.ReceiptHandle,\n\t\t\t\t\t\t\tVisibilityTimeout: int32(utils.Clamp(delay, time.Second, 12*time.Hour).Seconds()),\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif visibilityChangeErr != nil {\n\t\t\t\t\t\t\tlog.Warn().Err(visibilityChangeErr).Str(\"msg_id\", msgWrapper.MessageId).Msg(\"unable to change message visibility to apply backoff rules\")\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If the message was processed successfully, delete it from the queue\n\t\t\t\t\t\t_, err = t.sqsClient.DeleteMessage(t.ctxs.Connection, &sqs.DeleteMessageInput{\n\t\t\t\t\t\t\tQueueUrl:      aws.String(implCfg.ProviderName),\n\t\t\t\t\t\t\tReceiptHandle: msg.ReceiptHandle,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlogger.Err(err).Str(\"msg_id\", msgWrapper.MessageId).Msg(\"unable to delete message from SQS queue\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t)\n\n\t\t\tif err != nil && t.ctxs.Fetch.Err() == nil {\n\t\t\t\tlogger.Warn().Err(err).Msg(\"pubsub subscription failed, retrying in 5 seconds\")\n\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc parseInt(m map[string]string, key string) (int64, error) {\n\tvalue, ok := m[key]\n\tif !ok {\n\t\treturn 0, errors.New(\"attribute was not received\")\n\t}\n\n\treturn strconv.ParseInt(value, 10, 64)\n}\n\n// SNSMessageWrapper matches the JSON that is sent to SQS from an SNS subscription\ntype SNSMessageWrapper struct {\n\tType              string    `json:\"Type\"`\n\tMessageId         string    `json:\"MessageId\"`\n\tTopicArn          string    `json:\"TopicArn\"`\n\tMessage           string    `json:\"Message\"`\n\tTimestamp         time.Time `json:\"Timestamp\"`\n\tSignatureVersion  string    `json:\"SignatureVersion\"`\n\tSigningCertURL    string    `json:\"SigningCertURL\"`\n\tUnsubscribeURL    string    `json:\"UnsubscribeURL\"`\n\tMessageAttributes map[string]struct {\n\t\tType  string `json:\"Type\"`\n\t\tValue string `json:\"Value\"`\n\t} `json:\"MessageAttributes\"`\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/aws/topic_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\t\"github.com/rs/zerolog/log\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\nconst (\n\ttestTopicARN = \"arn:aws:sns:us-west-2:406859400861:test-app-1-test-env-1-doms-test-topic\"\n\ttestQueueURL = \"https://sqs.us-west-2.amazonaws.com/406859400861/test-app-1-test-env-1-doms-test-topic_test-subscriber\"\n)\n\nfunc Test_AWS_PubSub_E2E(t *testing.T) {\n\t// Skip this test if the access keys needed are not set\n\tif os.Getenv(\"AWS_ACCESS_KEY_ID\") == \"\" {\n\t\tt.Skip(\"AWS_ACCESS_KEY_ID is not set\")\n\t}\n\tif os.Getenv(\"AWS_SECRET_ACCESS_KEY\") == \"\" {\n\t\tt.Skip(\"AWS_SECRET_ACCESS_KEY is not set\")\n\t}\n\n\truntime := &config.Runtime{\n\t\tPubsubProviders: []*config.PubsubProvider{\n\t\t\t{AWS: &config.AWSPubsubProvider{}},\n\t\t},\n\t\tPubsubTopics: map[string]*config.PubsubTopic{\n\t\t\t\"test-topic\": {\n\t\t\t\tEncoreName:   \"test-topic\",\n\t\t\t\tProviderID:   0,\n\t\t\t\tProviderName: testTopicARN,\n\t\t\t\tSubscriptions: map[string]*config.PubsubSubscription{\n\t\t\t\t\t\"test-subscription\": {\n\t\t\t\t\t\tEncoreName:   \"test-subscription\",\n\t\t\t\t\t\tProviderName: testQueueURL,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tctxs := utils.NewContexts(ctx)\n\tdefer cancel()\n\tmgr := NewManager(ctxs)\n\n\ttopic := mgr.NewTopic(runtime.PubsubProviders[0], types.TopicConfig{DeliveryGuarantee: types.AtLeastOnce}, runtime.PubsubTopics[\"test-topic\"])\n\n\t// Purge the queue of any messages from previous failed tests\n\t_, err := mgr.getSQSClient(ctx).PurgeQueue(ctx, &sqs.PurgeQueueInput{\n\t\tQueueUrl: aws.String(testQueueURL),\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"failed to purge queue: %s\", err)\n\t}\n\n\t// Subscribe to the queue\n\tmsgChan := make(chan string)\n\tvar sentMessageID string\n\ttopic.Subscribe(&log.Logger, 0, time.Second, nil, runtime.PubsubTopics[\"test-topic\"].Subscriptions[\"test-subscription\"], func(ctx context.Context, msgID string, publishTime time.Time, deliveryAttempt int, attrs map[string]string, data []byte) error {\n\t\tif attrs[\"attr-1\"] != \"foo\" {\n\t\t\tt.Errorf(\"expected attr-1 to be foo, got %s\", attrs[\"attr-1\"])\n\t\t}\n\t\tif msgID != sentMessageID {\n\t\t\tt.Errorf(\"expected message ID to be %s, got %s\", sentMessageID, msgID)\n\t\t}\n\t\tif deliveryAttempt != 1 {\n\t\t\tt.Errorf(\"expected delivery attempt to be 1, got %d\", deliveryAttempt)\n\t\t}\n\t\tif publishTime.Before(time.Now().Add(-1 * time.Minute)) {\n\t\t\tt.Errorf(\"expected publish time to be within the last minute, got %s\", publishTime)\n\t\t}\n\t\tmsgChan <- string(data)\n\t\treturn nil\n\t})\n\n\t// Publish a message on the queue\n\tsentMessageID, err = topic.PublishMessage(context.Background(), \"\", map[string]string{\"attr-1\": \"foo\"}, []byte(\"{\\\"hello\\\":\\\"world\\\"}\"))\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %v\", err)\n\t}\n\n\t// Verify receipt of the message\n\tselect {\n\tcase msg := <-msgChan:\n\t\tif msg != \"{\\\"hello\\\":\\\"world\\\"}\" {\n\t\t\tt.Errorf(\"expected message to be {\\\"hello\\\":\\\"world\\\"}, got %s\", msg)\n\t\t}\n\n\t\t// Sleep to allow time for the subscription go routine to delete the message\n\t\ttime.Sleep(1 * time.Second)\n\tcase <-ctx.Done():\n\t\tt.Errorf(\"timed out waiting for message\")\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/azure/clients.go",
    "content": "package azure\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// getClient returns a singleton azure servicebus client for the given project or panics if it cannot be created.\nfunc (mgr *Manager) getClient(cfg *config.AzureServiceBusProvider) *azservicebus.Client {\n\tmgr.clientMu.RLock()\n\tclient, ok := mgr._clients[cfg.Namespace]\n\tmgr.clientMu.RUnlock()\n\tif ok {\n\t\treturn client\n\t}\n\tmgr.clientMu.Lock()\n\tdefer mgr.clientMu.Unlock()\n\t// Create a new client\n\tcredential, err := azidentity.NewDefaultAzureCredential(nil)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to create azure credential: %v\", err))\n\t}\n\tazclient, err := azservicebus.NewClient(\n\t\tfmt.Sprintf(\"%s.servicebus.windows.net\",\n\t\t\tcfg.Namespace), credential, nil)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to create azure client: %s\", err))\n\t}\n\tmgr._clients[cfg.Namespace] = azclient\n\n\treturn azclient\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/azure/topic.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/to\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n\t\"encore.dev/types/uuid\"\n)\n\nconst RetryCountAttribute = \"encore-retry-count\"\nconst TargetSubAttribute = \"encore-target-sub\"\n\ntype Manager struct {\n\tctxs *utils.Contexts\n\n\tclientMu sync.RWMutex\n\t_clients map[string]*azservicebus.Client // access via getClient()\n}\n\nfunc NewManager(ctxs *utils.Contexts) *Manager {\n\treturn &Manager{ctxs: ctxs, _clients: map[string]*azservicebus.Client{}}\n}\n\nfunc (mgr *Manager) ProviderName() string { return \"azure\" }\n\nfunc (mgr *Manager) Matches(cfg *config.PubsubProvider) bool {\n\treturn cfg.Azure != nil\n}\n\ntype topic struct {\n\tmgr        *Manager\n\tclient     *azservicebus.Client\n\ttopicCfg   *config.PubsubTopic\n\tsenderOnce sync.Once\n\t_sender    *azservicebus.Sender\n}\n\nvar _ types.TopicImplementation = (*topic)(nil)\n\nfunc (mgr *Manager) NewTopic(providerCfg *config.PubsubProvider, _ types.TopicConfig, runtimeCfg *config.PubsubTopic) types.TopicImplementation {\n\t// Create the topic\n\tclient := mgr.getClient(providerCfg.Azure)\n\treturn &topic{mgr: mgr, client: client, topicCfg: runtimeCfg}\n}\n\nfunc (t *topic) sender() *azservicebus.Sender {\n\tt.senderOnce.Do(func() {\n\t\tsender, err := t.client.NewSender(t.topicCfg.ProviderName, nil)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"failed to create pubsub sender for topic %s: %s\", t.topicCfg.EncoreName, err))\n\t\t}\n\t\tt._sender = sender\n\t})\n\treturn t._sender\n}\n\nfunc (t *topic) PublishMessage(ctx context.Context, groupingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\n\tmessageID, err := uuid.NewV4()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate message ID: %v\", err.Error())\n\t}\n\tmsg := &azservicebus.Message{\n\t\tMessageID:             to.Ptr(messageID.String()),\n\t\tBody:                  data,\n\t\tApplicationProperties: map[string]interface{}{},\n\t}\n\tfor k, v := range attrs {\n\t\tmsg.ApplicationProperties[k] = v\n\t}\n\n\t// Attempt to publish the message\n\terr = t.sender().SendMessage(ctx, msg, nil)\n\treturn *msg.MessageID, err\n}\n\nfunc (t *topic) scheduleRetry(subName string, msg *azservicebus.ReceivedMessage, backoff time.Duration) error {\n\tretryCount, _ := strconv.ParseInt(fmt.Sprintf(\"%v\", msg.ApplicationProperties[RetryCountAttribute]), 10, 64)\n\tmsg.ApplicationProperties[RetryCountAttribute] = retryCount + 1\n\tmsg.ApplicationProperties[TargetSubAttribute] = subName\n\n\treMsg := &azservicebus.Message{\n\t\tApplicationProperties: msg.ApplicationProperties,\n\t\tBody:                  msg.Body,\n\t\tContentType:           msg.ContentType,\n\t\tCorrelationID:         msg.CorrelationID,\n\t\tMessageID:             &msg.MessageID,\n\t\tPartitionKey:          msg.PartitionKey,\n\t\tReplyTo:               msg.ReplyTo,\n\t\tReplyToSessionID:      msg.ReplyToSessionID,\n\t\tSessionID:             msg.SessionID,\n\t\tSubject:               msg.Subject,\n\t\tTimeToLive:            msg.TimeToLive,\n\t\tTo:                    msg.To,\n\t}\n\t_, err := t.sender().ScheduleMessages(\n\t\tt.mgr.ctxs.Connection, []*azservicebus.Message{reMsg}, time.Now().Add(backoff), nil)\n\treturn err\n}\n\nfunc (t *topic) processMessage(\n\tctx context.Context,\n\tlogger *zerolog.Logger, receiver *azservicebus.Receiver, ackDeadline time.Duration, subCfg *config.PubsubSubscription,\n\tmsg *azservicebus.ReceivedMessage, rp *types.RetryPolicy, f types.RawSubscriptionCallback) (err error) {\n\n\tctx, cancel := context.WithTimeout(ctx, ackDeadline)\n\tdefer cancel()\n\n\tattrs := make(map[string]string, len(msg.ApplicationProperties))\n\tfor k, v := range msg.ApplicationProperties {\n\t\tattrs[k] = fmt.Sprintf(\"%v\", v)\n\t}\n\tretryCount, _ := strconv.ParseInt(fmt.Sprintf(\"%v\", msg.ApplicationProperties[RetryCountAttribute]), 10, 64)\n\tdeliveryAttempt := retryCount + 1\n\terr = f(ctx, msg.MessageID, *msg.EnqueuedTime, int(deliveryAttempt), attrs, msg.Body)\n\tif err != nil {\n\t\tlogger.Warn().Err(err).Msg(\"failed to process messsage\")\n\t\tshouldRetry, backoff := utils.GetDelay(\n\t\t\trp.MaxRetries, rp.MinBackoff, rp.MaxBackoff, uint16(deliveryAttempt))\n\t\tif !shouldRetry {\n\t\t\tlogger.Warn().Msg(\"deadlettering msg\")\n\t\t\terr = receiver.DeadLetterMessage(t.mgr.ctxs.Connection, msg, &azservicebus.DeadLetterOptions{\n\t\t\t\tErrorDescription:   to.Ptr(fmt.Sprintf(\"failed to process message after %v retries\", deliveryAttempt)),\n\t\t\t\tReason:             to.Ptr(\"ExhaustedRetries\"),\n\t\t\t\tPropertiesToModify: map[string]interface{}{RetryCountAttribute: 0},\n\t\t\t})\n\t\t} else {\n\t\t\tlogger.Warn().Msgf(\"scheduling msg retry in %v (attempt %v)\", backoff, deliveryAttempt)\n\t\t\terr = t.scheduleRetry(subCfg.ProviderName, msg, backoff)\n\t\t}\n\t}\n\t// if err == nil we have either successfully processed the message or we have scheduled/deadlettered it\n\tif err == nil {\n\t\terr = receiver.CompleteMessage(t.mgr.ctxs.Connection, msg, nil)\n\t\tif err != nil {\n\t\t\tlogger.Warn().Err(err).Msg(\"failed to complete message\")\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (t *topic) Subscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy, subCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\treceiver, err := t.client.NewReceiverForSubscription(t.topicCfg.ProviderName, subCfg.ProviderName, nil)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to create pubsub receiver for subscription %s: %s\", subCfg.EncoreName, err))\n\t}\n\n\tif maxConcurrency == 0 {\n\t\tmaxConcurrency = 1 // FIXME(domblack): This retains the old behaviour, but allows user customisation - in a future release we should remove this\n\t}\n\n\t// Start the subscription\n\tgo func() {\n\t\tfor t.mgr.ctxs.Fetch.Err() == nil {\n\t\t\terr := utils.WorkConcurrently(\n\t\t\t\tt.mgr.ctxs, maxConcurrency, 0,\n\t\t\t\tfunc(ctx context.Context, maxToFetch int) ([]*azservicebus.ReceivedMessage, error) {\n\t\t\t\t\t// Subscribe to the topic to receive messages\n\t\t\t\t\tmessages, err := receiver.ReceiveMessages(ctx, maxToFetch, nil)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\treturn messages, nil\n\t\t\t\t},\n\t\t\t\tfunc(ctx context.Context, work *azservicebus.ReceivedMessage) error {\n\t\t\t\t\treturn t.processMessage(ctx, logger, receiver, ackDeadline, subCfg, work, retryPolicy, f)\n\t\t\t\t},\n\t\t\t)\n\n\t\t\t// If there was an error and we're not shutting down, log it and then sleep for a bit before trying again\n\t\t\tif err != nil && t.mgr.ctxs.Fetch.Err() == nil {\n\t\t\t\tlogger.Warn().Err(err).Msg(\"pubsub subscription failed, retrying in 5 seconds\")\n\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t}\n\t\t}\n\n\t}()\n\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/encorecloud/manager.go",
    "content": "package encorecloud\n\nimport (\n\t\"go.encore.dev/platform-sdk\"\n\t\"go.encore.dev/platform-sdk/encorecloud\"\n\t\"go.encore.dev/platform-sdk/pkg/auth\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\ntype Manager struct {\n\tctxs         *utils.Contexts\n\tclient       *encorecloud.Client\n\tpushRegistry types.PushEndpointRegistry\n}\n\nfunc NewManager(ctxs *utils.Contexts, runtime *config.Runtime, pushRegistry types.PushEndpointRegistry) *Manager {\n\t// It's possible that the runtime is nil, for example if the app isn't using this manager\n\t// so we need to check for that.\n\tserver := \"\"\n\tvar authKeys []auth.Key\n\tif runtime.EncoreCloudAPI != nil {\n\t\tserver = runtime.EncoreCloudAPI.Server\n\t\tauthKeys = runtime.EncoreCloudAPI.AuthKeys\n\t}\n\n\tsdk := platform.NewSDK(\n\t\tplatform.WithHost(server),\n\t\tplatform.WithAppDetails(runtime.AppSlug, runtime.EnvName),\n\t\tplatform.WithAuthKeys(authKeys...),\n\t)\n\treturn &Manager{ctxs: ctxs, client: sdk.EncoreCloud, pushRegistry: pushRegistry}\n}\n\nfunc (mgr *Manager) ProviderName() string {\n\treturn \"encorecloud\"\n}\n\nfunc (mgr *Manager) Matches(providerCfg *config.PubsubProvider) bool {\n\treturn providerCfg.EncoreCloud != nil\n}\n\nfunc (mgr *Manager) NewTopic(_ *config.PubsubProvider, _ types.TopicConfig, runtimeCfg *config.PubsubTopic) types.TopicImplementation {\n\treturn &topic{mgr, runtimeCfg}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/encorecloud/topic.go",
    "content": "package encorecloud\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n)\n\ntype topic struct {\n\tmgr *Manager\n\tcfg *config.PubsubTopic\n}\n\nfunc (t *topic) PublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\treturn t.mgr.client.PublishToTopic(ctx, t.cfg.ProviderName, orderingKey, attrs, data)\n}\n\nfunc (t *topic) Subscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy, subCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\tif subCfg.ID == \"\" {\n\t\tpanic(\"encorecloud pubsub subscriptions must have an ID\")\n\t}\n\n\t// registerPushEndpoint registers a push endpoint for a subscription from Encore Cloud\n\tt.mgr.pushRegistry.RegisterPushSubscriptionHandler(\n\t\ttypes.SubscriptionID(subCfg.ID),\n\t\tt.mgr.client.CreateSubscriptionHandler(subCfg.ID, logger, f),\n\t)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/gcp/clients.go",
    "content": "package gcp\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"cloud.google.com/go/pubsub\"\n)\n\n// getClientForProject returns a singleton pubsub client for the given project or panics if it cannot be created.\nfunc (mgr *Manager) getClientForProject(projectID string) *pubsub.Client {\n\tmgr.clientsMu.Lock()\n\tdefer mgr.clientsMu.Unlock()\n\n\tclient, ok := mgr.clients[projectID]\n\tif !ok {\n\t\t// Create a new client\n\t\tcl, err := pubsub.NewClient(mgr.ctxs.Connection, projectID)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"failed to create pubsub client: %s\", err))\n\t\t}\n\t\tclient = cl\n\t\tmgr.clients[projectID] = cl\n\t}\n\n\treturn client\n}\n\n// numGoroutines computes the number of goroutines to use for the subscription,\n// by adaptively taking into account gRPC stream limits and the number of subscriptions.\nfunc numGoroutines(numSubs int) int {\n\tif numSubs < 1 {\n\t\tnumSubs = 1 // avoid division by zero\n\t}\n\n\tmaxProcs := runtime.GOMAXPROCS(0)\n\tnumConns := min(4, maxProcs)\n\tmaxStreams := numConns * 90\n\n\t// Clamp to [1, 10].\n\treturn max(min(maxStreams/numSubs, 10), 1)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/gcp/push_handler.go",
    "content": "package gcp\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\t\"google.golang.org/api/idtoken\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/internal/platformauth\"\n\t\"encore.dev/pubsub/internal/types\"\n)\n\n// This is documented in https://cloud.google.com/pubsub/docs/push\ntype pushPayload struct {\n\tMessage struct {\n\t\tAttributes  map[string]string `json:\"attributes\"`\n\t\tData        []byte            `json:\"data\"`\n\t\tMessageID   string            `json:\"messageId\"`\n\t\tPublishTime time.Time         `json:\"publishTime\"`\n\t} `json:\"message\"`\n\tSubscription    string `json:\"subscription\"`\n\tDeliveryAttempt int    `json:\"deliveryAttempt,omitempty\"` // Field documented in: https://cloud.google.com/pubsub/docs/handling-failures#track_delivery_attempts\n}\n\nfunc (mgr *Manager) registerPushEndpoint(logger *zerolog.Logger, subscriptionConfig *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\thandler := func(req *http.Request) error {\n\t\t// If the request has not come from the Encore platform it must have\n\t\t// a valid JWT set by Google.\n\t\tif !platformauth.IsEncorePlatformRequest(req.Context()) {\n\t\t\tif err := mgr.validateGoogleJWT(req, subscriptionConfig.GCP.PushServiceAccount); err != nil {\n\t\t\t\treturn errs.Wrap(err, \"unable to validate JWT\")\n\t\t\t}\n\t\t}\n\n\t\t// Decode the payload\n\t\tpayload := &pushPayload{}\n\t\tif err := json.NewDecoder(req.Body).Decode(payload); err != nil {\n\t\t\treturn errs.WrapCode(err, errs.InvalidArgument, \"invalid push payload\")\n\t\t}\n\n\t\t// Call the subscription callback\n\t\treturn f(\n\t\t\treq.Context(),\n\t\t\tpayload.Message.MessageID, payload.Message.PublishTime, payload.DeliveryAttempt,\n\t\t\tpayload.Message.Attributes, payload.Message.Data,\n\t\t)\n\t}\n\n\tmgr.pushRegistry.RegisterPushSubscriptionHandler(\n\t\ttypes.SubscriptionID(subscriptionConfig.ID),\n\t\tfunc(w http.ResponseWriter, request *http.Request) {\n\t\t\terr := handler(request)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Err(err).Msg(\"error while handling PubSub subscription message\")\n\t\t\t}\n\t\t\terrs.HTTPError(w, err)\n\t\t},\n\t)\n}\n\nfunc (mgr *Manager) validateGoogleJWT(req *http.Request, serviceAccountEmail string) error {\n\t// Extract the JWT from the header\n\tauthType, token, _ := strings.Cut(req.Header.Get(\"Authorization\"), \" \")\n\tif authType != \"Bearer\" {\n\t\treturn errs.B().Code(errs.InvalidArgument).Msg(\"invalid auth header\").Err()\n\t}\n\n\t// Validate it\n\tjwt, err := idtoken.Validate(req.Context(), token, mgr.runtime.AppID+\"-\"+mgr.runtime.EnvID)\n\tif err != nil {\n\t\treturn errs.B().Code(errs.InvalidArgument).Msg(\"unable to validate authorization\").Err()\n\t}\n\tif jwt.Issuer != \"accounts.google.com\" && jwt.Issuer != \"https://accounts.google.com\" {\n\t\treturn errs.B().Code(errs.InvalidArgument).Msg(\"invalid issuer\").Err()\n\t}\n\tif jwt.Claims[\"email\"] != serviceAccountEmail || jwt.Claims[\"email_verified\"] != true {\n\t\treturn errs.B().Code(errs.Unauthenticated).Meta(\n\t\t\t\"expected_email\", serviceAccountEmail,\n\t\t\t\"got_email\", jwt.Claims[\"email\"],\n\t\t).Msg(\"invalid email\").Err()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/gcp/topic.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"cloud.google.com/go/pubsub\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\ntype Manager struct {\n\tctxs         *utils.Contexts\n\truntime      *config.Runtime\n\tpushRegistry types.PushEndpointRegistry\n\texperiments  *experiments.Set // The set of experiments enabled for this runtime\n\n\tclientsMu sync.Mutex                // clientsMu protects access to the clients map\n\tclients   map[string]*pubsub.Client // A map of project ID to pubsub client\n}\n\nfunc NewManager(ctxs *utils.Contexts, static *config.Static, runtime *config.Runtime, pushRegistry types.PushEndpointRegistry) *Manager {\n\texperiments := experiments.FromConfig(static, runtime)\n\treturn &Manager{ctxs: ctxs, runtime: runtime, pushRegistry: pushRegistry, experiments: experiments, clients: make(map[string]*pubsub.Client)}\n}\n\ntype topic struct {\n\tmgr      *Manager\n\tgcpTopic *pubsub.Topic\n\ttopicCfg *config.PubsubTopic\n}\n\nfunc (mgr *Manager) ProviderName() string { return \"gcp\" }\n\nfunc (mgr *Manager) Matches(cfg *config.PubsubProvider) bool {\n\treturn cfg.GCP != nil\n}\n\nfunc (mgr *Manager) NewTopic(_ *config.PubsubProvider, staticCfg types.TopicConfig, runtimeCfg *config.PubsubTopic) types.TopicImplementation {\n\t// Create the topic\n\tgcpTopic := mgr.getClientForProject(runtimeCfg.GCP.ProjectID).Topic(runtimeCfg.ProviderName)\n\n\t// Enable message ordering if we have an ordering key set\n\tgcpTopic.EnableMessageOrdering = staticCfg.OrderingAttribute != \"\"\n\n\t// Check we have permissions to interact with the given topic\n\t// (note: the call to Topic() above only creates the object, it doesn't verify that we have permissions to interact with it)\n\t_, err := gcpTopic.Config(mgr.ctxs.Connection)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"pubsub topic %s status call failed: %s\", runtimeCfg.EncoreName, err))\n\t}\n\n\treturn &topic{mgr, gcpTopic, runtimeCfg}\n}\n\nfunc (t *topic) PublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\tgcpMsg := &pubsub.Message{\n\t\tData:        data,\n\t\tAttributes:  attrs,\n\t\tOrderingKey: orderingKey,\n\t}\n\n\t// Attempt to publish the message\n\tid, err = t.gcpTopic.Publish(ctx, gcpMsg).Get(ctx)\n\n\tif t.gcpTopic.EnableMessageOrdering && err != nil {\n\t\tt.gcpTopic.ResumePublish(orderingKey)\n\t}\n\treturn id, err\n}\n\nfunc (t *topic) Subscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy, subCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\tif subCfg.PushOnly && subCfg.ID == \"\" {\n\t\tpanic(\"push-only subscriptions must have a subscription ID\")\n\t}\n\tgcpCfg := subCfg.GCP\n\tif gcpCfg == nil {\n\t\tpanic(\"GCP subscriptions must have GCP-specific configuration provided, got nil\")\n\t}\n\n\t// If we have a subscription ID, register a push endpoint for it\n\tif subCfg.ID != \"\" {\n\t\tif gcpCfg.PushServiceAccount != \"\" {\n\t\t\tt.mgr.registerPushEndpoint(logger, subCfg, f)\n\t\t} else if subCfg.PushOnly {\n\t\t\tpanic(\"push-only subscriptions require a push service account to be configured for the PubSub server config\")\n\t\t}\n\t}\n\n\t// If we're not push only, then also set up the subscription\n\tif !subCfg.PushOnly {\n\t\t// Create the subscription object (and then check it exists on GCP's side)\n\t\tsubscription := t.mgr.getClientForProject(gcpCfg.ProjectID).Subscription(subCfg.ProviderName)\n\n\t\t// Set the concurrency\n\t\tif maxConcurrency == 0 {\n\t\t\tmaxConcurrency = 1000 // FIXME(domblack): This retains the old behaviour, but allows user customisation - in a future release we should remove this\n\t\t}\n\t\tsubscription.ReceiveSettings.MaxOutstandingMessages = maxConcurrency\n\n\t\tif experiments.AdaptiveGCPPubSubGoroutines.Enabled(t.mgr.experiments) {\n\t\t\t// Compute the number of goroutines to use for this subscription.\n\t\t\tstreamingSubsInProject := 0\n\t\t\tfor _, topic := range t.mgr.runtime.PubsubTopics {\n\t\t\t\tfor _, sub := range topic.Subscriptions {\n\t\t\t\t\tif !sub.PushOnly && sub.GCP != nil && sub.GCP.ProjectID == gcpCfg.ProjectID {\n\t\t\t\t\t\tstreamingSubsInProject++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tsubscription.ReceiveSettings.NumGoroutines = numGoroutines(streamingSubsInProject)\n\t\t}\n\n\t\t// Start the subscription with the GCP library\n\t\tgo func() {\n\t\t\tfor t.mgr.ctxs.Fetch.Err() == nil {\n\t\t\t\t// Subscribe to the topic to receive messages\n\t\t\t\terr := subscription.Receive(t.mgr.ctxs.Fetch, func(_ context.Context, msg *pubsub.Message) {\n\t\t\t\t\tdeliveryAttempt := 1\n\t\t\t\t\tif msg.DeliveryAttempt != nil {\n\t\t\t\t\t\tdeliveryAttempt = *msg.DeliveryAttempt\n\t\t\t\t\t}\n\n\t\t\t\t\t// Create a context from the handler context with a deadline of the ackdeadline\n\t\t\t\t\tctx, cancel := context.WithTimeout(t.mgr.ctxs.Handler, ackDeadline)\n\t\t\t\t\tdefer cancel()\n\n\t\t\t\t\tvar result *pubsub.AckResult\n\t\t\t\t\tif err := f(ctx, msg.ID, msg.PublishTime, deliveryAttempt, msg.Attributes, msg.Data); err != nil {\n\t\t\t\t\t\tresult = msg.NackWithResult()\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult = msg.AckWithResult()\n\t\t\t\t\t}\n\n\t\t\t\t\tres, err := result.Get(t.mgr.ctxs.Connection)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Warn().Err(err).Str(\"msg_id\", msg.ID).Msg(\"failed to ack/nack message\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tswitch res {\n\t\t\t\t\t\tcase pubsub.AcknowledgeStatusSuccess:\n\t\t\t\t\t\tcase pubsub.AcknowledgeStatusPermissionDenied:\n\t\t\t\t\t\t\tlogger.Error().Str(\"msg_id\", msg.ID).Msg(\"failed to ack/nack message due to permissions\")\n\t\t\t\t\t\tcase pubsub.AcknowledgeStatusFailedPrecondition:\n\t\t\t\t\t\t\tlogger.Error().Str(\"msg_id\", msg.ID).Msg(\"failed to ack/nack message due to precondition\")\n\t\t\t\t\t\tcase pubsub.AcknowledgeStatusInvalidAckID:\n\t\t\t\t\t\t\tlogger.Error().Str(\"msg_id\", msg.ID).Msg(\"failed to ack/nack message due to invalid ack ID\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tlogger.Error().Str(\"msg_id\", msg.ID).Msg(\"failed to ack/nack message due to unknown error\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\t// If there was an error and we're not shutting down, log it and then sleep for a bit before trying again\n\t\t\t\tif err != nil && t.mgr.ctxs.Fetch.Err() == nil {\n\t\t\t\t\tlogger.Warn().Err(err).Msg(\"pubsub subscription failed, retrying in 5 seconds\")\n\t\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/noop/topic.go",
    "content": "package noop\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/pubsub/internal/types\"\n)\n\ntype Topic struct{}\n\nvar _ types.TopicImplementation = (*Topic)(nil)\n\nvar ErrNoop = errors.New(\n\t\"pubsub: this service is not configured to use this topic. \" +\n\t\t\"Use pubsub.TopicRef in the service to get a reference and access to the topic from this service\",\n)\n\nfunc (t *Topic) PublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\treturn \"\", ErrNoop\n}\n\nfunc (t *Topic) Subscribe(logger *zerolog.Logger, maxConcurrency int, _ time.Duration, _ *types.RetryPolicy, subCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\t// no-op\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/nsq/log_adapter.go",
    "content": "package nsq\n\nimport (\n\t\"strings\"\n\n\t\"github.com/rs/zerolog\"\n)\n\ntype LogAdapter struct{ Logger *zerolog.Logger }\n\nfunc (l *LogAdapter) Output(maxdepth int, s string) error {\n\t// Attempt to extract the level, start with cutting on \":\"\n\tlvl, logMsg, found := strings.Cut(s, \":\")\n\tif !found || strings.Contains(lvl, \" \") {\n\t\t// then if that fails or we have a space in that cut, try cutting on the first space\n\t\tnewLvl, suffix, _ := strings.Cut(lvl, \" \")\n\t\tlvl = newLvl\n\n\t\tif found {\n\t\t\tlogMsg = suffix + \":\" + logMsg\n\t\t}\n\t}\n\n\t// Attempt to convert the level string to a zerolog level\n\tlogLevel := l.OutputLevel(lvl)\n\tif logLevel == zerolog.NoLevel {\n\t\t// and if that fails, then just log the message\n\t\tlogMsg = s\n\t}\n\n\tlogMsg = strings.TrimSpace(logMsg)\n\tif logMsg != \"\" {\n\t\tl.Logger.WithLevel(logLevel).Msg(logMsg)\n\t}\n\n\treturn nil\n}\n\nfunc (l *LogAdapter) OutputLevel(lvl string) zerolog.Level {\n\tswitch strings.ToLower(lvl) {\n\tcase \"debug\", \"dbg\":\n\t\treturn zerolog.DebugLevel\n\tcase \"info\", \"inf\":\n\t\treturn zerolog.InfoLevel\n\tcase \"warn\", \"wrn\":\n\t\treturn zerolog.WarnLevel\n\tcase \"error\", \"err\":\n\t\treturn zerolog.ErrorLevel\n\tcase \"fatal\":\n\t\treturn zerolog.FatalLevel\n\tdefault:\n\t\treturn zerolog.NoLevel\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/nsq/topic.go",
    "content": "package nsq\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/nsqio/go-nsq\"\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\ntype Manager struct {\n\tctxs *utils.Contexts\n\trt   *reqtrack.RequestTracker\n}\n\nfunc NewManager(ctxs *utils.Contexts, rt *reqtrack.RequestTracker) *Manager {\n\treturn &Manager{ctxs, rt}\n}\n\n// topic is the nsq implementation of pubsub.Topic. It exposes methods to publish\n// and subscribe to messages of a topic\ntype topic struct {\n\tmgr       *Manager\n\tname      string\n\taddr      string\n\tm         sync.Mutex\n\tproducer  *nsq.Producer\n\tconsumers map[string]*nsq.Consumer\n}\n\nfunc (mgr *Manager) ProviderName() string { return \"nsq\" }\n\nfunc (mgr *Manager) Matches(cfg *config.PubsubProvider) bool {\n\treturn cfg.NSQ != nil\n}\n\nfunc (mgr *Manager) NewTopic(providerCfg *config.PubsubProvider, _ types.TopicConfig, runtimeCfg *config.PubsubTopic) types.TopicImplementation {\n\treturn &topic{\n\t\tmgr:       mgr,\n\t\tname:      runtimeCfg.EncoreName,\n\t\taddr:      providerCfg.NSQ.Host,\n\t\tproducer:  nil,\n\t\tconsumers: make(map[string]*nsq.Consumer),\n\t}\n}\n\n// messageWrapper is a local representation of a topic published to NSQ.\n// it wraps the raw data with an ID and an Attribute map.\n// It must be synchronized with the e2e-tests/testscript_test.go file.\ntype messageWrapper struct {\n\tID         string\n\tAttributes map[string]string\n\tData       json.RawMessage\n}\n\nfunc (l *topic) Subscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy, implCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\tif implCfg.PushOnly {\n\t\tpanic(\"push-only subscriptions are not supported by nsq\")\n\t}\n\n\tl.m.Lock()\n\tdefer l.m.Unlock()\n\n\tif _, ok := l.consumers[implCfg.EncoreName]; ok {\n\t\tpanic(\"NewSubscription must use a unique subscription name\")\n\t}\n\n\tif maxConcurrency == 0 {\n\t\tmaxConcurrency = 1 // FIXME(domblack): This retains the old behaviour, but allows user customisation - in a future release we should remove this\n\t}\n\n\tif maxConcurrency < 0 {\n\t\t// nsq does not support the concept of unlimited concurrency, so we set it to a high number here\n\t\t// (value of 0 will pause consumption)\n\t\tmaxConcurrency = 100\n\t}\n\n\tconCfg := getConsumerConfig(maxConcurrency, ackDeadline, retryPolicy)\n\tconsumer, err := nsq.NewConsumer(l.name, implCfg.EncoreName, conCfg)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"unable to setup subscription %s for topic %s: %v\", implCfg.EncoreName, l.name, err))\n\t}\n\t// only log warnings and above from the NSQ library\n\tconsumer.SetLogger(&LogAdapter{Logger: logger}, nsq.LogLevelWarning)\n\n\t// create a dedicated handler which forwards messages to the encore subscription\n\tconsumer.AddConcurrentHandlers(nsq.HandlerFunc(func(m *nsq.Message) error {\n\t\t// create a message to unmarshal the raw nsq body into\n\t\tmsg := &messageWrapper{}\n\n\t\tdefer func() {\n\t\t\tif !m.HasResponded() {\n\t\t\t\tretry, delay := utils.GetDelay(retryPolicy.MaxRetries, retryPolicy.MinBackoff, retryPolicy.MaxBackoff, m.Attempts)\n\t\t\t\tif !retry {\n\n\t\t\t\t\tlogger.Error().Str(\"msg_id\", msg.ID).Int(\"retry\", int(m.Attempts)-1).Msg(\"depleted message retries. Dropping message\")\n\t\t\t\t\t// TODO; offload this to the dead letter queue\n\t\t\t\t\tm.Finish()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tm.RequeueWithoutBackoff(delay)\n\t\t\t}\n\t\t}()\n\n\t\terr = json.Unmarshal(m.Body, msg)\n\t\tif err != nil {\n\t\t\treturn errs.B().Cause(err).Code(errs.InvalidArgument).Msg(\"failed to unmarshal message wrapper\").Err()\n\t\t}\n\n\t\t// forward the message to the subscriber\n\t\tmsgCtx, cancel := context.WithTimeout(l.mgr.ctxs.Handler, ackDeadline)\n\t\tdefer cancel()\n\n\t\terr = f(msgCtx, msg.ID, time.Unix(0, m.Timestamp), int(m.Attempts), msg.Attributes, msg.Data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tm.Finish()\n\t\treturn nil\n\t}), maxConcurrency)\n\n\t// add the consumer to the known consumers\n\tl.consumers[implCfg.EncoreName] = consumer\n\n\tgo func() {\n\t\t// Allow the rest of the service to initialize before we connect to NSQD.\n\t\t// This is necessary because NSQD is so fast the receiver can process messages\n\t\t// before all package-level initialization functions have been called.\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\terr = consumer.ConnectToNSQD(l.addr)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"failed to connect %s to nsqd for topic %s: %v\", implCfg.EncoreName, l.name, err))\n\t\t}\n\t}()\n\n\t// Stop the consumer when the the fetch context is done\n\tgo func() {\n\t\t<-l.mgr.ctxs.Fetch.Done()\n\t\tconsumer.Stop()\n\t}()\n}\n\n// PublishMessage publishes a message to an nsq Topic\nfunc (l *topic) PublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\t// instantiate a Producer if there isn;t one already\n\tif l.producer == nil {\n\t\tl.m.Lock()\n\t\tdefer l.m.Unlock()\n\t\tif l.producer == nil {\n\t\t\tcfg := nsq.NewConfig()\n\t\t\tproducer, err := nsq.NewProducer(l.addr, cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", errs.B().Cause(err).Code(errs.Internal).Msg(\"failed to connect to NSQD\").Err()\n\t\t\t}\n\t\t\t// only log warnings and above from the NSQ library\n\t\t\tlog := l.mgr.rt.Logger().With().Str(\"topic\", l.name).Logger()\n\t\t\tproducer.SetLogger(&LogAdapter{Logger: &log}, nsq.LogLevelWarning)\n\t\t\tl.producer = producer\n\t\t}\n\t}\n\n\t// generate a new message ID\n\tmsgID := xid.New().String()\n\n\t// create and publish the message wrapper\n\tdata, err = json.Marshal(&messageWrapper{ID: msgID, Data: data, Attributes: attrs})\n\tif err != nil {\n\t\treturn \"\", errs.B().Cause(err).Code(errs.Internal).Msg(\"failed to marshal message\").Err()\n\t}\n\terr = l.producer.Publish(l.name, data)\n\tif err != nil {\n\t\treturn \"\", errs.B().Cause(err).Code(errs.Internal).Msg(\"failed to connect to NSQD\").Err()\n\t}\n\treturn msgID, nil\n}\n\nfunc getConsumerConfig(maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy) *nsq.Config {\n\tconCfg := nsq.NewConfig()\n\tconCfg.MsgTimeout = utils.Clamp(ackDeadline, 0, 15*time.Minute)\n\tconCfg.MaxInFlight = maxConcurrency\n\tconCfg.DefaultRequeueDelay = utils.Clamp(retryPolicy.MinBackoff, 0, 60*time.Minute)\n\tconCfg.MaxRequeueDelay = utils.Clamp(retryPolicy.MaxBackoff, 0, 60*time.Minute)\n\n\tswitch retryPolicy.MaxRetries {\n\tcase 0:\n\t\tconCfg.MaxAttempts = 100\n\tcase types.InfiniteRetries:\n\t\tconCfg.MaxAttempts = 65535\n\tdefault:\n\t\tconst maxVal = 65535 // from the nsq library config\n\t\tif retryPolicy.MaxRetries > maxVal {\n\t\t\tconCfg.MaxAttempts = maxVal\n\t\t} else {\n\t\t\tconCfg.MaxAttempts = uint16(retryPolicy.MaxRetries)\n\t\t}\n\t}\n\n\treturn conCfg\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/test/topic.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\n// TestTopic is used during a \"encore test\" call.\n//\n// It records all published messages on a per-test basis, allowing a unit test\n// to assert that the correct messages were published.\n//\n// Any messages published to this type of topic _will not_ be passed to subscribers.\ntype TestTopic[T any] struct {\n\tts          *testsupport.Manager\n\tname        string\n\tm           sync.RWMutex\n\tinstances   map[*testing.T]*testInstance[T]\n\tsubscribers map[string]types.RawSubscriptionCallback\n}\n\nfunc NewTopic[T any](ts *testsupport.Manager, name string) types.TopicImplementation {\n\treturn &TestTopic[T]{\n\t\tts:          ts,\n\t\tname:        name,\n\t\tinstances:   make(map[*testing.T]*testInstance[T]),\n\t\tsubscribers: make(map[string]types.RawSubscriptionCallback),\n\t}\n}\n\n// PublishMessage will record the message against the test instance\n// and if subscribers are enabled for the test instance, it will also trigger\n// those subscribers. (The default behaviour is subscribers are disabled in tests)\nfunc (t *TestTopic[T]) PublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error) {\n\tif err := ctx.Err(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttest := t.ts.CurrentTest()\n\tunmarshalled, err := utils.UnmarshalMessage[T](attrs, data)\n\tif err != nil {\n\t\ttest.Fatalf(\"failed to unmarshal published message: %s\", err)\n\t}\n\n\tinstance := t.TestInstance(test)\n\n\tmsgID, err := instance.publishMessage(unmarshalled)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// If subscriptions are enabled for this test, then trigger those subscribers asynchronously\n\t// allowing the publishing code to continue as it would in a real system\n\tif instance.subscriptionsEnabled {\n\t\tpublished := time.Now()\n\n\t\tfor name, sub := range t.subscribers {\n\t\t\tname := name\n\t\t\tsub := sub\n\t\t\tt.ts.RunAsyncCodeInTest(test, func(ctx context.Context) {\n\t\t\t\tif err := sub(ctx, msgID, published, 1, attrs, data); err != nil {\n\t\t\t\t\ttest.Errorf(\"an error was returned while processing subscription %s for message %s: %s\", name, msgID, err)\n\t\t\t\t\ttest.Fail()\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n\treturn msgID, nil\n}\n\n// Subscribe will register a new subscriber for the pub sub topic. By default these will not be called during tests\nfunc (t *TestTopic[T]) Subscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *types.RetryPolicy, implCfg *config.PubsubSubscription, f types.RawSubscriptionCallback) {\n\tt.m.Lock()\n\tdefer t.m.Unlock()\n\tt.subscribers[implCfg.EncoreName] = f\n}\n\n// TestInstance returns this tests specific instance of the topic and creates it if it does not exist\nfunc (t *TestTopic[T]) TestInstance(test *testing.T) *testInstance[T] {\n\tt.m.RLock()\n\tinstance, found := t.instances[test]\n\tt.m.RUnlock()\n\tif found {\n\t\treturn instance\n\t}\n\n\tt.m.Lock()\n\tdefer t.m.Unlock()\n\tif _, found := t.instances[test]; !found {\n\t\tt.instances[test] = &testInstance[T]{\n\t\t\ttopicName: t.name,\n\t\t\tt:         test,\n\t\t}\n\t}\n\n\treturn t.instances[test]\n}\n\n// testInstance represents a topic, as it is seen from a test\n// This struct implements test.TestTopic[T] to allow the testing package to interface with it\ntype testInstance[T any] struct {\n\ttopicName            string     // The topic name\n\tt                    *testing.T // The test we're running against\n\tmsgID                int32      // The last message ID we sent (updated atomically)\n\tm                    sync.Mutex // Mutex for the published messages\n\tmessages             []T        // What messages have been published\n\tsubscriptionsEnabled bool       // If subscriptions are enabled for this test\n}\n\n// publishMessage records the message which was sent, and generates a deterministic message ID\n// which is guaranteed to be unique across all tests\nfunc (t *testInstance[T]) publishMessage(unmarshalled T) (id string, err error) {\n\tmsgID := atomic.AddInt32(&t.msgID, 1)\n\n\tt.m.Lock()\n\tdefer t.m.Unlock()\n\tt.messages = append(t.messages, unmarshalled)\n\n\t// we use \"/\" as the separator to mirror the behaviour of tests and sub tests\n\treturn fmt.Sprintf(\"%s/%s/%d\", t.t.Name(), t.topicName, msgID), nil\n}\n\nfunc (t *testInstance[T]) PublishedMessages() []T {\n\tt.m.Lock()\n\tdefer t.m.Unlock()\n\treturn t.messages\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/types/private.go",
    "content": "package types\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\n// RawSubscriptionCallback represents a unified callback structure allowing us to create a standardised callback for each implementation\ntype RawSubscriptionCallback func(ctx context.Context, msgID string, publishTime time.Time, deliveryAttempt int, attrs map[string]string, data []byte) error\n\n// TopicImplementation gives us a private API to implementing topics, which we can change without impacting the public API\ntype TopicImplementation interface {\n\tPublishMessage(ctx context.Context, orderingKey string, attrs map[string]string, data []byte) (id string, err error)\n\tSubscribe(logger *zerolog.Logger, maxConcurrency int, ackDeadline time.Duration, retryPolicy *RetryPolicy, implCfg *config.PubsubSubscription, f RawSubscriptionCallback)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/types/public.go",
    "content": "package types\n\nimport (\n\t\"time\"\n)\n\n// RetryPolicy defines how a subscription should handle retries\n// after errors either delivering the message or processing the message.\n//\n// The values given to this structure are parsed at compile time, such that\n// the correct Cloud resources can be provisioned to support the queue.\n//\n// As such the values given here may be clamped to the supported values by\n// the target cloud. (i.e. min/max values brought within the supported range\n// by the target cloud).\ntype RetryPolicy struct {\n\t// The minimum time to wait between retries. Defaults to 10 seconds.\n\tMinBackoff time.Duration\n\n\t// The maximum time to wait between retries. Defaults to 10 minutes.\n\tMaxBackoff time.Duration\n\n\t// MaxRetries is used to control deadletter queuing logic, when:\n\t//   n == 0: A default value of 100 retries will be used\n\t//   n > 0:  Encore will forward a message to a dead letter queue after n retries\n\t//   n == pubsub.InfiniteRetries: Messages will not be forwarded to the dead letter queue by the Encore framework\n\tMaxRetries int\n}\n\nconst (\n\t// NoRetries is used to control deadletter queuing logic, when set as the MaxRetires within the RetryPolicy\n\t// it will attempt to immediately forward a message to the dead letter queue if the subscription Handler\n\t// returns any error or panics.\n\t//\n\t// Note: With some cloud providers, having no retries may not be supported, in which case the minimum number of\n\t// retries permitted by the provider will be used.\n\tNoRetries = -2\n\n\t// InfiniteRetries is used to control deadletter queuing logic, when set as the MaxRetires within the RetryPolicy\n\t// it will attempt to always retry a message without ever sending it to the dead letter queue.\n\t//\n\t// Note: With some cloud providers, infinite retries may not be supported, in which case the maximum number of\n\t// retries permitted by the provider will be used.\n\tInfiniteRetries = -1\n)\n\n// DeliveryGuarantee is used to configure the delivery contract for a topic\ntype DeliveryGuarantee int\n\nconst (\n\t// AtLeastOnce guarantees that a message for a subscription is delivered to\n\t// a consumer at least once.\n\t//\n\t// On AWS and GCP there is no limit to the throughput for a topic.\n\tAtLeastOnce DeliveryGuarantee = iota + 1\n\n\t// ExactlyOnce guarantees that a message for a subscription is delivered to\n\t// a consumer exactly once, to the best of the system's ability.\n\t//\n\t// However, there are edge cases when a message might be redelivered.\n\t// For example, if a networking issue causes the acknowledgement of success\n\t// processing the message to be lost before the cloud provider receives it.\n\t//\n\t// It is also important to note that the ExactlyOnce delivery guarantee only\n\t// applies to the delivery of the message to the consumer, and not to the\n\t// original publishing of the message, such that if a message is published twice,\n\t// such as due to an retry within the application logic, it will be delivered twice.\n\t// (i.e. ExactlyOnce delivery does not imply message deduplication on publish)\n\t//\n\t// As such it's recommended that the subscription handler function is idempotent\n\t// and is able to handle duplicate messages.\n\t//\n\t// Subscriptions attached to ExactlyOnce topics have higher message delivery latency compared to AtLeastOnce.\n\t//\n\t// By using ExactlyOnce semantics on a topic, the throughput will be limited depending on the cloud provider:\n\t//\n\t// - AWS: 300 messages per second for the topic (see [AWS SQS Quotas]).\n\t// - GCP: At least 3,000 messages per second across all topics in the region\n\t// \t\t  (can be higher on the region see [GCP PubSub Quotas]).\n\t//\n\t// [AWS SQS Quotas]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html\n\t// [GCP PubSub Quotas]: https://cloud.google.com/pubsub/quotas#quotas\n\tExactlyOnce\n)\n\n// TopicConfig is used when creating a Topic\ntype TopicConfig struct {\n\t// DeliveryGuarantee is used to configure the delivery guarantee of a Topic\n\t//\n\t// This field is required.\n\tDeliveryGuarantee DeliveryGuarantee\n\n\t// OrderingAttribute is the message attribute to use as a ordering key for\n\t// messages and delivery will ensure that messages with the same value will\n\t// be delivered in the order they where published.\n\t//\n\t// If OrderingAttribute is not set, messages can be delivered in any order.\n\t//\n\t// It is important to note, that in the case of an error being returned by a\n\t// subscription handler, the message will be retried before any subsequent\n\t// messages for that ordering key are delivered. This means depending on the\n\t// retry configuration, a large backlog of messages for a given ordering key\n\t// may build up. When using OrderingAttribute, it is recommended to use reason\n\t// about your failure modes and set the retry configuration appropriately.\n\t//\n\t// Once the maximum number of retries has been reached, the message will be\n\t// forwarded to the dead letter queue, and the next message for that ordering\n\t// key will be delivered.\n\t//\n\t// To create attributes on a message, use the `pubsub-attr` struct tag:\n\t//\n\t//\ttype UserEvent struct {\n\t//\t\tUserID string `pubsub-attr:\"user-id\"`\n\t//\t\tAction string\n\t//\t}\n\t//\n\t//  var topic = pubsub.NewTopic[UserEvent](\"user-events\", pubsub.TopicConfig{\n\t// \t\tDeliveryGuarantee: pubsub.AtLeastOnce,\n\t//\t\tOrderingAttribute: \"user-id\", // Messages with the same user-id will be delivered in the order they where published\n\t//\t})\n\t//\n\t//  topic.Publish(ctx, &UserEvent{UserID: \"1\", Action: \"login\"})  // This message will be delivered before the logout\n\t//  topic.Publish(ctx, &UserEvent{UserID: \"2\", Action: \"login\"})  // This could be delivered at any time because it has a different user id\n\t//  topic.Publish(ctx, &UserEvent{UserID: \"1\", Action: \"logout\"}) // This message will be delivered after the first message\n\t//\n\t// By using OrderingAttribute, the throughput will be limited depending on the cloud provider:\n\t//\n\t// - AWS: 300 messages per second for the topic (see [AWS SQS Quotas]).\n\t// - GCP: 1MB/s for each ordering key (see [GCP PubSub Quotas]).\n\t//\n\t// Note: OrderingAttribute currently has no effect during local development.\n\t//\n\t// [AWS SQS Quotas]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html\n\t// [GCP PubSub Quotas]: https://cloud.google.com/pubsub/quotas#resource_limits\n\tOrderingAttribute string\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/types/push_registry.go",
    "content": "package types\n\nimport \"net/http\"\n\ntype SubscriptionID string\n\ntype PushEndpointHandler func(req *http.Request) error\n\ntype PushEndpointRegistry interface {\n\tRegisterPushSubscriptionHandler(id SubscriptionID, handler http.HandlerFunc)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/utils/contexts.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n)\n\n// Contexts is a struct containing all the contexts used by the pubsub package\ntype Contexts struct {\n\t// Fetch is the context used for fetching messages from the pubsub provider\n\t//\n\t// It is cancelled when the manager is shutdown and is used to indicate that\n\t// no more messages should be fetched from the provider.\n\t//\n\t// The fetch context is always the first context to be cancelled.\n\tFetch                 context.Context\n\tStopFetchingNewEvents context.CancelFunc\n\n\t// Handler is the context used for handling messages from the pubsub provider\n\t//\n\t// It is cancelled when the manager is told to stop any active handlers.\n\t//\n\t// If cancelled before the fetch context, it will also cancel the fetch context.\n\tHandler       context.Context\n\tCancelHandler context.CancelFunc\n\n\t// Connection is the context used for the connection to the pubsub provider\n\t//\n\t// If cancelled, it will cancel both the fetch and handler contexts.\n\tConnection       context.Context\n\tCloseConnections context.CancelFunc\n}\n\n// NewContexts creates a new set of contexts for the pubsub package\nfunc NewContexts(base context.Context) *Contexts {\n\tconnection, cancelConnection := context.WithCancel(base)\n\thandler, cancelHandler := context.WithCancel(connection)\n\tfetch, cancelFetch := context.WithCancel(handler)\n\n\tctxs := &Contexts{\n\t\tFetch:                 fetch,\n\t\tStopFetchingNewEvents: cancelFetch,\n\t\tHandler:               handler,\n\t\tCancelHandler:         cancelHandler,\n\t\tConnection:            connection,\n\t\tCloseConnections:      cancelConnection,\n\t}\n\n\treturn ctxs\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"cmp\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/pubsub/internal/types\"\n)\n\nconst AttrTag = \"pubsub-attr\"\n\n// UnmarshalMessage unmarshals a message into a struct. The message must be a JSON object.\nfunc UnmarshalMessage[T any](attrs map[string]string, data []byte) (msg T, err error) {\n\tif err = json.Unmarshal(data, &msg); err != nil {\n\t\terr = errs.B().Cause(err).Code(errs.InvalidArgument).Msg(\"failed to unmarshal message\").Err()\n\t\treturn\n\t}\n\n\tif err = UnmarshalFields(attrs, &msg, AttrTag); err != nil {\n\t\terr = errs.B().Cause(err).Code(errs.InvalidArgument).Msg(\"failed to unmarshal attributes\").Err()\n\t\treturn\n\t}\n\n\treturn\n}\n\n// MarshalFields creates a map[string]string of fields in `msg` tagged with `tag`. The name of the tag\n// will be used as map key, and values are converted to strings using fmt.Sprintf. Pointers will be dereferenced\n// and ignored if nil. Only basic types (bool, numeric, string) and pointers to those types are supported fields.\n// Fields without a tag will not be marshalled. `msg` must be a struct or pointer to a struct\nfunc MarshalFields[T any](msg T, tag string) (map[string]string, error) {\n\t// Create a map to return\n\trtn := map[string]string{}\n\tmsgVal := reflect.ValueOf(msg)\n\t// Dereference the input msg\n\tfor msgVal.Kind() == reflect.Ptr {\n\t\tmsgVal = msgVal.Elem()\n\t}\n\t// Only support structs, or pointers to structs\n\tif msgVal.Kind() != reflect.Struct {\n\t\treturn nil, errors.New(\"pubsub messages must be structs or a pointer to struct\")\n\t}\n\n\t// Iterate through the message fields and look for `tag` tags, marshal if found\n\tfor i := 0; i < msgVal.NumField(); i++ {\n\t\tmsgField := msgVal.Field(i)\n\t\tif name, ok := msgVal.Type().Field(i).Tag.Lookup(tag); ok {\n\t\t\tisNil := false\n\t\t\t// We need to dereference pointers to get the value\n\t\t\tfor msgField.Kind() == reflect.Pointer {\n\t\t\t\t// If it's a nil pointer we want to skip the field\n\t\t\t\tif msgField.IsNil() {\n\t\t\t\t\tisNil = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tmsgField = msgField.Elem()\n\t\t\t}\n\t\t\tif isNil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// if the dereferenced type is not a basic type, return an error\n\t\t\tif msgField.Kind() >= reflect.Array && msgField.Kind() != reflect.String {\n\t\t\t\treturn nil, errors.New(fmt.Sprintf(\"unsupported kind: %s\", msgField.Kind()))\n\t\t\t}\n\t\t\t// serialize the value using string formatting\n\t\t\trtn[name] = fmt.Sprintf(\"%v\", msgField.Interface())\n\t\t}\n\t}\n\treturn rtn, nil\n}\n\nvar decodeCache = sync.Map{}\n\n// UnmarshalFields copies values from the attrs map to val the struct. The attrs key to copy the value from is\n// retrieved from the name of the field tag with key `tag`. The string value is parsed to the target type before\n// being assigned. Invalid values will return an error. Only basic types (bool, numeric, string) and pointers\n// to those types are supported fields. Fields without a tag will not be touched. `val` must be a pointer to a struct,\n// otherwise we cannot populate the fields\nfunc UnmarshalFields[T any](attrs map[string]string, val T, tag string) error {\n\tv := reflect.ValueOf(val)\n\t// target type must be a pointer for us to set fields\n\tif v.Kind() != reflect.Pointer {\n\t\treturn errors.New(\"target must be a pointer to a struct\")\n\t}\n\t// Dereference the pointer\n\tfor v.Kind() == reflect.Ptr {\n\t\tv = v.Elem()\n\t}\n\t// We only support structs for now\n\tif v.Kind() != reflect.Struct {\n\t\treturn errors.New(fmt.Sprintf(\"unsupported kind, fields can only be unmarshaled into a struct. Got a %s\", v.Kind()))\n\t}\n\n\t// Loop through the fields of the struct and look for `tag`s\n\tfor i := 0; i < v.NumField(); i++ {\n\t\tif name, ok := v.Type().Field(i).Tag.Lookup(tag); ok {\n\t\t\t// If the attributes contain an element with the tag name, unmarshal it into target\n\t\t\tif attrVal, ok := attrs[name]; ok {\n\t\t\t\tdataField := v.Field(i)\n\t\t\t\tdataType := dataField.Type()\n\t\t\t\tptrDepth := 0\n\t\t\t\t// Dereference the type (we can't use the value here because it might be nil)\n\t\t\t\t// Keep track of the pointer depth which is used later to create a pointer to\n\t\t\t\t// the value\n\t\t\t\tfor ; dataType.Kind() == reflect.Ptr; ptrDepth++ {\n\t\t\t\t\tdataType = dataType.Elem()\n\t\t\t\t}\n\n\t\t\t\tassigner, _ := decodeCache.LoadOrStore(dataType.Kind(), func() fieldAssigner {\n\t\t\t\t\t// Parse and set the value. If the target type is a pointer, we need to\n\t\t\t\t\t// create a pointer. We use the type specific setter (e.g. SetInt) to handle\n\t\t\t\t\t// int bitsize conversions, and the generic setter (Set) to set pointers\n\t\t\t\t\tswitch dataType.Kind() {\n\t\t\t\t\tcase reflect.String:\n\t\t\t\t\t\treturn getFieldAssigner(\n\t\t\t\t\t\t\tfunc(v string) (string, error) {\n\t\t\t\t\t\t\t\treturn v, nil\n\t\t\t\t\t\t\t}, reflect.Value.SetString)\n\t\t\t\t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\t\t\t\t\treturn getFieldAssigner(\n\t\t\t\t\t\t\tfunc(v string) (int64, error) {\n\t\t\t\t\t\t\t\treturn strconv.ParseInt(v, 10, 64)\n\t\t\t\t\t\t\t}, reflect.Value.SetInt)\n\t\t\t\t\tcase reflect.Float32, reflect.Float64:\n\t\t\t\t\t\treturn getFieldAssigner(\n\t\t\t\t\t\t\tfunc(v string) (float64, error) {\n\t\t\t\t\t\t\t\treturn strconv.ParseFloat(v, 64)\n\t\t\t\t\t\t\t}, reflect.Value.SetFloat)\n\t\t\t\t\tcase reflect.Bool:\n\t\t\t\t\t\treturn getFieldAssigner(\n\t\t\t\t\t\t\tfunc(v string) (bool, error) {\n\t\t\t\t\t\t\t\treturn strconv.ParseBool(v)\n\t\t\t\t\t\t\t}, reflect.Value.SetBool)\n\t\t\t\t\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:\n\t\t\t\t\t\treturn getFieldAssigner(\n\t\t\t\t\t\t\tfunc(v string) (uint64, error) {\n\t\t\t\t\t\t\t\treturn strconv.ParseUint(v, 10, 64)\n\t\t\t\t\t\t\t}, reflect.Value.SetUint)\n\t\t\t\t\tcase reflect.Complex64, reflect.Complex128:\n\t\t\t\t\t\treturn getFieldAssigner(\n\t\t\t\t\t\t\tfunc(v string) (complex128, error) {\n\t\t\t\t\t\t\t\treturn strconv.ParseComplex(v, 128)\n\t\t\t\t\t\t\t}, reflect.Value.SetComplex)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn func(val string, ptrDepth int, dataField reflect.Value) error {\n\t\t\t\t\t\t\treturn errors.New(fmt.Sprintf(\"unsupported kind: %s\", dataField.Kind()))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}())\n\t\t\t\tassignFunc := assigner.(fieldAssigner)\n\t\t\t\terr := assignFunc(attrVal, ptrDepth, dataField)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype fieldAssigner = func(val string, ptrDepth int, dataField reflect.Value) error\ntype valueSetter[T any] func(reflect.Value, T)\ntype fieldParser[T any] func(string) (T, error)\n\nfunc getFieldAssigner[T any](parser fieldParser[T], setter valueSetter[T]) fieldAssigner {\n\treturn func(val string, ptrDepth int, dataField reflect.Value) error {\n\t\tv, err := parser(val)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsetValue(v, ptrDepth, dataField, setter)\n\t\treturn nil\n\t}\n}\n\n// setValue assigns a value or a (nested) pointer to a value to f\nfunc setValue[T any](val T, ptrDepth int, f reflect.Value, valSetter func(reflect.Value, T)) {\n\t// if the type is not a pointer, we can set the value directly on the field\n\tif ptrDepth == 0 {\n\t\tvalSetter(f, val)\n\t} else {\n\t\t// otherwise we need to create a stack of `ptrDepth` pointers and assign val\n\t\t// to a new value instance\n\t\troot := f.Type()\n\t\t// Find the dereferenced type\n\t\tfor root.Kind() == reflect.Pointer {\n\t\t\troot = root.Elem()\n\t\t}\n\t\t// Create a new value of the dereferenced type (and dereference it)\n\t\trval := reflect.New(root).Elem()\n\t\t// Set the content of the new Value\n\t\tvalSetter(rval, val)\n\t\t// Wrap it in pointers and set the target field to the wrapped value\n\t\tf.Set(pointify(rval, ptrDepth))\n\t}\n}\n\n// pointify wraps a value in `ptrDepth` pointers\nfunc pointify(rval reflect.Value, ptrDepth int) reflect.Value {\n\tfor i := 0; i < ptrDepth; i++ {\n\t\tv := reflect.New(rval.Type())\n\t\tv.Elem().Set(rval)\n\t\trval = v\n\t}\n\treturn rval\n}\n\n// GetDelay returns whether a message should be retried and if so the backoff duration based on the\n// configuration in the RetryPolicy\nfunc GetDelay(maxRetries int, minDelay, maxDelay time.Duration, attempt uint16) (shouldRetry bool, backoff time.Duration) {\n\tif maxRetries == types.NoRetries || (int(attempt) > maxRetries && maxRetries != types.InfiniteRetries) {\n\t\t// return the max delay here, so if we can't dead letter a message\n\t\t// the could will at least apply the largest allowed backoff\n\t\treturn false, maxDelay\n\t}\n\tif maxDelay < minDelay {\n\t\treturn true, maxDelay\n\t}\n\tbackoff = time.Duration( /**/ math.Max(float64(1*time.Second), float64(minDelay))) // backoff at least 1 second\n\n\tfor i := uint16(0); i < attempt; i++ {\n\t\tbackoff *= 2\n\t\tif backoff > maxDelay {\n\t\t\treturn true, maxDelay\n\t\t}\n\t}\n\treturn true, backoff\n}\n\n// WithDefaultValue returns setValue if it is a non zero value, otherwise it returns defaultValue\nfunc WithDefaultValue[T comparable](setValue, defaultValue T) T {\n\tvar zeroValue T\n\n\tif setValue == zeroValue {\n\t\treturn defaultValue\n\t}\n\n\treturn setValue\n}\n\n// Clamp returns the value clamped to the range [min, max].\nfunc Clamp[T cmp.Ordered](d T, min T, max T) T {\n\tif d < min {\n\t\treturn min\n\t}\n\tif d > max {\n\t\treturn max\n\t}\n\treturn d\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/utils/utils_test.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype EmbedStruct struct {\n\tVal1 string\n\tVal2 string\n}\n\ntype TestStruct struct {\n\tStringAttr    string    `pubsub-attr:\"string\"`\n\tStringPtrAttr ***string `pubsub-attr:\"stringptr\"`\n\tComplexAttr   complex64 `pubsub-attr:\"complex\"`\n\tUintAttr      uint8     `pubsub-attr:\"uint\"`\n\tUintPtrAttr   *uint8    `pubsub-attr:\"uintptr\"`\n\tStruct        EmbedStruct\n\tString        string\n}\n\nfunc TestSetAttributes(t *testing.T) {\n\ttestStruct := TestStruct{}\n\terr := UnmarshalFields(\n\t\tmap[string]string{\n\t\t\t\"string\":    \"stringval\",\n\t\t\t\"stringptr\": \"stringptrval\",\n\t\t\t\"uint\":      \"8\",\n\t\t\t\"complex\":   \"(12+8i)\",\n\t\t\t\"uintptr\":   \"88\",\n\t\t}, &testStruct, \"pubsub-attr\")\n\tAssert(t, err, IsNil)\n\tAssert(t, testStruct.StringAttr, Equals, \"stringval\")\n\tAssert(t, testStruct.StringPtrAttr, DeepEquals, createTriplePointer(\"stringptrval\"))\n\tAssert(t, testStruct.UintAttr, Equals, uint8(8))\n\tAssert(t, testStruct.ComplexAttr, Equals, complex64(12+8i))\n\tAssert(t, testStruct.UintPtrAttr, DeepEquals, createPointer(uint8(88)))\n}\n\ntype CheckType int\n\nconst (\n\tIsNil CheckType = iota\n\tEquals\n\tDeepEquals\n\tLessThanEqual\n\tIsTrue\n)\n\nfunc Assert(t *testing.T, val any, check CheckType, want ...any) {\n\tswitch check {\n\tcase IsNil:\n\t\tif val != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v is not nil\\n%s\", val, stack)\n\t\t}\n\tcase Equals:\n\t\tif fmt.Sprintf(\"%v\", val) != fmt.Sprintf(\"%v\", want[0]) {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v != %v\\n%s\", val, want[0], stack)\n\t\t}\n\tcase DeepEquals:\n\t\tderefVal := deref(val)\n\t\tderefWant := deref(want[0])\n\t\tif derefVal != derefWant {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v != %v\\n%s\", derefVal, derefWant, stack)\n\t\t}\n\tcase LessThanEqual:\n\t\tfVal, err1 := strconv.ParseFloat(fmt.Sprintf(\"%d\", val), 64)\n\t\tif err1 != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v is not numeric\\n%s\", val, stack)\n\t\t}\n\t\tfWant, err2 := strconv.ParseFloat(fmt.Sprintf(\"%d\", want[0]), 64)\n\t\tif err2 != nil {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v is not numeric\\n%s\", want[0], stack)\n\t\t}\n\t\tif fVal > fWant {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v > %v\\n%s\", val, want[0], stack)\n\t\t}\n\tcase IsTrue:\n\t\tbVal, ok := val.(bool)\n\t\tif !ok {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v is not a boolv\\n%s\", val, stack)\n\t\t}\n\t\tif !bVal {\n\t\t\tstack := debug.Stack()\n\t\t\tt.Fatalf(\"%v is not True\\n%s\", val, stack)\n\t\t}\n\t}\n}\n\nfunc deref(val interface{}) interface{} {\n\tv := reflect.ValueOf(val)\n\tfor v.Kind() == reflect.Pointer {\n\t\tv = v.Elem()\n\t}\n\treturn v.Interface()\n}\n\nfunc createPointer[T any](val T) *T {\n\treturn &val\n}\n\nfunc createTriplePointer[T any](val T) ***T {\n\tptr := &val\n\tptr2 := &ptr\n\treturn &ptr2\n}\n\nfunc TestGetAttributes(t *testing.T) {\n\ttestStruct := &TestStruct{\n\t\tStringAttr:    \"stringattrval\",\n\t\tStringPtrAttr: createTriplePointer(\"stringptrval\"),\n\t\tUintAttr:      8,\n\t\tUintPtrAttr:   createPointer(uint8(88)),\n\t\tComplexAttr:   12 + 8i,\n\t}\n\n\tattrs, err := MarshalFields(testStruct, \"pubsub-attr\")\n\tAssert(t, err, IsNil)\n\tAssert(t, attrs[\"string\"], Equals, \"stringattrval\")\n\tAssert(t, attrs[\"stringptr\"], Equals, \"stringptrval\")\n\tAssert(t, attrs[\"uint\"], Equals, \"8\")\n\tAssert(t, attrs[\"complex\"], Equals, \"(12+8i)\")\n\tAssert(t, attrs[\"uintptr\"], Equals, \"88\")\n}\n\nconst maxAttempt = 100\n\nfunc TestGetDelay(t *testing.T) {\n\ttests := map[string]*struct {\n\t\tMinRetryDelay time.Duration\n\t\tMaxRetryDelay time.Duration\n\t\tMaxRetries    int\n\t}{\n\t\t\"limited retries\": {\n\t\t\tMinRetryDelay: 2 * time.Second,\n\t\t\tMaxRetryDelay: 10 * time.Second,\n\t\t\tMaxRetries:    15,\n\t\t},\n\t\t\"unlimited retries\": {\n\t\t\tMinRetryDelay: 2 * time.Second,\n\t\t\tMaxRetryDelay: 10 * time.Second,\n\t\t\tMaxRetries:    -1,\n\t\t},\n\t\t\"0 retries\": {\n\t\t\tMinRetryDelay: 2 * time.Second,\n\t\t\tMaxRetryDelay: 10 * time.Second,\n\t\t\tMaxRetries:    0,\n\t\t},\n\t\t\"min > max\": {\n\t\t\tMinRetryDelay: 10 * time.Second,\n\t\t\tMaxRetryDelay: 2 * time.Second,\n\t\t\tMaxRetries:    10,\n\t\t},\n\t\t\"min == max\": {\n\t\t\tMinRetryDelay: 10 * time.Second,\n\t\t\tMaxRetryDelay: 10 * time.Second,\n\t\t\tMaxRetries:    10,\n\t\t},\n\t\t\"negative delay\": {\n\t\t\tMinRetryDelay: -10 * time.Second,\n\t\t\tMaxRetryDelay: 10 * time.Second,\n\t\t\tMaxRetries:    10,\n\t\t},\n\t}\n\n\tfor name, policy := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tretry := true\n\t\t\tattempt := uint16(0)\n\t\t\tprevDelay := 0 * time.Second\n\t\t\tfor ; retry && attempt < maxAttempt; attempt++ {\n\t\t\t\tvar delay time.Duration\n\t\t\t\tretry, delay = GetDelay(policy.MaxRetries, policy.MinRetryDelay, policy.MaxRetryDelay, attempt)\n\t\t\t\tif !retry {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tAssert(t, delay > prevDelay || delay == policy.MaxRetryDelay, IsTrue)\n\t\t\t\tif policy.MinRetryDelay > policy.MaxRetryDelay {\n\t\t\t\t\tAssert(t, delay, Equals, policy.MaxRetryDelay)\n\t\t\t\t} else if policy.MinRetryDelay < 1 && attempt == 0 {\n\t\t\t\t\tAssert(t, delay, Equals, 1*time.Second)\n\t\t\t\t} else if attempt == 0 {\n\t\t\t\t\tAssert(t, delay, Equals, policy.MinRetryDelay)\n\t\t\t\t}\n\t\t\t\tAssert(t, delay, LessThanEqual, policy.MaxRetryDelay)\n\t\t\t\tprevDelay = delay\n\t\t\t}\n\t\t\tif policy.MaxRetries == -1 {\n\t\t\t\tAssert(t, attempt, Equals, maxAttempt)\n\t\t\t} else {\n\t\t\t\tAssert(t, attempt, Equals, policy.MaxRetries+2) // +2 as delivery attempts are not 0 index, they start at 1\n\t\t\t}\n\t\t})\n\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/utils/workers.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"encore.dev/beta/errs\"\n)\n\nconst (\n\t// Between work processors finishing a work item, how long we debounce before fetching more work\n\t// (this is to avoid fetching work items in batches of 1)\n\tworkFetchDebounce = 25 * time.Millisecond\n\n\t// What is the maximum amount of time we wait before fetching work items when debouncing\n\tmaxFetchDebounce = 250 * time.Millisecond\n\n\tnoWorkDebounce = 500 * time.Millisecond\n)\n\n// WorkFetcher is a function that fetches work from a queue, it should fetch at most maxToFetch items\n// and block until it has at least one item to return.\ntype WorkFetcher[Work any] func(ctx context.Context, maxToFetch int) ([]Work, error)\n\n// WorkProcessor is a function that processes a single work item, it should block until the work item is processed\ntype WorkProcessor[Work any] func(ctx context.Context, work Work) error\n\n// WorkConcurrently fetches work using the given fetch function and then passes it to the worker function\n//\n// It will fetch at most maxBatchSize items at a time and guarantees that at most maxConcurrency items have been fetched\n// and are being processed at any given time.\n//\n// If maxBatchSize >= 1, will fetch at most maxBatchSize items at a time\n// If maxBatchSize <= 0, will fetch as most maxConcurrency items at a time\n//\n// If maxConcurrency <= 0 then there is no limit on the number of items being processed at a time\n//\n// This function will block until an error is returned from either the fetcher or the worker functions or until\n// the context is cancelled.\n//\n// In the event of an error occurring, calls to worker will be allowed to continue in the background until the\n// context is cancelled however, this function will still return immediately with the error. Thus if you immediately call\n// this again you could end up with 2x maxConcurrency workers running at the same time. (1x from the original run who\n// are still processing work and 1x from the new run).\nfunc WorkConcurrently[Work any](ctxs *Contexts, maxConcurrency int, maxBatchSize int, fetch WorkFetcher[Work], worker WorkProcessor[Work]) error {\n\tfetchWithPanicHandling := func(ctx context.Context, maxToFetch int) (work []Work, err error) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terr = errs.B().Msgf(\"panic: %v\", r).Err()\n\t\t\t}\n\t\t}()\n\t\treturn fetch(ctx, maxToFetch)\n\t}\n\n\tworkWithPanicHandling := func(ctx context.Context, work Work) (err error) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terr = errs.B().Msgf(\"panic: %v\", r).Err()\n\t\t\t}\n\t\t}()\n\t\treturn worker(ctx, work)\n\t}\n\n\tif maxConcurrency == 1 {\n\t\t// If there's no concurrency, we can just do everything synchronously within this goroutine\n\t\t// This avoids the overhead of creating mutexes being used\n\t\treturn workInSingleRoutine(ctxs, fetchWithPanicHandling, workWithPanicHandling)\n\n\t} else if maxConcurrency <= 0 {\n\t\t// If there's infinite concurrency, we can just do everything by spawning goroutines\n\t\t// for each work item\n\t\treturn workInInfiniteRoutines(ctxs, maxBatchSize, fetchWithPanicHandling, workWithPanicHandling)\n\n\t} else {\n\t\t// Else there's a cap on concurrency, we need to use channels to communicate between the fetcher and the workers\n\t\treturn workInWorkPool(ctxs, maxConcurrency, maxBatchSize, fetchWithPanicHandling, workWithPanicHandling)\n\t}\n}\n\nfunc workInSingleRoutine[Work any](ctxs *Contexts, fetch func(ctx context.Context, maxToFetch int) (work []Work, err error), worker func(ctx context.Context, work Work) (err error)) error {\n\tfor {\n\t\t// check if the context has been cancelled before fetching work\n\t\tif err := ctxs.Fetch.Err(); err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// fetch 1 item\n\t\twork, err := fetch(ctxs.Fetch, 1)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// loop over the items (we might get zero, and a buggy implementation might return more than 1, so a loop is safer)\n\t\tfor _, w := range work {\n\t\t\t// check if the context has been cancelled before processing the work\n\t\t\tif err := ctxs.Handler.Err(); err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// process the work\n\t\t\tif err := worker(ctxs.Handler, w); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc workInInfiniteRoutines[Work any](ctxs *Contexts, maxBatchSize int, fetch func(ctx context.Context, maxToFetch int) (work []Work, err error), worker func(ctx context.Context, work Work) (err error)) error {\n\tfetchCtx, cancel := context.WithCancelCause(ctxs.Fetch)\n\tdefer cancel(nil)\n\n\tif maxBatchSize <= 0 {\n\t\tmaxBatchSize = 100\n\t}\n\n\tfor fetchCtx.Err() == nil {\n\t\twork, err := fetch(fetchCtx, maxBatchSize)\n\t\tif err != nil {\n\t\t\tcancel(err)\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, w := range work {\n\t\t\tw := w\n\t\t\tgo func() {\n\t\t\t\t// the worker uses the parent context, such that if we have a fetch error, the existing workers will\n\t\t\t\t// continue to run until they finish processing their work\n\t\t\t\tif err := worker(ctxs.Handler, w); err != nil {\n\t\t\t\t\tcancel(err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\n\t// Return the reason for cancellation if it wasn't due to the parent context being cancelled\n\tcancelCause := context.Cause(fetchCtx)\n\tif errors.Is(cancelCause, context.Canceled) {\n\t\treturn nil\n\t}\n\treturn cancelCause\n}\n\nfunc workInWorkPool[Work any](ctxs *Contexts, maxConcurrency int, maxBatchSize int, doFetch func(ctx context.Context, maxToFetch int) (work []Work, err error), doProcessWork func(ctx context.Context, work Work) (err error)) error {\n\tfetchCtx, cancelFetch := context.WithCancelCause(ctxs.Fetch)\n\tdefer cancelFetch(nil)\n\n\t// workChan is a channel that is used to pass work from the fetcher to the workers\n\tworkChan := make(chan Work)\n\tdefer close(workChan) // close the channel when we're done so the workers know to stop\n\n\tnumWorkers := maxConcurrency\n\n\t// workDone is a channel that is used to signal that a worker has finished processing an item\n\tworkDone := make(chan struct{})\n\n\tvar workItemsBeingProcessed atomic.Int64\n\n\t// processWorkItem is a small wrapper around the worker function that tracks the number of items being processed\n\t// and cancels the context if an error is returned\n\tprocessWorkItem := func(work Work) {\n\t\tworkItemsBeingProcessed.Add(1)\n\n\t\tdefer func() {\n\t\t\t// Decrement the number of items being processed.\n\t\t\t// Note: it's important this happens BEFORE we\n\t\t\t// try to unblock the fetcher, otherwise we can end up\n\t\t\t// with a race where we unblock the fetcher but it doesn't\n\t\t\t// find any work to do and deadlocks.\n\t\t\tworkItemsBeingProcessed.Add(-1)\n\n\t\t\t// Attempt to unblock the fetcher if they're\n\t\t\t// waiting for a work item to complete.\n\t\t\tselect {\n\t\t\tcase workDone <- struct{}{}:\n\t\t\tdefault:\n\t\t\t}\n\t\t}()\n\n\t\t// We use the parent context here, such that if we have a fetch error, the existing workers will\n\t\t// continue to run until they finish processing any work already have started on\n\t\tif err := doProcessWork(ctxs.Handler, work); err != nil {\n\t\t\tcancelFetch(err)\n\t\t}\n\t}\n\tworker := func() {\n\t\tfor work := range workChan {\n\t\t\tprocessWorkItem(work)\n\t\t}\n\t}\n\n\t// Start the workers\n\tfor i := 0; i < numWorkers; i++ {\n\t\tgo worker()\n\t}\n\n\t// spuriousWakeup is a ticker used to periodically wake the fetcher\n\t// even in the absence of any work being completed. This is here\n\t// because there's a (theoretical) race condition in the fetcher logic\n\t// in cases where all the workers are busy:\n\t//\n\t//    1. The fetcher detects all workers are busy\n\t//    2. Before getting to the select {} statement, all workers complete\n\t//       their work, and fail to send on the workDone channel because\n\t//       the fetcher isn't yet waiting on it.\n\t//    3. The fetcher gets stuck waiting indefinitely for a work item to complete,\n\t//       but there is no work being done.\n\t//\n\t// Guard against this by having a spurious periodic wakeup. In normal circumstances\n\t// this won't ever be used.\n\tspuriousWakeup := time.NewTicker(1 * time.Second)\n\tdefer spuriousWakeup.Stop()\n\n\t// Start fetching work\nFetchLoop:\n\tfor fetchCtx.Err() == nil {\n\t\t// Determine how many items to fetch\n\t\ttoFetch := maxConcurrency - int(workItemsBeingProcessed.Load())\n\t\tif maxBatchSize > 0 && toFetch > maxBatchSize {\n\t\t\ttoFetch = maxBatchSize\n\t\t}\n\n\t\tif toFetch == 0 {\n\t\t\t// All our workers are busy, so we can't fetch any more work right now.\n\t\t\t// Wait until there's more work.\n\t\t\tselect {\n\t\t\tcase <-fetchCtx.Done():\n\t\t\t\t// We're done; stop fetching.\n\t\t\t\tbreak FetchLoop\n\t\t\tcase <-workDone:\n\t\t\t\t// A work item has been completed. Wait for a little bit\n\t\t\t\t// before we retry the loop to \"debounce\" and allow for a few\n\t\t\t\t// more work items to complete so we can fetch bigger batches.\n\t\t\t\ttime.Sleep(workFetchDebounce)\n\t\t\t\tcontinue FetchLoop\n\t\t\tcase <-spuriousWakeup.C:\n\t\t\t\t// See comment on spuriousWakeup above for motivation.\n\t\t\t\tcontinue FetchLoop\n\t\t\t}\n\t\t}\n\n\t\t// We have some work to fetch.\n\t\twork, err := doFetch(fetchCtx, toFetch)\n\t\tif err != nil {\n\t\t\tcancelFetch(err)\n\t\t\tbreak FetchLoop\n\t\t}\n\n\t\t// If we didn't get any items, sleep before we try again\n\t\t// to avoid hammering the server.\n\t\tif len(work) == 0 {\n\t\t\ttime.Sleep(noWorkDebounce)\n\t\t\tcontinue FetchLoop\n\t\t}\n\n\t\t// Pass the work to workers\n\t\tfor _, w := range work {\n\t\t\tselect {\n\t\t\tcase workChan <- w:\n\t\t\t\t// success, we passed the work to a worker\n\t\t\tcase <-fetchCtx.Done():\n\t\t\t\tbreak FetchLoop\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the reason for cancellation, unless it was due to the parent context being cancelled.\n\tcause := context.Cause(fetchCtx)\n\tif errors.Is(cause, context.Canceled) {\n\t\tcause = nil\n\t}\n\treturn cause\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/internal/utils/workers_test.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"slices\"\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc TestWorkConcurrently(t *testing.T) {\n\tt.Parallel()\n\n\tparams := []struct {\n\t\tconcurrency  int\n\t\tmaxBatchSize int\n\t\tfetchErr     error\n\t\tprocessErr   error\n\t}{\n\t\t// Simple concurrency tests\n\t\t{1, 10, nil, nil},\n\t\t{10, 10, nil, nil},\n\t\t{50, 50, nil, nil},\n\n\t\t// Test batch sizes\n\t\t{50, 0, nil, nil}, // Unlimited batch size\n\t\t{50, 1, nil, nil},\n\t\t{50, 10, nil, nil},\n\n\t\t// Unlimited concurrency\n\t\t{-1, 0, nil, nil},  // No batch size and unlimited concurrency\n\t\t{-1, 10, nil, nil}, // Unlimited concurrency, but a batch size\n\n\t\t// Test errors\n\t\t{50, 10, fmt.Errorf(\"fetch error\"), nil},\n\t\t{50, 10, nil, fmt.Errorf(\"process error\")},\n\t}\n\n\tfor _, tt := range params {\n\t\ttt := tt\n\t\tt.Run(fmt.Sprintf(\"c%d_b%d\", tt.concurrency, tt.maxBatchSize), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := qt.New(t)\n\n\t\t\t// Create a context which will timeout the test\n\t\t\ttimeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 15*time.Second)\n\t\t\tdefer timeoutCancel()\n\n\t\t\t// Then create a context which will cancel the work generator off that\n\t\t\t// (which we use to break out of the work generator loop)\n\t\t\tctx, cancel := context.WithCancel(timeoutCtx)\n\t\t\tdefer cancel()\n\n\t\t\t// The number of items we've generated\n\t\t\ttoGenerate := tt.concurrency * 3 // We want to generate enough work to fill the workers plus test other outcomes\n\t\t\tif toGenerate <= 0 {\n\t\t\t\ttoGenerate = 2_000 // If we have unlimited concurrency, we need to generate a lot of work\n\t\t\t}\n\t\t\ttoReturnFromFetcher := make([]int, toGenerate)\n\t\t\tfor i := 0; i < toGenerate; i++ {\n\t\t\t\ttoReturnFromFetcher[i] = i\n\t\t\t}\n\t\t\ttype fetchReq struct {\n\t\t\t\ttoFetch     int\n\t\t\t\tnumReturned int\n\t\t\t}\n\t\t\tvar fetchRequests []*fetchReq // We want to test that the fetcher is called the correct number of times\n\n\t\t\t// We want to test that the concurrency is respected and the max concurrency is reached\n\t\t\t// so this section setups a counter and max counter to track the number of active workers\n\t\t\tvar counterMu sync.Mutex\n\t\t\tvar activeWorkers int\n\t\t\tvar maxActiveWorkers int\n\t\t\tincActiveWorkers := func() {\n\t\t\t\tcounterMu.Lock()\n\t\t\t\tdefer counterMu.Unlock()\n\t\t\t\tactiveWorkers++\n\t\t\t\tif activeWorkers > maxActiveWorkers {\n\t\t\t\t\tmaxActiveWorkers = activeWorkers\n\t\t\t\t}\n\t\t\t}\n\t\t\tdecActiveWorkers := func() {\n\t\t\t\tcounterMu.Lock()\n\t\t\t\tdefer counterMu.Unlock()\n\t\t\t\tactiveWorkers--\n\t\t\t}\n\n\t\t\t// We want to test that all the work was received by the processor\n\t\t\t// so we'll track the work received in a slice protected by a mutex\n\t\t\tvar workMu sync.Mutex\n\t\t\tvar receivedWork []int\n\n\t\t\t// Create the fetcher function\n\t\t\tfetcher := func(ctx context.Context, toFetch int) ([]int, error) {\n\t\t\t\t// No work to fetch return nothing\n\t\t\t\tif len(toReturnFromFetcher) == 0 {\n\t\t\t\t\treturn nil, nil\n\t\t\t\t}\n\n\t\t\t\t// record this fetch request\n\t\t\t\treq := &fetchReq{\n\t\t\t\t\ttoFetch: toFetch,\n\t\t\t\t}\n\t\t\t\tfetchRequests = append(fetchRequests, req)\n\n\t\t\t\t// One of the fetches should return no data\n\t\t\t\tswitch len(fetchRequests) {\n\t\t\t\tcase 2:\n\t\t\t\t\t// simulate only one piece of data on fetch 1\n\t\t\t\t\ttoFetch = 1\n\t\t\t\tcase 3:\n\t\t\t\t\t// simulate no data on fetch 2\n\t\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\t\treturn nil, nil\n\t\t\t\tcase 4:\n\t\t\t\t\t// simulate only half the data is available on fetch 3\n\t\t\t\t\ttoFetch = toFetch / 2\n\t\t\t\tcase 5:\n\t\t\t\t\t// If we have a fetch error, return it on fetch 4\n\t\t\t\t\tif tt.fetchErr != nil {\n\t\t\t\t\t\treturn nil, tt.fetchErr\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif toFetch > len(toReturnFromFetcher) {\n\t\t\t\t\ttoFetch = len(toReturnFromFetcher)\n\t\t\t\t}\n\t\t\t\trtn := make([]int, toFetch)\n\t\t\t\tcopy(rtn, toReturnFromFetcher[0:toFetch])\n\t\t\t\ttoReturnFromFetcher = toReturnFromFetcher[toFetch:]\n\n\t\t\t\t// If we've generated enough work to fill the workers, cancel the context in a little bit\n\t\t\t\t// giving time for the workers to process the work\n\t\t\t\tif len(toReturnFromFetcher) == 0 {\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\t// Wait a little bit to give the workers time to process the work\n\t\t\t\t\t\t// before cancelling the context as on a slow machine, the workers\n\t\t\t\t\t\t// might not have started processing the work yet\n\t\t\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\t\t\tcancel()\n\t\t\t\t\t}()\n\t\t\t\t}\n\n\t\t\t\treq.numReturned = len(rtn)\n\t\t\t\treturn rtn, nil\n\t\t\t}\n\n\t\t\t// Create the processor function\n\t\t\tprocessor := func(ctx context.Context, work int) error {\n\t\t\t\tincActiveWorkers()\n\t\t\t\tdefer decActiveWorkers()\n\n\t\t\t\t// simulate some work\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\n\t\t\t\tworkMu.Lock()\n\t\t\t\tdefer workMu.Unlock()\n\t\t\t\treceivedWork = append(receivedWork, work)\n\n\t\t\t\t// If we have a process error, return it around half way through the work\n\t\t\t\tif tt.processErr != nil && len(receivedWork) > (toGenerate/2) {\n\t\t\t\t\treturn tt.processErr\n\t\t\t\t}\n\n\t\t\t\tif len(receivedWork) == toGenerate {\n\t\t\t\t\t// If we've received all the work, cancel the context\n\t\t\t\t\tcancel()\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\terr := WorkConcurrently(NewContexts(ctx), tt.concurrency, tt.maxBatchSize, fetcher, processor)\n\n\t\t\tworkMu.Lock()\n\t\t\tdefer workMu.Unlock()\n\n\t\t\t// Run assertions on the exit conditions\n\t\t\tc.Assert(timeoutCtx.Err(), qt.IsNil, qt.Commentf(\"test timed out - not all work was fetched within the timeout\"))\n\t\t\tswitch {\n\t\t\tcase tt.fetchErr != nil:\n\t\t\t\tc.Assert(err, qt.ErrorIs, tt.fetchErr, qt.Commentf(\"unexpected error from work concurrently\"))\n\t\t\t\tc.Assert(len(receivedWork) < toGenerate, qt.IsTrue, qt.Commentf(\"all the work was fetched even though there was a fetch error\"))\n\t\t\t\treturn\n\t\t\tcase tt.processErr != nil:\n\t\t\t\tc.Assert(err, qt.ErrorIs, tt.processErr, qt.Commentf(\"unexpected error from work concurrently\"))\n\t\t\t\tc.Assert(len(receivedWork) < toGenerate, qt.IsTrue, qt.Commentf(\"all the work was fetched even though there was a process error\"))\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tc.Assert(err, qt.IsNil, qt.Commentf(\"unexpected error from work concurrently\"))\n\t\t\t}\n\n\t\t\t// Run assertions on the processed data\n\t\t\tc.Assert(receivedWork, qt.HasLen, toGenerate, qt.Commentf(\"not all work was received/processed\"))\n\t\t\tif tt.concurrency > 0 {\n\t\t\t\tc.Assert(maxActiveWorkers <= tt.concurrency, qt.IsTrue, qt.Commentf(\"max concurrency was not respected; reached %d workers\", maxActiveWorkers))\n\t\t\t\tc.Assert(maxActiveWorkers == tt.concurrency, qt.IsTrue, qt.Commentf(\"max concurrency was not reached; only got %d workers at one time\", maxActiveWorkers))\n\t\t\t}\n\t\t\tsort.Ints(receivedWork)\n\t\t\tfor i, work := range receivedWork {\n\t\t\t\tc.Assert(work, qt.Equals, i, qt.Commentf(\"unexpected work received (once sorted); expected %d, got %d\", i, work))\n\t\t\t}\n\n\t\t\t// Run assertions on the fetch requests\n\t\t\tmaxBatchSize := tt.maxBatchSize\n\t\t\tif maxBatchSize <= 0 || (maxBatchSize > tt.concurrency && tt.concurrency > 0) {\n\t\t\t\tif tt.concurrency > 0 {\n\t\t\t\t\tmaxBatchSize = tt.concurrency\n\t\t\t\t} else {\n\t\t\t\t\tmaxBatchSize = 100\n\t\t\t\t}\n\t\t\t}\n\t\t\tc.Assert(fetchRequests[0].toFetch, qt.Equals, maxBatchSize, qt.Commentf(\"first fetch request was not the max batch size\"))\n\t\t\tc.Assert(fetchRequests[0].numReturned, qt.Equals, maxBatchSize, qt.Commentf(\"first fetch request did not return a full batch\"))\n\t\t\tfor i, req := range fetchRequests {\n\t\t\t\tc.Assert(req.toFetch, qt.Not(qt.Equals), 0, qt.Commentf(\"fetch request %d was 0\", i))\n\t\t\t\tc.Assert(req.toFetch <= maxBatchSize, qt.IsTrue, qt.Commentf(\"max batch size was not respected; requested %d items on fetch %d\", req, i))\n\t\t\t\tc.Assert(req.toFetch >= req.numReturned, qt.IsTrue, qt.Commentf(\"test function returned too many items\"))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWorkConcurrentlyLoad(t *testing.T) {\n\tconst load = 2000\n\tallMessages := make([]string, load)\n\tfor i := 0; i < load; i++ {\n\t\tallMessages[i] = fmt.Sprintf(\"msg %d\", i)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\n\tdefer cancel()\n\n\tvar err error\n\n\tvar (\n\t\tnumFetched        atomic.Int64\n\t\tnumProcessed      atomic.Int64\n\t\tprocessedMessages sync.Map\n\t)\n\n\tconst (\n\t\tmaxConcurrency = 25\n\t\tmaxBatchSize   = 10\n\t)\n\n\tfor ctx.Err() == nil {\n\t\terr = WorkConcurrently(NewContexts(ctx), maxConcurrency, maxBatchSize, func(ctx context.Context, numToFetch int) ([]string, error) {\n\t\t\t// Introduce random behavior\n\t\t\tswitch rand.Intn(10) {\n\t\t\tcase 0:\n\t\t\t\tpanic(\"random panic\")\n\t\t\tcase 1:\n\t\t\t\treturn nil, fmt.Errorf(\"random error\")\n\t\t\t}\n\n\t\t\t// Determine which slice of messages we should fetch\n\t\t\tend := numFetched.Add(int64(numToFetch))\n\t\t\tstart := end - int64(numToFetch)\n\n\t\t\tif numToFetch < 0 {\n\t\t\t\tpanic(fmt.Sprintf(\"negative numToFetch: %d\", numToFetch))\n\t\t\t} else if numToFetch > maxBatchSize {\n\t\t\t\tpanic(fmt.Sprintf(\"numToFetch too large: %d\", numToFetch))\n\t\t\t}\n\n\t\t\t// Clamp start and end to the bounds of the message slice\n\t\t\tif start > load {\n\t\t\t\tstart = load\n\t\t\t}\n\t\t\tif end > load {\n\t\t\t\tend = load\n\t\t\t}\n\n\t\t\tfetched := slices.Clone(allMessages[start:end])\n\n\t\t\t// If we didn't find enough items to fetch, decrease the num fetched again.\n\t\t\tif diff := numToFetch - len(fetched); diff > 0 {\n\t\t\t\tnumFetched.Add(-int64(diff))\n\t\t\t}\n\n\t\t\treturn fetched, nil\n\t\t}, func(ctx context.Context, work string) error {\n\t\t\tprocessedMessages.Store(work, true)\n\n\t\t\tif numProcessed.Add(1) == load {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\n\t\tfmt.Printf(\"published %v / consumed %v / target %v / err %v\\n\",\n\t\t\tnumFetched.Load(), numProcessed.Load(), load, err)\n\t}\n\n\tif numProcessed.Load() != load {\n\t\tt.Fatalf(\"expected %d processed items, got %d\", load, numProcessed.Load())\n\t}\n\tfor _, msg := range allMessages {\n\t\tif _, ok := processedMessages.Load(msg); !ok {\n\t\t\tt.Fatalf(\"message %q was not processed\", msg)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/manager_internal.go",
    "content": "package pubsub\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"sync\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\ntype Manager struct {\n\tctxs       *utils.Contexts\n\tstatic     *config.Static\n\truntime    *config.Runtime\n\trt         *reqtrack.RequestTracker\n\tts         *testsupport.Manager\n\trootLogger zerolog.Logger\n\tjson       jsoniter.API\n\tproviders  []provider\n\n\tpublishCounter  uint64\n\tpushHandlers    map[types.SubscriptionID]http.HandlerFunc\n\trunningFetches  sync.WaitGroup\n\trunningHandlers sync.WaitGroup\n}\n\nfunc NewManager(static *config.Static, runtime *config.Runtime, rt *reqtrack.RequestTracker,\n\tts *testsupport.Manager, rootLogger zerolog.Logger, json jsoniter.API) *Manager {\n\tmgr := &Manager{\n\t\tctxs:         utils.NewContexts(context.Background()),\n\t\tstatic:       static,\n\t\truntime:      runtime,\n\t\trt:           rt,\n\t\tts:           ts,\n\t\trootLogger:   rootLogger,\n\t\tjson:         json,\n\t\tpushHandlers: make(map[types.SubscriptionID]http.HandlerFunc),\n\t}\n\n\tfor _, p := range providerRegistry {\n\t\tmgr.providers = append(mgr.providers, p(mgr))\n\t}\n\n\treturn mgr\n}\n\n// Shutdown stops the manager from fetching new messages and processing them.\nfunc (mgr *Manager) Shutdown(p *shutdown.Process) error {\n\t// Once it's time to force-close tasks, cancel the base context.\n\tgo func() {\n\t\t<-p.ForceCloseTasks.Done()\n\t\tmgr.ctxs.CancelHandler()\n\t}()\n\n\tp.Log.Trace().Msg(\"pubsub: stop fetching new events\")\n\n\t// Immediately fetching new events.\n\tmgr.ctxs.StopFetchingNewEvents()\n\tp.Log.Trace().Msg(\"pubsub: waiting on running fetches\")\n\tmgr.runningFetches.Wait()\n\n\t// Wait for running handlers to finish.\n\tmgr.runningHandlers.Wait()\n\tp.MarkOutstandingPubSubMessagesCompleted()\n\n\t// Finally, close all connections to the PubSub providers.\n\tmgr.ctxs.CloseConnections()\n\n\treturn nil\n}\n\ntype provider interface {\n\tProviderName() string\n\tMatches(providerCfg *config.PubsubProvider) bool\n\tNewTopic(providerCfg *config.PubsubProvider, staticCfg TopicConfig, runtimeCfg *config.PubsubTopic) types.TopicImplementation\n}\n\nvar providerRegistry []func(*Manager) provider\n\nfunc registerProvider(p func(mgr *Manager) provider) {\n\tproviderRegistry = append(providerRegistry, p)\n}\n\nvar _ types.PushEndpointRegistry = (*Manager)(nil)\n\nfunc (mgr *Manager) RegisterPushSubscriptionHandler(id types.SubscriptionID, handler http.HandlerFunc) {\n\tmgr.pushHandlers[id] = handler\n}\n\n// HandlePubSubPush is an HTTP handler that accepts PubSub push HTTP requests\n// and routes them to the appropriate push handler.\nfunc (mgr *Manager) HandlePubSubPush(w http.ResponseWriter, req *http.Request, subscriptionID string) {\n\thandler, found := mgr.pushHandlers[types.SubscriptionID(subscriptionID)]\n\tif !found {\n\t\terr := errs.B().Code(errs.NotFound).Msg(\"unknown pubsub subscription\").Err()\n\t\tmgr.rootLogger.Err(err).Str(\"subscription_id\", subscriptionID).Msg(\"invalid PubSub push request\")\n\t\terrs.HTTPError(w, err)\n\t\treturn\n\t}\n\n\thandler(w, req)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/package.go",
    "content": "// Package pubsub provides Encore applications with the ability\n// to create Pub/Sub Topics and multiple Subscriptions on those\n// topics in a cloud-agnostic manner.\n//\n// For more information see https://encore.dev/docs/develop/pubsub\npackage pubsub\n"
  },
  {
    "path": "runtimes/go/pubsub/pkgfn.go",
    "content": "//go:build encore_app\n\npackage pubsub\n\n// NewTopic is used to declare a Topic. Encore will use static\n// analysis to identify Topics and automatically provision them\n// for you.\n//\n// A call to NewTopic can only be made when declaring a package level variable. Any\n// calls to this function made outside a package level variable declaration will result\n// in a compiler error.\n//\n// The topic name must be unique within an Encore application. Topic names must be defined\n// in kebab-case (lowercase alphanumerics and hyphen seperated). The topic name must start with a letter\n// and end with either a letter or number. It cannot be longer than 63 characters. Once created and deployed never\n// change the topic name. When refactoring the topic name must stay the same.\n// This allows for messages already on the topic to continue to be received after the refactored\n// code is deployed.\n//\n// Example:\n//\n//\t import \"encore.dev/pubsub\"\n//\n//\t type MyEvent struct {\n//\t   Foo string\n//\t }\n//\n//\t var MyTopic = pubsub.NewTopic[*MyEvent](\"my-topic\", pubsub.TopicConfig{\n//\t   DeliveryGuarantee: pubsub.AtLeastOnce,\n//\t })\n//\n//\t//encore:api public\n//\tfunc DoFoo(ctx context.Context) error {\n//\t  msgID, err := MyTopic.Publish(ctx, &MyEvent{Foo: \"bar\"})\n//\t  if err != nil { return err }\n//\t  rlog.Info(\"foo published\", \"message_id\", msgID)\n//\t  return nil\n//\t}\nfunc NewTopic[T any](name string, cfg TopicConfig) *Topic[T] {\n\treturn newTopic[T](Singleton, name, cfg)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/provider_aws.go",
    "content": "//go:build !encore_no_aws\n\npackage pubsub\n\nimport \"encore.dev/pubsub/internal/aws\"\n\nfunc init() {\n\tregisterProvider(func(mgr *Manager) provider {\n\t\treturn aws.NewManager(mgr.ctxs)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/provider_azure.go",
    "content": "//go:build !encore_no_azure\n\npackage pubsub\n\nimport \"encore.dev/pubsub/internal/azure\"\n\nfunc init() {\n\tregisterProvider(func(mgr *Manager) provider {\n\t\treturn azure.NewManager(mgr.ctxs)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/provider_encorecloud.go",
    "content": "//go:build !encore_no_encorecloud\n\npackage pubsub\n\nimport (\n\t\"encore.dev/pubsub/internal/encorecloud\"\n)\n\nfunc init() {\n\tregisterProvider(func(mgr *Manager) provider {\n\t\treturn encorecloud.NewManager(mgr.ctxs, mgr.runtime, mgr)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/provider_gcp.go",
    "content": "//go:build !encore_no_gcp\n\npackage pubsub\n\nimport (\n\t\"encore.dev/pubsub/internal/gcp\"\n)\n\nfunc init() {\n\tregisterProvider(func(mgr *Manager) provider {\n\t\treturn gcp.NewManager(mgr.ctxs, mgr.static, mgr.runtime, mgr)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/provider_nsq.go",
    "content": "//go:build !encore_no_local\n\npackage pubsub\n\nimport (\n\t\"encore.dev/pubsub/internal/nsq\"\n)\n\nfunc init() {\n\tregisterProvider(func(mgr *Manager) provider {\n\t\treturn nsq.NewManager(mgr.ctxs, mgr.rt)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/refs.go",
    "content": "package pubsub\n\nimport \"context\"\n\n// TopicPerms is the type constraint for all permission-declaring\n// interfaces that can be used with TopicRef.\ntype TopicPerms[T any] interface {\n\tMeta() TopicMeta\n}\n\n// Publisher is the interface for publishing messages to a topic.\n// It can be used in conjunction with [TopicRef] to declare\n// a reference that can publish messages to the topic.\n//\n// For example:\n//\n//\tvar MyTopic = pubsub.NewTopic[Msg](...)\n//\tvar ref = pubsub.TopicRef[pubsub.Publisher[Msg]](MyTopic)\n//\n// The ref object can then be used to publish messages and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyTopic.\ntype Publisher[T any] interface {\n\t// Publish publishes a message to the topic.\n\tPublish(ctx context.Context, msg T) (id string, err error)\n\n\t// Meta returns metadata about the topic.\n\tMeta() TopicMeta\n}\n\n// TopicRef returns an interface reference to a topic,\n// that can be freely passed around within a service\n// without being subject to Encore's typical static analysis\n// restrictions that normally apply to *Topic objects.\n//\n// This works because using TopicRef effectively declares\n// which operations you want to be able to perform since the\n// type argument P must be a permission-declaring interface (TopicPerms[T]).\n//\n// The returned reference is scoped down to those permissions.\n//\n// For example:\n//\n//\tvar MyTopic = pubsub.NewTopic[Msg](...)\n//\tvar ref = pubsub.TopicRef[pubsub.Publisher[Msg]](MyTopic)\n//\t// ref.Publish(...) can now be used to publish messages to MyTopic.\nfunc TopicRef[P TopicPerms[T], T any](topic *Topic[T]) P {\n\treturn any(topicRef[T]{Topic: topic}).(P)\n}\n\ntype topicRef[T any] struct {\n\t*Topic[T]\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/subscription.go",
    "content": "package pubsub\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/cfgutil\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/pubsub/internal/noop\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\n// Subscription represents a subscription to a Topic.\ntype Subscription[T any] struct {\n\ttopic *Topic[T]\n\tname  string\n\tcfg   SubscriptionConfig[T]\n\tmgr   *Manager\n}\n\n// NewSubscription is used to declare a Subscription to a topic. The passed in handler will be called\n// for each message published to the topic.\n//\n// A call to NewSubscription can only be made when declaring a package level variable. Any\n// calls to this function made outside a package level variable declaration will result\n// in a compiler error.\n//\n// The subscription name must be unique for that topic. Subscription names must be defined\n// in kebab-case (lowercase alphanumerics and hyphen separated). The subscription name must start with a letter\n// and end with either a letter or number. It cannot be longer than 63 characters.\n//\n// Once created and deployed never change the subscription name, or the topic name otherwise messages will be lost which\n// could be in flight.\n//\n// Example:\n//\n//\t\timport \"encore.dev/pubsub\"\n//\n//\t\ttype MyEvent struct {\n//\t\t  Foo string\n//\t\t}\n//\n//\t\tvar MyTopic = pubsub.NewTopic[*MyEvent](\"my-topic\", pubsub.TopicConfig{\n//\t\t  DeliveryGuarantee: pubsub.AtLeastOnce,\n//\t\t})\n//\n//\t\tvar Subscription = pubsub.NewSubscription(MyTopic, \"my-subscription\", pubsub.SubscriptionConfig[*MyEvent]{\n//\t\t  Handler:     HandleEvent,\n//\t\t  RetryPolicy: &pubsub.RetryPolicy{MaxRetries: 10},\n//\t      MaxConcurrency: 5,\n//\t\t})\n//\n//\t\tfunc HandleEvent(ctx context.Context, event *MyEvent) error {\n//\t\t  rlog.Info(\"received foo\")\n//\t\t  return nil\n//\t\t}\nfunc NewSubscription[T any](topic *Topic[T], name string, cfg SubscriptionConfig[T]) *Subscription[T] {\n\tif topic.runtimeCfg == nil || topic.topic == nil || topic.mgr == nil {\n\t\tpanic(\"pubsub topic was not created using pubsub.NewTopic\")\n\t}\n\n\tmgr := topic.mgr\n\tif _, isNoop := topic.topic.(*noop.Topic); isNoop {\n\t\t// no-op means no-op!\n\t\treturn &Subscription[T]{topic: topic, name: name, cfg: cfg, mgr: mgr}\n\t}\n\n\t// Set default config values for missing values\n\tif cfg.RetryPolicy == nil {\n\t\tcfg.RetryPolicy = &RetryPolicy{\n\t\t\tMaxRetries: 100,\n\t\t}\n\t}\n\tif cfg.RetryPolicy.MinBackoff < 0 {\n\t\tpanic(\"MinRetryDelay cannot be negative\")\n\t}\n\tif cfg.RetryPolicy.MaxBackoff < 0 {\n\t\tpanic(\"MaxRetryDelay cannot be negative\")\n\t}\n\tcfg.RetryPolicy.MinBackoff = utils.WithDefaultValue(cfg.RetryPolicy.MinBackoff, 10*time.Second)\n\tcfg.RetryPolicy.MaxBackoff = utils.WithDefaultValue(cfg.RetryPolicy.MaxBackoff, 10*time.Minute)\n\n\tif cfg.AckDeadline == 0 {\n\t\tcfg.AckDeadline = 30 * time.Second\n\t} else if cfg.AckDeadline < 0 {\n\t\tpanic(\"AckDeadline cannot be negative\")\n\t}\n\n\tsubscription, staticCfg, exists := topic.getSubscriptionConfig(name)\n\tif !exists {\n\t\t// Noop subscription\n\t\treturn &Subscription[T]{topic: topic, name: name, cfg: cfg, mgr: mgr}\n\t}\n\n\t// If the service isn't hosted, return a noop subscription.\n\tif !cfgutil.IsHostedService(mgr.runtime, staticCfg.Service) {\n\t\treturn &Subscription[T]{topic: topic, name: name, cfg: cfg, mgr: mgr}\n\t}\n\n\tpanicCatchWrapper := func(ctx context.Context, msg T) (err error) {\n\t\tdefer func() {\n\t\t\tif err2 := recover(); err2 != nil {\n\t\t\t\terr = errs.B().Code(errs.Internal).Msgf(\"subscriber panicked: %s\", err2).Err()\n\t\t\t}\n\t\t}()\n\n\t\treturn cfg.Handler(ctx, msg)\n\t}\n\n\tlog := mgr.rootLogger.With().\n\t\tStr(\"service\", staticCfg.Service).\n\t\tStr(\"topic\", topic.runtimeCfg.EncoreName).\n\t\tStr(\"subscription\", name).\n\t\tLogger()\n\n\t// Subscribe to the topic\n\ttopic.topic.Subscribe(&log, cfg.MaxConcurrency, cfg.AckDeadline, cfg.RetryPolicy, subscription, func(ctx context.Context, msgID string, publishTime time.Time, deliveryAttempt int, attrs map[string]string, data []byte) (err error) {\n\t\tif ctx.Err() != nil {\n\t\t\treturn ctx.Err()\n\t\t}\n\t\tmgr.runningHandlers.Add(1)\n\t\tdefer mgr.runningHandlers.Done()\n\n\t\tif !mgr.static.Testing {\n\t\t\t// Under test we're already inside an operation\n\t\t\tmgr.rt.BeginOperation()\n\t\t\tdefer mgr.rt.FinishOperation()\n\t\t}\n\n\t\tmsg, err := utils.UnmarshalMessage[T](attrs, data)\n\t\tif err != nil {\n\t\t\tlog.Err(err).Str(\"msg_id\", msgID).Int(\"delivery_attempt\", deliveryAttempt).Msg(\"failed to unmarshal message\")\n\t\t\treturn errs.B().Code(errs.Internal).Cause(err).Msg(\"failed to unmarshal message\").Err()\n\t\t}\n\n\t\tlogCtx := log.With()\n\n\t\ttraceID, err := model.GenTraceID()\n\t\tif err != nil {\n\t\t\tlog.Err(err).Str(\"msg_id\", msgID).Int(\"delivery_attempt\", deliveryAttempt).Msg(\"failed to generate trace id\")\n\t\t\treturn errs.B().Code(errs.Internal).Cause(err).Msg(\"failed to generate trace id\").Err()\n\t\t} else if traceID != (model.TraceID{}) {\n\t\t\tlogCtx = logCtx.Str(\"trace_id\", traceID.String())\n\t\t}\n\n\t\tspanID, err := model.GenSpanID()\n\t\tif err != nil {\n\t\t\tlog.Err(err).Str(\"msg_id\", msgID).Int(\"delivery_attempt\", deliveryAttempt).Msg(\"failed to generate span id\")\n\t\t\treturn errs.B().Code(errs.Internal).Cause(err).Msg(\"failed to generate span id\").Err()\n\t\t}\n\n\t\tvar parentTraceID model.TraceID\n\t\tif parentTraceIDStr := attrs[parentTraceIDAttribute]; parentTraceIDStr != \"\" {\n\t\t\tparentTraceID, err = model.ParseTraceID(parentTraceIDStr)\n\t\t\tif err != nil {\n\t\t\t\tlog.Err(err).Str(\"msg_id\", msgID).Int(\"delivery_attempt\", deliveryAttempt).Msg(\"failed to parse parent trace id\")\n\t\t\t}\n\t\t}\n\n\t\t// Default to logging with the external correlation id if present\n\t\textCorrelationID := attrs[extCorrelationIDAttribute]\n\t\tif extCorrelationID != \"\" {\n\t\t\tlogCtx = logCtx.Str(\"x_correlation_id\", extCorrelationID)\n\t\t} else if parentTraceID != (model.TraceID{}) {\n\t\t\tlogCtx = logCtx.Str(\"x_correlation_id\", parentTraceID.String())\n\t\t}\n\n\t\ttraced := attrs[forceTraceAttribute] == \"true\" ||\n\t\t\tmgr.rt.SamplePubSub(staticCfg.Service, topic.runtimeCfg.EncoreName, subscription.EncoreName)\n\n\t\t// Start the request tracing span\n\t\treq := &model.Request{\n\t\t\tType:             model.PubSubMessage,\n\t\t\tTraceID:          traceID,\n\t\t\tSpanID:           spanID,\n\t\t\tParentTraceID:    parentTraceID,\n\t\t\tExtCorrelationID: extCorrelationID,\n\t\t\tStart:            time.Now(),\n\t\t\tMsgData: &model.PubSubMsgData{\n\t\t\t\tDesc: &model.PubSubSubscriptionDesc{\n\t\t\t\t\tService:      staticCfg.Service,\n\t\t\t\t\tTopic:        topic.runtimeCfg.EncoreName,\n\t\t\t\t\tSubscription: subscription.EncoreName,\n\t\t\t\t\tScrubPaths:   staticCfg.ScrubPaths,\n\t\t\t\t},\n\t\t\t\tMessageID:      msgID,\n\t\t\t\tAttempt:        deliveryAttempt,\n\t\t\t\tPublished:      publishTime,\n\t\t\t\tDecodedPayload: msg,\n\t\t\t\tPayload:        marshalParams(mgr.json, msg),\n\t\t\t},\n\t\t\tDefLoc: staticCfg.TraceIdx,\n\t\t\tSvcNum: staticCfg.SvcNum,\n\t\t\tTraced: traced,\n\t\t}\n\t\treqLogger := logCtx.Logger()\n\t\treq.Logger = &reqLogger\n\n\t\t// Copy the previous request information over, if any\n\t\t{\n\t\t\tprev := mgr.rt.Current()\n\t\t\tif prevReq := prev.Req; prevReq != nil {\n\t\t\t\t// TODO(andre) is this correct, or should it be prevReq.SpanID?\n\t\t\t\t// Maybe it doesn't matter since subscriptions are always root spans anyway.\n\t\t\t\treq.ParentSpanID = prevReq.ParentSpanID\n\n\t\t\t\treq.Test = prevReq.Test\n\t\t\t}\n\t\t}\n\n\t\tmgr.rt.BeginRequest(req)\n\t\tcurr := mgr.rt.Current()\n\t\tif curr.Trace != nil {\n\t\t\tcurr.Trace.PubsubMessageSpanStart(req, curr.Goctr)\n\t\t}\n\n\t\terr = panicCatchWrapper(ctx, msg)\n\n\t\tif curr.Trace != nil {\n\t\t\tresp := &model.Response{\n\t\t\t\tDuration:   time.Since(req.Start),\n\t\t\t\tErr:        err,\n\t\t\t\tHTTPStatus: errs.HTTPStatus(err),\n\t\t\t}\n\t\t\tcurr.Trace.PubsubMessageSpanEnd(trace2.PubsubMessageSpanEndParams{\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: req.TraceID,\n\t\t\t\t\tSpanID:  req.SpanID,\n\t\t\t\t},\n\t\t\t\tReq:  req,\n\t\t\t\tResp: resp,\n\t\t\t})\n\t\t}\n\t\tmgr.rt.FinishRequest(false)\n\n\t\treturn err\n\t})\n\n\tif !mgr.static.Testing {\n\t\t// Log the subscription registration - unless we're in unit tests\n\t\tlog.Trace().Msg(\"registered subscription\")\n\t}\n\n\treturn &Subscription[T]{topic: topic, name: name, cfg: cfg, mgr: mgr}\n}\n\n// SubscriptionMeta contains metadata about a subscription.\n// The fields should not be modified by the caller.\n// Additional fields may be added in the future.\ntype SubscriptionMeta[T any] struct {\n\t// Name is the name of the subscription, as provided in the constructor to NewSubscription.\n\tName string\n\n\t// Config is the subscriptions's configuration.\n\tConfig SubscriptionConfig[T]\n\n\t// Topic provides metadata about the topic it subscribes to.\n\tTopic TopicMeta\n}\n\n// Meta returns metadata about the topic.\nfunc (t *Subscription[T]) Meta() SubscriptionMeta[T] {\n\treturn SubscriptionMeta[T]{\n\t\tName:   t.name,\n\t\tConfig: t.cfg,\n\t\tTopic:  t.topic.Meta(),\n\t}\n}\n\n// Config returns the subscription's configuration.\n// It must not be modified by the caller.\nfunc (s *Subscription[T]) Config() SubscriptionConfig[T] {\n\treturn s.cfg\n}\n\nfunc (t *Topic[T]) getSubscriptionConfig(name string) (cfg *config.PubsubSubscription, staticCfg *config.StaticPubsubSubscription, ok bool) {\n\tif t.mgr.static.Testing {\n\t\t// No subscriptions occur in testing\n\t\tsvcName, svcNum := t.mgr.ts.TestService()\n\t\treturn &config.PubsubSubscription{EncoreName: name}, &config.StaticPubsubSubscription{\n\t\t\tService: svcName,\n\t\t\tSvcNum:  svcNum,\n\t\t}, true\n\t}\n\n\t// Fetch the subscription configuration\n\tsubscription, ok := t.runtimeCfg.Subscriptions[name]\n\tif !ok {\n\t\treturn nil, nil, false\n\t}\n\n\tstaticCfg, ok = t.mgr.static.PubsubTopics[t.runtimeCfg.EncoreName].Subscriptions[name]\n\tif !ok {\n\t\treturn nil, nil, false\n\t}\n\n\treturn subscription, staticCfg, true\n}\n\nfunc marshalParams[Resp any](json jsoniter.API, resp Resp) []byte {\n\tdata, _ := json.Marshal(resp)\n\treturn data\n}\n\n// MethodHandler is used to define a subscription Handler that references a service struct method.\n//\n// Example Usage:\n//\n//\t//encore:service\n//\ttype Service struct {}\n//\n//\tfunc (s *Service) Method(ctx context.Context, msg *Event) error { /* ... */ }\n//\n//\tvar _ = pubsub.NewSubscription(Topic, \"subscription-name\", pubsub.SubscriptionConfig[*Event]{\n//\t\tHandler: pubsub.MethodHandler((*MyService).MyMethod),\n//\t\t// ...\n//\t})\nfunc MethodHandler[T, SvcStruct any](handler func(s SvcStruct, ctx context.Context, msg T) error) func(ctx context.Context, msg T) error {\n\t// The use of MethodHandler acts as a sentinel for the code generator,\n\t// which replaces the call with some generated code to initialize the service struct.\n\t// As such this function should never be called in practice.\n\treturn func(ctx context.Context, msg T) error {\n\t\treturn fmt.Errorf(\"pubsub.MethodHandler is not usable in this context\")\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/test_internal.go",
    "content": "package pubsub\n\nimport (\n\t\"encore.dev/pubsub/internal/test\"\n)\n\n// GetTestTopicInstance is an internal API for Encore. This function should\n// never be directly called as it is considered an unstable API and Encore\n// can change it at any time\nfunc GetTestTopicInstance[T any](topic *Topic[T]) any {\n\ttestTopic, ok := topic.topic.(*test.TestTopic[T])\n\tif !ok {\n\t\tpanic(\"testTopic not called with a test topic\")\n\t}\n\n\treq := topic.mgr.rt.Current().Req\n\tif req == nil || req.Test == nil {\n\t\tpanic(\"testTopic called outside of test\")\n\t}\n\treturn testTopic.TestInstance(req.Test.Current)\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/topic.go",
    "content": "package pubsub\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/internal/limiter\"\n\t\"encore.dev/pubsub/internal/noop\"\n\t\"encore.dev/pubsub/internal/test\"\n\t\"encore.dev/pubsub/internal/types\"\n\t\"encore.dev/pubsub/internal/utils\"\n)\n\n// Topic presents a flow of events of type T from any number of publishers to\n// any number of subscribers.\n//\n// Each subscription will receive a copy of each message published to the topic.\n//\n// See NewTopic for more information on how to declare a Topic.\ntype Topic[T any] struct {\n\tmgr            *Manager\n\tappCfg         TopicConfig // The config as defined in the applications source code\n\tstaticCfg      *config.StaticPubsubTopic\n\truntimeCfg     *config.PubsubTopic // The config for this running instance of the application\n\ttopic          types.TopicImplementation\n\tpublishLimiter limiter.Limiter\n}\n\nfunc newTopic[T any](mgr *Manager, name string, cfg TopicConfig) *Topic[T] {\n\tif mgr.static.Testing {\n\t\treturn &Topic[T]{\n\t\t\tappCfg:         cfg,\n\t\t\tstaticCfg:      mgr.static.PubsubTopics[name],\n\t\t\tmgr:            mgr,\n\t\t\truntimeCfg:     &config.PubsubTopic{EncoreName: name},\n\t\t\ttopic:          test.NewTopic[T](mgr.ts, name),\n\t\t\tpublishLimiter: limiter.New(nil), // Create a no-op limiter\n\t\t}\n\t}\n\n\t// Look up the topic configuration\n\ttopic, hasRuntimeCfg := mgr.runtime.PubsubTopics[name]\n\tstaticCfg, hasStaticCfg := mgr.static.PubsubTopics[name]\n\tif !hasRuntimeCfg || !hasStaticCfg {\n\t\t// If we don't have a topic configuration for this topic, it means that the topic was not registered for this instance\n\t\t// thus we should default to the noop implementation.\n\t\treturn &Topic[T]{\n\t\t\tappCfg:         cfg,\n\t\t\tmgr:            mgr,\n\t\t\truntimeCfg:     &config.PubsubTopic{EncoreName: name},\n\t\t\ttopic:          &noop.Topic{},\n\t\t\tpublishLimiter: limiter.New(nil), // Create a no-op limiter\n\t\t}\n\t}\n\n\t// Look up the server config\n\tprovider := mgr.runtime.PubsubProviders[topic.ProviderID]\n\n\ttried := make([]string, 0, len(mgr.providers))\n\tfor _, p := range mgr.providers {\n\t\tif p.Matches(provider) {\n\t\t\timpl := p.NewTopic(provider, cfg, topic)\n\t\t\treturn &Topic[T]{\n\t\t\t\tappCfg:         cfg,\n\t\t\t\tstaticCfg:      staticCfg,\n\t\t\t\tmgr:            mgr,\n\t\t\t\truntimeCfg:     topic,\n\t\t\t\ttopic:          impl,\n\t\t\t\tpublishLimiter: limiter.New(topic.Limiter),\n\t\t\t}\n\t\t}\n\t\ttried = append(tried, p.ProviderName())\n\t}\n\n\tmgr.rootLogger.Fatal().Msgf(\"unsupported PubSub provider for server[%d], tried: %v\",\n\t\ttopic.ProviderID, tried)\n\tpanic(\"unreachable\")\n}\n\n// TopicMeta contains metadata about a topic.\n// The fields should not be modified by the caller.\n// Additional fields may be added in the future.\ntype TopicMeta struct {\n\t// Name is the name of the topic, as provided in the constructor to NewTopic.\n\tName string\n\t// Config is the topic's configuration.\n\tConfig TopicConfig\n}\n\n// Meta returns metadata about the topic.\nfunc (t *Topic[T]) Meta() TopicMeta {\n\treturn TopicMeta{\n\t\tName:   t.runtimeCfg.EncoreName,\n\t\tConfig: t.appCfg,\n\t}\n}\n\n// Publish will publish a message to the topic and returns a unique message ID for the message.\n//\n// This function will not return until the message has been successfully accepted by the topic.\n//\n// If an error is returned, it is probable that the message failed to be published, however it is possible\n// that the message could still be received by subscriptions to the topic.\nfunc (t *Topic[T]) Publish(ctx context.Context, msg T) (id string, err error) {\n\tif ctx.Err() != nil {\n\t\treturn \"\", ctx.Err()\n\t}\n\n\tif t.runtimeCfg == nil || t.topic == nil {\n\t\treturn \"\", errs.B().Code(errs.Unimplemented).Msg(\"pubsub topic was not created using pubsub.NewTopic\").Err()\n\t}\n\n\t// Extract the message attributes\n\tattrs, err := utils.MarshalFields(msg, utils.AttrTag)\n\tif err != nil {\n\t\treturn \"\", errs.B().Cause(err).Code(errs.InvalidArgument).Msgf(\"failed to extract message attributes for topic %s\", t.runtimeCfg.EncoreName).Err()\n\t}\n\n\t// Marshal the message to JSON\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn \"\", errs.B().Cause(err).Code(errs.InvalidArgument).Msgf(\"failed to marshal message to JSON for topic %s\", t.runtimeCfg.EncoreName).Err()\n\t}\n\n\t// Add the ordering attribute if it is set\n\tvar orderingKey string\n\tif t.appCfg.OrderingAttribute != \"\" {\n\t\tvalue, found := attrs[t.appCfg.OrderingAttribute]\n\t\tif !found {\n\t\t\t// This is checked statically, so this should never happen\n\t\t\treturn \"\", errs.B().Code(errs.InvalidArgument).Msgf(\"ordering attribute %s not found in message for topic %s\", t.appCfg.OrderingAttribute, t.runtimeCfg.EncoreName).Err()\n\t\t}\n\n\t\tif value == \"\" {\n\t\t\treturn \"\", errs.B().Code(errs.InvalidArgument).Msgf(\"ordering attribute %s cannot be an empty string for topic %s\", t.appCfg.OrderingAttribute, t.runtimeCfg.EncoreName).Err()\n\t\t}\n\n\t\torderingKey = value\n\t}\n\n\t// Add the correlation ID to the attributes\n\tif req := t.mgr.rt.Current().Req; req != nil {\n\t\t// Pass our trace ID through, so the subscribers can mark their traces as children of this trace\n\t\tif req.TraceID != (model.TraceID{}) {\n\t\t\tattrs[parentTraceIDAttribute] = req.TraceID.String()\n\t\t}\n\n\t\tif req.ExtCorrelationID != \"\" {\n\t\t\t// If we have a correlation ID from the request, use that\n\t\t\tattrs[extCorrelationIDAttribute] = req.ExtCorrelationID\n\t\t} else if req.TraceID != (model.TraceID{}) {\n\t\t\t// Otherwise this is the first request in the event chain, so this trace ID becomes the correlation ID\n\t\t\tattrs[extCorrelationIDAttribute] = req.TraceID.String()\n\t\t}\n\n\t\t// If this is a platform request, propagate the sampled flag so that\n\t\t// subscribers always trace platform-initiated messages.\n\t\tif req.RPCData != nil && req.RPCData.FromEncorePlatform {\n\t\t\tattrs[forceTraceAttribute] = \"true\"\n\t\t}\n\t}\n\n\t// Start the trace span\n\tcurr := t.mgr.rt.Current()\n\tvar startEventID trace2.EventID\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tdesc := &model.PubSubTopicDesc{\n\t\t\tTopic: t.runtimeCfg.EncoreName,\n\t\t}\n\t\tif t.staticCfg != nil {\n\t\t\tdesc.ScrubPaths = t.staticCfg.ScrubPaths\n\t\t}\n\t\tstartEventID = curr.Trace.PubsubPublishStart(trace2.PubsubPublishStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tDesc:    desc,\n\t\t\tMessage: data,\n\t\t\tStack:   stack.Build(1),\n\t\t})\n\t}\n\n\t// Publish once the rate limiter allows it\n\tif err = t.publishLimiter.Wait(ctx); err == nil {\n\t\t// Publish to the clouds topic\n\t\tid, err = t.topic.PublishMessage(ctx, orderingKey, attrs, data)\n\t}\n\n\t// End the trace span\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.PubsubPublishEnd(trace2.PubsubPublishEndParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tStartID:   startEventID,\n\t\t\tMessageID: id,\n\t\t\tErr:       err,\n\t\t})\n\t}\n\n\tif err != nil {\n\t\treturn \"\", errs.B().Cause(err).Code(errs.Unavailable).Msgf(\"failed to publish message to %s\", t.runtimeCfg.EncoreName).Err()\n\t}\n\n\treturn id, nil\n}\n"
  },
  {
    "path": "runtimes/go/pubsub/types.go",
    "content": "package pubsub\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"encore.dev/pubsub/internal/types\"\n)\n\n// parentTraceIDAttribute is the attribute name we use to track request correlation IDs\nconst parentTraceIDAttribute = \"encore_parent_trace_id\"\n\n// extCorrelationIDAttribute is the attribute name we use to track externally provided correlation IDs\nconst extCorrelationIDAttribute = \"encore_ext_correlation_id\"\n\n// forceTraceAttribute is set to \"true\" when the message must always be traced,\n// such as when the publishing request is a platform request.\nconst forceTraceAttribute = \"encore_force_trace\"\n\n// SubscriptionConfig is used when creating a subscription\n//\n// The values given here may be clamped to the supported values by\n// the target cloud. (i.e. ack deadline may be brought within the supported range\n// by the target cloud pubsub implementation).\ntype SubscriptionConfig[T any] struct {\n\t// Handler is the function which will be called to process a message\n\t// sent on the topic.\n\t//\n\t// To reference a method on an [Encore service struct]\n\t// you can use the [MethodHandler] function. For example:\n\t//\n\t//\tHandler: pubsub.MethodHandler((*MyService).MyMethod)\n\t//\n\t// It is important for the Handler function to block and not return\n\t// until all processing relating to the message has been completed.\n\t//\n\t// When the handler returns a nil error the message will be\n\t// acknowledged (acked) from the topic, and should not be redelivered.\n\t//\n\t// When this function returns a non-nil error the message will be\n\t// negatively acknowledged (nacked), which will cause a redelivery\n\t// attempt to be made (unless the retry policy's MaxRetries has been reached).\n\t//\n\t// The ctx passed to the handler will be cancelled when\n\t// the AckDeadline passes.\n\t//\n\t// This field is required.\n\t//\n\t// [Encore service struct]: https://encore.dev/docs/primitives/services-and-apis/service-structs\n\tHandler func(ctx context.Context, msg T) error\n\n\t// MaxConcurrency is the maximum number of messages which will be processed\n\t// simultaneously per instance of the service for this subscription.\n\t//\n\t// Note that this is per instance of the service, so if your service has\n\t// scaled to 10 instances and this is set to 10, then 100 messages could be\n\t// processed simultaneously.\n\t//\n\t// If the value is negative, then there will be no limit on the number\n\t// of messages processed simultaneously.\n\t//\n\t// Note: This is not supported by all cloud providers; specifically on GCP\n\t// when using Cloud Run instances on a topic with at-least-once delivery, the\n\t// subscription will be configured as a Push Subscription and will have an adaptive\n\t// concurrency. See [GCP Push Delivery Rate].\n\t//\n\t// This setting also has no effect on Encore Cloud environments.\n\t//\n\t// If not set, it uses a reasonable default based on the cloud provider.\n\t//\n\t// [GCP Push Delivery Rate]: https://cloud.google.com/pubsub/docs/push#push_delivery_rate\n\tMaxConcurrency int\n\n\t// Filter is a boolean expression using =, !=, IN, &&\n\t// It is used to filter which messages are forwarded from the\n\t// topic to a subscription\n\t// Filter string - Filters are not currently supported\n\n\t// AckDeadline is the time a consumer has to process a message\n\t// before it's returned to the subscription\n\t//\n\t// Default is 30 seconds, however the ack deadline must be at least\n\t// 1 second.\n\tAckDeadline time.Duration\n\n\t// MessageRetention is how long an undelivered message is kept\n\t// on the topic before it's purged\n\t// Default is 7 days.\n\tMessageRetention time.Duration\n\n\t// RetryPolicy defines how a message should be retried when\n\t// the subscriber returns an error\n\tRetryPolicy *RetryPolicy\n}\n\ntype RetryPolicy = types.RetryPolicy\n\nconst (\n\tNoRetries = types.NoRetries\n\n\tInfiniteRetries = types.InfiniteRetries\n)\n\ntype DeliveryGuarantee = types.DeliveryGuarantee\n\nconst (\n\tAtLeastOnce = types.AtLeastOnce\n\n\tExactlyOnce = types.ExactlyOnce\n)\n\ntype TopicConfig = types.TopicConfig\n"
  },
  {
    "path": "runtimes/go/pubsub/zzz_singleton_internal.go",
    "content": "//go:build encore_app\n\npackage pubsub\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/jsonapi\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\n// Initialize the singleton instance.\n// NOTE: This file is named zzz_singleton_internal.go so that\n// the init function is initialized after all the providers\n// have been registered.\n\n//publicapigen:drop\nvar Singleton *Manager\n\nfunc init() {\n\tSingleton = NewManager(\n\t\tappconf.Static, appconf.Runtime, reqtrack.Singleton, testsupport.Singleton,\n\t\tlogging.RootLogger, jsonapi.Default,\n\t)\n\tshutdown.Singleton.RegisterShutdownHandler(Singleton.Shutdown)\n}\n"
  },
  {
    "path": "runtimes/go/request.go",
    "content": "package encore\n\nimport (\n\t\"net/http\"\n\t\"reflect\"\n\t\"slices\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/model\"\n)\n\nvar applicationStartTime = time.Now()\n\n// APIDesc describes the API endpoint being called.\ntype APIDesc struct {\n\t// RequestType specifies the type of the request payload,\n\t// or nil if the endpoint has no request payload or is Raw.\n\tRequestType reflect.Type\n\n\t// ResponseType specifies the type of the response payload,\n\t// or nil if the endpoint has no response payload or is Raw.\n\tResponseType reflect.Type\n\n\t// Raw specifies whether the endpoint is a Raw endpoint.\n\tRaw bool\n\n\t// Tags describes what tags are attached to the endpoint.\n\tTags Tags\n\n\t// Exposed is true if the endpoint is exposed to the public internet.\n\t// This is true for \"public\" and \"auth\" endpoints.\n\tExposed bool\n\n\t// AuthRequired is true if the endpoint requires authentication to be called.\n\t// This is true for \"auth\" endpoints.\n\tAuthRequired bool\n}\n\n// Request provides metadata about how and why the currently running code was started.\n//\n// The current request can be returned by calling CurrentRequest()\ntype Request struct {\n\tType    RequestType // What caused this request to start\n\tStarted time.Time   // What time the trigger occurred\n\n\t// Trace contains the trace information for the current request.\n\tTrace *TraceData\n\n\t// APICall specific parameters.\n\t// These will be empty for operations with a type not APICall\n\tAPI        *APIDesc   // Metadata about the API endpoint being called\n\tService    string     // Which service is processing this request\n\tEndpoint   string     // Which API endpoint is being called\n\tPath       string     // What was the path made to the API server\n\tPathParams PathParams // If there are path parameters, what are they?\n\tMethod     string     // What HTTP method was used\n\n\t// Headers contains the request headers sent with the request, if any.\n\t//\n\t// It is currently empty for service-to-service API calls when the caller\n\t// and callee are both running within the same process.\n\t// This behavior may change in the future.\n\tHeaders http.Header\n\n\t// PubSubMessage specific parameters.\n\t// Message contains information about the PubSub message,\n\tMessage *MessageData\n\n\t// Payload is the decoded request payload or Pub/Sub message payload,\n\t// or nil if the API endpoint has no request payload or the endpoint is raw.\n\tPayload any\n\n\t// CronIdempotencyKey contains a unique id for a particular cron job execution\n\t// if this request was triggered by a Cron Job.\n\t//\n\t// It can be used to uniquely identify a particular Cron Job execution event,\n\t// and also serves as a way to distinguish between Cron Job-triggered requests\n\t// and other requests.\n\t//\n\t// If the request was not triggered by a Cron Job the value is the empty string.\n\tCronIdempotencyKey string\n}\n\n// TraceData describes the trace information for a request.\ntype TraceData struct {\n\tTraceID          string\n\tSpanID           string\n\tParentTraceID    string // empty if no parent trace\n\tParentSpanID     string // empty if no parent span\n\tExtCorrelationID string // empty if no correlation id\n\tRecorded         bool   // true if this trace is being recorded\n}\n\n// MessageData describes the request data for a Pub/Sub message.\ntype MessageData struct {\n\t// Service is the name of the service with the subscription.\n\tService string\n\n\t// Topic is the name of the topic the message was published to.\n\tTopic string\n\n\t// Subscription is the name of the subscription the message was received on.\n\tSubscription string\n\n\t// ID is the unique ID of the message assigned by the messaging service.\n\t// It is the same value returned by topic.Publish() and is the same\n\t// across delivery attempts.\n\tID string\n\n\t// Published is the time the message was first published.\n\tPublished time.Time\n\n\t// DeliveryAttempt is a counter for how many times the messages\n\t// has been attempted to be delivered.\n\tDeliveryAttempt int\n}\n\n// RequestType describes how the currently running code was triggered\ntype RequestType string\n\nconst (\n\tNone          RequestType = \"none\"           // There was no external trigger which caused this code to run. Most likely it was triggered by a package level init function.\n\tAPICall       RequestType = \"api-call\"       // The code was triggered via an API call to a service\n\tPubSubMessage RequestType = \"pubsub-message\" // The code was triggered by a PubSub subscriber\n)\n\n// PathParams contains the path parameters parsed from the request path.\n// The ordering of the parameters in the path will be maintained from the URL.\ntype PathParams []PathParam\n\n// PathParam represents a parsed path parameter.\ntype PathParam struct {\n\tName  string // the name of the path parameter, without leading ':' or '*'.\n\tValue string // the parsed path parameter value.\n}\n\n// Get returns the value of the path parameter with the given name.\n// If no such parameter exists it reports \"\".\nfunc (p PathParams) Get(name string) string {\n\tfor _, param := range p {\n\t\tif param.Name == name {\n\t\t\treturn param.Value\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc (mgr *Manager) CurrentRequest() *Request {\n\treq := mgr.rt.Current().Req\n\tif req == nil {\n\t\treturn &Request{\n\t\t\tType:    None,\n\t\t\tStarted: applicationStartTime,\n\t\t}\n\t}\n\n\tresult := &Request{\n\t\tStarted: req.Start,\n\t\tTrace: &TraceData{\n\t\t\tTraceID:          req.TraceID.String(),\n\t\t\tSpanID:           req.SpanID.String(),\n\t\t\tParentTraceID:    req.ParentTraceID.String(),\n\t\t\tParentSpanID:     req.ParentSpanID.String(),\n\t\t\tExtCorrelationID: req.ExtCorrelationID,\n\t\t\tRecorded:         req.Traced,\n\t\t},\n\t}\n\n\tswitch req.Type {\n\tcase model.RPCCall, model.AuthHandler:\n\t\tdata := req.RPCData\n\t\tdesc := data.Desc\n\n\t\tresult.Type = APICall\n\t\tresult.Service = desc.Service\n\t\tresult.Endpoint = desc.Endpoint\n\t\tresult.Payload = data.TypedPayload\n\n\t\tresult.Path = data.Path\n\t\tresult.PathParams = make(PathParams, len(data.PathParams))\n\t\tfor i, param := range data.PathParams {\n\t\t\tresult.PathParams[i].Name = param.Name\n\t\t\tresult.PathParams[i].Value = param.Value\n\t\t}\n\t\tresult.Method = data.HTTPMethod\n\t\tresult.Headers = data.RequestHeaders\n\n\t\tresult.API = &APIDesc{\n\t\t\tRequestType:  desc.RequestType,\n\t\t\tResponseType: desc.ResponseType,\n\t\t\tRaw:          desc.Raw,\n\t\t\tTags:         desc.Tags,\n\t\t\tExposed:      desc.Exposed,\n\t\t\tAuthRequired: desc.AuthRequired,\n\t\t}\n\n\t\tif data.FromEncorePlatform {\n\t\t\tresult.CronIdempotencyKey = data.RequestHeaders.Get(\"X-Encore-Cron-Execution\")\n\t\t}\n\n\tcase model.PubSubMessage:\n\t\tresult.Type = PubSubMessage\n\t\tresult.Service = req.MsgData.Desc.Service\n\t\tresult.Payload = req.MsgData.DecodedPayload\n\t\tresult.Message = &MessageData{\n\t\t\tService:         req.MsgData.Desc.Service,\n\t\t\tTopic:           req.MsgData.Desc.Topic,\n\t\t\tSubscription:    req.MsgData.Desc.Subscription,\n\t\t\tID:              req.MsgData.MessageID,\n\t\t\tPublished:       req.MsgData.Published,\n\t\t\tDeliveryAttempt: req.MsgData.Attempt,\n\t\t}\n\t}\n\n\treturn result\n}\n\n// Tags describes a set of tags an endpoint is tagged with,\n// without the \"tag:\" prefix.\n//\n// The ordering is unspecified.\ntype Tags []string\n\n// Has reports whether the set contains the given tag.\n// The provided value should not contain the \"tag:\" prefix.\nfunc (tags Tags) Has(tag string) bool {\n\treturn slices.Contains(tags, tag)\n}\n"
  },
  {
    "path": "runtimes/go/rlog/pkgfn.go",
    "content": "//go:build encore_app\n\npackage rlog\n\nimport \"encore.dev/appruntime/shared/reqtrack\"\n\n//publicapigen:drop\nvar Singleton = NewManager(reqtrack.Singleton)\n\n// Debug logs a debug-level message.\n// The variadic key-value pairs are treated as they are in With.\nfunc Debug(msg string, keysAndValues ...any) {\n\tSingleton.Debug(msg, keysAndValues...)\n}\n\n// Info logs an info-level message.\n// The variadic key-value pairs are treated as they are in With.\nfunc Info(msg string, keysAndValues ...any) {\n\tSingleton.Info(msg, keysAndValues...)\n}\n\n// Warn logs a warn-level message.\n// The variadic key-value pairs are treated as they are in With.\nfunc Warn(msg string, keysAndValues ...any) {\n\tSingleton.Warn(msg, keysAndValues...)\n}\n\n// Error logs an error-level message.\n// The variadic key-value pairs are treated as they are in With.\nfunc Error(msg string, keysAndValues ...any) {\n\tSingleton.Error(msg, keysAndValues...)\n}\n\n// With adds a variadic number of fields to the logging context.\n// The keysAndValues must be pairs of string keys and arbitrary data.\nfunc With(keysAndValues ...any) Ctx {\n\treturn Singleton.With(keysAndValues...)\n}\n"
  },
  {
    "path": "runtimes/go/rlog/rlog.go",
    "content": "// Package rlog provides a simple logging interface which is integrated with Encore's\n// inbuilt distributed tracing.\n//\n// For more information about logging inside Encore applications see https://encore.dev/docs/observability/logging.\npackage rlog\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/types/uuid\"\n)\n\n// InternalKeyPrefix is the prefix of log field keys that are reserved for\n// internal use only. Log fields starting with this value have an additional \"x_\"\n// prefix prepended to avoid interference with reserved names.\n//\n//publicapigen:drop\nconst InternalKeyPrefix = \"encore_\"\n\n//publicapigen:drop\ntype Manager struct {\n\trt *reqtrack.RequestTracker\n}\n\n//publicapigen:drop\nfunc NewManager(rt *reqtrack.RequestTracker) *Manager {\n\treturn &Manager{rt}\n}\n\n// Ctx holds additional logging context for use with the Infoc and family\n// of logging functions.\ntype Ctx struct {\n\tlogger zerolog.Logger\n\tmgr    *Manager\n\tfields []any\n}\n\nfunc (l *Manager) Debug(msg string, keysAndValues ...any) {\n\tfields := pairs(keysAndValues)\n\tl.doLog(model.LevelDebug, l.rt.Logger().Debug(), msg, nil, fields)\n}\n\nfunc (l *Manager) Info(msg string, keysAndValues ...any) {\n\tfields := pairs(keysAndValues)\n\tl.doLog(model.LevelInfo, l.rt.Logger().Info(), msg, nil, fields)\n}\n\nfunc (l *Manager) Warn(msg string, keysAndValues ...any) {\n\tfields := pairs(keysAndValues)\n\tl.doLog(model.LevelWarn, l.rt.Logger().Warn(), msg, nil, fields)\n}\n\nfunc (l *Manager) Error(msg string, keysAndValues ...any) {\n\tfields := pairs(keysAndValues)\n\tl.doLog(model.LevelError, l.rt.Logger().Error(), msg, nil, fields)\n}\n\nfunc (l *Manager) With(keysAndValues ...any) Ctx {\n\tctx := l.rt.Logger().With()\n\tfields := pairs(keysAndValues)\n\tfor i := 0; i < len(fields); i += 2 {\n\t\tkey := fields[i].(string)\n\t\tval := fields[i+1]\n\t\tctx = addContext(ctx, key, val)\n\t}\n\treturn Ctx{logger: ctx.Logger(), mgr: l, fields: fields}\n}\n\n// Debug logs a debug-level message, merging the context from ctx\n// with the additional context provided as key-value pairs.\n// The variadic key-value pairs are treated as they are in With.\nfunc (ctx Ctx) Debug(msg string, keysAndValues ...any) {\n\tl := ctx.logger\n\tfields := pairs(keysAndValues)\n\tctx.mgr.doLog(model.LevelDebug, l.Debug(), msg, ctx.fields, fields)\n}\n\n// Info logs an info-level message, merging the context from ctx\n// with the additional context provided as key-value pairs.\n// The variadic key-value pairs are treated as they are in With.\nfunc (ctx Ctx) Info(msg string, keysAndValues ...any) {\n\tl := ctx.logger\n\tfields := pairs(keysAndValues)\n\tctx.mgr.doLog(model.LevelInfo, l.Info(), msg, ctx.fields, fields)\n}\n\n// Warn logs a warn-level message, merging the context from ctx\n// with the additional context provided as key-value pairs.\n// The variadic key-value pairs are treated as they are in With.\nfunc (ctx Ctx) Warn(msg string, keysAndValues ...any) {\n\tl := ctx.logger\n\tfields := pairs(keysAndValues)\n\tctx.mgr.doLog(model.LevelWarn, l.Warn(), msg, ctx.fields, fields)\n}\n\n// Error logs an error-level message, merging the context from ctx\n// with the additional context provided as key-value pairs.\n// The variadic key-value pairs are treated as they are in With.\nfunc (ctx Ctx) Error(msg string, keysAndValues ...any) {\n\tl := ctx.logger\n\tfields := pairs(keysAndValues)\n\tctx.mgr.doLog(model.LevelError, l.Error(), msg, ctx.fields, fields)\n}\n\n// With creates a new logging context that inherits the context\n// from the original ctx and adds additional context on top.\n// The original ctx is not affected.\nfunc (ctx Ctx) With(keysAndValues ...any) Ctx {\n\tc := ctx.logger.With()\n\tfields := pairs(keysAndValues)\n\tfor i := 0; i < len(fields); i += 2 {\n\t\tkey := fields[i].(string)\n\t\tval := fields[i+1]\n\t\tc = addContext(c, key, val)\n\t}\n\n\tnewFields := make([]any, len(ctx.fields)+len(fields))\n\tcopy(newFields, ctx.fields)\n\tcopy(newFields[len(ctx.fields):], fields)\n\n\treturn Ctx{logger: c.Logger(), mgr: ctx.mgr, fields: newFields}\n}\n\nfunc (l *Manager) doLog(level model.LogLevel, ev *zerolog.Event, msg string, ctxFields, logFields []any) {\n\tvar (\n\t\ttp     trace2.LogMessageParams\n\t\ttraced bool\n\t)\n\tcurr := l.rt.Current()\n\tnumFields := len(ctxFields)/2 + len(logFields)/2\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\ttraced = true\n\t\ttp = trace2.LogMessageParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tLevel:  level,\n\t\t\tMsg:    msg,\n\t\t\tStack:  stack.Build(3),\n\t\t\tFields: make([]trace2.LogField, 0, numFields),\n\t\t}\n\n\t\tfor i := 0; i < len(ctxFields); i += 2 {\n\t\t\tkey := ctxFields[i].(string)\n\t\t\tval := ctxFields[i+1]\n\t\t\ttp.Fields = append(tp.Fields, trace2.LogField{Key: key, Value: val})\n\t\t}\n\t}\n\n\tfor i := 0; i < len(logFields); i += 2 {\n\t\tkey := logFields[i].(string)\n\t\tval := logFields[i+1]\n\t\taddEventEntry(ev, key, val)\n\t\tif traced {\n\t\t\ttp.Fields = append(tp.Fields, trace2.LogField{Key: key, Value: val})\n\t\t}\n\t}\n\n\tev.Msg(msg)\n\n\tif traced {\n\t\tcurr.Trace.LogMessage(tp)\n\t}\n}\n\nfunc addEventEntry(ev *zerolog.Event, key string, val any) {\n\tif reserved(key) {\n\t\tkey = \"x_\" + key\n\t}\n\n\tswitch val := val.(type) {\n\tcase error:\n\t\tev.AnErr(key, val)\n\tcase string:\n\t\tev.Str(key, val)\n\tcase bool:\n\t\tev.Bool(key, val)\n\n\tcase time.Time:\n\t\tev.Time(key, val)\n\tcase time.Duration:\n\t\tev.Dur(key, val)\n\tcase uuid.UUID:\n\t\tev.Str(key, val.String())\n\n\tdefault:\n\t\tev.Interface(key, val)\n\n\tcase int8:\n\t\tev.Int8(key, val)\n\tcase int16:\n\t\tev.Int16(key, val)\n\tcase int32:\n\t\tev.Int32(key, val)\n\tcase int64:\n\t\tev.Int64(key, val)\n\tcase int:\n\t\tev.Int(key, val)\n\n\tcase uint8:\n\t\tev.Uint8(key, val)\n\tcase uint16:\n\t\tev.Uint16(key, val)\n\tcase uint32:\n\t\tev.Uint32(key, val)\n\tcase uint64:\n\t\tev.Uint64(key, val)\n\tcase uint:\n\t\tev.Uint(key, val)\n\n\tcase float32:\n\t\tev.Float32(key, val)\n\tcase float64:\n\t\tev.Float64(key, val)\n\t}\n}\n\nfunc addContext(ctx zerolog.Context, key string, val any) zerolog.Context {\n\tif reserved(key) {\n\t\tkey = \"x_\" + key\n\t}\n\n\tswitch val := val.(type) {\n\tcase error:\n\t\treturn ctx.AnErr(key, val)\n\tcase string:\n\t\treturn ctx.Str(key, val)\n\tcase bool:\n\t\treturn ctx.Bool(key, val)\n\n\tcase time.Time:\n\t\treturn ctx.Time(key, val)\n\tcase time.Duration:\n\t\treturn ctx.Dur(key, val)\n\tcase uuid.UUID:\n\t\treturn ctx.Str(key, val.String())\n\n\tdefault:\n\t\treturn ctx.Interface(key, val)\n\n\tcase int8:\n\t\treturn ctx.Int8(key, val)\n\tcase int16:\n\t\treturn ctx.Int16(key, val)\n\tcase int32:\n\t\treturn ctx.Int32(key, val)\n\tcase int64:\n\t\treturn ctx.Int64(key, val)\n\tcase int:\n\t\treturn ctx.Int(key, val)\n\n\tcase uint8:\n\t\treturn ctx.Uint8(key, val)\n\tcase uint16:\n\t\treturn ctx.Uint16(key, val)\n\tcase uint32:\n\t\treturn ctx.Uint32(key, val)\n\tcase uint64:\n\t\treturn ctx.Uint64(key, val)\n\tcase uint:\n\t\treturn ctx.Uint(key, val)\n\n\tcase float32:\n\t\treturn ctx.Float32(key, val)\n\tcase float64:\n\t\treturn ctx.Float64(key, val)\n\t}\n}\n\nfunc reserved(key string) bool {\n\treturn strings.HasPrefix(key, InternalKeyPrefix)\n}\n\n// pairs ensures the key-values are in pairs.\n// It drops the last entry if there's an odd number of entries.\nfunc pairs(keysAndValues []any) []any {\n\tfields := keysAndValues\n\tnum := len(fields)\n\tif num%2 == 1 {\n\t\t// Odd number of key-values, drop the last one\n\t\tnum--\n\t\tfields = fields[:num]\n\t}\n\treturn fields\n}\n"
  },
  {
    "path": "runtimes/go/rlog/rlog_test.go",
    "content": "package rlog\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/rs/zerolog\"\n)\n\nfunc TestReserveEncoreKey(t *testing.T) {\n\ttestCases := []struct {\n\t\tKey  string\n\t\tWant string\n\t}{\n\t\t{\n\t\t\tKey: \"key\",\n\t\t\tWant: `{\"level\":\"info\",\"key\":\"value\"}\n`,\n\t\t},\n\t\t{\n\t\t\tKey: \"encore_key\",\n\t\t\tWant: `{\"level\":\"info\",\"x_encore_key\":\"value\"}\n`,\n\t\t},\n\t\t{\n\t\t\tKey: \"encorekey\",\n\t\t\tWant: `{\"level\":\"info\",\"encorekey\":\"value\"}\n`,\n\t\t},\n\t}\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.Key, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := zerolog.New(&buf)\n\t\t\tev := logger.Info()\n\t\t\taddEventEntry(ev, testCase.Key, \"value\")\n\t\t\tev.Send()\n\t\t\tactual := buf.String()\n\t\t\tif actual != testCase.Want {\n\t\t\t\tt.Fatalf(\"\\nwant:\\n\\t%q\\ngot:\\n\\t%q\\n\", testCase.Want, actual)\n\t\t\t}\n\t\t})\n\t\tt.Run(testCase.Key, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := zerolog.New(&buf)\n\t\t\tlogger = addContext(logger.With(), testCase.Key, \"value\").Logger()\n\t\t\tlogger.Info().Send()\n\t\t\tactual := buf.String()\n\t\t\tif actual != testCase.Want {\n\t\t\t\tt.Fatalf(\"\\nwant:\\n\\t%q\\ngot:\\n\\t%q\\n\", testCase.Want, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/shutdown/shutdown.go",
    "content": "package shutdown\n\nimport \"context\"\n\n// Progress provides progress information about an ongoing graceful shutdown process.\n//\n// The process broadly consists of two phases:\n//\n// 1. Drain active tasks\n//\n// As soon as the graceful shutdown process is initiated, the service will stop accepting new\n// incoming API calls and Pub/Sub messages. It will continue to process already running tasks\n// until they complete (or the ForceCloseTasks deadline is reached).\n//\n// Additionally, all service structs that implement [Handler] will have their [Handler.Shutdown]\n// function called when this phase begins. The [Handler.Shutdown] method receives a [Progress]\n// struct that can be used to monitor the progress of the shutdown process, and allows the service\n// to perform any necessary cleanup at the right time.\n//\n// This phase continues until all active tasks and handlers have completed or the ForceCloseTasks deadline\n// is reached, whichever happens first. The OutstandingRequests, OutstandingPubSubMessages, and\n// OutstandingTasks contexts provide insight into what tasks are still active.\n//\n// 2. Shut down infrastructure resources\n//\n// When all active tasks and [Handler.Shutdown] calls have completed, Encore begins shutting down\n// infrastructure resources. Encore automatically closes all open database connections, cache connections,\n// Pub/Sub connections, and other infrastructure resources.\n//\n// This phase continues until all infrastructure resources have been closed or the ForceShutdown deadline\n// is reached, whichever happens first.\n//\n// 3. Exit\n//\n// Once phase two has completed, the process will exit.\n// The exit code is 0 if the graceful shutdown completed successfully (meaning all resources\n// returned before the exit deadline), or 1 otherwise.\ntype Progress struct {\n\t// OutstandingRequests is canceled when the service is no longer processing any incoming API calls.\n\tOutstandingRequests context.Context\n\n\t// OutstandingPubSubMessages is canceled when the service is no longer processing any Pub/Sub messages.\n\tOutstandingPubSubMessages context.Context\n\n\t// ForceCloseTasks is canceled when the graceful shutdown deadline is reached and it's time to\n\t// forcibly close active tasks (outstanding incoming API requests and Pub/Sub subscription messages).\n\t//\n\t// When ForceCloseTasks is closed, the contexts for all outstanding tasks are canceled.\n\t//\n\t// It is canceled early if all active tasks are done.\n\tForceCloseTasks context.Context\n\n\t// OutstandingTasks is canceled when the service is no longer actively processing any tasks,\n\t// which includes both incoming API calls and Pub/Sub messages.\n\t//\n\t// It is canceled as soon as both OutstandingRequests and OutstandingPubSubMessages have been canceled.\n\tOutstandingTasks context.Context\n\n\t// ForceShutdown is closed when the graceful shutdown window has closed and it's time to\n\t// forcefully shut down.\n\t//\n\t// If the graceful shutdown window lapses before the cooperative shutdown is complete,\n\t// the ForceShutdown channel may be closed before RunningHandlers is canceled.\n\t//\n\t// It is canceled early if all running tasks have completed, all infrastructure resources are closed,\n\t// and all registered service Handler.Shutdown methods have returned.\n\tForceShutdown context.Context\n}\n\n// Handler is the interface for resources that participate in the graceful shutdown process.\ntype Handler interface {\n\t// Shutdown is called by Encore when the graceful shutdown process is initiated.\n\t//\n\t// The provided Progress struct provides information about the graceful shutdown progress,\n\t// which can be used to determine at what point in time it's appropriate to close certain resources.\n\t//\n\t// For example, a service struct may want to wait for all incoming requests to complete\n\t// before it closes its client to a third-party service:\n\t//\n\t// \t\t\tfunc (s *MyService) Shutdown(p *shutdown.Progress) error {\n\t//\t\t\t\t<-p.OutstandingRequests.Done()\n\t//\t\t\t\treturn s.client.Close()\n\t//\t\t\t}\n\t//\n\t// The shutdown process is cooperative (to the extent it is possible),\n\t// and Encore will wait for all Handlers to return before closing\n\t// infrastructure resources and exiting the process,\n\t// until the ForceShutdown deadline is reached.\n\t//\n\t// The return value of Shutdown is used to report shutdown errors only,\n\t// and has no effect on the shutdown process.\n\tShutdown(Progress) error\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/basic.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\n// NewStringKeyspace creates a keyspace that stores string values in the given cluster.\n//\n// The type parameter K specifies the key type, which can either be a\n// named struct type or a basic type (string, int, etc).\nfunc NewStringKeyspace[K any](cluster *Cluster, cfg KeyspaceConfig) *StringKeyspace[K] {\n\tfromRedis := func(val string) (string, error) { return val, nil }\n\ttoRedis := func(val string) (any, error) { return val, nil }\n\n\treturn &StringKeyspace[K]{\n\t\t&basicKeyspace[K, string]{\n\t\t\tnewClient[K, string](cluster, cfg, fromRedis, toRedis),\n\t\t},\n\t}\n}\n\n// StringKeyspace represents a set of cache keys that hold string values.\ntype StringKeyspace[K any] struct {\n\t*basicKeyspace[K, string]\n}\n\n// Get gets the value stored at key.\n// If the key does not exist, it returns an error matching Miss.\n//\n// See https://redis.io/commands/get/ for more information.\nfunc (s *StringKeyspace[K]) Get(ctx context.Context, key K) (string, error) {\n\treturn s.basicKeyspace.Get(ctx, key)\n}\n\n// MultiGet gets the values stored at multiple keys.\n// For each key, the result contains an Err field indicating success or failure.\n// If Err is nil, Value contains the cached value.\n// If Err matches Miss, the key was not found.\n//\n// See https://redis.io/commands/mget/ for more information.\nfunc (s *StringKeyspace[K]) MultiGet(ctx context.Context, keys ...K) ([]Result[string], error) {\n\treturn s.basicKeyspace.MultiGet(ctx, keys...)\n}\n\n// Set updates the value stored at key to val.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *StringKeyspace[K]) Set(ctx context.Context, key K, val string) error {\n\treturn s.basicKeyspace.Set(ctx, key, val)\n}\n\n// SetIfNotExists sets the value stored at key to val, but only if the key does not exist beforehand.\n// If the key already exists, it reports an error matching KeyExists.\n//\n// See https://redis.io/commands/setnx/ for more information.\nfunc (s *StringKeyspace[K]) SetIfNotExists(ctx context.Context, key K, val string) error {\n\treturn s.basicKeyspace.SetIfNotExists(ctx, key, val)\n}\n\n// Replace replaces the existing value stored at key to val.\n// If the key does not already exist, it reports an error matching Miss.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *StringKeyspace[K]) Replace(ctx context.Context, key K, val string) error {\n\treturn s.basicKeyspace.Replace(ctx, key, val)\n}\n\n// GetAndSet updates the value of key to val and returns the previously stored value.\n// If the key does not already exist, it sets it and returns \"\", nil.\n//\n// See https://redis.io/commands/getset/ for more information.\nfunc (s *StringKeyspace[K]) GetAndSet(ctx context.Context, key K, val string) (oldVal string, err error) {\n\treturn s.basicKeyspace.GetAndSet(ctx, key, val)\n}\n\n// GetAndDelete deletes the key and returns the previously stored value.\n// If the key does not already exist, it does nothing and returns \"\", nil.\n//\n// See https://redis.io/commands/getdel/ for more information.\nfunc (s *StringKeyspace[K]) GetAndDelete(ctx context.Context, key K) (oldVal string, err error) {\n\treturn s.basicKeyspace.GetAndDelete(ctx, key)\n}\n\n// Delete deletes the specified keys.\n//\n// If a key does not exist it is ignored.\n//\n// It reports the number of keys that were deleted.\n//\n// See https://redis.io/commands/del/ for more information.\nfunc (s *StringKeyspace[K]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\treturn s.client.Delete(ctx, keys...)\n}\n\n// With returns a reference to the same keyspace but with customized write options.\n// The primary use case is for overriding the expiration time for certain cache operations.\n//\n// It is intended to be used with method chaining:\n//\n//\tmyKeyspace.With(cache.ExpireIn(3 * time.Second)).Set(...)\nfunc (k *StringKeyspace[K]) With(opts ...WriteOption) *StringKeyspace[K] {\n\treturn &StringKeyspace[K]{k.with(opts)}\n}\n\n// Append appends to the string with the given key.\n//\n// If the key does not exist it is first created and set as the empty string,\n// causing Append to behave like Set.\n//\n// It returns the new string length.\n//\n// See https://redis.io/commands/append/ for more information.\nfunc (s *StringKeyspace[K]) Append(ctx context.Context, key K, val string) (newLen int64, err error) {\n\tconst op = \"append\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.Append(ctx, k, val)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// GetRange returns a substring of the string value stored in key.\n//\n// The from and to values are zero-based indices, but unlike Go slicing\n// the 'to' value is inclusive.\n//\n// Negative values can be used in order to provide an offset starting\n// from the end of the string, so -1 means the last character\n// and -len(str) the first character, and so forth.\n//\n// If the string does not exist it returns the empty string.\n//\n// See https://redis.io/commands/setrange/ for more information.\nfunc (s *StringKeyspace[K]) GetRange(ctx context.Context, key K, from, to int64) (val string, err error) {\n\tconst op = \"get range\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tres, err := s.client.redis.GetRange(ctx, k, from, to).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// SetRange overwrites part of the string stored at key, starting at\n// the zero-based offset and for the entire length of val, extending\n// the string if necessary to make room for val.\n//\n// If the offset is larger than the current string length stored at key,\n// the string is first padded with zero-bytes to make offset fit.\n//\n// Non-existing keys are considered as empty strings.\n//\n// See https://redis.io/commands/setrange/ for more information.\nfunc (s *StringKeyspace[K]) SetRange(ctx context.Context, key K, offset int64, val string) (newLen int64, err error) {\n\tconst op = \"set range\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do[K, string, *redis.IntCmd](s.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.SetRange(ctx, k, offset, val)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// Len reports the length of the string value stored at key.\n//\n// Non-existing keys are considered as empty strings.\n//\n// See https://redis.io/commands/strlen/ for more information.\nfunc (s *StringKeyspace[K]) Len(ctx context.Context, key K) (length int64, err error) {\n\tconst op = \"len\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := s.client.redis.StrLen(ctx, k).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// NewIntKeyspace creates a keyspace that stores int64 values in the given cluster.\n//\n// The type parameter K specifies the key type, which can either be a\n// named struct type or a basic type (string, int, etc).\nfunc NewIntKeyspace[K any](cluster *Cluster, cfg KeyspaceConfig) *IntKeyspace[K] {\n\tfromRedis := func(val string) (int64, error) { return strconv.ParseInt(val, 10, 64) }\n\ttoRedis := func(val int64) (any, error) { return val, nil }\n\n\treturn &IntKeyspace[K]{\n\t\t&basicKeyspace[K, int64]{\n\t\t\tnewClient[K, int64](cluster, cfg, fromRedis, toRedis),\n\t\t},\n\t}\n}\n\n// IntKeyspace is a cache keyspace that stores int64 values.\ntype IntKeyspace[K any] struct {\n\t*basicKeyspace[K, int64]\n}\n\n// With returns a reference to the same keyspace but with customized write options.\n// The primary use case is for overriding the expiration time for certain cache operations.\n//\n// It is intended to be used with method chaining:\n//\n//\tmyKeyspace.With(cache.ExpireIn(3 * time.Second)).Set(...)\nfunc (k *IntKeyspace[K]) With(opts ...WriteOption) *IntKeyspace[K] {\n\treturn &IntKeyspace[K]{k.basicKeyspace.with(opts)}\n}\n\n// Get gets the value stored at key.\n// If the key does not exist, it returns an error matching Miss.\n//\n// See https://redis.io/commands/get/ for more information.\nfunc (s *IntKeyspace[K]) Get(ctx context.Context, key K) (int64, error) {\n\treturn s.basicKeyspace.Get(ctx, key)\n}\n\n// MultiGet gets the values stored at multiple keys.\n// For each key, the result contains an Err field indicating success or failure.\n// If Err is nil, Value contains the cached value.\n// If Err matches Miss, the key was not found.\n//\n// See https://redis.io/commands/mget/ for more information.\nfunc (s *IntKeyspace[K]) MultiGet(ctx context.Context, keys ...K) ([]Result[int64], error) {\n\treturn s.basicKeyspace.MultiGet(ctx, keys...)\n}\n\n// Set updates the value stored at key to val.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *IntKeyspace[K]) Set(ctx context.Context, key K, val int64) error {\n\treturn s.basicKeyspace.Set(ctx, key, val)\n}\n\n// SetIfNotExists sets the value stored at key to val, but only if the key does not exist beforehand.\n// If the key already exists, it reports an error matching KeyExists.\n//\n// See https://redis.io/commands/setnx/ for more information.\nfunc (s *IntKeyspace[K]) SetIfNotExists(ctx context.Context, key K, val int64) error {\n\treturn s.basicKeyspace.SetIfNotExists(ctx, key, val)\n}\n\n// Replace replaces the existing value stored at key to val.\n// If the key does not already exist, it reports an error matching Miss.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *IntKeyspace[K]) Replace(ctx context.Context, key K, val int64) error {\n\treturn s.basicKeyspace.Replace(ctx, key, val)\n}\n\n// GetAndSet updates the value of key to val and returns the previously stored value.\n// If the key does not already exist, it sets it and returns 0, nil.\n//\n// See https://redis.io/commands/getset/ for more information.\nfunc (s *IntKeyspace[K]) GetAndSet(ctx context.Context, key K, val int64) (oldVal int64, err error) {\n\treturn s.basicKeyspace.GetAndSet(ctx, key, val)\n}\n\n// GetAndDelete deletes the key and returns the previously stored value.\n// If the key does not already exist, it does nothing and returns 0, nil.\n//\n// See https://redis.io/commands/getdel/ for more information.\nfunc (s *IntKeyspace[K]) GetAndDelete(ctx context.Context, key K) (oldVal int64, err error) {\n\treturn s.basicKeyspace.GetAndDelete(ctx, key)\n}\n\n// Delete deletes the specified keys.\n//\n// If a key does not exist it is ignored.\n//\n// It reports the number of keys that were deleted.\n//\n// See https://redis.io/commands/del/ for more information.\nfunc (s *IntKeyspace[K]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\treturn s.client.Delete(ctx, keys...)\n}\n\n// Increment increments the number stored in key by delta,\n// and returns the new value.\n//\n// If the key does not exist it is first created with a value of 0\n// before incrementing.\n//\n// Negative values can be used to decrease the value,\n// but typically you want to use the Decrement method for that.\n//\n// See https://redis.io/commands/incrby/ for more information.\nfunc (s *IntKeyspace[K]) Increment(ctx context.Context, key K, delta int64) (newVal int64, err error) {\n\tconst op = \"increment\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.IncrBy(ctx, k, delta)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// Decrement decrements the number stored in key by delta,\n// and returns the new value.\n//\n// If the key does not exist it is first created with a value of 0\n// before decrementing.\n//\n// Negative values can be used to increase the value,\n// but typically you want to use the Increment method for that.\n//\n// See https://redis.io/commands/decrby/ for more information.\nfunc (s *IntKeyspace[K]) Decrement(ctx context.Context, key K, delta int64) (newVal int64, err error) {\n\tconst op = \"decrement\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.DecrBy(ctx, k, delta)\n\t}).Result()\n\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// NewFloatKeyspace creates a keyspace that stores float64 values in the given cluster.\n//\n// The type parameter K specifies the key type, which can either be a\n// named struct type or a basic type (string, int, etc).\nfunc NewFloatKeyspace[K any](cluster *Cluster, cfg KeyspaceConfig) *FloatKeyspace[K] {\n\tfromRedis := func(val string) (float64, error) { return strconv.ParseFloat(val, 64) }\n\ttoRedis := func(val float64) (any, error) { return val, nil }\n\n\treturn &FloatKeyspace[K]{\n\t\t&basicKeyspace[K, float64]{\n\t\t\tnewClient[K, float64](cluster, cfg, fromRedis, toRedis),\n\t\t},\n\t}\n}\n\n// FloatKeyspace is a cache keyspace that stores float64 values.\ntype FloatKeyspace[K any] struct {\n\t*basicKeyspace[K, float64]\n}\n\n// With returns a reference to the same keyspace but with customized write options.\n// The primary use case is for overriding the expiration time for certain cache operations.\n//\n// It is intended to be used with method chaining:\n//\n//\tmyKeyspace.With(cache.ExpireIn(3 * time.Second)).Set(...)\nfunc (k *FloatKeyspace[K]) With(opts ...WriteOption) *FloatKeyspace[K] {\n\treturn &FloatKeyspace[K]{k.basicKeyspace.with(opts)}\n}\n\n// Get gets the value stored at key.\n// If the key does not exist, it returns an error matching Miss.\n//\n// See https://redis.io/commands/get/ for more information.\nfunc (s *FloatKeyspace[K]) Get(ctx context.Context, key K) (float64, error) {\n\treturn s.basicKeyspace.Get(ctx, key)\n}\n\n// MultiGet gets the values stored at multiple keys.\n// For each key, the result contains an Err field indicating success or failure.\n// If Err is nil, Value contains the cached value.\n// If Err matches Miss, the key was not found.\n//\n// See https://redis.io/commands/mget/ for more information.\nfunc (s *FloatKeyspace[K]) MultiGet(ctx context.Context, keys ...K) ([]Result[float64], error) {\n\treturn s.basicKeyspace.MultiGet(ctx, keys...)\n}\n\n// Set updates the value stored at key to val.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *FloatKeyspace[K]) Set(ctx context.Context, key K, val float64) error {\n\treturn s.basicKeyspace.Set(ctx, key, val)\n}\n\n// SetIfNotExists sets the value stored at key to val, but only if the key does not exist beforehand.\n// If the key already exists, it reports an error matching KeyExists.\n//\n// See https://redis.io/commands/setnx/ for more information.\nfunc (s *FloatKeyspace[K]) SetIfNotExists(ctx context.Context, key K, val float64) error {\n\treturn s.basicKeyspace.SetIfNotExists(ctx, key, val)\n}\n\n// Replace replaces the existing value stored at key to val.\n// If the key does not already exist, it reports an error matching Miss.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *FloatKeyspace[K]) Replace(ctx context.Context, key K, val float64) error {\n\treturn s.basicKeyspace.Replace(ctx, key, val)\n}\n\n// GetAndSet updates the value of key to val and returns the previously stored value.\n// If the key does not already exist, it sets it and returns 0, nil.\n//\n// See https://redis.io/commands/getset/ for more information.\nfunc (s *FloatKeyspace[K]) GetAndSet(ctx context.Context, key K, val float64) (oldVal float64, err error) {\n\treturn s.basicKeyspace.GetAndSet(ctx, key, val)\n}\n\n// GetAndDelete deletes the key and returns the previously stored value.\n// If the key does not already exist, it does nothing and returns 0, nil.\n//\n// See https://redis.io/commands/getdel/ for more information.\nfunc (s *FloatKeyspace[K]) GetAndDelete(ctx context.Context, key K) (oldVal float64, err error) {\n\treturn s.basicKeyspace.GetAndDelete(ctx, key)\n}\n\n// Delete deletes the specified keys.\n//\n// If a key does not exist it is ignored.\n//\n// It reports the number of keys that were deleted.\n//\n// See https://redis.io/commands/del/ for more information.\nfunc (s *FloatKeyspace[K]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\treturn s.client.Delete(ctx, keys...)\n}\n\n// Increment increments the number stored in key by delta,\n// and returns the new value.\n//\n// If the key does not exist it is first created with a value of 0\n// before incrementing.\n//\n// Negative values can be used to decrease the value,\n// but typically you want to use the Decrement method for that.\n//\n// See https://redis.io/commands/incrbyfloat/ for more information.\nfunc (s *FloatKeyspace[K]) Increment(ctx context.Context, key K, delta float64) (newVal float64, err error) {\n\tconst op = \"increment\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do[K, float64, *redis.FloatCmd](s.client, ctx, k, func(c cmdable) *redis.FloatCmd {\n\t\treturn c.IncrByFloat(ctx, k, delta)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// Decrement decrements the number stored in key by delta,\n// and returns the new value.\n//\n// If the key does not exist it is first created with a value of 0\n// before decrementing.\n//\n// Negative values can be used to increase the value,\n// but typically you want to use the Increment method for that.\n//\n// See https://redis.io/commands/incrbyfloat/ for more information.\nfunc (s *FloatKeyspace[K]) Decrement(ctx context.Context, key K, delta float64) (newVal float64, err error) {\n\tconst op = \"decrement\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do[K, float64, *redis.FloatCmd](s.client, ctx, k, func(c cmdable) *redis.FloatCmd {\n\t\treturn c.IncrByFloat(ctx, k, -delta)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\ntype basicKeyspace[K, V any] struct {\n\t*client[K, V]\n}\n\nfunc (s *basicKeyspace[K, V]) with(opts []WriteOption) *basicKeyspace[K, V] {\n\treturn &basicKeyspace[K, V]{s.client.with(opts)}\n}\n\nfunc (s *basicKeyspace[K, V]) Get(ctx context.Context, key K) (val V, err error) {\n\tconst op = \"get\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\n\tres, err := s.redis.Get(ctx, k).Result()\n\tif err == nil {\n\t\tval, err = s.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\nfunc (s *basicKeyspace[K, V]) MultiGet(ctx context.Context, keys ...K) ([]Result[V], error) {\n\tconst op = \"multi get\"\n\tks, err := s.keys(keys, op)\n\tendTrace := s.doTrace(op, false, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar firstKey string\n\tif len(ks) > 0 {\n\t\tfirstKey = ks[0]\n\t}\n\tres, err := s.redis.MGet(ctx, ks...).Result()\n\tif err != nil {\n\t\treturn nil, toErr(err, op, firstKey)\n\t}\n\tresults := make([]Result[V], 0, len(res))\n\tfor i, r := range res {\n\t\tif r == nil {\n\t\t\tresults = append(results, Result[V]{Err: toErr(Miss, op, ks[i])})\n\t\t\tcontinue\n\t\t}\n\t\tstrVal, ok := r.(string)\n\t\tif !ok {\n\t\t\tresults = append(results, Result[V]{Err: toErr(errors.New(\"invalid redis value type\"), op, ks[i])})\n\t\t\tcontinue\n\t\t}\n\t\tval, fromRedisErr := s.fromRedis(strVal)\n\t\tif fromRedisErr != nil {\n\t\t\tresults = append(results, Result[V]{Err: toErr(fromRedisErr, op, ks[i])})\n\t\t\tcontinue\n\t\t}\n\t\tresults = append(results, Result[V]{Value: val})\n\t}\n\treturn results, nil\n}\n\nfunc (s *basicKeyspace[K, V]) Set(ctx context.Context, key K, val V) error {\n\t_, _, err := s.set(ctx, key, val, 0, \"set\")\n\treturn err\n}\n\nfunc (s *basicKeyspace[K, V]) SetIfNotExists(ctx context.Context, key K, val V) error {\n\tconst op = \"set if not exists\"\n\t_, _, err := s.set(ctx, key, val, setNX, op)\n\treturn err\n}\n\nfunc (s *basicKeyspace[K, V]) Replace(ctx context.Context, key K, val V) error {\n\t_, _, err := s.set(ctx, key, val, setXX, \"replace\")\n\treturn err\n}\n\nfunc (s *basicKeyspace[K, V]) GetAndSet(ctx context.Context, key K, val V) (prev V, err error) {\n\tconst op = \"get and set\"\n\tres, k, err := s.set(ctx, key, val, setGet, op)\n\tif err == nil {\n\t\tval, err = s.fromRedis(res)\n\t\terr = toErr(err, op, k)\n\t}\n\treturn val, err\n}\n\nfunc (s *basicKeyspace[K, V]) GetAndDelete(ctx context.Context, key K) (val V, err error) {\n\tconst op = \"get and delete\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\n\t// When deleting we don't need to deal with expiry\n\tres, err := s.redis.GetDel(ctx, k).Result()\n\tif err == nil {\n\t\tval, err = s.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\nfunc (s *client[K, V]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\tconst op = \"delete\"\n\tks, err := s.keys(keys, op)\n\tendTrace := s.doTrace(op, true, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar firstKey string\n\tif len(ks) > 0 {\n\t\tfirstKey = ks[0]\n\t}\n\n\t// When deleting we don't need to deal with expiry\n\tres, err := s.redis.Del(ctx, ks...).Result()\n\terr = toErr(err, op, firstKey)\n\treturn int(res), err\n}\n\ntype setFlag uint8\n\nconst (\n\tsetGet setFlag = 1 << iota\n\tsetNX\n\tsetXX\n)\n\nfunc (s *basicKeyspace[K, V]) set(ctx context.Context, key K, val V, flag setFlag, op string) (strVal, k string, err error) {\n\tk, err = s.key(key, op)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\n\tget := (flag & setGet) == setGet\n\tnx := (flag & setNX) == setNX\n\txx := (flag & setXX) == setXX\n\n\tif nx {\n\t\t// If this is a setNX, convert Miss to KeyExists.\n\t\tdefer func() {\n\t\t\tif errors.Is(err, Miss) {\n\t\t\t\terr = toErr(KeyExists, op, k)\n\t\t\t}\n\t\t}()\n\t}\n\n\tredisVal, err := s.toRedis(val)\n\tif err != nil {\n\t\treturn \"\", k, toErr(err, op, k)\n\t}\n\n\targs := make([]any, 3, 7)\n\targs[0] = \"set\"\n\targs[1] = k\n\targs[2] = redisVal\n\tif nx {\n\t\targs = append(args, \"nx\")\n\t} else if xx {\n\t\targs = append(args, \"xx\")\n\t}\n\tif get {\n\t\targs = append(args, \"get\")\n\t}\n\n\tnow := time.Now()\n\texp := s.expiry(now)\n\tswitch exp {\n\tcase neverExpire:\n\t\t// do nothing; default Redis behavior\n\tcase keepTTL:\n\t\targs = append(args, \"keepttl\")\n\tdefault:\n\t\tdur := exp.Sub(now)\n\t\tif dur < 0 {\n\t\t\t// The expiry is in the past; use a very old unix timestamp to\n\t\t\t// delete the key immediately. Note that we can't use timestamp 0\n\t\t\t// or else [Mini]redis complains.\n\t\t\targs = append(args, \"exat\", 1)\n\t\t} else {\n\t\t\tif usePreciseDur(dur) {\n\t\t\t\targs = append(args, \"px\", int64(dur/time.Millisecond))\n\t\t\t} else {\n\t\t\t\targs = append(args, \"ex\", int64(dur/time.Second))\n\t\t\t}\n\t\t}\n\t}\n\n\tif get {\n\t\tcmd := redis.NewStringCmd(ctx, args...)\n\t\t_ = s.redis.Process(ctx, cmd)\n\t\tres, err := cmd.Result()\n\t\terr = toErr(err, op, k)\n\t\treturn res, k, err\n\t}\n\n\tcmd := redis.NewStatusCmd(ctx, args...)\n\t_ = s.redis.Process(ctx, cmd)\n\treturn \"\", k, toErr(cmd.Err(), op, k)\n}\n\nfunc usePreciseDur(dur time.Duration) bool {\n\treturn dur < time.Second || dur%time.Second != 0\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/basic_test.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n)\n\nfunc TestBasicKeyspace(t *testing.T) {\n\tkt := newStringTest(t)\n\tks, ctx := kt.ks, kt.ctx\n\n\tkt.Set(\"one\", \"alpha\")\n\tkt.Val(\"one\", \"alpha\")\n\n\tcheck(ks.With(ExpireIn(time.Second)).Set(ctx, \"one\", \"beta\"))\n\tkt.Val(\"one\", \"beta\")\n\tkt.TTL(\"one\", time.Second)\n\n\tcheck(ks.With(ExpireIn(-time.Second)).Set(ctx, \"one\", \"charlie\"))\n\tkt.Missing(\"one\")\n\n\tkt.Set(\"one\", \"delta\")\n\tkt.Val(\"one\", \"delta\")\n\tmust(ks.Delete(ctx, \"one\"))\n\tkt.Missing(\"one\")\n\n\t// Replace should fail if the key is missing\n\tif err := ks.Replace(ctx, \"one\", \"test\"); !errors.Is(err, Miss) {\n\t\tt.Errorf(\"replace: unexpected error: %v\", err)\n\t}\n\tcheck(ks.SetIfNotExists(ctx, \"one\", \"added\"))\n\n\t// Add should fail if the key is already present.\n\tif err := ks.SetIfNotExists(ctx, \"one\", \"added twice\"); !errors.Is(err, KeyExists) {\n\t\tt.Errorf(\"add: unexpected error: %v\", err)\n\t}\n\tkt.Val(\"one\", \"added\")\n\n\tcheck(ks.Replace(ctx, \"one\", \"replaced\"))\n\tkt.Val(\"one\", \"replaced\")\n\n\told := must(ks.GetAndSet(ctx, \"one\", \"updated\"))\n\tif old != \"replaced\" {\n\t\tt.Errorf(\"get and set: want old value %q, got %v\", \"replaced\", old)\n\t}\n\n\told = must(ks.GetAndDelete(ctx, \"one\"))\n\tif old != \"updated\" {\n\t\tt.Errorf(\"get and delete: want old value %q, got %v\", \"updated\", old)\n\t}\n\tkt.Missing(\"one\")\n}\n\nfunc TestStringKeyspace(t *testing.T) {\n\tkt := newStringTest(t)\n\tks, ctx := kt.ks, kt.ctx\n\n\tkt.Set(\"one\", \"alpha\")\n\n\tnewLen := must(ks.Append(ctx, \"one\", \" bravo\"))\n\twant := \"alpha bravo\"\n\tkt.Val(\"one\", want)\n\tif newLen != int64(len(want)) {\n\t\tt.Errorf(\"append: got resulting length %d, want %d\", newLen, len(want))\n\t}\n\n\tnewLen = must(ks.SetRange(ctx, \"one\", 6, \"ch\"))\n\twant = \"alpha chavo\"\n\tkt.Val(\"one\", want)\n\tif newLen != int64(len(want)) {\n\t\tt.Errorf(\"setrange: got resulting length %d, want %d\", newLen, len(want))\n\t}\n\n\tnewLen = must(ks.SetRange(ctx, \"one\", 13, \"pad\"))\n\twant = \"alpha chavo\\x00\\x00pad\"\n\tkt.Val(\"one\", want)\n\tif newLen != int64(len(want)) {\n\t\tt.Errorf(\"setrange: got resulting length %d, want %d\", newLen, len(want))\n\t}\n\n\tgot := must(ks.GetRange(ctx, \"one\", 2, 4))\n\tif want := \"pha\"; got != want {\n\t\tt.Errorf(\"getrange: got %q, want %q\", got, want)\n\t}\n\n\tgotLen := int(must(ks.Len(ctx, \"one\")))\n\tif want := len(must(ks.Get(ctx, \"one\"))); gotLen != want {\n\t\tt.Errorf(\"len: got %v, want %v\", got, want)\n\t}\n}\n\nfunc TestIntKeyspace(t *testing.T) {\n\tks := newIntTest(t)\n\tctx := context.Background()\n\n\tcheck(ks.Set(ctx, \"one\", 1))\n\tif got, want := must(ks.Get(ctx, \"one\")), int64(1); got != want {\n\t\tt.Errorf(\"set/get: got %v, want %v\", got, want)\n\t}\n\n\tif got, want := must(ks.Increment(ctx, \"one\", 3)), int64(4); got != want {\n\t\tt.Errorf(\"incr: got %v, want %v\", got, want)\n\t}\n\n\tif got, want := must(ks.Decrement(ctx, \"one\", 1)), int64(3); got != want {\n\t\tt.Errorf(\"decr: got %v, want %v\", got, want)\n\t}\n}\n\nfunc TestFloatKeyspace(t *testing.T) {\n\tks := newFloatTest(t)\n\tctx := context.Background()\n\n\t// We may need to change these to approximate comparisons, but it's\n\t// at least passing for me right now.\n\tcheck(ks.Set(ctx, \"one\", 1))\n\tif got, want := must(ks.Get(ctx, \"one\")), float64(1); got != want {\n\t\tt.Errorf(\"set/get: got %v, want %v\", got, want)\n\t}\n\n\tif got, want := must(ks.Increment(ctx, \"one\", 3)), float64(4); got != want {\n\t\tt.Errorf(\"incr: got %v, want %v\", got, want)\n\t}\n\n\tif got, want := must(ks.Decrement(ctx, \"one\", 1)), float64(3); got != want {\n\t\tt.Errorf(\"decr: got %v, want %v\", got, want)\n\t}\n}\n\nfunc TestMultiGet(t *testing.T) {\n\tkt := newStringTest(t)\n\tks, ctx := kt.ks, kt.ctx\n\n\t// Set up test data\n\tkt.Set(\"key1\", \"value1\")\n\tkt.Set(\"key2\", \"value2\")\n\n\tresults := must(ks.MultiGet(ctx, \"key1\", \"key2\", \"missing\"))\n\tif len(results) != 3 {\n\t\tt.Fatalf(\"expected 3 results, got %d\", len(results))\n\t}\n\n\tif results[0].Err != nil || results[0].Value != \"value1\" {\n\t\tt.Errorf(\"key1: got Err=%v, Value=%q, want Err=nil, Value=%q\", results[0].Err, results[0].Value, \"value1\")\n\t}\n\tif results[1].Err != nil || results[1].Value != \"value2\" {\n\t\tt.Errorf(\"key2: got Err=%v, Value=%q, want Err=nil, Value=%q\", results[1].Err, results[1].Value, \"value2\")\n\t}\n\tif !errors.Is(results[2].Err, Miss) {\n\t\tt.Errorf(\"missing: got Err=%v, want Miss\", results[2].Err)\n\t}\n}\n\nfunc newStringTest(t *testing.T) *stringTester {\n\tcluster, srv := newTestCluster(t)\n\tks := NewStringKeyspace[string](cluster, KeyspaceConfig{\n\t\tEncoreInternal_KeyMapper: func(s string) string { return s },\n\t})\n\n\tctx := context.Background()\n\treturn &stringTester{t: t, ctx: ctx, ks: ks, srv: srv}\n}\n\nfunc newIntTest(t *testing.T) *IntKeyspace[string] {\n\tcluster, _ := newTestCluster(t)\n\tks := NewIntKeyspace[string](cluster, KeyspaceConfig{\n\t\tEncoreInternal_KeyMapper: func(s string) string { return s },\n\t})\n\treturn ks\n}\n\nfunc newFloatTest(t *testing.T) *FloatKeyspace[string] {\n\tcluster, _ := newTestCluster(t)\n\tks := NewFloatKeyspace[string](cluster, KeyspaceConfig{\n\t\tEncoreInternal_KeyMapper: func(s string) string { return s },\n\t})\n\treturn ks\n}\n\ntype stringTester struct {\n\tt   *testing.T\n\tctx context.Context\n\tks  *StringKeyspace[string]\n\tsrv *miniredis.Miniredis\n}\n\nfunc (t *stringTester) Set(key, val string) {\n\tt.t.Helper()\n\tif err := t.ks.Set(t.ctx, key, val); err != nil {\n\t\tt.t.Errorf(\"Set(%q, %q) = %v, want nil\", key, val, err)\n\t}\n}\n\nfunc (t *stringTester) Val(key, want string) {\n\tt.t.Helper()\n\tif got, err := t.ks.Get(t.ctx, key); err != nil {\n\t\tif err == Miss {\n\t\t\tt.t.Errorf(\"key %s: key not in cache\", key)\n\t\t} else {\n\t\t\tt.t.Errorf(\"key %s: got err %v, want nil\", key, err)\n\t\t}\n\t} else if got != want {\n\t\tt.t.Errorf(\"key %s: got value %q, want %q\", key, got, want)\n\t}\n}\n\nfunc (t *stringTester) TTL(key string, want time.Duration) {\n\tt.t.Helper()\n\tif !t.srv.Exists(key) {\n\t\tt.t.Errorf(\"key %s: key not in cache\", key)\n\t}\n\tgot := t.srv.TTL(key)\n\tif got != want {\n\t\tt.t.Errorf(\"key %s: got ttl %v, want %v\", key, got, want)\n\t}\n}\n\nfunc (t *stringTester) Missing(key string) {\n\tt.t.Helper()\n\tif _, err := t.ks.Get(t.ctx, key); err == nil {\n\t\tt.t.Errorf(\"key %s: key still in cache\", key)\n\t} else if !errors.Is(err, Miss) {\n\t\tt.t.Errorf(\"key %s: got err %v, want Miss\", key, err)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/cache.go",
    "content": "// Package cache provides the ability to define distributed Redis cache clusters\n// and functionality to use them in a fully type-safe manner.\n//\n// For more information see https://encore.dev/docs/develop/caching\npackage cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\n// ClusterConfig represents the configuration of cache clusters.\ntype ClusterConfig struct {\n\t// EvictionPolicy decides how the cache evicts existing keys\n\t// to make room for new data when the max memory limit is reached.\n\t//\n\t// If not specified the cache defaults to AllKeysLRU.\n\tEvictionPolicy EvictionPolicy\n}\n\n// An EvictionPolicy describes how the cache evicts keys to make room for new data\n// when the maximum memory limit is reached.\n//\n// See https://redis.io/docs/manual/eviction/#eviction-policies for more information.\ntype EvictionPolicy string\n\n// NOTE: These values need to be added to the runtimeconstants package\n// and the parser package for the parser to be aware of them.\n\n// The eviction policies Encore supports.\n// See https://redis.io/docs/manual/eviction/#eviction-policies for more information.\nconst (\n\t// AllKeysLRU keeps most recently used keys and removes least recently used (LRU) keys.\n\t// This is a good default choice for most cache use cases if you're not sure.\n\tAllKeysLRU EvictionPolicy = \"allkeys-lru\"\n\n\t// AllKeysLFU keeps frequently used keys and removes least frequently used (LFU) keys.\n\tAllKeysLFU EvictionPolicy = \"allkeys-lfu\"\n\n\t// AllKeysRandom randomly removes keys as needed.\n\tAllKeysRandom EvictionPolicy = \"allkeys-random\"\n\n\t// VolatileLRU removes least recently used keys with an expiration set.\n\t// It behaves like NoEviction if there are no keys to evict with an expiration set.\n\tVolatileLRU EvictionPolicy = \"volatile-lru\"\n\n\t// VolatileLFU removes least frequently used keys with an expiration set.\n\t// It behaves like NoEviction if there are no keys to evict with an expiration set.\n\tVolatileLFU EvictionPolicy = \"volatile-lfu\"\n\n\t// VolatileTTL removes keys with an expiration set and the shortest time to live (TTL).\n\t// It behaves like NoEviction if there are no keys to evict with an expiration set.\n\tVolatileTTL EvictionPolicy = \"volatile-ttl\"\n\n\t// VolatileRandom randomly removes keys with an expiration set.\n\t// It behaves like NoEviction if there are no keys to evict with an expiration set.\n\tVolatileRandom EvictionPolicy = \"volatile-random\"\n\n\t// NoEviction does not evict any keys, and instead returns an error to the client\n\t// when the max memory limit is reached.\n\tNoEviction EvictionPolicy = \"noeviction\"\n)\n\n//publicapigen:keep\ntype constStr string\n\n// Cluster represents a Redis cache cluster.\ntype Cluster struct {\n\tcfg ClusterConfig\n\tmgr *Manager\n\tcl  *redis.Client\n}\n\n// KeyspaceConfig specifies the configuration options for a cache keyspace.\ntype KeyspaceConfig struct {\n\t// KeyPattern is a string literal representing the\n\t// cache key pattern for this keyspace.\n\tKeyPattern constStr\n\n\t// DefaultExpiry specifies the default key expiry for cache items\n\t// in this keyspace.\n\t//\n\t// When set, all write operations set (for new keys)\n\t// or update (for existing keys) the expiration time.\n\t//\n\t// When updating the expiration time Encore always\n\t// performs the combined operation atomically.\n\t//\n\t// If nil, cache items have no expiry date by default.\n\t//\n\t// The default behavior can be overridden by passing in\n\t// an ExpiryFunc or KeepTTL as a WriteOption to a specific operation.\n\tDefaultExpiry ExpiryFunc\n\n\t// EncoreInternal_DefLoc specifies where the keyspace is defined.\n\t// It's an internal field set by Encore's compiler.\n\t//publicapigen:drop\n\tEncoreInternal_DefLoc uint32\n\n\t// EncoreInternal_KeyMapper specifies how typed keys are translated\n\t// to a string. It's of type any to avoid making KeyspaceConfig\n\t// a generic type. It's an internal field set by Encore's compiler.\n\t//\n\t// The type must be func(K) string.\n\t//publicapigen:drop\n\tEncoreInternal_KeyMapper any\n}\n\n// An OpError describes the operation that failed.\ntype OpError struct {\n\tOperation string\n\tRawKey    string\n\tErr       error\n}\n\nfunc (e *OpError) Error() string {\n\treturn fmt.Sprintf(\"cache: %s %q: %v\", e.Operation, e.RawKey, e.Err)\n}\n\nfunc (e *OpError) Unwrap() error {\n\treturn e.Err\n}\n\n// Miss is the error value reported when a key is missing from the cache.\n// It must be checked against with errors.Is.\nvar Miss = errors.New(\"cache miss\")\n\n// KeyExists is the error reported when a key already exists\n// and the requested operation is specified to only apply to\n// keys that do not already exist.\n// It must be checked against with errors.Is.\nvar KeyExists = errors.New(\"key already exists\")\n\n// Result represents the result of a cache operation that may or may not have found a value.\n// If Err is nil, Value contains the cached value.\n// If Err matches Miss, the key was not found in the cache.\n// Otherwise Err contains the error that occurred.\ntype Result[V any] struct {\n\t// Value holds the cached value if Err is nil, otherwise the zero value.\n\tValue V\n\t// Err is nil on success, Miss if the key was not found, or another error.\n\tErr error\n}\n\n// An WriteOption customizes the behavior of a single cache write operation.\ntype WriteOption interface {\n\t//publicapigen:keep\n\twriteOption() // ensure only our package can implement\n}\n\ntype expiryOption interface {\n\tWriteOption\n\texpiry(now time.Time) time.Time\n}\n\n// ExpiryFunc is a function that reports when a key should expire\n// given the current time. It can be used as a WriteOption to customize\n// the expiration for that particular operation.\ntype ExpiryFunc func(now time.Time) time.Time\n\n// option implements WriteOption.\n//\n//publicapigen:keep\nfunc (ExpiryFunc) writeOption() {}\n\n// expiry implements expiryOption.\nfunc (fn ExpiryFunc) expiry(now time.Time) time.Time {\n\treturn fn(now)\n}\n\nvar _ expiryOption = (ExpiryFunc)(nil)\n\n// ExpireIn returns an ExpiryFunc that expires keys after a constant duration.\nfunc ExpireIn(dur time.Duration) ExpiryFunc {\n\treturn func(now time.Time) time.Time { return now.Add(dur) }\n}\n\n// ExpireDailyAt returns an ExpiryFunc that expires keys daily at the given time of day in loc.\n// ExpireDailyAt panics if loc is nil.\nfunc ExpireDailyAt(hour, minute, second int, loc *time.Location) ExpiryFunc {\n\treturn func(now time.Time) time.Time {\n\t\tyear, month, day := now.Date()\n\t\tnext := time.Date(year, month, day, hour, minute, second, 0, loc)\n\t\t// If next has already passed today, move it to tomorrow.\n\t\tif next.Before(now) {\n\t\t\tnext = next.AddDate(0, 0, 1)\n\t\t}\n\t\treturn next\n\t}\n}\n\n// expiryTime is a type for time constants that are also WriteOptions.\n//\n//publicapigen:keep\ntype expiryTime time.Time\n\n//publicapigen:keep\nfunc (expiryTime) writeOption() {}\n\nfunc (et expiryTime) expiry(_ time.Time) time.Time {\n\treturn time.Time(et)\n}\n\nvar _ expiryOption = (expiryTime)(time.Time{})\n\nvar (\n\t// NeverExpire is a WriteOption indicating the key should never expire.\n\tNeverExpire = expiryTime(neverExpire)\n\n\t// KeepTTL is a WriteOption indicating the key's TTL should be kept the same.\n\tKeepTTL = expiryTime(keepTTL)\n)\n\n//publicapigen:keep\nvar (\n\tneverExpire = time.Unix(0, 1)\n\tkeepTTL     = time.Unix(0, 2)\n)\n\nfunc fnMap[A, B any](src []A, fn func(A) B) []B {\n\tdst := make([]B, len(src))\n\tfor i, v := range src {\n\t\tdst[i] = fn(v)\n\t}\n\treturn dst\n}\n\n// do executes a Redis command, by invoking fn.\n//\n// Depending on the keyspace's expiration logic it either calls fn with\n// a plain Redis client or with a transactional pipeline, in order to ensure\n// the command executes atomically with the expiration update.\nfunc do[K, V, Res any](cl *client[K, V], ctx context.Context, key string, fn func(cmdable) Res) Res {\n\texp := cl.expiryCmd(ctx, key)\n\tif exp == nil {\n\t\treturn fn(cl.redis)\n\t}\n\n\tpipe := cl.redis.TxPipeline()\n\tres := fn(pipe)\n\t_ = pipe.Process(ctx, exp)\n\t_, _ = pipe.Exec(ctx)\n\treturn res\n}\n\n// do2 is like do, except it operates on two keys instead of one.\nfunc do2[K, V, Res any](\n\tcl *client[K, V],\n\tctx context.Context,\n\tkeyA, keyB string,\n\tfn func(cmdable) Res,\n) Res {\n\texpA := cl.expiryCmd(ctx, keyA)\n\texpB := cl.expiryCmd(ctx, keyB)\n\n\t// If we don't have any expiry commands, process the command directly.\n\tif expA == nil && expB == nil {\n\t\treturn fn(cl.redis)\n\t}\n\n\t// Otherwise use a pipeline.\n\tpipe := cl.redis.TxPipeline()\n\tres := fn(pipe)\n\tif expA != nil {\n\t\t_ = pipe.Process(ctx, expA)\n\t}\n\tif expB != nil {\n\t\t_ = pipe.Process(ctx, expB)\n\t}\n\t_, _ = pipe.Exec(ctx)\n\n\treturn res\n}\n\ntype cmdable interface {\n\tredis.Cmdable\n\tProcess(context.Context, redis.Cmder) error\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/cache_test.go",
    "content": "package cache\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n\t\"github.com/go-redis/redis/v8\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n)\n\nfunc newTestCluster(t *testing.T) (*Cluster, *miniredis.Miniredis) {\n\tsrv := miniredis.RunT(t)\n\tredisClient := redis.NewClient(&redis.Options{Addr: srv.Addr()})\n\n\trt := reqtrack.New(zerolog.New(os.Stdout), nil, nil)\n\tmgr := &Manager{\n\t\tstatic: &config.Static{\n\t\t\t// We're testing the \"production mode\" of the cache, not the test mode.\n\t\t\tTesting: false,\n\t\t},\n\t\trt: rt,\n\t}\n\tcluster := &Cluster{\n\t\tmgr: mgr,\n\t\tcl:  redisClient,\n\t}\n\treturn cluster, srv\n}\n\nfunc must[T any](val T, err error) T {\n\tcheck(err)\n\treturn val\n}\n\nfunc check(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/error_internal.go",
    "content": "package cache\n\nimport (\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/apisdk/api/errmarshalling\"\n)\n\n// Register an internal error marshaller for the cache.OpError\nfunc init() {\n\terrmarshalling.RegisterErrorMarshaller(\n\t\tfunc(err *OpError, stream *jsoniter.Stream) {\n\t\t\tstream.WriteObjectField(\"op\")\n\t\t\tstream.WriteString(err.Operation)\n\n\t\t\tstream.WriteMore()\n\t\t\tstream.WriteObjectField(\"raw_key\")\n\t\t\tstream.WriteString(err.RawKey)\n\n\t\t\tif err.Err != nil {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(errmarshalling.WrappedKey)\n\t\t\t\tstream.WriteVal(err.Err)\n\t\t\t}\n\t\t},\n\t\tfunc(err *OpError, iter *jsoniter.Iterator) {\n\t\t\titer.ReadObjectCB(func(iter *jsoniter.Iterator, field string) bool {\n\t\t\t\tswitch field {\n\t\t\t\tcase \"op\":\n\t\t\t\t\terr.Operation = iter.ReadString()\n\t\t\t\tcase \"raw_key\":\n\t\t\t\t\terr.RawKey = iter.ReadString()\n\t\t\t\tcase errmarshalling.WrappedKey:\n\t\t\t\t\terr.Err = errmarshalling.UnmarshalError(iter)\n\t\t\t\tdefault:\n\t\t\t\t\titer.Skip()\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/list.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\n// BasicType is a constraint for basic types that\n// can be used as element types in Redis lists and sets.\ntype BasicType interface {\n\tstring | int | int64 | float64\n}\n\n// NewListKeyspace creates a keyspace that stores ordered lists in the given cluster.\n//\n// The type parameter K specifies the key type, which can either be a\n// named struct type or a basic type (string, int, etc).\n//\n// The type parameter V specifies the value type, which is the type\n// of the elements in each list. It must be a basic type (string, int, int64, or float64).\nfunc NewListKeyspace[K any, V BasicType](cluster *Cluster, cfg KeyspaceConfig) *ListKeyspace[K, V] {\n\tfromRedis := basicFromRedisFactory[V]()\n\ttoRedis := basicToRedisFactory[V]()\n\n\treturn &ListKeyspace[K, V]{\n\t\tclient: newClient[K, V](cluster, cfg, fromRedis, toRedis),\n\t}\n}\n\n// ListKeyspace represents a set of cache keys,\n// each containing an ordered list of values of type V.\ntype ListKeyspace[K any, V BasicType] struct {\n\t*client[K, V]\n}\n\n// With returns a reference to the same keyspace but with customized write options.\n// The primary use case is for overriding the expiration time for certain cache operations.\n//\n// It is intended to be used with method chaining:\n//\n//\tmyKeyspace.With(cache.ExpireIn(3 * time.Second)).Set(...)\nfunc (k *ListKeyspace[K, V]) With(opts ...WriteOption) *ListKeyspace[K, V] {\n\treturn &ListKeyspace[K, V]{k.client.with(opts)}\n}\n\n// Delete deletes the specified keys.\n//\n// If a key does not exist it is ignored.\n//\n// It reports the number of keys that were deleted.\n//\n// See https://redis.io/commands/del/ for more information.\nfunc (s *ListKeyspace[K, V]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\treturn s.client.Delete(ctx, keys...)\n}\n\n// PushLeft pushes one or more values at the head of the list stored at key.\n// If the key does not already exist, it is first created as an empty list.\n//\n// If multiple values are given, they are inserted one after another,\n// starting with the leftmost value. For instance,\n//\n//\tPushLeft(ctx, \"mylist\", \"a\", \"b\", \"c\")\n//\n// will result in a list containing \"c\" as its first element,\n// \"b\" as its second element, and \"a\" as its third element.\n//\n// See https://redis.io/commands/lpush/ for more information.\nfunc (l *ListKeyspace[K, V]) PushLeft(ctx context.Context, key K, values ...V) (newLen int64, err error) {\n\tconst op = \"push left\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Convert []V to []any since that's what the Redis library expects.\n\tvals := fnMap(values, func(v V) any { return v })\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.LPush(ctx, k, vals...)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// PushRight pushes one or more values at the tail of the list stored at key.\n// If the key does not already exist, it is first created as an empty list.\n//\n// If multiple values are given, they are inserted one after another,\n// starting with the leftmost value. For instance,\n//\n//\tPushRight(ctx, \"mylist\", \"a\", \"b\", \"c\")\n//\n// will result in a list containing \"c\" as its last element,\n// \"b\" as its second to last element, and \"a\" as its third-to-last element.\n//\n// See https://redis.io/commands/rpush/ for more information.\nfunc (l *ListKeyspace[K, V]) PushRight(ctx context.Context, key K, values ...V) (newLen int64, err error) {\n\tconst op = \"push right\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Convert []V to []any since that's what the Redis library expects.\n\tvals := fnMap(values, func(v V) any { return v })\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.RPush(ctx, k, vals...)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// PopLeft pops a single element off the head of the list stored at key and returns it.\n// If the key does not exist, it returns an error matching Miss.\n//\n// See https://redis.io/commands/lpop/ for more information.\nfunc (l *ListKeyspace[K, V]) PopLeft(ctx context.Context, key K) (val V, err error) {\n\tconst op = \"pop left\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.StringCmd {\n\t\treturn c.LPop(ctx, k)\n\t}).Result()\n\n\tif err == nil {\n\t\tval, err = l.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\n// PopRight pops a single element off the tail of the list stored at key and returns it.\n// If the key does not exist, it returns an error matching Miss.\n//\n// See https://redis.io/commands/rpop/ for more information.\nfunc (l *ListKeyspace[K, V]) PopRight(ctx context.Context, key K) (val V, err error) {\n\tconst op = \"pop right\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.StringCmd {\n\t\treturn c.RPop(ctx, k)\n\t}).Result()\n\tif err == nil {\n\t\tval, err = l.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\n// Len reports the length of the list stored at key.\n//\n// Non-existing keys are considered as empty lists.\n//\n// See https://redis.io/commands/llen/ for more information.\nfunc (l *ListKeyspace[K, V]) Len(ctx context.Context, key K) (length int64, err error) {\n\tconst op = \"list len\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := l.redis.LLen(ctx, k).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// Trim trims the list stored at key to only contain the elements between the indices\n// start and stop (inclusive). Both start and stop are zero-based indices.\n//\n// Negative indices can be used to indicate offsets from the end of the list,\n// where -1 is the last element of the list, -2 the penultimate element, and so on.\n//\n// Out of range indices are valid and are treated as if they specify the start or end of the list,\n// respectively. If start > stop the end result is an empty list.\n//\n// See https://redis.io/commands/ltrim/ for more information.\nfunc (l *ListKeyspace[K, V]) Trim(ctx context.Context, key K, start, stop int64) error {\n\tconst op = \"list trim\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn err\n\t}\n\tres := do(l.client, ctx, k, func(c cmdable) *redis.StatusCmd {\n\t\treturn c.LTrim(ctx, k, start, stop)\n\t})\n\treturn toErr(res.Err(), op, k)\n}\n\n// Set updates the list element with the given idx to val.\n//\n// Negative indices can be used to indicate offsets from the end of the list,\n// where -1 is the last element of the list, -2 the penultimate element, and so on.\n//\n// An error is returned for out of bounds indices.\n//\n// See https://redis.io/commands/lset/ for more information.\nfunc (l *ListKeyspace[K, V]) Set(ctx context.Context, key K, idx int64, val V) (err error) {\n\tconst op = \"list set\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres := do(l.client, ctx, k, func(c cmdable) *redis.StatusCmd {\n\t\treturn c.LSet(ctx, k, idx, val)\n\t})\n\treturn toErr(res.Err(), op, k)\n}\n\n// Get returns the value of list element with the given idx.\n//\n// Negative indices can be used to indicate offsets from the end of the list,\n// where -1 is the last element of the list, -2 the penultimate element, and so on.\n//\n// For out of bounds indices or the key not existing in the cache, an error matching Miss is returned.\n//\n// See https://redis.io/commands/lget/ for more information.\nfunc (l *ListKeyspace[K, V]) Get(ctx context.Context, key K, idx int64) (val V, err error) {\n\tconst op = \"list get\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\tres, err := l.redis.LIndex(ctx, k, idx).Result()\n\n\tif err == nil {\n\t\tval, err = l.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\n// Items returns all the elements in the list stored at key.\n//\n// If the key does not exist it returns an empty list.\n//\n// See https://redis.io/commands/lrange/ for more information.\nfunc (l *ListKeyspace[K, V]) Items(ctx context.Context, key K) ([]V, error) {\n\treturn l.getRange(ctx, key, 0, -1, \"items\")\n}\n\n// GetRange returns all the elements in the list stored at key between start and stop.\n//\n// Negative indices can be used to indicate offsets from the end of the list,\n// where -1 is the last element of the list, -2 the penultimate element, and so on.\n//\n// If the key does not exist it returns an empty list.\n//\n// See https://redis.io/commands/lrange/ for more information.\nfunc (l *ListKeyspace[K, V]) GetRange(ctx context.Context, key K, start, stop int64) ([]V, error) {\n\treturn l.getRange(ctx, key, start, stop, \"get range\")\n}\n\nfunc (l *ListKeyspace[K, V]) getRange(ctx context.Context, key K, from, to int64, op string) (vals []V, err error) {\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres, err := l.redis.LRange(ctx, k, from, to).Result()\n\tif err != nil {\n\t\treturn nil, toErr(err, op, k)\n\t}\n\n\tret := make([]V, len(res))\n\tfor i, s := range res {\n\t\tval, err := l.fromRedis(s)\n\t\tif err != nil {\n\t\t\treturn nil, toErr(err, op, k)\n\t\t}\n\t\tret[i] = val\n\t}\n\treturn ret, nil\n}\n\n// InsertBefore inserts newVal into the list stored at key, at the position just before needle.\n//\n// If the list stored at key does not contain needle the value is not inserted,\n// and an error matching Miss is returned.\n//\n// It returns the new list length.\n//\n// See https://redis.io/commands/linsert/ for more information.\nfunc (l *ListKeyspace[K, V]) InsertBefore(ctx context.Context, key K, needle, newVal V) (newLen int64, err error) {\n\tconst op = \"insert before\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tnewLen, err = do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.LInsertBefore(ctx, k, needle, newVal)\n\t}).Result()\n\n\tif err == nil && newLen == -1 {\n\t\treturn 0, toErr(Miss, op, k)\n\t}\n\treturn newLen, toErr(err, op, k)\n}\n\n// InsertAfter inserts newVal into the list stored at key, at the position just after needle.\n//\n// It reports the new list length.\n//\n// If the list stored at key does not contain needle the value is not inserted,\n// and an error matching Miss is returned.\n//\n// See https://redis.io/commands/linsert/ for more information.\nfunc (l *ListKeyspace[K, V]) InsertAfter(ctx context.Context, key K, needle, newVal V) (newLen int64, err error) {\n\tconst op = \"insert after\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tnewLen, err = do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.LInsertAfter(ctx, k, needle, newVal)\n\t}).Result()\n\n\tif err == nil && newLen == -1 {\n\t\treturn 0, toErr(Miss, op, k)\n\t}\n\treturn newLen, toErr(err, op, k)\n}\n\n// RemoveAll removes all values equal to needle in the list stored at key.\n//\n// It reports the number of elements removed.\n//\n// If the list does not contain needle, or the list does not exist, it reports 0, nil.\n//\n// See https://redis.io/commands/lrem/ for more information.\nfunc (l *ListKeyspace[K, V]) RemoveAll(ctx context.Context, key K, needle V) (removed int64, err error) {\n\tconst op = \"remove all\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.LRem(ctx, k, 0, needle)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// RemoveFirst removes the first 'count' values equal to needle in the list stored at key.\n//\n// It reports the number of elements removed.\n//\n// If the list does not contain needle, or the list does not exist, it reports 0, nil.\n//\n// See https://redis.io/commands/lrem/ for more information.\nfunc (l *ListKeyspace[K, V]) RemoveFirst(ctx context.Context, key K, count int64, needle V) (removed int64, err error) {\n\tconst op = \"remove first\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif count < 0 {\n\t\terr = toErr(errors.New(\"negative count\"), op, k)\n\t\treturn 0, err\n\t} else if count == 0 {\n\t\treturn 0, nil\n\t}\n\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.LRem(ctx, k, count, needle)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// RemoveLast removes the last 'count' values equal to needle in the list stored at key.\n//\n// It reports the number of elements removed.\n//\n// If the list does not contain needle, or the list does not exist, it reports 0, nil.\n//\n// See https://redis.io/commands/lrem/ for more information.\nfunc (l *ListKeyspace[K, V]) RemoveLast(ctx context.Context, key K, count int64, needle V) (removed int64, err error) {\n\tconst op = \"remove last\"\n\tk, err := l.key(key, op)\n\tendTrace := l.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif count < 0 {\n\t\terr = toErr(errors.New(\"negative count\"), op, k)\n\t\treturn 0, err\n\t} else if count == 0 {\n\t\treturn 0, nil\n\t}\n\n\tres, err := do(l.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.LRem(ctx, k, -count, needle)\n\t}).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\ntype ListPos string\n\nconst (\n\tLeft  ListPos = \"LEFT\"\n\tRight ListPos = \"RIGHT\"\n)\n\n// Move atomically moves an element from the list stored at src to the list stored at dst.\n//\n// The value moved can be either the head (fromPos == Left) or tail (fromPos == Right) of the list at src.\n// Similarly, the value can be placed either at the head (toPos == Left) or tail (toPos == Right) of the list at dst.\n//\n// If src does not exist it reports an error matching Miss.\n//\n// If src and dst are the same list, the value is atomically rotated from one end to the other when fromPos != toPos,\n// or if fromPos == toPos nothing happens.\nfunc (l *ListKeyspace[K, V]) Move(ctx context.Context, src, dst K, fromPos, toPos ListPos) (moved V, err error) {\n\tconst op = \"list move\"\n\tks, err := l.keys([]K{src, dst}, op)\n\tendTrace := l.doTrace(op, true, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn moved, err\n\t}\n\tsrcKey, dstKey := ks[0], ks[1]\n\n\tres, err := do2(l.client, ctx, srcKey, dstKey, func(c cmdable) *redis.StringCmd {\n\t\treturn c.LMove(ctx, srcKey, dstKey, string(fromPos), string(toPos))\n\t}).Result()\n\n\tif err == nil {\n\t\tmoved, err = l.fromRedis(res)\n\t}\n\terr = toErr(err, op, srcKey)\n\treturn moved, err\n}\n\nfunc basicFromRedisFactory[V BasicType]() func(val string) (V, error) {\n\tvar zero V\n\ttyp := any(zero)\n\n\tvar fn any\n\tswitch typ.(type) {\n\tcase string:\n\t\tfn = func(val string) (string, error) {\n\t\t\treturn val, nil\n\t\t}\n\tcase int:\n\t\tfn = func(val string) (int, error) {\n\t\t\tres, err := strconv.ParseInt(val, 10, 64)\n\t\t\treturn int(res), err\n\t\t}\n\tcase int64:\n\t\tfn = func(val string) (int64, error) {\n\t\t\treturn strconv.ParseInt(val, 10, 64)\n\t\t}\n\tcase float64:\n\t\tfn = func(val string) (float64, error) {\n\t\t\treturn strconv.ParseFloat(val, 64)\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported BasicType %T\", typ))\n\t}\n\n\treturn fn.(func(val string) (V, error))\n}\n\nfunc basicToRedisFactory[V BasicType]() func(val V) (any, error) {\n\treturn func(val V) (any, error) { return val, nil }\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/list_test.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n)\n\nfunc TestListKeyspace(t *testing.T) {\n\tkt := newListTest(t)\n\tks, ctx := kt.ks, kt.ctx\n\n\tif got, want := kt.PushLeft(\"one\", \"a\"), int64(1); got != want {\n\t\tt.Errorf(\"lpush: got len %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"a\")\n\n\tif got, want := kt.PushLeft(\"one\", \"b\"), int64(2); got != want {\n\t\tt.Errorf(\"lpush: got len %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"b\", \"a\")\n\n\tif got, want := kt.PushRight(\"one\", \"c\"), int64(3); got != want {\n\t\tt.Errorf(\"rpush: got len %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"b\", \"a\", \"c\")\n\n\tkt.Set(\"one\", 1, \"d\")\n\tkt.Val(\"one\", \"b\", \"d\", \"c\")\n\n\tkt.Trim(\"one\", 1, 1)\n\tkt.Val(\"one\", \"d\")\n\n\tif got, want := must(ks.InsertBefore(ctx, \"one\", \"d\", \"e\")), int64(2); got != want {\n\t\tt.Errorf(\"insertbefore: got len %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"e\", \"d\")\n\tif got, want := must(ks.InsertAfter(ctx, \"one\", \"e\", \"f\")), int64(3); got != want {\n\t\tt.Errorf(\"insertafter: got len %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"e\", \"f\", \"d\")\n\n\tkt.PushRight(\"one\", \"e\")\n\tkt.PushRight(\"one\", \"e\")\n\tkt.PushRight(\"one\", \"f\")\n\tkt.Val(\"one\", \"e\", \"f\", \"d\", \"e\", \"e\", \"f\")\n\n\tif got, want := must(ks.RemoveFirst(ctx, \"one\", 2, \"e\")), int64(2); got != want {\n\t\tt.Errorf(\"removefirst: got %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"f\", \"d\", \"e\", \"f\")\n\tif got, want := must(ks.RemoveLast(ctx, \"one\", 1, \"f\")), int64(1); got != want {\n\t\tt.Errorf(\"removelast: got %v, want %v\", got, want)\n\t}\n\tkt.Val(\"one\", \"f\", \"d\", \"e\")\n\n\tif got, want := must(ks.Move(ctx, \"one\", \"one\", Left, Right)), \"f\"; got != want {\n\t\tt.Errorf(\"move: got %q, want %q\", got, want)\n\t}\n\tkt.Val(\"one\", \"d\", \"e\", \"f\")\n\n\tkt.PushRight(\"one\", \"d\")\n\tkt.Val(\"one\", \"d\", \"e\", \"f\", \"d\")\n\tmust(ks.RemoveAll(ctx, \"one\", \"d\"))\n\tkt.Val(\"one\", \"e\", \"f\")\n\n\tkt.PushLeft(\"one\", \"a\", \"b\")\n\tkt.PushRight(\"one\", \"c\", \"d\")\n\tkt.Val(\"one\", \"b\", \"a\", \"e\", \"f\", \"c\", \"d\")\n}\n\nfunc newListTest(t *testing.T) *listTester {\n\tcluster, srv := newTestCluster(t)\n\tks := NewListKeyspace[string, string](cluster, KeyspaceConfig{\n\t\tEncoreInternal_KeyMapper: func(s string) string { return s },\n\t})\n\tctx := context.Background()\n\treturn &listTester{t: t, ctx: ctx, ks: ks, srv: srv}\n}\n\ntype listTester struct {\n\tt   *testing.T\n\tctx context.Context\n\tks  *ListKeyspace[string, string]\n\tsrv *miniredis.Miniredis\n}\n\nfunc (t *listTester) PushLeft(key string, val ...string) int64 {\n\tt.t.Helper()\n\tnewLen, err := t.ks.PushLeft(t.ctx, key, val...)\n\tif err != nil {\n\t\tt.t.Errorf(\"LPush(%q, %q) = %v, want nil\", key, val, err)\n\t}\n\treturn newLen\n}\n\nfunc (t *listTester) PushRight(key string, val ...string) int64 {\n\tt.t.Helper()\n\tnewLen, err := t.ks.PushRight(t.ctx, key, val...)\n\tif err != nil {\n\t\tt.t.Errorf(\"RPush(%q, %q) = %v, want nil\", key, val, err)\n\t}\n\treturn newLen\n}\n\nfunc (t *listTester) Set(key string, idx int, val string) {\n\tt.t.Helper()\n\terr := t.ks.Set(t.ctx, key, int64(idx), val)\n\tif err != nil {\n\t\tt.t.Errorf(\"Set(%q, %v, %q) = %v, want nil\", key, idx, val, err)\n\t}\n}\n\nfunc (t *listTester) Trim(key string, from, to int) {\n\tt.t.Helper()\n\terr := t.ks.Trim(t.ctx, key, int64(from), int64(to))\n\tif err != nil {\n\t\tt.t.Errorf(\"Trim(%q, %v, %v) = %v, want nil\", key, from, to, err)\n\t}\n}\n\nfunc (t *listTester) Val(key string, want ...string) {\n\tt.t.Helper()\n\tgot := must(t.ks.GetRange(t.ctx, key, 0, -1))\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.t.Errorf(\"key %s: got %+v, want %+v\", key, got, want)\n\t}\n\tnum := must(t.ks.Len(t.ctx, key))\n\tif num != int64(len(got)) {\n\t\tt.t.Errorf(\"got len %d, expected %d (from getRange)\", num, len(got))\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/manager_internal.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\tmathrand \"math/rand\" // nosemgrep\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n\t\"github.com/go-redis/redis/v8\"\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/syncutil\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\n// Manager manages cache clients.\ntype Manager struct {\n\tstatic  *config.Static\n\truntime *config.Runtime\n\trt      *reqtrack.RequestTracker\n\tts      *testsupport.Manager\n\tjson    jsoniter.API\n\n\tinitTestSrv syncutil.Once\n\ttestSrv     *miniredis.Miniredis\n\n\tclientMu sync.RWMutex\n\tclients  map[string]*redis.Client\n}\n\nfunc NewManager(static *config.Static, runtime *config.Runtime, rt *reqtrack.RequestTracker, ts *testsupport.Manager, json jsoniter.API) *Manager {\n\treturn &Manager{\n\t\tstatic:  static,\n\t\truntime: runtime,\n\t\trt:      rt,\n\t\tts:      ts,\n\t\tjson:    json,\n\t\tclients: make(map[string]*redis.Client),\n\t}\n}\n\nfunc (mgr *Manager) getClient(clusterName string) *redis.Client {\n\tmgr.clientMu.RLock()\n\tcl := mgr.clients[clusterName]\n\tmgr.clientMu.RUnlock()\n\tif cl != nil {\n\t\treturn cl\n\t}\n\n\t// Client not found; acquire a write lock and set up the client\n\tmgr.clientMu.Lock()\n\tdefer mgr.clientMu.Unlock()\n\n\t// Did we race someone else and they set up the client first?\n\tif cl := mgr.clients[clusterName]; cl != nil {\n\t\treturn cl\n\t}\n\n\t// Are we in a test? If so, use the in-memory redis library.\n\tif mgr.static.Testing {\n\t\tcl, err := mgr.newMiniredisClient()\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"cache: unable to start redis mock: %v\", err))\n\t\t}\n\t\tmgr.clients[clusterName] = cl\n\t\treturn cl\n\t}\n\n\tfor _, rdb := range mgr.runtime.RedisDatabases {\n\t\tif rdb.EncoreName == clusterName {\n\t\t\t// If the server is configured for in-memory, use miniredis.\n\t\t\tsrv := mgr.runtime.RedisServers[rdb.ServerID]\n\t\t\tif srv.InMemory {\n\t\t\t\tcl, err := mgr.newMiniredisClient()\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"cache: unable to start redis mock: %v\", err))\n\t\t\t\t}\n\t\t\t\tmgr.clients[clusterName] = cl\n\t\t\t\treturn cl\n\t\t\t}\n\n\t\t\tcl, err := mgr.newClient(rdb)\n\t\t\tif err != nil {\n\t\t\t\tpanic(fmt.Sprintf(\"cache: unable to create redis client: %v\", err))\n\t\t\t}\n\t\t\tmgr.clients[clusterName] = cl\n\t\t\treturn cl\n\t\t}\n\t}\n\n\treturn newNoopClient()\n}\n\nfunc (mgr *Manager) newClient(rdb *config.RedisDatabase) (*redis.Client, error) {\n\tsrv := mgr.runtime.RedisServers[rdb.ServerID]\n\topts := &redis.Options{\n\t\tNetwork:      \"tcp\",\n\t\tAddr:         srv.Host,\n\t\tUsername:     srv.User,\n\t\tPassword:     srv.Password,\n\t\tDB:           rdb.Database,\n\t\tMinIdleConns: orDefault(rdb.MinConnections, 1),\n\t\tPoolSize:     orDefault(rdb.MaxConnections, runtime.GOMAXPROCS(0)*10),\n\t}\n\tif strings.HasPrefix(srv.Host, \"/\") {\n\t\topts.Network = \"unix\"\n\t}\n\n\tif srv.EnableTLS || srv.ServerCACert != \"\" || srv.ClientCert != \"\" {\n\t\topts.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}\n\t\tif srv.ServerCACert != \"\" {\n\t\t\tcaCertPool := x509.NewCertPool()\n\t\t\tif !caCertPool.AppendCertsFromPEM([]byte(srv.ServerCACert)) {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid server ca cert\")\n\t\t\t}\n\t\t\topts.TLSConfig.RootCAs = caCertPool\n\t\t}\n\t\tif srv.ClientCert != \"\" {\n\t\t\tcert, err := tls.X509KeyPair([]byte(srv.ClientCert), []byte(srv.ClientKey))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"parse client cert: %v\", err)\n\t\t\t}\n\t\t\topts.TLSConfig.Certificates = []tls.Certificate{cert}\n\t\t}\n\t}\n\n\treturn redis.NewClient(opts), nil\n}\n\nfunc (mgr *Manager) newMiniredisClient() (*redis.Client, error) {\n\terr := mgr.initTestSrv.Do(func() error {\n\t\tvar err error\n\t\tmgr.testSrv, err = miniredis.Run()\n\n\t\t// Periodically clean up cache keys if not running in tests\n\t\t// (i.e. running with in-memory flag set in a long-lived process).\n\t\tif err == nil && !mgr.static.Testing {\n\t\t\tgo miniredisCleanup(mgr.testSrv, 15*time.Second, 100)\n\t\t}\n\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\topts := &redis.Options{\n\t\tNetwork:      \"tcp\",\n\t\tAddr:         mgr.testSrv.Addr(),\n\t\tDB:           0,\n\t\tMinIdleConns: 1,\n\t\tPoolSize:     runtime.GOMAXPROCS(0) * 10,\n\t}\n\tcl := redis.NewClient(opts)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)\n\terr = cl.Ping(ctx).Err()\n\tcancel()\n\treturn cl, err\n}\n\nfunc (mgr *Manager) Shutdown(p *shutdown.Process) error {\n\t// The redis client does not have the concept of graceful shutdown,\n\t// so wait for user code to shut down first.\n\t<-p.ServicesShutdownCompleted.Done()\n\t<-p.OutstandingTasks.Done()\n\n\tmgr.clientMu.Lock()\n\tmgr.clientMu.Unlock()\n\tfor _, c := range mgr.clients {\n\t\t_ = c.Close()\n\t}\n\treturn nil\n}\n\nfunc newClient[K, V any](cluster *Cluster, cfg KeyspaceConfig,\n\tfromRedis func(string) (V, error),\n\ttoRedis func(V) (any, error),\n) *client[K, V] {\n\tkeyMapper := cfg.EncoreInternal_KeyMapper.(func(K) string)\n\tif mgr := cluster.mgr; mgr.static.Testing {\n\t\t// If we're running tests, map keys to a test-specific key.\n\t\torig := keyMapper\n\t\tkeyMapper = func(k K) string {\n\t\t\tkey := orig(k)\n\t\t\tif t := mgr.ts.CurrentTest(); t != nil {\n\t\t\t\tkey = t.Name() + \"::\" + key\n\t\t\t}\n\t\t\treturn key\n\t\t}\n\t}\n\n\t// Determine the default expiry function.\n\tdefaultExpiry := cfg.DefaultExpiry\n\tif defaultExpiry == nil {\n\t\tdefaultExpiry = func(now time.Time) time.Time {\n\t\t\treturn neverExpire\n\t\t}\n\t}\n\n\treturn &client[K, V]{\n\t\trt:        cluster.mgr.rt,\n\t\tredis:     cluster.cl,\n\t\tcfg:       cfg,\n\t\texpiry:    defaultExpiry,\n\t\tkeyMapper: keyMapper,\n\t\ttoRedis:   toRedis,\n\t\tfromRedis: fromRedis,\n\t}\n}\n\ntype client[K, V any] struct {\n\trt        *reqtrack.RequestTracker\n\tredis     *redis.Client\n\tcfg       KeyspaceConfig\n\texpiry    ExpiryFunc\n\tkeyMapper func(K) string\n\ttoRedis   func(V) (any, error)\n\tfromRedis func(string) (V, error)\n}\n\nfunc (c *client[K, V]) with(opts []WriteOption) *client[K, V] {\n\texpFunc := c.expiry\n\tfor _, opt := range opts {\n\t\tif eo, ok := opt.(expiryOption); ok {\n\t\t\texpFunc = eo.expiry\n\t\t}\n\t}\n\n\tc2 := *c\n\tc2.expiry = expFunc\n\treturn &c2\n}\n\nfunc (s *client[K, V]) key(k K, op string) (string, error) {\n\tres := s.keyMapper(k)\n\tif strings.HasPrefix(res, \"__encore\") {\n\t\treturn \"\", &OpError{\n\t\t\tOperation: op,\n\t\t\tRawKey:    res,\n\t\t\tErr:       errors.New(`use of reserved key prefix \"encore\"`),\n\t\t}\n\t}\n\treturn res, nil\n}\n\nfunc (s *client[K, V]) keys(keys []K, op string) ([]string, error) {\n\tstrs := make([]string, len(keys))\n\tvar err error\n\tfor i, k := range keys {\n\t\tstrs[i], err = s.key(k, op)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn strs, nil\n}\n\nfunc (s *client[K, V]) fromRedisMulti(res []string) ([]V, error) {\n\tvals := make([]V, len(res))\n\tfor i, r := range res {\n\t\tv, err := s.fromRedis(r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvals[i] = v\n\t}\n\treturn vals, nil\n}\n\nfunc (s *client[K, V]) valPtr(res string) (*V, error) {\n\tvv, err := s.fromRedis(res)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &vv, nil\n}\n\nfunc (s *client[K, V]) valOrNil(res string, err error) (*V, error) {\n\tif err == redis.Nil || errors.Is(err, Miss) {\n\t\treturn nil, nil\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.valPtr(res)\n}\n\nfunc (s *client[K, V]) expiryCmd(ctx context.Context, key string) *redis.BoolCmd {\n\tnow := time.Now()\n\texpTime := s.expiry(now)\n\tif expTime == keepTTL {\n\t\treturn nil\n\t} else if expTime == neverExpire {\n\t\treturn redis.NewBoolCmd(ctx, \"persist\", key)\n\t}\n\n\texpMs := expTime.UnixNano() / int64(time.Millisecond)\n\treturn redis.NewBoolCmd(ctx, \"pexpireat\", key, expMs)\n}\n\nfunc (s *client[K, V]) expiryDur() time.Duration {\n\tnow := time.Now()\n\texpTime := s.expiry(now)\n\n\tvar exp time.Duration\n\tswitch {\n\tcase expTime == neverExpire:\n\t\texp = 0\n\tcase expTime == keepTTL:\n\t\texp = redis.KeepTTL\n\tdefault:\n\t\texp = expTime.Sub(now)\n\t}\n\treturn exp\n}\n\nfunc (c *client[K, V]) doTrace(op string, write bool, keys ...string) func(error) {\n\teventID := c.traceStart(op, write, keys...)\n\treturn func(err error) {\n\t\tc.traceEnd(eventID, err)\n\t}\n}\n\nfunc (c *client[K, V]) traceStart(op string, write bool, keys ...string) (eventID model.TraceEventID) {\n\tif curr := c.rt.Current(); curr.Trace != nil && curr.Req != nil {\n\t\teventID = curr.Trace.CacheCallStart(trace2.CacheCallStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\tDefLoc:  c.cfg.EncoreInternal_DefLoc,\n\t\t\t},\n\t\t\tOperation: op,\n\t\t\tIsWrite:   write,\n\t\t\tKeys:      keys,\n\t\t\tStack:     stack.Build(3),\n\t\t})\n\t}\n\n\treturn eventID\n}\n\nfunc (c *client[K, V]) traceEnd(startEventID model.TraceEventID, err error, values ...any) {\n\tif startEventID == 0 { // indicates the operation start was not traced\n\t\treturn\n\t}\n\n\tif curr := c.rt.Current(); curr.Trace != nil && curr.Req != nil {\n\t\tvar cacheErr error\n\t\tvar res trace2.CacheCallResult\n\t\tswitch {\n\t\tcase err == nil:\n\t\t\tres = trace2.CacheOK\n\t\tcase errors.Is(err, Miss):\n\t\t\tres = trace2.CacheNoSuchKey\n\t\tcase errors.Is(err, KeyExists):\n\t\t\tres = trace2.CacheConflict\n\t\tcase err != nil:\n\t\t\tres = trace2.CacheErr\n\t\t\tcacheErr = err\n\t\t}\n\n\t\tcurr.Trace.CacheCallEnd(trace2.CacheCallEndParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tStartID: startEventID,\n\t\t\tRes:     res,\n\t\t\tErr:     cacheErr,\n\t\t})\n\t}\n}\n\ntype errWrapper struct {\n\terr error\n}\n\nfunc (ew errWrapper) Error() string {\n\treturn ew.err.Error()\n}\n\nfunc (ew errWrapper) Unwrap() error {\n\treturn ew.err\n}\n\nfunc toErr(err error, op, key string) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// Convert redis.Nil to cache.Miss.\n\tif errors.Is(err, redis.Nil) {\n\t\terr = Miss\n\t}\n\n\t// Is it already an OpError? If so, do nothing.\n\tvar opErr *OpError\n\tif errors.As(err, &opErr) {\n\t\treturn err\n\t}\n\n\terr = &OpError{Operation: op, RawKey: key, Err: err}\n\n\t// Wrap the error in an opaque type to ensure callers check for errors\n\t// with errors.Is and errors.As.\n\treturn errWrapper{err}\n}\n\nfunc toErr2[T any](val T, err error, op, key string) (T, error) {\n\treturn val, toErr(err, op, key)\n}\n\nfunc orDefault[T comparable](val, orDefault T) T {\n\tvar zero T\n\tif val == zero {\n\t\treturn orDefault\n\t}\n\treturn val\n}\n\nfunc miniredisCleanup(srv *miniredis.Miniredis, every time.Duration, maxKeys int) {\n\tvar acc time.Duration\n\tticker := time.NewTicker(time.Second)\n\tfor range ticker.C {\n\t\tsrv.FastForward(time.Second)\n\n\t\t// Clean up keys every so often\n\t\tacc += every\n\t\tif acc > every {\n\t\t\tacc -= every\n\n\t\t\tkeys := srv.Keys()\n\t\t\tfor len(keys) > 100 {\n\t\t\t\tid := mathrand.Intn(len(keys))\n\t\t\t\tif keys[id] != \"\" {\n\t\t\t\t\tsrv.Del(keys[id])\n\t\t\t\t\tkeys[id] = \"\" // mark it as deleted\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/noop_internal.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\nvar ErrNoopClient = errors.New(\n\t\"cache: this service is not configured to use this cache\",\n)\n\nfunc newNoopClient() *redis.Client {\n\tclient := redis.NewClient(&redis.Options{\n\t\tMinIdleConns: 0,\n\t\tDialer: func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\treturn nil, ErrNoopClient\n\t\t},\n\t\tOnConnect: func(ctx context.Context, cn *redis.Conn) error {\n\t\t\treturn ErrNoopClient\n\t\t},\n\t})\n\n\tclient.AddHook(&noopHook{})\n\n\treturn client\n}\n\ntype noopHook struct{}\n\nfunc (n *noopHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {\n\treturn nil, ErrNoopClient\n}\n\nfunc (n *noopHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error {\n\treturn ErrNoopClient\n}\n\nfunc (n *noopHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {\n\treturn nil, ErrNoopClient\n}\n\nfunc (n *noopHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error {\n\treturn ErrNoopClient\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/pkgfn.go",
    "content": "//go:build encore_app\n\npackage cache\n\n// NewCluster declares a new cache cluster.\n//\n// See https://encore.dev/docs/develop/caching for more information.\nfunc NewCluster(name string, cfg ClusterConfig) *Cluster {\n\treturn &Cluster{\n\t\tcfg: cfg,\n\t\tmgr: Singleton,\n\t\tcl:  Singleton.getClient(name),\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/set.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/go-redis/redis/v8\"\n)\n\n// NewSetKeyspace creates a keyspace that stores unordered sets in the given cluster.\n//\n// The type parameter K specifies the key type, which can either be a\n// named struct type or a basic type (string, int, etc).\n//\n// The type parameter V specifies the value type, which is the type\n// of the elements in each set. It must be a basic type (string, int, int64, or float64).\nfunc NewSetKeyspace[K any, V BasicType](cluster *Cluster, cfg KeyspaceConfig) *SetKeyspace[K, V] {\n\tfromRedis := basicFromRedisFactory[V]()\n\ttoRedis := basicToRedisFactory[V]()\n\n\treturn &SetKeyspace[K, V]{\n\t\tnewClient[K, V](cluster, cfg, fromRedis, toRedis),\n\t}\n}\n\n// SetKeyspace represents a set of cache keys,\n// each containing an unordered set of values of type V.\ntype SetKeyspace[K any, V BasicType] struct {\n\t*client[K, V]\n}\n\n// With returns a reference to the same keyspace but with customized write options.\n// The primary use case is for overriding the expiration time for certain cache operations.\n//\n// It is intended to be used with method chaining:\n//\n//\tmyKeyspace.With(cache.ExpireIn(3 * time.Second)).Set(...)\nfunc (k *SetKeyspace[K, V]) With(opts ...WriteOption) *SetKeyspace[K, V] {\n\treturn &SetKeyspace[K, V]{k.client.with(opts)}\n}\n\n// Delete deletes the specified keys.\n//\n// If a key does not exist it is ignored.\n//\n// It reports the number of keys that were deleted.\n//\n// See https://redis.io/commands/del/ for more information.\nfunc (s *SetKeyspace[K, V]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\treturn s.client.Delete(ctx, keys...)\n}\n\n// Add adds one or more values to the set stored at key.\n// If the key does not already exist, it is first created as an empty set.\n//\n// It reports the number of values that were added to the set,\n// not including values already present beforehand.\n//\n// See https://redis.io/commands/sadd/ for more information.\nfunc (s *SetKeyspace[K, V]) Add(ctx context.Context, key K, values ...V) (added int, err error) {\n\tconst op = \"set add\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvals := fnMap(values, func(v V) any { return v })\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.SAdd(ctx, k, vals...)\n\t}).Result()\n\n\terr = toErr(err, op, k)\n\treturn int(res), err\n}\n\n// Remove removes one or more values from the set stored at key.\n//\n// If a value is not present in the set is it ignored.\n//\n// Remove reports the number of values that were removed from the set.\n// If the key does not already exist, it is a no-op and reports 0, nil.\n//\n// See https://redis.io/commands/srem/ for more information.\nfunc (s *SetKeyspace[K, V]) Remove(ctx context.Context, key K, values ...V) (removed int, err error) {\n\tconst op = \"set remove\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvals := fnMap(values, func(v V) any { return v })\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.IntCmd {\n\t\treturn c.SRem(ctx, k, vals...)\n\t}).Result()\n\n\terr = toErr(err, op, k)\n\treturn int(res), err\n}\n\n// PopOne removes a random element from the set stored at key and returns it.\n//\n// If the set is empty it reports an error matching Miss.\n//\n// See https://redis.io/commands/spop/ for more information.\nfunc (s *SetKeyspace[K, V]) PopOne(ctx context.Context, key K) (val V, err error) {\n\tconst op = \"set pop one\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.StringCmd {\n\t\treturn c.SPop(ctx, k)\n\t}).Result()\n\n\tif err == nil {\n\t\tval, err = s.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\n// Pop removes up to 'count' random elements (bounded by the set's size)\n// from the set stored at key and returns them.\n//\n// If the set is empty it returns an empty slice and no error.\n//\n// See https://redis.io/commands/spop/ for more information.\nfunc (s *SetKeyspace[K, V]) Pop(ctx context.Context, key K, count int) (values []V, err error) {\n\tconst op = \"set pop\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, true, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := do(s.client, ctx, k, func(c cmdable) *redis.StringSliceCmd {\n\t\treturn c.SPopN(ctx, k, int64(count))\n\t}).Result()\n\n\tif err == nil {\n\t\tvalues, err = s.fromRedisMulti(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn values, err\n}\n\n// Contains reports whether the set stored at key contains the given value.\n//\n// If the key does not exist it reports false, nil.\n//\n// See https://redis.io/commands/sismember/ for more information.\nfunc (s *SetKeyspace[K, V]) Contains(ctx context.Context, key K, val V) (contains bool, err error) {\n\tconst op = \"set contains\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tres, err := s.redis.SIsMember(ctx, k, val).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// Len reports the number of elements in the set stored at key.\n//\n// If the key does not exist it reports 0, nil.\n//\n// See https://redis.io/commands/slen/ for more information.\nfunc (s *SetKeyspace[K, V]) Len(ctx context.Context, key K) (length int64, err error) {\n\tconst op = \"set len\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := s.redis.SCard(ctx, k).Result()\n\terr = toErr(err, op, k)\n\treturn res, err\n}\n\n// Items returns the elements in the set stored at key.\n//\n// If the key does not exist it returns an empty slice and no error.\n//\n// See https://redis.io/commands/smembers/ for more information.\nfunc (s *SetKeyspace[K, V]) Items(ctx context.Context, key K) (values []V, err error) {\n\tconst op = \"set items\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres, err := s.items(ctx, op, k)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toSlice(res, op, k)\n}\n\n// ItemsMap is identical to Items except it returns the values as a map.\n//\n// If the key does not exist it returns an empty (but non-nil) map and no error.\n//\n// See https://redis.io/commands/smembers/ for more information.\nfunc (s *SetKeyspace[K, V]) ItemsMap(ctx context.Context, key K) (values map[V]struct{}, err error) {\n\tconst op = \"set items\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := s.items(ctx, op, k)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toMap(res, op, k)\n}\n\nfunc (s *SetKeyspace[K, V]) items(ctx context.Context, op, key string) ([]string, error) {\n\tres, err := s.redis.SMembers(ctx, key).Result()\n\terr = toErr(err, op, key)\n\treturn res, err\n}\n\n// Diff computes the set difference, between the first set and all the consecutive sets.\n//\n// Set difference means the values present in the first set that are not present\n// in any of the other sets.\n//\n// Keys that do not exist are considered as empty sets.\n//\n// At least one key must be provided: if no keys are provided an error is reported.\n//\n// See https://redis.io/commands/sdiff/ for more information.\nfunc (s *SetKeyspace[K, V]) Diff(ctx context.Context, keys ...K) ([]V, error) {\n\tconst op = \"set diff\"\n\tres, firstKey, err := s.diff(ctx, op, keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toSlice(res, op, firstKey)\n}\n\n// DiffMap is identical to Diff except it returns the values as a map.\n//\n// See https://redis.io/commands/sdiff/ for more information.\nfunc (s *SetKeyspace[K, V]) DiffMap(ctx context.Context, keys ...K) (map[V]struct{}, error) {\n\tconst op = \"set diff\"\n\tres, firstKey, err := s.diff(ctx, op, keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toMap(res, op, firstKey)\n}\n\nfunc (s *SetKeyspace[K, V]) diff(ctx context.Context, op string, keys []K) (vals []string, firstKey string, err error) {\n\tks, err := s.keys(keys, op)\n\tendTrace := s.doTrace(op, false, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tif len(ks) > 0 {\n\t\tfirstKey = ks[0]\n\t}\n\n\tvals, err = s.redis.SDiff(ctx, ks...).Result()\n\treturn vals, firstKey, toErr(err, op, firstKey)\n}\n\n// DiffStore computes the set difference between keys (like Diff) and stores the result in destination.\n//\n// It reports the size of the resulting set.\n//\n// See https://redis.io/commands/sdiffstore/ for more information.\nfunc (s *SetKeyspace[K, V]) DiffStore(ctx context.Context, destination K, keys ...K) (size int64, err error) {\n\tconst op = \"store set diff\"\n\tdst, err := s.key(destination, op)\n\tendTrace := s.doTrace(op, true, dst)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tks, err := s.keys(keys, op)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do(s.client, ctx, dst, func(c cmdable) *redis.IntCmd {\n\t\treturn c.SDiffStore(ctx, dst, ks...)\n\t}).Result()\n\n\terr = toErr(err, op, dst)\n\treturn res, err\n}\n\n// Intersect computes the set intersection between the sets stored at the given keys.\n//\n// Set intersection means the values common to all the provided sets.\n//\n// Keys that do not exist are considered to be empty sets.\n// As a result, if any key is missing the final result is the empty set.\n//\n// At least one key must be provided: if no keys are provided an error is reported.\n//\n// See https://redis.io/commands/sinter/ for more information.\nfunc (s *SetKeyspace[K, V]) Intersect(ctx context.Context, keys ...K) ([]V, error) {\n\tconst op = \"intersect\"\n\tres, firstKey, err := s.intersect(ctx, op, keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toSlice(res, op, firstKey)\n}\n\n// IntersectMap is identical to Intersect except it returns the values as a map.\n//\n// See https://redis.io/commands/sinter/ for more information.\nfunc (s *SetKeyspace[K, V]) IntersectMap(ctx context.Context, keys ...K) (map[V]struct{}, error) {\n\tconst op = \"intersect\"\n\tres, firstKey, err := s.intersect(ctx, op, keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toMap(res, op, firstKey)\n}\n\nfunc (s *SetKeyspace[K, V]) intersect(ctx context.Context, op string, keys []K) (vals []string, firstKey string, err error) {\n\tks, err := s.keys(keys, op)\n\tendTrace := s.doTrace(op, false, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tif len(ks) > 0 {\n\t\tfirstKey = ks[0]\n\t}\n\n\tres, err := s.redis.SInter(ctx, ks...).Result()\n\terr = toErr(err, op, firstKey)\n\treturn res, firstKey, err\n}\n\n// IntersectStore computes the set intersection between keys (like Intersect) and stores the result in destination.\n//\n// It reports the size of the resulting set.\n//\n// See https://redis.io/commands/sinterstore/ for more information.\nfunc (s *SetKeyspace[K, V]) IntersectStore(ctx context.Context, destination K, keys ...K) (size int64, err error) {\n\tconst op = \"store set intersect\"\n\tdst, err := s.key(destination, op)\n\tendTrace := s.doTrace(op, true, dst)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tks, err := s.keys(keys, op)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres, err := do(s.client, ctx, dst, func(c cmdable) *redis.IntCmd {\n\t\treturn c.SInterStore(ctx, dst, ks...)\n\t}).Result()\n\terr = toErr(err, op, dst)\n\treturn res, err\n}\n\n// Union computes the set union between the sets stored at the given keys.\n//\n// Set union means the values present in at least one of the provided sets.\n//\n// Keys that do not exist are considered to be empty sets.\n//\n// At least one key must be provided: if no keys are provided an error is reported.\n//\n// See https://redis.io/commands/sunion/ for more information.\nfunc (s *SetKeyspace[K, V]) Union(ctx context.Context, keys ...K) ([]V, error) {\n\tconst op = \"union\"\n\tres, firstKey, err := s.union(ctx, op, keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toSlice(res, op, firstKey)\n}\n\n// UnionMap is identical to Union except it returns the values as a map.\n//\n// See https://redis.io/commands/sunion/ for more information.\nfunc (s *SetKeyspace[K, V]) UnionMap(ctx context.Context, keys ...K) (map[V]struct{}, error) {\n\tconst op = \"union\"\n\tres, firstKey, err := s.union(ctx, op, keys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn s.toMap(res, op, firstKey)\n}\n\nfunc (s *SetKeyspace[K, V]) union(ctx context.Context, op string, keys []K) (vals []string, firstKey string, err error) {\n\tks, err := s.keys(keys, op)\n\tendTrace := s.doTrace(op, false, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tif len(ks) > 0 {\n\t\tfirstKey = ks[0]\n\t}\n\n\tres, err := s.redis.SUnion(ctx, ks...).Result()\n\terr = toErr(err, op, firstKey)\n\treturn res, firstKey, err\n}\n\n// UnionStore computes the set union between sets (like Union) and stores the result in destination.\n//\n// It reports the size of the resulting set.\n//\n// See https://redis.io/commands/sunionstore/ for more information.\nfunc (s *SetKeyspace[K, V]) UnionStore(ctx context.Context, destination K, keys ...K) (size int64, err error) {\n\tconst op = \"store set union\"\n\tdst, err := s.key(destination, op)\n\tendTrace := s.doTrace(op, true, dst)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tks, err := s.keys(keys, op)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tres, err := do(s.client, ctx, dst, func(c cmdable) *redis.IntCmd {\n\t\treturn c.SUnionStore(ctx, dst, ks...)\n\t}).Result()\n\terr = toErr(err, op, dst)\n\treturn res, err\n}\n\n// SampleOne returns a random member from the set stored at key.\n//\n// If the key does not exist it reports an error matching Miss.\n//\n// See https://redis.io/commands/srandmember/ for more information.\nfunc (s *SetKeyspace[K, V]) SampleOne(ctx context.Context, key K) (val V, err error) {\n\tconst op = \"set sample one\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn val, err\n\t}\n\n\tres, err := s.redis.SRandMember(ctx, k).Result()\n\tif err == nil {\n\t\tval, err = s.fromRedis(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn val, err\n}\n\n// Sample returns up to 'count' distinct random elements from the set stored at key.\n// The same element is never returned multiple times.\n//\n// If the key does not exist it returns an empty slice and no error.\n//\n// See https://redis.io/commands/srandmember/ for more information.\nfunc (s *SetKeyspace[K, V]) Sample(ctx context.Context, key K, count int) (values []V, err error) {\n\tconst op = \"set sample\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif count < 0 {\n\t\terr = toErr(errors.New(\"negative count\"), op, k)\n\t\treturn nil, err\n\t} else if count == 0 {\n\t\treturn nil, nil\n\t}\n\n\tres, err := s.redis.SRandMemberN(ctx, k, int64(count)).Result()\n\tif err == nil {\n\t\tvalues, err = s.fromRedisMulti(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn values, err\n}\n\n// SampleWithReplacement returns count random elements from the set stored at key.\n// The same element may be returned multiple times.\n//\n// If the key does not exist it returns an empty slice and no error.\n//\n// See https://redis.io/commands/srandmember/ for more information.\nfunc (s *SetKeyspace[K, V]) SampleWithReplacement(ctx context.Context, key K, count int) (values []V, err error) {\n\tconst op = \"set sample with replacement\"\n\tk, err := s.key(key, op)\n\tendTrace := s.doTrace(op, false, k)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif count < 0 {\n\t\terr = toErr(errors.New(\"negative count\"), op, k)\n\t\treturn nil, err\n\t} else if count == 0 {\n\t\treturn nil, nil\n\t}\n\n\tres, err := s.redis.SRandMemberN(ctx, k, -int64(count)).Result()\n\tif err == nil {\n\t\tvalues, err = s.fromRedisMulti(res)\n\t}\n\terr = toErr(err, op, k)\n\treturn values, err\n}\n\n// Move atomically moves the given value from the set stored at src\n// to the set stored at dst.\n//\n// If the element does not exist in src it reports false, nil.\n//\n// If the element already exists in dst it is still removed from src\n// and Move still reports true, nil.\nfunc (s *SetKeyspace[K, V]) Move(ctx context.Context, src, dst K, val V) (moved bool, err error) {\n\tconst op = \"move\"\n\tks, err := s.keys([]K{src, dst}, op)\n\tendTrace := s.doTrace(op, true, ks...)\n\tdefer func() { endTrace(err) }()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tsrcKey, dstKey := ks[0], ks[1]\n\tres, err := do2(s.client, ctx, srcKey, dstKey, func(c cmdable) *redis.BoolCmd {\n\t\treturn c.SMove(ctx, srcKey, dstKey, val)\n\t}).Result()\n\treturn res, toErr(err, op, srcKey)\n}\n\nfunc (s *SetKeyspace[K, V]) toSlice(res []string, op, key string) ([]V, error) {\n\tret := make([]V, len(res))\n\tfor i, r := range res {\n\t\tval, err := s.fromRedis(r)\n\t\tif err != nil {\n\t\t\treturn nil, toErr(err, op, key)\n\t\t}\n\t\tret[i] = val\n\t}\n\treturn ret, nil\n}\n\nfunc (s *SetKeyspace[K, V]) toMap(res []string, op, key string) (map[V]struct{}, error) {\n\tret := make(map[V]struct{}, len(res))\n\tfor _, r := range res {\n\t\tval, err := s.fromRedis(r)\n\t\tif err != nil {\n\t\t\treturn nil, toErr(err, op, key)\n\t\t}\n\t\tret[val] = struct{}{}\n\t}\n\treturn ret, nil\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/set_test.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/alicebob/miniredis/v2\"\n)\n\nfunc TestSets(t *testing.T) {\n\tkt := newSetTest(t)\n\tks, ctx := kt.ks, kt.ctx\n\n\tif got, want := kt.Add(\"one\", \"a\", \"b\"), 2; got != want {\n\t\tt.Errorf(\"Add() = %d, want %d\", got, want)\n\t}\n\tkt.Val(\"one\", \"a\", \"b\")\n\tif got, want := kt.Remove(\"one\", \"b\", \"c\"), 1; got != want {\n\t\tt.Errorf(\"Remove() = %d, want %d\", got, want)\n\t}\n\tkt.Val(\"one\", \"a\")\n\n\t{\n\t\tgot := must(ks.SampleWithReplacement(ctx, \"one\", 3))\n\t\tcheckSorted(t, got, \"a\", \"a\", \"a\")\n\t}\n\n\tif got, want := must(ks.SampleOne(ctx, \"one\")), \"a\"; got != want {\n\t\tt.Errorf(\"SampleOne: got %v, want %v\", got, want)\n\t}\n\tif got, want := must(ks.PopOne(ctx, \"one\")), \"a\"; got != want {\n\t\tt.Errorf(\"PopOne: got %v, want %v\", got, want)\n\t}\n\tif _, err := ks.SampleOne(ctx, \"one\"); !errors.Is(err, Miss) {\n\t\tt.Errorf(\"SampleOne: got err %v, want %v\", err, Miss)\n\t}\n\tif _, err := ks.PopOne(ctx, \"one\"); !errors.Is(err, Miss) {\n\t\tt.Errorf(\"PopOne: got err %v, want %v\", err, Miss)\n\t}\n\n\tkt.Add(\"one\", \"a\", \"b\")\n\tkt.Contains(\"one\", \"a\")\n\tkt.Missing(\"one\", \"c\")\n\tif got, want := kt.Add(\"one\", \"a\", \"b\", \"c\"), 1; got != want {\n\t\tt.Errorf(\"Add() = %d, want %d\", got, want)\n\t}\n\n\tcheckSetMap(t, must(ks.ItemsMap(ctx, \"one\")), \"a\", \"b\", \"c\")\n\n\tif got, want := must(ks.Len(ctx, \"one\")), int64(3); got != want {\n\t\tt.Errorf(\"Len() = %d, want %d\", got, want)\n\t}\n\n\t{\n\t\tgot := must(ks.Sample(ctx, \"one\", 3))\n\t\tcheckSorted(t, got, \"a\", \"b\", \"c\")\n\t}\n\n\t{\n\t\tgot := must(ks.Pop(ctx, \"one\", 3))\n\t\tcheckSorted(t, got, \"a\", \"b\", \"c\")\n\t}\n\n\t{\n\t\tkt.Add(\"D1\", \"a\", \"b\", \"c\")\n\t\tkt.Add(\"D2\", \"c\", \"d\", \"e\")\n\t\tgot := must(ks.Diff(ctx, \"D1\", \"D2\"))\n\t\twant := []string{\"a\", \"b\"}\n\t\tcheckSorted(t, got, want...)\n\t\tcheckSetMap(t, must(ks.DiffMap(ctx, \"D1\", \"D2\")), want...)\n\t\tif got, want := must(ks.DiffStore(ctx, \"D3\", \"D1\", \"D2\")), int64(len(want)); got != want {\n\t\t\tt.Errorf(\"DiffStore: got %v, want %v\", got, want)\n\t\t}\n\t\tkt.Val(\"D3\", want...)\n\t}\n\n\t{\n\t\tkt.Add(\"I1\", \"a\", \"b\", \"c\")\n\t\tkt.Add(\"I2\", \"c\", \"d\", \"e\")\n\t\tgot := must(ks.Intersect(ctx, \"I1\", \"I2\"))\n\t\twant := []string{\"c\"}\n\t\tcheckSorted(t, got, want...)\n\t\tcheckSetMap(t, must(ks.IntersectMap(ctx, \"I1\", \"I2\")), want...)\n\t\tif got, want := must(ks.IntersectStore(ctx, \"I3\", \"I1\", \"I2\")), int64(len(want)); got != want {\n\t\t\tt.Errorf(\"IntersectStore: got %v, want %v\", got, want)\n\t\t}\n\t\tkt.Val(\"I3\", want...)\n\t}\n\n\t{\n\t\tkt.Add(\"U1\", \"a\", \"b\", \"c\")\n\t\tkt.Add(\"U2\", \"c\", \"d\", \"e\")\n\t\tgot := must(ks.Union(ctx, \"U1\", \"U2\"))\n\t\twant := []string{\"a\", \"b\", \"c\", \"d\", \"e\"}\n\t\tcheckSorted(t, got, want...)\n\t\tcheckSetMap(t, must(ks.UnionMap(ctx, \"U1\", \"U2\")), want...)\n\t\tif got, want := must(ks.UnionStore(ctx, \"U3\", \"U1\", \"U2\")), int64(len(want)); got != want {\n\t\t\tt.Errorf(\"UnionStore: got %v, want %v\", got, want)\n\t\t}\n\t\tkt.Val(\"U3\", want...)\n\t}\n\n\t{\n\t\tkt.Add(\"M1\", \"a\", \"b\", \"c\")\n\t\tkt.Add(\"M2\", \"a\")\n\n\t\tmust(ks.Move(ctx, \"M1\", \"M2\", \"a\"))\n\t\tkt.Val(\"M1\", \"b\", \"c\")\n\t\tkt.Val(\"M2\", \"a\")\n\n\t\tmust(ks.Move(ctx, \"M1\", \"M2\", \"b\"))\n\t\tkt.Val(\"M1\", \"c\")\n\t\tkt.Val(\"M2\", \"a\", \"b\")\n\t}\n}\n\nfunc checkSorted(t *testing.T, got []string, want ...string) {\n\tt.Helper()\n\tsort.Strings(got)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"got %v, want %v\", got, want)\n\t}\n}\n\nfunc checkSetMap(t *testing.T, got map[string]struct{}, want ...string) {\n\tt.Helper()\n\tif len(got) != len(want) {\n\t\tt.Errorf(\"got %d items, want %d\", len(got), len(want))\n\t\treturn\n\t}\n\tfor _, w := range want {\n\t\tif _, ok := got[w]; !ok {\n\t\t\tt.Errorf(\"wanted key %q, not found\", w)\n\t\t}\n\t}\n}\n\nfunc newSetTest(t *testing.T) *setTester {\n\tcluster, srv := newTestCluster(t)\n\tks := NewSetKeyspace[string, string](cluster, KeyspaceConfig{\n\t\tEncoreInternal_KeyMapper: func(s string) string { return s },\n\t})\n\tctx := context.Background()\n\treturn &setTester{t: t, ctx: ctx, ks: ks, srv: srv}\n}\n\ntype setTester struct {\n\tt   *testing.T\n\tctx context.Context\n\tks  *SetKeyspace[string, string]\n\tsrv *miniredis.Miniredis\n}\n\nfunc (t *setTester) Add(key string, val ...string) int {\n\tt.t.Helper()\n\tnumAdded, err := t.ks.Add(t.ctx, key, val...)\n\tif err != nil {\n\t\tt.t.Errorf(\"Add(%q, %v+) = %v, want nil\", key, val, err)\n\t}\n\treturn numAdded\n}\n\nfunc (t *setTester) Remove(key string, val ...string) int {\n\tt.t.Helper()\n\tnumRemoved, err := t.ks.Remove(t.ctx, key, val...)\n\tif err != nil {\n\t\tt.t.Errorf(\"Remove(%q, %+v) = %v, want nil\", key, val, err)\n\t}\n\treturn int(numRemoved)\n}\n\nfunc (t *setTester) Contains(key string, val string) {\n\tt.t.Helper()\n\tif ok, err := t.ks.Contains(t.ctx, key, val); err != nil {\n\t\tt.t.Errorf(\"Contains(%q, %q) = %v, want nil\", key, val, err)\n\t} else if !ok {\n\t\tt.t.Errorf(\"key %q: value %q is missing in the set\", key, val)\n\t}\n}\n\nfunc (t *setTester) Missing(key string, val string) {\n\tt.t.Helper()\n\tif ok, err := t.ks.Contains(t.ctx, key, val); err != nil {\n\t\tt.t.Errorf(\"Contains(%q, %q) = %v, want nil\", key, val, err)\n\t} else if ok {\n\t\tt.t.Errorf(\"key %q: value %q is present in the set\", key, val)\n\t}\n}\n\nfunc (t *setTester) Val(key string, want ...string) {\n\tt.t.Helper()\n\tgot := must(t.ks.Items(t.ctx, key))\n\tsort.Strings(want)\n\tsort.Strings(got)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.t.Errorf(\"key %s: got %+v, want %+v\", key, got, want)\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/struct.go",
    "content": "package cache\n\nimport \"context\"\n\n// NewStructKeyspace creates a keyspace that stores structs in the given cluster.\n//\n// The type parameter K specifies the key type, which can either be a\n// named struct type or a basic type (string, int, etc).\n//\n// The value parameter V specifies the named struct type that should be stored.\nfunc NewStructKeyspace[K, V any](cluster *Cluster, cfg KeyspaceConfig) *StructKeyspace[K, V] {\n\tjson := cluster.mgr.json\n\tfromRedis := func(val string) (V, error) {\n\t\tvar v V\n\t\terr := json.UnmarshalFromString(val, &v)\n\t\treturn v, err\n\t}\n\ttoRedis := func(val V) (any, error) {\n\t\treturn json.MarshalToString(val)\n\t}\n\n\treturn &StructKeyspace[K, V]{\n\t\t&basicKeyspace[K, V]{\n\t\t\tnewClient[K, V](cluster, cfg, fromRedis, toRedis),\n\t\t},\n\t}\n}\n\n// StructKeyspace represents a set of cache keys that hold struct values.\ntype StructKeyspace[K, V any] struct {\n\t*basicKeyspace[K, V]\n}\n\n// With returns a reference to the same keyspace but with customized write options.\n// The primary use case is for overriding the expiration time for certain cache operations.\n//\n// It is intended to be used with method chaining:\n//\n//\tmyKeyspace.With(cache.ExpireIn(3 * time.Second)).Set(...)\nfunc (k *StructKeyspace[K, V]) With(opts ...WriteOption) *StructKeyspace[K, V] {\n\treturn &StructKeyspace[K, V]{k.basicKeyspace.with(opts)}\n}\n\n// Get gets the value stored at key.\n// If the key does not exist, it returns an error matching Miss.\n//\n// See https://redis.io/commands/get/ for more information.\nfunc (s *StructKeyspace[K, V]) Get(ctx context.Context, key K) (V, error) {\n\treturn s.basicKeyspace.Get(ctx, key)\n}\n\n// MultiGet gets the values stored at multiple keys.\n// For each key, the result contains an Err field indicating success or failure.\n// If Err is nil, Value contains the cached value.\n// If Err matches Miss, the key was not found.\n//\n// See https://redis.io/commands/mget/ for more information.\nfunc (s *StructKeyspace[K, V]) MultiGet(ctx context.Context, keys ...K) ([]Result[V], error) {\n\treturn s.basicKeyspace.MultiGet(ctx, keys...)\n}\n\n// Set updates the value stored at key to val.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *StructKeyspace[K, V]) Set(ctx context.Context, key K, val V) error {\n\treturn s.basicKeyspace.Set(ctx, key, val)\n}\n\n// SetIfNotExists sets the value stored at key to val, but only if the key does not exist beforehand.\n// If the key already exists, it reports an error matching KeyExists.\n//\n// See https://redis.io/commands/setnx/ for more information.\nfunc (s *StructKeyspace[K, V]) SetIfNotExists(ctx context.Context, key K, val V) error {\n\treturn s.basicKeyspace.SetIfNotExists(ctx, key, val)\n}\n\n// Replace replaces the existing value stored at key to val.\n// If the key does not already exist, it reports an error matching Miss.\n//\n// See https://redis.io/commands/set/ for more information.\nfunc (s *StructKeyspace[K, V]) Replace(ctx context.Context, key K, val V) error {\n\treturn s.basicKeyspace.Replace(ctx, key, val)\n}\n\n// GetAndSet updates the value of key to val and returns the previously stored value.\n// If the key does not already exist, it sets it and returns 0, nil.\n//\n// See https://redis.io/commands/getset/ for more information.\nfunc (s *StructKeyspace[K, V]) GetAndSet(ctx context.Context, key K, val V) (oldVal V, err error) {\n\treturn s.basicKeyspace.GetAndSet(ctx, key, val)\n}\n\n// GetAndDelete deletes the key and returns the previously stored value.\n// If the key does not already exist, it does nothing and returns the zero value of V and nil.\n//\n// See https://redis.io/commands/getdel/ for more information.\nfunc (s *StructKeyspace[K, V]) GetAndDelete(ctx context.Context, key K) (oldVal V, err error) {\n\treturn s.basicKeyspace.GetAndDelete(ctx, key)\n}\n\n// Delete deletes the specified keys.\n//\n// If a key does not exist it is ignored.\n//\n// It reports the number of keys that were deleted.\n//\n// See https://redis.io/commands/del/ for more information.\nfunc (s *StructKeyspace[K, V]) Delete(ctx context.Context, keys ...K) (deleted int, err error) {\n\treturn s.client.Delete(ctx, keys...)\n}\n"
  },
  {
    "path": "runtimes/go/storage/cache/zzz_singleton_internal.go",
    "content": "//go:build encore_app\n\npackage cache\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/jsonapi\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\n// Initialize the singleton instance.\n// NOTE: This file is named zzz_singleton_internal.go so that\n// the init function is initialized after all the providers\n// have been registered.\n\n//publicapigen:drop\nvar Singleton *Manager\n\nfunc init() {\n\tSingleton = NewManager(appconf.Static, appconf.Runtime, reqtrack.Singleton, testsupport.Singleton, jsonapi.Default)\n\tshutdown.Singleton.RegisterShutdownHandler(Singleton.Shutdown)\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/bucket.go",
    "content": "package objects\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"iter\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/storage/objects/internal/providers/noop\"\n\t\"encore.dev/storage/objects/internal/types\"\n)\n\n// Bucket represents an object storage bucket, containing a set of files.\n//\n// See NewBucket for more information on how to declare a Bucket.\ntype Bucket struct {\n\tmgr        *Manager\n\truntimeCfg *config.Bucket // The config for this running instance of the application\n\timpl       types.BucketImpl\n\tname       string\n\n\t// Prefix to prepend to all cloud names.\n\tbaseCloudPrefix string\n\n\t// publicBaseURL, if the bucket is public\n\tpublicBaseURL *url.URL\n}\n\n// BucketConfig is the configuration for a Bucket.\ntype BucketConfig struct {\n\t// Whether objects in the bucket should be publicly accessible, via CDN.\n\tPublic bool\n\n\t// Whether objects stored in the bucket should be versioned.\n\t//\n\t// If true, the bucket will store multiple versions of each object\n\t// whenever it changes, as opposed to overwriting the old version.\n\tVersioned bool\n}\n\nfunc newBucket(mgr *Manager, name string) *Bucket {\n\t// Look up the bkt configuration\n\tbkt, ok := mgr.runtime.Buckets[name]\n\tif !ok {\n\t\t// No runtime config; return the noop implementation.\n\t\treturn &Bucket{\n\t\t\tmgr:        mgr,\n\t\t\truntimeCfg: &config.Bucket{EncoreName: name},\n\t\t\timpl:       &noop.BucketImpl{},\n\t\t\tname:       name,\n\t\t}\n\t}\n\n\t// Look up the provider config\n\tprovider := mgr.runtime.BucketProviders[bkt.ProviderID]\n\n\ttried := make([]string, 0, len(mgr.providers))\n\tfor _, p := range mgr.providers {\n\t\tif p.Matches(provider) {\n\t\t\timpl := p.NewBucket(provider, bkt)\n\n\t\t\tvar publicBaseURL *url.URL\n\t\t\tif bkt.PublicBaseURL != \"\" {\n\t\t\t\tvar err error\n\t\t\t\tpublicBaseURL, err = url.Parse(bkt.PublicBaseURL)\n\t\t\t\tif err != nil {\n\t\t\t\t\tmgr.rootLogger.Fatal().Msgf(\"invalid public base url for bucket %s: %v\", name, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn &Bucket{\n\t\t\t\tmgr:             mgr,\n\t\t\t\truntimeCfg:      bkt,\n\t\t\t\timpl:            impl,\n\t\t\t\tname:            name,\n\t\t\t\tbaseCloudPrefix: bkt.KeyPrefix,\n\t\t\t\tpublicBaseURL:   publicBaseURL,\n\t\t\t}\n\t\t}\n\n\t\ttried = append(tried, p.ProviderName())\n\t}\n\n\tmgr.rootLogger.Fatal().Msgf(\"unsupported Object Storage provider for provider[%d], tried: %v\",\n\t\tbkt.ProviderID, tried)\n\tpanic(\"unreachable\")\n}\n\n// Upload uploads a new object to the bucket.\n//\n// The returned writer must be successfully closed for the upload to complete.\n// To abort the upload, call (*Writer).Abort or cancel the provided context.\nfunc (b *Bucket) Upload(ctx context.Context, object string, options ...UploadOption) *Writer {\n\tvar opt uploadOptions\n\tfor _, o := range options {\n\t\to.applyUpload(&opt)\n\t}\n\n\tw := &Writer{\n\t\tbkt: b,\n\t\tctx: ctx,\n\t\tobj: object,\n\t\topt: opt,\n\t}\n\n\tcurr := b.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tw.curr = curr\n\t\tw.startEventID = curr.Trace.BucketObjectUploadStart(trace2.BucketObjectUploadStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tBucket: b.name,\n\t\t\tObject: object,\n\t\t\tAttrs: trace2.BucketObjectAttributes{\n\t\t\t\tContentType: ptrOrNil(opt.attrs.ContentType),\n\t\t\t},\n\t\t\tStack: stack.Build(1),\n\t\t})\n\t}\n\n\treturn w\n}\n\n// PublicURL returns the public URL for accessing an object in the bucket.\nfunc (b *Bucket) PublicURL(object string, options ...PublicURLOption) *url.URL {\n\tif b.publicBaseURL == nil {\n\t\t// This should never happen, since access to this method is controlled\n\t\t// by static analysis.\n\t\tpanic(\"internal encore error: missing publicBaseURL for bucket \" + b.name)\n\t}\n\n\t// Clone the base url\n\tu := *b.publicBaseURL\n\n\tif !strings.HasSuffix(u.Path, \"/\") {\n\t\tu.Path += \"/\"\n\t}\n\tu.Path += escape(object, encodePath)\n\n\treturn &u\n}\n\n// Writer is the writer for an object being uploaded to a bucket.\ntype Writer struct {\n\tbkt *Bucket\n\n\tctx context.Context\n\tobj string\n\n\topt uploadOptions\n\n\t// Initialized on first write\n\tu types.Uploader\n\n\t// Set if tracing\n\tcurr         reqtrack.Current\n\tstartEventID trace2.EventID\n}\n\n// Write writes data to the object being uploaded.\nfunc (w *Writer) Write(p []byte) (int, error) {\n\tu := w.initUpload()\n\treturn u.Write(p)\n}\n\n// Abort aborts the upload.\nfunc (w *Writer) Abort(err error) {\n\tif err == nil {\n\t\terr = errors.New(\"upload aborted\")\n\t}\n\tu := w.initUpload()\n\tu.Abort(err)\n}\n\n// Close closes the upload, completing the upload if no errors occurred.\nfunc (w *Writer) Close() error {\n\tu := w.initUpload()\n\tattrs, err := u.Complete()\n\n\tif w.curr.Trace != nil {\n\t\tparams := trace2.BucketObjectUploadEndParams{\n\t\t\tStartID: w.startEventID,\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: w.curr.Req.TraceID,\n\t\t\t\tSpanID:  w.curr.Req.SpanID,\n\t\t\t\tGoid:    w.curr.Goctr,\n\t\t\t},\n\t\t\tErr: err,\n\t\t}\n\n\t\tif attrs != nil {\n\t\t\tparams.Size = uint64(attrs.Size)\n\t\t\tparams.Version = ptrOrNil(attrs.Version)\n\t\t}\n\t\tw.curr.Trace.BucketObjectUploadEnd(params)\n\t}\n\n\treturn err\n}\n\nfunc (w *Writer) initUpload() types.Uploader {\n\tif w.u == nil {\n\t\tu, err := w.bkt.impl.Upload(types.UploadData{\n\t\t\tCtx:    w.ctx,\n\t\t\tObject: w.bkt.toCloudObject(w.obj),\n\t\t\tAttrs:  w.opt.attrs,\n\t\t\tPre: types.Preconditions{\n\t\t\t\tNotExists: w.opt.pre.NotExists,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tw.u = &errUploader{err: err}\n\t\t} else {\n\t\t\tw.u = u\n\t\t}\n\t}\n\n\treturn w.u\n}\n\ntype errUploader struct {\n\terr error\n}\n\nfunc (e *errUploader) Write(p []byte) (int, error) {\n\treturn 0, e.err\n}\nfunc (e *errUploader) Abort(err error) {}\nfunc (e *errUploader) Complete() (*types.ObjectAttrs, error) {\n\treturn nil, e.err\n}\n\nvar _ types.Uploader = &errUploader{}\n\n// Download downloads an object from the bucket.\n// Any error is encountered is reported by the methods on *Reader.\n// To check if the operation failed, call (*Reader).Err.\n//\n// If the object does not exist, the error may be checked with errors.Is(err, ErrObjectNotFound).\nfunc (b *Bucket) Download(ctx context.Context, object string, options ...DownloadOption) *Reader {\n\tvar opt downloadOptions\n\tfor _, o := range options {\n\t\to.applyDownload(&opt)\n\t}\n\n\tvar startEventID trace2.EventID\n\tcurr := b.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tstartEventID = curr.Trace.BucketObjectDownloadStart(trace2.BucketObjectDownloadStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tBucket:  b.name,\n\t\t\tObject:  object,\n\t\t\tVersion: ptrOrNil(opt.version),\n\t\t\tStack:   stack.Build(1),\n\t\t})\n\t}\n\n\tr, err := b.impl.Download(types.DownloadData{\n\t\tCtx:     ctx,\n\t\tObject:  b.toCloudObject(object),\n\t\tVersion: opt.version,\n\t})\n\treturn &Reader{r: r, err: err, curr: curr, startEventID: startEventID}\n}\n\n// Reader is the reader for an object being downloaded from a bucket.\ntype Reader struct {\n\terr       error // any error encountered\n\tr         types.Downloader\n\ttotalRead uint64\n\n\t// Set if traced\n\ttraceCompleted bool\n\tcurr           reqtrack.Current\n\tstartEventID   trace2.EventID\n}\n\n// Err returns the error encountered during reading, if any.\nfunc (r *Reader) Err() error {\n\treturn r.err\n}\n\n// Read reads data from the object being downloaded.\nfunc (r *Reader) Read(p []byte) (int, error) {\n\tif r.err != nil {\n\t\treturn 0, r.err\n\t}\n\n\tn, err := r.r.Read(p)\n\tr.err = err\n\tr.totalRead += uint64(n)\n\treturn n, err\n}\n\n// Close closes the reader.\n// It must be called to release resources.\nfunc (r *Reader) Close() error {\n\tdefer r.completeTrace()\n\tif r.err != nil {\n\t\treturn r.err\n\t}\n\n\tr.err = r.r.Close()\n\treturn r.err\n}\n\nfunc (r *Reader) completeTrace() {\n\tif r.traceCompleted {\n\t\treturn\n\t}\n\n\tr.traceCompleted = true\n\tif r.curr.Trace != nil && r.startEventID != 0 {\n\t\tr.curr.Trace.BucketObjectDownloadEnd(trace2.BucketObjectDownloadEndParams{\n\t\t\tStartID: r.startEventID,\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: r.curr.Req.TraceID,\n\t\t\t\tSpanID:  r.curr.Req.SpanID,\n\t\t\t\tGoid:    r.curr.Goctr,\n\t\t\t},\n\t\t\tErr:  r.err,\n\t\t\tSize: r.totalRead,\n\t\t})\n\t}\n}\n\n// Query describes the set of objects to query for using List.\ntype Query struct {\n\t// Prefix indicates to only return objects\n\t// whose name starts with the given prefix.\n\tPrefix string\n\n\t// Maximum number of objects to return. Zero means no limit.\n\tLimit int64\n}\n\nfunc (b *Bucket) mapQuery(ctx context.Context, q *Query) types.ListData {\n\treturn types.ListData{\n\t\tCtx:    ctx,\n\t\tPrefix: b.baseCloudPrefix + q.Prefix,\n\t\tLimit:  ptrOrNil(q.Limit),\n\t}\n}\n\n// ObjectAttrs describes the attributes of an object.\ntype ObjectAttrs struct {\n\t// The name of the object.\n\tName string\n\n\t// The version of the object, if bucket versioning is enabled.\n\tVersion string\n\n\t// The content type of the object, if set during upload.\n\tContentType string\n\n\t// The size of the object, in bytes.\n\tSize int64\n\n\t// The computed ETag of the object.\n\tETag string\n}\n\nfunc (b *Bucket) mapAttrs(attrs *types.ObjectAttrs) *ObjectAttrs {\n\treturn &ObjectAttrs{\n\t\tName:        b.fromCloudObject(attrs.Object),\n\t\tVersion:     attrs.Version,\n\t\tContentType: attrs.ContentType,\n\t\tSize:        attrs.Size,\n\t\tETag:        attrs.ETag,\n\t}\n}\n\n// ListEntry describes an objects during listing.\ntype ListEntry struct {\n\t// The name of the object.\n\tName string\n\t// The size of the object, in bytes.\n\tSize int64\n\t// The computed ETag of the object.\n\tETag string\n}\n\nfunc (b *Bucket) mapListEntry(entry *types.ListEntry) *ListEntry {\n\treturn &ListEntry{\n\t\tName: b.fromCloudObject(entry.Object),\n\t\tSize: entry.Size,\n\t\tETag: entry.ETag,\n\t}\n}\n\ntype SignedUploadURL struct {\n\t// The signed URL\n\tURL string\n}\n\ntype SignedDownloadURL struct {\n\t// The signed URL\n\tURL string\n}\n\n// List lists objects in the bucket.\nfunc (b *Bucket) List(ctx context.Context, query *Query, options ...ListOption) iter.Seq2[*ListEntry, error] {\n\treturn func(yield func(*ListEntry, error) bool) {\n\t\t// Tracing state\n\t\tvar (\n\t\t\tlistErr  error\n\t\t\tobserved uint64\n\t\t\thasMore  bool\n\t\t)\n\n\t\tcurr := b.mgr.rt.Current()\n\t\tif curr.Req != nil && curr.Trace != nil {\n\t\t\tstartEventID := curr.Trace.BucketListObjectsStart(trace2.BucketListObjectsStartParams{\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\t},\n\t\t\t\tBucket: b.name,\n\t\t\t\tPrefix: ptrOrNil(query.Prefix),\n\t\t\t\tStack:  stack.Build(1),\n\t\t\t})\n\n\t\t\tdefer curr.Trace.BucketListObjectsEnd(trace2.BucketListObjectsEndParams{\n\t\t\t\tStartID: startEventID,\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\t},\n\t\t\t\tErr:      listErr,\n\t\t\t\tObserved: observed,\n\t\t\t\tHasMore:  hasMore,\n\t\t\t})\n\t\t}\n\n\t\titer := b.impl.List(b.mapQuery(ctx, query))\n\t\tfor entry, err := range iter {\n\t\t\tif err != nil {\n\t\t\t\tlistErr = err\n\t\t\t\tif !yield(nil, err) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tobserved++\n\t\t\tif !yield(b.mapListEntry(entry), nil) {\n\t\t\t\t// Consumer didn't want any more entries; set hasMore = true\n\t\t\t\thasMore = true\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Remove removes an object from the bucket.\nfunc (b *Bucket) Remove(ctx context.Context, object string, options ...RemoveOption) error {\n\tvar opts removeOptions\n\tfor _, o := range options {\n\t\to.applyRemove(&opts)\n\t}\n\n\tvar removeErr error\n\tcurr := b.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tstartEventID := curr.Trace.BucketDeleteObjectsStart(trace2.BucketDeleteObjectsStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tBucket: b.name,\n\t\t\tObjects: []trace2.BucketDeleteObjectsEntry{\n\t\t\t\t{\n\t\t\t\t\tObject:  object,\n\t\t\t\t\tVersion: ptrOrNil(opts.version),\n\t\t\t\t},\n\t\t\t},\n\t\t\tStack: stack.Build(1),\n\t\t})\n\n\t\tdefer curr.Trace.BucketDeleteObjectsEnd(trace2.BucketDeleteObjectsEndParams{\n\t\t\tStartID: startEventID,\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tErr: removeErr,\n\t\t})\n\t}\n\n\tremoveErr = b.impl.Remove(types.RemoveData{\n\t\tCtx:     ctx,\n\t\tObject:  b.toCloudObject(object),\n\t\tVersion: opts.version,\n\t})\n\n\treturn removeErr\n}\n\nvar (\n\t// ErrObjectNotFound is returned when requested object does not exist in the bucket.\n\tErrObjectNotFound = types.ErrObjectNotExist\n\n\t// ErrPreconditionFailed is returned when a precondition for an operation is not met,\n\t// such as when an object already exists and Preconditions.NotExists is true.\n\tErrPreconditionFailed = types.ErrPreconditionFailed\n\n\t// ErrInvalidArgument is returned when an argument for an operation is invalid or out\n\t// of bounds. Such as when a too long time-to-live is passed to a sign URL operation.\n\tErrInvalidArgument = types.ErrInvalidArgument\n)\n\n// Attrs returns the attributes of an object in the bucket.\n// If the object does not exist, it returns ErrObjectNotFound.\nfunc (b *Bucket) Attrs(ctx context.Context, object string, options ...AttrsOption) (*ObjectAttrs, error) {\n\tvar opt attrsOptions\n\tfor _, o := range options {\n\t\to.applyAttrs(&opt)\n\t}\n\n\tvar (\n\t\tattrs    *types.ObjectAttrs\n\t\tattrsErr error\n\t)\n\n\tcurr := b.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tstartEventID := curr.Trace.BucketObjectGetAttrsStart(trace2.BucketObjectGetAttrsStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tBucket:  b.name,\n\t\t\tObject:  object,\n\t\t\tVersion: ptrOrNil(opt.version),\n\t\t\tStack:   stack.Build(1),\n\t\t})\n\n\t\tdefer func() {\n\t\t\tparams := trace2.BucketObjectGetAttrsEndParams{\n\t\t\t\tStartID: startEventID,\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\t},\n\t\t\t\tErr: attrsErr,\n\t\t\t}\n\t\t\tif attrs != nil {\n\t\t\t\tsize := uint64(attrs.Size)\n\t\t\t\tparams.Attrs = &trace2.BucketObjectAttributes{\n\t\t\t\t\tSize:        &size,\n\t\t\t\t\tVersion:     ptrOrNil(attrs.Version),\n\t\t\t\t\tETag:        ptrOrNil(attrs.ETag),\n\t\t\t\t\tContentType: ptrOrNil(attrs.ContentType),\n\t\t\t\t}\n\t\t\t}\n\t\t\tcurr.Trace.BucketObjectGetAttrsEnd(params)\n\t\t}()\n\t}\n\n\tattrs, attrsErr = b.impl.Attrs(types.AttrsData{\n\t\tCtx:     ctx,\n\t\tObject:  b.toCloudObject(object),\n\t\tVersion: opt.version,\n\t})\n\tif attrsErr != nil {\n\t\treturn nil, attrsErr\n\t}\n\n\treturn b.mapAttrs(attrs), nil\n}\n\n// Generates an external URL to allow uploading an object to the bucket.\n//\n// Anyone with possession of the URL can write to the given object name\n// without any additional auth.\nfunc (b *Bucket) SignedUploadURL(ctx context.Context, object string, options ...UploadURLOption) (*SignedUploadURL, error) {\n\tvar opt uploadURLOptions\n\tfor _, o := range options {\n\t\to.applyUploadURL(&opt)\n\t}\n\tif opt.TTL == 0 {\n\t\topt.TTL = time.Hour\n\t}\n\tif opt.TTL > 7*24*time.Hour {\n\t\treturn nil, types.ErrInvalidArgument\n\t}\n\turl, err := b.impl.SignedUploadURL(types.UploadURLData{\n\t\tCtx:    ctx,\n\t\tObject: b.toCloudObject(object),\n\t\tTTL:    opt.TTL,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &SignedUploadURL{URL: url}, nil\n}\n\n// Generates an external URL to allow downloading an object from the bucket.\n//\n// Anyone with possession of the URL can read the given object\n// without any additional auth.\nfunc (b *Bucket) SignedDownloadURL(ctx context.Context, object string, options ...DownloadURLOption) (*SignedDownloadURL, error) {\n\tvar opt downloadURLOptions\n\tfor _, o := range options {\n\t\to.applyDownloadURL(&opt)\n\t}\n\tif opt.TTL == 0 {\n\t\topt.TTL = time.Hour\n\t}\n\tif opt.TTL > 7*24*time.Hour {\n\t\treturn nil, types.ErrInvalidArgument\n\t}\n\turl, err := b.impl.SignedDownloadURL(types.DownloadURLData{\n\t\tCtx:    ctx,\n\t\tObject: b.toCloudObject(object),\n\t\tTTL:    opt.TTL,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &SignedDownloadURL{URL: url}, nil\n}\n\n// Exists reports whether an object exists in the bucket.\nfunc (b *Bucket) Exists(ctx context.Context, object string, options ...ExistsOption) (bool, error) {\n\tvar opt existsOptions\n\tfor _, o := range options {\n\t\to.applyExists(&opt)\n\t}\n\n\tvar (\n\t\tattrs    *types.ObjectAttrs\n\t\tattrsErr error\n\t)\n\n\tcurr := b.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tstartEventID := curr.Trace.BucketObjectGetAttrsStart(trace2.BucketObjectGetAttrsStartParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t},\n\t\t\tBucket:  b.name,\n\t\t\tObject:  object,\n\t\t\tVersion: ptrOrNil(opt.version),\n\t\t\tStack:   stack.Build(1),\n\t\t})\n\n\t\tdefer func() {\n\t\t\tparams := trace2.BucketObjectGetAttrsEndParams{\n\t\t\t\tStartID: startEventID,\n\t\t\t\tEventParams: trace2.EventParams{\n\t\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\t},\n\t\t\t\tErr: attrsErr,\n\t\t\t}\n\t\t\tif attrs != nil {\n\t\t\t\tsize := uint64(attrs.Size)\n\t\t\t\tparams.Attrs = &trace2.BucketObjectAttributes{\n\t\t\t\t\tSize:        &size,\n\t\t\t\t\tVersion:     ptrOrNil(attrs.Version),\n\t\t\t\t\tETag:        ptrOrNil(attrs.ETag),\n\t\t\t\t\tContentType: ptrOrNil(attrs.ContentType),\n\t\t\t\t}\n\t\t\t}\n\t\t\tcurr.Trace.BucketObjectGetAttrsEnd(params)\n\t\t}()\n\t}\n\n\tattrs, attrsErr = b.impl.Attrs(types.AttrsData{\n\t\tCtx:     ctx,\n\t\tObject:  b.toCloudObject(object),\n\t\tVersion: opt.version,\n\t})\n\tif errors.Is(attrsErr, ErrObjectNotFound) {\n\t\treturn false, nil\n\t} else if attrsErr != nil {\n\t\treturn false, attrsErr\n\t}\n\treturn true, nil\n}\n\nfunc (b *Bucket) toCloudObject(object string) types.CloudObject {\n\treturn types.CloudObject(b.cloudPrefix() + object)\n}\n\n// cloudPrefix computes the cloud prefix to use.\n// It adds the current test name as a prefix when running tests, for test isolation.\nfunc (b *Bucket) cloudPrefix() string {\n\tprefix := b.baseCloudPrefix\n\n\tif b.mgr.static.Testing {\n\t\ttest := b.mgr.ts.CurrentTest()\n\t\tif prefix != \"\" {\n\t\t\tprefix += \"/\"\n\t\t}\n\t\tprefix += test.Name() + \"/__test__/\"\n\t}\n\n\treturn prefix\n}\n\nfunc (b *Bucket) fromCloudObject(object types.CloudObject) string {\n\treturn strings.TrimPrefix(string(object), b.cloudPrefix())\n}\n\nfunc ptrOrNil[V comparable](val V) *V {\n\tvar zero V\n\tif val != zero {\n\t\treturn &val\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/providers/gcs/bucket.go",
    "content": "package gcs\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"iter\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"google.golang.org/api/googleapi\"\n\t\"google.golang.org/api/iterator\"\n\t\"google.golang.org/api/option\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/storage/objects/internal/types\"\n)\n\ntype Manager struct {\n\tctx     context.Context\n\truntime *config.Runtime\n\tclients map[*config.BucketProvider]*storage.Client\n}\n\nfunc NewManager(ctx context.Context, runtime *config.Runtime) *Manager {\n\treturn &Manager{ctx: ctx, runtime: runtime, clients: make(map[*config.BucketProvider]*storage.Client)}\n}\n\ntype localSignOptions struct {\n\tbaseURL    string\n\taccessID   string\n\tprivateKey string\n}\n\ntype bucket struct {\n\tclient    *storage.Client\n\tcfg       *config.Bucket\n\thandle    *storage.BucketHandle\n\tlocalSign *localSignOptions\n}\n\nfunc (mgr *Manager) ProviderName() string { return \"gcs\" }\n\nfunc (mgr *Manager) Matches(cfg *config.BucketProvider) bool {\n\treturn cfg.GCS != nil\n}\n\nfunc (mgr *Manager) NewBucket(provider *config.BucketProvider, runtimeCfg *config.Bucket) types.BucketImpl {\n\tclient := mgr.clientForProvider(provider)\n\n\tlocalSign := localSignOptionsForProvider(provider)\n\thandle := client.Bucket(runtimeCfg.CloudName)\n\treturn &bucket{client, runtimeCfg, handle, localSign}\n}\n\nfunc (b *bucket) Download(data types.DownloadData) (types.Downloader, error) {\n\tobj := b.handle.Object(data.Object.String())\n\tif data.Version != \"\" {\n\t\tif gen, err := strconv.ParseInt(data.Version, 10, 64); err == nil {\n\t\t\tobj = obj.Generation(gen)\n\t\t}\n\t}\n\tr, err := obj.NewReader(data.Ctx)\n\treturn r, mapErr(err)\n}\n\nfunc (b *bucket) Upload(data types.UploadData) (types.Uploader, error) {\n\tctx, cancel := context.WithCancelCause(data.Ctx)\n\tobj := b.handle.Object(data.Object.String())\n\n\tif data.Pre.NotExists {\n\t\tobj = obj.If(storage.Conditions{\n\t\t\tDoesNotExist: true,\n\t\t})\n\t}\n\n\tw := obj.NewWriter(ctx)\n\tw.ContentType = data.Attrs.ContentType\n\n\tu := &uploader{\n\t\tcancel: cancel,\n\t\tw:      w,\n\t}\n\treturn u, nil\n}\n\ntype uploader struct {\n\tcancel context.CancelCauseFunc\n\tw      *storage.Writer\n}\n\nfunc (u *uploader) Write(p []byte) (int, error) {\n\tn, err := u.w.Write(p)\n\treturn n, mapErr(err)\n}\n\nfunc (u *uploader) Complete() (*types.ObjectAttrs, error) {\n\tif err := u.w.Close(); err != nil {\n\t\treturn nil, mapErr(err)\n\t}\n\n\tattrs := u.w.Attrs()\n\treturn mapAttrs(attrs), nil\n}\n\nfunc (u *uploader) Abort(err error) {\n\tu.cancel(err)\n}\n\nfunc mapAttrs(attrs *storage.ObjectAttrs) *types.ObjectAttrs {\n\tif attrs == nil {\n\t\treturn nil\n\t}\n\treturn &types.ObjectAttrs{\n\t\tObject:      types.CloudObject(attrs.Name),\n\t\tVersion:     strconv.FormatInt(attrs.Generation, 10),\n\t\tContentType: attrs.ContentType,\n\t\tSize:        attrs.Size,\n\t\tETag:        attrs.Etag,\n\t}\n}\n\nfunc mapListEntry(attrs *storage.ObjectAttrs) *types.ListEntry {\n\treturn &types.ListEntry{\n\t\tObject: types.CloudObject(attrs.Name),\n\t\tSize:   attrs.Size,\n\t\tETag:   attrs.Etag,\n\t}\n}\n\nfunc (b *bucket) List(data types.ListData) iter.Seq2[*types.ListEntry, error] {\n\titer := b.handle.Objects(data.Ctx, &storage.Query{\n\t\tPrefix: data.Prefix,\n\t})\n\tvar n int64\n\treturn func(yield func(*types.ListEntry, error) bool) {\n\t\tfor {\n\t\t\tres, err := iter.Next()\n\t\t\tif err == iterator.Done {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Are we over the limit?\n\t\t\tif data.Limit != nil && n >= *data.Limit {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tn++\n\n\t\t\tvar entry *types.ListEntry\n\t\t\tif res != nil {\n\t\t\t\tentry = mapListEntry(res)\n\t\t\t}\n\n\t\t\tif !yield(entry, mapErr(err)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (b *bucket) Remove(data types.RemoveData) error {\n\tobj := b.handle.Object(data.Object.String())\n\n\tif data.Version != \"\" {\n\t\tif gen, err := strconv.ParseInt(data.Version, 10, 64); err == nil {\n\t\t\tobj = obj.Generation(gen)\n\t\t}\n\t}\n\n\terr := obj.Delete(data.Ctx)\n\treturn mapErr(err)\n}\n\nfunc (b *bucket) Attrs(data types.AttrsData) (*types.ObjectAttrs, error) {\n\tobj := b.handle.Object(data.Object.String())\n\n\tif data.Version != \"\" {\n\t\tif gen, err := strconv.ParseInt(data.Version, 10, 64); err == nil {\n\t\t\tobj = obj.Generation(gen)\n\t\t}\n\t}\n\n\tresp, err := obj.Attrs(data.Ctx)\n\treturn mapAttrs(resp), mapErr(err)\n}\n\nfunc (b *bucket) SignedUploadURL(data types.UploadURLData) (string, error) {\n\topts := &storage.SignedURLOptions{\n\t\tScheme:  storage.SigningSchemeV4,\n\t\tMethod:  \"PUT\",\n\t\tExpires: time.Now().Add(data.TTL),\n\t}\n\treturn b.signedURL(data.Object.String(), opts)\n}\n\nfunc (b *bucket) SignedDownloadURL(data types.DownloadURLData) (string, error) {\n\topts := &storage.SignedURLOptions{\n\t\tScheme:  storage.SigningSchemeV4,\n\t\tMethod:  \"GET\",\n\t\tExpires: time.Now().Add(data.TTL),\n\t}\n\treturn b.signedURL(data.Object.String(), opts)\n}\n\nfunc (b *bucket) signedURL(object string, opts *storage.SignedURLOptions) (string, error) {\n\t// We use a fake GCS service for local development. Ideally, the runtime\n\t// code would be oblivious to this once the GCS client is set up. But that\n\t// turns out to be difficult for URL signing, so we add a special case\n\t// here.\n\tif b.localSign != nil {\n\t\topts.GoogleAccessID = b.localSign.accessID\n\t\topts.PrivateKey = []byte(b.localSign.privateKey)\n\t}\n\n\turl, err := b.handle.SignedURL(object, opts)\n\tif err != nil {\n\t\treturn \"\", mapErr(err)\n\t}\n\n\t// More special handling for the local dev case.\n\tif b.localSign != nil {\n\t\turl = replaceURLPrefix(url, b.localSign.baseURL)\n\t}\n\n\treturn url, nil\n}\n\nfunc replaceURLPrefix(origUrl string, base string) string {\n\tu, err := url.Parse(origUrl)\n\tif err != nil {\n\t\treturn origUrl // If the input URL is not valid, just return it as-is\n\t}\n\tout := base\n\tif u.Path != \"\" {\n\t\tout = strings.TrimRight(out, \"/\") + \"/\" + strings.TrimLeft(u.Path, \"/\")\n\t}\n\tif u.RawQuery != \"\" {\n\t\tout += \"?\" + u.RawQuery\n\t}\n\treturn out\n}\n\nfunc (mgr *Manager) clientForProvider(prov *config.BucketProvider) *storage.Client {\n\tif client, ok := mgr.clients[prov]; ok {\n\t\treturn client\n\t}\n\n\tvar opts []option.ClientOption\n\tif prov.GCS.Anonymous {\n\t\topts = append(opts, option.WithoutAuthentication())\n\t}\n\tif prov.GCS.Endpoint != \"\" {\n\t\topts = append(opts, option.WithEndpoint(prov.GCS.Endpoint))\n\t}\n\n\tclient, err := storage.NewClient(mgr.ctx, opts...)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to create object storage client: %s\", err))\n\t}\n\n\tmgr.clients[prov] = client\n\treturn client\n}\n\nfunc localSignOptionsForProvider(prov *config.BucketProvider) *localSignOptions {\n\tif opts := prov.GCS.LocalSign; opts == nil {\n\t\treturn nil\n\t} else {\n\t\treturn &localSignOptions{\n\t\t\tbaseURL:    opts.BaseURL,\n\t\t\taccessID:   opts.AccessID,\n\t\t\tprivateKey: opts.PrivateKey,\n\t\t}\n\t}\n}\n\nfunc mapErr(err error) error {\n\tswitch {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.Is(err, storage.ErrObjectNotExist):\n\t\treturn types.ErrObjectNotExist\n\tdefault:\n\t\t// Handle precondition failures\n\t\t{\n\t\t\tvar e *googleapi.Error\n\t\t\tif ok := errors.As(err, &e); ok && e.Code == http.StatusPreconditionFailed {\n\t\t\t\treturn types.ErrPreconditionFailed\n\t\t\t}\n\t\t}\n\n\t\t{\n\t\t\tif s, ok := status.FromError(err); ok && s.Code() == codes.AlreadyExists || s.Code() == codes.FailedPrecondition {\n\t\t\t\treturn types.ErrPreconditionFailed\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/providers/noop/noop.go",
    "content": "package noop\n\nimport (\n\t\"fmt\"\n\t\"iter\"\n\n\t\"encore.dev/storage/objects/internal/types\"\n)\n\ntype BucketImpl struct {\n\tEncoreName string\n}\n\nfunc (b *BucketImpl) Download(data types.DownloadData) (types.Downloader, error) {\n\treturn nil, fmt.Errorf(\"cannot download from noop bucket\")\n}\n\nfunc (b *BucketImpl) Upload(data types.UploadData) (types.Uploader, error) {\n\treturn nil, fmt.Errorf(\"cannot upload to noop bucket\")\n}\n\nfunc (b *BucketImpl) List(data types.ListData) iter.Seq2[*types.ListEntry, error] {\n\treturn func(yield func(*types.ListEntry, error) bool) {\n\t\tyield(nil, fmt.Errorf(\"cannot list objects from noop bucket\"))\n\t}\n}\n\nfunc (b *BucketImpl) Remove(data types.RemoveData) error {\n\treturn fmt.Errorf(\"cannot remove from noop bucket\")\n}\n\nfunc (b *BucketImpl) Attrs(data types.AttrsData) (*types.ObjectAttrs, error) {\n\treturn nil, fmt.Errorf(\"cannot get attributes from noop bucket\")\n}\n\nfunc (b *BucketImpl) SignedUploadURL(data types.UploadURLData) (string, error) {\n\treturn \"\", fmt.Errorf(\"cannot get upload url from noop bucket\")\n}\n\nfunc (b *BucketImpl) SignedDownloadURL(data types.DownloadURLData) (string, error) {\n\treturn \"\", fmt.Errorf(\"cannot get download url from noop bucket\")\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/providers/s3/bucket.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"iter\"\n\t\"sync\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\tawsConfig \"github.com/aws/aws-sdk-go-v2/config\"\n\tawsCreds \"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\ts3types \"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/aws/smithy-go\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/storage/objects/internal/types\"\n)\n\ntype Manager struct {\n\tctx     context.Context\n\truntime *config.Runtime\n\tclients map[*config.BucketProvider]*clientSet\n\n\tcfgOnce          sync.Once\n\tawsDefaultConfig aws.Config\n}\n\nfunc NewManager(ctx context.Context, runtime *config.Runtime) *Manager {\n\treturn &Manager{ctx: ctx, runtime: runtime, clients: make(map[*config.BucketProvider]*clientSet)}\n}\n\ntype bucket struct {\n\tclient        *s3.Client\n\tpresignClient *s3.PresignClient\n\tcfg           *config.Bucket\n}\n\ntype clientSet struct {\n\tclient        *s3.Client\n\tpresignClient *s3.PresignClient\n}\n\nfunc (mgr *Manager) ProviderName() string { return \"s3\" }\n\nfunc (mgr *Manager) Matches(cfg *config.BucketProvider) bool {\n\treturn cfg.S3 != nil\n}\n\nfunc (mgr *Manager) NewBucket(provider *config.BucketProvider, runtimeCfg *config.Bucket) types.BucketImpl {\n\tclients := mgr.clientForProvider(provider)\n\treturn &bucket{\n\t\tclient:        clients.client,\n\t\tpresignClient: clients.presignClient,\n\t\tcfg:           runtimeCfg,\n\t}\n}\n\nfunc (b *bucket) Download(data types.DownloadData) (types.Downloader, error) {\n\tobject := string(data.Object)\n\tresp, err := b.client.GetObject(data.Ctx, &s3.GetObjectInput{\n\t\tBucket:    &b.cfg.CloudName,\n\t\tKey:       &object,\n\t\tVersionId: ptrOrNil(data.Version),\n\t})\n\tif err != nil {\n\t\treturn nil, mapErr(err)\n\t}\n\treturn resp.Body, nil\n}\n\nfunc (b *bucket) Upload(data types.UploadData) (types.Uploader, error) {\n\treturn newUploader(b.client, b.cfg.CloudName, data), nil\n}\n\nfunc mapListEntry(attrs *storage.ObjectAttrs) *types.ListEntry {\n\treturn &types.ListEntry{\n\t\tObject: types.CloudObject(attrs.Name),\n\t\tSize:   attrs.Size,\n\t\tETag:   attrs.Etag,\n\t}\n}\n\nfunc (b *bucket) List(data types.ListData) iter.Seq2[*types.ListEntry, error] {\n\treturn func(yield func(*types.ListEntry, error) bool) {\n\t\tvar n int64\n\t\tvar continuationToken string\n\t\tfor data.Limit == nil || n < *data.Limit {\n\t\t\t// Abort early if the context is canceled.\n\t\t\tif err := data.Ctx.Err(); err != nil {\n\t\t\t\tyield(nil, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmaxKeys := int32(1000)\n\t\t\tif data.Limit != nil {\n\t\t\t\tmaxKeys = min(int32(*data.Limit-n), 1000)\n\t\t\t}\n\t\t\tresp, err := b.client.ListObjectsV2(data.Ctx, &s3.ListObjectsV2Input{\n\t\t\t\tBucket:            &b.cfg.CloudName,\n\t\t\t\tMaxKeys:           &maxKeys,\n\t\t\t\tContinuationToken: ptrOrNil(continuationToken),\n\t\t\t\tPrefix:            ptrOrNil(data.Prefix),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tyield(nil, mapErr(err))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, obj := range resp.Contents {\n\t\t\t\tif !yield(&types.ListEntry{\n\t\t\t\t\tObject: types.CloudObject(*obj.Key),\n\t\t\t\t\tSize:   *obj.Size,\n\t\t\t\t\tETag:   *obj.ETag,\n\t\t\t\t}, nil) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t}\n\n\t\t\t// Are we done?\n\t\t\tif !valOrZero(resp.IsTruncated) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcontinuationToken = valOrZero(resp.NextContinuationToken)\n\t\t}\n\t}\n}\n\nfunc (b *bucket) Remove(data types.RemoveData) error {\n\tobject := string(data.Object)\n\t_, err := b.client.DeleteObject(data.Ctx, &s3.DeleteObjectInput{\n\t\tBucket:    &b.cfg.CloudName,\n\t\tKey:       &object,\n\t\tVersionId: ptrOrNil(data.Version),\n\t})\n\treturn mapErr(err)\n}\n\nfunc (b *bucket) Attrs(data types.AttrsData) (*types.ObjectAttrs, error) {\n\tobject := string(data.Object)\n\tresp, err := b.client.HeadObject(data.Ctx, &s3.HeadObjectInput{\n\t\tBucket:    &b.cfg.CloudName,\n\t\tKey:       &object,\n\t\tVersionId: ptrOrNil(data.Version),\n\t})\n\tif err != nil {\n\t\treturn nil, mapErr(err)\n\t}\n\treturn &types.ObjectAttrs{\n\t\tObject:      data.Object,\n\t\tVersion:     valOrZero(resp.VersionId),\n\t\tContentType: valOrZero(resp.ContentType),\n\t\tSize:        valOrZero(resp.ContentLength),\n\t\tETag:        valOrZero(resp.ETag),\n\t}, nil\n}\n\nfunc (b *bucket) SignedUploadURL(data types.UploadURLData) (string, error) {\n\tobject := string(data.Object)\n\tparams := s3.PutObjectInput{\n\t\tBucket: &b.cfg.CloudName,\n\t\tKey:    &object,\n\t}\n\tsign_opts := func(opts *s3.PresignOptions) {\n\t\topts.Expires = data.TTL\n\t}\n\treq, err := b.presignClient.PresignPutObject(data.Ctx, &params, sign_opts)\n\tif err != nil {\n\t\treturn \"\", mapErr(err)\n\t}\n\t// TODO: add check/warn against unexpected method and headers\n\t// (we expect PUT and host:<> but nothing else.)\n\treturn req.URL, nil\n}\n\nfunc (b *bucket) SignedDownloadURL(data types.DownloadURLData) (string, error) {\n\tobject := string(data.Object)\n\tparams := s3.GetObjectInput{\n\t\tBucket: &b.cfg.CloudName,\n\t\tKey:    &object,\n\t}\n\tsign_opts := func(opts *s3.PresignOptions) {\n\t\topts.Expires = data.TTL\n\t}\n\treq, err := b.presignClient.PresignGetObject(data.Ctx, &params, sign_opts)\n\tif err != nil {\n\t\treturn \"\", mapErr(err)\n\t}\n\t// TODO: add check/warn against unexpected method and headers\n\t// (we expect PUT and host:<> but nothing else.)\n\treturn req.URL, nil\n}\n\nfunc (mgr *Manager) clientForProvider(prov *config.BucketProvider) *clientSet {\n\tif cs, ok := mgr.clients[prov]; ok {\n\t\treturn cs\n\t}\n\n\t// If we have a custom access key and secret, use them instead of the default config.\n\tvar cfg aws.Config\n\tif prov.S3.AccessKeyID != nil && prov.S3.SecretAccessKey != nil {\n\t\tvar err error\n\t\tcfg, err = awsConfig.LoadDefaultConfig(context.Background(),\n\t\t\tawsConfig.WithCredentialsProvider(awsCreds.NewStaticCredentialsProvider(*prov.S3.AccessKeyID, *prov.S3.SecretAccessKey, \"\")),\n\t\t)\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"unable to load AWS config: %v\", err))\n\t\t}\n\t} else {\n\t\tcfg = mgr.defaultConfig()\n\t}\n\n\tclient := s3.New(s3.Options{\n\t\tRegion:       prov.S3.Region,\n\t\tBaseEndpoint: prov.S3.Endpoint,\n\t\tCredentials:  cfg.Credentials,\n\t})\n\n\tclients := &clientSet{\n\t\tclient:        client,\n\t\tpresignClient: s3.NewPresignClient(client),\n\t}\n\n\tmgr.clients[prov] = clients\n\treturn clients\n}\n\n// defaultConfig loads the required AWS config to connect to AWS\nfunc (mgr *Manager) defaultConfig() aws.Config {\n\tmgr.cfgOnce.Do(func() {\n\t\tcfg, err := awsConfig.LoadDefaultConfig(context.Background())\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"unable to load AWS config: %v\", err))\n\t\t}\n\t\tmgr.awsDefaultConfig = cfg\n\t})\n\treturn mgr.awsDefaultConfig\n}\n\nfunc mapErr(err error) error {\n\tvar (\n\t\tnoSuchKey *s3types.NoSuchKey\n\t\tgeneric   smithy.APIError\n\t)\n\tswitch {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.As(err, &noSuchKey):\n\t\treturn types.ErrObjectNotExist\n\tcase errors.As(err, &generic):\n\t\tif generic.ErrorCode() == \"PreconditionFailed\" {\n\t\t\treturn types.ErrPreconditionFailed\n\t\t}\n\t\treturn err\n\tdefault:\n\t\treturn err\n\n\t}\n}\n\nfunc ptrOrNil[T comparable](val T) *T {\n\tvar zero T\n\tif val != zero {\n\t\treturn &val\n\t}\n\treturn nil\n}\n\nfunc valOrZero[T comparable](val *T) T {\n\tif val != nil {\n\t\treturn *val\n\t}\n\tvar zero T\n\treturn zero\n}\n\nfunc ptr[T any](val T) *T {\n\treturn &val\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/providers/s3/mock_client_test.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: ./uploader.go\n\n// Package s3 is a generated GoMock package.\npackage s3\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\ts3 \"github.com/aws/aws-sdk-go-v2/service/s3\"\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// Mocks3Client is a mock of s3Client interface.\ntype Mocks3Client struct {\n\tctrl     *gomock.Controller\n\trecorder *Mocks3ClientMockRecorder\n}\n\n// Mocks3ClientMockRecorder is the mock recorder for Mocks3Client.\ntype Mocks3ClientMockRecorder struct {\n\tmock *Mocks3Client\n}\n\n// NewMocks3Client creates a new mock instance.\nfunc NewMocks3Client(ctrl *gomock.Controller) *Mocks3Client {\n\tmock := &Mocks3Client{ctrl: ctrl}\n\tmock.recorder = &Mocks3ClientMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *Mocks3Client) EXPECT() *Mocks3ClientMockRecorder {\n\treturn m.recorder\n}\n\n// AbortMultipartUpload mocks base method.\nfunc (m *Mocks3Client) AbortMultipartUpload(ctx context.Context, params *s3.AbortMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{ctx, params}\n\tfor _, a := range optFns {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"AbortMultipartUpload\", varargs...)\n\tret0, _ := ret[0].(*s3.AbortMultipartUploadOutput)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// AbortMultipartUpload indicates an expected call of AbortMultipartUpload.\nfunc (mr *Mocks3ClientMockRecorder) AbortMultipartUpload(ctx, params interface{}, optFns ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{ctx, params}, optFns...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"AbortMultipartUpload\", reflect.TypeOf((*Mocks3Client)(nil).AbortMultipartUpload), varargs...)\n}\n\n// CompleteMultipartUpload mocks base method.\nfunc (m *Mocks3Client) CompleteMultipartUpload(ctx context.Context, params *s3.CompleteMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{ctx, params}\n\tfor _, a := range optFns {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"CompleteMultipartUpload\", varargs...)\n\tret0, _ := ret[0].(*s3.CompleteMultipartUploadOutput)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CompleteMultipartUpload indicates an expected call of CompleteMultipartUpload.\nfunc (mr *Mocks3ClientMockRecorder) CompleteMultipartUpload(ctx, params interface{}, optFns ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{ctx, params}, optFns...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CompleteMultipartUpload\", reflect.TypeOf((*Mocks3Client)(nil).CompleteMultipartUpload), varargs...)\n}\n\n// CreateMultipartUpload mocks base method.\nfunc (m *Mocks3Client) CreateMultipartUpload(ctx context.Context, params *s3.CreateMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{ctx, params}\n\tfor _, a := range optFns {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"CreateMultipartUpload\", varargs...)\n\tret0, _ := ret[0].(*s3.CreateMultipartUploadOutput)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CreateMultipartUpload indicates an expected call of CreateMultipartUpload.\nfunc (mr *Mocks3ClientMockRecorder) CreateMultipartUpload(ctx, params interface{}, optFns ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{ctx, params}, optFns...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CreateMultipartUpload\", reflect.TypeOf((*Mocks3Client)(nil).CreateMultipartUpload), varargs...)\n}\n\n// PutObject mocks base method.\nfunc (m *Mocks3Client) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{ctx, params}\n\tfor _, a := range optFns {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"PutObject\", varargs...)\n\tret0, _ := ret[0].(*s3.PutObjectOutput)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// PutObject indicates an expected call of PutObject.\nfunc (mr *Mocks3ClientMockRecorder) PutObject(ctx, params interface{}, optFns ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{ctx, params}, optFns...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PutObject\", reflect.TypeOf((*Mocks3Client)(nil).PutObject), varargs...)\n}\n\n// UploadPart mocks base method.\nfunc (m *Mocks3Client) UploadPart(ctx context.Context, params *s3.UploadPartInput, optFns ...func(*s3.Options)) (*s3.UploadPartOutput, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{ctx, params}\n\tfor _, a := range optFns {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"UploadPart\", varargs...)\n\tret0, _ := ret[0].(*s3.UploadPartOutput)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UploadPart indicates an expected call of UploadPart.\nfunc (mr *Mocks3ClientMockRecorder) UploadPart(ctx, params interface{}, optFns ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{ctx, params}, optFns...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UploadPart\", reflect.TypeOf((*Mocks3Client)(nil).UploadPart), varargs...)\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/providers/s3/uploader.go",
    "content": "package s3\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"encore.dev/storage/objects/internal/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype uploader struct {\n\tclient s3Client\n\tbucket string\n\tdata   types.UploadData\n\tctx    context.Context\n\tout    chan uploadEvent\n\n\tinit  sync.Once\n\tdone  chan struct{}\n\tattrs *types.ObjectAttrs\n\terr   error\n\n\tcurr *buffer\n}\n\ntype uploadEvent struct {\n\tdata  *buffer\n\tabort error\n\tdone  bool\n}\n\ntype buffer struct {\n\tbuf []byte\n\tn   int // number of bytes in buf\n}\n\nfunc newUploader(client s3Client, bucket string, data types.UploadData) *uploader {\n\treturn &uploader{\n\t\tbucket: bucket,\n\t\tclient: client,\n\t\tctx:    data.Ctx,\n\t\tdata:   data,\n\t\tout:    make(chan uploadEvent, 10),\n\t\tdone:   make(chan struct{}),\n\t}\n}\n\nfunc (u *uploader) Write(p []byte) (n int, err error) {\n\tu.initUpload()\n\tfor len(p) > 0 {\n\t\tcurr := u.curr\n\t\tif curr == nil {\n\t\t\tcurr = getBuf()\n\t\t}\n\n\t\tcopied := copy(curr.buf[curr.n:], p)\n\t\tn += copied\n\t\tcurr.n += copied\n\n\t\tif copied < len(p) {\n\t\t\t// Buffer is full, send it.\n\t\t\tp = p[copied:]\n\t\t\tselect {\n\t\t\tcase u.out <- uploadEvent{data: curr}:\n\t\t\tcase <-u.done:\n\t\t\t\treturn n, u.err\n\t\t\t}\n\n\t\t\tu.curr, curr = nil, nil\n\t\t} else {\n\t\t\t// We've written all the data. Keep track\n\t\t\t// of the buffer for the next call.\n\t\t\tu.curr = curr\n\t\t\treturn n, nil\n\t\t}\n\t}\n\n\treturn n, nil\n}\n\nfunc (u *uploader) Complete() (*types.ObjectAttrs, error) {\n\tu.initUpload()\n\t// If we have a current buffer, send it.\n\tif curr := u.curr; curr != nil && curr.n > 0 {\n\t\tselect {\n\t\tcase u.out <- uploadEvent{data: curr, done: true}:\n\t\tcase <-u.done:\n\t\t}\n\t\tu.curr = nil\n\t} else {\n\t\tselect {\n\t\tcase u.out <- uploadEvent{done: true}:\n\t\tcase <-u.done:\n\t\t}\n\t}\n\n\t// Wait for the upload to finish.\n\t<-u.done\n\treturn u.attrs, u.err\n}\n\nfunc (u *uploader) Abort(err error) {\n\tu.initUpload()\n\t// Ensure err is non-nil\n\tif err == nil {\n\t\terr = errors.New(\"upload aborted\")\n\t}\n\n\tselect {\n\tcase u.out <- uploadEvent{abort: err}:\n\tcase <-u.done:\n\t}\n}\n\nfunc (u *uploader) initUpload() {\n\tu.init.Do(func() {\n\t\tgo func() {\n\t\t\tdefer close(u.done)\n\t\t\tattrs, err := u.doUpload()\n\t\t\tu.attrs, u.err = attrs, mapErr(err)\n\t\t}()\n\t})\n}\n\nfunc (u *uploader) doUpload() (*types.ObjectAttrs, error) {\n\tev := <-u.out\n\tif ev.abort != nil {\n\t\t// Nothing to do.\n\t\treturn nil, ev.abort\n\t} else if ev.done {\n\t\t// First buffer is the final one; we can do a single-part upload.\n\t\tvar buf []byte\n\t\tif ev.data != nil {\n\t\t\tbuf = ev.data.buf[:ev.data.n]\n\t\t}\n\t\treturn u.singlePartUpload(buf)\n\t}\n\n\treturn u.multiPartUpload(ev.data)\n}\n\ntype s3Client interface {\n\tPutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)\n\tCreateMultipartUpload(ctx context.Context, params *s3.CreateMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error)\n\tUploadPart(ctx context.Context, params *s3.UploadPartInput, optFns ...func(*s3.Options)) (*s3.UploadPartOutput, error)\n\tCompleteMultipartUpload(ctx context.Context, params *s3.CompleteMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error)\n\tAbortMultipartUpload(ctx context.Context, params *s3.AbortMultipartUploadInput, optFns ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error)\n}\n\nfunc (u *uploader) singlePartUpload(buf []byte) (*types.ObjectAttrs, error) {\n\tkey := ptr(u.data.Object.String())\n\tmd5sum := md5.Sum(buf)\n\tcontentMD5 := base64.StdEncoding.EncodeToString(md5sum[:])\n\n\tvar ifNoneMatch *string\n\tif u.data.Pre.NotExists {\n\t\tifNoneMatch = ptr(\"*\")\n\t}\n\n\tresp, err := u.client.PutObject(u.ctx, &s3.PutObjectInput{\n\t\tBucket:        &u.bucket,\n\t\tKey:           key,\n\t\tBody:          bytes.NewReader(buf),\n\t\tContentType:   ptrOrNil(u.data.Attrs.ContentType),\n\t\tContentMD5:    &contentMD5,\n\t\tContentLength: ptr(int64(len(buf))),\n\t\tIfNoneMatch:   ifNoneMatch,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &types.ObjectAttrs{\n\t\tObject:      u.data.Object,\n\t\tVersion:     valOrZero(resp.VersionId),\n\t\tContentType: u.data.Attrs.ContentType,\n\t\tSize:        int64(len(buf)),\n\t\tETag:        valOrZero(resp.ETag),\n\t}, nil\n}\n\nfunc (u *uploader) multiPartUpload(initial *buffer) (attrs *types.ObjectAttrs, err error) {\n\tkey := ptr(u.data.Object.String())\n\tresp, err := u.client.CreateMultipartUpload(u.ctx, &s3.CreateMultipartUploadInput{\n\t\tBucket:      &u.bucket,\n\t\tKey:         key,\n\t\tContentType: ptrOrNil(u.data.Attrs.ContentType),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuploadID := valOrZero(resp.UploadId)\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\t// The upload failed. Abort the multipart upload.\n\t\t\tgo func() {\n\t\t\t\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\t\t\t\tdefer cancel()\n\t\t\t\t_, _ = u.client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{\n\t\t\t\t\tBucket:   &u.bucket,\n\t\t\t\t\tKey:      key,\n\t\t\t\t\tUploadId: &uploadID,\n\t\t\t\t})\n\t\t\t}()\n\t\t}\n\t}()\n\n\tg, groupCtx := errgroup.WithContext(u.ctx)\n\tpartNumber := int32(1)\n\tvar totalSize int64\n\tuploadPart := func(buf *buffer) {\n\t\tif buf == nil {\n\t\t\t// No data to upload.\n\t\t\treturn\n\t\t}\n\n\t\ttotalSize += int64(buf.n)\n\t\tpart := partNumber\n\t\tpartNumber++\n\t\tg.Go(func() error {\n\t\t\tdata := buf.buf[:buf.n]\n\t\t\tdefer putBuf(buf)\n\n\t\t\tmd5sum := md5.Sum(data)\n\t\t\tcontentMD5 := base64.StdEncoding.EncodeToString(md5sum[:])\n\t\t\t_, err := u.client.UploadPart(groupCtx, &s3.UploadPartInput{\n\t\t\t\tBucket:        &u.bucket,\n\t\t\t\tUploadId:      &uploadID,\n\t\t\t\tPartNumber:    &part,\n\t\t\t\tBody:          bytes.NewReader(data),\n\t\t\t\tContentLength: ptr(int64(len(data))),\n\t\t\t\tContentMD5:    ptr(contentMD5),\n\t\t\t})\n\t\t\treturn err\n\t\t})\n\t}\n\n\t// Upload the first part, if given.\n\tuploadPart(initial)\n\tfor {\n\t\tev := <-u.out\n\t\tif ev.abort != nil {\n\t\t\treturn nil, ev.abort\n\t\t}\n\n\t\tif ev.data != nil {\n\t\t\tuploadPart(ev.data)\n\t\t}\n\n\t\tif ev.done {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Wait for the uploads to complete.\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Complete the multipart upload.\n\tvar ifNoneMatch *string\n\tif u.data.Pre.NotExists {\n\t\tifNoneMatch = ptr(\"*\")\n\t}\n\n\tvar completeResp *s3.CompleteMultipartUploadOutput\n\tcompleteResp, err = u.client.CompleteMultipartUpload(u.ctx, &s3.CompleteMultipartUploadInput{\n\t\tBucket:      &u.bucket,\n\t\tKey:         key,\n\t\tUploadId:    &uploadID,\n\t\tIfNoneMatch: ifNoneMatch,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &types.ObjectAttrs{\n\t\tObject:      u.data.Object,\n\t\tVersion:     valOrZero(completeResp.VersionId),\n\t\tContentType: u.data.Attrs.ContentType,\n\t\tSize:        totalSize,\n\t\tETag:        valOrZero(completeResp.ETag),\n\t}, nil\n}\n\n// bufSize is the size of buffers allocated by bufPool.\n// It's a variable for testing purposes.\nvar bufSize = 10 * 1024 * 1024\n\nvar bufPool = sync.Pool{\n\tNew: func() any {\n\t\treturn &buffer{\n\t\t\tbuf: make([]byte, bufSize),\n\t\t}\n\t},\n}\n\nfunc getBuf() *buffer {\n\tbuf := bufPool.Get().(*buffer)\n\tbuf.n = 0\n\treturn buf\n}\n\nfunc putBuf(buf *buffer) {\n\tbufPool.Put(buf)\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/providers/s3/uploader_test.go",
    "content": "package s3\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"encore.dev/storage/objects/internal/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/golang/mock/gomock\"\n)\n\n//go:generate mockgen -source=./uploader.go -destination ./mock_client_test.go -package s3 s3Client\n\nfunc TestUploader_Sync(t *testing.T) {\n\tc := qt.New(t)\n\n\tctrl := gomock.NewController(c)\n\tclient := NewMocks3Client(ctrl)\n\n\tconst (\n\t\tbucket      = \"bucket\"\n\t\tobject      = \"object\"\n\t\tcontentType = \"text/plain\"\n\t)\n\tu := newUploader(client, bucket, types.UploadData{\n\t\tCtx:    context.Background(),\n\t\tObject: object,\n\t\tAttrs: types.UploadAttrs{\n\t\t\tContentType: contentType,\n\t\t},\n\t\tPre: types.Preconditions{},\n\t})\n\n\tconst (\n\t\tversion = \"version\"\n\t\tetag    = \"etag\"\n\t)\n\tclient.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{\n\t\tVersionId: ptr(version),\n\t\tETag:      ptr(etag),\n\t}, nil)\n\n\tcontent := []byte(\"test\")\n\tn, err := u.Write(content)\n\tc.Assert(n, qt.Equals, 4)\n\tc.Assert(err, qt.Equals, nil)\n\n\tattrs, err := u.Complete()\n\tc.Assert(err, qt.Equals, nil)\n\tc.Assert(attrs, qt.DeepEquals, &types.ObjectAttrs{\n\t\tObject:      types.CloudObject(object),\n\t\tVersion:     version,\n\t\tContentType: contentType,\n\t\tETag:        etag,\n\t\tSize:        int64(len(content)),\n\t})\n}\n\nfunc TestUploader_MultipleWrites(t *testing.T) {\n\tc := qt.New(t)\n\n\tctrl := gomock.NewController(c)\n\tclient := NewMocks3Client(ctrl)\n\n\tconst (\n\t\tbucket      = \"bucket\"\n\t\tobject      = \"object\"\n\t\tcontentType = \"text/plain\"\n\t)\n\tu := newUploader(client, bucket, types.UploadData{\n\t\tCtx:    context.Background(),\n\t\tObject: object,\n\t\tAttrs: types.UploadAttrs{\n\t\t\tContentType: contentType,\n\t\t},\n\t\tPre: types.Preconditions{},\n\t})\n\n\tconst (\n\t\tversion = \"version\"\n\t\tetag    = \"etag\"\n\t)\n\tclient.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{\n\t\tVersionId: ptr(version),\n\t\tETag:      ptr(etag),\n\t}, nil)\n\n\tbaseContent := \"test\"\n\ttotalContent := strings.Repeat(baseContent, 10)\n\tfor i := 0; i < 10; i++ {\n\t\tn, err := u.Write([]byte(baseContent))\n\t\tc.Assert(n, qt.Equals, len(baseContent))\n\t\tc.Assert(err, qt.Equals, nil)\n\t}\n\n\tattrs, err := u.Complete()\n\tc.Assert(err, qt.Equals, nil)\n\tc.Assert(attrs, qt.DeepEquals, &types.ObjectAttrs{\n\t\tObject:      types.CloudObject(object),\n\t\tVersion:     version,\n\t\tContentType: contentType,\n\t\tETag:        etag,\n\t\tSize:        int64(len(totalContent)),\n\t})\n}\n\nfunc TestUploader_MultipartUpload(t *testing.T) {\n\tc := qt.New(t)\n\n\tctrl := gomock.NewController(c)\n\tclient := NewMocks3Client(ctrl)\n\n\tconst (\n\t\tbucket      = \"bucket\"\n\t\tobject      = \"object\"\n\t\tcontentType = \"text/plain\"\n\t)\n\tu := newUploader(client, bucket, types.UploadData{\n\t\tCtx:    context.Background(),\n\t\tObject: object,\n\t\tAttrs: types.UploadAttrs{\n\t\t\tContentType: contentType,\n\t\t},\n\t\tPre: types.Preconditions{},\n\t})\n\n\twithBufSize(c, 10)\n\tconst (\n\t\tversion  = \"version\"\n\t\tetag     = \"etag\"\n\t\tuploadID = \"uploadID\"\n\t)\n\tclient.EXPECT().CreateMultipartUpload(gomock.Any(), gomock.Any()).Return(&s3.CreateMultipartUploadOutput{\n\t\tUploadId: ptr(uploadID),\n\t}, nil)\n\tclient.EXPECT().UploadPart(gomock.Any(), &partMatcher{num: 1, data: \"abcdefghij\"}).Return(&s3.UploadPartOutput{}, nil)\n\tclient.EXPECT().UploadPart(gomock.Any(), &partMatcher{num: 2, data: \"klmabcdefg\"}).Return(&s3.UploadPartOutput{}, nil)\n\tclient.EXPECT().UploadPart(gomock.Any(), &partMatcher{num: 3, data: \"hijklmabcd\"}).Return(&s3.UploadPartOutput{}, nil)\n\tclient.EXPECT().UploadPart(gomock.Any(), &partMatcher{num: 4, data: \"efghijklm\"}).Return(&s3.UploadPartOutput{}, nil)\n\tclient.EXPECT().CompleteMultipartUpload(gomock.Any(), gomock.Any()).Return(&s3.CompleteMultipartUploadOutput{\n\t\tVersionId: ptr(version),\n\t\tETag:      ptr(etag),\n\t}, nil)\n\n\tbaseContent := \"abcdefghijklm\"\n\ttotalContent := strings.Repeat(baseContent, 3)\n\tfor i := 0; i < 3; i++ {\n\t\tn, err := u.Write([]byte(baseContent))\n\t\tc.Assert(n, qt.Equals, len(baseContent))\n\t\tc.Assert(err, qt.Equals, nil)\n\t}\n\n\tattrs, err := u.Complete()\n\tc.Assert(err, qt.Equals, nil)\n\tc.Assert(attrs, qt.DeepEquals, &types.ObjectAttrs{\n\t\tObject:      types.CloudObject(object),\n\t\tVersion:     version,\n\t\tContentType: contentType,\n\t\tETag:        etag,\n\t\tSize:        int64(len(totalContent)),\n\t})\n}\n\nfunc withBufSize(c *qt.C, n int) {\n\torig := bufSize\n\tbufSize = n\n\tc.Cleanup(func() { bufSize = orig })\n}\n\ntype partMatcher struct {\n\tnum  int\n\tdata string\n}\n\nfunc (m *partMatcher) Matches(x interface{}) bool {\n\tpart, ok := x.(*s3.UploadPartInput)\n\tif !ok {\n\t\treturn false\n\t}\n\tdata, err := io.ReadAll(part.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tpart.Body = bytes.NewReader(data)\n\tfmt.Printf(\"part %d: %q\\n\", valOrZero(part.PartNumber), data)\n\treturn valOrZero(part.PartNumber) == int32(m.num) && string(data) == m.data\n}\n\nfunc (m *partMatcher) String() string {\n\treturn fmt.Sprintf(\"is part %d with data %q\", m.num, m.data)\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/internal/types/types.go",
    "content": "package types\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"iter\"\n\t\"time\"\n)\n\ntype BucketImpl interface {\n\tUpload(data UploadData) (Uploader, error)\n\tDownload(data DownloadData) (Downloader, error)\n\tList(data ListData) iter.Seq2[*ListEntry, error]\n\tRemove(data RemoveData) error\n\tAttrs(data AttrsData) (*ObjectAttrs, error)\n\tSignedUploadURL(data UploadURLData) (string, error)\n\tSignedDownloadURL(data DownloadURLData) (string, error)\n}\n\n// CloudObject is the cloud name for an object.\n// It can differ from the logical name when using a prefix bucket.\ntype CloudObject string\n\nfunc (o CloudObject) String() string { return string(o) }\n\ntype UploadData struct {\n\tCtx    context.Context\n\tObject CloudObject\n\n\tAttrs UploadAttrs\n\tPre   Preconditions\n}\n\ntype Preconditions struct {\n\tNotExists bool\n}\n\ntype UploadAttrs struct {\n\tContentType string\n}\n\ntype Uploader interface {\n\tio.Writer\n\tAbort(err error)\n\tComplete() (*ObjectAttrs, error)\n}\n\ntype DownloadData struct {\n\tCtx    context.Context\n\tObject CloudObject\n\n\t// Non-zero to download a specific version\n\tVersion string\n}\n\ntype Downloader interface {\n\tio.Reader\n\tio.Closer\n}\n\ntype ObjectAttrs struct {\n\tObject      CloudObject\n\tVersion     string\n\tContentType string\n\tSize        int64\n\tETag        string\n}\n\ntype ListData struct {\n\tCtx    context.Context\n\tPrefix string\n\tLimit  *int64\n}\n\ntype ListEntry struct {\n\tObject CloudObject\n\tSize   int64\n\tETag   string\n}\n\ntype RemoveData struct {\n\tCtx    context.Context\n\tObject CloudObject\n\n\tVersion string // non-zero means specific version\n}\n\ntype AttrsData struct {\n\tCtx    context.Context\n\tObject CloudObject\n\n\tVersion string // non-zero means specific version\n}\n\ntype UploadURLData struct {\n\tCtx    context.Context\n\tObject CloudObject\n\n\tTTL time.Duration\n}\n\ntype DownloadURLData struct {\n\tCtx    context.Context\n\tObject CloudObject\n\n\tTTL time.Duration\n}\n\n//publicapigen:keep\nvar (\n\t//publicapigen:keep\n\tErrObjectNotExist = errors.New(\"objects: object doesn't exist\")\n\t//publicapigen:keep\n\tErrPreconditionFailed = errors.New(\"objects: precondition failed\")\n\t//publicapigen:keep\n\tErrInvalidArgument = errors.New(\"objects: invalid argument\")\n)\n"
  },
  {
    "path": "runtimes/go/storage/objects/manager_internal.go",
    "content": "package objects\n\nimport (\n\t\"context\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\ntype Manager struct {\n\tctx        context.Context\n\tcancelCtx  func()\n\tstatic     *config.Static\n\truntime    *config.Runtime\n\trt         *reqtrack.RequestTracker\n\tts         *testsupport.Manager\n\trootLogger zerolog.Logger\n\tproviders  []provider\n}\n\nfunc NewManager(static *config.Static, runtime *config.Runtime, rt *reqtrack.RequestTracker,\n\tts *testsupport.Manager, rootLogger zerolog.Logger) *Manager {\n\tctx, cancel := context.WithCancel(context.Background())\n\tmgr := &Manager{\n\t\tctx:        ctx,\n\t\tcancelCtx:  cancel,\n\t\tstatic:     static,\n\t\truntime:    runtime,\n\t\trt:         rt,\n\t\tts:         ts,\n\t\trootLogger: rootLogger,\n\t}\n\n\tfor _, p := range providerRegistry {\n\t\tmgr.providers = append(mgr.providers, p(mgr.ctx, mgr.runtime))\n\t}\n\n\treturn mgr\n}\n\n// Shutdown stops the manager from fetching new messages and processing them.\nfunc (mgr *Manager) Shutdown(p *shutdown.Process) error {\n\t// Once it's time to force-close tasks, cancel the base context.\n\tgo func() {\n\t\t<-p.ForceCloseTasks.Done()\n\t\tmgr.cancelCtx()\n\t}()\n\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/objects.go",
    "content": "//go:build encore_app\n\npackage objects\n\n// NewBucket declares a new object storage bucket.\n//\n// See https://encore.dev/docs/primitives/object-storage for more information.\nfunc NewBucket(name string, cfg BucketConfig) *Bucket {\n\treturn newBucket(Singleton, name)\n}\n\n// constStr is a string that can only be provided as a constant.\n//\n//publicapigen:keep\ntype constStr string\n\n// Named returns a database object connected to the database with the given name.\n//\n// The name must be a string literal constant, to facilitate static analysis.\nfunc Named(name constStr) *Bucket {\n\treturn newBucket(Singleton, string(name))\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/options.go",
    "content": "package objects\n\nimport (\n\t\"time\"\n\n\t\"encore.dev/storage/objects/internal/types\"\n)\n\n// DownloadOption describes available options for the Download operation.\ntype DownloadOption interface {\n\t//publicapigen:keep\n\tdownloadOption()\n\n\tapplyDownload(*downloadOptions)\n}\n\n// WithVersion specifies that the operation should be performed\n// against the provided version of the object.\nfunc WithVersion(version string) withVersionOption {\n\treturn withVersionOption{version: version}\n}\n\n//publicapigen:keep\ntype withVersionOption struct {\n\tversion string\n}\n\n//publicapigen:keep\nfunc (o withVersionOption) downloadOption() {}\n\n//publicapigen:keep\nfunc (o withVersionOption) removeOption() {}\n\n//publicapigen:keep\nfunc (o withVersionOption) attrsOption() {}\n\n//publicapigen:keep\nfunc (o withVersionOption) existsOption() {}\n\n//publicapigen:keep\nfunc (o withTTLOption) uploadURLOption() {}\n\n//publicapigen:keep\nfunc (o withTTLOption) downloadURLOption() {}\n\nfunc (o withVersionOption) applyDownload(opts *downloadOptions)   { opts.version = o.version }\nfunc (o withVersionOption) applyRemove(opts *removeOptions)       { opts.version = o.version }\nfunc (o withVersionOption) applyAttrs(opts *attrsOptions)         { opts.version = o.version }\nfunc (o withVersionOption) applyExists(opts *existsOptions)       { opts.version = o.version }\nfunc (o withTTLOption) applyUploadURL(opts *uploadURLOptions)     { opts.TTL = o.TTL }\nfunc (o withTTLOption) applyDownloadURL(opts *downloadURLOptions) { opts.TTL = o.TTL }\n\n// WithTTL is used for signed URLs, to specify the lifetime of the generated\n// URL. The max value is seven days. The default lifetime, if this\n// option is missing, is one hour.\nfunc WithTTL(TTL time.Duration) withTTLOption {\n\treturn withTTLOption{TTL: TTL}\n}\n\n//publicapigen:keep\ntype withTTLOption struct {\n\tTTL time.Duration\n}\n\n//publicapigen:keep\ntype downloadOptions struct {\n\tversion string\n}\n\n// UploadOption describes available options for the Upload operation.\ntype UploadOption interface {\n\tuploadOption()\n\n\tapplyUpload(*uploadOptions)\n}\n\n// WithPreconditions is an UploadOption for only uploading an object\n// if certain preconditions are met.\nfunc WithPreconditions(pre Preconditions) withPreconditionsOption {\n\treturn withPreconditionsOption{pre: pre}\n}\n\n// Preconditions are the available preconditions for an upload operation.\ntype Preconditions struct {\n\t// NotExists specifies that the object must not exist prior to uploading.\n\tNotExists bool\n}\n\n//publicapigen:keep\ntype withPreconditionsOption struct {\n\tpre Preconditions\n}\n\n//publicapigen:keep\nfunc (o withPreconditionsOption) uploadOption() {}\n\nfunc (o withPreconditionsOption) applyUpload(opts *uploadOptions) {\n\topts.pre = o.pre\n}\n\n// UploadAttrs specifies additional object attributes to set during upload.\ntype UploadAttrs struct {\n\t// ContentType specifies the content type of the object.\n\tContentType string\n}\n\n// WithUploadAttrs is an UploadOption for specifying additional object attributes\n// to set during upload.\nfunc WithUploadAttrs(attrs UploadAttrs) withUploadAttrsOption {\n\treturn withUploadAttrsOption{attrs: attrs}\n}\n\n//publicapigen:keep\ntype withUploadAttrsOption struct {\n\tattrs UploadAttrs\n}\n\n//publicapigen:keep\nfunc (o withUploadAttrsOption) uploadOption() {}\n\nfunc (o withUploadAttrsOption) applyUpload(opts *uploadOptions) {\n\topts.attrs = types.UploadAttrs{\n\t\tContentType: o.attrs.ContentType,\n\t}\n}\n\ntype uploadOptions struct {\n\tattrs types.UploadAttrs\n\tpre   Preconditions\n}\n\n// ListOption describes available options for the List operation.\ntype ListOption interface {\n\t//publicapigen:keep\n\tlistOption()\n\n\tapplyList(*listOptions)\n}\n\ntype listOptions struct{}\n\n// RemoveOption describes available options for the Remove operation.\ntype RemoveOption interface {\n\t//publicapigen:keep\n\tremoveOption()\n\n\tapplyRemove(*removeOptions)\n}\n\ntype removeOptions struct {\n\tversion string\n}\n\n// AttrsOption describes available options for the Attrs operation.\ntype AttrsOption interface {\n\t//publicapigen:keep\n\tattrsOption()\n\n\tapplyAttrs(*attrsOptions)\n}\n\ntype attrsOptions struct {\n\tversion string\n}\n\n// UploadURLOption describes available options for the SignedUploadURL operation.\ntype UploadURLOption interface {\n\t//publicapigen:keep\n\tuploadURLOption()\n\n\tapplyUploadURL(*uploadURLOptions)\n}\n\ntype uploadURLOptions struct {\n\tTTL time.Duration\n}\n\n// DownloadURLOption describes available options for the SignedDownloadURL operation.\ntype DownloadURLOption interface {\n\t//publicapigen:keep\n\tdownloadURLOption()\n\n\tapplyDownloadURL(*downloadURLOptions)\n}\n\ntype downloadURLOptions struct {\n\tTTL time.Duration\n}\n\n// ExistsOption describes available options for the Exists operation.\ntype ExistsOption interface {\n\t//publicapigen:keep\n\texistsOption()\n\n\tapplyExists(*existsOptions)\n}\n\ntype existsOptions struct {\n\tversion string\n}\n\n// PublicURLOption describes available options for the PublicURL operation.\ntype PublicURLOption interface {\n\t//publicapigen:keep\n\tpublicURLOption()\n\n\tapplyPublicURL(*publicURLOptions)\n}\n\n// No options yet\ntype publicURLOptions struct{}\n"
  },
  {
    "path": "runtimes/go/storage/objects/package.go",
    "content": "// Package objects provides Encore applications with the ability\n// to create and use Object Storage buckets (like for example Amazon S3)\n// for storing and retrieving files in a cloud-agnostic manner.\n//\n// For more information see https://encore.dev/docs/primitives/object-storage\npackage objects\n"
  },
  {
    "path": "runtimes/go/storage/objects/path_escape.go",
    "content": "package objects\n\n// The code below is copied from the Go standard library's net/url package.\n//\n// Copyright 2009 The Go Authors. All rights reserved.\n//\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file at\n// https://github.com/golang/go.\n\nconst upperhex = \"0123456789ABCDEF\"\n\ntype encoding int\n\nconst (\n\tencodePath encoding = 1 + iota\n\tencodePathSegment\n\tencodeHost\n\tencodeZone\n\tencodeUserPassword\n\tencodeQueryComponent\n\tencodeFragment\n)\n\nfunc escape(s string, mode encoding) string {\n\tspaceCount, hexCount := 0, 0\n\tfor i := 0; i < len(s); i++ {\n\t\tc := s[i]\n\t\tif shouldEscape(c, mode) {\n\t\t\tif c == ' ' && mode == encodeQueryComponent {\n\t\t\t\tspaceCount++\n\t\t\t} else {\n\t\t\t\thexCount++\n\t\t\t}\n\t\t}\n\t}\n\n\tif spaceCount == 0 && hexCount == 0 {\n\t\treturn s\n\t}\n\n\tvar buf [64]byte\n\tvar t []byte\n\n\trequired := len(s) + 2*hexCount\n\tif required <= len(buf) {\n\t\tt = buf[:required]\n\t} else {\n\t\tt = make([]byte, required)\n\t}\n\n\tif hexCount == 0 {\n\t\tcopy(t, s)\n\t\tfor i := 0; i < len(s); i++ {\n\t\t\tif s[i] == ' ' {\n\t\t\t\tt[i] = '+'\n\t\t\t}\n\t\t}\n\t\treturn string(t)\n\t}\n\n\tj := 0\n\tfor i := 0; i < len(s); i++ {\n\t\tswitch c := s[i]; {\n\t\tcase c == ' ' && mode == encodeQueryComponent:\n\t\t\tt[j] = '+'\n\t\t\tj++\n\t\tcase shouldEscape(c, mode):\n\t\t\tt[j] = '%'\n\t\t\tt[j+1] = upperhex[c>>4]\n\t\t\tt[j+2] = upperhex[c&15]\n\t\t\tj += 3\n\t\tdefault:\n\t\t\tt[j] = s[i]\n\t\t\tj++\n\t\t}\n\t}\n\treturn string(t)\n}\n\n// Return true if the specified character should be escaped when\n// appearing in a URL string, according to RFC 3986.\n//\n// Please be informed that for now shouldEscape does not check all\n// reserved characters correctly. See golang.org/issue/5684.\nfunc shouldEscape(c byte, mode encoding) bool {\n\t// §2.3 Unreserved characters (alphanum)\n\tif 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {\n\t\treturn false\n\t}\n\n\tif mode == encodeHost || mode == encodeZone {\n\t\t// §3.2.2 Host allows\n\t\t//\tsub-delims = \"!\" / \"$\" / \"&\" / \"'\" / \"(\" / \")\" / \"*\" / \"+\" / \",\" / \";\" / \"=\"\n\t\t// as part of reg-name.\n\t\t// We add : because we include :port as part of host.\n\t\t// We add [ ] because we include [ipv6]:port as part of host.\n\t\t// We add < > because they're the only characters left that\n\t\t// we could possibly allow, and Parse will reject them if we\n\t\t// escape them (because hosts can't use %-encoding for\n\t\t// ASCII bytes).\n\t\tswitch c {\n\t\tcase '!', '$', '&', '\\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '\"':\n\t\t\treturn false\n\t\t}\n\t}\n\n\tswitch c {\n\tcase '-', '_', '.', '~': // §2.3 Unreserved characters (mark)\n\t\treturn false\n\n\tcase '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)\n\t\t// Different sections of the URL allow a few of\n\t\t// the reserved characters to appear unescaped.\n\t\tswitch mode {\n\t\tcase encodePath: // §3.3\n\t\t\t// The RFC allows : @ & = + $ but saves / ; , for assigning\n\t\t\t// meaning to individual path segments. This package\n\t\t\t// only manipulates the path as a whole, so we allow those\n\t\t\t// last three as well. That leaves only ? to escape.\n\t\t\treturn c == '?'\n\n\t\tcase encodePathSegment: // §3.3\n\t\t\t// The RFC allows : @ & = + $ but saves / ; , for assigning\n\t\t\t// meaning to individual path segments.\n\t\t\treturn c == '/' || c == ';' || c == ',' || c == '?'\n\n\t\tcase encodeUserPassword: // §3.2.1\n\t\t\t// The RFC allows ';', ':', '&', '=', '+', '$', and ',' in\n\t\t\t// userinfo, so we must escape only '@', '/', and '?'.\n\t\t\t// The parsing of userinfo treats ':' as special so we must escape\n\t\t\t// that too.\n\t\t\treturn c == '@' || c == '/' || c == '?' || c == ':'\n\n\t\tcase encodeQueryComponent: // §3.4\n\t\t\t// The RFC reserves (so we must escape) everything.\n\t\t\treturn true\n\n\t\tcase encodeFragment: // §4.1\n\t\t\t// The RFC text is silent but the grammar allows\n\t\t\t// everything, so escape nothing.\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif mode == encodeFragment {\n\t\t// RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are\n\t\t// included in reserved from RFC 2396 §2.2. The remaining sub-delims do not\n\t\t// need to be escaped. To minimize potential breakage, we apply two restrictions:\n\t\t// (1) we always escape sub-delims outside of the fragment, and (2) we always\n\t\t// escape single quote to avoid breaking callers that had previously assumed that\n\t\t// single quotes would be escaped. See issue #19917.\n\t\tswitch c {\n\t\tcase '!', '(', ')', '*':\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Everything else must be escaped.\n\treturn true\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/provider_gcs.go",
    "content": "//go:build !encore_no_gcp || !encore_no_encorecloud || !encore_no_local\n\npackage objects\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/storage/objects/internal/providers/gcs\"\n)\n\nfunc init() {\n\tregisterProvider(func(ctx context.Context, runtimeCfg *config.Runtime) provider {\n\t\treturn gcs.NewManager(ctx, runtimeCfg)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/provider_s3.go",
    "content": "//go:build !encore_no_aws\n\npackage objects\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/storage/objects/internal/providers/s3\"\n)\n\nfunc init() {\n\tregisterProvider(func(ctx context.Context, runtimeCfg *config.Runtime) provider {\n\t\treturn s3.NewManager(ctx, runtimeCfg)\n\t})\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/refs.go",
    "content": "package objects\n\nimport (\n\t\"context\"\n\t\"iter\"\n\t\"net/url\"\n)\n\n// BucketPerms is the type constraint for all permission-declaring\n// interfaces that can be used with BucketRef.\ntype BucketPerms interface {\n\tperms()\n}\n\n// ReadWriter is a utility permission interface that provides\n// all the read-write permissions.\ntype ReadWriter interface {\n\tUploader\n\tSignedUploader\n\tDownloader\n\tSignedDownloader\n\tRemover\n\tLister\n\tAttrser\n}\n\n// Uploader is the interface for uploading objects to a bucket.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can upload objects to the bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.Uploader](MyBucket)\n//\n// The ref object can then be used to upload objects and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype Uploader interface {\n\t// Upload begins uploading an object to the bucket.\n\tUpload(ctx context.Context, object string, options ...UploadOption) *Writer\n\n\tperms()\n}\n\n// SignedUploader is the interface for creating external URLs to upload objects\n// to a bucket. It can be used in conjunction with [BucketRef] to declare\n// a reference that can generate upload URLs to the bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.SignedUploader](MyBucket)\n//\n// The ref object can then be used to generate upload URLs and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype SignedUploader interface {\n\t// SignedUploadURL returns a signed URL that can be used to upload directly to\n\t// storage, without any other authentication.\n\tSignedUploadURL(ctx context.Context, object string, options ...UploadURLOption) (*SignedUploadURL, error)\n\n\tperms()\n}\n\n// Downloader is the interface for downloading objects from a bucket.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can download objects from the bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.Downloader](MyBucket)\n//\n// The ref object can then be used to download objects and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype Downloader interface {\n\t// Download downloads an object from the bucket.\n\tDownload(ctx context.Context, object string, options ...DownloadOption) *Reader\n\n\tperms()\n}\n\n// SignedDownloader is the interface for creating external URLs to download objects\n// from a bucket.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can generate download URLs for the bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.SignedDownloader](MyBucket)\n//\n// The ref object can then be used to generate download URLs and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype SignedDownloader interface {\n\t// SignedDownloadURL returns a signed URL that can be used to download directly\n\t// from storage, without any other authentication.\n\tSignedDownloadURL(ctx context.Context, object string, options ...DownloadURLOption) (*SignedDownloadURL, error)\n\n\tperms()\n}\n\n// Lister is the interface for listing objects in a bucket.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can list objects in the bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.Lister](MyBucket)\n//\n// The ref object can then be used to list objects and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype Lister interface {\n\t// List lists objects in the bucket.\n\tList(ctx context.Context, query *Query, options ...ListOption) iter.Seq2[*ListEntry, error]\n\n\tperms()\n}\n\n// Remove is the interface for removing objects from a bucket.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can remove objects from the bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.Remover](MyBucket)\n//\n// The ref object can then be used to remove objects and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype Remover interface {\n\t// Remove removes an object from the bucket.\n\tRemove(ctx context.Context, object string, options ...RemoveOption) error\n\n\tperms()\n}\n\n// Attrser is the interface for resolving objects' attributes in a bucket.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can check object attributes in a bucket.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.Attrser](MyBucket)\n//\n// The ref object can then be used to remove objects and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype Attrser interface {\n\t// Attrs resolves the attributes of an object.\n\tAttrs(ctx context.Context, object string, options ...AttrsOption) (*ObjectAttrs, error)\n\n\t// Exists checks whether an object exists in the bucket.\n\tExists(ctx context.Context, object string, options ...ExistsOption) (bool, error)\n\n\tperms()\n}\n\n// PublicURLer is the interface for resolving the public URL for an object.\n// It can be used in conjunction with [BucketRef] to declare\n// a reference that can resolve an object's public URL.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.PublicURLer](MyBucket)\n//\n// The ref object can then be used to remove objects and can be\n// passed around freely within the service, without being subject\n// to Encore's static analysis restrictions that apply to MyBucket.\ntype PublicURLer interface {\n\t// PublicURL resolves the public URL for retrieving an object.\n\tPublicURL(object string, options ...PublicURLOption) *url.URL\n\n\tperms()\n}\n\n// BucketRef returns an interface reference to a bucket,\n// that can be freely passed around within a service\n// without being subject to Encore's typical static analysis\n// restrictions that normally apply to *Bucket objects.\n//\n// This works because using BucketRef effectively declares\n// which operations you want to be able to perform since the\n// type argument P must be a permission-declaring interface (implementing BucketPerms).\n//\n// The returned reference is scoped down to those permissions.\n//\n// For example:\n//\n//\tvar MyBucket = objects.NewBucket(...)\n//\tvar ref = objects.BucketRef[objects.ReadWriter](MyBucket)\n//\t// ref.Upload(...) can now be used to upload objects to MyBucket.\n//\n// Multiple permissions can be combined by defining a custom interface\n// that embeds multiple permission interfaces:\n//\n//\tvar ref = objects.BucketRef[interface { objects.Uploader; objects.Downloader }](MyBucket)\nfunc BucketRef[P BucketPerms](bucket *Bucket) P {\n\treturn any(bucketRef{Bucket: bucket}).(P)\n}\n\ntype bucketRef struct {\n\t*Bucket\n}\n\nfunc (r bucketRef) perms() {}\n"
  },
  {
    "path": "runtimes/go/storage/objects/registry_internal.go",
    "content": "package objects\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/storage/objects/internal/types\"\n)\n\ntype provider interface {\n\tProviderName() string\n\tMatches(providerCfg *config.BucketProvider) bool\n\tNewBucket(providerCfg *config.BucketProvider, runtimeCfg *config.Bucket) types.BucketImpl\n}\n\nvar providerRegistry []func(context.Context, *config.Runtime) provider\n\nfunc registerProvider(p func(context.Context, *config.Runtime) provider) {\n\tproviderRegistry = append(providerRegistry, p)\n}\n"
  },
  {
    "path": "runtimes/go/storage/objects/zzz_singleton_internal.go",
    "content": "//go:build encore_app\n\npackage objects\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\n// Initialize the singleton instance.\n// NOTE: This file is named zzz_singleton_internal.go so that\n// the init function is initialized after all the providers\n// have been registered.\n\n//publicapigen:drop\nvar Singleton *Manager\n\nfunc init() {\n\tSingleton = NewManager(appconf.Static, appconf.Runtime, reqtrack.Singleton,\n\t\ttestsupport.Singleton, logging.RootLogger)\n\tshutdown.Singleton.RegisterShutdownHandler(Singleton.Shutdown)\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/db.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"database/sql\"\n\t\"database/sql/driver\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/storage/sqldb/internal/stdlibdriver\"\n)\n\ntype Database struct {\n\tname     string\n\torigName string // original name if this was cloned.\n\tmgr      *Manager\n\thooks    *hookList\n\n\tnoopDB bool // true if this is a dummy database that does nothing and returns errors for all operations\n\n\tinitOnce sync.Once\n\tpool     *pgxpool.Pool\n\tconnStr  string\n\n\tstdlibOnce sync.Once\n\tstdlib     *sql.DB\n}\n\n// Hooks defines callbacks that can be registered for database lifecycle events.\ntype Hooks struct {\n\t// AfterConnect is called whenever a new database connection is established.\n\t// Returning an error aborts creation of that connection.\n\tAfterConnect func(context.Context, *pgx.Conn) error\n}\n\ntype hookList struct {\n\tmu    sync.RWMutex\n\thooks []Hooks\n}\n\n// AddHooks registers callbacks to run for this database.\n// Does not have any effect when using the database/sql integration via the (*Database).Stdlib method.\nfunc (db *Database) AddHooks(h Hooks) {\n\tdb.hooks.mu.Lock()\n\tdb.hooks.hooks = append(db.hooks.hooks, h)\n\tdb.hooks.mu.Unlock()\n}\n\nfunc (hooks *hookList) runAfterConnectHooks(ctx context.Context, conn *pgx.Conn) error {\n\tif hooks == nil {\n\t\treturn nil\n\t}\n\thooks.mu.RLock()\n\tregistered := hooks.hooks\n\thooks.mu.RUnlock()\n\n\tfor _, hook := range registered {\n\t\tif hook.AfterConnect != nil {\n\t\t\tif err := hook.AfterConnect(ctx, conn); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar errNoopDB = errors.New(\"sqldb: this service is not configured to use this database. Use sqldb.Named in this service to get a reference and access to the database from this service\")\n\nfunc (db *Database) init() {\n\tif db.noopDB {\n\t\treturn\n\t}\n\n\tdb.initOnce.Do(func() {\n\t\tif db.pool == nil {\n\t\t\tpool, found := db.mgr.getPool(db.origName, db.name, db.hooks)\n\t\t\tdb.pool, db.noopDB = pool, !found\n\t\t}\n\n\t\tif !db.noopDB {\n\t\t\tdb.connStr = stdlibdriver.RegisterConnConfig(db.pool.Config().ConnConfig)\n\t\t}\n\t})\n}\n\n// Stdlib returns a *sql.DB object that is connected to the same db,\n// for use with libraries that expect a *sql.DB.\nfunc (db *Database) Stdlib() *sql.DB {\n\t// If this is a noop database, return a dummy *sql.DB that returns errors for all operations.\n\tif db.noopDB {\n\t\tregisterNoopDriverOnce.Do(func() {\n\t\t\tsql.Register(noopDriverName, noopDriver{})\n\t\t})\n\n\t\treturn sql.OpenDB(noopConnector{})\n\t}\n\n\tdb.init()\n\n\tvar openErr error\n\tdb.stdlibOnce.Do(func() {\n\t\tc, err := registerStdlibDriver(db.mgr).(driver.DriverContext).OpenConnector(db.connStr)\n\t\tif err == nil {\n\t\t\tdb.stdlib = sql.OpenDB(c)\n\n\t\t\t// Set the pool size based on the config.\n\t\t\tcfg := db.pool.Config()\n\t\t\tmaxConns := int(cfg.MaxConns)\n\t\t\tdb.stdlib.SetMaxOpenConns(maxConns)\n\t\t\tdb.stdlib.SetConnMaxIdleTime(cfg.MaxConnIdleTime)\n\t\t\tdb.stdlib.SetMaxIdleConns(maxConns)\n\t\t}\n\t\topenErr = err\n\t})\n\tif openErr != nil {\n\t\t// This should never happen as (*stdlib.Driver).OpenConnector is hard-coded to never return nil.\n\t\t// Guard it with a panic so we detect it as early as possible in case this changes.\n\t\tpanic(\"sqldb: stdlib.OpenConnector failed: \" + openErr.Error())\n\t}\n\treturn db.stdlib\n}\n\nfunc (db *Database) shutdown() {\n\tif db.pool != nil {\n\t\tdb.pool.Close()\n\t}\n\tif db.stdlib != nil {\n\t\t_ = db.stdlib.Close()\n\t}\n}\n\n// dbConf computes a suitable pgxpool config given a database config.\n// If dbNameOverride is provided, it overrides the database name used when connecting,\n// for testing purposes.\nfunc dbConf(srv *config.SQLServer, db *config.SQLDatabase, dbNameOverride string) (*pgxpool.Config, error) {\n\tdbName := dbNameOverride\n\tif dbName == \"\" {\n\t\tdbName = db.DatabaseName\n\t}\n\turi := fmt.Sprintf(\"user=%s password=%s dbname=%s\", db.User, db.Password, dbName)\n\n\t// Handle different ways of expressing the host\n\tif strings.HasPrefix(srv.Host, \"/\") {\n\t\turi += \" host=\" + srv.Host // unix socket\n\t} else if host, port, err := net.SplitHostPort(srv.Host); err == nil {\n\t\turi += fmt.Sprintf(\" host=%s port=%s\", host, port) // host:port\n\t} else {\n\t\turi += \" host=\" + srv.Host // hostname\n\t}\n\n\tif srv.ServerCACert != \"\" {\n\t\turi += \" sslmode=verify-ca\"\n\t} else {\n\t\turi += \" sslmode=prefer\"\n\t}\n\n\tcfg, err := pgxpool.ParseConfig(uri)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid database uri: %v\", err)\n\t}\n\n\t// Set the pool size based on the config.\n\tcfg.MaxConns = 30\n\tif n := db.MaxConnections; n > 0 {\n\t\tcfg.MaxConns = int32(n)\n\t}\n\n\t// If we have a server CA, set it in the TLS config.\n\tif srv.ServerCACert != \"\" {\n\t\tcaCertPool := x509.NewCertPool()\n\t\tif !caCertPool.AppendCertsFromPEM([]byte(srv.ServerCACert)) {\n\t\t\treturn nil, fmt.Errorf(\"invalid server ca cert\")\n\t\t}\n\t\tcfg.ConnConfig.TLSConfig.RootCAs = caCertPool\n\t\tcfg.ConnConfig.TLSConfig.ClientCAs = caCertPool\n\t}\n\n\t// If we have a client cert, set it in the TLS config.\n\tif srv.ClientCert != \"\" {\n\t\tcert, err := tls.X509KeyPair([]byte(srv.ClientCert), []byte(srv.ClientKey))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse client cert: %v\", err)\n\t\t}\n\t\tcfg.ConnConfig.TLSConfig.Certificates = []tls.Certificate{cert}\n\t}\n\n\treturn cfg, nil\n}\n\n// Exec executes a query without returning any rows.\n// The args are for any placeholder parameters in the query.\n//\n// See (*database/sql.DB).ExecContext() for additional documentation.\nfunc (db *Database) Exec(ctx context.Context, query string, args ...interface{}) (ExecResult, error) {\n\tif db.noopDB {\n\t\treturn nil, errNoopDB\n\t}\n\n\tdb.init()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tcurr := db.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tTxStartID:   0,\n\t\t\tStack:       stack.Build(4),\n\t\t})\n\t}\n\n\tres, err := db.pool.Exec(markTraced(ctx), query, args...)\n\terr = convertErr(err)\n\n\tif curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn res, err\n}\n\n// Query executes a query that returns rows, typically a SELECT.\n// The args are for any placeholder parameters in the query.\n//\n// See (*database/sql.DB).QueryContext() for additional documentation.\nfunc (db *Database) Query(ctx context.Context, query string, args ...interface{}) (*Rows, error) {\n\tif db.noopDB {\n\t\treturn nil, errNoopDB\n\t}\n\n\tdb.init()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tcurr := db.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(4),\n\t\t})\n\t}\n\n\trows, err := db.pool.Query(markTraced(ctx), query, args...)\n\terr = convertErr(err)\n\n\tif curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Rows{std: rows}, nil\n}\n\n// QueryRow executes a query that is expected to return at most one row.\n//\n// See (*database/sql.DB).QueryRowContext() for additional documentation.\nfunc (db *Database) QueryRow(ctx context.Context, query string, args ...interface{}) *Row {\n\tif db.noopDB {\n\t\treturn &Row{err: errNoopDB}\n\t}\n\n\tdb.init()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tcurr := db.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(4),\n\t\t})\n\t}\n\n\trows, err := db.pool.Query(markTraced(ctx), query, args...)\n\terr = convertErr(err)\n\tr := &Row{rows: rows, err: err}\n\n\tif curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn r\n}\n\n// Begin opens a new database transaction.\n//\n// See (*database/sql.DB).Begin() for additional documentation.\nfunc (db *Database) Begin(ctx context.Context) (*Tx, error) {\n\tif db.noopDB {\n\t\treturn nil, errNoopDB\n\t}\n\n\tdb.init()\n\ttx, err := db.pool.Begin(markTraced(ctx))\n\terr = convertErr(err)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar startID model.TraceEventID\n\tcurr := db.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tstartID = curr.Trace.DBTransactionStart(trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t}, stack.Build(4))\n\t}\n\n\treturn &Tx{mgr: db.mgr, std: tx, startID: startID}, nil\n}\n\n// Driver returns the underlying database driver for this database connection pool.\n//\n//\tvar db = sqldb.Driver[*pgxpool.Pool](sqldb.Named(\"mydatabase\"))\n//\n// This is defined as a generic function to allow compile-time type checking\n// that the Encore application is expecting a driver that is supported.\n//\n// At some point in the future where Encore adds support for a different database driver\n// this will be made with backwards compatibility in mind, providing ample notice and\n// time to migrate in an opt-in fashion.\nfunc Driver[T SupportedDrivers](db *Database) T {\n\tdb.init()\n\tif db.noopDB {\n\t\tvar zero T\n\t\treturn zero\n\t}\n\n\treturn any(db.pool).(T)\n}\n\n// SupportedDrivers is a type list of all supported database drivers.\n// Currently only [*pgxpool.Pool] is supported.\ntype SupportedDrivers interface {\n\t*pgxpool.Pool\n}\n\n// DriverConn provides access to the underlying driver connection given a stdlib\n// *sql.Conn connection. The driverConn must not be used outside of f, and conn\n// must be a *sql.Conn originating from sqldb.Database or this function panics.\n//\n//\tconn, _ := db.Stdlib().Conn(ctx) // Checkout a connection from the pool\n//\tsqldb.DriverConn(conn, func(driverConn *pgx.Conn) error) error {\n//\t  // do stuff with *pgx.Conn\n//\t}\n//\n// This is defined as a generic function to allow compile-time type checking\n// that the Encore application is expecting a driver that is supported.\n//\n// At some point in the future where Encore adds support for a different\n// database driver this will be made with backwards compatibility in mind,\n// providing ample notice and time to migrate in an opt-in fashion.\nfunc DriverConn[T SupportedDriverConns](conn *sql.Conn, f func(driverConn T) error) error {\n\treturn conn.Raw(func(c any) error {\n\t\tswitch c := c.(type) {\n\t\tcase wrappedConn:\n\t\t\tparentConn := c.parent\n\t\t\trawConn := (parentConn).(*stdlibdriver.Conn).Conn()\n\t\t\treturn f(rawConn)\n\t\tcase noopConn:\n\t\t\treturn errNoopDB\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"sqldb.DriverConn: unsupported connection type %T\", c))\n\t\t}\n\t})\n}\n\n// SupportedDriverConns is a type list of all supported database drivers\n// connections. Currently only [*pgx.Conn] is supported.\ntype SupportedDriverConns interface {\n\t*pgx.Conn\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/db_hooks_test.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/jackc/pgx/v5\"\n)\n\nfunc TestDatabaseAddHooksAppends(t *testing.T) {\n\tdb := &Database{hooks: &hookList{}}\n\n\tfirstCalled := false\n\tdb.AddHooks(Hooks{AfterConnect: func(context.Context, *pgx.Conn) error {\n\t\tfirstCalled = true\n\t\treturn nil\n\t}})\n\n\tsecondCalled := false\n\tdb.AddHooks(Hooks{AfterConnect: func(context.Context, *pgx.Conn) error {\n\t\tsecondCalled = true\n\t\treturn nil\n\t}})\n\n\tlist := db.hooks\n\tif list == nil {\n\t\tt.Fatal(\"expected hook list to be initialized\")\n\t}\n\tlist.mu.RLock()\n\tcount := len(list.hooks)\n\tlist.mu.RUnlock()\n\tif count != 2 {\n\t\tt.Fatalf(\"expected 2 hooks, got %d\", count)\n\t}\n\n\tif err := db.hooks.runAfterConnectHooks(context.Background(), nil); err != nil {\n\t\tt.Fatalf(\"unexpected error running hooks: %v\", err)\n\t}\n\tif !firstCalled || !secondCalled {\n\t\tt.Fatalf(\"expected both hooks to be called, first=%v second=%v\", firstCalled, secondCalled)\n\t}\n}\n\nfunc TestDatabaseRunAfterConnectHooksSkipsNilAndReturnsError(t *testing.T) {\n\tdb := &Database{hooks: &hookList{}}\n\twantErr := errors.New(\"boom\")\n\tcalled := false\n\n\tdb.AddHooks(Hooks{})\n\tdb.AddHooks(Hooks{AfterConnect: func(context.Context, *pgx.Conn) error {\n\t\tcalled = true\n\t\treturn wantErr\n\t}})\n\n\terr := db.hooks.runAfterConnectHooks(context.Background(), nil)\n\tif !errors.Is(err, wantErr) {\n\t\tt.Fatalf(\"expected %v, got %v\", wantErr, err)\n\t}\n\tif !called {\n\t\tt.Fatal(\"expected non-nil hook to be invoked\")\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/errors.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgconn\"\n\n\t\"encore.dev/beta/errs\"\n\t\"encore.dev/storage/sqldb/sqlerr\"\n)\n\n// ErrCode reports the error code for a given error.\n// If the error is nil or is not of type *Error it reports sqlerr.Other.\nfunc ErrCode(err error) sqlerr.Code {\n\tvar pgerr *Error\n\tif errors.As(err, &pgerr) {\n\t\treturn pgerr.Code\n\t}\n\treturn sqlerr.Other\n}\n\n// Error represents an error reported by the database server.\n// It's not guaranteed all errors reported by sqldb functions will be of this type;\n// it is only returned when the database reports an error.\n//\n// Note that the fields for schema name, table name, column name, data type name,\n// and constraint name are supplied only for a limited number of error types;\n// see https://www.postgresql.org/docs/current/errcodes-appendix.html.\n//\n// You should not assume that the presence of any of these fields guarantees\n// the presence of another field.\ntype Error struct {\n\t// Code defines the general class of the error.\n\t// See [sqlerr.Code] for a list of possible values.\n\tCode sqlerr.Code\n\n\t// Severity is the severity of the error.\n\tSeverity sqlerr.Severity\n\n\t// DatabaseCode is the database server-specific error code.\n\t// It is specific to the underlying database server.\n\tDatabaseCode string\n\n\t// Message: the primary human-readable error message. This should be accurate\n\t// but terse (typically one line). Always present.\n\tMessage string\n\n\t// SchemaName: if the error was associated with a specific database object,\n\t// the name of the schema containing that object, if any.\n\tSchemaName string\n\n\t// TableName: if the error was associated with a specific table, the name of the table.\n\t// (Refer to the schema name field for the name of the table's schema.)\n\tTableName string\n\n\t// ColumnName: if the error was associated with a specific table column,\n\t// the name of the column. (Refer to the schema and table name fields to identify the table.)\n\tColumnName string\n\n\t// Data type name: if the error was associated with a specific data type,\n\t// the name of the data type. (Refer to the schema name field for the name of the data type's schema.)\n\tDataTypeName string\n\n\t// Constraint name: if the error was associated with a specific constraint,\n\t// the name of the constraint. Refer to fields listed above for the associated\n\t// table or domain. (For this purpose, indexes are treated as constraints,\n\t// even if they weren't created with constraint syntax.)\n\tConstraintName string\n\n\t// driverErr is the underlying error from the driver.\n\t// It's used to support errors.As and errors.Is to preserve\n\t// backwards compatibility.\n\tdriverErr error\n}\n\nfunc (pe *Error) Error() string {\n\treturn string(pe.Severity) + \": \" + pe.Message + \" (Code \" + string(pe.Code) + \": SQLSTATE \" + pe.DatabaseCode + \")\"\n}\n\nfunc (pe *Error) Unwrap() error {\n\treturn pe.driverErr\n}\n\nfunc convertErr(err error) error {\n\tvar pgerr *pgconn.PgError\n\tif errors.As(err, &pgerr) {\n\t\terr = convertPgError(pgerr)\n\t}\n\n\tswitch {\n\tcase errors.Is(err, pgx.ErrNoRows), errors.Is(err, sql.ErrNoRows):\n\t\terr = errs.WrapCode(sql.ErrNoRows, errs.NotFound, \"\")\n\tcase errors.Is(err, pgx.ErrTxClosed), errors.Is(err, pgx.ErrTxCommitRollback), errors.Is(err, sql.ErrTxDone), errors.Is(err, sql.ErrConnDone):\n\t\terr = errs.WrapCode(err, errs.Internal, \"\")\n\tcase errors.Is(err, context.DeadlineExceeded):\n\t\terr = errs.WrapCode(err, errs.DeadlineExceeded, \"\")\n\tcase errors.Is(err, context.Canceled):\n\t\terr = errs.WrapCode(err, errs.Canceled, \"\")\n\tdefault:\n\t\terr = errs.WrapCode(err, errs.Unavailable, \"\")\n\t}\n\n\treturn errs.DropStackFrame(err)\n}\n\nfunc convertPgError(src *pgconn.PgError) error {\n\treturn &Error{\n\t\tCode:           sqlerr.MapCode(src.Code),\n\t\tSeverity:       sqlerr.MapSeverity(src.Severity),\n\t\tDatabaseCode:   src.Code,\n\t\tMessage:        src.Message,\n\t\tSchemaName:     src.SchemaName,\n\t\tTableName:      src.TableName,\n\t\tColumnName:     src.ColumnName,\n\t\tDataTypeName:   src.DataTypeName,\n\t\tConstraintName: src.ConstraintName,\n\t\tdriverErr:      src,\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/errors_internal.go",
    "content": "package sqldb\n\nimport (\n\tjsoniter \"github.com/json-iterator/go\"\n\n\t\"encore.dev/appruntime/apisdk/api/errmarshalling\"\n\t\"encore.dev/storage/sqldb/sqlerr\"\n)\n\n// Register an internal error marshaller for the sqldb.Error\nfunc init() {\n\terrmarshalling.RegisterErrorMarshaller(\n\t\tfunc(err *Error, stream *jsoniter.Stream) {\n\t\t\tstream.WriteObjectField(\"code\")\n\t\t\tstream.WriteString(string(err.Code))\n\n\t\t\tstream.WriteMore()\n\t\t\tstream.WriteObjectField(\"severity\")\n\t\t\tstream.WriteString(string(err.Severity))\n\n\t\t\tstream.WriteMore()\n\t\t\tstream.WriteObjectField(\"db_code\")\n\t\t\tstream.WriteString(err.DatabaseCode)\n\n\t\t\tstream.WriteMore()\n\t\t\tstream.WriteObjectField(errmarshalling.MessageKey)\n\t\t\tstream.WriteString(err.Message)\n\n\t\t\tif err.SchemaName != \"\" {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(\"schema\")\n\t\t\t\tstream.WriteString(err.SchemaName)\n\t\t\t}\n\n\t\t\tif err.TableName != \"\" {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(\"table\")\n\t\t\t\tstream.WriteString(err.TableName)\n\t\t\t}\n\n\t\t\tif err.ColumnName != \"\" {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(\"column\")\n\t\t\t\tstream.WriteString(err.ColumnName)\n\t\t\t}\n\n\t\t\tif err.DataTypeName != \"\" {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(\"data_type\")\n\t\t\t\tstream.WriteString(err.DataTypeName)\n\t\t\t}\n\n\t\t\tif err.ConstraintName != \"\" {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(\"constraint\")\n\t\t\t\tstream.WriteString(err.ConstraintName)\n\t\t\t}\n\n\t\t\tif err.driverErr != nil {\n\t\t\t\tstream.WriteMore()\n\t\t\t\tstream.WriteObjectField(errmarshalling.WrappedKey)\n\t\t\t\tstream.WriteVal(err.driverErr)\n\t\t\t}\n\t\t},\n\t\tfunc(err *Error, iter *jsoniter.Iterator) {\n\t\t\titer.ReadObjectCB(func(iter *jsoniter.Iterator, field string) bool {\n\t\t\t\tswitch field {\n\t\t\t\tcase \"code\":\n\t\t\t\t\terr.Code = sqlerr.Code(iter.ReadString())\n\t\t\t\tcase \"severity\":\n\t\t\t\t\terr.Severity = sqlerr.Severity(iter.ReadString())\n\t\t\t\tcase \"db_code\":\n\t\t\t\t\terr.DatabaseCode = iter.ReadString()\n\t\t\t\tcase errmarshalling.MessageKey:\n\t\t\t\t\terr.Message = iter.ReadString()\n\t\t\t\tcase \"schema\":\n\t\t\t\t\terr.SchemaName = iter.ReadString()\n\t\t\t\tcase \"table\":\n\t\t\t\t\terr.TableName = iter.ReadString()\n\t\t\t\tcase \"column\":\n\t\t\t\t\terr.ColumnName = iter.ReadString()\n\t\t\t\tcase \"data_type\":\n\t\t\t\t\terr.DataTypeName = iter.ReadString()\n\t\t\t\tcase \"constraint\":\n\t\t\t\t\terr.ConstraintName = iter.ReadString()\n\t\t\t\tcase errmarshalling.WrappedKey:\n\t\t\t\t\terr.driverErr = errmarshalling.UnmarshalError(iter)\n\t\t\t\tdefault:\n\t\t\t\t\titer.Skip()\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/errors_test.go",
    "content": "package sqldb\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"encore.dev/storage/sqldb/sqlerr\"\n)\n\nfunc TestErrCode(t *testing.T) {\n\ttests := []struct {\n\t\terr  error\n\t\twant sqlerr.Code\n\t}{\n\t\t{err: nil, want: sqlerr.Other},\n\t\t{err: io.EOF, want: sqlerr.Other},\n\t\t{err: errors.New(\"some error\"), want: sqlerr.Other},\n\t\t{err: &Error{Code: sqlerr.UniqueViolation}, want: sqlerr.UniqueViolation},\n\t\t{err: fmt.Errorf(\"wrapped: %w\", &Error{Code: sqlerr.UniqueViolation}), want: sqlerr.UniqueViolation},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := ErrCode(tt.err); got != tt.want {\n\t\t\tt.Errorf(\"ErrCode(%v) = %v, want %v\", tt.err, got, tt.want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/internal/stdlibdriver/LICENSE",
    "content": "Copyright (c) 2013-2021 Jack Christensen\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/internal/stdlibdriver/stdlibdriver.go",
    "content": "// Package stdlibdriver is a vendored version of github.com/jackc/pgx/v5/stdlib,\n// skipping the stdlib driver registration since we use OpenDB and pass in a driver.\n// Its license can be found in ./LICENSE.\npackage stdlibdriver\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"database/sql/driver\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgconn\"\n\t\"github.com/jackc/pgx/v5/pgtype\"\n)\n\n// Only intrinsic types should be binary format with database/sql.\nvar databaseSQLResultFormats pgx.QueryResultFormatsByOID\n\nvar pgxDriver *Driver\n\nfunc init() {\n\tpgxDriver = &Driver{\n\t\tconfigs: make(map[string]*pgx.ConnConfig),\n\t}\n\n\tdatabaseSQLResultFormats = pgx.QueryResultFormatsByOID{\n\t\tpgtype.BoolOID:        1,\n\t\tpgtype.ByteaOID:       1,\n\t\tpgtype.CIDOID:         1,\n\t\tpgtype.DateOID:        1,\n\t\tpgtype.Float4OID:      1,\n\t\tpgtype.Float8OID:      1,\n\t\tpgtype.Int2OID:        1,\n\t\tpgtype.Int4OID:        1,\n\t\tpgtype.Int8OID:        1,\n\t\tpgtype.OIDOID:         1,\n\t\tpgtype.TimestampOID:   1,\n\t\tpgtype.TimestamptzOID: 1,\n\t\tpgtype.XIDOID:         1,\n\t}\n}\n\n// GetDefaultDriver returns the driver initialized in the init function\n// and used when the pgx driver is registered.\nfunc GetDefaultDriver() driver.Driver {\n\treturn pgxDriver\n}\n\ntype Driver struct {\n\tconfigMutex sync.Mutex\n\tconfigs     map[string]*pgx.ConnConfig\n\tsequence    int\n}\n\nfunc (d *Driver) Open(name string) (driver.Conn, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Ensure eventual timeout\n\tdefer cancel()\n\n\tconnector, err := d.OpenConnector(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn connector.Connect(ctx)\n}\n\nfunc (d *Driver) OpenConnector(name string) (driver.Connector, error) {\n\treturn &driverConnector{driver: d, name: name}, nil\n}\n\nfunc (d *Driver) registerConnConfig(c *pgx.ConnConfig) string {\n\td.configMutex.Lock()\n\tconnStr := fmt.Sprintf(\"registeredConnConfig%d\", d.sequence)\n\td.sequence++\n\td.configs[connStr] = c\n\td.configMutex.Unlock()\n\treturn connStr\n}\n\nfunc (d *Driver) unregisterConnConfig(connStr string) {\n\td.configMutex.Lock()\n\tdelete(d.configs, connStr)\n\td.configMutex.Unlock()\n}\n\ntype driverConnector struct {\n\tdriver *Driver\n\tname   string\n}\n\nfunc (dc *driverConnector) Connect(ctx context.Context) (driver.Conn, error) {\n\tvar connConfig *pgx.ConnConfig\n\n\tdc.driver.configMutex.Lock()\n\tconnConfig = dc.driver.configs[dc.name]\n\tdc.driver.configMutex.Unlock()\n\n\tif connConfig == nil {\n\t\tvar err error\n\t\tconnConfig, err = pgx.ParseConfig(dc.name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tconn, err := pgx.ConnectConfig(ctx, connConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Conn{\n\t\tconn:             conn,\n\t\tdriver:           dc.driver,\n\t\tconnConfig:       *connConfig,\n\t\tresetSessionFunc: func(context.Context, *pgx.Conn) error { return nil },\n\t}\n\n\treturn c, nil\n}\n\nfunc (dc *driverConnector) Driver() driver.Driver {\n\treturn dc.driver\n}\n\n// RegisterConnConfig registers a ConnConfig and returns the connection string to use with Open.\nfunc RegisterConnConfig(c *pgx.ConnConfig) string {\n\treturn pgxDriver.registerConnConfig(c)\n}\n\n// UnregisterConnConfig removes the ConnConfig registration for connStr.\nfunc UnregisterConnConfig(connStr string) {\n\tpgxDriver.unregisterConnConfig(connStr)\n}\n\ntype Conn struct {\n\tconn                 *pgx.Conn\n\tpsCount              int64 // Counter used for creating unique prepared statement names\n\tdriver               *Driver\n\tconnConfig           pgx.ConnConfig\n\tresetSessionFunc     func(context.Context, *pgx.Conn) error // Function is called before a connection is reused\n\tlastResetSessionTime time.Time\n}\n\n// Conn returns the underlying *pgx.Conn\nfunc (c *Conn) Conn() *pgx.Conn {\n\treturn c.conn\n}\n\nfunc (c *Conn) Prepare(query string) (driver.Stmt, error) {\n\treturn c.PrepareContext(context.Background(), query)\n}\n\nfunc (c *Conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {\n\tif c.conn.IsClosed() {\n\t\treturn nil, driver.ErrBadConn\n\t}\n\n\tname := fmt.Sprintf(\"pgx_%d\", c.psCount)\n\tc.psCount++\n\n\tsd, err := c.conn.Prepare(ctx, name, query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Stmt{sd: sd, conn: c}, nil\n}\n\nfunc (c *Conn) Close() error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*5)\n\tdefer cancel()\n\treturn c.conn.Close(ctx)\n}\n\nfunc (c *Conn) Begin() (driver.Tx, error) {\n\treturn c.BeginTx(context.Background(), driver.TxOptions{})\n}\n\nfunc (c *Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {\n\tif c.conn.IsClosed() {\n\t\treturn nil, driver.ErrBadConn\n\t}\n\n\tvar pgxOpts pgx.TxOptions\n\tswitch sql.IsolationLevel(opts.Isolation) {\n\tcase sql.LevelDefault:\n\tcase sql.LevelReadUncommitted:\n\t\tpgxOpts.IsoLevel = pgx.ReadUncommitted\n\tcase sql.LevelReadCommitted:\n\t\tpgxOpts.IsoLevel = pgx.ReadCommitted\n\tcase sql.LevelRepeatableRead, sql.LevelSnapshot:\n\t\tpgxOpts.IsoLevel = pgx.RepeatableRead\n\tcase sql.LevelSerializable:\n\t\tpgxOpts.IsoLevel = pgx.Serializable\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported isolation: %v\", opts.Isolation)\n\t}\n\n\tif opts.ReadOnly {\n\t\tpgxOpts.AccessMode = pgx.ReadOnly\n\t}\n\n\ttx, err := c.conn.BeginTx(ctx, pgxOpts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn wrapTx{ctx: ctx, tx: tx}, nil\n}\n\nfunc (c *Conn) ExecContext(ctx context.Context, query string, argsV []driver.NamedValue) (driver.Result, error) {\n\tif c.conn.IsClosed() {\n\t\treturn nil, driver.ErrBadConn\n\t}\n\n\targs := namedValueToInterface(argsV)\n\n\tcommandTag, err := c.conn.Exec(ctx, query, args...)\n\t// if we got a network error before we had a chance to send the query, retry\n\tif err != nil {\n\t\tif pgconn.SafeToRetry(err) {\n\t\t\treturn nil, driver.ErrBadConn\n\t\t}\n\t}\n\treturn driver.RowsAffected(commandTag.RowsAffected()), err\n}\n\nfunc (c *Conn) QueryContext(ctx context.Context, query string, argsV []driver.NamedValue) (driver.Rows, error) {\n\tif c.conn.IsClosed() {\n\t\treturn nil, driver.ErrBadConn\n\t}\n\n\targs := []any{databaseSQLResultFormats}\n\targs = append(args, namedValueToInterface(argsV)...)\n\n\trows, err := c.conn.Query(ctx, query, args...)\n\tif err != nil {\n\t\tif pgconn.SafeToRetry(err) {\n\t\t\treturn nil, driver.ErrBadConn\n\t\t}\n\t\treturn nil, err\n\t}\n\n\t// Preload first row because otherwise we won't know what columns are available when database/sql asks.\n\tmore := rows.Next()\n\tif err = rows.Err(); err != nil {\n\t\trows.Close()\n\t\treturn nil, err\n\t}\n\treturn &Rows{conn: c, rows: rows, skipNext: true, skipNextMore: more}, nil\n}\n\nfunc (c *Conn) Ping(ctx context.Context) error {\n\tif c.conn.IsClosed() {\n\t\treturn driver.ErrBadConn\n\t}\n\n\terr := c.conn.Ping(ctx)\n\tif err != nil {\n\t\t// A Ping failure implies some sort of fatal state. The connection is almost certainly already closed by the\n\t\t// failure, but manually close it just to be sure.\n\t\t_ = c.Close()\n\t\treturn driver.ErrBadConn\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) CheckNamedValue(*driver.NamedValue) error {\n\t// Underlying pgx supports sql.Scanner and driver.Valuer interfaces natively. So everything can be passed through directly.\n\treturn nil\n}\n\nfunc (c *Conn) ResetSession(ctx context.Context) error {\n\tif c.conn.IsClosed() {\n\t\treturn driver.ErrBadConn\n\t}\n\n\tnow := time.Now()\n\tif now.Sub(c.lastResetSessionTime) > time.Second {\n\t\tif err := c.conn.PgConn().CheckConn(); err != nil {\n\t\t\treturn driver.ErrBadConn\n\t\t}\n\t}\n\tc.lastResetSessionTime = now\n\n\treturn c.resetSessionFunc(ctx, c.conn)\n}\n\ntype Stmt struct {\n\tsd   *pgconn.StatementDescription\n\tconn *Conn\n}\n\nfunc (s *Stmt) Close() error {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second*5)\n\tdefer cancel()\n\treturn s.conn.conn.Deallocate(ctx, s.sd.Name)\n}\n\nfunc (s *Stmt) NumInput() int {\n\treturn len(s.sd.ParamOIDs)\n}\n\nfunc (s *Stmt) Exec(argsV []driver.Value) (driver.Result, error) {\n\treturn nil, errors.New(\"Stmt.Exec deprecated and not implemented\")\n}\n\nfunc (s *Stmt) ExecContext(ctx context.Context, argsV []driver.NamedValue) (driver.Result, error) {\n\treturn s.conn.ExecContext(ctx, s.sd.Name, argsV)\n}\n\nfunc (s *Stmt) Query(argsV []driver.Value) (driver.Rows, error) {\n\treturn nil, errors.New(\"Stmt.Query deprecated and not implemented\")\n}\n\nfunc (s *Stmt) QueryContext(ctx context.Context, argsV []driver.NamedValue) (driver.Rows, error) {\n\treturn s.conn.QueryContext(ctx, s.sd.Name, argsV)\n}\n\ntype rowValueFunc func(src []byte) (driver.Value, error)\n\ntype Rows struct {\n\tconn         *Conn\n\trows         pgx.Rows\n\tvalueFuncs   []rowValueFunc\n\tskipNext     bool\n\tskipNextMore bool\n\n\tcolumnNames []string\n}\n\nfunc (r *Rows) Columns() []string {\n\tif r.columnNames == nil {\n\t\tfields := r.rows.FieldDescriptions()\n\t\tr.columnNames = make([]string, len(fields))\n\t\tfor i, fd := range fields {\n\t\t\tr.columnNames[i] = string(fd.Name)\n\t\t}\n\t}\n\n\treturn r.columnNames\n}\n\n// ColumnTypeDatabaseTypeName returns the database system type name. If the name is unknown the OID is returned.\nfunc (r *Rows) ColumnTypeDatabaseTypeName(index int) string {\n\tif dt, ok := r.conn.conn.TypeMap().TypeForOID(r.rows.FieldDescriptions()[index].DataTypeOID); ok {\n\t\treturn strings.ToUpper(dt.Name)\n\t}\n\n\treturn strconv.FormatInt(int64(r.rows.FieldDescriptions()[index].DataTypeOID), 10)\n}\n\nconst varHeaderSize = 4\n\n// ColumnTypeLength returns the length of the column type if the column is a\n// variable length type. If the column is not a variable length type ok\n// should return false.\nfunc (r *Rows) ColumnTypeLength(index int) (int64, bool) {\n\tfd := r.rows.FieldDescriptions()[index]\n\n\tswitch fd.DataTypeOID {\n\tcase pgtype.TextOID, pgtype.ByteaOID:\n\t\treturn math.MaxInt64, true\n\tcase pgtype.VarcharOID, pgtype.BPCharArrayOID:\n\t\treturn int64(fd.TypeModifier - varHeaderSize), true\n\tdefault:\n\t\treturn 0, false\n\t}\n}\n\n// ColumnTypePrecisionScale should return the precision and scale for decimal\n// types. If not applicable, ok should be false.\nfunc (r *Rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) {\n\tfd := r.rows.FieldDescriptions()[index]\n\n\tswitch fd.DataTypeOID {\n\tcase pgtype.NumericOID:\n\t\tmod := fd.TypeModifier - varHeaderSize\n\t\tprecision = int64((mod >> 16) & 0xffff)\n\t\tscale = int64(mod & 0xffff)\n\t\treturn precision, scale, true\n\tdefault:\n\t\treturn 0, 0, false\n\t}\n}\n\n// ColumnTypeScanType returns the value type that can be used to scan types into.\nfunc (r *Rows) ColumnTypeScanType(index int) reflect.Type {\n\tfd := r.rows.FieldDescriptions()[index]\n\n\tswitch fd.DataTypeOID {\n\tcase pgtype.Float8OID:\n\t\treturn reflect.TypeOf(float64(0))\n\tcase pgtype.Float4OID:\n\t\treturn reflect.TypeOf(float32(0))\n\tcase pgtype.Int8OID:\n\t\treturn reflect.TypeOf(int64(0))\n\tcase pgtype.Int4OID:\n\t\treturn reflect.TypeOf(int32(0))\n\tcase pgtype.Int2OID:\n\t\treturn reflect.TypeOf(int16(0))\n\tcase pgtype.BoolOID:\n\t\treturn reflect.TypeOf(false)\n\tcase pgtype.NumericOID:\n\t\treturn reflect.TypeOf(float64(0))\n\tcase pgtype.DateOID, pgtype.TimestampOID, pgtype.TimestamptzOID:\n\t\treturn reflect.TypeOf(time.Time{})\n\tcase pgtype.ByteaOID:\n\t\treturn reflect.TypeOf([]byte(nil))\n\tdefault:\n\t\treturn reflect.TypeOf(\"\")\n\t}\n}\n\nfunc (r *Rows) Close() error {\n\tr.rows.Close()\n\treturn r.rows.Err()\n}\n\nfunc (r *Rows) Next(dest []driver.Value) error {\n\tm := r.conn.conn.TypeMap()\n\tfieldDescriptions := r.rows.FieldDescriptions()\n\n\tif r.valueFuncs == nil {\n\t\tr.valueFuncs = make([]rowValueFunc, len(fieldDescriptions))\n\n\t\tfor i, fd := range fieldDescriptions {\n\t\t\tdataTypeOID := fd.DataTypeOID\n\t\t\tformat := fd.Format\n\n\t\t\tswitch fd.DataTypeOID {\n\t\t\tcase pgtype.BoolOID:\n\t\t\t\tvar d bool\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn d, err\n\t\t\t\t}\n\t\t\tcase pgtype.ByteaOID:\n\t\t\t\tvar d []byte\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn d, err\n\t\t\t\t}\n\t\t\tcase pgtype.CIDOID, pgtype.OIDOID, pgtype.XIDOID:\n\t\t\t\tvar d pgtype.Uint32\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\treturn d.Value()\n\t\t\t\t}\n\t\t\tcase pgtype.DateOID:\n\t\t\t\tvar d pgtype.Date\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\treturn d.Value()\n\t\t\t\t}\n\t\t\tcase pgtype.Float4OID:\n\t\t\t\tvar d float32\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn float64(d), err\n\t\t\t\t}\n\t\t\tcase pgtype.Float8OID:\n\t\t\t\tvar d float64\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn d, err\n\t\t\t\t}\n\t\t\tcase pgtype.Int2OID:\n\t\t\t\tvar d int16\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn int64(d), err\n\t\t\t\t}\n\t\t\tcase pgtype.Int4OID:\n\t\t\t\tvar d int32\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn int64(d), err\n\t\t\t\t}\n\t\t\tcase pgtype.Int8OID:\n\t\t\t\tvar d int64\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn d, err\n\t\t\t\t}\n\t\t\tcase pgtype.JSONOID, pgtype.JSONBOID:\n\t\t\t\tvar d []byte\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\treturn d, nil\n\t\t\t\t}\n\t\t\tcase pgtype.TimestampOID:\n\t\t\t\tvar d pgtype.Timestamp\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\treturn d.Value()\n\t\t\t\t}\n\t\t\tcase pgtype.TimestamptzOID:\n\t\t\t\tvar d pgtype.Timestamptz\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\treturn d.Value()\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tvar d string\n\t\t\t\tscanPlan := m.PlanScan(dataTypeOID, format, &d)\n\t\t\t\tr.valueFuncs[i] = func(src []byte) (driver.Value, error) {\n\t\t\t\t\terr := scanPlan.Scan(src, &d)\n\t\t\t\t\treturn d, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvar more bool\n\tif r.skipNext {\n\t\tmore = r.skipNextMore\n\t\tr.skipNext = false\n\t} else {\n\t\tmore = r.rows.Next()\n\t}\n\n\tif !more {\n\t\tif r.rows.Err() == nil {\n\t\t\treturn io.EOF\n\t\t} else {\n\t\t\treturn r.rows.Err()\n\t\t}\n\t}\n\n\tfor i, rv := range r.rows.RawValues() {\n\t\tif rv != nil {\n\t\t\tvar err error\n\t\t\tdest[i], err = r.valueFuncs[i](rv)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"convert field %d failed: %v\", i, err)\n\t\t\t}\n\t\t} else {\n\t\t\tdest[i] = nil\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc namedValueToInterface(argsV []driver.NamedValue) []any {\n\targs := make([]any, 0, len(argsV))\n\tfor _, v := range argsV {\n\t\tif v.Value != nil {\n\t\t\targs = append(args, v.Value.(any))\n\t\t} else {\n\t\t\targs = append(args, nil)\n\t\t}\n\t}\n\treturn args\n}\n\ntype wrapTx struct {\n\tctx context.Context\n\ttx  pgx.Tx\n}\n\nfunc (wtx wrapTx) Commit() error { return wtx.tx.Commit(wtx.ctx) }\n\nfunc (wtx wrapTx) Rollback() error { return wtx.tx.Rollback(wtx.ctx) }\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/manager_internal.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/jackc/pgx/v5/pgxpool\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\n// Manager manages database connections.\ntype Manager struct {\n\truntime    *config.Runtime\n\trt         *reqtrack.RequestTracker\n\tts         *testsupport.Manager\n\trootLogger zerolog.Logger\n\n\tmu  sync.RWMutex\n\tdbs map[string]*Database\n}\n\nfunc NewManager(runtime *config.Runtime, rt *reqtrack.RequestTracker, ts *testsupport.Manager, rootLogger zerolog.Logger) *Manager {\n\treturn &Manager{\n\t\truntime:    runtime,\n\t\trt:         rt,\n\t\tts:         ts,\n\t\trootLogger: rootLogger,\n\t\tdbs:        make(map[string]*Database),\n\t}\n}\n\n// GetCurrentDB gets the database for the current request.\nfunc (mgr *Manager) GetCurrentDB() *Database {\n\tvar dbName string\n\tif curr := mgr.rt.Current(); curr.Req != nil {\n\t\tdbName = curr.Req.Service()\n\t} else if testSvc, _ := mgr.ts.TestService(); testSvc != \"\" {\n\t\tdbName = testSvc\n\t} else {\n\t\tpanic(\"sqldb: no current request\")\n\t}\n\treturn mgr.GetDB(dbName)\n}\n\nfunc (mgr *Manager) GetDB(dbName string) *Database {\n\tmgr.mu.RLock()\n\tdb, ok := mgr.dbs[dbName]\n\tmgr.mu.RUnlock()\n\tif ok {\n\t\treturn db\n\t}\n\n\tmgr.mu.Lock()\n\tdefer mgr.mu.Unlock()\n\t// Check again now that we've re-acquired the mutex\n\tif db, ok := mgr.dbs[dbName]; ok {\n\t\treturn db\n\t}\n\thooks := &hookList{}\n\tpool, found := mgr.getPool(dbName, \"\", hooks)\n\tdb = &Database{\n\t\tname:     dbName,\n\t\torigName: dbName,\n\t\tmgr:      mgr,\n\t\tnoopDB:   !found,\n\t\tpool:     pool,\n\t\thooks:    hooks,\n\t}\n\tmgr.dbs[dbName] = db\n\treturn db\n}\n\n// getPool returns a database connection pool for the given database name.\n// Each time it's called it returns a new pool.\nfunc (mgr *Manager) getPool(encoreName, dbNameOverride string, hooks *hookList) (pool *pgxpool.Pool, found bool) {\n\tvar db *config.SQLDatabase\n\tfor _, d := range mgr.runtime.SQLDatabases {\n\t\tif d.EncoreName == encoreName {\n\t\t\tdb = d\n\t\t\tbreak\n\t\t}\n\t}\n\tif db == nil {\n\t\treturn nil, false\n\t}\n\n\tsrv := mgr.runtime.SQLServers[db.ServerID]\n\tcfg, err := dbConf(srv, db, dbNameOverride)\n\tif err != nil {\n\t\tpanic(\"sqldb: \" + err.Error())\n\t}\n\n\tcfg.ConnConfig.Tracer = &pgxTracer{mgr: mgr}\n\tcfg.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {\n\t\treturn hooks.runAfterConnectHooks(ctx, conn)\n\t}\n\tpool, err = pgxpool.NewWithConfig(context.Background(), cfg)\n\tif err != nil {\n\t\tpanic(\"sqldb: setup db: \" + err.Error())\n\t}\n\n\treturn pool, true\n}\n\nfunc (mgr *Manager) Shutdown(p *shutdown.Process) error {\n\t// Wait for all user code to finish before shutting down databases.\n\t<-p.ServicesShutdownCompleted.Done()\n\t<-p.OutstandingTasks.Done()\n\n\tvar wg sync.WaitGroup\n\tmgr.mu.RLock()\n\tdefer mgr.mu.RUnlock()\n\n\twg.Add(len(mgr.dbs))\n\tfor _, db := range mgr.dbs {\n\t\tdb := db\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tdb.shutdown()\n\t\t}()\n\t}\n\twg.Wait()\n\treturn nil\n}\n\nfunc (mgr *Manager) Named(name string) *Database {\n\treturn mgr.GetDB(name)\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/pgx_tracer_internal.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\n\t\"github.com/jackc/pgx/v5\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\ntype pgxTracer struct {\n\tmgr *Manager\n}\n\ntype ctxKey string\n\nconst (\n\tpgxQueryKey ctxKey = \"pgx_query\"\n\n\t// pgxAlreadyTracedKey is a context key that indicates\n\t// that the query is already traced through the sqldb integration.\n\tpgxAlreadyTracedKey ctxKey = \"pgx_query\"\n)\n\nfunc markTraced(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, pgxAlreadyTracedKey, true)\n}\n\ntype queryValue struct {\n\ttrace       trace2.Logger\n\teventParams trace2.EventParams\n\tstartID     model.TraceEventID\n}\n\nfunc (t *pgxTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {\n\tif ctx.Value(pgxAlreadyTracedKey) != nil {\n\t\treturn ctx\n\t}\n\n\tcurr := t.mgr.rt.Current()\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams := trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartID := curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       data.SQL,\n\t\t\tStack:       stack.Build(5),\n\t\t})\n\t\tctx = context.WithValue(ctx, pgxQueryKey, &queryValue{\n\t\t\ttrace:       curr.Trace,\n\t\t\teventParams: eventParams,\n\t\t\tstartID:     startID,\n\t\t})\n\t}\n\treturn ctx\n}\n\nfunc (t *pgxTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {\n\tif qv, ok := ctx.Value(pgxQueryKey).(*queryValue); ok {\n\t\tqv.trace.DBQueryEnd(qv.eventParams, qv.startID, data.Err)\n\t}\n}\n\nvar (\n\t_ pgx.QueryTracer = (*pgxTracer)(nil)\n\t_ pgx.QueryTracer = (*pgxTracer)(nil)\n)\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/pkgfn.go",
    "content": "//go:build encore_app\n\npackage sqldb\n\nimport (\n\t\"context\"\n)\n\n// NewDatabase declares a new SQL database.\n//\n// Encore uses static analysis to identify databases and their configuration,\n// so all parameters passed to this function must be constant literals.\n//\n// A call to NewDatabase can only be made when declaring a package level variable. Any\n// calls to this function made outside a package level variable declaration will result\n// in a compiler error.\n//\n// The database name must be unique within the Encore application. Database names must be defined\n// in kebab-case (lowercase alphanumerics and hyphen separated). Once created and deployed never\n// change the database name, or else a new database will be created.\nfunc NewDatabase(name string, config DatabaseConfig) *Database {\n\treturn Singleton.GetDB(name)\n}\n\n// DatabaseConfig specifies configuration for declaring a new database.\ntype DatabaseConfig struct {\n\t// Migrations is the directory containing the migration files\n\t// for this database.\n\t//\n\t// The path must be slash-separated relative path, and must be rooted within\n\t// the package directory (it cannot contain \"../\").\n\t// Valid paths are, for example, \"migrations\" or \"db/migrations\".\n\t//\n\t// Migrations are an ordered sequence of sql files of the format <number>_<description>.up.sql.\n\tMigrations string\n}\n\n// Exec executes a query without returning any rows.\n// The args are for any placeholder parameters in the query.\n//\n// See (*database/sql.DB).ExecContext() for additional documentation.\nfunc Exec(ctx context.Context, query string, args ...interface{}) (ExecResult, error) {\n\treturn getCurrentDB().Exec(ctx, query, args...)\n}\n\n// Query executes a query that returns rows, typically a SELECT.\n// The args are for any placeholder parameters in the query.\n//\n// See (*database/sql.DB).QueryContext() for additional documentation.\nfunc Query(ctx context.Context, query string, args ...interface{}) (*Rows, error) {\n\treturn getCurrentDB().Query(ctx, query, args...)\n}\n\n// QueryRow executes a query that is expected to return at most one row.\n//\n// See (*database/sql.DB).QueryRowContext() for additional documentation.\nfunc QueryRow(ctx context.Context, query string, args ...interface{}) *Row {\n\treturn getCurrentDB().QueryRow(ctx, query, args...)\n}\n\n// Begin opens a new database transaction.\n//\n// See (*database/sql.DB).Begin() for additional documentation.\nfunc Begin(ctx context.Context) (*Tx, error) {\n\treturn getCurrentDB().Begin(ctx)\n}\n\n// Commit commits the given transaction.\n//\n// See (*database/sql.Tx).Commit() for additional documentation.\n// Deprecated: use tx.Commit() instead.\nfunc Commit(tx *Tx) error {\n\treturn tx.Commit()\n}\n\n// Rollback rolls back the given transaction.\n//\n// See (*database/sql.Tx).Rollback() for additional documentation.\n// Deprecated: use tx.Rollback() instead.\nfunc Rollback(tx *Tx) error {\n\treturn tx.Rollback()\n}\n\n// ExecTx is like Exec but executes the query in the given transaction.\n//\n// See (*database/sql.Tx).ExecContext() for additional documentation.\n// Deprecated: use tx.Exec() instead.\nfunc ExecTx(tx *Tx, ctx context.Context, query string, args ...interface{}) (ExecResult, error) {\n\treturn tx.Exec(ctx, query, args...)\n}\n\n// QueryTx is like Query but executes the query in the given transaction.\n//\n// See (*database/sql.Tx).QueryContext() for additional documentation.\n// Deprecated: use tx.Query() instead.\nfunc QueryTx(tx *Tx, ctx context.Context, query string, args ...interface{}) (*Rows, error) {\n\treturn tx.Query(ctx, query, args...)\n}\n\n// QueryRowTx is like QueryRow but executes the query in the given transaction.\n//\n// See (*database/sql.Tx).QueryRowContext() for additional documentation.\n// Deprecated: use tx.QueryRow() instead.\nfunc QueryRowTx(tx *Tx, ctx context.Context, query string, args ...interface{}) *Row {\n\treturn tx.QueryRow(ctx, query, args...)\n}\n\n// constStr is a string that can only be provided as a constant.\n//\n//publicapigen:keep\ntype constStr string\n\n// Named returns a database object connected to the database with the given name.\n//\n// The name must be a string literal constant, to facilitate static analysis.\nfunc Named(name constStr) *Database {\n\treturn Singleton.GetDB(string(name))\n}\n\nfunc getCurrentDB() *Database {\n\treturn Singleton.GetCurrentDB()\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/sqldb.go",
    "content": "// Package sqldb provides Encore services direct access to their databases.\n//\n// For the documentation on how to use databases within Encore see https://encore.dev/docs/develop/databases.\npackage sqldb\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/jackc/pgx/v5\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n\t\"encore.dev/beta/errs\"\n)\n\n// ErrNoRows is an error reported by Scan when QueryRow doesn't return a row.\n// It must be tested against with errors.Is.\nvar ErrNoRows = sql.ErrNoRows\n\n// ExecResult is the result of an Exec query.\ntype ExecResult interface {\n\t// RowsAffected returns the number of rows affected. If the result was not\n\t// for a row affecting command (e.g. \"CREATE TABLE\") then it returns 0.\n\tRowsAffected() int64\n}\n\n// Tx is a handle to a database transaction.\n//\n// See *database/sql.Tx for additional documentation.\ntype Tx struct {\n\tmgr *Manager\n\tstd pgx.Tx\n\n\tstartID model.TraceEventID\n}\n\n// Commit commits the given transaction.\n//\n// See (*database/sql.Tx).Commit() for additional documentation.\nfunc (tx *Tx) Commit() error { return tx.commit() }\n\n// Rollback rolls back the given transaction.\n//\n// See (*database/sql.Tx).Rollback() for additional documentation.\nfunc (tx *Tx) Rollback() error { return tx.rollback() }\n\nfunc (tx *Tx) commit() error {\n\terr := tx.std.Commit(markTraced(context.Background()))\n\terr = convertErr(err)\n\n\tif curr := tx.mgr.rt.Current(); curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.DBTransactionEnd(trace2.DBTransactionEndParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\tDefLoc:  0,\n\t\t\t},\n\t\t\tStartID: tx.startID,\n\t\t\tCommit:  true,\n\t\t\tErr:     err,\n\t\t\tStack:   stack.Build(4),\n\t\t})\n\t}\n\n\treturn err\n}\n\nfunc (tx *Tx) rollback() error {\n\terr := tx.std.Rollback(markTraced(context.Background()))\n\terr = convertErr(err)\n\n\tif curr := tx.mgr.rt.Current(); curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.DBTransactionEnd(trace2.DBTransactionEndParams{\n\t\t\tEventParams: trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\tDefLoc:  0,\n\t\t\t},\n\t\t\tStartID: tx.startID,\n\t\t\tCommit:  false,\n\t\t\tErr:     err,\n\t\t\tStack:   stack.Build(4),\n\t\t})\n\t}\n\n\treturn err\n}\n\nfunc (tx *Tx) Exec(ctx context.Context, query string, args ...interface{}) (ExecResult, error) {\n\treturn tx.exec(ctx, query, args...)\n}\n\nfunc (tx *Tx) exec(ctx context.Context, query string, args ...interface{}) (ExecResult, error) {\n\tcurr := tx.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tTxStartID:   tx.startID,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(4),\n\t\t})\n\t}\n\n\tres, err := tx.std.Exec(markTraced(ctx), query, args...)\n\terr = convertErr(err)\n\n\tif startEventID > 0 {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn res, err\n}\n\nfunc (tx *Tx) Query(ctx context.Context, query string, args ...interface{}) (*Rows, error) {\n\tcurr := tx.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tTxStartID:   tx.startID,\n\t\t\tStack:       stack.Build(4),\n\t\t})\n\t}\n\n\trows, err := tx.std.Query(markTraced(ctx), query, args...)\n\terr = convertErr(err)\n\n\tif startEventID > 0 {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Rows{std: rows}, nil\n}\n\nfunc (tx *Tx) QueryRow(ctx context.Context, query string, args ...interface{}) *Row {\n\tcurr := tx.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tcurr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tTxStartID:   tx.startID,\n\t\t\tStack:       stack.Build(4),\n\t\t})\n\t}\n\n\t// pgx currently does not support .Err() on Row.\n\t// Work around this by using Query.\n\trows, err := tx.std.Query(markTraced(ctx), query, args...)\n\terr = convertErr(err)\n\tr := &Row{rows: rows, err: err}\n\n\tif startEventID > 0 {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn r\n}\n\n// Rows is the result of a query. Its cursor starts before the first row\n// of the result set. Use Next to advance from row to row.\n//\n// See *database/sql.Rows for additional documentation.\ntype Rows struct {\n\tstd pgx.Rows\n}\n\n// Close closes the Rows, preventing further enumeration.\n//\n// See (*database/sql.Rows).Close() for additional documentation.\nfunc (r *Rows) Close() { r.std.Close() }\n\n// Scan copies the columns in the current row into the values pointed\n// at by dest. The number of values in dest must be the same as the\n// number of columns in Rows.\n//\n// See (*database/sql.Rows).Scan() for additional documentation.\nfunc (r *Rows) Scan(dest ...interface{}) error { return r.std.Scan(dest...) }\n\n// Err returns the error, if any, that was encountered during iteration.\n// Err may be called after an explicit or implicit Close.\n//\n// See (*database/sql.Rows).Err() for additional documentation.\nfunc (r *Rows) Err() error { return r.std.Err() }\n\n// Next prepares the next result row for reading with the Scan method. It\n// returns true on success, or false if there is no next result row or an error\n// happened while preparing it. Err should be consulted to distinguish between\n// the two cases.\n//\n// Every call to Scan, even the first one, must be preceded by a call to Next.\n//\n// See (*database/sql.Rows).Next() for additional documentation.\nfunc (r *Rows) Next() bool { return r.std.Next() }\n\n// Row is the result of calling QueryRow to select a single row.\n//\n// See *database/sql.Row for additional documentation.\ntype Row struct {\n\trows pgx.Rows\n\terr  error\n}\n\n// Scan copies the columns from the matched row into the values\n// pointed at by dest.\n//\n// See (*database/sql.Row).Scan() for additional documentation.\nfunc (r *Row) Scan(dest ...interface{}) error {\n\tif r.err != nil {\n\t\treturn r.err\n\t}\n\tif !r.rows.Next() {\n\t\tif err := r.rows.Err(); err != nil {\n\t\t\treturn convertErr(err)\n\t\t}\n\t\treturn errs.DropStackFrame(errs.WrapCode(sql.ErrNoRows, errs.NotFound, \"\"))\n\t}\n\t_ = r.rows.Scan(dest...)\n\tr.rows.Close()\n\treturn convertErr(r.rows.Err())\n}\n\nfunc (r *Row) Err() error {\n\tif r.err != nil {\n\t\treturn r.err\n\t}\n\treturn convertErr(r.rows.Err())\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/sqldb_test.go",
    "content": "package sqldb\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t_ \"unsafe\" // for go:linkname\n\n\t\"encore.dev/appruntime/exported/config\"\n)\n\nfunc TestDBConf(t *testing.T) {\n\t// Unset all the \"PG*\" variables that may affect the test.\n\tfor _, env := range os.Environ() {\n\t\tkey, _, _ := strings.Cut(env, \"=\")\n\t\tif strings.HasPrefix(key, \"PG\") {\n\t\t\t_ = os.Unsetenv(key)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tSrv      *config.SQLServer\n\t\tDB       *config.SQLDatabase\n\t\tHost     string\n\t\tPort     uint16\n\t\tMaxConns uint32\n\t}{\n\t\t{\n\t\t\tSrv: &config.SQLServer{\n\t\t\t\tHost: \"/cloudsql/foo\",\n\t\t\t},\n\t\t\tDB: &config.SQLDatabase{\n\t\t\t\tEncoreName:     \"ignore\",\n\t\t\t\tDatabaseName:   \"dbname\",\n\t\t\t\tUser:           \"user\",\n\t\t\t\tPassword:       \"password\",\n\t\t\t\tMaxConnections: 10,\n\t\t\t},\n\t\t\tHost:     \"/cloudsql/foo\",\n\t\t\tPort:     5432,\n\t\t\tMaxConns: 10,\n\t\t},\n\t\t{\n\t\t\tSrv: &config.SQLServer{\n\t\t\t\tHost: \"test:123\",\n\t\t\t},\n\t\t\tDB: &config.SQLDatabase{\n\t\t\t\tEncoreName:     \"ignore\",\n\t\t\t\tDatabaseName:   \"dbname\",\n\t\t\t\tUser:           \"user\",\n\t\t\t\tPassword:       \"password\",\n\t\t\t\tMaxConnections: 0,\n\t\t\t},\n\t\t\tHost:     \"test\",\n\t\t\tPort:     123,\n\t\t\tMaxConns: 30,\n\t\t},\n\t\t{\n\t\t\tSrv: &config.SQLServer{\n\t\t\t\tHost: \"hostname\",\n\t\t\t},\n\t\t\tDB: &config.SQLDatabase{\n\t\t\t\tEncoreName:     \"ignore\",\n\t\t\t\tDatabaseName:   \"dbname\",\n\t\t\t\tUser:           \"user\",\n\t\t\t\tPassword:       \"password\",\n\t\t\t\tMaxConnections: 100,\n\t\t\t},\n\t\t\tHost:     \"hostname\",\n\t\t\tPort:     5432,\n\t\t\tMaxConns: 100,\n\t\t},\n\t}\n\n\tfor i, test := range tests {\n\t\tcfg, err := dbConf(test.Srv, test.DB, \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"test %d: unexpected error: %v\", i, err)\n\t\t}\n\n\t\tif cfg.ConnConfig.Host != test.Host {\n\t\t\tt.Fatalf(\"test %d: got host %s, want %q\", i, cfg.ConnConfig.Host, test.Host)\n\t\t} else if cfg.ConnConfig.Port != test.Port {\n\t\t\tt.Fatalf(\"test %d: got port %d, want %d\", i, cfg.ConnConfig.Port, test.Port)\n\t\t} else if cfg.ConnConfig.Database != test.DB.DatabaseName {\n\t\t\tt.Fatalf(\"test %d: got db %s, want %q\", i, cfg.ConnConfig.Database, test.DB.DatabaseName)\n\t\t} else if cfg.ConnConfig.User != test.DB.User {\n\t\t\tt.Fatalf(\"test %d: got user %s, want %q\", i, cfg.ConnConfig.User, test.DB.User)\n\t\t} else if cfg.ConnConfig.Password != test.DB.Password {\n\t\t\tt.Fatalf(\"test %d: got password %s, want %q\", i, cfg.ConnConfig.Password, test.DB.Password)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/sqlerr/sqlerr.go",
    "content": "// Package sqlerr provides a set of common error codes for SQL datbaases.\n//\n// Not all error codes are supported by all databases, and multiple\n// underlying database errors may map to the same error code if the\n// meaning overlaps.\n//\n// In future releases additional error codes may be added to this\n// package, and the mapping of underlying database errors to these\n// error codes may change.\npackage sqlerr\n\n// Code describes a specific type of database error.\n// The value Other is reported when an error does not map to any of the defined codes.\n//\n// Not all error codes are supported by all databases, and multiple\n// underlying database errors may map to the same error code if the\n// meaning overlaps.\ntype Code string\n\nconst (\n\t// Other is reported when an error does not map to any of the defined codes.\n\t// Consult the underlying database error for more information.\n\tOther Code = \"other\"\n\n\t// NotNullViolation is reported when a not null constraint would be violated.\n\tNotNullViolation Code = \"not_null_violation\"\n\n\t// ForeignKeyViolation is reported when a foreign key constraint would be violated.\n\tForeignKeyViolation Code = \"foreign_key_violation\"\n\n\t// UniqueViolation is reported when a unique constraint would be violated.\n\tUniqueViolation Code = \"unique_violation\"\n\n\t// CheckViolation is reported when a check constraint would be violated.\n\tCheckViolation Code = \"check_violation\"\n\n\t// ExcludeViolation is reported when an exclusion constraint would be violated.\n\tExcludeViolation Code = \"exclude_violation\"\n\n\t// TransactionFailed is reported when running a command in a failed transaction,\n\t// due to some previous command failure.\n\tTransactionFailed Code = \"transaction_failed\"\n\n\t// DeadlockDetected is reported when a deadlock is detected.\n\t// Deadlock detection is done on a best-effort basis and not all deadlocks\n\t// can be detected.\n\tDeadlockDetected Code = \"deadlock_detected\"\n\n\t// TooManyConnections is reported when the database rejects a connection request\n\t// due to reaching the maximum number of connections.\n\t// This is different from blocking waiting on a connection pool.\n\tTooManyConnections Code = \"too_many_connections\"\n)\n\n// MapCode maps an underlying database error to an Code.\n//\n//publicapigen:drop\nfunc MapCode(code string) Code {\n\tswitch code {\n\tcase \"23502\":\n\t\treturn NotNullViolation\n\tcase \"23503\":\n\t\treturn ForeignKeyViolation\n\tcase \"23505\":\n\t\treturn UniqueViolation\n\tcase \"23514\":\n\t\treturn CheckViolation\n\tcase \"23P01\":\n\t\treturn ExcludeViolation\n\tcase \"25P02\":\n\t\treturn TransactionFailed\n\tcase \"40P01\":\n\t\treturn DeadlockDetected\n\tcase \"53300\":\n\t\treturn TooManyConnections\n\tdefault:\n\t\treturn Other\n\t}\n}\n\n// Severity defines the severity of a database error.\ntype Severity string\n\nconst (\n\tSeverityError   Severity = \"ERROR\"\n\tSeverityFatal   Severity = \"FATAL\"\n\tSeverityPanic   Severity = \"PANIC\"\n\tSeverityWarning Severity = \"WARNING\"\n\tSeverityNotice  Severity = \"NOTICE\"\n\tSeverityDebug   Severity = \"DEBUG\"\n\tSeverityInfo    Severity = \"INFO\"\n\tSeverityLog     Severity = \"LOG\"\n)\n\n// MapSeverity maps the severity string from the underlying database\n// to a Severity.\n//\n//publicapigen:drop\nfunc MapSeverity(severity string) Severity {\n\tswitch severity {\n\tcase \"ERROR\":\n\t\treturn SeverityError\n\tcase \"FATAL\":\n\t\treturn SeverityFatal\n\tcase \"PANIC\":\n\t\treturn SeverityPanic\n\tcase \"WARNING\":\n\t\treturn SeverityWarning\n\tcase \"NOTICE\":\n\t\treturn SeverityNotice\n\tcase \"DEBUG\":\n\t\treturn SeverityDebug\n\tcase \"INFO\":\n\t\treturn SeverityInfo\n\tcase \"LOG\":\n\t\treturn SeverityLog\n\tdefault:\n\t\treturn SeverityError\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/stdlib.go",
    "content": "package sqldb\n\nimport (\n\t\"database/sql\"\n\t\"database/sql/driver\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"encore.dev/storage/sqldb/internal/stdlibdriver\"\n)\n\n// RegisterStdlibDriver returns a connection string that can be used with\n// the standard library's sql.Open function to connect to the same db.\n//\n// The connection string should be used with the \"encore\" driver name:\n//\n//\tconnStr := sqldb.RegisterStdlibDriver(myDB)\n//\tdb, err := sql.Open(\"encore\", connStr)\n//\n// The main use case is to support libraries that expect to call sql.Open\n// themselves without exposing the underlying database credentials.\nfunc RegisterStdlibDriver(db *Database) string {\n\tif db == nil {\n\t\tpanic(\"sqldb.StdlibDriver: received nil db\")\n\t}\n\n\t// Initialize the standard library integration for this db.\n\t_ = db.Stdlib()\n\n\treturn registeredAdapters.Register(db)\n}\n\n// combinedDriver combines driver.Driver and driver.DriverContext into a single interface,\n// for drivers that support both interfaces.\ntype combinedDriver interface {\n\tdriver.Driver\n\tdriver.DriverContext\n}\n\nvar (\n\tstdlibDriverOnce      sync.Once\n\tstdlibDriverSingleton combinedDriver\n)\n\n// stdlibDriver registers the stdlib driver.\n// It uses a sync.Once so it's safe to call multiple times.\nfunc registerStdlibDriver(mgr *Manager) combinedDriver {\n\tstdlibDriverOnce.Do(func() {\n\t\td := &wrappedDriver{\n\t\t\tparent: stdlibdriver.GetDefaultDriver(),\n\t\t\tmw:     &interceptor{mgr: mgr},\n\t\t}\n\t\tsql.Register(stdlibDriverName, d)\n\t\tstdlibDriverSingleton = d\n\t})\n\treturn stdlibDriverSingleton\n}\n\nconst stdlibDriverName = \"__encore_stdlib\"\n\n// registeredAdapters is the singleton registry of all databases that have been\n// registered for stdlib adapters.\nvar registeredAdapters = &adapterRegistry{\n\tnameToID: make(map[string]string),\n\tidToDB:   make(map[string]*Database),\n}\n\ntype adapterRegistry struct {\n\tmu       sync.Mutex\n\tnameToID map[string]string\n\tidToDB   map[string]*Database\n}\n\nfunc (r *adapterRegistry) Register(db *Database) string {\n\tregisterAdapterDriver(db.mgr)\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\t// If it's already registered, return the same identifier.\n\tif id, ok := r.nameToID[db.name]; ok {\n\t\treturn id\n\t}\n\n\tident := fmt.Sprintf(\"encore/stdlibdriver/%s\", db.name)\n\tr.nameToID[db.name] = ident\n\tr.idToDB[ident] = db\n\treturn ident\n}\n\nfunc (r *adapterRegistry) Get(ident string) (*Database, bool) {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\tdb, ok := r.idToDB[ident]\n\treturn db, ok\n}\n\nconst adapterDriverName = \"encore\"\n\ntype adapterDriver struct{}\n\nfunc (adapterDriver) Open(ident string) (driver.Conn, error) {\n\tdb, ok := registeredAdapters.Get(ident)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"sqldb: unknown database %q (did you register it with sqldb.StdlibDriver?)\", ident)\n\t} else if db.noopDB {\n\t\treturn nil, errNoopDB\n\t}\n\treturn registerStdlibDriver(db.mgr).Open(db.connStr)\n}\n\nfunc (adapterDriver) OpenConnector(ident string) (driver.Connector, error) {\n\tdb, ok := registeredAdapters.Get(ident)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"sqldb: unknown database %q (did you register it with sqldb.StdlibDriver?)\", ident)\n\t} else if db.noopDB {\n\t\treturn nil, errNoopDB\n\t}\n\treturn registerStdlibDriver(db.mgr).OpenConnector(db.connStr)\n}\n\nvar (\n\tadapterDriverOnce      sync.Once\n\tadapterDriverSingleton combinedDriver\n)\n\n// stdlibDriver registers the stdlib driver.\n// It uses a sync.Once so it's safe to call multiple times.\nfunc registerAdapterDriver(mgr *Manager) combinedDriver {\n\tadapterDriverOnce.Do(func() {\n\t\td := &wrappedDriver{\n\t\t\tparent: adapterDriver{},\n\t\t\tmw:     &interceptor{mgr: mgr},\n\t\t}\n\t\tsql.Register(adapterDriverName, d)\n\t\tadapterDriverSingleton = d\n\t})\n\treturn adapterDriverSingleton\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/stdlib_noop_internal.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"database/sql/driver\"\n\t\"sync\"\n)\n\nvar (\n\tregisterNoopDriverOnce sync.Once\n)\n\nconst noopDriverName = \"__encore_noop\"\n\n// noopDriver is a driver.Driver that returns will \"connect\" and manage\n// a connection to a database without an error, however whenever you try and use\n// the database, it will return a noop error.\n//\n// This is because we expect people to use drivers as package level variables,\n// but some services may not be running\ntype noopDriver struct{}\n\nvar (\n\t_ driver.Driver        = noopDriver{}\n\t_ driver.DriverContext = noopDriver{}\n)\n\nfunc (n noopDriver) Open(name string) (driver.Conn, error) {\n\treturn noopConn{}, nil\n}\n\nfunc (n noopDriver) OpenConnector(name string) (driver.Connector, error) {\n\treturn noopConnector{}, nil\n}\n\n// noopConnector is a driver.Connector that returns will \"connect\" and\n// manage a connection to a database without an error using the noopConn\ntype noopConnector struct{}\n\nvar (\n\t_ driver.Connector = noopConnector{}\n)\n\nfunc (n noopConnector) Connect(ctx context.Context) (driver.Conn, error) {\n\treturn noopConn{}, nil\n}\n\nfunc (n noopConnector) Driver() driver.Driver {\n\treturn noopDriver{}\n}\n\n// noopConn is a driver.Conn that will error on any operation not\n// related to managing the connection\ntype noopConn struct{}\n\nvar (\n\t_ driver.Conn               = noopConn{}\n\t_ driver.ConnBeginTx        = noopConn{}\n\t_ driver.ConnPrepareContext = noopConn{}\n\t_ driver.ExecerContext      = noopConn{}\n\t_ driver.Pinger             = noopConn{}\n\t_ driver.QueryerContext     = noopConn{}\n\t_ driver.SessionResetter    = noopConn{}\n\t_ driver.NamedValueChecker  = noopConn{}\n)\n\nfunc (n noopConn) Prepare(query string) (driver.Stmt, error) {\n\treturn nil, errNoopDB\n}\n\nfunc (n noopConn) Close() error {\n\treturn nil // Don't return an error here as we want \"connections\" to act normal until they are used\n}\n\nfunc (n noopConn) Begin() (driver.Tx, error) {\n\treturn nil, errNoopDB\n}\n\nfunc (n noopConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {\n\treturn nil, errNoopDB\n}\n\nfunc (n noopConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {\n\treturn nil, errNoopDB\n}\n\nfunc (n noopConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {\n\treturn nil, errNoopDB\n}\n\nfunc (n noopConn) Ping(ctx context.Context) error {\n\treturn nil // Don't return an error here as we want \"connections\" to act normal until they are used\n}\n\nfunc (n noopConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {\n\treturn nil, errNoopDB\n}\n\nfunc (n noopConn) ResetSession(ctx context.Context) error {\n\treturn nil // Don't return an error here as we want \"connections\" to act normal until they are used\n}\n\nfunc (n noopConn) CheckNamedValue(value *driver.NamedValue) error {\n\treturn errNoopDB\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/stdlib_wrapper_internal.go",
    "content": "/*\n\nThis file is adapted from of github.com/ngrok/sqlmw, with license:\n\nMIT License\n\nCopyright (c) 2017 Expansive Worlds\nCopyright (c) 2017 Avalanche Studios\nCopyright (c) 2020 Alan Shreve\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*/\n\npackage sqldb\n\nimport (\n\t\"context\"\n\t\"database/sql/driver\"\n\t\"errors\"\n\n\t\"encore.dev/appruntime/exported/model\"\n\t\"encore.dev/appruntime/exported/stack\"\n\t\"encore.dev/appruntime/exported/trace2\"\n)\n\ntype middleware interface {\n\tConnExec(context.Context, driver.ExecerContext, string, []driver.NamedValue) (driver.Result, error)\n\tConnQuery(context.Context, driver.QueryerContext, string, []driver.NamedValue) (driver.Rows, error)\n\tStmtExec(context.Context, driver.StmtExecContext, string, []driver.NamedValue) (driver.Result, error)\n\tStmtQuery(context.Context, driver.StmtQueryContext, string, []driver.NamedValue) (driver.Rows, error)\n\n\tConnBegin(tx driver.Tx) (driver.Tx, error)\n\tTxCommit(context.Context, driver.Tx) error\n\tTxRollback(context.Context, driver.Tx) error\n}\n\ntype interceptor struct {\n\tmgr *Manager\n}\n\nvar _ middleware = (*interceptor)(nil)\n\nfunc (i *interceptor) ConnQuery(ctx context.Context, conn driver.QueryerContext, query string, args []driver.NamedValue) (driver.Rows, error) {\n\tcurr := i.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(5),\n\t\t})\n\t}\n\n\trows, err := conn.QueryContext(markTraced(ctx), query, args)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn rows, err\n}\n\nfunc (i *interceptor) ConnExec(ctx context.Context, conn driver.ExecerContext, query string, args []driver.NamedValue) (driver.Result, error) {\n\tcurr := i.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\n\t\tstartEventID = curr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(5),\n\t\t})\n\t}\n\n\tres, err := conn.ExecContext(markTraced(ctx), query, args)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn res, err\n}\n\nfunc (i *interceptor) StmtQuery(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {\n\tcurr := i.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tcurr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(5),\n\t\t})\n\t}\n\n\trows, err := conn.QueryContext(markTraced(ctx), args)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn rows, err\n}\n\nfunc (i *interceptor) StmtExec(ctx context.Context, conn driver.StmtExecContext, query string, args []driver.NamedValue) (driver.Result, error) {\n\tcurr := i.mgr.rt.Current()\n\n\tvar (\n\t\tstartEventID model.TraceEventID\n\t\teventParams  trace2.EventParams\n\t)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams = trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tcurr.Trace.DBQueryStart(trace2.DBQueryStartParams{\n\t\t\tEventParams: eventParams,\n\t\t\tQuery:       query,\n\t\t\tStack:       stack.Build(5),\n\t\t})\n\t}\n\n\tres, err := conn.ExecContext(markTraced(ctx), args)\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\tcurr.Trace.DBQueryEnd(eventParams, startEventID, err)\n\t}\n\n\treturn res, err\n}\n\nfunc (i *interceptor) ConnBegin(tx driver.Tx) (driver.Tx, error) {\n\tcurr := i.mgr.rt.Current()\n\n\tvar startEventID model.TraceEventID\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams := trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBTransactionStart(eventParams, stack.Build(5))\n\t}\n\n\treturn stdlibTx{Tx: tx, startID: startEventID}, nil\n}\n\nfunc (i *interceptor) ConnBeginTx(ctx context.Context, conn driver.ConnBeginTx, opts driver.TxOptions) (driver.Tx, error) {\n\ttx, err := conn.BeginTx(markTraced(ctx), opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcurr := i.mgr.rt.Current()\n\n\tvar startEventID model.TraceEventID\n\n\tif curr.Req != nil && curr.Trace != nil {\n\t\teventParams := trace2.EventParams{\n\t\t\tTraceID: curr.Req.TraceID,\n\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\tGoid:    curr.Goctr,\n\t\t\tDefLoc:  0,\n\t\t}\n\t\tstartEventID = curr.Trace.DBTransactionStart(eventParams, stack.Build(5))\n\t}\n\n\treturn stdlibTx{Tx: tx, startID: startEventID}, nil\n}\n\ntype stdlibTx struct {\n\tdriver.Tx\n\n\tstartID model.TraceEventID\n}\n\nfunc (i *interceptor) TxCommit(ctx context.Context, tx driver.Tx) error {\n\terr := tx.Commit()\n\n\tif s, ok := tx.(stdlibTx); ok {\n\t\tcurr := i.mgr.rt.Current()\n\t\tif curr.Req != nil && curr.Trace != nil {\n\t\t\teventParams := trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\tDefLoc:  0,\n\t\t\t}\n\t\t\tcurr.Trace.DBTransactionEnd(trace2.DBTransactionEndParams{\n\t\t\t\tEventParams: eventParams,\n\t\t\t\tStartID:     s.startID,\n\t\t\t\tCommit:      true,\n\t\t\t\tErr:         err,\n\t\t\t\tStack:       stack.Build(5),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn err\n}\n\nfunc (i *interceptor) TxRollback(ctx context.Context, tx driver.Tx) error {\n\terr := tx.Rollback()\n\n\tif s, ok := tx.(stdlibTx); ok {\n\t\tcurr := i.mgr.rt.Current()\n\t\tif curr.Req != nil && curr.Trace != nil {\n\t\t\teventParams := trace2.EventParams{\n\t\t\t\tTraceID: curr.Req.TraceID,\n\t\t\t\tSpanID:  curr.Req.SpanID,\n\t\t\t\tGoid:    curr.Goctr,\n\t\t\t\tDefLoc:  0,\n\t\t\t}\n\t\t\tcurr.Trace.DBTransactionEnd(trace2.DBTransactionEndParams{\n\t\t\t\tEventParams: eventParams,\n\t\t\t\tStartID:     s.startID,\n\t\t\t\tCommit:      false,\n\t\t\t\tErr:         err,\n\t\t\t\tStack:       stack.Build(5),\n\t\t\t})\n\t\t}\n\t}\n\treturn err\n}\n\ntype wrappedDriver struct {\n\tparent driver.Driver\n\tmw     middleware\n}\n\nvar (\n\t_ driver.Driver        = wrappedDriver{}\n\t_ driver.DriverContext = wrappedDriver{}\n)\n\nfunc (d wrappedDriver) Open(name string) (driver.Conn, error) {\n\tconn, err := d.parent.Open(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn wrappedConn{mw: d.mw, parent: conn}, nil\n}\n\nfunc (d wrappedDriver) OpenConnector(name string) (driver.Connector, error) {\n\tdriver, ok := d.parent.(driver.DriverContext)\n\tif !ok {\n\t\treturn wrappedConnector{\n\t\t\tparent:    dsnConnector{dsn: name, driver: d.parent},\n\t\t\tdriverRef: &d,\n\t\t}, nil\n\t}\n\tconn, err := driver.OpenConnector(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn wrappedConnector{parent: conn, driverRef: &d}, nil\n}\n\ntype wrappedConnector struct {\n\tparent    driver.Connector\n\tdriverRef *wrappedDriver\n}\n\nvar (\n\t_ driver.Connector = wrappedConnector{}\n)\n\nfunc (c wrappedConnector) Connect(ctx context.Context) (conn driver.Conn, err error) {\n\tconn, err = c.parent.Connect(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn wrappedConn{mw: c.driverRef.mw, parent: conn}, nil\n}\n\nfunc (c wrappedConnector) Driver() driver.Driver {\n\treturn c.driverRef\n}\n\n// dsnConnector is a fallback connector placed in position of wrappedConnector.parent\n// when given Driver does not comply with DriverContext interface.\ntype dsnConnector struct {\n\tdsn    string\n\tdriver driver.Driver\n}\n\nfunc (t dsnConnector) Connect(_ context.Context) (driver.Conn, error) {\n\treturn t.driver.Open(t.dsn)\n}\n\nfunc (t dsnConnector) Driver() driver.Driver {\n\treturn t.driver\n}\n\ntype wrappedConn struct {\n\tmw     middleware\n\tparent driver.Conn\n}\n\n// Compile time validation that our types implement the expected interfaces\nvar (\n\t_ driver.Conn               = wrappedConn{}\n\t_ driver.ConnBeginTx        = wrappedConn{}\n\t_ driver.ConnPrepareContext = wrappedConn{}\n\t_ driver.ExecerContext      = wrappedConn{}\n\t_ driver.Pinger             = wrappedConn{}\n\t_ driver.QueryerContext     = wrappedConn{}\n\t_ driver.SessionResetter    = wrappedConn{}\n\t_ driver.NamedValueChecker  = wrappedConn{}\n)\n\nfunc (c wrappedConn) Prepare(query string) (driver.Stmt, error) {\n\tstmt, err := c.parent.Prepare(query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn wrappedStmt{mw: c.mw, query: query, parent: stmt, conn: c}, nil\n}\n\nfunc (c wrappedConn) Close() error {\n\treturn c.parent.Close()\n}\n\nfunc (c wrappedConn) Begin() (tx driver.Tx, err error) {\n\ttx, err = c.parent.Begin()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttx, err = c.mw.ConnBegin(tx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn wrappedTx{mw: c.mw, parent: tx}, nil\n}\n\nfunc (c wrappedConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {\n\twrappedParent := wrappedParentConn{c.parent}\n\ttx, err = wrappedParent.BeginTx(ctx, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttx, err = c.mw.ConnBegin(tx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn wrappedTx{mw: c.mw, ctx: ctx, parent: tx}, nil\n}\n\nfunc (c wrappedConn) PrepareContext(ctx context.Context, query string) (stmt driver.Stmt, err error) {\n\twrappedParent := wrappedParentConn{c.parent}\n\tstmt, err = wrappedParent.PrepareContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn wrappedStmt{mw: c.mw, ctx: ctx, query: query, parent: stmt, conn: c}, nil\n}\n\nfunc (c wrappedConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (r driver.Result, err error) {\n\twrappedParent := wrappedParentConn{c.parent}\n\treturn c.mw.ConnExec(ctx, wrappedParent, query, args)\n}\n\nfunc (c wrappedConn) Ping(ctx context.Context) (err error) {\n\tif pinger, ok := c.parent.(driver.Pinger); ok {\n\t\treturn pinger.Ping(ctx)\n\t}\n\treturn nil\n}\n\nfunc (c wrappedConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (rows driver.Rows, err error) {\n\t// Quick skip path: If the wrapped connection implements neither QueryerContext nor Queryer, we have absolutely nothing to do\n\t_, hasQueryerContext := c.parent.(driver.QueryerContext)\n\t_, hasQueryer := c.parent.(driver.Queryer)\n\tif !hasQueryerContext && !hasQueryer {\n\t\treturn nil, driver.ErrSkip\n\t}\n\twrappedParent := wrappedParentConn{c.parent}\n\treturn c.mw.ConnQuery(ctx, wrappedParent, query, args)\n}\n\nfunc (c wrappedConn) ResetSession(ctx context.Context) error {\n\tif conn, ok := c.parent.(driver.SessionResetter); ok {\n\t\treturn conn.ResetSession(ctx)\n\t}\n\treturn nil\n}\n\nfunc defaultCheckNamedValue(nv *driver.NamedValue) (err error) {\n\tnv.Value, err = driver.DefaultParameterConverter.ConvertValue(nv.Value)\n\treturn err\n}\n\nfunc (c wrappedConn) CheckNamedValue(v *driver.NamedValue) error {\n\tif checker, ok := c.parent.(driver.NamedValueChecker); ok {\n\t\treturn checker.CheckNamedValue(v)\n\t}\n\n\treturn defaultCheckNamedValue(v)\n}\n\ntype wrappedParentConn struct {\n\tdriver.Conn\n}\n\nfunc (c wrappedParentConn) BeginTx(ctx context.Context, opts driver.TxOptions) (tx driver.Tx, err error) {\n\tif connBeginTx, ok := c.Conn.(driver.ConnBeginTx); ok {\n\t\treturn connBeginTx.BeginTx(ctx, opts)\n\t}\n\t// Fallback implementation\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tdefault:\n\t\treturn c.Conn.Begin()\n\t}\n}\n\nfunc (c wrappedParentConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) {\n\tif connPrepareCtx, ok := c.Conn.(driver.ConnPrepareContext); ok {\n\t\treturn connPrepareCtx.PrepareContext(ctx, query)\n\t}\n\t// Fallback implementation\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tdefault:\n\t\treturn c.Conn.Prepare(query)\n\t}\n}\n\nfunc (c wrappedParentConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (r driver.Result, err error) {\n\tif execContext, ok := c.Conn.(driver.ExecerContext); ok {\n\t\treturn execContext.ExecContext(ctx, query, args)\n\t}\n\t// Fallback implementation\n\tdargs, err := namedValueToValue(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tdefault:\n\t\treturn c.Conn.(driver.Execer).Exec(query, dargs)\n\t}\n}\n\nfunc (c wrappedParentConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (rows driver.Rows, err error) {\n\tif queryerContext, ok := c.Conn.(driver.QueryerContext); ok {\n\t\treturn queryerContext.QueryContext(ctx, query, args)\n\t}\n\t// Fallback implementation\n\tdargs, err := namedValueToValue(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\tdefault:\n\t\treturn c.Conn.(driver.Queryer).Query(query, dargs)\n\t}\n}\n\n// namedValueToValue is a helper function copied from the database/sql package\nfunc namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) {\n\tdargs := make([]driver.Value, len(named))\n\tfor n, param := range named {\n\t\tif len(param.Name) > 0 {\n\t\t\treturn nil, errors.New(\"sql: driver does not support the use of Named Parameters\")\n\t\t}\n\t\tdargs[n] = param.Value\n\t}\n\treturn dargs, nil\n}\n\ntype wrappedStmt struct {\n\tmw     middleware\n\tctx    context.Context\n\tquery  string\n\tparent driver.Stmt\n\tconn   wrappedConn\n}\n\n// Compile time validation that our types implement the expected interfaces\nvar (\n\t_ driver.Stmt              = wrappedStmt{}\n\t_ driver.StmtExecContext   = wrappedStmt{}\n\t_ driver.StmtQueryContext  = wrappedStmt{}\n\t_ driver.ColumnConverter   = wrappedStmt{}\n\t_ driver.NamedValueChecker = wrappedStmt{}\n)\n\nfunc (s wrappedStmt) Close() (err error) {\n\treturn s.parent.Close()\n}\n\nfunc (s wrappedStmt) NumInput() int {\n\treturn s.parent.NumInput()\n}\n\nfunc (s wrappedStmt) Exec(args []driver.Value) (res driver.Result, err error) {\n\treturn s.parent.Exec(args)\n}\n\nfunc (s wrappedStmt) Query(args []driver.Value) (rows driver.Rows, err error) {\n\treturn s.parent.Query(args)\n}\n\nfunc (s wrappedStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {\n\twrappedParent := wrappedParentStmt{Stmt: s.parent}\n\treturn s.mw.StmtExec(ctx, wrappedParent, s.query, args)\n}\n\nfunc (s wrappedStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {\n\twrappedParent := wrappedParentStmt{Stmt: s.parent}\n\treturn s.mw.StmtQuery(ctx, wrappedParent, s.query, args)\n}\n\nfunc (s wrappedStmt) ColumnConverter(idx int) driver.ValueConverter {\n\tif converter, ok := s.parent.(driver.ColumnConverter); ok {\n\t\treturn converter.ColumnConverter(idx)\n\t}\n\n\treturn driver.DefaultParameterConverter\n}\n\nfunc (s wrappedStmt) CheckNamedValue(v *driver.NamedValue) error {\n\tif checker, ok := s.parent.(driver.NamedValueChecker); ok {\n\t\treturn checker.CheckNamedValue(v)\n\t}\n\n\tif checker, ok := s.conn.parent.(driver.NamedValueChecker); ok {\n\t\treturn checker.CheckNamedValue(v)\n\t}\n\n\treturn defaultCheckNamedValue(v)\n}\n\ntype wrappedParentStmt struct {\n\tdriver.Stmt\n}\n\nfunc (s wrappedParentStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (rows driver.Rows, err error) {\n\tif stmtQueryContext, ok := s.Stmt.(driver.StmtQueryContext); ok {\n\t\treturn stmtQueryContext.QueryContext(ctx, args)\n\t}\n\t// Fallback implementation\n\tdargs, err := namedValueToValue(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tselect {\n\tdefault:\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\t}\n\treturn s.Stmt.Query(dargs)\n}\n\nfunc (s wrappedParentStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) {\n\tif stmtExecContext, ok := s.Stmt.(driver.StmtExecContext); ok {\n\t\treturn stmtExecContext.ExecContext(ctx, args)\n\t}\n\t// Fallback implementation\n\tdargs, err := namedValueToValue(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tselect {\n\tdefault:\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\t}\n\treturn s.Stmt.Exec(dargs)\n}\n\ntype wrappedTx struct {\n\tmw     middleware\n\tctx    context.Context\n\tparent driver.Tx\n}\n\n// Compile time validation that our types implement the expected interfaces\nvar (\n\t_ driver.Tx = wrappedTx{}\n)\n\nfunc (t wrappedTx) Commit() (err error) {\n\treturn t.mw.TxCommit(t.ctx, t.parent)\n}\n\nfunc (t wrappedTx) Rollback() (err error) {\n\treturn t.mw.TxRollback(t.ctx, t.parent)\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/test_db.go",
    "content": "package sqldb\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/jackc/pgx/v5\"\n\t\"github.com/rs/xid\"\n)\n\n//publicapigen:drop\nfunc (mgr *Manager) NewTestDatabase(ctx context.Context, name string) (*Database, error) {\n\tdb := mgr.GetDB(name)\n\tif db.noopDB {\n\t\treturn nil, fmt.Errorf(\"et: unknown database name: %q\", name)\n\t}\n\n\tdbName := db.origName + \"_\" + xid.New().String()\n\ttemplateName := db.origName + \"_template\"\n\tif _, err := db.Exec(ctx, fmt.Sprintf(\"CREATE DATABASE %s TEMPLATE %s\",\n\t\tpgx.Identifier{dbName}.Sanitize(),\n\t\tpgx.Identifier{templateName}.Sanitize(),\n\t)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tclone := &Database{\n\t\tname:     dbName,\n\t\torigName: db.origName,\n\t\tmgr:      mgr,\n\t\thooks:    db.hooks,\n\t}\n\n\tmgr.ts.AddEndCallback(func(t *testing.T) {\n\t\t// Shut down the connection pools and attempt to drop the database.\n\t\tclone.shutdown()\n\t\t_, err := db.Exec(context.Background(), fmt.Sprintf(\"DROP DATABASE %s WITH (FORCE)\",\n\t\t\tpgx.Identifier{dbName}.Sanitize()))\n\t\tif err != nil {\n\t\t\tmgr.rootLogger.Error().Err(err).Str(\"database\", dbName).Msg(\"failed to clean up test database\")\n\t\t}\n\t})\n\treturn clone, nil\n}\n"
  },
  {
    "path": "runtimes/go/storage/sqldb/zzz_singleton_internal.go",
    "content": "//go:build encore_app\n\npackage sqldb\n\nimport (\n\t\"encore.dev/appruntime/shared/appconf\"\n\t\"encore.dev/appruntime/shared/logging\"\n\t\"encore.dev/appruntime/shared/reqtrack\"\n\t\"encore.dev/appruntime/shared/shutdown\"\n\t\"encore.dev/appruntime/shared/testsupport\"\n)\n\n// Initialize the singleton instance.\n// NOTE: This file is named zzz_singleton_internal.go so that\n// the init function is initialized after all the providers\n// have been registered.\n\n//publicapigen:drop\nvar Singleton *Manager\n\nfunc init() {\n\tSingleton = NewManager(appconf.Runtime, reqtrack.Singleton, testsupport.Singleton, logging.RootLogger)\n\tshutdown.Singleton.RegisterShutdownHandler(Singleton.Shutdown)\n}\n"
  },
  {
    "path": "runtimes/go/types/option/option.go",
    "content": "// Package option provides a generic Option type for representing optional values\n// in a more type-safe way than using pointers or zero values.\npackage option\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n)\n\n// Option is a type that represents a value that may or may not be present.\ntype Option[T any] struct {\n\tvalue   T\n\tpresent bool\n}\n\nfunc (o Option[T]) MarshalJSON() ([]byte, error) {\n\tif !o.present {\n\t\treturn []byte(\"null\"), nil\n\t}\n\treturn json.Marshal(o.value)\n}\n\nfunc (o *Option[T]) UnmarshalJSON(data []byte) error {\n\tif string(data) == \"null\" {\n\t\tvar zero T\n\t\to.present = false\n\t\to.value = zero\n\t\treturn nil\n\t}\n\n\to.present = true\n\treturn json.Unmarshal(data, &o.value)\n}\n\n// FromComparable returns Some(v) if v is not zero, and None otherwise.\n// If T implements an IsZero() bool method, that is also used to determine if v is zero.\nfunc FromComparable[T comparable](v T) Option[T] {\n\tvar zero T\n\tif v == zero {\n\t\treturn None[T]()\n\t} else if z, ok := any(v).(interface{ IsZero() bool }); ok && z.IsZero() {\n\t\treturn None[T]()\n\t}\n\treturn Some[T](v)\n}\n\n// FromPointer returns Some(*v) if v is not nil, and None otherwise.\nfunc FromPointer[T any](v *T) Option[T] {\n\tif v == nil {\n\t\treturn None[T]()\n\t}\n\treturn Some[T](*v)\n}\n\n// Some returns an Option with the given value and present set to true.\nfunc Some[T any](v T) Option[T] {\n\treturn Option[T]{value: v, present: true}\n}\n\n// None returns an Option with no value set.\nfunc None[T any]() Option[T] {\n\treturn Option[T]{present: false}\n}\n\n// IsSome returns true if the Option has a value set.\nfunc (o Option[T]) IsSome() bool {\n\treturn o.present\n}\n\n// IsNone returns true if the Option has no value set.\nfunc (o Option[T]) IsNone() bool {\n\treturn !o.present\n}\n\n// IsZero is an alias for IsNone, to support usage in structs with \"omitempty\".\nfunc (o Option[T]) IsZero() bool {\n\treturn !o.present\n}\n\n// Get gets the option value and returns ok==true if present.\n// Commonly used in the \"comma ok\" idiom:\n//\n//\tif val, ok := option.Get(); ok {\n//\t    ...\n//\t}\nfunc (o Option[T]) Get() (val T, ok bool) {\n\treturn o.value, o.present\n}\n\n// GetOrElse returns the value if present, otherwise returns alternative.\nfunc (o Option[T]) GetOrElse(alternative T) T {\n\tif o.present {\n\t\treturn o.value\n\t}\n\treturn alternative\n}\n\n// GetOrElseF returns the value if present, otherwise returns alternative().\nfunc (o Option[T]) GetOrElseF(alternative func() T) T {\n\tif o.present {\n\t\treturn o.value\n\t}\n\treturn alternative()\n}\n\n// MustGet returns the value if present, and otherwise panics.\nfunc (o Option[T]) MustGet() T {\n\tif o.present {\n\t\treturn o.value\n\t}\n\tpanic(\"option is None\")\n}\n\n// OrElse returns an Option with the value if present, otherwise returns Some(alternative).\nfunc (o Option[T]) OrElse(alternative T) Option[T] {\n\tif o.present {\n\t\treturn o\n\t}\n\treturn Some(alternative)\n}\n\n// Contains returns pred(v) if the option contains v, and false otherwise.\nfunc (o Option[T]) Contains(predicate func(v T) bool) bool {\n\tif o.present {\n\t\treturn predicate(o.value)\n\t}\n\treturn false\n}\n\nfunc (o Option[T]) String() string {\n\tif o.present {\n\t\treturn fmt.Sprintf(\"%v\", o.value)\n\t}\n\treturn \"null\"\n}\n\nfunc (o Option[T]) GoString() string {\n\tif o.present {\n\t\treturn fmt.Sprintf(\"option.Some(%v)\", o.value)\n\t}\n\treturn fmt.Sprintf(\"option.None[%T]()\", o.value)\n}\n\n// PtrOrNil returns the value as a pointer if present, or nil otherwise.\nfunc (o Option[T]) PtrOrNil() *T {\n\tif o.present {\n\t\treturn &o.value\n\t}\n\treturn nil\n}\n\n// Equal reports whether a and b are equal, using ==.\n// If both are None, they are considered equal.\nfunc Equal[T comparable](a, b Option[T]) bool {\n\tif a.present != b.present {\n\t\treturn false\n\t}\n\tif !a.present {\n\t\treturn true\n\t}\n\treturn a.value == b.value\n}\n\n// Contains returns true if the option is present and matches the given value.\nfunc Contains[T comparable](option Option[T], matches T) bool {\n\tif option.present {\n\t\treturn option.value == matches\n\t}\n\treturn false\n}\n\n// Map returns an Option with the value mapped by the given function if present, otherwise returns None.\nfunc Map[T, R any](option Option[T], f func(T) R) Option[R] {\n\tif option.present {\n\t\treturn Some(f(option.value))\n\t}\n\treturn None[R]()\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/codec.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"fmt\"\n)\n\n// FromBytes returns a UUID generated from the raw byte slice input.\n// It will return an error if the slice isn't 16 bytes long.\nfunc FromBytes(input []byte) (UUID, error) {\n\tu := UUID{}\n\terr := u.UnmarshalBinary(input)\n\treturn u, err\n}\n\n// FromBytesOrNil returns a UUID generated from the raw byte slice input.\n// Same behavior as FromBytes(), but returns uuid.Nil instead of an error.\nfunc FromBytesOrNil(input []byte) UUID {\n\tuuid, err := FromBytes(input)\n\tif err != nil {\n\t\treturn Nil\n\t}\n\treturn uuid\n}\n\n// FromString returns a UUID parsed from the input string.\n// Input is expected in a form accepted by UnmarshalText.\nfunc FromString(input string) (UUID, error) {\n\tu := UUID{}\n\terr := u.UnmarshalText([]byte(input))\n\treturn u, err\n}\n\n// FromStringOrNil returns a UUID parsed from the input string.\n// Same behavior as FromString(), but returns uuid.Nil instead of an error.\nfunc FromStringOrNil(input string) UUID {\n\tuuid, err := FromString(input)\n\tif err != nil {\n\t\treturn Nil\n\t}\n\treturn uuid\n}\n\n// MarshalText implements the encoding.TextMarshaler interface.\n// The encoding is the same as returned by the String() method.\nfunc (u UUID) MarshalText() ([]byte, error) {\n\treturn []byte(u.String()), nil\n}\n\n// UnmarshalText implements the encoding.TextUnmarshaler interface.\n// Following formats are supported:\n//\n//\t\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\",\n//\t\"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\",\n//\t\"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n//\t\"6ba7b8109dad11d180b400c04fd430c8\"\n//\t\"{6ba7b8109dad11d180b400c04fd430c8}\",\n//\t\"urn:uuid:6ba7b8109dad11d180b400c04fd430c8\"\n//\n// ABNF for supported UUID text representation follows:\n//\n//\tURN := 'urn'\n//\tUUID-NID := 'uuid'\n//\n//\thexdig := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |\n//\t          'a' | 'b' | 'c' | 'd' | 'e' | 'f' |\n//\t          'A' | 'B' | 'C' | 'D' | 'E' | 'F'\n//\n//\thexoct := hexdig hexdig\n//\t2hexoct := hexoct hexoct\n//\t4hexoct := 2hexoct 2hexoct\n//\t6hexoct := 4hexoct 2hexoct\n//\t12hexoct := 6hexoct 6hexoct\n//\n//\thashlike := 12hexoct\n//\tcanonical := 4hexoct '-' 2hexoct '-' 2hexoct '-' 6hexoct\n//\n//\tplain := canonical | hashlike\n//\tuuid := canonical | hashlike | braced | urn\n//\n//\tbraced := '{' plain '}' | '{' hashlike  '}'\n//\turn := URN ':' UUID-NID ':' plain\nfunc (u *UUID) UnmarshalText(text []byte) error {\n\tswitch len(text) {\n\tcase 32:\n\t\treturn u.decodeHashLike(text)\n\tcase 34, 38:\n\t\treturn u.decodeBraced(text)\n\tcase 36:\n\t\treturn u.decodeCanonical(text)\n\tcase 41, 45:\n\t\treturn u.decodeURN(text)\n\tdefault:\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID length %d in string %q\", len(text), text)\n\t}\n}\n\n// decodeCanonical decodes UUID strings that are formatted as defined in RFC-4122 (section 3):\n// \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\".\nfunc (u *UUID) decodeCanonical(t []byte) error {\n\tif t[8] != '-' || t[13] != '-' || t[18] != '-' || t[23] != '-' {\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID format in string %q\", t)\n\t}\n\n\tsrc := t\n\tdst := u[:]\n\n\tfor i, byteGroup := range byteGroups {\n\t\tif i > 0 {\n\t\t\tsrc = src[1:] // skip dash\n\t\t}\n\t\t_, err := hex.Decode(dst[:byteGroup/2], src[:byteGroup])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsrc = src[byteGroup:]\n\t\tdst = dst[byteGroup/2:]\n\t}\n\n\treturn nil\n}\n\n// decodeHashLike decodes UUID strings that are using the following format:\n//\n//\t\"6ba7b8109dad11d180b400c04fd430c8\".\nfunc (u *UUID) decodeHashLike(t []byte) error {\n\tsrc := t[:]\n\tdst := u[:]\n\n\t_, err := hex.Decode(dst, src)\n\treturn err\n}\n\n// decodeBraced decodes UUID strings that are using the following formats:\n//\n//\t\"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\"\n//\t\"{6ba7b8109dad11d180b400c04fd430c8}\".\nfunc (u *UUID) decodeBraced(t []byte) error {\n\tl := len(t)\n\n\tif t[0] != '{' || t[l-1] != '}' {\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID format in string %q\", t)\n\t}\n\n\treturn u.decodePlain(t[1 : l-1])\n}\n\n// decodeURN decodes UUID strings that are using the following formats:\n//\n//\t\"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n//\t\"urn:uuid:6ba7b8109dad11d180b400c04fd430c8\".\nfunc (u *UUID) decodeURN(t []byte) error {\n\ttotal := len(t)\n\n\turnUUIDPrefix := t[:9]\n\n\tif !bytes.Equal(urnUUIDPrefix, urnPrefix) {\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID format in string %q\", t)\n\t}\n\n\treturn u.decodePlain(t[9:total])\n}\n\n// decodePlain decodes UUID strings that are using the following formats:\n//\n//\t\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\" or in hash-like format\n//\t\"6ba7b8109dad11d180b400c04fd430c8\".\nfunc (u *UUID) decodePlain(t []byte) error {\n\tswitch len(t) {\n\tcase 32:\n\t\treturn u.decodeHashLike(t)\n\tcase 36:\n\t\treturn u.decodeCanonical(t)\n\tdefault:\n\t\treturn fmt.Errorf(\"uuid: incorrect UUID length %d in string %q\", len(t), t)\n\t}\n}\n\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (u UUID) MarshalBinary() ([]byte, error) {\n\treturn u.Bytes(), nil\n}\n\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\n// It will return an error if the slice isn't 16 bytes long.\nfunc (u *UUID) UnmarshalBinary(data []byte) error {\n\tif len(data) != Size {\n\t\treturn fmt.Errorf(\"uuid: UUID must be exactly 16 bytes long, got %d bytes\", len(data))\n\t}\n\tcopy(u[:], data)\n\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/codec_test.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\n// codecTestData holds []byte data for a UUID we commonly use for testing.\nvar codecTestData = []byte{0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}\n\n// codecTestUUID is the UUID value corresponding to codecTestData.\nvar codecTestUUID = UUID{0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}\n\nfunc TestFromBytes(t *testing.T) {\n\tt.Run(\"Valid\", func(t *testing.T) {\n\t\tgot, err := FromBytes(codecTestData)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got != codecTestUUID {\n\t\t\tt.Fatalf(\"FromBytes(%x) = %v, want %v\", codecTestData, got, codecTestUUID)\n\t\t}\n\t})\n\tt.Run(\"Invalid\", func(t *testing.T) {\n\t\tvar short [][]byte\n\t\tfor i := 0; i < len(codecTestData); i++ {\n\t\t\tshort = append(short, codecTestData[:i])\n\t\t}\n\t\tvar long [][]byte\n\t\tfor i := 1; i < 17; i++ {\n\t\t\ttmp := append(codecTestData, make([]byte, i)...)\n\t\t\tlong = append(long, tmp)\n\t\t}\n\t\tinvalid := append(short, long...)\n\t\tfor _, b := range invalid {\n\t\t\tgot, err := FromBytes(b)\n\t\t\tif err == nil {\n\t\t\t\tt.Fatalf(\"FromBytes(%x): want err != nil, got %v\", b, got)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestFromBytesOrNil(t *testing.T) {\n\tt.Run(\"Invalid\", func(t *testing.T) {\n\t\tb := []byte{4, 8, 15, 16, 23, 42}\n\t\tgot := FromBytesOrNil(b)\n\t\tif got != Nil {\n\t\t\tt.Errorf(\"FromBytesOrNil(%x): got %v, want %v\", b, got, Nil)\n\t\t}\n\t})\n\tt.Run(\"Valid\", func(t *testing.T) {\n\t\tgot := FromBytesOrNil(codecTestData)\n\t\tif got != codecTestUUID {\n\t\t\tt.Errorf(\"FromBytesOrNil(%x): got %v, want %v\", codecTestData, got, codecTestUUID)\n\t\t}\n\t})\n\n}\n\ntype fromStringTest struct {\n\tinput   string\n\tvariant string\n}\n\n// Run runs the FromString test in a subtest of t, named by fst.variant.\nfunc (fst fromStringTest) Run(t *testing.T) {\n\tt.Run(fst.variant, func(t *testing.T) {\n\t\tgot, err := FromString(fst.input)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"FromString(%q): %v\", fst.input, err)\n\t\t}\n\t\tif want := codecTestUUID; got != want {\n\t\t\tt.Fatalf(\"FromString(%q) = %v, want %v\", fst.input, got, want)\n\t\t}\n\t})\n}\n\n// fromStringTests contains UUID variants that are expected to be parsed\n// successfully by UnmarshalText / FromString.\n//\n// variants must be unique across elements of this slice. Please see the\n// comment in fuzz.go if you change this slice or add new tests to it.\nvar fromStringTests = []fromStringTest{\n\t{\n\t\tinput:   \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\",\n\t\tvariant: \"Canonical\",\n\t},\n\t{\n\t\tinput:   \"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\",\n\t\tvariant: \"BracedCanonical\",\n\t},\n\t{\n\t\tinput:   \"{6ba7b8109dad11d180b400c04fd430c8}\",\n\t\tvariant: \"BracedHashlike\",\n\t},\n\t{\n\t\tinput:   \"6ba7b8109dad11d180b400c04fd430c8\",\n\t\tvariant: \"Hashlike\",\n\t},\n\t{\n\t\tinput:   \"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8\",\n\t\tvariant: \"URNCanonical\",\n\t},\n\t{\n\t\tinput:   \"urn:uuid:6ba7b8109dad11d180b400c04fd430c8\",\n\t\tvariant: \"URNHashlike\",\n\t},\n}\n\nvar invalidFromStringInputs = []string{\n\t// short\n\t\"6ba7b810-9dad-11d1-80b4-00c04fd430c\",\n\t\"6ba7b8109dad11d180b400c04fd430c\",\n\n\t// invalid hex\n\t\"6ba7b8109dad11d180b400c04fd430q8\",\n\n\t// long\n\t\"6ba7b810-9dad-11d1-80b4-00c04fd430c8=\",\n\t\"6ba7b810-9dad-11d1-80b4-00c04fd430c8}\",\n\t\"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}f\",\n\t\"6ba7b810-9dad-11d1-80b4-00c04fd430c800c04fd430c8\",\n\n\t// malformed in other ways\n\t\"ba7b8109dad11d180b400c04fd430c8}\",\n\t\"6ba7b8109dad11d180b400c04fd430c86ba7b8109dad11d180b400c04fd430c8\",\n\t\"urn:uuid:{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\",\n\t\"uuid:urn:6ba7b810-9dad-11d1-80b4-00c04fd430c8\",\n\t\"uuid:urn:6ba7b8109dad11d180b400c04fd430c8\",\n\t\"6ba7b8109-dad-11d1-80b4-00c04fd430c8\",\n\t\"6ba7b810-9dad1-1d1-80b4-00c04fd430c8\",\n\t\"6ba7b810-9dad-11d18-0b4-00c04fd430c8\",\n\t\"6ba7b810-9dad-11d1-80b40-0c04fd430c8\",\n\t\"6ba7b810+9dad+11d1+80b4+00c04fd430c8\",\n\t\"(6ba7b810-9dad-11d1-80b4-00c04fd430c8}\",\n\t\"{6ba7b810-9dad-11d1-80b4-00c04fd430c8>\",\n\t\"zba7b810-9dad-11d1-80b4-00c04fd430c8\",\n\t\"6ba7b810-9dad11d180b400c04fd430c8\",\n\t\"6ba7b8109dad-11d180b400c04fd430c8\",\n\t\"6ba7b8109dad11d1-80b400c04fd430c8\",\n\t\"6ba7b8109dad11d180b4-00c04fd430c8\",\n}\n\nfunc TestFromString(t *testing.T) {\n\tt.Run(\"Valid\", func(t *testing.T) {\n\t\tfor _, fst := range fromStringTests {\n\t\t\tfst.Run(t)\n\t\t}\n\t})\n\tt.Run(\"Invalid\", func(t *testing.T) {\n\t\tfor _, s := range invalidFromStringInputs {\n\t\t\tgot, err := FromString(s)\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"FromString(%q): want err != nil, got %v\", s, got)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestFromStringOrNil(t *testing.T) {\n\tt.Run(\"Invalid\", func(t *testing.T) {\n\t\ts := \"bad\"\n\t\tgot := FromStringOrNil(s)\n\t\tif got != Nil {\n\t\t\tt.Errorf(\"FromStringOrNil(%q): got %v, want Nil\", s, got)\n\t\t}\n\t})\n\tt.Run(\"Valid\", func(t *testing.T) {\n\t\ts := \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n\t\tgot := FromStringOrNil(s)\n\t\tif got != codecTestUUID {\n\t\t\tt.Errorf(\"FromStringOrNil(%q): got %v, want %v\", s, got, codecTestUUID)\n\t\t}\n\t})\n}\n\nfunc TestMarshalBinary(t *testing.T) {\n\tgot, err := codecTestUUID.MarshalBinary()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !bytes.Equal(got, codecTestData) {\n\t\tt.Fatalf(\"%v.MarshalBinary() = %x, want %x\", codecTestUUID, got, codecTestData)\n\t}\n}\n\nfunc TestMarshalText(t *testing.T) {\n\twant := []byte(\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\")\n\tgot, err := codecTestUUID.MarshalText()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !bytes.Equal(got, want) {\n\t\tt.Errorf(\"%v.MarshalText(): got %s, want %s\", codecTestUUID, got, want)\n\t}\n}\n\nfunc TestDecodePlainWithWrongLength(t *testing.T) {\n\targ := []byte{'4', '2'}\n\n\tu := UUID{}\n\n\tif u.decodePlain(arg) == nil {\n\t\tt.Errorf(\"%v.decodePlain(%q): should return error, but it did not\", u, arg)\n\t}\n}\n\nvar stringBenchmarkSink string\n\nfunc BenchmarkString(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tstringBenchmarkSink = codecTestUUID.String()\n\t}\n}\n\nfunc BenchmarkFromBytes(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = FromBytes(codecTestData)\n\t}\n}\n\nfunc BenchmarkFromString(b *testing.B) {\n\tb.Run(\"canonical\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_, _ = FromString(\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\")\n\t\t}\n\t})\n\tb.Run(\"urn\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_, _ = FromString(\"urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8\")\n\t\t}\n\t})\n\tb.Run(\"braced\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\t_, _ = FromString(\"{6ba7b810-9dad-11d1-80b4-00c04fd430c8}\")\n\t\t}\n\t})\n}\n\nfunc BenchmarkMarshalBinary(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = codecTestUUID.MarshalBinary()\n\t}\n}\n\nfunc BenchmarkMarshalText(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\t_, _ = codecTestUUID.MarshalText()\n\t}\n}\n\nvar seedFuzzCorpus = flag.Bool(\"seed_fuzz_corpus\", false, \"seed fuzz test corpus\")\n\nfunc TestSeedFuzzCorpus(t *testing.T) {\n\t// flag.Parse() is called for us by the test binary.\n\tif !*seedFuzzCorpus {\n\t\tt.Skip(\"seeding fuzz test corpus only on demand\")\n\t}\n\tcorpusDir := filepath.Join(\".\", \"testdata\", \"corpus\")\n\twriteSeedFile := func(name, data string) error {\n\t\tpath := filepath.Join(corpusDir, name)\n\t\treturn os.WriteFile(path, []byte(data), os.ModePerm)\n\t}\n\tfor _, fst := range fromStringTests {\n\t\tname := \"seed_valid_\" + fst.variant\n\t\tif err := writeSeedFile(name, fst.input); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\tfor i, s := range invalidFromStringInputs {\n\t\tname := fmt.Sprintf(\"seed_invalid_%d\", i)\n\t\tif err := writeSeedFile(name, s); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/fuzz.go",
    "content": "// Copyright (c) 2018 Andrei Tudor Călin <mail@acln.ro>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n//go:build gofuzz\n// +build gofuzz\n\npackage uuid\n\n// Fuzz implements a simple fuzz test for FromString / UnmarshalText.\n//\n// To run:\n//\n//\t$ go get github.com/dvyukov/go-fuzz/...\n//\t$ cd $GOPATH/src/github.com/gofrs/uuid\n//\t$ go-fuzz-build github.com/gofrs/uuid\n//\t$ go-fuzz -bin=uuid-fuzz.zip -workdir=./testdata\n//\n// If you make significant changes to FromString / UnmarshalText and add\n// new cases to fromStringTests (in codec_test.go), please run\n//\n//\t$ go test -seed_fuzz_corpus\n//\n// to seed the corpus with the new interesting inputs, then run the fuzzer.\n// nosemgrep\nfunc Fuzz(data []byte) int {\n\t_, err := FromString(string(data))\n\tif err != nil {\n\t\treturn 0\n\t}\n\treturn 1\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/generator.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/binary\"\n\t\"hash\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Difference in 100-nanosecond intervals between\n// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970).\nconst epochStart = 122192928000000000\n\ntype epochFunc func() time.Time\n\nvar (\n\tposixUID = uint32(os.Getuid())\n\tposixGID = uint32(os.Getgid())\n)\n\n// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name.\nfunc NewV3(ns UUID, name string) UUID {\n\treturn g.NewV3(ns, name)\n}\n\n// NewV4 returns a randomly generated UUID.\nfunc NewV4() (UUID, error) {\n\treturn g.NewV4()\n}\n\n// NewV5 returns a UUID based on SHA-1 hash of the namespace UUID and name.\nfunc NewV5(ns UUID, name string) UUID {\n\treturn g.NewV5(ns, name)\n}\n\n// gen is a reference UUID generator based on the specifications laid out in\n// RFC-4122 and DCE 1.1: Authentication and Security Services. This type\n// satisfies the Generator interface as defined in this package.\n//\n// For consumers who are generating V1 UUIDs, but don't want to expose the MAC\n// address of the node generating the UUIDs, the NewGenWithHWAF() function has been\n// provided as a convenience. See the function's documentation for more info.\n//\n// The authors of this package do not feel that the majority of users will need\n// to obfuscate their MAC address, and so we recommend using NewGen() to create\n// a new generator.\ntype gen struct {\n\tclockSequenceOnce sync.Once\n\tstorageMutex      sync.Mutex\n\n\trand io.Reader\n\n\tepochFunc     epochFunc\n\tlastTime      uint64\n\tclockSequence uint16\n}\n\n// newGen returns a new instance of gen with some default values set.\nvar g = &gen{\n\tepochFunc: time.Now,\n\trand:      rand.Reader,\n}\n\n// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name.\nfunc (g *gen) NewV3(ns UUID, name string) UUID {\n\t// nosemgrep\n\tu := newFromHash(md5.New(), ns, name)\n\tu.SetVersion(V3)\n\tu.setVariant(variantRFC4122)\n\n\treturn u\n}\n\n// NewV4 returns a randomly generated UUID.\nfunc (g *gen) NewV4() (UUID, error) {\n\tu := UUID{}\n\tif _, err := io.ReadFull(g.rand, u[:]); err != nil {\n\t\treturn Nil, err\n\t}\n\tu.SetVersion(V4)\n\tu.setVariant(variantRFC4122)\n\n\treturn u, nil\n}\n\n// NewV5 returns a UUID based on SHA-1 hash of the namespace UUID and name.\nfunc (g *gen) NewV5(ns UUID, name string) UUID {\n\t// nosemgrep\n\tu := newFromHash(sha1.New(), ns, name)\n\tu.SetVersion(V5)\n\tu.setVariant(variantRFC4122)\n\n\treturn u\n}\n\n// Returns the epoch and clock sequence.\nfunc (g *gen) getClockSequence() (uint64, uint16, error) {\n\tvar err error\n\tg.clockSequenceOnce.Do(func() {\n\t\tbuf := make([]byte, 2)\n\t\tif _, err = io.ReadFull(g.rand, buf); err != nil {\n\t\t\treturn\n\t\t}\n\t\tg.clockSequence = binary.BigEndian.Uint16(buf)\n\t})\n\tif err != nil {\n\t\treturn 0, 0, err\n\t}\n\n\tg.storageMutex.Lock()\n\tdefer g.storageMutex.Unlock()\n\n\ttimeNow := g.getEpoch()\n\t// Clock didn't change since last UUID generation.\n\t// Should increase clock sequence.\n\tif timeNow <= g.lastTime {\n\t\tg.clockSequence++\n\t}\n\tg.lastTime = timeNow\n\n\treturn timeNow, g.clockSequence, nil\n}\n\n// Returns the difference between UUID epoch (October 15, 1582)\n// and current time in 100-nanosecond intervals.\nfunc (g *gen) getEpoch() uint64 {\n\treturn epochStart + uint64(g.epochFunc().UnixNano()/100)\n}\n\n// Returns the UUID based on the hashing of the namespace UUID and name.\nfunc newFromHash(h hash.Hash, ns UUID, name string) UUID {\n\tu := UUID{}\n\th.Write(ns[:])\n\th.Write([]byte(name))\n\tcopy(u[:], h.Sum(nil))\n\n\treturn u\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/generator_test.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestGenerator(t *testing.T) {\n\tt.Run(\"NewV3\", testNewV3)\n\tt.Run(\"NewV4\", testNewV4)\n\tt.Run(\"NewV5\", testNewV5)\n}\n\nfunc testNewV3(t *testing.T) {\n\tt.Run(\"Basic\", testNewV3Basic)\n\tt.Run(\"EqualNames\", testNewV3EqualNames)\n\tt.Run(\"DifferentNamespaces\", testNewV3DifferentNamespaces)\n}\n\nfunc testNewV3Basic(t *testing.T) {\n\tns := namespaceDNS\n\tname := \"www.example.com\"\n\tu := NewV3(ns, name)\n\tif got, want := u.Version(), V3; got != want {\n\t\tt.Errorf(\"NewV3(%v, %q): got version %d, want %d\", ns, name, got, want)\n\t}\n\tif got, want := u.variant(), variantRFC4122; got != want {\n\t\tt.Errorf(\"NewV3(%v, %q): got variant %d, want %d\", ns, name, got, want)\n\t}\n\twant := \"5df41881-3aed-3515-88a7-2f4a814cf09e\"\n\tif got := u.String(); got != want {\n\t\tt.Errorf(\"NewV3(%v, %q) = %q, want %q\", ns, name, got, want)\n\t}\n}\n\nfunc testNewV3EqualNames(t *testing.T) {\n\tns := namespaceDNS\n\tname := \"example.com\"\n\tu1 := NewV3(ns, name)\n\tu2 := NewV3(ns, name)\n\tif u1 != u2 {\n\t\tt.Errorf(\"NewV3(%v, %q) generated %v and %v across two calls\", ns, name, u1, u2)\n\t}\n}\n\nfunc testNewV3DifferentNamespaces(t *testing.T) {\n\tname := \"example.com\"\n\tns1 := namespaceDNS\n\tns2 := namespaceURL\n\tu1 := NewV3(ns1, name)\n\tu2 := NewV3(ns2, name)\n\tif u1 == u2 {\n\t\tt.Errorf(\"NewV3(%v, %q) == NewV3(%d, %q) (%v)\", ns1, name, ns2, name, u1)\n\t}\n}\n\nfunc testNewV4(t *testing.T) {\n\tt.Run(\"Basic\", testNewV4Basic)\n\tt.Run(\"DifferentAcrossCalls\", testNewV4DifferentAcrossCalls)\n\tt.Run(\"FaultyRand\", testNewV4FaultyRand)\n\tt.Run(\"ShortRandomRead\", testNewV4ShortRandomRead)\n}\n\nfunc testNewV4Basic(t *testing.T) {\n\tu, err := NewV4()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got, want := u.Version(), V4; got != want {\n\t\tt.Errorf(\"got version %d, want %d\", got, want)\n\t}\n\tif got, want := u.variant(), variantRFC4122; got != want {\n\t\tt.Errorf(\"got variant %d, want %d\", got, want)\n\t}\n}\n\nfunc testNewV4DifferentAcrossCalls(t *testing.T) {\n\tu1, err := NewV4()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tu2, err := NewV4()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif u1 == u2 {\n\t\tt.Errorf(\"generated identical UUIDs across calls: %v\", u1)\n\t}\n}\n\nfunc testNewV4FaultyRand(t *testing.T) {\n\tg := &gen{\n\t\tepochFunc: time.Now,\n\t\trand: &faultyReader{\n\t\t\treadToFail: 0, // fail immediately\n\t\t},\n\t}\n\tu, err := g.NewV4()\n\tif err == nil {\n\t\tt.Errorf(\"got %v, nil error\", u)\n\t}\n}\n\nfunc testNewV4ShortRandomRead(t *testing.T) {\n\tg := &gen{\n\t\tepochFunc: time.Now,\n\t\trand:      bytes.NewReader([]byte{42}),\n\t}\n\tu, err := g.NewV4()\n\tif err == nil {\n\t\tt.Errorf(\"got %v, nil error\", u)\n\t}\n}\n\nfunc testNewV5(t *testing.T) {\n\tt.Run(\"Basic\", testNewV5Basic)\n\tt.Run(\"EqualNames\", testNewV5EqualNames)\n\tt.Run(\"DifferentNamespaces\", testNewV5DifferentNamespaces)\n}\n\nfunc testNewV5Basic(t *testing.T) {\n\tns := namespaceDNS\n\tname := \"www.example.com\"\n\tu := NewV5(ns, name)\n\tif got, want := u.Version(), V5; got != want {\n\t\tt.Errorf(\"NewV5(%v, %q): got version %d, want %d\", ns, name, got, want)\n\t}\n\tif got, want := u.variant(), variantRFC4122; got != want {\n\t\tt.Errorf(\"NewV5(%v, %q): got variant %d, want %d\", ns, name, got, want)\n\t}\n\twant := \"2ed6657d-e927-568b-95e1-2665a8aea6a2\"\n\tif got := u.String(); got != want {\n\t\tt.Errorf(\"NewV5(%v, %q) = %q, want %q\", ns, name, got, want)\n\t}\n}\n\nfunc testNewV5EqualNames(t *testing.T) {\n\tns := namespaceDNS\n\tname := \"example.com\"\n\tu1 := NewV5(ns, name)\n\tu2 := NewV5(ns, name)\n\tif u1 != u2 {\n\t\tt.Errorf(\"NewV5(%v, %q) generated %v and %v across two calls\", ns, name, u1, u2)\n\t}\n}\n\nfunc testNewV5DifferentNamespaces(t *testing.T) {\n\tname := \"example.com\"\n\tns1 := namespaceDNS\n\tns2 := namespaceURL\n\tu1 := NewV5(ns1, name)\n\tu2 := NewV5(ns2, name)\n\tif u1 == u2 {\n\t\tt.Errorf(\"NewV5(%v, %q) == NewV5(%v, %q) (%v)\", ns1, name, ns2, name, u1)\n\t}\n}\n\nfunc BenchmarkGenerator(b *testing.B) {\n\tb.Run(\"NewV3\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tNewV3(namespaceDNS, \"www.example.com\")\n\t\t}\n\t})\n\tb.Run(\"NewV4\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tif _, err := NewV4(); err != nil {\n\t\t\t\tb.Fatalf(\"NewV4() failed: %s\", err)\n\t\t\t}\n\t\t}\n\t})\n\tb.Run(\"NewV5\", func(b *testing.B) {\n\t\tfor i := 0; i < b.N; i++ {\n\t\t\tNewV5(namespaceDNS, \"www.example.com\")\n\t\t}\n\t})\n}\n\ntype faultyReader struct {\n\tcallsNum   int\n\treadToFail int // Read call number to fail\n}\n\nfunc (r *faultyReader) Read(dest []byte) (int, error) {\n\tr.callsNum++\n\tif (r.callsNum - 1) == r.readToFail {\n\t\treturn 0, fmt.Errorf(\"io: reader is faulty\")\n\t}\n\treturn rand.Read(dest)\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/sql.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"database/sql/driver\"\n\t\"fmt\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\nvar json = jsoniter.Config{\n\tEscapeHTML:             false,\n\tSortMapKeys:            true,\n\tValidateJsonRawMessage: true,\n}.Froze()\n\n// Value implements the driver.Valuer interface.\nfunc (u UUID) Value() (driver.Value, error) {\n\treturn u.String(), nil\n}\n\n// Scan implements the sql.Scanner interface.\n// A 16-byte slice will be handled by UnmarshalBinary, while\n// a longer byte slice or a string will be handled by UnmarshalText.\nfunc (u *UUID) Scan(src interface{}) error {\n\tswitch src := src.(type) {\n\tcase UUID: // support gorm convert from UUID to NullUUID\n\t\t*u = src\n\t\treturn nil\n\n\tcase []byte:\n\t\tif len(src) == Size {\n\t\t\treturn u.UnmarshalBinary(src)\n\t\t}\n\t\treturn u.UnmarshalText(src)\n\n\tcase string:\n\t\treturn u.UnmarshalText([]byte(src))\n\t}\n\n\treturn fmt.Errorf(\"uuid: cannot convert %T to UUID\", src)\n}\n\n// NullUUID can be used with the standard sql package to represent a\n// UUID value that can be NULL in the database.\ntype NullUUID struct {\n\tUUID  UUID\n\tValid bool\n}\n\n// Value implements the driver.Valuer interface.\nfunc (u NullUUID) Value() (driver.Value, error) {\n\tif !u.Valid {\n\t\treturn nil, nil\n\t}\n\t// Delegate to UUID Value function\n\treturn u.UUID.Value()\n}\n\n// Scan implements the sql.Scanner interface.\nfunc (u *NullUUID) Scan(src interface{}) error {\n\tif src == nil {\n\t\tu.UUID, u.Valid = Nil, false\n\t\treturn nil\n\t}\n\n\t// Delegate to UUID Scan function\n\tu.Valid = true\n\treturn u.UUID.Scan(src)\n}\n\n// MarshalJSON marshals the NullUUID as null or the nested UUID\nfunc (u NullUUID) MarshalJSON() ([]byte, error) {\n\tif !u.Valid {\n\t\treturn json.Marshal(nil)\n\t}\n\n\treturn json.Marshal(u.UUID)\n}\n\n// UnmarshalJSON unmarshals a NullUUID\nfunc (u *NullUUID) UnmarshalJSON(b []byte) error {\n\tif bytes.Equal(b, []byte(\"null\")) {\n\t\tu.UUID, u.Valid = Nil, false\n\t\treturn nil\n\t}\n\n\tif err := json.Unmarshal(b, &u.UUID); err != nil {\n\t\treturn err\n\t}\n\n\tu.Valid = true\n\n\treturn nil\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/sql_test.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestSQL(t *testing.T) {\n\tt.Run(\"Value\", testSQLValue)\n\tt.Run(\"Scan\", func(t *testing.T) {\n\t\tt.Run(\"Binary\", testSQLScanBinary)\n\t\tt.Run(\"String\", testSQLScanString)\n\t\tt.Run(\"Text\", testSQLScanText)\n\t\tt.Run(\"Unsupported\", testSQLScanUnsupported)\n\t\tt.Run(\"Nil\", testSQLScanNil)\n\t})\n}\n\nfunc testSQLValue(t *testing.T) {\n\tv, err := codecTestUUID.Value()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgot, ok := v.(string)\n\tif !ok {\n\t\tt.Fatalf(\"Value() returned %T, want string\", v)\n\t}\n\tif want := codecTestUUID.String(); got != want {\n\t\tt.Errorf(\"Value() == %q, want %q\", got, want)\n\t}\n}\n\nfunc testSQLScanBinary(t *testing.T) {\n\tgot := UUID{}\n\terr := got.Scan(codecTestData)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got != codecTestUUID {\n\t\tt.Errorf(\"Scan(%x): got %v, want %v\", codecTestData, got, codecTestUUID)\n\t}\n}\n\nfunc testSQLScanString(t *testing.T) {\n\ts := \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n\tgot := UUID{}\n\terr := got.Scan(s)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got != codecTestUUID {\n\t\tt.Errorf(\"Scan(%q): got %v, want %v\", s, got, codecTestUUID)\n\t}\n}\n\nfunc testSQLScanText(t *testing.T) {\n\ttext := []byte(\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\")\n\tgot := UUID{}\n\terr := got.Scan(text)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif got != codecTestUUID {\n\t\tt.Errorf(\"Scan(%q): got %v, want %v\", text, got, codecTestUUID)\n\t}\n}\n\nfunc testSQLScanUnsupported(t *testing.T) {\n\tunsupported := []interface{}{\n\t\ttrue,\n\t\t42,\n\t}\n\tfor _, v := range unsupported {\n\t\tgot := UUID{}\n\t\terr := got.Scan(v)\n\t\tif err == nil {\n\t\t\tt.Errorf(\"Scan(%T) succeeded, got %v\", v, got)\n\t\t}\n\t}\n}\n\nfunc testSQLScanNil(t *testing.T) {\n\tgot := UUID{}\n\terr := got.Scan(nil)\n\tif err == nil {\n\t\tt.Errorf(\"Scan(nil) succeeded, got %v\", got)\n\t}\n}\n\nfunc TestNullUUID(t *testing.T) {\n\tt.Run(\"Value\", func(t *testing.T) {\n\t\tt.Run(\"Nil\", testNullUUIDValueNil)\n\t\tt.Run(\"Valid\", testNullUUIDValueValid)\n\t})\n\n\tt.Run(\"Scan\", func(t *testing.T) {\n\t\tt.Run(\"Nil\", testNullUUIDScanNil)\n\t\tt.Run(\"Valid\", testNullUUIDScanValid)\n\t\tt.Run(\"UUID\", testNullUUIDScanUUID)\n\t})\n\n\tt.Run(\"MarshalJSON\", func(t *testing.T) {\n\t\tt.Run(\"Nil\", testNullUUIDMarshalJSONNil)\n\t\tt.Run(\"Null\", testNullUUIDMarshalJSONNull)\n\t\tt.Run(\"Valid\", testNullUUIDMarshalJSONValid)\n\t})\n\n\tt.Run(\"UnmarshalJSON\", func(t *testing.T) {\n\t\tt.Run(\"Nil\", testNullUUIDUnmarshalJSONNil)\n\t\tt.Run(\"Null\", testNullUUIDUnmarshalJSONNull)\n\t\tt.Run(\"Valid\", testNullUUIDUnmarshalJSONValid)\n\t\tt.Run(\"Malformed\", testNullUUIDUnmarshalJSONMalformed)\n\t})\n}\n\nfunc testNullUUIDValueNil(t *testing.T) {\n\tnu := NullUUID{}\n\tgot, err := nu.Value()\n\tif got != nil {\n\t\tt.Errorf(\"null NullUUID.Value returned non-nil driver.Value\")\n\t}\n\tif err != nil {\n\t\tt.Errorf(\"null NullUUID.Value returned non-nil error\")\n\t}\n}\n\nfunc testNullUUIDValueValid(t *testing.T) {\n\tnu := NullUUID{\n\t\tValid: true,\n\t\tUUID:  codecTestUUID,\n\t}\n\tgot, err := nu.Value()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ts, ok := got.(string)\n\tif !ok {\n\t\tt.Errorf(\"Value() returned %T, want string\", got)\n\t}\n\twant := \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n\tif s != want {\n\t\tt.Errorf(\"%v.Value() == %s, want %s\", nu, s, want)\n\t}\n}\n\nfunc testNullUUIDScanNil(t *testing.T) {\n\tu := NullUUID{}\n\terr := u.Scan(nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif u.Valid {\n\t\tt.Error(\"NullUUID is valid after Scan(nil)\")\n\t}\n\tif u.UUID != Nil {\n\t\tt.Errorf(\"NullUUID.UUID is %v after Scan(nil) want Nil\", u.UUID)\n\t}\n}\n\nfunc testNullUUIDScanValid(t *testing.T) {\n\ts := \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n\tu := NullUUID{}\n\terr := u.Scan(s)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !u.Valid {\n\t\tt.Errorf(\"Valid == false after Scan(%q)\", s)\n\t}\n\tif u.UUID != codecTestUUID {\n\t\tt.Errorf(\"UUID == %v after Scan(%q), want %v\", u.UUID, s, codecTestUUID)\n\t}\n}\n\nfunc testNullUUIDScanUUID(t *testing.T) {\n\tu := NullUUID{}\n\terr := u.Scan(codecTestUUID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !u.Valid {\n\t\tt.Errorf(\"Valid == false after scan(%v)\", codecTestUUID)\n\t}\n\tif u.UUID != codecTestUUID {\n\t\tt.Errorf(\"UUID == %v after Scan(%v), want %v\", u.UUID, codecTestUUID, codecTestUUID)\n\t}\n}\n\nfunc testNullUUIDMarshalJSONNil(t *testing.T) {\n\tu := NullUUID{Valid: true}\n\n\tdata, err := u.MarshalJSON()\n\tif err != nil {\n\t\tt.Fatalf(\"(%#v).MarshalJSON err want: <nil>, got: %v\", u, err)\n\t}\n\n\tdataStr := string(data)\n\n\tif dataStr != fmt.Sprintf(\"%q\", Nil) {\n\t\tt.Fatalf(\"(%#v).MarshalJSON value want: %s, got: %s\", u, Nil, dataStr)\n\t}\n}\n\nfunc testNullUUIDMarshalJSONValid(t *testing.T) {\n\tu := NullUUID{\n\t\tValid: true,\n\t\tUUID:  codecTestUUID,\n\t}\n\n\tdata, err := u.MarshalJSON()\n\tif err != nil {\n\t\tt.Fatalf(\"(%#v).MarshalJSON err want: <nil>, got: %v\", u, err)\n\t}\n\n\tdataStr := string(data)\n\n\tif dataStr != fmt.Sprintf(\"%q\", codecTestUUID) {\n\t\tt.Fatalf(\"(%#v).MarshalJSON value want: %s, got: %s\", u, codecTestUUID, dataStr)\n\t}\n}\n\nfunc testNullUUIDMarshalJSONNull(t *testing.T) {\n\tu := NullUUID{}\n\n\tdata, err := u.MarshalJSON()\n\tif err != nil {\n\t\tt.Fatalf(\"(%#v).MarshalJSON err want: <nil>, got: %v\", u, err)\n\t}\n\n\tdataStr := string(data)\n\n\tif dataStr != \"null\" {\n\t\tt.Fatalf(\"(%#v).MarshalJSON value want: %s, got: %s\", u, \"null\", dataStr)\n\t}\n}\n\nfunc testNullUUIDUnmarshalJSONNil(t *testing.T) {\n\tvar u NullUUID\n\n\tdata := []byte(`\"00000000-0000-0000-0000-000000000000\"`)\n\n\tif err := json.Unmarshal(data, &u); err != nil {\n\t\tt.Fatalf(\"json.Unmarshal err = %v, want <nil>\", err)\n\t}\n\n\tif !u.Valid {\n\t\tt.Fatalf(\"u.Valid = false, want true\")\n\t}\n\n\tif u.UUID != Nil {\n\t\tt.Fatalf(\"u.UUID = %v, want %v\", u.UUID, Nil)\n\t}\n}\n\nfunc testNullUUIDUnmarshalJSONNull(t *testing.T) {\n\tvar u NullUUID\n\n\tdata := []byte(`null`)\n\n\tif err := json.Unmarshal(data, &u); err != nil {\n\t\tt.Fatalf(\"json.Unmarshal err = %v, want <nil>\", err)\n\t}\n\n\tif u.Valid {\n\t\tt.Fatalf(\"u.Valid = true, want false\")\n\t}\n\n\tif u.UUID != Nil {\n\t\tt.Fatalf(\"u.UUID = %v, want %v\", u.UUID, Nil)\n\t}\n}\nfunc testNullUUIDUnmarshalJSONValid(t *testing.T) {\n\tvar u NullUUID\n\n\tdata := []byte(`\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"`)\n\n\tif err := json.Unmarshal(data, &u); err != nil {\n\t\tt.Fatalf(\"json.Unmarshal err = %v, want <nil>\", err)\n\t}\n\n\tif !u.Valid {\n\t\tt.Fatalf(\"u.Valid = false, want true\")\n\t}\n\n\tif u.UUID != codecTestUUID {\n\t\tt.Fatalf(\"u.UUID = %v, want %v\", u.UUID, Nil)\n\t}\n}\n\nfunc testNullUUIDUnmarshalJSONMalformed(t *testing.T) {\n\tvar u NullUUID\n\n\tdata := []byte(`257`)\n\n\tif err := json.Unmarshal(data, &u); err == nil {\n\t\tt.Fatal(\"json.Unmarshal err = <nil>, want error\")\n\t}\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/uuid.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n// Package uuid provides implementations of the Universally Unique Identifier (UUID), as specified in RFC-4122 and DCE 1.1.\n//\n// RFC-4122[1] provides the specification for versions 1, 3, 4, and 5.\n//\n// DCE 1.1[2] provides the specification for version 2.\n//\n// [1] https://tools.ietf.org/html/rfc4122\n// [2] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01\npackage uuid\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\n// Size of a UUID in bytes.\nconst Size = 16\n\n// UUID is an array type to represent the value of a UUID, as defined in RFC-4122.\ntype UUID [Size]byte\n\n// UUID versions.\nconst (\n\t_  byte = iota\n\tV1      // Version 1 (date-time and MAC address)\n\tV2      // Version 2 (date-time and MAC address, DCE security version)\n\tV3      // Version 3 (namespace name-based)\n\tV4      // Version 4 (random)\n\tV5      // Version 5 (namespace name-based)\n)\n\n// UUID layout variants.\nconst (\n\tvariantNCS byte = iota\n\tvariantRFC4122\n\tvariantMicrosoft\n\tvariantFuture\n)\n\n// String parse helpers.\nvar (\n\turnPrefix  = []byte(\"urn:uuid:\")\n\tbyteGroups = []int{8, 4, 4, 4, 12}\n)\n\n// Nil is the nil UUID, as specified in RFC-4122, that has all 128 bits set to\n// zero.\nvar Nil = UUID{}\n\n// Version returns the algorithm version used to generate the UUID.\nfunc (u UUID) Version() byte {\n\treturn u[6] >> 4\n}\n\n// variant returns the UUID layout variant.\nfunc (u UUID) variant() byte {\n\tswitch {\n\tcase (u[8] >> 7) == 0x00:\n\t\treturn variantNCS\n\tcase (u[8] >> 6) == 0x02:\n\t\treturn variantRFC4122\n\tcase (u[8] >> 5) == 0x06:\n\t\treturn variantMicrosoft\n\tcase (u[8] >> 5) == 0x07:\n\t\tfallthrough\n\tdefault:\n\t\treturn variantFuture\n\t}\n}\n\n// Bytes returns a byte slice representation of the UUID.\nfunc (u UUID) Bytes() []byte {\n\treturn u[:]\n}\n\n// String returns a canonical RFC-4122 string representation of the UUID:\n// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.\nfunc (u UUID) String() string {\n\tbuf := make([]byte, 36)\n\n\thex.Encode(buf[0:8], u[0:4])\n\tbuf[8] = '-'\n\thex.Encode(buf[9:13], u[4:6])\n\tbuf[13] = '-'\n\thex.Encode(buf[14:18], u[6:8])\n\tbuf[18] = '-'\n\thex.Encode(buf[19:23], u[8:10])\n\tbuf[23] = '-'\n\thex.Encode(buf[24:], u[10:])\n\n\treturn string(buf)\n}\n\n// Format implements fmt.Formatter for UUID values.\n//\n// The behavior is as follows:\n// The 'x' and 'X' verbs output only the hex digits of the UUID, using a-f for 'x' and A-F for 'X'.\n// The 'v', '+v', 's' and 'q' verbs return the canonical RFC-4122 string representation.\n// The 'S' verb returns the RFC-4122 format, but with capital hex digits.\n// The '#v' verb returns the \"Go syntax\" representation, which is a 16 byte array initializer.\n// All other verbs not handled directly by the fmt package (like '%p') are unsupported and will return\n// \"%!verb(uuid.UUID=value)\" as recommended by the fmt package.\nfunc (u UUID) Format(f fmt.State, c rune) {\n\tswitch c {\n\tcase 'x', 'X':\n\t\ts := hex.EncodeToString(u.Bytes())\n\t\tif c == 'X' {\n\t\t\ts = strings.Map(toCapitalHexDigits, s)\n\t\t}\n\t\t_, _ = io.WriteString(f, s)\n\tcase 'v':\n\t\tvar s string\n\t\tif f.Flag('#') {\n\t\t\ts = fmt.Sprintf(\"%#v\", [Size]byte(u))\n\t\t} else {\n\t\t\ts = u.String()\n\t\t}\n\t\t_, _ = io.WriteString(f, s)\n\tcase 's', 'S':\n\t\ts := u.String()\n\t\tif c == 'S' {\n\t\t\ts = strings.Map(toCapitalHexDigits, s)\n\t\t}\n\t\t_, _ = io.WriteString(f, s)\n\tcase 'q':\n\t\t_, _ = io.WriteString(f, `\"`+u.String()+`\"`)\n\tdefault:\n\t\t// invalid/unsupported format verb\n\t\t_, _ = fmt.Fprintf(f, \"%%!%c(uuid.UUID=%s)\", c, u.String())\n\t}\n}\n\nfunc toCapitalHexDigits(ch rune) rune {\n\t// convert a-f hex digits to A-F\n\tswitch ch {\n\tcase 'a':\n\t\treturn 'A'\n\tcase 'b':\n\t\treturn 'B'\n\tcase 'c':\n\t\treturn 'C'\n\tcase 'd':\n\t\treturn 'D'\n\tcase 'e':\n\t\treturn 'E'\n\tcase 'f':\n\t\treturn 'F'\n\tdefault:\n\t\treturn ch\n\t}\n}\n\n// setVersion sets the version bits.\nfunc (u *UUID) SetVersion(v byte) {\n\tu[6] = (u[6] & 0x0f) | (v << 4)\n}\n\n// setVariant sets the variant bits.\nfunc (u *UUID) setVariant(v byte) {\n\tswitch v {\n\tcase variantNCS:\n\t\tu[8] = (u[8]&(0xff>>1) | (0x00 << 7))\n\tcase variantRFC4122:\n\t\tu[8] = (u[8]&(0xff>>2) | (0x02 << 6))\n\tcase variantMicrosoft:\n\t\tu[8] = (u[8]&(0xff>>3) | (0x06 << 5))\n\tcase variantFuture:\n\t\tfallthrough\n\tdefault:\n\t\tu[8] = (u[8]&(0xff>>3) | (0x07 << 5))\n\t}\n}\n\n// Must is a helper that wraps a call to a function returning (UUID, error)\n// and panics if the error is non-nil. It is intended for use in variable\n// initializations such as\n//\n//\tvar packageUUID = uuid.Must(uuid.FromString(\"123e4567-e89b-12d3-a456-426655440000\"))\nfunc Must(u UUID, err error) UUID {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn u\n}\n"
  },
  {
    "path": "runtimes/go/types/uuid/uuid_test.go",
    "content": "// Copyright (C) 2013-2018 by Maxim Bublis <b@codemonkey.ru>\n//\n// Permission is hereby granted, free of charge, to any person obtaining\n// a copy of this software and associated documentation files (the\n// \"Software\"), to deal in the Software without restriction, including\n// without limitation the rights to use, copy, modify, merge, publish,\n// distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to\n// the following conditions:\n//\n// The above copyright notice and this permission notice shall be\n// included in all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\npackage uuid\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n)\n\n// Predefined namespace UUIDs.\nvar (\n\tnamespaceDNS  = Must(FromString(\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"))\n\tnamespaceURL  = Must(FromString(\"6ba7b811-9dad-11d1-80b4-00c04fd430c8\"))\n\tnamespaceOID  = Must(FromString(\"6ba7b812-9dad-11d1-80b4-00c04fd430c8\"))\n\tnamespaceX500 = Must(FromString(\"6ba7b814-9dad-11d1-80b4-00c04fd430c8\"))\n)\n\nfunc TestUUID(t *testing.T) {\n\tt.Run(\"Bytes\", testUUIDBytes)\n\tt.Run(\"String\", testUUIDString)\n\tt.Run(\"Version\", testUUIDVersion)\n\tt.Run(\"Variant\", testUUIDVariant)\n\tt.Run(\"SetVersion\", testUUIDSetVersion)\n\tt.Run(\"SetVariant\", testUUIDSetVariant)\n\tt.Run(\"Format\", testUUIDFormat)\n}\n\nfunc testUUIDBytes(t *testing.T) {\n\tgot := codecTestUUID.Bytes()\n\twant := codecTestData\n\tif !bytes.Equal(got, want) {\n\t\tt.Errorf(\"%v.Bytes() = %x, want %x\", codecTestUUID, got, want)\n\t}\n}\n\nfunc testUUIDString(t *testing.T) {\n\tgot := namespaceDNS.String()\n\twant := \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"\n\tif got != want {\n\t\tt.Errorf(\"%v.String() = %q, want %q\", namespaceDNS, got, want)\n\t}\n}\n\nfunc testUUIDVersion(t *testing.T) {\n\tu := UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}\n\tif got, want := u.Version(), V1; got != want {\n\t\tt.Errorf(\"%v.Version() == %d, want %d\", u, got, want)\n\t}\n}\n\nfunc testUUIDVariant(t *testing.T) {\n\ttests := []struct {\n\t\tu    UUID\n\t\twant byte\n\t}{\n\t\t{\n\t\t\tu:    UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\t\t\twant: variantNCS,\n\t\t},\n\t\t{\n\t\t\tu:    UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\t\t\twant: variantRFC4122,\n\t\t},\n\t\t{\n\t\t\tu:    UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\t\t\twant: variantMicrosoft,\n\t\t},\n\t\t{\n\t\t\tu:    UUID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},\n\t\t\twant: variantFuture,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := tt.u.variant(); got != tt.want {\n\t\t\tt.Errorf(\"%v.variant() == %d, want %d\", tt.u, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc testUUIDSetVersion(t *testing.T) {\n\tu := UUID{}\n\twant := V4\n\tu.SetVersion(want)\n\tif got := u.Version(); got != want {\n\t\tt.Errorf(\"%v.Version() == %d after setVersion(%d)\", u, got, want)\n\t}\n}\n\nfunc testUUIDSetVariant(t *testing.T) {\n\tvariants := []byte{\n\t\tvariantNCS,\n\t\tvariantRFC4122,\n\t\tvariantMicrosoft,\n\t\tvariantFuture,\n\t}\n\tfor _, want := range variants {\n\t\tu := UUID{}\n\t\tu.setVariant(want)\n\t\tif got := u.variant(); got != want {\n\t\t\tt.Errorf(\"%v.variant() == %d after SetVariant(%d)\", u, got, want)\n\t\t}\n\t}\n}\n\nfunc testUUIDFormat(t *testing.T) {\n\tval := Must(FromString(\"12345678-90ab-cdef-1234-567890abcdef\"))\n\ttests := []struct {\n\t\tu    UUID\n\t\tf    string\n\t\twant string\n\t}{\n\t\t{u: val, f: \"%s\", want: \"12345678-90ab-cdef-1234-567890abcdef\"},\n\t\t{u: val, f: \"%S\", want: \"12345678-90AB-CDEF-1234-567890ABCDEF\"},\n\t\t{u: val, f: \"%q\", want: `\"12345678-90ab-cdef-1234-567890abcdef\"`},\n\t\t{u: val, f: \"%x\", want: \"1234567890abcdef1234567890abcdef\"},\n\t\t{u: val, f: \"%X\", want: \"1234567890ABCDEF1234567890ABCDEF\"},\n\t\t{u: val, f: \"%v\", want: \"12345678-90ab-cdef-1234-567890abcdef\"},\n\t\t{u: val, f: \"%+v\", want: \"12345678-90ab-cdef-1234-567890abcdef\"},\n\t\t{u: val, f: \"%#v\", want: \"[16]uint8{0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef}\"},\n\t\t{u: val, f: \"%T\", want: \"uuid.UUID\"},\n\t\t{u: val, f: \"%t\", want: \"%!t(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%b\", want: \"%!b(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%c\", want: \"%!c(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%d\", want: \"%!d(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%e\", want: \"%!e(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%E\", want: \"%!E(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%f\", want: \"%!f(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%F\", want: \"%!F(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%g\", want: \"%!g(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%G\", want: \"%!G(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%o\", want: \"%!o(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t\t{u: val, f: \"%U\", want: \"%!U(uuid.UUID=12345678-90ab-cdef-1234-567890abcdef)\"},\n\t}\n\tfor _, tt := range tests {\n\t\tgot := fmt.Sprintf(tt.f, tt.u)\n\t\tif tt.want != got {\n\t\t\tt.Errorf(`Format(\"%s\") got %s, want %s`, tt.f, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestMust(t *testing.T) {\n\tsentinel := fmt.Errorf(\"uuid: sentinel error\")\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil {\n\t\t\tt.Fatalf(\"did not panic, want %v\", sentinel)\n\t\t}\n\t\terr, ok := r.(error)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"panicked with %T, want error (%v)\", r, sentinel)\n\t\t}\n\t\tif err != sentinel {\n\t\t\tt.Fatalf(\"panicked with %v, want %v\", err, sentinel)\n\t\t}\n\t}()\n\tfn := func() (UUID, error) {\n\t\treturn Nil, sentinel\n\t}\n\tMust(fn())\n}\n"
  },
  {
    "path": "runtimes/js/.gitignore",
    "content": "encore-runtime.node*\nnode_modules\ndist\n.turbo\n"
  },
  {
    "path": "runtimes/js/Cargo.toml",
    "content": "[package]\nname = \"encore-js-runtime\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nnapi = { version = \"2.12.2\", default-features = false, features = [\n    \"napi8\",\n    \"tokio_rt\",\n    \"serde-json\",\n    \"anyhow\",\n    \"async\",\n] }\nnapi-derive = \"2.12.2\"\nencore-runtime-core = { path = \"../core\" }\naxum = { version = \"0.7.5\" }\ntokio = \"1.35.1\"\nlog = \"0.4.20\"\nserde_json = { version = \"1.0.111\", features = [] }\nanyhow = \"1.0.76\"\nbytes = \"1.5.0\"\nprost = \"0.12.3\"\nprost-types = \"0.12.3\"\nfutures = \"0.3.30\"\nmappable-rc = \"0.1.1\"\ntokio-util = { version = \"0.7.10\", features = [\"io\"] }\nchrono = \"0.4.38\"\nnum_cpus = \"1.16.0\"\nmalachite = \"0.6.1\"\nmetrics = \"0.24.2\"\nconvert_case = \"0.6.0\"\n\n[build-dependencies]\nnapi-build = \"2.0.1\"\n"
  },
  {
    "path": "runtimes/js/build.rs",
    "content": "extern crate napi_build;\n\nfn main() -> std::io::Result<()> {\n    napi_build::setup();\n    Ok(())\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/LICENSE",
    "content": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n     means each individual or legal entity that creates, contributes to the\n     creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n     means the combination of the Contributions of others (if any) used by a\n     Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n     means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n     means Source Code Form to which the initial Contributor has attached the\n     notice in Exhibit A, the Executable Form of such Source Code Form, and\n     Modifications of such Source Code Form, in each case including portions\n     thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\nmeans\n\n     a. that the initial Contributor has attached the notice described in\n        Exhibit B to the Covered Software; or\n\n     b. that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the terms of\n        a Secondary License.\n\n1.6. \"Executable Form\"\n\n     means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n     means a work that combines Covered Software with other material, in a\n     separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n     means this document.\n\n1.9. \"Licensable\"\n\n     means having the right to grant, to the maximum extent possible, whether\n     at the time of the initial grant or subsequently, any and all of the\n     rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n     means any of the following:\n\n     a. any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered Software; or\n\n     b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n      means any patent claim(s), including without limitation, method,\n      process, and apparatus claims, in any patent Licensable by such\n      Contributor that would be infringed, but for the grant of the License,\n      by the making, using, selling, offering for sale, having made, import,\n      or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n      means either the GNU General Public License, Version 2.0, the GNU Lesser\n      General Public License, Version 2.1, the GNU Affero General Public\n      License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n      means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n      means an individual or a legal entity exercising rights under this\n      License. For legal entities, \"You\" includes any entity that controls, is\n      controlled by, or is under common control with You. For purposes of this\n      definition, \"control\" means (a) the power, direct or indirect, to cause\n      the direction or management of such entity, whether by contract or\n      otherwise, or (b) ownership of more than fifty percent (50%) of the\n      outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n     Each Contributor hereby grants You a world-wide, royalty-free,\n     non-exclusive license:\n\n     a. under intellectual property rights (other than patent or trademark)\n        Licensable by such Contributor to use, reproduce, make available,\n        modify, display, perform, distribute, and otherwise exploit its\n        Contributions, either on an unmodified basis, with Modifications, or\n        as part of a Larger Work; and\n\n     b. under Patent Claims of such Contributor to make, use, sell, offer for\n        sale, have made, import, and otherwise transfer either its\n        Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n     The licenses granted in Section 2.1 with respect to any Contribution\n     become effective for each Contribution on the date the Contributor first\n     distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n     The licenses granted in this Section 2 are the only rights granted under\n     this License. No additional rights or licenses will be implied from the\n     distribution or licensing of Covered Software under this License.\n     Notwithstanding Section 2.1(b) above, no patent license is granted by a\n     Contributor:\n\n     a. for any code that a Contributor has removed from Covered Software; or\n\n     b. for infringements caused by: (i) Your and any other third party's\n        modifications of Covered Software, or (ii) the combination of its\n        Contributions with other software (except as part of its Contributor\n        Version); or\n\n     c. under Patent Claims infringed by Covered Software in the absence of\n        its Contributions.\n\n     This License does not grant any rights in the trademarks, service marks,\n     or logos of any Contributor (except as may be necessary to comply with\n     the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n     No Contributor makes additional grants as a result of Your choice to\n     distribute the Covered Software under a subsequent version of this\n     License (see Section 10.2) or under the terms of a Secondary License (if\n     permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n     Each Contributor represents that the Contributor believes its\n     Contributions are its original creation(s) or it has sufficient rights to\n     grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n     This License is not intended to limit any rights You have under\n     applicable copyright doctrines of fair use, fair dealing, or other\n     equivalents.\n\n2.7. Conditions\n\n     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n     Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n     All distribution of Covered Software in Source Code Form, including any\n     Modifications that You create or to which You contribute, must be under\n     the terms of this License. You must inform recipients that the Source\n     Code Form of the Covered Software is governed by the terms of this\n     License, and how they can obtain a copy of this License. You may not\n     attempt to alter or restrict the recipients' rights in the Source Code\n     Form.\n\n3.2. Distribution of Executable Form\n\n     If You distribute Covered Software in Executable Form then:\n\n     a. such Covered Software must also be made available in Source Code Form,\n        as described in Section 3.1, and You must inform recipients of the\n        Executable Form how they can obtain a copy of such Source Code Form by\n        reasonable means in a timely manner, at a charge no more than the cost\n        of distribution to the recipient; and\n\n     b. You may distribute such Executable Form under the terms of this\n        License, or sublicense it under different terms, provided that the\n        license for the Executable Form does not attempt to limit or alter the\n        recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n     You may create and distribute a Larger Work under terms of Your choice,\n     provided that You also comply with the requirements of this License for\n     the Covered Software. If the Larger Work is a combination of Covered\n     Software with a work governed by one or more Secondary Licenses, and the\n     Covered Software is not Incompatible With Secondary Licenses, this\n     License permits You to additionally distribute such Covered Software\n     under the terms of such Secondary License(s), so that the recipient of\n     the Larger Work may, at their option, further distribute the Covered\n     Software under the terms of either this License or such Secondary\n     License(s).\n\n3.4. Notices\n\n     You may not remove or alter the substance of any license notices\n     (including copyright notices, patent notices, disclaimers of warranty, or\n     limitations of liability) contained within the Source Code Form of the\n     Covered Software, except that You may alter any license notices to the\n     extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n     You may choose to offer, and to charge a fee for, warranty, support,\n     indemnity or liability obligations to one or more recipients of Covered\n     Software. However, You may do so only on Your own behalf, and not on\n     behalf of any Contributor. You must make it absolutely clear that any\n     such warranty, support, indemnity, or liability obligation is offered by\n     You alone, and You hereby agree to indemnify every Contributor for any\n     liability incurred by such Contributor as a result of warranty, support,\n     indemnity or liability terms You offer. You may include additional\n     disclaimers of warranty and limitations of liability specific to any\n     jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n   If it is impossible for You to comply with any of the terms of this License\n   with respect to some or all of the Covered Software due to statute,\n   judicial order, or regulation then You must: (a) comply with the terms of\n   this License to the maximum extent possible; and (b) describe the\n   limitations and the code they affect. Such description must be placed in a\n   text file included with all distributions of the Covered Software under\n   this License. Except to the extent prohibited by statute or regulation,\n   such description must be sufficiently detailed for a recipient of ordinary\n   skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\nfail to comply with any of its terms. However, if You become compliant,\nthen the rights granted under this License from a particular Contributor\nare reinstated (a) provisionally, unless and until such Contributor\nexplicitly and finally terminates Your grants, and (b) on an ongoing\nbasis, if such Contributor fails to notify You of the non-compliance by\nsome reasonable means prior to 60 days after You have come back into\ncompliance. Moreover, Your grants from a particular Contributor are\nreinstated on an ongoing basis if such Contributor notifies You of the\nnon-compliance by some reasonable means, this is the first time You have\nreceived notice of non-compliance with this License from such\nContributor, and You become compliant prior to 30 days after Your receipt\nof the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\nlicense agreements (excluding distributors and resellers) which have been\nvalidly granted by You or Your distributors under this License prior to\ntermination shall survive termination.\n\n6. Disclaimer of Warranty\n\n   Covered Software is provided under this License on an \"as is\" basis,\n   without warranty of any kind, either expressed, implied, or statutory,\n   including, without limitation, warranties that the Covered Software is free\n   of defects, merchantable, fit for a particular purpose or non-infringing.\n   The entire risk as to the quality and performance of the Covered Software\n   is with You. Should any Covered Software prove defective in any respect,\n   You (not any Contributor) assume the cost of any necessary servicing,\n   repair, or correction. This disclaimer of warranty constitutes an essential\n   part of this License. No use of  any Covered Software is authorized under\n   this License except under this disclaimer.\n\n7. Limitation of Liability\n\n   Under no circumstances and under no legal theory, whether tort (including\n   negligence), contract, or otherwise, shall any Contributor, or anyone who\n   distributes Covered Software as permitted above, be liable to You for any\n   direct, indirect, special, incidental, or consequential damages of any\n   character including, without limitation, damages for lost profits, loss of\n   goodwill, work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses, even if such party shall have been\n   informed of the possibility of such damages. This limitation of liability\n   shall not apply to liability for death or personal injury resulting from\n   such party's negligence to the extent applicable law prohibits such\n   limitation. Some jurisdictions do not allow the exclusion or limitation of\n   incidental or consequential damages, so this exclusion and limitation may\n   not apply to You.\n\n8. Litigation\n\n   Any litigation relating to this License may be brought only in the courts\n   of a jurisdiction where the defendant maintains its principal place of\n   business and such litigation shall be governed by laws of that\n   jurisdiction, without reference to its conflict-of-law provisions. Nothing\n   in this Section shall prevent a party's ability to bring cross-claims or\n   counter-claims.\n\n9. Miscellaneous\n\n   This License represents the complete agreement concerning the subject\n   matter hereof. If any provision of this License is held to be\n   unenforceable, such provision shall be reformed only to the extent\n   necessary to make it enforceable. Any law or regulation which provides that\n   the language of a contract shall be construed against the drafter shall not\n   be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n      Mozilla Foundation is the license steward. Except as provided in Section\n      10.3, no one other than the license steward has the right to modify or\n      publish new versions of this License. Each version will be given a\n      distinguishing version number.\n\n10.2. Effect of New Versions\n\n      You may distribute the Covered Software under the terms of the version\n      of the License under which You originally received the Covered Software,\n      or under the terms of any subsequent version published by the license\n      steward.\n\n10.3. Modified Versions\n\n      If you create software not governed by this License, and you want to\n      create a new license for such software, you may create and use a\n      modified version of this License if you rename the license and remove\n      any references to the name of the license steward (except to note that\n      such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses If You choose to distribute Source Code Form that is\nIncompatible With Secondary Licenses under the terms of this version of\nthe License, the notice described in Exhibit B of this License must be\nattached.\n\nExhibit A - Source Code Form License Notice\n\n      This Source Code Form is subject to the\n      terms of the Mozilla Public License, v.\n      2.0. If a copy of the MPL was not\n      distributed with this file, You can\n      obtain one at\n      http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n      This Source Code Form is \"Incompatible\n      With Secondary Licenses\", as defined by\n      the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "runtimes/js/encore.dev/README.md",
    "content": "# Encore JavaScript/TypeScript SDK\n\nThis package contains the Encore JavaScript/TypeScript SDK, it is intended for you to use in your applications to develop\nagainst the Encore API.\n"
  },
  {
    "path": "runtimes/js/encore.dev/api/error.ts",
    "content": "export class APIError extends Error {\n  /**\n   * The error code.\n   */\n  public readonly code: ErrCode;\n  public readonly details?: ErrDetails;\n\n  // Constructs an APIError with the Canceled error code.\n  static canceled(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Canceled, msg, cause);\n  }\n\n  // Constructs an APIError with the Unknown error code.\n  static unknown(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Unknown, msg, cause);\n  }\n\n  // Constructs an APIError with the InvalidArgument error code.\n  static invalidArgument(msg: string, cause?: Error) {\n    return new APIError(ErrCode.InvalidArgument, msg, cause);\n  }\n\n  // Constructs an APIError with the DeadlineExceeded error code.\n  static deadlineExceeded(msg: string, cause?: Error) {\n    return new APIError(ErrCode.DeadlineExceeded, msg, cause);\n  }\n\n  // Constructs an APIError with the NotFound error code.\n  static notFound(msg: string, cause?: Error) {\n    return new APIError(ErrCode.NotFound, msg, cause);\n  }\n\n  // Constructs an APIError with the AlreadyExists error code.\n  static alreadyExists(msg: string, cause?: Error) {\n    return new APIError(ErrCode.AlreadyExists, msg, cause);\n  }\n\n  // Constructs an APIError with the PermissionDenied error code.\n  static permissionDenied(msg: string, cause?: Error) {\n    return new APIError(ErrCode.PermissionDenied, msg, cause);\n  }\n\n  // Constructs an APIError with the ResourceExhausted error code.\n  static resourceExhausted(msg: string, cause?: Error) {\n    return new APIError(ErrCode.ResourceExhausted, msg, cause);\n  }\n\n  // Constructs an APIError with the FailedPrecondition error code.\n  static failedPrecondition(msg: string, cause?: Error) {\n    return new APIError(ErrCode.FailedPrecondition, msg, cause);\n  }\n\n  // Constructs an APIError with the Aborted error code.\n  static aborted(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Aborted, msg, cause);\n  }\n\n  // Constructs an APIError with the OutOfRange error code.\n  static outOfRange(msg: string, cause?: Error) {\n    return new APIError(ErrCode.OutOfRange, msg, cause);\n  }\n\n  // Constructs an APIError with the Unimplemented error code.\n  static unimplemented(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Unimplemented, msg, cause);\n  }\n\n  // Constructs an APIError with the Internal error code.\n  static internal(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Internal, msg, cause);\n  }\n\n  // Constructs an APIError with the Unavailable error code.\n  static unavailable(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Unavailable, msg, cause);\n  }\n\n  // Constructs an APIError with the DataLoss error code.\n  static dataLoss(msg: string, cause?: Error) {\n    return new APIError(ErrCode.DataLoss, msg, cause);\n  }\n\n  // Constructs an APIError with the Unauthenticated error code.\n  static unauthenticated(msg: string, cause?: Error) {\n    return new APIError(ErrCode.Unauthenticated, msg, cause);\n  }\n\n  // Constructs a new APIError from the previous one with the provided details\n  withDetails(details: ErrDetails): APIError {\n    return new APIError(this.code, this.message, this.cause as Error, details);\n  }\n\n  // Constructs an APIError with the given error code, message, and (optionally) cause.\n  constructor(code: ErrCode, msg: string, cause?: Error, details?: ErrDetails) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(msg, { cause });\n    this.code = code;\n    this.details = details;\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"APIError\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, APIError.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\nexport type ErrDetails = Record<string, any>;\n\nexport enum ErrCode {\n  /**\n   * OK indicates the operation was successful.\n   */\n  OK = \"ok\",\n\n  /**\n   * Canceled indicates the operation was canceled (typically by the caller).\n   *\n   * Encore will generate this error code when cancellation is requested.\n   */\n  Canceled = \"canceled\",\n\n  /**\n   * Unknown error. An example of where this error may be returned is\n   * if a Status value received from another address space belongs to\n   * an error-space that is not known in this address space. Also\n   * errors raised by APIs that do not return enough error information\n   * may be converted to this error.\n   *\n   * Encore will generate this error code in the above two mentioned cases.\n   */\n  Unknown = \"unknown\",\n\n  /**\n   * InvalidArgument indicates client specified an invalid argument.\n   * Note that this differs from FailedPrecondition. It indicates arguments\n   * that are problematic regardless of the state of the system\n   * (e.g., a malformed file name).\n   *\n   * This error code will not be generated by the gRPC framework.\n   */\n  InvalidArgument = \"invalid_argument\",\n\n  /**\n   * DeadlineExceeded means operation expired before completion.\n   * For operations that change the state of the system, this error may be\n   * returned even if the operation has completed successfully. For\n   * example, a successful response from a server could have been delayed\n   * long enough for the deadline to expire.\n   *\n   * The gRPC framework will generate this error code when the deadline is\n   * exceeded.\n   */\n  DeadlineExceeded = \"deadline_exceeded\",\n\n  /**\n   * NotFound means some requested entity (e.g., file or directory) was\n   * not found.\n   *\n   * This error code will not be generated by the gRPC framework.\n   */\n  NotFound = \"not_found\",\n\n  /**\n   * AlreadyExists means an attempt to create an entity failed because one\n   * already exists.\n   *\n   * This error code will not be generated by the gRPC framework.\n   */\n  AlreadyExists = \"already_exists\",\n\n  /**\n   * PermissionDenied indicates the caller does not have permission to\n   * execute the specified operation. It must not be used for rejections\n   * caused by exhausting some resource (use ResourceExhausted\n   * instead for those errors). It must not be\n   * used if the caller cannot be identified (use Unauthenticated\n   * instead for those errors).\n   *\n   * This error code will not be generated by the gRPC core framework,\n   * but expect authentication middleware to use it.\n   */\n  PermissionDenied = \"permission_denied\",\n\n  /**\n   * ResourceExhausted indicates some resource has been exhausted, perhaps\n   * a per-user quota, or perhaps the entire file system is out of space.\n   *\n   * This error code will be generated by the gRPC framework in\n   * out-of-memory and server overload situations, or when a message is\n   * larger than the configured maximum size.\n   */\n  ResourceExhausted = \"resource_exhausted\",\n\n  /**\n   * FailedPrecondition indicates operation was rejected because the\n   * system is not in a state required for the operation's execution.\n   * For example, directory to be deleted may be non-empty, an rmdir\n   * operation is applied to a non-directory, etc.\n   *\n   * A litmus test that may help a service implementor in deciding\n   * between FailedPrecondition, Aborted, and Unavailable:\n   *  (a) Use Unavailable if the client can retry just the failing call.\n   *  (b) Use Aborted if the client should retry at a higher-level\n   *      (e.g., restarting a read-modify-write sequence).\n   *  (c) Use FailedPrecondition if the client should not retry until\n   *      the system state has been explicitly fixed. E.g., if an \"rmdir\"\n   *      fails because the directory is non-empty, FailedPrecondition\n   *      should be returned since the client should not retry unless\n   *      they have first fixed up the directory by deleting files from it.\n   *  (d) Use FailedPrecondition if the client performs conditional\n   *      REST Get/Update/Delete on a resource and the resource on the\n   *      server does not match the condition. E.g., conflicting\n   *      read-modify-write on the same resource.\n   *\n   * This error code will not be generated by the gRPC framework.\n   */\n  FailedPrecondition = \"failed_precondition\",\n\n  /**\n   * Aborted indicates the operation was aborted, typically due to a\n   * concurrency issue like sequencer check failures, transaction aborts,\n   * etc.\n   *\n   * See litmus test above for deciding between FailedPrecondition,\n   * Aborted, and Unavailable.\n   */\n  Aborted = \"aborted\",\n\n  /**\n   * OutOfRange means operation was attempted past the valid range.\n   * E.g., seeking or reading past end of file.\n   *\n   * Unlike InvalidArgument, this error indicates a problem that may\n   * be fixed if the system state changes. For example, a 32-bit file\n   * system will generate InvalidArgument if asked to read at an\n   * offset that is not in the range [0,2^32-1], but it will generate\n   * OutOfRange if asked to read from an offset past the current\n   * file size.\n   *\n   * There is a fair bit of overlap between FailedPrecondition and\n   * OutOfRange. We recommend using OutOfRange (the more specific\n   * error) when it applies so that callers who are iterating through\n   * a space can easily look for an OutOfRange error to detect when\n   * they are done.\n   *\n   * This error code will not be generated by the gRPC framework.\n   */\n  OutOfRange = \"out_of_range\",\n\n  /**\n   * Unimplemented indicates operation is not implemented or not\n   * supported/enabled in this service.\n   *\n   * This error code will be generated by the gRPC framework. Most\n   * commonly, you will see this error code when a method implementation\n   * is missing on the server. It can also be generated for unknown\n   * compression algorithms or a disagreement as to whether an RPC should\n   * be streaming.\n   */\n  Unimplemented = \"unimplemented\",\n\n  /**\n   * Internal errors. Means some invariants expected by underlying\n   * system has been broken. If you see one of these errors,\n   * something is very broken.\n   *\n   * This error code will be generated by the gRPC framework in several\n   * internal error conditions.\n   */\n  Internal = \"internal\",\n\n  /**\n   * Unavailable indicates the service is currently unavailable.\n   * This is a most likely a transient condition and may be corrected\n   * by retrying with a backoff. Note that it is not always safe to retry\n   * non-idempotent operations.\n   *\n   * See litmus test above for deciding between FailedPrecondition,\n   * Aborted, and Unavailable.\n   *\n   * This error code will be generated by the gRPC framework during\n   * abrupt shutdown of a server process or network connection.\n   */\n  Unavailable = \"unavailable\",\n\n  /**\n   * DataLoss indicates unrecoverable data loss or corruption.\n   *\n   * This error code will not be generated by the gRPC framework.\n   */\n  DataLoss = \"data_loss\",\n\n  /**\n   * Unauthenticated indicates the request does not have valid\n   * authentication credentials for the operation.\n   *\n   * The gRPC framework will generate this error code when the\n   * authentication metadata is invalid or a Credentials callback fails,\n   * but also expect authentication middleware to generate it.\n   */\n  Unauthenticated = \"unauthenticated\"\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/api/gateway.ts",
    "content": "import { AuthHandlerBrand } from \"../auth/mod\";\nimport * as runtime from \"../internal/runtime/mod\";\nimport { setCurrentRequest } from \"../internal/reqtrack/mod\";\n\nexport class Gateway {\n  public readonly name: string;\n  public readonly cfg: GatewayConfig;\n  private impl: runtime.Gateway;\n\n  constructor(cfg: GatewayConfig) {\n    this.name = \"api-gateway\";\n    this.cfg = cfg;\n\n    let auth: any = cfg.authHandler;\n    if (auth) {\n      const handler = auth;\n      auth = (req: runtime.Request) => {\n        setCurrentRequest(req);\n        return handler(req.payload());\n      };\n    }\n\n    this.impl = runtime.RT.gateway(\"api-gateway\", {\n      auth,\n    });\n  }\n}\n\nexport interface GatewayConfig {\n  authHandler?: AuthHandlerBrand;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/api/httpstatus.ts",
    "content": "export const HttpStatusValues = {\n  // 1xx Informational responses\n  Continue: 100,\n  SwitchingProtocols: 101,\n  Processing: 102,\n  EarlyHints: 103,\n\n  // 2xx Success\n  OK: 200,\n  Created: 201,\n  Accepted: 202,\n  NonAuthoritativeInformation: 203,\n  NoContent: 204,\n  ResetContent: 205,\n  PartialContent: 206,\n  MultiStatus: 207,\n  AlreadyReported: 208,\n  IMUsed: 226,\n\n  // 3xx Redirection\n  MultipleChoices: 300,\n  MovedPermanently: 301,\n  Found: 302,\n  SeeOther: 303,\n  NotModified: 304,\n  UseProxy: 305,\n  SwitchProxy: 306,\n  TemporaryRedirect: 307,\n  PermanentRedirect: 308,\n\n  // 4xx Client Error\n  BadRequest: 400,\n  Unauthorized: 401,\n  PaymentRequired: 402,\n  Forbidden: 403,\n  NotFound: 404,\n  MethodNotAllowed: 405,\n  NotAcceptable: 406,\n  ProxyAuthenticationRequired: 407,\n  RequestTimeout: 408,\n  Conflict: 409,\n  Gone: 410,\n  LengthRequired: 411,\n  PreconditionFailed: 412,\n  PayloadTooLarge: 413,\n  URITooLong: 414,\n  UnsupportedMediaType: 415,\n  RangeNotSatisfiable: 416,\n  ExpectationFailed: 417,\n  ImATeapot: 418,\n  MisdirectedRequest: 421,\n  UnprocessableEntity: 422,\n  Locked: 423,\n  FailedDependency: 424,\n  TooEarly: 425,\n  UpgradeRequired: 426,\n  PreconditionRequired: 428,\n  TooManyRequests: 429,\n  RequestHeaderFieldsTooLarge: 431,\n  UnavailableForLegalReasons: 451,\n\n  // 5xx Server Error\n  InternalServerError: 500,\n  NotImplemented: 501,\n  BadGateway: 502,\n  ServiceUnavailable: 503,\n  GatewayTimeout: 504,\n  HTTPVersionNotSupported: 505,\n  VariantAlsoNegotiates: 506,\n  InsufficientStorage: 507,\n  LoopDetected: 508,\n  NotExtended: 510,\n  NetworkAuthenticationRequired: 511\n} as const;\n"
  },
  {
    "path": "runtimes/js/encore.dev/api/mod.ts",
    "content": "/* eslint-disable */\n\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { RequestMeta, currentRequest } from \"../mod\";\nimport { RawResponse } from \"./mod\";\nimport { RawRequest } from \"./mod\";\nimport { InternalHandlerResponse } from \"../internal/appinit/mod\";\nimport { IterableSocket, IterableStream, Sink } from \"./stream\";\nexport { RawRequest, RawResponse } from \"../internal/api/node_http\";\n\nexport type Method =\n  | \"GET\"\n  | \"POST\"\n  | \"PUT\"\n  | \"DELETE\"\n  | \"PATCH\"\n  | \"HEAD\"\n  | \"OPTIONS\"\n  | \"TRACE\"\n  | \"CONNECT\";\n\nexport type Header<\n  TypeOrName extends string | number | boolean | Date = string,\n  Name extends string = \"\"\n> = TypeOrName extends string ? string : TypeOrName;\n\nexport type Query<\n  TypeOrName extends\n    | string\n    | string[]\n    | number\n    | number[]\n    | boolean\n    | boolean[]\n    | Date\n    | Date[] = string,\n  Name extends string = \"\"\n> = TypeOrName extends string ? string : TypeOrName;\n\nexport type CookieWithOptions<T> = {\n  value: T;\n  expires?: Date;\n  sameSite?: \"Strict\" | \"Lax\" | \"None\";\n  domain?: string;\n  path?: string;\n  maxAge?: number;\n  secure?: boolean;\n  httpOnly?: boolean;\n  partitioned?: boolean;\n};\n\nexport type Cookie<\n  TypeOrName extends string | number | boolean | Date = string,\n  Name extends string = \"\"\n> = TypeOrName extends string\n  ? CookieWithOptions<string>\n  : CookieWithOptions<TypeOrName>;\n\nexport interface APIOptions {\n  /**\n   * The HTTP method(s) to match for this endpoint.\n   * Use \"*\" to match any method.\n   */\n  method?: Method | Method[] | \"*\";\n\n  /**\n   * The request path to match for this endpoint.\n   *\n   * Use `:` to define single-segment parameters, e.g. `/users/:id`.\n   * Use `*` to match any number of segments, e.g. `/files/*path`.\n   *\n   * If not specified, it defaults to `/<service-name>.<endpoint-name>`.\n   */\n  path?: string;\n\n  /**\n   * Whether or not to make this endpoint publicly accessible.\n   * If false, the endpoint is only accessible from the internal network.\n   *\n   * Defaults to false if not specified.\n   */\n  expose?: boolean;\n\n  /**\n   * Whether or not the request must contain valid authentication credentials.\n   * If set to true and the request is not authenticated,\n   * Encore returns a 401 Unauthorized error.\n   *\n   * Defaults to false if not specified.\n   */\n  auth?: boolean;\n\n  /**\n   * The maximum body size, in bytes. If the request body exceeds this value,\n   * Encore stops request processing and returns an error.\n   *\n   * If left unspecified it defaults to a reasonable default (currently 2MiB).\n   * If set to `null`, the body size is unlimited.\n   **/\n  bodyLimit?: number | null;\n\n  /**\n   * Tags to filter endpoints when generating clients and in middlewares.\n   */\n  tags?: string[];\n\n  /**\n   * When set to true, request information such as payloads and headers will be excluded from traces.\n   */\n  sensitive?: boolean;\n}\n\nexport interface StreamOptions {\n  /**\n   * The request path to match for this endpoint.\n   *\n   * Use `:` to define single-segment parameters, e.g. `/users/:id`.\n   * Use `*` to match any number of segments, e.g. `/files/*path`.\n   *\n   * If not specified, it defaults to `/<service-name>.<endpoint-name>`.\n   */\n  path?: string;\n\n  /**\n   * Whether or not to make this endpoint publicly accessible.\n   * If false, the endpoint is only accessible from the internal network.\n   *\n   * Defaults to false if not specified.\n   */\n  expose?: boolean;\n\n  /**\n   * Whether or not the request must contain valid authentication credentials.\n   * If set to true and the request is not authenticated,\n   * Encore returns a 401 Unauthorized error.\n   *\n   * Defaults to false if not specified.\n   */\n  auth?: boolean;\n\n  /**\n   * Tags to filter endpoints when generating clients and in middlewares.\n   */\n  tags?: string[];\n\n  /**\n   * When set to true, request information such as payloads and headers will be excluded from traces.\n   */\n  sensitive?: boolean;\n}\n\ntype HandlerFn<Params, Response> = Params extends void\n  ? () => Promise<Response>\n  : (params: Params) => Promise<Response>;\n\nexport function api<\n  Params extends object | void = void,\n  Response extends object | void = void\n>(\n  options: APIOptions,\n  fn: (params: Params) => Promise<Response>\n): HandlerFn<Params, Response>;\n\nexport function api<\n  Params extends object | void = void,\n  Response extends object | void = void\n>(\n  options: APIOptions,\n  fn: (params: Params) => Response\n): HandlerFn<Params, Response>;\nexport function api(options: APIOptions, fn: any): typeof fn {\n  return fn;\n}\n\nexport type RawHandler = (req: IncomingMessage, resp: ServerResponse) => void;\n\napi.raw = function raw(options: APIOptions, fn: RawHandler) {\n  return fn;\n};\n\nexport interface StreamIn<Request> extends AsyncIterable<Request> {\n  recv: () => Promise<Request>;\n}\nexport interface StreamOutWithResponse<Request, Response>\n  extends StreamOut<Request> {\n  response: () => Promise<Response>;\n}\n\nexport interface StreamOut<Response> {\n  send: (msg: Response) => Promise<void>;\n  close: () => Promise<void>;\n}\n\nexport type StreamInOutHandlerFn<HandshakeData, Request, Response> =\n  HandshakeData extends void\n    ? (stream: StreamInOut<Request, Response>) => Promise<void>\n    : (\n        data: HandshakeData,\n        stream: StreamInOut<Request, Response>\n      ) => Promise<void>;\n\nexport type StreamOutHandlerFn<HandshakeData, Response> =\n  HandshakeData extends void\n    ? (stream: StreamOut<Response>) => Promise<void>\n    : (data: HandshakeData, stream: StreamOut<Response>) => Promise<void>;\n\nexport type StreamInHandlerFn<HandshakeData, Request, Response> =\n  HandshakeData extends void\n    ? (stream: StreamIn<Request>) => Promise<Response>\n    : (data: HandshakeData, stream: StreamIn<Request>) => Promise<Response>;\n\nexport type StreamInOut<Request, Response> = StreamIn<Request> &\n  StreamOut<Response>;\n\nfunction streamInOut<HandshakeData, Request, Response>(\n  options: StreamOptions,\n  fn: (\n    data: HandshakeData,\n    stream: StreamInOut<Request, Response>\n  ) => Promise<void>\n): StreamInOutHandlerFn<HandshakeData, Request, Response>;\nfunction streamInOut<Request, Response>(\n  options: StreamOptions,\n  fn: (stream: StreamInOut<Request, Response>) => Promise<void>\n): StreamInOutHandlerFn<void, Request, Response>;\nfunction streamInOut(options: StreamOptions, fn: any): typeof fn {\n  return fn;\n}\n\nfunction streamIn<Request>(\n  options: StreamOptions,\n  fn: (stream: StreamIn<Request>) => Promise<void>\n): StreamInHandlerFn<void, Request, void>;\nfunction streamIn<Request, Response>(\n  options: StreamOptions,\n  fn: (stream: StreamIn<Request>) => Promise<Response>\n): StreamInHandlerFn<void, Request, Response>;\nfunction streamIn<HandshakeData, Request, Response>(\n  options: StreamOptions,\n  fn: (data: HandshakeData, stream: StreamIn<Request>) => Promise<Response>\n): StreamInHandlerFn<HandshakeData, Request, Response>;\nfunction streamIn(options: StreamOptions, fn: any): typeof fn {\n  return fn;\n}\n\nfunction streamOut<HandshakeData, Response>(\n  options: StreamOptions,\n  fn: (data: HandshakeData, stream: StreamOut<Response>) => Promise<void>\n): StreamOutHandlerFn<HandshakeData, Response>;\nfunction streamOut<Response>(\n  options: StreamOptions,\n  fn: (stream: StreamOut<Response>) => Promise<void>\n): StreamOutHandlerFn<void, Response>;\nfunction streamOut(options: StreamOptions, fn: any): typeof fn {\n  return fn;\n}\n\napi.streamInOut = streamInOut;\napi.streamIn = streamIn;\napi.streamOut = streamOut;\n\nexport interface StaticOptions {\n  /**\n   * The request path to match for this endpoint.\n   *\n   * Use `:` to define single-segment parameters, e.g. `/users/:id`.\n   * Use `*` to match any number of segments, e.g. `/files/*path`.\n   *\n   * If not specified, it defaults to `/<service-name>.<endpoint-name>`.\n   */\n  path?: string;\n\n  /**\n   * Whether or not to make this endpoint publicly accessible.\n   * If false, the endpoint is only accessible from the internal network.\n   *\n   * Defaults to false if not specified.\n   */\n  expose?: boolean;\n\n  /**\n   * Whether or not the request must contain valid authentication credentials.\n   * If set to true and the request is not authenticated,\n   * Encore returns a 401 Unauthorized error.\n   *\n   * Defaults to false if not specified.\n   */\n  auth?: boolean;\n\n  /**\n   * The relative path to the directory containing the static files to serve.\n   *\n   * The provided path must be a subdirectory from the calling file's directory.\n   */\n  dir: string;\n\n  /**\n   * Path to the file to serve when the requested file is not found.\n   * The path must be a relative path to within the calling file's directory.\n   */\n  notFound?: string;\n\n  /**\n   * Http Status code used when serving notFound fallback.\n   * Defaults to 404.\n   */\n  notFoundStatus?: number;\n\n  /**\n   * Custom HTTP headers to apply to all static files served.\n   *\n   * @example\n   * ```typescript\n   * headers: {\n   *   \"Cache-Control\": \"public, max-age=3600\",\n   *   \"X-Content-Type-Options\": \"nosniff\",\n   * }\n   * ```\n   */\n  headers?: Record<string, string | string[]>;\n}\n\nexport class StaticAssets {\n  public readonly options: StaticOptions;\n\n  constructor(options: StaticOptions) {\n    this.options = options;\n  }\n}\n\napi.static = function staticAssets(options: StaticOptions) {\n  return new StaticAssets(options);\n};\n\nexport interface MiddlewareOptions {\n  /**\n   * Configuration for what endpoints that should be targeted by the middleware\n   */\n  target?: {\n    /**\n     * If set, only run middleware on endpoints that are either exposed or not\n     * exposed.\n     */\n    expose?: boolean;\n\n    /**\n     * If set, only run middleware on endpoints that either require or not\n     * requires auth.\n     */\n    auth?: boolean;\n\n    /**\n     * If set, only run middleware on endpoints that are raw endpoints.\n     */\n    isRaw?: boolean;\n\n    /**\n     * If set, only run middleware on endpoints that are stream endpoints.\n     */\n    isStream?: boolean;\n\n    /**\n     * If set, only run middleware on endpoints that have specific tags.\n     * These tags are evaluated with OR, meaning the middleware applies to an\n     * API if the API has at least one of those tags.\n     */\n    tags?: string[];\n  };\n}\n\nexport class MiddlewareRequest {\n  private _reqMeta?: RequestMeta;\n  private _stream?: IterableStream | IterableSocket | Sink;\n  private _rawReq?: RawRequest;\n  private _rawResp?: RawResponse;\n  private _data?: Record<string, any>;\n\n  constructor(\n    stream?: IterableStream | IterableSocket | Sink,\n    rawReq?: RawRequest,\n    rawResp?: RawResponse\n  ) {\n    this._stream = stream;\n    this._rawReq = rawReq;\n    this._rawResp = rawResp;\n  }\n\n  /**\n   * requestMeta is set when the handler is a typed handler or a stream handler.\n   * for raw handlers, see rawRequest and rawResponse.\n   */\n  public get requestMeta(): RequestMeta | undefined {\n    return this._reqMeta || (this._reqMeta = currentRequest());\n  }\n\n  /**\n   * stream is set when the handler is a stream handler.\n   */\n  public get stream(): IterableStream | IterableSocket | Sink | undefined {\n    return this._stream;\n  }\n\n  /**\n   * rawRequest is set when the handler is a raw request handler.\n   */\n  public get rawRequest(): RawRequest | undefined {\n    return this._rawReq;\n  }\n\n  /**\n   * rawResponse is set when the handler is a raw request handler.\n   */\n  public get rawResponse(): RawResponse | undefined {\n    return this._rawResp;\n  }\n\n  /**\n   * data can be used to pass data from middlewares to the handler.\n   * The data will be available via `currentRequest()`\n   */\n  public get data(): Record<string, any> {\n    if (this._data === undefined) {\n      this._data = {};\n    }\n    return this._data;\n  }\n}\n\nexport class ResponseHeader {\n  headers: Record<string, string | string[]>;\n\n  constructor() {\n    this.headers = {};\n  }\n\n  /**\n   * set will set a header value for a key, if a previous middleware has\n   * already set a value, it will be overridden.\n   */\n  public set(key: string, value: string | string[]) {\n    this.headers[key] = value;\n  }\n\n  /**\n   * add adds a header value to a key, if a previous middleware has\n   * already set a value, they will be appended.\n   */\n  public add(key: string, value: string | string[]) {\n    const prev = this.headers[key];\n\n    if (prev === undefined) {\n      this.headers[key] = value;\n    } else {\n      this.headers[key] = [prev, value].flat();\n    }\n  }\n}\n\nexport class HandlerResponse {\n  /**\n   * The payload returned by the handler when the handler is either\n   * a typed handler or stream handler.\n   */\n  payload: any;\n\n  private _headers?: ResponseHeader;\n  private _status?: number;\n\n  constructor(payload: any) {\n    this.payload = payload;\n  }\n\n  /**\n   * header can be used by middlewares to set headers to the\n   * response. This only works for typed handler. For raw handlers\n   * see MiddlewareRequest.rawResponse.\n   */\n  public get header(): ResponseHeader {\n    if (this._headers === undefined) {\n      this._headers = new ResponseHeader();\n    }\n\n    return this._headers;\n  }\n\n  /**\n   * Override the http status code for successful requests for typed endpoints.\n   */\n  public set status(s: number) {\n    this._status = s;\n  }\n\n  /**\n   * __internalToResponse converts a response to the internal representation\n   */\n  __internalToResponse(): InternalHandlerResponse {\n    return {\n      payload: this.payload,\n      extraHeaders: this._headers?.headers,\n      status: this._status\n    };\n  }\n}\n\nexport type Next = (req: MiddlewareRequest) => Promise<HandlerResponse>;\n\nexport type MiddlewareFn = (\n  req: MiddlewareRequest,\n  next: Next\n) => Promise<HandlerResponse>;\n\nexport interface Middleware extends MiddlewareFn {\n  options?: MiddlewareOptions;\n}\n\nexport function middleware(m: MiddlewareFn): Middleware;\nexport function middleware(\n  options: MiddlewareOptions,\n  fn: MiddlewareFn\n): Middleware;\n\nexport function middleware(\n  a: MiddlewareFn | MiddlewareOptions,\n  b?: MiddlewareFn\n): Middleware {\n  if (b === undefined) {\n    return a as Middleware;\n  } else {\n    const opts = a as MiddlewareOptions;\n    // Wrap the middleware function to delegate calls and preserve the original options.\n    // The options object is stored separately and made immutable to prevent accidental mutation.\n    const mw: Middleware = (req: MiddlewareRequest, next: Next) => {\n      return b(req, next);\n    };\n    mw.options = Object.freeze({ ...opts });\n\n    return mw;\n  }\n}\n\n/**\n * Options when making api calls.\n *\n * This interface will be extended with additional fields from\n * app's generated code.\n */\nexport interface CallOpts {\n  /* authData?: AuthData */\n}\n\nimport { HttpStatusValues } from \"./httpstatus\";\nexport const HttpStatus = HttpStatusValues;\nexport type HttpStatus =\n  (typeof HttpStatusValues)[keyof typeof HttpStatusValues];\n\nexport { APIError, ErrCode } from \"./error\";\nexport { Gateway, type GatewayConfig } from \"./gateway\";\nexport { IterableSocket, IterableStream, Sink } from \"./stream\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/api/stream.ts",
    "content": "import * as runtime from \"../internal/runtime/mod\";\n\nexport class IterableStream {\n  private stream: runtime.Stream;\n\n  constructor(stream: runtime.Stream) {\n    this.stream = stream;\n  }\n\n  recv(): Promise<Record<string, any>> {\n    return this.stream.recv();\n  }\n\n  async *[Symbol.asyncIterator]() {\n    while (true) {\n      try {\n        yield await this.stream.recv();\n      } catch (e) {\n        break;\n      }\n    }\n  }\n}\n\nexport class IterableSocket {\n  private socket: runtime.Socket;\n\n  constructor(socket: runtime.Socket) {\n    this.socket = socket;\n  }\n\n  send(msg: Record<string, any>): void {\n    return this.socket.send(msg);\n  }\n  recv(): Promise<Record<string, any>> {\n    return this.socket.recv();\n  }\n\n  close(): void {\n    this.socket.close();\n  }\n\n  async *[Symbol.asyncIterator]() {\n    while (true) {\n      try {\n        yield await this.socket.recv();\n      } catch (e) {\n        break;\n      }\n    }\n  }\n}\n\nexport class Sink {\n  private sink: runtime.Sink;\n\n  constructor(sink: runtime.Sink) {\n    this.sink = sink;\n  }\n\n  send(msg: Record<string, any>): void {\n    return this.sink.send(msg);\n  }\n\n  close(): void {\n    this.sink.close();\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/app_meta.ts",
    "content": "import * as runtime from \"./internal/runtime/mod\";\n\n// Describes the running Encore application.\nexport interface AppMeta {\n  // The Encore application ID. If the application is not linked to the Encore platform this will be an empty string.\n  // To link to the Encore platform run `encore app link` from your terminal in the root directory of the Encore app.\n  appId: string;\n\n  // The base URL which can be used to call the API of this running application.\n  //\n  // For local development it is \"http://localhost:<port>\", typically \"http://localhost:4000\".\n  //\n  // If a custom domain is used for this environment it is returned here, but note that\n  // changes only take effect at the time of deployment while custom domains can be updated at any time.\n  apiBaseUrl: string;\n\n  // Information about the environment the app is running in.\n  environment: EnvironmentMeta;\n\n  // Information about the build.\n  build: BuildMeta;\n\n  // Information about the deployment.\n  deploy: DeployMeta;\n}\n\n// Describes the environment the Encore application is running in.\nexport interface EnvironmentMeta {\n  // The name of environment that this application.\n  // For local development it is \"local\".\n  name: string;\n\n  // The type of environment is this application running in.\n  // For local development it is \"development\".\n  type: EnvironmentType;\n\n  // The cloud this is running in.\n  // For local development it is \"local\".\n  cloud: CloudProvider;\n}\n\n// Describes what type of environment the application is running in.\nexport type EnvironmentType =\n  // A production environment.\n  | \"production\"\n  // A long-lived cloud-hosted, non-production environment, such as test environments, or local development.\n  | \"development\"\n  // A short-lived cloud-hosted, non-production environments, such as preview environments\n  | \"ephemeral\"\n  // When running automated tests.\n  | \"test\";\n\n// Describes what cloud provider the application is running in.\nexport type CloudProvider =\n  | \"aws\" // Amazon Web Services\n  | \"gcp\" // Google Cloud Platform\n  | \"azure\" // Microsoft Azure\n  | \"encore\" // Encore Cloud.\n  | \"local\"; // Local development\n\n// Information about the build that formed the running application.\nexport interface BuildMeta {\n  // The git commit that formed the base of this build.\n  revision: string;\n\n  // Whether there were uncommitted changes on top of the commit.\n  uncommittedChanges: boolean;\n}\n\n// Information about the deployment of the running application.\nexport interface DeployMeta {\n  // The unique id of the deployment. Generated by the Encore Platform.\n  id: string;\n\n  // The services hosted by this deployment, keyed by the service name. \n  hostedServices: Record<string, HostedService>;\n}\n\nexport interface HostedService {\n  // The name of the service\n  name: string;\n}\n\n// Returns metadata about the running Encore application.\n//\n// The metadata is cached and is the same object each call,\n// and therefore must not be modified by the caller.\nexport function appMeta(): AppMeta {\n  // Compute the metadata on first use.\n  if (cached === null) {\n    let rt = runtime.RT.appMeta();\n    cached = {\n      appId: rt.appId,\n      apiBaseUrl: rt.apiBaseUrl,\n      environment: {\n        name: rt.environment.name,\n        type: envType(rt.environment.type),\n        cloud: cloudProvider(rt.environment.cloud)\n      },\n      build: {\n        revision: rt.build.revision,\n        uncommittedChanges: rt.build.uncommittedChanges\n      },\n      deploy: {\n        id: rt.deploy.id,\n        hostedServices: rt.deploy.hostedServices\n      }\n    };\n  }\n\n  return cached;\n}\n\nfunction envType(rtType: runtime.EnvironmentType): EnvironmentType {\n  switch (rtType) {\n    case runtime.EnvironmentType.Production:\n      return \"production\";\n    case runtime.EnvironmentType.Development:\n      return \"development\";\n    case runtime.EnvironmentType.Ephemeral:\n      return \"ephemeral\";\n    case runtime.EnvironmentType.Test:\n      return \"test\";\n    default:\n      return \"development\";\n  }\n}\n\nfunction cloudProvider(rtType: runtime.CloudProvider): CloudProvider {\n  switch (rtType) {\n    case runtime.CloudProvider.AWS:\n      return \"aws\";\n    case runtime.CloudProvider.GCP:\n      return \"gcp\";\n    case runtime.CloudProvider.Azure:\n      return \"azure\";\n    case runtime.CloudProvider.Encore:\n      return \"encore\";\n    case runtime.CloudProvider.Local:\n      return \"local\";\n    default:\n      return \"local\";\n  }\n}\n\n// The cached app metadata. Set on first use.\nlet cached: AppMeta | null = null;\n"
  },
  {
    "path": "runtimes/js/encore.dev/auth/mod.ts",
    "content": "export type AuthHandler<\n  Params extends object,\n  AuthData extends { userID: string },\n> = ((params: Params) => Promise<AuthData | null>) & AuthHandlerBrand;\n\nexport type AuthHandlerBrand = { readonly __authHandlerBrand: unique symbol };\n\nexport function authHandler<\n  Params extends object,\n  AuthData extends { userID: string },\n>(\n  fn: (params: Params) => Promise<AuthData | null>,\n): AuthHandler<Params, AuthData> {\n  return fn as any;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/config/mod.ts",
    "content": "export type { Secret, AnySecret } from \"./secrets\";\nexport { secret } from \"./secrets\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/config/secrets.ts",
    "content": "import * as runtime from \"../internal/runtime/mod\";\nimport { StringLiteral } from \"../internal/utils/constraints\";\n\n/**\n * Secret represents a single secret value that is loaded\n * into the application. It is strongly typed for that secret,\n * so that you can write functions which expect a specific one.\n *\n * You can use {@link AnySecret} to represent any secret without knowing\n * it's name.\n *\n * @example\n *\n * function doFoo(s: Secret<\"foo\">): void {\n *   const foo = s();\n * }\n */\nexport interface Secret<Name extends string> {\n  /**\n   * Returns the current value of the secret.\n   *\n   * Encore will periodically refresh the value of the secret, so this\n   * value may change over time and could be stale for upto a couple of\n   * minutes. If you need to ensure you have the latest value, use\n   * {@link latest}.\n   */\n  (): string;\n\n  /**\n   * The name of the secret.\n   */\n  readonly name: Name;\n}\n\n/**\n * AnySecret is a type which can be used to represent any {@link Secret}\n * without knowing its name.\n */\nexport type AnySecret = Secret<string>;\n\n/**\n * secret is used to load a single {@link Secret} into the application.\n *\n * If you wish to load multiple secrets at once, see {@link secrets}.\n *\n * @example loading a single secret\n *  import {secret} from \"encore.dev/config/secrets\";\n *  const foo = secret<\"foo\">();\n */\nexport function secret<Name extends string>(\n  name: StringLiteral<Name>\n): Secret<Name> {\n  // Get the secret implementation from the runtime.\n  // It reports null if the secret isn't in the runtime config.\n  const impl = runtime.RT.secret(name);\n  const secretObj = () => {\n    if (impl === null) {\n      // During local development we don't consider missing secrets a fatal error.\n      if (\n        runtime.RT.appMeta().environment.cloud === runtime.CloudProvider.Local\n      ) {\n        return \"\";\n      }\n      throw new Error(`secret ${name} is not set`);\n    }\n    return impl.cached();\n  };\n\n  secretObj.toString = () => {\n    if (impl === null) {\n      return `Secret<${name}>(not set)`;\n    }\n    return `Secret<${name}>(*********)`;\n  };\n  Object.defineProperty(secretObj, \"name\", { value: name });\n\n  return secretObj as unknown as Secret<Name>;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/cron/mod.ts",
    "content": "/* eslint-disable */\n\nimport { DurationString } from \"../internal/types/mod\";\n\nexport class CronJob {\n  public readonly name: string;\n  public readonly cfg: CronJobConfig;\n  constructor(name: string, cfg: CronJobConfig) {\n    this.name = name;\n    this.cfg = cfg;\n  }\n}\n\nexport type CronJobConfig = {\n  endpoint: () => Promise<unknown>;\n  title?: string;\n} & ({ every: DurationString } | { schedule: string });\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/api/mod.ts",
    "content": "import * as runtime from \"../runtime/mod\";\nimport { getCurrentRequest } from \"../reqtrack/mod\";\nimport { APIError, ErrCode } from \"../../api/error\";\n\nexport async function apiCall(\n  service: string,\n  endpoint: string,\n  data: any,\n  opts?: runtime.CallOpts\n): Promise<any> {\n  const source = getCurrentRequest();\n  const resp = await runtime.RT.apiCall(service, endpoint, data, source, opts);\n\n  // Convert any call error to our APIError type.\n  // We do this here because NAPI doesn't have great support\n  // for custom exception types yet.\n  if (resp instanceof runtime.ApiCallError) {\n    throw new APIError(\n      resp.code as ErrCode,\n      resp.message,\n      undefined,\n      resp.details\n    );\n  }\n\n  return resp;\n}\n\nexport async function streamInOut(\n  service: string,\n  endpoint: string,\n  data: any,\n  opts?: runtime.CallOpts\n): Promise<any> {\n  const source = getCurrentRequest();\n  const stream = await runtime.RT.stream(service, endpoint, data, source, opts);\n\n  return {\n    async send(msg: any) {\n      stream.send(msg);\n    },\n    async recv(): Promise<any> {\n      return stream.recv();\n    },\n    async close() {\n      stream.close();\n    },\n    async *[Symbol.asyncIterator]() {\n      while (true) {\n        try {\n          yield await stream.recv();\n        } catch (e) {\n          break;\n        }\n      }\n    }\n  };\n}\n\nexport async function streamIn(\n  service: string,\n  endpoint: string,\n  data: any,\n  opts?: runtime.CallOpts\n): Promise<any> {\n  const source = getCurrentRequest();\n  const stream = await runtime.RT.stream(service, endpoint, data, source, opts);\n  const response = new Promise(async (resolve, reject) => {\n    try {\n      resolve(await stream.recv());\n    } catch (e) {\n      reject(e);\n    }\n  });\n\n  return {\n    async send(msg: any) {\n      stream.send(msg);\n    },\n    async close() {\n      stream.close();\n    },\n    async response(): Promise<any> {\n      return response;\n    }\n  };\n}\n\nexport async function streamOut(\n  service: string,\n  endpoint: string,\n  data: any,\n  opts?: runtime.CallOpts\n): Promise<any> {\n  const source = getCurrentRequest();\n  const stream = await runtime.RT.stream(service, endpoint, data, source, opts);\n\n  return {\n    async recv(): Promise<any> {\n      return stream.recv();\n    },\n    async close() {\n      stream.close();\n    },\n    async *[Symbol.asyncIterator]() {\n      while (true) {\n        try {\n          yield await stream.recv();\n        } catch (e) {\n          break;\n        }\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/api/node_http.ts",
    "content": "import type {\n  IncomingHttpHeaders,\n  OutgoingHttpHeader,\n  OutgoingHttpHeaders\n} from \"node:http\";\nimport type { AddressInfo, Socket, SocketReadyState } from \"node:net\";\nimport * as stream from \"node:stream\";\nimport * as runtime from \"../runtime/mod\";\n\nexport class RawRequest extends stream.Readable {\n  complete: boolean;\n\n  trailers: NodeJS.Dict<string>;\n  trailersDistinct: NodeJS.Dict<string[]>;\n  rawTrailers: string[];\n\n  readonly connection: Socket | null; // deprecated\n  readonly socket: Socket | null;\n\n  private body: runtime.BodyReader;\n  private req: runtime.Request;\n\n  constructor(req: runtime.Request, body: runtime.BodyReader) {\n    super({});\n    this.req = req;\n    this.complete = false;\n\n    this.trailers = {};\n    this.trailersDistinct = {};\n    this.rawTrailers = [];\n\n    this.body = body;\n    this.body.start(this.push.bind(this), this.destroy.bind(this));\n\n    // Set the socket to a dummy value for legacy compatibility with Express.js.\n    this.socket = new DummySocket() as Socket;\n    this.connection = this.socket; // legacy alias\n  }\n\n  get method(): string {\n    return this.meta.apiCall!.method;\n  }\n\n  _url: string | undefined;\n  get url(): string {\n    if (!this._url) {\n      this._url = this.meta.apiCall!.pathAndQuery;\n    }\n    return this._url!;\n  }\n  set url(value: string) {\n    this._url = value;\n  }\n\n  get headers(): IncomingHttpHeaders {\n    return this.meta.apiCall!.headers;\n  }\n\n  _headersDistinct: NodeJS.Dict<string[]> | undefined;\n  get headersDistinct(): NodeJS.Dict<string[]> {\n    if (this._headersDistinct) {\n      return this._headersDistinct;\n    }\n\n    const headers: NodeJS.Dict<string[]> = {};\n    for (const [key, value] of Object.entries(this.headers)) {\n      if (value !== undefined) {\n        const val: string[] = Array.isArray(value) ? value : [value];\n        headers[key] = val;\n      }\n    }\n    this._headersDistinct = headers;\n    return headers;\n  }\n\n  _rawHeaders: string[] | undefined;\n  get rawHeaders(): string[] {\n    if (this._rawHeaders) {\n      return this._rawHeaders;\n    }\n\n    this._rawHeaders = Object.keys(this.headers);\n    return this._rawHeaders;\n  }\n\n  private _meta: runtime.RequestMeta | undefined;\n  private get meta(): runtime.RequestMeta {\n    if (this._meta === undefined) {\n      this._meta = this.req.meta();\n    }\n    return this._meta;\n  }\n\n  _read(size: number): void {\n    this.body.read();\n  }\n\n  setTimeout(msecs: number, callback?: () => void): this {\n    // Not yet implemented.\n    return this;\n  }\n}\n\nexport class RawResponse extends stream.Writable {\n  readonly req: RawRequest;\n  chunkedEncoding: boolean;\n  shouldKeepAlive: boolean;\n  // useChunkedEncodingByDefault: boolean;\n  sendDate: boolean;\n  statusCode: number;\n  statusMessage: string | undefined;\n\n  finished: boolean; // deprecated\n  headersSent: boolean;\n  strictContentLength: boolean;\n\n  readonly connection: Socket | null; // deprecated\n  readonly socket: Socket | null;\n\n  private w: runtime.ResponseWriter;\n  private headers: OutgoingHttpHeaders;\n\n  constructor(req: RawRequest, w: runtime.ResponseWriter) {\n    super({ highWaterMark: 1024 * 1024 }); // TODO?\n    this.req = req;\n    this.chunkedEncoding = false; // TODO\n    this.shouldKeepAlive = true;\n    this.sendDate = true;\n    this.statusCode = 200;\n    this.statusMessage = undefined;\n    this.finished = false;\n    this.strictContentLength = false;\n    this.headersSent = false;\n    this.headers = {};\n\n    this.w = w;\n\n    // Set the socket to a dummy value for legacy compatibility with Express.js.\n    this.socket = new DummySocket() as Socket;\n    this.connection = this.socket; // legacy alias\n  }\n\n  write(\n    chunk: any,\n    callback?: ((error: Error | null | undefined) => void) | undefined\n  ): boolean;\n  write(\n    chunk: any,\n    encoding: BufferEncoding,\n    callback?: ((error: Error | null | undefined) => void) | undefined\n  ): boolean;\n  write(chunk: unknown, encoding?: unknown, callback?: unknown): boolean {\n    const res = super.write(chunk, encoding as any, callback as any);\n    // HACK: Work around pipe deadlock in Node.js when writing a large response.\n    return true;\n  }\n\n  // Needed for Next.js compatibility.\n  _implicitHeader() {\n    this._writeHeaderIfNeeded();\n  }\n\n  _writeHeaderIfNeeded() {\n    if (!this.headersSent) {\n      this.w.writeHead(\n        this.statusCode,\n        this.headers as Record<string, string | number | string[]>\n      );\n      this.headersSent = true;\n    }\n  }\n\n  _write(\n    chunk: Buffer,\n    _encoding: BufferEncoding,\n    callback: (error?: Error | null) => void\n  ) {\n    this._writeHeaderIfNeeded();\n    this.w.writeBody(chunk, callback);\n  }\n\n  _writev(\n    chunks: Array<{ chunk: Buffer }>,\n    callback: (error?: Error | null) => void\n  ) {\n    this._writeHeaderIfNeeded();\n    this.w.writeBodyMulti(\n      chunks.map((ch) => ch.chunk),\n      callback\n    );\n  }\n\n  _final(callback: (error?: Error | null | undefined) => void): void {\n    this._writeHeaderIfNeeded();\n    this.w.close(undefined, callback);\n  }\n\n  setTimeout(msecs: number, callback?: () => void): this {\n    // Not implemented yet.\n    return this;\n  }\n\n  setHeader(name: string, value: number | string | string[]): this {\n    this.headers[name] = value;\n    return this;\n  }\n\n  appendHeader(name: string, value: number | string | string[]): this {\n    const existing = this.headers[name];\n    const existingIsArr = Array.isArray(existing);\n    const valIsArr = Array.isArray(value);\n    if (existingIsArr && valIsArr) {\n      existing.push(...value);\n    } else if (existingIsArr) {\n      existing.push(\"\" + value);\n    } else if (existing !== undefined) {\n      this.headers[name] = [\"\" + existing, \"\" + value];\n    } else {\n      this.headers[name] = value;\n    }\n    return this;\n  }\n\n  getHeader(name: string): number | string | string[] | undefined {\n    return this.headers[name];\n  }\n\n  getHeaders(): OutgoingHttpHeaders {\n    return this.headers;\n  }\n\n  getHeaderNames(): string[] {\n    return Object.keys(this.headers);\n  }\n\n  hasHeader(name: string): boolean {\n    return this.headers[name] !== undefined;\n  }\n\n  removeHeader(name: string): void {\n    delete this.headers[name];\n  }\n\n  addTrailers(\n    headers: OutgoingHttpHeaders | readonly [string, string][]\n  ): void {\n    // Not implemented yet.\n  }\n\n  flushHeaders(): void {\n    this._writeHeaderIfNeeded();\n  }\n\n  writeHead(\n    statusCode: number,\n    headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]\n  ): this;\n  writeHead(\n    statusCode: number,\n    statusMessage?: string,\n    headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]\n  ): this;\n  writeHead(\n    statusCode: number,\n    statusMessageOrHeaders?:\n      | string\n      | OutgoingHttpHeaders\n      | OutgoingHttpHeader[],\n    headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]\n  ): this {\n    this.statusCode = statusCode;\n\n    const headersIn =\n      typeof statusMessageOrHeaders === \"string\"\n        ? headers\n        : statusMessageOrHeaders;\n\n    // Merge headers, if provided.\n    if (headersIn) {\n      if (Array.isArray(headersIn)) {\n        for (let i = 0; i < headersIn.length; i += 2) {\n          const key = headersIn[i];\n          const value = headersIn[i + 1];\n          if (typeof key === \"string\" && typeof value === \"string\") {\n            this.headers[key] = value;\n          }\n        }\n      } else {\n        for (const key in headersIn) {\n          const value = headersIn[key];\n          this.headers[key] = value;\n        }\n      }\n    }\n\n    this._writeHeaderIfNeeded();\n    return this;\n  }\n}\n\n// DummySocket is a dummy implementation of the net.Socket class.\n//\n// It's provided because certain libraries like Express expect the `socket` attribute\n// to be non-null on the request and response object.\nclass DummySocket extends stream.Duplex {\n  destroySoon(): void { }\n  write(): boolean { return true; }\n  connect(): this { return this; }\n  setEncoding(_encoding?: BufferEncoding): this { return this; }\n  pause(): this { return this; }\n  resetAndDestroy(): this { return this; }\n  resume(): this { return this; }\n  setTimeout(_timeout: number, _callback?: () => void): this { return this; }\n  setNoDelay(_noDelay?: boolean): this { return this; }\n  setKeepAlive(_enable?: boolean, _initialDelay?: number): this { return this; }\n  address(): AddressInfo | {} { return {}; }\n  unref(): this { return this; }\n  ref(): this { return this; }\n  readonly autoSelectFamilyAttemptedAddresses: string[] = [];\n  readonly bufferSize: number = 0;\n  readonly bytesRead: number = 0;\n  readonly bytesWritten: number = 0;\n  readonly connecting: boolean = false;\n  readonly pending: boolean = false;\n  readonly destroyed: boolean = false;\n  readonly localAddress?: string = undefined;\n  readonly localPort?: number = undefined;\n  readonly localFamily?: string = undefined;\n  readonly readyState: SocketReadyState = 'open';\n  readonly remoteAddress?: string | undefined = undefined;\n  readonly remoteFamily?: string | undefined = undefined;\n  readonly remotePort?: number | undefined = undefined;\n  readonly timeout?: number | undefined = undefined;\n  end(): this { return this; }\n  addListener(_event: string, _listener: (...args: any[]) => void): this { return this; }\n  emit(_event: string | symbol, ..._args: any[]): boolean { return true; }\n  on(_event: string, _listener: (...args: any[]) => void): this { return this; }\n  once(_event: string, _listener: (...args: any[]) => void): this { return this; }\n  prependListener(_event: string, _listener: (...args: any[]) => void): this { return this; }\n  prependOnceListener(_event: string, _listener: (...args: any[]) => void): this { return this; }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/appinit/mod.ts",
    "content": "import { isMainThread, Worker, workerData } from \"node:worker_threads\";\nimport { Gateway } from \"../../api/gateway\";\nimport { Middleware, MiddlewareRequest, HandlerResponse } from \"../../api/mod\";\nimport { IterableSocket, IterableStream, Sink } from \"../../api/stream\";\nimport { RawRequest, RawResponse } from \"../api/node_http\";\nimport { setCurrentRequest } from \"../reqtrack/mod\";\nimport * as runtime from \"../runtime/mod\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n  initGlobalMetricsBuffer,\n  setGlobalMetricsBuffer\n} from \"../metrics/registry\";\n\nexport type Handler = {\n  apiRoute: runtime.ApiRoute;\n  middlewares: Middleware[];\n  endpointOptions: EndpointOptions;\n};\n\nexport function registerHandlers(handlers: Handler[]) {\n  runtime.RT.registerHandlers(handlers.map((h) => transformHandler(h)));\n}\n\nexport function registerTestHandler(handler: Handler) {\n  runtime.RT.registerTestHandler(transformHandler(handler));\n}\n\nexport function registerGateways(gateways: Gateway[]) {\n  // This function exists to ensure gateways are imported and executed.\n  // It intentionally doesn't need to do anything.\n}\n\nexport async function run(entrypoint: string) {\n  if (isMainThread) {\n    const metricsBuffer = initGlobalMetricsBuffer();\n    const extraWorkers = runtime.RT.numWorkerThreads() - 1;\n    if (extraWorkers > 0) {\n      const path = fileURLToPath(entrypoint);\n      for (let i = 0; i < extraWorkers; i++) {\n        new Worker(path, {\n          workerData: { metricsBuffer }\n        });\n      }\n    }\n\n    return runtime.RT.runForever();\n  }\n\n  // Worker thread: set metrics buffer from workerData\n  if (workerData && workerData.metricsBuffer) {\n    setGlobalMetricsBuffer(workerData.metricsBuffer);\n  }\n\n  // This is a worker thread. The runtime is already initialized, so block forever.\n  await new Promise(() => {});\n}\n\ninterface EndpointOptions {\n  expose: boolean;\n  auth: boolean;\n  isRaw: boolean;\n  isStream: boolean;\n  tags: string[];\n}\n\nexport interface InternalHandlerResponse {\n  payload: any;\n  extraHeaders?: Record<string, string | string[]>;\n  status?: number;\n}\n\n// recursively calls all middlewares\nasync function invokeMiddlewareChain(\n  curReq: runtime.Request,\n  req: MiddlewareRequest,\n  chain: Middleware[],\n  handler: () => Promise<any>\n): Promise<InternalHandlerResponse> {\n  const execute = async (\n    index: number,\n    req: MiddlewareRequest\n  ): Promise<HandlerResponse> => {\n    const currentMiddleware = chain.at(index);\n\n    // no more middlewares, execute the handler\n    if (currentMiddleware === undefined) {\n      const mwData = req.data;\n      if (mwData !== undefined) {\n        (curReq as any).middlewareData = mwData;\n      }\n      return new HandlerResponse(await handler());\n    }\n\n    // execute current middleware\n    return currentMiddleware(req, (req) => {\n      return execute(index + 1, req);\n    });\n  };\n\n  return (await execute(0, req)).__internalToResponse();\n}\n\n// calculate what middlewares should run for an endpoint\nfunction calculateMiddlewareChain(\n  endpointOptions: EndpointOptions,\n  ms: Middleware[]\n): Middleware[] {\n  const middlewares = ms.filter((m) => {\n    const target = m.options?.target;\n    if (!target) return true;\n    const { auth, expose, isRaw, isStream, tags } = target;\n    return (\n      (auth === undefined || auth === endpointOptions.auth) &&\n      (expose === undefined || expose === endpointOptions.expose) &&\n      (isRaw === undefined || isRaw === endpointOptions.isRaw) &&\n      (isStream === undefined || isStream === endpointOptions.isStream) &&\n      (tags === undefined ||\n        tags.some((tag) => endpointOptions.tags.includes(tag)))\n    );\n  });\n\n  return middlewares;\n}\n\nfunction transformHandler(h: Handler): runtime.ApiRoute {\n  const middlewares = calculateMiddlewareChain(\n    h.endpointOptions,\n    h.middlewares\n  );\n\n  if (h.apiRoute.streamingResponse || h.apiRoute.streamingRequest) {\n    return {\n      ...h.apiRoute,\n      // req is the upgrade request.\n      // stream is either a bidirectional stream, in stream or out stream.\n      handler: (\n        req: runtime.Request,\n        stream: runtime.Sink | runtime.Stream | runtime.Socket\n      ) => {\n        setCurrentRequest(req);\n\n        // make readable streams async iterators\n        const streamArg: IterableStream | IterableSocket | Sink =\n          stream instanceof runtime.Stream\n            ? new IterableStream(stream)\n            : stream instanceof runtime.Socket\n              ? new IterableSocket(stream)\n              : new Sink(stream);\n\n        if (middlewares.length === 0) {\n          const payload = req.payload();\n          return toResponse(\n            payload !== null\n              ? h.apiRoute.handler(payload, streamArg)\n              : h.apiRoute.handler(streamArg)\n          );\n        }\n\n        const handler = async () => {\n          // handshake payload\n          const payload = req.payload();\n          return payload !== null\n            ? h.apiRoute.handler(payload, streamArg)\n            : h.apiRoute.handler(streamArg);\n        };\n\n        const mwRequest = new MiddlewareRequest(\n          streamArg,\n          undefined,\n          undefined\n        );\n        return invokeMiddlewareChain(req, mwRequest, middlewares, handler);\n      }\n    };\n  }\n\n  if (h.apiRoute.raw) {\n    return {\n      ...h.apiRoute,\n      handler: (\n        req: runtime.Request,\n        resp: runtime.ResponseWriter,\n        body: runtime.BodyReader\n      ) => {\n        setCurrentRequest(req);\n\n        const rawReq = new RawRequest(req, body);\n        const rawResp = new RawResponse(rawReq, resp);\n\n        if (middlewares.length === 0) {\n          return toResponse(h.apiRoute.handler(rawReq, rawResp));\n        }\n\n        const handler = async () => {\n          return h.apiRoute.handler(rawReq, rawResp);\n        };\n\n        const mwRequest = new MiddlewareRequest(undefined, rawReq, rawResp);\n        return invokeMiddlewareChain(req, mwRequest, middlewares, handler);\n      }\n    };\n  }\n\n  return {\n    ...h.apiRoute,\n    handler: (req: runtime.Request) => {\n      setCurrentRequest(req);\n\n      if (middlewares.length === 0) {\n        const payload = req.payload();\n        return toResponse(\n          payload !== null ? h.apiRoute.handler(payload) : h.apiRoute.handler()\n        );\n      }\n\n      const handler = async () => {\n        const payload = req.payload();\n        return payload !== null\n          ? h.apiRoute.handler(payload)\n          : h.apiRoute.handler();\n      };\n\n      const mwRequest = new MiddlewareRequest(undefined, undefined, undefined);\n      return invokeMiddlewareChain(req, mwRequest, middlewares, handler);\n    }\n  };\n}\n\nfunction toResponse(\n  payload: any\n): InternalHandlerResponse | Promise<InternalHandlerResponse> {\n  if (payload instanceof Promise) {\n    return payload.then((payload) => {\n      return new HandlerResponse(payload).__internalToResponse();\n    });\n  } else {\n    return new HandlerResponse(payload).__internalToResponse();\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/auth/mod.ts",
    "content": "import {getCurrentRequest} from \"../reqtrack/mod\";\n\nexport function getAuthData<T>(): T | null {\n    const authData = getCurrentRequest()?.getAuthData();\n    if (!authData) {\n        return null;\n    } else {\n        return authData as T;\n    }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/codegen/api.ts",
    "content": "export { apiCall, streamIn, streamOut, streamInOut } from \"../api/mod\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/codegen/appinit.ts",
    "content": "export {registerGateways, registerHandlers, registerTestHandler, run, type Handler} from \"../appinit/mod\";      "
  },
  {
    "path": "runtimes/js/encore.dev/internal/codegen/auth.ts",
    "content": "export {getAuthData} from \"../auth/mod\";"
  },
  {
    "path": "runtimes/js/encore.dev/internal/metrics/mod.ts",
    "content": "/**\n * Internal class that handles atomic counter operations on SharedArrayBuffer.\n */\nexport class AtomicCounter {\n  private view: BigUint64Array;\n  private slot: number;\n\n  constructor(buffer: SharedArrayBuffer, slot: number) {\n    this.view = new BigUint64Array(buffer);\n    this.slot = slot;\n  }\n\n  increment(value: number = 1): void {\n    const v = BigInt(Math.floor(value));\n    Atomics.add(this.view, this.slot, v);\n  }\n}\n\n/**\n * Internal class that handles atomic gauge operations on SharedArrayBuffer.\n */\nexport class AtomicGauge {\n  private view: BigUint64Array;\n  private slot: number;\n\n  constructor(buffer: SharedArrayBuffer, slot: number) {\n    this.view = new BigUint64Array(buffer);\n    this.slot = slot;\n  }\n\n  set(value: number): void {\n    // For gauges, store f64 bits as u64\n    const float64 = new Float64Array(1);\n    float64[0] = value;\n    const uint64View = new BigUint64Array(float64.buffer);\n    const v = uint64View[0];\n    Atomics.store(this.view, this.slot, v);\n  }\n}\n\n/**\n * Serialize labels to a consistent string key for map lookups.\n * @internal\n */\nexport function serializeLabels(\n  labels: Record<string, string | number | boolean>\n): string {\n  const sorted = Object.entries(labels).sort(([a], [b]) => a.localeCompare(b));\n  return JSON.stringify(sorted);\n}\n\n/**\n * Process labels into an array of key/value pairs, converting numbers to floored integers.\n * @internal\n */\nexport function processLabelsToPairs(\n  labels: Record<string, string | number | boolean>\n): [string, string][] {\n  return Object.entries(labels).map(([key, value]) => [\n    key,\n    typeof value === \"number\" ? String(Math.floor(value)) : String(value)\n  ]);\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/metrics/registry.ts",
    "content": "import * as runtime from \"../runtime/mod\";\nimport { RT } from \"../runtime/mod\";\nimport log from \"../../log/mod\";\n\nlet globalRegistry: runtime.MetricsRegistry | undefined;\nlet globalBuffer: SharedArrayBuffer | undefined;\nlet initErrorLogged = false;\n\nconst testMode = process.env.NODE_ENV === \"test\";\n\n/**\n * Called during encores initialization\n * @internal\n */\nexport function setGlobalMetricsBuffer(buffer: SharedArrayBuffer): void {\n  globalBuffer = buffer;\n  globalRegistry = RT.getMetricsRegistry();\n}\n\n/**\n * Called during encores initialization, should be called on the main thread\n * @internal\n */\nexport function initGlobalMetricsBuffer(): SharedArrayBuffer {\n  const INITIAL_METRICS_SLOTS = 10_000;\n  const metricsBuffer = new SharedArrayBuffer(INITIAL_METRICS_SLOTS * 8);\n  const view = new BigUint64Array(metricsBuffer);\n  runtime.RT.createMetricsRegistry(view);\n  setGlobalMetricsBuffer(metricsBuffer);\n\n  return metricsBuffer;\n}\n\nexport function getRegistry(): runtime.MetricsRegistry | undefined {\n  if (!globalRegistry) {\n    // In test mode, silently return undefined (no-op)\n    if (testMode) {\n      return undefined;\n    }\n\n    // In non-test mode, log error once and return undefined\n    if (!initErrorLogged) {\n      initErrorLogged = true;\n      log.error(\n        \"Metrics registry not initialized. Metrics will not be collected. \"\n      );\n    }\n    return undefined;\n  }\n  return globalRegistry;\n}\n\nexport function getBuffer(): SharedArrayBuffer | undefined {\n  if (!globalBuffer) {\n    // In test mode, silently return undefined (no-op)\n    if (testMode) {\n      return undefined;\n    }\n\n    // In non-test mode, log error once and return undefined\n    if (!initErrorLogged) {\n      initErrorLogged = true;\n      log.error(\n        \"Metrics buffer not initialized. Metrics will not be collected. \"\n      );\n    }\n    return undefined;\n  }\n  return globalBuffer;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/reqtrack/mod.ts",
    "content": "import * as runtime from \"../runtime/mod\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst asyncLocalStorage = new AsyncLocalStorage();\n\nexport function setCurrentRequest(req: runtime.Request) {\n  asyncLocalStorage.enterWith(req);\n}\n\nexport function getCurrentRequest(): runtime.Request | null {\n  return (asyncLocalStorage.getStore() as runtime.Request) ?? null;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/runtime/.gitignore",
    "content": "/napi\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/runtime/.npmignore",
    "content": "# Intentionally left blank, because we want the napi directory\n# to be included in packages, but we don't want it committed to the repository.\n# If this file does not exist then the npm packaging code will use the .gitignore file instead.\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/runtime/mod.ts",
    "content": "import { Decimal } from \"../../types/mod\";\nimport { Runtime } from \"./napi/napi.cjs\";\n\nexport * from \"./napi/napi.cjs\";\n\nconst testMode = process.env.NODE_ENV === \"test\";\n\nexport const RT = new Runtime({\n  testMode,\n  typeConstructors: {\n    decimal: (val: string | number | bigint) => new Decimal(val)\n  }\n});\n\nexport interface Metric {\n  name: string;\n  services: string[];\n}\n\nexport interface RuntimeConfig {\n  metrics: Record<string, Metric>;\n}\n\nlet cached: RuntimeConfig | null = null;\nexport function runtimeConfig(): RuntimeConfig {\n  if (cached === null) {\n    let cfg = RT.runtimeConfig();\n    cached = {\n      metrics: cfg.metrics\n    };\n  }\n  return cached;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/types/mod.ts",
    "content": "type durationUnit = \"ns\" | \"µs\" | \"ms\" | \"s\" | \"m\" | \"h\";\ntype durationComponent = `${number}${durationUnit}`;\n\n/**\n * A duration is a string representing a length of time.\n */\nexport type DurationString =\n  | durationComponent\n  | `${durationComponent}${durationComponent}`\n  | `${durationComponent} ${durationComponent}`;\n"
  },
  {
    "path": "runtimes/js/encore.dev/internal/utils/constraints.ts",
    "content": "type UnUnion<T, S> = T extends S ? ([S] extends [T] ? T : never) : never;\ntype NotUnion<T> = UnUnion<T, T>;\n\n/**\n * Literal requires that the given type is a literal value, and not a union of literals.\n */\nexport type Literal<Value extends OfType, OfType> = OfType extends Value\n  ? never // But the type must not extend the value (i.e. the two must not be equal)\n  : NotUnion<Value>; // And the type must not be a union of literals\n\n/** Value must be a single string literal */\nexport type StringLiteral<Value extends string> = Literal<Value, string>;\n"
  },
  {
    "path": "runtimes/js/encore.dev/log/mod.ts",
    "content": "import { getCurrentRequest } from \"../internal/reqtrack/mod\";\nimport * as runtime from \"../internal/runtime/mod\";\n\nexport type { Logger };\n\n/* eslint-disable @typescript-eslint/unified-signatures */\n/**\n * A field value we support logging\n */\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-expect-error\nexport type FieldValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | FieldsObject\n  | FieldValue[];\n\n/**\n * A map of fields that can be logged\n */\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-expect-error\nexport type FieldsObject = Record<string, FieldValue>;\n\nexport enum LogLevel {\n  Trace = runtime.LogLevel.Trace,\n  Debug = runtime.LogLevel.Debug,\n  Info = runtime.LogLevel.Info,\n  Warn = runtime.LogLevel.Warn,\n  Error = runtime.LogLevel.Error,\n}\n\nclass Logger {\n  private impl: runtime.Logger;\n\n  constructor(impl: runtime.Logger) {\n    this.impl = impl;\n  }\n\n  /**\n   * Returns a new logger with the specified level.\n   */\n  withLevel(level: LogLevel): Logger {\n    return new Logger(this.impl.withLevel(level));\n  }\n\n  /**\n   * Returns a new logger with the given fields added to the context.\n   */\n  with(fields: FieldsObject): Logger {\n    return new Logger(this.impl.with(fields));\n  }\n\n  /**\n   * Trace logs a message at the trace level.\n   */\n  trace(msg: string, fields?: FieldsObject) {\n    this.log(runtime.LogLevel.Trace, msg, fields);\n  }\n\n  /**\n   * Debug logs a message at the debug level.\n   */\n  debug(msg: string, fields?: FieldsObject) {\n    this.log(runtime.LogLevel.Debug, msg, fields);\n  }\n\n  /**\n   * Info logs a message at the info level.\n   */\n  info(msg: string, fields?: FieldsObject) {\n    this.log(runtime.LogLevel.Info, msg, fields);\n  }\n\n  /**\n   * Warn logs a message at the warn level.\n   */\n  warn(err: Error | unknown, fields?: FieldsObject): void;\n  warn(err: Error | unknown, msg: string, fields?: FieldsObject): void;\n  warn(msg: string, fields?: FieldsObject): void;\n  warn(errOrMsg: unknown, msgOrFields: unknown, fields?: unknown) {\n    this.log(runtime.LogLevel.Warn, errOrMsg, msgOrFields, fields);\n  }\n\n  error(err: Error | unknown, fields?: FieldsObject): void;\n  error(err: Error | unknown, msg: string, fields?: FieldsObject): void;\n  error(msg: string, fields?: FieldsObject): void;\n  error(errOrMsg: unknown, msgOrFields: unknown, fields?: unknown) {\n    this.log(runtime.LogLevel.Error, errOrMsg, msgOrFields, fields);\n  }\n\n  /**\n   * The actual logging implementation.\n   */\n  private log(\n    level: runtime.LogLevel,\n    errOrMsg: unknown,\n    msgOrFields?: unknown,\n    possibleFields?: unknown\n  ) {\n    let err: Error | undefined;\n    let msg: string;\n    let fields: FieldsObject | undefined;\n\n    // Parse the arguments\n    if (typeof errOrMsg === \"string\") {\n      // log(msg, fields?)\n      err = undefined;\n      msg = errOrMsg;\n      fields = msgOrFields as FieldsObject | undefined;\n    } else if (typeof msgOrFields === \"string\") {\n      // log(err, msg, fields?)\n      if (errOrMsg) {\n        if (errOrMsg instanceof Error) {\n          err = errOrMsg;\n        } else {\n          err = new Error(String(errOrMsg));\n        }\n      }\n      msg = msgOrFields;\n      fields = possibleFields as FieldsObject | undefined;\n    } else {\n      // log(err, fields?)\n      if (errOrMsg) {\n        if (errOrMsg instanceof Error) {\n          err = errOrMsg;\n        } else {\n          err = new Error(String(errOrMsg));\n        }\n      }\n\n      msg = \"\";\n      fields = msgOrFields as FieldsObject | undefined;\n\n      // if (possibleFields) {\n      //   throw new Error(\"Invalid arguments to log\");\n      // }\n    }\n\n    const req = getCurrentRequest();\n\n    this.impl.log(req, level, msg, err, undefined, fields);\n  }\n}\n\nconst log = new Logger(runtime.RT.logger());\n\n/**\n * The default logger for the app\n */\nexport default log;\n\n/**\n * Trace logs a message at the trace level\n */\nexport function trace(msg: string, fields?: FieldsObject) {\n  log.trace(msg, fields);\n}\n\n/**\n * Debug logs a message at the debug level\n */\nexport function debug(msg: string, fields?: FieldsObject) {\n  log.debug(msg, fields);\n}\n\n/**\n * Info logs a message at the info level\n */\nexport function info(msg: string, fields?: FieldsObject) {\n  log.info(msg, fields);\n}\n\n/**\n * Warn logs a message at the warn level\n */\nexport function warn(err: Error | unknown, fields?: FieldsObject): void;\nexport function warn(\n  err: Error | unknown,\n  msg: string,\n  fields?: FieldsObject\n): void;\nexport function warn(msg: string, fields?: FieldsObject): void;\nexport function warn(\n  errOrMsg: unknown,\n  msgOrFields: unknown,\n  fields?: unknown\n) {\n  // the type cast here is just for TSC to be happy - the underlying method uses the same overloads so\n  // will type check the arguments correctly\n  log.warn(\n    errOrMsg as Error,\n    msgOrFields as string,\n    fields as FieldsObject | undefined\n  );\n}\n\n/**\n * Error logs a message at the error level\n */\nexport function error(err: Error | unknown, fields?: FieldsObject): void;\nexport function error(\n  err: Error | unknown,\n  msg: string,\n  fields?: FieldsObject\n): void;\nexport function error(msg: string, fields?: FieldsObject): void;\nexport function error(\n  errOrMsg: unknown,\n  msgOrFields: unknown,\n  fields?: unknown\n) {\n  // the type cast here is just for TSC to be happy - the underlying method uses the same overloads so\n  // will type check the arguments correctly\n  log.error(\n    errOrMsg as Error,\n    msgOrFields as string,\n    fields as FieldsObject | undefined\n  );\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/metrics/mod.ts",
    "content": "/**\n * Custom metrics for Encore applications.\n *\n * This module provides counters and gauges that can be statically analyzed\n * by the Encore compiler and automatically exported to observability backends.\n *\n * @example Simple counter\n * ```typescript\n * import { Counter } from 'encore.dev/metrics';\n *\n * export const ordersProcessed = new Counter(\"orders_processed\");\n *\n * ordersProcessed.increment();\n * ```\n *\n * @example Counter with labels\n * ```typescript\n * import { CounterGroup } from 'encore.dev/metrics';\n *\n * interface Labels {\n *   success: boolean;\n * }\n *\n * export const ordersProcessed = new CounterGroup<Labels>(\"orders_processed\");\n *\n * ordersProcessed.with({ success: true }).increment();\n * ```\n */\n\nimport * as runtime from \"../internal/runtime/mod\";\nimport { MetricType } from \"../internal/runtime/mod\";\nimport { getRegistry, getBuffer } from \"../internal/metrics/registry\";\nimport {\n  AtomicCounter,\n  AtomicGauge,\n  processLabelsToPairs,\n  serializeLabels\n} from \"../internal/metrics/mod\";\nimport { currentRequest } from \"../req_meta\";\n\nexport interface MetricConfig {}\n\n/**\n * Resolves the service name for a metric by checking:\n * 1. If there's only one service using this metric in the runtime config\n * 2. Otherwise, looks at the current request context\n */\nfunction resolveServiceName(metricName: string): string | undefined {\n  const rtConfig = runtime.runtimeConfig();\n  const rtSvcs = rtConfig.metrics[metricName]?.services ?? [];\n  if (rtSvcs.length === 1) {\n    return rtSvcs[0];\n  }\n\n  const currReq = currentRequest();\n  if (currReq) {\n    if (currReq.type === \"api-call\") {\n      return currReq.api.service;\n    } else {\n      return currReq.service;\n    }\n  }\n\n  return undefined;\n}\n\n/**\n * A Counter tracks cumulative values that only increase.\n * Use counters for metrics like request counts, errors, etc.\n */\nexport class Counter {\n  private name: string;\n  private cache: Map<string, AtomicCounter>;\n  private labelPairs: [string, string][];\n  private cfg: MetricConfig;\n\n  constructor(name: string, cfg?: MetricConfig) {\n    this.name = name;\n    this.cfg = cfg ?? {};\n    this.labelPairs = [];\n    this.cache = new Map();\n  }\n\n  /**\n   * Increment the counter by the given value (default 1).\n   */\n  increment(value: number = 1): void {\n    const serviceName = resolveServiceName(this.name);\n    if (!serviceName) {\n      return;\n    }\n\n    let metric = this.cache.get(serviceName);\n    if (!metric) {\n      const registry = getRegistry();\n      const buffer = getBuffer();\n\n      // If registry or buffer are not initialized, silently skip\n      if (!registry || !buffer) {\n        return;\n      }\n\n      const slot = registry.allocateSlot(\n        this.name,\n        this.labelPairs,\n        serviceName,\n        MetricType.Counter\n      );\n      metric = new AtomicCounter(buffer, slot);\n      this.cache.set(serviceName, metric);\n    }\n\n    metric.increment(value);\n  }\n\n  ref(): Counter {\n    return this;\n  }\n}\n\n/**\n * A CounterGroup tracks counters with labels.\n * Each unique combination of label values creates a separate counter time series.\n *\n * @typeParam L - The label interface (must have string/number/boolean fields)\n * Note: Number values in labels are converted to integers using Math.floor().\n */\nexport class CounterGroup<\n  L extends Record<keyof L, string | number | boolean>\n> {\n  private name: string;\n  private labelCache: Map<string, Counter>;\n  private cfg: MetricConfig;\n\n  constructor(name: string, cfg?: MetricConfig) {\n    this.name = name;\n    this.labelCache = new Map();\n    this.cfg = cfg ?? {};\n  }\n\n  /**\n   * Get a counter for the given label values.\n   *\n   * Note: Number values in labels are converted to integers using Math.floor().\n   */\n  with(labels: L): Counter {\n    const labelKey = serializeLabels(labels);\n\n    let cached = this.labelCache.get(labelKey);\n    if (!cached) {\n      // Create counter instance\n      cached = new Counter(this.name, this.cfg);\n\n      const labelPairs = processLabelsToPairs(labels);\n      (cached as any).labelPairs = labelPairs;\n\n      this.labelCache.set(labelKey, cached);\n    }\n\n    return cached;\n  }\n\n  ref(): CounterGroup<L> {\n    return this;\n  }\n}\n\n/**\n * A Gauge tracks values that can go up or down.\n * Use gauges for metrics like memory usage, active connections, temperature, etc.\n */\nexport class Gauge {\n  private name: string;\n  private cache: Map<string, AtomicGauge>;\n  private labelPairs: [string, string][];\n  private cfg: MetricConfig;\n\n  constructor(name: string, cfg?: MetricConfig) {\n    this.name = name;\n    this.cfg = cfg ?? {};\n    this.labelPairs = [];\n    this.cache = new Map();\n  }\n\n  /**\n   * Set the gauge to the given value.\n   */\n  set(value: number): void {\n    const serviceName = resolveServiceName(this.name);\n    if (!serviceName) {\n      return;\n    }\n\n    let metric = this.cache.get(serviceName);\n    if (!metric) {\n      const registry = getRegistry();\n      const buffer = getBuffer();\n\n      // If registry or buffer are not initialized, silently skip\n      if (!registry || !buffer) {\n        return;\n      }\n\n      const slot = registry.allocateSlot(\n        this.name,\n        this.labelPairs,\n        serviceName,\n        MetricType.Gauge\n      );\n      metric = new AtomicGauge(buffer, slot);\n      this.cache.set(serviceName, metric);\n    }\n\n    metric.set(value);\n  }\n\n  ref(): Gauge {\n    return this;\n  }\n}\n\nexport class GaugeGroup<L extends Record<keyof L, string | number | boolean>> {\n  private name: string;\n  private labelCache: Map<string, Gauge>;\n  private cfg: MetricConfig;\n\n  constructor(name: string, cfg?: MetricConfig) {\n    this.name = name;\n    this.labelCache = new Map();\n    this.cfg = cfg ?? {};\n  }\n\n  /**\n   * Get a gauge for the given label values.\n   *\n   * Note: Number values in labels are converted to integers using Math.floor().\n   */\n  with(labels: L): Gauge {\n    const labelKey = serializeLabels(labels);\n\n    let cached = this.labelCache.get(labelKey);\n    if (!cached) {\n      // Create gauge instance\n      cached = new Gauge(this.name, this.cfg);\n\n      const labelPairs = processLabelsToPairs(labels);\n      (cached as any).labelPairs = labelPairs;\n\n      this.labelCache.set(labelKey, cached);\n    }\n\n    return cached;\n  }\n\n  ref(): GaugeGroup<L> {\n    return this;\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/mod.ts",
    "content": "export { appMeta } from \"./app_meta\";\nexport type {\n  AppMeta,\n  BuildMeta,\n  CloudProvider,\n  DeployMeta,\n  EnvironmentMeta,\n  EnvironmentType\n} from \"./app_meta\";\n\nexport { currentRequest } from \"./req_meta\";\nexport type {\n  APICallMeta,\n  APIDesc,\n  Method,\n  PubSubMessageMeta,\n  RequestMeta,\n  TraceData\n} from \"./req_meta\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/package.json",
    "content": "{\n  \"name\": \"encore.dev\",\n  \"description\": \"Encore's JavaScript/TypeScript SDK\",\n  \"version\": \"0.0.0-devel.202311141645\",\n  \"license\": \"MPL-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/encoredev/encore/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/encoredev/encore\",\n    \"directory\": \"runtimes/js/encore.dev\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc -b\",\n    \"lint\": \"eslint \\\"**/*.ts*\\\"\",\n    \"test\": \"bun test\"\n  },\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": {\n      \"types\": \"./mod.ts\",\n      \"bun\": \"./mod.ts\",\n      \"default\": \"./dist/mod.js\"\n    },\n    \"./api\": {\n      \"types\": \"./api/mod.ts\",\n      \"bun\": \"./api/mod.ts\",\n      \"default\": \"./dist/api/mod.js\"\n    },\n    \"./auth\": {\n      \"types\": \"./auth/mod.ts\",\n      \"bun\": \"./auth/mod.ts\",\n      \"default\": \"./dist/auth/mod.js\"\n    },\n    \"./config\": {\n      \"types\": \"./config/mod.ts\",\n      \"bun\": \"./config/mod.ts\",\n      \"default\": \"./dist/config/mod.js\"\n    },\n    \"./cron\": {\n      \"types\": \"./cron/mod.ts\",\n      \"bun\": \"./cron/mod.ts\",\n      \"default\": \"./dist/cron/mod.js\"\n    },\n    \"./log\": {\n      \"types\": \"./log/mod.ts\",\n      \"bun\": \"./log/mod.ts\",\n      \"default\": \"./dist/log/mod.js\"\n    },\n    \"./pubsub\": {\n      \"types\": \"./pubsub/mod.ts\",\n      \"bun\": \"./pubsub/mod.ts\",\n      \"default\": \"./dist/pubsub/mod.js\"\n    },\n    \"./service\": {\n      \"types\": \"./service/mod.ts\",\n      \"bun\": \"./service/mod.ts\",\n      \"default\": \"./dist/service/mod.js\"\n    },\n    \"./storage/sqldb\": {\n      \"types\": \"./storage/sqldb/mod.ts\",\n      \"bun\": \"./storage/sqldb/mod.ts\",\n      \"default\": \"./dist/storage/sqldb/mod.js\"\n    },\n    \"./storage/objects\": {\n      \"types\": \"./storage/objects/mod.ts\",\n      \"bun\": \"./storage/objects/mod.ts\",\n      \"default\": \"./dist/storage/objects/mod.js\"\n    },\n    \"./storage/cache\": {\n      \"types\": \"./storage/cache/mod.ts\",\n      \"bun\": \"./storage/cache/mod.ts\",\n      \"default\": \"./dist/storage/cache/mod.js\"\n    },\n    \"./validate\": {\n      \"types\": \"./validate/mod.ts\",\n      \"bun\": \"./validate/mod.ts\",\n      \"default\": \"./dist/validate/mod.js\"\n    },\n    \"./types\": {\n      \"types\": \"./types/mod.ts\",\n      \"bun\": \"./types/mod.ts\",\n      \"default\": \"./dist/types/mod.js\"\n    },\n    \"./metrics\": {\n      \"types\": \"./metrics/mod.ts\",\n      \"bun\": \"./metrics/mod.ts\",\n      \"default\": \"./dist/metrics/mod.js\"\n    },\n    \"./internal/codegen/*\": {\n      \"types\": \"./internal/codegen/*.ts\",\n      \"bun\": \"./internal/codegen/*.ts\",\n      \"default\": \"./dist/internal/codegen/*.js\"\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  },\n  \"files\": [\n    \"dist/\",\n    \"*.ts\",\n    \"**/*.ts\",\n    \"*.cjs\",\n    \"*.cts\",\n    \"README.md\",\n    \"LICENSE\"\n  ],\n  \"devDependencies\": {\n    \"@types/node\": \"^20.11.19\",\n    \"tsc-esm-fix\": \"^2.20.26\",\n    \"typescript\": \"^5.3.3\"\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/pubsub/mod.ts",
    "content": "export { Topic } from \"./topic\";\nexport type { TopicConfig, DeliveryGuarantee } from \"./topic\";\n\nexport { Subscription } from \"./subscription\";\nexport type { SubscriptionConfig } from \"./subscription\";\n\nexport type { TopicPerms, Publisher } from \"./refs\";\n\n/**\n * Attribute represents a field on a message that should be sent\n * as an attribute in a PubSub message, rather than in the message\n * body.\n *\n * This is useful for ordering messages, or for filtering messages\n * on a subscription - otherwise you should not use this.\n *\n * To create attributes on a message, use the `Attribute` type:\n *   type Message = {\n *     user_id: Attribute<number>;\n *     name: string;\n *   };\n *\n *   const msg: Message = {\n *     user_id: 123,\n *     name:    \"John Doe\",\n *   };\n *\n * The union of brandedAttribute is simply used to help the TypeScript compiler\n * understand that the type is an attribute and allow the AttributesOf type\n * to extract the keys of said type.\n */\nexport type Attribute<T extends string | number | boolean> =\n  | T\n  | brandedAttribute<T>;\n\n/**\n * AttributesOf is a helper type to extract all keys from an object\n * who's type is an Attribute type.\n *\n * For example:\n *    type Message = {\n *        user_id: Attribute<number>;\n *        name: string;\n *        age: Attribute<number>;\n *    };\n *\n *    type MessageAttributes = AttributesOf<Message>; // \"user_id\" | \"age\"\n */\nexport type AttributesOf<T extends object> = keyof {\n  [Key in keyof // for (const Key in T)\n  T as Extract<T[Key], allBrandedTypes> extends never //  if (typeof T[Key] !== oneof(allBrandedTypes))\n    ? never // drop the key\n    : Key]: never; // else keep the key\n};\n\n/**\n * supportedAttributeTypes is a union of all primitive types that are supported as attributes\n */\ntype supportedAttributeTypes = string | number | boolean;\n\n/**\n * brandedAttribute is a helper type to brand a type as an attribute\n * which is distinct from the base type. It is a compile time only\n * type and has no runtime representation.\n */\ntype brandedAttribute<T> = T & { readonly __attributeBrand: unique symbol };\n\n/**\n * allBrandedTypes is a helper type to create a union of all branded supported attribute types\n *\n * The result of this is: brandedAttribute<string> | brandedAttribute<number> | brandedAttribute<boolean>\n */\ntype allBrandedTypes<Union = supportedAttributeTypes> =\n  Union extends supportedAttributeTypes ? brandedAttribute<Union> : never;\n"
  },
  {
    "path": "runtimes/js/encore.dev/pubsub/refs.ts",
    "content": "export abstract class TopicPerms {\n  private topicPerms(): void {}\n}\n\nexport abstract class Publisher<Msg extends object> extends TopicPerms {\n  abstract publish(msg: Msg): Promise<string>;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/pubsub/subscription.ts",
    "content": "import { setCurrentRequest } from \"../internal/reqtrack/mod\";\nimport { DurationString } from \"../internal/types/mod\";\nimport { Topic } from \"./topic\";\nimport * as runtime from \"../internal/runtime/mod\";\n\nexport class Subscription<Msg extends object> {\n  private readonly topic: Topic<Msg>;\n  private readonly name: string;\n  private readonly impl: runtime.PubSubSubscription;\n\n  constructor(topic: Topic<Msg>, name: string, cfg: SubscriptionConfig<Msg>) {\n    this.topic = topic;\n    this.name = name;\n\n    const handler = (msg: runtime.Request) => {\n      setCurrentRequest(msg);\n      return cfg.handler(msg.payload() as Msg);\n    };\n\n    this.impl = runtime.RT.pubsubSubscription({\n      topicName: topic.name,\n      subscriptionName: name,\n      handler,\n    });\n\n    this.startSubscribing();\n  }\n\n  private startSubscribing() {\n    const that = this;\n    this.impl.subscribe().finally(() => {\n      setTimeout(() => that.startSubscribing(), 1000);\n    });\n  }\n}\n\n/**\n * SubscriptionConfig is used when creating a subscription\n *\n * The values given here may be clamped to the supported values by\n * the target cloud. (i.e. ack deadline may be brought within the supported range\n * by the target cloud pubsub implementation).\n */\nexport interface SubscriptionConfig<Msg> {\n  /**\n   * Handler is the function which will be called to process a message\n   * sent on the topic.\n   *\n   * When this function returns an error the message will be\n   * negatively acknowledged (nacked), which will cause a redelivery\n   * attempt to be made (unless the retry policy's MaxRetries has been reached).\n   */\n  handler: (msg: Msg) => Promise<unknown>;\n\n  /**\n   * MaxConcurrency is the maximum number of messages which will be processed\n   * simultaneously per instance of the service for this subscription.\n   *\n   * Note that this is per instance of the service, so if your service has\n   * scaled to 10 instances and this is set to 10, then 100 messages could be\n   * processed simultaneously.\n   *\n   * If the value is negative, then there will be no limit on the number\n   * of messages processed simultaneously.\n   *\n   * Note: This is not supported by all cloud providers; specifically on GCP\n   * when using Cloud Run instances on an unordered topic the subscription will\n   * be configured as a Push Subscription and will have an adaptive concurrency\n   * See [GCP Push Delivery Rate](https://cloud.google.com/pubsub/docs/push#push_delivery_rate).\n   *\n   * This setting also has no effect on Encore Cloud environments.\n   * If not set, it uses a reasonable default based on the cloud provider.\n   */\n  maxConcurrency?: number;\n\n  /**\n   * AckDeadline is the time a consumer has to process a message\n   * before it's returned to the subscription\n   *\n   * Default is 30 seconds, however the ack deadline must be at least\n   * 1 second.\n   */\n  ackDeadline?: DurationString;\n\n  /**\n   * MessageRetention is how long an undelivered message is kept\n   * on the topic before it's purged.\n   *\n   * Default is 7 days.\n   */\n  messageRetention?: DurationString;\n\n  /**\n   * RetryPolicy defines how a message should be retried when\n   * the subscriber returns an error\n   */\n  retryPolicy?: RetryPolicy;\n}\n\n/**\n * RetryPolicy defines how a subscription should handle retries\n * after errors either delivering the message or processing the message.\n *\n * The values given to this structure are parsed at compile time, such that\n * the correct Cloud resources can be provisioned to support the queue.\n *\n * As such the values given here may be clamped to the supported values by\n * the target cloud. (i.e. min/max values brought within the supported range\n * by the target cloud).\n */\nexport interface RetryPolicy {\n  /**\n   * The minimum time to wait between retries. Defaults to 10 seconds.\n   */\n  minBackoff?: DurationString;\n\n  /**\n   * The maximum time to wait between retries. Defaults to 10 minutes.\n   */\n  maxBackoff?: DurationString;\n\n  /**\n   * MaxRetries is used to control deadletter queuing logic, when:\n   *   n == 0: A default value of 100 retries will be used\n   *   n > 0:  Encore will forward a message to a dead letter queue after n retries\n   *   n == pubsub.InfiniteRetries: Messages will not be forwarded to the dead letter queue by the Encore framework\n   */\n  maxRetries?: number;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/pubsub/topic.ts",
    "content": "import { getCurrentRequest } from \"../internal/reqtrack/mod\";\nimport type { AttributesOf } from \"./mod\";\nimport * as runtime from \"../internal/runtime/mod\";\nimport { Publisher, TopicPerms } from \"./refs\";\n\n/**\n * A topic is a resource to which you can publish messages\n * to be delivered to subscribers of that topic.\n */\nexport class Topic<Msg extends object>\n  extends TopicPerms\n  implements Publisher<Msg>\n{\n  public readonly name: string;\n  public readonly cfg: TopicConfig<Msg>;\n  private impl: runtime.PubSubTopic;\n\n  constructor(name: string, cfg: TopicConfig<Msg>) {\n    super();\n    this.name = name;\n    this.cfg = cfg;\n    this.impl = runtime.RT.pubsubTopic(name);\n  }\n\n  public async publish(msg: Msg): Promise<string> {\n    const source = getCurrentRequest();\n    return this.impl.publish(msg, source);\n  }\n\n  public ref<P extends TopicPerms>(): P {\n    return this as unknown as P;\n  }\n}\n\n/**\n * DeliveryGuarantee is used to configure the delivery contract for a topic.\n */\nexport type DeliveryGuarantee = \"at-least-once\" | \"exactly-once\";\n\n/**\n * At Least Once delivery guarantees that a message for a subscription is delivered to\n * a consumer at least once.\n *\n * On AWS and GCP there is no limit to the throughput for a topic.\n */\nexport const atLeastOnce: DeliveryGuarantee = \"at-least-once\";\n\n/**\n * ExactlyOnce guarantees that a message for a subscription is delivered to\n * a consumer exactly once, to the best of the system's ability.\n *\n * However, there are edge cases when a message might be redelivered.\n * For example, if a networking issue causes the acknowledgement of success\n * processing the message to be lost before the cloud provider receives it.\n *\n * It is also important to note that the ExactlyOnce delivery guarantee only\n * applies to the delivery of the message to the consumer, and not to the\n * original publishing of the message, such that if a message is published twice,\n * such as due to an retry within the application logic, it will be delivered twice.\n * (i.e. ExactlyOnce delivery does not imply message deduplication on publish)\n *\n * As such it's recommended that the subscription handler function is idempotent\n * and is able to handle duplicate messages.\n *\n * Subscriptions attached to ExactlyOnce topics have higher message delivery latency compared to AtLeastOnce.\n *\n * By using ExactlyOnce semantics on a topic, the throughput will be limited depending on the cloud provider:\n * - AWS: 300 messages per second for the topic (see [AWS SQS Quotas]).\n * - GCP: At least 3,000 messages per second across all topics in the region\n *      (can be higher on the region see [GCP PubSub Quotas]).\n *\n * [AWS SQS Quotas]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html\n * [GCP PubSub Quotas]: https://cloud.google.com/pubsub/quotas#quotas\n */\nexport const exactlyOnce: DeliveryGuarantee = \"exactly-once\";\n\n/**\n * TopicConfig is used when creating a Topic\n */\nexport interface TopicConfig<Msg extends object> {\n  /**\n   * DeliveryGuarantee is used to configure the delivery guarantee of a Topic\n   */\n  deliveryGuarantee: DeliveryGuarantee;\n\n  /**\n   * OrderingAttribute is the message attribute to use as a ordering key for\n   * messages and delivery will ensure that messages with the same value will\n   * be delivered in the order they where published.\n   *\n   * If OrderingAttribute is not set, messages can be delivered in any order.\n   *\n   * It is important to note, that in the case of an error being returned by a\n   * subscription handler, the message will be retried before any subsequent\n   * messages for that ordering key are delivered. This means depending on the\n   * retry configuration, a large backlog of messages for a given ordering key\n   * may build up. When using OrderingAttribute, it is recommended to use reason\n   * about your failure modes and set the retry configuration appropriately.\n   *\n   * Once the maximum number of retries has been reached, the message will be\n   * forwarded to the dead letter queue, and the next message for that ordering\n   * key will be delivered.\n   *\n   * To create attributes on a message, use the `Attribute` type:\n   *\n   *  type UserEvent = {\n   *    user_id: Attribute<string>;\n   *    action:  string;\n   *  }\n   *\n   *  const topic = new Topic<UserEvent>(\"user-events\", {\n   *    deliveryGuarantee: DeliveryGuarantee.AtLeastOnce,\n   *    orderingAttribute: \"user_id\", // Messages with the same user-id will be delivered in the order they where\n   * published\n   *  })\n   *\n   *  topic.publish(ctx, {user_id: \"1\", action: \"login\"})  // This message will be delivered before the logout\n   *  topic.publish(ctx, {user_id: \"2\", action: \"login\"})  // This could be delivered at any time because it has a different user id\n   *  topic.publish(ctx, {user_id: \"1\", action: \"logout\"}) // This message will be delivered after the first message\n   *\n   * By using OrderingAttribute, the throughput will be limited depending on the cloud provider:\n   *\n   * - AWS: 300 messages per second for the topic (see [AWS SQS Quotas]).\n   * - GCP: 1MB/s for each ordering key (see [GCP PubSub Quotas]).\n   *\n   * Note: OrderingAttribute currently has no effect during local development.\n   *\n   * [AWS SQS Quotas]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html\n   * [GCP PubSub Quotas]: https://cloud.google.com/pubsub/quotas#resource_limits\n   */\n  orderingAttribute?: AttributesOf<Msg>;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/req_meta.ts",
    "content": "import { getCurrentRequest } from \"./internal/reqtrack/mod\";\n\n/** Describes an API endpoint. */\nexport interface APIDesc {\n  /** The name of the service that the endpoint belongs to. */\n  service: string;\n\n  /** The name of the endpoint itself. */\n  endpoint: string;\n\n  /** Whether the endpoint is a raw endpoint. */\n  raw: boolean;\n\n  /** Whether the endpoint requires auth. */\n  auth: boolean;\n\n  /** Tags specified on the endpoint. */\n  tags: string[];\n}\n\nexport type Method =\n  | \"GET\"\n  | \"POST\"\n  | \"PUT\"\n  | \"PATCH\"\n  | \"DELETE\"\n  | \"HEAD\"\n  | \"OPTIONS\"\n  | \"CONNECT\"\n  | \"TRACE\";\n\n/** Describes an API call being processed. */\nexport interface APICallMeta {\n  /** Specifies that the request is an API call. */\n  type: \"api-call\";\n\n  /** Describes the API Endpoint being called. */\n  api: APIDesc;\n\n  /** The HTTP method used in the API call. */\n  method: Method;\n\n  /**\n   * The request URL path used in the API call,\n   * excluding any query string parameters.\n   * For example \"/path/to/endpoint\".\n   */\n  path: string;\n\n  /**\n   * The request URL path used in the API call,\n   * including any query string parameters.\n   * For example \"/path/to/endpoint?with=querystring\".\n   */\n  pathAndQuery: string;\n\n  /**\n   * The parsed path parameters for the API endpoint.\n   * The keys are the names of the path parameters,\n   * from the API definition.\n   *\n   * For example {id: 5}.\n   */\n  pathParams: Record<string, any>;\n\n  /**\n   * The request headers from the HTTP request.\n   * The values are arrays if the header contains multiple values,\n   * either separated by \";\" or when the header key appears more than once.\n   */\n  headers: Record<string, string | string[]>;\n\n  /**\n   * The parsed request payload, as expected by the application code.\n   * Not provided for raw endpoints or when the API endpoint expects no\n   * request data.\n   */\n  parsedPayload?: Record<string, any>;\n\n  /**\n   * Contains values set in middlewares via `MiddlewareRequest.data`.\n   */\n  middlewareData?: Record<string, any>;\n}\n\n/** Describes a Pub/Sub message being processed. */\nexport interface PubSubMessageMeta {\n  /** Specifies that the request is a Pub/Sub message. */\n  type: \"pubsub-message\";\n\n  /** The service processing the message. */\n  service: string;\n\n  /** The name of the Pub/Sub topic. */\n  topic: string;\n\n  /** The name of the Pub/Sub subscription. */\n  subscription: string;\n\n  /**\n   * The unique id of the Pub/Sub message.\n   * It is the same id returned by `topic.publish()`.\n   * The message id stays the same across delivery attempts.\n   */\n  messageId: string;\n\n  /**\n   * The delivery attempt. The first attempt starts at 1,\n   * and increases by 1 for each retry.\n   */\n  deliveryAttempt: number;\n\n  /**\n   * The parsed request payload, as expected by the application code.\n   */\n  parsedPayload?: Record<string, any>;\n}\n\n/** Provides information about the active trace. */\nexport interface TraceData {\n  /** The trace id. */\n  traceId: string;\n  /** The current span id. */\n  spanId: string;\n\n  /**\n   * The trace id that initiated this trace, if any.\n   */\n  parentTraceId?: string;\n\n  /**\n   * The span that initiated this span, if any.\n   */\n  parentSpanId?: string;\n\n  /**\n   * The external correlation id provided when the trace\n   * was created, if any.\n   * For example via the `Request-Id` or `X-Correlation-Id` headers.\n   */\n  extCorrelationId?: string;\n}\n\ninterface BaseRequestMeta {\n  /** Information about the trace, if the request is being traced */\n  trace?: TraceData;\n}\n\n/** Describes an API call or Pub/Sub message being processed. */\nexport type RequestMeta = (APICallMeta | PubSubMessageMeta) & BaseRequestMeta;\n\n/**\n * Returns information about the running Encore request,\n * such as API calls and Pub/Sub messages being processed.\n *\n * Returns undefined only if no request is being processed,\n * such as during system initialization.\n */\nexport function currentRequest(): RequestMeta | undefined {\n  const req = getCurrentRequest();\n  if (!req) {\n    return undefined;\n  }\n  const meta = req.meta();\n\n  const base: BaseRequestMeta = {\n    trace: meta.trace\n  };\n\n  if (meta.apiCall) {\n    const api: APICallMeta = {\n      type: \"api-call\",\n      api: {\n        service: meta.apiCall.api.service,\n        endpoint: meta.apiCall.api.endpoint,\n        raw: meta.apiCall.api.raw,\n        auth: meta.apiCall.api.requiresAuth,\n        tags: meta.apiCall.api.tags\n      },\n      method: meta.apiCall.method as Method,\n      path: meta.apiCall.path,\n      pathAndQuery: meta.apiCall.pathAndQuery,\n      pathParams: meta.apiCall.pathParams ?? {},\n      parsedPayload: meta.apiCall.parsedPayload,\n      headers: meta.apiCall.headers,\n      middlewareData: (req as any).middlewareData\n    };\n    return { ...base, ...api };\n  } else if (meta.pubsubMessage) {\n    const msg: PubSubMessageMeta = {\n      type: \"pubsub-message\",\n      service: meta.pubsubMessage.service,\n      topic: meta.pubsubMessage.topic,\n      subscription: meta.pubsubMessage.subscription,\n      messageId: meta.pubsubMessage.id,\n      deliveryAttempt: meta.pubsubMessage.deliveryAttempt,\n      parsedPayload: meta.pubsubMessage.parsedPayload\n    };\n    return { ...base, ...msg };\n  } else {\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/service/mod.ts",
    "content": "import { Middleware } from \"../api/mod\";\n\n/**\n * Defines an Encore backend service.\n *\n * Use this class to define a new backend service with the given name.\n * The scope of the service is its containing directory, and all subdirectories.\n *\n * It must be called from files named `encore.service.ts`, to enable Encore to\n * efficiently identify possible service definitions.\n */\nexport class Service {\n  public readonly name: string;\n  public readonly cfg: ServiceConfig;\n\n  constructor(name: string, cfg?: ServiceConfig) {\n    this.name = name;\n    this.cfg = cfg ?? {};\n  }\n}\n\nexport interface ServiceConfig {\n  middlewares?: Middleware[];\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/basic.ts",
    "content": "import { getCurrentRequest } from \"../../internal/reqtrack/mod\";\nimport { CacheCluster } from \"./cluster\";\nimport { CacheMiss, CacheKeyExists } from \"./errors\";\nimport { Keyspace, KeyspaceConfig, WriteOptions } from \"./keyspace\";\n\n/**\n * Base class for basic (scalar value) keyspaces.\n * Provides get/set/replace/etc operations.\n * @internal\n */\nabstract class BasicKeyspace<K, V> extends Keyspace<K> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  /**\n   * Serializes a value to a Buffer for storage.\n   */\n  protected abstract serialize(value: V): Buffer;\n\n  /**\n   * Deserializes a Buffer from storage to a value.\n   */\n  protected abstract deserialize(data: Buffer): V;\n\n  /**\n   * Gets the value stored at key.\n   * If the key does not exist, it returns `undefined`.\n   *\n   * @returns The value, or `undefined` if the key does not exist.\n   * @see https://redis.io/commands/get/\n   */\n  async get(key: K): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.get(mappedKey, source);\n\n    if (result === null) {\n      return undefined;\n    }\n\n    return this.deserialize(result);\n  }\n\n  /**\n   * Gets the values stored at multiple keys.\n   *\n   * @returns An array of values in the same order as the provided keys.\n   * Each element is the value or `undefined` if the key was not found.\n   * @see https://redis.io/commands/mget/\n   */\n  async multiGet(...keys: K[]): Promise<(V | undefined)[]> {\n    const source = getCurrentRequest();\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const results = await this.cluster.impl.mget(mappedKeys, source);\n    return results.map((r) =>\n      r === null || r === undefined ? undefined : this.deserialize(r)\n    );\n  }\n\n  /**\n   * Updates the value stored at key to val.\n   *\n   * @see https://redis.io/commands/set/\n   */\n  async set(key: K, value: V, options?: WriteOptions): Promise<void> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = this.serialize(value);\n    const ttlMs = this.resolveTtl(options);\n\n    await this.cluster.impl.set(mappedKey, serialized, ttlMs, source);\n  }\n\n  /**\n   * Sets the value stored at key to val, but only if the key does not exist beforehand.\n   *\n   * @throws {CacheKeyExists} If the key already exists.\n   * @see https://redis.io/commands/setnx/\n   */\n  async setIfNotExists(\n    key: K,\n    value: V,\n    options?: WriteOptions\n  ): Promise<void> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = this.serialize(value);\n    const ttlMs = this.resolveTtl(options);\n\n    const set = await this.cluster.impl.setIfNotExists(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n\n    if (!set) {\n      throw new CacheKeyExists(mappedKey);\n    }\n  }\n\n  /**\n   * Replaces the existing value stored at key to val.\n   *\n   * @throws {CacheMiss} If the key does not already exist.\n   * @see https://redis.io/commands/set/\n   */\n  async replace(key: K, value: V, options?: WriteOptions): Promise<void> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = this.serialize(value);\n    const ttlMs = this.resolveTtl(options);\n\n    const replaced = await this.cluster.impl.replace(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n\n    if (!replaced) {\n      throw new CacheMiss(mappedKey);\n    }\n  }\n\n  /**\n   * Updates the value of key to val and returns the previously stored value.\n   * If the key does not already exist, it sets it and returns `undefined`.\n   *\n   * @returns The previous value, or `undefined` if the key did not exist.\n   * @see https://redis.io/commands/getset/\n   */\n  async getAndSet(\n    key: K,\n    value: V,\n    options?: WriteOptions\n  ): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = this.serialize(value);\n    const ttlMs = this.resolveTtl(options);\n\n    const oldValue = await this.cluster.impl.getAndSet(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n\n    if (oldValue === null) {\n      return undefined;\n    }\n\n    return this.deserialize(oldValue);\n  }\n\n  /**\n   * Deletes the key and returns the previously stored value.\n   * If the key does not already exist, it returns `undefined`.\n   *\n   * @returns The previous value, or `undefined` if the key did not exist.\n   * @see https://redis.io/commands/getdel/\n   */\n  async getAndDelete(key: K): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n\n    const value = await this.cluster.impl.getAndDelete(mappedKey, source);\n\n    if (value === null) {\n      return undefined;\n    }\n\n    return this.deserialize(value);\n  }\n}\n\n/**\n * StringKeyspace stores string values.\n *\n * @example\n * ```ts\n * const tokens = new StringKeyspace<string>(cluster, {\n *   keyPattern: \"token/:id\",\n *   defaultExpiry: ExpireIn(3600000), // 1 hour\n * });\n *\n * await tokens.set(\"abc123\", \"user-token-value\");\n * const token = await tokens.get(\"abc123\");\n * ```\n */\nexport class StringKeyspace<K> extends BasicKeyspace<K, string> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serialize(value: string): Buffer {\n    return Buffer.from(value, \"utf-8\");\n  }\n\n  protected deserialize(data: Buffer): string {\n    return data.toString(\"utf-8\");\n  }\n\n  /**\n   * Appends a string to the value stored at key.\n   *\n   * If the key does not exist it is first created and set as the empty string,\n   * causing append to behave like set.\n   *\n   * @returns The new string length.\n   * @see https://redis.io/commands/append/\n   */\n  async append(key: K, value: string, options?: WriteOptions): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.append(\n      mappedKey,\n      Buffer.from(value, \"utf-8\"),\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Returns a substring of the string value stored at key.\n   *\n   * The `start` and `end` values are zero-based indices, but unlike typical slicing\n   * the `end` value is inclusive.\n   *\n   * Negative values can be used in order to provide an offset starting\n   * from the end of the string, so -1 means the last character.\n   *\n   * If the string does not exist it returns the empty string.\n   *\n   * @param key - The cache key.\n   * @param start - Start index (inclusive, 0-based).\n   * @param end - End index (inclusive, 0-based). Use -1 for end of string.\n   * @returns The substring.\n   * @see https://redis.io/commands/getrange/\n   */\n  async getRange(key: K, start: number, end: number): Promise<string> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.getRange(\n      mappedKey,\n      start,\n      end,\n      source\n    );\n    return result.toString(\"utf-8\");\n  }\n\n  /**\n   * Overwrites part of the string stored at key, starting at\n   * the zero-based `offset` and for the entire length of `value`, extending\n   * the string if necessary.\n   *\n   * If the offset is larger than the current string length stored at key,\n   * the string is first padded with zero-bytes to make offset fit.\n   *\n   * Non-existing keys are considered as empty strings.\n   *\n   * @param key - The cache key.\n   * @param offset - Zero-based byte offset to start writing at.\n   * @param value - The string to write.\n   * @returns The length of the string after the operation.\n   * @see https://redis.io/commands/setrange/\n   */\n  async setRange(\n    key: K,\n    offset: number,\n    value: string,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.setRange(\n      mappedKey,\n      offset,\n      Buffer.from(value, \"utf-8\"),\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Returns the length of the string value stored at key.\n   *\n   * Non-existing keys are considered as empty strings.\n   *\n   * @returns The string length.\n   * @see https://redis.io/commands/strlen/\n   */\n  async len(key: K): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.strlen(mappedKey, source);\n    return Number(result);\n  }\n}\n\n/**\n * IntKeyspace stores 64-bit integer values.\n * Values are floored to integers using `Math.floor`.\n * For fractional values, use {@link FloatKeyspace} instead.\n *\n * @example\n * ```ts\n * const counters = new IntKeyspace<string>(cluster, {\n *   keyPattern: \"counter/:name\",\n * });\n *\n * await counters.set(\"page-views\", 0);\n * const newCount = await counters.increment(\"page-views\", 1);\n * ```\n */\nexport class IntKeyspace<K> extends BasicKeyspace<K, number> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serialize(value: number): Buffer {\n    return Buffer.from(String(Math.floor(value)), \"utf-8\");\n  }\n\n  protected deserialize(data: Buffer): number {\n    return parseInt(data.toString(\"utf-8\"), 10);\n  }\n\n  /**\n   * Increments the number stored at key by `delta`.\n   *\n   * If the key does not exist it is first created with a value of 0\n   * before incrementing.\n   *\n   * Negative values can be used to decrease the value,\n   * but typically you want to use {@link decrement} for that.\n   *\n   * @param key - The cache key.\n   * @param delta - The amount to increment by (default 1).\n   * @returns The new value after incrementing.\n   * @see https://redis.io/commands/incrby/\n   */\n  async increment(\n    key: K,\n    delta: number = 1,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    return await this.cluster.impl.incrBy(\n      mappedKey,\n      Math.floor(delta),\n      ttlMs,\n      source\n    );\n  }\n\n  /**\n   * Decrements the number stored at key by `delta`.\n   *\n   * If the key does not exist it is first created with a value of 0\n   * before decrementing.\n   *\n   * Negative values can be used to increase the value,\n   * but typically you want to use {@link increment} for that.\n   *\n   * @param key - The cache key.\n   * @param delta - The amount to decrement by (default 1).\n   * @returns The new value after decrementing.\n   * @see https://redis.io/commands/decrby/\n   */\n  async decrement(\n    key: K,\n    delta: number = 1,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    return await this.cluster.impl.decrBy(\n      mappedKey,\n      Math.floor(delta),\n      ttlMs,\n      source\n    );\n  }\n}\n\n/**\n * FloatKeyspace stores 64-bit floating point values.\n *\n * @example\n * ```ts\n * const scores = new FloatKeyspace<string>(cluster, {\n *   keyPattern: \"score/:playerId\",\n * });\n *\n * await scores.set(\"player1\", 100.5);\n * const newScore = await scores.increment(\"player1\", 10.25);\n * ```\n */\nexport class FloatKeyspace<K> extends BasicKeyspace<K, number> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serialize(value: number): Buffer {\n    return Buffer.from(String(value), \"utf-8\");\n  }\n\n  protected deserialize(data: Buffer): number {\n    return parseFloat(data.toString(\"utf-8\"));\n  }\n\n  /**\n   * Increments the number stored at key by `delta`.\n   *\n   * If the key does not exist it is first created with a value of 0\n   * before incrementing.\n   *\n   * Negative values can be used to decrease the value,\n   * but typically you want to use {@link decrement} for that.\n   *\n   * @param key - The cache key.\n   * @param delta - The amount to increment by (default 1).\n   * @returns The new value after incrementing.\n   * @see https://redis.io/commands/incrbyfloat/\n   */\n  async increment(\n    key: K,\n    delta: number = 1,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    return await this.cluster.impl.incrByFloat(mappedKey, delta, ttlMs, source);\n  }\n\n  /**\n   * Decrements the number stored at key by `delta`.\n   *\n   * If the key does not exist it is first created with a value of 0\n   * before decrementing.\n   *\n   * Negative values can be used to increase the value,\n   * but typically you want to use {@link increment} for that.\n   *\n   * @param key - The cache key.\n   * @param delta - The amount to decrement by (default 1).\n   * @returns The new value after decrementing.\n   * @see https://redis.io/commands/incrbyfloat/\n   */\n  async decrement(\n    key: K,\n    delta: number = 1,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    return await this.cluster.impl.incrByFloat(\n      mappedKey,\n      -delta,\n      ttlMs,\n      source\n    );\n  }\n}\n\n/**\n * StructKeyspace stores arbitrary objects serialized as JSON.\n *\n * @example\n * ```ts\n * interface User {\n *   id: string;\n *   name: string;\n *   email: string;\n * }\n *\n * const users = new StructKeyspace<string, User>(cluster, {\n *   keyPattern: \"user/:id\",\n *   defaultExpiry: ExpireIn(3600000),\n * });\n *\n * await users.set(\"user1\", { id: \"user1\", name: \"Alice\", email: \"alice@example.com\" });\n * const user = await users.get(\"user1\");\n * ```\n */\nexport class StructKeyspace<K, V> extends BasicKeyspace<K, V> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serialize(value: V): Buffer {\n    return Buffer.from(JSON.stringify(value), \"utf-8\");\n  }\n\n  protected deserialize(data: Buffer): V {\n    return JSON.parse(data.toString(\"utf-8\")) as V;\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/cluster.ts",
    "content": "import * as runtime from \"../../internal/runtime/mod\";\nimport { StringLiteral } from \"../../internal/utils/constraints\";\n\n/**\n * Redis eviction policy that determines how keys are evicted when memory is full.\n */\nexport type EvictionPolicy =\n  | \"noeviction\"\n  | \"allkeys-lru\"\n  | \"allkeys-lfu\"\n  | \"allkeys-random\"\n  | \"volatile-lru\"\n  | \"volatile-lfu\"\n  | \"volatile-ttl\"\n  | \"volatile-random\";\n\n/**\n * Configuration options for a cache cluster.\n */\nexport interface CacheClusterConfig {\n  /**\n   * The eviction policy to use when the cache is full.\n   * Defaults to \"allkeys-lru\".\n   */\n  evictionPolicy?: EvictionPolicy;\n}\n\n/**\n * CacheCluster represents a Redis cache cluster.\n *\n * Create a new cluster using `new CacheCluster(name)`.\n * Reference an existing cluster using `CacheCluster.named(name)`.\n *\n * @example\n * ```ts\n * import { CacheCluster } from \"encore.dev/storage/cache\";\n *\n * const myCache = new CacheCluster(\"my-cache\", {\n *   evictionPolicy: \"allkeys-lru\",\n * });\n * ```\n */\nexport class CacheCluster {\n  /** @internal */\n  readonly impl: runtime.CacheCluster;\n  /** @internal */\n  readonly clusterName: string;\n\n  /**\n   * Creates a new cache cluster with the given name and configuration.\n   * @param name - The unique name for this cache cluster\n   * @param cfg - Optional configuration for the cluster\n   */\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  constructor(name: string, cfg?: CacheClusterConfig) {\n    this.clusterName = name;\n    this.impl = runtime.RT.cacheCluster(name);\n  }\n\n  /**\n   * Reference an existing cache cluster by name.\n   * To create a new cache cluster, use `new CacheCluster(...)` instead.\n   */\n  static named<Name extends string>(name: StringLiteral<Name>): CacheCluster {\n    return new CacheCluster(name);\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/errors.ts",
    "content": "/**\n * CacheError is the base class for all cache-related errors.\n */\nexport class CacheError extends Error {\n  constructor(msg: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(msg);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"CacheError\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, CacheError.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\n/**\n * CacheMiss is thrown when a cache key is not found.\n */\nexport class CacheMiss extends CacheError {\n  constructor(key: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(`cache key \"${key}\" not found`);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"CacheMiss\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, CacheMiss.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\n/**\n * CacheKeyExists is thrown when attempting to set a key that already exists\n * using setIfNotExists.\n */\nexport class CacheKeyExists extends CacheError {\n  constructor(key: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(`cache key \"${key}\" already exists`);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"CacheKeyExists\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, CacheKeyExists.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/expiry.ts",
    "content": "/**\n * Expiry represents a cache key expiration configuration.\n * Use the helper functions to create expiry configurations.\n */\nexport type Expiry =\n  | { type: \"duration\"; durationMs: number }\n  | { type: \"time\"; hours: number; minutes: number; seconds: number }\n  | \"never\"\n  | \"keep-ttl\";\n\n/**\n * expireIn sets the cache entry to expire after the specified duration.\n * @param ms - Duration in milliseconds\n */\nexport function expireIn(ms: number): Expiry {\n  return { type: \"duration\", durationMs: ms };\n}\n\n/**\n * expireInSeconds sets the cache entry to expire after the specified seconds.\n * @param seconds - Duration in seconds\n */\nexport function expireInSeconds(seconds: number): Expiry {\n  return { type: \"duration\", durationMs: seconds * 1000 };\n}\n\n/**\n * expireInMinutes sets the cache entry to expire after the specified minutes.\n * @param minutes - Duration in minutes\n */\nexport function expireInMinutes(minutes: number): Expiry {\n  return { type: \"duration\", durationMs: minutes * 60 * 1000 };\n}\n\n/**\n * expireInHours sets the cache entry to expire after the specified hours.\n * @param hours - Duration in hours\n */\nexport function expireInHours(hours: number): Expiry {\n  return { type: \"duration\", durationMs: hours * 60 * 60 * 1000 };\n}\n\n/**\n * expireDailyAt sets the cache entry to expire at a specific time each day (UTC).\n * @param hours - Hour (0-23)\n * @param minutes - Minutes (0-59)\n * @param seconds - Seconds (0-59)\n */\nexport function expireDailyAt(\n  hours: number,\n  minutes: number,\n  seconds: number\n): Expiry {\n  return { type: \"time\", hours, minutes, seconds };\n}\n\n/**\n * neverExpire sets the cache entry to never expire.\n * Note: Redis may still evict the key based on the eviction policy.\n */\nexport const neverExpire: Expiry = \"never\";\n\n/**\n * keepTTL preserves the existing TTL when updating a cache entry.\n * If the key doesn't exist, no TTL is set.\n */\nexport const keepTTL: Expiry = \"keep-ttl\";\n\n/**\n * Resolves an Expiry to a duration in milliseconds, \"never\", or \"keep-ttl\".\n * @internal\n */\nexport function resolveExpiry(expiry: Expiry): number | \"never\" | \"keep-ttl\" {\n  switch (expiry) {\n    case \"never\":\n      return \"never\";\n    case \"keep-ttl\":\n      return \"keep-ttl\";\n  }\n\n  switch (expiry.type) {\n    case \"duration\":\n      return expiry.durationMs;\n\n    case \"time\": {\n      const now = new Date();\n      const target = new Date(now);\n      target.setUTCHours(expiry.hours, expiry.minutes, expiry.seconds, 0);\n\n      // If target time has passed today, set for tomorrow\n      if (target.getTime() <= now.getTime()) {\n        target.setUTCDate(target.getUTCDate() + 1);\n      }\n\n      return target.getTime() - now.getTime();\n    }\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/keyspace.ts",
    "content": "import { getCurrentRequest } from \"../../internal/reqtrack/mod\";\nimport { CacheCluster } from \"./cluster\";\nimport { Expiry, keepTTL, neverExpire, resolveExpiry } from \"./expiry\";\n\n/**\n * Configuration for a cache keyspace.\n */\nexport interface KeyspaceConfig<K> {\n  /**\n   * The pattern for generating cache keys.\n   * Use `:fieldName` to include a field from the key type.\n   *\n   * @example\n   * // For a simple key type (string, number)\n   * keyPattern: \"user/:id\"\n   *\n   * // For a struct key type\n   * keyPattern: \"user/:userId/region/:region\"\n   */\n  keyPattern: string;\n\n  /**\n   * Default expiry for cache entries in this keyspace.\n   * If not set, entries do not expire.\n   */\n  defaultExpiry?: Expiry;\n}\n\n/**\n * Options for write operations.\n */\nexport interface WriteOptions {\n  /**\n   * Expiry for this specific write operation.\n   * Overrides the keyspace's defaultExpiry.\n   */\n  expiry?: Expiry;\n}\n\n/**\n * Base class for all keyspace types (basic, list, set).\n * Provides key mapping, TTL resolution, with(), and delete().\n * @internal\n */\nexport abstract class Keyspace<K> {\n  protected readonly cluster: CacheCluster;\n  protected readonly config: KeyspaceConfig<K>;\n  protected readonly keyMapper: (key: K) => string;\n  private _effectiveExpiry?: Expiry;\n\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    this.cluster = cluster;\n    this.config = config;\n    this.keyMapper = this.createKeyMapper(config.keyPattern);\n  }\n\n  /**\n   * Creates a key mapper by parsing the key pattern.\n   */\n  private createKeyMapper(pattern: string): (key: K) => string {\n    const segments = pattern.split(\"/\").map((seg) => {\n      if (seg.startsWith(\":\")) {\n        return { isLiteral: false, value: seg.slice(1), field: seg.slice(1) };\n      }\n      return { isLiteral: true, value: seg };\n    });\n\n    return (key: K) => {\n      return segments\n        .map((seg) => {\n          if (seg.isLiteral) return seg.value;\n\n          let val: unknown;\n          if (typeof key === \"object\" && key !== null && seg.field) {\n            val = (key as Record<string, unknown>)[seg.field];\n          } else {\n            val = key;\n          }\n\n          // Escape forward slashes in string values\n          const str = String(val);\n          return str.replace(/\\//g, \"\\\\/\");\n        })\n        .join(\"/\");\n    };\n  }\n\n  /**\n   * Maps a key to its Redis key string.\n   */\n  protected mapKey(key: K): string {\n    const mapped = this.keyMapper(key);\n    if (mapped.startsWith(\"__encore\")) {\n      throw new Error('use of reserved key prefix \"__encore\"');\n    }\n    return mapped;\n  }\n\n  /**\n   * Resolves the TTL for a write operation.\n   * Returns i64 sentinel for NAPI: undefined=no config, -1=KeepTTL, -2=Persist/NeverExpire, >=0=ms\n   */\n  protected resolveTtl(options?: WriteOptions): number | undefined {\n    const expiry =\n      options?.expiry ?? this._effectiveExpiry ?? this.config.defaultExpiry;\n    if (!expiry) return undefined;\n\n    const resolved = resolveExpiry(expiry);\n    if (resolved === \"keep-ttl\") return -1; // KeepTTL\n    if (resolved === \"never\") return -2; // NeverExpire → Persist\n    return resolved; // milliseconds\n  }\n\n  /**\n   * Returns a shallow clone of this keyspace with the specified write options applied.\n   * This allows setting expiry for a chain of operations.\n   *\n   * @example\n   * ```ts\n   * await myKeyspace.with({ expiry: expireIn(5000) }).set(key, value);\n   * ```\n   */\n  with(options: WriteOptions): this {\n    const clone = Object.create(Object.getPrototypeOf(this)) as this;\n    Object.assign(clone, this);\n    (clone as any)._effectiveExpiry = options.expiry ?? this._effectiveExpiry;\n    return clone;\n  }\n\n  /**\n   * Deletes the specified keys.\n   * If a key does not exist it is ignored.\n   *\n   * @returns The number of keys that were deleted.\n   * @see https://redis.io/commands/del/\n   */\n  async delete(...keys: K[]): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    return await this.cluster.impl.delete(mappedKeys, source);\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/list.ts",
    "content": "import { getCurrentRequest } from \"../../internal/reqtrack/mod\";\nimport { CacheCluster } from \"./cluster\";\nimport { Keyspace, KeyspaceConfig, WriteOptions } from \"./keyspace\";\n\n/**\n * Position in a list (left/head or right/tail).\n */\nexport type ListPosition = \"left\" | \"right\";\n\n/**\n * Base class for list keyspaces with all list operations.\n * Subclasses provide typed serialization/deserialization.\n * @internal\n */\nabstract class ListKeyspace<K, V> extends Keyspace<K> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected abstract serializeItem(value: V): Buffer;\n  protected abstract deserializeItem(data: Buffer): V;\n\n  /**\n   * Pushes one or more values at the head of the list stored at key.\n   * If the key does not already exist, it is first created as an empty list.\n   *\n   * If multiple values are given, they are inserted one after another,\n   * starting with the leftmost value. For instance,\n   * `pushLeft(key, \"a\", \"b\", \"c\")` will result in a list containing\n   * \"c\" as its first element, \"b\" as its second, and \"a\" as its third.\n   *\n   * @returns The length of the list after the operation.\n   * @see https://redis.io/commands/lpush/\n   */\n  async pushLeft(key: K, ...values: V[]): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = values.map((v) => this.serializeItem(v));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.lpush(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Pushes one or more values at the tail of the list stored at key.\n   * If the key does not already exist, it is first created as an empty list.\n   *\n   * If multiple values are given, they are inserted one after another,\n   * starting with the leftmost value. For instance,\n   * `pushRight(key, \"a\", \"b\", \"c\")` will result in a list containing\n   * \"a\" as its first element, \"b\" as its second, and \"c\" as its third.\n   *\n   * @returns The length of the list after the operation.\n   * @see https://redis.io/commands/rpush/\n   */\n  async pushRight(key: K, ...values: V[]): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = values.map((v) => this.serializeItem(v));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.rpush(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Pops a single element off the head of the list stored at key.\n   *\n   * @returns The popped value, or `undefined` if the key does not exist.\n   * @see https://redis.io/commands/lpop/\n   */\n  async popLeft(key: K, options?: WriteOptions): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.lpop(mappedKey, ttlMs, source);\n    if (result === null) {\n      return undefined;\n    }\n    return this.deserializeItem(result);\n  }\n\n  /**\n   * Pops a single element off the tail of the list stored at key.\n   *\n   * @returns The popped value, or `undefined` if the key does not exist.\n   * @see https://redis.io/commands/rpop/\n   */\n  async popRight(key: K, options?: WriteOptions): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.rpop(mappedKey, ttlMs, source);\n    if (result === null) {\n      return undefined;\n    }\n    return this.deserializeItem(result);\n  }\n\n  /**\n   * Returns the length of the list stored at key.\n   *\n   * Non-existing keys are considered as empty lists.\n   *\n   * @returns The list length.\n   * @see https://redis.io/commands/llen/\n   */\n  async len(key: K): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.llen(mappedKey, source);\n    return Number(result);\n  }\n\n  /**\n   * Trims the list stored at key to only contain the elements between the indices\n   * `start` and `stop` (inclusive). Both are zero-based indices.\n   *\n   * Negative indices can be used to indicate offsets from the end of the list,\n   * where -1 is the last element of the list, -2 the penultimate element, and so on.\n   *\n   * Out of range indices are valid and are treated as if they specify the start or end of the list,\n   * respectively. If `start` > `stop` the end result is an empty list.\n   *\n   * @param key - The cache key.\n   * @param start - Start index (inclusive).\n   * @param stop - Stop index (inclusive).\n   * @see https://redis.io/commands/ltrim/\n   */\n  async trim(\n    key: K,\n    start: number,\n    stop: number,\n    options?: WriteOptions\n  ): Promise<void> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    await this.cluster.impl.ltrim(mappedKey, start, stop, ttlMs, source);\n  }\n\n  /**\n   * Updates the list element at the given index.\n   *\n   * Negative indices can be used to indicate offsets from the end of the list,\n   * where -1 is the last element of the list, -2 the penultimate element, and so on.\n   *\n   * @param key - The cache key.\n   * @param index - Zero-based index of the element to update.\n   * @param value - The new value.\n   * @throws {Error} If the index is out of range.\n   * @see https://redis.io/commands/lset/\n   */\n  async set(\n    key: K,\n    index: number,\n    value: V,\n    options?: WriteOptions\n  ): Promise<void> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = this.serializeItem(value);\n    const ttlMs = this.resolveTtl(options);\n    await this.cluster.impl.lset(mappedKey, index, serialized, ttlMs, source);\n  }\n\n  /**\n   * Returns the value of the list element at the given index.\n   *\n   * Negative indices can be used to indicate offsets from the end of the list,\n   * where -1 is the last element of the list, -2 the penultimate element, and so on.\n   *\n   * @param key - The cache key.\n   * @param index - Zero-based index of the element to retrieve.\n   * @returns The value at the index, or `undefined` if out of range or the key does not exist.\n   * @see https://redis.io/commands/lindex/\n   */\n  async get(key: K, index: number): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.lindex(mappedKey, index, source);\n\n    if (result === null) {\n      return undefined;\n    }\n\n    return this.deserializeItem(result);\n  }\n\n  /**\n   * Returns all the elements in the list stored at key.\n   *\n   * If the key does not exist it returns an empty array.\n   *\n   * @returns All elements in the list.\n   * @see https://redis.io/commands/lrange/\n   */\n  async items(key: K): Promise<V[]> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const results = await this.cluster.impl.lrangeAll(mappedKey, source);\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Returns the elements in the list stored at key between `start` and `stop` (inclusive).\n   * Both are zero-based indices.\n   *\n   * Negative indices can be used to indicate offsets from the end of the list,\n   * where -1 is the last element of the list, -2 the penultimate element, and so on.\n   *\n   * If the key does not exist it returns an empty array.\n   *\n   * @param key - The cache key.\n   * @param start - Start index (inclusive).\n   * @param stop - Stop index (inclusive).\n   * @returns The elements in the specified range.\n   * @see https://redis.io/commands/lrange/\n   */\n  async getRange(key: K, start: number, stop: number): Promise<V[]> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const results = await this.cluster.impl.lrange(\n      mappedKey,\n      start,\n      stop,\n      source\n    );\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Inserts `value` into the list stored at key, at the position just before `pivot`.\n   *\n   * If the list does not contain `pivot`, the value is not inserted and -1 is returned.\n   *\n   * @param key - The cache key.\n   * @param pivot - The existing element to insert before.\n   * @param value - The value to insert.\n   * @returns The new list length, or -1 if `pivot` was not found.\n   * @see https://redis.io/commands/linsert/\n   */\n  async insertBefore(\n    key: K,\n    pivot: V,\n    value: V,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const pivotSerialized = this.serializeItem(pivot);\n    const valueSerialized = this.serializeItem(value);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.linsertBefore(\n      mappedKey,\n      pivotSerialized,\n      valueSerialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Inserts `value` into the list stored at key, at the position just after `pivot`.\n   *\n   * If the list does not contain `pivot`, the value is not inserted and -1 is returned.\n   *\n   * @param key - The cache key.\n   * @param pivot - The existing element to insert after.\n   * @param value - The value to insert.\n   * @returns The new list length, or -1 if `pivot` was not found.\n   * @see https://redis.io/commands/linsert/\n   */\n  async insertAfter(\n    key: K,\n    pivot: V,\n    value: V,\n    options?: WriteOptions\n  ): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const pivotSerialized = this.serializeItem(pivot);\n    const valueSerialized = this.serializeItem(value);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.linsertAfter(\n      mappedKey,\n      pivotSerialized,\n      valueSerialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Removes all occurrences of `value` in the list stored at key.\n   *\n   * If the list does not contain `value`, or the list does not exist, returns 0.\n   *\n   * @param key - The cache key.\n   * @param value - The value to remove.\n   * @returns The number of elements removed.\n   * @see https://redis.io/commands/lrem/\n   */\n  async removeAll(key: K, value: V, options?: WriteOptions): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const valueSerialized = this.serializeItem(value);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.lremAll(\n      mappedKey,\n      valueSerialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Removes the first `count` occurrences of `value` in the list stored at key,\n   * scanning from head to tail.\n   *\n   * If the list does not contain `value`, or the list does not exist, returns 0.\n   *\n   * @param key - The cache key.\n   * @param count - Maximum number of occurrences to remove.\n   * @param value - The value to remove.\n   * @returns The number of elements removed.\n   * @see https://redis.io/commands/lrem/\n   */\n  async removeFirst(\n    key: K,\n    count: number,\n    value: V,\n    options?: WriteOptions\n  ): Promise<number> {\n    if (count < 0) {\n      throw new Error(\"count must be non-negative\");\n    }\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const valueSerialized = this.serializeItem(value);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.lremFirst(\n      mappedKey,\n      count,\n      valueSerialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Removes the last `count` occurrences of `value` in the list stored at key,\n   * scanning from tail to head.\n   *\n   * If the list does not contain `value`, or the list does not exist, returns 0.\n   *\n   * @param key - The cache key.\n   * @param count - Maximum number of occurrences to remove.\n   * @param value - The value to remove.\n   * @returns The number of elements removed.\n   * @see https://redis.io/commands/lrem/\n   */\n  async removeLast(\n    key: K,\n    count: number,\n    value: V,\n    options?: WriteOptions\n  ): Promise<number> {\n    if (count < 0) {\n      throw new Error(\"count must be non-negative\");\n    }\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const valueSerialized = this.serializeItem(value);\n    const ttlMs = this.resolveTtl(options);\n    // Negative count means remove from tail to head\n    const result = await this.cluster.impl.lremLast(\n      mappedKey,\n      count,\n      valueSerialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Atomically moves an element from the list stored at `src` to the list stored at `dst`.\n   *\n   * The value moved can be either the head (`srcPos === \"left\"`) or tail (`srcPos === \"right\"`)\n   * of the list at `src`. Similarly, the value can be placed either at the head (`dstPos === \"left\"`)\n   * or tail (`dstPos === \"right\"`) of the list at `dst`.\n   *\n   * If `src` and `dst` are the same list, the value is atomically rotated from one end to the other\n   * when `srcPos !== dstPos`, or if `srcPos === dstPos` nothing happens.\n   *\n   * @param src - Source list key.\n   * @param dst - Destination list key.\n   * @param srcPos - Position to pop from in the source list.\n   * @param dstPos - Position to push to in the destination list.\n   * @returns The moved element, or `undefined` if the source list does not exist.\n   * @see https://redis.io/commands/lmove/\n   */\n  async move(\n    src: K,\n    dst: K,\n    srcPos: ListPosition,\n    dstPos: ListPosition,\n    options?: WriteOptions\n  ): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const srcKey = this.mapKey(src);\n    const dstKey = this.mapKey(dst);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.lmove(\n      srcKey,\n      dstKey,\n      srcPos,\n      dstPos,\n      ttlMs,\n      source\n    );\n    if (result === null || result === undefined) {\n      return undefined;\n    }\n    return this.deserializeItem(result);\n  }\n}\n\n/**\n * StringListKeyspace stores lists of string values.\n *\n * @example\n * ```ts\n * const recentViews = new StringListKeyspace<string>(cluster, {\n *   keyPattern: \"recent-views/:userId\",\n *   defaultExpiry: ExpireIn(86400000), // 24 hours\n * });\n *\n * await recentViews.pushLeft(\"user1\", \"product-123\", \"product-456\");\n * const views = await recentViews.items(\"user1\");\n * ```\n */\nexport class StringListKeyspace<K> extends ListKeyspace<K, string> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serializeItem(value: string): Buffer {\n    return Buffer.from(value, \"utf-8\");\n  }\n\n  protected deserializeItem(data: Buffer): string {\n    return data.toString(\"utf-8\");\n  }\n}\n\n/**\n * NumberListKeyspace stores lists of numeric values.\n *\n * @example\n * ```ts\n * const scores = new NumberListKeyspace<string>(cluster, {\n *   keyPattern: \"scores/:gameId\",\n * });\n *\n * await scores.pushRight(\"game1\", 100, 200, 300);\n * const allScores = await scores.items(\"game1\");\n * ```\n */\nexport class NumberListKeyspace<K> extends ListKeyspace<K, number> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serializeItem(value: number): Buffer {\n    return Buffer.from(String(value), \"utf-8\");\n  }\n\n  protected deserializeItem(data: Buffer): number {\n    return Number(data.toString(\"utf-8\"));\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/mod.ts",
    "content": "// Cache cluster\nexport { CacheCluster } from \"./cluster\";\nexport type { CacheClusterConfig, EvictionPolicy } from \"./cluster\";\n\n// Keyspace configuration\nexport type { KeyspaceConfig, WriteOptions } from \"./keyspace\";\n\n// Basic keyspaces\nexport {\n  StringKeyspace,\n  IntKeyspace,\n  FloatKeyspace,\n  StructKeyspace\n} from \"./basic\";\n\n// List keyspaces\nexport { StringListKeyspace, NumberListKeyspace } from \"./list\";\nexport type { ListPosition } from \"./list\";\n\n// Set keyspaces\nexport { StringSetKeyspace, NumberSetKeyspace } from \"./set\";\n\n// Expiry utilities\nexport {\n  expireIn,\n  expireInSeconds,\n  expireInMinutes,\n  expireInHours,\n  expireDailyAt,\n  neverExpire,\n  keepTTL\n} from \"./expiry\";\nexport type { Expiry } from \"./expiry\";\n\n// Error types\nexport { CacheError, CacheMiss, CacheKeyExists } from \"./errors\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/cache/set.ts",
    "content": "import { getCurrentRequest } from \"../../internal/reqtrack/mod\";\nimport { CacheCluster } from \"./cluster\";\nimport { Keyspace, KeyspaceConfig, WriteOptions } from \"./keyspace\";\n\n/**\n * Base class for set keyspaces with all set operations.\n * Subclasses provide typed serialization/deserialization.\n * @internal\n */\nabstract class SetKeyspace<K, V> extends Keyspace<K> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected abstract serializeItem(value: V): Buffer;\n  protected abstract deserializeItem(data: Buffer): V;\n\n  /**\n   * Adds one or more values to the set stored at key.\n   * If the key does not already exist, it is first created as an empty set.\n   *\n   * @returns The number of values that were added to the set,\n   * not including values already present beforehand.\n   * @see https://redis.io/commands/sadd/\n   */\n  async add(key: K, ...members: V[]): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = members.map((m) => this.serializeItem(m));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.sadd(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Removes one or more values from the set stored at key.\n   * Values not present in the set are ignored.\n   * If the key does not already exist, it is a no-op.\n   *\n   * @returns The number of values that were removed from the set.\n   * @see https://redis.io/commands/srem/\n   */\n  async remove(key: K, ...members: V[]): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = members.map((m) => this.serializeItem(m));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.srem(\n      mappedKey,\n      serialized,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Removes a random element from the set stored at key and returns it.\n   *\n   * @returns The removed member, or `undefined` if the set is empty.\n   * @see https://redis.io/commands/spop/\n   */\n  async popOne(key: K, options?: WriteOptions): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    const result = await this.cluster.impl.spop(mappedKey, ttlMs, source);\n    if (result === null || result === undefined) {\n      return undefined;\n    }\n    return this.deserializeItem(result);\n  }\n\n  /**\n   * Removes up to `count` random elements (bounded by the set's size)\n   * from the set stored at key and returns them.\n   *\n   * If the set is empty it returns an empty array.\n   *\n   * @param key - The cache key.\n   * @param count - Number of members to pop.\n   * @returns The removed members (may be fewer than `count` if the set is small).\n   * @see https://redis.io/commands/spop/\n   */\n  async pop(key: K, count: number, options?: WriteOptions): Promise<V[]> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const ttlMs = this.resolveTtl(options);\n    const results = await this.cluster.impl.spopN(\n      mappedKey,\n      count,\n      ttlMs,\n      source\n    );\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Reports whether the set stored at key contains the given value.\n   *\n   * If the key does not exist it returns `false`.\n   *\n   * @returns `true` if the member exists in the set, `false` otherwise.\n   * @see https://redis.io/commands/sismember/\n   */\n  async contains(key: K, member: V): Promise<boolean> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const serialized = this.serializeItem(member);\n    return await this.cluster.impl.sismember(mappedKey, serialized, source);\n  }\n\n  /**\n   * Returns the number of elements in the set stored at key.\n   *\n   * If the key does not exist it returns 0.\n   *\n   * @returns The set cardinality.\n   * @see https://redis.io/commands/scard/\n   */\n  async len(key: K): Promise<number> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.scard(mappedKey, source);\n    return Number(result);\n  }\n\n  /**\n   * Returns the elements in the set stored at key.\n   *\n   * If the key does not exist it returns an empty array.\n   *\n   * @returns All members of the set.\n   * @see https://redis.io/commands/smembers/\n   */\n  async items(key: K): Promise<V[]> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const results = await this.cluster.impl.smembers(mappedKey, source);\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Identical to {@link items} except it returns the values as a `Set`.\n   *\n   * If the key does not exist it returns an empty `Set`.\n   *\n   * @returns All members of the set as a `Set`.\n   * @see https://redis.io/commands/smembers/\n   */\n  async itemsSet(key: K): Promise<Set<V>> {\n    const members = await this.items(key);\n    return new Set(members);\n  }\n\n  /**\n   * Computes the set difference between the first set and all the consecutive sets.\n   *\n   * Set difference means the values present in the first set that are not present\n   * in any of the other sets.\n   *\n   * Keys that do not exist are considered as empty sets.\n   *\n   * @param keys - Keys of sets to compute difference for. At least one must be provided.\n   * @returns Members in the first set but not in any of the other sets.\n   * @throws {Error} If no keys are provided.\n   * @see https://redis.io/commands/sdiff/\n   */\n  async diff(...keys: K[]): Promise<V[]> {\n    if (keys.length === 0) {\n      throw new Error(\"at least one key must be provided\");\n    }\n    const source = getCurrentRequest();\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const results = await this.cluster.impl.sdiff(mappedKeys, source);\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Identical to {@link diff} except it returns the values as a `Set`.\n   *\n   * @see https://redis.io/commands/sdiff/\n   */\n  async diffSet(...keys: K[]): Promise<Set<V>> {\n    const items = await this.diff(...keys);\n    return new Set(items);\n  }\n\n  /**\n   * Computes the set difference between keys (like {@link diff}) and stores the result\n   * in `destination`.\n   *\n   * @param destination - Key to store the result.\n   * @param keys - Keys of sets to compute difference for.\n   * @returns The size of the resulting set.\n   * @see https://redis.io/commands/sdiffstore/\n   */\n  async diffStore(destination: K, ...keys: K[]): Promise<number> {\n    if (keys.length === 0) {\n      throw new Error(\"at least one key must be provided\");\n    }\n    const source = getCurrentRequest();\n    const destKey = this.mapKey(destination);\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.sdiffstore(\n      destKey,\n      mappedKeys,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Computes the set intersection between the sets stored at the given keys.\n   *\n   * Set intersection means the values common to all the provided sets.\n   *\n   * Keys that do not exist are considered to be empty sets.\n   * As a result, if any key is missing the final result is the empty set.\n   *\n   * @param keys - Keys of sets to compute intersection for. At least one must be provided.\n   * @returns Members common to all sets.\n   * @throws {Error} If no keys are provided.\n   * @see https://redis.io/commands/sinter/\n   */\n  async intersect(...keys: K[]): Promise<V[]> {\n    if (keys.length === 0) {\n      throw new Error(\"at least one key must be provided\");\n    }\n    const source = getCurrentRequest();\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const results = await this.cluster.impl.sinter(mappedKeys, source);\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Identical to {@link intersect} except it returns the values as a `Set`.\n   *\n   * @see https://redis.io/commands/sinter/\n   */\n  async intersectSet(...keys: K[]): Promise<Set<V>> {\n    const items = await this.intersect(...keys);\n    return new Set(items);\n  }\n\n  /**\n   * Computes the set intersection between keys (like {@link intersect}) and stores the result\n   * in `destination`.\n   *\n   * @param destination - Key to store the result.\n   * @param keys - Keys of sets to compute intersection for.\n   * @returns The size of the resulting set.\n   * @see https://redis.io/commands/sinterstore/\n   */\n  async intersectStore(destination: K, ...keys: K[]): Promise<number> {\n    if (keys.length === 0) {\n      throw new Error(\"at least one key must be provided\");\n    }\n    const source = getCurrentRequest();\n    const destKey = this.mapKey(destination);\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.sinterstore(\n      destKey,\n      mappedKeys,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Computes the set union between the sets stored at the given keys.\n   *\n   * Set union means the values present in at least one of the provided sets.\n   *\n   * Keys that do not exist are considered to be empty sets.\n   *\n   * @param keys - Keys of sets to compute union for. At least one must be provided.\n   * @returns Members in any of the provided sets.\n   * @throws {Error} If no keys are provided.\n   * @see https://redis.io/commands/sunion/\n   */\n  async union(...keys: K[]): Promise<V[]> {\n    if (keys.length === 0) {\n      throw new Error(\"at least one key must be provided\");\n    }\n    const source = getCurrentRequest();\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const results = await this.cluster.impl.sunion(mappedKeys, source);\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Identical to {@link union} except it returns the values as a `Set`.\n   *\n   * @see https://redis.io/commands/sunion/\n   */\n  async unionSet(...keys: K[]): Promise<Set<V>> {\n    const items = await this.union(...keys);\n    return new Set(items);\n  }\n\n  /**\n   * Computes the set union between sets (like {@link union}) and stores the result\n   * in `destination`.\n   *\n   * @param destination - Key to store the result.\n   * @param keys - Keys of sets to compute union for.\n   * @returns The size of the resulting set.\n   * @see https://redis.io/commands/sunionstore/\n   */\n  async unionStore(destination: K, ...keys: K[]): Promise<number> {\n    if (keys.length === 0) {\n      throw new Error(\"at least one key must be provided\");\n    }\n    const source = getCurrentRequest();\n    const destKey = this.mapKey(destination);\n    const mappedKeys = keys.map((k) => this.mapKey(k));\n    const ttlMs = this.resolveTtl();\n    const result = await this.cluster.impl.sunionstore(\n      destKey,\n      mappedKeys,\n      ttlMs,\n      source\n    );\n    return Number(result);\n  }\n\n  /**\n   * Returns a random member from the set stored at key without removing it.\n   *\n   * @returns A random member, or `undefined` if the key does not exist.\n   * @see https://redis.io/commands/srandmember/\n   */\n  async sampleOne(key: K): Promise<V | undefined> {\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const result = await this.cluster.impl.srandmember(mappedKey, source);\n    if (result === null || result === undefined) {\n      return undefined;\n    }\n    return this.deserializeItem(result);\n  }\n\n  /**\n   * Returns up to `count` distinct random elements from the set stored at key.\n   * The same element is never returned multiple times.\n   *\n   * If the key does not exist it returns an empty array.\n   *\n   * @param key - The cache key.\n   * @param count - Number of distinct members to return.\n   * @returns Random members (may be fewer than `count` if the set is small).\n   * @see https://redis.io/commands/srandmember/\n   */\n  async sample(key: K, count: number): Promise<V[]> {\n    if (count < 0) {\n      throw new Error(\"count must be non-negative\");\n    }\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    const results = await this.cluster.impl.srandmemberN(\n      mappedKey,\n      count,\n      source\n    );\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Returns `count` random elements from the set stored at key.\n   * The same element may be returned multiple times.\n   *\n   * If the key does not exist it returns an empty array.\n   *\n   * @param key - The cache key.\n   * @param count - Number of members to return (may include duplicates).\n   * @returns Random members, possibly with duplicates.\n   * @see https://redis.io/commands/srandmember/\n   */\n  async sampleWithReplacement(key: K, count: number): Promise<V[]> {\n    if (count < 0) {\n      throw new Error(\"count must be non-negative\");\n    }\n    const source = getCurrentRequest();\n    const mappedKey = this.mapKey(key);\n    // Negative count in Redis SRANDMEMBER allows duplicates\n    const results = await this.cluster.impl.srandmemberN(\n      mappedKey,\n      -count,\n      source\n    );\n    return results.map((r) => this.deserializeItem(r));\n  }\n\n  /**\n   * Atomically moves the given member from the set stored at `src`\n   * to the set stored at `dst`.\n   *\n   * If the element already exists in `dst` it is still removed from `src`.\n   *\n   * @param src - Source set key.\n   * @param dst - Destination set key.\n   * @param member - The member to move.\n   * @returns `true` if the member was moved, `false` if not found in `src`.\n   * @see https://redis.io/commands/smove/\n   */\n  async move(\n    src: K,\n    dst: K,\n    member: V,\n    options?: WriteOptions\n  ): Promise<boolean> {\n    const source = getCurrentRequest();\n    const srcKey = this.mapKey(src);\n    const dstKey = this.mapKey(dst);\n    const serialized = this.serializeItem(member);\n    const ttlMs = this.resolveTtl(options);\n    return await this.cluster.impl.smove(\n      srcKey,\n      dstKey,\n      serialized,\n      ttlMs,\n      source\n    );\n  }\n}\n\n/**\n * StringSetKeyspace stores sets of unique string values.\n *\n * @example\n * ```ts\n * const tags = new StringSetKeyspace<string>(cluster, {\n *   keyPattern: \"tags/:articleId\",\n * });\n *\n * await tags.add(\"article1\", \"typescript\", \"programming\", \"web\");\n * const hasTech = await tags.contains(\"article1\", \"typescript\");\n * const allTags = await tags.items(\"article1\");\n * const tagSet = await tags.itemsSet(\"article1\");\n * ```\n */\nexport class StringSetKeyspace<K> extends SetKeyspace<K, string> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serializeItem(value: string): Buffer {\n    return Buffer.from(value, \"utf-8\");\n  }\n\n  protected deserializeItem(data: Buffer): string {\n    return data.toString(\"utf-8\");\n  }\n}\n\n/**\n * NumberSetKeyspace stores sets of unique numeric values.\n *\n * @example\n * ```ts\n * const scores = new NumberSetKeyspace<string>(cluster, {\n *   keyPattern: \"unique-scores/:gameId\",\n * });\n *\n * await scores.add(\"game1\", 100, 200, 300);\n * const hasScore = await scores.contains(\"game1\", 100);\n * ```\n */\nexport class NumberSetKeyspace<K> extends SetKeyspace<K, number> {\n  constructor(cluster: CacheCluster, config: KeyspaceConfig<K>) {\n    super(cluster, config);\n  }\n\n  protected serializeItem(value: number): Buffer {\n    return Buffer.from(String(value), \"utf-8\");\n  }\n\n  protected deserializeItem(data: Buffer): number {\n    return Number(data.toString(\"utf-8\"));\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/objects/bucket.ts",
    "content": "import { getCurrentRequest } from \"../../internal/reqtrack/mod\";\nimport * as runtime from \"../../internal/runtime/mod\";\nimport { StringLiteral } from \"../../internal/utils/constraints\";\nimport { unwrapErr } from \"./error\";\nimport { BucketPerms, Uploader, SignedUploader, Downloader, SignedDownloader, Attrser, Lister, Remover, PublicUrler } from \"./refs\";\n\nexport interface BucketConfig {\n  /**\n   * Whether the objects in the bucket should be publicly\n   * accessible, via CDN. Defaults to false if unset.\n  */\n  public?: boolean;\n\n  /**\n   * Whether to enable versioning of the objects in the bucket.\n   * Defaults to false if unset.\n   */\n  versioned?: boolean;\n}\n\n/**\n * Defines a new Object Storage bucket infrastructure resource.\n */\nexport class Bucket extends BucketPerms implements Uploader, SignedUploader, Downloader, SignedDownloader, Attrser, Lister, Remover, PublicUrler {\n  impl: runtime.Bucket;\n\n  /**\n   * Creates a new bucket with the given name and configuration\n   */\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  constructor(name: string, cfg?: BucketConfig) {\n    super();\n    this.impl = runtime.RT.bucket(name);\n  }\n\n  /**\n   * Reference an existing bucket by name.\n   * To create a new storage bucket, use `new StorageBucket(...)` instead.\n   */\n  static named<name extends string>(name: StringLiteral<name>): Bucket {\n    return new Bucket(name, {});\n  }\n\n  async *list(options: ListOptions): AsyncGenerator<ListEntry> {\n    const source = getCurrentRequest();\n    const iter = unwrapErr(await this.impl.list(options, source));\n    while (true) {\n      const entry = await iter.next();\n      if (entry === null) {\n        iter.markDone();\n        break;\n      }\n      yield entry;\n    }\n  }\n\n  /**\n   * Returns whether the object exists in the bucket.\n   * Throws an error on network failure.\n   */\n  async exists(name: string, options?: ExistsOptions): Promise<boolean> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const res = await impl.exists(options, source);\n    return unwrapErr(res);\n  }\n\n  /**\n   * Returns the object's attributes.\n   * Throws an error if the object does not exist.\n   */\n  async attrs(name: string, options?: AttrsOptions): Promise<ObjectAttrs> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const res = await impl.attrs(options, source);\n    return unwrapErr(res);\n  }\n\n  /**\n   * Uploads an object to the bucket.\n   */\n  async upload(name: string, data: Buffer, options?: UploadOptions): Promise<ObjectAttrs> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const res = await impl.upload(data, options, source);\n    return unwrapErr(res);\n  }\n\n  /**\n   * Generate an external URL to allow uploading an object to the bucket.\n   * \n   * Anyone with possession of the URL can write to the given object name\n   * without any additional auth.\n   */\n  async signedUploadUrl(name: string, options?: UploadUrlOptions): Promise<SignedUploadUrl> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const res = await impl.signedUploadUrl(options, source);\n    return unwrapErr(res);\n  }\n\n  /**\n   * Generate an external URL to allow downloading an object from the bucket.\n   *\n   * Anyone with possession of the URL can download the given object without\n   * any additional auth.\n   */\n  async signedDownloadUrl(name: string, options?: DownloadUrlOptions): Promise<SignedDownloadUrl> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const res = await impl.signedDownloadUrl(options, source);\n    return unwrapErr(res);\n  }\n\n  /**\n   * Downloads an object from the bucket and returns its contents.\n   */\n  async download(name: string, options?: DownloadOptions): Promise<Buffer> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const res = await impl.downloadAll(options, source);\n    return unwrapErr(res);\n  }\n\n  /**\n   * Removes an object from the bucket.\n   * Throws an error on network failure.\n   */\n  async remove(name: string, options?: DeleteOptions): Promise<void> {\n    const source = getCurrentRequest();\n    const impl = this.impl.object(name);\n    const err = await impl.delete(options, source);\n    if (err) {\n      unwrapErr(err);\n    }\n  }\n\n  /**\n  * Returns the public URL for accessing the object with the given name.\n  * Throws an error if the bucket is not public.\n  */\n  publicUrl(name: string): string {\n    const obj = this.impl.object(name);\n    return obj.publicUrl();\n  }\n\n  ref<P extends BucketPerms>(): P {\n    return this as unknown as P\n  }\n}\n\nexport interface ListOptions {\n  /**\n   * Only include objects with this prefix in the listing.\n   * If unset, all objects are included.\n  */\n  prefix?: string;\n\n  /** Maximum number of objects to return. Defaults to no limit. */\n  limit?: number;\n}\n\nexport interface AttrsOptions {\n  /**\n   * The object version to retrieve attributes for.\n   * Defaults to the lastest version if unset.\n   *\n   * If bucket versioning is not enabled, this option is ignored.\n   */\n  version?: string;\n}\n\nexport interface ExistsOptions {\n  /**\n   * The object version to check for existence.\n   * Defaults to the lastest version if unset.\n   *\n   * If bucket versioning is not enabled, this option is ignored.\n   */\n  version?: string;\n}\n\nexport interface DeleteOptions {\n  /**\n   * The object version to delete.\n   * Defaults to the lastest version if unset.\n   *\n   * If bucket versioning is not enabled, this option is ignored.\n   */\n  version?: string;\n}\n\nexport interface DownloadOptions {\n  /**\n   * The object version to download.\n   * Defaults to the lastest version if unset.\n   *\n   * If bucket versioning is not enabled, this option is ignored.\n   */\n  version?: string;\n}\n\nexport interface ObjectAttrs {\n  name: string;\n  size: number;\n  /** The version of the object, if bucket versioning is enabled. */\n  version?: string;\n  etag: string;\n  contentType?: string;\n}\n\nexport interface ListEntry {\n  name: string;\n  size: number;\n  etag: string;\n}\n\nexport interface UploadOptions {\n  contentType?: string;\n  preconditions?: {\n    notExists?: boolean;\n  }\n}\n\nexport interface UploadUrlOptions {\n  /** The expiration time of the url, in seconds from signing. The maximum\n   * value is seven days. If no value is given, a default of one hour is\n   * used. */\n  ttl?: number;\n}\n\nexport interface SignedUploadUrl {\n  url: string;\n}\n\nexport interface DownloadUrlOptions {\n  /** The expiration time of the url, in seconds from signing. The maximum\n   * value is seven days. If no value is given, a default of one hour is\n   * used. */\n  ttl?: number;\n}\n\nexport interface SignedDownloadUrl {\n  url: string;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/objects/error.ts",
    "content": "import * as runtime from \"../../internal/runtime/mod\";\n\nexport class ObjectsError extends Error {\n  constructor(msg: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(msg);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"ObjectsError\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, ObjectsError.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\nexport class ObjectNotFound extends ObjectsError {\n  constructor(msg: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(msg);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"ObjectNotFound\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, ObjectNotFound.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\nexport class PreconditionFailed extends ObjectsError {\n  constructor(msg: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(msg);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"PrecondionFailed\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, PreconditionFailed.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\nexport class InvalidArgument extends ObjectsError {\n  constructor(msg: string) {\n    // extending errors causes issues after you construct them, unless you apply the following fixes\n    super(msg);\n\n    // set error name as constructor name, make it not enumerable to keep native Error behavior\n    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors\n    Object.defineProperty(this, \"name\", {\n      value: \"InvalidArgument\",\n      enumerable: false,\n      configurable: true\n    });\n\n    // Fix the prototype chain, capture stack trace.\n    Object.setPrototypeOf(this, InvalidArgument.prototype);\n    Error.captureStackTrace(this, this.constructor);\n  }\n}\n\nexport function unwrapErr<T>(val: T | runtime.TypedObjectError): T {\n  if (val instanceof runtime.TypedObjectError) {\n    switch (val.kind) {\n      case runtime.ObjectErrorKind.NotFound:\n        throw new ObjectNotFound(val.message);\n      case runtime.ObjectErrorKind.PreconditionFailed:\n        throw new PreconditionFailed(val.message);\n      case runtime.ObjectErrorKind.InvalidArgument:\n        throw new InvalidArgument(val.message);\n      default:\n        throw new ObjectsError(val.message);\n    }\n  }\n\n  return val;\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/objects/mod.ts",
    "content": "export { Bucket } from \"./bucket\";\nexport type { BucketConfig, ObjectAttrs, UploadOptions } from \"./bucket\";\nexport { ObjectsError, ObjectNotFound, PreconditionFailed } from \"./error\";\nexport type {\n  BucketPerms,\n  Uploader,\n  SignedUploader,\n  Downloader,\n  SignedDownloader,\n  Attrser,\n  Lister,\n  ReadWriter,\n  PublicUrler,\n  Remover\n} from \"./refs\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/objects/refs.ts",
    "content": "import type { AttrsOptions, DeleteOptions, DownloadOptions, DownloadUrlOptions, ExistsOptions, ListEntry,\n  ListOptions, ObjectAttrs, SignedDownloadUrl, SignedUploadUrl, UploadOptions, UploadUrlOptions} from \"./bucket\";\n\nexport abstract class BucketPerms {\n  private bucketPerms(): void { };\n}\n\nexport abstract class Uploader extends BucketPerms {\n  abstract upload(name: string, data: Buffer, options?: UploadOptions): Promise<ObjectAttrs>;\n}\n\nexport abstract class SignedUploader extends BucketPerms {\n  abstract signedUploadUrl(name: string, options?: UploadUrlOptions): Promise<SignedUploadUrl>;\n}\n\nexport abstract class Downloader extends BucketPerms {\n  abstract download(name: string, options?: DownloadOptions): Promise<Buffer>;\n}\n\nexport abstract class SignedDownloader extends BucketPerms {\n  abstract signedDownloadUrl(name: string, options?: DownloadUrlOptions): Promise<SignedDownloadUrl>;\n}\n\nexport abstract class Attrser extends BucketPerms {\n  abstract attrs(name: string, options?: AttrsOptions): Promise<ObjectAttrs>;\n  abstract exists(name: string, options?: ExistsOptions): Promise<boolean>;\n}\n\nexport abstract class Lister extends BucketPerms {\n  abstract list(options: ListOptions): AsyncGenerator<ListEntry>;\n}\n\nexport abstract class Remover extends BucketPerms {\n  abstract remove(name: string, options?: DeleteOptions): Promise<void>;\n}\n\nexport abstract class PublicUrler extends BucketPerms {\n  abstract publicUrl(name: string): string;\n}\n\nexport type ReadWriter =\n  & Uploader\n  & SignedUploader\n  & Downloader\n  & SignedDownloader\n  & Attrser\n  & Lister\n  & Remover;\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/sqldb/database.ts",
    "content": "import { getCurrentRequest } from \"../../internal/reqtrack/mod\";\nimport * as runtime from \"../../internal/runtime/mod\";\nimport { StringLiteral } from \"../../internal/utils/constraints\";\n\nexport interface SQLMigrationsConfig {\n  path: string;\n  source?: \"prisma\" | \"drizzle\" | \"drizzle/v1\";\n}\nexport interface SQLDatabaseConfig {\n  migrations?: string | SQLMigrationsConfig;\n}\n\nconst driverName = \"node-pg\";\n\n/**\n * Represents a single row from a query result\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type Row = Record<string, any>;\n\n/** Represents a type that can be used in query template literals */\nexport type Primitive =\n  | string\n  | string[]\n  | number\n  | number[]\n  | boolean\n  | boolean[]\n  | Buffer\n  | Date\n  | Date[]\n  | Record<string, any>\n  | Record<string, any>[]\n  | BigInt\n  | BigInt[]\n  | null\n  | undefined;\n\ntype SQLQueryExecutor =\n  | runtime.SQLConn\n  | runtime.SQLDatabase\n  | runtime.Transaction;\n\n/** Base class containing shared query functionality */\nclass BaseQueryExecutor {\n  constructor(protected readonly impl: SQLQueryExecutor) {}\n\n  /**\n   * query queries the database using a template string, replacing your placeholders in the template\n   * with parametrised values without risking SQL injections.\n   *\n   * It returns an async generator, that allows iterating over the results\n   * in a streaming fashion using `for await`.\n   *\n   * @example\n   *\n   * const email = \"foo@example.com\";\n   * const result = database.query`SELECT id FROM users WHERE email=${email}`\n   *\n   * This produces the query: \"SELECT id FROM users WHERE email=$1\".\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  async *query<T extends Row = Record<string, any>>(\n    strings: TemplateStringsArray,\n    ...params: Primitive[]\n  ): AsyncGenerator<T> {\n    const query = buildQuery(strings, params);\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n    const cursor = await this.impl.query(query, args, source);\n    while (true) {\n      const row = await cursor.next();\n      if (row === null) break;\n      yield row.values() as T;\n    }\n  }\n\n  /**\n   * rawQuery queries the database using a raw parametrised SQL query and parameters.\n   *\n   * It returns an async generator, that allows iterating over the results\n   * in a streaming fashion using `for await`.\n   *\n   * @example\n   * const query = \"SELECT id FROM users WHERE email=$1\";\n   * const email = \"foo@example.com\";\n   * for await (const row of database.rawQuery(query, email)) {\n   *   console.log(row);\n   * }\n   *\n   * @param query - The raw SQL query string.\n   * @param params - The parameters to be used in the query.\n   * @returns An async generator that yields rows from the query result.\n   */\n  async *rawQuery<T extends Row = Record<string, any>>(\n    query: string,\n    ...params: Primitive[]\n  ): AsyncGenerator<T> {\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n    const result = await this.impl.query(query, args, source);\n    while (true) {\n      const row = await result.next();\n      if (row === null) break;\n      yield row.values() as T;\n    }\n  }\n\n  /**\n   * queryAll queries the database using a template string, replacing your placeholders in the template\n   * with parametrised values without risking SQL injections.\n   *\n   * It returns an array of all results.\n   *\n   * @example\n   *\n   * const email = \"foo@example.com\";\n   * const result = database.queryAll`SELECT id FROM users WHERE email=${email}`\n   *\n   * This produces the query: \"SELECT id FROM users WHERE email=$1\".\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  async queryAll<T extends Row = Record<string, any>>(\n    strings: TemplateStringsArray,\n    ...params: Primitive[]\n  ): Promise<T[]> {\n    const query = buildQuery(strings, params);\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n    const cursor = await this.impl.query(query, args, source);\n    const result: T[] = [];\n    while (true) {\n      const row = await cursor.next();\n      if (row === null) break;\n      result.push(row.values() as T);\n    }\n    return result;\n  }\n\n  /**\n   * rawQueryAll queries the database using a raw parametrised SQL query and parameters.\n   *\n   * It returns an array of all results.\n   *\n   * @example\n   *\n   * const query = \"SELECT id FROM users WHERE email=$1\";\n   * const email = \"foo@example.com\";\n   * const rows = await database.rawQueryAll(query, email);\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  async rawQueryAll<T extends Row = Record<string, any>>(\n    query: string,\n    ...params: Primitive[]\n  ): Promise<T[]> {\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n    const cursor = await this.impl.query(query, args, source);\n    const result: T[] = [];\n    while (true) {\n      const row = await cursor.next();\n      if (row === null) break;\n      result.push(row.values() as T);\n    }\n    return result;\n  }\n\n  /**\n   * queryRow is like query but returns only a single row.\n   * If the query selects no rows it returns null.\n   * Otherwise it returns the first row and discards the rest.\n   *\n   * @example\n   * const email = \"foo@example.com\";\n   * const result = database.queryRow`SELECT id FROM users WHERE email=${email}`\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  async queryRow<T extends Row = Record<string, any>>(\n    strings: TemplateStringsArray,\n    ...params: Primitive[]\n  ): Promise<T | null> {\n    const query = buildQuery(strings, params);\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n    const result = await this.impl.query(query, args, source);\n    const row = await result.next();\n    return row ? (row.values() as T) : null;\n  }\n\n  /**\n   * rawQueryRow is like rawQuery but returns only a single row.\n   * If the query selects no rows, it returns null.\n   * Otherwise, it returns the first row and discards the rest.\n   *\n   * @example\n   * const query = \"SELECT id FROM users WHERE email=$1\";\n   * const email = \"foo@example.com\";\n   * const result = await database.rawQueryRow(query, email);\n   * console.log(result);\n   *\n   * @param query - The raw SQL query string.\n   * @param params - The parameters to be used in the query.\n   * @returns A promise that resolves to a single row or null.\n   */\n  async rawQueryRow<T extends Row = Record<string, any>>(\n    query: string,\n    ...params: Primitive[]\n  ): Promise<T | null> {\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n    const result = await this.impl.query(query, args, source);\n    const row = await result.next();\n    return row ? (row.values() as T) : null;\n  }\n\n  /**\n   * exec executes a query without returning any rows.\n   *\n   * @example\n   * const email = \"foo@example.com\";\n   * const result = database.exec`DELETE FROM users WHERE email=${email}`\n   */\n  async exec(\n    strings: TemplateStringsArray,\n    ...params: Primitive[]\n  ): Promise<void> {\n    const query = buildQuery(strings, params);\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n\n    // Need to await the cursor to process any errors from things like\n    // unique constraint violations.\n    const cur = await this.impl.query(query, args, source);\n    await cur.next();\n  }\n\n  /**\n   * rawExec executes a query without returning any rows.\n   *\n   * @example\n   * const query = \"DELETE FROM users WHERE email=$1\";\n   * const email = \"foo@example.com\";\n   * await database.rawExec(query, email);\n   *\n   * @param query - The raw SQL query string.\n   * @param params - The parameters to be used in the query.\n   * @returns A promise that resolves when the query has been executed.\n   */\n  async rawExec(query: string, ...params: Primitive[]): Promise<void> {\n    const args = buildQueryArgs(params);\n    const source = getCurrentRequest();\n\n    // Need to await the cursor to process any errors from things like\n    // unique constraint violations.\n    const cur = await this.impl.query(query, args, source);\n    await cur.next();\n  }\n}\n\n/**\n * Constructing a new database object will result in Encore provisioning a database with\n * that name and returning this object to represent it.\n *\n * If you want to reference an existing database, use `Database.Named(name)` as it is a\n * compile error to create duplicate databases.\n */\nexport class SQLDatabase extends BaseQueryExecutor {\n  declare protected readonly impl: runtime.SQLDatabase;\n\n  constructor(name: string, cfg?: SQLDatabaseConfig) {\n    super(runtime.RT.sqlDatabase(name));\n  }\n\n  /**\n   * Reference an existing database by name, if the database doesn't\n   * exist yet, use `new Database(name)` instead.\n   */\n  static named<name extends string>(name: StringLiteral<name>): SQLDatabase {\n    return new SQLDatabase(name);\n  }\n\n  /**\n   * Returns the connection string for the database\n   */\n  get connectionString(): string {\n    return this.impl.connString();\n  }\n\n  /**\n   * Acquires a database connection from the database pool.\n   *\n   * When the connection is closed or is garbage-collected, it is returned to the pool.\n   * @returns a new connection to the database\n   */\n  async acquire(): Promise<Connection> {\n    const impl = await this.impl.acquire();\n    return new Connection(impl);\n  }\n\n  /**\n   * Begins a database transaction.\n   *\n   * Make sure to always call `rollback` or `commit` to prevent hanging transactions.\n   * @returns a transaction object that implements AsycDisposable\n   */\n  async begin(): Promise<Transaction> {\n    const source = getCurrentRequest();\n    const impl = await this.impl.begin(source);\n    return new Transaction(impl);\n  }\n}\n\nexport class Transaction extends BaseQueryExecutor implements AsyncDisposable {\n  declare protected readonly impl: runtime.Transaction;\n  private done: boolean = false;\n\n  constructor(impl: runtime.Transaction) {\n    super(impl);\n  }\n\n  /**\n   * Commit the transaction.\n   */\n  async commit() {\n    this.done = true;\n    const source = getCurrentRequest();\n    await this.impl.commit(source);\n  }\n\n  /**\n   * Rollback the transaction.\n   */\n  async rollback() {\n    this.done = true;\n    const source = getCurrentRequest();\n    await this.impl.rollback(source);\n  }\n\n  async [Symbol.asyncDispose]() {\n    if (!this.done) {\n      await this.rollback();\n    }\n  }\n}\n\n/**\n * Represents a dedicated connection to a database.\n */\nexport class Connection extends BaseQueryExecutor {\n  declare protected readonly impl: runtime.SQLConn;\n\n  constructor(impl: runtime.SQLConn) {\n    super(impl);\n  }\n\n  /**\n   * Returns the connection to the database pool.\n   */\n  async close() {\n    await this.impl.close();\n  }\n}\n\nfunction buildQuery(strings: TemplateStringsArray, expr: Primitive[]): string {\n  let query = \"\";\n  for (let i = 0; i < strings.length; i++) {\n    query += strings[i];\n\n    if (i < expr.length) {\n      query += \"$\" + (i + 1);\n    }\n  }\n\n  // return queryWithComment(query, driverName);\n  return query;\n}\n\nfunction buildQueryArgs(params: Primitive[]): runtime.QueryArgs {\n  // Convert undefined to null.\n  return new runtime.QueryArgs(params.map((p) => p ?? null));\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/storage/sqldb/mod.ts",
    "content": "export { SQLDatabase } from \"./database\";\nexport type { SQLDatabaseConfig, Row as ResultRow } from \"./database\";\n"
  },
  {
    "path": "runtimes/js/encore.dev/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"include\": [\"**/*.ts\", \"**/*.cts\"],\n  \"exclude\": [\"dist/**\", \"**/*.test.ts\"],\n  \"compilerOptions\": {\n    /* Basic Options */\n    \"lib\": [\"ES2022\"],\n    \"target\": \"ES2022\",\n    \"module\": \"ES2022\",\n\n    \"outDir\": \"./dist\",\n    \"rootDir\": \".\",\n\n    /* Workspace Settings */\n    \"composite\": true,\n\n    /* Strict Type-Checking Options */\n    \"strict\": true,\n\n    /* Module Resolution Options */\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"isolatedModules\": true, // This is required to ensure we don't write code which results in runtime issues if transpiled file by file\n    \"sourceMap\": true,\n\n    \"declaration\": true,\n\n    /* Advanced Options */\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/types/mod.ts",
    "content": "import * as runtime from \"../internal/runtime/mod\";\n\nexport type ToDecimal = string | number | bigint;\n\n/**\n * A decimal type that can hold values with arbitrary precision.\n * Unlike JavaScript's native number type, this can accurately represent\n * decimal values without floating-point precision errors.\n */\nexport class Decimal {\n  private impl: runtime.Decimal;\n\n  constructor(value: ToDecimal) {\n    this.impl = new runtime.Decimal(String(value));\n  }\n\n  private static fromImpl(impl: runtime.Decimal): Decimal {\n    const d = Object.create(Decimal.prototype);\n    d.impl = impl;\n    return d;\n  }\n\n  private toImpl(value: Decimal | ToDecimal): runtime.Decimal {\n    return value instanceof Decimal\n      ? value.impl\n      : new runtime.Decimal(String(value));\n  }\n\n  /**\n   * Adds this decimal to another decimal value.\n   */\n  add(d: Decimal | ToDecimal): Decimal {\n    return Decimal.fromImpl(this.impl.add(this.toImpl(d)));\n  }\n\n  /**\n   * Subtracts another decimal value from this decimal.\n   */\n  sub(d: Decimal | ToDecimal): Decimal {\n    return Decimal.fromImpl(this.impl.sub(this.toImpl(d)));\n  }\n\n  /**\n   * Multiplies this decimal by another decimal value.\n   */\n  mul(d: Decimal | ToDecimal): Decimal {\n    return Decimal.fromImpl(this.impl.mul(this.toImpl(d)));\n  }\n\n  /**\n   * Divides this decimal by another decimal value.\n   */\n  div(d: Decimal | ToDecimal): Decimal {\n    return Decimal.fromImpl(this.impl.div(this.toImpl(d)));\n  }\n\n  get value(): string {\n    return this.impl.toString();\n  }\n\n  toJSON(): string {\n    return this.impl.toString();\n  }\n  toString(): string {\n    return this.impl.toString();\n  }\n\n  [Symbol.toPrimitive](hint: string) {\n    if (hint === \"number\") {\n      return +this.value;\n    }\n\n    return this.value;\n  }\n\n  private get __encore_decimal(): boolean {\n    return true;\n  }\n}\n"
  },
  {
    "path": "runtimes/js/encore.dev/validate/mod.ts",
    "content": "declare const __validate: unique symbol;\n\nexport type Min<N extends number> = {\n  [__validate]?: {\n    minValue: N;\n  };\n};\n\nexport type Max<N extends number> = {\n  [__validate]?: {\n    maxValue: N;\n  };\n};\n\nexport type MinLen<N extends number> = {\n  [__validate]?: {\n    minLen: N;\n  };\n};\n\nexport type MaxLen<N extends number> = {\n  [__validate]?: {\n    maxLen: N;\n  };\n};\n\nexport type MatchesRegexp<S extends string> = {\n  [__validate]?: {\n    matchesRegexp: S;\n  };\n};\n\nexport type StartsWith<S extends string> = {\n  [__validate]?: {\n    startsWith: S;\n  };\n};\n\nexport type EndsWith<S extends string> = {\n  [__validate]?: {\n    endsWith: S;\n  };\n};\n\nexport type IsEmail = {\n  [__validate]?: {\n    isEmail: true;\n  };\n};\n\nexport type IsURL = {\n  [__validate]?: {\n    isURL: true;\n  };\n};\n"
  },
  {
    "path": "runtimes/js/src/api.rs",
    "content": "use crate::error::coerce_to_api_error;\nuse crate::headers::parse_header_map;\nuse crate::napi_util::{await_promise, PromiseHandler};\nuse crate::pvalue::{\n    encode_auth_payload, encode_request_payload, parse_pvalues, pvalues_or_null,\n    transform_pvalues_response,\n};\nuse crate::request_meta::RequestMeta;\nuse crate::threadsafe_function::{\n    ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,\n};\nuse crate::{raw_api, request_meta, websocket_api};\nuse encore_runtime_core::api::{self, schema, HandlerResponse, HandlerResponseInner};\nuse encore_runtime_core::model::RequestData;\nuse napi::{Env, JsFunction, JsObject, JsUnknown, NapiRaw};\nuse napi_derive::napi;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\n#[napi(object)]\npub struct APIRoute {\n    pub service: String,\n    pub name: String,\n    pub raw: bool,\n    pub streaming_request: bool,\n    pub streaming_response: bool,\n    pub handler: JsFunction,\n}\n\npub fn new_api_handler(\n    env: Env,\n    func: JsFunction,\n    raw: bool,\n    streaming: bool,\n    resp_schema: Option<Arc<schema::Response>>,\n) -> napi::Result<Arc<dyn api::BoxedHandler>> {\n    if streaming {\n        return websocket_api::new_handler(env, func);\n    }\n    if raw {\n        return raw_api::new_handler(env, func);\n    }\n    let handler = ThreadsafeFunction::create(\n        env.raw(),\n        // SAFETY: `handler` is a valid JS function.\n        unsafe { func.raw() },\n        0,\n        typed_resolve_on_js_thread,\n    )?;\n    Ok(Arc::new(JSTypedHandler {\n        handler,\n        resp_schema,\n    }))\n}\n\n#[napi]\npub struct Request {\n    pub(crate) inner: Arc<encore_runtime_core::model::Request>,\n}\n\n#[napi]\nimpl Request {\n    pub fn new(inner: Arc<encore_runtime_core::model::Request>) -> Self {\n        Self { inner }\n    }\n\n    #[napi]\n    pub fn payload(&self, env: Env) -> napi::Result<JsUnknown> {\n        match &self.inner.data {\n            RequestData::RPC(data) => encode_request_payload(env, data.parsed_payload.as_ref()),\n            RequestData::Auth(data) => encode_auth_payload(env, &data.parsed_payload),\n            RequestData::PubSub(data) => pvalues_or_null(env, data.parsed_payload.as_ref()),\n            RequestData::Stream(data) => encode_request_payload(env, data.parsed_payload.as_ref()),\n        }\n    }\n\n    #[napi]\n    pub fn meta(&self) -> napi::Result<RequestMeta> {\n        request_meta::meta(&self.inner).map_err(napi::Error::from)\n    }\n\n    #[napi]\n    pub fn get_auth_data(&self, env: Env) -> napi::Result<JsUnknown> {\n        use RequestData::*;\n        match &self.inner.data {\n            RPC(data) => pvalues_or_null(env, data.auth_data.as_ref()),\n            Stream(data) => pvalues_or_null(env, data.auth_data.as_ref()),\n            Auth(_) | PubSub(_) => env.get_null().map(|val| val.into_unknown()),\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct APIPromiseHandler {\n    pub resp_schema: Option<Arc<schema::Response>>,\n}\n\nimpl PromiseHandler for APIPromiseHandler {\n    type Output = HandlerResponse;\n\n    fn resolve(&self, env: Env, val: Option<napi::JsUnknown>) -> Self::Output {\n        let Some(val) = val else {\n            return Ok(HandlerResponseInner {\n                payload: None,\n                extra_headers: None,\n                status: None,\n            });\n        };\n\n        let obj: JsObject = val\n            .try_into()\n            .map_err(|e| api::Error::invalid_argument(\"invalid handler response\", e))?;\n\n        let payload = obj\n            .get_named_property::<napi::JsUnknown>(\"payload\")\n            .map_err(api::Error::internal)?;\n\n        let extra_headers = obj\n            .get_named_property::<napi::JsUnknown>(\"extraHeaders\")\n            .map_err(api::Error::internal)?;\n\n        let status = obj\n            .get_named_property::<napi::JsUnknown>(\"status\")\n            .map_err(api::Error::internal)?;\n\n        let status = if status\n            .get_type()\n            .is_ok_and(|t| matches!(t, napi::ValueType::Number))\n        {\n            Some(\n                status\n                    .coerce_to_number()\n                    .map_err(api::Error::internal)\n                    .and_then(|s| s.get_uint32().map_err(api::Error::internal))\n                    .and_then(|s| {\n                        u16::try_from(s).map_err(|e| {\n                            api::Error::invalid_argument(\"invalid http status code\", e)\n                        })\n                    })?,\n            )\n        } else {\n            None\n        };\n\n        match parse_pvalues(payload) {\n            Ok(val) => {\n                let val = match &self.resp_schema {\n                    Some(schema) => val\n                        .map(|v| transform_pvalues_response(v, schema.clone()))\n                        .transpose()\n                        .map_err(|e| api::Error::invalid_argument(\"couldn't parse response\", e))?,\n                    None => val,\n                };\n                Ok(HandlerResponseInner {\n                    payload: val,\n                    status,\n                    extra_headers: parse_header_map(extra_headers).map_err(|e| {\n                        api::Error::invalid_argument(\"unable to parse extraHeaders\", e)\n                    })?,\n                })\n            }\n            Err(err) => self.error(env, err),\n        }\n    }\n\n    fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output {\n        Err(coerce_to_api_error(env, val)?)\n    }\n\n    fn error(&self, _: Env, err: napi::Error) -> Self::Output {\n        Err(api::Error {\n            code: api::ErrCode::Internal,\n            message: api::ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(err.to_string()),\n            stack: None,\n            details: None,\n        })\n    }\n}\n\nstruct TypedRequestMessage {\n    req: Request,\n    resp_schema: Option<Arc<schema::Response>>,\n    tx: tokio::sync::mpsc::UnboundedSender<HandlerResponse>,\n}\n\npub struct JSTypedHandler {\n    handler: ThreadsafeFunction<TypedRequestMessage>,\n    resp_schema: Option<Arc<schema::Response>>,\n}\n\nimpl api::BoxedHandler for JSTypedHandler {\n    fn call(\n        self: Arc<Self>,\n        req: api::HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = api::ResponseData> + Send + 'static>> {\n        Box::pin(async move {\n            // Create a one-shot channel\n            let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();\n\n            // Call the handler.\n            let req = Request::new(req);\n            self.handler.call(\n                TypedRequestMessage {\n                    tx,\n                    req,\n                    resp_schema: self.resp_schema.clone(),\n                },\n                ThreadsafeFunctionCallMode::Blocking,\n            );\n\n            // Wait for a response.\n            let resp = match rx.recv().await {\n                Some(Ok(resp)) => Ok(resp),\n                Some(Err(err)) => Err(err),\n                None => Err(api::Error::internal(anyhow::anyhow!(\n                    \"handler did not respond\",\n                ))),\n            };\n\n            api::ResponseData::Typed(resp)\n        })\n    }\n}\n\nfn typed_resolve_on_js_thread(ctx: ThreadSafeCallContext<TypedRequestMessage>) -> napi::Result<()> {\n    let req = ctx.value.req.into_instance(ctx.env)?;\n    let handler = APIPromiseHandler {\n        resp_schema: ctx.value.resp_schema,\n    };\n    match ctx.callback.unwrap().call(None, &[req]) {\n        Ok(result) => {\n            await_promise(ctx.env, result, ctx.value.tx.clone(), handler);\n            Ok(())\n        }\n        Err(err) => {\n            let res = handler.error(ctx.env, err);\n            _ = ctx.value.tx.send(res);\n            Ok(())\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/cache.rs",
    "content": "use crate::api::Request;\nuse encore_runtime_core::cache;\nuse encore_runtime_core::cache::TtlOp;\nuse napi::bindgen_prelude::*;\nuse napi::{Error, Status};\nuse napi_derive::napi;\nuse std::sync::{Arc, OnceLock};\n\n/// Maps i64 sentinel values from TypeScript to TtlOp.\n/// - None → None (no TTL config)\n/// - -1 → Keep\n/// - -2 → Persist (NeverExpire)\n/// - >= 0 → SetMs(ms)\nfn to_ttl_op(ttl_ms: Option<i64>) -> Option<TtlOp> {\n    match ttl_ms {\n        None => None,\n        Some(-1) => Some(TtlOp::Keep),\n        Some(-2) => Some(TtlOp::Persist),\n        Some(ms) if ms >= 0 => Some(TtlOp::SetMs(ms as u64)),\n        Some(_) => None, // invalid negative values treated as no TTL\n    }\n}\n\n/// A cache cluster for storing cached data.\n#[napi]\npub struct CacheCluster {\n    inner: Arc<dyn cache::Cluster>,\n    client: OnceLock<napi::Result<cache::Client>>,\n}\n\n#[napi]\nimpl CacheCluster {\n    pub fn new(inner: Arc<dyn cache::Cluster>) -> napi::Result<Self> {\n        Ok(Self {\n            inner,\n            client: OnceLock::new(),\n        })\n    }\n\n    fn client(&self) -> napi::Result<&cache::Client> {\n        self.client\n            .get_or_init(|| {\n                self.inner.client().map_err(|e| {\n                    Error::new(\n                        Status::GenericFailure,\n                        format!(\"failed to create cache client: {e}\"),\n                    )\n                })\n            })\n            .as_ref()\n            .map_err(|e| Error::new(e.status, e.reason.clone()))\n    }\n\n    /// Get a value by key.\n    #[napi]\n    pub async fn get(&self, key: String, source: Option<&Request>) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.get(&key, source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Set a value by key with optional TTL.\n    #[napi]\n    pub async fn set(\n        &self,\n        key: String,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<()> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .set(&key, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Set a value only if the key doesn't exist.\n    #[napi]\n    pub async fn set_if_not_exists(\n        &self,\n        key: String,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<bool> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .set_if_not_exists(&key, &value, to_ttl_op(ttl_ms), source)\n            .await;\n        as_bool(result)\n    }\n\n    /// Replace a value only if the key exists.\n    #[napi]\n    pub async fn replace(\n        &self,\n        key: String,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<bool> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .replace(&key, &value, to_ttl_op(ttl_ms), source)\n            .await;\n        as_bool(result)\n    }\n\n    /// Get old value and set new value atomically.\n    #[napi]\n    pub async fn get_and_set(\n        &self,\n        key: String,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .get_and_set(&key, &value, to_ttl_op(ttl_ms), source)\n            .await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Get value and delete key atomically.\n    #[napi]\n    pub async fn get_and_delete(\n        &self,\n        key: String,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.get_and_delete(&key, source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Delete one or more keys.\n    #[napi]\n    pub async fn delete(&self, keys: Vec<String>, source: Option<&Request>) -> napi::Result<u32> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        let result = self\n            .client()?\n            .delete(&key_refs, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result as u32)\n    }\n\n    /// Get multiple values.\n    #[napi]\n    pub async fn mget(\n        &self,\n        keys: Vec<String>,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Option<Buffer>>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        let result = self\n            .client()?\n            .mget(&key_refs, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.map(|b| b.into())).collect())\n    }\n\n    /// Append to a string value.\n    #[napi]\n    pub async fn append(\n        &self,\n        key: String,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .append(&key, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Get a substring of a string value.\n    #[napi]\n    pub async fn get_range(\n        &self,\n        key: String,\n        start: i32,\n        end: i32,\n        source: Option<&Request>,\n    ) -> napi::Result<Buffer> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .get_range(&key, start as i64, end as i64, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into())\n    }\n\n    /// Set a substring at a specific offset.\n    #[napi]\n    pub async fn set_range(\n        &self,\n        key: String,\n        offset: i32,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .set_range(&key, offset as i64, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Get string length.\n    #[napi]\n    pub async fn strlen(&self, key: String, source: Option<&Request>) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?.strlen(&key, source).await.map_err(to_error)\n    }\n\n    /// Increment an integer value.\n    #[napi]\n    pub async fn incr_by(\n        &self,\n        key: String,\n        delta: i64,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .incr_by(&key, delta, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Decrement an integer value.\n    #[napi]\n    pub async fn decr_by(\n        &self,\n        key: String,\n        delta: i64,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .decr_by(&key, delta, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Increment a float value.\n    #[napi]\n    pub async fn incr_by_float(\n        &self,\n        key: String,\n        delta: f64,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<f64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .incr_by_float(&key, delta, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Push values to the left (head) of a list.\n    #[napi]\n    pub async fn lpush(\n        &self,\n        key: String,\n        values: Vec<Buffer>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let value_refs: Vec<&[u8]> = values.iter().map(|v| v.as_ref()).collect();\n        self.client()?\n            .lpush(&key, &value_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Push values to the right (tail) of a list.\n    #[napi]\n    pub async fn rpush(\n        &self,\n        key: String,\n        values: Vec<Buffer>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let value_refs: Vec<&[u8]> = values.iter().map(|v| v.as_ref()).collect();\n        self.client()?\n            .rpush(&key, &value_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Pop a value from the left (head) of a list.\n    /// Returns null if the list is empty or doesn't exist.\n    #[napi]\n    pub async fn lpop(\n        &self,\n        key: String,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.lpop(&key, to_ttl_op(ttl_ms), source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Pop a value from the right (tail) of a list.\n    /// Returns null if the list is empty or doesn't exist.\n    #[napi]\n    pub async fn rpop(\n        &self,\n        key: String,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.rpop(&key, to_ttl_op(ttl_ms), source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Get element at index from a list.\n    #[napi]\n    pub async fn lindex(\n        &self,\n        key: String,\n        index: i32,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.lindex(&key, index as i64, source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Get a range of elements from a list.\n    #[napi]\n    pub async fn lrange(\n        &self,\n        key: String,\n        start: i32,\n        stop: i32,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .lrange(&key, start as i64, stop as i64, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Get all elements of a list (traced as \"items\").\n    #[napi]\n    pub async fn lrange_all(\n        &self,\n        key: String,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .lrange_all(&key, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Get list length.\n    #[napi]\n    pub async fn llen(&self, key: String, source: Option<&Request>) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?.llen(&key, source).await.map_err(to_error)\n    }\n\n    /// Trim a list to the specified range.\n    #[napi]\n    pub async fn ltrim(\n        &self,\n        key: String,\n        start: i32,\n        stop: i32,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<()> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .ltrim(&key, start as i64, stop as i64, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Set element at index in list.\n    #[napi]\n    pub async fn lset(\n        &self,\n        key: String,\n        index: i32,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<()> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .lset(&key, index as i64, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Insert value before pivot in list.\n    #[napi]\n    pub async fn linsert_before(\n        &self,\n        key: String,\n        pivot: Buffer,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .linsert_before(&key, &pivot, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Insert value after pivot in list.\n    #[napi]\n    pub async fn linsert_after(\n        &self,\n        key: String,\n        pivot: Buffer,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .linsert_after(&key, &pivot, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Remove elements from list.\n    #[napi]\n    pub async fn lrem_all(\n        &self,\n        key: String,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .lrem_all(&key, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Remove elements from list.\n    #[napi]\n    pub async fn lrem_first(\n        &self,\n        key: String,\n        count: u32,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .lrem_first(&key, count as u64, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Remove elements from list.\n    #[napi]\n    pub async fn lrem_last(\n        &self,\n        key: String,\n        count: u32,\n        value: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .lrem_last(&key, count as u64, &value, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Move element between lists atomically.\n    #[napi]\n    pub async fn lmove(\n        &self,\n        src: String,\n        dst: String,\n        src_dir: String,\n        dst_dir: String,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let src_dir = match src_dir.as_str() {\n            \"left\" => cache::ListDirection::Left,\n            \"right\" => cache::ListDirection::Right,\n            _ => return Err(Error::new(Status::InvalidArg, \"invalid source direction\")),\n        };\n        let dst_dir = match dst_dir.as_str() {\n            \"left\" => cache::ListDirection::Left,\n            \"right\" => cache::ListDirection::Right,\n            _ => {\n                return Err(Error::new(\n                    Status::InvalidArg,\n                    \"invalid destination direction\",\n                ))\n            }\n        };\n        let result = self\n            .client()?\n            .lmove(&src, &dst, src_dir, dst_dir, to_ttl_op(ttl_ms), source)\n            .await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Add members to a set.\n    #[napi]\n    pub async fn sadd(\n        &self,\n        key: String,\n        members: Vec<Buffer>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let member_refs: Vec<&[u8]> = members.iter().map(|v| v.as_ref()).collect();\n        self.client()?\n            .sadd(&key, &member_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Remove members from a set.\n    #[napi]\n    pub async fn srem(\n        &self,\n        key: String,\n        members: Vec<Buffer>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let member_refs: Vec<&[u8]> = members.iter().map(|v| v.as_ref()).collect();\n        self.client()?\n            .srem(&key, &member_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Check if member exists in set.\n    #[napi]\n    pub async fn sismember(\n        &self,\n        key: String,\n        member: Buffer,\n        source: Option<&Request>,\n    ) -> napi::Result<bool> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .sismember(&key, &member, source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Get all members of a set.\n    #[napi]\n    pub async fn smembers(\n        &self,\n        key: String,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .smembers(&key, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Get set cardinality.\n    #[napi]\n    pub async fn scard(&self, key: String, source: Option<&Request>) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?.scard(&key, source).await.map_err(to_error)\n    }\n\n    /// Pop a single random member from a set.\n    /// Returns null if the set is empty or doesn't exist.\n    #[napi]\n    pub async fn spop(\n        &self,\n        key: String,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.spop(&key, to_ttl_op(ttl_ms), source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Pop random members from a set.\n    #[napi]\n    pub async fn spop_n(\n        &self,\n        key: String,\n        count: u32,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .spop_n(&key, count as usize, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Get a single random member from a set (without removing).\n    /// Returns null if the set is empty or doesn't exist.\n    #[napi]\n    pub async fn srandmember(\n        &self,\n        key: String,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self.client()?.srandmember(&key, source).await;\n        Ok(miss_as_none(result)?.map(|v| v.into()))\n    }\n\n    /// Get random members from a set (without removing).\n    /// Positive count returns distinct elements, negative count may return duplicates.\n    #[napi]\n    pub async fn srandmember_n(\n        &self,\n        key: String,\n        count: i32,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let result = self\n            .client()?\n            .srandmember_n(&key, count as i64, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Get the difference between sets.\n    #[napi]\n    pub async fn sdiff(\n        &self,\n        keys: Vec<String>,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        let result = self\n            .client()?\n            .sdiff(&key_refs, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Store the difference between sets.\n    #[napi]\n    pub async fn sdiffstore(\n        &self,\n        destination: String,\n        keys: Vec<String>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        self.client()?\n            .sdiffstore(&destination, &key_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Get the intersection of sets.\n    #[napi]\n    pub async fn sinter(\n        &self,\n        keys: Vec<String>,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        let result = self\n            .client()?\n            .sinter(&key_refs, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Store the intersection of sets.\n    #[napi]\n    pub async fn sinterstore(\n        &self,\n        destination: String,\n        keys: Vec<String>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        self.client()?\n            .sinterstore(&destination, &key_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Get the union of sets.\n    #[napi]\n    pub async fn sunion(\n        &self,\n        keys: Vec<String>,\n        source: Option<&Request>,\n    ) -> napi::Result<Vec<Buffer>> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        let result = self\n            .client()?\n            .sunion(&key_refs, source)\n            .await\n            .map_err(to_error)?;\n        Ok(result.into_iter().map(|v| v.into()).collect())\n    }\n\n    /// Store the union of sets.\n    #[napi]\n    pub async fn sunionstore(\n        &self,\n        destination: String,\n        keys: Vec<String>,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<i64> {\n        let source = source.map(|s| s.inner.as_ref());\n        let key_refs: Vec<&str> = keys.iter().map(|s| s.as_str()).collect();\n        self.client()?\n            .sunionstore(&destination, &key_refs, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n\n    /// Move member from one set to another.\n    #[napi]\n    pub async fn smove(\n        &self,\n        src: String,\n        dst: String,\n        member: Buffer,\n        ttl_ms: Option<i64>,\n        source: Option<&Request>,\n    ) -> napi::Result<bool> {\n        let source = source.map(|s| s.inner.as_ref());\n        self.client()?\n            .smove(&src, &dst, &member, to_ttl_op(ttl_ms), source)\n            .await\n            .map_err(to_error)\n    }\n}\n\nfn to_error(e: cache::OpError) -> napi::Error {\n    Error::new(Status::GenericFailure, format!(\"{e}\"))\n}\n\n/// Convert an OpResult into Option, mapping Miss to None.\nfn miss_as_none<T>(result: cache::OpResult<T>) -> napi::Result<Option<T>> {\n    match result {\n        Ok(v) => Ok(Some(v)),\n        Err(e) if matches!(e.source, cache::Error::Miss) => Ok(None),\n        Err(e) => Err(to_error(e)),\n    }\n}\n\n/// Convert an OpResult<()> into bool, mapping KeyExist/Miss to false.\nfn as_bool(result: cache::OpResult<()>) -> napi::Result<bool> {\n    match result {\n        Ok(()) => Ok(true),\n        Err(e) if matches!(e.source, cache::Error::KeyExist | cache::Error::Miss) => Ok(false),\n        Err(e) => Err(to_error(e)),\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/cookies.rs",
    "content": "use crate::pvalue::PVal;\n\nuse encore_runtime_core::api::{self, Cookie, DateTime, PValue, PValues};\nuse napi::{bindgen_prelude::*, JsObject, Result};\n\n// Helper struct to parse a PValue::Object from javascript into a PValue::Cookie\npub(crate) struct JsCookie;\n\nimpl JsCookie {\n    fn get_bool(vals: &PValues, key: &str) -> Result<Option<bool>> {\n        match vals.get(key) {\n            None => Ok(None),\n            Some(PValue::Bool(b)) => Ok(Some(*b)),\n            Some(_) => Err(Error::new(\n                Status::InvalidArg,\n                format!(\"cookie field {key} must be a boolean\"),\n            )),\n        }\n    }\n    fn get_string(vals: &PValues, key: &str) -> Result<Option<String>> {\n        match vals.get(key) {\n            None => Ok(None),\n            Some(PValue::String(s)) => Ok(Some(s.clone())),\n            Some(_) => Err(Error::new(\n                Status::InvalidArg,\n                format!(\"cookie field {key} must be a string\"),\n            )),\n        }\n    }\n    fn get_datetime(vals: &PValues, key: &str) -> Result<Option<DateTime>> {\n        match vals.get(key) {\n            None => Ok(None),\n            Some(PValue::DateTime(d)) => Ok(Some(*d)),\n            Some(_) => Err(Error::new(\n                Status::InvalidArg,\n                format!(\"cookie field {key} must be a datetime\"),\n            )),\n        }\n    }\n    fn get_max_age(vals: &PValues, key: &str) -> Result<Option<u64>> {\n        match vals.get(key) {\n            None => Ok(None),\n            Some(PValue::Number(n)) => {\n                if n.is_i64() {\n                    let n = n.as_i64().unwrap();\n                    if n < 0 {\n                        Ok(Some(0u64))\n                    } else {\n                        Ok(Some(n as u64))\n                    }\n                } else if n.is_u64() {\n                    Ok(Some(n.as_u64().unwrap()))\n                } else {\n                    Err(Error::new(\n                        Status::InvalidArg,\n                        \"cookie field {} must be an integer\",\n                    ))\n                }\n            }\n            Some(_) => Err(Error::new(\n                Status::InvalidArg,\n                format!(\"cookie field {key} must be an integer\"),\n            )),\n        }\n    }\n\n    pub fn parse_cookie(obj: &PValues, name: &str, value: &PValue) -> Result<Cookie> {\n        Ok(Cookie {\n            name: name.to_string(),\n            value: Box::new(value.clone()),\n            path: Self::get_string(obj, \"path\")?,\n            domain: Self::get_string(obj, \"domain\")?,\n            secure: Self::get_bool(obj, \"secure\")?,\n            http_only: Self::get_bool(obj, \"httpOnly\")?,\n            expires: Self::get_datetime(obj, \"expires\")?,\n            max_age: Self::get_max_age(obj, \"maxAge\")?,\n            same_site: Self::get_string(obj, \"sameSite\")?\n                .map(|s| match s.as_str() {\n                    \"Lax\" => Ok(api::SameSite::Lax),\n                    \"Strict\" => Ok(api::SameSite::Strict),\n                    \"None\" => Ok(api::SameSite::None),\n                    _ => Err(Error::new(\n                        Status::InvalidArg,\n                        \"cookie field sameSite must be one of Lax, Strict or None\",\n                    )),\n                })\n                .transpose()?,\n            partitioned: Self::get_bool(obj, \"partitioned\")?,\n        })\n    }\n}\n\npub(crate) unsafe fn cookie_to_napi_value(\n    env: sys::napi_env,\n    c: api::Cookie,\n) -> Result<sys::napi_value> {\n    let env2 = Env::from_raw(env);\n    let mut cookie = env2.create_object()?;\n\n    cookie.set(\"name\", &c.name)?;\n    cookie.set(\"value\", PVal(*c.value))?;\n\n    if let Some(secure) = c.secure {\n        cookie.set(\"secure\", secure)?;\n    }\n    if let Some(http_only) = c.http_only {\n        cookie.set(\"httpOnly\", http_only)?;\n    }\n\n    if let Some(domain) = &c.domain {\n        cookie.set(\"domain\", domain)?;\n    }\n    if let Some(path) = &c.path {\n        cookie.set(\"path\", path)?;\n    }\n    if let Some(expires) = c.expires {\n        cookie.set(\"expires\", PVal(PValue::DateTime(expires)))?;\n    }\n    if let Some(same_site) = c.same_site {\n        cookie.set(\"sameSite\", same_site.to_string())?;\n    }\n    JsObject::to_napi_value(env, cookie)\n}\n"
  },
  {
    "path": "runtimes/js/src/error.rs",
    "content": "use crate::{log::parse_js_stack, pvalue::parse_pvalues};\nuse encore_runtime_core::api;\nuse napi::{Env, JsUnknown};\n\npub fn coerce_to_api_error(env: Env, val: napi::JsUnknown) -> Result<api::Error, api::Error> {\n    let obj = val.coerce_to_object().map_err(|_| api::Error {\n        code: api::ErrCode::Internal,\n        message: api::ErrCode::Internal.default_public_message().into(),\n        internal_message: Some(\"an unknown exception was thrown\".into()),\n        details: None,\n        stack: None,\n    })?;\n\n    // Get the details field.\n    let details = obj\n        .get_named_property::<napi::JsUnknown>(\"details\")\n        .and_then(parse_pvalues)\n        .map(|val| val.map(Box::new))\n        .map_err(|e| api::Error {\n            code: api::ErrCode::Internal,\n            message: api::ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(format!(\"unable to parse error details: {e}\")),\n            details: None,\n            stack: None,\n        })?;\n\n    // Get the message field.\n    let mut message: String = obj\n        .get_named_property::<JsUnknown>(\"message\")\n        .and_then(|val| val.coerce_to_string())\n        .and_then(|val| env.from_js_value(val))\n        .map_err(|e| api::Error {\n            code: api::ErrCode::Internal,\n            message: api::ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(format!(\"unable to parse error message: {e}\")),\n            details: None,\n            stack: None,\n        })?;\n\n    // Get the error code field.\n    let code: api::ErrCode = obj\n        .get_named_property::<JsUnknown>(\"code\")\n        .and_then(|val| val.coerce_to_string())\n        .and_then(|val| env.from_js_value::<String, _>(val))\n        .map(|val| {\n            val.parse::<api::ErrCode>()\n                .unwrap_or(api::ErrCode::Internal)\n        })\n        .unwrap_or(api::ErrCode::Internal);\n\n    // Get the cause field\n    let cause: Option<String> = obj\n        .get_named_property::<JsUnknown>(\"cause\")\n        .ok()\n        .and_then(|val| {\n            let val_type = val.get_type().ok()?;\n            if val_type == napi::ValueType::Null || val_type == napi::ValueType::Undefined {\n                None\n            } else {\n                val.coerce_to_object()\n                    .and_then(|val| val.get_named_property::<JsUnknown>(\"message\"))\n                    .and_then(|val| val.coerce_to_string())\n                    .and_then(|val| env.from_js_value(val))\n                    .ok()\n            }\n        });\n\n    if let Some(cause) = cause {\n        message.push_str(\": \");\n        message.push_str(&cause);\n    }\n\n    // Get the JS stack\n    let stack = obj\n        .get_named_property::<JsUnknown>(\"stack\")\n        .and_then(|val| parse_js_stack(&env, val))\n        .ok();\n\n    let mut internal_message = None;\n    if code == api::ErrCode::Internal {\n        internal_message = Some(message);\n        message = api::ErrCode::Internal.default_public_message().into();\n    }\n\n    Ok(api::Error {\n        code,\n        message,\n        stack,\n        internal_message,\n        details,\n    })\n}\n"
  },
  {
    "path": "runtimes/js/src/gateway.rs",
    "content": "use crate::api::Request;\nuse crate::error::coerce_to_api_error;\nuse crate::napi_util::{await_promise, PromiseHandler};\nuse crate::pvalue::parse_pvalues;\nuse crate::threadsafe_function::{\n    ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,\n};\nuse encore_runtime_core::api::HandlerResponse;\nuse encore_runtime_core::api::{self, HandlerResponseInner};\nuse napi::{Env, JsFunction, NapiRaw};\nuse napi_derive::napi;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\n#[napi]\npub struct Gateway {\n    #[allow(dead_code)]\n    gateway: Option<api::gateway::Gateway>,\n}\n\nimpl Gateway {\n    pub fn new(\n        env: Env,\n        gateway: Option<api::gateway::Gateway>,\n        cfg: GatewayConfig,\n    ) -> napi::Result<Self> {\n        if let Some(gw) = &gateway {\n            if let Some(auth) = gw.auth_handler() {\n                if let Some(handler) = cfg.auth {\n                    let handler: Arc<dyn api::TypedHandler> = to_auth_handler(env, handler)?;\n\n                    auth.set_local_handler_impl(Some(handler));\n                }\n            }\n        }\n\n        Ok(Self { gateway })\n    }\n}\n\n#[napi(object)]\npub struct GatewayConfig {\n    pub auth: Option<JsFunction>,\n}\n\nfn to_auth_handler(env: Env, handler: JsFunction) -> napi::Result<Arc<JSAuthHandler>> {\n    let tsfn = ThreadsafeFunction::create(\n        env.raw(),\n        // SAFETY: `handler` is a valid JS function.\n        unsafe { handler.raw() },\n        0,\n        resolve_on_js_thread,\n    )?;\n\n    Ok(Arc::new(JSAuthHandler { handler: tsfn }))\n}\n\npub struct JSAuthHandler {\n    handler: ThreadsafeFunction<AuthMessage>,\n}\n\nimpl api::TypedHandler for JSAuthHandler {\n    fn call(\n        self: Arc<Self>,\n        req: api::HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = api::HandlerResponse> + Send + 'static>> {\n        Box::pin(async move {\n            // Create a one-shot channel\n            let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();\n\n            // Call the handler.\n            let req = Request::new(req);\n            self.handler.call(\n                AuthMessage { tx, req },\n                ThreadsafeFunctionCallMode::Blocking,\n            );\n\n            // Wait for a response.\n            match rx.recv().await {\n                Some(Ok(resp)) => Ok(resp),\n                Some(Err(err)) => Err(err),\n                None => Err(api::Error::internal(anyhow::anyhow!(\n                    \"handler did not respond\",\n                ))),\n            }\n        })\n    }\n}\n\nstruct AuthMessage {\n    req: Request,\n    tx: tokio::sync::mpsc::UnboundedSender<HandlerResponse>,\n}\n\nfn resolve_on_js_thread(ctx: ThreadSafeCallContext<AuthMessage>) -> napi::Result<()> {\n    let req = ctx.value.req.into_instance(ctx.env)?;\n    let handler = AuthPromiseHandler;\n    match ctx.callback.unwrap().call(None, &[req]) {\n        Ok(result) => {\n            await_promise(ctx.env, result, ctx.value.tx.clone(), handler);\n            Ok(())\n        }\n        Err(err) => {\n            let res = handler.error(ctx.env, err);\n            _ = ctx.value.tx.send(res);\n            Ok(())\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy)]\nstruct AuthPromiseHandler;\n\nimpl PromiseHandler for AuthPromiseHandler {\n    type Output = HandlerResponse;\n\n    fn resolve(&self, env: Env, val: Option<napi::JsUnknown>) -> Self::Output {\n        let Some(val) = val else {\n            return Ok(HandlerResponseInner {\n                payload: None,\n                extra_headers: None,\n                status: None,\n            });\n        };\n        match parse_pvalues(val) {\n            Ok(val) => Ok(HandlerResponseInner {\n                payload: val,\n                extra_headers: None,\n                status: None,\n            }),\n            Err(err) => self.error(env, err),\n        }\n    }\n\n    fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output {\n        Err(coerce_to_api_error(env, val)?)\n    }\n\n    fn error(&self, _: Env, err: napi::Error) -> Self::Output {\n        Err(api::Error {\n            code: api::ErrCode::Internal,\n            message: api::ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(err.to_string()),\n            stack: None,\n            details: None,\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/headers.rs",
    "content": "use axum::http::{HeaderMap, HeaderName, HeaderValue};\nuse napi::{\n    bindgen_prelude::{assert_type_of, check_status, type_of, FromNapiValue},\n    sys, Error, JsObject, JsUnknown, Result, ValueType,\n};\n\npub fn parse_header_map(val: JsUnknown) -> Result<Option<HeaderMap>> {\n    if val\n        .get_type()\n        .is_ok_and(|t| matches!(t, napi::ValueType::Undefined | napi::ValueType::Null))\n    {\n        return Ok(None);\n    }\n\n    let val = WrappedHeaderMap::from_unknown(val)?;\n    Ok(Some(val.0))\n}\n\npub struct WrappedHeaderMap(pub HeaderMap);\n\nimpl FromNapiValue for WrappedHeaderMap {\n    unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {\n        assert_type_of!(env, napi_val, ValueType::Object)?;\n        let obj = JsObject::from_napi_value(env, napi_val)?;\n\n        let mut map = WrappedHeaderMap(HeaderMap::new());\n        for key in JsObject::keys(&obj)?.into_iter() {\n            if let Some(vals) = obj_get_header_val(env, napi_val, &key)? {\n                let hname = HeaderName::from_bytes(key.as_bytes()).map_err(|e| {\n                    Error::new(\n                        napi::Status::InvalidArg,\n                        format!(\"invalid header name: {e}\"),\n                    )\n                })?;\n\n                for val in vals {\n                    let hval = HeaderValue::from_bytes(val.as_bytes()).map_err(|e| {\n                        Error::new(\n                            napi::Status::InvalidArg,\n                            format!(\"invalid header value: {e}\"),\n                        )\n                    })?;\n\n                    map.0.append(&hname, hval);\n                }\n            }\n        }\n\n        Ok(map)\n    }\n}\n\nfn obj_get_header_val<K: AsRef<str>>(\n    env: sys::napi_env,\n    obj: sys::napi_value,\n    field: K,\n) -> Result<Option<Vec<String>>> {\n    let c_field = std::ffi::CString::new(field.as_ref())?;\n\n    unsafe {\n        let mut ret = std::ptr::null_mut();\n\n        check_status!(\n            sys::napi_get_named_property(env, obj, c_field.as_ptr(), &mut ret),\n            \"Failed to get property with field `{}`\",\n            field.as_ref()\n        )?;\n\n        let ty = type_of!(env, ret)?;\n\n        match ty {\n            ValueType::Undefined => Ok(None),\n            ValueType::String => {\n                let val = String::from_napi_value(env, ret)?;\n                Ok(Some(vec![val]))\n            }\n            ValueType::Object => {\n                let mut is_arr = false;\n                check_status!(\n                    sys::napi_is_array(env, ret, &mut is_arr),\n                    \"Failed to detect whether given js is an array\"\n                )?;\n\n                if is_arr {\n                    let vals = Vec::<String>::from_napi_value(env, ret).map_err(|_e| {\n                        Error::new(\n                            napi::Status::InvalidArg,\n                            \"unable to parse header values array as strings\",\n                        )\n                    })?;\n\n                    Ok(Some(vals))\n                } else {\n                    Err(Error::new(\n                        napi::Status::InvalidArg,\n                        \"invalid header value type\",\n                    ))\n                }\n            }\n            _ => Err(Error::new(\n                napi::Status::InvalidArg,\n                \"header map value must be string or array of strings\",\n            )),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/lib.rs",
    "content": "#![deny(clippy::all)]\n\npub mod api;\npub mod cache;\nmod cookies;\nmod error;\nmod gateway;\nmod headers;\nmod log;\nmod meta;\npub mod metrics;\nmod napi_util;\npub mod objects;\npub mod pubsub;\nmod pvalue;\nmod raw_api;\nmod request_meta;\npub mod runtime;\nmod runtime_config;\nmod secret;\nmod sqldb;\nmod stream;\nmod threadsafe_function;\nmod websocket_api;\n"
  },
  {
    "path": "runtimes/js/src/log.rs",
    "content": "use crate::api::Request;\nuse encore_runtime_core::error::{AppError, StackFrame, StackTrace};\nuse encore_runtime_core::log::Fields;\nuse encore_runtime_core::log::LogFromExternalRuntime;\nuse napi::{Env, Error};\nuse napi_derive::napi;\nuse std::collections::HashMap;\n\n/// A logger that can be used to log messages from the runtime.\n#[napi]\npub struct Logger {\n    pub(crate) logger: encore_runtime_core::log::Logger,\n}\n\n#[napi]\npub enum LogLevel {\n    Trace = 1,\n    Debug,\n    Info,\n    Warn,\n    Error,\n}\n\nimpl From<LogLevel> for log::LevelFilter {\n    fn from(value: LogLevel) -> Self {\n        match value {\n            LogLevel::Trace => log::LevelFilter::Trace,\n            LogLevel::Debug => log::LevelFilter::Debug,\n            LogLevel::Info => log::LevelFilter::Info,\n            LogLevel::Warn => log::LevelFilter::Warn,\n            LogLevel::Error => log::LevelFilter::Error,\n        }\n    }\n}\n\nimpl From<LogLevel> for log::Level {\n    fn from(value: LogLevel) -> Self {\n        match value {\n            LogLevel::Trace => log::Level::Trace,\n            LogLevel::Debug => log::Level::Debug,\n            LogLevel::Info => log::Level::Info,\n            LogLevel::Warn => log::Level::Warn,\n            LogLevel::Error => log::Level::Error,\n        }\n    }\n}\n\nimpl Default for Logger {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n#[napi]\nimpl Logger {\n    pub fn new() -> Self {\n        Self {\n            logger: encore_runtime_core::log::root().clone(),\n        }\n    }\n\n    /// log a message from the application\n    #[napi]\n    #[allow(clippy::too_many_arguments)]\n    pub fn log(\n        &self,\n        env: Env,\n        request: Option<&Request>,\n        level: LogLevel,\n        msg: String,\n        #[napi(ts_arg_type = \"Error\")] error: Option<napi::JsObject>,\n        #[napi(ts_arg_type = \"string\")] caller: Option<String>,\n        #[napi(ts_arg_type = \"Record<string, unknown>\")] fields: Option<\n            HashMap<String, napi::JsUnknown>,\n        >,\n    ) {\n        let error = convert_error(env, error).unwrap_or_else(|err| {\n            ::log::error!(\"couldn't convert error to api error: {err}\");\n            None\n        });\n\n        let res = self\n            .logger\n            .log(\n                request.map(|r| r.inner.as_ref()),\n                level.into(),\n                msg,\n                error,\n                caller,\n                convert_fields(env, fields),\n            )\n            .map_err(Error::from);\n\n        if let Err(err) = res {\n            ::log::error!(\"logging failed: {err}\");\n        }\n    }\n\n    /// Returns a new logger with the specified level\n    #[napi]\n    pub fn with_level(&self, level: LogLevel) -> Self {\n        Self {\n            logger: self.logger.with_level(level.into()),\n        }\n    }\n\n    /// Returns a new logger with the given fields added to the context\n    /// that the logger will use when emitting logs as extra fields\n    #[napi]\n    pub fn with(\n        &self,\n        env: Env,\n        #[napi(ts_arg_type = \"Record<string, unknown>\")] fields: HashMap<String, napi::JsUnknown>,\n    ) -> napi::Result<Self> {\n        let fields = convert_fields(env, Some(fields)).unwrap();\n\n        Ok(Self {\n            logger: self.logger.with(fields),\n        })\n    }\n}\n\nfn convert_error(env: Env, input: Option<napi::JsObject>) -> napi::Result<Option<AppError>> {\n    match input {\n        None => Ok(None),\n        Some(input) => {\n            let message: napi::JsUnknown = input.get_named_property(\"message\")?;\n            let message = message.coerce_to_string()?;\n            let message: String = env.from_js_value(message)?;\n\n            // try to convert the JS stack trace\n            let stack: napi::Result<napi::JsUnknown> = input.get_named_property(\"stack\");\n            let stack = stack\n                .map(|unknown| parse_js_stack(&env, unknown).unwrap_or_default())\n                .unwrap_or_default();\n\n            Ok(Some(AppError {\n                message,\n                stack,\n                cause: None,\n            }))\n        }\n    }\n}\n\npub fn parse_js_stack(env: &Env, value: napi::JsUnknown) -> napi::Result<StackTrace> {\n    let value = value.coerce_to_string()?;\n    let value: String = env.from_js_value(value)?;\n    parse_stack(value)\n}\n\npub fn parse_stack(s: String) -> napi::Result<StackTrace> {\n    Ok(s.lines()\n        .filter_map(extract_frame)\n        .filter(|frame| !is_common_frame(frame))\n        .collect::<Vec<StackFrame>>())\n}\n\n/// is_common_frame returns true if the frame is a common frame that should be ignored.\n/// as it is outside the users code base (such as it comes from the node runtime).\nfn is_common_frame(frame: &StackFrame) -> bool {\n    // \"node:\" is a node internal frame\n    // \"bun:\" is a bun internal frame\n    // \"deno:\" is a deno internal frame\n    // \"\" is a frame with no file, so a core JS function\n    frame.file.starts_with(\"node:\")\n        || frame.file.starts_with(\"bun:\")\n        || frame.file.starts_with(\"deno:\")\n        || frame.file.contains(\"node_modules/encore.dev/\")\n        || frame.file.is_empty()\n}\n\n/// Attempts to extract a stack frame from a line of text.\n///\n/// All frames in javascript engines are in one of the formats of (after whitespaces):\n/// - \"at <function> (<file>:<line>:<column>)\"\n/// - \"at <file>:<line>:<column>\"\nfn extract_frame(line: &str) -> Option<StackFrame> {\n    let line = line.trim_start().trim_end();\n    if !line.starts_with(\"at \") {\n        return None;\n    }\n    let line = line[2..].trim_start();\n\n    let (function, file, line, column) = if line.ends_with(')') {\n        match line.rsplit_once('(') {\n            Some((function, rest)) => {\n                let (file, line, column) = match extract_file_line_col(rest.strip_suffix(')')?) {\n                    Some((file, line, column)) => (file, line, column),\n                    None => return None,\n                };\n\n                (Some(function.trim()), file, line, column)\n            }\n            None => return None,\n        }\n    } else {\n        extract_file_line_col(line).map(|(file, line, column)| (None, file, line, column))?\n    };\n\n    // Format the file path to be relative to the current working directory\n    let file = match std::env::current_dir() {\n        Ok(cwd) => {\n            let prefix = format!(\"{}/\", cwd.to_string_lossy());\n            file.strip_prefix(prefix.as_str()).unwrap_or(file.as_str())\n        }\n        Err(_) => file.as_str(),\n    };\n\n    Some(StackFrame {\n        file: file.to_string(),\n        line,\n        column,\n        module: None,\n        function: function.map(|s| s.to_string()),\n    })\n}\n\nfn extract_file_line_col(string: &str) -> Option<(String, u32, Option<u32>)> {\n    let string = string.strip_prefix(\"file://\").unwrap_or(string);\n    let (file, rest) = string.split_once(':')?;\n\n    match rest.split_once(':') {\n        Some((line, column)) => Some((\n            file.trim().to_string(),\n            line.parse::<u32>().ok()?,\n            Some(column.parse::<u32>().ok()?),\n        )),\n        None => Some((file.trim().to_string(), rest.parse::<u32>().ok()?, None)),\n    }\n}\n\n/// converts a hash map of unknown JS values to a BTree of serde_json::Value's\nfn convert_fields(env: Env, input: Option<HashMap<String, napi::JsUnknown>>) -> Option<Fields> {\n    match input {\n        None => None,\n        Some(input) if input.is_empty() => None,\n        Some(input) => {\n            let mut fields = Fields::new();\n\n            for (key, value) in input {\n                let val: napi::Result<serde_json::Value> = env.from_js_value(&value);\n                match val {\n                    Ok(val) => {\n                        fields.insert(key, val);\n                    }\n                    Err(_) => {\n                        // if value is not deserializable (e.g Function), print its type\n                        let value_type = serde_json::Value::from(\n                            value\n                                .get_type()\n                                .unwrap_or(napi::ValueType::Unknown)\n                                .to_string(),\n                        );\n                        fields.insert(key, value_type);\n                    }\n                };\n            }\n\n            Some(fields)\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/meta.rs",
    "content": "use std::collections::HashMap;\n\nuse encore_runtime_core::meta;\nuse napi_derive::napi;\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct AppMeta {\n    pub app_id: String,\n    pub api_base_url: String,\n    pub environment: EnvironmentMeta,\n    pub build: BuildMeta,\n    pub deploy: DeployMeta,\n}\n\nimpl From<meta::AppMeta> for AppMeta {\n    fn from(rt: meta::AppMeta) -> Self {\n        AppMeta {\n            app_id: rt.app_id,\n            api_base_url: rt.api_base_url,\n            environment: rt.environment.into(),\n            build: rt.build.into(),\n            deploy: rt.deploy.into(),\n        }\n    }\n}\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct EnvironmentMeta {\n    pub name: String,\n    pub r#type: EnvironmentType,\n    pub cloud: CloudProvider,\n}\n\nimpl From<meta::EnvironmentMeta> for EnvironmentMeta {\n    fn from(rt: meta::EnvironmentMeta) -> Self {\n        EnvironmentMeta {\n            name: rt.name,\n            r#type: rt.r#type.into(),\n            cloud: rt.cloud.into(),\n        }\n    }\n}\n\n#[napi]\n#[derive(Debug)]\npub enum EnvironmentType {\n    Production,\n    Development,\n    Ephemeral,\n    Test,\n}\n\nimpl From<meta::EnvironmentType> for EnvironmentType {\n    fn from(rt: meta::EnvironmentType) -> Self {\n        match rt {\n            meta::EnvironmentType::Production => Self::Production,\n            meta::EnvironmentType::Development => Self::Development,\n            meta::EnvironmentType::Ephemeral => Self::Ephemeral,\n            meta::EnvironmentType::Test => Self::Test,\n        }\n    }\n}\n\n#[napi]\n#[derive(Debug)]\n#[allow(clippy::upper_case_acronyms)]\npub enum CloudProvider {\n    AWS,\n    GCP,\n    Azure,\n    Encore,\n    Local,\n}\n\nimpl From<meta::CloudProvider> for CloudProvider {\n    fn from(rt: meta::CloudProvider) -> Self {\n        match rt {\n            meta::CloudProvider::AWS => Self::AWS,\n            meta::CloudProvider::GCP => Self::GCP,\n            meta::CloudProvider::Azure => Self::Azure,\n            meta::CloudProvider::Encore => Self::Encore,\n            meta::CloudProvider::Local => Self::Local,\n        }\n    }\n}\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct BuildMeta {\n    pub revision: String,\n    pub uncommitted_changes: bool,\n}\n\nimpl From<meta::BuildMeta> for BuildMeta {\n    fn from(rt: meta::BuildMeta) -> Self {\n        BuildMeta {\n            revision: rt.revision,\n            uncommitted_changes: rt.uncommitted_changes,\n        }\n    }\n}\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct HostedService {\n    pub name: String,\n}\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct DeployMeta {\n    pub id: String,\n    pub deploy_time: String,\n    pub hosted_services: HashMap<String, HostedService>,\n}\n\nimpl From<meta::DeployMeta> for DeployMeta {\n    fn from(rt: meta::DeployMeta) -> Self {\n        DeployMeta {\n            id: rt.id,\n            deploy_time: rt.deploy_time.to_string(),\n            hosted_services: rt\n                .hosted_services\n                .into_iter()\n                .map(|svc| (svc.name.clone(), svc.into()))\n                .collect(),\n        }\n    }\n}\n\nimpl From<meta::HostedService> for HostedService {\n    fn from(rt: meta::HostedService) -> Self {\n        HostedService { name: rt.name }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/metrics.rs",
    "content": "use convert_case::{Case, Casing};\nuse encore_runtime_core::metrics::{CollectedMetric, MetricValue, MetricsCollector};\nuse metrics::{Key, Label};\nuse napi::{Env, NapiRaw};\nuse napi_derive::napi;\nuse std::collections::HashMap;\nuse std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse std::time::SystemTime;\n\n#[derive(Debug)]\n#[napi(string_enum)]\npub enum MetricType {\n    Counter,\n    Gauge,\n}\n\n#[derive(Debug, Clone)]\npub struct MetricMetadata {\n    pub slot: usize,\n    pub key: Key,\n    pub metric_type: MetricType,\n    pub registered_at: SystemTime,\n}\n\n/// Internal state of the metrics registry, wrapped in Arc for sharing\npub(crate) struct MetricsRegistryInner {\n    buffer_ptr: BufferPtr,\n    next_slot: AtomicUsize,\n    slot_map: Mutex<HashMap<Key, MetricMetadata>>,\n}\n\n/// Global singleton for the metrics registry, shared across all worker threads\n/// TODO: use get_or_try_init when stable\nstatic METRICS_REGISTRY: OnceLock<napi::Result<Arc<MetricsRegistryInner>>> = OnceLock::new();\n\n/// MetricsRegistry manages the SharedArrayBuffer and slot allocation\n/// for custom application metrics.\n/// This is a lightweight wrapper around Arc<MetricsRegistryInner> for NAPI.\n#[napi]\npub struct MetricsRegistry {\n    pub(crate) inner: Arc<MetricsRegistryInner>,\n}\n\n/// Thread-safe wrapper around a SharedArrayBuffer-backed TypedArray pointer.\n///\n/// # Safety\n///\n/// This struct wraps a raw pointer obtained from a BigUint64Array view of a JavaScript\n/// SharedArrayBuffer and must uphold critical safety invariants:\n///\n/// ## Lifetime Invariants\n/// 1. **JavaScript Ownership**: The underlying SharedArrayBuffer is owned by JavaScript\n///    and its lifetime is managed by V8's garbage collector.\n/// 2. **JavaScript Lifetime Management**: The SharedArrayBuffer is stored in a JavaScript\n///    module-level global variable (`globalBuffer`), which prevents garbage\n///    collection for the entire application lifetime.\n/// 3. **Pointer Stability**: SharedArrayBuffer backing stores do not move in memory,\n///    so the pointer remains valid as long as the buffer exists. The TypedArray view's\n///    data pointer is stable and points directly to the backing store.\n///\n/// ## Thread Safety\n/// - The pointer is `Send + Sync` because:\n///   - JavaScript uses `Atomics` operations for all writes to the SharedArrayBuffer\n///   - Rust uses `AtomicU64::load()` with SeqCst ordering for all reads\n///   - The backing SharedArrayBuffer is explicitly designed for concurrent access\n///   - Multiple TypedArray views of the same SharedArrayBuffer share the same backing store\n///\n/// ## Usage Contract\n/// - This pointer must NEVER outlive the `MetricsRegistry` that created it\n/// - All access must be through atomic operations\n/// - Bounds checking must be performed before dereferencing\n/// - The pointer was obtained via `napi_get_typedarray_info`, which works correctly\n///   with SharedArrayBuffer-backed TypedArrays (unlike `napi_get_arraybuffer_info`)\nstruct BufferPtr {\n    ptr: *mut u64,\n    len: usize,\n}\n\nunsafe impl Send for BufferPtr {}\nunsafe impl Sync for BufferPtr {}\n\n#[napi]\nimpl MetricsRegistry {\n    #[napi(constructor)]\n    pub fn new(env: Env, typed_array: napi::JsObject) -> napi::Result<Self> {\n        // Extract the raw pointer from the TypedArray (BigUint64Array)\n        // This works with SharedArrayBuffer-backed TypedArrays\n        let (ptr, len) = unsafe {\n            let mut arraybuffer: napi::sys::napi_value = std::ptr::null_mut();\n            let mut byte_offset: usize = 0;\n            let mut length: usize = 0;\n            let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut();\n\n            let raw_env = env.raw();\n            let raw_value = typed_array.raw();\n\n            // Get TypedArray info (works with SharedArrayBuffer backing)\n            let status = napi::sys::napi_get_typedarray_info(\n                raw_env,\n                raw_value,\n                std::ptr::null_mut(), // Don't need type\n                &mut length,\n                &mut data_ptr,\n                &mut arraybuffer,\n                &mut byte_offset,\n            );\n\n            if status != napi::sys::Status::napi_ok {\n                return Err(napi::Error::new(\n                    napi::Status::from(status),\n                    \"Failed to get TypedArray info\",\n                ));\n            }\n\n            let ptr = data_ptr as *mut u64;\n            // Length is already in elements (u64), not bytes\n\n            (ptr, length)\n        };\n\n        Ok(Self {\n            inner: Arc::new(MetricsRegistryInner {\n                buffer_ptr: BufferPtr { ptr, len },\n                next_slot: AtomicUsize::new(0),\n                slot_map: Mutex::new(HashMap::new()),\n            }),\n        })\n    }\n\n    /// Allocate (or get) a slot for a unique metric\n    #[napi]\n    pub fn allocate_slot(\n        &self,\n        name: String,\n        labels: Vec<(String, String)>,\n        service_name: Option<String>,\n        metric_type: MetricType,\n    ) -> u32 {\n        let mut label_vec: Vec<Label> = labels\n            .into_iter()\n            .map(|(k, v)| Label::new(k.to_case(Case::Snake), v))\n            .collect();\n\n        if let Some(ref svc) = service_name {\n            label_vec.push(Label::new(\"service\", svc.clone()));\n        }\n\n        let key = Key::from_parts(name, label_vec);\n        let mut slot_map = self.inner.slot_map.lock().expect(\"mutex poisoned\");\n\n        if let Some(existing) = slot_map.get(&key) {\n            return existing.slot as u32;\n        }\n\n        // Allocate new slot and insert metadata\n        let slot = self.inner.next_slot.fetch_add(1, Ordering::SeqCst);\n        slot_map.insert(\n            key.clone(),\n            MetricMetadata {\n                slot,\n                key,\n                metric_type,\n                registered_at: SystemTime::now(),\n            },\n        );\n\n        slot as u32\n    }\n\n    /// Get the number of allocated slots\n    #[napi]\n    pub fn slot_count(&self) -> u32 {\n        self.inner.next_slot.load(Ordering::SeqCst) as u32\n    }\n\n    /// Initialize the global metrics registry (should be called once by main thread)\n    /// Returns the existing registry if already initialized\n    pub(crate) fn get_or_init_global(\n        env: Env,\n        typed_array: napi::JsObject,\n    ) -> napi::Result<Arc<MetricsRegistryInner>> {\n        METRICS_REGISTRY\n            .get_or_init(|| Self::new(env, typed_array).map(|registry| Arc::clone(&registry.inner)))\n            .clone()\n    }\n\n    /// Get the global metrics registry if it has been initialized\n    pub fn get_global() -> Option<Self> {\n        METRICS_REGISTRY.get().and_then(|result| {\n            result.as_ref().ok().map(|inner| Self {\n                inner: Arc::clone(inner),\n            })\n        })\n    }\n}\n\n/// Collector that bridges JS metrics to the core runtime's metrics system\npub struct JsMetricsCollector {\n    registry: Arc<MetricsRegistryInner>,\n}\n\nimpl JsMetricsCollector {\n    pub fn new(registry: &MetricsRegistry) -> Self {\n        Self {\n            registry: Arc::clone(&registry.inner),\n        }\n    }\n\n    /// Read a u64 value from the SharedArrayBuffer at the given slot\n    fn read_slot(&self, slot: usize) -> u64 {\n        if slot >= self.registry.buffer_ptr.len {\n            return 0;\n        }\n        // SAFETY: The pointer is valid for the lifetime of the SharedArrayBuffer,\n        // and JavaScript uses Atomics to write to it. We use atomic loads for thread safety.\n        unsafe {\n            let ptr = self.registry.buffer_ptr.ptr.add(slot);\n            AtomicU64::from_ptr(ptr).load(Ordering::SeqCst)\n        }\n    }\n}\n\nimpl MetricsCollector for JsMetricsCollector {\n    fn collect(&self) -> Vec<CollectedMetric> {\n        let slot_map = self.registry.slot_map.lock().expect(\"mutext poisoned\");\n        let mut collected = Vec::with_capacity(slot_map.len());\n\n        for meta in slot_map.values() {\n            let raw_value = self.read_slot(meta.slot);\n\n            let value = match meta.metric_type {\n                MetricType::Counter => MetricValue::CounterU64(raw_value),\n                MetricType::Gauge => MetricValue::GaugeF64(f64::from_bits(raw_value)),\n            };\n\n            collected.push(CollectedMetric {\n                key: meta.key.clone(),\n                value,\n                registered_at: meta.registered_at,\n            });\n        }\n\n        collected\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/napi_util.rs",
    "content": "use napi::{Either, Env, JsFunction, JsObject, JsUnknown};\nuse std::sync::RwLock;\n\npub trait PromiseHandler: Clone + Send + Sync + 'static {\n    type Output: Send + 'static;\n\n    fn resolve(&self, env: Env, val: Option<napi::JsUnknown>) -> Self::Output;\n    fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output;\n    fn error(&self, env: Env, err: napi::Error) -> Self::Output;\n}\n\npub fn await_promise<T, H>(\n    env: Env,\n    result: JsUnknown,\n    tx: tokio::sync::mpsc::UnboundedSender<T>,\n    handler: H,\n) where\n    H: PromiseHandler<Output = T>,\n    T: Send + 'static,\n{\n    // An inner function to enable using ? for error handling.\n    let outer_handler = handler.clone();\n    let outer_tx = tx.clone();\n    let inner = move || -> napi::Result<()> {\n        // If the result is a promise, wait for it to resolve, and send the result to the channel.\n        // Otherwise, send the result immediately.\n        if result.is_promise()? {\n            let result: JsObject = result.try_into()?;\n            let then: JsFunction = result.get_named_property(\"then\")?;\n\n            let cb = {\n                let handler = handler.clone();\n                let tx = tx.clone();\n                env.create_function_from_closure(\"callback\", move |ctx| {\n                    let res = match ctx.try_get::<JsUnknown>(0) {\n                        Ok(Either::A(success)) => handler.resolve(env, Some(success)),\n                        Ok(Either::B(_)) => handler.resolve(env, None),\n                        Err(err) => handler.error(env, err),\n                    };\n\n                    _ = tx.send(res);\n                    ctx.env.get_undefined()\n                })?\n            };\n\n            let eb = {\n                let handler = handler.clone();\n                env.create_function_from_closure(\"error_callback\", move |ctx| {\n                    let res = match ctx.get(0) {\n                        Ok(exception) => handler.reject(env, exception),\n                        Err(err) => handler.error(env, err),\n                    };\n\n                    _ = tx.send(res);\n                    ctx.env.get_undefined()\n                })?\n            };\n\n            then.call(Some(&result), &[cb, eb])?;\n        } else {\n            let res = handler.resolve(env, Some(result));\n            _ = tx.send(res);\n        }\n\n        Ok(())\n    };\n\n    inner().unwrap_or_else(move |err| {\n        let res = outer_handler.error(env, err);\n        _ = outer_tx.send(res);\n    });\n}\n\n/// EnvMap is a thread-safe map that stores values associated with Env objects.\n/// It is intended for storing one value per napi_env. We need the map to work with\n/// worker pooling, where we can have multiple napi envs that each need their own copy.\n/// It uses a vector under the hood since the number of values is small (one per worker).\npub struct EnvMap<T> {\n    map: RwLock<Vec<(usize, T)>>,\n}\n\nimpl<T> EnvMap<T> {\n    pub const fn new() -> Self {\n        Self {\n            map: RwLock::new(Vec::new()),\n        }\n    }\n\n    pub fn get(&self, env: Env) -> Option<T>\n    where\n        T: Clone,\n    {\n        let elems = self.map.read().unwrap();\n        for (addr, value) in elems.iter() {\n            if *addr == env.raw().addr() {\n                return Some(value.clone());\n            }\n        }\n        None\n    }\n\n    pub fn get_or_init<F>(&self, env: Env, init: F) -> T\n    where\n        T: Clone,\n        F: FnOnce() -> T,\n    {\n        let addr = env.raw().addr();\n\n        // First try to read\n        let num_scanned = {\n            let map = self.map.read().unwrap();\n            for (key, value) in map.iter() {\n                if *key == addr {\n                    return value.clone();\n                }\n            }\n            map.len()\n        };\n\n        // If not found, get write lock and initialize\n        let mut map = self.map.write().unwrap();\n\n        // Double-check in case another thread initialized it.\n        // We only need to check from the last scanned index\n        for (key, value) in map[num_scanned..].iter() {\n            if *key == addr {\n                return value.clone();\n            }\n        }\n\n        let value = init();\n        map.push((addr, value.clone()));\n        value\n    }\n}\n\nimpl<T> Default for EnvMap<T> {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/objects.rs",
    "content": "use encore_runtime_core::objects as core;\nuse napi::bindgen_prelude::Buffer;\nuse napi::{Env, JsBuffer, JsObject};\nuse napi_derive::napi;\nuse std::time::Duration;\n\nuse crate::api::Request;\n\n#[napi]\npub struct Bucket {\n    bkt: core::Bucket,\n}\n\n#[napi]\nimpl Bucket {\n    pub(crate) fn new(bkt: core::Bucket) -> Self {\n        Self { bkt }\n    }\n\n    #[napi]\n    pub fn object(&self, name: String) -> BucketObject {\n        BucketObject::new(self.bkt.object(name))\n    }\n\n    #[napi]\n    pub async fn list(\n        &self,\n        options: Option<ListOptions>,\n        source: Option<&Request>,\n    ) -> napi::Either<ListIterator, TypedObjectError> {\n        let options = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.bkt.list(options, source).await {\n            Ok(iter) => napi::Either::A(ListIterator::new(iter)),\n            Err(err) => napi::Either::B(err.into()),\n        }\n    }\n}\n\n#[napi]\npub struct BucketObject {\n    obj: core::Object,\n}\n\n#[napi]\nimpl BucketObject {\n    pub(crate) fn new(obj: core::Object) -> Self {\n        Self { obj }\n    }\n\n    #[napi]\n    pub async fn attrs(\n        &self,\n        options: Option<AttrsOptions>,\n        source: Option<&Request>,\n    ) -> napi::Either<ObjectAttrs, TypedObjectError> {\n        let options = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.obj.attrs(options, source).await {\n            Ok(attrs) => napi::Either::A(attrs.into()),\n            Err(err) => napi::Either::B(err.into()),\n        }\n    }\n\n    #[napi]\n    pub async fn exists(\n        &self,\n        options: Option<ExistsOptions>,\n        source: Option<&Request>,\n    ) -> napi::Either<bool, TypedObjectError> {\n        let opts = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.obj.exists(opts, source).await {\n            Ok(val) => napi::Either::A(val),\n            Err(err) => napi::Either::B(err.into()),\n        }\n    }\n\n    #[napi(ts_return_type = \"Promise<ObjectAttrs | TypedObjectError>\")]\n    pub fn upload(\n        &self,\n        env: Env,\n        data: JsBuffer,\n        opts: Option<UploadOptions>,\n        source: Option<&Request>,\n    ) -> napi::Result<JsObject> {\n        // TODO: reference the data via a Ref, so that we can keep it alive throughout the upload.\n        let data = data.into_value()?.as_ref().to_vec();\n\n        let cursor = std::io::Cursor::new(data);\n        let opts = opts.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n\n        let fut = self.obj.upload(Box::new(cursor), opts, source);\n\n        // We need to always execute the handler below so that we can decrement the ref count.\n        // To do so, we need the future to be a napi::Result::Ok. So wrap the result inside that\n        // so that the handler gets called regardless of result.\n        let fut = async move { Ok(fut.await) };\n\n        env.execute_tokio_future(fut, move |&mut _env, result| {\n            // TODO: Decrement the ref count on the data buffer.\n            match result {\n                Ok(attrs) => Ok(napi::Either::A(ObjectAttrs::from(attrs))),\n                Err(err) => Ok(napi::Either::B(TypedObjectError::from(err))),\n            }\n        })\n    }\n\n    #[napi]\n    pub async fn signed_upload_url(\n        &self,\n        options: Option<UploadUrlOptions>,\n        source: Option<&Request>,\n    ) -> napi::Either<SignedUploadUrl, TypedObjectError> {\n        let options = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.obj.signed_upload_url(options, source).await {\n            Ok(url) => napi::Either::A(SignedUploadUrl { url }),\n            Err(err) => napi::Either::B(err.into()),\n        }\n    }\n\n    #[napi]\n    pub async fn signed_download_url(\n        &self,\n        options: Option<DownloadUrlOptions>,\n        source: Option<&Request>,\n    ) -> napi::Either<SignedDownloadUrl, TypedObjectError> {\n        let options = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.obj.signed_download_url(options, source).await {\n            Ok(url) => napi::Either::A(SignedDownloadUrl { url }),\n            Err(err) => napi::Either::B(err.into()),\n        }\n    }\n\n    #[napi]\n    pub async fn download_all(\n        &self,\n        options: Option<DownloadOptions>,\n        source: Option<&Request>,\n    ) -> napi::Either<Buffer, TypedObjectError> {\n        let options = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.obj.download_all(options, source).await {\n            Ok(buf) => napi::Either::A(buf.into()),\n            Err(err) => napi::Either::B(err.into()),\n        }\n    }\n\n    #[napi]\n    pub async fn delete(\n        &self,\n        options: Option<DeleteOptions>,\n        source: Option<&Request>,\n    ) -> Option<TypedObjectError> {\n        let options = options.unwrap_or_default().into();\n        let source = source.map(|s| s.inner.clone());\n        match self.obj.delete(options, source).await {\n            Ok(()) => None,\n            Err(err) => Some(err.into()),\n        }\n    }\n\n    #[napi]\n    pub fn public_url(&self) -> napi::Result<String> {\n        match self.obj.public_url() {\n            Ok(url) => Ok(url),\n            Err(err) => Err(napi::Error::new(\n                napi::Status::GenericFailure,\n                err.to_string(),\n            )),\n        }\n    }\n}\n\n#[napi]\npub struct ObjectAttrs {\n    pub name: String,\n    pub version: Option<String>,\n    pub size: i64,\n    pub content_type: Option<String>,\n    pub etag: String,\n}\n\nimpl From<core::ObjectAttrs> for ObjectAttrs {\n    fn from(value: core::ObjectAttrs) -> Self {\n        Self {\n            name: value.name,\n            version: value.version,\n            size: value.size as i64,\n            content_type: value.content_type,\n            etag: value.etag,\n        }\n    }\n}\n\n#[napi]\npub struct ListEntry {\n    pub name: String,\n    pub size: i64,\n    pub etag: String,\n}\n\nimpl From<core::ListEntry> for ListEntry {\n    fn from(value: core::ListEntry) -> Self {\n        Self {\n            name: value.name,\n            size: value.size as i64,\n            etag: value.etag,\n        }\n    }\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct UploadOptions {\n    pub content_type: Option<String>,\n    pub preconditions: Option<UploadPreconditions>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct UploadPreconditions {\n    pub not_exists: Option<bool>,\n}\n\nimpl From<UploadOptions> for core::UploadOptions {\n    fn from(value: UploadOptions) -> Self {\n        Self {\n            content_type: value.content_type,\n            preconditions: value.preconditions.map(|p| p.into()),\n        }\n    }\n}\n\nimpl From<UploadPreconditions> for core::UploadPreconditions {\n    fn from(value: UploadPreconditions) -> Self {\n        Self {\n            not_exists: value.not_exists,\n        }\n    }\n}\n\n#[napi]\npub enum ObjectErrorKind {\n    NotFound,\n    PreconditionFailed,\n    InvalidArgument,\n    Other,\n    Internal,\n}\n\n#[napi]\npub struct TypedObjectError {\n    pub kind: ObjectErrorKind,\n    pub message: String,\n}\n\nimpl From<core::Error> for TypedObjectError {\n    fn from(value: core::Error) -> Self {\n        let kind = match &value {\n            core::Error::NotFound => ObjectErrorKind::NotFound,\n            core::Error::PreconditionFailed => ObjectErrorKind::PreconditionFailed,\n            core::Error::InvalidArgument => ObjectErrorKind::InvalidArgument,\n            core::Error::Internal(_) => ObjectErrorKind::Internal,\n            core::Error::Other(_) => ObjectErrorKind::Other,\n        };\n        Self {\n            kind,\n            message: value.to_string(),\n        }\n    }\n}\n\n#[napi]\npub struct ListIterator {\n    stream: tokio::sync::Mutex<Option<core::ListIterator>>,\n}\n\n#[napi]\nimpl ListIterator {\n    fn new(stream: core::ListIterator) -> Self {\n        Self {\n            stream: tokio::sync::Mutex::new(Some(stream)),\n        }\n    }\n\n    #[napi]\n    pub async fn next(&self) -> napi::Result<Option<ListEntry>> {\n        let mut stream = self.stream.lock().await;\n        if let Some(stream) = stream.as_mut() {\n            let row =\n                stream.next().await.transpose().map_err(|e| {\n                    napi::Error::new(napi::Status::GenericFailure, format!(\"{e:#?}\"))\n                })?;\n            Ok(row.map(ListEntry::from))\n        } else {\n            Err(napi::Error::new(\n                napi::Status::GenericFailure,\n                \"iterator is closed\",\n            ))\n        }\n    }\n\n    #[napi]\n    pub fn mark_done(&mut self) {\n        if let Some(stream) = self.stream.get_mut().take() {\n            drop(stream);\n        };\n    }\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct ExistsOptions {\n    pub version: Option<String>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct AttrsOptions {\n    pub version: Option<String>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct UploadUrlOptions {\n    pub ttl: Option<i64>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct SignedUploadUrl {\n    pub url: String,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct DownloadUrlOptions {\n    pub ttl: Option<i64>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct SignedDownloadUrl {\n    pub url: String,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct DeleteOptions {\n    pub version: Option<String>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct DownloadOptions {\n    pub version: Option<String>,\n}\n\n#[napi(object)]\n#[derive(Debug, Default)]\npub struct ListOptions {\n    pub prefix: Option<String>,\n    pub limit: Option<i64>,\n}\n\nimpl From<DownloadOptions> for core::DownloadOptions {\n    fn from(value: DownloadOptions) -> Self {\n        Self {\n            version: value.version,\n        }\n    }\n}\n\nimpl From<DeleteOptions> for core::DeleteOptions {\n    fn from(value: DeleteOptions) -> Self {\n        Self {\n            version: value.version,\n        }\n    }\n}\n\nimpl From<ExistsOptions> for core::ExistsOptions {\n    fn from(value: ExistsOptions) -> Self {\n        Self {\n            version: value.version,\n        }\n    }\n}\n\nimpl From<AttrsOptions> for core::AttrsOptions {\n    fn from(value: AttrsOptions) -> Self {\n        Self {\n            version: value.version,\n        }\n    }\n}\n\nimpl From<UploadUrlOptions> for core::UploadUrlOptions {\n    fn from(value: UploadUrlOptions) -> Self {\n        Self {\n            ttl: Duration::from_secs(value.ttl.map(|v| v as u64).unwrap_or(3600)),\n        }\n    }\n}\n\nimpl From<DownloadUrlOptions> for core::DownloadUrlOptions {\n    fn from(value: DownloadUrlOptions) -> Self {\n        Self {\n            ttl: Duration::from_secs(value.ttl.map(|v| v as u64).unwrap_or(3600)),\n        }\n    }\n}\n\nimpl From<ListOptions> for core::ListOptions {\n    fn from(value: ListOptions) -> Self {\n        Self {\n            prefix: value.prefix,\n            limit: value.limit.map(|v| v as u64),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/pubsub.rs",
    "content": "use std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse anyhow::Context;\nuse napi::{Env, Error, JsFunction, JsObject, JsUnknown, NapiRaw, Status};\nuse napi_derive::napi;\n\nuse encore_runtime_core::pubsub::{SubscriptionObj, TopicObj};\nuse encore_runtime_core::{api, model, pubsub};\n\nuse crate::api::Request;\nuse crate::error::coerce_to_api_error;\nuse crate::napi_util::{await_promise, PromiseHandler};\nuse crate::pvalue::parse_pvalues;\nuse crate::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunction};\n\n#[napi]\npub struct PubSubTopic {\n    topic: TopicObj,\n}\n\n#[napi]\nimpl PubSubTopic {\n    pub(crate) fn new(topic: TopicObj) -> Self {\n        Self { topic }\n    }\n\n    #[napi(ts_return_type = \"Promise<string>\")]\n    pub fn publish(\n        &self,\n        env: Env,\n        body: JsUnknown,\n        source: Option<&Request>,\n    ) -> napi::Result<JsObject> {\n        let Some(payload) = parse_pvalues(body).context(\"failed to parse payload\")? else {\n            return Err(Error::new(\n                Status::InvalidArg,\n                \"no message payload provided\",\n            ));\n        };\n\n        let source = source.map(|s| s.inner.clone());\n        let fut = self.topic.publish(payload, source);\n        let fut = async move {\n            match fut.await {\n                Ok(id) => Ok(id),\n                Err(e) => Err(Error::new(\n                    Status::GenericFailure,\n                    format!(\"failed to publish: {e}\"),\n                )),\n            }\n        };\n\n        env.spawn_future(fut)\n    }\n}\n\n#[napi(object)]\npub struct PubSubSubscriptionConfig {\n    pub topic_name: String,\n    pub subscription_name: String,\n    pub handler: JsFunction,\n}\n\nimpl PubSubSubscriptionConfig {\n    pub fn to_handler(&self, env: Env) -> napi::Result<JSSubscriptionHandler> {\n        let tsfn = ThreadsafeFunction::create(\n            env.raw(),\n            // SAFETY: `handler` is a valid JS function.\n            unsafe { self.handler.raw() },\n            0,\n            resolve_on_js_thread,\n        )?;\n\n        Ok(JSSubscriptionHandler {\n            handler: Arc::new(tsfn),\n        })\n    }\n}\n\n#[napi]\npub struct PubSubSubscription {\n    sub: Arc<SubscriptionObj>,\n    handler: Arc<JSSubscriptionHandler>,\n}\n\nimpl PubSubSubscription {\n    pub fn new(sub: Arc<SubscriptionObj>, handler: Arc<JSSubscriptionHandler>) -> Self {\n        Self { sub, handler }\n    }\n}\n\n#[napi]\nimpl PubSubSubscription {\n    #[napi]\n    pub async fn subscribe(&self) -> napi::Result<()> {\n        self.sub\n            .subscribe(self.handler.clone())\n            .await\n            .map_err(|e| Error::new(Status::GenericFailure, format!(\"failed to subscribe: {e}\")))\n    }\n}\n\nstruct PubSubMessageRequest {\n    req: Request,\n    tx: tokio::sync::mpsc::UnboundedSender<Result<(), api::Error>>,\n}\n\n#[derive(Debug)]\npub struct JSSubscriptionHandler {\n    handler: Arc<ThreadsafeFunction<PubSubMessageRequest>>,\n}\n\nimpl pubsub::SubscriptionHandler for JSSubscriptionHandler {\n    fn handle_message(\n        &self,\n        msg: Arc<model::Request>,\n    ) -> Pin<Box<dyn Future<Output = Result<(), api::Error>> + Send + '_>> {\n        let handler = self.handler.clone();\n        Box::pin(async move {\n            let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();\n            let req = Request::new(msg);\n            handler.call(\n                PubSubMessageRequest { req, tx },\n                crate::threadsafe_function::ThreadsafeFunctionCallMode::Blocking,\n            );\n\n            match rx.recv().await {\n                Some(Ok(())) => Ok(()),\n                Some(Err(err)) => Err(err),\n                None => Err(api::Error::internal(anyhow::anyhow!(\n                    \"subscription handler did not respond\",\n                ))),\n            }\n        })\n    }\n}\n\n#[derive(Debug, Clone, Copy)]\nstruct SubscriptionPromiseHandler;\n\nimpl PromiseHandler for SubscriptionPromiseHandler {\n    type Output = Result<(), api::Error>;\n\n    fn resolve(&self, _: Env, _: Option<JsUnknown>) -> Self::Output {\n        Ok(())\n    }\n\n    fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output {\n        Err(coerce_to_api_error(env, val)?)\n    }\n\n    fn error(&self, _: Env, err: Error) -> Self::Output {\n        Err(api::Error {\n            code: api::ErrCode::Internal,\n            message: api::ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(err.to_string()),\n            stack: None,\n            details: None,\n        })\n    }\n}\n\nfn resolve_on_js_thread(ctx: ThreadSafeCallContext<PubSubMessageRequest>) -> napi::Result<()> {\n    let handler = SubscriptionPromiseHandler;\n    let req = ctx.value.req.into_instance(ctx.env)?;\n    match ctx.callback.unwrap().call(None, &[req]) {\n        Ok(result) => {\n            await_promise(ctx.env, result, ctx.value.tx.clone(), handler);\n            Ok(())\n        }\n        Err(err) => {\n            let res = handler.error(ctx.env, err);\n            _ = ctx.value.tx.send(res);\n            Ok(())\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/pvalue.rs",
    "content": "use std::{str::FromStr, sync::Arc};\n\nuse crate::cookies::{cookie_to_napi_value, JsCookie};\nuse crate::runtime::Runtime;\nuse chrono::TimeZone;\nuse encore_runtime_core::api::{self, auth, schema, Decimal, PValue, PValues};\nuse malachite::{rational::Rational, Integer, Natural};\nuse napi::{\n    bindgen_prelude::*, sys, JsDate, JsObject, JsString, JsUnknown, NapiRaw, NapiValue, Result,\n};\nuse serde_json::Number;\n\n#[allow(dead_code)]\npub fn parse_pvalue(val: JsUnknown) -> Result<PValue> {\n    let val = PVal::from_unknown(val)?;\n    Ok(val.0)\n}\n\npub fn parse_pvalues(val: JsUnknown) -> Result<Option<PValues>> {\n    if val\n        .get_type()\n        .is_ok_and(|t| t == napi::ValueType::Undefined || t == napi::ValueType::Null)\n    {\n        return Ok(None);\n    }\n\n    let val = PVals::from_unknown(val)?;\n    Ok(Some(val.0))\n}\n\n#[allow(dead_code)]\npub fn pvalue_to_js(env: Env, val: &PValue) -> Result<JsUnknown> {\n    let env = env.raw();\n\n    unsafe {\n        // Safety: the memory representation is the same for &PValue and &PVal.\n        let pv = std::mem::transmute::<&PValue, &PVal>(val);\n        let val = ToNapiValue::to_napi_value(env, pv)?;\n        JsUnknown::from_raw(env, val)\n    }\n}\n\n#[allow(dead_code)]\npub fn pvalues_to_js(env: Env, val: &PValues) -> Result<JsUnknown> {\n    let env = env.raw();\n    unsafe {\n        let pv = std::mem::transmute::<&PValues, &PVals>(val);\n        let val = ToNapiValue::to_napi_value(env, pv)?;\n        JsUnknown::from_raw(env, val)\n    }\n}\n\n// transforms vals according to the response schema\npub fn transform_pvalues_response(\n    mut vals: PValues,\n    schema: Arc<schema::Response>,\n) -> Result<PValues> {\n    if let Some(cookie_schema) = &schema.cookie {\n        for (key, field) in cookie_schema.fields() {\n            if let Some(PValue::Object(obj)) = vals.get(key) {\n                let cookie_name = field.name_override.as_deref().unwrap_or(key.as_ref());\n                let cookie_value = obj.get(\"value\").ok_or(Error::new(\n                    Status::InvalidArg,\n                    \"cookie requires a value\".to_owned(),\n                ))?;\n\n                let cookie = JsCookie::parse_cookie(obj, cookie_name, cookie_value)?;\n                vals.insert(key.to_string(), PValue::Cookie(cookie));\n            }\n        }\n    }\n\n    Ok(vals)\n}\n\n// transforms vals according to the request schema\npub fn transform_pvalues_request(\n    mut vals: PValues,\n    schema: Arc<schema::Request>,\n) -> Result<PValues> {\n    if let Some(cookie_schema) = &schema.cookie {\n        for (key, field) in cookie_schema.fields() {\n            if let Some(PValue::Object(obj)) = vals.get(key) {\n                let cookie_name = field.name_override.as_deref().unwrap_or(key.as_ref());\n                let cookie_value = obj.get(\"value\").ok_or(Error::new(\n                    Status::InvalidArg,\n                    \"cookie requires a value\".to_owned(),\n                ))?;\n\n                let cookie = JsCookie::parse_cookie(obj, cookie_name, cookie_value)?;\n                vals.insert(key.to_string(), PValue::Cookie(cookie));\n            }\n        }\n    }\n\n    Ok(vals)\n}\n\n#[derive(Clone, Debug)]\npub struct PVal(pub PValue);\n#[derive(Clone, Debug)]\npub struct PVals(pub PValues);\n\nimpl ToNapiValue for PVal {\n    unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> {\n        match val.0 {\n            PValue::Null => unsafe { Null::to_napi_value(env, Null) },\n            PValue::Bool(b) => unsafe { bool::to_napi_value(env, b) },\n            PValue::Number(n) => unsafe { Number::to_napi_value(env, n.to_owned()) },\n            PValue::Decimal(d) => {\n                let env2 = Env::from_raw(env);\n                let decimal_js = Runtime::create_decimal(env2, &d.to_string())?;\n                unsafe { Ok(decimal_js.raw()) }\n            }\n            PValue::String(s) => unsafe { ToNapiValue::to_napi_value(env, s) },\n            PValue::Array(arr) => {\n                let env2 = Env::from_raw(env);\n                let mut out = env2.create_array(arr.len() as u32)?;\n\n                for (i, v) in arr.into_iter().enumerate() {\n                    out.set(i as u32, PVal(v))?;\n                }\n\n                unsafe { Array::to_napi_value(env, out) }\n            }\n            PValue::Object(obj) => unsafe { ToNapiValue::to_napi_value(env, PVals(obj)) },\n            PValue::Cookie(c) => crate::cookies::cookie_to_napi_value(env, c),\n            PValue::DateTime(dt) => {\n                let env2 = Env::from_raw(env);\n                let ts = dt.timestamp_millis();\n                let dt = env2.create_date(ts as f64)?;\n                JsDate::to_napi_value(env, dt)\n            }\n        }\n    }\n}\n\nimpl ToNapiValue for &PVal {\n    unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> {\n        match &val.0 {\n            PValue::Null => unsafe { Null::to_napi_value(env, Null) },\n            PValue::Bool(b) => unsafe { bool::to_napi_value(env, *b) },\n            PValue::Number(n) => unsafe { Number::to_napi_value(env, n.to_owned()) },\n            PValue::Decimal(d) => {\n                let env2 = Env::from_raw(env);\n                let decimal_js = Runtime::create_decimal(env2, &d.to_string())?;\n                unsafe { Ok(decimal_js.raw()) }\n            }\n            PValue::String(s) => unsafe { ToNapiValue::to_napi_value(env, s) },\n            PValue::Array(arr) => {\n                let env2 = Env::from_raw(env);\n                let mut out = env2.create_array(arr.len() as u32)?;\n\n                for (i, v) in arr.iter().enumerate() {\n                    // Safety: the memory representation is the same for &PValue and &PVal.\n                    let pv = unsafe { std::mem::transmute::<&PValue, &PVal>(v) };\n                    out.set(i as u32, pv)?;\n                }\n\n                unsafe { Array::to_napi_value(env, out) }\n            }\n            PValue::Object(obj) => unsafe {\n                // Safety: the memory representation is the same for &PValue and &PVal.\n                let pv = std::mem::transmute::<&PValues, &PVals>(obj);\n                ToNapiValue::to_napi_value(env, pv)\n            },\n            PValue::Cookie(c) => cookie_to_napi_value(env, c.clone()),\n            PValue::DateTime(dt) => {\n                let env2 = Env::from_raw(env);\n                let ts = dt.timestamp_millis();\n                let dt = env2.create_date(ts as f64)?;\n                JsDate::to_napi_value(env, dt)\n            }\n        }\n    }\n}\n\nimpl FromNapiValue for PVal {\n    unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {\n        let ty = type_of!(env, napi_val)?;\n        let val = PVal(match ty {\n            ValueType::Boolean => PValue::Bool(unsafe { bool::from_napi_value(env, napi_val)? }),\n            ValueType::Number => PValue::Number(unsafe { Number::from_napi_value(env, napi_val)? }),\n            ValueType::String => PValue::String(unsafe { String::from_napi_value(env, napi_val)? }),\n            ValueType::Object => {\n                let mut is_arr = false;\n                check_status!(\n                    unsafe { sys::napi_is_array(env, napi_val, &mut is_arr) },\n                    \"Failed to detect whether given js is an array\"\n                )?;\n\n                if is_arr {\n                    PValue::Array(unsafe {\n                        let vals = Vec::<PVal>::from_napi_value(env, napi_val)?;\n                        // Transmute Vec<PVal> to Vec<PValue> since that's what PValue::Array expects.\n                        // This is safe because PVal is a newtype around PValue.\n                        std::mem::transmute::<Vec<PVal>, Vec<PValue>>(vals)\n                    })\n                } else {\n                    // Is it a date?\n                    let mut is_date = false;\n                    check_status!(\n                        unsafe { sys::napi_is_date(env, napi_val, &mut is_date) },\n                        \"Failed to detect whether given js is a date\"\n                    )?;\n                    if is_date {\n                        let dt = JsDate::from_napi_value(env, napi_val)?;\n                        let millis = dt.value_of()?;\n                        let ts = timestamp_to_dt(millis);\n                        PValue::DateTime(ts)\n                    } else {\n                        // Check if it's a Decimal instance by checking for __encore_decimal property\n                        let obj = JsObject::from_napi_value(env, napi_val)?;\n\n                        if obj.has_property(\"__encore_decimal\")? {\n                            let value_str = obj\n                                .get::<&str, JsString>(\"value\")?\n                                .ok_or_else(|| {\n                                    Error::new(\n                                        Status::InvalidArg,\n                                        \"Decimal object missing 'value' property\".to_owned(),\n                                    )\n                                })?\n                                .into_utf8()?\n                                .into_owned()?;\n                            PValue::Decimal(Decimal::from_str(&value_str).map_err(|e| {\n                                Error::new(\n                                    Status::InvalidArg,\n                                    format!(\"Failed to parse Decimal value: {}\", e),\n                                )\n                            })?)\n                        } else {\n                            PValue::Object(unsafe { PVals::from_napi_value(env, napi_val)?.0 })\n                        }\n                    }\n                }\n            }\n            ValueType::BigInt => {\n                let bi = BigInt::from_napi_value(env, napi_val)?;\n                let n = Natural::from_owned_limbs_asc(bi.words);\n                let i = Integer::from_sign_and_abs(!bi.sign_bit, n);\n                let r = Rational::from_integers(i, 1.into());\n                PValue::Decimal(r.into())\n            }\n            ValueType::Null => PValue::Null,\n            ValueType::Function => {\n                return Err(Error::new(\n                    Status::InvalidArg,\n                    \"JS functions cannot be represented as a serde_json::Value\".to_owned(),\n                ))\n            }\n            ValueType::Undefined => {\n                return Err(Error::new(\n                    Status::InvalidArg,\n                    \"undefined cannot be represented as a serde_json::Value\".to_owned(),\n                ))\n            }\n            ValueType::Symbol => {\n                return Err(Error::new(\n                    Status::InvalidArg,\n                    \"JS symbols cannot be represented as a serde_json::Value\".to_owned(),\n                ))\n            }\n            ValueType::External => {\n                return Err(Error::new(\n                    Status::InvalidArg,\n                    \"External JS objects cannot be represented as a serde_json::Value\".to_owned(),\n                ))\n            }\n            _ => {\n                return Err(Error::new(\n                    Status::InvalidArg,\n                    \"Unknown JS variables cannot be represented as a serde_json::Value\".to_owned(),\n                ))\n            }\n        });\n\n        Ok(val)\n    }\n}\n\nimpl ToNapiValue for PVals {\n    unsafe fn to_napi_value(raw_env: sys::napi_env, val: Self) -> Result<sys::napi_value> {\n        let env = Env::from_raw(raw_env);\n        let mut obj = env.create_object()?;\n\n        for (k, v) in val.0.into_iter() {\n            obj.set(k, PVal(v))?;\n        }\n\n        unsafe { JsObject::to_napi_value(raw_env, obj) }\n    }\n}\n\nimpl ToNapiValue for &PVals {\n    unsafe fn to_napi_value(raw_env: sys::napi_env, val: Self) -> Result<sys::napi_value> {\n        let env = Env::from_raw(raw_env);\n        let mut obj = env.create_object()?;\n\n        for (k, v) in val.0.iter() {\n            // Safety: the memory representation is the same for &PValue and &PVal.\n            let pv = unsafe { std::mem::transmute::<&PValue, &PVal>(v) };\n            obj.set(k, pv)?;\n        }\n\n        unsafe { JsObject::to_napi_value(raw_env, obj) }\n    }\n}\n\nimpl FromNapiValue for PVals {\n    unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {\n        assert_type_of!(env, napi_val, ValueType::Object)?;\n        let obj = JsObject::from_napi_value(env, napi_val)?;\n\n        let mut map = PVals(PValues::new());\n        for key in JsObject::keys(&obj)?.into_iter() {\n            if let Some(val) = obj_get_pval(env, napi_val, &key)? {\n                map.0.insert(key, val);\n            }\n        }\n\n        Ok(map)\n    }\n}\n\npub fn encode_request_payload(\n    env: Env,\n    p: Option<&api::RequestPayload>,\n) -> napi::Result<JsUnknown> {\n    let Some(p) = p else {\n        return Ok(env.get_null()?.into_unknown());\n    };\n\n    let mut obj = env.create_object()?;\n\n    add_fields_to_obj(&mut obj, p.path.as_ref())?;\n    add_fields_to_obj(&mut obj, p.query.as_ref())?;\n    add_fields_to_obj(&mut obj, p.header.as_ref())?;\n    add_fields_to_obj(&mut obj, p.cookie.as_ref())?;\n\n    match &p.body {\n        api::Body::Typed(typed) => add_fields_to_obj(&mut obj, typed.as_ref())?,\n        api::Body::Raw(_) => {}\n    }\n\n    Ok(obj.into_unknown())\n}\n\npub fn encode_auth_payload(env: Env, p: &auth::AuthPayload) -> napi::Result<JsUnknown> {\n    let mut obj = env.create_object()?;\n    add_fields_to_obj(&mut obj, p.query.as_ref())?;\n    add_fields_to_obj(&mut obj, p.header.as_ref())?;\n    add_fields_to_obj(&mut obj, p.cookie.as_ref())?;\n    Ok(obj.into_unknown())\n}\n\npub fn pvalues_or_null(env: Env, vals: Option<&PValues>) -> napi::Result<JsUnknown> {\n    vals.map_or_else(\n        || env.get_null().map(|v| v.into_unknown()),\n        |v| pvalues_to_js(env, v),\n    )\n}\n\nfn add_fields_to_obj<'a, I: IntoIterator<Item = (&'a String, &'a PValue)>>(\n    obj: &'a mut JsObject,\n    vals: Option<I>,\n) -> napi::Result<()> {\n    let Some(vals) = vals else {\n        return Ok(());\n    };\n\n    for (k, v) in vals.into_iter() {\n        let pv = unsafe { std::mem::transmute::<&PValue, &PVal>(v) };\n        obj.set(k, pv)?;\n    }\n    Ok(())\n}\n\nfn timestamp_to_dt(millis: f64) -> chrono::DateTime<chrono::FixedOffset> {\n    let millis = millis.trunc() as i64;\n    let secs = millis / 1000;\n    let nanos = (millis % 1000) * 1_000_000;\n    let ts = chrono::Utc.timestamp_opt(secs, nanos as u32);\n    ts.unwrap().fixed_offset()\n}\n\n// This is an inlined version of JsObject::get that distinguishes between null and undefined.\nfn obj_get_pval<K: AsRef<str>>(\n    env: sys::napi_env,\n    obj: sys::napi_value,\n    field: K,\n) -> Result<Option<PValue>> {\n    let c_field = std::ffi::CString::new(field.as_ref())?;\n\n    unsafe {\n        let mut ret = std::ptr::null_mut();\n\n        check_status!(\n            sys::napi_get_named_property(env, obj, c_field.as_ptr(), &mut ret),\n            \"Failed to get property with field `{}`\",\n            field.as_ref(),\n        )?;\n\n        let ty = type_of!(env, ret)?;\n\n        if ty == ValueType::Undefined {\n            return Ok(None);\n        }\n\n        let pval = PVal::from_napi_value(env, ret)?;\n        Ok(Some(pval.0))\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/raw_api.rs",
    "content": "#![allow(clippy::result_large_err)]\n\nuse std::collections::HashMap;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nuse axum::body::Body;\nuse axum::http::{Response, StatusCode};\nuse bytes::Bytes;\nuse napi::bindgen_prelude::{Buffer, Either3};\nuse napi::{Either, Env, JsFunction, JsUnknown, NapiRaw};\nuse napi_derive::napi;\nuse tokio::sync::{mpsc, oneshot};\n\nuse encore_runtime_core::api::{self, ToResponse};\n\nuse crate::api::Request;\nuse crate::error::coerce_to_api_error;\nuse crate::napi_util::{await_promise, PromiseHandler};\nuse crate::stream;\nuse crate::threadsafe_function::{\n    ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,\n};\n\npub struct JSRawHandler {\n    handler: ThreadsafeFunction<RawRequestMessage>,\n}\n\npub fn new_handler(env: Env, func: JsFunction) -> napi::Result<Arc<dyn api::BoxedHandler>> {\n    let handler = ThreadsafeFunction::create(\n        env.raw(),\n        // SAFETY: `handler` is a valid JS function.\n        unsafe { func.raw() },\n        0,\n        raw_resolve_on_js_thread,\n    )?;\n    Ok(Arc::new(JSRawHandler { handler }))\n}\n\nstruct RawRequestMessage {\n    req: Request,\n    resp: ResponseWriter,\n    body: BodyReader,\n    err_tx: mpsc::UnboundedSender<Result<(), api::Error>>,\n}\n\n#[derive(Debug)]\nenum ResponseWriterState {\n    Initial {\n        resp: axum::http::response::Builder,\n        sender: oneshot::Sender<Response<Body>>,\n    },\n    StreamingBody {\n        write: stream::write::WriteHalf,\n    },\n    Done,\n}\n\nimpl ResponseWriterState {\n    pub fn new(sender: oneshot::Sender<Response<Body>>) -> Self {\n        let resp = axum::response::Response::builder().status(StatusCode::OK);\n        Self::Initial { resp, sender }\n    }\n\n    pub fn set_head(\n        self,\n        status: u16,\n        headers: axum::http::HeaderMap,\n    ) -> Result<Self, (Self, anyhow::Error)> {\n        let status = match StatusCode::from_u16(status) {\n            Ok(status) => status,\n            Err(err) => return Err((self, err.into())),\n        };\n\n        match self {\n            Self::Initial { mut resp, sender } => {\n                resp = resp.status(status);\n                for (k, v) in headers.iter() {\n                    resp = resp.header(k, v);\n                }\n                Ok(Self::Initial { resp, sender })\n            }\n            _ => Ok(self),\n        }\n    }\n\n    pub fn flush_header(self) -> Result<Self, (Self, anyhow::Error)> {\n        match self {\n            Self::Initial { resp, sender } => {\n                let (write, read) = stream::write::new();\n                let read = tokio_util::io::ReaderStream::new(read);\n\n                let resp = match resp.body(Body::from_stream(read)) {\n                    Ok(resp) => resp,\n                    Err(err) => {\n                        ::log::error!(err = err.to_string(); \"failed to set raw response body to flush header\");\n                        return Err((Self::Done, err.into()));\n                    }\n                };\n\n                let _ = sender.send(resp);\n                Ok(Self::StreamingBody { write })\n            }\n            _ => Ok(self),\n        }\n    }\n\n    pub fn close(\n        self,\n        env: Env,\n        buf: Option<Bytes>,\n        callback: Option<JsFunction>,\n    ) -> Result<Self, (Self, anyhow::Error)> {\n        match self {\n            Self::Initial { resp, sender } => {\n                let body = match buf {\n                    Some(buf) => Body::from(buf),\n                    None => Body::empty(),\n                };\n                let resp = match resp.body(body) {\n                    Ok(resp) => resp,\n                    Err(err) => {\n                        ::log::error!(err = err.to_string(); \"failed to close raw response body\");\n                        return Err((Self::Done, err.into()));\n                    }\n                };\n                let _ = sender.send(resp);\n                Ok(Self::Done)\n            }\n            Self::StreamingBody { mut write } => {\n                if let Some(buf) = buf {\n                    write.write(buf, None);\n                }\n\n                let tx = match to_sender(env, callback) {\n                    Ok(tx) => tx,\n                    Err(err) => return Err((Self::StreamingBody { write }, err.into())),\n                };\n                write.end(tx);\n\n                Ok(Self::Done)\n            }\n            Self::Done => Ok(self),\n        }\n    }\n\n    pub fn write_body(\n        self,\n        env: Env,\n        buf: Bytes,\n        callback: Option<JsFunction>,\n    ) -> Result<Self, (Self, anyhow::Error)> {\n        self.write_body_multi(env, vec![buf], callback)\n    }\n\n    pub fn write_body_multi(\n        mut self,\n        env: Env,\n        bufs: Vec<Bytes>,\n        callback: Option<JsFunction>,\n    ) -> Result<Self, (Self, anyhow::Error)> {\n        self = self.flush_header_if_needed();\n\n        match self {\n            Self::StreamingBody { mut write } => {\n                let tx = match to_sender(env, callback) {\n                    Ok(tx) => tx,\n                    Err(err) => return Err((Self::StreamingBody { write }, err.into())),\n                };\n                write.writev(bufs, tx);\n                Ok(Self::StreamingBody { write })\n            }\n            _ => Ok(self),\n        }\n    }\n\n    fn flush_header_if_needed(self) -> Self {\n        match self {\n            Self::Initial { .. } => match self.flush_header() {\n                Ok(state) => state,\n                Err((state, _)) => state,\n            },\n            _ => self,\n        }\n    }\n}\n\nfn to_sender(env: Env, callback: Option<JsFunction>) -> napi::Result<Option<oneshot::Sender<()>>> {\n    let Some(callback) = callback else {\n        return Ok(None);\n    };\n    let (tx, rx) = oneshot::channel::<()>();\n\n    let mut callback = env.create_reference(callback)?;\n    let fut = async move {\n        _ = rx.await;\n        Ok(())\n    };\n    env.execute_tokio_future(fut, move |&mut env, _| {\n        let cb: JsFunction = env.get_reference_value(&callback)?;\n        callback.unref(env)?;\n        cb.call_without_args(None)?;\n        Ok(())\n    })?;\n    Ok(Some(tx))\n}\n\n#[napi]\n#[derive(Debug)]\npub struct ResponseWriter {\n    // Option to support moving out of self.\n    state: Option<ResponseWriterState>,\n}\n\n#[napi]\nimpl ResponseWriter {\n    #[napi]\n    #[allow(clippy::type_complexity)]\n    pub fn write_head(\n        &mut self,\n        status: u16,\n        headers: Either<Vec<String>, HashMap<String, Either3<String, i32, Vec<String>>>>,\n    ) -> napi::Result<()> {\n        let Some(state) = self.state.take() else {\n            return Err(napi::Error::new(\n                napi::Status::GenericFailure,\n                \"missing state\".to_string(),\n            ));\n        };\n\n        ::log::trace!(\"write_head: status={}, headers={:?}\", status, headers);\n        let headers = parse_headers(headers)?;\n\n        let (state, result) = match state.set_head(status, headers) {\n            Ok(state) => (state, Ok(())),\n            Err((state, err)) => (state, Err(err)),\n        };\n        self.state = Some(state);\n        result.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.to_string()))\n    }\n\n    #[napi]\n    pub fn write_body(\n        &mut self,\n        env: Env,\n        buf: Buffer,\n        callback: Option<JsFunction>,\n    ) -> napi::Result<()> {\n        let Some(state) = self.state.take() else {\n            return Err(napi::Error::new(\n                napi::Status::GenericFailure,\n                \"missing state\".to_string(),\n            ));\n        };\n\n        ::log::trace!(\"write_body: self={:#?}\", self);\n\n        let buf = Bytes::from(buf.to_vec());\n        let (state, result) = match state.write_body(env, buf, callback) {\n            Ok(state) => (state, Ok(())),\n            Err((state, err)) => (state, Err(err)),\n        };\n        self.state = Some(state);\n        result.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.to_string()))\n    }\n\n    #[napi]\n    pub fn write_body_multi(\n        &mut self,\n        env: Env,\n        bufs: Vec<Buffer>,\n        callback: Option<JsFunction>,\n    ) -> napi::Result<()> {\n        ::log::trace!(\"write_body_body: self={:#?}\", self);\n        let Some(state) = self.state.take() else {\n            return Err(napi::Error::new(\n                napi::Status::GenericFailure,\n                \"missing state\".to_string(),\n            ));\n        };\n\n        let bufs: Vec<_> = bufs\n            .into_iter()\n            .map(|buf| Bytes::from(buf.to_vec()))\n            .collect();\n        let (state, result) = match state.write_body_multi(env, bufs, callback) {\n            Ok(state) => (state, Ok(())),\n            Err((state, err)) => (state, Err(err)),\n        };\n        self.state = Some(state);\n        result.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.to_string()))\n    }\n\n    #[napi]\n    pub fn close(\n        &mut self,\n        env: Env,\n        buf: Option<Buffer>,\n        callback: Option<JsFunction>,\n    ) -> napi::Result<()> {\n        ::log::trace!(\"close: self={:#?}\", self);\n        let Some(state) = self.state.take() else {\n            return Err(napi::Error::new(\n                napi::Status::GenericFailure,\n                \"missing state\".to_string(),\n            ));\n        };\n\n        let buf = buf.map(|buf| Bytes::from(buf.to_vec()));\n        let (state, result) = match state.close(env, buf, callback) {\n            Ok(state) => (state, Ok(())),\n            Err((state, err)) => (state, Err(err)),\n        };\n        self.state = Some(state);\n        result.map_err(|err| napi::Error::new(napi::Status::GenericFailure, err.to_string()))\n    }\n}\n\n#[napi]\npub struct BodyReader {\n    reader: stream::read::Reader<axum::body::BodyDataStream>,\n}\n\n#[napi]\nimpl BodyReader {\n    pub fn new(body: axum::body::BodyDataStream) -> Self {\n        Self {\n            reader: stream::read::Reader::new(body),\n        }\n    }\n\n    #[napi]\n    pub fn start(&mut self, env: Env, push: JsFunction, destroy: JsFunction) -> napi::Result<()> {\n        self.reader.start(env, push, destroy)\n    }\n\n    #[napi]\n    pub fn read(&self) -> napi::Result<()> {\n        self.reader.read()\n    }\n}\n\nimpl api::BoxedHandler for JSRawHandler {\n    fn call(\n        self: Arc<Self>,\n        req: api::HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = api::ResponseData> + Send + 'static>> {\n        Box::pin(async move {\n            let (body_tx, mut body_rx) = oneshot::channel();\n\n            let internal_caller = req.internal_caller.clone();\n\n            let Some(body) = req.take_raw_body() else {\n                let err = api::Error::internal(anyhow::anyhow!(\"missing body\"));\n                return api::ResponseData::Raw(err.to_response(internal_caller));\n            };\n\n            // Call the handler.\n            let req = Request::new(req);\n            let resp = ResponseWriter {\n                state: Some(ResponseWriterState::new(body_tx)),\n            };\n            let body = BodyReader::new(body.into_data_stream());\n\n            let (err_tx, mut err_rx) = mpsc::unbounded_channel();\n\n            self.handler.call(\n                RawRequestMessage {\n                    req,\n                    resp,\n                    body,\n                    err_tx,\n                },\n                ThreadsafeFunctionCallMode::Blocking,\n            );\n\n            // Wait for a response body on body_rx, or an error to be received on err_rx.\n            let resp = tokio::select! {\n                resp = &mut body_rx => {\n                    match resp {\n                        Ok(resp) => resp,\n                        Err(_) => {\n                            let err_resp = api::Error::internal(anyhow::anyhow!(\"handler did not respond\"));\n                            err_resp.to_response(internal_caller)\n                        }\n                    }\n                }\n                err = err_rx.recv() => {\n                    match err {\n                        Some(Err(err)) => err.to_response(internal_caller),\n                        _ => {\n                            // We didn't get an error. Wait for the response body instead.\n                            match body_rx.await {\n                                Ok(resp) => resp,\n                                Err(_) => {\n                                    let err_resp = api::Error::internal(anyhow::anyhow!(\"handler did not respond\"));\n                                    err_resp.to_response(internal_caller)\n                                }\n                            }\n                        }\n                    }\n                }\n            };\n\n            api::ResponseData::Raw(resp)\n        })\n    }\n}\n\n#[allow(clippy::type_complexity)]\nfn parse_headers(\n    headers: Either<Vec<String>, HashMap<String, Either3<String, i32, Vec<String>>>>,\n) -> napi::Result<axum::http::HeaderMap> {\n    fn key_err(err: axum::http::header::InvalidHeaderName) -> napi::Error {\n        napi::Error::new(napi::Status::GenericFailure, err.to_string())\n    }\n    fn val_err(err: axum::http::header::InvalidHeaderValue) -> napi::Error {\n        napi::Error::new(napi::Status::GenericFailure, err.to_string())\n    }\n\n    let mut map = axum::http::HeaderMap::new();\n    match headers {\n        Either::A(headers) => {\n            for i in (0..headers.len()).step_by(2) {\n                let key: axum::http::HeaderName = headers[i].parse().map_err(key_err)?;\n                let value = &headers[i + 1];\n                let value: axum::http::HeaderValue = value.parse().map_err(val_err)?;\n                map.append(key, value);\n            }\n        }\n\n        Either::B(headers) => {\n            for (key, value) in headers {\n                let key: axum::http::HeaderName = key.parse().map_err(key_err)?;\n                match value {\n                    Either3::A(value) => {\n                        let value: axum::http::HeaderValue = value.parse().map_err(val_err)?;\n                        map.append(key, value);\n                    }\n                    Either3::B(value) => {\n                        let value: axum::http::HeaderValue =\n                            value.to_string().parse().map_err(val_err)?;\n                        map.append(key, value);\n                    }\n                    Either3::C(values) => {\n                        for value in values {\n                            let value: axum::http::HeaderValue = value.parse().map_err(val_err)?;\n                            map.append(key.clone(), value);\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Ok(map)\n}\n\nfn raw_resolve_on_js_thread(ctx: ThreadSafeCallContext<RawRequestMessage>) -> napi::Result<()> {\n    let req = ctx.value.req.into_instance(ctx.env)?;\n    let resp = ctx.value.resp.into_instance(ctx.env)?;\n    let body = ctx.value.body.into_instance(ctx.env)?;\n    let req = req.as_object(ctx.env);\n    let resp = resp.as_object(ctx.env);\n    let body = body.as_object(ctx.env);\n\n    let handler = RawPromiseHandler;\n    match ctx.callback.unwrap().call(None, &[req, resp, body]) {\n        Ok(result) => {\n            await_promise(ctx.env, result, ctx.value.err_tx.clone(), handler);\n            Ok(())\n        }\n        Err(err) => {\n            let res = handler.error(ctx.env, err);\n            _ = ctx.value.err_tx.send(res);\n            Ok(())\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy)]\nstruct RawPromiseHandler;\n\nimpl PromiseHandler for RawPromiseHandler {\n    type Output = Result<(), api::Error>;\n\n    fn resolve(&self, _env: Env, _val: Option<JsUnknown>) -> Self::Output {\n        Ok(())\n    }\n\n    fn reject(&self, env: Env, val: napi::JsUnknown) -> Self::Output {\n        Err(coerce_to_api_error(env, val)?)\n    }\n\n    fn error(&self, _: Env, err: napi::Error) -> Self::Output {\n        Err(api::Error {\n            code: api::ErrCode::Internal,\n            message: api::ErrCode::Internal.default_public_message().into(),\n            internal_message: Some(err.to_string()),\n            stack: None,\n            details: None,\n        })\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/request_meta.rs",
    "content": "use chrono::{DateTime, SecondsFormat, Utc};\nuse napi_derive::napi;\n\nuse encore_runtime_core::{api::reqauth::meta::HeaderValueExt, model};\n\nuse crate::pvalue::PVals;\n\npub fn meta(req: &model::Request) -> Result<RequestMeta, serde_json::Error> {\n    let dt: DateTime<Utc> = req.start_time.into();\n    let started_at = dt.to_rfc3339_opts(SecondsFormat::Secs, true);\n\n    let (api_call, pubsub_message) = match &req.data {\n        model::RequestData::RPC(rpc) => {\n            let api = APICallData {\n                api: APIDesc {\n                    service: rpc.endpoint.name.service().to_string(),\n                    endpoint: rpc.endpoint.name.endpoint().to_string(),\n                    raw: rpc.endpoint.raw,\n                    requires_auth: rpc.endpoint.requires_auth,\n                    tags: rpc.endpoint.tags.clone(),\n                },\n                method: rpc.method.as_str().to_string(),\n                path: rpc.path.clone(),\n                path_and_query: rpc.path_and_query.clone(),\n                path_params: rpc\n                    .path_params\n                    .as_ref()\n                    .map(serde_json::to_value)\n                    .transpose()?,\n                parsed_payload: rpc\n                    .parsed_payload\n                    .as_ref()\n                    .map(serde_json::to_value)\n                    .transpose()?,\n                headers: serialize_headers(&rpc.req_headers),\n            };\n            (Some(api), None)\n        }\n\n        model::RequestData::Stream(data) => {\n            let api = APICallData {\n                api: APIDesc {\n                    service: data.endpoint.name.service().to_string(),\n                    endpoint: data.endpoint.name.endpoint().to_string(),\n                    raw: data.endpoint.raw,\n                    requires_auth: data.endpoint.requires_auth,\n                    tags: data.endpoint.tags.clone(),\n                },\n                method: Default::default(),\n                path: data.path.clone(),\n                path_and_query: data.path_and_query.clone(),\n                path_params: data\n                    .path_params\n                    .as_ref()\n                    .map(serde_json::to_value)\n                    .transpose()?,\n                parsed_payload: data\n                    .parsed_payload\n                    .as_ref()\n                    .map(serde_json::to_value)\n                    .transpose()?,\n                headers: Default::default(),\n            };\n            (Some(api), None)\n        }\n        model::RequestData::PubSub(msg) => {\n            let pubsub_message = PubSubMessageData {\n                service: msg.service.to_string(),\n                topic: msg.topic.to_string(),\n                subscription: msg.subscription.to_string(),\n                id: msg.message_id.clone(),\n                published_at: msg.published.to_rfc3339_opts(SecondsFormat::Secs, true),\n                delivery_attempt: msg.attempt,\n                parsed_payload: msg.parsed_payload.as_ref().map(|pv| PVals(pv.clone())),\n            };\n            (None, Some(pubsub_message))\n        }\n        model::RequestData::Auth(_) => (None, None),\n    };\n\n    let trace = Some(TraceData {\n        trace_id: req.span.0.serialize_encore(),\n        span_id: req.span.1.serialize_encore(),\n        parent_trace_id: req.parent_trace.map(|id| id.serialize_encore()),\n        parent_span_id: req.parent_span.map(|id| id.1.serialize_encore()),\n        ext_correlation_id: req.ext_correlation_id.clone(),\n    });\n\n    Ok(RequestMeta {\n        started_at,\n        trace,\n        api_call,\n        pubsub_message,\n    })\n}\n\n#[napi(object)]\npub struct RequestMeta {\n    pub started_at: String,\n    pub trace: Option<TraceData>,\n    pub api_call: Option<APICallData>,\n    pub pubsub_message: Option<PubSubMessageData>,\n}\n\n#[napi(object)]\npub struct APICallData {\n    pub api: APIDesc,\n    pub method: String,\n    pub path: String,\n    pub path_and_query: String,\n    pub path_params: Option<serde_json::Value>,\n    pub parsed_payload: Option<serde_json::Value>,\n    pub headers: serde_json::Map<String, serde_json::Value>,\n}\n\n#[napi(object)]\npub struct APIDesc {\n    pub service: String,\n    pub endpoint: String,\n    pub raw: bool,\n    pub requires_auth: bool,\n    pub tags: Vec<String>,\n}\n\n#[napi(object)]\npub struct PubSubMessageData {\n    pub service: String,\n    pub topic: String,\n    pub subscription: String,\n    pub id: String,\n    pub published_at: String,\n    pub delivery_attempt: u32,\n    pub parsed_payload: Option<PVals>,\n}\n\n#[napi(object)]\npub struct TraceData {\n    pub trace_id: String,\n    pub span_id: String,\n    pub parent_trace_id: Option<String>,\n    pub parent_span_id: Option<String>,\n    pub ext_correlation_id: Option<String>,\n}\n\nfn serialize_headers(\n    headers: &axum::http::HeaderMap,\n) -> serde_json::Map<String, serde_json::Value> {\n    use serde_json::{map::Entry, Map, Value};\n    let mut map = Map::with_capacity(headers.len());\n\n    for (k, v) in headers {\n        let Ok(v) = v.to_utf8_str() else {\n            continue;\n        };\n\n        // Skip Encore-internal headers.\n        if v.starts_with(\"x-encore-meta\") {\n            continue;\n        }\n\n        let v = Value::String(v.to_string());\n\n        // Insert the value as a string value if the entry does not yet exist.\n        // If it does exist, convert it to an array and append the new value.\n        match map.entry(k.as_str().to_string()) {\n            Entry::Vacant(entry) => {\n                entry.insert(v);\n            }\n\n            Entry::Occupied(entry) => {\n                let arr = entry.into_mut();\n                match arr {\n                    Value::String(s) => {\n                        let str = std::mem::replace(s, \"\".to_string());\n                        *arr = Value::Array(vec![Value::String(str), v]);\n                    }\n                    Value::Array(arr) => {\n                        arr.push(v);\n                    }\n                    _ => unreachable!(),\n                }\n            }\n        }\n    }\n\n    map\n}\n"
  },
  {
    "path": "runtimes/js/src/runtime.rs",
    "content": "use crate::api::{new_api_handler, APIRoute, Request};\nuse crate::cache::CacheCluster;\nuse crate::gateway::{Gateway, GatewayConfig};\nuse crate::log::Logger;\nuse crate::napi_util::EnvMap;\nuse crate::pubsub::{PubSubSubscription, PubSubSubscriptionConfig, PubSubTopic};\nuse crate::pvalue::{parse_pvalues, transform_pvalues_request, PVals};\nuse crate::secret::Secret;\nuse crate::sqldb::SQLDatabase;\nuse crate::{meta, objects, runtime_config, websocket_api};\nuse encore_runtime_core::api::{AuthOpts, PValues};\nuse encore_runtime_core::pubsub::SubName;\nuse encore_runtime_core::{api, EncoreName, EndpointName};\nuse napi::Ref;\nuse napi::{bindgen_prelude::*, JsFunction, JsObject};\nuse napi::{Error, JsUnknown, Status};\nuse napi_derive::napi;\nuse std::future::Future;\nuse std::str::FromStr;\nuse std::sync::{Arc, OnceLock};\nuse std::thread;\n\n// TODO: remove storing of result after `get_or_try_init` is stabilized\nstatic RUNTIME: OnceLock<napi::Result<Arc<encore_runtime_core::Runtime>>> = OnceLock::new();\n\n// Type constructors registered from javascript so we can create those type from rust\nstatic TYPE_CONSTRUCTORS: EnvMap<Arc<TypeConstructorRefs>> = EnvMap::new();\n\nstruct TypeConstructorRefs {\n    decimal: Ref<()>,\n}\n\n#[napi]\npub struct Runtime {\n    pub(crate) runtime: Arc<encore_runtime_core::Runtime>,\n}\n\n#[napi]\nimpl Runtime {\n    pub fn create_decimal(env: Env, val: &str) -> napi::Result<JsUnknown> {\n        let constructors = TYPE_CONSTRUCTORS.get(env).ok_or_else(|| {\n            Error::new(Status::GenericFailure, \"Type constructors not initialized\")\n        })?;\n\n        let constructor: JsFunction = env.get_reference_value(&constructors.decimal)?;\n        constructor.call(None, &[env.create_string(val)?])\n    }\n}\n\n#[napi(object)]\npub struct RuntimeTypeConstructors {\n    pub decimal: JsFunction,\n}\n\n#[napi(object)]\npub struct RuntimeOptions {\n    pub test_mode: Option<bool>,\n    pub type_constructors: RuntimeTypeConstructors,\n}\n\nfn init_runtime(test_mode: bool) -> napi::Result<encore_runtime_core::Runtime> {\n    // Initialize logging.\n    encore_runtime_core::log::init();\n\n    encore_runtime_core::Runtime::builder()\n        .with_test_mode(test_mode)\n        .with_meta_autodetect()\n        .with_runtime_config_from_env()\n        .build()\n        .map_err(|e| {\n            Error::new(\n                Status::GenericFailure,\n                format!(\"failed to initialize runtime: {e:?}\"),\n            )\n        })\n}\n\n#[napi]\nimpl Runtime {\n    #[napi(constructor)]\n    pub fn new(env: Env, options: RuntimeOptions) -> napi::Result<Self> {\n        let test_mode = options\n            .test_mode\n            .unwrap_or(std::env::var(\"NODE_ENV\").is_ok_and(|val| val == \"test\"));\n\n        TYPE_CONSTRUCTORS.get_or_init(env, || {\n            Arc::new(TypeConstructorRefs {\n                decimal: env\n                    .create_reference(options.type_constructors.decimal)\n                    .expect(\"couldn't create reference to Decimal\"),\n            })\n        });\n\n        if test_mode {\n            // Don't reuse the runtime in tests, as vitest and other test frameworks\n            // use multiple workers to isolate tests from each other. We don't want tests\n            // to be making API calls to other tests' workers.\n            let runtime = Arc::new(init_runtime(true)?);\n\n            // If we're running tests, there's no specific entrypoint so\n            // start the runtime in the background immediately.\n            {\n                let rt = runtime.clone();\n                thread::spawn(move || {\n                    rt.run_blocking();\n                });\n            }\n\n            return Ok(Self { runtime });\n        }\n\n        let runtime = RUNTIME\n            .get_or_init(|| Ok(Arc::new(init_runtime(false)?)))\n            .clone()?;\n\n        Ok(Self { runtime })\n    }\n\n    #[napi]\n    pub async fn run_forever(&self) {\n        let runtime = self.runtime.clone();\n        thread::spawn(move || {\n            runtime.run_blocking();\n        });\n\n        // Block the async function forever.\n        futures::future::pending::<()>().await;\n    }\n\n    #[napi]\n    pub fn sql_database(&self, encore_name: String) -> SQLDatabase {\n        let encore_name: encore_runtime_core::EncoreName = encore_name.into();\n        let db = self.runtime.sqldb().database(&encore_name);\n        SQLDatabase::new(db)\n    }\n\n    #[napi]\n    pub fn pubsub_topic(&self, encore_name: String) -> napi::Result<PubSubTopic> {\n        let topic = self\n            .runtime\n            .pubsub()\n            .topic(encore_name.into())\n            .ok_or_else(|| Error::new(Status::GenericFailure, \"topic not found\"))?;\n        Ok(PubSubTopic::new(topic))\n    }\n\n    #[napi]\n    pub fn bucket(&self, encore_name: String) -> napi::Result<objects::Bucket> {\n        let bkt = self\n            .runtime\n            .objects()\n            .bucket(encore_name.into())\n            .ok_or_else(|| Error::new(Status::GenericFailure, \"bucket not found\"))?;\n        Ok(objects::Bucket::new(bkt))\n    }\n\n    #[napi]\n    pub fn cache_cluster(&self, encore_name: String) -> napi::Result<CacheCluster> {\n        let cluster = self.runtime.cache().cluster(&encore_name.into());\n        CacheCluster::new(cluster)\n    }\n\n    #[napi]\n    pub fn gateway(\n        &self,\n        env: Env,\n        encore_name: String,\n        cfg: GatewayConfig,\n    ) -> napi::Result<Gateway> {\n        let name: EncoreName = encore_name.into();\n        let gw = self.runtime.api().gateway(&name).cloned();\n        Gateway::new(env, gw, cfg)\n    }\n\n    /// Gets the root logger from the runtime\n    #[napi]\n    pub fn logger(&self) -> Logger {\n        Logger::new()\n    }\n\n    #[napi]\n    pub fn pubsub_subscription(\n        &self,\n        env: Env,\n        cfg: PubSubSubscriptionConfig,\n    ) -> napi::Result<PubSubSubscription> {\n        let handler = Arc::new(cfg.to_handler(env)?);\n        let sub = self\n            .runtime\n            .pubsub()\n            .subscription(SubName {\n                topic: cfg.topic_name.into(),\n                subscription: cfg.subscription_name.into(),\n            })\n            .ok_or_else(|| Error::new(Status::GenericFailure, \"subscription not found\"))?;\n        Ok(PubSubSubscription::new(sub, handler))\n    }\n\n    #[napi]\n    pub fn register_handler(&self, env: Env, route: APIRoute) -> napi::Result<()> {\n        let endpoint_name = encore_runtime_core::EndpointName::new(route.service, route.name);\n\n        let eps = self.runtime.api().endpoints();\n        let resp_schema = eps.get(&endpoint_name).map(|ep| ep.response.clone());\n\n        let handler = new_api_handler(\n            env,\n            route.handler,\n            route.raw,\n            route.streaming_request || route.streaming_response,\n            resp_schema,\n        )?;\n\n        // If we're not hosting an API server, this is a no-op.\n        let Some(srv) = self.runtime.api().server() else {\n            return Ok(());\n        };\n\n        srv.register_handler(endpoint_name, handler).map_err(|e| {\n            Error::new(\n                Status::GenericFailure,\n                format!(\"failed to register handler: {e:?}\"),\n            )\n        })\n    }\n\n    #[napi]\n    pub fn register_test_handler(&self, env: Env, route: APIRoute) -> napi::Result<()> {\n        // Currently no difference between test and non-test handlers.\n        self.register_handler(env, route)\n    }\n\n    #[napi]\n    pub fn register_handlers(&self, env: Env, routes: Vec<APIRoute>) -> napi::Result<()> {\n        for route in routes {\n            self.register_handler(env, route)?;\n        }\n        Ok(())\n    }\n\n    #[napi]\n    pub fn secret(&self, encore_name: String) -> Option<Secret> {\n        self.runtime\n            .secrets()\n            .app_secret(encore_name.into())\n            .map(Secret::new)\n    }\n\n    #[napi(ts_return_type = \"Promise<Record<string, any> | null | ApiCallError>\")]\n    pub fn api_call(\n        &self,\n        env: Env,\n        service: String,\n        endpoint: String,\n        payload: Option<JsUnknown>,\n        source: Option<&Request>,\n        opts: Option<CallOpts>,\n    ) -> napi::Result<JsObject> {\n        let endpoint_name = encore_runtime_core::EndpointName::new(service, endpoint);\n\n        let eps = self.runtime.api().endpoints();\n        let req_schema = eps.get(&endpoint_name).map(|ep| ep.request[0].clone());\n\n        let payload = payload\n            .and_then(|p| parse_pvalues(p).transpose())\n            .transpose()?\n            .map(|payload| match req_schema {\n                Some(schema) => transform_pvalues_request(payload, schema)\n                    .map_err(|err| napi::Error::new(napi::Status::InvalidArg, err.to_string())),\n                None => Ok(payload),\n            })\n            .transpose()?;\n\n        let opts = opts.map(TryFrom::try_from).transpose()?;\n        let fut = self.do_api_call(endpoint_name, payload, source, opts);\n        let fut = async move {\n            let res: napi::Result<Either<Option<PValues>, APICallError>> = match fut.await {\n                Ok(data) => Ok(Either::A(data)),\n                Err(err) => Ok(Either::B(err.into())),\n            };\n            res\n        };\n\n        env.execute_tokio_future(fut, |&mut _env, res| {\n            Ok(match res {\n                Either::A(pvals) => Either::A(pvals.map(PVals)),\n                Either::B(err) => Either::B(err),\n            })\n        })\n    }\n\n    fn do_api_call<'a>(\n        &'a self,\n        endpoint: EndpointName,\n        payload: Option<PValues>,\n        source: Option<&'a Request>,\n        opts: Option<api::CallOpts>,\n    ) -> impl Future<Output = api::APIResult<Option<PValues>>> + 'static {\n        let source = source.map(|s| s.inner.clone());\n        let fut = self.runtime.api().call(endpoint, payload, source, opts);\n\n        async move {\n            let data = fut.await?;\n            Ok(match (data.header, data.body) {\n                (None, api::Body::Raw(_) | api::Body::Typed(None)) => None,\n                (None, api::Body::Typed(Some(body))) => Some(body),\n                (Some(header), api::Body::Raw(_) | api::Body::Typed(None)) => Some(header),\n                (Some(header), api::Body::Typed(Some(body))) => {\n                    let mut combined = header;\n                    combined.extend(body);\n                    Some(combined)\n                }\n            })\n        }\n    }\n\n    #[napi(ts_return_type = \"Promise<WebSocketClient>\")]\n    pub fn stream(\n        &self,\n        env: Env,\n        service: String,\n        endpoint: String,\n        payload: Option<JsUnknown>,\n        source: Option<&Request>,\n        opts: Option<CallOpts>,\n    ) -> napi::Result<JsObject> {\n        let payload = match payload {\n            Some(payload) => parse_pvalues(payload)?,\n            None => None,\n        };\n        let endpoint = encore_runtime_core::EndpointName::new(service, endpoint);\n        let source = source.map(|s| s.inner.clone());\n        let opts = opts.map(TryFrom::try_from).transpose()?;\n        let fut = self.runtime.api().stream(endpoint, payload, source, opts);\n\n        let fut = async move {\n            fut.await.map_err(|e| {\n                Error::new(\n                    Status::GenericFailure,\n                    format!(\"failed to make api call: {e:?}\"),\n                )\n            })\n        };\n\n        env.execute_tokio_future(fut, |&mut _env, res| {\n            Ok(websocket_api::WebSocketClient::new(res))\n        })\n    }\n\n    /// Returns the version of the Encore runtime being used\n    #[napi]\n    pub fn version() -> String {\n        encore_runtime_core::version().to_string()\n    }\n\n    /// Returns the git commit hash used to build the Encore runtime\n    #[napi]\n    pub fn build_commit() -> String {\n        encore_runtime_core::build_commit().to_string()\n    }\n\n    #[napi]\n    pub fn app_meta(&self) -> meta::AppMeta {\n        let md = self.runtime.app_meta();\n        md.clone().into()\n    }\n\n    #[napi]\n    pub fn runtime_config(&self) -> runtime_config::RuntimeConfig {\n        let rt = self.runtime.runtime_config();\n        rt.clone().into()\n    }\n\n    /// Reports the total number of worker threads,\n    /// including the main thread.\n    #[napi]\n    pub fn num_worker_threads(&self) -> u32 {\n        match self.runtime.compute().worker_threads {\n            Some(n) => {\n                if n > 0 {\n                    n as u32\n                } else {\n                    num_cpus::get() as u32\n                }\n            }\n            None => 1u32,\n        }\n    }\n\n    /// Register the shared metrics buffer (called once by main thread)\n    /// All worker threads will use the same registry\n    #[napi]\n    pub fn create_metrics_registry(&self, env: Env, buffer: JsObject) -> napi::Result<()> {\n        let inner = crate::metrics::MetricsRegistry::get_or_init_global(env, buffer)?;\n\n        // Create a registry wrapper to pass to the collector\n        let registry = crate::metrics::MetricsRegistry { inner };\n\n        // Register the JS metrics collector with the core runtime (idempotent)\n        let collector = Arc::new(crate::metrics::JsMetricsCollector::new(&registry));\n        self.runtime\n            .metrics()\n            .registry()\n            .register_collector(collector);\n\n        Ok(())\n    }\n\n    /// Get the shared metrics registry (all worker threads use this)\n    #[napi]\n    pub fn get_metrics_registry(&self) -> napi::Result<crate::metrics::MetricsRegistry> {\n        crate::metrics::MetricsRegistry::get_global().ok_or_else(|| {\n            napi::Error::new(\n                napi::Status::GenericFailure,\n                \"Metrics registry not initialized\",\n            )\n        })\n    }\n}\n\n#[napi(object)]\n#[derive(Clone, Default, Debug)]\n/// CallOpts can be used to set options for API calls.\npub struct CallOpts {\n    pub auth_data: Option<PVals>,\n}\n\nimpl TryFrom<CallOpts> for api::CallOpts {\n    type Error = napi::Error<napi::Status>;\n\n    fn try_from(value: CallOpts) -> Result<api::CallOpts> {\n        let auth = if let Some(ref data) = value.auth_data {\n            let user_id = data\n                .0\n                .get(\"userID\")\n                .and_then(|v| v.as_str())\n                .ok_or_else(|| {\n                    napi::Error::new(napi::Status::InvalidArg, \"userID missing in auth data\")\n                })?;\n            Some(AuthOpts {\n                user_id: user_id.to_string(),\n                data: data.0.clone(),\n            })\n        } else {\n            None\n        };\n\n        Ok(api::CallOpts { auth })\n    }\n}\n\n#[napi]\npub struct APICallError {\n    pub code: String,\n    pub message: String,\n    pub details: Option<PVals>,\n}\n\nimpl From<api::Error> for APICallError {\n    fn from(value: api::Error) -> Self {\n        Self {\n            code: value.code.to_string(),\n            message: value.message,\n            details: value.details.map(|d| PVals(*d)),\n        }\n    }\n}\n\n#[napi]\npub struct Decimal {\n    inner: encore_runtime_core::api::Decimal,\n}\n\n#[napi]\nimpl Decimal {\n    #[napi(constructor)]\n    pub fn new(value: String) -> napi::Result<Self> {\n        match encore_runtime_core::api::Decimal::from_str(&value) {\n            Ok(decimal) => Ok(Self { inner: decimal }),\n            Err(err) => Err(Error::new(\n                Status::InvalidArg,\n                format!(\"Invalid decimal format: '{}'\", err),\n            )),\n        }\n    }\n\n    #[napi(js_name = \"toString\")]\n    pub fn js_to_string(&self) -> String {\n        self.inner.to_string()\n    }\n\n    #[napi]\n    pub fn add(&self, other: &Decimal) -> Decimal {\n        use std::ops::Add;\n        Decimal {\n            inner: self.inner.add(&other.inner),\n        }\n    }\n\n    #[napi]\n    pub fn sub(&self, other: &Decimal) -> Decimal {\n        use std::ops::Sub;\n        Decimal {\n            inner: self.inner.sub(&other.inner),\n        }\n    }\n\n    #[napi]\n    pub fn mul(&self, other: &Decimal) -> Decimal {\n        use std::ops::Mul;\n        Decimal {\n            inner: self.inner.mul(&other.inner),\n        }\n    }\n\n    #[napi]\n    pub fn div(&self, other: &Decimal) -> Decimal {\n        use std::ops::Div;\n        Decimal {\n            inner: self.inner.div(&other.inner),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/runtime_config.rs",
    "content": "use std::collections::HashMap;\n\nuse napi_derive::napi;\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct Metric {\n    pub name: String,\n    pub services: Vec<String>,\n}\n\n#[napi(object)]\n#[derive(Debug, Clone)]\npub struct RuntimeConfig {\n    pub metrics: HashMap<String, Metric>,\n}\n\nimpl From<encore_runtime_core::runtime_config::Metric> for Metric {\n    fn from(metric: encore_runtime_core::runtime_config::Metric) -> Self {\n        Self {\n            name: metric.name,\n            services: metric.services,\n        }\n    }\n}\n\nimpl From<encore_runtime_core::runtime_config::RuntimeConfig> for RuntimeConfig {\n    fn from(config: encore_runtime_core::runtime_config::RuntimeConfig) -> Self {\n        Self {\n            metrics: config\n                .metrics\n                .into_iter()\n                .map(|(k, v)| (k, v.into()))\n                .collect(),\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/secret.rs",
    "content": "use napi::{Error, Status};\nuse napi_derive::napi;\nuse std::sync::Arc;\n\n#[napi]\npub struct Secret {\n    secret: Arc<encore_runtime_core::secrets::Secret>,\n}\n\nimpl Secret {\n    pub fn new(secret: Arc<encore_runtime_core::secrets::Secret>) -> Self {\n        Self { secret }\n    }\n}\n\n#[napi]\nimpl Secret {\n    /// Returns the cached value of the secret.\n    #[napi]\n    pub fn cached(&self) -> napi::Result<String> {\n        let val = self.secret.get().map_err(|e| {\n            Error::new(\n                Status::GenericFailure,\n                format!(\"failed to resolve secret: {e}\"),\n            )\n        })?;\n        String::from_utf8(val.to_vec())\n            .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/sqldb.rs",
    "content": "use crate::api::Request;\nuse crate::pvalue::{parse_pvalue, pvalue_to_js};\nuse encore_runtime_core::sqldb;\nuse mappable_rc::Marc;\nuse napi::{Env, JsUnknown};\nuse napi_derive::napi;\nuse std::collections::HashMap;\nuse std::fmt::Display;\nuse std::sync::{Arc, OnceLock};\n\n#[napi]\npub struct SQLDatabase {\n    db: Arc<dyn sqldb::Database>,\n    pool: OnceLock<Marc<napi::Result<sqldb::Pool>>>,\n}\n\n#[napi]\npub struct QueryArgs {\n    values: std::sync::Mutex<Vec<sqldb::RowValue>>,\n}\n\n#[napi]\nimpl QueryArgs {\n    #[napi(constructor)]\n    pub fn new(params: Vec<JsUnknown>) -> napi::Result<Self> {\n        let values = convert_row_values(params)?;\n        Ok(Self {\n            values: std::sync::Mutex::new(values),\n        })\n    }\n}\n\nfn convert_row_values(params: Vec<JsUnknown>) -> napi::Result<Vec<sqldb::RowValue>> {\n    use napi::JsBuffer;\n    params\n        .into_iter()\n        .map(|val| -> napi::Result<sqldb::RowValue> {\n            if val.is_buffer()? {\n                let buf: JsBuffer = val.try_into()?;\n                let buf = buf.into_value()?;\n                return Ok(sqldb::RowValue::Bytes(buf.to_vec()));\n            }\n            let pval = parse_pvalue(val)?;\n            Ok(sqldb::RowValue::PVal(pval))\n        })\n        .collect()\n}\n\n#[napi]\nimpl SQLDatabase {\n    pub(crate) fn new(db: Arc<dyn sqldb::Database>) -> Self {\n        Self {\n            db,\n            pool: OnceLock::new(),\n        }\n    }\n\n    /// Reports the connection string to connect to this database.\n    #[napi]\n    pub fn conn_string(&self) -> &str {\n        self.db.proxy_conn_string()\n    }\n\n    /// Begins a transaction\n    #[napi]\n    pub async fn begin(&self, source: Option<&Request>) -> napi::Result<Transaction> {\n        let source = source.map(|s| s.inner.as_ref());\n        let tx = self\n            .pool()?\n            .begin(source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n\n        Ok(Transaction {\n            tx: tokio::sync::Mutex::new(Some(tx)),\n        })\n    }\n\n    #[napi]\n    pub async fn query(\n        &self,\n        query: String,\n        args: &QueryArgs,\n        source: Option<&Request>,\n    ) -> napi::Result<Cursor> {\n        let values: Vec<_> = args.values.lock().unwrap().drain(..).collect();\n        let source = source.map(|s| s.inner.as_ref());\n        let stream = self\n            .pool()?\n            .query_raw(&query, values, source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        Ok(Cursor {\n            stream: tokio::sync::Mutex::new(stream),\n        })\n    }\n\n    #[napi]\n    pub async fn query_row(\n        &self,\n        query: String,\n        args: &QueryArgs,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Row>> {\n        let values: Vec<_> = args.values.lock().unwrap().drain(..).collect();\n        let source = source.map(|s| s.inner.as_ref());\n        let mut stream = self\n            .pool()?\n            .query_raw(&query, values, source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        let row = stream\n            .next()\n            .await\n            .transpose()\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        Ok(row.map(|row| Row { row }))\n    }\n\n    fn pool(&self) -> napi::Result<&sqldb::Pool> {\n        match self.pool_marc().as_ref() {\n            Ok(pool) => Ok(pool),\n            Err(e) => Err(e.clone()),\n        }\n    }\n\n    fn pool_marc(&self) -> &Marc<napi::Result<sqldb::Pool>> {\n        self.pool.get_or_init(|| {\n            let pool = self\n                .db\n                .new_pool()\n                .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e));\n            Marc::new(pool)\n        })\n    }\n}\n\n#[napi]\npub struct Transaction {\n    tx: tokio::sync::Mutex<Option<sqldb::Transaction>>,\n}\n\n#[napi]\nimpl Transaction {\n    #[napi]\n    pub async fn commit(&self, source: Option<&Request>) -> napi::Result<()> {\n        let source = source.map(|s| s.inner.as_ref());\n        let tx = self.tx.lock().await.take().ok_or(napi::Error::new(\n            napi::Status::GenericFailure,\n            \"transaction closed\",\n        ))?;\n        tx.commit(source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))\n    }\n\n    #[napi]\n    pub async fn rollback(&self, source: Option<&Request>) -> napi::Result<()> {\n        let source = source.map(|s| s.inner.as_ref());\n        let tx = self.tx.lock().await.take().ok_or(napi::Error::new(\n            napi::Status::GenericFailure,\n            \"transaction closed\",\n        ))?;\n        tx.rollback(source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))\n    }\n\n    #[napi]\n    pub async fn query(\n        &self,\n        query: String,\n        args: &QueryArgs,\n        source: Option<&Request>,\n    ) -> napi::Result<Cursor> {\n        let values: Vec<_> = args.values.lock().unwrap().drain(..).collect();\n        let source = source.map(|s| s.inner.as_ref());\n        let tx = self.tx.lock().await;\n        let stream = tx\n            .as_ref()\n            .ok_or(napi::Error::new(\n                napi::Status::GenericFailure,\n                \"transaction closed\",\n            ))?\n            .query_raw(&query, values, source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        Ok(Cursor {\n            stream: tokio::sync::Mutex::new(stream),\n        })\n    }\n}\n\n#[napi]\npub struct Cursor {\n    stream: tokio::sync::Mutex<sqldb::Cursor>,\n}\n\n#[napi]\npub struct Row {\n    row: sqldb::Row,\n}\n\n#[napi]\nimpl Row {\n    #[napi]\n    pub fn values(&self, env: Env) -> napi::Result<HashMap<String, JsUnknown>> {\n        let vals = self.row.values()?;\n        let mut map = HashMap::with_capacity(vals.len());\n        for (key, val) in vals {\n            let val: JsUnknown = match val {\n                sqldb::RowValue::PVal(val) => pvalue_to_js(env, &val)?,\n                sqldb::RowValue::Bytes(val) => {\n                    env.create_arraybuffer_with_data(val)?.into_unknown()\n                }\n                sqldb::RowValue::Uuid(val) => env.create_string(&val.to_string())?.into_unknown(),\n                sqldb::RowValue::Cidr(val) => env.create_string(&val.to_string())?.into_unknown(),\n                sqldb::RowValue::Inet(val) => env.create_string(&val.to_string())?.into_unknown(),\n            };\n            map.insert(key, val);\n        }\n        Ok(map)\n    }\n}\n\n#[napi]\nimpl Cursor {\n    #[napi]\n    pub async fn next(&self) -> napi::Result<Option<Row>> {\n        let mut stream = self.stream.lock().await;\n        let row = stream\n            .next()\n            .await\n            .transpose()\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, format!(\"{e:#?}\")))?;\n\n        Ok(row.map(|row| Row { row }))\n    }\n}\n\n#[napi]\nimpl SQLDatabase {\n    #[napi]\n    pub async fn acquire(&self) -> napi::Result<SQLConn> {\n        let conn = self.pool()?.acquire().await.map_err(to_napi_err)?;\n        log::info!(\"acquired connection\");\n        Ok(SQLConn {\n            inner: Arc::new(conn),\n        })\n    }\n}\n\n#[napi]\npub struct SQLConn {\n    inner: Arc<sqldb::Connection>,\n}\n\n#[napi]\nimpl SQLConn {\n    #[napi]\n    pub async fn close(&self) {\n        self.inner.close().await\n    }\n\n    #[napi]\n    pub async fn query(\n        &self,\n        query: String,\n        args: &QueryArgs,\n        source: Option<&Request>,\n    ) -> napi::Result<Cursor> {\n        let values: Vec<_> = args.values.lock().unwrap().drain(..).collect();\n        let source = source.map(|s| s.inner.as_ref());\n        let stream = self\n            .inner\n            .query_raw(&query, values, source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        Ok(Cursor {\n            stream: tokio::sync::Mutex::new(stream),\n        })\n    }\n\n    #[napi]\n    pub async fn query_row(\n        &self,\n        query: String,\n        args: &QueryArgs,\n        source: Option<&Request>,\n    ) -> napi::Result<Option<Row>> {\n        let values: Vec<_> = args.values.lock().unwrap().drain(..).collect();\n        let source = source.map(|s| s.inner.as_ref());\n        let mut stream = self\n            .inner\n            .query_raw(&query, values, source)\n            .await\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        let row = stream\n            .next()\n            .await\n            .transpose()\n            .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n        Ok(row.map(|row| Row { row }))\n    }\n}\n\nfn to_napi_err<E: Display>(e: E) -> napi::Error {\n    napi::Error::new(napi::Status::GenericFailure, e.to_string())\n}\n"
  },
  {
    "path": "runtimes/js/src/stream/mod.rs",
    "content": "pub mod read;\npub mod write;\n"
  },
  {
    "path": "runtimes/js/src/stream/read.rs",
    "content": "use std::future::Future;\n\nuse bytes::{Bytes, BytesMut};\nuse futures::{Stream, StreamExt};\nuse napi::{noop_finalize, Env, JsFunction, JsUnknown, NapiRaw, Status};\n\nuse crate::threadsafe_function::{\n    ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,\n};\n\npub struct Reader<S> {\n    state: ReaderState<S>,\n}\n\nimpl<S, E> Reader<S>\nwhere\n    S: Stream<Item = Result<Bytes, E>> + Unpin + Send + 'static,\n    E: std::error::Error + Send + 'static,\n{\n    pub fn new(stream: S) -> Self {\n        Self {\n            state: ReaderState::Initial(stream),\n        }\n    }\n\n    pub fn start(&mut self, env: Env, push: JsFunction, destroy: JsFunction) -> napi::Result<()> {\n        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();\n        let state = std::mem::replace(&mut self.state, ReaderState::Running(tx));\n        let stream = match state {\n            ReaderState::Initial(stream) => stream,\n            _ => {\n                return Err(napi::Error::new(\n                    Status::GenericFailure,\n                    \"reader has already been started\".to_owned(),\n                ))\n            }\n        };\n\n        let push = ThreadsafeFunction::create(\n            env.raw(),\n            // SAFETY: `push` is a valid JS function.\n            unsafe { push.raw() },\n            0,\n            execute_push,\n        )?;\n        let destroy = ThreadsafeFunction::create(\n            env.raw(),\n            // SAFETY: `destroy` is a valid JS function.\n            unsafe { destroy.raw() },\n            0,\n            execute_destroy,\n        )?;\n\n        let machine = StateMachine {\n            stream,\n            read_requests: rx,\n            push,\n            destroy,\n            did_destroy: false,\n        };\n        tokio::spawn(machine.run());\n        Ok(())\n    }\n\n    pub fn read(&self) -> napi::Result<()> {\n        match &self.state {\n            ReaderState::Running(tx) => {\n                let _ = tx.send(());\n                Ok(())\n            }\n            ReaderState::Initial(_) => Err(napi::Error::new(\n                Status::GenericFailure,\n                \"reader has not been started\".to_owned(),\n            )),\n        }\n    }\n}\n\nenum ReaderState<S> {\n    Initial(S),\n    Running(tokio::sync::mpsc::UnboundedSender<()>),\n}\n\nstruct StateMachine<S> {\n    stream: S,\n    read_requests: tokio::sync::mpsc::UnboundedReceiver<()>,\n    push: ThreadsafeFunction<PushRequest>,\n    destroy: ThreadsafeFunction<DestroyRequest>,\n    did_destroy: bool,\n}\n\nimpl<S, E> StateMachine<S>\nwhere\n    S: Stream<Item = Result<Bytes, E>> + Unpin + Send,\n    E: std::error::Error + Send + 'static,\n{\n    async fn run(mut self) {\n        'ReadRequestLoop: loop {\n            // Wait for a read request.\n            let Some(()) = self.read_requests.recv().await else {\n                // The sender was dropped.\n                self.notify_close();\n                return;\n            };\n\n            // Read repeatedly until push() returns false.\n            'PushLoop: loop {\n                match self.stream.next().await.transpose() {\n                    Ok(data) => {\n                        let is_none = data.is_none();\n                        match self.push(data).await {\n                            // The stream successfully ended.\n                            Ok(_) if is_none => {\n                                // Note: don't notify_close; the node:stream API will handle\n                                // automatically closing the stream when the data has actually been read.\n                                // If we close here we end up destroying the stream too early, preventing\n                                // the last data from being read.\n                                return;\n                            }\n\n                            // We haven't reached the high water mark yet; continue pushing data.\n                            Ok(true) => continue 'PushLoop,\n\n                            // We've reached the high water mark; wait for the next read request.\n                            Ok(false) => continue 'ReadRequestLoop,\n\n                            // Something went wrong communicating with node. Close the stream with an error.\n                            Err(err) => {\n                                self.notify_err(err);\n                                return;\n                            }\n                        }\n                    }\n                    Err(err) => {\n                        self.notify_err(err);\n                        return;\n                    }\n                }\n            }\n        }\n    }\n\n    fn notify_err<Err: std::error::Error + 'static>(&mut self, err: Err) {\n        if self.did_destroy {\n            return;\n        }\n        self.did_destroy = true;\n        let req = DestroyRequest {\n            err: Some(Box::new(err)),\n        };\n        self.destroy.call(req, ThreadsafeFunctionCallMode::Blocking);\n    }\n\n    fn notify_close(&mut self) {\n        if self.did_destroy {\n            return;\n        }\n        self.did_destroy = true;\n        let req = DestroyRequest { err: None };\n        self.destroy.call(req, ThreadsafeFunctionCallMode::Blocking);\n    }\n\n    fn push(&self, data: Option<Bytes>) -> impl Future<Output = napi::Result<bool>> {\n        let (tx, rx) = tokio::sync::oneshot::channel();\n        let req = PushRequest { data, response: tx };\n\n        let result = self.push.call(req, ThreadsafeFunctionCallMode::Blocking);\n\n        async move {\n            match result {\n                Status::Ok => match rx.await {\n                    Ok(more) => Ok(more),\n                    Err(_) => Err(napi::Error::new(\n                        Status::GenericFailure,\n                        \"push response channel was dropped\".to_owned(),\n                    )),\n                },\n\n                status => Err(napi::Error::new(\n                    status,\n                    \"failed to call push function\".to_owned(),\n                )),\n            }\n        }\n    }\n}\n\nstruct PushRequest {\n    data: Option<Bytes>,\n    response: tokio::sync::oneshot::Sender<bool>,\n}\n\nfn execute_push(ctx: ThreadSafeCallContext<PushRequest>) -> napi::Result<()> {\n    let data: JsUnknown = match ctx.value.data {\n        Some(data) => {\n            // We need to convert this to a BytesMut since\n            // JavaScript may modify the buffer.\n            let mut data = BytesMut::from(data);\n            let buf = unsafe {\n                ctx.env.create_buffer_with_borrowed_data(\n                    data.as_mut_ptr(),\n                    data.len(),\n                    data,\n                    noop_finalize,\n                )?\n            };\n            buf.into_unknown()\n        }\n        None => ctx.env.get_null()?.into_unknown(),\n    };\n\n    let more = ctx\n        .callback\n        .unwrap()\n        .call(None, &[data])?\n        .coerce_to_bool()?\n        .get_value()?;\n    _ = ctx.value.response.send(more);\n    Ok(())\n}\n\nstruct DestroyRequest {\n    err: Option<Box<dyn std::error::Error>>,\n}\n\nfn execute_destroy(ctx: ThreadSafeCallContext<DestroyRequest>) -> napi::Result<()> {\n    if let Some(err) = ctx.value.err {\n        let err = ctx\n            .env\n            .create_error(napi::Error::new(Status::GenericFailure, err.to_string()))?;\n        ctx.callback.unwrap().call(None, &[err.into_unknown()])?;\n    } else {\n        ctx.callback.unwrap().call_without_args(None)?;\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "runtimes/js/src/stream/write.rs",
    "content": "use std::collections::VecDeque;\nuse std::pin::Pin;\nuse std::task::{Context, Poll};\n\nuse bytes::Bytes;\nuse tokio::io::AsyncRead;\nuse tokio::sync::oneshot;\n\npub fn new() -> (WriteHalf, ReadHalf) {\n    let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();\n    let write = WriteHalf { sender };\n    let read = ReadHalf {\n        events: receiver,\n        done: false,\n        done_callback: None,\n        bufs: VecDeque::with_capacity(16),\n        partial: None,\n    };\n    (write, read)\n}\n\n#[derive(Debug)]\npub struct WriteHalf {\n    sender: tokio::sync::mpsc::UnboundedSender<StreamEvent>,\n}\n\nimpl WriteHalf {\n    pub fn write(&mut self, data: Bytes, callback: Option<oneshot::Sender<()>>) {\n        // TODO: Log error?\n        _ = self.sender.send(StreamEvent {\n            data: StreamEventData::Write(data),\n            callback,\n        });\n    }\n\n    pub fn writev(&mut self, bufs: Vec<Bytes>, callback: Option<oneshot::Sender<()>>) {\n        // TODO: Log error?\n        _ = self.sender.send(StreamEvent {\n            data: StreamEventData::WriteMulti(bufs),\n            callback,\n        });\n    }\n\n    pub fn end(&mut self, callback: Option<oneshot::Sender<()>>) {\n        // TODO: Log error?\n        _ = self.sender.send(StreamEvent {\n            data: StreamEventData::End,\n            callback,\n        });\n    }\n}\n\nstruct StreamEvent {\n    data: StreamEventData,\n    callback: Option<oneshot::Sender<()>>,\n}\n\nenum StreamEventData {\n    Write(Bytes),\n    WriteMulti(Vec<Bytes>),\n    End,\n}\n\npub struct ReadHalf {\n    events: tokio::sync::mpsc::UnboundedReceiver<StreamEvent>,\n\n    // True if the write end has been closed.\n    done: bool,\n    done_callback: Option<oneshot::Sender<()>>,\n\n    // The bufs to read from.\n    bufs: VecDeque<BufWithCB>,\n    // The partially read buffer.\n    partial: Option<PartiallyRead>,\n}\n\nstruct BufWithCB {\n    buf: Bytes,\n    callback: Option<oneshot::Sender<()>>,\n}\n\nstruct PartiallyRead {\n    buf: Bytes,\n    pos: usize,\n    callback: Option<oneshot::Sender<()>>,\n}\n\nimpl PartiallyRead {\n    /// The number of bytes left to read in the buffer.\n    fn len(&self) -> usize {\n        self.buf.len() - self.pos\n    }\n}\n\nimpl AsyncRead for ReadHalf {\n    fn poll_read(\n        mut self: Pin<&mut Self>,\n        cx: &mut Context<'_>,\n        buf: &mut tokio::io::ReadBuf<'_>,\n    ) -> Poll<std::io::Result<()>> {\n        // First process any outstanding events.\n        if !self.done {\n            'EventLoop: loop {\n                match self.events.poll_recv(cx) {\n                    // The events channel has been closed.\n                    Poll::Ready(None) => {\n                        self.done = true;\n                        break 'EventLoop;\n                    }\n\n                    // There is no more events to read at the moment.\n                    Poll::Pending => break 'EventLoop,\n\n                    Poll::Ready(Some(event)) => {\n                        let mut callback = event.callback;\n                        match event.data {\n                            StreamEventData::Write(buf) => {\n                                self.bufs.push_back(BufWithCB { buf, callback });\n                            }\n                            StreamEventData::WriteMulti(bufs) => {\n                                let num_bufs = bufs.len();\n                                self.bufs.reserve(num_bufs);\n                                for (i, buf) in bufs.into_iter().enumerate() {\n                                    // Only add the callback to the last buffer.\n                                    let callback = if i == num_bufs - 1 {\n                                        callback.take()\n                                    } else {\n                                        None\n                                    };\n                                    self.bufs.push_back(BufWithCB { buf, callback });\n                                }\n                            }\n                            StreamEventData::End => {\n                                self.done = true;\n                                self.done_callback = callback;\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        let mut did_read = false;\n        'ReadLoop: loop {\n            let space_remaining = buf.remaining();\n            if space_remaining == 0 || (self.bufs.is_empty() && self.partial.is_none()) {\n                // No space remaining in the buffer, or no more data to read.\n                break 'ReadLoop;\n            }\n\n            // Determine the buffer to read from.\n            let mut partial = match self.partial.take() {\n                Some(partial) => partial,\n                None => {\n                    // Find the next non-empty buffer to read from.\n                    let next = 'BufferLoop: loop {\n                        match self.bufs.pop_front() {\n                            // Found a non-empty buffer.\n                            Some(next) if !next.buf.is_empty() => break next,\n                            // Found an empty buffer; skip it.\n                            Some(_) => continue 'BufferLoop,\n                            // No more buffers to read from.\n                            None => continue 'ReadLoop,\n                        }\n                    };\n                    PartiallyRead {\n                        buf: next.buf,\n                        pos: 0,\n                        callback: next.callback,\n                    }\n                }\n            };\n\n            // Post-condition: we have a non-empty buffer to read from, and self.partial is None.\n            assert!(self.partial.is_none());\n            assert!(partial.len() > 0);\n\n            if partial.len() > space_remaining {\n                // We can't fit the whole buffer in the space remaining.\n                let pos = partial.pos;\n                buf.put_slice(&partial.buf[pos..(pos + space_remaining)]);\n\n                // Store the partial read state in self.partial.\n                partial.pos += space_remaining;\n                self.partial = Some(partial);\n\n                return Poll::Ready(Ok(()));\n            } else {\n                // We can fit the whole buffer in the space remaining.\n                buf.put_slice(&partial.buf[partial.pos..]);\n\n                if let Some(callback) = partial.callback {\n                    _ = callback.send(());\n                }\n\n                // Store that we did read data so we can return Ready.\n                did_read = true;\n            }\n        }\n\n        if self.done {\n            if let Some(done_callback) = self.done_callback.take() {\n                _ = done_callback.send(());\n            }\n        }\n\n        if did_read || self.done {\n            // We read some data, or we're done, so we're ready.\n            Poll::Ready(Ok(()))\n        } else {\n            Poll::Pending\n        }\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/threadsafe_function.rs",
    "content": "// Fork of threadsafe_function from napi-rs that allows calling JS function manually rather than\n// only returning args. This enables us to use the return value of the function.\n\n#![allow(clippy::single_component_path_imports)]\n\nuse std::convert::Into;\nuse std::ffi::CString;\nuse std::fmt::Debug;\nuse std::marker::PhantomData;\nuse std::os::raw::c_void;\nuse std::ptr;\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\nuse std::sync::Arc;\n\nuse napi::{check_status, sys, Env, Result, Status};\nuse napi::{JsError, JsFunction, NapiValue};\n\n/// ThreadSafeFunction Context object\n/// the `value` is the value passed to `call` method\npub struct ThreadSafeCallContext<T: 'static> {\n    pub env: Env,\n    pub value: T,\n    pub callback: Option<JsFunction>,\n}\n\n#[repr(u8)]\npub enum ThreadsafeFunctionCallMode {\n    NonBlocking,\n    Blocking,\n}\n\nimpl From<ThreadsafeFunctionCallMode> for sys::napi_threadsafe_function_call_mode {\n    fn from(value: ThreadsafeFunctionCallMode) -> Self {\n        match value {\n            ThreadsafeFunctionCallMode::Blocking => sys::ThreadsafeFunctionCallMode::blocking,\n            ThreadsafeFunctionCallMode::NonBlocking => sys::ThreadsafeFunctionCallMode::nonblocking,\n        }\n    }\n}\n\n/// Communicate with the addon's main thread by invoking a JavaScript function from other threads.\n///\n/// ## Example\n/// An example of using `ThreadsafeFunction`:\n///\n/// ```rust\n/// #[macro_use]\n/// extern crate napi_derive;\n///\n/// use std::thread;\n///\n/// use napi::{\n///     threadsafe_function::{\n///         ThreadSafeCallContext, ThreadsafeFunctionCallMode, ThreadsafeFunctionReleaseMode,\n///     },\n///     CallContext, Error, JsFunction, JsNumber, JsUndefined, Result, Status,\n/// };\n///\n/// #[js_function(1)]\n/// pub fn test_threadsafe_function(ctx: CallContext) -> Result<JsUndefined> {\n///   let func = ctx.get::<JsFunction>(0)?;\n///\n///   let tsfn =\n///       ctx\n///           .env\n///           .create_threadsafe_function(&func, 0, |ctx: ThreadSafeCallContext<Vec<u32>>| {\n///             ctx.value\n///                 .iter()\n///                 .map(|v| ctx.env.create_uint32(*v))\n///                 .collect::<Result<Vec<JsNumber>>>()\n///           })?;\n///\n///   let tsfn_cloned = tsfn.clone();\n///\n///   thread::spawn(move || {\n///       let output: Vec<u32> = vec![0, 1, 2, 3];\n///       // It's okay to call a threadsafe function multiple times.\n///       tsfn.call(Ok(output.clone()), ThreadsafeFunctionCallMode::Blocking);\n///   });\n///\n///   thread::spawn(move || {\n///       let output: Vec<u32> = vec![3, 2, 1, 0];\n///       // It's okay to call a threadsafe function multiple times.\n///       tsfn_cloned.call(Ok(output.clone()), ThreadsafeFunctionCallMode::NonBlocking);\n///   });\n///\n///   ctx.env.get_undefined()\n/// }\n/// ```\npub struct ThreadsafeFunction<T: 'static> {\n    raw_tsfn: sys::napi_threadsafe_function,\n    aborted: Arc<AtomicBool>,\n    ref_count: Arc<AtomicUsize>,\n    _phantom: PhantomData<T>,\n}\n\nimpl<T> Debug for ThreadsafeFunction<T> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"ThreadsafeFunction\").finish()\n    }\n}\n\nimpl<T: 'static> Clone for ThreadsafeFunction<T> {\n    fn clone(&self) -> Self {\n        if !self.aborted.load(Ordering::Acquire) {\n            let acquire_status = unsafe { sys::napi_acquire_threadsafe_function(self.raw_tsfn) };\n            debug_assert!(\n                acquire_status == sys::Status::napi_ok,\n                \"Acquire threadsafe function failed in clone\"\n            );\n        }\n\n        Self {\n            raw_tsfn: self.raw_tsfn,\n            aborted: Arc::clone(&self.aborted),\n            ref_count: Arc::clone(&self.ref_count),\n            _phantom: PhantomData,\n        }\n    }\n}\n\nunsafe impl<T> Send for ThreadsafeFunction<T> {}\nunsafe impl<T> Sync for ThreadsafeFunction<T> {}\n\nimpl<T: 'static> ThreadsafeFunction<T> {\n    /// See [napi_create_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_create_threadsafe_function)\n    /// for more information.\n    pub(crate) fn create<R: 'static + Send + FnMut(ThreadSafeCallContext<T>) -> Result<()>>(\n        env: sys::napi_env,\n        func: sys::napi_value,\n        max_queue_size: usize,\n        callback: R,\n    ) -> Result<Self> {\n        let mut async_resource_name = ptr::null_mut();\n        let s = \"napi_rs_threadsafe_function\";\n        let len = s.len();\n        let s = CString::new(s)?;\n        check_status!(unsafe {\n            sys::napi_create_string_utf8(env, s.as_ptr(), len, &mut async_resource_name)\n        })?;\n\n        let initial_thread_count = 1usize;\n        let mut raw_tsfn = ptr::null_mut();\n        let ptr = Box::into_raw(Box::new(callback)) as *mut c_void;\n        check_status!(unsafe {\n            sys::napi_create_threadsafe_function(\n                env,\n                func,\n                ptr::null_mut(),\n                async_resource_name,\n                max_queue_size,\n                initial_thread_count,\n                ptr,\n                Some(thread_finalize_cb::<T, R>),\n                ptr,\n                Some(call_js_cb::<T, R>),\n                &mut raw_tsfn,\n            )\n        })?;\n\n        let aborted = Arc::new(AtomicBool::new(false));\n        let aborted_ptr = Arc::into_raw(aborted.clone()) as *mut c_void;\n        check_status!(unsafe {\n            sys::napi_add_env_cleanup_hook(env, Some(cleanup_cb), aborted_ptr)\n        })?;\n\n        Ok(ThreadsafeFunction {\n            raw_tsfn,\n            aborted,\n            ref_count: Arc::new(AtomicUsize::new(initial_thread_count)),\n            _phantom: PhantomData,\n        })\n    }\n}\n\nimpl<T: 'static> ThreadsafeFunction<T> {\n    /// See [napi_call_threadsafe_function](https://nodejs.org/api/n-api.html#n_api_napi_call_threadsafe_function)\n    /// for more information.\n    pub fn call(&self, value: T, mode: ThreadsafeFunctionCallMode) -> Status {\n        if self.aborted.load(Ordering::Acquire) {\n            return Status::Closing;\n        }\n        unsafe {\n            sys::napi_call_threadsafe_function(\n                self.raw_tsfn,\n                Box::into_raw(Box::new(value)) as *mut _,\n                mode.into(),\n            )\n        }\n        .into()\n    }\n}\n\nimpl<T: 'static> Drop for ThreadsafeFunction<T> {\n    fn drop(&mut self) {\n        if !self.aborted.load(Ordering::Acquire) && self.ref_count.load(Ordering::Acquire) > 0usize\n        {\n            let release_status = unsafe {\n                sys::napi_release_threadsafe_function(\n                    self.raw_tsfn,\n                    sys::ThreadsafeFunctionReleaseMode::release,\n                )\n            };\n            assert!(\n                release_status == sys::Status::napi_ok,\n                \"Threadsafe Function release failed\"\n            );\n        }\n    }\n}\n\nunsafe extern \"C\" fn cleanup_cb(cleanup_data: *mut c_void) {\n    let aborted = Arc::<AtomicBool>::from_raw(cleanup_data.cast());\n    aborted.store(true, Ordering::SeqCst);\n}\n\nunsafe extern \"C\" fn thread_finalize_cb<T: 'static, R>(\n    _raw_env: sys::napi_env,\n    finalize_data: *mut c_void,\n    _finalize_hint: *mut c_void,\n) where\n    R: 'static + Send + FnMut(ThreadSafeCallContext<T>) -> Result<()>,\n{\n    // cleanup\n    drop(Box::<R>::from_raw(finalize_data.cast()));\n}\n\nunsafe extern \"C\" fn call_js_cb<T: 'static, R>(\n    raw_env: sys::napi_env,\n    js_callback: sys::napi_value,\n    context: *mut c_void,\n    data: *mut c_void,\n) where\n    R: 'static + Send + FnMut(ThreadSafeCallContext<T>) -> Result<()>,\n{\n    // env and/or callback can be null when shutting down\n    if raw_env.is_null() {\n        return;\n    }\n\n    let ctx: &mut R = &mut *context.cast::<R>();\n    let val: Result<T> = Ok(*Box::<T>::from_raw(data.cast()));\n\n    let mut recv = ptr::null_mut();\n    sys::napi_get_undefined(raw_env, &mut recv);\n\n    let ret = val.and_then(|v| {\n        (ctx)(ThreadSafeCallContext {\n            env: Env::from_raw(raw_env),\n            value: v,\n            callback: if js_callback.is_null() {\n                None\n            } else {\n                Some(JsFunction::from_raw(raw_env, js_callback).unwrap()) // TODO: unwrap\n            },\n        })\n    });\n\n    let status = match ret {\n        Ok(()) => sys::Status::napi_ok,\n        Err(e) => sys::napi_fatal_exception(raw_env, JsError::from(e).into_value(raw_env)),\n    };\n    if status == sys::Status::napi_ok {\n        return;\n    }\n    if status == sys::Status::napi_pending_exception {\n        let mut error_result = ptr::null_mut();\n        assert_eq!(\n            sys::napi_get_and_clear_last_exception(raw_env, &mut error_result),\n            sys::Status::napi_ok\n        );\n\n        // When shutting down, napi_fatal_exception sometimes returns another exception\n        let stat = sys::napi_fatal_exception(raw_env, error_result);\n        assert!(stat == sys::Status::napi_ok || stat == sys::Status::napi_pending_exception);\n    } else {\n        let error_code: Status = status.into();\n        let error_code_string = format!(\"{error_code:?}\");\n        let mut error_code_value = ptr::null_mut();\n        assert_eq!(\n            sys::napi_create_string_utf8(\n                raw_env,\n                error_code_string.as_ptr() as *const _,\n                error_code_string.len(),\n                &mut error_code_value,\n            ),\n            sys::Status::napi_ok,\n        );\n        let error_msg = \"Call JavaScript callback failed in thread safe function\";\n        let mut error_msg_value = ptr::null_mut();\n        assert_eq!(\n            sys::napi_create_string_utf8(\n                raw_env,\n                error_msg.as_ptr() as *const _,\n                error_msg.len(),\n                &mut error_msg_value,\n            ),\n            sys::Status::napi_ok,\n        );\n        let mut error_value = ptr::null_mut();\n        assert_eq!(\n            sys::napi_create_error(raw_env, error_code_value, error_msg_value, &mut error_value),\n            sys::Status::napi_ok,\n        );\n        assert_eq!(\n            sys::napi_fatal_exception(raw_env, error_value),\n            sys::Status::napi_ok\n        );\n    }\n}\n"
  },
  {
    "path": "runtimes/js/src/websocket_api.rs",
    "content": "use crate::api::{APIPromiseHandler, Request};\nuse crate::napi_util::await_promise;\nuse crate::napi_util::PromiseHandler;\nuse crate::pvalue::{parse_pvalues, PVals};\nuse crate::threadsafe_function::{\n    ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,\n};\nuse encore_runtime_core::api::websocket::StreamMessagePayload;\nuse encore_runtime_core::api::{self, HandlerRequest, HandlerResponse};\nuse encore_runtime_core::api::{websocket_client, ToResponse};\nuse napi::{Env, JsFunction, JsObject, JsUnknown, NapiRaw};\nuse napi_derive::napi;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::Arc;\n\nstruct WsRequestMessage {\n    req: Request,\n    payload: StreamMessagePayload,\n    tx: tokio::sync::mpsc::UnboundedSender<HandlerResponse>,\n}\n\npub struct JSWebSocketHandler {\n    handler: ThreadsafeFunction<WsRequestMessage>,\n}\n\nimpl api::BoxedHandler for JSWebSocketHandler {\n    fn call(\n        self: Arc<Self>,\n        req: HandlerRequest,\n    ) -> Pin<Box<dyn Future<Output = api::ResponseData> + Send + 'static>> {\n        Box::pin(async move {\n            let internal_caller = req.internal_caller.clone();\n            let resp = api::websocket::upgrade_request(req, |req, payload, tx| async move {\n                self.handler.call(\n                    WsRequestMessage {\n                        tx,\n                        payload,\n                        req: Request::new(req),\n                    },\n                    ThreadsafeFunctionCallMode::Blocking,\n                );\n            });\n\n            match resp {\n                Ok(resp) => api::ResponseData::Raw(resp),\n                Err(e) => api::ResponseData::Raw(e.to_response(internal_caller)),\n            }\n        })\n    }\n}\n\npub fn new_handler(env: Env, func: JsFunction) -> napi::Result<Arc<dyn api::BoxedHandler>> {\n    let handler = ThreadsafeFunction::create(\n        env.raw(),\n        // SAFETY: `handler` is a valid JS function.\n        unsafe { func.raw() },\n        0,\n        ws_resolve_on_js_thread,\n    )?;\n\n    Ok(Arc::new(JSWebSocketHandler { handler }))\n}\n\n#[napi]\nstruct Socket {\n    #[allow(dead_code)]\n    inner: api::websocket::Socket,\n}\n\n#[napi]\nimpl Socket {\n    fn new(inner: api::websocket::Socket) -> Self {\n        Socket { inner }\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub fn send(&self, msg: PVals) -> napi::Result<()> {\n        self.inner\n            .send(msg.0)\n            .map_err(|e| napi::Error::new(napi::Status::Unknown, e))\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub async fn recv(&self) -> napi::Result<PVals> {\n        self.inner\n            .recv()\n            .await\n            .map(PVals)\n            .ok_or_else(|| napi::Error::new(napi::Status::Unknown, \"socket receive channel closed\"))\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub fn close(&self) {\n        self.inner.close()\n    }\n}\n\n#[napi]\nstruct Sink {\n    #[allow(dead_code)]\n    inner: api::websocket::Sink,\n}\n\n#[napi]\nimpl Sink {\n    fn new(inner: api::websocket::Sink) -> Self {\n        Sink { inner }\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub fn send(&self, msg: PVals) -> napi::Result<()> {\n        self.inner\n            .send(msg.0)\n            .map_err(|e| napi::Error::new(napi::Status::Unknown, e))\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub fn close(&self) {\n        self.inner.close()\n    }\n}\n\n#[napi]\nstruct Stream {\n    #[allow(dead_code)]\n    inner: api::websocket::Stream,\n}\n\n#[napi]\nimpl Stream {\n    fn new(inner: api::websocket::Stream) -> Self {\n        Stream { inner }\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub async fn recv(&self) -> napi::Result<PVals> {\n        self.inner\n            .recv()\n            .await\n            .ok_or_else(|| napi::Error::new(napi::Status::Unknown, \"socket receive channel closed\"))\n            .map(PVals)\n    }\n}\n\n#[napi]\npub struct WebSocketClient {\n    inner: Arc<websocket_client::WebSocketClient>,\n}\n\n#[napi]\nimpl WebSocketClient {\n    pub fn new(inner: websocket_client::WebSocketClient) -> Self {\n        WebSocketClient {\n            inner: Arc::new(inner),\n        }\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub fn send(&self, msg: JsUnknown) -> napi::Result<()> {\n        let Some(msg) = parse_pvalues(msg)? else {\n            return Err(napi::Error::new(\n                napi::Status::InvalidArg,\n                \"no message data provided\",\n            ));\n        };\n\n        self.inner\n            .send(msg)\n            .map_err(|e| napi::Error::new(napi::Status::Unknown, e))?;\n\n        Ok(())\n    }\n\n    #[napi(ts_return_type = \"Promise<Record<string, any>>\")]\n    #[allow(dead_code)]\n    pub fn recv(&self, env: Env) -> napi::Result<JsObject> {\n        let inner = self.inner.clone();\n        let fut = async move {\n            inner\n                .recv()\n                .await\n                .ok_or_else(|| {\n                    napi::Error::new(\n                        napi::Status::Unknown,\n                        \"websocket client receive channel closed\",\n                    )\n                })?\n                .map_err(|e| {\n                    log::warn!(\"unable to parse incoming message: {e}\");\n                    napi::Error::new(\n                        napi::Status::GenericFailure,\n                        \"unable to parse incoming message according to schema\",\n                    )\n                })\n        };\n\n        env.execute_tokio_future(fut, |&mut _env, res| Ok(PVals(res)))\n    }\n\n    #[napi]\n    #[allow(dead_code)]\n    pub fn close(&self) {\n        self.inner.close()\n    }\n}\n\nfn ws_resolve_on_js_thread(ctx: ThreadSafeCallContext<WsRequestMessage>) -> napi::Result<()> {\n    let req = ctx\n        .value\n        .req\n        .into_instance(ctx.env)?\n        .as_object(ctx.env)\n        .into_unknown();\n\n    let stream_arg = match ctx.value.payload {\n        StreamMessagePayload::InOut(socket) => Socket::new(socket)\n            .into_instance(ctx.env)?\n            .as_object(ctx.env)\n            .into_unknown(),\n        StreamMessagePayload::Out(sink) => Sink::new(sink)\n            .into_instance(ctx.env)?\n            .as_object(ctx.env)\n            .into_unknown(),\n        StreamMessagePayload::In(stream) => Stream::new(stream)\n            .into_instance(ctx.env)?\n            .as_object(ctx.env)\n            .into_unknown(),\n    };\n\n    let handler = APIPromiseHandler { resp_schema: None };\n\n    match ctx.callback.unwrap().call(None, &[req, stream_arg]) {\n        Ok(result) => {\n            await_promise(ctx.env, result, ctx.value.tx.clone(), handler);\n            Ok(())\n        }\n        Err(err) => {\n            let res = handler.error(ctx.env, err);\n            _ = ctx.value.tx.send(res);\n            Ok(())\n        }\n    }\n}\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "tab_spaces = 4\nedition = \"2021\"\n"
  },
  {
    "path": "supervisor/Cargo.toml",
    "content": "[package]\nname = \"encore-supervisor\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nenv_logger = \"0.11.3\"\nlibc = \"0.2.155\"\nlog = { version = \"0.4.22\", features = [\"kv\"] }\ntokio = { version = \"1.38.0\", features = [\n    \"process\",\n    \"macros\",\n    \"signal\",\n    \"rt\",\n    \"rt-multi-thread\",\n] }\ntokio-retry = \"0.3.0\"\ntokio-util = { version = \"0.7.11\", features = [\"rt\"] }\nbase64 = \"0.21.5\"\nprost = \"0.12.3\"\nprost-types = \"0.12.3\"\nserde_json = \"1.0\"\nflate2 = \"1.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\npingora = { version = \"0.8\", features = [\"lb\"] }\nanyhow = \"1.0.76\"\nhyper = { version = \"1.1.0\", features = [\"server\", \"http1\", \"http2\", \"client\"] }\nurl = \"2.5.0\"\naxum = { version = \"0.7.5\", features = [\"ws\"] }\nhttp = \"1.0.0\"\nfutures = \"0.3.30\"\nreqwest = { version = \"0.12.4\", features = [\"blocking\", \"stream\", \"json\"] }\nbytes = { version = \"1.5.0\", features = [] }\nopenssl = { version = \"0.10.68\", features = [\"vendored\"] }\n\n[build-dependencies]\nprost-build = { version = \"0.12.1\" }\n"
  },
  {
    "path": "supervisor/build.rs",
    "content": "use std::io::Result;\n\nfn main() -> Result<()> {\n    prost_build::compile_protos(\n        &[\n            \"../proto/encore/runtime/v1/runtime.proto\",\n            \"../proto/encore/parser/meta/v1/meta.proto\",\n        ],\n        &[\"../proto/\"],\n    )?;\n    Ok(())\n}\n"
  },
  {
    "path": "supervisor/src/bin/supervisor-encore.rs",
    "content": "use encore_supervisor::config;\nuse encore_supervisor::proxy;\nuse encore_supervisor::supervisor::Supervisor;\nuse std::env;\nuse std::net::IpAddr;\nuse std::net::Ipv4Addr;\nuse tokio_util::sync::CancellationToken;\n\n#[tokio::main]\npub async fn main() {\n    env_logger::init();\n\n    let supervisor_config =\n        config::load_supervisor_config().expect(\"could not load supervisor config\");\n\n    // Get the exposed port from the environment variable, defaulting to 8080 if not set\n    let exposed_port = env::var(\"PORT\")\n        .ok()\n        .and_then(|p| p.parse::<u16>().ok())\n        .unwrap_or(8080);\n\n    if supervisor_config.hosted_gateways.is_empty() && supervisor_config.hosted_services.len() > 1 {\n        panic!(\"Cannot run supervisor with no gateways and multiple services.\");\n    }\n\n    let use_proxy = !supervisor_config.hosted_gateways.is_empty()\n        && !supervisor_config.hosted_services.is_empty();\n\n    // Assign a unique port to each hosted service\n    let mut service_ports = std::collections::HashMap::new();\n\n    // Start assigning services to ports (reserve the exposed port to the proxy if it's used)\n    let mut port: u16 = exposed_port + if use_proxy { 1 } else { 0 };\n\n    for service in &supervisor_config.hosted_services {\n        service_ports.insert(service.clone(), port);\n        port += 1;\n    }\n\n    // Create a process for each service and assign it to the selected port\n    let mut procs = Vec::new();\n    for (service_name, service_port) in &service_ports {\n        procs.push(\n            config::create_process_config(\n                vec![service_name.clone()],\n                vec![],\n                *service_port,\n                &service_ports,\n                &supervisor_config.binary_config,\n            )\n            .expect(\"Failed to create process for service\"),\n        );\n    }\n\n    let upstream_port = port;\n    if !supervisor_config.hosted_gateways.is_empty() {\n        service_ports.insert(\"api-gateway\".to_string(), port);\n        procs.push(\n            config::create_process_config(\n                vec![],\n                supervisor_config.hosted_gateways,\n                port,\n                &service_ports,\n                &supervisor_config.binary_config,\n            )\n            .expect(\"Failed to create process for gateways\"),\n        );\n    }\n\n    let mut handles = Vec::new();\n    let mut results = Vec::new();\n    let root_token = CancellationToken::new();\n\n    let sv = Supervisor::new(procs);\n    let supervisor_token = root_token.child_token();\n    handles.push(tokio::spawn(sv.supervise(supervisor_token)));\n\n    if use_proxy {\n        let proxy = proxy::GatewayProxy::new(\n            reqwest::Client::new(),\n            std::net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), upstream_port),\n            service_ports.clone(),\n        );\n        let proxy_token = root_token.child_token();\n        let proxy_fut = proxy.serve(format!(\"0.0.0.0:{exposed_port}\"), proxy_token);\n        handles.push(tokio::spawn(proxy_fut));\n    }\n\n    // Spawn a task to listen for SIGINT or SIGTERM and cancel the root token\n    tokio::spawn(async move {\n        // Wait for SIGINT or SIGTERM\n        tokio::signal::ctrl_c()\n            .await\n            .expect(\"Failed to listen for ctrl+c\");\n        log::info!(\"Received shutdown signal. Initiating graceful shutdown...\");\n        root_token.cancel();\n    });\n\n    for handle in handles {\n        results.push(handle.await);\n    }\n\n    for result in results {\n        if let Err(e) = result {\n            log::error!(\"Error while shutting down process: {:?}\", e);\n        }\n    }\n    log::info!(\"All processes have exited. Shutting down.\");\n}\n"
  },
  {
    "path": "supervisor/src/config.rs",
    "content": "use crate::supervisor::Process;\nuse anyhow::{Context, Result};\nuse base64::Engine;\nuse prost::Message;\nuse runtime::v1 as runtimepb;\nuse std::io::Read;\nuse std::time::Duration;\nuse std::{collections::HashMap, env, fs::File};\nuse tokio_retry::strategy::ExponentialBackoff;\n\npub mod runtime {\n    pub mod v1 {\n        include!(concat!(env!(\"OUT_DIR\"), \"/encore.runtime.v1.rs\"));\n    }\n}\n\n// loads the binary config and the runtime config and merges them into a supervisor config\npub fn load_supervisor_config() -> Result<SupervisorConfig> {\n    let (services, gateways) = load_hosted_processes()?;\n    Ok(SupervisorConfig {\n        binary_config: load_binary_config()?,\n        hosted_services: services,\n        hosted_gateways: gateways,\n    })\n}\n\n// loads the binary config (defaults to /encore/supervisor.config.json). It contains\n// information on how to start the available binaries and what services/gateways they contain\nfn load_binary_config() -> Result<BinaryConfig> {\n    // Parse config path from args, defaulting to /encore/supervisor.config.json if not specified\n    let args: Vec<String> = env::args().collect();\n    let config_path = args\n        .iter()\n        .position(|arg| arg == \"-c\")\n        .and_then(|index| args.get(index + 1))\n        .map(|s| s.to_string())\n        .unwrap_or_else(|| \"/encore/supervisor.config.json\".to_string());\n\n    // Open and read the supervisor config file\n    let mut file = File::open(config_path)?;\n    let mut contents = String::new();\n    file.read_to_string(&mut contents)?;\n    serde_json::from_str(&contents).map_err(|e| anyhow::anyhow!(e))\n}\n\n// attempts to read the encore runtime config either as proto or json. Extracts and returns the\n// hosted services and gateways.\nfn load_hosted_processes() -> Result<(Vec<String>, Vec<String>)> {\n    let infra_cfg_path = env::var(\"ENCORE_INFRA_CONFIG_PATH\");\n    if let Ok(cfg) = infra_cfg_path {\n        let mut file = File::open(cfg).context(\"Failed to open ENCORE_INFRA_CONFIG_PATH\")?;\n        let mut contents = String::new();\n        file.read_to_string(&mut contents)?;\n        let config: InfraConfig = serde_json::from_str(contents.as_str())\n            .context(\"Failed to parse InfraConfig as JSON\")?;\n        return Ok((config.hosted_services, config.hosted_gateways));\n    }\n\n    // Read and decode the runtime config bytes from the environment variable\n    let runtime_config = env::var(\"ENCORE_RUNTIME_CONFIG\")\n        .context(\"Failed to read ENCORE_RUNTIME_CONFIG env var\")\n        .and_then(|encoded| {\n            if encoded.starts_with(\"gzip:\") {\n                let gzipped = encoded.trim_start_matches(\"gzip:\");\n                base64::engine::general_purpose::STANDARD\n                    .decode(gzipped.as_bytes())\n                    .context(\"failed base64 decoding ENCORE_RUNTIME_CONFIG\")\n                    .and_then(|bytes| {\n                        let mut decoder = flate2::read::GzDecoder::new(&bytes[..]);\n                        let mut decompressed = Vec::new();\n                        decoder\n                            .read_to_end(&mut decompressed)\n                            .context(\"failed unzipping runtime config\")?;\n                        Ok(decompressed)\n                    })\n            } else {\n                base64::engine::general_purpose::STANDARD\n                    .decode(encoded.as_bytes())\n                    .context(\"failed base64 decoding ENCORE_RUNTIME_CONFIG\")\n            }\n        })?;\n\n    // Decode the runtime config based on its format (protobuf or JSON)\n    match runtimepb::RuntimeConfig::decode(&runtime_config[..]) {\n        Ok(config) => {\n            let deployment = config\n                .deployment\n                .context(\"Deployment not found in RuntimeConfig\")?;\n            let gateways = config\n                .infra\n                .context(\"Infrastructure not found in RuntimeConfig\")?\n                .resources\n                .context(\"Resources not found in Infrastructure\")?\n                .gateways;\n            Ok((\n                deployment\n                    .hosted_services\n                    .iter()\n                    .map(|s| s.name.clone())\n                    .collect(),\n                deployment\n                    .hosted_gateways\n                    .iter()\n                    .map(|rid| {\n                        Ok(gateways\n                            .iter()\n                            .find(|g| g.rid == *rid)\n                            .context(\"Gateway rid not found in infra resources\")?\n                            .encore_name\n                            .clone())\n                    })\n                    .collect::<Result<Vec<String>>>()?,\n            ))\n        }\n        Err(_) => {\n            // If protobuf decoding fails, try JSON decoding\n            let config: RuntimeConfig = serde_json::from_slice(&runtime_config)\n                .context(\"Failed to parse RuntimeConfig as JSON\")?;\n            Ok((\n                config.hosted_services,\n                config.gateways.iter().map(|g| g.name.clone()).collect(),\n            ))\n        }\n    }\n}\n\n// Create a process config for a given set of services and gateways\npub fn create_process_config(\n    services: Vec<String>,\n    gateways: Vec<String>,\n    port: u16,\n    service_ports: &HashMap<String, u16>,\n    cfg: &BinaryConfig,\n) -> Result<Process> {\n    // Append all supervisor environment variables\n    let mut env = std::env::vars().collect::<HashMap<String, String>>();\n\n    // Find a process config that contains all the services and gateways\n    let binary_config = cfg\n        .procs\n        .iter()\n        .find(|p| {\n            (services.iter().all(|s| p.services.contains(s)))\n                && (gateways.iter().all(|g| p.gateways.contains(g)))\n        })\n        .context(format!(\n            \"No matching proc found for services {services:?} gateways {gateways:?}\"\n        ))?;\n\n    // Add proc-specific environment variables\n    env.extend(binary_config.env.iter().map(|e| {\n        let parts: Vec<&str> = e.splitn(2, '=').collect();\n        (\n            parts[0].to_string(),\n            parts.get(1).unwrap_or(&\"\").to_string(),\n        )\n    }));\n\n    // Add the port and process config to the environment\n    env.extend(vec![\n        (\"PORT\".to_string(), port.to_string()),\n        (\n            \"ENCORE_PROCESS_CONFIG\".to_string(),\n            base64::engine::general_purpose::STANDARD.encode(\n                serde_json::to_string(&ProcessConfig {\n                    hosted_gateways: gateways,\n                    hosted_services: services,\n                    local_service_ports: service_ports.clone(),\n                })\n                .context(\"Failed to serialize ProcessConfig\")?,\n            ),\n        ),\n    ]);\n\n    let policy = ExponentialBackoff::from_millis(100).max_delay(Duration::from_millis(1000));\n\n    Ok(Process {\n        name: binary_config.id.clone(),\n        program: binary_config\n            .command\n            .first()\n            .context(\"missing binary command\")?\n            .to_string(),\n        args: binary_config.command[1..].to_vec(),\n        env: env.into_iter().collect(),\n        cwd: std::env::current_dir().context(\"Failed to get current directory\")?,\n        restart_policy: Box::new(policy),\n    })\n}\n\n// Supervisor config is the config bundled with the supervisor binary\n// It contains the list of available binaries and which services and gateways they implement\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct SupervisorConfig {\n    pub binary_config: BinaryConfig,\n    pub hosted_services: Vec<String>,\n    pub hosted_gateways: Vec<String>,\n}\n\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct InfraConfig {\n    #[serde(default)]\n    pub hosted_services: Vec<String>,\n    #[serde(default)]\n    pub hosted_gateways: Vec<String>,\n}\n\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct BinaryConfig {\n    pub procs: Vec<Proc>,\n}\n\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct Proc {\n    id: String,\n    command: Vec<String>,\n    env: Vec<String>,\n    services: Vec<String>,\n    gateways: Vec<String>,\n}\n\n// Process config is the config for a given process\n// It overrides the RuntimeConfig for local service discovery, port allocation\n// and which services and gateways are hosted on this process\n#[derive(serde::Serialize, serde::Deserialize)]\nstruct ProcessConfig {\n    local_service_ports: HashMap<String, u16>,\n    hosted_gateways: Vec<String>,\n    hosted_services: Vec<String>,\n}\n\n// RuntimeConfig is a partial version of the config used for GO apps\n// Only hosted services and gateways are parsed as those are the only ones\n// we need to produce ProcessConfigs\n#[derive(serde::Serialize, serde::Deserialize)]\nstruct RuntimeConfig {\n    #[serde(default)]\n    pub hosted_services: Vec<String>,\n    #[serde(default)]\n    pub gateways: Vec<GatewayConfig>,\n}\n\n#[derive(serde::Serialize, serde::Deserialize)]\nstruct GatewayConfig {\n    pub name: String,\n}\n"
  },
  {
    "path": "supervisor/src/lib.rs",
    "content": "pub mod config;\npub mod proxy;\npub mod supervisor;\n"
  },
  {
    "path": "supervisor/src/proxy.rs",
    "content": "use anyhow::{Context, Result};\nuse axum::async_trait;\nuse bytes::Bytes;\nuse hyper::header;\nuse pingora::http::ResponseHeader;\nuse pingora::protocols::http::error_resp;\nuse pingora::proxy::{http_proxy_service, FailToProxy, ProxyHttp, Session};\nuse pingora::server::configuration::{Opt, ServerConf};\nuse pingora::services::Service;\nuse pingora::upstreams::peer::HttpPeer;\nuse pingora::{Error, ErrorSource, ErrorType, OrErr};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std::net::SocketAddr;\nuse std::sync::Arc;\nuse tokio::sync::watch;\nuse tokio_util::sync::CancellationToken;\n\n#[derive(Clone)]\npub struct GatewayProxy {\n    services: HashMap<String, u16>,\n    upstream: SocketAddr,\n    client: reqwest::Client,\n}\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct HealthzResponse {\n    pub code: String,\n    pub message: String,\n    pub details: HealthzDetails,\n}\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct HealthzDetails {\n    pub app_revision: String,\n    pub encore_compiler: String,\n    pub deploy_id: String,\n    pub checks: Vec<HealthzCheckResult>,\n    pub enabled_experiments: Vec<String>,\n}\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct HealthzCheckResult {\n    pub name: String,\n    pub passed: bool,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub error: Option<String>,\n}\n\nimpl GatewayProxy {\n    pub fn new(\n        client: reqwest::Client,\n        upstream: SocketAddr,\n        services: HashMap<String, u16>,\n    ) -> Self {\n        GatewayProxy {\n            client,\n            upstream,\n            services,\n        }\n    }\n\n    pub async fn serve(self, listen_addr: String, token: CancellationToken) {\n        let conf = Arc::new(\n            ServerConf::new_with_opt_override(&Opt {\n                upgrade: false,\n                daemon: false,\n                nocapture: false,\n                test: false,\n                conf: None,\n            })\n            .unwrap(),\n        );\n        let mut proxy = http_proxy_service(&conf, self);\n\n        proxy.add_tcp(listen_addr.as_str());\n\n        let (tx, rx) = watch::channel(false);\n\n        tokio::select! {\n            _ = proxy.start_service(\n                #[cfg(unix)]\n                None,\n                rx,\n                1, // listeners_per_fd\n            ) => {},\n            _ = token.cancelled() => {\n                log::info!(\"Shutting down pingora proxy\");\n                tx.send(true).expect(\"failed to shutdown pingora\");\n            }\n        }\n    }\n\n    // concurrently calls /__encore/healthz for all services. Returns \"unhealthy\" if any of them\n    // does not return \"ok\".\n    pub async fn health_check(&self) -> Result<HealthzResponse> {\n        let handles = self.services.clone().into_iter().map(|(svc, port)| {\n            let client = self.client.clone();\n            let url = format!(\"http://127.0.0.1:{port}/__encore/healthz\");\n            tokio::spawn(async move {\n                let err_resp = || HealthzResponse {\n                    code: \"unhealthy\".to_string(),\n                    message: \"healhtcheck failed\".to_string(),\n                    details: HealthzDetails {\n                        app_revision: \"\".to_string(),\n                        encore_compiler: \"\".to_string(),\n                        deploy_id: \"\".to_string(),\n                        checks: vec![HealthzCheckResult {\n                            name: format!(\"service.{svc}.initialized\"),\n                            passed: false,\n                            error: None,\n                        }],\n                        enabled_experiments: vec![],\n                    },\n                };\n\n                match client\n                    .get(url.as_str())\n                    .send()\n                    .await\n                    .context(\"failed to get url\")\n                    .and_then(|r| {\n                        if r.status().is_success() {\n                            Ok(r)\n                        } else {\n                            Err(anyhow::anyhow!(\"Unsuccessful request\"))\n                        }\n                    }) {\n                    Ok(res) => {\n                        match res\n                            .json::<HealthzResponse>()\n                            .await\n                            .context(\"Failed to parse response body\")\n                        {\n                            Ok(res) => res,\n                            Err(_) => err_resp(),\n                        }\n                    }\n                    Err(_) => err_resp(),\n                }\n            })\n        });\n\n        let results: Vec<HealthzResponse> = futures::future::join_all(handles)\n            .await\n            .into_iter()\n            .map(|r| r.context(\"failed future\"))\n            .collect::<Result<Vec<_>>>()\n            .context(\"http healthcheck failed\")?;\n\n        results\n            .iter()\n            .fold(None::<HealthzResponse>, |rtn, resp| match rtn {\n                Some(mut res) => {\n                    if resp.code != \"ok\" {\n                        res.code = \"unhealthy\".to_string();\n                        res.details.checks.extend(resp.details.checks.clone())\n                    }\n                    Some(res)\n                }\n                None => Some(resp.clone()),\n            })\n            .ok_or(anyhow::anyhow!(\"No results\"))\n    }\n}\n\n#[async_trait]\nimpl ProxyHttp for GatewayProxy {\n    type CTX = Option<String>;\n\n    fn new_ctx(&self) -> Self::CTX {\n        None\n    }\n\n    // see https://github.com/cloudflare/pingora/blob/main/docs/user_guide/internals.md for\n    // details on when different filters are called.\n\n    async fn request_filter(\n        &self,\n        session: &mut Session,\n        _ctx: &mut Self::CTX,\n    ) -> pingora::Result<bool>\n    where\n        Self::CTX: Send + Sync,\n    {\n        if session.req_header().uri.path() == \"/__encore/healthz\" {\n            let healthz_resp = self\n                .health_check()\n                .await\n                .or_err(ErrorType::HTTPStatus(503), \"failed to run health check\")?;\n            let healthz_bytes: Vec<u8> = serde_json::to_vec(&healthz_resp)\n                .or_err(ErrorType::HTTPStatus(503), \"could not encode response\")?;\n\n            let code = if healthz_resp.code == \"ok\" { 200 } else { 503 };\n            let mut header = ResponseHeader::build(code, None)?;\n            header.insert_header(header::CONTENT_LENGTH, healthz_bytes.len())?;\n            header.insert_header(header::CONTENT_TYPE, \"application/json\")?;\n            session\n                .write_response_header(Box::new(header), false)\n                .await?;\n            session\n                .write_response_body(Some(Bytes::from(healthz_bytes)), true)\n                .await?;\n\n            return Ok(true);\n        }\n        Ok(false)\n    }\n\n    async fn upstream_peer(\n        &self,\n        _session: &mut Session,\n        _ctx: &mut Self::CTX,\n    ) -> pingora::Result<Box<HttpPeer>> {\n        let peer: HttpPeer = HttpPeer::new(self.upstream, false, \"localhost\".to_string());\n        Ok(Box::new(peer))\n    }\n\n    async fn fail_to_proxy(\n        &self,\n        session: &mut Session,\n        e: &Error,\n        _ctx: &mut Self::CTX,\n    ) -> FailToProxy\n    where\n        Self::CTX: Send + Sync,\n    {\n        // modified version of `Session::respond_error`\n\n        let code = match e.etype() {\n            ErrorType::HTTPStatus(code) => *code,\n            _ => {\n                match e.esource() {\n                    ErrorSource::Upstream => 502,\n                    ErrorSource::Downstream => {\n                        match e.etype() {\n                            ErrorType::WriteError\n                            | ErrorType::ReadError\n                            | ErrorType::ConnectionClosed => {\n                                /* conn already dead */\n                                return FailToProxy {\n                                    error_code: 0,\n                                    can_reuse_downstream: false,\n                                };\n                            }\n                            _ => 400,\n                        }\n                    }\n                    ErrorSource::Internal | ErrorSource::Unset => 500,\n                }\n            }\n        };\n\n        let (resp, body) = (\n            match code {\n                /* common error responses are pre-generated */\n                502 => error_resp::HTTP_502_RESPONSE.clone(),\n                400 => error_resp::HTTP_400_RESPONSE.clone(),\n                _ => error_resp::gen_error_response(code),\n            },\n            None,\n        );\n        session.set_keepalive(None);\n        session\n            .write_response_header(Box::new(resp), false)\n            .await\n            .unwrap_or_else(|e| {\n                log::error!(\"failed to send error response to downstream: {e}\");\n            });\n\n        session\n            .write_response_body(body, true)\n            .await\n            .unwrap_or_else(|e| log::error!(\"failed to write body: {e}\"));\n\n        FailToProxy {\n            error_code: code,\n            can_reuse_downstream: false,\n        }\n    }\n}\n"
  },
  {
    "path": "supervisor/src/supervisor.rs",
    "content": "//! Supervisor of Encore processes.\n//!\n//! The supervisor ensures all services (and gateways) hosted\n//! by the Encore deployment are started and running.\n\nuse std::{ffi::OsStr, io, os::unix::ffi::OsStrExt, path::PathBuf, time::Duration};\nuse tokio::process::{Child, Command};\nuse tokio_retry::Retry;\nuse tokio_util::{sync::CancellationToken, task::TaskTracker};\n\n/// The supervisor.\npub struct Supervisor {\n    /// The processes to supervise.\n    procs: Vec<Process>,\n}\n\nimpl Supervisor {\n    /// Creates a new supervisor.\n    pub fn new(procs: Vec<Process>) -> Self {\n        Self { procs }\n    }\n\n    /// Runs the supervisor.\n    ///\n    /// It returns when all processes have exited, due to either\n    /// crashing or cancellation.\n    pub async fn supervise(self, token: CancellationToken) {\n        let tracker = TaskTracker::new();\n\n        for p in self.procs {\n            let tok = token.clone();\n            tracker.spawn(async move {\n                let _ = p.run(tok.clone()).await;\n                tok.cancel();\n            });\n        }\n\n        tracker.close();\n        tracker.wait().await;\n    }\n}\n\n/// A supervised process.\npub struct Process {\n    /// The name of the process, for display purposes.\n    pub name: String,\n\n    /// The binary to start.\n    pub program: String,\n\n    /// Arguments to the program.\n    pub args: Vec<String>,\n\n    /// The working directory of the process.\n    pub cwd: PathBuf,\n\n    /// Env variables to set for the process.\n    /// The current process's env vars are NOT inherited.\n    pub env: Vec<(String, String)>,\n\n    /// How to restart the process if it exits.\n    pub restart_policy: Box<dyn RestartPolicy>,\n}\n\nimpl Process {\n    /// Runs the process, waiting for it to exit.\n    ///\n    /// It restarts the process on exit according to the restart policy,\n    /// unless the cancellation token is canceled.\n    async fn run(&self, token: CancellationToken) -> io::Result<()> {\n        let name = self.name.as_str();\n        Retry::spawn(self.restart_policy.retries(), move || {\n            let token = token.clone();\n\n            log::info!(proc = name; \"starting process\");\n\n            let res = self.run_once(token.clone());\n            async move {\n                // Wait for the process to complete.\n                let _ = res.await;\n                log::info!(proc = name; \"process exited\");\n                if token.is_cancelled() {\n                    Ok(())\n                } else {\n                    Err(io::Error::other(\"exited\"))\n                }\n            }\n        })\n        .await\n    }\n\n    async fn run_once(&self, token: CancellationToken) -> io::Result<()> {\n        // If the token is already cancelled, do nothing.\n        if token.is_cancelled() {\n            return Err(io::Error::other(\"canceled\"));\n        }\n\n        let envs = self.env.iter().map(|(k, v)| {\n            (\n                OsStr::from_bytes(k.as_bytes()),\n                OsStr::from_bytes(v.as_bytes()),\n            )\n        });\n\n        let mut cmd = Command::new(&self.program)\n            .args(&self.args)\n            .env_clear()\n            .envs(envs)\n            .current_dir(&self.cwd)\n            .spawn()?;\n\n        // Wait for the process to exit, or the token to be cancelled,\n        // whichever happens first.\n        tokio::select! {\n            status = cmd.wait() => status.map(|_| ()),\n\n            _ = token.cancelled() => {\n                kill_gracefully(&mut cmd).await\n            },\n        }\n    }\n}\n\n/// Attempts to kill a child process gracefully.\nasync fn kill_gracefully(child: &mut Child) -> io::Result<()> {\n    do_kill_gracefully(child).await\n}\n\n#[cfg(target_os = \"windows\")]\nasync fn do_kill_gracefully(child: &mut Child) -> io::Result<()> {\n    child.kill().await\n}\n\n#[cfg(not(target_os = \"windows\"))]\nasync fn do_kill_gracefully(child: &mut Child) -> io::Result<()> {\n    use std::time::Duration;\n    if let Some(pid) = child.id() {\n        for (sig, wait) in [\n            (libc::SIGINT, Duration::from_secs(2)),\n            (libc::SIGTERM, Duration::from_secs(2)),\n        ] {\n            unsafe {\n                libc::kill(pid as i32, sig);\n            }\n\n            tokio::select! {\n                _ = child.wait() => return Ok(()),\n                _ = tokio::time::sleep(wait) => {\n                    // Still running, escalate.\n                }\n            }\n        }\n    }\n\n    // We're out of graceful signals.\n    child.kill().await\n}\n\npub trait RestartPolicy: Send + Sync + 'static {\n    fn retries(&self) -> Box<dyn Iterator<Item = Duration> + Send + 'static>;\n}\n\nimpl<T> RestartPolicy for T\nwhere\n    T: Iterator<Item = Duration> + Clone + Send + Sync + 'static,\n{\n    fn retries(&self) -> Box<dyn Iterator<Item = Duration> + Send + 'static> {\n        Box::new(self.clone())\n    }\n}\n"
  },
  {
    "path": "tools/publicapigen/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/printer\"\n\t\"go/token\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/rs/zerolog/log\"\n\t\"golang.org/x/tools/go/ast/astutil\"\n\t\"golang.org/x/tools/imports\"\n)\n\ntype registeredType struct {\n\tnode ast.Expr\n\tdocs *ast.CommentGroup\n}\n\ntype registeredConstant struct {\n\tnode ast.Expr\n\ttyp  ast.Expr\n\tdocs *ast.CommentGroup\n}\n\ntype parsedFile struct {\n\tfileName string\n\tdir      string\n\tast      *ast.File\n}\n\nvar (\n\tresolvedRepo        = repoDir()\n\tgitRef              = repoCommit()\n\tfset                = token.NewFileSet()\n\tformattedFset       = token.NewFileSet()\n\tfiles               = []*parsedFile{}\n\tcommentsToAddToFile = map[*ast.File]map[string]*ast.CommentGroup{}\n\tconstants           = map[string]map[string]registeredConstant{}\n\ttypes               = map[string]map[string]registeredType{}\n\ttypesToDrop         = map[string]map[string]bool{}\n\tusesPanicWrapper    = map[string]bool{} // package dir -> true\n)\n\nfunc main() {\n\tlog.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Timestamp().Caller().Logger()\n\tlog.Info().Msg(\"generating public api\")\n\n\t// Walk the directory tree and parse all the Go files\n\tlog.Info().Msg(\"parsing source files...\")\n\tif err := walkDir(filepath.Join(resolvedRepo, \"runtimes\", \"go\"), \"./\", readAST); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"unable to walk runtime directory to parse go files\")\n\t}\n\tslices.SortFunc(files, func(a, b *parsedFile) int {\n\t\treturn cmp.Compare(a.fileName, b.fileName)\n\t})\n\n\t// Register all consts and types in our private files, just in case we reference them in the public API\n\tlog.Info().Msg(\"registering types...\")\n\tfor _, f := range files {\n\t\tif isPrivateFile(f.fileName) {\n\t\t\tlog.Debug().Str(\"file\", f.fileName).Msg(\"registering types and constants from private implementation files\")\n\t\t\tregisterTypes(f.fileName, f.ast)\n\t\t}\n\t\tregisterTypesToDrop(f.ast)\n\t}\n\n\t// Then rewrite all the AST to remove implementations\n\tlog.Info().Msg(\"rewriting ast to remove implementations and unexported items...\")\n\tremaining := make([]*parsedFile, 0, len(files))\n\tfor _, f := range files {\n\t\tlog.Debug().Str(\"file\", f.fileName).Msg(\"rewriting ast\")\n\t\tusesPanic, err := rewriteAST(f.ast)\n\t\tif err != nil {\n\t\t\tlog.Fatal().Err(err).Str(\"file\", f.fileName).Msg(\"unable to rewrite ast\")\n\t\t}\n\n\t\tif len(f.ast.Decls) == 0 && f.ast.Doc == nil {\n\t\t\tlog.Debug().Str(\"file\", f.fileName).Msg(\"removing file as there are no exported decelerations or package comments\")\n\t\t\tcontinue\n\t\t}\n\n\t\tif usesPanic {\n\t\t\tusesPanicWrapper[f.dir] = true\n\t\t}\n\n\t\tremaining = append(remaining, f)\n\t}\n\tfiles = remaining\n\n\twrittenPanicWrapper := make(map[string]bool) // package dir -> true\n\n\t// Then write the AST to a file\n\toutDir := outDir()\n\tlog.Info().Str(\"out\", outDir).Msg(\"writing public api files...\")\n\tfor _, f := range files {\n\t\tif isPrivateFile(f.fileName) {\n\t\t\t// \"runtime\" is a private package as are internal packages\n\t\t\t// any files suffixed with _internal.go are also private and are considered unstable API's\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Debug().Str(\"file\", f.fileName).Msg(\"writing public api file\")\n\n\t\t// Pretty print the file and then re-parse it\n\t\t// This repopulates the AST with the comments and formatting\n\t\tformattedFile := convertASTToFormattedSrc(fset, f.ast, f.fileName, nil)\n\t\tformattedAST, err := parser.ParseFile(formattedFset, f.fileName, formattedFile, parser.ParseComments)\n\t\tif err != nil {\n\t\t\tlog.Fatal().Err(err).Str(\"file\", f.fileName).Msg(\"unable to convert back to an AST\")\n\t\t}\n\n\t\t// Now we can add brand new comments list given set, we\n\t\t// can write the help comment into the functions\n\t\twritePendingComments(f.ast, formattedAST)\n\n\t\t// Now let's write the file out\n\t\toutputFile := filepath.Join(outDir, f.fileName)\n\t\toutputDir := filepath.Dir(outputFile)\n\n\t\tif err := os.MkdirAll(outputDir, 0755); err != nil {\n\t\t\tlog.Fatal().Err(err).Str(\"dir\", outputDir).Msg(\"unable to create output directory\")\n\t\t}\n\n\t\tif isEmptyFile(formattedAST) {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar extraContents []byte\n\t\tdoWritePanicWrapper := usesPanicWrapper[f.dir] && !writtenPanicWrapper[f.dir]\n\t\tif doWritePanicWrapper {\n\t\t\textraContents = []byte(panicWrapperSnippet)\n\t\t\twrittenPanicWrapper[f.dir] = true\n\t\t}\n\t\tout := convertASTToFormattedSrc(formattedFset, formattedAST, f.fileName, extraContents)\n\n\t\tif err := os.WriteFile(outputFile, out, 0644); err != nil {\n\t\t\tlog.Fatal().Err(err).Str(\"file\", f.fileName).Msg(\"unable to write file\")\n\t\t}\n\t}\n\n\tif err := buildsSuccessfully(outDir); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"generated code does not build\")\n\t}\n\n\tlog.Info().Msg(\"done\")\n}\n\nfunc isEmptyFile(f *ast.File) bool {\n\treturn len(f.Decls) == 0 && f.Doc.Text() == \"\"\n}\n\nfunc convertASTToFormattedSrc(fset *token.FileSet, fAST *ast.File, fileName string, extraContents []byte) []byte {\n\t// Print the AST to a buffer\n\tvar buf bytes.Buffer\n\tif err := printer.Fprint(&buf, fset, fAST); err != nil {\n\t\tlog.Fatal().Err(err).Str(\"file\", fileName).Msg(\"unable to write ast to file\")\n\t}\n\n\t// Add any extra contents\n\tbuf.Write(extraContents)\n\n\t// Then pass that to goimports to format the imports\n\timports.LocalPrefix = \"encore.dev\"\n\tformatted, err := imports.Process(fileName, buf.Bytes(), nil)\n\tif err != nil {\n\t\tlog.Fatal().Err(err).Str(\"file\", fileName).Msg(\"unable to process imports\")\n\t}\n\n\treturn formatted\n}\n\nfunc registerTypes(name string, fAST *ast.File) {\n\tpkg := filepath.Base(filepath.Dir(name))\n\tif _, found := types[pkg]; !found {\n\t\ttypes[pkg] = map[string]registeredType{}\n\t\tconstants[pkg] = map[string]registeredConstant{}\n\t}\n\n\tfor _, decl := range fAST.Decls {\n\t\tswitch d := decl.(type) {\n\t\tcase *ast.GenDecl:\n\t\t\tfor _, spec := range d.Specs {\n\t\t\t\tswitch s := spec.(type) {\n\t\t\t\tcase *ast.TypeSpec:\n\t\t\t\t\tif s.Name != nil && s.Name.IsExported() {\n\t\t\t\t\t\tlog.Debug().Str(\"type\", s.Name.Name).Msg(\"registering type\")\n\t\t\t\t\t\ttypes[pkg][s.Name.Name] = registeredType{s.Type, removePosFromCommentGroup(d.Doc)}\n\t\t\t\t\t}\n\t\t\t\tcase *ast.ValueSpec:\n\t\t\t\t\tdir := lookupDirective(d.Doc)\n\t\t\t\t\tif d.Tok == token.CONST || (d.Tok == token.VAR && dir == mustKeep) {\n\t\t\t\t\t\tfor i, name := range s.Names {\n\t\t\t\t\t\t\tif len(s.Values) <= i {\n\t\t\t\t\t\t\t\tif len(s.Values) == 0 && name.IsExported() {\n\t\t\t\t\t\t\t\t\tlog.Debug().Str(\"const\", name.Name).Msg(\"registering enum const\")\n\t\t\t\t\t\t\t\t\tconstants[pkg][name.Name] = registeredConstant{nil, nil, removePosFromCommentGroup(s.Doc)}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif name.IsExported() {\n\t\t\t\t\t\t\t\tlog.Debug().Str(\"const\", name.Name).Interface(\"value\", s.Values[i]).Msg(\"registering basic const\")\n\t\t\t\t\t\t\t\tconstants[pkg][name.Name] = registeredConstant{removePosition(s.Values[i]), removePosition(s.Type), removePosFromCommentGroup(s.Doc)}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc registerTypesToDrop(fAST *ast.File) {\n\tpkg := fAST.Name.Name\n\tif _, found := typesToDrop[pkg]; !found {\n\t\ttypesToDrop[pkg] = map[string]bool{}\n\t}\n\n\tfor _, decl := range fAST.Decls {\n\t\tswitch d := decl.(type) {\n\t\tcase *ast.GenDecl:\n\t\t\tfor _, spec := range d.Specs {\n\t\t\t\tswitch s := spec.(type) {\n\t\t\t\tcase *ast.TypeSpec:\n\t\t\t\t\tif lookupDirective(d.Doc, s.Doc) == mustDrop {\n\t\t\t\t\t\tfmt.Printf(\"dropping %s.%s\\n\", pkg, s.Name.Name)\n\t\t\t\t\t\ttypesToDrop[pkg][s.Name.Name] = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// readAST parses the AST of all non-test Go files in a directory and stores it in the files map\nfunc readAST(path, rel string, file []os.DirEntry) error {\n\tfor _, f := range file {\n\t\tif !strings.HasSuffix(f.Name(), \".go\") {\n\t\t\t// ignore non-go files\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasSuffix(f.Name(), \"_test.go\") {\n\t\t\t// Ignore test files\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Debug().Str(\"rel\", rel).Str(\"file\", f.Name()).Msg(\"parsing file\")\n\n\t\tfAST, err := parser.ParseFile(fset, filepath.Join(path, f.Name()), nil, parser.ParseComments)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error parsing %s: %w\", f.Name(), err)\n\t\t}\n\n\t\t// We only want to track comments if they are part of a decl we're keeping\n\t\t// so we need to nil out this field on the file so they aren't tracked globally\n\t\tfAST.Comments = nil\n\t\tfiles = append(files, &parsedFile{\n\t\t\tfileName: filepath.Join(rel, f.Name()),\n\t\t\tdir:      rel,\n\t\t\tast:      fAST,\n\t\t})\n\t}\n\n\treturn nil\n}\n\nfunc rewriteAST(f *ast.File) (usesPanicWrapper bool, err error) {\n\tvar lastIfaceType *ast.InterfaceType\n\n\tastutil.Apply(\n\t\tf,\n\t\tfunc(c *astutil.Cursor) bool {\n\t\t\tswitch node := c.Node().(type) {\n\t\t\tcase *ast.ImportSpec:\n\t\t\t\t// Drop \"_\" imports as they are implementation details and not needed in the API contract\n\t\t\t\tif node.Name != nil && node.Name.Name == \"_\" {\n\t\t\t\t\tc.Delete()\n\t\t\t\t}\n\t\t\tcase *ast.FuncDecl:\n\t\t\t\tdir := lookupDirective(node.Doc)\n\t\t\t\tif dir == mustKeep {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\t\t// Should we delete this function declaration if it's unexported or a receiver on an unexported object?\n\t\t\t\tshouldDelete := !node.Name.IsExported() || dir == mustDrop\n\t\t\t\tif node.Recv != nil {\n\t\t\t\t\tfor i, field := range node.Recv.List {\n\t\t\t\t\t\tif ident := typeName(field.Type); ident != nil && (!ident.IsExported() || typesToDrop[f.Name.Name][ident.Name]) {\n\t\t\t\t\t\t\tshouldDelete = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove the node name from the receiver as we won't reference it inside an empty body\n\t\t\t\t\t\t// if we keep the field\n\t\t\t\t\t\tnode.Recv.List[i].Names = nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif shouldDelete {\n\t\t\t\t\tclearCommentGroup(node.Doc)\n\t\t\t\t\tc.Delete()\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\t\tstart := token.NoPos\n\t\t\t\tend := token.NoPos\n\n\t\t\t\tif node.Body != nil {\n\t\t\t\t\tstart = node.Body.Lbrace\n\t\t\t\t\tend = node.Body.Rbrace\n\n\t\t\t\t\tif _, found := commentsToAddToFile[f]; !found {\n\t\t\t\t\t\tcommentsToAddToFile[f] = map[string]*ast.CommentGroup{}\n\t\t\t\t\t}\n\n\t\t\t\t\tstartLine := fset.Position(start).Line\n\t\t\t\t\tendLine := fset.Position(end).Line\n\t\t\t\t\tfilePath := strings.TrimPrefix(fset.File(node.Pos()).Name(), resolvedRepo)\n\n\t\t\t\t\tcommentsToAddToFile[f][funcName(node)] = &ast.CommentGroup{\n\t\t\t\t\t\tList: []*ast.Comment{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tText: \"// Encore will provide an implementation to this function at runtime, we do not expose\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tText: \"// the implementation in the API contract as it is an implementation detail, which may change\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tText: \"// between releases.\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tText: \"//\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tText: \"// The current implementation of this function can be found here:\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tText: \"//    https://github.com/encoredev/encore/blob/\" + gitRef + filePath + \"#L\" + strconv.Itoa(startLine) + \"-L\" + strconv.Itoa(endLine),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Drop any parameters that are prefixed with \"__\" as these are used to indicate that\n\t\t\t\t// the arguments are added in by Encore's code generators and should be ignored in\n\t\t\t\t// the customer facing code.\n\t\t\t\tnewFieldList := &ast.FieldList{\n\t\t\t\t\tOpening: token.NoPos,\n\t\t\t\t\tClosing: token.NoPos,\n\t\t\t\t}\n\t\t\t\tfor _, p := range node.Type.Params.List {\n\t\t\t\t\tif len(p.Names) == 0 || strings.HasPrefix(p.Names[0].Name, \"__\") {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tnewFieldList.List = append(newFieldList.List, p)\n\t\t\t\t}\n\t\t\t\tnode.Type.Params = newFieldList\n\n\t\t\t\t// If we have any results, add a placeholder names so we can use a naked return.\n\t\t\t\tresults := node.Type.Results\n\t\t\t\tif results != nil && results.NumFields() > 0 {\n\t\t\t\t\tfor _, res := range results.List {\n\t\t\t\t\t\tif len(res.Names) == 0 {\n\t\t\t\t\t\t\tres.Names = []*ast.Ident{ast.NewIdent(\"_\")}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If we are keeping the function, replace the implementation with a panic\n\t\t\t\t// as the code we're generating is only to help the IDE and users understand our API\n\t\t\t\t// but isn't intended to be used in running apps\n\t\t\t\tnode.Body = &ast.BlockStmt{\n\t\t\t\t\tLbrace: token.NoPos,\n\t\t\t\t\tList: []ast.Stmt{\n\t\t\t\t\t\t&ast.ExprStmt{\n\t\t\t\t\t\t\tX: &ast.CallExpr{\n\t\t\t\t\t\t\t\tFun:    ast.NewIdent(\"doPanic\"),\n\t\t\t\t\t\t\t\tLparen: start,\n\t\t\t\t\t\t\t\tArgs: []ast.Expr{\n\t\t\t\t\t\t\t\t\t&ast.BasicLit{\n\t\t\t\t\t\t\t\t\t\tValuePos: token.NoPos,\n\t\t\t\t\t\t\t\t\t\tKind:     token.STRING,\n\t\t\t\t\t\t\t\t\t\tValue:    \"\\\"encore apps must be run using the encore command\\\"\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tEllipsis: 0,\n\t\t\t\t\t\t\t\tRparen:   end,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&ast.ReturnStmt{},\n\t\t\t\t\t},\n\t\t\t\t\tRbrace: token.NoPos,\n\t\t\t\t}\n\t\t\t\tusesPanicWrapper = true\n\t\t\t\treturn false\n\n\t\t\tcase *ast.TypeSpec:\n\t\t\t\tvar genDeclComment *ast.CommentGroup\n\t\t\t\tif gd, ok := c.Parent().(*ast.GenDecl); ok {\n\t\t\t\t\tgenDeclComment = gd.Doc\n\t\t\t\t}\n\n\t\t\t\tdir := lookupDirective(node.Doc, node.Comment, genDeclComment)\n\t\t\t\tkeep := node.Name.IsExported() || dir == mustKeep\n\t\t\t\tif dir == mustDrop || typesToDrop[f.Name.Name][node.Name.Name] {\n\t\t\t\t\tkeep = false\n\t\t\t\t}\n\n\t\t\t\t// Remove unexported types\n\t\t\t\tif !keep {\n\t\t\t\t\tc.Delete()\n\t\t\t\t\treturn false\n\t\t\t\t} else if sel, ok := node.Type.(*ast.SelectorExpr); ok {\n\t\t\t\t\t// else if we have a selector, rewrite the package name to see if this is an export from\n\t\t\t\t\t// one of our private packages, in which case we want to copy it into the public API\n\t\t\t\t\tif pkg, ok := sel.X.(*ast.Ident); ok && types[pkg.Name] != nil {\n\t\t\t\t\t\ttyp, found := types[pkg.Name][sel.Sel.Name]\n\t\t\t\t\t\tif found {\n\t\t\t\t\t\t\tnode.Assign = token.NoPos // remove an alias assignment (as it was an alias for our own types)\n\t\t\t\t\t\t\tnode.Type = typ.node      // replace the type with the actual type\n\t\t\t\t\t\t\tif typ.docs != nil {\n\t\t\t\t\t\t\t\tif parent, ok := c.Parent().(*ast.GenDecl); ok {\n\t\t\t\t\t\t\t\t\tif parent.Doc == nil {\n\t\t\t\t\t\t\t\t\t\tparent.Doc = typ.docs // copy the docs over\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else if node.Doc == nil {\n\t\t\t\t\t\t\t\t\tnode.Doc = typ.docs // copy the docs over\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase *ast.FuncType:\n\t\t\t\treturn false\n\n\t\t\tcase *ast.InterfaceType:\n\t\t\t\tlastIfaceType = node\n\n\t\t\tcase *ast.ValueSpec:\n\t\t\t\t// Remove unexported variables and constants\n\t\t\t\tkeep := false\n\t\t\t\tfor i, name := range node.Names {\n\t\t\t\t\tif name.IsExported() {\n\t\t\t\t\t\tkeep = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnode.Names[i] = ast.NewIdent(\"_\")\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdir := lookupDirective(node.Doc, node.Comment)\n\t\t\t\tif dir == mustKeep {\n\t\t\t\t\tkeep = true\n\t\t\t\t} else if dir == mustDrop {\n\t\t\t\t\tkeep = false\n\t\t\t\t}\n\n\t\t\t\tif !keep {\n\t\t\t\t\tc.Delete()\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\tcase *ast.GenDecl:\n\t\t\t\tdir := lookupDirective(node.Doc)\n\t\t\t\tif dir == mustKeep {\n\t\t\t\t\treturn false\n\t\t\t\t} else if dir == mustDrop {\n\t\t\t\t\tc.Delete()\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\t\t// else if we have a selector, rewrite the package name to see if this is an export from\n\t\t\t\t// one of our private packages, in which case we want to copy it into the public API\n\t\t\t\tif node.Tok == token.CONST || node.Tok == token.VAR {\n\t\t\t\t\tfor _, spec := range node.Specs {\n\t\t\t\t\t\tswitch spec := spec.(type) {\n\t\t\t\t\t\tcase *ast.ValueSpec:\n\t\t\t\t\t\t\tfor i, value := range spec.Values {\n\t\t\t\t\t\t\t\tif selector, ok := value.(*ast.SelectorExpr); ok {\n\t\t\t\t\t\t\t\t\tif pkg, ok := selector.X.(*ast.Ident); ok && constants[pkg.Name] != nil {\n\t\t\t\t\t\t\t\t\t\ttyp, found := constants[pkg.Name][selector.Sel.Name]\n\t\t\t\t\t\t\t\t\t\tif found {\n\t\t\t\t\t\t\t\t\t\t\tif typ.node == nil && typ.typ == nil {\n\t\t\t\t\t\t\t\t\t\t\t\tspec.Values = nil\n\t\t\t\t\t\t\t\t\t\t\t\tspec.Type = nil\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tspec.Values[i] = typ.node\n\n\t\t\t\t\t\t\t\t\t\t\t\tif typ.typ != nil {\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Copy the type over\n\t\t\t\t\t\t\t\t\t\t\t\t\tspec.Type = typ.typ\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\tif typ.docs != nil && spec.Doc == nil {\n\t\t\t\t\t\t\t\t\t\t\t\tif _, found := commentsToAddToFile[f]; !found {\n\t\t\t\t\t\t\t\t\t\t\t\t\tcommentsToAddToFile[f] = map[string]*ast.CommentGroup{}\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\t\t\t// copy the docs over in a later stage\n\t\t\t\t\t\t\t\t\t\t\t\t// otherwise we'll put the first line of the doc in the wrong place\n\t\t\t\t\t\t\t\t\t\t\t\tcommentsToAddToFile[f][spec.Names[0].Name] = typ.docs\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase *ast.Field:\n\t\t\t\t// Keep exported fields if there's a doc saying we should\n\t\t\t\tdir := lookupDirective(node.Doc, node.Comment)\n\t\t\t\tkeep := dir == mustKeep\n\n\t\t\t\tif !keep {\n\t\t\t\t\t// Remove unexported fields from structs\n\t\t\t\t\tfor i, name := range node.Names {\n\t\t\t\t\t\tif name.IsExported() {\n\t\t\t\t\t\t\tkeep = true\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnode.Names[i] = ast.NewIdent(\"_\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Is this field a type list? If so, keep it.\n\t\t\t\tif bin, ok := node.Type.(*ast.BinaryExpr); ok && bin.Op == token.OR && node.Names == nil {\n\t\t\t\t\tkeep = true\n\t\t\t\t}\n\t\t\t\t// Or is it a type list with a single field within an interface?\n\t\t\t\tif node.Names == nil && lastIfaceType != nil && posWithin(node.Pos(), lastIfaceType) {\n\t\t\t\t\tkeep = true\n\t\t\t\t}\n\n\t\t\t\tif dir == mustDrop {\n\t\t\t\t\tkeep = false\n\t\t\t\t}\n\n\t\t\t\tif !keep {\n\t\t\t\t\tc.Delete()\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\t\tfunc(c *astutil.Cursor) bool {\n\t\t\tswitch node := c.Node().(type) {\n\t\t\tcase *ast.GenDecl:\n\t\t\t\tif len(node.Specs) == 0 {\n\t\t\t\t\tclearCommentGroup(node.Doc)\n\t\t\t\t\tc.Delete()\n\t\t\t\t}\n\n\t\t\tcase *ast.StructType:\n\t\t\t\tif len(node.Fields.List) == 0 {\n\t\t\t\t\tnode.Fields.List = append(node.Fields.List, &ast.Field{\n\t\t\t\t\t\tDoc:   nil,\n\t\t\t\t\t\tNames: []*ast.Ident{ast.NewIdent(\"_\")},\n\t\t\t\t\t\tType:  ast.NewIdent(\"int\"),\n\t\t\t\t\t\tTag:   nil,\n\t\t\t\t\t\tComment: &ast.CommentGroup{List: []*ast.Comment{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSlash: token.NoPos,\n\t\t\t\t\t\t\t\tText:  \"// for godoc to show unexported fields\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\t)\n\treturn usesPanicWrapper, nil\n}\n\nfunc writePendingComments(originalFile *ast.File, formattedFile *ast.File) {\n\tcomments, found := commentsToAddToFile[originalFile]\n\tif !found {\n\t\treturn\n\t}\n\n\tastutil.Apply(formattedFile, func(c *astutil.Cursor) bool {\n\t\tswitch node := c.Node().(type) {\n\t\tcase *ast.FuncDecl:\n\t\t\tif comments, found := comments[funcName(node)]; found {\n\t\t\t\t// check if the body is inline like this:\n\t\t\t\t//   func Foo() { panic(\"foo\") }\n\t\t\t\t// and if so, then add a new line to the beginning of the comment\n\t\t\t\t// to force it to be formatted like:\n\t\t\t\t//   func Foo() {\n\t\t\t\t//      // comment\n\t\t\t\t//      panic(\"foo\")\n\t\t\t\t//   }\n\t\t\t\tfile := formattedFset.File(node.Pos())\n\t\t\t\tpanicPos := node.Body.List[0].Pos()\n\t\t\t\tpanicLine := file.Line(panicPos)\n\t\t\t\tif file.LineStart(panicLine) < panicPos-3 {\n\t\t\t\t\tcomments.List[0].Text = \"\\n\" + comments.List[0].Text\n\t\t\t\t}\n\n\t\t\t\t// Position the comment just before the first expression in the body\n\t\t\t\tcomments.List[0].Slash = panicPos - 1\n\t\t\t\tformattedFile.Comments = append(formattedFile.Comments, comments)\n\t\t\t}\n\n\t\tcase *ast.GenDecl:\n\t\t\tif node.Tok == token.CONST {\n\t\t\t\tfor _, spec := range node.Specs {\n\t\t\t\t\tname := spec.(*ast.ValueSpec).Names[0]\n\t\t\t\t\tif comments, found := comments[name.Name]; found {\n\t\t\t\t\t\tfile := formattedFset.File(node.Pos())\n\t\t\t\t\t\tcomments.List[0].Slash = file.LineStart(file.Line(name.Pos()))\n\t\t\t\t\t\tformattedFile.Comments = append(formattedFile.Comments, comments)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}, nil)\n\n\t// The comments _must_ be sorted, otherwise the formatter will get confused\n\t// and put all out of order comments under the last ordered comment\n\tsort.SliceStable(formattedFile.Comments, func(i, j int) bool {\n\t\treturn formattedFile.Comments[i].List[0].Slash < formattedFile.Comments[j].List[0].Slash\n\t})\n}\n\n// walkDir recursively descends path, calling walkFn for directory\nfunc walkDir(dir, rel string, f func(path, rel string, files []os.DirEntry) error) error {\n\tif rel == \"types/uuid\" {\n\t\t// we don't want to rewrite this package\n\t\treturn nil\n\t}\n\n\tlog.Debug().Str(\"rel\", rel).Msg(\"walking directory\")\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Split the files and dirs\n\tvar dirs, files []os.DirEntry\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tdirs = append(dirs, entry)\n\t\t} else {\n\t\t\tfiles = append(files, entry)\n\t\t}\n\t}\n\n\tif err := f(dir, rel, files); err != nil {\n\t\treturn fmt.Errorf(\"error walking %s: %w\", rel, err)\n\t}\n\n\tfor _, d := range dirs {\n\t\tdir2 := filepath.Join(dir, d.Name())\n\t\trel2 := path.Join(rel, d.Name())\n\n\t\tif err := walkDir(dir2, rel2, f); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc repoDir() string {\n\tcmd := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\")\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tpanic(\"unable to get repo directory\")\n\t}\n\treturn string(bytes.TrimSpace(out))\n}\n\nfunc repoCommit() string {\n\t// First check if we have a tag pointed at this commit\n\tcmd := exec.Command(\"git\", \"tag\", \"--points-at\", \"HEAD\")\n\tcmd.Dir = resolvedRepo\n\tout, err := cmd.CombinedOutput()\n\n\t// If that doesn't work, then just get the commit hash\n\tif err != nil || string(bytes.TrimSpace(out)) == \"\" {\n\t\tcmd := exec.Command(\"git\", \"rev-parse\", \"HEAD\")\n\t\tcmd.Dir = resolvedRepo\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tpanic(\"unable to get repo commit\")\n\t\t}\n\t\treturn string(bytes.TrimSpace(out))\n\t}\n\n\tparts := strings.Split(string(bytes.TrimSpace(out)), \"\\n\")\n\treturn strings.TrimSpace(parts[0])\n}\n\nfunc outDir() string {\n\tsrcDir := filepath.Dir(repoDir())\n\treturn filepath.Join(srcDir, \"encore.dev\")\n}\n\n// typeName returns the identifier of the expression unwrapping any pointers, selectors or generics, or nil if it is not an identifier\nfunc typeName(node ast.Expr) *ast.Ident {\n\t// Remove any wrapped references\n\tfor {\n\t\tif star, ok := node.(*ast.StarExpr); ok {\n\t\t\tnode = star.X\n\t\t\tcontinue\n\t\t}\n\n\t\tif index, ok := node.(*ast.IndexExpr); ok {\n\t\t\tnode = index.X\n\t\t\tcontinue\n\t\t}\n\n\t\tif indexList, ok := node.(*ast.IndexListExpr); ok {\n\t\t\tnode = indexList.X\n\t\t\tcontinue\n\t\t}\n\n\t\tif selector, ok := node.(*ast.SelectorExpr); ok {\n\t\t\tnode = selector.X\n\t\t}\n\n\t\tbreak\n\t}\n\n\tif ident, ok := node.(*ast.Ident); ok {\n\t\treturn ident\n\t}\n\treturn nil\n}\n\nfunc clearCommentGroup(node *ast.CommentGroup) {\n\tif node == nil {\n\t\treturn\n\t}\n\tnode.List = []*ast.Comment{node.List[0]}\n\tnode.List[0].Text = \"  \" // double space to prevent a panic when printing the file back out\n}\n\ntype keepDirective string\n\nconst (\n\tnone     keepDirective = \"none\"\n\tmustKeep               = \"keep\"\n\tmustDrop               = \"drop\"\n)\n\n// directiveCache caches the directive for a given comment group,\n// as the lookupDirective function mutates the comment which would\n// otherwise cause subsequent calls to return a different value.\nvar directiveCache = make(map[*ast.CommentGroup]keepDirective)\n\nfunc lookupDirective(nodes ...*ast.CommentGroup) keepDirective {\n\tresult := none\n\tfor _, node := range nodes {\n\t\tdir, cached := directiveCache[node]\n\t\tif !cached {\n\t\t\tdir = parseDirective(node)\n\t\t\tclearDirectives(node)\n\t\t\tdirectiveCache[node] = dir\n\t\t}\n\n\t\tif dir != none {\n\t\t\tresult = dir\n\t\t}\n\t}\n\treturn result\n}\n\nfunc parseDirective(node *ast.CommentGroup) keepDirective {\n\tif node != nil && node.List != nil {\n\t\tfor _, comment := range node.List {\n\t\t\ttext := strings.TrimSpace(comment.Text)\n\t\t\tswitch text {\n\t\t\tcase \"//publicapigen:keep\":\n\t\t\t\treturn mustKeep\n\t\t\tcase \"//publicapigen:drop\":\n\t\t\t\treturn mustDrop\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\treturn none\n}\n\nfunc clearDirectives(node *ast.CommentGroup) {\n\tif node != nil && node.List != nil {\n\t\tfor i, comment := range node.List {\n\t\t\ttext := strings.TrimSpace(comment.Text)\n\t\t\tswitch text {\n\t\t\tcase \"//publicapigen:keep\":\n\t\t\tcase \"//publicapigen:drop\":\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif i == 0 {\n\t\t\t\tcomment.Text = \"  \"\n\t\t\t} else {\n\t\t\t\tcomment.Text = \"//\" // empty comment line as I want the docs to remain active, but I can't remove this without causing a blank line between the comment group and what ever it's associated with\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc isPrivateFile(fileName string) bool {\n\treturn strings.HasPrefix(fileName, \"appruntime/\") ||\n\t\tstrings.Contains(fileName, \"internal/\") ||\n\t\tstrings.HasSuffix(fileName, \"_internal.go\")\n}\n\nfunc removePosFromCommentGroup(doc *ast.CommentGroup) *ast.CommentGroup {\n\tif doc == nil {\n\t\treturn nil\n\t}\n\n\trtn := *doc\n\n\tfor i, originalLine := range rtn.List {\n\t\tline := *originalLine\n\t\tline.Slash = token.NoPos\n\t\trtn.List[i] = &line\n\t}\n\treturn &rtn\n}\n\nfunc removePosition(node ast.Expr) ast.Expr {\n\tif node == nil {\n\t\treturn nil\n\t}\n\n\tswitch node := node.(type) {\n\tcase *ast.BasicLit:\n\t\tlit := *node\n\t\tlit.ValuePos = token.NoPos\n\t\treturn &lit\n\n\tcase *ast.Ident:\n\t\tident := *node\n\t\tident.NamePos = token.NoPos\n\t\treturn &ident\n\n\tcase *ast.SelectorExpr:\n\t\tsel := *node\n\t\tsel.X = removePosition(sel.X)\n\t\tsel.Sel = removePosition(sel.Sel).(*ast.Ident)\n\t\treturn &sel\n\n\tcase *ast.UnaryExpr:\n\t\tunary := *node\n\t\tunary.OpPos = token.NoPos\n\t\tunary.X = removePosition(unary.X)\n\t\treturn &unary\n\n\tcase *ast.BinaryExpr:\n\t\tbinary := *node\n\t\tbinary.OpPos = token.NoPos\n\t\tbinary.X = removePosition(binary.X)\n\t\tbinary.Y = removePosition(binary.Y)\n\t\treturn &binary\n\n\tdefault:\n\t\tlog.Warn().Interface(\"node\", node).Msg(\"unhandled node type to remove position from\")\n\t\treturn node\n\t}\n}\n\nfunc funcName(node *ast.FuncDecl) string {\n\tvar name strings.Builder\n\n\tif node.Recv != nil {\n\t\tname.WriteRune('(')\n\t\ttyp := node.Recv.List[0].Type\n\n\t\tif star, ok := typ.(*ast.StarExpr); ok {\n\t\t\ttyp = star.X\n\t\t\tname.WriteRune('*')\n\t\t}\n\n\t\tif index, ok := typ.(*ast.IndexExpr); ok {\n\t\t\tname.WriteString(index.X.(*ast.Ident).Name)\n\t\t\tname.WriteString(\"[]\")\n\t\t} else if indexList, ok := typ.(*ast.IndexListExpr); ok {\n\t\t\tname.WriteString(indexList.X.(*ast.Ident).Name)\n\t\t\tname.WriteString(\"[]\")\n\t\t} else {\n\t\t\tname.WriteString(typ.(*ast.Ident).Name)\n\t\t}\n\n\t\tname.WriteString(\").\")\n\t}\n\n\tname.WriteString(node.Name.Name)\n\treturn name.String()\n}\n\nconst panicWrapperSnippet = `\n// doPanic is a wrapper around panic to prevent static analysis tools\n// from thinking Encore APIs unconditionally panic.,\nfunc doPanic(v any) {\n\tif os.Getenv(\"ENCORERUNTIME_NOPANIC\") == \"\" {\n\t\tpanic(v)\n\t}\n}\n`\n\nfunc posWithin(pos token.Pos, node ast.Node) bool {\n\treturn pos >= node.Pos() && pos < node.End()\n}\n\nfunc buildsSuccessfully(dir string) error {\n\tcmd := exec.Command(\"go\", \"build\", \"./...\")\n\tcmd.Dir = dir\n\t// nosemgrep trailofbits.go.invalid-usage-of-modified-variable.invalid-usage-of-modified-variable\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\terr = fmt.Errorf(\"'go build' failed: %v: %s\", err, out)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "tools/semgrep-rules/README.md",
    "content": "# SemGrep Rules\n\nThis directory contains a set of Semgrep rules we run against Encore's source code. You can find more information about the syntax of rules in the [Semgrep documentation](https://semgrep.dev/docs/writing-rules/overview/).\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 Damian Gryski <damian@gryski.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/README.md",
    "content": "semgrep-go\n==========\n\n> *Encore Note*\n> \n> We've copied in the rules from https://github.com/dgryski/semgrep-go on 2023-07-27\n> However delete some rules that we don't want to run in our codebase and updated\n> some other rules.\n\nThis repo holds patterns for finding odd Go code.\n\nThe rules engines currently supported:\n\n* [semgrep](https://semgrep.dev/)\n* [ruleguard](https://github.com/quasilyte/go-ruleguard)\n\nI'll accept [comby](https://comby.dev) patterns if you can't get them to work with either semgrep or ruleguard.\n\nTo run a single semgrep rule:\n\n```\n$ semgrep -f path/to/semgrep-go/rule.yml .\n```\n\nTo run all semgrep rules:\n\n```\n$ semgrep -f path/to/semgrep-go/ .\n```\n\nTo run all the ruleguard rules:\n\n```\n$ ruleguard -c=0 -rules path/to/semgrep-go/ruleguard.rules.go ./...\n```\n\n\nSemgrep checks:\n* badexponentiation: check for `2^x` and `10^x` which look like exponentiation\n* badnilguard: check for poorly formed nil guards\n* errtodo: check for TODOs in error handling code\n* hmac-bytes: check for using bytes.Equal() with HMACs\n* hostport: check for using fmt.Sprintf() instead of net.JoinHostPort()\n* mathbits: check for places you should use math/bits instead\n* mail-address: check for using fmt.Sprintf() instead of net/mail.Address.String()\n* oddbitwise: check for odd bit-wise expressions\n* oddcompare: check for odd comparisons\n* oddcompound: check for odd compound += or -= expressions\n* oddifsequence: check for an odd sequence of ifs\n* oddmathbits: check for odd uses of math/bits\n* parseint-downcast: check for places a parsed 64-bit int is downcast to 32-bits\n* returnnil: check for odd nil-returns\n* sprinterr: check for fmt.Sprint(err) instead of err.Error()\n* joinpath: check for using strings.Join() to construct paths\n* readfull: check for extra length check for io.ReadFull()\n* nilerr: returning a nil err instead of a nil value\n* errclosed: check for call strings.Contains() to detect net.ErrClosed\n* hmac-hash: check for bad hash.New passed to hmac.New()\n* readeof: check for ignoring io.EOF as a successful read\n* writestring: check for using io.WriteString(w, string(b))\n* wronglock: find incorrect lock/unlock pairs for rwmutex\n* contexttodo: find context.TODO() usage and suggest to change it\n* close-sql-query-rows: find places database/sql.Rows instance isn't Close()d\n* unixnano: check for time.Time comparisons using UnixNano()\n* timeafter: leaky use of time.After()\n* contextCancelable: checks for cancelable contexts not systematically canceled\n\nRuleguard checks are in ruleguard.rules.go.\n* unconvert: check for unnecessary conversions\n* timeeq: check for using == and != with time.Time values\n* errnoterror: check for variables called `err` which are not the error type\n* ifbodythenbody: check for if statements with identical if and else bodies\n* subtractnoteq: check for x-y==0 instead of x==y\n* selfassign: check for variable self-assignments\n* oddnestedif: check for odd patterns of nested-ifs.\n* oddbitwise: check for odd bitwise expressions\n* ifreturn: check for off if/return sequences\n* oddifsequence: check for if sequences\n* nestedifsequence: check for odd nested if sequences\n* identicalassignments:  check for `x = y ; y = x` pairs.\n* oddcompoundop: check for odd compound operations\n* constswitch: check for switch statements with expressions\n* oddcomparisons: check for odd comparisons\n* oddmathbits: check for odd uses of math/bits\n* floateq: check for exact comparisons of floating point values\n* badexponent: check for `2^x` and `10^x` , which look like exponentiation\n* floatloop: check for using floats as loop counters\n* urlredacted: check for logging urls without calling url.Redacted()\n* sprinterr: check for calling fmt.Sprint(err) instead of err.Error()\n* largeloopcopy: check for large value copies in loops\n* joinpath: check for using strings.Join() to construct paths\n* readfull: check for extra length check for io.ReadFull()\n* nilerr: returning an nil error instead of a nil value\n* errnetclosed: check for call strings.Contains() to detect net.ErrClosed\n* hmac-hash: check for bad hash.New passed to hmac.New()\n* readeof: check for ignoring io.EOF as a successful read\n* writestring: check for using io.WriteString(w, string(b)) when b is []byte\n* badlock: find incorrect lock/unlock pairs for rwmutex\n* contexttodo: find context.TODO() usage and suggest to change it\n_\n\n*Find this useful? [Buy me a coffee!](https://www.buymeacoffee.com/dgryski)*\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/badexponentiation.yml",
    "content": "rules:\n  - id: bad-exponentiation\n    patterns:\n        - pattern-either:\n              - pattern: 10 ^ $X\n              - pattern: 2 ^ $X\n    message: \"Caret (^) is not exponentiation\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/badnilguard.yml",
    "content": "rules:\n  - id: bad-nil-guard\n    patterns:\n        - pattern-either:\n              - pattern: $X == nil && <... $X.$F ...>\n              - pattern: $X != nil || <... $X.$F ...>\n              - pattern: <... $X.$F ...> && $X != nil\n              - pattern: <... $X.$F ...> || $X == nil\n              - pattern: <... $X.$F ...> && $X == nil\n              - pattern: <... $X.$F ...> || $X != nil\n    message: \"Bad nil guard\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/close-sql-query-rows.yml",
    "content": "rules:\n  - id: rows-not-closed\n    patterns:\n        - pattern: $Q.QueryxContext(...)\n        - pattern-not-inside: return $X.QueryxContext(...)\n\n        # direct close\n        - pattern-not-inside: |\n            $ROWS, $ERR = $Q.QueryxContext(...)\n            ...\n            $ROWS.Close()\n        # defer close\n        - pattern-not-inside: |\n            $ROWS, $ERR = $Q.QueryxContext(...)\n            ...\n            defer $ROWS.Close()\n        # passing rows to another function that is expected to close it\n        - pattern-not-inside: |\n            $ROWS, $ERR = $Q.QueryxContext(...)\n            ...\n            $FUNC($ROWS)\n        # close outside if\n        - pattern-not-inside: |\n            if ... {\n              ...\n              $ROWS, $ERR = $Q.QueryxContext(...)\n              ...\n            } else { ... }\n            $ROWS.Close()\n        - pattern-not-inside: |\n            if ... {\n              ...\n              $ROWS, $ERR = $Q.QueryxContext(...)\n              ...\n            } else { ... }\n            ...\n            defer $ROWS.Close()\n        # close outside else\n        - pattern-not-inside: |\n            if ... { ...  } else {\n              ...\n              $ROWS, $ERR = $Q.QueryxContext(...)\n              ...\n            }\n            ...\n            $ROWS.Close()\n        - pattern-not-inside: |\n            if ... { ...  } else {\n              ...\n              $ROWS, $ERR = $Q.QueryxContext(...)\n              ...\n            }\n            ...\n            defer $ROWS.Close()\n    message: \"QueryxContext rows must be closed (or use ExecContext)\"\n    languages: [go]\n    severity: ERROR"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/contextCancelable.yml",
    "content": "rules:\n  - id: cancelable-context-not-systematically-cancelled\n    patterns:\n      - pattern: $_, $X := context.$CANCELABLE(...)\n      - pattern-not-inside: |\n          $_, $X := context.$CANCELABLE(...)\n          ...\n          defer $X()\n      - pattern-not-inside: |\n          $_, $X := context.$CANCELABLE(...)\n          ...\n          $X()\n      - pattern-not-inside: |\n          $C = quicktest.New(...)\n          ...\n          $_, $X := context.$CANCELABLE(...)\n          ...\n          $C.Cleanup($X)\n      - metavariable-regex:\n          metavariable: '$CANCELABLE'\n          regex: '(WithDeadline|WithTimeout)'\n    message: it is good practice to call context cancellation function, $X(), in any case\n    languages: [go]\n    severity: WARNING\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/contextTODO.yml",
    "content": "rules:\n  - id: context-todo\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      context.TODO()\n    message: \"Consider to use well-defined context\"\n    languages: [go]\n    severity: WARNING\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ctx-time.yml",
    "content": "rules:\n- id: ctx-done-and-timers\n  patterns:\n    - pattern-either: \n      - pattern: |\n          select {\n          case <-ctx.Done():\n              $BODY\n          case <-time.After(...):\n              $BODY\n          }\n      - pattern: |\n          $T := time.NewTicker(...)\n          ...\n          select {\n          case <-ctx.Done():\n              $BODY\n          case <-$T.C:\n              $BODY\n          }\n  message: \"ctx.Done() and time.After/time.NewTicker\"\n  languages: [go]\n  severity: ERROR\n\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/errclosed.yml",
    "content": "rules:\n  - id: use-net-errclosed\n    patterns:\n            - pattern: strings.Contains($ERR.Error(), $X)\n            - metavariable-regex:\n                      metavariable: $X\n                      regex: '\".*closed network connection.*\"'\n    message: \"Use errors.Is($ERR, net.ErrClosed) instead\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/errnilcheck.yml",
    "content": "\nrules:\n  - id: err-nil-check\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      if err != nil {\n                              return err\n                      }\n                      return nil\n              - pattern: |\n                      if err != nil {\n                              return $X, err\n                      }\n                      return $X, nil\n        - pattern-not-inside: |\n            if err != nil {\n                    return err\n            } else if ... {\n                    return ...\n            }       \n            return nil\n        - pattern-not-inside: |\n            if err != nil {\n                    return $X, err\n            } else if ... {\n                    return ...\n            }       \n            return $X, nil\n    message: \"superfluous nil err check before return\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/errtodo.yml",
    "content": "rules:\n  - id: err-todo\n    patterns:\n        - pattern-either:\n                - patterns:\n                        - pattern-inside:\n                              if err != nil {\n                                      ...\n                              }\n                        - pattern-regex: // ?(TODO|FIXME).*\n\n        - pattern-either:\n                - patterns:\n                        - pattern-inside: |\n                                      if ... ; err != nil {\n                                              ...\n                                      }\n                        - pattern-regex: // ?(TODO|FIXME).*\n\n    message: \"TODO in error handling code\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/gofuzz.yml",
    "content": "rules:\n  - id: go-fuzz-to-native-fuzzing\n    patterns:\n            - pattern: func Fuzz($DATA []byte) int { $...BODY }\n    fix: |\n      // remove gofuzz build tag\n      // rename file to _test.go\n      // convert corpus with file2fuzz\n      func FuzzData(f *testing.F) {\n        f.Fuzz(func(t *testing.T, $DATA []byte) {\n          func() int {\n            $...BODY\n          }()\n        })\n      }\n    message: \"old-style go-fuzz fuzz function found\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/hashsum.yml",
    "content": "rules:\n  - id: hash-sum-without-write\n    patterns:\n                - pattern-either:\n                        - pattern: |\n                                $HASH.New().Sum($SLICE)\n                        - pattern: |\n                                $H := $HASH.New()\n                                ...\n                                $H.Sum($SLICE)\n                - pattern-not: |\n                        $H := $HASH.New()\n                        ...\n                        $H.Write(...)\n                        ...\n                        $H.Sum($SLICE)\n                - pattern-not: |\n                        $H := $HASH.New()\n                        ...\n                        $FUNC(..., $H, ...)\n                        ...\n                        $H.Sum($SLICE)\n    message: \"odd hash.Sum call flow\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/hmac-bytes.yml",
    "content": "rules:\n  - id: use-hmac-equal\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      $MAC = hmac.New(...)\n                      ...\n                      $H = $MAC.Sum(...)\n                      ...\n                      bytes.Equal($H, ...)\n              - pattern: |\n                      $MAC = hmac.New(...)\n                      ...\n                      $H = $MAC.Sum(...)\n                      ...\n                      bytes.Equal(..., $H)\n    message: \"Comparing a MAC with bytes.Equal()\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/hmac-hash.yml",
    "content": "rules:\n  - id: hmac-needs-new\n    patterns:\n        - pattern-either:\n                - pattern: | \n                        $H := $HASH.New()\n                        ...\n                        $FUNC := func() hash.Hash { return $H }\n                        ...\n                        hmac.New($FUNC, ...)\n                - pattern: | \n                        $H := $HASH.New()\n                        ...\n                        hmac.New(func() hash.Hash { return $H }, ...)\n\n                - pattern: |\n                        hmac.New(func() hash.Hash { return ( $H : hash.Hash) }, ...)\n\n    message: \"calling hmac.New with unchanging hash.New\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/hostport.yml",
    "content": "# https://github.com/golang/go/issues/28308, from @stapelberg\nrules:\n- id: sprintf-host-port\n  pattern-either:\n    - patterns:\n        - pattern-either:\n          - pattern: fmt.Sprintf(\"%s:%s\", $NET, $XX)\n          - pattern: fmt.Sprintf(\"%s:%d\", $NET, $XX)\n          - pattern: fmt.Sprintf(\"%s:%s\", $XX, $NET)\n          - pattern: fmt.Sprintf(\"%s:%d\", $XX, $NET)\n          - pattern: $NET = fmt.Sprintf(\"%s:%d\", ..., ...)\n          - pattern: $NET = fmt.Sprintf(\"%s:%s\", ..., ...)\n        - metavariable-regex:\n            metavariable: '$NET'\n            regex: '((?i).*(port|addr|host|listen|bind))|((?i)^ip$)|(ip[A-Z0-9].*|.*(Ip)$|.*(Ip)[A-Z0-9].*)'\n    - patterns:\n      - pattern: fmt.Sprintf($XX, $NET)\n      - metavariable-regex:\n          metavariable: '$XX'\n          regex: '\"%s:[0-9]+\"'\n      - metavariable-regex:\n          metavariable: '$NET'\n          regex: '((?i).*(port|addr|host|listen|bind))|((?i)^ip$)|(ip[A-Z0-9].*|.*(Ip)$|.*(Ip)[A-Z0-9].*)'\n  message: |\n    use net.JoinHostPort instead of fmt.Sprintf($XX, $NET)\n  languages: [go]\n  severity: ERROR\n\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/http-ctx-goroutine.yml",
    "content": "\nrules:\n  - id: http-request-go-context\n    patterns:\n    - pattern-either: \n      - pattern: |\n          $CTX := ($R : *http.Request).Context()\n          ...\n          go $F($CTX, ...)\n      - pattern: |\n          go $F(($R : *http.Request).Context(), ...)\n    message: \"passing an http-request scoped Context to a goroutine\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-discard.yml",
    "content": "rules:\n- id: deprecated-ioutil-discard\n  pattern: ioutil.Discard\n  fix: io.Discard\n  message: ioutil.Discard is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-nop-closer.yml",
    "content": "rules:\n- id: deprecated-ioutil-nopcloser\n  pattern: ioutil.NopCloser($R)\n  fix:  io.NopCloser($R)\n  message: ioutil.NopCloser is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-readall.yml",
    "content": "rules:\n- id: deprecated-ioutil-readall\n  pattern: ioutil.ReadAll($R)\n  fix: io.ReadAll($R)\n  message: ioutil.ReadAll is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-readdir.yml",
    "content": "\nrules:\n- id: deprecated-ioutil-readdir\n  pattern: ioutil.ReadDir($D)\n  message: ioutil.ReadDir is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-readfile.yml",
    "content": "rules:\n- id: deprecated-ioutil-readfile\n  pattern: ioutil.ReadFile($F)\n  fix: os.ReadFile($F)\n  message: ioutil.ReadFile is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-tmpdir.yml",
    "content": "rules:\n- id: deprecated-ioutil-tempdir\n  pattern: ioutil.TempDir($D, $P)\n  fix: os.MkdirTemp($D, $P)\n  message: ioutil.TempDir is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-tmpfile.yml",
    "content": "rules:\n- id: deprecated-ioutil-tempfile\n  pattern: ioutil.TempFile($D, $P)\n  fix: os.CreateTemp($D, $P)\n  message: ioutil.TempFile is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ioutil-writefile.yml",
    "content": "rules:\n- id: deprecated-ioutil-writefile\n  pattern: ioutil.WriteFile($F, $D, $P)\n  fix: os.WriteFile($F, $D, $P)\n  message: ioutil.WriteFile is deprecated\n  languages: [go]\n  severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/joinpath.yml",
    "content": "rules:\n  - id: use-strings-join-path\n    patterns:\n           - pattern-either:\n                        - pattern: strings.Join(..., \"/\")\n                        - pattern: strings.Join(..., \"\\\\\")\n                        - pattern: strings.Join(..., `\\`)\n                        - pattern: strings.Join(..., os.PathSeparator)\n    message: \"did you want path.Join() or filepath.Join()?\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/json-writer.yml",
    "content": "rules:\n  - id: json-encoder-needs-type\n    patterns:\n                - pattern: |\n                        $ENC := json.NewEncoder(($W: http.ResponseWriter))\n                        ...\n                        $ENC.Encode(...)\n                - pattern-not: |\n                        $ENC := json.NewEncoder(($W: http.ResponseWriter))\n                        ...\n                        $W.Header().Set(\"=~/Content-Type/i\", \"=~/application/json/\")\n                        ...\n                        $ENC.Encode(...)\n                - pattern-not-inside: |\n                        $W.Header().Set(\"=~/Content-Type/i\", \"=~/application/json/\")\n                        ...\n                        $ENC := json.NewEncoder($W)\n                        ...\n                        $ENC.Encode(...)\n                - pattern-not: |\n                        $ENC := json.NewEncoder(($W: http.ResponseWriter))\n                        ...\n                        $W.Header().Add(\"=~/Content-Type/i\", \"=~/application/json/\")\n                        ...\n                        $ENC.Encode(...)\n                - pattern-not-inside: |\n                        $W.Header().Add(\"=~/Content-Type/i\", \"=~/application/json/\")\n                        ...\n                        $ENC := json.NewEncoder($W)\n                        ...\n                        $ENC.Encode(...)\n\n    message: \"calling json.Encode() on an http.ResponseWriter will set Content-Type text/plain\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/mail-address.yml",
    "content": "rules:\n  - id: sprintf-mail-address\n    pattern-either:\n        - pattern: fmt.Sprintf(`\"%s\" <%s>`, $NAME, $EMAIL)\n        - pattern: fmt.Sprintf(`\"%s\"<%s>`, $NAME, $EMAIL)\n        - pattern: fmt.Sprintf(\"\\\"%s\\\"<%s>\", $NAME, $EMAIL)\n        - pattern: fmt.Sprintf(\"\\\"%s\\\" <%s>\", $NAME, $EMAIL)\n        - pattern: fmt.Sprintf(\"%s<%s>\", $NAME, $EMAIL)\n    message: \"use net/mail Address.String() instead of fmt.Sprintf()\"\n    fix: \"(&mail.Address{Name:$NAME, Address:$EMAIL}).String()\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/marshaljson.yml",
    "content": "rules:\n  - id: marshal-json-misspell\n    pattern-either:\n      - patterns:\n          - pattern-regex: (?i)func \\((.+)\\) marshal[l]?json\\((.*)\\)\n          - pattern-not-regex: func \\(.+\\) MarshalJSON\\(\n    fix: func ($1) MarshalJSON($2)\n    message: |\n      Misspelling of MarshalJSON.\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/marshalyaml.yml",
    "content": "rules:\n  - id: marshal-yaml-misspell\n    pattern-either:\n      - patterns:\n          - pattern-regex: (?i)func \\((.+)\\) marshal[l]?yaml\\((.*)\\)\n          - pattern-not-regex: func \\(.+\\) MarshalYAML\\(\n    fix: func ($1) MarshalYAML($2)\n    message: |\n      Misspelling of MarshalYAML.\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/mathbits.yml",
    "content": "rules:\n  - id: use-math-bits\n    patterns:\n        - pattern-either:\n              - pattern: $X >> $N | $X << (8 - $N)\n              - pattern: $X << $N | $X >> (8 - $N)\n              - pattern: $X >> (8 - $N) | $X << $N\n              - pattern: $X << (8 - $N) | $X >> $N\n              - pattern: $X >> $N | $X << (16 - $N)\n              - pattern: $X << $N | $X >> (16 - $N)\n              - pattern: $X >> (16 - $N) | $X << $N\n              - pattern: $X << (16 - $N) | $X >> $N\n              - pattern: $X >> $N | $X << (32 - $N)\n              - pattern: $X << $N | $X >> (32 - $N)\n              - pattern: $X >> (32 - $N) | $X << $N\n              - pattern: $X << (32 - $N) | $X >> $N\n              - pattern: $X >> $N | $X << (64 - $N)\n              - pattern: $X << $N | $X >> (64 - $N)\n              - pattern: $X >> (64 - $N) | $X << $N\n              - pattern: $X << (64 - $N) | $X >> $N\n    message: \"Try using math/bits instead\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/nilerr.yml",
    "content": "rules:\n  - id: return-nil-err\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      if err == nil {\n                              return err\n                      }\n              - pattern: |\n                      if err == nil {\n                              return ..., err\n                      }\n    message: \"return nil err instead of nil value\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/nrtxn.yml",
    "content": "rules:\n  - id: newrelic-start-without-end\n    patterns:\n                - pattern-not-inside: |\n                        $TXN := $NR.StartTransaction($N)\n                        ...\n                        defer $TXN.End()\n                - pattern-not-inside: |\n                            $TXN := $NR.StartTransaction($N)\n                            ...\n                            $TXN.End()\n                - pattern-either:\n                   - pattern: $TXN := $NR.StartTransaction($N)\n    message: \"missing new relic end transaction\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddbitwise.yml",
    "content": "rules:\n  - id: odd-bitwise\n    patterns:\n        - pattern-either:\n              - pattern: $X | $X\n              - pattern: $X | ^$X\n              - pattern: ^$X | $X\n              - pattern: $X & $X\n              - pattern: $X & ^$X\n              - pattern: ^$X & $X\n              - pattern: $X &^ $X\n    message: \"Odd bitwise expression\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-subtract-eq-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-subtract-eq-zero\n    patterns:\n      - pattern: $X - $Y == 0\n    fix: $X == $Y\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-subtract-gt-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-subtract-gt-zero\n    patterns:\n      - pattern: $X - $Y > 0\n    fix: $X > $Y\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-subtract-gte-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-subtract-gte-zero\n    patterns:\n      - pattern: $X - $Y >= 0\n    fix: $X >= $Y\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-subtract-lt-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-subtract-lt-zero\n    patterns:\n      - pattern: $X - $Y < 0\n    fix: $Y > $X\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-subtract-lte-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-subtract-lte-zero\n    patterns:\n      - pattern: $X - $Y <= 0\n    fix: $Y >= $X\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-subtract-neq-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-subtract-neq-zero\n    patterns:\n      - pattern: $X - $Y != 0\n    fix: $X != $Y\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-xor-eq-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-xor-eq-zero\n    patterns:\n      - pattern: $X ^ $Y == 0\n    fix: $X == $Y\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompare-xor-neq-zero.yml",
    "content": "# flag these odd comparisons as they all have simpler\n# equivalents with just $X and $Y and no zero term\nrules:\n  - id: odd-comparison-xor-neq-zero\n    patterns:\n      - pattern: $X ^ $Y != 0\n    fix: $X != $Y\n    message: \"Odd comparison\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddcompound.yml",
    "content": "rules:\n  - id: odd-compound-expression\n    patterns:\n        - pattern-either:\n              - pattern: $X += $X + $Y\n              - pattern: $X += $X - $Y\n              - pattern: $X -= $X + $Y\n              - pattern: $X -= $X - $Y\n    message: \"Odd compound += or -= expression\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddifsequence.yml",
    "content": "rules:\n  - id: odd-sequence-ifs\n    patterns:\n        - pattern-either:\n          - pattern: |\n                  if $X { return ... }\n                  if $X { ... }\n          - pattern: |\n                  if ! $X { return ... }\n                  if $X { ... }\n          - pattern: |\n                  if $X { return ... }\n                  if ! $X { ... }\n          - pattern: |\n                  if $X == $Y { return ... }\n                  if $X != $Y { ... }\n          - pattern: |\n                  if $X != $Y { return ... }\n                  if $X == $Y { ... }\n          - pattern: |\n                  if $X { return  ... }\n                  for $X { ... }\n\n          - pattern: |\n                  if $X { break }\n                  if $X { ... }\n          - pattern: |\n                  if ! $X { break }\n                  if $X { ... }\n          - pattern: |\n                  if $X { break }\n                  if ! $X { ... }\n          - pattern: |\n                  if $X == $Y { break }\n                  if $X != $Y { ... }\n          - pattern: |\n                  if $X != $Y { break }\n                  if $X == $Y { ... }\n          - pattern: |\n                  if $X { break }\n                  for $X { ... }\n\n          - pattern: |\n                  if $X { continue }\n                  if $X { ... }\n          - pattern: |\n                  if ! $X { continue }\n                  if $X { ... }\n          - pattern: |\n                  if $X { continue }\n                  if ! $X { ... }\n          - pattern: |\n                  if $X == $Y { continue }\n                  if $X != $Y { ... }\n          - pattern: |\n                  if $X != $Y { continue }\n                  if $X == $Y { ... }\n          - pattern: |\n                  if $X { continue }\n                  for $X { ... }\n\n          - pattern: |\n                  if $X {\n                          if $X { ... }\n                          ...\n                  }\n          - pattern: |\n                  if $X {\n                          if ! $X { ... }\n                          ...\n                  }\n          - pattern: |\n                  if ! $X {\n                          if $X { ... }\n                        ...\n                  }\n          - pattern: |\n                  if $X == $Y {\n                          if $X != $Y { ... }\n                          ...\n                  }\n          - pattern: |\n                  if $X != $Y {\n                          if $X == $Y { ... }\n                          ...\n                  }\n          - pattern: |\n                  if $X {\n                          for ! $X { ... }\n                          ...\n                  }\n          - pattern: |\n                  if ! $X {\n                          for $X { ... }\n                        ...\n                  }\n          - pattern: |\n                  if $X == $Y {\n                          for $X != $Y { ... }\n                          ...\n                  }\n          - pattern: |\n                  if $X != $Y {\n                          for $X == $Y { ... }\n                          ...\n                  }\n          - pattern: |\n                  for $X {\n                          if $X { ... }\n                          ...\n                  }\n          - pattern: |\n                  for $X {\n                          if ! $X { ... }\n                          ...\n                  }\n          - pattern: |\n                  for ! $X {\n                          if $X { ... }\n                          ...\n                  }\n          - pattern: |\n                  for $X == $Y {\n                          if $X != $Y { ... }\n                          ...\n                  }\n          - pattern: |\n                  for $X != $Y {\n                          if $X == $Y { ... }\n                          ...\n                  }\n    message: \"Odd sequence of ifs\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/oddmathbits.yml",
    "content": "rules:\n  - id: odd-bits-leadingzeros\n    patterns:\n        - pattern-either:\n              - pattern: 64 - bits.LeadingZeros64($X)\n              - pattern: 32 - bits.LeadingZeros32($X)\n              - pattern: 16 - bits.LeadingZeros16($X)\n              - pattern: 8 - bits.LeadingZeros8($X)\n    message: \"Odd bits.LeadingZeros() expression should perhaps be bits.Len()\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/os-error-is-exist.yml",
    "content": "rules:\n  - id: os-error-is-exist\n    patterns:\n     - pattern: os.IsExist($ERR)\n    fix: errors.Is($ERR, fs.ErrExist)\n    message: \"New code should use errors.Is with the appropriate error type\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/os-error-is-not-exist.yml",
    "content": "rules:\n  - id: os-error-is-not-exist\n    patterns:\n     - pattern: os.IsNotExist($ERR)\n    fix: errors.Is($ERR, fs.ErrNotExist)\n    message: \"New code should use errors.Is with the appropriate error type\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/os-error-is-permission.yml",
    "content": "rules:\n  - id: os-error-is-permission\n    patterns:\n     - pattern: os.IsPermission($ERR)\n    fix: errors.Is($ERR, fs.ErrPermission)\n    message: \"New code should use errors.Is with the appropriate error type\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/os-error-is-timeout.yml",
    "content": "rules:\n  - id: os-error-is-timeout\n    patterns:\n     - pattern: os.IsTimeout(...)\n    message: \"New code should use errors.Is with the appropriate error type\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/parseint-downcast.yml",
    "content": "rules:\n  - id: parseint-downcast\n    patterns:\n      - pattern-either:\n              - pattern: |\n                              $X, ... = strconv.ParseInt(..., ..., 64)\n                              ...\n                              int32($X)\n              - pattern: |\n                              $X, ... = strconv.ParseInt(..., ..., 64)\n                              ...\n                              uint32($X)\n              - pattern: |\n                              $X, ... = strconv.ParseUint(..., ..., 64)\n                              ...\n                              int32($X)\n              - pattern: |\n                              $X, ... = strconv.ParseUint(..., ..., 64)\n                              ...\n                              uint32($X)\n              - pattern: |\n                              $X = strconv.Atoi(...)\n                              ...\n                              int32($X)\n              - pattern: |\n                              $X = strconv.Atoi(...)\n                              ...\n                              uint32($X)\n    message: \"64-bit integer parsed and downcast to u/int32\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/readeof.yml",
    "content": "rules:\n  - id: read-io-eof\n    patterns:\n                    - pattern: |\n                        $N, $ERR := $R.Read(($SLICE : []byte))\n                        if $ERR != nil {\n                            return ...\n                        }\n                    - pattern-not: |\n                        $N, $ERR := rand.Read(($SLICE : []byte))\n                        if $ERR != nil {\n                            return ...\n                        }\n    message: \"Read() can return n bytes and io.EOF\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/readfull.yml",
    "content": "rules:\n  - id: io-readfull-n\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      $N, $ERR = io.ReadFull($R, $SLICE)\n                      if $ERR != nil || $N != len($SLICE) {\n                              ...\n                      }\n              - pattern: |\n                      $N, $ERR = io.ReadFull($R, $SLICE)\n                      if $N != len($SLICE) || $ERR != nil {\n                              ...\n                      }\n    message: \"io.ReadFull() returns err == nil iff n == len(slice)\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/returnnil.yml",
    "content": "rules:\n  - id: return-nil\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      if $X == nil {\n                              return $X\n                      }\n              - pattern: |\n                      if $X != nil {\n                              return ...\n                      }\n                      return $X\n    message: \"return nil instead of nil value\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/ruleguard.rules.go",
    "content": "//go:build ignore\n// +build ignore\n\npackage gorules\n\nimport \"github.com/quasilyte/go-ruleguard/dsl\"\n\n// This is a collection of rules for ruleguard: https://github.com/quasilyte/go-ruleguard\n\n// Remove extra conversions: mdempsky/unconvert\nfunc unconvert(m dsl.Matcher) {\n\tm.Match(\"int($x)\").Where(m[\"x\"].Type.Is(\"int\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\n\tm.Match(\"float32($x)\").Where(m[\"x\"].Type.Is(\"float32\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"float64($x)\").Where(m[\"x\"].Type.Is(\"float64\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\n\t// m.Match(\"byte($x)\").Where(m[\"x\"].Type.Is(\"byte\")).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\t// m.Match(\"rune($x)\").Where(m[\"x\"].Type.Is(\"rune\")).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"bool($x)\").Where(m[\"x\"].Type.Is(\"bool\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\n\tm.Match(\"int8($x)\").Where(m[\"x\"].Type.Is(\"int8\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"int16($x)\").Where(m[\"x\"].Type.Is(\"int16\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"int32($x)\").Where(m[\"x\"].Type.Is(\"int32\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"int64($x)\").Where(m[\"x\"].Type.Is(\"int64\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\n\tm.Match(\"uint8($x)\").Where(m[\"x\"].Type.Is(\"uint8\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"uint16($x)\").Where(m[\"x\"].Type.Is(\"uint16\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"uint32($x)\").Where(m[\"x\"].Type.Is(\"uint32\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\tm.Match(\"uint64($x)\").Where(m[\"x\"].Type.Is(\"uint64\") && !m[\"x\"].Const).Report(\"unnecessary conversion\").Suggest(\"$x\")\n\n\tm.Match(\"time.Duration($x)\").Where(m[\"x\"].Type.Is(\"time.Duration\") &&\n\t\t!m[\"x\"].Node.Is(\"BasicLit\") &&\n\t\t!m[\"x\"].Text.Matches(\"^[0-9_]*$\")).Report(\"unnecessary conversion\").Suggest(\"$x\")\n}\n\n// Don't use == or != with time.Time\n// https://github.com/dominikh/go-tools/issues/47 : Wontfix\nfunc timeeq(m dsl.Matcher) {\n\tm.Match(\"$t0 == $t1\").Where(m[\"t0\"].Type.Is(\"time.Time\")).Report(\"using == with time.Time\")\n\tm.Match(\"$t0 != $t1\").Where(m[\"t0\"].Type.Is(\"time.Time\")).Report(\"using != with time.Time\")\n\tm.Match(`map[$k]$v`).Where(m[\"k\"].Type.Is(\"time.Time\")).Report(\"map with time.Time keys are easy to misuse\")\n}\n\n// err but no an error\nfunc errnoterror(m dsl.Matcher) {\n\n\t// Would be easier to check for all err identifiers instead, but then how do we get the type from m[] ?\n\n\tm.Match(\n\t\t\"if $*_, err := $x; $err != nil { $*_ } else if $_ { $*_ }\",\n\t\t\"if $*_, err := $x; $err != nil { $*_ } else { $*_ }\",\n\t\t\"if $*_, err := $x; $err != nil { $*_ }\",\n\n\t\t\"if $*_, err = $x; $err != nil { $*_ } else if $_ { $*_ }\",\n\t\t\"if $*_, err = $x; $err != nil { $*_ } else { $*_ }\",\n\t\t\"if $*_, err = $x; $err != nil { $*_ }\",\n\n\t\t\"$*_, err := $x; if $err != nil { $*_ } else if $_ { $*_ }\",\n\t\t\"$*_, err := $x; if $err != nil { $*_ } else { $*_ }\",\n\t\t\"$*_, err := $x; if $err != nil { $*_ }\",\n\n\t\t\"$*_, err = $x; if $err != nil { $*_ } else if $_ { $*_ }\",\n\t\t\"$*_, err = $x; if $err != nil { $*_ } else { $*_ }\",\n\t\t\"$*_, err = $x; if $err != nil { $*_ }\",\n\t).\n\t\tWhere(m[\"err\"].Text == \"err\" && !m[\"err\"].Type.Is(\"error\") && m[\"x\"].Text != \"recover()\").\n\t\tReport(\"err variable not error type\")\n}\n\n// Identical if and else bodies\nfunc ifbodythenbody(m dsl.Matcher) {\n\tm.Match(\"if $*_ { $body } else { $body }\").\n\t\tReport(\"identical if and else bodies\")\n\n\t// Lots of false positives.\n\t// m.Match(\"if $*_ { $body } else if $*_ { $body }\").\n\t//\tReport(\"identical if and else bodies\")\n}\n\n// Odd inequality: A - B < 0 instead of !=\n// Too many false positives.\n/*\nfunc subtractnoteq(m dsl.Matcher) {\n\tm.Match(\"$a - $b < 0\").Report(\"consider $a != $b\")\n\tm.Match(\"$a - $b > 0\").Report(\"consider $a != $b\")\n\tm.Match(\"0 < $a - $b\").Report(\"consider $a != $b\")\n\tm.Match(\"0 > $a - $b\").Report(\"consider $a != $b\")\n}\n*/\n\n// Self-assignment\nfunc selfassign(m dsl.Matcher) {\n\tm.Match(\"$x = $x\").Report(\"useless self-assignment\")\n}\n\n// Odd nested ifs\nfunc oddnestedif(m dsl.Matcher) {\n\tm.Match(\"if $x { if $x { $*_ }; $*_ }\",\n\t\t\"if $x == $y { if $x != $y {$*_ }; $*_ }\",\n\t\t\"if $x != $y { if $x == $y {$*_ }; $*_ }\",\n\t\t\"if $x { if !$x { $*_ }; $*_ }\",\n\t\t\"if !$x { if $x { $*_ }; $*_ }\").\n\t\tReport(\"odd nested ifs\")\n\n\tm.Match(\"for $x { if $x { $*_ }; $*_ }\",\n\t\t\"for $x == $y { if $x != $y {$*_ }; $*_ }\",\n\t\t\"for $x != $y { if $x == $y {$*_ }; $*_ }\",\n\t\t\"for $x { if !$x { $*_ }; $*_ }\",\n\t\t\"for !$x { if $x { $*_ }; $*_ }\").\n\t\tReport(\"odd nested for/ifs\")\n}\n\n// odd bitwise expressions\nfunc oddbitwise(m dsl.Matcher) {\n\tm.Match(\"$x | $x\",\n\t\t\"$x | ^$x\",\n\t\t\"^$x | $x\").\n\t\tReport(\"odd bitwise OR\")\n\n\tm.Match(\"$x & $x\",\n\t\t\"$x & ^$x\",\n\t\t\"^$x & $x\").\n\t\tReport(\"odd bitwise AND\")\n\n\tm.Match(\"$x &^ $x\").\n\t\tReport(\"odd bitwise AND-NOT\")\n}\n\n// odd sequence of if tests with return\nfunc ifreturn(m dsl.Matcher) {\n\tm.Match(\"if $x { return $*_ }; if $x {$*_ }\").Report(\"odd sequence of if test\")\n\tm.Match(\"if $x { return $*_ }; if !$x {$*_ }\").Report(\"odd sequence of if test\")\n\tm.Match(\"if !$x { return $*_ }; if $x {$*_ }\").Report(\"odd sequence of if test\")\n\tm.Match(\"if $x == $y { return $*_ }; if $x != $y {$*_ }\").Report(\"odd sequence of if test\")\n\tm.Match(\"if $x != $y { return $*_ }; if $x == $y {$*_ }\").Report(\"odd sequence of if test\")\n\n}\n\nfunc oddifsequence(m dsl.Matcher) {\n\t/*\n\t\tm.Match(\"if $x { $*_ }; if $x {$*_ }\").Report(\"odd sequence of if test\")\n\n\t\tm.Match(\"if $x == $y { $*_ }; if $y == $x {$*_ }\").Report(\"odd sequence of if tests\")\n\t\tm.Match(\"if $x != $y { $*_ }; if $y != $x {$*_ }\").Report(\"odd sequence of if tests\")\n\n\t\tm.Match(\"if $x < $y { $*_ }; if $y > $x {$*_ }\").Report(\"odd sequence of if tests\")\n\t\tm.Match(\"if $x <= $y { $*_ }; if $y >= $x {$*_ }\").Report(\"odd sequence of if tests\")\n\n\t\tm.Match(\"if $x > $y { $*_ }; if $y < $x {$*_ }\").Report(\"odd sequence of if tests\")\n\t\tm.Match(\"if $x >= $y { $*_ }; if $y <= $x {$*_ }\").Report(\"odd sequence of if tests\")\n\t*/\n}\n\n// odd sequence of nested if tests\nfunc nestedifsequence(m dsl.Matcher) {\n\t/*\n\t\tm.Match(\"if $x < $y { if $x >= $y {$*_ }; $*_ }\").Report(\"odd sequence of nested if tests\")\n\t\tm.Match(\"if $x <= $y { if $x > $y {$*_ }; $*_ }\").Report(\"odd sequence of nested if tests\")\n\t\tm.Match(\"if $x > $y { if $x <= $y {$*_ }; $*_ }\").Report(\"odd sequence of nested if tests\")\n\t\tm.Match(\"if $x >= $y { if $x < $y {$*_ }; $*_ }\").Report(\"odd sequence of nested if tests\")\n\t*/\n}\n\n// odd sequence of assignments\nfunc identicalassignments(m dsl.Matcher) {\n\tm.Match(\"$x  = $y; $y = $x\").Report(\"odd sequence of assignments\")\n}\n\nfunc oddcompoundop(m dsl.Matcher) {\n\tm.Match(\"$x += $x + $_\",\n\t\t\"$x += $x - $_\").\n\t\tReport(\"odd += expression\")\n\n\tm.Match(\"$x -= $x + $_\",\n\t\t\"$x -= $x - $_\").\n\t\tReport(\"odd -= expression\")\n}\n\nfunc constswitch(m dsl.Matcher) {\n\tm.Match(\"switch $x { $*_ }\", \"switch $*_; $x { $*_ }\").\n\t\tWhere(m[\"x\"].Const && !m[\"x\"].Text.Matches(`^runtime\\.`)).\n\t\tReport(\"constant switch\")\n}\n\n// oddcomparisons flags comparisons which all have simpler\n// equivalents with just $x and $y and no zero term\nfunc oddcomparisons(m dsl.Matcher) {\n\tm.Match(\"$x - $y == 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$x == $y\")\n\n\tm.Match(\"$x - $y != 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$x != $y\")\n\n\tm.Match(\"$x - $y < 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$y > $x\")\n\n\tm.Match(\"$x - $y <= 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$y >= $x\")\n\n\tm.Match(\"$x - $y > 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$x > $y\")\n\n\tm.Match(\"$x - $y >= 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$x >= $y\")\n\n\tm.Match(\"$x ^ $y == 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$x == $y\")\n\n\tm.Match(\"$x ^ $y != 0\").\n\t\tReport(\"odd comparison\").\n\t\tSuggest(\"$x != $y\")\n}\n\nfunc oddmathbits(m dsl.Matcher) {\n\tm.Match(\n\t\t\"64 - bits.LeadingZeros64($x)\",\n\t\t\"32 - bits.LeadingZeros32($x)\",\n\t\t\"16 - bits.LeadingZeros16($x)\",\n\t\t\"8 - bits.LeadingZeros8($x)\",\n\t).Report(\"odd math/bits expression: use bits.Len*() instead?\")\n}\n\nfunc floateq(m dsl.Matcher) {\n\tm.Match(\n\t\t\"$x == $y\",\n\t\t\"$x != $y\",\n\t).\n\t\tWhere(m[\"x\"].Type.Is(\"float32\") && !m[\"x\"].Const && !m[\"y\"].Text.Matches(\"0(.0+)?\")).\n\t\tReport(\"floating point tested for equality\")\n\n\tm.Match(\n\t\t\"$x == $y\",\n\t\t\"$x != $y\",\n\t).\n\t\tWhere(m[\"x\"].Type.Is(\"float64\") && !m[\"x\"].Const && !m[\"y\"].Text.Matches(\"0(.0+)?\")).\n\t\tReport(\"floating point tested for equality\")\n\n\tm.Match(\"switch $x { $*_ }\", \"switch $*_; $x { $*_ }\").\n\t\tWhere(m[\"x\"].Type.Is(\"float32\")).\n\t\tReport(\"floating point as switch expression\")\n\n\tm.Match(\"switch $x { $*_ }\", \"switch $*_; $x { $*_ }\").\n\t\tWhere(m[\"x\"].Type.Is(\"float64\")).\n\t\tReport(\"floating point as switch expression\")\n\n}\n\nfunc badexponent(m dsl.Matcher) {\n\tm.Match(\n\t\t\"2 ^ $x\",\n\t\t\"10 ^ $x\",\n\t).\n\t\tReport(\"caret (^) is not exponentiation\")\n}\n\nfunc floatloop(m dsl.Matcher) {\n\tm.Match(\n\t\t\"for $i := $x; $i < $y; $i += $z { $*_ }\",\n\t\t\"for $i = $x; $i < $y; $i += $z { $*_ }\",\n\t).\n\t\tWhere(m[\"i\"].Type.Is(\"float64\")).\n\t\tReport(\"floating point for loop counter\")\n\n\tm.Match(\n\t\t\"for $i := $x; $i < $y; $i += $z { $*_ }\",\n\t\t\"for $i = $x; $i < $y; $i += $z { $*_ }\",\n\t).\n\t\tWhere(m[\"i\"].Type.Is(\"float32\")).\n\t\tReport(\"floating point for loop counter\")\n}\n\nfunc urlredacted(m dsl.Matcher) {\n\n\tm.Match(\n\t\t\"log.Println($x, $*_)\",\n\t\t\"log.Println($*_, $x, $*_)\",\n\t\t\"log.Println($*_, $x)\",\n\t\t\"log.Printf($*_, $x, $*_)\",\n\t\t\"log.Printf($*_, $x)\",\n\n\t\t\"log.Println($x, $*_)\",\n\t\t\"log.Println($*_, $x, $*_)\",\n\t\t\"log.Println($*_, $x)\",\n\t\t\"log.Printf($*_, $x, $*_)\",\n\t\t\"log.Printf($*_, $x)\",\n\t).\n\t\tWhere(m[\"x\"].Type.Is(\"*url.URL\")).\n\t\tReport(\"consider $x.Redacted() when outputting URLs\")\n}\n\nfunc sprinterr(m dsl.Matcher) {\n\tm.Match(`fmt.Sprint($err)`,\n\t\t`fmt.Sprintf(\"%s\", $err)`,\n\t\t`fmt.Sprintf(\"%v\", $err)`,\n\t).\n\t\tWhere(m[\"err\"].Type.Is(\"error\")).\n\t\tReport(\"maybe call $err.Error() instead of fmt.Sprint()?\")\n\n}\n\nfunc largeloopcopy(m dsl.Matcher) {\n\tm.Match(\n\t\t`for $_, $v := range $_ { $*_ }`,\n\t).\n\t\tWhere(m[\"v\"].Type.Size > 512).\n\t\tReport(`loop copies large value each iteration`)\n}\n\nfunc joinpath(m dsl.Matcher) {\n\tm.Match(\n\t\t`strings.Join($_, \"/\")`,\n\t\t`strings.Join($_, \"\\\\\")`,\n\t\t\"strings.Join($_, `\\\\`)\",\n\t).\n\t\tReport(`did you mean path.Join() or filepath.Join() ?`)\n}\n\nfunc readfull(m dsl.Matcher) {\n\tm.Match(`$n, $err := io.ReadFull($_, $slice)\n                 if $err != nil || $n != len($slice) {\n                              $*_\n\t\t }`,\n\t\t`$n, $err := io.ReadFull($_, $slice)\n                 if $n != len($slice) || $err != nil {\n                              $*_\n\t\t }`,\n\t\t`$n, $err = io.ReadFull($_, $slice)\n                 if $err != nil || $n != len($slice) {\n                              $*_\n\t\t }`,\n\t\t`$n, $err = io.ReadFull($_, $slice)\n                 if $n != len($slice) || $err != nil {\n                              $*_\n\t\t }`,\n\t\t`if $n, $err := io.ReadFull($_, $slice); $n != len($slice) || $err != nil {\n                              $*_\n\t\t }`,\n\t\t`if $n, $err := io.ReadFull($_, $slice); $err != nil || $n != len($slice) {\n                              $*_\n\t\t }`,\n\t\t`if $n, $err = io.ReadFull($_, $slice); $n != len($slice) || $err != nil {\n                              $*_\n\t\t }`,\n\t\t`if $n, $err = io.ReadFull($_, $slice); $err != nil || $n != len($slice) {\n                              $*_\n\t\t }`,\n\t).Report(\"io.ReadFull() returns err == nil iff n == len(slice)\")\n}\n\nfunc nilerr(m dsl.Matcher) {\n\tm.Match(\n\t\t`if err == nil { return err }`,\n\t\t`if err == nil { return $*_, err }`,\n\t).\n\t\tReport(`return nil error instead of nil value`)\n\n}\n\nfunc mailaddress(m dsl.Matcher) {\n\tm.Match(\n\t\t\"fmt.Sprintf(`\\\"%s\\\" <%s>`, $NAME, $EMAIL)\",\n\t\t\"fmt.Sprintf(`\\\"%s\\\"<%s>`, $NAME, $EMAIL)\",\n\t\t\"fmt.Sprintf(`%s <%s>`, $NAME, $EMAIL)\",\n\t\t\"fmt.Sprintf(`%s<%s>`, $NAME, $EMAIL)\",\n\t\t`fmt.Sprintf(\"\\\"%s\\\"<%s>\", $NAME, $EMAIL)`,\n\t\t`fmt.Sprintf(\"\\\"%s\\\" <%s>\", $NAME, $EMAIL)`,\n\t\t`fmt.Sprintf(\"%s<%s>\", $NAME, $EMAIL)`,\n\t\t`fmt.Sprintf(\"%s <%s>\", $NAME, $EMAIL)`,\n\t).\n\t\tReport(\"use net/mail Address.String() instead of fmt.Sprintf()\").\n\t\tSuggest(\"(&mail.Address{Name:$NAME, Address:$EMAIL}).String()\")\n\n}\n\nfunc errnetclosed(m dsl.Matcher) {\n\tm.Match(\n\t\t`strings.Contains($err.Error(), $text)`,\n\t).\n\t\tWhere(m[\"text\"].Text.Matches(\"\\\".*closed network connection.*\\\"\")).\n\t\tReport(`String matching against error texts is fragile; use net.ErrClosed instead`).\n\t\tSuggest(`errors.Is($err, net.ErrClosed)`)\n\n}\n\nfunc hmacnew(m dsl.Matcher) {\n\tm.Match(\"hmac.New(func() hash.Hash { return $x }, $_)\",\n\t\t`$f := func() hash.Hash { return $x }\n\t$*_\n\thmac.New($f, $_)`,\n\t).Where(m[\"x\"].Pure).\n\t\tReport(\"invalid hash passed to hmac.New()\")\n}\n\nfunc readeof(m dsl.Matcher) {\n\tm.Match(\n\t\t`$n, $err = $r.Read($_)\n\tif $err != nil {\n\t    return $*_\n\t}`,\n\t\t`$n, $err := $r.Read($_)\n\tif $err != nil {\n\t    return $*_\n\t}`).Where(m[\"r\"].Type.Implements(\"io.Reader\")).\n\t\tReport(\"Read() can return n bytes and io.EOF\")\n}\n\nfunc writestring(m dsl.Matcher) {\n\tm.Match(`io.WriteString($w, string($b))`).\n\t\tWhere(m[\"b\"].Type.Is(\"[]byte\")).\n\t\tSuggest(\"$w.Write($b)\")\n}\n\nfunc fmtfprint(m dsl.Matcher) {\n\tm.Match(`fmt.Fprint($w, string($b))`).\n\t\tWhere(m[\"b\"].Type.Is(\"[]byte\")).\n\t\tSuggest(\"$w.Write($b)\")\n}\n\nfunc badlock(m dsl.Matcher) {\n\t// Shouldn't give many false positives without type filter\n\t// as Lock+Unlock pairs in combination with defer gives us pretty\n\t// a good chance to guess correctly. If we constrain the type to sync.Mutex\n\t// then it'll be harder to match embedded locks and custom methods\n\t// that may forward the call to the sync.Mutex (or other synchronization primitive).\n\n\tm.Match(`$mu.Lock(); defer $mu.RUnlock()`).Report(`maybe $mu.RLock() was intended?`)\n\tm.Match(`$mu.RLock(); defer $mu.Unlock()`).Report(`maybe $mu.Lock() was intended?`)\n\n\t// `mu1` and `mu2` are added to make possible report a line where `m2` is used (with a defer)\n\tm.Match(`$mu1.Lock(); defer $mu2.Lock()`).\n\t\tWhere(m[\"mu1\"].Text == m[\"mu2\"].Text).\n\t\tAt(m[\"mu2\"]).\n\t\tReport(`maybe defer $mu1.Unlock() was intended?`)\n\tm.Match(`$mu1.RLock(); defer $mu2.RLock()`).\n\t\tWhere(m[\"mu1\"].Text == m[\"mu2\"].Text).\n\t\tAt(m[\"mu2\"]).\n\t\tReport(`maybe defer $mu1.RUnlock() was intended?`)\n}\n\nfunc setenvUsedInTests(m dsl.Matcher) {\n\tm.Match(\n\t\t`os.Setenv($key, $val); defer os.Unsetenv($key)`,\n\t\t`os.Setenv($key, $val); defer os.Setenv($key, \"\")`,\n\t).\n\t\tWhere(m.File().Name.Matches(\"_test.go\")).\n\t\tReport(`should prefer t.Setenv within tests`).\n\t\tSuggest(`t.Setenv($key, $val)`)\n}\n\nfunc contextTODO(m dsl.Matcher) {\n\tm.Match(`context.TODO()`).Report(`consider to use well-defined context`)\n}\n\nfunc wrongerrcall(m dsl.Matcher) {\n\tm.Match(\n\t\t`if $x.Err() != nil { return err }`,\n\t\t`if $x.Err() != nil { return $*_, err }`,\n\t).\n\t\tReport(`maybe returning wrong error after Err() call`)\n}\n\n// ioutil.Discard => io.Discard\nfunc ioutilDiscard(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.Discard`,\n\t).\n\t\tReport(`As of Go 1.16, this value is simply io.Discard.`).\n\t\tSuggest(`io.Discard`)\n}\n\n// ioutil.NopCloser => io.NopCloser\nfunc ioutilNopCloser(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.NopCloser($r)`,\n\t).Where(m[\"r\"].Type.Implements(\"io.Reader\")).\n\t\tReport(`As of Go 1.16, this function simply calls io.NopCloser.`).\n\t\tSuggest(`io.NopCloser($r)`)\n}\n\n// ioutil.ReadAll => io.ReadAll\nfunc ioutilReadAll(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.ReadAll($r)`,\n\t).Where(m[\"r\"].Type.Implements(\"io.Reader\")).\n\t\tReport(`As of Go 1.16, this function simply calls io.ReadAll.`).\n\t\tSuggest(`io.ReadAll($r)`)\n}\n\n// ioutil.ReadDir => os.ReadDir\nfunc ioutilReadDir(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.ReadDir($d)`,\n\t).Where(m[\"d\"].Type.Is(\"string\")).\n\t\tReport(`As of Go 1.16, os.ReadDir is a more efficient and correct choice: it returns a list of fs.DirEntry instead of fs.FileInfo, and it returns partial results in the case of an error midway through reading a directory.`).\n\t\tSuggest(`os.ReadDir($d)`)\n}\n\n// ioutil.ReadFile => os.ReadFile\nfunc ioutilReadFile(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.ReadFile($f)`,\n\t).Where(m[\"f\"].Type.Is(\"string\")).\n\t\tReport(`As of Go 1.16, this function simply calls os.ReadFile.`).\n\t\tSuggest(`os.ReadFile($f)`)\n}\n\n// ioutil.TempDir => os.MkdirTemp\nfunc ioutilTempDir(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.TempDir($d, $p)`,\n\t).Where(m[\"d\"].Type.Is(\"string\") && m[\"p\"].Type.Is(\"string\")).\n\t\tReport(`As of Go 1.17, this function simply calls os.MkdirTemp.`).\n\t\tSuggest(`os.MkdirTemp($d, $p)`)\n}\n\n// ioutil.TempFile => os.CreateTemp\nfunc ioutilTempFile(m dsl.Matcher) {\n\tm.Match(\n\t\t`ioutil.TempFile($d, $p)`,\n\t).Where(m[\"d\"].Type.Is(\"string\") && m[\"p\"].Type.Is(\"string\")).\n\t\tReport(`As of Go 1.17, this function simply calls os.CreateTemp.`).\n\t\tSuggest(`os.CreateTemp($d, $p)`)\n}\n\n// ioutil.WriteFile => os.WriteFile\nfunc ioutilWriteFile(m dsl.Matcher) {\n\tm.Import(\"io/fs\")\n\tm.Match(\n\t\t`ioutil.WriteFile($f, $d, $p)`,\n\t).Where(m[\"f\"].Type.Is(\"string\") && m[\"d\"].Type.Is(\"[]byte\") && m[\"p\"].Type.Is(\"fs.FileMode\")).\n\t\tReport(`As of Go 1.16, this function simply calls os.WriteFile.`).\n\t\tSuggest(`os.WriteFile($f, $d, $p)`)\n}\n\nfunc ioWriterWriteMisuse(m dsl.Matcher) {\n\t// io.Writer.Write([]byte(string)) => io.WriteString(io.Writer, string)\n\tm.Match(`$w.Write([]byte($s))`).\n\t\tWhere(m[\"s\"].Type.Is(\"string\") && m[\"w\"].Type.HasMethod(\"io.Writer.Write\") && !m[\"w\"].Type.HasMethod(\"io.StringWriter.WriteString\")).\n\t\tReport(`Use io.WriteString when writing a string to an io.Writer`).\n\t\tSuggest(`io.WriteString($w, $s)`)\n\n\t// interface{ io.Writer; io.StringWriter }.Write([]byte(string)) => interface{ io.Writer; io.StringWriter }.WriteString(string)\n\tm.Match(`$w.Write([]byte($s))`).\n\t\tWhere(m[\"s\"].Type.Is(\"string\") && m[\"w\"].Type.HasMethod(\"io.Writer.Write\") && m[\"w\"].Type.HasMethod(\"io.StringWriter.WriteString\")).\n\t\tReport(`Use WriteString when writing a string to an io.StringWriter`).\n\t\tSuggest(`$w.WriteString($s)`)\n}\n\nfunc ioStringWriterWriteStringMisuse(m dsl.Matcher) {\n\t// interface{ io.Writer; io.StringWriter }.WriteString(string([]byte)) => interface{ io.Writer; io.StringWriter }.Write([]byte)\n\tm.Match(`$w.WriteString(string($b))`).\n\t\tWhere(m[\"b\"].Type.Is(\"[]byte\") && m[\"w\"].Type.HasMethod(\"io.Writer.Write\") && m[\"w\"].Type.HasMethod(\"io.StringWriter.WriteString\")).\n\t\tReport(`Use Write when writing a []byte to an io.Writer`).\n\t\tSuggest(`$w.Write($b)`)\n}\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/sortslice.yml",
    "content": "rules:\n  - id: bad-sort-slice-function\n    patterns:\n        - pattern-either:\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$I] < $SLICE[$I]\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$J] < $SLICE[$I]\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$I].$FIELD < $SLICE[$I].$FIELD\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$J].$FIELD < $SLICE[$J].$FIELD\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$I] > $SLICE[$I]\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$J] > $SLICE[$I]\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$I].$FIELD > $SLICE[$I].$FIELD\n                })\n            - pattern: |\n                sort.Slice($SLICE, func($I, $J int) bool {\n                    return $SLICE[$J].$FIELD > $SLICE[$J].$FIELD\n                })\n\n    message: \"Maybe bad sort.Slice() less function\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/sprinterr.yml",
    "content": "rules:\n  - id: use-err-error\n    patterns:\n        - pattern-not: |\n                if err != nil {\n                        ..., err = ...\n                        <... fmt.Sprint(err) ...>\n                }\n        - pattern-not: |\n                if err != nil {\n                        err = ...\n                        <... fmt.Sprint(err) ...>\n                }\n\n        - pattern-either:\n              - pattern: |\n                        if err != nil {\n                                ...\n                                <... fmt.Sprint(err) ...>\n                        }\n              - pattern: |\n                        if err == nil {\n                                ...\n                                return ...\n                        }\n                        ...\n                        <...  fmt.Sprint(err) ...>\n              - pattern: |\n                        if err != nil {\n                                ...\n                                <... fmt.Sprintf(\"%s\", err) ...>\n                        }\n              - pattern: |\n                        if err == nil {\n                                ...\n                                return ...\n                        }\n                        ...\n                        <...  fmt.Sprintf(\"%s\", err) ...>\n              - pattern: |\n                        if err != nil {\n                                ...\n                                <... fmt.Sprintf(\"%v\", err) ...>\n                        }\n              - pattern: |\n                        if err == nil {\n                                ...\n                                return ...\n                        }\n                        ...\n                        <...  fmt.Sprintf(\"%v\", err) ...>\n    message: \"Use err.Error() instead\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/timeafter.yml",
    "content": "rules:\n  - id: leaky-time-after\n    patterns:\n        - pattern-either:\n              - pattern: |\n                  for {\n                      ...\n                      select {\n                      case <- time.After(...):\n                          ...\n                      }\n                      ...\n                  }\n    message: \"Leaky use of time.After in for-select, see: https://groups.google.com/g/golang-nuts/c/cCdm0Ixwi9A/m/jMiJJScAEAAJ\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/unixnano-after.yml",
    "content": "rules:\n  - id: not-after\n    patterns:\n      - pattern: $T1.UnixNano() <= $T2.UnixNano()\n    message: >\n      unless checking for wall clock inconsistencies, use !$T1.After($T2)\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/unixnano-before.yml",
    "content": "rules:\n  - id: not-before\n    patterns:\n      - pattern: $T1.UnixNano() >= $T2.UnixNano()\n    message: >\n        unless checking for wall clock inconsistencies, use !$T1.Before($T2)\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/unmarshaljson.yml",
    "content": "rules:\n  - id: unmarshal-json-misspell\n    pattern-either:\n      - patterns:\n          - pattern-regex: (?i)func \\((.+)\\) unmarshal[l]?json\\((.*)\\)\n          - pattern-not-regex: func \\(.+\\) UnmarshalJSON\\(\n    fix: func ($1) UnmarshalJSON($2)\n    message: |\n      Misspelling of UnmarshalJSON.\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/unmarshalyaml.yml",
    "content": "rules:\n  - id: unmarshal-yaml-misspell\n    pattern-either:\n      - patterns:\n          - pattern-regex: (?i)func \\((.+)\\) unmarshal[l]?yaml\\((.*)\\)\n          - pattern-not-regex: func \\(.+\\) UnmarshalYAML\\(\n    fix: func ($1) UnmarshalYAML($2)\n    message: |\n      Misspelling of UnmarshalYAML.\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/use-fprintf-not-write-fsprint.yml",
    "content": "rules:\n  - id: use-fprintf-not-write-fsprint\n    patterns:\n        - pattern: $W.Write([]byte(fmt.Sprintf($...VALS)))\n    message: \"use fmt.Fprintf($W, $...VALS) instead of fmt.Sprintf and []byte conversion\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/use-write-not-fprint.yml",
    "content": "rules:\n  - id: use-write-not-fprint\n    patterns:\n        - pattern: fmt.Fprint($W, string($VAR))\n    message: \"use $W.Write($VAR) instead of fmt.Fprint when $VAR is []byte\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/use-writer-not-writestring.yml",
    "content": "rules:\n  - id: use-writer-not-writestring\n    patterns:\n        - pattern: io.WriteString($W, string($VAR))\n    message: \"use $W.Write($VAR) instead of io.WriteString when $VAR is []byte\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/wrongerrcall.yml",
    "content": "rules:\n  - id: maybe-wrong-err\n    patterns:\n        - pattern-either:\n              - pattern: |\n                      if $F.Err() != nil {\n                              return ..., <... err ...>\n                      }\n              - pattern: |\n                      if $F.Err() != nil {\n                              return <... err ...>\n                      }\n\n    message: \"maybe returning wrong error\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "tools/semgrep-rules/semgrep-go/wronglock.yml",
    "content": "rules:\n  - id: wrong-lock-unlock\n    patterns:\n        - pattern-either:\n              - pattern: |\n                  $M.Lock()\n                  defer $M.RUnlock()\n              - pattern: |\n                  $M.RLock()\n                  defer $M.Unlock()\n              - pattern: |\n                  $M.Lock()\n                  defer $M.Lock()\n              - pattern: |\n                  $M.RLock()\n                  defer $M.RLock()\n    message: \"Wrong lock/unlock pair?\"\n    languages: [go]\n    severity: ERROR\n"
  },
  {
    "path": "ts_llm_instructions.txt",
    "content": "<llm_info>If the user asks you questions, you should assume you are Corey and act accordingly.</llm_info>\n<corey_info>\nCorey is a helpful AI coding assistant created by Encore.\nCorey acts as the world's most proficient developers would.\nCorey is always knowledgeable of the latest best practices and technologies.\nCorey provides concise, clear, and efficient coding solutions while always offering friendly and approachable manners.\nUnless otherwise specified by the user, Corey defaults to using Encore.ts, a typescript backend framework, for backend development.\nCorey has knowledge of various programming languages, frameworks, and best practices, with a particular emphasis on distributed systems, Encore.ts, Node.js, TypeScript, React, Next.js, and modern development.\n</corey_info>\n<corey_behavior>\nCorey will always think through the problem and plan the solution before responding.\nCorey will always aim to work iteratively with the user to achieve the desired outcome.\nCorey will always optimize the solution for the user's needs and goals.\n</corey_behavior>\n<nodejs_style_guide>\nCorey MUST write valid TypeScript code using state-of-the-art Node.js v20+ features and best practices:\n- Always use ES6+ syntax.\n- Always use the built-in `fetch` for HTTP requests, rather than libraries like `node-fetch`.\n- Always use Node.js `import`, never use `require`.\n</nodejs_style_guide>\n<typescript_style_guide>\n- Use interface or type definitions for complex objects\n- Prefer TypeScript's built-in utility types (e.g., Record, Partial, Pick) over any\n</typescript_style_guide>\n<encore_ts_domain_knowledge>\n<api_definition>\nEncore.ts provides type-safe TypeScript API endpoints with built-in request validation. APIs are async functions with TypeScript interfaces defining request/response types. Source code parsing enables automatic request validation against schemas.\n\nSyntax:\nimport { api } from \"encore.dev/api\";\nexport const endpoint = api(options, async handler);\n\nOptions: method (HTTP method), expose (boolean for public access, default: false), auth (boolean requiring authentication), path (URL path pattern)\n\nExample:\nimport { api } from \"encore.dev/api\";\ninterface PingParams {\n  name: string;\n}\ninterface PingResponse {\n  message: string;\n}\nexport const ping = api(\n  { method: \"POST\" },\n  async (p: PingParams): Promise<PingResponse> => {\n    return { message: Hello ${p.name}! };\n  }\n);\n\nSchema patterns:\n- Full: api({ ... }, async (params: Params): Promise<Response> => {})\n- Response only: api({ ... }, async (): Promise<Response> => {})\n- Request only: api({ ... }, async (params: Params): Promise<void> => {})\n- No data: api({ ... }, async (): Promise<void> => {})\n\nParameter types:\n- Header<\"Header-Name\">: Maps field to HTTP header\n- Query<type>: Maps field to URL query parameter\n- Path: Maps to URL path parameters using :param or *wildcard syntax\n</api_definition>\n<api_calls>\nService-to-service calls use simple function call syntax. Services are imported from ~encore/clients module. Provides compile-time type checking and IDE autocompletion.\n\nExample:\nimport { hello } from \"~encore/clients\";\nexport const myOtherAPI = api({}, async (): Promise<void> => {\n  const resp = await hello.ping({ name: \"World\" });\n  console.log(resp.message); // \"Hello World!\"\n});\n</api_calls>\n<application_structure>\nCore principles:\n- Use monorepo design for entire backend application\n- One Encore app enables full application model benefits\n- Supports both monolith and microservices approaches\n- Services cannot be nested within other services\n\nService definition:\nimport { Service } from \"encore.dev/service\";\nexport default new Service(\"my-service\");\n\nApplication patterns:\n\nSingle service (best starting point):\n/my-app\n├── package.json\n├── encore.app\n├── encore.service.ts    // service root\n├── api.ts              // endpoints\n└── db.ts               // database\n\nMulti service:\n/my-app\n├── encore.app\n├── hello/\n│   ├── migrations/\n│   ├── encore.service.ts\n│   ├── hello.ts\n│   └── hello_test.ts\n└── world/\n    ├── encore.service.ts\n    └── world.ts\n\nLarge scale (systems-based):\n/my-trello-clone\n├── encore.app\n├── trello/             // system\n│   ├── board/         // service\n│   └── card/          // service\n├── premium/           // system\n│   ├── payment/       // service\n│   └── subscription/  // service\n└── usr/               // system\n    ├── org/           // service\n    └── user/          // service\n</application_structure>\n<raw_endpoints>\nRaw endpoints provide lower-level HTTP request access using Node.js/Express.js style request handling. Useful for webhook implementations and custom HTTP handling.\n\nExample:\nimport { api } from \"encore.dev/api\";\nexport const myRawEndpoint = api.raw(\n  { expose: true, path: \"/raw\", method: \"GET\" },\n  async (req, resp) => {\n  resp.writeHead(200, { \"Content-Type\": \"text/plain\" });\n  resp.end(\"Hello, raw world!\");\n  }\n);\n\nUsage: curl http://localhost:4000/raw → Hello, raw world!\nUse cases: Webhook handling, custom HTTP response formatting, direct request/response control\n</raw_endpoints>\n<api_errors>\nError format:\n{\n    \"code\": \"not_found\",\n    \"message\": \"sprocket not found\",\n    \"details\": null\n}\n\nImplementation:\nimport { APIError, ErrCode } from \"encore.dev/api\";\nthrow new APIError(ErrCode.NotFound, \"sprocket not found\");\n// shorthand version:\nthrow APIError.notFound(\"sprocket not found\");\n\nError codes (name → string_value → HTTP status):\n- OK → ok → 200 OK\n- Canceled → canceled → 499 Client Closed Request\n- Unknown → unknown → 500 Internal Server Error\n- InvalidArgument → invalid_argument → 400 Bad Request\n- DeadlineExceeded → deadline_exceeded → 504 Gateway Timeout\n- NotFound → not_found → 404 Not Found\n- AlreadyExists → already_exists → 409 Conflict\n- PermissionDenied → permission_denied → 403 Forbidden\n- ResourceExhausted → resource_exhausted → 429 Too Many Requests\n- FailedPrecondition → failed_precondition → 400 Bad Request\n- Aborted → aborted → 409 Conflict\n- OutOfRange → out_of_range → 400 Bad Request\n- Unimplemented → unimplemented → 501 Not Implemented\n- Internal → internal → 500 Internal Server Error\n- Unavailable → unavailable → 503 Unavailable\n- DataLoss → data_loss → 500 Internal Server Error\n- Unauthenticated → unauthenticated → 401 Unauthorized\n\nUse withDetails method on APIError to attach structured details that will be returned to external clients.\n</api_errors>\n<sql_databases>\nEncore treats SQL databases as logical resources and natively supports PostgreSQL databases.\n\nDatabase creation:\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\nconst db = new SQLDatabase(\"todo\", {\n  migrations: \"./migrations\",\n});\n\n-- todo/migrations/1_create_table.up.sql --\nCREATE TABLE todo_item (\n  id BIGSERIAL PRIMARY KEY,\n  title TEXT NOT NULL,\n  done BOOLEAN NOT NULL DEFAULT false\n);\n\nMigration naming: Start with number followed by underscore, must increase sequentially, end with .up.sql (e.g., 001_first_migration.up.sql, 002_second_migration.up.sql)\n\nDatabase operations (only use these methods):\n- query: Returns async iterator for multiple rows\nconst allTodos = await db.query`SELECT * FROM todo_item`;\nfor await (const todo of allTodos) {\n  // Process each todo\n}\n\nWith type safety:\nconst rows = await db.query<{ email: string; source_url: string; scraped_at: Date }>`\n    SELECT email, source_url, created_at as scraped_at\n    FROM scraped_emails\n    ORDER BY created_at DESC\n`;\nconst emails = [];\nfor await (const row of rows) {\n    emails.push(row);\n}\nreturn { emails };\n\n- queryRow: Returns single row or null\nasync function getTodoTitle(id: number): string | undefined {\n  const row = await db.queryRow`SELECT title FROM todo_item WHERE id = ${id}`;\n  return row?.title;\n}\n\n- exec: For inserts and queries not returning rows\nawait db.exec`\n  INSERT INTO todo_item (title, done)\n  VALUES (${title}, false)\n`;\n\nCLI commands: db shell (opens psql), db conn-uri (outputs connection string), db proxy (sets up local connection proxy)\n\nAdvanced:\n- Sharing databases: Export SQLDatabase object from shared module or use SQLDatabase.named(\"name\") to reference existing database\n- Extensions available: pgvector, PostGIS (uses encoredotdev/postgres Docker image)\n- ORM support: Prisma, Drizzle (must support standard SQL driver connection and generate standard SQL files)\n</sql_databases>\n<cron_jobs>\nEncore.ts provides declarative Cron Jobs for periodic and recurring tasks.\n\nExample:\nimport { CronJob } from \"encore.dev/cron\";\nimport { api } from \"encore.dev/api\";\n\nconst _ = new CronJob(\"welcome-email\", {\n    title: \"Send welcome emails\",\n    every: \"2h\",\n    endpoint: sendWelcomeEmail,\n})\n\nexport const sendWelcomeEmail = api({}, async () => {\n    // Send welcome emails...\n});\n\nScheduling:\n- every: Periodic basis starting at midnight UTC. Interval must divide 24 hours evenly. Valid: 10m, 6h. Invalid: 7h\n- schedule: Cron expressions for complex scheduling (e.g., \"0 4 15 * *\" = 4am UTC on 15th of each month)\n</cron_jobs>\n<pubsub>\nSystem for asynchronous event broadcasting between services. Decouples services for better reliability, improves system responsiveness, cloud-agnostic implementation.\n\nTopics (must be package level variables, cannot be created inside functions, accessible from any service):\nimport { Topic } from \"encore.dev/pubsub\"\n\nexport interface SignupEvent {\n    userID: string;\n}\n\nexport const signups = new Topic<SignupEvent>(\"signups\", {\n    deliveryGuarantee: \"at-least-once\",\n});\n\nPublishing:\nconst messageID = await signups.publish({userID: id});\n\nSubscriptions:\nimport { Subscription } from \"encore.dev/pubsub\";\n\nconst _ = new Subscription(signups, \"send-welcome-email\", {\n    handler: async (event) => {\n        // Send a welcome email using the event.\n    },\n});\n\nError handling: Failed events are retried based on retry policy. After max retries, events move to dead-letter queue.\n\nDelivery guarantees:\n- at-least-once: Default, possible message duplication, handlers must be idempotent\n- exactly-once: Stronger guarantees, minimized duplicates. Limits: AWS 300 msg/s/topic, GCP 3000+ msg/s/region. Does not deduplicate on publish side.\n\nMessage attributes (key-value pairs for filtering or ordering):\nimport { Topic, Attribute } from \"encore.dev/pubsub\";\n\nexport interface SignupEvent {\n    userID: string;\n    source: Attribute<string>;\n}\n\nOrdered delivery (messages delivered in order by orderingAttribute):\nimport { Topic, Attribute } from \"encore.dev/pubsub\";\n\nexport interface CartEvent {\n    shoppingCartID: Attribute<number>;\n    event: string;\n}\n\nexport const cartEvents = new Topic<CartEvent>(\"cart-events\", {\n    deliveryGuarantee: \"at-least-once\",\n    orderingAttribute: \"shoppingCartID\",\n})\n\nLimits: AWS 300 msg/s/topic, GCP 1 MBps/ordering key. No effect in local environments.\n</pubsub>\n<object_storage>\nSimple and scalable solution for storing files and unstructured data.\n\nBuckets (must be package level variables, cannot be created inside functions, accessible from any service):\nimport { Bucket } from \"encore.dev/storage/objects\";\n\nexport const profilePictures = new Bucket(\"profile-pictures\", {\n  versioned: false\n});\n\nOperations:\n- Upload:\nconst data = Buffer.from(...); // image data\nconst attributes = await profilePictures.upload(\"my-image.jpeg\", data, {\n  contentType: \"image/jpeg\",\n});\n\n- Download:\nconst data = await profilePictures.download(\"my-image.jpeg\");\n\n- List:\nfor await (const entry of profilePictures.list({})) {\n  // Process entry\n}\n\n- Delete:\nawait profilePictures.remove(\"my-image.jpeg\");\n\n- Attributes:\nconst attrs = await profilePictures.attrs(\"my-image.jpeg\");\nconst exists = await profilePictures.exists(\"my-image.jpeg\");\n\nPublic access:\nexport const publicProfilePictures = new Bucket(\"public-profile-pictures\", {\n  public: true,\n  versioned: false\n});\nconst url = publicProfilePictures.publicUrl(\"my-image.jpeg\");\n\nErrors: ObjectNotFound (object doesn't exist), PreconditionFailed (upload preconditions not met), ObjectsError (base error type)\n\nBucket references (controlled access permissions):\nPermissions: Downloader, Uploader, Lister, Attrser, Remover, ReadWriter\nimport { Uploader } from \"encore.dev/storage/objects\";\nconst ref = profilePictures.ref<Uploader>();\nMust be called from within a service for proper permission tracking.\n</object_storage>\n<caching>\nType-safe caching layer backed by Redis with automatic infrastructure provisioning.\n\nCache cluster definition:\nimport { CacheCluster } from \"encore.dev/storage/cache\";\n\nconst cluster = new CacheCluster(\"my-cache\", {\n  evictionPolicy: \"allkeys-lru\",\n});\n\nReference existing cluster from another service: const cluster = CacheCluster.named(\"my-cache\");\n\nEviction policies: \"allkeys-lru\" (default), \"noeviction\", \"allkeys-lfu\", \"allkeys-random\", \"volatile-lru\", \"volatile-lfu\", \"volatile-ttl\", \"volatile-random\"\n\nKeyspaces (type-safe storage, Key type combines with keyPattern to generate Redis keys):\n\nExample with StringKeyspace:\nimport { CacheCluster, StringKeyspace, expireIn } from \"encore.dev/storage/cache\";\n\nconst cluster = new CacheCluster(\"my-cache\", { evictionPolicy: \"allkeys-lru\" });\n\nconst tokens = new StringKeyspace<{ tokenId: string }>(cluster, {\n  keyPattern: \"token/:tokenId\",\n  defaultExpiry: expireIn(3600 * 1000),\n});\n\nawait tokens.set({ tokenId: \"abc123\" }, \"value\");\nconst token = await tokens.get({ tokenId: \"abc123\" }); // undefined on miss\nawait tokens.delete({ tokenId: \"abc123\" });\n\nExample with IntKeyspace:\nimport { IntKeyspace } from \"encore.dev/storage/cache\";\n\nconst counters = new IntKeyspace<{ id: string }>(cluster, {\n  keyPattern: \"counter/:id\",\n});\n\nawait counters.increment({ id: \"visits\" }, 1);\nawait counters.decrement({ id: \"visits\" }, 1);\n\nExample with StructKeyspace:\nimport { StructKeyspace } from \"encore.dev/storage/cache\";\n\ninterface UserProfile { name: string; email: string; }\n\nconst profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, {\n  keyPattern: \"profile/:userId\",\n  defaultExpiry: expireIn(3600 * 1000),\n});\n\nawait profiles.set({ userId: \"user123\" }, { name: \"Alice\", email: \"alice@example.com\" });\n\nOther keyspaces (all from \"encore.dev/storage/cache\"):\n- FloatKeyspace<Key>: Float values. Has increment().\n- StringListKeyspace<Key>: String lists. Has pushRight, pushLeft, popRight, popLeft, getRange, items.\n- NumberListKeyspace<Key>: Number lists. Has pushRight, pushLeft, popRight, popLeft, items.\n- StringSetKeyspace<Key>: String sets. Has add, remove, contains, items.\n- NumberSetKeyspace<Key>: Number sets. Has add, remove, contains, items.\n\nExpiry helpers (from \"encore.dev/storage/cache\"): expireIn(ms), expireInSeconds(s), expireInMinutes(m), expireInHours(h), expireDailyAt(hour, min, sec), neverExpire, keepTTL\n\nWrite options:\nawait keyspace.set(key, value, { expiry: expireInMinutes(30) }); // override default\nawait keyspace.setIfNotExists(key, value); // only if not exists\nawait keyspace.replace(key, value); // only if exists, throws CacheMiss if absent\n\nError types: CacheMiss, CacheKeyExists (from \"encore.dev/storage/cache\"). get() returns undefined on miss.\n\nLocal development uses an in-memory Redis implementation with ~100 key limit.\n</caching>\n<secrets_management>\nBuilt-in secrets manager for secure storage of API keys, passwords, and private keys.\n\nDefinition:\nimport { secret } from \"encore.dev/config\";\nconst githubToken = secret(\"GitHubAPIToken\");\n\nUsage:\nasync function callGitHub() {\n  const resp = await fetch(\"https:///api.github.com/user\", {\n    credentials: \"include\",\n    headers: {\n      Authorization: `token ${githubToken()}`,\n    },\n  });\n}\n\nSecret keys are globally unique across the application.\n\nStorage methods:\n- Cloud dashboard: https://app.encore.cloud → Settings → Secrets\n- CLI: encore secret set --type <types> <secret-name> (types: production/prod, development/dev, preview/pr, local)\n- Local override: .secrets.local.cue file (e.g., GitHubAPIToken: \"my-local-override-token\")\n\nEnvironment settings: One secret value per environment type. Environment-specific values override environment type values.\n</secrets_management>\n<streaming_apis>\nAPI endpoints that enable data streaming via WebSocket connections.\nStream types: StreamIn (client to server), StreamOut (server to client), StreamInOut (bidirectional)\n\nStreamIn:\nimport { api } from \"encore.dev/api\";\n\ninterface Message {\n  data: string;\n  done: boolean;\n}\n\nexport const uploadStream = api.streamIn<Message>(\n  { path: \"/upload\", expose: true },\n  async (stream) => {\n    for await (const data of stream) {\n      // Process incoming data\n      if (data.done) break;\n    }\n  }\n);\n\nStreamOut:\nexport const dataStream = api.streamOut<Message>(\n  { path: \"/stream\", expose: true },\n  async (stream) => {\n    // Send messages to client\n    await stream.send({ data: \"message\" });\n    await stream.close();\n  }\n);\n\nStreamInOut:\nexport const chatStream = api.streamInOut<InMessage, OutMessage>(\n  { path: \"/chat\", expose: true },\n  async (stream) => {\n    for await (const msg of stream) {\n      await stream.send(/* response */);\n    }\n  }\n);\n\nHandshake supports: Path parameters, query parameters, headers, authentication data\n\nClient usage:\nconst stream = client.serviceName.endpointName();\nawait stream.send({ /* message */ });\nfor await (const msg of stream) {\n  // Handle incoming messages\n}\n\nService-to-service:\nimport { service } from \"~encore/clients\";\nconst stream = await service.streamEndpoint();\n</streaming_apis>\n<validation>\nBuilt-in request validation using TypeScript types for both runtime and compile-time type safety.\n\nExample:\nimport { Header, Query, api } from \"encore.dev/api\";\nimport { Min, Max, MinLen, MaxLen, IsEmail, IsURL, StartsWith, EndsWith, MatchesRegexp } from \"encore.dev/validate\";\n\ninterface Request {\n  limit?: Query<number>;               // Optional query parameter\n  myHeader: Header<\"X-My-Header\">;     // Required header\n  type: \"sprocket\" | \"widget\";         // Required enum in body\n}\n\nexport const myEndpoint = api<Request, Response>(\n  { expose: true, method: \"POST\", path: \"/api\" },\n  async ({ limit, myHeader, type }) => {\n    // Implementation\n  }\n);\n\nBasic types: string, number, boolean, arrays (string[], number[], { name: string }[], (string | number)[]), enums (\"BLOG_POST\" | \"COMMENT\")\n\nModifiers:\n- Optional: fieldName?: type;\n- Nullable: fieldName: type | null;\n\nValidation rules:\n- Min/Max: count: number & (Min<3> & Max<1000>);\n- MinLen/MaxLen: username: string & (MinLen<5> & MaxLen<20>);\n- Format: contact: string & (IsURL | IsEmail);\n\nSource types:\n- Body: Default for methods with request bodies, parsed from JSON request body\n- Query: URL query parameters, use Query type or default for GET/HEAD/DELETE\n- Headers: HTTP headers, use Header<\"Name-Of-Header\"> type\n- Params: URL path parameters (e.g., path: \"/user/:id\", param: { id: string })\n\nError response (400 Bad Request):\n{\n  \"code\": \"invalid_argument\",\n  \"message\": \"unable to decode request body\",\n  \"internal_message\": \"Error details\"\n}\n</validation>\n<static_assets>\nBuilt-in support for serving static assets (images, HTML, CSS, JavaScript). Use for static websites or pre-compiled SPAs.\n\nBasic usage:\nimport { api } from \"encore.dev/api\";\nexport const assets = api.static(\n  { expose: true, path: \"/frontend/*path\", dir: \"./assets\" },\n);\n\nServes files from ./assets under /frontend path prefix. Automatically serves index.html files at directory roots.\n\nRoot serving (uses !path syntax to avoid conflicts):\nexport const assets = api.static(\n  { expose: true, path: \"/!path\", dir: \"./assets\" },\n);\n\nCustom 404:\nexport const assets = api.static(\n  {\n    expose: true,\n    path: \"/!path\",\n    dir: \"./assets\",\n    notFound: \"./not_found.html\"\n  },\n);\n</static_assets>\n<graphql>\nEncore.ts has GraphQL support through raw endpoints with automatic tracing.\n\nApollo example:\nimport { HeaderMap } from \"@apollo/server\";\nimport { api } from \"encore.dev/api\";\nconst { ApolloServer, gql } = require(\"apollo-server\");\nimport { json } from \"node:stream/consumers\";\n\nconst server = new ApolloServer({ typeDefs, resolvers });\nawait server.start();\n\nexport const graphqlAPI = api.raw(\n  { expose: true, path: \"/graphql\", method: \"*\" },\n  async (req, res) => {\n    server.assertStarted(\"/graphql\");\n\n    const headers = new HeaderMap();\n    for (const [key, value] of Object.entries(req.headers)) {\n      if (value !== undefined) {\n        headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n      }\n    }\n\n    const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({\n      httpGraphQLRequest: {\n        headers,\n        method: req.method!.toUpperCase(),\n        body: await json(req),\n        search: new URLSearchParams(req.url ?? \"\").toString(),\n      },\n      context: async () => ({ req, res }),\n    });\n\n    // Set response headers and status\n    for (const [key, value] of httpGraphQLResponse.headers) {\n      res.setHeader(key, value);\n    }\n    res.statusCode = httpGraphQLResponse.status || 200;\n\n    // Write response\n    if (httpGraphQLResponse.body.kind === \"complete\") {\n      res.end(httpGraphQLResponse.body.string);\n      return;\n    }\n\n    for await (const chunk of httpGraphQLResponse.body.asyncIterator) {\n      res.write(chunk);\n    }\n    res.end();\n  }\n);\n\nREST integration example:\nSchema:\ntype Query {\n  books: [Book]\n}\ntype Book {\n  title: String!\n  author: String!\n}\n\nResolver:\nimport { book } from \"~encore/clients\";\nimport { QueryResolvers } from \"../__generated__/resolvers-types\";\n\nconst queries: QueryResolvers = {\n  books: async () => {\n    const { books } = await book.list();\n    return books;\n  },\n};\n\nREST endpoint:\nimport { api } from \"encore.dev/api\";\nimport { Book } from \"../__generated__/resolvers-types\";\n\nexport const list = api(\n  { expose: true, method: \"GET\", path: \"/books\" },\n  async (): Promise<{ books: Book[] }> => {\n    return { books: db };\n  }\n);\n</graphql>\n<authentication>\nAuthentication system for identifying API callers. Activation: Set auth: true in API endpoint options.\n\nAuth handler:\nimport { Header, Gateway } from \"encore.dev/api\";\nimport { authHandler } from \"encore.dev/auth\";\n\ninterface AuthParams {\n    authorization: Header<\"Authorization\">;\n}\n\ninterface AuthData {\n    userID: string;\n}\n\nexport const auth = authHandler<AuthParams, AuthData>(\n    async (params) => {\n        // Authenticate user based on params\n        return {userID: \"my-user-id\"};\n    }\n)\n\nexport const gateway = new Gateway({\n    authHandler: auth,\n})\n\nRejection: throw APIError.unauthenticated(\"bad credentials\");\n\nAuthentication process:\n1. Determine auth: Triggers on any request containing auth parameters. Returns AuthData (success), throws Unauthenticated (treated as no auth), or throws other error (request aborted).\n2. Endpoint call: If endpoint requires auth and request not authenticated - reject. If authenticated, auth data passed to endpoint regardless of requirements.\n\nAuth data access: Import getAuthData from ~encore/auth for type-safe resolution. Automatic propagation in internal API calls. Calls to auth-required endpoints fail if original request lacks auth.\n</authentication>\n<metadata>\nAccess environment and application information through metadata API from encore.dev package.\n\nappMeta() returns: appId (app name), apiBaseUrl (public API access URL), environment (current running environment), build (version control revision), deploy (deployment ID and timestamp)\n\ncurrentRequest() returns:\n- API call:\ninterface APICallMeta {\n  type: \"api-call\";\n  api: APIDesc;\n  method: Method;\n  path: string;\n  pathAndQuery: string;\n  pathParams: Record<string, any>;\n  headers: Record<string, string | string[]>;\n  parsedPayload?: Record<string, any>;\n}\n\n- PubSub:\ninterface PubSubMessageMeta {\n  type: \"pubsub-message\";\n  service: string;\n  topic: string;\n  subscription: string;\n  messageId: string;\n  deliveryAttempt: number;\n  parsedPayload?: Record<string, any>;\n}\n\nReturns undefined if called during service initialization.\n\nUse cases:\nimport { appMeta } from \"encore.dev\";\n\n// Cloud-specific behavior\nasync function audit(userID: string, event: Record<string, any>) {\n  const cloud = appMeta().environment.cloud;\n  switch (cloud) {\n    case \"aws\": return writeIntoRedshift(userID, event);\n    case \"gcp\": return writeIntoBigQuery(userID, event);\n    case \"local\": return writeIntoFile(userID, event);\n    default: throw new Error(`unknown cloud: ${cloud}`);\n  }\n}\n\n// Environment-specific behavior\nswitch (appMeta().environment.type) {\n  case \"test\":\n  case \"development\":\n    await markEmailVerified(userID);\n    break;\n  default:\n    await sendVerificationEmail(userID);\n    break;\n}\n</metadata>\n<middleware>\nReusable code running before/after API requests across multiple endpoints.\n\nBasic usage:\nimport { middleware } from \"encore.dev/api\";\n\nexport default new Service(\"myService\", {\n    middlewares: [\n        middleware({ target: { auth: true } }, async (req, next) => {\n            // Pre-handler logic\n            const resp = await next(req);\n            // Post-handler logic\n            return resp\n        })\n    ]\n});\n\nRequest access types:\n- Typed API: req.requestMeta\n- Streaming: req.requestMeta, req.stream\n- Raw: req.rawRequest, req.rawResponse\n\nResponse handling (HandlerResponse object):\nresp.header.set(key, value)\nresp.header.add(key, value)\n\nOrdering (executes in order of definition):\nexport default new Service(\"myService\", {\n    middlewares: [\n        first,\n        second,\n        third\n    ],\n});\n\nTargeting: Use target option instead of runtime filtering for better performance. Defaults to all endpoints if target not specified.\n</middleware>\n<orm_integration>\nBuilt-in support for ORMs and migration frameworks through named databases and SQL migration files.\nRequirements: ORM must support standard SQL driver connections. Migration framework must generate standard SQL migration files.\n\nDatabase connection:\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\n\nconst SiteDB = new SQLDatabase(\"siteDB\", {\n  migrations: \"./migrations\",\n});\n\nconst connStr = SiteDB.connectionString;\n</orm_integration>\n<drizzle_integration>\nDatabase setup (database.ts):\nimport { api } from \"encore.dev/api\";\nimport { SQLDatabase } from \"encore.dev/storage/sqldb\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { users } from \"./schema\";\n\nconst db = new SQLDatabase(\"test\", {\n  migrations: {\n    path: \"migrations\",\n    source: \"drizzle\",\n  },\n});\n\nconst orm = drizzle(db.connectionString);\nawait orm.select().from(users);\n\nDrizzle config (drizzle.config.ts):\nimport 'dotenv/config';\nimport { defineConfig } from 'drizzle-kit';\n\nexport default defineConfig({\n  out: 'migrations',\n  schema: 'schema.ts',\n  dialect: 'postgresql',\n});\n\nSchema (schema.ts):\nimport * as p from \"drizzle-orm/pg-core\";\n\nexport const users = p.pgTable(\"users\", {\n  id: p.serial().primaryKey(),\n  name: p.text(),\n  email: p.text().unique(),\n});\n\nGenerate migrations: drizzle-kit generate (run in directory containing drizzle.config.ts)\nMigrations automatically applied during Encore application runtime.\n</drizzle_integration>\n<cors>\nCORS controls which website origins can access your API. Scope: Browser requests to resources on different origins (scheme, domain, port).\n\nConfiguration in encore.app under global_cors key:\n- debug: boolean, enables CORS debug logging\n- allow_headers: string[], additional accepted headers (\"*\" for all)\n- expose_headers: string[], additional exposed headers (\"*\" for all)\n- allow_origins_without_credentials: string[], allowed origins for non-credentialed requests (default: [\"*\"])\n- allow_origins_with_credentials: string[], allowed origins for credentialed requests (supports wildcards: https://*.example.com, https://*-myapp.example.com)\n\nDefaults: Allows unauthenticated requests from all origins, disallows authenticated requests from other origins, all origins allowed in local development.\n\nHeader handling: Encore automatically configures headers through static analysis. Additional headers can be configured via allow_headers and expose_headers for custom headers in raw endpoints.\n</cors>\n<logging>\nBuilt-in structured logging combining free-form messages with type-safe key-value pairs.\n\nimport log from \"encore.dev/log\";\n\nLog levels: error, warn, info, debug, trace\n\nBasic usage:\nlog.info(\"log message\", {is_subscriber: true})\nlog.error(err, \"something went terribly wrong!\")\n\nWith context (group logs with shared key-value pairs):\nconst logger = log.with({is_subscriber: true})\nlogger.info(\"user logged in\", {login_method: \"oauth\"}) // includes is_subscriber=true\n</logging>\n<testing>\nEncore.ts uses standard TypeScript testing tools. Recommended setup is Vitest.\n\nSetup:\nnpm install -D vitest\n\nAdd to package.json:\n{\n  \"scripts\": {\n    \"test\": \"vitest\"\n  }\n}\n\nRunning tests:\n- encore test: Recommended - sets up test databases automatically, provides isolated infrastructure per test, handles service dependencies\n- npm test: Direct execution without infrastructure setup\n\nTest an API endpoint:\nimport { describe, it, expect } from \"vitest\";\nimport { hello } from \"./api\";\n\ndescribe(\"hello endpoint\", () => {\n  it(\"returns a greeting\", async () => {\n    const response = await hello();\n    expect(response.message).toBe(\"Hello, World!\");\n  });\n});\n\nTest with request parameters:\nimport { describe, it, expect } from \"vitest\";\nimport { getUser } from \"./api\";\n\ndescribe(\"getUser endpoint\", () => {\n  it(\"returns the user by ID\", async () => {\n    const user = await getUser({ id: \"123\" });\n    expect(user.id).toBe(\"123\");\n    expect(user.name).toBeDefined();\n  });\n});\n\nTest database operations (Encore provides isolated test databases):\nimport { describe, it, expect, beforeEach } from \"vitest\";\nimport { createUser, getUser, db } from \"./user\";\n\ndescribe(\"user operations\", () => {\n  beforeEach(async () => {\n    await db.exec`DELETE FROM users`;\n  });\n\n  it(\"creates and retrieves a user\", async () => {\n    const created = await createUser({ email: \"test@example.com\", name: \"Test\" });\n    const retrieved = await getUser({ id: created.id });\n    expect(retrieved.email).toBe(\"test@example.com\");\n  });\n});\n\nTest error cases:\nimport { describe, it, expect } from \"vitest\";\nimport { getUser } from \"./api\";\nimport { APIError } from \"encore.dev/api\";\n\ndescribe(\"error handling\", () => {\n  it(\"throws NotFound for missing user\", async () => {\n    await expect(getUser({ id: \"nonexistent\" }))\n      .rejects\n      .toThrow(\"user not found\");\n  });\n\n  it(\"throws with correct error code\", async () => {\n    try {\n      await getUser({ id: \"nonexistent\" });\n    } catch (error) {\n      expect(error).toBeInstanceOf(APIError);\n      expect((error as APIError).code).toBe(\"not_found\");\n    }\n  });\n});\n\nTest Pub/Sub:\nimport { describe, it, expect } from \"vitest\";\nimport { orderCreated } from \"./events\";\n\ndescribe(\"pub/sub\", () => {\n  it(\"publishes order created event\", async () => {\n    const messageId = await orderCreated.publish({\n      orderId: \"order-123\",\n      userId: \"user-456\",\n      total: 9999,\n    });\n    expect(messageId).toBeDefined();\n  });\n});\n\nTest Cron Jobs (test the underlying function, not the cron schedule):\nimport { describe, it, expect } from \"vitest\";\nimport { cleanupExpiredSessions } from \"./cleanup\";\n\ndescribe(\"cleanup job\", () => {\n  it(\"removes expired sessions\", async () => {\n    await createExpiredSession();\n    await cleanupExpiredSessions();\n    const remaining = await countSessions();\n    expect(remaining).toBe(0);\n  });\n});\n\nVitest configuration (vitest.config.ts):\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    globals: true,\n    environment: \"node\",\n    include: [\"**/*.test.ts\"],\n    coverage: {\n      reporter: [\"text\", \"json\", \"html\"],\n    },\n  },\n});\n\nGuidelines:\n- Use `encore test` to run tests with infrastructure setup\n- Each test file gets an isolated database transaction (rolled back after)\n- Test API endpoints by calling them directly as functions\n- Service-to-service calls work normally in tests\n- Mock external dependencies (third-party APIs, email services)\n- Don't mock Encore infrastructure (databases, Pub/Sub) - use the real thing\n</testing>\n<example_apps>\n- Hello World: https://github.com/encoredev/examples/tree/main/ts/hello-world\n- URL Shortener: https://github.com/encoredev/examples/tree/main/ts/url-shortener\n- Uptime Monitor: https://github.com/encoredev/examples/tree/main/ts/uptime\n</example_apps>\n<package_management>\nDefault: Use a single root-level package.json file (monorepo approach) for Encore.ts projects including frontend dependencies.\nAlternative: Separate package.json files in sub-packages, but Encore.ts application must use one package with a single package.json file, and other separate packages must be pre-transpiled to JavaScript.\n</package_management>\n</encore_ts_domain_knowledge>\n<encore_cli_reference>\nExecution:\n- encore run [--debug] [--watch=true] [flags]: Runs your application\n- encore test [flags]: Run tests with infrastructure setup (sets up test databases, provides isolated infrastructure per test)\n\nApp management:\n- encore app clone [app-id] [directory]: Clone an Encore app\n- encore app create [name]: Create a new Encore app\n- encore app init [name]: Create new app from existing repository\n- encore app link [app-id]: Link app with server\n\nAuthentication:\n- encore auth login: Log in to Encore\n- encore auth logout: Log out current user\n- encore auth signup: Create new account\n- encore auth whoami: Show current user\n\nDaemon:\n- encore daemon: Restart daemon for unexpected behavior\n- encore daemon env: Output environment information\n\nDatabase:\n- encore db shell database-name [--env=name]: Connect via psql shell (--write, --admin, --superuser flags available)\n- encore db conn-uri database-name [--env=name]: Output connection string\n- encore db proxy [--env=name]: Set up local database connection proxy\n- encore db reset [service-names...]: Reset specified service databases\n\nCode generation:\n- encore gen client [app-id] [--env=name] [--lang=lang]: Generate API client (languages: go, typescript, javascript, openapi)\n\nLogging:\n- encore logs [--env=prod] [--json]: Stream application logs\n\nKubernetes:\n- encore k8s configure --env=ENV_NAME: Update kubectl config for environment\n\nSecrets:\n- encore secret set --type types secret-name: Set secret value (types: production, development, preview, local)\n- encore secret list [keys...]: List secrets\n- encore secret archive id: Archive secret value\n- encore secret unarchive id: Unarchive secret value\n\nVersion:\n- encore version: Report current version\n- encore version update: Check and apply updates\n\nVPN:\n- encore vpn start: Set up secure connection to private environments\n- encore vpn status: Check VPN connection status\n- encore vpn stop: Stop VPN connection\n\nBuild:\n- encore build docker: Build portable Docker image (--base string: Define base image, --push: Push to remote repository)\n</encore_cli_reference>\n"
  },
  {
    "path": "tsparser/Cargo.toml",
    "content": "[package]\nname = \"encore-tsparser\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[[bin]]\nname = \"tsparser-encore\"\nrequired-features = [\"native\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[features]\ndefault = [\"native\"]\nnative = [\n    \"dep:pg_query\",\n    \"dep:walkdir\",\n    \"dep:duct\",\n    \"dep:symlink\",\n    \"dep:junction\",\n    \"dep:env_logger\",\n    \"swc_ecma_loader/node\",\n    \"swc_ecma_loader/tsc\",\n    \"swc_common/tty-emitter\",\n]\nserde-meta = []\n\n[dependencies]\nswc_ecma_parser = { version = \"0.141.21\", features = [\"typescript\"] }\nswc_ecma_ast = \"0.110.9\"\nswc_ecma_visit = { version = \"0.96.9\", features = [\"path\"] }\nswc_ecma_transforms_base = \"0.134.30\"\nswc_ecma_loader = \"0.45.9\"\nswc_common = \"0.33.8\"\nwalkdir = { version = \"2\", optional = true }\nanyhow = { version = \"1.0.75\", features = [\"backtrace\"] }\nclean-path = \"0.2.1\"\nlog = \"0.4.20\"\nenv_logger = { version = \"0.10.0\", optional = true }\ntxtar = { version = \"1.0.0\", path = \"./txtar\" }\nlitparser = { version = \"0.1.0\", path = \"./litparser\" }\nlitparser-derive = { version = \"0.1.0\", path = \"./litparser-derive\" }\nprost = \"0.12.1\"\ntempdir = \"0.3.7\"\ncron-parser = \"0.8.0\"\nchrono = \"0.4.31\"\nregex = \"1.9.5\"\nonce_cell = \"1.18.0\"\nhandlebars = { version = \"4.4.0\", features = [\"no_logging\"] }\nserde = { version = \"1.0.188\", features = [\"rc\"] }\nserde_json = { version = \"1.0.107\", features = [\"preserve_order\"] }\nurl = \"2.4.1\"\nconvert_case = \"0.6.0\"\nitertools = \"0.13.0\"\nduct = { version = \"0.13.6\", optional = true }\nindexmap = { version = \"2.1.0\", features = [\"serde\"] }\nserde_yaml = \"0.9.32\"\nsymlink = { version = \"0.1.0\", optional = true }\ntracing = \"0.1.40\"\ntracing-subscriber = { version = \"0.3.18\", features = [\"env-filter\"] }\njunction = { version = \"1.2.0\", optional = true }\nthiserror = \"1.0.64\"\nmatchit = \"0.7.3\"\nsemver = \"1.0.24\"\npg_query = { version = \"6.1.1\", optional = true }\n\n[build-dependencies]\nprost-build = { version = \"0.12.1\" }\n\n[dev-dependencies]\nassert_fs = \"1.1.1\"\nassert_matches = \"1.5.0\"\ninsta = { version = \"1.38.0\", features = [\"yaml\", \"glob\"] }\nonce_cell = \"1.19.0\"\n"
  },
  {
    "path": "tsparser/build.rs",
    "content": "use std::io::Result;\n\nfn main() -> Result<()> {\n    let mut config = prost_build::Config::new();\n    if std::env::var(\"CARGO_FEATURE_SERDE_META\").is_ok() {\n        config.type_attribute(\".\", \"#[derive(serde::Serialize)]\");\n    }\n    config.compile_protos(\n        &[\"../proto/encore/parser/meta/v1/meta.proto\"],\n        &[\"../proto/\"],\n    )?;\n    Ok(())\n}\n"
  },
  {
    "path": "tsparser/examples/testparse.rs",
    "content": "use std::fmt;\nuse std::io::{self, Write};\nuse std::path::PathBuf;\nuse std::rc::Rc;\nuse std::sync::{Arc, Mutex};\n\nuse swc_common::errors::{Emitter, EmitterWriter, Handler, HANDLER};\nuse swc_common::{Globals, SourceMap, SourceMapper, GLOBALS};\n\nuse encore_tsparser::builder;\nuse encore_tsparser::builder::Builder;\nuse encore_tsparser::parser::parser::ParseContext;\n\nfn main() {\n    env_logger::init();\n\n    // Read the app root from the first arg.\n    let app_root = PathBuf::from(std::env::args().nth(1).expect(\"missing app root\"));\n\n    let globals = Globals::new();\n\n    let cm: Rc<SourceMap> = Default::default();\n    let errors: Rc<Mutex<Vec<String>>> = Default::default();\n    let emitter = ErrorList {\n        cm: cm.clone(),\n        errors: errors.clone(),\n    };\n\n    let errs = Rc::new(Handler::with_emitter(true, false, Box::new(emitter)));\n\n    GLOBALS.set(&globals, || {\n        HANDLER.set(&errs, || {\n            let builder = Builder::new().expect(\"unable to construct builder\");\n\n            {\n                let pp = builder::PrepareParams {\n                    app_root: app_root.clone(),\n                    encore_dev_version: builder::PackageVersion::Published(\"0.0.0\".to_string()),\n                };\n                builder.prepare(&pp).unwrap();\n            }\n\n            let pc = ParseContext::new(app_root.clone(), None, cm.clone(), errs.clone()).unwrap();\n\n            let app = builder::App {\n                root: app_root.clone(),\n                platform_id: None,\n                local_id: \"test\".to_string(),\n            };\n            let pp = builder::ParseParams {\n                app: &app,\n                pc: &pc,\n                working_dir: &app_root,\n                parse_tests: false,\n            };\n\n            match builder.parse(&pp) {\n                Some(_desc) => {\n                    println!(\"successfully parsed {}\", app_root.display());\n                }\n                None => {\n                    // Get any errors from the emitter.\n                    let errs = errors.lock().unwrap();\n                    let mut err_msg = String::new();\n                    for err in errs.iter() {\n                        err_msg.push_str(err);\n                        err_msg.push('\\n');\n                    }\n                    eprintln!(\"{err_msg}\");\n                    panic!(\"parse failure\")\n                }\n            }\n        })\n    })\n}\n\nstruct ErrorList {\n    cm: Rc<dyn SourceMapper>,\n    errors: Rc<Mutex<Vec<String>>>,\n}\n\nimpl Emitter for ErrorList {\n    fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) {\n        let buf: AtomicBuf = Default::default();\n\n        let mut w = EmitterWriter::new(Box::new(buf.clone()), Some(self.cm.clone()), false, false);\n        w.emit(db);\n\n        let s = buf.to_string();\n        self.errors.lock().unwrap().push(s);\n    }\n}\n\n#[derive(Default, Clone)]\nstruct AtomicBuf(Arc<Mutex<Vec<u8>>>);\n\nimpl fmt::Display for AtomicBuf {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", String::from_utf8_lossy(&self.0.lock().unwrap()))\n    }\n}\n\nimpl Write for AtomicBuf {\n    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {\n        self.0.lock().unwrap().extend_from_slice(buf);\n        Ok(buf.len())\n    }\n\n    fn flush(&mut self) -> io::Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "tsparser/litparser/.gitignore",
    "content": "/target\n"
  },
  {
    "path": "tsparser/litparser/Cargo.toml",
    "content": "[package]\nname = \"litparser\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nanyhow = \"1.0\"\nduration-string = \"0.3.0\"\nswc_ecma_ast = \"0.110.9\"\nswc_common = { version = \"0.33.8\", features = [\"tty-emitter\"] }\nclean-path = \"0.2.1\"\nnum-bigint = \"0.4.5\"\nnum-integer = \"0.1.46\"\nnum-traits = \"0.2.19\"\n"
  },
  {
    "path": "tsparser/litparser/src/lib.rs",
    "content": "use duration_string::DurationString;\nuse num_bigint::{BigInt, ToBigInt};\nuse std::{\n    error::Error,\n    fmt::{Debug, Display},\n    ops::{Deref, DerefMut},\n    path::{Component, PathBuf},\n};\nuse swc_common::{errors::HANDLER, pass::Either, util::take::Take, Span, Spanned};\nuse swc_ecma_ast as ast;\n\n#[derive(Debug, Clone, Hash)]\npub struct ParseError {\n    pub span: Span,\n    pub message: String,\n}\n\nimpl Error for ParseError {}\n\nimpl Display for ParseError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.message)\n    }\n}\n\nimpl ParseError {\n    pub fn report(self) {\n        HANDLER.with(|handler| {\n            handler.span_err(self.span, &self.message);\n        });\n    }\n}\n\n#[macro_export]\nmacro_rules! report_and_continue {\n    ($e:expr) => {\n        match $e {\n            Ok(v) => v,\n            Err(err) => {\n                err.report();\n                continue;\n            }\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! report_and_return {\n    ($e:expr) => {\n        match $e {\n            Ok(v) => v,\n            Err(err) => {\n                err.report();\n                return;\n            }\n        }\n    };\n}\n\npub trait ToParseErr {\n    fn parse_err<S: Into<String>>(&self, message: S) -> ParseError;\n}\n\nimpl<T> ToParseErr for T\nwhere\n    T: Spanned,\n{\n    fn parse_err<S: Into<String>>(&self, message: S) -> ParseError {\n        ParseError {\n            span: self.span(),\n            message: message.into(),\n        }\n    }\n}\n\npub type ParseResult<T> = Result<T, ParseError>;\n\npub trait LitParser: Sized {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self>;\n}\n\nimpl<T> LitParser for Sp<T>\nwhere\n    T: LitParser,\n{\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        let res = T::parse_lit(input)?;\n        Ok(Sp(input.span(), res))\n    }\n}\n\nimpl LitParser for String {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Lit(ast::Lit::Str(str)) => Ok(str.value.to_string()),\n            _ => Err(input.parse_err(\"expected string literal\")),\n        }\n    }\n}\n\nimpl LitParser for bool {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Lit(ast::Lit::Bool(b)) => Ok(b.value),\n            _ => Err(input.parse_err(\"expected boolean literal\")),\n        }\n    }\n}\n\nimpl LitParser for i32 {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        let big = parse_const_bigint(input)?;\n        big.try_into()\n            .map_err(|_| input.parse_err(\"expected number literal\"))\n    }\n}\n\nimpl LitParser for u32 {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        let big = parse_const_bigint(input)?;\n        big.try_into()\n            .map_err(|_| input.parse_err(\"expected unsigned number literal\"))\n    }\n}\n\nimpl LitParser for i64 {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        let big = parse_const_bigint(input)?;\n        big.try_into()\n            .map_err(|_| input.parse_err(\"expected number literal\"))\n    }\n}\n\nimpl LitParser for u64 {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        let big = parse_const_bigint(input)?;\n        big.try_into()\n            .map_err(|_| input.parse_err(\"expected unsigned number literal\"))\n    }\n}\n\nimpl LitParser for ast::Expr {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        Ok(input.clone())\n    }\n}\n\nimpl<T> LitParser for Option<T>\nwhere\n    T: LitParser,\n{\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Option<T>> {\n        let t = T::parse_lit(input)?;\n        Ok(Some(t))\n    }\n}\n\nimpl<L, R> LitParser for Either<L, R>\nwhere\n    L: LitParser,\n    R: LitParser,\n{\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Either<L, R>> {\n        let res = L::parse_lit(input)\n            .map(Either::Left)\n            .or_else(|_| R::parse_lit(input).map(Either::Right))?;\n\n        Ok(res)\n    }\n}\n\nimpl LitParser for std::time::Duration {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Lit(ast::Lit::Str(str)) => {\n                let dur = DurationString::try_from(str.value.to_string())\n                    .map_err(|e| str.parse_err(e))?;\n                Ok(dur.into())\n            }\n            _ => Err(input.parse_err(\"expected duration string literal\")),\n        }\n    }\n}\n\nimpl<T> LitParser for Vec<T>\nwhere\n    T: LitParser,\n{\n    fn parse_lit(input: &swc_ecma_ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Array(array) => {\n                let mut vec = Vec::new();\n                for elem in &array.elems {\n                    if let Some(expr) = elem {\n                        let parsed_elem = T::parse_lit(&expr.expr)?;\n                        vec.push(parsed_elem);\n                    } else {\n                        return Err(array.span.parse_err(\"expected array element\"));\n                    }\n                }\n                Ok(vec)\n            }\n            _ => Err(input.parse_err(\"expected array literal\")),\n        }\n    }\n}\n\n/// Represents a local, relative path (without \"..\" or a root).\n#[derive(Debug, Clone)]\npub struct LocalRelPath {\n    pub span: Span,\n    pub buf: PathBuf,\n}\n\nimpl Spanned for LocalRelPath {\n    fn span(&self) -> Span {\n        self.span\n    }\n}\n\nimpl LocalRelPath {\n    pub fn try_from<S: AsRef<str>>(sp: Span, str: S) -> ParseResult<Self> {\n        let str = str.as_ref();\n        let path = PathBuf::from(str);\n        for c in path.components() {\n            match c {\n                Component::CurDir => {}\n                Component::Normal(_) => {}\n                _ => return Err(sp.parse_err(\"expected a local relative path\")),\n            }\n        }\n        Ok(LocalRelPath {\n            span: sp,\n            buf: clean_path::clean(path),\n        })\n    }\n}\n\nimpl LitParser for LocalRelPath {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Lit(ast::Lit::Str(str)) => LocalRelPath::try_from(str.span, &str.value),\n            _ => Err(input.parse_err(\"expected a local relative path\")),\n        }\n    }\n}\n\n#[derive(Debug)]\npub enum Nullable<T> {\n    Present(T),\n    Null,\n}\n\nimpl<T> LitParser for Nullable<T>\nwhere\n    T: LitParser,\n{\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Lit(ast::Lit::Null(_)) => Ok(Nullable::Null),\n            _ => {\n                let t = T::parse_lit(input)?;\n                Ok(Nullable::Present(t))\n            }\n        }\n    }\n}\n\nimpl<T> Clone for Nullable<T>\nwhere\n    T: Clone,\n{\n    fn clone(&self) -> Self {\n        match self {\n            Nullable::Present(t) => Nullable::Present(t.clone()),\n            Nullable::Null => Nullable::Null,\n        }\n    }\n}\n\nfn parse_const_bigint(expr: &ast::Expr) -> ParseResult<BigInt> {\n    match expr {\n        ast::Expr::Lit(ast::Lit::Num(num)) => {\n            let int = num.value as i64;\n            if int as f64 != num.value {\n                return Err(num.parse_err(\"expected integer literal\"));\n            }\n            let Some(big) = int.to_bigint() else {\n                return Err(num.parse_err(\"integer too large\"));\n            };\n            Ok(big)\n        }\n        ast::Expr::Unary(unary) => match unary.op {\n            ast::UnaryOp::Minus => {\n                let x = parse_const_bigint(&unary.arg)?;\n                Ok(-x)\n            }\n            ast::UnaryOp::Plus => parse_const_bigint(&unary.arg),\n            _ => Err(unary.parse_err(format!(\"unsupported unary operator {:?}\", unary.op))),\n        },\n        ast::Expr::Bin(bin) => {\n            let x = parse_const_bigint(&bin.left)?;\n            let y = parse_const_bigint(&bin.right)?;\n            match bin.op {\n                ast::BinaryOp::Add => Ok(x + y),\n                ast::BinaryOp::Sub => Ok(x - y),\n                ast::BinaryOp::Mul => Ok(x * y),\n                ast::BinaryOp::Mod => Ok(x % y),\n                ast::BinaryOp::Div => {\n                    // Does it divide evenly?\n                    use num_integer::Integer;\n                    use num_traits::Zero;\n                    let (quo, remainder) = x.div_rem(&y);\n                    if remainder.is_zero() {\n                        Ok(quo)\n                    } else {\n                        Err(bin.parse_err(\"expected integer division\"))\n                    }\n                }\n                _ => Err(bin.parse_err(format!(\"expected arithmetic operator, got {:?}\", bin.op))),\n            }\n        }\n        _ => Err(expr.parse_err(\"expected integer literal\")),\n    }\n}\n\npub struct Sp<T>(Span, T);\n\nimpl<T> Clone for Sp<T>\nwhere\n    T: Clone,\n{\n    fn clone(&self) -> Self {\n        Sp(self.0, self.1.clone())\n    }\n}\n\nimpl<T> Sp<T> {\n    pub fn new(sp: Span, val: T) -> Self {\n        Self(sp, val)\n    }\n\n    pub fn with_dummy(val: T) -> Self {\n        Self::new(Span::dummy(), val)\n    }\n\n    pub fn with<U>(&self, val: U) -> Sp<U> {\n        Sp::new(self.0, val)\n    }\n\n    pub fn split(self) -> (Span, T) {\n        (self.0, self.1)\n    }\n\n    pub fn span(&self) -> Span {\n        self.0\n    }\n\n    pub fn take(self) -> T {\n        self.1\n    }\n\n    pub fn map<F, U>(self, f: F) -> Sp<U>\n    where\n        F: FnOnce(T) -> U,\n    {\n        Sp(self.0, f(self.1))\n    }\n\n    pub fn get(&self) -> &T {\n        &self.1\n    }\n\n    pub fn as_deref(&self) -> &T::Target\n    where\n        T: Deref,\n    {\n        self.1.deref()\n    }\n}\n\nimpl<T, E> Sp<Result<T, E>> {\n    pub fn transpose(self) -> Result<Sp<T>, E> {\n        match self.1 {\n            Ok(inner) => Ok(Sp(self.0, inner)),\n            Err(err) => Err(err),\n        }\n    }\n}\n\nimpl<T> AsRef<T> for Sp<T> {\n    fn as_ref(&self) -> &T {\n        &self.1\n    }\n}\n\nimpl<T> AsMut<T> for Sp<T> {\n    fn as_mut(&mut self) -> &mut T {\n        &mut self.1\n    }\n}\n\nimpl<T> Deref for Sp<T> {\n    type Target = T;\n\n    fn deref(&self) -> &Self::Target {\n        &self.1\n    }\n}\n\nimpl<T> DerefMut for Sp<T> {\n    fn deref_mut(&mut self) -> &mut Self::Target {\n        &mut self.1\n    }\n}\n\nimpl<T> PartialEq for Sp<T>\nwhere\n    T: PartialEq,\n{\n    fn eq(&self, other: &Self) -> bool {\n        self.1 == other.1\n    }\n}\n\nimpl<T> Eq for Sp<T> where T: Eq {}\n\nimpl<T> PartialOrd for Sp<T>\nwhere\n    T: PartialOrd,\n{\n    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {\n        self.1.partial_cmp(&other.1)\n    }\n}\n\nimpl<T> Ord for Sp<T>\nwhere\n    T: Ord,\n{\n    fn cmp(&self, other: &Self) -> std::cmp::Ordering {\n        self.1.cmp(&other.1)\n    }\n}\n\nimpl<T> Copy for Sp<T> where T: Copy {}\n\nimpl<T> Debug for Sp<T>\nwhere\n    T: Debug,\n{\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.1.fmt(f)\n    }\n}\n\nimpl<T> Display for Sp<T>\nwhere\n    T: Display,\n{\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        self.1.fmt(f)\n    }\n}\n\nimpl<T> From<T> for Sp<T>\nwhere\n    T: Spanned,\n{\n    fn from(value: T) -> Self {\n        Self(value.span(), value)\n    }\n}\n\nimpl<T> Spanned for Sp<T> {\n    fn span(&self) -> Span {\n        self.0\n    }\n}\n"
  },
  {
    "path": "tsparser/litparser-derive/.gitignore",
    "content": "/target\n"
  },
  {
    "path": "tsparser/litparser-derive/Cargo.toml",
    "content": "[package]\nname = \"litparser-derive\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nproc-macro = true\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nsyn = { version = \"2.0\", features = [\"extra-traits\"] }\nquote = \"1.0\"\nproc-macro2 = \"1.0.67\"\nanyhow = \"1.0\"\nlitparser = { version = \"0.1.0\", path = \"../litparser\" }\nswc_ecma_parser = { version = \"0.141.21\", features = [\"typescript\"] }\nswc_ecma_ast = \"0.110.9\"\nswc_common = { version = \"0.33.8\", features = [\"tty-emitter\"] }\n\n[dev-dependencies]\nprettyplease = \"0.2\"\n"
  },
  {
    "path": "tsparser/litparser-derive/src/lib.rs",
    "content": "extern crate proc_macro;\n\nuse quote::{format_ident, quote, quote_spanned};\nuse syn::spanned::Spanned;\nuse syn::{parse_macro_input, parse_quote, Data, DeriveInput, Fields, GenericParam, Generics};\n\n#[proc_macro_derive(LitParser)]\npub fn derive_lit_parser(input: proc_macro::TokenStream) -> proc_macro::TokenStream {\n    // Parse the input tokens into a syntax tree\n    let input = parse_macro_input!(input as DeriveInput);\n\n    let name = input.ident;\n\n    // Add a bound `T: LitParser` to every type parameter T.\n    let generics = add_trait_bounds(input.generics);\n    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();\n\n    let input_ident = format_ident!(\"input\");\n    let impl_stream = generate_impl(&input.data, &input_ident);\n\n    // Build the output, possibly using quasi-quotation\n    let expanded = quote! {\n        // The generated impl.\n        #[allow(non_snake_case)]\n        impl #impl_generics litparser::LitParser for #name #ty_generics #where_clause {\n            fn parse_lit(#input_ident: &swc_ecma_ast::Expr) -> litparser::ParseResult<Self> {\n                #impl_stream\n            }\n        }\n    };\n\n    // Hand the output tokens back to the compiler.\n    proc_macro::TokenStream::from(expanded)\n}\n\n// Add a bound `T: LitParser` to every type parameter T.\nfn add_trait_bounds(mut generics: Generics) -> Generics {\n    for param in &mut generics.params {\n        if let GenericParam::Type(ref mut type_param) = *param {\n            type_param\n                .bounds\n                .push(parse_quote!(tsparser::litparser::LitParser));\n        }\n    }\n    generics\n}\n\n// Generate an implementation to parse literals for a type.\nfn generate_impl(data: &Data, input_ident: &syn::Ident) -> proc_macro2::TokenStream {\n    let init_stream = fields_init(data);\n    let match_stream = match_expr(data, input_ident);\n    let return_stream = gen_return(data, input_ident);\n    match *data {\n        Data::Struct(ref data) => match data.fields {\n            Fields::Named(_) => {\n                quote! {\n                    #init_stream\n                    #match_stream\n                    #return_stream\n                }\n            }\n            Fields::Unnamed(_) => {\n                unimplemented!()\n            }\n            Fields::Unit => {\n                unimplemented!()\n            }\n        },\n        Data::Enum(_) | Data::Union(_) => unimplemented!(),\n    }\n}\n\n/// Turns a struct into a list of field initialization statements in the form:\n///    let field_name: ::std::option::Option<field_type> = None;\nfn fields_init(data: &Data) -> proc_macro2::TokenStream {\n    match *data {\n        Data::Struct(ref data) => match data.fields {\n            Fields::Named(ref fields) => {\n                let inits = fields.named.iter().map(|f| {\n                    let name = &f.ident;\n                    let typ = &f.ty;\n                    quote_spanned! {f.span() =>\n                        let mut #name: ::std::option::Option<#typ> = None;\n                    }\n                });\n                quote! { #(#inits)* }\n            }\n            Fields::Unnamed(_) => {\n                todo!()\n            }\n            Fields::Unit => {\n                todo!()\n            }\n        },\n        Data::Enum(_) | Data::Union(_) => unimplemented!(),\n    }\n}\n\nfn match_expr(data: &Data, input_ident: &syn::Ident) -> proc_macro2::TokenStream {\n    let lit_ident = format_ident!(\"lit\");\n    let prop_ident = format_ident!(\"prop\");\n    let kv_ident = format_ident!(\"kv\");\n    let field_case_stream = gen_field_match_cases(data, &prop_ident, &kv_ident);\n    let match_prop_stream = match_prop(&prop_ident, &kv_ident, field_case_stream);\n    quote! {\n        match #input_ident {\n            swc_ecma_ast::Expr::Object(ref #lit_ident) => {\n                for #prop_ident in &#lit_ident.props {\n                    #match_prop_stream\n                }\n            }\n            _ => return Err(litparser::ParseError {\n                span: <swc_ecma_ast::Expr as swc_common::Spanned>::span(#input_ident),\n                message: \"expected object literal\".to_string(),\n            }),\n        }\n    }\n}\n\n// Generates an expression for matching a prop with the given prop expression.\nfn match_prop(\n    prop_ident: &syn::Ident,\n    kv_ident: &syn::Ident,\n    match_case_stream: proc_macro2::TokenStream,\n) -> proc_macro2::TokenStream {\n    quote! {\n        match #prop_ident {\n            swc_ecma_ast::PropOrSpread::Spread(_) => {\n                return Err(litparser::ParseError {\n                    span: <swc_ecma_ast::PropOrSpread as swc_common::Spanned>::span(#prop_ident),\n                    message: \"spread operator unsupported\".to_string(),\n                });\n            }\n            swc_ecma_ast::PropOrSpread::Prop(prop) => match prop.as_ref() {\n                swc_ecma_ast::Prop::Shorthand(_)\n                | swc_ecma_ast::Prop::Assign(_)\n                | swc_ecma_ast::Prop::Getter(_)\n                | swc_ecma_ast::Prop::Setter(_)\n                | swc_ecma_ast::Prop::Method(_) => {\n                    return Err(litparser::ParseError {\n                        span: <swc_ecma_ast::Prop as swc_common::Spanned>::span(prop),\n                        message: format!(\"prop type {:?} not supported\", prop),\n                    });\n                }\n\n                swc_ecma_ast::Prop::KeyValue(#kv_ident) => match &#kv_ident.key {\n                    swc_ecma_ast::PropName::Ident(ident) => match ident.sym.as_ref() {\n                        #match_case_stream\n                    },\n                    swc_ecma_ast::PropName::Str(str) => match str.value.as_ref() {\n                        #match_case_stream\n                    }\n                    swc_ecma_ast::PropName::Num(_)\n                    | swc_ecma_ast::PropName::BigInt(_)\n                    | swc_ecma_ast::PropName::Computed(_) => {\n                        return Err(litparser::ParseError {\n                            span: <swc_ecma_ast::KeyValueProp as swc_common::Spanned>::span(#kv_ident),\n                            message: format!(\"prop name kind {:?} not supported\", kv.key),\n                        });\n                    }\n                },\n            }\n        }\n    }\n}\n\n// Generates an expression for the match cases for the fields.\nfn gen_field_match_cases(\n    data: &Data,\n    prop_ident: &syn::Ident,\n    kv_ident: &syn::Ident,\n) -> proc_macro2::TokenStream {\n    match *data {\n        Data::Struct(ref data) => match data.fields {\n            Fields::Named(ref fields) => {\n                let match_cases = fields.named.iter().map(|f| {\n                    let name = f.ident.as_ref().unwrap();\n                    let match_literal = format!(\"{name}\");\n                    quote_spanned! {f.span() =>\n                        #match_literal => {\n                            if #name.is_some() {\n                                return Err(litparser::ParseError {\n                                    span: <swc_ecma_ast::Prop as swc_common::Spanned>::span(#prop_ident),\n                                    message: format!(\"field {} set twice\", #match_literal),\n                                });\n                            }\n                            let val = LitParser::parse_lit(&*#kv_ident.value)?;\n                            #name = Some(val);\n                        }\n                    }\n                });\n                quote! {\n                    #(#match_cases)*\n                    x @ _ => {\n                        return Err(litparser::ParseError {\n                            span: <swc_ecma_ast::Prop as swc_common::Spanned>::span(#prop_ident),\n                            message: \"unexpected field\".to_string(),\n                        });\n                    }\n                }\n            }\n            Fields::Unnamed(_) => {\n                todo!()\n            }\n            Fields::Unit => {\n                todo!()\n            }\n        },\n        Data::Enum(_) | Data::Union(_) => unimplemented!(),\n    }\n}\n\nfn gen_return(data: &Data, input_ident: &syn::Ident) -> proc_macro2::TokenStream {\n    match *data {\n        Data::Struct(ref data) => match data.fields {\n            Fields::Named(ref fields) => {\n                let field_names = fields.named.iter().map(|f| {\n                    let name = &f.ident;\n\n                    if is_optional(&f.ty) {\n                        quote_spanned! {f.span() =>\n                            #name: #name.flatten()\n                        }\n                    } else {\n                        quote_spanned! {f.span() =>\n                            #name: #name.ok_or_else(|| litparser::ParseError {\n                                span: <swc_ecma_ast::Expr as swc_common::Spanned>::span(#input_ident),\n                                message: format!(\"field {} is required but is missing\", stringify!(#name)),\n                            })?\n                        }\n                    }\n                });\n                quote! {\n                    Ok(Self {\n                        #(#field_names),*\n                    })\n                }\n            }\n            Fields::Unnamed(_) => {\n                todo!()\n            }\n            Fields::Unit => {\n                todo!()\n            }\n        },\n        Data::Enum(_) | Data::Union(_) => unimplemented!(),\n    }\n}\n\nfn is_optional(ty: &syn::Type) -> bool {\n    match ty {\n        syn::Type::Path(syn::TypePath {\n            qself: None,\n            path: syn::Path { segments, .. },\n        }) => {\n            // Return true if the last path segment is \"Option\".\n            segments.last().is_some_and(|seg| seg.ident == \"Option\")\n        }\n        _ => false,\n    }\n}\n"
  },
  {
    "path": "tsparser/litparser-derive/tests/integration_tests.rs",
    "content": "#![allow(clippy::disallowed_names)]\nuse std::option;\n\nuse litparser_derive::LitParser;\nuse swc_common::errors::{ColorConfig, Handler};\nuse swc_common::input::StringInput;\nuse swc_common::sync::Lrc;\nuse swc_common::{FileName, SourceMap};\nuse swc_ecma_parser::lexer::Lexer;\nuse swc_ecma_parser::{Parser, Syntax};\n\nuse litparser::{LitParser, ParseResult};\n\n#[test]\nfn test_parse() {\n    #[derive(LitParser)]\n    struct Foo {\n        foo: String,\n        bar: std::time::Duration,\n    }\n\n    let expr = parse(r#\"{ foo: \"foo\", bar: \"1h\" }\"#);\n    let foo = Foo::parse_lit(&expr).expect(\"failed to parse lit\");\n    assert_eq!(foo.foo, \"foo\");\n    assert_eq!(foo.bar, std::time::Duration::from_secs(3600));\n}\n\n#[test]\nfn test_parse_str_keys() {\n    #[derive(LitParser)]\n    struct Foo {\n        foo: String,\n        bar: i32,\n    }\n\n    let expr = parse(r#\"{ \"foo\": \"foo\", \"bar\": 3 }\"#);\n    let foo = Foo::parse_lit(&expr).expect(\"failed to parse lit\");\n    assert_eq!(foo.foo, \"foo\");\n    assert_eq!(foo.bar, 3);\n}\n\n#[test]\n#[allow(dead_code)]\nfn test_parse_refs() {\n    struct Dummy<'a> {\n        foo: Option<&'a str>,\n    }\n    impl LitParser for Dummy<'_> {\n        fn parse_lit(_input: &swc_ecma_ast::Expr) -> ParseResult<Self> {\n            Ok(Self { foo: None })\n        }\n    }\n\n    #[derive(LitParser)]\n    struct Foo<'a> {\n        foo: String,\n        dummy: Dummy<'a>,\n    }\n\n    let expr = parse(r#\"{ foo: \"foo\", \"dummy\": null }\"#);\n    let foo = Foo::parse_lit(&expr).expect(\"failed to parse lit\");\n    assert_eq!(foo.foo, \"foo\");\n}\n\n#[test]\nfn test_parse_option() {\n    // let foo: Option<Option<T>> = None;\n\n    #[derive(LitParser)]\n    struct Foo {\n        // Try with different ways of writing Option, since we parse the syntax tree.\n        foo: Option<String>,\n        bar: option::Option<String>,\n        baz: std::option::Option<String>,\n        boo: ::std::option::Option<String>,\n    }\n\n    // The empty case\n    {\n        let expr = parse(r#\"{ }\"#);\n        let foo = Foo::parse_lit(&expr).expect(\"failed to parse lit\");\n        assert_eq!(foo.foo, None);\n        assert_eq!(foo.bar, None);\n        assert_eq!(foo.baz, None);\n        assert_eq!(foo.boo, None);\n    }\n\n    // The non-empty case\n    {\n        let expr = parse(r#\"{ foo: \"foo\", bar: \"bar\", baz: \"baz\", boo: \"boo\" }\"#);\n        let foo = Foo::parse_lit(&expr).expect(\"failed to parse lit\");\n        assert_eq!(foo.foo, Some(\"foo\".to_string()));\n        assert_eq!(foo.bar, Some(\"bar\".to_string()));\n        assert_eq!(foo.baz, Some(\"baz\".to_string()));\n        assert_eq!(foo.boo, Some(\"boo\".to_string()));\n    }\n}\n\nfn parse(src: &str) -> Box<swc_ecma_ast::Expr> {\n    let cm: Lrc<SourceMap> = Default::default();\n    let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone()));\n\n    let fm = cm.new_source_file(FileName::Custom(\"test.ts\".into()), src.into());\n    let lexer = Lexer::new(\n        Syntax::Es(Default::default()),\n        Default::default(),\n        StringInput::from(&*fm),\n        None,\n    );\n\n    let mut parser = Parser::new_from(lexer);\n\n    for e in parser.take_errors() {\n        e.into_diagnostic(&handler).emit();\n    }\n\n    parser.parse_expr().expect(\"failed to parse expr\")\n}\n"
  },
  {
    "path": "tsparser/src/app/mod.rs",
    "content": "use std::collections::{HashMap, HashSet};\n\nuse itertools::Itertools;\nuse matchit::InsertError;\nuse swc_common::errors::HANDLER;\nuse swc_common::Span;\n\nuse crate::encore::parser::meta::v1;\nuse crate::legacymeta::compute_meta;\nuse crate::parser::parser::{ParseContext, ParseResult};\nuse crate::parser::resources::apis::api::{Endpoint, Method, Methods};\nuse crate::parser::resources::apis::encoding::{Param, ParamData};\nuse crate::parser::resources::Resource;\nuse crate::parser::respath::Path;\nuse crate::parser::types::visitor::VisitWith;\nuse crate::parser::types::{\n    validation, visitor, Basic, Custom, Interface, ObjectId, ResolveState, Type, Validated,\n    WireLocation, WireSpec,\n};\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\nuse litparser::Sp;\n\n#[derive(Debug)]\npub struct AppDesc {\n    pub parse: ParseResult,\n    pub meta: v1::Data,\n}\n\nstruct Router {\n    methods: HashMap<Method, matchit::Router<Range>>,\n}\n\nimpl Router {\n    fn new() -> Self {\n        Router {\n            methods: HashMap::new(),\n        }\n    }\n}\n\nimpl Router {\n    fn try_add(&mut self, methods: &Methods, path: &Path, range: Range) {\n        let methods = match methods {\n            Methods::All => Method::all().to_vec(),\n            Methods::Some(vec) => vec.to_vec(),\n        };\n\n        for method in methods {\n            let method_router = self.methods.entry(method).or_default();\n            if let Err(e) = method_router.insert(path.to_string(), range) {\n                match e {\n                    InsertError::Conflict { with } => {\n                        let prev_range = *method_router.at(&with).unwrap().value;\n                        HANDLER.with(|handler| {\n                            handler\n                                .struct_span_err(\n                                    range,\n                                    \"api endpoints with conflicting paths defined\",\n                                )\n                                .span_note(prev_range, \"previously defined here\")\n                                .emit()\n                        })\n                    }\n                    _ => HANDLER.with(|handler| handler.span_err(range, &e.to_string())),\n                }\n            }\n        }\n    }\n}\n\npub fn validate_and_describe(pc: &ParseContext, parse: ParseResult) -> Option<AppDesc> {\n    AppValidator { pc, parse: &parse }.validate();\n\n    if pc.errs.has_errors() {\n        return None;\n    }\n\n    match compute_meta(pc, &parse) {\n        Ok(meta) => Some(AppDesc { parse, meta }),\n        Err(err) => {\n            err.report();\n            None\n        }\n    }\n}\n\nstruct AppValidator<'a> {\n    pc: &'a ParseContext,\n    parse: &'a ParseResult,\n}\n\nimpl AppValidator<'_> {\n    fn validate(&self) {\n        self.validate_apis();\n        self.validate_pubsub();\n        self.validate_sqldb();\n        self.validate_metrics();\n        self.validate_crons();\n        self.validate_buckets();\n        self.validate_auth_handlers();\n        self.validate_caches();\n    }\n\n    fn validate_apis(&self) {\n        let mut seen = HashMap::new();\n        for resource in &self.parse.resources {\n            if let Resource::APIEndpoint(ep) = resource {\n                let key = (ep.service_name.clone(), ep.name.clone());\n                if let Some(prev) = seen.insert(key, ep.name_range) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(\n                                ep.name_range,\n                                \"api endpoints with conflicting names defined within the same service\",\n                            )\n                            .span_note(prev, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n\n        for service in self.parse.services.iter() {\n            let mut router = Router::new();\n            for bind in &service.binds {\n                if let Resource::APIEndpoint(endpoint) = &bind.resource {\n                    let encoding = &endpoint.encoding;\n                    router.try_add(&encoding.methods, &encoding.path, endpoint.range);\n\n                    self.validate_endpoint(endpoint);\n                }\n            }\n        }\n    }\n\n    fn validate_endpoint(&self, ep: &Endpoint) {\n        if let Some(req_enc) = &ep.encoding.handshake {\n            self.validate_req_params(&req_enc.params);\n        }\n        if !ep.streaming_request {\n            for req_enc in &ep.encoding.req {\n                self.validate_req_params(&req_enc.params);\n            }\n        }\n        if !ep.streaming_response {\n            self.validate_resp_params(&ep.encoding.resp.params);\n        }\n        if let Some(schema) = &ep.encoding.raw_handshake_schema {\n            self.validate_schema_type(schema);\n            self.validate_validations(schema);\n        }\n        if let Some(schema) = &ep.encoding.raw_req_schema {\n            self.validate_schema_type(schema);\n            self.validate_validations(schema);\n        }\n        if let Some(schema) = &ep.encoding.raw_resp_schema {\n            self.validate_schema_type(schema);\n            self.validate_validations(schema);\n        }\n    }\n\n    fn validate_req_params(&self, params: &Vec<Param>) {\n        for param in params {\n            if let ParamData::Query { .. } = param.loc {\n                fn is_valid_query_type(state: &ResolveState, typ: &Type) -> bool {\n                    match resolve_to_concrete(state, typ) {\n                        Type::Basic(_) | Type::Literal(_) => true,\n                        Type::Enum(_) => true,\n                        Type::Array(ref t) => is_valid_query_type(state, &t.0),\n                        Type::Union(ref u) => u.types.iter().all(|t| is_valid_query_type(state, t)),\n                        Type::Custom(Custom::Decimal) => true,\n                        Type::Custom(Custom::WireSpec(WireSpec {\n                            location: WireLocation::Query,\n                            underlying: typ,\n                            ..\n                        })) => is_valid_query_type(state, &typ),\n                        _ => false,\n                    }\n                }\n\n                if !is_valid_query_type(self.pc.type_checker.state(), &param.typ) {\n                    HANDLER.with(|handler| {\n                        handler.span_err(param.range, \"type not supported for query parameters\")\n                    });\n                }\n            };\n        }\n    }\n\n    fn validate_resp_params(&self, params: &[Param]) {\n        let http_status_params: Vec<_> = params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::HTTPStatus))\n            .sorted_by(|a, b| a.range.cmp(&b.range))\n            .collect();\n\n        if http_status_params.len() > 1 {\n            let first = http_status_params[0];\n            HANDLER.with(|handler| {\n                let mut err = handler.struct_span_err(\n                    first.range,\n                    \"http status can only be defined once per response type\",\n                );\n\n                for param in &http_status_params[1..] {\n                    err.span_note(param.range, \"also defined here\");\n                }\n\n                err.emit();\n            });\n        }\n    }\n\n    fn validate_schema_type(&self, schema: &Sp<Type>) {\n        let state = self.pc.type_checker.state();\n        let concrete = resolve_to_concrete(state, schema.get());\n\n        let error_msg = match concrete {\n            Type::Interface(Interface { index: Some(_), .. }) => {\n                Some(\"type index is not supported in schema types\")\n            }\n            Type::Interface(Interface { call: Some(_), .. }) => {\n                Some(\"call signatures are not supported in schema types\")\n            }\n            Type::Interface(_) | Type::Basic(Basic::Void) => None,\n            _ => Some(\"request and response types must be interfaces or void\"),\n        };\n\n        if let Some(msg) = error_msg {\n            HANDLER.with(|handler| handler.span_err(schema.span(), msg));\n        }\n    }\n\n    fn validate_validations(&self, schema: &Sp<Type>) {\n        struct Visitor<'a> {\n            state: &'a ResolveState,\n            span: Span,\n            seen_decls: HashSet<ObjectId>,\n        }\n\n        impl visitor::Visit for Visitor<'_> {\n            fn resolve_state(&self) -> &ResolveState {\n                self.state\n            }\n            fn seen_decls(&mut self) -> &mut HashSet<ObjectId> {\n                &mut self.seen_decls\n            }\n\n            fn visit_validated(&mut self, node: &Validated) {\n                if let Err(err) = node.expr.supports_type(&node.typ) {\n                    let s = err.to_string();\n                    self.span.err(&s);\n                } else {\n                    // Don't recurse into the validation expression, as it would report an error\n                    // below as if the expression was standalone.\n                    node.typ.visit_with(self);\n                }\n            }\n\n            fn visit_validation(&mut self, node: &validation::Expr) {\n                HANDLER.with(|h| {\n                    h.struct_span_err(\n                        self.span,\n                        &format!(\"unsupported standalone validation expression: {node}\"),\n                    )\n                    .note(\"validation expressions must be attached to a regular type using '&'\")\n                    .emit()\n                });\n            }\n        }\n\n        let state = self.pc.type_checker.state();\n        let mut visitor = Visitor {\n            state,\n            span: schema.span(),\n            seen_decls: HashSet::new(),\n        };\n        schema.visit_with(&mut visitor);\n    }\n\n    fn validate_pubsub(&self) {\n        let mut seen_topics = HashMap::new();\n        for res in self.parse.resources.iter() {\n            if let Resource::PubSubTopic(topic) = &res {\n                self.validate_validations(&topic.message_type);\n\n                if let Some(prev_span) = seen_topics.insert(topic.name.clone(), topic.span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(\n                                topic.span,\n                                \"PubSub topic with this name already defined\",\n                            )\n                            .span_note(prev_span, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n\n        // Validate subscription name uniqueness per topic.\n        let mut seen_subs: HashMap<*const (), HashMap<String, Span>> = HashMap::new();\n        for res in self.parse.resources.iter() {\n            if let Resource::PubSubSubscription(sub) = &res {\n                let topic_ptr = std::rc::Rc::as_ptr(sub.topic.get()) as *const ();\n                let topic_subs = seen_subs.entry(topic_ptr).or_default();\n                let sub_span = sub.range.to_span();\n                if let Some(prev_span) = topic_subs.insert(sub.name.clone(), sub_span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(\n                                sub_span,\n                                \"PubSub subscription with this name already defined on this topic\",\n                            )\n                            .span_note(prev_span, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n    }\n\n    fn validate_crons(&self) {\n        let mut seen = HashMap::new();\n        for resource in &self.parse.resources {\n            if let Resource::CronJob(cron) = resource {\n                if let Some(prev_span) = seen.insert(cron.name.clone(), cron.span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(cron.span, \"cron job with this name already defined\")\n                            .span_note(prev_span, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n    }\n\n    fn validate_buckets(&self) {\n        let mut seen = HashMap::new();\n        for resource in &self.parse.resources {\n            if let Resource::Bucket(bucket) = resource {\n                if let Some(prev_span) = seen.insert(bucket.name.clone(), bucket.span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(bucket.span, \"bucket with this name already defined\")\n                            .span_note(prev_span, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n    }\n\n    fn validate_caches(&self) {\n        // Validate duplicate cache cluster names.\n        let mut seen_clusters = HashMap::new();\n        for resource in &self.parse.resources {\n            if let Resource::CacheCluster(cluster) = resource {\n                if let Some(prev_span) = seen_clusters.insert(cluster.name.clone(), cluster.span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(\n                                cluster.span,\n                                \"cache cluster with this name already defined\",\n                            )\n                            .span_note(prev_span, \"previously defined here\")\n                            .help(\"if you wish to reuse the same cluster, export the original CacheCluster object and import it, or use CacheCluster.named()\")\n                            .emit();\n                    })\n                }\n            }\n        }\n    }\n\n    fn validate_auth_handlers(&self) {\n        let mut first: Option<Span> = None;\n        for resource in &self.parse.resources {\n            if let Resource::AuthHandler(auth) = resource {\n                let span = auth.range.to_span();\n                match first {\n                    None => first = Some(span),\n                    Some(prev_span) => HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(\n                                span,\n                                \"only one auth handler can be defined per application\",\n                            )\n                            .span_note(prev_span, \"previously defined here\")\n                            .emit();\n                    }),\n                }\n            }\n        }\n    }\n\n    fn validate_sqldb(&self) {\n        let mut seen = HashMap::new();\n        for resource in &self.parse.resources {\n            if let Resource::SQLDatabase(db) = resource {\n                if let Some(prev_range) = seen.insert(db.name.clone(), db.span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(db.span, \"SQL Database with this name already defined\")\n                            .span_note(prev_range, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n    }\n\n    fn validate_metrics(&self) {\n        let mut seen = HashMap::new();\n        for resource in &self.parse.resources {\n            if let Resource::Metric(metric) = resource {\n                if let Some(prev_span) = seen.insert(metric.name.clone(), metric.span) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(\n                                metric.span,\n                                &format!(\n                                    \"metric '{}' is defined multiple times; metrics must have unique names across the entire application\",\n                                    metric.name\n                                ),\n                            )\n                            .span_note(prev_span, \"previously defined here\")\n                            .emit();\n                    })\n                }\n            }\n        }\n    }\n}\n\nfn resolve_to_concrete(state: &ResolveState, typ: &Type) -> Type {\n    match typ {\n        Type::Optional(opt) => resolve_to_concrete(state, &opt.0),\n        Type::Validated(v) => resolve_to_concrete(state, &v.typ),\n        Type::Named(named) => {\n            let underlying = named.underlying(state);\n            resolve_to_concrete(state, &underlying)\n        }\n        _ => typ.clone(),\n    }\n}\n"
  },
  {
    "path": "tsparser/src/bin/tsparser-encore.rs",
    "content": "use std::convert::Infallible;\nuse std::fmt::{self, Display};\nuse std::io::{self, BufRead, Write};\nuse std::path::PathBuf;\nuse std::rc::Rc;\nuse std::sync::{Arc, Mutex};\n\nuse anyhow::Result;\nuse prost::Message;\nuse serde::Deserialize;\nuse swc_common::errors::{Emitter, EmitterWriter, Handler, HANDLER};\nuse swc_common::{Globals, SourceMap, SourceMapper, GLOBALS};\n\nuse encore_tsparser::builder::{Builder, DebugMode, NodeJSRuntime, PlainError};\nuse encore_tsparser::parser::parser::ParseContext;\nuse encore_tsparser::{app, builder};\n\nfn main() -> Result<()> {\n    // tracing_subscriber::fmt()\n    //     .with_span_events(FmtSpan::ENTER)\n    //     .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())\n    //     .with_writer(io::stderr)\n    //     .init();\n    let cwd = std::env::current_dir()?;\n\n    let globals = Globals::new();\n\n    let cm: Rc<SourceMap> = Default::default();\n    let errors: Rc<Mutex<Vec<String>>> = Default::default();\n    let emitter = ErrorList {\n        cm: cm.clone(),\n        errors: errors.clone(),\n    };\n\n    let errs = Rc::new(Handler::with_emitter(true, false, Box::new(emitter)));\n\n    GLOBALS.set(&globals, || -> Result<()> {\n        HANDLER.set(&errs, || -> Result<()> {\n            let builder = Builder::new()?;\n            let mut parse: Option<(builder::App, app::AppDesc)> = None;\n\n            let prepare = match parse_cmd()? {\n                Some(Command::Prepare(prepare)) => prepare,\n                Some(_) => anyhow::bail!(\"expected prepare command\"),\n                None => return Ok(()),\n            };\n\n            {\n                let pp = builder::PrepareParams {\n                    app_root: prepare.app_root.clone(),\n\n                    encore_dev_version: match prepare.local_runtime_override {\n                        Some(buf) => builder::PackageVersion::Local(buf.join(\"encore.dev\")),\n                        None => builder::PackageVersion::Published(\n                            // Remove the leading \"v\" from the runtime version, as JS version numbers don't use it.\n                            prepare.runtime_version.trim_start_matches(\"v\").to_string(),\n                        ),\n                    },\n                };\n\n                match builder.prepare(&pp) {\n                    Ok(result) => {\n                        let json = serde_json::to_string(&result)?;\n                        write_result(<Result<_, Infallible>>::Ok(json.as_bytes()))?;\n                    }\n                    Err(err) => {\n                        log::error!(\"failed to prepare: {:?}\", err);\n                        write_result(Err(err))?\n                    }\n                }\n            }\n\n            let pc = match ParseContext::new(prepare.app_root, None, cm.clone(), errs.clone()) {\n                Ok(pc) => pc,\n                Err(err) => {\n                    log::error!(\"failed to construct parse context: {:?}\", err);\n                    write_result(Err(err))?;\n                    return Ok(());\n                }\n            };\n\n            loop {\n                let cmd = match parse_cmd()? {\n                    Some(cmd) => cmd,\n                    None => return Ok(()),\n                };\n\n                match cmd {\n                    Command::Prepare(input) => {\n                        log::debug!(\"got prepare input {:?}\", input);\n                    }\n\n                    Command::Parse(input) => {\n                        log::debug!(\"got parse input {:?}\", input);\n                        if parse.is_some() {\n                            anyhow::bail!(\"already parsed!\");\n                        }\n\n                        let app = builder::App {\n                            root: input.app_root.clone(),\n                            platform_id: input.platform_id,\n                            local_id: input.local_id,\n                        };\n                        let pp = builder::ParseParams {\n                            app: &app,\n                            pc: &pc,\n                            working_dir: &cwd,\n                            parse_tests: input.parse_tests,\n                        };\n\n                        match builder.parse(&pp) {\n                            Some(result) => {\n                                log::info!(\"parse successful\");\n                                write_result(<Result<_, Infallible>>::Ok(\n                                    result.meta.encode_to_vec().as_slice(),\n                                ))?;\n                                parse = Some((app, result));\n                            }\n                            None => {\n                                // Get errors from the emitter.\n                                let errs = errors.lock().unwrap();\n                                let mut err_msg = String::new();\n                                for err in errs.iter() {\n                                    err_msg.push_str(err);\n                                    err_msg.push('\\n');\n                                }\n\n                                // Don't include stack trace or detailed error info\n                                // if this is a parse error.\n                                write_result(Err(anyhow::anyhow!(PlainError(err_msg))))?\n                            }\n                        }\n                    }\n\n                    Command::Compile(input) => match &parse {\n                        None => anyhow::bail!(\"no parse!\"),\n                        Some((app, parse)) => {\n                            let cp = builder::CompileParams {\n                                app,\n                                pc: &pc,\n                                working_dir: &cwd,\n                                desc: parse,\n                                debug: input.debug,\n                                nodejs_runtime: input.nodejs_runtime,\n                            };\n\n                            log::info!(\"starting compile\");\n                            match builder.compile(&cp) {\n                                Ok(compile) => {\n                                    log::info!(\"compile successful\");\n                                    let json = serde_json::to_string(&compile)?;\n                                    write_result(<Result<_, Infallible>>::Ok(json.as_bytes()))?;\n                                }\n                                Err(err) => {\n                                    log::error!(\"failed to compile: {:?}\", err);\n                                    write_result(Err(err))?\n                                }\n                            };\n                        }\n                    },\n\n                    Command::Test(_input) => match &parse {\n                        None => anyhow::bail!(\"no parse!\"),\n                        Some((app, parse)) => {\n                            let p = builder::TestParams {\n                                app,\n                                pc: &pc,\n                                working_dir: &cwd,\n                                parse,\n                            };\n\n                            match builder.test(&p) {\n                                Ok(compile) => {\n                                    let json = serde_json::to_string(&compile)?;\n                                    write_result(<Result<_, Infallible>>::Ok(json.as_bytes()))?;\n                                }\n                                Err(err) => write_result(Err(err))?,\n                            };\n                        }\n                    },\n\n                    Command::GenUserFacing(_input) => match &parse {\n                        None => anyhow::bail!(\"no parse!\"),\n                        Some((app, parse)) => {\n                            let cp = builder::CodegenParams {\n                                app,\n                                pc: &pc,\n                                working_dir: &cwd,\n                                desc: parse,\n                            };\n\n                            log::info!(\"starting generate user facing code\");\n                            match builder.generate_code(&cp) {\n                                Ok(_) => write_result(<Result<_, Infallible>>::Ok(&[]))?,\n                                Err(err) => {\n                                    log::error!(\"failed to generate code: {:?}\", err);\n                                    write_result(Err(err))?\n                                }\n                            };\n                        }\n                    },\n                }\n            }\n        })\n    })\n}\n\nfn write_data(is_ok: bool, data: &[u8]) -> io::Result<()> {\n    let mut stdout = std::io::stdout().lock();\n    let byte_len = ((data.len() + 1) as u32).to_le_bytes();\n    stdout.write_all(&byte_len)?;\n    stdout.write_all(&[if is_ok { 0 } else { 1 }])?;\n    stdout.write_all(data)?;\n    stdout.flush()?;\n    Ok(())\n}\n\nfn write_result<E>(res: Result<&[u8], E>) -> io::Result<()>\nwhere\n    E: Display,\n{\n    match res {\n        Ok(bytes) => write_data(true, bytes),\n        Err(err) => {\n            let s = err.to_string();\n            write_data(false, s.as_bytes())\n        }\n    }\n}\n\nenum Command {\n    Prepare(PrepareInput),\n    Parse(ParseInput),\n    Compile(CompileInput),\n    Test(TestInput),\n    GenUserFacing(GenUserFacingInput),\n}\n\nfn parse_cmd() -> Result<Option<Command>> {\n    let stdin = io::stdin();\n    let mut stdin = stdin.lock();\n\n    // Read a line and see what it says.\n    let line = {\n        let mut line = String::new();\n        stdin.read_line(&mut line)?;\n        line\n    };\n\n    match line.trim() {\n        \"\" => Ok(None),\n        \"prepare\" => {\n            let mut de = serde_json::Deserializer::from_reader(stdin);\n            let input = PrepareInput::deserialize(&mut de)?;\n            Ok(Some(Command::Prepare(input)))\n        }\n        \"parse\" => {\n            let mut de = serde_json::Deserializer::from_reader(stdin);\n            let input = ParseInput::deserialize(&mut de)?;\n            Ok(Some(Command::Parse(input)))\n        }\n        \"gen-user-facing\" => {\n            let mut de = serde_json::Deserializer::from_reader(stdin);\n            let input = GenUserFacingInput::deserialize(&mut de)?;\n            Ok(Some(Command::GenUserFacing(input)))\n        }\n        \"compile\" => {\n            let mut de = serde_json::Deserializer::from_reader(stdin);\n            let input = CompileInput::deserialize(&mut de)?;\n            Ok(Some(Command::Compile(input)))\n        }\n        \"test\" => {\n            let mut de = serde_json::Deserializer::from_reader(stdin);\n            let input = TestInput::deserialize(&mut de)?;\n            Ok(Some(Command::Test(input)))\n        }\n        _ => anyhow::bail!(\"unknown command {:#?}\", line),\n    }\n}\n\n#[derive(Deserialize, Debug)]\nstruct ParseInput {\n    app_root: PathBuf,\n    platform_id: Option<String>,\n    local_id: String,\n    parse_tests: bool,\n}\n\n#[derive(Deserialize, Debug)]\nstruct PrepareInput {\n    app_root: PathBuf,\n    runtime_version: String,\n    #[serde(default)]\n    local_runtime_override: Option<PathBuf>,\n}\n\n#[derive(Deserialize, Debug)]\nstruct CompileInput {\n    debug: DebugMode,\n    #[serde(default)]\n    nodejs_runtime: NodeJSRuntime,\n}\n\n#[derive(Deserialize, Debug)]\nstruct TestInput {}\n\n#[derive(Deserialize, Debug)]\nstruct GenUserFacingInput {}\n\nstruct ErrorList {\n    cm: Rc<dyn SourceMapper>,\n    errors: Rc<Mutex<Vec<String>>>,\n}\n\nimpl Emitter for ErrorList {\n    fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) {\n        let buf: AtomicBuf = Default::default();\n\n        let mut w = EmitterWriter::new(Box::new(buf.clone()), Some(self.cm.clone()), false, false);\n        w.emit(db);\n\n        let s = buf.to_string();\n        self.errors.lock().unwrap().push(s);\n    }\n}\n\n#[derive(Default, Clone)]\nstruct AtomicBuf(Arc<Mutex<Vec<u8>>>);\n\nimpl fmt::Display for AtomicBuf {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", String::from_utf8_lossy(&self.0.lock().unwrap()))\n    }\n}\n\nimpl Write for AtomicBuf {\n    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {\n        self.0.lock().unwrap().extend_from_slice(buf);\n        Ok(buf.len())\n    }\n\n    fn flush(&mut self) -> io::Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/codegen.rs",
    "content": "use std::collections::HashMap;\nuse std::fs;\nuse std::fs::DirBuilder;\nuse std::io::Write;\nuse std::path::{Component, Path, PathBuf};\n\nuse crate::app::AppDesc;\nuse anyhow::Context;\nuse clean_path::Clean;\nuse itertools::Itertools;\nuse serde_json::{json, Value};\n\nuse crate::builder::package_mgmt::resolve_package_manager;\nuse crate::parser::parser::ParseContext;\nuse crate::parser::resourceparser::bind::BindKind::Create;\nuse crate::parser::resources::apis::api::Methods;\nuse crate::parser::resources::Resource;\nuse crate::parser::{FilePath, Range};\n\nuse super::prepare::{PackageVersion, PrepareError};\nuse super::{App, Builder};\n\n#[derive(Debug)]\npub struct CodegenParams<'a> {\n    pub app: &'a App,\n    pub pc: &'a ParseContext,\n    pub working_dir: &'a Path,\n    pub desc: &'a AppDesc,\n}\n\npub struct CodegenResult {\n    /// The path to node_modules.\n    pub node_modules: PathBuf,\n}\n\nimpl Builder<'_> {\n    pub fn setup_deps(\n        &self,\n        app_root: &Path,\n        encore_dev_version: &PackageVersion,\n    ) -> Result<(), PrepareError> {\n        let pkg_mgr = resolve_package_manager(app_root)?;\n        pkg_mgr.setup_deps(encore_dev_version)\n    }\n\n    pub fn generate_code(&self, params: &CodegenParams) -> Result<CodegenResult, PrepareError> {\n        // Find the node_modules dir and the relative path back to the app root.\n        let (node_modules, _rel_return_path) = self\n            .find_node_modules_dir(&params.app.root)\n            .ok_or(PrepareError::NodeModulesNotFound)?;\n\n        let files = self.codegen_data(params)?;\n\n        write_gen_encore_dir(&params.app.root, &files)?;\n\n        Ok(CodegenResult { node_modules })\n    }\n\n    fn codegen_data(&self, params: &CodegenParams) -> Result<Vec<CodegenFile>, PrepareError> {\n        let mut files = vec![];\n\n        let mut auth_ctx = Vec::new();\n\n        let get_svc_rel_path = |svc_root: &Path, range: Range, strip_ext: bool| -> String {\n            match range.file(&params.pc.file_set) {\n                FilePath::Real(mut buf) => {\n                    if strip_ext {\n                        buf.set_extension(\"\");\n                    }\n                    let rel = match buf.strip_prefix(svc_root) {\n                        Ok(p) => p,\n                        Err(_) => &buf,\n                    };\n\n                    rel.to_owned()\n                        .as_os_str()\n                        .to_str()\n                        .expect(\"unicode path\")\n                        .to_owned()\n                }\n                FilePath::Custom(str) => str,\n            }\n        };\n\n        // Generate the files for each service.\n        for svc in &params.desc.parse.services {\n            let mut endpoints = Vec::new();\n            let mut gateways = Vec::new();\n            let mut subscriptions = Vec::new();\n            let mut auth_handlers = Vec::new();\n            let mut metrics = Vec::new();\n            let mut service = None;\n\n            let svc_rel_path = params.app.rel_path_string(&svc.root)?;\n\n            for b in &svc.binds {\n                match &b.resource {\n                    Resource::APIEndpoint(ep) => {\n                        if ep.static_assets.is_some() {\n                            continue; // Skip static assets.\n                        }\n                        endpoints.push(ep);\n                    }\n                    Resource::Gateway(gw) => {\n                        let bind_name = b\n                            .name\n                            .as_deref()\n                            .context(\"gateway objects must be assigned to a variable\")\n                            .map_err(PrepareError::Internal)?;\n                        gateways.push((gw, bind_name));\n                    }\n                    Resource::PubSubSubscription(sub) => {\n                        subscriptions.push(sub);\n                    }\n                    Resource::AuthHandler(ah) if b.kind == Create => {\n                        auth_handlers.push(ah);\n                    }\n                    Resource::Service(svc) => {\n                        service = Some(svc);\n                    }\n                    Resource::Metric(_) => {\n                        // Collect metrics that have a bind name (are exported variables)\n                        if let Some(bind_name) = &b.name {\n                            if let Some(range) = b.range {\n                                let rel_path = get_svc_rel_path(&svc.root, range, true);\n                                let import_path = Path::new(\"../../../../../\")\n                                    .join(&svc_rel_path)\n                                    .join(rel_path);\n\n                                metrics.push(json!({\n                                    \"name\": bind_name,\n                                    \"import_path\": import_path,\n                                }));\n                            }\n                        }\n                    }\n                    _ => {}\n                }\n            }\n\n            // Add the auth handlers to the auth context.\n            for ah in &auth_handlers {\n                // Don't strip the extension here, so we support e.g. \"auth.controller.ts\" -> \"auth.controller.js\".\n                let rel_path = get_svc_rel_path(&svc.root, ah.range, false);\n\n                let import_path = Path::new(\"../../../\")\n                    .join(&svc_rel_path)\n                    .join(rel_path)\n                    .with_extension(\"js\");\n                auth_ctx.push(json!({\n                    \"name\": ah.name,\n                    \"import_path\": import_path,\n                }));\n            }\n\n            // Service Main\n            {\n                let mut endpoint_ctx = Vec::new();\n                let mut subscription_ctx = Vec::new();\n\n                let service_ctx = service\n                    .map(|service| {\n                        let rel_path = get_svc_rel_path(&svc.root, service.range, true);\n                        let import_path = Path::new(\"../../../../../\")\n                            .join(&svc_rel_path)\n                            .join(rel_path);\n\n                        json!({\n                            \"name\": service.name,\n                            \"import_path\": import_path,\n                        })\n                    })\n                    .unwrap_or_else(|| json!({\"name\": svc.name}));\n\n                for rpc in &endpoints {\n                    let rel_path = get_svc_rel_path(&svc.root, rpc.range, true);\n                    let import_path = Path::new(\"../../../../../\")\n                        .join(&svc_rel_path)\n                        .join(rel_path);\n\n                    let has_params = if rpc.streaming_request || rpc.streaming_response {\n                        rpc.encoding.handshake.is_some()\n                    } else {\n                        !rpc.encoding.default_request_encoding().params.is_empty()\n                    };\n\n                    endpoint_ctx.push(json!({\n                        \"name\": rpc.name,\n                        \"raw\": rpc.raw,\n                        \"has_params\": has_params,\n                        \"streaming_request\": rpc.streaming_request,\n                        \"streaming_response\": rpc.streaming_response,\n                        \"import_path\": import_path,\n                        \"service_name\": svc.name,\n                        \"endpoint_options\": json!({\n                            \"expose\": rpc.expose,\n                            \"auth\": rpc.require_auth,\n                            \"isRaw\": rpc.raw,\n                            \"isStream\": rpc.streaming_request || rpc.streaming_response,\n                            \"tags\": rpc.tags,\n                        }),\n                    }));\n                }\n\n                for sub in &subscriptions {\n                    let rel_path = get_svc_rel_path(&svc.root, sub.range, true);\n                    let import_path = Path::new(\"../../../../../\")\n                        .join(&svc_rel_path)\n                        .join(rel_path);\n\n                    subscription_ctx.push(json!({\n                        \"topic_name\": sub.topic.name,\n                        \"sub_name\": sub.topic.name,\n                        \"import_path\": import_path,\n                    }));\n                }\n\n                let ctx = &json!({\n                    \"name\": svc.name,\n                    \"endpoints\": endpoint_ctx,\n                    \"subscriptions\": subscription_ctx,\n                    \"service\": service_ctx,\n                    \"metrics\": metrics,\n                });\n                let main = self.entrypoint_service_main.render(&self.reg, ctx)?;\n\n                files.push(CodegenFile {\n                    path: PathBuf::from(\"internal\")\n                        .join(\"entrypoints\")\n                        .join(\"services\")\n                        .join(&svc.name)\n                        .join(\"main\")\n                        .with_extension(\"ts\"),\n                    contents: main,\n                });\n            }\n\n            // Gateway Main\n            for (gw, bind_name) in &gateways {\n                let name = &gw.name;\n\n                // Compute the import path for the endpoint.\n                let rel_path = get_svc_rel_path(&svc.root, gw.range, true);\n                let import_path = Path::new(\"../../../../../\")\n                    .join(&svc_rel_path)\n                    .join(rel_path);\n\n                let ctx = &json!({\n                    \"name\": name,\n                    \"gateways\": [{\n                        \"encore_name\": gw.name,\n                        \"bind_name\": bind_name,\n                        \"import_path\": import_path,\n                    }],\n                });\n                let main = self.entrypoint_gateway_main.render(&self.reg, ctx)?;\n\n                files.push(CodegenFile {\n                    path: PathBuf::from(\"internal\")\n                        .join(\"entrypoints\")\n                        .join(\"gateways\")\n                        .join(name)\n                        .join(\"main\")\n                        .with_extension(\"ts\"),\n                    contents: main,\n                });\n            }\n\n            // Catalog client\n            {\n                let mut endpoint_ctx = Vec::new();\n                let svc_rel_path = params.app.rel_path_string(&svc.root)?;\n                // let node_modules_to_svc = node_modules_to_app_root.join(&svc_rel_path);\n\n                let mut has_streams = false;\n\n                for rpc in &endpoints {\n                    if rpc.streaming_request || rpc.streaming_response {\n                        has_streams = true;\n                    }\n\n                    // Don't strip the extension here, so we support e.g. \"site.controller.ts\" -> \"site.controller.js\".\n                    let rel_path = get_svc_rel_path(&svc.root, rpc.range, false);\n\n                    let import_path = Path::new(\"../../../../\")\n                        .join(&svc_rel_path)\n                        .join(rel_path)\n                        .with_extension(\"js\");\n\n                    let has_params = if rpc.streaming_request || rpc.streaming_response {\n                        rpc.encoding.handshake.is_some()\n                    } else {\n                        !rpc.encoding.default_request_encoding().params.is_empty()\n                    };\n\n                    endpoint_ctx.push(json!({\n                        \"name\": rpc.name,\n                        \"raw\": rpc.raw,\n                        \"has_params\": has_params,\n                        \"streaming_request\": rpc.streaming_request,\n                        \"streaming_response\": rpc.streaming_response,\n                        \"import_path\": import_path,\n                        \"endpoint_options\": json!({\n                            \"expose\": rpc.expose,\n                            \"auth\": rpc.require_auth,\n                            \"isRaw\": rpc.raw,\n                            \"isStream\": rpc.streaming_request || rpc.streaming_response,\n                            \"tags\": rpc.tags,\n                        }),\n                    }));\n                }\n\n                let service_ctx = service\n                    .map(|service| {\n                        let rel_path = get_svc_rel_path(&svc.root, service.range, true);\n                        let import_path =\n                            Path::new(\"../../../../\").join(&svc_rel_path).join(rel_path);\n\n                        json!({\n                            \"name\": service.name,\n                            \"import_path\": import_path,\n                        })\n                    })\n                    .unwrap_or_else(|| json!({\"name\": svc.name}));\n\n                let ctx = &json!({\n                    \"name\": svc.name,\n                    \"endpoints\": endpoint_ctx,\n                    \"has_streams\": has_streams,\n                    \"service\": service_ctx,\n                });\n\n                let service_d_ts = self.catalog_clients_service_d_ts.render(&self.reg, ctx)?;\n                files.push(CodegenFile {\n                    path: PathBuf::from(\"internal\")\n                        .join(\"clients\")\n                        .join(&svc.name)\n                        .join(\"endpoints\")\n                        .with_extension(\"d.ts\"),\n                    contents: service_d_ts,\n                });\n\n                let service_js = self.catalog_clients_service_js.render(&self.reg, ctx)?;\n                files.push(CodegenFile {\n                    path: PathBuf::from(\"internal\")\n                        .join(\"clients\")\n                        .join(&svc.name)\n                        .join(\"endpoints\")\n                        .with_extension(\"js\"),\n                    contents: service_js,\n                });\n\n                let service_testing_js = self\n                    .catalog_clients_service_testing_js\n                    .render(&self.reg, ctx)?;\n                files.push(CodegenFile {\n                    path: PathBuf::from(\"internal\")\n                        .join(\"clients\")\n                        .join(&svc.name)\n                        .join(\"endpoints_testing\")\n                        .with_extension(\"js\"),\n                    contents: service_testing_js,\n                });\n            }\n        }\n\n        // Catalog Auth\n        {\n            let ctx = &json!({\n               \"auth_handlers\": auth_ctx,\n            });\n\n            let index_d_ts = self.catalog_auth_index_ts.render(&self.reg, ctx)?;\n            files.push(CodegenFile {\n                path: PathBuf::from(\"auth\").join(\"index\").with_extension(\"ts\"),\n                contents: index_d_ts,\n            });\n\n            let auth_ts = self.catalog_auth_auth_ts.render(&self.reg, ctx)?;\n            files.push(CodegenFile {\n                path: PathBuf::from(\"internal\")\n                    .join(\"auth\")\n                    .join(\"auth\")\n                    .with_extension(\"ts\"),\n                contents: auth_ts,\n            });\n        }\n\n        // Combined Main\n        {\n            let mut endpoint_ctx = Vec::new();\n            let mut gateway_ctx = Vec::new();\n            let mut subscription_ctx = Vec::new();\n            let mut metrics_ctx = Vec::new();\n            let mut services_ctx = HashMap::new();\n\n            for svc in &params.desc.parse.services {\n                let mut endpoints = Vec::new();\n                let mut gateways = Vec::new();\n                let mut subscriptions = Vec::new();\n                let mut metrics = Vec::new();\n\n                let svc_rel_path = params.app.rel_path_string(&svc.root)?;\n\n                for b in &svc.binds {\n                    match &b.resource {\n                        Resource::APIEndpoint(ep) => {\n                            if ep.static_assets.is_some() {\n                                continue; // Skip static assets.\n                            }\n                            endpoints.push(ep);\n                        }\n                        Resource::Gateway(gw) => {\n                            let bind_name = b\n                                .name\n                                .as_deref()\n                                .context(\"gateway objects must be assigned to a variable\")\n                                .map_err(PrepareError::Internal)?;\n                            gateways.push((gw, bind_name));\n                        }\n                        Resource::PubSubSubscription(sub) => {\n                            subscriptions.push(sub);\n                        }\n                        Resource::Metric(_) => {\n                            // Collect metrics that have a bind name (are exported variables)\n                            if let Some(bind_name) = &b.name {\n                                if let Some(range) = b.range {\n                                    metrics.push((bind_name.as_str(), range));\n                                }\n                            }\n                        }\n                        Resource::Service(service) => {\n                            let rel_path = get_svc_rel_path(&svc.root, service.range, true);\n                            let import_path =\n                                Path::new(\"../../../../\").join(&svc_rel_path).join(rel_path);\n                            services_ctx.insert(\n                                svc.name.clone(),\n                                json!({\n                                    \"name\": service.name,\n                                    \"import_path\": import_path,\n                                }),\n                            );\n                        }\n                        _ => {}\n                    }\n                }\n\n                // Service Main\n                for rpc in &endpoints {\n                    if !services_ctx.contains_key(&svc.name) {\n                        services_ctx.insert(svc.name.clone(), json!({\"name\": svc.name}));\n                    }\n\n                    let rel_path = get_svc_rel_path(&svc.root, rpc.range, true);\n                    let import_path = Path::new(\"../../../../\").join(&svc_rel_path).join(rel_path);\n\n                    let has_params = if rpc.streaming_request || rpc.streaming_response {\n                        rpc.encoding.handshake.is_some()\n                    } else {\n                        !rpc.encoding.default_request_encoding().params.is_empty()\n                    };\n\n                    endpoint_ctx.push(json!({\n                        \"name\": rpc.name,\n                        \"raw\": rpc.raw,\n                        \"has_params\": has_params,\n                        \"streaming_request\": rpc.streaming_request,\n                        \"streaming_response\": rpc.streaming_response,\n                        \"service_name\": svc.name,\n                        \"import_path\": import_path,\n                        \"endpoint_options\": json!({\n                            \"expose\": rpc.expose,\n                            \"auth\": rpc.require_auth,\n                            \"isRaw\": rpc.raw,\n                            \"isStream\": rpc.streaming_request || rpc.streaming_response,\n                            \"tags\": rpc.tags,\n                        }),\n                    }));\n                }\n\n                // Gateway Main\n                for (gw, bind_name) in &gateways {\n                    let _name = &gw.name;\n                    let rel_path = get_svc_rel_path(&svc.root, gw.range, true);\n                    let import_path = Path::new(\"../../../../\").join(&svc_rel_path).join(rel_path);\n\n                    gateway_ctx.push(json!({\n                        \"encore_name\": gw.name,\n                        \"bind_name\": bind_name,\n                        \"import_path\": import_path,\n                    }));\n                }\n\n                // Subscriptions\n                for sub in &subscriptions {\n                    let rel_path = get_svc_rel_path(&svc.root, sub.range, true);\n                    let import_path = Path::new(\"../../../../\").join(&svc_rel_path).join(rel_path);\n\n                    subscription_ctx.push(json!({\n                        \"topic_name\": sub.topic.name,\n                        \"sub_name\": sub.topic.name,\n                        \"import_path\": import_path,\n                    }));\n                }\n\n                // Metrics\n                for (bind_name, range) in &metrics {\n                    let rel_path = get_svc_rel_path(&svc.root, *range, true);\n                    let import_path = Path::new(\"../../../../\").join(&svc_rel_path).join(rel_path);\n\n                    metrics_ctx.push(json!({\n                        \"name\": bind_name,\n                        \"import_path\": import_path,\n                        \"service_name\": svc.name,\n                    }));\n                }\n            }\n\n            let ctx = &json!({\n                \"endpoints\": endpoint_ctx,\n                \"gateways\": gateway_ctx,\n                \"subscriptions\": subscription_ctx,\n                \"metrics\": metrics_ctx,\n                \"services\": services_ctx,\n            });\n            let main = self.entrypoint_combined_main.render(&self.reg, ctx)?;\n\n            files.push(CodegenFile {\n                path: PathBuf::from(\"internal\")\n                    .join(\"entrypoints\")\n                    .join(\"combined\")\n                    .join(\"main\")\n                    .with_extension(\"ts\"),\n                contents: main,\n            });\n        }\n\n        // Catalog Index\n        {\n            let mut services_ctx = Vec::new();\n            for svc in &params.desc.parse.services {\n                services_ctx.push(json!({\n                    \"name\": svc.name,\n                }));\n            }\n\n            let ctx = &json!({\n                \"services\": services_ctx,\n            });\n\n            let index_js = self.catalog_clients_index_js.render(&self.reg, ctx)?;\n            files.push(CodegenFile {\n                path: PathBuf::from(\"clients\").join(\"index\").with_extension(\"js\"),\n                contents: index_js,\n            });\n\n            let index_d_ts = self.catalog_clients_index_d_ts.render(&self.reg, ctx)?;\n            files.push(CodegenFile {\n                path: PathBuf::from(\"clients\")\n                    .join(\"index\")\n                    .with_extension(\"d.ts\"),\n                contents: index_d_ts,\n            });\n        }\n\n        let mut duplicates = files.iter().duplicates_by(|f| f.path.clone());\n        if let Some(dup) = duplicates.next() {\n            return Err(PrepareError::Internal(anyhow::anyhow!(\n                \"duplicate file path: {:?}\",\n                dup.path\n            )));\n        }\n\n        Ok(files)\n    }\n\n    /// Find the node_modules_dir in parent directories of base,\n    /// and at the same time compute the relative path from it to get\n    /// back to base.\n    pub fn find_node_modules_dir(&self, base: &Path) -> Option<(PathBuf, PathBuf)> {\n        let pred = |p: &Path| p.join(\"node_modules\").exists();\n        let (ancestor, return_path) = find_ancestor(base, pred)?;\n\n        let node_modules_dir = ancestor.join(\"node_modules\").clean();\n\n        // Prepend \"../\" to the return path since we're appending \"node_modules\" above.\n        let return_path = Path::new(\"..\").join(return_path).clean();\n        Some((node_modules_dir, return_path))\n    }\n}\n\nfn _http_methods(methods: &Methods) -> Value {\n    match methods {\n        Methods::All => json!(\"*\"),\n        Methods::Some(methods) => {\n            let strs = methods.iter().map(|m| m.as_str()).collect::<Vec<_>>();\n            json!(strs)\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct CodegenFile {\n    pub path: PathBuf,\n    // relative to the node_modules/.encoredev folder\n    pub contents: String,\n}\n\nfn write_gen_encore_dir(app_root: &Path, files: &[CodegenFile]) -> Result<(), PrepareError> {\n    let base_dir = app_root.join(\"encore.gen\");\n    for f in files {\n        if f.path.is_absolute() {\n            return Err(PrepareError::Internal(anyhow::anyhow!(\n                \"path {:?} is not relative to the encore.gen folder\",\n                f.path\n            )));\n        }\n\n        let file_path = base_dir.join(&f.path);\n        // Create the parent of the file, if needed\n        if let Some(parent) = file_path.parent() {\n            DirBuilder::new()\n                .recursive(true)\n                .create(parent)\n                .map_err(PrepareError::GenerateCode)?;\n        }\n\n        let file = fs::File::create(file_path).map_err(PrepareError::GenerateCode)?;\n        let mut buf = std::io::BufWriter::new(file);\n        buf.write_all(f.contents.as_bytes())\n            .map_err(PrepareError::GenerateCode)?;\n        buf.flush().map_err(PrepareError::GenerateCode)?;\n    }\n\n    Ok(())\n}\n\n/// Finds the first ancestor of base that satisfies predicate.\n/// It returns the path to the ancestor (in the form \"../../..\" etc),\n/// as well as the return path back to the base directory.\n/// If predicate(base) is true, it reports (\".\", \".\").\nfn find_ancestor(base: &Path, predicate: fn(&Path) -> bool) -> Option<(PathBuf, PathBuf)> {\n    let mut comps = base.components();\n    let mut return_path = Vec::new();\n\n    // Algorithm sketch as follows.\n    // 1. Check if the predicate is satisfied on the current path. If true, return.\n    // 2. Otherwise, remove the last component of the path, and add it to the return path.\n    // 3. Add the component to the return path, and go to step 1.\n    loop {\n        let curr = comps.as_path();\n        if predicate(curr) {\n            // Compute the return path. If it's empty return \".\".\n            let return_path = if !return_path.is_empty() {\n                // The components of the return path have been inserted in backwards order,\n                // so reverse it now.\n                return_path.iter().rev().collect::<PathBuf>()\n            } else {\n                PathBuf::from(\".\")\n            };\n            return Some((curr.to_path_buf(), return_path));\n        }\n\n        let comp = comps.next_back()?;\n\n        match comp {\n            Component::Normal(_) | Component::ParentDir => {\n                return_path.push(comp);\n            }\n\n            // \".\" doesn't affect the predicate or the return path, so ignore it.\n            Component::CurDir => {}\n\n            // We've reached the beginning of the path and haven't found a match;\n            // we're done.\n            Component::Prefix(_) | Component::RootDir => return None,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_find_ancestor() {\n        {\n            let pred = |p: &Path| p.ends_with(\"true\");\n            let base = Path::new(\"/foo/true/bar/baz\");\n\n            let (ancestor, return_path) = find_ancestor(base, pred).unwrap();\n            assert_eq!(ancestor, Path::new(\"/foo/true\"));\n            assert_eq!(return_path, Path::new(\"bar/baz\"));\n        }\n\n        {\n            let pred = |_p: &Path| true;\n            let base = Path::new(\"/foo/bar/baz\");\n\n            let (ancestor, return_path) = find_ancestor(base, pred).unwrap();\n            assert_eq!(ancestor, Path::new(\"/foo/bar/baz\"));\n            assert_eq!(return_path, Path::new(\".\"));\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/compile.rs",
    "content": "use std::fs;\nuse std::path::{Path, PathBuf};\n\nuse crate::app::AppDesc;\nuse anyhow::{Context, Result};\nuse serde::Serialize;\n\nuse crate::builder::codegen::CodegenParams;\nuse crate::builder::transpiler::{\n    EsbuildCompiler, ExternalPackages, Input, InputKind, OutputTranspiler, TranspileParams,\n};\nuse crate::parser::parser::ParseContext;\n\nuse super::{App, Builder, DebugMode, NodeJSRuntime};\n\n#[derive(Debug)]\npub struct CompileParams<'a> {\n    pub app: &'a App,\n    pub pc: &'a ParseContext,\n    pub working_dir: &'a Path,\n    pub desc: &'a AppDesc,\n    pub debug: DebugMode,\n    pub nodejs_runtime: NodeJSRuntime,\n}\n\n#[derive(Serialize, Debug)]\npub struct CompileResult {\n    pub outputs: Vec<JSBuildOutput>,\n}\n\n#[derive(Serialize, Debug)]\npub struct JSBuildOutput {\n    pub artifact_dir: PathBuf,\n    pub entrypoints: Vec<Entrypoint>,\n}\n\n#[derive(Serialize, Debug)]\npub struct Entrypoint {\n    pub cmd: CmdSpec,\n    pub services: Vec<String>,\n    pub gateways: Vec<String>,\n    pub use_runtime_config_v2: bool,\n}\n\npub type ArtifactString = String;\n\n#[derive(Serialize, Debug)]\npub struct CmdSpec {\n    pub command: Vec<ArtifactString>,\n    pub env: Vec<ArtifactString>,\n    pub prioritized_files: Vec<ArtifactString>,\n}\n\nimpl Builder<'_> {\n    pub fn compile(&self, params: &CompileParams) -> Result<CompileResult> {\n        self.generate_code(&CodegenParams {\n            app: params.app,\n            pc: params.pc,\n            working_dir: params.working_dir,\n            desc: params.desc,\n        })\n        .context(\"generate code\")?;\n\n        let build_dir = params.app.root.join(\".encore\").join(\"build\");\n        fs::create_dir_all(&build_dir)?;\n\n        let (node_modules, _) = self\n            .find_node_modules_dir(params.app.root.as_path())\n            .ok_or_else(|| anyhow::anyhow!(\"could not find node_modules directory\"))?;\n\n        let transpiler = EsbuildCompiler {\n            node_modules_dir: node_modules.as_path(),\n            external: ExternalPackages::All,\n        };\n\n        let inputs = {\n            let mut inputs = Vec::with_capacity(\n                params.desc.parse.services.len() + params.desc.meta.gateways.len(),\n            );\n\n            let service_names = params\n                .desc\n                .parse\n                .services\n                .iter()\n                .map(|s| s.name.clone())\n                .collect();\n            let gateway_names = params\n                .desc\n                .meta\n                .gateways\n                .iter()\n                .map(|g| g.encore_name.clone())\n                .collect();\n\n            inputs.push(Input {\n                kind: InputKind::Combined(gateway_names, service_names),\n                entrypoint: params\n                    .app\n                    .root\n                    .join(\"encore.gen\")\n                    .join(\"internal\")\n                    .join(\"entrypoints\")\n                    .join(\"combined\")\n                    .join(\"main.ts\"),\n            });\n\n            inputs\n        };\n\n        let result = transpiler.transpile(TranspileParams {\n            artifact_dir: build_dir.as_path(),\n            cwd: params.app.root.as_path(),\n            debug: params.debug,\n            nodejs_runtime: params.nodejs_runtime,\n            inputs,\n        })?;\n\n        Ok(CompileResult {\n            outputs: vec![JSBuildOutput {\n                artifact_dir: build_dir,\n                entrypoints: result.entrypoints,\n            }],\n        })\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/mod.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse convert_case::{Case, Casing};\nuse handlebars::*;\nuse prepare::PrepareError;\nuse serde::{Deserialize, Serialize};\n\npub use codegen::{CodegenParams, CodegenResult};\npub use compile::CompileParams;\npub use parse::{ParseError, ParseParams};\npub use prepare::{PackageVersion, PrepareParams};\npub use test::TestParams;\n\nmod codegen;\nmod compile;\nmod package_mgmt;\nmod parse;\nmod prepare;\nmod test;\nmod transpiler;\n\npub struct Builder<'a> {\n    reg: Handlebars<'a>,\n    entrypoint_service_main: Template<'a>,\n    entrypoint_gateway_main: Template<'a>,\n    entrypoint_combined_main: Template<'a>,\n\n    catalog_clients_index_js: Template<'a>,\n    catalog_clients_index_d_ts: Template<'a>,\n    catalog_clients_service_js: Template<'a>,\n    catalog_clients_service_testing_js: Template<'a>,\n    catalog_clients_service_d_ts: Template<'a>,\n    catalog_auth_index_ts: Template<'a>,\n    catalog_auth_auth_ts: Template<'a>,\n}\n\nimpl Builder<'_> {\n    pub fn new() -> Result<Self, PrepareError> {\n        let mut reg = Handlebars::new();\n        reg.register_helper(\"toJSON\", Box::new(to_json));\n        reg.register_helper(\"stripExt\", Box::new(strip_ext));\n        reg.register_helper(\"toPascalCase\", Box::new(to_pascal_case));\n        reg.register_helper(\"encoreNameToIdent\", Box::new(encore_name_to_ident));\n        let entrypoint_service_main =\n            Template::new(&mut reg, \"service_main\", ENTRYPOINT_SERVICE_MAIN)\n                .map_err(|e| PrepareError::Internal(e.into()))?;\n        let entrypoint_gateway_main =\n            Template::new(&mut reg, \"gateway_main\", ENTRYPOINT_GATEWAY_MAIN)\n                .map_err(|e| PrepareError::Internal(e.into()))?;\n        let entrypoint_combined_main =\n            Template::new(&mut reg, \"combined_main\", ENTRYPOINT_COMBINED_MAIN)\n                .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_clients_index_js = Template::new(\n            &mut reg,\n            \"catalog_clients_index_js\",\n            CATALOG_CLIENTS_INDEX_JS,\n        )\n        .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_clients_index_d_ts = Template::new(\n            &mut reg,\n            \"catalog_clients_index_d_ts\",\n            CATALOG_CLIENTS_INDEX_D_TS,\n        )\n        .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_clients_service_js = Template::new(\n            &mut reg,\n            \"catalog_clients_service_js\",\n            CATALOG_CLIENTS_SERVICE_JS,\n        )\n        .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_clients_service_testing_js = Template::new(\n            &mut reg,\n            \"catalog_clients_service_test_js\",\n            CATALOG_CLIENTS_SERVICE_TESTING_JS,\n        )\n        .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_clients_service_d_ts = Template::new(\n            &mut reg,\n            \"catalog_clients_service_d_ts\",\n            CATALOG_CLIENTS_SERVICE_D_TS,\n        )\n        .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_auth_index_ts =\n            Template::new(&mut reg, \"catalog_auth_index_ts\", CATALOG_AUTH_INDEX_TS)\n                .map_err(|e| PrepareError::Internal(e.into()))?;\n        let catalog_auth_auth_ts =\n            Template::new(&mut reg, \"catalog_auth_auth_ts\", CATALOG_AUTH_AUTH_TS)\n                .map_err(|e| PrepareError::Internal(e.into()))?;\n\n        Ok(Self {\n            reg,\n            entrypoint_service_main,\n            entrypoint_gateway_main,\n            entrypoint_combined_main,\n            catalog_clients_index_js,\n            catalog_clients_index_d_ts,\n            catalog_clients_service_js,\n            catalog_clients_service_testing_js,\n            catalog_clients_service_d_ts,\n            catalog_auth_index_ts,\n            catalog_auth_auth_ts,\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct App {\n    pub root: PathBuf,\n    pub platform_id: Option<String>,\n    pub local_id: String,\n}\n\nimpl App {\n    /// Compute the relative path from the app root.\n    /// It reports an error if the path is not under the app root.\n    fn rel_path<'b>(&self, path: &'b Path) -> Result<&'b Path, PrepareError> {\n        let suffix = path.strip_prefix(&self.root).map_err(|err| {\n            PrepareError::Internal(anyhow::anyhow!(\n                \"unable to compute relative path to app root from {path:?}: {err}\"\n            ))\n        })?;\n\n        Ok(suffix)\n    }\n\n    /// Compute the relative path from the app root as a String.\n    fn rel_path_string(&self, path: &Path) -> Result<String, PrepareError> {\n        let suffix = self.rel_path(path)?;\n        let s = suffix\n            .to_str()\n            .ok_or(PrepareError::Internal(anyhow::anyhow!(\n                \"invalid path: {:?}\",\n                path\n            )))?;\n        Ok(s.to_string())\n    }\n}\n\nstruct Template<'a> {\n    name: &'a str,\n}\n\nimpl<'a> Template<'a> {\n    fn new(reg: &mut Handlebars, name: &'a str, template_str: &str) -> Result<Self, PrepareError> {\n        reg.register_template_string(name, template_str)\n            .map_err(|e| PrepareError::Internal(e.into()))?;\n\n        Ok(Self { name })\n    }\n\n    fn render(&self, reg: &Handlebars, data: &impl Serialize) -> Result<String, PrepareError> {\n        reg.render(self.name, data)\n            .map_err(|e| PrepareError::Internal(e.into()))\n    }\n}\n\nconst ENTRYPOINT_SERVICE_MAIN: &str =\n    include_str!(\"templates/entrypoints/services/main.handlebars\");\nconst ENTRYPOINT_GATEWAY_MAIN: &str =\n    include_str!(\"templates/entrypoints/gateways/main.handlebars\");\nconst ENTRYPOINT_COMBINED_MAIN: &str =\n    include_str!(\"templates/entrypoints/combined/main.handlebars\");\n\nconst CATALOG_CLIENTS_INDEX_JS: &str =\n    include_str!(\"templates/catalog/clients/index_js.handlebars\");\nconst CATALOG_CLIENTS_INDEX_D_TS: &str =\n    include_str!(\"templates/catalog/clients/index_d_ts.handlebars\");\nconst CATALOG_CLIENTS_SERVICE_JS: &str =\n    include_str!(\"templates/catalog/clients/endpoints_js.handlebars\");\nconst CATALOG_CLIENTS_SERVICE_TESTING_JS: &str =\n    include_str!(\"templates/catalog/clients/endpoints_testing_js.handlebars\");\nconst CATALOG_CLIENTS_SERVICE_D_TS: &str =\n    include_str!(\"templates/catalog/clients/endpoints_d_ts.handlebars\");\nconst CATALOG_AUTH_INDEX_TS: &str = include_str!(\"templates/catalog/auth/index_ts.handlebars\");\nconst CATALOG_AUTH_AUTH_TS: &str = include_str!(\"templates/catalog/auth/auth_ts.handlebars\");\n\nhandlebars_helper!(strip_ext: |v: String| v.rsplit_once('.').map(|(a, _)| a.to_string()).unwrap_or(v));\nhandlebars_helper!(to_pascal_case: |v: String| v.to_case(Case::Pascal));\nhandlebars_helper!(encore_name_to_ident: |v: String| v.replace('-', \"_\"));\n\nfn to_json(\n    h: &Helper<'_, '_>,\n    _: &Handlebars<'_>,\n    _: &Context,\n    _rc: &mut RenderContext<'_, '_>,\n    out: &mut dyn Output,\n) -> HelperResult {\n    // get parameter from helper or throw an error\n    let param = h\n        .param(0)\n        .map(|v| serde_json::to_string(v.value()).unwrap())\n        .unwrap_or_default();\n    out.write(param.as_ref())?;\n    Ok(())\n}\n\n/// An error that is rendered plainly, without a backtrace.\n#[derive(Debug)]\npub struct PlainError(pub String);\n\nimpl std::fmt::Display for PlainError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\n#[derive(Deserialize, Debug, Copy, Clone)]\n#[serde(rename_all = \"lowercase\")]\npub enum DebugMode {\n    Disabled,\n    Enabled,\n    Break,\n}\n\n#[derive(Deserialize, Debug, Copy, Clone)]\n#[serde(rename_all = \"lowercase\")]\n#[derive(Default)]\npub enum NodeJSRuntime {\n    #[default]\n    NodeJS,\n    Bun,\n}\n"
  },
  {
    "path": "tsparser/src/builder/package_mgmt.rs",
    "content": "use std::collections::HashMap;\nuse std::io;\nuse std::path::{Path, PathBuf};\nuse std::str::FromStr;\n\nuse duct::cmd;\nuse serde::Deserialize;\n\nuse crate::builder::compile::CmdSpec;\n\nuse super::prepare::{PackageVersion, PrepareError};\n\n#[derive(Debug, Clone)]\npub enum InstalledVersion {\n    /// The package is not installed.\n    NotInstalled,\n    /// The package is installed but older than the required version.\n    Older(String),\n    /// The package is installed but different than the required version,\n    /// in a way that cannot be compared (i.e. not semver but \"local development\" version).\n    Different(String),\n    /// The package is equal to the required version.\n    Current,\n    /// The package is newer than the required version.\n    Newer(String),\n}\n\nimpl PackageVersion {\n    /// Reports whether the package is installed and if it is, whether it's the correct version.\n    /// The `package_path` is needed to resolve local paths when running in development mode,\n    /// since `npm install /path/to/package.json` rewrites it to a path relative from the package.json directory.\n    pub fn is_installed(&self, ver: &str, package_path: &Path) -> InstalledVersion {\n        use InstalledVersion::*;\n        match self {\n            Self::Local(want) => {\n                if let Some(path) = ver.strip_prefix(\"file:\") {\n                    let got = PathBuf::from(path);\n\n                    // Check for exact match or cleaned match.\n                    if got == *want || clean_path::clean(&got) == clean_path::clean(want) {\n                        return Current;\n                    } else if got.is_relative() {\n                        // Check if the paths are equal after resolving relative to the package.json directory.\n                        let abs = package_path.join(&got);\n                        if abs == *want || clean_path::clean(abs) == clean_path::clean(want) {\n                            return Current;\n                        }\n                    }\n                }\n\n                Different(ver.to_string())\n            }\n\n            Self::Published(want) => {\n                let got = ver.trim_start_matches(['^', '=', '~']);\n\n                // Check if the version is an exact match.\n                if got == want {\n                    return Current;\n                }\n\n                // Parse the version and check if it's equal or greater, semver-wise.\n                let installed = semver::Version::parse(got).ok();\n                let want = semver::Version::parse(want).ok();\n                if let (Some(installed), Some(want)) = (installed, want) {\n                    use std::cmp::Ordering;\n                    match installed.cmp(&want) {\n                        Ordering::Less => Older(got.to_string()),\n                        Ordering::Equal => Current,\n                        Ordering::Greater => Newer(got.to_string()),\n                    }\n                } else {\n                    Different(got.to_string())\n                }\n            }\n        }\n    }\n}\n\nfn find_workspace_package_manager(mut dir: PathBuf) -> Result<Option<String>, PrepareError> {\n    while dir.pop() {\n        let package_json_path = dir.join(\"package.json\");\n        if package_json_path.exists() {\n            let package_json = PackageJson::parse_file(&package_json_path)?;\n            if let Some(pm) = package_json.package_manager.as_deref() {\n                if !pm.is_empty() {\n                    return Ok(package_json.package_manager);\n                }\n            }\n        }\n    }\n\n    Ok(None)\n}\n\npub(super) fn resolve_package_manager(\n    package_dir: &Path,\n) -> Result<Box<dyn PackageManager>, PrepareError> {\n    let package_json_path = package_dir.join(\"package.json\");\n    let package_json = PackageJson::parse_file(&package_json_path)?;\n\n    let package_manager = match package_json.package_manager {\n        Some(ref pm) if !pm.is_empty() => Some(pm.clone()),\n        _ => find_workspace_package_manager(package_dir.to_path_buf())?,\n    };\n\n    let package_manager = package_manager.as_deref().unwrap_or(\"npm\");\n    let package_manager = match package_manager.split_once('@') {\n        Some((name, _)) => name,\n        None => package_manager,\n    };\n\n    match package_manager.to_lowercase().as_ref() {\n        \"npm\" => Ok(Box::new(NpmPackageManager {\n            pkg_json: package_json,\n            dir: package_dir.to_path_buf(),\n        })),\n        \"bun\" => Ok(Box::new(BunPackageManager {\n            pkg_json: package_json,\n            dir: package_dir.to_path_buf(),\n        })),\n        \"yarn\" => Ok(Box::new(YarnPackageManager {\n            pkg_json: package_json,\n            dir: package_dir.to_path_buf(),\n        })),\n        \"pnpm\" => Ok(Box::new(PnpmPackageManager {\n            pkg_json: package_json,\n            dir: package_dir.to_path_buf(),\n        })),\n        _ => Err(PrepareError::UnsupportedPackageManagerError(\n            package_manager.to_string(),\n        )),\n    }\n}\n\npub(super) trait PackageManager {\n    fn setup_deps(&self, encore_dev_version: &PackageVersion) -> Result<(), PrepareError>;\n\n    fn run_tests(&self) -> CmdSpec;\n\n    #[allow(dead_code)]\n    fn mgr_name(&self) -> &'static str;\n}\n\nstruct NpmPackageManager {\n    pkg_json: PackageJson,\n    dir: PathBuf,\n}\n\nimpl PackageManager for NpmPackageManager {\n    fn setup_deps(&self, encore_dev_version: &PackageVersion) -> Result<(), PrepareError> {\n        // Install `encore.dev` if necessary\n        let installed = self.pkg_json.dependencies.get(\"encore.dev\");\n        let v = installed.map_or(InstalledVersion::NotInstalled, |v| {\n            encore_dev_version.is_installed(v, &self.dir)\n        });\n\n        // First ensure the package.json file is up to date.\n        let modified = update_package_json(&self.dir, v, encore_dev_version)?;\n\n        // If we modified anything or don't have a node_modules directory, run 'npm install'.\n        if modified || !self.dir.join(\"node_modules\").exists() {\n            cmd!(\"npm\", \"install\")\n                .dir(&self.dir)\n                .stdout_to_stderr()\n                .run()\n                .map_err(|e| PrepareError::InstallNodeModules(e, \"npm install\".into()))?;\n        }\n\n        Ok(())\n    }\n\n    fn run_tests(&self) -> CmdSpec {\n        CmdSpec {\n            command: vec![\n                \"npm\".to_string(),\n                \"run\".to_string(),\n                \"test\".to_string(),\n                // Specify '--' so that additional arguments added from the test runner\n                // aren't interpreted by npm.\n                \"--\".to_string(),\n            ],\n            env: vec![],\n            prioritized_files: vec![],\n        }\n    }\n\n    fn mgr_name(&self) -> &'static str {\n        \"npm\"\n    }\n}\n\nstruct BunPackageManager {\n    pkg_json: PackageJson,\n    dir: PathBuf,\n}\n\nimpl PackageManager for BunPackageManager {\n    fn setup_deps(&self, encore_dev_version: &PackageVersion) -> Result<(), PrepareError> {\n        // Install `encore.dev` if necessary\n        let installed = self.pkg_json.dependencies.get(\"encore.dev\");\n        let v = installed.map_or(InstalledVersion::NotInstalled, |v| {\n            encore_dev_version.is_installed(v, &self.dir)\n        });\n\n        // First ensure the package.json file is up to date.\n        let modified = update_package_json(&self.dir, v, encore_dev_version)?;\n\n        // If we modified anything or don't have a node_modules directory, run 'bun install'.\n        if modified || !self.dir.join(\"node_modules\").exists() {\n            let backend = std::env::var(\"BUN_INSTALL_BACKEND\").unwrap_or_default();\n            if backend.is_empty() {\n                cmd!(\"bun\", \"install\")\n                    .dir(&self.dir)\n                    .stdout_to_stderr()\n                    .run()\n                    .map_err(|e| PrepareError::InstallNodeModules(e, \"bun install\".into()))?;\n            } else {\n                cmd!(\"bun\", \"install\", \"--backend\", &backend)\n                    .dir(&self.dir)\n                    .stdout_to_stderr()\n                    .run()\n                    .map_err(|e| {\n                        PrepareError::InstallNodeModules(\n                            e,\n                            format!(\"bun install --backend {backend}\"),\n                        )\n                    })?;\n            }\n        }\n\n        Ok(())\n    }\n\n    fn run_tests(&self) -> CmdSpec {\n        CmdSpec {\n            command: vec![\n                \"bun\".to_string(),\n                \"run\".to_string(),\n                \"test\".to_string(),\n                // Specify '--' so that additional arguments added from the test runner\n                // aren't interpreted by bun.\n                \"--\".to_string(),\n            ],\n            env: vec![],\n            prioritized_files: vec![],\n        }\n    }\n\n    fn mgr_name(&self) -> &'static str {\n        \"bun\"\n    }\n}\n\nstruct YarnPackageManager {\n    pkg_json: PackageJson,\n    dir: PathBuf,\n}\n\nimpl PackageManager for YarnPackageManager {\n    fn setup_deps(&self, encore_dev_version: &PackageVersion) -> Result<(), PrepareError> {\n        self.ensure_nodelinker()?;\n\n        // Install `encore.dev` if necessary\n        let installed = self.pkg_json.dependencies.get(\"encore.dev\");\n        let v = installed.map_or(InstalledVersion::NotInstalled, |v| {\n            encore_dev_version.is_installed(v, &self.dir)\n        });\n\n        // First ensure the package.json file is up to date.\n        let modified = update_package_json(&self.dir, v, encore_dev_version)?;\n\n        // If we modified anything or don't have a node_modules directory, run 'npm install'.\n        if modified || !self.dir.join(\"node_modules\").exists() {\n            cmd!(\"yarn\", \"install\")\n                .dir(&self.dir)\n                .stdout_to_stderr()\n                .run()\n                .map_err(|e| PrepareError::InstallNodeModules(e, \"yarn install\".into()))?;\n        }\n\n        Ok(())\n    }\n\n    fn run_tests(&self) -> CmdSpec {\n        CmdSpec {\n            command: vec![\"yarn\".to_string(), \"run\".to_string(), \"test\".to_string()],\n            env: vec![],\n            prioritized_files: vec![],\n        }\n    }\n\n    fn mgr_name(&self) -> &'static str {\n        \"yarn\"\n    }\n}\n\nimpl YarnPackageManager {\n    /// Ensures the .yarnrc.yml file exists in the app root and has the nodeLinker set to \"node-modules\".\n    fn ensure_nodelinker(&self) -> Result<(), PrepareError> {\n        let file_path = self.dir.join(\".yarnrc.yml\");\n        if !file_path.exists() {\n            // Create the file with our desired contents.\n            let content = \"nodeLinker: node-modules\\n\";\n            std::fs::write(&file_path, content).map_err(PrepareError::SetupYarnNodeLinker)?;\n            return Ok(());\n        }\n\n        // Read the file as yaml.\n        let contents =\n            std::fs::read_to_string(&file_path).map_err(PrepareError::SetupYarnNodeLinker)?;\n        let mut map = serde_yaml::from_str::<serde_yaml::Mapping>(&contents).map_err(|err| {\n            PrepareError::SetupYarnNodeLinker(io::Error::new(io::ErrorKind::InvalidData, err))\n        })?;\n\n        // Modify the map and write it back out.\n        map.insert(\n            serde_yaml::Value::String(\"nodeLinker\".into()),\n            serde_yaml::Value::String(\"node-modules\".into()),\n        );\n        let contents = serde_yaml::to_string(&map).map_err(|err| {\n            PrepareError::SetupYarnNodeLinker(io::Error::new(io::ErrorKind::InvalidData, err))\n        })?;\n        std::fs::write(&file_path, contents).map_err(PrepareError::SetupYarnNodeLinker)?;\n\n        Ok(())\n    }\n}\n\nstruct PnpmPackageManager {\n    pkg_json: PackageJson,\n    dir: PathBuf,\n}\n\nimpl PackageManager for PnpmPackageManager {\n    fn setup_deps(&self, encore_dev_version: &PackageVersion) -> Result<(), PrepareError> {\n        // Install `encore.dev` if necessary\n        let installed = self.pkg_json.dependencies.get(\"encore.dev\");\n        let v = installed.map_or(InstalledVersion::NotInstalled, |v| {\n            encore_dev_version.is_installed(v, &self.dir)\n        });\n\n        // First ensure the package.json file is up to date.\n        let modified = update_package_json(&self.dir, v, encore_dev_version)?;\n\n        // If we modified anything or don't have a node_modules directory, run 'npm install'.\n        if modified || !self.dir.join(\"node_modules\").exists() {\n            cmd!(\"pnpm\", \"install\")\n                .dir(&self.dir)\n                .stdout_to_stderr()\n                .run()\n                .map_err(|e| PrepareError::InstallNodeModules(e, \"pnpm install\".into()))?;\n        }\n        Ok(())\n    }\n\n    fn run_tests(&self) -> CmdSpec {\n        CmdSpec {\n            command: vec![\"pnpm\".to_string(), \"run\".to_string(), \"test\".to_string()],\n            env: vec![],\n            prioritized_files: vec![],\n        }\n    }\n\n    fn mgr_name(&self) -> &'static str {\n        \"pnpm\"\n    }\n}\n\n/// Updates the package.json file in the given directory to include the specified version of encore.dev.\n/// Returns whether the package.json file was modified.\nfn update_package_json(\n    dir: &Path,\n    v: InstalledVersion,\n    encore_dev_version: &PackageVersion,\n) -> Result<bool, PrepareError> {\n    let path = dir.join(\"package.json\");\n    match v {\n        InstalledVersion::Newer(v) => Err(PrepareError::EncoreDevTooNew(v)),\n\n        InstalledVersion::Current => Ok(false),\n\n        InstalledVersion::Older(_)\n        | InstalledVersion::Different(_)\n        | InstalledVersion::NotInstalled => {\n            let ver = match encore_dev_version {\n                PackageVersion::Local(buf) => format!(\"file:{}\", buf.to_string_lossy()),\n                PackageVersion::Published(ver) => format!(\"^{ver}\"),\n            };\n\n            // Update package.json.\n            {\n                let data = std::fs::read_to_string(&path).map_err(PrepareError::ReadPackageJson)?;\n                let mut data: serde_json::Map<String, serde_json::Value> =\n                    serde_json::from_str(&data).map_err(|source| {\n                        PrepareError::InvalidPackageJson {\n                            source,\n                            path: path.clone(),\n                        }\n                    })?;\n\n                // Update the \"encore.dev\" key in the \"dependencies\" map.\n                {\n                    let Some(deps) = data\n                        .entry(\"dependencies\")\n                        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()))\n                        .as_object_mut()\n                    else {\n                        return Err(PrepareError::ReadPackageJson(io::Error::new(\n                            io::ErrorKind::InvalidData,\n                            \"dependencies field is not an object\",\n                        )));\n                    };\n                    deps.insert(\"encore.dev\".to_string(), ver.into());\n                }\n\n                // Write it back out.\n                let data = serde_json::to_string_pretty(&data).map_err(|source| {\n                    PrepareError::InvalidPackageJson {\n                        source,\n                        path: path.clone(),\n                    }\n                })?;\n                std::fs::write(&path, &data).map_err(PrepareError::WritePackageJson)?;\n            }\n\n            Ok(true)\n        }\n    }\n}\n\n#[derive(Deserialize)]\nstruct PackageJson {\n    #[serde(default, rename = \"packageManager\")]\n    package_manager: Option<String>,\n\n    #[serde(default)]\n    dependencies: HashMap<String, String>,\n}\n\nimpl FromStr for PackageJson {\n    type Err = serde_json::Error;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        serde_json::from_str(s)\n    }\n}\n\nimpl PackageJson {\n    pub fn parse_file(path: &Path) -> Result<Self, PrepareError> {\n        let data = std::fs::read_to_string(path).map_err(PrepareError::ReadPackageJson)?;\n        data.parse()\n            .map_err(|source| PrepareError::InvalidPackageJson {\n                source,\n                path: path.to_path_buf(),\n            })\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/parse.rs",
    "content": "use std::fmt::Display;\nuse std::path::Path;\n\nuse crate::app::{validate_and_describe, AppDesc};\nuse crate::parser::parser::{ParseContext, Parser};\nuse crate::parser::resourceparser::PassOneParser;\n\nuse super::{App, Builder};\n\n#[derive(Debug, Clone)]\npub struct ParseParams<'a> {\n    pub app: &'a App,\n    pub pc: &'a ParseContext,\n    pub working_dir: &'a Path,\n    pub parse_tests: bool,\n}\n\n#[derive(Debug)]\npub struct ParseError;\n\nimpl Display for ParseError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"failed to parse TypeScript code\")\n    }\n}\n\nimpl Builder<'_> {\n    pub fn parse(&self, params: &ParseParams) -> Option<AppDesc> {\n        let pc = params.pc;\n        let pass1 = PassOneParser::new(\n            pc.file_set.clone(),\n            pc.type_checker.clone(),\n            Default::default(),\n        );\n        let parser = Parser::new(pc, pass1);\n\n        let result = parser.parse();\n        let desc = validate_and_describe(pc, result)?;\n\n        if pc.errs.has_errors() {\n            None\n        } else {\n            Some(desc)\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/prepare.rs",
    "content": "use std::{io, path::PathBuf};\n\nuse thiserror::Error;\n\nuse super::Builder;\n\n#[derive(Debug, Clone)]\npub struct PrepareParams {\n    pub encore_dev_version: PackageVersion,\n    pub app_root: PathBuf,\n}\n\n#[derive(Debug, Clone)]\npub enum PackageVersion {\n    Local(PathBuf),\n    Published(String),\n}\n\n#[derive(Debug, Error)]\npub enum PrepareError {\n    #[error(\"package.json file not found (expected at {0})\")]\n    PackageJsonNotFound(PathBuf),\n    #[error(\"failed to read package.json: {0}\")]\n    ReadPackageJson(#[source] io::Error),\n    #[error(\"failed to update package.json: {0}\")]\n    WritePackageJson(#[source] io::Error),\n    #[error(\"invalid package.json: {source}\")]\n    InvalidPackageJson {\n        source: serde_json::Error,\n        path: PathBuf,\n    },\n    #[error(\"package manager '{0}' not supported\")]\n    UnsupportedPackageManagerError(String),\n\n    #[error(\n        \"installed 'encore.dev' package version ({0}) newer than 'encore' release, run 'encore version update' first\"\n    )]\n    EncoreDevTooNew(String),\n\n    #[error(\"installing node_modules failed: {0}, run '{1}' manually to see the error\")]\n    InstallNodeModules(#[source] io::Error, String),\n\n    #[error(\"failed to install 'encore.dev' package: {0}\")]\n    InstallEncoreDev(#[source] io::Error),\n\n    #[error(\"failed to configure yarn nodeLinker to node-modules: {0}\")]\n    SetupYarnNodeLinker(#[source] io::Error),\n\n    #[error(\"node_modules directory not found\")]\n    NodeModulesNotFound,\n\n    #[error(\"unable to generate code\")]\n    GenerateCode(#[source] io::Error),\n\n    #[error(\"internal error: {0}\")]\n    Internal(#[source] anyhow::Error),\n}\n\nimpl Builder<'_> {\n    pub fn prepare(&self, params: &PrepareParams) -> Result<(), PrepareError> {\n        self.setup_deps(&params.app_root, &params.encore_dev_version)\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/auth/auth_ts.handlebars",
    "content": "{{#if auth_handlers}}\n{{#each auth_handlers}}\nimport { getAuthData as _getAuthData } from \"encore.dev/internal/codegen/auth\";\nimport { {{name}} as _auth_{{name}} } from {{toJSON import_path}};\n{{/each}}\n\nexport type AuthData = {{#each auth_handlers }}{{#unless @first}}\n    | {{/unless}}Awaited<ReturnType<typeof _auth_{{name}}>>{{/each}};\n\nexport function getAuthData(): AuthData | null {\n    return _getAuthData()\n}\n\n{{else}}\nexport type AuthData = null;\n\n// getAuthData throws an error until an auth handler is added\nexport function getAuthData(): AuthData | null {\n    throw new Error(\"authData cannot be called when there are no auth handlers.\")\n}\n{{/if}}\ndeclare module \"encore.dev/api\" {\n  interface CallOpts {\n    authData?: AuthData;\n  }\n}\n\n"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/auth/index_ts.handlebars",
    "content": "export {type AuthData, getAuthData} from \"../internal/auth/auth\";"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/clients/endpoints_d_ts.handlebars",
    "content": "import { CallOpts } from \"encore.dev/api\";\n\n{{#if has_streams}}\nimport {\n  StreamInOutHandlerFn,\n  StreamInHandlerFn,\n  StreamOutHandlerFn,\n  StreamOutWithResponse,\n  StreamIn,\n  StreamInOut,\n} from \"encore.dev/api\";\n\n{{#each endpoints}}\n{{#if (or streaming_request streaming_response)}}\nimport { {{name}} as {{name}}_handler } from {{toJSON import_path}};\n{{/if}}\n{{/each}}\n\ntype StreamHandshake<Type extends (...args: any[]) => any> = Parameters<Type> extends [infer H, any] ? H : void;\n\ntype StreamRequest<Type> = Type extends\n  | StreamInOutHandlerFn<any, infer Req, any>\n  | StreamInHandlerFn<any, infer Req, any>\n  | StreamOutHandlerFn<any, any>\n  ? Req\n  : never;\n\ntype StreamResponse<Type> = Type extends\n  | StreamInOutHandlerFn<any, any, infer Resp>\n  | StreamInHandlerFn<any, any, infer Resp>\n  | StreamOutHandlerFn<any, infer Resp>\n  ? Resp\n  : never;\n\n{{/if}}\ntype Parameters<T> = T extends (...args: infer P) => unknown ? P : never;\ntype WithCallOpts<T extends (...args: any) => any> = (\n  ...args: [...Parameters<T>, opts?: CallOpts]\n) => ReturnType<T>;\n\n{{#each endpoints}}\n{{#if (or streaming_request streaming_response)~}}\n\n{{#if (and streaming_request streaming_response)}}\ntype {{name}}_Type = (\n  ...args: StreamHandshake<typeof {{name}}_handler> extends void\n    ? [opts?: CallOpts]\n    : [data: StreamHandshake<typeof {{name}}_handler>, opts?: CallOpts]\n) => Promise<\n  StreamInOut<\n    StreamResponse<typeof {{name}}_handler>,\n    StreamRequest<typeof {{name}}_handler>\n  >\n>;\n{{else}}\n{{#if streaming_request}}\ntype {{name}}_Type = (\n  ...args: StreamHandshake<typeof {{name}}_handler> extends void\n    ? [opts?: CallOpts]\n    : [data: StreamHandshake<typeof {{name}}_handler>, opts?: CallOpts]\n) => Promise<\n  StreamOutWithResponse<\n    StreamRequest<typeof {{name}}_handler>,\n    StreamResponse<typeof {{name}}_handler>\n  >\n>;\n{{/if}}\n{{#if streaming_response}}\ntype {{name}}_Type = (\n  ...args: StreamHandshake<typeof {{name}}_handler> extends void\n    ? [opts?: CallOpts]\n    : [data: StreamHandshake<typeof {{name}}_handler>, opts?: CallOpts]\n) => Promise<\n  StreamIn<\n    StreamResponse<typeof {{name}}_handler>\n  >\n>;\n{{/if}}\n{{/if}}\n\nexport declare const {{name}}: {{name}}_Type;\n\n{{~else}}\nimport { {{name}} as {{name}}_handler } from {{toJSON import_path}};\ntype {{name}}_Type = WithCallOpts<typeof {{name}}_handler>;\ndeclare const {{name}}: {{name}}_Type;\nexport { {{name}} };\n\n{{/if}}\n{{/each}}\n\nexport class Client {\n  private constructor();\n\n{{#each endpoints}}\n  readonly {{name}}: {{name}}_Type;\n{{/each}}\n}\n\nexport declare function ref(): Client;\n"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/clients/endpoints_js.handlebars",
    "content": "import { apiCall, streamIn, streamOut, streamInOut } from \"encore.dev/internal/codegen/api\";\n\nconst TEST_ENDPOINTS = typeof ENCORE_DROP_TESTS === \"undefined\" && process.env.NODE_ENV === \"test\"\n    ? await import(\"./endpoints_testing.js\")\n    : null;\n\n{{#each endpoints}}\n{{#if has_params}}\nexport async function {{name}}(params, opts) {\n{{else}}\nexport async function {{name}}(opts) {\n    const params = undefined;\n{{/if}}\n    if (typeof ENCORE_DROP_TESTS === \"undefined\" && process.env.NODE_ENV === \"test\") {\n        return TEST_ENDPOINTS.{{name}}(params, opts);\n    }\n\n    {{#if (or streaming_request streaming_response)}}\n    {{#if (and streaming_request streaming_response)}}\n    return streamInOut(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{else}}\n    {{#if streaming_request}}\n    return streamIn(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{else}}\n    return streamOut(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{/if}}\n    {{/if}}\n    {{else}}\n    return apiCall(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{/if}}\n}\n{{/each}}\n\nexport class Client {\n  constructor() {\n{{#each endpoints}}\n    this.{{name}} = {{name}};\n{{/each}}\n  }\n}\n\nlet _client_instance;\n\nexport function ref() {\n  if (!_client_instance) {\n    _client_instance = new Client();\n  }\n  return _client_instance;\n}\n"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/clients/endpoints_testing_js.handlebars",
    "content": "import { apiCall, streamIn, streamOut, streamInOut } from \"encore.dev/internal/codegen/api\";\nimport { registerTestHandler } from \"encore.dev/internal/codegen/appinit\";\n\n{{#with service}}\n{{#if import_path}}\nimport * as {{encoreNameToIdent name}}_service from {{toJSON import_path}};\n{{/if}}\n{{/with}}\n\n{{#each endpoints}}\nexport async function {{name}}(params, opts) {\n    const handler = (await import({{toJSON (stripExt import_path)}})).{{name}};\n    registerTestHandler({\n        apiRoute: { service: \"{{../name}}\", name: \"{{name}}\", raw: {{toJSON raw}}, handler, streamingRequest: {{ streaming_request }}, streamingResponse: {{ streaming_response }} },\n        middlewares: {{#if ../service.import_path}}{{encoreNameToIdent ../service.name}}_service.default.cfg.middlewares || {{/if}}[],\n        endpointOptions: {{toJSON endpoint_options}},\n    });\n\n    {{#if (or streaming_request streaming_response)}}\n    {{#if (and streaming_request streaming_response)}}\n    return streamInOut(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{else}}\n    {{#if streaming_request}}\n    return streamIn(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{else}}\n    return streamOut(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{/if}}\n    {{/if}}\n    {{else}}\n    return apiCall(\"{{../name}}\", \"{{name}}\", params, opts);\n    {{/if}}\n}\n\n{{/each}}\n"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/clients/index_d_ts.handlebars",
    "content": "{{#each services}}\nexport * as {{encoreNameToIdent name}} from \"../internal/clients/{{name}}/endpoints\";\n{{/each}}\n"
  },
  {
    "path": "tsparser/src/builder/templates/catalog/clients/index_js.handlebars",
    "content": "{{#each services}}\nexport * as {{encoreNameToIdent name}} from \"../internal/clients/{{name}}/endpoints.js\";\n{{/each}}\n"
  },
  {
    "path": "tsparser/src/builder/templates/entrypoints/combined/main.handlebars",
    "content": "import { registerGateways, registerHandlers, run, type Handler } from \"encore.dev/internal/codegen/appinit\";\n\n{{#each gateways}}\nimport { {{bind_name}} as {{encoreNameToIdent encore_name}}GW } from {{toJSON import_path}};\n{{/each}}\n{{#each endpoints}}\nimport { {{name}} as {{encoreNameToIdent service_name}}_{{name}}Impl{{@index}} } from {{toJSON import_path}};\n{{/each}}\n{{#each subscriptions}}\nimport {{toJSON import_path}};\n{{/each}}\n{{#each services}}\n{{#if import_path}}\nimport * as {{encoreNameToIdent name}}_service from {{toJSON import_path}};\n{{/if}}\n{{/each}}\n{{#each metrics}}\nimport { {{name}} as metric{{@index}} } from {{toJSON import_path}};\n{{/each}}\n\n\nconst gateways: any[] = [\n{{#each gateways}}\n    {{encoreNameToIdent encore_name}}GW,\n{{/each}}\n];\n\nconst handlers: Handler[] = [\n{{#each endpoints}}\n    {\n        apiRoute: {\n            service:           {{toJSON service_name}},\n            name:              {{toJSON name}},\n            handler:           {{encoreNameToIdent service_name}}_{{name}}Impl{{@index}},\n            raw:               {{toJSON raw}},\n            streamingRequest:  {{toJSON streaming_request}},\n            streamingResponse: {{toJSON streaming_response}},\n        },\n        endpointOptions: {{toJSON endpoint_options}},\n        middlewares: {{#with (lookup (lookup ../services service_name) \"import_path\")}}{{encoreNameToIdent ../service_name}}_service.default.cfg.middlewares || {{/with}}[],\n    },\n{{/each}}\n];\n\nregisterGateways(gateways);\nregisterHandlers(handlers);\n\nawait run(import.meta.url);\n"
  },
  {
    "path": "tsparser/src/builder/templates/entrypoints/gateways/main.handlebars",
    "content": "import { registerGateways, run } from \"encore.dev/internal/codegen/appinit\";\nimport { Worker, isMainThread } from \"node:worker_threads\";\nimport { fileURLToPath } from \"node:url\";\nimport { availableParallelism } from \"node:os\";\n\n{{#each gateways}}\nimport { {{bind_name}} as {{encoreNameToIdent encore_name}}Impl } from {{toJSON import_path}};\n{{/each}}\n\nconst gateways = [\n{{#each gateways}}\n    {{encoreNameToIdent encore_name}}Impl,\n{{/each}}\n];\n\nregisterGateways(gateways);\n\nawait run(import.meta.url);\n"
  },
  {
    "path": "tsparser/src/builder/templates/entrypoints/services/main.handlebars",
    "content": "import { registerHandlers, run, type Handler } from \"encore.dev/internal/codegen/appinit\";\nimport { Worker, isMainThread } from \"node:worker_threads\";\nimport { fileURLToPath } from \"node:url\";\nimport { availableParallelism } from \"node:os\";\n\n{{#each endpoints}}\nimport { {{name}} as {{name}}Impl{{@index}} } from {{toJSON import_path}};\n{{/each}}\n{{#each subscriptions}}\nimport {{toJSON import_path}};\n{{/each}}\n{{#with service}}\n{{#if import_path}}\nimport * as {{encoreNameToIdent name}}_service from {{toJSON import_path}};\n{{/if}}\n{{/with}}\n{{#each metrics}}\nimport { {{name}} as metric{{@index}} } from {{toJSON import_path}};\n{{/each}}\n\nconst handlers: Handler[] = [\n{{#each endpoints}}\n    {\n        apiRoute: {\n            service:           {{toJSON ../name}},\n            name:              {{toJSON name}},\n            handler:           {{name}}Impl{{@index}},\n            raw:               {{toJSON raw}},\n            streamingRequest:  {{toJSON streaming_request}},\n            streamingResponse: {{toJSON streaming_response}},\n        },\n        endpointOptions: {{toJSON endpoint_options}},\n        middlewares: {{#if ../service.import_path}}{{encoreNameToIdent ../service.name}}_service.default.cfg.middlewares || {{/if}}[],\n    },\n{{/each}}\n];\n\nregisterHandlers(handlers);\n\nawait run(import.meta.url);\n"
  },
  {
    "path": "tsparser/src/builder/test.rs",
    "content": "use std::collections::HashMap;\nuse std::fs;\nuse std::path::Path;\n\nuse crate::app::AppDesc;\nuse serde::{Deserialize, Serialize};\n\nuse crate::builder::codegen::CodegenParams;\nuse crate::builder::compile::CmdSpec;\nuse crate::builder::package_mgmt::resolve_package_manager;\nuse crate::parser::parser::ParseContext;\n\nuse super::{prepare::PrepareError, App, Builder};\n\n#[derive(Debug)]\npub struct TestParams<'a> {\n    pub app: &'a App,\n    pub pc: &'a ParseContext,\n    pub working_dir: &'a Path,\n    pub parse: &'a AppDesc,\n}\n\n#[derive(Serialize, Debug)]\npub struct TestResult {\n    pub cmd: Option<CmdSpec>,\n}\n\nimpl Builder<'_> {\n    pub fn test(&self, params: &TestParams) -> Result<TestResult, PrepareError> {\n        // Is there a \"test\" script defined in package.json?\n        {\n            #[derive(Deserialize)]\n            struct PackageJson {\n                #[serde(default)]\n                scripts: HashMap<String, String>,\n            }\n            let package_json_path = params.app.root.join(\"package.json\");\n            let package_json =\n                fs::read_to_string(&package_json_path).map_err(PrepareError::ReadPackageJson)?;\n            let package_json: PackageJson =\n                serde_json::from_str(&package_json).map_err(|source| {\n                    PrepareError::InvalidPackageJson {\n                        source,\n                        path: package_json_path.to_path_buf(),\n                    }\n                })?;\n            if !package_json.scripts.contains_key(\"test\") {\n                log::info!(\"no test script defined in package.json, skipping tests\");\n                return Ok(TestResult { cmd: None });\n            }\n        }\n\n        self.generate_code(&CodegenParams {\n            app: params.app,\n            pc: params.pc,\n            working_dir: params.working_dir,\n            desc: params.parse,\n        })?;\n\n        let pkg_mgr = resolve_package_manager(&params.app.root)?;\n        let cmd = pkg_mgr.run_tests();\n        Ok(TestResult { cmd: Some(cmd) })\n    }\n}\n"
  },
  {
    "path": "tsparser/src/builder/transpiler.rs",
    "content": "use std::ffi::OsStr;\nuse std::path::{Path, PathBuf};\n\nuse crate::builder::compile::{CmdSpec, Entrypoint};\nuse crate::builder::{DebugMode, PlainError};\nuse anyhow::{Context, Result};\n\nuse super::NodeJSRuntime;\n\n#[allow(dead_code)]\npub enum ExternalPackages<'a> {\n    All,\n    Some(&'a [&'a str]),\n    None,\n}\n\n#[allow(dead_code)]\npub enum InputKind {\n    Combined(Vec<String>, Vec<String>),\n    Service(String),\n    Gateway(String),\n}\n\npub struct Input {\n    // What kind of input is it.\n    pub kind: InputKind,\n\n    /// The path to the pre-compiled entrypoint of the service.\n    pub entrypoint: PathBuf,\n}\n\npub struct TranspileParams<'a> {\n    /// Where the built artifacts should be written.\n    pub artifact_dir: &'a Path,\n\n    /// The working directory to use to run the transpiler,\n    /// for generating useful error messages.\n    pub cwd: &'a Path,\n\n    /// The services and gateways to transpile.\n    pub inputs: Vec<Input>,\n\n    // If we should transpile for debug\n    pub debug: DebugMode,\n\n    // What runtime should be used as entrypoint\n    pub nodejs_runtime: NodeJSRuntime,\n}\n\npub struct TranspileResult {\n    /// The compiled entrypoints.\n    pub entrypoints: Vec<Entrypoint>,\n}\n\npub(super) trait OutputTranspiler {\n    fn transpile(&self, params: TranspileParams) -> Result<TranspileResult>;\n}\n\n#[allow(dead_code)]\npub struct EsbuildCompiler<'a> {\n    pub node_modules_dir: &'a Path,\n    pub external: ExternalPackages<'a>,\n}\n\nimpl OutputTranspiler for EsbuildCompiler<'_> {\n    fn transpile(&self, p: TranspileParams) -> Result<TranspileResult> {\n        let bundle = move |inputs: Vec<Input>, name_prefix| -> Result<Vec<Entrypoint>> {\n            // let program = self.node_modules_dir.join(\".bin\").join(\"esbuild\");\n            let tsbundler_path = std::env::var(\"ENCORE_TSBUNDLER_PATH\")\n                .ok()\n                .unwrap_or(\"tsbundler-encore\".into());\n\n            let mut cmd = std::process::Command::new(tsbundler_path);\n            cmd.current_dir(p.cwd);\n            cmd.arg(\"--bundle\")\n                .arg(\"--engine=node:21\")\n                // .arg(\"--format=esm\")\n                // .arg(\"--platform=node\")\n                // .arg(\"--sourcemap\")\n                // .arg(\"--out-extension:.js=.mjs\")\n                // .arg(\"--entry-names=[dir]/[name]\")\n                .arg(format!(\n                    \"--outdir={}\",\n                    p.artifact_dir.join(name_prefix).to_string_lossy(),\n                ));\n\n            for input in &inputs {\n                cmd.arg(&input.entrypoint);\n            }\n\n            log::info!(\"running tsbundler-encore: {:?}\", cmd);\n            let output = cmd.output().context(\"failed to tsbundler-encore\")?;\n            if !output.status.success() {\n                anyhow::bail!(PlainError(\n                    String::from_utf8_lossy(&output.stderr).to_string()\n                ));\n            }\n            log::info!(\"successfully bundled\");\n\n            let mut entrypoints = Vec::new();\n            for i in inputs {\n                let entrypoint_path = {\n                    let (file_stem, dir) = file_stem_and_dir(&i.entrypoint)?;\n                    format!(\n                        \"$ARTIFACT_DIR/{}/{}/{}.mjs\",\n                        name_prefix,\n                        dir.to_string_lossy(),\n                        file_stem.to_string_lossy(),\n                    )\n                };\n\n                let mut command = match p.nodejs_runtime {\n                    NodeJSRuntime::NodeJS => vec![\"node\".into(), \"--enable-source-maps\".into()],\n                    NodeJSRuntime::Bun => vec![\"bun\".into(), \"run\".into()],\n                };\n\n                match p.debug {\n                    DebugMode::Disabled => {}\n                    DebugMode::Enabled => command.push(\"--inspect\".to_string()),\n                    DebugMode::Break => command.push(\"--inspect-brk\".to_string()),\n                }\n\n                // Finally we want to add the path to the bundled app\n                command.push(entrypoint_path.clone());\n\n                let (services, gateways) = match i.kind {\n                    InputKind::Service(name) => (vec![name], vec![]),\n                    InputKind::Gateway(name) => (vec![], vec![name]),\n                    InputKind::Combined(gateways, services) => (services, gateways),\n                };\n                entrypoints.push(Entrypoint {\n                    cmd: CmdSpec {\n                        command,\n                        env: vec![],\n                        prioritized_files: vec![entrypoint_path],\n                    },\n                    services,\n                    gateways,\n                    use_runtime_config_v2: true,\n                });\n            }\n\n            Ok(entrypoints)\n        };\n\n        let (service_inputs, gateway_inputs, combined_inputs) = {\n            let mut service_inputs = Vec::new();\n            let mut gateway_inputs = Vec::new();\n            let mut combined_inputs = Vec::new();\n            for i in p.inputs {\n                match i.kind {\n                    InputKind::Service(_) => service_inputs.push(i),\n                    InputKind::Gateway(_) => gateway_inputs.push(i),\n                    InputKind::Combined(_, _) => {\n                        combined_inputs.push(i);\n                    }\n                }\n            }\n            (service_inputs, gateway_inputs, combined_inputs)\n        };\n\n        if !combined_inputs.is_empty() {\n            let entrypoints = bundle(combined_inputs, \"combined\")?;\n            Ok(TranspileResult { entrypoints })\n        } else {\n            let services = bundle(service_inputs, \"services\")?;\n            let gateways = bundle(gateway_inputs, \"gateways\")?;\n            let entrypoints = services.into_iter().chain(gateways).collect();\n\n            Ok(TranspileResult { entrypoints })\n        }\n    }\n}\n\nfn file_stem_and_dir(p: &Path) -> Result<(&OsStr, &OsStr)> {\n    let file_stem = p.file_stem().context(\"no file name in entrypoint\")?;\n    let dir_name = p\n        .parent()\n        .and_then(|parent| parent.file_name())\n        .context(\"no parent\")?;\n    Ok((file_stem, dir_name))\n}\n"
  },
  {
    "path": "tsparser/src/exports.rs",
    "content": "use std::collections::HashSet;\nuse std::fmt;\nuse std::path::PathBuf;\n\nuse indexmap::IndexMap;\nuse serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};\n\n/// The parsed representation of the \"exports\" field in a package.json file.\n/// See https://nodejs.org/api/packages.html#package-entry-points for syntax.\n#[derive(Debug)]\npub struct Exports {\n    subpaths: IndexMap<String, Subpath>,\n}\n\nimpl Exports {\n    /// Resolves a relative path to a target file path.\n    pub fn resolve_import_path(\n        &self,\n        rel_path: &str,\n        conditions: &HashSet<&str>,\n    ) -> Option<PathBuf> {\n        let mut wildcard_match = None;\n        for (candidate, subpath) in self.subpaths.iter() {\n            match candidate_matches(candidate, rel_path) {\n                None => continue,\n                Some(Match::Exact) => {\n                    return subpath.matches(conditions).and_then(|m| match m {\n                        SubpathMatch::Target(path) => Some(path.into()),\n                        SubpathMatch::Exclude => None,\n                    });\n                }\n                Some(Match::Wildcard { replacement }) => {\n                    match subpath.matches(conditions) {\n                        None => continue,\n\n                        // If we have a target, save it as a candidate.\n                        // We have to keep looking as there may be an exclude directive later.\n                        Some(SubpathMatch::Target(path)) => {\n                            if wildcard_match.is_none() {\n                                wildcard_match = Some((path, replacement));\n                            }\n                        }\n\n                        // If we have an exclude, stop looking and immediately return none.\n                        Some(SubpathMatch::Exclude) => return None,\n                    }\n                }\n            }\n        }\n\n        wildcard_match.map(|(path, replacement)| path.replace('*', replacement).into())\n    }\n}\n\nenum Match<'a> {\n    Exact,\n    Wildcard { replacement: &'a str },\n}\n\nfn candidate_matches<'a>(candidate: &str, rel_path: &'a str) -> Option<Match<'a>> {\n    let candidate = match (candidate, rel_path) {\n        (\".\", \"\") => return Some(Match::Exact),\n        (\".\", _) => return None,\n        (_, _) => {\n            // Strip \"./\" prefix.\n            let candidate = candidate.strip_prefix(\"./\").unwrap_or(candidate);\n            if candidate == rel_path {\n                return Some(Match::Exact);\n            }\n            candidate\n        }\n    };\n\n    if let Some(idx) = candidate.find('*') {\n        let (prefix, suffix) = candidate.split_at(idx);\n        if rel_path.starts_with(prefix) && rel_path.ends_with(suffix) {\n            // Get the middle part of the path, to be injected into the result.\n            let replacement = &rel_path[prefix.len()..(rel_path.len() - suffix.len())];\n            return Some(Match::Wildcard { replacement });\n        }\n    }\n\n    None\n}\n\n#[derive(Debug, PartialEq, Eq)]\nenum Subpath {\n    Target(String),\n    Conditions(IndexMap<String, Subpath>),\n    Exclude,\n}\n\nenum SubpathMatch<'a> {\n    Target(&'a str),\n    Exclude,\n}\n\nimpl Subpath {\n    fn matches(&self, active_conditions: &HashSet<&str>) -> Option<SubpathMatch<'_>> {\n        match self {\n            Subpath::Target(path) => Some(SubpathMatch::Target(path)),\n            Subpath::Exclude => Some(SubpathMatch::Exclude),\n            Subpath::Conditions(conds) => {\n                for (cond, subpath) in conds.iter() {\n                    if active_conditions.contains(cond.as_str()) {\n                        return subpath.matches(active_conditions);\n                    }\n                }\n                None\n            }\n        }\n    }\n}\n\n// The \"exports\" value can be defined in lots of ways.\n//\n// - A single subpath, e.g. \"exports\": \"./index.js\".\n//   Syntactic sugar for {\".\": \"./index.js\"}\n//\n// - A condition spec: {\"node\": \"./index.node.js\"}\n//   Conditions can be nested: {\"node\": {\"import\": \"./index.node.js\", \"default\": \"./index.js\"}}\n//\n// - An ordered map of subpaths key-value pairs: {\".\": \"./index.js\", \"./foo/*\": \"./foo/*.js\"}\n// - Each subpath value can be one of:\n//   - A single target path, e.g. \"./index.js\" (possibly with wildcards, \"./foo/*.js\")\n//   - A condition spec.\n//   - Null, indicating the subpath is not exported (overriding other exports that may match).\n\nimpl<'de> Deserialize<'de> for Exports {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        struct StringOrMap;\n        impl<'de> Visitor<'de> for StringOrMap {\n            type Value = Exports;\n\n            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n                formatter.write_str(\"string or map\")\n            }\n\n            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>\n            where\n                E: de::Error,\n            {\n                let mut subpaths = IndexMap::<String, Subpath>::new();\n                subpaths.insert(\".\".into(), Subpath::Target(value.to_string()));\n                Ok(Exports { subpaths })\n            }\n\n            fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>\n            where\n                M: MapAccess<'de>,\n            {\n                let mut subpaths = IndexMap::new();\n\n                // Peek at the first entry to decide whether it's a map of subpaths or conditions.\n                let Some((key, value)) = access.next_entry::<String, Subpath>()? else {\n                    // Empty map.\n                    return Ok(Exports { subpaths });\n                };\n\n                if !key.starts_with('.') {\n                    let mut conditions: IndexMap<String, Subpath> = IndexMap::new();\n                    conditions.insert(key, value);\n                    let rest: IndexMap<String, Subpath> =\n                        Deserialize::deserialize(de::value::MapAccessDeserializer::new(access))?;\n                    conditions.extend(rest);\n                    subpaths.insert(\".\".to_string(), Subpath::Conditions(conditions));\n                    return Ok(Exports { subpaths });\n                }\n\n                subpaths.insert(key, value);\n                while let Some((key, value)) = access.next_entry::<String, Subpath>()? {\n                    subpaths.insert(key, value);\n                }\n\n                Ok(Exports { subpaths })\n            }\n        }\n\n        deserializer.deserialize_any(StringOrMap)\n    }\n}\n\nstruct SubpathVisitor;\n\nimpl<'de> Visitor<'de> for SubpathVisitor {\n    type Value = Subpath;\n\n    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n        formatter.write_str(\"string or map\")\n    }\n\n    fn visit_unit<E>(self) -> Result<Self::Value, E>\n    where\n        E: de::Error,\n    {\n        Ok(Subpath::Exclude)\n    }\n\n    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>\n    where\n        E: de::Error,\n    {\n        Ok(Subpath::Target(value.to_string()))\n    }\n\n    fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>\n    where\n        M: MapAccess<'de>,\n    {\n        let conditions: IndexMap<String, Subpath> =\n            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;\n        Ok(Subpath::Conditions(conditions))\n    }\n}\n\nimpl<'de> Deserialize<'de> for Subpath {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: Deserializer<'de>,\n    {\n        deserializer.deserialize_any(SubpathVisitor)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn parse_subpaths() {\n        let exports: Exports = serde_json::from_str(\n            r#\"{\n            \".\": \"./index.js\",\n            \"./foo/*\": \"./foo/*.js\",\n            \"./bar\": {\"node\": \"./bar.node.js\", \"default\": \"./bar.js\"},\n            \"./baz\": {\"node\": \"./baz.node.js\", \"default\": null},\n            \"./qux\": null\n        }\"#,\n        )\n        .unwrap();\n        assert_eq!(exports.subpaths.len(), 5);\n        assert_eq!(\n            exports.subpaths.get(\".\").unwrap(),\n            &Subpath::Target(\"./index.js\".into())\n        );\n        assert_eq!(\n            exports.subpaths.get(\"./foo/*\").unwrap(),\n            &Subpath::Target(\"./foo/*.js\".into())\n        );\n        assert_eq!(\n            exports.subpaths.get(\"./bar\").unwrap(),\n            &Subpath::Conditions({\n                let mut map = IndexMap::new();\n                map.insert(\"node\".to_owned(), Subpath::Target(\"./bar.node.js\".into()));\n                map.insert(\"default\".to_owned(), Subpath::Target(\"./bar.js\".into()));\n                map\n            })\n        );\n        assert_eq!(\n            exports.subpaths.get(\"./baz\").unwrap(),\n            &Subpath::Conditions({\n                let mut map = IndexMap::new();\n                map.insert(\"node\".to_owned(), Subpath::Target(\"./baz.node.js\".into()));\n                map.insert(\"default\".to_owned(), Subpath::Exclude);\n                map\n            })\n        );\n        assert_eq!(exports.subpaths.get(\"./qux\").unwrap(), &Subpath::Exclude);\n    }\n\n    #[test]\n    fn parse_toplevel_conditions() {\n        let exports: Exports = serde_json::from_str(\n            r#\"{\n            \"node\": {\"import\": \"./bar.node.js\", \"default\": \"./bar.js\"},\n            \"default\": \"./index.js\"\n        }\"#,\n        )\n        .unwrap();\n        assert_eq!(exports.subpaths.len(), 1);\n        assert_eq!(\n            exports.subpaths.get(\".\").unwrap(),\n            &Subpath::Conditions({\n                let mut map = IndexMap::new();\n                map.insert(\n                    \"node\".to_owned(),\n                    Subpath::Conditions({\n                        let mut map = IndexMap::new();\n                        map.insert(\"import\".to_owned(), Subpath::Target(\"./bar.node.js\".into()));\n                        map.insert(\"default\".to_owned(), Subpath::Target(\"./bar.js\".into()));\n                        map\n                    }),\n                );\n                map.insert(\"default\".to_owned(), Subpath::Target(\"./index.js\".into()));\n                map\n            })\n        );\n    }\n\n    #[test]\n    fn toplevel_condition_priority_order() {\n        let exports: Exports = serde_json::from_str(\n            r#\"{\n            \"types\": \"./dist/index.d.ts\",\n            \"import\": \"./dist/index.mjs\",\n            \"default\": \"./dist/index.js\"\n        }\"#,\n        )\n        .unwrap();\n\n        let conditions: HashSet<&str> = [\"types\", \"import\", \"default\"].into_iter().collect();\n        let resolved = exports.resolve_import_path(\"\", &conditions).unwrap();\n        // \"types\" must win because it appears first in the JSON.\n        assert_eq!(resolved, PathBuf::from(\"./dist/index.d.ts\"));\n    }\n}\n"
  },
  {
    "path": "tsparser/src/legacymeta/api_schema.rs",
    "content": "use crate::parser::respath::Path;\nuse crate::parser::types::{FieldName, Interface};\n\npub(super) fn strip_path_params(path: &Path, typ: &mut Interface) {\n    if !path.has_dynamic_segments() {\n        return;\n    }\n\n    let is_path_param = |name: &str| path.dynamic_segments().any(|seg| seg.lit_or_name() == name);\n\n    // Drop any fields whose type is Path.\n    typ.fields.retain(|f| {\n        if let FieldName::String(name) = &f.name {\n            if is_path_param(name) {\n                return false;\n            }\n        }\n        true\n    });\n}\n"
  },
  {
    "path": "tsparser/src/legacymeta/mod.rs",
    "content": "use std::collections::{HashMap, HashSet};\nuse std::path::Path;\nuse std::rc::Rc;\n\nuse convert_case::{Case, Casing};\nuse swc_common::errors::HANDLER;\n\nuse crate::encore::parser::meta::v1::{self, selector, Selector};\nuse crate::encore::parser::schema::v1::Builtin;\nuse crate::legacymeta::schema::{loc_from_range, SchemaBuilder};\nuse crate::parser::parser::{ParseContext, ParseResult, Service};\nuse crate::parser::resourceparser::bind::{Bind, BindKind};\nuse crate::parser::resources::apis::{authhandler, gateway};\nuse crate::parser::resources::infra::cron::CronJobSchedule;\nuse crate::parser::resources::infra::metrics::MetricType;\nuse crate::parser::resources::infra::pubsub_topic::TopicOperation;\nuse crate::parser::resources::infra::{\n    cache, cron, objects, pubsub_subscription, pubsub_topic, sqldb,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::types::{validation, Basic, FieldName, Literal, Type};\nuse crate::parser::types::{Object, ObjectId};\nuse crate::parser::usageparser::Usage;\nuse crate::parser::{respath, FilePath, Range};\nuse litparser::{ParseResult as PResult, ToParseErr};\n\nmod api_schema;\nmod schema;\n\nconst DEFAULT_API_GATEWAY_NAME: &str = \"api-gateway\";\n\npub fn compute_meta(pc: &ParseContext, parse: &ParseResult) -> PResult<v1::Data> {\n    let app_root = pc.app_root.as_path();\n\n    let schema = SchemaBuilder::new(pc, app_root);\n    MetaBuilder {\n        pc,\n        schema,\n        parse,\n        app_root,\n        data: new_meta(),\n    }\n    .build()\n}\n\nstruct MetaBuilder<'a> {\n    pc: &'a ParseContext,\n    schema: SchemaBuilder<'a>,\n    parse: &'a ParseResult,\n    app_root: &'a Path,\n\n    data: v1::Data,\n}\n\nimpl MetaBuilder<'_> {\n    pub fn build(mut self) -> PResult<v1::Data> {\n        // self.data.app_revision = parse_app_revision(&self.app_root)?;\n        self.data.app_revision = std::env::var(\"ENCORE_APP_REVISION\").unwrap_or_default();\n\n        let mut svc_index = HashMap::new();\n        let mut svc_to_pkg_index = HashMap::new();\n        for svc in &self.parse.services {\n            let Some(rel_path) = self.rel_path_string(svc.root.as_path()) else {\n                HANDLER.with(|h| {\n                    h.err(&format!(\n                        \"unable to compute relative path to service: {}\",\n                        svc.name\n                    ))\n                });\n                continue;\n            };\n\n            svc_to_pkg_index.insert(svc.name.clone(), self.data.pkgs.len());\n            self.data.pkgs.push(v1::Package {\n                rel_path: rel_path.clone(),\n                name: svc.name.clone(),\n                service_name: svc.name.clone(),\n                doc: svc.doc.clone().unwrap_or_default(),\n\n                rpc_calls: vec![],   // added below\n                secrets: vec![],     // added below\n                trace_nodes: vec![], // TODO?\n            });\n\n            svc_index.insert(svc.name.clone(), self.data.svcs.len());\n            self.data.svcs.push(v1::Service {\n                name: svc.name.clone(),\n                rel_path,\n                rpcs: vec![],      // filled in later\n                databases: vec![], // filled in later\n                buckets: vec![],   // filled in later\n                metrics: vec![],   // filled in later\n                has_config: false, // TODO change when config is supported\n\n                // We no longer care about migrations in a service, so just set\n                // this to the empty array. The field is required for backwards compatibility.\n                migrations: vec![],\n            });\n        }\n\n        // Store resources that are dependent on other resources\n        // so we can do another pass to resolve them.\n        enum Dependent<'a> {\n            // Depends on topic objects\n            PubSubSubscription((&'a Bind, &'a pubsub_subscription::Subscription)),\n\n            // Depends on endpoint objects\n            CronJob((&'a Bind, &'a cron::CronJob)),\n\n            // Depends on auth handler objects\n            Gateway((&'a Bind, &'a gateway::Gateway)),\n\n            // Depends on cache cluster objects\n            CacheKeyspace(&'a cache::CacheKeyspace),\n        }\n\n        let mut dependent: Vec<Dependent> = Vec::new();\n        let mut topic_idx: HashMap<ObjectId, usize> = HashMap::new();\n        let mut endpoint_idx: HashMap<ObjectId, (usize, usize)> = HashMap::new();\n        let mut topic_by_name: HashMap<String, usize> = HashMap::new();\n\n        let mut auth_handlers: HashMap<ObjectId, Rc<authhandler::AuthHandler>> = HashMap::new();\n        let mut cache_cluster_idx: HashMap<ObjectId, usize> = HashMap::new();\n        let mut cache_cluster_by_name: HashMap<String, usize> = HashMap::new();\n\n        for b in &self.parse.binds {\n            if b.kind != BindKind::Create {\n                continue;\n            }\n\n            match &b.resource {\n                // Do nothing for these resources:\n                Resource::Service(_) => {}\n                Resource::ServiceClient(_) => {}\n\n                Resource::APIEndpoint(ep) => {\n                    let handshake_schema = self.schema.transform_handshake(ep)?;\n                    let request_schema = self.schema.transform_request(ep)?;\n                    let response_schema = self\n                        .schema\n                        .transform_response(ep.encoding.raw_resp_schema.clone().map(|s| s.take()))\n                        .map_err(|err| {\n                            let sp = ep\n                                .encoding\n                                .raw_resp_schema\n                                .as_ref()\n                                .map_or(ep.range.to_span(), |s| s.span());\n                            sp.parse_err(err.to_string())\n                        })?;\n\n                    let access_type: i32 = match (ep.expose, ep.require_auth) {\n                        (false, _) => v1::rpc::AccessType::Private as i32,\n                        (true, false) => v1::rpc::AccessType::Public as i32,\n                        (true, true) => v1::rpc::AccessType::Auth as i32,\n                    };\n\n                    let static_assets = ep\n                        .static_assets\n                        .as_ref()\n                        .map(|sa| -> PResult<v1::rpc::StaticAssets> {\n                            let dir_rel_path = self.rel_path_string(&sa.dir).ok_or(\n                                sa.dir.parse_err(\"could not resolve static asset directory\"),\n                            )?;\n                            let not_found_rel_path = sa\n                                .not_found\n                                .as_ref()\n                                .map(|p| {\n                                    self.rel_path_string(p).ok_or(\n                                        p.parse_err(\"could not resolve static notFound path\"),\n                                    )\n                                })\n                                .transpose()?;\n\n                            // Convert headers to protobuf format\n                            let headers = sa\n                                .headers\n                                .as_ref()\n                                .map(|h| {\n                                    h.iter()\n                                        .map(|(k, v)| {\n                                            (\n                                                k.clone(),\n                                                v1::rpc::static_assets::HeaderValues {\n                                                    values: v.clone(),\n                                                },\n                                            )\n                                        })\n                                        .collect()\n                                })\n                                .unwrap_or_default();\n\n                            Ok(v1::rpc::StaticAssets {\n                                dir_rel_path,\n                                not_found_rel_path,\n                                not_found_status: sa.not_found_status,\n                                headers,\n                            })\n                        })\n                        .transpose()?;\n\n                    let tags = ep\n                        .tags\n                        .iter()\n                        .map(|tag| Selector {\n                            r#type: selector::Type::Tag.into(),\n                            value: tag.clone(),\n                        })\n                        .collect();\n\n                    let rpc = v1::Rpc {\n                        name: ep.name.clone(),\n                        doc: ep.doc.clone(),\n                        service_name: ep.service_name.clone(),\n                        access_type,\n                        handshake_schema,\n                        request_schema,\n                        response_schema,\n                        proto: if ep.raw {\n                            v1::rpc::Protocol::Raw\n                        } else {\n                            v1::rpc::Protocol::Regular\n                        } as i32,\n                        path: Some(ep.encoding.path.to_meta()),\n                        http_methods: ep.encoding.methods.to_vec(),\n                        tags,\n                        sensitive: ep.sensitive,\n                        loc: Some(loc_from_range(self.app_root, &self.pc.file_set, ep.range)?),\n                        allow_unauthenticated: !ep.require_auth,\n                        body_limit: ep.body_limit,\n                        expose: {\n                            let mut map = HashMap::new();\n                            if ep.expose {\n                                map.insert(\n                                    DEFAULT_API_GATEWAY_NAME.to_string(),\n                                    v1::rpc::ExposeOptions {},\n                                );\n                            }\n                            map\n                        },\n                        streaming_request: ep.streaming_request,\n                        streaming_response: ep.streaming_response,\n                        static_assets,\n                    };\n\n                    let Some(service_idx) =\n                        svc_index.get(&ep.service_name).map(|idx| idx.to_owned())\n                    else {\n                        return Err(ep\n                            .range\n                            .to_span()\n                            .parse_err(format!(\"missing service {}\", ep.service_name)));\n                    };\n                    let service = &mut self.data.svcs[service_idx];\n\n                    if let Some(obj) = &b.object {\n                        let ep_idx = service.rpcs.len();\n                        endpoint_idx.insert(obj.id, (service_idx, ep_idx));\n                    }\n\n                    service.rpcs.push(rpc);\n                }\n\n                Resource::AuthHandler(ah) => {\n                    if let Some(obj) = &b.object {\n                        auth_handlers.insert(obj.id, ah.clone());\n                    }\n                }\n\n                Resource::SQLDatabase(db) => {\n                    self.data.sql_databases.push(self.sql_database(db)?);\n                }\n\n                Resource::Bucket(bkt) => {\n                    self.data.buckets.push(self.bucket(bkt));\n                }\n\n                Resource::PubSubTopic(topic) => {\n                    let idx = self.data.pubsub_topics.len();\n                    let top = self.pubsub_topic(topic)?;\n                    self.data.pubsub_topics.push(top);\n                    if let Some(obj) = &b.object {\n                        topic_idx.insert(obj.id, idx);\n                    }\n                    topic_by_name.insert(topic.name.clone(), idx);\n                }\n\n                Resource::Secret(secret) => {\n                    let service = self.service_for_range(&secret.range).ok_or(\n                        secret\n                            .range\n                            .parse_err(\"secrets must be loaded from within services\"),\n                    )?;\n\n                    let pkg_idx = svc_to_pkg_index\n                        .get(&service.name)\n                        .ok_or(\n                            secret\n                                .range\n                                .parse_err(format!(\"missing service: {}\", &service.name)),\n                        )?\n                        .to_owned();\n                    let pkg = &mut self.data.pkgs[pkg_idx];\n                    pkg.secrets.push(secret.name.clone());\n                }\n\n                // Dependent resources\n                Resource::PubSubSubscription(sub) => {\n                    dependent.push(Dependent::PubSubSubscription((b, sub)));\n                }\n                Resource::CronJob(cj) => {\n                    dependent.push(Dependent::CronJob((b, cj)));\n                }\n                Resource::Gateway(gw) => {\n                    dependent.push(Dependent::Gateway((b, gw)));\n                }\n\n                Resource::Metric(m) => {\n                    use crate::encore::parser::schema::v1::Builtin;\n\n                    // Metrics can be defined outside of services, so service_name is None\n                    // Service usage is tracked via Service.metrics field instead\n                    let service_name = None;\n\n                    let value_type = match m.metric_type {\n                        MetricType::Counter | MetricType::CounterGroup => Builtin::Int64 as i32,\n                        MetricType::Gauge | MetricType::GaugeGroup => Builtin::Float64 as i32,\n                    };\n\n                    let mut metric = v1::Metric {\n                        name: m.name.clone(),\n                        doc: m.doc.clone().unwrap_or_default(),\n                        value_type,\n                        service_name,\n                        labels: vec![],\n                        kind: match m.metric_type {\n                            MetricType::Counter | MetricType::CounterGroup => {\n                                v1::metric::MetricKind::Counter as i32\n                            }\n                            MetricType::Gauge | MetricType::GaugeGroup => {\n                                v1::metric::MetricKind::Gauge as i32\n                            }\n                        },\n                    };\n\n                    // Process labels if present\n                    if let Some(ref label_type_sp) = m.label_type {\n                        use crate::parser::resources::parseutil::resolve_interface;\n\n                        // Extract the interface from either Type::Interface or Type::Named\n                        if let Some(iface) = resolve_interface(&self.pc.type_checker, label_type_sp)\n                        {\n                            for field in &iface.fields {\n                                if let FieldName::String(key) = &field.name {\n                                    let label_key = key.to_case(Case::Snake);\n\n                                    // Validate label name is not \"service\" (reserved)\n                                    if label_key == \"service\" {\n                                        field.range.err(&format!(\n                                            \"invalid label name '{}': the label name 'service' is reserved and automatically added by the Encore runtime\",\n                                            key\n                                        ));\n                                        continue;\n                                    }\n\n                                    let field_type = match type_to_proto(&field.typ) {\n                                        Ok(builtin) => builtin as i32,\n                                        Err(err_msg) => {\n                                            field.range.err(&format!(\n                                                \"invalid type for metric label '{}': {}. Labels must be string, number, or boolean (or unions thereof)\",\n                                                key, err_msg\n                                            ));\n                                            continue;\n                                        }\n                                    };\n\n                                    // Extract doc comment for the label\n                                    let doc = self\n                                        .pc\n                                        .loader\n                                        .module_containing_pos(field.range.start)\n                                        .and_then(|module| {\n                                            module.preceding_comments(field.range.start)\n                                        })\n                                        .unwrap_or_default();\n\n                                    metric.labels.push(v1::metric::Label {\n                                        key: label_key,\n                                        doc,\n                                        r#type: field_type,\n                                    });\n                                }\n                            }\n                        }\n                    }\n\n                    self.data.metrics.push(metric);\n                }\n\n                Resource::CacheCluster(cluster) => {\n                    let idx = self.data.cache_clusters.len();\n                    if let Some(obj) = &b.object {\n                        cache_cluster_idx.insert(obj.id, idx);\n                    }\n                    cache_cluster_by_name.insert(cluster.name.clone(), idx);\n                    self.data.cache_clusters.push(v1::CacheCluster {\n                        name: cluster.name.clone(),\n                        doc: cluster.doc.clone().unwrap_or_default(),\n                        keyspaces: vec![],\n                        eviction_policy: cluster.eviction_policy.as_str().to_string(),\n                    });\n                }\n\n                Resource::CacheKeyspace(keyspace) => {\n                    dependent.push(Dependent::CacheKeyspace(keyspace));\n                }\n            }\n        }\n\n        // Keep track of things we've seen so we can report errors pointing at\n        // the previous definition when we see a duplicate.\n        let mut first_gateway: Option<&gateway::Gateway> = None;\n        let mut first_auth_handler: Option<&Object> = None;\n\n        // Make a second pass for resources that depend on other resources.\n        // Register Reference bind Object IDs for CacheCluster so that keyspaces\n        // referencing a .named() cluster can find the correct cluster index.\n        for b in &self.parse.binds {\n            if b.kind == BindKind::Reference {\n                if let Resource::CacheCluster(cluster) = &b.resource {\n                    if let (Some(obj), Some(&idx)) =\n                        (&b.object, cache_cluster_by_name.get(&cluster.name))\n                    {\n                        cache_cluster_idx.insert(obj.id, idx);\n                    }\n                }\n            }\n        }\n\n        for r in &dependent {\n            match r {\n                Dependent::PubSubSubscription((b, sub)) => {\n                    let topic_idx = topic_idx\n                        .get(&sub.topic.id)\n                        .ok_or_else(|| sub.topic.parse_err(\"topic not found\"))?\n                        .to_owned();\n                    let result = self.pubsub_subscription(b, sub)?;\n                    let topic = &mut self.data.pubsub_topics[topic_idx];\n                    topic.subscriptions.push(result);\n                }\n\n                Dependent::CronJob((_b, cj)) => {\n                    let (svc_idx, ep_idx) = endpoint_idx\n                        .get(&cj.endpoint.id)\n                        .ok_or(cj.endpoint.parse_err(\"endpoint not found\"))?\n                        .to_owned();\n                    let svc = &self.data.svcs[svc_idx];\n                    let ep = &svc.rpcs[ep_idx];\n\n                    let title = cj.title.clone().unwrap_or(cj.name.clone());\n                    let result = v1::CronJob {\n                        id: cj.name.clone(),\n                        doc: cj.doc.to_owned(),\n                        title,\n                        endpoint: Some(v1::QualifiedName {\n                            pkg: svc.rel_path.clone(),\n                            name: ep.name.clone(),\n                        }),\n                        schedule: match &cj.schedule {\n                            CronJobSchedule::Cron(expr) => format!(\"schedule:{}\", expr.0),\n                            CronJobSchedule::Every(mins) => format!(\"every:{mins}\"),\n                        },\n                    };\n                    self.data.cron_jobs.push(result);\n                }\n\n                Dependent::Gateway((_b, gw)) => {\n                    let auth_handler = if let Some(auth_handler) = &gw.auth_handler {\n                        let Some(ah) = auth_handlers.get(&auth_handler.id) else {\n                            gw.range.err(\"auth handler not found\");\n                            continue;\n                        };\n\n                        let service_name = self\n                            .service_for_range(&ah.range)\n                            .ok_or(\n                                ah.range\n                                    .parse_err(\"unable to determine service for auth handler\"),\n                            )?\n                            .name\n                            .clone();\n\n                        let loc = loc_from_range(self.app_root, &self.pc.file_set, ah.range)?;\n                        let params = self\n                            .schema\n                            .typ(&ah.encoding.auth_param)\n                            .map_err(|err| ah.encoding.auth_param.parse_err(err.to_string()))?;\n                        let auth_data = self\n                            .schema\n                            .typ(&ah.encoding.auth_data)\n                            .map_err(|err| ah.encoding.auth_data.parse_err(err.to_string()))?;\n                        Some(v1::AuthHandler {\n                            name: ah.name.clone(),\n                            doc: ah.doc.clone().unwrap_or_default(),\n                            pkg_path: loc.pkg_path.clone(),\n                            pkg_name: loc.pkg_name.clone(),\n                            loc: Some(loc),\n                            params: Some(params),\n                            auth_data: Some(auth_data),\n                            service_name,\n                        })\n                    } else {\n                        None\n                    };\n\n                    let service_name = self\n                        .service_for_range(&gw.range)\n                        .ok_or(\n                            gw.range\n                                .parse_err(\"unable to determine service for gateway\"),\n                        )?\n                        .name\n                        .clone();\n\n                    if let Some(first) = first_gateway {\n                        HANDLER.with(|h| {\n                            h.struct_span_err(\n                                gw.range.to_span(),\n                                \"multiple gateways not yet supported\",\n                            )\n                            .span_help(first.range.to_span(), \"previous gateway defined here\")\n                            .emit();\n                        });\n                        continue;\n                    } else {\n                        first_gateway = Some(gw);\n                    }\n\n                    if let Some(ah) = &gw.auth_handler {\n                        if let Some(first) = first_auth_handler {\n                            HANDLER.with(|h| {\n                                h.struct_span_err(\n                                    ah.range.to_span(),\n                                    \"multiple auth handlers not yet supported\",\n                                )\n                                .span_help(\n                                    first.range.to_span(),\n                                    \"previous auth handler defined here\",\n                                )\n                                .emit();\n                            });\n                            continue;\n                        } else {\n                            first_auth_handler = Some(ah);\n                        }\n                    }\n\n                    self.data.auth_handler.clone_from(&auth_handler);\n\n                    if gw.name != \"api-gateway\" {\n                        gw.range.err(\"only the 'api-gateway' gateway is supported\");\n                        continue;\n                    }\n                    let encore_name = DEFAULT_API_GATEWAY_NAME.to_string();\n\n                    self.data.gateways.push(v1::Gateway {\n                        encore_name,\n                        explicit: Some(v1::gateway::Explicit {\n                            service_name,\n                            auth_handler,\n                        }),\n                    });\n                }\n\n                Dependent::CacheKeyspace(keyspace) => {\n                    let svc = self.service_for_range(&keyspace.span.into());\n                    let svc_name = svc.map(|s| s.name.clone()).unwrap_or_default();\n\n                    let key_type = self.schema.typ(&keyspace.key_type).ok();\n                    let value_type = keyspace\n                        .value_type\n                        .as_ref()\n                        .and_then(|vt| self.schema.typ(vt).ok());\n\n                    let path_pattern = parse_key_pattern(&keyspace.key_pattern);\n\n                    let ks_info = v1::cache_cluster::Keyspace {\n                        service: svc_name,\n                        key_type,\n                        value_type,\n                        doc: keyspace.doc.clone().unwrap_or_default(),\n                        path_pattern: Some(path_pattern),\n                    };\n\n                    if let Some(&idx) = cache_cluster_idx.get(&keyspace.cluster.id) {\n                        self.data.cache_clusters[idx].keyspaces.push(ks_info);\n                    } else {\n                        log::warn!(\n                            \"cache keyspace references unknown cluster object {:?}, skipping\",\n                            keyspace.cluster.id,\n                        );\n                    }\n                }\n            }\n        }\n\n        let mut seen_publishers = HashSet::new();\n        let mut seen_calls = HashSet::new();\n\n        let mut bucket_perms = HashMap::new();\n        let mut cache_cluster_services: HashMap<String, HashSet<String>> = HashMap::new();\n        for u in &self.parse.usages {\n            match u {\n                Usage::Topic(access) => {\n                    if access.ops.contains(&TopicOperation::Publish) {\n                        let svc =\n                            self.service_for_range(&access.range)\n                                .ok_or(access.range.parse_err(\n                                    \"cannot determine which service is accessing this topic\",\n                                ))?;\n\n                        // Add the publisher if it hasn't already been seen.\n                        let key = (svc.name.clone(), access.topic.name.clone());\n                        if seen_publishers.insert(key) {\n                            let service_name = svc.name.clone();\n\n                            let idx = topic_by_name\n                                .get(&access.topic.name)\n                                .ok_or(access.range.parse_err(\"could not resolve topic\"))?\n                                .to_owned();\n                            let topic = &mut self.data.pubsub_topics[idx];\n                            topic\n                                .publishers\n                                .push(v1::pub_sub_topic::Publisher { service_name });\n                        }\n                    }\n                }\n                Usage::AccessDatabase(access) => {\n                    let Some(svc) = self.service_for_range(&access.range) else {\n                        access\n                            .range\n                            .parse_err(\"cannot determine which service is accessing this database\");\n                        continue;\n                    };\n\n                    let idx = svc_index.get(&svc.name).unwrap();\n                    self.data.svcs[*idx].databases.push(access.db.name.clone());\n                }\n\n                Usage::Bucket(access) => {\n                    let Some(svc) = self.service_for_range(&access.range) else {\n                        access\n                            .range\n                            .err(\"cannot determine which service is accessing this bucket\");\n                        continue;\n                    };\n\n                    use objects::Operation;\n                    let ops = access.ops.iter().map(|op| match op {\n                        Operation::DeleteObject => v1::bucket_usage::Operation::DeleteObject,\n                        Operation::ListObjects => v1::bucket_usage::Operation::ListObjects,\n                        Operation::ReadObjectContents => {\n                            v1::bucket_usage::Operation::ReadObjectContents\n                        }\n                        Operation::WriteObject => v1::bucket_usage::Operation::WriteObject,\n                        Operation::UpdateObjectMetadata => {\n                            v1::bucket_usage::Operation::UpdateObjectMetadata\n                        }\n                        Operation::GetObjectMetadata => {\n                            v1::bucket_usage::Operation::GetObjectMetadata\n                        }\n                        Operation::GetPublicUrl => v1::bucket_usage::Operation::GetPublicUrl,\n                        Operation::SignedUploadUrl => v1::bucket_usage::Operation::SignedUploadUrl,\n                        Operation::SignedDownloadUrl => {\n                            v1::bucket_usage::Operation::SignedDownloadUrl\n                        }\n                    } as i32);\n\n                    let idx = svc_index.get(&svc.name).unwrap();\n                    bucket_perms\n                        .entry((*idx, &access.bucket.name))\n                        .or_insert(vec![])\n                        .extend(ops);\n                }\n\n                Usage::Metric(access) => {\n                    // Track which services use which metrics (increment/set operations)\n                    let Some(svc) = self.service_for_range(&access.range) else {\n                        access\n                            .range\n                            .err(\"cannot determine which service is accessing this metric\");\n                        continue;\n                    };\n\n                    let idx = svc_index.get(&svc.name).unwrap();\n                    let service = &mut self.data.svcs[*idx];\n\n                    // Only add if not already present (avoid duplicates)\n                    if !service.metrics.contains(&access.metric.name) {\n                        service.metrics.push(access.metric.name.clone());\n                    }\n                }\n\n                Usage::CacheCluster(access) => {\n                    // Track which services use which cache clusters.\n                    // This is used to populate keyspace service mappings in metadata.\n                    let Some(svc) = self.service_for_range(&access.range) else {\n                        access\n                            .range\n                            .err(\"cannot determine which service is accessing this cache cluster\");\n                        continue;\n                    };\n\n                    cache_cluster_services\n                        .entry(access.cluster.name.clone())\n                        .or_default()\n                        .insert(svc.name.clone());\n                }\n\n                Usage::CallEndpoint(call) => {\n                    let src_service = self\n                        .service_for_range(&call.range)\n                        .ok_or(call.range.parse_err(\"unable to determine service for call\"))?\n                        .name\n                        .clone();\n                    let dst_service = call.service.clone();\n                    let dst_endpoint = call.endpoint.clone().unwrap_or_else(|| \"\".to_string());\n\n                    let dst_idx = svc_to_pkg_index\n                        .get(&dst_service)\n                        .ok_or(\n                            call.range\n                                .parse_err(\"could not resolve destination service\"),\n                        )?\n                        .to_owned();\n\n                    let dst_pkg_rel_path = self.data.pkgs[dst_idx].rel_path.clone();\n\n                    let src_idx = svc_to_pkg_index\n                        .get(&src_service)\n                        .ok_or(call.range.parse_err(\"could not resolve calling service\"))?\n                        .to_owned();\n                    let src_pkg = &mut self.data.pkgs[src_idx];\n\n                    let call_key = (src_service, dst_service, dst_endpoint.clone());\n                    if seen_calls.insert(call_key) {\n                        src_pkg.rpc_calls.push(v1::QualifiedName {\n                            pkg: dst_pkg_rel_path.clone(),\n                            name: dst_endpoint,\n                        });\n                    }\n                }\n            }\n        }\n\n        // Add the computed bucket permissions to the services.\n        for ((svc_idx, bucket), mut operations) in bucket_perms {\n            // Make the bucket perms sorted and unique.\n            operations.sort();\n            operations.dedup();\n            self.data.svcs[svc_idx].buckets.push(v1::BucketUsage {\n                bucket: bucket.clone(),\n                operations,\n            });\n        }\n\n        // Add keyspaces to cache clusters based on service usage.\n        // This is a fallback for cases where keyspace parsing didn't capture all usage,\n        // ensuring the runtime config generator includes the cache cluster.\n        for (cluster_name, services) in cache_cluster_services {\n            if let Some(&idx) = cache_cluster_by_name.get(&cluster_name) {\n                let cluster = &mut self.data.cache_clusters[idx];\n                for svc_name in services {\n                    // Only add if we don't already have a keyspace for this service\n                    // (from parsed CacheKeyspace resources)\n                    let already_has_service =\n                        cluster.keyspaces.iter().any(|ks| ks.service == svc_name);\n                    if !already_has_service {\n                        cluster.keyspaces.push(v1::cache_cluster::Keyspace {\n                            service: svc_name,\n                            // Fallback: no type info available from usage tracking\n                            key_type: None,\n                            value_type: None,\n                            doc: String::new(),\n                            path_pattern: None,\n                        });\n                    }\n                }\n            }\n        }\n\n        // Sort the packages for deterministic output.\n        self.data.pkgs.sort_by(|a, b| a.name.cmp(&b.name));\n\n        // Remove duplicate secrets.\n        for pkg in &mut self.data.pkgs {\n            pkg.secrets.sort();\n            pkg.secrets.dedup();\n        }\n\n        for svc in &mut self.data.svcs {\n            // Remove duplicate database access.\n            svc.databases.sort();\n            svc.databases.dedup();\n\n            // Sort buckets by name for deterministic output.\n            svc.buckets.sort_by(|a, b| a.bucket.cmp(&b.bucket));\n\n            // Sort the endpoints for deterministic output.\n            svc.rpcs.sort_by(|a, b| a.name.cmp(&b.name));\n        }\n\n        // If there is no gateway, add a default one.\n        if self.data.gateways.is_empty() {\n            self.data.gateways.push(v1::Gateway {\n                encore_name: \"api-gateway\".to_string(),\n                explicit: None,\n            });\n        }\n\n        self.data.decls = self.schema.into_decls();\n        Ok(self.data)\n    }\n\n    fn pubsub_topic(&mut self, topic: &pubsub_topic::Topic) -> PResult<v1::PubSubTopic> {\n        use pubsub_topic::DeliveryGuarantee;\n        let message_type = self.schema.typ(&topic.message_type).map_err(|e| {\n            topic\n                .message_type\n                .parse_err(format!(\"could not resolve message type: {e}\"))\n        })?;\n        Ok(v1::PubSubTopic {\n            name: topic.name.clone(),\n            doc: topic.doc.clone(),\n            message_type: Some(message_type),\n            delivery_guarantee: match topic.delivery_guarantee {\n                DeliveryGuarantee::AtLeastOnce => v1::pub_sub_topic::DeliveryGuarantee::AtLeastOnce,\n                DeliveryGuarantee::ExactlyOnce => v1::pub_sub_topic::DeliveryGuarantee::ExactlyOnce,\n            } as i32,\n            ordering_key: topic.ordering_attribute.clone().unwrap_or_default(),\n            publishers: vec![],    // filled in below\n            subscriptions: vec![], // filled in below\n        })\n    }\n\n    fn pubsub_subscription(\n        &self,\n        bind: &Bind,\n        sub: &pubsub_subscription::Subscription,\n    ) -> PResult<v1::pub_sub_topic::Subscription> {\n        let service_name = self\n            .service_for_range(&bind.range.unwrap_or(sub.range))\n            .ok_or(\n                sub.range\n                    .parse_err(\"unable to determine which service the subscription belongs to\"),\n            )?\n            .name\n            .clone();\n\n        Ok(v1::pub_sub_topic::Subscription {\n            name: sub.name.clone(),\n            service_name,\n            ack_deadline: sub.config.ack_deadline.as_nanos() as i64,\n            message_retention: sub.config.message_retention.as_nanos() as i64,\n            max_concurrency: sub.config.max_concurrency.map(|v| v as i32),\n            retry_policy: Some(v1::pub_sub_topic::RetryPolicy {\n                min_backoff: sub.config.min_retry_backoff.as_nanos() as i64,\n                max_backoff: sub.config.max_retry_backoff.as_nanos() as i64,\n                max_retries: sub.config.max_retries as i64,\n            }),\n        })\n    }\n\n    fn sql_database(&self, db: &sqldb::SQLDatabase) -> PResult<v1::SqlDatabase> {\n        // Transform the migrations into the metadata format.\n        let (migration_rel_path, migrations, allow_non_sequential_migrations) = match &db.migrations\n        {\n            Some(spec) => {\n                let rel_path = self\n                    .rel_path_string(&spec.dir)\n                    .ok_or(spec.parse_err(\"unable to resolve migration directory\"))?;\n\n                let migrations = spec\n                    .migrations\n                    .iter()\n                    .map(|m| v1::DbMigration {\n                        filename: m.file_name.clone(),\n                        description: m.description.clone(),\n                        number: m.number,\n                    })\n                    .collect::<Vec<_>>();\n                (Some(rel_path), migrations, spec.non_seq_migrations)\n            }\n            None => (None, vec![], false),\n        };\n\n        Ok(v1::SqlDatabase {\n            name: db.name.clone(),\n            doc: db.doc.clone(),\n            migration_rel_path,\n            migrations,\n            allow_non_sequential_migrations,\n        })\n    }\n\n    fn bucket(&self, bkt: &objects::Bucket) -> v1::Bucket {\n        v1::Bucket {\n            name: bkt.name.clone(),\n            doc: bkt.doc.clone(),\n            versioned: bkt.versioned,\n            public: bkt.public,\n        }\n    }\n\n    /// Compute the relative path from the app root.\n    /// It reports an error if the path is not under the app root.\n    fn rel_path<'b>(&self, path: &'b Path) -> Option<&'b Path> {\n        path.strip_prefix(self.app_root).ok()\n    }\n\n    /// Compute the relative path from the app root as a String.\n    fn rel_path_string(&self, path: &Path) -> Option<String> {\n        let suffix = self.rel_path(path)?;\n        suffix.to_str().map(|s| s.to_string())\n    }\n\n    fn service_for_range(&self, range: &Range) -> Option<&Service> {\n        let path = match range.file(&self.pc.file_set) {\n            FilePath::Real(path) => path,\n            FilePath::Custom(_) => return None,\n        };\n        self.parse\n            .services\n            .iter()\n            .find(|svc| path.starts_with(svc.root.as_path()))\n    }\n}\n\nimpl respath::Path {\n    fn to_meta(&self) -> v1::Path {\n        use respath::{Segment, ValueType};\n        use v1::path_segment::{ParamType, SegmentType};\n        v1::Path {\n            r#type: v1::path::Type::Url as i32,\n            segments: self\n                .segments\n                .iter()\n                .map(|seg| match seg.get() {\n                    Segment::Literal(lit) => v1::PathSegment {\n                        r#type: SegmentType::Literal as i32,\n                        value_type: ParamType::String as i32,\n                        value: lit.clone(),\n                        validation: None,\n                    },\n                    Segment::Param {\n                        name,\n                        value_type,\n                        validation,\n                    } => v1::PathSegment {\n                        r#type: SegmentType::Param as i32,\n                        value_type: match value_type {\n                            ValueType::String => ParamType::String as i32,\n                            ValueType::Int => ParamType::Int as i32,\n                            ValueType::Bool => ParamType::Bool as i32,\n                        },\n                        value: name.clone(),\n                        validation: validation.as_ref().map(validation::Expr::to_pb),\n                    },\n                    Segment::Wildcard { name, validation } => v1::PathSegment {\n                        r#type: SegmentType::Wildcard as i32,\n                        value_type: ParamType::String as i32,\n                        value: name.clone(),\n                        validation: validation.as_ref().map(validation::Expr::to_pb),\n                    },\n                    Segment::Fallback { name, validation } => v1::PathSegment {\n                        r#type: SegmentType::Fallback as i32,\n                        value_type: ParamType::String as i32,\n                        value: name.clone(),\n                        validation: validation.as_ref().map(validation::Expr::to_pb),\n                    },\n                })\n                .collect(),\n        }\n    }\n}\n\nfn basic_to_proto(basic: &crate::parser::types::Basic) -> Builtin {\n    match basic {\n        Basic::Boolean => Builtin::Bool,\n        Basic::String => Builtin::String,\n        Basic::Number => Builtin::Int64,\n        _ => Builtin::Any,\n    }\n}\n\n/// Parses a cache keyspace key pattern like \"greeting/:name\" into a Path proto.\nfn parse_key_pattern(pattern: &str) -> v1::Path {\n    let segments: Vec<v1::PathSegment> = pattern\n        .split('/')\n        .filter(|s| !s.is_empty())\n        .map(|segment| {\n            if let Some(param_name) = segment.strip_prefix(':') {\n                // Parameter segment like \":name\"\n                v1::PathSegment {\n                    r#type: v1::path_segment::SegmentType::Param as i32,\n                    value_type: v1::path_segment::ParamType::String as i32,\n                    value: param_name.to_string(),\n                    validation: None,\n                }\n            } else {\n                // Literal segment\n                v1::PathSegment {\n                    r#type: v1::path_segment::SegmentType::Literal as i32,\n                    value_type: v1::path_segment::ParamType::String as i32,\n                    value: segment.to_string(),\n                    validation: None,\n                }\n            }\n        })\n        .collect();\n\n    v1::Path {\n        r#type: v1::path::Type::CacheKeyspace as i32,\n        segments,\n    }\n}\n\nfn literal_to_proto(lit: &crate::parser::types::Literal) -> Builtin {\n    match lit {\n        Literal::String(_) => Builtin::String,\n        Literal::Number(_) | Literal::BigInt(_) => Builtin::Int64,\n        Literal::Boolean(_) => Builtin::Bool,\n    }\n}\n/// Converts a Type to a protobuf builtin type, handling unions and literals.\n/// Returns an error if the type is not valid for metric labels.\nfn type_to_proto(typ: &Type) -> Result<Builtin, String> {\n    match typ {\n        Type::Basic(basic @ (Basic::String | Basic::Number | Basic::Boolean)) => {\n            Ok(basic_to_proto(basic))\n        }\n        Type::Basic(other) => Err(format!(\"type '{:?}' is not allowed\", other)),\n\n        Type::Literal(lit) => Ok(literal_to_proto(lit)),\n\n        Type::Union(union) => check_union_same_proto_type(&union.types),\n\n        Type::Array(_) => Err(\"array types are not allowed\".to_string()),\n        Type::Interface(_) => Err(\"object types are not allowed\".to_string()),\n        Type::Named(n) => Err(format!(\n            \"named type '{}' is not allowed\",\n            n.obj.name.clone().unwrap_or_else(|| \"unknown\".to_string())\n        )),\n        Type::Optional(_) => Err(\"optional types are not allowed\".to_string()),\n        _ => Err(\"this type is not allowed\".to_string()),\n    }\n}\n\n/// Checks if all types in a union map to the same protobuf type.\n/// For example: \"hello\" | \"world\" → String, 1 | 2 | 3 → Int64, true | false → Bool\nfn check_union_same_proto_type(types: &[Type]) -> Result<Builtin, String> {\n    if types.is_empty() {\n        return Err(\"empty union type\".to_string());\n    }\n\n    // Get the proto type of the first member\n    let first_proto = type_to_proto(&types[0])?;\n\n    // Check if all remaining types map to the same proto type\n    for typ in &types[1..] {\n        let proto = type_to_proto(typ)?;\n        if proto != first_proto {\n            return Err(format!(\n                \"union contains incompatible types: expected all members to be compatible with {:?}\",\n                first_proto\n            ));\n        }\n    }\n\n    Ok(first_proto)\n}\n\nfn new_meta() -> v1::Data {\n    v1::Data {\n        module_path: \"app\".to_string(),\n        app_revision: \"\".to_string(),\n        uncommitted_changes: false,\n        decls: vec![],\n        pkgs: vec![],\n        svcs: vec![],\n        auth_handler: None,\n        cron_jobs: vec![],\n        pubsub_topics: vec![],\n        middleware: vec![],\n        cache_clusters: vec![],\n        experiments: vec![],\n        metrics: vec![],\n        sql_databases: vec![],\n        buckets: vec![],\n        gateways: vec![],\n        language: v1::Lang::Typescript as i32,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use swc_common::errors::{Handler, HANDLER};\n    use swc_common::{Globals, SourceMap, GLOBALS};\n    use tempdir::TempDir;\n\n    use crate::parser::parser::Parser;\n    use crate::parser::resourceparser::PassOneParser;\n    use crate::testutil::testresolve::TestResolver;\n    use crate::testutil::JS_RUNTIME_PATH;\n\n    use super::*;\n\n    fn parse(tmp_dir: &Path, src: &str) -> anyhow::Result<v1::Data> {\n        let globals = Globals::new();\n        let cm: Rc<SourceMap> = Default::default();\n        let errs = Rc::new(Handler::with_tty_emitter(\n            swc_common::errors::ColorConfig::Auto,\n            true,\n            false,\n            Some(cm.clone()),\n        ));\n\n        GLOBALS.set(&globals, || -> anyhow::Result<_> {\n            HANDLER.set(&errs, || -> anyhow::Result<_> {\n                let ar = txtar::from_str(src);\n                ar.materialize(tmp_dir)?;\n\n                let resolver = Box::new(TestResolver::new(tmp_dir.to_path_buf(), ar.clone()));\n                let pc = ParseContext::with_resolver(\n                    tmp_dir.to_path_buf(),\n                    Some(JS_RUNTIME_PATH.clone()),\n                    resolver,\n                    cm,\n                    errs.clone(),\n                )\n                .unwrap();\n                let _mods = pc.loader.load_archive(tmp_dir, &ar).unwrap();\n\n                let pass1 = PassOneParser::new(\n                    pc.file_set.clone(),\n                    pc.type_checker.clone(),\n                    Default::default(),\n                );\n                let parser = Parser::new(&pc, pass1);\n                let parse = parser.parse();\n                let md = compute_meta(&pc, &parse)?;\n                Ok(md)\n            })\n        })\n    }\n\n    #[test]\n    fn test_legacymeta() -> anyhow::Result<()> {\n        let src = r#\"\n-- foo.ts --\nimport { Bar } from './bar.ts';\n-- bar.ts --\nexport const Bar = 5;\n        \"#;\n        let tmp_dir = TempDir::new(\"tsparser-test\")?;\n        let meta = parse(tmp_dir.path(), src)?;\n        assert_eq!(meta.svcs.len(), 0);\n        Ok(())\n    }\n\n    #[test]\n    fn test_cache_keyspace_metadata() -> anyhow::Result<()> {\n        let src = r#\"\n-- svc/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\nexport default new Service(\"svc\");\n\n-- svc/cache.ts --\nimport {\n    CacheCluster,\n    StringKeyspace,\n    IntKeyspace,\n    FloatKeyspace,\n    StructKeyspace,\n    StringListKeyspace,\n    NumberListKeyspace,\n    StringSetKeyspace,\n    NumberSetKeyspace,\n} from \"encore.dev/storage/cache\";\n\nexport const cluster = new CacheCluster(\"myCluster\", {\n    evictionPolicy: \"allkeys-lru\",\n});\n\nexport const greetings = new StringKeyspace<string>(cluster, {\n    keyPattern: \"greeting/:key\",\n});\nexport const counters = new IntKeyspace<string>(cluster, {\n    keyPattern: \"counter/:key\",\n});\nexport const scores = new FloatKeyspace<string>(cluster, {\n    keyPattern: \"score/:key\",\n});\ninterface User { name: string; email: string; }\nexport const users = new StructKeyspace<string, User>(cluster, {\n    keyPattern: \"user/:key\",\n});\nexport const recentViews = new StringListKeyspace<string>(cluster, {\n    keyPattern: \"recent-views/:key\",\n});\nexport const scoreHistory = new NumberListKeyspace<string>(cluster, {\n    keyPattern: \"score-history/:key\",\n});\nexport const tags = new StringSetKeyspace<string>(cluster, {\n    keyPattern: \"tags/:key\",\n});\nexport const uniqueScores = new NumberSetKeyspace<string>(cluster, {\n    keyPattern: \"unique-scores/:key\",\n});\n\n-- package.json --\n{ \"name\": \"test\", \"type\": \"module\", \"dependencies\": { \"encore.dev\": \"^1.35.0\" } }\n        \"#;\n        let tmp_dir = TempDir::new(\"tsparser-cache-test\")?;\n        let meta = parse(tmp_dir.path(), src)?;\n\n        // Should have exactly one cache cluster\n        assert_eq!(meta.cache_clusters.len(), 1, \"expected 1 cache cluster\");\n        let cluster = &meta.cache_clusters[0];\n        assert_eq!(cluster.name, \"myCluster\");\n\n        // Should have 8 keyspaces (one for each type)\n        assert_eq!(\n            cluster.keyspaces.len(),\n            8,\n            \"expected 8 keyspaces, got {}: {:?}\",\n            cluster.keyspaces.len(),\n            cluster\n                .keyspaces\n                .iter()\n                .map(|ks| &ks.doc)\n                .collect::<Vec<_>>()\n        );\n\n        // Verify all keyspaces have the correct service\n        for ks in &cluster.keyspaces {\n            assert_eq!(ks.service, \"svc\", \"keyspace service should be 'svc'\");\n        }\n\n        // Verify all keyspaces have path patterns\n        for ks in &cluster.keyspaces {\n            assert!(\n                ks.path_pattern.is_some(),\n                \"keyspace should have a path pattern\"\n            );\n        }\n\n        Ok(())\n    }\n\n    #[test]\n    fn test_cache_named_keyspace_metadata() -> anyhow::Result<()> {\n        let src = r#\"\n-- svc/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\nexport default new Service(\"svc\");\n\n-- svc/cache.ts --\nimport { CacheCluster, StringKeyspace } from \"encore.dev/storage/cache\";\n\nexport const cluster = new CacheCluster(\"myCluster\", {\n    evictionPolicy: \"allkeys-lru\",\n});\n\nexport const greetings = new StringKeyspace<string>(cluster, {\n    keyPattern: \"greeting/:key\",\n});\n\n-- other/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\nexport default new Service(\"other\");\n\n-- other/cache.ts --\nimport { CacheCluster, StringListKeyspace, StringSetKeyspace } from \"encore.dev/storage/cache\";\n\nconst cluster = CacheCluster.named(\"myCluster\");\n\nexport const recentViews = new StringListKeyspace<string>(cluster, {\n    keyPattern: \"recent-views/:key\",\n});\n\nexport const tags = new StringSetKeyspace<string>(cluster, {\n    keyPattern: \"tags/:key\",\n});\n\n-- package.json --\n{ \"name\": \"test\", \"type\": \"module\", \"dependencies\": { \"encore.dev\": \"^1.35.0\" } }\n        \"#;\n        let tmp_dir = TempDir::new(\"tsparser-cache-named-test\")?;\n        let meta = parse(tmp_dir.path(), src)?;\n\n        assert_eq!(meta.cache_clusters.len(), 1, \"expected 1 cache cluster\");\n        let cluster = &meta.cache_clusters[0];\n        assert_eq!(cluster.name, \"myCluster\");\n\n        // Should have 3 keyspaces: 1 from svc + 2 from other\n        assert_eq!(\n            cluster.keyspaces.len(),\n            3,\n            \"expected 3 keyspaces, got {}: services={:?}\",\n            cluster.keyspaces.len(),\n            cluster\n                .keyspaces\n                .iter()\n                .map(|ks| &ks.service)\n                .collect::<Vec<_>>()\n        );\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "tsparser/src/legacymeta/schema.rs",
    "content": "use std::borrow::Cow;\nuse std::collections::HashMap;\nuse std::path::Path;\n\nuse anyhow::Result;\nuse itertools::Itertools;\nuse litparser::{ParseResult, ToParseErr};\nuse swc_common::errors::HANDLER;\n\nuse crate::encore::parser::schema::v1::r#type as styp;\nuse crate::encore::parser::schema::v1::{self as schema};\nuse crate::legacymeta::api_schema::strip_path_params;\nuse crate::parser::parser::ParseContext;\n\nuse crate::parser::resources::apis::api::Endpoint;\nuse crate::parser::resources::apis::encoding::resolve_wire_spec;\nuse crate::parser::types::{\n    drop_empty_or_void, unwrap_validated, Basic, Custom, EnumValue, FieldName, Generic, Interface,\n    Literal, Named, ObjectId, Type, Union, WireLocation,\n};\nuse crate::parser::{FilePath, FileSet, Range};\n\npub(super) struct SchemaBuilder<'a> {\n    pc: &'a ParseContext,\n    app_root: &'a Path,\n\n    decls: Vec<schema::Decl>,\n    obj_to_decl: HashMap<ObjectId, u32>,\n}\n\nstruct BuilderCtx<'a, 'b> {\n    builder: &'a mut SchemaBuilder<'b>,\n\n    // The id of the current decl being built.\n    // Used for generating TypeParameterRefs.\n    decl_id: Option<u32>,\n}\n\nimpl<'a> SchemaBuilder<'a> {\n    pub(super) fn new(pc: &'a ParseContext, app_root: &'a Path) -> Self {\n        SchemaBuilder {\n            pc,\n            app_root,\n            decls: Vec::new(),\n            obj_to_decl: HashMap::new(),\n        }\n    }\n\n    pub(super) fn into_decls(self) -> Vec<schema::Decl> {\n        self.decls\n    }\n\n    pub(super) fn typ(&mut self, typ: &Type) -> Result<schema::Type> {\n        let mut ctx = BuilderCtx {\n            builder: self,\n            decl_id: None,\n        };\n        ctx.typ(typ)\n    }\n\n    pub fn transform_handshake(&mut self, ep: &Endpoint) -> ParseResult<Option<schema::Type>> {\n        let mut ctx = BuilderCtx {\n            builder: self,\n            decl_id: None,\n        };\n        ctx.transform_handshake(ep)\n    }\n    pub fn transform_request(&mut self, ep: &Endpoint) -> ParseResult<Option<schema::Type>> {\n        let mut ctx = BuilderCtx {\n            builder: self,\n            decl_id: None,\n        };\n        ctx.transform_request(ep)\n    }\n\n    pub fn transform_response(&mut self, typ: Option<Type>) -> Result<Option<schema::Type>> {\n        match typ {\n            Some(typ) => Ok(Some(self.typ(&typ)?)),\n            None => Ok(None),\n        }\n    }\n}\n\nimpl BuilderCtx<'_, '_> {\n    #[tracing::instrument(skip(self), ret, level = \"trace\")]\n    fn typ(&mut self, typ: &Type) -> Result<schema::Type> {\n        Ok(match typ {\n            Type::Basic(tt) => self.basic(tt),\n            Type::Array(tt) => {\n                let elem = self.typ(&tt.0)?;\n                schema::Type {\n                    typ: Some(styp::Typ::List(Box::new(schema::List {\n                        elem: Some(Box::new(elem)),\n                    }))),\n                    validation: None,\n                }\n            }\n            Type::Interface(tt) => self.interface(tt)?,\n\n            Type::Enum(tt) => schema::Type {\n                // Treat this as a union.\n                typ: Some(styp::Typ::Union(schema::Union {\n                    types: tt\n                        .members\n                        .iter()\n                        .cloned()\n                        .map(|m| schema::Type {\n                            typ: Some(styp::Typ::Literal(schema::Literal {\n                                value: Some(match m.value {\n                                    EnumValue::String(str) => schema::literal::Value::Str(str),\n                                    EnumValue::Number(n) => schema::literal::Value::Int(n),\n                                }),\n                            })),\n                            validation: None,\n                        })\n                        .collect(),\n                })),\n                validation: None,\n            },\n\n            Type::Union(union) => schema::Type {\n                typ: Some(styp::Typ::Union(schema::Union {\n                    types: self.types(&union.types)?,\n                })),\n                validation: None,\n            },\n            Type::Tuple(_) => anyhow::bail!(\"tuple types are not yet supported in schemas\"),\n            Type::Literal(tt) => schema::Type {\n                typ: Some(styp::Typ::Literal(self.literal(tt))),\n                validation: None,\n            },\n            Type::Class(_) => anyhow::bail!(\"class types are not yet supported in schemas\"),\n            Type::Named(tt) => {\n                let has_type_params = tt.obj.kind.type_params().count() > 0;\n                let state = self.builder.pc.type_checker.state();\n                if state.is_universe(tt.obj.module_id) {\n                    let underlying = tt.underlying(state);\n                    self.typ(&underlying)?\n                } else if has_type_params {\n                    tracing::trace!(\n                        \"got named type with type arguments, resolving to underlying type\"\n                    );\n                    // The type is a generic type.\n                    // To avoid having to reproduce the full generic type resolution,\n                    // concretize the type here.\n                    let underlying = tt.underlying(state);\n                    tracing::trace!(underlying = ?underlying, \"underlying type\");\n                    self.typ(&underlying)?\n                } else {\n                    schema::Type {\n                        typ: Some(styp::Typ::Named(self.named(tt)?)),\n                        validation: None,\n                    }\n                }\n            }\n            Type::Optional(_) => anyhow::bail!(\"optional types are not yet supported in schemas\"),\n            Type::This(_) => anyhow::bail!(\"this types are not yet supported in schemas\"),\n            Type::Generic(typ) => match typ {\n                Generic::TypeParam(param) => {\n                    let decl_id = self\n                        .decl_id\n                        .ok_or_else(|| anyhow::anyhow!(\"missing decl_id\"))?;\n                    schema::Type {\n                        typ: Some(styp::Typ::TypeParameter(schema::TypeParameterRef {\n                            decl_id,\n                            param_idx: param.idx as u32,\n                        })),\n                        validation: None,\n                    }\n                }\n\n                typ => {\n                    anyhow::bail!(\n                        \"unresolved generic types are not supported in schemas, got: {:#?}\",\n                        typ\n                    )\n                }\n            },\n\n            Type::Validation(expr) => {\n                anyhow::bail!(\n                    \"unresolved standalone validation expression not supported in api schema: {:#?}\",\n                    expr\n                )\n            }\n\n            Type::Validated(validated) => {\n                let mut typ = self.typ(&validated.typ)?;\n                // Simplify the validation expression, if possible.\n                let expr = validated.expr.clone().simplify();\n                typ.validation = Some(expr.to_pb());\n                typ\n            }\n\n            Type::Custom(Custom::WireSpec(spec)) => self.typ(&spec.underlying)?,\n            Type::Custom(Custom::Decimal) => schema::Type {\n                typ: Some(styp::Typ::Builtin(schema::Builtin::Decimal as i32)),\n                validation: None,\n            },\n            Type::Function(_) => {\n                anyhow::bail!(\"function types are not supported in schemas\")\n            }\n        })\n    }\n\n    fn basic(&self, typ: &Basic) -> schema::Type {\n        let b = |b: schema::Builtin| schema::Type {\n            typ: Some(styp::Typ::Builtin(b as i32)),\n            validation: None,\n        };\n        match typ {\n            Basic::Any | Basic::Unknown => b(schema::Builtin::Any),\n            Basic::String => b(schema::Builtin::String),\n            Basic::Boolean => b(schema::Builtin::Bool),\n            Basic::Date => b(schema::Builtin::Time),\n            Basic::Number => {\n                // TODO handle float/int distinction somehow\n                b(schema::Builtin::Float64)\n            }\n            Basic::Null => schema::Type {\n                typ: Some(styp::Typ::Literal(schema::Literal {\n                    value: Some(schema::literal::Value::Null(true)),\n                })),\n                validation: None,\n            },\n            Basic::BigInt => b(schema::Builtin::Decimal),\n            Basic::Void | Basic::Object | Basic::Symbol | Basic::Undefined | Basic::Never => {\n                HANDLER.with(|h| h.err(&format!(\"unsupported basic type in schema: {typ:?}\")));\n                b(schema::Builtin::Any)\n            }\n        }\n    }\n\n    fn literal(&self, typ: &Literal) -> schema::Literal {\n        use schema::literal::Value;\n        let val = match typ.clone() {\n            Literal::String(val) => Value::Str(val),\n            Literal::Boolean(bool) => Value::Boolean(bool),\n            Literal::Number(float) => {\n                // If this can be represented as an int64, do that.\n                let int = float as i64;\n                if float == (int as f64) {\n                    Value::Int(int)\n                } else {\n                    Value::Float(float)\n                }\n            }\n            Literal::BigInt(str) => Value::Str(str),\n        };\n        schema::Literal { value: Some(val) }\n    }\n\n    fn interface(&mut self, typ: &Interface) -> Result<schema::Type> {\n        // Is this an index signature?\n        if let Some((key, value)) = typ.index.as_ref() {\n            if !typ.fields.is_empty() {\n                anyhow::bail!(\"index signature with additional fields is not supported\");\n            }\n            return Ok(schema::Type {\n                typ: Some(styp::Typ::Map(Box::new(schema::Map {\n                    key: Some(Box::new(self.typ(key)?)),\n                    value: Some(Box::new(self.typ(value)?)),\n                }))),\n                validation: None,\n            });\n        }\n\n        let mut fields = Vec::with_capacity(typ.fields.len());\n        for f in &typ.fields {\n            let FieldName::String(field_name) = &f.name else {\n                continue;\n            };\n            let (tt, had_undefined) = drop_undefined_union(&f.typ);\n            let optional = f.optional || had_undefined;\n\n            let mut tags = vec![];\n\n            // Tag it as `encore:\"optional\"` if the field is optional.\n            if optional {\n                tags.push(schema::Tag {\n                    key: \"encore\".into(),\n                    name: \"optional\".into(),\n                    options: vec![],\n                });\n            }\n\n            let mut query_string_name = String::new();\n\n            // Resolve any wire spec overrides.\n            let (tt, validation_expr) = unwrap_validated(&tt);\n            let (mut typ, wire) = if let Some(spec) = resolve_wire_spec(tt) {\n                (\n                    self.typ(&spec.underlying)?,\n                    match &spec.location {\n                        WireLocation::Header => {\n                            let name = spec.name_override.clone().unwrap_or(field_name.clone());\n                            tags.push(schema::Tag {\n                                key: \"header\".into(),\n                                name,\n                                options: if f.optional {\n                                    vec![\"optional\".into()]\n                                } else {\n                                    vec![]\n                                },\n                            });\n\n                            Some(schema::WireSpec {\n                                location: Some(schema::wire_spec::Location::Header(\n                                    schema::wire_spec::Header {\n                                        name: spec.name_override.clone(),\n                                    },\n                                )),\n                            })\n                        }\n                        WireLocation::Query => {\n                            query_string_name =\n                                spec.name_override.clone().unwrap_or(field_name.clone());\n                            tags.push(schema::Tag {\n                                key: \"query\".into(),\n                                name: query_string_name.clone(),\n                                options: if f.optional {\n                                    vec![\"optional\".into()]\n                                } else {\n                                    vec![]\n                                },\n                            });\n\n                            Some(schema::WireSpec {\n                                location: Some(schema::wire_spec::Location::Query(\n                                    schema::wire_spec::Query {\n                                        name: spec.name_override.clone(),\n                                    },\n                                )),\n                            })\n                        }\n\n                        WireLocation::PubSubAttr => {\n                            let name = spec.name_override.clone().unwrap_or(field_name.clone());\n                            tags.push(schema::Tag {\n                                key: \"pubsub-attr\".into(),\n                                name,\n                                options: vec![],\n                            });\n\n                            None\n                        }\n\n                        WireLocation::Cookie => {\n                            let name = spec.name_override.as_ref().unwrap_or(field_name);\n                            tags.push(schema::Tag {\n                                key: \"cookie\".into(),\n                                name: name.clone(),\n                                options: if f.optional {\n                                    vec![\"optional\".into()]\n                                } else {\n                                    vec![]\n                                },\n                            });\n\n                            Some(schema::WireSpec {\n                                location: Some(schema::wire_spec::Location::Cookie(\n                                    schema::wire_spec::Cookie {\n                                        name: spec.name_override.clone(),\n                                    },\n                                )),\n                            })\n                        }\n\n                        WireLocation::HttpStatus => {\n                            tags.push(schema::Tag {\n                                key: \"encore\".into(),\n                                name: \"httpstatus\".into(),\n                                options: if f.optional {\n                                    vec![\"optional\".into()]\n                                } else {\n                                    vec![]\n                                },\n                            });\n\n                            Some(schema::WireSpec {\n                                location: Some(schema::wire_spec::Location::HttpStatus(\n                                    schema::wire_spec::HttpStatus {},\n                                )),\n                            })\n                        }\n                    },\n                )\n            } else {\n                (self.typ(tt)?, None)\n            };\n\n            // Propagate the validation expression to the field.\n            if let Some(expr) = validation_expr {\n                typ.validation = Some(expr.to_pb());\n            }\n\n            let raw_tag = tags\n                .iter()\n                .map(|tag| {\n                    let mut s = tag.key.clone();\n                    s.push(':');\n                    s.push('\"');\n                    s.push_str(&tag.name);\n                    for opt in &tag.options {\n                        s.push(',');\n                        s.push_str(opt);\n                    }\n                    s.push('\"');\n                    s\n                })\n                .join(\" \");\n\n            let doc = self\n                .builder\n                .pc\n                .loader\n                .module_containing_pos(f.range.start)\n                .and_then(|module| module.preceding_comments(f.range.start));\n            fields.push(schema::Field {\n                typ: Some(typ),\n                name: field_name.clone(),\n                json_name: field_name.clone(),\n                optional,\n                wire,\n                tags,\n                raw_tag,\n                query_string_name,\n                doc: doc.unwrap_or_else(|| \"\".into()),\n            });\n        }\n\n        Ok(schema::Type {\n            typ: Some(styp::Typ::Struct(schema::Struct { fields })),\n            validation: None,\n        })\n    }\n\n    fn named(&mut self, typ: &Named) -> Result<schema::Named> {\n        let type_arguments = self.types(&typ.type_arguments)?;\n        let obj = &typ.obj;\n        if let Some(decl_id) = self.builder.obj_to_decl.get(&obj.id) {\n            return Ok(schema::Named {\n                id: *decl_id,\n                type_arguments,\n            });\n        }\n\n        // Allocate a new decl.\n        let id = self.builder.decls.len() as u32;\n        let Some(name) = typ.obj.name.as_ref() else {\n            anyhow::bail!(\"missing name for named object\");\n        };\n\n        // Allocate the object and add it to the list without the underlying type.\n        // We'll add the underlying type afterwards to properly handle recursive types.\n        let loc = loc_from_range(self.builder.app_root, &self.builder.pc.file_set, obj.range)?;\n\n        let decl = schema::Decl {\n            id,\n            name: name.clone(),\n            r#type: None,        // computed below\n            type_params: vec![], // TODO\n            doc: \"\".into(),      // TODO\n            loc: Some(loc),\n        };\n        self.builder.decls.push(decl);\n        self.builder.obj_to_decl.insert(obj.id, id);\n\n        let obj_typ = self.builder.pc.type_checker.resolve_obj_type(obj);\n        let obj_typ = self\n            .builder\n            .pc\n            .type_checker\n            .concrete(obj.module_id, &obj_typ);\n\n        let mut nested = BuilderCtx {\n            builder: self.builder,\n            decl_id: Some(id),\n        };\n\n        let schema_typ = nested.typ(&obj_typ)?;\n        self.builder.decls.get_mut(id as usize).unwrap().r#type = Some(schema_typ);\n\n        Ok(schema::Named { id, type_arguments })\n    }\n\n    fn new_named_from_type(\n        &mut self,\n        name: String,\n        underlying: Type,\n        range: Range,\n        type_arguments: Vec<Type>,\n    ) -> Result<schema::Named> {\n        let type_arguments = self.types(&type_arguments)?;\n        let underlying = self.typ(&underlying)?;\n\n        // Allocate a new decl.\n        let id = self.builder.decls.len() as u32;\n        // Allocate the object and add it to the list without the underlying type.\n        // We'll add the underlying type afterwards to properly handle recursive types.\n        let loc = loc_from_range(self.builder.app_root, &self.builder.pc.file_set, range)?;\n        let decl = schema::Decl {\n            id,\n            name: name.clone(),\n            r#type: Some(underlying),\n            type_params: vec![], // TODO\n            doc: \"\".into(),      // TODO\n            loc: Some(loc),\n        };\n        self.builder.decls.push(decl);\n        Ok(schema::Named { id, type_arguments })\n    }\n\n    fn types(&mut self, types: &[Type]) -> Result<Vec<schema::Type>> {\n        let mut result = Vec::with_capacity(types.len());\n        for t in types {\n            result.push(self.typ(t)?);\n        }\n        Ok(result)\n    }\n\n    fn transform_handshake(&mut self, ep: &Endpoint) -> ParseResult<Option<schema::Type>> {\n        let schema = ep.encoding.raw_handshake_schema.as_ref().map(|s| s.get());\n        self.transform_request_type(ep, schema).map_err(|err| {\n            let sp = ep\n                .encoding\n                .raw_handshake_schema\n                .as_ref()\n                .map_or(ep.range.to_span(), |s| s.span());\n            sp.parse_err(err.to_string())\n        })\n    }\n    fn transform_request(&mut self, ep: &Endpoint) -> ParseResult<Option<schema::Type>> {\n        let schema = ep.encoding.raw_req_schema.as_ref().map(|s| s.get());\n        self.transform_request_type(ep, schema).map_err(|err| {\n            let sp = ep\n                .encoding\n                .raw_req_schema\n                .as_ref()\n                .map_or(ep.range.to_span(), |s| s.span());\n            sp.parse_err(err.to_string())\n        })\n    }\n\n    fn transform_request_type(\n        &mut self,\n        ep: &Endpoint,\n        raw_schema: Option<&Type>,\n    ) -> Result<Option<schema::Type>> {\n        let Some(typ) = raw_schema.cloned() else {\n            return Ok(None);\n        };\n\n        let rs = self.builder.pc.type_checker.state();\n        Ok(match typ {\n            Type::Interface(mut interface) => {\n                strip_path_params(&ep.encoding.path, &mut interface);\n                let Some(typ) = drop_empty_or_void(Type::Interface(interface)) else {\n                    return Ok(None);\n                };\n                Some(self.typ(&typ)?)\n            }\n            Type::Named(ref named) => {\n                let underlying = named.underlying(rs).clone();\n                if let Type::Interface(mut iface) = underlying {\n                    strip_path_params(&ep.encoding.path, &mut iface);\n                    let obj = &named.obj;\n                    let Some(underlying) = drop_empty_or_void(Type::Interface(iface)) else {\n                        return Ok(None);\n                    };\n\n                    let named = self.new_named_from_type(\n                        obj.name.clone().unwrap(),\n                        underlying,\n                        obj.range,\n                        named.type_arguments.clone(),\n                    )?;\n\n                    return Ok(Some(schema::Type {\n                        typ: Some(styp::Typ::Named(named)),\n                        validation: None,\n                    }));\n                } else {\n                    match drop_empty_or_void(typ) {\n                        Some(typ) => Some(self.typ(&typ)?),\n                        None => None,\n                    }\n                }\n            }\n            _ => match drop_empty_or_void(typ) {\n                Some(typ) => Some(self.typ(&typ)?),\n                None => None,\n            },\n        })\n    }\n}\n\n/// If typ is a union type containing, drop the undefined type and return the modified\n/// union and `true` to indicate the type included \"| undefined\".\n/// Otherwise, returns the original type and `false`.\nfn drop_undefined_union(typ: &Type) -> (Cow<'_, Type>, bool) {\n    if let Type::Union(union) = &typ {\n        for (i, t) in union.types.iter().enumerate() {\n            if let Type::Basic(Basic::Undefined) = &t {\n                // If we have a union with only two types, return the other type.\n                return if union.types.len() == 2 {\n                    (Cow::Borrowed(&union.types[1 - i]), true)\n                } else {\n                    let mut types = union.types.clone();\n                    types.swap_remove(i);\n                    (Cow::Owned(Type::Union(Union { types })), true)\n                };\n            }\n        }\n    }\n\n    (Cow::Borrowed(typ), false)\n}\n\npub(super) fn loc_from_range(\n    app_root: &Path,\n    fset: &FileSet,\n    range: Range,\n) -> ParseResult<schema::Loc> {\n    let loc = range.loc(fset)?;\n    let (pkg_path, pkg_name, filename) = match loc.file {\n        FilePath::Custom(ref str) => {\n            return Err(range.parse_err(format!(\"unsupported file path in schema: {str}\")));\n        }\n        FilePath::Real(buf) => match buf.strip_prefix(app_root) {\n            Ok(rel_path) => {\n                let file_name = rel_path\n                    .file_name()\n                    .map(|s| s.to_string_lossy().to_string())\n                    .ok_or(range.parse_err(\"missing file name\"))?;\n                let pkg_name = rel_path\n                    .parent()\n                    .and_then(|p| p.file_name())\n                    .or_else(|| app_root.file_name())\n                    .map(|s| s.to_string_lossy().to_string())\n                    .ok_or(range.parse_err(\"missing package name\"))?;\n                let pkg_path = rel_path\n                    .parent()\n                    .map_or(\".\".to_string(), |s| s.to_string_lossy().to_string());\n                (pkg_path, pkg_name, file_name)\n            }\n            Err(_) => {\n                // The file is not relative to the app root.\n                // Use a simplified path.\n                let file_name = buf\n                    .file_name()\n                    .map(|s| s.to_string_lossy().to_string())\n                    .ok_or(range.parse_err(format!(\"missing file name: {}\", buf.display())))?;\n                let pkg_name = buf\n                    .parent()\n                    .and_then(|p| p.file_name())\n                    .map(|s| s.to_string_lossy().to_string())\n                    .ok_or(\n                        range.parse_err(format!(\"missing package name for {}\", buf.display())),\n                    )?;\n                let pkg_path = format!(\"unknown/{pkg_name}\");\n                (pkg_path, pkg_name, file_name)\n            }\n        },\n    };\n\n    Ok(schema::Loc {\n        pkg_path,\n        pkg_name,\n        filename,\n        start_pos: loc.start_pos as i32,\n        end_pos: loc.end_pos as i32,\n        src_line_start: loc.src_line_start as i32,\n        src_line_end: loc.src_line_end as i32,\n        src_col_start: loc.src_col_start as i32,\n        src_col_end: loc.src_col_end as i32,\n    })\n}\n"
  },
  {
    "path": "tsparser/src/lib.rs",
    "content": "pub mod app;\n#[cfg(not(target_arch = \"wasm32\"))]\npub mod builder;\npub mod exports;\nmod legacymeta;\npub mod parser;\npub mod resolve_utils;\nmod span_err;\npub mod tsconfig;\n\npub mod encore {\n    pub mod parser {\n        pub mod meta {\n            pub mod v1 {\n                include!(concat!(env!(\"OUT_DIR\"), \"/encore.parser.meta.v1.rs\"));\n            }\n        }\n\n        pub mod schema {\n            pub mod v1 {\n                include!(concat!(env!(\"OUT_DIR\"), \"/encore.parser.schema.v1.rs\"));\n            }\n        }\n    }\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nmod runtimeresolve;\n#[cfg(test)]\npub mod testutil;\n"
  },
  {
    "path": "tsparser/src/parser/doc_comments.rs",
    "content": "/// Compute the doc comment on the line(s) immediately preceding the given position.\n/// It returns None if there are no comments.\npub fn doc_comments_before(\n    source_map: &swc_common::SourceMap,\n    comments: &dyn swc_common::comments::Comments,\n    pos: swc_common::BytePos,\n) -> Option<String> {\n    // Get the file and line number of the position.\n    // It returns Err(file) if there is no line number information, in which\n    // case we won't be able to find any comments.\n    let Ok(res) = source_map.lookup_line(pos) else {\n        return None;\n    };\n\n    let candidates = comments.get_leading(pos).or_else(|| {\n        // If there are no comments at the pos, look up the start of the line\n        // and try there.\n        let start_pos = res.sf.lines.get(res.line)?;\n        comments.get_leading(*start_pos)\n    })?;\n\n    // The list of comments includes all consecutive comments in the AST, even if\n    // there are lines without comments in-between. We only want to include the comments that\n    // are attached to each other.\n\n    let mut comments = Vec::new();\n\n    // Iterate over the comments in reverse. For every comment, ensure the line number\n    for (i, c) in candidates.iter().rev().enumerate() {\n        if i > 0 && c.kind == swc_common::comments::CommentKind::Block {\n            // If we have a block comment that's not the first comment, ignore it.\n            // We only ever combine multiple line comments.\n            break;\n        }\n\n        // Ensure we don't have any gaps in lines between the AST node and the comment in question.\n        let Some(start_pos_ith_line) = res.sf.lines.get(res.line - (i + 1)) else {\n            break;\n        };\n        if c.span.hi >= *start_pos_ith_line {\n            comments.push(c);\n        } else {\n            break;\n        }\n\n        // If this was a block comment, don't consider any additional comments.\n        if c.kind == swc_common::comments::CommentKind::Block {\n            break;\n        }\n    }\n\n    if !comments.is_empty() {\n        let mut result = String::new();\n        for comment in comments.iter().rev() {\n            let is_jsdoc = comment.kind == swc_common::comments::CommentKind::Block\n                && comment.text.starts_with('*');\n\n            for line in comment.text.lines() {\n                let mut trimmed = line.trim();\n                if is_jsdoc {\n                    if trimmed.starts_with(\"/**\") {\n                        trimmed = trimmed[3..].trim_start();\n                    } else if trimmed.starts_with(\"*/\") {\n                        trimmed = trimmed[2..].trim_start();\n                    } else if trimmed.starts_with('*') {\n                        trimmed = trimmed[1..].trim_start();\n                    }\n                }\n                result.push_str(trimmed);\n                result.push('\\n');\n            }\n        }\n\n        let trimmed = result.trim();\n        if !trimmed.is_empty() {\n            return Some(trimmed.to_string());\n        }\n    }\n\n    None\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use swc_common::comments::SingleThreadedComments;\n    use swc_common::input::StringInput;\n    use swc_common::{FileName, SourceMap, Spanned};\n    use swc_ecma_ast as ast;\n    use swc_ecma_parser::lexer::Lexer;\n    use swc_ecma_parser::{Parser, Syntax};\n\n    fn decl_comments(src: &str) -> Vec<Option<String>> {\n        let source_map: SourceMap = Default::default();\n        let file = source_map.new_source_file(FileName::Custom(\"test.ts\".into()), src.into());\n        let comments: Box<SingleThreadedComments> = Box::default();\n        let lexer = Lexer::new(\n            Syntax::Typescript(Default::default()),\n            ast::EsVersion::Es2022,\n            StringInput::from(file.as_ref()),\n            Some(&comments),\n        );\n\n        let mut parser = Parser::new_from(lexer);\n        let ast = parser.parse_module().unwrap();\n\n        let mut result = Vec::new();\n        for it in ast.body {\n            if let ast::ModuleItem::Stmt(ast::Stmt::Decl(decl)) = it {\n                let c = doc_comments_before(&source_map, &comments, decl.span_lo());\n                result.push(c);\n            }\n        }\n\n        result\n    }\n\n    #[test]\n    fn parse_comments() {\n        let comments = decl_comments(\n            r#\"\nlet a = 0;\n\n// one-line\nlet b = 1;\n\n// ignored due to line-break\n\n// one\n// two\nlet c = 2;\n// ignored due to trailing comment\n\n/* line block\ncomment */\nlet d = 3;\n\n/* encroaching on decl\n*/ let e = 4;\n\n/* same line as decl */ let f = 5;\n\n// line comment\n/* followed by block\ncomment */\nlet g = 6;\n\n/* block */\n// line one\n// line two\nlet h = 7;\n\n// line one\n/* block */\n// line two\nlet i = 8;\n\n/**\n * JSDoc comment\n * multiple lines\n */\n let j = 9;\n            \"#,\n        );\n        assert_eq!(\n            comments,\n            vec![\n                None,\n                Some(\"one-line\".into()),\n                Some(\"one\\ntwo\".into()),\n                Some(\"line block\\ncomment\".into()),\n                Some(\"encroaching on decl\".into()),\n                Some(\"same line as decl\".into()),\n                Some(\"followed by block\\ncomment\".into()),\n                Some(\"line one\\nline two\".into()),\n                Some(\"line two\".into()),\n                Some(\"JSDoc comment\\nmultiple lines\".into()),\n            ]\n        );\n    }\n\n    #[test]\n    fn parse_jsdoc() {\n        let comments = decl_comments(\n            r#\"\n/**\n * JSDoc comment\n * multiple lines\n */\nlet i = 0;\n            \"#,\n        );\n        assert_eq!(\n            comments,\n            vec![Some(\"JSDoc comment\\nmultiple lines\".into()),]\n        );\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/fileset.rs",
    "content": "use std::io;\nuse std::path::{Path, PathBuf};\n\nuse crate::parser::doc_comments::doc_comments_before;\nuse litparser::{ParseResult, ToParseErr};\nuse serde::Serialize;\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\nuse swc_common::{Span, Spanned, SyntaxContext};\n\npub struct FileSet {\n    source_map: Lrc<swc_common::SourceMap>,\n}\n\nimpl std::fmt::Debug for FileSet {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"FileSet\").finish()\n    }\n}\n\nimpl FileSet {\n    pub(super) fn new(source_map: Lrc<swc_common::SourceMap>) -> Lrc<Self> {\n        Lrc::new(Self { source_map })\n    }\n\n    pub fn lookup_file<P: Into<Pos>>(&self, pos: P) -> Option<Lrc<SourceFile>> {\n        let pos = pos.into();\n        if pos.0 == 0 {\n            return None;\n        }\n        let f = self.source_map.lookup_byte_offset(pos.into());\n        Some(Lrc::new(SourceFile { file: f.sf }))\n    }\n\n    pub fn lookup_line<P: Into<Pos>>(&self, pos: P) -> (Lrc<SourceFile>, Option<usize>) {\n        let pos = pos.into();\n        match self.source_map.lookup_line(pos.into()) {\n            Ok(file_and_line) => {\n                let f = Lrc::new(SourceFile {\n                    file: file_and_line.sf,\n                });\n                (f, Some(file_and_line.line))\n            }\n            Err(file) => (Lrc::new(SourceFile { file }), None),\n        }\n    }\n\n    pub fn load_file(&self, path: &Path) -> io::Result<Lrc<SourceFile>> {\n        let file = self.source_map.load_file(path)?;\n        Ok(Lrc::new(SourceFile { file }))\n    }\n\n    pub fn new_source_file(&self, file_name: FilePath, src: String) -> Lrc<SourceFile> {\n        let file = self.source_map.new_source_file(file_name.into(), src);\n        Lrc::new(SourceFile { file })\n    }\n\n    pub fn preceding_comments(\n        &self,\n        comments: &dyn swc_common::comments::Comments,\n        pos: Pos,\n    ) -> Option<String> {\n        doc_comments_before(&self.source_map, comments, pos.into())\n    }\n}\n\npub struct SourceFile {\n    file: Lrc<swc_common::SourceFile>,\n}\n\nimpl SourceFile {\n    pub fn name(&self) -> FilePath {\n        match self.file.name {\n            swc_common::FileName::Real(ref p) => FilePath::Real(p.to_owned()),\n            swc_common::FileName::Custom(ref s) => FilePath::Custom(s.to_owned()),\n            _ => panic!(\"expected real file name\"),\n        }\n    }\n}\n\nimpl<'a> From<&'a SourceFile> for swc_common::input::StringInput<'a> {\n    fn from(file: &'a SourceFile) -> Self {\n        file.file.as_ref().into()\n    }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Hash)]\npub enum FilePath {\n    Real(PathBuf),\n    Custom(String),\n}\n\nimpl FilePath {\n    pub fn is_tsx(&self) -> bool {\n        match self {\n            FilePath::Real(p) => p.extension().is_some_and(|ext| ext == \"tsx\"),\n            FilePath::Custom(p) => p.ends_with(\".tsx\"),\n        }\n    }\n\n    pub fn is_dts(&self) -> bool {\n        match self {\n            FilePath::Real(p) => p.ends_with(\".d.ts\"),\n            FilePath::Custom(p) => p.ends_with(\".d.ts\"),\n        }\n    }\n}\n\nimpl std::fmt::Display for FilePath {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            FilePath::Real(p) => write!(f, \"FilePath::Real({})\", p.display()),\n            FilePath::Custom(p) => write!(f, \"FilePath::Custom({p})\"),\n        }\n    }\n}\n\nimpl From<FilePath> for swc_common::FileName {\n    fn from(value: FilePath) -> Self {\n        match value {\n            FilePath::Real(p) => swc_common::FileName::Real(p),\n            FilePath::Custom(p) => swc_common::FileName::Custom(p),\n        }\n    }\n}\n\nimpl From<PathBuf> for FilePath {\n    fn from(p: PathBuf) -> Self {\n        Self::Real(p)\n    }\n}\n\nimpl From<&str> for FilePath {\n    fn from(p: &str) -> Self {\n        Self::from(PathBuf::from(p))\n    }\n}\n\n#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Ord, PartialOrd, Serialize)]\npub struct Pos(pub u32);\n\nimpl From<Pos> for swc_common::BytePos {\n    fn from(value: Pos) -> Self {\n        swc_common::BytePos(value.0)\n    }\n}\n\nimpl From<swc_common::BytePos> for Pos {\n    fn from(pos: swc_common::BytePos) -> Self {\n        Self(pos.0)\n    }\n}\n\n#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Ord, PartialOrd, Serialize)]\npub struct Range {\n    pub start: Pos,\n    pub end: Pos,\n}\n\nimpl Range {\n    /// Report the file name this range is in.\n    pub fn file(&self, fset: &FileSet) -> FilePath {\n        let f = fset.source_map.lookup_byte_offset(self.start.into());\n        match &f.sf.name {\n            swc_common::FileName::Real(p) => FilePath::Real(p.to_owned()),\n            swc_common::FileName::Custom(s) => FilePath::Custom(s.to_owned()),\n            _ => panic!(\"expected real file name\"),\n        }\n    }\n\n    /// Report the file name this range is in.\n    pub fn loc(&self, fset: &FileSet) -> ParseResult<Loc> {\n        let sp = self.to_span();\n        Ok(match fset.source_map.span_to_lines(sp) {\n            Ok(lines) => {\n                let file = match &lines.file.name {\n                    swc_common::FileName::Real(p) => FilePath::Real(p.to_owned()),\n                    swc_common::FileName::Custom(s) => FilePath::Custom(s.to_owned()),\n                    _ => {\n                        return Err(sp.parse_err(\"expected real file name\"));\n                    }\n                };\n                match (lines.lines.first(), lines.lines.last()) {\n                    (Some(first), Some(last)) => Loc {\n                        file,\n                        start_pos: (self.start.0 - lines.file.start_pos.0) as usize,\n                        end_pos: (self.end.0 - lines.file.start_pos.0) as usize,\n                        src_line_start: first.line_index + 1,\n                        src_line_end: last.line_index + 1,\n                        src_col_start: first.start_col.0,\n                        src_col_end: last.end_col.0,\n                    },\n                    (_, _) => {\n                        return Err(sp.parse_err(\"missing line information\"));\n                    }\n                }\n            }\n            Err(_) => {\n                return Err(sp.parse_err(\"missing file information\"));\n            }\n        })\n    }\n\n    /// Whether the range contains another range.\n    pub fn contains(&self, other: &Self) -> bool {\n        self.start <= other.start && other.end <= self.end\n    }\n\n    pub fn to_span(&self) -> swc_common::Span {\n        swc_common::Span {\n            lo: swc_common::BytePos(self.start.0),\n            hi: swc_common::BytePos(self.end.0),\n            ctxt: SyntaxContext::empty(),\n        }\n    }\n\n    pub fn err(&self, msg: &str) {\n        HANDLER.with(|handler| handler.span_err(self.to_span(), msg))\n    }\n}\n\nimpl Spanned for Range {\n    fn span(&self) -> Span {\n        self.to_span()\n    }\n}\n\npub struct Loc {\n    pub file: FilePath,\n\n    /// Start and end positions within the file.\n    pub start_pos: usize,\n    pub end_pos: usize,\n\n    /// Start and end lines within the file.\n    pub src_line_start: usize,\n    pub src_line_end: usize,\n\n    /// Start and end columns within the line.\n    pub src_col_start: usize,\n    pub src_col_end: usize,\n}\n\nimpl From<swc_common::Span> for Range {\n    fn from(span: swc_common::Span) -> Self {\n        Self {\n            start: Pos(span.lo.0),\n            end: Pos(span.hi.0),\n        }\n    }\n}\n\nimpl From<Range> for swc_common::Span {\n    fn from(range: Range) -> Self {\n        swc_common::Span {\n            lo: range.start.into(),\n            hi: range.end.into(),\n            ctxt: SyntaxContext::empty(),\n        }\n    }\n}\n\nimpl From<Range> for swc_common::MultiSpan {\n    fn from(range: Range) -> Self {\n        swc_common::MultiSpan::from_span(range.into())\n    }\n}\n\nimpl Default for Range {\n    fn default() -> Self {\n        ZERO_RANGE\n    }\n}\n\npub const ZERO_RANGE: Range = Range {\n    start: Pos(0),\n    end: Pos(0),\n};\n"
  },
  {
    "path": "tsparser/src/parser/memory_resolver.rs",
    "content": "use std::collections::{HashMap, HashSet};\nuse std::path::{Path, PathBuf};\n\nuse swc_common::FileName;\nuse swc_ecma_loader::resolve::Resolve;\n\nuse crate::exports::Exports;\nuse crate::resolve_utils::{dts_counterpart, split_package_name};\nuse crate::tsconfig::TsConfigPathResolver;\n\n/// Conditions used when resolving package.json \"exports\" in the browser/WASM context.\nstatic RESOLVE_CONDITIONS: &[&str] = &[\"types\", \"import\", \"default\"];\n\n/// Extensions to try when resolving a module path.\nstatic RESOLVE_EXTENSIONS: &[&str] = &[\n    \".d.ts\", \".ts\", \".tsx\", \".d.mts\", \".mts\", \".d.cts\", \".cts\", \".js\", \".jsx\", \".mjs\", \".cjs\",\n];\n\n/// Index filenames to try when resolving a directory-like import.\nstatic INDEX_NAMES: &[&str] = &[\n    \"index.d.ts\",\n    \"index.ts\",\n    \"index.d.mts\",\n    \"index.mts\",\n    \"index.js\",\n    \"index.mjs\",\n];\n\n/// A module resolver that works entirely in-memory, for use in WASM/browser contexts.\n/// It resolves `encore.dev/*` imports to custom filenames, relative imports\n/// against a known set of file paths, and bare specifier imports via node_modules.\npub struct InMemoryResolver {\n    known_files: HashSet<PathBuf>,\n    app_root: PathBuf,\n    package_jsons: HashMap<PathBuf, serde_json::Value>,\n    tsconfig: Option<TsConfigPathResolver>,\n}\n\nimpl InMemoryResolver {\n    pub fn new(app_root: PathBuf, file_paths: Vec<PathBuf>) -> Self {\n        let known_files = file_paths.into_iter().collect();\n        Self {\n            known_files,\n            app_root,\n            package_jsons: HashMap::new(),\n            tsconfig: None,\n        }\n    }\n\n    /// Set the tsconfig path resolver for path alias support.\n    pub fn set_tsconfig(&mut self, tsconfig: TsConfigPathResolver) {\n        self.tsconfig = Some(tsconfig);\n    }\n\n    /// Register a parsed package.json for a given path (e.g. `/app/node_modules/zod/package.json`).\n    pub fn register_package_json(&mut self, path: PathBuf, value: serde_json::Value) {\n        self.package_jsons.insert(path, value);\n    }\n\n    /// Try to resolve a bare specifier (e.g. \"zod\", \"zod/lib/types\") to a file path.\n    fn resolve_bare_specifier(&self, target: &str) -> Result<FileName, anyhow::Error> {\n        let (pkg_name, subpath) = split_package_name(target);\n\n        // Try the package itself, then fall back to @types/{name} for non-scoped packages.\n        let candidates: Vec<String> = if pkg_name.starts_with('@') {\n            vec![pkg_name.to_string()]\n        } else {\n            vec![pkg_name.to_string(), format!(\"@types/{}\", pkg_name)]\n        };\n\n        for candidate_pkg in &candidates {\n            let pkg_json_path = self\n                .app_root\n                .join(\"node_modules\")\n                .join(candidate_pkg)\n                .join(\"package.json\");\n\n            let Some(pkg_json) = self.package_jsons.get(&pkg_json_path) else {\n                continue;\n            };\n\n            let pkg_dir = pkg_json_path.parent().unwrap().to_path_buf();\n\n            // 1. Try \"exports\" field\n            if let Some(exports_val) = pkg_json.get(\"exports\") {\n                if let Ok(exports) = serde_json::from_value::<Exports>(exports_val.clone()) {\n                    let conditions: HashSet<&str> = RESOLVE_CONDITIONS.iter().copied().collect();\n                    if let Some(resolved) = exports.resolve_import_path(subpath, &conditions) {\n                        let full_path = pkg_dir.join(&resolved);\n                        if let Some(found) = self.try_with_dts(&full_path) {\n                            return Ok(FileName::Real(found));\n                        }\n                    }\n                }\n            }\n\n            // Only use fallback fields when resolving the package root (no subpath).\n            if !subpath.is_empty() {\n                // For subpath imports without exports, try direct file resolution.\n                let direct = pkg_dir.join(subpath);\n                for candidate in file_candidates(&direct) {\n                    if self.known_files.contains(&candidate) {\n                        return Ok(FileName::Real(candidate));\n                    }\n                }\n                continue;\n            }\n\n            // 2. Fallback: \"types\" field\n            if let Some(types) = pkg_json.get(\"types\").or_else(|| pkg_json.get(\"typings\")) {\n                if let Some(types_str) = types.as_str() {\n                    let full_path = pkg_dir.join(types_str);\n                    if self.known_files.contains(&full_path) {\n                        return Ok(FileName::Real(full_path));\n                    }\n                }\n            }\n\n            // 3. Fallback: \"main\" field (try .d.ts counterpart)\n            if let Some(main) = pkg_json.get(\"main\") {\n                if let Some(main_str) = main.as_str() {\n                    let full_path = pkg_dir.join(main_str);\n                    if let Some(found) = self.try_with_dts(&full_path) {\n                        return Ok(FileName::Real(found));\n                    }\n                }\n            }\n\n            // 4. Last resort: index.d.ts, index.ts\n            for name in &[\"index.d.ts\", \"index.ts\"] {\n                let candidate = pkg_dir.join(name);\n                if self.known_files.contains(&candidate) {\n                    return Ok(FileName::Real(candidate));\n                }\n            }\n        }\n\n        Err(anyhow::anyhow!(\n            \"unable to resolve bare specifier: {target}\"\n        ))\n    }\n\n    /// Try the .d.ts counterpart first, then the original path.\n    fn try_with_dts(&self, path: &Path) -> Option<PathBuf> {\n        if let Some(dts) = dts_counterpart(path) {\n            if self.known_files.contains(&dts) {\n                return Some(dts);\n            }\n        }\n        if self.known_files.contains(path) {\n            return Some(path.to_path_buf());\n        }\n        None\n    }\n}\n\n/// Generate candidate file paths to try (with extensions and index variants).\nfn file_candidates(base: &Path) -> Vec<PathBuf> {\n    let mut candidates = Vec::with_capacity(1 + RESOLVE_EXTENSIONS.len() + INDEX_NAMES.len());\n    // Exact path\n    candidates.push(base.to_path_buf());\n    // With extensions\n    for ext in RESOLVE_EXTENSIONS {\n        candidates.push(PathBuf::from(format!(\"{}{}\", base.display(), ext)));\n    }\n    // Index variants\n    for name in INDEX_NAMES {\n        candidates.push(base.join(name));\n    }\n    candidates\n}\n\nimpl Resolve for InMemoryResolver {\n    fn resolve(&self, base: &FileName, target: &str) -> Result<FileName, anyhow::Error> {\n        // Try tsconfig path aliases (e.g. \"@/*\" -> \"./src/*\")\n        if let Some(tsconfig) = &self.tsconfig {\n            if let Some(resolved) =\n                tsconfig.resolve_with_checker(target, |p| self.known_files.contains(p))\n            {\n                // The resolved path is relative to tsconfig's base dir.\n                // Resolve it the same way we resolve relative imports.\n                if let FileName::Real(tsconfig_base) = tsconfig.base() {\n                    let resolved_path = normalize_path(&tsconfig_base.join(resolved.as_ref()));\n                    for candidate in file_candidates(&resolved_path) {\n                        if self.known_files.contains(&candidate) {\n                            return Ok(FileName::Real(candidate));\n                        }\n                    }\n                }\n            }\n        }\n\n        // Handle relative imports\n        if target.starts_with(\"./\") || target.starts_with(\"../\") {\n            if let FileName::Real(base_path) = base {\n                let parent = base_path.parent().unwrap_or(base_path);\n                let resolved = normalize_path(&parent.join(target));\n\n                for candidate in file_candidates(&resolved) {\n                    if self.known_files.contains(&candidate) {\n                        return Ok(FileName::Real(candidate));\n                    }\n                }\n            }\n        }\n\n        // Handle bare specifier imports (e.g. \"zod\", \"@types/react\")\n        if let Ok(resolved) = self.resolve_bare_specifier(target) {\n            return Ok(resolved);\n        }\n\n        Err(anyhow::anyhow!(\"unable to resolve {target}\"))\n    }\n}\n\n/// Normalize a path by resolving `.` and `..` components without filesystem access.\nfn normalize_path(path: &Path) -> PathBuf {\n    use std::path::Component;\n    let mut components = Vec::new();\n    for component in path.components() {\n        match component {\n            Component::CurDir => {}\n            Component::ParentDir => {\n                components.pop();\n            }\n            c => components.push(c),\n        }\n    }\n    components.iter().collect()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn make_resolver(files: &[&str], package_jsons: &[(&str, &str)]) -> InMemoryResolver {\n        let app_root = PathBuf::from(\"/app\");\n        let file_paths: Vec<PathBuf> = files.iter().map(|f| app_root.join(f)).collect();\n        let mut resolver = InMemoryResolver::new(app_root.clone(), file_paths);\n        for (path, content) in package_jsons {\n            let value: serde_json::Value = serde_json::from_str(content).unwrap();\n            resolver.register_package_json(app_root.join(path), value);\n        }\n        resolver\n    }\n\n    #[test]\n    fn resolve_encore_dev() {\n        let resolver = make_resolver(\n            &[\"node_modules/encore.dev/api/mod.ts\"],\n            &[(\n                \"node_modules/encore.dev/package.json\",\n                r#\"{\"name\": \"encore.dev\", \"exports\": {\"./api\": {\"types\": \"./api/mod.ts\"}}}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"encore.dev/api\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/encore.dev/api/mod.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_encore_dev_not_found() {\n        // Without encore.dev in node_modules, resolution should fail\n        let resolver = make_resolver(&[], &[]);\n        let result = resolver.resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"encore.dev/api\");\n        assert!(result.is_err());\n    }\n\n    #[test]\n    fn resolve_relative_ts() {\n        let resolver = make_resolver(&[\"src/bar.ts\"], &[]);\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"./bar\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/bar.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_relative_dts() {\n        let resolver = make_resolver(&[\"src/types.d.ts\"], &[]);\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"./types\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/types.d.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_relative_index() {\n        let resolver = make_resolver(&[\"src/utils/index.ts\"], &[]);\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"./utils\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/utils/index.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_relative_index_dts() {\n        let resolver = make_resolver(&[\"src/utils/index.d.ts\"], &[]);\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"./utils\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/utils/index.d.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_relative_parent() {\n        let resolver = make_resolver(&[\"lib/helper.ts\"], &[]);\n        let result = resolver\n            .resolve(\n                &FileName::Real(\"/app/src/deep/foo.ts\".into()),\n                \"../../lib/helper\",\n            )\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/lib/helper.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_bare_with_types_field() {\n        let resolver = make_resolver(\n            &[\"node_modules/foo/dist/index.d.ts\"],\n            &[(\n                \"node_modules/foo/package.json\",\n                r#\"{\"name\": \"foo\", \"types\": \"dist/index.d.ts\"}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"foo\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/foo/dist/index.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_bare_with_exports() {\n        let resolver = make_resolver(\n            &[\"node_modules/zod/lib/index.d.mts\"],\n            &[(\n                \"node_modules/zod/package.json\",\n                r#\"{\"name\": \"zod\", \"exports\": {\".\": {\"types\": \"./lib/index.d.mts\"}}}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"zod\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/zod/lib/index.d.mts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_bare_exports_js_to_dts() {\n        // exports points to .js, but .d.ts counterpart exists and should be preferred\n        let resolver = make_resolver(\n            &[\n                \"node_modules/pkg/dist/index.js\",\n                \"node_modules/pkg/dist/index.d.ts\",\n            ],\n            &[(\n                \"node_modules/pkg/package.json\",\n                r#\"{\"name\": \"pkg\", \"exports\": {\".\": \"./dist/index.js\"}}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"pkg\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/pkg/dist/index.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_bare_main_dts_counterpart() {\n        let resolver = make_resolver(\n            &[\"node_modules/lib/main.d.ts\"],\n            &[(\n                \"node_modules/lib/package.json\",\n                r#\"{\"name\": \"lib\", \"main\": \"main.js\"}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"lib\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/lib/main.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_bare_index_fallback() {\n        let resolver = make_resolver(\n            &[\"node_modules/simple/index.d.ts\"],\n            &[(\"node_modules/simple/package.json\", r#\"{\"name\": \"simple\"}\"#)],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"simple\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/simple/index.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_bare_types_fallback() {\n        // Package not found directly, but @types/foo exists\n        let resolver = make_resolver(\n            &[\"node_modules/@types/foo/index.d.ts\"],\n            &[(\n                \"node_modules/@types/foo/package.json\",\n                r#\"{\"name\": \"@types/foo\", \"types\": \"index.d.ts\"}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"foo\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/@types/foo/index.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_scoped_package() {\n        let resolver = make_resolver(\n            &[\"node_modules/@scope/pkg/dist/index.d.ts\"],\n            &[(\n                \"node_modules/@scope/pkg/package.json\",\n                r#\"{\"name\": \"@scope/pkg\", \"types\": \"dist/index.d.ts\"}\"#,\n            )],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"@scope/pkg\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/@scope/pkg/dist/index.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_subpath_import() {\n        let resolver = make_resolver(\n            &[\"node_modules/pkg/lib/utils.d.ts\"],\n            &[(\"node_modules/pkg/package.json\", r#\"{\"name\": \"pkg\"}\"#)],\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"pkg/lib/utils\")\n            .unwrap();\n        assert_eq!(\n            result,\n            FileName::Real(\"/app/node_modules/pkg/lib/utils.d.ts\".into())\n        );\n    }\n\n    #[test]\n    fn resolve_unresolvable() {\n        let resolver = make_resolver(&[], &[]);\n        let result = resolver.resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"nonexistent\");\n        assert!(result.is_err());\n    }\n\n    fn make_resolver_with_tsconfig(files: &[&str], tsconfig_json: &str) -> InMemoryResolver {\n        let app_root = PathBuf::from(\"/app\");\n        let file_paths: Vec<PathBuf> = files.iter().map(|f| app_root.join(f)).collect();\n        let mut resolver = InMemoryResolver::new(app_root.clone(), file_paths);\n        let tsconfig = TsConfigPathResolver::from_str(&app_root, tsconfig_json).unwrap();\n        resolver.set_tsconfig(tsconfig);\n        resolver\n    }\n\n    #[test]\n    fn resolve_tsconfig_wildcard_alias() {\n        let resolver = make_resolver_with_tsconfig(\n            &[\"src/utils/helper.ts\"],\n            r#\"{\"compilerOptions\": {\"paths\": {\"@/*\": [\"./src/*\"]}}}\"#,\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"@/utils/helper\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/utils/helper.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_tsconfig_exact_alias() {\n        let resolver = make_resolver_with_tsconfig(\n            &[\"src/config.ts\"],\n            r#\"{\"compilerOptions\": {\"paths\": {\"config\": [\"./src/config\"]}}}\"#,\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"config\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/config.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_tsconfig_with_base_url() {\n        let resolver = make_resolver_with_tsconfig(\n            &[\"src/lib/utils.ts\"],\n            r#\"{\"compilerOptions\": {\"baseUrl\": \"./src\", \"paths\": {\"@lib/*\": [\"./lib/*\"]}}}\"#,\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"@lib/utils\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/lib/utils.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_tsconfig_fallback_values() {\n        // First path doesn't exist, second does\n        let resolver = make_resolver_with_tsconfig(\n            &[\"lib/helper.ts\"],\n            r#\"{\"compilerOptions\": {\"paths\": {\"@/*\": [\"./src/*\", \"./lib/*\"]}}}\"#,\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/main.ts\".into()), \"@/helper\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/lib/helper.ts\".into()));\n    }\n\n    #[test]\n    fn resolve_tsconfig_no_match_falls_through() {\n        // tsconfig alias doesn't match, should fall through to relative resolution\n        let resolver = make_resolver_with_tsconfig(\n            &[\"src/bar.ts\"],\n            r#\"{\"compilerOptions\": {\"paths\": {\"@/*\": [\"./src/*\"]}}}\"#,\n        );\n        let result = resolver\n            .resolve(&FileName::Real(\"/app/src/foo.ts\".into()), \"./bar\")\n            .unwrap();\n        assert_eq!(result, FileName::Real(\"/app/src/bar.ts\".into()));\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/mod.rs",
    "content": "mod doc_comments;\nmod fileset;\npub mod memory_resolver;\npub mod module_loader;\n#[allow(clippy::module_inception)]\npub mod parser;\npub mod resourceparser;\npub mod resources;\npub mod respath;\nmod service_discovery;\npub mod types;\npub mod usageparser;\n\npub use fileset::{FilePath, FileSet, Pos, Range, ZERO_RANGE};\n"
  },
  {
    "path": "tsparser/src/parser/module_loader.rs",
    "content": "use std::cell::{OnceCell, RefCell};\nuse std::collections::HashMap;\nuse std::ffi::OsStr;\nuse std::io;\nuse std::path::{Path, PathBuf};\n\nuse swc_common::comments::{Comments, NoopComments, SingleThreadedComments};\nuse swc_common::errors::Handler;\nuse swc_common::input::StringInput;\nuse swc_common::sync::Lrc;\nuse swc_common::{FileName, Mark, Span, Spanned};\nuse swc_ecma_ast as ast;\nuse swc_ecma_ast::EsVersion;\nuse swc_ecma_loader::resolve::Resolve;\nuse swc_ecma_parser::lexer::Lexer;\nuse swc_ecma_parser::{Parser, Syntax};\nuse swc_ecma_visit::FoldWith;\nuse thiserror::Error;\n\nuse crate::parser::fileset::SourceFile;\nuse crate::parser::{FilePath, FileSet, Pos};\n\n// File extensions that should be parsed as modules\nconst MODULE_EXTENSIONS: &[&str] = &[\"js\", \"ts\", \"mjs\", \"mts\", \"cjs\", \"cts\", \"jsx\", \"tsx\"];\n\n/// A unique id for a module.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub struct ModuleId(pub usize);\n\npub struct ModuleLoader {\n    errs: Lrc<Handler>,\n    file_set: Lrc<FileSet>,\n    resolver: Box<dyn Resolve>,\n    encore_gen_root: PathBuf,\n    by_path: RefCell<HashMap<FilePath, Lrc<Module>>>,\n\n    /// In-memory file contents. In WASM mode all files are registered here since\n    /// filesystem access is unavailable. In native mode this is unused.\n    in_memory_files: RefCell<HashMap<PathBuf, String>>,\n\n    // The universe module, if it's been loaded.\n    universe: OnceCell<Lrc<Module>>,\n\n    /// The generated encore.gen/clients module.\n    encore_app_clients: OnceCell<Lrc<Module>>,\n    /// The generated encore.gen/auth module.\n    encore_auth: OnceCell<Lrc<Module>>,\n}\n\nimpl std::fmt::Debug for ModuleLoader {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"ModuleLoader\")\n            .field(\"file_set\", &self.file_set)\n            .field(\"mods\", &self.by_path)\n            .finish()\n    }\n}\n\n#[derive(Debug, Error)]\npub enum Error {\n    #[error(\"unable to resolve module {0}\")]\n    UnableToResolve(String, #[source] anyhow::Error),\n    #[error(\"invalid filename {0}\")]\n    InvalidFilename(FileName),\n    #[error(\"unable to load file from filesystem\")]\n    LoadFile(#[source] io::Error),\n    #[error(\"error when parsing module\")]\n    ParseError(swc_ecma_parser::error::Error),\n}\n\nimpl Error {\n    pub fn span(&self) -> Option<Span> {\n        match self {\n            Error::UnableToResolve(..) | Error::InvalidFilename(_) | Error::LoadFile(_) => None,\n            Error::ParseError(e) => Some(e.span()),\n        }\n    }\n\n    pub fn msg(&self) -> String {\n        match self {\n            Error::UnableToResolve(s, source) => {\n                format!(\"unable to resolve module {s}: {source:?}\")\n            }\n            Error::InvalidFilename(_) | Error::LoadFile(_) => self.to_string(),\n            Error::ParseError(e) => e.clone().into_kind().msg().to_string(),\n        }\n    }\n}\n\nimpl ModuleLoader {\n    pub fn new(\n        errs: Lrc<Handler>,\n        file_set: Lrc<FileSet>,\n        resolver: Box<dyn Resolve>,\n        app_root: PathBuf,\n    ) -> Self {\n        let encore_gen_root = app_root.join(\"encore.gen\");\n        Self {\n            errs,\n            file_set,\n            resolver,\n            encore_gen_root,\n            by_path: RefCell::new(HashMap::new()),\n            in_memory_files: RefCell::new(HashMap::new()),\n            universe: OnceCell::new(),\n            encore_app_clients: OnceCell::new(),\n            encore_auth: OnceCell::new(),\n        }\n    }\n\n    /// Register in-memory file contents for import resolution.\n    pub fn register_file_contents(&self, files: HashMap<PathBuf, String>) {\n        self.in_memory_files.borrow_mut().extend(files);\n    }\n\n    pub fn modules(&self) -> Vec<Lrc<Module>> {\n        self.by_path.borrow().values().cloned().collect::<Vec<_>>()\n    }\n\n    pub fn module_containing_pos(&self, pos: Pos) -> Option<Lrc<Module>> {\n        let file = self.file_set.lookup_file(pos)?;\n        let path = file.name();\n        self.by_path.borrow().get(&path).cloned()\n    }\n\n    pub fn resolve_import_from_module(\n        &self,\n        module: &Module,\n        import_path: &str,\n    ) -> Result<Option<Lrc<Module>>, Error> {\n        self.resolve_import(&module.swc_file_path, import_path)\n    }\n\n    pub fn resolve_import(\n        &self,\n        from_file: &swc_common::FileName,\n        import_path: &str,\n    ) -> Result<Option<Lrc<Module>>, Error> {\n        // Special case for the generated clients.\n        // TODO: Fix this to do actual import path resolution.\n        // It's a bit tricky because we can't use the resolver since the files may not exist.\n        if import_path == \"~encore/clients\" {\n            return Ok(Some(self.encore_app_clients()));\n        } else if import_path == \"~encore/auth\" {\n            return Ok(Some(self.encore_auth()));\n        }\n\n        let target_file_path = {\n            // TODO: cache this\n            let mod_path = self\n                .resolver\n                .resolve(from_file, import_path)\n                .map_err(|err| Error::UnableToResolve(import_path.to_string(), err))?;\n            match mod_path {\n                FileName::Real(ref buf) => {\n                    if let Some(ext) = buf.extension().and_then(OsStr::to_str) {\n                        if !MODULE_EXTENSIONS.contains(&ext) {\n                            return Ok(None);\n                        }\n                    }\n\n                    // Check for the generated clients again, using the resolved path,\n                    // in case the \"~encore/*\" alias is not set up.\n                    if let Ok(suffix) = buf.strip_prefix(&self.encore_gen_root) {\n                        // Need to check for trailing slash since the resolved path\n                        // will be something like \"clients/index.js\".\n                        if suffix.starts_with(\"clients/\") {\n                            return Ok(Some(self.encore_app_clients()));\n                        } else if suffix.starts_with(\"auth/\") {\n                            return Ok(Some(self.encore_auth()));\n                        }\n                    }\n\n                    FilePath::Real(buf.clone())\n                }\n                FileName::Custom(ref str) => FilePath::Custom(str.clone()),\n                _ => return Err(Error::InvalidFilename(mod_path)),\n            }\n        };\n\n        if let Some(module) = self.by_path.borrow().get(&target_file_path) {\n            return Ok(Some(module.clone()));\n        }\n\n        // Determine the module path.\n        // https://www.typescriptlang.org/docs/handbook/module-resolution.html#relative-vs-non-relative-module-imports\n        let module_path = if import_path.starts_with(\"./\")\n            || import_path.starts_with(\"../\")\n            || import_path.starts_with('/')\n        {\n            None\n        } else {\n            Some(import_path.to_owned())\n        };\n\n        match target_file_path {\n            FilePath::Real(ref path) => self.load_fs_file(path.as_path(), module_path).map(Some),\n            FilePath::Custom(_) => self\n                .load_custom_file(target_file_path, \"\", module_path)\n                .map(Some),\n        }\n    }\n\n    /// Load a file from the filesystem into the module loader.\n    pub fn load_fs_file(\n        &self,\n        path: &Path,\n        module_path: Option<String>,\n    ) -> Result<Lrc<Module>, Error> {\n        // Is it already stored?\n        let file_name = FilePath::from(path.to_owned());\n        if let Some(module) = self.by_path.borrow().get(&file_name) {\n            return Ok(module.clone());\n        }\n\n        // Try in-memory files first.\n        if let Some(content) = self.in_memory_files.borrow().get(path).cloned() {\n            return self.parse_and_store(\n                self.file_set.new_source_file(file_name, content),\n                module_path,\n            );\n        }\n\n        // Use file system for non-wasm targets\n        #[cfg(not(target_arch = \"wasm32\"))]\n        {\n            self.parse_and_store(\n                self.file_set.load_file(path).map_err(Error::LoadFile)?,\n                module_path,\n            )\n        }\n\n        #[cfg(target_arch = \"wasm32\")]\n        Err(Error::LoadFile(io::Error::new(\n            io::ErrorKind::NotFound,\n            format!(\"file not found in memory: {}\", path.display()),\n        )))\n    }\n\n    /// Load a file from the filesystem into the module loader.\n    fn load_custom_file<S: Into<String>>(\n        &self,\n        file_name: FilePath,\n        src: S,\n        module_path: Option<String>,\n    ) -> Result<Lrc<Module>, Error> {\n        // Is it already stored?\n        if let Some(module) = self.by_path.borrow().get(&file_name) {\n            return Ok(module.clone());\n        }\n        let file = self\n            .file_set\n            .new_source_file(file_name.to_owned(), src.into());\n        let module = self.parse_and_store(file, module_path)?;\n        Ok(module)\n    }\n\n    pub fn universe(&self) -> Lrc<Module> {\n        self.universe\n            .get_or_init(|| {\n                let file = self\n                    .file_set\n                    .new_source_file(FilePath::Real(\"universe.ts\".into()), UNIVERSE_TS.into());\n                self.parse_and_store(file, Some(\"__universe__\".into()))\n                    .unwrap()\n            })\n            .to_owned()\n    }\n\n    pub fn encore_app_clients(&self) -> Lrc<Module> {\n        self.encore_app_clients\n            .get_or_init(|| {\n                let file = self\n                    .file_set\n                    .new_source_file(FilePath::Real(\"encore.gen/clients\".into()), \"\".into());\n                self.parse_and_store(file, Some(\"encore.gen/clients\".into()))\n                    .unwrap()\n            })\n            .to_owned()\n    }\n\n    pub fn encore_auth(&self) -> Lrc<Module> {\n        self.encore_auth\n            .get_or_init(|| {\n                let file = self\n                    .file_set\n                    .new_source_file(FilePath::Real(\"encore.gen/auth\".into()), \"\".into());\n                self.parse_and_store(file, Some(\"encore.gen/auth\".into()))\n                    .unwrap()\n            })\n            .to_owned()\n    }\n\n    /// Parse and store a file.\n    pub fn parse_and_store(\n        &self,\n        file: Lrc<SourceFile>,\n        module_path: Option<String>,\n    ) -> Result<Lrc<Module>, Error> {\n        let (ast, comments) = self.parse_file(file.clone())?;\n\n        let mut mods = self.by_path.borrow_mut();\n        let id = ModuleId(mods.len() + 1);\n\n        let module = Module::new(\n            self.file_set.clone(),\n            id,\n            file.name(),\n            module_path,\n            ast,\n            Some(comments),\n        );\n        mods.insert(module.file_path.clone(), module.clone());\n        Ok(module)\n    }\n\n    /// Parse a file.\n    fn parse_file(\n        &self,\n        file: Lrc<SourceFile>,\n    ) -> Result<(ast::Module, Box<SingleThreadedComments>), Error> {\n        let comments: Box<SingleThreadedComments> = Box::default();\n\n        let syntax = Syntax::Typescript(swc_ecma_parser::TsConfig {\n            tsx: file.name().is_tsx(),\n            dts: file.name().is_dts(),\n            decorators: true,\n            no_early_errors: false,\n            disallow_ambiguous_jsx_like: false,\n        });\n\n        let lexer = Lexer::new(\n            syntax,\n            EsVersion::Es2022,\n            StringInput::from(file.as_ref()),\n            Some(&comments),\n        );\n        let mut parser = Parser::new_from(lexer);\n        for e in parser.take_errors() {\n            e.into_diagnostic(&self.errs).emit();\n        }\n\n        let ast = parser.parse_module().map_err(Error::ParseError)?;\n\n        // Resolve identifiers.\n        let mut resolver = swc_ecma_transforms_base::resolver(Mark::new(), Mark::new(), true);\n        let ast_module = ast.fold_with(&mut resolver);\n\n        Ok((ast_module, comments))\n    }\n}\n\npub struct Module {\n    file_set: Lrc<FileSet>,\n    pub id: ModuleId,\n    pub ast: swc_ecma_ast::Module,\n    pub file_path: FilePath,\n    pub swc_file_path: swc_common::FileName,\n    /// How the module was imported, if it's an external module.\n    pub module_path: Option<String>,\n    pub comments: Box<dyn Comments>,\n    cached_imports: OnceCell<Vec<ast::ImportDecl>>,\n}\n\nimpl std::fmt::Debug for Module {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"Module\")\n            .field(\"id\", &self.id)\n            .field(\"file_path\", &self.file_path)\n            .field(\"swc_file_path\", &self.swc_file_path)\n            .field(\"module_path\", &self.module_path)\n            .finish()\n    }\n}\n\nimpl Module {\n    fn new(\n        file_set: Lrc<FileSet>,\n        id: ModuleId,\n        file_path: FilePath,\n        module_path: Option<String>,\n        ast: ast::Module,\n        comments: Option<Box<dyn Comments>>,\n    ) -> Lrc<Self> {\n        let comments: Box<dyn Comments> = comments.unwrap_or_else(|| Box::new(NoopComments {}));\n        let swc_file_path = file_path.clone().into();\n        Lrc::new(Self {\n            file_set,\n            id,\n            ast,\n            file_path,\n            swc_file_path,\n            module_path,\n            comments,\n            cached_imports: OnceCell::new(),\n        })\n    }\n\n    pub fn imports(&self) -> &Vec<ast::ImportDecl> {\n        self.cached_imports\n            .get_or_init(move || imports_from_mod(&self.ast))\n    }\n\n    pub fn preceding_comments(&self, pos: Pos) -> Option<String> {\n        self.file_set.preceding_comments(&self.comments, pos)\n    }\n}\n\nimpl Spanned for Module {\n    fn span(&self) -> Span {\n        self.ast.span\n    }\n}\n\n/// imports_from_mod returns the import declarations in the given module.\nfn imports_from_mod(ast: &ast::Module) -> Vec<ast::ImportDecl> {\n    (ast.body)\n        .iter()\n        .filter_map(|it| match &it {\n            ast::ModuleItem::ModuleDecl(ast::ModuleDecl::Import(imp)) => Some(imp.clone()),\n            _ => None,\n        })\n        .collect()\n}\n\n#[cfg(test)]\nimpl ModuleLoader {\n    /// Injects a new file into the module loader.\n    /// If a file with that name has already been added it does nothing.\n    pub fn inject_file(&self, path: FilePath, src: &str) -> anyhow::Result<Lrc<Module>> {\n        // Check if the file has already been added if the file has a unique identity.\n        // For other file types (like anonymous files) don't check for this so that we can inject\n        //  multiple anonymous files for testing purposes.\n        match path {\n            FilePath::Real(..) => {\n                if let Some(module) = self.by_path.borrow().get(&path) {\n                    return Ok(module.clone());\n                }\n            }\n            FilePath::Custom(_) => {}\n        }\n\n        use swc_common::{Globals, GLOBALS};\n        let globals = Globals::new();\n        GLOBALS.set(&globals, || {\n            let file = self.file_set.new_source_file(path, src.into());\n            let module = self.parse_and_store(file, None)?;\n            Ok(module)\n        })\n    }\n\n    pub fn load_archive(\n        &self,\n        base: &Path,\n        ar: &txtar::Archive,\n    ) -> anyhow::Result<HashMap<FilePath, Lrc<Module>>> {\n        let mut result = HashMap::new();\n        for file in &ar.files {\n            if file.name.extension().is_none_or(|ext| ext != \"ts\") {\n                continue;\n            }\n\n            let file_name = FilePath::Real(base.join(&file.name));\n            let file = self.file_set.new_source_file(file_name, file.data.clone());\n            let module = self.parse_and_store(file, None)?;\n            result.insert(module.file_path.clone(), module);\n        }\n\n        if self.errs.has_errors() {\n            Err(anyhow::anyhow!(\"parse error\"))\n        } else {\n            Ok(result)\n        }\n    }\n}\n\nconst UNIVERSE_TS: &str = include_str!(\"./universe.ts\");\n"
  },
  {
    "path": "tsparser/src/parser/parser.rs",
    "content": "use std::collections::HashMap;\nuse std::ffi::OsStr;\nuse std::fmt::Formatter;\nuse std::path::{Path, PathBuf};\n\nuse anyhow::Result;\nuse swc_common::errors::{Handler, HANDLER};\nuse swc_common::sync::Lrc;\nuse swc_common::{SourceMap, Spanned, DUMMY_SP};\nuse swc_ecma_loader::resolve::Resolve;\n#[cfg(not(target_arch = \"wasm32\"))]\nuse swc_ecma_loader::resolvers::node::NodeModulesResolver;\n#[cfg(not(target_arch = \"wasm32\"))]\nuse swc_ecma_loader::TargetEnv;\n#[cfg(not(target_arch = \"wasm32\"))]\nuse walkdir::WalkDir;\n\nuse crate::parser::module_loader::ModuleLoader;\nuse crate::parser::resourceparser::bind::{Bind, BindKind};\nuse crate::parser::resourceparser::PassOneParser;\nuse crate::parser::resources::apis::service_client::ServiceClient;\nuse crate::parser::resources::Resource;\nuse crate::parser::service_discovery::{discover_services, DiscoveredService};\nuse crate::parser::types::TypeChecker;\nuse crate::parser::usageparser::{Usage, UsageResolver};\nuse crate::parser::{FilePath, FileSet};\n#[cfg(not(target_arch = \"wasm32\"))]\nuse crate::runtimeresolve::{EncoreRuntimeResolver, TsConfigPathResolver};\nuse crate::span_err::ErrReporter;\n\nuse super::resourceparser::bind::ResourceOrPath;\nuse super::resourceparser::UnresolvedBind;\nuse super::resources::ResourcePath;\n\n/// Whether a file or directory name should be ignored during parsing.\nfn is_ignored_name(name: &str) -> bool {\n    matches!(name, \"node_modules\" | \"encore.gen\" | \"__tests__\")\n        || name.starts_with('.')\n        || name.ends_with(\".test.ts\")\n        || name.ends_with(\".spec.ts\")\n        || name.ends_with(\".test.js\")\n        || name.ends_with(\".spec.js\")\n}\n\npub struct ParseContext {\n    /// App root to parse for Encore resources.\n    /// The directory containing the 'encore.app' file.\n    pub app_root: PathBuf,\n\n    /// The module loader to use.\n    pub loader: Lrc<ModuleLoader>,\n\n    pub type_checker: Lrc<TypeChecker>,\n\n    /// The file set to use.\n    pub file_set: Lrc<FileSet>,\n\n    /// The error handler to emit errors to.\n    pub errs: Lrc<Handler>,\n}\n\nimpl std::fmt::Debug for ParseContext {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"ParseContext\")\n            .field(\"app_root\", &self.app_root)\n            .finish()\n    }\n}\n\nimpl ParseContext {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn new(\n        app_root: PathBuf,\n        js_runtime_path: Option<PathBuf>,\n        cm: Lrc<SourceMap>,\n        errs: Lrc<Handler>,\n    ) -> Result<Self> {\n        let resolver = NodeModulesResolver::with_export_conditions(\n            TargetEnv::Node,\n            Default::default(),\n            true,\n            vec![\"bun\".into(), \"deno\".into(), \"types\".into()],\n        );\n        Self::with_resolver(app_root, js_runtime_path, resolver, cm, errs)\n    }\n\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn with_resolver<R>(\n        app_root: PathBuf,\n        js_runtime_path: Option<PathBuf>,\n        resolver: R,\n        cm: Lrc<SourceMap>,\n        errs: Lrc<Handler>,\n    ) -> Result<Self>\n    where\n        R: Resolve + 'static,\n    {\n        let mut resolver =\n            EncoreRuntimeResolver::new(resolver, js_runtime_path, vec![\"types\".into()]);\n\n        // Do we have a tsconfig.json file in the app root?\n        {\n            let tsconfig_path = app_root.join(\"tsconfig.json\");\n            if tsconfig_path.exists() {\n                let tsconfig = Lrc::new(TsConfigPathResolver::from_file(&tsconfig_path)?);\n                resolver = resolver.with_tsconfig_resolver(tsconfig.clone());\n            }\n        }\n\n        Self::with_boxed_resolver(app_root, Box::new(resolver), cm, errs)\n    }\n\n    /// Create a ParseContext with a pre-boxed resolver. Used by both native and WASM paths.\n    pub fn with_boxed_resolver(\n        app_root: PathBuf,\n        resolver: Box<dyn Resolve>,\n        cm: Lrc<SourceMap>,\n        errs: Lrc<Handler>,\n    ) -> Result<Self> {\n        let file_set = FileSet::new(cm.clone());\n        let loader = Lrc::new(ModuleLoader::new(\n            errs.clone(),\n            file_set.clone(),\n            resolver,\n            app_root.clone(),\n        ));\n        let type_checker = Lrc::new(TypeChecker::new(loader.clone()));\n\n        Ok(Self {\n            app_root,\n            loader,\n            type_checker,\n            file_set,\n            errs,\n        })\n    }\n}\n\npub struct Parser<'a> {\n    pc: &'a ParseContext,\n    pass1: PassOneParser<'a>,\n}\n\n#[derive(Debug)]\npub struct ParseResult {\n    pub resources: Vec<Resource>,\n    pub binds: Vec<Lrc<Bind>>,\n    pub usages: Vec<Usage>,\n    pub services: Vec<Service>,\n}\n\nimpl<'a> Parser<'a> {\n    pub fn new(pc: &'a ParseContext, pass1: PassOneParser<'a>) -> Self {\n        Self { pc, pass1 }\n    }\n\n    /// Run the parser by walking the filesystem.\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn parse(mut self) -> ParseResult {\n        fn is_service(e: &walkdir::DirEntry) -> bool {\n            e.path().ends_with(\"encore.service.ts\")\n        }\n\n        let walker = WalkDir::new(&self.pc.app_root)\n            .sort_by(|a, b| {\n                if is_service(a) {\n                    std::cmp::Ordering::Less\n                } else if is_service(b) {\n                    std::cmp::Ordering::Greater\n                } else {\n                    a.file_name().cmp(b.file_name())\n                }\n            })\n            .into_iter()\n            .filter_entry(|e| !is_ignored_name(e.file_name().to_str().unwrap_or_default()));\n\n        // Parse the modules in the app root.\n        let (resources, binds) = {\n            let loader = &self.pc.loader;\n            let mut all_resources = Vec::new();\n            let mut all_binds = Vec::new();\n\n            // Keep track of the current service being parsed.\n            let mut curr_service: Option<(PathBuf, String)> = None;\n\n            for entry in walker {\n                let entry = match entry {\n                    Ok(e) => e,\n                    Err(err) => {\n                        HANDLER.with(|h| h.err(&format!(\"unable to walk filesystem: {err}\")));\n                        continue;\n                    }\n                };\n\n                if entry.file_type().is_dir() {\n                    // Is this directory outside the service directory?\n                    // If so, unset the current service.\n                    if let Some((service_dir, _)) = &curr_service {\n                        if !entry.path().starts_with(service_dir) {\n                            curr_service = None;\n                        }\n                    }\n                    continue;\n                }\n\n                // Skip non-files.\n                if !entry.file_type().is_file() {\n                    continue;\n                }\n\n                let path = entry.path();\n\n                // Skip non-\".ts\" files.\n                let ext = entry.path().extension().and_then(OsStr::to_str);\n                if ext.is_none_or(|ext| ext != \"ts\") {\n                    continue;\n                }\n\n                // Parse the module.\n                let module = match loader.load_fs_file(entry.path(), None) {\n                    Ok(module) => module,\n                    Err(err) => {\n                        HANDLER.with(|handler| {\n                            if let Some(span) = err.span() {\n                                handler.span_err(span, &err.msg());\n                            } else {\n                                handler.err(&err.msg());\n                            }\n                        });\n                        continue;\n                    }\n                };\n\n                let (resources, binds) =\n                    self.process_module(&module, path, is_service(&entry), &mut curr_service);\n                all_resources.extend(resources);\n                all_binds.extend(binds);\n            }\n\n            (all_resources, all_binds)\n        };\n\n        self.finalize(resources, binds)\n    }\n\n    /// Parse from pre-loaded file contents (for WASM/in-memory use).\n    /// Files should be provided as (path, content) pairs.\n    /// The paths should be relative to the app root (e.g., \"myservice/api.ts\").\n    pub fn parse_from_files(mut self, files: Vec<(PathBuf, String)>) -> ParseResult {\n        // Sort files so encore.service.ts files come first within each directory,\n        // matching the behavior of the native parse() method (WalkDir sorts per-directory).\n        let mut sorted_files = files;\n        sorted_files.sort_by(|a, b| {\n            let a_parent = a.0.parent();\n            let b_parent = b.0.parent();\n            a_parent\n                .cmp(&b_parent)\n                .then_with(|| {\n                    let a_svc = a.0.ends_with(\"encore.service.ts\");\n                    let b_svc = b.0.ends_with(\"encore.service.ts\");\n                    b_svc.cmp(&a_svc) // true > false, so service files come first\n                })\n                .then_with(|| a.0.cmp(&b.0))\n        });\n\n        let (resources, binds) = {\n            let mut all_resources = Vec::new();\n            let mut all_binds = Vec::new();\n            let mut curr_service: Option<(PathBuf, String)> = None;\n\n            for (rel_path, content) in &sorted_files {\n                // Skip non-.ts files.\n                let ext = rel_path.extension().and_then(OsStr::to_str);\n                if ext.is_none_or(|ext| ext != \"ts\") {\n                    continue;\n                }\n\n                // Skip ignored file/directory patterns.\n                let should_skip = rel_path\n                    .components()\n                    .any(|c| c.as_os_str().to_str().is_some_and(is_ignored_name));\n                if should_skip {\n                    continue;\n                }\n\n                let full_path = self.pc.app_root.join(rel_path);\n                let is_service_file = full_path.ends_with(\"encore.service.ts\");\n\n                // Track current service directory.\n                if let Some((service_dir, _)) = &curr_service {\n                    if !full_path.starts_with(service_dir) {\n                        curr_service = None;\n                    }\n                }\n\n                // Create in-memory source file and parse it.\n                let file_path = FilePath::Real(full_path.clone());\n                let file = self.pc.file_set.new_source_file(file_path, content.clone());\n                let module = match self.pc.loader.parse_and_store(file, None) {\n                    Ok(m) => m,\n                    Err(err) => {\n                        HANDLER.with(|handler| {\n                            if let Some(span) = err.span() {\n                                handler.span_err(span, &err.msg());\n                            } else {\n                                handler.err(&err.msg());\n                            }\n                        });\n                        continue;\n                    }\n                };\n\n                let (resources, binds) =\n                    self.process_module(&module, &full_path, is_service_file, &mut curr_service);\n                all_resources.extend(resources);\n                all_binds.extend(binds);\n            }\n\n            (all_resources, all_binds)\n        };\n\n        self.finalize(resources, binds)\n    }\n\n    /// Process a parsed module: run pass1 parsing, validate service files, track service context.\n    fn process_module(\n        &mut self,\n        module: &Lrc<super::module_loader::Module>,\n        full_path: &Path,\n        is_service_file: bool,\n        curr_service: &mut Option<(PathBuf, String)>,\n    ) -> (Vec<Resource>, Vec<UnresolvedBind>) {\n        let module_span = module.ast.span();\n        let service_name = curr_service.as_ref().map(|(_, name)| name.as_str());\n        let (resources, binds) = self.pass1.parse(module.clone(), service_name);\n\n        if is_service_file {\n            let found = resources.iter().any(|r| matches!(r, Resource::Service(_)));\n            if !found {\n                module_span\n                    .shrink_to_lo()\n                    .err(\"encore.service.ts must define a Service resource\");\n            }\n        }\n\n        for res in &resources {\n            if let Resource::Service(svc) = res {\n                if let Some(parent) = full_path.parent() {\n                    *curr_service = Some((parent.to_path_buf(), svc.name.clone()));\n                }\n                break;\n            }\n        }\n\n        (resources, binds)\n    }\n\n    /// Shared post-processing: resolve binds, discover services, resolve usage.\n    fn finalize(\n        &mut self,\n        mut resources: Vec<Resource>,\n        binds: Vec<UnresolvedBind>,\n    ) -> ParseResult {\n        let mut binds = resolve_binds(&resources, binds);\n        let services = discover_services(&self.pc.file_set, &binds);\n        let (additional_resources, additional_binds) =\n            self.inject_generated_service_clients(&services);\n\n        resources.extend(additional_resources);\n        binds.extend(additional_binds);\n\n        let resolver =\n            UsageResolver::new(&self.pc.loader, &self.pc.type_checker, &resources, &binds);\n        let mut usages = Vec::new();\n\n        for module in self.pc.loader.modules() {\n            let exprs = resolver.scan_usage_exprs(&module);\n            let u = resolver.resolve_usage(&module, &exprs);\n            usages.extend(u);\n        }\n\n        let services = collect_services(&self.pc.file_set, &binds, services);\n\n        ParseResult {\n            resources,\n            binds,\n            usages,\n            services,\n        }\n    }\n\n    fn inject_generated_service_clients(\n        &mut self,\n        services: &[DiscoveredService],\n    ) -> (Vec<Resource>, Vec<Lrc<Bind>>) {\n        // Find the services we have\n        let mut resources = Vec::new();\n        let mut binds = Vec::new();\n\n        let module = self.pc.loader.encore_app_clients();\n        for svc in services {\n            let client = Lrc::new(ServiceClient {\n                service_name: svc.name.clone(),\n            });\n            let resource = Resource::ServiceClient(client.clone());\n            resources.push(resource.clone());\n\n            let id = self.pass1.alloc_bind_id();\n            binds.push(Lrc::new(Bind {\n                id,\n                // The bind name is the service name but with \"-\" replaced\n                // with \"_\" to make it a valid identifier.\n                name: Some(svc.name.replace(\"-\", \"_\")),\n                object: None,\n                kind: BindKind::Create,\n                resource,\n                range: None,\n                internal_bound_id: None,\n                module_id: module.id,\n            }));\n        }\n\n        (resources, binds)\n    }\n}\n\nfn resolve_binds(resources: &[Resource], binds: Vec<UnresolvedBind>) -> Vec<Lrc<Bind>> {\n    // Collect the resources we support by path.\n    let resource_paths = resources\n        .iter()\n        .filter_map(|r| match r {\n            Resource::SQLDatabase(db) => Some((\n                ResourcePath::SQLDatabase {\n                    name: db.name.clone(),\n                },\n                r,\n            )),\n            Resource::Bucket(bkt) => Some((\n                ResourcePath::Bucket {\n                    name: bkt.name.clone(),\n                },\n                r,\n            )),\n            Resource::CacheCluster(cluster) => Some((\n                ResourcePath::CacheCluster {\n                    name: cluster.name.clone(),\n                },\n                r,\n            )),\n            _ => None,\n        })\n        .collect::<HashMap<ResourcePath, &Resource>>();\n\n    let mut result = Vec::with_capacity(binds.len());\n    for b in binds {\n        let resource: Resource = match b.resource {\n            ResourceOrPath::Resource(res) => res,\n            ResourceOrPath::Path(path) => {\n                let Some(res) = resource_paths.get(&path) else {\n                    b.range\n                        .map_or(DUMMY_SP, |r| r.to_span())\n                        .err(&format!(\"resource not found: {path:?}\"));\n                    continue;\n                };\n                (*res).to_owned()\n            }\n        };\n\n        result.push(Lrc::new(Bind {\n            id: b.id,\n            range: b.range,\n            resource,\n            kind: b.kind,\n            module_id: b.module_id,\n            name: b.name,\n            object: b.object,\n            internal_bound_id: b.internal_bound_id,\n        }));\n    }\n\n    result\n}\n\n/// Describes an Encore service.\n#[derive(Debug)]\npub struct Service {\n    pub name: String,\n\n    /// Associated documentation.\n    pub doc: Option<String>,\n\n    /// The root directory of the service.\n    pub root: PathBuf,\n\n    /// The binds defined within the service.\n    pub binds: Vec<Lrc<Bind>>,\n}\n\nfn collect_services(\n    file_set: &FileSet,\n    binds: &[Lrc<Bind>],\n    discovered: Vec<DiscoveredService>,\n) -> Vec<Service> {\n    let mut services = Vec::with_capacity(discovered.len());\n    for svc in discovered.into_iter() {\n        services.push(Service {\n            name: svc.name,\n            root: svc.root,\n\n            // Filled in below.\n            binds: vec![],\n            doc: None,\n        });\n    }\n\n    // Sort the services by path so we can do a binary search below.\n    services.sort_by(|a, b| a.root.cmp(&b.root));\n\n    for b in binds {\n        let Some(range) = b.range else { continue };\n        let file_path = range.file(file_set);\n        let path: &Path = match file_path {\n            FilePath::Real(ref buf) => buf.as_path(),\n            FilePath::Custom(_) => continue,\n        };\n\n        // found is where the bind would be inserted:\n        // - Ok(idx) means an exact path match\n        // - Err(idx) means where the path would be inserted.\n        //   For this case we need to check if the path is a subdirectory of the service root.\n        let found = services.binary_search_by_key(&path, |s| s.root.as_path());\n        match found {\n            Ok(idx) => services[idx].binds.push(b.clone()),\n            Err(idx) => {\n                // Is this path a subdirectory of the preceding service root?\n                if idx > 0 {\n                    let svc = &mut services[idx - 1];\n                    if path.starts_with(&svc.root) {\n                        // If we have a service resource, copy its documentation.\n                        if let Resource::Service(s) = &b.resource {\n                            svc.doc = s.doc.clone();\n                        }\n\n                        svc.binds.push(b.clone());\n                        continue;\n                    }\n                }\n            }\n        }\n    }\n\n    services\n}\n"
  },
  {
    "path": "tsparser/src/parser/resourceparser/bind.rs",
    "content": "use std::hash::{Hash, Hasher};\nuse std::rc::Rc;\n\nuse swc_ecma_ast as ast;\n\nuse crate::parser::module_loader::ModuleId;\nuse crate::parser::resources::{Resource, ResourcePath};\nuse crate::parser::types::Object;\nuse crate::parser::Range;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub struct Id(u32);\n\nimpl From<u32> for Id {\n    fn from(id: u32) -> Self {\n        Self(id)\n    }\n}\n\n#[derive(Debug)]\npub enum ResourceOrPath {\n    Resource(Resource),\n    Path(ResourcePath),\n}\n\n#[derive(Debug)]\npub enum BindName {\n    Named(ast::Ident),\n    DefaultExport,\n    Anonymous,\n}\n\nimpl BindName {\n    pub fn name(&self) -> Option<String> {\n        match self {\n            BindName::Named(ident) => Some(ident.sym.to_string()),\n            BindName::DefaultExport => Some(\"default\".to_string()),\n            BindName::Anonymous => None,\n        }\n    }\n\n    pub fn ident(&self) -> Option<&ast::Ident> {\n        if let BindName::Named(name) = self {\n            Some(name)\n        } else {\n            None\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct BindData {\n    pub resource: ResourceOrPath,\n    pub range: Range,\n\n    /// The identifier it is bound to.\n    pub ident: BindName,\n    // The object it is bound to, if any.\n    pub object: Option<Rc<Object>>,\n    pub kind: BindKind,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum BindKind {\n    Create,\n    Reference,\n}\n\n#[derive(Debug)]\npub struct Bind {\n    pub id: Id,\n    pub range: Option<Range>,\n    pub resource: Resource,\n    pub kind: BindKind,\n\n    /// The module the bind is defined in.\n    pub module_id: ModuleId,\n\n    /// The identifier it is bound to, if any.\n    /// None means it's an anonymous bind (e.g. `_`).\n    pub name: Option<String>,\n\n    /// The object it is bound to, if any.\n    pub object: Option<Rc<Object>>,\n\n    /// The identifier it's bound to in the source module.\n    /// None means it's an anonymous bind (e.g. `_`).\n    /// It's used for computing usage within the module itself,\n    /// where we need to know its id.\n    pub internal_bound_id: Option<ast::Id>,\n}\n\nimpl PartialEq<Self> for Bind {\n    fn eq(&self, other: &Self) -> bool {\n        self.id == other.id\n    }\n}\n\nimpl Hash for Bind {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        self.id.hash(state);\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resourceparser/mod.rs",
    "content": "use std::borrow::Cow;\nuse std::rc::Rc;\n\nuse bind::BindName;\nuse swc_common::sync::Lrc;\nuse swc_ecma_ast as ast;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::BindData;\nuse crate::parser::resourceparser::resource_parser::ResourceParserRegistry;\nuse crate::parser::resources::Resource;\nuse crate::parser::types::TypeChecker;\nuse crate::parser::{FileSet, Pos};\n\nuse self::bind::BindKind;\n\nuse super::module_loader::ModuleId;\nuse super::types::Object;\nuse super::Range;\n\npub mod bind;\npub mod paths;\npub mod resource_parser;\n\n#[derive(Debug)]\npub struct PassOneParser<'a> {\n    file_set: Lrc<FileSet>,\n    type_checker: Lrc<TypeChecker>,\n    registry: ResourceParserRegistry<'a>,\n    next_id: u32,\n}\n\n#[derive(Debug)]\npub struct UnresolvedBind {\n    pub id: bind::Id,\n    pub range: Option<Range>,\n    pub resource: bind::ResourceOrPath,\n    pub kind: BindKind,\n\n    /// The module the bind is defined in.\n    pub module_id: ModuleId,\n\n    /// The identifier it is bound to, if any.\n    /// None means it's an anonymous bind (e.g. `_`).\n    pub name: Option<String>,\n\n    /// The object it is bound to, if any.\n    pub object: Option<Rc<Object>>,\n\n    /// The identifier it's bound to in the source module.\n    /// None means it's an anonymous bind (e.g. `_`).\n    /// It's used for computing usage within the module itself,\n    /// where we need to know its id.\n    pub internal_bound_id: Option<ast::Id>,\n}\n\nimpl<'a> PassOneParser<'a> {\n    pub fn new(\n        file_set: Lrc<FileSet>,\n        type_checker: Lrc<TypeChecker>,\n        registry: ResourceParserRegistry<'a>,\n    ) -> Self {\n        Self {\n            file_set,\n            type_checker,\n            registry,\n            next_id: 0,\n        }\n    }\n\n    pub fn alloc_bind_id(&mut self) -> bind::Id {\n        self.next_id += 1;\n        self.next_id.into()\n    }\n\n    pub fn parse(\n        &mut self,\n        module: Lrc<Module>,\n        service_name: Option<&str>,\n    ) -> (Vec<Resource>, Vec<UnresolvedBind>) {\n        let parsers = self.registry.interested_parsers(&module);\n\n        let mut ctx = ResourceParseContext::new(\n            &self.file_set,\n            &self.type_checker,\n            module.clone(),\n            service_name.map(Cow::Borrowed),\n        );\n\n        for parser in parsers {\n            let num_resources = ctx.resources.len();\n            (parser.run)(&mut ctx);\n\n            // Look at any new resources to see if we have a new service.\n            // If so, update our ctx so that later parsers have up-to-date information.\n            for res in &ctx.resources[num_resources..] {\n                if let Resource::Service(svc) = res {\n                    ctx.service_name = Some(Cow::Owned(svc.name.clone()));\n                }\n            }\n        }\n\n        let mut binds = Vec::with_capacity(ctx.binds.len());\n        for b in ctx.binds {\n            self.next_id += 1;\n            binds.push(UnresolvedBind {\n                id: self.next_id.into(),\n                name: b.ident.name(),\n                object: b.object,\n                kind: b.kind,\n                resource: b.resource,\n                range: Some(b.range),\n                internal_bound_id: b.ident.ident().map(|i| i.to_id()),\n                module_id: module.id,\n            });\n        }\n\n        (ctx.resources, binds)\n    }\n}\n\n/// Encompasses the necessary information for parsing resources from a single TS file.\n#[derive(Debug)]\npub struct ResourceParseContext<'a> {\n    pub module: Lrc<Module>,\n    pub type_checker: &'a TypeChecker,\n    pub service_name: Option<Cow<'a, str>>,\n    file_set: &'a FileSet,\n    resources: Vec<Resource>,\n    binds: Vec<BindData>,\n}\n\nimpl<'a> ResourceParseContext<'a> {\n    pub fn new(\n        file_set: &'a FileSet,\n        type_checker: &'a TypeChecker,\n        module: Lrc<Module>,\n        service_name: Option<Cow<'a, str>>,\n    ) -> Self {\n        Self {\n            module,\n            type_checker,\n            file_set,\n            service_name,\n            resources: Vec::new(),\n            binds: Vec::new(),\n        }\n    }\n\n    /// Register a resource.\n    pub fn add_resource(&mut self, res: Resource) {\n        log::debug!(\"found resource {}\", res);\n        self.resources.push(res);\n    }\n\n    /// Register a bind.\n    pub fn add_bind(&mut self, bind: BindData) {\n        // Treat \"_\" as an anonymous bind.\n        let ident = match bind.ident {\n            BindName::Named(name) if name.sym == \"_\" => BindName::Anonymous,\n            x => x,\n        };\n        self.binds.push(BindData { ident, ..bind });\n    }\n\n    pub fn preceding_comments(&self, pos: Pos) -> Option<String> {\n        self.file_set.preceding_comments(&self.module.comments, pos)\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resourceparser/paths.rs",
    "content": "/// A path to a package, e.g. `@foo/bar`.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub struct PkgPath<'a>(pub &'a str);\n\n/// A path to a specific object in a package, e.g. 'Moo' in '@foo/bar'.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub struct PkgObj<'a>(pub PkgPath<'a>, pub &'a str);\n"
  },
  {
    "path": "tsparser/src/parser/resourceparser/resource_parser.rs",
    "content": "use std::collections::{HashMap, HashSet};\n\nuse swc_ecma_ast as ast;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::ResourceParseContext;\nuse crate::parser::resources::DEFAULT_RESOURCE_PARSERS;\n\n/// A parser for a specific resource type.\n#[derive(Debug)]\npub struct ResourceParser {\n    pub name: &'static str,\n\n    pub interesting_pkgs: &'static [PkgPath<'static>],\n\n    pub run: fn(&mut ResourceParseContext),\n}\n\nimpl PartialEq for ResourceParser {\n    fn eq(&self, other: &Self) -> bool {\n        self.name == other.name\n    }\n}\n\n#[derive(Debug)]\npub struct ResourceParserRegistry<'a> {\n    /// List of registered parsers.\n    parsers: Vec<&'a ResourceParser>,\n    interested_for_paths: HashMap<PkgPath<'a>, Vec<&'a ResourceParser>>,\n}\n\nimpl Default for ResourceParserRegistry<'_> {\n    fn default() -> Self {\n        Self::new(DEFAULT_RESOURCE_PARSERS)\n    }\n}\n\nimpl<'a> ResourceParserRegistry<'a> {\n    pub fn new(parsers: &[&'a ResourceParser]) -> Self {\n        let mut registry = Self {\n            parsers: Vec::new(),\n            interested_for_paths: HashMap::new(),\n        };\n        for p in parsers {\n            registry.register(p);\n        }\n        registry\n    }\n\n    /// Register a new parser.\n    pub fn register(&mut self, parser: &'a ResourceParser) {\n        self.parsers.push(parser);\n        for path in parser.interesting_pkgs {\n            self.interested_for_paths\n                .entry(*path)\n                .or_default()\n                .push(parser);\n        }\n    }\n\n    /// Compute the parsers interested in processing the given module.\n    pub fn interested_parsers(&self, module: &Module) -> Vec<&'a ResourceParser> {\n        let mut parsers = Vec::new();\n        let mut seen_parsers = HashSet::new();\n\n        // Iterate over the imports in this module and collect all the interested parsers.\n        for it in &module.ast.body {\n            if let ast::ModuleItem::ModuleDecl(ast::ModuleDecl::Import(import)) = it {\n                let path = PkgPath(&import.src.value);\n                if let Some(found) = self.interested_for_paths.get(&path) {\n                    for p in found {\n                        // If we haven't already seen the parser with that name,\n                        // add it to the list of parsers to run.\n                        if !seen_parsers.contains(p.name) {\n                            seen_parsers.insert(p.name);\n                            parsers.push(*p);\n                        }\n                    }\n                }\n            }\n        }\n\n        parsers\n    }\n}\n\n#[cfg(test)]\nmod tests {\n\n    use crate::parser::resources::apis::api::ENDPOINT_PARSER;\n\n    use crate::testutil::testparse::test_parse;\n\n    use super::*;\n\n    #[test]\n    fn test_parser_registry() {\n        let registry = ResourceParserRegistry::new(&[&ENDPOINT_PARSER]);\n\n        // Should return the parser when the import path matches.\n        {\n            let res = test_parse(\"import { APIEndpoint } from 'encore.dev/api';\");\n            let got = registry.interested_parsers(&res);\n            let want = vec![&ENDPOINT_PARSER];\n            assert_eq!(got, want);\n        }\n\n        // Should also work for wildcard imports.\n        {\n            let res = test_parse(\"import * as foo from 'encore.dev/api';\");\n            let got = registry.interested_parsers(&res);\n            let want = vec![&ENDPOINT_PARSER];\n            assert_eq!(got, want);\n        }\n\n        // Should be empty otherwise.\n        {\n            let res = test_parse(\"import { APIEndpoint } from 'encore.dev/api2';\");\n            let got = registry.interested_parsers(&res);\n            let want: Vec<&ResourceParser> = vec![];\n            assert_eq!(got, want);\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/api.rs",
    "content": "use std::collections::HashMap;\nuse std::path::PathBuf;\nuse std::str::FromStr;\n\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\nuse swc_common::{Span, Spanned};\nuse swc_ecma_ast::{self as ast, FnExpr};\n\nuse litparser::{\n    report_and_continue, LitParser, LocalRelPath, Nullable, ParseResult, Sp, ToParseErr,\n};\nuse litparser_derive::LitParser;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::{BindData, BindKind, BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::apis::encoding::{\n    describe_endpoint, describe_static_assets, describe_stream_endpoint, EndpointEncoding,\n};\nuse crate::parser::resources::parseutil::{\n    extract_bind_name, extract_type_param, iter_references, ReferenceParser, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::respath::Path;\nuse crate::parser::usageparser::{ResolveUsageData, Usage};\nuse crate::parser::{FilePath, Range};\nuse crate::span_err::ErrReporter;\n\n#[derive(Debug, Clone)]\npub struct Endpoint {\n    pub range: Range,\n    pub name: String,\n    pub name_range: Range,\n    pub service_name: String,\n    pub doc: Option<String>,\n    pub expose: bool,\n    pub raw: bool,\n    pub require_auth: bool,\n    pub tags: Vec<String>,\n    pub sensitive: bool,\n\n    /// Body limit in bytes.\n    /// None means no limit.\n    pub body_limit: Option<u64>,\n\n    pub streaming_request: bool,\n    pub streaming_response: bool,\n    pub static_assets: Option<StaticAssets>,\n\n    pub encoding: EndpointEncoding,\n}\n\n#[derive(Debug, Clone)]\npub enum Methods {\n    All,\n    Some(Vec<Method>),\n}\n\nimpl Methods {\n    pub fn to_vec(&self) -> Vec<String> {\n        let methods = match self {\n            Methods::All => Method::all(),\n            Methods::Some(vec) => vec,\n        };\n        methods.iter().map(|s| s.as_str().to_string()).collect()\n    }\n\n    pub fn contains(&self, m: Method) -> bool {\n        match self {\n            Methods::All => true,\n            Methods::Some(vec) => vec.contains(&m),\n        }\n    }\n\n    pub fn first(&self) -> Option<Method> {\n        match self {\n            Methods::All => Some(Method::Post),\n            Methods::Some(vec) => vec.first().cloned(),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]\npub enum Method {\n    Get,\n    Post,\n    Patch,\n    Put,\n    Delete,\n    Head,\n    Options,\n    Trace,\n    Connect,\n}\n\nimpl Method {\n    /// Whether the method supports a request body.\n    pub fn supports_body(&self) -> bool {\n        match self {\n            Self::Post | Self::Put | Self::Patch | Self::Connect => true,\n            Self::Get | Self::Delete | Self::Head | Self::Options | Self::Trace => false,\n        }\n    }\n\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            Self::Connect => \"CONNECT\",\n            Self::Delete => \"DELETE\",\n            Self::Get => \"GET\",\n            Self::Head => \"HEAD\",\n            Self::Options => \"OPTIONS\",\n            Self::Patch => \"PATCH\",\n            Self::Post => \"POST\",\n            Self::Put => \"PUT\",\n            Self::Trace => \"TRACE\",\n        }\n    }\n\n    /// List all methods.\n    pub fn all() -> &'static [Self] {\n        &[\n            Self::Get,\n            Self::Post,\n            Self::Patch,\n            Self::Put,\n            Self::Delete,\n            Self::Head,\n            Self::Options,\n            Self::Trace,\n            // Skip connect for now, since axum doesn't support it.\n            // Self::Connect,\n        ]\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct InvalidMethodError;\n\nimpl std::fmt::Display for InvalidMethodError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"invalid method\")\n    }\n}\n\nimpl FromStr for Method {\n    type Err = InvalidMethodError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        Ok(match s {\n            \"CONNECT\" => Self::Connect,\n            \"DELETE\" => Self::Delete,\n            \"GET\" => Self::Get,\n            \"HEAD\" => Self::Head,\n            \"OPTIONS\" => Self::Options,\n            \"PATCH\" => Self::Patch,\n            \"POST\" => Self::Post,\n            \"PUT\" => Self::Put,\n            \"TRACE\" => Self::Trace,\n            _ => return Err(InvalidMethodError),\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct StaticAssets {\n    /// Files to serve.\n    pub dir: Sp<PathBuf>,\n\n    /// File to serve when the path is not found.\n    pub not_found: Option<Sp<PathBuf>>,\n\n    /// Http Status Code to use when serving not_found\n    pub not_found_status: Option<u32>,\n\n    /// Custom HTTP headers to apply to all static files served.\n    pub headers: Option<HashMap<String, Vec<String>>>,\n}\n\npub const ENDPOINT_PARSER: ResourceParser = ResourceParser {\n    name: \"endpoint\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/api\")],\n\n    run: |pass| {\n        let module = pass.module.clone();\n\n        let service_name = match &pass.service_name {\n            Some(name) => Some(name.to_string()),\n            None => {\n                // TODO handle this in a better way.\n                match &module.file_path {\n                    FilePath::Real(ref buf) => buf\n                        .parent()\n                        .and_then(|p| p.file_name())\n                        .and_then(|s| s.to_str())\n                        .map(|s| s.to_string()),\n                    FilePath::Custom(_) => None,\n                }\n            }\n        };\n\n        let names = TrackedNames::new(&[(\"encore.dev/api\", \"api\")]);\n\n        for r in iter_references::<APIEndpointLiteral>(&module, &names) {\n            let r = report_and_continue!(r);\n            let Some(service_name) = service_name.as_ref() else {\n                module.err(\"unable to determine service name for file\");\n                continue;\n            };\n\n            let (config_span, cfg) = r.config.split();\n            let path_span = cfg.path.as_ref().map_or(config_span, |p| p.span());\n            let path_str = cfg\n                .path\n                .as_deref()\n                .cloned()\n                .unwrap_or_else(|| format!(\"/{}.{}\", &service_name, r.endpoint_name));\n\n            let path = match Path::parse(path_span, &path_str, Default::default()) {\n                Ok(path) => path,\n                Err(err) => {\n                    if cfg.path.is_some() {\n                        err.report();\n                    } else {\n                        // We don't have an explicit path, so add a note to the error.\n                        HANDLER.with(|h| {\n                            h.struct_span_err(err.span, &err.error.to_string())\n                                .span_note(\n                                    config_span,\n                                    &format!(\"no path provided, so defaulting to {path_str}\"),\n                                )\n                                .emit();\n                        });\n                    }\n                    continue;\n                }\n            };\n\n            let object = pass\n                .type_checker\n                .resolve_obj(pass.module.clone(), &ast::Expr::Ident(r.bind_name.clone()));\n\n            let methods = cfg.method.unwrap_or(Methods::Some(vec![Method::Post]));\n\n            let raw = matches!(r.kind, EndpointKind::Raw);\n\n            let mut streaming_request = false;\n            let mut streaming_response = false;\n            let mut static_assets = None;\n\n            let encoding = match r.kind {\n                EndpointKind::Typed { request, response } => {\n                    let request = match request {\n                        None => None,\n                        Some(t) => Some(pass.type_checker.resolve_type(module.clone(), &t)),\n                    };\n                    let response = match response {\n                        None => None,\n                        Some(t) => Some(pass.type_checker.resolve_type(module.clone(), &t)),\n                    };\n\n                    report_and_continue!(describe_endpoint(\n                        r.range.to_span(),\n                        pass.type_checker,\n                        methods,\n                        path,\n                        request,\n                        response,\n                        false,\n                    ))\n                }\n                EndpointKind::Raw => {\n                    report_and_continue!(describe_endpoint(\n                        r.range.to_span(),\n                        pass.type_checker,\n                        methods,\n                        path,\n                        None,\n                        None,\n                        true,\n                    ))\n                }\n                EndpointKind::TypedStream {\n                    handshake,\n                    request,\n                    response,\n                } => {\n                    streaming_request = request.is_stream();\n                    streaming_response = response.is_stream();\n\n                    // always register as a get endpoint\n                    let methods = Methods::Some(vec![Method::Get]);\n\n                    let request = request\n                        .ts_type()\n                        .map(|t| pass.type_checker.resolve_type(module.clone(), t));\n                    let response = response\n                        .ts_type()\n                        .map(|t| pass.type_checker.resolve_type(module.clone(), t));\n                    let handshake =\n                        handshake.map(|t| pass.type_checker.resolve_type(module.clone(), &t));\n\n                    report_and_continue!(describe_stream_endpoint(\n                        r.range.to_span(),\n                        pass.type_checker,\n                        methods,\n                        path,\n                        request,\n                        response,\n                        handshake,\n                    ))\n                }\n                EndpointKind::StaticAssets {\n                    dir,\n                    not_found,\n                    not_found_status,\n                } => {\n                    // Support HEAD and GET for static assets.\n                    let methods = Methods::Some(vec![Method::Head, Method::Get]);\n\n                    let FilePath::Real(module_file_path) = &module.file_path else {\n                        module\n                            .ast\n                            .err(\"cannot use custom file path for static assets\");\n                        continue;\n                    };\n\n                    // Ensure the path has at most one dynamic segment, at the end.\n                    {\n                        let mut seen_dynamic = false;\n                        for seg in &path.segments {\n                            if seen_dynamic {\n                                if seg.is_dynamic() {\n                                    path_span.err(\"static assets path cannot contain multiple dynamic segments\");\n                                } else {\n                                    path_span.err(\"static assets path cannot have static segments after dynamic segments\");\n                                }\n                                break;\n                            }\n\n                            if seg.is_dynamic() {\n                                seen_dynamic = true;\n                            }\n                        }\n                    }\n\n                    let assets_dir = dir.with(module_file_path.parent().unwrap().join(&dir.buf));\n                    #[cfg(not(target_arch = \"wasm32\"))]\n                    if let Err(err) = std::fs::read_dir(assets_dir.as_path()) {\n                        dir.err(&format!(\"unable to read static assets directory: {err}\"));\n                    }\n\n                    // Ensure the not_found file exists.\n                    let not_found_path =\n                        not_found.map(|p| p.with(module_file_path.parent().unwrap().join(&p.buf)));\n                    #[cfg(not(target_arch = \"wasm32\"))]\n                    if let Some(not_found_path) = &not_found_path {\n                        if !not_found_path.is_file() {\n                            not_found_path.err(\"file does not exist\");\n                        }\n                    }\n\n                    static_assets = Some(StaticAssets {\n                        dir: assets_dir,\n                        not_found: not_found_path,\n                        not_found_status,\n                        headers: cfg.headers.as_ref().map(|h| h.0.clone()),\n                    });\n\n                    describe_static_assets(r.range.to_span(), methods, path)\n                }\n            };\n\n            // Compute the body limit. Null means no limit. No value means 2MiB.\n            let body_limit: Option<u64> = match cfg.bodyLimit {\n                Some(Nullable::Present(val)) => Some(val),\n                Some(Nullable::Null) => None,\n                None => Some(2 * 1024 * 1024),\n            };\n\n            let resource = Resource::APIEndpoint(Lrc::new(Endpoint {\n                range: r.range,\n                name: r.endpoint_name,\n                name_range: r.bind_name.span.into(),\n                service_name: service_name.clone(),\n                doc: r.doc_comment,\n                expose: cfg.expose.unwrap_or(false),\n                require_auth: cfg.auth.unwrap_or(false),\n                raw,\n                streaming_request,\n                streaming_response,\n                static_assets,\n                body_limit,\n                encoding,\n                tags: cfg.tags.unwrap_or_default(),\n                sensitive: cfg.sensitive.unwrap_or(false),\n            }));\n\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: BindName::Named(r.bind_name),\n            });\n        }\n    },\n};\n\n#[derive(Debug)]\npub struct CallEndpointUsage {\n    pub range: Range,\n    pub service: String,\n    // None when using client ref, then we don't know what endpoint is used\n    pub endpoint: Option<String>,\n}\n\n#[derive(Debug)]\npub struct ReferenceEndpointUsage {\n    pub range: Range,\n    pub endpoint: Lrc<Endpoint>,\n}\n\npub fn resolve_endpoint_usage(_data: &ResolveUsageData, _endpoint: Lrc<Endpoint>) -> Option<Usage> {\n    // Endpoints are just normal functions in TS, so no usage to resolve.\n    None\n}\n\n#[derive(Debug)]\nstruct APIEndpointLiteral {\n    pub range: Range,\n    pub doc_comment: Option<String>,\n    pub endpoint_name: String,\n    pub bind_name: ast::Ident,\n    pub config: Sp<EndpointConfig>,\n    pub kind: EndpointKind,\n}\n\nimpl Spanned for APIEndpointLiteral {\n    fn span(&self) -> Span {\n        self.range.to_span()\n    }\n}\n\n#[derive(Debug)]\nenum ParameterType {\n    Stream(ast::TsType),\n    Single(ast::TsType),\n    None,\n}\n\nimpl ParameterType {\n    fn is_stream(&self) -> bool {\n        matches!(self, ParameterType::Stream(..))\n    }\n\n    fn ts_type(&self) -> Option<&ast::TsType> {\n        match self {\n            ParameterType::Stream(t) => Some(t),\n            ParameterType::Single(t) => Some(t),\n            ParameterType::None => None,\n        }\n    }\n}\n\n#[derive(Debug)]\nenum EndpointKind {\n    Typed {\n        request: Option<ast::TsType>,\n        response: Option<ast::TsType>,\n    },\n    TypedStream {\n        handshake: Option<ast::TsType>,\n        request: ParameterType,\n        response: ParameterType,\n    },\n    StaticAssets {\n        dir: Sp<LocalRelPath>,\n        not_found: Option<Sp<LocalRelPath>>,\n        not_found_status: Option<u32>,\n    },\n    Raw,\n}\n\n/// Custom type to parse headers as Record<string, string | string[]>\n#[derive(Debug, Clone)]\nstruct HeadersMap(HashMap<String, Vec<String>>);\n\nimpl LitParser for HeadersMap {\n    fn parse_lit(expr: &ast::Expr) -> ParseResult<Self> {\n        let ast::Expr::Object(obj) = expr else {\n            return Err(expr.parse_err(\"headers must be an object\"));\n        };\n\n        let mut map = HashMap::new();\n        for prop in &obj.props {\n            let ast::PropOrSpread::Prop(prop) = prop else {\n                continue;\n            };\n\n            let ast::Prop::KeyValue(kv) = prop.as_ref() else {\n                continue;\n            };\n\n            let key = match &kv.key {\n                ast::PropName::Ident(ident) => ident.sym.to_string(),\n                ast::PropName::Str(s) => s.value.to_string(),\n                _ => continue,\n            };\n\n            let values = match kv.value.as_ref() {\n                // Single string value\n                ast::Expr::Lit(ast::Lit::Str(s)) => vec![s.value.to_string()],\n                // Array of strings\n                ast::Expr::Array(arr) => {\n                    let mut values = Vec::new();\n                    for elem in arr.elems.iter().flatten() {\n                        if let ast::Expr::Lit(ast::Lit::Str(s)) = elem.expr.as_ref() {\n                            values.push(s.value.to_string());\n                        } else {\n                            return Err(elem\n                                .expr\n                                .parse_err(\"header value must be a string or array of strings\"));\n                        }\n                    }\n                    values\n                }\n                _ => {\n                    return Err(kv\n                        .value\n                        .parse_err(\"header value must be a string or array of strings\"))\n                }\n            };\n\n            map.insert(key, values);\n        }\n\n        Ok(HeadersMap(map))\n    }\n}\n\n#[derive(LitParser, Debug)]\n#[allow(non_snake_case)]\nstruct EndpointConfig {\n    method: Option<Methods>,\n    path: Option<Sp<String>>,\n    expose: Option<bool>,\n    auth: Option<bool>,\n    bodyLimit: Option<Nullable<u64>>,\n    tags: Option<Vec<String>>,\n    sensitive: Option<bool>,\n\n    // For static assets.\n    dir: Option<Sp<LocalRelPath>>,\n    notFound: Option<Sp<LocalRelPath>>,\n    notFoundStatus: Option<u32>,\n    headers: Option<HeadersMap>,\n}\n\nimpl ReferenceParser for APIEndpointLiteral {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for node in path.iter().rev() {\n            if let swc_ecma_visit::AstParentNodeRef::CallExpr(\n                expr,\n                swc_ecma_visit::fields::CallExprField::Callee,\n            ) = node\n            {\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n                let Some(bind_name) = extract_bind_name(path)? else {\n                    return Err(\n                        expr.parse_err(\"API endpoint must be bound to an exported variable\")\n                    );\n                };\n\n                let Some(config) = expr.args.first() else {\n                    return Err(expr.parse_err(\n                        \"API endpoint must have a config object as its first argument\",\n                    ));\n                };\n                let cfg = <Sp<EndpointConfig>>::parse_lit(config.expr.as_ref())?;\n\n                let ast::Callee::Expr(callee) = &expr.callee else {\n                    return Err(expr.callee.parse_err(\"invalid api definition expression\"));\n                };\n\n                // Determine what kind of endpoint it is.\n                return Ok(Some(match callee.as_ref() {\n                    ast::Expr::Member(member) if member.prop.is_ident_with(\"raw\") => {\n                        // Raw endpoint\n                        let Some(_) = &expr.args.get(1) else {\n                            return Err(expr.args[0].span_hi().parse_err(\n                                \"API endpoint must have a handler function as its second argument\",\n                            ));\n                        };\n\n                        Self {\n                            range: expr.span.into(),\n                            doc_comment,\n                            endpoint_name: bind_name.sym.to_string(),\n                            bind_name,\n                            config: cfg,\n                            kind: EndpointKind::Raw,\n                        }\n                    }\n\n                    ast::Expr::Member(member) if member.prop.is_ident_with(\"streamInOut\") => {\n                        // Bidirectional stream\n                        let Some(handler) = &expr.args.get(1) else {\n                            return Err(expr.args[0].span_hi().parse_err(\n                                \"API endpoint must have a handler function as its second argument\",\n                            ));\n                        };\n\n                        let Some(type_params) = expr.type_args.as_deref() else {\n                            return Err(\n                                expr.parse_err(\"missing type parameters in call to streamInOut\")\n                            );\n                        };\n\n                        let (has_handshake, _return_type) =\n                            parse_stream_endpoint_signature(&handler.expr)?;\n\n                        let type_params_count = type_params.params.len();\n                        let expected_count = if has_handshake { 3 } else { 2 };\n\n                        if type_params_count != expected_count {\n                            return Err(type_params.parse_err(format!(\"wrong number of type parameters, expected {expected_count}, found {type_params_count}\")));\n                        }\n\n                        let handshake = has_handshake\n                            .then(|| {\n                                extract_type_param(Some(type_params), 0).ok_or_else(|| {\n                                    type_params.parse_err(\"missing type for stream handshake\")\n                                })\n                            })\n                            .transpose()?;\n\n                        let Some(request) = extract_type_param(\n                            Some(type_params),\n                            if has_handshake { 1 } else { 0 },\n                        ) else {\n                            return Err(type_params.parse_err(\"missing request type parameter\"));\n                        };\n\n                        let Some(response) = extract_type_param(\n                            Some(type_params),\n                            if has_handshake { 2 } else { 1 },\n                        ) else {\n                            return Err(type_params.parse_err(\"missing response type parameter\"));\n                        };\n\n                        Self {\n                            range: expr.span.into(),\n                            doc_comment,\n                            endpoint_name: bind_name.sym.to_string(),\n                            bind_name,\n                            config: cfg,\n                            kind: EndpointKind::TypedStream {\n                                handshake: handshake.cloned(),\n                                request: ParameterType::Stream(request.clone()),\n                                response: ParameterType::Stream(response.clone()),\n                            },\n                        }\n                    }\n                    ast::Expr::Member(member) if member.prop.is_ident_with(\"streamIn\") => {\n                        // Incoming stream\n                        let Some(handler) = &expr.args.get(1) else {\n                            return Err(expr.args[0].span_hi().parse_err(\n                                \"API endpoint must have a handler function as its second argument\",\n                            ));\n                        };\n\n                        let Some(type_params) = expr.type_args.as_deref() else {\n                            return Err(\n                                expr.parse_err(\"missing type parameters in call to streamIn\")\n                            );\n                        };\n\n                        let (has_handshake, return_type) =\n                            parse_stream_endpoint_signature(&handler.expr)?;\n\n                        let type_params_count = type_params.params.len();\n                        let expected_count = if has_handshake { [2, 3] } else { [1, 2] };\n\n                        if !expected_count.contains(&type_params_count) {\n                            return Err(type_params.parse_err(format!(\"wrong number of type parameters, expected one of {expected_count:?}, found {type_params_count}\")));\n                        }\n\n                        let handshake = has_handshake\n                            .then(|| {\n                                extract_type_param(Some(type_params), 0).ok_or_else(|| {\n                                    type_params.parse_err(\"missing type for handshake\")\n                                })\n                            })\n                            .transpose()?;\n\n                        let Some(request) = extract_type_param(\n                            Some(type_params),\n                            if has_handshake { 1 } else { 0 },\n                        ) else {\n                            return Err(type_params.parse_err(\"missing request type parameter\"));\n                        };\n\n                        let response = extract_type_param(\n                            Some(type_params),\n                            if has_handshake { 2 } else { 1 },\n                        );\n\n                        let response = match response {\n                            None => match return_type {\n                                Some(t) => ParameterType::Single(t.clone()),\n                                None => ParameterType::None,\n                            },\n                            Some(t) => ParameterType::Single(t.clone()),\n                        };\n\n                        Self {\n                            range: expr.span.into(),\n                            doc_comment,\n                            endpoint_name: bind_name.sym.to_string(),\n                            bind_name,\n                            config: cfg,\n                            kind: EndpointKind::TypedStream {\n                                handshake: handshake.cloned(),\n                                request: ParameterType::Stream(request.clone()),\n                                response,\n                            },\n                        }\n                    }\n                    ast::Expr::Member(member) if member.prop.is_ident_with(\"streamOut\") => {\n                        // Outgoing stream\n                        let Some(handler) = &expr.args.get(1) else {\n                            return Err(expr.args[0].span_hi().parse_err(\n                                \"API endpoint must have a handler function as its second argument\",\n                            ));\n                        };\n\n                        let Some(type_params) = expr.type_args.as_deref() else {\n                            return Err(\n                                expr.parse_err(\"missing type parameters in call to streamOut\")\n                            );\n                        };\n\n                        let (has_handshake, _return_type) =\n                            parse_stream_endpoint_signature(&handler.expr)?;\n\n                        let type_params_count = type_params.params.len();\n                        let expected_count = if has_handshake { 2 } else { 1 };\n\n                        if type_params_count != expected_count {\n                            return Err(type_params.parse_err(format!(\"wrong number of type parameters, expected {expected_count}, found {type_params_count}\")));\n                        }\n\n                        let handshake = if has_handshake {\n                            let t = extract_type_param(Some(type_params), 0);\n                            if t.is_none() {\n                                return Err(\n                                    type_params.parse_err(\"missing type parameter for handshake\")\n                                );\n                            }\n                            t\n                        } else {\n                            None\n                        };\n\n                        let Some(response) = extract_type_param(\n                            Some(type_params),\n                            if has_handshake { 1 } else { 0 },\n                        ) else {\n                            return Err(\n                                type_params.parse_err(\"missing type parameter for response\")\n                            );\n                        };\n\n                        Self {\n                            range: expr.span.into(),\n                            doc_comment,\n                            endpoint_name: bind_name.sym.to_string(),\n                            bind_name,\n                            config: cfg,\n                            kind: EndpointKind::TypedStream {\n                                handshake: handshake.cloned(),\n                                request: ParameterType::None,\n                                response: ParameterType::Stream(response.clone()),\n                            },\n                        }\n                    }\n\n                    ast::Expr::Member(member) if member.prop.is_ident_with(\"static\") => {\n                        // Static assets\n                        let Some(dir) = cfg.dir.clone() else {\n                            return Err(config\n                                .expr\n                                .parse_err(\"static assets must have the 'dir' field set\"));\n                        };\n\n                        let not_found = cfg.notFound.clone();\n                        let not_found_status = cfg.notFoundStatus;\n\n                        Self {\n                            range: expr.span.into(),\n                            doc_comment,\n                            endpoint_name: bind_name.sym.to_string(),\n                            bind_name,\n                            config: cfg,\n                            kind: EndpointKind::StaticAssets {\n                                dir,\n                                not_found,\n                                not_found_status,\n                            },\n                        }\n                    }\n                    _ => {\n                        // Regular endpoint\n                        let Some(handler) = &expr.args.get(1) else {\n                            return Err(expr.args[0]\n                                .span_hi()\n                                .parse_err(\"API endpoint must have a handler function\"));\n                        };\n                        let (mut req, mut resp) = parse_endpoint_signature(&handler.expr)?;\n\n                        if req.is_none() {\n                            req = extract_type_param(expr.type_args.as_deref(), 0);\n                        }\n                        if resp.is_none() {\n                            resp = extract_type_param(expr.type_args.as_deref(), 1);\n                        }\n\n                        Self {\n                            range: expr.span.into(),\n                            doc_comment,\n                            endpoint_name: bind_name.sym.to_string(),\n                            bind_name,\n                            config: cfg,\n                            kind: EndpointKind::Typed {\n                                request: req.cloned(),\n                                response: resp.cloned(),\n                            },\n                        }\n                    }\n                }));\n            }\n        }\n\n        Ok(None)\n    }\n}\n\nfn parse_stream_endpoint_signature(expr: &ast::Expr) -> ParseResult<(bool, Option<&ast::TsType>)> {\n    let (has_handshake_param, type_params, return_type) = match expr {\n        ast::Expr::Fn(FnExpr { function, .. }) => (\n            function.params.len() == 2,\n            function.type_params.as_deref(),\n            function.return_type.as_deref(),\n        ),\n        ast::Expr::Arrow(arrow) => (\n            arrow.params.len() == 2,\n            arrow.type_params.as_deref(),\n            arrow.return_type.as_deref(),\n        ),\n        _ => return Ok((false, None)),\n    };\n\n    if let Some(type_params) = type_params {\n        return Err(type_params.parse_err(\"stream endpoint handler cannot have type parameters\"));\n    }\n\n    let return_type = return_type.map(|t| t.type_ann.as_ref());\n\n    Ok((has_handshake_param, return_type))\n}\n\nfn parse_endpoint_signature(\n    expr: &ast::Expr,\n) -> ParseResult<(Option<&ast::TsType>, Option<&ast::TsType>)> {\n    let (req_param, type_params, return_type) = match expr {\n        ast::Expr::Fn(func) => (\n            func.function.params.first().map(|p| &p.pat),\n            func.function.type_params.as_deref(),\n            func.function.return_type.as_deref(),\n        ),\n        ast::Expr::Arrow(arrow) => (\n            arrow.params.first(),\n            arrow.type_params.as_deref(),\n            arrow.return_type.as_deref(),\n        ),\n        _ => return Ok((None, None)),\n    };\n\n    if let Some(type_params) = type_params {\n        return Err(type_params.parse_err(\"endpoint handler cannot have type parameters\"));\n    }\n\n    let req_type = match req_param {\n        None => None,\n        Some(param) => match &param {\n            ast::Pat::Ident(pat) => pat.type_ann.as_deref(),\n            ast::Pat::Array(pat) => pat.type_ann.as_deref(),\n            ast::Pat::Rest(pat) => pat.type_ann.as_deref(),\n            ast::Pat::Object(pat) => pat.type_ann.as_deref(),\n\n            ast::Pat::Assign(_) | ast::Pat::Invalid(_) | ast::Pat::Expr(_) => None,\n        },\n    };\n\n    let req = req_type.map(|t| t.type_ann.as_ref());\n    let resp = return_type.map(|t| t.type_ann.as_ref());\n\n    Ok((req, resp))\n}\n\nimpl LitParser for Methods {\n    fn parse_lit(expr: &ast::Expr) -> ParseResult<Self> {\n        Ok(match expr {\n            ast::Expr::Lit(ast::Lit::Str(s)) => {\n                if s.value.as_ref() == \"*\" {\n                    Self::All\n                } else {\n                    match Method::from_str(s.value.as_ref()) {\n                        Ok(m) => Self::Some(vec![m]),\n                        Err(err) => {\n                            return Err(s.parse_err(format!(\"invalid method: {err}\")));\n                        }\n                    }\n                }\n            }\n            ast::Expr::Array(arr) => {\n                let mut methods = Vec::with_capacity(arr.elems.len());\n                for ast::ExprOrSpread { expr, .. } in arr.elems.iter().flatten() {\n                    if let ast::Expr::Lit(ast::Lit::Str(s)) = expr.as_ref() {\n                        if s.value.as_ref() == \"*\" {\n                            return if arr.elems.len() > 1 {\n                                Err(arr\n                                    .parse_err(\"invalid methods: cannot mix * and other methods\"))\n                            } else {\n                                Ok(Self::All)\n                            };\n                        }\n                        let m = Method::from_str(s.value.as_ref())\n                            .map_err(|err| s.parse_err(err.to_string()))?;\n                        methods.push(m);\n                    }\n                }\n                methods.sort();\n                methods.dedup();\n                Self::Some(methods)\n            }\n            _ => {\n                return Err(expr.parse_err(\"invalid methods: must be string or array of strings\"));\n            }\n        })\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/authhandler.rs",
    "content": "use litparser::{report_and_continue, ParseResult, ToParseErr};\nuse swc_common::sync::Lrc;\nuse swc_ecma_ast as ast;\nuse swc_ecma_ast::TsTypeParamInstantiation;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::{BindData, BindKind, BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::apis::encoding::{describe_auth_handler, AuthHandlerEncoding};\nuse crate::parser::resources::parseutil::{\n    extract_bind_name, iter_references, ReferenceParser, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::{FilePath, Range};\nuse crate::span_err::ErrReporter;\n\nuse super::encoding::iface_fields;\n\n#[derive(Debug, Clone)]\npub struct AuthHandler {\n    pub range: Range,\n    pub name: String,\n    pub service_name: String,\n    pub doc: Option<String>,\n    pub encoding: AuthHandlerEncoding,\n}\n\npub const AUTHHANDLER_PARSER: ResourceParser = ResourceParser {\n    name: \"authhandler\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/auth\")],\n\n    run: |pass| {\n        let module = pass.module.clone();\n\n        let service_name = match &pass.service_name {\n            Some(name) => Some(name.to_string()),\n            None => {\n                // TODO handle this in a better way.\n                match &module.file_path {\n                    FilePath::Real(ref buf) => buf\n                        .parent()\n                        .and_then(|p| p.file_name())\n                        .and_then(|s| s.to_str())\n                        .map(|s| s.to_string()),\n                    FilePath::Custom(_) => None,\n                }\n            }\n        };\n\n        let names = TrackedNames::new(&[(\"encore.dev/auth\", \"authHandler\")]);\n\n        'RefLoop: for r in iter_references::<AuthHandlerLiteral>(&module, &names) {\n            let r = report_and_continue!(r);\n            let Some(service_name) = service_name.as_ref() else {\n                module.err(\"unable to determine service name for file\");\n                continue;\n            };\n\n            let request = pass.type_checker.resolve_type(module.clone(), &r.request);\n            let response = pass.type_checker.resolve_type(module.clone(), &r.response);\n\n            let fields = match iface_fields(pass.type_checker, &request) {\n                Ok(fields) => fields,\n                Err(e) => {\n                    e.report();\n                    continue;\n                }\n            };\n\n            for (_, v) in fields {\n                if !v.is_custom() {\n                    v.range().to_span().err(\n                        \"authHandler parameter type can only consist of Query and Header fields\",\n                    );\n                    continue 'RefLoop;\n                }\n            }\n\n            let object = pass\n                .type_checker\n                .resolve_obj(pass.module.clone(), &ast::Expr::Ident(r.bind_name.clone()));\n\n            let encoding = describe_auth_handler(pass.type_checker.state(), request, response);\n\n            let resource = Resource::AuthHandler(Lrc::new(AuthHandler {\n                range: r.range,\n                name: r.endpoint_name,\n                service_name: service_name.to_string(),\n                doc: r.doc_comment,\n                encoding,\n            }));\n\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: BindName::Named(r.bind_name),\n            });\n        }\n    },\n};\n\n#[derive(Debug)]\nstruct AuthHandlerLiteral {\n    pub range: Range,\n    pub doc_comment: Option<String>,\n    pub endpoint_name: String,\n    pub bind_name: ast::Ident,\n    pub request: ast::TsType,\n    pub response: ast::TsType,\n}\n\nimpl ReferenceParser for AuthHandlerLiteral {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for node in path.iter().rev() {\n            if let swc_ecma_visit::AstParentNodeRef::CallExpr(\n                expr,\n                swc_ecma_visit::fields::CallExprField::Callee,\n            ) = node\n            {\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n                let Some(bind_name) = extract_bind_name(path)? else {\n                    return Err(expr.parse_err(\"auth handler must be bound to a variable\"));\n                };\n\n                let Some(handler) = &expr.args.first() else {\n                    return Err(expr.parse_err(\n                        \"auth handler must have a handler function as its first argument\",\n                    ));\n                };\n                let (mut req, mut resp) = parse_auth_handler_signature(&handler.expr)?;\n\n                if req.is_none() {\n                    req = extract_type_param(expr.type_args.as_deref(), 0);\n                }\n                if resp.is_none() {\n                    resp = extract_type_param(expr.type_args.as_deref(), 1);\n                }\n\n                let Some(req) = req else {\n                    return Err(expr\n                        .parse_err(\"auth handler must have an explicitly defined parameter type\"));\n                };\n                let Some(resp) = resp else {\n                    return Err(\n                        expr.parse_err(\"auth handler must have an explicitly defined result type\")\n                    );\n                };\n\n                return Ok(Some(Self {\n                    range: expr.span.into(),\n                    doc_comment,\n                    endpoint_name: bind_name.sym.to_string(),\n                    bind_name,\n                    request: req.clone(),\n                    response: resp.clone(),\n                }));\n            }\n        }\n        Ok(None)\n    }\n}\n\nfn parse_auth_handler_signature(\n    expr: &ast::Expr,\n) -> ParseResult<(Option<&ast::TsType>, Option<&ast::TsType>)> {\n    let (req_param, type_params, return_type) = match expr {\n        ast::Expr::Fn(func) => (\n            func.function.params.first().map(|p| &p.pat),\n            func.function.type_params.as_deref(),\n            func.function.return_type.as_deref(),\n        ),\n        ast::Expr::Arrow(arrow) => (\n            arrow.params.first(),\n            arrow.type_params.as_deref(),\n            arrow.return_type.as_deref(),\n        ),\n        _ => return Ok((None, None)),\n    };\n\n    if let Some(type_params) = &type_params {\n        return Err(type_params.parse_err(\"auth handler cannot have type parameters\"));\n    }\n\n    let req_type = match req_param {\n        None => None,\n        Some(param) => match &param {\n            ast::Pat::Ident(pat) => pat.type_ann.as_deref(),\n            ast::Pat::Array(pat) => pat.type_ann.as_deref(),\n            ast::Pat::Rest(pat) => pat.type_ann.as_deref(),\n            ast::Pat::Object(pat) => pat.type_ann.as_deref(),\n\n            ast::Pat::Assign(_) | ast::Pat::Invalid(_) | ast::Pat::Expr(_) => None,\n        },\n    };\n\n    let req = req_type.map(|t| t.type_ann.as_ref());\n    let resp = return_type.map(|t| t.type_ann.as_ref());\n\n    Ok((req, resp))\n}\n\nfn extract_type_param(\n    params: Option<&TsTypeParamInstantiation>,\n    idx: usize,\n) -> Option<&ast::TsType> {\n    let params = params?;\n    let param = params.params.get(idx)?;\n    Some(param.as_ref())\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/encoding.rs",
    "content": "use std::collections::HashMap;\n\nuse litparser::{ParseResult, Sp, ToParseErr};\nuse swc_common::Span;\nuse thiserror::Error;\n\nuse crate::parser::resources::apis::api::{Method, Methods};\nuse crate::parser::respath::Path;\nuse crate::parser::types::{\n    drop_empty_or_void, unwrap_promise, unwrap_validated, validation, Basic, Custom, FieldName,\n    Interface, InterfaceField, ResolveState, Type, TypeChecker, WireLocation, WireSpec,\n};\nuse crate::parser::Range;\nuse crate::span_err::{ErrorWithSpanExt, SpErr};\n\n/// Describes how an API endpoint can be encoded on the wire.\n#[derive(Debug, Clone)]\npub struct EndpointEncoding {\n    /// The endpoint's definition span\n    pub span: Span,\n\n    /// The endpoint's API path.\n    pub path: Path,\n\n    /// The methods the endpoint can be called with.\n    pub methods: Methods,\n\n    /// The default method to use for calling this endpoint.\n    pub default_method: Method,\n\n    pub req: Vec<RequestEncoding>,\n    pub resp: ResponseEncoding,\n\n    /// Schema for the websocket handshake, if stream.\n    pub handshake: Option<RequestEncoding>,\n\n    /// The raw request and schemas, from the source code.\n    pub raw_handshake_schema: Option<Sp<Type>>,\n    pub raw_req_schema: Option<Sp<Type>>,\n    pub raw_resp_schema: Option<Sp<Type>>,\n}\n\nimpl EndpointEncoding {\n    pub fn default_request_encoding(&self) -> &RequestEncoding {\n        &self.req[0]\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]\npub enum ParamLocation {\n    Path,\n    Header,\n    Query,\n    Body,\n    Cookie,\n}\n\n#[derive(Debug, Clone)]\npub enum ParamData {\n    Path { index: usize },\n    Header { header: String },\n    Query { query: String },\n    Body,\n    Cookie,\n    HTTPStatus,\n}\n\n#[derive(Debug, Clone)]\npub struct Param {\n    pub name: String,\n    pub loc: ParamData,\n    pub typ: Type,\n    pub optional: bool,\n    pub range: Range,\n}\n\n#[derive(Debug, Clone)]\npub struct RequestEncoding {\n    /// The method(s) this encoding is for.\n    pub methods: Methods,\n\n    /// Parsed params.\n    pub params: Vec<Param>,\n}\n\n#[derive(Debug, Clone)]\npub struct ResponseEncoding {\n    /// Parsed params.\n    pub params: Vec<Param>,\n}\n\n#[derive(Debug, Clone)]\npub struct AuthHandlerEncoding {\n    pub auth_param: Sp<Type>,\n    pub auth_data: Sp<Type>,\n}\n\nimpl RequestEncoding {\n    pub fn path(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Path { .. }))\n    }\n\n    pub fn header(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Header { .. }))\n    }\n\n    pub fn query(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Query { .. }))\n    }\n\n    pub fn body(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Body))\n    }\n\n    pub fn cookie(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Cookie))\n    }\n}\n\nimpl ResponseEncoding {\n    pub fn cookie(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Cookie))\n    }\n\n    pub fn header(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Header { .. }))\n    }\n\n    pub fn body(&self) -> impl Iterator<Item = &Param> {\n        self.params\n            .iter()\n            .filter(|p| matches!(p.loc, ParamData::Body))\n    }\n\n    pub fn http_status(&self) -> Option<&Param> {\n        self.params\n            .iter()\n            .find(|p| matches!(p.loc, ParamData::HTTPStatus))\n    }\n}\n\npub fn describe_stream_endpoint(\n    def_span: Span,\n    tc: &TypeChecker,\n    methods: Methods,\n    path: Path,\n    req: Option<Sp<Type>>,\n    resp: Option<Sp<Type>>,\n    handshake: Option<Sp<Type>>,\n) -> ParseResult<EndpointEncoding> {\n    let resp = if let Some(resp) = resp {\n        let (span, resp) = resp.split();\n        drop_empty_or_void(unwrap_promise(tc.state(), &resp).clone()).map(|t| Sp::new(span, t))\n    } else {\n        None\n    };\n\n    let default_method = default_method(&methods);\n\n    let (handshake_enc, _req_schema) = describe_req(\n        def_span,\n        tc,\n        &Methods::Some(vec![Method::Get]),\n        Some(&path),\n        &handshake,\n        false,\n    )?;\n\n    let handshake_enc = match handshake_enc.as_slice() {\n        [] => None,\n        [ref enc] => Some(enc.clone()),\n        _ => return Err(def_span.parse_err(\"unexpected handshake encoding\")),\n    };\n\n    let (req_enc, _req_schema) = if handshake_enc.is_some() {\n        describe_req(def_span, tc, &methods, None, &req, false)?\n    } else {\n        describe_req(def_span, tc, &methods, Some(&path), &req, false)?\n    };\n\n    let (resp_enc, _resp_schema) = describe_resp(tc, &methods, &resp)?;\n\n    let path = if let Some(ref enc) = handshake_enc {\n        rewrite_path_types(enc, path, false)?\n    } else {\n        path\n    };\n\n    Ok(EndpointEncoding {\n        span: def_span,\n        path,\n        methods,\n        default_method,\n        req: req_enc,\n        resp: resp_enc,\n        handshake: handshake_enc,\n        raw_handshake_schema: handshake,\n        raw_req_schema: req,\n        raw_resp_schema: resp,\n    })\n}\n\npub fn describe_endpoint(\n    def_span: Span,\n    tc: &TypeChecker,\n    methods: Methods,\n    path: Path,\n    req: Option<Sp<Type>>,\n    resp: Option<Sp<Type>>,\n    raw: bool,\n) -> ParseResult<EndpointEncoding> {\n    let resp = if let Some(resp) = resp {\n        let (span, resp) = resp.split();\n        drop_empty_or_void(unwrap_promise(tc.state(), &resp).clone()).map(|t| Sp::new(span, t))\n    } else {\n        None\n    };\n\n    let default_method = default_method(&methods);\n\n    let (req_enc, _req_schema) = describe_req(def_span, tc, &methods, Some(&path), &req, raw)?;\n    let (resp_enc, _resp_schema) = describe_resp(tc, &methods, &resp)?;\n\n    let path = rewrite_path_types(&req_enc[0], path, raw)?;\n\n    Ok(EndpointEncoding {\n        span: def_span,\n        path,\n        methods,\n        default_method,\n        req: req_enc,\n        resp: resp_enc,\n        handshake: None,\n        raw_handshake_schema: None,\n        raw_req_schema: req,\n        raw_resp_schema: resp,\n    })\n}\n\npub fn describe_static_assets(def_span: Span, methods: Methods, path: Path) -> EndpointEncoding {\n    EndpointEncoding {\n        span: def_span,\n        path,\n        methods: methods.clone(),\n        default_method: Method::Get,\n        req: vec![RequestEncoding {\n            methods,\n            params: vec![],\n        }],\n        resp: ResponseEncoding { params: vec![] },\n        handshake: None,\n        raw_handshake_schema: None,\n        raw_req_schema: None,\n        raw_resp_schema: None,\n    }\n}\n\nfn describe_req(\n    def_span: Span,\n    tc: &TypeChecker,\n    methods: &Methods,\n    path: Option<&Path>,\n    req_schema: &Option<Sp<Type>>,\n    raw: bool,\n) -> ParseResult<(Vec<RequestEncoding>, Option<FieldMap>)> {\n    let Some(req_schema) = req_schema else {\n        // We don't have any request schema. This is valid if and only if\n        // we have no path parameters or it's a raw endpoint.\n        if path.is_none() || !path.unwrap().has_dynamic_segments() || raw {\n            return Ok((\n                vec![RequestEncoding {\n                    methods: methods.clone(),\n                    params: vec![],\n                }],\n                None,\n            ));\n        } else {\n            return Err(\n                def_span.parse_err(\"request schema must be defined when having path parameters\")\n            );\n        }\n    };\n\n    let mut fields =\n        iface_fields(tc, req_schema).map_err(|err| err.span.parse_err(err.error.to_string()))?;\n    let path_params = if let Some(path) = path {\n        extract_path_params(path, &mut fields)?\n    } else {\n        vec![]\n    };\n\n    // If there are no fields remaining, we can use this encoding for all methods.\n    if fields.is_empty() {\n        return Ok((\n            vec![RequestEncoding {\n                methods: methods.clone(),\n                params: path_params,\n            }],\n            None,\n        ));\n    }\n\n    // Otherwise, the fields should be grouped by location depending on the method.\n    let mut encodings = Vec::new();\n\n    for (loc, methods) in split_by_loc(methods) {\n        let mut params = path_params.clone();\n        params.extend(extract_loc_params(&fields, loc)?);\n        encodings.push(RequestEncoding {\n            methods: Methods::Some(methods),\n            params,\n        });\n    }\n\n    Ok((encodings, Some(fields)))\n}\n\nfn describe_resp(\n    tc: &TypeChecker,\n    _methods: &Methods,\n    resp_schema: &Option<Sp<Type>>,\n) -> ParseResult<(ResponseEncoding, Option<FieldMap>)> {\n    let Some(resp_schema) = resp_schema else {\n        return Ok((ResponseEncoding { params: vec![] }, None));\n    };\n\n    let fields =\n        iface_fields(tc, resp_schema).map_err(|err| err.span.parse_err(err.error.to_string()))?;\n\n    let params = extract_loc_params(&fields, ParamLocation::Body)?;\n\n    let fields = if fields.is_empty() {\n        None\n    } else {\n        Some(fields)\n    };\n\n    Ok((ResponseEncoding { params }, fields))\n}\n\npub fn describe_auth_handler(\n    ctx: &ResolveState,\n    params: Sp<Type>,\n    response: Sp<Type>,\n) -> AuthHandlerEncoding {\n    let (span, response) = response.split();\n    let response = unwrap_promise(ctx, &response).clone();\n\n    AuthHandlerEncoding {\n        auth_param: params,\n        auth_data: Sp::new(span, response),\n    }\n}\n\nfn default_method(methods: &Methods) -> Method {\n    match methods {\n        Methods::All => Method::Post,\n        Methods::Some(methods) => {\n            if methods.contains(&Method::Post) {\n                Method::Post\n            } else {\n                methods[0]\n            }\n        }\n    }\n}\n\nfn split_by_loc(methods: &Methods) -> Vec<(ParamLocation, Vec<Method>)> {\n    let methods = match methods {\n        Methods::All => Method::all(),\n        Methods::Some(methods) => methods,\n    };\n\n    let mut locs = HashMap::new();\n    for m in methods {\n        let loc = if m.supports_body() {\n            ParamLocation::Body\n        } else {\n            ParamLocation::Query\n        };\n        locs.entry(loc).or_insert(Vec::new()).push(*m);\n    }\n\n    let mut items: Vec<_> = locs.into_iter().collect();\n    items.sort();\n    items\n}\n\npub type FieldMap = HashMap<String, Field>;\n\n#[derive(Debug)]\npub struct Field {\n    name: String,\n    typ: Type,\n    optional: bool,\n    custom: Option<WireSpec>,\n    range: Range,\n}\n\nimpl Field {\n    pub fn is_custom(&self) -> bool {\n        self.custom.is_some()\n    }\n\n    pub fn range(&self) -> Range {\n        self.range\n    }\n}\n\n#[derive(Error, Debug)]\npub enum Error {\n    #[error(\"expected named interface type, found {0}\")]\n    ExpectedNamedInterfaceType(String),\n    #[error(\"invalid custom type field\")]\n    InvalidCustomType(#[source] anyhow::Error),\n}\n\npub(crate) fn iface_fields<'a>(\n    tc: &'a TypeChecker,\n    typ: &'a Sp<Type>,\n) -> Result<FieldMap, SpErr<Error>> {\n    use crate::parser::resources::parseutil::resolve_interface;\n\n    fn to_fields(iface: &Interface) -> Result<FieldMap, SpErr<Error>> {\n        let mut map = HashMap::new();\n        for f in &iface.fields {\n            if let FieldName::String(name) = &f.name {\n                map.insert(name.clone(), rewrite_custom_type_field(f, name));\n            }\n        }\n        Ok(map)\n    }\n\n    let span = typ.span();\n    let typ = unwrap_promise(tc.state(), typ);\n\n    match typ {\n        Type::Basic(Basic::Void) => Ok(HashMap::new()),\n        _ => match resolve_interface(tc, &Sp::new(span, typ.clone())) {\n            Some(iface) => to_fields(&iface),\n            None => Err(Error::ExpectedNamedInterfaceType(format!(\"{typ:?}\")).with_span(span)),\n        },\n    }\n}\n\nfn extract_path_params(path: &Path, fields: &mut FieldMap) -> ParseResult<Vec<Param>> {\n    let mut params = Vec::new();\n    for (index, seg) in path.dynamic_segments().enumerate() {\n        let name = seg.lit_or_name();\n        let Some(f) = fields.remove(name) else {\n            return Err(seg.parse_err(\"path parameter not found in request schema\"));\n        };\n        params.push(Param {\n            name: name.to_string(),\n            loc: ParamData::Path { index },\n            typ: f.typ.clone(),\n            optional: f.optional,\n            range: f.range,\n        });\n    }\n\n    Ok(params)\n}\n\nfn extract_loc_params(fields: &FieldMap, default_loc: ParamLocation) -> ParseResult<Vec<Param>> {\n    let mut params = Vec::new();\n\n    for f in fields.values() {\n        let get_name = |spec: &WireSpec, field_name: &str| -> String {\n            spec.name_override\n                .as_deref()\n                .unwrap_or(field_name)\n                .to_string()\n        };\n\n        // Determine the parameter data based on location\n        let param_data = match &f.custom {\n            Some(spec) => match spec.location {\n                WireLocation::Header => ParamData::Header {\n                    header: get_name(spec, &f.name),\n                },\n                WireLocation::Query => ParamData::Query {\n                    query: get_name(spec, &f.name),\n                },\n                WireLocation::PubSubAttr => ParamData::Body,\n                WireLocation::Cookie => ParamData::Cookie,\n                WireLocation::HttpStatus => ParamData::HTTPStatus,\n            },\n            None => match default_loc {\n                ParamLocation::Query => ParamData::Query {\n                    query: f.name.clone(),\n                },\n                ParamLocation::Body => ParamData::Body,\n                ParamLocation::Cookie => ParamData::Cookie,\n                ParamLocation::Header => ParamData::Header {\n                    header: f.name.clone(),\n                },\n                ParamLocation::Path => {\n                    return Err(f\n                        .range\n                        .to_span()\n                        .parse_err(\"path params are not supported as a default loc\"))\n                }\n            },\n        };\n\n        params.push(Param {\n            name: f.name.clone(),\n            loc: param_data,\n            typ: f.typ.clone(),\n            optional: f.optional,\n            range: f.range,\n        });\n    }\n    Ok(params)\n}\n\nfn rewrite_path_types(req: &RequestEncoding, path: Path, raw: bool) -> ParseResult<Path> {\n    use crate::parser::respath::{Segment, ValueType};\n    // Get the path params into a map, keyed by name.\n    let path_params = req\n        .path()\n        .map(|param| (param.name.as_str(), param))\n        .collect::<HashMap<_, _>>();\n\n    fn typ_to_value_type(param: &Param) -> ParseResult<(ValueType, Option<validation::Expr>)> {\n        // Unwrap any validation expression before we check the type.\n        let (typ, expr) = unwrap_validated(&param.typ);\n        if let Some(expr) = &expr {\n            if let Err(err) = expr.supports_type(typ) {\n                return Err(param.range.parse_err(err.to_string()));\n            }\n        }\n\n        match typ {\n            Type::Basic(Basic::String) => Ok((ValueType::String, expr.cloned())),\n            Type::Basic(Basic::Boolean) => Ok((ValueType::Bool, expr.cloned())),\n            Type::Basic(Basic::Number | Basic::BigInt) => Ok((ValueType::Int, expr.cloned())),\n            typ => Err(param\n                .range\n                .to_span()\n                .parse_err(format!(\"unsupported path parameter type: {typ:?}\"))),\n        }\n    }\n\n    let resolve_value_type = |span: Span, name: &str| {\n        match path_params.get(name) {\n            Some(param) => typ_to_value_type(param),\n            None => {\n                // Raw endpoints assume path params are strings.\n                if raw {\n                    Ok((ValueType::String, None))\n                } else {\n                    Err(span.parse_err(\"path parameter not found in request schema\"))\n                }\n            }\n        }\n    };\n\n    let mut segments = Vec::with_capacity(path.segments.len());\n    for seg in path.segments.into_iter() {\n        let (seg_span, seg) = seg.split();\n        let seg = match seg {\n            Segment::Literal(_) => seg,\n            Segment::Param { name, .. } => {\n                let (value_type, validation) = resolve_value_type(seg_span, &name)?;\n                Segment::Param {\n                    name,\n                    value_type,\n                    validation,\n                }\n            }\n            Segment::Wildcard { name, .. } => {\n                let (_, validation) = resolve_value_type(seg_span, &name)?;\n                Segment::Wildcard { name, validation }\n            }\n            Segment::Fallback { name, .. } => {\n                let (_, validation) = resolve_value_type(seg_span, &name)?;\n                Segment::Fallback { name, validation }\n            }\n        };\n        segments.push(Sp::new(seg_span, seg));\n    }\n\n    Ok(Path {\n        span: path.span,\n        segments,\n    })\n}\n\npub fn resolve_wire_spec(typ: &Type) -> Option<&WireSpec> {\n    match typ {\n        Type::Custom(Custom::WireSpec(spec)) => Some(spec),\n        Type::Validated(v) => resolve_wire_spec(&v.typ),\n        Type::Optional(opt) => resolve_wire_spec(&opt.0),\n        _ => None,\n    }\n}\n\nfn rewrite_custom_type_field(field: &InterfaceField, field_name: &str) -> Field {\n    let mut standard_field = Field {\n        name: field_name.to_string(),\n        typ: field.typ.clone(),\n        optional: field.optional,\n        custom: None,\n        range: field.range,\n    };\n\n    if let Some(spec) = resolve_wire_spec(&field.typ) {\n        standard_field.custom = Some(spec.clone());\n    };\n\n    standard_field\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/gateway.rs",
    "content": "use litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_ecma_ast as ast;\n\nuse litparser::{report_and_continue, LitParser};\n\nuse crate::parser::resourceparser::bind::{BindData, BindKind, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    iter_references, resolve_object_for_bind_name, TrackedNames, UnnamedClassResource,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::types::Object;\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\n\n#[derive(Debug, Clone)]\npub struct Gateway {\n    pub range: Range,\n    pub name: String,\n    pub doc: Option<String>,\n    pub auth_handler: Option<Lrc<Object>>,\n}\n\n#[allow(non_snake_case, dead_code)]\n#[derive(Debug, LitParser)]\nstruct DecodedGatewayConfig {\n    authHandler: Option<ast::Expr>,\n}\n\npub const GATEWAY_PARSER: ResourceParser = ResourceParser {\n    name: \"gateway\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/api\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/api\", \"Gateway\")]);\n\n        let module = pass.module.clone();\n        type Res = UnnamedClassResource<DecodedGatewayConfig>;\n        for r in iter_references::<Res>(&module, &names) {\n            let r = report_and_continue!(r);\n\n            let object =\n                resolve_object_for_bind_name(pass.type_checker, pass.module.clone(), &r.bind_name);\n\n            let auth_handler = if let Some(expr) = r.config.authHandler {\n                let Some(obj) = pass.type_checker.resolve_obj(pass.module.clone(), &expr) else {\n                    expr.err(\"cannot resolve auth handler\");\n                    continue;\n                };\n                Some(obj)\n            } else {\n                None\n            };\n\n            let resource = Resource::Gateway(Lrc::new(Gateway {\n                range: r.range,\n                name: \"api-gateway\".into(),\n                doc: r.doc_comment,\n                auth_handler,\n            }));\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: r.bind_name,\n            });\n        }\n    },\n};\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/mod.rs",
    "content": "pub mod api;\npub mod authhandler;\npub mod encoding;\npub mod gateway;\npub mod service;\npub mod service_client;\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/service.rs",
    "content": "use litparser::{report_and_continue, ParseResult};\nuse swc_common::sync::Lrc;\nuse swc_common::Spanned;\nuse swc_ecma_ast::{self as ast};\n\nuse litparser::LitParser;\nuse litparser_derive::LitParser;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::{BindData, BindKind};\nuse crate::parser::resourceparser::bind::{BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    extract_bind_name, extract_resource_name, iter_references, TrackedNames,\n};\nuse crate::parser::resources::parseutil::{is_default_export, ReferenceParser};\nuse crate::parser::resources::Resource;\nuse crate::parser::{FilePath, Range};\nuse crate::span_err::ErrReporter;\n\n#[derive(Debug, Clone)]\npub struct Service {\n    pub range: Range,\n    pub name: String,\n    pub doc: Option<String>,\n}\n\n#[allow(dead_code)]\n#[derive(LitParser, Default, Debug)]\nstruct DecodedServiceConfig {\n    middlewares: Option<ast::Expr>,\n}\n\npub static SERVICE_PARSER: ResourceParser = ResourceParser {\n    name: \"service\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/service\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/service\", \"Service\")]);\n\n        let module = pass.module.clone();\n        {\n            for (i, r) in iter_references::<ServiceLiteral>(&module, &names).enumerate() {\n                let r = report_and_continue!(r);\n\n                if i > 0 {\n                    r.range\n                        .err(\"cannot have multiple service declarations in the same module\");\n                    continue;\n                }\n\n                // This resource is only allowed to be defined in a module named \"encore.service.ts\".\n                // Check that that is the case.\n                match &pass.module.file_path {\n                    FilePath::Real(buf) if buf.ends_with(\"encore.service.ts\") => {}\n                    _ => {\n                        r.range\n                            .err(\"service declarations are only allowed in encore.service.ts\");\n                        continue;\n                    }\n                }\n\n                let resource = Resource::Service(Lrc::new(Service {\n                    range: r.range,\n                    name: r.resource_name,\n                    doc: r.doc_comment,\n                }));\n                pass.add_resource(resource.clone());\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Resource(resource),\n                    object: None,\n                    kind: BindKind::Create,\n                    ident: BindName::DefaultExport,\n                });\n            }\n        }\n    },\n};\n\n#[allow(dead_code)]\n#[derive(Debug)]\nstruct ServiceLiteral {\n    pub range: Range,\n    pub doc_comment: Option<String>,\n    pub resource_name: String,\n    pub config: Option<DecodedServiceConfig>,\n}\n\nimpl ReferenceParser for ServiceLiteral {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for node in path.iter().rev() {\n            if let swc_ecma_visit::AstParentNodeRef::NewExpr(\n                expr,\n                swc_ecma_visit::fields::NewExprField::Callee,\n            ) = node\n            {\n                let Some(args) = &expr.args else {\n                    expr.span().err(\"missing constructor arguments\");\n                    continue;\n                };\n\n                if let Some(bind_name) = extract_bind_name(path)? {\n                    bind_name\n                        .span()\n                        .err(\"service definitions should not be bound to a variable\");\n                    continue;\n                }\n\n                let resource_name = extract_resource_name(expr.span, args, 0)?;\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n\n                let config = args\n                    .get(1)\n                    .map(|arg| DecodedServiceConfig::parse_lit(&arg.expr))\n                    .transpose()?;\n\n                if !is_default_export(path, (*expr).into()) {\n                    expr.span().err(\"service must be default export\");\n                    continue;\n                }\n\n                return Ok(Some(Self {\n                    range: expr.span.into(),\n                    doc_comment,\n                    resource_name: resource_name.to_string(),\n                    config,\n                }));\n            }\n        }\n\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/apis/service_client.rs",
    "content": "use crate::parser::resources::apis::api::CallEndpointUsage;\nuse crate::parser::usageparser::{ResolveUsageData, Usage, UsageExprKind};\nuse crate::span_err::ErrReporter;\nuse swc_common::sync::Lrc;\n\n#[derive(Debug, Clone)]\npub struct ServiceClient {\n    pub service_name: String,\n}\n\npub fn resolve_service_client_usage(\n    data: &ResolveUsageData,\n    client: Lrc<ServiceClient>,\n) -> Option<Usage> {\n    match &data.expr.kind {\n        UsageExprKind::FieldAccess(field) => {\n            if field.field.sym.as_ref() == \"Client\" {\n                return None;\n            }\n\n            data.expr.err(\"invalid service client field access\");\n            None\n        }\n        UsageExprKind::MethodCall(method) => {\n            let method_name = method.method.as_ref();\n\n            Some(Usage::CallEndpoint(CallEndpointUsage {\n                range: data.expr.range,\n                service: client.service_name.clone(),\n                endpoint: if method_name == \"ref\" {\n                    None\n                } else {\n                    Some(method_name.to_string())\n                },\n            }))\n        }\n        _ => {\n            data.expr.err(\"invalid service client usage\");\n            None\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/cache.rs",
    "content": "use std::collections::HashMap;\nuse std::collections::HashSet;\nuse std::rc::Rc;\n\nuse litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_common::Span;\nuse swc_common::Spanned;\nuse swc_ecma_ast as ast;\n\nuse litparser::{report_and_continue, LitParser, Sp, ToParseErr};\n\nuse crate::parser::resourceparser::bind::{BindData, BindKind, BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    extract_bind_name, extract_type_param, is_default_export, iter_references,\n    resolve_object_for_bind_name, NamedClassResourceOptionalConfig, NamedStaticMethod,\n    ReferenceParser, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::resources::ResourcePath;\nuse crate::parser::types::Basic;\nuse crate::parser::types::FieldName;\nuse crate::parser::types::Object;\nuse crate::parser::types::Type;\nuse crate::parser::usageparser::{ResolveUsageData, Usage, UsageExprKind};\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\n\n/// Redis eviction policy.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]\npub enum EvictionPolicy {\n    NoEviction,\n    #[default]\n    AllKeysLRU,\n    AllKeysLFU,\n    AllKeysRandom,\n    VolatileLRU,\n    VolatileLFU,\n    VolatileTTL,\n    VolatileRandom,\n}\n\nimpl EvictionPolicy {\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            EvictionPolicy::NoEviction => \"noeviction\",\n            EvictionPolicy::AllKeysLRU => \"allkeys-lru\",\n            EvictionPolicy::AllKeysLFU => \"allkeys-lfu\",\n            EvictionPolicy::AllKeysRandom => \"allkeys-random\",\n            EvictionPolicy::VolatileLRU => \"volatile-lru\",\n            EvictionPolicy::VolatileLFU => \"volatile-lfu\",\n            EvictionPolicy::VolatileTTL => \"volatile-ttl\",\n            EvictionPolicy::VolatileRandom => \"volatile-random\",\n        }\n    }\n}\n\nimpl std::str::FromStr for EvictionPolicy {\n    type Err = String;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s {\n            \"noeviction\" => Ok(EvictionPolicy::NoEviction),\n            \"allkeys-lru\" => Ok(EvictionPolicy::AllKeysLRU),\n            \"allkeys-lfu\" => Ok(EvictionPolicy::AllKeysLFU),\n            \"allkeys-random\" => Ok(EvictionPolicy::AllKeysRandom),\n            \"volatile-lru\" => Ok(EvictionPolicy::VolatileLRU),\n            \"volatile-lfu\" => Ok(EvictionPolicy::VolatileLFU),\n            \"volatile-ttl\" => Ok(EvictionPolicy::VolatileTTL),\n            \"volatile-random\" => Ok(EvictionPolicy::VolatileRandom),\n            _ => Err(format!(\"unknown eviction policy: {}\", s)),\n        }\n    }\n}\n\n/// A cache cluster resource.\n#[derive(Debug, Clone)]\npub struct CacheCluster {\n    pub span: Span,\n    pub name: String,\n    pub doc: Option<String>,\n    pub eviction_policy: EvictionPolicy,\n}\n\n/// The type of keyspace (determines the value type).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum KeyspaceType {\n    /// Stores arbitrary struct/object values (serialized as JSON).\n    Struct,\n    /// Stores string values.\n    String,\n    /// Stores integer values (i64).\n    Int,\n    /// Stores float values (f64).\n    Float,\n    /// Stores list of string values.\n    StringList,\n    /// Stores list of numeric values.\n    NumberList,\n    /// Stores set of unique string values.\n    StringSet,\n    /// Stores set of unique numeric values.\n    NumberSet,\n}\n\nimpl KeyspaceType {\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            KeyspaceType::Struct => \"struct\",\n            KeyspaceType::String => \"string\",\n            KeyspaceType::Int => \"int\",\n            KeyspaceType::Float => \"float\",\n            KeyspaceType::StringList => \"string_list\",\n            KeyspaceType::NumberList => \"number_list\",\n            KeyspaceType::StringSet => \"string_set\",\n            KeyspaceType::NumberSet => \"number_set\",\n        }\n    }\n}\n\n/// A cache keyspace resource.\n#[derive(Debug, Clone)]\npub struct CacheKeyspace {\n    pub span: Span,\n    /// Reference to the cache cluster object.\n    pub cluster: Sp<Rc<Object>>,\n    pub doc: Option<String>,\n    pub keyspace_type: KeyspaceType,\n    /// The pattern for generating cache keys.\n    pub key_pattern: String,\n    /// The key type (for generating key mappers).\n    pub key_type: Sp<crate::parser::types::Type>,\n    /// The value type (for struct keyspaces).\n    pub value_type: Option<Sp<crate::parser::types::Type>>,\n}\n\n#[allow(non_snake_case)]\n#[derive(LitParser, Default, Debug)]\nstruct DecodedClusterConfig {\n    #[allow(dead_code)]\n    evictionPolicy: Option<Sp<String>>,\n}\n\n#[allow(non_snake_case)]\n#[derive(LitParser, Debug)]\nstruct DecodedKeyspaceConfig {\n    keyPattern: Sp<String>,\n    #[allow(dead_code)]\n    defaultExpiry: Option<ast::Expr>,\n}\n\n/// Specification for a keyspace constructor.\nstruct KeyspaceConstructorSpec {\n    class_name: &'static str,\n    keyspace_type: KeyspaceType,\n    /// Number of type parameters (1 for implicit value types, 2 for explicit)\n    num_type_params: usize,\n}\n\npub const CACHE_CLUSTER_PARSER: ResourceParser = ResourceParser {\n    name: \"cache_cluster\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/storage/cache\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/storage/cache\", \"CacheCluster\")]);\n\n        let module = pass.module.clone();\n        {\n            type Res = NamedClassResourceOptionalConfig<DecodedClusterConfig>;\n            for r in iter_references::<Res>(&module, &names) {\n                let r = report_and_continue!(r);\n\n                let eviction_policy = match r\n                    .config\n                    .as_ref()\n                    .and_then(|c| c.evictionPolicy.as_ref())\n                {\n                    Some(policy) => match policy.parse() {\n                        Ok(p) => p,\n                        Err(_) => {\n                            policy.span().err(\"invalid eviction policy: must be one of noeviction, allkeys-lru, allkeys-lfu, allkeys-random, volatile-lru, volatile-lfu, volatile-ttl, or volatile-random\");\n                            continue;\n                        }\n                    },\n                    None => EvictionPolicy::default(),\n                };\n\n                let object = resolve_object_for_bind_name(\n                    pass.type_checker,\n                    pass.module.clone(),\n                    &r.bind_name,\n                );\n\n                let resource = Resource::CacheCluster(Lrc::new(CacheCluster {\n                    span: r.range.to_span(),\n                    name: r.resource_name.to_string(),\n                    doc: r.doc_comment.clone(),\n                    eviction_policy,\n                }));\n\n                pass.add_resource(resource.clone());\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Resource(resource),\n                    object,\n                    kind: BindKind::Create,\n                    ident: r.bind_name,\n                });\n            }\n        }\n\n        {\n            for r in iter_references::<NamedStaticMethod>(&module, &names) {\n                let r = report_and_continue!(r);\n                let object = resolve_object_for_bind_name(\n                    pass.type_checker,\n                    pass.module.clone(),\n                    &r.bind_name,\n                );\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Path(ResourcePath::CacheCluster {\n                        name: r.resource_name,\n                    }),\n                    object,\n                    kind: BindKind::Reference,\n                    ident: r.bind_name,\n                });\n            }\n        }\n    },\n};\n\n/// All supported keyspace constructors.\nconst KEYSPACE_CONSTRUCTORS: &[KeyspaceConstructorSpec] = &[\n    KeyspaceConstructorSpec {\n        class_name: \"StringKeyspace\",\n        keyspace_type: KeyspaceType::String,\n        num_type_params: 1, // Only key type, value is implicitly string\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"IntKeyspace\",\n        keyspace_type: KeyspaceType::Int,\n        num_type_params: 1, // Only key type, value is implicitly int\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"FloatKeyspace\",\n        keyspace_type: KeyspaceType::Float,\n        num_type_params: 1, // Only key type, value is implicitly float\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"StringListKeyspace\",\n        keyspace_type: KeyspaceType::StringList,\n        num_type_params: 1, // Only key type, value is implicitly string\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"NumberListKeyspace\",\n        keyspace_type: KeyspaceType::NumberList,\n        num_type_params: 1, // Only key type, value is implicitly number\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"StringSetKeyspace\",\n        keyspace_type: KeyspaceType::StringSet,\n        num_type_params: 1, // Only key type, value is implicitly string\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"NumberSetKeyspace\",\n        keyspace_type: KeyspaceType::NumberSet,\n        num_type_params: 1, // Only key type, value is implicitly number\n    },\n    KeyspaceConstructorSpec {\n        class_name: \"StructKeyspace\",\n        keyspace_type: KeyspaceType::Struct,\n        num_type_params: 2, // Key type and value type\n    },\n];\n\npub const CACHE_KEYSPACE_PARSER: ResourceParser = ResourceParser {\n    name: \"cache_keyspace\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/storage/cache\")],\n\n    run: |pass| {\n        // Build tracked names for all keyspace constructors\n        let keyspace_names: Vec<(&str, &str)> = KEYSPACE_CONSTRUCTORS\n            .iter()\n            .map(|spec| (\"encore.dev/storage/cache\", spec.class_name))\n            .collect();\n\n        let names = TrackedNames::new(&keyspace_names);\n        let module = pass.module.clone();\n\n        // Create a map from class name to spec for quick lookup\n        let spec_map: HashMap<&str, &KeyspaceConstructorSpec> = KEYSPACE_CONSTRUCTORS\n            .iter()\n            .map(|spec| (spec.class_name, spec))\n            .collect();\n\n        for r in iter_references::<KeyspaceReference>(&module, &names) {\n            let r = report_and_continue!(r);\n\n            // Get the spec for this keyspace type\n            let Some(spec) = spec_map.get(r.class_name.as_str()) else {\n                continue;\n            };\n\n            // Validate we have the right number of type parameters\n            let type_args = r.expr.type_args.as_deref();\n            let key_type_ast = extract_type_param(type_args, 0);\n            let value_type_ast = if spec.num_type_params > 1 {\n                extract_type_param(type_args, 1)\n            } else {\n                None\n            };\n\n            if key_type_ast.is_none() {\n                r.expr.span().err(\"missing key type parameter\");\n                continue;\n            }\n\n            // Resolve the key type\n            let key_type = pass\n                .type_checker\n                .resolve_type(pass.module.clone(), key_type_ast.unwrap());\n\n            // Validate key type - disallow any/unknown\n            let is_invalid_key_type = match key_type.as_ref() {\n                Type::Basic(Basic::Any) => Some(\"'any' is not supported as a cache key type\"),\n                Type::Basic(Basic::Unknown) => {\n                    Some(\"'unknown' is not supported as a cache key type\")\n                }\n                _ => None,\n            };\n            if let Some(err_msg) = is_invalid_key_type {\n                key_type_ast.unwrap().span().err(err_msg);\n                continue;\n            }\n\n            // Resolve the value type (if explicit from type parameter, or implicit from keyspace type)\n            let value_type = if let Some(vt) = value_type_ast {\n                // Explicit value type from type parameter\n                Some(pass.type_checker.resolve_type(pass.module.clone(), vt))\n            } else {\n                // Implicit value type based on keyspace type\n                match spec.keyspace_type {\n                    KeyspaceType::String | KeyspaceType::StringList | KeyspaceType::StringSet => {\n                        Some(Sp::new(r.expr.span(), Type::Basic(Basic::String)))\n                    }\n                    KeyspaceType::Int\n                    | KeyspaceType::Float\n                    | KeyspaceType::NumberList\n                    | KeyspaceType::NumberSet => {\n                        Some(Sp::new(r.expr.span(), Type::Basic(Basic::Number)))\n                    }\n                    // Struct keyspace requires explicit value type parameter\n                    _ => None,\n                }\n            };\n\n            // Validate key pattern\n            let key_pattern = &r.config.keyPattern;\n\n            // Check for reserved prefix\n            if key_pattern.starts_with(\"__encore\") {\n                key_pattern\n                    .span()\n                    .err(\"the prefix `__encore` is reserved for internal use by Encore\");\n                continue;\n            }\n\n            // For basic (non-struct) key types, the parameter must be named `:key`\n            if matches!(key_type.as_ref(), Type::Basic(_)) {\n                // Check that any parameter segment is named \"key\"\n                let mut has_invalid_param = false;\n                let pattern_str: &str = key_pattern.as_str();\n                for segment in pattern_str.split('/') {\n                    if let Some(param_name) = segment.strip_prefix(':') {\n                        if param_name != \"key\" {\n                            key_pattern.span().err(\n                                \"KeyPattern parameter must be named ':key' for basic (non-struct) key types\",\n                            );\n                            has_invalid_param = true;\n                            break;\n                        }\n                    }\n                }\n                if has_invalid_param {\n                    continue;\n                }\n            } else {\n                // Struct key type validation: validate interface fields match key pattern\n                let key_typ = match key_type.as_ref() {\n                    Type::Named(named) => &named.underlying(pass.type_checker.state()),\n                    typ => typ,\n                };\n\n                let interface_fields = match key_typ {\n                    Type::Interface(iface) => &iface.fields,\n                    t => {\n                        key_type.span().err(&format!(\"unsupported key type: {t}\"));\n                        continue;\n                    }\n                };\n\n                // Extract parameter names from key pattern\n                let pattern_str: &str = key_pattern.as_str();\n                let mut pattern_params: HashSet<&str> = HashSet::new();\n                for segment in pattern_str.split('/') {\n                    if let Some(param) = segment.strip_prefix(':') {\n                        pattern_params.insert(param);\n                    }\n                }\n\n                // Validate each field\n                let mut field_names: HashSet<String> = HashSet::new();\n                let mut has_error = false;\n\n                for field in interface_fields {\n                    let field_name = match &field.name {\n                        FieldName::String(s) => s.clone(),\n                        FieldName::Symbol(_) => {\n                            key_type_ast\n                                .unwrap()\n                                .span()\n                                .err(\"cache key type must not contain symbol fields\");\n                            has_error = true;\n                            continue;\n                        }\n                    };\n\n                    // Check field type is a basic type (string, number, boolean)\n                    let is_valid_field_type = matches!(\n                        &field.typ,\n                        Type::Basic(Basic::String)\n                            | Type::Basic(Basic::Number)\n                            | Type::Basic(Basic::Boolean)\n                            | Type::Basic(Basic::BigInt)\n                    );\n\n                    if !is_valid_field_type {\n                        key_type_ast.unwrap().span().err(&format!(\n                                \"cache key field '{}' must be a basic type (string, number, or boolean)\",\n                                field_name\n                            ));\n                        has_error = true;\n                        continue;\n                    }\n\n                    // Check field is used in key pattern\n                    if !pattern_params.contains(field_name.as_str()) {\n                        key_type_ast.unwrap().span().err(&format!(\n                            \"cache key field '{}' is not used in the keyPattern\",\n                            field_name\n                        ));\n                        has_error = true;\n                    }\n\n                    field_names.insert(field_name);\n                }\n\n                // Check all pattern params correspond to fields\n                for param in &pattern_params {\n                    if !field_names.contains(*param) {\n                        key_pattern.span().err(&format!(\n                            \"keyPattern parameter '{}' does not exist in the key type\",\n                            param\n                        ));\n                        has_error = true;\n                    }\n                }\n\n                if has_error {\n                    continue;\n                }\n            }\n\n            // For StructKeyspace, validate that the value type is an interface/object type\n            if spec.keyspace_type == KeyspaceType::Struct {\n                if let Some(ref vt) = value_type {\n                    let vt = match vt.as_ref() {\n                        Type::Named(named) => &named.underlying(pass.type_checker.state()),\n                        typ => typ,\n                    };\n                    if !matches!(vt, Type::Interface(_) | Type::Class(_)) {\n                        value_type_ast\n                            .unwrap()\n                            .span()\n                            .err(\"StructKeyspace value type must be an interface or object type\");\n                        continue;\n                    }\n                }\n            }\n\n            // Resolve the cluster reference from the stored expression\n            let cluster_span = r.cluster_expr.span();\n            let Some(cluster_obj) = pass\n                .type_checker\n                .resolve_obj(pass.module.clone(), &r.cluster_expr)\n            else {\n                r.expr\n                    .span()\n                    .err(\"could not resolve cache cluster reference\");\n                continue;\n            };\n\n            let object =\n                resolve_object_for_bind_name(pass.type_checker, pass.module.clone(), &r.bind_name);\n\n            let resource = Resource::CacheKeyspace(Lrc::new(CacheKeyspace {\n                span: r.expr.span(),\n                cluster: Sp::new(cluster_span, cluster_obj),\n                doc: r.doc_comment.clone(),\n                keyspace_type: spec.keyspace_type,\n                key_pattern: r.config.keyPattern.to_string(),\n                key_type,\n                value_type,\n            }));\n\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.expr.span().into(),\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: r.bind_name,\n            });\n        }\n    },\n};\n\n/// Parsed keyspace reference from constructor call.\n#[derive(Debug)]\nstruct KeyspaceReference {\n    expr: ast::NewExpr,\n    class_name: String,\n    /// The AST expression for the cluster argument (resolved later in the main loop).\n    cluster_expr: Box<ast::Expr>,\n    config: DecodedKeyspaceConfig,\n    doc_comment: Option<String>,\n    bind_name: BindName,\n}\n\nimpl ReferenceParser for KeyspaceReference {\n    fn parse_resource_reference(\n        module: &crate::parser::module_loader::Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> litparser::ParseResult<Option<Self>> {\n        use swc_ecma_visit::AstParentNodeRef;\n\n        for node in path.iter().rev() {\n            if let AstParentNodeRef::NewExpr(expr, swc_ecma_visit::fields::NewExprField::Callee) =\n                node\n            {\n                let Some(args) = &expr.args else {\n                    return Err(expr.span.parse_err(\"missing constructor arguments\"));\n                };\n\n                if args.len() < 2 {\n                    return Err(expr\n                        .span\n                        .parse_err(\"keyspace constructor requires cluster and config arguments\"));\n                }\n\n                // Extract the class name from the callee\n                let class_name = match &*expr.callee {\n                    ast::Expr::Ident(ident) => ident.sym.to_string(),\n                    _ => {\n                        return Err(expr.span.parse_err(\"expected keyspace class name\"));\n                    }\n                };\n\n                // First argument is the cluster reference\n                let cluster_arg = &args[0];\n                if cluster_arg.spread.is_some() {\n                    return Err(cluster_arg\n                        .span()\n                        .parse_err(\"cannot use spread for cluster\"));\n                }\n\n                // Store the cluster expression to be resolved later\n                let cluster_expr = cluster_arg.expr.clone();\n\n                // Second argument is the config\n                let config_arg = &args[1];\n                if config_arg.spread.is_some() {\n                    return Err(config_arg.span().parse_err(\"cannot use spread for config\"));\n                }\n\n                let config = DecodedKeyspaceConfig::parse_lit(&config_arg.expr)?;\n\n                let bind_name = match extract_bind_name(path)? {\n                    Some(name) => BindName::Named(name),\n                    None => {\n                        if is_default_export(path, (*expr).into()) {\n                            BindName::DefaultExport\n                        } else {\n                            BindName::Anonymous\n                        }\n                    }\n                };\n\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n\n                return Ok(Some(Self {\n                    expr: (*expr).clone(),\n                    class_name,\n                    cluster_expr,\n                    config,\n                    doc_comment,\n                    bind_name,\n                }));\n            }\n        }\n\n        Ok(None)\n    }\n}\n\nimpl Spanned for KeyspaceReference {\n    fn span(&self) -> Span {\n        self.expr.span()\n    }\n}\n\n/// Resolves usage of cache resources.\npub fn resolve_cache_cluster_usage(\n    data: &ResolveUsageData,\n    cluster: Lrc<CacheCluster>,\n) -> Option<Usage> {\n    match &data.expr.kind {\n        UsageExprKind::ConstructorArg(_) => {\n            // Track when cluster is passed to keyspace constructors.\n            // e.g., new StringKeyspace(cluster, { ... })\n            Some(Usage::CacheCluster(CacheClusterUsage {\n                cluster,\n                range: data.expr.range,\n            }))\n        }\n        _ => None,\n    }\n}\n\n#[derive(Debug)]\npub struct CacheClusterUsage {\n    pub cluster: Lrc<CacheCluster>,\n    pub range: Range,\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/cron.rs",
    "content": "use std::rc::Rc;\n\nuse litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_common::{Span, Spanned};\nuse swc_ecma_ast as ast;\n\nuse litparser::{report_and_continue, LitParser, ParseResult, Sp, ToParseErr};\n\nuse crate::parser::resourceparser::bind::{BindData, BindKind, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resourceparser::ResourceParseContext;\nuse crate::parser::resources::parseutil::{\n    iter_references, resolve_object_for_bind_name, NamedClassResource, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::types::Object;\n\n#[derive(Debug, Clone)]\npub struct CronJob {\n    pub name: String,\n    pub title: Option<String>,\n    pub doc: Option<String>,\n    pub schedule: CronJobSchedule,\n    pub endpoint: Sp<Rc<Object>>,\n    pub span: Span,\n}\n\n#[derive(Debug, Clone)]\npub enum CronJobSchedule {\n    Every(u32), // every N minutes\n    Cron(CronExpr),\n}\n\n#[derive(Debug, Clone)]\npub struct CronExpr(pub String);\n\n#[derive(Debug, LitParser)]\nstruct DecodedCronJobConfig {\n    endpoint: ast::Expr,\n    title: Option<String>,\n    every: Option<Sp<std::time::Duration>>,\n    schedule: Option<Sp<CronExpr>>,\n}\n\npub const CRON_PARSER: ResourceParser = ResourceParser {\n    name: \"cron\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/cron\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/cron\", \"CronJob\")]);\n\n        let module = pass.module.clone();\n        type Res = NamedClassResource<DecodedCronJobConfig>;\n        for r in iter_references::<Res>(&module, &names) {\n            let r = report_and_continue!(r);\n            report_and_continue!(parse_cron_job(pass, r));\n        }\n    },\n};\n\nfn parse_cron_job(\n    pass: &mut ResourceParseContext,\n    r: NamedClassResource<DecodedCronJobConfig>,\n) -> ParseResult<()> {\n    let object = resolve_object_for_bind_name(pass.type_checker, pass.module.clone(), &r.bind_name);\n\n    let endpoint = pass\n        .type_checker\n        .resolve_obj(pass.module.clone(), &r.config.endpoint)\n        .ok_or(r.config.endpoint.parse_err(\"cannot resolve endpoint\"))?;\n\n    let schedule = r.config.parse_schedule(r.range.to_span())?;\n    let resource = Resource::CronJob(Lrc::new(CronJob {\n        name: r.resource_name.to_owned(),\n        doc: r.doc_comment,\n        title: r.config.title,\n        endpoint: Sp::new(r.config.endpoint.span(), endpoint),\n        schedule,\n        span: r.range.to_span(),\n    }));\n    pass.add_resource(resource.clone());\n    pass.add_bind(BindData {\n        range: r.range,\n        resource: ResourceOrPath::Resource(resource),\n        object,\n        kind: BindKind::Create,\n        ident: r.bind_name,\n    });\n    Ok(())\n}\n\nimpl LitParser for CronExpr {\n    fn parse_lit(input: &ast::Expr) -> ParseResult<Self> {\n        match input {\n            ast::Expr::Lit(ast::Lit::Str(str)) => {\n                // Ensure the cron expression is valid\n                let expr = str.value.as_ref();\n                cron_parser::parse(expr, &chrono::Utc::now())\n                    .map_err(|err| input.parse_err(err.to_string()))?;\n                Ok(CronExpr(expr.to_string()))\n            }\n            _ => Err(input.parse_err(\"expected cron expression\")),\n        }\n    }\n}\n\nimpl DecodedCronJobConfig {\n    fn parse_schedule(&self, def_span: Span) -> ParseResult<CronJobSchedule> {\n        match (self.every, self.schedule.as_ref()) {\n            (None, Some(schedule)) => Ok(CronJobSchedule::Cron(schedule.clone().take())),\n            (Some(every), None) => {\n                // TODO introduce more robust validation and error reporting here.\n                let secs = every.as_secs();\n                if secs % 60 != 0 {\n                    return Err(every\n                        .span()\n                        .parse_err(\"`every` must be a multiple of 60 seconds\"));\n                }\n                let mins = secs / 60;\n                if mins > (24 * 60) {\n                    return Err(every.span().parse_err(\"`every` must be at most 24 hours\"));\n                }\n                Ok(CronJobSchedule::Every(mins as u32))\n            }\n            (None, None) => {\n                Err(def_span.parse_err(\"expected either `every` or `schedule` to be set\"))\n            }\n            (Some(_), Some(_)) => {\n                Err(def_span.parse_err(\"expected either `every` or `schedule` to be set, not both\"))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/metrics.rs",
    "content": "use convert_case::{Case, Casing};\nuse litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_common::Span;\nuse swc_ecma_ast as ast;\n\nuse litparser::{report_and_continue, ParseResult, Sp};\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::{BindData, BindKind, BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    extract_type_param, iter_references, resolve_object_for_bind_name, validate_snake_case_name,\n    NamedClassResourceOptionalConfig, ReferenceParser, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::types::{FieldName, Type};\nuse crate::parser::usageparser::{ResolveUsageData, Usage, UsageExprKind};\nuse crate::parser::Range;\n\n#[derive(Debug, Clone)]\npub struct Metric {\n    pub name: String,\n    pub doc: Option<String>,\n    pub metric_type: MetricType,\n    /// The type parameter for labels (for CounterGroup/GaugeGroup)\n    pub label_type: Option<Sp<Type>>,\n    /// The source location where this metric was defined.\n    pub span: Span,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum MetricType {\n    Counter,\n    CounterGroup,\n    Gauge,\n    GaugeGroup,\n}\n\n#[derive(Debug, LitParser, Default)]\n#[allow(non_snake_case, dead_code)]\nstruct DecodedMetricConfig {\n    // Reserved for future configuration options\n}\n\npub const METRIC_PARSER: ResourceParser = ResourceParser {\n    name: \"metrics\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/metrics\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[\n            (\"encore.dev/metrics\", \"Counter\"),\n            (\"encore.dev/metrics\", \"CounterGroup\"),\n            (\"encore.dev/metrics\", \"Gauge\"),\n            (\"encore.dev/metrics\", \"GaugeGroup\"),\n        ]);\n        let module = pass.module.clone();\n\n        for r in iter_references::<MetricDefinition>(&module, &names) {\n            let r = report_and_continue!(r);\n\n            // Validate metric name is snake_case and doesn't start with \"e_\"\n            if let Err(err_msg) = validate_snake_case_name(&r.resource_name, Some(\"e_\")) {\n                r.range.err(&format!(\n                    \"invalid metric name '{}': {}.\",\n                    r.resource_name, err_msg\n                ));\n                continue;\n            }\n\n            // Validate label names if this is a group metric\n            if let Some(ref label_type) = r.label_type {\n                // Resolve the interface to get the fields\n                use crate::parser::resources::parseutil::resolve_interface;\n                let label_type_sp = Sp::new(\n                    r.range.to_span(),\n                    pass.type_checker\n                        .resolve_type(pass.module.clone(), label_type),\n                );\n\n                if let Some(iface) = resolve_interface(pass.type_checker, &label_type_sp) {\n                    for field in &iface.fields {\n                        if let FieldName::String(key) = &field.name {\n                            let label_key = key.to_case(Case::Snake);\n\n                            // Check if the label is named \"service\" (reserved)\n                            if label_key == \"service\" {\n                                r.range.err(&format!(\n                                    \"invalid label name '{}': the label name 'service' is reserved and automatically added by the Encore runtime\",\n                                    key\n                                ));\n                                continue;\n                            }\n                        }\n                    }\n                }\n            }\n\n            let object =\n                resolve_object_for_bind_name(pass.type_checker, pass.module.clone(), &r.bind_name);\n\n            let label_type = r\n                .label_type\n                .as_ref()\n                .map(|lt| pass.type_checker.resolve_type(pass.module.clone(), lt));\n\n            let resource = Resource::Metric(Lrc::new(Metric {\n                name: r.resource_name.to_owned(),\n                doc: r.doc_comment,\n                metric_type: r.metric_type,\n                label_type,\n                span: r.range.to_span(),\n            }));\n\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: r.bind_name,\n            });\n        }\n    },\n};\n\n#[derive(Debug)]\nstruct MetricDefinition {\n    pub range: Range,\n    pub resource_name: String,\n    /// Reserved for future configuration validation\n    #[allow(dead_code)]\n    pub config: Option<DecodedMetricConfig>,\n    pub doc_comment: Option<String>,\n    pub bind_name: BindName,\n    pub metric_type: MetricType,\n    /// Type parameter for labels (for *Group types)\n    pub label_type: Option<ast::TsType>,\n}\n\nimpl ReferenceParser for MetricDefinition {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        // Constructor: new Counter(name) or new Counter(name, config)\n        // NAME_IDX=0 (name), CONFIG_IDX=1 (optional config)\n        let Some(res) =\n            NamedClassResourceOptionalConfig::<DecodedMetricConfig, 0, 1>::parse_resource_reference(module, path)?\n        else {\n            return Ok(None);\n        };\n\n        // Get the class name from the new expression callee to determine if it's Counter/Gauge\n        let class_name = match res.expr.callee.as_ref() {\n            ast::Expr::Ident(ident) => ident.sym.as_ref(),\n            _ => return Ok(None),\n        };\n\n        // Determine if it's a counter or gauge\n        let is_counter = class_name == \"Counter\" || class_name == \"CounterGroup\";\n\n        // Check if it has a label type parameter (Group variants)\n        let label_type = extract_type_param(res.expr.type_args.as_deref(), 0);\n        let has_labels = label_type.is_some();\n\n        // Determine the metric type\n        let metric_type = match (is_counter, has_labels) {\n            (true, false) => MetricType::Counter,\n            (true, true) => MetricType::CounterGroup,\n            (false, false) => MetricType::Gauge,\n            (false, true) => MetricType::GaugeGroup,\n        };\n\n        Ok(Some(Self {\n            range: res.expr.span.into(),\n            resource_name: res.resource_name,\n            config: res.config,\n            doc_comment: res.doc_comment,\n            bind_name: res.bind_name,\n            metric_type,\n            label_type: label_type.map(|lt| lt.to_owned()),\n        }))\n    }\n}\n\n#[derive(Debug)]\npub struct MetricUsage {\n    pub range: Range,\n    pub metric: Lrc<Metric>,\n    pub ops: Vec<MetricOperation>,\n}\n\npub fn resolve_metric_usage(data: &ResolveUsageData, metric: Lrc<Metric>) -> Option<Usage> {\n    match &data.expr.kind {\n        UsageExprKind::MethodCall(call) => {\n            if call.method.as_ref() == \"ref\" || call.method.as_ref() == \"with\" {\n                let ops = determine_metric_operations(&metric);\n                return Some(Usage::Metric(MetricUsage {\n                    range: data.expr.range,\n                    metric,\n                    ops,\n                }));\n            }\n\n            // Determine the operation based on method name and metric type\n            let operation = match call.method.as_ref() {\n                \"increment\" => Some(MetricOperation::Increment),\n                \"set\" => Some(MetricOperation::Set),\n                _ => None,\n            };\n\n            operation.map(|op| {\n                Usage::Metric(MetricUsage {\n                    range: data.expr.range,\n                    metric,\n                    ops: vec![op],\n                })\n            })\n        }\n        UsageExprKind::ConstructorArg(_arg) => {\n            // Metrics used as constructor args (similar to topics in subscriptions)\n            None\n        }\n        _ => {\n            data.expr.range.err(\"invalid metric usage\");\n            None\n        }\n    }\n}\n\n/// Determine what operations are available for a given metric type\nfn determine_metric_operations(metric: &Metric) -> Vec<MetricOperation> {\n    match metric.metric_type {\n        MetricType::Counter | MetricType::CounterGroup => vec![MetricOperation::Increment],\n        MetricType::Gauge | MetricType::GaugeGroup => vec![MetricOperation::Set],\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum MetricOperation {\n    /// Incrementing a counter.\n    Increment,\n    /// Setting a gauge value.\n    Set,\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/mod.rs",
    "content": "pub mod cache;\npub mod cron;\npub mod metrics;\npub mod objects;\npub mod pubsub_subscription;\npub mod pubsub_topic;\npub mod secret;\npub mod sqldb;\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/objects.rs",
    "content": "use std::ops::Deref;\n\nuse litparser::{report_and_continue, LitParser};\nuse litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_common::Span;\nuse swc_ecma_ast as ast;\n\nuse crate::parser::resourceparser::bind::ResourceOrPath;\nuse crate::parser::resourceparser::bind::{BindData, BindKind};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    iter_references, resolve_object_for_bind_name, TrackedNames,\n};\nuse crate::parser::resources::parseutil::{NamedClassResourceOptionalConfig, NamedStaticMethod};\nuse crate::parser::resources::Resource;\nuse crate::parser::resources::ResourcePath;\nuse crate::parser::types::{Generic, Type};\nuse crate::parser::usageparser::{MethodCall, ResolveUsageData, Usage, UsageExprKind};\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\n\n#[derive(Debug, Clone)]\npub struct Bucket {\n    pub name: String,\n    pub doc: Option<String>,\n    pub versioned: bool,\n    pub public: bool,\n    pub span: Span,\n}\n\n#[derive(LitParser, Default)]\nstruct DecodedBucketConfig {\n    pub versioned: Option<bool>,\n    pub public: Option<bool>,\n}\n\npub const OBJECTS_PARSER: ResourceParser = ResourceParser {\n    name: \"objects\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/storage/objects\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/storage/objects\", \"Bucket\")]);\n\n        let module = pass.module.clone();\n        {\n            type Res = NamedClassResourceOptionalConfig<DecodedBucketConfig>;\n            for r in iter_references::<Res>(&module, &names) {\n                let r = report_and_continue!(r);\n                let cfg = r.config.unwrap_or_default();\n\n                let object = resolve_object_for_bind_name(\n                    pass.type_checker,\n                    pass.module.clone(),\n                    &r.bind_name,\n                );\n\n                let resource = Resource::Bucket(Lrc::new(Bucket {\n                    name: r.resource_name,\n                    doc: r.doc_comment,\n                    versioned: cfg.versioned.unwrap_or(false),\n                    public: cfg.public.unwrap_or(false),\n                    span: r.range.to_span(),\n                }));\n\n                pass.add_resource(resource.clone());\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Resource(resource),\n                    object,\n                    kind: BindKind::Create,\n                    ident: r.bind_name,\n                });\n            }\n        }\n\n        {\n            for r in iter_references::<NamedStaticMethod>(&module, &names) {\n                let r = report_and_continue!(r);\n                let object = resolve_object_for_bind_name(\n                    pass.type_checker,\n                    pass.module.clone(),\n                    &r.bind_name,\n                );\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Path(ResourcePath::Bucket {\n                        name: r.resource_name,\n                    }),\n                    object,\n                    kind: BindKind::Reference,\n                    ident: r.bind_name,\n                });\n            }\n        }\n    },\n};\n\npub fn resolve_bucket_usage(data: &ResolveUsageData, bucket: Lrc<Bucket>) -> Option<Usage> {\n    match &data.expr.kind {\n        UsageExprKind::MethodCall(call) => {\n            if call.method.as_ref() == \"ref\" {\n                let Some(type_args) = call.call.type_args.as_deref() else {\n                    call.call\n                        .span\n                        .err(\"expected a type argument in call to Bucket.ref\");\n                    return None;\n                };\n\n                let Some(type_arg) = type_args.params.first() else {\n                    call.call\n                        .span\n                        .err(\"expected a type argument in call to Bucket.ref\");\n                    return None;\n                };\n\n                return parse_bucket_ref(data, bucket, call, type_arg);\n            }\n\n            let op = match call.method.as_ref() {\n                \"list\" => Operation::ListObjects,\n                \"exists\" | \"attrs\" => Operation::GetObjectMetadata,\n                \"upload\" => Operation::WriteObject,\n                \"signedUploadUrl\" => Operation::SignedUploadUrl,\n                \"signedDownloadUrl\" => Operation::SignedDownloadUrl,\n                \"download\" => Operation::ReadObjectContents,\n                \"remove\" => Operation::DeleteObject,\n\n                \"publicUrl\" => {\n                    // Make sure the bucket is public.\n                    if !bucket.public {\n                        call.call\n                            .span\n                            .err(\"cannot call publicUrl on a non-public bucket\");\n                    }\n\n                    // Technically, getting a public URL does not require additional\n                    // permissions, but we track it anyway so that we can track which\n                    // service(s) need to receive the bucket configuration.\n                    Operation::GetPublicUrl\n                }\n\n                _ => {\n                    call.method.err(\"unsupported bucket operation\");\n                    return None;\n                }\n            };\n\n            Some(Usage::Bucket(BucketUsage {\n                range: data.expr.range,\n                bucket,\n                ops: vec![op],\n            }))\n        }\n\n        _ => {\n            data.expr\n                .range\n                .to_span()\n                .err(\"invalid use of bucket resource\");\n            None\n        }\n    }\n}\n\nfn parse_bucket_ref(\n    data: &ResolveUsageData,\n    bucket: Lrc<Bucket>,\n    _call: &MethodCall,\n    type_arg: &ast::TsType,\n) -> Option<Usage> {\n    fn process_type(\n        data: &ResolveUsageData,\n        sp: &swc_common::Span,\n        t: &Type,\n        depth: usize,\n    ) -> Option<Vec<Operation>> {\n        if depth > 10 {\n            // Prevent infinite recursion.\n            return None;\n        }\n\n        match t {\n            Type::Named(named) => {\n                let ops = match named.obj.name.as_deref() {\n                    Some(\"Lister\") => vec![Operation::ListObjects],\n                    Some(\"Attrser\") => vec![Operation::GetObjectMetadata],\n                    Some(\"Uploader\") => vec![Operation::WriteObject],\n                    Some(\"SignedUploader\") => vec![Operation::SignedUploadUrl],\n                    Some(\"Downloader\") => vec![Operation::ReadObjectContents],\n                    Some(\"SignedDownloader\") => vec![Operation::SignedDownloadUrl],\n                    Some(\"Remover\") => vec![Operation::DeleteObject],\n                    Some(\"PublicUrler\") => vec![Operation::GetPublicUrl],\n                    _ => {\n                        let underlying = data.type_checker.resolve_obj_type(&named.obj);\n                        return process_type(data, sp, &underlying, depth + 1);\n                    }\n                };\n\n                Some(ops)\n            }\n\n            Type::Class(cls) => {\n                let ops = cls\n                    .methods\n                    .iter()\n                    .filter_map(|method| {\n                        let op = match method.as_str() {\n                            \"list\" => Operation::ListObjects,\n                            \"exists\" | \"attrs\" => Operation::GetObjectMetadata,\n                            \"upload\" => Operation::WriteObject,\n                            \"signedUploadUrl\" => Operation::SignedUploadUrl,\n                            \"download\" => Operation::ReadObjectContents,\n                            \"signedDownloadUrl\" => Operation::SignedDownloadUrl,\n                            \"remove\" => Operation::DeleteObject,\n                            \"publicUrl\" => Operation::GetPublicUrl,\n                            _ => {\n                                // Ignore other methods.\n                                return None;\n                            }\n                        };\n\n                        Some(op)\n                    })\n                    .collect();\n                Some(ops)\n            }\n\n            Type::Generic(Generic::Intersection(int)) => {\n                let mut result = Vec::new();\n                for t in &[&int.x, &int.y] {\n                    if let Some(ops) = process_type(data, sp, t, depth + 1) {\n                        result.extend(ops);\n                    }\n                }\n\n                if result.is_empty() {\n                    None\n                } else {\n                    Some(result)\n                }\n            }\n\n            _ => {\n                sp.err(&format!(\"unsupported bucket permission type {t:#?}\"));\n                None\n            }\n        }\n    }\n\n    let typ = data\n        .type_checker\n        .resolve_type(data.module.clone(), type_arg);\n\n    if let Some(ops) = process_type(data, &typ.span(), typ.deref(), 0) {\n        if !bucket.public && ops.contains(&Operation::GetPublicUrl) {\n            typ.span()\n                .err(\"cannot use publicUrl on a non-public bucket\");\n        }\n\n        Some(Usage::Bucket(BucketUsage {\n            range: data.expr.range,\n            bucket,\n            ops,\n        }))\n    } else {\n        typ.err(\"no bucket permissions found in type argument\");\n        None\n    }\n}\n\n#[derive(Debug)]\npub struct BucketUsage {\n    pub range: Range,\n    pub bucket: Lrc<Bucket>,\n    pub ops: Vec<Operation>,\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum Operation {\n    /// Listing objects and accessing their metadata during list operations.\n    ListObjects,\n\n    /// Reading the contents of an object.\n    ReadObjectContents,\n\n    /// Creating or updating an object, with contents and metadata.\n    WriteObject,\n\n    /// Updating the metadata of an object, without reading or writing its contents.\n    UpdateObjectMetadata,\n\n    /// Reading the metadata of an object, or checking for its existence.\n    GetObjectMetadata,\n\n    /// Deleting an object.\n    DeleteObject,\n\n    /// Getting the public URL for the bucket/an object.\n    GetPublicUrl,\n\n    /// Generating a signed URL to allow an external recipient to create or\n    /// update an object.\n    SignedUploadUrl,\n\n    /// Generating a signed URL to allow an external recipient to read an object.\n    SignedDownloadUrl,\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/pubsub_subscription.rs",
    "content": "use std::rc::Rc;\n\nuse litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_common::Spanned;\nuse swc_ecma_ast as ast;\n\nuse litparser::{report_and_continue, LitParser, Sp};\n\nuse crate::parser::resourceparser::bind::{BindData, BindKind, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    iter_references, resolve_object_for_bind_name, NamedClassResource, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::types::Object;\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\n\n#[derive(Debug, Clone)]\npub struct Subscription {\n    pub range: Range,\n    pub topic: Sp<Rc<Object>>,\n    pub name: String,\n    pub doc: Option<String>,\n    pub config: SubscriptionConfig,\n}\n\n#[derive(Debug, Clone)]\npub struct SubscriptionConfig {\n    pub ack_deadline: std::time::Duration,\n    pub message_retention: std::time::Duration,\n    pub min_retry_backoff: std::time::Duration,\n    pub max_retry_backoff: std::time::Duration,\n    pub max_retries: u32,\n    pub max_concurrency: Option<u32>,\n}\n\n#[allow(non_snake_case)]\n#[derive(Debug, LitParser)]\nstruct DecodedSubscriptionConfig {\n    #[allow(dead_code)]\n    handler: ast::Expr,\n    #[allow(dead_code)]\n    maxConcurrency: Option<u32>,\n    ackDeadline: Option<std::time::Duration>,\n    messageRetention: Option<std::time::Duration>,\n    retryPolicy: Option<DecodedRetryPolicy>,\n}\n\n#[allow(non_snake_case)]\n#[derive(Debug, LitParser)]\nstruct DecodedRetryPolicy {\n    minBackoff: Option<std::time::Duration>,\n    maxBackoff: Option<std::time::Duration>,\n    maxRetries: Option<u32>,\n}\n\npub const SUBSCRIPTION_PARSER: ResourceParser = ResourceParser {\n    name: \"pubsub_subscription\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/pubsub\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/pubsub\", \"Subscription\")]);\n        let module = pass.module.clone();\n\n        type Res = NamedClassResource<DecodedSubscriptionConfig, 1, 2>;\n        for r in iter_references::<Res>(&module, &names) {\n            let r = report_and_continue!(r);\n            let topic_expr = r.constructor_args[0].clone();\n            if let Some(spread) = topic_expr.spread.as_ref() {\n                spread.err(\"cannot use ... for PubSub topic reference\");\n                continue;\n            }\n            let object =\n                resolve_object_for_bind_name(pass.type_checker, pass.module.clone(), &r.bind_name);\n\n            let Some(topic) = pass\n                .type_checker\n                .resolve_obj(pass.module.clone(), &topic_expr.expr)\n            else {\n                topic_expr.expr.err(\"cannot resolve topic reference\");\n                continue;\n            };\n\n            let resource = Resource::PubSubSubscription(Lrc::new(Subscription {\n                range: r.range,\n                topic: Sp::new(topic_expr.expr.span(), topic),\n                name: r.resource_name.to_owned(),\n                doc: r.doc_comment,\n                config: SubscriptionConfig {\n                    ack_deadline: r\n                        .config\n                        .ackDeadline\n                        .unwrap_or(std::time::Duration::from_secs(30)),\n                    message_retention: r\n                        .config\n                        .messageRetention\n                        .unwrap_or(std::time::Duration::from_secs(7 * 24 * 60 * 60)),\n                    min_retry_backoff: r\n                        .config\n                        .retryPolicy\n                        .as_ref()\n                        .and_then(|p| p.minBackoff)\n                        .unwrap_or(std::time::Duration::from_secs(10)),\n                    max_retry_backoff: r\n                        .config\n                        .retryPolicy\n                        .as_ref()\n                        .and_then(|p| p.maxBackoff)\n                        .unwrap_or(std::time::Duration::from_secs(10 * 60)),\n                    max_retries: r\n                        .config\n                        .retryPolicy\n                        .as_ref()\n                        .and_then(|p| p.maxRetries)\n                        .unwrap_or(100),\n                    max_concurrency: r.config.maxConcurrency,\n                },\n            }));\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: r.bind_name,\n            });\n        }\n    },\n};\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/pubsub_topic.rs",
    "content": "use std::ops::Deref;\n\nuse litparser_derive::LitParser;\nuse swc_common::sync::Lrc;\nuse swc_common::Span;\nuse swc_ecma_ast as ast;\n\nuse litparser::{report_and_continue, LitParser, ParseResult, Sp, ToParseErr};\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::{BindData, BindKind, BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    extract_type_param, iter_references, resolve_object_for_bind_name, NamedClassResource,\n    ReferenceParser, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::types::{Generic, Type};\nuse crate::parser::usageparser::{MethodCall, ResolveUsageData, Usage, UsageExprKind};\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\n\n#[derive(Debug, Clone)]\npub struct Topic {\n    pub name: String,\n    pub doc: Option<String>,\n    pub delivery_guarantee: DeliveryGuarantee,\n    pub ordering_attribute: Option<String>,\n    pub message_type: Sp<Type>,\n    pub span: Span,\n}\n\n#[derive(Debug, Clone, Copy)]\npub enum DeliveryGuarantee {\n    AtLeastOnce,\n    ExactlyOnce,\n}\n\n#[derive(Debug, LitParser)]\n#[allow(non_snake_case, dead_code)]\nstruct DecodedTopicConfig {\n    deliveryGuarantee: Option<Sp<String>>,\n    orderingAttribute: Option<String>,\n}\n\nimpl DecodedTopicConfig {\n    fn delivery_guarantee(&self) -> ParseResult<DeliveryGuarantee> {\n        let Some(delivery_guarantee) = &self.deliveryGuarantee else {\n            return Ok(DeliveryGuarantee::AtLeastOnce);\n        };\n\n        match delivery_guarantee.as_str() {\n            \"at-least-once\" => Ok(DeliveryGuarantee::AtLeastOnce),\n            \"exactly-once\" => Ok(DeliveryGuarantee::ExactlyOnce),\n            _ => Err(delivery_guarantee.parse_err(\"invalid delivery guarantee\")),\n        }\n    }\n}\n\npub const TOPIC_PARSER: ResourceParser = ResourceParser {\n    name: \"pubsub_topic\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/pubsub\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/pubsub\", \"Topic\")]);\n        let module = pass.module.clone();\n\n        for r in iter_references::<PubSubTopicDefinition>(&module, &names) {\n            let r = report_and_continue!(r);\n            let object =\n                resolve_object_for_bind_name(pass.type_checker, pass.module.clone(), &r.bind_name);\n\n            let message_type = pass\n                .type_checker\n                .resolve_type(pass.module.clone(), &r.message_type);\n\n            let delivery_guarantee = report_and_continue!(r.config.delivery_guarantee());\n            let resource = Resource::PubSubTopic(Lrc::new(Topic {\n                name: r.resource_name.to_owned(),\n                doc: r.doc_comment,\n                delivery_guarantee,\n                message_type,\n                ordering_attribute: r.config.orderingAttribute,\n                span: r.range.to_span(),\n            }));\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: r.bind_name,\n            });\n        }\n    },\n};\n\n#[derive(Debug)]\nstruct PubSubTopicDefinition {\n    pub range: Range,\n    pub resource_name: String,\n    pub config: DecodedTopicConfig,\n    pub doc_comment: Option<String>,\n    pub bind_name: BindName,\n    pub message_type: ast::TsType,\n}\n\nimpl ReferenceParser for PubSubTopicDefinition {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        let Some(res) =\n            NamedClassResource::<DecodedTopicConfig, 0, 1>::parse_resource_reference(module, path)?\n        else {\n            return Ok(None);\n        };\n\n        let Some(message_type) = extract_type_param(res.expr.type_args.as_deref(), 0) else {\n            return Err(res.expr.parse_err(\"missing message type parameter\"));\n        };\n\n        Ok(Some(Self {\n            range: res.expr.span.into(),\n            resource_name: res.resource_name,\n            config: res.config,\n            doc_comment: res.doc_comment,\n            bind_name: res.bind_name,\n            message_type: message_type.to_owned(),\n        }))\n    }\n}\n\n#[derive(Debug)]\npub struct TopicUsage {\n    pub range: Range,\n    pub topic: Lrc<Topic>,\n    pub ops: Vec<TopicOperation>,\n}\n\npub fn resolve_topic_usage(data: &ResolveUsageData, topic: Lrc<Topic>) -> Option<Usage> {\n    match &data.expr.kind {\n        UsageExprKind::MethodCall(call) => {\n            if call.method.as_ref() == \"ref\" {\n                let Some(type_args) = call.call.type_args.as_deref() else {\n                    call.call\n                        .span\n                        .err(\"expected a type argument in call to Topic.ref\");\n                    return None;\n                };\n\n                let Some(type_arg) = type_args.params.first() else {\n                    call.call\n                        .span\n                        .err(\"expected a type argument in call to Topic.ref\");\n                    return None;\n                };\n\n                return parse_topic_ref(data, topic, call, type_arg);\n            }\n\n            if call.method.as_ref() == \"publish\" {\n                Some(Usage::Topic(TopicUsage {\n                    range: data.expr.range,\n                    topic,\n                    ops: vec![TopicOperation::Publish],\n                }))\n            } else {\n                None\n            }\n        }\n        UsageExprKind::ConstructorArg(_arg) => {\n            // TODO validate: used as a subscription arg most likely\n            None\n        }\n        _ => {\n            data.expr.err(\"invalid topic usage\");\n            None\n        }\n    }\n}\n\nfn parse_topic_ref(\n    data: &ResolveUsageData,\n    topic: Lrc<Topic>,\n    _call: &MethodCall,\n    type_arg: &ast::TsType,\n) -> Option<Usage> {\n    fn process_type(\n        data: &ResolveUsageData,\n        sp: &swc_common::Span,\n        t: &Type,\n        depth: usize,\n    ) -> Option<Vec<TopicOperation>> {\n        if depth > 10 {\n            // Prevent infinite recursion.\n            return None;\n        }\n\n        match t {\n            Type::Named(named) => {\n                let ops = match named.obj.name.as_deref() {\n                    Some(\"Publisher\") => vec![TopicOperation::Publish],\n                    _ => {\n                        let underlying = data.type_checker.resolve_obj_type(&named.obj);\n                        return process_type(data, sp, &underlying, depth + 1);\n                    }\n                };\n\n                Some(ops)\n            }\n\n            Type::Class(cls) => {\n                let ops = cls\n                    .methods\n                    .iter()\n                    .filter_map(|method| {\n                        let op = match method.as_str() {\n                            \"publish\" => TopicOperation::Publish,\n                            _ => {\n                                // Ignore other methods.\n                                return None;\n                            }\n                        };\n\n                        Some(op)\n                    })\n                    .collect();\n                Some(ops)\n            }\n\n            Type::Generic(Generic::Intersection(int)) => {\n                let mut result = Vec::new();\n                for t in &[&int.x, &int.y] {\n                    if let Some(ops) = process_type(data, sp, t, depth + 1) {\n                        result.extend(ops);\n                    }\n                }\n\n                if result.is_empty() {\n                    None\n                } else {\n                    Some(result)\n                }\n            }\n\n            _ => {\n                sp.err(&format!(\"unsupported topic permission type {t:#?}\"));\n                None\n            }\n        }\n    }\n\n    let typ = data\n        .type_checker\n        .resolve_type(data.module.clone(), type_arg);\n\n    if let Some(ops) = process_type(data, &typ.span(), typ.deref(), 0) {\n        Some(Usage::Topic(TopicUsage {\n            range: data.expr.range,\n            topic,\n            ops,\n        }))\n    } else {\n        typ.err(\"no topic permissions found in type argument\");\n        None\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum TopicOperation {\n    /// Publishing messages to the topic.\n    Publish,\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/secret.rs",
    "content": "use crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::{BindData, BindKind, BindName, ResourceOrPath};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    extract_bind_name, iter_references, ReferenceParser, TrackedNames,\n};\nuse crate::parser::resources::Resource;\nuse crate::parser::Range;\nuse crate::span_err::ErrReporter;\nuse litparser::{report_and_continue, LitParser, ParseResult};\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\nuse swc_common::Span;\nuse swc_ecma_ast as ast;\n\n#[derive(Debug, Clone)]\npub struct Secret {\n    pub range: Range,\n    pub name: String,\n    pub doc: Option<String>,\n}\n\npub const SECRET_PARSER: ResourceParser = ResourceParser {\n    name: \"secret\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/config\")],\n\n    run: |pass| {\n        let module = pass.module.clone();\n        let names = TrackedNames::new(&[(\"encore.dev/config\", \"secret\")]);\n\n        for r in iter_references::<SecretLiteral>(&module, &names) {\n            let r = report_and_continue!(r);\n            let resource = Resource::Secret(Lrc::new(Secret {\n                range: r.range,\n                name: r.secret_name,\n                doc: r.doc_comment,\n            }));\n\n            let object = pass\n                .type_checker\n                .resolve_obj(pass.module.clone(), &ast::Expr::Ident(r.bind_name.clone()));\n\n            pass.add_resource(resource.clone());\n            pass.add_bind(BindData {\n                range: r.range,\n                resource: ResourceOrPath::Resource(resource),\n                object,\n                kind: BindKind::Create,\n                ident: BindName::Named(r.bind_name),\n            });\n        }\n    },\n};\n\n#[derive(Debug)]\nstruct SecretLiteral {\n    pub range: Range,\n    pub doc_comment: Option<String>,\n    pub secret_name: String,\n    pub bind_name: ast::Ident,\n}\n\nfn inside_function(path: &swc_ecma_visit::AstNodePath) -> Option<Span> {\n    for item in path.iter().rev() {\n        match item {\n            swc_ecma_visit::AstParentNodeRef::ArrowExpr(expr, ..) => return Some(expr.span),\n            swc_ecma_visit::AstParentNodeRef::Function(expr, ..) => return Some(expr.span),\n            _ => continue,\n        }\n    }\n\n    None\n}\n\nimpl ReferenceParser for SecretLiteral {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for node in path.iter().rev() {\n            if let swc_ecma_visit::AstParentNodeRef::CallExpr(\n                expr,\n                swc_ecma_visit::fields::CallExprField::Callee,\n            ) = node\n            {\n                if let Some(fn_span) = inside_function(path) {\n                    HANDLER.with(|handler| {\n                        handler\n                            .struct_span_err(expr.span, \"secrets must be defined globally\")\n                            .span_note(fn_span, \"secret defined within this function\")\n                            .emit();\n                    });\n                    return Ok(None);\n                }\n\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n                let Some(bind_name) = extract_bind_name(path)? else {\n                    expr.span.err(\"secrets must be bound to a variable\");\n                    continue;\n                };\n\n                let Some(secret_name) = &expr.args.first() else {\n                    expr.span.err(\"secret() takes a single argument, the name of the secret as a string literal\");\n                    continue;\n                };\n                let secret_name = String::parse_lit(secret_name.expr.as_ref())?;\n\n                return Ok(Some(Self {\n                    range: expr.span.into(),\n                    doc_comment,\n                    secret_name,\n                    bind_name,\n                }));\n            }\n        }\n        Ok(None)\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/infra/sqldb.rs",
    "content": "use std::path::{Path, PathBuf};\nuse std::str::FromStr;\n\nuse itertools::Either;\nuse litparser_derive::LitParser;\n#[cfg(not(target_arch = \"wasm32\"))]\nuse once_cell::sync::Lazy;\n#[cfg(not(target_arch = \"wasm32\"))]\nuse regex::Regex;\nuse swc_common::sync::Lrc;\nuse swc_common::{Span, Spanned};\n#[cfg(not(target_arch = \"wasm32\"))]\nuse swc_ecma_ast as ast;\n\n#[cfg(not(target_arch = \"wasm32\"))]\nuse litparser::ToParseErr;\nuse litparser::{report_and_continue, LitParser, Sp};\nuse litparser::{LocalRelPath, ParseResult};\n\nuse crate::parser::resourceparser::bind::ResourceOrPath;\nuse crate::parser::resourceparser::bind::{BindData, BindKind};\nuse crate::parser::resourceparser::paths::PkgPath;\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::parseutil::{\n    iter_references, resolve_object_for_bind_name, TrackedNames,\n};\nuse crate::parser::resources::parseutil::{NamedClassResourceOptionalConfig, NamedStaticMethod};\nuse crate::parser::resources::Resource;\nuse crate::parser::resources::ResourcePath;\nuse crate::parser::usageparser::{ResolveUsageData, Usage, UsageExprKind};\nuse crate::parser::{FilePath, Range};\nuse crate::span_err::{ErrReporter, ErrorWithSpanExt};\n\n#[derive(Debug, Clone)]\npub struct SQLDatabase {\n    pub span: Span,\n    pub name: String,\n    pub doc: Option<String>,\n    pub migrations: Option<Sp<DBMigrations>>,\n}\n\n#[derive(Clone, Debug)]\npub enum MigrationFileSource {\n    Prisma,\n    Drizzle,\n    DrizzleV1,\n}\n\n#[derive(Debug, thiserror::Error)]\npub enum MigrationFileSourceParseError {\n    #[error(\"unexpected value for migration file source: {0}\")]\n    UnexpectedValue(String),\n}\n\nimpl FromStr for MigrationFileSource {\n    type Err = MigrationFileSourceParseError;\n\n    fn from_str(input: &str) -> Result<MigrationFileSource, Self::Err> {\n        match input {\n            \"prisma\" => Ok(MigrationFileSource::Prisma),\n            \"drizzle\" => Ok(MigrationFileSource::Drizzle),\n            \"drizzle/v1\" => Ok(MigrationFileSource::DrizzleV1),\n            _ => Err(MigrationFileSourceParseError::UnexpectedValue(\n                input.to_string(),\n            )),\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct DBMigrations {\n    pub dir: PathBuf,\n    pub migrations: Vec<DBMigration>,\n    pub non_seq_migrations: bool,\n}\n\n#[derive(Debug, Clone)]\npub struct DBMigration {\n    pub file_name: String,\n    pub description: String,\n    pub number: u64,\n}\n\n#[derive(LitParser, Debug)]\nstruct MigrationsConfig {\n    path: LocalRelPath,\n    source: Option<String>,\n}\n\n#[derive(LitParser, Default, Debug)]\nstruct DecodedDatabaseConfig {\n    migrations: Option<Either<LocalRelPath, MigrationsConfig>>,\n}\n\npub const SQLDB_PARSER: ResourceParser = ResourceParser {\n    name: \"sqldb\",\n    interesting_pkgs: &[PkgPath(\"encore.dev/storage/sqldb\")],\n\n    run: |pass| {\n        let names = TrackedNames::new(&[(\"encore.dev/storage/sqldb\", \"SQLDatabase\")]);\n\n        let module = pass.module.clone();\n        {\n            type Res = NamedClassResourceOptionalConfig<DecodedDatabaseConfig>;\n            for r in iter_references::<Res>(&module, &names) {\n                let r = report_and_continue!(r);\n                let cfg = r.config.unwrap_or_default();\n\n                let migrations = match (cfg.migrations, &pass.module.file_path) {\n                    (None, _) => None,\n                    (_, FilePath::Custom(_)) => {\n                        pass.module\n                            .ast\n                            .span()\n                            .shrink_to_lo()\n                            .err(\"cannot use custom file path for db migrations\");\n                        continue;\n                    }\n                    (Some(Either::Left(rel)), FilePath::Real(path)) => {\n                        let dir = path.parent().unwrap().join(rel.buf);\n                        let migrations =\n                            report_and_continue!(parse_migrations(rel.span, &dir, None));\n                        Some(Sp::new(\n                            rel.span,\n                            DBMigrations {\n                                dir,\n                                migrations,\n                                non_seq_migrations: false,\n                            },\n                        ))\n                    }\n                    (Some(Either::Right(cfg)), FilePath::Real(path)) => {\n                        let dir = path.parent().unwrap().join(cfg.path.buf);\n                        let source = if let Some(ref string) = cfg.source {\n                            match MigrationFileSource::from_str(string) {\n                                Ok(source) => Some(source),\n                                Err(e) => {\n                                    e.with_span(r.range.into()).report();\n                                    continue;\n                                }\n                            }\n                        } else {\n                            None\n                        };\n\n                        let migrations = report_and_continue!(parse_migrations(\n                            cfg.path.span,\n                            &dir,\n                            source.as_ref()\n                        ));\n                        let non_seq_migrations = matches!(\n                            source,\n                            Some(MigrationFileSource::Prisma | MigrationFileSource::DrizzleV1)\n                        );\n                        Some(Sp::new(\n                            cfg.path.span,\n                            DBMigrations {\n                                dir,\n                                migrations,\n                                non_seq_migrations,\n                            },\n                        ))\n                    }\n                };\n\n                let object = resolve_object_for_bind_name(\n                    pass.type_checker,\n                    pass.module.clone(),\n                    &r.bind_name,\n                );\n\n                let resource = Resource::SQLDatabase(Lrc::new(SQLDatabase {\n                    span: r.range.to_span(),\n                    name: r.resource_name,\n                    doc: r.doc_comment,\n                    migrations,\n                }));\n                pass.add_resource(resource.clone());\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Resource(resource),\n                    object,\n                    kind: BindKind::Create,\n                    ident: r.bind_name,\n                });\n            }\n        }\n\n        {\n            for r in iter_references::<NamedStaticMethod>(&module, &names) {\n                let r = report_and_continue!(r);\n                let object = resolve_object_for_bind_name(\n                    pass.type_checker,\n                    pass.module.clone(),\n                    &r.bind_name,\n                );\n                pass.add_bind(BindData {\n                    range: r.range,\n                    resource: ResourceOrPath::Path(ResourcePath::SQLDatabase {\n                        name: r.resource_name,\n                    }),\n                    object,\n                    kind: BindKind::Reference,\n                    ident: r.bind_name,\n                });\n            }\n        }\n    },\n};\n\n#[cfg(not(target_arch = \"wasm32\"))]\nfn visit_dirs(\n    span: Span,\n    dir: &Path,\n    depth: i8,\n    max_depth: i8,\n    cb: &mut dyn FnMut(&std::fs::DirEntry) -> ParseResult<()>,\n) -> ParseResult<()> {\n    let entries = std::fs::read_dir(dir).map_err(|err| span.parse_err(err.to_string()))?;\n    for entry in entries {\n        let entry = entry.map_err(|err| span.parse_err(err.to_string()))?;\n        let path = entry.path();\n        if path.is_dir() && depth < max_depth {\n            visit_dirs(span, &path, depth + 1, max_depth, cb)?;\n        } else {\n            cb(&entry)?;\n        }\n    }\n    Ok(())\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nfn parse_default(span: Span, dir: &Path) -> ParseResult<Vec<DBMigration>> {\n    let mut migrations = vec![];\n    static FILENAME_RE: Lazy<Regex> =\n        Lazy::new(|| Regex::new(r\"^(\\d+)_([^.]+)\\.(up|down).sql$\").unwrap());\n\n    visit_dirs(span, dir, 0, 0, &mut |entry| -> ParseResult<()> {\n        let path = entry.path();\n        let name = entry.file_name();\n        let name = name.to_str().ok_or(span.parse_err(format!(\n            \"invalid migration filename: {}\",\n            name.to_string_lossy()\n        )))?;\n\n        // If the file is not an SQL file ignore it, to allow for other files to be present\n        // in the migration directory. For SQL files we want to ensure they're properly named\n        // so that we complain loudly about potential typos. (It's theoretically possible to\n        // typo the filename extension as well, but it's less likely due to syntax highlighting).\n        let ext = path.extension().and_then(|ext| ext.to_str());\n        if ext != Some(\"sql\") {\n            return Ok(());\n        }\n\n        // Ensure the file name matches the regex.\n        let captures = FILENAME_RE\n            .captures(name)\n            .ok_or(span.parse_err(format!(\"invalid migration filename: {name}\")))?;\n        if captures[3].eq(\"up\") {\n            migrations.push(DBMigration {\n                file_name: name.to_string(),\n                description: captures[2].to_string(),\n                number: captures[1]\n                    .parse::<u64>()\n                    .map_err(|err| span.parse_err(err.to_string()))?,\n            });\n        }\n        Ok(())\n    })?;\n\n    migrations.sort_by_key(|m| m.number);\n    Ok(migrations)\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nfn parse_drizzle(span: Span, dir: &Path) -> ParseResult<Vec<DBMigration>> {\n    let mut migrations = vec![];\n\n    static FILENAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r\"^(\\d+)_([^.]+)\\.sql$\").unwrap());\n\n    visit_dirs(span, dir, 0, 0, &mut |entry| -> ParseResult<()> {\n        let path = entry.path();\n        let name = entry.file_name();\n        let name = name.to_str().ok_or(span.parse_err(format!(\n            \"invalid migration filename: {}\",\n            name.to_string_lossy()\n        )))?;\n\n        // If the file is not an SQL file ignore it, to allow for other files to be present\n        // in the migration directory. For SQL files we want to ensure they're properly named\n        // so that we complain loudly about potential typos. (It's theoretically possible to\n        // typo the filename extension as well, but it's less likely due to syntax highlighting).\n        let ext = path.extension().and_then(|ext| ext.to_str());\n        if ext != Some(\"sql\") {\n            return Ok(());\n        }\n\n        // Ensure the file name matches the regex.\n        let captures = FILENAME_RE\n            .captures(name)\n            .ok_or(span.parse_err(format!(\"invalid migration filename: {name}\")))?;\n        migrations.push(DBMigration {\n            file_name: name.to_string(),\n            description: captures[2].to_string(),\n            number: captures[1]\n                .parse::<u64>()\n                .map_err(|err| span.parse_err(err.to_string()))?,\n        });\n\n        Ok(())\n    })?;\n    Ok(migrations)\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nfn parse_prisma(span: Span, dir: &Path) -> ParseResult<Vec<DBMigration>> {\n    let mut migrations = vec![];\n\n    static FILENAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r\"^(\\d+)_(.*)$\").unwrap());\n\n    visit_dirs(span, dir, 0, 1, &mut |entry| -> ParseResult<()> {\n        let path = entry.path();\n        let name = entry.file_name();\n        let name = name.to_str().ok_or(span.parse_err(format!(\n            \"invalid migration filename: {}\",\n            name.to_string_lossy()\n        )))?;\n        if name != \"migration.sql\" {\n            return Ok(());\n        }\n        let dir_name = path\n            .parent()\n            .ok_or(span.parse_err(\"migration directory has no parent\"))?\n            .file_name()\n            .ok_or(span.parse_err(\"migration directory has no name\"))?\n            .to_str()\n            .ok_or(span.parse_err(\"migration directory has invalid name\"))?;\n\n        // Ensure the file name matches the regex.\n        let captures = FILENAME_RE\n            .captures(dir_name)\n            .ok_or(span.parse_err(format!(\"invalid migration directory name: {dir_name}\")))?;\n        migrations.push(DBMigration {\n            file_name: path\n                .strip_prefix(dir)\n                .map_err(|_| {\n                    span.parse_err(format!(\n                        \"migration directory is not a subdirectory of {}\",\n                        dir.display()\n                    ))\n                })?\n                .to_string_lossy()\n                .to_string(),\n            description: captures[2].to_string(),\n            number: captures[1]\n                .parse::<u64>()\n                .map_err(|err| span.parse_err(err.to_string()))?,\n        });\n        Ok(())\n    })?;\n    Ok(migrations)\n}\n\n/// Parses Drizzle v1 migrations with the directory structure:\n/// migration-dir/\n/// ├── 0000_init_migration/\n/// │   ├── migration.sql\n/// │   └── snapshot.json\n/// ├── 0001_add_user_profile/\n/// │   ├── migration.sql\n/// │   └── snapshot.json\n/// └── meta/\n#[cfg(not(target_arch = \"wasm32\"))]\nfn parse_drizzle_v1(span: Span, dir: &Path) -> ParseResult<Vec<DBMigration>> {\n    let mut migrations = vec![];\n\n    static DIR_NAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r\"^(\\d+)_(.+)$\").unwrap());\n\n    visit_dirs(span, dir, 0, 1, &mut |entry| -> ParseResult<()> {\n        let path = entry.path();\n        let name = entry.file_name();\n        let name = name.to_str().ok_or(span.parse_err(format!(\n            \"invalid migration filename: {}\",\n            name.to_string_lossy()\n        )))?;\n\n        // Only look for migration.sql files\n        if name != \"migration.sql\" {\n            return Ok(());\n        }\n\n        let dir_name = path\n            .parent()\n            .ok_or(span.parse_err(\"migration file has no parent directory\"))?\n            .file_name()\n            .ok_or(span.parse_err(\"migration directory has no name\"))?\n            .to_str()\n            .ok_or(span.parse_err(\"migration directory has invalid name\"))?;\n\n        // Skip the meta directory\n        if dir_name == \"meta\" {\n            return Ok(());\n        }\n\n        // Ensure the directory name matches the expected pattern\n        let captures = DIR_NAME_RE\n            .captures(dir_name)\n            .ok_or(span.parse_err(format!(\"invalid migration directory name: {dir_name}\")))?;\n\n        migrations.push(DBMigration {\n            file_name: path\n                .strip_prefix(dir)\n                .map_err(|_| {\n                    span.parse_err(format!(\n                        \"migration directory is not a subdirectory of {}\",\n                        dir.display()\n                    ))\n                })?\n                .to_string_lossy()\n                .to_string(),\n            description: captures[2].to_string(),\n            number: captures[1]\n                .parse::<u64>()\n                .map_err(|err| span.parse_err(err.to_string()))?,\n        });\n\n        Ok(())\n    })?;\n\n    Ok(migrations)\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nfn parse_migrations(\n    span: Span,\n    dir: &Path,\n    source: Option<&MigrationFileSource>,\n) -> ParseResult<Vec<DBMigration>> {\n    if !dir.exists() {\n        return Err(span.parse_err(\"migrations directory does not exist\"));\n    } else if !dir.is_dir() {\n        return Err(span.parse_err(\"migrations path is not a directory\"));\n    }\n\n    let mut migrations = match source {\n        Some(MigrationFileSource::Drizzle) => parse_drizzle(span, dir),\n        Some(MigrationFileSource::DrizzleV1) => parse_drizzle_v1(span, dir),\n        Some(MigrationFileSource::Prisma) => parse_prisma(span, dir),\n        None => parse_default(span, dir),\n    }?;\n    migrations.sort_by_key(|m| m.number);\n    Ok(migrations)\n}\n\n#[cfg(target_arch = \"wasm32\")]\nfn parse_migrations(\n    _span: Span,\n    _dir: &Path,\n    _source: Option<&MigrationFileSource>,\n) -> ParseResult<Vec<DBMigration>> {\n    // Migration file parsing requires filesystem access, unavailable in WASM builds.\n    Ok(vec![])\n}\n\npub fn resolve_database_usage(data: &ResolveUsageData, db: Lrc<SQLDatabase>) -> Option<Usage> {\n    // Validate database queries, when possible.\n    #[cfg(not(target_arch = \"wasm32\"))]\n    match &data.expr.kind {\n        UsageExprKind::TemplateCall(call) => {\n            let method = &call.method.sym;\n            if method == \"query\" || method == \"queryRow\" || method == \"queryAll\" || method == \"exec\"\n            {\n                if let Some(err) = parse_template_query(&call.tpl) {\n                    let msg = match err {\n                        pg_query::Error::Parse(msg) => msg,\n                        other => other.to_string(),\n                    };\n                    call.tpl\n                        .tpl\n                        .span\n                        .err(&format!(\"invalid database query: {}\", msg));\n                }\n            }\n        }\n\n        UsageExprKind::MethodCall(call) => {\n            let method = &call.method.sym;\n            if method == \"rawQuery\"\n                || method == \"rawQueryRow\"\n                || method == \"rawQueryAll\"\n                || method == \"rawExec\"\n            {\n                // If we have string literal as the query, validate it.\n                if let Some(ast::Lit::Str(str)) =\n                    call.call.args.first().and_then(|arg| arg.expr.as_lit())\n                {\n                    if let Err(err) = pg_query::parse(str.value.as_str()) {\n                        let msg = match err {\n                            pg_query::Error::Parse(msg) => msg,\n                            other => other.to_string(),\n                        };\n                        str.span.err(&format!(\"invalid database query: {}\", msg));\n                    }\n                }\n            }\n        }\n\n        // Ignore other usage expressions.\n        _ => {}\n    }\n\n    match &data.expr.kind {\n        UsageExprKind::MethodCall(_)\n        | UsageExprKind::TemplateCall(_)\n        | UsageExprKind::FieldAccess(_)\n        | UsageExprKind::CallArg(_)\n        | UsageExprKind::ConstructorArg(_) => Some(Usage::AccessDatabase(AccessDatabaseUsage {\n            range: data.expr.range,\n            db,\n        })),\n\n        UsageExprKind::Other(_) | UsageExprKind::Callee(_) => {\n            data.expr.err(\"invalid use of database resource\");\n            None\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct AccessDatabaseUsage {\n    pub range: Range,\n    pub db: Lrc<SQLDatabase>,\n}\n\n#[cfg(not(target_arch = \"wasm32\"))]\nfn parse_template_query(tpl: &ast::TaggedTpl) -> Option<pg_query::Error> {\n    let mut query = String::new();\n    for (i, q) in tpl.tpl.quasis.iter().enumerate() {\n        query.push_str(&q.raw);\n        if !q.tail {\n            query.push_str(&format!(\"${}\", i + 1));\n        }\n    }\n\n    pg_query::parse(&query).err()\n}\n"
  },
  {
    "path": "tsparser/src/parser/resources/mod.rs",
    "content": "use std::fmt::{Display, Formatter};\n\nuse swc_common::sync::Lrc;\n\nuse crate::parser::resourceparser::resource_parser::ResourceParser;\nuse crate::parser::resources::apis::api::ENDPOINT_PARSER;\nuse crate::parser::resources::apis::authhandler::AUTHHANDLER_PARSER;\nuse crate::parser::resources::apis::gateway::GATEWAY_PARSER;\nuse crate::parser::resources::apis::service::SERVICE_PARSER;\nuse crate::parser::resources::infra::cache::{CACHE_CLUSTER_PARSER, CACHE_KEYSPACE_PARSER};\nuse crate::parser::resources::infra::cron::CRON_PARSER;\nuse crate::parser::resources::infra::metrics::METRIC_PARSER;\nuse crate::parser::resources::infra::objects::OBJECTS_PARSER;\nuse crate::parser::resources::infra::pubsub_subscription::SUBSCRIPTION_PARSER;\nuse crate::parser::resources::infra::pubsub_topic::TOPIC_PARSER;\nuse crate::parser::resources::infra::secret::SECRET_PARSER;\nuse crate::parser::resources::infra::sqldb::SQLDB_PARSER;\n\npub mod apis;\npub mod infra;\npub mod parseutil;\n\n#[derive(Debug, Clone)]\npub enum Resource {\n    ServiceClient(Lrc<apis::service_client::ServiceClient>),\n    APIEndpoint(Lrc<apis::api::Endpoint>),\n    AuthHandler(Lrc<apis::authhandler::AuthHandler>),\n    Gateway(Lrc<apis::gateway::Gateway>),\n    Service(Lrc<apis::service::Service>),\n    SQLDatabase(Lrc<infra::sqldb::SQLDatabase>),\n    Bucket(Lrc<infra::objects::Bucket>),\n    PubSubTopic(Lrc<infra::pubsub_topic::Topic>),\n    PubSubSubscription(Lrc<infra::pubsub_subscription::Subscription>),\n    CronJob(Lrc<infra::cron::CronJob>),\n    Secret(Lrc<infra::secret::Secret>),\n    Metric(Lrc<infra::metrics::Metric>),\n    CacheCluster(Lrc<infra::cache::CacheCluster>),\n    CacheKeyspace(Lrc<infra::cache::CacheKeyspace>),\n}\n\n#[derive(Debug, Eq, Hash, PartialEq, Clone)]\npub enum ResourcePath {\n    SQLDatabase { name: String },\n    Bucket { name: String },\n    CacheCluster { name: String },\n}\n\nimpl Display for Resource {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Resource::ServiceClient(client) => write!(f, \"ServiceClient({})\", client.service_name),\n            Resource::APIEndpoint(api) => {\n                write!(f, \"APIEndpoint({}::{})\", api.service_name, api.name)\n            }\n            Resource::AuthHandler(handler) => {\n                write!(f, \"AuthHandler({}::{})\", handler.service_name, handler.name)\n            }\n            Resource::Gateway(gw) => {\n                write!(f, \"Gateway({})\", gw.name)\n            }\n            Resource::SQLDatabase(db) => write!(f, \"SQLDatabase({})\", db.name),\n            Resource::Bucket(db) => write!(f, \"Bucket({})\", db.name),\n            Resource::PubSubTopic(topic) => write!(f, \"PubSubTopic({})\", topic.name),\n            Resource::PubSubSubscription(sub) => write!(f, \"PubSubSubscription({})\", sub.name),\n            Resource::CronJob(cron) => write!(f, \"CronJob({})\", cron.name),\n            Resource::Secret(secret) => write!(f, \"Secret({})\", secret.name),\n            Resource::Service(svc) => write!(f, \"Service({})\", svc.name),\n            Resource::Metric(metric) => write!(f, \"Metric({})\", metric.name),\n            Resource::CacheCluster(cluster) => write!(f, \"CacheCluster({})\", cluster.name),\n            Resource::CacheKeyspace(keyspace) => {\n                let cluster_name = keyspace.cluster.name.as_deref().unwrap_or(\"<unknown>\");\n                write!(\n                    f,\n                    \"CacheKeyspace({}::{})\",\n                    cluster_name, keyspace.key_pattern\n                )\n            }\n        }\n    }\n}\n\npub static DEFAULT_RESOURCE_PARSERS: &[&ResourceParser] = &[\n    // The service parser must come first, as other resources may depend on\n    // knowing which service they belong to.\n    &SERVICE_PARSER,\n    &ENDPOINT_PARSER,\n    &AUTHHANDLER_PARSER,\n    &GATEWAY_PARSER,\n    &SQLDB_PARSER,\n    &OBJECTS_PARSER,\n    &TOPIC_PARSER,\n    &SUBSCRIPTION_PARSER,\n    &CRON_PARSER,\n    &SECRET_PARSER,\n    &METRIC_PARSER,\n    // Cache cluster must come before keyspace as keyspaces depend on clusters.\n    &CACHE_CLUSTER_PARSER,\n    &CACHE_KEYSPACE_PARSER,\n];\n"
  },
  {
    "path": "tsparser/src/parser/resources/parseutil.rs",
    "content": "use std::collections::{HashMap, HashSet};\nuse std::rc::Rc;\n\nuse litparser::{LitParser, ParseResult, ToParseErr};\nuse swc_common::{Span, Spanned};\nuse swc_ecma_ast::{self as ast, CallExpr, MemberExpr, NewExpr, TsTypeParamInstantiation};\nuse swc_ecma_visit::VisitWithPath;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::resourceparser::bind::BindName;\nuse crate::parser::types::{Basic, Interface, Object, Type, TypeChecker};\nuse crate::parser::Range;\nuse litparser::Sp;\n\npub trait ReferenceParser\nwhere\n    Self: Sized,\n{\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>>;\n}\n\npub struct NamedClassResource<Config, const NAME_IDX: usize = 0, const CONFIG_IDX: usize = 1> {\n    pub range: Range,\n    pub constructor_args: Vec<ast::ExprOrSpread>,\n    pub doc_comment: Option<String>,\n    pub resource_name: String,\n    pub bind_name: BindName,\n    pub config: Config,\n    pub expr: ast::NewExpr,\n}\n\nimpl<Config, const NAME_IDX: usize, const CONFIG_IDX: usize> Spanned\n    for NamedClassResource<Config, NAME_IDX, CONFIG_IDX>\n{\n    fn span(&self) -> Span {\n        self.range.to_span()\n    }\n}\n\nimpl<Config: LitParser, const NAME_IDX: usize, const CONFIG_IDX: usize> ReferenceParser\n    for NamedClassResource<Config, NAME_IDX, CONFIG_IDX>\n{\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        let res = match NamedClassResourceOptionalConfig::<Config, NAME_IDX, CONFIG_IDX>::parse_resource_reference(module, path)? {\n            None => return Ok(None),\n            Some(res) => res,\n        };\n        let Some(config) = res.config else {\n            return Err(res\n                .range\n                .to_span()\n                .parse_err(\"missing required config object\"));\n        };\n\n        Ok(Some(Self {\n            range: res.range,\n            constructor_args: res.constructor_args,\n            doc_comment: res.doc_comment,\n            resource_name: res.resource_name,\n            bind_name: res.bind_name,\n            config,\n            expr: res.expr,\n        }))\n    }\n}\n\n#[derive(Debug)]\npub struct NamedClassResourceOptionalConfig<\n    Config,\n    const NAME_IDX: usize = 0,\n    const CONFIG_IDX: usize = 1,\n> {\n    pub range: Range,\n    pub constructor_args: Vec<ast::ExprOrSpread>,\n    pub doc_comment: Option<String>,\n    pub resource_name: String,\n    pub bind_name: BindName,\n    pub config: Option<Config>,\n    pub expr: ast::NewExpr,\n}\n\nimpl<Config, const NAME_IDX: usize, const CONFIG_IDX: usize> Spanned\n    for NamedClassResourceOptionalConfig<Config, NAME_IDX, CONFIG_IDX>\n{\n    fn span(&self) -> Span {\n        self.range.to_span()\n    }\n}\n\nimpl<Config: LitParser, const NAME_IDX: usize, const CONFIG_IDX: usize> ReferenceParser\n    for NamedClassResourceOptionalConfig<Config, NAME_IDX, CONFIG_IDX>\n{\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for node in path.iter().rev() {\n            if let swc_ecma_visit::AstParentNodeRef::NewExpr(\n                expr,\n                swc_ecma_visit::fields::NewExprField::Callee,\n            ) = node\n            {\n                let Some(args) = &expr.args else {\n                    return Err(expr.span.parse_err(\"missing constructor arguments\"));\n                };\n\n                let bind_name = match extract_bind_name(path)? {\n                    Some(name) => BindName::Named(name),\n                    None => {\n                        if is_default_export(path, (*expr).into()) {\n                            BindName::DefaultExport\n                        } else {\n                            BindName::Anonymous\n                        }\n                    }\n                };\n                let resource_name = extract_resource_name(expr.span, args, NAME_IDX)?;\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n\n                let config = args\n                    .get(CONFIG_IDX)\n                    .map(|arg| Config::parse_lit(&arg.expr))\n                    .transpose()?;\n\n                return Ok(Some(Self {\n                    range: expr.span.into(),\n                    constructor_args: args.clone(),\n                    resource_name: resource_name.to_string(),\n                    doc_comment,\n                    bind_name,\n                    config,\n                    expr: (*expr).to_owned(),\n                }));\n            }\n        }\n        Ok(None)\n    }\n}\n\npub struct UnnamedClassResource<Config, const CONFIG_IDX: usize = 0> {\n    pub range: Range,\n    #[allow(dead_code)]\n    pub constructor_args: Vec<ast::ExprOrSpread>,\n    pub doc_comment: Option<String>,\n    pub bind_name: BindName,\n    pub config: Config,\n}\n\nimpl<Config, const CONFIG_IDX: usize> Spanned for UnnamedClassResource<Config, CONFIG_IDX> {\n    fn span(&self) -> Span {\n        self.range.to_span()\n    }\n}\n\nimpl<Config: LitParser, const CONFIG_IDX: usize> ReferenceParser\n    for UnnamedClassResource<Config, CONFIG_IDX>\n{\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for node in path.iter().rev() {\n            if let swc_ecma_visit::AstParentNodeRef::NewExpr(\n                expr,\n                swc_ecma_visit::fields::NewExprField::Callee,\n            ) = node\n            {\n                let Some(args) = &expr.args else {\n                    return Err(expr.span.parse_err(\"missing constructor arguments\"));\n                };\n                let Some(config_arg) = args.get(CONFIG_IDX) else {\n                    return Err(expr.span.parse_err(\"missing config object\"));\n                };\n\n                let bind_name = match extract_bind_name(path)? {\n                    Some(name) => BindName::Named(name),\n                    None => {\n                        if is_default_export(path, (*expr).into()) {\n                            BindName::DefaultExport\n                        } else {\n                            BindName::Anonymous\n                        }\n                    }\n                };\n\n                let config = Config::parse_lit(&config_arg.expr)?;\n                let doc_comment = module.preceding_comments(expr.span.lo.into());\n\n                return Ok(Some(Self {\n                    range: expr.span.into(),\n                    constructor_args: args.clone(),\n                    doc_comment,\n                    bind_name,\n                    config,\n                }));\n            }\n        }\n        Ok(None)\n    }\n}\n\npub struct NamedStaticMethod<const NAME_IDX: usize = 0> {\n    pub range: Range,\n    #[allow(dead_code)]\n    pub constructor_args: Vec<ast::ExprOrSpread>,\n    #[allow(dead_code)]\n    pub doc_comment: Option<String>,\n    pub resource_name: String,\n    pub bind_name: BindName,\n}\n\nimpl<const NAME_IDX: usize> ReferenceParser for NamedStaticMethod<NAME_IDX> {\n    fn parse_resource_reference(\n        module: &Module,\n        path: &swc_ecma_visit::AstNodePath,\n    ) -> ParseResult<Option<Self>> {\n        for (idx, node) in path.iter().rev().enumerate() {\n            if let swc_ecma_visit::AstParentNodeRef::MemberExpr(\n                expr,\n                swc_ecma_visit::fields::MemberExprField::Obj,\n            ) = node\n            {\n                let ast::MemberProp::Ident(method_name) = &expr.prop else {\n                    continue;\n                };\n                if method_name.sym != \"named\" {\n                    continue;\n                }\n\n                let idx = path.len() - idx - 1;\n\n                // Make sure the parent is a call expression.\n                // The path goes:\n                // CallExpr -> Callee -> Expr -> MemberExpr\n                // So we want idx-3.\n                let Some(parent) = path.get(idx - 3) else {\n                    continue;\n                };\n                let swc_ecma_visit::AstParentNodeRef::CallExpr(\n                    call,\n                    swc_ecma_visit::fields::CallExprField::Callee,\n                ) = parent\n                else {\n                    continue;\n                };\n\n                let bind_name = match extract_bind_name(path)? {\n                    Some(name) => BindName::Named(name),\n                    None => {\n                        if is_default_export(path, (*expr).into()) {\n                            BindName::DefaultExport\n                        } else {\n                            BindName::Anonymous\n                        }\n                    }\n                };\n                let resource_name = extract_resource_name(call.span, &call.args, NAME_IDX)?;\n                let doc_comment = module.preceding_comments(call.span.lo.into());\n\n                return Ok(Some(Self {\n                    range: call.span.into(),\n                    constructor_args: call.args.clone(),\n                    resource_name: resource_name.to_string(),\n                    doc_comment,\n                    bind_name,\n                }));\n            }\n        }\n        Ok(None)\n    }\n}\n\n/// Extracts the name of a resource.\npub fn extract_resource_name(\n    span: swc_common::Span,\n    args: &[ast::ExprOrSpread],\n    idx: usize,\n) -> ParseResult<&str> {\n    let Some(val) = args.get(idx) else {\n        return Err(span.parse_err(format!(\"missing resource name as argument[{idx}]\")));\n    };\n    if val.spread.is_none() {\n        if let ast::Expr::Lit(ast::Lit::Str(str)) = val.expr.as_ref() {\n            return Ok(str.value.as_ref());\n        }\n    }\n\n    Err(span.parse_err(\"expected string literal\"))\n}\n\npub fn extract_bind_name(path: &swc_ecma_visit::AstNodePath) -> ParseResult<Option<ast::Ident>> {\n    for node in path.iter().rev() {\n        if let swc_ecma_visit::AstParentNodeRef::VarDecl(\n            var,\n            swc_ecma_visit::fields::VarDeclField::Decls(idx),\n        ) = node\n        {\n            let Some(decl) = var.decls.get(*idx) else {\n                return Err(var\n                    .span\n                    .parse_err(format!(\"missing declaration at index {idx}\")));\n            };\n            match &decl.name {\n                ast::Pat::Ident(bind_name) => {\n                    return Ok(Some(bind_name.id.clone()));\n                }\n                _ => {\n                    return Err(decl.name.parse_err(\"expected identifier as bind name\"));\n                }\n            }\n        }\n    }\n    Ok(None)\n}\n\npub enum Expr<'a> {\n    New(&'a NewExpr),\n    Call(&'a CallExpr),\n    Member(&'a MemberExpr),\n}\n\nimpl<'a> From<&'a NewExpr> for Expr<'a> {\n    fn from(expr: &'a NewExpr) -> Self {\n        Self::New(expr)\n    }\n}\nimpl<'a> From<&'a CallExpr> for Expr<'a> {\n    fn from(expr: &'a CallExpr) -> Self {\n        Self::Call(expr)\n    }\n}\n\nimpl<'a> From<&'a MemberExpr> for Expr<'a> {\n    fn from(expr: &'a MemberExpr) -> Self {\n        Self::Member(expr)\n    }\n}\n\n// checks if `expr` is the default export in `path`\npub fn is_default_export(path: &swc_ecma_visit::AstNodePath, expr: Expr) -> bool {\n    for node in path.iter().rev() {\n        match node {\n            swc_ecma_visit::AstParentNodeRef::ExportDefaultExpr(\n                swc_ecma_ast::ExportDefaultExpr {\n                    expr: exported_expr,\n                    ..\n                },\n                swc_ecma_visit::fields::ExportDefaultExprField::Expr,\n            ) => {\n                return match expr {\n                    Expr::Member(member_expr) => {\n                        matches!(**exported_expr, swc_ecma_ast::Expr::Member(ref expr) if expr == member_expr)\n                    }\n                    Expr::Call(call_expr) => {\n                        matches!(**exported_expr, swc_ecma_ast::Expr::Call(ref expr) if expr == call_expr)\n                    }\n                    Expr::New(new_expr) => {\n                        matches!(**exported_expr, swc_ecma_ast::Expr::New(ref expr) if expr == new_expr)\n                    }\n                }\n            }\n            _ => continue,\n        }\n    }\n    false\n}\n\npub struct TrackedNames<'a>(HashMap<&'a str, Vec<&'a str>>);\n\nimpl<'a> TrackedNames<'a> {\n    pub fn new(names: &'a [(&'a str, &'a str)]) -> Self {\n        let mut modules = HashMap::new();\n        for &(module, name) in names {\n            modules.entry(module).or_insert_with(Vec::new).push(name);\n        }\n\n        Self(modules)\n    }\n\n    pub fn get(&self, module: &str) -> Option<&[&str]> {\n        self.0.get(module).map(|v| &v[..])\n    }\n}\n\n/// Collect the idents matching the given names to track.\nfn collect_import_idents<'a>(\n    module: &'a Module,\n    tracked_names: &'a TrackedNames<'a>,\n) -> (HashSet<ast::Id>, HashMap<ast::Id, &'a [&'a str]>) {\n    let mut local_names = HashSet::new();\n    let mut module_names = HashMap::new();\n\n    for it in &module.ast.body {\n        if let ast::ModuleItem::ModuleDecl(ast::ModuleDecl::Import(import)) = it {\n            // Is the module in question one we care about?\n            let Some(tracked) = tracked_names.get(import.src.value.as_ref()) else {\n                continue;\n            };\n\n            // Iterate over the specifiers and determine the local idents.\n            for spec in &import.specifiers {\n                match spec {\n                    ast::ImportSpecifier::Named(named) => {\n                        // We are importing specific names from the module.\n                        // Determine if the name is one we care about.\n                        let is_relevant = tracked.iter().any(|t| match named.imported {\n                            Some(ast::ModuleExportName::Ident(ref i)) => i.sym.as_ref() == *t,\n                            Some(ast::ModuleExportName::Str(_)) => false,\n                            None => named.local.sym.as_ref() == *t,\n                        });\n\n                        if is_relevant {\n                            // The name is one we care about, so add it to the set.\n                            local_names.insert(named.local.to_id());\n                        }\n                    }\n\n                    ast::ImportSpecifier::Default(_) => {\n                        // We are importing the default export from the module.\n\n                        // Do we want to handle this? If so we need to identify the\n                        // default import when calling this function.\n                        // For now, do nothing.\n                    }\n                    ast::ImportSpecifier::Namespace(namespace) => {\n                        // We're importing the module as a namespace (\"import * as foo from 'foo'\").\n                        module_names.insert(namespace.local.to_id(), tracked);\n                    }\n                }\n            }\n        }\n    }\n\n    (local_names, module_names)\n}\n\npub fn iter_references<R: ReferenceParser>(\n    module: &Module,\n    names: &TrackedNames,\n) -> impl Iterator<Item = ParseResult<R>> {\n    let (local_ids, _module_ids) = collect_import_idents(module, names);\n    let mut visitor = <IterReferenceVisitor<'_, R>>::new(module, local_ids);\n    module\n        .ast\n        .visit_with_path(&mut visitor, &mut Default::default());\n    visitor.results.into_iter()\n}\n\nstruct IterReferenceVisitor<'a, R> {\n    module: &'a Module,\n    local_ids: HashSet<ast::Id>,\n    results: Vec<ParseResult<R>>,\n}\n\nimpl<'a, R> IterReferenceVisitor<'a, R> {\n    fn new(module: &'a Module, local_ids: HashSet<ast::Id>) -> Self {\n        Self {\n            module,\n            local_ids,\n            results: Vec::new(),\n        }\n    }\n}\n\nimpl<R: ReferenceParser> swc_ecma_visit::VisitAstPath for IterReferenceVisitor<'_, R> {\n    fn visit_ident<'ast: 'r, 'r>(\n        &mut self,\n        n: &'ast ast::Ident,\n        path: &mut swc_ecma_visit::AstNodePath<'r>,\n    ) {\n        if !self.local_ids.contains(&n.to_id()) {\n            return;\n        };\n        // TODO check for module_ids\n\n        // If this is part of an import declaration, ignore it.\n        if path\n            .kinds()\n            .iter()\n            .any(|p| matches!(p, swc_ecma_visit::AstParentKind::ImportDecl(_)))\n        {\n            return;\n        }\n\n        match R::parse_resource_reference(self.module, path) {\n            Ok(None) => {} // do nothing\n            Ok(Some(r)) => self.results.push(Ok(r)),\n            Err(e) => self.results.push(Err(e)),\n        }\n    }\n}\n\npub fn extract_type_param(\n    params: Option<&TsTypeParamInstantiation>,\n    idx: usize,\n) -> Option<&ast::TsType> {\n    let params = params?;\n    let param = params.params.get(idx)?;\n    Some(param.as_ref())\n}\n\npub fn resolve_object_for_bind_name(\n    type_checker: &TypeChecker,\n    module: Rc<Module>,\n    bind_name: &BindName,\n) -> Option<Rc<Object>> {\n    match bind_name {\n        BindName::Anonymous => None,\n        BindName::DefaultExport => type_checker.resolve_default_export(module.clone()),\n        BindName::Named(ref id) => {\n            type_checker.resolve_obj(module.clone(), &ast::Expr::Ident(id.clone()))\n        }\n    }\n}\n\n/// Returns the interface if the type resolves to an Interface, otherwise returns None.\npub fn resolve_interface(tc: &TypeChecker, typ: &Sp<Type>) -> Option<Interface> {\n    use crate::parser::types::unwrap_promise;\n\n    let span = typ.span();\n    let typ = unwrap_promise(tc.state(), typ);\n    match typ {\n        Type::Basic(Basic::Void) => None,\n        Type::Interface(iface) => Some(iface.clone()),\n        Type::Named(named) => {\n            let underlying = tc.underlying(named.obj.module_id, typ);\n            resolve_interface(tc, &Sp::new(span, underlying))\n        }\n        _ => None,\n    }\n}\n\n/// Validates that a resource name follows snake_case naming conventions.\n///\n/// Snake case names must:\n/// - Be between 1 and 63 characters long\n/// - Start with a lowercase letter\n/// - End with a lowercase letter or number\n/// - Only contain lowercase letters, numbers, and underscores\n/// - Not start with the reserved prefix (if provided)\n///\n/// Returns `Ok(())` if valid, or an error string if invalid.\npub fn validate_snake_case_name(name: &str, reserved_prefix: Option<&str>) -> Result<(), String> {\n    const MAX_LENGTH: usize = 63;\n\n    // Check length\n    if name.is_empty() || name.len() > MAX_LENGTH {\n        return Err(format!(\n            \"name must be between 1 and {} characters long (got {})\",\n            MAX_LENGTH,\n            name.len()\n        ));\n    }\n\n    // Check snake_case format: ^[a-z]([_a-z0-9]*[a-z0-9])?$\n    let mut chars = name.chars();\n\n    // First character must be a lowercase letter\n    let first = chars.next().unwrap();\n    if !first.is_ascii_lowercase() {\n        return Err(format!(\n            \"name must start with a lowercase letter (got '{}')\",\n            first\n        ));\n    }\n\n    // If there's only one character, it's valid\n    if name.len() == 1 {\n        // Check reserved prefix\n        if let Some(prefix) = reserved_prefix {\n            if name.starts_with(prefix) {\n                return Err(format!(\n                    \"name must not start with reserved prefix '{}' (got '{}')\",\n                    prefix, name\n                ));\n            }\n        }\n        return Ok(());\n    }\n\n    // Last character must be lowercase letter or digit\n    let last = name.chars().last().unwrap();\n    if !last.is_ascii_lowercase() && !last.is_ascii_digit() {\n        return Err(format!(\n            \"name must end with a lowercase letter or digit (got '{}')\",\n            last\n        ));\n    }\n\n    // Middle characters must be lowercase letters, digits, or underscores\n    for (i, c) in name.chars().enumerate() {\n        if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' {\n            return Err(format!(\n                \"name must only contain lowercase letters, numbers, and underscores (got '{}' at position {})\",\n                c, i\n            ));\n        }\n    }\n\n    // Check reserved prefix\n    if let Some(prefix) = reserved_prefix {\n        if name.starts_with(prefix) {\n            return Err(format!(\n                \"name must not start with reserved prefix '{}' (got '{}')\",\n                prefix, name\n            ));\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "tsparser/src/parser/respath.rs",
    "content": "use std::{fmt::Write, ops::Deref};\n\nuse litparser::Sp;\nuse swc_common::{BytePos, Span};\n\nuse crate::span_err::{ErrorWithSpanExt, SpErr};\n\nuse super::types::validation;\n\n#[derive(Debug, Clone)]\npub struct Path {\n    pub span: Span,\n    pub segments: Vec<Sp<Segment>>,\n}\n\nimpl Path {\n    pub fn dynamic_segments(&self) -> impl Iterator<Item = &Sp<Segment>> {\n        self.segments\n            .iter()\n            .filter(|s| !matches!(s.get(), Segment::Literal(_)))\n    }\n\n    pub fn has_dynamic_segments(&self) -> bool {\n        self.dynamic_segments().next().is_some()\n    }\n}\n\nimpl std::fmt::Display for Path {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        for seg in &self.segments {\n            f.write_char('/')?;\n            if let Some(sigil) = seg.sigil() {\n                f.write_char(sigil)?;\n            }\n            f.write_str(seg.lit_or_name())?;\n        }\n        Ok(())\n    }\n}\n\n#[derive(Debug, Clone, PartialEq)]\npub enum Segment {\n    Literal(String),\n    Param {\n        name: String,\n        value_type: ValueType,\n        validation: Option<validation::Expr>,\n    },\n    Wildcard {\n        name: String,\n        validation: Option<validation::Expr>,\n    },\n    Fallback {\n        name: String,\n        validation: Option<validation::Expr>,\n    },\n}\n\nimpl Segment {\n    pub fn sigil(&self) -> Option<char> {\n        match self {\n            Segment::Literal(_s) => None,\n            Segment::Param { .. } => Some(':'),\n            Segment::Wildcard { .. } => Some('*'),\n            Segment::Fallback { .. } => Some('!'),\n        }\n    }\n\n    pub fn lit_or_name(&self) -> &str {\n        match self {\n            Segment::Literal(s) => s,\n            Segment::Param { name, .. } => name,\n            Segment::Wildcard { name, .. } => name,\n            Segment::Fallback { name, .. } => name,\n        }\n    }\n\n    pub fn is_literal(&self) -> bool {\n        matches!(self, Segment::Literal(_))\n    }\n\n    pub fn is_dynamic(&self) -> bool {\n        !self.is_literal()\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq)]\npub enum ValueType {\n    String,\n    Int,\n    Bool,\n}\n\npub struct ParseOptions {\n    pub allow_wildcard: bool,\n    pub allow_fallback: bool,\n    pub prefix_slash: bool,\n}\n\nimpl Default for ParseOptions {\n    fn default() -> Self {\n        ParseOptions {\n            allow_wildcard: true,\n            allow_fallback: true,\n            prefix_slash: true,\n        }\n    }\n}\n\n#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]\npub enum PathParseError {\n    #[error(\"empty path\")]\n    EmptyPath,\n    #[error(\"path must start with '/'\")]\n    MustStartWithSlash,\n    #[error(\"path must not start with '/'\")]\n    MustNotStartWithSlash,\n    #[error(\"path cannot contain empty path segment\")]\n    EmptySegment,\n    #[error(\"path parameters must have a name\")]\n    UnnamedParam,\n    #[error(\"wildcard segment must be at the end of the path\")]\n    WildcardNotAtEnd,\n    #[error(\"fallback segment must be at the end of the path\")]\n    FallbackNotAtEnd,\n    #[error(\"path cannot contain query parameters (the '?' character)\")]\n    ContainsQuery,\n    #[error(\"path cannot contain url fragment (the '#' character)\")]\n    ContainsFragment,\n    #[error(\"path cannot contain url scheme\")]\n    ContainsScheme,\n    #[error(\"path cannot contain url authority\")]\n    ContainsAuthority,\n    #[error(\"path cannot contain hostname\")]\n    ContainsHostname,\n    #[error(\"path is invalid: {0}\")]\n    Invalid(String),\n}\n\nimpl Path {\n    pub fn parse(\n        span: Span,\n        path: &str,\n        opts: ParseOptions,\n    ) -> Result<Self, SpErr<PathParseError>> {\n        if path.is_empty() {\n            return Err(PathParseError::EmptyPath.with_span(span));\n        } else if !path.starts_with('/') && opts.prefix_slash {\n            return Err(PathParseError::MustStartWithSlash.with_span(span));\n        } else if path.starts_with('/') && !opts.prefix_slash {\n            return Err(PathParseError::MustNotStartWithSlash.with_span(span));\n        }\n\n        // Ensure this is a valid url path.\n        parse_url_path(path).map_err(|err| err.with_span(span))?;\n\n        let mut segments = vec![];\n\n        let path_end = path.len();\n        let mut idx = 0;\n        while idx < path_end {\n            if opts.prefix_slash || !segments.is_empty() {\n                idx += 1; // drop leading slash\n            }\n\n            let seg_start = idx;\n            let seg_end = {\n                let remainder = &path[idx..];\n                idx + remainder.find('/').unwrap_or(remainder.len())\n            };\n\n            // Find the next path segment.\n            let val = &path[seg_start..seg_end];\n            let seg: Segment = match val.chars().next() {\n                Some(':') => Segment::Param {\n                    name: val[1..].to_string(),\n                    value_type: ValueType::String,\n                    validation: None,\n                },\n                Some('*') if opts.allow_wildcard => Segment::Wildcard {\n                    name: val[1..].to_string(),\n                    validation: None,\n                },\n                Some('!') if opts.allow_wildcard => Segment::Fallback {\n                    name: val[1..].to_string(),\n                    validation: None,\n                },\n                _ => Segment::Literal(val.to_string()),\n            };\n\n            let span = span\n                .with_lo(span.lo + BytePos(seg_start as u32))\n                .with_hi(span.hi + BytePos(seg_end as u32));\n\n            segments.push(Sp::new(span, seg));\n            idx = seg_end;\n        }\n\n        // Validate the segments.\n        for (idx, seg) in segments.iter().enumerate() {\n            match seg.deref() {\n                Segment::Literal(lit) if lit.is_empty() && segments.len() > 1 => {\n                    return Err(PathParseError::EmptySegment.with_span(seg.span()));\n                }\n                Segment::Param { name, .. } if name.is_empty() => {\n                    return Err(PathParseError::UnnamedParam.with_span(seg.span()));\n                }\n                Segment::Wildcard { name, .. } if name.is_empty() => {\n                    return Err(PathParseError::UnnamedParam.with_span(seg.span()));\n                }\n                Segment::Wildcard { .. } if idx != segments.len() - 1 => {\n                    return Err(PathParseError::WildcardNotAtEnd.with_span(seg.span()));\n                }\n                Segment::Fallback { .. } if idx != segments.len() - 1 => {\n                    return Err(PathParseError::FallbackNotAtEnd.with_span(seg.span()));\n                }\n                _ => {}\n            }\n        }\n\n        Ok(Path { span, segments })\n    }\n}\n\nfn parse_url_path(path: &str) -> Result<(), PathParseError> {\n    // The url crate only supports parsing absolute urls, so use a dummy base\n    // and ensure it is the same after parsing.\n    let base = url::Url::parse(\"base://url.here\").expect(\"internal error: invalid base url\");\n\n    let url = url::Url::options()\n        .base_url(Some(&base))\n        .parse(path)\n        .map_err(|err| PathParseError::Invalid(err.to_string()))?;\n\n    if url.scheme() != base.scheme() {\n        return Err(PathParseError::ContainsScheme);\n    } else if url.authority() != base.authority() {\n        return Err(PathParseError::ContainsAuthority);\n    }\n\n    match url.host_str() {\n        None => {\n            // We should always have a host since the base url has one.\n            return Err(PathParseError::ContainsHostname);\n        }\n        Some(host) => {\n            if host != base.host_str().unwrap() {\n                return Err(PathParseError::ContainsHostname);\n            }\n        }\n    }\n\n    if url.query().is_some() {\n        Err(PathParseError::ContainsQuery)\n    } else if url.fragment().is_some() {\n        Err(PathParseError::ContainsFragment)\n    } else {\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use swc_common::DUMMY_SP;\n\n    use super::*;\n\n    #[test]\n    fn test_parse() {\n        let tests = vec![\n            (\"/\", Ok(vec![Segment::Literal(\"\".to_string())])),\n            (\"/foo\", Ok(vec![Segment::Literal(\"foo\".to_string())])),\n            (\n                \"/foo/bar\",\n                Ok(vec![\n                    Segment::Literal(\"foo\".to_string()),\n                    Segment::Literal(\"bar\".to_string()),\n                ]),\n            ),\n            (\n                \"/:foo/*bar\",\n                Ok(vec![\n                    Segment::Param {\n                        name: \"foo\".to_string(),\n                        value_type: ValueType::String,\n                        validation: None,\n                    },\n                    Segment::Wildcard {\n                        name: \"bar\".to_string(),\n                        validation: None,\n                    },\n                ]),\n            ),\n            (\"\", Err(PathParseError::EmptyPath)),\n            (\"/foo//bar\", Err(PathParseError::EmptySegment)),\n            (\"/foo/\", Err(PathParseError::EmptySegment)),\n            (\"/:foo/*\", Err(PathParseError::UnnamedParam)),\n            (\"/:foo/*/bar\", Err(PathParseError::UnnamedParam)),\n            (\"/:foo/*bar/baz\", Err(PathParseError::WildcardNotAtEnd)),\n            (\"/foo?bar=baz\", Err(PathParseError::ContainsQuery)),\n            (\"/foo#bar\", Err(PathParseError::ContainsFragment)),\n            (\n                \"/foo/!fallback\",\n                Ok(vec![\n                    Segment::Literal(\"foo\".to_string()),\n                    Segment::Fallback {\n                        name: \"fallback\".to_string(),\n                        validation: None,\n                    },\n                ]),\n            ),\n        ];\n\n        for (path, want) in tests {\n            let got = Path::parse(DUMMY_SP, path, Default::default());\n            match (got, want) {\n                (Ok(got), Ok(want)) => {\n                    let segments: Vec<_> = got.segments.into_iter().map(|s| s.take()).collect();\n                    assert_eq!(segments, want, \"path {path:?}\");\n                }\n                (Err(got), Err(want)) => {\n                    assert_eq!(got.error, want, \"path {path:?}\");\n                }\n                (Ok(got), Err(want)) => {\n                    panic!(\"got {got:?}, want err {want:?}, path {path:?}\");\n                }\n                (Err(got), Ok(want)) => {\n                    panic!(\"got err {got:?}, want {want:?}, path {path:?}\");\n                }\n            }\n        }\n    }\n\n    #[test]\n    fn test_parse_url_path() {\n        let ok_paths = &[\"foo\", \"/foo/bar\", \"/foo/:bar\", \"/*wildcard\", \"/!fallback\"];\n        let err_paths = &[\"http://foo.com\"];\n\n        for path in ok_paths {\n            let path = parse_url_path(path);\n            assert!(path.is_ok(), \"path {path:?} should be ok\");\n        }\n\n        for path in err_paths {\n            let path = parse_url_path(path);\n            assert!(path.is_err(), \"path {path:?} should be err\");\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/service_discovery.rs",
    "content": "use std::collections::{HashMap, HashSet};\nuse std::path::PathBuf;\n\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\n\nuse crate::parser::resourceparser::bind::Bind;\nuse crate::parser::resources::Resource;\nuse crate::parser::{FilePath, FileSet};\n\n/// Discover the services in an Encore application, based on the parsed resources.\npub fn discover_services<'a>(\n    file_set: &'a FileSet,\n    binds: &'a Vec<Lrc<Bind>>,\n) -> Vec<DiscoveredService> {\n    let sd = ServiceDiscoverer {\n        file_set,\n        binds,\n        services: HashMap::new(),\n        strong_root: HashSet::new(),\n    };\n    sd.discover()\n}\n\n#[derive(Debug, PartialEq, Eq)]\npub struct DiscoveredService {\n    pub name: String,\n    pub root: PathBuf,\n}\n\nstruct ServiceDiscoverer<'a> {\n    // Inputs\n    file_set: &'a FileSet,\n    binds: &'a Vec<Lrc<Bind>>,\n\n    // Outputs\n    services: HashMap<PathBuf, DiscoveredService>,\n    strong_root: HashSet<PathBuf>,\n}\n\nimpl ServiceDiscoverer<'_> {\n    fn discover(mut self) -> Vec<DiscoveredService> {\n        for b in self.binds {\n            match &b.resource {\n                Resource::Service(svc) => {\n                    self.possible_service_root(b.as_ref(), true, Some(svc.name.clone()))\n                }\n                Resource::APIEndpoint(_) => self.possible_service_root(b.as_ref(), false, None),\n                Resource::PubSubSubscription(_) => {\n                    self.possible_service_root(b.as_ref(), false, None)\n                }\n                Resource::Gateway(_) => self.possible_service_root(b.as_ref(), false, None),\n                Resource::AuthHandler(_) => self.possible_service_root(b.as_ref(), false, None),\n                _ => {}\n            }\n        }\n\n        let mut svcs = self.services.into_values().collect::<Vec<_>>();\n\n        // Validate the services.\n        for (i, svc) in svcs.iter().enumerate() {\n            for other in &svcs[i + 1..] {\n                if svc.root.starts_with(&other.root) {\n                    HANDLER.with(|h| {\n                        h.err(&format!(\n                            \"service {} cannot be contained within service {}\",\n                            svc.name, other.name\n                        ))\n                    });\n                } else if other.root.starts_with(&svc.root) {\n                    HANDLER.with(|h| {\n                        h.err(&format!(\n                            \"service {} cannot be contained within service {}\",\n                            other.name, svc.name,\n                        ))\n                    });\n                } else if svc.name == other.name {\n                    HANDLER.with(|h| h.err(&format!(\"service {} defined twice\", svc.name,)));\n                }\n            }\n        }\n\n        // Sort the services by name for deterministic output.\n        svcs.sort_by(|a, b| a.name.cmp(&b.name));\n\n        svcs\n    }\n\n    fn possible_service_root(&mut self, bind: &Bind, strong: bool, service_name: Option<String>) {\n        let Some(range) = bind.range else {\n            return;\n        };\n        let file = range.file(self.file_set);\n        let file_path = match file {\n            FilePath::Real(ref buf) => buf,\n            FilePath::Custom(_) => return,\n        };\n        let Some(root) = file_path.parent().map(|p| p.to_path_buf()) else {\n            return;\n        };\n\n        // Determine the service name.\n        let service_name = match service_name {\n            Some(name) => name,\n            None => {\n                // Ensure we have a valid service name.\n                let dir_name = root\n                    .file_name()\n                    .and_then(|x| x.to_str())\n                    .map(|x| x.to_string());\n                let Some(dir_name) = dir_name else {\n                    return;\n                };\n                dir_name\n            }\n        };\n\n        if strong {\n            // Always mark the root as a strong root, even if it is already marked as a service.\n            self.strong_root.insert(root.clone());\n        }\n\n        // If the service is already marked, we don't need to do anything.\n        if self.services.contains_key(&root) {\n            return;\n        }\n\n        // Loop over the existing services and remove any that are subdirectories of this one.\n        // Also look for any existing services which are parents of this root.\n        let mut to_delete = Vec::new();\n        for existing_root in self.services.keys() {\n            // If the existing service is a subdirectory of this one, we can remove it.\n            if existing_root.starts_with(&root) {\n                // If the existing service is a strong root, we can't merge it with this one.\n                if self.strong_root.contains(existing_root) {\n                    continue;\n                } else {\n                    // The existing service is a descendant of this one,\n                    // so remove it in favor of this one.\n                    to_delete.push(existing_root.clone());\n                }\n            } else if root.starts_with(existing_root) && !strong {\n                // The new service is a descendant of an existing service, and this is not\n                // a strong root so we consider this to be part of the existing service,\n                // so we're done.\n                return;\n            }\n        }\n\n        // Delete ones marked for deletion.\n        for root in to_delete {\n            self.services.remove(&root);\n        }\n\n        // The new service is not a descendant of any existing service, so add it.\n        self.services.insert(\n            root.clone(),\n            DiscoveredService {\n                name: service_name.to_string(),\n                root,\n            },\n        );\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::path::Path;\n    use std::rc::Rc;\n\n    use swc_common::errors::{Handler, HANDLER};\n    use swc_common::{Globals, SourceMap, GLOBALS};\n    use tempdir::TempDir;\n\n    use crate::parser::parser::{ParseContext, Parser};\n    use crate::parser::resourceparser::PassOneParser;\n    use crate::testutil::testresolve::TestResolver;\n    use crate::testutil::JS_RUNTIME_PATH;\n\n    use super::*;\n\n    fn parse(tmp_dir: &Path, src: &str) -> anyhow::Result<Vec<DiscoveredService>> {\n        let globals = Globals::new();\n        let cm: Rc<SourceMap> = Default::default();\n        let errs = Rc::new(Handler::with_tty_emitter(\n            swc_common::errors::ColorConfig::Auto,\n            true,\n            false,\n            Some(cm.clone()),\n        ));\n\n        GLOBALS.set(&globals, || {\n            HANDLER.set(&errs, || {\n                let ar = txtar::from_str(src);\n                ar.materialize(tmp_dir)?;\n\n                let resolver = Box::new(TestResolver::new(tmp_dir.to_path_buf(), ar.clone()));\n                let pc = ParseContext::with_resolver(\n                    tmp_dir.to_path_buf(),\n                    Some(JS_RUNTIME_PATH.clone()),\n                    resolver,\n                    cm,\n                    errs.clone(),\n                )\n                .unwrap();\n                pc.loader.load_archive(tmp_dir, &ar)?;\n\n                let pass1 = PassOneParser::new(\n                    pc.file_set.clone(),\n                    pc.type_checker.clone(),\n                    Default::default(),\n                );\n                let parser = Parser::new(&pc, pass1);\n                let result = parser.parse();\n                Ok(discover_services(&pc.file_set, &result.binds))\n            })\n        })\n    }\n\n    #[test]\n    fn test_api_endpoints() {\n        let tmp_dir = TempDir::new(\"tsparser-test\").unwrap();\n        let svcs = parse(\n            tmp_dir.path(),\n            r#\"\n-- systemA/svc1/foo.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const foo = api(\n  { method: \"POST\" },\n  async (): Promise<void> => {}\n);\n\n-- systemA/svc2/bar.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const bar = api(\n  { method: \"POST\" },\n  async (): Promise<void> => {}\n);\n\n-- svc3/bar.ts --\nimport { api } from \"encore.dev/api\";\n\nexport const bar = api(\n  { method: \"POST\" },\n  async (): Promise<void> => {}\n);\n\"#,\n        );\n\n        match svcs {\n            Err(err) => {\n                panic!(\"{err:#?}\");\n            }\n            Ok(svcs) => {\n                let tmp_root = tmp_dir.path();\n                assert_eq!(svcs.len(), 3);\n                assert_eq!(svcs[0].name, \"svc1\");\n                assert_eq!(svcs[1].name, \"svc2\");\n                assert_eq!(svcs[2].name, \"svc3\");\n                assert_eq!(svcs[0].root, tmp_root.join(\"systemA/svc1\"));\n                assert_eq!(svcs[1].root, tmp_root.join(\"systemA/svc2\"));\n                assert_eq!(svcs[2].root, tmp_root.join(\"svc3\"));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/ast_id.rs",
    "content": "use std::hash::Hash;\nuse swc_ecma_ast as ast;\n\n/// AstId is a convenience wrapper around ast::Id that also tracks the name of the identifier,\n/// for debugging purposes. It can be swapped out for ast::Id later.\n#[derive(Debug)]\npub struct AstId(ast::Id, String);\n\nimpl AstId {\n    pub fn new(id: ast::Id, name: String) -> Self {\n        Self(id, name)\n    }\n}\n\nimpl std::fmt::Display for AstId {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.1)\n    }\n}\n\nimpl From<ast::Id> for AstId {\n    fn from(id: ast::Id) -> Self {\n        Self(id, \"unknown\".into())\n    }\n}\n\nimpl From<&ast::Ident> for AstId {\n    fn from(ident: &ast::Ident) -> Self {\n        Self(ident.to_id(), ident.sym.as_ref().to_string())\n    }\n}\n\nimpl Hash for AstId {\n    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n        self.0.hash(state);\n    }\n}\n\nimpl PartialEq for AstId {\n    fn eq(&self, other: &Self) -> bool {\n        self.0.eq(&other.0)\n    }\n}\nimpl Eq for AstId {}\n\nimpl PartialOrd for AstId {\n    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {\n        Some(self.cmp(other))\n    }\n}\nimpl Ord for AstId {\n    fn cmp(&self, other: &Self) -> std::cmp::Ordering {\n        self.0.cmp(&other.0)\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/binding.rs",
    "content": "use swc_ecma_ast as ast;\n\npub(super) struct BindingPat {\n    /// The ast id, for id tracking.\n    pub id: ast::Id,\n\n    /// The variable name this is bound to.\n    pub name: String,\n\n    /// The type annotation, if any.\n    pub type_ann: Option<ast::TsTypeAnn>,\n\n    /// The default value if the destructuring expression fails to match.\n    #[allow(dead_code)]\n    pub default: Option<ast::Expr>,\n\n    /// The destructuring expression to evaluate the RHS against\n    /// to arrive at the value.\n    pub destructure_path: Vec<DestructuringExpr>,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub(super) enum DestructuringExpr {\n    /// The whole expression.\n    Full,\n    /// A single entry in an array with the given index.\n    ArrayIndex(usize),\n    /// The remainder of an array; e.g. `...xs`, starting from the given index (inclusive).\n    ArrayRest(usize),\n    /// A single entry in an object with the given key.\n    ObjectKey(DestructuringObjectKey),\n\n    /// The remainder of an object; e.g. `...xs`.\n    ObjectRest {\n        /// The keys that have already been destructured.\n        except: Vec<String>,\n    },\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub(super) enum DestructuringObjectKey {\n    /// A literal key.\n    Ident(String),\n    /// A computed key.\n    Computed(ast::Expr),\n}\n\npub(super) fn bindings(pat: &ast::Pat) -> Vec<BindingPat> {\n    /// Compute sub_bindings for a pattern, stripping the DestructuringExpr::Full\n    /// from the head of the destructure_path. This is used so that\n    /// we don't end up with \"full\" expressions other than for \"let x = ...\".\n    fn sub_bindings(pat: &ast::Pat) -> Vec<BindingPat> {\n        let mut result = bindings(pat);\n        for b in &mut result {\n            if matches!(b.destructure_path.first(), Some(DestructuringExpr::Full)) {\n                b.destructure_path.remove(0);\n            }\n        }\n        result\n    }\n\n    match pat {\n        ast::Pat::Ident(ident) => {\n            // A basic identifier, e.g. \"let x = ...\"\n            vec![BindingPat {\n                id: ident.id.to_id(),\n                name: ident.id.sym.as_ref().to_string(),\n                type_ann: ident.type_ann.as_ref().map(|ann| *ann.clone()),\n                default: None,\n                destructure_path: vec![DestructuringExpr::Full],\n            }]\n        }\n\n        ast::Pat::Array(arr) => {\n            // An array destructuring expression, e.g. \"let [x, y] = ...\"\n            let mut result = Vec::with_capacity(arr.elems.len());\n            for (idx, elem) in arr.elems.iter().enumerate() {\n                // Skip over None elems (e.g. \"let [x, , y] = ...\")\n                let Some(elem) = elem else { continue };\n\n                match elem {\n                    // Handle the rest expression here as we know we're dealing\n                    // with an array destructuring.\n                    ast::Pat::Rest(rest) => {\n                        // A rest expression, e.g. \"...xs\"\n                        let rest_bindings = sub_bindings(&rest.arg);\n                        for mut rb in rest_bindings {\n                            // Add the rest expression.\n                            rb.destructure_path\n                                .insert(0, DestructuringExpr::ArrayRest(idx));\n                            result.push(rb);\n                        }\n                    }\n\n                    _ => {\n                        // For every child binding, add it to the result\n                        // while wrapping it in an array index expression.\n                        let elem_bindings = sub_bindings(elem);\n                        for mut eb in elem_bindings {\n                            eb.destructure_path\n                                .insert(0, DestructuringExpr::ArrayIndex(idx));\n                            result.push(eb);\n                        }\n                    }\n                }\n            }\n\n            result\n        }\n\n        ast::Pat::Object(obj) => {\n            // An object destructuring expression, e.g. \"let {x, y} = ...\"\n            let mut result = Vec::with_capacity(obj.props.len());\n            let mut rest: Option<&ast::RestPat> = None;\n            for prop in obj.props.iter() {\n                match prop {\n                    ast::ObjectPatProp::KeyValue(kv) => {\n                        // E.g. \"let {x: y} = ...\", indicates a nested destructuring\n                        // where x is not actually bound.\n                        let bindings = sub_bindings(&kv.value);\n\n                        // Figure out what the prop key is.\n                        let obj_key: DestructuringObjectKey = match &kv.key {\n                            ast::PropName::Ident(id) => {\n                                DestructuringObjectKey::Ident(id.sym.to_string())\n                            }\n                            ast::PropName::Str(str) => {\n                                DestructuringObjectKey::Ident(str.value.to_string())\n                            }\n                            ast::PropName::Num(num) => {\n                                DestructuringObjectKey::Ident(num.value.to_string())\n                            }\n                            ast::PropName::BigInt(big) => {\n                                DestructuringObjectKey::Ident(big.value.to_string())\n                            }\n                            ast::PropName::Computed(computed) => {\n                                DestructuringObjectKey::Computed(*computed.expr.clone())\n                            }\n                        };\n\n                        for mut b in bindings {\n                            b.destructure_path\n                                .insert(0, DestructuringExpr::ObjectKey(obj_key.clone()));\n                            result.push(b);\n                        }\n                    }\n\n                    ast::ObjectPatProp::Assign(assign) => {\n                        // E.g. \"let {x} = ...\" or \"let { x = default_value } = ...\".\n                        // Indicates a new bind named x, optionally with a default value.\n                        let key = assign.key.sym.to_string();\n                        result.push(BindingPat {\n                            id: assign.key.to_id(),\n                            name: key.clone(),\n                            type_ann: None,\n                            default: assign.value.as_ref().map(|v| *v.clone()),\n                            destructure_path: vec![DestructuringExpr::ObjectKey(\n                                DestructuringObjectKey::Ident(key),\n                            )],\n                        });\n                    }\n\n                    ast::ObjectPatProp::Rest(r) => {\n                        // E.g. \"let {x, ...xs} = ...\", indicates a rest expression.\n                        rest = Some(r);\n                    }\n                }\n            }\n\n            // If we have a rest expression, compute the except list.\n            if let Some(rest) = rest {\n                // Determine the keys that have already been destructured.\n                let mut except = Vec::with_capacity(result.len());\n                for b in &result {\n                    if let Some(DestructuringExpr::ObjectKey(DestructuringObjectKey::Ident(id))) =\n                        b.destructure_path.first()\n                    {\n                        except.push(id.clone());\n                    }\n                }\n\n                let rest_bindings = sub_bindings(&rest.arg);\n                for mut b in rest_bindings {\n                    b.destructure_path.insert(\n                        0,\n                        DestructuringExpr::ObjectRest {\n                            except: except.clone(),\n                        },\n                    );\n                    result.push(b);\n                }\n            }\n\n            result\n        }\n\n        ast::Pat::Assign(_assign) => {\n            // TODO what does this even mean?\n            todo!(\"assign pattern\")\n        }\n\n        ast::Pat::Rest(_) => {\n            // This shouldn't happen here as we handle it in the array and object cases directly.\n            vec![]\n        }\n        ast::Pat::Invalid(_) | ast::Pat::Expr(_) => {\n            // These shouldn't happen; ignore them.\n            vec![]\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::parser::types::binding::{DestructuringExpr, DestructuringObjectKey};\n    use crate::testutil::testparse::test_parse;\n\n    #[test]\n    fn test_bindings() {\n        let tests = vec![\n            (\"x\", vec![(\"x\", vec![DestructuringExpr::Full])]),\n            (\n                \"{x}\",\n                vec![(\n                    \"x\",\n                    vec![DestructuringExpr::ObjectKey(DestructuringObjectKey::Ident(\n                        \"x\".into(),\n                    ))],\n                )],\n            ),\n            (\n                \"{x: y}\",\n                vec![(\n                    \"y\",\n                    vec![DestructuringExpr::ObjectKey(DestructuringObjectKey::Ident(\n                        \"x\".into(),\n                    ))],\n                )],\n            ),\n            (\n                \"{a, ...rest, b: c}\",\n                vec![\n                    (\n                        \"a\",\n                        vec![DestructuringExpr::ObjectKey(DestructuringObjectKey::Ident(\n                            \"a\".into(),\n                        ))],\n                    ),\n                    (\n                        \"c\",\n                        vec![DestructuringExpr::ObjectKey(DestructuringObjectKey::Ident(\n                            \"b\".into(),\n                        ))],\n                    ),\n                    (\n                        \"rest\",\n                        vec![DestructuringExpr::ObjectRest {\n                            except: vec![\"a\".into(), \"b\".into()],\n                        }],\n                    ),\n                ],\n            ),\n            (\n                \"[, a, , b, ...rest]\",\n                vec![\n                    (\"a\", vec![DestructuringExpr::ArrayIndex(1)]),\n                    (\"b\", vec![DestructuringExpr::ArrayIndex(3)]),\n                    (\"rest\", vec![DestructuringExpr::ArrayRest(4)]),\n                ],\n            ),\n        ];\n\n        for (expr, want) in tests {\n            let stmt = format!(\"let {expr} = 1;\");\n            let module = test_parse(&stmt);\n            let var = module.ast.body[0]\n                .as_stmt()\n                .unwrap()\n                .as_decl()\n                .unwrap()\n                .as_var()\n                .unwrap();\n            let got = super::bindings(&var.decls[0].name);\n            assert_eq!(got.len(), want.len());\n            for (got, want) in got.iter().zip(want.iter()) {\n                assert_eq!(got.name, want.0, \"expr: {expr}\");\n                assert_eq!(got.destructure_path, want.1, \"expr: {expr}\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/mod.rs",
    "content": "mod ast_id;\nmod binding;\nmod object;\nmod typ;\nmod type_resolve;\nmod type_string;\nmod utils;\npub mod visitor;\n\nmod resolved;\n#[cfg(test)]\nmod tests;\npub mod validation;\n\npub use object::{Object, ObjectId, ObjectKind, ResolveState};\npub use typ::*;\npub use type_resolve::TypeChecker;\npub use utils::*;\n"
  },
  {
    "path": "tsparser/src/parser/types/object.rs",
    "content": "use std::cell::{Cell, OnceCell, RefCell};\nuse std::collections::HashMap;\nuse std::fmt::Debug;\nuse std::hash::Hash;\nuse std::rc::Rc;\n\nuse anyhow::Result;\nuse serde::Serialize;\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\nuse swc_common::Spanned;\nuse swc_ecma_ast as ast;\n\nuse crate::parser::module_loader::ModuleId;\nuse crate::parser::types::ast_id::AstId;\nuse crate::parser::types::binding::bindings;\nuse crate::parser::types::typ;\nuse crate::parser::{module_loader, Range};\n\n#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]\npub struct ObjectId(pub(super) usize);\n\n/// An Object describes a named language entity such as a module, constant, type, variable, function, etc.\npub struct Object {\n    pub id: ObjectId,\n    pub range: Range,\n    pub name: Option<String>,\n    pub kind: ObjectKind,\n    pub module_id: ModuleId,\n    pub(super) state: RefCell<CheckState>,\n}\n\nimpl Serialize for Object {\n    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {\n        serializer.serialize_str(self.name.as_ref().unwrap_or(&\"\".to_string()))\n    }\n}\n\nimpl Debug for Object {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"Object\")\n            // .field(\"id\", &self.id)\n            // .field(\"range\", &self.range)\n            .field(\"name\", &self.name)\n            // .field(\"kind\", &self.kind)\n            // .field(\"module_id\", &self.module_id)\n            .finish()\n    }\n}\n\nimpl Hash for Object {\n    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {\n        self.id.hash(state);\n    }\n}\n\nimpl PartialEq for Object {\n    fn eq(&self, other: &Self) -> bool {\n        self.id == other.id\n    }\n}\n\nimpl Eq for Object {}\n\n#[derive(Debug)]\npub enum ObjectKind {\n    TypeName(TypeName),\n    Enum(Enum),\n    Var(Var),\n    Using(Using),\n    Func(Func),\n    Class(Class),\n    Module(Rc<Module>),\n    Namespace(Namespace),\n}\n\nimpl ObjectKind {\n    pub fn type_params<'a>(&'a self) -> Box<dyn Iterator<Item = &'a ast::TsTypeParam> + 'a> {\n        match self {\n            ObjectKind::TypeName(TypeName { decl }) => match decl {\n                TypeNameDecl::Interface(i) => {\n                    Box::new(i.type_params.iter().flat_map(|p| p.params.iter()))\n                }\n                TypeNameDecl::TypeAlias(t) => {\n                    Box::new(t.type_params.iter().flat_map(|p| p.params.iter()))\n                }\n            },\n            _ => Box::new([].iter()),\n        }\n    }\n}\n\n#[derive(Debug)]\npub(super) enum CheckState {\n    NotStarted,\n    InProgress,\n    Completed(typ::Type),\n}\n\n#[derive(Debug)]\npub struct TypeName {\n    pub decl: TypeNameDecl,\n}\n\n#[derive(Debug)]\npub enum TypeNameDecl {\n    Interface(ast::TsInterfaceDecl),\n    TypeAlias(ast::TsTypeAliasDecl),\n}\n\n#[derive(Debug)]\npub struct Class {\n    #[allow(dead_code)]\n    pub spec: Box<ast::Class>,\n}\n\n#[derive(Debug)]\npub struct Enum {\n    pub members: Vec<ast::TsEnumMember>,\n}\n\n#[derive(Debug)]\npub struct Func {\n    #[allow(dead_code)]\n    pub spec: Box<ast::Function>,\n}\n\n#[derive(Debug)]\npub struct Namespace {\n    pub data: Box<NSData>,\n}\n\n#[derive(Debug)]\npub struct Var {\n    pub type_ann: Option<ast::TsTypeAnn>,\n    pub expr: Option<Box<ast::Expr>>,\n}\n\n#[derive(Debug)]\npub struct Using {\n    pub type_ann: Option<ast::TsTypeAnn>,\n    pub expr: Option<Box<ast::Expr>>,\n}\n\n#[derive(Debug)]\npub struct NamedReexport {\n    pub orig_name: String,\n    pub renamed: Option<String>,\n}\n\n#[derive(Debug)]\npub enum Reexport {\n    List {\n        items: Vec<NamedReexport>,\n        import_path: String,\n    },\n    All {\n        import_path: String,\n    },\n}\n\n#[derive(Debug)]\npub struct NSData {\n    /// The objects imported by the module.\n    pub imports: HashMap<AstId, ImportedName>,\n\n    /// Top-level objects, keyed by their id.\n    pub top_level: HashMap<AstId, Rc<Object>>,\n\n    /// The named exports.\n    pub named_exports: HashMap<String, Rc<Object>>,\n\n    /// The default export, if any.\n    pub default_export: Option<Rc<Object>>,\n\n    // Reexports from other modules.\n    pub reexports: Vec<Reexport>,\n\n    /// Export items that haven't yet been processed.\n    #[allow(dead_code)]\n    pub unprocessed_exports: Vec<ast::ModuleItem>,\n}\n\n#[derive(Debug)]\npub struct Module {\n    pub base: Lrc<module_loader::Module>,\n    pub data: Box<NSData>,\n}\n\n#[derive(Debug, Clone)]\npub struct ImportedName {\n    pub range: Range,\n    pub import_path: String,\n    pub kind: ImportKind,\n}\n\n#[derive(Debug, Clone)]\npub enum ImportKind {\n    Named(String),\n    Default,\n    Namespace,\n}\n\nimpl NSData {\n    fn new() -> Self {\n        Self {\n            imports: HashMap::new(),\n            top_level: HashMap::new(),\n            named_exports: HashMap::new(),\n            default_export: None,\n            reexports: vec![],\n            unprocessed_exports: vec![],\n        }\n    }\n\n    pub fn get_named_export(\n        &self,\n        ctx: &ResolveState,\n        curr_module: &swc_common::FileName,\n        needle: &str,\n    ) -> Option<Rc<Object>> {\n        if needle == \"default\" {\n            if let Some(default) = &self.default_export {\n                return Some(default.clone());\n            }\n        }\n        if let Some(obj) = self.named_exports.get(needle) {\n            return Some(obj.clone());\n        }\n\n        for re in &self.reexports {\n            match re {\n                Reexport::List { import_path, items } => {\n                    for item in items {\n                        let export_name = item.renamed.as_ref().unwrap_or(&item.orig_name);\n                        if export_name == needle {\n                            let module = ctx.resolve_module_import(curr_module, import_path)?;\n                            return module.data.get_named_export(\n                                ctx,\n                                &module.base.swc_file_path,\n                                &item.orig_name,\n                            );\n                        }\n                    }\n                }\n\n                Reexport::All { import_path } => {\n                    if let Some(module) = ctx.resolve_module_import(curr_module, import_path) {\n                        if let Some(export) =\n                            module\n                                .data\n                                .get_named_export(ctx, &module.base.swc_file_path, needle)\n                        {\n                            return Some(export);\n                        }\n                    }\n                }\n            }\n        }\n        None\n    }\n\n    fn add_top_level(&mut self, id: AstId, obj: Rc<Object>) -> Rc<Object> {\n        if let Some(other) = self.top_level.get(&id) {\n            // Unhandled overload most likely, return the existing object for now.\n            return other.clone();\n        }\n\n        self.top_level.insert(id, obj.clone());\n        obj\n    }\n\n    fn add_import(&mut self, id: AstId, import: ImportedName) {\n        if self.imports.contains_key(&id) {\n            HANDLER.with(|handler| {\n                handler.span_err(import.range.to_span(), &format!(\"`{id}` already imported\"));\n            });\n            return;\n        }\n\n        self.imports.insert(id, import);\n    }\n}\n\nfn process_module_items(ctx: &ResolveState, ns: &mut NSData, items: &[ast::ModuleItem]) {\n    for it in items {\n        match it {\n            ast::ModuleItem::ModuleDecl(md) => match md {\n                ast::ModuleDecl::Import(import) => process_import(ns, import),\n                ast::ModuleDecl::ExportDecl(decl) => {\n                    let objs = process_decl(ctx, ns, &decl.decl);\n                    for obj in objs {\n                        if let Some(name) = &obj.name {\n                            ns.named_exports.insert(name.clone(), obj);\n                        }\n                    }\n                }\n\n                ast::ModuleDecl::ExportDefaultDecl(decl) => {\n                    let obj = process_default_decl(ctx, &decl.decl);\n                    if ns.default_export.is_some() {\n                        obj.range.err(\"duplicate default export\");\n                    }\n                    ns.default_export = Some(obj);\n                }\n\n                // TODO(andre) Can this affect the module namespace?\n                ast::ModuleDecl::ExportDefaultExpr(_expr) => {\n                    // TODO this is e.g `export default new SQLDatabase`\n                    // need to resolve to object\n                    log::debug!(\"TODO export default expr\");\n                }\n\n                ast::ModuleDecl::ExportNamed(decl) => {\n                    if let Some(src) = &decl.src {\n                        // Re-exporting from another module.\n                        ns.reexports.push(Reexport::List {\n                            import_path: src.value.to_string(),\n                            items: decl\n                                .specifiers\n                                .iter()\n                                .filter_map(|spec| match spec {\n                                    ast::ExportSpecifier::Named(named) => {\n                                        let orig_name = module_export_name_to_string(&named.orig);\n                                        Some(NamedReexport {\n                                            orig_name,\n                                            renamed: named\n                                                .exported\n                                                .as_ref()\n                                                .map(module_export_name_to_string),\n                                        })\n                                    }\n                                    ast::ExportSpecifier::Default(_) => {\n                                        log::debug!(\"TODO: ExportNamed with default\");\n                                        None\n                                    }\n                                    ast::ExportSpecifier::Namespace(_) => {\n                                        log::debug!(\"TODO: ExportNamed with namespace\");\n                                        None\n                                    }\n                                })\n                                .collect(),\n                        });\n                    } else {\n                        // Exporting from the same module (no src).\n                        // We need to defer this until after processing all declarations.\n                        ns.unprocessed_exports.push(it.clone());\n                    }\n                }\n\n                ast::ModuleDecl::ExportAll(decl) => {\n                    // Re-exporting * from another module.\n                    ns.reexports.push(Reexport::All {\n                        import_path: decl.src.value.to_string(),\n                    });\n                }\n\n                ast::ModuleDecl::TsImportEquals(_) => {\n                    log::debug!(\"TODO ts import equals\");\n                }\n\n                ast::ModuleDecl::TsExportAssignment(decl) => {\n                    log::debug!(\"TsExportAssignment {:#?}\", decl);\n                }\n\n                ast::ModuleDecl::TsNamespaceExport(decl) => {\n                    log::debug!(\"TsNamespaceExport {:#?}\", decl);\n                }\n            },\n\n            ast::ModuleItem::Stmt(stmt) => {\n                process_stmt(ctx, ns, stmt);\n            }\n        }\n    }\n\n    // Process deferred exports (exports from the same module).\n    process_local_exports(ns);\n}\n\n/// Process exports from the same module (export { foo, bar as baz }).\nfn process_local_exports(ns: &mut NSData) {\n    let unprocessed = std::mem::take(&mut ns.unprocessed_exports);\n\n    for item in unprocessed {\n        if let ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportNamed(decl)) = item {\n            for spec in &decl.specifiers {\n                match spec {\n                    ast::ExportSpecifier::Named(named) => {\n                        let orig_name = module_export_name_to_string(&named.orig);\n                        let export_name = named\n                            .exported\n                            .as_ref()\n                            .map(module_export_name_to_string)\n                            .unwrap_or_else(|| orig_name.clone());\n\n                        // Look up the object in top_level by name.\n                        if let Some(obj) = ns.top_level.values().find(|obj| {\n                            obj.name.as_ref().is_some_and(|n| n == &orig_name)\n                        }) {\n                            ns.named_exports.insert(export_name, obj.clone());\n                        } else if let Some(import) = ns.imports.values().find(|imp| {\n                            matches!(&imp.kind, ImportKind::Named(name) if name == &orig_name)\n                        }) {\n                            // The export refers to an import - we need to add it as a reexport.\n                            ns.reexports.push(Reexport::List {\n                                import_path: import.import_path.clone(),\n                                items: vec![NamedReexport {\n                                    orig_name: orig_name.clone(),\n                                    renamed: if export_name != orig_name {\n                                        Some(export_name)\n                                    } else {\n                                        None\n                                    },\n                                }],\n                            });\n                        } else {\n                            log::debug!(\"Export '{}' not found in module\", orig_name);\n                        }\n                    }\n                    ast::ExportSpecifier::Default(_) => {\n                        log::debug!(\"TODO: local export with default\");\n                    }\n                    ast::ExportSpecifier::Namespace(_) => {\n                        log::debug!(\"TODO: local export with namespace\");\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Process an import declaration, adding imports to the module.\nfn process_import(ns: &mut NSData, import: &ast::ImportDecl) {\n    for specifier in &import.specifiers {\n        match specifier {\n            ast::ImportSpecifier::Named(named) => {\n                let export_name = named.imported.as_ref().map_or_else(\n                    || named.local.clone(),\n                    |export_name| match export_name {\n                        ast::ModuleExportName::Ident(id) => id.clone(),\n                        ast::ModuleExportName::Str(str) => {\n                            ast::Ident::new(str.value.clone(), str.span)\n                        }\n                    },\n                );\n\n                ns.add_import(\n                    AstId::from(&named.local),\n                    ImportedName {\n                        range: named.span.into(),\n                        import_path: import.src.value.to_string(),\n                        kind: ImportKind::Named(export_name.sym.as_ref().to_string()),\n                    },\n                );\n            }\n            ast::ImportSpecifier::Default(default) => {\n                ns.add_import(\n                    AstId::from(&default.local),\n                    ImportedName {\n                        range: default.span.into(),\n                        import_path: import.src.value.to_string(),\n                        kind: ImportKind::Default,\n                    },\n                );\n            }\n            ast::ImportSpecifier::Namespace(ns_import) => {\n                // import * as foo\n                ns.add_import(\n                    AstId::from(&ns_import.local),\n                    ImportedName {\n                        range: ns_import.span.into(),\n                        import_path: import.src.value.to_string(),\n                        kind: ImportKind::Namespace,\n                    },\n                );\n            }\n        }\n    }\n}\n\nfn process_stmt(ctx: &ResolveState, ns: &mut NSData, stmt: &ast::Stmt) -> Vec<Rc<Object>> {\n    match stmt {\n        ast::Stmt::Decl(decl) => process_decl(ctx, ns, decl),\n        ast::Stmt::Block(block) => {\n            let mut objs = vec![];\n            for stmt in &block.stmts {\n                objs.extend(process_stmt(ctx, ns, stmt));\n            }\n            objs\n        }\n\n        // NOTE(andre): I believe other statements can't really declare things,\n        // since they're inside blocks.\n        _ => vec![],\n    }\n}\n\nfn process_decl(ctx: &ResolveState, ns: &mut NSData, decl: &ast::Decl) -> Vec<Rc<Object>> {\n    let range: Range = decl.span().into();\n    match decl {\n        ast::Decl::Class(d) => {\n            let name = Some(d.ident.sym.to_string());\n            let obj = ctx.new_obj(\n                name,\n                range,\n                ObjectKind::Class(Class {\n                    spec: d.class.clone(),\n                }),\n            );\n            ns.add_top_level(AstId::from(&d.ident), obj.clone());\n            vec![obj]\n        }\n\n        ast::Decl::Fn(d) => {\n            let name = Some(d.ident.sym.to_string());\n            let obj = ctx.new_obj(\n                name,\n                range,\n                ObjectKind::Func(Func {\n                    spec: d.function.clone(),\n                }),\n            );\n            ns.add_top_level(AstId::from(&d.ident), obj.clone());\n            vec![obj]\n        }\n\n        ast::Decl::Var(d) => {\n            let mut objs = vec![];\n            for var_decl in &d.decls {\n                for b in bindings(&var_decl.name) {\n                    let name = Some(b.name.to_string());\n                    let range = var_decl.span.into();\n                    let obj = ctx.new_obj(\n                        name,\n                        range,\n                        ObjectKind::Var(Var {\n                            type_ann: b.type_ann,\n                            expr: var_decl.init.clone(),\n                        }),\n                    );\n                    ns.add_top_level(AstId::new(b.id, b.name.clone()), obj.clone());\n                    objs.push(obj);\n                }\n            }\n            objs\n        }\n\n        ast::Decl::Using(d) => {\n            let mut objs = vec![];\n            for var_decl in &d.decls {\n                for b in bindings(&var_decl.name) {\n                    let name = Some(b.name.to_string());\n                    let range = var_decl.span.into();\n                    let obj = ctx.new_obj(\n                        name,\n                        range,\n                        ObjectKind::Using(Using {\n                            type_ann: b.type_ann,\n                            expr: var_decl.init.clone(),\n                        }),\n                    );\n                    ns.add_top_level(AstId::new(b.id, b.name.clone()), obj.clone());\n                    objs.push(obj);\n                }\n            }\n            objs\n        }\n\n        ast::Decl::TsInterface(d) => {\n            let name = Some(d.id.sym.to_string());\n            let obj = ctx.new_obj(\n                name,\n                range,\n                ObjectKind::TypeName(TypeName {\n                    decl: TypeNameDecl::Interface(*d.clone()),\n                }),\n            );\n            ns.add_top_level(AstId::from(&d.id), obj.clone());\n            vec![obj]\n        }\n\n        ast::Decl::TsTypeAlias(d) => {\n            let name = d.id.sym.to_string();\n            let obj = ctx.new_obj(\n                Some(name),\n                range,\n                ObjectKind::TypeName(TypeName {\n                    decl: TypeNameDecl::TypeAlias(*d.clone()),\n                }),\n            );\n            ns.add_top_level(AstId::from(&d.id), obj.clone());\n            vec![obj]\n        }\n\n        ast::Decl::TsEnum(d) => {\n            let name = Some(d.id.sym.to_string());\n            let obj = ctx.new_obj(\n                name,\n                range,\n                ObjectKind::Enum(Enum {\n                    members: d.members.clone(),\n                }),\n            );\n            ns.add_top_level(AstId::from(&d.id), obj.clone());\n            vec![obj]\n        }\n\n        ast::Decl::TsModule(d) => {\n            // Namespace declaration\n            match &d.id {\n                ast::TsModuleName::Ident(id) => {\n                    let mut ns2 = Namespace {\n                        data: Box::new(NSData::new()),\n                    };\n                    if let Some(body) = &d.body {\n                        process_namespace_body(ctx, &mut ns2.data, body);\n                    }\n\n                    let name = Some(id.sym.to_string());\n                    let obj = ctx.new_obj(name, range, ObjectKind::Namespace(ns2));\n                    ns.add_top_level(AstId::from(id), obj.clone());\n                    vec![obj]\n                }\n                ast::TsModuleName::Str(_) => {\n                    // This is not valid for namespace declarations, ignore it.\n                    vec![]\n                }\n            }\n        }\n    }\n}\n\nfn process_default_decl(ctx: &ResolveState, decl: &ast::DefaultDecl) -> Rc<Object> {\n    let range: Range = decl.span().into();\n    match decl {\n        ast::DefaultDecl::Class(d) => {\n            let name = d.ident.as_ref().map(|id| id.sym.to_string());\n            ctx.new_obj(\n                name,\n                range,\n                ObjectKind::Class(Class {\n                    spec: d.class.clone(),\n                }),\n            )\n        }\n\n        ast::DefaultDecl::Fn(d) => {\n            let name = d.ident.as_ref().map(|id| id.sym.to_string());\n            ctx.new_obj(\n                name,\n                range,\n                ObjectKind::Func(Func {\n                    spec: d.function.clone(),\n                }),\n            )\n        }\n\n        ast::DefaultDecl::TsInterfaceDecl(d) => {\n            let name = Some(d.id.sym.to_string());\n            ctx.new_obj(\n                name,\n                range,\n                ObjectKind::TypeName(TypeName {\n                    decl: TypeNameDecl::Interface(*d.clone()),\n                }),\n            )\n        }\n    }\n}\n\nfn process_namespace_body(ctx: &ResolveState, ns: &mut NSData, body: &ast::TsNamespaceBody) {\n    match body {\n        ast::TsNamespaceBody::TsModuleBlock(block) => {\n            process_module_items(ctx, ns, &block.body[..]);\n        }\n        ast::TsNamespaceBody::TsNamespaceDecl(decl) => {\n            let name = Some(decl.id.sym.to_string());\n            let mut ns2 = Namespace {\n                data: Box::new(NSData::new()),\n            };\n            process_namespace_body(ctx, &mut ns2.data, &decl.body);\n\n            let range = decl.span.into();\n            let obj = ctx.new_obj(name, range, ObjectKind::Namespace(ns2));\n            ns.add_top_level(AstId::from(&decl.id), obj);\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct ResolveState {\n    loader: Lrc<module_loader::ModuleLoader>,\n    module_objects: RefCell<HashMap<ModuleId, Rc<Object>>>,\n    module_stack: RefCell<Vec<ModuleId>>,\n    universe: OnceCell<Rc<Module>>,\n    next_id: Cell<usize>,\n}\n\nimpl ResolveState {\n    pub(super) fn new(loader: Lrc<module_loader::ModuleLoader>) -> Self {\n        Self {\n            loader,\n            module_objects: RefCell::new(HashMap::new()),\n            module_stack: RefCell::new(vec![]),\n            universe: OnceCell::new(),\n            next_id: Cell::new(1),\n        }\n    }\n\n    pub(super) fn universe(&self) -> Rc<Module> {\n        if let Some(universe) = self.universe.get() {\n            return universe.to_owned();\n        }\n\n        let ast = self.loader.universe();\n        let module = self.get_or_init_module(ast);\n        self.universe.set(module.clone()).unwrap();\n        self.universe.get().unwrap().to_owned()\n    }\n\n    pub(super) fn new_obj(\n        &self,\n        name: Option<String>,\n        range: Range,\n        kind: ObjectKind,\n    ) -> Rc<Object> {\n        let obj_id = self.next_id.get();\n        self.next_id.set(obj_id + 1);\n\n        let module_id = self.module_id().expect(\"no current module\");\n        Rc::new(Object {\n            id: ObjectId(obj_id),\n            range,\n            module_id,\n            name,\n            kind,\n            state: RefCell::new(CheckState::NotStarted),\n        })\n    }\n\n    pub fn lookup_module(&self, id: ModuleId) -> Option<Rc<Module>> {\n        self.module_objects\n            .borrow()\n            .get(&id)\n            .and_then(|obj| match &obj.kind {\n                ObjectKind::Module(module) => Some(module.clone()),\n                _ => None,\n            })\n    }\n\n    pub fn is_universe(&self, id: ModuleId) -> bool {\n        let universe = self.universe();\n        universe.base.id == id\n    }\n\n    pub fn is_module_path(&self, id: ModuleId, name: &str) -> bool {\n        if let Some(module) = self.lookup_module(id) {\n            module.base.module_path.as_ref().is_some_and(|p| p == name)\n        } else {\n            false\n        }\n    }\n\n    pub fn get_or_init_module(&self, module: Lrc<module_loader::Module>) -> Rc<Module> {\n        let module_id = module.id;\n        if let Some(m) = self.lookup_module(module_id) {\n            return m;\n        }\n\n        let mut data = Box::new(NSData::new());\n        self.with_curr_module(module_id, || {\n            process_module_items(self, &mut data, &module.ast.body[..])\n        });\n\n        let new_module = Rc::new(Module { base: module, data });\n\n        self.module_objects.borrow_mut().insert(\n            module_id,\n            self.with_curr_module(module_id, || {\n                self.new_obj(\n                    None,\n                    new_module.base.ast.span.into(),\n                    ObjectKind::Module(new_module.clone()),\n                )\n            }),\n        );\n\n        new_module\n    }\n\n    fn with_curr_module<Fn, Res>(&self, module_id: ModuleId, f: Fn) -> Res\n    where\n        Fn: FnOnce() -> Res,\n    {\n        self.module_stack.borrow_mut().push(module_id);\n        let result = f();\n        self.module_stack.borrow_mut().pop();\n        result\n    }\n\n    fn module_id(&self) -> Result<ModuleId> {\n        let stack = self.module_stack.borrow();\n        let module = stack\n            .last()\n            .ok_or_else(|| anyhow::anyhow!(\"internal error: no module on stack\"))?;\n        Ok(module.to_owned())\n    }\n\n    pub(super) fn resolve_module_default_export(&self, module_id: ModuleId) -> Option<Rc<Object>> {\n        self.lookup_module(module_id)?.data.default_export.clone()\n    }\n\n    pub(super) fn resolve_module_ident(\n        &self,\n        module_id: ModuleId,\n        ident: &ast::Ident,\n    ) -> Option<Rc<Object>> {\n        let module = self.lookup_module(module_id)?;\n\n        // Is it a top-level object in this module?\n        let ast_id = AstId::from(ident);\n        if let Some(obj) = module.data.top_level.get(&ast_id) {\n            return Some(obj.clone());\n        }\n\n        // Otherwise, is it an import?\n        if let Some(imp_name) = module.data.imports.get(&ast_id) {\n            return self.resolve_import(&module, imp_name);\n        }\n\n        // Is it in universe scope?\n        {\n            let universe = self.universe();\n            let name = ident.sym.as_ref();\n            if let Some(obj) = universe.data.named_exports.get(name) {\n                return Some(obj.clone());\n            }\n        }\n\n        // Otherwise we don't know about this object.\n        None\n    }\n\n    pub(super) fn resolve_module_import(\n        &self,\n        from_file: &swc_common::FileName,\n        import_path: &str,\n    ) -> Option<Rc<Module>> {\n        let ast_module = match self.loader.resolve_import(from_file, import_path) {\n            Ok(Some(ast_module)) => ast_module,\n            Ok(None) | Err(_) => return None,\n        };\n        Some(self.get_or_init_module(ast_module))\n    }\n\n    pub(super) fn resolve_import(&self, module: &Module, imp: &ImportedName) -> Option<Rc<Object>> {\n        let ast_module = match self\n            .loader\n            .resolve_import(&module.base.swc_file_path, &imp.import_path)\n        {\n            Ok(None) => return None,\n            Ok(Some(ast_module)) => Ok(ast_module),\n            Err(err) => Err(err),\n        };\n\n        let ast_module = ast_module\n            .inspect_err(|err| {\n                HANDLER.with(|handler| {\n                    handler.span_err(imp.range.to_span(), &format!(\"import not found: {err}\"))\n                })\n            })\n            .ok()?;\n\n        match &imp.kind {\n            ImportKind::Named(name) => {\n                let imported = self.get_or_init_module(ast_module);\n                let obj = imported\n                    .data\n                    .get_named_export(self, &imported.base.swc_file_path, name);\n\n                if obj.is_none() {\n                    HANDLER.with(|handler| {\n                        handler.span_err(imp.range.to_span(), &format!(\"object not found: {name}\"));\n                    });\n                }\n\n                obj\n            }\n            ImportKind::Default => {\n                let imported = self.get_or_init_module(ast_module);\n                let obj =\n                    imported\n                        .data\n                        .get_named_export(self, &imported.base.swc_file_path, \"default\");\n\n                if obj.is_none() {\n                    HANDLER.with(|handler| {\n                        handler.span_err(imp.range.to_span(), \"default export not found\");\n                    });\n                }\n\n                obj\n            }\n            ImportKind::Namespace => {\n                let imported = self.get_or_init_module(ast_module);\n                let obj = self.module_objects.borrow().get(&imported.base.id).cloned();\n\n                if obj.is_none() {\n                    HANDLER.with(|handler| {\n                        handler.span_err(\n                            imp.range.to_span(),\n                            \"object for namespaced import not found\",\n                        );\n                    });\n                }\n\n                obj\n            }\n        }\n    }\n}\n\nfn module_export_name_to_string(name: &ast::ModuleExportName) -> String {\n    match name {\n        ast::ModuleExportName::Ident(i) => i.sym.to_string(),\n        ast::ModuleExportName::Str(str) => str.value.as_str().to_string(),\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/resolved.rs",
    "content": "use std::borrow::{Borrow, Cow};\nuse std::cmp::Ordering;\nuse std::fmt;\nuse std::hash::{Hash, Hasher};\nuse std::ops::Deref;\n\nuse Resolved::*;\n\npub enum Resolved<'a, B: ?Sized + 'a>\nwhere\n    B: ToOwned,\n{\n    New(<B as ToOwned>::Owned),\n    Changed(&'a B),\n    Same(&'a B),\n}\n\nimpl<'a, B: ?Sized + 'a> From<Resolved<'a, B>> for Cow<'a, B>\nwhere\n    B: ToOwned,\n{\n    fn from(val: Resolved<'a, B>) -> Self {\n        match val {\n            New(owned) => Cow::Owned(owned),\n            Changed(borrowed) | Same(borrowed) => Cow::Borrowed(borrowed),\n        }\n    }\n}\n\nimpl<B: ?Sized + ToOwned> Clone for Resolved<'_, B> {\n    fn clone(&self) -> Self {\n        match *self {\n            New(ref o) => {\n                let b: &B = o.borrow();\n                New(b.to_owned())\n            }\n            Changed(b) => Changed(b),\n            Same(b) => Same(b),\n        }\n    }\n\n    fn clone_from(&mut self, source: &Self) {\n        match (self, source) {\n            (&mut New(ref mut dest), New(o)) => o.borrow().clone_into(dest),\n            (t, s) => t.clone_from(s),\n        }\n    }\n}\n\nimpl<B: ?Sized + ToOwned> Resolved<'_, B> {\n    /// Converts `Same` to `Changed`.\n    pub fn same_to_changed(self) -> Self {\n        match self {\n            Same(borrowed) => Changed(borrowed),\n            _ => self,\n        }\n    }\n    ///\n    /// Converts `Same` to `Changed`.\n    pub fn into_new(self) -> Resolved<'static, B> {\n        match self {\n            Same(borrowed) | Changed(borrowed) => New(borrowed.to_owned()),\n            New(typ) => New(typ),\n        }\n    }\n\n    /// Extracts the owned data.\n    ///\n    /// Clones the data if it is not already owned.\n    pub fn into_owned(self) -> <B as ToOwned>::Owned {\n        match self {\n            Same(borrowed) | Changed(borrowed) => borrowed.to_owned(),\n            New(owned) => owned,\n        }\n    }\n}\n\nimpl<B: ?Sized + ToOwned> Deref for Resolved<'_, B>\nwhere\n    B::Owned: Borrow<B>,\n{\n    type Target = B;\n\n    fn deref(&self) -> &B {\n        match *self {\n            Changed(borrowed) | Same(borrowed) => borrowed,\n            New(ref owned) => owned.borrow(),\n        }\n    }\n}\n\nimpl<B: ?Sized> Eq for Resolved<'_, B> where B: Eq + ToOwned {}\n\nimpl<B: ?Sized> Ord for Resolved<'_, B>\nwhere\n    B: Ord + ToOwned,\n{\n    #[inline]\n    fn cmp(&self, other: &Self) -> Ordering {\n        Ord::cmp(&**self, &**other)\n    }\n}\n\nimpl<'b, B: ?Sized, C: ?Sized> PartialEq<Resolved<'b, C>> for Resolved<'_, B>\nwhere\n    B: PartialEq<C> + ToOwned,\n    C: ToOwned,\n{\n    #[inline]\n    fn eq(&self, other: &Resolved<'b, C>) -> bool {\n        PartialEq::eq(&**self, &**other)\n    }\n}\n\nimpl<'a, B: ?Sized> PartialOrd for Resolved<'a, B>\nwhere\n    B: PartialOrd + ToOwned,\n{\n    #[inline]\n    fn partial_cmp(&self, other: &Resolved<'a, B>) -> Option<Ordering> {\n        PartialOrd::partial_cmp(&**self, &**other)\n    }\n}\n\nimpl<B: ?Sized, D> fmt::Debug for Resolved<'_, B>\nwhere\n    D: fmt::Debug,\n    B: fmt::Debug + ToOwned<Owned = D>,\n{\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match *self {\n            Changed(ref b) => write!(f, \"Changed({b:?})\"),\n            Same(ref b) => write!(f, \"Same({b:?})\"),\n            New(ref o) => write!(f, \"New({o:?})\"),\n        }\n    }\n}\n\nimpl<B: ?Sized, D> fmt::Display for Resolved<'_, B>\nwhere\n    D: fmt::Display,\n    B: fmt::Display + ToOwned<Owned = D>,\n{\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match *self {\n            Changed(ref b) | Same(ref b) => fmt::Display::fmt(b, f),\n            New(ref o) => fmt::Display::fmt(o, f),\n        }\n    }\n}\n\nimpl<B: ?Sized, D> Default for Resolved<'_, B>\nwhere\n    D: Default,\n    B: ToOwned<Owned = D>,\n{\n    /// Creates an owned Resolved<'a, B> with the default value for the contained owned value.\n    fn default() -> Self {\n        New(<B as ToOwned>::Owned::default())\n    }\n}\n\nimpl<B: ?Sized> Hash for Resolved<'_, B>\nwhere\n    B: Hash + ToOwned,\n{\n    #[inline]\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        Hash::hash(&**self, state)\n    }\n}\n\nimpl<T: ?Sized + ToOwned> AsRef<T> for Resolved<'_, T> {\n    fn as_ref(&self) -> &T {\n        self\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@basic.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/basic.ts\n---\n{\n    \"Interface\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Exclude1\": Union(\n        Union {\n            types: [\n                Literal(\n                    String(\n                        \"bar\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"optional\",\n                    ),\n                ),\n            ],\n        },\n    ),\n    \"Pick1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Pick2\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Omit1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Omit2\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Partial1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Optional\": Generic(\n        Intersection(\n            Intersection {\n                x: Generic(\n                    Mapped(\n                        Mapped {\n                            in_type: Named(\n                                Named {\n                                    obj: Object {\n                                        name: Some(\n                                            \"Exclude\",\n                                        ),\n                                    },\n                                    type_arguments: [\n                                        Generic(\n                                            Keyof(\n                                                Keyof(\n                                                    Generic(\n                                                        TypeParam(\n                                                            TypeParam {\n                                                                idx: 0,\n                                                                constraint: None,\n                                                            },\n                                                        ),\n                                                    ),\n                                                ),\n                                            ),\n                                        ),\n                                        Generic(\n                                            TypeParam(\n                                                TypeParam {\n                                                    idx: 1,\n                                                    constraint: Some(\n                                                        Generic(\n                                                            Keyof(\n                                                                Keyof(\n                                                                    Generic(\n                                                                        TypeParam(\n                                                                            TypeParam {\n                                                                                idx: 0,\n                                                                                constraint: None,\n                                                                            },\n                                                                        ),\n                                                                    ),\n                                                                ),\n                                                            ),\n                                                        ),\n                                                    ),\n                                                },\n                                            ),\n                                        ),\n                                    ],\n                                },\n                            ),\n                            value_type: Generic(\n                                Index(\n                                    Index {\n                                        source: Generic(\n                                            TypeParam(\n                                                TypeParam {\n                                                    idx: 0,\n                                                    constraint: None,\n                                                },\n                                            ),\n                                        ),\n                                        index: Generic(\n                                            MappedKeyType(\n                                                MappedKeyType,\n                                            ),\n                                        ),\n                                    },\n                                ),\n                            ),\n                            optional: None,\n                            as_type: None,\n                        },\n                    ),\n                ),\n                y: Named(\n                    Named {\n                        obj: Object {\n                            name: Some(\n                                \"Partial\",\n                            ),\n                        },\n                        type_arguments: [\n                            Named(\n                                Named {\n                                    obj: Object {\n                                        name: Some(\n                                            \"Pick\",\n                                        ),\n                                    },\n                                    type_arguments: [\n                                        Generic(\n                                            TypeParam(\n                                                TypeParam {\n                                                    idx: 0,\n                                                    constraint: None,\n                                                },\n                                            ),\n                                        ),\n                                        Generic(\n                                            TypeParam(\n                                                TypeParam {\n                                                    idx: 1,\n                                                    constraint: Some(\n                                                        Generic(\n                                                            Keyof(\n                                                                Keyof(\n                                                                    Generic(\n                                                                        TypeParam(\n                                                                            TypeParam {\n                                                                                idx: 0,\n                                                                                constraint: None,\n                                                                            },\n                                                                        ),\n                                                                    ),\n                                                                ),\n                                                            ),\n                                                        ),\n                                                    ),\n                                                },\n                                            ),\n                                        ),\n                                    ],\n                                },\n                            ),\n                        ],\n                    },\n                ),\n            },\n        ),\n    ),\n    \"Optional1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Index\": Interface(\n        Interface {\n            fields: [],\n            index: Some(\n                (\n                    Basic(\n                        String,\n                    ),\n                    Union(\n                        Union {\n                            types: [\n                                Basic(\n                                    Boolean,\n                                ),\n                                Basic(\n                                    Number,\n                                ),\n                            ],\n                        },\n                    ),\n                ),\n            ),\n            call: None,\n        },\n    ),\n    \"Intersect1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Intersect2\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"literal\",\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Intersect3\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Never,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Intersect4\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"optional\",\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Intersect5\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"a\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Any,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"b\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"c\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Never,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Enum1\": Enum(\n        EnumType {\n            members: [\n                EnumMember {\n                    name: \"A\",\n                    value: Number(\n                        0,\n                    ),\n                },\n                EnumMember {\n                    name: \"B\",\n                    value: Number(\n                        1,\n                    ),\n                },\n                EnumMember {\n                    name: \"C\",\n                    value: Number(\n                        2,\n                    ),\n                },\n                EnumMember {\n                    name: \"D\",\n                    value: String(\n                        \"foo\",\n                    ),\n                },\n                EnumMember {\n                    name: \"E\",\n                    value: Number(\n                        5,\n                    ),\n                },\n                EnumMember {\n                    name: \"F\",\n                    value: Number(\n                        6,\n                    ),\n                },\n            ],\n        },\n    ),\n    \"EnumFields\": Union(\n        Union {\n            types: [\n                Literal(\n                    String(\n                        \"A\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"B\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"C\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"D\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"E\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"F\",\n                    ),\n                ),\n            ],\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@call_expressions.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/call_expressions.ts\n---\n{\n    \"Ret\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"id\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"description\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"x\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"id\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"description\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"y\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"id\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"description\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"optResult\": Basic(\n        String,\n    ),\n    \"OptType\": Basic(\n        String,\n    ),\n    \"restResult\": Basic(\n        Boolean,\n    ),\n    \"RestType\": Basic(\n        Boolean,\n    ),\n    \"objResult\": Basic(\n        Number,\n    ),\n    \"ObjType\": Basic(\n        Number,\n    ),\n    \"arrResult\": Basic(\n        String,\n    ),\n    \"ArrType\": Basic(\n        String,\n    ),\n    \"nested\": Basic(\n        String,\n    ),\n    \"NestedType\": Basic(\n        String,\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@call_signatures.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/call_signatures.ts\n---\n{\n    \"BasicCallable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                String,\n                            ),\n                        ],\n                        Basic(\n                            Number,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"MultiParamCallable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                String,\n                            ),\n                            Basic(\n                                Number,\n                            ),\n                        ],\n                        Basic(\n                            Boolean,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"OptionalCallable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                String,\n                            ),\n                            Basic(\n                                Number,\n                            ),\n                        ],\n                        Basic(\n                            Void,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"MixedCallable\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"prop\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                Number,\n                            ),\n                        ],\n                        Basic(\n                            String,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"RestCallable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                String,\n                            ),\n                            Array(\n                                Array(\n                                    Basic(\n                                        Number,\n                                    ),\n                                ),\n                            ),\n                        ],\n                        Basic(\n                            Boolean,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"CallableInterface\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                Number,\n                            ),\n                        ],\n                        Basic(\n                            String,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"NoParamCallable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [],\n                        Basic(\n                            String,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"OverloadedCallable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                String,\n                            ),\n                        ],\n                        Basic(\n                            Number,\n                        ),\n                    ),\n                    (\n                        [\n                            Basic(\n                                Number,\n                            ),\n                        ],\n                        Basic(\n                            String,\n                        ),\n                    ),\n                    (\n                        [\n                            Basic(\n                                Boolean,\n                            ),\n                            Basic(\n                                Number,\n                            ),\n                            Basic(\n                                String,\n                            ),\n                        ],\n                        Basic(\n                            Void,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"Callable\": Interface(\n        Interface {\n            fields: [],\n            index: None,\n            call: Some(\n                [\n                    (\n                        [\n                            Basic(\n                                String,\n                            ),\n                        ],\n                        Basic(\n                            Number,\n                        ),\n                    ),\n                ],\n            ),\n        },\n    ),\n    \"Res\": Basic(\n        Number,\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@export_default.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/export_default.ts\n---\n{\n    \"default\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@export_wildcard.txt.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/export_wildcard.txt\n---\n{\n    \"foo\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@extends.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/extends.ts\n---\n{\n    \"Foo\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Union(\n                        Union {\n                            types: [\n                                Basic(\n                                    String,\n                                ),\n                                Basic(\n                                    Number,\n                                ),\n                                Basic(\n                                    Null,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Bar\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Union(\n                        Union {\n                            types: [\n                                Basic(\n                                    String,\n                                ),\n                                Basic(\n                                    Null,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"moo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Generic\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Union(\n                        Union {\n                            types: [\n                                Generic(\n                                    TypeParam(\n                                        TypeParam {\n                                            idx: 0,\n                                            constraint: None,\n                                        },\n                                    ),\n                                ),\n                                Basic(\n                                    Null,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"ExtendGeneric\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Union(\n                        Union {\n                            types: [\n                                Union(\n                                    Union {\n                                        types: [\n                                            Basic(\n                                                String,\n                                            ),\n                                            Basic(\n                                                Number,\n                                            ),\n                                        ],\n                                    },\n                                ),\n                                Basic(\n                                    Null,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"MergeGeneric\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Union(\n                        Union {\n                            types: [\n                                Literal(\n                                    Number(\n                                        5.0,\n                                    ),\n                                ),\n                                Basic(\n                                    Null,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@generic.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/generic.ts\n---\n{\n    \"Interface\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 0,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"InterfaceDefault\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 0,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"InterfaceDefaultMulti\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 0,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 1,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"baz\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 2,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"qux\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 3,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"quux\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 4,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"corge\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 5,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"X\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Y\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Z\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"M1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Object,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Undefined,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"baz\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Null,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"qux\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"quux\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"corge\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"M2\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Object,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Undefined,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"baz\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Null,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"qux\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        BigInt,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"quux\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"corge\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"M3\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Object,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Undefined,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"baz\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Null,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"qux\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        BigInt,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"quux\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        Boolean(\n                            false,\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"corge\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"M4\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Object,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Undefined,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"baz\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Null,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"qux\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        BigInt,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"quux\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        Boolean(\n                            false,\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"corge\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"literal\",\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@generics.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/generics.ts\n---\n{\n    \"Generic1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"cond\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        Conditional(\n                            Conditional {\n                                check_type: Generic(\n                                    TypeParam(\n                                        TypeParam {\n                                            idx: 0,\n                                            constraint: None,\n                                        },\n                                    ),\n                                ),\n                                extends_type: Basic(\n                                    String,\n                                ),\n                                true_type: Literal(\n                                    String(\n                                        \"literal\",\n                                    ),\n                                ),\n                                false_type: Basic(\n                                    Number,\n                                ),\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Generic2\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"value\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 0,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"cond\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        Conditional(\n                            Conditional {\n                                check_type: Generic(\n                                    TypeParam(\n                                        TypeParam {\n                                            idx: 0,\n                                            constraint: None,\n                                        },\n                                    ),\n                                ),\n                                extends_type: Basic(\n                                    String,\n                                ),\n                                true_type: Literal(\n                                    String(\n                                        \"literal\",\n                                    ),\n                                ),\n                                false_type: Basic(\n                                    Number,\n                                ),\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Concrete1\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"one\",\n                    ),\n                    optional: false,\n                    typ: Named(\n                        Named {\n                            obj: Object {\n                                name: Some(\n                                    \"Generic1\",\n                                ),\n                            },\n                            type_arguments: [\n                                Basic(\n                                    String,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"two\",\n                    ),\n                    optional: false,\n                    typ: Named(\n                        Named {\n                            obj: Object {\n                                name: Some(\n                                    \"Generic1\",\n                                ),\n                            },\n                            type_arguments: [\n                                Literal(\n                                    String(\n                                        \"test\",\n                                    ),\n                                ),\n                            ],\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"three\",\n                    ),\n                    optional: false,\n                    typ: Named(\n                        Named {\n                            obj: Object {\n                                name: Some(\n                                    \"Generic2\",\n                                ),\n                            },\n                            type_arguments: [\n                                Basic(\n                                    Null,\n                                ),\n                            ],\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"four\",\n                    ),\n                    optional: false,\n                    typ: Named(\n                        Named {\n                            obj: Object {\n                                name: Some(\n                                    \"Generic2\",\n                                ),\n                            },\n                            type_arguments: [\n                                Named(\n                                    Named {\n                                        obj: Object {\n                                            name: Some(\n                                                \"Generic1\",\n                                            ),\n                                        },\n                                        type_arguments: [\n                                            Basic(\n                                                Boolean,\n                                            ),\n                                        ],\n                                    },\n                                ),\n                            ],\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"five\",\n                    ),\n                    optional: false,\n                    typ: Named(\n                        Named {\n                            obj: Object {\n                                name: Some(\n                                    \"GenericIface\",\n                                ),\n                            },\n                            type_arguments: [\n                                Named(\n                                    Named {\n                                        obj: Object {\n                                            name: Some(\n                                                \"Generic1\",\n                                            ),\n                                        },\n                                        type_arguments: [\n                                            Basic(\n                                                Boolean,\n                                            ),\n                                        ],\n                                    },\n                                ),\n                            ],\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"GenericIface\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Generic(\n                        TypeParam(\n                            TypeParam {\n                                idx: 0,\n                                constraint: None,\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@infer.txt.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/infer.txt\n---\n{\n    \"Iface\": Generic(\n        Conditional(\n            Conditional {\n                check_type: Generic(\n                    TypeParam(\n                        TypeParam {\n                            idx: 0,\n                            constraint: None,\n                        },\n                    ),\n                ),\n                extends_type: Interface(\n                    Interface {\n                        fields: [\n                            InterfaceField {\n                                name: String(\n                                    \"X\",\n                                ),\n                                optional: false,\n                                typ: Generic(\n                                    Inferred(\n                                        Inferred(\n                                            0,\n                                        ),\n                                    ),\n                                ),\n                            },\n                        ],\n                        index: None,\n                        call: None,\n                    },\n                ),\n                true_type: Generic(\n                    Inferred(\n                        Inferred(\n                            0,\n                        ),\n                    ),\n                ),\n                false_type: Basic(\n                    Never,\n                ),\n            },\n        ),\n    ),\n    \"Infer1\": Basic(\n        String,\n    ),\n    \"Infer2\": Basic(\n        Never,\n    ),\n    \"Infer3\": Basic(\n        String,\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@keyofenum.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/keyofenum.ts\n---\n{\n    \"ObjectValues\": Generic(\n        Index(\n            Index {\n                source: Generic(\n                    TypeParam(\n                        TypeParam {\n                            idx: 0,\n                            constraint: None,\n                        },\n                    ),\n                ),\n                index: Generic(\n                    Keyof(\n                        Keyof(\n                            Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                        ),\n                    ),\n                ),\n            },\n        ),\n    ),\n    \"MY_ENUM\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"VARIANT_1\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"VARIANT_1\",\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"VARIANT_2\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"VARIANT_2\",\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"VARIANT_3\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"VARIANT_3\",\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"MyEnum\": Union(\n        Union {\n            types: [\n                Literal(\n                    String(\n                        \"VARIANT_1\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"VARIANT_2\",\n                    ),\n                ),\n                Literal(\n                    String(\n                        \"VARIANT_3\",\n                    ),\n                ),\n            ],\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@mapped_as.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/mapped_as.ts\n---\n{\n    \"Person\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"age\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"kind\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"RemoveKind\": Generic(\n        Mapped(\n            Mapped {\n                in_type: Generic(\n                    Keyof(\n                        Keyof(\n                            Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                        ),\n                    ),\n                ),\n                value_type: Generic(\n                    Index(\n                        Index {\n                            source: Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                            index: Generic(\n                                MappedKeyType(\n                                    MappedKeyType,\n                                ),\n                            ),\n                        },\n                    ),\n                ),\n                optional: None,\n                as_type: Some(\n                    Generic(\n                        Conditional(\n                            Conditional {\n                                check_type: Generic(\n                                    MappedKeyType(\n                                        MappedKeyType,\n                                    ),\n                                ),\n                                extends_type: Literal(\n                                    String(\n                                        \"kind\",\n                                    ),\n                                ),\n                                true_type: Basic(\n                                    Never,\n                                ),\n                                false_type: Generic(\n                                    MappedKeyType(\n                                        MappedKeyType,\n                                    ),\n                                ),\n                            },\n                        ),\n                    ),\n                ),\n            },\n        ),\n    ),\n    \"PersonNoKind\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"age\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Identity\": Generic(\n        Mapped(\n            Mapped {\n                in_type: Generic(\n                    Keyof(\n                        Keyof(\n                            Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                        ),\n                    ),\n                ),\n                value_type: Generic(\n                    Index(\n                        Index {\n                            source: Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                            index: Generic(\n                                MappedKeyType(\n                                    MappedKeyType,\n                                ),\n                            ),\n                        },\n                    ),\n                ),\n                optional: None,\n                as_type: Some(\n                    Generic(\n                        MappedKeyType(\n                            MappedKeyType,\n                        ),\n                    ),\n                ),\n            },\n        ),\n    ),\n    \"IdentityPerson\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"age\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"kind\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"OnlyStrings\": Generic(\n        Mapped(\n            Mapped {\n                in_type: Generic(\n                    Keyof(\n                        Keyof(\n                            Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                        ),\n                    ),\n                ),\n                value_type: Generic(\n                    Index(\n                        Index {\n                            source: Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                            index: Generic(\n                                MappedKeyType(\n                                    MappedKeyType,\n                                ),\n                            ),\n                        },\n                    ),\n                ),\n                optional: None,\n                as_type: Some(\n                    Generic(\n                        Conditional(\n                            Conditional {\n                                check_type: Generic(\n                                    Index(\n                                        Index {\n                                            source: Generic(\n                                                TypeParam(\n                                                    TypeParam {\n                                                        idx: 0,\n                                                        constraint: None,\n                                                    },\n                                                ),\n                                            ),\n                                            index: Generic(\n                                                MappedKeyType(\n                                                    MappedKeyType,\n                                                ),\n                                            ),\n                                        },\n                                    ),\n                                ),\n                                extends_type: Basic(\n                                    String,\n                                ),\n                                true_type: Generic(\n                                    MappedKeyType(\n                                        MappedKeyType,\n                                    ),\n                                ),\n                                false_type: Basic(\n                                    Never,\n                                ),\n                            },\n                        ),\n                    ),\n                ),\n            },\n        ),\n    ),\n    \"PersonStrings\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"kind\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@method_signatures.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/method_signatures.ts\n---\n{\n    \"SimpleMethod\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Basic(\n                                Void,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Basic(\n                                String,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"MethodsWithParams\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"simple\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"a\",\n                                    ),\n                                    typ: Basic(\n                                        String,\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Basic(\n                                Void,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"multiple\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"a\",\n                                    ),\n                                    typ: Basic(\n                                        String,\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                                FunctionParam {\n                                    name: Some(\n                                        \"b\",\n                                    ),\n                                    typ: Basic(\n                                        Number,\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Basic(\n                                Boolean,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"withOptional\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"a\",\n                                    ),\n                                    typ: Basic(\n                                        String,\n                                    ),\n                                    optional: true,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Basic(\n                                Void,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"withRest\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"args\",\n                                    ),\n                                    typ: Array(\n                                        Array(\n                                            Basic(\n                                                String,\n                                            ),\n                                        ),\n                                    ),\n                                    optional: false,\n                                    rest: true,\n                                },\n                            ],\n                            return_type: Basic(\n                                Void,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"ComplexReturns\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"returnsObject\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Interface(\n                                Interface {\n                                    fields: [\n                                        InterfaceField {\n                                            name: String(\n                                                \"x\",\n                                            ),\n                                            optional: false,\n                                            typ: Basic(\n                                                Number,\n                                            ),\n                                        },\n                                    ],\n                                    index: None,\n                                    call: None,\n                                },\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"returnsArray\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Array(\n                                Array(\n                                    Basic(\n                                        String,\n                                    ),\n                                ),\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"returnsUnion\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Union(\n                                Union {\n                                    types: [\n                                        Basic(\n                                            String,\n                                        ),\n                                        Basic(\n                                            Number,\n                                        ),\n                                    ],\n                                },\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"OptionalMethods\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"required\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Basic(\n                                Void,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optional\",\n                    ),\n                    optional: true,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Basic(\n                                Void,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"MixedInterface\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"prop\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"method\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Basic(\n                                Number,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optionalProp\",\n                    ),\n                    optional: true,\n                    typ: Basic(\n                        Boolean,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"optionalMethod\",\n                    ),\n                    optional: true,\n                    typ: Function(\n                        FunctionType {\n                            params: [],\n                            return_type: Basic(\n                                String,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"Column\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"mapFromDriverValue\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"value\",\n                                    ),\n                                    typ: Generic(\n                                        TypeParam(\n                                            TypeParam {\n                                                idx: 0,\n                                                constraint: None,\n                                            },\n                                        ),\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 1,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"mapToDriverValue\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"value\",\n                                    ),\n                                    typ: Generic(\n                                        TypeParam(\n                                            TypeParam {\n                                                idx: 1,\n                                                constraint: None,\n                                            },\n                                        ),\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Generic(\n                                TypeParam(\n                                    TypeParam {\n                                        idx: 0,\n                                        constraint: None,\n                                    },\n                                ),\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"SqlDialect\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"escape\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"str\",\n                                    ),\n                                    typ: Basic(\n                                        String,\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Basic(\n                                String,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"escapeId\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"id\",\n                                    ),\n                                    typ: Basic(\n                                        String,\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Basic(\n                                String,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"buildQuery\",\n                    ),\n                    optional: false,\n                    typ: Function(\n                        FunctionType {\n                            params: [\n                                FunctionParam {\n                                    name: Some(\n                                        \"sql\",\n                                    ),\n                                    typ: Basic(\n                                        String,\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                                FunctionParam {\n                                    name: Some(\n                                        \"values\",\n                                    ),\n                                    typ: Array(\n                                        Array(\n                                            Basic(\n                                                Any,\n                                            ),\n                                        ),\n                                    ),\n                                    optional: false,\n                                    rest: false,\n                                },\n                            ],\n                            return_type: Basic(\n                                String,\n                            ),\n                            type_params: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@namespace_import.txt.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/namespace_import.txt\n---\n{\n    \"TestBar\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"id\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"description\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"TestBaz\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"id\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        Number,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"name\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"description\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@qualified_name.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/qualified_name.ts\n---\n{\n    \"T1\": Basic(\n        String,\n    ),\n    \"T2\": Basic(\n        Number,\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@reexport_local.txt.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/reexport_local.txt\n---\n{\n    \"MyType\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"field\",\n                    ),\n                    optional: false,\n                    typ: Named(\n                        Named {\n                            obj: Object {\n                                name: Some(\n                                    \"foo\",\n                                ),\n                            },\n                            type_arguments: [],\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@reexport_single.txt.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/reexport_single.txt\n---\n{\n    \"foo\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@reexport_wildcard.txt.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/reexport_wildcard.txt\n---\n{\n    \"foo\": Literal(\n        String(\n            \"foo\",\n        ),\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@typeof.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/typeof.ts\n---\n{\n    \"O\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Literal(\n                        String(\n                            \"blah\",\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"nested\",\n                    ),\n                    optional: false,\n                    typ: Interface(\n                        Interface {\n                            fields: [\n                                InterfaceField {\n                                    name: String(\n                                        \"baz\",\n                                    ),\n                                    optional: false,\n                                    typ: Literal(\n                                        Number(\n                                            55.0,\n                                        ),\n                                    ),\n                                },\n                            ],\n                            index: None,\n                            call: None,\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"S\": Literal(\n        String(\n            \"blah\",\n        ),\n    ),\n    \"N\": Literal(\n        Number(\n            55.0,\n        ),\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@validation.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/validation.ts\n---\n{\n    \"Params\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"foo\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Basic(\n                                Number,\n                            ),\n                            expr: And(\n                                [\n                                    Rule(\n                                        MinVal(\n                                            N(\n                                                3.0,\n                                            ),\n                                        ),\n                                    ),\n                                    Rule(\n                                        MaxVal(\n                                            N(\n                                                1000.0,\n                                            ),\n                                        ),\n                                    ),\n                                ],\n                            ),\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"bar\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Basic(\n                                String,\n                            ),\n                            expr: And(\n                                [\n                                    Rule(\n                                        MinLen(\n                                            5,\n                                        ),\n                                    ),\n                                    Rule(\n                                        MaxLen(\n                                            20,\n                                        ),\n                                    ),\n                                ],\n                            ),\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"urlOrEmail\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Basic(\n                                String,\n                            ),\n                            expr: Or(\n                                [\n                                    Rule(\n                                        Is(\n                                            Url,\n                                        ),\n                                    ),\n                                    Rule(\n                                        Is(\n                                            Email,\n                                        ),\n                                    ),\n                                ],\n                            ),\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"emails\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Array(\n                                Array(\n                                    Validated(\n                                        Validated {\n                                            typ: Basic(\n                                                String,\n                                            ),\n                                            expr: Rule(\n                                                Is(\n                                                    Email,\n                                                ),\n                                            ),\n                                        },\n                                    ),\n                                ),\n                            ),\n                            expr: Rule(\n                                MaxLen(\n                                    10,\n                                ),\n                            ),\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/snapshots/encore_tsparser__parser__types__tests__resolve_types@wirespec.ts.snap",
    "content": "---\nsource: tsparser/src/parser/types/tests.rs\nexpression: result\ninput_file: tsparser/src/parser/types/testdata/wirespec.ts\n---\n{\n    \"Foo\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"a\",\n                    ),\n                    optional: false,\n                    typ: Custom(\n                        WireSpec(\n                            WireSpec {\n                                location: Query,\n                                underlying: Basic(\n                                    Number,\n                                ),\n                                name_override: None,\n                            },\n                        ),\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"b\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Custom(\n                                WireSpec(\n                                    WireSpec {\n                                        location: Query,\n                                        underlying: Basic(\n                                            Number,\n                                        ),\n                                        name_override: None,\n                                    },\n                                ),\n                            ),\n                            expr: Rule(\n                                MaxVal(\n                                    N(\n                                        10.0,\n                                    ),\n                                ),\n                            ),\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"c\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Custom(\n                                WireSpec(\n                                    WireSpec {\n                                        location: Query,\n                                        underlying: Basic(\n                                            String,\n                                        ),\n                                        name_override: None,\n                                    },\n                                ),\n                            ),\n                            expr: Rule(\n                                MinLen(\n                                    3,\n                                ),\n                            ),\n                        },\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"d\",\n                    ),\n                    optional: false,\n                    typ: Validated(\n                        Validated {\n                            typ: Custom(\n                                WireSpec(\n                                    WireSpec {\n                                        location: Header,\n                                        underlying: Basic(\n                                            String,\n                                        ),\n                                        name_override: Some(\n                                            \"X-Header\",\n                                        ),\n                                    },\n                                ),\n                            ),\n                            expr: Rule(\n                                MinLen(\n                                    3,\n                                ),\n                            ),\n                        },\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n    \"ResponseWithStatus\": Interface(\n        Interface {\n            fields: [\n                InterfaceField {\n                    name: String(\n                        \"data\",\n                    ),\n                    optional: false,\n                    typ: Basic(\n                        String,\n                    ),\n                },\n                InterfaceField {\n                    name: String(\n                        \"status\",\n                    ),\n                    optional: false,\n                    typ: Custom(\n                        WireSpec(\n                            WireSpec {\n                                location: HttpStatus,\n                                underlying: Basic(\n                                    Number,\n                                ),\n                                name_override: None,\n                            },\n                        ),\n                    ),\n                },\n            ],\n            index: None,\n            call: None,\n        },\n    ),\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/basic.ts",
    "content": "// Basic interface\nexport interface Interface {\n  foo: string;\n  bar: number;\n  optional?: boolean;\n}\n\n// Utility types\nexport type Exclude1 = Exclude<keyof Interface, \"foo\">;\nexport type Pick1 = Pick<Interface, \"foo\">;\nexport type Pick2 = Pick<Interface, \"foo\" | \"optional\">;\nexport type Omit1 = Omit<Interface, \"foo\">;\nexport type Omit2 = Omit<Interface, \"foo\" | \"bar\">;\n\nexport type Partial1 = Partial<Interface>;\n\nexport type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;\nexport type Optional1 = Optional<Interface, \"foo\">;\n\n// Index signatures\nexport type Index = { [key: string]: boolean | number };\n\n// Intersections\nexport type Intersect1 = { foo: string } & { bar: number };\nexport type Intersect2 = { foo: string } & { foo: \"literal\" };\nexport type Intersect3 = { foo: string } & { foo: number };\nexport type Intersect4 = { foo?: \"optional\" } & { foo: string };\nexport type Intersect5 = { a: string; b: string; c: string } & {\n  a: any;\n  b: unknown;\n  c: never;\n};\n\n// Enums\nexport enum Enum1 {\n  A,\n  B,\n  C,\n  D = \"foo\",\n  E = 5,\n  F\n}\nexport type EnumFields = keyof typeof Enum1;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/call_expressions.ts",
    "content": "// Basic return type\nexport type Ret = {\n  id: number;\n  name: string;\n  description: string;\n};\n\n// Simple function with explicit return type\nfunction HelloWorld(a: string, b: number): Ret {\n  return {\n    id: 1,\n    name: \"Hello\",\n    description: \"Hello description\",\n  };\n}\n\n// Call expression - typeof should resolve to Ret\nexport const x = HelloWorld(\"hello\", 5);\nexport type y = typeof x;\n\n// Function with optional parameters\nfunction withOptional(a: string, b?: number): string {\n  return a;\n}\n\nexport const optResult = withOptional(\"test\");\nexport type OptType = typeof optResult;\n\n// Function with rest parameters\nfunction withRest(a: string, ...rest: number[]): boolean {\n  return true;\n}\n\nexport const restResult = withRest(\"test\", 1, 2, 3);\nexport type RestType = typeof restResult;\n\n// Function with destructured object parameter\nfunction withObjParam({ x, y }: { x: number; y: number }): number {\n  return x + y;\n}\n\nexport const objResult = withObjParam({ x: 1, y: 2 });\nexport type ObjType = typeof objResult;\n\n// Function with destructured array parameter\nfunction withArrayParam([a, b]: [string, string]): string {\n  return a + b;\n}\n\nexport const arrResult = withArrayParam([\"a\", \"b\"]);\nexport type ArrType = typeof arrResult;\n\n// Nested function calls\nfunction getNumber(): number {\n  return 42;\n}\n\nfunction useNumber(n: number): string {\n  return \"result\";\n}\n\nexport const nested = useNumber(getNumber());\nexport type NestedType = typeof nested;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/call_signatures.ts",
    "content": "// Basic call signature\nexport type BasicCallable = {\n  (arg: string): number;\n};\n\n// Call signature with multiple parameters\nexport type MultiParamCallable = {\n  (a: string, b: number): boolean;\n};\n\n// Call signature with optional parameters\nexport type OptionalCallable = {\n  (required: string, optional?: number): void;\n};\n\n// Mixed: properties and call signature\nexport type MixedCallable = {\n  prop: string;\n  (arg: number): string;\n};\n\n// Call signature with rest parameters\nexport type RestCallable = {\n  (first: string, ...rest: number[]): boolean;\n};\n\n// Interface with call signature\nexport interface CallableInterface {\n  (x: number): string;\n}\n\n// Call signature with no parameters\nexport type NoParamCallable = {\n  (): string;\n};\n\n// Overloaded call signatures\nexport type OverloadedCallable = {\n  (x: string): number;\n  (x: number): string;\n  (x: boolean, y: number, z: string): void;\n};\n\nexport const Callable: BasicCallable = (arg: string): number => {\n  return 1;\n};\nconst res = Callable(\"test\");\nexport type Res = typeof res;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/export_default.ts",
    "content": "export default interface foo {\n  name: string;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/extends.ts",
    "content": "export interface Foo {\n  foo: string | number | null;\n  bar: number;\n  optional?: boolean;\n}\n\nexport interface Bar extends Foo {\n  foo: string | null; // now never a number\n  optional: boolean; // now required\n  moo: string;\n}\n\nexport interface Generic<T> {\n  foo: T | null;\n}\n\nexport interface ExtendGeneric extends Generic<string | number> {\n  bar: string;\n}\n\nexport interface MergeGeneric extends Generic<number> {\n  foo: 5 | null;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/generic.ts",
    "content": "// Basic interface with generic param\nexport interface Interface<T> {\n  foo: T;\n}\n\n// Basic interface with generic param with default type\nexport interface InterfaceDefault<T = string> {\n  foo: T;\n}\n\n// Interface with multiple generic param with default type\nexport interface InterfaceDefaultMulti<\n  A,\n  B,\n  C,\n  D = string,\n  E = number,\n  F = boolean\n> {\n  foo: A;\n  bar: B;\n  baz: C;\n  qux: D;\n  quux: E;\n  corge: F;\n}\n\nexport type X = Interface<string>;\nexport type Y = InterfaceDefault;\nexport type Z = InterfaceDefault<boolean>;\n\nexport type M1 = InterfaceDefaultMulti<object, undefined, null>;\nexport type M2 = InterfaceDefaultMulti<object, undefined, null, bigint>;\nexport type M3 = InterfaceDefaultMulti<object, undefined, null, bigint, false>;\nexport type M4 = InterfaceDefaultMulti<\n  object,\n  undefined,\n  null,\n  bigint,\n  false,\n  \"literal\"\n>;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/generics.ts",
    "content": "export type Generic1<T> = {\n  cond: T extends string ? \"literal\" : number;\n}\n\nexport type Generic2<T> = {\n  value: T;\n  cond: T extends string ? \"literal\" : number;\n}\n\nexport type Concrete1 = {\n  one: Generic1<string>;\n  two: Generic1<\"test\">;\n  three: Generic2<null>;\n  four: Generic2<Generic1<boolean>>;\n  five: GenericIface<Generic1<boolean>>;\n}\n\nexport interface GenericIface<T> {\n  foo: T;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/infer.txt",
    "content": "export type Iface<T> = T extends { X: infer A } ? A : never;\n\nexport type Infer1 = Iface<{ X: string }>; // string\nexport type Infer2 = Iface<{ Y: string }>; // never\nexport type Infer3 = { X: string } extends { X: infer A } ? A : never; // string\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/keyofenum.ts",
    "content": "export type ObjectValues<T> = T[keyof T];\n\nexport const MY_ENUM = {\n  VARIANT_1: \"VARIANT_1\",\n  VARIANT_2: \"VARIANT_2\",\n  VARIANT_3: \"VARIANT_3\"\n} as const;\n\nexport type MyEnum = ObjectValues<typeof MY_ENUM>;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/mapped_as.ts",
    "content": "// Mapped types with 'as' clause for key remapping\n\nexport interface Person {\n  name: string;\n  age: number;\n  kind: string;\n}\n\n// Test 1: Filter out properties using never\nexport type RemoveKind<T> = {\n  [K in keyof T as K extends \"kind\" ? never : K]: T[K]\n}\nexport type PersonNoKind = RemoveKind<Person>;\n\n// Test 2: Identity mapping\nexport type Identity<T> = {\n  [K in keyof T as K]: T[K]\n}\nexport type IdentityPerson = Identity<Person>;\n\n// Test 3: Filter based on value type\nexport type OnlyStrings<T> = {\n  [K in keyof T as T[K] extends string ? K : never]: T[K]\n}\nexport type PersonStrings = OnlyStrings<Person>;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/method_signatures.ts",
    "content": "// Simple method signatures\nexport type SimpleMethod = {\n  foo(): void;\n  bar(): string;\n};\n\n// Methods with parameters\nexport type MethodsWithParams = {\n  simple(a: string): void;\n  multiple(a: string, b: number): boolean;\n  withOptional(a?: string): void;\n  withRest(...args: string[]): void;\n};\n\n// Methods with complex return types\nexport type ComplexReturns = {\n  returnsObject(): { x: number };\n  returnsArray(): string[];\n  returnsUnion(): string | number;\n};\n\n// Optional methods\nexport type OptionalMethods = {\n  required(): void;\n  optional?(): void;\n};\n\n// Mixed properties and methods\nexport type MixedInterface = {\n  prop: string;\n  method(): number;\n  optionalProp?: boolean;\n  optionalMethod?(): string;\n};\n\n// Example mimicking the drizzle-orm pattern that was failing\nexport type Column<TDriverParam = unknown, TData = TDriverParam> = {\n  mapFromDriverValue(value: TDriverParam): TData;\n  mapToDriverValue(value: TData): TDriverParam;\n};\n\n// More complex example with multiple methods\nexport interface SqlDialect {\n  escape(str: string): string;\n  escapeId(id: string): string;\n  buildQuery(sql: string, values: any[]): string;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/namespace_import.txt",
    "content": "import * as pkg from \"./pkg\";\n\nexport type TestBar = pkg.Bar;\nexport type TestBaz = pkg.Baz;\n\n-- pkg.ts --\nexport * from \"./bar\";\nexport * from \"./baz\";\n\n-- bar.ts --\nexport type Bar = {\n  id: number;\n  name: string;\n  description: string;\n};\n\n-- baz.ts --\nexport type Baz = {\n  id: number;\n  name: string;\n  description: string;\n};\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/qualified_name.ts",
    "content": "namespace Foo {\n  export type Bar = string;\n\n  export namespace Nested {\n    export type Baz = number;\n  }\n};\n\n\nexport type T1 = Foo.Bar;\nexport type T2 = Foo.Nested.Baz;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/reexport_local.txt",
    "content": "import { foo } from \"./foo\";\n\nexport interface MyType {\n    field: foo;\n}\n\n-- foo.ts --\ninterface foo {\n  bar: string;\n}\nexport { foo };\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/reexport_single.txt",
    "content": "export { foo } from \"./foo\";\n\n-- foo.ts --\nexport interface foo {\n  bar: string;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/reexport_wildcard.txt",
    "content": "export { foo } from \"./foo\";\n\n-- foo.ts --\nexport * from \"./bar\";\n\n-- bar.ts --\nexport const foo = \"foo\";\nexport interface bar {\n    num: number;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/typeof.ts",
    "content": "const foo = {\n  bar: \"blah\",\n  nested: {\n    baz: 55\n  }\n};\n\nexport type O = typeof foo;\nexport type S = typeof foo.bar;\nexport type N = typeof foo.nested.baz;\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/validation.ts",
    "content": "import { Min, Max, MinLen, MaxLen, IsURL, IsEmail } from \"encore.dev/validate\";\n\nexport interface Params {\n  // A number between 3 and 1000 (inclusive).\n  foo: number & (Min<3> & Max<1000>);\n\n  // A string between 5 and 20 characters long.\n  bar: string & (MinLen<5> & MaxLen<20>);\n\n  // A string that is either a URL or an email address.\n  urlOrEmail: string & (IsURL | IsEmail);\n\n  // An array of up to 10 email addresses.\n  emails: Array<string & IsEmail> & MaxLen<10>;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/testdata/wirespec.ts",
    "content": "import { Query, Header, HttpStatus } from \"encore.dev/api\";\nimport { MinLen, Max } from \"encore.dev/validate\";\n\nexport interface Foo {\n  a: Query<number>;\n  b: Query<number> & Max<10>;\n  c: Query<string> & MinLen<3>;\n  d: Header<\"X-Header\"> & MinLen<3>;\n}\n\nexport interface ResponseWithStatus {\n  data: string;\n  status: HttpStatus;\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/tests.rs",
    "content": "use super::*;\nuse std::fs;\nuse std::rc::Rc;\n\nuse crate::parser::parser::ParseContext;\nuse crate::parser::types::type_resolve::Ctx;\nuse crate::parser::FilePath;\nuse crate::testutil::testresolve::TestResolver;\nuse crate::testutil::JS_RUNTIME_PATH;\nuse indexmap::IndexMap;\nuse insta::{assert_debug_snapshot, glob};\nuse itertools::Itertools;\nuse object::Reexport;\nuse swc_common::errors::{Handler, HANDLER};\nuse swc_common::{Globals, SourceMap, GLOBALS};\nuse tempdir::TempDir;\n\n#[test]\nfn resolve_types() {\n    // tracing_subscriber::fmt()\n    //     .with_span_events(FmtSpan::ACTIVE)\n    //     .init();\n    env_logger::init();\n    glob!(\"testdata/*\", |path| {\n        let globals = Globals::new();\n        let errs = Rc::new(Handler::with_tty_emitter(\n            swc_common::errors::ColorConfig::Auto,\n            true,\n            false,\n            None,\n        ));\n\n        GLOBALS.set(&globals, || {\n            HANDLER.set(&errs, || {\n                let input = fs::read_to_string(path).unwrap();\n                let ar = txtar::from_str(&input);\n                let tmp_dir = TempDir::new(\"tsparser-test\").unwrap();\n                ar.materialize(&tmp_dir).unwrap();\n\n                let resolver =\n                    Box::new(TestResolver::new(tmp_dir.path().to_path_buf(), ar.clone()));\n                let cm: Rc<SourceMap> = Default::default();\n\n                let pc = ParseContext::with_resolver(\n                    tmp_dir.path().to_path_buf(),\n                    Some(JS_RUNTIME_PATH.clone()),\n                    resolver,\n                    cm,\n                    errs.clone(),\n                )\n                .unwrap();\n\n                let _mods = pc.loader.load_archive(tmp_dir.path(), &ar).unwrap();\n\n                let file_name = FilePath::Real(tmp_dir.path().join(\"test.ts\"));\n                let module = pc.loader.inject_file(file_name, &ar.comment).unwrap();\n\n                let resolve = ResolveState::new(pc.loader.clone());\n                let module = resolve.get_or_init_module(module);\n                let ctx = Ctx::new(&resolve, module.base.id);\n\n                let mut result = module\n                    .data\n                    .named_exports\n                    .iter()\n                    .sorted_by(|a, b| a.1.range.cmp(&b.1.range))\n                    .map(|(name, obj)| {\n                        let typ = ctx.obj_type(obj);\n                        let typ = ctx.underlying(&typ).into_owned();\n                        (name, typ)\n                    })\n                    .collect::<IndexMap<_, _>>();\n\n                let default_key = \"default\".to_string();\n                if let Some(default_export) = module.data.default_export.as_ref() {\n                    let typ = ctx.obj_type(default_export);\n                    let typ = ctx.underlying(&typ).into_owned();\n                    result.insert(&default_key, typ);\n                }\n\n                for re in &module.data.reexports {\n                    match re {\n                        Reexport::All { .. } => {}\n                        Reexport::List { items, .. } => {\n                            for it in items {\n                                let export_name = it.renamed.as_ref().unwrap_or(&it.orig_name);\n                                let obj = module\n                                    .data\n                                    .get_named_export(\n                                        &resolve,\n                                        &module.base.swc_file_path,\n                                        export_name,\n                                    )\n                                    .unwrap();\n                                let typ = ctx.obj_type(&obj);\n                                let typ = ctx.underlying(&typ).into_owned();\n                                result.insert(export_name, typ);\n                            }\n                        }\n                    }\n                }\n\n                assert_debug_snapshot!(result);\n            })\n        })\n    });\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/typ.rs",
    "content": "use crate::parser::types::type_resolve::Ctx;\nuse crate::parser::types::{object, validation, Object, ResolveState};\nuse crate::parser::Range;\nuse indexmap::IndexMap;\nuse itertools::Itertools;\nuse serde::Serialize;\nuse std::borrow::Cow;\nuse std::collections::HashMap;\nuse std::fmt::{Debug, Display};\nuse std::hash::{Hash, Hasher};\nuse std::rc::Rc;\nuse swc_common::errors::HANDLER;\n\n#[derive(Debug, Clone, Hash, PartialEq, Eq)]\npub struct TypeArgId(usize);\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub enum Type {\n    /// strings, etc\n    Basic(Basic),\n    /// T[], Array<T>\n    Array(Array),\n    /// { foo: string }\n    Interface(Interface),\n    /// a | b | c\n    Union(Union),\n    /// [string, number]\n    Tuple(Tuple),\n    /// \"foo\"\n    Literal(Literal),\n    /// class Foo {}\n    Class(ClassType),\n    /// enum Foo {}\n    Enum(EnumType),\n\n    /// A named type, with optional type arguments.\n    Named(Named),\n\n    /// e.g. \"string?\" in tuples\n    Optional(Optional),\n\n    /// \"this\", see https://www.typescriptlang.org/docs/handbook/advanced-types.html#polymorphic-this-types\n    This(This),\n\n    Generic(Generic),\n\n    /// A standalone validation expression.\n    Validation(validation::Expr),\n\n    /// A type with validation applied to it.\n    Validated(Validated),\n\n    // A custom type of some kind.\n    Custom(Custom),\n\n    /// A function type\n    Function(FunctionType),\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Array(pub Box<Type>);\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Optional(pub Box<Type>);\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Tuple {\n    pub types: Vec<Type>,\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Union {\n    pub types: Vec<Type>,\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Validated {\n    pub typ: Box<Type>,\n    pub expr: validation::Expr,\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct FunctionType {\n    /// Function parameters with names, types, and modifiers\n    pub params: Vec<FunctionParam>,\n\n    /// Return type of the function\n    pub return_type: Box<Type>,\n\n    /// Generic type parameters\n    pub type_params: Option<Vec<GenericTypeParam>>,\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct FunctionParam {\n    /// Parameter name (optional for destructured params)\n    pub name: Option<String>,\n\n    /// Parameter type\n    pub typ: Type,\n\n    /// Whether parameter is optional (foo?: string)\n    pub optional: bool,\n\n    /// Whether this is a rest parameter (...args: string[])\n    pub rest: bool,\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct GenericTypeParam {\n    pub idx: usize,\n    pub name: String,\n    pub constraint: Option<Box<Type>>,\n    pub default: Option<Box<Type>>,\n}\n\nimpl FunctionType {\n    pub fn identical(&self, other: &FunctionType) -> bool {\n        // Check parameter count and types\n        if self.params.len() != other.params.len() {\n            return false;\n        }\n\n        for (a, b) in self.params.iter().zip(&other.params) {\n            if !a.identical(b) {\n                return false;\n            }\n        }\n\n        // Check return type\n        if !self.return_type.identical(&other.return_type) {\n            return false;\n        }\n\n        // Type parameters must match in count (names don't matter for structural equality)\n        match (&self.type_params, &other.type_params) {\n            (Some(a), Some(b)) if a.len() == b.len() => {\n                for (tp_a, tp_b) in a.iter().zip(b) {\n                    if let (Some(c_a), Some(c_b)) = (&tp_a.constraint, &tp_b.constraint) {\n                        if !c_a.identical(c_b) {\n                            return false;\n                        }\n                    } else if tp_a.constraint.is_some() != tp_b.constraint.is_some() {\n                        return false;\n                    }\n                }\n                true\n            }\n            (None, None) => true,\n            _ => false,\n        }\n    }\n}\n\nimpl FunctionParam {\n    pub fn identical(&self, other: &FunctionParam) -> bool {\n        self.typ.identical(&other.typ) && self.optional == other.optional && self.rest == other.rest\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Hash)]\npub enum Custom {\n    /// A specification of how the type should be encoded on the wire.\n    WireSpec(WireSpec),\n    /// A Decimal type with arbitrary precision.\n    Decimal,\n}\n\nimpl Custom {\n    pub fn identical(&self, other: &Custom) -> bool {\n        match (self, other) {\n            (Custom::WireSpec(a), Custom::WireSpec(b)) => a.identical(b),\n            (Custom::Decimal, Custom::Decimal) => true,\n            _ => false,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Serialize, Hash)]\npub struct WireSpec {\n    /// What location in the request should be used for this field.\n    pub location: WireLocation,\n\n    /// The underlying type this is.\n    pub underlying: Box<Type>,\n\n    /// Whether this specifies a name override.\n    pub name_override: Option<String>,\n}\n\n#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq)]\npub enum WireLocation {\n    Query,\n    Header,\n    PubSubAttr,\n    Cookie,\n    HttpStatus,\n}\n\nimpl WireSpec {\n    pub fn identical(&self, other: &WireSpec) -> bool {\n        self.location == other.location\n            && self.name_override == other.name_override\n            && self.underlying.identical(&other.underlying)\n    }\n}\n\nimpl Type {\n    pub fn is_void(&self) -> bool {\n        matches!(self, Type::Basic(Basic::Void))\n    }\n}\n\nimpl Type {\n    pub fn identical(&self, other: &Type) -> bool {\n        match (self, other) {\n            (Type::Basic(a), Type::Basic(b)) => a == b,\n            (Type::Array(a), Type::Array(b)) => a.0.identical(&b.0),\n            (Type::Interface(a), Type::Interface(b)) => a.identical(b),\n            (Type::Union(a), Type::Union(b)) => {\n                a.types.iter().zip(&b.types).all(|(a, b)| a.identical(b))\n            }\n            (Type::Tuple(a), Type::Tuple(b)) => {\n                a.types.iter().zip(&b.types).all(|(a, b)| a.identical(b))\n            }\n            (Type::Literal(a), Type::Literal(b)) => a == b,\n            (Type::Class(a), Type::Class(b)) => a.identical(b),\n            (Type::Named(a), Type::Named(b)) => a.identical(b),\n            (Type::Optional(a), Type::Optional(b)) => a.0.identical(&b.0),\n            (Type::This(This), Type::This(This)) => true,\n            (Type::Generic(a), Type::Generic(b)) => a.identical(b),\n            (Type::Enum(a), Type::Enum(b)) => a.identical(b),\n            (Type::Custom(a), Type::Custom(b)) => a.identical(b),\n            (Type::Function(a), Type::Function(b)) => a.identical(b),\n            _ => false,\n        }\n    }\n\n    /// Returns a union type that merges `self` and `other`, if possible.\n    /// If the types cannot be merged, it returns None.\n    #[tracing::instrument(ret, level = \"trace\")]\n    pub(super) fn union_merge(&self, other: &Type) -> Option<Type> {\n        match (self, other) {\n            // 'any' and any type unify to 'any'.\n            (Type::Basic(Basic::Any), _) | (_, Type::Basic(Basic::Any)) => {\n                Some(Type::Basic(Basic::Any))\n            }\n\n            // Type literals unify with their basic type\n            (Type::Basic(basic), Type::Literal(lit)) | (Type::Literal(lit), Type::Basic(basic))\n                if *basic == lit.basic() =>\n            {\n                Some(Type::Basic(*basic))\n            }\n\n            // Unify validation.\n            (Type::Validation(a), Type::Validation(b)) => {\n                Some(Type::Validation(a.clone().or(b.clone())))\n            }\n            (Type::Validated(validated), Type::Validation(expr))\n            | (Type::Validation(expr), Type::Validated(validated)) => {\n                Some(Type::Validated(Validated {\n                    typ: validated.typ.to_owned(),\n                    expr: validated.expr.clone().or(expr.clone()),\n                }))\n            }\n\n            // Functions don't merge in unions\n            (Type::Function(_), Type::Function(_)) => None,\n\n            // TODO more rules?\n\n            // Identical types unify.\n            (this, other) if this.identical(other) => Some(this.clone()),\n\n            // Otherwise no unification is possible.\n            (_, _) => None,\n        }\n    }\n\n    pub(super) fn simplify_or_union(self, other: Type) -> Type {\n        match self.union_merge(&other) {\n            Some(typ) => typ,\n            None => Type::Union(Union {\n                types: vec![self, other],\n            }),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]\npub enum Basic {\n    Any,\n    String,\n    Boolean,\n    Number,\n    Object,\n    BigInt,\n    Date,\n    Symbol,\n    Undefined,\n    Null,\n    Void,\n    Unknown,\n    Never,\n}\n\nimpl Display for Basic {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let s: &'static str = match self {\n            Basic::Any => \"any\",\n            Basic::String => \"string\",\n            Basic::Boolean => \"boolean\",\n            Basic::Number => \"number\",\n            Basic::Object => \"object\",\n            Basic::BigInt => \"bigint\",\n            Basic::Date => \"Date\",\n            Basic::Symbol => \"symbol\",\n            Basic::Undefined => \"undefined\",\n            Basic::Null => \"null\",\n            Basic::Void => \"void\",\n            Basic::Unknown => \"unknown\",\n            Basic::Never => \"never\",\n        };\n        f.write_str(s)\n    }\n}\n\n#[derive(Debug, Clone, Serialize)]\npub enum Literal {\n    String(String),\n    Boolean(bool),\n    Number(f64),\n    BigInt(String),\n}\n\nimpl Literal {\n    pub fn basic(&self) -> Basic {\n        match self {\n            Literal::String(_) => Basic::String,\n            Literal::Boolean(_) => Basic::Boolean,\n            Literal::Number(_) => Basic::Number,\n            Literal::BigInt(_) => Basic::BigInt,\n        }\n    }\n}\n\nimpl PartialEq for Literal {\n    fn eq(&self, other: &Self) -> bool {\n        match (self, other) {\n            (Literal::String(a), Literal::String(b)) => a == b,\n            (Literal::Boolean(a), Literal::Boolean(b)) => a == b,\n            (Literal::Number(a), Literal::Number(b)) => a == b,\n            (Literal::BigInt(a), Literal::BigInt(b)) => a == b,\n            _ => false,\n        }\n    }\n}\n\n// Safe because the float literals don't include non-Eq values like NaN since they're literals.\nimpl Eq for Literal {}\n\nimpl Hash for Literal {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        fn integer_decode(val: f64) -> (u64, i16, i8) {\n            let bits: u64 = val.to_bits();\n            let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 };\n            let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16;\n            let mantissa = if exponent == 0 {\n                (bits & 0xfffffffffffff) << 1\n            } else {\n                (bits & 0xfffffffffffff) | 0x10000000000000\n            };\n\n            exponent -= 1023 + 52;\n            (mantissa, exponent, sign)\n        }\n\n        match self {\n            Literal::String(s) => s.hash(state),\n            Literal::Boolean(b) => b.hash(state),\n            Literal::Number(n) => {\n                self.hash(state);\n                integer_decode(*n).hash(state);\n            }\n            Literal::BigInt(s) => s.hash(state),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Interface {\n    /// Explicitly defined fields.\n    pub fields: Vec<InterfaceField>,\n\n    /// Set for index signature types, like `[key: string]: number`.\n    pub index: Option<(Box<Type>, Box<Type>)>,\n\n    /// Callable signatures, like `(a: number): string`.\n    /// Supports multiple overloaded signatures.\n    /// Each tuple contains (params, return_type).\n    pub call: Option<Vec<(Vec<Type>, Box<Type>)>>,\n}\n\nimpl Interface {\n    pub fn identical(&self, other: &Interface) -> bool {\n        if self.fields.len() != other.fields.len() {\n            return false;\n        }\n        if self.index.is_some() != other.index.is_some() {\n            return false;\n        }\n\n        // Collect the fields by name.\n        #[allow(clippy::mutable_key_type)]\n        let by_name = self\n            .fields\n            .iter()\n            .map(|f| (f.name.clone(), f))\n            .collect::<HashMap<_, _>>();\n\n        // Check that all fields in `other` are in `self`.\n        for field in &other.fields {\n            if let Some(self_field) = by_name.get(&field.name) {\n                if !self_field.identical(field) {\n                    return false;\n                }\n            } else {\n                return false;\n            }\n        }\n\n        // Compare index signatures.\n        if let (Some((self_key, self_value)), Some((other_key, other_value))) =\n            (&self.index, &other.index)\n        {\n            if !self_key.identical(other_key) || !self_value.identical(other_value) {\n                return false;\n            }\n        }\n\n        // Compare call signatures.\n        match (&self.call, &other.call) {\n            (Some(self_overloads), Some(other_overloads)) => {\n                if self_overloads.len() != other_overloads.len() {\n                    return false;\n                }\n                for ((self_params, self_ret), (other_params, other_ret)) in\n                    self_overloads.iter().zip(other_overloads)\n                {\n                    if self_params.len() != other_params.len() {\n                        return false;\n                    }\n                    for (a, b) in self_params.iter().zip(other_params) {\n                        if !a.identical(b) {\n                            return false;\n                        }\n                    }\n                    if !self_ret.as_ref().identical(other_ret.as_ref()) {\n                        return false;\n                    }\n                }\n            }\n            (None, None) => {}\n            _ => return false,\n        }\n\n        true\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize, Eq, PartialEq)]\npub enum FieldName {\n    String(String),\n    Symbol(Rc<Object>),\n}\n\nimpl FieldName {\n    pub fn eq_str(&self, str: &str) -> bool {\n        match self {\n            FieldName::String(s) => s == str,\n            FieldName::Symbol(_) => false,\n        }\n    }\n}\n\n#[derive(Clone, Hash, Serialize)]\npub struct InterfaceField {\n    pub range: Range,\n    pub name: FieldName,\n    pub optional: bool,\n    pub typ: Type,\n}\n\nimpl Debug for InterfaceField {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"InterfaceField\")\n            .field(\"name\", &self.name)\n            .field(\"optional\", &self.optional)\n            .field(\"typ\", &self.typ)\n            .finish()\n    }\n}\n\nimpl InterfaceField {\n    pub fn identical(&self, other: &InterfaceField) -> bool {\n        self.name == other.name && self.typ.identical(&other.typ) && self.optional == other.optional\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct ClassType {\n    pub methods: Vec<String>,\n    // TODO: include class fields here\n}\n\nimpl ClassType {\n    pub fn identical(&self, _other: &ClassType) -> bool {\n        todo!()\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize, Eq, PartialEq)]\npub struct EnumType {\n    pub members: Vec<EnumMember>,\n}\n\n#[derive(Debug, Clone, Hash, Serialize, Eq, PartialEq)]\npub struct EnumMember {\n    pub name: String,\n    pub value: EnumValue,\n}\n\n#[derive(Debug, Clone, Hash, Serialize, Eq, PartialEq)]\npub enum EnumValue {\n    String(String),\n    Number(i64),\n}\n\nimpl EnumValue {\n    pub fn to_literal(self) -> Literal {\n        match self {\n            EnumValue::String(s) => Literal::String(s),\n            EnumValue::Number(n) => Literal::Number(n as f64),\n        }\n    }\n\n    pub fn to_type(self) -> Type {\n        Type::Literal(self.to_literal())\n    }\n}\n\nimpl EnumType {\n    pub fn identical(&self, other: &EnumType) -> bool {\n        if self.members.len() != other.members.len() {\n            return false;\n        }\n        *self == *other\n    }\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct Named {\n    pub obj: Rc<object::Object>,\n    pub type_arguments: Vec<Type>,\n}\n\nimpl Hash for Named {\n    fn hash<H: Hasher>(&self, state: &mut H) {\n        self.obj.id.hash(state);\n        self.type_arguments.hash(state);\n    }\n}\n\nimpl Named {\n    pub fn new(obj: Rc<object::Object>, type_arguments: Vec<Type>) -> Self {\n        Self {\n            obj,\n            type_arguments,\n        }\n    }\n\n    pub fn identical(&self, other: &Named) -> bool {\n        if self.obj.id != other.obj.id || self.type_arguments.len() != other.type_arguments.len() {\n            return false;\n        }\n\n        for (a, b) in self.type_arguments.iter().zip(&other.type_arguments) {\n            if !a.identical(b) {\n                return false;\n            }\n        }\n        true\n    }\n\n    pub fn underlying(&self, state: &ResolveState) -> Type {\n        let ctx = Ctx::new(state, self.obj.module_id);\n        ctx.underlying_named(self)\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub enum Generic {\n    /// A reference to a generic type parameter.\n    TypeParam(TypeParam),\n\n    /// An index lookup, like `T[U]`, where at least one of the types is a generic.\n    Index(Index),\n\n    /// A mapped type.\n    Mapped(Mapped),\n\n    /// A reference to the 'key' type when evaluating a mapped type.\n    MappedKeyType(MappedKeyType),\n\n    Keyof(Keyof),\n    Conditional(Conditional),\n    // A reference to the 'as' type when evaluating a mapped type.\n    // MappedAsType,\n\n    // An intersection type.\n    Intersection(Intersection),\n\n    /// A reference to an inferred type parameter,\n    /// referencing its index in infer_type_params.\n    Inferred(Inferred),\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Index {\n    pub source: Box<Type>,\n    pub index: Box<Type>,\n}\n\n/// A reference to an inferred type parameter,\n/// referencing its index in infer_type_params.\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Inferred(pub usize);\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Keyof(pub Box<Type>);\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct This;\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct MappedKeyType;\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct TypeParam {\n    // The index of the type parameter in the current scope.\n    pub idx: usize,\n\n    // Any additional constraint on the type parameter.\n    // If provided, it can be assumed that the type parameter is assignable to this type.\n    pub constraint: Option<Box<Type>>,\n}\n\nimpl TypeParam {\n    pub fn identical(&self, other: &TypeParam) -> bool {\n        self.idx == other.idx\n            && match (self.constraint.as_ref(), other.constraint.as_ref()) {\n                (Some(a), Some(b)) => a.identical(b),\n                (None, None) => true,\n                _ => false,\n            }\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Mapped {\n    /// The type being evaluated to find property names.\n    /// Must be evaluated using the property name in the evaluation context.\n    pub in_type: Box<Type>,\n\n    /// The value of each property in the mapped type.\n    /// Must be evaluated using the property name in the evaluation context.\n    pub value_type: Box<Type>,\n\n    /// Whether to force fields to be optional (Some(True)), to make them required (Some(False)),\n    /// or to keep them as-is (None).\n    pub optional: Option<bool>,\n\n    /// Indicates a remapping of the property name.\n    /// Must be evaluated using the property name in the evaluation context.\n    pub as_type: Option<Box<Type>>,\n}\n\nimpl Mapped {\n    pub fn identical(&self, other: &Mapped) -> bool {\n        self.in_type.identical(&other.in_type)\n            && self.value_type.identical(&other.value_type)\n            && match (&self.as_type, &other.as_type) {\n                (Some(a), Some(b)) => a.identical(b),\n                (None, None) => true,\n                _ => false,\n            }\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Intersection {\n    pub x: Box<Type>,\n    pub y: Box<Type>,\n}\n\nimpl Intersection {\n    pub fn identical(&self, other: &Intersection) -> bool {\n        self.x.identical(&other.x) && self.y.identical(&other.y)\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize)]\npub struct Conditional {\n    pub check_type: Box<Type>,\n    pub extends_type: Box<Type>,\n    pub true_type: Box<Type>,\n    pub false_type: Box<Type>,\n}\n\nimpl Generic {\n    pub fn identical(&self, other: &Generic) -> bool {\n        match (self, other) {\n            (Generic::TypeParam(a), Generic::TypeParam(b)) => a.identical(b),\n            (Generic::Mapped(a), Generic::Mapped(b)) => a.identical(b),\n            _ => false,\n        }\n    }\n}\n\nimpl Type {\n    pub fn iter_unions<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Type> + 'a> {\n        match self {\n            Type::Union(union) => Box::new(union.types.iter().flat_map(|t| t.iter_unions())),\n            Type::Optional(tt) => Box::new(\n                tt.0.iter_unions()\n                    .chain(std::iter::once(&Type::Basic(Basic::Undefined))),\n            ),\n            Type::Validated(v) => v.typ.iter_unions(),\n            _ => Box::new(std::iter::once(self)),\n        }\n    }\n\n    pub fn into_iter_unions(self) -> Box<dyn Iterator<Item = Type>> {\n        match self {\n            Type::Union(union) => {\n                Box::new(union.types.into_iter().flat_map(|t| t.into_iter_unions()))\n            }\n            Type::Optional(tt) => Box::new(\n                tt.0.into_iter_unions()\n                    .chain(std::iter::once(Type::Basic(Basic::Undefined))),\n            ),\n            Type::Validated(v) => v.typ.into_iter_unions(),\n            _ => Box::new(std::iter::once(self)),\n        }\n    }\n}\n\nimpl Type {\n    /// Reports whether `self` is assignable to `other`.\n    /// If the result is indeterminate due to an unresolved type, it reports None.\n    pub fn assignable(&self, state: &ResolveState, other: &Type) -> Option<bool> {\n        match (self, other) {\n            (_, Type::Basic(Basic::Any)) => Some(true),\n            (_, Type::Basic(Basic::Never)) => Some(false),\n            (Type::Generic(_), _) | (_, Type::Generic(_)) => None,\n\n            // Unwrap named types.\n            (Type::Named(a), b) => {\n                let a = a.underlying(state);\n                a.assignable(state, b)\n            }\n            (a, Type::Named(b)) => {\n                let b = b.underlying(state);\n                a.assignable(state, &b)\n            }\n\n            (Type::Basic(a), Type::Basic(b)) => Some(a == b),\n            (Type::Literal(a), Type::Basic(b)) => Some(matches!(\n                (a, b),\n                (_, Basic::Any)\n                    | (Literal::String(_), Basic::String)\n                    | (Literal::Boolean(_), Basic::Boolean)\n                    | (Literal::Number(_), Basic::Number)\n                    | (Literal::BigInt(_), Basic::BigInt)\n            )),\n\n            (Type::Validated(v), _) | (_, Type::Validated(v)) => v.typ.assignable(state, other),\n\n            (this, Type::Optional(other)) => {\n                if matches!(this, Type::Basic(Basic::Undefined)) {\n                    Some(true)\n                } else {\n                    this.assignable(state, &other.0)\n                }\n            }\n\n            (Type::Tuple(this), other) => match other {\n                Type::Tuple(other) => {\n                    if this.types.len() != other.types.len() {\n                        return Some(false);\n                    }\n\n                    let mut found_none = false;\n                    for (this, other) in this.types.iter().zip(&other.types) {\n                        match this.assignable(state, other) {\n                            Some(true) => {}\n                            Some(false) => return Some(false),\n                            None => found_none = true,\n                        }\n                    }\n                    if found_none {\n                        None\n                    } else {\n                        Some(true)\n                    }\n                }\n\n                Type::Array(other) => {\n                    // Ensure every element in `this` is a subtype of `other`.\n                    for this in &this.types {\n                        match this.assignable(state, &other.0) {\n                            Some(true) => {}\n                            Some(false) => return Some(false),\n                            None => return None,\n                        }\n                    }\n                    Some(true)\n                }\n                _ => Some(false),\n            },\n\n            (Type::Enum(a), other) => {\n                let this_fields: HashMap<&str, &EnumValue> =\n                    HashMap::from_iter(a.members.iter().map(|m| (m.name.as_str(), &m.value)));\n                match other {\n                    Type::Enum(other) => {\n                        // Does every field in `other` exist in `this_fields`?\n                        for mem in &other.members {\n                            if let Some(this_field) = this_fields.get(mem.name.as_str()) {\n                                if **this_field == mem.value {\n                                    continue;\n                                }\n                            }\n                            return Some(false);\n                        }\n                        Some(true)\n                    }\n\n                    Type::Interface(other) => {\n                        // Does every field in `other` exist in `iface`?\n                        let mut found_none = false;\n                        for field in &other.fields {\n                            if let FieldName::String(name) = &field.name {\n                                if let Some(this_field) = this_fields.get(name.as_str()) {\n                                    let this_typ = (*this_field).clone().to_type();\n                                    match this_typ.assignable(state, &field.typ) {\n                                        Some(true) => continue,\n                                        Some(false) => return Some(false),\n                                        None => found_none = true,\n                                    }\n                                }\n                            }\n                        }\n                        if found_none {\n                            None\n                        } else {\n                            Some(true)\n                        }\n                    }\n                    _ => Some(false),\n                }\n            }\n\n            (Type::Interface(iface), other) => {\n                #[allow(clippy::mutable_key_type)]\n                let this_fields: HashMap<&FieldName, &InterfaceField> =\n                    HashMap::from_iter(iface.fields.iter().map(|f| (&f.name, f)));\n                match other {\n                    Type::Interface(other) => {\n                        // Does every field in `other` exist in `iface`?\n                        let mut found_none = false;\n                        for field in &other.fields {\n                            if let Some(this_field) = this_fields.get(&field.name) {\n                                match this_field.typ.assignable(state, &field.typ) {\n                                    Some(true) => {}\n                                    Some(false) => return Some(false),\n                                    None => found_none = true,\n                                }\n                            } else {\n                                return Some(false);\n                            }\n                        }\n                        if found_none {\n                            None\n                        } else {\n                            Some(true)\n                        }\n                    }\n                    _ => Some(false),\n                }\n            }\n\n            (this, Type::Union(other)) => {\n                // Is every element in `this` assignable to `other`?\n                'ThisLoop: for t in this.iter_unions() {\n                    let mut found_none = false;\n                    for o in &other.types {\n                        match t.assignable(state, o) {\n                            // Found a match; check the next element in `this`.\n                            Some(true) => continue 'ThisLoop,\n\n                            // Not a match; keep going.\n                            Some(false) => {}\n                            None => found_none = true,\n                        }\n                    }\n\n                    // Couldn't find any match\n                    return if found_none { None } else { Some(false) };\n                }\n\n                // All elements passed the test.\n                Some(true)\n            }\n\n            (a, b) => Some(a.identical(b)),\n        }\n    }\n}\n\npub enum Extends<'a> {\n    Yes(Vec<(usize, Cow<'a, Type>)>),\n    No,\n    Unknown,\n}\n\nimpl Extends<'_> {\n    pub fn into_static(self) -> Extends<'static> {\n        match self {\n            Extends::Yes(v) => Extends::Yes(\n                v.into_iter()\n                    .map(|(idx, t)| (idx, Cow::Owned(t.into_owned())))\n                    .collect(),\n            ),\n            Extends::No => Extends::No,\n            Extends::Unknown => Extends::Unknown,\n        }\n    }\n}\n\nimpl Type {\n    /// Reports whether `self` is assignable to `other`.\n    /// If the result is indeterminate due to an unresolved type, it reports None.\n    pub fn extends<'a>(&'a self, state: &'_ ResolveState, other: &'_ Type) -> Extends<'a> {\n        use Extends::*;\n\n        fn empty_yes_or_no(val: bool) -> Extends<'static> {\n            if val {\n                Yes(vec![])\n            } else {\n                No\n            }\n        }\n\n        match (self, other) {\n            (this, Type::Generic(Generic::Inferred(inferred))) => {\n                Yes(vec![(inferred.0, Cow::Borrowed(this))])\n            }\n\n            (_, Type::Basic(Basic::Any)) => Yes(vec![]),\n            (_, Type::Basic(Basic::Never)) => No,\n            (Type::Generic(_), _) | (_, Type::Generic(_)) => Unknown,\n\n            // Unwrap named types.\n            (Type::Named(a), b) => {\n                let a = a.underlying(state);\n                a.extends(state, b).into_static()\n            }\n            (a, Type::Named(b)) => {\n                let b = b.underlying(state);\n                a.extends(state, &b)\n            }\n\n            (Type::Basic(a), Type::Basic(b)) => empty_yes_or_no(a == b),\n            (Type::Literal(a), Type::Basic(b)) => empty_yes_or_no(matches!(\n                (a, b),\n                (_, Basic::Any)\n                    | (Literal::String(_), Basic::String)\n                    | (Literal::Boolean(_), Basic::Boolean)\n                    | (Literal::Number(_), Basic::Number)\n                    | (Literal::BigInt(_), Basic::BigInt)\n            )),\n\n            (Type::Validated(v), _) | (_, Type::Validated(v)) => {\n                v.typ.extends(state, other).into_static()\n            }\n\n            (this, Type::Optional(other)) => {\n                if matches!(this, Type::Basic(Basic::Undefined)) {\n                    Yes(vec![])\n                } else {\n                    this.extends(state, &other.0)\n                }\n            }\n\n            (Type::Tuple(this), other) => match other {\n                Type::Tuple(other) => {\n                    if this.types.len() != other.types.len() {\n                        return No;\n                    }\n\n                    let mut found_unknown = false;\n                    let mut inferred = vec![];\n                    for (this, other) in this.types.iter().zip(&other.types) {\n                        match this.extends(state, other) {\n                            Yes(inf) => {\n                                inferred.extend(inf);\n                            }\n                            No => return No,\n                            Unknown => found_unknown = true,\n                        }\n                    }\n                    if found_unknown {\n                        Unknown\n                    } else {\n                        Yes(inferred)\n                    }\n                }\n\n                Type::Array(other) => {\n                    // Ensure every element in `this` is a subtype of `other`.\n                    let mut inferred = vec![];\n                    for this in &this.types {\n                        match this.extends(state, &other.0) {\n                            Yes(infer) => inferred.extend(infer),\n                            No => return No,\n                            Unknown => return Unknown,\n                        }\n                    }\n\n                    // Since `this` is a tuple but `other` is a single array type,\n                    // it's possible we'll have multiple inferred types for the same index.\n                    // Group them by index and turn them into a union type if necessary.\n                    inferred.sort_by_key(|(idx, _)| *idx);\n\n                    let inferred = inferred\n                        .into_iter()\n                        .chunk_by(|(idx, _)| *idx)\n                        .into_iter()\n                        .map(|(idx, types)| {\n                            let types = types.map(|(_, t)| t.into_owned()).collect();\n                            let typ = simplify_union(types);\n                            (idx, Cow::Owned(typ))\n                        })\n                        .collect();\n\n                    Yes(inferred)\n                }\n                _ => No,\n            },\n\n            (Type::Enum(a), other) => {\n                let this_fields: HashMap<&str, &EnumValue> =\n                    HashMap::from_iter(a.members.iter().map(|m| (m.name.as_str(), &m.value)));\n                match other {\n                    Type::Enum(other) => {\n                        // Does every field in `other` exist in `this_fields`?\n                        for mem in &other.members {\n                            if let Some(this_field) = this_fields.get(mem.name.as_str()) {\n                                if **this_field == mem.value {\n                                    continue;\n                                }\n                            }\n                            return No;\n                        }\n                        Yes(vec![])\n                    }\n\n                    Type::Interface(other) => {\n                        // Does every field in `other` exist in `iface`?\n                        let mut found_none = false;\n                        for field in &other.fields {\n                            if let FieldName::String(name) = &field.name {\n                                if let Some(this_field) = this_fields.get(name.as_str()) {\n                                    let this_typ = (*this_field).clone().to_type();\n                                    match this_typ.assignable(state, &field.typ) {\n                                        Some(true) => continue,\n                                        Some(false) => return No,\n                                        None => found_none = true,\n                                    }\n                                }\n                            }\n                        }\n                        if found_none {\n                            Unknown\n                        } else {\n                            Yes(vec![])\n                        }\n                    }\n                    _ => No,\n                }\n            }\n\n            (Type::Interface(iface), other) => {\n                #[allow(clippy::mutable_key_type)]\n                let this_fields: HashMap<&FieldName, &InterfaceField> =\n                    HashMap::from_iter(iface.fields.iter().map(|f| (&f.name, f)));\n                match other {\n                    Type::Interface(other) => {\n                        // Does every field in `other` exist in `iface`?\n                        let mut found_unknown = false;\n                        let mut inferred = vec![];\n                        for field in &other.fields {\n                            if let Some(this_field) = this_fields.get(&field.name) {\n                                match this_field.typ.extends(state, &field.typ) {\n                                    Yes(inf) => inferred.extend(inf),\n                                    No => return No,\n                                    Unknown => found_unknown = true,\n                                }\n                            } else {\n                                return No;\n                            }\n                        }\n                        if found_unknown {\n                            Unknown\n                        } else {\n                            Yes(inferred)\n                        }\n                    }\n                    _ => No,\n                }\n            }\n\n            (this, Type::Union(other)) => {\n                // Is every element in `this` assignable to `other`?\n                let mut found_yes = false;\n                let mut found_unknown = false;\n                let mut inferred = Vec::new();\n                for t in this.iter_unions() {\n                    for o in &other.types {\n                        match t.extends(state, o) {\n                            Yes(inf) => {\n                                found_yes = true;\n                                inferred.extend(inf);\n                            }\n                            No => {}\n                            Unknown => found_unknown = true,\n                        }\n                    }\n                }\n\n                if found_yes {\n                    Yes(inferred)\n                } else if found_unknown {\n                    Unknown\n                } else {\n                    No\n                }\n            }\n\n            (a, b) => empty_yes_or_no(a.identical(b)),\n        }\n    }\n}\n\npub fn simplify_union(types: Vec<Type>) -> Type {\n    let mut results: Vec<Type> = Vec::with_capacity(types.len());\n\n    for typ in types {\n        // Ignore `never` in unions.\n        if matches!(typ, Type::Basic(Basic::Never)) {\n            continue;\n        }\n\n        let mut found = false;\n        for unified_typ in &mut results {\n            match unified_typ.union_merge(&typ) {\n                Some(u) => {\n                    *unified_typ = u;\n                    found = true;\n                    break;\n                }\n                None => {\n                    // No unification possible; keep going.\n                }\n            }\n        }\n\n        if !found {\n            results.push(typ);\n        }\n    }\n\n    match results.len() {\n        0 => Type::Basic(Basic::Never),\n        1 => results.remove(0),\n        _ => Type::Union(Union { types: results }),\n    }\n}\n\n/// Computes (a & b), the intersection of two types.\n#[tracing::instrument(level = \"trace\", skip(ctx), ret)]\npub fn intersect<'a: 'b, 'b>(\n    ctx: &'b Ctx<'a>,\n    a: Cow<'a, Type>,\n    b: Cow<'a, Type>,\n) -> Cow<'a, Type> {\n    let union_with = |a: Cow<'_, Type>, b: Cow<'_, Type>| {\n        let result = a\n            .into_owned()\n            .into_iter_unions()\n            .filter_map(\n                |typ| match intersect(ctx, Cow::Owned(typ), b.clone()).into_owned() {\n                    Type::Basic(Basic::Never) => None,\n                    other => Some(other),\n                },\n            )\n            .collect();\n        Cow::Owned(simplify_union(result))\n    };\n    let literal = |lit: &Literal, b: &Basic| -> bool {\n        matches!(\n            (lit, b),\n            (\n                Literal::String(_),\n                Basic::String | Basic::Any | Basic::Unknown\n            ) | (\n                Literal::Boolean(_),\n                Basic::Boolean | Basic::Any | Basic::Unknown\n            ) | (\n                Literal::Number(_),\n                Basic::Number | Basic::Any | Basic::Unknown\n            ) | (\n                Literal::BigInt(_),\n                Basic::BigInt | Basic::Any | Basic::Unknown\n            )\n        )\n    };\n\n    match (a.as_ref(), b.as_ref()) {\n        // T & unknown == T\n        (Type::Basic(Basic::Unknown), _) => b,\n        (_, Type::Basic(Basic::Unknown)) => a,\n\n        // T & any == any\n        (Type::Basic(Basic::Any), _) | (_, Type::Basic(Basic::Any)) => {\n            Cow::Owned(Type::Basic(Basic::Any))\n        }\n\n        // T & never == never\n        (Type::Basic(Basic::Never), _) | (_, Type::Basic(Basic::Never)) => {\n            Cow::Owned(Type::Basic(Basic::Never))\n        }\n\n        (Type::Basic(a), Type::Basic(b)) => {\n            if a == b {\n                Cow::Owned(Type::Basic(*a))\n            } else {\n                Cow::Owned(Type::Basic(Basic::Never))\n            }\n        }\n\n        // Intersection distributes into unions.\n        (Type::Union(a), Type::Union(b)) => {\n            let mut types = Vec::with_capacity(a.types.len() * b.types.len());\n            for typ in &a.types {\n                for other in &b.types {\n                    match intersect(ctx, Cow::Borrowed(typ), Cow::Borrowed(other)).into_owned() {\n                        Type::Basic(Basic::Never) => {}\n                        other => types.push(other),\n                    }\n                }\n            }\n            Cow::Owned(simplify_union(types))\n        }\n        (Type::Union(_), _) => union_with(a, b),\n        (_, Type::Union(_)) => union_with(b, a),\n\n        (Type::Literal(x), Type::Literal(y)) if x == y => a,\n        (Type::Literal(lit), Type::Basic(x)) => {\n            if literal(lit, x) {\n                a\n            } else {\n                Cow::Owned(Type::Basic(Basic::Never))\n            }\n        }\n        (Type::Basic(x), Type::Literal(lit)) => {\n            if literal(lit, x) {\n                b\n            } else {\n                Cow::Owned(Type::Basic(Basic::Never))\n            }\n        }\n\n        (Type::Array(x), Type::Array(y)) => Cow::Owned(Type::Array(Array(Box::new(\n            intersect(\n                ctx,\n                Cow::Borrowed(x.0.as_ref()),\n                Cow::Borrowed(y.0.as_ref()),\n            )\n            .into_owned(),\n        )))),\n        (Type::Array(x), Type::Tuple(y)) | (Type::Tuple(y), Type::Array(x)) => {\n            Cow::Owned(Type::Array(Array(Box::new(if y.types.is_empty() {\n                Type::Basic(Basic::Never)\n            } else {\n                // Inspect the first element of the tuple for intersection.\n                // It's not completely correct but close enough for now.\n                intersect(ctx, Cow::Borrowed(x.0.as_ref()), Cow::Borrowed(&y.types[0])).into_owned()\n            }))))\n        }\n\n        (Type::Tuple(x), Type::Tuple(y)) => {\n            let mut types = Vec::with_capacity(x.types.len().min(y.types.len()));\n            for (a, b) in x.types.iter().zip(y.types.iter()) {\n                types.push(intersect(ctx, Cow::Borrowed(a), Cow::Borrowed(b)).into_owned());\n            }\n            Cow::Owned(Type::Tuple(Tuple { types }))\n        }\n\n        (Type::Optional(x), Type::Optional(y)) => Cow::Owned(Type::Optional(Optional(Box::new(\n            intersect(ctx, Cow::Borrowed(&x.0), Cow::Borrowed(&y.0)).into_owned(),\n        )))),\n        // Treat optional as \"T | undefined\".\n        (Type::Optional(x), y) | (y, Type::Optional(x)) => {\n            union_with(Cow::Borrowed(&x.0), Cow::Borrowed(y))\n        }\n\n        (Type::This(This), Type::This(This)) => Cow::Owned(Type::This(This)),\n\n        // Combine validation expressions into a validated type.\n        (Type::Validated(a), Type::Validation(b)) => Cow::Owned(Type::Validated(Validated {\n            typ: a.typ.clone(),\n            expr: a.expr.clone().and(b.clone()),\n        })),\n        (Type::Validation(a), Type::Validated(b)) => Cow::Owned(Type::Validated(Validated {\n            typ: b.typ.clone(),\n            expr: a.clone().and(b.expr.clone()),\n        })),\n\n        // Merge validation expressions together.\n        (Type::Validation(a), Type::Validation(b)) => {\n            Cow::Owned(Type::Validation(a.clone().and(b.clone())))\n        }\n        (_, Type::Validation(expr)) => Cow::Owned(Type::Validated(Validated {\n            typ: Box::new(a.into_owned()),\n            expr: expr.clone(),\n        })),\n        (Type::Validation(expr), _) => Cow::Owned(Type::Validated(Validated {\n            typ: Box::new(b.into_owned()),\n            expr: expr.clone(),\n        })),\n\n        (Type::Generic(_), _) | (_, Type::Generic(_)) => {\n            Cow::Owned(Type::Generic(Generic::Intersection(Intersection {\n                x: Box::new(a.into_owned()),\n                y: Box::new(b.into_owned()),\n            })))\n        }\n\n        (Type::Class(_), Type::Class(_)) => {\n            Cow::Owned(Type::Generic(Generic::Intersection(Intersection {\n                x: Box::new(a.into_owned()),\n                y: Box::new(b.into_owned()),\n            })))\n        }\n\n        (Type::Named(x), _) => {\n            let x = ctx.underlying_named(x);\n            intersect(ctx, Cow::Owned(x), b)\n        }\n        (_, Type::Named(y)) => {\n            let y = ctx.underlying_named(y);\n            intersect(ctx, a, Cow::Owned(y))\n        }\n\n        (Type::Interface(_), Type::Interface(_)) => {\n            let Type::Interface(x) = a.into_owned() else {\n                unreachable!();\n            };\n            let Type::Interface(y) = b.into_owned() else {\n                unreachable!();\n            };\n\n            let fields = {\n                let mut y_fields = y\n                    .fields\n                    .into_iter()\n                    .map(|f| (f.name.clone(), Some(f)))\n                    .collect::<IndexMap<_, _>>();\n\n                // Fields are added together.\n                // If they have the same name, the type is the intersection.\n                let mut result = Vec::with_capacity(x.fields.len() + y_fields.len());\n\n                for mut field in x.fields {\n                    if let Some(other) = y_fields.get_mut(&field.name) {\n                        // Intersect the type.\n                        let other = other.take().expect(\"field name should not appear twice\");\n                        field.typ = intersect(ctx, Cow::Owned(field.typ), Cow::Owned(other.typ))\n                            .into_owned();\n                        field.optional = field.optional && other.optional;\n                    }\n                    result.push(field);\n                }\n\n                // Add any remaining fields from `y`.\n                for (_, other) in y_fields {\n                    if let Some(other) = other {\n                        result.push(other);\n                    }\n                }\n                result\n            };\n\n            // If we have any fields, ignore the index signature.\n            let index = if fields.is_empty() {\n                None\n            } else {\n                match (x.index, y.index) {\n                    (Some((x_key, x_value)), Some((y_key, y_value))) => {\n                        let key =\n                            intersect(ctx, Cow::Owned(*x_key), Cow::Owned(*y_key)).into_owned();\n                        let value =\n                            intersect(ctx, Cow::Owned(*x_value), Cow::Owned(*y_value)).into_owned();\n                        Some((Box::new(key), Box::new(value)))\n                    }\n                    (Some((k, v)), None) | (None, Some((k, v))) => Some((k, v)),\n                    (None, None) => None,\n                }\n            };\n\n            if x.call.is_some() || y.call.is_some() {\n                HANDLER.with(|handler| {\n                    handler.err(\"intersection of call signature types not yet supported\");\n                })\n            }\n\n            Cow::Owned(Type::Interface(Interface {\n                fields,\n                index,\n                call: None,\n            }))\n        }\n\n        (Type::Interface(_), _) | (_, Type::Interface(_)) => {\n            Cow::Owned(Type::Generic(Generic::Intersection(Intersection {\n                x: Box::new(a.into_owned()),\n                y: Box::new(b.into_owned()),\n            })))\n        }\n\n        (_, _) => Cow::Owned(Type::Basic(Basic::Never)),\n    }\n}\n\npub fn unwrap_validated(typ: &Type) -> (&Type, Option<&validation::Expr>) {\n    match typ {\n        Type::Validated(validated) => (&validated.typ, Some(&validated.expr)),\n        _ => (typ, None),\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/type_resolve.rs",
    "content": "use std::borrow::Cow;\nuse std::borrow::Cow::{Borrowed, Owned};\nuse std::cell::RefCell;\nuse std::fmt::Debug;\nuse std::ops::Deref;\nuse std::rc::Rc;\n\nuse litparser::Sp;\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\nuse swc_common::{BytePos, Span, Spanned};\nuse swc_ecma_ast::{self as ast, TsTypeParam};\n\nuse crate::parser::module_loader::ModuleId;\nuse crate::parser::types::object::{CheckState, ObjectKind, ResolveState, TypeNameDecl};\nuse crate::parser::types::{validation, Object};\nuse crate::parser::{module_loader, Range};\nuse crate::span_err::ErrReporter;\n\nuse super::resolved::{Resolved, Resolved::*};\nuse super::typ::*;\n\n#[derive(Debug)]\npub struct TypeChecker {\n    ctx: ResolveState,\n}\n\nimpl TypeChecker {\n    pub fn new(loader: Lrc<module_loader::ModuleLoader>) -> Self {\n        Self {\n            ctx: ResolveState::new(loader),\n        }\n    }\n\n    pub fn state(&self) -> &ResolveState {\n        &self.ctx\n    }\n\n    pub fn resolve_type(&self, module: Lrc<module_loader::Module>, expr: &ast::TsType) -> Sp<Type> {\n        // Ensure the module is initialized.\n        let module_id = module.id;\n        _ = self.ctx.get_or_init_module(module);\n\n        let ctx = Ctx::new(&self.ctx, module_id);\n        let typ = ctx.typ(expr);\n        Sp::new(\n            expr.span(),\n            match ctx.concrete(&typ) {\n                New(typ) => typ,\n                Changed(typ) => typ.clone(),\n                Same(_) => typ,\n            },\n        )\n    }\n\n    pub fn concrete(&self, module_id: ModuleId, typ: &Type) -> Type {\n        // Ensure the module is initialized.\n        let ctx = Ctx::new(&self.ctx, module_id);\n        ctx.concrete(typ).into_owned()\n    }\n\n    pub fn underlying(&self, module_id: ModuleId, typ: &Type) -> Type {\n        let ctx = Ctx::new(&self.ctx, module_id);\n        ctx.underlying(typ).into_owned()\n    }\n\n    pub fn resolve_obj(\n        &self,\n        module: Lrc<module_loader::Module>,\n        expr: &ast::Expr,\n    ) -> Option<Rc<Object>> {\n        // Ensure the module is initialized.\n        let module_id = module.id;\n        _ = self.ctx.get_or_init_module(module);\n\n        let ctx = Ctx::new(&self.ctx, module_id);\n        ctx.resolve_obj(expr)\n    }\n\n    pub fn resolve_obj_type(&self, obj: &Object) -> Type {\n        let ctx = Ctx::new(&self.ctx, obj.module_id);\n        ctx.obj_type(obj)\n    }\n\n    pub fn resolve_default_export(&self, module: Lrc<module_loader::Module>) -> Option<Rc<Object>> {\n        // Ensure the module is initialized.\n        let module_id = module.id;\n        _ = self.ctx.get_or_init_module(module);\n\n        let ctx = Ctx::new(&self.ctx, module_id);\n        ctx.resolve_default_export()\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Ctx<'a> {\n    state: &'a ResolveState,\n\n    /// The current module being resolved.\n    module: ModuleId,\n\n    /// The type parameters in the current type resolution scope.\n    type_params: &'a [&'a ast::TsTypeParam],\n\n    /// The type arguments in the current type resolution scope.\n    type_args: &'a [Type],\n\n    /// Context for the current mapped type being processed, if any.\n    mapped_key_id: Option<ast::Id>,\n\n    /// The mapped key type to substitute when concretising, if any.\n    mapped_key_type: Option<&'a Type>,\n\n    /// Encountered \"infer Type\" type parameters in the current scope.\n    /// Rc<RefCell<...>> so we can mutate it in nested contexts.\n    infer_type_params: Option<Rc<RefCell<Vec<ast::Id>>>>,\n\n    /// Type arguments to fill in for inferred type parameters.\n    infer_type_args: &'a [Cow<'a, Type>],\n}\n\nimpl<'a> Ctx<'a> {\n    pub fn new(state: &'a ResolveState, module: ModuleId) -> Self {\n        Self {\n            state,\n            module,\n            type_params: &[],\n            type_args: &[],\n            mapped_key_id: None,\n            mapped_key_type: None,\n            infer_type_params: None,\n            infer_type_args: &[],\n        }\n    }\n\n    fn with_type_params(self, type_params: &'a [&'a ast::TsTypeParam]) -> Self {\n        Self {\n            type_params,\n            ..self\n        }\n    }\n\n    fn with_type_args(self, type_args: &'a [Type]) -> Self {\n        Self { type_args, ..self }\n    }\n\n    fn with_mapped_key_id(self, mapped_key_id: Option<ast::Id>) -> Self {\n        Self {\n            mapped_key_id,\n            ..self\n        }\n    }\n\n    fn with_mapped_key_type(self, mapped_key_type: Option<&'a Type>) -> Self {\n        Self {\n            mapped_key_type,\n            ..self\n        }\n    }\n\n    fn with_infer_type_params(self, infer_type_params: Rc<RefCell<Vec<ast::Id>>>) -> Self {\n        Self {\n            infer_type_params: Some(infer_type_params),\n            ..self\n        }\n    }\n\n    fn with_infer_type_args(self, infer_type_args: &'a [Cow<'a, Type>]) -> Self {\n        Self {\n            infer_type_args,\n            ..self\n        }\n    }\n}\n\nimpl Ctx<'_> {\n    pub fn typ(&self, typ: &ast::TsType) -> Type {\n        match typ {\n            ast::TsType::TsKeywordType(tt) => self.keyword(tt),\n            ast::TsType::TsThisType(_) => Type::This(This),\n            ast::TsType::TsArrayType(tt) => self.array(tt),\n            ast::TsType::TsTupleType(tt) => self.tuple(tt),\n            ast::TsType::TsUnionOrIntersectionType(ast::TsUnionOrIntersectionType::TsUnionType(tt)) => self.union(tt),\n            ast::TsType::TsUnionOrIntersectionType(ast::TsUnionOrIntersectionType::TsIntersectionType(tt)) => self.intersection(tt),\n            ast::TsType::TsParenthesizedType(tt) => self.typ(&tt.type_ann),\n            ast::TsType::TsTypeLit(tt) => self.type_lit(tt),\n            ast::TsType::TsTypeRef(tt) => self.type_ref(tt),\n            ast::TsType::TsOptionalType(tt) => self.optional(tt),\n            ast::TsType::TsTypeQuery(tt) => self.type_query(tt),\n\n            ast::TsType::TsConditionalType(tt) => self.conditional(tt),\n            ast::TsType::TsLitType(tt) => self.lit_type(tt),\n            ast::TsType::TsTypeOperator(tt) => self.type_op(tt),\n            ast::TsType::TsMappedType(tt) => self.mapped(tt),\n            ast::TsType::TsIndexedAccessType(tt) => self.indexed_access(tt),\n            ast::TsType::TsInferType(tt) => self.infer(tt),\n\n            ast::TsType::TsFnOrConstructorType(_)\n            | ast::TsType::TsRestType(_) // same?\n            | ast::TsType::TsTypePredicate(_) // https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates, https://www.typescriptlang.org/docs/handbook/2/classes.html#this-based-type-guards\n            | ast::TsType::TsImportType(_) // ??\n            => {\n                HANDLER.with(|handler| handler.span_err(typ.span(), &format!(\"unsupported: {typ:#?}\")));\n                Type::Basic(Basic::Never)\n            }, // typeof\n        }\n    }\n\n    pub fn types<'b, I: IntoIterator<Item = &'b ast::TsType>>(&self, types: I) -> Vec<Type> {\n        types.into_iter().map(|t| self.typ(t)).collect()\n    }\n\n    pub fn btyp(&self, typ: &ast::TsType) -> Box<Type> {\n        Box::new(self.typ(typ))\n    }\n\n    /// Resolves keyof, unique, readonly, etc.\n    fn type_op(&self, tt: &ast::TsTypeOperator) -> Type {\n        let underlying = self.typ(&tt.type_ann);\n        match tt.op {\n            ast::TsTypeOperatorOp::ReadOnly => underlying,\n            ast::TsTypeOperatorOp::Unique => underlying,\n            ast::TsTypeOperatorOp::KeyOf => self.keyof(&underlying),\n        }\n    }\n\n    /// Resolves a mapped type, which represents another type being modified.\n    /// https://www.typescriptlang.org/docs/handbook/2/mapped-types.html\n    fn mapped(&self, tt: &ast::TsMappedType) -> Type {\n        // [K in keyof T]: T[K]\n\n        let Some(value_type) = &tt.type_ann else {\n            HANDLER.with(|handler| handler.span_err(tt.span, \"missing value type annotation\"));\n            return Type::Basic(Basic::Never);\n        };\n\n        let Some(in_type) = &tt.type_param.constraint else {\n            HANDLER.with(|handler| handler.span_err(tt.span, \"missing 'in' type annotation\"));\n            return Type::Basic(Basic::Never);\n        };\n\n        // First parse the \"in\" type.\n        let in_type = self.btyp(in_type);\n\n        // Next, introduce a nested ctx that introduces the \"K\" mapped type parameter.\n        let nested = self\n            .clone()\n            // .with_type_args(&[])\n            .with_mapped_key_id(Some(tt.type_param.name.to_id()));\n\n        // Next, parse the value type.\n        let value_type = nested.btyp(value_type);\n\n        // Parse the 'as' type annotation if present.\n        let as_type = tt.name_type.as_ref().map(|nt| nested.btyp(nt));\n\n        let optional = match tt.optional {\n            None => None,\n            Some(ast::TruePlusMinus::Plus | ast::TruePlusMinus::True) => Some(true),\n            Some(ast::TruePlusMinus::Minus) => Some(false),\n        };\n\n        Type::Generic(Generic::Mapped(Mapped {\n            in_type,\n            value_type,\n            optional,\n            as_type,\n        }))\n    }\n\n    // https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html#handbook-content\n    fn indexed_access(&self, tt: &ast::TsIndexedAccessType) -> Type {\n        let obj = self.typ(&tt.obj_type);\n        let idx = self.typ(&tt.index_type);\n        self.type_index(tt.span, &obj, &idx)\n    }\n\n    fn type_index(&self, span: Span, obj: &Type, idx: &Type) -> Type {\n        match (obj, idx) {\n            // If either obj or index is a generic type, we need to store it as a Generic::Index.\n            pair @ ((Type::Generic(_), _) | (_, Type::Generic(_))) => {\n                Type::Generic(Generic::Index(Index {\n                    source: Box::new(pair.0.clone()),\n                    index: Box::new(pair.1.clone()),\n                }))\n            }\n\n            (Type::Named(named), idx) => {\n                let underlying = named.underlying(self.state);\n                self.type_index(span, &underlying, idx)\n            }\n\n            (obj, Type::Named(idx)) => {\n                let underlying = idx.underlying(self.state);\n                self.type_index(span, obj, &underlying)\n            }\n\n            // Otherwise, look up the concrete result.\n            (Type::Interface(iface), idx) => match idx {\n                Type::Literal(Literal::String(s)) => iface\n                    .fields\n                    .iter()\n                    .find(|f| match &f.name {\n                        FieldName::String(name) => *name == *s,\n                        FieldName::Symbol(_) => false,\n                    })\n                    .map_or(Type::Basic(Basic::Never), |f| {\n                        let typ = f.typ.clone();\n                        // If the field is optional, wrap the type in Optional.\n                        if f.optional {\n                            Type::Optional(Optional(Box::new(typ)))\n                        } else {\n                            typ\n                        }\n                    }),\n\n                Type::Basic(Basic::String | Basic::Number) => iface\n                    .index\n                    .as_ref()\n                    .map_or(Type::Basic(Basic::Never), |(_, value)| *value.clone()),\n\n                Type::Union(union) => {\n                    let mut types = vec![];\n                    for index_type in union.types.clone() {\n                        match index_type {\n                            Type::Literal(Literal::String(ref str)) => {\n                                for field in &iface.fields {\n                                    if field.name.eq_str(str) {\n                                        types.push(field.typ.clone());\n                                        break;\n                                    }\n                                }\n                            }\n                            _ => {\n                                HANDLER.with(|handler| {\n                                    handler.span_err(\n                                        span,\n                                        \"only string literals supported when using index access with union\",\n                                    )\n                                });\n                                return Type::Basic(Basic::Never);\n                            }\n                        }\n                    }\n\n                    Type::Union(Union { types })\n                }\n                _ => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(span, \"unsupported index access type operation\")\n                    });\n                    Type::Basic(Basic::Never)\n                }\n            },\n\n            (Type::Validated(v), idx) => {\n                let typ = self.type_index(span, &v.typ, idx);\n                Type::Validated(Validated {\n                    typ: Box::new(typ),\n                    expr: v.expr.clone(),\n                })\n            }\n\n            (obj, idx) => {\n                HANDLER.with(|handler| {\n                    handler.span_err(\n                        span,\n                        &format!(\n                            \"unsupported indexed access type operation: obj {obj:#?} index {idx:#?}\"\n                        ),\n                    )\n                });\n                Type::Basic(Basic::Never)\n            }\n        }\n    }\n\n    fn infer(&self, tt: &ast::TsInferType) -> Type {\n        // Do we have an infer context?\n        if let Some(params) = self.infer_type_params.as_ref() {\n            let id = tt.type_param.name.to_id();\n            let mut params = params.borrow_mut();\n            let idx = params.len();\n            params.push(id);\n            Type::Generic(Generic::Inferred(Inferred(idx)))\n        } else {\n            tt.span.err(\"infer type outside of infer context\");\n            Type::Basic(Basic::Never)\n        }\n\n        // TODO figure out what type to return here\n    }\n\n    /// Given a type, produces a union type of the underlying keys,\n    /// e.g. `keyof {foo: string; bar: number}` yields `\"foo\" | \"bar\"`.\n    fn keyof(&self, typ: &Type) -> Type {\n        match typ {\n            Type::Basic(tt) => match tt {\n                Basic::Any => Type::Union(Union {\n                    types: vec![\n                        Type::Basic(Basic::String),\n                        Type::Basic(Basic::Number),\n                        Type::Basic(Basic::Symbol),\n                    ],\n                }),\n\n                // These should technically enumerate the built-in properties\n                // on these types, but we haven't implemented that yet.\n                Basic::String | Basic::Boolean | Basic::Number | Basic::BigInt | Basic::Symbol => {\n                    Type::Union(Union { types: vec![] })\n                }\n\n                // keyof these yields never.\n                Basic::Object\n                | Basic::Undefined\n                | Basic::Null\n                | Basic::Void\n                | Basic::Date\n                | Basic::Unknown\n                | Basic::Never => Type::Basic(Basic::Never),\n            },\n\n            Type::Enum(tt) => Type::Union(Union {\n                types: tt\n                    .members\n                    .iter()\n                    .map(|m| Type::Literal(Literal::String(m.name.clone())))\n                    .collect(),\n            }),\n\n            // These should technically enumerate the built-in properties\n            // on these types, but we haven't implemented that yet.\n            Type::Array(_) | Type::Tuple(_) => Type::Union(Union { types: vec![] }),\n\n            Type::Interface(interface) => {\n                let keys = interface\n                    .fields\n                    .iter()\n                    .filter_map(|f| match &f.name {\n                        FieldName::String(name) => {\n                            Some(Type::Literal(Literal::String(name.clone())))\n                        }\n                        FieldName::Symbol(_) => None,\n                    })\n                    .collect();\n                Type::Union(Union { types: keys })\n            }\n\n            Type::Named(_) => {\n                let underlying = self.underlying(typ);\n                self.keyof(&underlying)\n            }\n\n            Type::Class(_) => {\n                HANDLER.with(|handler| handler.err(\"keyof ClassType not yet supported\"));\n                Type::Basic(Basic::Never)\n            }\n\n            Type::Optional(typ) => self.keyof(&typ.0),\n            Type::Union(union) => {\n                let res: Vec<_> = union.types.iter().map(|t| self.keyof(t)).collect();\n                Type::Union(Union { types: res })\n            }\n\n            // keyof \"blah\" is the same as keyof string, which should yield all properties.\n            Type::Literal(_) => Type::Union(Union { types: vec![] }),\n\n            Type::This(This) => Type::Basic(Basic::Never),\n\n            Type::Generic(generic) => Type::Generic(Generic::Keyof(Keyof(Box::new(\n                Type::Generic(generic.clone()),\n            )))),\n            Type::Validated(v) => self.keyof(&v.typ),\n            Type::Validation(_) => {\n                HANDLER.with(|handler| handler.err(\"keyof ValidationExpr unsupported\"));\n                Type::Basic(Basic::Never)\n            }\n\n            Type::Custom(Custom::WireSpec(spec)) => self.keyof(&spec.underlying),\n            Type::Custom(Custom::Decimal) => Type::Basic(Basic::Never),\n            Type::Function(_) => Type::Basic(Basic::Never),\n        }\n    }\n\n    /// Resolves the typeof operator.\n    fn type_query(&self, typ: &ast::TsTypeQuery) -> Type {\n        if typ.type_args.is_some() {\n            HANDLER.with(|handler| {\n                handler.span_err(typ.span, \"typeof with type args not yet supported\")\n            });\n            return Type::Basic(Basic::Never);\n        }\n\n        match &typ.expr_name {\n            ast::TsTypeQueryExpr::TsEntityName(ast::TsEntityName::Ident(ident)) => {\n                let obj = self.ident_obj(ident);\n                if let Some(obj) = obj {\n                    self.obj_type(&obj)\n                } else {\n                    HANDLER.with(|handler| handler.span_err(ident.span, \"unknown identifier\"));\n                    Type::Basic(Basic::Never)\n                }\n            }\n            ast::TsTypeQueryExpr::TsEntityName(qn @ ast::TsEntityName::TsQualifiedName(_)) => {\n                // Helper function to recursively resolve qualified names to their type\n                fn resolve_qualified_type(ctx: &Ctx, entity: &ast::TsEntityName) -> Type {\n                    match entity {\n                        ast::TsEntityName::Ident(ident) => {\n                            // Base case: simple identifier\n                            if let Some(obj) = ctx.ident_obj(ident) {\n                                ctx.obj_type(&obj)\n                            } else {\n                                HANDLER.with(|handler| {\n                                    handler.span_err(ident.span, \"unknown identifier\")\n                                });\n                                Type::Basic(Basic::Never)\n                            }\n                        }\n                        ast::TsEntityName::TsQualifiedName(qn) => {\n                            // resolve the left side first\n                            let base_type = resolve_qualified_type(ctx, &qn.left);\n\n                            // Then access the right side property on that type\n                            let prop = ast::MemberProp::Ident(qn.right.clone());\n                            ctx.resolve_member_prop(&base_type, &prop)\n                        }\n                    }\n                }\n\n                resolve_qualified_type(self, qn)\n            }\n            _ => {\n                HANDLER.with(|handler| {\n                    handler.span_err(typ.span, \"typeof with non-ident not yet supported\")\n                });\n                Type::Basic(Basic::Never)\n            }\n        }\n    }\n\n    fn type_lit(&self, type_lit: &ast::TsTypeLit) -> Type {\n        let mut fields: Vec<InterfaceField> = Vec::with_capacity(type_lit.members.len());\n        let mut index = None;\n        let mut call_signatures: Vec<(Vec<Type>, Box<Type>)> = Vec::new();\n        for m in &type_lit.members {\n            match m {\n                ast::TsTypeElement::TsPropertySignature(p) => {\n                    let name = match *p.key {\n                        ast::Expr::Ident(ref i) => FieldName::String(i.sym.as_ref().to_string()),\n                        ast::Expr::Lit(ast::Lit::Str(ref str)) => {\n                            FieldName::String(str.value.to_string())\n                        }\n                        _ => {\n                            HANDLER.with(|handler| {\n                                handler.span_err(p.key.span(), \"unsupported property key\")\n                            });\n                            continue;\n                        }\n                    };\n\n                    if let Some(type_params) = &p.type_params {\n                        HANDLER.with(|handler| {\n                            handler.span_err(type_params.span(), \"unsupported type parameters\")\n                        });\n                        continue;\n                    }\n                    if p.type_ann.is_none() {\n                        HANDLER.with(|handler| {\n                            handler.span_err(p.span(), \"unsupported missing type annotation\")\n                        });\n                        continue;\n                    }\n\n                    let typ = self.typ(p.type_ann.as_ref().unwrap().type_ann.as_ref());\n                    fields.push(InterfaceField {\n                        range: m.span().into(),\n                        name,\n                        typ,\n                        optional: p.optional,\n                    });\n                }\n\n                ast::TsTypeElement::TsIndexSignature(idx) => {\n                    // [foo: K]: V;\n                    let Some(ast::TsFnParam::Ident(ident)) = idx.params.first() else {\n                        HANDLER.with(|handler| {\n                            handler.span_err(idx.span(), \"missing index signature key\")\n                        });\n                        continue;\n                    };\n                    let Some(key_type_ann) = &ident.type_ann else {\n                        HANDLER.with(|handler| {\n                            handler.span_err(ident.span(), \"missing key type annotation\")\n                        });\n                        continue;\n                    };\n\n                    let Some(value_type_ann) = &idx.type_ann else {\n                        HANDLER.with(|handler| {\n                            handler.span_err(idx.span(), \"missing value type annotation\")\n                        });\n                        continue;\n                    };\n\n                    let key = self.typ(&key_type_ann.type_ann);\n                    let value = self.typ(&value_type_ann.type_ann);\n                    index = Some((Box::new(key), Box::new(value)))\n                }\n\n                ast::TsTypeElement::TsMethodSignature(method) => {\n                    let name = match *method.key {\n                        ast::Expr::Ident(ref ident) => {\n                            FieldName::String(ident.sym.as_ref().to_string())\n                        }\n                        ast::Expr::Lit(ast::Lit::Str(ref str)) => {\n                            FieldName::String(str.value.to_string())\n                        }\n                        _ => {\n                            if method.computed {\n                                HANDLER.with(|handler| {\n                                    handler.span_err(\n                                        method.key.span(),\n                                        \"computed method names not yet supported\",\n                                    )\n                                });\n                            } else {\n                                HANDLER.with(|handler| {\n                                    handler.span_err(method.key.span(), \"unsupported method key\")\n                                });\n                            }\n                            continue;\n                        }\n                    };\n\n                    // Check for unsupported type parameters\n                    if let Some(type_params) = &method.type_params {\n                        HANDLER.with(|handler| {\n                            handler.span_err(\n                                type_params.span(),\n                                \"generic methods are not yet supported in object type literals\",\n                            )\n                        });\n                        continue;\n                    }\n\n                    // Parse function type\n                    let function_type = self.parse_method_signature(method);\n\n                    fields.push(InterfaceField {\n                        range: m.span().into(),\n                        name,\n                        typ: Type::Function(function_type),\n                        optional: method.optional,\n                    });\n                }\n                ast::TsTypeElement::TsCallSignatureDecl(call_sig) => {\n                    // Check for unsupported generic type parameters\n                    if let Some(type_params) = &call_sig.type_params {\n                        HANDLER.with(|handler| {\n                            handler.span_err(\n                                type_params.span,\n                                \"generic call signatures are not yet supported\",\n                            )\n                        });\n                        continue;\n                    }\n\n                    // Parse parameters\n                    let params = call_sig\n                        .params\n                        .iter()\n                        .filter_map(|param| {\n                            // Parse the parameter and extract just the type\n                            self.parse_function_param(param).map(|fp| fp.typ)\n                        })\n                        .collect::<Vec<_>>();\n\n                    // Parse return type\n                    let return_type = if let Some(type_ann) = &call_sig.type_ann {\n                        self.typ(&type_ann.type_ann)\n                    } else {\n                        HANDLER.with(|handler| {\n                            handler.span_err(\n                                call_sig.span,\n                                \"call signature must have explicit return type annotation\",\n                            )\n                        });\n                        Type::Basic(Basic::Never)\n                    };\n\n                    call_signatures.push((params, Box::new(return_type)));\n                }\n                ast::TsTypeElement::TsConstructSignatureDecl(_) => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(\n                            m.span(),\n                            \"constructor signatures are not yet supported in object type literals\",\n                        )\n                    });\n                    continue;\n                }\n                ast::TsTypeElement::TsGetterSignature(_) => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(\n                            m.span(),\n                            \"getter signatures are not yet supported in object type literals\",\n                        )\n                    });\n                    continue;\n                }\n                ast::TsTypeElement::TsSetterSignature(_) => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(\n                            m.span(),\n                            \"setter signatures are not yet supported in object type literals\",\n                        )\n                    });\n                    continue;\n                }\n            }\n        }\n\n        Type::Interface(Interface {\n            fields,\n            index,\n            call: if call_signatures.is_empty() {\n                None\n            } else {\n                Some(call_signatures)\n            },\n        })\n    }\n\n    fn parse_method_signature(&self, method: &ast::TsMethodSignature) -> FunctionType {\n        if let Some(type_params) = &method.type_params {\n            HANDLER.with(|handler| {\n                handler.span_err(type_params.span, \"generic methods are not yet supported\")\n            });\n        }\n\n        // Parse parameters\n        let params = method\n            .params\n            .iter()\n            .filter_map(|param| self.parse_function_param(param))\n            .collect();\n\n        // Parse return type\n        let return_type = if let Some(type_ann) = &method.type_ann {\n            Box::new(self.typ(&type_ann.type_ann))\n        } else {\n            HANDLER.with(|handler| {\n                handler.span_err(\n                    method.span,\n                    \"method must have explicit return type annotation (function body parsing not yet supported)\",\n                )\n            });\n            Box::new(Type::Basic(Basic::Never))\n        };\n\n        FunctionType {\n            params,\n            return_type,\n            type_params: None,\n        }\n    }\n\n    fn parse_function(&self, func: &ast::Function) -> FunctionType {\n        if let Some(type_params) = &func.type_params {\n            HANDLER.with(|handler| {\n                handler.span_err(type_params.span, \"generic functions are not yet supported\")\n            });\n        }\n\n        // Parse parameters\n        let params = func\n            .params\n            .iter()\n            .filter_map(|param| match &param.pat {\n                ast::Pat::Ident(ident) => {\n                    let name = Some(ident.id.sym.as_ref().to_string());\n                    let optional = ident.id.optional;\n\n                    let typ = if let Some(type_ann) = &ident.type_ann {\n                        self.typ(&type_ann.type_ann)\n                    } else {\n                        Type::Basic(Basic::Any)\n                    };\n\n                    Some(FunctionParam {\n                        name,\n                        typ,\n                        optional,\n                        rest: false,\n                    })\n                }\n                ast::Pat::Array(array) => {\n                    let typ = if let Some(type_ann) = &array.type_ann {\n                        self.typ(&type_ann.type_ann)\n                    } else {\n                        Type::Basic(Basic::Any)\n                    };\n\n                    Some(FunctionParam {\n                        name: None,\n                        typ,\n                        optional: array.optional,\n                        rest: false,\n                    })\n                }\n                ast::Pat::Object(object) => {\n                    let typ = if let Some(type_ann) = &object.type_ann {\n                        self.typ(&type_ann.type_ann)\n                    } else {\n                        Type::Basic(Basic::Any)\n                    };\n\n                    Some(FunctionParam {\n                        name: None,\n                        typ,\n                        optional: object.optional,\n                        rest: false,\n                    })\n                }\n                ast::Pat::Rest(rest) => {\n                    let name = match rest.arg.as_ref() {\n                        ast::Pat::Ident(ident) => Some(ident.id.sym.as_ref().to_string()),\n                        _ => None,\n                    };\n\n                    let typ = if let Some(type_ann) = &rest.type_ann {\n                        self.typ(&type_ann.type_ann)\n                    } else {\n                        Type::Basic(Basic::Any)\n                    };\n\n                    Some(FunctionParam {\n                        name,\n                        typ,\n                        optional: false,\n                        rest: true,\n                    })\n                }\n                ast::Pat::Assign(_) | ast::Pat::Expr(_) | ast::Pat::Invalid(_) => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(\n                            param.span(),\n                            \"pattern not yet supported in function parameters\",\n                        )\n                    });\n                    None\n                }\n            })\n            .collect();\n\n        // Parse return type\n        let return_type = if let Some(type_ann) = &func.return_type {\n            Box::new(self.typ(&type_ann.type_ann))\n        } else {\n            HANDLER.with(|handler| {\n                handler.span_err(\n                    func.span,\n                    \"function must have explicit return type annotation (function body parsing not yet supported)\",\n                )\n            });\n            Box::new(Type::Basic(Basic::Never))\n        };\n\n        FunctionType {\n            params,\n            return_type,\n            type_params: None,\n        }\n    }\n\n    fn parse_function_param(&self, param: &ast::TsFnParam) -> Option<FunctionParam> {\n        match param {\n            ast::TsFnParam::Ident(ident) => {\n                let name = Some(ident.id.sym.as_ref().to_string());\n                let optional = ident.id.optional;\n\n                let typ = if let Some(type_ann) = &ident.type_ann {\n                    self.typ(&type_ann.type_ann)\n                } else {\n                    Type::Basic(Basic::Any)\n                };\n\n                Some(FunctionParam {\n                    name,\n                    typ,\n                    optional,\n                    rest: false,\n                })\n            }\n\n            ast::TsFnParam::Rest(rest) => {\n                // Rest parameter: ...args: string[]\n                let name = match rest.arg.as_ref() {\n                    ast::Pat::Ident(ident) => Some(ident.id.sym.as_ref().to_string()),\n                    _ => None,\n                };\n\n                let typ = if let Some(type_ann) = &rest.type_ann {\n                    self.typ(&type_ann.type_ann)\n                } else {\n                    Type::Basic(Basic::Any)\n                };\n\n                Some(FunctionParam {\n                    name,\n                    typ,\n                    optional: false,\n                    rest: true,\n                })\n            }\n\n            ast::TsFnParam::Array(array) => {\n                // Destructured array parameter: [a, b]: [string, number]\n                // We extract the type but lose the destructuring structure\n                let typ = if let Some(type_ann) = &array.type_ann {\n                    self.typ(&type_ann.type_ann)\n                } else {\n                    Type::Basic(Basic::Any)\n                };\n\n                Some(FunctionParam {\n                    name: None, // No simple name for destructured param\n                    typ,\n                    optional: array.optional,\n                    rest: false,\n                })\n            }\n\n            ast::TsFnParam::Object(object) => {\n                // Destructured object parameter: {x, y}: {x: number, y: number}\n                // We extract the type but lose the destructuring structure\n                let typ = if let Some(type_ann) = &object.type_ann {\n                    self.typ(&type_ann.type_ann)\n                } else {\n                    Type::Basic(Basic::Any)\n                };\n\n                Some(FunctionParam {\n                    name: None, // No simple name for destructured param\n                    typ,\n                    optional: object.optional,\n                    rest: false,\n                })\n            }\n        }\n    }\n\n    /// Resolves literals.\n    fn lit_type(&self, lit_type: &ast::TsLitType) -> Type {\n        Type::Literal(match &lit_type.lit {\n            ast::TsLit::Str(val) => Literal::String(val.value.to_string()),\n            ast::TsLit::Number(val) => Literal::Number(val.value),\n            ast::TsLit::Bool(val) => Literal::Boolean(val.value),\n            ast::TsLit::BigInt(val) => Literal::BigInt(val.value.to_string()),\n            ast::TsLit::Tpl(_) => {\n                // A template literal.\n                // https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html\n                HANDLER.with(|handler| {\n                    handler.span_err(\n                        lit_type.span,\n                        \"template literal expression not yet supported\",\n                    )\n                });\n                Literal::String(\"\".into())\n            }\n        })\n    }\n\n    fn type_ref(&self, typ: &ast::TsTypeRef) -> Type {\n        let obj = match &typ.type_name {\n            ast::TsEntityName::Ident(ident) => {\n                let ident_id = ident.to_id();\n                // Is this a reference to a type parameter?\n                let type_param = self\n                    .type_params\n                    .iter()\n                    .enumerate()\n                    .find(|tp| tp.1.name.to_id() == ident_id)\n                    .map(|tp| (tp.0, *tp.1));\n                if let Some((idx, type_param)) = type_param {\n                    return if let Some(type_arg) = self.type_args.get(idx) {\n                        type_arg.clone()\n                    } else {\n                        let constraint = type_param.constraint.as_ref().map(|c| self.btyp(c));\n                        Type::Generic(Generic::TypeParam(TypeParam { idx, constraint }))\n                    };\n                }\n\n                // Otherwise, is this a reference to the current mapped 'key' type?\n                if let Some(mapped_type_ctx) = &self.mapped_key_id {\n                    if ident.to_id() == *mapped_type_ctx {\n                        // Do we have a mapped key type?\n                        return if let Some(mapped_key_type) = self.mapped_key_type {\n                            mapped_key_type.clone()\n                        } else {\n                            Type::Generic(Generic::MappedKeyType(MappedKeyType))\n                        };\n                    }\n                }\n\n                // Otherwise, is this a reference to an inferred type parameter?\n                if let Some(infer_type_params) = &self.infer_type_params {\n                    let inferred_type_param = infer_type_params\n                        .borrow()\n                        .iter()\n                        .enumerate()\n                        .find(|tp| *tp.1 == ident_id)\n                        .map(|tp| tp.0);\n                    if let Some(idx) = inferred_type_param {\n                        return if let Some(type_arg) = self.infer_type_args.get(idx) {\n                            type_arg.clone().into_owned()\n                        } else {\n                            Type::Generic(Generic::Inferred(Inferred(idx)))\n                        };\n                    }\n                }\n\n                let Some(obj) = self.ident_obj(ident) else {\n                    HANDLER.with(|handler| handler.span_err(ident.span, \"unknown identifier\"));\n                    return Type::Basic(Basic::Never);\n                };\n                obj\n            }\n            ast::TsEntityName::TsQualifiedName(qn) => {\n                let Some(obj) = self.qualified_name_obj(qn) else {\n                    HANDLER.with(|handler| handler.span_err(qn.span(), \"unknown qualified name\"));\n                    return Type::Basic(Basic::Never);\n                };\n                obj\n            }\n        };\n\n        // Is this a reference to the built-in 'Date' class?\n        if obj.name.as_ref().is_some_and(|s| s == \"Date\") && self.state.is_universe(obj.module_id) {\n            return Type::Basic(Basic::Date);\n        }\n\n        let num_params = typ.type_params.as_ref().map_or(0, |p| p.params.len());\n        let mut type_arguments = Vec::with_capacity(num_params);\n        if let Some(params) = &typ.type_params {\n            for p in &params.params {\n                type_arguments.push(self.typ(p));\n            }\n        }\n\n        // Is this a reference to the built-in 'Array' class?\n        if obj.name.as_ref().is_some_and(|s| s == \"Array\") && self.state.is_universe(obj.module_id)\n        {\n            let elem = type_arguments.pop().unwrap_or(Type::Basic(Basic::Never));\n            return Type::Array(Array(Box::new(elem)));\n        }\n\n        // Is this a reference to the \"Header\", \"Query\", \"Cookie\", or \"HttpStatus\" wire spec overrides?\n        if obj\n            .name\n            .as_ref()\n            .is_some_and(|s| s == \"Header\" || s == \"Query\" || s == \"Cookie\" || s == \"HttpStatus\")\n            && self.state.is_module_path(obj.module_id, \"encore.dev/api\")\n        {\n            if let Some(wire_spec) = self.parse_wire_spec(typ.span, &obj, &type_arguments) {\n                return Type::Custom(Custom::WireSpec(wire_spec));\n            }\n        }\n\n        // Is this a encore Decimal?\n        if obj.name.as_ref().is_some_and(|s| s == \"Decimal\")\n            && self.state.is_module_path(obj.module_id, \"encore.dev/types\")\n        {\n            return Type::Custom(Custom::Decimal);\n        }\n\n        // Is this a reference to the \"Attribute\" pub/sub wire spec override?\n        if obj.name.as_ref().is_some_and(|s| s == \"Attribute\")\n            && self\n                .state\n                .is_module_path(obj.module_id, \"encore.dev/pubsub\")\n        {\n            if let Some(wire_spec) = self.parse_wire_spec(typ.span, &obj, &type_arguments) {\n                return Type::Custom(Custom::WireSpec(wire_spec));\n            }\n        }\n\n        match &obj.kind {\n            ObjectKind::TypeName(_) => {\n                let named = Named::new(obj, type_arguments);\n\n                if self\n                    .state\n                    .is_module_path(named.obj.module_id, \"encore.dev/validate\")\n                {\n                    if let Some(expr) = self.parse_validation(typ.span, &named) {\n                        return Type::Validation(expr);\n                    }\n                }\n\n                // Don't reference named types in the universe,\n                // otherwise we try to find them on disk.\n                // if self.state.is_universe(named.obj.module_id) {\n                // named.underlying(self.state).clone()\n                // } else {\n                Type::Named(named)\n                // }\n            }\n            ObjectKind::Enum(_) | ObjectKind::Class(_) => {\n                Type::Named(Named::new(obj, type_arguments))\n            }\n            ObjectKind::Var(_) | ObjectKind::Using(_) | ObjectKind::Func(_) => {\n                HANDLER.with(|handler| handler.span_err(typ.span, \"value used as type\"));\n                Type::Basic(Basic::Never)\n            }\n            ObjectKind::Module(_) => {\n                HANDLER.with(|handler| handler.span_err(typ.span, \"module used as type\"));\n                Type::Basic(Basic::Never)\n            }\n            ObjectKind::Namespace(_) => {\n                HANDLER.with(|handler| handler.span_err(typ.span, \"namespace used as type\"));\n                Type::Basic(Basic::Never)\n            }\n        }\n    }\n\n    fn qualified_name_obj(&self, qn: &ast::TsQualifiedName) -> Option<Rc<Object>> {\n        let obj = match &qn.left {\n            ast::TsEntityName::Ident(ident) => self.ident_obj(ident)?,\n            ast::TsEntityName::TsQualifiedName(qn) => self.qualified_name_obj(qn)?,\n        };\n\n        let name = qn.right.sym.as_str();\n        match &obj.kind {\n            ObjectKind::TypeName(_) => {\n                qn.right\n                    .span\n                    .err(\"cannot yet resolve qualified name on type\");\n                None\n            }\n            ObjectKind::Enum(_) => {\n                qn.right\n                    .span\n                    .err(\"cannot yet resolve qualified name on enum\");\n                None\n            }\n            ObjectKind::Var(_) => {\n                qn.right\n                    .span\n                    .err(\"cannot yet resolve qualified name on variable\");\n                None\n            }\n            ObjectKind::Using(_) => {\n                qn.right\n                    .span\n                    .err(\"cannot yet resolve qualified name on using\");\n                None\n            }\n            ObjectKind::Func(_) => {\n                qn.right\n                    .span\n                    .err(\"cannot yet resolve qualified name on function\");\n                None\n            }\n            ObjectKind::Class(_) => {\n                qn.right\n                    .span\n                    .err(\"cannot yet resolve qualified name on class\");\n                None\n            }\n            ObjectKind::Module(module) => {\n                module\n                    .data\n                    .get_named_export(self.state, &module.base.swc_file_path, name)\n            }\n            ObjectKind::Namespace(ns) => {\n                if name == \"default\" {\n                    ns.data.default_export.clone()\n                } else {\n                    ns.data.named_exports.get(name).cloned()\n                }\n            }\n        }\n    }\n\n    fn array(&self, tt: &ast::TsArrayType) -> Type {\n        Type::Array(Array(Box::new(self.typ(&tt.elem_type))))\n    }\n\n    fn optional(&self, tt: &ast::TsOptionalType) -> Type {\n        Type::Optional(Optional(Box::new(self.typ(&tt.type_ann))))\n    }\n\n    fn tuple(&self, tuple: &ast::TsTupleType) -> Type {\n        let types = self.types(tuple.elem_types.iter().filter_map(|t|\n            // As far as I can tell labels don't actually impact type-checking\n            // at all, so we can ignore them.\n            // See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html.\n            if t.label.is_some() {\n                None\n            } else {\n                Some(t.ty.as_ref())\n            }));\n\n        Type::Tuple(Tuple { types })\n    }\n\n    fn union(&self, union_type: &ast::TsUnionType) -> Type {\n        let types = self.types(union_type.types.iter().map(|t| t.as_ref()));\n        simplify_union(types)\n    }\n\n    // https://www.typescriptlang.org/docs/handbook/2/conditional-types.html\n    fn conditional(&self, tt: &ast::TsConditionalType) -> Type {\n        let check = self.typ(&tt.check_type);\n        let infer_params: Rc<RefCell<Vec<ast::Id>>> = Default::default();\n        let extends = self\n            .clone()\n            .with_infer_type_params(infer_params.clone())\n            .typ(&tt.extends_type);\n\n        // Do we have a union type in `check`, and the AST is a naked type parameter?\n        // If so, we need to treat it as a distributive conditional type.\n        // See: https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types\n        if let Type::Union(union) = &check {\n            if let ast::TsType::TsTypeRef(ref check) = tt.check_type.as_ref() {\n                if check.type_params.is_none() {\n                    if let Some(ident) = check.type_name.as_ident() {\n                        if self\n                            .type_params\n                            .iter()\n                            .any(|tp| tp.name.to_id() == ident.to_id())\n                        {\n                            // Apply the conditional to each type in the union.\n                            let result = union\n                                .types\n                                .iter()\n                                .map(|t| match t.assignable(self.state, &extends) {\n                                    Some(true) => self.typ(&tt.true_type),\n                                    Some(false) => self.typ(&tt.false_type),\n                                    None => Type::Generic(Generic::Conditional(Conditional {\n                                        check_type: Box::new(t.clone()),\n                                        extends_type: Box::new(extends.clone()),\n                                        true_type: self.btyp(&tt.true_type),\n                                        false_type: self.btyp(&tt.false_type),\n                                    })),\n                                })\n                                .collect::<Vec<_>>();\n                            return simplify_union(result);\n                        }\n                    }\n                }\n            }\n        }\n\n        match check.extends(self.state, &extends) {\n            Extends::Yes(mut inferred) => {\n                // Convert the inferred types to a vector with the gaps\n                // filled in with the `unknown` type.\n                inferred.sort_by_key(|(i, _)| *i);\n                let mut inf = Vec::new();\n                for (idx, typ) in inferred {\n                    while inf.len() < idx {\n                        inf.push(Cow::Owned(Type::Basic(Basic::Unknown)));\n                    }\n                    inf.push(typ);\n                }\n\n                self.clone()\n                    .with_infer_type_params(infer_params)\n                    .with_infer_type_args(&inf[..])\n                    .typ(&tt.true_type)\n            }\n\n            Extends::No => self.typ(&tt.false_type),\n            Extends::Unknown => Type::Generic(Generic::Conditional(Conditional {\n                check_type: Box::new(check),\n                extends_type: Box::new(extends),\n                true_type: self\n                    .clone()\n                    .with_infer_type_params(infer_params)\n                    .btyp(&tt.true_type),\n                false_type: self.btyp(&tt.false_type),\n            })),\n        }\n    }\n\n    fn intersection(&self, typ: &ast::TsIntersectionType) -> Type {\n        let mut result = Owned(self.typ(&typ.types[0]));\n        for t in &typ.types[1..] {\n            let t = self.typ(t);\n            result = intersect(self, result, Owned(t));\n        }\n        result.into_owned()\n    }\n\n    fn keyword(&self, typ: &ast::TsKeywordType) -> Type {\n        let basic: Basic = match typ.kind {\n            ast::TsKeywordTypeKind::TsAnyKeyword => Basic::Any,\n            ast::TsKeywordTypeKind::TsUnknownKeyword => Basic::Unknown,\n            ast::TsKeywordTypeKind::TsNumberKeyword => Basic::Number,\n            ast::TsKeywordTypeKind::TsObjectKeyword => Basic::Object,\n            ast::TsKeywordTypeKind::TsBooleanKeyword => Basic::Boolean,\n            ast::TsKeywordTypeKind::TsBigIntKeyword => Basic::BigInt,\n            ast::TsKeywordTypeKind::TsStringKeyword => Basic::String,\n            ast::TsKeywordTypeKind::TsSymbolKeyword => Basic::Symbol,\n            ast::TsKeywordTypeKind::TsVoidKeyword => Basic::Void,\n            ast::TsKeywordTypeKind::TsUndefinedKeyword => Basic::Undefined,\n            ast::TsKeywordTypeKind::TsNullKeyword => Basic::Null,\n            ast::TsKeywordTypeKind::TsNeverKeyword => Basic::Never,\n            ast::TsKeywordTypeKind::TsIntrinsicKeyword => {\n                HANDLER.with(|handler| {\n                    handler.span_err(typ.span, \"unimplemented: TsIntrinsicKeyword\")\n                });\n                Basic::Never\n            }\n        };\n\n        Type::Basic(basic)\n    }\n\n    fn type_alias_decl(&self, decl: &ast::TsTypeAliasDecl) -> Type {\n        if let Some(type_params) = &decl.type_params {\n            let args: Vec<_> = type_params.params.iter().collect();\n            self.clone().with_type_params(&args[..]).typ(&decl.type_ann)\n        } else {\n            self.typ(&decl.type_ann)\n        }\n    }\n\n    fn interface_decl(&self, decl: &ast::TsInterfaceDecl) -> Type {\n        if let Some(type_params) = &decl.type_params {\n            let args: Vec<_> = type_params.params.iter().collect();\n            self.clone()\n                .with_type_params(&args[..])\n                .do_interface_decl(decl)\n        } else {\n            self.do_interface_decl(decl)\n        }\n    }\n\n    fn do_interface_decl(&self, decl: &ast::TsInterfaceDecl) -> Type {\n        let base = self.typ(&ast::TsType::TsTypeLit(ast::TsTypeLit {\n            span: decl.span,\n            members: decl.body.body.clone(),\n        }));\n        if decl.extends.is_empty() {\n            return base;\n        }\n\n        // We have an extends clause. Compute the intersection.\n        let mut result = Owned(base);\n        for extends in &decl.extends {\n            // Resolve the extend type, using its type arguments if necessary.\n            let t = if let Some(type_args) = extends.type_args.as_ref() {\n                let Some(obj) = self.resolve_obj(&extends.expr) else {\n                    HANDLER.with(|handler| {\n                        handler.span_err(extends.span, \"extends with non-ident type\")\n                    });\n                    continue;\n                };\n\n                // We have to manually construct a Named here, because the type arguments\n                // are not provided alongside the type expression.\n                let types: Vec<_> = type_args.params.iter().map(|t| self.typ(t)).collect();\n                let named = Named::new(obj, types);\n                let typ = Type::Named(named);\n                self.concrete(&typ).into_owned()\n            } else {\n                self.expr(&extends.expr)\n            };\n\n            result = intersect(self, result, Owned(t));\n        }\n        result.into_owned()\n    }\n\n    fn expr(&self, expr: &ast::Expr) -> Type {\n        match expr {\n            ast::Expr::This(_) => Type::This(This),\n            ast::Expr::Array(lit) => self.array_lit(lit),\n            ast::Expr::Object(lit) => self.object_lit(lit),\n            ast::Expr::Fn(_) => {\n                HANDLER.with(|handler| handler.span_err(expr.span(), \"fn expr not yet supported\"));\n                Type::Basic(Basic::Never)\n            }\n            ast::Expr::Unary(expr) => match expr.op {\n                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void\n                ast::UnaryOp::Void => Type::Basic(Basic::Undefined),\n\n                // This is the JavaScript typeof operator, not the TypeScript typeof operator.\n                // See https://www.typescriptlang.org/docs/handbook/2/typeof-types.html\n                ast::UnaryOp::TypeOf => Type::Basic(Basic::String),\n\n                ast::UnaryOp::Plus => Type::Basic(Basic::Number),\n                ast::UnaryOp::Minus => match self.expr(&expr.arg) {\n                    Type::Literal(Literal::Number(num)) => Type::Literal(Literal::Number(-num)),\n                    other => other,\n                },\n\n                ast::UnaryOp::Bang | ast::UnaryOp::Tilde | ast::UnaryOp::Delete => {\n                    self.expr(&expr.arg)\n                }\n            },\n            ast::Expr::Update(expr) => self.expr(&expr.arg),\n            ast::Expr::Bin(expr) => {\n                let left = self.expr(&expr.left);\n                let right = self.expr(&expr.right);\n\n                match left.union_merge(&right) {\n                    Some(unified) => unified,\n                    // TODO handle this correctly.\n                    None => left,\n                }\n            }\n            ast::Expr::Assign(expr) => self.expr(&expr.right),\n            ast::Expr::Member(expr) => self.member_expr(expr),\n            ast::Expr::SuperProp(_) => {\n                HANDLER\n                    .with(|handler| handler.span_err(expr.span(), \"super prop not yet supported\"));\n                Type::Basic(Basic::Never)\n            }\n            ast::Expr::Cond(cond) => {\n                let left = self.expr(&cond.cons);\n                let right = self.expr(&cond.alt);\n                left.simplify_or_union(right)\n            }\n            ast::Expr::Call(expr) => {\n                // Resolve the callee to get its type\n                let callee_type = match &expr.callee {\n                    ast::Callee::Expr(callee_expr) => self.expr(callee_expr),\n                    ast::Callee::Super(_) | ast::Callee::Import(_) => {\n                        HANDLER.with(|handler| {\n                            handler.span_err(expr.span, \"super/import calls not yet supported\")\n                        });\n                        return Type::Basic(Basic::Never);\n                    }\n                };\n\n                // Extract and return the function's return type\n                self.get_return_type(&callee_type, expr.span)\n            }\n            ast::Expr::New(expr) => {\n                // The type of a class instance is the same as the class itself.\n                if let Some(type_args) = &expr.type_args {\n                    let type_args: Vec<_> = self.types(type_args.params.iter().map(|t| t.as_ref()));\n                    self.clone()\n                        .with_type_args(&type_args[..])\n                        .expr(&expr.callee)\n                } else {\n                    self.expr(&expr.callee)\n                }\n            }\n            ast::Expr::Seq(expr) => match expr.exprs.last() {\n                Some(expr) => self.expr(expr),\n                None => Type::Basic(Basic::Never),\n            },\n            ast::Expr::Ident(expr) => {\n                let Some(obj) = self.ident_obj(expr) else {\n                    HANDLER.with(|handler| handler.span_err(expr.span, \"unknown identifier\"));\n                    return Type::Basic(Basic::Never);\n                };\n\n                let named = Named::new(obj, vec![]);\n                Type::Named(named)\n            }\n            ast::Expr::PrivateName(expr) => {\n                let Some(obj) = self.ident_obj(&expr.id) else {\n                    HANDLER.with(|handler| handler.span_err(expr.id.span, \"unknown identifier\"));\n                    return Type::Basic(Basic::Never);\n                };\n\n                Type::Named(Named::new(obj, vec![]))\n            }\n            ast::Expr::Lit(expr) => match &expr {\n                ast::Lit::Str(val) => Type::Literal(Literal::String(val.value.to_string())),\n                ast::Lit::Bool(val) => Type::Literal(Literal::Boolean(val.value)),\n                ast::Lit::Num(val) => Type::Literal(Literal::Number(val.value)),\n                ast::Lit::Null(_) => Type::Basic(Basic::Null),\n                ast::Lit::BigInt(_) => Type::Basic(Basic::BigInt),\n                ast::Lit::Regex(_) => {\n                    HANDLER\n                        .with(|handler| handler.span_err(expr.span(), \"regex not yet supported\"));\n                    Type::Basic(Basic::Never)\n                }\n                ast::Lit::JSXText(_) => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(expr.span(), \"jsx text not yet supported\")\n                    });\n                    Type::Basic(Basic::Never)\n                }\n            },\n            ast::Expr::Tpl(_) => Type::Basic(Basic::String),\n            ast::Expr::TaggedTpl(_) => {\n                HANDLER.with(|handler| {\n                    handler.span_err(expr.span(), \"tagged template not yet supported\")\n                });\n                Type::Basic(Basic::Never)\n            }\n            ast::Expr::Arrow(_) => {\n                HANDLER\n                    .with(|handler| handler.span_err(expr.span(), \"arrow expr not yet supported\"));\n                Type::Basic(Basic::Never)\n            }\n            ast::Expr::Class(_) => {\n                HANDLER\n                    .with(|handler| handler.span_err(expr.span(), \"class expr not yet supported\"));\n                Type::Basic(Basic::Never)\n            }\n            ast::Expr::Yield(expr) => match &expr.arg {\n                Some(arg) => self.expr(arg),\n                None => Type::Basic(Basic::Undefined),\n            },\n            ast::Expr::MetaProp(expr) => match expr.kind {\n                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target\n                ast::MetaPropKind::NewTarget => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(expr.span, \"new.target not yet supported\")\n                    });\n                    Type::Basic(Basic::Never)\n                }\n                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta\n                ast::MetaPropKind::ImportMeta => Type::Basic(Basic::Object),\n            },\n            ast::Expr::Await(expr) => {\n                let prom = self.expr(&expr.arg);\n                if let Type::Named(mut named) = prom {\n                    if named.obj.name.as_deref() == Some(\"Promise\")\n                        && self.state.is_universe(named.obj.module_id)\n                        && !named.type_arguments.is_empty()\n                    {\n                        return named.type_arguments.swap_remove(0);\n                    }\n                }\n                Type::Basic(Basic::Unknown)\n            }\n\n            ast::Expr::Paren(expr) => self.expr(&expr.expr),\n\n            ast::Expr::JSXMember(_)\n            | ast::Expr::JSXNamespacedName(_)\n            | ast::Expr::JSXEmpty(_)\n            | ast::Expr::JSXElement(_)\n            | ast::Expr::JSXFragment(_) => Type::Basic(Basic::Never),\n\n            // <T>foo\n            ast::Expr::TsTypeAssertion(expr) => self.typ(&expr.type_ann),\n            // foo as T\n            ast::Expr::TsAs(expr) => self.typ(&expr.type_ann),\n\n            ast::Expr::TsConstAssertion(expr) => self.expr(&expr.expr),\n\n            // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html\n            ast::Expr::TsSatisfies(expr) => self.expr(&expr.expr),\n\n            ast::Expr::TsInstantiation(expr) => {\n                if !expr.type_args.params.is_empty() {\n                    let type_args: Vec<_> =\n                        self.types(expr.type_args.params.iter().map(|t| t.as_ref()));\n                    self.clone().with_type_args(&type_args[..]).expr(&expr.expr)\n                } else {\n                    self.expr(&expr.expr)\n                }\n            }\n\n            // The \"foo!\" operator\n            ast::Expr::TsNonNull(expr) => {\n                let base = self.expr(&expr.expr);\n                match base {\n                    Type::Optional(typ) => *typ.0,\n                    Type::Union(union) => {\n                        let non_null = union\n                            .types\n                            .into_iter()\n                            .filter(|t| {\n                                !matches!(\n                                    t,\n                                    Type::Basic(Basic::Undefined) | Type::Basic(Basic::Null)\n                                )\n                            })\n                            .collect::<Vec<_>>();\n                        match non_null.len() {\n                            0 => Type::Basic(Basic::Never),\n                            1 => non_null[0].clone(),\n                            _ => Type::Union(Union { types: non_null }),\n                        }\n                    }\n                    _ => base,\n                }\n            }\n\n            // \"foo?.bar\"\n            ast::Expr::OptChain(expr) => {\n                HANDLER.with(|handler| {\n                    handler.span_err(expr.span, \"optional chaining not yet supported\")\n                });\n                Type::Basic(Basic::Never)\n            }\n\n            ast::Expr::Invalid(_) => Type::Basic(Basic::Never),\n        }\n    }\n\n    fn array_lit(&self, lit: &ast::ArrayLit) -> Type {\n        let elem_types = Vec::with_capacity(lit.elems.len());\n\n        // Track the current element type.\n        let mut elem_type: Option<Type> = None;\n\n        for elem in lit.elems.iter().flatten() {\n            let mut base = self.expr(&elem.expr);\n            if elem.spread.is_some() {\n                // The type of [...[\"a\"]] is string[].\n                if let Type::Array(arr) = base {\n                    base = *arr.0;\n                }\n            }\n\n            match &elem_type {\n                Some(Type::Union(_elem_types)) => {}\n                Some(typ) => {\n                    elem_type = Some(Type::Union(Union {\n                        types: vec![typ.clone(), base],\n                    }));\n                }\n                None => {\n                    elem_type = Some(base);\n                }\n            }\n        }\n\n        Type::Union(Union { types: elem_types })\n    }\n\n    fn object_lit(&self, lit: &ast::ObjectLit) -> Type {\n        let mut fields = Vec::with_capacity(lit.props.len());\n\n        for prop in &lit.props {\n            match prop {\n                ast::PropOrSpread::Prop(prop) => {\n                    let (name, typ) = match prop.as_ref() {\n                        ast::Prop::Shorthand(id) => {\n                            let Some(obj) = self.ident_obj(id) else {\n                                HANDLER.with(|handler| {\n                                    handler.span_err(id.span, \"unknown identifier\")\n                                });\n                                return Type::Basic(Basic::Never);\n                            };\n\n                            let obj_type = self.obj_type(&obj);\n                            (Cow::Borrowed(id.sym.as_ref()), obj_type)\n                        }\n                        ast::Prop::KeyValue(kv) => {\n                            let key = self.prop_name_to_string(&kv.key);\n                            let val_typ = self.expr(&kv.value);\n                            (key, val_typ)\n                        }\n                        ast::Prop::Assign(prop) => {\n                            HANDLER.with(|handler| {\n                                handler\n                                    .span_err(prop.span(), \"unsupported assign in object literal\")\n                            });\n                            return Type::Basic(Basic::Never);\n                        }\n                        ast::Prop::Getter(prop) => {\n                            let key = self.prop_name_to_string(&prop.key);\n                            // We can't figure out the value type here as it relies on\n                            // doing type analysis on the function body.\n                            (key, Type::Basic(Basic::Unknown))\n                        }\n                        ast::Prop::Setter(prop) => {\n                            let key = self.prop_name_to_string(&prop.key);\n                            // We can't figure out the value type here as it relies on\n                            // doing type analysis on the function body.\n                            (key, Type::Basic(Basic::Unknown))\n                        }\n                        ast::Prop::Method(prop) => {\n                            let key = self.prop_name_to_string(&prop.key);\n                            // We can't figure out the value type here as it relies on\n                            // doing type analysis on the function body.\n                            (key, Type::Basic(Basic::Unknown))\n                        }\n                    };\n                    fields.push(InterfaceField {\n                        range: prop.span().into(),\n                        name: FieldName::String(name.into_owned()),\n                        typ,\n                        optional: false,\n                    });\n                }\n                ast::PropOrSpread::Spread(spread) => {\n                    let typ = self.expr(&spread.expr);\n                    match typ {\n                        Type::Interface(interface) => {\n                            fields.extend(interface.fields);\n                        }\n                        _ => {\n                            HANDLER.with(|handler| {\n                                handler.span_err(spread.span(), \"unsupported spread\")\n                            });\n                        }\n                    }\n                }\n            }\n        }\n\n        Type::Interface(Interface {\n            fields,\n\n            // TODO should these be set?\n            index: None,\n            call: None,\n        })\n    }\n\n    fn member_expr(&self, expr: &ast::MemberExpr) -> Type {\n        let obj_type = self.expr(&expr.obj);\n        self.resolve_member_prop(&obj_type, &expr.prop)\n    }\n\n    fn resolve_member_prop(&self, obj_type: &Type, prop: &ast::MemberProp) -> Type {\n        match obj_type {\n            Type::Basic(_)\n            | Type::Literal(_)\n            | Type::Array(_)\n            | Type::Tuple(_)\n            | Type::Union(_)\n            | Type::Optional(_)\n            | Type::This(_)\n            | Type::Generic(_)\n            | Type::Class(_)\n            | Type::Validation(_) => {\n                HANDLER.with(|handler| {\n                    handler.span_err(\n                        prop.span(),\n                        &format!(\"unsupported member on type {obj_type}\"),\n                    )\n                });\n                Type::Basic(Basic::Never)\n            }\n            Type::Enum(tt) => {\n                for m in tt.members.iter() {\n                    let name = m.name.as_str();\n                    let matches = match prop {\n                        ast::MemberProp::Ident(i) => name == i.sym.as_ref(),\n                        ast::MemberProp::PrivateName(i) => name == i.id.sym.as_ref(),\n                        ast::MemberProp::Computed(i) => match self.expr(&i.expr) {\n                            Type::Literal(lit) => match lit {\n                                Literal::String(str) => name == str,\n                                Literal::Number(num) => name == num.to_string().as_str(),\n                                _ => false,\n                            },\n                            _ => false,\n                        },\n                    };\n                    if matches {\n                        return m.value.clone().to_type();\n                    }\n                }\n                Type::Basic(Basic::Never)\n            }\n            Type::Interface(tt) => {\n                for field in tt.fields.iter() {\n                    let matches = match prop {\n                        ast::MemberProp::Ident(i) => field.name.eq_str(i.sym.as_ref()),\n                        ast::MemberProp::PrivateName(i) => field.name.eq_str(i.id.sym.as_ref()),\n                        ast::MemberProp::Computed(i) => match self.expr(&i.expr) {\n                            Type::Literal(lit) => match lit {\n                                Literal::String(str) => field.name.eq_str(&str),\n                                Literal::Number(num) => field.name.eq_str(num.to_string().as_str()),\n                                _ => false,\n                            },\n                            _ => false,\n                        },\n                    };\n                    if matches {\n                        return field.typ.clone();\n                    }\n                }\n\n                // Otherwise use the index signature's value type, if present.\n                if let Some(idx) = &tt.index {\n                    *idx.1.clone()\n                } else {\n                    Type::Basic(Basic::Never)\n                }\n            }\n            Type::Named(_) => {\n                let underlying = self.underlying(obj_type);\n                self.resolve_member_prop(&underlying, prop)\n            }\n            Type::Validated(v) => self.resolve_member_prop(&v.typ, prop),\n            Type::Custom(Custom::WireSpec(spec)) => {\n                self.resolve_member_prop(&spec.underlying, prop)\n            }\n            Type::Custom(Custom::Decimal) => {\n                // Decimal has no properties accessible\n                Type::Basic(Basic::Never)\n            }\n            Type::Function(_) => {\n                // Functions have methods like 'call', 'apply', 'bind'\n                // but we don't support accessing them yet\n                HANDLER.with(|handler| {\n                    handler.span_err(\n                        prop.span(),\n                        \"member access on function type not yet supported\",\n                    )\n                });\n                Type::Basic(Basic::Never)\n            }\n        }\n    }\n\n    /// Resolves a prop name to the underlying string literal.\n    fn prop_name_to_string<'b>(&self, prop: &'b ast::PropName) -> Cow<'b, str> {\n        match prop {\n            ast::PropName::Ident(id) => Borrowed(id.sym.as_ref()),\n            ast::PropName::Str(str) => Borrowed(str.value.as_ref()),\n            ast::PropName::Num(num) => Owned(num.value.to_string()),\n            ast::PropName::BigInt(bigint) => Owned(bigint.value.to_string()),\n            ast::PropName::Computed(expr) => {\n                if let Type::Literal(lit) = self.expr(&expr.expr) {\n                    match lit {\n                        Literal::String(str) => return Owned(str),\n                        Literal::Number(num) => return Owned(num.to_string()),\n                        _ => {}\n                    }\n                }\n\n                HANDLER.with(|handler| handler.span_err(expr.span, \"unsupported computed prop\"));\n                Borrowed(\"\")\n            }\n        }\n    }\n\n    /// Extracts the return type from a callable type.\n    fn get_return_type(&self, typ: &Type, span: Span) -> Type {\n        match typ {\n            Type::Function(func) => *func.return_type.clone(),\n            Type::Named(_) => {\n                let underlying = self.underlying(typ);\n                self.get_return_type(&underlying, span)\n            }\n            Type::Interface(iface) => {\n                if let Some(overloads) = &iface.call {\n                    if overloads.len() == 1 {\n                        let (_, return_type) = &overloads[0];\n                        *return_type.clone()\n                    } else {\n                        HANDLER.with(|handler| {\n                            handler.span_err(\n                                span,\n                                \"cannot call interface with multiple overloaded signatures (overload resolution not yet implemented)\",\n                            )\n                        });\n                        Type::Basic(Basic::Never)\n                    }\n                } else {\n                    HANDLER.with(|handler| {\n                        handler.span_err(span, \"cannot call non-callable interface\")\n                    });\n                    Type::Basic(Basic::Never)\n                }\n            }\n\n            _ => {\n                HANDLER.with(|handler| {\n                    handler.span_err(span, &format!(\"cannot call non-function type {typ}\"))\n                });\n                Type::Basic(Basic::Never)\n            }\n        }\n    }\n}\n\nimpl Ctx<'_> {\n    pub fn obj_type(&self, obj: &Object) -> Type {\n        if matches!(&obj.kind, ObjectKind::Module(_)) {\n            // Modules don't have a type.\n            return Type::Basic(Basic::Never);\n        };\n\n        match obj.state.borrow().deref() {\n            CheckState::Completed(typ) => return typ.clone(),\n            CheckState::InProgress => {\n                // TODO support certain types of circular references.\n                HANDLER.with(|handler| {\n                    handler.span_err(obj.range.to_span(), \"circular type reference\");\n                });\n                return Type::Basic(Basic::Never);\n            }\n            CheckState::NotStarted => {\n                // Fall through below to do actual type-checking.\n                // Needs to be handled separately to avoid borrowing issues.\n            }\n        }\n        // Post-condition: state is NotStarted.\n\n        // Mark this object as being checked.\n        *obj.state.borrow_mut() = CheckState::InProgress;\n\n        let type_params: Vec<_> = obj.kind.type_params().collect();\n\n        let typ = {\n            // Create a nested ctx that uses the object's module.\n            let ctx = Ctx::new(self.state, obj.module_id).with_type_params(&type_params);\n            ctx.resolve_obj_type(obj)\n        };\n\n        *obj.state.borrow_mut() = CheckState::Completed(typ.clone());\n        typ\n    }\n\n    fn resolve_obj_type(&self, obj: &Object) -> Type {\n        match &obj.kind {\n            ObjectKind::TypeName(tn) => match &tn.decl {\n                TypeNameDecl::Interface(iface) => self.interface_decl(iface),\n                TypeNameDecl::TypeAlias(ta) => self.type_alias_decl(ta),\n            },\n\n            ObjectKind::Enum(o) => {\n                let mut members = Vec::with_capacity(o.members.len());\n                let mut prev_value = None;\n                for m in &o.members {\n                    // Determine the initializer type, if provided.\n                    let init = m.init.as_ref().map(|t| self.expr(t));\n                    let value = match init {\n                        None => {\n                            // We didn't have an initializer.\n                            // Determine the value based on the previous value.\n                            match prev_value {\n                                // No previous value; this is the first entry.\n                                None => EnumValue::Number(0),\n                                Some(EnumValue::Number(i)) => EnumValue::Number(i + 1),\n                                Some(EnumValue::String(_)) => {\n                                    HANDLER.with(|h| {\n                                        h.span_err(\n                                            m.span(),\n                                            \"implicit enum value cannot follow string value\",\n                                        )\n                                    });\n                                    EnumValue::Number(0)\n                                }\n                            }\n                        }\n                        Some(Type::Literal(lit)) => match lit {\n                            Literal::String(str) => EnumValue::String(str),\n                            Literal::Number(num) => {\n                                // Ensure the number is an integer.\n                                if num.fract() != 0.0 {\n                                    HANDLER.with(|h| {\n                                        h.span_err(m.span(), \"enum value must be an integer\")\n                                    });\n                                }\n                                EnumValue::Number(num as i64)\n                            }\n                            _ => {\n                                HANDLER.with(|h| h.span_err(m.span(), \"unsupported enum value\"));\n                                EnumValue::Number(0)\n                            }\n                        },\n                        _ => {\n                            HANDLER.with(|h| h.span_err(m.span(), \"unsupported enum value\"));\n                            EnumValue::Number(0)\n                        }\n                    };\n\n                    let name = match &m.id {\n                        ast::TsEnumMemberId::Ident(id) => id.sym.as_ref().to_string(),\n                        ast::TsEnumMemberId::Str(str) => str.value.as_ref().to_string(),\n                    };\n                    prev_value = Some(value.clone());\n                    members.push(EnumMember { name, value });\n                }\n                Type::Enum(EnumType { members })\n            }\n\n            ObjectKind::Var(o) => {\n                // Do we have a type annotation? If so, use that.\n                if let Some(type_ann) = &o.type_ann {\n                    self.typ(&type_ann.type_ann)\n                } else if let Some(expr) = &o.expr {\n                    self.expr(expr)\n                } else {\n                    Type::Basic(Basic::Never)\n                }\n            }\n\n            ObjectKind::Using(o) => {\n                // Do we have a type annotation? If so, use that.\n                if let Some(type_ann) = &o.type_ann {\n                    self.typ(&type_ann.type_ann)\n                } else if let Some(expr) = &o.expr {\n                    self.expr(expr)\n                } else {\n                    Type::Basic(Basic::Never)\n                }\n            }\n\n            ObjectKind::Func(o) => Type::Function(self.parse_function(&o.spec)),\n\n            ObjectKind::Class(o) => {\n                let methods = o\n                    .spec\n                    .body\n                    .iter()\n                    .filter_map(|mem| match mem {\n                        ast::ClassMember::Method(m) => {\n                            m.key.as_ident().map(|id| id.sym.to_string())\n                        }\n                        _ => None,\n                    })\n                    .collect();\n                Type::Class(ClassType { methods })\n            }\n\n            ObjectKind::Module(_o) => Type::Basic(Basic::Never),\n            ObjectKind::Namespace(_o) => {\n                // TODO include namespace objects in interface\n                Type::Basic(Basic::Object)\n            }\n        }\n    }\n\n    fn resolve_obj(&self, expr: &ast::Expr) -> Option<Rc<Object>> {\n        match self.expr(expr) {\n            Type::Named(named) => Some(named.obj.clone()),\n            _ => None,\n        }\n    }\n\n    fn ident_obj(&self, ident: &ast::Ident) -> Option<Rc<Object>> {\n        // Does this represent a type parameter?\n        self.state.resolve_module_ident(self.module, ident)\n    }\n\n    fn resolve_default_export(&self) -> Option<Rc<Object>> {\n        self.state.resolve_module_default_export(self.module)\n    }\n}\n\nimpl Ctx<'_> {\n    #[tracing::instrument(skip(self), ret, level = \"trace\")]\n    pub fn concrete<'b>(&'b self, typ: &'b Type) -> Resolved<'b, Type> {\n        match typ {\n            // Basic types that never change.\n            Type::Basic(_) | Type::Literal(_) | Type::Enum(_) | Type::This(_) => Same(typ),\n\n            // Nested types that recurse.\n            Type::Array(elem) => match self.concrete(&elem.0) {\n                New(t) => New(Type::Array(Array(Box::new(t)))),\n                Changed(t) => New(Type::Array(Array(Box::new(t.clone())))),\n                Same(_) => Same(typ),\n            },\n            Type::Tuple(tuple) => match self.concrete_list(&tuple.types) {\n                New(t) => New(Type::Tuple(Tuple { types: t })),\n                Changed(t) => New(Type::Tuple(Tuple {\n                    types: t.to_owned(),\n                })),\n                Same(_) => Same(typ),\n            },\n            Type::Union(union) => match self.concrete_list(&union.types) {\n                New(t) => New(Type::Union(Union { types: t })),\n                Changed(t) => New(Type::Union(Union {\n                    types: t.to_owned(),\n                })),\n                Same(_) => Same(typ),\n            },\n            Type::Optional(opt) => match self.concrete(&opt.0) {\n                New(t) => New(Type::Optional(Optional(Box::new(t)))),\n                Changed(t) => New(Type::Optional(Optional(Box::new(t.to_owned())))),\n                Same(_) => Same(typ),\n            },\n\n            Type::Named(named) => match self.concrete_list(&named.type_arguments) {\n                New(t) => New(Type::Named(Named::new(named.obj.clone(), t))),\n                Changed(t) => New(Type::Named(Named::new(named.obj.clone(), t.to_owned()))),\n                Same(_) => Same(typ),\n            },\n\n            Type::Interface(iface) => {\n                let concrete_fields =\n                    |fields: &'b [InterfaceField]| -> Resolved<'b, [InterfaceField]> {\n                        for (i, field) in fields.iter().enumerate() {\n                            let t = match self.concrete(&field.typ) {\n                                New(t) => t,\n                                Changed(t) => t.clone(),\n                                Same(_) => continue,\n                            };\n                            // We have a new type, so we need to clone the entire list.\n                            let mut res = Vec::with_capacity(fields.len());\n                            res.extend(fields[0..i].iter().cloned());\n\n                            res.push(InterfaceField {\n                                range: field.range,\n                                typ: t,\n                                name: field.name.clone(),\n                                optional: field.optional,\n                            });\n\n                            // Copy all remaining elements.\n                            res.extend(fields[i + 1..].iter().map(|t| InterfaceField {\n                                range: t.range,\n                                name: t.name.clone(),\n                                typ: self.concrete(&t.typ).into_owned(),\n                                optional: t.optional,\n                            }));\n\n                            return New(res);\n                        }\n\n                        // All types are the same, so we can just return the original list.\n                        Same(fields)\n                    };\n\n                let fields = concrete_fields(&iface.fields);\n                let index = iface\n                    .index\n                    .as_ref()\n                    .map(|(key, val)| (self.concrete(key), self.concrete(val)));\n                let call = iface.call.as_ref().map(|overloads| {\n                    overloads\n                        .iter()\n                        .map(|(params, ret)| (self.concrete_list(params), self.concrete(ret)))\n                        .collect::<Vec<_>>()\n                });\n\n                // If we have any parts that aren't Same, we need to make the whole thing New.\n                // Otherwise return the original type.\n                let call_changed = call.as_ref().is_some_and(|overloads| {\n                    overloads\n                        .iter()\n                        .any(|(params, ret)| !matches!(params, Same(_)) || !matches!(ret, Same(_)))\n                });\n                if !matches!(fields, Same(_))\n                    || !matches!(index, Some((Same(_), Same(_))))\n                    || call_changed\n                {\n                    New(Type::Interface(Interface {\n                        fields: fields.into_owned(),\n                        index: index\n                            .map(|(k, v)| (Box::new(k.into_owned()), Box::new(v.into_owned()))),\n                        call: call.map(|overloads| {\n                            overloads\n                                .into_iter()\n                                .map(|(p, r)| (p.into_owned(), Box::new(r.into_owned())))\n                                .collect()\n                        }),\n                    }))\n                } else {\n                    Same(typ)\n                }\n            }\n\n            // TODO is this correct?\n            // Class types are already concrete.\n            Type::Class(_) => Same(typ),\n\n            Type::Generic(generic) => match generic {\n                Generic::TypeParam(param) => {\n                    // If the type parameter is a concrete type, return that.\n                    if let Some(arg) = self.type_args.get(param.idx) {\n                        Changed(arg)\n                    } else {\n                        // We don't have a concrete type, so return the original type.\n                        Same(typ)\n                    }\n                }\n\n                Generic::Inferred(inferred) => {\n                    // If we have a concrete inferred type, return that.\n                    if let Some(arg) = self.infer_type_args.get(inferred.0) {\n                        Changed(arg)\n                    } else {\n                        // We don't have a concrete type, so return the original type.\n                        Same(typ)\n                    }\n                }\n\n                Generic::Keyof(source) => {\n                    let concrete_source = self.concrete(&source.0);\n                    let keys = self.keyof(&concrete_source);\n                    New(keys)\n                }\n\n                Generic::Intersection(intersection) => {\n                    let x = self.concrete(&intersection.x);\n                    let y = self.concrete(&intersection.y);\n\n                    match (x, y) {\n                        (Same(_), Same(_)) => Same(typ),\n                        (x, y) => match intersect(self, x.into(), y.into()) {\n                            Owned(t) => New(t),\n                            Borrowed(t) => Changed(t),\n                        },\n                    }\n                }\n\n                Generic::Conditional(cond) => {\n                    let check = self.concrete(&cond.check_type);\n                    let extends = self.concrete(&cond.extends_type);\n\n                    // Is this a \"distributed conditional type\"?\n                    // See https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types\n\n                    match (cond.check_type.as_ref(), check.into_owned()) {\n                        (Type::Generic(Generic::TypeParam(param)), Type::Union(check)) => {\n                            // If check is a union, apply the check to each type in the union.\n                            let mut type_args = self.type_args.to_owned();\n\n                            // Construct a modified context that modifies the given type argument\n                            // to refer only to the concrete type for this union.\n                            let result: Vec<_> = check\n                                .types\n                                .into_iter()\n                                .filter_map(|c| {\n                                    // Modify the type args.\n                                    if type_args.len() > param.idx {\n                                        type_args[param.idx] = c.clone();\n                                    }\n                                    let nested = self.clone().with_type_args(&type_args);\n                                    match c.assignable(self.state, &extends) {\n                                        Some(true) => {\n                                            Some(nested.concrete(&cond.true_type).into_owned())\n                                        }\n                                        Some(false) => {\n                                            Some(nested.concrete(&cond.false_type).into_owned())\n                                        }\n                                        // This implies there's a generic type in this mix,\n                                        // which shouldn't happen when concretizing.\n                                        None => None,\n                                    }\n                                })\n                                .collect();\n\n                            New(simplify_union(result))\n                        }\n\n                        // Otherwise just check the single element.\n                        (_, check) => match check.extends(self.state, &extends).into_static() {\n                            Extends::Yes(mut inferred) => {\n                                // Convert the inferred types to a vector with the gaps\n                                // filled in with the `unknown` type.\n                                inferred.sort_by_key(|(i, _)| *i);\n                                let mut inf = Vec::new();\n                                for (idx, typ) in inferred {\n                                    while inf.len() < idx {\n                                        inf.push(Cow::Owned(Type::Basic(Basic::Unknown)));\n                                    }\n                                    inf.push(typ);\n                                }\n\n                                self.clone()\n                                    .with_infer_type_args(&inf[..])\n                                    .concrete(&cond.true_type)\n                                    .into_new()\n                            }\n                            Extends::No => self.concrete(&cond.false_type).same_to_changed(),\n\n                            // We don't yet have enough type information to resolve the conditional.\n                            // Still, return a new type with the concretized types we have.\n                            Extends::Unknown => {\n                                New(Type::Generic(Generic::Conditional(Conditional {\n                                    check_type: Box::new(check),\n                                    extends_type: Box::new(extends.into_owned()),\n                                    true_type: Box::new(\n                                        self.concrete(&cond.true_type).into_owned(),\n                                    ),\n                                    false_type: Box::new(\n                                        self.concrete(&cond.false_type).into_owned(),\n                                    ),\n                                })))\n                            }\n                        },\n                    }\n                }\n\n                Generic::Index(index) => {\n                    let source = self.concrete(&index.source);\n                    let index = self.concrete(&index.index);\n                    let result = self.type_index(Span::default(), &source, &index);\n                    New(result)\n                }\n\n                Generic::Mapped(mapped) => {\n                    let mut iface = Interface {\n                        fields: vec![],\n                        index: None,\n                        call: None,\n                    };\n\n                    let keys = self.underlying(&mapped.in_type).into_owned();\n                    for key in keys.into_iter_unions() {\n                        let ctx_with_key = self.clone().with_mapped_key_type(Some(&key));\n\n                        let value = ctx_with_key.concrete(&mapped.value_type).into_owned();\n\n                        // If the value resolves to 'never' it should be skipped.\n                        if let Type::Basic(Basic::Never) = &value {\n                            continue;\n                        }\n\n                        // Evaluate the as_type to get the transformed key, if present.\n                        let transformed_key = if let Some(as_type) = &mapped.as_type {\n                            let concrete = ctx_with_key.concrete(as_type).into_owned();\n                            // Get the underlying type if it's a Named type\n                            ctx_with_key.underlying(&concrete).into_owned()\n                        } else {\n                            key.clone()\n                        };\n\n                        // If the transformed key is 'never', skip this property (filtering).\n                        if let Type::Basic(Basic::Never) = &transformed_key {\n                            continue;\n                        }\n\n                        match transformed_key {\n                            // Never means the field should be excluded.\n                            Type::Basic(Basic::Never) => {\n                                HANDLER.with(|handler| {\n                                    handler.err(\"unexpected 'never' type as mapped type key\");\n                                });\n                            }\n\n                            // An unresolved generic type means we can't resolve this yet.\n                            Type::Generic(_) => {\n                                return New(Type::Generic(Generic::Mapped(Mapped {\n                                    in_type: Box::new(self.concrete(&mapped.in_type).into_owned()),\n                                    value_type: Box::new(\n                                        self.concrete(&mapped.value_type).into_owned(),\n                                    ),\n                                    optional: mapped.optional,\n                                    as_type: mapped\n                                        .as_type\n                                        .as_ref()\n                                        .map(|at| Box::new(self.concrete(at).into_owned())),\n                                })))\n                            }\n\n                            // Do we have a wildcard type like \"string\" or \"number\"?\n                            // If so treat it as an index signature.\n                            source @ (Type::Basic(Basic::String)\n                            | Type::Basic(Basic::Number)\n                            | Type::Basic(Basic::Symbol)) => {\n                                // TODO actually do the mapping/filtering\n                                iface.index = Some((Box::new(source.clone()), Box::new(value)));\n                            }\n\n                            // Do we have a string literal?\n                            Type::Literal(Literal::String(str)) => {\n                                // Unwrap optional and record it on the field instead.\n                                let (typ, optional) = match value {\n                                    // Never means the field should be excluded.\n                                    Type::Basic(Basic::Never) => continue,\n                                    Type::Optional(typ) => (*typ.0, true),\n                                    typ => (typ, false),\n                                };\n\n                                iface.fields.push(InterfaceField {\n                                    range: Range::default(),\n                                    name: FieldName::String(str.clone()),\n                                    typ,\n                                    optional,\n                                });\n                            }\n\n                            typ => {\n                                HANDLER.with(|handler| {\n                                    handler.err(&format!(\"unsupported mapped key type: {typ:#?}\"));\n                                });\n                            }\n                        }\n                    }\n\n                    // If the mapped type contains optional modifiers, apply them.\n                    if let Some(optional) = &mapped.optional {\n                        if let Some((key, value)) = iface.index.take() {\n                            let value = if *optional {\n                                match value.as_ref() {\n                                    Type::Optional(_) => value,\n                                    _ => Box::new(Type::Optional(Optional(value))),\n                                }\n                            } else if let Type::Optional(Optional(inner)) = *value {\n                                inner\n                            } else {\n                                value\n                            };\n                            iface.index = Some((key, value));\n                        }\n\n                        for field in iface.fields.iter_mut() {\n                            field.optional = *optional;\n                        }\n                    }\n\n                    New(Type::Interface(iface))\n                }\n\n                Generic::MappedKeyType(_) => match self.mapped_key_type {\n                    Some(key) => Changed(key),\n                    None => Same(typ),\n                },\n            },\n\n            Type::Validated(v) => match self.concrete(&v.typ) {\n                New(inner) => New(Type::Validated(Validated {\n                    typ: Box::new(inner),\n                    expr: v.expr.clone(),\n                })),\n                Changed(inner) => New(Type::Validated(Validated {\n                    typ: Box::new(inner.clone()),\n                    expr: v.expr.clone(),\n                })),\n                Same(_) => Same(typ),\n            },\n\n            Type::Validation(_) => Same(typ),\n            Type::Custom(Custom::WireSpec(spec)) => match self.concrete(&spec.underlying) {\n                New(inner) => New(Type::Custom(Custom::WireSpec(WireSpec {\n                    underlying: Box::new(inner),\n                    ..spec.clone()\n                }))),\n                Changed(inner) => New(Type::Custom(Custom::WireSpec(WireSpec {\n                    underlying: Box::new(inner.clone()),\n                    ..spec.clone()\n                }))),\n                Same(_) => Same(typ),\n            },\n            Type::Custom(Custom::Decimal) => Same(typ), // Decimal is already concrete\n            Type::Function(func) => {\n                // Concretize parameter types\n                let params_resolved = func\n                    .params\n                    .iter()\n                    .map(|p| match self.concrete(&p.typ) {\n                        New(t) => (\n                            true,\n                            FunctionParam {\n                                name: p.name.clone(),\n                                typ: t,\n                                optional: p.optional,\n                                rest: p.rest,\n                            },\n                        ),\n                        Changed(t) => (\n                            true,\n                            FunctionParam {\n                                name: p.name.clone(),\n                                typ: t.clone(),\n                                optional: p.optional,\n                                rest: p.rest,\n                            },\n                        ),\n                        Same(_) => (false, p.clone()),\n                    })\n                    .collect::<Vec<_>>();\n\n                let return_resolved = self.concrete(&func.return_type);\n\n                // Check if anything changed\n                let params_changed = params_resolved.iter().any(|(changed, _)| *changed);\n                let return_changed = !matches!(return_resolved, Same(_));\n\n                if params_changed || return_changed {\n                    New(Type::Function(FunctionType {\n                        params: params_resolved.into_iter().map(|(_, p)| p).collect(),\n                        return_type: Box::new(return_resolved.into_owned()),\n                        type_params: func.type_params.clone(),\n                    }))\n                } else {\n                    Same(typ)\n                }\n            }\n        }\n    }\n\n    pub fn underlying_named(&self, named: &Named) -> Type {\n        let type_params = named.obj.kind.type_params().collect::<Vec<_>>();\n\n        // Create a complete list of type arguments with defaults applied where needed\n        if named.type_arguments.len() < type_params.len() {\n            let mut args = named.type_arguments.clone();\n\n            // For each parameter that wasn't provided, try to use its default\n            for param in type_params.iter().skip(args.len()) {\n                if let Some(default) = param.default.as_ref() {\n                    args.push(self.typ(default));\n                }\n            }\n\n            self.underlying_type(named, &args, &type_params)\n        } else {\n            self.underlying_type(named, &named.type_arguments, &type_params)\n        }\n    }\n\n    pub fn underlying<'b>(&'b self, typ: &'b Type) -> Resolved<'b, Type> {\n        // Ensure we resolve the concrete type.\n        match self.concrete(typ) {\n            Same(tt) => match tt {\n                Type::Named(named) => New(named.underlying(self.state)),\n                _ => Same(typ),\n            },\n            Changed(tt) => match tt {\n                Type::Named(named) => New(named.underlying(self.state)),\n                _ => Changed(tt),\n            },\n            New(tt) => match tt {\n                Type::Named(named) => New(named.underlying(self.state)),\n                _ => New(tt),\n            },\n        }\n    }\n\n    fn underlying_type(\n        &self,\n        named: &Named,\n        type_arguments: &[Type],\n        type_params: &[&TsTypeParam],\n    ) -> Type {\n        let type_args = self.concrete_list(type_arguments);\n        let typ = self.obj_type(&named.obj);\n\n        let ctx = self\n            .clone()\n            .with_type_params(type_params)\n            .with_type_args(&type_args);\n\n        let span = tracing::trace_span!(\"underlying_named\", ?named, ?type_args);\n        let _guard = span.enter();\n        ctx.underlying(&typ).into_owned()\n    }\n\n    fn concrete_list<'b>(&'b self, v: &'b [Type]) -> Resolved<'b, [Type]> {\n        for (i, typ) in v.iter().enumerate() {\n            let t = match self.concrete(typ) {\n                New(t) => t,\n                Changed(t) => t.clone(),\n                Same(_) => continue,\n            };\n\n            // We have a new type, so we need to clone the entire list.\n            let mut res = Vec::with_capacity(v.len());\n            res.extend(v[0..i].iter().cloned());\n            res.push(t);\n\n            // Copy all remaining elements.\n            res.extend(v[i + 1..].iter().map(|t| self.concrete(t).into_owned()));\n            return New(res);\n        }\n\n        // All types are the same, so we can just return the original list.\n        Same(v)\n    }\n\n    #[allow(dead_code)]\n    fn doc_comment(&self, pos: BytePos) -> Option<String> {\n        self.state\n            .lookup_module(self.module)\n            .and_then(|m| m.base.preceding_comments(pos.into()))\n    }\n\n    fn parse_validation(&self, sp: Span, named: &Named) -> Option<validation::Expr> {\n        let name = named.obj.name.as_deref()?;\n\n        #[allow(dead_code)]\n        fn i64_lit(typ: &Type) -> Option<i64> {\n            if let Type::Literal(Literal::Number(n)) = typ {\n                let i = *n as i64;\n                if i as f64 == *n {\n                    return Some(i);\n                }\n            }\n            None\n        }\n\n        fn u64_lit(typ: &Type) -> Option<u64> {\n            if let Type::Literal(Literal::Number(n)) = typ {\n                let u = *n as u64;\n                if u as f64 == *n {\n                    return Some(u);\n                }\n            }\n            None\n        }\n\n        fn f64_lit(typ: &Type) -> Option<f64> {\n            if let Type::Literal(Literal::Number(n)) = typ {\n                return Some(*n);\n            }\n            None\n        }\n\n        fn str_lit(typ: &Type) -> Option<String> {\n            if let Type::Literal(Literal::String(s)) = typ {\n                return Some(s.clone());\n            }\n            None\n        }\n\n        use validation::{Expr, Is, Rule, N};\n        match name {\n            \"Min\" => {\n                if let Some(num) = named.type_arguments.first().and_then(f64_lit) {\n                    Some(Expr::Rule(Rule::MinVal(N(num))))\n                } else {\n                    sp.err(\"Min requires a number literal as its first type argument\");\n                    None\n                }\n            }\n            \"Max\" => {\n                if let Some(num) = named.type_arguments.first().and_then(f64_lit) {\n                    Some(Expr::Rule(Rule::MaxVal(validation::N(num))))\n                } else {\n                    sp.err(\"Max requires a number literal as its first type argument\");\n                    None\n                }\n            }\n            \"MinLen\" => {\n                if let Some(num) = named.type_arguments.first().and_then(u64_lit) {\n                    Some(Expr::Rule(Rule::MinLen(num)))\n                } else {\n                    sp.err(\"MinLen requires a number literal as its first type argument\");\n                    None\n                }\n            }\n            \"MaxLen\" => {\n                if let Some(num) = named.type_arguments.first().and_then(u64_lit) {\n                    Some(Expr::Rule(Rule::MaxLen(num)))\n                } else {\n                    sp.err(\"MaxLen requires a number literal as its first type argument\");\n                    None\n                }\n            }\n            \"StartsWith\" => {\n                if let Some(str) = named.type_arguments.first().and_then(str_lit) {\n                    Some(Expr::Rule(Rule::StartsWith(str)))\n                } else {\n                    sp.err(\"StartsWith requires a string literal as its first type argument\");\n                    None\n                }\n            }\n            \"EndsWith\" => {\n                if let Some(str) = named.type_arguments.first().and_then(str_lit) {\n                    Some(Expr::Rule(Rule::EndsWith(str)))\n                } else {\n                    sp.err(\"EndsWith requires a string literal as its first type argument\");\n                    None\n                }\n            }\n            \"MatchesRegexp\" => {\n                if let Some(str) = named.type_arguments.first().and_then(str_lit) {\n                    Some(Expr::Rule(Rule::MatchesRegexp(str)))\n                } else {\n                    sp.err(\"MatchesRegexp requires a string literal as its first type argument\");\n                    None\n                }\n            }\n            \"IsEmail\" => Some(Expr::Rule(Rule::Is(Is::Email))),\n            \"IsURL\" => Some(Expr::Rule(Rule::Is(Is::Url))),\n            _ => None,\n        }\n    }\n\n    fn parse_wire_spec(&self, span: Span, obj: &Object, type_args: &[Type]) -> Option<WireSpec> {\n        let location = match &obj.name.as_deref() {\n            Some(\"Header\") => WireLocation::Header,\n            Some(\"Query\") => WireLocation::Query,\n            Some(\"Attribute\") => WireLocation::PubSubAttr,\n            Some(\"Cookie\") => WireLocation::Cookie,\n            Some(\"HttpStatus\") => WireLocation::HttpStatus,\n            _ => return None,\n        };\n\n        fn str_lit(sp: Span, typ: &Type) -> Option<String> {\n            if let Type::Literal(Literal::String(s)) = typ {\n                return Some(s.clone());\n            }\n            sp.err(\"expected a string literal as the second type argument\");\n            None\n        }\n\n        let (underlying, name_override) = match &location {\n            WireLocation::HttpStatus => {\n                // HttpStatus doesn't take generic parameters and should have no type args\n                if !type_args.is_empty() {\n                    span.err(\"HttpStatus doesn't take generic type parameters\");\n                    return None;\n                }\n                (Type::Basic(Basic::Number), None)\n            }\n            _ => {\n                match (type_args.first(), type_args.get(1)) {\n                    (None, None) => (Type::Basic(Basic::String), None),\n\n                    (Some(first), None) => {\n                        // If we only have a single argument, check its type.\n                        // If it's a string literal it's the name, otherwise it's the type.\n                        match first {\n                            Type::Literal(Literal::String(lit)) => {\n                                (Type::Basic(Basic::String), Some(lit.to_string()))\n                            }\n                            _ => (first.clone(), None),\n                        }\n                    }\n\n                    (Some(typ), Some(name)) => (typ.clone(), str_lit(span, name)),\n                    (None, Some(_)) => unreachable!(),\n                }\n            }\n        };\n\n        Some(WireSpec {\n            location,\n            underlying: Box::new(underlying),\n            name_override,\n        })\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/type_string.rs",
    "content": "use std::fmt::{Display, Write};\n\nuse super::{validation, Basic, Custom, Generic, Interface, Type, Validated, WireSpec};\n\nimpl Display for Type {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let mut renderer = TypeRenderer { buf: f };\n        renderer.render_type(self)\n    }\n}\n\nstruct TypeRenderer<B> {\n    buf: B,\n}\n\nimpl<B> TypeRenderer<B>\nwhere\n    B: Write,\n{\n    fn render_type(&mut self, typ: &Type) -> std::fmt::Result {\n        match typ {\n            Type::Basic(b) => self.render_basic(b),\n            Type::Array(arr) => {\n                self.buf.write_str(\"Array<\")?;\n                self.render_type(&arr.0)?;\n                self.buf.write_char('>')\n            }\n            Type::Interface(iface) => self.render_iface(iface),\n            Type::Union(union) => {\n                for (i, typ) in union.types.iter().enumerate() {\n                    if i > 0 {\n                        self.buf.write_str(\" | \")?;\n                    }\n                    self.render_type(typ)?;\n                }\n                Ok(())\n            }\n            Type::Tuple(tup) => {\n                self.buf.write_char('[')?;\n                for (i, typ) in tup.types.iter().enumerate() {\n                    if i > 0 {\n                        self.buf.write_str(\", \")?;\n                    }\n                    self.render_type(typ)?;\n                }\n                self.buf.write_char(']')\n            }\n            Type::Literal(lit) => self.render_literal(lit),\n            Type::Class(cls) => self.render_class(cls),\n            Type::Enum(e) => self.render_enum(e),\n            Type::Named(named) => self.render_named(named),\n            Type::Optional(opt) => {\n                self.render_type(&opt.0)?;\n                self.buf.write_char('?')\n            }\n            Type::This(_) => self.buf.write_str(\"this\"),\n            Type::Generic(g) => self.render_generic(g),\n            Type::Validation(v) => self.render_validation(v),\n            Type::Validated(v) => self.render_validated(v),\n            Type::Custom(c) => self.render_custom(c),\n            Type::Function(func) => self.render_function(func),\n        }\n    }\n\n    fn render_basic(&mut self, b: &Basic) -> std::fmt::Result {\n        use Basic::*;\n        let s = match b {\n            Any => \"any\",\n            String => \"string\",\n            Boolean => \"boolean\",\n            Number => \"number\",\n            Object => \"object\",\n            BigInt => \"bigint\",\n            Date => \"Date\",\n            Symbol => \"symbol\",\n            Undefined => \"undefined\",\n            Null => \"null\",\n            Void => \"void\",\n            Unknown => \"unknown\",\n            Never => \"never\",\n        };\n        self.buf.write_str(s)\n    }\n\n    fn render_iface(&mut self, iface: &Interface) -> std::fmt::Result {\n        self.buf.write_str(\"interface { \")?;\n        for (i, field) in iface.fields.iter().enumerate() {\n            if i > 0 {\n                self.buf.write_str(\"; \")?;\n            }\n\n            use super::FieldName;\n            self.buf.write_str(match &field.name {\n                FieldName::String(s) => s,\n                FieldName::Symbol(_) => \"symbol\",\n            })?;\n            if field.optional {\n                self.buf.write_char('?')?;\n            }\n            self.buf.write_str(\": \")?;\n            self.render_type(&field.typ)?;\n        }\n        self.buf.write_str(\" }\")\n    }\n\n    fn render_literal(&mut self, lit: &super::Literal) -> std::fmt::Result {\n        use super::Literal;\n        match lit {\n            Literal::String(s) => self.buf.write_fmt(format_args!(\"{s:#?}\")),\n            Literal::Boolean(b) => self.buf.write_fmt(format_args!(\"{b}\")),\n            Literal::Number(n) => self.buf.write_fmt(format_args!(\"{n}\")),\n            Literal::BigInt(n) => self.buf.write_str(n),\n        }\n    }\n\n    fn render_class(&mut self, _cls: &super::ClassType) -> std::fmt::Result {\n        self.buf.write_str(\"class {}\")\n    }\n\n    fn render_enum(&mut self, e: &super::EnumType) -> std::fmt::Result {\n        self.buf.write_str(\"enum { \")?;\n        for (i, mem) in e.members.iter().enumerate() {\n            if i > 0 {\n                self.buf.write_str(\", \")?;\n            }\n            self.buf.write_str(&mem.name)?;\n        }\n        self.buf.write_str(\" }\")\n    }\n\n    fn render_named(&mut self, named: &super::Named) -> std::fmt::Result {\n        let name = named.obj.name.as_deref().unwrap_or(\"UnknownObject\");\n        self.buf.write_str(name)?;\n\n        if !named.type_arguments.is_empty() {\n            self.buf.write_char('<')?;\n            for (i, arg) in named.type_arguments.iter().enumerate() {\n                if i > 0 {\n                    self.buf.write_str(\", \")?;\n                }\n                self.render_type(arg)?;\n            }\n            self.buf.write_char('>')?;\n        }\n        Ok(())\n    }\n\n    fn render_validation(&mut self, v: &validation::Expr) -> std::fmt::Result {\n        self.buf.write_fmt(format_args!(\"{v}\"))\n    }\n\n    fn render_validated(&mut self, v: &Validated) -> std::fmt::Result {\n        self.render_type(&v.typ)?;\n        self.buf.write_fmt(format_args!(\" & {}\", v.expr))\n    }\n\n    fn render_custom(&mut self, c: &Custom) -> std::fmt::Result {\n        match c {\n            Custom::WireSpec(s) => self.render_wire_spec(s),\n            Custom::Decimal => write!(self.buf, \"Decimal\"),\n        }\n    }\n\n    fn render_wire_spec(&mut self, s: &WireSpec) -> std::fmt::Result {\n        match &s.location {\n            super::WireLocation::Query => {\n                self.buf.write_str(\"Query<\")?;\n                self.render_type(&s.underlying)?;\n                if let Some(name) = &s.name_override {\n                    self.buf.write_fmt(format_args!(\", {name:#?}\"))?;\n                }\n                self.buf.write_char('>')\n            }\n            super::WireLocation::Header => {\n                self.buf.write_str(\"Header<\")?;\n                self.render_type(&s.underlying)?;\n                if let Some(name) = &s.name_override {\n                    self.buf.write_fmt(format_args!(\", {name:#?}\"))?;\n                }\n                self.buf.write_char('>')\n            }\n            super::WireLocation::PubSubAttr => {\n                self.buf.write_str(\"Attribute<\")?;\n                self.render_type(&s.underlying)?;\n                if let Some(name) = &s.name_override {\n                    self.buf.write_fmt(format_args!(\", {name:#?}\"))?;\n                }\n                self.buf.write_char('>')\n            }\n            super::WireLocation::Cookie => {\n                self.buf.write_str(\"Cookie<\")?;\n                self.render_type(&s.underlying)?;\n                if let Some(name) = &s.name_override {\n                    self.buf.write_fmt(format_args!(\", {name:#?}\"))?;\n                }\n                self.buf.write_char('>')\n            }\n            super::WireLocation::HttpStatus => {\n                // HttpStatus doesn't take generic parameters, it's just the union type itself\n                self.buf.write_str(\"HttpStatus\")\n            }\n        }\n    }\n\n    fn render_generic(&mut self, g: &Generic) -> std::fmt::Result {\n        match g {\n            Generic::TypeParam(tp) => self.buf.write_fmt(format_args!(\"TypeParam#{}\", tp.idx)),\n            Generic::Index(idx) => {\n                self.render_type(&idx.source)?;\n                self.buf.write_char('[')?;\n                self.render_type(&idx.index)?;\n                self.buf.write_char(']')\n            }\n            Generic::Mapped(m) => {\n                self.buf.write_str(\"{ [P in \")?;\n                self.render_type(&m.in_type)?;\n                self.buf.write_str(\"]: \")?;\n                self.render_type(&m.value_type)?;\n                self.buf.write_str(\" }\")\n            }\n            Generic::MappedKeyType(_) => self.buf.write_char('P'),\n            Generic::Keyof(k) => {\n                self.buf.write_str(\"keyof \")?;\n                self.render_type(&k.0)\n            }\n            Generic::Conditional(c) => {\n                self.render_type(&c.check_type)?;\n                self.buf.write_str(\" extends \")?;\n                self.render_type(&c.extends_type)?;\n                self.buf.write_str(\" ? \")?;\n                self.render_type(&c.true_type)?;\n                self.buf.write_str(\" : \")?;\n                self.render_type(&c.false_type)\n            }\n            Generic::Inferred(i) => self.buf.write_fmt(format_args!(\"Inferred#{}\", i.0)),\n            Generic::Intersection(i) => {\n                self.render_type(&i.x)?;\n                self.buf.write_str(\" & \")?;\n                self.render_type(&i.y)\n            }\n        }\n    }\n\n    fn render_function(&mut self, func: &super::FunctionType) -> std::fmt::Result {\n        self.buf.write_char('(')?;\n\n        for (i, param) in func.params.iter().enumerate() {\n            if i > 0 {\n                self.buf.write_str(\", \")?;\n            }\n\n            if let Some(name) = &param.name {\n                self.buf.write_str(name)?;\n                if param.optional {\n                    self.buf.write_char('?')?;\n                }\n                self.buf.write_str(\": \")?;\n            }\n\n            if param.rest {\n                self.buf.write_str(\"...\")?;\n            }\n\n            self.render_type(&param.typ)?;\n        }\n\n        self.buf.write_str(\") => \")?;\n        self.render_type(&func.return_type)\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/utils.rs",
    "content": "use crate::parser::types::{Basic, ResolveState, Type};\n\npub fn unwrap_promise<'a>(state: &ResolveState, typ: &'a Type) -> &'a Type {\n    if let Type::Named(named) = &typ {\n        if named.obj.name.as_deref() == Some(\"Promise\") && state.is_universe(named.obj.module_id) {\n            if let Some(t) = named.type_arguments.first() {\n                return t;\n            }\n        }\n    }\n    typ\n}\n\npub fn drop_empty_or_void(typ: Type) -> Option<Type> {\n    match typ {\n        Type::Interface(iface) => {\n            if iface.fields.is_empty() && iface.index.is_none() {\n                None\n            } else {\n                Some(Type::Interface(iface))\n            }\n        }\n        Type::Basic(Basic::Void) => None,\n        _ => Some(typ),\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/validation.rs",
    "content": "use crate::encore::parser::schema::v1 as schema;\nuse core::hash::{Hash, Hasher};\nuse serde::Serialize;\nuse std::{\n    error::Error,\n    fmt::{Display, Write},\n    ops::Deref,\n};\n\nuse super::{Basic, Custom, Type};\n\n#[derive(Debug, Clone, Hash, Serialize, PartialEq, Eq)]\npub enum Expr {\n    Rule(Rule),\n    And(Vec<Expr>),\n    Or(Vec<Expr>),\n}\n\nimpl Display for Expr {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Rule(r) => f.write_fmt(format_args!(\"{r}\")),\n            Self::And(a) => {\n                f.write_char('(')?;\n                for (i, e) in a.iter().enumerate() {\n                    if i > 0 {\n                        f.write_str(\" & \")?;\n                    }\n                    f.write_fmt(format_args!(\"{e}\"))?;\n                }\n                f.write_char(')')\n            }\n            Self::Or(a) => {\n                f.write_char('(')?;\n                for (i, e) in a.iter().enumerate() {\n                    if i > 0 {\n                        f.write_str(\" | \")?;\n                    }\n                    f.write_fmt(format_args!(\"{e}\"))?;\n                }\n                f.write_char(')')\n            }\n        }\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize, PartialEq, Eq)]\npub enum Rule {\n    MinLen(u64),\n    MaxLen(u64),\n    MinVal(N),\n    MaxVal(N),\n    StartsWith(String),\n    EndsWith(String),\n    MatchesRegexp(String),\n    Is(Is),\n}\n\nimpl Display for Rule {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::MinLen(n) => f.write_fmt(format_args!(\"MinLen<{n}>\")),\n            Self::MaxLen(n) => f.write_fmt(format_args!(\"MaxLen<{n}>\")),\n            Self::MinVal(n) => f.write_fmt(format_args!(\"Min<{n}>\")),\n            Self::MaxVal(n) => f.write_fmt(format_args!(\"Max<{n}>\")),\n            Self::StartsWith(s) => f.write_fmt(format_args!(\"StartsWith<{s:#?}>\")),\n            Self::EndsWith(s) => f.write_fmt(format_args!(\"EndsWith<{s:#?}\")),\n            Self::MatchesRegexp(s) => f.write_fmt(format_args!(\"MatchesRegexp<{s:#?}\")),\n            Self::Is(Is::Email) => f.write_str(\"IsEmail\"),\n            Self::Is(Is::Url) => f.write_str(\"IsURL\"),\n        }\n    }\n}\n\n#[derive(Debug, Clone, Hash, Serialize, PartialEq, Eq)]\npub enum Is {\n    Email,\n    Url,\n}\n\nimpl Rule {\n    pub fn merge_and(&self, other: &Self) -> Option<Self> {\n        use Rule::*;\n        Some(match (self, other) {\n            (MinLen(a), MinLen(b)) => MinLen((*a).max(*b)),\n            (MaxLen(a), MaxLen(b)) => MaxLen((*a).min(*b)),\n            (MinVal(a), MinVal(b)) => MinVal(N((*a).max(**b))),\n            (MaxVal(a), MaxVal(b)) => MaxVal(N((*a).min(**b))),\n            _ => return None,\n        })\n    }\n\n    pub fn merge_or(&self, other: &Self) -> Option<Self> {\n        use Rule::*;\n        Some(match (self, other) {\n            (MinLen(a), MinLen(b)) => MinLen((*a).min(*b)),\n            (MaxLen(a), MaxLen(b)) => MaxLen((*a).max(*b)),\n            (MinVal(a), MinVal(b)) => MinVal(N((*a).min(**b))),\n            (MaxVal(a), MaxVal(b)) => MaxVal(N((*a).max(**b))),\n            _ => return None,\n        })\n    }\n\n    pub fn to_pb(&self) -> schema::validation_rule::Rule {\n        use schema::validation_rule::Rule as VR;\n        match self {\n            Rule::MinLen(n) => VR::MinLen(*n),\n            Rule::MaxLen(n) => VR::MaxLen(*n),\n            Rule::MinVal(n) => VR::MinVal(**n),\n            Rule::MaxVal(n) => VR::MaxVal(**n),\n            Rule::StartsWith(str) => VR::StartsWith(str.clone()),\n            Rule::EndsWith(str) => VR::EndsWith(str.clone()),\n            Rule::MatchesRegexp(str) => VR::MatchesRegexp(str.clone()),\n            Rule::Is(is) => VR::Is(match is {\n                Is::Email => schema::validation_rule::Is::Email,\n                Is::Url => schema::validation_rule::Is::Url,\n            } as i32),\n        }\n    }\n\n    pub fn supports_type(&self, typ: &Type) -> bool {\n        // If this is a WireSpec, unwrap it as it is intended to be transparent.\n        let typ = match typ {\n            Type::Custom(Custom::WireSpec(spec)) => &spec.underlying,\n            _ => typ,\n        };\n\n        match self {\n            Self::MinLen(_) | Self::MaxLen(_) => {\n                matches!(typ, Type::Array(_) | Type::Basic(Basic::String))\n            }\n            Self::MinVal(_) | Self::MaxVal(_) => {\n                matches!(\n                    typ,\n                    Type::Basic(Basic::Number) | Type::Custom(Custom::Decimal)\n                )\n            }\n            Self::StartsWith(_)\n            | Self::EndsWith(_)\n            | Self::MatchesRegexp(_)\n            | Self::Is(Is::Email)\n            | Self::Is(Is::Url) => {\n                matches!(typ, Type::Basic(Basic::String))\n            }\n        }\n    }\n}\n\nimpl Expr {\n    pub fn and(self, other: Self) -> Self {\n        match (self, other) {\n            (Expr::And(mut a), Expr::And(mut b)) => {\n                // Can we merge any of the rules into a?\n                a.append(&mut b);\n                Expr::And(a)\n            }\n            (Expr::And(mut a), b) => {\n                a.push(b);\n                Expr::And(a)\n            }\n            (a, Expr::And(mut b)) => {\n                b.insert(0, a);\n                Expr::And(b)\n            }\n            (a, b) => Expr::And(vec![a, b]),\n        }\n    }\n\n    pub fn or(self, other: Self) -> Self {\n        match (self, other) {\n            (Expr::Or(mut a), Expr::Or(mut b)) => {\n                a.append(&mut b);\n                Expr::Or(a)\n            }\n            (Expr::Or(mut a), b) => {\n                a.push(b);\n                Expr::Or(a)\n            }\n            (a, Expr::Or(mut b)) => {\n                b.insert(0, a);\n                Expr::Or(b)\n            }\n            (a, b) => Expr::Or(vec![a, b]),\n        }\n    }\n\n    pub fn rule(rule: Rule) -> Self {\n        Expr::Rule(rule)\n    }\n\n    pub fn simplify(self) -> Self {\n        match self {\n            Self::And(mut exprs) => {\n                let mut i = 0;\n                let mut size = exprs.len();\n                while i < size {\n                    if !matches!(&exprs[i], Expr::Rule(_)) {\n                        i += 1;\n                        continue;\n                    };\n\n                    let j = i + 1;\n                    let (a, b) = exprs.split_at_mut(j);\n                    let Expr::Rule(i_rule) = &mut a[i] else {\n                        panic!(\"logic error\");\n                    };\n\n                    let mut b_size = b.len();\n                    let mut b_idx = 0;\n                    'outer: while b_idx < b_size {\n                        if let Expr::Rule(other) = &b[b_idx] {\n                            if let Some(merged) = i_rule.merge_and(other) {\n                                *i_rule = merged;\n\n                                // Swap this element to the end of b\n                                // and update the sizes.\n                                b.swap(b_idx, b_size - 1);\n                                size -= 1;\n                                b_size -= 1;\n\n                                // Don't increment the index since we now have\n                                // a new element at the current index.\n                                continue 'outer;\n                            }\n                        }\n                        b_idx += 1;\n                    }\n\n                    i += 1;\n                }\n\n                exprs.truncate(size);\n                if exprs.len() == 1 {\n                    exprs.pop().unwrap()\n                } else {\n                    Self::And(exprs)\n                }\n            }\n\n            Self::Or(mut exprs) => {\n                let mut i = 0;\n                let mut size = exprs.len();\n                while i < size {\n                    if !matches!(&exprs[i], Expr::Rule(_)) {\n                        i += 1;\n                        continue;\n                    };\n\n                    let j = i + 1;\n                    let (a, b) = exprs.split_at_mut(j);\n                    let Expr::Rule(i_rule) = &mut a[i] else {\n                        panic!(\"logic error\");\n                    };\n\n                    let mut b_size = b.len();\n                    let mut b_idx = 0;\n                    'outer: while b_idx < b_size {\n                        if let Expr::Rule(other) = &b[b_idx] {\n                            if let Some(merged) = i_rule.merge_or(other) {\n                                *i_rule = merged;\n\n                                // Swap this element to the end of b\n                                // and update the sizes.\n                                b.swap(b_idx, b_size - 1);\n                                size -= 1;\n                                b_size -= 1;\n\n                                // Don't increment the index since we now have\n                                // a new element at the current index.\n                                continue 'outer;\n                            }\n                        }\n                        b_idx += 1;\n                    }\n\n                    i += 1;\n                }\n\n                exprs.truncate(size);\n                if exprs.len() == 1 {\n                    exprs.pop().unwrap()\n                } else {\n                    Self::Or(exprs)\n                }\n            }\n\n            _ => self,\n        }\n    }\n\n    pub fn to_pb(&self) -> schema::ValidationExpr {\n        use schema::validation_expr::Expr as VE;\n\n        schema::ValidationExpr {\n            expr: Some(match self {\n                Expr::Rule(r) => VE::Rule(schema::ValidationRule {\n                    rule: Some(r.to_pb()),\n                }),\n                Expr::And(exprs) => VE::And(schema::validation_expr::And {\n                    exprs: exprs.iter().map(Self::to_pb).collect(),\n                }),\n                Expr::Or(exprs) => VE::Or(schema::validation_expr::Or {\n                    exprs: exprs.iter().map(Self::to_pb).collect(),\n                }),\n            }),\n        }\n    }\n\n    pub fn supports_type<'a>(\n        &'a self,\n        typ: &'a Type,\n    ) -> Result<(), UnsupportedValidationsError<'a>> {\n        match self {\n            Self::And(exprs) | Self::Or(exprs) => {\n                let mut errors = Vec::new();\n                for expr in exprs {\n                    if let Err(e) = expr.supports_type(typ) {\n                        errors.extend(e.0);\n                    }\n                }\n                if errors.is_empty() {\n                    Ok(())\n                } else {\n                    Err(UnsupportedValidationsError(errors))\n                }\n            }\n            Self::Rule(rule) => {\n                if !rule.supports_type(typ) {\n                    let v = UnsupportedValidation { typ, rule };\n                    Err(UnsupportedValidationsError(vec![v]))\n                } else {\n                    Ok(())\n                }\n            }\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct UnsupportedValidation<'a> {\n    pub typ: &'a Type,\n    pub rule: &'a Rule,\n}\n\n#[derive(Debug, Clone)]\npub struct UnsupportedValidationsError<'a>(pub Vec<UnsupportedValidation<'a>>);\n\nimpl Display for UnsupportedValidation<'_> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_fmt(format_args!(\n            \"{} cannot be applied to {}\",\n            self.rule, self.typ\n        ))\n    }\n}\n\nimpl Display for UnsupportedValidationsError<'_> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        if self.0.len() == 1 {\n            write!(f, \"unsupported validation: {}\", &self.0[0])\n        } else {\n            f.write_str(\"unsupported validation: \")?;\n            for (i, rule) in self.0.iter().enumerate() {\n                if i > 0 {\n                    f.write_str(\", \")?;\n                }\n                write!(f, \"{rule}\")?;\n            }\n            Ok(())\n        }\n    }\n}\n\nimpl Error for UnsupportedValidationsError<'_> {}\nimpl Error for UnsupportedValidation<'_> {}\n\n#[derive(Debug, Clone, Copy, Serialize)]\npub struct N(pub f64);\n\nimpl Display for N {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\nimpl Deref for N {\n    type Target = f64;\n\n    fn deref(&self) -> &f64 {\n        &self.0\n    }\n}\n\nimpl PartialEq for N {\n    fn eq(&self, other: &Self) -> bool {\n        self.0 == other.0\n    }\n}\n\nimpl Eq for N {}\n\nimpl Hash for N {\n    fn hash<H: Hasher>(&self, h: &mut H) {\n        if self.0 == 0.0f64 {\n            // There are 2 zero representations, +0 and -0, which\n            // compare equal but have different bits. We use the +0 hash\n            // for both so that hash(+0) == hash(-0).\n            0.0f64.to_bits().hash(h);\n        } else {\n            self.0.to_bits().hash(h);\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn test_simplify() {\n        use super::*;\n\n        {\n            let expr = Expr::Or(vec![\n                Expr::Rule(Rule::MinLen(10)),\n                Expr::Rule(Rule::MinLen(30)),\n            ]);\n            let simplified = expr.simplify();\n            assert_eq!(simplified, Expr::Rule(Rule::MinLen(10)));\n        }\n\n        {\n            let expr = Expr::And(vec![\n                Expr::Rule(Rule::MaxVal(N(10.0))),\n                Expr::Rule(Rule::MaxVal(N(30.0))),\n            ]);\n            let simplified = expr.simplify();\n            assert_eq!(simplified, Expr::Rule(Rule::MaxVal(N(10.0))));\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/types/visitor.rs",
    "content": "use std::collections::HashSet;\n\nuse litparser::Sp;\n\nuse super::{\n    validation, Array, Basic, ClassType, Conditional, Custom, EnumType, FunctionParam,\n    FunctionType, Generic, Index, Inferred, Interface, InterfaceField, Intersection, Keyof,\n    Literal, Mapped, MappedKeyType, Named, ObjectId, Optional, ResolveState, This, Tuple, Type,\n    TypeParam, Union, Validated, WireSpec,\n};\n\npub trait Visit {\n    fn resolve_state(&self) -> &ResolveState;\n    fn seen_decls(&mut self) -> &mut HashSet<ObjectId>;\n\n    #[inline]\n    fn visit_basic(&mut self, node: &Basic) {\n        <Basic as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_array(&mut self, node: &Array) {\n        <Array as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_interface(&mut self, node: &Interface) {\n        <Interface as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_interface_field(&mut self, node: &InterfaceField) {\n        <InterfaceField as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_union(&mut self, node: &Union) {\n        <Union as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_tuple(&mut self, node: &Tuple) {\n        <Tuple as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_literal(&mut self, node: &Literal) {\n        <Literal as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_class(&mut self, node: &ClassType) {\n        <ClassType as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_enum(&mut self, node: &EnumType) {\n        <EnumType as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_named(&mut self, node: &Named) {\n        <Named as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_optional(&mut self, node: &Optional) {\n        <Optional as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_this(&mut self, node: &This) {\n        <This as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_validation(&mut self, node: &validation::Expr) {\n        <validation::Expr as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_validated(&mut self, node: &Validated) {\n        <Validated as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_type(&mut self, node: &Type) {\n        <Type as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_types(&mut self, node: &[Type]) {\n        <[Type] as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_generic(&mut self, node: &Generic) {\n        <Generic as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_type_param(&mut self, node: &TypeParam) {\n        <TypeParam as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_index(&mut self, node: &Index) {\n        <Index as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_mapped(&mut self, node: &Mapped) {\n        <Mapped as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_mapped_key_type(&mut self, node: &MappedKeyType) {\n        <MappedKeyType as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_keyof(&mut self, node: &Keyof) {\n        <Keyof as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_conditional(&mut self, node: &Conditional) {\n        <Conditional as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_intersection(&mut self, node: &Intersection) {\n        <Intersection as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_inferred(&mut self, node: &Inferred) {\n        <Inferred as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_custom(&mut self, node: &Custom) {\n        <Custom as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_wire_spec(&mut self, node: &WireSpec) {\n        <WireSpec as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_function(&mut self, node: &FunctionType) {\n        <FunctionType as VisitWith<Self>>::visit_children_with(node, self)\n    }\n\n    #[inline]\n    fn visit_function_param(&mut self, node: &FunctionParam) {\n        <FunctionParam as VisitWith<Self>>::visit_children_with(node, self)\n    }\n}\n\npub trait VisitWith<V: ?Sized + Visit> {\n    fn visit_with(&self, visitor: &mut V);\n    fn visit_children_with(&self, visitor: &mut V);\n}\n\nimpl<V> VisitWith<V> for This\nwhere\n    V: ?Sized + Visit,\n{\n    #[inline]\n    fn visit_with(&self, _visitor: &mut V) {}\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Basic {\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_basic(visitor, self)\n    }\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Interface {\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_interface(visitor, self)\n    }\n\n    fn visit_children_with(&self, visitor: &mut V) {\n        self.fields\n            .iter()\n            .for_each(|item| <InterfaceField as VisitWith<V>>::visit_with(item, visitor))\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for InterfaceField {\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_interface_field(visitor, self)\n    }\n\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.typ, visitor)\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Array {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_array(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.0, visitor)\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Union {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_union(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        self.types\n            .iter()\n            .for_each(|item| <Type as VisitWith<V>>::visit_with(item, visitor))\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Tuple {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_tuple(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        self.types\n            .iter()\n            .for_each(|item| <Type as VisitWith<V>>::visit_with(item, visitor))\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Optional {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_optional(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.0, visitor)\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for validation::Expr {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_validation(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Validated {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_validated(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.typ, visitor);\n        <validation::Expr as VisitWith<V>>::visit_with(&self.expr, visitor);\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Literal {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_literal(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for ClassType {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_class(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for EnumType {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_enum(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for TypeParam {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_type_param(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        if let Some(constraint) = &self.constraint {\n            <Type as VisitWith<V>>::visit_with(constraint, visitor);\n        }\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Index {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_index(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.source, visitor);\n        <Type as VisitWith<V>>::visit_with(&self.index, visitor);\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Mapped {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_mapped(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.in_type, visitor);\n        <Type as VisitWith<V>>::visit_with(&self.value_type, visitor);\n        if let Some(as_type) = &self.as_type {\n            <Type as VisitWith<V>>::visit_with(as_type, visitor);\n        }\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for MappedKeyType {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_mapped_key_type(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Keyof {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_keyof(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.0, visitor);\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Conditional {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_conditional(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.check_type, visitor);\n        <Type as VisitWith<V>>::visit_with(&self.extends_type, visitor);\n        <Type as VisitWith<V>>::visit_with(&self.true_type, visitor);\n        <Type as VisitWith<V>>::visit_with(&self.false_type, visitor);\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Intersection {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_intersection(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.x, visitor);\n        <Type as VisitWith<V>>::visit_with(&self.y, visitor);\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Inferred {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_inferred(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, _visitor: &mut V) {}\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Named {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_named(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        // Only recurse if we haven't seen this object before, to avoid infinite recursion\n        // with recursive types.\n        if visitor.seen_decls().insert(self.obj.id) {\n            let underlying = self.underlying(visitor.resolve_state());\n            <Type as VisitWith<V>>::visit_with(&underlying, visitor);\n        }\n\n        self.type_arguments\n            .iter()\n            .for_each(|item| <Type as VisitWith<V>>::visit_with(item, visitor))\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Generic {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_generic(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        match self {\n            Generic::TypeParam(inner) => <TypeParam as VisitWith<V>>::visit_with(inner, visitor),\n            Generic::Index(inner) => <Index as VisitWith<V>>::visit_with(inner, visitor),\n            Generic::Mapped(inner) => <Mapped as VisitWith<V>>::visit_with(inner, visitor),\n            Generic::MappedKeyType(inner) => {\n                <MappedKeyType as VisitWith<V>>::visit_with(inner, visitor)\n            }\n            Generic::Keyof(inner) => <Keyof as VisitWith<V>>::visit_with(inner, visitor),\n            Generic::Conditional(inner) => {\n                <Conditional as VisitWith<V>>::visit_with(inner, visitor)\n            }\n            Generic::Intersection(inner) => {\n                <Intersection as VisitWith<V>>::visit_with(inner, visitor)\n            }\n            Generic::Inferred(inner) => <Inferred as VisitWith<V>>::visit_with(inner, visitor),\n        }\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Custom {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_custom(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        match self {\n            Custom::WireSpec(inner) => <WireSpec as VisitWith<V>>::visit_with(inner, visitor),\n            Custom::Decimal => {} // No children to visit\n        }\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for WireSpec {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_wire_spec(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.underlying, visitor)\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for Type {\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_type(visitor, self)\n    }\n\n    fn visit_children_with(&self, visitor: &mut V) {\n        match self {\n            Self::Basic(basic) => <Basic as VisitWith<V>>::visit_with(basic, visitor),\n            Self::Array(array) => <Array as VisitWith<V>>::visit_with(array, visitor),\n            Type::Interface(interface) => {\n                <Interface as VisitWith<V>>::visit_with(interface, visitor)\n            }\n            Type::Union(union) => <Union as VisitWith<V>>::visit_with(union, visitor),\n            Type::Tuple(tuple) => <Tuple as VisitWith<V>>::visit_with(tuple, visitor),\n            Type::Literal(literal) => <Literal as VisitWith<V>>::visit_with(literal, visitor),\n            Type::Class(class_type) => <ClassType as VisitWith<V>>::visit_with(class_type, visitor),\n            Type::Enum(enum_type) => <EnumType as VisitWith<V>>::visit_with(enum_type, visitor),\n            Type::Named(named) => <Named as VisitWith<V>>::visit_with(named, visitor),\n            Type::Optional(opt) => <Optional as VisitWith<V>>::visit_with(opt, visitor),\n            Type::This(t) => <This as VisitWith<V>>::visit_with(t, visitor),\n            Type::Generic(generic) => <Generic as VisitWith<V>>::visit_with(generic, visitor),\n            Type::Validation(expr) => <validation::Expr as VisitWith<V>>::visit_with(expr, visitor),\n            Type::Validated(expr) => <Validated as VisitWith<V>>::visit_with(expr, visitor),\n            Type::Custom(custom) => <Custom as VisitWith<V>>::visit_with(custom, visitor),\n            Type::Function(func) => <FunctionType as VisitWith<V>>::visit_with(func, visitor),\n        }\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for [Type] {\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_types(visitor, self)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        self.iter()\n            .for_each(|item| <Type as VisitWith<V>>::visit_with(item, visitor))\n    }\n}\n\nimpl<V, T> VisitWith<V> for std::boxed::Box<T>\nwhere\n    V: ?Sized + Visit,\n    T: VisitWith<V>,\n{\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <T as VisitWith<V>>::visit_with(&**self, visitor)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <T as VisitWith<V>>::visit_children_with(&**self, visitor)\n    }\n}\n\nimpl<V, T> VisitWith<V> for Sp<T>\nwhere\n    V: ?Sized + Visit,\n    T: VisitWith<V>,\n{\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <T as VisitWith<V>>::visit_with(&**self, visitor)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <T as VisitWith<V>>::visit_children_with(&**self, visitor)\n    }\n}\n\nimpl<V, T> VisitWith<V> for std::vec::Vec<T>\nwhere\n    V: ?Sized + Visit,\n    [T]: VisitWith<V>,\n{\n    #[inline]\n    fn visit_with(&self, visitor: &mut V) {\n        <[T] as VisitWith<V>>::visit_with(self, visitor)\n    }\n\n    #[inline]\n    fn visit_children_with(&self, visitor: &mut V) {\n        <[T] as VisitWith<V>>::visit_children_with(self, visitor)\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for FunctionType {\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_function(visitor, self)\n    }\n\n    fn visit_children_with(&self, visitor: &mut V) {\n        self.params\n            .iter()\n            .for_each(|item| <FunctionParam as VisitWith<V>>::visit_with(item, visitor));\n        <Type as VisitWith<V>>::visit_with(&self.return_type, visitor);\n    }\n}\n\nimpl<V: ?Sized + Visit> VisitWith<V> for FunctionParam {\n    fn visit_with(&self, visitor: &mut V) {\n        <V as Visit>::visit_function_param(visitor, self)\n    }\n\n    fn visit_children_with(&self, visitor: &mut V) {\n        <Type as VisitWith<V>>::visit_with(&self.typ, visitor);\n    }\n}\n"
  },
  {
    "path": "tsparser/src/parser/universe.ts",
    "content": "\nexport class Promise<T> { }\n\n/**\n * Make all properties in T optional\n */\nexport type Partial<T> = {\n  [P in keyof T]?: T[P];\n};\n\n/**\n * Make all properties in T required\n */\nexport type Required<T> = {\n  [P in keyof T]-?: T[P];\n};\n\n/**\n * Make all properties in T readonly\n */\nexport type Readonly<T> = {\n  readonly [P in keyof T]: T[P];\n};\n\n/**\n * From T, pick a set of properties whose keys are in the union K\n */\nexport type Pick<T, K extends keyof T> = {\n  [P in K]: T[P];\n};\n\n/**\n * Construct a type with a set of properties K of type T\n */\nexport type Record<K extends keyof any, T> = {\n  [P in K]: T;\n};\n\n/**\n * Exclude from T those types that are assignable to U\n */\nexport type Exclude<T, U> = T extends U ? never : T;\n\n/**\n * Extract from T those types that are assignable to U\n */\nexport type Extract<T, U> = T extends U ? T : never;\n\n/**\n * Construct a type with the properties of T except for those in type K.\n */\nexport type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;\n\n/**\n * Exclude null and undefined from T\n */\nexport type NonNullable<T> = T & {};\n\n/** Builtin date class. */\nexport class Date { };\n\n/** Builtin array type */\nexport type Array<T> = T[];\n"
  },
  {
    "path": "tsparser/src/parser/usageparser/mod.rs",
    "content": "use std::collections::HashMap;\n\nuse swc_common::errors::HANDLER;\nuse swc_common::sync::Lrc;\nuse swc_common::Spanned;\nuse swc_ecma_ast as ast;\nuse swc_ecma_visit::fields::{\n    CallExprField, CalleeField, MemberExprField, NewExprField, TaggedTplField,\n};\nuse swc_ecma_visit::{AstNodePath, AstParentNodeRef, VisitAstPath, VisitWithPath};\n\nuse crate::parser::module_loader::{Module, ModuleId, ModuleLoader};\nuse crate::parser::resourceparser::bind::Bind;\nuse crate::parser::resources::{apis, infra, Resource};\nuse crate::parser::Range;\n\nuse super::types::TypeChecker;\n\n#[derive(Debug)]\npub struct UsageExpr {\n    pub range: Range,\n    pub bind: Lrc<Bind>,\n    pub kind: UsageExprKind,\n}\n\nimpl Spanned for UsageExpr {\n    fn span(&self) -> swc_common::Span {\n        self.range.to_span()\n    }\n}\n\n#[derive(Debug)]\npub enum UsageExprKind {\n    /// A field on a resource being accessed.\n    FieldAccess(FieldAccess),\n\n    /// A method on a resource being called.\n    MethodCall(MethodCall),\n\n    /// A method on a resource being called, as a tagged template literal.\n    TemplateCall(TemplateCall),\n\n    /// A resource being called as a function.\n    Callee(Callee),\n\n    /// A resource being passed as a function argument.\n    CallArg(CallArg),\n\n    /// A resource passed as a constructor argument.\n    ConstructorArg(ConstructorArg),\n\n    /// Any other resource usage.\n    Other(Other),\n}\n\n#[derive(Debug)]\npub struct MethodCall {\n    pub method: ast::Ident,\n    pub call: ast::CallExpr,\n}\n\n#[derive(Debug)]\npub struct TemplateCall {\n    pub method: ast::Ident,\n    pub tpl: ast::TaggedTpl,\n}\n\n#[derive(Debug)]\npub struct FieldAccess {\n    pub field: ast::Ident,\n}\n\n#[derive(Debug)]\npub struct Callee {\n    _call: ast::CallExpr,\n}\n\n#[derive(Debug)]\npub struct CallArg {\n    pub arg_idx: usize,\n    _call: ast::CallExpr,\n}\n\n#[derive(Debug)]\npub struct ConstructorArg {\n    pub arg_idx: usize,\n    _call: ast::NewExpr,\n}\n\n#[derive(Debug)]\npub struct Other {\n    _enclosing_expr: ast::Expr,\n}\n\npub struct UsageResolver<'a> {\n    module_loader: &'a ModuleLoader,\n    type_checker: &'a TypeChecker,\n    resources: &'a [Resource],\n    binds_by_module: HashMap<ModuleId, Vec<Lrc<Bind>>>,\n}\n\nimpl<'a> UsageResolver<'a> {\n    pub fn new(\n        module_loader: &'a ModuleLoader,\n        type_checker: &'a TypeChecker,\n        resources: &'a [Resource],\n        binds: &[Lrc<Bind>],\n    ) -> Self {\n        let mut resolver = Self {\n            module_loader,\n            type_checker,\n            resources,\n            binds_by_module: HashMap::new(),\n        };\n\n        for b in binds {\n            resolver\n                .binds_by_module\n                .entry(b.module_id)\n                .or_default()\n                .push(b.clone());\n        }\n\n        resolver\n    }\n\n    pub fn scan_usage_exprs(&self, module: &Module) -> Vec<UsageExpr> {\n        let external = self.external_binds_to_scan_for(module);\n        let internal = self.internal_binds_to_scan_for(module);\n        let combined: Vec<BindToScan> = external.into_iter().chain(internal).collect();\n\n        let mut visitor = UsageVisitor::new(&combined);\n        module\n            .ast\n            .visit_with_path(&mut visitor, &mut Default::default());\n\n        visitor.usages\n    }\n\n    /// external_binds_to_scan_for computes the external binds to scan for given a module.\n    fn external_binds_to_scan_for(&self, module: &Module) -> Vec<BindToScan<'_>> {\n        let mut external = Vec::new();\n\n        for imp in module.imports() {\n            // Type-only imports don't contribute to usage.\n            if imp.type_only {\n                continue;\n            }\n\n            // Resolve the module\n            let resolved_module = match self\n                .module_loader\n                .resolve_import(&module.swc_file_path, &imp.src.value)\n            {\n                Ok(None) => continue,\n                Ok(Some(resolved_module)) => resolved_module,\n                Err(err) => {\n                    HANDLER.with(|handler| {\n                        handler.span_err(err.span().unwrap_or_else(|| imp.span()), &err.msg())\n                    });\n                    continue;\n                }\n            };\n\n            let resolved_binds = self.binds_by_module.get(&resolved_module.id);\n            for names in &imp.specifiers {\n                match names {\n                    ast::ImportSpecifier::Named(named) => {\n                        // src_name is the original name of the bind in the module it was defined.\n                        let src_name: &str = match &named.imported {\n                            Some(ast::ModuleExportName::Ident(id)) => id.sym.as_ref(),\n                            Some(ast::ModuleExportName::Str(id)) => id.value.as_ref(),\n                            None => named.local.sym.as_ref(),\n                        };\n\n                        // found_bind is the matching bind in the resolved module, if any.\n                        let found_bind = resolved_binds\n                            .into_iter()\n                            .flatten()\n                            .find(|b| b.name.as_ref().is_some_and(|i| i == src_name));\n\n                        if let Some(bind) = found_bind {\n                            external.push(BindToScan {\n                                bound_name: named.local.to_id(),\n                                selector: None,\n                                bind: bind.to_owned(),\n                            });\n                        }\n                    }\n\n                    spec @ ast::ImportSpecifier::Default(_)\n                    | spec @ ast::ImportSpecifier::Namespace(_) => {\n                        let local_name = match &spec {\n                            ast::ImportSpecifier::Default(default) => &default.local,\n                            ast::ImportSpecifier::Namespace(ns) => &ns.local,\n                            _ => unreachable!(),\n                        };\n\n                        for bind in resolved_binds.into_iter().flatten() {\n                            // don't add anonymous binds e.g `const _ = new ...`\n                            if bind.name.is_some() {\n                                external.push(BindToScan {\n                                    bound_name: local_name.to_id(),\n                                    selector: bind.name.as_deref(),\n                                    bind: bind.to_owned(),\n                                });\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        external\n    }\n\n    /// internal_binds_to_scan_for computes the internal binds to scan for given a module.\n    fn internal_binds_to_scan_for(&self, module: &Module) -> Vec<BindToScan<'_>> {\n        let mut internal = Vec::new();\n\n        if let Some(module_binds) = self.binds_by_module.get(&module.id) {\n            for b in module_binds {\n                if let Some(id) = &b.internal_bound_id {\n                    internal.push(BindToScan {\n                        bound_name: id.to_owned(),\n                        selector: None,\n                        bind: b.to_owned(),\n                    });\n                }\n            }\n        }\n\n        internal\n    }\n}\n\n#[derive(Debug)]\npub enum Usage {\n    CallEndpoint(apis::api::CallEndpointUsage),\n    Topic(infra::pubsub_topic::TopicUsage),\n    AccessDatabase(infra::sqldb::AccessDatabaseUsage),\n    Bucket(infra::objects::BucketUsage),\n    Metric(infra::metrics::MetricUsage),\n    CacheCluster(infra::cache::CacheClusterUsage),\n}\n\npub struct ResolveUsageData<'a> {\n    pub module: &'a Lrc<Module>,\n    pub type_checker: &'a TypeChecker,\n    pub expr: &'a UsageExpr,\n    pub resources: &'a [Resource],\n}\n\nimpl UsageResolver<'_> {\n    pub fn resolve_usage(&self, module: &Lrc<Module>, exprs: &[UsageExpr]) -> Vec<Usage> {\n        let mut usages = Vec::new();\n        for expr in exprs {\n            let data = ResolveUsageData {\n                module,\n                type_checker: self.type_checker,\n                expr,\n                resources: self.resources,\n            };\n            match &expr.bind.resource {\n                Resource::APIEndpoint(ep) => {\n                    if let Some(usage) = apis::api::resolve_endpoint_usage(&data, ep.clone()) {\n                        usages.push(usage)\n                    }\n                }\n                Resource::ServiceClient(client) => {\n                    if let Some(u) =\n                        apis::service_client::resolve_service_client_usage(&data, client.clone())\n                    {\n                        usages.push(u)\n                    }\n                }\n                Resource::PubSubTopic(topic) => {\n                    if let Some(u) = infra::pubsub_topic::resolve_topic_usage(&data, topic.clone())\n                    {\n                        usages.push(u)\n                    }\n                }\n                Resource::SQLDatabase(db) => {\n                    if let Some(u) = infra::sqldb::resolve_database_usage(&data, db.clone()) {\n                        usages.push(u)\n                    }\n                }\n                Resource::Bucket(bkt) => {\n                    if let Some(u) = infra::objects::resolve_bucket_usage(&data, bkt.clone()) {\n                        usages.push(u)\n                    }\n                }\n                Resource::Metric(metric) => {\n                    if let Some(u) = infra::metrics::resolve_metric_usage(&data, metric.clone()) {\n                        usages.push(u)\n                    }\n                }\n                Resource::CacheCluster(cluster) => {\n                    if let Some(u) =\n                        infra::cache::resolve_cache_cluster_usage(&data, cluster.clone())\n                    {\n                        usages.push(u)\n                    }\n                }\n                _ => {}\n            }\n        }\n\n        usages\n    }\n}\n\n#[derive(Debug, PartialEq)]\nstruct BindToScan<'a> {\n    /// The bound name within the module being parsed.\n    /// If [selector] is None it's the id of the bind itself (`import { Name } from 'module'`).\n    /// Otherwise it's the id of the module (`import module from 'module'`), and the specific\n    /// bind's name is found in [selector].\n    bound_name: ast::Id,\n\n    /// The selector within the bound name, in the case of module imports.\n    selector: Option<&'a str>,\n\n    /// The bind itself.\n    bind: Lrc<Bind>,\n}\n\nstruct UsageVisitor<'a> {\n    binds: HashMap<ast::Id, &'a BindToScan<'a>>,\n    usages: Vec<UsageExpr>,\n}\n\nimpl<'a> UsageVisitor<'a> {\n    pub fn new(binds: &'a [BindToScan]) -> Self {\n        let mut map = HashMap::with_capacity(binds.len());\n        for b in binds {\n            map.insert(b.bound_name.clone(), b);\n        }\n\n        Self {\n            binds: map,\n            usages: Vec::new(),\n        }\n    }\n\n    /// Report whether the given id represents the bind definition itself.\n    fn is_bind_def(&self, bind: &Bind, id: &ast::Ident) -> bool {\n        bind.range.is_some_and(|r| r.contains(&id.span.into()))\n    }\n\n    /// Report whether the given path represents an import declaration.\n    fn is_import_def(&self, path: &AstNodePath) -> bool {\n        for k in path.kinds().iter() {\n            if let swc_ecma_visit::AstParentKind::ImportDecl(_) = k {\n                return true;\n            }\n        }\n        false\n    }\n\n    fn classify_usage(&self, bind: Lrc<Bind>, path: &AstNodePath) -> Option<UsageExpr> {\n        let idx = path.len() - 1;\n        let parent = path.get(idx - 1);\n        let grandparent = path.get(idx - 2);\n\n        match parent {\n            Some(AstParentNodeRef::MemberExpr(sel, MemberExprField::Obj)) => {\n                // We have a member expression, where the object (\"foo\" in foo.bar) is the bind.\n                // Ensure \"bar\" is a static identifier and not a private field or a computed property.\n                match &sel.prop {\n                    ast::MemberProp::PrivateName(_) => {\n                        // self.errs.emit(\n                        //     private.span.into(),\n                        //     \"cannot use private member for resource\",\n                        //     Error,\n                        // );\n                        None\n                    }\n                    ast::MemberProp::Computed(_) => {\n                        // self.errs.emit(\n                        //     computed.span.into(),\n                        //     \"cannot use computed member for resource\",\n                        //     Error,\n                        // );\n                        None\n                    }\n                    ast::MemberProp::Ident(id) => {\n                        // bind.SomeField or bind.SomeField() or bind.SomeField`foo`\n\n                        if let Some(AstParentNodeRef::CallExpr(call, CallExprField::Callee)) =\n                            path.get(idx - 4)\n                        {\n                            Some(UsageExpr {\n                                range: call.span.into(),\n                                bind: bind.clone(),\n                                kind: UsageExprKind::MethodCall(MethodCall {\n                                    call: (*call).to_owned(),\n                                    method: id.to_owned(),\n                                }),\n                            })\n                        } else if let Some(AstParentNodeRef::TaggedTpl(tpl, TaggedTplField::Tag)) =\n                            path.get(idx - 3)\n                        {\n                            Some(UsageExpr {\n                                range: tpl.span.into(),\n                                bind: bind.clone(),\n                                kind: UsageExprKind::TemplateCall(TemplateCall {\n                                    method: id.to_owned(),\n                                    tpl: (*tpl).to_owned(),\n                                }),\n                            })\n                        } else {\n                            Some(UsageExpr {\n                                range: sel.span.into(),\n                                bind: bind.clone(),\n                                kind: UsageExprKind::FieldAccess(FieldAccess {\n                                    field: id.to_owned(),\n                                }),\n                            })\n                        }\n                    }\n                }\n            }\n\n            Some(AstParentNodeRef::Callee(_, CalleeField::Expr)) => {\n                // This bind is being called as a function.\n                if let Some(AstParentNodeRef::CallExpr(call, _)) = grandparent {\n                    Some(UsageExpr {\n                        range: call.span.into(),\n                        bind: bind.clone(),\n                        kind: UsageExprKind::Callee(Callee {\n                            _call: (*call).to_owned(),\n                        }),\n                    })\n                } else {\n                    // TODO emit error (parent of Callee should always be a Call)\n                    None\n                }\n            }\n\n            _ => {\n                match grandparent {\n                    // The bind is being passed as an argument to a function.\n                    Some(AstParentNodeRef::CallExpr(call, CallExprField::Args(idx))) => {\n                        Some(UsageExpr {\n                            range: call.span.into(),\n                            bind: bind.clone(),\n                            kind: UsageExprKind::CallArg(CallArg {\n                                _call: (*call).to_owned(),\n                                arg_idx: *idx,\n                            }),\n                        })\n                    }\n\n                    Some(AstParentNodeRef::NewExpr(new, NewExprField::Args(idx))) => {\n                        Some(UsageExpr {\n                            range: new.span.into(),\n                            bind: bind.clone(),\n                            kind: UsageExprKind::ConstructorArg(ConstructorArg {\n                                _call: (*new).to_owned(),\n                                arg_idx: *idx,\n                            }),\n                        })\n                    }\n\n                    // Some other expression.\n                    _ => {\n                        // Find the largest enclosing expression.\n                        let enclosing = path.iter().find_map(|node| match node {\n                            AstParentNodeRef::Expr(expr, _) => Some(*expr),\n                            _ => None,\n                        });\n                        enclosing.map(|enclosing| UsageExpr {\n                            range: enclosing.span().into(),\n                            bind: bind.clone(),\n                            kind: UsageExprKind::Other(Other {\n                                _enclosing_expr: enclosing.to_owned(),\n                            }),\n                        })\n                    }\n                }\n            }\n        }\n    }\n}\n\nimpl VisitAstPath for UsageVisitor<'_> {\n    fn visit_ident<'ast: 'r, 'r>(&mut self, n: &'ast ast::Ident, path: &mut AstNodePath<'r>) {\n        if let Some(b) = self.binds.get(&n.to_id()) {\n            // If this ident represents the bind's definition itself, ignore it.\n            if self.is_bind_def(&b.bind, n) {\n                return;\n            }\n\n            // If this ident is part of an import declaration we don't consider that a usage.\n            if self.is_import_def(path) {\n                return;\n            }\n\n            if let Some(u) = self.classify_usage(b.bind.clone(), path) {\n                self.usages.push(u);\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use assert_fs::fixture::PathChild;\n    use assert_fs::TempDir;\n    use std::path::PathBuf;\n    use std::rc::Rc;\n\n    use assert_matches::assert_matches;\n    use swc_common::errors::Handler;\n    use swc_common::{Globals, SourceMap, DUMMY_SP, GLOBALS};\n\n    use crate::parser::parser::ParseContext;\n    use crate::parser::resourceparser::bind::BindKind;\n    use crate::parser::resources::apis::api::{Endpoint, Method, Methods};\n    use crate::parser::resources::apis::encoding::{\n        EndpointEncoding, RequestEncoding, ResponseEncoding,\n    };\n    use crate::parser::resources::Resource;\n    use crate::parser::respath::Path;\n    use crate::testutil::testresolve::TestResolver;\n    use crate::testutil::JS_RUNTIME_PATH;\n\n    use super::*;\n\n    #[test]\n    fn test_scan_external_binds() {\n        let globals = Globals::new();\n        GLOBALS.set(&globals, || {\n            let ar = txtar::from_str(\n                \"\n-- foo.ts --\nimport { Bar } from './bar.ts';\n-- bar.ts --\nexport const Bar = 5;\n        \",\n            );\n\n            let base = PathBuf::from(\"/dummy\");\n            let resolver = Box::new(TestResolver::new(base.to_path_buf(), ar.clone()));\n            let tmp = TempDir::new().unwrap();\n            let app_root = tmp.child(\"app_root\").to_path_buf();\n            let cm: Rc<SourceMap> = Default::default();\n            let errs = Rc::new(Handler::with_tty_emitter(\n                swc_common::errors::ColorConfig::Auto,\n                true,\n                false,\n                Some(cm.clone()),\n            ));\n            let pc = ParseContext::with_resolver(\n                app_root,\n                Some(JS_RUNTIME_PATH.clone()),\n                resolver,\n                cm,\n                errs,\n            )\n            .unwrap();\n            let mods = pc.loader.load_archive(&base, &ar).unwrap();\n\n            let foo_mod = mods.get(&\"/dummy/foo.ts\".into()).unwrap();\n            let bar_mod = mods.get(&\"/dummy/bar.ts\".into()).unwrap();\n\n            let res = Resource::APIEndpoint(Lrc::new(Endpoint {\n                range: Default::default(),\n                service_name: \"svc\".into(),\n                name: \"Bar\".into(),\n                name_range: Default::default(),\n                doc: None,\n                expose: true,\n                raw: false,\n                require_auth: false,\n                body_limit: None,\n                encoding: EndpointEncoding {\n                    span: DUMMY_SP,\n                    default_method: Method::Post,\n                    methods: Methods::Some(vec![Method::Post]),\n                    handshake: None,\n                    req: vec![RequestEncoding {\n                        methods: Methods::Some(vec![Method::Post]),\n                        params: vec![],\n                    }],\n                    resp: ResponseEncoding { params: vec![] },\n                    path: Path::parse(DUMMY_SP, \"/svc.Bar\", Default::default()).unwrap(),\n                    raw_handshake_schema: None,\n                    raw_req_schema: None,\n                    raw_resp_schema: None,\n                },\n                streaming_request: false,\n                streaming_response: false,\n                static_assets: None,\n                tags: vec![],\n                sensitive: false,\n            }));\n\n            let bar_binds = vec![Lrc::new(Bind {\n                kind: BindKind::Create,\n                object: None,\n                id: 1.into(),\n                range: None,\n                name: Some(\"Bar\".into()),\n                resource: res.clone(),\n                internal_bound_id: None,\n                module_id: bar_mod.id,\n            })];\n\n            let resources = [res];\n            let ur = UsageResolver::new(&pc.loader, &pc.type_checker, &resources, &bar_binds);\n\n            let result = ur.external_binds_to_scan_for(foo_mod);\n            assert_eq!(result.len(), 1);\n            assert_eq!(result[0].bind, bar_binds[0]);\n        });\n    }\n\n    #[test]\n    fn test_scan_usage() {\n        let globals = Globals::new();\n        GLOBALS.set(&globals, || {\n            let ar = txtar::from_str(\n                \"\n-- foo.ts --\nimport { Bar } from './bar.ts';\n\nBar.field;      // FieldAccess\nBar.method();   // MethodCall\nBar();          // Callee\nfoo(x, Bar)     // CallArg\nnew Class(Bar); // ConstructorArg\nlet foo = Bar;  // Other\n\n// Inside nested function calls\nconst _ = nested(\n  () => {\n    bb.on('close', async () => {\n      const _ = await Bar.nested_method();\n    })\n  }\n)\n\nBar.tpl`blah`;  // TemplateCall\n\n-- bar.ts --\nexport const Bar = 5;\n            \",\n            );\n\n            let base = PathBuf::from(\"/dummy\");\n            let resolver = Box::new(TestResolver::new(base.clone(), ar.clone()));\n            let tmp = TempDir::new().unwrap();\n            let app_root = tmp.child(\"app_root\").to_path_buf();\n            let cm: Rc<SourceMap> = Default::default();\n            let errs = Rc::new(Handler::with_tty_emitter(\n                swc_common::errors::ColorConfig::Auto,\n                true,\n                false,\n                Some(cm.clone()),\n            ));\n            let pc = ParseContext::with_resolver(app_root, Some(JS_RUNTIME_PATH.clone()), resolver, cm, errs).unwrap();\n            let mods = pc.loader.load_archive(&base, &ar).unwrap();\n\n            let foo_mod = mods.get(&\"/dummy/foo.ts\".into()).unwrap();\n            let bar_mod = mods.get(&\"/dummy/bar.ts\".into()).unwrap();\n\n            let res = Resource::APIEndpoint(Lrc::new(Endpoint {\n                range: Default::default(),\n                name: \"Bar\".to_string(),\n                name_range: Default::default(),\n                service_name: \"svc\".to_string(),\n                doc: None,\n                expose: true,\n                raw: false,\n                require_auth: false,\n                body_limit: None,\n                encoding: EndpointEncoding {\n                    span: DUMMY_SP,\n                    default_method: Method::Post,\n                    methods: Methods::Some(vec![Method::Post]),\n                    handshake: None,\n                    req: vec![RequestEncoding {\n                        methods: Methods::Some(vec![Method::Post]),\n                        params: vec![],\n                    }],\n                    resp: ResponseEncoding {\n                        params: vec![],\n                    },\n                    path: Path::parse(DUMMY_SP, \"/svc.Bar\", Default::default()).unwrap(),\n                    raw_handshake_schema: None,\n                    raw_req_schema: None,\n                    raw_resp_schema: None,\n                },\n                streaming_request: false,\n                streaming_response: false,\n                static_assets: None,\n                tags: vec![],\n                sensitive: false,\n            }));\n            let bar_binds = vec![Lrc::new(Bind {\n                kind: BindKind::Create,\n                object: None,\n                id: 1.into(),\n                range: None,\n                name: Some(\"Bar\".into()),\n                resource: res.clone(),\n                internal_bound_id: None,\n                module_id: bar_mod.id,\n            })];\n\n            let resources = [res];\n            let ur = UsageResolver::new(&pc.loader, &pc.type_checker, &resources, &bar_binds);\n\n            let usages = ur.scan_usage_exprs(foo_mod);\n            assert_eq!(usages.len(), 8);\n\n            assert_matches!(&usages[0].kind, UsageExprKind::FieldAccess(field) if field.field.as_ref() == \"field\");\n            assert_matches!(&usages[1].kind, UsageExprKind::MethodCall(method) if method.method.as_ref() == \"method\");\n            assert_matches!(&usages[2].kind, UsageExprKind::Callee(_));\n            assert_matches!(&usages[3].kind, UsageExprKind::CallArg(arg) if arg.arg_idx == 1);\n            assert_matches!(&usages[4].kind, UsageExprKind::ConstructorArg(arg) if arg.arg_idx == 0);\n            assert_matches!(&usages[5].kind, UsageExprKind::Other(_));\n            assert_matches!(&usages[6].kind, UsageExprKind::MethodCall(method) if method.method.as_ref() == \"nested_method\");\n            assert_matches!(&usages[7].kind, UsageExprKind::TemplateCall(tpl) if tpl.method.as_ref() == \"tpl\");\n        });\n    }\n}\n"
  },
  {
    "path": "tsparser/src/resolve_utils.rs",
    "content": "use std::path::{Path, PathBuf};\n\n/// Split a bare specifier into (package_name, subpath).\n/// - \"foo\" => (\"foo\", \"\")\n/// - \"foo/bar\" => (\"foo\", \"bar\")\n/// - \"@scope/pkg\" => (\"@scope/pkg\", \"\")\n/// - \"@scope/pkg/sub\" => (\"@scope/pkg\", \"sub\")\npub fn split_package_name(target: &str) -> (&str, &str) {\n    match target.find('/') {\n        None => (target, \"\"),\n        Some(idx) => {\n            if target.starts_with('@') {\n                let rem = &target[idx + 1..];\n                match rem.find('/') {\n                    None => (target, \"\"),\n                    Some(rem_idx) => {\n                        let sep = idx + rem_idx + 1;\n                        (&target[..sep], &target[sep + 1..])\n                    }\n                }\n            } else {\n                (&target[..idx], &target[(idx + 1)..])\n            }\n        }\n    }\n}\n\n/// Given a .js/.mjs/.cjs path, return the .d.ts/.d.mts/.d.cts counterpart.\npub fn dts_counterpart(path: &Path) -> Option<PathBuf> {\n    let ext = path.extension()?.to_str()?;\n    let new_ext = match ext {\n        \"js\" => \"d.ts\",\n        \"mjs\" => \"d.mts\",\n        \"cjs\" => \"d.cts\",\n        _ => return None,\n    };\n    let mut dts = path.to_path_buf();\n    dts.set_extension(new_ext);\n    Some(dts)\n}\n"
  },
  {
    "path": "tsparser/src/runtimeresolve/mod.rs",
    "content": "mod node;\nmod tsconfig;\n\npub use node::EncoreRuntimeResolver;\npub use tsconfig::TsConfigPathResolver;\n"
  },
  {
    "path": "tsparser/src/runtimeresolve/node.rs",
    "content": "use std::collections::HashSet;\nuse std::{\n    fs::File,\n    io::BufReader,\n    path::{Component, Path, PathBuf},\n};\n\nuse anyhow::{bail, Context, Error};\nuse clean_path::Clean;\nuse serde::Deserialize;\nuse swc_common::sync::Lrc;\nuse swc_common::FileName;\nuse swc_ecma_loader::resolve::Resolve;\n\nuse crate::exports::Exports;\nuse crate::resolve_utils;\nuse crate::runtimeresolve::tsconfig::TsConfigPathResolver;\n\nstatic PACKAGE: &str = \"package.json\";\n\n#[derive(Deserialize)]\nstruct PackageJson {\n    #[allow(dead_code)]\n    #[serde(default)]\n    main: Option<String>,\n    #[allow(dead_code)]\n    #[serde(default)]\n    module: Option<String>,\n    #[serde(default)]\n    exports: Option<Exports>,\n}\n\n#[derive(Debug)]\npub struct EncoreRuntimeResolver<R> {\n    inner: R,\n    js_runtime_path: Option<PathBuf>,\n    extra_export_conditions: Vec<String>,\n    tsconfig_resolver: Option<Lrc<TsConfigPathResolver>>,\n}\n\nstatic DEFAULT_CONDITIONS: &[&str] = &[\"node-addons\", \"node\", \"import\", \"require\", \"default\"];\n\nimpl<R> EncoreRuntimeResolver<R> {\n    pub fn new(\n        inner: R,\n        js_runtime_path: Option<PathBuf>,\n        extra_export_conditions: Vec<String>,\n    ) -> Self {\n        Self {\n            inner,\n            js_runtime_path,\n            extra_export_conditions,\n            tsconfig_resolver: None,\n        }\n    }\n\n    pub fn with_tsconfig_resolver(self, resolver: Lrc<TsConfigPathResolver>) -> Self {\n        Self {\n            tsconfig_resolver: Some(resolver),\n            ..self\n        }\n    }\n\n    /// Resolve a path from the \"exports\" directive in the package.json file, if present.\n    fn resolve_export(&self, pkg_dir: &Path, rel_target: &str) -> Result<Option<PathBuf>, Error> {\n        let package_json_path = pkg_dir.join(PACKAGE);\n        if !package_json_path.is_file() {\n            bail!(\"package.json not found: {}\", package_json_path.display());\n        }\n\n        let file = File::open(&package_json_path)?;\n        let reader = BufReader::new(file);\n        let pkg: PackageJson = serde_json::from_reader(reader).context(format!(\n            \"failed to deserialize {}\",\n            package_json_path.display()\n        ))?;\n\n        let Some(exports) = &pkg.exports else {\n            bail!(\"no exports field in {}\", package_json_path.display());\n        };\n\n        let mut conditions =\n            HashSet::from_iter(self.extra_export_conditions.iter().map(|s| s.as_str()));\n        conditions.extend(DEFAULT_CONDITIONS);\n\n        // The result is relative to the package directory, whereas we want to return an absolute path.\n        let result = exports\n            .resolve_import_path(rel_target, &conditions)\n            .map(|p| p.to_path_buf());\n        Ok(result.map(|p| pkg_dir.join(p)))\n    }\n\n    fn pkg_name_from_target<'b>(&self, target: &'b str) -> (&'b str, &'b str) {\n        resolve_utils::split_package_name(target)\n    }\n\n    fn resolve_encore_module(&self, target: &str) -> Result<Option<PathBuf>, Error> {\n        let Some(js_runtime_path) = &self.js_runtime_path else {\n            return Ok(None);\n        };\n\n        let target_path = Path::new(target);\n        let mut components = target_path.components();\n\n        if let Some(Component::Normal(_)) = components.next() {\n            // It's a normal import, not an absolute or relative path.\n            let (pkg_name, pkg_path) = self.pkg_name_from_target(target);\n            let pkg_dir = js_runtime_path.join(pkg_name);\n\n            if pkg_dir.exists() {\n                return self.resolve_export(&pkg_dir, pkg_path);\n            }\n        }\n\n        Ok(None)\n    }\n}\n\nimpl<R> Resolve for EncoreRuntimeResolver<R>\nwhere\n    R: Resolve,\n{\n    fn resolve(&self, base: &FileName, target: &str) -> Result<FileName, Error> {\n        if let Some(tsconfig) = &self.tsconfig_resolver {\n            if let Some(buf) = tsconfig.resolve(target) {\n                return self.inner.resolve(tsconfig.base(), buf.as_ref());\n            }\n        }\n\n        let result = match self.resolve_encore_module(target)? {\n            Some(buf) => FileName::Real(buf.clean()),\n            None => self.inner.resolve(base, target)?,\n        };\n\n        // Prefer TypeScript declaration files (.d.ts) over JavaScript files if they exist.\n        if let FileName::Real(ref path) = result {\n            if let Some(dts_path) = declaration_file(path) {\n                return Ok(FileName::Real(dts_path));\n            }\n        }\n\n        Ok(result)\n    }\n}\n\nfn declaration_file(path: &Path) -> Option<PathBuf> {\n    let dts = resolve_utils::dts_counterpart(path)?;\n    if dts.exists() {\n        Some(dts)\n    } else {\n        None\n    }\n}\n"
  },
  {
    "path": "tsparser/src/runtimeresolve/tsconfig.rs",
    "content": "// Re-export from the shared tsconfig module.\npub use crate::tsconfig::*;\n"
  },
  {
    "path": "tsparser/src/span_err.rs",
    "content": "use swc_common::{errors::HANDLER, Span, Spanned};\n\npub trait ErrReporter {\n    fn err(&self, msg: &str);\n}\n\nimpl<T> ErrReporter for T\nwhere\n    T: Spanned,\n{\n    fn err(&self, msg: &str) {\n        HANDLER.with(|h| h.span_err(self.span(), msg));\n    }\n}\n\n#[derive(Debug)]\npub struct SpErr<E> {\n    pub span: Span,\n    pub error: E,\n}\n\nimpl<E> SpErr<E>\nwhere\n    E: std::error::Error,\n{\n    pub fn new(span: Span, error: E) -> Self {\n        SpErr { span, error }\n    }\n\n    pub fn report(&self) {\n        HANDLER.with(|handler| handler.span_err(self.span, &self.error.to_string()))\n    }\n}\n\nimpl<E> std::error::Error for SpErr<E>\nwhere\n    E: std::error::Error,\n{\n    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {\n        self.error.source()\n    }\n}\n\nimpl<E> std::fmt::Display for SpErr<E>\nwhere\n    E: std::fmt::Display,\n{\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        std::fmt::Display::fmt(&self.error, f)\n    }\n}\n\npub trait ErrorWithSpanExt: std::error::Error + Sized {\n    fn with_span(self, span: Span) -> SpErr<Self> {\n        SpErr::new(span, self)\n    }\n}\n\nimpl<T> ErrorWithSpanExt for T where T: std::error::Error {}\n"
  },
  {
    "path": "tsparser/src/testutil/mod.rs",
    "content": "use duct::cmd;\nuse once_cell::sync::Lazy;\nuse std::path::PathBuf;\n\npub mod testparse;\npub mod testresolve;\npub mod typeparse;\n\npub static JS_RUNTIME_PATH: Lazy<PathBuf> = Lazy::new(js_runtime_path);\n\nfn js_runtime_path() -> PathBuf {\n    let repo_root = cmd!(\"git\", \"rev-parse\", \"--show-toplevel\")\n        .stdout_capture()\n        .read()\n        .unwrap();\n    PathBuf::from(repo_root.trim()).join(\"runtimes\").join(\"js\")\n}\n"
  },
  {
    "path": "tsparser/src/testutil/testparse.rs",
    "content": "use std::rc::Rc;\nuse swc_common::sync::Lrc;\n\nuse crate::parser::module_loader::Module;\nuse crate::parser::parser::ParseContext;\nuse assert_fs::TempDir;\nuse swc_common::errors::Handler;\nuse swc_common::SourceMap;\n\npub fn test_parse(src: &str) -> Lrc<Module> {\n    let root = TempDir::new().unwrap();\n\n    let cm: Rc<SourceMap> = Default::default();\n    let errs = Rc::new(Handler::with_tty_emitter(\n        swc_common::errors::ColorConfig::Auto,\n        true,\n        false,\n        Some(cm.clone()),\n    ));\n\n    let pc = ParseContext::new(root.to_path_buf(), None, cm, errs).unwrap();\n    pc.loader.inject_file(\"test.ts\".into(), src).unwrap()\n}\n"
  },
  {
    "path": "tsparser/src/testutil/testresolve.rs",
    "content": "use anyhow::{anyhow, bail, Error};\nuse std::path::PathBuf;\nuse swc_common::FileName;\nuse swc_ecma_loader::resolve::Resolve;\n\npub struct TestResolver {\n    root_dir: PathBuf,\n    ar: txtar::Archive,\n}\n\nimpl TestResolver {\n    pub fn new(root_dir: PathBuf, ar: txtar::Archive) -> Self {\n        Self { root_dir, ar }\n    }\n}\n\nimpl Resolve for TestResolver {\n    fn resolve(&self, base: &FileName, module_specifier: &str) -> Result<FileName, Error> {\n        // Fake the existence of the runtime module for now.\n        if module_specifier.starts_with(\"encore.dev/\") {\n            let mut path = PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"))\n                .join(\"runtime\")\n                .join(\n                    // Get the suffix after the \"encore.dev/\" part.\n                    PathBuf::from(module_specifier)\n                        .strip_prefix(\"encore.dev/\")\n                        .unwrap(),\n                );\n            path.set_extension(\"ts\");\n            return Ok(FileName::Real(path));\n        }\n\n        let base = match base {\n            FileName::Real(v) => v,\n            _ => bail!(\"TestResolver supports only files\"),\n        };\n\n        let base_dir = base.parent().ok_or(anyhow!(\n            \"file has no parent directory: {}\",\n            base.to_string_lossy()\n        ))?;\n\n        let mut path = base_dir.join(module_specifier);\n        path.set_extension(\"ts\");\n        path = clean_path::clean(path);\n\n        for file in &self.ar.files {\n            let cleaned = clean_path::clean(self.root_dir.join(&file.name));\n            if cleaned == path {\n                return Ok(FileName::Real(path));\n            }\n        }\n        Err(anyhow!(\"import not found: {}\", module_specifier))\n    }\n}\n\npub struct NoopResolver;\n\nimpl Resolve for NoopResolver {\n    fn resolve(&self, _: &FileName, _: &str) -> Result<FileName, Error> {\n        bail!(\"NoopResolver does not support resolving files\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_resolve() {\n        let ar = txtar::from_str(\n            \"\n-- foo.ts --\nimport { Bar } from './bar.ts';\n-- bar.ts --\nexport const Bar = 5;\n        \",\n        );\n\n        let root = PathBuf::from(\"\");\n        let r = TestResolver::new(root, ar);\n\n        let base = FileName::Real(\"foo.ts\".into());\n        assert_eq!(\n            r.resolve(&base, \"./bar.ts\").unwrap(),\n            FileName::Real(\"bar.ts\".into())\n        );\n\n        assert_eq!(\n            r.resolve(&base, \"./moo.ts\").unwrap_err().to_string(),\n            \"import not found: ./moo.ts\"\n        );\n    }\n}\n"
  },
  {
    "path": "tsparser/src/testutil/typeparse.rs",
    "content": "use anyhow::Result;\nuse swc_common::errors::{ColorConfig, Handler};\nuse swc_common::input::StringInput;\nuse swc_common::sync::Lrc;\nuse swc_common::{FileName, SourceMap};\nuse swc_ecma_ast as ast;\nuse swc_ecma_ast::EsVersion;\nuse swc_ecma_parser::lexer::Lexer;\nuse swc_ecma_parser::{Parser, Syntax};\n\npub fn parse_type_expr(s: &str) -> Result<ast::TsType> {\n    let cm: Lrc<SourceMap> = Default::default();\n    let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone()));\n\n    let filename = FileName::Real(\"test.ts\".into());\n    let fm = cm.new_source_file(filename, s.into());\n    let lexer = Lexer::new(\n        Syntax::Typescript(Default::default()),\n        EsVersion::Es2022,\n        StringInput::from(&*fm),\n        None,\n    );\n    let mut parser = Parser::new_from(lexer);\n    for e in parser.take_errors() {\n        e.into_diagnostic(&handler).emit();\n    }\n    let result = parser\n        .parse_type()\n        .map_err(|e| {\n            // Unrecoverable fatal error occurred\n            e.into_diagnostic(&handler).emit();\n        })\n        .unwrap();\n    Ok(*result)\n}\n"
  },
  {
    "path": "tsparser/src/tsconfig.rs",
    "content": "use std::borrow::Cow;\nuse std::path::{Path, PathBuf};\n\nuse indexmap::IndexMap;\nuse serde::Deserialize;\nuse swc_common::FileName;\nuse thiserror::Error;\n\n#[derive(Debug)]\npub struct TsConfigPathResolver {\n    /// The base path from which relative paths are resolved.\n    base: PathBuf,\n    base_filename: FileName,\n\n    /// The parsed paths, sorted by descending prefix length (before any '*' wildcard).\n    paths: Vec<PathEntry>,\n}\n\n#[derive(Error, Debug)]\npub enum TsConfigError {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    #[error(\"reading {0} failed\")]\n    Read(PathBuf, #[source] std::io::Error),\n    #[error(\"parsing {0} failed\")]\n    Parse(PathBuf, #[source] serde_json::Error),\n    #[error(\"{0} has no parent directory\")]\n    NoParent(PathBuf),\n}\n\nimpl TsConfigPathResolver {\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn from_file(tsconfig_path: &Path) -> Result<Self, TsConfigError> {\n        let tsconfig_dir = tsconfig_path\n            .parent()\n            .ok_or_else(|| TsConfigError::NoParent(tsconfig_path.to_path_buf()))?;\n        let tsconfig = std::fs::read_to_string(tsconfig_path)\n            .map_err(|e| TsConfigError::Read(tsconfig_path.to_path_buf(), e))?;\n        Self::from_str(tsconfig_dir, &tsconfig)\n    }\n\n    /// Parse a tsconfig from a JSON/JSONC string. Works in both native and WASM.\n    pub fn from_str(tsconfig_dir: &Path, content: &str) -> Result<Self, TsConfigError> {\n        let content = strip_jsonc_comments(content, false);\n        let tsconfig: TSConfig = serde_json::from_str(&content)\n            .map_err(|e| TsConfigError::Parse(tsconfig_dir.to_path_buf(), e))?;\n        Ok(Self::from_config(tsconfig_dir, tsconfig))\n    }\n\n    pub fn from_config(tsconfig_dir: &Path, tsconfig: TSConfig) -> Self {\n        let base = tsconfig\n            .compiler_options\n            .base_url\n            .map(|p| tsconfig_dir.join(p))\n            .unwrap_or(tsconfig_dir.to_path_buf());\n\n        let mut paths: Vec<PathEntry> = tsconfig\n            .compiler_options\n            .paths\n            .into_iter()\n            .map(|(key, val)| {\n                let key = PathVal::from(key);\n                let values = val.into_iter().map(PathVal::from).collect();\n                PathEntry { key, values }\n            })\n            .collect();\n\n        // Sort the paths by descending prefix length.\n        paths.sort_by(|a, b| {\n            let a = a.key.prefix_len();\n            let b = b.key.prefix_len();\n            b.cmp(&a)\n        });\n\n        let base_filename = FileName::Real(base.clone());\n\n        Self {\n            base,\n            base_filename,\n            paths,\n        }\n    }\n\n    pub fn base(&self) -> &FileName {\n        &self.base_filename\n    }\n\n    /// Resolve a tsconfig path alias, checking file existence via the provided function.\n    pub fn resolve_with_checker(\n        &self,\n        import: &str,\n        exists: impl Fn(&Path) -> bool,\n    ) -> Option<Cow<'_, str>> {\n        for entry in &self.paths {\n            match &entry.key {\n                PathVal::Exact(key) if import == key => {\n                    for val in &entry.values {\n                        if let PathVal::Exact(val) = val {\n                            for candidate in file_candidates(self.base.join(val)) {\n                                if exists(&candidate) {\n                                    return Some(Cow::Borrowed(val));\n                                }\n                            }\n                        }\n                    }\n                }\n\n                PathVal::Wildcard { prefix, suffix }\n                    if import.starts_with(prefix) && import.ends_with(suffix) =>\n                {\n                    let wildcard = &import[prefix.len()..import.len() - suffix.len()];\n                    for val in &entry.values {\n                        match val {\n                            PathVal::Wildcard { prefix, suffix } => {\n                                let mut rel_path = String::with_capacity(\n                                    prefix.len() + suffix.len() + wildcard.len() + 2,\n                                );\n                                rel_path.push_str(prefix);\n                                rel_path.push_str(wildcard);\n                                rel_path.push_str(suffix);\n\n                                for candidate in file_candidates(self.base.join(&rel_path)) {\n                                    if exists(&candidate) {\n                                        return Some(Cow::Owned(rel_path));\n                                    }\n                                }\n                            }\n                            PathVal::Exact(val) => {\n                                for candidate in file_candidates(self.base.join(val)) {\n                                    if exists(&candidate) {\n                                        return Some(Cow::Borrowed(val));\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n                _ => {}\n            }\n        }\n\n        None\n    }\n\n    /// Resolve a tsconfig path alias using filesystem `.exists()` checks.\n    #[cfg(not(target_arch = \"wasm32\"))]\n    pub fn resolve(&self, import: &str) -> Option<Cow<'_, str>> {\n        self.resolve_with_checker(import, |p| p.exists())\n    }\n}\n\n#[derive(Debug)]\nstruct PathEntry {\n    key: PathVal,\n    values: Vec<PathVal>,\n}\n\n/// Represents a path, possibly with a wildcard.\n#[derive(Debug)]\nenum PathVal {\n    /// The path is an exact path.\n    Exact(String),\n\n    /// The path is a wildcard path, with the given prefix and suffix surrounding the '*'.\n    Wildcard { prefix: String, suffix: String },\n}\n\nimpl PathVal {\n    /// Returns the length of the prefix, or of the full path if it is an exact path.\n    fn prefix_len(&self) -> usize {\n        match self {\n            PathVal::Exact(s) => s.len(),\n            PathVal::Wildcard { prefix, .. } => prefix.len(),\n        }\n    }\n}\n\nimpl From<String> for PathVal {\n    fn from(s: String) -> Self {\n        match s.split_once('*') {\n            Some((prefix, suffix)) => PathVal::Wildcard {\n                prefix: prefix.to_string(),\n                suffix: suffix.to_string(),\n            },\n            None => PathVal::Exact(s),\n        }\n    }\n}\n\n#[derive(Deserialize)]\npub struct TSConfig {\n    #[serde(default, rename = \"compilerOptions\")]\n    pub compiler_options: CompilerOptions,\n}\n\n#[derive(Deserialize, Default)]\npub struct CompilerOptions {\n    #[serde(rename = \"baseUrl\")]\n    pub base_url: Option<PathBuf>,\n    #[serde(default)]\n    pub paths: IndexMap<String, Vec<String>>,\n}\n\n/// Takes a string of jsonc content and returns a comment free version\n/// which should parse fine as regular json.\n/// Nested block comments are supported.\n/// preserve_locations will replace most comments with spaces, so that JSON parsing\n/// errors should point to the right location.\n///\n/// From https://github.com/serde-rs/json/issues/168#issuecomment-761831843.\nfn strip_jsonc_comments(jsonc_input: &str, preserve_locations: bool) -> String {\n    let mut json_output = String::new();\n\n    let mut block_comment_depth: u8 = 0;\n    let mut is_in_string: bool = false; // Comments cannot be in strings\n\n    for line in jsonc_input.split('\\n') {\n        let mut last_char: Option<char> = None;\n        for cur_char in line.chars() {\n            // Check whether we're in a string\n            if block_comment_depth == 0 && last_char != Some('\\\\') && cur_char == '\"' {\n                is_in_string = !is_in_string;\n            }\n\n            // Check for line comment start\n            if !is_in_string && last_char == Some('/') && cur_char == '/' {\n                last_char = None;\n                if preserve_locations {\n                    json_output.push_str(\"  \");\n                }\n                break; // Stop outputting or parsing this line\n            }\n            // Check for block comment start\n            if !is_in_string && last_char == Some('/') && cur_char == '*' {\n                block_comment_depth += 1;\n                last_char = None;\n                if preserve_locations {\n                    json_output.push_str(\"  \");\n                }\n                // Check for block comment end\n            } else if !is_in_string && last_char == Some('*') && cur_char == '/' {\n                block_comment_depth = block_comment_depth.saturating_sub(1);\n                last_char = None;\n                if preserve_locations {\n                    json_output.push_str(\"  \");\n                }\n                // Output last char if not in any block comment\n            } else {\n                if block_comment_depth == 0 {\n                    if let Some(last_char) = last_char {\n                        json_output.push(last_char);\n                    }\n                } else if preserve_locations {\n                    json_output.push(' ');\n                }\n                last_char = Some(cur_char);\n            }\n        }\n\n        // Add last char and newline if not in any block comment\n        if let Some(last_char) = last_char {\n            if block_comment_depth == 0 {\n                json_output.push(last_char);\n            } else if preserve_locations {\n                json_output.push(' ');\n            }\n        }\n\n        // Remove trailing whitespace from line\n        while json_output.ends_with(' ') {\n            json_output.pop();\n        }\n        json_output.push('\\n');\n    }\n\n    json_output\n}\n\nstruct PathResolveIterator {\n    base: PathBuf,\n    base_ext: Option<String>,\n    candidates: &'static [&'static str],\n    idx: usize,\n}\n\nimpl Iterator for PathResolveIterator {\n    type Item = PathBuf;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        if self.idx < self.candidates.len() {\n            let candidate = self.candidates[self.idx];\n            self.idx += 1;\n\n            if candidate.is_empty() {\n                Some(self.base.clone())\n            } else if let Some(base_ext) = &self.base_ext {\n                // Combine the candidate extension with the base extension.\n                let ext = format!(\"{base_ext}.{candidate}\");\n                Some(self.base.with_extension(ext))\n            } else {\n                Some(self.base.with_extension(candidate))\n            }\n        } else {\n            None\n        }\n    }\n}\n\nfn file_candidates(base: PathBuf) -> impl Iterator<Item = PathBuf> {\n    let base_ext = base\n        .extension()\n        .and_then(|s| s.to_str())\n        .map(|s| s.to_string());\n\n    let candidates: &'static [&'static str] = match base_ext.as_deref() {\n        Some(\"js\") => &[\"ts\", \"tsx\", \"d.ts\", \"js\", \"jsx\"],\n        Some(\"mjs\") => &[\"mts\", \"d.mts\", \"mjs\"],\n        Some(\"cjs\") => &[\"cts\", \"d.cts\", \"cjs\"],\n\n        // If we don't have an extension, or it's not recognized, try all extensions.\n        _ => &[\n            \"ts\", \"tsx\", \"d.ts\", \"js\", \"jsx\", \"mts\", \"d.mts\", \"mjs\", \"cts\", \"d.cts\", \"cjs\", \"\",\n        ],\n    };\n\n    PathResolveIterator {\n        base,\n        candidates,\n        base_ext,\n        idx: 0,\n    }\n}\n"
  },
  {
    "path": "tsparser/tests/common/mod.rs",
    "content": "use clean_path::Clean;\nuse std::path::PathBuf;\n\npub fn js_runtime_path() -> PathBuf {\n    PathBuf::from(env!(\"CARGO_MANIFEST_DIR\"))\n        .join(\"../runtimes/js\")\n        .clean()\n}\n"
  },
  {
    "path": "tsparser/tests/parse_tests.rs",
    "content": "use std::fs;\nuse std::path::Path;\nuse std::rc::Rc;\n\nuse anyhow::Result;\nuse common::js_runtime_path;\nuse insta::glob;\nuse swc_common::errors::{Handler, HANDLER};\nuse swc_common::{Globals, SourceMap, GLOBALS};\nuse tempdir::TempDir;\n\nuse encore_tsparser::builder::Builder;\nuse encore_tsparser::parser::parser::ParseContext;\nuse encore_tsparser::{app, builder};\n\nmod common;\n\n#[test]\nfn test_parser() {\n    env_logger::init();\n    glob!(\"testdata/*.txt\", |path| {\n        let input = fs::read_to_string(path).unwrap();\n        let ar = txtar::from_str(&input);\n        let tmp_dir = TempDir::new(\"parse\").unwrap();\n        ar.materialize(&tmp_dir).unwrap();\n        match parse_txtar(tmp_dir.path()) {\n            Ok(_) => {}\n            Err(e) => {\n                panic!(\"{:#?}\\n{}\", e, e.backtrace());\n            }\n        }\n    });\n}\n\nfn parse_txtar(app_root: &Path) -> Result<app::AppDesc> {\n    let globals = Globals::new();\n    let cm: Rc<SourceMap> = Default::default();\n    let errs = Rc::new(Handler::with_tty_emitter(\n        swc_common::errors::ColorConfig::Auto,\n        true,\n        false,\n        Some(cm.clone()),\n    ));\n\n    GLOBALS.set(&globals, || -> Result<app::AppDesc> {\n        HANDLER.set(&errs, || -> Result<app::AppDesc> {\n            let builder = Builder::new()?;\n            let pc = ParseContext::new(\n                app_root.to_path_buf(),\n                Some(js_runtime_path()),\n                cm,\n                errs.clone(),\n            )?;\n\n            let app = builder::App {\n                root: app_root.to_path_buf(),\n                platform_id: None,\n                local_id: \"test\".to_string(),\n            };\n            let pp = builder::ParseParams {\n                app: &app,\n                pc: &pc,\n                working_dir: app_root,\n                parse_tests: false,\n            };\n\n            builder.parse(&pp).ok_or(anyhow::anyhow!(\"parse failed\"))\n        })\n    })\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/builtins.txt",
    "content": "-- foo/foo.ts --\nimport { api } from \"encore.dev/api\";\n\ntype Params = {\n    field: Record<string, string>;\n};\n\nexport const ping = api<Params, void>({}, () => {});\n\n-- package.json --\n{\n  \"name\": \"foo\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/cache.txt",
    "content": "-- svc/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"svc\");\n\n-- svc/cache.ts --\nimport {\n    CacheCluster,\n    StringKeyspace,\n    IntKeyspace,\n    FloatKeyspace,\n    StructKeyspace,\n    StringListKeyspace,\n    NumberListKeyspace,\n    StringSetKeyspace,\n    NumberSetKeyspace,\n} from \"encore.dev/storage/cache\";\n\n// Define a cache cluster\nexport const cluster = new CacheCluster(\"myCluster\", {\n    evictionPolicy: \"allkeys-lru\",\n});\n\n// String keyspace with a simple key pattern\nexport const greetings = new StringKeyspace<string>(cluster, {\n    keyPattern: \"greeting/:key\",\n});\n\n// Int keyspace\nexport const counters = new IntKeyspace<{ userId: string }>(cluster, {\n    keyPattern: \"counter/:userId\",\n});\n\n// Float keyspace\nexport const scores = new FloatKeyspace<string>(cluster, {\n    keyPattern: \"score/:key\",\n});\n\n// Struct keyspace\ninterface User {\n    name: string;\n    email: string;\n}\n\nexport const users = new StructKeyspace<string, User>(cluster, {\n    keyPattern: \"user/:key\",\n});\n\n// String list keyspace\nexport const recentViews = new StringListKeyspace<string>(cluster, {\n    keyPattern: \"recent-views/:key\",\n});\n\n// Number list keyspace\nexport const scoreHistory = new NumberListKeyspace<string>(cluster, {\n    keyPattern: \"score-history/:key\",\n});\n\n// String set keyspace\nexport const tags = new StringSetKeyspace<string>(cluster, {\n    keyPattern: \"tags/:key\",\n});\n\n// Number set keyspace\nexport const uniqueScores = new NumberSetKeyspace<string>(cluster, {\n    keyPattern: \"unique-scores/:key\",\n});\n\n// Multiple clusters\nexport const cluster2 = new CacheCluster(\"secondCluster\", {\n    evictionPolicy: \"volatile-lru\",\n});\n\nexport const otherData = new StringKeyspace<string>(cluster2, {\n    keyPattern: \"other/:key\",\n});\n\n-- package.json --\n{\n  \"name\": \"cache-test\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/cache_named.txt",
    "content": "-- svc/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"svc\");\n\n-- svc/cache.ts --\nimport { CacheCluster, StringKeyspace } from \"encore.dev/storage/cache\";\n\n// Define a cache cluster\nexport const cluster = new CacheCluster(\"myCluster\", {\n    evictionPolicy: \"allkeys-lru\",\n});\n\n// Define a keyspace in the same service\nexport const greetings = new StringKeyspace<string>(cluster, {\n    keyPattern: \"greeting/:key\",\n});\n\n-- other/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"other\");\n\n-- other/cache.ts --\nimport { CacheCluster, IntKeyspace, StructKeyspace } from \"encore.dev/storage/cache\";\n\n// Reference the cluster from a different service using .named()\nconst cluster = CacheCluster.named(\"myCluster\");\n\n// Use the referenced cluster for keyspaces\nexport const counters = new IntKeyspace<{ userId: string }>(cluster, {\n    keyPattern: \"counter/:userId\",\n});\n\n// Struct keyspace with multiple key fields\ninterface SessionKey {\n    userId: string;\n    deviceId: string;\n}\n\ninterface SessionData {\n    token: string;\n    expiresAt: number;\n}\n\nexport const sessions = new StructKeyspace<SessionKey, SessionData>(cluster, {\n    keyPattern: \"session/:userId/:deviceId\",\n});\n\n-- package.json --\n{\n  \"name\": \"cache-named-test\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/mapped_as_clause.txt",
    "content": "-- foo/foo.ts --\nimport { api } from \"encore.dev/api\";\n\n// Test filtering with never\ntype RemoveKind<T> = {\n  [K in keyof T as K extends \"kind\" ? never : K]: T[K]\n}\n\ninterface Person {\n  name: string;\n  age: number;\n  kind: string;\n}\n\ntype PersonWithoutKind = RemoveKind<Person>;\n\n// Test identity mapping\ntype Identity<T> = {\n  [K in keyof T as K]: T[K]\n}\n\ntype IdentityPerson = Identity<Person>;\n\n// Test conditional on value type\ntype OnlyStrings<T> = {\n  [K in keyof T as T[K] extends string ? K : never]: T[K]\n}\n\ntype PersonStrings = OnlyStrings<Person>;\n\n// Test Drizzle ORM-style pattern with nested type transformations\ntype RequiredKeyOnly<TKey extends string, TColumn> = TColumn extends { notNull: true } ? TKey : never;\ntype MapColumnName<TKey extends string, TColumn, TMapping> = TKey;\n\ninterface TColumns {\n  id: { notNull: true; data: number };\n  name: { notNull: false; data: string };\n  age: { notNull: true; data: number };\n}\n\ntype TConfig = {\n  dbColumnNames: false;\n};\n\n// This pattern from Drizzle uses complex nested type transformations in the as clause\ntype DrizzleTable = {\n  [Key in keyof TColumns & string as RequiredKeyOnly<MapColumnName<Key, TColumns[Key], TConfig['dbColumnNames']>, TColumns[Key]>]: TColumns[Key];\n};\n\ninterface Params {\n    person: PersonWithoutKind;\n    identity: IdentityPerson;\n    strings: PersonStrings;\n    table: DrizzleTable;\n};\n\nexport const ping = api<Params, void>({}, () => {});\n\n-- package.json --\n{\n  \"name\": \"foo\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/mapped_types.txt",
    "content": "-- foo/foo.ts --\nimport { api } from \"encore.dev/api\";\n\ntype OnlyBoolsAndHorses = {\n  [key: string]: boolean | number;\n};\n\ninterface Params {\n    blah: OnlyBoolsAndHorses;\n};\n\nexport const ping = api<Params, void>({}, () => {});\n\n-- package.json --\n{\n  \"name\": \"foo\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/metrics.txt",
    "content": "-- svc/encore.service.ts --\nimport { Service } from \"encore.dev/service\";\n\nexport default new Service(\"svc\");\n\n-- svc/metrics.ts --\nimport { Counter, CounterGroup, Gauge, GaugeGroup } from \"encore.dev/metrics\";\n\n// Simple counter\nexport const OrdersProcessed = new Counter(\"orders_processed\");\n\n// Counter with labels\ninterface RequestLabels extends Record<string, string | number | boolean> {\n  success: boolean;\n  method: string;\n}\n\nexport const RequestsTotal = new CounterGroup<RequestLabels>(\"http_requests_total\");\n\n// Simple gauge\nexport const MemoryUsage = new Gauge(\"memory_usage_bytes\");\n\n// Gauge with labels\ninterface CPULabels extends Record<string, string | number | boolean> {\n  core: number;\n}\n\nexport const CPUUsage = new GaugeGroup<CPULabels>(\"cpu_usage_percent\");\n\n-- package.json --\n{\n  \"name\": \"metrics-test\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/query_header.txt",
    "content": "-- foo/foo.ts --\nimport { api, Query, Header } from \"encore.dev/api\";\n\ninterface Params {\n    q1: Query;\n    q2: Query<boolean>;\n    q3: Query<\"my-query\">;\n    q4: Query<boolean, \"my-query\">;\n\n    h1: Header;\n    h2: Header<boolean>;\n    h3: Header<\"my-header\">;\n    h4: Header<boolean, \"my-header\">;\n};\n\nexport const t1 = api<Params, void>({}, () => {});\nexport const t2 = api<void, Params>({}, () => {});\nexport const t3 = api<Params, Params>({}, () => {});\n\n-- package.json --\n{\n  \"name\": \"foo\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n"
  },
  {
    "path": "tsparser/tests/testdata/tsconfig.txt",
    "content": "-- foo/foo.ts --\nimport { api } from \"encore.dev/api\";\nimport { blah } from \"@bar/bar\";\n\nexport const ping = api<void, void>({}, () => {});\n\n-- bar/bar.ts --\n\nexport const blah = 5;\n\n-- package.json --\n{\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"encore.dev\": \"^1.35.0\"\n  }\n}\n\n-- tsconfig.json --\n{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"~encore/*\": [\"./encore.gen/*\"],\n      \"@bar/*\": [\"./bar/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "tsparser/txtar/.gitignore",
    "content": "/target\n"
  },
  {
    "path": "tsparser/txtar/Cargo.toml",
    "content": "[package]\nname = \"txtar\"\nversion = \"1.0.0\"\nedition = \"2021\"\nauthors = [\"Frank de Jong <f.jin@protonmail.com>\"]\ndescription = \"A Rust implementation of the txtar Go package\"\nlicense = \"MIT/Apache-2.0\"\nreadme = \"README.md\"\nrepository = \"https://gitlab.com/foo-jin/txtar\"\ndocumentation = \"https://docs.rs/txtar/latest/txtar/\"\ncategories = [\"filesystem\", \"development-tools::testing\"]\nkeywords = [\"fs\", \"filesystem\", \"txtar\", \"tempfs\"]\nexclude = [\".gitignore\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nclean-path = \"0.2.1\"\nthiserror = \"1.0\"\n\n[dev-dependencies]\nassert_fs = \"1.0.7\"\npredicates = \"2.1.1\"\nsimilar-asserts = \"1.2.0\"\n"
  },
  {
    "path": "tsparser/txtar/LICENSE-APACHE",
    "content": "                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don't include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\nCopyright 2022 Frank de Jong\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "tsparser/txtar/LICENSE-MIT",
    "content": "MIT License\n\nCopyright (c) 2022 Frank de Jong\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "tsparser/txtar/README.md",
    "content": "# txtar\n\n[![crates.io version][1]][2]\n[![docs.rs docs][3]][4]\n[![license][5]][6]\n\nA Rust implementation of the [txtar](https://github.com/golang/tools/tree/master/txtar) Go package.\n\n```sh\ncargo add txtar\n```\n\n## Usage\n```rust no_run\nlet txt = \"\\\ncomment1\ncomment2\n-- file1 --\nFile 1 text.\n-- foo/bar --\nFile 2 text.\n-- empty --\n-- noNL --\nhello world\";\n\nlet archive = txtar::from_str(txt);\narchive.materialize(\"/tmp/somedir/\").unwrap();\n```\n\n## Txtar goals\nAs described in the Go package:\n\n> Package txtar implements a trivial text-based file archive format.\n>\n> The goals for the format are:\n>\n>\t- be trivial enough to create and edit by hand.\n>\t- be able to store trees of text files describing go command test cases.\n>\t- diff nicely in git history and code reviews.\n>\n> Non-goals include being a completely general archive format,\n> storing binary data, storing file modes, storing special files like\n> symbolic links, and so on.\n\n\n## Txtar format spec\nThe format spec as written in the `txtar` Go package source code:\n\n> Txtar format\n>\n> A txtar archive is zero or more comment lines and then a sequence of file entries.\n> Each file entry begins with a file marker line of the form \"-- FILENAME --\"\n> and is followed by zero or more file content lines making up the file data.\n> The comment or file content ends at the next file marker line.\n> The file marker line must begin with the three-byte sequence \"-- \"\n> and end with the three-byte sequence \" --\", but the enclosed\n> file name can be surrounded by additional white space,\n> all of which is stripped.\n>\n> If the txtar file is missing a trailing newline on the final line,\n> parsers should consider a final newline to be present anyway.\n>\n> There are no possible syntax errors in a txtar archive.\n\n## License\n\n[MIT](./LICENSE-MIT) OR [Apache-2.0](./LICENSE-APACHE)\n\n\n[1]: https://img.shields.io/crates/v/txtar.svg?style=flat-square\n[2]: https://crates.io/crates/txtar\n[3]: https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square\n[4]: https://docs.rs/txtar\n[5]: https://img.shields.io/crates/l/txtar.svg?style=flat-square\n[6]: #license\n"
  },
  {
    "path": "tsparser/txtar/src/error.rs",
    "content": "use std::io;\n\nuse thiserror::Error;\n\n#[derive(Error, Debug)]\npub enum MaterializeError {\n    #[error(\"{0}\")]\n    Io(#[from] io::Error),\n    #[error(\"{0}: outside parent directory\")]\n    DirEscape(String),\n}\n"
  },
  {
    "path": "tsparser/txtar/src/lib.rs",
    "content": "//! `txtar` is a rust implementation of the `txtar` Go package.\n//!\n//! # About\n//!\n//! `txtar`s purpose is best described in the original Go package:\n//!\n//! > Package txtar implements a trivial text-based file archive format.\n//! >\n//! > The goals for the format are:\n//! >\n//! > - be trivial enough to create and edit by hand.\n//! > - be able to store trees of text files describing go command test cases.\n//! > - diff nicely in git history and code reviews.\n//! >\n//! > Non-goals include being a completely general archive format,\n//! > storing binary data, storing file modes, storing special files like\n//! > symbolic links, and so on.\n//!\n//!\n//! ## format spec\n//! The format spec as written in the `txtar` Go package source code:\n//!\n//! > Txtar format\n//! >\n//! > A txtar archive is zero or more comment lines and then a sequence of file entries.\n//! > Each file entry begins with a file marker line of the form \"`-- FILENAME --`\"\n//! > and is followed by zero or more file content lines making up the file data.\n//! > The comment or file content ends at the next file marker line.\n//! > The file marker line must begin with the three-byte sequence \"`-- `\"\n//! > and end with the three-byte sequence \"` --`\", but the enclosed\n//! > file name can be surrounding by additional white space,\n//! > all of which is stripped.\n//! >\n//! > If the txtar file is missing a trailing newline on the final line,\n//! > parsers should consider a final newline to be present anyway.\n//! >\n//! > There are no possible syntax errors in a txtar archive.\n//!\n//! # Example\n//!\n//! ```rust no_run\n//! let txt = \"\\\n//! comment1\n//! comment2\n//! -- file1 --\n//! File 1 text.\n//! -- foo/bar --\n//! File 2 text.\n//! -- empty --\n//! -- noNL --\n//! hello world\";\n//!\n//! let archive = txtar::from_str(txt);\n//! archive.materialize(\"/tmp/somedir/\").unwrap();\n//! ```\n\nmod error;\n\nuse std::{\n    fmt::Display,\n    fs,\n    io::{self, BufWriter, Write},\n    path::{Path, PathBuf},\n    str,\n};\n\nuse clean_path::Clean;\n\npub use error::MaterializeError;\n\n/**\nAn archive represents a tree of text files.\n\nThis type is used to read txtar files from disk and materialize the\ncorresponding file layout.\n\n# Examples\n\n```rust no_run\nuse txtar::Archive;\n\nlet txt = \"\\\ncomment1\ncomment2\n-- file1 --\nFile 1 text.\n-- foo --\nFile 2 text.\n-- empty --\n-- noNL --\nhello world\";\n\nlet archive = Archive::from(txt);\narchive.materialize(\"/tmp/somedir/\").unwrap();\n```\n**/\n#[derive(Debug, Default, Eq, PartialEq, Clone)]\npub struct Archive {\n    // internal invariant:\n    // comment is fix_newlined\n    pub comment: String,\n    pub files: Vec<File>,\n}\n\n#[derive(Debug, Eq, PartialEq, Clone)]\npub struct File {\n    pub name: PathBuf,\n    // internal invariant:\n    // data is fix_newlined\n    pub data: String,\n}\n\nimpl File {\n    pub fn new<P: AsRef<Path>>(name: P, data: &str) -> File {\n        let name = name.as_ref().to_owned();\n        let mut data = data.to_owned();\n        fix_newline(&mut data);\n\n        File { name, data }\n    }\n}\n\nimpl Archive {\n    fn new(comment: &str, files: Vec<File>) -> Archive {\n        let mut comment = comment.to_owned();\n        fix_newline(&mut comment);\n\n        Archive { comment, files }\n    }\n\n    /// Serialize the archive as txtar into the I/O stream.\n    pub fn to_writer<W: Write>(&self, writer: &mut W) -> io::Result<()> {\n        write!(writer, \"{self}\")\n    }\n\n    /// Writes each file in this archive to the directory at the given\n    /// path.\n    ///\n    /// # Errors\n    ///\n    /// This function will error in the event a file would be written\n    /// outside of the directory or if an existing file would be\n    /// overwritten. Additionally, any errors caused by the underlying\n    /// I/O operations will be propagated.\n    pub fn materialize<P: AsRef<Path>>(&self, path: P) -> Result<(), MaterializeError> {\n        let path = path.as_ref();\n        for File { name, data } in &self.files {\n            let name_path = name.clean();\n            if name_path.starts_with(\"../\") || name_path.is_absolute() {\n                return Err(MaterializeError::DirEscape(\n                    name_path.to_string_lossy().to_string(),\n                ));\n            }\n\n            let rel_path = name_path;\n            let path = path.join(rel_path);\n            if let Some(p) = path.parent() {\n                fs::create_dir_all(p)?;\n            }\n\n            let mut file = fs::File::options()\n                .write(true)\n                .create_new(true)\n                .open(path)?;\n            let mut w = BufWriter::new(&mut file);\n            w.write_all(data.as_bytes())?;\n        }\n\n        Ok(())\n    }\n}\n\nimpl Display for Archive {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.comment)?;\n\n        for File { name, data } in &self.files {\n            let name = name.display();\n            writeln!(f, \"-- {name} --\")?;\n            write!(f, \"{data}\")?;\n        }\n\n        Ok(())\n    }\n}\n\nimpl TryFrom<&[u8]> for Archive {\n    type Error = std::str::Utf8Error;\n\n    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {\n        let s = str::from_utf8(slice)?;\n        Ok(Archive::from(s))\n    }\n}\n\nimpl From<&str> for Archive {\n    fn from(s: &str) -> Archive {\n        let (comment, mut name, mut s) = split_file_markers(s);\n        let mut files = Vec::new();\n\n        while !name.is_empty() {\n            let (data, next_name, rest) = split_file_markers(s);\n\n            let file = File::new(name, data);\n            files.push(file);\n\n            name = next_name;\n            s = rest;\n        }\n\n        Archive::new(comment, files)\n    }\n}\n\n/// Read an archive from a string of txtar data.\npub fn from_str(s: &str) -> Archive {\n    Archive::from(s)\n}\n\n/// Try to read an archive from bytes of txtar data.\npub fn from_bytes(slice: &[u8]) -> Result<Archive, std::str::Utf8Error> {\n    Archive::try_from(slice)\n}\n\nfn split_file_markers(s: &str) -> (&str, &str, &str) {\n    const NEWLINE_MARKER: &str = \"\\n-- \";\n    const MARKER: &str = \"-- \";\n    const MARKER_END: &str = \" --\";\n\n    let (prefix, rest) = if s.starts_with(MARKER) {\n        (\"\", s)\n    } else {\n        match s.find(NEWLINE_MARKER) {\n            None => return (s, \"\", \"\"),\n            Some(offset) => s.split_at(offset + 1),\n        }\n    };\n    debug_assert!(rest.starts_with(MARKER));\n\n    let (filename, suffix) = match rest.split_once('\\n') {\n        None if rest.ends_with(MARKER_END) => (rest, \"\"),\n        None => return (s, \"\", \"\"),\n        Some((n, pf)) => (n, pf),\n    };\n\n    let filename = filename.trim_end_matches('\\r');\n    debug_assert!(filename.ends_with(MARKER_END));\n\n    let filename = filename\n        .strip_prefix(MARKER)\n        .and_then(|filename| filename.strip_suffix(MARKER_END))\n        .unwrap()\n        .trim();\n    (prefix, filename, suffix)\n}\n\nfn fix_newline(s: &mut String) {\n    if !s.is_empty() && !s.ends_with('\\n') {\n        s.push('\\n');\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use assert_fs::{prelude::*, TempDir};\n    use predicates::prelude::{predicate::str::contains, *};\n    use similar_asserts::assert_eq;\n\n    const BASIC: &str = \"\\\ncomment1\ncomment2\n-- file1 --\nFile 1 text.\n-- foo --\nFile 2 text.\n-- empty --\n-- noNL --\nhello world\";\n\n    #[test]\n    fn parse_format() {\n        // Test simplest\n        {\n            let simplest = \"-- simplest.txt --\";\n            let expected = format!(\"{simplest}\\n\");\n            check_parse_format(\"simplest\", simplest, &expected);\n        }\n\n        // Test basic variety of inputs\n        {\n            let basic = BASIC;\n            let expected = format!(\"{basic}\\n\");\n            check_parse_format(\"basic\", basic, &expected);\n        }\n\n        // Test CRLF input\n        {\n            let crlf = \"blah\\r\\n-- hello --\\r\\nhello\\r\\n\";\n            let expected = \"\\\nArchive { comment: \\\"blah\\\\r\\\\n\\\", files: [File { name: \\\"hello\\\", data: \\\"hello\\\\r\\\\n\\\" }] }\";\n\n            let arch = format!(\"{:?}\", Archive::from(crlf));\n            assert_eq!(&arch, expected, \"parse[CRLF input]\",);\n        }\n\n        // Test whitespace handling\n        {\n            let txtar = \"--  a  --\";\n            let expected = \"-- a --\\n\";\n            check_parse_format(\"whitespace\", txtar, expected)\n        }\n    }\n\n    fn check_parse_format(name: &str, txtar: &str, expected: &str) {\n        let arch = Archive::from(txtar);\n        let txtar = arch.to_string();\n        assert_eq!(txtar, expected, \"parse[{name}]\");\n    }\n\n    #[test]\n    fn materialize_basic() {\n        let dir = TempDir::new().unwrap();\n        let exists = predicate::path::exists();\n        let empty = predicate::str::is_empty().from_utf8().from_file_path();\n        {\n            let good = Archive::from(\"-- good.txt --\");\n            good.materialize(&dir)\n                .expect(\"good.materialize should not error\");\n            dir.child(\"good.txt\").assert(exists).assert(empty);\n        }\n        {\n            let basic = Archive::from(BASIC);\n            basic\n                .materialize(&dir)\n                .expect(\"basic.materialize should not error\");\n\n            check_contents(&dir, \"file1\", \"File 1 text.\");\n            check_contents(&dir, \"foo\", \"File 2 text.\");\n            check_contents(&dir, \"noNL\", \"hello world\");\n            dir.child(\"empty\").assert(exists).assert(empty);\n        }\n        {\n            let bad_rel = Archive::from(\"-- ../bad.txt --\");\n            check_bad_materialize(&dir, bad_rel, \"../bad.txt\");\n\n            let bad_abs = Archive::from(\"-- /bad.txt --\");\n            check_bad_materialize(&dir, bad_abs, \"/bad.txt\");\n        }\n    }\n\n    #[test]\n    fn materialize_nested() {\n        let dir = TempDir::new().unwrap();\n\n        {\n            let nested = Archive::from(\n                \"comment\\n\\\n\t\t\t -- foo/foo.txt --\\nThis is foo.\\n\\\n\t\t\t -- bar/bar.txt --\\nThis is bar.\\n\\\n\t\t\t -- bar/deep/deeper/abyss.txt --\\nThis is in the DEEPS.\",\n            );\n            nested\n                .materialize(&dir)\n                .expect(\"nested.materialize should not error\");\n\n            check_contents(&dir, \"foo/foo.txt\", \"This is foo.\");\n            check_contents(&dir, \"bar/bar.txt\", \"This is bar.\");\n            check_contents(&dir, \"bar/deep/deeper/abyss.txt\", \"This is in the DEEPS.\");\n        }\n        {\n            let bad_nested_rel = Archive::from(\"-- bar/deep/deeper/../../../../escaped.txt --\");\n            check_bad_materialize(&dir, bad_nested_rel, \"../escaped.txt\");\n        }\n    }\n\n    fn check_contents(dir: &TempDir, child: &str, contents: &str) {\n        let exists = predicate::path::exists();\n        let newline_ending = predicate::str::ends_with(\"\\n\").from_utf8().from_file_path();\n        dir.child(child)\n            .assert(exists)\n            .assert(contains(contents))\n            .assert(newline_ending);\n    }\n\n    fn check_bad_materialize(dir: &TempDir, bad_rel: Archive, expected: &str) {\n        let err = bad_rel.materialize(dir);\n        match err {\n            Err(MaterializeError::DirEscape(p)) => assert_eq!(p, expected.to_string()),\n            Err(e) => panic!(\"expected `MaterializeError::DirEscape`, got {e:?}\"),\n            Ok(_) => panic!(\"materialize({expected}) outside of parent dir should have failed\"),\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/wasm/Cargo.toml",
    "content": "[package]\nname = \"tsparser-wasm\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nencore-tsparser = { path = \"..\", default-features = false, features = [\"serde-meta\"] }\nwasm-bindgen = \"0.2\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nswc_common = \"0.33.8\"\nconsole_error_panic_hook = \"0.1\"\n"
  },
  {
    "path": "tsparser/wasm/README.md",
    "content": "# tsparser-wasm\n\nWebAssembly build of the Encore TypeScript parser. Parses Encore.ts source files in the browser and returns application metadata (APIs, services, infrastructure resources, etc.).\n\n## Prerequisites\n\n- [Rust toolchain](https://rustup.rs/)\n- [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer/)\n\n```sh\ncargo install wasm-pack\n```\n\n## Building\n\nFrom this directory (`tsparser/wasm/`):\n\n```sh\nwasm-pack build --target web\n```\n\nThis produces a `pkg/` directory containing:\n- `tsparser_wasm_bg.wasm` — the compiled WebAssembly module\n- `tsparser_wasm.js` — JavaScript bindings (ES module)\n- `tsparser_wasm.d.ts` — TypeScript type declarations\n- `package.json` — ready to publish to npm or use locally\n\n## Usage\n\n```js\nimport init, { parse } from './pkg/tsparser_wasm.js';\n\nawait init();\n\nconst files = [\n  // User source files\n  { name: \"myservice/encore.service.ts\", content: `import { Service } from \"encore.dev/service\";\\nexport default new Service(\"myservice\");` },\n  { name: \"myservice/api.ts\", content: `import { api } from \"encore.dev/api\";\\nexport const ping = api({ method: \"GET\", path: \"/ping\" }, async () => { return { message: \"pong\" }; });` },\n\n  // Optional: tsconfig.json for path alias support\n  { name: \"tsconfig.json\", content: `{\"compilerOptions\": {\"paths\": {\"@/*\": [\"./src/*\"]}}}` },\n\n  // Optional: node_modules files (package.json + .d.ts type declarations)\n  { name: \"node_modules/zod/package.json\", content: `{\"name\": \"zod\", \"exports\": {\".\": {\"types\": \"./lib/index.d.mts\"}}}` },\n  { name: \"node_modules/zod/lib/index.d.mts\", content: `export declare function string(): ZodString; ...` },\n];\n\nconst result = JSON.parse(parse(JSON.stringify(files)));\n\nif (result.ok) {\n  console.log(\"Parsed metadata:\", result.meta);\n} else {\n  console.error(\"Parse errors:\", result.errors);\n}\n```\n\n### Input format\n\n`parse()` accepts a JSON string containing an array of `{name, content}` objects:\n\n| File pattern | Treatment |\n|---|---|\n| `node_modules/**` | Registered for module resolution but not parsed as user code. `package.json` files are used for bare import resolution (`exports`, `types`, `main` fields). |\n| `tsconfig.json` | Parsed for `compilerOptions.paths` and `baseUrl` to support path aliases. JSONC (comments) is supported. |\n| Everything else | Parsed as user source code. Only `.ts` files are processed. |\n\n### Output format\n\nReturns a JSON string:\n\n```json\n{\n  \"ok\": true,\n  \"errors\": [],\n  \"meta\": { /* encore.parser.meta.v1 protobuf as JSON */ }\n}\n```\n\nWhen `ok` is `false`, `errors` contains human-readable error messages and `meta` is absent.\n"
  },
  {
    "path": "tsparser/wasm/build.rs",
    "content": "use std::env;\nuse std::fs;\nuse std::io::Write;\nuse std::path::{Path, PathBuf};\n\nfn main() {\n    let encore_dev_dir = PathBuf::from(\"../../runtimes/js/encore.dev\");\n    println!(\n        \"cargo:rerun-if-changed={}\",\n        encore_dev_dir.to_string_lossy()\n    );\n\n    let out_dir = env::var(\"OUT_DIR\").unwrap();\n    let dest_path = Path::new(&out_dir).join(\"encore_dev_files.rs\");\n    let mut out = fs::File::create(&dest_path).unwrap();\n\n    // Collect all .ts files and package.json\n    let mut files: Vec<(String, PathBuf)> = Vec::new();\n\n    // Add package.json\n    let pkg_json = encore_dev_dir.join(\"package.json\");\n    if pkg_json.exists() {\n        files.push((\"node_modules/encore.dev/package.json\".into(), pkg_json));\n    }\n\n    // Walk for .ts files\n    collect_ts_files(&encore_dev_dir, &encore_dev_dir, &mut files);\n\n    writeln!(out, \"static ENCORE_DEV_FILES: &[(&str, &str)] = &[\").unwrap();\n\n    for (rel_path, abs_path) in &files {\n        let abs = fs::canonicalize(abs_path).unwrap();\n        writeln!(\n            out,\n            \"    (\\\"{rel_path}\\\", include_str!(\\\"{}\\\")),\",\n            abs.to_string_lossy().replace('\\\\', \"/\")\n        )\n        .unwrap();\n    }\n\n    writeln!(out, \"];\").unwrap();\n}\n\nfn collect_ts_files(base: &Path, dir: &Path, files: &mut Vec<(String, PathBuf)>) {\n    let Ok(entries) = fs::read_dir(dir) else {\n        return;\n    };\n\n    for entry in entries.flatten() {\n        let path = entry.path();\n        if path.is_dir() {\n            // Skip dist/, node_modules/, and hidden dirs\n            let name = entry.file_name();\n            let name = name.to_string_lossy();\n            if name == \"dist\" || name == \"node_modules\" || name.starts_with('.') {\n                continue;\n            }\n            collect_ts_files(base, &path, files);\n        } else if let Some(ext) = path.extension() {\n            if ext == \"ts\" {\n                let rel = path.strip_prefix(base).unwrap();\n                let nm_path = format!(\"node_modules/encore.dev/{}\", rel.to_string_lossy());\n                files.push((nm_path, path));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tsparser/wasm/src/lib.rs",
    "content": "use std::collections::HashMap;\nuse std::fmt;\nuse std::io::{self, Write};\nuse std::path::PathBuf;\nuse std::sync::{Arc, Mutex};\n\nuse serde::{Deserialize, Serialize};\nuse swc_common::errors::{Emitter, EmitterWriter, Handler, HANDLER};\nuse swc_common::sync::Lrc;\nuse swc_common::{Globals, SourceMap, SourceMapper, GLOBALS};\nuse wasm_bindgen::prelude::*;\n\nuse encore_tsparser::app::validate_and_describe;\nuse encore_tsparser::parser::memory_resolver::InMemoryResolver;\nuse encore_tsparser::parser::parser::{ParseContext, Parser};\nuse encore_tsparser::parser::resourceparser::PassOneParser;\nuse encore_tsparser::tsconfig::TsConfigPathResolver;\n\n// Bundled encore.dev SDK files (package.json + .ts sources).\ninclude!(concat!(env!(\"OUT_DIR\"), \"/encore_dev_files.rs\"));\n\n#[wasm_bindgen(start)]\npub fn init_panic_hook() {\n    console_error_panic_hook::set_once();\n}\n\n#[derive(Deserialize)]\nstruct InputFile {\n    name: String,\n    content: String,\n}\n\n#[derive(Serialize)]\nstruct ParseOutput {\n    ok: bool,\n    errors: Vec<String>,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    meta: Option<serde_json::Value>,\n}\n\n/// Parse source files.\n///\n/// `files_json`: JSON array of `[{name, content}]`.\n/// Files with paths starting with `node_modules/` are treated as dependencies\n/// (registered for module resolution but not parsed as user code).\n#[wasm_bindgen]\npub fn parse(files_json: &str) -> String {\n    let all_files: Vec<InputFile> = match serde_json::from_str(files_json) {\n        Ok(f) => f,\n        Err(e) => {\n            return error_output(&format!(\"invalid input JSON: {e}\"));\n        }\n    };\n\n    let (nm_files, user_files): (Vec<_>, Vec<_>) = all_files\n        .into_iter()\n        .partition(|f| f.name.starts_with(\"node_modules/\"));\n\n    run_parse(user_files, nm_files)\n}\n\nfn run_parse(files: Vec<InputFile>, mut nm_files: Vec<InputFile>) -> String {\n    // Filter out any user-provided encore.dev files and use the bundled version instead,\n    // since the parser is coupled to a specific SDK version.\n    nm_files.retain(|f| !f.name.starts_with(\"node_modules/encore.dev/\"));\n    for &(name, content) in ENCORE_DEV_FILES {\n        nm_files.push(InputFile {\n            name: name.to_string(),\n            content: content.to_string(),\n        });\n    }\n\n    let globals = Globals::new();\n    let cm: Lrc<SourceMap> = Default::default();\n    let errors: Arc<Mutex<Vec<String>>> = Default::default();\n\n    let emitter = WasmErrorEmitter {\n        cm: cm.clone(),\n        errors: errors.clone(),\n    };\n    let errs = Lrc::new(Handler::with_emitter(true, false, Box::new(emitter)));\n\n    let result = GLOBALS.set(&globals, || {\n        HANDLER.set(&errs, || {\n            let app_root = PathBuf::from(\"/app\");\n\n            // Build file paths for all files (user + node_modules)\n            let user_file_paths: Vec<PathBuf> =\n                files.iter().map(|f| app_root.join(&f.name)).collect();\n            let nm_file_paths: Vec<PathBuf> =\n                nm_files.iter().map(|f| app_root.join(&f.name)).collect();\n\n            let all_file_paths: Vec<PathBuf> = user_file_paths\n                .iter()\n                .chain(nm_file_paths.iter())\n                .cloned()\n                .collect();\n\n            // Create resolver and register package.json files\n            let mut resolver = InMemoryResolver::new(app_root.clone(), all_file_paths);\n\n            for f in &nm_files {\n                if f.name.ends_with(\"package.json\") {\n                    if let Ok(value) = serde_json::from_str::<serde_json::Value>(&f.content) {\n                        let path = app_root.join(&f.name);\n                        resolver.register_package_json(path, value);\n                    }\n                }\n            }\n\n            // Set up tsconfig path aliases if tsconfig.json is provided\n            if let Some(tsconfig_file) = files.iter().find(|f| f.name == \"tsconfig.json\") {\n                match TsConfigPathResolver::from_str(&app_root, &tsconfig_file.content) {\n                    Ok(tsconfig) => resolver.set_tsconfig(tsconfig),\n                    Err(e) => errors\n                        .lock()\n                        .unwrap()\n                        .push(format!(\"failed to parse tsconfig.json: {e}\")),\n                }\n            }\n\n            let pc = match ParseContext::with_boxed_resolver(\n                app_root.clone(),\n                Box::new(resolver),\n                cm.clone(),\n                errs.clone(),\n            ) {\n                Ok(pc) => pc,\n                Err(e) => {\n                    errors\n                        .lock()\n                        .unwrap()\n                        .push(format!(\"failed to create parse context: {e}\"));\n                    return None::<serde_json::Value>;\n                }\n            };\n\n            // Register all file contents with the module loader so that\n            // import resolution works for both user files and node_modules.\n            let all_contents: HashMap<PathBuf, String> = files\n                .iter()\n                .chain(nm_files.iter())\n                .map(|f| (app_root.join(&f.name), f.content.clone()))\n                .collect();\n            pc.loader.register_file_contents(all_contents);\n\n            let pass1 = PassOneParser::new(\n                pc.file_set.clone(),\n                pc.type_checker.clone(),\n                Default::default(),\n            );\n            let parser = Parser::new(&pc, pass1);\n\n            let file_data: Vec<(PathBuf, String)> = files\n                .iter()\n                .map(|f| (PathBuf::from(&f.name), f.content.clone()))\n                .collect();\n\n            let parse_result = parser.parse_from_files(file_data);\n            let app_desc = validate_and_describe(&pc, parse_result);\n\n            app_desc.and_then(|desc| serde_json::to_value(&desc.meta).ok())\n        })\n    });\n\n    let collected_errors: Vec<String> = errors.lock().unwrap().clone();\n    format_output(result, collected_errors)\n}\n\nfn error_output(msg: &str) -> String {\n    serde_json::to_string(&ParseOutput {\n        ok: false,\n        errors: vec![msg.to_string()],\n        meta: None,\n    })\n    .unwrap()\n}\n\nfn format_output(meta: Option<serde_json::Value>, collected_errors: Vec<String>) -> String {\n    let output = ParseOutput {\n        ok: collected_errors.is_empty() && meta.is_some(),\n        errors: collected_errors,\n        meta,\n    };\n\n    serde_json::to_string(&output).unwrap()\n}\n\nstruct WasmErrorEmitter {\n    cm: Lrc<dyn SourceMapper>,\n    errors: Arc<Mutex<Vec<String>>>,\n}\n\nimpl Emitter for WasmErrorEmitter {\n    fn emit(&mut self, db: &swc_common::errors::DiagnosticBuilder<'_>) {\n        let buf: WriteBuf = Default::default();\n        let mut w = EmitterWriter::new(Box::new(buf.clone()), Some(self.cm.clone()), false, false);\n        w.emit(db);\n        let s = buf.to_string();\n        self.errors.lock().unwrap().push(s);\n    }\n}\n\n#[derive(Default, Clone)]\nstruct WriteBuf(Arc<Mutex<Vec<u8>>>);\n\nimpl fmt::Display for WriteBuf {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", String::from_utf8_lossy(&self.0.lock().unwrap()))\n    }\n}\n\nimpl Write for WriteBuf {\n    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {\n        self.0.lock().unwrap().extend_from_slice(buf);\n        Ok(buf.len())\n    }\n\n    fn flush(&mut self) -> io::Result<()> {\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "v2/app/api_framework.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n)\n\nfunc configureAPIFramework(pc *parsectx.Context, services []*Service, res *parser.Result) option.Option[*apiframework.AppDesc] {\n\tvar (\n\t\tendpoints      = parser.Resources[*api.Endpoint](res)\n\t\tmiddlewares    = parser.Resources[*middleware.Middleware](res)\n\t\tauthHandlers   = parser.Resources[*authhandler.AuthHandler](res)\n\t\tserviceStructs = parser.Resources[*servicestruct.ServiceStruct](res)\n\t\tsubscriptions  = parser.Resources[*pubsub.Subscription](res)\n\t)\n\n\tif len(endpoints) == 0 && len(middlewares) == 0 && len(authHandlers) == 0 && len(serviceStructs) == 0 && len(subscriptions) == 0 {\n\t\treturn option.None[*apiframework.AppDesc]()\n\t}\n\n\tfw := &apiframework.AppDesc{}\n\n\t// First handle global API framework usage\n\t// i.e. auth handlers and middleware which apply across all services\n\n\t// Add the middleware\n\tvar svcMiddleware []*middleware.Middleware\n\tfor _, mw := range middlewares {\n\t\tif mw.Global {\n\t\t\tfw.GlobalMiddleware = append(fw.GlobalMiddleware, mw)\n\t\t} else {\n\t\t\tsvcMiddleware = append(svcMiddleware, mw)\n\t\t}\n\t}\n\n\t// Add the app's auth handler\n\tfor _, ah := range authHandlers {\n\t\tif fw.AuthHandler.Empty() {\n\t\t\tfw.AuthHandler = option.Some(ah)\n\t\t} else {\n\t\t\tpc.Errs.Add(\n\t\t\t\tauthhandler.ErrMultipleAuthHandlers.\n\t\t\t\t\tAtGoNode(fw.AuthHandler.MustGet().Decl.AST.Type, errors.AsError(\"first auth handler defined here\")).\n\t\t\t\t\tAtGoNode(ah.Decl.AST.Type, errors.AsError(\"second auth handler defined here\")),\n\t\t\t)\n\t\t}\n\t}\n\n\tmodifySvcDesc := func(pkg *pkginfo.Package, errTemplate *errors.Template, fn func(svc *Service, desc *apiframework.ServiceDesc)) {\n\t\tfor _, svc := range services {\n\t\t\tif pkg.FSPath.HasPrefix(svc.FSRoot) {\n\t\t\t\t// We've found the service. Initialize the framework service description\n\t\t\t\t// if necessary, and then call fn.\n\t\t\t\tdesc, ok := svc.Framework.Get()\n\t\t\t\tif !ok {\n\t\t\t\t\tdesc = &apiframework.ServiceDesc{\n\t\t\t\t\t\tRootPkg: pkg,\n\t\t\t\t\t}\n\t\t\t\t\tsvc.Framework = option.Some(desc)\n\t\t\t\t}\n\t\t\t\tfn(svc, desc)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// We couldn't find the service. Add an error and don't call fn.\n\t\tif errTemplate != nil {\n\t\t\tpc.Errs.Add(*errTemplate)\n\t\t} else {\n\t\t\tpc.Errs.Add(errNoServiceFound(pkg.ImportPath))\n\t\t}\n\t}\n\n\tfor _, ep := range endpoints {\n\t\tmodifySvcDesc(ep.Package(), nil, func(svc *Service, desc *apiframework.ServiceDesc) {\n\t\t\tdesc.Endpoints = append(desc.Endpoints, ep)\n\t\t})\n\t}\n\n\tfor _, mw := range middlewares {\n\t\tif !mw.Global {\n\t\t\tmissingErr := middleware.ErrSvcMiddlewareNotInService.AtGoNode(mw.Decl.AST.Name)\n\n\t\t\t// Per-service middleware.\n\t\t\tmodifySvcDesc(mw.Package(), &missingErr, func(svc *Service, desc *apiframework.ServiceDesc) {\n\t\t\t\tdesc.Middleware = append(desc.Middleware, mw)\n\t\t\t})\n\t\t}\n\t}\n\n\tfor _, ss := range serviceStructs {\n\t\tmodifySvcDesc(ss.Package(), nil, func(svc *Service, desc *apiframework.ServiceDesc) {\n\t\t\tif desc.ServiceStruct.Empty() {\n\t\t\t\tdesc.ServiceStruct = option.Some(ss)\n\t\t\t} else {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\tservicestruct.ErrDuplicateServiceStructs.\n\t\t\t\t\t\tAtGoNode(desc.ServiceStruct.MustGet().Decl.AST, errors.AsError(\"first service struct defined here\")).\n\t\t\t\t\t\tAtGoNode(ss.Decl.AST, errors.AsError(\"second service struct defined here\")),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Call modifySvcDesc for subscriptions and auth handlers, even though they don't actually\n\t// modify the framework description, to actually tag the package as a service.\n\tfor _, ss := range subscriptions {\n\t\tmodifySvcDesc(ss.Package(), nil, func(svc *Service, desc *apiframework.ServiceDesc) {})\n\t}\n\tfor _, ah := range authHandlers {\n\t\tmodifySvcDesc(ah.Package(), nil, func(svc *Service, desc *apiframework.ServiceDesc) {})\n\t}\n\n\treturn option.Some(fw)\n}\n"
  },
  {
    "path": "v2/app/apiframework/apiframework.go",
    "content": "package apiframework\n\nimport (\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n)\n\n// AppDesc describes an Encore Framework-based application.\ntype AppDesc struct {\n\t// GlobalMiddleware is the list of application-global middleware.\n\tGlobalMiddleware []*middleware.Middleware\n\n\t// AuthHandler defines the application's auth handler, if any.\n\tAuthHandler option.Option[*authhandler.AuthHandler]\n}\n\n// ServiceDesc describes an Encore Framework-based service.\n//\n// For code that deals with general services, use *service.Service instead of this type.\ntype ServiceDesc struct {\n\t// Middleware are the service-specific middleware\n\tMiddleware []*middleware.Middleware\n\n\t// RootPkg is the root package of the service.\n\tRootPkg *pkginfo.Package\n\n\t// Endpoints are the endpoints defined in this service.\n\tEndpoints []*api.Endpoint\n\n\t// ServiceStruct defines the service's service struct, if any.\n\tServiceStruct option.Option[*servicestruct.ServiceStruct]\n}\n"
  },
  {
    "path": "v2/app/app.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\n// Desc describes an Encore application.\ntype Desc struct {\n\tErrs *perr.List\n\n\tBuildInfo  parsectx.BuildInfo\n\tMainModule *pkginfo.Module\n\tParse      *parser.Result\n\tServices   []*Service\n\tGateways   []*Gateway\n\n\t// Framework describes API Framework-specific application-global data.\n\tFramework option.Option[*apiframework.AppDesc]\n\n\t// ResourceUsageOutsideServices describes resources that are used outside of a service.\n\tResourceUsageOutsideServices map[resource.Resource][]usage.Usage\n}\n\n// MatchingMiddleware reports which middleware applies to the given RPC,\n// and the order they apply in.\nfunc (d *Desc) MatchingMiddleware(ep *api.Endpoint) []*middleware.Middleware {\n\tvar matches []*middleware.Middleware\n\n\t// Ensure middleware ordering is preserved.\n\n\t// First add global middleware.\n\td.Framework.ForAll(func(fw *apiframework.AppDesc) {\n\t\tfor _, mw := range fw.GlobalMiddleware {\n\t\t\tif mw.Target.ContainsAny(ep.Tags) {\n\t\t\t\tmatches = append(matches, mw)\n\t\t\t}\n\t\t}\n\t})\n\n\t// Then add service-specific middleware.\n\tif svc, ok := d.ServiceForPath(ep.File.Pkg.FSPath); ok {\n\t\tsvc.Framework.ForAll(func(fw *apiframework.ServiceDesc) {\n\t\t\tfor _, mw := range fw.Middleware {\n\t\t\t\tif mw.Target.ContainsAny(ep.Tags) {\n\t\t\t\t\tmatches = append(matches, mw)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\treturn matches\n}\n\n// MatchingGlobalMiddleware reports which global middleware applies to the given RPC,\n// and the order they apply in.\nfunc (d *Desc) MatchingGlobalMiddleware(ep *api.Endpoint) []*middleware.Middleware {\n\tvar matches []*middleware.Middleware\n\td.Framework.ForAll(func(fw *apiframework.AppDesc) {\n\t\tfor _, mw := range fw.GlobalMiddleware {\n\t\t\tif mw.Target.ContainsAny(ep.Tags) {\n\t\t\t\tmatches = append(matches, mw)\n\t\t\t}\n\t\t}\n\t})\n\treturn matches\n}\n\n// ValidateAndDescribe validates the application and computes the\n// application description.\nfunc ValidateAndDescribe(pc *parsectx.Context, result *parser.Result) *Desc {\n\tdefer pc.Trace(\"app.ValidateAndDescribe\").Done()\n\n\t// First we want to discover the service layout\n\tservices := discoverServices(pc, result)\n\n\t// We always have a default API gateway, for now.\n\tgateways := []*Gateway{{EncoreName: \"api-gateway\"}}\n\n\t// Now we can configure the API framework by combining the service information\n\t// with the parse results.\n\tframework := configureAPIFramework(pc, services, result)\n\n\tdesc := &Desc{\n\t\tErrs:                         pc.Errs,\n\t\tBuildInfo:                    pc.Build,\n\t\tMainModule:                   result.MainModule(),\n\t\tParse:                        result,\n\t\tServices:                     services,\n\t\tGateways:                     gateways,\n\t\tFramework:                    framework,\n\t\tResourceUsageOutsideServices: make(map[resource.Resource][]usage.Usage),\n\t}\n\n\t// Find each services infra binds and usage.\n\tdesc.locateResourceBinds(result)\n\tdesc.locateResourceUsage(result)\n\n\t// Run the application-level validations against the application description.\n\tdesc.validate(pc, result)\n\n\treturn desc\n}\n\n// ServiceForPath returns the service a given folder path belongs to, if any.\nfunc (d *Desc) ServiceForPath(path paths.FS) (*Service, bool) {\n\tfor _, svc := range d.Services {\n\t\tif path.HasPrefix(svc.FSRoot) {\n\t\t\treturn svc, true\n\t\t}\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "v2/app/errors.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nconst (\n\tserviceHelp = \"For more information on services and how to define them, see https://encore.dev/docs/primitives/services\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"app\",\n\t\t\"\",\n\t)\n\n\terrServiceContainedWitinAnother = errRange.Newf(\n\t\t\"Service contained within another service\",\n\t\t\"The service %s was found within the service %s. Encore does not allow services to be nested.\",\n\t\terrors.WithDetails(serviceHelp),\n\t)\n\n\terrDuplicateServiceNames = errRange.Newf(\n\t\t\"Duplicate service names\",\n\t\t\"Two services were found with the same name %q, services must have unique names.\",\n\t)\n\n\terrNoServiceFound = errRange.Newf(\n\t\t\"No service found\",\n\t\t\"No service was found for package %q.\",\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrResourceDefinedOutsideOfService = errRange.New(\n\t\t\"Resource defined outside of service\",\n\t\t\"Resources can only be defined within a service.\",\n\t)\n\n\terrResourceUsedOutsideService = errRange.New(\n\t\t\"Invalid resource usage\",\n\t\t\"Infrastructure resources can only be referenced within services.\",\n\t\terrors.WithDetails(\"To use infrastructure resources outside services, instead pass a reference to the resource into the library.\"),\n\t)\n)\n"
  },
  {
    "path": "v2/app/gateway.go",
    "content": "package app\n\n// Gateway describes an Encore gateway.\ntype Gateway struct {\n\t// EncoreName is the encore-name of the gateway.\n\tEncoreName string\n}\n"
  },
  {
    "path": "v2/app/legacymeta/legacymeta.go",
    "content": "package legacymeta\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"go/token\"\n\tgotoken \"go/token\"\n\t\"slices\"\n\t\"sort\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/caches\"\n\t\"encr.dev/v2/parser/infra/config\"\n\t\"encr.dev/v2/parser/infra/crons\"\n\t\"encr.dev/v2/parser/infra/metrics\"\n\t\"encr.dev/v2/parser/infra/objects\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/infra/secrets\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\ntype builder struct {\n\terrs *perr.List\n\tapp  *app.Desc\n\tmd   *meta.Data // metadata being generated\n\n\tdecls map[declKey]uint32\n\tnodes *TraceNodes\n}\n\nfunc Compute(errs *perr.List, appDesc *app.Desc) (*meta.Data, *TraceNodes) {\n\tb := &builder{\n\t\terrs:  errs,\n\t\tapp:   appDesc,\n\t\tdecls: make(map[declKey]uint32),\n\t}\n\tb.nodes = newTraceNodes(b)\n\n\tmd := b.Build()\n\n\treturn md, b.nodes\n}\n\nfunc (b *builder) Build() *meta.Data {\n\t// TODO(andre) We assume the framework is used for now.\n\t// When we add support for not using the framework we'll need\n\t// to handle this differently.\n\n\tb.md = &meta.Data{\n\t\tModulePath:         string(b.app.MainModule.Path),\n\t\tAppRevision:        b.app.BuildInfo.Revision,\n\t\tUncommittedChanges: b.app.BuildInfo.UncommittedChanges,\n\t\tExperiments:        b.app.BuildInfo.Experiments.StringList(),\n\t\tLanguage:           meta.Lang_GO,\n\t}\n\tmd := b.md\n\n\tfor _, gw := range b.app.Gateways {\n\t\tb.md.Gateways = append(b.md.Gateways, &meta.Gateway{\n\t\t\tEncoreName: gw.EncoreName,\n\t\t})\n\t}\n\n\tsvcByName := make(map[string]*meta.Service, len(b.app.Services))\n\tfor _, svc := range b.app.Services {\n\t\tout := &meta.Service{\n\t\t\tName: svc.Name,\n\t\t}\n\t\tsvcByName[svc.Name] = out\n\t\tmd.Svcs = append(md.Svcs, out)\n\n\t\tif fw, ok := svc.Framework.Get(); ok {\n\t\t\tout.RelPath = b.relPath(fw.RootPkg.ImportPath)\n\t\t\tfor _, ep := range fw.Endpoints {\n\t\t\t\trpc := &meta.RPC{\n\t\t\t\t\tName:           ep.Name,\n\t\t\t\t\tDoc:            zeroNil(ep.Doc),\n\t\t\t\t\tServiceName:    svc.Name,\n\t\t\t\t\tRequestSchema:  b.schemaTypeUnwrapPointer(ep.Request),\n\t\t\t\t\tResponseSchema: b.schemaTypeUnwrapPointer(ep.Response),\n\t\t\t\t\tProto:          meta.RPC_REGULAR,\n\t\t\t\t\tLoc:            b.schemaLoc(ep.Decl.File, ep.Decl.AST),\n\t\t\t\t\tPath:           b.apiPath(ep.Decl.AST.Pos(), ep.Path),\n\t\t\t\t\tHttpMethods:    ep.HTTPMethods,\n\t\t\t\t\tTags:           ep.Tags.ToProto(),\n\t\t\t\t\tSensitive:      ep.Sensitive,\n\t\t\t\t\tExpose:         make(map[string]*meta.RPC_ExposeOptions),\n\t\t\t\t}\n\t\t\t\tif ep.Raw {\n\t\t\t\t\trpc.Proto = meta.RPC_RAW\n\t\t\t\t}\n\n\t\t\t\tswitch ep.Access {\n\t\t\t\tcase api.Public:\n\t\t\t\t\trpc.AccessType = meta.RPC_PUBLIC\n\t\t\t\t\trpc.Expose[\"api-gateway\"] = &meta.RPC_ExposeOptions{}\n\t\t\t\t\trpc.AllowUnauthenticated = true\n\t\t\t\tcase api.Auth:\n\t\t\t\t\trpc.AccessType = meta.RPC_AUTH\n\t\t\t\t\trpc.Expose[\"api-gateway\"] = &meta.RPC_ExposeOptions{}\n\t\t\t\tcase api.Private:\n\t\t\t\t\trpc.AccessType = meta.RPC_PRIVATE\n\t\t\t\t\trpc.AllowUnauthenticated = true\n\t\t\t\tdefault:\n\t\t\t\t\tb.errs.Addf(ep.Decl.AST.Pos(), \"internal error: unknown API access type %v\", ep.Access)\n\t\t\t\t}\n\n\t\t\t\tout.Rpcs = append(out.Rpcs, rpc)\n\t\t\t\tb.nodes.addEndpoint(ep, svc.Name)\n\t\t\t}\n\n\t\t\t// Sort the RPCs for deterministic output.\n\t\t\tslices.SortFunc(out.Rpcs, func(a, b *meta.RPC) int {\n\t\t\t\treturn cmp.Compare(a.Name, b.Name)\n\t\t\t})\n\n\t\t\t// Do we have a database associated with the service?\n\t\t\t// Note: we use the binds because it's possible to have an\n\t\t\t// implicit bind that's not actually used. This is to ensure\n\t\t\t// compatibility with the v1 parser.\n\t\t\tfor res := range svc.ResourceBinds {\n\t\t\t\tswitch res := res.(type) {\n\t\t\t\tcase *sqldb.Database:\n\t\t\t\t\tout.Databases = append(out.Databases, res.Name)\n\t\t\t\t\t// If the database name is the same as the service,\n\t\t\t\t\t// it's the database defined by said service.\n\t\t\t\t\tif res.Name == svc.Name {\n\t\t\t\t\t\tout.Migrations = fns.Map(res.Migrations, transformMigration)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t}\n\n\tappPackages := b.app.Parse.AppPackages()\n\tpkgByPath := make(map[paths.Pkg]*meta.Package, len(appPackages))\n\tfor _, pkg := range appPackages {\n\t\tmetaPkg := &meta.Package{\n\t\t\tRelPath:     b.relPath(pkg.ImportPath),\n\t\t\tName:        pkg.Name,\n\t\t\tDoc:         pkg.Doc,\n\t\t\tServiceName: \"\",\n\t\t\tSecrets:     nil,\n\t\t\tRpcCalls:    nil,\n\t\t\tTraceNodes:  nil,\n\t\t}\n\t\tpkgByPath[pkg.ImportPath] = metaPkg\n\n\t\tif svc, ok := b.app.ServiceForPath(pkg.FSPath); ok {\n\t\t\tmetaPkg.ServiceName = svc.Name\n\t\t}\n\n\t\t// Don't add main packages to the list of packages.\n\t\t// Still track it in the map since other resources\n\t\t// may depend on the package being known.\n\t\tif pkg.Name != \"main\" {\n\t\t\tmd.Pkgs = append(md.Pkgs, metaPkg)\n\t\t}\n\n\t\tseenRPCCalls := make(map[pkginfo.QualifiedName]bool)\n\t\taddRPCCall := func(ep *api.Endpoint) {\n\t\t\tpkg := ep.Package()\n\t\t\tqn := pkginfo.Q(pkg.ImportPath, ep.Name)\n\t\t\tif !seenRPCCalls[qn] {\n\t\t\t\tseenRPCCalls[qn] = true\n\t\t\t\tmetaPkg.RpcCalls = append(metaPkg.RpcCalls, &meta.QualifiedName{\n\t\t\t\t\tPkg:  b.relPath(pkg.ImportPath),\n\t\t\t\t\tName: ep.Name,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tfor _, u := range b.app.Parse.UsagesInPkg(pkg.ImportPath) {\n\t\t\tswitch u := u.(type) {\n\t\t\tcase *api.CallUsage:\n\t\t\t\taddRPCCall(u.Endpoint)\n\t\t\tcase *api.ReferenceUsage:\n\t\t\t\t// NOTE: The legacy meta does not distinguish between calls and references,\n\t\t\t\t// and adds both to the list of RPC calls. Replicate this behavior.\n\t\t\t\taddRPCCall(u.Endpoint)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Keep track of state needed for dependent resources.\n\tvar (\n\t\t// dependent are the resources that depend on other resources.\n\t\t// They're processed in a second pass.\n\t\tdependent []resource.Resource\n\n\t\ttopicMap   = make(map[pkginfo.QualifiedName]*meta.PubSubTopic)\n\t\tclusterMap = make(map[pkginfo.QualifiedName]*meta.CacheCluster)\n\t)\n\n\tselectorLookup := computeSelectorLookup(b.app)\n\tfor _, r := range b.app.Parse.Resources() {\n\t\tswitch r := r.(type) {\n\t\tcase *crons.Job:\n\t\t\tcj := &meta.CronJob{\n\t\t\t\tId:       r.Name,\n\t\t\t\tTitle:    r.Title,\n\t\t\t\tDoc:      zeroNil(r.Doc),\n\t\t\t\tSchedule: r.Schedule,\n\t\t\t\tEndpoint: nil,\n\t\t\t}\n\t\t\tmd.CronJobs = append(md.CronJobs, cj)\n\t\t\tif ep, ok := b.app.Parse.ResourceForQN(r.Endpoint).Get(); ok {\n\t\t\t\tendpoint := ep.(*api.Endpoint)\n\t\t\t\tcj.Endpoint = &meta.QualifiedName{\n\t\t\t\t\tPkg:  b.relPath(endpoint.File.Pkg.ImportPath),\n\t\t\t\t\tName: endpoint.Name,\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tb.errs.Addf(r.EndpointAST.Pos(), \"could not find endpoint %q\", r.Endpoint)\n\t\t\t}\n\n\t\tcase *authhandler.AuthHandler:\n\t\t\tif svc, ok := b.app.ServiceForPath(r.Decl.File.FSPath); ok {\n\t\t\t\tah := &meta.AuthHandler{\n\t\t\t\t\tName:        r.Name,\n\t\t\t\t\tDoc:         r.Doc,\n\t\t\t\t\tPkgPath:     r.Package().ImportPath.String(),\n\t\t\t\t\tPkgName:     r.Package().Name,\n\t\t\t\t\tLoc:         b.schemaLoc(r.Decl.File, r.Decl.AST),\n\t\t\t\t\tParams:      b.schemaTypeUnwrapPointer(r.Param),\n\t\t\t\t\tServiceName: svc.Name,\n\t\t\t\t}\n\t\t\t\tif data, ok := r.AuthData.Get(); ok {\n\t\t\t\t\tah.AuthData = b.typeDeclRefUnwrapPointer(data)\n\t\t\t\t}\n\t\t\t\tmd.AuthHandler = ah\n\t\t\t\tb.nodes.addAuthHandler(r, svc.Name)\n\t\t\t} else {\n\t\t\t\tb.errs.Addf(r.Decl.AST.Pos(), \"auth handler %q must be defined within a service\", r.Name)\n\t\t\t}\n\n\t\tcase *sqldb.Database:\n\t\t\tdb := &meta.SQLDatabase{\n\t\t\t\tName:             r.Name,\n\t\t\t\tDoc:              zeroNil(r.Doc),\n\t\t\t\tMigrationRelPath: zeroNil(r.MigrationDir.String()),\n\t\t\t\tMigrations:       fns.Map(r.Migrations, transformMigration),\n\t\t\t}\n\t\t\tmd.SqlDatabases = append(md.SqlDatabases, db)\n\n\t\tcase *pubsub.Topic:\n\t\t\ttopic := &meta.PubSubTopic{\n\t\t\t\tName:          r.Name,\n\t\t\t\tDoc:           zeroNil(r.Doc),\n\t\t\t\tMessageType:   b.typeDeclRefUnwrapPointer(r.MessageType),\n\t\t\t\tOrderingKey:   r.OrderingAttribute,\n\t\t\t\tPublishers:    nil,\n\t\t\t\tSubscriptions: nil, // filled in later\n\t\t\t}\n\n\t\t\tseenPublishers := make(map[string]bool)\n\t\t\taddPublisher := func(svcName string) {\n\t\t\t\tif !seenPublishers[svcName] {\n\t\t\t\t\tseenPublishers[svcName] = true\n\t\t\t\t\ttopic.Publishers = append(topic.Publishers, &meta.PubSubTopic_Publisher{\n\t\t\t\t\t\tServiceName: svcName,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Find all the publishers\n\t\t\tfor _, u := range b.app.Parse.Usages(r) {\n\t\t\t\tswitch u := u.(type) {\n\t\t\t\tcase *pubsub.PublishUsage:\n\t\t\t\t\tif svc, ok := b.app.ServiceForPath(u.DeclaredIn().FSPath); ok {\n\t\t\t\t\t\t// Is the publish call within a service? If so add that service as the publisher.\n\t\t\t\t\t\taddPublisher(svc.Name)\n\t\t\t\t\t} else if res2, ok := b.app.Parse.ResourceConstructorContaining(u).Get(); ok {\n\t\t\t\t\t\t// Otherwise, is the publish call within a global middleware?\n\t\t\t\t\t\t// If so add all services that middleware applies to.\n\t\t\t\t\t\tswitch res2 := res2.(type) {\n\t\t\t\t\t\tcase *middleware.Middleware:\n\t\t\t\t\t\t\tif res2.Global {\n\t\t\t\t\t\t\t\tfor _, svc := range selectorLookup.GetServices(res2.Target) {\n\t\t\t\t\t\t\t\t\taddPublisher(svc.Name)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\tcase *pubsub.RefUsage:\n\t\t\t\t\tif u.HasPerm(pubsub.PublishPerm) {\n\t\t\t\t\t\tif svc, ok := b.app.ServiceForPath(u.DeclaredIn().FSPath); ok {\n\t\t\t\t\t\t\t// Is the publish call within a service? If so add that service as the publisher.\n\t\t\t\t\t\t\taddPublisher(svc.Name)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort the publishers\n\t\t\tslices.SortFunc(topic.Publishers, func(a, b *meta.PubSubTopic_Publisher) int {\n\t\t\t\treturn cmp.Compare(a.ServiceName, b.ServiceName)\n\t\t\t})\n\n\t\t\tswitch r.DeliveryGuarantee {\n\t\t\tcase pubsub.ExactlyOnce:\n\t\t\t\ttopic.DeliveryGuarantee = meta.PubSubTopic_EXACTLY_ONCE\n\t\t\tcase pubsub.AtLeastOnce:\n\t\t\t\ttopic.DeliveryGuarantee = meta.PubSubTopic_AT_LEAST_ONCE\n\t\t\tdefault:\n\t\t\t\tpanic(fmt.Sprintf(\"unknown delivery guarantee %v\", r.DeliveryGuarantee))\n\t\t\t}\n\n\t\t\tfor _, b := range b.app.Parse.PkgDeclBinds(r) {\n\t\t\t\ttopicMap[b.QualifiedName()] = topic\n\t\t\t}\n\t\t\tmd.PubsubTopics = append(md.PubsubTopics, topic)\n\n\t\tcase *objects.Bucket:\n\t\t\tbkt := &meta.Bucket{\n\t\t\t\tName:      r.Name,\n\t\t\t\tDoc:       zeroNil(r.Doc),\n\t\t\t\tVersioned: r.Versioned,\n\t\t\t\tPublic:    r.Public,\n\t\t\t}\n\t\t\tmd.Buckets = append(md.Buckets, bkt)\n\n\t\t\tpermsBySvc := make(map[string][]objects.Perm)\n\t\t\taddPerms := func(svcName string, perms ...objects.Perm) {\n\t\t\t\tpermsBySvc[svcName] = append(permsBySvc[svcName], perms...)\n\t\t\t}\n\n\t\t\t// Record all the permissions.\n\t\t\tfor _, u := range b.app.Parse.Usages(r) {\n\t\t\t\tswitch u := u.(type) {\n\t\t\t\tcase *objects.MethodUsage:\n\t\t\t\t\tif svc, ok := b.app.ServiceForPath(u.DeclaredIn().FSPath); ok {\n\t\t\t\t\t\taddPerms(svc.Name, u.Perm)\n\t\t\t\t\t}\n\t\t\t\tcase *objects.RefUsage:\n\t\t\t\t\tif svc, ok := b.app.ServiceForPath(u.DeclaredIn().FSPath); ok {\n\t\t\t\t\t\taddPerms(svc.Name, u.Perms...)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Collect the perms\n\t\t\tfor svcName, perms := range permsBySvc {\n\t\t\t\tif svc, ok := svcByName[svcName]; ok {\n\t\t\t\t\tops := fns.Map(perms, func(p objects.Perm) meta.BucketUsage_Operation {\n\t\t\t\t\t\top, ok := p.ToMeta()\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tb.errs.Addf(token.NoPos, \"unsupported permission %v\", p)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn op\n\t\t\t\t\t})\n\t\t\t\t\tslices.Sort(ops)\n\t\t\t\t\tsvc.Buckets = append(svc.Buckets, &meta.BucketUsage{\n\t\t\t\t\t\tBucket:     bkt.Name,\n\t\t\t\t\t\tOperations: ops,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase *caches.Cluster:\n\t\t\tcluster := &meta.CacheCluster{\n\t\t\t\tName:           r.Name,\n\t\t\t\tDoc:            r.Doc,\n\t\t\t\tKeyspaces:      nil,\n\t\t\t\tEvictionPolicy: r.EvictionPolicy,\n\t\t\t}\n\t\t\tfor _, b := range b.app.Parse.PkgDeclBinds(r) {\n\t\t\t\tclusterMap[b.QualifiedName()] = cluster\n\t\t\t}\n\t\t\tmd.CacheClusters = append(md.CacheClusters, cluster)\n\n\t\tcase *metrics.Metric:\n\t\t\tvar svcName *string\n\t\t\tif svc, ok := b.app.ServiceForPath(r.File.Pkg.FSPath); ok {\n\t\t\t\tsvcName = &svc.Name\n\t\t\t}\n\n\t\t\tm := &meta.Metric{\n\t\t\t\tName:        r.Name,\n\t\t\t\tValueType:   b.builtinType(r.ValueType),\n\t\t\t\tDoc:         r.Doc,\n\t\t\t\tServiceName: svcName,\n\t\t\t}\n\t\t\tfor _, label := range r.Labels {\n\t\t\t\tm.Labels = append(m.Labels, &meta.Metric_Label{\n\t\t\t\t\tKey:  label.Key,\n\t\t\t\t\tDoc:  label.Doc,\n\t\t\t\t\tType: b.builtinType(label.Type),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif typ, ok := r.LabelType.Get(); ok {\n\t\t\t\t// Register any declarations\n\t\t\t\tb.schemaType(typ)\n\t\t\t}\n\n\t\t\tswitch r.Type {\n\t\t\tcase metrics.Counter:\n\t\t\t\tm.Kind = meta.Metric_COUNTER\n\t\t\tcase metrics.Gauge:\n\t\t\t\tm.Kind = meta.Metric_GAUGE\n\t\t\tdefault:\n\t\t\t\tpanic(fmt.Sprintf(\"unknown metric type %v\", r.Type))\n\t\t\t}\n\n\t\t\tmd.Metrics = append(md.Metrics, m)\n\n\t\tcase *config.Load:\n\t\t\tif svc, ok := b.app.ServiceForPath(r.File.Pkg.FSPath); ok {\n\t\t\t\tif metaSvc, ok := svcByName[svc.Name]; ok {\n\t\t\t\t\tmetaSvc.HasConfig = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Register the types.\n\t\t\tb.schemaType(r.Type)\n\n\t\tcase *secrets.Secrets:\n\t\t\tpkg, ok := pkgByPath[r.Package().ImportPath]\n\t\t\tif !ok {\n\t\t\t\tb.errs.Addf(r.ASTExpr().Pos(), \"could not find package %q\", r.Package().ImportPath)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpkg.Secrets = append(pkg.Secrets, r.Keys...)\n\t\t\tsort.Strings(pkg.Secrets)\n\n\t\tcase *middleware.Middleware:\n\t\t\tmw := &meta.Middleware{\n\t\t\t\tName: &meta.QualifiedName{\n\t\t\t\t\tPkg:  b.relPath(r.Package().ImportPath),\n\t\t\t\t\tName: r.Decl.Name,\n\t\t\t\t},\n\t\t\t\tDoc:         r.Doc,\n\t\t\t\tLoc:         b.schemaLoc(r.Decl.File, r.Decl.AST),\n\t\t\t\tGlobal:      r.Global,\n\t\t\t\tServiceName: nil,\n\t\t\t\tTarget:      r.Target.ToProto(),\n\t\t\t}\n\t\t\tmd.Middleware = append(md.Middleware, mw)\n\t\t\tif svc, ok := b.app.ServiceForPath(r.File.Pkg.FSPath); ok {\n\t\t\t\tmw.ServiceName = &svc.Name\n\t\t\t}\n\n\t\t\tb.nodes.addMiddleware(r)\n\n\t\tcase *servicestruct.ServiceStruct:\n\t\t\tif svc, ok := b.app.ServiceForPath(r.Decl.File.FSPath); ok {\n\t\t\t\tb.nodes.addServiceStruct(r, svc.Name)\n\t\t\t}\n\n\t\tcase *pubsub.Subscription, *caches.Keyspace:\n\t\t\tdependent = append(dependent, r)\n\t\t}\n\t}\n\n\t// Make a second pass for resources that depend on other resources.\n\tfor _, r := range dependent {\n\t\tswitch r := r.(type) {\n\t\tcase *pubsub.Subscription:\n\t\t\ttopic, ok := topicMap[r.Topic]\n\t\t\tif !ok {\n\t\t\t\tb.errs.Addf(r.ASTExpr().Pos(), \"topic %q not found\",\n\t\t\t\t\tr.Topic.NaiveDisplayName())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsvc, ok := b.app.ServiceForPath(r.File.Pkg.FSPath)\n\t\t\tif !ok {\n\t\t\t\tb.errs.Addf(r.ASTExpr().Pos(), \"pubsub subscription %q must be defined within a service\",\n\t\t\t\t\tr.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttopic.Subscriptions = append(topic.Subscriptions, &meta.PubSubTopic_Subscription{\n\t\t\t\tName:             r.Name,\n\t\t\t\tServiceName:      svc.Name,\n\t\t\t\tAckDeadline:      r.Cfg.AckDeadline.Nanoseconds(),\n\t\t\t\tMessageRetention: r.Cfg.MessageRetention.Nanoseconds(),\n\t\t\t\tMaxConcurrency:   zeroNil(int32(r.Cfg.MaxConcurrency)),\n\t\t\t\tRetryPolicy: &meta.PubSubTopic_RetryPolicy{\n\t\t\t\t\tMinBackoff: r.Cfg.MinRetryBackoff.Nanoseconds(),\n\t\t\t\t\tMaxBackoff: r.Cfg.MaxRetryBackoff.Nanoseconds(),\n\t\t\t\t\tMaxRetries: int64(r.Cfg.MaxRetries),\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tb.nodes.addSub(r, svc.Name, topic.Name)\n\n\t\tcase *caches.Keyspace:\n\t\t\tcluster, ok := clusterMap[r.Cluster]\n\t\t\tif !ok {\n\t\t\t\tb.errs.Addf(r.ASTExpr().Pos(), \"cluster %q not found\",\n\t\t\t\t\tr.Cluster.NaiveDisplayName())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsvc, ok := b.app.ServiceForPath(r.File.Pkg.FSPath)\n\t\t\tif !ok {\n\t\t\t\tb.errs.Addf(r.ASTExpr().Pos(), \"cache keyspace must be defined within a service\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcluster.Keyspaces = append(cluster.Keyspaces, &meta.CacheCluster_Keyspace{\n\t\t\t\tService:     svc.Name,\n\t\t\t\tKeyType:     b.schemaType(r.KeyType),\n\t\t\t\tValueType:   b.schemaType(r.ValueType),\n\t\t\t\tPathPattern: b.keyspacePath(r.Path),\n\t\t\t\tDoc:         r.Doc,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Add the allocated trace nodes to each package.\n\tfor pkgPath, pkg := range pkgByPath {\n\t\tpkg.TraceNodes = b.nodes.forPkg(pkgPath)\n\t}\n\n\t// If we have an auth handler, consider that an explicit gateway.\n\tif md.AuthHandler != nil && len(md.Gateways) == 0 {\n\t\tmd.Gateways = append(md.Gateways, &meta.Gateway{\n\t\t\tEncoreName: \"api-gateway\",\n\t\t\tExplicit: &meta.Gateway_Explicit{\n\t\t\t\tServiceName: md.AuthHandler.ServiceName,\n\t\t\t\tAuthHandler: md.AuthHandler,\n\t\t\t},\n\t\t})\n\t} else if len(md.Gateways) == 0 {\n\t\t// Otherwise, add a default gateway.\n\t\tmd.Gateways = append(md.Gateways, &meta.Gateway{\n\t\t\tEncoreName: \"api-gateway\",\n\t\t\tExplicit:   nil,\n\t\t})\n\t}\n\n\treturn md\n}\n\nfunc (b *builder) apiPath(pos gotoken.Pos, path *resourcepaths.Path) *meta.Path {\n\tres := &meta.Path{\n\t\tType: meta.Path_URL,\n\t}\n\tfor _, p := range path.Segments {\n\t\tseg := &meta.PathSegment{\n\t\t\tValue: p.Value,\n\t\t}\n\n\t\tswitch p.ValueType {\n\t\tcase schema.String:\n\t\t\tseg.ValueType = meta.PathSegment_STRING\n\t\tcase schema.Bool:\n\t\t\tseg.ValueType = meta.PathSegment_BOOL\n\t\tcase schema.Int8:\n\t\t\tseg.ValueType = meta.PathSegment_INT8\n\t\tcase schema.Int16:\n\t\t\tseg.ValueType = meta.PathSegment_INT16\n\t\tcase schema.Int32:\n\t\t\tseg.ValueType = meta.PathSegment_INT32\n\t\tcase schema.Int64:\n\t\t\tseg.ValueType = meta.PathSegment_INT64\n\t\tcase schema.Int:\n\t\t\tseg.ValueType = meta.PathSegment_INT\n\t\tcase schema.Uint8:\n\t\t\tseg.ValueType = meta.PathSegment_UINT8\n\t\tcase schema.Uint16:\n\t\t\tseg.ValueType = meta.PathSegment_UINT16\n\t\tcase schema.Uint32:\n\t\t\tseg.ValueType = meta.PathSegment_UINT32\n\t\tcase schema.Uint64:\n\t\t\tseg.ValueType = meta.PathSegment_UINT64\n\t\tcase schema.Uint:\n\t\t\tseg.ValueType = meta.PathSegment_UINT\n\t\tcase schema.UUID:\n\t\t\tseg.ValueType = meta.PathSegment_UUID\n\t\tdefault:\n\t\t\tb.errs.Addf(pos, \"internal error: unknown path segment value type %v\", p.ValueType)\n\t\t}\n\n\t\tswitch p.Type {\n\t\tcase resourcepaths.Literal:\n\t\t\tseg.Type = meta.PathSegment_LITERAL\n\t\tcase resourcepaths.Param:\n\t\t\tseg.Type = meta.PathSegment_PARAM\n\t\tcase resourcepaths.Wildcard:\n\t\t\tseg.Type = meta.PathSegment_WILDCARD\n\t\tcase resourcepaths.Fallback:\n\t\t\tseg.Type = meta.PathSegment_FALLBACK\n\t\t}\n\n\t\tres.Segments = append(res.Segments, seg)\n\t}\n\treturn res\n}\n\nfunc transformMigration(res sqldb.MigrationFile) *meta.DBMigration {\n\treturn &meta.DBMigration{\n\t\tFilename:    res.Filename,\n\t\tNumber:      uint64(res.Number),\n\t\tDescription: res.Description,\n\t}\n}\n\nfunc (b *builder) keyspacePath(path *resourcepaths.Path) *meta.Path {\n\tres := &meta.Path{\n\t\tType: meta.Path_CACHE_KEYSPACE,\n\t}\n\tfor _, p := range path.Segments {\n\t\tseg := &meta.PathSegment{\n\t\t\tValue: p.Value,\n\t\t}\n\n\t\tswitch p.Type {\n\t\tcase resourcepaths.Literal:\n\t\t\tseg.Type = meta.PathSegment_LITERAL\n\t\tcase resourcepaths.Param:\n\t\t\tseg.Type = meta.PathSegment_PARAM\n\t\t}\n\n\t\tres.Segments = append(res.Segments, seg)\n\t}\n\treturn res\n}\n\nfunc (b *builder) relPath(pkg paths.Pkg) string {\n\trel, ok := b.app.MainModule.Path.RelativePathToPkg(pkg)\n\tif !ok {\n\t\tpanic(\"cannot compute relative path to package outside main module: \" + pkg.String())\n\t}\n\treturn rel.String()\n}\n\nfunc zeroNil[T comparable](val T) *T {\n\tvar zero T\n\tif val == zero {\n\t\treturn nil\n\t}\n\treturn &val\n}\n"
  },
  {
    "path": "v2/app/legacymeta/schema.go",
    "content": "package legacymeta\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/pkg/paths\"\n\tschema \"encr.dev/proto/encore/parser/schema/v1\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\tschemav2 \"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"github.com/fatih/structtag\"\n)\n\nfunc (b *builder) builtinType(typ schemav2.BuiltinType) schema.Builtin {\n\tswitch typ.Kind {\n\tcase schemav2.Bool:\n\t\treturn schema.Builtin_BOOL\n\tcase schemav2.Int:\n\t\treturn schema.Builtin_INT\n\tcase schemav2.Int8:\n\t\treturn schema.Builtin_INT8\n\tcase schemav2.Int16:\n\t\treturn schema.Builtin_INT16\n\tcase schemav2.Int32:\n\t\treturn schema.Builtin_INT32\n\tcase schemav2.Int64:\n\t\treturn schema.Builtin_INT64\n\tcase schemav2.Uint:\n\t\treturn schema.Builtin_UINT\n\tcase schemav2.Uint8:\n\t\treturn schema.Builtin_UINT8\n\tcase schemav2.Uint16:\n\t\treturn schema.Builtin_UINT16\n\tcase schemav2.Uint32:\n\t\treturn schema.Builtin_UINT32\n\tcase schemav2.Uint64:\n\t\treturn schema.Builtin_UINT64\n\n\tcase schemav2.Float32:\n\t\treturn schema.Builtin_FLOAT32\n\tcase schemav2.Float64:\n\t\treturn schema.Builtin_FLOAT64\n\tcase schemav2.String:\n\t\treturn schema.Builtin_STRING\n\tcase schemav2.Bytes:\n\t\treturn schema.Builtin_BYTES\n\n\tcase schemav2.Time:\n\t\treturn schema.Builtin_TIME\n\tcase schemav2.UUID:\n\t\treturn schema.Builtin_UUID\n\tcase schemav2.JSON:\n\t\treturn schema.Builtin_JSON\n\tcase schemav2.UserID:\n\t\treturn schema.Builtin_USER_ID\n\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown builtin type %v\", typ.Kind))\n\t}\n}\n\nfunc (b *builder) schemaType(typ schemav2.Type) *schema.Type {\n\tswitch typ := typ.(type) {\n\tcase nil:\n\t\treturn nil\n\n\tcase schemav2.BuiltinType:\n\t\treturn &schema.Type{Typ: &schema.Type_Builtin{\n\t\t\tBuiltin: b.builtinType(typ),\n\t\t}}\n\n\tcase schemav2.NamedType:\n\t\tif typ.DeclInfo.File.Pkg.ImportPath == \"encore.dev/config\" {\n\t\t\treturn b.configValue(typ)\n\t\t}\n\t\treturn &schema.Type{Typ: &schema.Type_Named{\n\t\t\tNamed: &schema.Named{\n\t\t\t\tId:            b.decl(typ.Decl()),\n\t\t\t\tTypeArguments: b.schemaTypes(typ.TypeArgs...),\n\t\t\t},\n\t\t}}\n\n\tcase schemav2.StructType:\n\t\tvar fields []*schema.Field\n\t\tfor _, f := range typ.Fields {\n\t\t\tif f.IsAnonymous() {\n\t\t\t\tcontinue // not supported by meta\n\t\t\t}\n\t\t\tfield := b.structField(f)\n\t\t\tif f.IsExported() { // to match legacy meta behavior\n\t\t\t\tfields = append(fields, field)\n\t\t\t}\n\t\t}\n\n\t\treturn &schema.Type{Typ: &schema.Type_Struct{\n\t\t\tStruct: &schema.Struct{\n\t\t\t\tFields: fields,\n\t\t\t},\n\t\t}}\n\n\tcase schemav2.MapType:\n\t\treturn &schema.Type{Typ: &schema.Type_Map{\n\t\t\tMap: &schema.Map{\n\t\t\t\tKey:   b.schemaType(typ.Key),\n\t\t\t\tValue: b.schemaType(typ.Value),\n\t\t\t},\n\t\t}}\n\n\tcase schemav2.ListType:\n\t\t// An array of bytes (like [16]byte for a UUID) is not represented\n\t\t// as the builtin BYTES in the schemav2 parser, but the legacy metadata does.\n\t\tif typ.Len >= 0 && schemautil.IsBuiltinKind(typ.Elem, schemav2.Uint8) {\n\t\t\treturn &schema.Type{Typ: &schema.Type_Builtin{\n\t\t\t\tBuiltin: schema.Builtin_BYTES,\n\t\t\t}}\n\t\t}\n\n\t\treturn &schema.Type{Typ: &schema.Type_List{\n\t\t\tList: &schema.List{\n\t\t\t\tElem: b.schemaType(typ.Elem),\n\t\t\t},\n\t\t}}\n\n\tcase schemav2.PointerType:\n\t\treturn &schema.Type{Typ: &schema.Type_Pointer{\n\t\t\tPointer: &schema.Pointer{\n\t\t\t\tBase: b.schemaType(typ.Elem),\n\t\t\t},\n\t\t}}\n\n\tcase schemav2.OptionType:\n\t\treturn &schema.Type{Typ: &schema.Type_Option{\n\t\t\tOption: &schema.Option{\n\t\t\t\tValue: b.schemaType(typ.Value),\n\t\t\t},\n\t\t}}\n\n\tcase schemav2.TypeParamRefType:\n\t\treturn &schema.Type{Typ: &schema.Type_TypeParameter{\n\t\t\tTypeParameter: &schema.TypeParameterRef{\n\t\t\t\tDeclId:   b.decl(typ.Decl),\n\t\t\t\tParamIdx: uint32(typ.Index),\n\t\t\t},\n\t\t}}\n\n\tdefault:\n\t\tb.errs.Addf(typ.ASTExpr().Pos(), \"unsupported schema type %T\", typ)\n\t}\n\n\treturn nil\n}\n\n// schemaTypeUnwrapPointer returns the schema type for the given type,\n// but unwraps the initial pointer if it is one.\n// This is used for backwards compatibility with the legacy metadata,\n// where certain types where returned without the leading pointer\n// (most usages of *est.Param).\nfunc (b *builder) schemaTypeUnwrapPointer(typ schemav2.Type) *schema.Type {\n\tif ptr, ok := typ.(schemav2.PointerType); ok {\n\t\treturn b.schemaType(ptr.Elem)\n\t}\n\treturn b.schemaType(typ)\n}\n\nfunc (b *builder) structField(f schemav2.StructField) *schema.Field {\n\tfield := &schema.Field{\n\t\tTyp:             b.schemaType(f.Type),\n\t\tName:            f.Name.MustGet(),\n\t\tDoc:             f.Doc,\n\t\tJsonName:        \"\",\n\t\tOptional:        false,\n\t\tQueryStringName: \"\",\n\t\tRawTag:          f.Tag.String(),\n\t\tTags:            nil,\n\t}\n\n\tfor _, tag := range f.Tag.Tags() {\n\t\tfield.Tags = append(field.Tags, &schema.Tag{\n\t\t\tKey:     tag.Key,\n\t\t\tName:    tag.Name,\n\t\t\tOptions: tag.Options,\n\t\t})\n\t}\n\n\t// Process encore tags\n\tif enc, _ := f.Tag.Get(\"encore\"); enc != nil {\n\t\tops := append([]string{enc.Name}, enc.Options...)\n\t\tfor _, o := range ops {\n\t\t\tswitch o {\n\t\t\tcase \"optional\":\n\t\t\t\tfield.Optional = true\n\t\t\tcase \"httpstatus\":\n\t\t\t\t// Set WireSpec for HttpStatus fields\n\t\t\t\tfield.Wire = &schema.WireSpec{\n\t\t\t\t\tLocation: &schema.WireSpec_HttpStatus_{\n\t\t\t\t\t\tHttpStatus: &schema.WireSpec_HttpStatus{},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Treat option types as optional.\n\tif schemautil.IsOption(f.Type) {\n\t\tfield.Optional = true\n\t}\n\n\t// Set WireSpec for header fields\n\tif header, _ := f.Tag.Get(\"header\"); header != nil {\n\t\theaderSpec := &schema.WireSpec_Header{}\n\t\tif header.Name != \"\" {\n\t\t\theaderSpec.Name = &header.Name\n\t\t}\n\t\tfield.Wire = &schema.WireSpec{\n\t\t\tLocation: &schema.WireSpec_Header_{\n\t\t\t\tHeader: headerSpec,\n\t\t\t},\n\t\t}\n\t}\n\n\tgetQueryTag := func() *structtag.Tag {\n\t\tif q, _ := f.Tag.Get(\"query\"); q != nil {\n\t\t\treturn q\n\t\t}\n\t\tif q, _ := f.Tag.Get(\"qs\"); q != nil {\n\t\t\treturn q\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Set WireSpec for query string fields\n\tif query := getQueryTag(); query != nil {\n\t\tquerySpec := &schema.WireSpec_Query{}\n\t\tif query.Name != \"\" {\n\t\t\tquerySpec.Name = &query.Name\n\t\t}\n\t\tfield.Wire = &schema.WireSpec{\n\t\t\tLocation: &schema.WireSpec_Query_{\n\t\t\t\tQuery: querySpec,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Set WireSpec for cookie fields\n\tif cookie, _ := f.Tag.Get(\"cookie\"); cookie != nil {\n\t\tcookieSpec := &schema.WireSpec_Cookie{}\n\t\tif cookie.Name != \"\" {\n\t\t\tcookieSpec.Name = &cookie.Name\n\t\t}\n\t\tfield.Wire = &schema.WireSpec{\n\t\t\tLocation: &schema.WireSpec_Cookie_{\n\t\t\t\tCookie: cookieSpec,\n\t\t\t},\n\t\t}\n\t}\n\n\tif js, _ := f.Tag.Get(\"json\"); js != nil {\n\t\tif v := js.Name; v != \"\" {\n\t\t\tfield.JsonName = v\n\t\t}\n\t}\n\n\tif qs := getQueryTag(); qs != nil {\n\t\tif v := qs.Name; v != \"\" {\n\t\t\tfield.QueryStringName = v\n\t\t}\n\t}\n\tif field.QueryStringName == \"\" {\n\t\tfield.QueryStringName = idents.Convert(field.Name, idents.SnakeCase)\n\t}\n\n\treturn field\n}\n\nfunc (b *builder) configValue(typ schemav2.NamedType) *schema.Type {\n\tswitch typ.DeclInfo.Name {\n\tcase \"Value\", \"Values\":\n\t\tisList := typ.DeclInfo.Name == \"Values\"\n\t\telem := b.schemaType(typ.TypeArgs[0])\n\n\t\tif isList {\n\t\t\telem = &schema.Type{Typ: &schema.Type_List{\n\t\t\t\tList: &schema.List{\n\t\t\t\t\tElem: elem,\n\t\t\t\t},\n\t\t\t}}\n\t\t}\n\n\t\treturn &schema.Type{Typ: &schema.Type_Config{\n\t\t\tConfig: &schema.ConfigValue{\n\t\t\t\tElem:         elem,\n\t\t\t\tIsValuesList: isList,\n\t\t\t},\n\t\t}}\n\n\tdefault:\n\t\t// Should be a named config type, like \"type String = Value[string]\".\n\t\tif named, ok := typ.Decl().Type.(schemav2.NamedType); ok {\n\t\t\treturn b.configValue(named)\n\t\t} else {\n\t\t\tb.errs.Addf(typ.ASTExpr().Pos(), \"unsupported config type %q\", typ.DeclInfo.Name)\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (b *builder) schemaTypes(typs ...schemav2.Type) []*schema.Type {\n\treturn fns.Map(typs, b.schemaType)\n}\n\nfunc (b *builder) decl(decl schemav2.Decl) uint32 {\n\ttypeDecl, ok := decl.(*schemav2.TypeDecl)\n\tif !ok {\n\t\tb.errs.Fatalf(decl.ASTNode().Pos(), \"cannot add declaration %q of type %T\", decl.String(), decl)\n\t\treturn 0 // unreachable\n\t}\n\n\tpkgName, ok := typeDecl.PkgName().Get()\n\tif !ok {\n\t\tb.errs.Fatalf(decl.ASTNode().Pos(), \"cannot add declaration %q that's not at package level\", decl.String())\n\t\treturn 0 // unreachable\n\t}\n\n\t// Do we already have this declaration added?\n\tfile := decl.DeclaredIn()\n\tpkg := file.Pkg\n\tk := declKey{pkgPath: pkg.ImportPath, pkgName: pkgName}\n\tif n, ok := b.decls[k]; ok {\n\t\treturn n\n\t}\n\n\t// Otherwise add it.\n\tdeclIdx := uint32(len(b.md.Decls))\n\tb.decls[k] = declIdx\n\n\ttypeParams := fns.Map(typeDecl.TypeParams, func(p schemav2.DeclTypeParam) *schema.TypeParameter {\n\t\treturn &schema.TypeParameter{Name: p.Name}\n\t})\n\n\t// Allocate the object and add it to the list\n\t// without the underlying type. We'll add the\n\t// underlying type afterwards to properly handle\n\t// recursive and mutually recursive types.\n\td := &schema.Decl{\n\t\tId:         declIdx,\n\t\tName:       pkgName,\n\t\tType:       nil, // computed below\n\t\tTypeParams: typeParams,\n\t\tDoc:        typeDecl.Info.Doc,\n\t\tLoc:        b.schemaLoc(file, decl.ASTNode()),\n\t}\n\tb.md.Decls = append(b.md.Decls, d)\n\n\td.Type = b.schemaType(typeDecl.Type)\n\n\treturn declIdx\n}\n\nfunc (b *builder) schemaLoc(f *pkginfo.File, node ast.Node) *schema.Loc {\n\ttokenFile := f.Token()\n\tsPos, ePos := tokenFile.Position(node.Pos()), tokenFile.Position(node.Pos())\n\treturn &schema.Loc{\n\t\tPkgName:      f.Pkg.Name,\n\t\tPkgPath:      string(f.Pkg.ImportPath),\n\t\tFilename:     f.Name,\n\t\tStartPos:     int32(tokenFile.Offset(node.Pos())),\n\t\tEndPos:       int32(tokenFile.Offset(node.End())),\n\t\tSrcLineStart: int32(sPos.Line),\n\t\tSrcLineEnd:   int32(ePos.Line),\n\t\tSrcColStart:  int32(sPos.Column),\n\t\tSrcColEnd:    int32(ePos.Column),\n\t}\n}\n\ntype declKey struct {\n\tpkgPath paths.Pkg\n\tpkgName string\n}\n\nfunc (b *builder) declKey(pkgPath paths.Pkg, pkgName string) uint32 {\n\tk := declKey{pkgPath: pkgPath, pkgName: pkgName}\n\tif n, ok := b.decls[k]; ok {\n\t\treturn n\n\t}\n\n\tn := uint32(len(b.decls))\n\tb.decls[k] = n\n\treturn n\n}\n\nfunc (b *builder) typeDeclRef(typ *schemav2.TypeDeclRef) *schema.Type {\n\treturn b.schemaType(typ.ToType())\n}\n\nfunc (b *builder) typeDeclRefUnwrapPointer(typ *schemav2.TypeDeclRef) *schema.Type {\n\treturn b.schemaTypeUnwrapPointer(typ.ToType())\n}\n"
  },
  {
    "path": "v2/app/legacymeta/selector_lookup.go",
    "content": "package legacymeta\n\nimport (\n\t\"fmt\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/selector\"\n)\n\n// selectorLookup is a helper cache for looking up services and RPC's by selector\ntype selectorLookup struct {\n\tservices  map[selector.Key]map[*app.Service]struct{}\n\tendpoints map[selector.Key]map[*api.Endpoint]struct{}\n}\n\n// computeSelectorLookup creates a selector lookup from this application\nfunc computeSelectorLookup(appDesc *app.Desc) *selectorLookup {\n\ts := &selectorLookup{\n\t\tservices:  make(map[selector.Key]map[*app.Service]struct{}),\n\t\tendpoints: make(map[selector.Key]map[*api.Endpoint]struct{}),\n\t}\n\n\t// Record all RPCs\n\tfor _, ep := range parser.Resources[*api.Endpoint](appDesc.Parse) {\n\t\tsvc, ok := appDesc.ServiceForPath(ep.File.FSPath)\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"no service found for endpoint %s.%s\", ep.File.Pkg.Name, ep.Name))\n\t\t}\n\n\t\t// Track against all\n\t\ts.recordEndpoint(selector.Selector{Type: selector.All}, ep, svc)\n\n\t\t// Track against any defined tags\n\t\tep.Tags.ForEach(func(sel selector.Selector) {\n\t\t\ts.recordEndpoint(sel, ep, svc)\n\t\t})\n\t}\n\n\treturn s\n}\n\n// recordRPC tracks both the RPC for the selector, but also the service the RPC is in\nfunc (sm *selectorLookup) recordEndpoint(s selector.Selector, ep *api.Endpoint, svc *app.Service) {\n\tkey := s.Key()\n\tif sm.endpoints[key] == nil {\n\t\tsm.endpoints[key] = make(map[*api.Endpoint]struct{})\n\t}\n\tsm.endpoints[key][ep] = struct{}{}\n\n\tif sm.services[key] == nil {\n\t\tsm.services[key] = make(map[*app.Service]struct{})\n\t}\n\tsm.services[key][svc] = struct{}{}\n}\n\n// GetEndpoints returns all the rpcs which match any of the given selectors\nfunc (sm *selectorLookup) GetEndpoints(targets selector.Set) (rtn []*api.Endpoint) {\n\ttargets.ForEach(func(s selector.Selector) {\n\t\tif rpcs, found := sm.endpoints[s.Key()]; found {\n\t\t\tfor rpc := range rpcs {\n\t\t\t\trtn = append(rtn, rpc)\n\t\t\t}\n\t\t}\n\t})\n\treturn\n}\n\n// GetServices returns all services which match any of the given selectors\nfunc (sm *selectorLookup) GetServices(targets selector.Set) (rtn []*app.Service) {\n\ttargets.ForEach(func(s selector.Selector) {\n\t\tif services, found := sm.services[s.Key()]; found {\n\t\t\tfor svc := range services {\n\t\t\t\trtn = append(rtn, svc)\n\t\t\t}\n\t\t}\n\t})\n\treturn\n}\n"
  },
  {
    "path": "v2/app/legacymeta/trace_nodes.go",
    "content": "package legacymeta\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"path\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n)\n\nfunc newTraceNodes(b *builder) *TraceNodes {\n\treturn &TraceNodes{\n\t\tb:           b,\n\t\tnodes:       make(map[paths.Pkg][]*meta.TraceNode),\n\t\tmiddlewares: make(map[*middleware.Middleware]*meta.TraceNode),\n\t\tsubs:        make(map[*pubsub.Subscription]*meta.TraceNode),\n\t\tsvcStructs:  make(map[*servicestruct.ServiceStruct]*meta.TraceNode),\n\t\tendpoints:   make(map[*api.Endpoint]*meta.TraceNode),\n\t}\n}\n\n// TraceNodes describes the allocated [meta.TraceNode] in the app.\n// The public methods are safe to use even on a nil TraceNodes.\ntype TraceNodes struct {\n\tb     *builder\n\tid    int32\n\tnodes map[paths.Pkg][]*meta.TraceNode\n\n\tauthHandler *meta.TraceNode\n\tmiddlewares map[*middleware.Middleware]*meta.TraceNode\n\tsubs        map[*pubsub.Subscription]*meta.TraceNode\n\tsvcStructs  map[*servicestruct.ServiceStruct]*meta.TraceNode\n\tendpoints   map[*api.Endpoint]*meta.TraceNode\n}\n\nfunc (n *TraceNodes) AuthHandler() uint32 {\n\tif n == nil {\n\t\treturn 0\n\t}\n\treturn nodeID(n.authHandler)\n}\n\nfunc (n *TraceNodes) Middleware(mw *middleware.Middleware) uint32 {\n\tif n == nil {\n\t\treturn 0\n\t}\n\treturn nodeID(n.middlewares[mw])\n}\n\nfunc (n *TraceNodes) Sub(sub *pubsub.Subscription) uint32 {\n\tif n == nil {\n\t\treturn 0\n\t}\n\treturn nodeID(n.subs[sub])\n}\n\nfunc (n *TraceNodes) SvcStruct(svcStruct *servicestruct.ServiceStruct) uint32 {\n\tif n == nil {\n\t\treturn 0\n\t}\n\treturn nodeID(n.svcStructs[svcStruct])\n}\n\nfunc (n *TraceNodes) Endpoint(ep *api.Endpoint) uint32 {\n\tif n == nil {\n\t\treturn 0\n\t}\n\treturn nodeID(n.endpoints[ep])\n}\n\nfunc (n *TraceNodes) addAuthHandler(ah *authhandler.AuthHandler, svcName string) {\n\ttraceNode, context := n.alloc(ah.Decl.File, ah.Decl.AST)\n\ttraceNode.Context = &meta.TraceNode_AuthHandlerDef{\n\t\tAuthHandlerDef: &meta.AuthHandlerDefNode{\n\t\t\tServiceName: svcName,\n\t\t\tName:        ah.Decl.Name,\n\t\t\tContext:     string(context),\n\t\t},\n\t}\n\tn.authHandler = traceNode\n}\n\nfunc (n *TraceNodes) addServiceStruct(s *servicestruct.ServiceStruct, svcName string) {\n\ttraceNode, context := n.alloc(s.Decl.File, s.Decl.AST)\n\ttraceNode.Context = &meta.TraceNode_ServiceInit{\n\t\tServiceInit: &meta.ServiceInitNode{\n\t\t\tServiceName:   svcName,\n\t\t\tSetupFuncName: option.Map(s.Init, func(init *schema.FuncDecl) string { return init.Name }).GetOrElse(\"\"),\n\t\t\tContext:       string(context),\n\t\t},\n\t}\n\tn.svcStructs[s] = traceNode\n}\n\nfunc (n *TraceNodes) addSub(sub *pubsub.Subscription, svcName, topicName string) {\n\ttraceNode, context := n.alloc(sub.File, sub.AST)\n\ttraceNode.Context = &meta.TraceNode_PubsubSubscriber{\n\t\tPubsubSubscriber: &meta.PubSubSubscriberNode{\n\t\t\tTopicName:      topicName,\n\t\t\tSubscriberName: sub.Name,\n\t\t\tServiceName:    svcName,\n\t\t\tContext:        string(context),\n\t\t},\n\t}\n\tn.subs[sub] = traceNode\n}\n\nfunc (n *TraceNodes) addMiddleware(mw *middleware.Middleware) {\n\trelPath := n.b.relPath(mw.File.Pkg.ImportPath)\n\ttraceNode, context := n.alloc(mw.Decl.File, mw.Decl.AST)\n\ttraceNode.Context = &meta.TraceNode_MiddlewareDef{\n\t\tMiddlewareDef: &meta.MiddlewareDefNode{\n\t\t\tPkgRelPath: relPath,\n\t\t\tName:       mw.Decl.Name,\n\t\t\tContext:    string(context),\n\t\t\tTarget:     mw.Target.ToProto(),\n\t\t},\n\t}\n\tn.middlewares[mw] = traceNode\n}\n\nfunc (n *TraceNodes) addEndpoint(ep *api.Endpoint, svcName string) {\n\ttraceNode, context := n.alloc(ep.Decl.File, ep.Decl.AST)\n\ttraceNode.Context = &meta.TraceNode_RpcDef{\n\t\tRpcDef: &meta.RPCDefNode{\n\t\t\tServiceName: svcName,\n\t\t\tRpcName:     ep.Name,\n\t\t\tContext:     string(context),\n\t\t},\n\t}\n\tn.endpoints[ep] = traceNode\n}\n\n// alloc allocates a trace node.\nfunc (n *TraceNodes) alloc(file *pkginfo.File, node ast.Node) (traceNode *meta.TraceNode, context []byte) {\n\tpkgPath := file.Pkg.ImportPath\n\tfileRelPath := path.Join(n.b.relPath(pkgPath), file.Name)\n\n\ttokenFile := file.Token()\n\tstartIdx := tokenFile.Offset(node.Pos())\n\tendIdx := tokenFile.Offset(node.End())\n\tcontext = file.Contents()[startIdx:endIdx]\n\n\tn.id++\n\n\tswitch node := node.(type) {\n\tcase *ast.CallExpr:\n\t\ttraceNode = &meta.TraceNode{\n\t\t\tId:       n.id,\n\t\t\tFilepath: fileRelPath,\n\t\t\tStartPos: int32(tokenFile.Offset(node.Lparen)),\n\t\t\tEndPos:   int32(tokenFile.Offset(node.Rparen)),\n\t\t}\n\tcase *ast.FuncDecl:\n\t\ttraceNode = &meta.TraceNode{\n\t\t\tId:       n.id,\n\t\t\tFilepath: fileRelPath,\n\t\t\tStartPos: int32(tokenFile.Offset(node.Type.Pos())),\n\t\t\tEndPos:   int32(tokenFile.Offset(node.Type.End())),\n\t\t}\n\tcase *ast.TypeSpec:\n\t\ttraceNode = &meta.TraceNode{\n\t\t\tId:       n.id,\n\t\t\tFilepath: fileRelPath,\n\t\t\tStartPos: int32(tokenFile.Offset(node.Pos())),\n\t\t\tEndPos:   int32(tokenFile.Offset(node.End())),\n\t\t}\n\tcase *ast.SelectorExpr:\n\t\ttraceNode = &meta.TraceNode{\n\t\t\tId:       n.id,\n\t\t\tFilepath: fileRelPath,\n\t\t\tStartPos: int32(tokenFile.Offset(node.Pos())),\n\t\t\tEndPos:   int32(tokenFile.Offset(node.End())),\n\t\t}\n\tcase *ast.Ident:\n\t\ttraceNode = &meta.TraceNode{\n\t\t\tId:       n.id,\n\t\t\tFilepath: fileRelPath,\n\t\t\tStartPos: int32(tokenFile.Offset(node.Pos())),\n\t\t\tEndPos:   int32(tokenFile.Offset(node.End())),\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled trace expression node %T\", node))\n\t}\n\tn.nodes[pkgPath] = append(n.nodes[pkgPath], traceNode)\n\n\tstart := tokenFile.Pos(int(traceNode.StartPos))\n\tend := tokenFile.Pos(int(traceNode.EndPos))\n\tsPos, ePos := tokenFile.Position(start), tokenFile.Position(end)\n\ttraceNode.SrcLineStart = int32(sPos.Line)\n\ttraceNode.SrcLineEnd = int32(ePos.Line)\n\ttraceNode.SrcColStart = int32(sPos.Column)\n\ttraceNode.SrcColEnd = int32(ePos.Column)\n\n\treturn\n}\n\nfunc (n *TraceNodes) forPkg(pkgPath paths.Pkg) []*meta.TraceNode {\n\treturn n.nodes[pkgPath]\n}\n\n// nodeID returns the trace node id for the given node.\n// If node is nil it returns 0.\nfunc nodeID(node *meta.TraceNode) uint32 {\n\tif node == nil {\n\t\treturn 0\n\t}\n\treturn uint32(node.Id)\n}\n"
  },
  {
    "path": "v2/app/resource_usage.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\n// locateResourceBinds finds all resource binds and assigns them to the appropriate service.\nfunc (d *Desc) locateResourceBinds(result *parser.Result) {\n\tallBinds := result.AllBinds()\n\n\tfor _, bind := range allBinds {\n\t\tres := result.ResourceForBind(bind)\n\t\tsvc, found := d.ServiceForPath(bind.Package().FSPath)\n\t\tif found {\n\t\t\t// If we found the service, then we know the resource is used within the service.\n\t\t\tsvc.ResourceBinds[res] = append(svc.ResourceBinds[res], bind)\n\t\t}\n\t}\n}\n\n// locateResourceUsage finds all resource usages and assigns them to the appropriate service.\n// If a resource is used outside of a service, it is assigned to the top-level app description.\nfunc (d *Desc) locateResourceUsage(result *parser.Result) {\n\tallUsages := result.AllUsages()\n\n\tfor _, use := range allUsages {\n\t\tres := result.ResourceForBind(use.ResourceBind())\n\n\t\tsvc, found := d.ServiceForPath(use.DeclaredIn().Pkg.FSPath)\n\t\tif found {\n\t\t\t// If we found the service, then we know the resource is used within the service.\n\t\t\tresUsages, found := svc.ResourceUsage[res]\n\t\t\tif !found {\n\t\t\t\tresUsages = make([]usage.Usage, 0, 1)\n\t\t\t\tsvc.ResourceUsage[res] = resUsages\n\t\t\t}\n\n\t\t\tsvc.ResourceUsage[res] = append(resUsages, use)\n\t\t} else {\n\t\t\t// Otherwise, the resource is used outside of a service.\n\t\t\tresUsages, found := d.ResourceUsageOutsideServices[res]\n\t\t\tif !found {\n\t\t\t\tresUsages = make([]usage.Usage, 0, 1)\n\t\t\t\td.ResourceUsageOutsideServices[res] = resUsages\n\t\t\t}\n\n\t\t\td.ResourceUsageOutsideServices[res] = append(resUsages, use)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/service.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\n// Service describes an Encore service.\ntype Service struct {\n\t// Name is the name of the service.\n\tName string\n\n\t// Num is the 1-based service number in the application.\n\tNum int\n\n\t// FSRoot is the root directory of the service.\n\tFSRoot paths.FS\n\n\t// Framework contains API Framework-specific data for this service.\n\tFramework option.Option[*apiframework.ServiceDesc]\n\n\t// ResourceBinds describes the infra resources bound within the service.\n\tResourceBinds map[resource.Resource][]resource.Bind\n\n\t// ResourceUsage describes the infra resources the service accesses and how.\n\tResourceUsage map[resource.Resource][]usage.Usage\n}\n\n// ContainsPackage reports whether the service contains the given package.\nfunc (s *Service) ContainsPackage(pkg *pkginfo.Package) bool {\n\treturn s.FSRoot.HasPrefix(pkg.FSPath)\n}\n"
  },
  {
    "path": "v2/app/service_discovery.go",
    "content": "package app\n\nimport (\n\t\"cmp\"\n\t\"slices\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\n// discoverServices discovers the services in the whole application.\n//\n// Services are defined as a collection of packages which are considered\n// to form a single logical service, which can be deployed and scaled as an\n// atomic unit.\n//\n// Services are defined by a root filesystem directory, with everything under\n// that directory considered part of the service.\n//\n// This means services cannot be nested, and a service cannot be split across\n// multiple filesystem directories (such as in separate git repositories).\n//\n// It operates in three phases:\n//\n// 1. Find any API Framework service structs and mark those packages as services (service structs are always the root of a service)\n// 2. Find any API Framework API's not currently in a service and mark those packages as services\n// 3. Find any PubSub subscribers not currently in a service and mark those packages as services\n//\n// This does not setup the service framework data.\n//\n// The returned slice is sorted by service name.\nfunc discoverServices(pc *parsectx.Context, result *parser.Result) []*Service {\n\tdefer pc.Trace(\"app.discoverServices\").Done()\n\n\tsd := &serviceDiscovery{\n\t\tservices:   make(map[paths.FS]*Service),\n\t\tstrongRoot: make(map[paths.FS]struct{}),\n\t}\n\n\tfor _, r := range result.Resources() {\n\t\tswitch r := r.(type) {\n\t\tcase *servicestruct.ServiceStruct:\n\t\t\tsd.possibleServiceRoot(r.Decl.File.Pkg, true)\n\t\tcase *api.Endpoint:\n\t\t\tsd.possibleServiceRoot(r.Decl.File.Pkg, false)\n\t\tcase *pubsub.Subscription:\n\t\t\tsd.possibleServiceRoot(r.File.Pkg, false)\n\t\tcase *authhandler.AuthHandler:\n\t\t\tsd.possibleServiceRoot(r.Decl.File.Pkg, false)\n\t\t}\n\t}\n\n\t// We then make an ordered slice of the services discovered\n\tservices := make([]*Service, 0, len(sd.services))\n\tfor _, s := range sd.services {\n\t\tservices = append(services, s)\n\t}\n\n\t// Note: we sort by the FSRoot because we use that for binary searches in [ServiceForPath]\n\tslices.SortStableFunc(services, func(a, b *Service) int {\n\t\treturn cmp.Compare(a.FSRoot.ToIO(), b.FSRoot.ToIO())\n\t})\n\n\t// Finally, let's validate our services so we can report errors\n\t// FIXME: we should put this back in as otherwise we need to specify\n\t//        the expected behaviour for an Encore app with no services!\n\t// if len(services) == 0 {\n\t//\tpc.Errs.AddStd(srcerrors.NoServicesFound())\n\t// }\n\n\t// Finally, let's validate our services\n\tfor i, s := range services {\n\t\tfor j := i + 1; j < len(services); j++ {\n\t\t\t// we need to check both directions of nesting as we only loop over one direction\n\t\t\tif s.FSRoot.HasPrefix(services[j].FSRoot) {\n\t\t\t\tpc.Errs.Add(errServiceContainedWitinAnother(s.Name, services[j].Name))\n\t\t\t} else if services[j].FSRoot.HasPrefix(s.FSRoot) {\n\t\t\t\tpc.Errs.Add(errServiceContainedWitinAnother(services[j].Name, s.Name))\n\t\t\t}\n\n\t\t\tif s.Name == services[j].Name {\n\t\t\t\tpc.Errs.Add(errDuplicateServiceNames(s.Name).InFile(s.FSRoot.ToIO()).InFile(services[j].FSRoot.ToIO()))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Sort the services by name and assign the service numbers.\n\tslices.SortFunc(services, func(a, b *Service) int {\n\t\treturn cmp.Compare(a.Name, b.Name)\n\t})\n\tfor idx, svc := range services {\n\t\tsvc.Num = idx + 1\n\t}\n\n\treturn services\n}\n\ntype serviceDiscovery struct {\n\terrs     *perr.List            // errs is the error list to add errors to.\n\tservices map[paths.FS]*Service // services maps the root folder to a service to the service\n\n\t// strongRoot marks a folder as a strong root, meaning we won't allow it to be merged\n\t// with a service in a parent folder.\n\tstrongRoot map[paths.FS]struct{}\n}\n\n// possibleServiceRoot marks a folder as a possible service if it is not already marked as being part of a service.\n//\n// If strong is true, the root path always becomes a new service, event if it is within an existing service.\n//\n// If any existing services are descendants of the new service, they are removed from the list, unless\n// the previous service was marked as a strong root.\nfunc (sd *serviceDiscovery) possibleServiceRoot(pkg *pkginfo.Package, strong bool) {\n\tname := pkg.Name\n\troot := pkg.FSPath\n\n\tif strong {\n\t\t// Always mark the root as a strong root, even if it is already marked as a service.\n\t\tsd.strongRoot[root] = struct{}{}\n\t}\n\n\t// If the service is already marked, we don't need to do anything.\n\tif _, ok := sd.services[root]; ok {\n\t\treturn\n\t}\n\n\t// Loop over the existing services and remove any that are descendants of this root\n\t// but also look for any existing services which are ancestors of this root.\n\tfor existingRoot := range sd.services {\n\t\tswitch {\n\t\t// If the existing service is a descendant of the new service, we can remove it.\n\t\tcase existingRoot.HasPrefix(root):\n\t\t\t// If the existing service is a strong root, we can't merge it with this service.\n\t\t\tif _, ok := sd.strongRoot[existingRoot]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// If the existing service is a descendant of the new service, we can remove it.\n\t\t\tdelete(sd.services, existingRoot)\n\n\t\t// If the new service is a descendant of an existing service, we're done\n\t\t// because we don't allow nested services so this directory is part of the existing service.\n\t\tcase root.HasPrefix(existingRoot) && !strong:\n\t\t\treturn\n\t\t}\n\t}\n\n\t// If we get here, the new service is not a descendant of any existing services.\n\t// We can add it to the list of services.\n\tsd.services[root] = &Service{\n\t\tName:          name,\n\t\tFSRoot:        root,\n\t\tResourceBinds: make(map[resource.Resource][]resource.Bind),\n\t\tResourceUsage: make(map[resource.Resource][]usage.Usage),\n\t}\n}\n"
  },
  {
    "path": "v2/app/service_discovery_test.go",
    "content": "package app\n\nimport (\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n)\n\nfunc Test_discoverServices(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\ttxtar        string\n\t\twantServices []string\n\t\twantErrors   []string\n\t}{\n\t\t// Happy Path Tests\n\t\t{\n\t\t\tname: \"services defined by APIs\",\n\t\t\ttxtar: `\n-- systemA/svc1/foo.go --\npackage svc1\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- systemA/svc2/bar.go --\npackage svc2\n\nimport \"context\"\n\n//encore:api public\nfunc Bar(ctx context.Context) error { return nil }\n\n-- svc3/bar.go --\npackage svc3\n\nimport \"context\"\n\n//encore:api public\nfunc Bar(ctx context.Context) error { return nil }\n`,\n\t\t\twantServices: []string{\"svc1\", \"svc2\", \"svc3\"},\n\t\t},\n\t\t{\n\t\t\tname: \"services defined by service structs\",\n\t\t\ttxtar: `\n-- svc1/foo.go --\npackage svc1\n\nimport \"context\"\n\n//encore:service\ntype Foo struct{}\n\nfunc initFoo() (*Foo, error) { return nil, nil }\n`,\n\t\t\twantServices: []string{\"svc1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"services defined by pubsub subscriptions\",\n\t\t\ttxtar: `\n-- svc1/foo.go --\npackage svc1\n\nimport (\n \t\"context\"\n\n\t\"encore.dev/pubsub\"\n)\n\ntype Msg struct{}\n\nvar T = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\nvar _ = pubsub.NewSubscription[Msg](T, \"sub\", pubsub.SubscriptionConfig[Msg]{\n\tHandler: func(ctx context.Context, msg Msg) error { return nil },\n})\n`,\n\t\t\twantServices: []string{\"svc1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"services defined by auth handler\",\n\t\t\ttxtar: `\n-- svc1/foo.go --\npackage svc1\n\nimport (\n \t\"context\"\n\n\t\"encore.dev/beta/auth\"\n)\n\n//encore:authhandler\nfunc MyAuthHandler(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n`,\n\t\t\twantServices: []string{\"svc1\"},\n\t\t},\n\t\t{\n\t\t\tname: \"services with nested packages with API's and pubsub subscriptions\",\n\t\t\ttxtar: `\n-- svc1/foo.go --\npackage svc1\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- svc1/events/bar.go --\npackage events\n\nimport (\n\t\"context\"\n\t\n\t\"encore.dev/pubsub\"\n)\n\ntype Msg struct{}\n\nvar T = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\nvar _ = pubsub.NewSubscription[Msg](T, \"sub\", pubsub.SubscriptionConfig[Msg]{\n\tHandler: func(ctx context.Context, msg Msg) error { return nil },\n})\n\n-- svc1/apis/baz.go --\npackage apis\n\nimport \"context\"\n\n//encore:api public\nfunc Baz(ctx context.Context) error { return nil }\n`,\n\t\t\twantServices: []string{\"svc1\"},\n\t\t},\n\n\t\t// Error Tests\n\t\t//\t\t{\n\t\t//\t\t\tname: \"error if no services\",\n\t\t//\t\t\ttxtar: `\n\t\t//-- svc1/foo.go --\n\t\t//package svc1\n\t\t//\n\t\t//type Foo struct{}\n\t\t//`,\n\t\t//\t\t\twantErrors: []string{\"No services were found in the application\"},\n\t\t//\t\t},\n\t\t{\n\t\t\t// Note: this test is also testing that the Go package name is being used\n\t\t\t// by the API framework - not the folder names/path\n\t\t\tname: \"error on duplicate service names\",\n\t\t\ttxtar: `\n-- systemA/svc1/foo.go --\npackage svc\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- systemB/svc2/bar.go --\npackage svc\n\nimport \"context\"\n\n//encore:api public\nfunc Bar(ctx context.Context) error { return nil }\n`,\n\t\t\twantErrors: []string{\"Two services were found with the same name \\\"svc\\\", services must have unique names\"},\n\t\t},\n\t\t{\n\t\t\tname: \"error if services declared in each other\",\n\t\t\ttxtar: `\n-- svc1/foo.go --\npackage svc1\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- svc1/svc2/bar.go --\npackage svc2\n\n//encore:service\ntype Bar struct{}\n\nfunc initBar() (*Bar, error) { return nil, nil }\n`,\n\t\t\twantErrors: []string{\"The service svc2 was found within the service svc1. Encore does not allow services to be nested\"},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := qt.New(t)\n\n\t\t\tctx, result := Parse(c, tt.txtar)\n\n\t\t\tservices := discoverServices(ctx.Context, result)\n\n\t\t\tif len(tt.wantErrors) > 0 {\n\t\t\t\tfor _, s := range services {\n\t\t\t\t\tc.Logf(\"got service: %s (%s)\", s.Name, s.FSRoot)\n\t\t\t\t}\n\t\t\t\tctx.DeferExpectError(tt.wantErrors...)\n\t\t\t} else {\n\t\t\t\tctx.FailTestOnErrors()\n\n\t\t\t\twanted := make(map[string]struct{})\n\t\t\t\tfor _, s := range tt.wantServices {\n\t\t\t\t\twanted[s] = struct{}{}\n\t\t\t\t}\n\n\t\t\t\tfor _, s := range services {\n\t\t\t\t\tif _, ok := wanted[s.Name]; !ok {\n\t\t\t\t\t\tc.Errorf(\"unexpected service %q\", s.Name)\n\t\t\t\t\t}\n\t\t\t\t\tdelete(wanted, s.Name)\n\t\t\t\t}\n\n\t\t\t\tfor s := range wanted {\n\t\t\t\t\tc.Errorf(\"missing service %q\", s)\n\t\t\t\t}\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/app/setup_test.go",
    "content": "package app\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n)\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(testscript.RunMain(m, nil))\n}\n\n// Parse will take the given archiveContent and parse it into a testutil.Context and parser.Result.\n//\n// If the parser errors, the test will immediately fail.\nfunc Parse(c *qt.C, archiveContent string) (*testutil.Context, *parser.Result) {\n\n\tarchive := testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\n\ngo 1.20\n\nrequire encore.dev v1.52.0\n\n` + archiveContent)\n\n\ttc := testutil.NewContext(c, false, archive)\n\ttc.GoModDownload()\n\tp := parser.NewParser(tc.Context)\n\ttc.FailTestOnBailout()\n\tparserResult := p.Parse()\n\n\tif tc.Errs.Len() > 0 {\n\t\tc.Fatalf(\"parsing failed: %v\", tc.Errs.FormatErrors())\n\t}\n\n\treturn tc, parserResult\n}\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_call.txt",
    "content": "! parse\nerr 'cannot reference auth handler svc.MyAuth from another package'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype MyData struct {\n    Name string\n}\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, token string) (auth.UID, *MyData, error) { return \"\", nil, nil }\n\n-- svc2/svc2.go --\npackage svc2\n\nimport (\n    \"context\"\n    \"test/svc\"\n)\n\nfunc Foo() {\n    svc.MyAuth(context.Background(), \"foo\")\n}\n\n-- want: errors --\n\n── Invalid auth handler usage ─────────────────────────────────────────────────────────────[E9999]──\n\nYou can not directly call an auth handler from another package.\n\n    ╭─[ svc2/svc2.go:9:5 ]\n    │\n  7 │\n  8 │ func Foo() {\n  9 │     svc.MyAuth(context.Background(), \"foo\")\n    ⋮     ────┬─────\n    ⋮         ╰─ called here\n 10 │ }\n 11 │\n────╯\n\n    ╭─[ svc/svc.go:13:6 ]\n    │\n 11 │\n 12 │ //encore:authhandler\n 13 │ func MyAuth(ctx context.Context, token string) (auth.UID, *MyData, error) { return \"\", nil, nil }\n    ⋮      ──┬───\n    ⋮        ╰─ auth handler defined here\n 14 │\n────╯\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_data.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype MyData struct {\n    Name string\n}\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, token string) (auth.UID, *MyData, error) { return \"\", nil, nil }"
  },
  {
    "path": "v2/app/testdata/auth_handler_invalid_builtin.txt",
    "content": "! parse\nerr 'second parameter must be of type string, or a pointer to a named struct'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, p int) (auth.UID, error) { return \"\", nil }\n\n-- want: errors --\n\n── Invalid auth handler Signature ─────────────────────────────────────────────────────────[E9999]──\n\nThe second parameter must be a string or a pointer to a named struct.\n\n    ╭─[ svc/svc.go:9:36 ]\n    │\n  7 │\n  8 │ //encore:authhandler\n  9 │ func MyAuth(ctx context.Context, p int) (auth.UID, error) { return \"\", nil }\n    ⋮                                    ───\n 10 │\n────╯\n\nhint: valid signatures are:\n\t- func(ctx context.Context, p *Params) (auth.UID, error)\n\t- func(ctx context.Context, p *Params) (auth.UID, *UserData, error)\n\t- func(ctx context.Context, token string) (auth.UID, error)\n\t- func(ctx context.Context, token string) (auth.UID, *UserData, error)\n\nnote: *Params and *UserData are custom data types you define\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_invalid_field_source.txt",
    "content": "! parse\nerr 'all struct fields used in auth handler parameter Params must'\nerr 'for the field\\(s\\): Foo, Bar'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype Params struct {\n    Authorization string `header:\"Authorization\"`\n    Foo string\n    ClientID string `query:\"client_id\"`\n    Bar int\n}\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, p *Params) (auth.UID, error) { return \"\", nil }\n\n-- want: errors --\n\n── Invalid auth payload ───────────────────────────────────────────────────────────────────[E9999]──\n\nAll fields used within an auth payload must originate from either an HTTP header, a query\nparameter, or a cookie.\n\n    ╭─[ svc/svc.go:10:5 ]\n    │\n  8 │ type Params struct {\n  9 │     Authorization string `header:\"Authorization\"`\n 10 │     Foo string\n    ⋮     ────┬─────\n    ⋮         ╰─ you must specify a \"header\", \"query\", or \"cookie\" tag for this field\n 11 │     ClientID string `query:\"client_id\"`\n 12 │     Bar int\n    ⋮     ───┬───\n    ⋮        ╰─ you must specify a \"header\", \"query\", or \"cookie\" tag for this field\n 13 │ }\n 14 │\n────╯\n\nYou can specify them for each field using the struct tags, for example with `header:\"X-My-Header\"`,\n`query:\"my-query\", or `cookie:\"my-cookie\"`.\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_invalid_named_type.txt",
    "content": "! parse\nerr 'Params must be a struct type'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype Params int\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, p Params) (auth.UID, error) { return \"\", nil }\n\n-- want: errors --\n\n── Invalid auth handler Signature ─────────────────────────────────────────────────────────[E9999]──\n\nThe second parameter must be a string or a pointer to a named struct.\n\n    ╭─[ svc/svc.go:11:36 ]\n    │\n  9 │\n 10 │ //encore:authhandler\n 11 │ func MyAuth(ctx context.Context, p Params) (auth.UID, error) { return \"\", nil }\n    ⋮                                    ──────\n 12 │\n────╯\n\nhint: valid signatures are:\n\t- func(ctx context.Context, p *Params) (auth.UID, error)\n\t- func(ctx context.Context, p *Params) (auth.UID, *UserData, error)\n\t- func(ctx context.Context, token string) (auth.UID, error)\n\t- func(ctx context.Context, token string) (auth.UID, *UserData, error)\n\nnote: *Params and *UserData are custom data types you define\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_multiple.txt",
    "content": "! parse\nerr 'cannot declare multiple auth handlers'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n\n//encore:authhandler\nfunc MyAuth2(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n\n-- want: errors --\n\n── Multiple auth handlers found ───────────────────────────────────────────────────────────[E9999]──\n\nMultiple auth handlers were found in the application. Encore only allows one auth handler to be\ndefined per application.\n\n    ╭─[ svc/svc.go:9:1 ]\n    │\n  7 │\n  8 │ //encore:authhandler\n  9 │ func MyAuth(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n    ⋮  ───────────────────────────────┬────────────────────────────────\n    ⋮                                 ╰─ first auth handler defined here\n 10 │\n 11 │ //encore:authhandler\n 12 │ func MyAuth2(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n    ⋮  ────────────────────────────────┬────────────────────────────────\n    ⋮                                  ╰─ second auth handler defined here\n 13 │\n────╯\n\nhint: valid signatures are:\n\t- func(ctx context.Context, p *Params) (auth.UID, error)\n\t- func(ctx context.Context, p *Params) (auth.UID, *UserData, error)\n\t- func(ctx context.Context, token string) (auth.UID, error)\n\t- func(ctx context.Context, token string) (auth.UID, *UserData, error)\n\nnote: *Params and *UserData are custom data types you define\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_reference.txt",
    "content": "! parse\nerr 'cannot reference auth handler svc.MyAuth from another package'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n\n-- svc2/svc2.go --\npackage svc2\n\nimport (\n    \"context\"\n    \"test/svc\"\n)\n\nfunc Foo() {\n    _ = svc.MyAuth\n}\n\n-- want: errors --\n\n── Invalid auth handler usage ─────────────────────────────────────────────────────────────[E9999]──\n\nAuth handlers can only be called, but not referenced.\n\n    ╭─[ svc2/svc2.go:9:9 ]\n    │\n  7 │\n  8 │ func Foo() {\n  9 │     _ = svc.MyAuth\n    ⋮         ────┬─────\n    ⋮             ╰─ referenced here\n 10 │ }\n 11 │\n────╯\n\n    ╭─[ svc/svc.go:9:6 ]\n    │\n  7 │\n  8 │ //encore:authhandler\n  9 │ func MyAuth(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }\n    ⋮      ──┬───\n    ⋮        ╰─ auth handler defined here\n 10 │\n────╯\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_simple.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, token string) (auth.UID, error) { return \"\", nil }"
  },
  {
    "path": "v2/app/testdata/auth_handler_struct.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype FooParams struct{}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *FooParams) error { return nil }\n\ntype Params struct {\n    Authorization string `header:\"Authorization\"`\n}\n\n//encore:authhandler\nfunc MyAuth(ctx context.Context, p *Params) (auth.UID, error) { return \"\", nil }\n"
  },
  {
    "path": "v2/app/testdata/auth_handler_svc_struct.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype FooParams struct{}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *FooParams) error { return nil }\n\ntype Params struct {\n    Authorization string `header:\"Authorization\"`\n}\n\n//encore:service\ntype Service struct{}\n\n//encore:authhandler\nfunc (s *Service) MyAuth(ctx context.Context, p *Params) (auth.UID, error) { return \"\", nil }\n"
  },
  {
    "path": "v2/app/testdata/cache_cluster_outside_svc.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n    \"test/lib\"\n)\n\ntype Key[T any] struct {\n    Foo T\n}\n\nvar keyspace = cache.NewStringKeyspace[Key[string]](lib.Cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:Foo\",\n})\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- lib/lib.go --\npackage lib\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar Cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n"
  },
  {
    "path": "v2/app/testdata/cache_definition.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar keyspace = cache.NewStringKeyspace[string](cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:key\",\n    DefaultExpiry: cache.ExpireIn(10 * time.Second),\n})\n\n//encore:api public\nfunc Foo(context.Context) error {\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/cache_err_duplicate_cluster.txt",
    "content": "! parse\nerr 'cache cluster names must be unique'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar cluster1 = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\nvar cluster2 = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n-- want: errors --\n\n── Duplicate Cache Cluster ────────────────────────────────────────────────────────────────[E9999]──\n\nCache clusters must have unique names.\n\n    ╭─[ svc/svc.go:9:33 ]\n    │\n  7 │ )\n  8 │\n  9 │ var cluster1 = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n    ⋮                                 ─────────\n 10 │ var cluster2 = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n    ⋮                                 ─────────\n 11 │\n 12 │ //encore:api public\n────╯\n\nI you wish to reuse the same cluster, export the original cache.Cluster object and reuse it here.\n\nFor more information see https://encore.dev/docs/primitives/caching\n"
  },
  {
    "path": "v2/app/testdata/cache_err_duplicate_paths.txt",
    "content": "! parse\nerr 'cache KeyPattern conflict'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\ntype Key[T any] struct {\n    Foo T\n}\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar keyspace1 = cache.NewStringKeyspace[Key[string]](cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:Foo\",\n})\n\nvar keyspace2 = cache.NewStringKeyspace[Key[string]](cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/bar/:Foo\",\n})\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n-- want: errors --\n\n── Path Conflict ──────────────────────────────────────────────────────────────────────────[E9999]──\n\nThe path segment `bar` conflicts with the path `/foo/:Foo`.\n\n    ╭─[ svc/svc.go:16:18 ]\n    │\n 14 │\n 15 │ var keyspace1 = cache.NewStringKeyspace[Key[string]](cluster, cache.KeyspaceConfig{\n 16 │     KeyPattern: \"foo/:Foo\",\n    ⋮                  ───────\n    ·\n    ·\n 18 │\n 19 │ var keyspace2 = cache.NewStringKeyspace[Key[string]](cluster, cache.KeyspaceConfig{\n 20 │     KeyPattern: \"foo/bar/:Foo\",\n    ⋮                  ───────────\n 21 │ })\n 22 │\n────╯\n\nPaths must be not be empty and always start with a '/'. You cannot define paths that conflict with\neach other, including static and parameterized paths. For example `/blog/:id` would conflict with\n`/:username`.\n\nFor more information about configuring Paths, see https://encore.dev/docs/primitives/apis#rest-apis\n"
  },
  {
    "path": "v2/app/testdata/cache_err_generic_type_nonbasic.txt",
    "content": "! parse\nerr 'has invalid key type parameter: struct field Foo is not a basic type'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\ntype Key[T any] struct {\n    Foo T\n}\n\nvar keyspace = cache.NewStringKeyspace[Key[Key[string]]](cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:Foo\",\n})\n\n//encore:api public\nfunc Foo(context.Context) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid Cache Key Type ─────────────────────────────────────────────────────────────────[E9999]──\n\nThe field Foo is invalid: must be a basic type\n\n    ╭─[ svc/svc.go:12:5 ]\n    │\n 10 │\n 11 │ type Key[T any] struct {\n 12 │     Foo T\n    ⋮     ──┬──\n    ⋮       ╰─ found Key[string]\n 13 │ }\n 14 │\n 15 │ var keyspace = cache.NewStringKeyspace[Key[Key[string]]](cluster, cache.KeyspaceConfig{\n    ⋮                                        ───────┬────────\n    ⋮                                               ╰─ instantiated here\n 16 │     KeyPattern: \"foo/:Foo\",\n 17 │ })\n────╯\n\nFor more information see https://encore.dev/docs/primitives/caching\n"
  },
  {
    "path": "v2/app/testdata/cache_err_keyspace_invalid.txt",
    "content": "! parse\nerr 'field Bar not used in KeyPattern'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\ntype Key struct {\n    ID int\n    Bar string\n}\n\nvar keyspace = cache.NewStringKeyspace[Key](cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:ID\",\n})\n\n//encore:api public\nfunc Foo(context.Context) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid Cache Key Type ─────────────────────────────────────────────────────────────────[E9999]──\n\nInvalid use of the key type, the field Bar was not used in the KeyPattern\n\n    ╭─[ svc/svc.go:13:5 ]\n    │\n 11 │ type Key struct {\n 12 │     ID int\n 13 │     Bar string\n    ⋮     ──────────\n    ·\n    ·\n 15 │\n 16 │ var keyspace = cache.NewStringKeyspace[Key](cluster, cache.KeyspaceConfig{\n 17 │     KeyPattern: \"foo/:ID\",\n    ⋮                 ─────────\n 18 │ })\n 19 │\n────╯\n\nFor more information see https://encore.dev/docs/primitives/caching\n"
  },
  {
    "path": "v2/app/testdata/cache_err_keyspace_outside_svc.txt",
    "content": "! parse\nerr 'cache.NewStringKeyspace can only be called from within Encore services'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar Cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- lib/lib.go --\npackage lib\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n    \"test/svc\"\n)\n\ntype Key[T any] struct {\n    Foo T\n}\n\nvar keyspace = cache.NewStringKeyspace[Key[string]](svc.Cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:Foo\",\n})\n-- want: errors --\n\n── Invalid Cache Keyspace ─────────────────────────────────────────────────────────────────[E9999]──\n\nCache keyspaces must be defined within a service.\n\n    ╭─[ lib/lib.go:14:16 ]\n    │\n 12 │ }\n 13 │\n 14 │ var keyspace = cache.NewStringKeyspace[Key[string]](svc.Cluster, cache.KeyspaceConfig{\n    ⋮                ────────────────────────────────────\n 15 │     KeyPattern: \"foo/:Foo\",\n 16 │ })\n────╯\n\nFor more information see https://encore.dev/docs/primitives/caching\n\n\n\n\n── Resource defined outside of service ────────────────────────────────────────────────────[E9999]──\n\nResources can only be defined within a service.\n\n    ╭─[ lib/lib.go:14:16 ]\n    │\n 12 │     }\n 13 │\n 14 │     var keyspace = cache.NewStringKeyspace[Key[string]](svc.Cluster, cache.KeyspaceConfig{\n    ⋮                    ▲\n    ⋮ ╭──────────────────╯\n 15 │ │       KeyPattern: \"foo/:Foo\",\n 16 │ │   })\n    ⋮ │    ▲\n    ⋮ ├────╯\n────╯\n"
  },
  {
    "path": "v2/app/testdata/cache_generic_type.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/cache\"\n)\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\ntype Key[T any] struct {\n    Foo T\n}\n\nvar keyspace = cache.NewStringKeyspace[Key[string]](cluster, cache.KeyspaceConfig{\n    KeyPattern: \"foo/:Foo\",\n})\n\n//encore:api public\nfunc Foo(context.Context) error {\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/config.txt",
    "content": "# Verify calls to config.Load are called\nparse\nexpectOut 'config svc Config\\[int\\]'\noutput 'config svc named:{type_arguments:{builtin:INT}}'\n\n-- libraries/shared/shared.go --\npackage shared\n\nimport (\n    \"encore.dev/config\"\n)\n\ntype LibraryConfig struct {\n    Name    config.String\n    Enabled config.Bool\n    ValueList config.Value[[]int]\n    List []int\n}\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/config\"\n\n    \"test/libraries/shared\"\n)\n\ntype Config[T any] struct {\n    Library    shared.LibraryConfig\n    OtherValue config.Value[T]\n    NameList   config.Values[string]\n    ValueList config.Value[[]T]\n    List []T\n}\n\nvar cfg = config.Load[Config[int]]()\n\n\n// encore:api\nfunc Subscriber1(ctx context.Context) error {\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/config_err_unexported_field.txt",
    "content": "! parse\nerr 'Field subField is not exported and is in a datatype which is used by a call'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/config\"\n)\n\ntype SubType struct {\n    subField string\n}\n\ntype Config struct {\n    FooEnabled bool\n    Sub        SubType\n}\n\nvar Cfg = config.Load[Config]()\n\n\n//encore:api\nfunc Subscriber1(ctx context.Context) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid config type ────────────────────────────────────────────────────────────────────[E9999]──\n\nField is not exported and is in a datatype which is used by a call to `config.Load[T]()`.\nUnexported fields cannot be initialised by Encore, thus are not allowed in this context.\n\n    ╭─[ svc/svc.go:10:5 ]\n    │\n  8 │\n  9 │ type SubType struct {\n 10 │     subField string\n    ⋮     ───────────────\n    ·\n    ·\n 16 │ }\n 17 │\n 18 │ var Cfg = config.Load[Config]()\n    ⋮           ──────────┬──────────\n    ⋮                     ╰─ config loaded here\n 19 │\n 20 │\n────╯\n\nFor more information on configuration, see https://encore.dev/docs/develop/config\n"
  },
  {
    "path": "v2/app/testdata/config_err_use_from_other_service.txt",
    "content": "# Verify calls to config.Load are called\n! parse\nerr 'A config instance can only be referenced from within the service that the call to'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/config\"\n)\n\ntype Config struct {\n    FooEnabled bool\n}\n\nvar Cfg = config.Load[Config]()\n\n\n//encore:api\nfunc Subscriber1(ctx context.Context) error {\n    return nil\n}\n\n\n-- libraries/foo/foo.go --\npackage foo\n\nimport (\n    \"test/svc\"\n)\n\nfunc init() {\n    if svc.Cfg.FooEnabled {\n        // do something\n    }\n}\n\n-- want: errors --\n\n── Cross service config use ───────────────────────────────────────────────────────────────[E9999]──\n\nA config instance can only be referenced from within the service that the call to\n`config.Load[T]()` was made in.\n\n    ╭─[ libraries/foo/foo.go:8:8 ]\n    │\n  6 │\n  7 │ func init() {\n  8 │     if svc.Cfg.FooEnabled {\n    ⋮        ────────┬─────────\n    ⋮                ╰─ used here\n  9 │         // do something\n 10 │     }\n────╯\n\n    ╭─[ svc/svc.go:13:11 ]\n    │\n 11 │ }\n 12 │\n 13 │ var Cfg = config.Load[Config]()\n    ⋮           ──────────┬──────────\n    ⋮                     ╰─ defined here\n 14 │\n 15 │\n────╯\n\nFor more information on configuration, see https://encore.dev/docs/develop/config\n"
  },
  {
    "path": "v2/app/testdata/config_err_wrapper_used_in_wrapper.txt",
    "content": "! parse\nerr 'The type of config.Value'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/config\"\n)\n\ntype Config struct {\n    Name config.Value[config.Value[string]]\n}\n\nvar cfg = config.Load[Config]()\n\n\n// encore:api\nfunc Subscriber1(ctx context.Context) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid config type ────────────────────────────────────────────────────────────────────[E9999]──\n\nThe type of config.Value[T] cannot be another config.Value[T]\n\n    ╭─[ svc/svc.go:10:23 ]\n    │\n  8 │\n  9 │ type Config struct {\n 10 │     Name config.Value[config.Value[string]]\n    ⋮                       ─────────┬──────────\n    ⋮                                ╰─ cannot use config.Value inside a config.Value\n 11 │ }\n 12 │\n 13 │ var cfg = config.Load[Config]()\n    ⋮           ──────────┬──────────\n    ⋮                     ╰─ config loaded here\n 14 │\n 15 │\n────╯\n\nFor more information on configuration, see https://encore.dev/docs/develop/config\n"
  },
  {
    "path": "v2/app/testdata/cron_job_definition.txt",
    "content": "# Verify the cron job definition\nparse\noutput 'rpc svc.CronAPI access=public raw=false path=/cron'\noutput 'cronJob cronfooboo title=\"Cron Foo Boo\"'\noutput 'cronJob cronfoo title=\"Cron Foo Bar\"'\noutput 'cronJob cronfood title=\"Cron Food Bar\"'\noutput 'cronJob cronfoocious title=\"Cron Foo Bar Bazz\"'\noutput 'cronJob cron-every title=\"Cron Foo Bar Bazz\"'\noutput 'cronJob cron-external'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"test/external\"\n\t\"encore.dev/cron\"\n)\n\nvar _ = cron.NewJob(\"cronfooboo\", cron.JobConfig{\n\tTitle:     \"Cron Foo Boo\",\n\tSchedule: \"* * * * 5\",\n\tEndpoint: CronAPI,\n})\n\n// A cron job to send out emails to newsletter subscribers.\n// On two lines.\nvar _ = cron.NewJob(\"cronfoo\", cron.JobConfig{\n\tTitle:     \"Cron Foo Bar\",\n\tSchedule: \"* * * * 5\",\n\tEndpoint: CronAPI,\n})\n\n/*\n\tIn this example, we are defining a cron job, together with\n\ta random string.\n\n\tvar _, bd = cron.NewJob(\"cronfood\", cron.JobConfig{\n\t\tTitle:     \"Cron Food Bar\",\n\t\tSchedule: \"* * * * 5\",\n\t\tEndpoint: CronAPI,\n\t}), \"barfoo\"\n\n\tWe are being very explicit, and we are also giving a description\n\tthat includes a code snippet.\n*/\nvar _, bd = cron.NewJob(\"cronfood\", cron.JobConfig{\n\tTitle:     \"Cron Food Bar\",\n\tSchedule: \"* * * * 5\",\n\tEndpoint: CronAPI,\n}), \"barfoo\"\n\n// A cron job to send out push notifications to new subscribers.\nvar _ = cron.NewJob(\"cronfoocious\", cron.JobConfig{\n\tTitle:     \"Cron Foo Bar Bazz\",\n\tSchedule: \"* * * * 4\",\n\tEndpoint: CronAPI,\n})\n\n// A cron job using Every instead of Schedule\nvar _ = cron.NewJob(\"cron-every\", cron.JobConfig{\n\tTitle:     \"Cron Foo Bar Bazz\",\n\tEvery:    3 * cron.Minute,\n\tEndpoint: CronAPI,\n})\n\n// A cron job using an external endpoint\nvar _ = cron.NewJob(\"cron-external\", cron.JobConfig{\n\tEvery:    cron.Minute,\n\tEndpoint: external.Endpoint,\n})\n\n//encore:api public path=/cron\nfunc CronAPI(ctx context.Context) (*Response, error) {\n\tmsg := \"Hello, Cron!\"\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n\n-- external/external.go --\npackage external\n\nimport (\n\t\"context\"\n)\n\n//encore:api private\nfunc Endpoint(ctx context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/app/testdata/cron_job_definition_init.txt",
    "content": "# Verify the cron job definition\n! parse\nerr 'A cron job cannot be declared here, they can only be declared in a package level variable.'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/cron\"\n)\n\nfunc init() {\n    // A cron job to send out emails to newsletter subscribers.\n    // On two lines.\n    var _ = cron.NewJob(\"cronfoo\", cron.JobConfig{\n        Title:    \"Cron Foo Bar\",\n        Schedule: \"* * * * 5\",\n        Endpoint: Cron,\n    })\n}\n\n//encore:api public path=/cron\nfunc Cron(ctx context.Context) (*Response, error) {\n\tmsg := \"Hello, Cron!\"\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n\n-- want: errors --\n\n── Invalid call ───────────────────────────────────────────────────────────────────────────[E9999]──\n\ncron.NewJob cannot be called here. It must be called from a package level variable.\n\n    ╭─[ svc/svc.go:12:13 ]\n    │\n 10 │     // A cron job to send out emails to newsletter subscribers.\n 11 │     // On two lines.\n 12 │     var _ = cron.NewJob(\"cronfoo\", cron.JobConfig{\n    ⋮             ───────────\n 13 │         Title:    \"Cron Foo Bar\",\n 14 │         Schedule: \"* * * * 5\",\n────╯\n\n\n\n\n── Invalid API Usage ──────────────────────────────────────────────────────────────────────[E9999]──\n\nAPIs can not be referenced without being called, unless they are used as a cron job endpoint, or a\nPubSub subscription handler.\n\n    ╭─[ svc/svc.go:15:19 ]\n    │\n 13 │         Title:    \"Cron Foo Bar\",\n 14 │         Schedule: \"* * * * 5\",\n 15 │         Endpoint: Cron,\n    ⋮                   ─┬──\n    ⋮                    ╰─ used here\n    ·\n    ·\n 18 │\n 19 │ //encore:api public path=/cron\n 20 │ func Cron(ctx context.Context) (*Response, error) {\n    ⋮      ─┬──\n    ⋮       ╰─ defined here\n 21 │     msg := \"Hello, Cron!\"\n 22 │     return &Response{Message: msg}, nil\n────╯\n\nFor more information on how to use APIs see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/cron_job_definition_repeat.txt",
    "content": "# Verify the cron job definition\n! parse\nerr 'cron job cronfood defined twice'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/cron\"\n)\n\n/*\n\tIn this example, we are defining a cron job, together with\n\ta random string.\n\n\tvar _, bd = cron.NewJob(\"cronfood\", cron.JobConfig{\n\t\tTitle:    \"Cron Food Bar\",\n\t\tSchedule: \"* * * * 5\",\n\t\tEndpoint: Cron,\n\t}), \"barfoo\"\n\n\tWe are being very explicit, and we are also giving a description\n\tthat includes a code snippet.\n*/\nvar _, bd = cron.NewJob(\"cronfood\", cron.JobConfig{\n\tTitle:    \"Cron Food Bar\",\n\tSchedule: \"* * * * 5\",\n\tEndpoint: Cron,\n}), \"barfoo\"\n\n// A cron job to send out push notiifications to new subscribers.\nvar _ = cron.NewJob(\"cronfood\", cron.JobConfig{\n\tTitle:    \"Cron Foo Bar Bazz\",\n\tSchedule: \"* * * * 4\",\n\tEndpoint: Cron,\n})\n\n//encore:api public path=/cron\nfunc Cron(ctx context.Context) (*Response, error) {\n\tmsg := \"Hello, Cron!\"\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n\n-- want: errors --\n\n── Duplicate Cron Jobs ────────────────────────────────────────────────────────────────────[E9999]──\n\nMultiple cron jobs with the same name were found. Cronjob names must be unique.\n\n    ╭─[ svc/svc.go:22:25 ]\n    │\n 20 │     that includes a code snippet.\n 21 │ */\n 22 │ var _, bd = cron.NewJob(\"cronfood\", cron.JobConfig{\n    ⋮                         ──────────\n    ·\n    ·\n 27 │\n 28 │ // A cron job to send out push notiifications to new subscribers.\n 29 │ var _ = cron.NewJob(\"cronfood\", cron.JobConfig{\n    ⋮                     ──────────\n 30 │     Title:    \"Cron Foo Bar Bazz\",\n 31 │     Schedule: \"* * * * 4\",\n────╯\n\nFor more information, see https://encore.dev/docs/primitives/cron-jobs\n"
  },
  {
    "path": "v2/app/testdata/cron_job_definition_rpc.txt",
    "content": "# Verify the cron job definition\n! parse\nerr 'A cron job cannot be declared here, they can only be declared in a package level variable.'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/cron\"\n)\n\n//encore:api public path=/cron\nfunc Cron(ctx context.Context) (*Response, error) {\n\tmsg := \"Hello, Cron!\"\n    // A cron job to send out emails to newsletter subscribers.\n    // On two lines.\n    var _ = cron.NewJob(\"cronfoo\", cron.JobConfig{\n        Title:    \"Cron Foo Bar\",\n        Schedule: \"* * * * 5\",\n        Endpoint: Cron,\n    })\n\treturn &Response{Message: msg}, nil\n}\n\ntype Response struct {\n\tMessage string\n}\n\n-- want: errors --\n\n── Invalid call ───────────────────────────────────────────────────────────────────────────[E9999]──\n\ncron.NewJob cannot be called here. It must be called from a package level variable.\n\n    ╭─[ svc/svc.go:14:13 ]\n    │\n 12 │     // A cron job to send out emails to newsletter subscribers.\n 13 │     // On two lines.\n 14 │     var _ = cron.NewJob(\"cronfoo\", cron.JobConfig{\n    ⋮             ───────────\n 15 │         Title:    \"Cron Foo Bar\",\n 16 │         Schedule: \"* * * * 5\",\n────╯\n\n\n\n\n── Invalid API Usage ──────────────────────────────────────────────────────────────────────[E9999]──\n\nAPIs can not be referenced without being called, unless they are used as a cron job endpoint, or a\nPubSub subscription handler.\n\n    ╭─[ svc/svc.go:10:6 ]\n    │\n  8 │\n  9 │ //encore:api public path=/cron\n 10 │ func Cron(ctx context.Context) (*Response, error) {\n    ⋮      ─┬──\n    ⋮       ╰─ defined here\n    ·\n    ·\n 15 │         Title:    \"Cron Foo Bar\",\n 16 │         Schedule: \"* * * * 5\",\n 17 │         Endpoint: Cron,\n    ⋮                   ─┬──\n    ⋮                    ╰─ used here\n 18 │     })\n 19 │     return &Response{Message: msg}, nil\n────╯\n\nFor more information on how to use APIs see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/cron_job_err_not_api.txt",
    "content": "# Verify the cron job definition\n! parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"encore.dev/cron\"\n)\n\nvar _ = cron.NewJob(\"my-job\", cron.JobConfig{\n\tSchedule: \"* * * * 5\",\n\tEndpoint: CronFunc,\n})\n\n\nfunc CronFunc(ctx context.Context) error {\n    return nil\n}\n-- want: errors --\n\n── Invalid call to cron.NewJob ────────────────────────────────────────────────────────────[E9999]──\n\nEndpoint does not reference an Encore API\n\n    ╭─[ svc/svc.go:12:12 ]\n    │\n 10 │ var _ = cron.NewJob(\"my-job\", cron.JobConfig{\n 11 │     Schedule: \"* * * * 5\",\n 12 │     Endpoint: CronFunc,\n    ⋮               ────────\n 13 │ })\n 14 │\n────╯\n\nFor more information, see https://encore.dev/docs/primitives/cron-jobs\n\n\n\n\n── Resource defined outside of service ────────────────────────────────────────────────────[E9999]──\n\nResources can only be defined within a service.\n\n    ╭─[ svc/svc.go:10:9 ]\n    │\n  8 │     )\n  9 │\n 10 │     var _ = cron.NewJob(\"my-job\", cron.JobConfig{\n    ⋮             ▲\n    ⋮ ╭───────────╯\n 11 │ │       Schedule: \"* * * * 5\",\n 12 │ │       Endpoint: CronFunc,\n 13 │ │   })\n    ⋮ │    ▲\n    ⋮ ├────╯\n 14 │\n 15 │\n────╯\n"
  },
  {
    "path": "v2/app/testdata/et.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n)\n\n//encore:api public\nfunc Foo(context.Context) error { return nil }\n\n\n-- svc/svc_test.go --\npackage svc\n\nimport (\n    \"context\"\n    \"testing\"\n\n    \"encore.dev/et\"\n)\n\nfunc TestFoo(t *testing.T) {\n    et.AuthHandler(MyAuth)\n}\n"
  },
  {
    "path": "v2/app/testdata/metrics_counter.txt",
    "content": "parse\noutput 'metric counter UINT64 COUNTER \\[\\]'\noutput 'metric counter_with_labels UINT64 COUNTER \\[label STRING Label doc string.\\n\\]'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/metrics\"\n)\n\nvar Counter = metrics.NewCounter[uint64](\"counter\", metrics.CounterConfig{})\n\ntype Labels struct {\n\tLabel string // Label doc string.\n}\n\nvar CounterWithLabels = metrics.NewCounterGroup[Labels, uint64](\"counter_with_labels\", metrics.CounterConfig{})\n\n//encore:api public\nfunc Foo(context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/app/testdata/metrics_gauge.txt",
    "content": "parse\noutput 'metric gauge FLOAT64 GAUGE \\[\\]'\noutput 'metric gauge_with_labels FLOAT64 GAUGE \\[label STRING Label doc string.\\n\\]'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/metrics\"\n)\n\nvar Gauge = metrics.NewGauge[float64](\"gauge\", metrics.CounterConfig{})\n\ntype Labels struct {\n\tLabel string // Label doc string.\n}\n\nvar GaugeWithLabels = metrics.NewGaugeGroup[Labels, float64](\"gauge_with_labels\", metrics.CounterConfig{})\n\n//encore:api public\nfunc Foo(context.Context) error {\n\treturn nil\n}"
  },
  {
    "path": "v2/app/testdata/middleware.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n\n//encore:api public tag:foo\nfunc API(ctx context.Context) error { return nil }\n\n-- svc/mw/mw.go --\npackage mw\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware target=tag:foo\nfunc TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n//encore:middleware target=all\nfunc AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n\n-- lib/globalmw/globalmw.go --\npackage globalmw\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware global target=tag:foo\nfunc TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n//encore:middleware global target=all\nfunc AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n"
  },
  {
    "path": "v2/app/testdata/middleware_err_no_matches.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n\n//encore:api public tag:bar\nfunc API(ctx context.Context) error { return nil }\n\n-- svc/mw/mw.go --\npackage mw\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware target=tag:foo\nfunc TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n//encore:middleware target=all\nfunc AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n\n-- lib/globalmw/globalmw.go --\npackage globalmw\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware global target=tag:foo\nfunc TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n//encore:middleware global target=all\nfunc AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n-- want: errors --\n\n── Invalid middleware target ──────────────────────────────────────────────────────────────[E9999]──\n\nThere are not matching targets in service \"svc\".\n\n   ╭─[ svc/mw/mw.go:5:28 ]\n   │\n 3 │ import \"encore.dev/middleware\"\n 4 │\n 5 │ //encore:middleware target=tag:foo\n   ⋮                            ───────\n 6 │ func TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n 7 │     return next(req)\n───╯\n\nhint: middleware must have the signature:\n\tfunc(req middleware.Request, next middleware.Next) middleware.Response\n\nFor more information on how to use middleware, see https://encore.dev/docs/develop/middleware\n\n\n\n\n── Invalid middleware target ──────────────────────────────────────────────────────────────[E9999]──\n\nThere are not matching targets in the application.\n\n   ╭─[ lib/globalmw/globalmw.go:5:35 ]\n   │\n 3 │ import \"encore.dev/middleware\"\n 4 │\n 5 │ //encore:middleware global target=tag:foo\n   ⋮                                   ───────\n 6 │ func TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n 7 │     return next(req)\n───╯\n\nhint: middleware must have the signature:\n\tfunc(req middleware.Request, next middleware.Next) middleware.Response\n\nFor more information on how to use middleware, see https://encore.dev/docs/develop/middleware\n"
  },
  {
    "path": "v2/app/testdata/middleware_err_not_in_service.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n\n//encore:api public tag:foo\nfunc API(ctx context.Context) error { return nil }\n\n-- svc/mw/mw.go --\npackage mw\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware target=tag:foo\nfunc TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n//encore:middleware target=all\nfunc AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n\n-- lib/globalmw/globalmw.go --\npackage globalmw\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware global target=tag:foo\nfunc TaggedInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n//encore:middleware target=all\nfunc AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n-- want: errors --\n\n── Invalid middleware function ────────────────────────────────────────────────────────────[E9999]──\n\nMiddleware must be defined in a service unless it is marked as being global.\n\n    ╭─[ lib/globalmw/globalmw.go:11:6 ]\n    │\n  9 │\n 10 │ //encore:middleware target=all\n 11 │ func AllInSvc(req middleware.Request, next middleware.Next) middleware.Response {\n    ⋮      ────────\n 12 │     return next(req)\n 13 │ }\n────╯\n\nhint: middleware must have the signature:\n\tfunc(req middleware.Request, next middleware.Next) middleware.Response\n\nFor more information on how to use middleware, see https://encore.dev/docs/develop/middleware\n"
  },
  {
    "path": "v2/app/testdata/missing_generic_param.txt",
    "content": "# This test makes sure we don't panic when parsing a missing generic type\n! parse\n-- foo.go --\npackage foo\n\nimport \"context\"\n\ntype Params struct {\n    C *Generic\n}\n\ntype Generic[T any] struct {\n    Val *T\n}\n\n//encore:api public\nfunc Dummy(ctx context.Context, p *Params) error {\n    return nil\n}\n-- want: errors --\n\n── Missing type argument ──────────────────────────────────────────────────────────────────[E9999]──\n\nMissing type argument\n\n    ╭─[ foo.go:6:7 ]\n    │\n  4 │\n  5 │ type Params struct {\n  6 │     C *Generic\n    ⋮       ───┬────\n    ⋮          ╰─ missing from here\n  7 │ }\n  8 │\n  9 │ type Generic[T any] struct {\n    ⋮              ──┬──\n    ⋮                ╰─ missing type parameter defined here\n 10 │     Val *T\n 11 │ }\n────╯\n\nFor more information, see https://encore.dev/docs/develop/api-schemas\n\n\n\n\n── Missing type argument ──────────────────────────────────────────────────────────────────[E9999]──\n\nMissing type argument\n\n    ╭─[ foo.go:6:7 ]\n    │\n  4 │\n  5 │ type Params struct {\n  6 │     C *Generic\n    ⋮       ───┬────\n    ⋮          ╰─ missing from here\n  7 │ }\n  8 │\n  9 │ type Generic[T any] struct {\n    ⋮              ──┬──\n    ⋮                ╰─ missing type parameter defined here\n 10 │     Val *T\n 11 │ }\n────╯\n\nFor more information, see https://encore.dev/docs/develop/api-schemas\n"
  },
  {
    "path": "v2/app/testdata/pubsub.txt",
    "content": "# Verify that pub sub is parsed\nparse\noutput 'pubsubTopic basic-topic'\noutput 'pubsubTopic another-topic'\noutput 'pubsubPublisher basic-topic foo'\noutput 'pubsubSubscriber basic-topic basic-subscription svc 45000000000 396000000000000 3 8000000000 1920000000000'\noutput 'pubsubSubscriber basic-topic another-subscription svc 30000000000 604800000000000 100 10000000000 600000000000'\noutput 'pubsubSubscriber basic-topic a-third-subscription foo 1000000000 119999999000 47 14399999998000 17999999997000'\n\n-- shared/topics.go --\npackage shared\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    AnotherTopic = pubsub.NewTopic[*MessageType](\"another-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.ExactlyOnce })\n)\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n    \"test/svc/domain\"\n)\n\n\nvar (\n    _ = pubsub.NewSubscription(shared.BasicTopic, \"basic-subscription\",\n        pubsub.SubscriptionConfig {\n            Handler: Subscriber1,\n            MaxConcurrency: 25,\n            AckDeadline: 45 * time.Second,\n            MessageRetention: 5 * time.Hour * 24 + -10 * time.Hour,\n            RetryPolicy: &pubsub.RetryPolicy{\n                MaxRetries: 3,\n                MinBackoff: 8 * time.Second,\n                MaxBackoff: 32 * time.Minute,\n            },\n        },\n    )\n\n    _ = pubsub.NewSubscription(shared.BasicTopic, \"another-subscription\",\n        pubsub.SubscriptionConfig { Handler: domain.SubscriptionCode },\n    )\n)\n\n// encore:api\nfunc Subscriber1(ctx context.Context, msg *shared.MessageType) error {\n    return nil\n}\n\n-- svc/domain/code.go --\npackage domain\n\nimport (\n    \"context\"\n\n    \"test/shared\"\n)\n\nfunc SubscriptionCode(ctx context.Context, msg *shared.MessageType) error {\n    return nil\n}\n\n-- foo/code.go --\npackage foo\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n)\n\nvar _ = pubsub.NewSubscription(\n    shared.BasicTopic,\n    \"a-third-subscription\",\n    pubsub.SubscriptionConfig {\n        Handler: func(ctx context.Context, msg *shared.MessageType) error {\n          return nil\n      },\n      AckDeadline: 1 * time.Second,\n      MessageRetention: 2 * time.Minute + -1 * time.Microsecond,\n      RetryPolicy: &pubsub.RetryPolicy{\n          MaxRetries: -3 + 10 * 5,\n          MinBackoff: 4 * time.Hour + -2 * time.Microsecond,\n          MaxBackoff: 5 * time.Hour + -3 * time.Microsecond,\n      },\n    },\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    shared.BasicTopic.Publish(ctx, &shared.MessageType{Name: \"foo\"})\n}\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_attributes_not_start_encore.txt",
    "content": "# Verify that pub sub is parsed\n! parse\nerr 'PubSub message attributes must not be prefixed with \"encore\".'\n\n-- shared/topics.go --\npackage shared\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string `pubsub-attr:\"encorename\"`\n}\n\nvar BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\n-- want: errors --\n\n── Invalid attribute prefix ───────────────────────────────────────────────────────────────[E9999]──\n\nPubSub message attributes must not be prefixed with \"encore\".\n\n    ╭─[ shared/topics.go:8:17 ]\n    │\n  6 │\n  7 │ type MessageType struct {\n  8 │     Name string `pubsub-attr:\"encorename\"`\n    ⋮                 ──────────────────────────\n  9 │ }\n 10 │\n 11 │ var BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    ⋮                                  ─────┬──────\n    ⋮                                       ╰─ used as a message type in this topic\n 12 │\n────╯\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_duplicate_subscription_names.txt",
    "content": "! parse\nerr 'Subscriptions names on topics must be unique.'\n\n-- shared/topics.go --\npackage shared\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n)\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n)\n\n\nvar (\n    _ = pubsub.NewSubscription(shared.BasicTopic, \"same-name\", pubsub.SusbcriptionConfig { Handler: Subscriber1 })\n)\n\n// encore:api\nfunc Subscriber1(ctx context.Context, msg *shared.MessageType) error {\n    return nil\n}\n\n-- foo/code.go --\npackage foo\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n)\n\nvar _ = pubsub.NewSubscription(\n    shared.BasicTopic,\n    \"same-name\",\n    pubsub.SusbcriptionConfig { Handler: func(ctx context.Context, msg *shared.MessageType) error {\n        return nil\n    }},\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    shared.BasicTopic.Publish(ctx, &shared.MessageType{Name: \"foo\"})\n}\n\n-- want: errors --\n\n── Duplicate PubSub subscription on topic ─────────────────────────────────────────────────[E9999]──\n\nSubscription names on topics must be unique.\n\n    ╭─[ svc/svc.go:13:51 ]\n    │\n 11 │\n 12 │ var (\n 13 │     _ = pubsub.NewSubscription(shared.BasicTopic, \"same-name\", pubsub.SusbcriptionConfig { Handler: Subscriber1 })\n    ⋮                                                   ─────┬─────\n    ⋮                                                        ╰─ duplicated here\n 14 │ )\n 15 │\n────╯\n\n    ╭─[ foo/code.go:13:5 ]\n    │\n 11 │ var _ = pubsub.NewSubscription(\n 12 │     shared.BasicTopic,\n 13 │     \"same-name\",\n    ⋮     ─────┬─────\n    ⋮          ╰─ originally defined here\n 14 │     pubsub.SusbcriptionConfig { Handler: func(ctx context.Context, msg *shared.MessageType) error {\n 15 │         return nil\n────╯\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_import_aliased_and_used_in_func.txt",
    "content": "! parse\nerr 'A pubsub topic cannot be declared here, they can only be declared in a package level variable.'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    pb \"encore.dev/pubsub\"\n)\n\ntype MyMessage struct {}\n\n//encore:api\nfunc SomeAPI(ctx context.Context) error {\n    topic := pb.NewTopic[MyMessage](\"basic-topic\")\n    topic.Publish(ctx, &MyMessage{})\n}\n\n-- want: errors --\n\n── Invalid call ───────────────────────────────────────────────────────────────────────────[E9999]──\n\npubsub.NewTopic cannot be called here. It must be called from a package level variable.\n\n    ╭─[ svc/svc.go:13:14 ]\n    │\n 11 │ //encore:api\n 12 │ func SomeAPI(ctx context.Context) error {\n 13 │     topic := pb.NewTopic[MyMessage](\"basic-topic\")\n    ⋮              ───────────\n 14 │     topic.Publish(ctx, &MyMessage{})\n 15 │ }\n────╯\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_missing_delivery_guarantee.txt",
    "content": "! parse\nerr 'pubsub.NewTopic requires the configuration field named \"DeliveryGuarantee\" to be explicitly set.'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{})\n    _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: Subscriber })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\nfunc Subscriber(ctx context.Context, msg *MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid PubSub topic config ────────────────────────────────────────────────────────────[E9999]──\n\nThe configuration field named \"DeliveryGuarantee\" must be set to pubsub.AtLeastOnce or\npubsub.ExactlyOnce.\n\n    ╭─[ svc/svc.go:14:63 ]\n    │\n 12 │\n 13 │ var (\n 14 │     BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{})\n    ⋮                                                               ────────────────────\n 15 │     _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: Subscriber })\n 16 │ )\n────╯\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_new_topic_func_aliased.txt",
    "content": "! parse\nerr 'pubsub.NewTopic can only be called as a function to create a new instance and not referenced'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MyMessage struct {}\n\nvar creator = pubsub.NewTopic[MyMessage]\n\nvar topic = creator(\"my-topic\")\n\n-- want: errors --\n\n── Invalid reference ──────────────────────────────────────────────────────────────────────[E9999]──\n\npubsub.NewTopic cannot be referenced without being called.\n\n    ╭─[ svc/svc.go:11:15 ]\n    │\n  9 │ type MyMessage struct {}\n 10 │\n 11 │ var creator = pubsub.NewTopic[MyMessage]\n    ⋮               ───────────────\n 12 │\n 13 │ var topic = creator(\"my-topic\")\n────╯\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_ordering_attribute_missing.txt",
    "content": "# Verify that pub sub is parsed\n! parse\n\n-- shared/topics.go --\npackage shared\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    ID   int64  `pubsub-attr:\"msg-id\"`\n    Name string `pubsub-attr:\"name\"`\n}\n\nvar BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n    OrderingAttribute: \"ID\",\n})\n\n-- want: errors --\n\n── Invalid PubSub topic config ────────────────────────────────────────────────────────────[E9999]──\n\nThe configuration field named \"OrderingAttribute\" must be a one of the export attributes on the\nmessage type.\n\n    ╭─[ shared/topics.go:14:24 ]\n    │\n 12 │ var BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{\n 13 │     DeliveryGuarantee: pubsub.AtLeastOnce,\n 14 │     OrderingAttribute: \"ID\",\n    ⋮                        ────\n 15 │ })\n 16 │\n────╯\n\nFor example `pubsub.NewTopic[MyMessage](\"my-topic\", pubsub.TopicConfig{ DeliveryGuarantee:\npubsub.AtLeastOnce })`\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_subscriber_different_service.txt",
    "content": "! parse\nerr 'The function passed to `pubsub.NewSubscription` must be declared in the the same service as the'\n\n-- shared/shared.go --\npackage shared\n\ntype MessageType struct {\n    Name string\n}\n\n-- svc1/svc.go --\npackage svc1\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n    \"test/svc2\"\n)\n\nvar BasicTopic = pubsub.NewTopic[*shared.MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\nvar _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: svc2.Subscriber })\n\n// encore:api\nfunc Stuff(ctx context.Context) error {\n    return nil\n}\n\n-- svc2/svc.go --\npackage svc2\n\nimport (\n    \"context\"\n\n    \"test/shared\"\n)\n\n//encore:api\nfunc Subscriber(ctx context.Context, msg *shared.MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid PubSub subscription handler ────────────────────────────────────────────────────[E9999]──\n\nThe handler for the subscription must be defined in the same service as the call to\npubsub.NewSubscription.\n\n    ╭─[ svc1/svc.go:14:103 ]\n    │\n 12 │ var BasicTopic = pubsub.NewTopic[*shared.MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n 13 │\n 14 │ var _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: svc2.Subscriber })\n    ⋮                                                                                                       ───────┬───────\n    ⋮                                                                                                              ╰─ handler specified here\n 15 │\n 16 │ // encore:api\n────╯\n\n    ╭─[ svc2/svc.go:10:6 ]\n    │\n  8 │\n  9 │ //encore:api\n 10 │ func Subscriber(ctx context.Context, msg *shared.MessageType) error {\n    ⋮      ────┬─────\n    ⋮          ╰─ endpoint defined here\n 11 │     return nil\n 12 │ }\n────╯\n\nA pubsub subscription must have a unique name per topic and be given a handler function for\nprocessing the message. The handler for the subscription must be defined in the same service as the\ncall to pubsub.NewSubscription and can be an inline function. For example:\n\tpubsub.NewSubscription(myTopic, \"subscription-name\", pubsub.SubscriptionConfig[MyMessage]{\n\t\tHandler: func(ctx context.Context, event MyMessage) error { return nil },\n\t})\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_subscriber_missing_handler.txt",
    "content": "! parse\nerr 'pubsub.NewSubscription requires the configuration field named \"Handler\" to populated with the'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig{})\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\nfunc Subscriber(ctx context.Context, msg *MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid Argument ───────────────────────────────────────────────────────────────────────[E9999]──\n\nMissing required field `Handler`\n\n    ╭─[ svc/svc.go:15:66 ]\n    │\n 13 │ var (\n 14 │     BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n 15 │     _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig{})\n    ⋮                                                                  ───────────────────────────\n 16 │ )\n 17 │\n────╯\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_subscriber_nil_handler.txt",
    "content": "! parse\nerr 'pubsub.NewSubscription requires the configuration field named \"Handler\" to populated with the'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig{ Handler: nil })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\nfunc Subscriber(ctx context.Context, msg *MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid Argument ───────────────────────────────────────────────────────────────────────[E9999]──\n\nMissing required field `Handler`\n\n    ╭─[ svc/svc.go:15:66 ]\n    │\n 13 │ var (\n 14 │     BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n 15 │     _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig{ Handler: nil })\n    ⋮                                                                  ─────────────────────────────────────────\n 16 │ )\n 17 │\n────╯\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_subscriber_not_function.txt",
    "content": "! parse\nerr 'The function passed to `pubsub.NewSubscription` must be declared in the the same service'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"test/shared\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: shared.Subscriber })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\n-- shared/shared.go --\npackage shared\n\nfunc Subscriber(ctx context.Context, msg *shared.MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid PubSub subscription handler ────────────────────────────────────────────────────[E9999]──\n\nThe handler for the subscription must be defined in the same service as the call to\npubsub.NewSubscription.\n\n    ╭─[ svc/svc.go:17:103 ]\n    │\n 15 │ var (\n 16 │     BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n 17 │     _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: shared.Subscriber })\n    ⋮                                                                                                       ────────┬────────\n    ⋮                                                                                                               ╰─ handler specified here\n 18 │ )\n 19 │\n────╯\n\n   ╭─[ shared/shared.go:3:6 ]\n   │\n 1 │ package shared\n 2 │\n 3 │ func Subscriber(ctx context.Context, msg *shared.MessageType) error {\n   ⋮      ────┬─────\n   ⋮          ╰─ handler function defined here\n 4 │     return nil\n 5 │ }\n───╯\n\nA pubsub subscription must have a unique name per topic and be given a handler function for\nprocessing the message. The handler for the subscription must be defined in the same service as the\ncall to pubsub.NewSubscription and can be an inline function. For example:\n\tpubsub.NewSubscription(myTopic, \"subscription-name\", pubsub.SubscriptionConfig[MyMessage]{\n\t\tHandler: func(ctx context.Context, event MyMessage) error { return nil },\n\t})\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_subscription_func_not_in_service.txt",
    "content": "! parse\nerr 'The function passed to `pubsub.NewSubscription` must be declared in the the same service as the'\n\n-- shared/shared.go --\npackage shared\n\ntype MessageType struct {\n    Name string\n}\n\n-- svc1/svc.go --\npackage svc1\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n    \"test/svc2\"\n)\n\nvar BasicTopic = pubsub.NewTopic[*shared.MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\nvar _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: svc2.Subscriber })\n\n// encore:api\nfunc Stuff(ctx context.Context) error {\n    return nil\n}\n\n-- svc2/svc.go --\npackage svc2\n\nimport (\n    \"context\"\n\n    \"test/shared\"\n)\n\n//encore:api\nfunc Subscriber(ctx context.Context, msg *shared.MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid PubSub subscription handler ────────────────────────────────────────────────────[E9999]──\n\nThe handler for the subscription must be defined in the same service as the call to\npubsub.NewSubscription.\n\n    ╭─[ svc1/svc.go:14:103 ]\n    │\n 12 │ var BasicTopic = pubsub.NewTopic[*shared.MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n 13 │\n 14 │ var _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: svc2.Subscriber })\n    ⋮                                                                                                       ───────┬───────\n    ⋮                                                                                                              ╰─ handler specified here\n 15 │\n 16 │ // encore:api\n────╯\n\n    ╭─[ svc2/svc.go:10:6 ]\n    │\n  8 │\n  9 │ //encore:api\n 10 │ func Subscriber(ctx context.Context, msg *shared.MessageType) error {\n    ⋮      ────┬─────\n    ⋮          ╰─ endpoint defined here\n 11 │     return nil\n 12 │ }\n────╯\n\nA pubsub subscription must have a unique name per topic and be given a handler function for\nprocessing the message. The handler for the subscription must be defined in the same service as the\ncall to pubsub.NewSubscription and can be an inline function. For example:\n\tpubsub.NewSubscription(myTopic, \"subscription-name\", pubsub.SubscriptionConfig[MyMessage]{\n\t\tHandler: func(ctx context.Context, event MyMessage) error { return nil },\n\t})\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_subscription_name_invalid.txt",
    "content": "! parse\nerr 'The pubsub.NewSubscription must be subscription name be defined in \"kebab-case\"'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    _ = pubsub.NewSubscription(BasicTopic, \"basic subscription\", pubsub.SusbcriptionConfig { Handler: Subscriber })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\nfunc Subscriber(ctx context.Context, msg *MessageType) error {\n    return nil\n}\n\n-- want: errors --\n\n── Invalid resource name ──────────────────────────────────────────────────────────────────[E9999]──\n\nThe pubsub.NewSubscription subscription name must be defined in \"kebab-case\".\n\n    ╭─[ svc/svc.go:15:44 ]\n    │\n 13 │ var (\n 14 │     BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n 15 │     _ = pubsub.NewSubscription(BasicTopic, \"basic subscription\", pubsub.SusbcriptionConfig { Handler: Subscriber })\n    ⋮                                            ─────────┬──────────\n    ⋮                                                     ╰─ try basic-subscription?\n 16 │ )\n 17 │\n────╯\n\npubsub.NewSubscription subscription name's must be defined as string literals, be between 1 and 63\ncharacters long, and defined in \"kebab-case\", meaning it must start with a letter, end with a\nletter or number and only contain lower case letters, numbers and dashes.\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_topic_declared_in_func.txt",
    "content": "! parse\nerr 'A pubsub topic cannot be declared here, they can only be declared in a package level variable.'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MyMessage struct {}\n\n//encore:api\nfunc SomeAPI(ctx context.Context) error {\n    topic := pubsub.NewTopic[MyMessage](\"basic-topic\")\n    topic.Publish(ctx, &MyMessage{})\n}\n\n-- want: errors --\n\n── Invalid call ───────────────────────────────────────────────────────────────────────────[E9999]──\n\npubsub.NewTopic cannot be called here. It must be called from a package level variable.\n\n    ╭─[ svc/svc.go:13:14 ]\n    │\n 11 │ //encore:api\n 12 │ func SomeAPI(ctx context.Context) error {\n 13 │     topic := pubsub.NewTopic[MyMessage](\"basic-topic\")\n    ⋮              ───────────────\n 14 │     topic.Publish(ctx, &MyMessage{})\n 15 │ }\n────╯\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_topic_invalid_usage.txt",
    "content": "# Verify that pub sub is parsed\n! parse2\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\nfunc SomeFunc(t *pubsub.Topic[*MessageType]) {}\n\nfunc init() {\n    SomeFunc(BasicTopic)\n}\n-- want: errors --\n\n── Invalid reference to pubsub.Topic ──────────────────────────────────────────────────────[E9999]──\n\nA reference to pubsub.Topic is not permissible here.\n\n    ╭─[ svc/svc.go:16:14 ]\n    │\n 14 │\n 15 │ func init() {\n 16 │     SomeFunc(BasicTopic)\n    ⋮              ──────────\n 17 │ }\n────╯\n\nThe topic can only be referenced by calling methods on it, or to pass it to pubsub.NewSubscription\nor et.Topic.\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_topic_must_be_unique.txt",
    "content": "# Verify that pub sub is parsed\n! parse\nerr 'A PubSub topic name must be unique within an application.'\n\n-- shared/topics.go --\npackage shared\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar AnotherTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n\n-- want: errors --\n\n── Duplicate PubSub topic name ────────────────────────────────────────────────────────────[E9999]──\n\nA PubSub topic name must be unique within a service.\n\n    ╭─[ svc/svc.go:13:50 ]\n    │\n 11 │ }\n 12 │\n 13 │ var AnotherTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    ⋮                                                  ─────┬─────\n    ⋮                                                       ╰─ duplicated here\n 14 │\n────╯\n\n    ╭─[ shared/topics.go:11:48 ]\n    │\n  9 │ }\n 10 │\n 11 │ var BasicTopic = pubsub.NewTopic[*MessageType](\"same-name\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    ⋮                                                ─────┬─────\n    ⋮                                                     ╰─ originally defined here\n 12 │\n────╯\n\nIf you wish to reuse the same topic, then you can export the original Topic object import it here.\n\nFor more information on PubSub, see https://encore.dev/docs/primitives/pubsub\n"
  },
  {
    "path": "v2/app/testdata/pubsub_err_topic_name_invalid.txt",
    "content": "! parse\nerr 'The pubsub.NewTopic must be topic name be defined in \"kebab-case\"'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\n-- want: errors --\n\n── Invalid resource name ──────────────────────────────────────────────────────────────────[E9999]──\n\nThe pubsub.NewTopic topic name must be defined in \"kebab-case\".\n\n    ╭─[ svc/svc.go:14:48 ]\n    │\n 12 │\n 13 │ var (\n 14 │     BasicTopic = pubsub.NewTopic[*MessageType](\"basic topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    ⋮                                                ──────┬──────\n    ⋮                                                      ╰─ try basic-topic?\n 15 │ )\n 16 │\n────╯\n\npubsub.NewTopic topic name's must be defined as string literals, be between 1 and 63 characters\nlong, and defined in \"kebab-case\", meaning it must start with a letter, end with a letter or number\nand only contain lower case letters, numbers and dashes.\n"
  },
  {
    "path": "v2/app/testdata/pubsub_publish_in_middleware.txt",
    "content": "parse\noutput 'pubsubPublisher middlware basic MiddlewareFunc'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    UserID int64 `pubsub-attr:\"user_id\"`\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic\", pubsub.TopicConfig{\n        DeliveryGuarantee: pubsub.AtLeastOnce,\n        OrderingAttribute: \"user_id\",\n    })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{UserID: 1, Name: \"foo\"})\n}\n\n\n-- middleware/middleware.go --\npackage middleware\n\nimport (\n\t\"time\"\n\n\t\"encore.dev/middleware\"\n\t\"encore.dev/pubsub\"\n\n\t\"test/svc\"\n)\n\n//encore:middleware global target=all\nfunc MiddlewareFunc(req middleware.Request, next middleware.Next) middleware.Response {\n    svc.BasicTopic.Publish(req.Context(), &svc.MessageType{UserID: 1, Name: \"bar\"})\n\n\treturn next(req)\n}\n"
  },
  {
    "path": "v2/app/testdata/pubsub_subscriber_creates_service.txt",
    "content": "# Verify that pub sub is parsed\nparse\noutput 'pubsubTopic basic-topic'\noutput 'pubsubPublisher basic-topic svc'\noutput 'pubsubSubscriber basic-topic foo-service-sub foo'\n\n-- shared/topics.go --\npackage shared\n\nimport (\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n)\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"test/shared\"\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context, msg *shared.MessageType) error {\n    shared.BasicTopic.Publish(ctx, msg)\n}\n\n-- foo/code.go --\npackage foo\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n\n    \"test/shared\"\n)\n\nvar _ = pubsub.NewSubscription(\n    shared.BasicTopic,\n    \"foo-service-sub\",\n    pubsub.SusbcriptionConfig { Handler: func(ctx context.Context, msg *shared.MessageType) error {\n        return nil\n    }},\n)\n"
  },
  {
    "path": "v2/app/testdata/pubsub_subscriber_in_same_service.txt",
    "content": "parse\noutput 'pubsubTopic basic-topic'\noutput 'pubsubPublisher basic-topic svc'\noutput 'pubsubSubscriber basic-topic basic-subscription svc'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/pubsub\"\n)\n\ntype MessageType struct {\n    Name string\n}\n\nvar (\n    BasicTopic = pubsub.NewTopic[*MessageType](\"basic-topic\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })\n    _ = pubsub.NewSubscription(BasicTopic, \"basic-subscription\", pubsub.SusbcriptionConfig { Handler: Subscriber })\n)\n\n// encore:api\nfunc DoStuff(ctx context.Context) error {\n    return BasicTopic.Publish(ctx, &MessageType{Name: \"foo\"})\n}\n\nfunc Subscriber(ctx context.Context, msg *MessageType) error {\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/recursive_types.txt",
    "content": "parse\n-- foo.go --\npackage foo\n\nimport \"context\"\n\n// We must reference these types in an RPC parameter to actually validate them.\ntype Params struct {\n    A *SelfRecursive\n    B *MutuallyRecursive\n    C *Generic[Generic[MutuallyRecursive]]\n}\n\n//encore:api public\nfunc Dummy(ctx context.Context, p *Params) error {\n    return nil\n}\n\ntype SelfRecursive struct {\n    A *SelfRecursive\n}\n\ntype MutuallyRecursive struct {\n    Other *Other\n}\n\ntype Other struct {\n    Original *MutuallyRecursive\n}\n\ntype Generic[T any] struct {\n    Val *T\n}\n"
  },
  {
    "path": "v2/app/testdata/rlog_call_outside_svc.txt",
    "content": "parse\n\n-- foo/foo.go --\npackage foo\n\nimport \"encore.dev/rlog\"\n\nfunc Log() {\n    rlog.Info(\"test\")\n}\n\n-- bar/bar.go --\npackage bar\n\nimport (\n    \"context\"\n\n    \"test/foo\"\n)\n\n//encore:api public\nfunc CallFoo(ctx context.Context) error {\n    foo.Log()\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/rpc_auth.txt",
    "content": "parse\noutput 'rpc svc.API access=auth raw=false'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \n    \"encore.dev/beta/auth\"\n)\n\n//encore:authhandler\nfunc MyAuthHandler(ctx context.Context, token string) (auth.UID, error) {\n    return \"\", nil\n}\n\n//encore:api auth\nfunc API(ctx context.Context) error { return nil }"
  },
  {
    "path": "v2/app/testdata/rpc_auth_no_authhandler.txt",
    "content": "! parse\nerr 'cannot use \"auth\" access type'\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n//encore:api auth\nfunc API(ctx context.Context) error { return nil }\n\n-- want: errors --\n\n── No Auth Handler Defined ────────────────────────────────────────────────────────────────[E9999]──\n\nAn auth handler must be defined to use the auth directive on an API.\n\n   ╭─[ svc/svc.go:5:14 ]\n   │\n 3 │ import \"context\"\n 4 │\n 5 │ //encore:api auth\n   ⋮              ────\n 6 │ func API(ctx context.Context) error { return nil }\n 7 │\n───╯\n\nYou can specify them for each field using the struct tags, for example with `header:\"X-My-Header\"`\nor `query:\"my-query\"`.\n\nFor more information on auth handlers and how to define them, see\nhttps://encore.dev/docs/develop/auth\n"
  },
  {
    "path": "v2/app/testdata/rpc_call_selector.txt",
    "content": "# Verify that the parser isn't confusing t.Foo() with calling Foo()\nparse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n)\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    var t typ\n    t.Foo()\n    return nil\n}\n\ntype typ struct{}\nfunc (typ) Foo() { }"
  },
  {
    "path": "v2/app/testdata/rpc_err_any.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n)\n\ntype Params struct {\n    Foo any\n}\n\n//encore:api public\nfunc Any(ctx context.Context, p *Params) error { return nil }\n-- want: errors --\n\n── Invalid API schema ─────────────────────────────────────────────────────────────────────[E9999]──\n\nInterfaces are not supported in API schemas.\n\n    ╭─[ svc/svc.go:8:9 ]\n    │\n  6 │\n  7 │ type Params struct {\n  8 │     Foo any\n    ⋮         ─┬─\n    ⋮          ╰─ defined here\n    ·\n    ·\n 10 │\n 11 │ //encore:api public\n 12 │ func Any(ctx context.Context, p *Params) error { return nil }\n    ⋮                                 ───┬───\n    ⋮                                    ╰─ used here\n────╯\n\nFor more information on API schemas, see https://encore.dev/docs/develop/api-schemas\n"
  },
  {
    "path": "v2/app/testdata/rpc_invalid_header_type.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Nested struct {\n    Baz string\n}\n\ntype Params struct {\n    Foo uuid.UUID\n    Bar *Nested `header:\"X-Bar\"`\n}\n\n//encore:api public method=POST\nfunc Str(ctx context.Context, p *Params) error { return nil }\n\n-- want: errors --\n\n── Invalid request type ───────────────────────────────────────────────────────────────────[E9999]──\n\nAPI request parameters of type *Nested are not supported in headers. You can only use built-in\ntypes types such as strings, booleans, int, time.Time.\n\n    ╭─[ svc/svc.go:14:9 ]\n    │\n 12 │ type Params struct {\n 13 │     Foo uuid.UUID\n 14 │     Bar *Nested `header:\"X-Bar\"`\n    ⋮         ───┬───\n    ⋮            ╰─ unsupported type\n    ·\n    ·\n 16 │\n 17 │ //encore:api public method=POST\n 18 │ func Str(ctx context.Context, p *Params) error { return nil }\n    ⋮                               ────┬────\n    ⋮                                   ╰─ used here\n 19 │\n────╯\n\nSee https://encore.dev/docs/develop/api-schemas#supported-types for more information.\n"
  },
  {
    "path": "v2/app/testdata/rpc_invalid_path_param_name.txt",
    "content": "! parse\nerr 'unexpected parameter name ''bar'', expected ''foo'' \\(to match path parameter '':foo''\\)'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:api public path=/str/:foo\nfunc Str(ctx context.Context, bar string) error { return nil }\n\n-- want: errors --\n\n── Invalid API Function ───────────────────────────────────────────────────────────────────[E9999]──\n\nUnexpected parameter name \"bar\" expected \"foo\" (to match path parameter \":foo\").\n\n    ╭─[ svc/svc.go:8:31 ]\n    │\n  6 │ )\n  7 │\n  8 │ //encore:api public path=/str/:foo\n    ⋮                               ────\n  9 │ func Str(ctx context.Context, bar string) error { return nil }\n    ⋮                               ──────────\n 10 │\n────╯\n\nhint: valid signatures are:\n\t- func(context.Context) error\n\t- func(context.Context) (*ResponseData, error)\n\t- func(context.Context, *RequestData) error\n\t- func(context.Context, *RequestType) (*ResponseData, error)\n\nFor more information on how to use APIs, see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_invalid_path_param_type.txt",
    "content": "! parse\nerr 'path parameter ''p'' must be a string, .+'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Struct struct{}\n\n//encore:api public path=/str/:p\nfunc Str(ctx context.Context, p *Struct) error { return nil }\n\n-- want: errors --\n\n── Invalid API Function ───────────────────────────────────────────────────────────────────[E9999]──\n\nPath parameter \"p\" must be a string, bool, integer, or encore.dev/types/uuid.UUID.\n\n    ╭─[ svc/svc.go:10:31 ]\n    │\n  8 │ type Struct struct{}\n  9 │\n 10 │ //encore:api public path=/str/:p\n    ⋮                               ──\n 11 │ func Str(ctx context.Context, p *Struct) error { return nil }\n    ⋮                               ─────────\n 12 │\n────╯\n\nhint: valid signatures are:\n\t- func(context.Context) error\n\t- func(context.Context) (*ResponseData, error)\n\t- func(context.Context, *RequestData) error\n\t- func(context.Context, *RequestType) (*ResponseData, error)\n\nFor more information on how to use APIs, see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_invalid_path_too_few_params.txt",
    "content": "! parse\nerr 'invalid API signature: expected function parameters named ''foo'', ''bar'' to match API path params'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:api public path=/str/:foo/:bar\nfunc Str(ctx context.Context) error { return nil }\n\n-- want: errors --\n\n── Invalid API Function ───────────────────────────────────────────────────────────────────[E9999]──\n\nExpected function parameters named 'foo', 'bar' to match Endpoint path params.\n\n    ╭─[ svc/svc.go:8:31 ]\n    │\n  6 │ )\n  7 │\n  8 │ //encore:api public path=/str/:foo/:bar\n    ⋮                               ──── ────\n  9 │ func Str(ctx context.Context) error { return nil }\n    ⋮         ─────────────────────\n 10 │\n────╯\n\nhint: valid signatures are:\n\t- func(context.Context) error\n\t- func(context.Context) (*ResponseData, error)\n\t- func(context.Context, *RequestData) error\n\t- func(context.Context, *RequestType) (*ResponseData, error)\n\nFor more information on how to use APIs, see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_invalid_query_type.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Nested struct {\n    Baz string\n}\n\ntype Params struct {\n    Foo uuid.UUID\n    Bar *Nested\n}\n\n//encore:api public method=GET\nfunc Str(ctx context.Context, p *Params) error { return nil }\n\n-- want: errors --\n\n── Invalid request type ───────────────────────────────────────────────────────────────────[E9999]──\n\nAPI request parameters of type *Nested are not supported in query strings. You can only use\nbuilt-in types, or slices of built-in types such as strings, booleans, int, time.Time.\n\n    ╭─[ svc/svc.go:14:9 ]\n    │\n 12 │ type Params struct {\n 13 │     Foo uuid.UUID\n 14 │     Bar *Nested\n    ⋮         ───┬───\n    ⋮            ╰─ unsupported type\n 15 │ }\n 16 │\n 17 │ //encore:api public method=GET\n    ⋮                     ────┬─────\n    ⋮                         ╰─ you could change this to a POST or PUT request\n 18 │ func Str(ctx context.Context, p *Params) error { return nil }\n    ⋮                               ────┬────\n    ⋮                                   ╰─ used here\n 19 │\n────╯\n\nAPIs which are sent as GET, HEAD or DELETE requests are unable to contain JSON bodies, thus all\nparameters must be sent as query strings or headers. See\nhttps://encore.dev/docs/develop/api-schemas#supported-types for more information.\n"
  },
  {
    "path": "v2/app/testdata/rpc_legacy_syntax.txt",
    "content": "parse\noutput 'svc.API access=private raw=false'\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n// encore:api\nfunc API(ctx context.Context) error { return nil }"
  },
  {
    "path": "v2/app/testdata/rpc_method.txt",
    "content": "parse\noutput 'rpc svc.Str access=public .* recv=\\*Service'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:service\ntype Service struct {}\n\ntype Params struct{}\n\n//encore:api public\nfunc (s *Service) Str(ctx context.Context, p *Params) error { return nil }\n"
  },
  {
    "path": "v2/app/testdata/rpc_non_raw_path.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n//encore:api public path=/foo/bar\nfunc API(ctx context.Context) error { return nil }"
  },
  {
    "path": "v2/app/testdata/rpc_option_types.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/option\"\n)\n\ntype MyParams struct {\n    Query option.Option[string] `query:\"foo\"`\n    Header option.Option[string] `header:\"foo\"`\n    Body option.Option[string]\n}\n\n//encore:api public path=/str\nfunc Str(ctx context.Context, p *MyParams) (*MyParams, error) { return nil, nil }\n"
  },
  {
    "path": "v2/app/testdata/rpc_outside_service.txt",
    "content": "! parse\nerr 'cannot reference API one.One outside of a service'\n\n-- one/one.go --\npackage one\n\nimport (\n    \"context\"\n)\n\n//encore:api public\nfunc One(ctx context.Context) error {\n    return nil\n}\n\n-- two/two.go --\npackage two\n\nimport (\n    \"context\"\n\n    \"test/one\"\n)\n\nfunc Foo(ctx context.Context) error {\n    return one.One(ctx)\n}\n\n-- want: errors --\n\n── Invalid API call ───────────────────────────────────────────────────────────────────────[E9999]──\n\nAPIs can only be called from within a service, the current call site is outside any services within\nthe application.\n\n    ╭─[ two/two.go:10:12 ]\n    │\n  8 │\n  9 │ func Foo(ctx context.Context) error {\n 10 │     return one.One(ctx)\n    ⋮            ───┬───\n    ⋮               ╰─ called here\n 11 │ }\n 12 │\n────╯\n\n    ╭─[ one/one.go:8:6 ]\n    │\n  6 │\n  7 │ //encore:api public\n  8 │ func One(ctx context.Context) error {\n    ⋮      ─┬─\n    ⋮       ╰─ defined here\n  9 │     return nil\n 10 │ }\n────╯\n\nFor more information on how to use APIs see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_path_params.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:api public path=/str/:p\nfunc Str(ctx context.Context, p string) error { return nil }\n\n//encore:api public path=/int/:p\nfunc Int(ctx context.Context, p int) error { return nil }\n\n//encore:api public path=/uuid/:p\nfunc UUID(ctx context.Context, p uuid.UUID) error { return nil }\n"
  },
  {
    "path": "v2/app/testdata/rpc_raw_call.txt",
    "content": "! parse\nerr 'calling raw API endpoint svc.API from another endpoint is not yet supported'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"net/http\"\n)\n\n//encore:api public raw\nfunc API(w http.ResponseWriter, req *http.Request) { }\n\n//encore:api public\nfunc NonRaw(ctx context.Context) error {\n    API(nil, nil)\n    return nil\n}\n\n-- want: errors --\n\n── Invalid API call ───────────────────────────────────────────────────────────────────────[E9999]──\n\nRaw APIs cannot be called from within an Encore application.\n\n    ╭─[ svc/svc.go:9:6 ]\n    │\n  7 │\n  8 │ //encore:api public raw\n  9 │ func API(w http.ResponseWriter, req *http.Request) { }\n    ⋮      ─┬─\n    ⋮       ╰─ defined here\n    ·\n    ·\n 11 │ //encore:api public\n 12 │ func NonRaw(ctx context.Context) error {\n 13 │     API(nil, nil)\n    ⋮     ─┬─\n    ⋮      ╰─ used here\n 14 │     return nil\n 15 │ }\n────╯\n\nhint: valid signatures are:\n\t- func(context.Context) error\n\t- func(context.Context) (*ResponseData, error)\n\t- func(context.Context, *RequestData) error\n\t- func(context.Context, *RequestType) (*ResponseData, error)\n\nFor more information on how to use APIs, see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_raw_custom_path.txt",
    "content": "# Verify that one can use custom paths for raw endpoints\n\nparse\noutput 'rpc svc.API access=public raw=true path=/foo'\n\n-- svc/svc.go --\npackage svc\n\nimport \"net/http\"\n\n//encore:api public raw path=/foo\nfunc API(w http.ResponseWriter, req *http.Request) { }"
  },
  {
    "path": "v2/app/testdata/rpc_raw_duplicate_path.txt",
    "content": "# Verify that one can use custom paths for raw endpoints\n\n! parse\nerr 'invalid API path: cannot combine path segment ''svc.Bar'' with path ''/:foo'''\n\n-- svc/svc.go --\npackage svc\n\nimport \"net/http\"\n\n//encore:api public raw path=/:foo\nfunc Foo(w http.ResponseWriter, req *http.Request) { }\n\n//encore:api public raw\nfunc Bar(w http.ResponseWriter, req *http.Request) { }\n\n-- want: errors --\n\n── Path Conflict ──────────────────────────────────────────────────────────────────────────[E9999]──\n\nThe path segment `foo` conflicts with the path `/fakesvcfortest.TestFunc`.\n\n    ╭─[ fakesvcfortest/test.go:7:6 ]\n    │\n  5 │\n  6 │ //encore:api public\n  7 │ func TestFunc(ctx context.Context) error { return nil }\n    ⋮      ────────\n────╯\n\n   ╭─[ svc/svc.go:5:31 ]\n   │\n 3 │ import \"net/http\"\n 4 │\n 5 │ //encore:api public raw path=/:foo\n   ⋮                               ────\n 6 │ func Foo(w http.ResponseWriter, req *http.Request) { }\n 7 │\n───╯\n\nPaths must be not be empty and always start with a '/'. You cannot define paths that conflict with\neach other, including static and parameterized paths. For example `/blog/:id` would conflict with\n`/:username`.\n\nFor more information about configuring Paths, see https://encore.dev/docs/primitives/apis#rest-apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_raw_internal.txt",
    "content": "# Verify that one cannot use internal and raw RPCs\n\n! parse\nerr 'private APIs cannot be declared raw'\n\n-- svc/svc.go --\npackage svc\n\nimport \"net/http\"\n\n//encore:api raw\nfunc API(w http.ResponseWriter, req *http.Request) { }\n\n-- want: errors --\n\n── Invalid API Directive ──────────────────────────────────────────────────────────────────[E9999]──\n\nPrivate APIs cannot be declared as raw endpoints.\n\n   ╭─[ svc/svc.go:5:14 ]\n   │\n 3 │ import \"net/http\"\n 4 │\n 5 │ //encore:api raw\n   ⋮              ─┬─\n   ⋮               ╰─ declared as raw here\n 6 │ func API(w http.ResponseWriter, req *http.Request) { }\n 7 │\n───╯\n\nhint: valid signatures are:\n\t- func(context.Context) error\n\t- func(context.Context) (*ResponseData, error)\n\t- func(context.Context, *RequestData) error\n\t- func(context.Context, *RequestType) (*ResponseData, error)\n\nFor more information on how to use APIs, see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/rpc_raw_public.txt",
    "content": "# Verify that one can use public and raw RPCs\n\nparse\noutput 'rpc svc.API access=public raw=true'\n\n-- svc/svc.go --\npackage svc\n\nimport \"net/http\"\n\n//encore:api public raw\nfunc API(w http.ResponseWriter, req *http.Request) { }"
  },
  {
    "path": "v2/app/testdata/rpc_receiver_invalid.txt",
    "content": "! parse\nerr 'type SomeOtherType is not defined as an encore:service struct'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:service\ntype Service struct {}\n\ntype Params struct{}\n\ntype SomeOtherType struct {}\n\n//encore:api public\nfunc (s *SomeOtherType) Str(ctx context.Context, p *Params) error { return nil }\n\n-- want: errors --\n\n── Invalid service struct for API ─────────────────────────────────────────────────────────[E9999]──\n\nAPI endpoints defined as receiver functions must be defined on a service struct.\n\n    ╭─[ svc/svc.go:9:6 ]\n    │\n  7 │\n  8 │ //encore:service\n  9 │ type Service struct {}\n    ⋮      ───┬───\n    ⋮         ╰─ this is the service struct for this service\n    ·\n    ·\n 14 │\n 15 │ //encore:api public\n 16 │ func (s *SomeOtherType) Str(ctx context.Context, p *Params) error { return nil }\n    ⋮      ────────┬─────────\n    ⋮              ╰─ try changing this to `*Service`\n 17 │\n────╯\n\nFor more information on service structs, see\nhttps://encore.dev/docs/primitives/services-and-apis/service-structs\n"
  },
  {
    "path": "v2/app/testdata/rpc_receiver_typo.txt",
    "content": "! parse\nerr 'undefined type: Serviice'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:service\ntype Service struct {}\n\ntype Params struct{}\n\n//encore:api public\nfunc (s *Serviice) Str(ctx context.Context, p *Params) error { return nil }\n\n-- want: errors --\n\n── Unknown identifier ─────────────────────────────────────────────────────────────────────[E9999]──\n\nUnknown identifier `Serviice`\n\n    ╭─[ svc/svc.go:14:10 ]\n    │\n 12 │\n 13 │ //encore:api public\n 14 │ func (s *Serviice) Str(ctx context.Context, p *Params) error { return nil }\n    ⋮          ────────\n 15 │\n────╯\n\nFor more information, see https://encore.dev/docs/develop/api-schemas\n"
  },
  {
    "path": "v2/app/testdata/rpc_without_calling.txt",
    "content": "# Verify that not calling an endpoint is caught by the parser\n! parse\nerr 'cannot reference API endpoint one.One without calling it'\n\n-- one/one.go --\npackage one\n\nimport (\n    \"context\"\n)\n\n//encore:api public\nfunc One(ctx context.Context) error {\n    return nil\n}\n\n-- two/two.go --\npackage two\n\nimport (\n    \"context\"\n\n    \"test/one\"\n)\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    f := one.One\n    f()\n    return nil\n}\n\n-- want: errors --\n\n── Invalid API Usage ──────────────────────────────────────────────────────────────────────[E9999]──\n\nAPIs can not be referenced without being called, unless they are used as a cron job endpoint, or a\nPubSub subscription handler.\n\n    ╭─[ two/two.go:11:10 ]\n    │\n  9 │ //encore:api public\n 10 │ func Foo(ctx context.Context) error {\n 11 │     f := one.One\n    ⋮          ───┬───\n    ⋮             ╰─ used here\n 12 │     f()\n 13 │     return nil\n────╯\n\n    ╭─[ one/one.go:8:6 ]\n    │\n  6 │\n  7 │ //encore:api public\n  8 │ func One(ctx context.Context) error {\n    ⋮      ─┬─\n    ⋮       ╰─ defined here\n  9 │     return nil\n 10 │ }\n────╯\n\nFor more information on how to use APIs see https://encore.dev/docs/primitives/apis\n"
  },
  {
    "path": "v2/app/testdata/secrets.txt",
    "content": "# Verify that secrets are parsed successfully\n\nparse\n\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\nvar secrets struct {\n    Foo string\n}\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/secrets_non_string.txt",
    "content": "# Verify that secret fields are of type string\n\n! parse\nerr 'field Foo is not of type string'\n\n-- svc/svc.go --\npackage svc\n\nvar secrets struct {\n    Foo int\n}\n\n-- want: errors --\n\n── Invalid secrets struct ─────────────────────────────────────────────────────────────────[E9999]──\n\nSecrets must be of type string.\n\n   ╭─[ svc/svc.go:4:9 ]\n   │\n 2 │\n 3 │ var secrets struct {\n 4 │     Foo int\n   ⋮         ─┬─\n   ⋮          ╰─ got int\n 5 │ }\n 6 │\n───╯\n\nFor more information about how to use secrets, see https://encore.dev/docs/primitives/secrets\n"
  },
  {
    "path": "v2/app/testdata/servicestruct_creates_service.txt",
    "content": "# Verify that a service struct forces the creation of a service\n# even if it has no API's defined.\n\nparse\noutput 'svc foobar '\n\n-- foobar/svc.go --\npackage foobar\n\nimport (\n\t\"context\"\n)\n\n//encore:service\ntype Service struct {}\n"
  },
  {
    "path": "v2/app/testdata/servicestruct_duplicate.txt",
    "content": "! parse\nerr 'duplicate encore:service directive'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/types/uuid\"\n)\n\n//encore:service\ntype Service struct {}\n\n//encore:service\ntype Duplicate struct {}\n\ntype Params struct{}\n\n//encore:api public\nfunc (s *Service) Str(ctx context.Context, p *Params) error { return nil }\n\n-- want: errors --\n\n── Multiple service structs found ─────────────────────────────────────────────────────────[E9999]──\n\nMultiple service structs were found in the same service. Encore only allows one service struct to\nbe defined per service.\n\n    ╭─[ svc/svc.go:9:6 ]\n    │\n  7 │\n  8 │ //encore:service\n  9 │ type Service struct {}\n    ⋮      ────────┬────────\n    ⋮              ╰─ first service struct defined here\n 10 │\n 11 │ //encore:service\n 12 │ type Duplicate struct {}\n    ⋮      ─────────┬─────────\n    ⋮               ╰─ second service struct defined here\n 13 │\n 14 │ type Params struct{}\n────╯\n\nFor more information on service structs, see\nhttps://encore.dev/docs/primitives/services-and-apis/service-structs\n"
  },
  {
    "path": "v2/app/testdata/servicestruct_ref.txt",
    "content": "! parse\nerr 'cannot reference encore:service struct type svc.Service from another service'\n\n-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n)\n\n//encore:service\ntype Service struct {}\n\n//encore:api public\nfunc (*Service) Foo(ctx context.Context) error { return nil }\n\n-- svc2/svc2.go --\npackage svc2\n\nimport (\n\t\"context\"\n\n\t\"test/svc\"\n)\n\n//encore:api public\nfunc Bar(ctx context.Context) error { return nil }\n\nfunc Foo(s *svc.Service) {}\n\n-- want: errors --\n\n── Service struct referenced in another service ───────────────────────────────────────────[E9999]──\n\nService structs cannot be referenced in other services. They can only be referenced in the service\nthat defines them.\n\n    ╭─[ svc2/svc2.go:12:13 ]\n    │\n 10 │ func Bar(ctx context.Context) error { return nil }\n 11 │\n 12 │ func Foo(s *svc.Service) {}\n    ⋮             ─────┬─────\n    ⋮                  ╰─ referenced in service \"svc2\"\n 13 │\n────╯\n\n    ╭─[ svc/svc.go:8:6 ]\n    │\n  6 │\n  7 │ //encore:service\n  8 │ type Service struct {}\n    ⋮      ───┬───\n    ⋮         ╰─ defined in service \"svc\"\n  9 │\n 10 │ //encore:api public\n────╯\n\nFor more information on service structs, see\nhttps://encore.dev/docs/primitives/services-and-apis/service-structs\n"
  },
  {
    "path": "v2/app/testdata/sqldb_cross_service.txt",
    "content": "parse\noutput 'svc svcb dbs=svca'\n\n-- svca/migrations/1_foo.up.sql --\n-- svca/svca.go --\npackage svca\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"svca\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n-- svcb/svcb.go --\npackage svcb\n\nimport (\n    \"context\"\n\n    \"test/svca\"\n)\n\n//encore:api public\nfunc Bar(ctx context.Context) error {\n    _ = svca.Moo.Query()\n    return nil\n}\n"
  },
  {
    "path": "v2/app/testdata/sqldb_err_unknown_db.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"unknown-db\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n-- want: errors --\n\n── Unknown sqldb database ─────────────────────────────────────────────────────────────────[E9999]──\n\nNo database named \"unknown-db\" was found in the application. Ensure it is created somewhere using\nsqldb.NewDatabase to be able to reference it.\n\n    ╭─[ svc/svc.go:9:5 ]\n    │\n  7 │ )\n  8 │\n  9 │ var Moo = sqldb.Named(\"unknown-db\")\n    ⋮     ───────────────────────────────\n 10 │\n 11 │ //encore:api public\n────╯\n\nFor more information about how to use databases in Encore, see\nhttps://encore.dev/docs/primitives/databases\n"
  },
  {
    "path": "v2/app/testdata/sqldb_err_unknown_db_stdlib.txt",
    "content": "! parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"unknown-db\").Stdlib()\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n-- want: errors --\n\n── Unknown sqldb database ─────────────────────────────────────────────────────────────────[E9999]──\n\nNo database named \"unknown-db\" was found in the application. Ensure it is created somewhere using\nsqldb.NewDatabase to be able to reference it.\n\nFor more information about how to use databases in Encore, see\nhttps://encore.dev/docs/primitives/databases\n"
  },
  {
    "path": "v2/app/testdata/sqldb_helper.txt",
    "content": "parse\noutput 'svc svc dbs=svc'\noutput 'resource SQLDBResource svc.Moo db=svc'\n\n-- svc/migrations/1_dummy.up.sql --\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"test/pkg\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"svc\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    pkg.Foo(Moo)\n    return nil\n}\n-- pkg/pkg.go --\npackage pkg\n\nimport (\n    \"context\"\n    \n    \"encore.dev/storage/sqldb\"\n)\n\nfunc Foo(db *sqldb.Database) {\n    _ = db.Query\n}\n"
  },
  {
    "path": "v2/app/testdata/sqldb_outside_ref.txt",
    "content": "! parse\nerr 'cannot reference resource svc.Moo outside the service'\n\n-- svc/migrations/1_dummy.up.sql --\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"svc\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n-- pkg/pkg.go --\npackage pkg\n\nimport (\n    \"context\"\n    \"test/svc\"\n)\n\nfunc Foo() {\n    _ = svc.Moo.Query\n}\n-- want: errors --\n\n── Invalid resource usage ─────────────────────────────────────────────────────────────────[E9999]──\n\nInfrastructure resources can only be referenced within services.\n\n    ╭─[ pkg/pkg.go:9:9 ]\n    │\n  7 │\n  8 │ func Foo() {\n  9 │     _ = svc.Moo.Query\n    ⋮         ──────┬──────\n    ⋮               ╰─ used here\n 10 │ }\n────╯\n\nTo use infrastructure resources outside services, instead pass a reference to the resource into the\nlibrary.\n"
  },
  {
    "path": "v2/app/testdata/sqldb_outside_svc.txt",
    "content": "parse\n\n-- svc/migrations/1_dummy.up.sql --\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n//encore:api\nfunc Foo(ctx context.Context) error { return nil }\n-- pkg/pkg.go --\npackage pkg\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"svc\")\n"
  },
  {
    "path": "v2/app/testdata/sqldb_outside_svc_test.txt",
    "content": " parse\n\n-- svc/migrations/1_dummy.up.sql --\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\n\n//encore:api\nfunc Foo(ctx context.Context) error { return nil }\n-- pkg/pkg_test.go --\npackage pkg\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"svc\")\n"
  },
  {
    "path": "v2/app/testdata/sqldb_success.txt",
    "content": "parse\noutput 'svc myservice dbs=myservice'\noutput 'resource SQLDBResource myservice.Moo db=myservice'\n\n-- myservice/migrations/1_foo.up.sql --\n-- myservice/myservice.go --\npackage myservice\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"myservice\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n-- myservice/pkg/pkg.go --\npackage pkg\n\nimport (\n    \"context\"\n    \"test/myservice\"\n)\n\nfunc Foo() {\n    _ = myservice.Moo.Baz()\n}\n"
  },
  {
    "path": "v2/app/testdata/sqldb_without_call.txt",
    "content": "parse\n\n-- svc/migrations/1_dummy.up.sql --\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"svc\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    return nil\n}\n-- svc/pkg/pkg.go --\npackage pkg\n\nimport (\n    \"context\"\n    \"test/svc\"\n)\n\nfunc Foo() {\n    _ = svc.Moo\n    _ = svc.Moo.Query\n}\n"
  },
  {
    "path": "v2/app/testdata/struct_duplicate_json_ignore.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \n    \"encore.dev/beta/auth\"\n)\n\ntype SomeStruct struct {\n    A string `json:\"-\"`\n    B string `json:\"-\"`\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *SomeStruct) error {\n    return nil\n}\n\n//encore:api public\nfunc Bar(ctx context.Context) (*SomeStruct, error) {\n    return nil, nil\n}\n"
  },
  {
    "path": "v2/app/testdata/svc_migration_db.txt",
    "content": "parse\noutput 'svc foo dbs=bar,foo,moo'\n\n-- foo/foo.go --\npackage foo\n\nimport (\n    \"context\"\n\n    \"encore.dev/storage/sqldb\"\n)\n\nvar Moo = sqldb.Named(\"moo\")\nvar Bar = sqldb.Named(\"bar\")\n\n//encore:api public\nfunc Foo(ctx context.Context) error {\n    sqldb.QueryRow(ctx, \"\")\n    Moo.Exec(ctx, \"\")\n    Bar.Stdlib()\n    return nil\n}\n-- foo/migrations/1_dummy.up.sql --\n-- moo/migrations/1_dummy.up.sql --\n-- bar/migrations/1_dummy.up.sql --\n-- moo/moo.go --\npackage moo\nimport \"context\"\n//encore:api public\nfunc Dummy(context.Context) error { return nil }\n\n-- bar/bar.go --\npackage bar\nimport \"context\"\n//encore:api public\nfunc Dummy(context.Context) error { return nil }\n"
  },
  {
    "path": "v2/app/testdata/type_ref_non_svc.txt",
    "content": "parse\n\n-- svc/svc.go --\npackage svc\n\nimport (\n    \"context\"\n    \"encore.dev/beta/auth\"\n)\n\ntype FooParams struct {\n    Name string\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *FooParams) error { return nil }\n\n-- pkg/pkg.go --\npackage pkg\n\nimport (\n    \"context\"\n    \"test/svc\"\n)\n\nfunc Test() {\n    _ = &svc.FooParams{}\n}"
  },
  {
    "path": "v2/app/validate.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/infra/caches\"\n\t\"encr.dev/v2/parser/infra/objects\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/infra/secrets\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n)\n\n// validate checks that the application is in a valid state across all services and compilation units.\nfunc (d *Desc) validate(pc *parsectx.Context, result *parser.Result) {\n\tdefer pc.Trace(\"app.validate\").Done()\n\n\t// Validate the framework\n\tif fw, ok := d.Framework.Get(); ok {\n\t\td.validateAuthHandlers(pc, fw)\n\t\td.validateAPIs(pc, fw, result)\n\t\td.validateMiddleware(pc, fw)\n\t\td.validateServiceStructs(pc, result)\n\t}\n\n\t// Validate infrastructure\n\td.validateCaches(pc, result)\n\td.validateConfigs(pc, result)\n\td.validateCrons(pc, result)\n\td.validateDatabases(pc, result)\n\td.validatePubSub(pc, result)\n\td.validateObjects(pc, result)\n\n\t// Validate all resources are defined within a service\n\tfor _, b := range result.AllBinds() {\n\t\tr := result.ResourceForBind(b)\n\t\tswitch r.(type) {\n\t\tcase *pubsub.Topic:\n\t\t\t// We allow pubsub topics to be declared outside of service code\n\t\t\tcontinue\n\t\tcase *objects.Bucket:\n\t\t\t// We allow buckets to be declared outside of service code\n\t\t\tcontinue\n\t\tcase *middleware.Middleware:\n\t\t\t// Middleware is also allowed to be declared outside of service code if it's global (validateMiddleware checks this already)\n\t\t\tcontinue\n\t\tcase *authhandler.AuthHandler:\n\t\t\t// AuthHandlers are also allowed to be declared outside of service code as it's shared code between all services\n\t\t\tcontinue\n\t\tcase *secrets.Secrets:\n\t\t\t// Secrets are allowed anywhere\n\t\t\tcontinue\n\t\tcase *sqldb.Database:\n\t\t\t// Databases are allowed anywhere\n\t\t\tcontinue\n\t\tcase *caches.Cluster:\n\t\t\t// Cache clusters are allowed anywhere\n\t\t\tcontinue\n\n\t\tdefault:\n\t\t\t_, ok := d.ServiceForPath(b.Package().FSPath)\n\n\t\t\t// It's permitted to declare resources in test files\n\t\t\t// or in the main pkg in the case of 'encore alpha exec'.\n\t\t\tmainPkgPath := d.BuildInfo.MainPkg.GetOrElse(\"\")\n\t\t\tinTestFile, inMainPkg := false, false\n\t\t\tif file, ok := b.DeclaredIn().Get(); ok {\n\t\t\t\tif file.TestFile {\n\t\t\t\t\tinTestFile = true\n\t\t\t\t}\n\t\t\t\tif mainPkgPath != \"\" && mainPkgPath.LexicallyContains(file.Pkg.ImportPath) {\n\t\t\t\t\tinMainPkg = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !ok && !inTestFile && !inMainPkg {\n\t\t\t\tpc.Errs.Add(errResourceDefinedOutsideOfService.AtGoNode(r))\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_apis.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/crons\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\nfunc (d *Desc) validateAPIs(pc *parsectx.Context, fw *apiframework.AppDesc, result *parser.Result) {\n\n\tapiPaths := resourcepaths.NewSet()\n\n\tfor _, svc := range d.Services {\n\t\tfwSvc, ok := svc.Framework.Get()\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tsvcStruct, hasSvcStruct := fwSvc.ServiceStruct.Get()\n\n\t\tfor _, ep := range fwSvc.Endpoints {\n\t\t\t// Check if an auth handler is defined for an endpoint that requires auth.\n\t\t\tif ep.Access == api.Auth && fw.AuthHandler.Empty() {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\terrors.AtOptionalNode(authhandler.ErrNoAuthHandlerDefined, ep.AccessField),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// Check for duplicate paths by adding them to the set\n\t\t\t// Note, errors will be reported automatically to pc.Errs\n\t\t\tfor _, method := range ep.HTTPMethods {\n\t\t\t\tapiPaths.Add(pc.Errs, method, ep.Path)\n\t\t\t}\n\n\t\t\tif receiver, ok := ep.Recv.Get(); ok {\n\t\t\t\tif !hasSvcStruct {\n\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\tservicestruct.ErrReceiverNotAServiceStruct.\n\t\t\t\t\t\t\tAtGoNode(receiver.AST, errors.AsError(\"there are no service structs defined in this service\")),\n\t\t\t\t\t)\n\t\t\t\t} else if receiver.Decl != svcStruct.Decl {\n\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\tservicestruct.ErrReceiverNotAServiceStruct.\n\t\t\t\t\t\t\tAtGoNode(receiver.AST, errors.AsError(\n\t\t\t\t\t\t\t\tfmt.Sprintf(\"try changing this to `*%s`\", svcStruct.Decl.Name),\n\t\t\t\t\t\t\t)).\n\t\t\t\t\t\t\tAtGoNode(svcStruct.Decl.AST.Name, errors.AsHelp(\n\t\t\t\t\t\t\t\t\"this is the service struct for this service\",\n\t\t\t\t\t\t\t)),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ep.Raw {\n\t\t\t\tfor _, rawUsage := range result.Usages(ep) {\n\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\tapi.ErrRawEndpointsCannotBeCalled.\n\t\t\t\t\t\t\tAtGoNode(rawUsage, errors.AsError(\"used here\")).\n\t\t\t\t\t\t\tAtGoNode(ep.Decl.AST.Name, errors.AsHelp(\"defined here\")),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// If typed endpoint, validate the types of the request and response\n\t\t\t\tif ep.Request != nil {\n\t\t\t\t\t// The request is always the first parameter after any path params (and after the ctx)\n\t\t\t\t\tfield, _ := schemautil.GetArgument(ep.Decl.AST.Type.Params, len(ep.Path.Params())+1)\n\t\t\t\t\td.validateType(pc, field.Type, ep.Request)\n\t\t\t\t}\n\n\t\t\t\tif ep.Response != nil {\n\t\t\t\t\t// The response is always the first return value\n\t\t\t\t\td.validateType(pc, ep.Decl.AST.Type.Results.List[0].Type, ep.Response)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check for usages outside of services\n\t\t\tfor _, invalidUsage := range d.ResourceUsageOutsideServices[ep] {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\tapi.ErrAPICalledOutsideService.\n\t\t\t\t\t\tAtGoNode(invalidUsage, errors.AsError(\"called here\")).\n\t\t\t\t\t\tAtGoNode(ep.Decl.AST.Name, errors.AsHelp(\"defined here\")),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// Check for invalid references\n\t\t\tfor _, usage := range result.Usages(ep) {\n\t\t\t\tswitch usage := usage.(type) {\n\t\t\t\tcase *api.ReferenceUsage:\n\t\t\t\t\tif usage.File.TestFile {\n\t\t\t\t\t\t// Ignore test files as we allow the MockAPI function to reference the API\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// API's can only be referenced\n\t\t\t\t\tisValid := result.ResourceConstructorContaining(usage).Contains(func(res resource.Resource) bool {\n\t\t\t\t\t\tswitch res := res.(type) {\n\t\t\t\t\t\tcase *pubsub.Subscription:\n\t\t\t\t\t\t\treturn res.Handler == usage.Ref\n\t\t\t\t\t\tcase *crons.Job:\n\t\t\t\t\t\t\treturn res.EndpointAST == usage.Ref\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\treturn false\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\n\t\t\t\t\tif !isValid {\n\t\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\t\tapi.ErrInvalidEndpointUsage.\n\t\t\t\t\t\t\t\tAtGoNode(usage, errors.AsError(\"used here\")).\n\t\t\t\t\t\t\t\tAtGoNode(ep.Decl.AST.Name, errors.AsHelp(\"defined here\")),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_authhandlers.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/parsectx\"\n)\n\nfunc (d *Desc) validateAuthHandlers(pc *parsectx.Context, fw *apiframework.AppDesc) {\n\thandler, found := fw.AuthHandler.Get()\n\tif !found {\n\t\treturn\n\t}\n\n\t// Validate the auth data can be marshalled\n\t// (the same validation we run on request/response types)\n\tif authData, found := handler.AuthData.Get(); found {\n\t\td.validateType(pc, handler.Decl.AST.Type.Results.List[1].Type, authData.ToType())\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_caches.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/infra/caches\"\n)\n\nfunc (d *Desc) validateCaches(pc *parsectx.Context, results *parser.Result) {\n\ttype cache struct {\n\t\tresource *caches.Cluster\n\t\tpaths    *resourcepaths.Set\n\t}\n\tfound := make(map[string]cache)\n\tbyBinding := make(map[pkginfo.QualifiedName]string)\n\n\t// First find all clusters\n\tvar keyspaces []*caches.Keyspace\n\tfor _, res := range d.Parse.Resources() {\n\t\tswitch res := res.(type) {\n\t\tcase *caches.Cluster:\n\t\t\tif existing, ok := found[res.Name]; ok {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\tcaches.ErrDuplicateCacheCluster.\n\t\t\t\t\t\tAtGoNode(existing.resource.AST.Args[0]).\n\t\t\t\t\t\tAtGoNode(res.AST.Args[0]),\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfound[res.Name] = cache{\n\t\t\t\tresource: res,\n\t\t\t\tpaths:    resourcepaths.NewSet(),\n\t\t\t}\n\n\t\t\tfor _, bind := range d.Parse.PkgDeclBinds(res) {\n\t\t\t\tbyBinding[bind.QualifiedName()] = res.Name\n\t\t\t}\n\n\t\tcase *caches.Keyspace:\n\t\t\tkeyspaces = append(keyspaces, res)\n\t\t}\n\t}\n\n\t// Then verify all keyspaces\n\tfor _, ks := range keyspaces {\n\t\tclusterName := byBinding[ks.Cluster]\n\t\tcluster, ok := found[clusterName]\n\t\tif !ok {\n\t\t\tpc.Errs.Add(caches.ErrCouldNotResolveCacheCluster.AtGoNode(ks.AST.Args[0]))\n\t\t\tcontinue\n\t\t}\n\n\t\tcluster.paths.Add(pc.Errs, \"*\", ks.Path)\n\n\t\tsvc, ok := d.ServiceForPath(ks.File.FSPath)\n\t\tif !ok {\n\t\t\tpc.Errs.Add(caches.ErrKeyspaceNotInService.AtGoNode(ks.AST.Fun))\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, use := range results.Usages(ks) {\n\t\t\terrTxt := \"used here\"\n\t\t\tuseSvc, ok := d.ServiceForPath(use.DeclaredIn().FSPath)\n\t\t\tif ok {\n\t\t\t\terrTxt = fmt.Sprintf(\"used in %q\", useSvc.Name)\n\t\t\t}\n\n\t\t\tif useSvc != svc {\n\t\t\t\tpc.Errs.Add(caches.ErrKeyspaceUsedInOtherService.\n\t\t\t\t\tAtGoNode(use, errors.AsError(errTxt)).\n\t\t\t\t\tAtGoNode(ks, errors.AsHelp(fmt.Sprintf(\"declared in %q\", svc.Name))),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_config.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/infra/config\"\n)\n\nfunc (d *Desc) validateConfigs(pc *parsectx.Context, result *parser.Result) {\n\t// validate all config loads\n\tfor _, res := range result.Resources() {\n\t\tswitch res := res.(type) {\n\t\tcase *config.Load:\n\t\t\td.validateConfig(pc, result, res)\n\t\t}\n\t}\n}\n\nfunc (d *Desc) validateConfig(pc *parsectx.Context, result *parser.Result, cfg *config.Load) {\n\t// Verify the config\n\tsvc, ok := d.ServiceForPath(cfg.File.FSPath)\n\tif !ok {\n\t\tpc.Errs.Add(config.ErrConfigUsedOutsideOfService.AtGoNode(cfg))\n\t\treturn\n\t}\n\tif svc.FSRoot != cfg.File.Pkg.FSPath {\n\t\tpc.Errs.Add(config.ErrConfigUsedInSubPackage.AtGoNode(cfg))\n\t}\n\n\t// Verify usages are in the same service\n\tfor _, use := range result.Usages(cfg) {\n\t\tif !use.DeclaredIn().FSPath.HasPrefix(svc.FSRoot) {\n\t\t\tpc.Errs.Add(\n\t\t\t\tconfig.ErrCrossServiceConfigUse.\n\t\t\t\t\tAtGoNode(use, errors.AsError(\"used here\")).\n\t\t\t\t\tAtGoNode(cfg, errors.AsHelp(\"defined here\")),\n\t\t\t)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_crons.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/infra/crons\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\nfunc (d *Desc) validateCrons(pc *parsectx.Context, result *parser.Result) {\n\tfoundCronjobs := make(map[string]*crons.Job)\n\n\tcronjobs := parser.Resources[*crons.Job](result)\n\tfor _, cronjob := range cronjobs {\n\t\tif previous, ok := foundCronjobs[cronjob.Name]; ok {\n\t\t\tpc.Errs.Add(\n\t\t\t\tcrons.ErrDuplicateNames.\n\t\t\t\t\tAtGoNode(cronjob.AST.Args[0]).\n\t\t\t\t\tAtGoNode(previous.AST.Args[0]),\n\t\t\t)\n\t\t}\n\t\tfoundCronjobs[cronjob.Name] = cronjob\n\n\t\tres, ok := result.ResourceForQN(cronjob.Endpoint).Get()\n\t\tif !ok || res.Kind() != resource.APIEndpoint {\n\t\t\tpc.Errs.Add(\n\t\t\t\tcrons.ErrEndpointNotAnAPI.AtGoNode(cronjob.EndpointAST),\n\t\t\t)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_databases.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n)\n\nfunc (d *Desc) validateDatabases(pc *parsectx.Context, result *parser.Result) {\n\tfoundDBs := make(map[string]*sqldb.Database)\n\n\tdbs := parser.Resources[*sqldb.Database](result)\n\tfor _, db := range dbs {\n\t\tif previous, ok := foundDBs[db.Name]; ok {\n\t\t\tpc.Errs.Add(\n\t\t\t\tsqldb.ErrDuplicateNames.\n\t\t\t\t\tAtGoNode(db.AST.Args[0]).\n\t\t\t\t\tAtGoNode(previous.AST.Args[0]),\n\t\t\t)\n\t\t}\n\t\tfoundDBs[db.Name] = db\n\t}\n\n\t// Check for usages outside of services\n\tfor _, db := range dbs {\n\t\tfor _, u := range d.ResourceUsageOutsideServices[db] {\n\t\t\tif !u.DeclaredIn().TestFile {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\terrResourceUsedOutsideService.AtGoNode(u, errors.AsError(\"used here\")),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_middleware.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/selector\"\n)\n\nfunc (d *Desc) validateMiddleware(pc *parsectx.Context, fw *apiframework.AppDesc) {\n\tvar allTags selector.Set\n\n\tfor _, svc := range d.Services {\n\t\tvar svcTags selector.Set\n\n\t\tif fwSvc, ok := svc.Framework.Get(); ok {\n\t\t\t// Collect all tags used in the service.\n\t\t\tfor _, ep := range fwSvc.Endpoints {\n\t\t\t\tsvcTags.Merge(ep.Tags)\n\t\t\t}\n\n\t\t\t// Check that service middleware targets are valid.\n\t\t\tfor _, m := range fwSvc.Middleware {\n\t\t\t\tm.Target.ForEach(func(s selector.Selector) {\n\t\t\t\t\tif s.Type == selector.Tag && !svcTags.Contains(s) {\n\t\t\t\t\t\tpc.Errs.Add(middleware.ErrInvalidTargetForService(svc.Name).AtGoNode(s))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tallTags.Merge(svcTags)\n\t}\n\n\tfor _, m := range fw.GlobalMiddleware {\n\t\tm.Target.ForEach(func(s selector.Selector) {\n\t\t\tif s.Type == selector.Tag && !allTags.Contains(s) {\n\t\t\t\tpc.Errs.Add(middleware.ErrInvalidTargetForApp.AtGoNode(s))\n\t\t\t}\n\t\t})\n\n\t\tsvc, ok := d.ServiceForPath(m.File.FSPath)\n\t\tif ok {\n\t\t\tpc.Errs.Add(middleware.ErrGlobalMiddlewareDefinedInService.AtGoNode(m.Decl.AST.Name, errors.AsError(fmt.Sprintf(\"defined in service %q\", svc.Name))))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_objects.go",
    "content": "package app\n\nimport (\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/infra/objects\"\n)\n\nfunc (d *Desc) validateObjects(pc *parsectx.Context, result *parser.Result) {\n\tbuckets := make(map[string]*objects.Bucket)\n\n\tfor _, res := range d.Parse.Resources() {\n\t\tswitch res := res.(type) {\n\t\tcase *objects.Bucket:\n\t\t\tif existing, ok := buckets[res.Name]; ok {\n\t\t\t\tpc.Errs.Add(objects.ErrBucketNameNotUnique.\n\t\t\t\t\tAtGoNode(existing.AST.Args[0], errors.AsHelp(\"originally defined here\")).\n\t\t\t\t\tAtGoNode(res.AST.Args[0], errors.AsError(\"duplicated here\")),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tbuckets[res.Name] = res\n\t\t\t}\n\n\t\t\t// Make sure any BucketRef calls are within a service.\n\t\t\tfor _, use := range d.Parse.Usages(res) {\n\t\t\t\tswitch use := use.(type) {\n\t\t\t\tcase *objects.RefUsage:\n\t\t\t\t\tif use.HasPerm(objects.GetPublicURL) && !res.Public {\n\t\t\t\t\t\tpc.Errs.Add(objects.ErrBucketNotPublic.\n\t\t\t\t\t\t\tAtGoNode(use, errors.AsError(\"used here\")))\n\t\t\t\t\t}\n\t\t\t\t\terrTxt := \"used here\"\n\t\t\t\t\tif _, ok := d.ServiceForPath(use.DeclaredIn().FSPath); !ok && !use.DeclaredIn().TestFile {\n\t\t\t\t\t\tpc.Errs.Add(objects.ErrBucketRefOutsideService.\n\t\t\t\t\t\t\tAtGoNode(use, errors.AsError(errTxt)),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\n\t\t\t\tcase *objects.MethodUsage:\n\t\t\t\t\tif use.Perm == objects.GetPublicURL && !res.Public {\n\t\t\t\t\t\tpc.Errs.Add(objects.ErrBucketNotPublic.\n\t\t\t\t\t\t\tAtGoNode(use, errors.AsError(\"used here\")))\n\t\t\t\t\t}\n\n\t\t\t\t\terrTxt := \"used here\"\n\t\t\t\t\tif _, ok := d.ServiceForPath(use.DeclaredIn().FSPath); !ok && !use.DeclaredIn().TestFile {\n\t\t\t\t\t\tpc.Errs.Add(objects.ErrUnsupportedOperationOutsideService(use.Method).\n\t\t\t\t\t\t\tAtGoNode(use, errors.AsError(errTxt)),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_pubsub.go",
    "content": "package app\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n)\n\nfunc (d *Desc) validatePubSub(pc *parsectx.Context, result *parser.Result) {\n\ttype topic struct {\n\t\tresource *pubsub.Topic\n\t\tsubs     map[string]*pubsub.Subscription\n\t}\n\ttopics := make(map[string]topic)\n\ttopicsByBinding := make(map[pkginfo.QualifiedName]string)\n\tserviceStructs := make(map[paths.Pkg]*servicestruct.ServiceStruct)\n\n\tvar subs []*pubsub.Subscription\n\n\tfor _, res := range d.Parse.Resources() {\n\t\tswitch res := res.(type) {\n\t\tcase *pubsub.Topic:\n\t\t\tif existing, ok := topics[res.Name]; ok {\n\t\t\t\tpc.Errs.Add(pubsub.ErrTopicNameNotUnique.\n\t\t\t\t\tAtGoNode(existing.resource.AST.Args[0], errors.AsHelp(\"originally defined here\")).\n\t\t\t\t\tAtGoNode(res.AST.Args[0], errors.AsError(\"duplicated here\")),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\ttopics[res.Name] = topic{\n\t\t\t\t\tresource: res,\n\t\t\t\t\tsubs:     make(map[string]*pubsub.Subscription),\n\t\t\t\t}\n\n\t\t\t\tfor _, bind := range d.Parse.PkgDeclBinds(res) {\n\t\t\t\t\ttopicsByBinding[bind.QualifiedName()] = res.Name\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Make sure any TopicRef calls are within a service.\n\t\t\tfor _, use := range d.Parse.Usages(res) {\n\t\t\t\tif _, ok := use.(*pubsub.RefUsage); ok {\n\t\t\t\t\terrTxt := \"used here\"\n\t\t\t\t\tif _, ok := d.ServiceForPath(use.DeclaredIn().FSPath); !ok && !use.DeclaredIn().TestFile {\n\t\t\t\t\t\tpc.Errs.Add(pubsub.ErrTopicRefOutsideService.\n\t\t\t\t\t\t\tAtGoNode(use, errors.AsError(errTxt)),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase *pubsub.Subscription:\n\t\t\tsubs = append(subs, res)\n\n\t\tcase *servicestruct.ServiceStruct:\n\t\t\tserviceStructs[res.Decl.File.Pkg.ImportPath] = res\n\t\t}\n\t}\n\n\tfor _, sub := range subs {\n\t\ttopicName := topicsByBinding[sub.Topic]\n\t\ttopic, ok := topics[topicName]\n\t\tif !ok {\n\t\t\tpc.Errs.Add(pubsub.ErrSubscriptionTopicNotResource.AtGoNode(sub.AST.Args[0]))\n\t\t\tcontinue\n\t\t}\n\n\t\tif existing, ok := topic.subs[sub.Name]; ok {\n\t\t\tpc.Errs.Add(pubsub.ErrSubscriptionNameNotUnique.\n\t\t\t\tAtGoNode(existing.AST.Args[1], errors.AsHelp(\"originally defined here\")).\n\t\t\t\tAtGoNode(sub.AST.Args[1], errors.AsError(\"duplicated here\")),\n\t\t\t)\n\t\t} else {\n\t\t\ttopic.subs[sub.Name] = sub\n\t\t}\n\n\t\tsubService, ok := d.ServiceForPath(sub.File.FSPath)\n\t\tif !ok {\n\t\t\tpc.Errs.Add(pubsub.ErrUnableToIdentifyServicesInvolved.AtGoNode(sub, errors.AsError(\"unable to identify service for subscription\")))\n\t\t}\n\n\t\t// Verify the handler is ok\n\t\thandlerIsAPIEndpoint := false\n\t\tif handlerUsage, ok := result.UsageFromNode(sub.Handler).Get(); ok {\n\t\t\tif endpointUsage, ok := handlerUsage.(*api.ReferenceUsage); ok {\n\t\t\t\tep := endpointUsage.Endpoint\n\n\t\t\t\tendpointService, ok := d.ServiceForPath(ep.File.FSPath)\n\t\t\t\tif !ok {\n\t\t\t\t\tpc.Errs.Add(pubsub.ErrUnableToIdentifyServicesInvolved.AtGoNode(ep, errors.AsError(\"unable to identify service for endpoint\")))\n\t\t\t\t}\n\n\t\t\t\tif endpointService != subService {\n\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\tpubsub.ErrSubscriptionHandlerNotDefinedInSameService.\n\t\t\t\t\t\t\tAtGoNode(sub.Handler, errors.AsError(\"handler specified here\")).\n\t\t\t\t\t\t\tAtGoNode(ep.Decl.AST.Name, errors.AsHelp(\"endpoint defined here\")),\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\thandlerIsAPIEndpoint = true\n\t\t\t}\n\t\t}\n\n\t\t// Do we have a method handler?\n\t\tif method, ok := sub.MethodHandler.Get(); ok {\n\t\t\t// Make sure the type is a service struct\n\t\t\tif _, ok := serviceStructs[method.Decl.File.Pkg.ImportPath]; !ok {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\tpubsub.ErrMethodHandlerTypeNotServiceStruct.\n\t\t\t\t\t\tAtGoNode(sub.Handler, errors.AsError(\"handler specified here\")),\n\t\t\t\t)\n\t\t\t} else if method.Decl.File.Pkg.ImportPath != sub.File.Pkg.ImportPath {\n\t\t\t\tpc.Errs.Add(\n\t\t\t\t\tpubsub.ErrMethodHandlerDifferentPackage.\n\t\t\t\t\t\tAtGoNode(sub.Handler, errors.AsError(\"handler specified here\")),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tif !handlerIsAPIEndpoint {\n\t\t\tqn, ok := sub.File.Names().ResolvePkgLevelRef(sub.Handler)\n\t\t\tif ok {\n\t\t\t\tif pkg, ok := result.PackageAt(qn.PkgPath).Get(); ok {\n\t\t\t\t\tfuncName := option.Map(pkg.Names().FuncDecl(qn.Name), func(f *ast.FuncDecl) *ast.Ident {\n\t\t\t\t\t\treturn f.Name\n\t\t\t\t\t}).GetOrElse(nil)\n\n\t\t\t\t\tif svc, ok := d.ServiceForPath(pkg.FSPath); ok {\n\t\t\t\t\t\tif svc != subService {\n\t\t\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\t\t\tpubsub.ErrSubscriptionHandlerNotDefinedInSameService.\n\t\t\t\t\t\t\t\t\tAtGoNode(sub.Handler, errors.AsError(\"handler specified here\")).\n\t\t\t\t\t\t\t\t\tAtGoNode(funcName, errors.AsHelp(\"handler function defined here\")),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\t\tpubsub.ErrSubscriptionHandlerNotDefinedInSameService.\n\t\t\t\t\t\t\t\tAtGoNode(sub.Handler, errors.AsError(\"handler specified here\")).\n\t\t\t\t\t\t\t\tAtGoNode(funcName, errors.AsHelp(\"handler function defined here\")),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\tpubsub.ErrUnableToIdentifyServicesInvolved.\n\t\t\t\t\t\t\tAtGoNode(sub.Handler, errors.AsError(\"unable to identify package for this reference\")),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// this is ok, because it means we're not dealing with an ident or selector - thus\n\t\t\t\t// it can't be a valid function reference and the normal Go compiler will pick this up\n\t\t\t}\n\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_servicestructs.go",
    "content": "package app\n\nimport (\n\t\"fmt\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n)\n\nfunc (d *Desc) validateServiceStructs(pc *parsectx.Context, result *parser.Result) {\n\tfor _, svc := range d.Services {\n\t\tif fwSvc, ok := svc.Framework.Get(); ok {\n\t\t\tif ss, ok := fwSvc.ServiceStruct.Get(); ok {\n\t\t\t\tfor _, use := range result.Usages(ss) {\n\t\t\t\t\trefText := \"referenced here\"\n\t\t\t\t\tuseSvc, ok := d.ServiceForPath(use.DeclaredIn().FSPath)\n\t\t\t\t\tif ok {\n\t\t\t\t\t\trefText = fmt.Sprintf(\"referenced in service %q\", useSvc.Name)\n\t\t\t\t\t}\n\n\t\t\t\t\tif !use.DeclaredIn().FSPath.HasPrefix(svc.FSRoot) && !use.DeclaredIn().TestFile {\n\t\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\t\tservicestruct.ErrServiceStructReferencedInAnotherService.\n\t\t\t\t\t\t\t\tAtGoNode(use, errors.AsError(refText)).\n\t\t\t\t\t\t\t\tAtGoNode(ss.Decl.AST.Name, errors.AsHelp(fmt.Sprintf(\"defined in service %q\", svc.Name))),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/app/validate_test.go",
    "content": "package app\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\tgoregexp \"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"cuelang.org/go/pkg/regexp\"\n\t\"github.com/pkg/diff\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/infra/config\"\n\t\"encr.dev/v2/parser/infra/crons\"\n\t\"encr.dev/v2/parser/infra/metrics\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n)\n\nvar goldenUpdate = flag.Bool(\"golden-update\", os.Getenv(\"GOLDEN_UPDATE\") != \"\", \"update golden files\")\n\nfunc TestValidation(t *testing.T) {\n\ttype testCfg struct {\n\t\tignoreOutputCommand bool\n\t}\n\tt.Parallel()\n\n\tupdate := false\n\tif goldenUpdate != nil && *goldenUpdate {\n\t\tupdate = true\n\t}\n\n\tsourceDir := \"testdata\"\n\n\ttestscript.Run(t, testscript.Params{\n\t\tDir:           sourceDir,\n\t\tUpdateScripts: update,\n\t\tSetup: func(env *testscript.Env) error {\n\t\t\tif err := testutil.TestScriptSetupFunc(env); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tenv.Values[\"stderr\"] = &bytes.Buffer{}\n\t\t\tenv.Values[\"stdout\"] = &bytes.Buffer{}\n\t\t\tenv.Values[\"cfg\"] = &testCfg{}\n\t\t\treturn nil\n\t\t},\n\t\tCmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){\n\t\t\t// The \"parse\" command runs the parser on the given testscript\n\t\t\t// and reports a failure if there are any errors.\n\t\t\t//\n\t\t\t// use like:\n\t\t\t//  - `parse`\n\t\t\t//  - `! parse` (if you want to check that there are errors)\n\t\t\t\"parse\":  parse,\n\t\t\t\"parse2\": parse,\n\n\t\t\t// expectOut is a command that checks the stdout output contains the\n\t\t\t// given regex.\n\t\t\t//\n\t\t\t// It unique to the v2 parser as that has different handling of types, as such\n\t\t\t// if it is used in a testscript, we ignore any following calls to \"output\"\n\t\t\t\"expectOut\": func(ts *testscript.TestScript, neg bool, args []string) {\n\t\t\t\tts.Value(\"cfg\").(*testCfg).ignoreOutputCommand = true\n\n\t\t\t\tstdout := ts.Value(\"stdout\").(*bytes.Buffer)\n\t\t\t\tm, err := regexp.Match(args[0], stdout.String())\n\t\t\t\tif err != nil {\n\t\t\t\t\tts.Fatalf(\"invalid pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !m && !neg {\n\t\t\t\t\tts.Fatalf(\"output does not match %q\", args[0])\n\t\t\t\t} else if m && neg {\n\t\t\t\t\tts.Fatalf(\"output unexpectedly matches %q\", args[0])\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// The \"output\" command checks that the output into stdout that we've collected\n\t\t\t// contains the given regex\n\t\t\t\"output\": func(ts *testscript.TestScript, neg bool, args []string) {\n\t\t\t\tif ts.Value(\"cfg\").(*testCfg).ignoreOutputCommand {\n\t\t\t\t\t// \"expectOut\" was called, so we ignore this command\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tstdout := ts.Value(\"stdout\").(*bytes.Buffer)\n\t\t\t\tm, err := regexp.Match(args[0], stdout.String())\n\t\t\t\tif err != nil {\n\t\t\t\t\tts.Fatalf(\"invalid pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif !m && !neg {\n\t\t\t\t\tts.Fatalf(\"output does not match %q\", args[0])\n\t\t\t\t} else if m && neg {\n\t\t\t\t\tts.Fatalf(\"output unexpectedly matches %q\", args[0])\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// The \"Err\" command is a no-op in the v2 parser, as we used expected errors\n\t\t\t// inside the test files to assert the full error message\n\t\t\t\"err\": func(ts *testscript.TestScript, neg bool, args []string) {},\n\t\t},\n\t})\n}\n\nfunc parse(ts *testscript.TestScript, neg bool, args []string) {\n\tupdate := false\n\tif goldenUpdate != nil && *goldenUpdate {\n\t\tupdate = true\n\t}\n\tsourceDir := \"testdata\"\n\n\tstdout := ts.Value(\"stdout\").(*bytes.Buffer)\n\tprintf := func(format string, args ...interface{}) {\n\t\tstdout.WriteString(fmt.Sprintf(format, args...) + \"\\n\")\n\t}\n\tstderr := ts.Value(\"stderr\").(*bytes.Buffer)\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tif l, ok := perr.IsBailout(err); ok {\n\t\t\t\tts.Fatalf(\"bailout: %v\", l.FormatErrors())\n\t\t\t} else {\n\t\t\t\t// We convert to an srcerrors error so that we can capture the stack\n\t\t\t\te := srcerrors.UnhandledPanic(err)\n\t\t\t\tts.Fatalf(\"panic: %v\", e)\n\t\t\t}\n\t\t}\n\n\t\tts.Logf(\"stdout: %s\", stdout.String())\n\t\tts.Logf(\"stderr: %s\", stderr.String())\n\t}()\n\n\t// Setup the parse context\n\ttc := testutil.NewContextForTestScript(ts, false)\n\ttc.GoModTidy()\n\ttc.GoModDownload()\n\tp := parser.NewParser(tc.Context)\n\n\t// Parse the testscript\n\tparseResult := p.Parse()\n\n\t// ValidateAndDescribe the testscript\n\tdesc := ValidateAndDescribe(tc.Context, parseResult)\n\n\t// If we're expecting parse errors, assert that we have them\n\tif neg {\n\t\tassertGoldenErrors(ts, tc.Errs, sourceDir, update)\n\t}\n\n\t// If we have errors, and we didn't expect them, fail the test\n\t// If we have no errors, and we expected them, fail the test\n\t// Otherwise write any errors to stderr so they can be asserted on\n\tif tc.Errs.Len() > 0 {\n\t\tif !neg {\n\t\t\tts.Fatalf(\"unexpected errors: %s\", tc.Errs.FormatErrors())\n\t\t}\n\n\t\tstderr.WriteString(tc.Errs.FormatErrors())\n\t} else if tc.Errs.Len() == 0 && neg {\n\t\tts.Fatalf(\"expected errors, but none found\")\n\t}\n\n\t// Now write to stdout the description of the parsed app\n\tfor _, svc := range desc.Services {\n\t\tif svc.Name != \"fakesvcfortest\" {\n\t\t\tvar dbNames []string\n\t\t\tfor res := range svc.ResourceUsage {\n\t\t\t\tif db, ok := res.(*sqldb.Database); ok {\n\t\t\t\t\tdbNames = append(dbNames, db.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t\tsort.Strings(dbNames)\n\t\t\tprintf(\"svc %s dbs=%s\", svc.Name, strings.Join(dbNames, \",\"))\n\t\t}\n\t}\n\n\tfor _, svc := range desc.Services {\n\t\tif svc.Name == \"fakesvcfortest\" {\n\t\t\t// this service only exists to suppress the \"no services found error\"\n\t\t\tcontinue\n\t\t}\n\n\t\tsvc.Framework.ForAll(func(fw *apiframework.ServiceDesc) {\n\n\t\t\tfor _, rpc := range fw.Endpoints {\n\t\t\t\tif rpc == nil {\n\t\t\t\t\tts.Fatalf(\"rpc is nil\")\n\t\t\t\t}\n\t\t\t\trecvName := option.Map(rpc.Recv, func(recv *schema.Receiver) string {\n\t\t\t\t\tswitch t := recv.Type.(type) {\n\t\t\t\t\tcase *schema.NamedType:\n\t\t\t\t\t\treturn \"*\" + t.Decl().Name\n\t\t\t\t\tcase schema.NamedType:\n\t\t\t\t\t\treturn \"*\" + t.Decl().Name\n\t\t\t\t\tcase *schema.PointerType:\n\t\t\t\t\t\treturn \"*\" + t.Elem.(*schema.NamedType).Decl().Name\n\t\t\t\t\tcase schema.PointerType:\n\t\t\t\t\t\treturn \"*\" + t.Elem.(schema.NamedType).Decl().Name\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tpanic(fmt.Sprintf(\"a reciver should only be a named type or pointer type: got %T\", t))\n\t\t\t\t\t}\n\t\t\t\t}).GetOrElse(\"\")\n\n\t\t\t\tprintf(\"rpc %s.%s access=%v raw=%v path=%v recv=%v\",\n\t\t\t\t\tsvc.Name, rpc.Name, rpc.Access, rpc.Raw, rpc.Path, recvName,\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n\n\t// First find all the bindings for each topic\n\ttopicsByName := make(map[pkginfo.QualifiedName]*pubsub.Topic)\n\tfor _, res := range desc.Parse.Resources() {\n\t\tswitch res := res.(type) {\n\t\tcase *pubsub.Topic:\n\t\t\tfor _, b := range desc.Parse.PkgDeclBinds(res) {\n\t\t\t\ttopicsByName[b.QualifiedName()] = res\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, res := range desc.Parse.Resources() {\n\t\tswitch res := res.(type) {\n\t\tcase *config.Load:\n\t\t\tsvc, found := desc.ServiceForPath(res.File.FSPath)\n\t\t\tif !found {\n\t\t\t\tts.Fatalf(\"could not find service for path %s\", res.File.FSPath)\n\t\t\t}\n\t\t\tprintf(\"config %s %s\", svc.Name, res.Type)\n\t\tcase *crons.Job:\n\t\t\tprintf(\"cronJob %s title=%q\", res.Name, res.Title)\n\t\tcase *sqldb.Database:\n\t\t\tfor _, b := range desc.Parse.PkgDeclBinds(res) {\n\t\t\t\tprintf(\"resource SQLDBResource %s.%s db=%s\",\n\t\t\t\t\tb.File.Pkg.Name, b.BoundName.Name, res.Name)\n\t\t\t}\n\t\tcase *pubsub.Topic:\n\t\t\tprintf(\"pubsubTopic %s\", res.Name)\n\n\t\t\tfor _, u := range desc.Parse.Usages(res) {\n\t\t\t\tif pub, ok := u.(*pubsub.PublishUsage); ok {\n\t\t\t\t\tif svc, found := desc.ServiceForPath(pub.File.FSPath); found {\n\t\t\t\t\t\tprintf(\"pubsubPublisher %s %s\\n\", res.Name, svc.Name)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif res2, ok := parseResult.ResourceConstructorContaining(u).Get(); ok {\n\t\t\t\t\t\t\tswitch res2 := res2.(type) {\n\t\t\t\t\t\t\tcase *middleware.Middleware:\n\t\t\t\t\t\t\t\tif res2.Global {\n\t\t\t\t\t\t\t\t\tprintf(\"pubsubPublisher middlware %s %s\\n\", res.Name, res2.Decl.Name)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase *pubsub.Subscription:\n\t\t\tsvc, found := desc.ServiceForPath(res.File.FSPath)\n\t\t\tif !found {\n\t\t\t\tts.Fatalf(\"could not find service for path %s\", res.File.FSPath)\n\t\t\t}\n\t\t\tprintf(\"pubsubSubscriber %s %s %s %d %d %d %d %d\",\n\t\t\t\ttopicsByName[res.Topic].Name, res.Name, svc.Name, res.Cfg.AckDeadline,\n\t\t\t\tres.Cfg.MessageRetention, res.Cfg.MaxRetries, res.Cfg.MinRetryBackoff,\n\t\t\t\tres.Cfg.MaxRetryBackoff)\n\t\tcase *metrics.Metric:\n\t\t\tprintf(\"metric %s %s %s %s\", res.Name, strings.ToUpper(res.ValueType.String()), strings.ToUpper(res.Type.String()), res.Labels)\n\t\t}\n\t}\n}\n\nfunc assertGoldenErrors(ts *testscript.TestScript, errs *perr.List, sourceDir string, updateGoldenFiles bool) {\n\t// Read the want: errors file\n\t// allow for it not to exist\n\twantFile := ts.MkAbs(\"want: errors\")\n\tdata, err := os.ReadFile(wantFile)\n\tvar wantErrors string\n\tif err == nil {\n\t\twantErrors = string(data)\n\t}\n\n\t// Build up the \"got errors string\"\n\tvar b strings.Builder\n\terrs.MakeRelative(ts.Getenv(\"WORK\"), \"\")\n\tfor i := 0; i < errs.Len(); i++ {\n\t\terr := *errs.At(i) // Copy the error so we can modify it\n\n\t\t// Remove the stack for the error, as it will change whenever the parser\n\t\t// changes, and that's not what we're testing for\n\t\terr.Stack = nil\n\n\t\t// Remove the code for the error, as it will change whenever the parser\n\t\t// has new errors introduced\n\t\terr.Params.Code = 9999\n\n\t\tif i != 0 {\n\t\t\tb.WriteString(\"\\n\\n\")\n\t\t}\n\n\t\tb.WriteString(err.Error())\n\t}\n\tgotErrors := b.String()\n\n\t// Remove all trailing whitespace for every line\n\tgotErrors = goregexp.MustCompile(`(?m)[ \\t]+$`).ReplaceAllString(gotErrors, \"\")\n\n\t// Ensure there is a single trailing newline\n\tgotErrors = strings.TrimSpace(gotErrors)\n\tif gotErrors != \"\" {\n\t\tgotErrors = \"\\n\" + gotErrors + \"\\n\"\n\t}\n\n\t// The two errors are the same, so we can return\n\tif wantErrors == gotErrors {\n\t\treturn\n\t}\n\n\t// If we're updating the golden files, then write the new file\n\t// and don't fail the test\n\tif updateGoldenFiles {\n\t\ttestutil.UpdateArchiveFile(ts, sourceDir, \"want: errors\", gotErrors)\n\t\treturn\n\t}\n\n\t// pkg/diff is quadratic at the moment.\n\t// If the product of the number of lines in the inputs is too large,\n\t// don't call pkg.Diff at all as it might take tons of memory or time.\n\t// We found one million to be reasonable for an average laptop.\n\tconst maxLineDiff = 1_000_000\n\tif strings.Count(wantErrors, \"\\n\")*strings.Count(gotErrors, \"\\n\") > maxLineDiff {\n\t\tts.Fatalf(\"errors differ (two large to diff)\")\n\t\treturn\n\t}\n\n\tvar sb strings.Builder\n\tif err := diff.Text(\"want: errors\", \"got: errors\", wantErrors, gotErrors, &sb); err != nil {\n\t\tts.Check(err)\n\t}\n\n\tts.Logf(\"%s\", sb.String())\n\tts.Fatalf(\"wanted errors differ from the actual errors\")\n}\n"
  },
  {
    "path": "v2/app/validate_types.go",
    "content": "package app\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n)\n\n// validateType validates the type of a field can be marshalled.\n// according to Encore's requirements.\n//\n// This walks the type recursively and validates the whole thing\nfunc (d *Desc) validateType(pc *parsectx.Context, usedAt ast.Node, typ schema.Type) {\n\t// Convert generic types to their concrete types\n\ttyp = schemautil.ConcretizeGenericType(pc.Errs, typ)\n\n\t// Walk the type recursively\n\tschemautil.Walk(typ, func(t schema.Type) bool {\n\t\tswitch t := t.(type) {\n\t\tcase schema.StructType:\n\t\t\tfor _, field := range t.Fields {\n\t\t\t\tif field.IsAnonymous() {\n\t\t\t\t\t// We don't support anonymous fields anywhere within\n\t\t\t\t\t// Encore types that we need to marshal.\n\t\t\t\t\tpc.Errs.Add(\n\t\t\t\t\t\tapienc.ErrAnonymousFieldsNotSupported.\n\t\t\t\t\t\t\tAtGoNode(field.AST, errors.AsError(\"defined here\")).\n\t\t\t\t\t\t\tAtGoNode(usedAt, errors.AsHelp(\"used here\")),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase schema.FuncType:\n\t\t\tpc.Errs.Add(\n\t\t\t\tapienc.ErrFuncNotSupported.\n\t\t\t\t\tAtGoNode(t.ASTExpr(), errors.AsError(\"defined here\")).\n\t\t\t\t\tAtGoNode(usedAt, errors.AsHelp(\"used here\")),\n\t\t\t)\n\n\t\tcase schema.InterfaceType:\n\t\t\tpc.Errs.Add(\n\t\t\t\tapienc.ErrInterfaceNotSupported.\n\t\t\t\t\tAtGoNode(t.ASTExpr(), errors.AsError(\"defined here\")).\n\t\t\t\t\tAtGoNode(usedAt, errors.AsHelp(\"used here\")),\n\t\t\t)\n\t\t}\n\t\treturn true\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/apigen.go",
    "content": "package apigen\n\nimport (\n\t\"maps\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/authhandlergen\"\n\t\"encr.dev/v2/codegen/apigen/endpointgen\"\n\t\"encr.dev/v2/codegen/apigen/maingen\"\n\t\"encr.dev/v2/codegen/apigen/middlewaregen\"\n\t\"encr.dev/v2/codegen/apigen/servicestructgen\"\n\t\"encr.dev/v2/codegen/apigen/userfacinggen\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n)\n\ntype Params struct {\n\tGen           *codegen.Generator\n\tDesc          *app.Desc\n\tMainModule    *pkginfo.Module\n\tRuntimeModule *pkginfo.Module\n\n\tCompilerVersion string\n\tAppRevision     string\n\tAppUncommitted  bool\n\n\tTest option.Option[codegen.TestConfig]\n\n\tExecScriptMainPkg option.Option[paths.Pkg]\n}\n\nfunc Process(p Params) *config.Static {\n\tgp := maingen.GenParams{\n\t\tGen:               p.Gen,\n\t\tDesc:              p.Desc,\n\t\tMainModule:        p.MainModule,\n\t\tRuntimeModule:     p.RuntimeModule,\n\t\tTest:              p.Test,\n\t\tExecScriptMainPkg: p.ExecScriptMainPkg,\n\n\t\tCompilerVersion: p.CompilerVersion,\n\t\tAppRevision:     p.AppRevision,\n\t\tAppUncommitted:  p.AppUncommitted,\n\n\t\tAPIHandlers:    make(map[*api.Endpoint]*codegen.VarDecl),\n\t\tMiddleware:     make(map[*middleware.Middleware]*codegen.VarDecl),\n\t\tServiceStructs: make(map[*app.Service]*codegen.VarDecl),\n\n\t\t// Set below\n\t\tAuthHandler: option.None[*codegen.VarDecl](),\n\t}\n\n\tif fw, ok := p.Desc.Framework.Get(); ok {\n\n\t\tsvcStructBySvc := make(map[string]*codegen.VarDecl)\n\n\t\tfor _, svc := range p.Desc.Services {\n\t\t\tvar svcStruct option.Option[*codegen.VarDecl]\n\n\t\t\tvar svcMiddleware map[*middleware.Middleware]*codegen.VarDecl\n\t\t\tif svcDesc, ok := svc.Framework.Get(); ok {\n\t\t\t\tif ss, ok := svcDesc.ServiceStruct.Get(); ok {\n\t\t\t\t\tdecl := servicestructgen.Gen(p.Gen, svc, ss)\n\t\t\t\t\tgp.ServiceStructs[svc] = decl\n\t\t\t\t\tsvcStruct = option.Some(decl)\n\t\t\t\t\tsvcStructBySvc[svc.Name] = decl\n\t\t\t\t}\n\n\t\t\t\tsvcMiddleware = middlewaregen.Gen(p.Gen, svcDesc.Middleware, svcStruct)\n\t\t\t\tmaps.Copy(gp.Middleware, svcMiddleware)\n\t\t\t}\n\n\t\t\teps := endpointgen.Gen(p.Gen, p.Desc, svc, svcStruct, svcMiddleware)\n\t\t\tmaps.Copy(gp.APIHandlers, eps)\n\n\t\t\t// Generate user-facing code with the implementation in place.\n\t\t\tuserfacinggen.Gen(p.Gen, svc, svcStruct)\n\t\t}\n\n\t\tgp.AuthHandler = option.Map(fw.AuthHandler, func(ah *authhandler.AuthHandler) *codegen.VarDecl {\n\t\t\tvar svcStruct option.Option[*codegen.VarDecl]\n\t\t\tif svc, ok := p.Desc.ServiceForPath(ah.Decl.File.FSPath); ok {\n\t\t\t\tsvcStruct = option.AsOptional(svcStructBySvc[svc.Name])\n\t\t\t}\n\t\t\treturn authhandlergen.Gen(p.Gen, p.Desc, ah, svcStruct)\n\t\t})\n\n\t\tmws := middlewaregen.Gen(p.Gen, fw.GlobalMiddleware, option.None[*codegen.VarDecl]())\n\t\tmaps.Copy(gp.Middleware, mws)\n\t}\n\n\treturn maingen.Gen(gp)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/apigenutil/apigenutil.go",
    "content": "package apigenutil\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n)\n\n// DecodeHeaders generates code for decoding HTTP headers from the http request\n// given by httpReqExpr and storing the result into the params given by paramsExpr.\nfunc DecodeHeaders(g *Group, httpHeaderExpr, paramExpr *Statement, dec *genutil.TypeUnmarshaller, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\n\tg.Comment(\"Decode headers\")\n\tg.Id(\"h\").Op(\":=\").Add(httpHeaderExpr)\n\tfor _, f := range params {\n\t\tsingleValExpr := Id(\"h\").Dot(\"Get\").Call(Lit(f.WireName))\n\t\tlistValExpr := Id(\"h\").Dot(\"Values\").Call(Lit(f.WireName))\n\t\tdecodeExpr := dec.UnmarshalQueryOrHeader(f.Type, f.WireName, singleValExpr, listValExpr)\n\t\tg.Add(paramExpr.Clone().Dot(f.SrcName).Op(\"=\").Add(decodeExpr))\n\t}\n\tg.Line()\n}\n\n// DecodeQuery is like DecodeHeaders but for query strings.\nfunc DecodeQuery(g *Group, urlValuesExpr, paramExpr *Statement, dec *genutil.TypeUnmarshaller, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\tg.Comment(\"Decode query string\")\n\tg.Id(\"qs\").Op(\":=\").Add(urlValuesExpr)\n\n\tfor _, f := range params {\n\t\tsingleValExpr := Id(\"qs\").Dot(\"Get\").Call(Lit(f.WireName))\n\t\tlistValExpr := Id(\"qs\").Index(Lit(f.WireName))\n\t\tdecodeExpr := dec.UnmarshalQueryOrHeader(f.Type, f.WireName, singleValExpr, listValExpr)\n\t\tg.Add(paramExpr.Clone()).Dot(f.SrcName).Op(\"=\").Add(decodeExpr)\n\t}\n\tg.Line()\n}\n\n// DecodeCookie is like DecodeHeaders but for cookies.\nfunc DecodeCookie(errs *perr.List, g *Group, httpReqExpr, paramExpr *Statement, dec *genutil.TypeUnmarshaller, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\tg.Comment(\"Decode cookies\")\n\n\tcookieType := pkginfo.Q(\"net/http\", \"Cookie\")\n\n\tfor _, f := range params {\n\t\tg.If(List(Id(\"c\"), Id(\"_\")).Op(\":=\").Add(httpReqExpr.Clone()).Dot(\"Cookie\").Call(Lit(f.WireName)).Op(\";\").Id(\"c\").Op(\"!=\").Nil()).BlockFunc(func(g *Group) {\n\t\t\t// Cookies can either be a builtin or a *http.Cookie.\n\t\t\tif builtin, ok := f.Type.(schema.BuiltinType); ok {\n\t\t\t\tdecodeExpr := dec.UnmarshalBuiltin(builtin.Kind, f.WireName, Id(\"c\").Dot(\"Value\"), false)\n\t\t\t\tg.Add(paramExpr.Clone()).Dot(f.SrcName).Op(\"=\").Add(decodeExpr)\n\t\t\t} else if info, ok := schemautil.DerefNamedInfo(f.Type, true); ok && info.QualifiedName() == cookieType {\n\t\t\t\tg.Add(paramExpr.Clone()).Dot(f.SrcName).Op(\"=\").Id(\"c\")\n\t\t\t\tg.Add(dec.IncNonEmpty())\n\t\t\t} else {\n\t\t\t\terrs.Addf(f.Type.ASTExpr().Pos(), \"cannot unmarshal cookie into field of type %s\", f.Type)\n\t\t\t}\n\t\t})\n\t}\n\tg.Line()\n}\n\nconst jsonIterPkg = \"github.com/json-iterator/go\"\n\n// DecodeBody decodes an io.Reader request body into the given parameters.\nfunc DecodeBody(g *Group, ioReaderExpr *Statement, paramsExpr *Statement, dec *genutil.TypeUnmarshaller, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\n\tg.Comment(\"Decode request body\")\n\tg.Id(\"payload\").Op(\":=\").Add(dec.ReadBody(ioReaderExpr))\n\tg.Id(\"iter\").Op(\":=\").Qual(jsonIterPkg, \"ParseBytes\").Call(Id(\"json\"), Id(\"payload\"))\n\tg.Line()\n\n\tg.For(Id(\"iter\").Dot(\"ReadObjectCB\").Call(\n\t\tFunc().Params(Id(\"_\").Op(\"*\").Qual(jsonIterPkg, \"Iterator\"), Id(\"key\").String()).Bool().Block(\n\t\t\tSwitch(Qual(\"strings\", \"ToLower\").Call(Id(\"key\"))).BlockFunc(func(g *Group) {\n\t\t\t\tfor _, f := range params {\n\t\t\t\t\tg.Case(Lit(strings.ToLower(f.WireName))).Block(\n\t\t\t\t\t\tdec.ParseJSON(f.SrcName, Id(\"iter\"), Op(\"&\").Add(paramsExpr.Clone()).Dot(f.SrcName)),\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tg.Default().Block(Id(\"_\").Op(\"=\").Id(\"iter\").Dot(\"SkipAndReturnBytes\").Call())\n\t\t\t}),\n\t\t\tReturn(True()),\n\t\t)).Block(),\n\t)\n\tg.Line()\n}\n\n// EncodeHeaders generates code for encoding HTTP headers into a http.Header map.\nfunc EncodeHeaders(errs *perr.List, g *Group, httpHeaderExpr, paramExpr *Statement, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\n\tg.Line()\n\tg.Comment(\"Encode headers\")\n\tg.Add(httpHeaderExpr.Clone().Op(\"=\").Make(Qual(\"net/http\", \"Header\"), Lit(len(params))))\n\n\tfor _, f := range params {\n\t\tif !schemautil.IsValidHeaderType(f.Type) {\n\t\t\terrs.Addf(f.Type.ASTExpr().Pos(), \"cannot marshal %s to header\", f.Type)\n\t\t\tcontinue\n\t\t}\n\n\t\tstrVals, ok := genutil.MarshalQueryOrHeader(f.Type, paramExpr.Clone().Dot(f.SrcName))\n\t\tif !ok {\n\t\t\terrs.Addf(f.Type.ASTExpr().Pos(), \"cannot marshal %s to header\", f.Type)\n\t\t\tcontinue\n\t\t}\n\n\t\tg.Add(httpHeaderExpr.Clone()).Index(\n\t\t\tQual(\"net/textproto\", \"CanonicalMIMEHeaderKey\").Call(Lit(f.WireName)),\n\t\t).Op(\"=\").Add(strVals)\n\t}\n\n\tg.Line()\n}\n\n// EncodeQuery generates code for encoding URL query values into a url.Values map.\nfunc EncodeQuery(errs *perr.List, g *Group, urlValuesExpr, paramExpr *Statement, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\n\tg.Line()\n\tg.Comment(\"Encode query string\")\n\tg.Add(urlValuesExpr.Clone().Op(\"=\").Make(Qual(\"net/url\", \"Values\"), Lit(len(params))))\n\n\tfor _, f := range params {\n\t\tif !schemautil.IsValidQueryType(f.Type) {\n\t\t\terrs.Addf(f.Type.ASTExpr().Pos(), \"cannot marshal %s to query string\", f.Type)\n\t\t\tcontinue\n\t\t}\n\n\t\tstrVals, ok := genutil.MarshalQueryOrHeader(f.Type, paramExpr.Clone().Dot(f.SrcName))\n\t\tif !ok {\n\t\t\terrs.Addf(f.Type.ASTExpr().Pos(), \"cannot marshal %s to query string\", f.Type)\n\t\t\tcontinue\n\t\t}\n\t\tg.Add(urlValuesExpr.Clone()).Index(Lit(f.WireName)).Op(\"=\").Add(strVals)\n\t}\n\n\tg.Line()\n}\n\n// EncodeBody encodes a request body into the given *jsoniter.Stream.\nfunc EncodeBody(gu *genutil.Helper, g *Group, streamExpr, paramExpr *Statement, params []*apienc.ParameterEncoding) {\n\tif len(params) == 0 {\n\t\treturn\n\t}\n\n\tg.Line()\n\tg.Comment(\"Encode request body\")\n\tg.Add(streamExpr.Clone().Dot(\"WriteObjectStart\").Call())\n\n\tfor i, p := range params {\n\t\twriteBlock := g\n\n\t\t// If this field is omitted when empty, we need to wrap the write in an if statement.\n\t\tif p.OmitEmpty {\n\t\t\tg.If(gu.IsNotJSONEmpty(paramExpr.Clone().Dot(p.SrcName), p.Type)).BlockFunc(func(g *Group) {\n\t\t\t\tg.Comment(fmt.Sprintf(\"%s is set to omitempty, so we need to check if it's empty before writing it\", p.SrcName))\n\t\t\t\twriteBlock = g\n\t\t\t})\n\t\t}\n\n\t\twriteBlock.Add(streamExpr.Clone().Dot(\"WriteObjectField\").Call(Lit(p.WireName)))\n\t\twriteBlock.Add(streamExpr.Clone().Dot(\"WriteVal\").Call(paramExpr.Clone().Dot(p.SrcName)))\n\t\tif i+1 < len(params) {\n\t\t\t// If we're not on the last field, write a comma.\n\t\t\t// we do this within the writeBlock so that we don't write a comma if we're omitting the field.\n\t\t\twriteBlock.Add(streamExpr.Clone().Dot(\"WriteMore\").Call())\n\t\t}\n\t}\n\tg.Add(streamExpr.Clone().Dot(\"WriteObjectEnd\").Call())\n\tg.Line()\n}\n\n// BuildErr returns an expression for returning an encore.dev/beta/errs.Error with the given code and message.\nfunc BuildErr(code, msg string) *Statement {\n\tp := \"encore.dev/beta/errs\"\n\treturn Qual(p, \"B\").Call().Dot(\"Code\").Call(Qual(p, code)).Dot(\"Msg\").Call(Lit(msg)).Dot(\"Err\").Call()\n}\n\n// BuildErrf is like BuildErr but with a format string.\nfunc BuildErrf(code, format string, args ...Code) *Statement {\n\tp := \"encore.dev/beta/errs\"\n\targs = append([]Code{Lit(format)}, args...)\n\treturn Qual(p, \"B\").Call().Dot(\"Code\").Call(Qual(p, code)).Dot(\"Msgf\").Call(args...).Dot(\"Err\").Call()\n}\n"
  },
  {
    "path": "v2/codegen/apigen/authhandlergen/authhandlergen.go",
    "content": "package authhandlergen\n\nimport (\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/apigenutil\"\n\t\"encr.dev/v2/codegen/apigen/typescrub\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n)\n\nfunc Gen(gen *codegen.Generator, appDesc *app.Desc, ah *authhandler.AuthHandler, svcStruct option.Option[*codegen.VarDecl]) *codegen.VarDecl {\n\tf := gen.File(ah.Decl.File.Pkg, \"authhandler\")\n\tenc := apienc.DescribeAuth(gen.Errs, ah.Param)\n\tgu := gen.Util\n\tdesc := f.VarDecl(\"AuthDesc\", ah.Name)\n\n\tsvcName := \"UNKNOWN\"\n\tsvcNum := 0\n\tif svc, ok := appDesc.ServiceForPath(ah.Decl.File.FSPath); ok {\n\t\tsvcName = svc.Name\n\t\tsvcNum = svc.Num\n\t}\n\n\tscrubMode := typescrub.AuthHandler\n\tif gen.Build.DisableSensitiveScrubbing {\n\t\tscrubMode |= typescrub.DisableScrubbing\n\t}\n\treqScrub := gen.TypeScrubber.Compute(ah.Param, scrubMode)\n\n\tdesc.Value(Op(\"&\").Add(apiQ(\"AuthHandlerDesc\")).Types(\n\t\tgu.Type(ah.Param),\n\t).Values(Dict{\n\t\tId(\"Service\"): Lit(svcName),\n\t\tId(\"SvcNum\"):  Lit(svcNum),\n\t\tId(\"DefLoc\"):  Lit(gen.TraceNodes.AuthHandler()),\n\n\t\tId(\"Endpoint\"):          Lit(ah.Name),\n\t\tId(\"HasAuthData\"):       Lit(ah.AuthData.Present()),\n\t\tId(\"DecodeAuth\"):        renderDecodeAuth(gen, f, ah, enc),\n\t\tId(\"AuthHandler\"):       renderAuthHandler(gen, ah, svcStruct),\n\t\tId(\"ScrubRequestPaths\"): typescrub.PathsToJen(reqScrub.Payload),\n\t}))\n\n\tf.Add(Func().Id(\"init\").Params().Block(\n\t\tQual(\"encore.dev/appruntime/apisdk/api\", \"RegisterAuthHandler\").Call(\n\t\t\tdesc.Qual(),\n\t\t),\n\t))\n\n\tif authData, ok := ah.AuthData.Get(); ok {\n\t\tsnippet := Qual(\"encore.dev/appruntime/apisdk/api\", \"RegisterAuthDataType\").\n\t\t\tTypes(gu.Type(authData.ToType())).Call()\n\n\t\tif dstPkg := authData.Decl.File.Pkg; dstPkg.ImportPath == ah.Decl.File.Pkg.ImportPath {\n\t\t\t// It's in the same package as the auth handler; add it to the file we're already generating.\n\t\t\tf.Add(Func().Id(\"init\").Params().Block(snippet))\n\t\t} else {\n\t\t\t// It's a different package; inject a new file.\n\t\t\tgen.File(dstPkg, \"authdata\").Add(Func().Id(\"init\").Params().Block(snippet))\n\t\t}\n\t}\n\n\treturn desc\n}\n\nfunc apiQ(name string) *Statement {\n\treturn Qual(\"encore.dev/appruntime/apisdk/api\", name)\n}\n\nfunc renderDecodeAuth(gen *codegen.Generator, f *codegen.File, ah *authhandler.AuthHandler, enc *apienc.AuthEncoding) *Statement {\n\tgu := gen.Util\n\treturn Func().Params(\n\t\tId(\"httpReq\").Op(\"*\").Qual(\"net/http\", \"Request\"),\n\t).Params(Id(\"params\").Add(gu.Type(ah.Param)), Err().Error()).BlockFunc(func(g *Group) {\n\t\t// Initialize params if it's a pointer so we're always dealing with a valid value\n\t\tif schemautil.IsPointer(ah.Param) {\n\t\t\tg.Id(\"params\").Op(\"=\").Add(gu.Initialize(ah.Param))\n\t\t}\n\n\t\tisLegacyToken := enc.LegacyTokenFormat\n\t\tif isLegacyToken {\n\t\t\tg.If(\n\t\t\t\tId(\"auth\").Op(\":=\").Id(\"httpReq\").Dot(\"Header\").Dot(\"Get\").Call(Lit(\"Authorization\")),\n\t\t\t\tId(\"auth\").Op(\"!=\").Lit(\"\"),\n\t\t\t).Block(\n\t\t\t\tFor(\n\t\t\t\t\tList(Id(\"_\"), Id(\"prefix\")).Op(\":=\").Range().Index(Op(\"...\")).String().Values(Lit(\"Bearer \"), Lit(\"Token \")),\n\t\t\t\t).Block(\n\t\t\t\t\tIf(Qual(\"strings\", \"HasPrefix\").Call(Id(\"auth\"), Id(\"prefix\"))).Block(\n\t\t\t\t\t\tIf(\n\t\t\t\t\t\t\tId(\"params\").Op(\"=\").Id(\"auth\").Index(Id(\"len\").Call(Id(\"prefix\")).Op(\":\")),\n\t\t\t\t\t\t\tId(\"params\").Op(\"!=\").Lit(\"\"),\n\t\t\t\t\t\t).Block(\n\t\t\t\t\t\t\tReturn(Id(\"params\"), Nil()),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t\tg.Return(gu.Zero(ah.Param), apigenutil.BuildErr(\"Unauthenticated\", \"invalid auth param\"))\n\t\t\treturn\n\t\t}\n\n\t\tdec := gu.NewTypeUnmarshaller(\"dec\")\n\t\tg.Add(dec.Init())\n\t\tapigenutil.DecodeHeaders(g, Id(\"httpReq\").Dot(\"Header\"), Id(\"params\"), dec, enc.HeaderParameters)\n\t\tapigenutil.DecodeQuery(g, Id(\"httpReq\").Dot(\"URL\").Dot(\"Query\").Call(), Id(\"params\"), dec, enc.QueryParameters)\n\t\tapigenutil.DecodeCookie(gen.Errs, g, Id(\"httpReq\"), Id(\"params\"), dec, enc.CookieParameters)\n\n\t\tg.If(dec.NumNonEmptyValues().Op(\"==\").Lit(0)).Block(\n\t\t\tReturn(gu.Zero(ah.Param), apigenutil.BuildErr(\"Unauthenticated\", \"missing auth param\")),\n\t\t).Else().If(Err().Op(\":=\").Add(dec.Err()), Err().Op(\"!=\").Nil()).Block(\n\t\t\tReturn(gu.Zero(ah.Param), apigenutil.BuildErrf(\"InvalidArgument\", \"invalid auth param: %v\", Err())),\n\t\t)\n\t\tg.Return(Id(\"params\"), Nil())\n\t})\n}\n\nfunc renderAuthHandler(gen *codegen.Generator, ah *authhandler.AuthHandler, svcStruct option.Option[*codegen.VarDecl]) *Statement {\n\tgu := gen.Util\n\treturn Func().Params(\n\t\tId(\"ctx\").Qual(\"context\", \"Context\"),\n\t\tId(\"params\").Add(gu.Type(ah.Param)),\n\t).Params(Id(\"info\").Qual(\"encore.dev/appruntime/exported/model\", \"AuthInfo\"), Err().Error()).BlockFunc(func(g *Group) {\n\t\t// fnExpr is the expression for the function we want to call,\n\t\t// either just MyRPCName or svc.MyRPCName if we have a service struct.\n\t\tvar fnExpr *Statement\n\n\t\tif ss, ok := svcStruct.Get(); ok && ah.Recv.Present() {\n\t\t\tg.List(Id(\"svc\"), Id(\"initErr\")).Op(\":=\").Add(ss.Qual()).Dot(\"Get\").Call()\n\t\t\tg.If(Id(\"initErr\").Op(\"!=\").Nil()).Block(\n\t\t\t\tReturn(Id(\"info\"), Id(\"initErr\")),\n\t\t\t)\n\t\t\tfnExpr = Id(\"svc\").Dot(ah.Name)\n\t\t} else {\n\t\t\tfnExpr = Qual(ah.Decl.File.Pkg.ImportPath.String(), ah.Name)\n\t\t}\n\n\t\tthreeParams := ah.AuthData.Present()\n\t\tg.ListFunc(func(g *Group) {\n\t\t\tg.Id(\"info\").Dot(\"UID\")\n\t\t\tif threeParams {\n\t\t\t\tg.Id(\"info\").Dot(\"UserData\")\n\t\t\t}\n\t\t\tg.Err()\n\t\t}).Op(\"=\").Add(fnExpr).Call(Id(\"ctx\"), Id(\"params\"))\n\t\tg.Return(Id(\"info\"), Err())\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/authhandlergen/authhandlergen_test.go",
    "content": "package authhandlergen\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/servicestructgen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tah := desc.Framework.MustGet().AuthHandler.MustGet()\n\n\t\tvar svcStruct option.Option[*codegen.VarDecl]\n\t\tif len(desc.Services) > 0 {\n\t\t\tsvc := desc.Services[0]\n\t\t\tif fw, ok := svc.Framework.Get(); ok {\n\t\t\t\tif ss, ok := fw.ServiceStruct.Get(); ok {\n\t\t\t\t\tdecl := servicestructgen.Gen(gen, svc, ss)\n\t\t\t\t\tsvcStruct = option.Some(decl)\n\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tGen(gen, desc, ah, svcStruct)\n\t}\n\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/authhandlergen/testdata/authdata.txt",
    "content": "-- code.go --\npackage code\n\nimport (\"context\"; \"encore.dev/beta/auth\")\n\ntype MyAuthParams struct {\n\tClientID string `header:\"X-Client-ID\"`\n\tAPIKey   string `query:\"key\"`\n}\n\ntype MyAuthData struct {\n    Username string\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, *MyAuthData, error) {\n    return \"\", nil, nil\n}\n-- want:encore_internal__authhandler.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__model \"encore.dev/appruntime/exported/model\"\n\tscrub \"encore.dev/appruntime/exported/scrub\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\terrs \"encore.dev/beta/errs\"\n\t\"net/http\"\n)\n\nvar EncoreInternal_authhandler_AuthDesc_AuthHandler = &__api.AuthHandlerDesc[*MyAuthParams]{\n\tAuthHandler: func(ctx context.Context, params *MyAuthParams) (info __model.AuthInfo, err error) {\n\t\tinfo.UID, info.UserData, err = AuthHandler(ctx, params)\n\t\treturn info, err\n\t},\n\tDecodeAuth: func(httpReq *http.Request) (params *MyAuthParams, err error) {\n\t\tparams = new(MyAuthParams)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Decode headers\n\t\th := httpReq.Header\n\t\tparams.ClientID = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"x-client-id\", h.Get(\"x-client-id\"), false)\n\n\t\t// Decode query string\n\t\tqs := httpReq.URL.Query()\n\t\tparams.APIKey = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"key\", qs.Get(\"key\"), false)\n\n\t\tif dec.NonEmptyValues == 0 {\n\t\t\treturn (*MyAuthParams)(nil), errs.B().Code(errs.Unauthenticated).Msg(\"missing auth param\").Err()\n\t\t} else if err := dec.Error; err != nil {\n\t\t\treturn (*MyAuthParams)(nil), errs.B().Code(errs.InvalidArgument).Msgf(\"invalid auth param: %v\", err).Err()\n\t\t}\n\t\treturn params, nil\n\t},\n\tDefLoc:            uint32(0x0),\n\tEndpoint:          \"AuthHandler\",\n\tHasAuthData:       true,\n\tScrubRequestPaths: []scrub.Path{[]scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"ClientID\\\"\", CaseSensitive: false}}, []scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"APIKey\\\"\", CaseSensitive: false}}},\n\tService:           \"code\",\n\tSvcNum:            1,\n}\n\nfunc init() {\n\t__api.RegisterAuthHandler(EncoreInternal_authhandler_AuthDesc_AuthHandler)\n}\n\nfunc init() {\n\t__api.RegisterAuthDataType[*MyAuthData]()\n}\n"
  },
  {
    "path": "v2/codegen/apigen/authhandlergen/testdata/basic.txt",
    "content": "-- basic.go --\npackage basic\n\nimport (\"context\"; \"encore.dev/beta/auth\")\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, token string) (auth.UID, error) {\n    return \"\", nil\n}\n-- want:encore_internal__authhandler.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__model \"encore.dev/appruntime/exported/model\"\n\terrs \"encore.dev/beta/errs\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nvar EncoreInternal_authhandler_AuthDesc_AuthHandler = &__api.AuthHandlerDesc[string]{\n\tAuthHandler: func(ctx context.Context, params string) (info __model.AuthInfo, err error) {\n\t\tinfo.UID, err = AuthHandler(ctx, params)\n\t\treturn info, err\n\t},\n\tDecodeAuth: func(httpReq *http.Request) (params string, err error) {\n\t\tif auth := httpReq.Header.Get(\"Authorization\"); auth != \"\" {\n\t\t\tfor _, prefix := range [...]string{\"Bearer \", \"Token \"} {\n\t\t\t\tif strings.HasPrefix(auth, prefix) {\n\t\t\t\t\tif params = auth[len(prefix):]; params != \"\" {\n\t\t\t\t\t\treturn params, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn \"\", errs.B().Code(errs.Unauthenticated).Msg(\"invalid auth param\").Err()\n\t},\n\tDefLoc:            uint32(0x0),\n\tEndpoint:          \"AuthHandler\",\n\tHasAuthData:       false,\n\tScrubRequestPaths: nil,\n\tService:           \"basic\",\n\tSvcNum:            1,\n}\n\nfunc init() {\n\t__api.RegisterAuthHandler(EncoreInternal_authhandler_AuthDesc_AuthHandler)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/authhandlergen/testdata/servicestruct.txt",
    "content": "-- code.go --\npackage code\n\nimport (\"context\"; \"encore.dev/beta/auth\")\n\n//encore:service\ntype Service struct{}\n\n//encore:authhandler\nfunc (*Service) AuthHandler(ctx context.Context, token string) (auth.UID, error) {\n    return \"\", nil\n}\n-- want:encore_internal__authhandler.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__model \"encore.dev/appruntime/exported/model\"\n\terrs \"encore.dev/beta/errs\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nvar EncoreInternal_authhandler_AuthDesc_AuthHandler = &__api.AuthHandlerDesc[string]{\n\tAuthHandler: func(ctx context.Context, params string) (info __model.AuthInfo, err error) {\n\t\tsvc, initErr := EncoreInternal_svcstruct_Service.Get()\n\t\tif initErr != nil {\n\t\t\treturn info, initErr\n\t\t}\n\t\tinfo.UID, err = svc.AuthHandler(ctx, params)\n\t\treturn info, err\n\t},\n\tDecodeAuth: func(httpReq *http.Request) (params string, err error) {\n\t\tif auth := httpReq.Header.Get(\"Authorization\"); auth != \"\" {\n\t\t\tfor _, prefix := range [...]string{\"Bearer \", \"Token \"} {\n\t\t\t\tif strings.HasPrefix(auth, prefix) {\n\t\t\t\t\tif params = auth[len(prefix):]; params != \"\" {\n\t\t\t\t\t\treturn params, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn \"\", errs.B().Code(errs.Unauthenticated).Msg(\"invalid auth param\").Err()\n\t},\n\tDefLoc:            uint32(0x0),\n\tEndpoint:          \"AuthHandler\",\n\tHasAuthData:       false,\n\tScrubRequestPaths: nil,\n\tService:           \"code\",\n\tSvcNum:            1,\n}\n\nfunc init() {\n\t__api.RegisterAuthHandler(EncoreInternal_authhandler_AuthDesc_AuthHandler)\n}\n-- want:encore_internal__svcstruct.go --\npackage code\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"code\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/authhandlergen/testdata/struct.txt",
    "content": "-- code.go --\npackage code\n\nimport (\"context\"; \"net/http\"; \"encore.dev/beta/auth\")\n\ntype MyAuthParams struct {\n\tClientID string `header:\"X-Client-ID\"`\n\tAPIKey   string `query:\"key\"`\n\tSessionToken string `cookie:\"session_token\"`\n\tOtherCookie *http.Cookie `cookie:\"other_cookie\"`\n\tIntCookie int `cookie:\"int_cookie\"`\n}\n\n//encore:authhandler\nfunc AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, error) {\n    return \"\", nil\n}\n-- want:encore_internal__authhandler.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__model \"encore.dev/appruntime/exported/model\"\n\tscrub \"encore.dev/appruntime/exported/scrub\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\terrs \"encore.dev/beta/errs\"\n\t\"net/http\"\n)\n\nvar EncoreInternal_authhandler_AuthDesc_AuthHandler = &__api.AuthHandlerDesc[*MyAuthParams]{\n\tAuthHandler: func(ctx context.Context, params *MyAuthParams) (info __model.AuthInfo, err error) {\n\t\tinfo.UID, err = AuthHandler(ctx, params)\n\t\treturn info, err\n\t},\n\tDecodeAuth: func(httpReq *http.Request) (params *MyAuthParams, err error) {\n\t\tparams = new(MyAuthParams)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Decode headers\n\t\th := httpReq.Header\n\t\tparams.ClientID = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"x-client-id\", h.Get(\"x-client-id\"), false)\n\n\t\t// Decode query string\n\t\tqs := httpReq.URL.Query()\n\t\tparams.APIKey = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"key\", qs.Get(\"key\"), false)\n\n\t\t// Decode cookies\n\t\tif c, _ := httpReq.Cookie(\"session_token\"); c != nil {\n\t\t\tparams.SessionToken = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"session_token\", c.Value, false)\n\t\t}\n\t\tif c, _ := httpReq.Cookie(\"other_cookie\"); c != nil {\n\t\t\tparams.OtherCookie = c\n\t\t\tdec.IncNonEmpty()\n\t\t}\n\t\tif c, _ := httpReq.Cookie(\"int_cookie\"); c != nil {\n\t\t\tparams.IntCookie = __etype.UnmarshalOne(dec, __etype.UnmarshalInt, \"int_cookie\", c.Value, false)\n\t\t}\n\n\t\tif dec.NonEmptyValues == 0 {\n\t\t\treturn (*MyAuthParams)(nil), errs.B().Code(errs.Unauthenticated).Msg(\"missing auth param\").Err()\n\t\t} else if err := dec.Error; err != nil {\n\t\t\treturn (*MyAuthParams)(nil), errs.B().Code(errs.InvalidArgument).Msgf(\"invalid auth param: %v\", err).Err()\n\t\t}\n\t\treturn params, nil\n\t},\n\tDefLoc:            uint32(0x0),\n\tEndpoint:          \"AuthHandler\",\n\tHasAuthData:       false,\n\tScrubRequestPaths: []scrub.Path{[]scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"ClientID\\\"\", CaseSensitive: false}}, []scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"APIKey\\\"\", CaseSensitive: false}}, []scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"SessionToken\\\"\", CaseSensitive: false}}, []scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"OtherCookie\\\"\", CaseSensitive: false}}, []scrub.PathEntry{{Kind: scrub.ObjectField, FieldName: \"\\\"IntCookie\\\"\", CaseSensitive: false}}},\n\tService:           \"code\",\n\tSvcNum:            1,\n}\n\nfunc init() {\n\t__api.RegisterAuthHandler(EncoreInternal_authhandler_AuthDesc_AuthHandler)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/api_calls.go",
    "content": "package endpointgen\n\nimport (\n\t\"go/ast\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/apis/api\"\n)\n\nfunc rewriteAPICalls(gen *codegen.Generator, parse *parser.Result, svc *app.Service, ep *api.Endpoint, desc *handlerDesc) {\n\tvar fd *codegen.FuncDecl\n\n\tfor _, u := range parse.Usages(ep) {\n\t\tif call, ok := u.(*api.CallUsage); ok {\n\t\t\t// Generate the wrapper the first time it's needed.\n\t\t\tif fd == nil {\n\t\t\t\tfd = genCallWrapper(gen, svc, ep, desc)\n\t\t\t}\n\n\t\t\trw := gen.Rewrite(call.File)\n\t\t\tif sel, ok := call.Call.Fun.(*ast.SelectorExpr); ok {\n\t\t\t\trw.ReplaceNode(sel.Sel, []byte(fd.Name()))\n\t\t\t} else {\n\t\t\t\trw.ReplaceNode(call.Call.Fun, []byte(fd.Name()))\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc genCallWrapper(gen *codegen.Generator, svc *app.Service, ep *api.Endpoint, handler *handlerDesc) *codegen.FuncDecl {\n\tgu := gen.Util\n\tfw := svc.Framework.MustGet()\n\tf := gen.File(fw.RootPkg, \"apicalls\")\n\tfd := f.FuncDecl(ep.Name)\n\n\ttype param struct {\n\t\tname string\n\t\ttyp  *Statement\n\t}\n\n\tvar params []param\n\taddParam := func(name string, typ *Statement) {\n\t\tparams = append(params, param{name, typ})\n\t\tfd.Params(Id(name).Add(typ.Clone()))\n\t}\n\n\t// Generate parameters\n\tfd.Params(Id(\"ctx\").Qual(\"context\", \"Context\"))\n\tfor idx, param := range ep.Path.Params() {\n\t\taddParam(handler.req.pathParamFieldName(idx), gu.Builtin(param.Pos(), param.ValueType))\n\t}\n\tif ep.Request != nil {\n\t\taddParam(handler.req.reqDataPayloadName(), gu.Type(ep.Request))\n\t}\n\n\t// Generate results\n\tif ep.Response != nil {\n\t\tfd.Results(gu.Type(ep.Response))\n\t}\n\tfd.Results(Error())\n\n\t// Generate body\n\tfd.BodyFunc(func(g *Group) {\n\t\tg.ListFunc(func(g *Group) {\n\t\t\tif ep.Response != nil {\n\t\t\t\tg.Id(\"resp\")\n\t\t\t} else {\n\t\t\t\tg.Id(\"_\")\n\t\t\t}\n\t\t\tg.Err()\n\t\t}).Op(\":=\").Id(handler.desc.Name()).Dot(\"Call\").CallFunc(func(g *Group) {\n\t\t\tg.Add(apiQ(\"NewCallContext\")).Call(Id(\"ctx\"))\n\t\t\tg.Op(\"&\").Id(handler.req.TypeName()).Values(DictFunc(func(d Dict) {\n\t\t\t\tfor _, p := range params {\n\t\t\t\t\td[Id(p.name)] = Id(p.name)\n\t\t\t\t}\n\t\t\t}))\n\t\t})\n\t\tg.If(Err().Op(\"!=\").Nil()).BlockFunc(func(g *Group) {\n\t\t\tif ep.Response != nil {\n\t\t\t\tg.Return(gu.Zero(ep.Response), Err())\n\t\t\t} else {\n\t\t\t\tg.Return(Err())\n\t\t\t}\n\t\t})\n\t\tif ep.Response != nil {\n\t\t\tg.Return(Id(\"resp\"), Nil())\n\t\t} else {\n\t\t\tg.Return(Nil())\n\t\t}\n\t})\n\n\treturn fd\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/endpointgen.go",
    "content": "package endpointgen\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/typescrub\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/selector\"\n)\n\nfunc Gen(gen *codegen.Generator, appDesc *app.Desc, svc *app.Service, svcStruct option.Option[*codegen.VarDecl], svcMiddleware map[*middleware.Middleware]*codegen.VarDecl) map[*api.Endpoint]*codegen.VarDecl {\n\tepMap := make(map[*api.Endpoint]*codegen.VarDecl)\n\n\tif fw, ok := svc.Framework.Get(); ok {\n\t\tf := gen.File(fw.RootPkg, \"api\")\n\n\t\tvar handlers []*handlerDesc\n\t\tfor _, ep := range fw.Endpoints {\n\t\t\thandler := genAPIDesc(gen, f, appDesc, svc, svcStruct, fw, ep, svcMiddleware)\n\t\t\trewriteAPICalls(gen, appDesc.Parse, svc, ep, handler)\n\t\t\tepMap[ep] = handler.desc\n\t\t\thandlers = append(handlers, handler)\n\t\t}\n\n\t\tregisterHandlers(appDesc, f, handlers)\n\t}\n\n\treturn epMap\n}\n\nfunc genAPIDesc(\n\tgen *codegen.Generator, f *codegen.File, appDesc *app.Desc, svc *app.Service, svcStruct option.Option[*codegen.VarDecl],\n\tfw *apiframework.ServiceDesc, ep *api.Endpoint, svcMiddleware map[*middleware.Middleware]*codegen.VarDecl,\n) *handlerDesc {\n\tgu := gen.Util\n\treqDesc := &requestDesc{gu: gen.Util, ep: ep}\n\trespDesc := &responseDesc{gu: gen.Util, ep: ep}\n\thandler := &handlerDesc{\n\t\tgu:        gen.Util,\n\t\tep:        ep,\n\t\tsvcStruct: svcStruct,\n\t\treq:       reqDesc,\n\t\tresp:      respDesc,\n\t}\n\n\tf.Add(reqDesc.TypeDecl())\n\tf.Add(respDesc.TypeDecl())\n\n\tmethods := ep.HTTPMethods\n\tif len(methods) == 1 && methods[0] == \"*\" {\n\t\t// All methods, from https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods\n\t\tmethods = []string{\"GET\", \"HEAD\", \"POST\", \"PUT\", \"DELETE\", \"CONNECT\", \"OPTIONS\", \"TRACE\", \"PATCH\"}\n\t}\n\n\tvar access *Statement\n\tswitch ep.Access {\n\tcase api.Public:\n\t\taccess = apiQ(\"Public\")\n\tcase api.Auth:\n\t\taccess = apiQ(\"RequiresAuth\")\n\tcase api.Private:\n\t\taccess = apiQ(\"Private\")\n\tdefault:\n\t\tgen.Errs.Addf(ep.Decl.AST.Pos(), \"unhandled access type %v\", ep.Access)\n\t}\n\n\tvar scrubMode typescrub.ParseMode\n\tif gen.Build.DisableSensitiveScrubbing {\n\t\tscrubMode |= typescrub.DisableScrubbing\n\t}\n\treqScrub := gen.TypeScrubber.Compute(ep.Request, scrubMode)\n\trespScrub := gen.TypeScrubber.Compute(ep.Response, scrubMode)\n\n\tpos := ep.Decl.AST.Pos()\n\tdesc := f.VarDecl(\"APIDesc\", ep.Name)\n\tdesc.Value(Op(\"&\").Add(apiQ(\"Desc\")).Types(\n\t\treqDesc.Type(),\n\t\trespDesc.Type(),\n\t).Values(Dict{\n\t\tId(\"Service\"):        Lit(svc.Name),\n\t\tId(\"SvcNum\"):         Lit(svc.Num),\n\t\tId(\"Endpoint\"):       Lit(ep.Name),\n\t\tId(\"Methods\"):        gu.GoToJen(pos, methods),\n\t\tId(\"Raw\"):            Lit(ep.Raw),\n\t\tId(\"Fallback\"):       Lit(ep.Path.HasFallback()),\n\t\tId(\"Path\"):           Lit(ep.Path.String()),\n\t\tId(\"RawPath\"):        Lit(rawPath(ep.Path)),\n\t\tId(\"DefLoc\"):         Lit(gen.TraceNodes.Endpoint(ep)),\n\t\tId(\"PathParamNames\"): pathParamNames(ep.Path),\n\t\tId(\"Tags\"):           tagNames(ep.Tags),\n\t\tId(\"Access\"):         access,\n\n\t\tId(\"DecodeReq\"):      reqDesc.DecodeRequest(),\n\t\tId(\"CloneReq\"):       reqDesc.Clone(),\n\t\tId(\"ReqPath\"):        reqDesc.ReqPath(),\n\t\tId(\"ReqUserPayload\"): reqDesc.UserPayload(),\n\n\t\tId(\"AppHandler\"): handler.Typed(),\n\t\tId(\"RawHandler\"): handler.Raw(),\n\t\tId(\"EncodeResp\"): respDesc.EncodeResponse(),\n\t\tId(\"CloneResp\"):  respDesc.Clone(),\n\n\t\tId(\"EncodeExternalReq\"):  reqDesc.EncodeExternalReq(),\n\t\tId(\"DecodeExternalResp\"): respDesc.DecodeExternalResp(),\n\n\t\tId(\"ServiceMiddleware\"):   serviceMiddleware(ep, fw, svcMiddleware),\n\t\tId(\"GlobalMiddlewareIDs\"): globalMiddleware(appDesc, ep),\n\n\t\tId(\"ScrubRequestPaths\"):    typescrub.PathsToJen(reqScrub.Payload),\n\t\tId(\"ScrubRequestHeaders\"):  typescrub.HeadersToJen(reqScrub.Headers),\n\t\tId(\"ScrubResponsePaths\"):   typescrub.PathsToJen(respScrub.Payload),\n\t\tId(\"ScrubResponseHeaders\"): typescrub.HeadersToJen(respScrub.Headers),\n\t}))\n\n\thandler.desc = desc\n\treturn handler\n}\n\nfunc serviceMiddleware(ep *api.Endpoint, fw *apiframework.ServiceDesc, svcMiddleware map[*middleware.Middleware]*codegen.VarDecl) *Statement {\n\treturn Index().Op(\"*\").Add(apiQ(\"Middleware\")).ValuesFunc(func(g *Group) {\n\t\tfor _, mw := range fw.Middleware {\n\t\t\tif mw.Target.ContainsAny(ep.Tags) {\n\t\t\t\tg.Add(svcMiddleware[mw].Qual())\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc globalMiddleware(appDesc *app.Desc, ep *api.Endpoint) *Statement {\n\treturn Index().String().ValuesFunc(func(g *Group) {\n\t\tfor _, mw := range appDesc.MatchingGlobalMiddleware(ep) {\n\t\t\tg.Add(Lit(mw.ID()))\n\t\t}\n\t})\n}\n\nfunc registerHandlers(appDesc *app.Desc, file *codegen.File, handlers []*handlerDesc) {\n\tf := file.Jen\n\tf.Func().Id(\"init\").Params().BlockFunc(func(g *Group) {\n\t\tfor _, h := range handlers {\n\t\t\tg.Qual(\"encore.dev/appruntime/apisdk/api\", \"RegisterEndpoint\").CallFunc(func(g *Group) {\n\t\t\t\tg.Add(h.desc.Qual())\n\n\t\t\t\tg.Add(Id(h.ep.Name))\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc apiQ(name string) *Statement {\n\treturn Qual(\"encore.dev/appruntime/apisdk/api\", name)\n}\n\n// rawPath creates a raw path representation, replacing path parameters\n// with their indices to ensure all httprouter paths use consistent path param names,\n// since otherwise httprouter reports path conflicts.\nfunc rawPath(path *resourcepaths.Path) string {\n\tvar b strings.Builder\n\tnParam := 0\n\tfor _, s := range path.Segments {\n\t\tb.WriteByte('/')\n\n\t\tswitch s.Type {\n\t\tcase resourcepaths.Literal:\n\t\t\tb.WriteString(s.Value)\n\t\t\tcontinue\n\n\t\tcase resourcepaths.Param:\n\t\t\tb.WriteByte(':')\n\t\tcase resourcepaths.Wildcard:\n\t\t\tb.WriteByte('*')\n\t\tcase resourcepaths.Fallback:\n\t\t\t// Fallback paths map to a wildcard route in httprouter terms.\n\t\t\tb.WriteByte('*')\n\t\t}\n\t\tb.WriteString(strconv.Itoa(nParam))\n\t\tnParam++\n\t}\n\treturn b.String()\n}\n\n// pathParamNames yields a []string literal containing the names\n// of the path parameters, in order.\nfunc pathParamNames(path *resourcepaths.Path) Code {\n\tif path.NumParams() == 0 {\n\t\treturn Nil()\n\t}\n\treturn Index().String().ValuesFunc(func(g *Group) {\n\t\tfor _, s := range path.Params() {\n\t\t\tg.Lit(s.Value)\n\t\t}\n\t})\n}\n\n// tagNames yields a []string literal containing the tag names.\nfunc tagNames(tags selector.Set) Code {\n\tif tags.Len() == 0 {\n\t\treturn Nil()\n\t}\n\treturn Index().String().ValuesFunc(func(g *Group) {\n\t\ttags.ForEach(func(sel selector.Selector) {\n\t\t\tif sel.Type == selector.Tag {\n\t\t\t\tg.Lit(sel.Value)\n\t\t\t}\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/endpointgen_test.go",
    "content": "package endpointgen\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/servicestructgen\"\n\t\"encr.dev/v2/codegen/apigen/userfacinggen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tsvc := desc.Services[0]\n\t\tvar svcStruct option.Option[*codegen.VarDecl]\n\t\tif fw, ok := svc.Framework.Get(); ok {\n\t\t\tif ss, ok := fw.ServiceStruct.Get(); ok {\n\t\t\t\tdecl := servicestructgen.Gen(gen, svc, ss)\n\t\t\t\tsvcStruct = option.Some(decl)\n\n\t\t\t}\n\n\t\t\tuserfacinggen.Gen(gen, svc, svcStruct)\n\t\t}\n\t\tGen(gen, desc, svc, svcStruct, nil)\n\t}\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/handlers.go",
    "content": "package endpointgen\n\nimport (\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/parser/apis/api\"\n)\n\ntype handlerDesc struct {\n\tgu        *genutil.Helper\n\tep        *api.Endpoint\n\tsvcStruct option.Option[*codegen.VarDecl]\n\n\treq  *requestDesc\n\tresp *responseDesc\n\tdesc *codegen.VarDecl\n}\n\nfunc (h *handlerDesc) Typed() *Statement {\n\tep := h.ep\n\tif ep.Raw {\n\t\treturn Nil()\n\t}\n\n\treturn Func().Params(\n\t\tId(\"ctx\").Qual(\"context\", \"Context\"),\n\t\th.req.reqDataExpr().Add(h.req.Type()),\n\t).Params(h.resp.Type(), Error()).BlockFunc(func(g *Group) {\n\t\t// fnExpr is the expression for the function we want to call,\n\t\t// either just MyRPCName or svc.MyRPCName if we have a service struct.\n\t\tvar fnExpr *Statement\n\n\t\t// If we have a service struct, initialize it first.\n\t\tif ss, ok := h.svcStruct.Get(); ok && ep.Recv.Present() {\n\t\t\tg.List(Id(\"svc\"), Id(\"initErr\")).Op(\":=\").Add(ss.Qual()).Dot(\"Get\").Call()\n\t\t\tg.If(Id(\"initErr\").Op(\"!=\").Nil()).Block(\n\t\t\t\tReturn(h.resp.zero(), Id(\"initErr\")),\n\t\t\t)\n\t\t\tfnExpr = Id(\"svc\").Dot(ep.Name)\n\t\t} else {\n\t\t\tfnExpr = Id(ep.Name)\n\t\t}\n\n\t\tg.Do(func(s *Statement) {\n\t\t\tif ep.Response != nil {\n\t\t\t\ts.List(Id(\"resp\"), Err())\n\t\t\t} else {\n\t\t\t\ts.Err()\n\t\t\t}\n\t\t}).Op(\":=\").Add(fnExpr).CallFunc(func(g *Group) {\n\t\t\tg.Id(\"ctx\")\n\t\t\tfor _, arg := range h.req.HandlerArgs() {\n\t\t\t\tg.Add(arg)\n\t\t\t}\n\t\t})\n\t\tg.If(Err().Op(\"!=\").Nil()).Block(Return(h.resp.zero(), Err()))\n\n\t\tif ep.Response != nil {\n\t\t\tg.Return(Id(\"resp\"), Nil())\n\t\t} else {\n\t\t\tg.Return(h.resp.zero(), Nil())\n\t\t}\n\t})\n}\n\nfunc (h *handlerDesc) Raw() *Statement {\n\tep := h.ep\n\tif !ep.Raw {\n\t\treturn Nil()\n\t}\n\n\treturn Func().Params(\n\t\tId(\"w\").Qual(\"net/http\", \"ResponseWriter\"),\n\t\tId(\"req\").Op(\"*\").Qual(\"net/http\", \"Request\"),\n\t).BlockFunc(func(g *Group) {\n\t\t// fnExpr is the expression for the function we want to call,\n\t\t// either just MyRPCName or svc.MyRPCName if we have a service struct.\n\t\tvar fnExpr *Statement\n\n\t\t// If we have a service struct, initialize it first.\n\t\tif ss, ok := h.svcStruct.Get(); ok && ep.Recv.Present() {\n\t\t\tg.List(Id(\"svc\"), Id(\"initErr\")).Op(\":=\").Add(ss.Qual()).Dot(\"Get\").Call()\n\t\t\tg.If(Id(\"initErr\").Op(\"!=\").Nil()).Block(\n\t\t\t\tQual(\"encore.dev/beta/errs\", \"HTTPErrorWithCode\").Call(Id(\"w\"), Id(\"initErr\"), Lit(0)),\n\t\t\t\tReturn(),\n\t\t\t)\n\t\t\tfnExpr = Id(\"svc\").Dot(ep.Name)\n\t\t} else {\n\t\t\tfnExpr = Id(ep.Name)\n\t\t}\n\n\t\tg.Add(fnExpr).Call(Id(\"w\"), Id(\"req\"))\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/request.go",
    "content": "package endpointgen\n\nimport (\n\t\"fmt\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/codegen/apigen/apigenutil\"\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n)\n\nconst jsonIterPkg = \"github.com/json-iterator/go\"\n\n// requestDesc describes the generated request type that contains the combined\n// request data + path parameters for the request.\ntype requestDesc struct {\n\tgu *genutil.Helper\n\tep *api.Endpoint\n}\n\nfunc (d *requestDesc) TypeName() string {\n\treturn \"EncoreInternal_\" + d.ep.Name + \"Req\"\n}\n\nfunc (d *requestDesc) Type() *Statement {\n\treturn Op(\"*\").Id(d.TypeName())\n}\n\nfunc (d *requestDesc) TypeDecl() *Statement {\n\treturn Type().Id(d.TypeName()).StructFunc(func(g *Group) {\n\t\tif d.ep.Request != nil {\n\t\t\tg.Id(d.reqDataPayloadName()).Add(d.gu.Type(d.ep.Request))\n\t\t}\n\t\t// Note: the path parameter order is important and must match the order of the segments as defined\n\t\t// as the parameters of the user's endpoint function. This behaviour is expected by the mocking\n\t\t// system - see runtimes/go/appruntime/apisdk/api/reflection.go.\n\t\tfor i, seg := range d.ep.Path.Params() {\n\t\t\tg.Id(d.pathParamFieldName(i)).Add(d.gu.Builtin(d.ep.Decl.AST.Pos(), seg.ValueType))\n\t\t}\n\t})\n}\n\nfunc (d *requestDesc) DecodeRequest() *Statement {\n\treturn Func().Params(\n\t\td.httpReqExpr().Op(\"*\").Qual(\"net/http\", \"Request\"),\n\t\td.pathParamsName().Add(apiQ(\"UnnamedParams\")),\n\t\tId(\"json\").Qual(jsonIterPkg, \"API\"),\n\t).Params(\n\t\td.reqDataExpr().Add(d.Type()),\n\t\tId(\"pathParams\").Add(apiQ(\"UnnamedParams\")),\n\t\tErr().Error(),\n\t).BlockFunc(func(g *Group) {\n\t\tg.Add(d.reqDataExpr()).Op(\"=\").New(Id(d.TypeName()))\n\n\t\tif d.ep.Path.NumParams() == 0 && d.ep.Request == nil {\n\t\t\t// Nothing to do; return an empty struct\n\t\t\tg.Return(d.reqDataExpr(), Nil(), Nil())\n\t\t\treturn\n\t\t}\n\n\t\tdec := d.gu.NewTypeUnmarshaller(\"dec\")\n\t\tg.Add(dec.Init())\n\t\td.renderPathDecoding(g, dec)\n\t\td.renderRequestDecoding(g, dec)\n\n\t\tg.If(Err().Op(\":=\").Add(dec.Err()), Err().Op(\"!=\").Nil()).Block(\n\t\t\tReturn(Nil(), Nil(), Err()),\n\t\t)\n\n\t\tg.Return(d.reqDataExpr(), d.pathParamsName(), Nil())\n\t})\n}\n\n// HandlerArgs returns the list of arguments to pass to the handler.\nfunc (d *requestDesc) HandlerArgs() []Code {\n\tnumPathParams := d.ep.Path.NumParams()\n\targs := make([]Code, 0, 1+numPathParams)\n\tfor i := 0; i < numPathParams; i++ {\n\t\targs = append(args, d.reqDataPathParamExpr(i))\n\t}\n\tif d.ep.Request != nil {\n\t\targs = append(args, d.reqDataPayloadExpr())\n\t}\n\treturn args\n}\n\n// renderPathDecoding renders the code to decode the path parameters.\n// The path parameters are accessible via the `pathParamsExpr` parameter.\n//\n// The generated code writes to the path segment fields in the request struct,\n// which is accessed via the `reqDescExpr` parameter.\nfunc (d *requestDesc) renderPathDecoding(g *Group, dec *genutil.TypeUnmarshaller) {\n\t// Collect all the non-literal path segments, and keep track of the wildcard segment, if any.\n\tsegs := make([]resourcepaths.Segment, 0, len(d.ep.Path.Segments))\n\tseenWildcard := false\n\twildcardIdx := 0\n\tfor _, s := range d.ep.Path.Segments {\n\t\tif s.Type != resourcepaths.Literal {\n\t\t\tsegs = append(segs, s)\n\t\t}\n\t\tif !seenWildcard {\n\t\t\t// Fallback is also considered a wildcard for these purposes.\n\t\t\tif s.Type == resourcepaths.Wildcard || s.Type == resourcepaths.Fallback {\n\t\t\t\tseenWildcard = true\n\t\t\t} else if s.Type == resourcepaths.Param {\n\t\t\t\twildcardIdx++\n\t\t\t}\n\t\t}\n\t}\n\n\tif seenWildcard {\n\t\tg.Comment(\"Trim the leading slash from wildcard parameter, as Encore's semantics excludes it,\")\n\t\tg.Comment(\"while the httprouter implementation includes it.\")\n\t\tg.Add(d.pathSegmentValue(wildcardIdx)).Op(\"=\").Qual(\"strings\", \"TrimPrefix\").Call(\n\t\t\td.pathSegmentValue(wildcardIdx), Lit(\"/\"))\n\t\tg.Line()\n\t}\n\n\t// Decode the path params\n\tfor segIdx, seg := range segs {\n\t\tpathSegmentValue := d.pathSegmentValue(segIdx)\n\n\t\t// If the segment type is a string, then we want to unescape it.\n\t\tswitch seg.ValueType {\n\t\tcase schema.String, schema.UUID:\n\t\t\tg.If(\n\t\t\t\tList(Id(\"value\"), Err()).Op(\":=\").Qual(\"net/url\", \"PathUnescape\").Call(pathSegmentValue),\n\t\t\t\tErr().Op(\"==\").Nil().Block(\n\t\t\t\t\td.pathSegmentValue(segIdx).Op(\"=\").Id(\"value\"),\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tg.Do(func(s *Statement) {\n\t\t\t// If it's a raw endpoint the params are not used, but validate them regardless.\n\t\t\tif d.ep.Raw {\n\t\t\t\ts.Id(\"_\").Op(\"=\")\n\t\t\t} else {\n\t\t\t\ts.Add(d.reqDataPathParamExpr(segIdx).Op(\"=\"))\n\t\t\t}\n\t\t}).Add(dec.UnmarshalBuiltin(seg.ValueType, seg.Value, pathSegmentValue, true))\n\t}\n}\n\n// httpReqExpr returns an expression to access the HTTP request variable\nfunc (d *requestDesc) httpReqExpr() *Statement {\n\treturn Id(\"httpReq\")\n}\n\n// reqDataExpr returns the expression to access the reqData variable.\nfunc (d *requestDesc) reqDataExpr() *Statement {\n\treturn Id(\"reqData\")\n}\n\n// reqDataPayloadName returns the name of the payload field in the reqData struct.\nfunc (d *requestDesc) reqDataPayloadName() string {\n\t// Note: this hardcoded value is used by reflection during mocking.\n\t// If you change this, update runtimes/go/appruntime/apisdk/api/reflection.go as well.\n\treturn \"Payload\"\n}\n\n// httpHeaderExpr returns an expression to access the HTTP header variable,\n// for request encoding.\nfunc (d *requestDesc) httpHeaderExpr() *Statement {\n\treturn Id(\"httpHeader\")\n}\n\n// queryStringExpr returns an expression to access the url.Values query string variable,\n// for request encoding.\nfunc (d *requestDesc) queryStringExpr() *Statement {\n\treturn Id(\"queryString\")\n}\n\n// jsonStream returns an expression to access the *jsoniter.Stream variable,\n// for request encoding.\nfunc (d *requestDesc) jsonStream() *Statement {\n\treturn Id(\"stream\")\n}\n\n// reqDataPayloadExpr returns an expression for accessing the payload\n// in the reqData variable.\nfunc (d *requestDesc) reqDataPayloadExpr() *Statement {\n\treturn d.reqDataExpr().Dot(d.reqDataPayloadName())\n}\n\n// reqDataPathParamExpr returns an expression for accessing the i'th path parameter\n// in the reqData variable.\nfunc (d *requestDesc) reqDataPathParamExpr(i int) *Statement {\n\treturn d.reqDataExpr().Dot(d.pathParamFieldName(i))\n}\n\n// reqDataPathParamName returns the field name for the i'th path parameter\n// in the reqData struct.\nfunc (d *requestDesc) pathParamFieldName(i int) string {\n\treturn fmt.Sprintf(\"P%d\", i)\n}\n\n// pathParamsName renders an expression for the name of the path parameters.\nfunc (d *requestDesc) pathParamsName() *Statement {\n\treturn Id(\"ps\")\n}\n\n// pathSegmentValue renders an expression to retrieve the value (as a string) of the i'th path segment.\nfunc (d *requestDesc) pathSegmentValue(i int) *Statement {\n\treturn d.pathParamsName().Index(Lit(i))\n}\n\nfunc (d *requestDesc) renderRequestDecoding(g *Group, dec *genutil.TypeUnmarshaller) {\n\tif d.ep.Request == nil {\n\t\treturn\n\t}\n\n\tif schemautil.IsPointer(d.ep.Request) {\n\t\tg.Id(\"params\").Op(\":=\").Add(d.gu.Initialize(d.ep.Request))\n\t\tg.Add(d.reqDataPayloadExpr()).Op(\"=\").Id(\"params\")\n\t} else {\n\t\tg.Id(\"params\").Op(\":=\").Op(\"&\").Add(d.reqDataPayloadExpr())\n\t}\n\n\t// Parsing requests for HTTP methods without a body (GET, HEAD, DELETE) are handled by parsing the query string,\n\t// while other methods are parsed by reading the body and unmarshalling it as JSON.\n\t// If the same endpoint supports both, handle it with a switch.\n\treqs := d.ep.RequestEncoding()\n\tif d.gu.Errs.Len() > 0 {\n\t\treturn\n\t}\n\tg.Add(Switch(Id(\"m\").Op(\":=\").Add(d.httpReqExpr()).Dot(\"Method\"), Id(\"m\")).BlockFunc(\n\t\tfunc(g *Group) {\n\t\t\tfor _, r := range reqs {\n\t\t\t\tg.CaseFunc(func(g *Group) {\n\t\t\t\t\tfor _, m := range r.HTTPMethods {\n\t\t\t\t\t\tg.Lit(m)\n\t\t\t\t\t}\n\t\t\t\t}).BlockFunc(func(g *Group) {\n\t\t\t\t\td.decodeRequestParameters(g, dec, r)\n\t\t\t\t})\n\t\t\t}\n\t\t\tg.Default().Add(Id(\"panic\").Call(Lit(\"HTTP method is not supported\")))\n\t\t},\n\t))\n}\n\nfunc (d *requestDesc) decodeRequestParameters(g *Group, dec *genutil.TypeUnmarshaller, req *apienc.RequestEncoding) {\n\tapigenutil.DecodeHeaders(g, d.httpReqExpr().Dot(\"Header\"), Id(\"params\"), dec, req.HeaderParameters)\n\tapigenutil.DecodeQuery(g, d.httpReqExpr().Dot(\"URL\").Dot(\"Query\").Call(), Id(\"params\"), dec, req.QueryParameters)\n\tapigenutil.DecodeBody(g, d.httpReqExpr().Dot(\"Body\"), Id(\"params\"), dec, req.BodyParameters)\n}\n\n// Clone returns the function literal to clone the request.\nfunc (d *requestDesc) Clone() *Statement {\n\tconst recv = \"r\"\n\treturn Func().Params(Id(recv).Add(d.Type())).Params(d.Type(), Error()).BlockFunc(func(g *Group) {\n\t\t// We could optimize the clone operation if there are no reference types (pointers, maps, slices)\n\t\t// in the struct. For now, simply serialize it as JSON and back.\n\t\tg.Var().Id(\"clone\").Add(d.Type())\n\t\tg.List(Id(\"bytes\"), Id(\"err\")).Op(\":=\").Qual(jsonIterPkg, \"ConfigDefault\").Dot(\"Marshal\").Call(Id(recv))\n\t\tg.If(Err().Op(\"==\").Nil()).Block(\n\t\t\tErr().Op(\"=\").Qual(jsonIterPkg, \"ConfigDefault\").Dot(\"Unmarshal\").Call(Id(\"bytes\"), Op(\"&\").Id(\"clone\")),\n\t\t)\n\t\tg.Return(Id(\"clone\"), Err())\n\t})\n}\n\n// ReqPath returns the function literal to compute the request path.\nfunc (d *requestDesc) ReqPath() *Statement {\n\treturn Func().Params(\n\t\td.reqDataExpr().Add(d.Type()),\n\t).Params(\n\t\tString(),\n\t\tapiQ(\"UnnamedParams\"),\n\t\tError(),\n\t).BlockFunc(func(g *Group) {\n\t\tpathParams := d.ep.Path.Params()\n\t\tif len(pathParams) == 0 {\n\t\t\tg.Return(Lit(d.ep.Path.String()), Nil(), Nil())\n\t\t\treturn\n\t\t}\n\n\t\tg.Id(\"params\").Op(\":=\").Add(apiQ(\"UnnamedParams\")).ValuesFunc(func(g *Group) {\n\t\t\tfor paramIdx, f := range pathParams {\n\t\t\t\tg.Add(genutil.MarshalBuiltin(f.ValueType, d.reqDataPathParamExpr(paramIdx)))\n\t\t\t}\n\t\t})\n\n\t\t// Construct the path as an expression in the form\n\t\t//\t\t\"/foo\" + params[N].Value + \"/bar\"\n\t\tpathExpr := CustomFunc(Options{\n\t\t\tSeparator: \" + \",\n\t\t}, func(g *Group) {\n\t\t\tidx := 0\n\t\t\tfor _, seg := range d.ep.Path.Segments {\n\t\t\t\tif seg.Type == resourcepaths.Literal {\n\t\t\t\t\tg.Lit(\"/\" + seg.Value)\n\t\t\t\t} else {\n\t\t\t\t\tg.Lit(\"/\")\n\t\t\t\t\tg.Qual(\"net/url\", \"PathEscape\").Call(Id(\"params\").Index(Lit(idx)))\n\t\t\t\t\tidx++\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\tg.Return(pathExpr, Id(\"params\"), Nil())\n\t})\n}\n\n// UserPayload returns the function literal to compute the user payload.\nfunc (d *requestDesc) UserPayload() *Statement {\n\treturn Func().Params(\n\t\t// input\n\t\td.reqDataExpr().Add(d.Type()),\n\t).Params(\n\t\t// output\n\t\tAny(),\n\t).BlockFunc(func(g *Group) {\n\t\tif d.ep.Request == nil {\n\t\t\tg.Return(Nil())\n\t\t} else {\n\t\t\tg.Return(d.reqDataPayloadExpr())\n\t\t}\n\t})\n}\n\nfunc (d *requestDesc) EncodeExternalReq() *Statement {\n\treturn Func().Params(\n\t\td.reqDataExpr().Add(d.Type()),\n\t\td.jsonStream().Add(Op(\"*\").Qual(jsonIterPkg, \"Stream\")),\n\t).Params(\n\t\td.httpHeaderExpr().Add(Qual(\"net/http\", \"Header\")),\n\t\td.queryStringExpr().Add(Qual(\"net/url\", \"Values\")),\n\t\tErr().Error(),\n\t).BlockFunc(func(g *Group) {\n\t\tif d.ep.Request == nil {\n\t\t\t// Nothing to do.\n\t\t\tg.Return(Nil(), Nil(), Nil())\n\t\t\treturn\n\t\t}\n\n\t\tif schemautil.IsPointer(d.ep.Request) {\n\t\t\tg.Id(\"params\").Op(\":=\").Add(d.reqDataPayloadExpr())\n\n\t\t\tg.If(Id(\"params\").Op(\"==\").Nil()).Block(\n\t\t\t\tComment(\"If the payload is nil, we need to return an empty request body.\"),\n\t\t\t\tReturn(d.httpHeaderExpr(), d.queryStringExpr(), Err()),\n\t\t\t)\n\t\t} else {\n\t\t\tg.Id(\"params\").Op(\":=\").Op(\"&\").Add(d.reqDataPayloadExpr())\n\t\t}\n\n\t\tencodings := d.ep.RequestEncoding()\n\t\tif len(encodings) == 0 {\n\t\t\t// This only happens in the case of an error, which has\n\t\t\t// then already been reported by RequestEncoding.\n\t\t\tg.Return(Nil(), Nil(), Nil())\n\t\t\treturn\n\t\t}\n\n\t\tenc := encodings[0]\n\n\t\tapigenutil.EncodeHeaders(d.gu.Errs, g, d.httpHeaderExpr(), Id(\"params\"), enc.HeaderParameters)\n\t\tapigenutil.EncodeQuery(d.gu.Errs, g, d.queryStringExpr(), Id(\"params\"), enc.QueryParameters)\n\t\tapigenutil.EncodeBody(d.gu, g, d.jsonStream(), Id(\"params\"), enc.BodyParameters)\n\n\t\tg.Return(d.httpHeaderExpr(), d.queryStringExpr(), Err())\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/response.go",
    "content": "package endpointgen\n\nimport (\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/codegen/apigen/apigenutil\"\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n)\n\n// responseDesc describes the generated response type.\ntype responseDesc struct {\n\tgu *genutil.Helper\n\tep *api.Endpoint\n}\n\nfunc (d *responseDesc) TypeName() string {\n\treturn \"EncoreInternal_\" + d.ep.Name + \"Resp\"\n}\n\nfunc (d *responseDesc) Type() *Statement {\n\treturn Id(d.TypeName())\n}\n\nfunc (d *responseDesc) TypeDecl() *Statement {\n\treturn Type().Id(d.TypeName()).Op(\"=\").Do(func(s *Statement) {\n\t\tif d.ep.Response != nil {\n\t\t\ts.Add(d.gu.Type(d.ep.Response))\n\t\t} else {\n\t\t\ts.Add(apiQ(\"Void\"))\n\t\t}\n\t})\n}\n\nfunc (d *responseDesc) ZeroType() *Statement {\n\tif d.ep.Response != nil {\n\t\treturn d.gu.Zero(d.ep.Response)\n\t} else {\n\t\treturn apiQ(\"Void\").Values()\n\t}\n}\n\nfunc (d *responseDesc) EncodeResponse() *Statement {\n\tif d.ep.Raw {\n\t\treturn Nil()\n\t}\n\n\treturn Func().Params(\n\t\tId(\"w\").Qual(\"net/http\", \"ResponseWriter\"),\n\t\tId(\"json\").Qual(\"github.com/json-iterator/go\", \"API\"),\n\t\tId(\"resp\").Add(d.Type()),\n\t\tId(\"status\").Int(),\n\t).Params(Err().Error()).BlockFunc(func(g *Group) {\n\t\tif d.ep.Response == nil {\n\t\t\tg.Return(Nil())\n\t\t\treturn\n\t\t}\n\n\t\tresp := apienc.DescribeResponse(d.gu.Errs, d.ep.Response)\n\t\tif len(resp.BodyParameters) > 0 {\n\t\t\tg.Id(\"respData\").Op(\":=\").Index().Byte().Parens(Lit(\"null\\n\"))\n\t\t} else {\n\t\t\tg.Id(\"respData\").Op(\":=\").Index().Byte().Values(LitRune('\\n'))\n\t\t}\n\t\tif len(resp.HeaderParameters) > 0 {\n\t\t\tg.Var().Id(\"headers\").Map(String()).Index().String()\n\t\t}\n\n\t\tresponseEncoder := CustomFunc(Options{Separator: \"\\n\"}, func(g *Group) {\n\t\t\tif len(resp.BodyParameters) > 0 {\n\t\t\t\tg.Comment(\"Encode JSON body\")\n\t\t\t\tg.List(Id(\"respData\"), Err()).Op(\"=\").Qual(\"encore.dev/appruntime/shared/serde\", \"SerializeJSONFunc\").Call(\n\t\t\t\t\tId(\"json\"),\n\t\t\t\t\tFunc().Params(\n\t\t\t\t\t\tId(\"ser\").Op(\"*\").Qual(\"encore.dev/appruntime/shared/serde\", \"JSONSerializer\"),\n\t\t\t\t\t).BlockFunc(\n\t\t\t\t\t\tfunc(g *Group) {\n\t\t\t\t\t\t\tfor _, f := range resp.BodyParameters {\n\t\t\t\t\t\t\t\tg.Add(Id(\"ser\").Dot(\"WriteField\").Call(Lit(f.WireName), Id(\"resp\").Dot(f.SrcName), Lit(f.OmitEmpty)))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}))\n\t\t\t\tg.If(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\t\tReturn(Err()),\n\t\t\t\t)\n\t\t\t\tg.Id(\"respData\").Op(\"=\").Append(Id(\"respData\"), LitRune('\\n'))\n\t\t\t}\n\n\t\t\tif len(resp.HeaderParameters) > 0 {\n\t\t\t\tg.Line().Comment(\"Encode headers\")\n\t\t\t\tg.Id(\"headers\").Op(\"=\").Map(String()).Index().String().Values(DictFunc(func(dict Dict) {\n\t\t\t\t\tfor _, f := range resp.HeaderParameters {\n\t\t\t\t\t\tencExpr, ok := genutil.MarshalQueryOrHeader(f.Type, Id(\"resp\").Dot(f.SrcName))\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\td.gu.Errs.Addf(f.Type.ASTExpr().Pos(), \"unsupported type in header: %s\", d.gu.TypeToString(f.Type))\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdict[Lit(f.WireName)] = encExpr\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\t}\n\t\t})\n\n\t\t// If response is a ptr we need to check it's not nil\n\t\tif schemautil.IsPointer(d.ep.Response) {\n\t\t\tg.If(Id(\"resp\").Op(\"!=\").Nil()).Block(responseEncoder)\n\t\t} else {\n\t\t\tg.Add(responseEncoder)\n\t\t}\n\n\t\tif len(resp.HeaderParameters) > 0 {\n\t\t\tg.Line().Comment(\"Set response headers\")\n\t\t\tg.For(List(Id(\"k\"), Id(\"vs\")).Op(\":=\").Range().Id(\"headers\")).Block(\n\t\t\t\tFor(List(Id(\"_\"), Id(\"v\")).Op(\":=\").Range().Id(\"vs\")).Block(\n\t\t\t\t\tId(\"w\").Dot(\"Header\").Call().Dot(\"Add\").Call(Id(\"k\"), Id(\"v\")),\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tg.Line().Comment(\"Set HTTP status code\")\n\t\tif resp.HTTPStatusParameter != nil {\n\t\t\tg.Id(\"statusCode\").Op(\":=\").Id(\"status\")\n\n\t\t\tvar statusFieldCond *Statement\n\t\t\tif schemautil.IsPointer(d.ep.Response) {\n\t\t\t\tstatusFieldCond = Id(\"resp\").Op(\"!=\").Nil().Op(\"&&\").Id(\"resp\").Dot(resp.HTTPStatusParameter.SrcName).Op(\"!=\").Lit(0)\n\t\t\t} else {\n\t\t\t\tstatusFieldCond = Id(\"resp\").Dot(resp.HTTPStatusParameter.SrcName).Op(\"!=\").Lit(0)\n\t\t\t}\n\n\t\t\tg.If(statusFieldCond).Block(\n\t\t\t\tId(\"statusCode\").Op(\"=\").Int().Call(Id(\"resp\").Dot(resp.HTTPStatusParameter.SrcName)),\n\t\t\t)\n\n\t\t\tg.If(Id(\"statusCode\").Op(\"!=\").Lit(0)).Block(\n\t\t\t\tId(\"w\").Dot(\"WriteHeader\").Call(Id(\"statusCode\")),\n\t\t\t)\n\t\t} else {\n\t\t\tg.If(Id(\"status\").Op(\"!=\").Lit(0)).Block(\n\t\t\t\tId(\"w\").Dot(\"WriteHeader\").Call(Id(\"status\")),\n\t\t\t)\n\t\t}\n\n\t\tg.Line().Comment(\"Write response body\")\n\t\tg.Id(\"w\").Dot(\"Write\").Call(Id(\"respData\"))\n\t\tg.Return(Nil())\n\t})\n}\n\nfunc (d *responseDesc) DecodeExternalResp() *Statement {\n\tif d.ep.Raw {\n\t\t// TODO(andre) support\n\t\treturn Nil()\n\t}\n\n\treturn Func().Params(\n\t\tId(\"httpResp\").Op(\"*\").Qual(\"net/http\", \"Response\"),\n\t\tId(\"json\").Qual(\"github.com/json-iterator/go\", \"API\"),\n\t).Params(\n\t\tId(\"resp\").Add(d.Type()),\n\t\tErr().Error(),\n\t).BlockFunc(func(g *Group) {\n\t\tif d.ep.Response == nil {\n\t\t\tg.Return(d.ZeroType(), Nil())\n\t\t\treturn\n\t\t}\n\n\t\tg.Id(\"resp\").Op(\"=\").Add(d.gu.Initialize(d.ep.Response))\n\n\t\tenc := d.ep.ResponseEncoding()\n\t\tdec := d.gu.NewTypeUnmarshaller(\"dec\")\n\t\tg.Add(dec.Init())\n\n\t\tif enc.HTTPStatusParameter != nil {\n\t\t\tg.Line().Comment(\"Set HTTP status field\")\n\t\t\tstatusType := d.gu.Type(enc.HTTPStatusParameter.Type)\n\t\t\tg.Id(\"resp\").Dot(enc.HTTPStatusParameter.SrcName).Op(\"=\").Add(statusType).Call(Id(\"httpResp\").Dot(\"StatusCode\"))\n\t\t}\n\n\t\tapigenutil.DecodeHeaders(g, Id(\"httpResp\").Dot(\"Header\"), Id(\"resp\"), dec, enc.HeaderParameters)\n\t\tapigenutil.DecodeBody(g, Id(\"httpResp\").Dot(\"Body\"), Id(\"resp\"), dec, enc.BodyParameters)\n\n\t\tg.If(Err().Op(\":=\").Add(dec.Err()), Err().Op(\"!=\").Nil()).Block(\n\t\t\tReturn(d.ZeroType(), Err()),\n\t\t)\n\t\tg.Return(Id(\"resp\"), Nil())\n\t})\n}\n\n// httpRespExpr returns an expression to access the HTTP response writer variable.\nfunc (d *requestDesc) httpRespExpr() *Statement {\n\treturn Id(\"httpResp\")\n}\n\n// respDataExpr returns the expression to access the respData variable.\nfunc (d *responseDesc) respDataExpr() *Statement {\n\treturn Id(\"respData\")\n}\n\n// respDataPayloadExpr returns an expression for accessing the payload\n// in the reqData variable.\nfunc (d *responseDesc) respDataPayloadExpr() *Statement {\n\treturn d.respDataExpr()\n}\n\n// zero returns an expression representing the zero value\n// of the response type.\nfunc (d *responseDesc) zero() *Statement {\n\tif d.ep.Response != nil {\n\t\treturn d.gu.Zero(d.ep.Response)\n\t} else {\n\t\treturn apiQ(\"Void\").Values()\n\t}\n}\n\n// Clone returns the function literal to clone the request.\nfunc (d *responseDesc) Clone() *Statement {\n\tconst recv = \"r\"\n\treturn Func().Params(Id(recv).Add(d.Type())).Params(d.Type(), Error()).BlockFunc(func(g *Group) {\n\t\t// We could optimize the clone operation if there are no reference types (pointers, maps, slices)\n\t\t// in the struct. For now, simply serialize it as JSON and back.\n\t\tg.Var().Id(\"clone\").Add(d.Type())\n\t\tg.List(Id(\"bytes\"), Id(\"err\")).Op(\":=\").Qual(jsonIterPkg, \"ConfigDefault\").Dot(\"Marshal\").Call(Id(recv))\n\t\tg.If(Err().Op(\"==\").Nil()).Block(\n\t\t\tErr().Op(\"=\").Qual(jsonIterPkg, \"ConfigDefault\").Dot(\"Unmarshal\").Call(Id(\"bytes\"), Op(\"&\").Id(\"clone\")),\n\t\t)\n\t\tg.Return(Id(\"clone\"), Err())\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/api_call.txt",
    "content": "-- svca/svca.go --\npackage svca\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n//encore:api public\nfunc Bar(ctx context.Context) error { return Foo(ctx) }\n-- svcb/svcb.go --\npackage svcb\n\nimport (\"context\"; \"example.com/svca\")\n\n//encore:api public\nfunc Baz(ctx context.Context) error { return svca.Foo(ctx) }\n-- want:svca/encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage svca\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n\n\tBar(ctx context.Context) error\n}\n-- want:svca/encore_internal__api.go --\npackage svca\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Bar, Bar)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/svca.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/svca.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/svca.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"svca\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n\ntype EncoreInternal_BarReq struct{}\n\ntype EncoreInternal_BarResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Bar = &__api.Desc[*EncoreInternal_BarReq, EncoreInternal_BarResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_BarReq) (EncoreInternal_BarResp, error) {\n\t\terr := Bar(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_BarReq) (*EncoreInternal_BarReq, error) {\n\t\tvar clone *EncoreInternal_BarReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_BarResp) (EncoreInternal_BarResp, error) {\n\t\tvar clone EncoreInternal_BarResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_BarResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_BarReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_BarReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_BarReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_BarResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Bar\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/svca.Bar\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/svca.Bar\",\n\tReqPath: func(reqData *EncoreInternal_BarReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/svca.Bar\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_BarReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"svca\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:svca/encore_internal__apicalls.go --\npackage svca\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n)\n\nfunc EncoreInternal_apicalls_Foo(ctx context.Context) error {\n\t_, err := EncoreInternal_api_APIDesc_Foo.Call(__api.NewCallContext(ctx), &EncoreInternal_FooReq{})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n-- want:svca/svca.go --\npackage svca\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n//encore:api public\nfunc Bar(ctx context.Context) error { return EncoreInternal_apicalls_Foo(ctx) }\n-- want:svcb/svcb.go --\npackage svcb\n\nimport (\"context\"; \"example.com/svca\")\n\n//encore:api public\nfunc Baz(ctx context.Context) error { return svca.EncoreInternal_apicalls_Foo(ctx) }\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/api_call_servicestruct.txt",
    "content": "-- svca/svca.go --\npackage svca\n\nimport \"context\"\n\n//encore:service\ntype Service struct{}\n\n//encore:api public\nfunc (s *Service) Foo(context.Context) error { return nil }\n\n-- svcb/svcb.go --\npackage svcb\n\nimport (\"context\"; \"example.com/svca\")\n\n//encore:api public\nfunc Baz(ctx context.Context) error { return svca.Foo(ctx) }\n-- want:svca/encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage svca\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\nfunc Foo(ctx context.Context) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.Foo(ctx)\n}\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:svca/encore_internal__api.go --\npackage svca\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tsvc, initErr := EncoreInternal_svcstruct_Service.Get()\n\t\tif initErr != nil {\n\t\t\treturn __api.Void{}, initErr\n\t\t}\n\t\terr := svc.Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/svca.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/svca.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/svca.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"svca\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:svca/encore_internal__apicalls.go --\npackage svca\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n)\n\nfunc EncoreInternal_apicalls_Foo(ctx context.Context) error {\n\t_, err := EncoreInternal_api_APIDesc_Foo.Call(__api.NewCallContext(ctx), &EncoreInternal_FooReq{})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n-- want:svca/encore_internal__svcstruct.go --\npackage svca\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"svca\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n-- want:svcb/svcb.go --\npackage svcb\n\nimport (\"context\"; \"example.com/svca\")\n\n//encore:api public\nfunc Baz(ctx context.Context) error { return svca.EncoreInternal_apicalls_Foo(ctx) }\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/basic.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\n//encore:api public\nfunc Foo(ctx context.Context) error { return nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/complex_omitempty.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\ntype NotComparable struct{\n    Foo []string\n}\n\ntype NamedNotComparable NotComparable\n\ntype Int int\n\ntype Params struct {\n    X NotComparable `json:\"x,omitempty\"`\n    Integer Int `json:\"int,omitempty\"`\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, p *Params) error\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tPayload *Params\n}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx, reqData.Payload)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\tparams := new(Params)\n\t\treqData.Payload = params\n\t\tswitch m := httpReq.Method; m {\n\t\tcase \"POST\":\n\t\t\t// Decode request body\n\t\t\tpayload := dec.ReadBody(httpReq.Body)\n\t\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\t\tswitch strings.ToLower(key) {\n\t\t\t\tcase \"x\":\n\t\t\t\t\tdec.ParseJSON(\"X\", iter, &params.X)\n\t\t\t\tcase \"int\":\n\t\t\t\t\tdec.ParseJSON(\"Integer\", iter, &params.Integer)\n\t\t\t\tdefault:\n\t\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}) {\n\t\t\t}\n\n\t\tdefault:\n\t\t\tpanic(\"HTTP method is not supported\")\n\t\t}\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\tparams := reqData.Payload\n\t\tif params == nil {\n\t\t\t// If the payload is nil, we need to return an empty request body.\n\t\t\treturn httpHeader, queryString, err\n\t\t}\n\n\t\t// Encode request body\n\t\tstream.WriteObjectStart()\n\t\tif true {\n\t\t\t// X is set to omitempty, so we need to check if it's empty before writing it\n\t\t\tstream.WriteObjectField(\"x\")\n\t\t\tstream.WriteVal(params.X)\n\t\t\tstream.WriteMore()\n\t\t}\n\t\tif params.Integer != 0 {\n\t\t\t// Integer is set to omitempty, so we need to check if it's empty before writing it\n\t\t\tstream.WriteObjectField(\"int\")\n\t\t\tstream.WriteVal(params.Integer)\n\t\t}\n\t\tstream.WriteObjectEnd()\n\n\t\treturn httpHeader, queryString, err\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn reqData.Payload\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/endpoint_tags.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\n//encore:api public tag:foo tag:bar\nfunc Tagged(ctx context.Context) error { return nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tTagged(ctx context.Context) error\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Tagged, Tagged)\n}\n\ntype EncoreInternal_TaggedReq struct{}\n\ntype EncoreInternal_TaggedResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Tagged = &__api.Desc[*EncoreInternal_TaggedReq, EncoreInternal_TaggedResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_TaggedReq) (EncoreInternal_TaggedResp, error) {\n\t\terr := Tagged(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_TaggedReq) (*EncoreInternal_TaggedReq, error) {\n\t\tvar clone *EncoreInternal_TaggedReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_TaggedResp) (EncoreInternal_TaggedResp, error) {\n\t\tvar clone EncoreInternal_TaggedResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_TaggedResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_TaggedReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_TaggedReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_TaggedReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_TaggedResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Tagged\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/code.Tagged\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Tagged\",\n\tReqPath: func(reqData *EncoreInternal_TaggedReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Tagged\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_TaggedReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 []string{\"bar\", \"foo\"},\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/fallback_path.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\n//encore:api public path=/!fallback\nfunc Foo(ctx context.Context, fallback string) error { return nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, fallback string) error\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tP0 string\n}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx, reqData.P0)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Trim the leading slash from wildcard parameter, as Encore's semantics excludes it,\n\t\t// while the httprouter implementation includes it.\n\t\tps[0] = strings.TrimPrefix(ps[0], \"/\")\n\n\t\tif value, err := url.PathUnescape(ps[0]); err == nil {\n\t\t\tps[0] = value\n\t\t}\n\t\treqData.P0 = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"fallback\", ps[0], true)\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            true,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/!fallback\",\n\tPathParamNames:      []string{\"fallback\"},\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/*0\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\tparams := __api.UnnamedParams{__etype.MarshalOne(__etype.MarshalString, reqData.P0)}\n\t\treturn \"/\" + url.PathEscape(params[0]), params, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/path_params.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\ntype Params struct {\n    String string\n    Int int\n}\n\n//encore:api public path=/foo/:id/*baz\nfunc Foo(ctx context.Context, id int, baz string, p *Params) error { return nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, id int, baz string, p *Params) error\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tPayload *Params\n\tP0      int\n\tP1      string\n}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx, reqData.P0, reqData.P1, reqData.Payload)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Trim the leading slash from wildcard parameter, as Encore's semantics excludes it,\n\t\t// while the httprouter implementation includes it.\n\t\tps[1] = strings.TrimPrefix(ps[1], \"/\")\n\n\t\treqData.P0 = __etype.UnmarshalOne(dec, __etype.UnmarshalInt, \"id\", ps[0], true)\n\t\tif value, err := url.PathUnescape(ps[1]); err == nil {\n\t\t\tps[1] = value\n\t\t}\n\t\treqData.P1 = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"baz\", ps[1], true)\n\t\tparams := new(Params)\n\t\treqData.Payload = params\n\t\tswitch m := httpReq.Method; m {\n\t\tcase \"POST\":\n\t\t\t// Decode request body\n\t\t\tpayload := dec.ReadBody(httpReq.Body)\n\t\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\t\tswitch strings.ToLower(key) {\n\t\t\t\tcase \"string\":\n\t\t\t\t\tdec.ParseJSON(\"String\", iter, &params.String)\n\t\t\t\tcase \"int\":\n\t\t\t\t\tdec.ParseJSON(\"Int\", iter, &params.Int)\n\t\t\t\tdefault:\n\t\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}) {\n\t\t\t}\n\n\t\tdefault:\n\t\t\tpanic(\"HTTP method is not supported\")\n\t\t}\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\tparams := reqData.Payload\n\t\tif params == nil {\n\t\t\t// If the payload is nil, we need to return an empty request body.\n\t\t\treturn httpHeader, queryString, err\n\t\t}\n\n\t\t// Encode request body\n\t\tstream.WriteObjectStart()\n\t\tstream.WriteObjectField(\"String\")\n\t\tstream.WriteVal(params.String)\n\t\tstream.WriteMore()\n\t\tstream.WriteObjectField(\"Int\")\n\t\tstream.WriteVal(params.Int)\n\t\tstream.WriteObjectEnd()\n\n\t\treturn httpHeader, queryString, err\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"POST\"},\n\tPath:                \"/foo/:id/*baz\",\n\tPathParamNames:      []string{\"id\", \"baz\"},\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/foo/:0/*1\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\tparams := __api.UnnamedParams{__etype.MarshalOne(__etype.MarshalInt, reqData.P0), __etype.MarshalOne(__etype.MarshalString, reqData.P1)}\n\t\treturn \"/foo\" + \"/\" + url.PathEscape(params[0]) + \"/\" + url.PathEscape(params[1]), params, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn reqData.Payload\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/raw_endpoint.txt",
    "content": "-- code.go --\npackage code\nimport \"net/http\"\n\n//encore:api public raw\nfunc Foo(w http.ResponseWriter, req *http.Request) {}\n\n//encore:service\ntype Service struct{}\n\n//encore:api public raw\nfunc (s *Service) Bar(w http.ResponseWriter, req *http.Request) {}\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n)\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\nfunc Bar(ctx context.Context, req *http.Request) (*http.Response, error) {\n\treturn nil, errors.New(\"encore: calling raw endpoints is not yet supported\")\n}\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface{}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\terrs \"encore.dev/beta/errs\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Bar, Bar)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess:     __api.Public,\n\tAppHandler: nil,\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: nil,\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp:          nil,\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"HEAD\", \"POST\", \"PUT\", \"DELETE\", \"CONNECT\", \"OPTIONS\", \"TRACE\", \"PATCH\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 true,\n\tRawHandler: func(w http.ResponseWriter, req *http.Request) {\n\t\tFoo(w, req)\n\t},\n\tRawPath: \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n\ntype EncoreInternal_BarReq struct{}\n\ntype EncoreInternal_BarResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Bar = &__api.Desc[*EncoreInternal_BarReq, EncoreInternal_BarResp]{\n\tAccess:     __api.Public,\n\tAppHandler: nil,\n\tCloneReq: func(r *EncoreInternal_BarReq) (*EncoreInternal_BarReq, error) {\n\t\tvar clone *EncoreInternal_BarReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_BarResp) (EncoreInternal_BarResp, error) {\n\t\tvar clone EncoreInternal_BarResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: nil,\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_BarReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_BarReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_BarReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp:          nil,\n\tEndpoint:            \"Bar\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"HEAD\", \"POST\", \"PUT\", \"DELETE\", \"CONNECT\", \"OPTIONS\", \"TRACE\", \"PATCH\"},\n\tPath:                \"/code.Bar\",\n\tPathParamNames:      nil,\n\tRaw:                 true,\n\tRawHandler: func(w http.ResponseWriter, req *http.Request) {\n\t\tsvc, initErr := EncoreInternal_svcstruct_Service.Get()\n\t\tif initErr != nil {\n\t\t\terrs.HTTPErrorWithCode(w, initErr, 0)\n\t\t\treturn\n\t\t}\n\t\tsvc.Bar(w, req)\n\t},\n\tRawPath: \"/code.Bar\",\n\tReqPath: func(reqData *EncoreInternal_BarReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Bar\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_BarReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:encore_internal__svcstruct.go --\npackage code\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"code\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/recursive.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\ntype Recursive struct {\n    Basic []Recursive\n    Pointer []*Recursive\n    Mutual []MutualTwo\n}\n\ntype MutualOne struct {\n    Basic []MutualTwo\n    Pointer []*MutualTwo\n    Self []Recursive\n    SelfPointer []*Recursive\n}\n\ntype MutualTwo struct {\n    Basic []MutualOne\n    Pointer []*MutualOne\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Recursive) (*Recursive, error) { return p, nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, p *Recursive) (*Recursive, error)\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\t__serde \"encore.dev/appruntime/shared/serde\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tPayload *Recursive\n}\n\ntype EncoreInternal_FooResp = *Recursive\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tresp, err := Foo(ctx, reqData.Payload)\n\t\tif err != nil {\n\t\t\treturn (*Recursive)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\tresp = new(Recursive)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Decode request body\n\t\tpayload := dec.ReadBody(httpResp.Body)\n\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\tswitch strings.ToLower(key) {\n\t\t\tcase \"basic\":\n\t\t\t\tdec.ParseJSON(\"Basic\", iter, &resp.Basic)\n\t\t\tcase \"pointer\":\n\t\t\t\tdec.ParseJSON(\"Pointer\", iter, &resp.Pointer)\n\t\t\tcase \"mutual\":\n\t\t\t\tdec.ParseJSON(\"Mutual\", iter, &resp.Mutual)\n\t\t\tdefault:\n\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t}\n\t\t\treturn true\n\t\t}) {\n\t\t}\n\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn (*Recursive)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\tparams := new(Recursive)\n\t\treqData.Payload = params\n\t\tswitch m := httpReq.Method; m {\n\t\tcase \"POST\":\n\t\t\t// Decode request body\n\t\t\tpayload := dec.ReadBody(httpReq.Body)\n\t\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\t\tswitch strings.ToLower(key) {\n\t\t\t\tcase \"basic\":\n\t\t\t\t\tdec.ParseJSON(\"Basic\", iter, &params.Basic)\n\t\t\t\tcase \"pointer\":\n\t\t\t\t\tdec.ParseJSON(\"Pointer\", iter, &params.Pointer)\n\t\t\t\tcase \"mutual\":\n\t\t\t\t\tdec.ParseJSON(\"Mutual\", iter, &params.Mutual)\n\t\t\t\tdefault:\n\t\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}) {\n\t\t\t}\n\n\t\tdefault:\n\t\t\tpanic(\"HTTP method is not supported\")\n\t\t}\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\tparams := reqData.Payload\n\t\tif params == nil {\n\t\t\t// If the payload is nil, we need to return an empty request body.\n\t\t\treturn httpHeader, queryString, err\n\t\t}\n\n\t\t// Encode request body\n\t\tstream.WriteObjectStart()\n\t\tstream.WriteObjectField(\"Basic\")\n\t\tstream.WriteVal(params.Basic)\n\t\tstream.WriteMore()\n\t\tstream.WriteObjectField(\"Pointer\")\n\t\tstream.WriteVal(params.Pointer)\n\t\tstream.WriteMore()\n\t\tstream.WriteObjectField(\"Mutual\")\n\t\tstream.WriteVal(params.Mutual)\n\t\tstream.WriteObjectEnd()\n\n\t\treturn httpHeader, queryString, err\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\trespData := []byte(\"null\\n\")\n\t\tif resp != nil {\n\t\t\t// Encode JSON body\n\t\t\trespData, err = __serde.SerializeJSONFunc(json, func(ser *__serde.JSONSerializer) {\n\t\t\t\tser.WriteField(\"Basic\", resp.Basic, false)\n\t\t\t\tser.WriteField(\"Pointer\", resp.Pointer, false)\n\t\t\t\tser.WriteField(\"Mutual\", resp.Mutual, false)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trespData = append(respData, '\\n')\n\t\t}\n\n\t\t// Set HTTP status code\n\t\tif status != 0 {\n\t\t\tw.WriteHeader(status)\n\t\t}\n\n\t\t// Write response body\n\t\tw.Write(respData)\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn reqData.Payload\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/request_headers.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\ntype Params struct {\n    Foo   string `header:\"X-Foo\"`\n    Ignore string `header:\"-\"`\n    Strings []string `header:\"X-Strings\"`\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, p *Params) error\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tPayload *Params\n}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx, reqData.Payload)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\tparams := new(Params)\n\t\treqData.Payload = params\n\t\tswitch m := httpReq.Method; m {\n\t\tcase \"POST\":\n\t\t\t// Decode headers\n\t\t\th := httpReq.Header\n\t\t\tparams.Foo = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"x-foo\", h.Get(\"x-foo\"), false)\n\t\t\tparams.Strings = __etype.UnmarshalList(dec, __etype.UnmarshalString, \"x-strings\", h.Values(\"x-strings\"), false)\n\n\t\tdefault:\n\t\t\tpanic(\"HTTP method is not supported\")\n\t\t}\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\tparams := reqData.Payload\n\t\tif params == nil {\n\t\t\t// If the payload is nil, we need to return an empty request body.\n\t\t\treturn httpHeader, queryString, err\n\t\t}\n\n\t\t// Encode headers\n\t\thttpHeader = make(http.Header, 2)\n\t\thttpHeader[textproto.CanonicalMIMEHeaderKey(\"x-foo\")] = __etype.MarshalOneAsList(__etype.MarshalString, params.Foo)\n\t\thttpHeader[textproto.CanonicalMIMEHeaderKey(\"x-strings\")] = __etype.MarshalList(__etype.MarshalString, params.Strings)\n\n\t\treturn httpHeader, queryString, err\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn reqData.Payload\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/request_params.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\ntype Params struct {\n    String string   `json:\",omitempty\"`\n    Array  [3]byte  `json:\",omitempty\"`\n    Int int\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, p *Params) error\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tPayload *Params\n}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx, reqData.Payload)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\tparams := new(Params)\n\t\treqData.Payload = params\n\t\tswitch m := httpReq.Method; m {\n\t\tcase \"POST\":\n\t\t\t// Decode request body\n\t\t\tpayload := dec.ReadBody(httpReq.Body)\n\t\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\t\tswitch strings.ToLower(key) {\n\t\t\t\tcase \"string\":\n\t\t\t\t\tdec.ParseJSON(\"String\", iter, &params.String)\n\t\t\t\tcase \"array\":\n\t\t\t\t\tdec.ParseJSON(\"Array\", iter, &params.Array)\n\t\t\t\tcase \"int\":\n\t\t\t\t\tdec.ParseJSON(\"Int\", iter, &params.Int)\n\t\t\t\tdefault:\n\t\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}) {\n\t\t\t}\n\n\t\tdefault:\n\t\t\tpanic(\"HTTP method is not supported\")\n\t\t}\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\tparams := reqData.Payload\n\t\tif params == nil {\n\t\t\t// If the payload is nil, we need to return an empty request body.\n\t\t\treturn httpHeader, queryString, err\n\t\t}\n\n\t\t// Encode request body\n\t\tstream.WriteObjectStart()\n\t\tif params.String != \"\" {\n\t\t\t// String is set to omitempty, so we need to check if it's empty before writing it\n\t\t\tstream.WriteObjectField(\"String\")\n\t\t\tstream.WriteVal(params.String)\n\t\t\tstream.WriteMore()\n\t\t}\n\t\tif true {\n\t\t\t// Array is set to omitempty, so we need to check if it's empty before writing it\n\t\t\tstream.WriteObjectField(\"Array\")\n\t\t\tstream.WriteVal(params.Array)\n\t\t\tstream.WriteMore()\n\t\t}\n\t\tstream.WriteObjectField(\"Int\")\n\t\tstream.WriteVal(params.Int)\n\t\tstream.WriteObjectEnd()\n\n\t\treturn httpHeader, queryString, err\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn reqData.Payload\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/request_query.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\ntype Params struct {\n    Foo   string `query:\"foo\"`\n    Ignore string `query:\"-\"`\n    Ints []int `query:\"ints\"`\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context, p *Params) error\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct {\n\tPayload *Params\n}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx, reqData.Payload)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\tdec := new(__etype.Unmarshaller)\n\t\tparams := new(Params)\n\t\treqData.Payload = params\n\t\tswitch m := httpReq.Method; m {\n\t\tcase \"POST\":\n\t\t\t// Decode query string\n\t\t\tqs := httpReq.URL.Query()\n\t\t\tparams.Foo = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"foo\", qs.Get(\"foo\"), false)\n\t\t\tparams.Ints = __etype.UnmarshalList(dec, __etype.UnmarshalInt, \"ints\", qs[\"ints\"], false)\n\n\t\tdefault:\n\t\t\tpanic(\"HTTP method is not supported\")\n\t\t}\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn reqData, ps, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\tparams := reqData.Payload\n\t\tif params == nil {\n\t\t\t// If the payload is nil, we need to return an empty request body.\n\t\t\treturn httpHeader, queryString, err\n\t\t}\n\n\t\t// Encode query string\n\t\tqueryString = make(url.Values, 2)\n\t\tqueryString[\"foo\"] = __etype.MarshalOneAsList(__etype.MarshalString, params.Foo)\n\t\tqueryString[\"ints\"] = __etype.MarshalList(__etype.MarshalInt, params.Ints)\n\n\t\treturn httpHeader, queryString, err\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn reqData.Payload\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/response_headers.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\ntype Params struct {\n    Foo   string `header:\"X-Foo\"`\n    Ignore string `header:\"-\"`\n}\n\n//encore:api public\nfunc Foo(ctx context.Context) (*Params, error) { return nil, nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) (*Params, error)\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = *Params\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tresp, err := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn (*Params)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\tresp = new(Params)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Decode headers\n\t\th := httpResp.Header\n\t\tresp.Foo = __etype.UnmarshalOne(dec, __etype.UnmarshalString, \"x-foo\", h.Get(\"x-foo\"), false)\n\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn (*Params)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\trespData := []byte{'\\n'}\n\t\tvar headers map[string][]string\n\t\tif resp != nil {\n\n\t\t\t// Encode headers\n\t\t\theaders = map[string][]string{\"x-foo\": __etype.MarshalOneAsList(__etype.MarshalString, resp.Foo)}\n\t\t}\n\n\t\t// Set response headers\n\t\tfor k, vs := range headers {\n\t\t\tfor _, v := range vs {\n\t\t\t\tw.Header().Add(k, v)\n\t\t\t}\n\t\t}\n\n\t\t// Set HTTP status code\n\t\tif status != 0 {\n\t\t\tw.WriteHeader(status)\n\t\t}\n\n\t\t// Write response body\n\t\tw.Write(respData)\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/response_params.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\ntype Params struct {\n    String string\n    Int int\n}\n\n//encore:api public\nfunc Foo(ctx context.Context) (*Params, error) { return nil, nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) (*Params, error)\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\t__serde \"encore.dev/appruntime/shared/serde\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = *Params\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tresp, err := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn (*Params)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\tresp = new(Params)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Decode request body\n\t\tpayload := dec.ReadBody(httpResp.Body)\n\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\tswitch strings.ToLower(key) {\n\t\t\tcase \"string\":\n\t\t\t\tdec.ParseJSON(\"String\", iter, &resp.String)\n\t\t\tcase \"int\":\n\t\t\t\tdec.ParseJSON(\"Int\", iter, &resp.Int)\n\t\t\tdefault:\n\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t}\n\t\t\treturn true\n\t\t}) {\n\t\t}\n\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn (*Params)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\trespData := []byte(\"null\\n\")\n\t\tif resp != nil {\n\t\t\t// Encode JSON body\n\t\t\trespData, err = __serde.SerializeJSONFunc(json, func(ser *__serde.JSONSerializer) {\n\t\t\t\tser.WriteField(\"String\", resp.String, false)\n\t\t\t\tser.WriteField(\"Int\", resp.Int, false)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trespData = append(respData, '\\n')\n\t\t}\n\n\t\t// Set HTTP status code\n\t\tif status != 0 {\n\t\t\tw.WriteHeader(status)\n\t\t}\n\n\t\t// Write response body\n\t\tw.Write(respData)\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/response_status.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\ntype Params struct {\n String string\n Int    int `encore:\"httpstatus\"`\n}\n\n//encore:api public\nfunc Foo(ctx context.Context) (*Params, error) { return nil, nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) (*Params, error)\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\t__serde \"encore.dev/appruntime/shared/serde\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = *Params\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tresp, err := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn (*Params)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\tresp = new(Params)\n\t\tdec := new(__etype.Unmarshaller)\n\n\t\t// Set HTTP status field\n\t\tresp.Int = int(httpResp.StatusCode)\n\t\t// Decode request body\n\t\tpayload := dec.ReadBody(httpResp.Body)\n\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\tswitch strings.ToLower(key) {\n\t\t\tcase \"string\":\n\t\t\t\tdec.ParseJSON(\"String\", iter, &resp.String)\n\t\t\tdefault:\n\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t}\n\t\t\treturn true\n\t\t}) {\n\t\t}\n\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn (*Params)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\trespData := []byte(\"null\\n\")\n\t\tif resp != nil {\n\t\t\t// Encode JSON body\n\t\t\trespData, err = __serde.SerializeJSONFunc(json, func(ser *__serde.JSONSerializer) {\n\t\t\t\tser.WriteField(\"String\", resp.String, false)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trespData = append(respData, '\\n')\n\t\t}\n\n\t\t// Set HTTP status code\n\t\tstatusCode := status\n\t\tif resp != nil && resp.Int != 0 {\n\t\t\tstatusCode = int(resp.Int)\n\t\t}\n\t\tif statusCode != 0 {\n\t\t\tw.WriteHeader(statusCode)\n\t\t}\n\n\t\t// Write response body\n\t\tw.Write(respData)\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/service_struct.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\n//encore:service\ntype Service struct{}\n\n//encore:api public\nfunc (s *Service) Foo(ctx context.Context) error { return nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\nfunc Foo(ctx context.Context) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.Foo(ctx)\n}\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:encore_internal__api.go --\npackage basic\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tsvc, initErr := EncoreInternal_svcstruct_Service.Get()\n\t\tif initErr != nil {\n\t\t\treturn __api.Void{}, initErr\n\t\t}\n\t\terr := svc.Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/basic.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/basic.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/basic.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"basic\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:encore_internal__svcstruct.go --\npackage basic\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"basic\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/endpointgen/testdata/unexported.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\ntype Response struct {\n    Exported bool\n    unexported string\n}\n\n//encore:api public\nfunc Foo(ctx context.Context) (*Response, error) { return nil, nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) (*Response, error)\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\t__serde \"encore.dev/appruntime/shared/serde\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = *Response\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Public,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tresp, err := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn (*Response)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\tresp = new(Response)\n\t\tdec := new(__etype.Unmarshaller)\n\t\t// Decode request body\n\t\tpayload := dec.ReadBody(httpResp.Body)\n\t\titer := jsoniter.ParseBytes(json, payload)\n\n\t\tfor iter.ReadObjectCB(func(_ *jsoniter.Iterator, key string) bool {\n\t\t\tswitch strings.ToLower(key) {\n\t\t\tcase \"exported\":\n\t\t\t\tdec.ParseJSON(\"Exported\", iter, &resp.Exported)\n\t\t\tdefault:\n\t\t\t\t_ = iter.SkipAndReturnBytes()\n\t\t\t}\n\t\t\treturn true\n\t\t}) {\n\t\t}\n\n\t\tif err := dec.Error; err != nil {\n\t\t\treturn (*Response)(nil), err\n\t\t}\n\t\treturn resp, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\trespData := []byte(\"null\\n\")\n\t\tif resp != nil {\n\t\t\t// Encode JSON body\n\t\t\trespData, err = __serde.SerializeJSONFunc(json, func(ser *__serde.JSONSerializer) {\n\t\t\t\tser.WriteField(\"Exported\", resp.Exported, false)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trespData = append(respData, '\\n')\n\t\t}\n\n\t\t// Set HTTP status code\n\t\tif status != 0 {\n\t\t\tw.WriteHeader(status)\n\t\t}\n\n\t\t// Write response body\n\t\tw.Write(respData)\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/load_app.go",
    "content": "package maingen\n\nimport (\n\t\"cmp\"\n\t\"maps\"\n\t\"net/http\"\n\t\"slices\"\n\t\"sort\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/typescrub\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n)\n\ntype testParams struct {\n\tExternalTestBinary bool\n\tEnvsToEmbed        map[string]string\n}\n\nfunc GenAppConfig(p GenParams, test option.Option[testParams]) *config.Static {\n\tallowHeaders, exposeHeaders := computeCORSHeaders(p.Desc)\n\n\trootDir := p.Desc.MainModule.RootDir.ToDisplay()\n\tif GenerateForInternalPackageTests {\n\t\trootDir = \"testing_path:main\"\n\t}\n\n\tcfg := &config.Static{\n\t\tEncoreCompiler: p.CompilerVersion,\n\t\tAppCommit: config.CommitInfo{\n\t\t\tRevision:    p.AppRevision,\n\t\t\tUncommitted: p.AppUncommitted,\n\t\t},\n\t\tCORSAllowHeaders:   allowHeaders,\n\t\tCORSExposeHeaders:  exposeHeaders,\n\t\tPubsubTopics:       pubsubTopics(p.Gen, p.Desc),\n\t\tTesting:            test.Present(),\n\t\tTestServiceMap:     testServiceMap(p.Desc),\n\t\tTestAppRootPath:    rootDir,\n\t\tBundledServices:    bundledServices(p.Desc),\n\t\tEnabledExperiments: p.Gen.Build.Experiments.StringList(),\n\t\tEmbeddedEnvs:       make(map[string]string),\n\t}\n\n\tif test, ok := test.Get(); ok {\n\t\tcfg.PrettyPrintLogs = test.ExternalTestBinary\n\t\tmaps.Copy(cfg.EmbeddedEnvs, test.EnvsToEmbed)\n\t}\n\n\treturn cfg\n}\n\nfunc pubsubTopics(gen *codegen.Generator, appDesc *app.Desc) map[string]*config.StaticPubsubTopic {\n\tresult := make(map[string]*config.StaticPubsubTopic)\n\t// Get all the topics and subscriptions\n\tvar (\n\t\ttopics      []*pubsub.Topic\n\t\tsubsByTopic = make(map[pkginfo.QualifiedName][]*pubsub.Subscription)\n\t)\n\tfor _, r := range appDesc.Parse.Resources() {\n\t\tswitch r := r.(type) {\n\t\tcase *pubsub.Topic:\n\t\t\ttopics = append(topics, r)\n\t\tcase *pubsub.Subscription:\n\t\t\tsubsByTopic[r.Topic] = append(subsByTopic[r.Topic], r)\n\t\t}\n\t}\n\n\tfor _, topic := range topics {\n\t\tsubs := make(map[string]*config.StaticPubsubSubscription)\n\n\t\tvar scrubMode typescrub.ParseMode\n\t\tif gen.Build.DisableSensitiveScrubbing {\n\t\t\tscrubMode |= typescrub.DisableScrubbing\n\t\t}\n\t\tscrubDesc := gen.TypeScrubber.Compute(topic.MessageType.ToType(), scrubMode)\n\n\t\tfor _, b := range appDesc.Parse.PkgDeclBinds(topic) {\n\t\t\tqn := b.QualifiedName()\n\t\t\tfor _, sub := range subsByTopic[qn] {\n\t\t\t\t// HACK: we should have a better way of knowing which service a subscription belongs to\n\t\t\t\tif svc, ok := appDesc.ServiceForPath(sub.File.Pkg.FSPath); ok {\n\n\t\t\t\t\tsubs[sub.Name] = &config.StaticPubsubSubscription{\n\t\t\t\t\t\tService:    svc.Name,\n\t\t\t\t\t\tSvcNum:     uint16(svc.Num),\n\t\t\t\t\t\tTraceIdx:   gen.TraceNodes.Sub(sub),\n\t\t\t\t\t\tScrubPaths: scrubDesc.Payload,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult[topic.Name] = &config.StaticPubsubTopic{\n\t\t\tSubscriptions: subs,\n\t\t\tScrubPaths:    scrubDesc.Payload,\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc bundledServices(appDesc *app.Desc) []string {\n\t// Sort the names by service number since that's what we're indexing by.\n\tsvcs := slices.Clone(appDesc.Services)\n\tslices.SortFunc(svcs, func(a, b *app.Service) int {\n\t\treturn cmp.Compare(a.Num, b.Num)\n\t})\n\n\treturn fns.Map(svcs, func(svc *app.Service) string {\n\t\treturn svc.Name\n\t})\n}\n\nfunc testServiceMap(appDesc *app.Desc) map[string]string {\n\tresult := make(map[string]string)\n\tfor _, svc := range appDesc.Services {\n\t\tpath := svc.FSRoot.ToIO()\n\t\tif GenerateForInternalPackageTests {\n\t\t\tpath = \"testing_path:\" + svc.Name\n\t\t}\n\t\tresult[svc.Name] = path\n\t}\n\treturn result\n}\n\nfunc enabledExperiments(experiments *experiments.Set) *Statement {\n\tlist := experiments.StringList()\n\n\tif len(list) == 0 {\n\t\treturn Nil()\n\t}\n\n\treturn Index().String().ValuesFunc(func(g *Group) {\n\t\tfor _, e := range list {\n\t\t\tg.Lit(e)\n\t\t}\n\t})\n}\n\nfunc computeCORSHeaders(appDesc *app.Desc) (allowHeaders, exposeHeaders []string) {\n\t// computeRequestHeaders computes the headers that are part of the request for a given RPC.\n\tcomputeRequestHeaders := func(ep *api.Endpoint) []*apienc.ParameterEncoding {\n\t\tvar params []*apienc.ParameterEncoding\n\t\tfor _, r := range ep.RequestEncoding() {\n\t\t\tparams = append(params, r.HeaderParameters...)\n\t\t}\n\t\treturn params\n\t}\n\n\t// computeResponseHeaders computes the headers that are part of the response for a given RPC.\n\tcomputeResponseHeaders := func(ep *api.Endpoint) []*apienc.ParameterEncoding {\n\t\treturn ep.ResponseEncoding().HeaderParameters\n\t}\n\n\ttype result struct {\n\t\tcomputeHeaders func(ep *api.Endpoint) []*apienc.ParameterEncoding\n\t\tseenHeader     map[string]bool\n\t\theaders        []string\n\t}\n\n\tvar (\n\t\tallow  = &result{computeHeaders: computeRequestHeaders}\n\t\texpose = &result{computeHeaders: computeResponseHeaders}\n\t)\n\n\tfor _, res := range []*result{allow, expose} {\n\t\tres.seenHeader = make(map[string]bool)\n\n\t\tfor _, svc := range appDesc.Services {\n\t\t\tsvc.Framework.ForAll(func(fw *apiframework.ServiceDesc) {\n\t\t\t\tfor _, ep := range fw.Endpoints {\n\t\t\t\t\tfor _, param := range res.computeHeaders(ep) {\n\t\t\t\t\t\tname := http.CanonicalHeaderKey(param.WireName)\n\t\t\t\t\t\tif !res.seenHeader[name] {\n\t\t\t\t\t\t\tres.seenHeader[name] = true\n\t\t\t\t\t\t\tres.headers = append(res.headers, name)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\t// Sort the headers so that the generated code is deterministic.\n\t\tsort.Strings(res.headers)\n\t}\n\treturn allow.headers, expose.headers\n}\n\n// GenerateForInternalPackageTests is set to true\n// when we're running the maingen package tests, to generate code without\n// temporary directories in the file paths (for reproducibility).\nvar GenerateForInternalPackageTests = false\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/maingen.go",
    "content": "package maingen\n\nimport (\n\tgotoken \"go/token\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n)\n\ntype GenParams struct {\n\tGen           *codegen.Generator\n\tDesc          *app.Desc\n\tMainModule    *pkginfo.Module\n\tRuntimeModule *pkginfo.Module\n\n\t// CompilerVersion is the version of the compiler to embed in the generated code.\n\tCompilerVersion string\n\t// AppRevision is the revision of the app to embed in the generated code.\n\tAppRevision string\n\t// AppUncommitted tracks whether there were uncommitted changes in the app\n\t// at the time of build.\n\tAppUncommitted bool\n\n\tAPIHandlers    map[*api.Endpoint]*codegen.VarDecl\n\tAuthHandler    option.Option[*codegen.VarDecl]\n\tMiddleware     map[*middleware.Middleware]*codegen.VarDecl\n\tServiceStructs map[*app.Service]*codegen.VarDecl\n\n\t// Test contains configuration for generating test code.\n\tTest option.Option[codegen.TestConfig]\n\n\t// ExecScriptMainPkg is the main package to build for an ExecScript execution.\n\tExecScriptMainPkg option.Option[paths.Pkg]\n}\n\nfunc Gen(p GenParams) *config.Static {\n\tif test, ok := p.Test.Get(); ok {\n\t\treturn genTestConfigs(p, test)\n\t} else if execScript, ok := p.ExecScriptMainPkg.Get(); ok {\n\t\treturn genExecScriptMain(p, execScript)\n\t} else {\n\t\treturn genMain(p)\n\t}\n}\n\nfunc genMain(p GenParams) *config.Static {\n\tmainPkgDir := p.MainModule.RootDir.Join(\"encore_internal\", \"main\")\n\tmainPkgPath := paths.Pkg(p.MainModule.Path).JoinSlash(\"encore_internal\", \"main\")\n\n\tfile := p.Gen.InjectFile(mainPkgPath, \"main\", mainPkgDir, \"main.go\", \"main\")\n\tf := file.Jen\n\n\t// All services should be imported by the main package so they get initialized on system startup\n\t// Services may not have API handlers as they could be purely operating on PubSub subscriptions\n\t// so without this anonymous package import, that service might not be initialized.\n\tfor _, svc := range p.Desc.Services {\n\t\tif fw, ok := svc.Framework.Get(); ok {\n\t\t\trootPkg := fw.RootPkg\n\t\t\tif rootPkg.ImportPath != mainPkgPath {\n\t\t\t\tf.Anon(rootPkg.ImportPath.String())\n\t\t\t}\n\t\t}\n\t}\n\t// Make sure auth handlers and global middleware are imported as well so they get registered.\n\tif fw, ok := p.Desc.Framework.Get(); ok {\n\t\tif ah, ok := fw.AuthHandler.Get(); ok {\n\t\t\tf.Anon(ah.Decl.File.Pkg.ImportPath.String())\n\t\t}\n\n\t\tfor _, mw := range fw.GlobalMiddleware {\n\t\t\tf.Anon(mw.Decl.File.Pkg.ImportPath.String())\n\t\t}\n\t}\n\n\tf.Func().Id(\"main\").Params().Block(\n\t\tQual(\"encore.dev/appruntime/apisdk/app/appinit\", \"AppMain\").Call(),\n\t)\n\n\treturn GenAppConfig(p, option.None[testParams]())\n}\n\nfunc genExecScriptMain(p GenParams, mainPkgPath paths.Pkg) *config.Static {\n\tmainPkgDir, ok := p.MainModule.FSPathToPkg(mainPkgPath)\n\tif !ok {\n\t\tp.Desc.Errs.Addf(gotoken.NoPos, \"cannot find package %s in module %s\",\n\t\t\tmainPkgPath, p.MainModule.Path)\n\t\treturn nil\n\t}\n\n\tfile := p.Gen.InjectFile(mainPkgPath, \"main\", mainPkgDir, \"encore_internal__execscript.go\", \"execscript\")\n\tf := file.Jen\n\n\t// All services should be imported by the main package so they get initialized on system startup\n\t// Services may not have API handlers as they could be purely operating on PubSub subscriptions\n\t// so without this anonymous package import, that service might not be initialized.\n\tfor _, svc := range p.Desc.Services {\n\t\tsvc.Framework.ForAll(func(svcDesc *apiframework.ServiceDesc) {\n\t\t\trootPkg := svcDesc.RootPkg\n\t\t\tif rootPkg.ImportPath != mainPkgPath {\n\t\t\t\tf.Anon(rootPkg.ImportPath.String())\n\t\t\t}\n\t\t})\n\t}\n\n\t// Make sure auth handlers and global middleware are imported as well so they get registered.\n\tif fw, ok := p.Desc.Framework.Get(); ok {\n\t\tif ah, ok := fw.AuthHandler.Get(); ok {\n\t\t\tf.Anon(ah.Decl.File.Pkg.ImportPath.String())\n\t\t}\n\t\tfor _, mw := range fw.GlobalMiddleware {\n\t\t\tf.Anon(mw.Decl.File.Pkg.ImportPath.String())\n\t\t}\n\t}\n\n\treturn GenAppConfig(p, option.None[testParams]())\n}\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/maingen_test.go",
    "content": "package maingen_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen\"\n\t\"encr.dev/v2/codegen/apigen/maingen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tmaingen.GenerateForInternalPackageTests = true\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tloader := pkginfo.New(gen.Context)\n\t\tmainModule := loader.MainModule()\n\t\tparams := apigen.Params{\n\t\t\tGen:           gen,\n\t\t\tDesc:          desc,\n\t\t\tMainModule:    mainModule,\n\t\t\tRuntimeModule: loader.RuntimeModule(),\n\t\t}\n\t\tstaticConfig := apigen.Process(params)\n\n\t\t// Create a synthetic file for golden tests to catch config changes.\n\t\tf := gen.InjectFile(\"synthetic/static_config\", \"synthetic\", mainModule.RootDir.Join(\"synthetic\"), \"static_config.go\", \"static_config\")\n\t\tconfigData, _ := json.MarshalIndent(staticConfig, \"\", \"\\t\")\n\t\tf.Jen.Comment(fmt.Sprintf(\"\\nThis is a synthetic file describing the generated static config:\\n\\n%s\\n\", configData))\n\t}\n\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/testdata/auth_handler.txt",
    "content": "-- code.go --\npackage code\n\nimport (\"context\"; \"encore.dev/beta/auth\")\n\n//encore:api\nfunc Foo(ctx context.Context) error { return nil }\n\ntype MyAuthData struct { Username string }\n\n//encore:authhandler\nfunc AuthHandler(context.Context, string) (auth.UID, *MyAuthData, error) {\n    return \"\", nil, nil\n}\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:encore_internal/main/main.go --\npackage main\n\nimport (\n\tappinit \"encore.dev/appruntime/apisdk/app/appinit\"\n\t_ \"example.com\"\n)\n\nfunc main() {\n\tappinit.AppMain()\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Private,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:encore_internal__authhandler.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\t__model \"encore.dev/appruntime/exported/model\"\n\terrs \"encore.dev/beta/errs\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nvar EncoreInternal_authhandler_AuthDesc_AuthHandler = &__api.AuthHandlerDesc[string]{\n\tAuthHandler: func(ctx context.Context, params string) (info __model.AuthInfo, err error) {\n\t\tinfo.UID, info.UserData, err = AuthHandler(ctx, params)\n\t\treturn info, err\n\t},\n\tDecodeAuth: func(httpReq *http.Request) (params string, err error) {\n\t\tif auth := httpReq.Header.Get(\"Authorization\"); auth != \"\" {\n\t\t\tfor _, prefix := range [...]string{\"Bearer \", \"Token \"} {\n\t\t\t\tif strings.HasPrefix(auth, prefix) {\n\t\t\t\t\tif params = auth[len(prefix):]; params != \"\" {\n\t\t\t\t\t\treturn params, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn \"\", errs.B().Code(errs.Unauthenticated).Msg(\"invalid auth param\").Err()\n\t},\n\tDefLoc:            uint32(0x0),\n\tEndpoint:          \"AuthHandler\",\n\tHasAuthData:       true,\n\tScrubRequestPaths: nil,\n\tService:           \"code\",\n\tSvcNum:            1,\n}\n\nfunc init() {\n\t__api.RegisterAuthHandler(EncoreInternal_authhandler_AuthDesc_AuthHandler)\n}\n\nfunc init() {\n\t__api.RegisterAuthDataType[*MyAuthData]()\n}\n-- want:synthetic/static_config.go --\npackage synthetic\n\n/*\n\nThis is a synthetic file describing the generated static config:\n\n{\n\t\"EncoreCompiler\": \"\",\n\t\"AppCommit\": {\n\t\t\"revision\": \"\",\n\t\t\"uncommitted\": false\n\t},\n\t\"CORSAllowHeaders\": null,\n\t\"CORSExposeHeaders\": null,\n\t\"PubsubTopics\": {},\n\t\"Testing\": false,\n\t\"TestServiceMap\": {\n\t\t\"code\": \"testing_path:code\"\n\t},\n\t\"TestAppRootPath\": \"testing_path:main\",\n\t\"PrettyPrintLogs\": false,\n\t\"BundledServices\": [\n\t\t\"code\"\n\t],\n\t\"EmbeddedEnvs\": {}\n}\n*/\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/testdata/basic.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\n//encore:api\nfunc Foo(ctx context.Context) error { return nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:encore_internal/main/main.go --\npackage main\n\nimport (\n\tappinit \"encore.dev/appruntime/apisdk/app/appinit\"\n\t_ \"example.com\"\n)\n\nfunc main() {\n\tappinit.AppMain()\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Private,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:synthetic/static_config.go --\npackage synthetic\n\n/*\n\nThis is a synthetic file describing the generated static config:\n\n{\n\t\"EncoreCompiler\": \"\",\n\t\"AppCommit\": {\n\t\t\"revision\": \"\",\n\t\t\"uncommitted\": false\n\t},\n\t\"CORSAllowHeaders\": null,\n\t\"CORSExposeHeaders\": null,\n\t\"PubsubTopics\": {},\n\t\"Testing\": false,\n\t\"TestServiceMap\": {\n\t\t\"code\": \"testing_path:code\"\n\t},\n\t\"TestAppRootPath\": \"testing_path:main\",\n\t\"PrettyPrintLogs\": false,\n\t\"BundledServices\": [\n\t\t\"code\"\n\t],\n\t\"EmbeddedEnvs\": {}\n}\n*/\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/testdata/multiple_services.txt",
    "content": "-- foo/foo.go --\npackage foo\n\nimport \"context\"\n\n//encore:api\nfunc Foo(ctx context.Context) error { return nil }\n-- bar/bar.go --\npackage bar\n\nimport \"context\"\n\n//encore:api\nfunc Bar(ctx context.Context) error { return nil }\n-- want:bar/encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage bar\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tBar(ctx context.Context) error\n}\n-- want:bar/encore_internal__api.go --\npackage bar\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Bar, Bar)\n}\n\ntype EncoreInternal_BarReq struct{}\n\ntype EncoreInternal_BarResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Bar = &__api.Desc[*EncoreInternal_BarReq, EncoreInternal_BarResp]{\n\tAccess: __api.Private,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_BarReq) (EncoreInternal_BarResp, error) {\n\t\terr := Bar(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_BarReq) (*EncoreInternal_BarReq, error) {\n\t\tvar clone *EncoreInternal_BarReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_BarResp) (EncoreInternal_BarResp, error) {\n\t\tvar clone EncoreInternal_BarResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_BarResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_BarReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_BarReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_BarReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_BarResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Bar\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/bar.Bar\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/bar.Bar\",\n\tReqPath: func(reqData *EncoreInternal_BarReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/bar.Bar\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_BarReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"bar\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:encore_internal/main/main.go --\npackage main\n\nimport (\n\tappinit \"encore.dev/appruntime/apisdk/app/appinit\"\n\t_ \"example.com/bar\"\n\t_ \"example.com/foo\"\n)\n\nfunc main() {\n\tappinit.AppMain()\n}\n-- want:foo/encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage foo\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:foo/encore_internal__api.go --\npackage foo\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Private,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\terr := Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/foo.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/foo.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/foo.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"foo\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               2,\n\tTags:                 nil,\n}\n-- want:synthetic/static_config.go --\npackage synthetic\n\n/*\n\nThis is a synthetic file describing the generated static config:\n\n{\n\t\"EncoreCompiler\": \"\",\n\t\"AppCommit\": {\n\t\t\"revision\": \"\",\n\t\t\"uncommitted\": false\n\t},\n\t\"CORSAllowHeaders\": null,\n\t\"CORSExposeHeaders\": null,\n\t\"PubsubTopics\": {},\n\t\"Testing\": false,\n\t\"TestServiceMap\": {\n\t\t\"bar\": \"testing_path:bar\",\n\t\t\"foo\": \"testing_path:foo\"\n\t},\n\t\"TestAppRootPath\": \"testing_path:main\",\n\t\"PrettyPrintLogs\": false,\n\t\"BundledServices\": [\n\t\t\"bar\",\n\t\t\"foo\"\n\t],\n\t\"EmbeddedEnvs\": {}\n}\n*/\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/testdata/service_struct.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\n\n//encore:service\ntype Service struct{}\n\n//encore:api\nfunc (s *Service) Foo(ctx context.Context) error { return nil }\n\nfunc initService() (*Service, error) { return nil, nil }\n\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\nimport \"context\"\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\nfunc Foo(ctx context.Context) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.Foo(ctx)\n}\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\tFoo(ctx context.Context) error\n}\n-- want:encore_internal/main/main.go --\npackage main\n\nimport (\n\tappinit \"encore.dev/appruntime/apisdk/app/appinit\"\n\t_ \"example.com\"\n)\n\nfunc main() {\n\tappinit.AppMain()\n}\n-- want:encore_internal__api.go --\npackage code\n\nimport (\n\t\"context\"\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc init() {\n\t__api.RegisterEndpoint(EncoreInternal_api_APIDesc_Foo, Foo)\n}\n\ntype EncoreInternal_FooReq struct{}\n\ntype EncoreInternal_FooResp = __api.Void\n\nvar EncoreInternal_api_APIDesc_Foo = &__api.Desc[*EncoreInternal_FooReq, EncoreInternal_FooResp]{\n\tAccess: __api.Private,\n\tAppHandler: func(ctx context.Context, reqData *EncoreInternal_FooReq) (EncoreInternal_FooResp, error) {\n\t\tsvc, initErr := EncoreInternal_svcstruct_Service.Get()\n\t\tif initErr != nil {\n\t\t\treturn __api.Void{}, initErr\n\t\t}\n\t\terr := svc.Foo(ctx)\n\t\tif err != nil {\n\t\t\treturn __api.Void{}, err\n\t\t}\n\t\treturn __api.Void{}, nil\n\t},\n\tCloneReq: func(r *EncoreInternal_FooReq) (*EncoreInternal_FooReq, error) {\n\t\tvar clone *EncoreInternal_FooReq\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tCloneResp: func(r EncoreInternal_FooResp) (EncoreInternal_FooResp, error) {\n\t\tvar clone EncoreInternal_FooResp\n\t\tbytes, err := jsoniter.ConfigDefault.Marshal(r)\n\t\tif err == nil {\n\t\t\terr = jsoniter.ConfigDefault.Unmarshal(bytes, &clone)\n\t\t}\n\t\treturn clone, err\n\t},\n\tDecodeExternalResp: func(httpResp *http.Response, json jsoniter.API) (resp EncoreInternal_FooResp, err error) {\n\t\treturn __api.Void{}, nil\n\t},\n\tDecodeReq: func(httpReq *http.Request, ps __api.UnnamedParams, json jsoniter.API) (reqData *EncoreInternal_FooReq, pathParams __api.UnnamedParams, err error) {\n\t\treqData = new(EncoreInternal_FooReq)\n\t\treturn reqData, nil, nil\n\t},\n\tDefLoc: uint32(0x0),\n\tEncodeExternalReq: func(reqData *EncoreInternal_FooReq, stream *jsoniter.Stream) (httpHeader http.Header, queryString url.Values, err error) {\n\t\treturn nil, nil, nil\n\t},\n\tEncodeResp: func(w http.ResponseWriter, json jsoniter.API, resp EncoreInternal_FooResp, status int) (err error) {\n\t\treturn nil\n\t},\n\tEndpoint:            \"Foo\",\n\tFallback:            false,\n\tGlobalMiddlewareIDs: []string{},\n\tMethods:             []string{\"GET\", \"POST\"},\n\tPath:                \"/code.Foo\",\n\tPathParamNames:      nil,\n\tRaw:                 false,\n\tRawHandler:          nil,\n\tRawPath:             \"/code.Foo\",\n\tReqPath: func(reqData *EncoreInternal_FooReq) (string, __api.UnnamedParams, error) {\n\t\treturn \"/code.Foo\", nil, nil\n\t},\n\tReqUserPayload: func(reqData *EncoreInternal_FooReq) any {\n\t\treturn nil\n\t},\n\tScrubRequestHeaders:  nil,\n\tScrubRequestPaths:    nil,\n\tScrubResponseHeaders: nil,\n\tScrubResponsePaths:   nil,\n\tService:              \"code\",\n\tServiceMiddleware:    []*__api.Middleware{},\n\tSvcNum:               1,\n\tTags:                 nil,\n}\n-- want:encore_internal__svcstruct.go --\npackage code\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"code\",\n\tSetup:       initService,\n\tSetupDefLoc: uint32(0x0),\n}\n-- want:synthetic/static_config.go --\npackage synthetic\n\n/*\n\nThis is a synthetic file describing the generated static config:\n\n{\n\t\"EncoreCompiler\": \"\",\n\t\"AppCommit\": {\n\t\t\"revision\": \"\",\n\t\t\"uncommitted\": false\n\t},\n\t\"CORSAllowHeaders\": null,\n\t\"CORSExposeHeaders\": null,\n\t\"PubsubTopics\": {},\n\t\"Testing\": false,\n\t\"TestServiceMap\": {\n\t\t\"code\": \"testing_path:code\"\n\t},\n\t\"TestAppRootPath\": \"testing_path:main\",\n\t\"PrettyPrintLogs\": false,\n\t\"BundledServices\": [\n\t\t\"code\"\n\t],\n\t\"EmbeddedEnvs\": {}\n}\n*/\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/testdata/subscription.txt",
    "content": "-- code.go --\npackage code\n\nimport \"context\"\nimport \"encore.dev/pubsub\"\n\ntype Event struct {}\n\nvar Topic = pubsub.NewTopic[*Event](\"topic\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\n\nvar _ = pubsub.NewSubscription(Topic, \"subscription\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: func(ctx context.Context, event *Event) error {\n            return nil\n        },\n    },\n)\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage code\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n-- want:encore_internal/main/main.go --\npackage main\n\nimport (\n\tappinit \"encore.dev/appruntime/apisdk/app/appinit\"\n\t_ \"example.com\"\n)\n\nfunc main() {\n\tappinit.AppMain()\n}\n-- want:encore_internal__api.go --\npackage code\n\nfunc init() {}\n-- want:synthetic/static_config.go --\npackage synthetic\n\n/*\n\nThis is a synthetic file describing the generated static config:\n\n{\n\t\"EncoreCompiler\": \"\",\n\t\"AppCommit\": {\n\t\t\"revision\": \"\",\n\t\t\"uncommitted\": false\n\t},\n\t\"CORSAllowHeaders\": null,\n\t\"CORSExposeHeaders\": null,\n\t\"PubsubTopics\": {\n\t\t\"topic\": {\n\t\t\t\"Subscriptions\": {\n\t\t\t\t\"subscription\": {\n\t\t\t\t\t\"Service\": \"code\",\n\t\t\t\t\t\"SvcNum\": 1,\n\t\t\t\t\t\"TraceIdx\": 0,\n\t\t\t\t\t\"ScrubPaths\": null\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"ScrubPaths\": null\n\t\t}\n\t},\n\t\"Testing\": false,\n\t\"TestServiceMap\": {\n\t\t\"code\": \"testing_path:code\"\n\t},\n\t\"TestAppRootPath\": \"testing_path:main\",\n\t\"PrettyPrintLogs\": false,\n\t\"BundledServices\": [\n\t\t\"code\"\n\t],\n\t\"EmbeddedEnvs\": {}\n}\n*/\n"
  },
  {
    "path": "v2/codegen/apigen/maingen/testgen.go",
    "content": "package maingen\n\nimport (\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/codegen\"\n)\n\nfunc genTestConfigs(p GenParams, test codegen.TestConfig) *config.Static {\n\tfor _, pkg := range test.Packages {\n\t\t// HACK(andre) Ensure we always import the testsupport package in every test binary,\n\t\t// since the \"testing\" package depends on it for the testing runtime hooks.\n\t\tp.Gen.InsertTestSupport(pkg)\n\n\t\t//\tf := p.Gen.InjectFile(pkg.ImportPath+\"!test\", pkg.Name+\"_test\", pkg.FSPath, \"encore_internal__test.go\", \"encoretest\")\n\t\t//\tf.ImportAnon(\"encore.dev/appruntime/shared/testsupport\")\n\t\t//\n\t\t//\tif fw, ok := p.Desc.Framework.Get(); ok {\n\t\t//\t\tif ah, ok := fw.AuthHandler.Get(); ok {\n\t\t//\t\t\tf.ImportAnon(ah.Decl.File.Pkg.ImportPath)\n\t\t//\t\t}\n\t\t//\t\tfor _, mw := range fw.GlobalMiddleware {\n\t\t//\t\t\tf.ImportAnon(mw.File.Pkg.ImportPath)\n\t\t//\t\t}\n\t\t//\t}\n\t}\n\n\treturn GenAppConfig(p, option.Some(testParams{\n\t\tEnvsToEmbed:        test.EnvsToEmbed,\n\t\tExternalTestBinary: len(test.EnvsToEmbed) > 0,\n\t}))\n}\n"
  },
  {
    "path": "v2/codegen/apigen/middlewaregen/middlewaregen.go",
    "content": "package middlewaregen\n\nimport (\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n)\n\nfunc Gen(gen *codegen.Generator, mws []*middleware.Middleware, svcStruct option.Option[*codegen.VarDecl]) map[*middleware.Middleware]*codegen.VarDecl {\n\tmwMap := make(map[*middleware.Middleware]*codegen.VarDecl)\n\n\tpkgMap := make(map[*pkginfo.Package][]*middleware.Middleware)\n\tfor _, mw := range mws {\n\t\tpkgMap[mw.File.Pkg] = append(pkgMap[mw.File.Pkg], mw)\n\t}\n\n\tfor pkg, mws := range pkgMap {\n\t\tf := gen.File(pkg, \"middleware\")\n\t\tfor _, mw := range mws {\n\t\t\tmwMap[mw] = genMiddleware(gen, f, mw, svcStruct)\n\t\t}\n\t}\n\n\treturn mwMap\n}\n\nfunc genMiddleware(gen *codegen.Generator, f *codegen.File, mw *middleware.Middleware, svcStruct option.Option[*codegen.VarDecl]) *codegen.VarDecl {\n\tinvoke := Qual(mw.File.Pkg.ImportPath.String(), mw.Decl.Name)\n\tif !mw.Global && mw.Recv.Present() && svcStruct.Present() {\n\t\tinvoke = Func().Params(\n\t\t\tId(\"req\").Qual(\"encore.dev/middleware\", \"Request\"),\n\t\t\tId(\"next\").Qual(\"encore.dev/middleware\", \"Next\"),\n\t\t).Params(Qual(\"encore.dev/middleware\", \"Response\")).BlockFunc(func(g *Group) {\n\t\t\tg.List(Id(\"svc\"), Err()).Op(\":=\").Add(svcStruct.MustGet().Qual()).Dot(\"Get\").Call()\n\t\t\tg.If(Err().Op(\"!=\").Nil()).Block(\n\t\t\t\tReturn(Qual(\"encore.dev/middleware\", \"Response\").Values(Dict{\n\t\t\t\t\tId(\"Err\"):        Err(),\n\t\t\t\t\tId(\"HTTPStatus\"): Qual(\"encore.dev/beta/errs\", \"HTTPStatus\").Call(Err()),\n\t\t\t\t})),\n\t\t\t)\n\t\t\tg.Return(Id(\"svc\").Dot(mw.Decl.Name).Call(Id(\"req\"), Id(\"next\")))\n\t\t})\n\t}\n\n\tdecl := f.VarDecl(\"middleware\", mw.Decl.Name).Value(Op(\"&\").Qual(\"encore.dev/appruntime/apisdk/api\", \"Middleware\").Values(Dict{\n\t\tId(\"ID\"):      Lit(mw.ID()),\n\t\tId(\"PkgName\"): Lit(mw.File.Pkg.Name),\n\t\tId(\"Name\"):    Lit(mw.Decl.Name),\n\t\tId(\"Global\"):  Lit(mw.Global),\n\t\tId(\"DefLoc\"):  Lit(gen.TraceNodes.Middleware(mw)),\n\t\tId(\"Invoke\"):  invoke,\n\t}))\n\n\tif mw.Global {\n\t\tf.Jen.Func().Id(\"init\").Params().Block(\n\t\t\tQual(\"encore.dev/appruntime/apisdk/api\", \"RegisterGlobalMiddleware\").Call(decl.Qual()),\n\t\t)\n\t}\n\n\treturn decl\n}\n"
  },
  {
    "path": "v2/codegen/apigen/middlewaregen/middlewaregen_test.go",
    "content": "package middlewaregen\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/servicestructgen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tGen(gen, desc.Framework.MustGet().GlobalMiddleware, option.None[*codegen.VarDecl]())\n\t\tfor _, svc := range desc.Services {\n\t\t\tvar svcStruct option.Option[*codegen.VarDecl]\n\t\t\tif fw, ok := svc.Framework.Get(); ok {\n\t\t\t\tif ss, ok := fw.ServiceStruct.Get(); ok {\n\t\t\t\t\tsvcStruct = option.Some(servicestructgen.Gen(gen, svc, ss))\n\t\t\t\t}\n\t\t\t}\n\t\t\tGen(gen, svc.Framework.MustGet().Middleware, svcStruct)\n\t\t}\n\t}\n\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/middlewaregen/testdata/basic.txt",
    "content": "-- basic.go --\npackage basic\n\nimport (\"context\"; \"encore.dev/middleware\")\n\n//encore:middleware target=all\nfunc Middleware(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n// Note: we need an API endpoint to be able to define service-specific middleware\n//encore:api\nfunc API(context.Context) error { return nil }\n-- want:encore_internal__middleware.go --\npackage basic\n\nimport __api \"encore.dev/appruntime/apisdk/api\"\n\nvar EncoreInternal_middleware_middleware_Middleware = &__api.Middleware{\n\tDefLoc:  uint32(0x0),\n\tGlobal:  false,\n\tID:      \"example.com.Middleware\",\n\tInvoke:  Middleware,\n\tName:    \"Middleware\",\n\tPkgName: \"basic\",\n}\n"
  },
  {
    "path": "v2/codegen/apigen/middlewaregen/testdata/global.txt",
    "content": "-- global.go --\npackage global\n\nimport \"encore.dev/middleware\"\n\n//encore:middleware global target=all\nfunc Middleware(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n-- want:encore_internal__middleware.go --\npackage global\n\nimport __api \"encore.dev/appruntime/apisdk/api\"\n\nfunc init() {\n\t__api.RegisterGlobalMiddleware(EncoreInternal_middleware_middleware_Middleware)\n}\n\nvar EncoreInternal_middleware_middleware_Middleware = &__api.Middleware{\n\tDefLoc:  uint32(0x0),\n\tGlobal:  true,\n\tID:      \"example.com.Middleware\",\n\tInvoke:  Middleware,\n\tName:    \"Middleware\",\n\tPkgName: \"global\",\n}\n"
  },
  {
    "path": "v2/codegen/apigen/middlewaregen/testdata/service_struct.txt",
    "content": "-- code.go --\npackage code\n\nimport (\"context\"; \"encore.dev/middleware\")\n\n//encore:service\ntype Service struct{}\n\n//encore:middleware target=all\nfunc (s *Service) Middleware(req middleware.Request, next middleware.Next) middleware.Response {\n    return next(req)\n}\n\n// Note: we need an API endpoint to be able to define service-specific middleware\n//encore:api\nfunc (s *Service) API(context.Context) error { return nil }\n-- want:encore_internal__middleware.go --\npackage code\n\nimport (\n\t__api \"encore.dev/appruntime/apisdk/api\"\n\terrs \"encore.dev/beta/errs\"\n\tmiddleware \"encore.dev/middleware\"\n)\n\nvar EncoreInternal_middleware_middleware_Middleware = &__api.Middleware{\n\tDefLoc: uint32(0x0),\n\tGlobal: false,\n\tID:     \"example.com.Middleware\",\n\tInvoke: func(req middleware.Request, next middleware.Next) middleware.Response {\n\t\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\t\tif err != nil {\n\t\t\treturn middleware.Response{\n\t\t\t\tErr:        err,\n\t\t\t\tHTTPStatus: errs.HTTPStatus(err),\n\t\t\t}\n\t\t}\n\t\treturn svc.Middleware(req, next)\n\t},\n\tName:    \"Middleware\",\n\tPkgName: \"code\",\n}\n-- want:encore_internal__svcstruct.go --\npackage code\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"code\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/servicestructgen/servicestructgen.go",
    "content": "package servicestructgen\n\nimport (\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n)\n\nfunc Gen(gen *codegen.Generator, svc *app.Service, s *servicestruct.ServiceStruct) *codegen.VarDecl {\n\tinitFuncName := option.Map(s.Init, func(init *schema.FuncDecl) *Statement {\n\t\treturn Id(init.Name)\n\t}).GetOrElse(Nil())\n\n\tf := gen.File(s.Decl.File.Pkg, \"svcstruct\")\n\tdecl := f.VarDecl(s.Decl.Name).Value(Op(\"&\").Qual(\"encore.dev/appruntime/apisdk/service\", \"Decl\").Types(\n\t\tId(s.Decl.Name),\n\t).Values(Dict{\n\t\tId(\"Service\"):     Lit(svc.Name),\n\t\tId(\"Name\"):        Lit(s.Decl.Name),\n\t\tId(\"Setup\"):       initFuncName,\n\t\tId(\"SetupDefLoc\"): Lit(gen.TraceNodes.SvcStruct(s)),\n\t}))\n\n\tf.Jen.Func().Id(\"init\").Params().Block(\n\t\tQual(\"encore.dev/appruntime/apisdk/service\", \"Register\").Call(decl.Qual()),\n\t)\n\n\treturn decl\n}\n"
  },
  {
    "path": "v2/codegen/apigen/servicestructgen/servicestructgen_test.go",
    "content": "package servicestructgen\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tsvc := desc.Services[0]\n\t\tGen(gen, svc, svc.Framework.MustGet().ServiceStruct.MustGet())\n\t}\n\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/apigen/servicestructgen/testdata/basic.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\n//encore:service\ntype Service struct {\n}\n\n//encore:api\nfunc API(context.Context) error { return nil }\n-- want:encore_internal__svcstruct.go --\npackage basic\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"basic\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/servicestructgen/testdata/init_svc.txt",
    "content": "-- basic.go --\npackage basic\n\nimport \"context\"\n\n//encore:service\ntype Service struct {\n}\n\nfunc initService() (*Service, error) {\n    return nil, nil\n}\n\n//encore:api\nfunc API(context.Context) error { return nil }\n-- want:encore_internal__svcstruct.go --\npackage basic\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"basic\",\n\tSetup:       initService,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/typescrub/jen.go",
    "content": "package typescrub\n\nimport (\n\t\"fmt\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\n\t. \"github.com/dave/jennifer/jen\"\n)\n\nfunc HeadersToJen(headers []string) Code {\n\tif len(headers) == 0 {\n\t\treturn Nil()\n\t}\n\treturn Map(String()).Bool().ValuesFunc(func(g *Group) {\n\t\tfor _, h := range headers {\n\t\t\tg.Add(Lit(h).Op(\":\").Lit(true))\n\t\t}\n\t})\n}\n\nfunc PathsToJen(paths []scrub.Path) Code {\n\tif len(paths) == 0 {\n\t\treturn Nil()\n\t}\n\treturn Index().Qual(\"encore.dev/appruntime/exported/scrub\", \"Path\").ValuesFunc(func(g *Group) {\n\t\tfor _, p := range paths {\n\t\t\tg.Add(scrubPathToJen(p))\n\t\t}\n\t})\n}\n\nfunc scrubPathToJen(path scrub.Path) Code {\n\treturn Index().Qual(\"encore.dev/appruntime/exported/scrub\", \"PathEntry\").ValuesFunc(func(g *Group) {\n\t\tfor _, pe := range path {\n\t\t\tkind := \"\"\n\t\t\tswitch pe.Kind {\n\t\t\tcase scrub.ObjectField:\n\t\t\t\tkind = \"ObjectField\"\n\t\t\tcase scrub.MapKey:\n\t\t\t\tkind = \"MapKey\"\n\t\t\tcase scrub.MapValue:\n\t\t\t\tkind = \"MapValue\"\n\t\t\tdefault:\n\t\t\t\tpanic(fmt.Sprintf(\"unknown PathEntry.Kind: %v\", pe.Kind))\n\t\t\t}\n\t\t\tg.Add(\n\t\t\t\tValues(\n\t\t\t\t\tId(\"Kind\").Op(\":\").Qual(\"encore.dev/appruntime/exported/scrub\", kind),\n\t\t\t\t\tId(\"FieldName\").Op(\":\").Lit(pe.FieldName),\n\t\t\t\t\tId(\"CaseSensitive\").Op(\":\").Lit(pe.CaseSensitive),\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/apigen/typescrub/typescrub.go",
    "content": "// Package typescrub computes scrub paths for schema types.\npackage typescrub\n\nimport (\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n)\n\n// New constructs a new Computer.\nfunc NewComputer(log zerolog.Logger) *Computer {\n\treturn &Computer{\n\t\tlog:       log.With().Str(\"component\", \"typescrub\").Logger(),\n\t\tdeclCache: make(map[declCacheKey]declResult),\n\t}\n}\n\n// Computer computes scrub paths for types, caching the computation.\n// It can safely be reused across multiple types.\n// It is not safe for concurrent use.\ntype Computer struct {\n\tlog zerolog.Logger\n\n\t// declCache caches the scrub paths for encountered declarations\n\tdeclCache map[declCacheKey]declResult\n}\n\ntype Desc struct {\n\tPayload []scrub.Path\n\tHeaders []string\n}\n\ntype ParseMode int\n\nconst (\n\t// AuthHandler specifies that the type is an auth handler.\n\tAuthHandler ParseMode = 1 << iota\n\t// DisableScrubbing specifies that scrubbing should be disabled.\n\t// Used for local development.\n\tDisableScrubbing\n)\n\n// Compute computes the scrub paths for the given typ.\n// It is not safe for concurrent use.\nfunc (c *Computer) Compute(typ schema.Type, mode ParseMode) Desc {\n\tif typ == nil || (mode&DisableScrubbing) != 0 {\n\t\treturn Desc{}\n\t}\n\n\tp := &typeParser{c: c, mode: mode}\n\tres := p.typ(typ)\n\n\t// Did we exceed the steps?\n\tif p.steps > maxSteps {\n\t\tc.log.Error().Msg(\"typescrub.Compute aborted due to exceeding max steps\")\n\t}\n\n\treturn Desc{\n\t\tPayload: res.scrub,\n\t\tHeaders: res.headers,\n\t}\n}\n\nconst maxSteps = 10000\n\ntype typeParser struct {\n\tc    *Computer\n\tmode ParseMode\n\n\t// steps is a fail-safe to catch any potential infinite loops.\n\tsteps int\n}\n\ntype declCacheKey struct {\n\tqn   pkginfo.QualifiedName\n\tmode ParseMode\n}\n\nfunc (p *typeParser) decl(d *schema.TypeDecl) declResult {\n\tkey := declCacheKey{d.Info.QualifiedName(), p.mode}\n\tif res, ok := p.c.declCache[key]; ok {\n\t\treturn res\n\t}\n\n\t// Mark that we're beginning to process this decl,\n\t// so we avoid infinite recursion.\n\tp.c.declCache[key] = declResult{}\n\n\t// Do the actual parsing.\n\tres := p.typ(d.Type)\n\n\t// We're done, cache the final result.\n\tp.c.declCache[key] = res\n\treturn res\n}\n\ntype declResult struct {\n\tscrub      []scrub.Path\n\ttypeParams []typeParamPath\n\theaders    []string\n}\n\nfunc (p *typeParser) typ(typ schema.Type) declResult {\n\tif typ == nil {\n\t\treturn declResult{}\n\t}\n\tp.steps++\n\tif p.steps > maxSteps {\n\t\treturn declResult{}\n\t}\n\n\tswitch t := typ.(type) {\n\tcase schema.NamedType:\n\t\tdecl := p.decl(t.Decl())\n\t\tout := declResult{\n\t\t\t// Clone the paths since we're modifying them.\n\t\t\tscrub: slices.Clone(decl.scrub),\n\n\t\t\t// Copy the headers directly since they never get modified,\n\t\t\t// since we only care about the top-level type's headers.\n\t\t\theaders: decl.headers,\n\t\t}\n\n\t\tfor i, arg := range t.TypeArgs {\n\t\t\targRes := p.typ(arg)\n\t\t\tif len(argRes.scrub) == 0 && len(argRes.typeParams) == 0 {\n\t\t\t\t// Nothing to do\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// For every type parameter, find the places\n\t\t\t// where it's used and copy it to the combined result.\n\t\t\tfor _, declParam := range decl.typeParams {\n\t\t\t\tif declParam.paramIdx == i {\n\t\t\t\t\tfor _, s := range argRes.scrub {\n\t\t\t\t\t\tpath := append(slices.Clone(declParam.p), s...)\n\t\t\t\t\t\tout.scrub = append(out.scrub, path)\n\t\t\t\t\t}\n\t\t\t\t\tfor _, s := range argRes.typeParams {\n\t\t\t\t\t\tpath := append(slices.Clone(declParam.p), s.p...)\n\t\t\t\t\t\tout.typeParams = append(out.typeParams, typeParamPath{\n\t\t\t\t\t\t\tp:        path,\n\t\t\t\t\t\t\tparamIdx: s.paramIdx,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t\treturn out\n\n\tcase schema.PointerType:\n\t\treturn p.typ(t.Elem)\n\n\tcase schema.OptionType:\n\t\treturn p.typ(t.Value)\n\n\tcase schema.ListType:\n\t\treturn p.typ(t.Elem)\n\n\tcase schema.MapType:\n\t\tkey := p.typ(t.Key)\n\t\tval := p.typ(t.Value)\n\n\t\tcombined := declResult{\n\t\t\tscrub:      make([]scrub.Path, 0, len(key.scrub)+len(val.scrub)),\n\t\t\ttypeParams: make([]typeParamPath, 0, len(key.typeParams)+len(val.typeParams)),\n\t\t}\n\t\tfor i, res := range [...]declResult{key, val} {\n\t\t\tkind := scrub.MapKey\n\t\t\tif i == 1 {\n\t\t\t\tkind = scrub.MapValue\n\t\t\t}\n\t\t\tfor _, e := range res.scrub {\n\t\t\t\tpath := append(scrub.Path{{Kind: kind}}, e...)\n\t\t\t\tcombined.scrub = append(combined.scrub, path)\n\t\t\t}\n\t\t\tfor _, e := range res.typeParams {\n\t\t\t\tpath := append(scrub.Path{{Kind: kind}}, e.p...)\n\t\t\t\tcombined.typeParams = append(combined.typeParams, typeParamPath{p: path, paramIdx: e.paramIdx})\n\t\t\t}\n\t\t}\n\t\treturn combined\n\n\tcase schema.StructType:\n\t\tvar out declResult\n\t\tfor _, f := range t.Fields {\n\t\t\tsensitive, fieldName, caseSensitive := isSensitive(f)\n\n\t\t\t// For Auth Handlers everything is sensitive.\n\t\t\tif (p.mode & AuthHandler) != 0 {\n\t\t\t\tsensitive = true\n\t\t\t}\n\n\t\t\tif sensitive {\n\t\t\t\t// If the field is sensitive add it directly.\n\t\t\t\tout.scrub = append(out.scrub, scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}})\n\n\t\t\t\t// If this is a header field, add it to the headers to scrub.\n\t\t\t\tif headerName, ok := isHeader(f); ok {\n\t\t\t\t\tout.headers = append(out.headers, headerName)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Otherwise check the type and see if there's anything to scrub within it.\n\t\t\t\tfieldRes := p.typ(f.Type)\n\t\t\t\tfor _, e := range fieldRes.scrub {\n\t\t\t\t\tpath := append(scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}, e...)\n\t\t\t\t\tout.scrub = append(out.scrub, path)\n\t\t\t\t}\n\t\t\t\tfor _, e := range fieldRes.typeParams {\n\t\t\t\t\tpath := append(scrub.Path{{Kind: scrub.ObjectField, FieldName: fieldName, CaseSensitive: caseSensitive}}, e.p...)\n\t\t\t\t\tout.typeParams = append(out.typeParams, typeParamPath{p: path, paramIdx: e.paramIdx})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn out\n\n\tcase schema.BuiltinType:\n\t\t// Nothing to do\n\t\treturn declResult{}\n\n\tcase schema.TypeParamRefType:\n\t\treturn declResult{typeParams: []typeParamPath{{\n\t\t\tparamIdx: t.Index,\n\t\t}}}\n\n\tdefault:\n\t\tp.c.log.Warn().Msgf(\"got unexpected schema.Type %T in typescrub, skipping\", t)\n\t\treturn declResult{}\n\t}\n}\n\nfunc isSensitive(f schema.StructField) (sensitive bool, fieldName string, caseSensitive bool) {\n\tfieldName = f.Name.GetOrElse(\"\")\n\n\t// Check for json tag override of field name\n\tif jsonTag, err := f.Tag.Get(\"json\"); err == nil && jsonTag.Name != \"\" && jsonTag.Name != \"-\" {\n\t\tfieldName = jsonTag.Name\n\t\tcaseSensitive = true\n\t}\n\n\tfieldName = strconv.Quote(fieldName) // the scrub package wants exact byte matches\n\n\tif encoreTag, err := f.Tag.Get(\"encore\"); err == nil {\n\t\tsensitive = encoreTag.Name == \"sensitive\" || slices.Contains(encoreTag.Options, \"sensitive\")\n\t}\n\treturn sensitive, fieldName, caseSensitive\n}\n\nfunc isHeader(f schema.StructField) (headerName string, ok bool) {\n\tif headerTag, err := f.Tag.Get(\"header\"); err == nil {\n\t\tname := headerTag.Name\n\t\tif name == \"\" {\n\t\t\tname = f.Name.GetOrElse(\"\")\n\t\t}\n\t\treturn name, true\n\t}\n\treturn \"\", false\n}\n\ntype typeParamPath struct {\n\tp        scrub.Path\n\tparamIdx int\n}\n"
  },
  {
    "path": "v2/codegen/apigen/typescrub/typescrub_test.go",
    "content": "package typescrub\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/scrub\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n)\n\nfunc TestScrub(t *testing.T) {\n\tc := qt.New(t)\n\treqType := testParse(c, `\n-- svc/svc.go --\npackage svc\nimport (\n\t\"context\"\n\t\"encore.dev/types/option\"\n)\n\ntype Params struct {\n\tFoo string SCRUB\n\n\tNestedScrub struct {\n\t\tInner string SCRUB\n\t} SCRUB\n\n\tNested struct {\n\t\tInner string SCRUB\n\t}\n\n\tList []string SCRUB\n\tListInner []struct { Inner string SCRUB }\n\n\tRecurseScrub *Params SCRUB\n\t// Ideally this should yield nested scrubs but we don't support that yet.\n\tRecurse *Params\n\n\tGen      Generic[string]\n\tGenScrub Generic[string] SCRUB\n\tGenInner Generic[Bar]\n\tMulti    NestedGeneric[string, Bar]\n\n\tOption option.Option[Generic[string]]\n\tOptionScrub option.Option[Generic[string]] SCRUB\n\n\tMapOne map[Generic[Bar]]NestedGeneric[string, Bar]\n\tMapTwo GenericMap[Bar, NestedGeneric[string, Bar]]\n\n\tJsonKey string `+\"`\"+`json:\"json_key\" encore:\"sensitive\"`+\"`\"+`\n\tHeader string `+\"`\"+`header:\"X-Header\" encore:\"sensitive\"`+\"`\"+`\n}\n\ntype Generic[T any] struct {\n\tFoo T\n\tFooScrub T SCRUB\n}\n\ntype NestedGeneric[A any, B any] struct {\n\tOne NestedGenericTwo[B, string]\n\tTwo B\n}\n\ntype NestedGenericTwo[A any, B any] struct {\n\tAlpha A\n\tBeta B\n}\n\ntype GenericMap[K comparable, V any] struct {\n\tFoo map[K]V\n}\n\ntype Bar struct {\n\tScrub string SCRUB\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n`)\n\n\tcmp := NewComputer(zerolog.New(os.Stdout))\n\tres := cmp.Compute(reqType, 0)\n\n\tf := func(name string) scrub.PathEntry {\n\t\treturn scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name)}\n\t}\n\tfCase := func(name string) scrub.PathEntry {\n\t\treturn scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name), CaseSensitive: true}\n\t}\n\tmapKey := scrub.PathEntry{Kind: scrub.MapKey}\n\tmapVal := scrub.PathEntry{Kind: scrub.MapValue}\n\n\tc.Assert(res.Payload, qt.DeepEquals, []scrub.Path{\n\t\t{f(\"Foo\")},\n\t\t{f(\"NestedScrub\")},\n\t\t{f(\"Nested\"), f(\"Inner\")},\n\t\t{f(\"List\")},\n\t\t{f(\"ListInner\"), f(\"Inner\")},\n\t\t{f(\"RecurseScrub\")},\n\t\t{f(\"Gen\"), f(\"FooScrub\")},\n\t\t{f(\"GenScrub\")},\n\t\t{f(\"GenInner\"), f(\"FooScrub\")},\n\t\t{f(\"GenInner\"), f(\"Foo\"), f(\"Scrub\")},\n\t\t{f(\"Multi\"), f(\"One\"), f(\"Alpha\"), f(\"Scrub\")},\n\t\t{f(\"Multi\"), f(\"Two\"), f(\"Scrub\")},\n\n\t\t{f(\"Option\"), f(\"FooScrub\")},\n\t\t{f(\"OptionScrub\")},\n\n\t\t{f(\"MapOne\"), mapKey, f(\"FooScrub\")},\n\t\t{f(\"MapOne\"), mapKey, f(\"Foo\"), f(\"Scrub\")},\n\t\t{f(\"MapOne\"), mapVal, f(\"One\"), f(\"Alpha\"), f(\"Scrub\")},\n\t\t{f(\"MapOne\"), mapVal, f(\"Two\"), f(\"Scrub\")},\n\t\t{f(\"MapTwo\"), f(\"Foo\"), mapKey, f(\"Scrub\")},\n\t\t{f(\"MapTwo\"), f(\"Foo\"), mapVal, f(\"One\"), f(\"Alpha\"), f(\"Scrub\")},\n\t\t{f(\"MapTwo\"), f(\"Foo\"), mapVal, f(\"Two\"), f(\"Scrub\")},\n\t\t{fCase(\"json_key\")},\n\t\t{f(\"Header\")},\n\t})\n\n\tc.Assert(res.Headers, qt.DeepEquals, []string{\"X-Header\"})\n}\n\nfunc TestScrubAuthHandler(t *testing.T) {\n\tc := qt.New(t)\n\treqType := testParse(c, `\n-- svc/svc.go --\npackage svc\nimport \"context\"\n\ntype Params struct {\n\tHeader string `+\"`\"+`header:\"Foo\"`+\"`\"+`\n\tQuery string `+\"`\"+`query:\"query\"`+\"`\"+`\n\tOther string\n}\n\n//encore:api public\nfunc Foo(ctx context.Context, p *Params) error { return nil }\n`)\n\n\tcmp := NewComputer(zerolog.New(os.Stdout))\n\tres := cmp.Compute(reqType, AuthHandler)\n\n\tf := func(name string) scrub.PathEntry {\n\t\treturn scrub.PathEntry{Kind: scrub.ObjectField, FieldName: strconv.Quote(name)}\n\t}\n\n\tc.Assert(res.Payload, qt.DeepEquals, []scrub.Path{\n\t\t{f(\"Header\")},\n\t\t{f(\"Query\")},\n\t\t{f(\"Other\")},\n\t})\n\n\tc.Assert(res.Headers, qt.DeepEquals, []string{\"Foo\"})\n}\n\n// testParse parses the given txtar code and returns the request schema.Type\n// for the first endpoint in the first service.\nfunc testParse(c *qt.C, code string) schema.Type {\n\tc.Helper()\n\tcode = strings.ReplaceAll(code, \"SCRUB\", \"`encore:\\\"sensitive\\\"`\")\n\tar := txtar.Parse([]byte(code))\n\n\ttc := testutil.NewContext(c, false, ar)\n\ttc.FailTestOnErrors()\n\n\t// Create a go.mod file if it doesn't already exist.\n\tmodPath := tc.MainModuleDir.Join(\"go.mod\").ToIO()\n\tif _, err := os.Stat(modPath); err != nil {\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\tc.Fatal(err)\n\t\t}\n\t\tmodContents := \"module example.com\\nrequire encore.dev v1.52.0\"\n\t\terr := os.WriteFile(modPath, []byte(modContents), 0644)\n\t\tc.Assert(err, qt.IsNil)\n\t}\n\n\ttc.GoModTidy()\n\ttc.GoModDownload()\n\n\tp := parser.NewParser(tc.Context)\n\tparserResult := p.Parse()\n\tappDesc := app.ValidateAndDescribe(tc.Context, parserResult)\n\n\tsvc := appDesc.Services[0]\n\tfw, ok := svc.Framework.Get()\n\tif !ok {\n\t\tc.Fatal(\"service has no framework\")\n\t}\n\tep := fw.Endpoints[0]\n\tif ep.Request == nil {\n\t\tc.Fatal(\"endpoint has no request type\")\n\t}\n\treturn ep.Request\n}\n"
  },
  {
    "path": "v2/codegen/apigen/userfacinggen/testdata/service_struct.txt",
    "content": "-- basic.go --\npackage basic\n\nimport (\"context\"; \"net/http\")\n\n//encore:service\ntype Service struct{}\n\n// Foo is an amazing API which does\n// x, y and z - it's really cool!\n//encore:api public\nfunc (s *Service) Foo(ctx context.Context) error { return nil }\n\n//encore:api public raw\nfunc (s *Service) Raw(w http.ResponseWriter, req *http.Request) {}\n\ntype Data struct{}\n\n//encore:api public\nfunc (s *Service) WithReq(context.Context, *Data) error { return nil }\n\n//encore:api public\nfunc (s *Service) WithResp(context.Context) (*Data, error) { return nil, nil }\n\n//encore:api public\nfunc (s *Service) WithReqResp(context.Context, *Data) (*Data, error) { return nil, nil }\n\n//encore:api public path=/pathing/:name/:age/*other\nfunc (s *Service) WithPathParams(ctx context.Context, name string, age int, other string) error { return nil }\n\n//encore:api public path=/fallback/!url\nfunc (s *Service) WithFallback(ctx context.Context, url string) error { return nil }\n\n// This API doesn't\n// exist on the service struct, but should still\n// appear\n//\n// on the service Interface\n//encore:api public\nfunc NoServiceStruct(context.Context) error { return nil }\n-- want:encore.gen.go --\n// Code generated by encore. DO NOT EDIT.\n\npackage basic\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n)\n\n// These functions are automatically generated and maintained by Encore\n// to simplify calling them from other services, as they were implemented as methods.\n// They are automatically updated by Encore whenever your API endpoints change.\n\n// Foo is an amazing API which does\n// x, y and z - it's really cool!\nfunc Foo(ctx context.Context) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.Foo(ctx)\n}\n\nfunc Raw(ctx context.Context, req *http.Request) (*http.Response, error) {\n\treturn nil, errors.New(\"encore: calling raw endpoints is not yet supported\")\n}\n\nfunc WithReq(ctx context.Context, p *Data) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.WithReq(ctx, p)\n}\n\nfunc WithResp(ctx context.Context) (*Data, error) {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn (*Data)(nil), err\n\t}\n\treturn svc.WithResp(ctx)\n}\n\nfunc WithReqResp(ctx context.Context, p *Data) (*Data, error) {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn (*Data)(nil), err\n\t}\n\treturn svc.WithReqResp(ctx, p)\n}\n\nfunc WithPathParams(ctx context.Context, name string, age int, other string) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.WithPathParams(ctx, name, age, other)\n}\n\nfunc WithFallback(ctx context.Context, url string) error {\n\tsvc, err := EncoreInternal_svcstruct_Service.Get()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.WithFallback(ctx, url)\n}\n\n// Interface defines the service's API surface area, primarily for mocking purposes.\n//\n// Raw endpoints are currently excluded from this interface, as Encore does not yet\n// support service-to-service API calls to raw endpoints.\ntype Interface interface {\n\t// Foo is an amazing API which does\n\t// x, y and z - it's really cool!\n\tFoo(ctx context.Context) error\n\n\tWithReq(ctx context.Context, p *Data) error\n\n\tWithResp(ctx context.Context) (*Data, error)\n\n\tWithReqResp(ctx context.Context, p *Data) (*Data, error)\n\n\tWithPathParams(ctx context.Context, name string, age int, other string) error\n\n\tWithFallback(ctx context.Context, url string) error\n\n\t// This API doesn't\n\t// exist on the service struct, but should still\n\t// appear\n\t//\n\t// on the service Interface\n\tNoServiceStruct(ctx context.Context) error\n}\n-- want:encore_internal__svcstruct.go --\npackage basic\n\nimport __service \"encore.dev/appruntime/apisdk/service\"\n\nfunc init() {\n\t__service.Register(EncoreInternal_svcstruct_Service)\n}\n\nvar EncoreInternal_svcstruct_Service = &__service.Decl[Service]{\n\tName:        \"Service\",\n\tService:     \"basic\",\n\tSetup:       nil,\n\tSetupDefLoc: uint32(0x0),\n}\n"
  },
  {
    "path": "v2/codegen/apigen/userfacinggen/userfacinggen.go",
    "content": "package userfacinggen\n\nimport (\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/namealloc\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/app/apiframework\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/parser/apis/api\"\n)\n\n// Gen generates the encore.gen.go file containing user-facing\n// generated code. If nothing needs to be generated it returns nil.\nfunc Gen(gen *codegen.Generator, svc *app.Service, withSvcStructImpl option.Option[*codegen.VarDecl]) option.Option[*codegen.File] {\n\tif fw, ok := svc.Framework.Get(); ok {\n\t\treturn genUserFacing(gen, fw, withSvcStructImpl)\n\t}\n\treturn option.None[*codegen.File]()\n}\n\nfunc genUserFacing(gen *codegen.Generator, svc *apiframework.ServiceDesc, withImpl option.Option[*codegen.VarDecl]) option.Option[*codegen.File] {\n\tf := gen.InjectFile(svc.RootPkg.ImportPath, svc.RootPkg.Name, svc.RootPkg.FSPath,\n\t\t\"encore.gen.go\", \"encoregen\")\n\n\tf.Jen.HeaderComment(\"Code generated by encore. DO NOT EDIT.\")\n\n\tf.Jen.Comment(\"These functions are automatically generated and maintained by Encore\")\n\tf.Jen.Comment(\"to simplify calling them from other services, as they were implemented as methods.\")\n\tf.Jen.Comment(\"They are automatically updated by Encore whenever your API endpoints change.\")\n\tf.Jen.Line()\n\n\t// count is the number of declarations we've generated in this file\n\tcount := 0\n\n\t// Generate endpoint functions for endpoints defined on the service struct\n\tfor _, ep := range svc.Endpoints {\n\t\tif ep.Recv.Empty() {\n\t\t\tcontinue\n\t\t}\n\n\t\tgenEndpoint(gen.Util, f, ep, withImpl)\n\t\tf.Jen.Line()\n\t\tcount++\n\t}\n\n\t// Generate the service interface (this is useful for mocking)\n\tif len(svc.Endpoints) > 0 {\n\t\tf.Jen.Comment(\"Interface defines the service's API surface area, primarily for mocking purposes.\")\n\t\tf.Jen.Comment(\"\")\n\t\tf.Jen.Comment(\"Raw endpoints are currently excluded from this interface, as Encore does not yet\")\n\t\tf.Jen.Comment(\"support service-to-service API calls to raw endpoints.\")\n\t\tf.Jen.Type().Id(\"Interface\").InterfaceFunc(func(g *Group) {\n\t\t\tfor _, ep := range svc.Endpoints {\n\t\t\t\tif !ep.Raw {\n\t\t\t\t\tgetEndpointPrototype(gen.Util, g, ep, false, option.None[*codegen.VarDecl]())\n\t\t\t\t\tcount++\n\t\t\t\t\tg.Line()\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\tf.Jen.Line()\n\t}\n\n\tif count == 0 {\n\t\t// If we've generated no declarations, then this file can be deleted\n\t\treturn option.None[*codegen.File]()\n\t} else {\n\t\treturn option.Some(f)\n\t}\n}\n\nfunc genEndpoint(gu *genutil.Helper, f *codegen.File, ep *api.Endpoint, withImpl option.Option[*codegen.VarDecl]) {\n\tstmt, pathParamNames, alloc, ctxName, paramName := getEndpointPrototype(gu, f.Jen.Group, ep, true, withImpl)\n\tstmt.BlockFunc(func(g *Group) {\n\t\tif svcStruct, ok := withImpl.Get(); ok {\n\t\t\tif ep.Raw {\n\t\t\t\tg.Return(Nil(), Qual(\"errors\", \"New\").Call(Lit(\"encore: calling raw endpoints is not yet supported\")))\n\t\t\t} else {\n\t\t\t\tsvcName := alloc(\"svc\", false)\n\t\t\t\tg.List(Id(svcName), Err()).Op(\":=\").Id(svcStruct.Name()).Dot(\"Get\").Call()\n\t\t\t\tg.If(Err().Op(\"!=\").Nil()).Block(ReturnFunc(func(g *Group) {\n\t\t\t\t\tif ep.Raw {\n\t\t\t\t\t\tg.Nil()\n\t\t\t\t\t} else if ep.Response != nil {\n\t\t\t\t\t\tg.Add(gu.Zero(ep.Response))\n\t\t\t\t\t}\n\t\t\t\t\tg.Err()\n\t\t\t\t}))\n\n\t\t\t\tg.Return(Id(\"svc\").Dot(ep.Name).CallFunc(func(g *Group) {\n\t\t\t\t\tg.Id(ctxName)\n\t\t\t\t\tfor _, name := range pathParamNames {\n\t\t\t\t\t\tg.Id(name)\n\t\t\t\t\t}\n\t\t\t\t\tif paramName != \"\" {\n\t\t\t\t\t\tg.Id(paramName)\n\t\t\t\t\t}\n\t\t\t\t}))\n\t\t\t}\n\t\t} else {\n\t\t\tg.Comment(\"The implementation is elided here, and generated at compile-time by Encore.\")\n\t\t\tif ep.Raw {\n\t\t\t\tg.Return(Nil(), Nil())\n\t\t\t} else if ep.Response != nil {\n\t\t\t\tg.Return(gu.Zero(ep.Response), Nil())\n\t\t\t} else {\n\t\t\t\t// Just an error return\n\t\t\t\tg.Return(Nil())\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc getEndpointPrototype(gu *genutil.Helper, grp *Group, ep *api.Endpoint, withFuncKeyWord bool, withImpl option.Option[*codegen.VarDecl]) (*Statement, []string, func(input string, pathParam bool) string, string, string) {\n\t// Add the doc comment\n\tif ep.Doc != \"\" {\n\t\tfor _, line := range strings.Split(strings.TrimSpace(ep.Doc), \"\\n\") {\n\t\t\tgrp.Comment(line)\n\t\t}\n\t}\n\n\tvar pathParamNames []string\n\n\tvar names namealloc.Allocator\n\talloc := func(input string, pathParam bool) string {\n\t\tname := names.Get(input)\n\t\tif pathParam {\n\t\t\tpathParamNames = append(pathParamNames, name)\n\t\t}\n\t\treturn name\n\t}\n\n\tvar (\n\t\tctxName    = alloc(\"ctx\", false)\n\t\trawReqName string\n\t\tparamName  string\n\t)\n\n\tvar stmt *Statement\n\tif withFuncKeyWord {\n\t\tstmt = grp.Func().Id(ep.Name)\n\t} else {\n\t\tstmt = grp.Id(ep.Name)\n\t}\n\n\tstmt = stmt.ParamsFunc(func(g *Group) {\n\t\tg.Id(ctxName).Qual(\"context\", \"Context\")\n\t\tfor _, p := range ep.Path.Params() {\n\t\t\ttyp := gu.Builtin(p.Pos(), p.ValueType)\n\t\t\tg.Id(alloc(p.Value, true)).Add(typ)\n\t\t}\n\t\tif ep.Raw {\n\t\t\trawReqName = alloc(\"req\", false)\n\t\t\tg.Id(rawReqName).Op(\"*\").Qual(\"net/http\", \"Request\")\n\t\t} else if req := ep.Request; req != nil {\n\t\t\tparamName = alloc(\"p\", false)\n\t\t\tg.Id(paramName).Add(gu.Type(req))\n\t\t}\n\t}).Do(func(s *Statement) {\n\t\tif withImpl.Present() {\n\t\t\tif ep.Raw {\n\t\t\t\ts.Params(Op(\"*\").Qual(\"net/http\", \"Response\"), Error())\n\t\t\t} else if resp := ep.Response; resp != nil {\n\t\t\t\ts.Params(gu.Type(resp), Error())\n\t\t\t} else {\n\t\t\t\ts.Params(Error())\n\t\t\t}\n\t\t} else {\n\t\t\tif ep.Raw {\n\t\t\t\ts.Params(Op(\"*\").Qual(\"net/http\", \"Response\"), Error())\n\t\t\t} else if resp := ep.Response; resp != nil {\n\t\t\t\ts.Params(gu.Type(resp), Error())\n\t\t\t} else {\n\t\t\t\ts.Error()\n\t\t\t}\n\t\t}\n\t})\n\n\treturn stmt, pathParamNames, alloc, ctxName, paramName\n}\n"
  },
  {
    "path": "v2/codegen/apigen/userfacinggen/userfacinggen_test.go",
    "content": "package userfacinggen\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen/servicestructgen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tsvc := desc.Services[0]\n\t\tvar svcStruct option.Option[*codegen.VarDecl]\n\t\tif fw, ok := svc.Framework.Get(); ok {\n\t\t\tif ss, ok := fw.ServiceStruct.Get(); ok {\n\t\t\t\tdecl := servicestructgen.Gen(gen, svc, ss)\n\t\t\t\tsvcStruct = option.Some(decl)\n\t\t\t}\n\t\t}\n\t\tGen(gen, svc, svcStruct)\n\t}\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/config.go",
    "content": "package codegen\n\nimport (\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\n// TestConfig describes common configuration for code generation\n// when running tests.\ntype TestConfig struct {\n\t// Packages are the packages to generate test code for.\n\tPackages []*pkginfo.Package\n\n\t// EnvsToEmbed are the environment variables to embed inside\n\t// the test binaries themselves. This is useful when\n\t// building tests with \"go test -c\", where the binary is\n\t// built first and executed later (such as by GoLand).\n\tEnvsToEmbed map[string]string\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/definition_generator.go",
    "content": "package cuegen\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"cuelang.org/go/cue/ast\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n)\n\n// definitionGenerator is used to count the number of types a specific named type\n// is used (named types include type arguments, such that Option[string] != Option[int]).\n//\n// It also counts the number of times a unique named type uses a decl.\n//\n// This allows us to:\n// - determine if we should inline a named type into a config field if it's only used once\n// - generate a unique name for a generic decl if it's used with multiple type arguments\ntype definitionGenerator struct {\n\tseenTypes      map[schemautil.TypeHash]schema.NamedType\n\tdefinitionName map[schemautil.TypeHash]string // type -> name\n\tcounts         map[schemautil.TypeHash]int    // type -> usage count\n\tnameCount      map[string]int                 // name -> usage count for base name\n}\n\nfunc newDefinitionGenerator() *definitionGenerator {\n\treturn &definitionGenerator{\n\t\tseenTypes:      make(map[schemautil.TypeHash]schema.NamedType),\n\t\tdefinitionName: make(map[schemautil.TypeHash]string),\n\t\tnameCount:      make(map[string]int),\n\t\tcounts:         make(map[schemautil.TypeHash]int),\n\t}\n}\n\n// ID returns the id for the named type. If two named types are passed in\n// which are identical, it will return the same id.\n//\n// This function then also creates a unique definition name for the named type\n// which we could use within the generated CUE file\nfunc (n *definitionGenerator) hash(named schema.NamedType) schemautil.TypeHash {\n\thash := schemautil.Hash(named)\n\tif _, ok := n.seenTypes[hash]; ok {\n\t\treturn hash\n\t}\n\tn.seenTypes[hash] = named\n\n\t// Create a unique name for this definition\n\tdefaultName := n.typeToDefinitionName(named)\n\tusageCount, found := n.nameCount[defaultName]\n\tif !found {\n\t\tn.definitionName[hash] = defaultName\n\t} else {\n\t\tn.definitionName[hash] = fmt.Sprintf(\"%s_%d\", defaultName, usageCount)\n\t}\n\tn.nameCount[defaultName] = usageCount + 1\n\n\treturn hash\n}\n\nfunc (n *definitionGenerator) CueIdent(named schema.NamedType) *ast.Ident {\n\treturn ast.NewIdent(\"#\" + n.definitionName[n.hash(named)])\n}\n\nfunc (n *definitionGenerator) Inc(named schema.NamedType) {\n\tid := n.hash(named)\n\tn.counts[id]++\n}\n\nfunc (n *definitionGenerator) Count(named schema.NamedType) int {\n\tid := n.hash(named)\n\treturn n.counts[id]\n}\n\nfunc (n *definitionGenerator) NamesWithCountsOver(x int) []schema.NamedType {\n\trtn := make([]schema.NamedType, 0, len(n.seenTypes))\n\n\t// Get the keys in sorted order for deterministic output\n\tkeys := fns.MapKeys(n.seenTypes)\n\tslices.SortFunc(keys, func(a, b schemautil.TypeHash) int {\n\t\treturn bytes.Compare(a[:], b[:])\n\t})\n\n\tfor _, hash := range keys {\n\t\tif n.counts[hash] > x {\n\t\t\trtn = append(rtn, n.seenTypes[hash])\n\t\t}\n\t}\n\n\treturn rtn\n}\n\n// typeToDefinitionName converts a schema type into a possible name we could\n// use for the CUE definition.\n//\n// It will not return anything for inline structs.\n// It will include type arguments in the name so the same decl created with\n// different types will result in different names.\nfunc (n *definitionGenerator) typeToDefinitionName(typ schema.Type) string {\n\tswitch typ := typ.(type) {\n\tcase schema.NamedType:\n\t\tvar name strings.Builder\n\t\tname.WriteString(typ.DeclInfo.Name)\n\t\tfor _, typeArg := range typ.TypeArgs {\n\t\t\tname.WriteString(\"_\")\n\t\t\tname.WriteString(n.typeToDefinitionName(typeArg))\n\t\t}\n\t\treturn name.String()\n\tcase schema.ListType:\n\t\treturn \"List_\" + n.typeToDefinitionName(typ.Elem)\n\tcase schema.MapType:\n\t\treturn \"Map_\" + n.typeToDefinitionName(typ.Key) + \"_\" + n.typeToDefinitionName(typ.Value)\n\tcase schema.PointerType:\n\t\treturn n.typeToDefinitionName(typ.Elem)\n\tcase schema.BuiltinType:\n\t\tswitch typ.Kind {\n\t\tcase schema.Any:\n\t\t\treturn \"any\"\n\t\tcase schema.Bool:\n\t\t\treturn \"bool\"\n\t\tcase schema.Int8:\n\t\t\treturn \"int8\"\n\t\tcase schema.Int16:\n\t\t\treturn \"int16\"\n\t\tcase schema.Int32:\n\t\t\treturn \"int32\"\n\t\tcase schema.Int64:\n\t\t\treturn \"int64\"\n\t\tcase schema.Uint8:\n\t\t\treturn \"uint8\"\n\t\tcase schema.Uint16:\n\t\t\treturn \"uint16\"\n\t\tcase schema.Uint32:\n\t\t\treturn \"uint32\"\n\t\tcase schema.Uint64:\n\t\t\treturn \"uint64\"\n\t\tcase schema.Float32:\n\t\t\treturn \"float32\"\n\t\tcase schema.Float64:\n\t\t\treturn \"float64\"\n\t\tcase schema.String:\n\t\t\treturn \"string\"\n\t\tcase schema.Bytes:\n\t\t\treturn \"bytes\"\n\t\tcase schema.Time:\n\t\t\treturn \"string\"\n\t\tcase schema.UUID:\n\t\t\treturn \"string\"\n\t\tcase schema.JSON:\n\t\t\treturn \"string\"\n\t\tcase schema.UserID:\n\t\t\treturn \"string\"\n\t\tcase schema.Int:\n\t\t\treturn \"int\"\n\t\tcase schema.Uint:\n\t\t\treturn \"uint\"\n\t\tdefault:\n\t\t\treturn \"\"\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/errors.go",
    "content": "package cuegen\n\nimport \"encr.dev/pkg/errors\"\n\nvar (\n\terrRange = errors.Range(\n\t\t\"cuegen\",\n\t\t\"\",\n\t)\n\n\terrNotNamedStruct = errRange.New(\n\t\t\"Invalid type for config.Load\",\n\t\t\"The type argument passed to config.Load must be a named struct type.\",\n\t)\n\n\terrInvalidFieldLabel = errRange.New(\n\t\t\"Invalid field label\",\n\t\t\"The field could not be rendered as a valid CUE label.\",\n\t)\n\n\terrInvalidCUEExpr = errRange.New(\n\t\t\"Invalid CUE struct tag expression\",\n\t\t\"The struct tag expression is not a valid CUE expression.\",\n\t)\n)\n"
  },
  {
    "path": "v2/codegen/cuegen/generator.go",
    "content": "package cuegen\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"slices\"\n\n\t\"cuelang.org/go/cue/ast\"\n\t\"cuelang.org/go/cue/ast/astutil\"\n\t\"cuelang.org/go/cue/format\"\n\t\"cuelang.org/go/cue/token\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/parser/infra/config\"\n)\n\ntype Generator struct {\n\tdesc *app.Desc\n}\n\nfunc NewGenerator(desc *app.Desc) *Generator {\n\treturn &Generator{\n\t\tdesc: desc,\n\t}\n}\n\n// UserFacing generates a CUE file for the given service.\n//\n// It includes constraints and requirements based on the types passed to `encore.dev/config.Load[T]()`\n// within the service.\nfunc (g *Generator) UserFacing(svc *app.Service) ([]byte, error) {\n\tvar loads []*config.Load\n\tfor res := range svc.ResourceBinds {\n\t\tif cfg, ok := res.(*config.Load); ok {\n\t\t\tloads = append(loads, cfg)\n\t\t}\n\t}\n\tif len(loads) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// Sort the loads so we iterate over them in a deterministic order.\n\tslices.SortFunc(loads, func(a, b *config.Load) int {\n\t\t// Sort by package path, then file name, then position.\n\t\t// We can't sort by position first because we're not guaranteed\n\t\t// files are added to the *token.FileSet in the same order since\n\t\t// we're parsing files concurrently.\n\t\tif n := cmp.Compare(a.File.Pkg.ImportPath, b.File.Pkg.ImportPath); n != 0 {\n\t\t\treturn n\n\t\t} else if n := cmp.Compare(a.File.Name, b.File.Name); n != 0 {\n\t\t\treturn n\n\t\t} else {\n\t\t\treturn cmp.Compare(a.Pos(), b.Pos())\n\t\t}\n\t})\n\n\t// Create a base file\n\tservice := &serviceFile{\n\t\tg:             g,\n\t\tsvc:           svc,\n\t\tfile:          &ast.File{},\n\t\tneededImports: make(map[string]string),\n\t\tfieldLookup:   make(map[string]*ast.Field),\n\t\ttypeUsage:     newDefinitionGenerator(),\n\t}\n\n\t// Count the number of times each named type is used\n\t// this allows us to determine if we inline the named type\n\t// or create and use a Definition\n\tfor _, configLoad := range loads {\n\t\tservice.countNamedUsagesAndCollectImports(configLoad.Type)\n\t}\n\n\t// Add all the top level fields required by this service\n\tfor _, configLoad := range loads {\n\t\tservice.registerTopLevelField(configLoad.Type)\n\t}\n\n\t// For the first top level field in a service, if it's not go a comment above it, then we want to put it's label position\n\t// as a new section. This forces a blank line between the type declarations and the first field.\n\tif len(service.topLevelFields) > 0 {\n\t\tif field, ok := service.topLevelFields[0].(*ast.Field); ok {\n\t\t\tif !hasCommentInPosition(field, 0) {\n\t\t\t\tif ident, ok := field.Label.(*ast.Ident); ok {\n\t\t\t\t\tident.NamePos = token.NewSection.Pos()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now generate the CUE\n\tservice.generateCue()\n\n\t// Cleanup the generated AST\n\tif err := astutil.Sanitize(service.file); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Format the AST into a set of bytes we can write\n\tb, err := format.Node(\n\t\tservice.file,\n\t\tformat.Simplify(),\n\t\tformat.UseSpaces(4),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn bytes.TrimSpace(b), nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/generator_test.go",
    "content": "package cuegen\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/golden\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n)\n\nfunc TestMain(m *testing.M) {\n\tgolden.TestMain(m)\n}\n\nfunc TestCodeGen_TestMain(t *testing.T) {\n\tc := qt.New(t)\n\ttests, err := filepath.Glob(\"./testdata/*.txt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tc.Assert(err, qt.IsNil)\n\n\tfor _, test := range tests {\n\t\tpath := test\n\t\tname := strings.TrimSuffix(filepath.Base(test), \".txt\")\n\t\tc.Run(name, func(c *qt.C) {\n\t\t\tarchiveData, err := os.ReadFile(path)\n\t\t\tc.Assert(err, qt.IsNil)\n\t\t\ta := txtar.Parse(archiveData)\n\n\t\t\ta.Files = append(a.Files, txtar.File{\n\t\t\t\tName: \"go.mod\",\n\t\t\t\tData: []byte(\"module encore.app\\nrequire encore.dev v1.52.0\\n\"),\n\t\t\t})\n\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\ttc.FailTestOnErrors()\n\t\t\tdefer tc.FailTestOnBailout()\n\n\t\t\tresult := parser.NewParser(tc.Context).Parse()\n\t\t\tdesc := app.ValidateAndDescribe(tc.Context, result)\n\t\t\tgen := NewGenerator(desc)\n\n\t\t\tfor _, svc := range desc.Services {\n\t\t\t\tf, err := gen.UserFacing(svc)\n\t\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\t\tgolden.TestAgainst(c.TB, fmt.Sprintf(\"%s_%s.cue\", name, svc.Name), string(f))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/service.go",
    "content": "package cuegen\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"slices\"\n\n\t\"cuelang.org/go/cue/ast\"\n\t\"cuelang.org/go/cue/parser\"\n\t\"cuelang.org/go/cue/token\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n)\n\n// service represents the single generated file we will create for a service\n// from all of it's config.Load calls\ntype serviceFile struct {\n\terrs           *perr.List\n\tg              *Generator\n\tsvc            *app.Service\n\tfile           *ast.File\n\tneededImports  map[string]string // map of package path to name\n\ttopLevelFields []any\n\tfieldLookup    map[string]*ast.Field\n\n\ttypeUsage *definitionGenerator\n}\n\n// countNamedUsagesAndCollectImports counts the number of times a named type is used in the service\nfunc (s *serviceFile) countNamedUsagesAndCollectImports(typ schema.Type) {\n\tvar processType func(typ schema.Type)\n\tprocessType = func(typ schema.Type) {\n\t\tswitch typ := typ.(type) {\n\t\tcase schema.NamedType:\n\t\t\tif unwrapped, wasConfig := unwrapConfig(s.errs, typ); wasConfig {\n\t\t\t\tprocessType(unwrapped)\n\t\t\t} else {\n\t\t\t\ts.typeUsage.Inc(typ)\n\t\t\t}\n\n\t\tcase schema.BuiltinType:\n\t\t\tif typ.Kind == schema.Time {\n\t\t\t\ts.neededImports[\"time\"] = \"time\"\n\t\t\t}\n\t\t}\n\t}\n\n\tschemautil.Walk(typ, func(typ schema.Type) bool {\n\t\tprocessType(typ)\n\t\treturn true\n\t})\n}\n\nfunc (s *serviceFile) registerTopLevelField(typ schema.Type) {\n\tst, ok := schemautil.ResolveNamedStruct(typ, false)\n\tif !ok {\n\t\ts.errs.Add(errNotNamedStruct.AtGoNode(typ.ASTExpr()))\n\t\treturn\n\t}\n\tconcrete := schemautil.ConcretizeWithTypeArgs(s.errs, st.Decl.Type, st.TypeArgs)\n\n\tfields := s.structToFields(concrete.(schema.StructType))\n\n\tfor _, field := range fields {\n\t\tf := field.CUE\n\t\tname, _, err := ast.LabelName(f.Label)\n\t\tif err != nil {\n\t\t\ts.errs.Add(errInvalidFieldLabel.AtGoNode(field.Go.AST).Wrapping(err))\n\t\t\tcontinue\n\t\t}\n\n\t\t// If we already know about the field, merge the definitions\n\t\t// (this could be the case from multiple calls to `config.Load` inside\n\t\t// the same service to either the same or different struct types\n\t\t// with both have the same field name\n\t\tif existing, found := s.fieldLookup[name]; found {\n\t\t\tif len(f.Comments()) > 0 {\n\t\t\t\tif len(existing.Comments()) == 0 {\n\t\t\t\t\texisting.SetComments(f.Comments())\n\t\t\t\t} else {\n\t\t\t\t\texistingCommentGrp := existing.Comments()[0]\n\t\t\t\t\tfor _, comment := range f.Comments() {\n\t\t\t\t\t\tif !commentAlreadyPresent(existing, comment) {\n\t\t\t\t\t\t\texistingCommentGrp.List = append(existingCommentGrp.List, comment.List...)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// If after this check the existing comment group is now multiline, we need to move the comment groups\n\t\t\t\t\t// position to be before the field label.\n\t\t\t\t\tif len(existingCommentGrp.List) > 0 {\n\t\t\t\t\t\texistingCommentGrp.Position = 0\n\t\t\t\t\t\texistingCommentGrp.List[0].Slash = token.NewSection.Pos()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Merge the values if they are different\n\t\t\tif !reflect.DeepEqual(existing.Value, f.Value) {\n\t\t\t\texisting.Value = ast.NewBinExpr(token.AND, existing.Value, f.Value)\n\t\t\t}\n\t\t} else {\n\t\t\t// otherwise add this field\n\t\t\ts.fieldLookup[name] = f\n\t\t\ts.topLevelFields = append(s.topLevelFields, f)\n\t\t}\n\t}\n}\n\nfunc (s *serviceFile) generateCue() {\n\t// If there are no top level fields, we've got nothing to do here\n\tif len(s.topLevelFields) == 0 {\n\t\treturn\n\t}\n\n\t// Add the package name and description comment\n\tpkg := &ast.Package{Name: ast.NewIdent(s.svc.Name)}\n\ts.file.Decls = append(s.file.Decls, pkg)\n\ts.file.AddComment(&ast.CommentGroup{\n\t\tList: []*ast.Comment{\n\t\t\t{Text: \"// Code generated by encore. DO NOT EDIT.\"},\n\t\t\t{Text: \"//\"},\n\t\t\t{Text: \"// The contents of this file are generated from the structs used in\"},\n\t\t\t{Text: \"// conjunction with Encore's `config.Load[T]()` function. This file\"},\n\t\t\t{Text: \"// automatically be regenerated if the data types within the struct\"},\n\t\t\t{Text: \"// are changed.\"},\n\t\t\t{Text: \"//\"},\n\t\t\t{Text: \"// For more information about this file, see:\"},\n\t\t\t{Text: \"// https://encore.dev/docs/develop/config\"},\n\t\t},\n\t})\n\n\ts.generateEnvironmentalDefinitions()\n\n\t// Add any missing imports\n\tif len(s.neededImports) > 0 {\n\t\t// Get an ordered list of the imports\n\t\timports := make([]string, 0, len(s.neededImports))\n\t\tfor pkg := range s.neededImports {\n\t\t\timports = append(imports, pkg)\n\t\t}\n\t\tslices.Sort(imports)\n\n\t\t// Create all the import specs\n\t\tfor _, importPath := range imports {\n\t\t\tvar ident *ast.Ident = nil\n\t\t\tif s.neededImports[importPath] != importPath {\n\t\t\t\tident = ast.NewIdent(s.neededImports[importPath])\n\t\t\t}\n\n\t\t\tspec := ast.NewImport(ident, importPath)\n\t\t\ts.file.Imports = append(s.file.Imports, spec)\n\t\t}\n\n\t\t// Now add the import statement\n\t\ts.file.Decls = append(s.file.Decls, &ast.ImportDecl{\n\t\t\tSpecs: s.file.Imports,\n\t\t})\n\t}\n\n\t// Now write the top level fields required in the config\n\tappConfigStruct := &ast.Field{\n\t\tLabel: ast.NewIdent(\"#Config\"),\n\t\tValue: ast.NewStruct(s.topLevelFields...),\n\t}\n\tappConfigStruct.AddComment(&ast.CommentGroup{\n\t\tList: []*ast.Comment{\n\t\t\t{Text: \"// #Config is the top level configuration for the application and is generated\"},\n\t\t\t{Text: \"// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\"},\n\t\t\t{Text: \"// of this struct which is closed, such that the CUE tooling can any typos of field names.\"},\n\t\t\t{Text: \"// this definition is then immediately inlined, so any fields within it are expected\"},\n\t\t\t{Text: \"// as fields at the package level.\"},\n\t\t},\n\t})\n\ts.file.Decls = append(s.file.Decls, appConfigStruct, ast.NewIdent(\"#Config\"))\n\n\t// Write any declarations we've used multiple times to the file\n\tfor _, named := range s.typeUsage.NamesWithCountsOver(1) {\n\t\tconcrete := schemautil.ConcretizeWithTypeArgs(s.errs, named.Decl().Type, named.TypeArgs)\n\t\tdefIdent := s.typeUsage.CueIdent(named)\n\t\tfieldType := s.toCueType(concrete)\n\n\t\tfield := &ast.Field{\n\t\t\tLabel: defIdent,\n\t\t\tValue: fieldType,\n\t\t}\n\t\tif named.DeclInfo.Doc != \"\" {\n\t\t\taddCommentToField(field, named.DeclInfo.Doc)\n\t\t} else {\n\t\t\t// If there isn't a doc, we want to force a new section\n\t\t\t// above the name (empty line above).\n\t\t\t// The doc block will add this for us\n\t\t\tdefIdent.NamePos = token.NewSection.Pos()\n\t\t}\n\t\ts.file.Decls = append(s.file.Decls, field)\n\t}\n}\n\ntype structField struct {\n\tGo  schema.StructField\n\tCUE *ast.Field\n}\n\n// structToFields converts a struct to a list of fields which can then be\n// either included in a definition, a line struct or the file level declarations\nfunc (s *serviceFile) structToFields(st schema.StructType) []structField {\n\tvar fields []structField\n\n\tfor _, f := range st.Fields {\n\t\tif f.IsAnonymous() {\n\t\t\t// TODO error?\n\t\t\tcontinue\n\t\t}\n\t\tisOptional := false\n\n\t\t// Convert the type to CUE\n\t\tfield := &ast.Field{\n\t\t\tLabel: ast.NewIdent(f.Name.MustGet()),\n\t\t\tValue: s.toCueType(f.Type),\n\t\t}\n\n\t\tfor _, tag := range f.Tag.Tags() {\n\t\t\tif tag.Key == \"json\" {\n\t\t\t\tif tag.Name != \"\" {\n\t\t\t\t\tfield.Label = ast.NewIdent(tag.Name)\n\t\t\t\t}\n\t\t\t\tfor _, option := range tag.Options {\n\t\t\t\t\tif option == \"omitempty\" {\n\t\t\t\t\t\tisOptional = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tag.Key == \"cue\" {\n\t\t\t\tif tag.Name != \"\" {\n\t\t\t\t\texpr, err := parser.ParseExpr(\"encore struct\", tag.Name)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\ts.errs.Add(errInvalidCUEExpr.AtGoNode(f.AST).Wrapping(err))\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfield.Value = ast.NewBinExpr(token.AND, field.Value, expr)\n\t\t\t\t}\n\t\t\t\tfor _, option := range tag.Options {\n\t\t\t\t\tif option == \"opt\" {\n\t\t\t\t\t\tisOptional = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Mark the field as optional if it is\n\t\tif isOptional {\n\t\t\tfield.Optional = token.Blank.Pos()\n\t\t}\n\n\t\t// Add the documentation to the field\n\t\tif f.Doc != \"\" {\n\t\t\taddCommentToField(field, f.Doc)\n\t\t}\n\n\t\tfields = append(fields, structField{Go: f, CUE: field})\n\t}\n\n\treturn fields\n}\n\n// Convert a schema type into a cue type\nfunc (s *serviceFile) toCueType(unknownType schema.Type) ast.Expr {\n\tswitch typ := unknownType.(type) {\n\tcase schema.NamedType:\n\t\tif underlying, wasConfig := unwrapConfig(s.errs, typ); wasConfig {\n\t\t\treturn s.toCueType(underlying)\n\t\t}\n\n\t\tusageCount := s.typeUsage.Count(typ)\n\t\tif usageCount <= 1 {\n\t\t\t// inline the type if it's only used once\n\t\t\tconcrete := schemautil.ConcretizeWithTypeArgs(s.errs, typ.Decl().Type, typ.TypeArgs)\n\t\t\treturn s.toCueType(concrete)\n\t\t} else {\n\t\t\treturn s.typeUsage.CueIdent(typ)\n\t\t}\n\tcase schema.StructType:\n\t\tfields := s.structToFields(typ)\n\t\tfieldsInterface := make([]any, len(fields))\n\t\tfor i, field := range fields {\n\t\t\tfieldsInterface[i] = field.CUE\n\t\t}\n\n\t\treturn ast.NewStruct(fieldsInterface...)\n\tcase schema.MapType:\n\t\tkeyType := s.toCueType(typ.Key)\n\t\tvalueType := s.toCueType(typ.Value)\n\t\treturn ast.NewStruct(ast.NewList(keyType), valueType)\n\n\tcase schema.ListType:\n\t\tlistType := s.toCueType(typ.Elem)\n\t\treturn ast.NewList(&ast.Ellipsis{Type: listType})\n\n\tcase schema.BuiltinType:\n\t\treturn s.builtinToCue(typ.Kind)\n\n\tcase schema.PointerType:\n\t\t// Pointers are not supported in CUE, so we just convert the underlying type.\n\t\treturn s.toCueType(typ.Elem)\n\n\tdefault:\n\t\t// TODO error instead\n\t\tpanic(fmt.Sprintf(\"unexpected type: %T\", typ))\n\t}\n}\n\nfunc (s *serviceFile) builtinToCue(kind schema.BuiltinKind) ast.Expr {\n\tswitch kind {\n\tcase schema.Any:\n\t\treturn ast.NewIdent(\"_\") // top\n\tcase schema.Bool:\n\t\treturn ast.NewIdent(\"bool\")\n\tcase schema.Int8:\n\t\treturn ast.NewIdent(\"int8\")\n\tcase schema.Int16:\n\t\treturn ast.NewIdent(\"int16\")\n\tcase schema.Int32:\n\t\treturn ast.NewIdent(\"int32\")\n\tcase schema.Int64:\n\t\treturn ast.NewIdent(\"int64\")\n\tcase schema.Uint8:\n\t\treturn ast.NewIdent(\"uint8\")\n\tcase schema.Uint16:\n\t\treturn ast.NewIdent(\"uint16\")\n\tcase schema.Uint32:\n\t\treturn ast.NewIdent(\"uint32\")\n\tcase schema.Uint64:\n\t\treturn ast.NewIdent(\"uint64\")\n\tcase schema.Float32:\n\t\treturn ast.NewIdent(\"float32\")\n\tcase schema.Float64:\n\t\treturn ast.NewIdent(\"float64\")\n\tcase schema.String:\n\t\treturn ast.NewIdent(\"string\")\n\tcase schema.Bytes:\n\t\treturn ast.NewIdent(\"bytes\")\n\tcase schema.Time:\n\t\treturn ast.NewSel(ast.NewIdent(\"time\"), \"Time\")\n\tcase schema.UUID:\n\t\treturn ast.NewIdent(\"string\")\n\tcase schema.JSON:\n\t\treturn ast.NewIdent(\"string\")\n\tcase schema.UserID:\n\t\treturn ast.NewIdent(\"string\")\n\tcase schema.Int:\n\t\treturn ast.NewIdent(\"int\")\n\tcase schema.Uint:\n\t\treturn ast.NewIdent(\"uint\")\n\tdefault:\n\t\t// TODO error instead\n\t\tpanic(fmt.Sprintf(\"unknown builtin: %s\", kind))\n\t}\n}\n\nfunc (s *serviceFile) generateEnvironmentalDefinitions() {\n\tappMetadata := createStruct(\n\t\t\"#Meta\",\n\t\tcreateTaggedDefinition(\n\t\t\t\"APIBaseURL\", \"APIBaseURL\",\n\t\t\t\"The base URL which can be used to call the API of this running application.\",\n\t\t\tast.NewIdent(\"string\"),\n\t\t),\n\t\tcreateStruct(\n\t\t\t\"Environment\",\n\t\t\tcreateTaggedDefinition(\n\t\t\t\t\"Name\", \"EnvName\",\n\t\t\t\t\"The name of this environment\",\n\t\t\t\tast.NewIdent(\"string\"),\n\t\t\t),\n\t\t\tcreateTaggedEnumDefinition(\n\t\t\t\t\"Type\", \"EnvType\",\n\t\t\t\t\"The type of environment that the application is running in\",\n\t\t\t\t\"production\", \"development\", \"ephemeral\", \"test\",\n\t\t\t),\n\t\t\tcreateTaggedEnumDefinition(\n\t\t\t\t\"Cloud\", \"CloudType\",\n\t\t\t\t\"The cloud provider that the application is running in\",\n\t\t\t\t\"aws\", \"azure\", \"gcp\", \"encore\", \"local\",\n\t\t\t),\n\t\t),\n\t)\n\n\tappMetadata.AddComment(&ast.CommentGroup{\n\t\tList: []*ast.Comment{\n\t\t\t{Text: \"// #Meta contains metadata about the running Encore application.\"},\n\t\t\t{Text: \"// The values in this struct will be injected by Encore upon deployment and can be\"},\n\t\t\t{Text: \"// referenced from other config values for example when configuring a callback URL:\"},\n\t\t\t{Text: \"//    CallbackURL: \\\"\\\\(#Meta.APIBaseURL)/webhooks.Handle`\\\"\"},\n\t\t},\n\t})\n\n\ts.file.Decls = append(s.file.Decls, appMetadata)\n}\n\nfunc createStruct(name string, fields ...any) *ast.Field {\n\treturn &ast.Field{\n\t\tLabel: ast.NewIdent(name),\n\t\tValue: ast.NewStruct(fields...),\n\t}\n}\n\nfunc createTaggedEnumDefinition(name string, tagName string, comment string, options ...string) *ast.Field {\n\tset := make([]ast.Expr, len(options))\n\tfor i, key := range options {\n\t\tset[i] = ast.NewString(key)\n\t}\n\n\treturn createTaggedDefinition(\n\t\tname, tagName,\n\t\tcomment,\n\t\tast.NewBinExpr(token.OR, set...),\n\t)\n}\n\nfunc createTaggedDefinition(name string, tagName string, comment string, value ast.Expr) *ast.Field {\n\tfield := &ast.Field{\n\t\tLabel: ast.NewIdent(name),\n\t\tValue: value,\n\t\tAttrs: []*ast.Attribute{\n\t\t\t{\n\t\t\t\tAt: token.NoPos,\n\t\t\t\tText: fmt.Sprintf(\n\t\t\t\t\t\"@tag(%s)\",\n\t\t\t\t\ttagName,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t}\n\n\tfield.AddComment(&ast.CommentGroup{\n\t\tPosition: 4,\n\t\tList: []*ast.Comment{\n\t\t\t{\n\t\t\t\tSlash: token.NoPos,\n\t\t\t\tText:  fmt.Sprintf(\"// %s\", comment),\n\t\t\t},\n\t\t},\n\t})\n\n\treturn field\n}\n\nfunc unwrapConfig(errs *perr.List, typ schema.Type) (unwrapped schema.Type, wasConfig bool) {\n\tif named, ok := typ.(schema.NamedType); ok {\n\t\tif underlying, isList, isConfig := schemautil.UnwrapConfigType(errs, named); isConfig {\n\t\t\tif isList {\n\t\t\t\tunderlying = schema.ListType{Elem: underlying}\n\t\t\t}\n\t\t\treturn underlying, true\n\t\t}\n\t}\n\treturn typ, false\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_config.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Name        string    // The users name\n    Port        uint16\n    ReadOnly    bool      // true if we're in read only mode\n\n    // MagicNumber is complicated and requires\n    // a multi-line comment to explain it.\n    MagicNumber int\n\n    ID          uuid.UUID // An ID\n\n    PublicKey []byte\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_config_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tName:     string // The users name\n\tPort:     uint16\n\tReadOnly: bool // true if we're in read only mode\n\n\t// MagicNumber is complicated and requires\n\t// a multi-line comment to explain it.\n\tMagicNumber: int\n\tID:          string // An ID\n\tPublicKey:   bytes\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_inline_struct.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    // The options for the HTTP server\n    HTTP struct {\n        Enabled bool // Is this option enabled?\n        Port    uint32 // What port should we run on?\n    }\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_inline_struct_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\t// The options for the HTTP server\n\tHTTP: {\n\t\tEnabled: bool   // Is this option enabled?\n\t\tPort:    uint32 // What port should we run on?\n\t}\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_lists.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Ages      []int32\n    OtherBits []string\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_lists_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tAges: [...int32]\n\tOtherBits: [...string]\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_maps.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Ages map[string]int\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_maps_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tAges: [string]: int\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_named_struct_multiple_uses.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\n// ServerOptions represent options for a server\ntype ServerOptions struct {\n    Enabled bool // Is this option enabled?\n    Port    uint32 // What port should we run on?\n}\n\ntype Config struct {\n    HTTP ServerOptions // The options for the HTTP server\n    TCP  ServerOptions // The options for the TCP server\n    GRPC ServerOptions // The options for the GRPC server\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_named_struct_multiple_uses_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tHTTP: #ServerOptions // The options for the HTTP server\n\tTCP:  #ServerOptions // The options for the TCP server\n\tGRPC: #ServerOptions // The options for the GRPC server\n}\n#Config\n\n// ServerOptions represent options for a server\n#ServerOptions: {\n\tEnabled: bool   // Is this option enabled?\n\tPort:    uint32 // What port should we run on?\n}"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_named_struct_single_use.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype ServerOptions struct {\n    Enabled bool // Is this option enabled?\n    Port    uint32 // What port should we run on?\n}\n\ntype Config struct {\n    HTTP ServerOptions // The options for the HTTP server\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_named_struct_single_use_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\t// The options for the HTTP server\n\tHTTP: {\n\t\tEnabled: bool   // Is this option enabled?\n\t\tPort:    uint32 // What port should we run on?\n\t}\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_no_config.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_no_config_svc.cue",
    "content": ""
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_with_cue_imports.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    SomeTime time.Time\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_with_cue_imports_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\nimport \"time\"\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tSomeTime: time.Time\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_wrappers.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Name        config.String    // The users name\n    Port        config.Uint16\n    ReadOnly    config.Bool      // true if we're in read only mode\n\n    // MagicNumber is complicated and requires\n    // a multi-line comment to explain it.\n    MagicNumber config.Int\n\n    Start       config.Time // The time at which the service was first started\n    ID          config.UUID // An ID\n\n    PublicKey config.Value[[]byte]\n\n    AdminUsers config.Values[string]\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/basic_wrappers_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\nimport \"time\"\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tName:     string // The users name\n\tPort:     uint16\n\tReadOnly: bool // true if we're in read only mode\n\n\t// MagicNumber is complicated and requires\n\t// a multi-line comment to explain it.\n\tMagicNumber: int\n\tStart:       time.Time // The time at which the service was first started\n\tID:          string    // An ID\n\tPublicKey:   bytes\n\tAdminUsers: [...string]\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/cue_optional_tag.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype ServerOption struct {\n    Option   int64\n    Disabled bool `cue:\",opt\"` // True if this is disabled\n}\n\ntype Config struct {\n    HTTP    ServerOption\n    Another ServerOption\n    TCP     ServerOption `cue:\",opt\"`\n    GRPC    ServerOption `cue:\",opt\"`\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/cue_optional_tag_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tHTTP:    #ServerOption\n\tAnother: #ServerOption\n\tTCP?:    #ServerOption\n\tGRPC?:   #ServerOption\n}\n#Config\n\n#ServerOption: {\n\tOption:    int64\n\tDisabled?: bool // True if this is disabled\n}"
  },
  {
    "path": "v2/codegen/cuegen/testdata/cue_tags.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    A int\n    B int `cue:\"A+C\"`\n    C int `cue:\"B-A\"`\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/cue_tags_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tA: int\n\tB: int & A+C\n\tC: int & B-A\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/generic_named_types.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\n// Generic option which can be disbaled\ntype DisablableOption[T any] struct {\n    Option   T\n    Disabled bool // True if this is disabled\n}\n\ntype List[T any] []T\n\n// A nice generic map\ntype Map[K any, V any] map[K]V\n\ntype Config struct {\n    HTTP    DisablableOption[uint16] // The options for the HTTP server\n    Another DisablableOption[uint64]\n    TCP     DisablableOption[uint16] // The options for the TCP server\n    GRPC    DisablableOption[uint64] // The options for the GRPC server\n    List1   List[string]             // A list of strings\n    List2   List[string]\n    List3   List[int]\n    Map1    Map[string, string]\n    Map2    Map[int, string]\n    Map3    Map[string, string]\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/generic_named_types_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tHTTP:    #DisablableOption_uint16 // The options for the HTTP server\n\tAnother: #DisablableOption_uint64\n\tTCP:     #DisablableOption_uint16 // The options for the TCP server\n\tGRPC:    #DisablableOption_uint64 // The options for the GRPC server\n\tList1:   #List_string             // A list of strings\n\tList2:   #List_string\n\tList3: [...int]\n\tMap1: #Map_string_string\n\tMap2: [int]: string\n\tMap3: #Map_string_string\n}\n#Config\n\n#List_string: [...string]\n\n// Generic option which can be disbaled\n#DisablableOption_uint64: {\n\tOption:   uint64\n\tDisabled: bool // True if this is disabled\n}\n\n// Generic option which can be disbaled\n#DisablableOption_uint16: {\n\tOption:   uint16\n\tDisabled: bool // True if this is disabled\n}\n\n// A nice generic map\n#Map_string_string: {\n\t[string]: string\n}"
  },
  {
    "path": "v2/codegen/cuegen/testdata/generic_top_level_type.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config[T int] struct {\n    Value T `json:\"value,omitempty\"` // Some config\n}\n\nvar _ = config.Load[*Config[uint]]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/generic_top_level_type_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tvalue?: uint // Some config\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/json_tags.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype ServerOption struct {\n    Option   int64\n    Disabled bool `json:\",omitempty\"` // True if this is disabled\n}\n\ntype Config struct {\n    HTTP    ServerOption\n    Another ServerOption `json:\"a_n_o_t_h_e_r\"`\n    TCP     ServerOption `json:\",omitempty\"`\n    GRPC    ServerOption `json:\",omitempty\"`\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/json_tags_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tHTTP:          #ServerOption\n\ta_n_o_t_h_e_r: #ServerOption\n\tTCP?:          #ServerOption\n\tGRPC?:         #ServerOption\n}\n#Config\n\n#ServerOption: {\n\tOption:    int64\n\tDisabled?: bool // True if this is disabled\n}"
  },
  {
    "path": "v2/codegen/cuegen/testdata/merge_identical_comments.txt",
    "content": "-- svc/a.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype AsConfig struct {\n    // Multiline test\n    // comment to deduplicate.\n    Foo config.String\n}\n\nvar _ = config.Load[*AsConfig]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- svc/b.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype BsConfig struct{\n    Foo config.String // Some extra comment\n}\n\nvar _ = config.Load[BsConfig]()\n\n-- svc/c.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype CsConfig struct{\n    // Multiline test\n    // comment to deduplicate.\n    Foo config.String\n}\n\nvar _ = config.Load[CsConfig]()\n\n-- svc/d.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype DsConfig struct{\n    Foo config.String // Some extra comment\n}\n\nvar _ = config.Load[DsConfig]()\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/merge_identical_comments_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\t// Multiline test\n\t// comment to deduplicate.\n\t// Some extra comment\n\tFoo: string\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/multiple_configs_in_service.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Name        string    // The users name\n    Port        uint16\n    ReadOnly    bool      // true if we're in read only mode\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- svc/a_file.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype OtherConfig struct{\n    Foo config.String // Foo is great in Otherconfig\n    Bar config.Int\n}\n\nvar _ = config.Load[OtherConfig]()\n\n-- svc/z_file.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ThisConfig struct{\n    Foo config.String // And even better in ThisConfig\n    Baz config.Bool\n}\n\nvar _ = config.Load[ThisConfig]()\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/multiple_configs_in_service_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\t// Foo is great in Otherconfig\n\t// And even better in ThisConfig\n\tFoo:      string\n\tBar:      int\n\tName:     string // The users name\n\tPort:     uint16\n\tReadOnly: bool // true if we're in read only mode\n\tBaz:      bool\n}\n#Config"
  },
  {
    "path": "v2/codegen/cuegen/testdata/types_from_multiple_packages.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n\n\t\"encore.app/svc/utils\"\n\t\"encore.app/svc/helpers\"\n)\n\ntype Config struct {\n    A helpers.ExtraConfig\n    B utils.ExtraConfig\n    C helpers.ExtraConfig\n    D utils.ExtraConfig\n    E helpers.SingleUse\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- svc/utils/config.go --\npackage utils\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ExtraConfig struct{\n    Foo config.String\n    Bar config.Int\n}\n\n-- svc/helpers/config.go --\npackage helpers\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ExtraConfig struct{\n    Foo config.String\n    Baz config.Value[[]byte]\n}\n\ntype SingleUse struct {\n    Lock bool\n}\n"
  },
  {
    "path": "v2/codegen/cuegen/testdata/types_from_multiple_packages_svc.cue",
    "content": "// Code generated by encore. DO NOT EDIT.\n//\n// The contents of this file are generated from the structs used in\n// conjunction with Encore's `config.Load[T]()` function. This file\n// automatically be regenerated if the data types within the struct\n// are changed.\n//\n// For more information about this file, see:\n// https://encore.dev/docs/develop/config\npackage svc\n\n// #Meta contains metadata about the running Encore application.\n// The values in this struct will be injected by Encore upon deployment and can be\n// referenced from other config values for example when configuring a callback URL:\n//    CallbackURL: \"\\(#Meta.APIBaseURL)/webhooks.Handle`\"\n#Meta: {\n\tAPIBaseURL: string @tag(APIBaseURL) // The base URL which can be used to call the API of this running application.\n\tEnvironment: {\n\t\tName:  string                                              @tag(EnvName)   // The name of this environment\n\t\tType:  \"production\" | \"development\" | \"ephemeral\" | \"test\" @tag(EnvType)   // The type of environment that the application is running in\n\t\tCloud: \"aws\" | \"azure\" | \"gcp\" | \"encore\" | \"local\"        @tag(CloudType) // The cloud provider that the application is running in\n\t}\n}\n\n// #Config is the top level configuration for the application and is generated\n// from the Go types you've passed into `config.Load[T]()`. Encore uses a definition\n// of this struct which is closed, such that the CUE tooling can any typos of field names.\n// this definition is then immediately inlined, so any fields within it are expected\n// as fields at the package level.\n#Config: {\n\tA: #ExtraConfig\n\tB: #ExtraConfig_1\n\tC: #ExtraConfig\n\tD: #ExtraConfig_1\n\tE: Lock: bool\n}\n#Config\n\n#ExtraConfig: {\n\tFoo: string\n\tBaz: bytes\n}\n\n#ExtraConfig_1: {\n\tFoo: string\n\tBar: int\n}"
  },
  {
    "path": "v2/codegen/cuegen/utils.go",
    "content": "package cuegen\n\nimport (\n\t\"strings\"\n\n\t\"cuelang.org/go/cue/ast\"\n\t\"cuelang.org/go/cue/token\"\n)\n\n// addCommentToField adds the given string to a Field\n//\n// If the str is a single line, then the comment group will be positioned at\n// the end of the node it is attached to.\n//\n// If the str is a multi-line string, then the comment group will be positioned\n// at the before the node it is attached to.\nfunc addCommentToField(field *ast.Field, str string) {\n\tlines := strings.Split(strings.TrimSpace(str), \"\\n\")\n\n\t// Position 4 = after the attached node, position 0 = before the attached node\n\tcommentPosition := int8(4)\n\t_, isStruct := field.Value.(*ast.StructLit)\n\tif len(lines) > 1 || isStruct {\n\t\tcommentPosition = 0\n\t}\n\tgrp := &ast.CommentGroup{\n\t\tPosition: commentPosition,\n\t}\n\n\tfor _, line := range lines {\n\t\tgrp.List = append(grp.List, &ast.Comment{Text: \"// \" + line})\n\t}\n\n\t// If this comment is multiline, then the first line should be positioned on a NewSection for force an empty line\n\t// before the comment group\n\tif commentPosition == 0 {\n\t\tgrp.List[0].Slash = token.NewSection.Pos()\n\t}\n\n\tfield.AddComment(grp)\n}\n\n// commentAlreadyPresent returns true if the field already contains all of the comment of `contains`\n// within one of it's existing comment groups\nfunc commentAlreadyPresent(field *ast.Field, contains *ast.CommentGroup) bool {\n\tif len(contains.List) == 0 {\n\t\treturn true\n\t}\n\ncommentGroupLoop:\n\tfor _, c := range field.Comments() {\n\t\t// Range over this group comment\n\tgroupLineLoop:\n\t\tfor i, line := range c.List {\n\t\t\tif i+len(contains.List) > len(c.List) {\n\t\t\t\t// If we've not got enough lines to contain the entire comment, then\n\t\t\t\t// we can try the next comment group\n\t\t\t\tcontinue commentGroupLoop\n\t\t\t}\n\n\t\t\t// If we find the first line then great, let's check it\n\t\t\tif strings.TrimSpace(line.Text) == strings.TrimSpace(contains.List[0].Text) {\n\t\t\t\tfor j, containsLine := range contains.List {\n\t\t\t\t\tif strings.TrimSpace(c.List[i+j].Text) != strings.TrimSpace(containsLine.Text) {\n\t\t\t\t\t\tcontinue groupLineLoop\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If here the entire comment group is contained within field already\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc hasCommentInPosition(field *ast.Field, pos int8) bool {\n\tfor _, c := range field.Comments() {\n\t\tif len(c.List) > 1 {\n\t\t\treturn true\n\t\t}\n\t\tif c.Position == pos {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "v2/codegen/decls.go",
    "content": "package codegen\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\nvar importNames = map[string]string{\n\t\"github.com/felixge/httpsnoop\":        \"httpsnoop\",\n\t\"github.com/json-iterator/go\":         \"jsoniter\",\n\t\"github.com/julienschmidt/httprouter\": \"httprouter\",\n\n\t\"encore.dev/appruntime/apisdk/api\":      \"__api\",\n\t\"encore.dev/appruntime/apisdk/app\":      \"__app\",\n\t\"encore.dev/appruntime/infrasdk/config\": \"__config\",\n\t\"encore.dev/appruntime/shared/etype\":    \"__etype\",\n\t\"encore.dev/appruntime/exported/model\":  \"__model\",\n\t\"encore.dev/appruntime/shared/serde\":    \"__serde\",\n\t\"encore.dev/appruntime/apisdk/service\":  \"__service\",\n\t\"encore.dev/beta/errs\":                  \"errs\",\n\t\"encore.dev/storage/sqldb\":              \"sqldb\",\n\t\"encore.dev/types/uuid\":                 \"uuid\",\n}\n\nfunc newFile(pkg *pkginfo.Package, baseName, shortName string) *File {\n\treturn newFileForPath(pkg.ImportPath, pkg.Name, pkg.FSPath, baseName, shortName)\n}\n\nfunc newFileForPath(pkgPath paths.Pkg, pkgName string, pkgDir paths.FS, baseName, shortName string) *File {\n\tjenFile := jen.NewFilePathName(pkgPath.String(), pkgName)\n\n\tfor pkgPath, alias := range importNames {\n\t\tjenFile.ImportAlias(pkgPath, alias)\n\t}\n\n\treturn &File{\n\t\tJen:       jenFile,\n\t\tdir:       pkgDir,\n\t\tpkgPath:   pkgPath,\n\t\tbaseName:  baseName,\n\t\tshortName: shortName,\n\t}\n}\n\n// File represents a generated file for a specific package.\ntype File struct {\n\tJen *jen.File // the jen file we're generating\n\n\t// dir is the filesystem directory where the file should exist\n\t// within the application source tree. It need not match\n\t// any existing physical directory in the case of overlays.\n\tdir paths.FS\n\n\tpkgPath  paths.Pkg // the package the file belongs to\n\tbaseName string    // the file's base name\n\t// shortName is the short version of the base name, for generated files.\n\t// For example if the base name is \"encore_internal__metrics.go\",\n\t// shortName is \"metrics\".\n\tshortName string\n\n\tdecls []any // ordered list of Decl or jen.Code\n}\n\n// ImportAnon adds an anonymous (\"_\"-prefixed) import of the given packages.\nfunc (f *File) ImportAnon(pkgs ...paths.Pkg) {\n\tf.Jen.Anon(fns.Map(pkgs, func(pkg paths.Pkg) string {\n\t\treturn pkg.String()\n\t})...)\n}\n\n// name returns the computed file name.\nfunc (f *File) name() string {\n\treturn f.baseName\n}\n\n// Add adds a declaration to the file.\nfunc (f *File) Add(code jen.Code) {\n\tif code != nil {\n\t\tf.decls = append(f.decls, code)\n\t}\n}\n\n// Render renders the file to the given writer.\nfunc (f *File) Render(w io.Writer) error {\n\tfor i, d := range f.decls {\n\t\tif i > 0 {\n\t\t\tf.Jen.Line()\n\t\t}\n\n\t\tswitch d := d.(type) {\n\t\tcase Decl:\n\t\t\tf.Jen.Add(d.Code())\n\t\tcase jen.Code:\n\t\t\tf.Jen.Add(d)\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"internal error: unknown decl type: %T\", d))\n\t\t}\n\t}\n\treturn f.Jen.Render(w)\n}\n\ntype Decl interface {\n\tName() string\n\tQual() *jen.Statement\n\tCode() *jen.Statement\n}\n\nfunc (f *File) HasDecl(nameParts ...string) bool {\n\tfor _, decl := range f.decls {\n\t\tswitch decl := decl.(type) {\n\t\tcase *FuncDecl:\n\t\t\tif slices.Equal(decl.nameParts, nameParts) {\n\t\t\t\treturn true\n\t\t\t}\n\t\tcase *VarDecl:\n\t\t\tif slices.Equal(decl.nameParts, nameParts) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (f *File) FuncDecl(nameParts ...string) *FuncDecl {\n\tif len(nameParts) == 0 {\n\t\tpanic(\"gen.VarDecl: empty nameParts\")\n\t}\n\td := &FuncDecl{\n\t\tFile:      f,\n\t\tnameParts: nameParts,\n\t}\n\tf.decls = append(f.decls, d)\n\treturn d\n}\n\nfunc (f *File) VarDecl(nameParts ...string) *VarDecl {\n\tif len(nameParts) == 0 {\n\t\tpanic(\"gen.VarDecl: empty nameParts\")\n\t}\n\td := &VarDecl{\n\t\tFile:      f,\n\t\tnameParts: nameParts,\n\t}\n\tf.decls = append(f.decls, d)\n\treturn d\n}\n\n// FuncDecl represents a generated declaration.\ntype FuncDecl struct {\n\tFile *File // file the declaration belongs to.\n\n\t// nameParts are the suffix parts of the generated name.\n\t// For example, if the parts are [\"foo\", \"bar\"] and the\n\t// file shortName is \"metrics\", the generated declaration name\n\t// is \"EncoreInternal_metrics_foo_bar\".\n\tnameParts []string\n\n\t// typeParams are the type parameters of the generated function.\n\ttypeParams []jen.Code\n\n\t// params are the parameters of the generated function.\n\tparams []jen.Code\n\n\t// results are the results of the generated function.\n\tresults []jen.Code\n\n\t// body is the body of the generated function.\n\tbody *jen.Statement\n}\n\n// Name returns the package-level name of the declaration.\nfunc (d *FuncDecl) Name() string {\n\treturn \"EncoreInternal_\" + d.File.shortName + \"_\" + strings.Join(d.nameParts, \"_\")\n}\n\n// Qual returns the qualified name of the declaration.\nfunc (d *FuncDecl) Qual() *jen.Statement {\n\treturn jen.Qual(d.File.pkgPath.String(), d.Name())\n}\n\n// Code returns the generated code.\nfunc (d *FuncDecl) Code() *jen.Statement {\n\ts := jen.Func().Id(d.Name())\n\tif len(d.typeParams) > 0 {\n\t\ts = s.Types(d.typeParams...)\n\t}\n\tif len(d.params) > 0 {\n\t\ts = s.Params(d.params...)\n\t}\n\tif len(d.results) > 0 {\n\t\ts = s.Params(d.results...)\n\t}\n\tif d.body != nil {\n\t\ts = s.Add(d.body)\n\t} else {\n\t\ts = s.Block()\n\t}\n\treturn s\n}\n\n// TypeParams appends to the type parameters of the generated function.\nfunc (d *FuncDecl) TypeParams(params ...jen.Code) *FuncDecl {\n\td.typeParams = append(d.typeParams, params...)\n\treturn d\n}\n\n// Params appends to the parameters of the generated function.\nfunc (d *FuncDecl) Params(params ...jen.Code) *FuncDecl {\n\td.params = append(d.params, params...)\n\treturn d\n}\n\n// Results appends to the results of the generated function.\nfunc (d *FuncDecl) Results(results ...jen.Code) *FuncDecl {\n\td.results = append(d.results, results...)\n\treturn d\n}\n\n// Body sets the body of the generated function.\nfunc (d *FuncDecl) Body(code ...jen.Code) *FuncDecl {\n\td.body = jen.Block(code...)\n\treturn d\n}\n\n// BodyFunc computes the body of the generated function.\nfunc (d *FuncDecl) BodyFunc(fn func(g *jen.Group)) *FuncDecl {\n\td.body = jen.BlockFunc(fn)\n\treturn d\n}\n\ntype VarDecl struct {\n\tFile *File // file the declaration belongs to.\n\n\t// nameParts are the suffix parts of the generated name.\n\t// For example, if the parts are [\"foo\", \"bar\"] and the\n\t// file shortName is \"metrics\", the generated declaration name\n\t// is \"EncoreInternal_metrics_foo_bar\".\n\tnameParts []string\n\n\tvalue *jen.Statement\n}\n\nfunc (d *VarDecl) Value(code ...jen.Code) *VarDecl {\n\td.value = jen.Add(code...)\n\treturn d\n}\n\nfunc (d *VarDecl) Code() *jen.Statement {\n\treturn jen.Var().Id(d.Name()).Op(\"=\").Add(d.value)\n}\n\nfunc (d *VarDecl) Name() string {\n\treturn \"EncoreInternal_\" + d.File.shortName + \"_\" + strings.Join(d.nameParts, \"_\")\n}\n\nfunc (d *VarDecl) Qual() *jen.Statement {\n\treturn jen.Qual(d.File.pkgPath.String(), d.Name())\n}\n"
  },
  {
    "path": "v2/codegen/errors.go",
    "content": "package codegen\n\nimport \"encr.dev/pkg/errors\"\n\nvar (\n\terrRange = errors.Range(\n\t\t\"codegen\",\n\t\t\"\",\n\t)\n\n\terrRender = errRange.New(\n\t\t\"Failed to render codegen\",\n\t\t\"Generated code could not be parsed.\",\n\t\terrors.MarkAsInternalError(),\n\t)\n)\n"
  },
  {
    "path": "v2/codegen/gen.go",
    "content": "package codegen\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app/legacymeta\"\n\t\"encr.dev/v2/codegen/apigen/typescrub\"\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/codegen/rewrite\"\n\t\"encr.dev/v2/internals/overlay\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\ntype Generator struct {\n\t*parsectx.Context\n\n\tUtil             *genutil.Helper\n\tTraceNodes       *legacymeta.TraceNodes\n\tTypeScrubber     *typescrub.Computer\n\trewrites         map[*pkginfo.File]*rewrite.Rewriter\n\tfiles            map[fileKey]*File\n\taddedAppInit     map[paths.Pkg]bool\n\taddedTestSupport map[paths.Pkg]bool\n}\n\nfunc New(c *parsectx.Context, traceNodes *legacymeta.TraceNodes) *Generator {\n\treturn &Generator{\n\t\tContext:          c,\n\t\tUtil:             genutil.NewHelper(c.Errs),\n\t\tTraceNodes:       traceNodes,\n\t\tTypeScrubber:     typescrub.NewComputer(c.Log),\n\t\trewrites:         make(map[*pkginfo.File]*rewrite.Rewriter),\n\t\tfiles:            make(map[fileKey]*File),\n\t\taddedAppInit:     make(map[paths.Pkg]bool),\n\t\taddedTestSupport: make(map[paths.Pkg]bool),\n\t}\n}\n\nfunc (g *Generator) Rewrite(file *pkginfo.File) *rewrite.Rewriter {\n\tif r, ok := g.rewrites[file]; ok {\n\t\treturn r\n\t}\n\tr := rewrite.New(file.Contents(), file.Token().Base())\n\tg.rewrites[file] = r\n\treturn r\n}\n\ntype fileKey struct {\n\tpkgPath  paths.Pkg\n\tbaseName string\n}\n\nfunc (g *Generator) File(pkg *pkginfo.Package, shortName string) *File {\n\tbaseName := \"encore_internal__\" + shortName + \".go\"\n\tkey := fileKey{pkg.ImportPath, baseName}\n\tif f, ok := g.files[key]; ok {\n\t\treturn f\n\t}\n\tf := newFile(pkg, baseName, shortName)\n\tg.files[key] = f\n\treturn f\n}\n\nfunc (g *Generator) InjectFile(pkgPath paths.Pkg, pkgName string, pkgDir paths.FS, baseName, shortName string) *File {\n\tkey := fileKey{pkgPath, baseName}\n\tif f, ok := g.files[key]; ok {\n\t\treturn f\n\t}\n\tf := newFileForPath(pkgPath, pkgName, pkgDir, baseName, shortName)\n\tg.files[key] = f\n\treturn f\n}\n\nfunc (g *Generator) Overlays() []overlay.File {\n\tvar of []overlay.File\n\n\tvar buf bytes.Buffer\n\tfor _, f := range g.files {\n\t\tsource := f.dir.Join(f.name())\n\n\t\tbuf.Reset()\n\t\tif err := f.Render(&buf); err != nil {\n\t\t\tg.Errs.Add(errRender.InFile(source.ToIO()).Wrapping(err))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Get a copy of the buffer since we reuse it across files.\n\t\tcontents := slices.Clone(buf.Bytes())\n\n\t\tof = append(of, overlay.File{\n\t\t\tSource:   source,\n\t\t\tContents: contents,\n\t\t})\n\t}\n\n\tfor f, rw := range g.rewrites {\n\t\tsource := f.Pkg.FSPath.Join(f.Name)\n\t\tof = append(of, overlay.File{\n\t\t\tSource:   source,\n\t\t\tContents: rw.Data(),\n\t\t})\n\t}\n\n\treturn of\n}\n\n// InsertTestSupport inserts an import of the testsupport package in the given package.\nfunc (g *Generator) InsertTestSupport(pkg *pkginfo.Package) {\n\tif g.addedTestSupport[pkg.ImportPath] {\n\t\treturn\n\t}\n\tg.addedTestSupport[pkg.ImportPath] = true\n\n\tfile := pkg.Files[0]\n\trw := g.Rewrite(file)\n\ta := file.AST()\n\n\tinsertPos := a.Name.End()\n\tln := g.FS.Position(insertPos)\n\trw.Insert(insertPos, []byte(fmt.Sprintf(\"\\nimport _ %s;/*line :%d:%d*/\",\n\t\tstrconv.Quote(\"encore.dev/appruntime/shared/testsupport\"), ln.Line, ln.Column)))\n}\n"
  },
  {
    "path": "v2/codegen/infragen/cachegen/cachegen.go",
    "content": "package cachegen\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/caches\"\n)\n\nfunc GenKeyspace(gen *codegen.Generator, pkg *pkginfo.Package, keyspaces []*caches.Keyspace) {\n\tf := gen.File(pkg, \"cache\")\n\tfor idx, ks := range keyspaces {\n\t\tgenKeyspaceMappers(gen, f, ks, idx)\n\t}\n}\n\nfunc genKeyspaceMappers(gen *codegen.Generator, f *codegen.File, ks *caches.Keyspace, idx int) {\n\t// Construct the key mapper function. We use the index since keyspaces\n\t// do not have resource names.\n\tmapper := f.FuncDecl(\"keyMapper\", strconv.Itoa(idx))\n\n\tconst input = \"in\"\n\tmapper.Params(Id(input).Add(gen.Util.Type(ks.KeyType)))\n\tmapper.Results(String())\n\tmapper.Body(Return(computePathExpression(gen.Errs, ks)))\n\n\t// Insert the label mapper configuration into the config literal.\n\tsnippet := fmt.Sprintf(\"EncoreInternal_KeyMapper: %s,\", mapper.Name())\n\tgen.Rewrite(ks.File).Insert(ks.ConfigLiteral.Lbrace+1, []byte(snippet))\n}\n\nconst input = \"in\"\n\nfunc computePathExpression(errs *perr.List, ks *caches.Keyspace) *Statement {\n\tstructFields, isBuiltin := getStructFields(errs, ks.KeyType)\n\tvar (\n\t\tpathLit strings.Builder\n\t\tfmtArgs []Code\n\t)\n\tfor i, seg := range ks.Path.Segments {\n\t\tif i > 0 {\n\t\t\tpathLit.WriteString(\"/\")\n\t\t}\n\t\tif seg.Type == resourcepaths.Literal {\n\t\t\tpathLit.WriteString(seg.Value)\n\t\t\tcontinue\n\t\t}\n\n\t\tif isBuiltin {\n\t\t\tverb, expr := rewriteBuiltin(structFields[builtinKey], Id(input))\n\t\t\tpathLit.WriteString(verb)\n\t\t\tfmtArgs = append(fmtArgs, expr)\n\t\t} else {\n\t\t\tverb, expr := rewriteBuiltin(structFields[seg.Value], Id(input).Dot(seg.Value))\n\t\t\tpathLit.WriteString(verb)\n\t\t\tfmtArgs = append(fmtArgs, expr)\n\t\t}\n\t}\n\n\tif len(fmtArgs) == 0 {\n\t\t// If there are no formatting arguments, return the string as a constant literal.\n\t\treturn Lit(pathLit.String())\n\t} else {\n\t\t// Otherwise pass them to fmt.Sprintf.\n\t\targs := append([]Code{Lit(pathLit.String())}, fmtArgs...)\n\t\treturn Qual(\"fmt\", \"Sprintf\").Call(args...)\n\t}\n}\n\n// builtinKey is the key to use into the structFields map when the key is a builtin.\nconst builtinKey = \"__builtin__\"\n\n// getStructFields resolves the struct key fields for the given keyspace.\nfunc getStructFields(errs *perr.List, keyType schema.Type) (structFields map[string]schema.BuiltinKind, isBuiltin bool) {\n\tif b, ok := keyType.(schema.BuiltinType); ok {\n\t\treturn map[string]schema.BuiltinKind{builtinKey: b.Kind}, true\n\t}\n\n\t// structFields provides a map of field names to the builtin\n\t// they represent. We're guaranteed these are all builtins by\n\t// the parser.\n\tstructFields = make(map[string]schema.BuiltinKind)\n\tref, ok := schemautil.ResolveNamedStruct(keyType, false)\n\tif !ok {\n\t\terrs.AddPos(keyType.ASTExpr().Pos(), \"invalid cache key type: must be a named struct\")\n\t\treturn nil, false\n\t} else if ref.Pointers > 0 {\n\t\terrs.AddPos(keyType.ASTExpr().Pos(), \"invalid cache key type: must not be a pointer type\")\n\t\treturn nil, false\n\t}\n\tst := schemautil.ConcretizeWithTypeArgs(errs, ref.Decl.Type, ref.TypeArgs).(schema.StructType)\n\n\tfor _, f := range st.Fields {\n\t\tif f.IsAnonymous() {\n\t\t\terrs.AddPos(f.AST.Pos(), \"anonymous fields are not supported in cache keys\")\n\t\t\tcontinue\n\t\t} else if f.Type.Family() != schema.Builtin {\n\t\t\terrs.Addf(f.AST.Pos(), \"invalid cache key field %s: must be builtin\",\n\t\t\t\tf.Name.MustGet())\n\t\t\tcontinue\n\t\t}\n\n\t\tstructFields[f.Name.MustGet()] = f.Type.(schema.BuiltinType).Kind\n\t}\n\treturn structFields, false\n}\n\n// rewriteBuiltin returns the code to rewrite a builtin type for use as a cache key.\nfunc rewriteBuiltin(kind schema.BuiltinKind, expr Code) (verb string, rewritten Code) {\n\tswitch kind {\n\tcase schema.String:\n\t\treturn \"%s\", Qual(\"strings\", \"ReplaceAll\").Call(expr, Lit(\"/\"), Lit(`\\/`))\n\tcase schema.Bytes:\n\t\treturn \"%s\", Qual(\"bytes\", \"ReplaceAll\").Call(\n\t\t\texpr,\n\t\t\tIndex().Byte().Parens(Lit(\"/\")),\n\t\t\tIndex().Byte().Parens(Lit(`\\/`)),\n\t\t)\n\tdefault:\n\t\treturn \"%v\", expr\n\t}\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/configgen.go",
    "content": "package configgen\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/eerror\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/internal/genutil\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/config\"\n)\n\nfunc Gen(gen *codegen.Generator, svc *app.Service, pkg *pkginfo.Package, loads []*config.Load) {\n\tf := gen.File(pkg, \"config_unmarshal\")\n\n\tbuilder := &configUnmarshalersBuilder{\n\t\terrs:           gen.Errs,\n\t\tgu:             gen.Util,\n\t\tf:              f.Jen,\n\t\tseenNames:      make(map[string]int),\n\t\tallocatedNames: make(map[pkginfo.QualifiedName]string),\n\t\tunmarshalers:   make([]*Statement, 0),\n\t}\n\tf.Jen.ImportAlias(\"github.com/json-iterator/go\", \"jsoniter\")\n\n\tf.Jen.Comment(`These functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().`)\n\n\t// Find all the types to write, and then write unmarshalers for them\n\ttypesToWrite := builder.FindAllDecls(loads)\n\n\t// Write load functions for generic types\n\tfor _, load := range loads {\n\t\tbuilder.generateConcreteUnmarshalers(load.Type)\n\t}\n\tif len(builder.unmarshalers) > 0 {\n\t\tbuilder.f.Line()\n\t\tbuilder.f.Comment(\"// Concrete unmarshalers for all config.Load calls, including those using generic types.\")\n\t\tbuilder.f.Comment(\"// These instances are used directly by calls to `config.Load[T]()`.\")\n\t\tbuilder.f.Var().DefsFunc(func(f *Group) {\n\t\t\tfor _, typ := range builder.unmarshalers {\n\t\t\t\tf.Add(typ)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Write unmarshalers for all the types needed for config\n\tfor _, decl := range typesToWrite {\n\t\tif err := builder.WriteTypeUnmarshaler(decl); err != nil {\n\t\t\tgen.Errs.Addf(decl.AST.Pos(), \"failed to generate config unmarshaler for %s: %v\", decl.Name, err)\n\t\t}\n\t}\n\n\t// Rewrite load functions to inject marshallers\n\tfor _, load := range loads {\n\t\trw := gen.Rewrite(load.File)\n\t\tvar buf bytes.Buffer\n\t\tbuf.WriteString(strconv.Quote(svc.Name))\n\t\tbuf.WriteString(\", \")\n\t\tbuf.WriteString(ConfigUnmarshalFuncName(gen.Util, load.Type))\n\t\tep := gen.FS.Position(load.FuncCall.Rparen)\n\t\t_, _ = fmt.Fprintf(&buf, \"/*line :%d:%d*/\", ep.Line, ep.Column)\n\t\trw.Replace(load.FuncCall.Lparen+1, load.FuncCall.Rparen, buf.Bytes())\n\t}\n}\n\ntype configUnmarshalersBuilder struct {\n\terrs *perr.List\n\tgu   *genutil.Helper\n\tf    *File\n\n\t// allocatedNames tracks maps type declarations to the allocated name\n\t// of the unmarshaller for that type.\n\tallocatedNames map[pkginfo.QualifiedName]string\n\t// seenNames tracks the number of times a declaration of a given name has been seen,\n\t// to avoid duplicates.\n\tseenNames map[string]int\n\n\t// Generated code\n\tunmarshalers []*Statement\n}\n\n// FindAllDecls returns a list of all decl's used within a list of config loads.\nfunc (cb *configUnmarshalersBuilder) FindAllDecls(loads []*config.Load) []*schema.TypeDecl {\n\ttypesToWrite := make(map[*schema.TypeDecl]struct{})\n\n\t// Walk the config load calls in this service and find all the named types used\n\tfor _, load := range loads {\n\t\tschemautil.Walk(load.Type, func(node schema.Type) bool {\n\t\t\tswitch n := node.(type) {\n\t\t\tcase schema.NamedType:\n\t\t\t\t// Ignore config.Foo types\n\t\t\t\tif n.DeclInfo.File.Pkg.ImportPath != \"encore.dev/config\" {\n\t\t\t\t\ttypesToWrite[n.Decl()] = struct{}{}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t}\n\n\t// Now convert the list of decls into a sorted list of nodes\n\tdecls := make([]*schema.TypeDecl, 0, len(typesToWrite))\n\tfor decl := range typesToWrite {\n\t\tdecls = append(decls, decl)\n\t}\n\tslices.SortFunc(decls, func(a, b *schema.TypeDecl) int {\n\t\t// Sort first by pkg path and then by name\n\t\tif n := cmp.Compare(a.File.Pkg.ImportPath, b.File.Pkg.ImportPath); n != 0 {\n\t\t\treturn n\n\t\t}\n\t\treturn cmp.Compare(a.Name, b.Name)\n\t})\n\n\treturn decls\n}\n\n// WriteTypeUnmarshaler writes a function which will unmarshal the given decl from JSON to the instance of the\n// Go type. If the decl takes type parameters, then the function generated will also be generic and will require\n// unmarshalers for the type parameters to be passed in.\nfunc (cb *configUnmarshalersBuilder) WriteTypeUnmarshaler(decl *schema.TypeDecl) (err error) {\n\tunmarshalerName, _ := cb.typeUnmarshalerName(decl)\n\n\tcb.f.Line()\n\tcb.f.Commentf(\n\t\t\"// %s will unmarshal the JSON representation into the given type, taking account for \"+\n\t\t\t\"\\n// the `config.Value` dynamic functions.\",\n\t\tunmarshalerName,\n\t)\n\tf := cb.f.Func().Id(unmarshalerName)\n\trtnType := Id(\"obj\").Add(genutil.Q(decl.Info))\n\n\t// This is the function body (plus arguments) needed for config.Unmarshaler\n\tunmarshalBody := Params(\n\t\tId(\"itr\").Op(\"*\").Qual(\"github.com/json-iterator/go\", \"Iterator\"),\n\t\tId(\"path\").Index().String(),\n\t).Params(\n\t\trtnType,\n\t).BlockFunc(func(f *Group) {\n\t\tstruc, ok := decl.Type.(schema.StructType)\n\t\tif !ok {\n\t\t\terr = eerror.New(\"codegen\", \"can only unmarshal struct types\", nil)\n\t\t\treturn\n\t\t}\n\t\tcb.readStruct(f, struc)\n\t\tf.Return()\n\t})\n\n\t// If this is a generic type, we add an additional wrapper so we can provide the generic parameters\n\tif len(decl.TypeParams) > 0 {\n\t\ttypeParams := make([]Code, len(decl.TypeParams))\n\t\touterParams := make([]Code, len(decl.TypeParams))\n\t\treturnTypeParams := make([]Code, len(decl.TypeParams))\n\t\tfor i, param := range decl.TypeParams {\n\t\t\ttypeParams[i] = Id(param.Name).Any()\n\t\t\touterParams[i] = Id(cb.typeParamUnmarshalerName(param)).Qual(\"encore.dev/config\", \"Unmarshaler\").Types(Id(param.Name))\n\t\t\treturnTypeParams[i] = Id(param.Name)\n\t\t}\n\t\tf = f.Types(typeParams...)\n\t\trtnType = rtnType.Types(returnTypeParams...)\n\n\t\t// Then the outer function needs to return a concrete instance of an unmarshaler which\n\t\t// uses the unmarshal body\n\t\tf = f.Params(outerParams...).\n\t\t\tParams(Id(\"concreteUnmarshaler\").Qual(\"encore.dev/config\", \"Unmarshaler\").Types(Id(decl.Name).Types(returnTypeParams...))).\n\t\t\tBlock(Return(Func().Add(unmarshalBody)))\n\t} else {\n\t\t// If this isn't generic, we can just use the unmarshal body directly\n\t\tf.Add(unmarshalBody)\n\t}\n\n\treturn\n}\n\n// readType generates code to read a single instance of the given type directly from `iter` which should be a pointer\n// to a jsoniter.Iterator.\n//\n// The `reader` code returned by this function is expected to be used as the right hand side of a single assignment\n// statement.\n// i.e. `var x = readType(...)` => `var x = iter.ReadBool()`\n//\n// The second return value is the identifier of the type from the first return value.\n// ie. `bool`\nfunc (cb *configUnmarshalersBuilder) readType(typ schema.Type, pathElement Code) (reader Code, rtnTyp *Statement) {\n\tswitch t := typ.(type) {\n\tcase schema.NamedType:\n\t\tif underlying, isList, isConfig := schemautil.UnwrapConfigType(cb.errs, t); isConfig {\n\t\t\tif isList {\n\t\t\t\tcode, _ := cb.readType(schema.ListType{Elem: underlying}, pathElement)\n\t\t\t\t_, returnType := cb.readType(underlying, pathElement)\n\t\t\t\treturn Qual(\"encore.dev/config\", \"CreateValueList\").Call(code, Append(Id(\"path\"), pathElement)), Qual(\"encore.dev/config\", \"Values\").Types(returnType)\n\t\t\t} else {\n\t\t\t\tcode, returnType := cb.readType(underlying, pathElement)\n\t\t\t\treturn Qual(\"encore.dev/config\", \"CreateValue\").Types(returnType).Call(\n\t\t\t\t\tcode, Append(Id(\"path\"), pathElement)), Qual(\"encore.dev/config\", \"Value\").Types(returnType)\n\t\t\t}\n\t\t}\n\n\t\tfuncRef, returnType := cb.typeUnmarshalerFunc(typ)\n\n\t\treturn funcRef.Call(\n\t\t\tId(\"itr\"),\n\t\t\tAppend(Id(\"path\"), pathElement),\n\t\t), returnType\n\n\tcase schema.StructType:\n\t\tvar returnType = Nil()\n\t\tblock := BlockFunc(func(f *Group) {\n\t\t\treturnType = cb.readStruct(f, t)\n\t\t\tf.Return()\n\t\t})\n\t\treturn Func().Params().Params(Id(\"obj\").Add(returnType)).Add(block).Call(), returnType\n\n\tcase schema.MapType:\n\t\t_, keyType := cb.readType(t.Key, nil)\n\t\tvalueUnmarshaler, valueType := cb.readType(t.Value, Id(\"keyAsString\"))\n\t\trtnType := Map(keyType).Add(valueType)\n\n\t\t// Call a helper method in the runtime package, which requires us to pass a callback\n\t\t// which returns the unmarshalled keys and values.\n\t\treturn Qual(\"encore.dev/config\", \"ReadMap\").Types(keyType, valueType).Call(\n\t\t\t\tId(\"itr\"),\n\t\t\t\tFunc().Params(\n\t\t\t\t\tId(\"itr\").Op(\"*\").Qual(\"github.com/json-iterator/go\", \"Iterator\"),\n\t\t\t\t\tId(\"keyAsString\").String(),\n\t\t\t\t).Params(keyType, valueType).BlockFunc(func(f *Group) {\n\t\t\t\t\t// Note because _all_ keys in JSON objects are strings, we use\n\t\t\t\t\t// the etype unmarshaler to unmarshal the key to the underlying datatype\n\t\t\t\t\tf.Comment(\"Decode the map key from the JSON string to the underlying type it needs to be\")\n\t\t\t\t\tu := cb.gu.NewTypeUnmarshaller(\"keyDecoder\")\n\t\t\t\t\tf.Add(u.Init())\n\t\t\t\t\tbuiltin, ok := t.Key.(schema.BuiltinType)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tcb.errs.Addf(t.Key.ASTExpr().Pos(), \"map keys must be builtins\")\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tf.Id(\"key\").Op(\":=\").Add(u.UnmarshalBuiltin(builtin.Kind, \"keyAsString\", Id(\"keyAsString\"), true))\n\t\t\t\t\tf.If(Err().Op(\":=\").Add(u.Err()), Err().Op(\"!=\").Nil()).Block(\n\t\t\t\t\t\tPanic(Qual(\"fmt\", \"Sprintf\").Call(Lit(\"unable to decode the config: %v\"), Err())))\n\n\t\t\t\t\t// Then we can just return the key and value\n\t\t\t\t\tf.Return(Id(\"key\"), valueUnmarshaler)\n\t\t\t\t}),\n\t\t\t),\n\t\t\trtnType\n\n\tcase schema.ListType:\n\t\tunmarshaler, returnType := cb.readType(t.Elem, Qual(\"strconv\", \"Itoa\").Call(Id(\"idx\")))\n\n\t\t// We'll call a helper method in the runtime package, which requires us to pass\n\t\t// an unmarshaler for the list type and builds the return slice\n\t\treturn Qual(\"encore.dev/config\", \"ReadArray\").Types(returnType).Call(\n\t\t\t\tId(\"itr\"),\n\t\t\t\tFunc().Params(\n\t\t\t\t\tId(\"itr\").Op(\"*\").Qual(\"github.com/json-iterator/go\", \"Iterator\"),\n\t\t\t\t\tId(\"idx\").Int(),\n\t\t\t\t).Add(returnType).Block(\n\t\t\t\t\tReturn(unmarshaler),\n\t\t\t\t),\n\t\t\t),\n\t\t\tIndex().Add(returnType)\n\n\tcase schema.BuiltinType:\n\t\treturn cb.readBuiltin(t.Kind)\n\n\tcase schema.PointerType:\n\t\treader, returnType := cb.readType(t.Elem, pathElement)\n\n\t\treturn Func().Params().Op(\"*\").Add(returnType).Block(\n\t\t\tComment(\"// If the value is null, we return nil\"),\n\t\t\tIf(Id(\"itr\").Dot(\"ReadNil\").Call()).Block(\n\t\t\t\tReturn(Nil()),\n\t\t\t),\n\t\t\tLine(),\n\t\t\tComment(\"// Otherwise we unmarshal the value and return a pointer to it\"),\n\t\t\tId(\"obj\").Op(\":=\").Add(reader),\n\t\t\tReturn(Op(\"&\").Id(\"obj\")),\n\t\t).Call(), Op(\"*\").Add(returnType)\n\n\tcase schema.TypeParamRefType:\n\t\ttypeParam := t.Decl.TypeParameters()[t.Index]\n\t\tfuncName := cb.typeParamUnmarshalerName(typeParam)\n\n\t\treturn Id(funcName).Call(Id(\"itr\"), Append(Id(\"path\"), pathElement)), Id(typeParam.Name)\n\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported type for config unmarshalling: %T\", t))\n\t}\n}\n\n// readStruct generates code to read a struct from the given iterator. The code generated by this function\n// expects a zero value of the struct to be present in the variable `obj`. This code will be written into the\n// group `f` passed in as a argument.\n//\n// The returned type will be the type definition of the struct.\nfunc (cb *configUnmarshalersBuilder) readStruct(f *Group, struc schema.StructType) (returnType *Statement) {\n\tfieldTypes := make([]Code, len(struc.Fields))\n\n\tf.Id(\"itr\").Dot(\"ReadObjectCB\").Call(Func().Params(\n\t\tId(\"itr\").Op(\"*\").Qual(\"github.com/json-iterator/go\", \"Iterator\"),\n\t\tId(\"field\").String(),\n\t).Bool().Block(\n\t\tSwitch(Id(\"field\")).BlockFunc(func(f *Group) {\n\t\t\tfor i, field := range struc.Fields {\n\t\t\t\tif field.IsAnonymous() {\n\t\t\t\t\tcontinue // TODO(andre) should this be an error?\n\t\t\t\t}\n\n\t\t\t\tfieldName := field.Name.MustGet()\n\t\t\t\tjsonName := fieldName\n\t\t\t\tfor _, tag := range field.Tag.Tags() {\n\t\t\t\t\tif tag.Key == \"json\" {\n\t\t\t\t\t\tif tag.Name != \"\" {\n\t\t\t\t\t\t\tjsonName = tag.Name\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trhs, returnType := cb.readType(field.Type, Lit(jsonName))\n\t\t\t\tf.Case(Lit(jsonName)).Block(Id(\"obj\").Dot(fieldName).Op(\"=\").Add(rhs))\n\n\t\t\t\tfieldTypes[i] = Id(fieldName).Add(returnType)\n\n\t\t\t}\n\n\t\t\tf.Default().Block(Id(\"itr\").Dot(\"Skip\").Call())\n\t\t}),\n\t\tReturn(True()),\n\t))\n\n\treturn Struct(fieldTypes...)\n}\n\n// typeUnmarshalerFunc returns a `f` function which can be used to read the given value of `typ` and the type\n// that function f returns.\n//\n// The returned function will either be an inline function or an identifier for function defined in the package\n// and is expected to comply with the `config.Unmarshaler[T]` type defined in the runtime.\nfunc (cb *configUnmarshalersBuilder) typeUnmarshalerFunc(typ schema.Type) (f *Statement, returnType *Statement) {\n\tswitch t := typ.(type) {\n\tcase schema.NamedType:\n\t\t// Treat the config type as its underlying type\n\t\tif underlying, isList, isConfig := schemautil.UnwrapConfigType(cb.errs, t); isConfig {\n\t\t\tif isList {\n\t\t\t\tunderlying = schema.ListType{Elem: underlying}\n\t\t\t}\n\t\t\treturn cb.typeUnmarshalerFunc(underlying)\n\t\t}\n\n\t\tname, returnType := cb.typeUnmarshalerName(t.Decl())\n\n\t\tif len(t.TypeArgs) == 0 {\n\t\t\treturn Id(name), returnType\n\t\t} else {\n\t\t\treturnTypes := make([]Code, len(t.TypeArgs))\n\t\t\tcall := CallFunc(func(f *Group) {\n\t\t\t\tfor i, arg := range t.TypeArgs {\n\t\t\t\t\tfuncToCall, returnType := cb.typeUnmarshalerFunc(arg)\n\t\t\t\t\treturnTypes[i] = returnType\n\t\t\t\t\tf.Add(funcToCall)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tf := Id(name).Types(returnTypes...).Add(call)\n\n\t\t\treturn f, returnType.Types(returnTypes...)\n\t\t}\n\tdefault:\n\t\tunmarshaler, returnType := cb.readType(typ, nil)\n\n\t\treturn Func().Params(\n\t\t\tId(\"itr\").Op(\"*\").Qual(\"github.com/json-iterator/go\", \"Iterator\"),\n\t\t\tId(\"path\").Index().String(),\n\t\t).Params(\n\t\t\treturnType,\n\t\t).Block(\n\t\t\tReturn(unmarshaler),\n\t\t), returnType\n\n\t}\n}\n\n// typeUnmarshalerName returns a generated name for the unmarshaler function for the given type and the type that\n// the decl is.\nfunc (cb *configUnmarshalersBuilder) typeUnmarshalerName(decl *schema.TypeDecl) (reader string, rtnTyp *Statement) {\n\trtnTyp = genutil.Q(decl.Info)\n\n\t// If we've already allocated a name for this type, reuse it.\n\tkey := decl.Info.QualifiedName()\n\tif allocated, ok := cb.allocatedNames[key]; ok {\n\t\treturn allocated, rtnTyp\n\t}\n\n\t// Otherwise allocate a new name.\n\tname := fmt.Sprintf(\"encoreInternalTypeConfigUnmarshaler_%s_%s\", decl.File.Pkg.Name, decl.Name)\n\tif n := cb.seenNames[name]; n > 0 {\n\t\tname += strconv.Itoa(n + 1)\n\t}\n\tcb.seenNames[decl.Name]++\n\tcb.allocatedNames[key] = name\n\n\treturn name, rtnTyp\n}\n\n// readBuiltin returns reader code for reading the built in type from `itr` (a `*jsonitor.Iterator`) and the\n// Go type for the built in value.\nfunc (cb *configUnmarshalersBuilder) readBuiltin(builtin schema.BuiltinKind) (reader Code, rtnTyp *Statement) {\n\tswitch builtin {\n\tcase schema.Bool:\n\t\treturn Id(\"itr\").Dot(\"ReadBool\").Call(), Bool()\n\tcase schema.Int:\n\t\treturn Id(\"itr\").Dot(\"ReadInt\").Call(), Int()\n\tcase schema.Int8:\n\t\treturn Id(\"itr\").Dot(\"ReadInt8\").Call(), Int8()\n\tcase schema.Int16:\n\t\treturn Id(\"itr\").Dot(\"ReadInt16\").Call(), Int16()\n\tcase schema.Int32:\n\t\treturn Id(\"itr\").Dot(\"ReadInt32\").Call(), Int32()\n\tcase schema.Int64:\n\t\treturn Id(\"itr\").Dot(\"ReadInt64\").Call(), Int64()\n\tcase schema.Uint:\n\t\treturn Id(\"itr\").Dot(\"ReadUint\").Call(), Uint()\n\tcase schema.Uint8:\n\t\treturn Id(\"itr\").Dot(\"ReadUint8\").Call(), Uint8()\n\tcase schema.Uint16:\n\t\treturn Id(\"itr\").Dot(\"ReadUint16\").Call(), Uint16()\n\tcase schema.Uint32:\n\t\treturn Id(\"itr\").Dot(\"ReadUint32\").Call(), Uint32()\n\tcase schema.Uint64:\n\t\treturn Id(\"itr\").Dot(\"ReadUint64\").Call(), Uint64()\n\tcase schema.Float32:\n\t\treturn Id(\"itr\").Dot(\"ReadFloat32\").Call(), Float32()\n\tcase schema.Float64:\n\t\treturn Id(\"itr\").Dot(\"ReadFloat64\").Call(), Float64()\n\tcase schema.String:\n\t\treturn Id(\"itr\").Dot(\"ReadString\").Call(), String()\n\tcase schema.Bytes, schema.Time, schema.UUID, schema.JSON, schema.UserID:\n\t\tvar rtnTyp *Statement\n\t\tswitch builtin {\n\t\tcase schema.Bytes:\n\t\t\trtnTyp = Index().Byte()\n\t\tcase schema.Time:\n\t\t\trtnTyp = Qual(\"time\", \"Time\")\n\t\tcase schema.UUID:\n\t\t\trtnTyp = Qual(\"encore.dev/types/uuid\", \"UUID\")\n\t\tcase schema.JSON:\n\t\t\trtnTyp = Qual(\"encoding/json\", \"RawMessage\")\n\t\tcase schema.UserID:\n\t\t\trtnTyp = Qual(\"encore.dev/beta/auth\", \"UID\")\n\t\t}\n\n\t\treturn Func().Params().Params(Id(\"rtn\").Add(rtnTyp)).BlockFunc(func(g *Group) {\n\t\t\tu := cb.gu.NewTypeUnmarshaller(\"decoder\")\n\t\t\tg.Add(u.Init())\n\t\t\tg.Id(\"rtn\").Op(\"=\").Add(u.UnmarshalBuiltin(\n\t\t\t\tbuiltin,\n\t\t\t\t\"value\",\n\t\t\t\tId(\"itr\").Dot(\"ReadString\").Call(),\n\t\t\t\ttrue,\n\t\t\t))\n\t\t\tg.If(Err().Op(\":=\").Add(u.Err()), Err().Op(\"!=\").Nil()).Block(\n\t\t\t\tPanic(Qual(\"fmt\", \"Sprintf\").Call(Lit(\"unable to decode the config: %v\"), Err())),\n\t\t\t)\n\t\t\tg.Return()\n\t\t}).Call(), rtnTyp\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported builtin type: %v\", builtin))\n\t}\n}\n\n// typeParamUnmarshalerName generates a name for an unmarshaler function given as a argument to a generic unmarshaler\n// function.\nfunc (cb *configUnmarshalersBuilder) typeParamUnmarshalerName(param schema.DeclTypeParam) string {\n\treturn fmt.Sprintf(\"_%s_unmarshaler\", param.Name)\n}\n\n// generateConcreteUnmarshaler generates a function that unmarshals a concrete type, taking into account any\n// type arguments passed to the given type\nfunc (cb *configUnmarshalersBuilder) generateConcreteUnmarshalers(typ schema.Type) {\n\tfuncBody, _ := cb.typeUnmarshalerFunc(typ)\n\tfuncName := ConfigUnmarshalFuncName(cb.gu, typ)\n\n\tcb.unmarshalers = append(cb.unmarshalers, Id(funcName).Op(\"=\").Add(funcBody))\n}\n\n// ConfigUnmarshalFuncName returns a unique name for an unmarshal function fo a concrete\n// instance of a type. For example the following types will result in the given names:\n//\n// - `int` -> `encoreInternal_LoadConfig_int`\n// - `ConfigType` -> `encoreInternal_LoadConfig_ConfigType`\n// - `ConfigType[int, string]` -> `encoreInternal_LoadConfig_ConfigType_int_string_`\nfunc ConfigUnmarshalFuncName(gu *genutil.Helper, typ schema.Type) string {\n\ttypeAsString := gu.TypeToString(typ)\n\ttypeAsString = strings.NewReplacer(\n\t\t\"*\", \"ptr_\",\n\t\t\"[\", \"_\",\n\t\t\"]\", \"_\",\n\t\t\",\", \"_\",\n\t\t\".\", \"_\",\n\t\t\" \", \"\",\n\t\t\"\\t\", \"\",\n\t\t\"\\n\", \"\",\n\t).Replace(typeAsString)\n\n\treturn fmt.Sprintf(\"encoreInternalConfigUnmarshaler_%s\", typeAsString)\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/configgen_test.go",
    "content": "package configgen_test\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/infragen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tinfragen.Process(gen, desc)\n\t}\n\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_config.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Name        string    // The users name\n    Port        uint16\n    ReadOnly    bool      // true if we're in read only mode\n\n    // MagicNumber is complicated and requires\n    // a multi-line comment to explain it.\n    MagicNumber int\n\n    ID          uuid.UUID // An ID\n\n    PublicKey []byte\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tuuid \"encore.dev/types/uuid\"\n\t\"fmt\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Name\":\n\t\t\tobj.Name = itr.ReadString()\n\t\tcase \"Port\":\n\t\t\tobj.Port = itr.ReadUint16()\n\t\tcase \"ReadOnly\":\n\t\t\tobj.ReadOnly = itr.ReadBool()\n\t\tcase \"MagicNumber\":\n\t\t\tobj.MagicNumber = itr.ReadInt()\n\t\tcase \"ID\":\n\t\t\tobj.ID = func() (rtn uuid.UUID) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalUUID, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}()\n\t\tcase \"PublicKey\":\n\t\t\tobj.PublicKey = func() (rtn []byte) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalBytes, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\t\"encore.dev/types/uuid\"\n)\n\ntype Config struct {\n    Name        string    // The users name\n    Port        uint16\n    ReadOnly    bool      // true if we're in read only mode\n\n    // MagicNumber is complicated and requires\n    // a multi-line comment to explain it.\n    MagicNumber int\n\n    ID          uuid.UUID // An ID\n\n    PublicKey []byte\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :24:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_inline_struct.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    // The options for the HTTP server\n    HTTP struct {\n        Enabled bool // Is this option enabled?\n        Port    uint32 // What port should we run on?\n    }\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport jsoniter \"github.com/json-iterator/go\"\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"HTTP\":\n\t\t\tobj.HTTP = func() (obj struct {\n\t\t\t\tEnabled bool\n\t\t\t\tPort    uint32\n\t\t\t}) {\n\t\t\t\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\t\t\t\tswitch field {\n\t\t\t\t\tcase \"Enabled\":\n\t\t\t\t\t\tobj.Enabled = itr.ReadBool()\n\t\t\t\t\tcase \"Port\":\n\t\t\t\t\t\tobj.Port = itr.ReadUint32()\n\t\t\t\t\tdefault:\n\t\t\t\t\t\titr.Skip()\n\t\t\t\t\t}\n\t\t\t\t\treturn true\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    // The options for the HTTP server\n    HTTP struct {\n        Enabled bool // Is this option enabled?\n        Port    uint32 // What port should we run on?\n    }\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :17:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_lists.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Ages      []int32\n    OtherBits []string\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\tconfig \"encore.dev/config\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Ages\":\n\t\t\tobj.Ages = config.ReadArray[int32](itr, func(itr *jsoniter.Iterator, idx int) int32 {\n\t\t\t\treturn itr.ReadInt32()\n\t\t\t})\n\t\tcase \"OtherBits\":\n\t\t\tobj.OtherBits = config.ReadArray[string](itr, func(itr *jsoniter.Iterator, idx int) string {\n\t\t\t\treturn itr.ReadString()\n\t\t\t})\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Ages      []int32\n    OtherBits []string\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :14:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_maps.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Ages map[string]int\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tconfig \"encore.dev/config\"\n\t\"fmt\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Ages\":\n\t\t\tobj.Ages = config.ReadMap[string, int](itr, func(itr *jsoniter.Iterator, keyAsString string) (string, int) {\n\t\t\t\t// Decode the map key from the JSON string to the underlying type it needs to be\n\t\t\t\tkeyDecoder := new(__etype.Unmarshaller)\n\t\t\t\tkey := __etype.UnmarshalOne(keyDecoder, __etype.UnmarshalString, \"keyAsString\", keyAsString, true)\n\t\t\t\tif err := keyDecoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn key, itr.ReadInt()\n\t\t\t})\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Ages map[string]int\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :13:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_named_struct_multiple_uses.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\n// ServerOptions represent options for a server\ntype ServerOptions struct {\n    Enabled bool // Is this option enabled?\n    Port    uint32 // What port should we run on?\n}\n\ntype Config struct {\n    HTTP ServerOptions // The options for the HTTP server\n    TCP  ServerOptions // The options for the TCP server\n    GRPC ServerOptions // The options for the GRPC server\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport jsoniter \"github.com/json-iterator/go\"\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"HTTP\":\n\t\t\tobj.HTTP = encoreInternalTypeConfigUnmarshaler_svc_ServerOptions(itr, append(path, \"HTTP\"))\n\t\tcase \"TCP\":\n\t\t\tobj.TCP = encoreInternalTypeConfigUnmarshaler_svc_ServerOptions(itr, append(path, \"TCP\"))\n\t\tcase \"GRPC\":\n\t\t\tobj.GRPC = encoreInternalTypeConfigUnmarshaler_svc_ServerOptions(itr, append(path, \"GRPC\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_ServerOptions will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_ServerOptions(itr *jsoniter.Iterator, path []string) (obj ServerOptions) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Enabled\":\n\t\t\tobj.Enabled = itr.ReadBool()\n\t\tcase \"Port\":\n\t\t\tobj.Port = itr.ReadUint32()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\n// ServerOptions represent options for a server\ntype ServerOptions struct {\n    Enabled bool // Is this option enabled?\n    Port    uint32 // What port should we run on?\n}\n\ntype Config struct {\n    HTTP ServerOptions // The options for the HTTP server\n    TCP  ServerOptions // The options for the TCP server\n    GRPC ServerOptions // The options for the GRPC server\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :21:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_named_struct_single_use.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ServerOptions struct {\n    Enabled bool // Is this option enabled?\n    Port    uint32 // What port should we run on?\n}\n\ntype Config struct {\n    HTTP ServerOptions // The options for the HTTP server\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport jsoniter \"github.com/json-iterator/go\"\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"HTTP\":\n\t\t\tobj.HTTP = encoreInternalTypeConfigUnmarshaler_svc_ServerOptions(itr, append(path, \"HTTP\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_ServerOptions will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_ServerOptions(itr *jsoniter.Iterator, path []string) (obj ServerOptions) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Enabled\":\n\t\t\tobj.Enabled = itr.ReadBool()\n\t\tcase \"Port\":\n\t\t\tobj.Port = itr.ReadUint32()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ServerOptions struct {\n    Enabled bool // Is this option enabled?\n    Port    uint32 // What port should we run on?\n}\n\ntype Config struct {\n    HTTP ServerOptions // The options for the HTTP server\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :18:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_no_config.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_with_cue_imports.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    SomeTime time.Time\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\t\"fmt\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"time\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"SomeTime\":\n\t\t\tobj.SomeTime = func() (rtn time.Time) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalTime, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n    \"time\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    SomeTime time.Time\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :14:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/basic_wrappers.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Name        config.String    // The users name\n    Port        config.Uint16\n    ReadOnly    config.Bool      // true if we're in read only mode\n\n    // MagicNumber is complicated and requires\n    // a multi-line comment to explain it.\n    MagicNumber config.Int\n\n    Start       config.Time // The time at which the service was first started\n    ID          config.UUID // An ID\n\n    PublicKey config.Value[[]byte]\n\n    AdminUsers config.Values[string]\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tconfig \"encore.dev/config\"\n\tuuid \"encore.dev/types/uuid\"\n\t\"fmt\"\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"time\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Name\":\n\t\t\tobj.Name = config.CreateValue[string](itr.ReadString(), append(path, \"Name\"))\n\t\tcase \"Port\":\n\t\t\tobj.Port = config.CreateValue[uint16](itr.ReadUint16(), append(path, \"Port\"))\n\t\tcase \"ReadOnly\":\n\t\t\tobj.ReadOnly = config.CreateValue[bool](itr.ReadBool(), append(path, \"ReadOnly\"))\n\t\tcase \"MagicNumber\":\n\t\t\tobj.MagicNumber = config.CreateValue[int](itr.ReadInt(), append(path, \"MagicNumber\"))\n\t\tcase \"Start\":\n\t\t\tobj.Start = config.CreateValue[time.Time](func() (rtn time.Time) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalTime, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}(), append(path, \"Start\"))\n\t\tcase \"ID\":\n\t\t\tobj.ID = config.CreateValue[uuid.UUID](func() (rtn uuid.UUID) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalUUID, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}(), append(path, \"ID\"))\n\t\tcase \"PublicKey\":\n\t\t\tobj.PublicKey = config.CreateValue[[]byte](func() (rtn []byte) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalBytes, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}(), append(path, \"PublicKey\"))\n\t\tcase \"AdminUsers\":\n\t\t\tobj.AdminUsers = config.CreateValueList(config.ReadArray[string](itr, func(itr *jsoniter.Iterator, idx int) string {\n\t\t\t\treturn itr.ReadString()\n\t\t\t}), append(path, \"AdminUsers\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Name        config.String    // The users name\n    Port        config.Uint16\n    ReadOnly    config.Bool      // true if we're in read only mode\n\n    // MagicNumber is complicated and requires\n    // a multi-line comment to explain it.\n    MagicNumber config.Int\n\n    Start       config.Time // The time at which the service was first started\n    ID          config.UUID // An ID\n\n    PublicKey config.Value[[]byte]\n\n    AdminUsers config.Values[string]\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :26:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/cue_optional_tag.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ServerOption struct {\n    Option   int64\n    Disabled bool `cue:\",opt\"` // True if this is disabled\n}\n\ntype Config struct {\n    HTTP    ServerOption\n    Another ServerOption\n    TCP     ServerOption `cue:\",opt\"`\n    GRPC    ServerOption `cue:\",opt\"`\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport jsoniter \"github.com/json-iterator/go\"\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"HTTP\":\n\t\t\tobj.HTTP = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"HTTP\"))\n\t\tcase \"Another\":\n\t\t\tobj.Another = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"Another\"))\n\t\tcase \"TCP\":\n\t\t\tobj.TCP = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"TCP\"))\n\t\tcase \"GRPC\":\n\t\t\tobj.GRPC = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"GRPC\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_ServerOption will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr *jsoniter.Iterator, path []string) (obj ServerOption) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Option\":\n\t\t\tobj.Option = itr.ReadInt64()\n\t\tcase \"Disabled\":\n\t\t\tobj.Disabled = itr.ReadBool()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ServerOption struct {\n    Option   int64\n    Disabled bool `cue:\",opt\"` // True if this is disabled\n}\n\ntype Config struct {\n    HTTP    ServerOption\n    Another ServerOption\n    TCP     ServerOption `cue:\",opt\"`\n    GRPC    ServerOption `cue:\",opt\"`\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :21:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/cue_tags.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    A int\n    B int `cue:\"A+C\"`\n    C int `cue:\"B-A\"`\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport jsoniter \"github.com/json-iterator/go\"\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"A\":\n\t\t\tobj.A = itr.ReadInt()\n\t\tcase \"B\":\n\t\t\tobj.B = itr.ReadInt()\n\t\tcase \"C\":\n\t\t\tobj.C = itr.ReadInt()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    A int\n    B int `cue:\"A+C\"`\n    C int `cue:\"B-A\"`\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :15:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/generics.txt",
    "content": "-- generics.go --\npackage generics\n\nimport (\n    \"context\"\n    \"encore.dev/config\"\n)\n\ntype CfgType[T any] struct {\n\tReadOnlyMode config.Bool\n\tPublicKey    config.Bytes\n\tAdminUsers   config.Values[string]\n\n\tSubConfig config.Value[struct {\n\t\tSubKey *SubCfgType[T]\n\t}]\n\n\tCurrencies map[string]struct {\n\t\tName    config.String\n\t\tCode    config.String\n\t\tAliases config.Values[string]\n\t}\n\n\tAnotherList config.Values[struct {\n\t\tName config.String\n\t}]\n}\n\ntype SubCfgType[T any] struct {\n\tMaxCount T\n}\n\nvar cfg = config.Load[*CfgType[uint]]()\n\ntype ConfigResponse struct {\n\tReadOnlyMode bool\n\tPublicKey    []byte\n\tSubKeyCount  uint\n\tAdminUsers   []string\n}\n\n// There must be an API endopint to use config.Load.\n//encore:api public\nfunc Dummy(context.Context) error { return nil }\n-- want:encore_internal__config_unmarshal.go --\npackage generics\n\nimport (\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tconfig \"encore.dev/config\"\n\t\"fmt\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_examplecom_CfgType_uint_ = func(itr *jsoniter.Iterator, path []string) *CfgType[uint] {\n\t\treturn func() *CfgType[uint] {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_generics_CfgType[uint](func(itr *jsoniter.Iterator, path []string) uint {\n\t\t\t\treturn itr.ReadUint()\n\t\t\t})(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_generics_CfgType will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_generics_CfgType[T any](_T_unmarshaler config.Unmarshaler[T]) (concreteUnmarshaler config.Unmarshaler[CfgType[T]]) {\n\treturn func(itr *jsoniter.Iterator, path []string) (obj CfgType[T]) {\n\t\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\t\tswitch field {\n\t\t\tcase \"ReadOnlyMode\":\n\t\t\t\tobj.ReadOnlyMode = config.CreateValue[bool](itr.ReadBool(), append(path, \"ReadOnlyMode\"))\n\t\t\tcase \"PublicKey\":\n\t\t\t\tobj.PublicKey = config.CreateValue[[]byte](func() (rtn []byte) {\n\t\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalBytes, \"value\", itr.ReadString(), true)\n\t\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}(), append(path, \"PublicKey\"))\n\t\t\tcase \"AdminUsers\":\n\t\t\t\tobj.AdminUsers = config.CreateValueList(config.ReadArray[string](itr, func(itr *jsoniter.Iterator, idx int) string {\n\t\t\t\t\treturn itr.ReadString()\n\t\t\t\t}), append(path, \"AdminUsers\"))\n\t\t\tcase \"SubConfig\":\n\t\t\t\tobj.SubConfig = config.CreateValue[struct {\n\t\t\t\t\tSubKey *SubCfgType[T]\n\t\t\t\t}](func() (obj struct {\n\t\t\t\t\tSubKey *SubCfgType[T]\n\t\t\t\t}) {\n\t\t\t\t\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\t\t\t\t\tswitch field {\n\t\t\t\t\t\tcase \"SubKey\":\n\t\t\t\t\t\t\tobj.SubKey = func() *SubCfgType[T] {\n\t\t\t\t\t\t\t\t// If the value is null, we return nil\n\t\t\t\t\t\t\t\tif itr.ReadNil() {\n\t\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\t\t\t\t\t\tobj := encoreInternalTypeConfigUnmarshaler_generics_SubCfgType[T](func(itr *jsoniter.Iterator, path []string) T {\n\t\t\t\t\t\t\t\t\treturn _T_unmarshaler(itr, append(path))\n\t\t\t\t\t\t\t\t})(itr, append(path, \"SubKey\"))\n\t\t\t\t\t\t\t\treturn &obj\n\t\t\t\t\t\t\t}()\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\titr.Skip()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}(), append(path, \"SubConfig\"))\n\t\t\tcase \"Currencies\":\n\t\t\t\tobj.Currencies = config.ReadMap[string, struct {\n\t\t\t\t\tName    config.Value[string]\n\t\t\t\t\tCode    config.Value[string]\n\t\t\t\t\tAliases config.Values[string]\n\t\t\t\t}](itr, func(itr *jsoniter.Iterator, keyAsString string) (string, struct {\n\t\t\t\t\tName    config.Value[string]\n\t\t\t\t\tCode    config.Value[string]\n\t\t\t\t\tAliases config.Values[string]\n\t\t\t\t}) {\n\t\t\t\t\t// Decode the map key from the JSON string to the underlying type it needs to be\n\t\t\t\t\tkeyDecoder := new(__etype.Unmarshaller)\n\t\t\t\t\tkey := __etype.UnmarshalOne(keyDecoder, __etype.UnmarshalString, \"keyAsString\", keyAsString, true)\n\t\t\t\t\tif err := keyDecoder.Error; err != nil {\n\t\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t\t}\n\t\t\t\t\treturn key, func() (obj struct {\n\t\t\t\t\t\tName    config.Value[string]\n\t\t\t\t\t\tCode    config.Value[string]\n\t\t\t\t\t\tAliases config.Values[string]\n\t\t\t\t\t}) {\n\t\t\t\t\t\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\t\t\t\t\t\tswitch field {\n\t\t\t\t\t\t\tcase \"Name\":\n\t\t\t\t\t\t\t\tobj.Name = config.CreateValue[string](itr.ReadString(), append(path, \"Name\"))\n\t\t\t\t\t\t\tcase \"Code\":\n\t\t\t\t\t\t\t\tobj.Code = config.CreateValue[string](itr.ReadString(), append(path, \"Code\"))\n\t\t\t\t\t\t\tcase \"Aliases\":\n\t\t\t\t\t\t\t\tobj.Aliases = config.CreateValueList(config.ReadArray[string](itr, func(itr *jsoniter.Iterator, idx int) string {\n\t\t\t\t\t\t\t\t\treturn itr.ReadString()\n\t\t\t\t\t\t\t\t}), append(path, \"Aliases\"))\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\titr.Skip()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn\n\t\t\t\t\t}()\n\t\t\t\t})\n\t\t\tcase \"AnotherList\":\n\t\t\t\tobj.AnotherList = config.CreateValueList(config.ReadArray[struct {\n\t\t\t\t\tName config.Value[string]\n\t\t\t\t}](itr, func(itr *jsoniter.Iterator, idx int) struct {\n\t\t\t\t\tName config.Value[string]\n\t\t\t\t} {\n\t\t\t\t\treturn func() (obj struct {\n\t\t\t\t\t\tName config.Value[string]\n\t\t\t\t\t}) {\n\t\t\t\t\t\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\t\t\t\t\t\tswitch field {\n\t\t\t\t\t\t\tcase \"Name\":\n\t\t\t\t\t\t\t\tobj.Name = config.CreateValue[string](itr.ReadString(), append(path, \"Name\"))\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\titr.Skip()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn\n\t\t\t\t\t}()\n\t\t\t\t}), append(path, \"AnotherList\"))\n\t\t\tdefault:\n\t\t\t\titr.Skip()\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\treturn\n\t}\n}\n\n// encoreInternalTypeConfigUnmarshaler_generics_SubCfgType will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_generics_SubCfgType[T any](_T_unmarshaler config.Unmarshaler[T]) (concreteUnmarshaler config.Unmarshaler[SubCfgType[T]]) {\n\treturn func(itr *jsoniter.Iterator, path []string) (obj SubCfgType[T]) {\n\t\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\t\tswitch field {\n\t\t\tcase \"MaxCount\":\n\t\t\t\tobj.MaxCount = _T_unmarshaler(itr, append(path, \"MaxCount\"))\n\t\t\tdefault:\n\t\t\t\titr.Skip()\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\treturn\n\t}\n}\n-- want:generics.go --\npackage generics\n\nimport (\n    \"context\"\n    \"encore.dev/config\"\n)\n\ntype CfgType[T any] struct {\n\tReadOnlyMode config.Bool\n\tPublicKey    config.Bytes\n\tAdminUsers   config.Values[string]\n\n\tSubConfig config.Value[struct {\n\t\tSubKey *SubCfgType[T]\n\t}]\n\n\tCurrencies map[string]struct {\n\t\tName    config.String\n\t\tCode    config.String\n\t\tAliases config.Values[string]\n\t}\n\n\tAnotherList config.Values[struct {\n\t\tName config.String\n\t}]\n}\n\ntype SubCfgType[T any] struct {\n\tMaxCount T\n}\n\nvar cfg = config.Load[*CfgType[uint]](\"generics\", encoreInternalConfigUnmarshaler_ptr_examplecom_CfgType_uint_/*line :32:39*/)\n\ntype ConfigResponse struct {\n\tReadOnlyMode bool\n\tPublicKey    []byte\n\tSubKeyCount  uint\n\tAdminUsers   []string\n}\n\n// There must be an API endopint to use config.Load.\n//encore:api public\nfunc Dummy(context.Context) error { return nil }\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/json_tags.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ServerOption struct {\n    Option   int64\n    Disabled bool `json:\",omitempty\"` // True if this is disabled\n}\n\ntype Config struct {\n    HTTP    ServerOption\n    Another ServerOption `json:\"a_n_o_t_h_e_r\"`\n    TCP     ServerOption `json:\",omitempty\"`\n    GRPC    ServerOption `json:\",omitempty\"`\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport jsoniter \"github.com/json-iterator/go\"\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"HTTP\":\n\t\t\tobj.HTTP = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"HTTP\"))\n\t\tcase \"a_n_o_t_h_e_r\":\n\t\t\tobj.Another = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"a_n_o_t_h_e_r\"))\n\t\tcase \"TCP\":\n\t\t\tobj.TCP = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"TCP\"))\n\t\tcase \"GRPC\":\n\t\t\tobj.GRPC = encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr, append(path, \"GRPC\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_ServerOption will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_ServerOption(itr *jsoniter.Iterator, path []string) (obj ServerOption) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Option\":\n\t\t\tobj.Option = itr.ReadInt64()\n\t\tcase \"Disabled\":\n\t\t\tobj.Disabled = itr.ReadBool()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype ServerOption struct {\n    Option   int64\n    Disabled bool `json:\",omitempty\"` // True if this is disabled\n}\n\ntype Config struct {\n    HTTP    ServerOption\n    Another ServerOption `json:\"a_n_o_t_h_e_r\"`\n    TCP     ServerOption `json:\",omitempty\"`\n    GRPC    ServerOption `json:\",omitempty\"`\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :21:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/merge_identical_comments.txt",
    "content": "-- svc/a.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype AsConfig struct {\n    // Multiline test\n    // comment to deduplicate.\n    Foo config.String\n}\n\nvar _ = config.Load[*AsConfig]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- svc/b.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype BsConfig struct{\n    Foo config.String // Some extra comment\n}\n\nvar _ = config.Load[BsConfig]()\n\n-- svc/c.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype CsConfig struct{\n    // Multiline test\n    // comment to deduplicate.\n    Foo config.String\n}\n\nvar _ = config.Load[CsConfig]()\n\n-- svc/d.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype DsConfig struct{\n    Foo config.String // Some extra comment\n}\n\nvar _ = config.Load[DsConfig]()\n-- want:svc/a.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype AsConfig struct {\n    // Multiline test\n    // comment to deduplicate.\n    Foo config.String\n}\n\nvar _ = config.Load[*AsConfig](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_AsConfig/*line :15:32*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- want:svc/b.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype BsConfig struct{\n    Foo config.String // Some extra comment\n}\n\nvar _ = config.Load[BsConfig](\"svc\", encoreInternalConfigUnmarshaler_svc_BsConfig/*line :11:31*/)\n\n-- want:svc/c.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype CsConfig struct{\n    // Multiline test\n    // comment to deduplicate.\n    Foo config.String\n}\n\nvar _ = config.Load[CsConfig](\"svc\", encoreInternalConfigUnmarshaler_svc_CsConfig/*line :13:31*/)\n\n-- want:svc/d.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype DsConfig struct{\n    Foo config.String // Some extra comment\n}\n\nvar _ = config.Load[DsConfig](\"svc\", encoreInternalConfigUnmarshaler_svc_DsConfig/*line :11:31*/)\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\tconfig \"encore.dev/config\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_AsConfig = func(itr *jsoniter.Iterator, path []string) *AsConfig {\n\t\treturn func() *AsConfig {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_AsConfig(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n\tencoreInternalConfigUnmarshaler_svc_BsConfig = encoreInternalTypeConfigUnmarshaler_svc_BsConfig\n\tencoreInternalConfigUnmarshaler_svc_CsConfig = encoreInternalTypeConfigUnmarshaler_svc_CsConfig\n\tencoreInternalConfigUnmarshaler_svc_DsConfig = encoreInternalTypeConfigUnmarshaler_svc_DsConfig\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_AsConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_AsConfig(itr *jsoniter.Iterator, path []string) (obj AsConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_BsConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_BsConfig(itr *jsoniter.Iterator, path []string) (obj BsConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_CsConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_CsConfig(itr *jsoniter.Iterator, path []string) (obj CsConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_DsConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_DsConfig(itr *jsoniter.Iterator, path []string) (obj DsConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/multi_package.txt",
    "content": "-- pkg/temporal/temporal.go --\npackage temporal\ntype ClientOptions struct {\n\tHostPort  string\n\tNamespace string\n}\n\n-- helloworld.go --\npackage helloworld\n\nimport (\n    \"context\"\n    \"encore.dev/config\"\n\n    \"example.com/pkg/temporal\"\n)\n\ntype Config struct {\n\tTemporal config.Value[temporal.ClientOptions]\n}\n\nvar cfg = config.Load[Config]()\n\n// There must be an API endpoint to use config.Load.\n//encore:api\nfunc Dummy(context.Context) error { return nil }\n-- want:encore_internal__config_unmarshal.go --\npackage helloworld\n\nimport (\n\tconfig \"encore.dev/config\"\n\ttemporal \"example.com/pkg/temporal\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_examplecom_Config = encoreInternalTypeConfigUnmarshaler_helloworld_Config\n)\n\n// encoreInternalTypeConfigUnmarshaler_helloworld_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_helloworld_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Temporal\":\n\t\t\tobj.Temporal = config.CreateValue[temporal.ClientOptions](encoreInternalTypeConfigUnmarshaler_temporal_ClientOptions(itr, append(path, \"Temporal\")), append(path, \"Temporal\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_temporal_ClientOptions will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_temporal_ClientOptions(itr *jsoniter.Iterator, path []string) (obj temporal.ClientOptions) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"HostPort\":\n\t\t\tobj.HostPort = itr.ReadString()\n\t\tcase \"Namespace\":\n\t\t\tobj.Namespace = itr.ReadString()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:helloworld.go --\npackage helloworld\n\nimport (\n    \"context\"\n    \"encore.dev/config\"\n\n    \"example.com/pkg/temporal\"\n)\n\ntype Config struct {\n\tTemporal config.Value[temporal.ClientOptions]\n}\n\nvar cfg = config.Load[Config](\"helloworld\", encoreInternalConfigUnmarshaler_examplecom_Config/*line :14:31*/)\n\n// There must be an API endpoint to use config.Load.\n//encore:api\nfunc Dummy(context.Context) error { return nil }\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/multiple_configs_in_service.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Name        string    // The users name\n    Port        uint16\n    ReadOnly    bool      // true if we're in read only mode\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- svc/a_file.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype OtherConfig struct{\n    Foo config.String // Foo is great in Otherconfig\n    Bar config.Int\n}\n\nvar _ = config.Load[OtherConfig]()\n\n-- svc/z_file.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype ThisConfig struct{\n    Foo config.String // And even better in ThisConfig\n    Baz config.Bool\n}\n\nvar _ = config.Load[ThisConfig]()\n-- want:svc/a_file.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype OtherConfig struct{\n    Foo config.String // Foo is great in Otherconfig\n    Bar config.Int\n}\n\nvar _ = config.Load[OtherConfig](\"svc\", encoreInternalConfigUnmarshaler_svc_OtherConfig/*line :12:34*/)\n\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\tconfig \"encore.dev/config\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_svc_OtherConfig = encoreInternalTypeConfigUnmarshaler_svc_OtherConfig\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config  = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n\tencoreInternalConfigUnmarshaler_svc_ThisConfig = encoreInternalTypeConfigUnmarshaler_svc_ThisConfig\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Name\":\n\t\t\tobj.Name = itr.ReadString()\n\t\tcase \"Port\":\n\t\t\tobj.Port = itr.ReadUint16()\n\t\tcase \"ReadOnly\":\n\t\t\tobj.ReadOnly = itr.ReadBool()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_OtherConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_OtherConfig(itr *jsoniter.Iterator, path []string) (obj OtherConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tcase \"Bar\":\n\t\t\tobj.Bar = config.CreateValue[int](itr.ReadInt(), append(path, \"Bar\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_svc_ThisConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_ThisConfig(itr *jsoniter.Iterator, path []string) (obj ThisConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tcase \"Baz\":\n\t\t\tobj.Baz = config.CreateValue[bool](itr.ReadBool(), append(path, \"Baz\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n)\n\ntype Config struct {\n    Name        string    // The users name\n    Port        uint16\n    ReadOnly    bool      // true if we're in read only mode\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :15:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- want:svc/z_file.go --\npackage svc\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype ThisConfig struct{\n    Foo config.String // And even better in ThisConfig\n    Baz config.Bool\n}\n\nvar _ = config.Load[ThisConfig](\"svc\", encoreInternalConfigUnmarshaler_svc_ThisConfig/*line :12:33*/)\n"
  },
  {
    "path": "v2/codegen/infragen/configgen/testdata/name_conflicts.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\n\t\"example.com/svc/utils\"\n\t\"example.com/svc/helpers\"\n)\n\ntype Config struct {\n    A helpers.ExtraConfig\n    B utils.ExtraConfig\n    C helpers.ExtraConfig\n    D utils.ExtraConfig\n    E helpers.SingleUse\n}\n\nvar _ = config.Load[*Config]()\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n-- svc/utils/config.go --\npackage utils\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype ExtraConfig struct{\n    Foo config.String\n    Bar config.Int\n}\n\n-- svc/helpers/config.go --\npackage helpers\n\nimport (\n\t\"encore.dev/config\"\n)\n\ntype ExtraConfig struct{\n    Foo config.String\n    Baz config.Value[[]byte]\n}\n\ntype SingleUse struct {\n    Lock bool\n}\n-- want:svc/encore_internal__config_unmarshal.go --\npackage svc\n\nimport (\n\t__etype \"encore.dev/appruntime/shared/etype\"\n\tconfig \"encore.dev/config\"\n\thelpers \"example.com/svc/helpers\"\n\tutils \"example.com/svc/utils\"\n\t\"fmt\"\n\tjsoniter \"github.com/json-iterator/go\"\n)\n\n/*\nThese functions are automatically generated and maintained by Encore to allow config values\nto be unmarshalled into the correct types. They are not intended to be used directly. They\nare automatically updated by Encore whenever you change the data types used within your\ncalls to config.Load[T]().\n*/\n\n// Concrete unmarshalers for all config.Load calls, including those using generic types.\n// These instances are used directly by calls to `config.Load[T]()`.\nvar (\n\tencoreInternalConfigUnmarshaler_ptr_svc_Config = func(itr *jsoniter.Iterator, path []string) *Config {\n\t\treturn func() *Config {\n\t\t\t// If the value is null, we return nil\n\t\t\tif itr.ReadNil() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Otherwise we unmarshal the value and return a pointer to it\n\t\t\tobj := encoreInternalTypeConfigUnmarshaler_svc_Config(itr, append(path))\n\t\t\treturn &obj\n\t\t}()\n\t}\n)\n\n// encoreInternalTypeConfigUnmarshaler_svc_Config will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_svc_Config(itr *jsoniter.Iterator, path []string) (obj Config) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"A\":\n\t\t\tobj.A = encoreInternalTypeConfigUnmarshaler_helpers_ExtraConfig(itr, append(path, \"A\"))\n\t\tcase \"B\":\n\t\t\tobj.B = encoreInternalTypeConfigUnmarshaler_utils_ExtraConfig(itr, append(path, \"B\"))\n\t\tcase \"C\":\n\t\t\tobj.C = encoreInternalTypeConfigUnmarshaler_helpers_ExtraConfig(itr, append(path, \"C\"))\n\t\tcase \"D\":\n\t\t\tobj.D = encoreInternalTypeConfigUnmarshaler_utils_ExtraConfig(itr, append(path, \"D\"))\n\t\tcase \"E\":\n\t\t\tobj.E = encoreInternalTypeConfigUnmarshaler_helpers_SingleUse(itr, append(path, \"E\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_helpers_ExtraConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_helpers_ExtraConfig(itr *jsoniter.Iterator, path []string) (obj helpers.ExtraConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tcase \"Baz\":\n\t\t\tobj.Baz = config.CreateValue[[]byte](func() (rtn []byte) {\n\t\t\t\tdecoder := new(__etype.Unmarshaller)\n\t\t\t\trtn = __etype.UnmarshalOne(decoder, __etype.UnmarshalBytes, \"value\", itr.ReadString(), true)\n\t\t\t\tif err := decoder.Error; err != nil {\n\t\t\t\t\tpanic(fmt.Sprintf(\"unable to decode the config: %v\", err))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}(), append(path, \"Baz\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_helpers_SingleUse will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_helpers_SingleUse(itr *jsoniter.Iterator, path []string) (obj helpers.SingleUse) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Lock\":\n\t\t\tobj.Lock = itr.ReadBool()\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n\n// encoreInternalTypeConfigUnmarshaler_utils_ExtraConfig will unmarshal the JSON representation into the given type, taking account for\n// the `config.Value` dynamic functions.\nfunc encoreInternalTypeConfigUnmarshaler_utils_ExtraConfig(itr *jsoniter.Iterator, path []string) (obj utils.ExtraConfig) {\n\titr.ReadObjectCB(func(itr *jsoniter.Iterator, field string) bool {\n\t\tswitch field {\n\t\tcase \"Foo\":\n\t\t\tobj.Foo = config.CreateValue[string](itr.ReadString(), append(path, \"Foo\"))\n\t\tcase \"Bar\":\n\t\t\tobj.Bar = config.CreateValue[int](itr.ReadInt(), append(path, \"Bar\"))\n\t\tdefault:\n\t\t\titr.Skip()\n\t\t}\n\t\treturn true\n\t})\n\treturn\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\n\t\"encore.dev/config\"\n\n\t\"example.com/svc/utils\"\n\t\"example.com/svc/helpers\"\n)\n\ntype Config struct {\n    A helpers.ExtraConfig\n    B utils.ExtraConfig\n    C helpers.ExtraConfig\n    D utils.ExtraConfig\n    E helpers.SingleUse\n}\n\nvar _ = config.Load[*Config](\"svc\", encoreInternalConfigUnmarshaler_ptr_svc_Config/*line :20:30*/)\n\n//encore:api\nfunc MyAPI(ctx context.Context) (error) {\n\treturn nil\n}\n\n"
  },
  {
    "path": "v2/codegen/infragen/infragen.go",
    "content": "package infragen\n\nimport (\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/infragen/cachegen\"\n\t\"encr.dev/v2/codegen/infragen/configgen\"\n\t\"encr.dev/v2/codegen/infragen/metricsgen\"\n\t\"encr.dev/v2/codegen/infragen/pubsubgen\"\n\t\"encr.dev/v2/codegen/infragen/secretsgen\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/infra/caches\"\n\t\"encr.dev/v2/parser/infra/config\"\n\t\"encr.dev/v2/parser/infra/metrics\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/infra/secrets\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\nfunc Process(gg *codegen.Generator, appDesc *app.Desc) {\n\ttype groupKey struct {\n\t\tpkg  paths.Pkg\n\t\tkind resource.Kind\n\t}\n\n\tgroups := make(map[groupKey][]resource.Resource)\n\tpkgMap := make(map[paths.Pkg]*pkginfo.Package)\n\tfor _, r := range appDesc.Parse.Resources() {\n\t\t// Group by package.\n\t\tvar pkg *pkginfo.Package\n\t\tswitch r := r.(type) {\n\t\tcase *caches.Keyspace:\n\t\t\tpkg = r.File.Pkg\n\t\tcase *metrics.Metric:\n\t\t\tpkg = r.File.Pkg\n\t\tcase *secrets.Secrets:\n\t\t\tpkg = r.File.Pkg\n\t\tcase *pubsub.Subscription:\n\t\t\tpkg = r.File.Pkg\n\t\tcase *config.Load:\n\t\t\tpkg = r.File.Pkg\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tkey := groupKey{pkg: pkg.ImportPath, kind: r.Kind()}\n\t\tgroups[key] = append(groups[key], r)\n\t\tpkgMap[pkg.ImportPath] = pkg\n\t}\n\n\tfor key, resources := range groups {\n\t\tpkg := pkgMap[key.pkg]\n\t\tswitch key.kind {\n\t\tcase resource.CacheKeyspace:\n\t\t\tcachegen.GenKeyspace(gg, pkg, fns.Map(resources, func(r resource.Resource) *caches.Keyspace {\n\t\t\t\treturn r.(*caches.Keyspace)\n\t\t\t}))\n\t\tcase resource.Metric:\n\t\t\tmetricsgen.Gen(gg, pkg, fns.Map(resources, func(r resource.Resource) *metrics.Metric {\n\t\t\t\treturn r.(*metrics.Metric)\n\t\t\t}))\n\t\tcase resource.PubSubSubscription:\n\t\t\tpubsubgen.Gen(gg, pkg, appDesc, fns.Map(resources, func(r resource.Resource) *pubsub.Subscription {\n\t\t\t\treturn r.(*pubsub.Subscription)\n\t\t\t}))\n\t\tcase resource.Secrets:\n\t\t\tsvc, _ := appDesc.ServiceForPath(pkg.FSPath)\n\t\t\tsecretsgen.Gen(gg, option.AsOptional(svc), pkg, fns.Map(resources, func(r resource.Resource) *secrets.Secrets {\n\t\t\t\treturn r.(*secrets.Secrets)\n\t\t\t}))\n\t\tcase resource.ConfigLoad:\n\t\t\tsvc, ok := appDesc.ServiceForPath(pkg.FSPath)\n\t\t\tif !ok {\n\t\t\t\tgg.Errs.Addf(resources[0].(*config.Load).AST.Pos(), \"config loads must be declared in a service package\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconfiggen.Gen(gg, svc, pkg, fns.Map(resources, func(r resource.Resource) *config.Load {\n\t\t\t\treturn r.(*config.Load)\n\t\t\t}))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/codegen/infragen/metricsgen/metricsgen.go",
    "content": "package metricsgen\n\nimport (\n\t\"fmt\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/metrics\"\n)\n\nfunc Gen(gen *codegen.Generator, pkg *pkginfo.Package, metrics []*metrics.Metric) {\n\tf := gen.File(pkg, \"metrics\")\n\tfor _, m := range metrics {\n\t\tgenLabelMapper(gen, f, m)\n\t}\n}\n\nfunc genLabelMapper(gen *codegen.Generator, f *codegen.File, m *metrics.Metric) {\n\t// If there is no label type there's nothing to do.\n\tif m.LabelType.Empty() {\n\t\treturn\n\t}\n\tlabelType := m.LabelType.MustGet()\n\n\tdeclRef, ok := schemautil.ResolveNamedStruct(labelType, false)\n\tif !ok {\n\t\tgen.Errs.AddPos(labelType.ASTExpr().Pos(), \"invalid metric label type: must be a named struct\")\n\t\treturn\n\t} else if declRef.Pointers > 0 {\n\t\tgen.Errs.AddPos(labelType.ASTExpr().Pos(), \"invalid metric label type: must not be a pointer type\")\n\t\treturn\n\t}\n\tconcrete := schemautil.ConcretizeWithTypeArgs(gen.Errs, declRef.Decl.Type, declRef.TypeArgs).(schema.StructType)\n\n\tmapper := f.FuncDecl(\"labelMapper\", m.Name)\n\n\tconst input = \"in\"\n\tmapper.Params(Id(input).Add(gen.Util.Type(labelType)))\n\tmapper.Results(Index().Qual(\"encore.dev/metrics\", \"KeyValue\"))\n\n\tmapper.Body(\n\t\tReturn(Index().Qual(\"encore.dev/metrics\", \"KeyValue\").ValuesFunc(func(g *Group) {\n\t\t\tfor _, f := range concrete.Fields {\n\t\t\t\tif f.IsAnonymous() {\n\t\t\t\t\tgen.Errs.AddPos(f.AST.Pos(), \"anonymous fields are not supported in metric labels\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tkey := idents.Convert(f.Name.MustGet(), idents.SnakeCase)\n\t\t\t\tg.Add(Values(Dict{\n\t\t\t\t\tId(\"Key\"):   Lit(key),\n\t\t\t\t\tId(\"Value\"): fieldToString(gen.Errs, f, Id(input).Dot(f.Name.MustGet())),\n\t\t\t\t}))\n\t\t\t}\n\t\t})),\n\t)\n\n\t// Insert the label mapper configuration into the metrics config literal.\n\tsnippet := fmt.Sprintf(\"EncoreInternal_LabelMapper: %s,\", mapper.Name())\n\tgen.Rewrite(m.File).Insert(m.ConfigLiteral.Lbrace+1, []byte(snippet))\n}\n\n// fieldToString returns the code to convert the given value of the given builtin type to a string.\n// If the field is not a valid builtin or is anonymous, it reports an error.\nfunc fieldToString(errs *perr.List, field schema.StructField, val Code) Code {\n\tfieldName := field.Name.MustGet()\n\n\ttyp, ok := field.Type.(schema.BuiltinType)\n\tif !ok {\n\t\terrs.Addf(field.AST.Pos(), \"invalid metric label field %s: must be string, bool, or integer type\",\n\t\t\tfieldName)\n\t\treturn Null()\n\t}\n\n\tkind := typ.Kind\n\tswitch true {\n\tcase kind == schema.String:\n\t\treturn val\n\tcase kind == schema.Bool:\n\t\treturn Qual(\"strconv\", \"FormatBool\").Call(val)\n\n\tcase schemautil.IsBuiltinKind(typ, schemautil.Signed...):\n\t\tcast := kind != schema.Int64\n\t\tif cast {\n\t\t\tval = Int64().Call(val)\n\t\t}\n\t\treturn Qual(\"strconv\", \"FormatInt\").Call(val, Lit(10))\n\n\tcase schemautil.IsBuiltinKind(typ, schemautil.Unsigned...):\n\t\tcast := kind != schema.Uint64\n\t\tif cast {\n\t\t\tval = Uint64().Call(val)\n\t\t}\n\t\treturn Qual(\"strconv\", \"FormatUint\").Call(val, Lit(10))\n\n\tdefault:\n\t\terrs.Addf(field.AST.Pos(), \"invalid metric label field %s: must be string, bool, or integer type\",\n\t\t\tfieldName)\n\t\treturn Null()\n\t}\n}\n"
  },
  {
    "path": "v2/codegen/infragen/pubsubgen/pubsubgen.go",
    "content": "package pubsubgen\n\nimport (\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\nfunc Gen(gen *codegen.Generator, pkg *pkginfo.Package, appDesc *app.Desc, subs []*pubsub.Subscription) {\n\tvar file *codegen.File // created on first use\n\tfor _, sub := range subs {\n\t\tif method, ok := sub.MethodHandler.Get(); ok {\n\t\t\tif file == nil {\n\t\t\t\tfile = gen.File(pkg, \"pubsub\")\n\t\t\t}\n\t\t\trewriteMethodHandler(gen, file, appDesc, sub, method)\n\t\t}\n\t}\n}\n\nfunc rewriteMethodHandler(gen *codegen.Generator, f *codegen.File, appDesc *app.Desc, sub *pubsub.Subscription, method pubsub.MethodHandler) {\n\tres, ok := appDesc.Parse.ResourceForQN(sub.Topic).Get()\n\tif !ok || res.Kind() != resource.PubSubTopic {\n\t\tgen.Errs.Add(\n\t\t\tpubsub.ErrSubscriptionTopicNotResource.AtGoNode(sub.AST.Args[0]),\n\t\t)\n\t\treturn\n\t}\n\ttopic := res.(*pubsub.Topic)\n\n\tsvc, ok := appDesc.ServiceForPath(sub.File.FSPath)\n\tif !ok {\n\t\tgen.Errs.Add(\n\t\t\tpubsub.ErrInvalidMethodHandler.AtGoNode(sub.Handler),\n\t\t)\n\t\treturn\n\t}\n\n\thandler := f.FuncDecl(\"handler\", strings.ReplaceAll(sub.Name, \"-\", \"_\"))\n\n\thandler.Params(Id(\"ctx\").Qual(\"context\", \"Context\"), Id(\"msg\").Add(gen.Util.Type(topic.MessageType.ToType())))\n\thandler.Results(Error())\n\n\thandler.BodyFunc(func(g *Group) {\n\t\t// svc, err := service.Get[*SvcStruct]()\n\t\tg.List(Id(\"svc\"), Err()).Op(\":=\").Qual(\"encore.dev/appruntime/apisdk/service\", \"Get\").Types(\n\t\t\tOp(\"*\").Id(method.Decl.Name),\n\t\t).Call(Lit(svc.Name))\n\t\t// if err != nil { return err }\n\t\tg.If(Err().Op(\"!=\").Nil()).Block(\n\t\t\tReturn(Err()),\n\t\t)\n\t\t// return svc.Method(ctx, msg)\n\t\tg.Return(Id(\"svc\").Dot(method.Method).Call(Id(\"ctx\"), Id(\"msg\")))\n\t})\n\n\tgen.Rewrite(sub.File).Replace(sub.Handler.Pos(), sub.Handler.End(), []byte(handler.Name()))\n}\n"
  },
  {
    "path": "v2/codegen/infragen/pubsubgen/pubsubgen_test.go",
    "content": "package pubsubgen_test\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/infragen\"\n\t\"encr.dev/v2/codegen/internal/codegentest\"\n)\n\nfunc TestCodegen(t *testing.T) {\n\tfn := func(gen *codegen.Generator, desc *app.Desc) {\n\t\tinfragen.Process(gen, desc)\n\t}\n\n\tcodegentest.Run(t, fn)\n}\n"
  },
  {
    "path": "v2/codegen/infragen/pubsubgen/testdata/basic.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/pubsub\"\n)\n\ntype Event struct {}\n\nvar Topic = pubsub.NewTopic[*Event](\"topic\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar _ = pubsub.NewSubscription(Topic, \"subscription\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: func(ctx context.Context, event *Event) error {\n            return nil\n        },\n    },\n)\n"
  },
  {
    "path": "v2/codegen/infragen/pubsubgen/testdata/method_handler.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/pubsub\"\n)\n\n//encore:service\ntype Service struct{}\n\nfunc (s *Service) PointerMethod(ctx context.Context, event *Event) error {\n    return nil\n}\n\nfunc (s Service) NonPointerMethod(ctx context.Context, event *Event) error {\n    return nil\n}\n\ntype Event struct {}\n\nvar Topic = pubsub.NewTopic[*Event](\"topic\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar _ = pubsub.NewSubscription(Topic, \"pointer\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: pubsub.MethodHandler((*Service).PointerMethod),\n    },\n)\n\nvar _ = pubsub.NewSubscription(Topic, \"non-pointer\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: pubsub.MethodHandler(Service.NonPointerMethod),\n    },\n)\n-- want:svc/encore_internal__pubsub.go --\npackage svc\n\nimport (\n\t\"context\"\n\t__service \"encore.dev/appruntime/apisdk/service\"\n)\n\nfunc EncoreInternal_pubsub_handler_pointer(ctx context.Context, msg *Event) error {\n\tsvc, err := __service.Get[*Service](\"svc\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.PointerMethod(ctx, msg)\n}\n\nfunc EncoreInternal_pubsub_handler_non_pointer(ctx context.Context, msg *Event) error {\n\tsvc, err := __service.Get[*Service](\"svc\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn svc.NonPointerMethod(ctx, msg)\n}\n-- want:svc/svc.go --\npackage svc\n\nimport (\n\t\"context\"\n\t\"encore.dev/pubsub\"\n)\n\n//encore:service\ntype Service struct{}\n\nfunc (s *Service) PointerMethod(ctx context.Context, event *Event) error {\n    return nil\n}\n\nfunc (s Service) NonPointerMethod(ctx context.Context, event *Event) error {\n    return nil\n}\n\ntype Event struct {}\n\nvar Topic = pubsub.NewTopic[*Event](\"topic\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar _ = pubsub.NewSubscription(Topic, \"pointer\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: EncoreInternal_pubsub_handler_pointer,\n    },\n)\n\nvar _ = pubsub.NewSubscription(Topic, \"non-pointer\",\n    pubsub.SubscriptionConfig[*Event]{\n        Handler: EncoreInternal_pubsub_handler_non_pointer,\n    },\n)\n"
  },
  {
    "path": "v2/codegen/infragen/secretsgen/secretsgen.go",
    "content": "package secretsgen\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/infra/secrets\"\n)\n\nfunc Gen(gen *codegen.Generator, svc option.Option[*app.Service], pkg *pkginfo.Package, secrets []*secrets.Secrets) {\n\taddedImport := make(map[*pkginfo.File]bool)\n\tfor _, secret := range secrets {\n\t\tfile := secret.File\n\t\trw := gen.Rewrite(file)\n\n\t\tif !addedImport[file] {\n\t\t\t// Add an import of the runtime package to be able to load secrets.\n\t\t\tinsertPos := file.AST().Name.End()\n\t\t\tln := gen.FS.Position(insertPos)\n\n\t\t\trw.Insert(insertPos, []byte(fmt.Sprintf(\"\\nimport __encore_secrets %s;/*line :%d:%d*/\",\n\t\t\t\tstrconv.Quote(\"encore.dev/appruntime/infrasdk/secrets\"),\n\t\t\t\tln.Line, ln.Column)))\n\t\t\taddedImport[secret.File] = true\n\t\t}\n\n\t\tgetName := func(svc *app.Service) string { return svc.Name }\n\t\tsvcName := strconv.Quote(option.Map(svc, getName).GetOrElse(\"\"))\n\n\t\t// Rewrite the value spec to load the secrets.\n\t\tspec := secret.Spec\n\t\tvar buf bytes.Buffer\n\t\tbuf.WriteString(\"{\\n\")\n\t\tfor _, key := range secret.Keys {\n\t\t\tfmt.Fprintf(&buf, \"\\t%s: __encore_secrets.Load(%s, %s),\\n\", key, strconv.Quote(key), svcName)\n\t\t}\n\t\tep := gen.FS.Position(spec.End())\n\t\tfmt.Fprintf(&buf, \"}/*line :%d:%d*/\", ep.Line, ep.Column)\n\t\trw.Insert(spec.Type.Pos(), []byte(\"= \"))\n\t\trw.Insert(spec.End(), buf.Bytes())\n\n\t}\n}\n"
  },
  {
    "path": "v2/codegen/internal/codegentest/codegentest.go",
    "content": "package codegentest\n\nimport (\n\tstdcmp \"cmp\"\n\t\"errors\"\n\t\"flag\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/rogpeppe/go-internal/renameio\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/compiler/build\"\n\t\"encr.dev/v2/internals/overlay\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n)\n\ntype Case struct {\n\tName     string\n\tCode     string\n\tWant     map[string]string // file name -> contents\n\tWantErrs []string\n}\n\nvar goldenUpdate = flag.Bool(\"golden-update\", os.Getenv(\"GOLDEN_UPDATE\") != \"\", \"update golden files\")\n\nfunc Run(t *testing.T, fn func(*codegen.Generator, *app.Desc)) {\n\tflag.Parse()\n\tc := qt.New(t)\n\ttests := readTestCases(c, \"testdata\")\n\tfor _, test := range tests {\n\t\tc.Run(test.name, func(c *qt.C) {\n\t\t\ttc := testutil.NewContext(c, false, test.input)\n\t\t\ttc.FailTestOnErrors()\n\n\t\t\t// Create a go.mod file in the main module directory if it doesn't already exist.\n\t\t\tmodPath := tc.MainModuleDir.Join(\"go.mod\").ToIO()\n\t\t\tif _, err := os.Stat(modPath); err != nil {\n\t\t\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\tc.Fatal(err)\n\t\t\t\t}\n\t\t\t\tmodContents := \"module example.com\\nrequire encore.dev v1.52.0\"\n\t\t\t\terr := os.WriteFile(modPath, []byte(modContents), 0644)\n\t\t\t\tc.Assert(err, qt.IsNil)\n\t\t\t}\n\n\t\t\ttc.GoModTidy()\n\t\t\ttc.GoModDownload()\n\n\t\t\tp := parser.NewParser(tc.Context)\n\t\t\tparserResult := p.Parse()\n\t\t\tgen := codegen.New(tc.Context, nil)\n\t\t\tappDesc := app.ValidateAndDescribe(tc.Context, parserResult)\n\n\t\t\t// Run the codegen\n\t\t\tfn(gen, appDesc)\n\n\t\t\t// Construct the map of generated code.\n\t\t\toverlays := gen.Overlays()\n\t\t\tgot := make(map[string]string, len(overlays))\n\t\t\tfor _, o := range overlays {\n\t\t\t\t// Try to compute a reasonable display path for the file.\n\t\t\t\t// If it's local within the main module, use that.\n\t\t\t\t// Otherwise check if it's within the runtime module,\n\t\t\t\t// and finally fall back to the absolute path.\n\t\t\t\tkey := o.Source.ToIO()\n\n\t\t\t\tmainRel, err := filepath.Rel(tc.MainModuleDir.ToIO(), o.Source.ToIO())\n\t\t\t\tif err == nil && filepath.IsLocal(mainRel) {\n\t\t\t\t\tkey = mainRel\n\t\t\t\t} else {\n\t\t\t\t\truntimeRel, err := filepath.Rel(tc.Build.EncoreRuntime.ToIO(), o.Source.ToIO())\n\t\t\t\t\tif err == nil && filepath.IsLocal(runtimeRel) {\n\t\t\t\t\t\tkey = runtimeRel\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tgot[key] = string(o.Contents)\n\t\t\t}\n\n\t\t\tif *goldenUpdate {\n\t\t\t\tupdateGoldenFiles(c, test, got)\n\t\t\t} else if diff := cmp.Diff(got, test.want); diff != \"\" {\n\t\t\t\tc.Fatalf(\"generated code differs (-got +want):\\n%s\", diff)\n\t\t\t}\n\n\t\t\t// Make sure it compiles\n\t\t\tgoBuild(tc, overlays)\n\t\t})\n\t}\n}\n\nfunc readTestCases(c *qt.C, dir string) []*testCase {\n\tfiles, err := filepath.Glob(filepath.Join(dir, \"*.txt\"))\n\tc.Assert(err, qt.IsNil)\n\n\tvar cases []*testCase\n\tfor _, file := range files {\n\t\tcases = append(cases, parseTestCase(c, file))\n\t}\n\treturn cases\n}\n\ntype testCase struct {\n\tfilename string\n\tname     string\n\tinput    *txtar.Archive\n\twant     map[string]string\n}\n\nfunc parseTestCase(c *qt.C, file string) *testCase {\n\tar, err := txtar.ParseFile(file)\n\tc.Assert(err, qt.IsNil)\n\n\twant := make(map[string]string)\n\tfor i := 0; i < len(ar.Files); i++ {\n\t\tf := ar.Files[i]\n\n\t\tif fn, ok := strings.CutPrefix(f.Name, \"want:\"); ok {\n\t\t\twant[fn] = string(f.Data)\n\t\t\tar.Files = slices.Delete(ar.Files, i, i+1)\n\t\t\ti--\n\t\t}\n\t}\n\n\treturn &testCase{\n\t\tfilename: file,\n\t\tname:     strings.TrimSuffix(filepath.Base(file), \".txt\"),\n\t\tinput:    ar,\n\t\twant:     want,\n\t}\n}\n\nfunc updateGoldenFiles(c *qt.C, tc *testCase, got map[string]string) {\n\tvar goldenFiles []txtar.File\n\tfor key, val := range got {\n\t\tgoldenFiles = append(goldenFiles, txtar.File{\n\t\t\tName: \"want:\" + key,\n\t\t\tData: []byte(val),\n\t\t})\n\t}\n\n\tslices.SortFunc(goldenFiles, func(a, b txtar.File) int {\n\t\treturn stdcmp.Compare(a.Name, b.Name)\n\t})\n\n\ttc.input.Files = append(tc.input.Files, goldenFiles...)\n\terr := renameio.WriteFile(tc.filename, txtar.Format(tc.input))\n\tc.Assert(err, qt.IsNil)\n}\n\nfunc goBuild(tc *testutil.Context, overlays []overlay.File) {\n\tbuild.Build(tc.Context.Ctx, &build.Config{\n\t\tCtx:      tc.Context,\n\t\tOverlays: overlays,\n\t\tMainPkg:  \"./...\",\n\t\tNoBinary: true,\n\t})\n}\n"
  },
  {
    "path": "v2/codegen/internal/genutil/etype.go",
    "content": "package genutil\n\nimport (\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n)\n\n// MarshalBuiltin generates the code to marshal a builtin type.\n// The resulting code if an expression of type string.\nfunc MarshalBuiltin(kind schema.BuiltinKind, value *Statement) Code {\n\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"MarshalOne\").Call(\n\t\tQual(\"encore.dev/appruntime/shared/etype\", \"Marshal\"+builtinToName(kind)),\n\t\tvalue.Clone(),\n\t)\n}\n\n// MarshalBuiltinList generates the code to marshal a list of builtins.\n// The resulting code is an expression of type []string.\nfunc MarshalBuiltinList(kind schema.BuiltinKind, value *Statement) Code {\n\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"MarshalList\").Call(\n\t\tQual(\"encore.dev/appruntime/shared/etype\", \"Marshal\"+builtinToName(kind)),\n\t\tvalue.Clone(),\n\t)\n}\n\n// MarshalQueryOrHeader generates the code to marshal a supported query value.\n// The resulting code is an expression of type []string.\nfunc MarshalQueryOrHeader(typ schema.Type, value *Statement) (code Code, ok bool) {\n\tif list, ok := typ.(schema.ListType); ok {\n\t\tmarshaller, ok := getQueryOrHeaderMarshaller(list.Elem)\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\n\t\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"MarshalList\").Call(\n\t\t\tmarshaller,\n\t\t\tvalue.Clone(),\n\t\t), true\n\t}\n\n\tmarshaller, ok := getQueryOrHeaderMarshaller(typ)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\n\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"MarshalOneAsList\").Call(\n\t\tmarshaller,\n\t\tvalue.Clone(),\n\t), true\n}\n\nfunc getQueryOrHeaderMarshaller(typ schema.Type) (s *Statement, ok bool) {\n\tswitch typ := typ.(type) {\n\tcase schema.BuiltinType:\n\t\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"Marshal\"+builtinToName(typ.Kind)), true\n\tcase schema.OptionType:\n\t\tinner, ok := getQueryOrHeaderMarshaller(typ.Value)\n\t\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"OptionMarshaller\").Call(\n\t\t\tinner,\n\t\t), ok\n\tdefault:\n\t\treturn Nil(), false\n\t}\n}\n\ntype TypeUnmarshaller struct {\n\terrs *perr.List\n\n\t// unmarshallerExpr is the code gen expression to resolve the unmarshaller.\n\t// It's typically just an identifier, but it can be more complex if need be.\n\tunmarshallerExpr *Statement\n}\n\nfunc (g *Helper) NewTypeUnmarshaller(objName string) *TypeUnmarshaller {\n\texpr := Id(objName)\n\treturn &TypeUnmarshaller{errs: g.Errs, unmarshallerExpr: expr}\n}\n\n// UnmarshallerTypeName returns a type expression for the etype.Unmarshaller type\n// in the runtime, in the form \"*etype.Unmarshaller\".\nfunc UnmarshallerTypeName() *Statement {\n\treturn Op(\"*\").Qual(\"encore.dev/appruntime/shared/etype\", \"Unmarshaller\")\n}\n\nfunc (u *TypeUnmarshaller) Init() *Statement {\n\treturn u.unmarshallerExpr.Clone().Op(\":=\").New(Qual(\"encore.dev/appruntime/shared/etype\", \"Unmarshaller\"))\n}\n\nfunc (u *TypeUnmarshaller) Err() *Statement {\n\treturn u.unmarshallerExpr.Clone().Dot(\"Error\")\n}\n\nfunc (u *TypeUnmarshaller) HasError() *Statement {\n\treturn u.unmarshallerExpr.Clone().Dot(\"Error\").Op(\"!=\").Nil()\n}\n\n// NumNonEmptyValues returns an integer expression that reports\n// the number of non-empty values the unmarshaller has processed.\nfunc (u *TypeUnmarshaller) NumNonEmptyValues() *Statement {\n\treturn u.unmarshallerExpr.Clone().Dot(\"NonEmptyValues\")\n}\n\n// IncNonEmpty returns a statement to increment the number of\n// non-empty values the unmarshaller has processed.\nfunc (u *TypeUnmarshaller) IncNonEmpty() *Statement {\n\treturn u.unmarshallerExpr.Clone().Dot(\"IncNonEmpty\").Call()\n}\n\nfunc (u *TypeUnmarshaller) UnmarshalBuiltin(kind schema.BuiltinKind, fieldName string, value *Statement, required bool) *Statement {\n\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"UnmarshalOne\").Call(\n\t\tu.unmarshallerExpr.Clone(),\n\t\tQual(\"encore.dev/appruntime/shared/etype\", \"Unmarshal\"+builtinToName(kind)),\n\t\tLit(fieldName),\n\t\tvalue.Clone(),\n\t\tLit(required),\n\t)\n}\n\n// UnmarshalQueryOrHeader returns the code to unmarshal a supported type.\nfunc (u *TypeUnmarshaller) UnmarshalQueryOrHeader(typ schema.Type, fieldName string, singleValue, listOfValues *Statement) *Statement {\n\tif !schemautil.IsValidHeaderType(typ) {\n\t\tu.errs.Addf(typ.ASTExpr().Pos(), \"cannot unmarshal string to type %s\", typ)\n\t\treturn Null()\n\t}\n\n\tif list, ok := typ.(schema.ListType); ok {\n\t\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"UnmarshalList\").Call(\n\t\t\tu.unmarshallerExpr.Clone(),\n\t\t\tu.getQueryOrHeaderUnmarshaller(list.Elem),\n\t\t\tLit(fieldName),\n\t\t\tlistOfValues,\n\t\t\tLit(false), // not required\n\t\t)\n\t}\n\n\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"UnmarshalOne\").Call(\n\t\tu.unmarshallerExpr.Clone(),\n\t\tu.getQueryOrHeaderUnmarshaller(typ),\n\t\tLit(fieldName),\n\t\tsingleValue,\n\t\tLit(false), // not required\n\t)\n}\n\nfunc (u *TypeUnmarshaller) getQueryOrHeaderUnmarshaller(typ schema.Type) *Statement {\n\tswitch typ := typ.(type) {\n\tcase schema.BuiltinType:\n\t\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"Unmarshal\"+builtinToName(typ.Kind))\n\tcase schema.OptionType:\n\t\treturn Qual(\"encore.dev/appruntime/shared/etype\", \"OptionUnmarshaller\").Call(\n\t\t\tu.getQueryOrHeaderUnmarshaller(typ.Value),\n\t\t)\n\tdefault:\n\t\tu.errs.Addf(typ.ASTExpr().Pos(), \"cannot unmarshal string to type %s\", typ)\n\t\treturn Null()\n\t}\n}\n\n// ReadBody returns an expression to read the full request body into a []byte.\nfunc (u *TypeUnmarshaller) ReadBody(bodyExpr *Statement) *Statement {\n\treturn u.unmarshallerExpr.Clone().Dot(\"ReadBody\").Call(bodyExpr.Clone())\n}\n\n// ParseJSON returns an expression to parse json.\n// It uses the iterator accessed through the given iteratorExpr to parse JSON into the given dstExpr.\n// The dstExpr must be a pointer value.\n// The field name is only used for error reporting.\nfunc (u *TypeUnmarshaller) ParseJSON(fieldName string, iteratorExpr *Statement, dstExpr *Statement) *Statement {\n\treturn u.unmarshallerExpr.Clone().Dot(\"ParseJSON\").Call(Lit(fieldName), iteratorExpr, dstExpr)\n}\n\n// builtinToName returns the string name of the builtin.\n//\n// Each kind's name corresponds with the functions in etype.\n// That is, if this function returns \"Foo\" it expects the etype package\n// in the runtime to contain \"MarshalFoo and UnmarshalFoo\".\nfunc builtinToName(kind schema.BuiltinKind) string {\n\treturn kind.String()\n}\n"
  },
  {
    "path": "v2/codegen/internal/genutil/types.go",
    "content": "package genutil\n\nimport (\n\t\"fmt\"\n\tgotoken \"go/token\"\n\t\"reflect\"\n\t\"strings\"\n\n\t. \"github.com/dave/jennifer/jen\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n)\n\nfunc NewHelper(errs *perr.List) *Helper {\n\treturn &Helper{Errs: errs}\n}\n\ntype Helper struct {\n\tErrs *perr.List\n}\n\n// Type generates a Go type from a schema type.\nfunc (g *Helper) Type(typ schema.Type) *Statement {\n\tswitch typ := typ.(type) {\n\tcase schema.NamedType:\n\t\treturn g.named(typ)\n\tcase schema.StructType:\n\t\treturn g.struct_(typ)\n\tcase schema.MapType:\n\t\treturn Map(g.Type(typ.Key)).Add(g.Type(typ.Value))\n\tcase schema.ListType:\n\t\telem := g.Type(typ.Elem)\n\t\tif typ.Len != -1 {\n\t\t\treturn Index(Lit(typ.Len)).Add(elem)\n\t\t}\n\t\treturn Index().Add(elem)\n\tcase schema.PointerType:\n\t\treturn Op(\"*\").Add(g.Type(typ.Elem))\n\tcase schema.OptionType:\n\t\treturn Qual(\"encore.dev/types/option\", \"Option\").Types(g.Type(typ.Value))\n\tcase schema.BuiltinType:\n\t\treturn g.Builtin(typ.AST.Pos(), typ.Kind)\n\tcase schema.InterfaceType:\n\t\tg.Errs.AddPos(typ.AST.Pos(), \"unexpected interface type\")\n\t\treturn Any()\n\tcase schema.TypeParamRefType:\n\t\ttypeParam := typ.Decl.TypeParameters()[typ.Index]\n\t\treturn Id(typeParam.Name)\n\tdefault:\n\t\tg.Errs.Addf(typ.ASTExpr().Pos(), \"unexpected schema type %T\", typ)\n\t\treturn Any()\n\t}\n}\n\nfunc (g *Helper) named(named schema.NamedType) *Statement {\n\tst := Q(named.DeclInfo)\n\n\tif len(named.TypeArgs) > 0 {\n\t\ttypes := fns.Map(named.TypeArgs, func(arg schema.Type) Code {\n\t\t\treturn g.Type(arg)\n\t\t})\n\t\tst = st.Types(types...)\n\t}\n\n\treturn st\n}\n\nfunc (g *Helper) struct_(st schema.StructType) *Statement {\n\tfields := make([]Code, len(st.Fields))\n\n\tfor i, field := range st.Fields {\n\t\tvar f *Statement\n\t\ttypExpr := g.Type(field.Type)\n\t\tif field.IsAnonymous() {\n\t\t\tf = typExpr\n\t\t} else {\n\t\t\tf = Id(field.Name.MustGet()).Add(typExpr)\n\t\t}\n\n\t\t// Add field tags\n\t\tif field.Tag.Len() > 0 {\n\t\t\ttagMap := make(map[string]string)\n\t\t\tfor _, tag := range field.Tag.Tags() {\n\t\t\t\ttagMap[tag.Key] = tag.Value()\n\t\t\t}\n\t\t\tf = f.Tag(tagMap)\n\t\t}\n\n\t\t// Add doc comment\n\t\tif doc := strings.TrimSpace(field.Doc); doc != \"\" {\n\t\t\tf = f.Comment(doc)\n\t\t}\n\t\tfields[i] = f\n\t}\n\n\treturn Struct(fields...)\n}\n\nfunc (g *Helper) Builtin(pos gotoken.Pos, kind schema.BuiltinKind) *Statement {\n\tswitch kind {\n\tcase schema.Any:\n\t\treturn Any()\n\tcase schema.Bool:\n\t\treturn Bool()\n\tcase schema.Int:\n\t\treturn Int()\n\tcase schema.Int8:\n\t\treturn Int8()\n\tcase schema.Int16:\n\t\treturn Int16()\n\tcase schema.Int32:\n\t\treturn Int32()\n\tcase schema.Int64:\n\t\treturn Int64()\n\tcase schema.Uint:\n\t\treturn Uint()\n\tcase schema.Uint8:\n\t\treturn Uint8()\n\tcase schema.Uint16:\n\t\treturn Uint16()\n\tcase schema.Uint32:\n\t\treturn Uint32()\n\tcase schema.Uint64:\n\t\treturn Uint64()\n\tcase schema.Float32:\n\t\treturn Float32()\n\tcase schema.Float64:\n\t\treturn Float64()\n\tcase schema.String:\n\t\treturn String()\n\tcase schema.Bytes:\n\t\treturn Index().Byte()\n\n\tcase schema.Time:\n\t\treturn Qual(\"time\", \"Time\")\n\tcase schema.JSON:\n\t\treturn Qual(\"encoding/json\", \"RawMessage\")\n\tcase schema.UUID:\n\t\treturn Qual(\"encore.dev/types/uuid\", \"UUID\")\n\tcase schema.UserID:\n\t\treturn Qual(\"encore.dev/beta/auth\", \"UID\")\n\tcase schema.Error:\n\t\treturn Error()\n\tdefault:\n\t\tg.Errs.Addf(pos, \"unsupported builtin kind: %v\", kind)\n\t\treturn Id(\"unsupported\")\n\t}\n}\n\n// TypeToString converts a schema.Type to a string.\nfunc (g *Helper) TypeToString(typ schema.Type) string {\n\t// We wrap the type before rendering in \"var _ {type}\" so Jen correctly formats,\n\t// then we strip the \"var _\" part.\n\treturn fmt.Sprintf(\"%#v\", Var().Id(\"_\").Add(g.Type(typ)))[6:]\n}\n\n// Q returns a qualified name (using [jen.Qual]) for the given info.\nfunc Q(info *pkginfo.PkgDeclInfo) *Statement {\n\treturn Qual(info.File.Pkg.ImportPath.String(), info.Name)\n}\n\nfunc (g *Helper) GoToJen(pos gotoken.Pos, val any) *Statement {\n\treturn g.goToJen(pos, reflect.ValueOf(val))\n}\n\nfunc (g *Helper) goToJen(pos gotoken.Pos, val reflect.Value) *Statement {\n\tswitch val.Kind() {\n\t// All the types supported by jen.Lit can be passed directly.\n\tcase reflect.Bool, reflect.String,\n\t\treflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,\n\t\treflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,\n\t\treflect.Float32, reflect.Float64,\n\t\treflect.Complex64, reflect.Complex128,\n\t\treflect.Uintptr:\n\t\treturn Lit(val.Interface())\n\n\tcase reflect.Slice, reflect.Array:\n\t\treturn g.goTypeToJen(pos, val.Type()).ValuesFunc(func(group *Group) {\n\t\t\tfor i := 0; i < val.Len(); i++ {\n\t\t\t\tgroup.Add(g.goToJen(pos, val.Index(i)))\n\t\t\t}\n\t\t})\n\tcase reflect.Map:\n\t\treturn g.goTypeToJen(pos, val.Type()).ValuesFunc(func(group *Group) {\n\t\t\titer := val.MapRange()\n\t\t\tfor iter.Next() {\n\t\t\t\tgroup.Add(g.goToJen(pos, iter.Key())).Op(\":\").Add(g.goToJen(pos, iter.Value()))\n\t\t\t}\n\t\t})\n\tdefault:\n\t\tg.Errs.Addf(pos, \"unsupported type: %T\", val.Interface())\n\t\treturn Null()\n\t}\n}\n\nfunc (g *Helper) goTypeToJen(pos gotoken.Pos, typ reflect.Type) *Statement {\n\tswitch typ.Kind() {\n\tcase reflect.Bool:\n\t\treturn Bool()\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn Int()\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:\n\t\treturn Uint()\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn Float32()\n\tcase reflect.String:\n\t\treturn String()\n\tcase reflect.Slice:\n\t\treturn Index().Add(g.goTypeToJen(pos, typ.Elem()))\n\tcase reflect.Pointer:\n\t\treturn Op(\"*\").Add(g.goTypeToJen(pos, typ.Elem()))\n\tcase reflect.Array:\n\t\treturn Index(Lit(typ.Len())).Add(g.goTypeToJen(pos, typ.Elem()))\n\tcase reflect.Map:\n\t\treturn Map(g.goTypeToJen(pos, typ.Key())).Add(g.goTypeToJen(pos, typ.Elem()))\n\tdefault:\n\t\tg.Errs.Addf(pos, \"unsupported Go type in codegen: %v\", typ)\n\t\treturn Null()\n\t}\n}\n\n// IsNotJSONEmpty returns a jen expression evaluating whether expr is not \"empty\".\n// \"Empty\" is defined by the encoding/json package as:\n// \"false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.\"\nfunc (g *Helper) IsNotJSONEmpty(expr *Statement, typ schema.Type) *Statement {\n\t// Dereference any named types so we get at the underlying type.\n\tfor {\n\t\tif named, ok := typ.(schema.NamedType); ok {\n\t\t\ttyp = named.Decl().Type\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tswitch typ := typ.(type) {\n\tcase schema.InterfaceType, schema.FuncType, schema.PointerType:\n\t\treturn expr.Op(\"!=\").Nil()\n\tcase schema.MapType:\n\t\treturn Len(expr).Op(\"!=\").Lit(0)\n\tcase schema.ListType:\n\t\tswitch {\n\t\tcase typ.Len < 0: // slice\n\t\t\treturn Len(expr).Op(\"!=\").Lit(0)\n\t\tcase typ.Len == 0:\n\t\t\t// zero-length array, always empty\n\t\t\treturn False()\n\t\tcase typ.Len > 0:\n\t\t\t// positive-length array, never empty\n\t\t\treturn True()\n\t\t}\n\tcase schema.BuiltinType:\n\t\tswitch typ.Kind {\n\t\tcase schema.Any, schema.Error:\n\t\t\treturn expr.Op(\"!=\").Nil()\n\t\tcase schema.Bool:\n\t\t\treturn expr\n\t\tcase schema.Int, schema.Int8, schema.Int16, schema.Int32, schema.Int64, schema.Uint, schema.Uint8, schema.Uint16, schema.Uint32, schema.Uint64, schema.Float32, schema.Float64:\n\t\t\treturn expr.Op(\"!=\").Lit(0)\n\t\tcase schema.String:\n\t\t\treturn expr.Op(\"!=\").Lit(\"\")\n\t\tcase schema.Bytes, schema.JSON:\n\t\t\treturn Len(expr).Op(\"!=\").Lit(0)\n\t\tcase schema.Time:\n\t\t\t// Technically not 100% compliant but closer to\n\t\t\t// the user's intention.\n\t\t\treturn Op(\"!\").Parens(expr.Dot(\"IsZero\").Call())\n\t\tcase schema.UserID:\n\t\t\treturn expr.Op(\"!=\").Lit(\"\")\n\t\t}\n\t}\n\treturn True()\n}\n\n// Zero returns a jen expression representing the zero value\n// for the given type. If the type is nil it returns \"nil\".\nfunc (g *Helper) Zero(typ schema.Type) *Statement {\n\tisNillable := func(typ schema.Type) bool {\n\t\tswitch typ := typ.(type) {\n\t\tcase nil, schema.PointerType, schema.MapType, schema.FuncType, schema.InterfaceType:\n\t\t\treturn true\n\t\tcase schema.ListType:\n\t\t\treturn typ.Len == -1 // -1 == slice, anything else is an array\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\ttyp, numPointers := schemautil.Deref(typ)\n\n\tif named, ok := typ.(schema.NamedType); ok {\n\t\t// If the underlying type is nillable or we have dereferenced pointers,\n\t\t// return (*Foo)(nil).\n\t\tif numPointers > 0 || isNillable(named.Decl().Type) {\n\t\t\t// Return (*Foo)(nil) if the underlying type is nillable.\n\t\t\tops := strings.Repeat(\"*\", numPointers)\n\t\t\treturn Parens(Op(ops).Add(g.Type(named))).Call(Nil())\n\t\t}\n\n\t}\n\tif numPointers > 0 || isNillable(typ) {\n\t\treturn Nil()\n\t} else if builtin, ok := typ.(schema.BuiltinType); ok {\n\t\treturn g.builtinZero(builtin)\n\t}\n\n\t// Otherwise return (Foo{}).\n\t// We need to wrap the type in parens as both generic types\n\t// and other types need brackets around them for the zero value.\n\t//\n\t// i.e.\n\t// \tif val != (Foo{}) { ... }\n\t// \tif val != (Foo[Generic]{}) { ... }\n\treturn Parens(g.Type(typ).Values())\n}\n\n// Initialize returns a jen expression for initializing\n// the given type. If the type is a pointer type it returns new(Foo),\n// and make(Foo) for slices and maps.\n//\n// Certain types like function types and interfaces return \"nil\"\n// as there is no canonical way to initialize them to a non-zero value.\nfunc (g *Helper) Initialize(typ schema.Type) *Statement {\n\tswitch typ := typ.(type) {\n\tcase schema.PointerType:\n\t\treturn New(g.Type(typ.Elem))\n\tcase schema.ListType:\n\t\treturn g.Type(typ).Values()\n\tcase schema.MapType:\n\t\treturn Make(g.Type(typ))\n\tcase schema.BuiltinType:\n\t\treturn g.Zero(typ)\n\tdefault:\n\t\treturn g.Zero(typ)\n\t}\n}\n\nfunc (g *Helper) builtinZero(builtin schema.BuiltinType) *Statement {\n\tswitch builtin.Kind {\n\tcase schema.Bool:\n\t\treturn False()\n\tcase schema.Int, schema.Int8, schema.Int16, schema.Int32, schema.Int64,\n\t\tschema.Uint, schema.Uint8, schema.Uint16, schema.Uint32, schema.Uint64:\n\t\treturn Lit(0)\n\tcase schema.Float32, schema.Float64:\n\t\treturn Lit(0.0)\n\tcase schema.String:\n\t\treturn Lit(\"\")\n\tcase schema.Bytes:\n\t\treturn Nil()\n\tcase schema.Time:\n\t\treturn Parens(Qual(\"time\", \"Time\").Values())\n\tcase schema.UUID:\n\t\treturn Parens(Qual(\"encore.dev/types/uuid\", \"UUID\").Values())\n\tcase schema.JSON:\n\t\t// Zero for JSON is nil, we don't return a typed nil as you can't use them\n\t\t// in if statements.\n\t\treturn Nil()\n\tcase schema.UserID:\n\t\treturn Qual(\"encore.dev/beta/auth\", \"UID\").Call(Lit(\"\"))\n\tcase schema.Error:\n\t\treturn Parens(Id(\"error\")).Call(nil)\n\tdefault:\n\t\tg.Errs.Addf(builtin.ASTExpr().Pos(), \"unsupported builtin type: %v\", builtin.Kind)\n\t\treturn Null()\n\t}\n}\n"
  },
  {
    "path": "v2/codegen/rewrite/rewrite.go",
    "content": "package rewrite\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n)\n\nfunc New(data []byte, base int) *Rewriter {\n\treturn &Rewriter{\n\t\tbase: 0,\n\t\tsegs: []segment{{\n\t\t\tdata:  data,\n\t\t\tstart: base,\n\t\t\tend:   base + len(data),\n\t\t}},\n\t}\n}\n\ntype Rewriter struct {\n\tbase int\n\tsegs []segment\n}\n\nfunc (r *Rewriter) Replace(start, end token.Pos, data []byte) {\n\tsi, so := r.seg(start, false)\n\tei, eo := r.seg(end, true)\n\tr.replace(si, so, ei, eo, data)\n}\n\nfunc (r *Rewriter) ReplaceNode(node ast.Node, data []byte) {\n\tr.Replace(node.Pos(), node.End(), data)\n}\n\nfunc (r *Rewriter) Append(data []byte) {\n\tstart := 0\n\tif len(r.segs) > 0 {\n\t\tstart = r.segs[len(r.segs)-1].end\n\t}\n\tr.segs = append(r.segs, segment{\n\t\tstart: int(start),\n\t\tend:   int(start),\n\t\tdata:  data,\n\t})\n}\n\nfunc (r *Rewriter) Insert(start token.Pos, data []byte) {\n\t// If the pos is at the very end of the file, insert a new segment directly,\n\t// since calling r.seg(start) would panic.\n\tif len(r.segs) > 0 && r.segs[len(r.segs)-1].end == int(start) {\n\t\tr.segs = append(r.segs, segment{\n\t\t\tstart: int(start),\n\t\t\tend:   int(start) + len(data),\n\t\t\tdata:  data,\n\t\t})\n\t\treturn\n\t}\n\n\tsi, so := r.seg(start, false)\n\tr.replace(si, so, si, so, data)\n}\n\nfunc (r *Rewriter) Delete(start, end token.Pos) {\n\tsi, so := r.seg(start, false)\n\tei, eo := r.seg(end, true)\n\tr.replace(si, so, ei, eo, nil)\n}\n\nfunc (r *Rewriter) Data() []byte {\n\tvar buf bytes.Buffer\n\tfor _, seg := range r.segs {\n\t\tbuf.Write(seg.data)\n\t}\n\treturn buf.Bytes()\n}\n\nfunc (r *Rewriter) replace(si, so, ei, eo int, data []byte) {\n\tif si == ei {\n\t\t// Same segment; cut it into two\n\t\tstart := r.segs[si]\n\t\tend := segment{\n\t\t\tstart: start.start + eo,\n\t\t\tend:   start.end,\n\t\t\tdata:  start.data[eo:],\n\t\t}\n\t\tstart.data = start.data[:so]\n\t\tstart.end = start.start + so\n\t\tmid := segment{\n\t\t\tstart: start.end,\n\t\t\tend:   end.start,\n\t\t\tdata:  data,\n\t\t}\n\t\tr.segs = append(r.segs[:si], append([]segment{start, mid, end}, r.segs[ei+1:]...)...)\n\t} else {\n\t\t// Already different segments; adjust start/end and replace segments in-between\n\t\tstart := r.segs[si]\n\t\tend := r.segs[ei]\n\t\tstart.end = start.start + so\n\t\tstart.data = start.data[:so]\n\t\tend.start += eo\n\t\tend.data = end.data[eo:]\n\t\tmid := segment{\n\t\t\tstart: start.end,\n\t\t\tend:   end.start,\n\t\t\tdata:  data,\n\t\t}\n\t\tr.segs = append(r.segs[:si], append([]segment{start, mid, end}, r.segs[ei+1:]...)...)\n\t}\n}\n\nfunc (r *Rewriter) seg(pos token.Pos, isEnd bool) (idx int, offset int) {\n\tp := int(pos)\n\tif isEnd {\n\t\tfor i, seg := range r.segs {\n\t\t\tif seg.start < p && p <= seg.end {\n\t\t\t\treturn i, min(int(p-seg.start), len(seg.data))\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor i, seg := range r.segs {\n\t\t\tif seg.start <= p && p < seg.end {\n\t\t\t\treturn i, min(int(p-seg.start), len(seg.data))\n\t\t\t}\n\t\t}\n\t}\n\n\tpanic(fmt.Sprintf(\"original file does not contain pos %v\", pos))\n}\n\ntype segment struct {\n\tstart int // inclusive\n\tend   int // exclusive\n\tdata  []byte\n}\n"
  },
  {
    "path": "v2/codegen/rewrite/rewrite_test.go",
    "content": "package rewrite\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestReplaceEnd(t *testing.T) {\n\trw := New([]byte(\"test\"), 1)\n\trw.Replace(4, 5, []byte(\"ting\"))\n\tif got, want := rw.Data(), []byte(\"testing\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n\n}\n\nfunc TestSplit(t *testing.T) {\n\trw := New([]byte(\"test\"), 1)\n\trw.Replace(2, 4, []byte(\"ou\"))  // \"tout\"\n\trw.Replace(1, 2, []byte(\"rea\")) // \"reaout\"\n\trw.Insert(4, []byte(\"h\"))       // \"reaouht\"\n\trw.Insert(4, []byte(\"a\"))       // \"reaouaht\"\n\tif got, want := rw.Data(), []byte(\"reaouhat\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n}\n\nfunc TestReplaceAcrossSegments(t *testing.T) {\n\trw := New([]byte(\"foo bar\"), 1)\n\trw.Replace(5, 6, []byte(\"l\"))  // \"foo lar\"\n\trw.Replace(2, 7, []byte(\"hi\")) // \"fhir\"\n\tif got, want := rw.Data(), []byte(\"fhir\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n}\n\nfunc TestReplaceTwice(t *testing.T) {\n\trw := New([]byte(\"foo bar\"), 1)\n\trw.Replace(5, 6, []byte(\"l\"))     // \"foo lar\"\n\trw.Replace(2, 7, []byte(\"hi\"))    // \"fhir\"\n\trw.Replace(2, 7, []byte(\"hello\")) // \"fhellor\"\n\tif got, want := rw.Data(), []byte(\"fhellor\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n}\n\nfunc TestDelete(t *testing.T) {\n\trw := New([]byte(\"foo bar\"), 1)\n\trw.Replace(5, 6, []byte(\"l\")) // \"foo lar\"\n\trw.Delete(2, 7)\n\tif got, want := rw.Data(), []byte(\"fr\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n}\n\nfunc TestInsertAtEnd(t *testing.T) {\n\trw := New([]byte(\"\"), 1)\n\trw.Insert(1, []byte(\"// test\"))\n\tif got, want := rw.Data(), []byte(\"// test\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n\trw.Delete(1, 4)\n\tif got, want := rw.Data(), []byte(\"test\"); !bytes.Equal(got, want) {\n\t\tt.Errorf(\"got data %s, want %s\", got, want)\n\t}\n}\n"
  },
  {
    "path": "v2/compiler/build/build.go",
    "content": "// Package build supports building and testing Encore applications\n// with codegen and rewrite overlays.\npackage build\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/token\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/mod/modfile\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/internal/etrace\"\n\tbuilderpkg \"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/pkg/xos\"\n\t\"encr.dev/v2/internals/overlay\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\ntype Config struct {\n\t// Ctx controls the build.\n\tCtx *parsectx.Context\n\n\t// Overlays describes the code generation overlays to apply,\n\t// in the form of rewritten files or generated files.\n\tOverlays []overlay.File\n\n\t// KeepOutput keeps the temporary build directory from being deleted in the case of failure.\n\tKeepOutput bool\n\n\t// Env are additional environment variables to set.\n\tEnv []string\n\n\t// MainPkg is the main package to build.\n\tMainPkg paths.Pkg\n\n\t// NoBinary specifies that no binary should be built.\n\t// It's used if MainPkg specifies multiple packages,\n\t// which for example is the case when checking for compilation errors\n\t// without building a binary (such as during tests).\n\tNoBinary bool\n\n\t// StaticConfig is the static config to embed into the binary.\n\tStaticConfig *config.Static\n}\n\ntype Result struct {\n\tDir paths.FS\n\tExe paths.FS\n}\n\nfunc Build(ctx context.Context, cfg *Config) *Result {\n\tb := &builder{\n\t\tctx:  ctx,\n\t\tcfg:  cfg,\n\t\tmode: buildMode,\n\t\terrs: cfg.Ctx.Errs,\n\t}\n\treturn b.Build()\n}\n\n// Mode is the build mode.\ntype mode string\n\nconst (\n\tbuildMode mode = \"build\"\n\ttestMode  mode = \"test\"\n)\n\ntype builder struct {\n\t// inputs\n\tctx  context.Context\n\tcfg  *Config\n\tmode mode\n\n\t// internal state\n\n\t// errs is the error list to use.\n\terrs *perr.List\n\n\t// overlayPath is set when the overlay file is written.\n\toverlayPath paths.FS\n\n\tworkdir paths.FS\n\t// deleteWorkDir reports whether the workdir should be deleted.\n\t// It's true for temporarily generated workdirs.\n\ttempWorkDir bool\n}\n\nfunc (b *builder) Build() *Result {\n\tworkdir, tempWorkDir := b.prepareWorkDir()\n\tb.workdir = workdir\n\n\tif tempWorkDir {\n\t\tdefer func() {\n\t\t\t// If we have a bailout or any errors, delete the workdir.\n\t\t\tif _, ok := perr.CatchBailout(recover()); ok || b.errs.Len() > 0 {\n\t\t\t\tif !b.cfg.KeepOutput && workdir != \"\" {\n\t\t\t\t\t_ = os.RemoveAll(workdir.ToIO())\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\tres := &Result{\n\t\tDir: b.workdir,\n\t\tExe: b.binaryPath(),\n\t}\n\n\tfor _, fn := range []func(){\n\t\tb.writeModFile,\n\t\tb.buildMain,\n\t} {\n\t\tfn()\n\t\t// Abort early if we encountered any errors.\n\t\tif b.errs.Len() > 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (b *builder) writeModFile() {\n\tetrace.Sync0(b.ctx, \"\", \"writeModFile\", func(ctx context.Context) {\n\t\tnewPath := b.cfg.Ctx.Build.EncoreRuntime.ToIO()\n\t\toldPath := \"encore.dev\"\n\n\t\tmodFilePath := b.cfg.Ctx.MainModuleDir.Join(\"go.mod\")\n\t\tsumFilePath := b.cfg.Ctx.MainModuleDir.Join(\"go.sum\")\n\t\tmodData, err := os.ReadFile(modFilePath.ToIO())\n\t\tif err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to read go.mod: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tmainMod, err := modfile.Parse(\"go.mod\", modData, nil)\n\t\tif err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to parse go.mod: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// Make sure there's a dependency on encore.dev so it can be replaced.\n\t\tif err := mainMod.AddRequire(\"encore.dev\", \"v0.0.0\"); err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to add 'require encore.dev' directive to go.mod: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif err := mainMod.AddReplace(oldPath, \"\", newPath, \"\"); err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to add 'replace encore.dev' directive to go.mod: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// We require Go 1.18+ now that we use generics in code gen.\n\t\tif !isGo118Plus(mainMod) {\n\t\t\t_ = mainMod.AddGoStmt(\"1.18\")\n\t\t}\n\t\tmainMod.Cleanup()\n\n\t\tdata := modfile.Format(mainMod.Syntax)\n\n\t\tgomodpath := b.workdir.Join(\"go.mod\")\n\t\tgosumpath := b.workdir.Join(\"go.sum\")\n\t\tif err := xos.WriteFile(gomodpath.ToIO(), data, 0o644); err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to write go.mod: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// Write an initial overlay for use with 'go mod tidy' when writing the go.mod file.\n\t\toverlayFiles := slices.Clone(b.cfg.Overlays)\n\t\toverlayPath, err := overlay.Write(b.workdir, overlayFiles)\n\t\tif err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to write overlay file: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// Run `go mod tidy` on this modfile.\n\t\t{\n\n\t\t\tbuild := b.cfg.Ctx.Build\n\t\t\tgoroot := build.GOROOT\n\t\t\tcmd := exec.Command(goroot.Join(\"bin\", \"go\"+b.exe()).ToIO(),\n\t\t\t\t\"mod\", \"tidy\",\n\t\t\t\t\"-overlay=\"+overlayPath.ToIO(),\n\t\t\t\t\"-modfile=\"+gomodpath.ToIO(),\n\t\t\t)\n\n\t\t\t// Copy the env before we add additional env vars\n\t\t\t// to avoid accidentally sharing the same backing array.\n\t\t\tenv := make([]string, len(b.cfg.Env))\n\t\t\tcopy(env, b.cfg.Env)\n\t\t\tenv = append(env,\n\t\t\t\t\"GO111MODULE=on\",\n\t\t\t\t\"GOROOT=\"+goroot.ToIO(),\n\t\t\t\t\"GOTOOLCHAIN=local\",\n\t\t\t)\n\t\t\tif goos := build.GOOS; goos != \"\" {\n\t\t\t\tenv = append(env, \"GOOS=\"+goos)\n\t\t\t}\n\t\t\tif goarch := build.GOARCH; goarch != \"\" {\n\t\t\t\tenv = append(env, \"GOARCH=\"+goarch)\n\t\t\t}\n\t\t\tif !build.CgoEnabled {\n\t\t\t\tenv = append(env, \"CGO_ENABLED=0\")\n\t\t\t}\n\n\t\t\tcmd.Env = append(os.Environ(), env...)\n\t\t\tcmd.Dir = b.cfg.Ctx.MainModuleDir.ToIO()\n\n\t\t\tout, err := cmd.CombinedOutput()\n\t\t\tif err != nil {\n\t\t\t\tif len(out) == 0 {\n\t\t\t\t\tout = []byte(err.Error())\n\t\t\t\t}\n\t\t\t\tout = convertCompileErrors(b.errs, out, b.workdir.ToIO(), b.cfg.Ctx.MainModuleDir.ToIO(), b.cfg.Ctx.MainModuleDir.ToIO())\n\t\t\t\tif len(out) > 0 {\n\t\t\t\t\t// HACK(andre): Make this nicer\n\t\t\t\t\tb.errs.AddStd(fmt.Errorf(\"'go mod tidy' failed: %s\", out))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the go.mod and go.sum files to the overlay, and write a new overlay file.\n\t\toverlayFiles = append(overlayFiles, overlay.File{\n\t\t\tSource: modFilePath,\n\t\t\tDest:   gomodpath,\n\t\t}, overlay.File{\n\t\t\tSource: sumFilePath,\n\t\t\tDest:   gosumpath,\n\t\t})\n\t\tb.overlayPath, err = overlay.Write(b.workdir, overlayFiles)\n\t\tif err != nil {\n\t\t\tb.errs.Addf(token.NoPos, \"unable to write overlay file: %v\", err)\n\t\t\treturn\n\t\t}\n\t})\n}\n\nfunc (b *builder) buildMain() {\n\tetrace.Sync0(b.ctx, \"\", \"buildMain\", func(ctx context.Context) {\n\t\tbuild := b.cfg.Ctx.Build\n\t\ttags := append([]string{\"encore\", \"encore_internal\", \"encore_app\"}, build.BuildTags...)\n\t\targs := []string{\n\t\t\t\"build\",\n\t\t\t\"-tags=\" + strings.Join(tags, \",\"),\n\t\t\t\"-overlay=\" + b.overlayPath.ToIO(),\n\t\t}\n\n\t\tif !b.cfg.NoBinary {\n\t\t\targs = append(args, \"-o=\"+b.binaryPath().ToIO())\n\t\t}\n\n\t\tvar ldflags strings.Builder\n\t\tb.writeStaticConfig(&ldflags)\n\n\t\tif b.cfg.Ctx.Build.StaticLink {\n\t\t\t// Enable external linking if we use cgo.\n\t\t\tif b.cfg.Ctx.Build.CgoEnabled {\n\t\t\t\tldflags.WriteString(\" -linkmode external\")\n\t\t\t}\n\n\t\t\tldflags.WriteString(` -extldflags \"-static\"`)\n\t\t}\n\t\targs = append(args, \"-ldflags\", ldflags.String())\n\n\t\tif b.cfg.Ctx.Build.Debug > builderpkg.DebugModeDisabled {\n\t\t\t// Disable inlining for better debugging.\n\t\t\targs = append(args, \"-gcflags\", \"all=-N -l\")\n\t\t}\n\n\t\targs = append(args, b.cfg.MainPkg.String())\n\t\t\n\t\tgoroot := build.GOROOT\n\t\tcmd := exec.Command(goroot.Join(\"bin\", \"go\"+b.exe()).ToIO(), args...)\n\n\t\t// Copy the env before we add additional env vars\n\t\t// to avoid accidentally sharing the same backing array.\n\t\tenv := make([]string, len(b.cfg.Env))\n\t\tcopy(env, b.cfg.Env)\n\n\t\tenv = append(env,\n\t\t\t\"GO111MODULE=on\",\n\t\t\t\"GOROOT=\"+goroot.ToIO(),\n\t\t\t\"GOTOOLCHAIN=local\",\n\t\t)\n\t\tif goos := build.GOOS; goos != \"\" {\n\t\t\tenv = append(env, \"GOOS=\"+goos)\n\t\t}\n\t\tif goarch := build.GOARCH; goarch != \"\" {\n\t\t\tenv = append(env, \"GOARCH=\"+goarch)\n\t\t}\n\t\tif !build.CgoEnabled {\n\t\t\tenv = append(env, \"CGO_ENABLED=0\")\n\t\t}\n\t\tcmd.Env = append(os.Environ(), env...)\n\t\tcmd.Dir = b.cfg.Ctx.MainModuleDir.ToIO()\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tif len(out) == 0 {\n\t\t\t\tout = []byte(err.Error())\n\t\t\t}\n\t\t\tout = convertCompileErrors(b.errs, out, b.workdir.ToIO(), b.cfg.Ctx.MainModuleDir.ToIO(), b.cfg.Ctx.MainModuleDir.ToIO())\n\t\t\tif len(out) > 0 {\n\t\t\t\t// HACK(andre): Make this nicer\n\t\t\t\tb.errs.AddStd(fmt.Errorf(\"compilation failure: %s\", out))\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc (b *builder) writeStaticConfig(ldflags *strings.Builder) {\n\t// Marshal the static config and add it as a linker flag.\n\tldflags.WriteString(\"-X 'encore.dev/appruntime/shared/appconf.static=\")\n\tdata, err := json.Marshal(b.cfg.StaticConfig)\n\tif err != nil {\n\t\tb.errs.Fatalf(token.NoPos, \"unable to marshal static config: %v\", err)\n\t}\n\tldflags.WriteString(base64.StdEncoding.EncodeToString(data))\n\tldflags.WriteByte('\\'')\n}\n\nconst BinaryName = \"encore_app_out\"\n\nfunc (b *builder) exe() string {\n\tgoos := b.cfg.Ctx.Build.GOOS\n\tif goos == \"\" {\n\t\tgoos = runtime.GOOS\n\t}\n\tif goos == \"windows\" {\n\t\treturn \".exe\"\n\t}\n\treturn \"\"\n}\n\nfunc (b *builder) binaryPath() paths.FS {\n\treturn b.workdir.Join(BinaryName + b.exe())\n}\n\n// convertCompileErrors goes through the errors and converts basic compiler errors into\n// ErrInSrc errors, which are more useful for the user.\nfunc convertCompileErrors(errList *perr.List, out []byte, workdir, appRoot, relwd string) []byte {\n\twdroot := filepath.Join(appRoot, relwd)\n\tlines := bytes.Split(out, []byte{'\\n'})\n\tprefix := append([]byte(workdir), '/')\n\tmodified := false\n\n\toutput := make([][]byte, 0)\n\n\tfor _, line := range lines {\n\t\tif !bytes.HasPrefix(line, prefix) {\n\t\t\toutput = append(output, line)\n\t\t\tcontinue\n\t\t}\n\t\tidx := bytes.IndexByte(line, ':')\n\t\tif idx == -1 || idx < len(prefix) {\n\t\t\toutput = append(output, line)\n\t\t\tcontinue\n\t\t}\n\n\t\tfilename := line[:idx]\n\t\tappPath := filepath.Join(appRoot, string(filename[len(prefix):]))\n\t\tif _, err := filepath.Rel(wdroot, appPath); err == nil {\n\t\t\tparts := strings.SplitN(string(line), \":\", 4)\n\t\t\tif len(parts) != 4 {\n\t\t\t\toutput = append(output, line)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlineNumber, err := strconv.Atoi(parts[1])\n\t\t\tif err != nil {\n\t\t\t\toutput = append(output, line)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcolNumber, err := strconv.Atoi(parts[2])\n\t\t\tif err != nil {\n\t\t\t\toutput = append(output, line)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmodified = true\n\t\t\terrList.AddStd(srcerrors.GenericGoCompilerError(changeToAppRootFile(parts[0], workdir, appRoot), lineNumber, colNumber, parts[3]))\n\t\t} else {\n\t\t\toutput = append(output, line)\n\t\t}\n\t}\n\n\tif !modified {\n\t\treturn out\n\t}\n\n\t// Append the err list for both the workDir and the appRoot\n\t// as files might be coming from either of them\n\terrList.MakeRelative(workdir, relwd)\n\terrList.MakeRelative(appRoot, relwd)\n\n\treturn bytes.Join(output, []byte{'\\n'})\n}\n\n// changeToAppRootFile will return the compiledFile path inside the appRoot directory\n// if that file exists within the app root. Otherwise it will return the original\n// compiledFile path.\n//\n// This means when we display compiler errors to the user, we will show them their\n// original rewritten code, where line numbers and column numbers will align with\n// their own code.\n//\n// However for generated files which don't exist in their own folders, we will\n// still be able to render the source causing the issue\nfunc changeToAppRootFile(compiledFile string, workDirectory, appRoot string) string {\n\tif strings.HasPrefix(compiledFile, workDirectory) {\n\t\tfileInOriginalSrc := strings.TrimPrefix(compiledFile, workDirectory)\n\t\tfileInOriginalSrc = path.Join(appRoot, fileInOriginalSrc)\n\n\t\tif _, err := os.Stat(fileInOriginalSrc); err == nil {\n\t\t\treturn fileInOriginalSrc\n\t\t}\n\t}\n\n\treturn compiledFile\n}\n\nfunc isGo118Plus(f *modfile.File) bool {\n\tif f.Go == nil {\n\t\treturn false\n\t}\n\tm := modfile.GoVersionRE.FindStringSubmatch(f.Go.Version)\n\tif m == nil {\n\t\treturn false\n\t}\n\tmajor, _ := strconv.Atoi(m[1])\n\tminor, _ := strconv.Atoi(m[2])\n\treturn major > 1 || (major == 1 && minor >= 18)\n}\n\nfunc (b *builder) prepareWorkDir() (workdir paths.FS, temporary bool) {\n\twork, isTemp := (func() (string, bool) {\n\t\t// If we have an appID, use a persistent work dir.\n\t\tif appID, ok := b.cfg.Ctx.AppID.Get(); ok {\n\t\t\tbaseDir, err := os.UserCacheDir()\n\t\t\tif err != nil {\n\t\t\t\tb.errs.Fatalf(token.NoPos, \"unable to get user cache dir: %v\", err)\n\t\t\t}\n\t\t\t// Use a per-mode work directory since the generated files are different.\n\t\t\t// Otherwise concurrent build and test runs interfere and cause spurious compilation errors.\n\t\t\tworkdir := filepath.Join(baseDir, \"encore-build\", appID, string(b.mode))\n\t\t\treturn workdir, false\n\t\t}\n\n\t\ttmp, err := os.MkdirTemp(\"\", \"encore-build\")\n\t\tif err != nil {\n\t\t\tb.errs.Fatalf(token.NoPos, \"unable to create workdir: %v\", err)\n\t\t}\n\t\treturn tmp, true\n\t})()\n\n\t// NOTE(andre): There appears to be a bug in go's handling of overlays\n\t// when the source or destination is a symlink.\n\t// I haven't dug into the root cause exactly, but it causes weird issues\n\t// with tests since macOS's /var/tmp is a symlink to /private/var/tmp.\n\tif d, err := filepath.EvalSymlinks(work); err == nil {\n\t\twork = d\n\t}\n\n\tif err := os.MkdirAll(work, 0o755); err != nil {\n\t\tb.errs.Fatalf(token.NoPos, \"unable to create workdir: %v\", err)\n\t}\n\n\treturn paths.RootedFSPath(work, \".\"), isTemp\n}\n"
  },
  {
    "path": "v2/compiler/build/build_test.go",
    "content": "package build\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/token\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\t\"github.com/rs/zerolog\"\n\n\tbuilderpkg \"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/overlay\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestMain(m *testing.M) {\n\tos.Exit(testscript.RunMain(m, map[string]func() int{\n\t\t\"run\": run,\n\t}))\n}\n\nfunc TestBuild(t *testing.T) {\n\t// Get existing go cache, if any\n\tgocache, err := exec.Command(\"go\", \"env\", \"GOCACHE\").CombinedOutput()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgocache = bytes.TrimSpace(gocache)\n\tif len(gocache) == 0 {\n\t\tgocache = []byte(t.TempDir())\n\t}\n\n\ttestscript.Run(t, testscript.Params{\n\t\tDir: \"testdata\",\n\t\tSetup: func(env *testscript.Env) error {\n\t\t\tenv.Setenv(testutil.EnvRepoDirOverride, testutil.EncoreRepoDir)\n\t\t\tenv.Setenv(\"GOCACHE\", string(gocache))\n\t\t\tfsPath := paths.RootedFSPath(env.WorkDir, \".\")\n\t\t\toverlays, err := processOverlays(fsPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tjsonOverlays, _ := json.Marshal(overlays)\n\t\t\tenv.Setenv(\"CODEGEN_OVERLAYS\", string(jsonOverlays))\n\t\t\treturn nil\n\t\t},\n\t})\n}\n\nfunc run() int {\n\tif len(os.Args) != 2 {\n\t\tlog.Fatal(\"usage: run <pkg>\")\n\t}\n\n\tvar overlays []overlay.File\n\tif desc := os.Getenv(\"CODEGEN_OVERLAYS\"); desc != \"\" {\n\t\tif err := json.Unmarshal([]byte(desc), &overlays); err != nil {\n\t\t\tlog.Fatalf(\"failed to unmarshal CODEGEN_OVERLAYS: %v\", err)\n\t\t}\n\t}\n\n\twork := os.Getenv(\"WORK\")\n\tres := build(work, paths.MustPkgPath(os.Args[1]), overlays)\n\tcmd := exec.Command(res.Exe.ToIO())\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc build(workdir string, pkgPath paths.Pkg, overlays []overlay.File) *Result {\n\truntimeArchive := testutil.ParseTxtar(dummyEncoreRuntime)\n\tif err := txtar.Write(runtimeArchive, workdir); err != nil {\n\t\tlog.Fatalf(\"failed to write runtime archive: %v\", err)\n\t}\n\twd := paths.RootedFSPath(workdir, \".\")\n\n\tctx := context.Background()\n\tfs := token.NewFileSet()\n\terrs := perr.NewList(ctx, fs)\n\tpc := &parsectx.Context{\n\t\tCtx: ctx,\n\t\tLog: zerolog.New(zerolog.NewConsoleWriter()).Level(zerolog.InfoLevel),\n\t\tBuild: parsectx.BuildInfo{\n\t\t\tExperiments: nil,\n\t\t\tGOARCH:      runtime.GOARCH,\n\t\t\tGOOS:        runtime.GOOS,\n\t\t\tGOROOT:      paths.RootedFSPath(runtime.GOROOT(), \".\"),\n\t\t\t// HACK(andre): Make this nicer\n\t\t\tEncoreRuntime: wd.Join(\"encore-runtime\"),\n\t\t\tBuildTags:     nil,\n\t\t\tCgoEnabled:    false,\n\t\t\tStaticLink:    false,\n\t\t\tDebug:         builderpkg.DebugModeDisabled,\n\t\t},\n\t\tFS:            fs,\n\t\tParseTests:    false,\n\t\tErrs:          errs,\n\t\tMainModuleDir: wd,\n\t}\n\n\tres := Build(ctx, &Config{\n\t\tCtx:        pc,\n\t\tOverlays:   overlays,\n\t\tMainPkg:    pkgPath,\n\t\tKeepOutput: false,\n\t})\n\tif errs.Len() > 0 {\n\t\tlog.Fatalf(\"build failed: %s\", errs.FormatErrors())\n\t}\n\treturn res\n}\n\nconst dummyEncoreRuntime = `\n-- encore-runtime/go.mod --\nmodule encore.dev\n-- encore-runtime/go.sum --\n`\n\nfunc processOverlays(workdir paths.FS) ([]overlay.File, error) {\n\tvar overlays []overlay.File\n\n\tfiles, _ := os.ReadDir(workdir.ToIO())\n\tfor _, f := range files {\n\t\tif f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := f.Name()\n\t\toverlayPath, ok := strings.CutPrefix(name, \"overlay:\")\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\toverlayPath = strings.ReplaceAll(overlayPath, \"\\\\\", \"/\")\n\n\t\tsrc := workdir.Join(filepath.FromSlash(overlayPath))\n\t\tdst := workdir.Join(name)\n\n\t\tcontents, err := os.ReadFile(dst.ToIO())\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\toverlays = append(overlays, overlay.File{\n\t\t\tSource:   src,\n\t\t\tContents: contents,\n\t\t})\n\n\t\tif err := os.Remove(dst.ToIO()); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to delete file: %v\", err)\n\t\t}\n\t}\n\n\treturn overlays, nil\n}\n"
  },
  {
    "path": "v2/compiler/build/errors.go",
    "content": "package build\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\"test\", \"\", errors.WithRangeSize(20))\n\n\tErrTestFailed = errRange.New(\"Test Failure\", \"One or more more tests failed.\")\n)\n"
  },
  {
    "path": "v2/compiler/build/testdata/basic.txt",
    "content": "run example.com\ncmp stderr expected_output\n\n-- main.go --\npackage main\nfunc main() {\n    println(\"Hello, world!\")\n}\n-- go.mod --\nmodule example.com\n\n-- expected_output --\nHello, world!\n"
  },
  {
    "path": "v2/compiler/build/testdata/overlay.txt",
    "content": "run example.com\ncmp stderr expected_output\n\n-- main.go --\npackage main\nfunc main() {\n}\n-- overlay:main.go --\npackage main\nfunc main() {\n    OverlayPrint() // defined in overlay\n}\n-- overlay:print.go --\npackage main\nimport (\"fmt\"; \"os\")\nfunc OverlayPrint() {\n    fmt.Fprintln(os.Stderr, \"Hello, overlay world!\")\n}\n-- go.mod --\nmodule example.com\n\n-- expected_output --\nHello, overlay world!\n"
  },
  {
    "path": "v2/compiler/build/testdata/rewrite.txt",
    "content": "run example.com\ncmp stderr expected_output\n\n-- main.go --\npackage main\nfunc main() {\n    println(\"Hello, world!\")\n}\n-- overlay:main.go --\npackage main\nfunc main() {\n    println(\"Hello, rewritten world!\")\n}\n-- go.mod --\nmodule example.com\n\n-- expected_output --\nHello, rewritten world!\n"
  },
  {
    "path": "v2/compiler/build/tests.go",
    "content": "// Package build supports building and testing Encore applications\n// with codegen and rewrite overlays.\npackage build\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/internal/etrace\"\n\tbuilderpkg \"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\ntype TestConfig struct {\n\tGenerateTestSpecConfig\n\tRunTestsConfig\n}\n\nfunc Test(ctx context.Context, cfg *TestConfig) {\n\tb := &builder{\n\t\tctx:  ctx,\n\t\tcfg:  &cfg.Config,\n\t\tmode: testMode,\n\t\terrs: cfg.Ctx.Errs,\n\t}\n\tspec := b.GenerateTestSpec(&cfg.GenerateTestSpecConfig)\n\tb.RunTests(spec, &cfg.RunTestsConfig)\n}\n\ntype TestSpec struct {\n\tCommand string\n\tArgs    []string\n\tEnviron []string\n\n\tcfg *Config\n}\n\ntype GenerateTestSpecConfig struct {\n\tConfig\n\n\t// Args are additional arguments to \"go test\".\n\tArgs []string\n}\n\nfunc GenerateTestSpec(ctx context.Context, cfg *GenerateTestSpecConfig) *TestSpec {\n\tb := &builder{\n\t\tctx:  ctx,\n\t\tcfg:  &cfg.Config,\n\t\tmode: testMode,\n\t\terrs: cfg.Ctx.Errs,\n\t}\n\treturn b.GenerateTestSpec(cfg)\n}\n\nfunc RunTests(ctx context.Context, spec *TestSpec, cfg *RunTestsConfig) {\n\tb := &builder{\n\t\tctx:  ctx,\n\t\tcfg:  spec.cfg,\n\t\tmode: testMode,\n\t\terrs: spec.cfg.Ctx.Errs,\n\t}\n\tb.RunTests(spec, cfg)\n}\n\ntype RunTestsConfig struct {\n\t// Stdout specifies the stdout to use.\n\tStdout io.Writer\n\n\t// Stderr specifies the stderr to use.\n\tStderr io.Writer\n\n\t// WorkingDir is the working directory to invoke\n\t// the \"go test\" command from.\n\tWorkingDir paths.FS\n}\n\nfunc (b *builder) RunTests(spec *TestSpec, testCfg *RunTestsConfig) {\n\tif b.cfg.KeepOutput && b.workdir != \"\" {\n\t\t_, _ = fmt.Fprintf(testCfg.Stdout, \"wrote generated code to: %s\\n\", b.workdir.ToIO())\n\t}\n\tb.runTests(spec, testCfg)\n}\n\nfunc (b *builder) GenerateTestSpec(cfg *GenerateTestSpecConfig) *TestSpec {\n\tworkdir, tempWorkDir := b.prepareWorkDir()\n\tb.workdir = workdir\n\n\tif tempWorkDir {\n\t\tdefer func() {\n\t\t\t// If we have a bailout or any errors, delete the workdir.\n\t\t\tif _, ok := perr.CatchBailout(recover()); ok || b.errs.Len() > 0 {\n\t\t\t\tif !b.cfg.KeepOutput && workdir != \"\" {\n\t\t\t\t\t_ = os.RemoveAll(workdir.ToIO())\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\tfor _, fn := range []func(){\n\t\tb.writeModFile,\n\t} {\n\t\tfn()\n\t\t// Abort early if we encountered any errors.\n\t\tif b.errs.Len() > 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn b.generateTestSpec(cfg)\n}\n\nfunc (b *builder) runTests(spec *TestSpec, testCfg *RunTestsConfig) {\n\tetrace.Sync0(b.ctx, \"\", \"runTests\", func(ctx context.Context) {\n\t\tcmd := exec.CommandContext(b.cfg.Ctx.Ctx, spec.Command, spec.Args...)\n\t\tcmd.Env = spec.Environ\n\t\tcmd.Dir = testCfg.WorkingDir.ToIO()\n\t\tcmd.Stdout = testCfg.Stdout\n\t\tcmd.Stderr = testCfg.Stderr\n\n\t\terr := cmd.Run()\n\t\tif err != nil {\n\t\t\tif err.Error() == \"exit status 1\" {\n\t\t\t\t// This is a standard error code for failed tests.\n\t\t\t\t// so we don't need to wrap it.\n\t\t\t\tb.errs.Add(ErrTestFailed)\n\t\t\t} else {\n\t\t\t\t// Otherwise we wrap the error.\n\t\t\t\tb.errs.Add(ErrTestFailed.Wrapping(err))\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc (b *builder) generateTestSpec(testCfg *GenerateTestSpecConfig) *TestSpec {\n\tbuild := b.cfg.Ctx.Build\n\ttags := append([]string{\"encore\", \"encore_internal\", \"encore_app\"}, build.BuildTags...)\n\targs := []string{\n\t\t\"test\",\n\t\t\"-tags=\" + strings.Join(tags, \",\"),\n\t\t\"-overlay=\" + b.overlayPath.ToIO(),\n\t\t\"-vet=off\",\n\t}\n\n\tvar ldflags strings.Builder\n\tb.writeStaticConfig(&ldflags)\n\n\tif b.cfg.Ctx.Build.StaticLink {\n\t\t// Enable external linking if we use cgo.\n\t\tif b.cfg.Ctx.Build.CgoEnabled {\n\t\t\tldflags.WriteString(\" -linkmode external\")\n\t\t}\n\n\t\tldflags.WriteString(` -extldflags \"-static\"`)\n\t}\n\targs = append(args, \"-ldflags\", ldflags.String())\n\n\tif b.cfg.Ctx.Build.Debug != builderpkg.DebugModeDisabled {\n\t\t// Disable inlining for better debugging.\n\t\targs = append(args, `-gcflags \"all=-N -l\"`)\n\t}\n\n\targs = append(args, testCfg.Args...)\n\n\tgoroot := build.GOROOT\n\n\t// Copy the env before we add additional env vars\n\t// to avoid accidentally sharing the same backing array.\n\tenv := make([]string, len(b.cfg.Env))\n\tcopy(env, b.cfg.Env)\n\n\tenv = append(env,\n\t\t\"GO111MODULE=on\",\n\t\t\"GOROOT=\"+goroot.ToIO(),\n\t\t\"GOTOOLCHAIN=local\",\n\t)\n\tif goos := build.GOOS; goos != \"\" {\n\t\tenv = append(env, \"GOOS=\"+goos)\n\t}\n\tif goarch := build.GOARCH; goarch != \"\" {\n\t\tenv = append(env, \"GOARCH=\"+goarch)\n\t}\n\tif !build.CgoEnabled {\n\t\tenv = append(env, \"CGO_ENABLED=0\")\n\t}\n\n\tenviron := append(os.Environ(), env...)\n\n\t// Get the last PATH= item\n\tvar originalPath string\n\tfor _, e := range environ {\n\t\tif path, ok := strings.CutPrefix(e, \"PATH=\"); ok {\n\t\t\toriginalPath = path\n\t\t}\n\t}\n\n\t// prefix PATH with encore-go, so it doesnt conflict with other installed go versions\n\t// if not set causes problems when running cover tests in go\n\tpath := goroot.Join(\"bin\").ToIO() + string(filepath.ListSeparator) + originalPath\n\tenviron = append(environ, \"PATH=\"+path)\n\n\treturn &TestSpec{\n\t\tCommand: goroot.Join(\"bin\", \"go\"+b.exe()).ToIO(),\n\t\tArgs:    args,\n\t\tEnviron: environ,\n\t\tcfg:     b.cfg,\n\t}\n}\n"
  },
  {
    "path": "v2/internals/overlay/overlay.go",
    "content": "package overlay\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/paths\"\n)\n\n// File describes a file to generate or rewrite.\ntype File struct {\n\t// Source is where on the filesystem the original file (in the case of a rewrite)\n\t// or where the generated file should be overlaid into.\n\tSource paths.FS\n\n\t// Contents are the file contents of the overlaid file.\n\tContents []byte\n\n\t// If set, overrides Contents and uses the dest directly instead.\n\tDest paths.FS\n}\n\n// Write writes the overlay files to the workdir\n// and generates an overlay.json file suitable for passing to 'go build -overlay'.\n// It returns the path to the written overlay.json file (which lives within workdir).\nfunc Write(workdir paths.FS, files []File) (overlayFile paths.FS, err error) {\n\toverlay := make(map[string]string) // src -> dst\n\tseenNames := make(map[string]bool)\n\n\taddFile := func(f File) error {\n\t\tsrc := f.Source.ToIO()\n\t\tif f.Dest != \"\" {\n\t\t\toverlay[src] = f.Dest.ToIO()\n\t\t\treturn nil\n\t\t}\n\n\t\tbaseName := \"gen_\" + filepath.Base(filepath.Dir(src)) + \"__\" + filepath.Base(src)\n\t\tif _, exists := overlay[src]; exists {\n\t\t\treturn fmt.Errorf(\"duplicate overlay of %s\", src)\n\t\t}\n\n\t\t// Compute a reasonable destination name for the file.\n\t\text := filepath.Ext(baseName)\n\t\tnameWithoutExt := strings.TrimSuffix(baseName, ext)\n\n\t\t// Keep generating names until we get one that doesn't conflict.\n\t\tcandidate := baseName\n\t\tfor i := 1; seenNames[candidate]; i++ {\n\t\t\tcandidate = fmt.Sprintf(\"%s_%d%s\", nameWithoutExt, i, ext)\n\t\t}\n\t\tseenNames[candidate] = true\n\t\tdst := workdir.Join(candidate)\n\n\t\t// Write the file.\n\t\toverlay[src] = dst.ToIO()\n\t\tif err := os.WriteFile(dst.ToIO(), f.Contents, 0644); err != nil {\n\t\t\treturn fmt.Errorf(\"write overlay file %s: %v\", dst, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor _, f := range files {\n\t\tif err := addFile(f); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// Compute the overlay.json data.\n\tdata, _ := json.Marshal(map[string]any{\"Replace\": overlay})\n\toverlayFile = workdir.Join(\"overlay.json\")\n\tif err := os.WriteFile(overlayFile.ToIO(), data, 0644); err != nil {\n\t\treturn \"\", fmt.Errorf(\"write overlay file %s: %v\", overlayFile, err)\n\t}\n\treturn overlayFile, nil\n}\n"
  },
  {
    "path": "v2/internals/parsectx/pctx.go",
    "content": "package parsectx\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"go/token\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"runtime/trace\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rs/xid\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\n// Context holds all the context for parsing.\ntype Context struct {\n\t// AppID is a unique id of the application, used for workdir caching.\n\t// If left empty, a random workdir is used.\n\tAppID option.Option[string]\n\n\t// Ctx provides cancellation.\n\tCtx context.Context\n\n\t// Log is the configured logger.\n\tLog zerolog.Logger\n\n\t// Build controls what files to build.\n\tBuild BuildInfo\n\n\t// MainModuleDir is the directory containing the main module.\n\tMainModuleDir paths.FS\n\n\t// FS holds the fileset used for parsing.\n\tFS *token.FileSet\n\n\t// ParseTests controls whether to parse test files.\n\tParseTests bool\n\n\t// Errs contains encountered errors.\n\tErrs *perr.List\n\n\t// Overlay is an optional replacement for reading files using the os pkg.\n\t// If unset, os is used instead.\n\tOverlay OverlaidOSFS\n}\n\nfunc (c *Context) ReadFile(dir string) ([]byte, error) {\n\tif c.Overlay == nil {\n\t\treturn os.ReadFile(dir)\n\t}\n\treturn c.Overlay.ReadFile(dir)\n}\n\nfunc (c *Context) ReadDir(dir string) ([]fs.DirEntry, error) {\n\tif c.Overlay == nil {\n\t\treturn os.ReadDir(dir)\n\t}\n\treturn c.Overlay.ReadDir(dir)\n}\n\nfunc (c *Context) ReadFileInfo(dir string) ([]fs.FileInfo, error) {\n\tentries, err := c.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn fns.MapErr(entries, func(entry fs.DirEntry) (fs.FileInfo, error) {\n\t\treturn entry.Info()\n\t})\n}\n\nfunc (c *Context) IsDir(path string) bool {\n\tstat := os.Stat\n\tif c.Overlay != nil {\n\t\tstat = c.Overlay.Stat\n\t}\n\tfi, err := stat(path)\n\treturn err == nil && fi.IsDir()\n}\n\nfunc (c *Context) OpenFile(file string) (io.ReadCloser, error) {\n\tif c.Overlay == nil {\n\t\treturn os.Open(file)\n\t}\n\treturn c.Overlay.Open(file)\n}\n\nfunc (c *Context) PkgOverlay() map[string][]byte {\n\tif c.Overlay == nil {\n\t\treturn nil\n\t}\n\treturn c.Overlay.PkgOverlay()\n}\n\n// OverlaidOSFS is an interface that allows overlaying the os package with custom implementations.\n// This is used to allow for e.g. in-memory files. The name parameters are os paths, like the\n// corresponding methods in the os package.\ntype OverlaidOSFS interface {\n\tReadDir(name string) ([]os.DirEntry, error)\n\tReadFile(name string) ([]byte, error)\n\tStat(name string) (os.FileInfo, error)\n\tOpen(name string) (io.ReadCloser, error)\n\tPkgOverlay() map[string][]byte\n}\n\n// BuildInfo represents the information needed to parse and build an Encore application.\ntype BuildInfo struct {\n\tGOARCH string   // target architecture\n\tGOOS   string   // target operating system\n\tGOROOT paths.FS // GOROOT to use\n\n\tEncoreRuntime paths.FS // Encore runtime to use\n\n\tBuildTags  []string // additional build tags to set\n\tCgoEnabled bool\n\n\t// Experiments are the enabled experiments.\n\tExperiments *experiments.Set\n\n\t// StaticLink enables static linking of C libraries.\n\tStaticLink bool\n\n\t// Debug enables compiling in debug mode.\n\tDebug builder.DebugMode\n\n\t// Revision specifies the revision of the build.\n\tRevision string\n\n\t// UncommittedChanges, if true, specifies there are uncommitted changes\n\t// part of the build .\n\tUncommittedChanges bool\n\n\t// MainPkg is the existing main package to use, if any.\n\t// If None a main package is generated.\n\tMainPkg option.Option[paths.Pkg]\n\n\t// DisableSensitiveScrubbing, if true, disables scrubbing of sensitive fields.\n\t// Used for local development.\n\tDisableSensitiveScrubbing bool\n}\n\n// Trace traces the execution of a function.\n// It emits trace-level log messages, using the given message and key-value pairs.\n// It returns the logger for logging additional information during the processing.\n//\n// Usage:\n//\n//\ttr := ctx.Trace(\"operation-name\", \"key\", value)\n//\t// ... invoke tr.Emit(...) to log additional information\n//\tdefer tr.Done()\nfunc (c *Context) Trace(op string, kvs ...any) *TraceLogger {\n\t// If we're not tracing, do nothing.\n\tif c.Log.GetLevel() > zerolog.TraceLevel && !trace.IsEnabled() {\n\t\treturn nil\n\t}\n\n\ttaskCtx, task := trace.NewTask(c.Ctx, op)\n\n\tlog := c.Log.With().Str(\"op\", op).Str(\"op_id\", \"op_\"+xid.New().String()).Logger()\n\tlog.Trace().Caller(1).Fields(kvs).Msg(\"start\")\n\tnow := time.Now()\n\treturn &TraceLogger{taskCtx: taskCtx, task: task, log: log, start: now, prev: now}\n}\n\ntype TraceLogger struct {\n\ttaskCtx context.Context\n\ttask    *trace.Task\n\tlog     zerolog.Logger\n\tstart   time.Time\n\tprev    time.Time\n}\n\nfunc (t *TraceLogger) Done(kvs ...any) {\n\tif t == nil {\n\t\treturn\n\t}\n\tt.emit(\"done\", kvs)\n\tt.task.End()\n}\n\nfunc (t *TraceLogger) Emit(msg string, kvs ...any) {\n\tif t == nil {\n\t\treturn\n\t}\n\tt.emit(msg, kvs)\n\n\t// Write to the trace log if tracing is enabled.\n\tif trace.IsEnabled() {\n\t\tvar logMsg strings.Builder\n\t\tlogMsg.WriteString(msg)\n\t\tfor i := 0; i < len(kvs)/2; i++ {\n\t\t\tkey := kvs[2*i]\n\t\t\tvalue := kvs[2*i+1]\n\t\t\tfmt.Fprintf(&logMsg, \" %v=%v\", key, value)\n\t\t}\n\n\t\ttrace.Log(t.taskCtx, \"\", logMsg.String())\n\t}\n}\n\nfunc (t *TraceLogger) emit(msg string, kvs []any) {\n\tnow := time.Now()\n\tt.prev = now\n\tt.log.Trace().\n\t\tCaller(1).\n\t\tDur(\"from_start\", now.Sub(t.start)).\n\t\tDur(\"from_prev\", now.Sub(t.prev)).\n\t\tFields(kvs).\n\t\tMsg(msg)\n}\n"
  },
  {
    "path": "v2/internals/perr/aserror.go",
    "content": "package perr\n\nimport (\n\t\"fmt\"\n\n\t\"encr.dev/pkg/errinsrc\"\n)\n\n// ListAsErr is a wrapper around a List that implements the error interface\n// allowing us to return a List as an error from functions that parse or compile\n// an application.\n//\n// We've not implemented Error on List directly because we want to avoid accidentally\n// returning a List as an error, and want to be explicit about it.\ntype ListAsErr struct {\n\tprefix string\n\tlist   *List\n}\n\nvar (\n\t_ error              = (*ListAsErr)(nil)\n\t_ errinsrc.ErrorList = (*ListAsErr)(nil) // We implement this to maintain compatibility with errinsrc detection within the Encore Platform\n)\n\n// Error returns the list of errors formatted as a single string.\nfunc (r *ListAsErr) Error() string {\n\tif r.prefix != \"\" {\n\t\treturn fmt.Sprintf(\"%s: %s\", r.prefix, r.list.FormatErrors())\n\t}\n\n\treturn r.list.FormatErrors()\n}\n\n// Unwrap returns the list of errors that make up this error.\n//\n// Note: This version of Unwrap is a Go 1.20+ feature\n//\n//goland:noinspection GoStandardMethods\nfunc (r *ListAsErr) Unwrap() []error {\n\trtn := make([]error, len(r.list.errs))\n\tfor i, err := range r.list.errs {\n\t\trtn[i] = err\n\t}\n\treturn rtn\n}\n\n// As implements the As method of the error interface.\n//\n// It supports the following types:\n//   - **ListAsErr\n//   - **List\nfunc (r *ListAsErr) As(err any) bool {\n\tswitch err := err.(type) {\n\tcase **ListAsErr:\n\t\t*err = r\n\tcase **List:\n\t\t*err = r.list\n\tdefault:\n\t\treturn false\n\t}\n\treturn true\n}\n\n// ErrorList returns the list of errors in the source that make up this error.\nfunc (r *ListAsErr) ErrorList() []*errinsrc.ErrInSrc {\n\treturn r.list.errs\n}\n"
  },
  {
    "path": "v2/internals/perr/perr.go",
    "content": "// Package perr provides utilities for handling parse errors.\npackage perr\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/scanner\"\n\t\"go/token\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"golang.org/x/tools/go/packages\"\n\n\t\"encr.dev/pkg/errinsrc\"\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\t\"encr.dev/pkg/errlist\"\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\tdaemonpb \"encr.dev/proto/encore/daemon\"\n)\n\n// NewList constructs a new list.\n//\n// It takes a ctx to add an error on context cancellation\n// since code often uses ctx cancellation to cause a bailout.\nfunc NewList(ctx context.Context, fset *token.FileSet, fileReaders ...paths.FileReader) *List {\n\treturn &List{ctx: ctx, fset: fset, fileReaders: fileReaders}\n}\n\n// List is a list of errors.\n// The same instance is shared between different components.\ntype List struct {\n\tctx         context.Context\n\tfset        *token.FileSet\n\tfileReaders []paths.FileReader\n\n\tmu   sync.Mutex\n\terrs errinsrc.List\n}\n\n// AsError returns this list an error if there are\n// errors in the list, otherwise it returns nil.\nfunc (l *List) AsError() error {\n\tif l.Len() == 0 {\n\t\treturn nil\n\t}\n\n\treturn &ListAsErr{list: l}\n}\n\n// Add adds a templated error\nfunc (l *List) Add(template errors.Template) {\n\tl.add(errinsrc.FromTemplate(template, l.fset, l.fileReaders...))\n}\n\n// Add adds an error at the given pos.\nfunc (l *List) AddPos(pos token.Pos, msg string) {\n\tl.add(srcerrors.GenericError(l.fset.Position(pos), msg, l.fileReaders...))\n}\n\n// Addf is equivalent to l.Add(pos, fmt.Sprintf(format, args...))\nfunc (l *List) Addf(pos token.Pos, format string, args ...any) {\n\tl.AddPos(pos, fmt.Sprintf(format, args...))\n}\n\n// AddStd adds an error from the stdlib packages that uses\n// scanner.ErrorList or *scanner.Error under the hood.\nfunc (l *List) AddStd(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\tswitch err := err.(type) {\n\tcase *errinsrc.ErrInSrc:\n\t\tl.add(err)\n\n\tcase errinsrc.ErrorList:\n\t\tfor _, e := range err.ErrorList() {\n\t\t\tl.add(e)\n\t\t}\n\n\tcase *scanner.Error:\n\t\tl.add(srcerrors.GenericGoParserError(err, l.fileReaders...))\n\n\tcase scanner.ErrorList:\n\t\tfor _, e := range err {\n\t\t\tl.add(srcerrors.GenericGoParserError(e, l.fileReaders...))\n\t\t}\n\n\tcase packages.Error:\n\t\tl.add(srcerrors.GenericGoPackageError(err, l.fileReaders...))\n\n\tdefault:\n\t\tl.add(srcerrors.StandardLibraryError(err))\n\t}\n}\n\nfunc (l *List) AddStdNode(err error, node ast.Node) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\tadd := func(err *errinsrc.ErrInSrc) {\n\t\terr.WithGoNode(l.fset, node, l.fileReaders...)\n\t\tl.add(err)\n\t}\n\n\tswitch err := err.(type) {\n\tcase *errinsrc.ErrInSrc:\n\t\tadd(err)\n\n\tcase errinsrc.ErrorList:\n\t\tfor _, e := range err.ErrorList() {\n\t\t\tadd(e)\n\t\t}\n\n\tcase *scanner.Error:\n\t\tadd(srcerrors.GenericGoParserError(err, l.fileReaders...))\n\n\tcase scanner.ErrorList:\n\t\tfor _, e := range err {\n\t\t\tadd(srcerrors.GenericGoParserError(e, l.fileReaders...))\n\t\t}\n\n\tcase packages.Error:\n\t\tadd(srcerrors.GenericGoPackageError(err, l.fileReaders...))\n\n\tdefault:\n\t\tadd(srcerrors.StandardLibraryError(err))\n\t}\n}\n\nfunc (l *List) Fatal(pos token.Pos, msg string) {\n\tl.AddPos(pos, msg)\n\tl.Bailout()\n}\n\nfunc (l *List) Fatalf(pos token.Pos, format string, args ...any) {\n\tl.Addf(pos, format, args...)\n\tl.Bailout()\n}\n\nfunc (l *List) Assert(template errors.Template) {\n\tl.Add(template)\n\tl.Bailout()\n}\n\nfunc (l *List) AssertStd(err error) {\n\tif err != nil {\n\t\tl.AddStd(err)\n\t\tl.Bailout()\n\t}\n}\n\n// Len returns the number of errors reported.\nfunc (l *List) Len() int {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tn := len(l.errs)\n\n\tif err := l.ctx.Err(); err != nil {\n\t\tn++\n\t}\n\treturn n\n}\n\n// At returns the i'th error. i must be 0 <= i < l.Len().\nfunc (l *List) At(i int) *errinsrc.ErrInSrc {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\treturn l.errs[i]\n}\n\nfunc (l *List) FS() *token.FileSet {\n\treturn l.fset\n}\n\nfunc (l *List) add(e *errinsrc.ErrInSrc) {\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tl.errs = append(l.errs, e)\n}\n\n// FormatErrors formats the errors as a newline-separated string.\n// If there are no errors it returns \"no errors\".\nfunc (l *List) FormatErrors() string {\n\tif n := l.Len(); n == 0 {\n\t\treturn \"no errors\"\n\t}\n\n\tvar b strings.Builder\n\tfor _, err := range l.errs {\n\t\tfmt.Fprintf(&b, \"%s\\n\", err.Error())\n\t}\n\tif err := l.ctx.Err(); err != nil {\n\t\tfmt.Fprintf(&b, \"%s\\n\", err.Error())\n\t}\n\treturn b.String()\n}\n\nfunc (l *List) GoString() string {\n\treturn \"&perr.List{...}\"\n}\n\n// MakeRelative rewrites the errors by making filenames within the\n// app root relative to the relwd (which must be a relative path\n// within the root).\nfunc (l *List) MakeRelative(root, relwd string) {\n\twdroot := filepath.Join(root, relwd)\n\tfor _, e := range l.errs {\n\t\tfor _, loc := range e.Params.Locations {\n\t\t\tif loc.File != nil {\n\t\t\t\tfn := loc.File.RelPath\n\t\t\t\tif strings.HasPrefix(fn, root) {\n\t\t\t\t\tif rel, err := filepath.Rel(wdroot, fn); err == nil {\n\t\t\t\t\t\tloc.File.RelPath = rel\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// SendToStream sends a GRPC command with this\n// full errlist\n//\n// If l is nil or empty, it sends a nil command\n// allowing the client to know that there are no\n// longer an error present\nfunc (l *List) SendToStream(stream interface {\n\tSend(*daemonpb.CommandMessage) error\n}) error {\n\tvar bytes []byte\n\tif l != nil && len(l.errs) > 0 {\n\t\t// TODO: For now we use the older errlist format in JSON to preserve\n\t\t// backward compatibility with the daemon while it could receive\n\t\t// errors from v1 or v2.\n\t\t//\n\t\t// Once we remove v1, we can remove this shim\n\t\terrList := errlist.New(l.fset)\n\t\terrList.List = l.errs\n\n\t\tvar err error\n\t\tbytes, err = json.Marshal(errList)\n\t\tif err != nil {\n\t\t\tpanic(\"unable to marshal error list\")\n\t\t}\n\t}\n\treturn stream.Send(\n\t\t&daemonpb.CommandMessage{\n\t\t\tMsg: &daemonpb.CommandMessage_Errors{\n\t\t\t\tErrors: &daemonpb.CommandDisplayErrors{\n\t\t\t\t\tErrinsrc: bytes,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t)\n}\n\n// BailoutOnErrors calls fn and bailouts if fn reports any errors.\nfunc (l *List) BailoutOnErrors(fn func()) {\n\tn := l.Len()\n\tfn()\n\tif l.Len() > n {\n\t\tl.Bailout()\n\t}\n}\n\nfunc (l *List) Bailout() {\n\tpanic(bailout{l})\n}\n\ntype bailout struct{ l *List }\n\nfunc (b bailout) GoString() string {\n\treturn fmt.Sprintf(\"perr.bailout: %s\", b.l.FormatErrors())\n}\n\nfunc (b bailout) String() string {\n\treturn fmt.Sprintf(\"perr.bailout: %s\", b.l.FormatErrors())\n}\n\n// CatchBailout catches a bailout panic and reports whether there was one.\n// If true it also returns the error list that caused the bailout.\n// Intended usage is:\n//\n//\t  if l, ok := perr.CatchBailout(recover()); ok {\n//\t\t// handle bailout\n//\t  }\nfunc CatchBailout(recovered any) (l *List, ok bool) {\n\tif recovered != nil {\n\t\tif b, ok := recovered.(bailout); ok {\n\t\t\treturn b.l, true\n\t\t} else {\n\t\t\tpanic(recovered)\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// CatchBailoutAndPanic is like CatchBailout but also catches other panics.\n// In both types of panics it converts the result to an error.\n//\n// If there is no panic it returns the error provided in the first argument\n// back to the caller. That way, it can usually be used like so\n// (inside a deferred function):\n//\n//\terr, _ = perr.CatchBailoutAndPanic(err, recover())\nfunc CatchBailoutAndPanic(currentErr error, recovered any) (err error, caught bool) {\n\tif recovered == nil {\n\t\treturn currentErr, false\n\t}\n\n\tif b, ok := recovered.(bailout); ok {\n\t\terr = b.l.AsError()\n\t} else {\n\t\terr = srcerrors.UnhandledPanic(recovered)\n\t}\n\treturn err, true\n}\n\n// IsBailout reports whether a recovered value is a bailout panic.\n// It reports the list that caused the bailout alongside.\nfunc IsBailout(recovered any) (l *List, ok bool) {\n\tif b, ok := recovered.(bailout); ok {\n\t\treturn b.l, true\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/errors.go",
    "content": "package pkginfo\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"pkginfo\",\n\t\t\"\",\n\n\t\terrors.WithRangeSize(25),\n\t)\n\n\terrReadingGoMod = errRange.New(\n\t\t\"Error Reading go.mod\",\n\t\t\"An error occurred while trying to read the go.mod file.\",\n\t)\n\n\terrInvalidModulePath = errRange.Newf(\n\t\t\"Invalid Module Path\",\n\t\t\"The module path %q in the go.mod file is invalid.\",\n\t)\n\n\terrReadingFile = errRange.New(\n\t\t\"Error Reading File\",\n\t\t\"An error occurred while trying to read a file.\",\n\t)\n\n\terrMatchingFile = errRange.New(\n\t\t\"Error Matching File\",\n\t\t\"An error occurred while trying to match a file to the build.\",\n\t)\n)\n"
  },
  {
    "path": "v2/internals/pkginfo/loader.go",
    "content": "package pkginfo\n\nimport (\n\t\"go/build\"\n\t\"go/token\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"golang.org/x/tools/go/packages\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\n// New creates a new Loader.\nfunc New(c *parsectx.Context) *Loader {\n\tl := &Loader{\n\t\tc:       c,\n\t\tmodules: make(map[paths.Mod]*Module),\n\t\tparsed:  make(map[paths.Pkg]*parseResult),\n\t}\n\tl.init()\n\treturn l\n}\n\n// A Loader provides lazy loading of package information.\ntype Loader struct {\n\tc *parsectx.Context\n\n\t// initialized by init.\n\tmainModule     *Module\n\truntimeModule  *Module\n\tbuildCtx       *build.Context\n\tpackagesConfig *packages.Config\n\n\t// modules contains loaded module information.\n\tmodulesMu sync.Mutex\n\tmodules   map[paths.Mod]*Module\n\n\t// parsed is a cache of parse results, guarded by parsedMu.\n\tparsedMu sync.Mutex\n\tparsed   map[paths.Pkg]*parseResult // importPath -> result\n}\n\nfunc (l *Loader) init() {\n\t// Resolve the main module.\n\tl.mainModule = l.loadModuleFromDisk(l.c.MainModuleDir, \"\")\n\t// Manually cache the main module.\n\tl.modules[l.mainModule.Path] = l.mainModule\n\n\t// Resolve the encore.dev runtime module.\n\tl.runtimeModule = &Module{\n\t\tl:       l,\n\t\tRootDir: l.c.Build.EncoreRuntime,\n\t\tPath:    \"encore.dev\",\n\t\tVersion: \"v1.0.0\",\n\t}\n\tl.modules[\"encore.dev\"] = l.runtimeModule\n\n\t// Resolve the stdlib module.\n\t{\n\t\t// If this is the standard library go/packages doesn't return\n\t\t// a Module object. Instead look it up from our GOROOT.\n\t\tgoroot := l.c.Build.GOROOT\n\t\trootPath := goroot.Join(\"src\")\n\n\t\t// Construct a synthetic Module object for the standard library.\n\t\tl.modules[paths.StdlibMod()] = &Module{\n\t\t\tl:       l,\n\t\t\tRootDir: rootPath,\n\t\t\tPath:    \"std\",\n\t\t\tVersion: \"\",\n\t\t}\n\t}\n\n\tb := l.c.Build\n\td := &build.Default\n\tl.buildCtx = &build.Context{\n\t\tGOARCH: b.GOARCH,\n\t\tGOOS:   b.GOOS,\n\t\tGOROOT: b.GOROOT.ToIO(),\n\n\t\tDir:         l.c.MainModuleDir.ToIO(),\n\t\tCgoEnabled:  b.CgoEnabled,\n\t\tUseAllFiles: false,\n\t\tCompiler:    d.Compiler,\n\t\tBuildTags:   append(slices.Clone(d.BuildTags), b.BuildTags...),\n\t\tToolTags:    slices.Clone(d.ToolTags),\n\t\tReleaseTags: slices.Clone(d.ReleaseTags),\n\t\tOpenFile:    l.c.OpenFile,\n\t\tReadDir:     l.c.ReadFileInfo,\n\t\tIsDir:       l.c.IsDir,\n\t}\n\n\t// Set up the go/packages configuration for resolving modules.\n\tcgoEnabled := \"0\"\n\tif b.CgoEnabled {\n\t\tcgoEnabled = \"1\"\n\t}\n\tUpdateGoPath(b.GOROOT)\n\tl.packagesConfig = &packages.Config{\n\t\tMode:    packages.NeedName | packages.NeedFiles | packages.NeedModule,\n\t\tContext: l.c.Ctx,\n\t\tDir:     l.c.MainModuleDir.ToIO(),\n\t\tEnv: append(os.Environ(),\n\t\t\t\"GOOS=\"+b.GOOS,\n\t\t\t\"GOARCH=\"+b.GOARCH,\n\t\t\t\"GOROOT=\"+b.GOROOT.ToIO(),\n\t\t\t\"CGO_ENABLED=\"+cgoEnabled,\n\t\t\t\"PATH=\"+b.GOROOT.Join(\"bin\").ToIO()+string(filepath.ListSeparator)+os.Getenv(\"PATH\"),\n\t\t),\n\t\tFset:    l.c.FS,\n\t\tTests:   l.c.ParseTests,\n\t\tOverlay: l.c.PkgOverlay(),\n\t\tLogf: func(format string, args ...any) {\n\t\t\tl.c.Log.Debug().Str(\"component\", \"pkgload\").Msgf(\"go/packages: \"+format, args...)\n\t\t},\n\t}\n}\n\n// MainModule returns the parsed main module.\nfunc (l *Loader) MainModule() *Module {\n\treturn l.mainModule\n}\n\n// RuntimeModule returns the parsed runtime module.\nfunc (l *Loader) RuntimeModule() *Module {\n\treturn l.runtimeModule\n}\n\n// MustLoadPkg loads a package.\n// If the package contains no Go files, it bails out.\nfunc (l *Loader) MustLoadPkg(cause token.Pos, pkgPath paths.Pkg) (pkg *Package) {\n\tpkg, ok := l.LoadPkg(cause, pkgPath)\n\tif !ok {\n\t\tl.c.Errs.Addf(cause, \"could not find package %q\", pkgPath)\n\t\tl.c.Errs.Bailout()\n\t}\n\treturn pkg\n}\n\n// LoadPkg loads a package.\n// If the package contains no Go files to build, it returns (nil, false).\nfunc (l *Loader) LoadPkg(cause token.Pos, pkgPath paths.Pkg) (pkg *Package, ok bool) {\n\t// Do we have the result cached already?\n\tl.parsedMu.Lock()\n\tresult, wasCached := l.parsed[pkgPath]\n\tif !wasCached {\n\t\t// Not cached; store a new entry so other goroutines will wait for us.\n\t\tresult = &parseResult{done: make(chan struct{})}\n\t\tl.parsed[pkgPath] = result\n\t\tdefer close(result.done)\n\t}\n\tl.parsedMu.Unlock()\n\n\tif wasCached {\n\t\t// We have a cached package. Wait for parsing to complete.\n\t\tselect {\n\t\tcase <-result.done:\n\t\t\tif result.bailout {\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t\treturn result.pkg, result.ok\n\n\t\tcase <-l.c.Ctx.Done():\n\t\t\t// The context was cancelled first.\n\t\t\treturn nil, false\n\t\t}\n\t}\n\n\t// Not cached. Do the parsing.\n\t// Catch any err since this runs in a separate goroutine.\n\tdefer func() {\n\t\tif _, caught := perr.CatchBailout(recover()); caught {\n\t\t\tresult.bailout = true\n\t\t\tpkg = nil\n\t\t\tok = false\n\t\t}\n\t}()\n\n\tmodule := l.resolveModuleForPkg(cause, pkgPath)\n\trelPath, ok := module.Path.RelativePathToPkg(pkgPath)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\n\tresult.pkg, result.ok = l.doParsePkg(loadPkgSpec{\n\t\tcause: cause,\n\t\tpath:  pkgPath,\n\t\tdir:   module.RootDir.Join(relPath.ToIO()),\n\t})\n\treturn result.pkg, result.ok\n}\n\n// UpdateGoPath updates the PATH environment variable to use the\n// \"go\" binary from Encore's GOROOT.\n// This is necessary because packages.Load invokes \"go list\" under the hood,\n// and we want to ensure it uses the same 'go' binary as Encore.\nfunc UpdateGoPath(goRoot paths.FS) {\n\tcurr := os.Getenv(\"PATH\")\n\tprefix := goRoot.Join(\"bin\").ToIO() + string(filepath.ListSeparator)\n\tif !strings.HasPrefix(curr, prefix) {\n\t\t_ = os.Setenv(\"PATH\", prefix+curr)\n\t}\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/loader_test.go",
    "content": "package pkginfo_test\n\nimport (\n\t\"go/token\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestLoader(t *testing.T) {\n\tt.Run(\"single\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- foo/foo.go --\n// Package doc\npackage pkgname // main file\n-- go.mod --\nmodule example.com\n`)\n\n\t\ttc := testutil.NewContext(c, false, a)\n\t\ttc.FailTestOnErrors()\n\t\tl := pkginfo.New(tc.Context)\n\n\t\tpkg, ok := l.LoadPkg(token.NoPos, \"example.com/foo\")\n\t\tc.Assert(ok, qt.Equals, true)\n\t\tc.Check(pkg.Name, qt.Equals, \"pkgname\")\n\t\tc.Check(pkg.ImportPath, qt.Equals, paths.MustPkgPath(\"example.com/foo\"))\n\t\tc.Check(pkg.Doc, qt.Equals, \"Package doc\\n\")\n\n\t\tc.Assert(pkg.Files, qt.HasLen, 1)\n\t\tf := pkg.Files[0]\n\t\tc.Check(f.Name, qt.Equals, \"foo.go\")\n\t\tc.Check(f.Pkg, qt.Equals, pkg)\n\t\tc.Check(f.TestFile, qt.IsFalse)\n\t\tc.Check(string(f.Contents()), qt.Equals, string(a.Files[0].Data))\n\t})\n\n\tt.Run(\"with_tests\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- foo/foo.go --\npackage foo // main file\n-- foo/foo_test.go --\npackage foo // test file\n-- go.mod --\nmodule example.com\n\t`)\n\n\t\ttc := testutil.NewContext(c, true, a)\n\t\ttc.FailTestOnErrors()\n\t\tl := pkginfo.New(tc.Context)\n\n\t\tpkg, ok := l.LoadPkg(token.NoPos, \"example.com/foo\")\n\t\tc.Assert(ok, qt.Equals, true)\n\t\tc.Assert(pkg.Files, qt.HasLen, 2)\n\n\t\tc.Check(pkg.Files[0].Name, qt.Equals, \"foo.go\")\n\t\tc.Check(pkg.Files[0].TestFile, qt.IsFalse)\n\t\tc.Check(string(pkg.Files[0].Contents()), qt.Equals, string(a.Files[0].Data))\n\n\t\tc.Check(pkg.Files[1].Name, qt.Equals, \"foo_test.go\")\n\t\tc.Check(pkg.Files[1].TestFile, qt.IsTrue)\n\t\tc.Check(string(pkg.Files[1].Contents()), qt.Equals, string(a.Files[1].Data))\n\t})\n\n\tt.Run(\"with_external_tests\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- foo/foo.go --\npackage foo // main file\n-- foo/foo_test.go --\npackage foo_test // external test file\n-- go.mod --\nmodule example.com\n\t`)\n\n\t\ttc := testutil.NewContext(c, true, a)\n\t\ttc.FailTestOnErrors()\n\t\tl := pkginfo.New(tc.Context)\n\n\t\tpkg, ok := l.LoadPkg(token.NoPos, \"example.com/foo\")\n\t\tc.Assert(ok, qt.Equals, true)\n\t\tc.Assert(pkg.Files, qt.HasLen, 2)\n\n\t\tc.Check(pkg.Files[0].Name, qt.Equals, \"foo.go\")\n\t\tc.Check(pkg.Files[0].TestFile, qt.IsFalse)\n\t\tc.Check(string(pkg.Files[0].Contents()), qt.Equals, string(a.Files[0].Data))\n\n\t\tc.Check(pkg.Files[1].Name, qt.Equals, \"foo_test.go\")\n\t\tc.Check(pkg.Files[1].TestFile, qt.IsTrue)\n\t\tc.Check(string(pkg.Files[1].Contents()), qt.Equals, string(a.Files[1].Data))\n\t})\n\n\tt.Run(\"with_tests_ignored\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- foo/foo.go --\npackage foo // main file\n-- foo/foo_test.go --\npackage foo // test file\n-- go.mod --\nmodule example.com\n\t`)\n\n\t\ttc := testutil.NewContext(c, false, a)\n\t\ttc.FailTestOnErrors()\n\n\t\tl := pkginfo.New(tc.Context)\n\n\t\tpkg, ok := l.LoadPkg(token.NoPos, \"example.com/foo\")\n\t\tc.Assert(ok, qt.Equals, true)\n\t\tc.Assert(pkg.Files, qt.HasLen, 1)\n\n\t\tc.Check(pkg.Files[0].Name, qt.Equals, \"foo.go\")\n\t\tc.Check(pkg.Files[0].TestFile, qt.IsFalse)\n\t\tc.Check(string(pkg.Files[0].Contents()), qt.Equals, string(a.Files[0].Data))\n\t})\n\n\tt.Run(\"with_parse_failure\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\n\t\ta := parse(`\n-- foo/foo.go --\nasdf\n-- go.mod --\nmodule example.com\n\t`)\n\n\t\ttc := testutil.NewContext(c, false, a)\n\t\tl := pkginfo.New(tc.Context)\n\n\t\tdefer tc.DeferExpectError(`expected 'package', found asdf`)\n\n\t\tl.LoadPkg(token.NoPos, \"example.com/foo\")\n\t})\n\n\tt.Run(\"external_module\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- go.mod --\nmodule example.com\nrequire rsc.io/hello v1.0.0\n`)\n\t\ttc := testutil.NewContext(c, false, a)\n\t\tl := pkginfo.New(tc.Context)\n\t\tdefer tc.FailTestOnBailout()\n\t\ttc.GoModDownload()\n\t\tpkg := l.MustLoadPkg(token.NoPos, \"rsc.io/hello\")\n\t\tc.Assert(pkg.Files, qt.HasLen, 1)\n\t\tc.Assert(pkg.Name, qt.Equals, \"main\")\n\t\tc.Assert(pkg.Doc, qt.Equals, \"Hello greets the world.\\n\")\n\n\t\tf := pkg.Files[0]\n\t\tc.Check(f.Name, qt.Equals, \"hello.go\")\n\t\tc.Check(f.TestFile, qt.IsFalse)\n\t\tc.Check(f.FSPath.ToIO(), qt.Matches, `.*/mod/rsc\\.io/hello@v1\\.0\\.0/hello.go`)\n\t\tc.Check(fns.MapKeys(f.Imports), qt.ContentEquals, []paths.Pkg{\"fmt\", \"rsc.io/quote\"})\n\t\tc.Check(string(pkg.Files[0].Contents()), qt.Contains, \"fmt.Println(quote.Hello())\")\n\t})\n}\n\nfunc parse(in string) *txtar.Archive {\n\treturn txtar.Parse([]byte(in))\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/modresolve.go",
    "content": "package pkginfo\n\nimport (\n\t\"errors\"\n\t\"go/token\"\n\t\"io/fs\"\n\t\"os\"\n\t\"slices\"\n\n\t\"golang.org/x/mod/modfile\"\n\t\"golang.org/x/tools/go/packages\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n)\n\n// File modresolve contains tools for resolving an import path\n// into information about the module it belongs to.\n\n// loadModuleFromDisk loads the module information from the given directory.\n// It does not consult the module cache; use resolveModule for that.\nfunc (l *Loader) loadModuleFromDisk(rootDir paths.FS, fallbackModPath paths.Mod) (m *Module) {\n\ttr := l.c.Trace(\"pkgload.loadModuleFromDisk\", \"dir\", rootDir)\n\tdefer tr.Done(\"result\", m)\n\n\t// Load the go.mod file from disk and validate it.\n\tgomodFilePath := rootDir.Join(\"go.mod\").ToIO()\n\tdata, err := os.ReadFile(gomodFilePath)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) && fallbackModPath != \"\" {\n\t\t\t// The dependency predates Go Modules. Simulate an empty module.\n\t\t\treturn &Module{\n\t\t\t\tRootDir: rootDir,\n\t\t\t\tPath:    fallbackModPath,\n\t\t\t\tVersion: \"v0.0.0-00010101000000-000000000000\",\n\t\t\t}\n\t\t}\n\n\t\tl.c.Errs.Assert(errReadingGoMod.Wrapping(err).InFile(gomodFilePath))\n\t}\n\tmodFile, err := modfile.Parse(gomodFilePath, data, nil)\n\tif err != nil {\n\t\tl.c.Errs.Assert(errReadingGoMod.Wrapping(err).InFile(gomodFilePath))\n\t}\n\tif !paths.ValidModPath(modFile.Module.Mod.Path) {\n\t\tl.c.Errs.Assert(errInvalidModulePath(modFile.Module.Mod.Path).InFile(gomodFilePath))\n\t}\n\n\tm = &Module{\n\t\tRootDir: rootDir,\n\t\tPath:    paths.MustModPath(modFile.Module.Mod.Path),\n\t\tVersion: modFile.Module.Mod.Version,\n\t\tfile:    option.Some(modFile),\n\t}\n\n\t// Parse the dependencies.\n\t// We ignore other directives (like replace) because they don't impact\n\t// how package paths are resolved to modules.\n\tfor _, dep := range modFile.Require {\n\t\tdepModPath := dep.Mod.Path\n\t\t// ignore invalid module paths. We could raise an error,\n\t\t// but the build step catches dependency issues anyway.\n\t\tif !paths.ValidModPath(depModPath) {\n\t\t\tcontinue\n\t\t}\n\t\tif m.Path.LexicallyContains(paths.MustPkgPath(depModPath)) {\n\t\t\tm.sortedNestedDeps = append(m.sortedNestedDeps, paths.MustModPath(depModPath))\n\t\t} else {\n\t\t\tm.sortedOtherDeps = append(m.sortedOtherDeps, paths.MustModPath(depModPath))\n\t\t}\n\t}\n\tslices.Sort(m.sortedNestedDeps)\n\tslices.Sort(m.sortedOtherDeps)\n\n\treturn m\n}\n\nvar stdModule = paths.StdlibMod()\n\n// moduleForPkgPath resolves the module path that contains\n// the given import path, based on the module information.\n// It consults the module path and the module's require directives.\nfunc (l *Loader) moduleForPkgPath(pkgPath paths.Pkg) (modPath paths.Mod, found bool) {\n\t// We resolve all packages by consulting the versions\n\t// in the main module, since only the main module's dependencies\n\t// track exactly which versions are used (due to MVS).\n\tm := l.mainModule\n\n\t// Fast path: first check if it's contained in the main module,\n\t// since that's what we scan the most frequently.\n\tif m.Path.LexicallyContains(pkgPath) {\n\t\t// The package is rooted within this module.\n\t\t// It's possible it's a nested module.\n\t\tif nested, ok := findModule(m.sortedNestedDeps, pkgPath); ok {\n\t\t\treturn nested, true\n\t\t}\n\t\t// It belongs to this module.\n\t\treturn m.Path, true\n\t}\n\n\t// Otherwise fall back to all the other dependencies.\n\tif modPath, found = findModule(m.sortedOtherDeps, pkgPath); found {\n\t\treturn modPath, true\n\t}\n\n\t// We couldn't find it in any module the main module depends on.\n\t// See if it belongs to the standard library.\n\tif stdModule.LexicallyContains(pkgPath) {\n\t\t// The package is rooted within the standard library.\n\t\treturn stdModule, true\n\t}\n\n\t// Couldn't find it. Give up.\n\treturn \"\", false\n}\n\n// findModule finds the module that contains pkg given a\n// sorted list of modules to consult.\nfunc findModule(sortedMods []paths.Mod, pkg paths.Pkg) (modPath paths.Mod, found bool) {\n\tidx, exactMatch := slices.BinarySearch(sortedMods, paths.Mod(pkg))\n\t// Two cases to consider: an exact match (unlikely) or a prefix match.\n\tif exactMatch {\n\t\treturn sortedMods[idx], true\n\t}\n\n\t// idx represents where the path would be inserted in the list.\n\t// Since we're interested in prefix matches, we expect the module the\n\t// package path is contained within to be at idx-1.\n\t// If idx == 0 the module wasn't found.\n\tif idx == 0 {\n\t\treturn \"\", false\n\t} else if candidate := sortedMods[idx-1]; candidate.LexicallyContains(pkg) {\n\t\treturn candidate, true\n\t} else {\n\t\t// It's possible to end up here if there are multiple dependencies\n\t\t// with module paths that are prefixes of one another.\n\t\t//\n\t\t// Consider the deps: [\"foo\", \"foo/bar\", \"foo/bar/baz\"].\n\t\t// Doing a binary search for \"foo/qux\" would return (idx=3, exactMatch=false),\n\t\t// but the module that contains \"foo/qux\" is \"foo\" at idx=0.\n\t\t//\n\t\t// To handle this case, keep iterating backwards until we find a prefix match.\n\t\tfor i := idx - 2; i >= 0; i-- {\n\t\t\tif candidate := sortedMods[i]; candidate.LexicallyContains(pkg) {\n\t\t\t\treturn candidate, true\n\t\t\t}\n\t\t}\n\n\t\treturn \"\", false\n\t}\n}\n\n// resolveModuleForPkg resolves information about the module that contains a package.\nfunc (l *Loader) resolveModuleForPkg(cause token.Pos, pkgPath paths.Pkg) (result *Module) {\n\t// Which module does this package belong to?\n\tmodPath, found := l.moduleForPkgPath(pkgPath)\n\tif !found {\n\t\tl.c.Errs.Addf(cause, \"package %q not found belonging to any module\", pkgPath)\n\t\tl.c.Errs.Bailout()\n\t\treturn nil // unreachable\n\t}\n\n\t// Is the module already cached?\n\tl.modulesMu.Lock()\n\tcached, ok := l.modules[modPath]\n\tl.modulesMu.Unlock()\n\tif ok {\n\t\treturn cached\n\t}\n\n\ttr := l.c.Trace(\"resolve module for package\", \"pkgPath\", pkgPath)\n\tdefer tr.Done(\"result\", result)\n\n\tpkgs, err := packages.Load(l.packagesConfig, \"pattern=\"+string(pkgPath))\n\tl.c.Errs.AssertStd(err)\n\n\tvar pkg *packages.Package\n\tfor _, candidate := range pkgs {\n\t\tif candidate.PkgPath == string(pkgPath) {\n\t\t\tpkg = candidate\n\t\t\tbreak\n\t\t}\n\t}\n\tif pkg == nil {\n\t\tl.c.Errs.Fatalf(cause, \"cannot find package %q\", pkgPath)\n\t} else if len(pkg.Errors) > 0 {\n\t\tfor _, err := range pkg.Errors {\n\t\t\tl.c.Errs.AddStd(err)\n\t\t}\n\t\tl.c.Errs.Bailout()\n\t} else if len(pkg.GoFiles) == 0 {\n\t\tl.c.Errs.Fatalf(cause, \"package %q has no Go files\", pkgPath)\n\t}\n\n\t// Load the module from disk.\n\tif pkg.Module == nil || pkg.Module.Dir == \"\" {\n\t\tl.c.Errs.Fatalf(cause, \"package %q has no module information\", pkgPath)\n\t}\n\trootPath := paths.RootedFSPath(pkg.Module.Dir, \".\")\n\tresult = l.loadModuleFromDisk(rootPath, modPath)\n\n\t// Add the module to the cache.\n\tl.modulesMu.Lock()\n\tdefer l.modulesMu.Unlock()\n\tl.modules[modPath] = result\n\treturn result\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/modresolve_test.go",
    "content": "package pkginfo\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\n\t\"encr.dev/pkg/paths\"\n)\n\nfunc Test_findModule(t *testing.T) {\n\tc := qt.New(t)\n\tdeps := []paths.Mod{\n\t\t\"foo\",\n\t\t\"foo/bar\",\n\t\t\"foo/bar/baz\",\n\t\t\"foo/unrelated\",\n\t\t\"encore.dev\",\n\t}\n\tslices.Sort(deps)\n\n\tyes := func(pkg paths.Pkg, mod paths.Mod) {\n\t\tgot, ok := findModule(deps, pkg)\n\t\tc.Assert(ok, qt.IsTrue, qt.Commentf(\"pkg=%q\", pkg))\n\t\tc.Assert(got, qt.Equals, mod, qt.Commentf(\"pkg=%q\", pkg))\n\t}\n\tno := func(pkg paths.Pkg, mod paths.Mod) {\n\t\t_, ok := findModule(deps, pkg)\n\t\tc.Assert(ok, qt.IsFalse, qt.Commentf(\"pkg=%q\", pkg))\n\t}\n\n\tyes(\"foo\", \"foo\")\n\tyes(\"foo/qux\", \"foo\")\n\tyes(\"foo/barbar\", \"foo\")\n\n\tyes(\"foo/bar\", \"foo/bar\")\n\tyes(\"foo/bar/boo\", \"foo/bar\")\n\tyes(\"foo/bar/baz\", \"foo/bar/baz\")\n\tyes(\"foo/bar/baz/boo\", \"foo/bar/baz\")\n\tyes(\"foo/bar/baz/boo\", \"foo/bar/baz\")\n\tyes(\"encore.dev\", \"encore.dev\")\n\tyes(\"encore.dev/foo\", \"encore.dev\")\n\tyes(\"encore.dev/foo/bar\", \"encore.dev\")\n\n\tno(\"fo\", \"\")\n\tno(\"foono\", \"\")\n\tno(\"encore\", \"\")\n\tno(\"encore.devno\", \"\")\n\tno(\"x\", \"\")\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/names.go",
    "content": "package pkginfo\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/build\"\n\t\"go/token\"\n\t\"path\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/agnivade/levenshtein\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n)\n\n// resolvePkgNames resolves package-level names for the given package.\nfunc resolvePkgNames(pkg *Package) *PkgNames {\n\tdecls := make(map[string]*PkgDeclInfo)\n\tscope := newScope(nil)\n\n\tfor _, f := range pkg.Files {\n\t\tfor _, d := range f.AST().Decls {\n\t\t\tswitch d := d.(type) {\n\t\t\tcase *ast.FuncDecl:\n\t\t\t\t// HACK(andre) If the RPC was defined as a method on a service struct we\n\t\t\t\t// generate a synthetic package-level func as part of the user-facing code generation.\n\t\t\t\t// This happens after parsing, so at the parsing phase we ignore the user-facing code generation.\n\t\t\t\t//\n\t\t\t\t// To properly parse code that references those package-level funcs, register\n\t\t\t\t// service struct-based APIs as existing with synthetic package-level identifiers.\n\t\t\t\tisServiceStructAPI := d.Recv != nil && isEncoreAPI(d)\n\n\t\t\t\tif d.Recv == nil || isServiceStructAPI {\n\t\t\t\t\tscope.Insert(d.Name.Name, pkgObject)\n\t\t\t\t\tdecls[d.Name.Name] = &PkgDeclInfo{\n\t\t\t\t\t\tName: d.Name.Name,\n\t\t\t\t\t\tFile: f,\n\t\t\t\t\t\tPos:  d.Name.Pos(),\n\t\t\t\t\t\tDoc:  d.Doc.Text(),\n\t\t\t\t\t\tType: token.FUNC,\n\t\t\t\t\t\tFunc: d,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase *ast.GenDecl:\n\t\t\t\tfor _, spec := range d.Specs {\n\t\t\t\t\tvar doc string\n\t\t\t\t\tswitch spec := spec.(type) {\n\t\t\t\t\tcase *ast.ValueSpec:\n\t\t\t\t\t\tdoc = spec.Doc.Text()\n\t\t\t\t\t\tif doc == \"\" {\n\t\t\t\t\t\t\tdoc = spec.Comment.Text()\n\t\t\t\t\t\t}\n\t\t\t\t\tcase *ast.TypeSpec:\n\t\t\t\t\t\tdoc = spec.Doc.Text()\n\t\t\t\t\t\tif doc == \"\" {\n\t\t\t\t\t\t\tdoc = spec.Comment.Text()\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif doc == \"\" && len(d.Specs) == 1 {\n\t\t\t\t\t\tdoc = d.Doc.Text()\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch spec := spec.(type) {\n\t\t\t\t\tcase *ast.ImportSpec:\n\t\t\t\t\t\t// Skip, file-level\n\t\t\t\t\tcase *ast.ValueSpec:\n\t\t\t\t\t\tfor _, name := range spec.Names {\n\t\t\t\t\t\t\tscope.Insert(name.Name, pkgObject)\n\t\t\t\t\t\t\tdecls[name.Name] = &PkgDeclInfo{\n\t\t\t\t\t\t\t\tName:    name.Name,\n\t\t\t\t\t\t\t\tFile:    f,\n\t\t\t\t\t\t\t\tPos:     name.Pos(),\n\t\t\t\t\t\t\t\tDoc:     doc,\n\t\t\t\t\t\t\t\tType:    d.Tok,\n\t\t\t\t\t\t\t\tSpec:    spec,\n\t\t\t\t\t\t\t\tGenDecl: d,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\tcase *ast.TypeSpec:\n\t\t\t\t\t\tscope.Insert(spec.Name.Name, pkgObject)\n\t\t\t\t\t\tdecls[spec.Name.Name] = &PkgDeclInfo{\n\t\t\t\t\t\t\tName:    spec.Name.Name,\n\t\t\t\t\t\t\tFile:    f,\n\t\t\t\t\t\t\tPos:     spec.Name.Pos(),\n\t\t\t\t\t\t\tDoc:     doc,\n\t\t\t\t\t\t\tType:    d.Tok,\n\t\t\t\t\t\t\tGenDecl: d,\n\t\t\t\t\t\t\tSpec:    spec,\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &PkgNames{\n\t\tPkgDecls: decls,\n\t\tpkgScope: scope,\n\t}\n}\n\n// fileNameResolver resolves file-local names within a package.\ntype fileNameResolver struct {\n\tl   *Loader\n\tf   *File\n\tpkg *Package\n\ttr  *parsectx.TraceLogger\n\n\t// res is the resulting name information.\n\tres *FileNames\n\n\t// scope is the current scope being processed.\n\tscope *scope\n}\n\nfunc resolveFileNames(f *File) *FileNames {\n\ttr := f.l.c.Trace(\"pkgload.resolveFileNames\", \"pkg\", f.Pkg.ImportPath, \"file\", f.Name)\n\tdefer tr.Done()\n\tr := &fileNameResolver{\n\t\tl:  f.l,\n\t\tf:  f,\n\t\ttr: tr,\n\t\tres: &FileNames{\n\t\t\tloader:           f.l,\n\t\t\tfile:             f,\n\t\t\tidents:           make(map[*ast.Ident]identKind),\n\t\t\tknownImportNames: make(map[string]*importedPkg),\n\t\t},\n\t\tscope: f.Pkg.Names().pkgScope,\n\t}\n\n\tr.processImports()\n\tfor _, decl := range f.AST().Decls {\n\t\tswitch decl := decl.(type) {\n\t\tcase *ast.GenDecl:\n\t\t\tfor _, spec := range decl.Specs {\n\t\t\t\tswitch spec := spec.(type) {\n\t\t\t\tcase *ast.ValueSpec:\n\t\t\t\t\tfor _, name := range spec.Names {\n\t\t\t\t\t\tr.ident(name)\n\t\t\t\t\t}\n\t\t\t\t\tr.exprList(spec.Values)\n\t\t\t\t\tr.expr(spec.Type)\n\t\t\t\tcase *ast.TypeSpec:\n\t\t\t\t\tr.expr(spec.Type)\n\t\t\t\t\tr.expr(spec.Type)\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase *ast.FuncDecl:\n\t\t\tr.funcDecl(decl)\n\t\t}\n\t}\n\n\treturn r.res\n}\n\n// processImports finds the file-local names of imports we care about.\n// The name mapping is stored in r.File.PathToName and r.File.NameToPath.\nfunc (r *fileNameResolver) processImports() {\n\tfor _, decl := range r.f.AST().Decls {\n\t\tgd, ok := decl.(*ast.GenDecl)\n\t\tif !ok || gd.Tok != token.IMPORT {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, spec := range gd.Specs {\n\t\t\tis := spec.(*ast.ImportSpec)\n\t\t\tpos := is.Path.Pos()\n\n\t\t\tstrPath, err := strconv.Unquote(is.Path.Value)\n\t\t\tif err != nil {\n\t\t\t\tr.l.c.Errs.Addf(pos, \"invalid import path %s\", is.Path.Value)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar dstPkgPath paths.Pkg\n\t\t\tif build.IsLocalImport(strPath) {\n\t\t\t\tdstPkgPath = r.pkg.ImportPath.JoinSlash(paths.RelSlash(strPath))\n\t\t\t} else {\n\t\t\t\tdstPkgPath, ok = paths.PkgPath(strPath)\n\t\t\t\tif !ok {\n\t\t\t\t\tr.l.c.Errs.Addf(pos, \"invalid import path %q\", strPath)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Do we have a local name?\n\t\t\tlocalName := \"\"\n\t\t\tif is.Name != nil {\n\t\t\t\tif is.Name.Name == \".\" {\n\t\t\t\t\t// TODO(andre) handle this\n\t\t\t\t\tr.l.c.Errs.Fatalf(pos, \"dot imports are currently unsupported by Encore's static analysis\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlocalName = is.Name.Name\n\t\t\t}\n\n\t\t\t// Track this import as long as the local name is not \"_\".\n\t\t\tif localName != \"_\" {\n\t\t\t\t// localName is generally \"\" if the package was imported without\n\t\t\t\t// giving it an explicit alias (like `import foo \"path/to/foo\"`).\n\t\t\t\t// If that's the case, attempt to classify the package name.\n\t\t\t\tif localName == \"\" {\n\t\t\t\t\tlocalName = r.resolveKnownPkgName(dstPkgPath)\n\t\t\t\t}\n\n\t\t\t\tr.res.imports = append(r.res.imports, &importedPkg{\n\t\t\t\t\timportPath:      dstPkgPath,\n\t\t\t\t\tlastPathSegment: path.Base(dstPkgPath.String()),\n\t\t\t\t\tlocalName:       localName,\n\t\t\t\t})\n\t\t\t}\n\n\t\t}\n\t}\n}\n\n// resolveKnownPkgName returns the known package name for the given import path, if any.\n// It uses the fact that the encore.dev module and the standard library guarantee\n// that the last path segment is the package name.\n//\n// If the name is not known it reports \"\".\nfunc (r *fileNameResolver) resolveKnownPkgName(pkgPath paths.Pkg) (name string) {\n\tstdlib := paths.StdlibMod()\n\tencoreRuntime := r.l.runtimeModule.Path\n\tif stdlib.LexicallyContains(pkgPath) || encoreRuntime.LexicallyContains(pkgPath) {\n\t\treturn path.Base(pkgPath.String())\n\t}\n\treturn \"\"\n}\n\nfunc (r *fileNameResolver) funcDecl(fd *ast.FuncDecl) {\n\tr.openScope()\n\tdefer r.closeScope()\n\n\t// First resolve types before introducing names\n\tfor _, param := range fd.Type.Params.List {\n\t\tr.expr(param.Type)\n\t}\n\tif fd.Type.Results != nil {\n\t\tfor _, result := range fd.Type.Results.List {\n\t\t\tr.expr(result.Type)\n\t\t}\n\t}\n\n\tif fd.Recv != nil {\n\t\tfor _, field := range fd.Recv.List {\n\t\t\tfor _, name := range field.Names {\n\t\t\t\tr.scope.Insert(name.Name, localObject)\n\t\t\t}\n\t\t}\n\t}\n\tfor _, field := range fd.Type.Params.List {\n\t\tfor _, name := range field.Names {\n\t\t\tr.scope.Insert(name.Name, localObject)\n\t\t}\n\t}\n\tif fd.Type.Results != nil {\n\t\tfor _, field := range fd.Type.Results.List {\n\t\t\tfor _, name := range field.Names {\n\t\t\t\tr.scope.Insert(name.Name, localObject)\n\t\t\t}\n\t\t}\n\t}\n\tif fd.Body != nil {\n\t\tr.stmtList(fd.Body.List)\n\t}\n}\n\nfunc (r *fileNameResolver) stmt(stmt ast.Stmt) {\n\tif stmt == nil {\n\t\treturn\n\t}\n\n\tswitch stmt := stmt.(type) {\n\tcase *ast.AssignStmt:\n\t\tr.exprList(stmt.Rhs)\n\t\tfor _, lhs := range stmt.Lhs {\n\t\t\tif id, ok := lhs.(*ast.Ident); ok && stmt.Tok == token.DEFINE {\n\t\t\t\tr.define(id, localObject)\n\t\t\t}\n\t\t}\n\n\tcase *ast.BlockStmt:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmtList(stmt.List)\n\n\tcase *ast.DeclStmt:\n\t\tdecl := stmt.Decl.(*ast.GenDecl)\n\t\tfor _, spec := range decl.Specs {\n\t\t\tswitch spec := spec.(type) {\n\t\t\tcase *ast.ValueSpec:\n\t\t\t\tr.exprList(spec.Values)\n\t\t\t\tr.expr(spec.Type)\n\t\t\t\tfor _, name := range spec.Names {\n\t\t\t\t\tr.define(name, localObject)\n\t\t\t\t}\n\t\t\tcase *ast.TypeSpec:\n\t\t\t\tr.expr(spec.Type)\n\t\t\t\tr.define(spec.Name, localObject)\n\t\t\t}\n\t\t}\n\n\tcase *ast.DeferStmt:\n\t\tr.expr(stmt.Call)\n\n\tcase *ast.ExprStmt:\n\t\tr.expr(stmt.X)\n\n\tcase *ast.ForStmt:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmt(stmt.Init)\n\t\tr.expr(stmt.Cond)\n\t\tr.stmt(stmt.Post)\n\t\tr.stmt(stmt.Body)\n\n\tcase *ast.GoStmt:\n\t\tr.expr(stmt.Call)\n\n\tcase *ast.IfStmt:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmt(stmt.Init)\n\t\tr.expr(stmt.Cond)\n\t\tr.stmt(stmt.Body)\n\t\tr.stmt(stmt.Else)\n\n\tcase *ast.IncDecStmt:\n\t\tr.expr(stmt.X)\n\n\tcase *ast.LabeledStmt:\n\t\tr.stmt(stmt.Stmt)\n\n\tcase *ast.RangeStmt:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.expr(stmt.X)\n\t\tr.expr(stmt.Key)\n\t\tr.expr(stmt.Value)\n\t\tr.stmt(stmt.Body)\n\n\tcase *ast.ReturnStmt:\n\t\tr.exprList(stmt.Results)\n\n\tcase *ast.SelectStmt:\n\t\tr.stmtList(stmt.Body.List)\n\n\tcase *ast.SendStmt:\n\t\tr.expr(stmt.Value)\n\t\tr.expr(stmt.Chan)\n\n\tcase *ast.SwitchStmt:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmt(stmt.Init)\n\t\tr.expr(stmt.Tag)\n\t\tr.stmt(stmt.Body)\n\n\tcase *ast.TypeSwitchStmt:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmt(stmt.Init)\n\t\tr.stmt(stmt.Assign)\n\t\tr.stmt(stmt.Body)\n\n\tcase *ast.CommClause:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmt(stmt.Comm)\n\t\tr.stmtList(stmt.Body)\n\n\tcase *ast.CaseClause:\n\t\tr.exprList(stmt.List)\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\t\tr.stmtList(stmt.Body)\n\n\tcase *ast.BadStmt, *ast.BranchStmt, *ast.EmptyStmt:\n\t\t// do nothing\n\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unhandled ast.Stmt type: %T\", stmt))\n\t}\n}\n\nfunc (r *fileNameResolver) expr(expr ast.Expr) {\n\tswitch expr := expr.(type) {\n\tcase nil:\n\t\t// do nothing\n\n\tcase *ast.Ident:\n\t\tr.ident(expr)\n\n\tcase *ast.Ellipsis:\n\t\tr.expr(expr.Elt)\n\n\tcase *ast.FuncLit:\n\t\tr.openScope()\n\t\tdefer r.closeScope()\n\n\t\t// First resolve types before introducing names\n\t\tfor _, param := range expr.Type.Params.List {\n\t\t\tr.expr(param.Type)\n\t\t}\n\t\tif expr.Type.Results != nil {\n\t\t\tfor _, result := range expr.Type.Results.List {\n\t\t\t\tr.expr(result.Type)\n\t\t\t}\n\t\t}\n\n\t\tfor _, field := range expr.Type.Params.List {\n\t\t\tfor _, name := range field.Names {\n\t\t\t\tr.define(name, localObject)\n\t\t\t}\n\t\t}\n\t\tif expr.Type.Results != nil {\n\t\t\tfor _, field := range expr.Type.Results.List {\n\t\t\t\tfor _, name := range field.Names {\n\t\t\t\t\tr.define(name, localObject)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif expr.Body != nil {\n\t\t\tr.stmt(expr.Body)\n\t\t}\n\n\tcase *ast.CompositeLit:\n\t\tr.expr(expr.Type)\n\t\tr.exprList(expr.Elts)\n\n\tcase *ast.ParenExpr:\n\t\tr.expr(expr.X)\n\n\tcase *ast.SelectorExpr:\n\t\tr.expr(expr.X)\n\t\t// Note: we don't treat 'Foo' in 'x.Foo' as an identifier,\n\t\t// as it does not introduce a new name to any scope.\n\n\tcase *ast.IndexExpr:\n\t\tr.expr(expr.X)\n\t\tr.expr(expr.Index)\n\n\tcase *ast.IndexListExpr:\n\t\t// An IndexListExpr node represents an expression followed by multiple indices.\n\t\t// e.g. `X[A, B, C]` or `X[1, 2]`\n\t\tr.expr(expr.X)\n\t\tfor _, index := range expr.Indices {\n\t\t\tr.expr(index)\n\t\t}\n\n\tcase *ast.SliceExpr:\n\t\tr.expr(expr.X)\n\t\tr.expr(expr.Low)\n\t\tr.expr(expr.High)\n\t\tr.expr(expr.Max)\n\n\tcase *ast.TypeAssertExpr:\n\t\tr.expr(expr.X)\n\t\tr.expr(expr.Type)\n\n\tcase *ast.CallExpr:\n\t\tr.expr(expr.Fun)\n\t\tr.exprList(expr.Args)\n\n\tcase *ast.StarExpr:\n\t\tr.expr(expr.X)\n\n\tcase *ast.UnaryExpr:\n\t\tr.expr(expr.X)\n\n\tcase *ast.BinaryExpr:\n\t\tr.expr(expr.X)\n\t\tr.expr(expr.Y)\n\n\tcase *ast.KeyValueExpr:\n\t\t// HACK: We want to track uses of functions. This is tricky because\n\t\t// struct types use keys that are idents that refer to the struct field,\n\t\t// while map types can use keys to refer to idents in scope.\n\t\t//\n\t\t// Unfortunately We cannot easily know the type of the composite literal\n\t\t// without typechecking. However, funcs are incomparable and therefore\n\t\t// are not valid as map keys. So let's simply avoid tracking idents\n\t\t// in the keys, and rely on the compiler to eventually catch this for us.\n\t\tif _, ok := expr.Key.(*ast.Ident); !ok {\n\t\t\tr.expr(expr.Key)\n\t\t}\n\t\tr.expr(expr.Value)\n\n\tcase *ast.ArrayType:\n\t\tr.expr(expr.Len)\n\t\tr.expr(expr.Elt)\n\n\tcase *ast.StructType:\n\t\tfor _, field := range expr.Fields.List {\n\t\t\tr.expr(field.Type)\n\t\t\t// Don't look at names; they don't resolve to outside scope\n\t\t}\n\n\tcase *ast.FuncType:\n\t\tfor _, field := range expr.Params.List {\n\t\t\tr.expr(field.Type)\n\t\t\t// Don't look at names; they don't resolve to outside scope\n\t\t}\n\t\tif expr.Results != nil {\n\t\t\tfor _, field := range expr.Results.List {\n\t\t\t\tr.expr(field.Type)\n\t\t\t\t// Don't look at names; they don't resolve to outside scope\n\t\t\t}\n\t\t}\n\n\tcase *ast.InterfaceType:\n\t\tfor _, field := range expr.Methods.List {\n\t\t\tr.expr(field.Type)\n\t\t\t// Don't look at names; they don't resolve to outside scope\n\t\t}\n\n\tcase *ast.MapType:\n\t\tr.expr(expr.Key)\n\t\tr.expr(expr.Value)\n\n\tcase *ast.ChanType:\n\t\tr.expr(expr.Value)\n\n\tcase *ast.BadExpr, *ast.BasicLit:\n\t\t// do nothing\n\n\tdefault:\n\t\t// If we don't process this then return false\n\t\tpanic(fmt.Sprintf(\"unhandled ast.Expr type: %T\", expr))\n\t}\n}\n\nfunc (r *fileNameResolver) stmtList(stmts []ast.Stmt) {\n\tfor _, s := range stmts {\n\t\tr.stmt(s)\n\t}\n}\n\nfunc (r *fileNameResolver) exprList(exprs []ast.Expr) {\n\tfor _, x := range exprs {\n\t\tr.expr(x)\n\t}\n}\n\nfunc (r *fileNameResolver) ident(id *ast.Ident) {\n\t// Map this ident. If the name is already in scope, use that definition.\n\t// Otherwise check if it's an imported name.\n\tif kind := r.scope.LookupParent(id.Name); kind != none {\n\t\tr.res.idents[id] = kind\n\t} else {\n\t\tr.res.idents[id] = importName\n\t}\n}\n\nfunc (r *fileNameResolver) define(id *ast.Ident, kind identKind) {\n\tr.res.idents[id] = kind\n\tr.scope.Insert(id.Name, kind)\n}\n\nfunc (r *fileNameResolver) openScope() {\n\tr.scope = newScope(r.scope)\n}\n\nfunc (r *fileNameResolver) closeScope() {\n\tr.scope = r.scope.Pop()\n}\n\nfunc isEncoreAPI(fd *ast.FuncDecl) bool {\n\tfd.Doc.Text()\n\tif fd.Doc == nil {\n\t\treturn false\n\t}\n\n\tconst directive = \"encore:api\"\n\tfor _, c := range fd.Doc.List {\n\t\tif strings.HasPrefix(c.Text, \"//\"+directive) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Legacy syntax\n\tlines := strings.Split(fd.Doc.Text(), \"\\n\")\n\tfor _, line := range lines {\n\t\tif strings.HasPrefix(line, directive) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// PkgDeclInfo provides metadata for a package-level declaration.\ntype PkgDeclInfo struct {\n\tName string\n\tFile *File\n\tPos  token.Pos\n\tDoc  string\n\n\t// Type describes what type of declaration this is.\n\t// It's one of CONST, TYPE, VAR, or FUNC.\n\tType token.Token\n\n\t// Spec is the spec for this declaration and GenDecl the declaration block\n\t// it belongs to. They are set only when Type != FUNC.\n\tSpec    ast.Spec\n\tGenDecl *ast.GenDecl\n\n\t// Func is the function declaration, if Type == FUNC.\n\tFunc *ast.FuncDecl // for Type == FUNC\n\t// Recv is the receiver type, if Type == FUNC and the function is a method.\n\tRecv *PkgDeclInfo\n}\n\nfunc (i *PkgDeclInfo) QualifiedName() QualifiedName {\n\treturn Q(i.File.Pkg.ImportPath, i.Name)\n}\n\n// PkgNames contains name information that's package-global.\ntype PkgNames struct {\n\t// PkgDecls contains package-level declarations, keyed by name.\n\tPkgDecls map[string]*PkgDeclInfo\n\n\t// Funcs are all the func declarations.\n\tFuncs []*PkgDeclInfo\n\n\t// pkgScope tracks the scope information for the package scope.\n\t// It's stored to avoid having to recompute it when querying\n\t// for individual files' file-level name information.\n\tpkgScope *scope\n}\n\nfunc (n *PkgNames) FuncDecl(name string) option.Option[*ast.FuncDecl] {\n\tif fn, ok := n.PkgDecls[name]; ok && fn.Type == token.FUNC {\n\t\treturn option.Some(fn.Func)\n\t}\n\treturn option.None[*ast.FuncDecl]()\n}\n\nfunc (n *PkgNames) GoString() string {\n\treturn \"&pkginfo.PkgNames{...}\"\n}\n\n// FileNames contains name resolution results for a single file.\ntype FileNames struct {\n\tloader *Loader // the loader this file comes from\n\n\t// file is the file the names belong to.\n\tfile *File\n\n\t// idents maps identifiers in the file to information about them.\n\tidents map[*ast.Ident]identKind\n\n\timports []*importedPkg\n\n\t// knownImportNames tracks the resolved import names for this file.\n\tknownImportNamesMu sync.Mutex\n\tknownImportNames   map[string]*importedPkg\n\n\t//// localNameToPkg maps local names to the package they refer to.\n\t//localNameToPkg map[string]*importedPkg\n\t//\n\t//\n\t//// nameToPath contains resolved local name -> import\n\t//nameToPath map[string]paths.Pkg // local name -> path\n}\n\nfunc (n *FileNames) GoString() string {\n\treturn \"&pkginfo.FileNames{...}\"\n}\n\n// ResolvePkgPath resolves the package path a given identifier name\n// resolves to.\nfunc (f *FileNames) ResolvePkgPath(cause token.Pos, name string) (pkgPath paths.Pkg, ok bool) {\n\treturn f.resolveImportPath(cause, name)\n}\n\n// ResolvePkgLevelRef resolves the node to the package-level reference it refers to.\n// Expr must be either *ast.Ident or *ast.SelectorExpr.\n// If it doesn't refer to a package-level reference it returns ok == false.\nfunc (f *FileNames) ResolvePkgLevelRef(expr ast.Expr) (name QualifiedName, ok bool) {\n\t// Unwrap type arguments\n\tswitch x := expr.(type) {\n\tcase *ast.IndexExpr:\n\t\texpr = x.X\n\tcase *ast.IndexListExpr:\n\t\texpr = x.X\n\t}\n\n\t// Resolve the identifier\n\tswitch node := expr.(type) {\n\tcase *ast.Ident:\n\t\t// If it's an ident, then we're looking for something which resolves to a package-level object defined\n\t\t// in the same package as the ident is located in.\n\t\tif f.idents[node] == pkgObject {\n\t\t\treturn QualifiedName{f.file.Pkg.ImportPath, node.Name}, true\n\t\t}\n\tcase *ast.SelectorExpr:\n\t\t// If it's a selector, then we're looking for something which has been imported from another package\n\t\tif pkgName, ok := node.X.(*ast.Ident); ok {\n\t\t\tif f.idents[pkgName] == importName {\n\t\t\t\tif importPath, ok := f.resolveImportPath(expr.Pos(), pkgName.Name); ok {\n\t\t\t\t\treturn QualifiedName{importPath, node.Sel.Name}, true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn QualifiedName{}, false\n}\n\nfunc (f *FileNames) resolveImportPath(cause token.Pos, identName string) (pkgPath paths.Pkg, ok bool) {\n\t// Do we already have this name resolved?\n\tf.knownImportNamesMu.Lock()\n\tpkg := f.knownImportNames[identName]\n\tf.knownImportNamesMu.Unlock()\n\tif pkg != nil {\n\t\treturn pkg.importPath, true\n\t}\n\n\t// Otherwise, resolve it. We do this by looking at the imported packages\n\t// and determine which one is most likely to be the one we're looking for.\n\n\tresolvePkg := func() *importedPkg {\n\t\t// First look for an explicit import alias.\n\t\tfor _, pkg := range f.imports {\n\t\t\tif pkg.localName == identName {\n\t\t\t\treturn pkg\n\t\t\t}\n\t\t}\n\n\t\t// Use heuristics to guess the package.\n\n\t\t// Bucket the packages into a group of exact matches\n\t\t// and everything else.\n\t\tvar (\n\t\t\texactMatches []*importedPkg\n\t\t\totherPkgs    = make([]*importedPkg, 0, len(f.imports))\n\t\t)\n\t\tfor _, pkg := range f.imports {\n\t\t\t// If there is an explicit local name, ignore the package\n\t\t\t// since we've already checked those above.\n\t\t\tif pkg.localName != \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif pkg.lastPathSegment == identName {\n\t\t\t\texactMatches = append(exactMatches, pkg)\n\t\t\t} else {\n\t\t\t\totherPkgs = append(otherPkgs, pkg)\n\t\t\t}\n\t\t}\n\n\t\tprocessGroup := func(pkgs []*importedPkg) *importedPkg {\n\t\t\tfor _, pkg := range pkgs {\n\t\t\t\t// Load the package to determine if it has the name we're looking for.\n\t\t\t\tpkg.loadOnce.Do(func() {\n\t\t\t\t\tpkg.loadedPkg = f.loader.MustLoadPkg(cause, pkg.importPath)\n\t\t\t\t})\n\n\t\t\t\tif pkg.loadedPkg.Name == identName {\n\t\t\t\t\t// We've found the package we're looking for.\n\t\t\t\t\treturn pkg\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\t// Check the exact matches first.\n\t\tif pkg := processGroup(exactMatches); pkg != nil {\n\t\t\treturn pkg\n\t\t}\n\n\t\t// If not, check the remaining packages. Sort them\n\t\t// by levenshtein distance to start with the likeliest\n\t\t// candidates first.\n\t\tdistances := fns.Map(otherPkgs, func(pkg *importedPkg) int {\n\t\t\treturn levenshtein.ComputeDistance(pkg.lastPathSegment, identName)\n\t\t})\n\t\tsort.Slice(otherPkgs, func(i, j int) bool {\n\t\t\treturn distances[i] < distances[j]\n\t\t})\n\t\treturn processGroup(otherPkgs)\n\t}\n\n\tif pkg := resolvePkg(); pkg != nil {\n\t\tf.knownImportNamesMu.Lock()\n\t\tf.knownImportNames[identName] = pkg\n\t\tf.knownImportNamesMu.Unlock()\n\t\treturn pkg.importPath, true\n\t}\n\n\treturn \"\", false\n}\n\ntype identKind string\n\nconst (\n\t// none is a none identKind.\n\tnone identKind = \"\"\n\n\t// pkgObject is used for identifiers pointing to package-level\n\t// objects in the same package as the identifier.\n\tpkgObject identKind = \"pkgObject\"\n\n\t// localObject is used for identifiers pointing to\n\t// function-local objects.\n\tlocalObject identKind = \"localObject\"\n\n\t// importName is used for identifiers pointing to\n\t// an imported package.\n\timportName identKind = \"importName\"\n)\n\n// importedPkg represents a package that has been imported\n// in a file.\n//\n// Since the local name is the package name, which can\n// be different from the last element of the import path, we\n// can't know for sure which package it refers to until\n// we've parsed that package.\ntype importedPkg struct {\n\timportPath paths.Pkg\n\n\t// lastPathSegment is the last segment of the import path.\n\t// It's used as a heuristic to guess which package to\n\t// parse first.\n\tlastPathSegment string\n\n\t// localName is the name of the package in the file.\n\tlocalName string\n\n\t// loadedPkg is the loaded package, if any.\n\t// It must be accessed using the sync.Once.\n\tloadOnce  sync.Once\n\tloadedPkg *Package\n}\n\n// scope maps names to information about them.\ntype scope struct {\n\tnames  map[string]identKind\n\tparent *scope\n}\n\nfunc newScope(parent *scope) *scope {\n\treturn &scope{\n\t\tnames:  make(map[string]identKind),\n\t\tparent: parent,\n\t}\n}\n\nfunc (s *scope) Pop() *scope {\n\treturn s.parent\n}\n\nfunc (s *scope) Insert(name string, kind identKind) {\n\tif name != \"_\" {\n\t\ts.names[name] = kind\n\t}\n}\n\nfunc (s *scope) Lookup(name string) identKind {\n\treturn s.names[name]\n}\n\nfunc (s *scope) LookupParent(name string) identKind {\n\tif kind := s.names[name]; kind != none {\n\t\treturn kind\n\t} else if s.parent != nil {\n\t\treturn s.parent.LookupParent(name)\n\t}\n\treturn none\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/names_test.go",
    "content": "package pkginfo_test\n\nimport (\n\t\"go/token\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestNames(t *testing.T) {\n\tt.Run(\"empty_pkg\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- foo/foo.go --\n// Package doc\npackage pkgname // main file\n-- go.mod --\nmodule example.com\n`)\n\n\t\ttc := testutil.NewContext(c, false, a)\n\t\ttc.FailTestOnErrors()\n\t\tl := pkginfo.New(tc.Context)\n\n\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com/foo\")\n\t\tc.Assert(pkg.Names().PkgDecls, qt.HasLen, 0)\n\t})\n\n\tt.Run(\"external_module\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- go.mod --\nmodule example.com\nrequire rsc.io/quote v1.5.2\n-- foo/foo.go --\npackage foo\nimport \"rsc.io/quote\"\nvar _ = quote.Hello()\n`)\n\t\ttc := testutil.NewContext(c, false, a)\n\t\tdefer tc.FailTestOnBailout()\n\t\ttc.GoModTidy()\n\n\t\tl := pkginfo.New(tc.Context)\n\t\tpkg := l.MustLoadPkg(token.NoPos, \"rsc.io/quote\")\n\n\t\tc.Assert(pkg.Names().PkgDecls, qt.CmpEquals(\n\t\t\tcmpopts.IgnoreFields(pkginfo.PkgDeclInfo{}, \"File\", \"Func\", \"Pos\", \"GenDecl\", \"Spec\", \"Doc\"),\n\t\t), map[string]*pkginfo.PkgDeclInfo{\n\t\t\t\"Glass\": {Name: \"Glass\", Type: token.FUNC},\n\t\t\t\"Go\":    {Name: \"Go\", Type: token.FUNC},\n\t\t\t\"Hello\": {Name: \"Hello\", Type: token.FUNC},\n\t\t\t\"Opt\":   {Name: \"Opt\", Type: token.FUNC},\n\t\t})\n\n\t\tgotPath, ok := pkg.Files[0].Names().ResolvePkgPath(token.NoPos, \"sampler\")\n\t\tc.Assert(ok, qt.IsTrue)\n\t\tc.Assert(gotPath, qt.Equals, paths.Pkg(\"rsc.io/sampler\"))\n\t})\n\n\tt.Run(\"external_module_major_version\", func(t *testing.T) {\n\t\tc := qt.New(t)\n\t\ta := parse(`\n-- go.mod --\nmodule example.com\nrequire rsc.io/quote v1.5.3-0.20180710144737-5d9f230bcfba\n-- foo/foo.go --\npackage foo\nimport \"rsc.io/quote\"\nvar _ = quote.HelloV3()\n`)\n\t\ttc := testutil.NewContext(c, false, a)\n\t\tdefer tc.FailTestOnBailout()\n\t\ttc.GoModTidy()\n\n\t\tl := pkginfo.New(tc.Context)\n\t\tpkg := l.MustLoadPkg(token.NoPos, \"rsc.io/quote/v3\")\n\n\t\tc.Assert(pkg.Names().PkgDecls, qt.CmpEquals(\n\t\t\tcmpopts.IgnoreFields(pkginfo.PkgDeclInfo{}, \"File\", \"Func\", \"Pos\", \"GenDecl\", \"Spec\", \"Doc\"),\n\t\t), map[string]*pkginfo.PkgDeclInfo{\n\t\t\t\"HelloV3\": {Name: \"HelloV3\", Type: token.FUNC},\n\t\t\t\"GlassV3\": {Name: \"GlassV3\", Type: token.FUNC},\n\t\t\t\"GoV3\":    {Name: \"GoV3\", Type: token.FUNC},\n\t\t\t\"OptV3\":   {Name: \"OptV3\", Type: token.FUNC},\n\t\t})\n\n\t\tgotPath, ok := pkg.Files[0].Names().ResolvePkgPath(token.NoPos, \"sampler\")\n\t\tc.Assert(ok, qt.IsTrue)\n\t\tc.Assert(gotPath, qt.Equals, paths.Pkg(\"rsc.io/sampler\"))\n\t})\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/pkgparse.go",
    "content": "package pkginfo\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"go/ast\"\n\tgoparser \"go/parser\"\n\t\"go/token\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n)\n\n// File pkgparse implements parsing of packages.\n\n// parseResult is the result from attempting to parse a package.\ntype parseResult struct {\n\tdone    chan struct{} // closed when parsing is completed\n\tpkg     *Package\n\tok      bool\n\tbailout bool\n}\n\n// loadPkgSpec is the specification for how to load a package.\ntype loadPkgSpec struct {\n\t// cause is the source position that caused the load.\n\t// It's used to generate useful error messages.\n\tcause token.Pos\n\n\t// path is the package path.\n\tpath paths.Pkg\n\n\t// dir is the directory containing the package.\n\tdir paths.FS\n}\n\n// doParsePkg parses a single package in the given directory.\n// It returns (nil, false) if the directory contains no Go files.\nfunc (l *Loader) doParsePkg(s loadPkgSpec) (pkg *Package, ok bool) {\n\tl.c.Errs.BailoutOnErrors(func() {\n\t\tastPkgs, files := l.parseAST(s)\n\t\tpkg = l.processPkg(s, astPkgs, files)\n\t})\n\treturn pkg, pkg != nil\n}\n\n// processPkg combines the results of parsing a package into a single *Package.\nfunc (l *Loader) processPkg(s loadPkgSpec, pkgs []*ast.Package, files []*File) *Package {\n\tif n := len(pkgs); n > 1 {\n\t\t// Make sure the extra packages are just \"_test\" packages.\n\t\t// Pull out the package names.\n\n\t\tslices.SortFunc(pkgs, func(a, b *ast.Package) int {\n\t\t\treturn cmp.Compare(a.Name, b.Name)\n\t\t})\n\t\tpkgNames := fns.Map(pkgs, func(pkg *ast.Package) string { return pkg.Name })\n\t\tif n == 2 && pkgNames[1] == pkgNames[0]+\"_test\" {\n\t\t\t// We're good\n\t\t} else {\n\t\t\tnames := strings.Join(pkgNames[:n-1], \", \") + \" and \" + pkgNames[n-1]\n\t\t\tl.c.Errs.Addf(s.cause, \"%s\", fmt.Sprintf(\"found multiple package names in package %s: %s\", s.path, names))\n\t\t}\n\t} else if n == 0 {\n\t\t// No Go files; ignore directory\n\t\treturn nil\n\t}\n\n\tp := pkgs[0]\n\tpkg := &Package{\n\t\tAST:        p,\n\t\tName:       p.Name,\n\t\tImportPath: s.path,\n\t\tFSPath:     s.dir,\n\t\tFiles:      files,\n\t\tImports:    make(map[paths.Pkg]ast.Node),\n\t}\n\n\tfor _, f := range files {\n\t\tf.Pkg = pkg\n\t\t// Fill in imports.\n\t\tfor importPath, pointer := range f.Imports {\n\t\t\tpkg.Imports[importPath] = pointer\n\t\t}\n\n\t\t// Fill in package docs.\n\t\tif pkg.Doc == \"\" && !f.TestFile && f.initialAST.Doc != nil {\n\t\t\tpkg.Doc = f.initialAST.Doc.Text()\n\t\t}\n\t}\n\n\treturn pkg\n}\n\n// parseAST is like go/parser.ParseDir but it constructs *File objects instead.\nfunc (l *Loader) parseAST(s loadPkgSpec) ([]*ast.Package, []*File) {\n\tdir := s.dir.ToIO()\n\tentries, err := l.c.ReadDir(dir)\n\tif err != nil {\n\t\tl.c.Errs.Addf(s.cause, \"parse package %q: %v\", s.path, err)\n\t\treturn nil, nil\n\t}\n\tshouldParseFile := func(info fs.DirEntry) bool {\n\t\tname := info.Name()\n\t\tswitch {\n\t\t// Don't parse encore.gen.go files, since they're not intended to be checked in.\n\t\t// We've had several issues where things work locally but not in CI/CD because\n\t\t// the encore.gen.go file was parsed for local development which papered over issues.\n\t\tcase strings.Contains(name, \"encore.gen.go\"):\n\t\t\treturn false\n\t\tcase !l.c.ParseTests && strings.HasSuffix(name, \"_test.go\"):\n\t\t\treturn false\n\t\tcase !strings.HasSuffix(name, \".go\"):\n\t\t\treturn false\n\t\tdefault:\n\t\t\treturn true\n\t\t}\n\t}\n\n\ttype fileInfo struct {\n\t\tpath     paths.FS\n\t\tioPath   string\n\t\tbaseName string\n\t}\n\n\tinfos := make([]fileInfo, 0, len(entries))\n\tfor _, e := range entries {\n\t\tif !e.IsDir() && shouldParseFile(e) {\n\t\t\tbaseName := e.Name()\n\t\t\tioPath := filepath.Join(dir, baseName)\n\t\t\tpath := s.dir.Join(baseName)\n\t\t\tinfos = append(infos, fileInfo{path: path, ioPath: ioPath, baseName: baseName})\n\t\t}\n\t}\n\n\tvar pkgs []*ast.Package\n\tvar files []*File\n\tseenPkgs := make(map[string]*ast.Package) // package name -> pkg\n\n\tfor _, d := range infos {\n\t\t// Check if this file should be part of the build\n\t\tmatched, err := l.buildCtx.MatchFile(dir, d.baseName)\n\t\tif err != nil {\n\t\t\tl.c.Errs.Add(errMatchingFile.InFile(d.ioPath).Wrapping(err))\n\t\t\tcontinue\n\t\t} else if !matched {\n\t\t\tcontinue\n\t\t} else if strings.EqualFold(d.baseName, \"encore.gen.go\") {\n\t\t\tcontinue\n\t\t}\n\n\t\treader, err := l.c.OpenFile(d.ioPath)\n\t\tif err != nil {\n\t\t\tl.c.Errs.Add(errReadingFile.InFile(d.ioPath).Wrapping(err))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Parse the package and imports only so code can consult that.\n\t\t// We parse the full AST on-demand later.\n\t\tmode := goparser.ParseComments | goparser.ImportsOnly\n\t\tastFile, err := goparser.ParseFile(l.c.FS, d.ioPath, reader, mode)\n\t\t_ = reader.Close()\n\t\tif err != nil {\n\t\t\tl.c.Errs.AddStd(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tpkgName := astFile.Name.Name\n\t\tpkg, found := seenPkgs[pkgName]\n\t\tif !found {\n\t\t\tpkg = &ast.Package{\n\t\t\t\tName:  pkgName,\n\t\t\t\tFiles: make(map[string]*ast.File),\n\t\t\t}\n\t\t\tseenPkgs[pkgName] = pkg\n\t\t\tpkgs = append(pkgs, pkg)\n\t\t}\n\n\t\tpkg.Files[d.ioPath] = astFile\n\n\t\tisTestFile := strings.HasSuffix(d.baseName, \"_test.go\") || strings.HasSuffix(pkgName, \"_test\")\n\t\tfiles = append(files, &File{\n\t\t\tl:        l,\n\t\t\tName:     d.baseName,\n\t\t\tFSPath:   d.path,\n\t\t\tPkg:      nil, // will be set later\n\t\t\tImports:  getFileImports(astFile),\n\t\t\tTestFile: isTestFile,\n\n\t\t\tinitialAST: astFile,\n\t\t})\n\t}\n\n\treturn pkgs, files\n}\n\nfunc getFileImports(f *ast.File) map[paths.Pkg]ast.Node {\n\timports := make(map[paths.Pkg]ast.Node)\n\tfor _, s := range f.Imports {\n\t\tif importPath, err := strconv.Unquote(s.Path.Value); err == nil {\n\t\t\tif p, ok := paths.PkgPath(importPath); ok {\n\t\t\t\timports[p] = s\n\t\t\t}\n\t\t}\n\t}\n\treturn imports\n}\n"
  },
  {
    "path": "v2/internals/pkginfo/types.go",
    "content": "package pkginfo\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\tgoparser \"go/parser\"\n\t\"go/token\"\n\t\"path\"\n\t\"sync\"\n\n\t\"golang.org/x/mod/modfile\"\n\t\"golang.org/x/tools/go/ast/inspector\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n)\n\n// Module describes a Go module.\ntype Module struct {\n\tl *Loader // the loader that created it.\n\n\tRootDir paths.FS  // the dir containing go.mod\n\tPath    paths.Mod // module path\n\tVersion string    // module version\n\n\t// file is the parsed modfile.\n\t// It's None if the module predates Go Modules.\n\tfile option.Option[*modfile.File]\n\n\t// sortedNestedDeps and sortedOtherDeps contain lists of\n\t// this module's dependencies, categorized into nested dependencies\n\t// (with module paths rooted within this module) and other dependencies (the rest).\n\t//\n\t// The lists are sorted to facilitate efficient lookups\n\t// to determine the right module to query for a given import path.\n\tsortedNestedDeps []paths.Mod\n\tsortedOtherDeps  []paths.Mod\n}\n\n// FSPathToPkg computes the filesystem path to the given package.\n// The package must lexically live within the given module,\n// otherwise it reports ok == false.\nfunc (m *Module) FSPathToPkg(pkgPath paths.Pkg) (path paths.FS, ok bool) {\n\tif rel, ok := m.Path.RelativePathToPkg(pkgPath); ok {\n\t\treturn m.RootDir.Join(rel.ToIO()), true\n\t}\n\n\treturn \"\", false\n}\n\ntype Package struct {\n\tl          *Loader // the loader that created it.\n\tAST        *ast.Package\n\tName       string\n\tDoc        string\n\tImportPath paths.Pkg\n\tFSPath     paths.FS\n\tFiles      []*File\n\tImports    map[paths.Pkg]ast.Node // union of all imports from files\n\n\tnamesOnce  sync.Once\n\tnamesCache *PkgNames\n}\n\nfunc (p *Package) GoString() string {\n\treturn fmt.Sprintf(\"&pkginfo.Package{ImportPath: %q, Name: %q}\", p.ImportPath, p.Name)\n}\n\n// Names returns the computed package-level names.\nfunc (p *Package) Names() *PkgNames {\n\tp.namesOnce.Do(func() {\n\t\tp.namesCache = resolvePkgNames(p)\n\t})\n\treturn p.namesCache\n}\n\ntype File struct {\n\tl        *Loader                // the loader that created it.\n\tName     string                 // file name (\"foo.go\")\n\tPkg      *Package               // package it belongs to\n\tFSPath   paths.FS               // where the file lives on disk\n\tImports  map[paths.Pkg]ast.Node // imports in the file, keyed by import path pointed at the import spec\n\tTestFile bool                   // whether the file is a test file\n\n\t// initialAST is the AST for the initial parse that only includes\n\t// package docs and imports.\n\tinitialAST *ast.File\n\n\t// Filled in lazily; each one guarded by a sync.Once.\n\tastCacheOnce sync.Once\n\tcachedAST    *ast.File\n\tcachedToken  *token.File\n\n\tcontentsOnce   sync.Once\n\tcachedContents []byte\n\n\tnamesOnce  sync.Once\n\tnamesCache *FileNames\n\n\tinspectorOnce  sync.Once\n\tinspectorCache *inspector.Inspector\n}\n\nfunc (f *File) GoString() string {\n\tif f == nil {\n\t\treturn \"(*pkginfo.File)(nil)\"\n\t}\n\n\tpkgPath := \"(UNKNOWN)\"\n\tif f.Pkg != nil {\n\t\tpkgPath = f.Pkg.ImportPath.String()\n\t}\n\treturn fmt.Sprintf(\"&pkginfo.File{Pkg: %q, Name: %q}\", pkgPath, f.Name)\n}\n\n// Names returns the computed file-level names.\nfunc (f *File) Names() *FileNames {\n\tf.namesOnce.Do(func() {\n\t\tf.namesCache = resolveFileNames(f)\n\t})\n\treturn f.namesCache\n}\n\n// Contents returns the full file contents.\nfunc (f *File) Contents() []byte {\n\tf.contentsOnce.Do(func() {\n\t\tioPath := f.FSPath.ToIO()\n\t\tdata, err := f.l.c.ReadFile(ioPath)\n\t\tif err != nil {\n\t\t\tf.l.c.Errs.Assert(errReadingFile.InFile(ioPath).Wrapping(err))\n\t\t}\n\t\tf.cachedContents = data\n\t})\n\treturn f.cachedContents\n}\n\n// AST returns the parsed AST for this file.\nfunc (f *File) AST() *ast.File {\n\tf.astCacheOnce.Do(func() {\n\t\tastFile, err := goparser.ParseFile(f.l.c.FS, f.FSPath.ToIO(), f.Contents(), goparser.ParseComments)\n\t\tf.l.c.Errs.AssertStd(err)\n\t\tf.cachedAST = astFile\n\t\tf.cachedToken = f.l.c.FS.File(astFile.Pos())\n\t})\n\treturn f.cachedAST\n}\n\nfunc (f *File) Token() *token.File {\n\t// Ensure f.cachedToken is set.\n\t_ = f.AST()\n\n\treturn f.cachedToken\n}\n\n// ASTInspector returns an AST inspector that's optimized for finding\n// nodes of particular types. See [inspector.Inspector] for more information.\nfunc (f *File) ASTInspector() *inspector.Inspector {\n\tf.inspectorOnce.Do(func() {\n\t\ttr := f.l.c.Trace(\"ASTInspector\", \"pkg\", f.Pkg.ImportPath, \"file\", f.Name)\n\t\tdefer tr.Done()\n\n\t\tf.inspectorCache = inspector.New([]*ast.File{f.AST()})\n\t})\n\n\treturn f.inspectorCache\n}\n\n// A QualifiedName is the combination of a package path and a package-level name.\n// It can be used to uniquely reference a package-level declaration.\ntype QualifiedName struct {\n\tPkgPath paths.Pkg\n\tName    string\n}\n\n// NaiveDisplayName returns the name as \"pkgname.Name\" by assuming\n// the package name is equal to the last path component.\nfunc (q QualifiedName) NaiveDisplayName() string {\n\treturn path.Base(string(q.PkgPath)) + \".\" + q.Name\n}\n\n// Q is a helper function to construct a QualifiedName.\nfunc Q(pkgPath paths.Pkg, name string) QualifiedName {\n\treturn QualifiedName{PkgPath: pkgPath, Name: name}\n}\n"
  },
  {
    "path": "v2/internals/posmap/posmap.go",
    "content": "package posmap\n\nimport (\n\t\"go/ast\"\n\t\"sort\"\n\n\t\"encr.dev/pkg/option\"\n)\n\nfunc Build[NodeLike ast.Node](nodes ...NodeLike) Map[NodeLike] {\n\treturn (&Builder[NodeLike]{}).Add(nodes...).Build()\n}\n\ntype Builder[NodeLike ast.Node] struct {\n\tnodes []NodeLike\n}\n\nfunc (b *Builder[NodeLike]) Add(nodes ...NodeLike) *Builder[NodeLike] {\n\tb.nodes = append(b.nodes, nodes...)\n\treturn b\n}\n\nfunc (b *Builder[NodeLike]) Build() Map[NodeLike] {\n\tsort.Slice(b.nodes, func(i, j int) bool {\n\t\treturn b.nodes[i].Pos() < b.nodes[j].Pos()\n\t})\n\treturn Map[NodeLike]{\n\t\tnodes: b.nodes,\n\t}\n}\n\ntype Map[NodeLike ast.Node] struct {\n\tnodes []NodeLike\n}\n\nfunc (m Map[NodeLike]) All() []NodeLike {\n\treturn m.nodes\n}\n\nfunc (m Map[NodeLike]) Within(node ast.Node) []NodeLike {\n\tstartIdx := sort.Search(len(m.nodes), func(i int) bool {\n\t\treturn m.nodes[i].Pos() >= node.Pos()\n\t})\n\n\tendIdx := startIdx\n\tfor endIdx < len(m.nodes) && m.nodes[endIdx].End() <= node.End() {\n\t\tendIdx++\n\t}\n\n\tnodes := make([]NodeLike, endIdx-startIdx)\n\tcopy(nodes, m.nodes[startIdx:endIdx])\n\treturn nodes\n}\n\nfunc (m Map[NodeLike]) Containing(node ast.Node) option.Option[NodeLike] {\n\tidx := sort.Search(len(m.nodes), func(i int) bool {\n\t\treturn m.nodes[i].Pos() >= node.Pos()\n\t})\n\n\t// The containing resource, if any, has a starting position before this node\n\t// and an ending position after this node.\n\tif idx > 0 {\n\t\tcandidate := m.nodes[idx-1]\n\t\tif candidate.Pos() <= node.Pos() && candidate.End() >= node.End() {\n\t\t\treturn option.Some(candidate)\n\t\t}\n\t}\n\n\tif len(m.nodes) > 0 {\n\t\tcandidate := m.nodes[0]\n\t\tif candidate.Pos() <= node.Pos() && candidate.End() >= node.End() {\n\t\t\treturn option.Some(candidate)\n\t\t}\n\t}\n\n\treturn option.None[NodeLike]()\n}\n"
  },
  {
    "path": "v2/internals/resourcepaths/errors.go",
    "content": "package resourcepaths\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"paths\",\n\t\t\"Paths must be not be empty and always start with a '/'. You cannot define paths that conflict \"+\n\t\t\t\"with each other, including static and parameterized paths. For example `/blog/:id` would conflict with `/:username`.\\n\\n\"+\n\t\t\t\"For more information about configuring Paths, see https://encore.dev/docs/primitives/apis#rest-apis\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrEmptyPath = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths must not be empty.\",\n\t)\n\n\terrInvalidPathPrefix = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths cannot start with a '/'.\",\n\t)\n\n\terrInvalidPathMissingPrefix = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths must always start with a '/'.\",\n\t)\n\n\terrInvalidPathURI = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths must be valid URIs. There was an error parsing the path.\",\n\t)\n\n\terrPathContainedQuery = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths must not contain the '?' character.\",\n\t)\n\n\terrEmptySegment = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths cannot contain an empty segment, i.e. a double slash ('//').\",\n\t)\n\n\terrTrailingSlash = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Paths cannot end with a trailing slash ('/').\",\n\t)\n\n\terrParameterMissingName = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Path parameters must have a name. For example, `/:id` is valid, but `/:` is not.\",\n\t)\n\n\terrInvalidParamIdentifier = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Path parameters must be valid Go identifiers.\",\n\t)\n\n\terrWildcardNotLastSegment = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Path wildcards must be the last segment in the path.\",\n\t)\n\n\terrFallbackNotLastSegment = errRange.New(\n\t\t\"Invalid Path\",\n\t\t\"Path fallbacks must be the last segment in the path.\",\n\t)\n\n\terrDuplicatePath = errRange.New(\n\t\t\"Path Conflict\",\n\t\t\"Duplicate Paths found.\",\n\t)\n\n\terrConflictingParameterizedPath = errRange.Newf(\n\t\t\"Path Conflict\",\n\t\t\"The parameter `:%s` conflicts with the path `%s`.\",\n\t)\n\n\terrConflictingWildcardPath = errRange.Newf(\n\t\t\"Path Conflict\",\n\t\t\"The wildcard `*%s` conflicts with the path `%s`.\",\n\t)\n\n\terrConflictingLiteralPath = errRange.Newf(\n\t\t\"Path Conflict\",\n\t\t\"The path segment `%s` conflicts with the path `%s`.\",\n\t)\n\n\terrConflictingFallbackPath = errRange.Newf(\n\t\t\"Path Conflict\",\n\t\t\"The fallback `!%s` conflicts with the path `%s`.\",\n\t)\n)\n"
  },
  {
    "path": "v2/internals/resourcepaths/paths.go",
    "content": "// Package resourcepaths parses API and other resource paths.\npackage resourcepaths\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"net/url\"\n\t\"strings\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/schema\"\n)\n\n// Path represents a parsed path.\ntype Path struct {\n\tStartPos token.Pos\n\tSegments []Segment\n}\n\nvar _ ast.Node = (*Path)(nil)\n\nfunc (p *Path) Pos() token.Pos {\n\treturn p.StartPos\n}\n\nfunc (p *Path) End() token.Pos {\n\tif len(p.Segments) > 0 {\n\t\treturn p.Segments[len(p.Segments)-1].End()\n\t}\n\treturn p.StartPos\n}\n\n// String returns the path's string representation.\nfunc (p *Path) String() string {\n\tvar b strings.Builder\n\tfor _, s := range p.Segments {\n\t\tb.WriteByte('/')\n\n\t\tswitch s.Type {\n\t\tcase Param:\n\t\t\tb.WriteByte(':')\n\t\tcase Wildcard:\n\t\t\tb.WriteByte('*')\n\t\tcase Fallback:\n\t\t\tb.WriteByte('!')\n\t\t}\n\t\tb.WriteString(s.Value)\n\t}\n\treturn b.String()\n}\n\n// NumParams reports the number of parameterized (non-literal) segments in the path.\nfunc (p *Path) NumParams() int {\n\tn := 0\n\tfor _, s := range p.Segments {\n\t\tif s.Type != Literal {\n\t\t\tn++\n\t\t}\n\t}\n\treturn n\n}\n\n// Params returns the segments that are not literals.\nfunc (p *Path) Params() []Segment {\n\tvar params []Segment\n\tfor _, s := range p.Segments {\n\t\tif s.Type != Literal {\n\t\t\tparams = append(params, s)\n\t\t}\n\t}\n\treturn params\n}\n\n// HasFallback is true if the path contains a fallback segment.\nfunc (p *Path) HasFallback() bool {\n\tfor _, s := range p.Segments {\n\t\tif s.Type == Fallback {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Segment represents a parsed path segment.\ntype Segment struct {\n\tType      SegmentType\n\tValue     string // literal if Type == Literal; name of parameter otherwise\n\tValueType schema.BuiltinKind\n\tStartPos  token.Pos\n\tEndPos    token.Pos\n}\n\nvar _ ast.Node = Segment{}\n\nfunc (s *Segment) String() string {\n\tswitch s.Type {\n\tcase Param:\n\t\treturn \":\" + s.Value\n\tcase Wildcard:\n\t\treturn \"*\" + s.Value\n\tcase Fallback:\n\t\treturn \"!\" + s.Value\n\tdefault:\n\t\treturn s.Value\n\t}\n}\n\nfunc (s Segment) Pos() token.Pos {\n\treturn s.StartPos\n}\n\nfunc (s Segment) End() token.Pos {\n\treturn s.EndPos\n}\n\n// SegmentType represents the different types of path segments recognized by the parser.\ntype SegmentType int\n\nconst (\n\t// Literal is a literal string path segment.\n\tLiteral SegmentType = iota\n\t// Param represents a single path segment of any (non-empty) value.\n\tParam\n\t// Wildcard represents zero or more path segments of any value.\n\tWildcard\n\t// Fallback represents zero or more path segments of any value\n\t// that are lower priority than any other path.\n\tFallback\n)\n\ntype Options struct {\n\t// AllowWildcard indicates whether the parser should allow wildcard segments.\n\tAllowWildcard bool\n\n\t// AllowFallback indicates whether the parser should allow fallback segments.\n\tAllowFallback bool\n\n\t// PrefixSlash indicates whether the parser should require a leading slash\n\t// or require that it's not present\n\tPrefixSlash bool\n}\n\n// Parse parses a slash-separated path into path segments.\n//\n// strPos is the position of where the path string was found in the source code.\nfunc Parse(errs *perr.List, startPos token.Pos, path string, options Options) (parsedPath *Path, ok bool) {\n\tendPos := token.Pos(len([]byte(path))) + startPos\n\n\tif path == \"\" {\n\t\terrs.Add(errEmptyPath.AtGoPos(startPos, endPos))\n\t\treturn nil, false\n\t} else if path[0] != '/' && options.PrefixSlash {\n\t\terrs.Add(errInvalidPathMissingPrefix.AtGoPos(startPos, endPos))\n\t\treturn nil, false\n\t} else if path[0] == '/' && !options.PrefixSlash {\n\t\terrs.Add(errInvalidPathPrefix.AtGoPos(startPos, endPos))\n\t\treturn nil, false\n\t}\n\n\turlPath := path\n\tif !options.PrefixSlash {\n\t\turlPath = \"/\" + urlPath\n\t}\n\tif _, err := url.ParseRequestURI(urlPath); err != nil {\n\t\terrs.Add(errInvalidPathURI.AtGoPos(startPos, endPos).Wrapping(err))\n\t\treturn nil, false\n\t} else if idx := strings.IndexByte(path, '?'); idx != -1 {\n\t\terrs.Add(errPathContainedQuery.AtGoPos(startPos, endPos))\n\t\treturn nil, false\n\t}\n\n\tvar segs []Segment\n\tsegStart := startPos\n\tfor path != \"\" {\n\t\tif options.PrefixSlash || len(segs) > 0 {\n\t\t\tpath = path[1:] // drop leading '/'\n\t\t\tsegStart++\n\t\t}\n\n\t\t// Find the next path segment\n\t\tvar val string\n\t\tidx := strings.IndexByte(path, '/')\n\t\tsegEnd := segStart\n\t\tswitch idx {\n\t\tcase 0:\n\t\t\terrs.Add(errEmptySegment.AtGoPos(segStart-1, segStart))\n\t\t\treturn nil, false\n\t\tcase -1:\n\t\t\tval = path\n\t\t\tpath = \"\"\n\t\tdefault:\n\t\t\tval = path[:idx]\n\t\t\tpath = path[idx:]\n\t\t}\n\t\tsegEnd += token.Pos(len([]byte(val))) - 1\n\n\t\tsegType := Literal\n\t\tif val != \"\" && val[0] == ':' {\n\t\t\tsegType = Param\n\t\t\tval = val[1:]\n\t\t} else if val != \"\" && val[0] == '*' && options.AllowWildcard {\n\t\t\tsegType = Wildcard\n\t\t\tval = val[1:]\n\t\t} else if val != \"\" && val[0] == '!' && options.AllowFallback {\n\t\t\tsegType = Fallback\n\t\t\tval = val[1:]\n\t\t}\n\t\tsegs = append(segs, Segment{\n\t\t\tType: segType, Value: val, ValueType: schema.String,\n\t\t\tStartPos: segStart - 1, EndPos: segEnd,\n\t\t})\n\t\tsegStart = segEnd + 1\n\t}\n\n\t// Validate the segments\n\tfor i, s := range segs {\n\t\tswitch s.Type {\n\t\tcase Literal:\n\t\t\tif s.Value == \"\" {\n\t\t\t\terrs.Add(errTrailingSlash.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\tcase Param:\n\t\t\tswitch {\n\t\t\tcase s.Value == \"\":\n\t\t\t\terrs.Add(errParameterMissingName.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\tcase !token.IsIdentifier(s.Value):\n\t\t\t\terrs.Add(errInvalidParamIdentifier.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\tcase Wildcard:\n\t\t\tswitch {\n\t\t\tcase s.Value == \"\":\n\t\t\t\terrs.Add(errParameterMissingName.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\tcase !token.IsIdentifier(s.Value):\n\t\t\t\terrs.Add(errInvalidParamIdentifier.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\tcase len(segs) > (i + 1):\n\t\t\t\terrs.Add(errWildcardNotLastSegment.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\tcase Fallback:\n\t\t\tswitch {\n\t\t\tcase s.Value == \"\":\n\t\t\t\terrs.Add(errParameterMissingName.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\tcase !token.IsIdentifier(s.Value):\n\t\t\t\terrs.Add(errInvalidParamIdentifier.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\tcase len(segs) > (i + 1):\n\t\t\t\terrs.Add(errFallbackNotLastSegment.AtGoNode(s))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &Path{StartPos: startPos, Segments: segs}, true\n}\n\nfunc (p *Path) ToProto() *meta.Path {\n\tmp := &meta.Path{}\n\tmp.Type = meta.Path_URL\n\n\tfor _, seg := range p.Segments {\n\t\ts := &meta.PathSegment{Value: seg.Value}\n\t\tswitch seg.Type {\n\t\tcase Literal:\n\t\t\ts.Type = meta.PathSegment_LITERAL\n\t\tcase Param:\n\t\t\ts.Type = meta.PathSegment_PARAM\n\t\tcase Wildcard:\n\t\t\ts.Type = meta.PathSegment_WILDCARD\n\t\tcase Fallback:\n\t\t\ts.Type = meta.PathSegment_FALLBACK\n\t\tdefault:\n\t\t\tpanic(fmt.Sprintf(\"unhandled path segment type %v\", seg.Type))\n\t\t}\n\n\t\tif s.Type != meta.PathSegment_LITERAL {\n\t\t\tswitch seg.ValueType {\n\t\t\tcase schema.String:\n\t\t\t\ts.ValueType = meta.PathSegment_STRING\n\t\t\tcase schema.Bool:\n\t\t\t\ts.ValueType = meta.PathSegment_BOOL\n\t\t\tcase schema.Int:\n\t\t\t\ts.ValueType = meta.PathSegment_INT\n\t\t\tcase schema.Int8:\n\t\t\t\ts.ValueType = meta.PathSegment_INT8\n\t\t\tcase schema.Int16:\n\t\t\t\ts.ValueType = meta.PathSegment_INT16\n\t\t\tcase schema.Int32:\n\t\t\t\ts.ValueType = meta.PathSegment_INT32\n\t\t\tcase schema.Int64:\n\t\t\t\ts.ValueType = meta.PathSegment_INT64\n\t\t\tcase schema.Uint:\n\t\t\t\ts.ValueType = meta.PathSegment_UINT\n\t\t\tcase schema.Uint8:\n\t\t\t\ts.ValueType = meta.PathSegment_UINT8\n\t\t\tcase schema.Uint16:\n\t\t\t\ts.ValueType = meta.PathSegment_UINT16\n\t\t\tcase schema.Uint32:\n\t\t\t\ts.ValueType = meta.PathSegment_UINT32\n\t\t\tcase schema.Uint64:\n\t\t\t\ts.ValueType = meta.PathSegment_UINT64\n\t\t\tcase schema.UUID:\n\t\t\t\ts.ValueType = meta.PathSegment_UUID\n\t\t\tdefault:\n\t\t\t\tpanic(fmt.Sprintf(\"unhandled path segment value type %v\", seg.ValueType))\n\t\t\t}\n\t\t}\n\n\t\tmp.Segments = append(mp.Segments, s)\n\t}\n\treturn mp\n}\n\n// Set tracks a set of paths, ensuring they are compatible with each other.\n// The zero value is ready to use.\ntype Set struct {\n\tmethods map[string]*node\n}\n\nfunc NewSet() *Set {\n\treturn &Set{\n\t\tmethods: make(map[string]*node),\n\t}\n}\n\n// Add adds a path to the set of paths.\nfunc (s *Set) Add(errs *perr.List, method string, path *Path) (ok bool) {\n\tif s.methods == nil {\n\t\ts.methods = make(map[string]*node)\n\t}\n\n\tvar candidates []string\n\tif method == \"*\" {\n\t\t// Check all defined methods\n\t\tfor m := range s.methods {\n\t\t\tif m != method {\n\t\t\t\tcandidates = append(candidates, m)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tcandidates = []string{\"*\"}\n\t}\n\n\t// Always check the target method last, so we don't add to the set if there's an error\n\t// for another method.\n\tcandidates = append(candidates, method)\n\nCandidateLoop:\n\tfor _, m := range candidates {\n\t\tcurr := s.methods[m]\n\t\tif curr == nil {\n\t\t\tcurr = &node{}\n\t\t\ts.methods[m] = curr\n\t\t}\n\n\t\tfor _, seg := range path.Segments {\n\t\t\tnext, ok := s.match(errs, path, seg, curr)\n\t\t\tif !ok {\n\t\t\t\treturn false\n\t\t\t} else if next != nil {\n\t\t\t\tcurr = next\n\t\t\t} else {\n\t\t\t\t// Could not find a match; add it to the tree (if this is the target method)\n\t\t\t\tif m != method {\n\t\t\t\t\tcontinue CandidateLoop\n\t\t\t\t}\n\t\t\t\tcurr.children = append(curr.children, &node{s: seg})\n\t\t\t\tcurr = curr.children[len(curr.children)-1]\n\t\t\t}\n\t\t}\n\n\t\tif curr.p != nil {\n\t\t\terrs.Add(errDuplicatePath.AtGoNode(path).AtGoNode(curr.p))\n\t\t\treturn false\n\t\t} else if m == method {\n\t\t\tcurr.p = path\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (s *Set) match(errs *perr.List, path *Path, seg Segment, curr *node) (next *node, ok bool) {\n\tfor _, ch := range curr.children {\n\t\tswitch ch.s.Type {\n\t\tcase Wildcard:\n\t\t\tswitch seg.Type {\n\t\t\tcase Param:\n\t\t\t\terrs.Add(errConflictingParameterizedPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\tcase Wildcard:\n\t\t\t\terrs.Add(errConflictingWildcardPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\tcase Literal:\n\t\t\t\terrs.Add(errConflictingLiteralPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\tcase Param:\n\t\t\tswitch seg.Type {\n\t\t\tcase Param:\n\t\t\t\treturn ch, true\n\t\t\tcase Wildcard:\n\t\t\t\terrs.Add(errConflictingWildcardPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\tcase Literal:\n\t\t\t\terrs.Add(errConflictingLiteralPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\tcase Literal:\n\t\t\tswitch seg.Type {\n\t\t\tcase Wildcard:\n\t\t\t\terrs.Add(errConflictingWildcardPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\tcase Param:\n\t\t\t\terrs.Add(errConflictingLiteralPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\tcase Literal:\n\t\t\t\tif seg.Value == ch.s.Value {\n\t\t\t\t\treturn ch, true\n\t\t\t\t}\n\t\t\t}\n\t\tcase Fallback:\n\t\t\tswitch seg.Type {\n\t\t\tcase Fallback:\n\t\t\t\terrs.Add(errConflictingFallbackPath(seg.Value, ch.findPath()).\n\t\t\t\t\tAtGoNode(path).\n\t\t\t\t\tAtGoNode(ch.findPath()))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, true\n}\n\ntype node struct {\n\ts        Segment\n\tchildren []*node\n\tp        *Path // leaf path, if any\n}\n\nfunc (n *node) findPath() *Path {\n\tfor n.p == nil {\n\t\tn = n.children[0]\n\t}\n\treturn n.p\n}\n"
  },
  {
    "path": "v2/internals/resourcepaths/paths_test.go",
    "content": "package resourcepaths\n\nimport (\n\t\"context\"\n\t\"go/token\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\n\t\"encr.dev/pkg/errinsrc\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/schema\"\n)\n\nfunc init() {\n\terrinsrc.ColoursInErrors(false)\n}\n\nfunc TestParseURL(t *testing.T) {\n\tc := qt.New(t)\n\n\tstr := schema.String\n\ttests := []struct {\n\t\tPath string\n\t\tWant []Segment\n\t\tErr  string\n\t}{\n\t\t{\"foo\", nil, \"Paths must always start with a '/'\"},\n\t\t{\"/foo\", []Segment{{Literal, \"foo\", str, 0, 3}}, \"\"},\n\t\t{\"/foo/\", nil, \"Paths cannot end with a trailing slash ('/')\"},\n\t\t{\"/foo/bar\", []Segment{{Literal, \"foo\", str, 0, 3}, {Literal, \"bar\", str, 4, 7}}, \"\"},\n\t\t{\"/foo//bar\", nil, \"Paths cannot contain an empty segment, i.e. a double slash ('//').\"},\n\t\t{\"/:foo/*bar\", []Segment{{Param, \"foo\", str, 0, 4}, {Wildcard, \"bar\", str, 5, 9}}, \"\"},\n\t\t{\"/:foo/*\", nil, \"Path parameters must have a name.\"},\n\t\t{\"/:foo/*/bar\", nil, \"Path parameters must have a name.\"},\n\t\t{\"/:foo/*bar/baz\", nil, \"Path wildcards must be the last segment in the path.\"},\n\t\t{\"/:foo/*;\", nil, \"Path parameters must be valid Go identifiers.\"},\n\t\t{\"/:;\", nil, \"Path parameters must be valid Go identifiers.\"},\n\t\t{\"/\\u0000\", nil, \"invalid control character in URL\"},\n\t\t{\"/foo?bar=baz\", nil, `Paths must not contain the '?' character.`},\n\t\t{\"/foo/!fallback\", []Segment{\n\t\t\t{Literal, \"foo\", str, 0, 3},\n\t\t\t{Fallback, \"fallback\", str, 4, 13}}, \"\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tc.Run(test.Path, func(c *qt.C) {\n\t\t\terrs := perr.NewList(context.Background(), token.NewFileSet())\n\t\t\tp, ok := Parse(errs, 0, test.Path, Options{\n\t\t\t\tAllowWildcard: true,\n\t\t\t\tAllowFallback: true,\n\t\t\t\tPrefixSlash:   true,\n\t\t\t})\n\t\t\tif !ok {\n\t\t\t\tc.Assert(errs.FormatErrors(), qt.Contains, test.Err)\n\t\t\t} else if test.Err != \"\" {\n\t\t\t\tc.Fatalf(\"expected err %q, got nil\", test.Err)\n\t\t\t} else {\n\t\t\t\tc.Assert(p.Segments, qt.DeepEquals, test.Want)\n\t\t\t\tc.Assert(errs.Len(), qt.Equals, 0)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAdd(t *testing.T) {\n\tc := qt.New(t)\n\n\tpaths := []struct {\n\t\tMethod string\n\t\tPath   string\n\t\tErr    string\n\t}{\n\t\t{\"POST\", \"/foo\", ``},\n\t\t{\"POST\", \"/foo\", `Duplicate Paths found.`},\n\t\t{\"GET\", \"/foo\", ``},\n\t\t{\"*\", \"/foo\", `Duplicate Paths found.`},\n\t\t{\"*\", \"/bar\", ``},\n\t\t{\"PATCH\", \"/bar\", `Duplicate Paths found.`},\n\t\t{\"POST\", \"/foo/bar\", ``},\n\t\t{\"POST\", \"/foo/:bar\", \"The path segment `bar` conflicts with the path `/foo/bar`\"},\n\t\t{\"POST\", \"/moo/:bar\", ``},\n\t\t{\"POST\", \"/moo/:baz\", `Duplicate Paths found.`},\n\t\t{\"POST\", \"/moo/:baz/test\", ``},\n\t\t{\"POST\", \"/moo/:baa/*wild\", \"The wildcard `*wild` conflicts with the path `/moo/:baz/test`.\"},\n\t\t{\"GET\", \"/moo/:baa/*wild\", ``},\n\t\t{\"POST\", \"/test/*wild\", ``},\n\t\t{\"POST\", \"/test/*card\", \"The wildcard `*card` conflicts with the path `/test/*wild`.\"},\n\t\t{\"POST\", \"/test/!fallback\", \"\"},\n\t\t{\"GET\", \"/test/!fallback\", \"\"},\n\t\t{\"POST\", \"/test/!fallback\", \"The fallback `!fallback` conflicts with the path `/test/!fallback`.\"},\n\t}\n\n\tset := &Set{}\n\n\tfor _, test := range paths {\n\t\terrs := perr.NewList(context.Background(), token.NewFileSet())\n\t\tp, ok := Parse(errs, 0, test.Path, Options{\n\t\t\tAllowWildcard: true,\n\t\t\tAllowFallback: true,\n\t\t\tPrefixSlash:   true,\n\t\t})\n\t\tc.Assert(ok, qt.IsTrue)\n\t\tok = set.Add(errs, test.Method, p)\n\t\tif test.Err != \"\" {\n\t\t\tc.Assert(errs.FormatErrors(), qt.Contains, test.Err, qt.Commentf(\"%s %s\", test.Method, test.Path))\n\t\t\tc.Assert(ok, qt.IsFalse)\n\t\t} else {\n\t\t\tc.Assert(errs.Len(), qt.Equals, 0, qt.Commentf(\"%s %s: %v\", test.Method, test.Path, errs.AsError()))\n\t\t\tc.Assert(ok, qt.IsTrue)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/internals/scan/collect.go",
    "content": "package scan\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"golang.org/x/mod/modfile\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\n// ProcessModule parses all the packages in the module located at modRoot.\n// It calls process for each package. Multiple goroutines may call process\n// concurrently.\nfunc ProcessModule(errs *perr.List, loader *pkginfo.Loader, moduleRoot paths.FS, process func(pkg *pkginfo.Package)) {\n\t// Resolve the module path for the main module.\n\tmodFilePath := moduleRoot.Join(\"go.mod\")\n\tmodPath, err := resolveModulePath(modFilePath)\n\tif err != nil {\n\t\terrs.Add(errResolvingModulePath.InFile(modFilePath.ToIO()).Wrapping(err))\n\t\treturn\n\t}\n\n\tquit := make(chan struct{})\n\tdefer close(quit)\n\tpkgCh := Packages(quit, errs, loader, moduleRoot, paths.Pkg(modPath))\n\n\tfor pkg := range pkgCh {\n\t\tprocess(pkg)\n\t}\n}\n\n// resolveModulePath resolves the main module's module path\n// by reading the go.mod file at goModPath.\nfunc resolveModulePath(goModPath paths.FS) (paths.Mod, error) {\n\tdata, err := os.ReadFile(goModPath.ToIO())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tmodFile, err := modfile.Parse(goModPath.ToDisplay(), data, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif !paths.ValidModPath(modFile.Module.Mod.Path) {\n\t\treturn \"\", fmt.Errorf(\"invalid module path %q\", modFile.Module.Mod.Path)\n\t}\n\treturn paths.MustModPath(modFile.Module.Mod.Path), nil\n}\n"
  },
  {
    "path": "v2/internals/scan/collect_test.go",
    "content": "package scan\n\nimport (\n\t\"cmp\"\n\t\"slices\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestProcessModule(t *testing.T) {\n\tc := qt.New(t)\n\ta := testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\n-- foo/foo.go --\npackage foo\n-- foo/bar/bar.go --\npackage bar\n`)\n\ttc := testutil.NewContext(c, false, a)\n\ttc.FailTestOnErrors()\n\tloader := pkginfo.New(tc.Context)\n\n\tvar got testutil.PackageList\n\tProcessModule(tc.Errs, loader, tc.MainModuleDir, got.Collector())\n\tc.Assert(got, qt.HasLen, 2)\n\n\t// Sort the packages by import path since collectPackages processes\n\t// packages concurrently.\n\tslices.SortFunc(got, func(a, b *pkginfo.Package) int {\n\t\treturn cmp.Compare(a.ImportPath, b.ImportPath)\n\t})\n\tc.Assert(got[0].ImportPath, qt.Equals, paths.Pkg(\"example.com/foo\"))\n\tc.Assert(got[1].ImportPath, qt.Equals, paths.Pkg(\"example.com/foo/bar\"))\n}\n"
  },
  {
    "path": "v2/internals/scan/errors.go",
    "content": "package scan\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"scan\",\n\t\t\"\",\n\t\terrors.WithRangeSize(25),\n\t)\n\n\terrResolvingModulePath = errRange.New(\n\t\t\"Error Resolving Module Path\",\n\t\t\"An error occurred while trying to resolve the module path.\",\n\t)\n)\n"
  },
  {
    "path": "v2/internals/scan/scan.go",
    "content": "package scan\n\nimport (\n\t\"go/token\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\n// Packages scans and parses the Go packages for all subdirectories in the root.\nfunc Packages(quit chan struct{}, errs *perr.List, l *pkginfo.Loader, root paths.FS, basePkgPath paths.Pkg) <-chan *pkginfo.Package {\n\t// a worker accepts work in the form of package paths to parse\n\t// and sends the parsed results back on the results channel.\n\t// It calls wg.Done() when it's done.\n\tworker := func(wg *sync.WaitGroup, work <-chan paths.Pkg, results chan<- *pkginfo.Package) {\n\t\tdefer wg.Done()\n\t\tfor pkgPath := range work {\n\t\t\tif pkg, ok := l.LoadPkg(token.NoPos, pkgPath); ok {\n\t\t\t\tselect {\n\t\t\t\tcase results <- pkg:\n\t\t\t\tcase <-quit:\n\t\t\t\t\treturn // we're done\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Enqueue all the directories to parse\n\twork := make(chan paths.Pkg, 100)\n\tgo func() {\n\t\tdefer close(work) // no more work when we're done\n\t\terr := walkGoPackages(root, basePkgPath, func(pkgPath paths.Pkg) {\n\t\t\tselect {\n\t\t\tcase work <- pkgPath:\n\t\t\tcase <-quit:\n\t\t\t\treturn // we're done\n\t\t\t}\n\t\t})\n\t\tif err != nil {\n\t\t\terrs.AddStd(err)\n\t\t}\n\t}()\n\n\t// Start the workers. One per GOMAXPROCS, but at least 4\n\tnumWorkers := runtime.GOMAXPROCS(0)\n\tif numWorkers < 4 {\n\t\tnumWorkers = 4\n\t}\n\tresults := make(chan *pkginfo.Package, numWorkers)\n\tvar activeWorkers sync.WaitGroup\n\tfor i := 0; i < numWorkers; i++ {\n\t\tactiveWorkers.Add(1)\n\t\tgo worker(&activeWorkers, work, results)\n\t}\n\n\t// When all the workers are done, close the results channel\n\tgo func() {\n\t\tactiveWorkers.Wait()\n\t\tclose(results)\n\t}()\n\n\treturn results\n}\n\ntype walkFunc func(pkgPath paths.Pkg)\n\n// walkGoPackages recursively walks all subdirectories of root,\n// calling walkFn for each directory that contains a go package\n// (as indicated by the presence of any .go files).\nfunc walkGoPackages(root paths.FS, basePkgPath paths.Pkg, walkFn walkFunc) error {\n\treturn walkDir(root.ToIO(), basePkgPath, walkFn)\n}\n\nfunc walkDir(dir string, pkgPath paths.Pkg, walkFn walkFunc) error {\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Iterate through the entries and keep track of any directories\n\t// we come across as well as whether there are any Go files.\n\tvar subdirs []string\n\tfoundGoFile := false\n\tfor _, e := range entries {\n\t\tname := e.Name()\n\t\tif ignored(name) {\n\t\t\tcontinue\n\t\t} else if e.IsDir() {\n\t\t\tsubdirs = append(subdirs, name)\n\t\t} else if !foundGoFile {\n\t\t\t// Only compute if we haven't already found a .go file\n\t\t\tfoundGoFile = strings.HasSuffix(name, \".go\")\n\t\t}\n\t}\n\n\tif foundGoFile {\n\t\twalkFn(pkgPath)\n\t}\n\tfor _, d := range subdirs {\n\t\tsubDir := filepath.Join(dir, d)\n\t\tsubPkg := pkgPath.JoinSlash(paths.RelSlash(d))\n\t\tif err := walkDir(subDir, subPkg, walkFn); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// ignored returns true if a given directory should be ignored for parsing.\nfunc ignored(dir string) bool {\n\tname := filepath.Base(filepath.Clean(dir))\n\tif strings.EqualFold(name, \"node_modules\") {\n\t\treturn true\n\t}\n\t// Don't watch hidden folders like `.git` or `.idea`.\n\tif len(name) > 1 && name[0] == '.' {\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "v2/internals/scan/scan_test.go",
    "content": "package scan\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\n\t\"encr.dev/pkg/paths\"\n)\n\nfunc TestWalkDirs(t *testing.T) {\n\ttests := []struct {\n\t\t// Tree represents a directory tree. See createTree.\n\t\tTree string\n\t\t// Pkgs are the packages yielded by the walk,\n\t\t// separated by space.\n\t\tPkgs string\n\t}{\n\t\t{\"a\", \"\"},\n\t\t{\"foo/\", \"\"},\n\t\t{\"a.go\", \".\"},\n\t\t{\"a.go foo/b.go foo/bar/c foo/bar/baz/d.go\", \". foo foo/bar/baz\"},\n\t}\n\n\t// createTree creates the directory tree represented by tree.\n\t// Nodes are space-separated; dirs end with a trailing slash.\n\t//\n\t// \"a b/c d/\" represents a root with one file \"a\",\n\t// the directory \"b\" (containing file \"c\") and an empty directory \"d\".\n\t// It reports the directory root.\n\tcreateTree := func(tree string) (root string) {\n\t\troot = t.TempDir()\n\t\tfor _, node := range strings.Fields(tree) {\n\t\t\t// Create the dir if we have a slash\n\t\t\tif idx := strings.LastIndexByte(node, '/'); idx > 0 {\n\t\t\t\tp := filepath.Join(root, filepath.FromSlash(node[:idx]))\n\t\t\t\tif err := os.MkdirAll(p, 0755); err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !strings.HasSuffix(node, \"/\") {\n\t\t\t\tf, err := os.Create(filepath.Join(root, filepath.FromSlash(node)))\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tf.Close()\n\t\t\t}\n\t\t}\n\t\treturn root\n\t}\n\n\tc := qt.New(t)\n\tfor _, test := range tests {\n\t\troot := createTree(test.Tree)\n\t\tbasePkgPath := paths.MustPkgPath(\"x\")\n\t\tvar got []paths.Pkg\n\t\terr := walkGoPackages(paths.RootedFSPath(root, \".\"), basePkgPath, func(p paths.Pkg) {\n\t\t\tgot = append(got, p)\n\t\t})\n\t\tc.Assert(err, qt.IsNil)\n\n\t\t// Compare the packages\n\t\twantPkgs := strings.Fields(test.Pkgs)\n\t\twant := make([]paths.Pkg, len(wantPkgs))\n\t\tfor i, p := range wantPkgs {\n\t\t\twant[i] = basePkgPath.JoinSlash(paths.RelSlash(p))\n\t\t}\n\t\tc.Assert(got, qt.CmpEquals(cmpopts.EquateEmpty()), want, qt.Commentf(\"tree: %s\", test.Tree))\n\t}\n}\n"
  },
  {
    "path": "v2/internals/schema/decls.go",
    "content": "package schema\n\nimport (\n\t\"go/ast\"\n\t\"slices\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\n// Decl is the common interface for different kinds of declarations.\ntype Decl interface {\n\tKind() DeclKind\n\n\tDeclaredIn() *pkginfo.File\n\t// PkgName reports the name if this is a package-level declaration.\n\t// Otherwise it reports None.\n\tPkgName() option.Option[string]\n\n\t// ASTNode returns the AST node that this declaration represents.\n\t// It's a *ast.FuncDecl or *ast.TypeSpec.\n\tASTNode() ast.Node\n\t// String returns the shorthand name for this declaration,\n\t// in the form \"pkgname.DeclName\".\n\tString() string\n\t// TypeParameters are the type parameters on this declaration.\n\tTypeParameters() []DeclTypeParam\n}\n\n// DeclKind represents different kinds of declarations.\ntype DeclKind int\n\nconst (\n\t// DeclType represents a type declaration.\n\tDeclType DeclKind = iota\n\t// DeclFunc represents a func declaration.\n\tDeclFunc\n)\n\n// TypeDecl represents a type declaration.\ntype TypeDecl struct {\n\t// AST is the AST node that this declaration represents.\n\tAST *ast.TypeSpec\n\n\tInfo *pkginfo.PkgDeclInfo // the underlying declaration info\n\tFile *pkginfo.File        // the file declaring the type\n\tName string               // name of the type declaration\n\tType Type                 // the declaration's underlying type\n\n\t// TypeParams are any type parameters on this declaration.\n\t// (note: instantiated types used within this declaration would not be captured here)\n\tTypeParams []DeclTypeParam\n}\n\nfunc (d *TypeDecl) Clone() *TypeDecl {\n\treturn &TypeDecl{\n\t\tAST:        d.AST,\n\t\tInfo:       d.Info,\n\t\tFile:       d.File,\n\t\tName:       d.Name,\n\t\tType:       d.Type,\n\t\tTypeParams: slices.Clone(d.TypeParams),\n\t}\n}\n\n// DeclTypeParam represents a type parameter on a declaration.\n// For example A in \"type Foo[A any] struct { ... }\"\ntype DeclTypeParam struct {\n\t// AST is the AST node that this type param represents.\n\t// Note that multiple fields may share the same *ast.Field node,\n\t// in cases with multiple names, like \"type Foo[A, B any]\".\n\tAST *ast.Field\n\n\tName string // the identifier given to the type parameter.\n}\n\n// A FuncDecl represents a function declaration.\ntype FuncDecl struct {\n\tAST *ast.FuncDecl\n\n\tFile *pkginfo.File            // the file declaring the type\n\tName string                   // the name of the function\n\tRecv option.Option[*Receiver] // none if not a method\n\tType FuncType                 // signature\n\n\t// TypeParams are any type parameters on this declaration.\n\t// (note: instantiated types used within this declaration would not be captured here)\n\tTypeParams []DeclTypeParam\n}\n\n// A Receiver represents a method receiver.\n// It describes the name and type of the receiver.\ntype Receiver struct {\n\tAST *ast.FieldList\n\t// Name is the name of the receiver (e.g. \"a\" in \"func (a *Foo) Bar()\").\n\t// It's None if the receiver is unnamed (e.g. \"func (*Foo) Bar()\").\n\tName option.Option[string]\n\n\t// Type is the type of the receiver.\n\t// It's either a NamedType or a NamedType wrapped in a PointerType.\n\tType Type\n\n\t// Decl is the underlying type declaration the receiver points to.\n\tDecl *TypeDecl\n}\n\nfunc (*TypeDecl) Kind() DeclKind                    { return DeclType }\nfunc (d *TypeDecl) DeclaredIn() *pkginfo.File       { return d.File }\nfunc (d *TypeDecl) ASTNode() ast.Node               { return d.AST }\nfunc (d *TypeDecl) String() string                  { return d.File.Pkg.Name + \".\" + d.Name }\nfunc (d *TypeDecl) TypeParameters() []DeclTypeParam { return d.TypeParams }\nfunc (d *TypeDecl) PkgName() option.Option[string]  { return option.Some(d.Name) }\n\nfunc (*FuncDecl) Kind() DeclKind                    { return DeclFunc }\nfunc (d *FuncDecl) DeclaredIn() *pkginfo.File       { return d.File }\nfunc (d *FuncDecl) ASTNode() ast.Node               { return d.AST }\nfunc (d *FuncDecl) String() string                  { return d.File.Pkg.Name + \".\" + d.Name }\nfunc (d *FuncDecl) TypeParameters() []DeclTypeParam { return d.TypeParams }\nfunc (d *FuncDecl) PkgName() option.Option[string] {\n\tif d.Recv.Empty() {\n\t\treturn option.Some(d.Name)\n\t}\n\treturn option.None[string]()\n}\n\n// ParseTypeDecl parses the type from a package declaration.\n// It errors if the declaration is not a type.\nfunc (p *Parser) ParseTypeDecl(d *pkginfo.PkgDeclInfo) *TypeDecl {\n\tpkg := d.File.Pkg\n\n\t// Have we already parsed this?\n\tkey := declKey{pkg: pkg.ImportPath, name: d.Name}\n\tp.declsMu.Lock()\n\tcached, ok := p.decls[key]\n\tp.declsMu.Unlock()\n\n\tif ok {\n\t\tif td, ok := cached.(*TypeDecl); ok {\n\t\t\treturn td\n\t\t} else {\n\t\t\tp.c.Errs.Fatalf(d.Spec.Pos(), \"decl %s is not a TypeDecl\", d.Name)\n\t\t}\n\t}\n\n\t// We haven't parsed this yet; do so now.\n\t// Allocate a decl immediately so that we can properly handle\n\t// recursive types by short-circuiting above the second time we get here.\n\tspec, ok := d.Spec.(*ast.TypeSpec)\n\tif !ok {\n\t\tp.c.Errs.Fatal(d.Spec.Pos(), \"unable to get TypeSpec from PkgDecl spec\")\n\t}\n\n\tdecl := &TypeDecl{\n\t\tAST:        spec,\n\t\tName:       d.Name,\n\t\tFile:       d.File,\n\t\tInfo:       d,\n\t\tTypeParams: nil,\n\t\t// Type is set below\n\t}\n\tp.declsMu.Lock()\n\tp.decls[key] = decl\n\tp.declsMu.Unlock()\n\n\t// If this is a parameterized declaration, get the type parameters\n\tvar typeParamsInScope map[string]int\n\ttypeParamsInScope, decl.TypeParams = computeDeclTypeParams(spec.TypeParams)\n\n\tr := p.newTypeResolver(decl, typeParamsInScope)\n\tdecl.Type = r.parseType(d.File, spec.Type)\n\n\treturn decl\n}\n\n// ParseFuncDecl parses the func from a package declaration.\n// It errors if the type is not a func declaration.\nfunc (p *Parser) ParseFuncDecl(file *pkginfo.File, fd *ast.FuncDecl) (*FuncDecl, bool) {\n\t// Have we already parsed this?\n\tkey := declKey{pkg: file.Pkg.ImportPath, name: fd.Name.Name}\n\n\t// Is there a receiver? If so we need to add that to the cache key.\n\tvar recv option.Option[*Receiver]\n\tif fd.Recv != nil {\n\t\tr, ok := p.parseRecv(file, fd.Recv)\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\t\tkey.recvName = r.Decl.Name\n\t\trecv = option.Some(r)\n\t}\n\n\tp.declsMu.Lock()\n\tcached, ok := p.decls[key]\n\tp.declsMu.Unlock()\n\n\tif ok {\n\t\tif fd, ok := cached.(*FuncDecl); ok {\n\t\t\treturn fd, true\n\t\t} else {\n\t\t\tp.c.Errs.Add(errDeclIsntFunction(fd.Name).AtGoNode(fd.AST))\n\t\t\treturn nil, false\n\t\t}\n\t}\n\n\t// We haven't parsed this yet; do so now.\n\t// Allocate a decl immediately so that we can properly handle\n\t// recursive types by short-circuiting above the second time we get here.\n\n\tdecl := &FuncDecl{\n\t\tAST:  fd,\n\t\tFile: file,\n\t\tName: fd.Name.Name,\n\t\tRecv: recv,\n\t\t// Type, and TypeParams are set below\n\t}\n\tp.declsMu.Lock()\n\tp.decls[key] = decl\n\tp.declsMu.Unlock()\n\n\t// If this is a parameterized declaration, get the type parameters.\n\t// If the function is a method, get the type parameters from the receiver's type declaration.\n\t// Otherwise, use the ones on the function, if any.\n\tvar typeParamsInScope map[string]int\n\tif recv.Present() {\n\t\t// Type params on the receiver do not become type params on the func declaration,\n\t\t// so we use \"_\" to ignore that value, unlike in the \"else\" case below.\n\t\ttypeParamsInScope, _ = computeDeclTypeParams(recv.MustGet().Decl.AST.TypeParams)\n\t} else {\n\t\ttypeParamsInScope, decl.TypeParams = computeDeclTypeParams(fd.Type.TypeParams)\n\t}\n\n\t// Resolve the function type.\n\tr := p.newTypeResolver(decl, typeParamsInScope)\n\tdecl.Type = r.parseFuncType(file, fd.Type)\n\treturn decl, true\n}\n\n// computeDeclTypeParams computes the type parameter placeholders for a declaration.\n// For example, given the \"[A, B any]\" in the following declaration:\n//\n//\ttype Foo[A, B any] struct { ... }\n//\n// it returns:\n//\n//\tnameMap = {\"A\": 0, \"B\": 1}\n//\tparams = [{Name: \"A\"}, {Name: \"B\"}]\nfunc computeDeclTypeParams(typeParams *ast.FieldList) (nameMap map[string]int, params []DeclTypeParam) {\n\tif typeParams == nil {\n\t\treturn nil, nil\n\t}\n\tnumParams := typeParams.NumFields()\n\tparams = make([]DeclTypeParam, 0, numParams)\n\tnameMap = make(map[string]int, numParams)\n\n\tparamIdx := 0\n\tfor _, typeParam := range typeParams.List {\n\t\tfor _, name := range typeParam.Names {\n\t\t\tparams = append(params, DeclTypeParam{\n\t\t\t\tAST:  typeParam,\n\t\t\t\tName: name.Name,\n\t\t\t})\n\t\t\tnameMap[name.Name] = paramIdx\n\t\t\tparamIdx++\n\t\t}\n\t}\n\treturn nameMap, params\n}\n\n// declKey is a unique key for the given declaration.\ntype declKey struct {\n\tpkg  paths.Pkg\n\tname string\n\n\t// recvName specifies the name of the receiver for method declarations.\n\trecvName string\n}\n"
  },
  {
    "path": "v2/internals/schema/errors.go",
    "content": "package schema\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"schema\",\n\t\t\"For more information, see https://encore.dev/docs/develop/api-schemas\",\n\n\t\terrors.WithRangeSize(10),\n\t)\n\n\terrExpectedOnReciever = errRange.Newf(\n\t\t\"Invalid receiver\",\n\t\t\"Expected exactly 1 receiver, got %d.\",\n\t)\n\n\terrUnknownIdentifier = errRange.Newf(\n\t\t\"Unknown identifier\",\n\t\t\"Unknown identifier `%s`\",\n\t)\n\n\terrDeclIsntFunction = errRange.Newf(\n\t\t\"Invalid declaration\",\n\t\t\"Declaration `%s` is not a function\",\n\t)\n)\n"
  },
  {
    "path": "v2/internals/schema/schema_parser.go",
    "content": "// Package schema implements parsing of Go types into Encore's schema format.\npackage schema\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"go/types\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/fatih/structtag\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\n// NewParser constructs a new schema parser.\nfunc NewParser(c *parsectx.Context, l *pkginfo.Loader) *Parser {\n\treturn &Parser{\n\t\tc:     c,\n\t\tl:     l,\n\t\tdecls: make(map[declKey]Decl),\n\t}\n}\n\n// Parser parses Go types into Encore's schema format.\ntype Parser struct {\n\tc *parsectx.Context\n\tl *pkginfo.Loader\n\n\tdeclsMu sync.Mutex\n\tdecls   map[declKey]Decl // pkg/path.Name -> decl\n}\n\n// ParseType parses the schema from a type expression.\nfunc (p *Parser) ParseType(file *pkginfo.File, expr ast.Expr) Type {\n\tr := p.newTypeResolver(nil, nil)\n\treturn r.parseType(file, expr)\n}\n\n// newTypeResolver is a helper function to create a new typeResolver.\nfunc (p *Parser) newTypeResolver(decl Decl, typeParamsInScope map[string]int) *typeResolver {\n\treturn &typeResolver{\n\t\tp:                 p,\n\t\terrs:              p.c.Errs,\n\t\tdecl:              decl,\n\t\ttypeParamsInScope: typeParamsInScope,\n\t}\n}\n\n// typeResolver resolves types from AST expressions.\ntype typeResolver struct {\n\t// p is the parser that created this type resolver.\n\tp    *Parser\n\terrs *perr.List\n\n\t// decl is the declaration being parsed, if any.\n\t// It's nil if the type expression isn't attached to a declaration.\n\tdecl Decl\n\n\t// typeParamsInScope contains the in-scope type parameters\n\t// as part of the declaration being processed.\n\t// It is nil if no declaration is being processed.\n\t//\n\t// The keys are the names of the type parameter (\"T\" and \"U\" in \"type Foo[T any, U io.Reader]\")\n\t// and the values are the index of the type parameter in the declaration (above, 0 for T and 1 for U).\n\ttypeParamsInScope map[string]int\n}\n\n// parseType parses a type expression and returns it.\n//\n// This function will never return nil as it will Bailout upon any error.\nfunc (r *typeResolver) parseType(file *pkginfo.File, expr ast.Expr) Type {\n\ttyp := func() Type {\n\t\tswitch expr := expr.(type) {\n\t\tcase *ast.StarExpr:\n\t\t\t// Pointer\n\t\t\treturn PointerType{\n\t\t\t\tAST:  expr,\n\t\t\t\tElem: r.parseType(file, expr.X),\n\t\t\t}\n\n\t\tcase *ast.Ident:\n\t\t\tpkgNames := file.Pkg.Names()\n\n\t\t\t// Check if we have a type parameter defined for this\n\t\t\tif idx, ok := r.typeParamsInScope[expr.Name]; ok {\n\t\t\t\treturn TypeParamRefType{\n\t\t\t\t\tAST:   expr,\n\t\t\t\t\tDecl:  r.decl,\n\t\t\t\t\tIndex: idx,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Local type name\n\t\t\tif d, ok := pkgNames.PkgDecls[expr.Name]; ok && d.Type == token.TYPE {\n\t\t\t\treturn newNamedType(r.p, expr, d)\n\t\t\t}\n\n\t\t\t// Finally check if it's a built-in type\n\t\t\tif b, ok := builtinTypes[expr.Name]; ok {\n\t\t\t\tif b == unsupported {\n\t\t\t\t\tr.errs.Addf(expr.Pos(), \"unsupported type: %s\", expr.Name)\n\t\t\t\t}\n\t\t\t\treturn BuiltinType{\n\t\t\t\t\tAST:  expr,\n\t\t\t\t\tKind: b,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tswitch expr.Name {\n\t\t\tcase \"any\":\n\t\t\t\treturn InterfaceType{\n\t\t\t\t\tAST: &ast.InterfaceType{\n\t\t\t\t\t\t// HACK: Set dummy positions to make the error messages nicer,\n\t\t\t\t\t\t// pointing at \"any\" instead of reporting no position whatsoever.\n\t\t\t\t\t\tInterface: expr.Pos(),\n\t\t\t\t\t\tMethods: &ast.FieldList{\n\t\t\t\t\t\t\tOpening: expr.Pos(),\n\t\t\t\t\t\t\tClosing: expr.End() - 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tr.errs.Addf(expr.Pos(), \"undefined type: %s\", expr.Name)\n\n\t\tcase *ast.SelectorExpr:\n\t\t\tfileNames := file.Names()\n\n\t\t\t// pkg.T\n\t\t\tif pkgName, ok := expr.X.(*ast.Ident); ok {\n\t\t\t\tpkgPath, ok := fileNames.ResolvePkgPath(pkgName.Pos(), pkgName.Name)\n\t\t\t\tif !ok {\n\t\t\t\t\tr.errs.Addf(expr.X.Pos(), \"unknown package: %s\", pkgName.Name)\n\t\t\t\t\tr.errs.Bailout()\n\t\t\t\t}\n\n\t\t\t\t// Do we have a supported builtin?\n\t\t\t\tif kind, ok := r.parseEncoreBuiltin(pkgPath, expr.Sel.Name); ok {\n\t\t\t\t\treturn BuiltinType{AST: expr, Kind: kind}\n\t\t\t\t}\n\n\t\t\t\t// Otherwise, load the external package and resolve the type.\n\t\t\t\totherPkg := r.p.l.MustLoadPkg(pkgName.Pos(), pkgPath)\n\t\t\t\tif d, ok := otherPkg.Names().PkgDecls[expr.Sel.Name]; ok && d.Type == token.TYPE {\n\t\t\t\t\treturn newNamedType(r.p, expr, d)\n\t\t\t\t}\n\t\t\t}\n\t\t\tr.errs.Addf(expr.Pos(), \"%s is not a type\", types.ExprString(expr))\n\n\t\tcase *ast.StructType:\n\t\t\tst := StructType{\n\t\t\t\tAST:    expr,\n\t\t\t\tFields: make([]StructField, 0, expr.Fields.NumFields()),\n\t\t\t}\n\n\t\t\tfor _, field := range expr.Fields.List {\n\t\t\t\ttyp := r.parseType(file, field.Type)\n\t\t\t\tif len(field.Names) == 0 {\n\t\t\t\t\t// r.errs.AddPos(field.Pos(), \"cannot use anonymous fields in Encore struct types\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Parse the struct tags, if any.\n\t\t\t\tvar tags structtag.Tags\n\t\t\t\tif field.Tag != nil {\n\t\t\t\t\tval, _ := strconv.Unquote(field.Tag.Value)\n\t\t\t\t\tt, err := structtag.Parse(val)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tr.errs.Addf(field.Tag.Pos(), \"invalid struct tag: %v\", err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttags = *t\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdocs := field.Doc.Text()\n\t\t\t\tif docs == \"\" {\n\t\t\t\t\tdocs = field.Comment.Text()\n\t\t\t\t}\n\n\t\t\t\tfor _, name := range field.Names {\n\t\t\t\t\tst.Fields = append(st.Fields, StructField{\n\t\t\t\t\t\tAST:  field,\n\t\t\t\t\t\tName: option.Some(name.Name),\n\t\t\t\t\t\tType: typ,\n\t\t\t\t\t\tTag:  tags,\n\t\t\t\t\t\tDoc:  docs,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn st\n\n\t\tcase *ast.MapType:\n\t\t\tkey := r.parseType(file, expr.Key)\n\t\t\tvalue := r.parseType(file, expr.Value)\n\t\t\treturn MapType{\n\t\t\t\tAST:   expr,\n\t\t\t\tKey:   key,\n\t\t\t\tValue: value,\n\t\t\t}\n\n\t\tcase *ast.ArrayType:\n\t\t\telem := r.parseType(file, expr.Elt)\n\n\t\t\tresult := ListType{AST: expr, Len: -1, Elem: elem}\n\t\t\tif expr.Len == nil {\n\t\t\t\t// We have a slice, not an array.\n\n\t\t\t\t// Translate list of bytes to the builtin Bytes kind.\n\t\t\t\tif elem.Family() == Builtin && elem.(BuiltinType).Kind == Uint8 {\n\t\t\t\t\treturn BuiltinType{AST: expr, Kind: Bytes}\n\t\t\t\t}\n\t\t\t\treturn result\n\t\t\t}\n\n\t\t\t// We have an array. Determine its length.\n\n\t\t\t// It's possible to define arrays with length equal to some constant\n\t\t\t// expression, but we don't support parsing that right now and treat\n\t\t\t// it as a slice.\n\t\t\tif basicLit, ok := expr.Len.(*ast.BasicLit); ok && basicLit.Kind == token.INT {\n\t\t\t\tif n, err := strconv.Atoi(basicLit.Value); err == nil {\n\t\t\t\t\tresult.Len = n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to a list type if we couldn't determine the length.\n\t\t\treturn result\n\n\t\tcase *ast.InterfaceType:\n\t\t\t// TODO(andre) Parse more complete information about the interface.\n\t\t\ttyp := InterfaceType{AST: expr}\n\n\t\t\tif expr.Methods != nil {\n\t\t\t\tfor _, field := range expr.Methods.List {\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase len(field.Names) > 0:\n\t\t\t\t\t\ttyp.Methods = append(typ.Methods, field)\n\t\t\t\t\tcase field.Type == nil:\n\t\t\t\t\t\t// shouldn't happen but let's be defensive\n\t\t\t\t\tdefault:\n\t\t\t\t\t\t// type switch or embedded interface\n\t\t\t\t\t\tswitch field.Type.(type) {\n\t\t\t\t\t\tcase *ast.BinaryExpr, *ast.UnaryExpr:\n\t\t\t\t\t\t\ttyp.TypeLists = append(typ.TypeLists, field.Type)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tt := r.parseType(file, field.Type)\n\t\t\t\t\t\t\ttyp.EmbeddedIfaces = append(typ.EmbeddedIfaces, t)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn typ\n\n\t\tcase *ast.ChanType:\n\t\t\tr.errs.AddPos(expr.Pos(), \"cannot use channel types in Encore schema definitions\")\n\n\t\tcase *ast.FuncType:\n\t\t\treturn r.parseFuncType(file, expr)\n\n\t\tcase *ast.IndexExpr:\n\t\t\t// Generic type application with a single type, like \"Foo[int]\"\n\t\t\treturn r.resolveTypeWithTypeArgs(file, expr, expr.X, []ast.Expr{expr.Index})\n\n\t\tcase *ast.IndexListExpr:\n\t\t\t// Same as IndexExpr, but with multiple types, like \"Foo[int, string]\"\n\t\t\treturn r.resolveTypeWithTypeArgs(file, expr, expr.X, expr.Indices)\n\n\t\tdefault:\n\t\t\tr.errs.Addf(expr.Pos(), \"%s is not a supported type; got %+v\",\n\t\t\t\ttypes.ExprString(expr), reflect.TypeOf(expr))\n\t\t}\n\t\tr.errs.Bailout()\n\t\treturn nil\n\t}()\n\n\treturn typ\n}\n\n// parseFuncType parses an *ast.FuncType into a *FuncType.\nfunc (r *typeResolver) parseFuncType(file *pkginfo.File, ft *ast.FuncType) FuncType {\n\tres := FuncType{\n\t\tAST:     ft,\n\t\tParams:  make([]Param, 0, ft.Params.NumFields()),\n\t\tResults: make([]Param, 0, ft.Results.NumFields()),\n\t}\n\n\t// iters describes how to iterate over the parameters and results.\n\titers := []struct {\n\t\tfields *ast.FieldList\n\t\tdst    *[]Param\n\t}{\n\t\t{ft.Params, &res.Params},\n\t\t{ft.Results, &res.Results},\n\t}\n\n\t// Loop over all the fields and parse the types.\n\tfor _, it := range iters {\n\t\t// The fields are nil if the function has no parameters or results.\n\t\tif it.fields == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, field := range it.fields.List {\n\t\t\ttyp := r.parseType(file, field.Type)\n\t\t\t// If we have any names it means they all have names.\n\t\t\tif len(field.Names) > 0 {\n\t\t\t\tfor _, name := range field.Names {\n\t\t\t\t\t*it.dst = append(*it.dst, Param{\n\t\t\t\t\t\tAST:  field,\n\t\t\t\t\t\tName: option.Some(name.Name),\n\t\t\t\t\t\tType: typ,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Otherwise we have a type-only parameter.\n\t\t\t\t*it.dst = append(*it.dst, Param{\n\t\t\t\t\tAST:  field,\n\t\t\t\t\tName: option.None[string](),\n\t\t\t\t\tType: typ,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res\n}\n\nfunc (r *typeResolver) resolveTypeWithTypeArgs(file *pkginfo.File, indexExpr, expr ast.Expr, typeArgs []ast.Expr) Type {\n\tbaseType := r.parseType(file, expr)\n\tif baseType.Family() != Named {\n\t\tr.errs.Addf(expr.Pos(), \"cannot use type arguments with non-named type %s\", types.ExprString(baseType.ASTExpr()))\n\t\treturn baseType\n\t}\n\n\tnamed := baseType.(NamedType)\n\n\tnamed.AST = indexExpr // Use the index expression as the AST for the named type as we've resolved the type arguments.\n\tdecl := named.Decl()\n\tif len(decl.TypeParams) != len(typeArgs) {\n\t\tr.errs.Addf(expr.Pos(), \"expected %d type parameters, got %d for reference to %s\",\n\t\t\tlen(decl.TypeParams), len(typeArgs), decl.Name)\n\t\treturn baseType\n\t}\n\n\tnamed.TypeArgs = make([]Type, len(typeArgs))\n\tfor idx, expr := range typeArgs {\n\t\tnamed.TypeArgs[idx] = r.parseType(file, expr)\n\t}\n\n\t// Is this an option.Option type? If so, we return an OptionType instead.\n\tif info := named.DeclInfo.QualifiedName(); info.PkgPath == optionImportPath && info.Name == \"Option\" {\n\t\tif len(named.TypeArgs) != 1 {\n\t\t\tr.errs.Addf(expr.Pos(), \"option.Option must have exactly one type argument, got %d\", len(named.TypeArgs))\n\t\t\treturn named\n\t\t}\n\t\treturn OptionType{AST: expr, Value: named.TypeArgs[0]}\n\t}\n\n\treturn named\n}\n\nconst (\n\tuuidImportPath   paths.Pkg = \"encore.dev/types/uuid\"\n\toptionImportPath paths.Pkg = \"encore.dev/types/option\"\n\tauthImportPath   paths.Pkg = \"encore.dev/beta/auth\"\n)\n\n// parseRecv parses a receiver AST into a Receiver.\nfunc (p *Parser) parseRecv(f *pkginfo.File, fields *ast.FieldList) (*Receiver, bool) {\n\tif fields.NumFields() != 1 {\n\t\tp.c.Errs.Add(errExpectedOnReciever(fields.NumFields()).AtGoNode(fields))\n\t\treturn nil, false\n\t}\n\n\t// To properly parse the receiver in the presence of type parameters,\n\t// we first need to resolve what the named type is that the receiver is attached to\n\t// WITHOUT the type parameters. Then we can construct a typeResolver\n\t// with the correct decl context.\n\tfield := fields.List[0]\n\trecvIdent := p.resolveReceiverIdent(field.Type)\n\tpkgDecl := f.Pkg.Names().PkgDecls[recvIdent.Name]\n\tif pkgDecl == nil {\n\t\tp.c.Errs.Add(errUnknownIdentifier(recvIdent.Name).AtGoNode(recvIdent))\n\t\treturn nil, false\n\t}\n\tdecl := p.ParseTypeDecl(pkgDecl)\n\n\t// Now that we have the declaration we can create a type resolver that\n\t// uses the type declaration's type parameters, and use that to resolve\n\t// the receiver type.\n\ttypeParamsInScope, _ := computeDeclTypeParams(decl.AST.TypeParams)\n\ttr := p.newTypeResolver(decl, typeParamsInScope)\n\n\trecv := &Receiver{\n\t\tAST:  fields,\n\t\tDecl: decl,\n\t\tType: tr.parseType(f, field.Type),\n\t}\n\tif len(field.Names) > 0 {\n\t\trecv.Name = option.Some(field.Names[0].Name)\n\t}\n\n\treturn recv, true\n}\n\n// parseEncoreBuiltin returns the builtin kind for the given package path and name.\n// If the type is not one of the Encore builtins it reports (unsupported, false).\nfunc (r *typeResolver) parseEncoreBuiltin(pkgPath paths.Pkg, name string) (BuiltinKind, bool) {\n\tswitch {\n\tcase pkgPath == uuidImportPath && name == \"UUID\":\n\t\treturn UUID, true\n\tcase pkgPath == authImportPath && name == \"UID\":\n\t\treturn UserID, true\n\tcase pkgPath == \"time\" && name == \"Time\":\n\t\treturn Time, true\n\tcase pkgPath == \"encoding/json\" && name == \"RawMessage\":\n\t\treturn JSON, true\n\t}\n\treturn unsupported, false\n}\n\nvar builtinTypes = map[string]BuiltinKind{\n\t\"bool\":       Bool,\n\t\"int\":        Int,\n\t\"int8\":       Int8,\n\t\"int16\":      Int16,\n\t\"int32\":      Int32,\n\t\"int64\":      Int64,\n\t\"uint\":       Uint,\n\t\"uint8\":      Uint8,\n\t\"uint16\":     Uint16,\n\t\"uint32\":     Uint32,\n\t\"uint64\":     Uint64,\n\t\"uintptr\":    unsupported,\n\t\"float32\":    Float32,\n\t\"float64\":    Float64,\n\t\"complex64\":  unsupported,\n\t\"complex128\": unsupported,\n\t\"string\":     String,\n\t\"byte\":       Uint8,\n\t\"rune\":       Uint32,\n\t\"error\":      Error,\n}\n\n// resolveReceiverIdent resolves the identifier for the receiver of a method.\n// It recurses through *ast.StarExpr, *ast.IndexExpr and *ast.IndexListExpr\n// to handle pointer/non-pointer as well as methods on generic types.\nfunc (p *Parser) resolveReceiverIdent(expr ast.Expr) *ast.Ident {\n\torig := expr // keep track of original for error messages\n\tfor i := 0; i < 10; i++ {\n\t\tswitch x := expr.(type) {\n\t\tcase *ast.Ident:\n\t\t\t// We're done\n\t\t\treturn x\n\n\t\tcase *ast.StarExpr:\n\t\t\texpr = x.X\n\t\tcase *ast.IndexExpr:\n\t\t\texpr = x.X\n\t\tcase *ast.IndexListExpr:\n\t\t\texpr = x.X\n\t\tdefault:\n\t\t\tp.c.Errs.Addf(orig.Pos(), \"invalid receiver expression: %s (invalid type %T)\", types.ExprString(orig), x)\n\t\t}\n\t}\n\tp.c.Errs.Fatalf(orig.Pos(), \"invalid receiver expression: %s (recursion limit reached)\", types.ExprString(orig))\n\treturn nil // unreachable\n}\n\n// newNamedType is a helper to construct a lazy-loaded NamedType.\nfunc newNamedType(p *Parser, expr ast.Expr, info *pkginfo.PkgDeclInfo) NamedType {\n\treturn NamedType{\n\t\tAST:      expr,\n\t\tDeclInfo: info,\n\t\tdecl: &lazyDecl{\n\t\t\tp:    p,\n\t\t\tinfo: info,\n\t\t},\n\t}\n}\n\n// newEagerNamedType is a helper to construct an eagerly loaded NamedType.\nfunc newEagerNamedType(expr ast.Expr, typeArgs []Type, decl *TypeDecl) NamedType {\n\tlazy := &lazyDecl{}\n\tlazy.once.Do(func() {}) // mark the lazy.once as used\n\tlazy.decl = decl\n\n\treturn NamedType{\n\t\tAST:      expr,\n\t\tDeclInfo: decl.Info,\n\t\tTypeArgs: typeArgs,\n\t\tdecl:     lazy,\n\t}\n}\n"
  },
  {
    "path": "v2/internals/schema/schema_parser_test.go",
    "content": "package schema\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/fatih/structtag\"\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestParser_ParseType(t *testing.T) {\n\ttype testCase struct {\n\t\tname     string\n\t\timports  []string\n\t\ttyp      string\n\t\twant     Type\n\t\twantErrs []string\n\t}\n\n\tfile := fileForPkg(\"foo\", \"example.com\")\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"builtin_int\",\n\t\t\ttyp:  \"int\",\n\t\t\twant: BuiltinType{Kind: Int, AST: ast.NewIdent(\"int\")},\n\t\t},\n\t\t{\n\t\t\tname:     \"unsupported_builtin\",\n\t\t\ttyp:      \"uintptr\",\n\t\t\twantErrs: []string{\".*unsupported type: uintptr\"},\n\t\t},\n\t\t{\n\t\t\tname: \"pointer\",\n\t\t\ttyp:  \"*string\",\n\t\t\twant: PointerType{Elem: BuiltinType{Kind: String, AST: ast.NewIdent(\"string\")}},\n\t\t},\n\t\t{\n\t\t\tname: \"decl\",\n\t\t\ttyp:  \"foo\\n\\ntype foo int\",\n\t\t\twant: namedTypeWithDecl(&TypeDecl{\n\t\t\t\tName: \"foo\",\n\t\t\t\tType: BuiltinType{Kind: Int, AST: ast.NewIdent(\"int\")},\n\t\t\t\tFile: file,\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"decl_with_type_params\",\n\t\t\ttyp:  \"foo[int]\\n\\ntype foo[T any] T\",\n\t\t\twant: namedTypeWithDecl(\n\t\t\t\t(func() *TypeDecl {\n\t\t\t\t\td := new(TypeDecl)\n\t\t\t\t\t*d = TypeDecl{\n\t\t\t\t\t\tName: \"foo\",\n\t\t\t\t\t\tFile: file,\n\t\t\t\t\t\tType: TypeParamRefType{\n\t\t\t\t\t\t\tAST:   ast.NewIdent(\"T\"),\n\t\t\t\t\t\t\tIndex: 0,\n\t\t\t\t\t\t\tDecl:  d,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTypeParams: []DeclTypeParam{\n\t\t\t\t\t\t\t{Name: \"T\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\treturn d\n\t\t\t\t})(),\n\t\t\t\tBuiltinType{Kind: Int, AST: ast.NewIdent(\"int\")},\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tname:    \"builtin_encore_uuid\",\n\t\t\timports: []string{\"encore.dev/types/uuid\"},\n\t\t\ttyp:     \"uuid.UUID\",\n\t\t\twant:    BuiltinType{Kind: UUID, AST: ast.NewIdent(\"uuid.UUID\")},\n\t\t},\n\t\t{\n\t\t\tname:    \"encore_option\",\n\t\t\timports: []string{\"encore.dev/types/option\", \"encore.dev/types/uuid\"},\n\t\t\ttyp:     \"option.Option[uuid.UUID]\",\n\t\t\twant:    OptionType{AST: ast.NewIdent(\"option.Option\"), Value: BuiltinType{Kind: UUID, AST: ast.NewIdent(\"uuid.UUID\")}},\n\t\t},\n\t\t{\n\t\t\tname:    \"builtin_encore_userid\",\n\t\t\timports: []string{\"encore.dev/beta/auth\"},\n\t\t\ttyp:     \"auth.UID\",\n\t\t\twant:    BuiltinType{Kind: UserID, AST: ast.NewIdent(\"auth.UID\")},\n\t\t},\n\t\t{\n\t\t\tname:    \"builtin_time\",\n\t\t\timports: []string{\"time\"},\n\t\t\ttyp:     \"time.Time\",\n\t\t\twant:    BuiltinType{Kind: Time, AST: ast.NewIdent(\"time.Time\")},\n\t\t},\n\t\t{\n\t\t\tname:    \"builtin_json\",\n\t\t\timports: []string{\"encoding/json\"},\n\t\t\ttyp:     \"json.RawMessage\",\n\t\t\twant:    BuiltinType{Kind: JSON, AST: ast.NewIdent(\"json.RawMessage\")},\n\t\t},\n\t\t{\n\t\t\tname: \"builtin_error\",\n\t\t\ttyp:  \"error\",\n\t\t\twant: BuiltinType{Kind: Error, AST: ast.NewIdent(\"error\")},\n\t\t},\n\t\t{\n\t\t\tname:    \"external_stdlib_type\",\n\t\t\timports: []string{\"database/sql\"},\n\t\t\ttyp:     \"sql.NullString\",\n\t\t\twant: namedTypeWithDecl(&TypeDecl{\n\t\t\t\tName: \"NullString\",\n\t\t\t\tFile: fileForPkg(\"sql\", \"database/sql\"),\n\t\t\t\tType: StructType{\n\t\t\t\t\tFields: []StructField{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: option.Some(\"String\"),\n\t\t\t\t\t\t\tType: BuiltinType{Kind: String},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: option.Some(\"Valid\"),\n\t\t\t\t\t\t\tType: BuiltinType{Kind: Bool},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"map\",\n\t\t\ttyp:  \"map[struct{A int}]struct{}\",\n\t\t\twant: MapType{\n\t\t\t\tKey: StructType{Fields: []StructField{\n\t\t\t\t\t{Name: option.Some(\"A\"), Type: BuiltinType{Kind: Int}},\n\t\t\t\t}},\n\t\t\t\tValue: StructType{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"slice\",\n\t\t\ttyp:  \"[]bool\",\n\t\t\twant: ListType{\n\t\t\t\tElem: BuiltinType{Kind: Bool, AST: ast.NewIdent(\"bool\")},\n\t\t\t\tLen:  -1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"array\",\n\t\t\ttyp:  \"[3]bool\",\n\t\t\twant: ListType{\n\t\t\t\tElem: BuiltinType{Kind: Bool, AST: ast.NewIdent(\"bool\")},\n\t\t\t\tLen:  3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"array_unknown_const\",\n\t\t\ttyp:  \"[someConst]bool\\nconst someConst = 3\",\n\t\t\twant: ListType{\n\t\t\t\tElem: BuiltinType{Kind: Bool, AST: ast.NewIdent(\"bool\")},\n\t\t\t\tLen:  -1, // unknown\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multi_generic\",\n\t\t\ttyp:  \"foo[int, string]\\n\\ntype foo[T any, U any] struct{A T; B U}\",\n\t\t\twant: namedTypeWithDecl(\n\t\t\t\t(func() *TypeDecl {\n\t\t\t\t\td := new(TypeDecl)\n\t\t\t\t\t*d = TypeDecl{\n\t\t\t\t\t\tName: \"foo\",\n\t\t\t\t\t\tFile: file,\n\t\t\t\t\t\tType: StructType{\n\t\t\t\t\t\t\tFields: []StructField{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName: option.Some(\"A\"),\n\t\t\t\t\t\t\t\t\tType: TypeParamRefType{\n\t\t\t\t\t\t\t\t\t\tAST:   ast.NewIdent(\"A\"),\n\t\t\t\t\t\t\t\t\t\tIndex: 0,\n\t\t\t\t\t\t\t\t\t\tDecl:  d,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tName: option.Some(\"B\"),\n\t\t\t\t\t\t\t\t\tType: TypeParamRefType{\n\t\t\t\t\t\t\t\t\t\tAST:   ast.NewIdent(\"B\"),\n\t\t\t\t\t\t\t\t\t\tIndex: 1,\n\t\t\t\t\t\t\t\t\t\tDecl:  d,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTypeParams: []DeclTypeParam{\n\t\t\t\t\t\t\t{Name: \"T\"},\n\t\t\t\t\t\t\t{Name: \"U\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\treturn d\n\t\t\t\t})(),\n\t\t\t\tBuiltinType{Kind: Int, AST: ast.NewIdent(\"int\")},\n\t\t\t\tBuiltinType{Kind: String, AST: ast.NewIdent(\"string\")},\n\t\t\t),\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timports := \"\"\n\t\tif len(test.imports) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range test.imports {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\nvar x ` + test.typ + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tp := NewParser(tc.Context, l)\n\n\t\t\tif len(test.wantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.wantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tf := pkg.Files[0]\n\t\t\ttypeExpr := pkg.Names().PkgDecls[\"x\"].Spec.(*ast.ValueSpec).Type\n\t\t\tgot := p.ParseType(f, typeExpr)\n\n\t\t\tif len(test.wantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\tvar options []cmp.Option\n\t\t\t\toptions = []cmp.Option{\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Node }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&pkginfo.File{}),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(StructField{}, structtag.Tags{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\t\t\t\t\tcmp.Comparer(func(a, b NamedType) bool {\n\t\t\t\t\t\treturn cmp.Equal(a.Decl(), b.Decl(), options...) && cmp.Equal(a.TypeArgs, b.TypeArgs, options...)\n\t\t\t\t\t}),\n\t\t\t\t}\n\n\t\t\t\tc.Assert(got.String(), qt.CmpEquals(options...), test.want.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParser_ParseFuncDecl(t *testing.T) {\n\ttype testCase struct {\n\t\tname     string\n\t\timports  []string\n\t\tdecl     string\n\t\twant     *FuncDecl\n\t\twantErrs []string\n\t}\n\tfile := fileForPkg(\"foo\", \"example.com\")\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"simple\",\n\t\t\tdecl: \"func x() {}\",\n\t\t\twant: &FuncDecl{\n\t\t\t\tName: \"x\",\n\t\t\t\tFile: fileForPkg(\"foo\", \"example.com\"),\n\t\t\t\tRecv: option.None[*Receiver](),\n\t\t\t\tType: FuncType{\n\t\t\t\t\tParams:  nil,\n\t\t\t\t\tResults: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"recv\",\n\t\t\tdecl: \"type Foo[A, B any] struct{}\\nfunc (f *Foo[A, B]) x() {}\",\n\t\t\twant: &FuncDecl{\n\t\t\t\tName: \"x\",\n\t\t\t\tFile: file,\n\t\t\t\tRecv: option.Some((func() *Receiver {\n\t\t\t\t\tFooDecl := &TypeDecl{\n\t\t\t\t\t\tFile:       file,\n\t\t\t\t\t\tName:       \"Foo\",\n\t\t\t\t\t\tType:       StructType{},\n\t\t\t\t\t\tTypeParams: []DeclTypeParam{{Name: \"A\"}, {Name: \"B\"}},\n\t\t\t\t\t}\n\n\t\t\t\t\treturn &Receiver{\n\t\t\t\t\t\tName: option.Some(\"f\"),\n\t\t\t\t\t\tDecl: FooDecl,\n\t\t\t\t\t\tType: PointerType{\n\t\t\t\t\t\t\tElem: namedTypeWithDecl(FooDecl,\n\t\t\t\t\t\t\t\tTypeParamRefType{\n\t\t\t\t\t\t\t\t\tDecl:  FooDecl,\n\t\t\t\t\t\t\t\t\tIndex: 0,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tTypeParamRefType{\n\t\t\t\t\t\t\t\t\tDecl:  FooDecl,\n\t\t\t\t\t\t\t\t\tIndex: 1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t})()),\n\t\t\t\tType: FuncType{\n\t\t\t\t\tParams:  nil,\n\t\t\t\t\tResults: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timports := \"\"\n\t\tif len(test.imports) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range test.imports {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.decl + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tp := NewParser(tc.Context, l)\n\n\t\t\tif len(test.wantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.wantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\n\t\t\t// Find the first func decl.\n\t\t\tvar fd *ast.FuncDecl\n\t\t\tf := pkg.Files[0]\n\t\t\tfor _, decl := range f.AST().Decls {\n\t\t\t\tif f, ok := decl.(*ast.FuncDecl); ok {\n\t\t\t\t\tfd = f\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tc.Assert(fd, qt.IsNotNil)\n\n\t\t\tgot, ok := p.ParseFuncDecl(f, fd)\n\t\t\tc.Assert(ok, qt.IsTrue)\n\n\t\t\tif len(test.wantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\tvar options []cmp.Option\n\t\t\t\toptions = []cmp.Option{\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Node }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&pkginfo.File{}),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(StructField{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\t\t\t\t\tcmp.Comparer(func(a, b NamedType) bool {\n\t\t\t\t\t\treturn cmp.Equal(a.Decl(), b.Decl(), options...) && cmp.Equal(a.TypeArgs, b.TypeArgs, options...)\n\t\t\t\t\t}),\n\t\t\t\t}\n\n\t\t\t\tc.Assert(got.String(), qt.CmpEquals(options...), test.want.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc namedTypeWithDecl(decl *TypeDecl, typeArgs ...Type) NamedType {\n\tlazy := &lazyDecl{}\n\tlazy.once.Do(func() {}) // mark the lazy.once as used\n\tlazy.decl = decl\n\n\treturn NamedType{\n\t\tAST:      nil,\n\t\tTypeArgs: typeArgs,\n\t\t// Approximate the PkgDeclInfo\n\t\tDeclInfo: &pkginfo.PkgDeclInfo{\n\t\t\tName: decl.Name,\n\t\t\tFile: decl.File,\n\t\t\tPos:  token.NoPos,\n\t\t\tType: token.TYPE,\n\t\t\tSpec: decl.AST,\n\t\t},\n\t\tdecl: lazy,\n\t}\n}\n\nfunc fileForPkg(pkgName string, pkgPath paths.Pkg) *pkginfo.File {\n\treturn &pkginfo.File{Pkg: &pkginfo.Package{\n\t\tName:       pkgName,\n\t\tImportPath: pkgPath,\n\t}}\n}\n"
  },
  {
    "path": "v2/internals/schema/schematest/schematest.go",
    "content": "// Package schematest provides utilities for writing tests\n// that make assertions about schema types and declarations.\npackage schematest\n\nimport (\n\t\"go/token\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n)\n\nfunc Ptr(elem schema.Type) schema.Type {\n\treturn schema.PointerType{Elem: elem}\n}\n\nfunc Option(elem schema.Type) schema.Type {\n\treturn schema.OptionType{Value: elem}\n}\n\nfunc Builtin(kind schema.BuiltinKind) schema.BuiltinType {\n\treturn schema.BuiltinType{Kind: kind}\n}\n\nfunc Named(info *pkginfo.PkgDeclInfo) schema.Type {\n\treturn schema.NamedType{DeclInfo: info}\n}\n\nfunc Slice(elem schema.Type) schema.Type {\n\treturn schema.ListType{Elem: elem}\n}\n\nfunc TypeInfo(name string) *pkginfo.PkgDeclInfo {\n\treturn &pkginfo.PkgDeclInfo{\n\t\tName: name,\n\t\tType: token.TYPE,\n\t}\n}\n\nfunc Param(typ schema.Type) schema.Param {\n\treturn schema.Param{Type: typ}\n}\n\nfunc String() schema.BuiltinType {\n\treturn Builtin(schema.String)\n}\n\nfunc Bool() schema.BuiltinType {\n\treturn Builtin(schema.Bool)\n}\n\nfunc Int() schema.BuiltinType {\n\treturn Builtin(schema.Int)\n}\n\nfunc Error() schema.BuiltinType {\n\treturn Builtin(schema.Error)\n}\n"
  },
  {
    "path": "v2/internals/schema/schemautil/astutil.go",
    "content": "package schemautil\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/option\"\n)\n\n// GetArgument gets the n'th argument from the field list.\n// It reports the name of the n'th argument if it has one.\nfunc GetArgument(fields *ast.FieldList, n int) (f *ast.Field, name option.Option[string]) {\n\tidx := 0\n\tfor _, f := range fields.List {\n\t\tnum := len(f.Names)\n\t\tif num == 0 {\n\t\t\tnum = 1\n\t\t}\n\t\tfor i := 0; i < num; i++ {\n\t\t\tif idx == n {\n\t\t\t\tvar name option.Option[string]\n\t\t\t\tif hasName := i < len(f.Names); hasName {\n\t\t\t\t\tname = option.Some(f.Names[i].Name)\n\t\t\t\t}\n\t\t\t\treturn f, name\n\t\t\t}\n\t\t\tidx++\n\t\t}\n\t}\n\treturn nil, option.None[string]()\n}\n"
  },
  {
    "path": "v2/internals/schema/schemautil/astutil_test.go",
    "content": "package schemautil\n\nimport (\n\t\"go/ast\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"encr.dev/pkg/option\"\n)\n\nfunc TestGetArgument(t *testing.T) {\n\tnoName := &ast.Field{\n\t\tNames: nil,\n\t\tType:  ast.NewIdent(\"noName\"),\n\t}\n\toneName := &ast.Field{\n\t\tNames: []*ast.Ident{ast.NewIdent(\"a\")},\n\t\tType:  ast.NewIdent(\"oneName\"),\n\t}\n\ttwoNames := &ast.Field{\n\t\tNames: []*ast.Ident{ast.NewIdent(\"b\"), ast.NewIdent(\"c\")},\n\t\tType:  ast.NewIdent(\"twoNames\"),\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\tfields   []*ast.Field\n\t\tn        int\n\t\twantF    *ast.Field\n\t\twantName option.Option[string]\n\t}{\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tfields:   []*ast.Field{},\n\t\t\tn:        0,\n\t\t\twantF:    nil,\n\t\t\twantName: option.None[string](),\n\t\t},\n\t\t{\n\t\t\tname:     \"noName\",\n\t\t\tfields:   []*ast.Field{noName},\n\t\t\tn:        0,\n\t\t\twantF:    noName,\n\t\t\twantName: option.None[string](),\n\t\t},\n\t\t{\n\t\t\tname:     \"noName_overflow\",\n\t\t\tfields:   []*ast.Field{noName},\n\t\t\tn:        1,\n\t\t\twantF:    nil,\n\t\t\twantName: option.None[string](),\n\t\t},\n\t\t{\n\t\t\tname:     \"multi_noName\",\n\t\t\tfields:   []*ast.Field{noName, noName},\n\t\t\tn:        1,\n\t\t\twantF:    noName,\n\t\t\twantName: option.None[string](),\n\t\t},\n\t\t{\n\t\t\tname:     \"first\",\n\t\t\tfields:   []*ast.Field{oneName, twoNames},\n\t\t\tn:        0,\n\t\t\twantF:    oneName,\n\t\t\twantName: option.Some(\"a\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"second\",\n\t\t\tfields:   []*ast.Field{oneName, twoNames},\n\t\t\tn:        1,\n\t\t\twantF:    twoNames,\n\t\t\twantName: option.Some(\"b\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"third\",\n\t\t\tfields:   []*ast.Field{oneName, twoNames},\n\t\t\tn:        2,\n\t\t\twantF:    twoNames,\n\t\t\twantName: option.Some(\"c\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"fourth\",\n\t\t\tfields:   []*ast.Field{oneName, twoNames, noName},\n\t\t\tn:        3,\n\t\t\twantF:    noName,\n\t\t\twantName: option.None[string](),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotF, gotName := GetArgument(&ast.FieldList{List: tt.fields}, tt.n)\n\t\t\tif !reflect.DeepEqual(gotF, tt.wantF) {\n\t\t\t\tt.Errorf(\"GetArgument() gotF = %v, want %v\", gotF, tt.wantF)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(gotName, tt.wantName) {\n\t\t\t\tt.Errorf(\"GetArgument() gotName = %v, want %v\", gotName, tt.wantName)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/internals/schema/schemautil/errors.go",
    "content": "package schemautil\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"schema\",\n\t\t\"For more information, see https://encore.dev/docs/develop/api-schemas\",\n\n\t\terrors.WithRangeSize(10),\n\t)\n\n\terrMissingTypeArg = errRange.New(\n\t\t\"Missing type argument\",\n\t\t\"Missing type argument\",\n\t)\n)\n"
  },
  {
    "path": "v2/internals/schema/schemautil/schemautil.go",
    "content": "package schemautil\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/types\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n)\n\n// IsNamed reports whether a given type is a named type with the given\n// package path and name.\nfunc IsNamed(t schema.Type, pkg paths.Pkg, name string) bool {\n\tif named, ok := t.(schema.NamedType); ok {\n\t\tdecl := named.DeclInfo\n\t\treturn decl.Name == name && decl.File.Pkg.ImportPath == pkg\n\t}\n\treturn false\n}\n\n// IsPointer reports whether t is a pointer type.\nfunc IsPointer(t schema.Type) bool {\n\t_, ok := t.(schema.PointerType)\n\treturn ok\n}\n\n// IsOption reports whether t is an option type.\nfunc IsOption(t schema.Type) bool {\n\t_, ok := t.(schema.OptionType)\n\treturn ok\n}\n\n// IsBuiltinKind reports whether the given type is a builtin\n// of one of the given kinds.\nfunc IsBuiltinKind(t schema.Type, kinds ...schema.BuiltinKind) bool {\n\tif b, ok := t.(schema.BuiltinType); ok {\n\t\tif slices.Contains(kinds, b.Kind) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IsBuiltinOrList reports whether the given type is a builtin,\n// a list of builtins, or neither.\nfunc IsBuiltinOrList(t schema.Type) (kind schema.BuiltinKind, isList bool, ok bool) {\n\tif b, ok := t.(schema.BuiltinType); ok {\n\t\treturn b.Kind, false, true\n\t}\n\tif list, ok := t.(schema.ListType); ok {\n\t\tif b, ok := list.Elem.(schema.BuiltinType); ok {\n\t\t\treturn b.Kind, true, true\n\t\t}\n\t}\n\treturn schema.Invalid, false, false\n}\n\n// IsValidHeaderType reports whether the given type is valid for use as an HTTP header value.\nfunc IsValidHeaderType(t schema.Type) bool {\n\tif list, ok := t.(schema.ListType); ok {\n\t\treturn isValidHeaderTypeValue(list.Elem)\n\t}\n\treturn isValidHeaderTypeValue(t)\n}\n\nfunc isValidHeaderTypeValue(t schema.Type) bool {\n\tswitch t := t.(type) {\n\tcase schema.BuiltinType:\n\t\treturn true\n\tcase schema.OptionType:\n\t\treturn isValidHeaderTypeValue(t.Value)\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// IsValidQueryType reports whether the given type is valid for use as a query parameter value.\nfunc IsValidQueryType(t schema.Type) bool {\n\tif list, ok := t.(schema.ListType); ok {\n\t\treturn isValidQueryTypeValue(list.Elem)\n\t}\n\treturn isValidQueryTypeValue(t)\n}\n\nfunc isValidQueryTypeValue(t schema.Type) bool {\n\tswitch t := t.(type) {\n\tcase schema.BuiltinType:\n\t\treturn true\n\tcase schema.OptionType:\n\t\treturn isValidHeaderTypeValue(t.Value)\n\tdefault:\n\t\treturn false\n\t}\n}\n\nvar Signed = []schema.BuiltinKind{\n\tschema.Int,\n\tschema.Int8,\n\tschema.Int16,\n\tschema.Int32,\n\tschema.Int64,\n}\n\nvar Unsigned = []schema.BuiltinKind{\n\tschema.Uint,\n\tschema.Uint8,\n\tschema.Uint16,\n\tschema.Uint32,\n\tschema.Uint64,\n}\n\n// Integers is a list of all integer builtin kinds.\nvar Integers = append(append([]schema.BuiltinKind{}, Signed...), Unsigned...)\n\n// Deref dereferences a type until it is not a pointer type.\n// It returns the number of pointer dereferences required.\nfunc Deref(t schema.Type) (schema.Type, int) {\n\tn := 0\n\tfor {\n\t\tif ptr, ok := t.(schema.PointerType); ok {\n\t\t\tt = ptr.Elem\n\t\t\tn++\n\t\t\tcontinue\n\t\t}\n\t\treturn t, n\n\t}\n}\n\n// ResolveNamedStruct reports whether a given type is a named type\n// pointing to a struct type.\n//\n// It always requires at most one pointer dereference, and if\n// requirePointer is true it must be exactly one pointer dereference.\n//\n// If it doesn't match the requirements it returns (nil, false).\nfunc ResolveNamedStruct(t schema.Type, requirePointer bool) (ref *schema.TypeDeclRef, ok bool) {\n\tt, derefs := Deref(t)\n\tif derefs > 1 || (requirePointer && derefs == 0) {\n\t\treturn nil, false\n\t}\n\n\tif named, ok := t.(schema.NamedType); ok {\n\t\tif decl := named.Decl(); decl.Type.Family() == schema.Struct {\n\t\t\treturn &schema.TypeDeclRef{\n\t\t\t\tDecl:     decl,\n\t\t\t\tPointers: derefs,\n\t\t\t\tTypeArgs: named.TypeArgs,\n\t\t\t}, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// DerefNamedInfo returns what package declaration a given named type references, if any.\n//\n// It always requires at most one pointer dereference, and if\n// requirePointer is true it must be exactly one pointer dereference.\n//\n// If the type is not a named type or otherwise doesn't match the requirements, it returns (nil, false).\nfunc DerefNamedInfo(t schema.Type, requirePointer bool) (info *pkginfo.PkgDeclInfo, ok bool) {\n\tt, derefs := Deref(t)\n\tif derefs > 1 || (requirePointer && derefs == 0) {\n\t\treturn nil, false\n\t}\n\tif named, ok := t.(schema.NamedType); ok {\n\t\treturn named.DeclInfo, true\n\t}\n\treturn nil, false\n}\n\n// ConcretizeGenericType takes a type and applies any type arguments\n// into the slots of the type parameters, producing a concrete type.\n//\n// To be more robust in the presence of typing errors it supports partial application,\n// where the number of type arguments may be different than the number of type parameters on the decl.\nfunc ConcretizeGenericType(errs *perr.List, typ schema.Type) schema.Type {\n\treturn concretize(errs, typ.ASTExpr(), typ, nil, nil)\n}\n\n// ConcretizeWithTypeArgs is like ConcretizeGenericType but operates with\n// a list of type arguments. It is used when the type arguments are known\n// separately from the type itself, such as when using *schema.TypeDeclRef.\nfunc ConcretizeWithTypeArgs(errs *perr.List, typ schema.Type, typeArgs []schema.Type) schema.Type {\n\treturn concretize(errs, typ.ASTExpr(), typ, typeArgs, nil)\n}\n\nfunc concretize(errs *perr.List, referencedFrom ast.Node, typ schema.Type, typeArgs []schema.Type, seenDecls map[TypeHash]*schema.TypeDecl) schema.Type {\n\t// seenDecls is used to avoid infinite recursion\n\t// for mutually recursive types.\n\tif seenDecls == nil {\n\t\tseenDecls = make(map[TypeHash]*schema.TypeDecl)\n\t}\n\n\tswitch typ := typ.(type) {\n\tcase schema.TypeParamRefType:\n\t\t// We have a reference to a type parameter.\n\t\t// Is the corresponding type argument in scope? If so replace it.\n\t\tif typ.Index < len(typeArgs) {\n\t\t\treturn typeArgs[typ.Index]\n\t\t} else {\n\t\t\terrs.Add(\n\t\t\t\terrMissingTypeArg.\n\t\t\t\t\tAtGoNode(typ.AST),\n\t\t\t)\n\t\t\treturn typ // return the type param ref as a placeholder\n\t\t}\n\n\tcase schema.BuiltinType:\n\t\treturn typ\n\tcase schema.PointerType:\n\t\treturn schema.PointerType{AST: typ.AST, Elem: concretize(errs, typ.AST, typ.Elem, typeArgs, seenDecls)}\n\tcase schema.OptionType:\n\t\treturn schema.OptionType{AST: typ.AST, Value: concretize(errs, typ.AST, typ.Value, typeArgs, seenDecls)}\n\tcase schema.ListType:\n\t\treturn schema.ListType{AST: typ.AST, Elem: concretize(errs, typ.AST.Elt, typ.Elem, typeArgs, seenDecls), Len: typ.Len}\n\tcase schema.MapType:\n\t\treturn schema.MapType{\n\t\t\tAST:   typ.AST,\n\t\t\tKey:   concretize(errs, typ.AST.Key, typ.Key, typeArgs, seenDecls),\n\t\t\tValue: concretize(errs, typ.AST.Value, typ.Value, typeArgs, seenDecls),\n\t\t}\n\tcase schema.StructType:\n\t\tresult := schema.StructType{\n\t\t\tAST:    typ.AST,\n\t\t\tFields: make([]schema.StructField, len(typ.Fields)),\n\t\t}\n\t\tfor i, f := range typ.Fields {\n\t\t\tresult.Fields[i] = f // copy\n\t\t\tresult.Fields[i].Type = concretize(errs, f.AST.Type, f.Type, typeArgs, seenDecls)\n\t\t}\n\t\treturn result\n\tcase schema.NamedType:\n\t\tif typParams := typ.Decl().TypeParams; len(typParams) > len(typ.TypeArgs) {\n\t\t\tnumMissing := len(typParams) - len(typ.TypeArgs)\n\t\t\tidx := len(typParams) - numMissing\n\t\t\terrs.Add(\n\t\t\t\terrMissingTypeArg.\n\t\t\t\t\tAtGoNode(referencedFrom, errors.AsError(\"missing from here\")).\n\t\t\t\t\tAtGoNode(typParams[idx].AST, errors.AsHelp(\"missing type parameter defined here\")),\n\t\t\t)\n\t\t\treturn typ // return the named type as a placeholder\n\t\t}\n\n\t\t// Clone the named type. Clone the slice so we don't overwrite the original.\n\t\tclone := typ\n\t\tclone.TypeArgs = slices.Clone(typ.TypeArgs)\n\n\t\tfor i, arg := range clone.TypeArgs {\n\t\t\tclone.TypeArgs[i] = concretize(errs, typ.AST, arg, typeArgs, seenDecls)\n\t\t}\n\n\t\t// If we've already seen this declaration, don't clone it to avoid\n\t\t// infinite recursion.\n\t\thash := Hash(typ)\n\t\tif decl, ok := seenDecls[hash]; ok {\n\t\t\treturn clone.WithDecl(decl)\n\t\t}\n\n\t\tdecl := clone.Decl().Clone()\n\n\t\t// Clone and concretize the declaration.\n\t\tseenDecls[hash] = decl\n\t\tdecl.Type = concretize(errs, typ.AST, decl.Type, clone.TypeArgs, seenDecls)\n\t\treturn clone.WithDecl(decl)\n\n\tcase schema.FuncType:\n\t\t// Clone the function type. Clone the slices so we don't overwrite the original.\n\t\tclone := typ // copy\n\t\tclone.Params = slices.Clone(typ.Params)\n\t\tclone.Results = slices.Clone(typ.Results)\n\n\t\tfor i, p := range clone.Params {\n\t\t\tclone.Params[i].Type = concretize(errs, p.AST.Type, p.Type, typeArgs, seenDecls)\n\t\t}\n\t\tfor i, p := range clone.Results {\n\t\t\tclone.Results[i].Type = concretize(errs, p.AST.Type, p.Type, typeArgs, seenDecls)\n\t\t}\n\t\treturn clone\n\tcase schema.InterfaceType:\n\t\t// TODO(andre) we currently don't track any information\n\t\t// about interfaces, so nothing to do right now.\n\t\treturn typ\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown type %T\", typ))\n\t}\n}\n\ntype pkgDeclKey struct {\n\tpkg  paths.Pkg\n\tname string\n}\n\n// Walk performs a depth-first walk of all schema nodes starting at node, calling visitor for each type.\n//\n// If visitor returns false, the walk is aborted.\nfunc Walk(root schema.Type, visitor func(node schema.Type) bool) {\n\tdeclChain := make([]pkgDeclKey, 0, 10)\n\twalk(root, visitor, declChain)\n}\n\nfunc walk(node schema.Type, visitor func(typ schema.Type) bool, declChain []pkgDeclKey) bool {\n\t// Check the visitor against the node type\n\tif !visitor(node) {\n\t\treturn false\n\t}\n\n\tswitch node := node.(type) {\n\tcase schema.NamedType:\n\t\tfor _, typ := range node.TypeArgs {\n\t\t\tif !walk(typ, visitor, declChain) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\t// Have we already visited this decl?\n\t\tdeclKey := pkgDeclKey{pkg: node.DeclInfo.File.Pkg.ImportPath, name: node.DeclInfo.Name}\n\t\tfor i := len(declChain) - 1; i >= 0; i-- {\n\t\t\tif declChain[i] == declKey {\n\t\t\t\treturn true // keep going elsewhere\n\t\t\t}\n\t\t}\n\t\tdeclChain = append(declChain, declKey)\n\n\t\treturn walk(node.Decl().Type, visitor, declChain)\n\tcase schema.StructType:\n\t\tfor _, field := range node.Fields {\n\t\t\tif !walk(field.Type, visitor, declChain) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\tcase schema.MapType:\n\t\tif !walk(node.Key, visitor, declChain) {\n\t\t\treturn false\n\t\t}\n\t\treturn walk(node.Value, visitor, declChain)\n\tcase schema.ListType:\n\t\treturn walk(node.Elem, visitor, declChain)\n\tcase schema.BuiltinType:\n\t\treturn true // keep going elsewhere\n\tcase schema.PointerType:\n\t\treturn walk(node.Elem, visitor, declChain)\n\tcase schema.OptionType:\n\t\treturn walk(node.Value, visitor, declChain)\n\tcase schema.FuncType:\n\t\tfor _, part := range [...][]schema.Param{node.Params, node.Results} {\n\t\t\tfor _, p := range part {\n\t\t\t\tif !walk(p.Type, visitor, declChain) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\tcase schema.InterfaceType:\n\t\treturn true\n\tcase schema.TypeParamRefType:\n\t\treturn true\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unsupported node type encountered during walk: %+v\", reflect.TypeOf(node)))\n\t}\n\n\treturn true\n}\n\ntype TypeHash [32]byte\n\n// Hash produces a hash of the given type.\n// Identical types return identical hashes.\nfunc Hash(typ schema.Type) TypeHash {\n\tvar buf bytes.Buffer\n\thashType(&buf, typ)\n\treturn sha256.Sum256(buf.Bytes())\n}\n\nfunc hashType(buf *bytes.Buffer, t schema.Type) {\n\tswitch t := t.(type) {\n\tcase schema.NamedType:\n\t\tbuf.WriteString(\"named:\")\n\t\tbuf.WriteString(t.DeclInfo.File.Pkg.ImportPath.String())\n\t\tbuf.WriteString(\".\")\n\t\tbuf.WriteString(t.DeclInfo.Name)\n\t\tif len(t.TypeArgs) > 0 {\n\t\t\tbuf.WriteString(\"[\")\n\t\t\tfor i, arg := range t.TypeArgs {\n\t\t\t\tif i > 0 {\n\t\t\t\t\tbuf.WriteString(\", \")\n\t\t\t\t}\n\t\t\t\thashType(buf, arg)\n\t\t\t}\n\t\t\tbuf.WriteString(\"]\")\n\t\t}\n\n\tcase schema.StructType:\n\t\tbuf.WriteString(\"struct{\")\n\t\tfor _, f := range t.Fields {\n\t\t\tif name, ok := f.Name.Get(); ok {\n\t\t\t\tbuf.WriteString(name)\n\t\t\t\thashType(buf, f.Type)\n\t\t\t\tbuf.WriteString(\";\")\n\t\t\t}\n\t\t}\n\t\tbuf.WriteString(\"}\")\n\n\tcase schema.MapType:\n\t\tbuf.WriteString(\"map[\")\n\t\thashType(buf, t.Key)\n\t\tbuf.WriteString(\"]\")\n\t\thashType(buf, t.Value)\n\n\tcase schema.ListType:\n\t\tif t.Len >= 0 {\n\t\t\tfmt.Fprintf(buf, \"[%d]\", t.Len)\n\t\t} else {\n\t\t\tbuf.WriteString(\"[]\")\n\t\t}\n\t\thashType(buf, t.Elem)\n\n\tcase schema.PointerType:\n\t\tbuf.WriteString(\"*\")\n\t\thashType(buf, t.Elem)\n\n\tcase schema.OptionType:\n\t\tbuf.WriteString(\"Option[\")\n\t\thashType(buf, t.Value)\n\t\tbuf.WriteString(\"]\")\n\n\tcase schema.FuncType:\n\t\tbuf.WriteString(\"func(\")\n\t\tfor i, p := range t.Params {\n\t\t\tif i > 0 {\n\t\t\t\tbuf.WriteString(\", \")\n\t\t\t}\n\t\t\thashType(buf, p.Type)\n\t\t}\n\t\tbuf.WriteString(\")\")\n\n\t\tif len(t.Results) > 0 {\n\t\t\tbuf.WriteString(\" (\")\n\t\t\tfor i, p := range t.Results {\n\t\t\t\tif i > 0 {\n\t\t\t\t\tbuf.WriteString(\", \")\n\t\t\t\t}\n\t\t\t\thashType(buf, p.Type)\n\t\t\t}\n\t\t\tbuf.WriteString(\")\")\n\t\t}\n\n\tcase schema.InterfaceType:\n\t\t// We don't track interface methods yet, so outsource\n\t\t// this to go/types for now.\n\t\ttypes.WriteExpr(buf, t.AST)\n\n\tcase schema.BuiltinType:\n\t\tbuf.WriteString(t.String())\n\n\tcase schema.TypeParamRefType:\n\t\tbuf.WriteString(\"typeparamref:\")\n\t\tbuf.WriteString(t.Decl.DeclaredIn().Pkg.ImportPath.String())\n\t\tbuf.WriteString(\".\")\n\t\tif name, ok := t.Decl.PkgName().Get(); ok {\n\t\t\tbuf.WriteString(name)\n\t\t} else {\n\t\t\tbuf.WriteString(\"anon\")\n\t\t}\n\t\tbuf.WriteString(\"#\")\n\t\tbuf.WriteString(strconv.Itoa(t.Index))\n\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"unknown type %T\", t))\n\t}\n}\n\n// UnwrapConfigType unwraps a config.Value[T] or config.Values[T] type to T or []T respectively.\n// If the type is not a config.Value[T] or config.Values[T] type, it returns the type unchanged.\n// If there are any errors encountered they are reported to errs.\nfunc UnwrapConfigType(errs *perr.List, t schema.NamedType) (typ schema.Type, isList, isConfig bool) {\n\tif t.DeclInfo.File.Pkg.ImportPath != \"encore.dev/config\" {\n\t\treturn t, false, false\n\t}\n\n\tif t.DeclInfo.Name == \"Values\" {\n\t\tif len(t.TypeArgs) == 0 {\n\t\t\t// Invalid use of config.Values[T]\n\t\t\terrs.AddPos(t.AST.Pos(), \"invalid use of config.Values[T]: no type arguments provided\")\n\t\t\treturn schema.BuiltinType{Kind: schema.Invalid}, true, true\n\t\t}\n\n\t\treturn t.TypeArgs[0], true, true\n\t} else if t.DeclInfo.Name == \"Value\" {\n\t\tif len(t.TypeArgs) == 0 {\n\t\t\t// Invalid use of config.Value[T]\n\t\t\terrs.AddPos(t.AST.Pos(), \"invalid use of config.Value[T]: no type arguments provided\")\n\t\t\treturn schema.BuiltinType{Kind: schema.Invalid}, false, true\n\t\t}\n\t\treturn t.TypeArgs[0], false, true\n\t} else {\n\t\t// Use of some helper type alias, like config.Bool\n\t\tdecl := t.Decl()\n\t\tif named, ok := decl.Type.(schema.NamedType); ok && named.DeclInfo.Name == \"Value\" {\n\t\t\tif len(named.TypeArgs) == 0 {\n\t\t\t\t// Invalid use of config.Value[T]\n\t\t\t\terrs.AddPos(t.AST.Pos(), \"invalid use of config.Value[T]: no type arguments provided\")\n\t\t\t\treturn schema.BuiltinType{Kind: schema.Invalid}, false, true\n\t\t\t}\n\t\t\treturn named.TypeArgs[0], false, true\n\t\t} else {\n\t\t\t// Invalid use of config.Value[T]\n\t\t\terrs.Addf(t.AST.Pos(), \"unrecognized config type: %s\", t.DeclInfo.Name)\n\t\t\treturn schema.BuiltinType{Kind: schema.Invalid}, false, true\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "v2/internals/schema/types.go",
    "content": "package schema\n\nimport (\n\t\"go/ast\"\n\t\"go/types\"\n\t\"sync\"\n\n\t\"github.com/fatih/structtag\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\ntype TypeFamily int\n\nconst (\n\tUnknown TypeFamily = iota\n\tNamed\n\tStruct\n\tMap\n\tList\n\tBuiltin\n\tPointer\n\tOption\n\tFunc\n\tInterface\n\tTypeParamRef\n)\n\ntype Type interface {\n\tFamily() TypeFamily\n\tASTExpr() ast.Expr\n\tString() string // Resolve to a string representation of the type.\n}\n\ntype NamedType struct {\n\tAST ast.Expr // *ast.Ident or *ast.SelectorExpr\n\n\t// DeclInfo is the declaration info for the declaration\n\t// this refers to.\n\tDeclInfo *pkginfo.PkgDeclInfo\n\n\t// TypeArgs are the type arguments used to instantiate this named type.\n\tTypeArgs []Type\n\n\t// decl lazy-initializes the type declaration.\n\t// It's a pointer since it's stateful via sync.Once.\n\tdecl *lazyDecl\n}\n\n// lazyDecl is a lazily-initialized type decl.\ntype lazyDecl struct {\n\tp    *Parser\n\tinfo *pkginfo.PkgDeclInfo\n\n\tonce sync.Once\n\tdecl *TypeDecl\n}\n\nfunc (t NamedType) Decl() *TypeDecl {\n\treturn t.decl.Decl()\n}\n\nfunc (t NamedType) WithDecl(decl *TypeDecl) NamedType {\n\treturn newEagerNamedType(t.AST, t.TypeArgs, decl)\n}\n\nfunc (d *lazyDecl) Decl() *TypeDecl {\n\td.once.Do(func() {\n\t\td.decl = d.p.ParseTypeDecl(d.info)\n\t})\n\treturn d.decl\n}\n\ntype StructType struct {\n\tAST    *ast.StructType\n\tFields []StructField\n}\n\ntype StructField struct {\n\t// AST is the AST node that this field represents.\n\t// Note that multiple fields may share the same *ast.Field node,\n\t// in cases with multiple names, like \"Foo, Bar int\".\n\tAST *ast.Field\n\n\tName option.Option[string] // field name, or None if anonymous\n\tType Type\n\tDoc  string\n\tTag  structtag.Tags\n}\n\nfunc (f *StructField) IsAnonymous() bool {\n\treturn f.Name.Empty()\n}\n\nfunc (f *StructField) IsExported() bool {\n\treturn f.IsAnonymous() || f.Name.Contains(ast.IsExported)\n}\n\ntype MapType struct {\n\tAST   *ast.MapType\n\tKey   Type\n\tValue Type\n}\n\ntype ListType struct {\n\tAST  *ast.ArrayType\n\tLen  int // -1 for a slice\n\tElem Type\n}\n\ntype PointerType struct {\n\tAST  *ast.StarExpr\n\tElem Type\n}\n\n// OptionType represents an option.Option[T] type.\ntype OptionType struct {\n\tAST   ast.Expr\n\tValue Type\n}\n\ntype BuiltinType struct {\n\tAST  ast.Expr\n\tKind BuiltinKind\n}\n\ntype FuncType struct {\n\tAST     *ast.FuncType\n\tParams  []Param\n\tResults []Param\n}\n\ntype InterfaceType struct {\n\tAST *ast.InterfaceType\n\n\t// EmbeddedIfaces are the interfaces this interface embeds.\n\tEmbeddedIfaces []Type\n\n\t// TODO change these out for more useful information.\n\tTypeLists []ast.Expr\n\tMethods   []*ast.Field\n}\n\n// Param represents a parameter or result field.\ntype Param struct {\n\tAST  *ast.Field\n\tName option.Option[string] // parameter name, or None if a type-only parameter.\n\tType Type\n}\n\n// TypeParamRefType is a reference to a `TypeParameter` within a declaration block\ntype TypeParamRefType struct {\n\tAST *ast.Ident\n\n\tDecl  Decl // the declaration this type parameter is defined on\n\tIndex int  // Index into the type parameter slice on the declaration\n}\n\ntype BuiltinKind int\n\n//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=BuiltinKind -output=types_string.go\n\nconst (\n\tInvalid BuiltinKind = iota\n\tAny\n\tBool\n\n\tInt\n\tInt8\n\tInt16\n\tInt32\n\tInt64\n\tUint\n\tUint8\n\tUint16\n\tUint32\n\tUint64\n\n\tFloat32\n\tFloat64\n\tString\n\tBytes\n\n\t// Additional Encore Types\n\n\tTime\n\tUUID\n\tJSON\n\tUserID\n\tError // builtin \"error\" type, for convenience\n\n\t// unsupported is a special value used\n\t// to indicate the particular builtin is known,\n\t// but is not something Encore supports.\n\tunsupported BuiltinKind = -1\n)\n\nvar _ Type = NamedType{}\nvar _ Type = StructType{}\nvar _ Type = MapType{}\nvar _ Type = ListType{}\nvar _ Type = PointerType{}\nvar _ Type = OptionType{}\nvar _ Type = BuiltinType{}\nvar _ Type = FuncType{}\nvar _ Type = InterfaceType{}\nvar _ Type = TypeParamRefType{}\n\nfunc (NamedType) Family() TypeFamily        { return Named }\nfunc (StructType) Family() TypeFamily       { return Struct }\nfunc (MapType) Family() TypeFamily          { return Map }\nfunc (ListType) Family() TypeFamily         { return List }\nfunc (PointerType) Family() TypeFamily      { return Pointer }\nfunc (OptionType) Family() TypeFamily       { return Option }\nfunc (BuiltinType) Family() TypeFamily      { return Builtin }\nfunc (FuncType) Family() TypeFamily         { return Func }\nfunc (InterfaceType) Family() TypeFamily    { return Interface }\nfunc (TypeParamRefType) Family() TypeFamily { return TypeParamRef }\n\nfunc (t NamedType) ASTExpr() ast.Expr        { return t.AST }\nfunc (t StructType) ASTExpr() ast.Expr       { return t.AST }\nfunc (t MapType) ASTExpr() ast.Expr          { return t.AST }\nfunc (t ListType) ASTExpr() ast.Expr         { return t.AST }\nfunc (t PointerType) ASTExpr() ast.Expr      { return t.AST }\nfunc (t OptionType) ASTExpr() ast.Expr       { return t.AST }\nfunc (t BuiltinType) ASTExpr() ast.Expr      { return t.AST }\nfunc (t FuncType) ASTExpr() ast.Expr         { return t.AST }\nfunc (t InterfaceType) ASTExpr() ast.Expr    { return t.AST }\nfunc (t TypeParamRefType) ASTExpr() ast.Expr { return t.AST }\n\nfunc (t NamedType) String() string {\n\tname := t.DeclInfo.Name\n\tif t.TypeArgs != nil {\n\t\tname += \"[\"\n\t\tfor i, arg := range t.TypeArgs {\n\t\t\tif i > 0 {\n\t\t\t\tname += \", \"\n\t\t\t}\n\t\t\tname += arg.String()\n\t\t}\n\t\tname += \"]\"\n\t}\n\treturn name\n}\nfunc (t StructType) String() string       { return \"struct\" }\nfunc (t MapType) String() string          { return \"map[\" + t.Key.String() + \"]\" + t.Value.String() }\nfunc (t ListType) String() string         { return \"[]\" + t.Elem.String() }\nfunc (t PointerType) String() string      { return \"*\" + t.Elem.String() }\nfunc (t OptionType) String() string       { return \"Option[\" + t.Value.String() + \"]\" }\nfunc (t BuiltinType) String() string      { return types.ExprString(t.AST) }\nfunc (t FuncType) String() string         { return \"function\" }\nfunc (t InterfaceType) String() string    { return \"interface\" }\nfunc (t TypeParamRefType) String() string { return t.AST.Name }\n\n// TypeDeclRef is a reference to a type declaration, through zero or more pointers\n// and possibly with type arguments.\ntype TypeDeclRef struct {\n\tDecl     *TypeDecl\n\tTypeArgs []Type\n\tPointers int\n}\n\nfunc (r *TypeDeclRef) ToType() Type {\n\tvar typ Type = newEagerNamedType(r.Decl.Type.ASTExpr(), r.TypeArgs, r.Decl)\n\tfor i := 0; i < r.Pointers; i++ {\n\t\ttyp = PointerType{Elem: typ}\n\t}\n\treturn typ\n}\n"
  },
  {
    "path": "v2/internals/schema/types_string.go",
    "content": "// Code generated by \"stringer -type=BuiltinKind -output=types_string.go\"; DO NOT EDIT.\n\npackage schema\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[Invalid-0]\n\t_ = x[Any-1]\n\t_ = x[Bool-2]\n\t_ = x[Int-3]\n\t_ = x[Int8-4]\n\t_ = x[Int16-5]\n\t_ = x[Int32-6]\n\t_ = x[Int64-7]\n\t_ = x[Uint-8]\n\t_ = x[Uint8-9]\n\t_ = x[Uint16-10]\n\t_ = x[Uint32-11]\n\t_ = x[Uint64-12]\n\t_ = x[Float32-13]\n\t_ = x[Float64-14]\n\t_ = x[String-15]\n\t_ = x[Bytes-16]\n\t_ = x[Time-17]\n\t_ = x[UUID-18]\n\t_ = x[JSON-19]\n\t_ = x[UserID-20]\n\t_ = x[Error-21]\n\t_ = x[unsupported - -1]\n}\n\nconst _BuiltinKind_name = \"unsupportedInvalidAnyBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64Float32Float64StringBytesTimeUUIDJSONUserIDError\"\n\nvar _BuiltinKind_index = [...]uint8{0, 11, 18, 21, 25, 28, 32, 37, 42, 47, 51, 56, 62, 68, 74, 81, 88, 94, 99, 103, 107, 111, 117, 122}\n\nfunc (i BuiltinKind) String() string {\n\tidx := int(i) - -1\n\tif i < -1 || idx >= len(_BuiltinKind_index)-1 {\n\t\treturn \"BuiltinKind(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _BuiltinKind_name[_BuiltinKind_index[idx]:_BuiltinKind_index[idx+1]]\n}\n"
  },
  {
    "path": "v2/internals/testutil/testutil.go",
    "content": "package testutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/build\"\n\t\"go/token\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sync\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/pkg/errinsrc\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\ntype Context struct {\n\t*parsectx.Context\n\tTestC *qt.C\n}\n\n// NewContext constructs a new Context for testing.\n// It defaults the build info to the host system.\nfunc NewContext(c *qt.C, parseTests bool, archive *txtar.Archive) *Context {\n\tmainModuleDir := WriteTxtar(c, archive)\n\n\treturn newContextForFSPath(c, mainModuleDir, parseTests)\n}\n\n// NewContextForTestScript constructs a new Context for testing when using testscript\n//\n// Your testscript test should call this [TestScriptSetupFunc] in the TestScript.Setup function.\nfunc NewContextForTestScript(ts *testscript.TestScript, parseTests bool) *Context {\n\tc := GetTestC(ts)\n\tworkdir := ts.Value(\"wd\").(string)\n\n\t// Write the go.mod file for the testscript as we don't expect each test to do this\n\tadditional := ParseTxtar(`-- go.mod --\nmodule test\n\ngo 1.20\n\nrequire encore.dev v1.52.0\n\n-- fakesvcfortest/test.go --\n// this service only exists to suppress the \"no services found error\"\npackage fakesvcfortest\n\nimport \"context\"\n\n//encore:api public\nfunc TestFunc(ctx context.Context) error { return nil }`)\n\tts.Check(txtar.Write(additional, workdir))\n\n\treturn newContextForFSPath(c, workdir, parseTests)\n}\n\n// TestScriptSetupFunc is a testscript setup function which sets up the testscript environment for\n// testing with testutil.\nfunc TestScriptSetupFunc(env *testscript.Env) error {\n\tenv.Values[\"TESTUTIL_SCRIPT_SETUP\"] = true\n\tenv.Values[\"wd\"] = env.WorkDir\n\n\ttb, ok := env.T().(testing.TB)\n\tif !ok {\n\t\tenv.T().Fatal(\"testscript's T did not implement testing.TB as expected\")\n\t}\n\tenv.Values[\"c\"] = qt.New(tb)\n\n\treturn nil\n}\n\n// GetTestC returns the *qt.C for the current testscript test.\n//\n// This should only be called from within a testscript test which has had [TestScriptSetupFunc] called\n// during the TestScript.Setup function.\nfunc GetTestC(ts *testscript.TestScript) *qt.C {\n\tif value := ts.Value(\"TESTUTIL_SCRIPT_SETUP\"); value != nil {\n\t\tif b, ok := value.(bool); !ok || !b {\n\t\t\tts.Fatalf(\"testutil.TestScriptSetupFunc was not called in the TestScript.Setup function\")\n\t\t}\n\t}\n\n\treturn ts.Value(\"c\").(*qt.C)\n}\n\nfunc newContextForFSPath(c *qt.C, mainModuleDir string, parseTests bool) *Context {\n\terrinsrc.ColoursInErrors(false) // disable colours in errors for tests\n\terrinsrc.IncludeStackByDefault = true\n\n\tctx, cancel := context.WithCancelCause(context.Background())\n\tc.Cleanup(func() {\n\t\tcancel(fmt.Errorf(\"test %s aborted\", c.Name()))\n\t})\n\n\td := &build.Default\n\tinfo := parsectx.BuildInfo{\n\t\tExperiments:   nil,\n\t\tGOARCH:        d.GOARCH,\n\t\tGOOS:          d.GOOS,\n\t\tGOROOT:        paths.RootedFSPath(env.EncoreGoRoot(), \".\"),\n\t\tBuildTags:     nil,\n\t\tCgoEnabled:    true,\n\t\tEncoreRuntime: paths.RootedFSPath(filepath.Join(RuntimeDir, \"go\"), \".\"),\n\t}\n\n\tfset := token.NewFileSet()\n\tparseCtx := &parsectx.Context{\n\t\tLog:           zerolog.New(zerolog.NewConsoleWriter(zerolog.ConsoleTestWriter(c))).Level(zerolog.InfoLevel),\n\t\tMainModuleDir: paths.RootedFSPath(mainModuleDir, mainModuleDir),\n\t\tCtx:           ctx,\n\t\tBuild:         info,\n\t\tFS:            fset,\n\t\tParseTests:    parseTests, // might as well\n\t\tErrs:          perr.NewList(ctx, fset),\n\t}\n\n\treturn &Context{Context: parseCtx, TestC: c}\n}\n\n// FailTestOnErrors is function that fails the test if an errors\n// are encountered when running fn.\nfunc (c *Context) FailTestOnErrors() {\n\tif c.TestC == nil {\n\t\tpanic(\"parsectx.Context created outside of NewContext\")\n\t}\n\n\tn := c.Errs.Len()\n\tc.TestC.Cleanup(func() {\n\t\tif c.Errs.Len() > n {\n\t\t\tc.TestC.Fatalf(\"parse errors: %s\", c.Errs.FormatErrors())\n\t\t}\n\t})\n}\n\n// FailTestOnBailout is a defer function that fails the test if a bailout\n// was triggered. It must be called as \"defer c.FailTestOnBailout()\".\nfunc (c *Context) FailTestOnBailout() {\n\tc.TestC.Helper()\n\tif l, caught := perr.CatchBailout(recover()); caught {\n\t\tc.TestC.Fatalf(\"bailout: %s\", l.FormatErrors())\n\t}\n}\n\n// DeferExpectError is a defer function that expects errors to be present.\n// Each argument is matched with the corresponding error.\n// If the number of errors differs from the number of matches the test fails.\nfunc (c *Context) DeferExpectError(matches ...string) {\n\t// Ignore any bailout; we'll check the list directly.\n\tperr.CatchBailout(recover())\n\n\tl := c.Errs\n\tn := l.Len()\n\tif len(matches) != n {\n\t\tc.TestC.Fatalf(\"expected %d errors, got %d: %s\", len(matches), n, l.FormatErrors())\n\t}\n\n\tfor i := range n {\n\t\terr := l.At(i)\n\t\tre := regexp.MustCompile(matches[i])\n\t\terrMsg := err.Error()\n\t\tc.TestC.Check(re.MatchString(errMsg), qt.IsTrue, qt.Commentf(\"err %v does not match regexp %q\",\n\t\t\terrMsg, matches[i]))\n\t}\n}\n\n// GoModTidy runs \"go mod tidy\" on the main module.\nfunc (c *Context) GoModTidy() {\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.Command(c.goBin(), \"mod\", \"tidy\")\n\tcmd.Dir = c.MainModuleDir.ToIO()\n\tc.TestC.Log(\"running 'go mod tidy'\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tc.TestC.Fatalf(\"go mod tidy: %v\\n%s\", err, out)\n\t}\n\tc.TestC.Log(\"'go mod tidy' completed successfully\")\n}\n\n// GoModDownload runs \"go mod download all\" on the main module.\nfunc (c *Context) GoModDownload() {\n\t// The \"all\" arg is needed to force 'go mod download' to update\n\t// the go.sum file. See https://go-review.googlesource.com/c/go/+/318629.\n\n\t// nosemgrep go.lang.security.audit.dangerous-exec-command.dangerous-exec-command\n\tcmd := exec.Command(c.goBin(), \"mod\", \"download\", \"all\")\n\tcmd.Dir = c.MainModuleDir.ToIO()\n\tc.TestC.Log(\"running 'go mod download'\")\n\t// nosemgrep\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tc.TestC.Fatalf(\"go mod download: %v\\n%s\", err, out)\n\t}\n\tc.TestC.Log(\"'go mod download' completed successfully\")\n}\n\nfunc (c *Context) goBin() string {\n\ts := c.Build.GOROOT.Join(\"bin\", \"go\").ToIO()\n\tif runtime.GOOS == \"windows\" {\n\t\ts += \".exe\"\n\t}\n\treturn s\n}\n\nfunc ParseTxtar(s string) *txtar.Archive {\n\treturn txtar.Parse([]byte(s))\n}\n\n// WriteTxtar writes the given txtar archive to a temporary directory\n// and returns the directory path.\nfunc WriteTxtar(c *qt.C, a *txtar.Archive) (dir string) {\n\tc.Helper()\n\tdir = c.TempDir()\n\n\t// NOTE(andre): There appears to be a bug in go's handling of overlays\n\t// when the source or destination is a symlink.\n\t// I haven't dug into the root cause exactly, but it causes weird issues\n\t// with tests since macOS's /var/tmp is a symlink to /private/var/tmp.\n\tif d, err := filepath.EvalSymlinks(dir); err == nil {\n\t\tdir = d\n\t}\n\n\terr := txtar.Write(a, dir)\n\tc.Assert(err, qt.IsNil)\n\treturn dir\n}\n\n// FindNodes finds all nodes of the type T in the given AST.\nfunc FindNodes[T ast.Node](root ast.Node) []T {\n\tvar results []T\n\tast.Inspect(root, func(n ast.Node) bool {\n\t\tif t, ok := n.(T); ok {\n\t\t\tresults = append(results, t)\n\t\t}\n\t\treturn true\n\t})\n\treturn results\n}\n\n// A PackageList is a list of packages that knows how to\n// collect itself, by passing (*PackageList).Collector() to\n// the scan.ProcessModule function. It's primarily a helper function\n// for testing purposes.\ntype PackageList []*pkginfo.Package\n\nfunc (l *PackageList) Collector() func(pkg *pkginfo.Package) {\n\tvar mu sync.Mutex\n\treturn func(pkg *pkginfo.Package) {\n\t\tmu.Lock()\n\t\t*l = append(*l, pkg)\n\t\tmu.Unlock()\n\t}\n}\n\n// ResourceDeepEquals is a quicktest comparator for resource.Resource and resource.Bind\n// that forces the comparison to include unexported fields\nvar ResourceDeepEquals = qt.CmpEquals(\n\tcmp.AllowUnexported(option.Option[*pkginfo.File]{}),\n)\n"
  },
  {
    "path": "v2/internals/testutil/update_archive_file.go",
    "content": "package testutil\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/rogpeppe/go-internal/renameio\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n)\n\nfunc UpdateArchiveFile(ts *testscript.TestScript, sourceDir string, filename string, content string) {\n\twd := ts.Getenv(\"WORK\")\n\n\t// Work out the archive name from the work directory\n\t// testscript always creates a directory with the name `script-<archive name>`\n\tbaseName := filepath.Base(wd)\n\tif !strings.HasPrefix(baseName, \"script-\") {\n\t\tts.Fatalf(\"unable to identify archvie name from work directory `%s`\", baseName)\n\t}\n\tarchiveName := strings.TrimPrefix(baseName, \"script-\")\n\n\t// Check the possible extensions\n\tfoundExt := false\n\tpossibleExts := []string{\".txt\", \".txtar\"}\n\tfor _, ext := range possibleExts {\n\t\tarchivePath := filepath.Join(sourceDir, archiveName+ext)\n\t\tif _, err := os.Stat(archivePath); err == nil {\n\t\t\tarchiveName = archiveName + ext\n\t\t\tfoundExt = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !foundExt {\n\t\tts.Fatalf(\"unable to identify archive file from work directory `%s/%s`\", sourceDir, archiveName)\n\t}\n\n\tts.Logf(\"Updating archive file `%s` with expected file `%s`...\", archiveName, filename)\n\n\t// Read the archive\n\tarchivePath := filepath.Join(sourceDir, archiveName)\n\tar, err := txtar.ParseFile(archivePath)\n\tif err != nil {\n\t\tts.Fatalf(\"unable to read archive file `%s`: %v\", archiveName, err)\n\t}\n\n\t// Update the archive\n\tfileFound := false\n\tfor i, f := range ar.Files {\n\t\tif f.Name == filename {\n\t\t\tar.Files[i].Data = []byte(content)\n\t\t\tfileFound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !fileFound {\n\t\tar.Files = append(ar.Files, txtar.File{Name: filename, Data: []byte(content)})\n\t}\n\n\t// Write the archive\n\terr = renameio.WriteFile(archivePath, txtar.Format(ar))\n\tif err != nil {\n\t\tts.Fatalf(\"unable to write archive file `%s`: %v\", archiveName, err)\n\t}\n}\n"
  },
  {
    "path": "v2/internals/testutil/utils.go",
    "content": "package testutil\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"encr.dev/internal/env\"\n)\n\nvar (\n\tEncoreRepoDir = repoDir()\n\tRuntimeDir    = filepath.Join(EncoreRepoDir, \"runtimes\")\n)\n\n// EnvRepoDirOverride is the name of the environment variable to override\n// the resolution of the path to this repository. It exists because\n// we have some tests that run in a temporary directory (such as when using\n// testscript in process execution mode, like compiler/build/build_test.go),\n// in which case the normal approach of invoking `git rev-parse --show-toplevel`\n// doesn't work.\nconst EnvRepoDirOverride = \"ENCORE_REPO_DIR\"\n\nfunc init() {\n\t// If we're not in the Encore repo, use the runtime path from the environment.\n\tif _, err := os.Stat(filepath.Join(EncoreRepoDir, \"go.mod\")); err != nil {\n\t\tRuntimeDir = env.EncoreRuntimesPath()\n\t}\n}\n\nfunc repoDir() string {\n\tif dir := os.Getenv(EnvRepoDirOverride); dir != \"\" {\n\t\treturn dir\n\t}\n\n\tcmd := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\")\n\t// nosemgrep\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"unable to get repo directory: %s\", out))\n\t}\n\treturn string(bytes.TrimSpace(out))\n}\n"
  },
  {
    "path": "v2/parser/apis/api/api.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/api/apienc\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/apis/selector\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\ntype AccessType string\n\nconst (\n\tPublic  AccessType = \"public\"\n\tPrivate AccessType = \"private\"\n\t// Auth is like public but requires authentication.\n\tAuth AccessType = \"auth\"\n)\n\ntype Endpoint struct {\n\terrs *perr.List\n\n\tName             string\n\tDoc              string\n\tFile             *pkginfo.File\n\tDecl             *schema.FuncDecl\n\tAccess           AccessType\n\tAccessField      option.Option[directive.Field]\n\tRaw              bool\n\tPath             *resourcepaths.Path\n\tHTTPMethods      []string\n\tHTTPMethodsField option.Option[directive.Field]\n\tRequest          schema.Type // request data; nil for Raw Endpoints\n\tResponse         schema.Type // response data; nil for Raw Endpoints\n\tTags             selector.Set\n\tRecv             option.Option[*schema.Receiver] // None if not a method\n\n\t// Sensitive indicates whether the endpoint has been tagged as sensitive,\n\t// meaning all request/response information will be redacted in traces.\n\tSensitive bool\n\n\treqEncOnce  sync.Once\n\treqEncoding []*apienc.RequestEncoding\n\n\trespEncOnce  sync.Once\n\trespEncoding *apienc.ResponseEncoding\n}\n\nfunc (ep *Endpoint) GoString() string {\n\tif ep == nil {\n\t\treturn \"(*api.Endpoint)(nil)\"\n\t}\n\treturn fmt.Sprintf(\"&api.Endpoint{Name: %q}\", ep.Name)\n}\n\nfunc (ep *Endpoint) Kind() resource.Kind       { return resource.APIEndpoint }\nfunc (ep *Endpoint) Package() *pkginfo.Package { return ep.File.Pkg }\nfunc (ep *Endpoint) Pos() token.Pos            { return ep.Decl.AST.Pos() }\nfunc (ep *Endpoint) End() token.Pos            { return ep.Decl.AST.End() }\nfunc (ep *Endpoint) SortKey() string           { return ep.File.Pkg.ImportPath.String() + \".\" + ep.Name }\n\nfunc (ep *Endpoint) RequestEncoding() []*apienc.RequestEncoding {\n\tif ep.Request == nil {\n\t\treturn nil\n\t}\n\n\tep.reqEncOnce.Do(func() {\n\t\trequestParam := ep.Decl.Type.Params[len(ep.Decl.Type.Params)-1]\n\t\tep.reqEncoding = apienc.DescribeRequest(ep.errs, requestParam, ep.Request, ep.HTTPMethodsField, ep.HTTPMethods...)\n\t})\n\treturn ep.reqEncoding\n}\n\nfunc (ep *Endpoint) ResponseEncoding() *apienc.ResponseEncoding {\n\tep.respEncOnce.Do(func() {\n\t\tep.respEncoding = apienc.DescribeResponse(ep.errs, ep.Response)\n\t})\n\treturn ep.respEncoding\n}\n\ntype ParseData struct {\n\tErrs   *perr.List\n\tSchema *schema.Parser\n\n\tFile *pkginfo.File\n\tFunc *ast.FuncDecl\n\tDir  *directive.Directive\n\tDoc  string\n}\n\n// Parse parses an API endpoint. It may return nil on errors.\nfunc Parse(d ParseData) *Endpoint {\n\trpc, ok := validateDirective(d.Errs, d.Dir)\n\tif !ok {\n\t\treturn nil\n\t}\n\trpc.errs = d.Errs\n\n\t// If there was no path, default to \"pkg.Decl\".\n\tif rpc.Path == nil {\n\t\trpc.Path = &resourcepaths.Path{\n\t\t\tStartPos: d.Func.Name.Pos(),\n\t\t\tSegments: []resourcepaths.Segment{{\n\t\t\t\tType:      resourcepaths.Literal,\n\t\t\t\tValue:     d.File.Pkg.Name + \".\" + d.Func.Name.Name,\n\t\t\t\tValueType: schema.String,\n\t\t\t\tStartPos:  d.Func.Name.Pos(),\n\t\t\t\tEndPos:    d.Func.Name.End(),\n\t\t\t}},\n\t\t}\n\t}\n\n\tdecl, ok := d.Schema.ParseFuncDecl(d.File, d.Func)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\trpc.Name = d.Func.Name.Name\n\trpc.Doc = d.Doc\n\trpc.Decl = decl\n\trpc.File = d.File\n\trpc.Recv = decl.Recv\n\n\t// Validate the API.\n\tif rpc.Raw {\n\t\tinitRawRPC(d.Errs, rpc)\n\t} else {\n\t\tinitTypedRPC(d.Errs, rpc)\n\t}\n\n\t// If we didn't get any HTTP methods, set a reasonable default.\n\t// TODO(andre) Replace this with the API encoding.\n\tif len(rpc.HTTPMethods) == 0 {\n\t\tif rpc.Raw {\n\t\t\trpc.HTTPMethods = []string{\"*\"}\n\t\t} else {\n\t\t\t// For non-raw endpoints, if there's a request payload\n\t\t\t// default to POST-only.\n\t\t\tif rpc.Request != nil {\n\t\t\t\trpc.HTTPMethods = []string{\"POST\"}\n\t\t\t} else {\n\t\t\t\trpc.HTTPMethods = []string{\"GET\", \"POST\"}\n\t\t\t}\n\t\t}\n\t}\n\n\t// RequestEncoding will validate the request payload.\n\trpc.RequestEncoding()\n\n\t// ResponseEncoding will validate the response payload.\n\trpc.ResponseEncoding()\n\n\treturn rpc\n}\n\nfunc initTypedRPC(errs *perr.List, endpoint *Endpoint) {\n\tdecl := endpoint.Decl\n\tsig := decl.Type\n\tnumParams := len(sig.Params)\n\tif numParams == 0 {\n\t\terrs.Add(errWrongNumberParams(numParams).AtGoNode(sig.AST.Params))\n\t\treturn\n\t}\n\n\tnumResults := len(sig.Results)\n\tif numResults == 0 || numResults > 2 {\n\t\terrs.Add(errWrongNumberResults(numResults).AtGoNode(sig.AST.Results))\n\t\treturn\n\t}\n\n\t// First type should always be context.Context\n\tctxParam := sig.Params[0]\n\tif !schemautil.IsNamed(ctxParam.Type, \"context\", \"Context\") {\n\t\terrs.Add(errInvalidFirstParam.AtGoNode(ctxParam.AST))\n\t\treturn\n\t}\n\n\t// For each path parameter, expect a parameter to match it\n\tvar pathParams []*resourcepaths.Segment\n\tfor i := 0; i < len(endpoint.Path.Segments); i++ {\n\t\tif s := &endpoint.Path.Segments[i]; s.Type != resourcepaths.Literal {\n\t\t\tpathParams = append(pathParams, s)\n\t\t}\n\t}\n\n\tseenParams := 0\n\tfor i := 0; i < numParams-1; i++ {\n\t\tparam := sig.Params[i+1] // +1 to skip context.Context\n\n\t\t// Is it a path parameter?\n\t\tif i < len(pathParams) {\n\t\t\tseg := pathParams[i]\n\t\t\tb := validatePathParam(errs, param, seg)\n\t\t\tpathParams[seenParams].ValueType = b\n\t\t\tseenParams++\n\t\t} else {\n\t\t\t// Otherwise it must be a payload parameter\n\t\t\tpayloadIdx := i - len(pathParams)\n\t\t\tif payloadIdx > 0 {\n\t\t\t\terrs.Add(errMultiplePayloads.AtGoNode(param.AST))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tendpoint.Request = param.Type\n\t\t}\n\t}\n\n\tif seenParams < len(pathParams) {\n\t\tvar missing []string\n\t\tvar missingParams []*resourcepaths.Segment\n\t\tfor i := seenParams; i < len(pathParams); i++ {\n\t\t\tmissing = append(missing, pathParams[i].Value)\n\t\t\tmissingParams = append(missingParams, pathParams[i])\n\t\t}\n\n\t\terr := errInvalidPathParams(strings.Join(missing, \"', '\")).AtGoNode(sig.AST.Params)\n\t\tfor _, p := range missingParams {\n\t\t\terr = err.AtGoNode(p)\n\t\t}\n\n\t\terrs.Add(err)\n\t}\n\n\t// First return value must be *T or *pkg.T\n\tif numResults >= 2 {\n\t\tresult := sig.Results[0]\n\t\tendpoint.Response = result.Type\n\t}\n\n\t// Make sure the last return is of type error.\n\tif err := sig.Results[numResults-1]; !schemautil.IsBuiltinKind(err.Type, schema.Error) {\n\t\terrs.Add(errLastResultMustBeError.AtGoNode(err.AST))\n\t\treturn\n\t}\n}\n\nfunc initRawRPC(errs *perr.List, endpoint *Endpoint) {\n\tdecl := endpoint.Decl\n\tsig := decl.Type\n\tparams := sig.Params\n\tif len(params) < 2 {\n\t\terrs.Add(errInvalidRawParams(len(params)).AtGoNode(sig.AST.Params))\n\t\treturn\n\t} else if len(params) > 2 {\n\t\terrs.Add(errInvalidRawParams(len(params)).AtGoNode(sig.AST.Params))\n\t} else if len(sig.Results) > 0 {\n\t\terrs.Add(errInvalidRawResults(len(sig.Results)).AtGoNode(sig.AST.Results))\n\t\treturn\n\t}\n\n\t// Ensure signature is func(http.ResponseWriter, *http.Request).\n\tif !schemautil.IsNamed(params[0].Type, \"net/http\", \"ResponseWriter\") {\n\t\terrs.Add(errRawNotResponeWriter.AtGoNode(params[0].AST))\n\t}\n\tif deref, n := schemautil.Deref(params[1].Type); n != 1 || !schemautil.IsNamed(deref, \"net/http\", \"Request\") {\n\t\terrs.Add(errRawNotRequest.AtGoNode(params[1].AST))\n\t}\n}\n\n// validatePathParam validates that the given func parameter is compatible with the given path segment.\n// It checks that the names match and that the func parameter is of a permissible type.\n// It returns the func parameter's builtin kind.\nfunc validatePathParam(errs *perr.List, param schema.Param, seg *resourcepaths.Segment) schema.BuiltinKind {\n\tif !option.Contains(param.Name, seg.Value) {\n\t\terrs.Add(errUnexpectedParameterName(param.Name, seg.Value, seg.String()).AtGoNode(seg).AtGoNode(param.AST))\n\t}\n\n\tbuiltin, _ := param.Type.(schema.BuiltinType)\n\tb := builtin.Kind\n\n\t// Wildcard path parameters must be strings.\n\tif b != schema.String && (seg.Type == resourcepaths.Wildcard || seg.Type == resourcepaths.Fallback) {\n\t\terrs.Add(errWildcardMustBeString(param.Name).AtGoNode(seg).AtGoNode(param.AST))\n\t}\n\n\tswitch b {\n\tcase schema.String, schema.Bool,\n\t\tschema.Int, schema.Int8, schema.Int16, schema.Int32, schema.Int64,\n\t\tschema.Uint, schema.Uint8, schema.Uint16, schema.Uint32, schema.Uint64,\n\t\tschema.UUID:\n\t\treturn b\n\tdefault:\n\t\terrs.Add(errInvalidPathParamType(param.Name).AtGoNode(seg).AtGoNode(param.AST))\n\t\treturn schema.Invalid\n\t}\n}\n\n// validateDirective validates the given encore:api directive\n// and returns an API with the respective fields set.\nfunc validateDirective(errs *perr.List, dir *directive.Directive) (*Endpoint, bool) {\n\tendpoint := &Endpoint{\n\t\tRaw: dir.HasOption(\"raw\"),\n\t}\n\n\tvar accessField directive.Field\n\tvar rawTag directive.Field\n\n\taccessOptions := []string{\"public\", \"private\", \"auth\"}\n\tok := directive.Validate(errs, dir, directive.ValidateSpec{\n\t\tAllowedOptions: append([]string{\"raw\", \"sensitive\"}, accessOptions...),\n\t\tAllowedFields:  []string{\"path\", \"method\"},\n\n\t\tValidateOption: func(errs *perr.List, opt directive.Field) (ok bool) {\n\t\t\t// If this is an access option, check for duplicates.\n\t\t\tif slices.Contains(accessOptions, opt.Value) {\n\t\t\t\tif endpoint.Access != \"\" {\n\t\t\t\t\terrs.Add(errDuplicateAccessOptions(endpoint.Access, opt.Value, strings.Join(accessOptions, \", \")).AtGoNode(opt).AtGoNode(accessField))\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\taccessField = opt\n\t\t\t\tendpoint.Access = AccessType(opt.Value)\n\t\t\t\tendpoint.AccessField = option.Some(opt)\n\t\t\t}\n\n\t\t\tswitch opt.Value {\n\t\t\tcase \"raw\":\n\t\t\t\trawTag = opt\n\t\t\tcase \"sensitive\":\n\t\t\t\tendpoint.Sensitive = true\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\t\tValidateField: func(errs *perr.List, f directive.Field) (ok bool) {\n\t\t\tswitch f.Key {\n\t\t\tcase \"path\":\n\t\t\t\tendpoint.Path, ok = resourcepaths.Parse(\n\t\t\t\t\terrs,\n\t\t\t\t\tf.End()-token.Pos(len([]byte(f.Value))-1),\n\t\t\t\t\tf.Value,\n\t\t\t\t\tresourcepaths.Options{\n\t\t\t\t\t\tAllowWildcard: true,\n\t\t\t\t\t\tAllowFallback: true,\n\t\t\t\t\t\tPrefixSlash:   true,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\tcase \"method\":\n\t\t\t\tendpoint.HTTPMethods = f.List()\n\t\t\t\tendpoint.HTTPMethodsField = option.Some(f)\n\t\t\t\tfor _, m := range endpoint.HTTPMethods {\n\t\t\t\t\tfor _, c := range m {\n\t\t\t\t\t\tif !(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') {\n\t\t\t\t\t\t\terrs.Add(errInvalidEndpointMethod(m).AtGoNode(f))\n\t\t\t\t\t\t\treturn false\n\t\t\t\t\t\t} else if !(c >= 'A' && c <= 'Z') {\n\t\t\t\t\t\t\terrs.Add(errEndpointMethodMustBeAllCaps.AtGoNode(f))\n\t\t\t\t\t\t\treturn false\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\tValidateTag: func(errs *perr.List, tag directive.Field) (ok bool) {\n\t\t\tsel, ok := selector.Parse(errs, tag.Pos(), tag.Value)\n\t\t\tif !ok {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tendpoint.Tags.Add(sel)\n\t\t\treturn true\n\t\t},\n\t})\n\tif !ok {\n\t\treturn nil, false\n\t}\n\n\t// Access defaults to private if not provided.\n\tif endpoint.Access == \"\" {\n\t\tendpoint.Access = Private\n\t}\n\tif endpoint.Access == Private && endpoint.Raw {\n\t\t// We don't support private raw APIs for now.\n\t\terrs.Add(errRawEndpointCantBePrivate.AtGoNode(rawTag, errors.AsError(\"declared as raw here\")).AtGoNode(accessField, errors.AsError(\"set as private here\")))\n\t\treturn nil, false\n\t}\n\n\treturn endpoint, true\n}\n"
  },
  {
    "path": "v2/parser/apis/api/api_test.go",
    "content": "package api\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/apis/selector\"\n)\n\nfunc TestParseRPC(t *testing.T) {\n\ttype testCase struct {\n\t\tname     string\n\t\timports  []string\n\t\tdef      string\n\t\twant     *Endpoint\n\t\twantErrs []string\n\t}\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"basic\",\n\t\t\tdef: `\n// Foo does things.\n//encore:api public\nfunc Foo(ctx context.Context) error {}\n`,\n\t\t\twant: &Endpoint{\n\t\t\t\tName:        \"Foo\",\n\t\t\t\tDoc:         \"Foo does things.\\n\",\n\t\t\t\tAccess:      Public,\n\t\t\t\tAccessField: option.Some(directive.Field{Value: \"public\"}),\n\t\t\t\tPath: &resourcepaths.Path{Segments: []resourcepaths.Segment{\n\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"foo.Foo\", ValueType: schema.String},\n\t\t\t\t}},\n\t\t\t\tHTTPMethods: []string{\"GET\", \"POST\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with_fields\",\n\t\t\tdef: `\n//encore:api private path=/foo method=PUT tag:some-tag\nfunc Foo(ctx context.Context) error {}\n`,\n\t\t\twant: &Endpoint{\n\t\t\t\tName:        \"Foo\",\n\t\t\t\tDoc:         \"\",\n\t\t\t\tAccess:      Private,\n\t\t\t\tAccessField: option.Some(directive.Field{Value: \"private\"}),\n\t\t\t\tPath: &resourcepaths.Path{Segments: []resourcepaths.Segment{\n\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"foo\", ValueType: schema.String},\n\t\t\t\t}},\n\t\t\t\tHTTPMethods: []string{\"PUT\"},\n\t\t\t\tTags:        selector.NewSet(selector.Selector{Type: selector.Tag, Value: \"some-tag\"}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with_string_param\",\n\t\t\tdef: `\n//encore:api auth path=/:key\nfunc Foo(ctx context.Context, key string) error {}\n`,\n\t\t\twant: &Endpoint{\n\t\t\t\tName:        \"Foo\",\n\t\t\t\tDoc:         \"\",\n\t\t\t\tAccess:      Auth,\n\t\t\t\tAccessField: option.Some(directive.Field{Value: \"auth\"}),\n\t\t\t\tPath: &resourcepaths.Path{Segments: []resourcepaths.Segment{\n\t\t\t\t\t{Type: resourcepaths.Param, Value: \"key\", ValueType: schema.String},\n\t\t\t\t}},\n\t\t\t\tHTTPMethods: []string{\"GET\", \"POST\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with_int_param\",\n\t\t\tdef: `\n//encore:api auth path=/foo/:key\nfunc Foo(ctx context.Context, key int) error {}\n`,\n\t\t\twant: &Endpoint{\n\t\t\t\tName:        \"Foo\",\n\t\t\t\tDoc:         \"\",\n\t\t\t\tAccess:      Auth,\n\t\t\t\tAccessField: option.Some(directive.Field{Value: \"auth\"}),\n\t\t\t\tPath: &resourcepaths.Path{Segments: []resourcepaths.Segment{\n\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"foo\", ValueType: schema.String},\n\t\t\t\t\t{Type: resourcepaths.Param, Value: \"key\", ValueType: schema.Int},\n\t\t\t\t}},\n\t\t\t\tHTTPMethods: []string{\"GET\", \"POST\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"raw\",\n\t\t\timports: []string{\"net/http\"},\n\t\t\tdef: `\n//encore:api public raw path=/raw\nfunc Raw(w http.ResponseWriter, req *http.Request) {}\n`,\n\t\t\twant: &Endpoint{\n\t\t\t\tName:        \"Raw\",\n\t\t\t\tDoc:         \"\",\n\t\t\t\tAccess:      Public,\n\t\t\t\tAccessField: option.Some(directive.Field{Value: \"public\"}),\n\t\t\t\tRaw:         true,\n\t\t\t\tPath: &resourcepaths.Path{Segments: []resourcepaths.Segment{\n\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"raw\", ValueType: schema.String},\n\t\t\t\t}},\n\t\t\t\tHTTPMethods: []string{\"*\"},\n\t\t\t},\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timportList := append([]string{\"context\"}, test.imports...)\n\t\timports := \"\"\n\t\tif len(importList) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range importList {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.def + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tschemaParser := schema.NewParser(tc.Context, l)\n\n\t\t\tif len(test.wantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.wantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tf := pkg.Files[0]\n\t\t\tfd := testutil.FindNodes[*ast.FuncDecl](f.AST())[0]\n\n\t\t\t// Parse the directive from the func declaration.\n\t\t\tdir, doc, ok := directive.Parse(tc.Errs, fd.Doc)\n\t\t\tc.Assert(ok, qt.IsTrue, qt.Commentf(\"directive not found in %s\", fd.Name.Name))\n\t\t\tpd := ParseData{\n\t\t\t\tErrs:   tc.Errs,\n\t\t\t\tSchema: schemaParser,\n\t\t\t\tFile:   f,\n\t\t\t\tFunc:   fd,\n\t\t\t\tDir:    dir,\n\t\t\t\tDoc:    doc,\n\t\t\t}\n\n\t\t\tgot := Parse(pd)\n\t\t\tif len(test.wantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\tcmpEqual := qt.CmpEquals(\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Node }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&schema.FuncDecl{}, &schema.TypeDecl{}, &pkginfo.File{}, &pkginfo.Package{}, token.Pos(0)),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(schema.StructField{}, schema.NamedType{}, Endpoint{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\tc.Assert(got, cmpEqual, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/api/apienc/encoding.go",
    "content": "package apienc\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/directive\"\n)\n\n// WireLoc is the location of a parameter in the HTTP request/response.\ntype WireLoc string\n\nconst (\n\tUndefined  WireLoc = \"undefined\"  // Parameter location is undefined\n\tHeader     WireLoc = \"header\"     // Parameter is placed in the HTTP header\n\tQuery      WireLoc = \"query\"      // Parameter is placed in the query string\n\tBody       WireLoc = \"body\"       // Parameter is placed in the body\n\tCookie     WireLoc = \"cookie\"     // Parameter is placed in cookies\n\tHTTPStatus WireLoc = \"httpstatus\" // Parameter represents the HTTP status code\n)\n\nvar (\n\tQueryTag = tagDescription{\n\t\tlocation:        Query,\n\t\toverrideDefault: true,\n\t}\n\tQsTag     = QueryTag\n\tHeaderTag = tagDescription{\n\t\tlocation:        Header,\n\t\toverrideDefault: true,\n\t\twireFormatter:   strings.ToLower,\n\t}\n\tJSONTag = tagDescription{\n\t\tlocation:        Body,\n\t\tomitEmptyOption: \"omitempty\",\n\t\toverrideDefault: false,\n\t}\n\tCookieTag = tagDescription{\n\t\tlocation:        Cookie,\n\t\tomitEmptyOption: \"omitempty\",\n\t\toverrideDefault: true,\n\t}\n\tHTTPStatusTag = tagDescription{\n\t\tlocation:        HTTPStatus,\n\t\toverrideDefault: true,\n\t}\n)\n\n// requestTags is a description of tags used for requests\nvar requestTags = map[string]tagDescription{\n\t\"query\":  QueryTag,\n\t\"qs\":     QsTag,\n\t\"header\": HeaderTag,\n\t\"json\":   JSONTag,\n}\n\n// responseTags is a description of tags used for responses\nvar responseTags = map[string]tagDescription{\n\t\"header\": HeaderTag,\n\t\"json\":   JSONTag,\n}\n\n// authTags is a description of tags used for auth\nvar authTags = map[string]tagDescription{\n\t\"query\":  QueryTag,\n\t\"header\": HeaderTag,\n\t\"cookie\": CookieTag,\n}\n\n// tagDescription is used to map struct field tags to param locations\n// if overrideDefault is set, tagDescription.location will be used instead of encodingHints.defaultLocation\n// if the tag matches the paramLocation, the param name will be replaced with the\n// tag name\ntype tagDescription struct {\n\tlocation        WireLoc\n\toverrideDefault bool\n\tomitEmptyOption string\n\twireFormatter   func(name string) string\n}\n\n// encodingHints is used to determine the default location and applicable tag overrides for http\n// request/response encoding\ntype encodingHints struct {\n\tdefaultLocation WireLoc\n\ttags            map[string]tagDescription\n}\n\n// APIEncoding expresses how an RPC should be encoded on the wire for both the request and responses.\ntype APIEncoding struct {\n\tDefaultMethod string `json:\"default_method\"`\n\t// Expresses how the default request encoding and method should be\n\t// Note: DefaultRequestEncoding.HTTPMethods will always be a slice with length 1\n\tDefaultRequestEncoding *RequestEncoding `json:\"request_encoding\"`\n\t// Expresses all the different ways the request can be encoded for this RPC\n\tRequestEncoding []*RequestEncoding `json:\"all_request_encodings\"`\n\t// Expresses how the response to this RPC will be encoded\n\tResponseEncoding *ResponseEncoding `json:\"response_encoding\"`\n}\n\n// RequestEncodingForMethod returns the request encoding required for the given HTTP method.\n// If the method is not supported by the RPC it reports nil.\nfunc (e *APIEncoding) RequestEncodingForMethod(method string) *RequestEncoding {\n\tvar wildcardOption *RequestEncoding\n\n\tfor _, reqEnc := range e.RequestEncoding {\n\t\tfor _, m := range reqEnc.HTTPMethods {\n\t\t\tif strings.EqualFold(m, method) {\n\t\t\t\treturn reqEnc\n\t\t\t}\n\n\t\t\tif m == \"*\" {\n\t\t\t\twildcardOption = reqEnc\n\t\t\t}\n\t\t}\n\t}\n\treturn wildcardOption\n}\n\n// ResponseEncoding expresses how a response should be encoded on the wire\ntype ResponseEncoding struct {\n\t// Contains metadata about how to marshal an HTTP parameter\n\tHeaderParameters []*ParameterEncoding `json:\"header_parameters\"`\n\tBodyParameters   []*ParameterEncoding `json:\"body_parameters\"`\n\t// HTTPStatusParameter contains encoding info for the HTTP status field, if any\n\tHTTPStatusParameter *ParameterEncoding `json:\"http_status_parameter,omitempty\"`\n}\n\nfunc (r *ResponseEncoding) AllParameters() []*ParameterEncoding {\n\tparams := append(r.HeaderParameters, r.BodyParameters...)\n\tif r.HTTPStatusParameter != nil {\n\t\tparams = append(params, r.HTTPStatusParameter)\n\t}\n\treturn params\n}\n\n// RequestEncoding expresses how a request should be encoded for an explicit set of HTTPMethods\ntype RequestEncoding struct {\n\t// The HTTP methods this encoding can be used for\n\tHTTPMethods []string `json:\"http_methods\"`\n\t// Contains metadata about how to marshal an HTTP parameter\n\tHeaderParameters []*ParameterEncoding `json:\"header_parameters\"`\n\tQueryParameters  []*ParameterEncoding `json:\"query_parameters\"`\n\tBodyParameters   []*ParameterEncoding `json:\"body_parameters\"`\n}\n\nfunc (r *RequestEncoding) AllParameters() []*ParameterEncoding {\n\treturn append(append(r.HeaderParameters, r.QueryParameters...), r.BodyParameters...)\n}\n\n// ParameterEncoding expresses how a parameter should be encoded on the wire\ntype ParameterEncoding struct {\n\t// SrcName is the name of the struct field\n\tSrcName string `json:\"src_name\"`\n\t// WireName is how the name is encoded on the wire.\n\tWireName string `json:\"wire_name\"`\n\t// Location is the location this encoding is for.\n\tLocation WireLoc `json:\"location\"`\n\t// OmitEmpty specifies whether the parameter should be omitted if it's empty.\n\tOmitEmpty bool `json:\"omit_empty\"`\n\t// Doc is the documentation of the struct field\n\tDoc string `json:\"doc\"`\n\t// Type is the field's type description.\n\tType schema.Type `json:\"type\"`\n}\n\n// DescribeResponse generates a ParameterEncoding per field of the response struct and returns it as\n// the ResponseEncoding\nfunc DescribeResponse(errs *perr.List, responseSchema schema.Type) *ResponseEncoding {\n\tif responseSchema == nil {\n\t\treturn &ResponseEncoding{}\n\t}\n\n\tresponseStruct, ok := getConcreteNamedStruct(errs, responseSchema)\n\tif !ok {\n\t\terrs.Add(errResponseMustBeNamedStruct.AtGoNode(responseSchema.ASTExpr()))\n\t\treturn &ResponseEncoding{}\n\t}\n\n\tfields, ok := describeParams(errs, &encodingHints{Body, responseTags}, responseStruct)\n\tif !ok {\n\t\t// describeParams already added the error to errs\n\t\treturn &ResponseEncoding{}\n\t}\n\n\t// Extract HTTP status parameter from fields and check for multiple status fields\n\tvar httpStatusParameter *ParameterEncoding\n\tif len(fields[HTTPStatus]) > 0 {\n\t\tswitch len(fields[HTTPStatus]) {\n\t\tcase 1:\n\t\t\thttpStatusParameter = fields[HTTPStatus][0]\n\t\tdefault:\n\t\t\terrs.Add(errMultipleHTTPStatusFields.AtGoNode(responseStruct.ASTExpr()))\n\t\t}\n\t}\n\n\t// Check for reserved header prefixes\n\tfor _, field := range fields[Header] {\n\t\tif strings.HasPrefix(strings.ToLower(field.WireName), \"x-encore-\") {\n\t\t\terrs.Add(errReservedHeaderPrefix.AtGoNode(field.Type.ASTExpr()))\n\t\t}\n\t}\n\n\tif keys := keyDiff(fields, Header, Body, HTTPStatus); len(keys) > 0 {\n\t\terr := errResponseTypeMustOnlyBeBodyOrHeaders.AtGoNode(responseSchema.ASTExpr())\n\n\t\tfor _, k := range keys {\n\t\t\tfor _, field := range fields[k] {\n\t\t\t\terr = err.AtGoNode(field.Type.ASTExpr(), errors.AsError(fmt.Sprintf(\"found %s\", field.Location)))\n\t\t\t}\n\t\t}\n\t\terrs.Add(err)\n\t\treturn &ResponseEncoding{}\n\t}\n\n\treturn &ResponseEncoding{\n\t\tBodyParameters:      fields[Body],\n\t\tHeaderParameters:    fields[Header],\n\t\tHTTPStatusParameter: httpStatusParameter,\n\t}\n}\n\nfunc getConcreteNamedStruct(errs *perr.List, typ schema.Type) (st schema.StructType, ok bool) {\n\tif res, ok := schemautil.ResolveNamedStruct(typ, false); ok {\n\t\tconcrete := schemautil.ConcretizeWithTypeArgs(errs, res.Decl.Type, res.TypeArgs)\n\t\treturn concrete.(schema.StructType), true\n\t}\n\treturn schema.StructType{}, false\n}\n\n// keyDiff returns the diff between src.keys and keys\nfunc keyDiff[T comparable, V any](src map[T]V, keys ...T) (diff []T) {\n\tfor k := range src {\n\t\tif !slices.Contains(keys, k) {\n\t\t\tdiff = append(diff, k)\n\t\t}\n\t}\n\treturn diff\n}\n\n// DescribeRequest groups the provided httpMethods by default WireLoc and returns a RequestEncoding\n// per WireLoc\nfunc DescribeRequest(errs *perr.List, requestAST schema.Param, requestSchema schema.Type, methodsField option.Option[directive.Field], httpMethods ...string) []*RequestEncoding {\n\tmethodsByDefaultLocation := make(map[WireLoc][]string)\n\tfor _, m := range httpMethods {\n\t\tswitch m {\n\t\tcase \"GET\", \"HEAD\", \"DELETE\":\n\t\t\tmethodsByDefaultLocation[Query] = append(methodsByDefaultLocation[Query], m)\n\t\tcase \"*\":\n\t\t\tmethodsByDefaultLocation[Body] = []string{\"POST\", \"PUT\", \"PATCH\"}\n\t\t\tmethodsByDefaultLocation[Query] = []string{\"GET\", \"HEAD\", \"DELETE\"}\n\t\tdefault:\n\t\t\tmethodsByDefaultLocation[Body] = append(methodsByDefaultLocation[Body], m)\n\t\t}\n\t}\n\n\tif requestSchema == nil {\n\t\t// If there is no request schema, add an empty encoding that supports\n\t\t// all methods allowed by the endpoint.\n\t\tenc := &RequestEncoding{\n\t\t\tHTTPMethods: httpMethods,\n\t\t}\n\t\tif len(httpMethods) > 0 && httpMethods[0] == \"*\" {\n\t\t\tenc.HTTPMethods = []string{\"POST\", \"PUT\", \"PATCH\", \"GET\", \"HEAD\", \"DELETE\"}\n\t\t}\n\t\treturn []*RequestEncoding{enc}\n\t}\n\n\tst, ok := getConcreteNamedStruct(errs, requestSchema)\n\tif !ok {\n\t\terrs.Add(errRequestMustBeNamedStruct.AtGoNode(requestSchema.ASTExpr()))\n\t\treturn nil\n\t}\n\n\tvar reqs []*RequestEncoding\n\tfor location, methods := range methodsByDefaultLocation {\n\t\tvar fields map[WireLoc][]*ParameterEncoding\n\n\t\tfields, ok := describeParams(errs, &encodingHints{location, requestTags}, st)\n\t\tif !ok {\n\t\t\t// report error in describeParams\n\t\t\treturn nil\n\t\t}\n\n\t\t// Check for reserved header prefixes or invalid data types\n\t\tfor _, field := range fields[Header] {\n\t\t\tif strings.HasPrefix(strings.ToLower(field.WireName), \"x-encore-\") {\n\t\t\t\terrs.Add(errReservedHeaderPrefix.AtGoNode(field.Type.ASTExpr()))\n\t\t\t}\n\n\t\t\tif !schemautil.IsValidHeaderType(field.Type) {\n\t\t\t\terrs.Add(\n\t\t\t\t\terrInvalidHeaderType(field.Type.String()).\n\t\t\t\t\t\tAtGoNode(field.Type.ASTExpr(), errors.AsError(\"unsupported type\")).\n\t\t\t\t\t\tAtGoNode(requestAST.AST, errors.AsHelp(\"used here\")),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\t// Check for invalid datatype in query parameters\n\t\tfor _, field := range fields[Query] {\n\t\t\tif !schemautil.IsValidQueryType(field.Type) {\n\t\t\t\terr := errInvalidQueryStringType(field.Type.String()).\n\t\t\t\t\tAtGoNode(field.Type.ASTExpr(), errors.AsError(\"unsupported type\")).\n\t\t\t\t\tAtGoNode(requestAST.AST, errors.AsHelp(\"used here\"))\n\n\t\t\t\tif field, ok := methodsField.Get(); ok {\n\t\t\t\t\thttpMethod := strings.ToUpper(field.Value)\n\t\t\t\t\tswitch httpMethod {\n\t\t\t\t\tcase \"GET\", \"HEAD\", \"DELETE\":\n\t\t\t\t\t\terr = err.AtGoNode(field, errors.AsHelp(\"you could change this to a POST or PUT request\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\terrs.Add(err)\n\t\t\t}\n\t\t}\n\n\t\tif errs.Len() > 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\tif keys := keyDiff(fields, Query, Header, Body); len(keys) > 0 {\n\t\t\terr := errRequestInvalidLocation.AtGoNode(requestSchema.ASTExpr())\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tfor _, field := range fields[k] {\n\t\t\t\t\terr = err.AtGoNode(field.Type.ASTExpr(), errors.AsError(fmt.Sprintf(\"found %s\", field.Location)))\n\t\t\t\t}\n\t\t\t}\n\t\t\terrs.Add(err)\n\t\t\treturn nil\n\t\t}\n\t\treqs = append(reqs, &RequestEncoding{\n\t\t\tHTTPMethods:      methods,\n\t\t\tQueryParameters:  fields[Query],\n\t\t\tHeaderParameters: fields[Header],\n\t\t\tBodyParameters:   fields[Body],\n\t\t})\n\t}\n\n\t// Sort by first method to get a deterministic order (list is randomized by map above)\n\tsort.Slice(reqs, func(i, j int) bool {\n\t\treturn reqs[i].HTTPMethods[0] < reqs[j].HTTPMethods[0]\n\t})\n\treturn reqs\n}\n\n// AuthEncoding expresses how a response should be encoded on the wire.\ntype AuthEncoding struct {\n\t// LegacyTokenFormat specifies whether the auth encoding uses the legacy format of\n\t// \"just give us a token as a string\". If true, the other parameters are all empty.\n\tLegacyTokenFormat bool\n\n\t// Contains metadata about how to marshal an HTTP parameter\n\tHeaderParameters []*ParameterEncoding `json:\"header_parameters\"`\n\tQueryParameters  []*ParameterEncoding `json:\"query_parameters\"`\n\tCookieParameters []*ParameterEncoding `json:\"cookie_parameters\"`\n}\n\n// DescribeAuth generates a ParameterEncoding per field of the auth struct and returns it as\n// the AuthEncoding. If authSchema is nil it returns nil.\nfunc DescribeAuth(errs *perr.List, authSchema schema.Type) *AuthEncoding {\n\tif authSchema == nil {\n\t\treturn nil\n\t}\n\n\t// Do we have a legacy, string-based handler?\n\tif builtin, ok := authSchema.(schema.BuiltinType); ok {\n\t\tif builtin.Kind != schema.String {\n\t\t\terrs.Add(authhandler.ErrInvalidAuthSchemaType.AtGoNode(builtin.ASTExpr()))\n\t\t}\n\t\treturn &AuthEncoding{LegacyTokenFormat: true}\n\t}\n\n\tst, ok := getConcreteNamedStruct(errs, authSchema)\n\tif !ok {\n\t\terrs.Add(authhandler.ErrInvalidAuthSchemaType.AtGoNode(authSchema.ASTExpr()))\n\t\treturn nil\n\t}\n\n\tfields, ok := describeParams(errs, &encodingHints{Undefined, authTags}, st)\n\tif !ok {\n\t\t// reported by describeParams\n\t\treturn nil\n\t}\n\tif locationDiff := keyDiff(fields, Header, Query, Cookie); len(locationDiff) > 0 {\n\t\terr := authhandler.ErrInvalidFieldTags.AtGoNode(authSchema.ASTExpr())\n\n\t\tfor _, k := range locationDiff {\n\t\t\tfor _, field := range fields[k] {\n\t\t\t\terr = err.AtGoNode(field.Type.ASTExpr(), errors.AsError(fmt.Sprintf(\"found %s\", field.Location)))\n\t\t\t}\n\t\t}\n\t\terrs.Add(err)\n\t\treturn nil\n\t}\n\treturn &AuthEncoding{\n\t\tQueryParameters:  fields[Query],\n\t\tHeaderParameters: fields[Header],\n\t\tCookieParameters: fields[Cookie],\n\t}\n}\n\n// describeParams calls describeParam() for each field in the payload struct\nfunc describeParams(errs *perr.List, encodingHints *encodingHints, payload schema.StructType) (fields map[WireLoc][]*ParameterEncoding, ok bool) {\n\tparamByLocation := make(map[WireLoc][]*ParameterEncoding)\n\tfor _, f := range payload.Fields {\n\t\tif !f.IsExported() {\n\t\t\tcontinue\n\t\t}\n\n\t\tf, ok := describeParam(errs, encodingHints, f)\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\n\t\tif f != nil {\n\t\t\tparamByLocation[f.Location] = append(paramByLocation[f.Location], f)\n\t\t}\n\t}\n\treturn paramByLocation, true\n}\n\n// formatName formats a parameter name with the default formatting for the location (e.g. snakecase for query)\nfunc formatName(location WireLoc, name string) string {\n\tswitch location {\n\tcase Query:\n\t\treturn idents.Convert(name, idents.SnakeCase)\n\tdefault:\n\t\treturn name\n\t}\n}\n\n// IgnoreField returns true if the field name is \"-\" is any of the valid request or response tags\n// or if the field is marked with encore:\"httpstatus\" (which shouldn't appear in client types)\nfunc IgnoreField(field schema.StructField) bool {\n\tfor _, tag := range field.Tag.Tags() {\n\t\tif _, found := requestTags[tag.Key]; found && tag.Name == \"-\" {\n\t\t\treturn true\n\t\t}\n\t\t// Skip fields with encore:\"httpstatus\" tag - they're for internal HTTP status handling only\n\t\tif tag.Key == \"encore\" && tag.Name == \"httpstatus\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// describeParam returns the ParameterEncoding which uses field tags to describe how the parameter\n// (e.g. qs, query, header) should be encoded in HTTP (name and location).\n//\n// It returns nil, nil if the field is not to be encoded.\nfunc describeParam(errs *perr.List, encodingHints *encodingHints, field schema.StructField) (*ParameterEncoding, bool) {\n\tif field.Name.Empty() {\n\t\t// TODO(andre) We don't yet support encoding anonymous fields.\n\t\terrs.Add(errAnonymousFields.AtGoNode(field.AST))\n\t\treturn nil, false\n\t}\n\tsrcName := field.Name.MustGet()\n\n\tdefaultWireName := formatName(encodingHints.defaultLocation, srcName)\n\tparam := ParameterEncoding{\n\t\tOmitEmpty: false,\n\t\tSrcName:   srcName,\n\t\tDoc:       field.Doc,\n\t\tType:      field.Type,\n\t\tWireName:  defaultWireName,\n\t}\n\n\t// Determine which location we should use for this field.\n\tlocation := encodingHints.defaultLocation\n\tvar usedOverrideTag string\n\tfor _, tag := range field.Tag.Tags() {\n\t\t// Handle fields with encore:\"httpstatus\" tag\n\t\tif tag.Key == \"encore\" && tag.Name == \"httpstatus\" {\n\t\t\tif !isValidHTTPStatusType(field.Type) {\n\t\t\t\terrs.Add(errHTTPStatusFieldMustBeInt.AtGoNode(field.AST))\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\treturn &ParameterEncoding{\n\t\t\t\tSrcName:   srcName,\n\t\t\t\tWireName:  \"httpstatus\",\n\t\t\t\tLocation:  HTTPStatus,\n\t\t\t\tOmitEmpty: false,\n\t\t\t\tDoc:       field.Doc,\n\t\t\t\tType:      field.Type,\n\t\t\t}, true\n\t\t}\n\n\t\ttagHint, ok := encodingHints.tags[tag.Key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the presence of this tag overrides the default, update the location.\n\t\tif tagHint.overrideDefault {\n\t\t\tif usedOverrideTag != \"\" {\n\t\t\t\t// There is only allowed to be a single override.\n\t\t\t\terrs.Add(errTagConflict(usedOverrideTag, tag.Key).AtGoNode(field.AST.Tag))\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t\tlocation = tagHint.location\n\t\t\tusedOverrideTag = tag.Key\n\t\t}\n\t}\n\n\t// For the location, see if there is tag information for it.\n\tfor _, tag := range field.Tag.Tags() {\n\t\tif tagHint, ok := encodingHints.tags[tag.Key]; ok && tagHint.location == location {\n\t\t\t// We have found the tag that applies to the default location.\n\t\t\t// Determine if this tag actually has a name. If not, use the existing name.\n\t\t\tif tag.Name == \"-\" {\n\t\t\t\t// This field is to be ignored.\n\t\t\t\treturn nil, true\n\t\t\t}\n\t\t\tif tag.Name != \"\" {\n\t\t\t\tif tagHint.wireFormatter != nil {\n\t\t\t\t\tparam.WireName = tagHint.wireFormatter(tag.Name)\n\t\t\t\t} else {\n\t\t\t\t\tparam.WireName = tag.Name\n\t\t\t\t}\n\t\t\t}\n\t\t\tparam.OmitEmpty = tag.HasOption(\"omitempty\")\n\t\t}\n\t}\n\n\tparam.Location = location\n\treturn &param, true\n}\n\n// isValidHTTPStatusType returns true if the given type is valid for HTTP status fields.\n// Valid types are integer types that can hold a http status code\nfunc isValidHTTPStatusType(typ schema.Type) bool {\n\tbuiltin, ok := typ.(schema.BuiltinType)\n\tif !ok {\n\t\treturn false\n\t}\n\n\tswitch builtin.Kind {\n\tcase schema.Int, schema.Int16, schema.Int32, schema.Int64,\n\t\tschema.Uint, schema.Uint16, schema.Uint32, schema.Uint64:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/api/apienc/errors.go",
    "content": "package apienc\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"apienc\",\n\t\t\"For more information on API schemas, see https://encore.dev/docs/develop/api-schemas\",\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrAnonymousFields = errRange.New(\n\t\t\"Invalid API schema\",\n\t\t\"Anonymous fields in top-level request/response types are not supported.\",\n\t)\n\n\terrTagConflict = errRange.Newf(\n\t\t\"Invalid API schema\",\n\t\t\"The tag \\\"%s\\\" cannot be used with the tag \\\"%s\\\".\",\n\t)\n\n\terrResponseMustBeNamedStruct = errRange.New(\n\t\t\"Invalid response type\",\n\t\t\"API response types must be named structs.\",\n\t)\n\n\terrResponseTypeMustOnlyBeBodyOrHeaders = errRange.New(\n\t\t\"Invalid response type\",\n\t\t\"API response type must only contain a body, headers or status parameters.\",\n\t)\n\n\terrRequestMustBeNamedStruct = errRange.New(\n\t\t\"Invalid request type\",\n\t\t\"API request types must be named structs.\",\n\t)\n\n\terrRequestInvalidLocation = errRange.New(\n\t\t\"Invalid request type\",\n\t\t\"API request must only contain query, body, and header parameters.\",\n\t)\n\n\terrReservedHeaderPrefix = errRange.New(\n\t\t\"Use of reserved header prefix\",\n\t\t\"HTTP headers starting with \\\"X-Encore\\\" are reserved for internal use.\",\n\t)\n\n\tErrFuncNotSupported = errRange.New(\n\t\t\"Invalid API schema\",\n\t\t\"Functions are not supported in API schemas.\",\n\t)\n\n\tErrInterfaceNotSupported = errRange.New(\n\t\t\"Invalid API schema\",\n\t\t\"Interfaces are not supported in API schemas.\",\n\t)\n\n\tErrAnonymousFieldsNotSupported = errRange.New(\n\t\t\"Invalid API schema\",\n\t\t\"Anonymous fields are not supported in API schemas.\",\n\t)\n\n\terrInvalidHeaderType = errRange.Newf(\n\t\t\"Invalid request type\",\n\t\t\"API request parameters of type %s are not supported in headers. You can only \"+\n\t\t\t\"use built-in types types such as strings, booleans, int, time.Time.\",\n\n\t\terrors.WithDetails(\"See https://encore.dev/docs/develop/api-schemas#supported-types for more information.\"),\n\t)\n\n\terrInvalidQueryStringType = errRange.Newf(\n\t\t\"Invalid request type\",\n\t\t\"API request parameters of type %s are not supported in query strings. You can only \"+\n\t\t\t\"use built-in types, or slices of built-in types such as strings, booleans, int, time.Time.\",\n\n\t\terrors.WithDetails(\"APIs which are sent as GET, HEAD or DELETE requests are unable to contain JSON bodies, \"+\n\t\t\t\"thus all parameters must be sent as query strings or headers. \"+\n\t\t\t\"See https://encore.dev/docs/develop/api-schemas#supported-types for more information.\"),\n\t)\n\n\terrMultipleHTTPStatusFields = errRange.New(\n\t\t\"Invalid response type\",\n\t\t\"Only one field can be tagged with encore:\\\"httpstatus\\\" per response struct.\",\n\t)\n\n\terrHTTPStatusFieldMustBeInt = errRange.New(\n\t\t\"Invalid response type\",\n\t\t\"Fields tagged with encore:\\\"httpstatus\\\" must be of an integer type.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/api/errors.go",
    "content": "package api\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nconst rawHint = `hint: signature must be func(http.ResponseWriter, *http.Request)\n\nFor more information on how to use raw APIs see https://encore.dev/docs/primitives/raw-endpoints`\n\nconst baseHint = \"For more information on how to use APIs see https://encore.dev/docs/primitives/apis\"\n\nvar (\n\terrRange = errors.Range(\n\t\t\"api\",\n\t\t`hint: valid signatures are:\n\t- func(context.Context) error\n\t- func(context.Context) (*ResponseData, error)\n\t- func(context.Context, *RequestData) error\n\t- func(context.Context, *RequestType) (*ResponseData, error)\n\nFor more information on how to use APIs, see https://encore.dev/docs/primitives/apis`,\n\n\t\terrors.WithRangeSize(50),\n\t)\n\n\terrDuplicateAccessOptions = errRange.Newf(\n\t\t\"Invalid API Directive\",\n\t\t\"Multiple access options have been defined for the API; %s and %s. Pick once from %s.\",\n\t)\n\n\terrInvalidEndpointMethod = errRange.Newf(\n\t\t\"Invalid API Directive\",\n\t\t\"Invalid endpoint method %q.\",\n\t)\n\n\terrEndpointMethodMustBeAllCaps = errRange.New(\n\t\t\"Invalid API Directive\",\n\t\t\"Endpoint method must be ALLCAPS.\",\n\t)\n\n\terrRawEndpointCantBePrivate = errRange.New(\n\t\t\"Invalid API Directive\",\n\t\t\"Private APIs cannot be declared as raw endpoints.\",\n\t)\n\n\terrWrongNumberParams = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"API functions must have at least 1 parameter, found %d parameters.\",\n\t)\n\n\terrWrongNumberResults = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"API functions must have at most 1 or 2 results, found %d results.\",\n\t)\n\n\terrInvalidFirstParam = errRange.New(\n\t\t\"Invalid API Function\",\n\t\t\"The first parameter of an API function must be context.Context.\",\n\t)\n\n\terrMultiplePayloads = errRange.New(\n\t\t\"Invalid API Function\",\n\t\t\"API functions can only have one payload parameter.\",\n\t)\n\n\terrInvalidPathParams = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"Expected function parameters named '%s' to match Endpoint path params.\",\n\t)\n\n\terrLastResultMustBeError = errRange.New(\n\t\t\"Invalid API Function\",\n\t\t\"The last result of an API function must be error.\",\n\t)\n\n\terrInvalidRawParams = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"Raw APIs must have a two parameters of type http.ResponseWriter and *http.Request, got %d parameters.\",\n\n\t\terrors.WithDetails(rawHint),\n\t)\n\n\terrInvalidRawResults = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"Raw APIs must not return any results, got %d results.\",\n\n\t\terrors.WithDetails(rawHint),\n\t)\n\n\terrRawNotResponeWriter = errRange.New(\n\t\t\"Invalid API Function\",\n\t\t\"Raw APIs must have a first parameter of type http.ResponseWriter.\",\n\n\t\terrors.WithDetails(rawHint),\n\t)\n\terrRawNotRequest = errRange.New(\n\t\t\"Invalid API Function\",\n\t\t\"Raw APIs must have a second parameter of type *http.Request.\",\n\n\t\terrors.WithDetails(rawHint),\n\t)\n\n\terrUnexpectedParameterName = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"Unexpected parameter name %q expected %q (to match path parameter %q).\",\n\t)\n\n\terrWildcardMustBeString = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"Wildcard parameter %q must be a string.\",\n\t)\n\n\terrInvalidPathParamType = errRange.Newf(\n\t\t\"Invalid API Function\",\n\t\t\"Path parameter %q must be a string, bool, integer, or encore.dev/types/uuid.UUID.\",\n\t)\n\n\tErrInvalidEndpointUsage = errRange.New(\n\t\t\"Invalid API Usage\",\n\t\t\"APIs can not be referenced without being called, unless they are used as a cron job endpoint, or a PubSub subscription handler.\",\n\n\t\terrors.WithDetails(baseHint),\n\t)\n\n\tErrAPICalledOutsideService = errRange.New(\n\t\t\"Invalid API call\",\n\t\t\"APIs can only be called from within a service, the current call site is outside any services within the application.\",\n\n\t\terrors.WithDetails(baseHint),\n\t)\n\n\tErrRawEndpointsCannotBeCalled = errRange.New(\n\t\t\"Invalid API call\",\n\t\t\"Raw APIs cannot be called from within an Encore application.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/api/usage.go",
    "content": "package api\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype CallUsage struct {\n\tusage.Base\n\n\t// Endpoint is the endpoint being called.\n\tEndpoint *Endpoint\n\n\t// Call is the function call expression.\n\tCall *ast.CallExpr\n}\n\ntype ReferenceUsage struct {\n\tusage.Base\n\n\t// Endpoint is the endpoint being referenced.\n\tEndpoint *Endpoint\n\n\t// Ref is the reference expression.\n\tRef ast.Expr\n}\n\nfunc ResolveEndpointUsage(data usage.ResolveData, ep *Endpoint) usage.Usage {\n\tswitch expr := data.Expr.(type) {\n\tcase *usage.FuncCall:\n\t\treturn &CallUsage{\n\t\t\tBase: usage.Base{\n\t\t\t\tFile: expr.File,\n\t\t\t\tBind: expr.Bind,\n\t\t\t\tExpr: expr,\n\t\t\t},\n\t\t\tEndpoint: ep,\n\t\t\tCall:     expr.Call,\n\t\t}\n\tcase *usage.Other:\n\t\treturn &ReferenceUsage{\n\t\t\tBase: usage.Base{\n\t\t\t\tFile: expr.File,\n\t\t\t\tBind: expr.Bind,\n\t\t\t\tExpr: expr,\n\t\t\t},\n\t\t\tEndpoint: ep,\n\t\t\tRef:      expr.BindRef,\n\t\t}\n\tcase *usage.FuncArg:\n\t\t// If this is a test file and we're calling `et.MockEndpoint[WithMiddleware]` this is allowed.\n\t\tif pkg, ok := expr.PkgFunc.Get(); ok && expr.DeclaredIn().TestFile && pkg.PkgPath == \"encore.dev/et\" && pkg.Name == \"MockEndpoint\" {\n\t\t\treturn &ReferenceUsage{\n\t\t\t\tBase: usage.Base{\n\t\t\t\t\tFile: expr.File,\n\t\t\t\t\tBind: expr.Bind,\n\t\t\t\t\tExpr: expr,\n\t\t\t\t},\n\t\t\t\tEndpoint: ep,\n\t\t\t\tRef:      expr.ASTExpr(),\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check if the resource is referenced in a permissible location.\n\t// Walk the stack to find the\n\n\tdata.Errs.Add(ErrInvalidEndpointUsage.AtGoNode(data.Expr))\n\treturn nil\n}\n"
  },
  {
    "path": "v2/parser/apis/authhandler/authhandler.go",
    "content": "package authhandler\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\n// AuthHandler describes an Encore authentication handler.\ntype AuthHandler struct {\n\tDecl *schema.FuncDecl\n\tDoc  string\n\tName string // the name of the auth handler.\n\n\t// Param is the auth parameters.\n\t// It's either a builtin string for token-based authentication,\n\t// or a named struct type for complex auth parameters.\n\tParam schema.Type\n\n\t// Recv is the type the auth handler is defined as a method on, if any.\n\tRecv option.Option[*schema.Receiver]\n\n\t// AuthData is the custom auth data type the app specifies\n\t// as part of the returns from the auth handler, if any.\n\tAuthData option.Option[*schema.TypeDeclRef]\n}\n\nfunc (ah *AuthHandler) Kind() resource.Kind       { return resource.AuthHandler }\nfunc (ah *AuthHandler) Package() *pkginfo.Package { return ah.Decl.File.Pkg }\nfunc (ah *AuthHandler) Pos() token.Pos            { return ah.Decl.AST.Pos() }\nfunc (ah *AuthHandler) End() token.Pos            { return ah.Decl.AST.End() }\nfunc (ah *AuthHandler) SortKey() string           { return ah.Decl.File.Pkg.ImportPath.String() + \".\" + ah.Name }\n\ntype ParseData struct {\n\tErrs   *perr.List\n\tSchema *schema.Parser\n\n\tFile *pkginfo.File\n\tFunc *ast.FuncDecl\n\tDir  *directive.Directive\n\tDoc  string\n}\n\n// Parse parses the auth handler in the provided declaration.\n// It may return nil on errors.\nfunc Parse(d ParseData) *AuthHandler {\n\tdecl, ok := d.Schema.ParseFuncDecl(d.File, d.Func)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tah := &AuthHandler{\n\t\tDecl: decl,\n\t\tName: decl.Name,\n\t\tDoc:  d.Doc,\n\t\tRecv: decl.Recv,\n\t}\n\n\tsig := decl.Type\n\tnumParams := len(sig.Params)\n\n\t// Validate the input\n\tif numParams != 2 {\n\t\td.Errs.Add(errInvalidNumberParameters(numParams).AtGoNode(sig.AST.Params))\n\n\t\tif numParams < 2 {\n\t\t\treturn ah\n\t\t}\n\t}\n\n\tnumResults := len(sig.Results)\n\tif numResults < 2 || numResults > 3 {\n\t\td.Errs.Add(errInvalidNumberResults(numResults).AtGoNode(sig.AST.Results))\n\n\t\tif numParams < 2 {\n\t\t\treturn ah\n\t\t}\n\t}\n\n\t// First param should always be context.Context\n\tctxParam := sig.Params[0]\n\tif !schemautil.IsNamed(ctxParam.Type, \"context\", \"Context\") {\n\t\td.Errs.Add(errInvalidFirstParameter.AtGoNode(ctxParam.AST))\n\t}\n\n\tah.Param = sig.Params[1].Type\n\n\t// Second param should be string, or a pointer to a named struct\n\t{\n\t\tparam := ah.Param\n\t\tif schemautil.IsBuiltinKind(param, schema.String) {\n\t\t\t// All good\n\t\t} else if ref, ok := schemautil.ResolveNamedStruct(param, true); !ok {\n\t\t\td.Errs.Add(ErrInvalidAuthSchemaType.AtGoNode(param.ASTExpr()))\n\t\t} else {\n\t\t\tvalidateStructFields(d.Errs, ref.Decl.Type.(schema.StructType))\n\t\t}\n\t}\n\n\t// First result must be auth.UID\n\tif uid := sig.Results[0]; !schemautil.IsBuiltinKind(uid.Type, schema.UserID) {\n\t\td.Errs.Add(errInvalidFirstResult.AtGoNode(uid.AST.Type))\n\t}\n\n\t// If we have three results, the second one must be a pointer to a named struct.\n\tif numResults > 2 {\n\t\tif ref, ok := schemautil.ResolveNamedStruct(sig.Results[1].Type, true); !ok {\n\t\t\td.Errs.Add(errInvalidSecondResult.AtGoNode(sig.Results[1].AST.Type))\n\t\t} else {\n\t\t\tah.AuthData = option.Some(ref)\n\t\t}\n\t}\n\n\treturn ah\n}\n\n// validateStructFields checks that the struct fields have the correct tags.\n// and all fields are either a header or query string\nfunc validateStructFields(errs *perr.List, ref schema.StructType) {\n\tfieldErr := ErrInvalidFieldTags\n\nnextField:\n\tfor _, field := range ref.Fields {\n\t\t// No tags, then we tell them they need to specify it\n\t\tif field.Tag.Len() == 0 {\n\t\t\tfieldErr = fieldErr.AtGoNode(field.AST, errors.AsError(\"you must specify a \\\"header\\\", \\\"query\\\", or \\\"cookie\\\" tag for this field\"))\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check each tag to see if we have a header or query tag\n\t\tfor _, tagKey := range field.Tag.Keys() {\n\t\t\terrMsg := \"\"\n\n\t\t\ttag, err := field.Tag.Get(tagKey)\n\t\t\tif err == nil {\n\t\t\t\tswitch tagKey {\n\t\t\t\tcase \"header\", \"query\", \"qs\", \"cookie\":\n\t\t\t\t\tif tag.Name != \"\" && tag.Name != \"-\" {\n\t\t\t\t\t\tcontinue nextField\n\t\t\t\t\t} else {\n\t\t\t\t\t\terrMsg = \"you must specify a name for this field\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfieldErr = fieldErr.AtGoNode(field.AST.Tag, errors.AsError(errMsg))\n\t\t}\n\t}\n\n\tif len(fieldErr.Locations) > 0 {\n\t\terrs.Add(fieldErr)\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/authhandler/authhandler_test.go",
    "content": "package authhandler\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t. \"encr.dev/v2/internals/schema/schematest\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n)\n\nfunc TestParseAuthHandler(t *testing.T) {\n\ttype testCase struct {\n\t\tname     string\n\t\timports  []string\n\t\tdef      string\n\t\twant     *AuthHandler\n\t\twantErrs []string\n\t}\n\n\tctxParam := Param(Named(TypeInfo(\"Context\")))\n\tuidResult := Param(Builtin(schema.UserID))\n\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"basic_legacy\",\n\t\t\tdef: `\n//encore:authhandler\nfunc Foo(ctx context.Context, token string) (auth.UID, error) {}\n`,\n\t\t\twant: &AuthHandler{\n\t\t\t\tDecl: &schema.FuncDecl{\n\t\t\t\t\tName: \"Foo\",\n\t\t\t\t\tType: schema.FuncType{\n\t\t\t\t\t\tParams: []schema.Param{\n\t\t\t\t\t\t\tctxParam,\n\t\t\t\t\t\t\tParam(String()),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tResults: []schema.Param{\n\t\t\t\t\t\t\tuidResult,\n\t\t\t\t\t\t\tParam(Error()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tParam: String(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"struct_params\",\n\t\t\tdef: `\ntype Params struct{}\n//encore:authhandler\nfunc Foo(ctx context.Context, p *Params) (auth.UID, error) {}\n`,\n\t\t\twant: &AuthHandler{\n\t\t\t\tDecl: &schema.FuncDecl{\n\t\t\t\t\tName: \"Foo\",\n\t\t\t\t\tType: schema.FuncType{\n\t\t\t\t\t\tParams: []schema.Param{\n\t\t\t\t\t\t\tctxParam,\n\t\t\t\t\t\t\tParam(Ptr(Named(TypeInfo(\"Params\")))),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tResults: []schema.Param{\n\t\t\t\t\t\t\tuidResult,\n\t\t\t\t\t\t\tParam(Error()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tParam: Ptr(Named(TypeInfo(\"Params\"))),\n\t\t\t},\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timportList := append([]string{\"context\", \"encore.dev/beta/auth\"}, test.imports...)\n\t\timports := \"\"\n\t\tif len(importList) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range importList {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.def + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tschemaParser := schema.NewParser(tc.Context, l)\n\n\t\t\tif len(test.wantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.wantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tf := pkg.Files[0]\n\t\t\tfd := testutil.FindNodes[*ast.FuncDecl](f.AST())[0]\n\n\t\t\t// Parse the directive from the func declaration.\n\t\t\tdir, doc, ok := directive.Parse(tc.Errs, fd.Doc)\n\t\t\tc.Assert(ok, qt.IsTrue)\n\n\t\t\tpd := ParseData{\n\t\t\t\tErrs:   tc.Errs,\n\t\t\t\tSchema: schemaParser,\n\t\t\t\tFile:   f,\n\t\t\t\tFunc:   fd,\n\t\t\t\tDir:    dir,\n\t\t\t\tDoc:    doc,\n\t\t\t}\n\n\t\t\tgot := Parse(pd)\n\n\t\t\tif len(test.wantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\tcmpEqual := qt.CmpEquals(\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Node }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&schema.FuncDecl{}, &schema.TypeDecl{}, &pkginfo.File{}, &pkginfo.Package{}, token.Pos(0)),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(schema.StructField{}, schema.NamedType{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\tc.Assert(got, cmpEqual, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/authhandler/errors.go",
    "content": "package authhandler\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nconst authLink = \"For more information on auth handlers and how to define them, see https://encore.dev/docs/develop/auth\"\n\nvar (\n\terrRange = errors.Range(\n\t\t\"authhandler\",\n\t\t`hint: valid signatures are:\n\t- func(ctx context.Context, p *Params) (auth.UID, error)\n\t- func(ctx context.Context, p *Params) (auth.UID, *UserData, error)\n\t- func(ctx context.Context, token string) (auth.UID, error)\n\t- func(ctx context.Context, token string) (auth.UID, *UserData, error)\n\nnote: *Params and *UserData are custom data types you define\n\n`+authLink,\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrInvalidNumberParameters = errRange.Newf(\n\t\t\"Invalid auth handler Signature\",\n\t\t\"The auth handler must have exactly 2 parameters, found %d.\",\n\t)\n\n\terrInvalidNumberResults = errRange.Newf(\n\t\t\"Invalid auth handler Signature\",\n\t\t\"The auth handler must have 2 or 3 result parameters, found %d.\",\n\t)\n\n\terrInvalidFirstParameter = errRange.New(\n\t\t\"Invalid auth handler Signature\",\n\t\t\"The first parameter must be a context.Context.\",\n\t)\n\n\tErrInvalidAuthSchemaType = errRange.New(\n\t\t\"Invalid auth handler Signature\",\n\t\t\"The second parameter must be a string or a pointer to a named struct.\",\n\t)\n\n\terrInvalidFirstResult = errRange.New(\n\t\t\"Invalid auth handler Signature\",\n\t\t\"The first result must be of type auth.UID.\",\n\t)\n\n\terrInvalidSecondResult = errRange.New(\n\t\t\"Invalid auth handler Signature\",\n\t\t\"The second result must be a pointer to a named struct.\",\n\t)\n\n\terrCannotCallFromAnotherPackage = errRange.New(\n\t\t\"Invalid auth handler usage\",\n\t\t\"You can not directly call an auth handler from another package.\",\n\t\terrors.WithDetails(authLink),\n\t)\n\n\terrInvalidReference = errRange.New(\n\t\t\"Invalid auth handler usage\",\n\t\t\"Auth handlers can only be called, but not referenced.\",\n\t\terrors.WithDetails(authLink),\n\t)\n\n\tErrInvalidFieldTags = errRange.New(\n\t\t\"Invalid auth payload\",\n\t\t\"All fields used within an auth payload must originate from either an HTTP header, a query parameter, or a cookie.\",\n\n\t\terrors.WithDetails(\n\t\t\t\"You can specify them for each field using the struct tags, for example with `header:\\\"X-My-Header\\\"`, `query:\\\"my-query\\\", or `cookie:\\\"my-cookie\\\"`.\\n\\n\"+\n\t\t\t\tauthLink,\n\t\t),\n\t)\n\n\tErrMultipleAuthHandlers = errRange.New(\n\t\t\"Multiple auth handlers found\",\n\t\t\"Multiple auth handlers were found in the application. Encore only allows one auth handler to be defined per application.\",\n\t)\n\n\tErrNoAuthHandlerDefined = errRange.New(\n\t\t\"No Auth Handler Defined\",\n\t\t\"An auth handler must be defined to use the auth directive on an API.\",\n\n\t\terrors.WithDetails(\n\t\t\t\"You can specify them for each field using the struct tags, for example with `header:\\\"X-My-Header\\\"` or `query:\\\"my-query\\\"`.\\n\\n\"+\n\t\t\t\tauthLink,\n\t\t),\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/authhandler/usage.go",
    "content": "package authhandler\n\nimport (\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\nfunc ResolveAuthHandlerUsage(data usage.ResolveData, handler *AuthHandler) usage.Usage {\n\tswitch expr := data.Expr.(type) {\n\tcase *usage.FuncCall:\n\t\tif expr.DeclaredIn().Pkg != handler.Package() {\n\t\t\tdata.Errs.Add(\n\t\t\t\terrCannotCallFromAnotherPackage.\n\t\t\t\t\tAtGoNode(expr, errors.AsError(\"called here\")).\n\t\t\t\t\tAtGoNode(handler.Decl.AST.Name, errors.AsHelp(\"auth handler defined here\")),\n\t\t\t)\n\t\t}\n\tdefault:\n\t\tdata.Errs.Add(\n\t\t\terrInvalidReference.\n\t\t\t\tAtGoNode(expr, errors.AsError(\"referenced here\")).\n\t\t\t\tAtGoNode(handler.Decl.AST.Name, errors.AsHelp(\"auth handler defined here\")),\n\t\t)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "v2/parser/apis/directive/directive.go",
    "content": "package directive\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\n// Directive represents a parsed \"encore:\" directive.\ntype Directive struct {\n\tAST *ast.CommentGroup // the comment group containing the directive\n\n\tName    string  // \"foo\" in \"encore:foo\"\n\tOptions []Field // options that are enabled (\"public\" and \"raw\" in \"encore:api public raw path=/foo\")\n\tFields  []Field // key-value pairs ({\"path\": \"/foo\"} in \"encore:api public raw path=/foo\")\n\tTags    []Field // tag names (\"tag:foo\" in \"encore:api public tag:foo\")\n\n\tstart   token.Pos // start position of the directive\n\tnameEnd token.Pos\n\tend     token.Pos // end position of the directive\n}\n\nvar _ ast.Node = (*Directive)(nil)\n\nfunc (d Directive) Pos() token.Pos {\n\treturn d.start\n}\n\nfunc (d Directive) End() token.Pos {\n\treturn d.end\n}\n\ntype Field struct {\n\tKey   string\n\tValue string\n\n\tstart token.Pos // position of the key\n\tend   token.Pos // position of the end value\n}\n\n// Equal reports whether two fields are equal.\n// It's implemented for testing purposes.\nfunc (f Field) Equal(other Field) bool {\n\treturn f.Key == other.Key && f.Value == other.Value\n}\n\nvar _ ast.Node = (*Field)(nil)\n\nfunc (f Field) Pos() token.Pos {\n\treturn f.start\n}\n\nfunc (f Field) End() token.Pos {\n\treturn f.end\n}\n\n// List returns the field value as a list, split by commas.\nfunc (f Field) List() []string {\n\treturn strings.Split(f.Value, \",\")\n}\n\n// String returns the string representation of d.\nfunc (d Directive) String() string {\n\tvar b strings.Builder\n\tb.WriteString(\"encore:\")\n\tb.WriteString(d.Name)\n\tfor _, o := range d.Options {\n\t\tb.WriteByte(' ')\n\t\tb.WriteString(o.Value)\n\t}\n\tfor _, f := range d.Fields {\n\t\tb.WriteByte(' ')\n\t\tb.WriteString(f.Key)\n\t\tb.WriteByte('=')\n\t\tb.WriteString(f.Value)\n\t}\n\tfor _, t := range d.Tags {\n\t\tb.WriteByte(' ')\n\t\tb.WriteString(t.Value)\n\t}\n\treturn b.String()\n}\n\n// HasOption reports whether the directive contains the given option.\nfunc (d Directive) HasOption(name string) bool {\n\tfor _, o := range d.Options {\n\t\tif o.Value == name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Get returns the value of the given field, if any.\n// If the field doesn't exist it reports \"\".\nfunc (d Directive) Get(name string) string {\n\tfor _, f := range d.Fields {\n\t\tif f.Key == name {\n\t\t\treturn f.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// GetList returns the value of the given field, split by commas.\n// If the field doesn't exist it reports nil.\nfunc (d Directive) GetList(name string) []string {\n\tfor _, f := range d.Fields {\n\t\tif f.Key == name {\n\t\t\treturn f.List()\n\t\t}\n\t}\n\treturn nil\n}\n\n// Parse parses the encore:foo directives in cg.\n// It returns the parsed directives, if any, and the\n// remaining doc text after stripping the directive lines.\nfunc Parse(errs *perr.List, cg *ast.CommentGroup) (dir *Directive, doc string, ok bool) {\n\tif cg == nil {\n\t\treturn nil, \"\", true\n\t}\n\n\t// Go has standardized on directives in the form \"//[a-z0-9]+:[a-z0-9+]\".\n\t// Encore has allowed a space between // and the Directive,\n\t// but we would like to migrate to the standard syntax.\n\t//\n\t// First try the standard syntax and fall back to the legacy syntax\n\t// if we don't find any directives.\n\n\t// Standard syntax\n\tvar dirs []*Directive\n\tfor _, c := range cg.List {\n\t\tconst prefix = \"//encore:\"\n\t\tif strings.HasPrefix(c.Text, prefix) {\n\t\t\tdir, ok := parseOne(errs, c.Pos()+2, c.Text[len(prefix):])\n\t\t\tif !ok {\n\t\t\t\treturn nil, \"\", false\n\t\t\t}\n\t\t\tdir.AST = cg\n\t\t\tdirs = append(dirs, &dir)\n\t\t}\n\t}\n\tif len(dirs) == 1 {\n\t\tdoc := cg.Text() // skips directives for us\n\t\treturn dirs[0], doc, true\n\t} else if len(dirs) > 1 {\n\t\terr := errMultipleDirectives\n\t\tfor _, dir := range dirs {\n\t\t\terr = err.AtGoNode(dir)\n\t\t}\n\t\terrs.Add(err)\n\t\treturn nil, \"\", false\n\t}\n\n\t// Legacy syntax\n\tlines := strings.Split(cg.Text(), \"\\n\")\n\tvar docLines []string\n\n\tfor _, line := range lines {\n\t\tconst prefix = \"encore:\"\n\t\tif strings.HasPrefix(line, prefix) {\n\t\t\tpos := cg.Pos()\n\n\t\t\t// Find the position of the directive.\n\t\t\tfor _, c := range cg.List {\n\t\t\t\tidx := bytes.Index([]byte(c.Text), []byte(line))\n\t\t\t\tif idx >= 0 {\n\t\t\t\t\tpos = c.Pos() + token.Pos(idx)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdir, ok := parseOne(errs, pos, line[len(prefix):])\n\t\t\tif !ok {\n\t\t\t\treturn nil, \"\", false\n\t\t\t}\n\t\t\tdir.AST = cg\n\t\t\tdirs = append(dirs, &dir)\n\t\t\tcontinue\n\t\t}\n\t\tdocLines = append(docLines, line)\n\t}\n\n\tif len(dirs) == 0 {\n\t\treturn nil, cg.Text(), true\n\t} else if len(dirs) > 1 {\n\t\terr := errMultipleDirectives\n\t\tfor _, dir := range dirs {\n\t\t\terr = err.AtGoNode(dir)\n\t\t}\n\t\terrs.Add(err)\n\t\treturn nil, \"\", false\n\t}\n\tdoc = strings.TrimSpace(strings.Join(docLines, \"\\n\"))\n\treturn dirs[0], doc, true\n}\n\nvar (\n\t// nameRe is the regexp for validating option names and field names.\n\tnameRe = regexp.MustCompile(`^[a-z]+$`)\n\t// tagRe is the regexp for validating tag values.\n\ttagRe = regexp.MustCompile(`^[a-z]([-_a-z0-9]*[a-z0-9])?$`)\n)\n\n// parseOne parses a single Directive from line.\n// It does not set Directive.AST.\nfunc parseOne(errs *perr.List, pos token.Pos, line string) (d Directive, ok bool) {\n\tfields := fields(pos+7, line) // +7 for \"encore:\"\n\tif len(fields) == 0 {\n\t\terrs.Add(errMissingDirectiveName.AtGoPos(pos, pos+7+token.Pos(len([]byte(line)))))\n\t\treturn Directive{}, false\n\t}\n\n\t// seen tracks fields already seen, to detect duplicates.\n\tseen := make(map[string]Field, len(fields))\n\n\td.Name = fields[0].Value\n\td.start = pos\n\td.nameEnd = pos + 7 + token.Pos(len([]byte(d.Name)))\n\td.end = pos + +7 + token.Pos(len([]byte(line)))\n\n\tfor _, f := range fields[1:] {\n\t\t// seenKey is the key to use for detecting duplicates.\n\t\t// Default it to the field itself.\n\t\tseenKey := f.Value\n\n\t\tif strings.HasPrefix(f.Value, \"tag:\") {\n\t\t\ttag := f.Value[len(\"tag:\"):]\n\n\t\t\tif other, found := seen[seenKey]; found {\n\t\t\t\terrs.Add(errDuplicateTag(seenKey).AtGoNode(other).AtGoNode(f))\n\t\t\t\treturn Directive{}, false\n\t\t\t} else if !tagRe.MatchString(tag) {\n\t\t\t\terrs.Add(errInvalidTag(tag).\n\t\t\t\t\tAtGoPos(f.start+4, f.end, errors.AsError(\n\t\t\t\t\t\tfmt.Sprintf(\"try %q?\", idents.GenerateSuggestion(tag, idents.KebabCase)),\n\t\t\t\t\t)))\n\t\t\t\treturn Directive{}, false\n\t\t\t}\n\n\t\t\td.Tags = append(d.Tags, f)\n\t\t} else if key, value, ok := strings.Cut(f.Value, \"=\"); ok {\n\t\t\tseenKey = key\n\t\t\tf.Key = key\n\t\t\tf.Value = value\n\n\t\t\tif value == \"\" {\n\t\t\t\terrs.Add(errFieldHasNoValue.AtGoNode(f))\n\t\t\t\treturn Directive{}, false\n\t\t\t} else if !nameRe.MatchString(key) {\n\t\t\t\terrs.Add(errInvalidFieldName(key).AtGoPos(f.start, f.start+token.Pos(len([]byte(key)))))\n\t\t\t\treturn Directive{}, false\n\t\t\t} else if other, found := seen[seenKey]; found {\n\t\t\t\terrs.Add(errDuplicateField(seenKey).AtGoNode(f).AtGoNode(other))\n\t\t\t\treturn Directive{}, false\n\t\t\t}\n\t\t\td.Fields = append(d.Fields, f)\n\t\t} else {\n\t\t\tif !nameRe.MatchString(f.Value) {\n\t\t\t\terrs.Add(errInvalidOptionName(f.Value).AtGoNode(f))\n\t\t\t\treturn Directive{}, false\n\t\t\t} else if other, found := seen[seenKey]; found {\n\t\t\t\terrs.Add(errDuplicateOption(seenKey).AtGoNode(f).AtGoNode(other))\n\t\t\t\treturn Directive{}, false\n\t\t\t}\n\t\t\td.Options = append(d.Options, f)\n\t\t}\n\n\t\tseen[seenKey] = f\n\t}\n\n\treturn d, true\n}\n\ntype ValidateSpec struct {\n\t// AllowedFields and AllowedOptions are the allowed fields and options.\n\t// Expected values must be found in the corresponding list.\n\tAllowedOptions []string\n\tAllowedFields  []string\n\n\t// ValidateOption, if non-nil, is called for each option in the directive.\n\tValidateOption func(*perr.List, Field) (ok bool)\n\n\t// ValidateField, if non-nil, is called for each field in the directive.\n\tValidateField func(*perr.List, Field) (ok bool)\n\n\t// ValidateTag, if non-nil, is called for each tag in the directive.\n\t// It is called with the whole tag, including the \"tag:\" prefix.\n\tValidateTag func(*perr.List, Field) (ok bool)\n}\n\n// Validate checks that the directive is valid according to spec.\nfunc Validate(errs *perr.List, d *Directive, spec ValidateSpec) (ok bool) {\n\t// Check the options.\n\tfor _, o := range d.Options {\n\t\tif !slices.Contains(spec.AllowedOptions, o.Value) {\n\t\t\terrs.Add(errUnknownOption(o.Value, strings.Join(spec.AllowedOptions, \", \")).AtGoNode(o))\n\t\t\treturn false\n\t\t}\n\t\tif spec.ValidateOption != nil {\n\t\t\tif !spec.ValidateOption(errs, o) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, f := range d.Fields {\n\t\tif !slices.Contains(spec.AllowedFields, f.Key) {\n\t\t\terrs.Add(errUnknownField(f.Key, strings.Join(spec.AllowedFields, \", \")).AtGoNode(f))\n\t\t\treturn false\n\t\t}\n\t\tif spec.ValidateField != nil {\n\t\t\tif !spec.ValidateField(errs, f) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\tfor _, t := range d.Tags {\n\t\tif spec.ValidateTag != nil {\n\t\t\tif !spec.ValidateTag(errs, t) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "v2/parser/apis/directive/directive_test.go",
    "content": "package directive\n\nimport (\n\t\"context\"\n\t\"go/token\"\n\t\"regexp\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\n\t\"encr.dev/v2/internals/perr\"\n)\n\nfunc TestParseDirective(t *testing.T) {\n\ttestcases := []struct {\n\t\tdesc     string\n\t\tline     string\n\t\texpected Directive\n\t\twantErr  string\n\t}{\n\t\t{\n\t\t\tdesc: \"api public endpoint\",\n\t\t\tline: \"api public\",\n\t\t\texpected: Directive{\n\t\t\t\tName:    \"api\",\n\t\t\t\tOptions: []Field{{Value: \"public\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"custom method\",\n\t\t\tline: \"api public method=FOO\",\n\t\t\texpected: Directive{\n\t\t\t\tName:    \"api\",\n\t\t\t\tOptions: []Field{{Value: \"public\"}},\n\t\t\t\tFields:  []Field{{Key: \"method\", Value: \"FOO\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"multiple methods\",\n\t\t\tline: \"api public raw method=GET,POST\",\n\t\t\texpected: Directive{\n\t\t\t\tName:    \"api\",\n\t\t\t\tOptions: []Field{{Value: \"public\"}, {Value: \"raw\"}},\n\t\t\t\tFields:  []Field{{Key: \"method\", Value: \"GET,POST\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"api with tags\",\n\t\t\tline: \"api public tag:foo method=FOO raw tag:bar\",\n\t\t\texpected: Directive{\n\t\t\t\tName:    \"api\",\n\t\t\t\tOptions: []Field{{Value: \"public\"}, {Value: \"raw\"}},\n\t\t\t\tFields:  []Field{{Key: \"method\", Value: \"FOO\"}},\n\t\t\t\tTags:    []Field{{Value: \"tag:foo\"}, {Value: \"tag:bar\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"api with duplicate tag\",\n\t\t\tline:    \"api public tag:foo tag:foo\",\n\t\t\twantErr: `(?m)The tag \"tag:foo\" is already defined on this declaration\\.`,\n\t\t},\n\t\t{\n\t\t\tdesc: \"middleware\",\n\t\t\tline: \"middleware target=tag:foo,tag:bar\",\n\t\t\texpected: Directive{\n\t\t\t\tName:   \"middleware\",\n\t\t\t\tFields: []Field{{Key: \"target\", Value: \"tag:foo,tag:bar\"}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc:    \"middleware empty target\",\n\t\t\tline:    \"middleware target=\",\n\t\t\twantErr: `(?m)Directive fields must have a value\\.`,\n\t\t},\n\t}\n\tfor _, tc := range testcases {\n\t\tt.Run(tc.desc, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\tfs := token.NewFileSet()\n\t\t\terrs := perr.NewList(context.Background(), fs)\n\t\t\tdir, ok := parseOne(errs, 0, tc.line)\n\t\t\tif tc.wantErr != \"\" {\n\t\t\t\tre := regexp.MustCompile(tc.wantErr)\n\t\t\t\tif errStr := errs.FormatErrors(); !re.MatchString(errStr) {\n\t\t\t\t\tc.Fatalf(\"error did not match regexp %s: %s\", tc.wantErr, errStr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tc.Assert(ok, qt.IsTrue)\n\n\t\t\t\tcmp := qt.CmpEquals(\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(Field{}),\n\t\t\t\t\tcmpopts.IgnoreUnexported(Directive{}),\n\t\t\t\t)\n\t\t\t\tc.Assert(dir, cmp, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/directive/errors.go",
    "content": "package directive\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"directive\",\n\t\t\"\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrMultipleDirectives = errRange.New(\n\t\t\"Multiple Directives\",\n\t\t\"Multiple directives are not allowed on the same declaration.\",\n\t)\n\n\terrMissingDirectiveName = errRange.New(\n\t\t\"Invalid Encore Directive\",\n\t\t\"Directives must have a name. For example, `//encore:api` is valid, but `//encore:` is not.\",\n\t)\n\n\terrDuplicateTag = errRange.Newf(\n\t\t\"Duplicate Tag\",\n\t\t\"The tag %q is already defined on this declaration. Tags must be unique per directive.\",\n\t)\n\n\terrInvalidTag = errRange.Newf(\n\t\t\"Invalid Tag\",\n\t\t\"Invalid tag %q. Tags must start with a letter and contain only letters, numbers, hyphens and underscores.\",\n\t)\n\n\terrFieldHasNoValue = errRange.New(\n\t\t\"Invalid Directive Field\",\n\t\t\"Directive fields must have a value. For example, `//encore:api foo=bar` is valid, but `//encore:api foo=` is not.\",\n\t)\n\n\terrInvalidFieldName = errRange.Newf(\n\t\t\"Invalid Directive Field\",\n\t\t\"Invalid field name %q. Field names must start only contain letters.\",\n\t)\n\n\terrDuplicateField = errRange.Newf(\n\t\t\"Duplicate Directive Field\",\n\t\t\"The field %q is already defined on this directive. Fields must be unique per directive.\",\n\t)\n\n\terrUnknownField = errRange.Newf(\n\t\t\"Invalid Directive Field\",\n\t\t\"Unknown field %q. Fields must be one of %s.\",\n\t)\n\n\terrInvalidOptionName = errRange.Newf(\n\t\t\"Invalid Directive Option\",\n\t\t\"Invalid option name %q. Options must start only contain letters.\",\n\t)\n\n\terrDuplicateOption = errRange.Newf(\n\t\t\"Duplicate Directive Option\",\n\t\t\"The option %q is already defined on this directive. Options must be unique per directive.\",\n\t)\n\n\terrUnknownOption = errRange.Newf(\n\t\t\"Invalid Directive Option\",\n\t\t\"Unknown option %q. Options must be one of %s.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/directive/fields.go",
    "content": "package directive\n\nimport (\n\t\"go/token\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\nvar asciiSpace = [256]uint8{'\\t': 1, '\\n': 1, '\\v': 1, '\\f': 1, '\\r': 1, ' ': 1}\n\n// Fields splits the string s around each instance of one or more consecutive white space\n// characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an\n// empty slice if s contains only white space.\nfunc fields(startPos token.Pos, s string) []Field {\n\t// First count the fields.\n\t// This is an exact count if s is ASCII, otherwise it is an approximation.\n\tn := 0\n\twasSpace := 1\n\t// setBits is used to track which bits are set in the bytes of s.\n\tsetBits := uint8(0)\n\tfor i := 0; i < len(s); i++ {\n\t\tr := s[i]\n\t\tsetBits |= r\n\t\tisSpace := int(asciiSpace[r])\n\t\tn += wasSpace & ^isSpace\n\t\twasSpace = isSpace\n\t}\n\n\tif setBits >= utf8.RuneSelf {\n\t\t// Some runes in the input string are not ASCII.\n\t\treturn fieldsFunc(startPos, s, unicode.IsSpace)\n\t}\n\t// ASCII fast path\n\ta := make([]Field, n)\n\tna := 0\n\tfieldStart := 0\n\ti := 0\n\t// Skip spaces in the front of the input.\n\tfor i < len(s) && asciiSpace[s[i]] != 0 {\n\t\ti++\n\t}\n\tfieldStart = i\n\tfor i < len(s) {\n\t\tif asciiSpace[s[i]] == 0 {\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\ta[na] = Field{\n\t\t\tValue: s[fieldStart:i],\n\t\t\tstart: startPos + token.Pos(fieldStart),\n\t\t\tend:   startPos + token.Pos(i),\n\t\t}\n\t\tna++\n\t\ti++\n\t\t// Skip spaces in between fields.\n\t\tfor i < len(s) && asciiSpace[s[i]] != 0 {\n\t\t\ti++\n\t\t}\n\t\tfieldStart = i\n\t}\n\tif fieldStart < len(s) { // Last field might end at EOF.\n\t\ta[na] = Field{\n\t\t\tValue: s[fieldStart:],\n\t\t\tstart: startPos + token.Pos(fieldStart),\n\t\t\tend:   startPos + token.Pos(len(s)),\n\t\t}\n\t}\n\treturn a\n}\n\n// FieldsFunc splits the string s at each run of Unicode code points c satisfying f(c)\n// and returns an array of slices of s. If all code points in s satisfy f(c) or the\n// string is empty, an empty slice is returned.\n//\n// FieldsFunc makes no guarantees about the order in which it calls f(c)\n// and assumes that f always returns the same value for a given c.\nfunc fieldsFunc(startPos token.Pos, s string, f func(rune) bool) []Field {\n\t// A span is used to record a slice of s of the form s[start:end].\n\t// The start index is inclusive and the end index is exclusive.\n\ttype span struct {\n\t\tstart int\n\t\tend   int\n\t}\n\tspans := make([]span, 0, 32)\n\n\t// Find the field start and end indices.\n\t// Doing this in a separate pass (rather than slicing the string s\n\t// and collecting the result substrings right away) is significantly\n\t// more efficient, possibly due to cache effects.\n\tstart := -1 // valid span start if >= 0\n\tfor end, rune := range s {\n\t\tif f(rune) {\n\t\t\tif start >= 0 {\n\t\t\t\tspans = append(spans, span{start, end})\n\t\t\t\t// Set start to a negative value.\n\t\t\t\t// Note: using -1 here consistently and reproducibly\n\t\t\t\t// slows down this code by a several percent on amd64.\n\t\t\t\tstart = ^start\n\t\t\t}\n\t\t} else {\n\t\t\tif start < 0 {\n\t\t\t\tstart = end\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last field might end at EOF.\n\tif start >= 0 {\n\t\tspans = append(spans, span{start, len(s)})\n\t}\n\n\t// Create strings from recorded field indices.\n\ta := make([]Field, len(spans))\n\tfor i, span := range spans {\n\t\ta[i] = Field{\n\t\t\tValue: s[span.start:span.end],\n\t\t\tstart: startPos + token.Pos(len([]byte(s[:span.start]))),\n\t\t}\n\t\ta[i].end = a[i].start + token.Pos(len([]byte(a[i].Value)))\n\t}\n\n\treturn a\n}\n"
  },
  {
    "path": "v2/parser/apis/errors.go",
    "content": "package apis\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"parser/apis\",\n\t\t\"\",\n\t)\n\n\terrUnexpectedDirective = errRange.Newf(\n\t\t\"Invalid directive\",\n\t\t\"Unexpected directive %q on function declaration.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/middleware/errors.go",
    "content": "package middleware\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"middleware\",\n\t\t\"hint: middleware must have the signature:\\n\\t\"+\n\t\t\t\"func(req middleware.Request, next middleware.Next) middleware.Response\\n\\n\"+\n\t\t\t\"For more information on how to use middleware, see https://encore.dev/docs/develop/middleware\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrInvalidSelectorType = errRange.Newf(\n\t\t\"Invalid middleware selector\",\n\t\t\"Middleware target only supports tags a selectors (got '%s').\",\n\t)\n\n\terrWrongNumberParams = errRange.Newf(\n\t\t\"Invalid middleware function\",\n\t\t\"Middleware functions must have exactly 2 parameters, found %d parameters.\",\n\t)\n\n\terrWrongNumberResults = errRange.Newf(\n\t\t\"Invalid middleware function\",\n\t\t\"Middleware functions must have exactly 1 result, found %d results.\",\n\t)\n\n\terrInvalidFirstParam = errRange.New(\n\t\t\"Invalid middleware function\",\n\t\t\"The first parameter of a middleware function must be middleware.Request.\",\n\t)\n\n\terrInvalidSecondParam = errRange.New(\n\t\t\"Invalid middleware function\",\n\t\t\"The second parameter of a middleware function must be middleware.Next.\",\n\t)\n\n\terrInvalidReturnType = errRange.New(\n\t\t\"Invalid middleware function\",\n\t\t\"The return type of a middleware function must be middleware.Response.\",\n\t)\n\n\tErrInvalidTargetForService = errRange.Newf(\n\t\t\"Invalid middleware target\",\n\t\t\"There are not matching targets in service %q.\",\n\t)\n\n\tErrInvalidTargetForApp = errRange.New(\n\t\t\"Invalid middleware target\",\n\t\t\"There are not matching targets in the application.\",\n\t)\n\n\tErrSvcMiddlewareNotInService = errRange.New(\n\t\t\"Invalid middleware function\",\n\t\t\"Middleware must be defined in a service unless it is marked as being global.\",\n\t)\n\n\tErrGlobalMiddlewareDefinedInService = errRange.New(\n\t\t\"Invalid middleware function\",\n\t\t\"Global middleware cannot be defined in a service.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/middleware/middleware.go",
    "content": "package middleware\n\nimport (\n\t\"cmp\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"slices\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/apis/selector\"\n\t\"encr.dev/v2/parser/internal/utils\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\n// Middleware describes an Encore middleware.\ntype Middleware struct {\n\tDecl *schema.FuncDecl\n\tDoc  string\n\tFile *pkginfo.File // file it's declared in\n\n\t// Global specifies whether the middleware is global or service-local.\n\tGlobal bool\n\n\t// Target specifies the set of API endpoints the middleware applies to.\n\tTarget selector.Set\n\n\t// Recv is the type the middleware is defined as a method on, if any.\n\tRecv option.Option[*schema.Receiver]\n}\n\n// ID returns a unique id for this specific middleware.\nfunc (mw *Middleware) ID() string {\n\treturn mw.Decl.File.Pkg.ImportPath.String() + \".\" + mw.Decl.Name\n}\n\nfunc (mw *Middleware) Kind() resource.Kind       { return resource.Middleware }\nfunc (mw *Middleware) Package() *pkginfo.Package { return mw.File.Pkg }\nfunc (mw *Middleware) Pos() token.Pos            { return mw.Decl.AST.Pos() }\nfunc (mw *Middleware) End() token.Pos            { return mw.Decl.AST.End() }\nfunc (mw *Middleware) SortKey() string {\n\treturn mw.Decl.File.Pkg.ImportPath.String() + \".\" + mw.Decl.Name\n}\n\ntype ParseData struct {\n\tErrs   *perr.List\n\tSchema *schema.Parser\n\n\tFile *pkginfo.File\n\tFunc *ast.FuncDecl\n\tDir  *directive.Directive\n\tDoc  string\n}\n\n// Parse parses the middleware in the provided declaration.\n// It may return nil on errors.\nfunc Parse(d ParseData) *Middleware {\n\tdecl, ok := d.Schema.ParseFuncDecl(d.File, d.Func)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tmw := &Middleware{\n\t\tDecl:   decl,\n\t\tDoc:    d.Doc,\n\t\tFile:   d.File,\n\t\tRecv:   decl.Recv,\n\t\tGlobal: d.Dir.HasOption(\"global\"),\n\t}\n\tok = directive.Validate(d.Errs, d.Dir, directive.ValidateSpec{\n\t\tAllowedOptions: []string{\"global\"},\n\t\tAllowedFields:  []string{\"target\"},\n\t\tValidateOption: nil,\n\t\tValidateField: func(errs *perr.List, f directive.Field) (ok bool) {\n\t\t\tswitch f.Key {\n\t\t\tcase \"target\":\n\t\t\t\tparts := f.List()\n\t\t\t\tfor _, p := range parts {\n\t\t\t\t\tsel, ok := selector.Parse(errs, f.Pos()+7, p) // + 7 for \"target=\"\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch sel.Type {\n\t\t\t\t\tcase selector.Tag, selector.All:\n\t\t\t\t\tdefault:\n\t\t\t\t\t\terrs.Add(errInvalidSelectorType(sel.Type).AtGoNode(f))\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t\tmw.Target.Add(sel)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\tValidateTag: nil,\n\t})\n\tif !ok {\n\t\treturn mw\n\t}\n\n\tsig := decl.Type\n\tnumParams := len(sig.Params)\n\n\t// Validate the input\n\tif numParams < 2 {\n\t\td.Errs.Add(errWrongNumberParams(numParams).AtGoNode(sig.AST.Params))\n\t\treturn mw\n\t} else if numParams > 2 {\n\t\td.Errs.Add(errWrongNumberParams(numParams).AtGoNode(sig.AST.Params))\n\t}\n\n\tnumResults := len(sig.Results)\n\tif numResults < 1 {\n\t\td.Errs.Add(errWrongNumberResults(numResults).AtGoNode(sig.AST.Results))\n\t\treturn mw\n\t} else if numResults > 1 {\n\t\td.Errs.Add(errWrongNumberResults(numResults).AtGoNode(sig.AST.Results))\n\t}\n\n\tif !schemautil.IsNamed(sig.Params[0].Type, \"encore.dev/middleware\", \"Request\") {\n\t\td.Errs.Add(\n\t\t\terrInvalidFirstParam.\n\t\t\t\tAtGoNode(sig.Params[0].AST, errors.AsError(fmt.Sprintf(\"got %s\", utils.PrettyPrint(sig.Params[0].Type.ASTExpr())))),\n\t\t)\n\t}\n\tif !schemautil.IsNamed(sig.Params[1].Type, \"encore.dev/middleware\", \"Next\") {\n\t\td.Errs.Add(\n\t\t\terrInvalidSecondParam.\n\t\t\t\tAtGoNode(sig.Params[1].AST, errors.AsError(fmt.Sprintf(\"got %s\", utils.PrettyPrint(sig.Params[1].Type.ASTExpr())))),\n\t\t)\n\t}\n\tif !schemautil.IsNamed(sig.Results[0].Type, \"encore.dev/middleware\", \"Response\") {\n\t\td.Errs.Add(\n\t\t\terrInvalidReturnType.\n\t\t\t\tAtGoNode(sig.Results[0].AST, errors.AsError(fmt.Sprintf(\"got %s\", utils.PrettyPrint(sig.Results[0].Type.ASTExpr())))),\n\t\t)\n\t}\n\n\treturn mw\n}\n\n// Sort sorts the middleware to ensure they execute in deterministic order.\nfunc Sort(mws []*Middleware) {\n\tsortFn := func(a, b *Middleware) int {\n\t\t// Globals come first\n\t\tif a.Global != b.Global {\n\t\t\tif a.Global {\n\t\t\t\treturn -1\n\t\t\t} else {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t}\n\n\t\t// Then sort by package path\n\t\taPkg, bPkg := a.Decl.File.Pkg, b.Decl.File.Pkg\n\t\tif n := cmp.Compare(aPkg.ImportPath, bPkg.ImportPath); n != 0 {\n\t\t\treturn n\n\t\t}\n\n\t\t// Then sort by file name\n\t\tif n := cmp.Compare(a.Decl.File.Name, b.Decl.File.Name); n != 0 {\n\t\t\treturn n\n\t\t}\n\n\t\t// Sort by declaration order within the file\n\t\treturn cmp.Compare(a.Decl.AST.Pos(), b.Decl.AST.Pos())\n\t}\n\n\tslices.SortStableFunc(mws, sortFn)\n}\n"
  },
  {
    "path": "v2/parser/apis/middleware/middleware_test.go",
    "content": "package middleware\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t. \"encr.dev/v2/internals/schema/schematest\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/apis/selector\"\n)\n\nfunc TestParseMiddleware(t *testing.T) {\n\ttype testCase struct {\n\t\tname     string\n\t\timports  []string\n\t\tdef      string\n\t\twant     *Middleware\n\t\twantErrs []string\n\t}\n\n\tmwParams := []schema.Param{\n\t\tParam(Named(TypeInfo(\"Request\"))),\n\t\tParam(Named(TypeInfo(\"Next\"))),\n\t}\n\tmwResults := []schema.Param{\n\t\tParam(Named(TypeInfo(\"Response\"))),\n\t}\n\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"basic\",\n\t\t\tdef: `\n//encore:middleware target=tag:foo\nfunc Foo(req middleware.Request, next middleware.Next) middleware.Response {}\n`,\n\t\t\twant: &Middleware{\n\t\t\t\tDecl: &schema.FuncDecl{\n\t\t\t\t\tName: \"Foo\",\n\t\t\t\t\tType: schema.FuncType{\n\t\t\t\t\t\tParams:  mwParams,\n\t\t\t\t\t\tResults: mwResults,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tTarget: selector.NewSet(selector.Selector{\n\t\t\t\t\tType:  selector.Tag,\n\t\t\t\t\tValue: \"foo\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timportList := append([]string{\"context\", \"encore.dev/middleware\"}, test.imports...)\n\t\timports := \"\"\n\t\tif len(importList) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range importList {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.def + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tschemaParser := schema.NewParser(tc.Context, l)\n\n\t\t\tif len(test.wantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.wantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tf := pkg.Files[0]\n\t\t\tfd := testutil.FindNodes[*ast.FuncDecl](f.AST())[0]\n\n\t\t\t// Parse the directive from the func declaration.\n\t\t\tdir, doc, ok := directive.Parse(tc.Errs, fd.Doc)\n\t\t\tc.Assert(ok, qt.IsTrue)\n\t\t\tc.Assert(ok, qt.IsTrue)\n\n\t\t\tpd := ParseData{\n\t\t\t\tErrs:   tc.Errs,\n\t\t\t\tSchema: schemaParser,\n\t\t\t\tFile:   f,\n\t\t\t\tFunc:   fd,\n\t\t\t\tDir:    dir,\n\t\t\t\tDoc:    doc,\n\t\t\t}\n\n\t\t\tgot := Parse(pd)\n\t\t\tif len(test.wantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\tcmpEqual := qt.CmpEquals(\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Node }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&schema.FuncDecl{}, &schema.TypeDecl{}, &pkginfo.File{}, &pkginfo.Package{}, token.Pos(0)),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(schema.StructField{}, schema.NamedType{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\tc.Assert(got, cmpEqual, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/parser.go",
    "content": "package apis\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/apis/middleware\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\nvar Parser = &resourceparser.Parser{\n\tName: \"APIs\",\n\n\tInterestingImports: resourceparser.RunAlways,\n\tRun: func(p *resourceparser.Pass) {\n\t\tfor _, file := range p.Pkg.Files {\n\t\t\tfor _, decl := range file.AST().Decls {\n\t\t\t\tswitch decl := decl.(type) {\n\t\t\t\tcase *ast.FuncDecl:\n\t\t\t\t\tif decl.Doc == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tdir, doc, ok := directive.Parse(p.Errs, decl.Doc)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else if dir == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch dir.Name {\n\t\t\t\t\tcase \"api\":\n\t\t\t\t\t\tep := api.Parse(api.ParseData{\n\t\t\t\t\t\t\tErrs:   p.Errs,\n\t\t\t\t\t\t\tSchema: p.SchemaParser,\n\t\t\t\t\t\t\tFile:   file,\n\t\t\t\t\t\t\tFunc:   decl,\n\t\t\t\t\t\t\tDir:    dir,\n\t\t\t\t\t\t\tDoc:    doc,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tif ep != nil {\n\t\t\t\t\t\t\tp.RegisterResource(ep)\n\t\t\t\t\t\t\t// We unconditionally register a package-level bind here,\n\t\t\t\t\t\t\t// even if the endpoint is defined on a service struct.\n\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t// This is the case because we generate a package-level\n\t\t\t\t\t\t\t// wrapper function that forwards to the service struct\n\t\t\t\t\t\t\t// method in that case.\n\t\t\t\t\t\t\tp.AddNamedBind(file, ep.Decl.AST.Name, ep)\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase \"authhandler\":\n\t\t\t\t\t\tah := authhandler.Parse(authhandler.ParseData{\n\t\t\t\t\t\t\tErrs:   p.Errs,\n\t\t\t\t\t\t\tSchema: p.SchemaParser,\n\t\t\t\t\t\t\tFile:   file,\n\t\t\t\t\t\t\tFunc:   decl,\n\t\t\t\t\t\t\tDir:    dir,\n\t\t\t\t\t\t\tDoc:    doc,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif ah != nil {\n\t\t\t\t\t\t\tp.RegisterResource(ah)\n\t\t\t\t\t\t\tif ah.Recv.Empty() {\n\t\t\t\t\t\t\t\tp.AddNamedBind(file, ah.Decl.AST.Name, ah)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase \"middleware\":\n\t\t\t\t\t\tmw := middleware.Parse(middleware.ParseData{\n\t\t\t\t\t\t\tErrs:   p.Errs,\n\t\t\t\t\t\t\tSchema: p.SchemaParser,\n\t\t\t\t\t\t\tFile:   file,\n\t\t\t\t\t\t\tFunc:   decl,\n\t\t\t\t\t\t\tDir:    dir,\n\t\t\t\t\t\t\tDoc:    doc,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tif mw != nil {\n\t\t\t\t\t\t\tp.RegisterResource(mw)\n\t\t\t\t\t\t\tif mw.Recv.Empty() {\n\t\t\t\t\t\t\t\tp.AddNamedBind(file, mw.Decl.AST.Name, mw)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tp.Errs.Add(errUnexpectedDirective(dir.Name).AtGoNode(decl))\n\t\t\t\t\t}\n\n\t\t\t\tcase *ast.GenDecl:\n\t\t\t\t\tif decl.Tok != token.TYPE {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else if decl.Doc == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tdir, doc, ok := directive.Parse(p.Errs, decl.Doc)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else if dir == nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tswitch dir.Name {\n\t\t\t\t\tcase \"service\":\n\t\t\t\t\t\tss := servicestruct.Parse(servicestruct.ParseData{\n\t\t\t\t\t\t\tErrs:   p.Errs,\n\t\t\t\t\t\t\tSchema: p.SchemaParser,\n\t\t\t\t\t\t\tFile:   file,\n\t\t\t\t\t\t\tDecl:   decl,\n\t\t\t\t\t\t\tDir:    dir,\n\t\t\t\t\t\t\tDoc:    doc,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\tif ss != nil {\n\t\t\t\t\t\t\tp.RegisterResource(ss)\n\t\t\t\t\t\t\tp.AddNamedBind(file, ss.Decl.AST.Name, ss)\n\t\t\t\t\t\t}\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tp.Errs.Add(errUnexpectedDirective(dir.Name).AtGoNode(decl))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n}\n"
  },
  {
    "path": "v2/parser/apis/selector/errors.go",
    "content": "package selector\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"selector\",\n\t\t\"\",\n\n\t\terrors.WithRangeSize(10),\n\t)\n\n\terrMissingSelectorType = errRange.New(\n\t\t\"Invalid Selector\",\n\t\t\"Missing selector type.\",\n\t)\n\n\terrUnknownSelectorType = errRange.Newf(\n\t\t\"Invalid Selector\",\n\t\t\"Unknown selector type %q.\",\n\t)\n\n\terrInvalidSelectorValue = errRange.Newf(\n\t\t\"Invalid Selector\",\n\t\t\"Invalid selector value %q.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/selector/selector.go",
    "content": "package selector\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n\t\"encr.dev/v2/internals/perr\"\n)\n\ntype Type string\n\n// NOTE: New types added should be also added to meta.proto and est/selectors.go.\n\nconst (\n\tAll Type = \"all\"\n\tTag Type = \"tag\"\n)\n\ntype Selector struct {\n\t// Ensure this type is not comparable,\n\t// since it contains position information.\n\t_ [0]func()\n\n\tType  Type\n\tValue string\n\n\tstartPos token.Pos\n\tendPos   token.Pos\n}\n\n// Key is a suitable key for a [Selector],\n// since selectors are not otherwise comparable.\ntype Key struct {\n\tType  Type\n\tValue string\n}\n\nvar _ ast.Node = Selector{}\n\nfunc (s Selector) Pos() token.Pos {\n\treturn s.startPos\n}\n\nfunc (s Selector) End() token.Pos {\n\treturn s.endPos\n}\n\nfunc (s Selector) String() string {\n\tif s.Type == All {\n\t\treturn \"all\"\n\t}\n\treturn string(s.Type) + \":\" + s.Value\n}\n\n// Key returns a Key for s.\nfunc (s Selector) Key() Key {\n\treturn Key{Type: s.Type, Value: s.Value}\n}\n\n// Equals reports whether s and o are equal on type and value.\n//\n// It does not compare the start and end positions.\nfunc (s Selector) Equals(o Selector) bool {\n\treturn s.Key() == o.Key()\n}\n\nfunc (s Selector) ToProto() *meta.Selector {\n\tpb := &meta.Selector{Type: meta.Selector_UNKNOWN, Value: s.Value}\n\tswitch s.Type {\n\tcase All:\n\t\tpb.Type = meta.Selector_ALL\n\tcase Tag:\n\t\tpb.Type = meta.Selector_TAG\n\t}\n\treturn pb\n}\n\nfunc Parse(errs *perr.List, startPos token.Pos, s string) (Selector, bool) {\n\tif s == \"all\" {\n\t\treturn Selector{Type: All, Value: \"\"}, true\n\t}\n\n\ttyp, val, ok := strings.Cut(s, \":\")\n\tif !ok {\n\t\terrs.Add(errMissingSelectorType.AtGoPos(startPos, startPos+token.Pos(len([]byte(s)))))\n\t\treturn Selector{}, false\n\t}\n\n\tsel := Selector{\n\t\tType:     Type(typ),\n\t\tValue:    val,\n\t\tstartPos: startPos,\n\t\tendPos:   startPos + token.Pos(len([]byte(s))),\n\t}\n\n\tvar re *regexp.Regexp\n\tswitch sel.Type {\n\tcase Tag:\n\t\tre = tagRegexp\n\tdefault:\n\t\terrs.Add(errUnknownSelectorType(typ).AtGoNode(sel))\n\t\treturn Selector{}, false\n\t}\n\n\tif !re.MatchString(val) {\n\t\terrs.Add(errInvalidSelectorValue(val).AtGoNode(sel))\n\t\treturn Selector{}, false\n\t}\n\n\treturn sel, true\n}\n\nvar (\n\ttagRegexp = regexp.MustCompile(`^[a-z]([-_a-z0-9]*[a-z0-9])?$`)\n)\n\ntype Set struct {\n\tvals []Selector\n}\n\nfunc NewSet(sels ...Selector) Set {\n\tvar s Set\n\tfor _, sel := range sels {\n\t\ts.Add(sel)\n\t}\n\treturn s\n}\n\n// Add adds a selector to the set. It reports whether the selector was added,\n// meaning it reports false iff the set already contained that selector.\n//\n// Add ensures that the set is sorted.\nfunc (s *Set) Add(sel Selector) (added bool) {\n\tidx := sort.Search(len(s.vals), func(i int) bool {\n\t\tv := &s.vals[i]\n\t\treturn v.Type >= sel.Type && v.Value >= sel.Value\n\t})\n\n\tif idx < len(s.vals) && (s.vals)[idx].Equals(sel) {\n\t\treturn false\n\t}\n\n\t// Insert at the end\n\tif idx == len(s.vals) {\n\t\ts.vals = append(s.vals, sel)\n\t\treturn true\n\t}\n\n\t// Make space for the new selector by shifting the rest of the slice\n\ts.vals = append(s.vals[:idx+1], s.vals[idx:]...)\n\n\t// Insert the new selector\n\ts.vals[idx] = sel\n\n\treturn true\n}\n\n// Merge adds all the selectors from other to s.\n//\n// It is equivalent to calling Add for each selector in other.\nfunc (s *Set) Merge(other Set) {\n\tfor _, sel := range other.vals {\n\t\ts.Add(sel)\n\t}\n}\n\n// Contains reports whether the set contains the given selector.\nfunc (s *Set) Contains(sel Selector) bool {\n\tidx := sort.Search(len(s.vals), func(i int) bool {\n\t\tv := &s.vals[i]\n\t\treturn v.Type >= sel.Type && v.Value >= sel.Value\n\t})\n\treturn idx < len(s.vals) && (s.vals)[idx].Equals(sel)\n}\n\n// ContainsAny reports whether the set contains any of the selectors in other.\n//\n// It compares in linear time O(N) where N is the number of selectors in the\n// larger set. It is faster than calling Contains for each selector in other.\nfunc (s *Set) ContainsAny(other Set) bool {\n\t// If this set contains the \"all\" selector it always matches.\n\tif slices.IndexFunc(s.vals, func(sel Selector) bool { return sel.Type == All }) != -1 {\n\t\treturn true\n\t}\n\n\ti, j := 0, 0\n\tfor i < len(s.vals) && j < len(other.vals) {\n\t\tiv, jv := &s.vals[i], &other.vals[j]\n\n\t\tswitch {\n\t\tcase iv.Type == All:\n\t\t\treturn true\n\n\t\tcase iv.Type == jv.Type:\n\t\t\tif s.vals[i].Equals(other.vals[j]) {\n\t\t\t\treturn true\n\t\t\t} else if iv.Value < jv.Value {\n\t\t\t\ti++\n\t\t\t} else {\n\t\t\t\tj++\n\t\t\t}\n\n\t\tcase iv.Type < jv.Type:\n\t\t\ti++\n\t\tcase iv.Type > jv.Type:\n\t\t\tj++\n\t\t}\n\t}\n\treturn false\n}\n\n// ForEach calls fn for each selector in the set.\nfunc (s *Set) ForEach(fn func(Selector)) {\n\tfor _, sel := range s.vals {\n\t\tfn(sel)\n\t}\n}\n\n// Len returns the number of selectors in the set.\nfunc (s *Set) Len() int {\n\treturn len(s.vals)\n}\n\n// ToProto returns the set as a slice of proto selectors.\nfunc (s *Set) ToProto() []*meta.Selector {\n\tpbs := make([]*meta.Selector, len(s.vals))\n\tfor i, sel := range s.vals {\n\t\tpbs[i] = sel.ToProto()\n\t}\n\treturn pbs\n}\n"
  },
  {
    "path": "v2/parser/apis/selector/selector_test.go",
    "content": "package selector\n\nimport \"testing\"\n\nfunc TestSet_ContainsAny(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tset   []Selector\n\t\tinput []Selector\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"empty_input\",\n\t\t\tset:   []Selector{{Type: All}},\n\t\t\tinput: nil,\n\t\t\twant:  true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NewSet(tt.set...)\n\t\t\tin := NewSet(tt.input...)\n\t\t\tif got := s.ContainsAny(in); got != tt.want {\n\t\t\t\tt.Errorf(\"ContainsAny() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/apis/servicestruct/errors.go",
    "content": "package servicestruct\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"servicestruct\",\n\t\t\"For more information on service structs, see https://encore.dev/docs/primitives/services-and-apis/service-structs\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrInvalidDirectivePlacement = errRange.New(\n\t\t\"Invalid encore:service directive\",\n\t\t\"encore:service directives must be placed on the declaration of a struct, not a group.\",\n\t)\n\n\terrServiceStructMustNotBeGeneric = errRange.New(\n\t\t\"Invalid service struct\",\n\t\t\"Service structs cannot be defined as generic types.\",\n\t)\n\n\terrServiceInitCannotBeGeneric = errRange.New(\n\t\t\"Invalid service init function\",\n\t\t\"Service init functions cannot be defined as generic functions.\",\n\t)\n\n\terrServiceInitCannotHaveParams = errRange.New(\n\t\t\"Invalid service init function\",\n\t\t\"Service init functions cannot have parameters.\",\n\t)\n\n\terrServiceInitInvalidReturnType = errRange.Newf(\n\t\t\"Invalid service init function\",\n\t\t\"Service init functions must return (*%s, error).\",\n\t)\n\n\tErrDuplicateServiceStructs = errRange.New(\n\t\t\"Multiple service structs found\",\n\t\t\"Multiple service structs were found in the same service. Encore only allows one service struct to be defined per service.\",\n\t)\n\n\tErrReceiverNotAServiceStruct = errRange.New(\n\t\t\"Invalid service struct for API\",\n\t\t\"API endpoints defined as receiver functions must be defined on a service struct.\",\n\t)\n\n\tErrServiceStructReferencedInAnotherService = errRange.New(\n\t\t\"Service struct referenced in another service\",\n\t\t\"Service structs cannot be referenced in other services. They can only be referenced in the service that defines them.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/apis/servicestruct/servicestruct.go",
    "content": "package servicestruct\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n\t\"encr.dev/v2/parser/internal/utils\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\n// ServiceStruct describes a dependency injection struct for a service.\ntype ServiceStruct struct {\n\tDecl *schema.TypeDecl // decl is the type declaration\n\tDoc  string\n\n\t// Init is the function for initializing this group.\n\t// It is nil if there is no initialization function.\n\tInit option.Option[*schema.FuncDecl]\n}\n\nfunc (ss *ServiceStruct) Kind() resource.Kind       { return resource.ServiceStruct }\nfunc (ss *ServiceStruct) Package() *pkginfo.Package { return ss.Decl.File.Pkg }\nfunc (ss *ServiceStruct) Pos() token.Pos            { return ss.Decl.AST.Pos() }\nfunc (ss *ServiceStruct) End() token.Pos            { return ss.Decl.AST.End() }\nfunc (ss *ServiceStruct) SortKey() string {\n\treturn ss.Decl.File.Pkg.ImportPath.String() + \".\" + ss.Decl.Name\n}\n\ntype ParseData struct {\n\tErrs   *perr.List\n\tSchema *schema.Parser\n\n\tFile *pkginfo.File\n\tDecl *ast.GenDecl\n\tDir  *directive.Directive\n\tDoc  string\n}\n\n// Parse parses the service struct in the provided type declaration.\nfunc Parse(d ParseData) *ServiceStruct {\n\t// We don't allow anything on the directive besides \"encore:service\".\n\tdirective.Validate(d.Errs, d.Dir, directive.ValidateSpec{})\n\n\t// We only support encore:service directives directly on the type declaration,\n\t// not on a group of type declarations.\n\tif len(d.Decl.Specs) != 1 {\n\t\td.Errs.Add(errInvalidDirectivePlacement.AtGoNode(d.Dir.AST))\n\t\tif len(d.Decl.Specs) == 0 {\n\t\t\t// We can't continue without at least one spec.\n\t\t\td.Errs.Bailout()\n\t\t}\n\t}\n\n\tspec := d.Decl.Specs[0].(*ast.TypeSpec)\n\tdeclInfo := d.File.Pkg.Names().PkgDecls[spec.Name.Name]\n\tdecl := d.Schema.ParseTypeDecl(declInfo)\n\n\tss := &ServiceStruct{\n\t\tDecl: decl,\n\t\tDoc:  d.Doc,\n\t}\n\n\t// Find the init function for this service struct, if any.\n\tinitFunc := d.File.Pkg.Names().PkgDecls[\"init\"+ss.Decl.Name]\n\tif initFunc != nil && initFunc.Type == token.FUNC {\n\t\tinit, ok := d.Schema.ParseFuncDecl(initFunc.File, initFunc.Func)\n\t\tif ok {\n\t\t\tss.Init = option.Some(init)\n\t\t}\n\t}\n\n\tvalidateServiceStruct(d, ss)\n\treturn ss\n}\n\n// validateServiceStruct validates that the service struct and associated init function\n// has the correct structure.\nfunc validateServiceStruct(d ParseData, ss *ServiceStruct) {\n\tif len(ss.Decl.TypeParams) > 0 {\n\t\td.Errs.Add(errServiceStructMustNotBeGeneric.AtGoNode(ss.Decl.TypeParams[0].AST))\n\t}\n\n\tss.Init.ForAll(func(initFunc *schema.FuncDecl) {\n\t\tif len(initFunc.TypeParams) > 0 {\n\t\t\td.Errs.Add(errServiceInitCannotBeGeneric.AtGoNode(initFunc.TypeParams[0].AST))\n\t\t}\n\t\tif len(initFunc.Type.Params) > 0 {\n\t\t\td.Errs.Add(errServiceInitCannotHaveParams.AtGoNode(initFunc.Type.Params[0].AST))\n\t\t}\n\n\t\t// Ensure the return type is (*T, error) where T is the service struct.\n\t\tif len(initFunc.Type.Results) != 2 {\n\t\t\t// Wrong number of returns\n\t\t\td.Errs.Add(errServiceInitInvalidReturnType(ss.Decl.Name).AtGoNode(initFunc.AST))\n\t\t} else if result, n := schemautil.Deref(initFunc.Type.Results[0].Type); n != 1 || !schemautil.IsNamed(result, ss.Decl.File.Pkg.ImportPath, ss.Decl.Name) {\n\t\t\t// First type is not *T\n\t\t\td.Errs.Add(\n\t\t\t\terrServiceInitInvalidReturnType(ss.Decl.Name).\n\t\t\t\t\tAtGoNode(initFunc.AST, errors.AsError(\n\t\t\t\t\t\tfmt.Sprintf(\"got %s\", utils.PrettyPrint(initFunc.Type.Results[0].Type.ASTExpr())),\n\t\t\t\t\t)),\n\t\t\t)\n\t\t} else if !schemautil.IsBuiltinKind(initFunc.Type.Results[1].Type, schema.Error) {\n\t\t\t// Second type is not builtin error.\n\t\t\td.Errs.Add(\n\t\t\t\terrServiceInitInvalidReturnType(ss.Decl.Name).\n\t\t\t\t\tAtGoNode(initFunc.AST, errors.AsError(\n\t\t\t\t\t\tfmt.Sprintf(\"got %s\", utils.PrettyPrint(initFunc.Type.Results[1].Type.ASTExpr())),\n\t\t\t\t\t)),\n\t\t\t)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "v2/parser/apis/servicestruct/servicestruct_test.go",
    "content": "package servicestruct\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser/apis/directive\"\n)\n\nfunc TestParseServiceStruct(t *testing.T) {\n\ttype testCase struct {\n\t\tname     string\n\t\timports  []string\n\t\tdef      string\n\t\twant     *ServiceStruct\n\t\twantErrs []string\n\t}\n\tfile := fileForPkg(\"foo\", \"example.com\")\n\ttests := []testCase{\n\t\t{\n\t\t\tname: \"basic\",\n\t\t\tdef: `\n//encore:service\ntype Foo struct {}\n`,\n\t\t\twant: &ServiceStruct{\n\t\t\t\tDecl: &schema.TypeDecl{\n\t\t\t\t\tFile:       file,\n\t\t\t\t\tName:       \"Foo\",\n\t\t\t\t\tType:       schema.StructType{},\n\t\t\t\t\tTypeParams: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with_init_func\",\n\t\t\tdef: `\n//encore:service\ntype Foo struct {}\nfunc initFoo() (*Foo, error) {}\n`,\n\t\t\twant: &ServiceStruct{\n\t\t\t\tDecl: &schema.TypeDecl{\n\t\t\t\t\tFile:       file,\n\t\t\t\t\tName:       \"Foo\",\n\t\t\t\t\tType:       schema.StructType{},\n\t\t\t\t\tTypeParams: nil,\n\t\t\t\t},\n\t\t\t\tInit: option.Some(&schema.FuncDecl{\n\t\t\t\t\tName: \"initFoo\",\n\t\t\t\t\tType: schema.FuncType{\n\t\t\t\t\t\tResults: []schema.Param{\n\t\t\t\t\t\t\t{Type: schema.PointerType{Elem: schema.NamedType{\n\t\t\t\t\t\t\t\tDeclInfo: &pkginfo.PkgDeclInfo{\n\t\t\t\t\t\t\t\t\tName: \"Foo\",\n\t\t\t\t\t\t\t\t\tType: token.TYPE,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}},\n\t\t\t\t\t\t\t{Type: schema.BuiltinType{Kind: schema.Error}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"error_init_no_service\",\n\t\t\tdef: `\n//encore:service\ntype Foo struct {}\nfunc initFoo() error {}\n`,\n\t\t\twantErrs: []string{`.*Service init functions must return \\(\\*Foo, error\\)`},\n\t\t},\n\t\t{\n\t\t\tname: \"error_init_no_pointer\",\n\t\t\tdef: `\n//encore:service\ntype Foo struct {}\nfunc initFoo() (Foo, error) {}\n`,\n\t\t\twantErrs: []string{`.*Service init functions must return \\(\\*Foo, error\\)`},\n\t\t},\n\t\t{\n\t\t\tname: \"error_init_shadow_error\",\n\t\t\tdef: `\n//encore:service\ntype Foo struct {}\nfunc initFoo() (*Foo, error) {}\ntype error int\n`,\n\t\t\twantErrs: []string{`.*Service init functions must return \\(\\*Foo, error\\)`},\n\t\t},\n\t\t{\n\t\t\tname: \"error_init_bad_params\",\n\t\t\tdef: `\n//encore:service\ntype Foo struct {}\nfunc initFoo(int) (*Foo, error) {}\n`,\n\t\t\twantErrs: []string{`.*Service init functions cannot have parameters`},\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timportList := append([]string{\"context\"}, test.imports...)\n\t\timports := \"\"\n\t\tif len(importList) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range importList {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.def + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tschemaParser := schema.NewParser(tc.Context, l)\n\n\t\t\tif len(test.wantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.wantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tf := pkg.Files[0]\n\t\t\tgd := testutil.FindNodes[*ast.GenDecl](f.AST())[1]\n\n\t\t\t// Parse the directive from the func declaration.\n\t\t\tdir, doc, ok := directive.Parse(tc.Errs, gd.Doc)\n\t\t\tc.Assert(ok, qt.IsTrue)\n\n\t\t\tpd := ParseData{\n\t\t\t\tErrs:   tc.Errs,\n\t\t\t\tSchema: schemaParser,\n\t\t\t\tFile:   f,\n\t\t\t\tDecl:   gd,\n\t\t\t\tDir:    dir,\n\t\t\t\tDoc:    doc,\n\t\t\t}\n\n\t\t\tgot := Parse(pd)\n\t\t\tif len(test.wantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\tcmpEqual := qt.CmpEquals(\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Node }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&schema.FuncDecl{}, &schema.TypeDecl{}, &pkginfo.File{}, &pkginfo.Package{}, token.Pos(0)),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(schema.StructField{}, schema.NamedType{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\tc.Assert(got, cmpEqual, test.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc fileForPkg(pkgName string, pkgPath paths.Pkg) *pkginfo.File {\n\treturn &pkginfo.File{Pkg: &pkginfo.Package{\n\t\tName:       pkgName,\n\t\tImportPath: pkgPath,\n\t}}\n}\n"
  },
  {
    "path": "v2/parser/apis/servicestruct/usage.go",
    "content": "package servicestruct\n\nimport (\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype Usage struct {\n\tusage.Base\n\n\tServiceStruct *ServiceStruct\n}\n\nfunc ResolveServiceStructUsage(data usage.ResolveData, s *ServiceStruct) usage.Usage {\n\treturn &Usage{\n\t\tBase: usage.Base{\n\t\t\tFile: data.Expr.DeclaredIn(),\n\t\t\tBind: data.Expr.ResourceBind(),\n\t\t\tExpr: data.Expr,\n\t\t},\n\t\tServiceStruct: s,\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/caches/cache_test.go",
    "content": "package caches\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/parser/resource/resourcetest\"\n)\n\nfunc TestParseCluster(t *testing.T) {\n\ttests := []resourcetest.Case[*Cluster]{\n\t\t{\n\t\t\tName: \"basic\",\n\t\t\tCode: `\n// Cluster docs\nvar x = cache.NewCluster(\"name\", cache.ClusterConfig{})\n`,\n\t\t\tWant: &Cluster{\n\t\t\t\tName:           \"name\",\n\t\t\t\tDoc:            \"Cluster docs\\n\",\n\t\t\t\tEvictionPolicy: \"allkeys-lru\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"with_eviction_policy\",\n\t\t\tCode: `\n// Cluster docs\nvar x = cache.NewCluster(\"name\", cache.ClusterConfig{EvictionPolicy: cache.VolatileLFU})\n`,\n\t\t\tWant: &Cluster{\n\t\t\t\tName:           \"name\",\n\t\t\t\tDoc:            \"Cluster docs\\n\",\n\t\t\t\tEvictionPolicy: \"volatile-lfu\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"with_bad_eviction_policy\",\n\t\t\tCode: `\n// Cluster docs\nvar x = cache.NewCluster(\"name\", cache.ClusterConfig{EvictionPolicy: \"x\"})\n`,\n\t\t\tWantErrs: []string{`.*Invalid Cache Eviction Policy.*`},\n\t\t},\n\t\t{\n\t\t\tName: \"with_invalid_eviction_policy\",\n\t\t\tCode: `\n// Cluster docs\nvar x = cache.NewCluster(\"name\", cache.ClusterConfig{EvictionPolicy: cache.NonExisting})\n`,\n\t\t\tWantErrs: []string{\n\t\t\t\t\".*Field `EvictionPolicy` must be a constant literal.*\",\n\t\t\t\t\"Must be one of the constants defined in the cache package\",\n\t\t\t},\n\t\t},\n\t}\n\n\tresourcetest.Run(t, ClusterParser, tests)\n}\n"
  },
  {
    "path": "v2/parser/infra/caches/cluster.go",
    "content": "package caches\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encore.dev/storage/cache\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\tliterals \"encr.dev/v2/parser/infra/internal/literals\"\n\tparseutil \"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Cluster struct {\n\tAST            *ast.CallExpr\n\tName           string // The unique name of the cache cluster\n\tDoc            string // The documentation on the cluster\n\tEvictionPolicy string\n\tFile           *pkginfo.File\n}\n\nfunc (c *Cluster) Kind() resource.Kind       { return resource.CacheCluster }\nfunc (c *Cluster) Package() *pkginfo.Package { return c.File.Pkg }\nfunc (c *Cluster) ASTExpr() ast.Expr         { return c.AST }\nfunc (c *Cluster) ResourceName() string      { return c.Name }\nfunc (c *Cluster) Pos() token.Pos            { return c.AST.Pos() }\nfunc (c *Cluster) End() token.Pos            { return c.AST.End() }\nfunc (c *Cluster) SortKey() string           { return c.Name }\n\nvar ClusterParser = &resourceparser.Parser{\n\tName: \"Cache Cluster\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/storage/cache\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{PkgPath: \"encore.dev/storage/cache\", Name: \"NewCluster\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 0,\n\t\t\tParse:       parseCluster,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parseCluster(d parseutil.ReferenceInfo) {\n\tdisplayName := d.ResourceFunc.NaiveDisplayName()\n\tif len(d.Call.Args) != 2 {\n\t\td.Pass.Errs.Add(errExpectsTwoArgs(displayName).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tclusterName := parseutil.ParseResourceName(d.Pass.Errs, displayName, \"cache cluster name\",\n\t\td.Call.Args[0], parseutil.KebabName, \"\")\n\tif clusterName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\tcfgLit, ok := literals.ParseStruct(d.Pass.Errs, d.File, \"cache.ClusterConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\t// Decode the config\n\ttype decodedConfig struct {\n\t\tEvictionPolicy string   `literal:\",optional,default\"`\n\t\tDefaultExpiry  ast.Expr `literal:\",optional,dynamic\"`\n\t}\n\tdefaultValues := decodedConfig{\n\t\tEvictionPolicy: string(cache.AllKeysLRU),\n\t}\n\tconfig := literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, &defaultValues)\n\n\tswitch cache.EvictionPolicy(config.EvictionPolicy) {\n\tcase cache.AllKeysLRU, cache.AllKeysLFU, cache.AllKeysRandom, cache.VolatileLRU,\n\t\tcache.VolatileLFU, cache.VolatileTTL, cache.VolatileRandom, cache.NoEviction:\n\t\t// all good\n\tdefault:\n\t\td.Pass.Errs.Add(errInvalidEvictionPolicy.AtGoNode(d.Call.Args[1]))\n\t\treturn\n\t}\n\n\tcluster := &Cluster{\n\t\tAST:            d.Call,\n\t\tName:           clusterName,\n\t\tDoc:            d.Doc,\n\t\tEvictionPolicy: config.EvictionPolicy,\n\t\tFile:           d.File,\n\t}\n\n\td.Pass.RegisterResource(cluster)\n\td.Pass.AddBind(d.File, d.Ident, cluster)\n}\n"
  },
  {
    "path": "v2/parser/infra/caches/errors.go",
    "content": "package caches\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"cache\",\n\t\t`For more information see https://encore.dev/docs/primitives/caching`,\n\n\t\terrors.WithRangeSize(25),\n\t)\n\n\terrExpectsTwoArgs = errRange.Newf(\"Invalid cache construction\", \"%s expects two arguments, got %d.)\")\n\n\tErrCouldNotResolveCacheCluster = errRange.New(\n\t\t\"Invalid Cache Keyspace Construction\",\n\t\t`Could not resolve the cache cluster: must refer to a package level variable.`,\n\t)\n\n\terrPrefixReserved = errRange.New(\n\t\t\"Invalid Cache Key Pattern\",\n\t\t\"The prefix `__encore` is reserved for internal use by Encore.\",\n\t)\n\n\terrKeyPatternMustBeNamedKey = errRange.New(\n\t\t\"Invalid Cache Key Pattern\",\n\t\t\"KeyPattern parameter must be named ':key' for basic (non-struct) key types\",\n\t)\n\n\terrInvalidKeyTypeParameter = errRange.Newf(\n\t\t\"Invalid Cache Key Type\",\n\t\t\"%s has an invalid key type parameter: %s\",\n\t)\n\n\terrKeyContainsAnonymousFields = errRange.New(\n\t\t\"Invalid Cache Key Type\",\n\t\t\"The key type must not contain anonymous fields.\",\n\t)\n\n\terrKeyContainsUnexportedFields = errRange.New(\n\t\t\"Invalid Cache Key Type\",\n\t\t\"The key type must not contain unexported fields.\",\n\t)\n\n\terrFieldNotUsedInKeyPattern = errRange.Newf(\n\t\t\"Invalid Cache Key Type\",\n\t\t\"Invalid use of the key type, the field %s was not used in the KeyPattern\",\n\t)\n\n\terrFieldIsInvalid = errRange.Newf(\n\t\t\"Invalid Cache Key Type\",\n\t\t\"The field %s is invalid: %s\",\n\t)\n\n\terrFieldDoesntExist = errRange.Newf(\n\t\t\"Invalid Cache Key Pattern\",\n\t\t\"Field %s does not existing in key type %s\",\n\t)\n\n\terrMustBeANamedStructType = errRange.New(\n\t\t\"Invalid Cache Value Type\",\n\t\t\"Must be a named struct type.\",\n\t)\n\n\terrStructMustNotBePointer = errRange.New(\n\t\t\"Invalid Cache Value Type\",\n\t\t\"Must not be a pointer type.\",\n\t)\n\n\terrInvalidEvictionPolicy = errRange.New(\n\t\t\"Invalid Cache Eviction Policy\",\n\t\t\"Must be one of the constants defined in the cache package.\",\n\t)\n\n\tErrDuplicateCacheCluster = errRange.New(\n\t\t\"Duplicate Cache Cluster\",\n\t\t\"Cache clusters must have unique names.\",\n\n\t\terrors.PrependDetails(\"I you wish to reuse the same cluster, export the original cache.Cluster object and reuse it here.\"),\n\t)\n\n\tErrKeyspaceNotInService = errRange.New(\n\t\t\"Invalid Cache Keyspace\",\n\t\t\"Cache keyspaces must be defined within a service.\",\n\t)\n\n\tErrKeyspaceUsedInOtherService = errRange.New(\n\t\t\"Invalid Cache Keyspace Usage\",\n\t\t\"Cache keyspaces must be used within the same service they are defined in.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/caches/keyspace.go",
    "content": "package caches\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/internal/literals\"\n\t\"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Keyspace struct {\n\tAST     *ast.CallExpr\n\tDoc     string        // The documentation on the keyspace\n\tFile    *pkginfo.File // File the keyspace is declared in.\n\tCluster pkginfo.QualifiedName\n\n\tKeyType   schema.Type\n\tValueType schema.Type\n\tPath      *resourcepaths.Path\n\n\t// The struct literal for the config. Used to inject additional configuration\n\t// at compile-time.\n\tConfigLiteral *ast.CompositeLit\n}\n\nfunc (k *Keyspace) Kind() resource.Kind       { return resource.CacheKeyspace }\nfunc (k *Keyspace) Package() *pkginfo.Package { return k.File.Pkg }\nfunc (k *Keyspace) ASTExpr() ast.Expr         { return k.AST }\nfunc (k *Keyspace) Pos() token.Pos            { return k.AST.Pos() }\nfunc (k *Keyspace) End() token.Pos            { return k.AST.End() }\nfunc (k *Keyspace) SortKey() string {\n\treturn fmt.Sprintf(\"%s:%s:%d\", k.File.Pkg.ImportPath, k.File.Name, k.AST.Pos())\n}\n\nvar KeyspaceParser = &resourceparser.Parser{\n\tName: \"Cache Keyspace\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/storage/cache\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tvar (\n\t\t\tnames []pkginfo.QualifiedName\n\t\t\tspecs = make(map[pkginfo.QualifiedName]*parseutil.ReferenceSpec)\n\t\t)\n\t\tfor _, c := range keyspaceConstructors {\n\t\t\tname := pkginfo.QualifiedName{PkgPath: \"encore.dev/storage/cache\", Name: c.FuncName}\n\t\t\tnames = append(names, name)\n\n\t\t\tnumTypeArgs := 1\n\t\t\tif c.ValueKind != implicitValue {\n\t\t\t\tnumTypeArgs = 2\n\t\t\t}\n\n\t\t\tc := c // capture for closure\n\t\t\tparseFn := func(d parseutil.ReferenceInfo) {\n\t\t\t\tparseKeyspace(c, d)\n\t\t\t}\n\n\t\t\tspec := &parseutil.ReferenceSpec{\n\t\t\t\tMinTypeArgs: numTypeArgs,\n\t\t\t\tMaxTypeArgs: numTypeArgs,\n\t\t\t\tParse:       parseFn,\n\t\t\t}\n\t\t\tspecs[name] = spec\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, names, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tspec := specs[name]\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\ntype valueKind int\n\nconst (\n\t// implicitValue means the user does not specify the value type as\n\t// a type parameter, but is implicit from the constructor name.\n\t// If used, the ImplicitValueType field must be set on the constructor spec.\n\timplicitValue valueKind = iota\n\n\t// basicValue means the constructor supports basic types only.\n\tbasicValue\n\n\t// structValue means the constructor supports struct values only.\n\tstructValue\n)\n\n// cacheKeyspaceConstructor describes a particular cache keyspace constructor.\ntype cacheKeyspaceConstructor struct {\n\tFuncName          string\n\tValueKind         valueKind\n\tImplicitValueType schema.Type\n}\n\nvar keyspaceConstructors = []cacheKeyspaceConstructor{\n\t{\"NewStringKeyspace\", implicitValue, schema.BuiltinType{Kind: schema.String}},\n\t{\"NewIntKeyspace\", implicitValue, schema.BuiltinType{Kind: schema.Int64}},\n\t{\"NewFloatKeyspace\", implicitValue, schema.BuiltinType{Kind: schema.Float64}},\n\t{\"NewListKeyspace\", basicValue, nil},\n\t{\"NewSetKeyspace\", basicValue, nil},\n\t{\"NewStructKeyspace\", structValue, nil},\n}\n\nfunc parseKeyspace(c cacheKeyspaceConstructor, d parseutil.ReferenceInfo) {\n\terrs := d.Pass.Errs\n\tconstructorName := d.ResourceFunc.NaiveDisplayName()\n\tif len(d.Call.Args) != 2 {\n\t\terrs.Add(errExpectsTwoArgs(constructorName, len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tclusterRef, ok := d.File.Names().ResolvePkgLevelRef(d.Call.Args[0])\n\tif !ok {\n\t\terrs.Add(ErrCouldNotResolveCacheCluster.AtGoNode(d.Call.Args[0]))\n\t\treturn\n\t}\n\n\tcfgLit, ok := literals.ParseStruct(errs, d.File, \"cache.KeyspaceConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\tkeyNode := d.TypeArgs[0].ASTExpr()\n\tpatternNode := cfgLit.Expr(\"KeyPattern\")\n\n\t// Decode the config\n\ttype decodedConfig struct {\n\t\tKeyPattern    string   `literal:\",required\"`\n\t\tDefaultExpiry ast.Expr `literal:\",optional,dynamic\"`\n\t}\n\tconfig := literals.Decode[decodedConfig](errs, cfgLit, nil)\n\n\tconst reservedPrefix = \"__encore\"\n\tif strings.HasPrefix(config.KeyPattern, reservedPrefix) {\n\t\terrs.Add(errPrefixReserved.AtGoNode(patternNode))\n\t\treturn\n\t}\n\n\tpath, ok := resourcepaths.Parse(\n\t\terrs,\n\t\tcfgLit.Pos(\"KeyPattern\")+1, // + 1 to offset the opening \\\"\n\t\tconfig.KeyPattern,\n\t\tresourcepaths.Options{\n\t\t\tAllowWildcard: false,\n\t\t\tAllowFallback: false,\n\t\t\tPrefixSlash:   false,\n\t\t},\n\t)\n\tif !ok {\n\t\treturn\n\t}\n\n\t// Get key and value types.\n\tkeyType := d.TypeArgs[0]\n\tvar valueType schema.Type\n\tif c.ValueKind == implicitValue {\n\t\tvalueType = c.ImplicitValueType\n\t} else {\n\t\tvalueType = d.TypeArgs[1]\n\t}\n\n\t// Check that the key type is a basic type or a named struct\n\t{\n\t\tvalidateBuiltin := func(b schema.BuiltinKind) error {\n\t\t\tswitch b {\n\t\t\tcase schema.Any:\n\t\t\t\treturn fmt.Errorf(\"'any'/'interface{}' is not supported\")\n\t\t\tcase schema.JSON:\n\t\t\t\treturn fmt.Errorf(\"json.RawMessage is not supported\")\n\t\t\tcase schema.Float64, schema.Float32:\n\t\t\t\treturn fmt.Errorf(\"floating point values are not supported\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tbuiltinKey, keyIsBuiltin := keyType.(schema.BuiltinType)\n\n\t\tunusedSegments := make(map[string]resourcepaths.Segment)\n\t\tfor _, seg := range path.Segments {\n\t\t\tif seg.Type == resourcepaths.Literal {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tname := seg.Value\n\t\t\tunusedSegments[name] = seg\n\n\t\t\tif keyIsBuiltin && name != \"key\" {\n\t\t\t\terrs.Add(errKeyPatternMustBeNamedKey.AtGoNode(seg))\n\t\t\t}\n\t\t}\n\n\t\t// It should either be a builtin type or a (possibly pointer to a) named struct.\n\t\tif keyIsBuiltin {\n\t\t\tif err := validateBuiltin(builtinKey.Kind); err != nil {\n\t\t\t\terrs.Add(errInvalidKeyTypeParameter(constructorName, err.Error()).AtGoNode(keyNode))\n\t\t\t}\n\t\t} else {\n\t\t\tref, ok := schemautil.ResolveNamedStruct(keyType, false)\n\t\t\tif !ok {\n\t\t\t\terrs.Add(errInvalidKeyTypeParameter(constructorName, \"must be a basic type or a named struct type\").AtGoNode(keyNode))\n\t\t\t} else if ref.Pointers > 0 {\n\t\t\t\terrs.Add(errInvalidKeyTypeParameter(constructorName, \"must not be a pointer type\").AtGoNode(keyNode))\n\t\t\t} else {\n\t\t\t\t// Validate the struct fields.\n\t\t\t\tst := schemautil.ConcretizeWithTypeArgs(errs, ref.Decl.Type, ref.TypeArgs).(schema.StructType)\n\n\t\t\t\t// Validate struct fields\n\t\t\t\tfor _, f := range st.Fields {\n\t\t\t\t\tif f.IsAnonymous() {\n\t\t\t\t\t\terrs.Add(errKeyContainsAnonymousFields.AtGoNode(f.AST))\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else if !f.IsExported() {\n\t\t\t\t\t\terrs.Add(errKeyContainsUnexportedFields.AtGoNode(f.AST))\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tfieldName := f.Name.MustGet() // guaranteed by f.IsAnonymous check above\n\t\t\t\t\tif _, exists := unusedSegments[fieldName]; !exists {\n\t\t\t\t\t\terrs.Add(errFieldNotUsedInKeyPattern(fieldName).AtGoNode(f.AST).AtGoNode(patternNode))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdelete(unusedSegments, fieldName)\n\t\t\t\t\t}\n\n\t\t\t\t\tif builtin, ok := f.Type.(schema.BuiltinType); ok {\n\t\t\t\t\t\tif err := validateBuiltin(builtin.Kind); err != nil {\n\t\t\t\t\t\t\terrs.Add(errFieldIsInvalid(fieldName, err.Error()).AtGoNode(f.AST))\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\terrs.Add(errFieldIsInvalid(fieldName, \"must be a basic type\").\n\t\t\t\t\t\t\tAtGoNode(f.AST, errors.AsError(fmt.Sprintf(\"found %s\", f.Type))).\n\t\t\t\t\t\t\tAtGoNode(keyNode, errors.AsHelp(\"instantiated here\")))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Ensure all path segments are valid field names\n\t\t\t\tfor fieldName, segment := range unusedSegments {\n\t\t\t\t\terrs.Add(errFieldDoesntExist(fieldName, ref.Decl).AtGoNode(segment).AtGoNode(ref.Decl.AST.Name))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check the value type. We only need to do this for struct types since they need\n\t// to be represented as 'any' constraints. Basic type constructors enforce that the value type\n\t// through the Go type system and don't need to be verified again.\n\tif c.ValueKind == structValue {\n\t\tif ref, ok := schemautil.ResolveNamedStruct(valueType, false); !ok {\n\t\t\terrs.Add(errMustBeANamedStructType.AtGoNode(d.TypeArgs[1].ASTExpr()))\n\t\t} else if ref.Pointers > 0 {\n\t\t\terrs.Add(errStructMustNotBePointer.AtGoNode(d.TypeArgs[1].ASTExpr()))\n\t\t}\n\t}\n\n\tks := &Keyspace{\n\t\tAST:           d.Call,\n\t\tDoc:           d.Doc,\n\t\tFile:          d.File,\n\t\tCluster:       clusterRef,\n\t\tConfigLiteral: cfgLit.Lit(),\n\t\tPath:          path,\n\t\tKeyType:       keyType,\n\t\tValueType:     valueType,\n\t}\n\n\td.Pass.RegisterResource(ks)\n\td.Pass.AddBind(d.File, d.Ident, ks)\n}\n"
  },
  {
    "path": "v2/parser/infra/caches/keyspace_test.go",
    "content": "package caches\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/resourcepaths\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schematest\"\n\t\"encr.dev/v2/parser/resource/resourcetest\"\n)\n\nfunc TestParseKeyspace(t *testing.T) {\n\ttests := []resourcetest.Case[*Keyspace]{\n\t\t{\n\t\t\tName: \"basic\",\n\t\t\tCode: `\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\n// Keyspace docs\nvar x = cache.NewStringKeyspace[string](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \":key\",\n})\n`,\n\t\t\tWant: &Keyspace{\n\t\t\t\tDoc:       \"Keyspace docs\\n\",\n\t\t\t\tKeyType:   schematest.String(),\n\t\t\t\tValueType: schematest.String(),\n\t\t\t\tCluster:   pkginfo.Q(\"example.com\", \"cluster\"),\n\t\t\t\tPath: &resourcepaths.Path{\n\t\t\t\t\tSegments: []resourcepaths.Segment{\n\t\t\t\t\t\t{Type: resourcepaths.Param, Value: \"key\", ValueType: schema.String},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"int\",\n\t\t\tCode: `\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar x = cache.NewIntKeyspace[int64](cluster, cache.KeyspaceConfig{\n\t\tKeyPattern: \"int\",\n\t})\n`,\n\t\t\tWant: &Keyspace{\n\t\t\t\tKeyType:   schematest.Builtin(schema.Int64),\n\t\t\t\tValueType: schematest.Builtin(schema.Int64),\n\t\t\t\tCluster:   pkginfo.Q(\"example.com\", \"cluster\"),\n\t\t\t\tPath: &resourcepaths.Path{\n\t\t\t\t\tSegments: []resourcepaths.Segment{\n\t\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"int\", ValueType: schema.String},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, {\n\t\t\tName: \"float\",\n\t\t\tCode: `\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar x = cache.NewFloatKeyspace[string](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"float\",\n})\n`,\n\t\t\tWant: &Keyspace{\n\t\t\t\tKeyType:   schematest.String(),\n\t\t\t\tValueType: schematest.Builtin(schema.Float64),\n\t\t\t\tCluster:   pkginfo.Q(\"example.com\", \"cluster\"),\n\t\t\t\tPath: &resourcepaths.Path{\n\t\t\t\t\tSegments: []resourcepaths.Segment{\n\t\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"float\", ValueType: schema.String},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"list\",\n\t\t\tCode: `\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar x = cache.NewListKeyspace[string, bool](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"list\",\n})\n`,\n\t\t\tWant: &Keyspace{\n\t\t\t\tKeyType:   schematest.String(),\n\t\t\t\tValueType: schematest.Bool(),\n\t\t\t\tCluster:   pkginfo.Q(\"example.com\", \"cluster\"),\n\t\t\t\tPath: &resourcepaths.Path{\n\t\t\t\t\tSegments: []resourcepaths.Segment{\n\t\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"list\", ValueType: schema.String},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"set\",\n\t\t\tCode: `\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar x = cache.NewSetKeyspace[string, bool](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"set\",\n})\n`,\n\t\t\tWant: &Keyspace{\n\t\t\t\tKeyType:   schematest.String(),\n\t\t\t\tValueType: schematest.Bool(),\n\t\t\t\tCluster:   pkginfo.Q(\"example.com\", \"cluster\"),\n\t\t\t\tPath: &resourcepaths.Path{\n\t\t\t\t\tSegments: []resourcepaths.Segment{\n\t\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"set\", ValueType: schema.String},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"struct\",\n\t\t\tCode: `\ntype Foo struct {\n\tBar string\n}\n\nvar cluster = cache.NewCluster(\"cluster\", cache.ClusterConfig{})\n\nvar x = cache.NewStructKeyspace[string, Foo](cluster, cache.KeyspaceConfig{\n\tKeyPattern: \"struct\",\n})\n`,\n\t\t\tWant: &Keyspace{\n\t\t\t\tKeyType:   schematest.String(),\n\t\t\t\tValueType: schematest.Named(schematest.TypeInfo(\"Foo\")),\n\t\t\t\tCluster:   pkginfo.Q(\"example.com\", \"cluster\"),\n\t\t\t\tPath: &resourcepaths.Path{\n\t\t\t\t\tSegments: []resourcepaths.Segment{\n\t\t\t\t\t\t{Type: resourcepaths.Literal, Value: \"struct\", ValueType: schema.String},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tresourcetest.Run(t, KeyspaceParser, tests)\n}\n"
  },
  {
    "path": "v2/parser/infra/caches/testdata/cluster.txt",
    "content": "parse cache\n\n-- cluster.go --\npackage test\n\nimport \"encore.dev/storage/cache\"\n\nvar a = cache.NewCluster( // res CacheCluster: {\"Name\": \"foo\", \"EvictionPolicy\": \"allkeys-lru\"}\n    \"foo\", cache.ClusterConfig{\n        EvictionPolicy: cache.AllKeysLRU,\n    },\n)\n\nvar b = cache.NewCluster( // ERR invalid \"EvictionPolicy\" value: \"x\"\n    \"foo\", cache.ClusterConfig{\n        EvictionPolicy: \"x\",\n    },\n)\n"
  },
  {
    "path": "v2/parser/infra/caches/usage.go",
    "content": "package caches\n\nimport (\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype KeyspaceUsage struct {\n\tusage.Base\n\n\tKeyspace *Keyspace\n}\n\nfunc ResolveKeyspaceUsage(data usage.ResolveData, keyspace *Keyspace) usage.Usage {\n\treturn &KeyspaceUsage{\n\t\tBase: usage.Base{\n\t\t\tFile: data.Expr.DeclaredIn(),\n\t\t\tBind: data.Expr.ResourceBind(),\n\t\t\tExpr: data.Expr,\n\t\t},\n\t\tKeyspace: keyspace,\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/config/config.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\n// Load represents a config load statement.\ntype Load struct {\n\tAST  *ast.CallExpr\n\tFile *pkginfo.File\n\n\t// Type is the type of the config struct being loaded.\n\t// It's guaranteed to be a (possibly pointer to a) named struct type.\n\tType schema.Type\n\n\t// FuncCall is the AST node that represents the config.Load expression.\n\tFuncCall *ast.CallExpr\n}\n\nfunc (*Load) Kind() resource.Kind         { return resource.ConfigLoad }\nfunc (l *Load) Package() *pkginfo.Package { return l.File.Pkg }\nfunc (l *Load) ASTExpr() ast.Expr         { return l.AST }\nfunc (l *Load) Pos() token.Pos            { return l.AST.Pos() }\nfunc (l *Load) End() token.Pos            { return l.AST.End() }\nfunc (l *Load) SortKey() string {\n\treturn fmt.Sprintf(\"%s:%s:%d\", l.File.Pkg.ImportPath, l.File.Name, l.AST.Pos())\n}\n\nvar LoadParser = &resourceparser.Parser{\n\tName: \"ConfigLoad\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/config\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{PkgPath: \"encore.dev/config\", Name: \"Load\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 1,\n\t\t\tParse:       parseLoad,\n\t\t}\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parseLoad(d parseutil.ReferenceInfo) {\n\terrs := d.Pass.Errs\n\n\tif len(d.Call.Args) > 0 {\n\t\terrs.Add(errInvalidLoad.AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tif len(d.TypeArgs) != 1 {\n\t\terrs.Add(errInvalidConfigType.AtGoNode(d.Call))\n\t}\n\n\t// Resolve the named struct used for the config type\n\tref, ok := schemautil.ResolveNamedStruct(d.TypeArgs[0], false)\n\tif !ok {\n\t\terrs.Add(errInvalidConfigType.AtGoNode(d.TypeArgs[0].ASTExpr()))\n\t\treturn\n\t}\n\n\tload := &Load{\n\t\tAST:      d.Call,\n\t\tFile:     d.File,\n\t\tType:     d.TypeArgs[0],\n\t\tFuncCall: d.Call,\n\t}\n\n\tconcrete := schemautil.ConcretizeWithTypeArgs(errs, ref.ToType(), ref.TypeArgs)\n\twalkCfgToVerify(d.Pass.Errs, load, concrete, false)\n\n\td.Pass.RegisterResource(load)\n\td.Pass.AddBind(d.File, d.Ident, load)\n}\n\nfunc walkCfgToVerify(errs *perr.List, load *Load, decl schema.Type, insideConfigValue bool) {\n\tswitch decl := decl.(type) {\n\tcase schema.BuiltinType:\n\t\t// no-op ok\n\tcase schema.NamedType:\n\t\tif decl.DeclInfo.File.Pkg.ImportPath == \"encore.dev/config\" {\n\t\t\tif insideConfigValue {\n\t\t\t\terrs.Add(errNestedValueUsage.\n\t\t\t\t\tAtGoNode(decl.ASTExpr(), errors.AsError(\"cannot use config.Value inside a config.Value\")).\n\t\t\t\t\tAtGoNode(load, errors.AsHelp(\"config loaded here\")),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tswitch decl.DeclInfo.Name {\n\t\t\tcase \"Value\", \"Values\":\n\t\t\t\t// Value / Values are magic wrappers that are used to indicate a realtime\n\t\t\t\t// config update\n\t\t\t\tif len(decl.TypeArgs) > 0 {\n\t\t\t\t\twalkCfgToVerify(errs, load, decl.TypeArgs[0], true)\n\n\t\t\t\t\t// return so we don't verify the standard type\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tinsideConfigValue = false\n\t\t}\n\n\t\twalkCfgToVerify(errs, load, decl.Decl().Type, insideConfigValue)\n\tcase schema.PointerType:\n\t\twalkCfgToVerify(errs, load, decl.Elem, false)\n\tcase schema.ListType:\n\t\twalkCfgToVerify(errs, load, decl.Elem, false)\n\tcase schema.MapType:\n\t\twalkCfgToVerify(errs, load, decl.Key, false)\n\t\twalkCfgToVerify(errs, load, decl.Value, false)\n\tcase schema.StructType:\n\t\tfor _, field := range decl.Fields {\n\t\t\tif !field.IsExported() {\n\t\t\t\terrs.Add(errUnexportedField.\n\t\t\t\t\tAtGoNode(field.AST).\n\t\t\t\t\tAtGoNode(load, errors.AsHelp(\"config loaded here\")),\n\t\t\t\t)\n\t\t\t} else if field.IsAnonymous() {\n\t\t\t\terrs.Add(errAnonymousField.\n\t\t\t\t\tAtGoNode(field.AST).\n\t\t\t\t\tAtGoNode(load, errors.AsHelp(\"config loaded here\")),\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\twalkCfgToVerify(errs, load, field.Type, false)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\terrs.Add(errInvalidConfigTypeUsed.\n\t\t\tAtGoNode(decl.ASTExpr(), errors.AsError(\"unsupported type\")).\n\t\t\tAtGoNode(load, errors.AsHelp(\"config loaded here\")),\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/config/errors.go",
    "content": "package config\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"config\",\n\t\t\"For more information on configuration, see https://encore.dev/docs/develop/config\",\n\n\t\terrors.WithRangeSize(25),\n\t)\n\n\terrInvalidLoad = errRange.New(\n\t\t\"Invalid call to config.Load[T]()\",\n\t\t\"A call to config.Load[T]() does not accept any arguments.\",\n\t)\n\n\terrInvalidConfigType = errRange.New(\n\t\t\"Invalid call to config.Load[T]()\",\n\t\t\"A call to config.Load[T]() must be passed a named struct type as it's argument.\",\n\t)\n\n\tErrConfigUsedOutsideOfService = errRange.New(\n\t\t\"Invalid call to config.Load[T]()\",\n\t\t\"A call to config.Load[T]() can only be made from within a service.\",\n\t)\n\n\tErrConfigUsedInSubPackage = errRange.New(\n\t\t\"Invalid call to config.Load[T]()\",\n\t\t\"A call to config.Load[T]() can only be made from the top level package of a service.\",\n\t)\n\n\tErrCrossServiceConfigUse = errRange.New(\n\t\t\"Cross service config use\",\n\t\t\"A config instance can only be referenced from within the service that the call to `config.Load[T]()` was made in.\",\n\t)\n\n\terrUnexportedField = errRange.New(\n\t\t\"Invalid config type\",\n\t\t\"Field is not exported and is in a datatype which is used by a call to `config.Load[T]()`. Unexported fields cannot be initialised by Encore, thus are not allowed in this context.\",\n\t)\n\n\terrAnonymousField = errRange.New(\n\t\t\"Invalid config type\",\n\t\t\"Field is an anonymous field and is in a datatype which is used by a call to `config.Load[T]()`. Anonymous fields cannot be initialised by Encore, thus are not allowed in this context.\",\n\t)\n\n\terrInvalidConfigTypeUsed = errRange.New(\n\t\t\"Invalid config type\",\n\t\t\"Types used within data structures which are used by a call to `config.Load[T]()` must either be a built-in type a inline struct or a named struct type.\",\n\t)\n\n\terrNestedValueUsage = errRange.New(\n\t\t\"Invalid config type\",\n\t\t\"The type of config.Value[T] cannot be another config.Value[T]\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/config/usage.go",
    "content": "package config\n\nimport (\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype ReferenceUsage struct {\n\tusage.Base\n\n\t// Load is the config load being used.\n\tCfg *Load\n}\n\nfunc ResolveConfigUsage(data usage.ResolveData, load *Load) usage.Usage {\n\treturn &ReferenceUsage{\n\t\tBase: usage.Base{\n\t\t\tFile: data.Expr.DeclaredIn(),\n\t\t\tBind: data.Expr.ResourceBind(),\n\t\t\tExpr: data.Expr,\n\t\t},\n\t\tCfg: load,\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/crons/cron.go",
    "content": "package crons\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"sort\"\n\n\tcronparser \"github.com/robfig/cron/v3\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\tliterals \"encr.dev/v2/parser/infra/internal/literals\"\n\tparseutil \"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Job struct {\n\tAST      *ast.CallExpr\n\tFile     *pkginfo.File\n\tName     string // The unique name of the cron job\n\tDoc      string // The documentation on the cron job\n\tTitle    string // cron job title\n\tSchedule string\n\n\tEndpoint    pkginfo.QualifiedName // The Endpoint reference\n\tEndpointAST ast.Expr\n}\n\nfunc (j *Job) Kind() resource.Kind       { return resource.CronJob }\nfunc (j *Job) Package() *pkginfo.Package { return j.File.Pkg }\nfunc (j *Job) ASTExpr() ast.Expr         { return j.AST }\nfunc (j *Job) ResourceName() string      { return j.Name }\nfunc (j *Job) Pos() token.Pos            { return j.AST.Pos() }\nfunc (j *Job) End() token.Pos            { return j.AST.End() }\nfunc (j *Job) SortKey() string           { return j.Name }\n\nvar JobParser = &resourceparser.Parser{\n\tName: \"Cron Job\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/cron\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{PkgPath: \"encore.dev/cron\", Name: \"NewJob\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 0,\n\t\t\tParse:       parseCronJob,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nconst (\n\tminute int64 = 60\n\thour         = 60 * minute\n)\n\nvar cronjobParser = cronparser.NewParser(cronparser.Minute | cronparser.Hour | cronparser.Dom | cronparser.Month | cronparser.Dow)\n\nfunc parseCronJob(d parseutil.ReferenceInfo) {\n\tdisplayName := d.ResourceFunc.NaiveDisplayName()\n\tif len(d.Call.Args) != 2 {\n\t\td.Pass.Errs.Add(errExpects2Arguments(len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tjobName := parseutil.ParseResourceName(d.Pass.Errs, displayName, \"cron job name\",\n\t\td.Call.Args[0], parseutil.KebabName, \"\")\n\tif jobName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\tcfgLit, ok := literals.ParseStruct(d.Pass.Errs, d.File, \"cron.JobConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\t// Decode the config\n\ttype decodedConfig struct {\n\t\tTitle    string   `literal:\",optional\"`\n\t\tEndpoint ast.Expr `literal:\",required,dynamic\"`\n\t\tEvery    int64    `literal:\",optional\"`\n\t\tSchedule string   `literal:\",optional\"`\n\t}\n\tconfig := literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, nil)\n\n\t// Resolve the endpoint\n\tendpoint, ok := d.File.Names().ResolvePkgLevelRef(config.Endpoint)\n\tif !ok {\n\t\td.Pass.Errs.Add(\n\t\t\terrUnableToResolveEndpoint.AtGoNode(config.Endpoint),\n\t\t)\n\t\treturn\n\t}\n\n\tjob := &Job{\n\t\tAST:         d.Call,\n\t\tFile:        d.File,\n\t\tName:        jobName,\n\t\tDoc:         d.Doc,\n\t\tTitle:       config.Title,\n\t\tEndpoint:    endpoint,\n\t\tEndpointAST: config.Endpoint,\n\t}\n\tif job.Title == \"\" {\n\t\tjob.Title = jobName\n\t}\n\n\t// Parse the schedule\n\tswitch {\n\tcase config.Every != 0 && config.Schedule != \"\":\n\t\td.Pass.Errs.Add(errScheduleSetTwice.AtGoNode(cfgLit.Expr(\"Schedule\")).AtGoNode(cfgLit.Expr(\"Every\")))\n\t\treturn\n\tcase config.Schedule != \"\":\n\t\t_, err := cronjobParser.Parse(config.Schedule)\n\t\tif err != nil {\n\t\t\td.Pass.Errs.Add(errInvalidSchedule.Wrapping(err).AtGoNode(cfgLit.Expr(\"Schedule\")))\n\t\t\treturn\n\t\t}\n\t\tjob.Schedule = fmt.Sprintf(\"schedule:%s\", config.Schedule)\n\tcase config.Every != 0:\n\t\tif rem := config.Every % minute; rem != 0 {\n\t\t\td.Pass.Errs.Add(errEveryMustBeInteger(config.Every).AtGoNode(cfgLit.Expr(\"Every\")))\n\t\t\treturn\n\t\t}\n\n\t\tminutes := config.Every / minute\n\t\tif minutes < 1 {\n\t\t\td.Pass.Errs.Add(errEveryMustBeOneOrGreater(config.Every).AtGoNode(cfgLit.Expr(\"Every\")))\n\t\t\treturn\n\t\t} else if minutes > 24*60 {\n\t\t\td.Pass.Errs.Add(errEveryMustBeLessThan24Hours(minutes).AtGoNode(cfgLit.Expr(\"Every\")))\n\t\t\treturn\n\t\t} else if suggestion, ok := isCronIntervalAllowed(int(minutes)); !ok {\n\t\t\tsuggestionStr := formatMinutes(suggestion)\n\t\t\tminutesStr := formatMinutes(int(minutes))\n\n\t\t\td.Pass.Errs.Add(\n\t\t\t\terrEveryMustBeMultipleOfMinute(minutesStr).\n\t\t\t\t\tAtGoNode(cfgLit.Expr(\"Every\"), errors.AsHelp(fmt.Sprintf(\"try setting it to %s\", suggestionStr))),\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tjob.Schedule = fmt.Sprintf(\"every:%d\", minutes)\n\t}\n\n\td.Pass.RegisterResource(job)\n\td.Pass.AddBind(d.File, d.Ident, job)\n}\n\n// abs returns the absolute value of x.\nfunc abs(x int) int {\n\tif x < 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc formatMinutes(minutes int) string {\n\tif minutes < 60 {\n\t\treturn fmt.Sprintf(\"%d * cron.Minute\", minutes)\n\t} else if minutes%60 == 0 {\n\t\treturn fmt.Sprintf(\"%d * cron.Hour\", minutes/60)\n\t}\n\treturn fmt.Sprintf(\"%d * cron.Hour + %d * cron.Minute\", minutes/60, minutes%60)\n}\n\nfunc isCronIntervalAllowed(val int) (suggestion int, ok bool) {\n\tallowed := []int{\n\t\t1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, 30, 32, 36, 40, 45,\n\t\t48, 60, 72, 80, 90, 96, 120, 144, 160, 180, 240, 288, 360, 480, 720, 1440,\n\t}\n\tidx := sort.SearchInts(allowed, val)\n\n\tif idx == len(allowed) {\n\t\treturn allowed[len(allowed)-1], false\n\t} else if allowed[idx] == val {\n\t\treturn val, true\n\t} else if idx == 0 {\n\t\treturn allowed[0], false\n\t} else if abs(val-allowed[idx-1]) < abs(val-allowed[idx]) {\n\t\treturn allowed[idx-1], false\n\t}\n\n\treturn allowed[idx], false\n}\n"
  },
  {
    "path": "v2/parser/infra/crons/cron_test.go",
    "content": "package crons\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/resource/resourcetest\"\n)\n\nfunc TestParseJob(t *testing.T) {\n\ttests := []resourcetest.Case[*Job]{\n\t\t{\n\t\t\tName: \"basic\",\n\t\t\tCode: `\n// Job docs\nvar x = cron.NewJob(\"name\", cron.JobConfig{\n\tTitle: \"title\",\n\tEvery: 3 * cron.Hour,\n\tEndpoint: MyEndpoint,\n})\n\nfunc MyEndpoint() {}\n`,\n\t\t\tWant: &Job{\n\t\t\t\tName:     \"name\",\n\t\t\t\tTitle:    \"title\",\n\t\t\t\tDoc:      \"Job docs\\n\",\n\t\t\t\tSchedule: \"every:180\",\n\t\t\t\tEndpoint: pkginfo.Q(\"example.com\", \"MyEndpoint\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"underscore_ident\",\n\t\t\tCode: `\nvar _ = cron.NewJob(\"name\", cron.JobConfig{\n\tEvery: 3 * cron.Hour,\n\tEndpoint: MyEndpoint,\n})\n\nfunc MyEndpoint() {}\n`,\n\t\t\tWant: &Job{\n\t\t\t\tName:     \"name\",\n\t\t\t\tTitle:    \"name\", // defaults from name if not specified\n\t\t\t\tSchedule: \"every:180\",\n\t\t\t\tEndpoint: pkginfo.Q(\"example.com\", \"MyEndpoint\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tresourcetest.Run(t, JobParser, tests)\n}\n"
  },
  {
    "path": "v2/parser/infra/crons/errors.go",
    "content": "package crons\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"cron\",\n\t\t\"For more information, see https://encore.dev/docs/primitives/cron-jobs\",\n\n\t\terrors.WithRangeSize(10),\n\t)\n\n\terrExpects2Arguments = errRange.Newf(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Expected 2 arguments, got %d\",\n\t)\n\n\terrScheduleSetTwice = errRange.New(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"The cron execution schedule was set twice, once in Every and once in Schedule. At least one of these must be set, but not both\",\n\t)\n\n\terrInvalidSchedule = errRange.New(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Schedule must be a valid cron expression\",\n\t)\n\n\terrEveryMustBeInteger = errRange.Newf(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Every must be an integer number of minutes, got %d seconds.\",\n\t)\n\n\terrEveryMustBeOneOrGreater = errRange.Newf(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Every must be between 1 minute and 24 hours, got %d seconds.\",\n\t)\n\n\terrEveryMustBeLessThan24Hours = errRange.Newf(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Every must be between 1 minute and 24 hours (1440 minutes), got %d minutes.\",\n\t)\n\n\terrEveryMustBeMultipleOfMinute = errRange.Newf(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Every 24 hour time range (from 00:00 to 23:59) needs to be evenly divided by the interval value (%s).\",\n\t)\n\n\tErrDuplicateNames = errRange.New(\n\t\t\"Duplicate Cron Jobs\",\n\t\t\"Multiple cron jobs with the same name were found. Cronjob names must be unique.\",\n\t)\n\n\terrUnableToResolveEndpoint = errRange.New(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Unable to resolve endpoint to a package level name. Is it defined?\",\n\t)\n\n\tErrEndpointNotAnAPI = errRange.New(\n\t\t\"Invalid call to cron.NewJob\",\n\t\t\"Endpoint does not reference an Encore API\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/internal/literals/constants.go",
    "content": "package literals\n\nimport (\n\t\"go/constant\"\n\t\"time\"\n\n\t\"encore.dev/storage/cache\"\n\t\"encr.dev/pkg/paths\"\n)\n\nvar constants = map[paths.Pkg]map[string]any{\n\t\"encore.dev/pubsub\": {\n\t\t\"NoRetries\":       -2,\n\t\t\"InfiniteRetries\": -1,\n\t\t\"AtLeastOnce\":     1,\n\t\t\"ExactlyOnce\":     2,\n\t},\n\t\"encore.dev/cron\": {\n\t\t\"Minute\": 60,\n\t\t\"Hour\":   60 * 60,\n\t},\n\t\"encore.dev/storage/cache\": {\n\t\t\"AllKeysLRU\":     string(cache.AllKeysLRU),\n\t\t\"AllKeysLFU\":     string(cache.AllKeysLFU),\n\t\t\"AllKeysRandom\":  string(cache.AllKeysRandom),\n\t\t\"VolatileLRU\":    string(cache.VolatileLRU),\n\t\t\"VolatileLFU\":    string(cache.VolatileLFU),\n\t\t\"VolatileTTL\":    string(cache.VolatileTTL),\n\t\t\"VolatileRandom\": string(cache.VolatileRandom),\n\t\t\"NoEviction\":     string(cache.NoEviction),\n\t},\n\t\"time\": {\n\t\t\"Nanosecond\":  int64(time.Nanosecond),\n\t\t\"Microsecond\": int64(time.Microsecond),\n\t\t\"Millisecond\": int64(time.Millisecond),\n\t\t\"Second\":      int64(time.Second),\n\t\t\"Minute\":      int64(time.Minute),\n\t\t\"Hour\":        int64(time.Hour),\n\t},\n}\n\n// runtimeConstant returns the value of a constant within the runtime,\n// if it's known to this file.\nfunc runtimeConstant(pkg paths.Pkg, name string) (constant.Value, bool) {\n\tpkgMap, found := constants[pkg]\n\tif !found {\n\t\treturn constant.MakeUnknown(), false\n\t}\n\n\tif value, found := pkgMap[name]; found {\n\t\t// constant.Make recognizes int64 but not int.\n\t\t// If we have an int, turn it to int64.\n\t\tif val, ok := value.(int); ok {\n\t\t\treturn constant.Make(int64(val)), true\n\t\t}\n\t\treturn constant.Make(value), true\n\t}\n\treturn constant.MakeUnknown(), false\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/literals/decode.go",
    "content": "package literals\n\nimport (\n\t\"go/ast\"\n\t\"go/constant\"\n\t\"reflect\"\n\n\t\"github.com/fatih/structtag\"\n\n\t\"encr.dev/v2/internals/perr\"\n)\n\nfunc Decode[T any](errs *perr.List, literal *Struct, defaultValues *T) T {\n\tvar decodeTo T\n\tdst := reflect.ValueOf(&decodeTo)\n\tif dst.Elem().Kind() != reflect.Struct {\n\t\terrs.Assert(errArgumentMustBeStruct.AtGoNode(literal.ast))\n\t}\n\tdst = dst.Elem()\n\n\tvar defaultValue reflect.Value\n\tif defaultValues != nil {\n\t\tdefaultValue = reflect.ValueOf(defaultValues).Elem()\n\t}\n\n\tfieldPaths := decodeStruct(errs, literal, dst, defaultValue)\n\n\tdecodedFields := make(map[string]bool)\n\tfor _, fp := range fieldPaths {\n\t\tdecodedFields[fp] = true\n\t}\n\n\t// Make sure all the fields we care about have been decoded and nothing else.\n\tfor _, f := range literal.FieldPaths() {\n\t\tif !decodedFields[f] {\n\t\t\terrs.Add(errUnexpectedField.AtGoNode(literal.Expr(f)))\n\t\t}\n\t}\n\n\treturn decodeTo\n}\n\nfunc decodeStruct(errs *perr.List, literal *Struct, dst reflect.Value, defaultValues reflect.Value) (fieldPaths []string) {\n\tfor i := 0; i < dst.NumField(); i++ {\n\t\tfieldType := dst.Type().Field(i)\n\t\tif fieldType.Anonymous {\n\t\t\terrs.Assert(errAnonymousFieldsNotSupported)\n\t\t} else if !ast.IsExported(fieldType.Name) {\n\t\t\terrs.Assert(errUnexportedFieldsNotSupported)\n\t\t}\n\n\t\tvar fieldDefault reflect.Value\n\t\tif defaultValues.IsValid() {\n\t\t\tfieldDefault = defaultValues.Field(i)\n\t\t}\n\n\t\tpaths := decodeField(errs, literal, fieldType, dst.Field(i), fieldDefault)\n\t\tfieldPaths = append(fieldPaths, paths...)\n\t}\n\treturn fieldPaths\n}\n\n// decodeField decodes a single constant literal field.\n// It reports the field path it decoded.\nfunc decodeField(errs *perr.List, literal *Struct, fieldType reflect.StructField, field reflect.Value, defaultField reflect.Value) (fieldPaths []string) {\n\t// Determine the path we want to find the field at\n\tfieldPath := fieldType.Name\n\n\trequired := false\n\tdynamicOK := false\n\tzeroValueOK := false\n\tuseDefaultValue := false\n\n\ttag, err := structtag.Parse(string(fieldType.Tag))\n\tif err != nil {\n\t\terrs.Assert(errInvalidTag.AtGoNode(literal.Expr(fieldPath)).Wrapping(err))\n\t}\n\tif tagOpts, err := tag.Get(\"literal\"); err == nil {\n\t\tif tagOpts.Name != \"\" {\n\t\t\tfieldPath = tagOpts.Name\n\t\t}\n\t\trequired = !tagOpts.HasOption(\"optional\")\n\t\tdynamicOK = tagOpts.HasOption(\"dynamic\")\n\t\tzeroValueOK = tagOpts.HasOption(\"zero-ok\")\n\t\tuseDefaultValue = tagOpts.HasOption(\"default\")\n\t}\n\n\tfieldPaths = []string{fieldPath}\n\n\t// If the field is required and isn't set, return an error.\n\tif isSet := literal.IsSet(fieldPath); !isSet {\n\t\tif required {\n\t\t\tpos := literal.Pos(fieldPath)\n\t\t\terrs.Add(errMissingRequiredField(fieldPath).AtGoPos(pos, pos))\n\t\t\treturn\n\t\t} else {\n\t\t\tif useDefaultValue && defaultField.IsValid() {\n\t\t\t\t// If the field is optional and we have a default value, use it.\n\t\t\t\tfield.Set(defaultField)\n\t\t\t}\n\t\t\t// Nothing to do\n\t\t\treturn\n\t\t}\n\t}\n\n\t// If the field is not dynamic and we don't allow dynamic fields, return an error.\n\tisDynamic := !literal.IsConstant(fieldPath)\n\tif isDynamic && !dynamicOK {\n\t\terrs.Add(errIsntConstant(fieldPath).AtGoNode(literal.Expr(fieldPath)))\n\t\treturn\n\t} else if isDynamic {\n\t\t// Make sure the type is Expr.\n\t\tif field.Type().PkgPath() != \"go/ast\" || field.Type().Name() != \"Expr\" {\n\t\t\terrs.Assert(errDyanmicFieldNotExpr.AtGoNode(literal.Expr(fieldPath)))\n\t\t}\n\t\tfield.Set(reflect.ValueOf(literal.Expr(fieldPath)))\n\t\treturn\n\t}\n\n\tval := literal.ConstantValue(fieldPath)\n\tswitch fieldType.Type.Kind() {\n\tcase reflect.String:\n\t\tif val.Kind() == constant.String {\n\t\t\tfield.SetString(constant.StringVal(val))\n\t\t} else {\n\t\t\terrs.Add(errWrongDynamicType(fieldPath, \"string\").AtGoNode(literal.Expr(fieldPath)))\n\t\t}\n\n\tcase reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64:\n\t\tif val.Kind() == constant.Int {\n\t\t\tn, _ := constant.Int64Val(val)\n\t\t\tfield.SetInt(n)\n\t\t} else {\n\t\t\terrs.Add(errWrongDynamicType(fieldPath, \"integer\").AtGoNode(literal.Expr(fieldPath)))\n\t\t}\n\n\tcase reflect.Bool:\n\t\tif val.Kind() == constant.Bool {\n\t\t\tfield.SetBool(constant.BoolVal(val))\n\t\t} else {\n\t\t\terrs.Add(errWrongDynamicType(fieldPath, \"boolean\").AtGoNode(literal.Expr(fieldPath)))\n\t\t}\n\n\tcase reflect.Struct:\n\t\tchild, ok := literal.ChildStruct(fieldPath)\n\t\tif !ok {\n\t\t\terrs.Add(errWrongDynamicType(fieldPath, \"inline struct\").AtGoNode(literal.Expr(fieldPath)))\n\t\t\treturn\n\t\t}\n\t\tchildPaths := decodeStruct(errs, child, field, defaultField)\n\t\tfor _, p := range childPaths {\n\t\t\tfieldPaths = append(fieldPaths, fieldPath+\".\"+p)\n\t\t}\n\t\treturn fieldPaths\n\n\tdefault:\n\t\terrs.Assert(errUnsupportedType(fieldType.Type.Kind()).AtGoNode(literal.Expr(fieldPath)))\n\t}\n\n\t// Now that we've set the value, if the field is required make sure it's not the zero value.\n\tif required && !zeroValueOK && field.IsZero() {\n\t\terrs.Add(errZeroValue(fieldPath).AtGoNode(literal.Expr(fieldPath)))\n\t}\n\n\treturn fieldPaths\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/literals/decode_test.go",
    "content": "package literals\n\nimport (\n\t\"go/ast\"\n\t\"testing\"\n\t\"time\"\n\n\tqt \"github.com/frankban/quicktest\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestDecode(t *testing.T) {\n\tc := qt.New(t)\n\ttc := testutil.NewContext(c, false, testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- foo.go --\npackage foo\n\nimport (\"time\"; \"encore.dev/pubsub\")\n\nvar x = pubsub.SubscriptionConfig{\n\tAckDeadline: 45 * time.Second,\n\tMessageRetention: 5 * time.Hour * 24 + -10 * time.Hour,\n\tRetryPolicy: &pubsub.RetryPolicy{\n\t\tMaxRetries: 3,\n\t\tMinBackoff: 8 * time.Second,\n\t\tMaxBackoff: 32 * time.Minute,\n\t},\n}\n`))\n\ttc.FailTestOnErrors()\n\ttc.GoModTidy()\n\n\tloader := pkginfo.New(tc.Context)\n\tpkg := loader.MustLoadPkg(0, \"example.com\")\n\n\tcfgLit, ok := ParseStruct(tc.Errs, pkg.Files[0], \"pubsub.SubscriptionConfig\",\n\t\tpkg.Names().PkgDecls[\"x\"].Spec.(*ast.ValueSpec).Values[0])\n\tc.Assert(ok, qt.IsTrue)\n\n\ttype decodedConfig struct {\n\t\t// Optional configuration\n\t\tAckDeadline      time.Duration `literal:\",optional\"`\n\t\tMessageRetention time.Duration `literal:\",optional\"`\n\t\tRetryPolicy      struct {\n\t\t\tMinRetryBackoff time.Duration `literal:\"MinBackoff,optional\"`\n\t\t\tMaxRetryBackoff time.Duration `literal:\"MaxBackoff,optional\"`\n\t\t\tMaxRetries      int           `literal:\"MaxRetries,optional\"`\n\t\t} `literal:\",optional\"`\n\t}\n\n\tcfg := Decode[decodedConfig](tc.Errs, cfgLit, nil)\n\n\tc.Assert(cfg, qt.DeepEquals, decodedConfig{\n\t\tAckDeadline:      45 * time.Second,\n\t\tMessageRetention: 5*time.Hour*24 + -10*time.Hour,\n\t\tRetryPolicy: struct {\n\t\t\tMinRetryBackoff time.Duration `literal:\"MinBackoff,optional\"`\n\t\t\tMaxRetryBackoff time.Duration `literal:\"MaxBackoff,optional\"`\n\t\t\tMaxRetries      int           `literal:\"MaxRetries,optional\"`\n\t\t}{\n\t\t\tMaxRetries:      3,\n\t\t\tMinRetryBackoff: 8 * time.Second,\n\t\t\tMaxRetryBackoff: 32 * time.Minute,\n\t\t},\n\t})\n\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/literals/errors.go",
    "content": "package literals\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"literals\",\n\t\t\"\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrArgumentMustBeStruct = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"The argument must be given as a struct literal.\",\n\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrUnexpectedField = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Unexpected field in struct literal.\",\n\t)\n\n\terrAnonymousFieldsNotSupported = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Anonymous fields are not supported.\",\n\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrUnexportedFieldsNotSupported = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Unexported fields are not supported.\",\n\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrInvalidTag = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Invalid tag on field\",\n\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrMissingRequiredField = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"Missing required field `%s`\",\n\t)\n\n\terrIsntConstant = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"Field `%s` must be a constant literal.\",\n\t)\n\n\terrDyanmicFieldNotExpr = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Dynamic field must be an expression.\",\n\t)\n\n\terrWrongDynamicType = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"The field `%s` must be a %s literal.\",\n\t)\n\n\terrUnsupportedType = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"Unsupported type %v.\",\n\t)\n\n\terrZeroValue = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"The field `%s` must not be the zero value.\",\n\t)\n\n\terrNotLiteral = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"Expected a literal instance of %s, got %s.\",\n\t)\n\n\terrExpectedKeyToBeIdentifier = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"Expected key to be an identifier, got %v.\",\n\t)\n\n\terrExpectedKeyPair = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"Expected key pair, got %v.\",\n\t)\n\n\terrPanicParsingExpression = errRange.Newf(\n\t\t\"Internal Error\",\n\t\t\"Unexpected panic while parsing expression: %v\",\n\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrUnableToParseLiteral = errRange.New(\n\t\t\"Internal Error\",\n\t\t\"Unable to parse literal.\",\n\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\terrDivideByZero = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Cannot divide by zero.\",\n\t)\n\n\terrInvalidShift = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Shift count must be an unsigned integer.\",\n\t)\n\n\terrUnsupportedOperation = errRange.Newf(\n\t\t\"Invalid Argument\",\n\t\t\"%s is an unsupported operation here.\",\n\t)\n\n\terrDefaultTagNotSupported = errRange.New(\n\t\t\"Invalid Argument\",\n\t\t\"Default tags are not supported.\",\n\t\terrors.MarkAsInternalError(),\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/internal/literals/literals.go",
    "content": "package literals\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/constant\"\n\t\"go/token\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\nvar noOpCasts = map[paths.Pkg][]string{\n\t\"encore.dev/cron\": {\"Duration\"},\n\t\"time\":            {\"Duration\"},\n}\n\n// ParseString parses the node as a string literal.\n// If it's not a string literal it reports \"\", false.\nfunc ParseString(node ast.Node) (string, bool) {\n\tif lit, ok := node.(*ast.BasicLit); ok && lit.Kind == token.STRING {\n\t\tif val, err := strconv.Unquote(lit.Value); err == nil {\n\t\t\treturn val, true\n\t\t}\n\t}\n\treturn \"\", false\n}\n\n// ParseStruct parses struct literal and returns a LiterialStruct object\n//\n// If there is a nested struct literal, then it's values will be nested in a dot syntax; i.e. `parentStructFieldName.childStructFieldName`\nfunc ParseStruct(errs *perr.List, file *pkginfo.File, expectedType string, node ast.Expr) (lit *Struct, ok bool) {\n\tcl, ok := node.(*ast.CompositeLit)\n\tif !ok {\n\t\terrs.Add(errNotLiteral(expectedType, PrettyPrint(node)).AtGoNode(node))\n\t\treturn nil, false\n\t}\n\n\tlit = &Struct{\n\t\tast:            cl,\n\t\tconstantFields: make(map[string]constant.Value),\n\t\tallFields:      make(map[string]ast.Expr),\n\t\tchildStructs:   make(map[string]*Struct),\n\t}\n\tok = true\n\nelemLoop:\n\tfor _, elem := range cl.Elts {\n\t\tswitch elem := elem.(type) {\n\t\tcase *ast.KeyValueExpr:\n\t\t\tident, ok := elem.Key.(*ast.Ident)\n\t\t\tif !ok {\n\t\t\t\terrs.Add(errExpectedKeyToBeIdentifier(reflect.TypeOf(elem.Key)).AtGoNode(elem.Key))\n\t\t\t\tcontinue elemLoop\n\t\t\t}\n\t\t\tif ident == nil {\n\t\t\t\terrs.Add(errExpectedKeyToBeIdentifier(\"nil\").AtGoNode(elem.Key))\n\t\t\t\tcontinue elemLoop\n\t\t\t}\n\n\t\t\t// Parse any sub data structures\n\t\t\tvar subStruct *ast.CompositeLit\n\t\t\tswitch value := elem.Value.(type) {\n\t\t\tcase *ast.UnaryExpr:\n\t\t\t\tif value.Op == token.AND {\n\t\t\t\t\tif compositeLiteral, ok := value.X.(*ast.CompositeLit); ok {\n\t\t\t\t\t\tsubStruct = compositeLiteral\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase *ast.CompositeLit:\n\t\t\t\tsubStruct = value\n\t\t\t}\n\n\t\t\tif subStruct != nil {\n\t\t\t\tsubLit, subOk := ParseStruct(errs, file, \"struct\", subStruct)\n\t\t\t\tok = ok && subOk\n\t\t\t\tlit.childStructs[ident.Name] = subLit\n\t\t\t} else if valueIdent, ok := elem.Value.(*ast.Ident); ok && valueIdent.Name == \"nil\" {\n\t\t\t\t// no-op for nil's\n\t\t\t} else {\n\t\t\t\t// Parse the value\n\t\t\t\tlit.allFields[ident.Name] = elem.Value\n\t\t\t\tvalue := ParseConstant(errs, file, elem.Value)\n\t\t\t\tif value.Kind() != constant.Unknown {\n\t\t\t\t\tlit.constantFields[ident.Name] = value\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\terrs.Add(errExpectedKeyPair(reflect.TypeOf(elem)).AtGoNode(elem))\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc ParseConstant(errs *perr.List, file *pkginfo.File, value ast.Expr) (rtn constant.Value) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\trtn = constant.MakeUnknown()\n\t\t\terrs.Add(errPanicParsingExpression(r).AtGoNode(value))\n\t\t}\n\t}()\n\n\tswitch value := value.(type) {\n\tcase *ast.FuncLit:\n\t\t// Functions are not literal constant values\n\t\treturn constant.MakeUnknown()\n\n\tcase *ast.Ident:\n\t\tswitch value.Name {\n\t\tcase \"true\":\n\t\t\treturn constant.MakeBool(true)\n\t\tcase \"false\":\n\t\t\treturn constant.MakeBool(false)\n\t\tdefault:\n\t\t\treturn constant.MakeUnknown()\n\t\t}\n\n\tcase *ast.BasicLit:\n\t\tv, err := basicLit(value)\n\t\tif err != nil {\n\t\t\terrs.Add(errUnableToParseLiteral.Wrapping(err).AtGoNode(value))\n\t\t\treturn constant.MakeUnknown()\n\t\t} else {\n\t\t\treturn v\n\t\t}\n\n\tcase *ast.SelectorExpr:\n\t\tif obj, ok := file.Names().ResolvePkgLevelRef(value); ok {\n\t\t\tif v, found := runtimeConstant(obj.PkgPath, obj.Name); found {\n\t\t\t\treturn v\n\t\t\t}\n\t\t}\n\n\t\treturn constant.MakeUnknown()\n\n\tcase *ast.BinaryExpr:\n\t\tlhs := ParseConstant(errs, file, value.X)\n\t\trhs := ParseConstant(errs, file, value.Y)\n\t\tif lhs.Kind() == constant.Unknown || rhs.Kind() == constant.Unknown {\n\t\t\treturn constant.MakeUnknown()\n\t\t}\n\n\t\tswitch value.Op {\n\t\tcase token.MUL, token.ADD, token.SUB, token.REM, token.AND, token.OR, token.XOR, token.AND_NOT:\n\t\t\treturn constant.BinaryOp(lhs, value.Op, rhs)\n\t\tcase token.QUO:\n\t\t\t// constant.BinaryOp panics when dividing by zero\n\t\t\tif floatValue, _ := constant.Float64Val(constant.ToFloat(rhs)); floatValue <= 0.000000001 && floatValue >= -0.000000001 {\n\t\t\t\terrs.Add(errDivideByZero.AtGoNode(value))\n\t\t\t\treturn constant.MakeUnknown()\n\t\t\t}\n\n\t\t\treturn constant.BinaryOp(lhs, value.Op, rhs)\n\t\tcase token.EQL, token.NEQ, token.LSS, token.LEQ, token.GTR, token.GEQ:\n\t\t\treturn constant.MakeBool(constant.Compare(lhs, value.Op, rhs))\n\n\t\tcase token.SHL, token.SHR:\n\t\t\tshiftValue, ok := constant.Uint64Val(constant.ToInt(rhs))\n\t\t\tif !ok {\n\t\t\t\terrs.Add(errInvalidShift.AtGoNode(value))\n\t\t\t}\n\t\t\treturn constant.Shift(lhs, value.Op, uint(shiftValue))\n\n\t\tdefault:\n\t\t\terrs.Add(errUnsupportedOperation(value.Op).AtGoNode(value))\n\t\t\treturn constant.MakeUnknown()\n\t\t}\n\n\tcase *ast.UnaryExpr:\n\t\tx := ParseConstant(errs, file, value.X)\n\t\treturn constant.UnaryOp(value.Op, x, 0)\n\n\tcase *ast.CallExpr:\n\t\t// We allow casts like \"time.Duration(143)\" or \"cron.Duration(143)\"\n\t\t// so we transparently go through them\n\t\tif sel, ok := value.Fun.(*ast.SelectorExpr); ok && len(value.Args) == 1 {\n\t\t\tif obj, ok := file.Names().ResolvePkgLevelRef(sel); ok {\n\t\t\t\tif pkgFuncs, found := noOpCasts[obj.PkgPath]; found {\n\t\t\t\t\tfor _, allowed := range pkgFuncs {\n\t\t\t\t\t\tif allowed == obj.Name {\n\t\t\t\t\t\t\treturn ParseConstant(errs, file, value.Args[0])\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn constant.MakeUnknown()\n\n\tcase *ast.ParenExpr:\n\t\treturn ParseConstant(errs, file, value.X)\n\n\tdefault:\n\t\terrs.Add(errUnsupportedType(reflect.TypeOf(value).Kind()).AtGoNode(value))\n\t\treturn constant.MakeUnknown()\n\t}\n}\n\nfunc basicLit(value *ast.BasicLit) (constant.Value, error) {\n\tswitch value.Kind {\n\tcase token.IDENT:\n\t\treturn constant.MakeString(value.Value), nil\n\tcase token.INT:\n\t\tv, err := strconv.ParseInt(value.Value, 10, 64)\n\t\tif err != nil {\n\t\t\treturn constant.MakeUnknown(), err\n\t\t}\n\t\treturn constant.MakeInt64(v), nil\n\tcase token.FLOAT:\n\t\tv, err := strconv.ParseFloat(value.Value, 64)\n\t\tif err != nil {\n\t\t\treturn constant.MakeUnknown(), err\n\t\t}\n\t\treturn constant.MakeFloat64(v), nil\n\tcase token.CHAR:\n\t\tc, _, _, err := strconv.UnquoteChar(value.Value, value.Value[0])\n\t\treturn constant.MakeFromBytes([]byte{byte(c)}), err\n\tcase token.STRING:\n\t\treturn constant.MakeString(value.Value[1 : len(value.Value)-1]), nil\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported literal type\")\n\t}\n}\n\n// Struct represents a struct literal at compile time\ntype Struct struct {\n\tast            *ast.CompositeLit         // The AST node which presents the literal\n\tconstantFields map[string]constant.Value // All found constant expressions\n\tallFields      map[string]ast.Expr       // All field expressions (constant or otherwise)\n\tchildStructs   map[string]*Struct        // Any child struct literals\n}\n\nfunc (l *Struct) Lit() *ast.CompositeLit {\n\treturn l.ast\n}\n\n// FullyConstant returns true if every value in this struct and the child structs fully known as compile time\n// as a constant value\nfunc (l *Struct) FullyConstant() bool {\n\tfor _, sub := range l.childStructs {\n\t\tif !sub.FullyConstant() {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn len(l.constantFields) == len(l.allFields)\n}\n\n// DynamicFields returns the names of the fields and ast.Expr that are not constant\n//\n// Child structs will be included with the field name prefixed with the struct name;\n// i.e. `parentField.childField`\nfunc (l *Struct) DynamicFields() map[string]ast.Expr {\n\tfields := make(map[string]ast.Expr)\n\tfor name, expr := range l.allFields {\n\t\tif _, found := l.constantFields[name]; !found {\n\t\t\tfields[name] = expr\n\t\t}\n\t}\n\n\tfor name, sub := range l.childStructs {\n\t\tfor k, v := range sub.DynamicFields() {\n\t\t\tfields[name+\".\"+k] = v\n\t\t}\n\t}\n\n\treturn fields\n}\n\n// IsSet returns true if the given field is set in this struct\n//\n// You can reference a child struct field with `.`; i.e. `parent.child`\nfunc (l *Struct) IsSet(fieldName string) bool {\n\t// Recurse into child fields\n\tbefore, after, found := strings.Cut(fieldName, \".\")\n\tif found {\n\t\tif child, found := l.childStructs[before]; found {\n\t\t\treturn child.IsSet(after)\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t} else if _, found := l.childStructs[fieldName]; found {\n\t\treturn true\n\t}\n\n\t_, found = l.allFields[fieldName]\n\treturn found\n}\n\nfunc (l *Struct) IsConstant(fieldName string) bool {\n\t// Recurse into child fields\n\tbefore, after, found := strings.Cut(fieldName, \".\")\n\tif found {\n\t\tif child, found := l.childStructs[before]; found {\n\t\t\treturn child.IsConstant(after)\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t} else if child, found := l.childStructs[fieldName]; found {\n\t\treturn child.FullyConstant()\n\t}\n\n\t_, found = l.constantFields[fieldName]\n\treturn found\n}\n\nfunc (l *Struct) ChildStruct(fieldName string) (st *Struct, ok bool) {\n\tst, ok = l.childStructs[fieldName]\n\treturn\n}\n\n// Pos returns the position of the field in the source code\n//\n// If the field is not found, the closest position to where\n// the field should have been will be returned\n//\n// You can reference a child struct field with `.`; i.e. `parent.child`\nfunc (l *Struct) Pos(fieldName string) token.Pos {\n\tbefore, after, found := strings.Cut(fieldName, \".\")\n\tif found {\n\t\tif child, found := l.childStructs[before]; found {\n\t\t\treturn child.Pos(after)\n\t\t} else {\n\t\t\treturn l.ast.Pos()\n\t\t}\n\t}\n\n\tvalue, found := l.allFields[fieldName]\n\tif found {\n\t\treturn value.Pos()\n\t} else {\n\t\treturn l.ast.Pos()\n\t}\n}\n\n// FieldPaths reports all field paths in the struct,\n// recursively including child structs (in the `parent.child` syntax).\nfunc (l *Struct) FieldPaths() []string {\n\tvar res []string\n\tfor name := range l.allFields {\n\t\tres = append(res, name)\n\t}\n\tfor name, child := range l.childStructs {\n\t\tfor _, path := range child.FieldPaths() {\n\t\t\tres = append(res, name+\".\"+path)\n\t\t}\n\t}\n\treturn res\n}\n\n// Expr returns ast.Expr for the given field name.\n//\n// If the field is known, it returns the ast.Expr\n// If the field is not known, it returns nil\n//\n// You can reference a child struct field with `.`; i.e. `parent.child`\nfunc (l *Struct) Expr(fieldName string) ast.Expr {\n\t// Recurse into child fields\n\tbefore, after, found := strings.Cut(fieldName, \".\")\n\tif found {\n\t\tif child, found := l.childStructs[before]; found {\n\t\t\treturn child.Expr(after)\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tvalue, found := l.allFields[fieldName]\n\tif found {\n\t\treturn value\n\t} else {\n\t\treturn nil\n\t}\n}\n\n// ConstantValue returns the value of the field as a constant.Value. If the field is not constant or\n// does not exist, an unknown value will be returned\n//\n// You can reference a child struct field with `.`; i.e. `parent.child`\nfunc (l *Struct) ConstantValue(fieldName string) constant.Value {\n\t// Recurse into child fields\n\tbefore, after, found := strings.Cut(fieldName, \".\")\n\tif found {\n\t\tif child, found := l.childStructs[before]; found {\n\t\t\treturn child.ConstantValue(after)\n\t\t} else {\n\t\t\treturn constant.MakeUnknown()\n\t\t}\n\t}\n\n\tvalue, found := l.constantFields[fieldName]\n\tif !found {\n\t\treturn constant.MakeUnknown()\n\t}\n\treturn value\n}\n\n// Int64 returns the value of the field as an int64\n//\n// This function will convert other number types into an Int64, but will not convert strings.\n// If after conversion the value is 0, the defaultValue will be returned\n//\n// You can reference a child struct field with `.`; i.e. `parent.child`\nfunc (l *Struct) Int64(fieldName string, defaultValue int64) int64 {\n\trealValue, ok := constant.Int64Val(constant.ToInt(l.ConstantValue(fieldName)))\n\tif !ok || realValue == 0 {\n\t\treturn defaultValue\n\t}\n\treturn realValue\n}\n\n// Str returns the value of the field as an string\n//\n// This function will convert all types to a string\n// If after conversion the value is \"\", the defaultValue will be returned\n//\n// You can reference a child struct field with `.`; i.e. `parent.child`\nfunc (l *Struct) Str(fieldName string, defaultValue string) string {\n\tvalue := l.ConstantValue(fieldName)\n\n\tstr := value.ExactString()\n\tif value.Kind() == constant.String || value.Kind() == constant.Unknown {\n\t\tstr = constant.StringVal(value)\n\n\t}\n\n\tif str == \"\" {\n\t\treturn defaultValue\n\t} else {\n\t\treturn str\n\t}\n}\n\nfunc PrettyPrint(node ast.Expr) string {\n\tswitch node := node.(type) {\n\tcase *ast.Ident:\n\t\treturn node.Name\n\n\tcase *ast.SelectorExpr:\n\t\treturn fmt.Sprintf(\"%s.%s\", PrettyPrint(node.X), node.Sel.Name)\n\n\tcase *ast.IndexExpr:\n\t\treturn fmt.Sprintf(\"%s[%s]\", PrettyPrint(node.X), PrettyPrint(node.Index))\n\n\tcase *ast.IndexListExpr:\n\t\tindices := make([]string, 0, len(node.Indices))\n\t\tfor _, n := range node.Indices {\n\t\t\tindices = append(indices, PrettyPrint(n))\n\t\t}\n\t\treturn fmt.Sprintf(\"%s[%s]\", PrettyPrint(node.X), strings.Join(indices, \", \"))\n\n\tcase *ast.FuncLit:\n\t\treturn \"a function literal\"\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(node))\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/literals/literals_test.go",
    "content": "package literals\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/constant\"\n\t\"go/token\"\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/testutil\"\n)\n\nfunc TestParseConstantValue(t *testing.T) {\n\ttype testCase struct {\n\t\tExpr    string\n\t\tImports []string\n\t\tWant    any\n\t\tErr     string\n\t}\n\n\ttests := []testCase{\n\t\t{\n\t\t\tExpr: \"true\",\n\t\t\tWant: true,\n\t\t},\n\t\t{\n\t\t\tExpr: \"false\",\n\t\t\tWant: false,\n\t\t},\n\t\t{\n\t\t\tExpr: \"\\\"hello world\\\"\",\n\t\t\tWant: \"hello world\",\n\t\t},\n\t\t{\n\t\t\tExpr: \"\\\"hello world\\\" == \\\"hello world\\\"\",\n\t\t\tWant: true,\n\t\t},\n\t\t{\n\t\t\tExpr: \"\\\"hello world\\\" != \\\"hello world\\\"\",\n\t\t\tWant: false,\n\t\t},\n\t\t{\n\t\t\tImports: []string{\"encore.dev/cron\"},\n\t\t\tExpr:    \"1*cron.Minute\",\n\t\t\tWant:    1 * 60,\n\t\t},\n\t\t{\n\t\t\tImports: []string{\"encore.dev/cron\"},\n\t\t\tExpr:    \"(4/2)*cron.Minute\",\n\t\t\tWant:    2 * 60,\n\t\t},\n\t\t{\n\t\t\tImports: []string{\"encore.dev/cron\"},\n\t\t\tExpr:    \"(4-2)*cron.Minute + cron.Hour\",\n\t\t\tWant:    2*60 + 3600,\n\t\t},\n\t\t{\n\t\t\tExpr: \"5 / 2\",\n\t\t\tWant: 2.5,\n\t\t},\n\t\t{\n\t\t\tExpr: \"(5 - 4) - 1\",\n\t\t\tWant: int64(0),\n\t\t},\n\t\t// Note the \"(?s)\" allows for \".\" to match newlines\n\t\t// This is needed when running tests with the tag `dev_build` which includes\n\t\t// stack traces from the parser in the error message.\n\t\t{\n\t\t\tExpr: \"2.3 / 0\",\n\t\t\tErr:  `(?s).+Cannot divide by zero.*`,\n\t\t},\n\t}\n\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test testCase) *txtar.Archive {\n\t\timports := \"\"\n\t\tif len(test.Imports) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range test.Imports {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\nconst x = ` + test.Expr + `\n`)\n\t}\n\n\tfor i, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"test[%d]\", i), func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tdecl := pkg.Names().PkgDecls[\"x\"]\n\t\t\tx := decl.Spec.(*ast.ValueSpec).Values[0]\n\n\t\t\tvalue := ParseConstant(tc.Errs, decl.File, x)\n\n\t\t\tif test.Err != \"\" {\n\t\t\t\tc.Check(value.Kind(), qt.Equals, constant.Unknown, qt.Commentf(\"Wanted an Unknown value, got: %s\", value.Kind().String()))\n\t\t\t\tc.Check(tc.Errs.Len(), qt.Not(qt.Equals), 0)\n\t\t\t\tc.Check(tc.Errs.FormatErrors(), qt.Matches, test.Err)\n\t\t\t} else {\n\t\t\t\tc.Check(tc.Errs.Len(), qt.Equals, 0)\n\t\t\t\tc.Check(value.Kind(), qt.Not(qt.Equals), constant.Unknown, qt.Commentf(\"Result was unknown: %s\", tc.Errs.FormatErrors()))\n\n\t\t\t\tswitch w := test.Want.(type) {\n\t\t\t\tcase bool:\n\t\t\t\t\tc.Check(constant.BoolVal(value), qt.Equals, test.Want)\n\t\t\t\tcase string:\n\t\t\t\t\tc.Check(constant.StringVal(value), qt.Equals, test.Want)\n\t\t\t\tcase float64:\n\t\t\t\t\tgot, _ := constant.Float64Val(constant.ToFloat(value))\n\t\t\t\t\tc.Check(got, qt.Equals, test.Want)\n\t\t\t\tcase int:\n\t\t\t\t\tgot, _ := constant.Int64Val(constant.ToInt(value))\n\t\t\t\t\tc.Check(got, qt.Equals, int64(w))\n\t\t\t\tcase int64:\n\t\t\t\t\tgot, _ := constant.Int64Val(constant.ToInt(value))\n\t\t\t\t\tc.Check(got, qt.Equals, test.Want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/locations/locations.go",
    "content": "package locations\n\nimport (\n\t\"go/ast\"\n)\n\ntype Classification interface {\n\tclassification()\n}\n\ntype PkgVar struct {\n\tIdent *ast.Ident\n\tName  string\n}\n\ntype InFunc struct {\n\tNode ast.Node // *ast.FuncDecl or *ast.FuncLit\n}\n\ntype OtherPkgExpr struct{}\n\nfunc (PkgVar) classification()       {}\nfunc (InFunc) classification()       {}\nfunc (OtherPkgExpr) classification() {}\n\n// Classify classifies the current location based on the ancestor stack.\n// The last node in the stack is the expression to classify.\nfunc Classify(stack []ast.Node) (c Classification) {\n\tnum := len(stack)\n\n\t// target is the target expression\n\ttarget := stack[num-1]\n\n\t// First determine if we're inside any function declaration,\n\t// as that takes precedence over any other classification.\n\tfor i := num - 2; i >= 0; i-- {\n\t\tswitch node := stack[i].(type) {\n\t\tcase *ast.FuncDecl, *ast.FuncLit:\n\t\t\treturn InFunc{Node: node}\n\t\t}\n\t}\n\n\t// Iterate over the node stack from inside to out.\n\tfor i := num - 2; i >= 0; i-- {\n\t\tswitch node := stack[i].(type) {\n\n\t\tcase *ast.ValueSpec:\n\t\t\tfor n := 0; n < len(node.Names); n++ {\n\t\t\t\tif len(node.Values) > n && node.Values[n] == target {\n\t\t\t\t\t// We've found the value that contains the resource.\n\t\t\t\t\treturn PkgVar{\n\t\t\t\t\t\tIdent: node.Names[n],\n\t\t\t\t\t\tName:  node.Names[n].Name,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn OtherPkgExpr{}\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/locations/locations_test.go",
    "content": "package locations\n\nimport (\n\t\"go/ast\"\n\t\"go/parser\"\n\t\"go/token\"\n\t\"slices\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"golang.org/x/tools/go/ast/inspector\"\n)\n\nfunc TestClassify(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\texpr string\n\t\twant Classification\n\t}{\n\t\t{\n\t\t\tname: \"simple_pkg_var\",\n\t\t\texpr: \"var x = TARGET\",\n\t\t\twant: PkgVar{Ident: ast.NewIdent(\"x\"), Name: \"x\"},\n\t\t},\n\t\t{\n\t\t\tname: \"field_lookup\",\n\t\t\texpr: \"var x = TARGET.Stdlib\",\n\t\t\twant: OtherPkgExpr{},\n\t\t},\n\t\t{\n\t\t\tname: \"method_call\",\n\t\t\texpr: \"var x = TARGET.Stdlib()\",\n\t\t\twant: OtherPkgExpr{},\n\t\t},\n\t\t{\n\t\t\tname: \"method_call_in_func_lit\",\n\t\t\texpr: \"var x = func() { TARGET.Stdlib() }\",\n\t\t\twant: InFunc{Node: &ast.FuncLit{}},\n\t\t},\n\t\t{\n\t\t\tname: \"method_call_in_func_lit_call\",\n\t\t\texpr: \"var x = func() { TARGET.Stdlib() }()\",\n\t\t\twant: InFunc{Node: &ast.FuncLit{}},\n\t\t},\n\t\t{\n\t\t\tname: \"in_func_decl\",\n\t\t\texpr: \"func foo() { TARGET }\",\n\t\t\twant: InFunc{Node: &ast.FuncDecl{}},\n\t\t},\n\t\t{\n\t\t\tname: \"func_arg\",\n\t\t\texpr: \"var x = foo(blah, TARGET)\",\n\t\t\twant: OtherPkgExpr{},\n\t\t},\n\t\t{\n\t\t\tname: \"func_arg_method\",\n\t\t\texpr: \"var x = foo(TARGET.Stdlib())\",\n\t\t\twant: OtherPkgExpr{},\n\t\t},\n\t\t{\n\t\t\tname: \"unary_expr\",\n\t\t\texpr: \"var x = +TARGET\",\n\t\t\twant: OtherPkgExpr{},\n\t\t},\n\t}\n\n\tcmpOpts := []cmp.Option{\n\t\tcmpopts.IgnoreInterfaces(struct {\n\t\t\tast.Expr\n\t\t\tast.Stmt\n\t\t}{}),\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\tstack := parseStack(c, test.expr)\n\t\t\tgot := Classify(stack)\n\t\t\tc.Assert(got, qt.CmpEquals(cmpOpts...), test.want)\n\t\t})\n\t}\n}\n\nfunc parseStack(c *qt.C, expr string) []ast.Node {\n\tcode := \"package pkg\\n\\n\" + expr\n\tfs := token.NewFileSet()\n\tfile, err := parser.ParseFile(fs, c.Name()+\".go\", code, parser.ParseComments)\n\tc.Assert(err, qt.IsNil)\n\n\tinsp := inspector.New([]*ast.File{file})\n\n\tvar found []ast.Node\n\tinsp.WithStack([]ast.Node{(*ast.Ident)(nil)}, func(n ast.Node, push bool, stack []ast.Node) bool {\n\t\tif id, ok := stack[len(stack)-1].(*ast.Ident); ok && id.Name == \"TARGET\" {\n\t\t\tfound = slices.Clone(stack)\n\t\t}\n\t\treturn true\n\t})\n\tc.Assert(found, qt.Not(qt.IsNil))\n\treturn found\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/parseutil/aststringer.go",
    "content": "package parseutil\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n// Converts a node to a string which looks like the original go code.\n// such as a ast.SelectorExpr will become \"foo.Blah\"\n//\n// It's not intended to be an exact representation, but rather a helperful\n// representation for error messages.\nfunc nodeAsGoSrc(node ast.Node) string {\n\tswitch node := node.(type) {\n\tcase *ast.Ident:\n\t\treturn node.Name\n\n\tcase *ast.SelectorExpr:\n\t\treturn fmt.Sprintf(\"%s.%s\", nodeAsGoSrc(node.X), node.Sel.Name)\n\n\tcase *ast.IndexExpr:\n\t\treturn fmt.Sprintf(\"%s[%s]\", nodeAsGoSrc(node.X), nodeAsGoSrc(node.Index))\n\n\tcase *ast.IndexListExpr:\n\t\tindices := make([]string, 0, len(node.Indices))\n\t\tfor _, n := range node.Indices {\n\t\t\tindices = append(indices, nodeAsGoSrc(n))\n\t\t}\n\t\treturn fmt.Sprintf(\"%s[%s]\", nodeAsGoSrc(node.X), strings.Join(indices, \", \"))\n\n\tcase *ast.FuncLit:\n\t\treturn \"a function literal\"\n\n\tcase *ast.BasicLit:\n\t\treturn node.Value\n\n\tcase *ast.CallExpr:\n\t\treturn fmt.Sprintf(\"%s(...)\", nodeAsGoSrc(node.Fun))\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(node))\n\t}\n}\n\n// NodeType converts a node to a string that can be used in an error message.\n// such as a ast.CallExpr will return \"a function call to foo.Blah\"\nfunc NodeType(node ast.Node) string {\n\tswitch node := node.(type) {\n\tcase *ast.Ident:\n\t\treturn \"an identifier\"\n\n\tcase *ast.SelectorExpr:\n\t\treturn \"an identifier\"\n\n\tcase *ast.IndexExpr:\n\t\treturn \"a identifier\"\n\n\tcase *ast.IndexListExpr:\n\t\treturn \"a identifier\"\n\n\tcase *ast.FuncLit:\n\t\treturn \"a function literal\"\n\n\tcase *ast.BasicLit:\n\t\tswitch node.Kind {\n\t\tcase token.INT:\n\t\t\treturn \"an integer literal\"\n\t\tcase token.FLOAT:\n\t\t\treturn \"a float literal\"\n\t\tcase token.IMAG:\n\t\t\treturn \"an imaginary literal\"\n\t\tcase token.CHAR:\n\t\t\treturn \"a character literal\"\n\t\tcase token.STRING:\n\t\t\treturn \"a string literal\"\n\t\tdefault:\n\t\t\treturn \"a literal\"\n\t\t}\n\n\tcase *ast.CallExpr:\n\t\treturn fmt.Sprintf(\"a function call to %s\", nodeAsGoSrc(node.Fun))\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(node))\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/parseutil/errors.go",
    "content": "package parseutil\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"parseutil\",\n\t\t\"\",\n\t)\n\n\terrRequiresTypeArgumentsNoneFound = errRange.Newf(\n\t\t\"Missing type arguments\",\n\t\t\"%s requires type arguments, but none were found.\",\n\t)\n\n\terrWrongNumberOfTypeArguments = errRange.Newf(\n\t\t\"Wrong number of type arguments\",\n\t\t\"%s requires%s %d type arguments, but %d were found.\",\n\t)\n\n\terrCannotBeReferencedWithoutBeingCalled = errRange.Newf(\n\t\t\"Invalid reference\",\n\t\t\"%s cannot be referenced without being called.\",\n\t)\n\n\terrCannotBeCalledHere = errRange.Newf(\n\t\t\"Invalid call\",\n\t\t\"%s cannot be called here. It must be called from %s.\",\n\t)\n\n\terrResourceNameMustBeStringLiteral = errRange.Newf(\n\t\t\"Invalid resource name\",\n\t\t\"A %s requires the %s given as a string literal.\",\n\t)\n\n\terrResourceNameInvalidLength = errRange.Newf(\n\t\t\"Invalid resource name\",\n\t\t\"The %s %s needs to be between 1 and 63 characters long.\",\n\t)\n\n\terrResourceNameNotCorrectFormat = errRange.Newf(\n\t\t\"Invalid resource name\",\n\t\t\"The %s %s must be defined in \\\"%s\\\".\",\n\t)\n\n\terrResourceNameReserved = errRange.Newf(\n\t\t\"Invalid resource name\",\n\t\t\"The %s %s %q used the reserved prefix %q.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/internal/parseutil/names.go",
    "content": "package parseutil\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errinsrc/srcerrors\"\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/parser/infra/internal/literals\"\n)\n\nconst resourceNameMaxLength int = 63\n\ntype resourceNameSpec struct {\n\tregexp         *regexp.Regexp\n\terrDetails     func(resourceName, paramName string) string\n\tinvalidNameErr func(node ast.Node, resourceName, paramName, name string) errors.Template\n\treservedErr    func(fset *token.FileSet, node ast.Node, resourceType, paramName, name, reservedPrefix string) error\n}\n\nvar KebabName = resourceNameSpec{\n\tregexp:     regexp.MustCompile(`^[a-z]([-a-z0-9]*[a-z0-9])?$`),\n\terrDetails: resourceNameHelpKebabCase,\n\tinvalidNameErr: func(node ast.Node, resourceType, paramName, name string) errors.Template {\n\t\terr := errResourceNameNotCorrectFormat(resourceType, paramName, \"kebab-case\").\n\t\t\tWithDetails(resourceNameHelpKebabCase(resourceType, paramName)).\n\t\t\tAtGoNode(node, errors.AsError(fmt.Sprintf(\"try %s?\", idents.GenerateSuggestion(name, idents.KebabCase))))\n\n\t\treturn err\n\t},\n\treservedErr: func(fset *token.FileSet, node ast.Node, resourceType, paramName, name, reservedPrefix string) error {\n\t\treturn srcerrors.ResourceNameReserved(fset, node, resourceType, paramName, name, reservedPrefix, false)\n\t},\n}\n\nvar SnakeName = resourceNameSpec{\n\tregexp:     regexp.MustCompile(`^[a-z]([_a-z0-9]*[a-z0-9])?$`),\n\terrDetails: resourceNameHelpSnakeCase,\n\tinvalidNameErr: func(node ast.Node, resourceType, paramName, name string) errors.Template {\n\t\terr := errResourceNameNotCorrectFormat(resourceType, paramName, \"snake_case\").\n\t\t\tWithDetails(resourceNameHelpSnakeCase(resourceType, paramName)).\n\t\t\tAtGoNode(node, errors.AsError(fmt.Sprintf(\"try %s?\", idents.GenerateSuggestion(name, idents.SnakeCase))))\n\n\t\treturn err\n\t},\n\treservedErr: func(fset *token.FileSet, node ast.Node, resourceType, paramName, name, reservedPrefix string) error {\n\t\treturn srcerrors.ResourceNameReserved(fset, node, resourceType, paramName, name, reservedPrefix, true)\n\t},\n}\n\n// ParseResourceName checks the given node is a string literal\n// and that it conforms to the given spec.\n//\n// If an error is encountered, it will report a parse error and return an empty string\n// otherwise it will return the parsed resource name\nfunc ParseResourceName(errs *perr.List, resourceType string, paramName string, node ast.Expr, nameSpec resourceNameSpec, reservedPrefix string) string {\n\tname, ok := literals.ParseString(node)\n\tif !ok {\n\t\terrs.Add(\n\t\t\terrResourceNameMustBeStringLiteral(resourceType, paramName).\n\t\t\t\tWithDetails(nameSpec.errDetails(resourceType, paramName)).\n\t\t\t\tAtGoNode(node, errors.AsError(fmt.Sprintf(\"was given %s\", NodeType(node)))),\n\t\t)\n\t\treturn \"\"\n\t}\n\tname = strings.TrimSpace(name)\n\tif name == \"\" || len(name) > resourceNameMaxLength {\n\t\terrs.Add(\n\t\t\terrResourceNameInvalidLength(resourceType, paramName).\n\t\t\t\tWithDetails(nameSpec.errDetails(resourceType, paramName)).\n\t\t\t\tAtGoNode(node, errors.AsError(fmt.Sprintf(\"is %d long\", len(name)))),\n\t\t)\n\t\treturn \"\"\n\t}\n\n\tif !nameSpec.regexp.MatchString(name) {\n\t\terrs.Add(nameSpec.invalidNameErr(node, resourceType, paramName, name))\n\t\treturn \"\"\n\t} else if reservedPrefix != \"\" && strings.HasPrefix(name, reservedPrefix) {\n\t\terrs.Add(\n\t\t\terrResourceNameReserved(resourceType, paramName, name, reservedPrefix).\n\t\t\t\tWithDetails(nameSpec.errDetails(resourceType, paramName)).\n\t\t\t\tAtGoNode(node),\n\t\t)\n\t\treturn \"\"\n\t}\n\n\treturn name\n}\n\nfunc resourceNameHelpKebabCase(resourceName string, paramName string) string {\n\treturn fmt.Sprintf(\"%s %s's must be defined as string literals, \"+\n\t\t\"be between 1 and 63 characters long, and defined in \\\"kebab-case\\\", meaning it must start with a letter, end with a letter \"+\n\t\t\"or number and only contain lower case letters, numbers and dashes.\",\n\t\tresourceName, paramName,\n\t)\n}\n\nfunc resourceNameHelpSnakeCase(resourceName string, paramName string) string {\n\treturn fmt.Sprintf(\"%s %s's must be defined as string literals, \"+\n\t\t\"be between 1 and 63 characters long, and defined in \\\"snake_case\\\", meaning it must start with a letter, end with a letter \"+\n\t\t\"or number and only contain lower case letters, numbers and underscores.\",\n\t\tresourceName, paramName,\n\t)\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/parseutil/parseutil.go",
    "content": "package parseutil\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\n// FindPkgNameRefs finds all references in the given package that references\n// any of the given pkgNames. For each such reference it calls fn.\nfunc FindPkgNameRefs(pkg *pkginfo.Package, pkgNames []pkginfo.QualifiedName, fn func(f *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node)) {\n\t// Scan files that contain the required imports.\n\trequiredImports := computeRequiredImports(pkgNames)\n\t// If the union of all imports is not a subset of the package's imports,\n\t// might as well not even look at individual files.\n\tif !hasRequiredImports(pkg.Imports, requiredImports) {\n\t\treturn\n\t}\n\n\t// Turn cfg.Funcs into a lookup table.\n\twantNames := make(map[pkginfo.QualifiedName]bool, len(pkgNames))\n\tfor _, name := range pkgNames {\n\t\twantNames[name] = true\n\t}\n\n\tfor _, file := range pkg.Files {\n\t\tif !hasRequiredImports(file.Imports, requiredImports) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// We have a file that contains the required imports.\n\t\t// Scan it for resource creation calls.\n\t\tinspector := file.ASTInspector()\n\t\tfileNames := file.Names()\n\n\t\t// nodeFilter are the AST types we care about inspecting.\n\t\tnodeFilter := []ast.Node{(*ast.SelectorExpr)(nil)}\n\n\t\t// Walk the AST to find references. Use stack information to resolve whether a particular\n\t\t// reference is in a valid location.\n\t\tinspector.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {\n\t\t\t// If we're popping the stack, there's nothing to do.\n\t\t\tif !push {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// We have a reference to a selector. See if that selector\n\t\t\t// references the constructors we care about.\n\t\t\tsel := n.(*ast.SelectorExpr) // guaranteed based on our node filter.\n\t\t\tname, ok := fileNames.ResolvePkgLevelRef(sel)\n\t\t\tif !ok {\n\t\t\t\t// Not a package-level reference.\n\t\t\t\treturn true\n\t\t\t} else if !wantNames[name] {\n\t\t\t\t// Not a reference we care about. Keep recursing.\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tfn(file, name, stack)\n\t\t\treturn true\n\t\t})\n\t}\n}\n\n// computeRequiredImports computes the required imports based on\n// a list of func names to look for.\nfunc computeRequiredImports(funcs []pkginfo.QualifiedName) []paths.Pkg {\n\tvar result []paths.Pkg\n\tseen := make(map[paths.Pkg]bool)\n\tfor _, f := range funcs {\n\t\tif !seen[f.PkgPath] {\n\t\t\tseen[f.PkgPath] = true\n\t\t\tresult = append(result, f.PkgPath)\n\t\t}\n\t}\n\treturn result\n}\n\n// hasRequiredImports reports whether the given imports set\n// contains all the required imports.\nfunc hasRequiredImports(imports map[paths.Pkg]ast.Node, required []paths.Pkg) bool {\n\tfor _, pkg := range required {\n\t\tif _, found := imports[pkg]; !found {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// resolveTypeArgs resolves the type argument expressions for the given node.\nfunc resolveTypeArgs(node ast.Node) []ast.Expr {\n\tswitch n := node.(type) {\n\tcase *ast.IndexExpr:\n\t\treturn []ast.Expr{n.Index}\n\tcase *ast.IndexListExpr:\n\t\treturn n.Indices\n\t}\n\treturn nil\n}\n\nfunc resolveResourceDoc(stack []ast.Node) (doc string) {\n\tgetDoc := func(candidates ...*ast.CommentGroup) string {\n\t\tfor _, cg := range candidates {\n\t\t\tif t := cg.Text(); t != \"\" {\n\t\t\t\treturn t\n\t\t\t}\n\t\t}\n\t\treturn \"\"\n\t}\n\n\tfor i := len(stack) - 1; i >= 0; i-- {\n\t\tswitch node := stack[i].(type) {\n\t\tcase *ast.Field:\n\t\t\tif cmt := getDoc(node.Doc, node.Comment); cmt != \"\" {\n\t\t\t\treturn cmt\n\t\t\t}\n\t\tcase *ast.ValueSpec:\n\t\t\tif cmt := getDoc(node.Doc, node.Comment); cmt != \"\" {\n\t\t\t\treturn cmt\n\t\t\t}\n\n\t\tcase *ast.GenDecl:\n\t\t\tif cmt := getDoc(node.Doc); cmt != \"\" {\n\t\t\t\treturn cmt\n\t\t\t}\n\n\t\tcase *ast.Comment:\n\t\t\tif node == nil {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn node.Text\n\n\t\tcase *ast.CommentGroup:\n\t\t\treturn getDoc(node)\n\n\t\tcase *ast.BlockStmt, *ast.StructType, *ast.InterfaceType, *ast.FuncType:\n\t\t\treturn \"\"\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "v2/parser/infra/internal/parseutil/reference.go",
    "content": "package parseutil\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/infra/internal/locations\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype ReferenceSpec struct {\n\tMinTypeArgs int\n\tMaxTypeArgs int\n\tParse       func(ReferenceInfo)\n}\n\ntype ReferenceInfo struct {\n\tPass         *resourceparser.Pass\n\tResourceFunc pkginfo.QualifiedName\n\tFile         *pkginfo.File\n\n\tStack    []ast.Node\n\tCall     *ast.CallExpr\n\tTypeArgs []schema.Type\n\tDoc      string\n\n\t// Ident is the identifier this reference is assigned to, if any.\n\tIdent option.Option[*ast.Ident]\n}\n\ntype ReferenceData struct {\n\tFile         *pkginfo.File\n\tStack        []ast.Node\n\tResourceFunc pkginfo.QualifiedName\n}\n\nfunc ParseReference(p *resourceparser.Pass, spec *ReferenceSpec, data ReferenceData) {\n\tselIdx := len(data.Stack) - 1\n\tconstructor := data.ResourceFunc\n\n\t// Do we have any type arguments?\n\tmaybeHasTypeArgs := spec.MaxTypeArgs > 0\n\n\t// If we have any type arguments it will be in the parent of the selector.\n\tvar typeArgs []schema.Type\n\tif maybeHasTypeArgs {\n\t\ttypeArgsIdx := selIdx - 1\n\t\tif typeArgsIdx < 0 {\n\t\t\tp.Errs.Add(errRequiresTypeArgumentsNoneFound(constructor.NaiveDisplayName()).AtGoNode(data.Stack[selIdx]))\n\t\t\treturn\n\t\t}\n\t\targs := resolveTypeArgs(data.Stack[typeArgsIdx])\n\t\tif len(args) < spec.MinTypeArgs {\n\t\t\tqualifier := \" at least\"\n\t\t\tif spec.MinTypeArgs == spec.MaxTypeArgs {\n\t\t\t\tqualifier = \"\"\n\t\t\t}\n\n\t\t\tp.Errs.Add(errWrongNumberOfTypeArguments(constructor.NaiveDisplayName(), qualifier, spec.MinTypeArgs, len(args)).AtGoNode(data.Stack[selIdx]))\n\t\t\treturn\n\t\t} else if len(args) > spec.MaxTypeArgs {\n\t\t\tqualifier := \" at most\"\n\t\t\tif spec.MinTypeArgs == spec.MaxTypeArgs {\n\t\t\t\tqualifier = \"\"\n\t\t\t}\n\t\t\tp.Errs.Add(errWrongNumberOfTypeArguments(constructor.NaiveDisplayName(), qualifier, spec.MaxTypeArgs, len(args)).AtGoNode(data.Stack[selIdx]))\n\t\t}\n\t\tfor _, arg := range args {\n\t\t\ttypeArgs = append(typeArgs, p.SchemaParser.ParseType(data.File, arg))\n\t\t}\n\t}\n\n\t// Make sure the reference is called\n\tcallIdx := selIdx - 1\n\tif len(typeArgs) > 0 {\n\t\t// If there are type arguments there's an intermediary IndexExpr or IndexListExpr node.\n\t\tcallIdx--\n\t}\n\tcall, ok := data.Stack[callIdx].(*ast.CallExpr)\n\tif !ok {\n\t\tp.Errs.Add(errCannotBeReferencedWithoutBeingCalled(constructor.NaiveDisplayName()).AtGoNode(data.Stack[selIdx]))\n\t\treturn\n\t}\n\n\t// Classify the location of the current node\n\tcls := locations.Classify(data.Stack[:callIdx+1])\n\n\tvar pkgIdent *ast.Ident\n\tswitch cls := cls.(type) {\n\tcase locations.PkgVar:\n\t\tpkgIdent = cls.Ident\n\tcase locations.OtherPkgExpr:\n\t\t// Allowed; not assigned to a variable\n\tdefault:\n\t\t// Everything else is disallowed\n\t\tp.Errs.Add(errCannotBeCalledHere(constructor.NaiveDisplayName(), \"a package level variable\").AtGoNode(data.Stack[selIdx]))\n\t\treturn\n\t}\n\n\tspec.Parse(ReferenceInfo{\n\t\tPass:         p,\n\t\tFile:         data.File,\n\t\tStack:        data.Stack,\n\t\tIdent:        option.AsOptional(pkgIdent),\n\t\tCall:         call,\n\t\tTypeArgs:     typeArgs,\n\t\tDoc:          resolveResourceDoc(data.Stack),\n\t\tResourceFunc: data.ResourceFunc,\n\t})\n}\n"
  },
  {
    "path": "v2/parser/infra/metrics/errors.go",
    "content": "package metrics\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"metrics\",\n\t\t\"For more information on metrics, see https://encore.dev/docs/observability/metrics\",\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrInvalidArgCount = errRange.Newf(\n\t\t\"Invalid metric construction\",\n\t\t\"%s requires 2 arguments; the metric name and the config object, got %d arguments.\",\n\t)\n\n\terrInvalidMetricType = errRange.New(\n\t\t\"Invalid metric construction\",\n\t\t\"The metric value type must be a builtin type.\",\n\t)\n\n\terrInvalidLabelType = errRange.New(\n\t\t\"Invalid metric construction\",\n\t\t\"The metric label type must be a named struct type.\",\n\t)\n\n\terrLabelNoPointer = errRange.New(\n\t\t\"Invalid metric construction\",\n\t\t\"The metric label type must not be a pointer.\",\n\t)\n\n\terrLabelNoAnonymous = errRange.New(\n\t\t\"Invalid metric label type\",\n\t\t\"Anonymous fields are not supported in metric labels.\",\n\t)\n\n\terrLabelInvalidType = errRange.New(\n\t\t\"Invalid metric label type\",\n\t\t\"Invalid metric label field: must be string, bool, or integer type.\",\n\t)\n\n\terrLabelReservedName = errRange.New(\n\t\t\"Invalid metric label name\",\n\t\t\"Metric labels cannot be named 'service' as this is reserved by Encore.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/metrics/metrics.go",
    "content": "package metrics\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/idents\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\tliterals \"encr.dev/v2/parser/infra/internal/literals\"\n\tparseutil \"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\n//go:generate stringer -type=MetricType -output=metrics_string.go\n\ntype MetricType int\n\nconst (\n\tCounter MetricType = iota\n\tGauge\n)\n\ntype Metric struct {\n\tAST  *ast.CallExpr\n\tName string     // The unique name of the metric\n\tDoc  string     // The documentation on the metric\n\tType MetricType // the type of metric it is\n\n\t// File is the file the metric is declared in.\n\tFile *pkginfo.File\n\n\t// LabelType is the label type of the metric,\n\t// if the metric is a group.\n\tLabelType option.Option[schema.Type]\n\n\t// Labels is the list of parsed labels.\n\tLabels []Label\n\n\tValueType schema.BuiltinType\n\n\t// The struct literal for the config. Used to inject additional configuration\n\t// at compile-time.\n\tConfigLiteral *ast.CompositeLit\n}\n\nfunc (m *Metric) Kind() resource.Kind       { return resource.Metric }\nfunc (m *Metric) Package() *pkginfo.Package { return m.File.Pkg }\nfunc (m *Metric) ASTExpr() ast.Expr         { return m.AST }\nfunc (m *Metric) ResourceName() string      { return m.Name }\nfunc (m *Metric) Pos() token.Pos            { return m.AST.Pos() }\nfunc (m *Metric) End() token.Pos            { return m.AST.End() }\nfunc (m *Metric) SortKey() string           { return m.Name }\n\ntype Label struct {\n\tKey  string\n\tType schema.BuiltinType\n\tDoc  string\n}\n\nfunc (l Label) String() string {\n\treturn fmt.Sprintf(\"%s %s %s\", l.Key, strings.ToUpper(l.Type.String()), l.Doc)\n}\n\n// metricConstructor describes a particular metric constructor function.\ntype metricConstructor struct {\n\tFuncName    string\n\tConfigName  string\n\tConfigParse configParseFunc\n\tHasLabels   bool\n\tType        MetricType\n}\n\nvar metricConstructors = []metricConstructor{\n\t{\"NewCounter\", \"CounterConfig\", parseCounterConfig, false, Counter},\n\t{\"NewCounterGroup\", \"CounterConfig\", parseCounterConfig, true, Counter},\n\t{\"NewGauge\", \"GaugeConfig\", parseGaugeConfig, false, Gauge},\n\t{\"NewGaugeGroup\", \"GaugeConfig\", parseGaugeConfig, true, Gauge},\n}\n\nvar MetricParser = &resourceparser.Parser{\n\tName: \"Metric\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/metrics\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tvar (\n\t\t\tnames []pkginfo.QualifiedName\n\t\t\tspecs = make(map[pkginfo.QualifiedName]*parseutil.ReferenceSpec)\n\t\t)\n\t\tfor _, c := range metricConstructors {\n\t\t\tname := pkginfo.QualifiedName{PkgPath: \"encore.dev/metrics\", Name: c.FuncName}\n\t\t\tnames = append(names, name)\n\n\t\t\tnumTypeArgs := 1\n\t\t\tif c.HasLabels {\n\t\t\t\tnumTypeArgs = 2\n\t\t\t}\n\n\t\t\tc := c // capture for closure\n\t\t\tparseFn := func(d parseutil.ReferenceInfo) {\n\t\t\t\tparseMetric(c, d)\n\t\t\t}\n\n\t\t\tspec := &parseutil.ReferenceSpec{\n\t\t\t\tMinTypeArgs: numTypeArgs,\n\t\t\t\tMaxTypeArgs: numTypeArgs,\n\t\t\t\tParse:       parseFn,\n\t\t\t}\n\t\t\tspecs[name] = spec\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, names, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tspec := specs[name]\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parseMetric(c metricConstructor, d parseutil.ReferenceInfo) {\n\tdisplayName := d.ResourceFunc.NaiveDisplayName()\n\terrs := d.Pass.Errs\n\tif len(d.Call.Args) != 2 {\n\t\terrs.Add(errInvalidArgCount(displayName, len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\t// Validate the metric name.\n\tmetricName := parseutil.ParseResourceName(errs, displayName, \"metric name\",\n\t\td.Call.Args[0], parseutil.SnakeName, \"e_\")\n\tif metricName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\t// Validate the metric value type.\n\tvalueType := d.TypeArgs[0]\n\tif c.HasLabels {\n\t\tvalueType = d.TypeArgs[1]\n\t}\n\tif valueType.Family() != schema.Builtin {\n\t\terrs.Add(errInvalidMetricType.AtGoNode(valueType.ASTExpr()))\n\t\treturn\n\t}\n\n\tvar labelType option.Option[schema.Type]\n\tvar labelFields []Label\n\tif c.HasLabels {\n\t\t// Make sure it's a named struct, without pointers.\n\t\ttypeArg := d.TypeArgs[0]\n\t\tdeclRef, ok := schemautil.ResolveNamedStruct(typeArg, false)\n\t\tif !ok {\n\t\t\terrs.Add(errInvalidLabelType.AtGoNode(typeArg.ASTExpr()))\n\t\t\treturn\n\t\t} else if declRef.Pointers > 0 {\n\t\t\terrs.Add(errLabelNoPointer.AtGoNode(typeArg.ASTExpr()))\n\t\t\treturn\n\t\t}\n\n\t\t// Make sure all the fields are builtin types.\n\t\tconcrete := schemautil.ConcretizeWithTypeArgs(errs, declRef.Decl.Type, declRef.TypeArgs).(schema.StructType)\n\t\tvalidKinds := append([]schema.BuiltinKind{schema.Bool, schema.String}, schemautil.Integers...)\n\t\tfor _, f := range concrete.Fields {\n\t\t\tif f.IsAnonymous() {\n\t\t\t\terrs.Add(errLabelNoAnonymous.AtGoNode(f.AST))\n\t\t\t} else if !schemautil.IsBuiltinKind(f.Type, validKinds...) {\n\t\t\t\terrs.Add(errLabelInvalidType.AtGoNode(f.AST.Type, errors.AsError(fmt.Sprintf(\"got %s\", literals.PrettyPrint(f.AST.Type)))))\n\t\t\t} else {\n\t\t\t\t// Validate the label\n\t\t\t\tlabel := idents.Convert(f.Name.MustGet(), idents.SnakeCase)\n\t\t\t\tif label == \"service\" {\n\t\t\t\t\terrs.Add(errLabelReservedName.AtGoNode(f.AST.Names[0]))\n\t\t\t\t}\n\n\t\t\t\tlabelFields = append(labelFields, Label{\n\t\t\t\t\tKey:  label,\n\t\t\t\t\tType: f.Type.(schema.BuiltinType),\n\t\t\t\t\tDoc:  f.Doc,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tlabelType = option.Some(typeArg)\n\t}\n\n\tm := &Metric{\n\t\tAST:       d.Call,\n\t\tName:      metricName,\n\t\tDoc:       d.Doc,\n\t\tType:      c.Type,\n\t\tFile:      d.File,\n\t\tValueType: valueType.(schema.BuiltinType),\n\t\tLabelType: labelType,\n\t\tLabels:    labelFields,\n\t}\n\n\t// Parse and validate the metric configuration.\n\tcfgLit, ok := literals.ParseStruct(errs, d.File, \"metrics.MetricConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\tc.ConfigParse(c, d, cfgLit, m)\n\tm.ConfigLiteral = cfgLit.Lit()\n\n\td.Pass.RegisterResource(m)\n\td.Pass.AddBind(d.File, d.Ident, m)\n}\n\ntype configParseFunc func(c metricConstructor, d parseutil.ReferenceInfo, cfgLit *literals.Struct, dst *Metric)\n\nfunc parseCounterConfig(c metricConstructor, d parseutil.ReferenceInfo, cfgLit *literals.Struct, dst *Metric) {\n\t// We don't have any actual configuration yet.\n\t// Parse anyway to make sure we don't have any fields we don't expect.\n\ttype decodedConfig struct{}\n\t_ = literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, nil)\n}\n\nfunc parseGaugeConfig(c metricConstructor, d parseutil.ReferenceInfo, cfgLit *literals.Struct, dst *Metric) {\n\t// We don't have any actual configuration yet.\n\t// Parse anyway to make sure we don't have any fields we don't expect.\n\ttype decodedConfig struct{}\n\t_ = literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, nil)\n}\n"
  },
  {
    "path": "v2/parser/infra/metrics/metrics_string.go",
    "content": "// Code generated by \"stringer -type=MetricType -output=metrics_string.go\"; DO NOT EDIT.\n\npackage metrics\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[Counter-0]\n\t_ = x[Gauge-1]\n}\n\nconst _MetricType_name = \"CounterGauge\"\n\nvar _MetricType_index = [...]uint8{0, 7, 12}\n\nfunc (i MetricType) String() string {\n\tif i < 0 || i >= MetricType(len(_MetricType_index)-1) {\n\t\treturn \"MetricType(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _MetricType_name[_MetricType_index[i]:_MetricType_index[i+1]]\n}\n"
  },
  {
    "path": "v2/parser/infra/metrics/metrics_test.go",
    "content": "package metrics\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\n\t\"encr.dev/v2/internals/schema/schematest\"\n\t\"encr.dev/v2/parser/resource/resourcetest\"\n)\n\nfunc TestParseMetrics(t *testing.T) {\n\ttests := []resourcetest.Case[*Metric]{\n\t\t{\n\t\t\tName: \"counter\",\n\t\t\tCode: `\n// Metric docs\nvar x = metrics.NewCounter[int](\"name\", metrics.CounterConfig{})\n`,\n\t\t\tWant: &Metric{\n\t\t\t\tName:      \"name\",\n\t\t\t\tDoc:       \"Metric docs\\n\",\n\t\t\t\tValueType: schematest.Int(),\n\t\t\t\tType:      Counter,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"gauge\",\n\t\t\tCode: `\n// Metric docs\nvar x = metrics.NewGauge[int](\"name\", metrics.GaugeConfig{})\n`,\n\t\t\tWant: &Metric{\n\t\t\t\tName:      \"name\",\n\t\t\t\tDoc:       \"Metric docs\\n\",\n\t\t\t\tType:      Gauge,\n\t\t\t\tValueType: schematest.Int(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"counter_group\",\n\t\t\tCode: `\n// Metric docs\nvar x = metrics.NewCounterGroup[Labels, int](\"name\", metrics.CounterConfig{})\n\ntype Labels struct {\n\tID string\n}\n`,\n\t\t\tWant: &Metric{\n\t\t\t\tName:      \"name\",\n\t\t\t\tDoc:       \"Metric docs\\n\",\n\t\t\t\tType:      Counter,\n\t\t\t\tLabels:    []Label{{Key: \"id\", Type: schematest.String()}},\n\t\t\t\tValueType: schematest.Int(),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"gauge_group\",\n\t\t\tCode: `\n// Metric docs\nvar x = metrics.NewGaugeGroup[Labels, int](\"name\", metrics.GaugeConfig{})\n\ntype Labels struct {\n\tID string\n}\n`,\n\t\t\tWant: &Metric{\n\t\t\t\tName:      \"name\",\n\t\t\t\tDoc:       \"Metric docs\\n\",\n\t\t\t\tLabels:    []Label{{Key: \"id\", Type: schematest.String()}},\n\t\t\t\tValueType: schematest.Int(),\n\t\t\t\tType:      Gauge,\n\t\t\t},\n\t\t},\n\t}\n\n\tresourcetest.Run(t, MetricParser, tests, cmpopts.IgnoreFields(Metric{}, \"LabelType\"))\n}\n"
  },
  {
    "path": "v2/parser/infra/objects/bucket.go",
    "content": "package objects\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\tliterals \"encr.dev/v2/parser/infra/internal/literals\"\n\tparseutil \"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Bucket struct {\n\tAST       *ast.CallExpr\n\tFile      *pkginfo.File\n\tName      string // The unique name of the bucket\n\tDoc       string // The documentation on the bucket\n\tVersioned bool\n\tPublic    bool\n}\n\nfunc (t *Bucket) Kind() resource.Kind       { return resource.Bucket }\nfunc (t *Bucket) Package() *pkginfo.Package { return t.File.Pkg }\nfunc (t *Bucket) ASTExpr() ast.Expr         { return t.AST }\nfunc (t *Bucket) ResourceName() string      { return t.Name }\nfunc (t *Bucket) Pos() token.Pos            { return t.AST.Pos() }\nfunc (t *Bucket) End() token.Pos            { return t.AST.End() }\nfunc (t *Bucket) SortKey() string           { return t.Name }\n\nvar BucketParser = &resourceparser.Parser{\n\tName: \"Bucket\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/storage/objects\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{Name: \"NewBucket\", PkgPath: \"encore.dev/storage/objects\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 0,\n\t\t\tParse:       parseBucket,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parseBucket(d parseutil.ReferenceInfo) {\n\terrs := d.Pass.Errs\n\n\tif len(d.Call.Args) != 2 {\n\t\terrs.Add(errNewBucketArgCount(len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tbucketName := parseutil.ParseResourceName(d.Pass.Errs, \"objects.NewBucket\", \"bucket name\",\n\t\td.Call.Args[0], parseutil.KebabName, \"\")\n\tif bucketName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\tcfgLit, ok := literals.ParseStruct(d.Pass.Errs, d.File, \"objects.BucketConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\t// Decode the config\n\ttype decodedConfig struct {\n\t\tVersioned bool `literal:\",optional\"`\n\t\tPublic    bool `literal:\",optional\"`\n\t}\n\tconfig := literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, nil)\n\n\tbkt := &Bucket{\n\t\tAST:       d.Call,\n\t\tFile:      d.File,\n\t\tName:      bucketName,\n\t\tDoc:       d.Doc,\n\t\tVersioned: config.Versioned,\n\t\tPublic:    config.Public,\n\t}\n\td.Pass.RegisterResource(bkt)\n\td.Pass.AddBind(d.File, d.Ident, bkt)\n}\n"
  },
  {
    "path": "v2/parser/infra/objects/errors.go",
    "content": "package objects\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nconst (\n\tobjectsNewBucketHelp = \"For example `objects.NewBucket(\\\"my-bucket\\\", objects.BucketConfig{ Versioned: false })`\"\n\n\tobjectsBucketUsageHelp = \"The bucket can only be referenced by calling methods on it, or by using objects.BucketRef.\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"pubsub\",\n\t\t\"For more information on Object Storage, see https://encore.dev/docs/primitives/object-storage\",\n\t)\n\n\terrNewBucketArgCount = errRange.Newf(\n\t\t\"Invalid objects.NewBucket call\",\n\t\t\"A call to objects.NewBucket requires 2 arguments; the bucket name and the config object, got %d arguments.\",\n\t\terrors.PrependDetails(objectsNewBucketHelp),\n\t)\n\n\terrInvalidBucketUsage = errRange.New(\n\t\t\"Invalid reference to objects.Bucket\",\n\t\t\"A reference to an objects.Bucket is not permissible here.\",\n\t\terrors.PrependDetails(objectsBucketUsageHelp),\n\t)\n\n\tErrBucketNameNotUnique = errRange.New(\n\t\t\"Duplicate bucket name\",\n\t\t\"An object storage bucket name must be unique.\",\n\n\t\terrors.PrependDetails(\"If you wish to reuse the same bucket, then you can export the original Bucket object and reference it from here.\"),\n\t)\n\n\tErrUnsupportedOperationOutsideService = errRange.Newf(\n\t\t\"Unsupported bucket operation outside of service\",\n\t\t\"The %s operation can only be performed within a service. Use objects.BucketRef to pass the bucket reference to other, non-service components.\",\n\t)\n\n\terrBucketRefNoTypeArgs = errRange.New(\n\t\t\"Invalid call to objects.BucketRef\",\n\t\t\"A type argument indicating the requested permissions must be provided.\",\n\t)\n\n\terrBucketRefInvalidPerms = errRange.New(\n\t\t\"Unrecognized permissions in call to objects.BucketRef\",\n\t\t\"The supported permissions are objects.{Uploader,Downloader,Attrser,Lister,Remover,PublicURLer,ReadWriter}.\",\n\t)\n\n\tErrBucketRefOutsideService = errRange.New(\n\t\t\"Call to objects.BucketRef outside service\",\n\t\t\"objects.BucketRef can only be called from within a service.\",\n\t)\n\n\tErrBucketNotPublic = errRange.New(\n\t\t\"Call to PublicURL for non-public objects.Bucket\",\n\t\t\"The PublicURL method can only be called on a public bucket.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/objects/usage.go",
    "content": "package objects\n\nimport (\n\t\"slices\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/resource/usage\"\n\n\tmeta \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\ntype MethodUsage struct {\n\tusage.Base\n\tMethod string\n\tPerm   Perm\n}\n\ntype RefUsage struct {\n\tusage.Base\n\tPerms []Perm\n}\n\nfunc (u *RefUsage) HasPerm(perm Perm) bool {\n\tfor _, p := range u.Perms {\n\t\tif p == perm {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\ntype Perm string\n\nconst (\n\tListObjects          Perm = \"list-objects\"\n\tReadObjectContents   Perm = \"read-object-contents\"\n\tWriteObject          Perm = \"write-object\"\n\tUpdateObjectMetadata Perm = \"update-object-metadata\"\n\tGetObjectMetadata    Perm = \"get-object-metadata\"\n\tDeleteObject         Perm = \"delete-object\"\n\tGetPublicURL         Perm = \"get-public-url\"\n\tSignedUploadURL      Perm = \"signed-upload-url\"\n\tSignedDownloadURL    Perm = \"signed-download-url\"\n)\n\nfunc (p Perm) ToMeta() (meta.BucketUsage_Operation, bool) {\n\tswitch p {\n\tcase ListObjects:\n\t\treturn meta.BucketUsage_LIST_OBJECTS, true\n\tcase ReadObjectContents:\n\t\treturn meta.BucketUsage_READ_OBJECT_CONTENTS, true\n\tcase WriteObject:\n\t\treturn meta.BucketUsage_WRITE_OBJECT, true\n\tcase UpdateObjectMetadata:\n\t\treturn meta.BucketUsage_UPDATE_OBJECT_METADATA, true\n\tcase GetObjectMetadata:\n\t\treturn meta.BucketUsage_GET_OBJECT_METADATA, true\n\tcase DeleteObject:\n\t\treturn meta.BucketUsage_DELETE_OBJECT, true\n\tcase GetPublicURL:\n\t\treturn meta.BucketUsage_GET_PUBLIC_URL, true\n\tcase SignedUploadURL:\n\t\treturn meta.BucketUsage_SIGNED_UPLOAD_URL, true\n\tcase SignedDownloadURL:\n\t\treturn meta.BucketUsage_SIGNED_DOWNLOAD_URL, true\n\tdefault:\n\t\treturn meta.BucketUsage_UNKNOWN, false\n\t}\n}\n\nfunc ResolveBucketUsage(data usage.ResolveData, bkt *Bucket) usage.Usage {\n\tswitch expr := data.Expr.(type) {\n\tcase *usage.MethodCall:\n\t\tvar perm Perm\n\t\tswitch expr.Method {\n\t\tcase \"Upload\":\n\t\t\tperm = WriteObject\n\t\tcase \"Download\":\n\t\t\tperm = ReadObjectContents\n\t\tcase \"List\":\n\t\t\tperm = ListObjects\n\t\tcase \"Remove\":\n\t\t\tperm = DeleteObject\n\t\tcase \"PublicURL\":\n\t\t\tperm = GetPublicURL\n\t\tcase \"SignedUploadURL\":\n\t\t\tperm = SignedUploadURL\n\t\tcase \"SignedDownloadURL\":\n\t\t\tperm = SignedDownloadURL\n\t\tcase \"Attrs\", \"Exists\":\n\t\t\tperm = GetObjectMetadata\n\t\tdefault:\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &MethodUsage{\n\t\t\tBase: usage.Base{\n\t\t\t\tFile: expr.File,\n\t\t\t\tBind: expr.Bind,\n\t\t\t\tExpr: expr,\n\t\t\t},\n\t\t\tMethod: expr.Method,\n\t\t\tPerm:   perm,\n\t\t}\n\n\tcase *usage.FuncArg:\n\t\tswitch {\n\t\tcase option.Contains(expr.PkgFunc, pkginfo.Q(\"encore.dev/storage/objects\", \"BucketRef\")):\n\t\t\treturn parseBucketRef(data.Errs, expr)\n\t\t}\n\t}\n\n\tdata.Errs.Add(errInvalidBucketUsage.AtGoNode(data.Expr))\n\treturn nil\n}\n\nfunc parseBucketRef(errs *perr.List, expr *usage.FuncArg) usage.Usage {\n\tif len(expr.TypeArgs) < 1 {\n\t\terrs.Add(errBucketRefNoTypeArgs.AtGoNode(expr.Call))\n\t\treturn nil\n\t}\n\n\tcheckUsage := func(types ...schema.Type) (usage.Usage, bool) {\n\t\tif len(types) == 0 {\n\t\t\treturn nil, false\n\t\t}\n\n\t\tvar perms []Perm\n\t\tfor _, typ := range types {\n\t\t\tswitch {\n\t\t\tcase isNamed(typ, \"Uploader\"):\n\t\t\t\tperms = append(perms, WriteObject)\n\t\t\tcase isNamed(typ, \"SignedUploader\"):\n\t\t\t\tperms = append(perms, SignedUploadURL)\n\t\t\tcase isNamed(typ, \"Downloader\"):\n\t\t\t\tperms = append(perms, ReadObjectContents)\n\t\t\tcase isNamed(typ, \"SignedDownloader\"):\n\t\t\t\tperms = append(perms, SignedDownloadURL)\n\t\t\tcase isNamed(typ, \"Lister\"):\n\t\t\t\tperms = append(perms, ListObjects)\n\t\t\tcase isNamed(typ, \"Remover\"):\n\t\t\t\tperms = append(perms, DeleteObject)\n\t\t\tcase isNamed(typ, \"Attrser\"):\n\t\t\t\tperms = append(perms, GetObjectMetadata)\n\t\t\tcase isNamed(typ, \"PublicURLer\"):\n\t\t\t\tperms = append(perms, GetPublicURL)\n\t\t\tcase isNamed(typ, \"ReadWriter\"):\n\t\t\t\tperms = append(perms,\n\t\t\t\t\tWriteObject, ReadObjectContents, ListObjects, DeleteObject,\n\t\t\t\t\tGetObjectMetadata, SignedUploadURL, SignedDownloadURL, UpdateObjectMetadata)\n\t\t\tdefault:\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t}\n\n\t\t// Sort and de-dup the perms.\n\t\tslices.Sort(perms)\n\t\tperms = slices.Compact(perms)\n\n\t\treturn &RefUsage{\n\t\t\tBase: usage.Base{\n\t\t\t\tFile: expr.File,\n\t\t\t\tBind: expr.Bind,\n\t\t\t\tExpr: expr,\n\t\t\t},\n\t\t\tPerms: perms,\n\t\t}, true\n\t}\n\n\t// Do we have a simple usage directly as the type argument?\n\tif u, ok := checkUsage(expr.TypeArgs[0]); ok {\n\t\treturn u\n\t}\n\n\t// Determine if we have a custom ref type,\n\t// either in the form \"type Foo = pubsub.Publisher[Msg]\"\n\t// or in the form \"type Foo interface { pubsub.Publisher[Msg] }\"\n\tif named, ok := expr.TypeArgs[0].(schema.NamedType); ok {\n\t\tunderlying := named.Decl().Type\n\t\tif u, ok := checkUsage(underlying); ok {\n\t\t\treturn u\n\t\t}\n\n\t\t// Otherwise make sure the interface only embeds the one supported type we have (pubsub.Publisher).\n\t\t// We'll need to extend this in the future to support multiple permissions.\n\t\tif iface, ok := underlying.(schema.InterfaceType); ok {\n\t\t\tif len(iface.Methods) == 0 && len(iface.TypeLists) == 0 {\n\t\t\t\tif u, ok := checkUsage(iface.EmbeddedIfaces...); ok {\n\t\t\t\t\treturn u\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\terrs.Add(errBucketRefInvalidPerms.AtGoNode(expr.Call))\n\treturn nil\n}\n\nfunc isNamed(typ schema.Type, name string) bool {\n\treturn schemautil.IsNamed(typ, \"encore.dev/storage/objects\", name)\n}\n"
  },
  {
    "path": "v2/parser/infra/objects/usage_test.go",
    "content": "package objects_test\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/parser/infra/objects\"\n\t\"encr.dev/v2/parser/resource/usage\"\n\t\"encr.dev/v2/parser/resource/usage/usagetest\"\n)\n\nfunc TestResolveBucketUsage(t *testing.T) {\n\ttests := []usagetest.Case{\n\t\t{\n\t\t\tName: \"none\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\n`,\n\t\t\tWant: []usage.Usage{},\n\t\t},\n\t\t{\n\t\t\tName: \"upload\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nfunc Foo() { bkt.Upload(context.Background(), \"key\") }\n\n`,\n\t\t\tWant: []usage.Usage{&objects.MethodUsage{Method: \"Upload\", Perm: objects.WriteObject}},\n\t\t},\n\t\t{\n\t\t\tName: \"sign_upload_url\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nfunc Foo() { bkt.SignedUploadURL(context.Background(), \"key\") }\n\n`,\n\t\t\tWant: []usage.Usage{&objects.MethodUsage{Method: \"SignedUploadURL\", Perm: objects.SignedUploadURL}},\n\t\t},\n\t\t{\n\t\t\tName: \"sign_download_url\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nfunc Foo() { bkt.SignedDownloadURL(context.Background(), \"key\") }\n\n`,\n\t\t\tWant: []usage.Usage{&objects.MethodUsage{Method: \"SignedDownloadURL\", Perm: objects.SignedDownloadURL}},\n\t\t},\n\t\t{\n\t\t\tName: \"attrs\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nfunc Foo() { bkt.Attrs(context.Background(), \"key\") }\n`,\n\t\t\tWant: []usage.Usage{&objects.MethodUsage{Method: \"Attrs\", Perm: objects.GetObjectMetadata}},\n\t\t},\n\t\t{\n\t\t\tName: \"exists\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nfunc Foo() { bkt.Exists(context.Background(), \"key\") }\n`,\n\t\t\tWant: []usage.Usage{&objects.MethodUsage{Method: \"Exists\", Perm: objects.GetObjectMetadata}},\n\t\t},\n\t\t{\n\t\t\tName: \"ref\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nvar ref = objects.BucketRef[objects.Uploader](bkt)\n`,\n\t\t\tWant: []usage.Usage{&objects.RefUsage{\n\t\t\t\tPerms: []objects.Perm{objects.WriteObject},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"ref_multi\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nvar ref = objects.BucketRef[objects.ReadWriter](bkt)\n`,\n\t\t\tWant: []usage.Usage{&objects.RefUsage{\n\t\t\t\tPerms: []objects.Perm{\n\t\t\t\t\tobjects.DeleteObject,\n\t\t\t\t\tobjects.GetObjectMetadata,\n\t\t\t\t\tobjects.ListObjects,\n\t\t\t\t\tobjects.ReadObjectContents,\n\t\t\t\t\tobjects.SignedDownloadURL,\n\t\t\t\t\tobjects.SignedUploadURL,\n\t\t\t\t\tobjects.UpdateObjectMetadata,\n\t\t\t\t\tobjects.WriteObject,\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"custom_ref_alias\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\ntype MyRef = objects.Uploader\n\nvar ref = objects.BucketRef[MyRef](bkt)\n`,\n\t\t\tWant: []usage.Usage{&objects.RefUsage{\n\t\t\t\tPerms: []objects.Perm{objects.WriteObject},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"custom_ref_interface\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\ntype MyRef interface { objects.Uploader }\n\nvar ref = objects.BucketRef[MyRef](bkt)\n`,\n\t\t\tWant: []usage.Usage{&objects.RefUsage{\n\t\t\t\tPerms: []objects.Perm{objects.WriteObject},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"custom_ref_interface_multi\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\ntype MyRef interface { objects.Uploader; objects.Downloader; objects.SignedUploader }\n\nvar ref = objects.BucketRef[MyRef](bkt)\n`,\n\t\t\tWant: []usage.Usage{&objects.RefUsage{\n\t\t\t\tPerms: []objects.Perm{\n\t\t\t\t\tobjects.ReadObjectContents,\n\t\t\t\t\tobjects.SignedUploadURL,\n\t\t\t\t\tobjects.WriteObject},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"invalid_ref\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\ntype MyRef interface { objects.Uploader; ~int | string; Publish() int }\n\nvar ref = objects.BucketRef[MyRef](bkt)\n`,\n\t\t\tWantErrs: []string{\"Unrecognized permissions in call to objects.BucketRef\"},\n\t\t},\n\t\t{\n\t\t\tName: \"invalid_ref_2\",\n\t\t\tCode: `\nvar bkt = objects.NewBucket(\"bucket\", objects.BucketConfig{})\n\nvar ref = objects.BucketRef[string](bkt)\n`,\n\t\t\tWantErrs: []string{\"Unrecognized permissions in call to objects.BucketRef\"},\n\t\t},\n\t}\n\n\tusagetest.Run(t, []string{\"encore.dev/storage/objects\"}, tests)\n}\n"
  },
  {
    "path": "v2/parser/infra/pubsub/errors.go",
    "content": "package pubsub\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nconst (\n\tpubsubNewTopicHelp = \"For example `pubsub.NewTopic[MyMessage](\\\"my-topic\\\", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce })`\"\n\n\tpubsubNewSubscriptionHelp = \"A pubsub subscription must have a unique name per topic and be given a handler function for processing the message. \" +\n\t\t\"The handler for the subscription must be defined in the same service as the call to pubsub.NewSubscription and can be an inline function. \" +\n\t\t\"For example:\\n\" +\n\t\t\"\\tpubsub.NewSubscription(myTopic, \\\"subscription-name\\\", pubsub.SubscriptionConfig[MyMessage]{\\n\" +\n\t\t\"\\t\\tHandler: func(ctx context.Context, event MyMessage) error { return nil },\\n\" +\n\t\t\"\\t})\"\n\n\tpubsubTopicUsageHelp = \"The topic can only be referenced by calling methods on it, or to pass it to pubsub.NewSubscription or et.Topic.\"\n\n\tpubsubMethodHandlerHelp = \"For example `pubsub.MethodHandler(Service.MethodName)` or `pubsub.MethodHandler((*Service).MethodName)`.`\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"pubsub\",\n\t\t\"For more information on PubSub, see https://encore.dev/docs/primitives/pubsub\",\n\t)\n\n\terrNewTopicArgCount = errRange.Newf(\n\t\t\"Invalid pubsub.NewTopic call\",\n\t\t\"A call to pubsub.NewTopic requires 2 arguments; the topic name and the config object, got %d arguments.\",\n\t\terrors.PrependDetails(pubsubNewTopicHelp),\n\t)\n\n\terrInvalidMessageType = errRange.New(\n\t\t\"Invalid PubSub message type\",\n\t\t\"The message type for a PubSub topic or subscription must be a named struct type.\",\n\t\terrors.PrependDetails(pubsubNewTopicHelp),\n\t)\n\n\terrInvalidDeliveryGuarantee = errRange.New(\n\t\t\"Invalid PubSub topic config\",\n\t\t\"The configuration field named \\\"DeliveryGuarantee\\\" must be set to pubsub.AtLeastOnce or pubsub.ExactlyOnce.\",\n\t)\n\n\terrOrderingKeyNotExported = errRange.New(\n\t\t\"Invalid PubSub topic config\",\n\t\t\"The configuration field named \\\"OrderingAttribute\\\" must be a one of the export attributes on the message type.\",\n\t\terrors.PrependDetails(pubsubNewTopicHelp),\n\t)\n\n\terrInvalidTopicUsage = errRange.New(\n\t\t\"Invalid reference to pubsub.Topic\",\n\t\t\"A reference to pubsub.Topic is not permissible here.\",\n\t\terrors.PrependDetails(pubsubTopicUsageHelp),\n\t)\n\n\terrNewSubscriptionArgCount = errRange.Newf(\n\t\t\"Invalid pubsub.NewSubscription call\",\n\t\t\"A call to pubsub.NewSubscription requires 3 arguments; the topic, the subscription name and the config object, got %d arguments.\",\n\t\terrors.PrependDetails(pubsubNewSubscriptionHelp),\n\t)\n\n\tErrSubscriptionTopicNotResource = errRange.New(\n\t\t\"Invalid call to pubsub.NewSubscription\",\n\t\t\"pubsub.NewSubscription requires the first argument to be a resource of type pubsub.Topic.\",\n\t\terrors.PrependDetails(pubsubNewSubscriptionHelp),\n\t)\n\n\terrInvalidAttrPrefix = errRange.New(\n\t\t\"Invalid attribute prefix\",\n\t\t\"PubSub message attributes must not be prefixed with \\\"encore\\\".\",\n\t)\n\n\tErrTopicNameNotUnique = errRange.New(\n\t\t\"Duplicate PubSub topic name\",\n\t\t\"A PubSub topic name must be unique within a service.\",\n\n\t\terrors.PrependDetails(\"If you wish to reuse the same topic, then you can export the original Topic object import it here.\"),\n\t)\n\n\tErrSubscriptionNameNotUnique = errRange.New(\n\t\t\"Duplicate PubSub subscription on topic\",\n\t\t\"Subscription names on topics must be unique.\",\n\t)\n\n\tErrUnableToIdentifyServicesInvolved = errRange.New(\n\t\t\"Unable to identify services involved\",\n\t\t\"Unable to identify services involved in the PubSub subscription.\",\n\t\terrors.MarkAsInternalError(),\n\t)\n\n\tErrSubscriptionHandlerNotDefinedInSameService = errRange.New(\n\t\t\"Invalid PubSub subscription handler\",\n\t\t\"The handler for the subscription must be defined in the same service as the call to pubsub.NewSubscription.\",\n\t\terrors.PrependDetails(pubsubNewSubscriptionHelp),\n\t)\n\n\terrSubscriptionAckDeadlineTooShort = errRange.New(\n\t\t\"Invalid PubSub subscription config\",\n\t\t\"The ack deadline must be at least 1 second.\",\n\t)\n\n\terrSubscriptionMessageRetentionTooShort = errRange.New(\n\t\t\"Invalid PubSub subscription config\",\n\t\t\"The message retention must be at least 1 minute.\",\n\t)\n\n\terrSubscriptionMinRetryBackoffTooShort = errRange.New(\n\t\t\"Invalid PubSub subscription config\",\n\t\t\"The min backoff for retries must be at least 1 second.\",\n\t)\n\n\terrSubscriptionMaxRetryBackoffTooShort = errRange.New(\n\t\t\"Invalid PubSub subscription config\",\n\t\t\"The max backoff for retries must be at least 1 second.\",\n\t)\n\n\terrSubscriptionMaxRetriesTooSmall = errRange.New(\n\t\t\"Invalid PubSub subscription config\",\n\t\t\"The max number of retries must be a positive number or the constants `pubsub.InfiniteRetries` or `pubsub.NoRetries`.\",\n\t)\n\n\terrTopicRefNoTypeArgs = errRange.New(\n\t\t\"Invalid call to pubsub.TopicRef\",\n\t\t\"A type argument indicating the requested permissions must be provided.\",\n\t)\n\n\terrTopicRefInvalidPerms = errRange.New(\n\t\t\"Unrecognized permissions in call to pubsub.TopicRef\",\n\t\t\"The only supported permission is currently pubsub.Publisher[MyMessage].\",\n\t)\n\n\tErrTopicRefOutsideService = errRange.New(\n\t\t\"Call to pubsub.TopicRef outside service\",\n\t\t\"pubsub.TopicRef can only be called from within a service.\",\n\t)\n\n\tErrInvalidMethodHandler = errRange.New(\n\t\t\"Invalid call to pubsub.MethodHandler\",\n\t\t\"pubsub.MethodHandler requires the first argument to be a reference to a method on a service struct.\",\n\t\terrors.PrependDetails(pubsubMethodHandlerHelp),\n\t)\n\n\tErrMethodHandlerTypeNotServiceStruct = errRange.New(\n\t\t\"Invalid call to pubsub.MethodHandler\",\n\t\t\"pubsub.MethodHandler can only reference methods that are defined on service structs.\",\n\t\terrors.PrependDetails(pubsubMethodHandlerHelp),\n\t)\n\n\tErrMethodHandlerDifferentPackage = errRange.New(\n\t\t\"Invalid call to pubsub.MethodHandler\",\n\t\t\"pubsub.MethodHandler can only reference the service struct defined in the same package as the subscription.\",\n\t\terrors.PrependDetails(pubsubMethodHandlerHelp),\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/pubsub/subscription.go",
    "content": "package pubsub\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"time\"\n\n\t\"golang.org/x/tools/go/ast/astutil\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/internal/literals\"\n\t\"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Subscription struct {\n\tAST   *ast.CallExpr\n\tFile  *pkginfo.File\n\tName  string // The unique name of the pub sub subscription\n\tDoc   string // The documentation on the pub sub subscription\n\tTopic pkginfo.QualifiedName\n\tCfg   SubscriptionConfig\n\n\t// Handler is the AST expression defining the handler function.\n\tHandler ast.Expr\n\n\t// MethodHandler specifies whether the handler is a method on a service struct.\n\tMethodHandler option.Option[MethodHandler]\n}\n\n// MethodHandler is used to describe a handler that references a method on a service struct.\ntype MethodHandler struct {\n\t// The type declaration the handler is a method on.\n\tDecl *pkginfo.PkgDeclInfo\n\t// Method is the name of the method.\n\tMethod string\n}\n\ntype SubscriptionConfig struct {\n\tAckDeadline      time.Duration\n\tMessageRetention time.Duration\n\tMinRetryBackoff  time.Duration\n\tMaxRetryBackoff  time.Duration\n\tMaxRetries       int\n\tMaxConcurrency   int\n}\n\nfunc (s *Subscription) Kind() resource.Kind       { return resource.PubSubSubscription }\nfunc (s *Subscription) Package() *pkginfo.Package { return s.File.Pkg }\nfunc (s *Subscription) ASTExpr() ast.Expr         { return s.AST }\nfunc (s *Subscription) ResourceName() string      { return s.Name }\nfunc (s *Subscription) Pos() token.Pos            { return s.AST.Pos() }\nfunc (s *Subscription) End() token.Pos            { return s.AST.End() }\nfunc (s *Subscription) SortKey() string {\n\treturn s.Topic.PkgPath.String() + \".\" + s.Topic.Name + \".\" + s.Name\n}\n\nvar SubscriptionParser = &resourceparser.Parser{\n\tName: \"PubSub Subscription\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/pubsub\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{Name: \"NewSubscription\", PkgPath: \"encore.dev/pubsub\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 1,\n\t\t\tParse:       parsePubSubSubscription,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parsePubSubSubscription(d parseutil.ReferenceInfo) {\n\tdisplayName := d.ResourceFunc.NaiveDisplayName()\n\terrs := d.Pass.Errs\n\tif len(d.Call.Args) != 3 {\n\t\terrs.Add(errNewSubscriptionArgCount(len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\ttopicExpr := d.Call.Args[0]\n\ttopicObj, ok := d.File.Names().ResolvePkgLevelRef(topicExpr)\n\tif !ok {\n\t\terrs.Add(ErrSubscriptionTopicNotResource.AtGoNode(topicExpr))\n\t\treturn\n\t}\n\n\tsubscriptionName := parseutil.ParseResourceName(d.Pass.Errs, displayName, \"subscription name\",\n\t\td.Call.Args[1], parseutil.KebabName, \"\")\n\tif subscriptionName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\t// Parse the literal struct representing the subscription configuration\n\t// so we can extract the reference to the handler function\n\tcfgLit, ok := literals.ParseStruct(d.Pass.Errs, d.File, \"pubsub.SubscriptionConfig\", d.Call.Args[2])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\ttype retryConfig struct {\n\t\tMinRetryBackoff time.Duration `literal:\"MinBackoff,optional,default\"`\n\t\tMaxRetryBackoff time.Duration `literal:\"MaxBackoff,optional,default\"`\n\t\tMaxRetries      int           `literal:\"MaxRetries,optional,default\"`\n\t}\n\ttype decodedConfig struct {\n\t\tHandler ast.Expr `literal:\",dynamic,required\"`\n\n\t\t// Optional configuration\n\t\tMaxConcurrency   int           `literal:\",optional,default\"`\n\t\tAckDeadline      time.Duration `literal:\",optional,default\"`\n\t\tMessageRetention time.Duration `literal:\",optional,default\"`\n\t\tRetryPolicy      retryConfig   `literal:\",optional,default\"`\n\t}\n\tdefaults := decodedConfig{\n\t\tMaxConcurrency:   100,\n\t\tAckDeadline:      30 * time.Second,\n\t\tMessageRetention: 7 * 24 * time.Hour,\n\t\tRetryPolicy: retryConfig{\n\t\t\tMinRetryBackoff: 10 * time.Second,\n\t\t\tMaxRetryBackoff: 10 * time.Minute,\n\t\t\tMaxRetries:      100,\n\t\t},\n\t}\n\n\tcfg := literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, &defaults)\n\n\t// Verify we have a config which is in-range of acceptable values\n\tif cfg.AckDeadline < 1*time.Second {\n\t\terrs.Add(errSubscriptionAckDeadlineTooShort.AtGoNode(cfgLit.Expr(\"AckDeadline\"), errors.AsError(fmt.Sprintf(\"got %s\", cfg.AckDeadline))))\n\t}\n\n\tif cfg.MessageRetention < 1*time.Minute {\n\t\terrs.Add(errSubscriptionMessageRetentionTooShort.AtGoNode(cfgLit.Expr(\"MessageRetention\"), errors.AsError(fmt.Sprintf(\"got %s\", cfg.MessageRetention))))\n\t}\n\n\tif cfg.RetryPolicy.MinRetryBackoff < 1*time.Second {\n\t\terrs.Add(errSubscriptionMinRetryBackoffTooShort.AtGoNode(cfgLit.Expr(\"RetryPolicy.MinBackoff\"), errors.AsError(fmt.Sprintf(\"got %s\", cfg.RetryPolicy.MinRetryBackoff))))\n\t}\n\n\tif cfg.RetryPolicy.MaxRetryBackoff < 1*time.Second {\n\t\terrs.Add(errSubscriptionMaxRetryBackoffTooShort.AtGoNode(cfgLit.Expr(\"RetryPolicy.MaxBackoff\"), errors.AsError(fmt.Sprintf(\"got %s\", cfg.RetryPolicy.MaxRetryBackoff))))\n\t}\n\n\tif cfg.RetryPolicy.MaxRetries < -2 {\n\t\terrs.Add(errSubscriptionMaxRetriesTooSmall.AtGoNode(cfgLit.Expr(\"RetryPolicy.MaxRetries\"), errors.AsError(fmt.Sprintf(\"got %d\", cfg.RetryPolicy.MaxRetries))))\n\t}\n\n\tsubCfg := SubscriptionConfig{\n\t\tAckDeadline:      cfg.AckDeadline,\n\t\tMessageRetention: cfg.MessageRetention,\n\t\tMinRetryBackoff:  cfg.RetryPolicy.MinRetryBackoff,\n\t\tMaxRetryBackoff:  cfg.RetryPolicy.MaxRetryBackoff,\n\t\tMaxRetries:       cfg.RetryPolicy.MaxRetries,\n\t\tMaxConcurrency:   cfg.MaxConcurrency,\n\t}\n\n\tif cfg.Handler == nil {\n\t\treturn\n\t}\n\n\tmethodHandler := parseMethodHandler(d, cfg.Handler)\n\tsub := &Subscription{\n\t\tAST:           d.Call,\n\t\tFile:          d.File,\n\t\tName:          subscriptionName,\n\t\tDoc:           d.Doc,\n\t\tTopic:         topicObj,\n\t\tCfg:           subCfg,\n\t\tHandler:       cfg.Handler,\n\t\tMethodHandler: methodHandler,\n\t}\n\td.Pass.RegisterResource(sub)\n\td.Pass.AddBind(d.File, d.Ident, sub)\n}\n\n// parseMethodHandler parses whether the subscription handler references\n// a method on a type.\nfunc parseMethodHandler(d parseutil.ReferenceInfo, handler ast.Expr) option.Option[MethodHandler] {\n\tvar (\n\t\tnone   = option.None[MethodHandler]()\n\t\tf      = d.File\n\t\terrs   = d.Pass.Errs\n\t\tparser = d.Pass.SchemaParser\n\t)\n\n\t// If the handler is a method handler it must be of the form:\n\t//  pubsub.MethodHandler(Service.Method) or pubsub.MethodHandler((*Service).Method)\n\tcall, ok := handler.(*ast.CallExpr)\n\tif !ok {\n\t\treturn none\n\t}\n\n\tqn, ok := f.Names().ResolvePkgLevelRef(call.Fun)\n\tif !ok || qn.PkgPath != \"encore.dev/pubsub\" || qn.Name != \"MethodHandler\" {\n\t\treturn none\n\t} else if len(call.Args) != 1 {\n\t\terrs.Add(ErrInvalidMethodHandler.AtGoNode(call))\n\t\treturn none\n\t}\n\n\t// The first arg must be in the form (*Service).Method or Service.Method.\n\tsel, ok := call.Args[0].(*ast.SelectorExpr)\n\tif !ok {\n\t\terrs.Add(ErrInvalidMethodHandler.AtGoNode(call))\n\t\treturn none\n\t}\n\tx := astutil.Unparen(sel.X)\n\n\t// Parse the type declaration.\n\ttyp := parser.ParseType(f, x)\n\tdecl, ok := schemautil.DerefNamedInfo(typ, false)\n\tif !ok {\n\t\terrs.Add(ErrInvalidMethodHandler.AtGoNode(call))\n\t\treturn none\n\t}\n\n\treturn option.Some(MethodHandler{\n\t\tDecl:   decl,\n\t\tMethod: sel.Sel.Name,\n\t})\n}\n"
  },
  {
    "path": "v2/parser/infra/pubsub/topic.go",
    "content": "package pubsub\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\tliterals \"encr.dev/v2/parser/infra/internal/literals\"\n\tparseutil \"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype DeliveryGuarantee int\n\nconst (\n\tAtLeastOnce DeliveryGuarantee = iota\n\tExactlyOnce\n)\n\ntype Topic struct {\n\tAST               *ast.CallExpr\n\tFile              *pkginfo.File\n\tName              string              // The unique name of the pub sub topic\n\tDoc               string              // The documentation on the pub sub topic\n\tDeliveryGuarantee DeliveryGuarantee   // What guarantees does the pub sub topic have?\n\tOrderingAttribute string              // What field in the message type should be used to ensure First-In-First-Out (FIFO) for messages with the same key\n\tMessageType       *schema.TypeDeclRef // The message type of the pub sub topic\n}\n\nfunc (t *Topic) Kind() resource.Kind       { return resource.PubSubTopic }\nfunc (t *Topic) Package() *pkginfo.Package { return t.File.Pkg }\nfunc (t *Topic) ASTExpr() ast.Expr         { return t.AST }\nfunc (t *Topic) ResourceName() string      { return t.Name }\nfunc (t *Topic) Pos() token.Pos            { return t.AST.Pos() }\nfunc (t *Topic) End() token.Pos            { return t.AST.End() }\nfunc (t *Topic) SortKey() string           { return t.Name }\n\nvar TopicParser = &resourceparser.Parser{\n\tName: \"PubSub Topic\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/pubsub\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{Name: \"NewTopic\", PkgPath: \"encore.dev/pubsub\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 1,\n\t\t\tMaxTypeArgs: 1,\n\t\t\tParse:       parsePubSubTopic,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parsePubSubTopic(d parseutil.ReferenceInfo) {\n\terrs := d.Pass.Errs\n\n\tif len(d.Call.Args) != 2 {\n\t\terrs.Add(errNewTopicArgCount(len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\ttopicName := parseutil.ParseResourceName(d.Pass.Errs, \"pubsub.NewTopic\", \"topic name\",\n\t\td.Call.Args[0], parseutil.KebabName, \"\")\n\tif topicName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\tmessageType, ok := schemautil.ResolveNamedStruct(d.TypeArgs[0], false)\n\tif !ok {\n\t\terrs.Add(errInvalidMessageType.AtGoNode(d.TypeArgs[0].ASTExpr(), errors.AsError(fmt.Sprintf(\"got %s\", parseutil.NodeType(d.TypeArgs[0].ASTExpr())))))\n\t\treturn\n\t}\n\n\tcfgLit, ok := literals.ParseStruct(d.Pass.Errs, d.File, \"pubsub.TopicConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\t// Decode the config\n\ttype decodedConfig struct {\n\t\tDeliveryGuarantee int    `literal:\",optional\"` // optional rather than required because we check for a zero value below\n\t\tOrderingAttribute string `literal:\",optional\"`\n\t}\n\tconfig := literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, nil)\n\n\t// Get the ordering key\n\tif config.OrderingAttribute != \"\" {\n\t\tvar foundField ast.Node\n\n\t\t// Make sure the OrderingAttribute value exists in the struct.\n\t\tstr := messageType.Decl.Type.(schema.StructType)\n\t\tfor _, field := range str.Fields {\n\t\t\tif attr, err := field.Tag.Get(\"pubsub-attr\"); err == nil && attr.Name == config.OrderingAttribute {\n\t\t\t\tfoundField = field.AST\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif foundField == nil || !ast.IsExported(foundField.(*ast.Field).Names[0].Name) {\n\t\t\tif foundField == nil {\n\t\t\t\tfoundField = cfgLit.Expr(\"OrderingAttribute\")\n\t\t\t}\n\t\t\terrs.Add(errOrderingKeyNotExported.AtGoNode(foundField))\n\t\t}\n\t}\n\n\tdeliveryGuarantee := DeliveryGuarantee(config.DeliveryGuarantee) - 1 // The runtime variables are 1 indexed so we can detect a zero value\n\tif deliveryGuarantee != AtLeastOnce && deliveryGuarantee != ExactlyOnce {\n\t\tpos := cfgLit.Pos(\"DeliveryGuarantee\")\n\t\terrs.Add(errInvalidDeliveryGuarantee.AtGoPos(pos, pos))\n\t}\n\n\t// Validate the message attributes are not using the reserved prefix\n\tif str, ok := messageType.Decl.Type.(schema.StructType); ok {\n\t\tfor _, field := range str.Fields {\n\t\t\tfor _, tagKey := range field.Tag.Keys() {\n\t\t\t\ttag, err := field.Tag.Get(tagKey)\n\t\t\t\tif err == nil {\n\t\t\t\t\tswitch tagKey {\n\t\t\t\t\tcase \"pubsub-attr\":\n\t\t\t\t\t\tif strings.HasPrefix(tag.Name, \"encore\") {\n\t\t\t\t\t\t\terrs.Add(errInvalidAttrPrefix.\n\t\t\t\t\t\t\t\tAtGoNode(field.AST.Tag).\n\t\t\t\t\t\t\t\tAtGoNode(d.TypeArgs[0].ASTExpr(), errors.AsHelp(\"used as a message type in this topic\")))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tpanic(\"not a struct\")\n\t}\n\n\ttopic := &Topic{\n\t\tAST:               d.Call,\n\t\tFile:              d.File,\n\t\tName:              topicName,\n\t\tDoc:               d.Doc,\n\t\tDeliveryGuarantee: deliveryGuarantee,\n\t\tOrderingAttribute: config.OrderingAttribute,\n\t\tMessageType:       messageType,\n\t}\n\td.Pass.RegisterResource(topic)\n\td.Pass.AddBind(d.File, d.Ident, topic)\n}\n"
  },
  {
    "path": "v2/parser/infra/pubsub/usage.go",
    "content": "package pubsub\n\nimport (\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype PublishUsage struct {\n\tusage.Base\n}\n\ntype RefUsage struct {\n\tusage.Base\n\tPerms []Perm\n}\n\nfunc (u *RefUsage) HasPerm(perm Perm) bool {\n\tfor _, p := range u.Perms {\n\t\tif p == perm {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\ntype Perm string\n\nconst (\n\tPublishPerm Perm = \"publish\"\n)\n\nfunc ResolveTopicUsage(data usage.ResolveData, topic *Topic) usage.Usage {\n\tswitch expr := data.Expr.(type) {\n\tcase *usage.MethodCall:\n\t\tif expr.Method == \"Publish\" {\n\t\t\treturn &PublishUsage{\n\t\t\t\tBase: usage.Base{\n\t\t\t\t\tFile: expr.File,\n\t\t\t\t\tBind: expr.Bind,\n\t\t\t\t\tExpr: expr,\n\t\t\t\t},\n\t\t\t}\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\n\tcase *usage.FuncArg:\n\t\tswitch {\n\t\tcase option.Contains(expr.PkgFunc, pkginfo.Q(\"encore.dev/pubsub\", \"NewSubscription\")):\n\t\t\t// Allowed usage\n\t\t\treturn nil\n\t\tcase option.Contains(expr.PkgFunc, pkginfo.Q(\"encore.dev/et\", \"Topic\")):\n\t\t\t// Allowed usage\n\t\t\treturn nil\n\t\tcase option.Contains(expr.PkgFunc, pkginfo.Q(\"encore.dev/pubsub\", \"TopicRef\")):\n\t\t\treturn parseTopicRef(data.Errs, expr)\n\t\t}\n\t}\n\n\tdata.Errs.Add(errInvalidTopicUsage.AtGoNode(data.Expr))\n\treturn nil\n}\n\nfunc parseTopicRef(errs *perr.List, expr *usage.FuncArg) usage.Usage {\n\tif len(expr.TypeArgs) < 1 {\n\t\terrs.Add(errTopicRefNoTypeArgs.AtGoNode(expr.Call))\n\t\treturn nil\n\t}\n\n\tcheckUsage := func(typ schema.Type) (usage.Usage, bool) {\n\t\tif schemautil.IsNamed(typ, \"encore.dev/pubsub\", \"Publisher\") {\n\t\t\treturn &RefUsage{\n\t\t\t\tBase: usage.Base{\n\t\t\t\t\tFile: expr.File,\n\t\t\t\t\tBind: expr.Bind,\n\t\t\t\t\tExpr: expr,\n\t\t\t\t},\n\t\t\t\tPerms: []Perm{PublishPerm},\n\t\t\t}, true\n\t\t}\n\t\treturn nil, false\n\t}\n\n\t// Do we have a simple usage directly as the type argument?\n\tif u, ok := checkUsage(expr.TypeArgs[0]); ok {\n\t\treturn u\n\t}\n\n\t// Determine if we have a custom ref type,\n\t// either in the form \"type Foo = pubsub.Publisher[Msg]\"\n\t// or in the form \"type Foo interface { pubsub.Publisher[Msg] }\"\n\tif named, ok := expr.TypeArgs[0].(schema.NamedType); ok {\n\t\tunderlying := named.Decl().Type\n\t\tif u, ok := checkUsage(underlying); ok {\n\t\t\treturn u\n\t\t}\n\n\t\t// Otherwise make sure the interface only embeds the one supported type we have (pubsub.Publisher).\n\t\t// We'll need to extend this in the future to support multiple permissions.\n\t\tif iface, ok := underlying.(schema.InterfaceType); ok {\n\t\t\tif len(iface.EmbeddedIfaces) == 1 && len(iface.Methods) == 0 && len(iface.TypeLists) == 0 {\n\t\t\t\tif u, ok := checkUsage(iface.EmbeddedIfaces[0]); ok {\n\t\t\t\t\treturn u\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\terrs.Add(errTopicRefInvalidPerms.AtGoNode(expr.Call))\n\treturn nil\n}\n"
  },
  {
    "path": "v2/parser/infra/pubsub/usage_test.go",
    "content": "package pubsub_test\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/resource/usage\"\n\t\"encr.dev/v2/parser/resource/usage/usagetest\"\n)\n\nfunc TestResolveTopicUsage(t *testing.T) {\n\ttests := []usagetest.Case{\n\t\t{\n\t\t\tName: \"none\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\n`,\n\t\t\tWant: []usage.Usage{},\n\t\t},\n\t\t{\n\t\t\tName: \"publish\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\nfunc Foo() { topic.Publish(context.Background(), Msg{}) }\n\n`,\n\t\t\tWant: []usage.Usage{&pubsub.PublishUsage{}},\n\t\t},\n\t\t{\n\t\t\tName: \"ref\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\nvar ref = pubsub.TopicRef[pubsub.Publisher[Msg]](topic)\n`,\n\t\t\tWant: []usage.Usage{&pubsub.RefUsage{\n\t\t\t\tPerms: []pubsub.Perm{pubsub.PublishPerm},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"custom_ref_alias\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\ntype MyRef = pubsub.Publisher[Msg]\n\nvar ref = pubsub.TopicRef[MyRef](topic)\n`,\n\t\t\tWant: []usage.Usage{&pubsub.RefUsage{\n\t\t\t\tPerms: []pubsub.Perm{pubsub.PublishPerm},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"custom_ref_interface\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\ntype MyRef interface { pubsub.Publisher[Msg] }\n\nvar ref = pubsub.TopicRef[MyRef](topic)\n`,\n\t\t\tWant: []usage.Usage{&pubsub.RefUsage{\n\t\t\t\tPerms: []pubsub.Perm{pubsub.PublishPerm},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"generic_custom_ref_interface\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\ntype MyRef[T any] interface { pubsub.Publisher[T] }\n\nvar ref = pubsub.TopicRef[MyRef[Msg]](topic)\n`,\n\t\t\tWant: []usage.Usage{&pubsub.RefUsage{\n\t\t\t\tPerms: []pubsub.Perm{pubsub.PublishPerm},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tName: \"invalid_ref\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\ntype MyRef interface { pubsub.Publisher[Msg]; ~int | string; Publish() int }\n\nvar ref = pubsub.TopicRef[MyRef](topic)\n`,\n\t\t\tWantErrs: []string{\"Unrecognized permissions in call to pubsub.TopicRef\"},\n\t\t},\n\t\t{\n\t\t\tName: \"invalid_ref_2\",\n\t\t\tCode: `\ntype Msg struct{}\n\nvar topic = pubsub.NewTopic[Msg](\"topic\", pubsub.TopicConfig{DeliveryGuarantee: pubsub.AtLeastOnce})\n\nvar ref = pubsub.TopicRef[string](topic)\n`,\n\t\t\tWantErrs: []string{\"Unrecognized permissions in call to pubsub.TopicRef\"},\n\t\t},\n\t}\n\n\tusagetest.Run(t, []string{\"encore.dev/pubsub\"}, tests)\n}\n"
  },
  {
    "path": "v2/parser/infra/secrets/errors.go",
    "content": "package secrets\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\terrRange = errors.Range(\n\t\t\"secrets\",\n\t\t\"For more information about how to use secrets, see https://encore.dev/docs/primitives/secrets\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrSecretsMustBeStruct = errRange.New(\n\t\t\"Invalid secrets variable\",\n\t\t\"The \\\"secrets\\\" variable type must be an inline struct.\",\n\t)\n\n\terrSecretsDefinedSeperately = errRange.New(\n\t\t\"Invalid secrets variable\",\n\t\t\"The \\\"secrets\\\" variable must be declared separately and not in a var block.\",\n\t)\n\n\terrSecretsGivenValue = errRange.New(\n\t\t\"Invalid secrets variable\",\n\t\t\"The \\\"secrets\\\" variable must not be given a value. Encore will ensure that the secrets are loaded at runtime.\",\n\t)\n\n\terrAnonymousFields = errRange.New(\n\t\t\"Invalid secrets struct\",\n\t\t\"Anonymous fields are not allowed in the secrets struct.\",\n\t)\n\n\terrSecretsMustBeString = errRange.New(\n\t\t\"Invalid secrets struct\",\n\t\t\"Secrets must be of type string.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/secrets/secrets.go",
    "content": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/schema/schemautil\"\n\t\"encr.dev/v2/parser/infra/internal/literals\"\n\t\"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\n// Secrets represents a secrets struct.\ntype Secrets struct {\n\tAST   *ast.StructType\n\tFile  *pkginfo.File // Where the secrets struct is declared\n\tIdent *ast.Ident    // The identifier of the secrets struct\n\tKeys  []string      // Secret keys to load\n\n\t// Spec is the value spec that defines the 'secrets' variable.\n\tSpec *ast.ValueSpec\n}\n\ntype SecretKey struct {\n\tName string\n}\n\nfunc (*Secrets) Kind() resource.Kind         { return resource.Secrets }\nfunc (s *Secrets) Package() *pkginfo.Package { return s.File.Pkg }\nfunc (s *Secrets) ASTExpr() ast.Expr         { return s.AST }\nfunc (s *Secrets) Pos() token.Pos            { return s.AST.Pos() }\nfunc (s *Secrets) End() token.Pos            { return s.AST.End() }\nfunc (s *Secrets) SortKey() string {\n\treturn fmt.Sprintf(\"%s:%s:%d\", s.File.Pkg.ImportPath, s.File.Name, s.AST.Pos())\n}\n\nvar SecretsParser = &resourceparser.Parser{\n\tName:               \"Secrets\",\n\tInterestingImports: resourceparser.RunAlways,\n\n\tRun: func(p *resourceparser.Pass) {\n\t\tsecrets := p.Pkg.Names().PkgDecls[\"secrets\"]\n\t\tif secrets == nil || secrets.Type != token.VAR {\n\t\t\treturn // nothing to do\n\t\t}\n\n\t\t// Note: we can't use schema.ParseTypeDecl since this is not a type declaration.\n\t\t// Resolve the type expression manually instead.\n\t\tspec := secrets.Spec.(*ast.ValueSpec)\n\t\tif spec.Type == nil {\n\t\t\tp.Errs.Add(errSecretsMustBeStruct.AtGoNode(spec, errors.AsError(fmt.Sprintf(\"got %s\", parseutil.NodeType(spec)))))\n\t\t\treturn\n\t\t} else if len(spec.Names) != 1 {\n\t\t\tp.Errs.Add(errSecretsDefinedSeperately.AtGoNode(spec))\n\t\t\treturn\n\t\t} else if len(spec.Values) != 0 {\n\t\t\tp.Errs.Add(errSecretsGivenValue.AtGoNode(spec.Values[0]))\n\t\t\treturn\n\t\t}\n\n\t\tst, ok := p.SchemaParser.ParseType(secrets.File, spec.Type).(schema.StructType)\n\t\tif !ok {\n\t\t\tp.Errs.Add(errSecretsMustBeStruct.AtGoNode(spec, errors.AsError(fmt.Sprintf(\"got %s\", parseutil.NodeType(spec)))))\n\t\t\treturn\n\t\t}\n\n\t\tres := &Secrets{\n\t\t\tAST:   spec.Type.(*ast.StructType),\n\t\t\tFile:  secrets.File,\n\t\t\tSpec:  spec,\n\t\t\tIdent: spec.Names[0],\n\t\t}\n\n\t\tfor _, f := range st.Fields {\n\t\t\tif f.IsAnonymous() {\n\t\t\t\tp.Errs.Add(errAnonymousFields.AtGoNode(f.AST))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !schemautil.IsBuiltinKind(f.Type, schema.String) {\n\t\t\t\tp.Errs.Add(errSecretsMustBeString.AtGoNode(f.AST.Type, errors.AsError(fmt.Sprintf(\"got %s\", literals.PrettyPrint(f.Type.ASTExpr())))))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tres.Keys = append(res.Keys, f.Name.MustGet())\n\t\t}\n\n\t\tp.RegisterResource(res)\n\t\tp.AddNamedBind(res.File, res.Ident, res)\n\t},\n}\n"
  },
  {
    "path": "v2/parser/infra/sqldb/errors.go",
    "content": "package sqldb\n\nimport (\n\t\"encr.dev/pkg/errors\"\n)\n\nvar (\n\tErrDuplicateNames = errRange.New(\n\t\t\"Duplicate Databases\",\n\t\t\"Multiple databases with the same name were found. Database names must be unique.\",\n\t)\n\n\terrRange = errors.Range(\n\t\t\"sqldb\",\n\t\t\"For more information about how to use databases in Encore, see https://encore.dev/docs/primitives/databases\",\n\n\t\terrors.WithRangeSize(20),\n\t)\n\n\terrUnableToParseMigrations = errRange.New(\n\t\t\"Unable to parse migrations\",\n\t\t\"Encore was unable to parse the database migrations. Please ensure that the migrations are in the correct format.\",\n\t)\n\n\terrNamedRequiresDatabaseName = errRange.Newf(\n\t\t\"Invalid call to sqldb.Named\",\n\t\t\"sqldb.Named requires a database name to be passed as the only argument, got %d arguments.\",\n\t)\n\n\terrNamedRequiresDatabaseNameString = errRange.New(\n\t\t\"Invalid call to sqldb.Named\",\n\t\t\"sqldb.Named requires a database name to be passed as a string literal.\",\n\t)\n\terrNewDatabaseArgCount = errRange.Newf(\n\t\t\"Invalid sqldb.NewDatabase call\",\n\t\t\"A call to sqldb.NewDatabase requires 2 arguments: the database name and the config object, got %d arguments.\",\n\t)\n\terrNewDatabaseAbsPath = errRange.New(\n\t\t\"Invalid sqldb.NewDatabase call\",\n\t\t\"The migration path must be a relative path rooted within the package directory, got an absolute path.\",\n\t)\n\terrNewDatabaseNonLocalPath = errRange.New(\n\t\t\"Invalid sqldb.NewDatabase call\",\n\t\t\"The migration path must be a relative path rooted within the package directory, got a non-local path.\",\n\t)\n\terrNewDatabaseMigrationDirNotFound = errRange.New(\n\t\t\"Invalid sqldb.NewDatabase call\",\n\t\t\"The migration directory does not exist.\",\n\t)\n\terrMigrationsNotInMainModule = errRange.New(\n\t\t\"Invalid database migration directory\",\n\t\t\"The migration path must be within the application's main module.\",\n\t)\n\terrInvalidPkgLevelQuery = errRange.Newf(\n\t\t\"Invalid use of sqldb package-level function\",\n\t\t\"The package-level query function sqldb.%s can only be used within Encore services that don't use sqldb.NewDatabase.\",\n\t)\n\tErrDatabaseNotFound = errRange.Newf(\n\t\t\"Unknown sqldb database\",\n\t\t\"No database named %q was found in the application. Ensure it is created somewhere using sqldb.NewDatabase to be able to reference it.\",\n\t)\n)\n"
  },
  {
    "path": "v2/parser/infra/sqldb/implicit.go",
    "content": "package sqldb\n\nimport (\n\t\"cmp\"\n\t\"go/ast\"\n\t\"slices\"\n\t\"sort\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\n// ComputeImplicitUsage computes the implicit usage of SQLDB resources via package-level\n// sqldb.{Query,QueryRow,Exec,etc} calls.\nfunc ComputeImplicitUsage(errs *perr.List, pkgs []*pkginfo.Package, binds []resource.Bind) []usage.Expr {\n\t// Compute the list of package paths that define SQLDB implicit binds.\n\ttype sqldbBind struct {\n\t\tPkg  paths.Pkg\n\t\tBind resource.Bind\n\t}\n\n\tvar sqldbBinds []sqldbBind\n\tfor _, b := range binds {\n\t\tif implicit, ok := b.(*resource.ImplicitBind); ok {\n\t\t\t// Is this a SQLDB resource?\n\t\t\tref := implicit.Resource\n\t\t\tif _, ok := ref.Resource.(*Database); ok {\n\t\t\t\tsqldbBinds = append(sqldbBinds, sqldbBind{Pkg: b.Package().ImportPath, Bind: b})\n\t\t\t} else if ref.Path != nil && ref.Path[0].Kind == resource.SQLDatabase {\n\t\t\t\tsqldbBinds = append(sqldbBinds, sqldbBind{Pkg: b.Package().ImportPath, Bind: b})\n\t\t\t}\n\t\t}\n\t}\n\n\t// Sort them to allow for binary searches.\n\tslices.SortFunc(sqldbBinds, func(a, b sqldbBind) int {\n\t\tif n := cmp.Compare(a.Pkg, b.Pkg); n != 0 {\n\t\t\treturn n\n\t\t}\n\t\treturn cmp.Compare(a.Bind.Pos(), b.Bind.Pos())\n\t})\n\n\tfindBind := func(pkg paths.Pkg) (b resource.Bind, ok bool) {\n\t\ti := sort.Search(len(sqldbBinds), func(i int) bool {\n\t\t\treturn sqldbBinds[i].Pkg >= pkg\n\t\t})\n\t\tif i < len(sqldbBinds) && sqldbBinds[i].Pkg == pkg {\n\t\t\treturn sqldbBinds[i].Bind, true\n\t\t} else if i > 0 && sqldbBinds[i-1].Pkg.LexicallyContains(pkg) {\n\t\t\treturn sqldbBinds[i-1].Bind, true\n\t\t}\n\t\treturn nil, false\n\t}\n\n\tconst sqldbPkg paths.Pkg = \"encore.dev/storage/sqldb\"\n\n\tvar usages []usage.Expr\n\tfor _, pkg := range pkgs {\n\t\tif _, found := pkg.Imports[sqldbPkg]; !found {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, file := range pkg.Files {\n\t\t\tif _, found := file.Imports[sqldbPkg]; !found {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// The file is using sqldb, so scan its AST for usages.\n\t\t\tinsp := file.ASTInspector()\n\t\t\tnames := file.Names()\n\t\t\tinsp.WithStack([]ast.Node{(*ast.SelectorExpr)(nil)}, func(n ast.Node, push bool, stack []ast.Node) bool {\n\t\t\t\tif !push {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\n\t\t\t\tsel := n.(*ast.SelectorExpr)\n\t\t\t\tqn, ok := names.ResolvePkgLevelRef(sel)\n\t\t\t\tif !ok || qn.PkgPath != sqldbPkg {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\n\t\t\t\tswitch qn.Name {\n\t\t\t\tcase \"Exec\", \"ExecTx\", \"QueryRow\", \"QueryRowTx\", \"Query\", \"QueryTx\", \"Begin\":\n\t\t\t\t\tvalidUsage := false\n\t\t\t\t\tif bind, ok := findBind(file.Pkg.ImportPath); ok {\n\t\t\t\t\t\tif u := classifySQLDBUsage(file, bind, sel, stack); u != nil {\n\t\t\t\t\t\t\tusages = append(usages, u)\n\t\t\t\t\t\t\tvalidUsage = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !validUsage {\n\t\t\t\t\t\terrs.Add(errInvalidPkgLevelQuery(qn.Name).AtGoNode(sel))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn true\n\t\t\t})\n\t\t}\n\t}\n\n\treturn usages\n}\n\nfunc classifySQLDBUsage(file *pkginfo.File, bind resource.Bind, sel *ast.SelectorExpr, stack []ast.Node) usage.Expr {\n\tidx := len(stack) - 1\n\tif idx >= 1 {\n\t\tif call, ok := stack[idx-1].(*ast.CallExpr); ok {\n\t\t\treturn &usage.MethodCall{\n\t\t\t\tFile:   file,\n\t\t\t\tBind:   bind,\n\t\t\t\tCall:   call,\n\t\t\t\tMethod: sel.Sel.Name,\n\t\t\t\tArgs:   call.Args,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Otherwise it's a field access.\n\treturn &usage.FieldAccess{\n\t\tFile:  file,\n\t\tBind:  bind,\n\t\tExpr:  sel,\n\t\tField: sel.Sel.Name,\n\t}\n}\n"
  },
  {
    "path": "v2/parser/infra/sqldb/named.go",
    "content": "package sqldb\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/errors\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/infra/internal/literals\"\n\t\"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\nvar NamedParser = &resourceparser.Parser{\n\tName: \"Named SQL Database\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/storage/sqldb\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{Name: \"Named\", PkgPath: \"encore.dev/storage/sqldb\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tParse:       parseNamedSQLDB,\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 0,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parseNamedSQLDB(d parseutil.ReferenceInfo) {\n\tif len(d.Call.Args) != 1 {\n\t\td.Pass.Errs.Add(errNamedRequiresDatabaseName(len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tdbName, ok := literals.ParseString(d.Call.Args[0])\n\tif !ok {\n\t\td.Pass.Errs.Add(\n\t\t\terrNamedRequiresDatabaseNameString.\n\t\t\t\tAtGoNode(d.Call.Args[0], errors.AsError(fmt.Sprintf(\"got %v\", parseutil.NodeType(d.Call.Args[0])))),\n\t\t)\n\t\treturn\n\t}\n\n\tif len(dbName) <= 0 {\n\t\td.Pass.Errs.Add(\n\t\t\terrNamedRequiresDatabaseNameString.AtGoNode(d.Call.Args[0], errors.AsError(\"got an empty string\")),\n\t\t)\n\t}\n\n\td.Pass.AddPathBind(d.File, d.Ident, resource.Path{{resource.SQLDatabase, dbName}})\n}\n"
  },
  {
    "path": "v2/parser/infra/sqldb/sqldb.go",
    "content": "package sqldb\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/infra/internal/literals\"\n\t\"encr.dev/v2/parser/infra/internal/parseutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Database struct {\n\tAST          *ast.CallExpr\n\tPkg          *pkginfo.Package\n\tName         string // The database name\n\tDoc          string\n\tFile         option.Option[*pkginfo.File]\n\tMigrationDir paths.MainModuleRelSlash\n\tMigrations   []MigrationFile\n}\n\nfunc (d *Database) Kind() resource.Kind       { return resource.SQLDatabase }\nfunc (d *Database) Package() *pkginfo.Package { return d.Pkg }\nfunc (d *Database) ResourceName() string      { return d.Name }\nfunc (d *Database) Pos() token.Pos            { return token.NoPos }\nfunc (d *Database) End() token.Pos            { return token.NoPos }\nfunc (d *Database) SortKey() string           { return d.Name }\n\ntype MigrationFile struct {\n\tFilename    string\n\tNumber      uint64\n\tDescription string\n}\n\nvar DatabaseParser = &resourceparser.Parser{\n\tName: \"SQL Database\",\n\n\tInterestingImports: []paths.Pkg{\"encore.dev/storage/sqldb\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tname := pkginfo.QualifiedName{PkgPath: \"encore.dev/storage/sqldb\", Name: \"NewDatabase\"}\n\n\t\tspec := &parseutil.ReferenceSpec{\n\t\t\tMinTypeArgs: 0,\n\t\t\tMaxTypeArgs: 0,\n\t\t\tParse:       parseDatabase,\n\t\t}\n\n\t\tparseutil.FindPkgNameRefs(p.Pkg, []pkginfo.QualifiedName{name}, func(file *pkginfo.File, name pkginfo.QualifiedName, stack []ast.Node) {\n\t\t\tparseutil.ParseReference(p, spec, parseutil.ReferenceData{\n\t\t\t\tFile:         file,\n\t\t\t\tStack:        stack,\n\t\t\t\tResourceFunc: name,\n\t\t\t})\n\t\t})\n\t},\n}\n\nfunc parseDatabase(d parseutil.ReferenceInfo) {\n\terrs := d.Pass.Errs\n\n\tif len(d.Call.Args) != 2 {\n\t\terrs.Add(errNewDatabaseArgCount(len(d.Call.Args)).AtGoNode(d.Call))\n\t\treturn\n\t}\n\n\tdatabaseName := parseutil.ParseResourceName(d.Pass.Errs, \"sqldb.NewDatabase\", \"database name\",\n\t\td.Call.Args[0], parseutil.SnakeName, \"\")\n\tif databaseName == \"\" {\n\t\t// we already reported the error inside ParseResourceName\n\t\treturn\n\t}\n\n\tcfgLit, ok := literals.ParseStruct(d.Pass.Errs, d.File, \"sqldb.DatabaseConfig\", d.Call.Args[1])\n\tif !ok {\n\t\treturn // error reported by ParseStruct\n\t}\n\n\t// Decode the config\n\ttype decodedConfig struct {\n\t\tMigrations string `literal:\",required\"`\n\t}\n\tconfig := literals.Decode[decodedConfig](d.Pass.Errs, cfgLit, nil)\n\n\tif path.IsAbs(config.Migrations) {\n\t\terrs.Add(errNewDatabaseAbsPath.AtGoNode(cfgLit.Expr(\"Migrations\")))\n\t\treturn\n\t}\n\tmigDir := filepath.FromSlash(config.Migrations)\n\tif !filepath.IsLocal(migDir) {\n\t\terrs.Add(errNewDatabaseNonLocalPath.AtGoNode(cfgLit.Expr(\"Migrations\")))\n\t\treturn\n\t}\n\n\tmigrationDir := d.Pass.Pkg.FSPath.Join(migDir)\n\tif fi, err := os.Stat(migrationDir.ToIO()); errors.Is(err, fs.ErrNotExist) || (err == nil && !fi.IsDir()) {\n\t\terrs.Add(errNewDatabaseMigrationDirNotFound.AtGoNode(cfgLit.Expr(\"Migrations\")))\n\t\treturn\n\t} else if err != nil {\n\t\terrs.AddStd(err)\n\t\treturn\n\t}\n\n\t// Compute the relative path to the migration directory from the main module.\n\trelMigrationDir, err := filepath.Rel(d.Pass.MainModuleDir.ToIO(), migrationDir.ToIO())\n\tif err != nil || !filepath.IsLocal(relMigrationDir) {\n\t\terrs.Add(errMigrationsNotInMainModule)\n\t\treturn\n\t}\n\n\tmigrations, err := parseMigrations(migrationDir)\n\tif err != nil {\n\t\terrs.Add(errUnableToParseMigrations.AtGoNode(cfgLit.Expr(\"Migrations\")).Wrapping(err))\n\t\treturn\n\t}\n\n\tdb := &Database{\n\t\tAST:          d.Call,\n\t\tPkg:          d.Pass.Pkg,\n\t\tName:         databaseName,\n\t\tDoc:          d.Doc,\n\t\tMigrationDir: paths.MainModuleRelSlash(filepath.ToSlash(relMigrationDir)),\n\t\tMigrations:   migrations,\n\t}\n\td.Pass.RegisterResource(db)\n\td.Pass.AddBind(d.File, d.Ident, db)\n}\n\nvar MigrationParser = &resourceparser.Parser{\n\tName: \"SQL Database\",\n\n\tInterestingSubdirs: []string{\"migrations\"},\n\tRun: func(p *resourceparser.Pass) {\n\t\tmigrationDir := p.Pkg.FSPath.Join(\"migrations\")\n\t\tmigrations, err := parseMigrations(migrationDir)\n\t\tif err != nil {\n\t\t\t// HACK(andre): We should only look for migration directories inside services,\n\t\t\t// but when this code runs we don't yet know what services exist.\n\t\t\t// For now, use some heuristics to guess if this is a service and otherwise ignore it.\n\t\t\tif !pkgIsLikelyService(p.Pkg) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr := fmt.Errorf(\"parsing db migrations in %s: %v\", p.Pkg.ImportPath, err)\n\t\t\tp.Errs.Add(errUnableToParseMigrations.Wrapping(err))\n\t\t\treturn\n\t\t} else if len(migrations) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\t// HACK(andre): We also need to do the check here, otherwise we get\n\t\t// spurious databases that are defined outside of services.\n\t\tif !pkgIsLikelyService(p.Pkg) {\n\t\t\treturn\n\t\t}\n\n\t\t// Compute the relative path to the migration directory from the main module.\n\t\trelMigrationDir, err := filepath.Rel(p.MainModuleDir.ToIO(), migrationDir.ToIO())\n\t\tif err != nil || !filepath.IsLocal(relMigrationDir) {\n\t\t\tp.Errs.Add(errMigrationsNotInMainModule)\n\t\t\treturn\n\t\t}\n\n\t\tres := &Database{\n\t\t\tPkg:          p.Pkg,\n\t\t\tName:         p.Pkg.Name,\n\t\t\tMigrationDir: paths.MainModuleRelSlash(filepath.ToSlash(relMigrationDir)),\n\t\t\tMigrations:   migrations,\n\t\t}\n\t\tp.RegisterResource(res)\n\t\tp.AddImplicitBind(res)\n\t},\n}\n\nvar migrationRe = regexp.MustCompile(`^(\\d+)(_[^.]+)?\\.(up|down).sql$`)\n\nfunc parseMigrations(migrationDir paths.FS) ([]MigrationFile, error) {\n\tfiles, err := os.ReadDir(migrationDir.ToIO())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not read migrations: %v\", err)\n\t}\n\tmigrations := make([]MigrationFile, 0, len(files))\n\tfor _, f := range files {\n\t\tif f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If the file is not an SQL file ignore it, to allow for other files to be present\n\t\t// in the migration directory. For SQL files we want to ensure they're properly named\n\t\t// so that we complain loudly about potential typos. (It's theoretically possible to\n\t\t// typo the filename extension as well, but it's less likely due to syntax highlighting).\n\t\tif filepath.Ext(strings.ToLower(f.Name())) != \".sql\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatch := migrationRe.FindStringSubmatch(f.Name())\n\t\tif match == nil {\n\t\t\treturn nil, fmt.Errorf(\"db migration %s: invalid name (must be of the format '[123]_[description].[up|down].sql')\",\n\t\t\t\tf.Name())\n\t\t}\n\t\tnum, err := strconv.ParseUint(match[1], 10, 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"db migration %s: invalid version number %q (must be a positive integer)\",\n\t\t\t\tf.Name(), match[1])\n\t\t}\n\n\t\tdescription := strings.TrimPrefix(match[2], \"_\")\n\t\tif match[3] == \"up\" {\n\t\t\tmigrations = append(migrations, MigrationFile{\n\t\t\t\tFilename:    f.Name(),\n\t\t\t\tNumber:      num,\n\t\t\t\tDescription: description,\n\t\t\t})\n\t\t}\n\t}\n\tsort.Slice(migrations, func(i, j int) bool {\n\t\treturn migrations[i].Number < migrations[j].Number\n\t})\n\n\t// Catch invalid migration numbers.\n\tseen := make(map[uint64]bool, len(migrations))\n\tfor _, mig := range migrations {\n\t\tfn, num := mig.Filename, mig.Number\n\t\tif num <= 0 {\n\t\t\treturn nil, fmt.Errorf(\"db migration %s: invalid migration number %d\", fn, num)\n\t\t} else if seen[num] {\n\t\t\treturn nil, fmt.Errorf(\"db migration %s: duplicate migration with number %d\", fn, num)\n\t\t}\n\t\tseen[num] = true\n\t}\n\n\treturn migrations, nil\n}\n\nfunc pkgIsLikelyService(pkg *pkginfo.Package) bool {\n\tisLikelyService := func(file *pkginfo.File) bool {\n\t\tcontents := file.Contents()\n\t\tswitch {\n\t\tcase bytes.Contains(contents, []byte(\"encore:api\")):\n\t\t\treturn true\n\t\tcase bytes.Contains(contents, []byte(\"pubsub.NewSubscription\")):\n\t\t\treturn true\n\t\tcase bytes.Contains(contents, []byte(\"encore:authhandler\")):\n\t\t\treturn true\n\t\tcase bytes.Contains(contents, []byte(\"encore:service\")):\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\tfor _, file := range pkg.Files {\n\t\tif isLikelyService(file) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "v2/parser/infra/sqldb/sqldb_test.go",
    "content": "package sqldb\n\nimport (\n\t\"testing\"\n\n\t\"encr.dev/v2/parser/resource/resourcetest\"\n)\n\nfunc TestParseDatabase(t *testing.T) {\n\ttests := []resourcetest.Case[*Database]{\n\t\t{\n\t\t\tName: \"constructor\",\n\t\t\tCode: `\nvar x = sqldb.NewDatabase(\"name\", sqldb.DatabaseConfig{\n\tMigrations: \"some/migration/path\",\n})\n-- some/migration/path/foo.txt --\n`,\n\t\t\tWant: &Database{\n\t\t\t\tName:         \"name\",\n\t\t\t\tMigrationDir: \"some/migration/path\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"migration_file\",\n\t\t\tCode: `\nvar x = sqldb.NewDatabase(\"name\", sqldb.DatabaseConfig{\n\tMigrations: \"some/migration/path\",\n})\n-- some/migration/path/1_foo.up.sql --\nCREATE TABLE foo (id int);\n`,\n\t\t\tWant: &Database{\n\t\t\t\tName:         \"name\",\n\t\t\t\tMigrationDir: \"some/migration/path\",\n\t\t\t\tMigrations: []MigrationFile{{\n\t\t\t\t\tFilename:    \"1_foo.up.sql\",\n\t\t\t\t\tNumber:      1,\n\t\t\t\t\tDescription: \"foo\",\n\t\t\t\t}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"abs_path\",\n\t\t\tCode: `\nvar x = sqldb.NewDatabase(\"name\", sqldb.DatabaseConfig{\n\tMigrations: \"/path\",\n})\n`,\n\t\t\tWantErrs: []string{`.*The migration path must be a relative path.*`},\n\t\t},\n\t\t{\n\t\t\tName: \"non_local_path\",\n\t\t\tCode: `\nvar x = sqldb.NewDatabase(\"name\", sqldb.DatabaseConfig{\n\tMigrations: \"../path\",\n})\n`,\n\t\t\tWantErrs: []string{`.*The migration path must be a relative path.*`},\n\t\t},\n\t}\n\n\tresourcetest.Run(t, DatabaseParser, tests)\n}\n"
  },
  {
    "path": "v2/parser/infra/sqldb/usage.go",
    "content": "package sqldb\n\nimport (\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype DatabaseUsage struct {\n\tusage.Base\n}\n\nfunc ResolveDatabaseUsage(data usage.ResolveData, db *Database) usage.Usage {\n\treturn &DatabaseUsage{\n\t\tBase: usage.Base{\n\t\t\tFile: data.Expr.DeclaredIn(),\n\t\t\tBind: data.Expr.ResourceBind(),\n\t\t\tExpr: data.Expr,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "v2/parser/internal/utils/prettyprint.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"reflect\"\n\t\"strings\"\n)\n\nfunc PrettyPrint(node ast.Expr) string {\n\tswitch node := node.(type) {\n\tcase *ast.Ident:\n\t\treturn node.Name\n\n\tcase *ast.SelectorExpr:\n\t\treturn fmt.Sprintf(\"%s.%s\", PrettyPrint(node.X), node.Sel.Name)\n\n\tcase *ast.IndexExpr:\n\t\treturn fmt.Sprintf(\"%s[%s]\", PrettyPrint(node.X), PrettyPrint(node.Index))\n\n\tcase *ast.IndexListExpr:\n\t\tindices := make([]string, 0, len(node.Indices))\n\t\tfor _, n := range node.Indices {\n\t\t\tindices = append(indices, PrettyPrint(n))\n\t\t}\n\t\treturn fmt.Sprintf(\"%s[%s]\", PrettyPrint(node.X), strings.Join(indices, \", \"))\n\n\tcase *ast.FuncLit:\n\t\treturn \"a function literal\"\n\n\tcase *ast.StarExpr:\n\t\treturn fmt.Sprintf(\"a pointer to %s\", PrettyPrint(node.X))\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"a %v\", reflect.TypeOf(node))\n\t}\n}\n"
  },
  {
    "path": "v2/parser/parser.go",
    "content": "package parser\n\nimport (\n\t\"cmp\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/scan\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/apis\"\n\t\"encr.dev/v2/parser/apis/api\"\n\t\"encr.dev/v2/parser/apis/authhandler\"\n\t\"encr.dev/v2/parser/apis/servicestruct\"\n\t\"encr.dev/v2/parser/infra/caches\"\n\t\"encr.dev/v2/parser/infra/config\"\n\t\"encr.dev/v2/parser/infra/crons\"\n\t\"encr.dev/v2/parser/infra/metrics\"\n\t\"encr.dev/v2/parser/infra/objects\"\n\t\"encr.dev/v2/parser/infra/pubsub\"\n\t\"encr.dev/v2/parser/infra/secrets\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\nfunc NewParser(c *parsectx.Context) *Parser {\n\tloader := pkginfo.New(c)\n\tschemaParser := schema.NewParser(c, loader)\n\treturn &Parser{\n\t\tc:             c,\n\t\tloader:        loader,\n\t\tschemaParser:  schemaParser,\n\t\tregistry:      resourceparser.NewRegistry(allParsers),\n\t\tusageResolver: newUsageResolver(),\n\t}\n}\n\ntype Parser struct {\n\tc             *parsectx.Context\n\tloader        *pkginfo.Loader\n\tschemaParser  *schema.Parser\n\tregistry      *resourceparser.Registry\n\tusageResolver *usage.Resolver\n}\n\nfunc (p *Parser) MainModule() *pkginfo.Module {\n\treturn p.loader.MainModule()\n}\n\nfunc (p *Parser) RuntimeModule() *pkginfo.Module {\n\treturn p.loader.RuntimeModule()\n}\n\n// Parse parses the given application for uses of the Encore API Framework\n// and the Encore infrastructure SDK.\nfunc (p *Parser) Parse() *Result {\n\tvar (\n\t\tmu        sync.Mutex\n\t\tpkgs      []*pkginfo.Package\n\t\tresources []resource.Resource\n\t\tbinds     []resource.Bind\n\t)\n\n\tscan.ProcessModule(p.c.Errs, p.loader, p.c.MainModuleDir, func(pkg *pkginfo.Package) {\n\t\tif pkg.Name == \"main\" {\n\t\t\t// Ignore main packages that aren't the main package we're building, if any.\n\t\t\tif mainPkg, ok := p.c.Build.MainPkg.Get(); !ok || pkg.ImportPath != mainPkg {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tpass := &resourceparser.Pass{\n\t\t\tContext:      p.c,\n\t\t\tSchemaParser: p.schemaParser,\n\t\t\tPkg:          pkg,\n\t\t}\n\n\t\tinterested := p.registry.InterestedParsers(pkg)\n\t\tfor _, p := range interested {\n\t\t\tp.Run(pass)\n\t\t}\n\n\t\tmu.Lock()\n\t\tpkgs = append(pkgs, pkg)\n\t\tresources = append(resources, pass.Resources()...)\n\t\tbinds = append(binds, pass.Binds()...)\n\t\tmu.Unlock()\n\t})\n\n\t// Normally every resource is independent, but in the case of implicit sqldb\n\t// databases that come up as a result of having a \"migrations\" folder\n\t// we can end up with duplicate resources if we also have a sqldb.NewDatabase call.\n\t// Deduplicate them for now.\n\tresources, binds = deduplicateSQLDBResources(resources, binds)\n\n\t// Sort resources by package and position so the array is stable between runs\n\t// as we process modules in parallel we can't rely on the order of the\n\t// resources being stable coming into this function.\n\tslices.SortFunc(resources, func(a, b resource.Resource) int {\n\t\tp1, p2 := p.c.FS.Position(a.Pos()), p.c.FS.Position(b.Pos())\n\t\tif n := cmp.Compare(p1.Filename, p2.Filename); n != 0 {\n\t\t\treturn n\n\t\t} else if n := cmp.Compare(p1.Line, p2.Line); n != 0 {\n\t\t\treturn n\n\t\t} else if n := cmp.Compare(p1.Column, p2.Column); n != 0 {\n\t\t\treturn n\n\t\t}\n\t\treturn cmp.Compare(a.Pos(), b.Pos())\n\t})\n\n\t// Then sort the binds\n\tslices.SortFunc(binds, func(a, b resource.Bind) int {\n\t\tif a.Package() != b.Package() {\n\t\t\treturn cmp.Compare(a.Package().FSPath, b.Package().FSPath)\n\t\t}\n\n\t\treturn cmp.Compare(a.Pos(), b.Pos())\n\t})\n\n\t// Finally, sort the packages\n\tslices.SortFunc(pkgs, func(a, b *pkginfo.Package) int {\n\t\treturn cmp.Compare(a.FSPath, b.FSPath)\n\t})\n\n\t// Because we've ordered pkgs and binds, usageExprs will be stable\n\tusageExprs := usage.ParseExprs(p.schemaParser, pkgs, binds)\n\n\t// Add the implicit sqldb usages.\n\tusageExprs = append(usageExprs, sqldb.ComputeImplicitUsage(p.c.Errs, pkgs, binds)...)\n\n\treturn computeResult(p.c.Errs, p.MainModule(), p.usageResolver, pkgs, resources, binds, usageExprs)\n}\n\n// allParsers are all the resource parsers we support.\nvar allParsers = []*resourceparser.Parser{\n\tapis.Parser,\n\tcaches.ClusterParser,\n\tcaches.KeyspaceParser,\n\tconfig.LoadParser,\n\tcrons.JobParser,\n\tmetrics.MetricParser,\n\tpubsub.TopicParser,\n\tpubsub.SubscriptionParser,\n\tsecrets.SecretsParser,\n\tsqldb.DatabaseParser,\n\tsqldb.MigrationParser,\n\tsqldb.NamedParser,\n\tobjects.BucketParser,\n}\n\nfunc newUsageResolver() *usage.Resolver {\n\tr := usage.NewResolver()\n\t// Infrastructure SDK\n\tusage.RegisterUsageResolver[*caches.Keyspace](r, caches.ResolveKeyspaceUsage)\n\tusage.RegisterUsageResolver[*config.Load](r, config.ResolveConfigUsage)\n\tusage.RegisterUsageResolver[*pubsub.Topic](r, pubsub.ResolveTopicUsage)\n\tusage.RegisterUsageResolver[*sqldb.Database](r, sqldb.ResolveDatabaseUsage)\n\tusage.RegisterUsageResolver[*objects.Bucket](r, objects.ResolveBucketUsage)\n\n\t// API Framework\n\tusage.RegisterUsageResolver[*api.Endpoint](r, api.ResolveEndpointUsage)\n\tusage.RegisterUsageResolver[*authhandler.AuthHandler](r, authhandler.ResolveAuthHandlerUsage)\n\tusage.RegisterUsageResolver[*servicestruct.ServiceStruct](r, servicestruct.ResolveServiceStructUsage)\n\treturn r\n}\n\n// deduplicateSQLDBResources deduplicates SQL Database resources and their associated binds\n// in the case where we have multiple SQL Database resources with the same name,\n// as a result of having an explicit bind (via sqldb.NewDatabase) and an implicit bind (via a \"migrations\" folder).\nfunc deduplicateSQLDBResources(resources []resource.Resource, binds []resource.Bind) ([]resource.Resource, []resource.Bind) {\n\tbindsPerDB := make(map[string][]resource.Bind)\n\tbindsPerMigrationDir := make(map[paths.MainModuleRelSlash][]resource.Bind)\n\tfor _, b := range binds {\n\t\t// All the binds we're interested in contain a resource and not a path.\n\t\tr := b.ResourceRef().Resource\n\t\tif db, ok := r.(*sqldb.Database); ok {\n\t\t\tbindsPerDB[db.Name] = append(bindsPerDB[db.Name], b)\n\t\t\tbindsPerMigrationDir[db.MigrationDir] = append(bindsPerMigrationDir[db.MigrationDir], b)\n\t\t}\n\t}\n\n\tresourcesToRemove := make(map[resource.Resource]bool)\n\tbindsToRemove := make(map[resource.Bind]bool)\n\tfor _, binds := range bindsPerDB {\n\t\timplicitIdx := slices.IndexFunc(binds, func(b resource.Bind) bool {\n\t\t\t_, ok := b.(*resource.ImplicitBind)\n\t\t\treturn ok\n\t\t})\n\t\texplicitIdx := slices.IndexFunc(binds, func(b resource.Bind) bool {\n\t\t\t_, ok := b.(*resource.ImplicitBind)\n\t\t\treturn !ok\n\t\t})\n\n\t\tif implicitIdx >= 0 && explicitIdx >= 0 {\n\t\t\t// We have both types of binds. Delete the implicit one.\n\t\t\timplicit := binds[implicitIdx]\n\t\t\tbindsToRemove[implicit] = true\n\n\t\t\t// If they refer to different underlying resources, delete the implicit resource.\n\t\t\texplicit := binds[explicitIdx]\n\t\t\tif res := implicit.ResourceRef().Resource; res != nil && res != explicit.ResourceRef().Resource {\n\t\t\t\tresourcesToRemove[res] = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now do the same based on migration dir\n\tfor _, binds := range bindsPerMigrationDir {\n\t\timplicitIdx := slices.IndexFunc(binds, func(b resource.Bind) bool {\n\t\t\t_, ok := b.(*resource.ImplicitBind)\n\t\t\treturn ok\n\t\t})\n\t\texplicitIdx := slices.IndexFunc(binds, func(b resource.Bind) bool {\n\t\t\t_, ok := b.(*resource.ImplicitBind)\n\t\t\treturn !ok\n\t\t})\n\n\t\tif implicitIdx >= 0 && explicitIdx >= 0 {\n\t\t\t// We have both types of binds. Delete the implicit one.\n\t\t\timplicit := binds[implicitIdx]\n\t\t\tbindsToRemove[implicit] = true\n\n\t\t\t// If they refer to different underlying resources, delete the implicit resource.\n\t\t\texplicit := binds[explicitIdx]\n\t\t\tif res := implicit.ResourceRef().Resource; res != nil && res != explicit.ResourceRef().Resource {\n\t\t\t\tresourcesToRemove[res] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(resourcesToRemove) == 0 && len(bindsToRemove) == 0 {\n\t\t// Nothing to do.\n\t\treturn resources, binds\n\t}\n\n\tupdatedResources := make([]resource.Resource, 0, len(resources))\n\tupdatedBinds := make([]resource.Bind, 0, len(binds))\n\n\tfor _, r := range resources {\n\t\tif !resourcesToRemove[r] {\n\t\t\tupdatedResources = append(updatedResources, r)\n\t\t}\n\t}\n\tfor _, b := range binds {\n\t\tif !bindsToRemove[b] {\n\t\t\tupdatedBinds = append(updatedBinds, b)\n\t\t}\n\t}\n\n\treturn updatedResources, updatedBinds\n}\n"
  },
  {
    "path": "v2/parser/parser_test.go",
    "content": "package parser\n\nimport (\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\nfunc Test_deduplicateSQLDBResources(t *testing.T) {\n\tfooDB := &sqldb.Database{Name: \"foo\", MigrationDir: \"./foo/migrations\"}\n\tbarDB := &sqldb.Database{Name: \"bar\", MigrationDir: \"./bar/migrations\"}\n\tquxDB := &sqldb.Database{Name: \"qux\", MigrationDir: \"./bar/migrations\"} // same migrations as bar\n\n\tfooImplicitBind := &resource.ImplicitBind{Resource: resource.ResourceOrPath{Resource: fooDB}}\n\tfooExplicitBind := &resource.PkgDeclBind{Resource: resource.ResourceOrPath{Resource: fooDB}}\n\tbarImplicitBind := &resource.ImplicitBind{Resource: resource.ResourceOrPath{Resource: barDB}}\n\tquxExplicitBind := &resource.PkgDeclBind{Resource: resource.ResourceOrPath{Resource: quxDB}}\n\n\tfoo2DB := &sqldb.Database{Name: \"foo\"}\n\tfoo2ImplicitBind := &resource.ImplicitBind{Resource: resource.ResourceOrPath{Resource: foo2DB}}\n\n\ttests := []struct {\n\t\tname      string\n\t\tres       []resource.Resource\n\t\tbinds     []resource.Bind\n\t\twantRes   []resource.Resource\n\t\twantBinds []resource.Bind\n\t}{\n\t\t{\n\t\t\tname:      \"implicit_only\",\n\t\t\tres:       []resource.Resource{fooDB},\n\t\t\tbinds:     []resource.Bind{fooImplicitBind},\n\t\t\twantRes:   []resource.Resource{fooDB},\n\t\t\twantBinds: []resource.Bind{fooImplicitBind},\n\t\t},\n\t\t{\n\t\t\tname:    \"resource_only\",\n\t\t\tres:     []resource.Resource{fooDB},\n\t\t\twantRes: []resource.Resource{fooDB},\n\t\t},\n\t\t{\n\t\t\tname:      \"explicit_only\",\n\t\t\tres:       []resource.Resource{fooDB},\n\t\t\tbinds:     []resource.Bind{fooExplicitBind},\n\t\t\twantRes:   []resource.Resource{fooDB},\n\t\t\twantBinds: []resource.Bind{fooExplicitBind},\n\t\t},\n\t\t{\n\t\t\tname:      \"deduplicate_simple\",\n\t\t\tres:       []resource.Resource{fooDB},\n\t\t\tbinds:     []resource.Bind{fooExplicitBind, fooImplicitBind},\n\t\t\twantRes:   []resource.Resource{fooDB},\n\t\t\twantBinds: []resource.Bind{fooExplicitBind},\n\t\t},\n\t\t{\n\t\t\tname:      \"deduplicate_keep_other_db\",\n\t\t\tres:       []resource.Resource{fooDB, barDB},\n\t\t\tbinds:     []resource.Bind{fooExplicitBind, barImplicitBind, fooImplicitBind},\n\t\t\twantRes:   []resource.Resource{fooDB, barDB},\n\t\t\twantBinds: []resource.Bind{fooExplicitBind, barImplicitBind},\n\t\t},\n\t\t{\n\t\t\tname:      \"deduplicate_multiple_identical_resources\",\n\t\t\tres:       []resource.Resource{fooDB, foo2DB},\n\t\t\tbinds:     []resource.Bind{fooExplicitBind, foo2ImplicitBind},\n\t\t\twantRes:   []resource.Resource{fooDB},\n\t\t\twantBinds: []resource.Bind{fooExplicitBind},\n\t\t},\n\t\t{\n\t\t\tname:      \"deduplicate_different_db\",\n\t\t\tres:       []resource.Resource{barDB, quxDB},\n\t\t\tbinds:     []resource.Bind{barImplicitBind, quxExplicitBind},\n\t\t\twantRes:   []resource.Resource{quxDB},\n\t\t\twantBinds: []resource.Bind{quxExplicitBind},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\tgotRes, gotBinds := deduplicateSQLDBResources(tt.res, tt.binds)\n\t\t\tc.Assert(gotRes, testutil.ResourceDeepEquals, tt.wantRes)\n\t\t\tc.Assert(gotBinds, testutil.ResourceDeepEquals, tt.wantBinds)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/resource/bind.go",
    "content": "package resource\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\ntype Bind interface {\n\tPos() token.Pos\n\tResourceRef() ResourceOrPath\n\tPackage() *pkginfo.Package\n\tDeclaredIn() option.Option[*pkginfo.File]\n\n\t// DescriptionForTest describes the bind for testing purposes.\n\tDescriptionForTest() string\n}\n\n// A PkgDeclBind is a bind consisting of a package declaration.\ntype PkgDeclBind struct {\n\tResource ResourceOrPath\n\tFile     *pkginfo.File\n\n\t// BoundName is the package-level identifier the bind is declared with.\n\tBoundName *ast.Ident\n}\n\nfunc (b *PkgDeclBind) Pos() token.Pos              { return b.BoundName.Pos() }\nfunc (b *PkgDeclBind) ResourceRef() ResourceOrPath { return b.Resource }\nfunc (b *PkgDeclBind) Package() *pkginfo.Package   { return b.File.Pkg }\nfunc (b *PkgDeclBind) DescriptionForTest() string  { return b.QualifiedName().NaiveDisplayName() }\nfunc (b *PkgDeclBind) DeclaredIn() option.Option[*pkginfo.File] {\n\treturn option.Some(b.File)\n}\n\n// QualifiedName returns the qualified name of the resource.\nfunc (b *PkgDeclBind) QualifiedName() pkginfo.QualifiedName {\n\treturn pkginfo.QualifiedName{\n\t\tPkgPath: b.File.Pkg.ImportPath,\n\t\tName:    b.BoundName.Name,\n\t}\n}\n\n// An AnonymousBind is similar to PkgDeclBind in that it's a package declaration,\n// but unlike PkgDeclBind it's bound to an \"_\" identifier so that it has no name.\ntype AnonymousBind struct {\n\tResource ResourceOrPath\n\tFile     *pkginfo.File\n}\n\nfunc (b *AnonymousBind) Pos() token.Pos              { return b.File.Pkg.AST.Pos() }\nfunc (b *AnonymousBind) ResourceRef() ResourceOrPath { return b.Resource }\nfunc (b *AnonymousBind) Package() *pkginfo.Package   { return b.File.Pkg }\nfunc (b *AnonymousBind) DescriptionForTest() string  { return \"anonymous\" }\nfunc (b *AnonymousBind) DeclaredIn() option.Option[*pkginfo.File] {\n\treturn option.Some(b.File)\n}\n\n// An ImplicitBind is a bind that implicitly binds to a package and its subpackages.\ntype ImplicitBind struct {\n\tResource ResourceOrPath\n\tPkg      *pkginfo.Package\n}\n\nfunc (b *ImplicitBind) Pos() token.Pos              { return b.Pkg.AST.Pos() }\nfunc (b *ImplicitBind) ResourceRef() ResourceOrPath { return b.Resource }\nfunc (b *ImplicitBind) Package() *pkginfo.Package   { return b.Pkg }\nfunc (b *ImplicitBind) DescriptionForTest() string  { return \"implicit\" }\nfunc (b *ImplicitBind) DeclaredIn() option.Option[*pkginfo.File] {\n\treturn option.None[*pkginfo.File]()\n}\n\n// ResourceOrPath is a reference to a particular resource,\n// either referencing the resource object directly\n// or through a path.\ntype ResourceOrPath struct {\n\tResource Resource\n\tPath     Path\n}\n\ntype Path []PathEntry\n\ntype PathEntry struct {\n\tKind Kind\n\tName string\n}\n"
  },
  {
    "path": "v2/parser/resource/resource.go",
    "content": "package resource\n\nimport (\n\t\"go/ast\"\n)\n\n//go:generate stringer -type=Kind -output=resource_string.go\n\ntype Kind int\n\nconst (\n\tUnknown Kind = iota\n\n\t// Infrastructure SDK Resources\n\tPubSubTopic\n\tPubSubSubscription\n\tSQLDatabase\n\tMetric\n\tCronJob\n\tCacheCluster\n\tCacheKeyspace\n\tConfigLoad\n\tSecrets\n\tBucket\n\n\t// API Framework Resources\n\tAPIEndpoint\n\tAuthHandler\n\tMiddleware\n\tServiceStruct\n)\n\ntype Resource interface {\n\t// Node is embedded so we can use the resource in a [posmap.Map].\n\t// The position should be the position of the resource declaration.\n\tast.Node\n\n\t// Kind is the kind of resource this is.\n\tKind() Kind\n\n\t// SortKey is a string that can be used to sort resources.\n\t// The sort key's value is arbitrary but should provide a\n\t// consistent sort order regardless of the parsing order.\n\t// The sort key only matters between resources of the same Kind.\n\tSortKey() string\n}\n\ntype Named interface {\n\tResource\n\n\t// ResourceName is the name of the resource.\n\tResourceName() string\n}\n"
  },
  {
    "path": "v2/parser/resource/resource_string.go",
    "content": "// Code generated by \"stringer -type=Kind -output=resource_string.go\"; DO NOT EDIT.\n\npackage resource\n\nimport \"strconv\"\n\nfunc _() {\n\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n\t// Re-run the stringer command to generate them again.\n\tvar x [1]struct{}\n\t_ = x[Unknown-0]\n\t_ = x[PubSubTopic-1]\n\t_ = x[PubSubSubscription-2]\n\t_ = x[SQLDatabase-3]\n\t_ = x[Metric-4]\n\t_ = x[CronJob-5]\n\t_ = x[CacheCluster-6]\n\t_ = x[CacheKeyspace-7]\n\t_ = x[ConfigLoad-8]\n\t_ = x[Secrets-9]\n\t_ = x[Bucket-10]\n\t_ = x[APIEndpoint-11]\n\t_ = x[AuthHandler-12]\n\t_ = x[Middleware-13]\n\t_ = x[ServiceStruct-14]\n}\n\nconst _Kind_name = \"UnknownPubSubTopicPubSubSubscriptionSQLDatabaseMetricCronJobCacheClusterCacheKeyspaceConfigLoadSecretsBucketAPIEndpointAuthHandlerMiddlewareServiceStruct\"\n\nvar _Kind_index = [...]uint8{0, 7, 18, 36, 47, 53, 60, 72, 85, 95, 102, 108, 119, 130, 140, 153}\n\nfunc (i Kind) String() string {\n\tif i < 0 || i >= Kind(len(_Kind_index)-1) {\n\t\treturn \"Kind(\" + strconv.FormatInt(int64(i), 10) + \")\"\n\t}\n\treturn _Kind_name[_Kind_index[i]:_Kind_index[i+1]]\n}\n"
  },
  {
    "path": "v2/parser/resource/resourceparser/registry.go",
    "content": "package resourceparser\n\nimport (\n\t\"os\"\n\t\"slices\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n)\n\nfunc NewRegistry(parsers []*Parser) *Registry {\n\tforImports, always := parsersForImports(parsers)\n\tsubdirs := fns.Filter(parsers, func(p *Parser) bool { return len(p.InterestingSubdirs) > 0 })\n\treturn &Registry{\n\t\tparsers:              parsers,\n\t\talwaysInterested:     always,\n\t\tinterestedForImports: forImports,\n\t\tsubdirsInterested:    subdirs,\n\t}\n}\n\ntype Registry struct {\n\tparsers []*Parser\n\n\t// alwaysInterested are the parses that should be run against all packages.\n\talwaysInterested []*Parser\n\n\t// interestedForImports maps from import paths to the list of parsers\n\t// interested in packages that import that package.\n\tinterestedForImports map[paths.Pkg][]*Parser\n\n\t// subdirsInterested are the parses that are interested in\n\t// specific subdirs.\n\tsubdirsInterested []*Parser\n}\n\n// InterestedParsers returns the parsers interested in processing a given package.\nfunc (r *Registry) InterestedParsers(pkg *pkginfo.Package) []*Parser {\n\tparsers := slices.Clone(r.alwaysInterested)\n\n\taddParser := func(p *Parser) {\n\t\tif !slices.Contains(parsers, p) {\n\t\t\tparsers = append(parsers, p)\n\t\t}\n\t}\n\n\t// Find the interested parsers based on imports.\n\tfor imp := range pkg.Imports {\n\t\t// Add the parsers that are interested in this package as long\n\t\t// as they're not already in the list.\n\t\t// Note: this is O(n^2) but n is small, so this should be faster than maintaining a set.\n\t\tfor _, p := range r.interestedForImports[imp] {\n\t\t\taddParser(p)\n\t\t}\n\t}\n\n\t// Find the interested parsers based on subdirs.\nParserLoop:\n\tfor _, p := range r.subdirsInterested {\n\t\tfor _, dir := range p.InterestingSubdirs {\n\t\t\tif stat, err := os.Stat(pkg.FSPath.Join(dir).ToIO()); err == nil && stat.IsDir() {\n\t\t\t\taddParser(p)\n\t\t\t\tcontinue ParserLoop\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parsers\n}\n\n// parsersForImports returns a map from package paths to the list of parsers\n// interested in that package.\nfunc parsersForImports(parsers []*Parser) (forImports map[paths.Pkg][]*Parser, always []*Parser) {\n\tforImports = make(map[paths.Pkg][]*Parser)\n\nParserLoop:\n\tfor _, parser := range parsers {\n\t\tfor _, imp := range parser.InterestingImports {\n\t\t\tif imp == \"*\" {\n\t\t\t\talways = append(always, parser)\n\t\t\t\tcontinue ParserLoop\n\t\t\t}\n\t\t\tforImports[imp] = append(forImports[imp], parser)\n\t\t}\n\t}\n\treturn forImports, always\n}\n"
  },
  {
    "path": "v2/parser/resource/resourceparser/resourceparser.go",
    "content": "package resourceparser\n\nimport (\n\t\"go/ast\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\ntype Parser struct {\n\tName string\n\n\t// InterestingImports are the imports paths that the parser is interested in.\n\t// If a package imports any one of them, the Run method is invoked.\n\tInterestingImports []paths.Pkg\n\n\t// InterestingSubdirs are the subdirectories of a package that a parser is interested in.\n\t// If a package has any one of these subdirectories, the Run method is invoked.\n\t// Its purpose is to support our current way of defining databases via a \"migrations\" dir.\n\tInterestingSubdirs []string\n\n\tRun func(*Pass)\n}\n\n// RunAlways is a value for InterestingImports to indicate to always run the parser.\nvar RunAlways = []paths.Pkg{\"*\"}\n\ntype Pass struct {\n\t*parsectx.Context\n\tSchemaParser *schema.Parser\n\n\tPkg *pkginfo.Package\n\n\tresources []resource.Resource\n\tbinds     []resource.Bind\n}\n\nfunc (p *Pass) RegisterResource(resource resource.Resource) {\n\tp.resources = append(p.resources, resource)\n}\n\nfunc (p *Pass) Resources() []resource.Resource {\n\treturn p.resources\n}\n\nfunc (p *Pass) Binds() []resource.Bind {\n\treturn p.binds\n}\n\nfunc (p *Pass) AddBind(file *pkginfo.File, boundName option.Option[*ast.Ident], res resource.Resource) {\n\tif id, ok := boundName.Get(); ok {\n\t\tp.AddNamedBind(file, id, res)\n\t} else {\n\t\tp.AddAnonymousBind(file, res)\n\t}\n}\n\nfunc (p *Pass) AddNamedBind(file *pkginfo.File, boundName *ast.Ident, res resource.Resource) {\n\tif boundName.Name == \"_\" {\n\t\tp.AddAnonymousBind(file, res)\n\t} else {\n\t\tp.binds = append(p.binds, &resource.PkgDeclBind{\n\t\t\tResource:  resource.ResourceOrPath{Resource: res},\n\t\t\tFile:      file,\n\t\t\tBoundName: boundName,\n\t\t})\n\t}\n}\n\nfunc (p *Pass) AddAnonymousBind(file *pkginfo.File, res resource.Resource) {\n\tp.binds = append(p.binds, &resource.AnonymousBind{\n\t\tResource: resource.ResourceOrPath{Resource: res},\n\t\tFile:     file,\n\t})\n}\n\nfunc (p *Pass) AddPathBind(file *pkginfo.File, boundName option.Option[*ast.Ident], path resource.Path) {\n\tif len(path) == 0 {\n\t\tpanic(\"AddPathBind: empty path\")\n\t}\n\n\tif id, ok := boundName.Get(); !ok || id.Name == \"_\" {\n\t\tp.binds = append(p.binds, &resource.AnonymousBind{\n\t\t\tResource: resource.ResourceOrPath{Path: path},\n\t\t\tFile:     file,\n\t\t})\n\t} else {\n\t\tp.binds = append(p.binds, &resource.PkgDeclBind{\n\t\t\tResource:  resource.ResourceOrPath{Path: path},\n\t\t\tFile:      file,\n\t\t\tBoundName: id,\n\t\t})\n\t}\n}\n\nfunc (p *Pass) AddImplicitBind(res resource.Resource) {\n\tp.binds = append(p.binds, &resource.ImplicitBind{\n\t\tResource: resource.ResourceOrPath{Resource: res},\n\t\tPkg:      p.Pkg,\n\t})\n}\n"
  },
  {
    "path": "v2/parser/resource/resourcetest/resourcetest.go",
    "content": "package resourcetest\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/resourceparser\"\n)\n\ntype Case[R resource.Resource] struct {\n\tName     string\n\tImports  []string\n\tCode     string\n\tWant     R\n\tWantErrs []string\n}\n\nfunc Run[R resource.Resource](t *testing.T, parser *resourceparser.Parser, tests []Case[R], cmpOpts ...cmp.Option) {\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test Case[R]) *txtar.Archive {\n\t\timportList := []string{\"context\"}\n\t\tfor _, imp := range parser.InterestingImports {\n\t\t\timportList = append(importList, imp.String())\n\t\t}\n\t\timportList = append(importList, test.Imports...)\n\n\t\timports := \"\"\n\t\tif len(importList) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range importList {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.Code + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.Name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tl := pkginfo.New(tc.Context)\n\t\t\tschemaParser := schema.NewParser(tc.Context, l)\n\n\t\t\tif len(test.WantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.WantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpkg := l.MustLoadPkg(token.NoPos, \"example.com\")\n\t\t\tpass := &resourceparser.Pass{\n\t\t\t\tContext:      tc.Context,\n\t\t\t\tSchemaParser: schemaParser,\n\t\t\t\tPkg:          pkg,\n\t\t\t}\n\t\t\tparser.Run(pass)\n\t\t\tgot := pass.Resources()\n\n\t\t\tif len(test.WantErrs) == 0 {\n\t\t\t\tc.Assert(got, qt.HasLen, 1)\n\n\t\t\t\tlookupMap := map[string]string{\n\t\t\t\t\t\"APPROOT\": tc.MainModuleDir.ToIO(),\n\t\t\t\t}\n\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\topts := append([]cmp.Option{\n\t\t\t\t\tcmpopts.IgnoreInterfaces(struct{ ast.Expr }{}),\n\t\t\t\t\tcmpopts.IgnoreTypes(&schema.FuncDecl{}, &schema.TypeDecl{}, &pkginfo.File{}, &pkginfo.Package{}, token.Pos(0)),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t\tcmpopts.IgnoreUnexported(schema.StructField{}, schema.NamedType{}),\n\t\t\t\t\tcmp.AllowUnexported(option.Option[*pkginfo.File]{}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.Package) bool {\n\t\t\t\t\t\treturn a.ImportPath == b.ImportPath\n\t\t\t\t\t}),\n\n\t\t\t\t\tcmp.Comparer(func(a, b paths.FS) bool {\n\t\t\t\t\t\treturn rewrite(a, lookupMap) == rewrite(b, lookupMap)\n\t\t\t\t\t}),\n\t\t\t\t\tcmp.Comparer(func(a, b *pkginfo.PkgDeclInfo) bool {\n\t\t\t\t\t\t// HACK(andre) We only check the subset of information that\n\t\t\t\t\t\t// the test helpers actually set. We should be more careful.\n\t\t\t\t\t\treturn a.Name == b.Name\n\t\t\t\t\t}),\n\t\t\t\t}, cmpOpts...)\n\t\t\t\tc.Assert(got[0], qt.CmpEquals(opts...), test.Want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc rewrite[T ~string](val T, lookup map[string]string) T {\n\treturn T(os.Expand(string(val), func(key string) string {\n\t\treturn lookup[key]\n\t}))\n}\n"
  },
  {
    "path": "v2/parser/resource/usage/resolver.go",
    "content": "package usage\n\nimport (\n\t\"go/ast\"\n\t\"go/token\"\n\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\n// Usage describes an infrastructure usage being used.\ntype Usage interface {\n\tast.Node\n\n\tusage() // marker method\n\tResourceBind() resource.Bind\n\tASTExpr() ast.Expr\n\tDeclaredIn() *pkginfo.File\n}\n\ntype Base struct {\n\tFile *pkginfo.File\n\tBind resource.Bind\n\tExpr Expr\n}\n\nfunc (b *Base) DeclaredIn() *pkginfo.File   { return b.File }\nfunc (b *Base) ASTExpr() ast.Expr           { return b.Expr.ASTExpr() }\nfunc (b *Base) ResourceBind() resource.Bind { return b.Bind }\nfunc (b *Base) Pos() token.Pos              { return b.Expr.Pos() }\nfunc (b *Base) End() token.Pos              { return b.Expr.End() }\n\nfunc (b *Base) usage() {}\n\ntype Resolver struct {\n\tresolvers map[resource.Kind]func(ResolveData, resource.Resource) Usage\n}\n\nfunc (r *Resolver) Resolve(errs *perr.List, expr Expr, res resource.Resource) option.Option[Usage] {\n\tfn, ok := r.resolvers[res.Kind()]\n\tif !ok {\n\t\treturn option.None[Usage]()\n\t}\n\tdata := ResolveData{\n\t\tErrs: errs,\n\t\tExpr: expr,\n\t}\n\treturn option.AsOptional(fn(data, res))\n}\n\nfunc NewResolver() *Resolver {\n\treturn &Resolver{\n\t\tresolvers: make(map[resource.Kind]func(ResolveData, resource.Resource) Usage),\n\t}\n}\n\nfunc RegisterUsageResolver[Res resource.Resource](r *Resolver, fn func(ResolveData, Res) Usage) {\n\tvar zero Res\n\n\tif r.resolvers[zero.Kind()] != nil {\n\t\tpanic(\"usage resolver already registered for type \" + zero.Kind().String())\n\t}\n\n\tr.resolvers[zero.Kind()] = func(data ResolveData, res resource.Resource) Usage {\n\t\treturn fn(data, res.(Res))\n\t}\n}\n"
  },
  {
    "path": "v2/parser/resource/usage/testdata/pubsub_usage.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nimport \"encore.dev/pubsub\"\n\ntype Message struct{}\n\nvar Topic = pubsub.NewTopic[*Message](\"foo\", pubsub.TopicConfig{\n    DeliveryGuarantee: pubsub.AtLeastOnce,\n})\n\nvar Sub = pubsub.NewSubscription(Topic, \"bar\", // use svc.Topic fn pubsub.NewSubscription arg 0\n    pubsub.SubscriptionConfig{\n        Handler: func(m *Message) error {\n            return nil\n        },\n    },\n)\n\nfunc init() {\n    Topic.Foo() // use svc.Topic call Foo\n}\n\n-- usage/usage.go --\npackage usage\nimport \"example.com/svc\"\n\nvar x = svc.Topic.Publish(svc.Message{}) // use svc.Topic call Publish\nvar y = Topic.Publish(svc.Message{})\n"
  },
  {
    "path": "v2/parser/resource/usage/testdata/secret_usage.txt",
    "content": "-- svc/svc.go --\npackage svc\n\nvar secrets struct {\n    Foo string\n}\n\nfunc init() {\n    secrets.Foo // use svc.secrets field Foo\n    x := secrets // use svc.secrets other\n}\n"
  },
  {
    "path": "v2/parser/resource/usage/testdata/sqldb_usage.txt",
    "content": "-- svc/migrations/1_foo.up.sql --\n-- svc/svc.go --\npackage svc\n\nimport \"context\"\nimport \"encore.dev/storage/sqldb\"\n\nvar DB = sqldb.Named(\"svc\")\n\nfunc init() {\n    DB.Foo() // use svc.DB call Foo\n    DB.Foo // use svc.DB field Foo\n    DB // use svc.DB other\n\n    1 + DB // use svc.DB other\n    x = DB // use svc.DB other\n}\n\n//encore:api public\nfunc Dummy(context.Context) error { return nil }\n\n-- usage/usage.go --\npackage usage\nimport \"example.com/svc\"\n\nvar x = svc.DB.QueryRow() // use svc.DB call QueryRow\nvar y = DB.Publish(svc.Message{})\n"
  },
  {
    "path": "v2/parser/resource/usage/usage.go",
    "content": "package usage\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"slices\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/schema\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\n// ResolveData is the data being passed to usage resolver functions.\ntype ResolveData struct {\n\tErrs *perr.List\n\tExpr Expr\n}\n\ntype Expr interface {\n\t// Node allows use to use the expression in error messages\n\t// where the Pos/End is used to point directly at the resource\n\t// bind being used, rather than the overall expression.\n\tast.Node\n\n\tResourceBind() resource.Bind\n\tASTExpr() ast.Expr\n\tDeclaredIn() *pkginfo.File\n\n\t// DescriptionForTest describes the expression for testing purposes.\n\tDescriptionForTest() string\n}\n\n// FuncCall describes a resource being called as a function.\ntype FuncCall struct {\n\tFile *pkginfo.File\n\tBind resource.Bind\n\tCall *ast.CallExpr\n\tArgs []ast.Expr\n}\n\nfunc (f *FuncCall) DeclaredIn() *pkginfo.File   { return f.File }\nfunc (f *FuncCall) ASTExpr() ast.Expr           { return f.Call }\nfunc (f *FuncCall) ResourceBind() resource.Bind { return f.Bind }\nfunc (f *FuncCall) DescriptionForTest() string  { return \"called\" }\nfunc (f *FuncCall) Pos() token.Pos              { return f.Call.Fun.Pos() }\nfunc (f *FuncCall) End() token.Pos              { return f.Call.Fun.End() }\n\n// MethodCall describes a resource usage via a method call.\ntype MethodCall struct {\n\tFile   *pkginfo.File\n\tBind   resource.Bind\n\tCall   *ast.CallExpr\n\tMethod string\n\tArgs   []ast.Expr\n}\n\nfunc (m *MethodCall) DeclaredIn() *pkginfo.File   { return m.File }\nfunc (m *MethodCall) ASTExpr() ast.Expr           { return m.Call }\nfunc (m *MethodCall) ResourceBind() resource.Bind { return m.Bind }\nfunc (m *MethodCall) DescriptionForTest() string  { return fmt.Sprintf(\"call %s\", m.Method) }\nfunc (m *MethodCall) Pos() token.Pos              { return m.Call.Fun.Pos() }\nfunc (m *MethodCall) End() token.Pos              { return m.Call.Fun.End() }\n\n// FieldAccess describes a resource usage via a field access.\ntype FieldAccess struct {\n\tFile  *pkginfo.File\n\tBind  resource.Bind\n\tExpr  *ast.SelectorExpr\n\tField string\n}\n\nfunc (f *FieldAccess) DeclaredIn() *pkginfo.File   { return f.File }\nfunc (f *FieldAccess) ASTExpr() ast.Expr           { return f.Expr }\nfunc (f *FieldAccess) ResourceBind() resource.Bind { return f.Bind }\nfunc (f *FieldAccess) DescriptionForTest() string  { return fmt.Sprintf(\"field %s\", f.Field) }\nfunc (f *FieldAccess) Pos() token.Pos              { return f.Expr.Pos() }\nfunc (f *FieldAccess) End() token.Pos              { return f.Expr.End() }\n\n// FuncArg describes a resource being used as a function argument.\ntype FuncArg struct {\n\tFile *pkginfo.File\n\tBind resource.Bind\n\tCall *ast.CallExpr\n\n\t// TypeArgs are the type arguments to the function being called, if any.\n\tTypeArgs []schema.Type\n\n\t// ArgIdx is the function argument index that represents\n\t// the resource bind, starting at 0.\n\tArgIdx int\n\n\t// PkgFunc is the package-level function that's being called.\n\t// It's None if the function is not a package-level function.\n\tPkgFunc option.Option[pkginfo.QualifiedName]\n}\n\nfunc (f *FuncArg) DeclaredIn() *pkginfo.File   { return f.File }\nfunc (f *FuncArg) ASTExpr() ast.Expr           { return f.Call }\nfunc (f *FuncArg) ResourceBind() resource.Bind { return f.Bind }\nfunc (f *FuncArg) DescriptionForTest() string {\n\tif fn, ok := f.PkgFunc.Get(); ok {\n\t\treturn fmt.Sprintf(\"fn %s arg %d\", fn.NaiveDisplayName(), f.ArgIdx)\n\t}\n\treturn fmt.Sprintf(\"arg %d\", f.ArgIdx)\n}\nfunc (f *FuncArg) Pos() token.Pos { return f.Call.Args[f.ArgIdx].Pos() }\nfunc (f *FuncArg) End() token.Pos { return f.Call.Args[f.ArgIdx].End() }\n\n// Other describes any other resource usage.\ntype Other struct {\n\tFile    *pkginfo.File\n\tBind    resource.Bind\n\tExpr    ast.Expr\n\tBindRef ast.Expr\n}\n\nfunc (o *Other) DeclaredIn() *pkginfo.File   { return o.File }\nfunc (o *Other) ASTExpr() ast.Expr           { return o.Expr }\nfunc (o *Other) ResourceBind() resource.Bind { return o.Bind }\nfunc (o *Other) DescriptionForTest() string  { return \"other\" }\nfunc (o *Other) Pos() token.Pos              { return o.BindRef.Pos() }\nfunc (o *Other) End() token.Pos              { return o.BindRef.End() }\n\nfunc ParseExprs(schema *schema.Parser, pkgs []*pkginfo.Package, binds []resource.Bind) []Expr {\n\tp := &usageParser{\n\t\tschema:      schema,\n\t\tbindsPerPkg: make(map[paths.Pkg][]resource.Bind, len(binds)),\n\t\tbindNames:   make(map[pkginfo.QualifiedName]resource.Bind, len(binds)),\n\t}\n\tfor _, b := range binds {\n\t\tpkg := b.Package()\n\t\tp.bindsPerPkg[pkg.ImportPath] = append(p.bindsPerPkg[pkg.ImportPath], b)\n\n\t\tif pkgDecl, ok := b.(*resource.PkgDeclBind); ok {\n\t\t\tp.bindNames[pkgDecl.QualifiedName()] = b\n\t\t}\n\t}\n\n\tvar usages []Expr\n\tfor _, pkg := range pkgs {\n\t\tusages = append(usages, p.scanUsage(pkg)...)\n\t}\n\n\treturn usages\n}\n\ntype usageParser struct {\n\tschema      *schema.Parser\n\tbindsPerPkg map[paths.Pkg][]resource.Bind\n\tbindNames   map[pkginfo.QualifiedName]resource.Bind\n}\n\nfunc (p *usageParser) scanUsage(pkg *pkginfo.Package) (usages []Expr) {\n\texternal, internal, files := p.bindsToScanFor(pkg)\n\n\thaveExternal := len(external) > 0\n\thaveInternal := len(internal) > 0\n\n\t// Compute types to scan for.\n\tvar types []ast.Node\n\tif haveExternal {\n\t\ttypes = append(types, (*ast.SelectorExpr)(nil))\n\t}\n\tif haveInternal {\n\t\ttypes = append(types, (*ast.Ident)(nil))\n\t}\n\n\tfor _, f := range files {\n\t\tinspector := f.ASTInspector()\n\t\tnames := f.Names()\n\t\tinspector.WithStack(types, func(n ast.Node, push bool, stack []ast.Node) bool {\n\t\t\tif !push {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// If we're scanning for both *ast.SelectorExpr and *ast.Ident,\n\t\t\t// we will first scan the *ast.SelectorExpr and then recurse and then scan the *ast.Ident.\n\t\t\t// To avoid having to deal with this case, detect this case and ignore the *ast.Ident\n\t\t\t// the second time around.\n\t\t\tif haveExternal && haveInternal {\n\t\t\t\tif id, ok := n.(*ast.Ident); ok {\n\t\t\t\t\tif sel, ok := stack[len(stack)-2].(*ast.SelectorExpr); ok && sel.Sel == id {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\texpr := n.(ast.Expr) // guaranteed since the types we scan for are all expressions\n\t\t\tif qn, ok := names.ResolvePkgLevelRef(expr); ok {\n\t\t\t\tif bind, ok := p.bindNames[qn]; ok {\n\t\t\t\t\t// Make sure this is not the actual bind definition, to avoid reporting spurious usages.\n\t\t\t\t\tif !p.isBind(pkg, expr, bind) {\n\t\t\t\t\t\tif u := p.classifyExpr(f, bind, stack); u != nil {\n\t\t\t\t\t\t\tusages = append(usages, u)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t}\n\n\treturn usages\n}\n\n// bindsToScanFor returns the binds to scan for in a given package,\n// and which files to scan.\n// The 'external' binds are those that are imported from other packages,\n// and 'internal' binds are those that are defined in the same package.\nfunc (p *usageParser) bindsToScanFor(pkg *pkginfo.Package) (external, internal []resource.Bind, files []*pkginfo.File) {\n\tinternal = p.bindsPerPkg[pkg.ImportPath]\n\n\tif len(internal) > 0 {\n\t\t// If we have any internal binds we need to scan all files,\n\t\t// since we can't rely on the file-level imports to tell us\n\t\t// which files to scan.\n\t\tfiles = pkg.Files\n\t}\n\n\tfor imp := range pkg.Imports {\n\t\texternal = append(external, p.bindsPerPkg[imp]...)\n\t}\n\n\tif len(external) > 0 && len(internal) == 0 {\n\t\t// If we have external binds but no internal binds,\n\t\t// figure out which files to parse precisely.\n\tFileLoop:\n\t\tfor _, f := range pkg.Files {\n\t\t\tfor imp := range f.Imports {\n\t\t\t\tif len(p.bindsPerPkg[imp]) > 0 {\n\t\t\t\t\tfiles = append(files, f)\n\t\t\t\t\tcontinue FileLoop\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (p *usageParser) isBind(pkg *pkginfo.Package, expr ast.Expr, bind resource.Bind) bool {\n\tif pkg.ImportPath != bind.Package().ImportPath {\n\t\treturn false\n\t}\n\n\t// If the bind isn't a package decl it can't be this bind.\n\tpkgDecl, ok := bind.(*resource.PkgDeclBind)\n\tif !ok {\n\t\treturn false\n\t}\n\n\tswitch x := expr.(type) {\n\tcase *ast.SelectorExpr:\n\t\treturn x.Sel == pkgDecl.BoundName\n\tcase *ast.Ident:\n\t\treturn x == pkgDecl.BoundName\n\t}\n\treturn false\n}\n\nfunc (p *usageParser) classifyExpr(file *pkginfo.File, bind resource.Bind, stack []ast.Node) Expr {\n\tidx := len(stack) - 1\n\n\tif idx >= 1 {\n\t\tif sel, ok := stack[idx-1].(*ast.SelectorExpr); ok {\n\t\t\t// bind.SomeField or bind.SomeMethod()\n\n\t\t\t// Check if this is a method call\n\t\t\tif idx >= 2 {\n\t\t\t\tif call, ok := stack[idx-2].(*ast.CallExpr); ok {\n\t\t\t\t\treturn &MethodCall{\n\t\t\t\t\t\tFile:   file,\n\t\t\t\t\t\tBind:   bind,\n\t\t\t\t\t\tCall:   call,\n\t\t\t\t\t\tMethod: sel.Sel.Name,\n\t\t\t\t\t\tArgs:   call.Args,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Otherwise it's a field access\n\t\t\treturn &FieldAccess{\n\t\t\t\tFile:  file,\n\t\t\t\tBind:  bind,\n\t\t\t\tExpr:  sel,\n\t\t\t\tField: sel.Sel.Name,\n\t\t\t}\n\t\t}\n\n\t\t// Is this bind being referenced in a function call argument?\n\t\tif call, ok := stack[idx-1].(*ast.CallExpr); ok {\n\n\t\t\tif call.Fun == stack[idx] {\n\t\t\t\treturn &FuncCall{\n\t\t\t\t\tFile: file,\n\t\t\t\t\tBind: bind,\n\t\t\t\t\tCall: call,\n\t\t\t\t\tArgs: call.Args,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Find which argument this is.\n\t\t\tif argIdx := slices.Index(call.Args, stack[idx].(ast.Expr)); argIdx >= 0 {\n\t\t\t\tpkgFunc := option.CommaOk(file.Names().ResolvePkgLevelRef(call.Fun))\n\t\t\t\treturn &FuncArg{\n\t\t\t\t\tFile:     file,\n\t\t\t\t\tBind:     bind,\n\t\t\t\t\tCall:     call,\n\t\t\t\t\tTypeArgs: p.parseTypeArgs(file, call),\n\t\t\t\t\tPkgFunc:  pkgFunc,\n\t\t\t\t\tArgIdx:   argIdx,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// It's some other kind of usage. Find the largest enclosing expression.\n\tenclosing := stack[idx].(ast.Expr) // guaranteed to be an expr by the caller.\n\tfor i := idx; i >= 0; i-- {\n\t\tif expr, ok := stack[i].(ast.Expr); ok {\n\t\t\tenclosing = expr\n\t\t}\n\t}\n\n\treturn &Other{\n\t\tFile:    file,\n\t\tBind:    bind,\n\t\tExpr:    enclosing,\n\t\tBindRef: stack[idx].(ast.Expr), // guaranteed to be an expr by the caller.\n\t}\n}\n\nfunc (p *usageParser) parseTypeArgs(file *pkginfo.File, call *ast.CallExpr) []schema.Type {\n\tvar args []ast.Expr\n\tswitch fun := call.Fun.(type) {\n\tcase *ast.IndexExpr:\n\t\targs = []ast.Expr{fun.Index}\n\tcase *ast.IndexListExpr:\n\t\targs = fun.Indices\n\tdefault:\n\t\treturn nil\n\t}\n\n\treturn fns.Map(args, func(arg ast.Expr) schema.Type {\n\t\treturn p.schema.ParseType(file, arg)\n\t})\n}\n"
  },
  {
    "path": "v2/parser/resource/usage/usage_test.go",
    "content": "package usage_test\n\nimport (\n\t\"cmp\"\n\t\"flag\"\n\tgoparser \"go/parser\"\n\tgotoken \"go/token\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\tgocmp \"github.com/google/go-cmp/cmp\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\nvar goldenUpdate = flag.Bool(\"golden-update\", os.Getenv(\"GOLDEN_UPDATE\") != \"\", \"update golden files\")\n\nfunc TestParse(t *testing.T) {\n\tflag.Parse()\n\tc := qt.New(t)\n\ttests := readTestCases(c, \"testdata\")\n\tfor _, test := range tests {\n\t\tc.Run(test.name, func(c *qt.C) {\n\t\t\ttc := testutil.NewContext(c, false, test.input)\n\t\t\ttc.FailTestOnErrors()\n\t\t\tdefer tc.FailTestOnBailout()\n\n\t\t\t// Create a go.mod file in the main module directory if it doesn't already exist.\n\t\t\tmodPath := tc.MainModuleDir.Join(\"go.mod\").ToIO()\n\t\t\tif _, err := os.Stat(modPath); err != nil {\n\t\t\t\tif !os.IsNotExist(err) {\n\t\t\t\t\tc.Fatal(err)\n\t\t\t\t}\n\t\t\t\tmodContents := \"module example.com\\nrequire encore.dev v1.52.0\"\n\t\t\t\terr := os.WriteFile(modPath, []byte(modContents), 0644)\n\t\t\t\tc.Assert(err, qt.IsNil)\n\t\t\t}\n\n\t\t\ttc.GoModTidy()\n\t\t\ttc.GoModDownload()\n\n\t\t\tp := parser.NewParser(tc.Context)\n\t\t\tres := p.Parse()\n\n\t\t\tgot := fns.Map(res.AllUsageExprs(), func(u usage.Expr) usageDesc {\n\t\t\t\treturn usageToDesc(tc.Context, u)\n\t\t\t})\n\t\t\twant := test.wants\n\n\t\t\t// Sort the slices to be able to compare them.\n\t\t\tfor _, slice := range [][]usageDesc{got, want} {\n\t\t\t\tslices.SortFunc(slice, func(a, b usageDesc) int {\n\t\t\t\t\tif n := cmp.Compare(a.Filename, b.Filename); n != 0 {\n\t\t\t\t\t\treturn n\n\t\t\t\t\t} else if n := cmp.Compare(a.Line, b.Line); n != 0 {\n\t\t\t\t\t\treturn n\n\t\t\t\t\t} else if n := cmp.Compare(a.Resource, b.Resource); n != 0 {\n\t\t\t\t\t\treturn n\n\t\t\t\t\t}\n\t\t\t\t\treturn cmp.Compare(a.Operation, b.Operation)\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif *goldenUpdate {\n\t\t\t\t//updateGoldenFiles(c, test, got)\n\t\t\t}\n\t\t\tif diff := gocmp.Diff(got, want); diff != \"\" {\n\t\t\t\tc.Fatalf(\"generated code differs (-got +want):\\n%s\", diff)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype testCase struct {\n\tfilename string\n\tname     string\n\tinput    *txtar.Archive\n\twants    []usageDesc\n}\n\ntype usageDesc struct {\n\tFilename  string\n\tLine      int\n\tResource  string\n\tOperation string\n}\n\nfunc readTestCases(c *qt.C, dir string) []*testCase {\n\tfiles, err := filepath.Glob(filepath.Join(dir, \"*.txt\"))\n\tc.Assert(err, qt.IsNil)\n\n\tvar cases []*testCase\n\tfor _, file := range files {\n\t\tcases = append(cases, parseTestCase(c, file))\n\t}\n\treturn cases\n}\n\nfunc parseTestCase(c *qt.C, file string) *testCase {\n\tar, err := txtar.ParseFile(file)\n\tc.Assert(err, qt.IsNil)\n\n\ttc := &testCase{\n\t\tfilename: file,\n\t\tname:     strings.TrimSuffix(filepath.Base(file), \".txt\"),\n\t\tinput:    ar,\n\t}\n\n\tfor _, f := range ar.Files {\n\t\tif strings.HasSuffix(f.Name, \".go\") {\n\t\t\tfset := gotoken.NewFileSet()\n\t\t\tastFile, err := goparser.ParseFile(fset, f.Name, f.Data, goparser.ParseComments)\n\t\t\tc.Assert(err, qt.IsNil)\n\n\t\t\tfor _, cg := range astFile.Comments {\n\t\t\t\tfor i, comment := range cg.List {\n\t\t\t\t\tif remainder, ok := strings.CutPrefix(comment.Text, \"// use \"); ok {\n\t\t\t\t\t\tresource, op, _ := strings.Cut(remainder, \" \")\n\t\t\t\t\t\ttc.wants = append(tc.wants, usageDesc{\n\t\t\t\t\t\t\tFilename:  f.Name,\n\t\t\t\t\t\t\tLine:      fset.Position(cg.Pos()).Line + i,\n\t\t\t\t\t\t\tResource:  resource,\n\t\t\t\t\t\t\tOperation: op,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t}\n\n\treturn tc\n}\n\nfunc usageToDesc(pc *parsectx.Context, u usage.Expr) usageDesc {\n\tpos := pc.FS.Position(u.ASTExpr().Pos())\n\tfilename := pos.Filename\n\tif rel, err := filepath.Rel(pc.MainModuleDir.ToIO(), pos.Filename); err == nil {\n\t\tfilename = rel\n\t}\n\n\treturn usageDesc{\n\t\tFilename:  filename,\n\t\tLine:      pos.Line,\n\t\tResource:  u.ResourceBind().DescriptionForTest(),\n\t\tOperation: u.DescriptionForTest(),\n\t}\n}\n"
  },
  {
    "path": "v2/parser/resource/usage/usagetest/usagetest.go",
    "content": "package usagetest\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\tqt \"github.com/frankban/quicktest\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/rogpeppe/go-internal/txtar\"\n\n\t\"encr.dev/v2/internals/testutil\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\ntype Case struct {\n\tName     string\n\tImports  []string\n\tCode     string\n\tWant     []usage.Usage\n\tWantErrs []string\n}\n\nfunc Run(t *testing.T, standardImports []string, tests []Case, cmpOpts ...cmp.Option) {\n\t// testArchive renders the txtar archive to use for a given test.\n\ttestArchive := func(test Case) *txtar.Archive {\n\t\timportList := append([]string{\"context\"}, standardImports...)\n\t\timportList = append(importList, test.Imports...)\n\n\t\timports := \"\"\n\t\tif len(importList) > 0 {\n\t\t\timports = \"import (\\n\"\n\t\t\tfor _, imp := range importList {\n\t\t\t\timports += \"\\t\" + strconv.Quote(imp) + \"\\n\"\n\t\t\t}\n\t\t\timports += \")\\n\"\n\t\t}\n\n\t\treturn testutil.ParseTxtar(`\n-- go.mod --\nmodule example.com\nrequire encore.dev v1.52.0\n-- code.go --\npackage foo\n` + imports + `\n\n` + test.Code + `\n`)\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.Name, func(t *testing.T) {\n\t\t\tc := qt.New(t)\n\t\t\ta := testArchive(test)\n\t\t\ttc := testutil.NewContext(c, false, a)\n\t\t\ttc.GoModDownload()\n\n\t\t\tif len(test.WantErrs) > 0 {\n\t\t\t\tdefer tc.DeferExpectError(test.WantErrs...)\n\t\t\t} else {\n\t\t\t\ttc.FailTestOnErrors()\n\t\t\t\tdefer tc.FailTestOnBailout()\n\t\t\t}\n\n\t\t\tpp := parser.NewParser(tc.Context)\n\t\t\tresult := pp.Parse()\n\n\t\t\tif len(test.WantErrs) == 0 {\n\t\t\t\t// Check for equality, ignoring all the AST nodes and pkginfo types.\n\t\t\t\topts := append([]cmp.Option{\n\t\t\t\t\tcmpopts.IgnoreTypes(usage.Base{}),\n\t\t\t\t\tcmpopts.EquateEmpty(),\n\t\t\t\t}, cmpOpts...)\n\t\t\t\tusages := result.AllUsages()\n\t\t\t\tc.Assert(usages, qt.CmpEquals(opts...), test.Want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v2/parser/result.go",
    "content": "package parser\n\nimport (\n\t\"fmt\"\n\t\"go/ast\"\n\t\"go/token\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/internals/posmap\"\n\t\"encr.dev/v2/parser/infra/sqldb\"\n\t\"encr.dev/v2/parser/resource\"\n\t\"encr.dev/v2/parser/resource/usage\"\n)\n\n// computeResult computes the combined resource description.\nfunc computeResult(errs *perr.List, mainModule *pkginfo.Module, ur *usage.Resolver, appPackages []*pkginfo.Package, resources []resource.Resource, binds []resource.Bind, usageExprs []usage.Expr) *Result {\n\td := &Result{\n\t\tmainModule:    mainModule,\n\t\tappPackages:   appPackages,\n\t\tresources:     resources,\n\t\tallBinds:      binds,\n\t\tallUsageExprs: usageExprs,\n\n\t\tresMap:         make(map[resource.Resource]*resourceMeta),\n\t\tbyType:         make(map[reflect.Type][]resource.Resource),\n\t\tbindToResource: make(map[resource.Bind]resource.Resource, len(binds)),\n\t\tqnToBind:       make(map[pkginfo.QualifiedName]resource.Bind, len(binds)),\n\t\tusageByPkg:     make(map[paths.Pkg][]usage.Usage),\n\t}\n\n\td.initResources()\n\td.initBinds(errs, binds)\n\td.initUsages(errs, ur, usageExprs)\n\treturn d\n}\n\nfunc Resources[R resource.Resource](res *Result) []R {\n\tvar zero R\n\tresources := res.byType[reflect.TypeOf(zero)]\n\treturn fns.Map(resources, func(r resource.Resource) R {\n\t\treturn r.(R)\n\t})\n}\n\ntype Result struct {\n\tmainModule    *pkginfo.Module\n\tappPackages   []*pkginfo.Package\n\tresources     []resource.Resource\n\tallBinds      []resource.Bind\n\tallUsageExprs []usage.Expr\n\n\tresMap         map[resource.Resource]*resourceMeta\n\tbyType         map[reflect.Type][]resource.Resource\n\tbindToResource map[resource.Bind]resource.Resource\n\tqnToBind       map[pkginfo.QualifiedName]resource.Bind\n\tresByPos       posmap.Map[resource.Resource]\n\tusageByPos     posmap.Map[usage.Usage]\n\tusageByPkg     map[paths.Pkg][]usage.Usage\n}\n\nfunc (d *Result) MainModule() *pkginfo.Module {\n\treturn d.mainModule\n}\n\nfunc (d *Result) AppPackages() []*pkginfo.Package {\n\treturn d.appPackages\n}\n\nfunc (d *Result) PackageAt(path paths.Pkg) option.Option[*pkginfo.Package] {\n\tfor _, pkg := range d.appPackages {\n\t\tif pkg.ImportPath == path {\n\t\t\treturn option.Some(pkg)\n\t\t}\n\t}\n\n\treturn option.None[*pkginfo.Package]()\n}\n\nfunc (d *Result) Resources() []resource.Resource {\n\treturn d.resources\n}\n\nfunc (d *Result) AllBinds() []resource.Bind {\n\treturn d.allBinds\n}\n\nfunc (d *Result) AllUsageExprs() []usage.Expr {\n\treturn d.allUsageExprs\n}\n\nfunc (d *Result) ResourceForBind(b resource.Bind) resource.Resource {\n\treturn d.bindToResource[b]\n}\n\nfunc (d *Result) ResourceForQN(qn pkginfo.QualifiedName) option.Option[resource.Resource] {\n\treturn option.Map(d.BindForQN(qn), d.ResourceForBind)\n}\n\nfunc (d *Result) BindForQN(qn pkginfo.QualifiedName) option.Option[resource.Bind] {\n\treturn option.AsOptional(d.qnToBind[qn])\n}\n\nfunc (d *Result) Binds(res resource.Resource) []resource.Bind {\n\treturn d.rd(res).binds\n}\n\nfunc (d *Result) PkgDeclBinds(res resource.Resource) []*resource.PkgDeclBind {\n\treturn d.rd(res).pkgDecls\n}\n\nfunc (d *Result) Usages(res resource.Resource) []usage.Usage {\n\treturn d.rd(res).usages\n}\n\nfunc (d *Result) AllUsages() []usage.Usage {\n\tvar all []usage.Usage\n\tfor _, res := range d.resources {\n\t\tall = append(all, d.Usages(res)...)\n\t}\n\treturn all\n}\n\nfunc (d *Result) ResourceConstructorContaining(node ast.Node) option.Option[resource.Resource] {\n\treturn d.resByPos.Containing(node)\n}\n\nfunc (d *Result) UsageFromNode(node ast.Node) option.Option[usage.Usage] {\n\treturn d.usageByPos.Containing(node)\n}\n\nfunc (d *Result) UsagesInPkg(pkgPath paths.Pkg) []usage.Usage {\n\treturn d.usageByPkg[pkgPath]\n}\n\nfunc (d *Result) rd(res resource.Resource) *resourceMeta {\n\tm := d.resMap[res]\n\tif m == nil {\n\t\tm = &resourceMeta{}\n\t\td.resMap[res] = m\n\t}\n\treturn m\n}\n\n// resourceMeta describes metadata about a resource.\ntype resourceMeta struct {\n\tbinds    []resource.Bind\n\tpkgDecls []*resource.PkgDeclBind\n\tusages   []usage.Usage\n}\n\nfunc (m *resourceMeta) addBind(b resource.Bind) {\n\tm.binds = append(m.binds, b)\n\tif pkgDecl, ok := b.(*resource.PkgDeclBind); ok {\n\t\tm.pkgDecls = append(m.pkgDecls, pkgDecl)\n\t}\n}\n\nfunc (m *resourceMeta) addUsage(u usage.Usage) {\n\tm.usages = append(m.usages, u)\n}\n\nfunc (d *Result) initResources() {\n\td.resByPos = posmap.Build(d.resources...)\n\n\tfor _, res := range d.resources {\n\t\ttyp := reflect.TypeOf(res)\n\t\td.byType[typ] = append(d.byType[typ], res)\n\t}\n}\n\nfunc (d *Result) initBinds(errs *perr.List, binds []resource.Bind) {\n\tbyPath := make(map[string]resource.Resource, len(d.resources))\n\n\tfor _, r := range d.resources {\n\t\t// If we have a named resource, add it to the path map.\n\t\tif named, ok := r.(resource.Named); ok {\n\t\t\tp := resource.Path{{named.Kind(), named.ResourceName()}}\n\t\t\tbyPath[pathKey(p)] = r\n\t\t}\n\t}\n\n\taddBind := func(r resource.Resource, b resource.Bind) {\n\t\td.rd(r).addBind(b)\n\t\td.bindToResource[b] = r\n\t\tif pkgBind, ok := b.(*resource.PkgDeclBind); ok && pkgBind != nil {\n\t\t\td.qnToBind[pkgBind.QualifiedName()] = b\n\t\t}\n\t}\n\n\tfor _, b := range binds {\n\t\t// Do we have a specific resource reference?\n\t\tref := b.ResourceRef()\n\t\tif r := ref.Resource; r != nil {\n\t\t\taddBind(r, b)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Otherwise figure out the resource from the bind path.\n\t\tkey := pathKey(ref.Path)\n\t\tif r, ok := byPath[key]; ok {\n\t\t\taddBind(r, b)\n\t\t} else {\n\t\t\tswitch {\n\t\t\tcase len(ref.Path) > 0 && ref.Path[0].Kind == resource.SQLDatabase:\n\t\t\t\tdbName := ref.Path[0].Name\n\t\t\t\terrs.Add(sqldb.ErrDatabaseNotFound(dbName).AtGoPos(b.Pos(), token.NoPos))\n\t\t\tdefault:\n\n\t\t\t\t// NOTE(andre): We could end up here in the future when we support\n\t\t\t\t// named references to PubSub subscriptions, since those would\n\t\t\t\t// involve a two-segment resource path (first the topic and then the subscription),\n\t\t\t\t// which we don't support today (the construction of byPath above only handles\n\t\t\t\t// the case of single-segment resource paths).\n\t\t\t\t// Since we don't support that today, this is fine for now.\n\t\t\t\terrs.Addf(b.Pos(), \"internal compiler error: unknown resource (path %q)\", key)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (d *Result) initUsages(errs *perr.List, ur *usage.Resolver, usageExprs []usage.Expr) {\n\tresourcesByBindName := make(map[pkginfo.QualifiedName]resource.Resource, len(d.resources))\n\tfor r, m := range d.resMap {\n\t\tfor _, pkgDecl := range m.pkgDecls {\n\t\t\tresourcesByBindName[pkgDecl.QualifiedName()] = r\n\t\t}\n\t}\n\n\tvar allUsages []usage.Usage\n\n\tprocessUsage := func(r resource.Resource, expr usage.Expr) {\n\t\tif u, ok := ur.Resolve(errs, expr, r).Get(); ok {\n\t\t\td.rd(r).addUsage(u)\n\t\t\tallUsages = append(allUsages, u)\n\n\t\t\tpkgPath := u.DeclaredIn().Pkg.ImportPath\n\t\t\td.usageByPkg[pkgPath] = append(d.usageByPkg[pkgPath], u)\n\t\t}\n\t}\n\n\tfor _, u := range usageExprs {\n\t\tbind := u.ResourceBind()\n\t\tref := bind.ResourceRef()\n\t\tif r := ref.Resource; r != nil {\n\t\t\tprocessUsage(r, u)\n\t\t} else if pkgDecl, ok := bind.(*resource.PkgDeclBind); ok {\n\t\t\tif r, ok := resourcesByBindName[pkgDecl.QualifiedName()]; ok {\n\t\t\t\tprocessUsage(r, u)\n\t\t\t} else {\n\t\t\t\terrs.Addf(u.ASTExpr().Pos(), \"internal compiler error: resource reference not found: %s\",\n\t\t\t\t\tpkgDecl.QualifiedName().NaiveDisplayName())\n\t\t\t}\n\t\t} else {\n\t\t\terrs.Addf(u.ASTExpr().Pos(), \"internal compiler error: invalid resource bind: %T\", bind)\n\t\t}\n\t}\n\n\td.usageByPos = posmap.Build[usage.Usage](allUsages...)\n}\n\nfunc pathKey(path resource.Path) string {\n\tvar b strings.Builder\n\tfor i, e := range path {\n\t\tif i > 0 {\n\t\t\tb.WriteString(\"/\")\n\t\t}\n\t\tfmt.Fprintf(&b, \"%s:%s\", e.Kind.String(), e.Name)\n\t}\n\treturn b.String()\n}\n"
  },
  {
    "path": "v2/tsbuilder/tsbuilder.go",
    "content": "package tsbuilder\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"encore.dev/appruntime/exported/experiments\"\n\t\"github.com/cockroachdb/errors\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/internal/lookpath\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\tmetav1 \"encr.dev/proto/encore/parser/meta/v1\"\n)\n\nfunc New() *BuilderImpl {\n\treturn &BuilderImpl{\n\t\tcmds: make(map[*runningCmd]bool),\n\t}\n}\n\ntype BuilderImpl struct {\n\tmu   sync.Mutex\n\tcmds map[*runningCmd]bool\n}\n\ntype parseInput struct {\n\tAppRoot    string `json:\"app_root\"`\n\tPlatformID string `json:\"platform_id,omitempty\"`\n\tLocalID    string `json:\"local_id\"`\n\tParseTests bool   `json:\"parse_tests\"`\n}\n\ntype data struct {\n\tcmd    *exec.Cmd\n\tstdin  io.Writer\n\tstdout io.Reader\n}\n\nfunc getTSParserPath() (string, error) {\n\tconst tsParserBinaryName = \"tsparser-encore\"\n\n\tif path := os.Getenv(\"ENCORE_TSPARSER_PATH\"); path != \"\" {\n\t\treturn path, nil\n\t}\n\n\t// Check the encore bin directory.\n\tif bin, ok := env.EncoreBin().Get(); ok {\n\t\tcandidate := filepath.Join(bin, tsParserBinaryName)\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate, nil\n\t\t}\n\t}\n\n\t// Now default to the path\n\treturn tsParserBinaryName, nil\n}\n\nfunc (i *BuilderImpl) Close() error {\n\ti.mu.Lock()\n\tcmds := maps.Clone(i.cmds)\n\ti.mu.Unlock()\n\n\tfor c := range cmds {\n\t\t_ = c.cmd.Cancel()\n\t}\n\n\tfor c := range cmds {\n\t\t<-c.done\n\t}\n\treturn nil\n}\n\nfunc (i *BuilderImpl) Prepare(ctx context.Context, p builder.PrepareParams) (*builder.PrepareResult, error) {\n\texe, err := getTSParserPath()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\truntimesDir := p.Build.EncoreRuntimes.GetOrElseF(func() paths.FS { return paths.FS(env.EncoreRuntimesPath()) })\n\tjsRuntimePath := jsRuntimeRoot(runtimesDir)\n\n\tcmd := exec.CommandContext(ctx, exe)\n\tcmd.Dir = filepath.Join(p.App.Root(), p.WorkingDir)\n\n\tcmd.Env = append(os.Environ(),\n\t\t\"RUST_LOG=error\",\n\t\t\"RUST_BACKTRACE=1\",\n\t)\n\tcmd.Env = append(cmd.Env, p.Build.Environ...)\n\tcmd.Env = append(cmd.Env,\n\t\t\"ENCORE_JS_RUNTIME_PATH=\"+jsRuntimePath.ToIO(),\n\t\t\"ENCORE_APP_REVISION=\"+p.Build.Revision,\n\t)\n\n\t// If we have an encore-bin directory, add it to the path.\n\tif bin, ok := env.EncoreBin().Get(); ok {\n\t\tcmd.Env = append(cmd.Env, \"PATH=\"+os.Getenv(\"PATH\")+string(filepath.ListSeparator)+bin)\n\t}\n\n\t// Close the process when the ctx is canceled.\n\tcmd.WaitDelay = 1 * time.Second\n\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to get stdin: %s\", err)\n\t}\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to get stdout: %s\", err)\n\t}\n\n\tif stderr, ok := p.Stderr.Get(); ok {\n\t\tcmd.Stderr = stderr\n\t} else {\n\t\tcmd.Stderr = os.Stderr\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to start builder: %s\", err)\n\t}\n\n\trc := newRunningCmd(cmd)\n\ti.cmds[rc] = true\n\n\tgo func() {\n\t\t<-rc.done\n\t\ti.mu.Lock()\n\t\tdelete(i.cmds, rc)\n\t\ti.mu.Unlock()\n\t}()\n\n\t// Send prepare command\n\tinput := prepareInput{\n\t\tAppRoot:        paths.FS(p.App.Root()),\n\t\tJSRuntimeRoot:  jsRuntimePath,\n\t\tRuntimeVersion: version.Version,\n\t}\n\tif p.Build.UseLocalJSRuntime {\n\t\tinput.LocalRuntimeOverride = jsRuntimePath.ToIO()\n\t}\n\tinputData, _ := json.Marshal(input)\n\t_, _ = stdin.Write([]byte(\"prepare\\n\"))\n\tif _, err := stdin.Write(inputData); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to write to stdin: %s\", err)\n\t}\n\n\tisSuccess, prepareResp, err := readResp(stdout)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read response: %s\", err)\n\t} else if !isSuccess {\n\t\treturn nil, errors.New(string(prepareResp))\n\t}\n\n\t// Return process state for Parse to use\n\treturn &builder.PrepareResult{\n\t\tData: &data{\n\t\t\tcmd:    cmd,\n\t\t\tstdin:  stdin,\n\t\t\tstdout: stdout,\n\t\t},\n\t}, nil\n}\n\nfunc (i *BuilderImpl) Parse(ctx context.Context, p builder.ParseParams) (*builder.ParseResult, error) {\n\t// Get prepared data from Prepare()\n\tdata := p.Prepare.Data.(*data)\n\n\t// Send parse command\n\tinput, _ := json.Marshal(parseInput{\n\t\tAppRoot:    p.App.Root(),\n\t\tPlatformID: p.App.PlatformID(),\n\t\tLocalID:    p.App.LocalID(),\n\t\tParseTests: p.ParseTests,\n\t})\n\t_, _ = data.stdin.Write([]byte(\"parse\\n\"))\n\tif _, err := data.stdin.Write(input); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to write to stdin: %s\", err)\n\t}\n\n\tisSuccess, parseResp, err := readResp(data.stdout)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read response: %s\", err)\n\t} else if !isSuccess {\n\t\treturn nil, errors.New(string(parseResp))\n\t}\n\n\tvar md metav1.Data\n\tif err := proto.Unmarshal(parseResp, &md); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to parse app: %s\", err)\n\t}\n\n\treturn &builder.ParseResult{Meta: &md, Data: data}, nil\n}\n\ntype compileInput struct {\n\tUseLocalRuntime bool              `json:\"use_local_runtime\"`\n\tDebug           builder.DebugMode `json:\"debug\"`\n\tNodeJSRuntime   NodeJSRuntime     `json:\"nodejs_runtime\"`\n}\n\nfunc (i *BuilderImpl) Compile(ctx context.Context, p builder.CompileParams) (*builder.CompileResult, error) {\n\tdata := p.Parse.Data.(*data)\n\n\tnodejsRuntime := NodeJS\n\tif experiments.BunRuntime.Enabled(p.Experiments) {\n\t\tnodejsRuntime = Bun\n\t}\n\n\tinput, _ := json.Marshal(compileInput{\n\t\tUseLocalRuntime: p.Build.UseLocalJSRuntime,\n\t\tDebug:           p.Build.DebugMode,\n\t\tNodeJSRuntime:   nodejsRuntime,\n\t})\n\n\t_, _ = data.stdin.Write([]byte(\"compile\\n\"))\n\tif _, err := data.stdin.Write(input); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to write to stdin: %s\", err)\n\t}\n\n\tisSuccess, compileResp, err := readResp(data.stdout)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read response: %s\", err)\n\t} else if !isSuccess {\n\t\treturn nil, errors.New(string(compileResp))\n\t}\n\n\tvar res struct {\n\t\tOutputs []*builder.JSBuildOutput `json:\"outputs\"`\n\t}\n\n\tif err := json.Unmarshal(compileResp, &res); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to decode response: %s\", err)\n\t}\n\n\treturn &builder.CompileResult{\n\t\tOS:   p.Build.GOOS,\n\t\tArch: p.Build.GOARCH,\n\t\tOutputs: fns.Map(res.Outputs, func(o *builder.JSBuildOutput) builder.BuildOutput {\n\t\t\treturn o\n\t\t}),\n\t}, nil\n}\n\nfunc (i *BuilderImpl) UseNewRuntimeConfig() bool {\n\treturn true\n}\n\nfunc (i *BuilderImpl) NeedsMeta() bool {\n\treturn true\n}\n\nfunc (i *BuilderImpl) ServiceConfigs(ctx context.Context, p builder.ServiceConfigsParams) (*builder.ServiceConfigsResult, error) {\n\treturn &builder.ServiceConfigsResult{\n\t\t// These are not currently supported\n\t\tConfigs:     nil,\n\t\tConfigFiles: nil,\n\t}, nil\n}\n\ntype testInput struct {\n\tRuntimeVersion  string `json:\"runtime_version\"`\n\tUseLocalRuntime bool   `json:\"use_local_runtime\"`\n}\n\nfunc (i *BuilderImpl) RunTests(ctx context.Context, p builder.RunTestsParams) error {\n\tcwd := p.WorkingDir.ToIO()\n\tbinary, err := lookpath.InDir(cwd, p.Spec.Environ, p.Spec.Command)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd := exec.CommandContext(ctx, binary, p.Spec.Args...)\n\tcmd.Env = p.Spec.Environ\n\tcmd.Dir = cwd\n\tcmd.Stdout = p.Stdout\n\tcmd.Stderr = p.Stderr\n\treturn cmd.Run()\n}\n\nfunc (i *BuilderImpl) TestSpec(ctx context.Context, p builder.TestSpecParams) (*builder.TestSpecResult, error) {\n\tdata := p.Compile.Parse.Data.(*data)\n\n\tinput, _ := json.Marshal(testInput{\n\t\tRuntimeVersion:  version.Version,\n\t\tUseLocalRuntime: p.Compile.Build.UseLocalJSRuntime,\n\t})\n\n\t_, _ = data.stdin.Write([]byte(\"test\\n\"))\n\tif _, err := data.stdin.Write(input); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to write to stdin: %s\", err)\n\t}\n\n\tisSuccess, testResp, err := readResp(data.stdout)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read response: %s\", err)\n\t} else if !isSuccess {\n\t\treturn nil, errors.New(string(testResp))\n\t}\n\n\tvar res struct {\n\t\tCmd option.Option[*builder.CmdSpec] `json:\"cmd\"`\n\t}\n\tif err := json.Unmarshal(testResp, &res); err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to decode response: %s\", err)\n\t}\n\n\tcmdSpec, ok := res.Cmd.Get()\n\tif !ok {\n\t\treturn nil, builder.ErrNoTests\n\t}\n\n\tcommand := cmdSpec.Command.Expand(\"\")\n\targs := append(command[1:], p.Args...)\n\n\t// Default to the error log level to avoid spamming the logs.\n\tenvs := append(os.Environ(), \"ENCORE_LOG_LEVEL=error\")\n\n\tenvs = append(envs, p.Env...)\n\tenvs = append(envs, cmdSpec.Env.Expand(\"\")...)\n\n\treturn &builder.TestSpecResult{\n\t\tCommand:     command[0],\n\t\tArgs:        args,\n\t\tEnviron:     envs,\n\t\tBuilderData: nil,\n\t}, nil\n}\n\nfunc (i *BuilderImpl) GenUserFacing(ctx context.Context, p builder.GenUserFacingParams) error {\n\tdata := p.Parse.Data.(*data)\n\n\tinput, _ := json.Marshal(genUserFacingInput{})\n\n\t_, _ = data.stdin.Write([]byte(\"gen-user-facing\\n\"))\n\tif _, err := data.stdin.Write(input); err != nil {\n\t\treturn fmt.Errorf(\"unable to write to stdin: %s\", err)\n\t}\n\n\tisSuccess, genUserFacingRep, err := readResp(data.stdout)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to read response: %s\", err)\n\t} else if !isSuccess {\n\t\treturn errors.New(string(genUserFacingRep))\n\t}\n\treturn nil\n}\n\ntype genUserFacingInput struct {\n}\n\ntype NodeJSRuntime string\n\nconst (\n\tNodeJS NodeJSRuntime = \"nodejs\"\n\tBun    NodeJSRuntime = \"bun\"\n)\n\ntype prepareInput struct {\n\tJSRuntimeRoot        paths.FS `json:\"js_runtime_root\"`\n\tAppRoot              paths.FS `json:\"app_root\"`\n\tRuntimeVersion       string   `json:\"runtime_version\"`\n\tLocalRuntimeOverride string   `json:\"local_runtime_override,omitempty\"`\n}\n\nfunc readResp(reader io.Reader) (isSuccess bool, data []byte, err error) {\n\tvar respLen uint32\n\tif err := binary.Read(reader, binary.LittleEndian, &respLen); err != nil {\n\t\treturn false, nil, fmt.Errorf(\"unable to read response length: %s\", err)\n\t}\n\tresp := make([]byte, respLen)\n\tif _, err := io.ReadFull(reader, resp); err != nil {\n\t\treturn false, nil, fmt.Errorf(\"unable to read response: %s\", err)\n\t}\n\n\tisSuccess, data = resp[0] == 0, resp[1:]\n\treturn isSuccess, data, nil\n}\n\n// Reports the JS Runtime root directory.\nfunc jsRuntimeRoot(runtimesPath paths.FS) paths.FS {\n\treturn runtimesPath.Join(\"js\")\n}\n\ntype runningCmd struct {\n\tcmd  *exec.Cmd\n\tdone chan struct{}\n\terr  error\n}\n\nfunc newRunningCmd(c *exec.Cmd) *runningCmd {\n\trc := &runningCmd{\n\t\tcmd:  c,\n\t\tdone: make(chan struct{}),\n\t\terr:  nil,\n\t}\n\tgo func() {\n\t\tdefer close(rc.done)\n\t\trc.err = c.Wait()\n\t}()\n\treturn rc\n}\n"
  },
  {
    "path": "v2/v2builder/v2builder.go",
    "content": "package v2builder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"go/token\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cockroachdb/errors\"\n\t\"github.com/rs/zerolog\"\n\n\t\"encore.dev/appruntime/exported/config\"\n\t\"encr.dev/internal/env\"\n\t\"encr.dev/internal/etrace\"\n\t\"encr.dev/internal/version\"\n\t\"encr.dev/pkg/builder\"\n\t\"encr.dev/pkg/cueutil\"\n\t\"encr.dev/pkg/fns\"\n\t\"encr.dev/pkg/option\"\n\t\"encr.dev/pkg/paths\"\n\t\"encr.dev/pkg/vfs\"\n\t\"encr.dev/v2/app\"\n\t\"encr.dev/v2/app/legacymeta\"\n\t\"encr.dev/v2/codegen\"\n\t\"encr.dev/v2/codegen/apigen\"\n\t\"encr.dev/v2/codegen/apigen/userfacinggen\"\n\t\"encr.dev/v2/codegen/cuegen\"\n\t\"encr.dev/v2/codegen/infragen\"\n\t\"encr.dev/v2/compiler/build\"\n\t\"encr.dev/v2/internals/parsectx\"\n\t\"encr.dev/v2/internals/perr\"\n\t\"encr.dev/v2/internals/pkginfo\"\n\t\"encr.dev/v2/parser\"\n\t\"encr.dev/v2/parser/resource\"\n)\n\nfunc New() *BuilderImpl {\n\treturn &BuilderImpl{}\n}\n\ntype BuilderImpl struct{}\n\nfunc (i *BuilderImpl) Close() error {\n\treturn nil\n}\n\nfunc (*BuilderImpl) Prepare(ctx context.Context, p builder.PrepareParams) (*builder.PrepareResult, error) {\n\treturn &builder.PrepareResult{}, nil\n}\n\nfunc (*BuilderImpl) Parse(ctx context.Context, p builder.ParseParams) (*builder.ParseResult, error) {\n\treturn etrace.Sync2(ctx, \"\", \"v2builder.Parse\", func(ctx context.Context) (res *builder.ParseResult, err error) {\n\t\tdefer func() {\n\t\t\terr, _ = perr.CatchBailoutAndPanic(err, recover())\n\t\t}()\n\t\tfset := token.NewFileSet()\n\t\terrs := perr.NewList(ctx, fset)\n\n\t\truntimesDir := p.Build.EncoreRuntimes.GetOrElseF(func() paths.FS { return paths.FS(env.EncoreRuntimesPath()) })\n\t\tpc := &parsectx.Context{\n\t\t\tAppID: option.Some(p.App.PlatformOrLocalID()),\n\t\t\tCtx:   ctx,\n\t\t\tLog:   p.Build.Logger.GetOrElse(zerolog.New(zerolog.NewConsoleWriter())).Level(zerolog.InfoLevel),\n\t\t\tBuild: parsectx.BuildInfo{\n\t\t\t\tExperiments: p.Experiments,\n\t\t\t\t// We use GetOrElseF here because GoRoot / Runtime path will panic\n\t\t\t\t// if they are not set, but we don't want to panic if the option\n\t\t\t\t// is set.\n\t\t\t\tGOROOT: p.Build.GoRoot.GetOrElseF(func() paths.FS {\n\t\t\t\t\treturn paths.RootedFSPath(env.EncoreGoRoot(), \".\")\n\t\t\t\t}),\n\t\t\t\tEncoreRuntime: runtimesDir.Join(\"go\"),\n\n\t\t\t\tGOARCH:                    p.Build.GOARCH,\n\t\t\t\tGOOS:                      p.Build.GOOS,\n\t\t\t\tCgoEnabled:                p.Build.CgoEnabled,\n\t\t\t\tStaticLink:                p.Build.StaticLink,\n\t\t\t\tDebug:                     p.Build.DebugMode,\n\t\t\t\tBuildTags:                 p.Build.BuildTags,\n\t\t\t\tRevision:                  p.Build.Revision,\n\t\t\t\tUncommittedChanges:        p.Build.UncommittedChanges,\n\t\t\t\tMainPkg:                   p.Build.MainPkg,\n\t\t\t\tDisableSensitiveScrubbing: p.Build.DisableSensitiveScrubbing,\n\t\t\t},\n\t\t\tMainModuleDir: paths.RootedFSPath(p.App.Root(), \".\"),\n\t\t\tFS:            fset,\n\t\t\tParseTests:    p.ParseTests,\n\t\t\tErrs:          errs,\n\t\t}\n\n\t\tparser := parser.NewParser(pc)\n\t\tparserResult := parser.Parse()\n\t\tappDesc := app.ValidateAndDescribe(pc, parserResult)\n\t\tmeta, traceNodes := legacymeta.Compute(pc.Errs, appDesc)\n\t\tmainModule := parser.MainModule()\n\t\truntimeModule := parser.RuntimeModule()\n\n\t\tif pc.Errs.Len() > 0 {\n\t\t\treturn nil, pc.Errs.AsError()\n\t\t}\n\n\t\treturn &builder.ParseResult{\n\t\t\tMeta: meta,\n\t\t\tData: &parseData{\n\t\t\t\tpc:            pc,\n\t\t\t\tappDesc:       appDesc,\n\t\t\t\tmainModule:    mainModule,\n\t\t\t\truntimeModule: runtimeModule,\n\t\t\t\ttraceNodes:    traceNodes,\n\t\t\t},\n\t\t}, nil\n\t})\n}\n\ntype parseData struct {\n\tpc            *parsectx.Context\n\tappDesc       *app.Desc\n\tmainModule    *pkginfo.Module\n\truntimeModule *pkginfo.Module\n\ttraceNodes    *legacymeta.TraceNodes\n}\n\nfunc (*BuilderImpl) Compile(ctx context.Context, p builder.CompileParams) (*builder.CompileResult, error) {\n\treturn etrace.Sync2(ctx, \"\", \"v2builder.Compile\", func(ctx context.Context) (res *builder.CompileResult, err error) {\n\t\tdefer func() {\n\t\t\terr, _ = perr.CatchBailoutAndPanic(err, recover())\n\t\t}()\n\n\t\tpd := p.Parse.Data.(*parseData)\n\n\t\tcodegenOp := p.OpTracker.Add(\"Generating boilerplate code\", time.Now())\n\n\t\tgg := codegen.New(pd.pc, pd.traceNodes)\n\t\tinfragen.Process(gg, pd.appDesc)\n\t\tstaticConfig := apigen.Process(apigen.Params{\n\t\t\tGen:               gg,\n\t\t\tDesc:              pd.appDesc,\n\t\t\tMainModule:        pd.mainModule,\n\t\t\tRuntimeModule:     pd.runtimeModule,\n\t\t\tCompilerVersion:   p.EncoreVersion.GetOrElse(fmt.Sprintf(\"EncoreCLI/%s\", version.Version)),\n\t\t\tAppRevision:       p.Build.Revision,\n\t\t\tAppUncommitted:    p.Build.UncommittedChanges,\n\t\t\tExecScriptMainPkg: p.Build.MainPkg,\n\t\t})\n\n\t\tif pd.pc.Errs.Len() > 0 {\n\t\t\tp.OpTracker.Fail(codegenOp, pd.pc.Errs.AsError())\n\t\t\treturn res, pd.pc.Errs.AsError()\n\t\t}\n\t\tp.OpTracker.Done(codegenOp, 450*time.Millisecond)\n\n\t\tcompileOp := p.OpTracker.Add(\"Compiling application source code\", time.Now())\n\t\tbuildResult := build.Build(ctx, &build.Config{\n\t\t\tCtx:          pd.pc,\n\t\t\tOverlays:     gg.Overlays(),\n\t\t\tMainPkg:      paths.Pkg(p.Build.MainPkg.GetOrElse(\"./encore_internal/main\")),\n\t\t\tKeepOutput:   p.Build.KeepOutput,\n\t\t\tStaticConfig: staticConfig,\n\t\t\tEnv:          p.Environ,\n\t\t})\n\n\t\toutput := &builder.GoBuildOutput{ArtifactDir: buildResult.Dir}\n\t\tres = &builder.CompileResult{\n\t\t\tOS:      p.Build.GOOS,\n\t\t\tArch:    p.Build.GOARCH,\n\t\t\tOutputs: []builder.BuildOutput{output},\n\t\t}\n\n\t\t// Set the built binaries according to the multi-proc build setting.\n\t\trelExe, err := filepath.Rel(output.ArtifactDir.ToIO(), buildResult.Exe.ToIO())\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"unable to compute relative path to executable\")\n\t\t}\n\n\t\texeFile := builder.ArtifactString(\"${ARTIFACT_DIR}\").Join(relExe)\n\n\t\tcmd := builder.ArtifactStrings{exeFile}\n\n\t\tif p.Build.DebugMode == builder.DebugModeBreak {\n\t\t\tdlvPath, err := exec.LookPath(\"dlv\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(\"unable to find the dlv debugger. Please install and make sure it's in $PATH.\")\n\t\t\t}\n\t\t\tcmd = append(builder.ArtifactStrings{builder.ArtifactString(dlvPath), \"--listen=127.0.0.1:2345\", \"--headless=true\", \"--api-version=2\", \"--accept-multiclient\", \"--wd\", builder.ArtifactString(p.App.Root()), \"exec\"}, cmd...)\n\t\t}\n\n\t\tspec := builder.CmdSpec{\n\t\t\tCommand:          cmd,\n\t\t\tPrioritizedFiles: builder.ArtifactStrings{exeFile},\n\t\t}\n\t\toutput.Entrypoints = []builder.Entrypoint{{\n\t\t\tCmd:      spec,\n\t\t\tServices: fns.Map(pd.appDesc.Services, func(svc *app.Service) string { return svc.Name }),\n\t\t\tGateways: fns.Map(pd.appDesc.Gateways, func(gw *app.Gateway) string { return gw.EncoreName }),\n\t\t}}\n\n\t\t// Check if the compile result caused errors and if it did return\n\t\tif pd.pc.Errs.Len() > 0 {\n\t\t\tp.OpTracker.Fail(compileOp, pd.pc.Errs.AsError())\n\t\t\treturn res, pd.pc.Errs.AsError()\n\t\t}\n\t\tp.OpTracker.Done(compileOp, 450*time.Millisecond)\n\n\t\t// Then check if the config generation caused an error\n\t\tif pd.pc.Errs.Len() > 0 {\n\t\t\treturn res, pd.pc.Errs.AsError()\n\t\t}\n\n\t\treturn res, nil\n\t})\n}\n\nfunc (*BuilderImpl) ServiceConfigs(ctx context.Context, p builder.ServiceConfigsParams) (res *builder.ServiceConfigsResult, err error) {\n\tdefer func() {\n\t\tif l, ok := perr.CatchBailout(recover()); ok && l.Len() > 0 {\n\t\t\terr = l.AsError()\n\t\t}\n\t}()\n\n\tpd := p.Parse.Data.(*parseData)\n\tcfg := computeConfigs(pd.pc.Errs, pd.appDesc, pd.mainModule, p.CueMeta)\n\tif err := pd.pc.Errs.AsError(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &builder.ServiceConfigsResult{\n\t\tConfigs:     cfg.configs,\n\t\tConfigFiles: cfg.files,\n\t}, nil\n}\n\nfunc (*BuilderImpl) UseNewRuntimeConfig() bool {\n\treturn false\n}\n\nfunc (*BuilderImpl) NeedsMeta() bool {\n\treturn false\n}\n\nfunc (i *BuilderImpl) RunTests(ctx context.Context, p builder.RunTestsParams) error {\n\treturn etrace.Sync1(ctx, \"\", \"v2builder.Test\", func(ctx context.Context) (err error) {\n\t\tdefer func() {\n\t\t\terr, _ = perr.CatchBailoutAndPanic(err, recover())\n\t\t}()\n\n\t\tdata, ok := p.Spec.BuilderData.(*testBuilderData)\n\t\tif !ok {\n\t\t\treturn errors.Newf(\"invalid builder data type %T\", p.Spec.BuilderData)\n\t\t}\n\n\t\tbuild.RunTests(ctx, data.spec, &build.RunTestsConfig{\n\t\t\tStdout:     p.Stdout,\n\t\t\tStderr:     p.Stderr,\n\t\t\tWorkingDir: p.WorkingDir,\n\t\t})\n\n\t\tif data.pc.Errs.Len() > 0 {\n\t\t\treturn data.pc.Errs.AsError()\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\ntype testBuilderData struct {\n\tspec *build.TestSpec\n\tpc   *parsectx.Context\n}\n\nfunc (i *BuilderImpl) TestSpec(ctx context.Context, p builder.TestSpecParams) (*builder.TestSpecResult, error) {\n\treturn etrace.Sync2(ctx, \"\", \"v2builder.TestSpec\", func(ctx context.Context) (res *builder.TestSpecResult, err error) {\n\t\tspec, err := i.generateTestSpec(ctx, p)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tpd := p.Compile.Parse.Data.(*parseData)\n\t\tdata := &testBuilderData{\n\t\t\tspec: spec,\n\t\t\tpc:   pd.pc,\n\t\t}\n\n\t\treturn &builder.TestSpecResult{\n\t\t\tCommand:     spec.Command,\n\t\t\tArgs:        spec.Args,\n\t\t\tEnviron:     spec.Environ,\n\t\t\tBuilderData: data,\n\t\t}, nil\n\t})\n}\n\nfunc (i *BuilderImpl) generateTestSpec(ctx context.Context, p builder.TestSpecParams) (*build.TestSpec, error) {\n\treturn etrace.Sync2(ctx, \"\", \"v2builder.generateTestSpec\", func(ctx context.Context) (res *build.TestSpec, err error) {\n\t\tdefer func() {\n\t\t\terr, _ = perr.CatchBailoutAndPanic(err, recover())\n\t\t}()\n\n\t\tpd := p.Compile.Parse.Data.(*parseData)\n\n\t\tgg := codegen.New(pd.pc, pd.traceNodes)\n\t\tstaticConfig := etrace.Sync1(ctx, \"\", \"codegen\", func(ctx context.Context) *config.Static {\n\t\t\ttestCfg := codegen.TestConfig{}\n\t\t\tfor _, pkg := range pd.appDesc.Parse.AppPackages() {\n\t\t\t\tisTestFile := func(f *pkginfo.File) bool { return f.TestFile }\n\t\t\t\thasTestFiles := slices.IndexFunc(pkg.Files, isTestFile) != -1\n\t\t\t\tif hasTestFiles {\n\t\t\t\t\ttestCfg.Packages = append(testCfg.Packages, pkg)\n\t\t\t\t}\n\t\t\t}\n\t\t\ttestCfg.EnvsToEmbed = i.testEnvVarsToEmbed(p.Args, p.Env)\n\n\t\t\tinfragen.Process(gg, pd.appDesc)\n\t\t\treturn apigen.Process(apigen.Params{\n\t\t\t\tGen:             gg,\n\t\t\t\tDesc:            pd.appDesc,\n\t\t\t\tMainModule:      pd.mainModule,\n\t\t\t\tRuntimeModule:   pd.runtimeModule,\n\t\t\t\tCompilerVersion: p.Compile.EncoreVersion.GetOrElse(fmt.Sprintf(\"EncoreCLI/%s\", version.Version)),\n\t\t\t\tAppRevision:     p.Compile.Build.Revision,\n\t\t\t\tAppUncommitted:  p.Compile.Build.UncommittedChanges,\n\t\t\t\tTest:            option.Some(testCfg),\n\t\t\t})\n\t\t})\n\n\t\tspec := build.GenerateTestSpec(ctx, &build.GenerateTestSpecConfig{\n\t\t\tConfig: build.Config{\n\t\t\t\tCtx:          pd.pc,\n\t\t\t\tOverlays:     gg.Overlays(),\n\t\t\t\tKeepOutput:   p.Compile.Build.KeepOutput,\n\t\t\t\tEnv:          p.Env,\n\t\t\t\tStaticConfig: staticConfig,\n\t\t\t},\n\t\t\tArgs: p.Args,\n\t\t})\n\n\t\tif pd.pc.Errs.Len() > 0 {\n\t\t\treturn nil, pd.pc.Errs.AsError()\n\t\t}\n\t\treturn spec, nil\n\t})\n}\n\n// testEnvVars takes a list of env vars and filters them down to the ones\n// that should be embedded within the test binary.\nfunc (i *BuilderImpl) testEnvVarsToEmbed(args, envs []string) map[string]string {\n\tif !slices.Contains(args, \"-c\") {\n\t\treturn nil\n\t}\n\n\ttoEmbed := make(map[string]string)\n\tfor _, e := range envs {\n\t\tif strings.HasPrefix(e, \"ENCORE_\") {\n\t\t\tif key, value, ok := strings.Cut(e, \"=\"); ok {\n\t\t\t\ttoEmbed[key] = value\n\t\t\t}\n\t\t}\n\t}\n\treturn toEmbed\n}\n\ntype configResult struct {\n\tconfigs map[string]string\n\tfiles   fs.FS\n}\n\nfunc computeConfigs(errs *perr.List, desc *app.Desc, mainModule *pkginfo.Module, cueMeta *cueutil.Meta) configResult {\n\tfiles := pickupConfigFiles(errs, mainModule)\n\n\t// TODO this is technically different from the \"app root\"\n\t// but it's close enough for now.\n\tappRoot := mainModule.RootDir.ToIO()\n\n\t// TODO this is a hack until we have proper resource usage tracking\n\tserviceUsesConfig := make(map[string]resource.Resource, len(desc.Services))\n\tfor _, b := range desc.Parse.AllBinds() {\n\t\tr := desc.Parse.ResourceForBind(b)\n\t\tif r.Kind() == resource.ConfigLoad {\n\t\t\tif svc, ok := desc.ServiceForPath(b.Package().FSPath); ok {\n\t\t\t\tserviceUsesConfig[svc.Name] = r\n\t\t\t}\n\t\t}\n\t}\n\n\tconfigs := make(map[string]string, len(desc.Services))\n\tfor _, svc := range desc.Services {\n\t\tresourceNode, ok := serviceUsesConfig[svc.Name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\trel, err := filepath.Rel(appRoot, svc.FSRoot.ToIO())\n\t\tif err != nil {\n\t\t\terrs.AddStdNode(err, resourceNode)\n\t\t\tcontinue\n\t\t}\n\t\t// Convert the path since io/fs operates on forward slashes.\n\t\trel = filepath.ToSlash(rel)\n\t\tcfg, err := cueutil.LoadFromFS(files, rel, cueMeta)\n\t\tif err != nil {\n\t\t\terrs.AddStdNode(err, resourceNode)\n\t\t\tcontinue\n\t\t}\n\t\tcfgData, err := cfg.MarshalJSON()\n\t\tif err != nil {\n\t\t\terrs.AddStdNode(err, resourceNode)\n\t\t\tcontinue\n\t\t}\n\n\t\tconfigs[svc.Name] = string(cfgData)\n\t}\n\treturn configResult{configs, files}\n}\n\nfunc pickupConfigFiles(errs *perr.List, mainModule *pkginfo.Module) fs.FS {\n\tinCueModFolder := func(path string, info fs.DirEntry) bool {\n\t\t// If it's not a directory, get the parent directory\n\t\tif !info.IsDir() {\n\t\t\tpath = filepath.Dir(path)\n\t\t}\n\n\t\tfor range 30 {\n\t\t\tbase := filepath.Base(path)\n\t\t\tif strings.ToLower(base) == \"cue.mod\" {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tparent := filepath.Dir(path)\n\t\t\tif parent == path {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpath = parent\n\t\t}\n\t\treturn false\n\t}\n\n\t// Create a virtual filesystem for the config files\n\tconfigFiles, err := vfs.FromDir(mainModule.RootDir.ToIO(), func(path string, info fs.DirEntry) bool {\n\t\treturn filepath.Ext(path) == \".cue\" || inCueModFolder(path, info)\n\t})\n\n\tif err != nil {\n\t\terrs.AssertStd(fmt.Errorf(\"unable to package configuration files: %w\", err))\n\t}\n\treturn configFiles\n}\n\nfunc (i *BuilderImpl) GenUserFacing(ctx context.Context, p builder.GenUserFacingParams) error {\n\treturn etrace.Sync1(ctx, \"\", \"v2builder.GenUserFacing\", func(ctx context.Context) (err error) {\n\t\tdefer func() {\n\t\t\terr, _ = perr.CatchBailoutAndPanic(err, recover())\n\t\t}()\n\n\t\tpd := p.Parse.Data.(*parseData)\n\t\terrs := pd.pc.Errs\n\t\tgg := codegen.New(pd.pc, pd.traceNodes)\n\t\tcueGen := cuegen.NewGenerator(pd.appDesc)\n\n\t\tvar buf bytes.Buffer\n\t\tfor _, svc := range pd.appDesc.Services {\n\t\t\t// Generate the user-facing Go code.\n\t\t\t{\n\t\t\t\t// Service structs are not needed if there is no implementation to be generated\n\t\t\t\tsvcStruct := option.None[*codegen.VarDecl]()\n\n\t\t\t\tbuf.Reset()\n\t\t\t\tif f, ok := userfacinggen.Gen(gg, svc, svcStruct).Get(); ok {\n\t\t\t\t\tif err := f.Render(&buf); err != nil {\n\t\t\t\t\t\terrs.Addf(token.NoPos, \"unable to render userfacing go code: %v\", err)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ti.writeOrDeleteFile(errs, buf.Bytes(), svc.FSRoot.Join(\"encore.gen.go\"))\n\t\t\t}\n\n\t\t\t// Generate the user-facing CUE code.\n\t\t\t{\n\t\t\t\tdata, err := cueGen.UserFacing(svc)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs.AddStd(err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdst := svc.FSRoot.Join(\"encore.gen.cue\")\n\t\t\t\ti.writeOrDeleteFile(errs, data, dst)\n\t\t\t}\n\t\t}\n\n\t\tif errs.Len() > 0 {\n\t\t\treturn errs.AsError()\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// writeOrDeleteFile writes the given data to dst. If data is empty, it will\n// instead delete the file at dst.\nfunc (i *BuilderImpl) writeOrDeleteFile(errs *perr.List, data []byte, dst paths.FS) {\n\tif len(data) == 0 {\n\t\t// No need for any generated code. Try to remove the existing file\n\t\t// if it's there as it's no longer needed.\n\t\t_ = os.Remove(dst.ToIO())\n\t} else {\n\t\tif err := os.WriteFile(dst.ToIO(), data, 0644); err != nil {\n\t\t\terrs.AddStd(err)\n\t\t}\n\t}\n}\n"
  }
]